From 95fc625d6052386233f7cc339c6317a1945a24ad Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 3 Mar 2026 14:00:38 -0800 Subject: [PATCH 1/9] Disable recursion for non-IN classes Force recursion off, and set allow-recursion/allow-recursion-on ACLs to none, for views with a class other than IN. Log a configuration warning if recursion is explicitly enabled for a non-IN view. This addresses YWH-PGM40640-74 and YWH-PGM40640-75 by preventing any attempt at recursive processing in a class-CHAOS view, ensuring that server addresses used for recursive queries and received in recursive responses are of the expected format. Fixes: isc-projects/bind9#5780 Fixes: isc-projects/bind9#5781 (cherry picked from commit 7becff1a14684a68208c92b3b0315c045c05ad75) --- bin/named/server.c | 41 +++++++-------------------- bin/tests/system/allow_query/tests.sh | 2 +- bin/tests/system/checkconf/tests.sh | 1 + bin/tests/system/resolver/tests.sh | 8 ++++-- lib/isccfg/check.c | 22 ++++++++++++-- 5 files changed, 37 insertions(+), 37 deletions(-) diff --git a/bin/named/server.c b/bin/named/server.c index df9acea28f..5d0bbfaa30 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -4428,7 +4428,8 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, obj = NULL; result = named_config_get(maps, "recursion", &obj); INSIST(result == ISC_R_SUCCESS); - view->recursion = cfg_obj_asboolean(obj); + view->recursion = (view->rdclass == dns_rdataclass_in && + cfg_obj_asboolean(obj)); if (named_g_maxcachesize != 0) { /* @@ -5144,35 +5145,15 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, } /* - * We have default hints for class IN if we need them. + * We have default root hints for class IN if we need them. + * Each view gets its own rootdb so a priming response only + * writes into that view's copy. Other classes don't support + * recursion and don't need hints. */ if (view->rdclass == dns_rdataclass_in && view->hints == NULL) { dns_view_sethints(view, named_g_server->in_roothints); } - /* - * If we still have no hints, this is a non-IN view with no - * "hints zone" configured. Issue a warning, except if this - * is a root server. Root servers never need to consult - * their hints, so it's no point requiring users to configure - * them. - */ - if (view->hints == NULL) { - dns_zone_t *rootzone = NULL; - (void)dns_view_findzone(view, dns_rootname, DNS_ZTFIND_EXACT, - &rootzone); - if (rootzone != NULL) { - dns_zone_detach(&rootzone); - need_hints = false; - } - if (need_hints) { - isc_log_write(named_g_lctx, NAMED_LOGCATEGORY_GENERAL, - NAMED_LOGMODULE_SERVER, ISC_LOG_WARNING, - "no root hints for view '%s'", - view->name); - } - } - /* * Configure the view's transports (DoT/DoH) */ @@ -5408,14 +5389,14 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, "allow-proxy-on", NULL, actx, named_g_mctx, &view->proxyonacl)); - if (strcmp(view->name, "_bind") != 0 && - view->rdclass != dns_rdataclass_chaos) - { - /* named.conf only */ + if (view->rdclass != dns_rdataclass_in) { + view->recursion = false; + dns_acl_none(named_g_mctx, &view->recursionacl); + dns_acl_none(named_g_mctx, &view->recursiononacl); + } else { CHECK(configure_view_acl(vconfig, config, NULL, "allow-recursion", NULL, actx, named_g_mctx, &view->recursionacl)); - /* named.conf only */ CHECK(configure_view_acl(vconfig, config, NULL, "allow-recursion-on", NULL, actx, named_g_mctx, &view->recursiononacl)); diff --git a/bin/tests/system/allow_query/tests.sh b/bin/tests/system/allow_query/tests.sh index b4c5aae555..be609a9f57 100644 --- a/bin/tests/system/allow_query/tests.sh +++ b/bin/tests/system/allow_query/tests.sh @@ -703,7 +703,7 @@ $DIG -p ${PORT} @10.53.1.2 d.normal.example a >dig.out.ns3.4.$n || ret=1 grep 'recursion requested but not available' dig.out.ns3.4.$n >/dev/null || ret=1 grep 'status: REFUSED' dig.out.ns3.4.$n >/dev/null || ret=1 grep 'EDE: 18 (Prohibited)' dig.out.ns3.4.$n >/dev/null || ret=1 -nextpart ns3/named.run | grep 'allow-recursion-on did not match' >/dev/null || ret=1 +nextpart ns3/named.run | grep 'allow-query-cache-on did not match' >/dev/null || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) diff --git a/bin/tests/system/checkconf/tests.sh b/bin/tests/system/checkconf/tests.sh index 3d4fd83cf4..2d7bb308c5 100644 --- a/bin/tests/system/checkconf/tests.sh +++ b/bin/tests/system/checkconf/tests.sh @@ -545,6 +545,7 @@ $CHECKCONF -l good.conf \ | grep -v "is not implemented" \ | grep -v "is not recommended" \ | grep -v "no longer exists" \ + | grep -v "recursion will be disabled" \ | grep -v "is obsolete" >checkconf.out$n || ret=1 diff good.zonelist checkconf.out$n >diff.out$n || ret=1 if [ $ret -ne 0 ]; then diff --git a/bin/tests/system/resolver/tests.sh b/bin/tests/system/resolver/tests.sh index 485d9d500e..1eec877a5a 100755 --- a/bin/tests/system/resolver/tests.sh +++ b/bin/tests/system/resolver/tests.sh @@ -782,10 +782,12 @@ if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) n=$((n + 1)) -echo_i "checking NXDOMAIN is returned when querying non existing domain in CH class ($n)" +echo_i "checking REFUSED is returned when querying non existing domain in CH class ($n)" ret=0 -dig_with_opts @10.53.0.1 id.hostname txt ch >dig.ns1.out.${n} || ret=1 -grep "status: NXDOMAIN" dig.ns1.out.${n} >/dev/null || ret=1 +dig_with_opts @10.53.0.1 hostname.chaostest txt ch >dig.ns1.out.1.${n} || ret=1 +grep "status: NOERROR" dig.ns1.out.1.${n} >/dev/null || ret=1 +dig_with_opts @10.53.0.1 id.hostname txt ch >dig.ns1.out.2.${n} || ret=1 +grep "status: REFUSED" dig.ns1.out.2.${n} >/dev/null || ret=1 if [ $ret != 0 ]; then echo_i "failed"; fi status=$((status + ret)) diff --git a/lib/isccfg/check.c b/lib/isccfg/check.c index 35be50fd60..e3e27590d0 100644 --- a/lib/isccfg/check.c +++ b/lib/isccfg/check.c @@ -3041,13 +3041,17 @@ check_mirror_zone_notify(const cfg_obj_t *zoptions, const char *znamestr, */ static bool check_recursion(const cfg_obj_t *config, const cfg_obj_t *voptions, - const cfg_obj_t *goptions, isc_log_t *logctx, - cfg_aclconfctx_t *actx, isc_mem_t *mctx) { + dns_rdataclass_t vclass, const cfg_obj_t *goptions, + isc_log_t *logctx, cfg_aclconfctx_t *actx, isc_mem_t *mctx) { dns_acl_t *acl = NULL; const cfg_obj_t *obj; isc_result_t result; bool retval = true; + if (vclass != dns_rdataclass_in) { + return false; + } + /* * Check the "recursion" option first. */ @@ -3905,7 +3909,8 @@ check_zoneconf(const cfg_obj_t *zconfig, const cfg_obj_t *voptions, * contradicts the purpose of the former. */ if (ztype == CFG_ZONE_MIRROR && - !check_recursion(config, voptions, goptions, logctx, actx, mctx)) + !check_recursion(config, voptions, zclass, goptions, logctx, actx, + mctx)) { cfg_obj_log(zoptions, logctx, ISC_LOG_ERROR, "zone '%s': mirror zones cannot be used if " @@ -5719,6 +5724,17 @@ check_viewconf(const cfg_obj_t *config, const cfg_obj_t *voptions, cfg_aclconfctx_create(mctx, &actx); + if (vclass != dns_rdataclass_in) { + if (check_recursion(config, voptions, dns_rdataclass_in, + options, logctx, actx, mctx)) + { + cfg_obj_log(opts, logctx, ISC_LOG_WARNING, + "recursion will be disabled for " + "non-IN view '%s'", + viewname); + } + } + if (voptions != NULL) { (void)cfg_map_get(voptions, "zone", &zones); } else { From 134706912f273c04a2e785e4a03b1fa8fa278f77 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Wed, 4 Mar 2026 13:24:52 -0800 Subject: [PATCH 2/9] Disable UPDATE and NOTIFY for non-IN classes Return NOTIMP for UPDATE and NOTIFY requests received for views with a class other than IN. Only QUERY is now supported for non-IN views such as CHAOS. When running dns dns_rdata_tostruct() with types that are only defined for class IN, ensure that the class is correct before proceeding. Add an assertion that any zone being updated is of class IN. (Note that previously, a DLZ zone could have its class value set incorrectly to NONE; this has been fixed.) This addresses YWH-PGM40640-70 and YWH-PGM40640-73 (as well as any similar problems that might have occurred in the future) by minimizing the code paths that can be reached by rdata classes other than IN, so it is safe for the implementation to assume that rdatatypes that are only defined for class IN, such as SVCB or WKS, have been parsed and validated, and not accepted as unknown/opaque data. Fixes: isc-projects/bind9#5777 Fixes: isc-projects/bind9#5779 (cherry picked from commit a6d8e330ed6cf0021bff3f00aa1dc7a296f5aec0) --- bin/named/server.c | 2 ++ lib/dns/adb.c | 5 +++-- lib/ns/client.c | 8 ++++++++ lib/ns/update.c | 43 ++++++++++++++++++++++++------------------- 4 files changed, 37 insertions(+), 21 deletions(-) diff --git a/bin/named/server.c b/bin/named/server.c index 5d0bbfaa30..63bb311d3a 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -1917,10 +1917,12 @@ dlzconfigure_callback(dns_view_t *view, dns_dlzdb_t *dlzdb, dns_zone_t *zone) { dns_rdataclass_t zclass = view->rdclass; isc_result_t result; + dns_zone_setclass(zone, zclass); result = dns_zonemgr_managezone(named_g_server->zonemgr, zone); if (result != ISC_R_SUCCESS) { return result; } + dns_zone_setstats(zone, named_g_server->zonestats); return named_zone_configure_writeable_dlz(dlzdb, zone, zclass, origin); diff --git a/lib/dns/adb.c b/lib/dns/adb.c index 1514fb46b9..7bfafff6d4 100644 --- a/lib/dns/adb.c +++ b/lib/dns/adb.c @@ -567,6 +567,9 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset, rdtype = rdataset->type; + REQUIRE(rdataset->rdclass == dns_rdataclass_in); + REQUIRE(rdtype == dns_rdatatype_a || rdtype == dns_rdatatype_aaaa); + switch (rdataset->trust) { case dns_trust_glue: case dns_trust_additional: @@ -579,8 +582,6 @@ import_rdataset(dns_adbname_t *adbname, dns_rdataset_t *rdataset, rdataset->ttl = ttlclamp(rdataset->ttl); } - REQUIRE(rdtype == dns_rdatatype_a || rdtype == dns_rdatatype_aaaa); - for (result = dns_rdataset_first(rdataset); result == ISC_R_SUCCESS; result = dns_rdataset_next(rdataset)) { diff --git a/lib/ns/client.c b/lib/ns/client.c index 9bae43b380..32f0240180 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -2413,6 +2413,10 @@ ns_client_request_continue(void *arg) { break; case dns_opcode_update: CTRACE("update"); + if (client->view->rdclass != dns_rdataclass_in) { + ns_client_error(client, DNS_R_NOTIMP); + break; + } #ifdef HAVE_DNSTAP dns_dt_send(client->view, DNS_DTTYPE_UQ, &client->peeraddr, &client->destsockaddr, transport_type, NULL, @@ -2423,6 +2427,10 @@ ns_client_request_continue(void *arg) { break; case dns_opcode_notify: CTRACE("notify"); + if (client->view->rdclass != dns_rdataclass_in) { + ns_client_error(client, DNS_R_NOTIMP); + break; + } ns_client_settimeout(client, 60); ns_notify_start(client, client->handle); break; diff --git a/lib/ns/update.c b/lib/ns/update.c index 031efef0a1..dbd5320367 100644 --- a/lib/ns/update.c +++ b/lib/ns/update.c @@ -997,7 +997,9 @@ ssu_checkrr(void *data, rr_t *rr) { RUNTIME_CHECK(result == ISC_R_SUCCESS); target = &ptr.ptr; } - if (rr->rdata.type == dns_rdatatype_srv) { + if (rr->rdata.rdclass == dns_rdataclass_in && + rr->rdata.type == dns_rdatatype_srv) + { result = dns_rdata_tostruct(&rr->rdata, &srv, NULL); RUNTIME_CHECK(result == ISC_R_SUCCESS); target = &srv.target; @@ -1352,7 +1354,10 @@ replaces_p(dns_rdata_t *update_rr, dns_rdata_t *db_rr) { return true; } } - if (db_rr->type == dns_rdatatype_wks) { + + if (db_rr->rdclass == dns_rdataclass_in && + db_rr->type == dns_rdatatype_wks) + { /* * Compare the address and protocol fields only. These * form the first five bytes of the RR data. Do a @@ -1495,8 +1500,7 @@ cleanup: * 'rdata', and 'ttl', respectively. */ static void -get_current_rr(dns_message_t *msg, dns_section_t section, - dns_rdataclass_t zoneclass, dns_name_t **name, +get_current_rr(dns_message_t *msg, dns_section_t section, dns_name_t **name, dns_rdata_t *rdata, dns_rdatatype_t *covers, dns_ttl_t *ttl, dns_rdataclass_t *update_class) { dns_rdataset_t *rdataset; @@ -1512,7 +1516,7 @@ get_current_rr(dns_message_t *msg, dns_section_t section, dns_rdataset_current(rdataset, rdata); INSIST(dns_rdataset_next(rdataset) == ISC_R_NOMORE); *update_class = rdata->rdclass; - rdata->rdclass = zoneclass; + rdata->rdclass = dns_rdataclass_in; } /*% @@ -1612,7 +1616,6 @@ send_update(ns_client_t *client, dns_zone_t *zone) { dns_message_t *request = client->message; isc_mem_t *mctx = client->manager->mctx; dns_aclenv_t *env = client->manager->aclenv; - dns_rdataclass_t zoneclass; dns_rdatatype_t covers; dns_name_t *zonename = NULL; unsigned int *maxbytype = NULL; @@ -1624,11 +1627,13 @@ send_update(ns_client_t *client, dns_zone_t *zone) { CHECK(dns_zone_getdb(zone, &db)); zonename = dns_db_origin(db); - zoneclass = dns_db_class(db); dns_zone_getssutable(zone, &ssutable); options = dns_zone_getoptions(zone); dns_db_currentversion(db, &ver); + /* Updates are only supported for class IN. */ + INSIST(dns_zone_getclass(zone) == dns_rdataclass_in); + /* * Update message processing can leak record existence information * so check that we are allowed to query this zone. Additionally, @@ -1678,13 +1683,13 @@ send_update(ns_client_t *client, dns_zone_t *zone) { INSIST(ssutable == NULL || update < maxbytypelen); - get_current_rr(request, DNS_SECTION_UPDATE, zoneclass, &name, - &rdata, &covers, &ttl, &update_class); + get_current_rr(request, DNS_SECTION_UPDATE, &name, &rdata, + &covers, &ttl, &update_class); if (!dns_name_issubdomain(name, zonename)) { FAILC(DNS_R_NOTZONE, "update RR is outside zone"); } - if (update_class == zoneclass) { + if (update_class == dns_rdataclass_in) { /* * Check for meta-RRs. The RFC2136 pseudocode says * check for ANY|AXFR|MAILA|MAILB, but the text adds @@ -1698,6 +1703,7 @@ send_update(ns_client_t *client, dns_zone_t *zone) { CHECK(DNS_R_REFUSED); } if ((options & DNS_ZONEOPT_CHECKSVCB) != 0 && + rdata.rdclass == dns_rdataclass_in && rdata.type == dns_rdatatype_svcb) { result = dns_rdata_checksvcb(name, &rdata); @@ -1785,7 +1791,6 @@ send_update(ns_client_t *client, dns_zone_t *zone) { } if (update_class == dns_rdataclass_any && - zoneclass == dns_rdataclass_in && (rdata.type == dns_rdatatype_ptr || rdata.type == dns_rdatatype_srv)) { @@ -2717,7 +2722,6 @@ update_action(void *arg) { isc_mem_t *mctx = client->manager->mctx; dns_rdatatype_t covers; dns_message_t *request = client->message; - dns_rdataclass_t zoneclass; dns_name_t *zonename = NULL; dns_fixedname_t tmpnamefixed; dns_name_t *tmpname = NULL; @@ -2734,9 +2738,10 @@ update_action(void *arg) { CHECK(dns_zone_getdb(zone, &db)); zonename = dns_db_origin(db); - zoneclass = dns_db_class(db); options = dns_zone_getoptions(zone); + INSIST(dns_zone_getclass(zone) == dns_rdataclass_in); + is_inline = (!dns_zone_israw(zone) && dns_zone_issecure(zone)); is_maintain = ((dns_zone_getkeyopts(zone) & DNS_ZONEKEY_MAINTAIN) != 0); is_signing = is_inline || (!is_inline && is_maintain); @@ -2761,8 +2766,8 @@ update_action(void *arg) { dns_rdataclass_t update_class; bool flag; - get_current_rr(request, DNS_SECTION_PREREQUISITE, zoneclass, - &name, &rdata, &covers, &ttl, &update_class); + get_current_rr(request, DNS_SECTION_PREREQUISITE, &name, &rdata, + &covers, &ttl, &update_class); if (ttl != 0) { PREREQFAILC(DNS_R_FORMERR, @@ -2825,7 +2830,7 @@ update_action(void *arg) { "prerequisite not satisfied"); } } - } else if (update_class == zoneclass) { + } else if (update_class == dns_rdataclass_in) { /* "temp += rr;" */ result = temp_append(&temp, name, &rdata); if (result != ISC_R_SUCCESS) { @@ -2887,10 +2892,10 @@ update_action(void *arg) { INSIST(ssutable == NULL || update < maxbytypelen); - get_current_rr(request, DNS_SECTION_UPDATE, zoneclass, &name, - &rdata, &covers, &ttl, &update_class); + get_current_rr(request, DNS_SECTION_UPDATE, &name, &rdata, + &covers, &ttl, &update_class); - if (update_class == zoneclass) { + if (update_class == dns_rdataclass_in) { /* * RFC1123 doesn't allow MF and MD in master files. */ From 39a4ad2330fdd4b95e919c398f00694183e6d79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Sur=C3=BD?= Date: Wed, 4 Mar 2026 10:46:58 +0100 Subject: [PATCH 3/9] Validate DNS message CLASS early in request processing Reject requests with unsupported or misused CLASS values before further processing. Only IN, CH, HS, RESERVED0 (for DNS Cookies), ANY (for TKEY negotiation), and NONE (for DNS UPDATE) are accepted; all other classes return NOTIMP. Misuse of NONE or ANY outside their allowed contexts returns FORMERR. This adds further protection against bugs of the same general class as YWH-PGM40640-70 and YWH-PGM40640-73. (cherry picked from commit 0a687451505037e9f9a850c9cb113aed4995b03f) --- bin/tests/system/unknown/tests.sh | 17 ++++++---- lib/ns/client.c | 55 ++++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/bin/tests/system/unknown/tests.sh b/bin/tests/system/unknown/tests.sh index eb61f21f28..cbc2943f17 100644 --- a/bin/tests/system/unknown/tests.sh +++ b/bin/tests/system/unknown/tests.sh @@ -25,6 +25,11 @@ dig_cmd() { "$DIG" $DIGOPTS "$@" | grep -v '^;' } +dig_full() { + # shellcheck disable=SC2086 + "$DIG" $DIGOPTS "$@" +} + n=$((n + 1)) echo_i "querying for various representations of an IN A record ($n)" for i in 1 2 3 4 5 6 7 8 9 10 11 12; do @@ -81,8 +86,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TYPE1 record ($n)" for i in 1 2; do ret=0 - dig_cmd +short @10.53.0.1 a$i.example a class10 >dig.out.$i.test$n - echo '\# 4 0A000001' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 a$i.example a class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi @@ -93,8 +98,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TXT record ($n)" for i in 1 2 3 4; do ret=0 - dig_cmd +short @10.53.0.1 txt$i.example txt class10 >dig.out.$i.test$n - echo '"hello"' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 txt$i.example txt class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi @@ -105,8 +110,8 @@ n=$((n + 1)) echo_i "querying for various representations of a CLASS10 TYPE123 record ($n)" for i in 1 2; do ret=0 - dig_cmd +short @10.53.0.1 unk$i.example type123 class10 >dig.out.$i.test$n - echo '\# 1 00' | diff - dig.out.$i.test$n || ret=1 + dig_full @10.53.0.1 unk$i.example type123 class10 >dig.out.$i.test$n + grep -q "NOTIMP" dig.out.$i.test$n || ret=1 if [ $ret != 0 ]; then echo_i "#$i failed" fi diff --git a/lib/ns/client.c b/lib/ns/client.c index 32f0240180..c10b122629 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -2041,7 +2042,9 @@ ns_client_request(isc_nmhandle_t *handle, isc_result_t eresult, } } - if (client->message->rdclass == 0) { + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + switch (client->message->rdclass) { + case dns_rdataclass_reserved0: if ((client->attributes & NS_CLIENTATTR_WANTCOOKIE) != 0 && client->message->opcode == dns_opcode_query && client->message->counts[DNS_SECTION_QUESTION] == 0U) @@ -2060,12 +2063,46 @@ ns_client_request(isc_nmhandle_t *handle, isc_result_t eresult, return; } + ns_client_dumpmessage(client, + "message class could not be determined"); + ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_FORMERR); + return; + case dns_rdataclass_in: + break; + case dns_rdataclass_chaos: + break; + case dns_rdataclass_hs: + break; + case dns_rdataclass_none: + if (client->message->opcode != dns_opcode_update) { + ns_client_dumpmessage(client, + "message class NONE can be only " + "used in DNS updates"); + ns_client_error(client, DNS_R_FORMERR); + return; + } + break; + case dns_rdataclass_any: + /* + * Required for TKEY negotiation. + */ + if (client->message->tkey == 0) { + ns_client_dumpmessage(client, + "message class ANY can be only " + "used for TKEY negotiation"); + ns_client_error(client, DNS_R_FORMERR); + return; + } + break; + default: + dns_rdataclass_format(client->message->rdclass, classbuf, + sizeof(classbuf)); + ns_client_dumpmessage(client, NULL); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), - "message class could not be determined"); - ns_client_dumpmessage(client, "message class could not be " - "determined"); - ns_client_error(client, notimp ? DNS_R_NOTIMP : DNS_R_FORMERR); + "invalid message class: %s", classbuf); + + ns_client_error(client, DNS_R_NOTIMP); return; } @@ -2149,9 +2186,6 @@ ns_client_request_continue(void *arg) { "SIG(0) checks quota reached"); if (can_log_sigchecks_quota()) { - ns_client_log(client, NS_LOGCATEGORY_CLIENT, - NS_LOGMODULE_CLIENT, ISC_LOG_INFO, - "SIG(0) checks quota reached"); ns_client_dumpmessage( client, "SIG(0) checks quota reached"); } @@ -2161,12 +2195,11 @@ ns_client_request_continue(void *arg) { dns_rdataclass_format(client->message->rdclass, classname, sizeof(classname)); + ns_client_dumpmessage(client, NULL); ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_CLIENT, ISC_LOG_DEBUG(1), "no matching view in class '%s'", classname); - ns_client_dumpmessage(client, - "no matching view in class"); } dns_ede_add(&client->edectx, DNS_EDE_PROHIBITED, NULL); @@ -2804,7 +2837,7 @@ ns_client_dumpmessage(ns_client_t *client, const char *reason) { int len = 1024; isc_result_t result; - if (!isc_log_wouldlog(ns_lctx, ISC_LOG_DEBUG(1))) { + if (!isc_log_wouldlog(ns_lctx, ISC_LOG_DEBUG(1)) || reason == NULL) { return; } From a40325c6a27c728c27becc6dc91513b5b953177d Mon Sep 17 00:00:00 2001 From: Mark Andrews Date: Wed, 4 Mar 2026 10:00:56 +1100 Subject: [PATCH 4/9] Reject meta-classes in UPDATE and NOTIFY messages NOTIFY and UPDATE messages must specify a data class in the QUESTION/ZONE section. NONE and ANY are meta-classes and not appropriate here. Return FORMERR if either is used. Rejecting messages with a query class of NONE addresses YWH-PGM40640-72, YWH-PGM40640-82, and YWH-PGM40640-83. Rejecting messages with a query class of ANY addresses YWH-PGM40640-87, YWH-PGM40640-88, and YWH-PGM40640-117. Fixes: isc-projects/bind9#5778 Fixes: isc-projects/bind9#5782 Fixes: isc-projects/bind9#5783 Fixes: isc-projects/bind9#5797 Fixes: isc-projects/bind9#5798 Fixes: isc-projects/bind9#5853 (cherry picked from commit c66a1b1e1bfd6c79d7b9bc8d4a59e69f4faa1563) --- lib/dns/message.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/dns/message.c b/lib/dns/message.c index 19bb6a8530..dce9c2d65f 100644 --- a/lib/dns/message.c +++ b/lib/dns/message.c @@ -1073,6 +1073,17 @@ getquestions(isc_buffer_t *source, dns_message_t *msg, dns_decompress_t dctx, rdtype = isc_buffer_getuint16(source); rdclass = isc_buffer_getuint16(source); + /* + * Notify and update messages need to specify the data class. + */ + if ((msg->opcode == dns_opcode_update || + msg->opcode == dns_opcode_notify) && + (rdclass == dns_rdataclass_none || + rdclass == dns_rdataclass_any)) + { + DO_ERROR(DNS_R_FORMERR); + } + /* * If this class is different than the one we already read, * this is an error. From ec1404cc4c124b456409e1dbe9d16cdbe31c68c9 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 17 Mar 2026 13:24:43 -0700 Subject: [PATCH 5/9] Skip "deny-answer-address" for non-IN addresses Ensure that we don't attempt an ACL match for answer addresses when handling a class-CHAOS zone. This is an additional line of defense for YWH-PGM40640-74. (cherry picked from commit e62673c765b52307c800e86f0185fe52b573c145) --- lib/dns/resolver.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/dns/resolver.c b/lib/dns/resolver.c index 87f423a69b..4b70963a96 100644 --- a/lib/dns/resolver.c +++ b/lib/dns/resolver.c @@ -6967,6 +6967,13 @@ is_answeraddress_allowed(dns_view_t *view, dns_name_t *name, return true; } + /* + * deny-answer-address doesn't apply to non-IN classes. + */ + if (rdataset->rdclass != dns_rdataclass_in) { + return true; + } + /* * Otherwise, search the filter list for a match for each * address record. If a match is found, the address should be From 8b62c25306b6f3e31f237ba7799ea4e0f15c64ae Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 17 Mar 2026 13:45:11 -0700 Subject: [PATCH 6/9] Test CHAOS view recursion behavior Check that recursive and forward queries to views of type CHAOS are REFUSED, but that authoritative queries are answered correctly. (cherry picked from commit f33927cd3dd1195f3e70f5798ff7c384f265867e) --- bin/tests/system/checkconf/tests.sh | 11 ++++ .../checkconf/warn-chaos-recursion.conf | 12 +++++ bin/tests/system/class/ns1/chaos.db.in | 4 ++ bin/tests/system/class/ns1/named.conf.j2 | 31 +++++++++++ bin/tests/system/class/ns2/example.db.in | 6 +++ bin/tests/system/class/ns2/localhost.db.in | 6 +++ bin/tests/system/class/ns2/named.conf.j2 | 42 +++++++++++++++ bin/tests/system/class/ns3/named.conf.j2 | 28 ++++++++++ bin/tests/system/class/setup.sh | 19 +++++++ bin/tests/system/class/tests_class_chaos.py | 54 +++++++++++++++++++ bin/tests/system/isctest/check.py | 4 ++ 11 files changed, 217 insertions(+) create mode 100644 bin/tests/system/checkconf/warn-chaos-recursion.conf create mode 100644 bin/tests/system/class/ns1/chaos.db.in create mode 100644 bin/tests/system/class/ns1/named.conf.j2 create mode 100644 bin/tests/system/class/ns2/example.db.in create mode 100644 bin/tests/system/class/ns2/localhost.db.in create mode 100644 bin/tests/system/class/ns2/named.conf.j2 create mode 100644 bin/tests/system/class/ns3/named.conf.j2 create mode 100644 bin/tests/system/class/setup.sh create mode 100644 bin/tests/system/class/tests_class_chaos.py diff --git a/bin/tests/system/checkconf/tests.sh b/bin/tests/system/checkconf/tests.sh index 2d7bb308c5..52666671a5 100644 --- a/bin/tests/system/checkconf/tests.sh +++ b/bin/tests/system/checkconf/tests.sh @@ -806,5 +806,16 @@ if [ $ret != 0 ]; then fi status=$((status + ret)) +n=$((n + 1)) +echo_i "check 'recursion yes;' is warned and disabled in a non-IN view ($n)" +ret=0 +$CHECKCONF warn-chaos-recursion.conf >checkconf.out$n 2>&1 || ret=1 +grep -F "recursion will be disabled" checkconf.out$n >/dev/null || ret=1 +if [ $ret != 0 ]; then + echo_i "failed" + ret=1 +fi +status=$((status + ret)) + echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/checkconf/warn-chaos-recursion.conf b/bin/tests/system/checkconf/warn-chaos-recursion.conf new file mode 100644 index 0000000000..01965102a4 --- /dev/null +++ b/bin/tests/system/checkconf/warn-chaos-recursion.conf @@ -0,0 +1,12 @@ +options { + directory "."; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + zone "." { + type hint; + file "chaos.hints"; + }; +}; diff --git a/bin/tests/system/class/ns1/chaos.db.in b/bin/tests/system/class/ns1/chaos.db.in new file mode 100644 index 0000000000..43ca58ffa8 --- /dev/null +++ b/bin/tests/system/class/ns1/chaos.db.in @@ -0,0 +1,4 @@ +. CH NS ns.root. +ns.root. CH A ns.root. 1 +ns.root. CH AAAA \# 1 00 + diff --git a/bin/tests/system/class/ns1/named.conf.j2 b/bin/tests/system/class/ns1/named.conf.j2 new file mode 100644 index 0000000000..76f85fc6c9 --- /dev/null +++ b/bin/tests/system/class/ns1/named.conf.j2 @@ -0,0 +1,31 @@ +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + listen-on-v6 { none; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + zone "." { + type hint; + file "chaos.db"; + }; + zone "version.bind" { + type primary; + database "_builtin version"; + }; +}; diff --git a/bin/tests/system/class/ns2/example.db.in b/bin/tests/system/class/ns2/example.db.in new file mode 100644 index 0000000000..a658ddbd89 --- /dev/null +++ b/bin/tests/system/class/ns2/example.db.in @@ -0,0 +1,6 @@ +$TTL 300 +@ CH SOA ns.example. hostmaster.example. 1 3600 1200 604800 300 +@ CH NS ns.example. +ns CH TXT "ns" +a CH A target.example. 1 +target CH TXT "target" diff --git a/bin/tests/system/class/ns2/localhost.db.in b/bin/tests/system/class/ns2/localhost.db.in new file mode 100644 index 0000000000..baa5f74862 --- /dev/null +++ b/bin/tests/system/class/ns2/localhost.db.in @@ -0,0 +1,6 @@ +$ORIGIN 1.0.0.127.in-addr.arpa. +$TTL 300 +@ IN SOA ns hostmaster 1 3600 900 604800 300 +@ IN NS ns +ns IN A 127.0.0.1 +@ IN KX 10 target.example. diff --git a/bin/tests/system/class/ns2/named.conf.j2 b/bin/tests/system/class/ns2/named.conf.j2 new file mode 100644 index 0000000000..5618c15216 --- /dev/null +++ b/bin/tests/system/class/ns2/named.conf.j2 @@ -0,0 +1,42 @@ +options { + directory "."; + query-source address 10.53.0.2; + notify-source 10.53.0.2; + transfer-source 10.53.0.2; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.2; }; + listen-on-v6 { none; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.2 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view default { + match-clients { any; }; + recursion no; + dnssec-validation no; + zone "1.0.0.127.in-addr.arpa." { + type primary; + file "localhost.db"; + update-policy { + grant * tcp-self . ANY; + }; + }; +}; + +view chaos ch { + match-clients { any; }; + recursion no; + zone example { + type primary; + file "example.db"; + allow-update { any; }; + }; +}; diff --git a/bin/tests/system/class/ns3/named.conf.j2 b/bin/tests/system/class/ns3/named.conf.j2 new file mode 100644 index 0000000000..3016333aad --- /dev/null +++ b/bin/tests/system/class/ns3/named.conf.j2 @@ -0,0 +1,28 @@ +options { + directory "."; + query-source address 10.53.0.3; + notify-source 10.53.0.3; + transfer-source 10.53.0.3; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.3; }; + listen-on-v6 { none; }; +}; + +key rndc_key { + secret "1234abcd8765"; + algorithm @DEFAULT_HMAC@; +}; + +controls { + inet 10.53.0.3 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +view chaos ch { + match-clients { any; }; + recursion yes; + dnssec-validation no; + forward only; + forwarders port @PORT@ { 10.53.0.2; }; + deny-answer-addresses { 0.0.0.0/0; ::/0; }; +}; diff --git a/bin/tests/system/class/setup.sh b/bin/tests/system/class/setup.sh new file mode 100644 index 0000000000..c70a2f8290 --- /dev/null +++ b/bin/tests/system/class/setup.sh @@ -0,0 +1,19 @@ +#!/bin/sh -e + +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, you can obtain one at https://mozilla.org/MPL/2.0/. +# +# See the COPYRIGHT file distributed with this work for additional +# information regarding copyright ownership. + +# shellcheck source=conf.sh +. ../conf.sh + +cp ns1/chaos.db.in ns1/chaos.db +cp ns2/example.db.in ns2/example.db +cp ns2/localhost.db.in ns2/localhost.db diff --git a/bin/tests/system/class/tests_class_chaos.py b/bin/tests/system/class/tests_class_chaos.py new file mode 100644 index 0000000000..5b4fef9ae4 --- /dev/null +++ b/bin/tests/system/class/tests_class_chaos.py @@ -0,0 +1,54 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form 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.opcode +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + + +def test_chaos_recursion(): + msg = isctest.query.create("foo.example.", "TXT", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.1") + isctest.check.refused(res) + + +def test_chaos_auth(): + msg = isctest.query.create("a.example.", "A", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.noerror(res) + + +def test_chaos_forward(): + msg = isctest.query.create("a.example.", "A", qclass="CH") + res = isctest.query.udp(msg, "10.53.0.3") + isctest.check.refused(res) + + +def test_chaos_notify(): + msg = isctest.query.create("example.", "SOA", qclass="CH", rd=False, dnssec=False) + msg.set_opcode(dns.opcode.NOTIFY) + msg.flags = dns.opcode.to_flags(dns.opcode.NOTIFY) + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.notimp(res) + + +def test_query_class_none(): + msg = isctest.query.create("example.", "A", qclass="NONE") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.formerr(res) diff --git a/bin/tests/system/isctest/check.py b/bin/tests/system/isctest/check.py index 6dbf6bca26..9b4aab5ab6 100644 --- a/bin/tests/system/isctest/check.py +++ b/bin/tests/system/isctest/check.py @@ -46,6 +46,10 @@ def servfail(message: dns.message.Message) -> None: rcode(message, dns.rcode.SERVFAIL) +def formerr(message: dns.message.Message) -> None: + rcode(message, dns.rcode.FORMERR) + + def adflag(message: dns.message.Message) -> None: assert (message.flags & dns.flags.AD) != 0, str(message) From aecc27189f6288237a0559b6fad2dcd09f1fb1e2 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Tue, 17 Mar 2026 13:45:11 -0700 Subject: [PATCH 7/9] Test UPDATE behavior in CHAOS and other non-IN classes Send various UPDATE requests that are known to have caused crashes previously with deliberately misconfigured non-IN zones; confirm that UPDATE is not processed. (cherry picked from commit e2f7ba2a4b6e7e5dba2fb1a2c9b2f0323e9a88be) --- bin/named/server.c | 1 - bin/tests/system/class/ns2/localhost.db.in | 5 + bin/tests/system/class/tests_class_update.py | 96 ++++++++++++++++++++ 3 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 bin/tests/system/class/tests_class_update.py diff --git a/bin/named/server.c b/bin/named/server.c index 63bb311d3a..df4bb6ff4f 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -5392,7 +5392,6 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, &view->proxyonacl)); if (view->rdclass != dns_rdataclass_in) { - view->recursion = false; dns_acl_none(named_g_mctx, &view->recursionacl); dns_acl_none(named_g_mctx, &view->recursiononacl); } else { diff --git a/bin/tests/system/class/ns2/localhost.db.in b/bin/tests/system/class/ns2/localhost.db.in index baa5f74862..a50e5167a9 100644 --- a/bin/tests/system/class/ns2/localhost.db.in +++ b/bin/tests/system/class/ns2/localhost.db.in @@ -3,4 +3,9 @@ $TTL 300 @ IN SOA ns hostmaster 1 3600 900 604800 300 @ IN NS ns ns IN A 127.0.0.1 + @ IN KX 10 target.example. +@ IN PX 10 map822.example. mapx400.example. +@ IN NSAP 0x47000580ffff0000000001e133ffffff00016200 +@ IN NSAP-PTR target.example. +@ in EID \# 01 aa diff --git a/bin/tests/system/class/tests_class_update.py b/bin/tests/system/class/tests_class_update.py new file mode 100644 index 0000000000..e53bbc77ea --- /dev/null +++ b/bin/tests/system/class/tests_class_update.py @@ -0,0 +1,96 @@ +# Copyright (C) Internet Systems Consortium, Inc. ("ISC") +# +# SPDX-License-Identifier: MPL-2.0 +# +# This Source Code Form 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 socket +import struct + +from dns import rdataclass, rdatatype, update + +import pytest + +import isctest + +pytestmark = pytest.mark.extra_artifacts( + [ + "*/*.db", + ] +) + + +def encode_name(name: str) -> bytes: + out = b"" + for label in name.rstrip(".").split("."): + out += bytes([len(label)]) + label.encode("ascii") + return out + b"\x00" + + +@pytest.mark.parametrize( + "rdtype,rdclass,ttl,rdata", + [ + (rdatatype.SRV, rdataclass.NONE, 0, b"\x00"), + (rdatatype.KX, rdataclass.NONE, 0, b""), + (rdatatype.PX, rdataclass.NONE, 0, b""), + (rdatatype.NSAP, rdataclass.NONE, 0, b""), + (rdatatype.NSAP_PTR, rdataclass.NONE, 0, b""), + (31, rdataclass.NONE, 0, b""), # dnspython doesn't define type EID + ], +) +def test_class_invalid(rdtype, rdclass, ttl, rdata, named_port): + # these update messages are badly formatted, so we construct + # them manually instead of using dnspython. + + # opcode=UPDATE, 1 RRset in ZONE, 1 RRset in UPDATE + header = struct.pack("!HHHHHH", 0, 0x2800, 1, 0, 1, 0) + + # ZONE section: QNAME=, QTYPE=SOA, QCLASS=ANY + zone_q = encode_name("1.0.0.127.in-addr.arpa") + struct.pack("!HH", 6, 255) + + # UPDATE section RR: + update_rr = ( + encode_name("1.0.0.127.in-addr.arpa") + + struct.pack("!HHIH", rdtype, rdclass, ttl, len(rdata)) + + rdata + ) + + m = header + zone_q + update_rr + packet = struct.pack("!H", len(m)) + m + + with socket.create_connection( + ("10.53.0.2", named_port), source_address=("127.0.0.1", 0), timeout=2.0 + ) as s: + s.sendall(packet) + try: + rwire = s.recv(4096) + res = dns.message.from_wire(rwire) + isctest.check.formerr(res) + except Exception: # pylint: disable=broad-except + pass + + # check the server is answering + msg = isctest.query.create("1.0.0.127.in-addr.arpa", "SRV") + res = isctest.query.udp(msg, "10.53.0.2") + isctest.check.noerror(res) + isctest.check.rr_count_eq(res.answer, 0) + + +@pytest.mark.parametrize( + "rdtype,rdata", + [ + (rdatatype.SVCB, "\\# 02 0000"), + (rdatatype.WKS, "\\# 02 4142"), + (rdatatype.WKS, "\\# 02 4344"), + ], +) +def test_class_chaosupdate(rdtype, rdata): + up = update.UpdateMessage("example.", rdclass=rdataclass.CHAOS) + up.add("foo.example.", 300, rdtype, rdata) + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.notimp(res) From 14f755029d2922ac37be10e0112a52dd4841bbb8 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Mon, 9 Mar 2026 15:50:04 +1100 Subject: [PATCH 8/9] Test server behavior when sending various UPDATE requests Send update messages for zones with CLASS0, ANY and NONE. The class ANY UPDATE also attempts to delete a KX record in an existing IN class zone to trigger a REQUIRE. Test that the server is still running. (cherry picked from commit 1fa1e84d286d5a6d9d3b72ed1c2c29142f40c81d) --- bin/tests/system/class/tests_class_update.py | 45 +++++++++++++++++++- bin/tests/system/nsupdate/setup.sh | 1 + bin/tests/system/nsupdate/tests.sh | 20 +++------ bin/tests/system/packet.pl | 25 +++++++++-- 4 files changed, 71 insertions(+), 20 deletions(-) diff --git a/bin/tests/system/class/tests_class_update.py b/bin/tests/system/class/tests_class_update.py index e53bbc77ea..30e3ba6d2a 100644 --- a/bin/tests/system/class/tests_class_update.py +++ b/bin/tests/system/class/tests_class_update.py @@ -12,7 +12,7 @@ import socket import struct -from dns import rdataclass, rdatatype, update +from dns import message, rdataclass, rdatatype, update import pytest @@ -35,6 +35,7 @@ def encode_name(name: str) -> bytes: @pytest.mark.parametrize( "rdtype,rdclass,ttl,rdata", [ + (rdatatype.SRV, rdataclass.NONE, 0, b"\x00\x00\x00\x00\x00\x00\x01"), (rdatatype.SRV, rdataclass.NONE, 0, b"\x00"), (rdatatype.KX, rdataclass.NONE, 0, b""), (rdatatype.PX, rdataclass.NONE, 0, b""), @@ -69,7 +70,7 @@ def test_class_invalid(rdtype, rdclass, ttl, rdata, named_port): s.sendall(packet) try: rwire = s.recv(4096) - res = dns.message.from_wire(rwire) + res = message.from_wire(rwire) isctest.check.formerr(res) except Exception: # pylint: disable=broad-except pass @@ -94,3 +95,43 @@ def test_class_chaosupdate(rdtype, rdata): up.add("foo.example.", 300, rdtype, rdata) res = isctest.query.tcp(up, "10.53.0.2") isctest.check.notimp(res) + + +def test_class_undefined(ns2): + up = update.UpdateMessage(".", rdclass=257) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.notimp(res) + watcher.wait_for_line("invalid message class: CLASS257") + + +def test_class_zero(ns2): + up = update.UpdateMessage(".", rdclass=0) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message class could not be determined") + + +def test_class_any(ns2): + up = update.UpdateMessage(".", rdclass=rdataclass.ANY) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message parsing failed: FORMERR") + + +def test_class_none(ns2): + up = update.UpdateMessage(".", rdclass=rdataclass.NONE) + up.present(".", 0) + up.answer[0].rdclass = rdataclass.NONE + with ns2.watch_log_from_here() as watcher: + res = isctest.query.tcp(up, "10.53.0.2") + isctest.check.formerr(res) + watcher.wait_for_line("message parsing failed: FORMERR") diff --git a/bin/tests/system/nsupdate/setup.sh b/bin/tests/system/nsupdate/setup.sh index 299330773a..9d27b20a76 100644 --- a/bin/tests/system/nsupdate/setup.sh +++ b/bin/tests/system/nsupdate/setup.sh @@ -35,6 +35,7 @@ update.nil IN SOA ns1.example.nil. hostmaster.example.nil. ( 3600 ; minimum (1 hour) ) update.nil. NS ns1.update.nil. +update.nil. KX 0 . ns1.update.nil. A 10.53.0.2 ns2.update.nil. AAAA ::1 EOF diff --git a/bin/tests/system/nsupdate/tests.sh b/bin/tests/system/nsupdate/tests.sh index e7c6283569..862ebe38b3 100755 --- a/bin/tests/system/nsupdate/tests.sh +++ b/bin/tests/system/nsupdate/tests.sh @@ -340,8 +340,10 @@ grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 n=$((n + 1)) ret=0 echo_i "check that TYPE=0 update is handled ($n)" +nextpart ns1/named.run >/dev/null echo "a0e4280000010000000100000000060001c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 + | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp -b >/dev/null || ret=1 +wait_for_log 2 "message parsing failed: FORMERR" ns1/named.run || ret=1 $DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 [ $ret = 0 ] || { @@ -352,20 +354,10 @@ grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 n=$((n + 1)) ret=0 echo_i "check that TYPE=0 additional data is handled ($n)" +nextpart ns1/named.run >/dev/null echo "a0e4280000010000000000010000060001c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 -$DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 -grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 -[ $ret = 0 ] || { - echo_i "failed" - status=1 -} - -n=$((n + 1)) -ret=0 -echo_i "check that update to undefined class is handled ($n)" -echo "a0e4280000010001000000000000060101c00c000000fe000000000000" \ - | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp >/dev/null || ret=1 + | $PERL ../packet.pl -a 10.53.0.1 -p ${PORT} -t tcp -b >/dev/null || ret=1 +wait_for_log 2 "message parsing failed: FORMERR" ns1/named.run || ret=1 $DIG $DIGOPTS +tcp version.bind txt ch @10.53.0.1 >dig.out.ns1.$n || ret=1 grep "status: NOERROR" dig.out.ns1.$n >/dev/null || ret=1 [ $ret = 0 ] || { diff --git a/bin/tests/system/packet.pl b/bin/tests/system/packet.pl index 900a0c071e..afb9f4784d 100644 --- a/bin/tests/system/packet.pl +++ b/bin/tests/system/packet.pl @@ -40,6 +40,7 @@ # -p : specify port # -t : specify UDP or TCP # -r : send packet times +# -b: blocking io # -d: dump response packets # # If not specified, address defaults to 127.0.0.1, port to 53, protocol @@ -51,6 +52,8 @@ use strict; use Getopt::Std; use IO::File; use IO::Socket; +use Net::DNS; +use Net::DNS::Packet; sub usage { print ("Usage: packet.pl [-a address] [-d] [-p port] [-t (tcp|udp)] [-r ] [file]\n"); @@ -61,8 +64,6 @@ my $sock; my $proto; sub dumppacket { - use Net::DNS; - use Net::DNS::Packet; my $rin; my $rout; @@ -96,7 +97,7 @@ sub dumppacket { } my %options={}; -getopts("a:dp:t:r:", \%options); +getopts("a:bdp:t:r:", \%options); my $addr = "127.0.0.1"; $addr = $options{a} if defined $options{a}; @@ -111,6 +112,8 @@ usage if ($proto !~ /^(udp|tcp)$/); my $repeats = 1; $repeats = $options{r} if defined $options{r}; +my $blocking = defined $options{b} ? 1 : 0; + my $file = "STDIN"; if (@ARGV >= 1) { my $filename = shift @ARGV; @@ -132,8 +135,22 @@ my $len = length $data; my $output = unpack("H*", $data); print ("sending $repeats time(s): $output\n"); + +if (defined $options{d}) { + my $request; + if ($Net::DNS::VERSION > 0.68) { + $request = new Net::DNS::Packet(\$data, 0); + $@ and die $@; + } else { + my $err; + ($request, $err) = new Net::DNS::Packet(\$data, 0); + $err and die $err; + } + $request->print; +} + $sock = IO::Socket::INET->new(PeerAddr => $addr, PeerPort => $port, - Blocking => 0, + Blocking => $blocking, Proto => $proto,) or die "$!"; STDOUT->autoflush(1); From a2d74e7356aaf56cbab2d074b47c3d896f381ad3 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Mon, 23 Feb 2026 16:27:52 -0800 Subject: [PATCH 9/9] Make the RD flag optional in isctest.query() Add an 'rd' parameter (default True) to isctest.query.create() so that non-recursive queries can be sent with rd=False. (cherry picked from commit 12e511310024aac38ce223ee47b5108f06caf8f9) --- bin/tests/system/isctest/query.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bin/tests/system/isctest/query.py b/bin/tests/system/isctest/query.py index 97ebb4bc26..a7e862b7f6 100644 --- a/bin/tests/system/isctest/query.py +++ b/bin/tests/system/isctest/query.py @@ -136,6 +136,7 @@ def create( qtype, qclass=dns.rdataclass.IN, dnssec: bool = True, + rd: bool = True, cd: bool = False, ad: bool = True, ) -> dns.message.Message: @@ -143,7 +144,9 @@ def create( msg = dns.message.make_query( qname, qtype, qclass, use_edns=True, want_dnssec=dnssec ) - msg.flags = dns.flags.RD + msg.flags = 0 + if rd: + msg.flags = dns.flags.RD if ad: msg.flags |= dns.flags.AD if cd: