diff --git a/bin/named/server.c b/bin/named/server.c index 35b3ac9253..64e5e06166 100644 --- a/bin/named/server.c +++ b/bin/named/server.c @@ -4271,6 +4271,22 @@ configure_view(dns_view_t *view, dns_viewlist_t *viewlist, cfg_obj_t *config, } } + obj = NULL; + result = named_config_get(maps, "send-report-channel", &obj); + if (view->rad != NULL) { + dns_name_free(view->rad, view->mctx); + isc_mem_put(view->mctx, view->rad, sizeof(*view->rad)); + } + if (result == ISC_R_SUCCESS) { + str = cfg_obj_asstring(obj); + if (strcmp(str, ".") != 0 && strcmp(str, "") != 0) { + view->rad = isc_mem_get(mctx, sizeof(*view->rad)); + dns_name_init(view->rad, NULL); + CHECK(dns_name_fromstring(view->rad, str, dns_rootname, + 0, mctx)); + } + } + obj = NULL; result = named_config_get(maps, "dnssec-accept-expired", &obj); INSIST(result == ISC_R_SUCCESS); diff --git a/bin/tests/system/auth/clean.sh b/bin/tests/system/auth/clean.sh index 5fb37aca02..56a99a55c7 100644 --- a/bin/tests/system/auth/clean.sh +++ b/bin/tests/system/auth/clean.sh @@ -11,10 +11,11 @@ # See the COPYRIGHT file distributed with this work for additional # information regarding copyright ownership. +rm -f */named.conf rm -f */named.memstats rm -f */named.run -rm -f */named.conf +rm -f */named.run.prev rm -f dig.out.test* +rm -f ns*/managed-keys.bind* ns*/*mkeys* rm -f ns2/example.com.bk rm -f ns2/example.net.bk -rm -f ns*/managed-keys.bind* ns*/*mkeys* diff --git a/bin/tests/system/auth/ns1/named.conf.in b/bin/tests/system/auth/ns1/named.conf.in index 7fdc1a1524..027a08851d 100644 --- a/bin/tests/system/auth/ns1/named.conf.in +++ b/bin/tests/system/auth/ns1/named.conf.in @@ -22,6 +22,7 @@ options { recursion no; notify yes; dnssec-validation no; + send-report-channel "rad.example.net"; }; view main in { diff --git a/bin/tests/system/auth/tests.sh b/bin/tests/system/auth/tests.sh index 2499b46950..591adfd9c2 100644 --- a/bin/tests/system/auth/tests.sh +++ b/bin/tests/system/auth/tests.sh @@ -186,5 +186,23 @@ lines=$(wc -l dig.out.test$n +grep "; Report-Channel: rad.example.net" dig.out.test$n >/dev/null || ret=1 +[ $ret -eq 0 ] || echo_i "failed" +status=$((status + ret)) + +n=$((n + 1)) +echo_i "check that error report queries are logged and no Report-Channel option is present in the response ($n)" +ret=0 +nextpart ns1/named.run >/dev/null +$DIG $DIGOPTS @10.53.0.1 _er.0.example.1._er.rad.example.net TXT >dig.out.test$n +nextpart ns1/named.run | grep "dns-reporting-agent '_er.0.example.1._er.rad.example.net/IN'" >/dev/null || ret=1 +grep "; Report-Channel: rad.example.net" dig.out.test$n >/dev/null && ret=1 +[ $ret -eq 0 ] || echo_i "failed" +status=$((status + ret)) + echo_i "exit status: $status" [ $status -eq 0 ] || exit 1 diff --git a/bin/tests/system/checkconf/bad-rad.conf b/bin/tests/system/checkconf/bad-rad.conf new file mode 100644 index 0000000000..f311f2d4a0 --- /dev/null +++ b/bin/tests/system/checkconf/bad-rad.conf @@ -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. + */ + +options { + /* invalid domain name */ + send-report-channel example..com; +}; diff --git a/bin/tests/system/checkconf/good-rad-view.conf b/bin/tests/system/checkconf/good-rad-view.conf new file mode 100644 index 0000000000..da7bd9100e --- /dev/null +++ b/bin/tests/system/checkconf/good-rad-view.conf @@ -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. + */ + +view view { + send-report-channel example.com; +}; diff --git a/bin/tests/system/checkconf/good-rad.conf b/bin/tests/system/checkconf/good-rad.conf new file mode 100644 index 0000000000..6d56da5b8e --- /dev/null +++ b/bin/tests/system/checkconf/good-rad.conf @@ -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. + */ + +options { + send-report-channel example.com; +}; + +zone example.com { + type primary; + file "example.db"; +}; diff --git a/bin/tests/system/checkconf/tests.sh b/bin/tests/system/checkconf/tests.sh index 461e4ef451..3f83a13100 100644 --- a/bin/tests/system/checkconf/tests.sh +++ b/bin/tests/system/checkconf/tests.sh @@ -772,5 +772,16 @@ if [ $ret != 0 ]; then fi status=$((status + ret)) +n=$((n + 1)) +echo_i "check that 'send-report-channel' warns if no matching zone exists ($n)" +ret=0 +$CHECKCONF -z warn-rad.conf >checkconf.out$n 2>&1 || ret=1 +grep -F "send-report-channel 'example.com' is not a primary or secondary zone" 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-rad.conf b/bin/tests/system/checkconf/warn-rad.conf new file mode 100644 index 0000000000..a6bfc33973 --- /dev/null +++ b/bin/tests/system/checkconf/warn-rad.conf @@ -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. + */ + +options { + /* nonexistent zone used as agent-domain */ + send-report-channel example.com; +}; diff --git a/doc/arm/general.rst b/doc/arm/general.rst index 0a125674a1..5cf402cfd7 100644 --- a/doc/arm/general.rst +++ b/doc/arm/general.rst @@ -388,6 +388,8 @@ in the DNS.* February 2014. :rfc:`8749` - W. Mekking and D. Mahoney. *Moving DNSSEC Lookaside Validation (DLV) to Historic Status.* March 2020. +:rfc:`9567` - R. Arends and M. Larson. *DNS Error Reporting* + Notes ~~~~~ diff --git a/doc/arm/logging-categories.inc.rst b/doc/arm/logging-categories.inc.rst index c3c2e68502..91f82b36d2 100644 --- a/doc/arm/logging-categories.inc.rst +++ b/doc/arm/logging-categories.inc.rst @@ -43,6 +43,9 @@ ``general`` A catch-all for many things that still are not classified into categories. +``dns-reporting-agent`` + Logs reports from clients that the there is an error in our responses. + ``lame-servers`` Misconfigurations in remote servers, discovered by BIND 9 when trying to query those servers during resolution. diff --git a/doc/arm/reference.rst b/doc/arm/reference.rst index 3eaeab576e..c6ff719c12 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -1680,6 +1680,41 @@ default is used. If all supported digest types are disabled, the zones covered by :any:`disable-ds-digests` are treated as insecure. +.. namedconf:statement:: send-report-channel + :tags: query + :short: Sets the Agent Domain value for the EDNS Report-Channel option + + The EDNS Report-Channel option can be added to responses by an + authoritative server to inform clients of a domain name to which + operational and protocol errors may be reported. This can help + operators find out about configuration errors that are causing + problems with resolution or validation elsewhere (for example, + expired DNSSEC signatures). + + When :any:`send-report-channel` is set in :namedconf:ref:`options`, or + :namedconf:ref:`view` :iscman:`named` adds a Report-Channel option to + authoritative responses, using the specified domain name as the + Agent-Domain. :iscman:`named` also logs any TXT queries received for + names matching the prescribed error-reporting format + (_er...._er.) to the + ``dns-reporting-agent`` logging category at level ``info``. + + There should be a zone delegated to respond to these queries with TXT + records, to avoid unnecessarily query repetition. For example: + + :: + + e.g. + $ORIGIN + @ 600 SOA 0 0 0 0 600 + @ 600 NS + @ 600 NS + *._er 600 TXT "" + + If :any:`send-report-channel` is not set, or is set to ``.``, then + the EDNS option is not added to responses, and error-report queries are + not logged. + .. namedconf:statement:: dnssec-must-be-secure :tags: deprecated :short: Defines hierarchies that must or may not be secure (signed and validated). diff --git a/doc/misc/options b/doc/misc/options index 4e54305b58..44186cd37b 100644 --- a/doc/misc/options +++ b/doc/misc/options @@ -266,6 +266,7 @@ options { rrset-order { [ class ] [ type ] [ name ] ; ... }; secroots-file ; send-cookie ; + send-report-channel ; serial-query-rate ; serial-update-method ( date | increment | unixtime ); server-id ( | none | hostname ); @@ -545,6 +546,7 @@ view [ ] { root-key-sentinel ; rrset-order { [ class ] [ type ] [ name ] ; ... }; send-cookie ; + send-report-channel ; serial-update-method ( date | increment | unixtime ); server { bogus ; diff --git a/lib/dns/include/dns/message.h b/lib/dns/include/dns/message.h index 5970f61160..48367d1d57 100644 --- a/lib/dns/include/dns/message.h +++ b/lib/dns/include/dns/message.h @@ -102,18 +102,20 @@ #define DNS_MESSAGEEXTFLAG_DO 0x8000U /*%< EDNS0 extended OPT codes */ -#define DNS_OPT_LLQ 1 /*%< LLQ opt code */ -#define DNS_OPT_UL 2 /*%< UL opt code */ -#define DNS_OPT_NSID 3 /*%< NSID opt code */ -#define DNS_OPT_CLIENT_SUBNET 8 /*%< client subnet opt code */ -#define DNS_OPT_EXPIRE 9 /*%< EXPIRE opt code */ -#define DNS_OPT_COOKIE 10 /*%< COOKIE opt code */ -#define DNS_OPT_TCP_KEEPALIVE 11 /*%< TCP keepalive opt code */ -#define DNS_OPT_PAD 12 /*%< PAD opt code */ -#define DNS_OPT_KEY_TAG 14 /*%< Key tag opt code */ -#define DNS_OPT_EDE 15 /*%< Extended DNS Error opt code */ -#define DNS_OPT_CLIENT_TAG 16 /*%< Client tag opt code */ -#define DNS_OPT_SERVER_TAG 17 /*%< Server tag opt code */ + +#define DNS_OPT_LLQ 1 /*%< LLQ opt code */ +#define DNS_OPT_UL 2 /*%< UL opt code */ +#define DNS_OPT_NSID 3 /*%< NSID opt code */ +#define DNS_OPT_CLIENT_SUBNET 8 /*%< client subnet opt code */ +#define DNS_OPT_EXPIRE 9 /*%< EXPIRE opt code */ +#define DNS_OPT_COOKIE 10 /*%< COOKIE opt code */ +#define DNS_OPT_TCP_KEEPALIVE 11 /*%< TCP keepalive opt code */ +#define DNS_OPT_PAD 12 /*%< PAD opt code */ +#define DNS_OPT_KEY_TAG 14 /*%< Key tag opt code */ +#define DNS_OPT_EDE 15 /*%< Extended DNS Error opt code */ +#define DNS_OPT_CLIENT_TAG 16 /*%< Client tag opt code */ +#define DNS_OPT_SERVER_TAG 17 /*%< Server tag opt code */ +#define DNS_OPT_REPORT_CHANNEL 18 /*%< DNS Reporting Channel */ /*%< Experimental options [65001...65534] as per RFC6891 */ @@ -122,7 +124,7 @@ * options we know about. Extended DNS Errors may occur multiple times, but we * will set only one per message (for now). */ -#define DNS_EDNSOPTIONS 8 +#define DNS_EDNSOPTIONS 9 /*%< EDNS0 extended DNS errors */ #define DNS_EDE_OTHER 0 /*%< Other Error */ diff --git a/lib/dns/include/dns/name.h b/lib/dns/include/dns/name.h index 7d90ff836f..740fb9c3f9 100644 --- a/lib/dns/include/dns/name.h +++ b/lib/dns/include/dns/name.h @@ -1350,6 +1350,19 @@ dns_name_size(const dns_name_t *name); /*%< * Return the amount of dynamically allocated memory associated with * 'name' (which is 0 if 'name' is not dynamic). + */ + +bool +dns_name_israd(const dns_name_t *name, const dns_name_t *rad); +/*%< + * Determine whether 'name' matches the prescribed format of a + * DNS error-reporting name: + * + * _er...._er.. + * + * AGENT-DOMAIN is specified by the 'rad' parameter. + * EDE is a numeric value representing an extended DNS error code. + * TYPE and EDE are not currently checked. * * Requires: * \li 'name' to be valid. diff --git a/lib/dns/include/dns/view.h b/lib/dns/include/dns/view.h index 845531c0d8..d6fbf86b64 100644 --- a/lib/dns/include/dns/view.h +++ b/lib/dns/include/dns/view.h @@ -186,6 +186,7 @@ struct dns_view { uint32_t maxrrperset; uint32_t maxtypepername; uint8_t max_restarts; + dns_name_t *rad; /* reporting agent domain */ /* * Configurable data for server use only, diff --git a/lib/dns/message.c b/lib/dns/message.c index a2f766ed15..fd239f4095 100644 --- a/lib/dns/message.c +++ b/lib/dns/message.c @@ -3606,6 +3606,26 @@ cleanup: return (result); } +static isc_result_t +render_reportchan(isc_buffer_t *optbuf, 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); + if (result == ISC_R_SUCCESS && isc_buffer_activelength(optbuf) == 0) { + dns_name_format(name, namebuf, sizeof(namebuf)); + ADD_STRING(target, " "); + ADD_STRING(target, namebuf); + return (result); + } + result = ISC_R_FAILURE; +cleanup: + return (result); +} + static isc_result_t dns_message_pseudosectiontoyaml(dns_message_t *msg, dns_pseudosection_t section, const dns_master_style_t *style, @@ -3862,6 +3882,19 @@ dns_message_pseudosectiontoyaml(dns_message_t *msg, dns_pseudosection_t section, ADD_STRING(target, buf); continue; } + } else if (optcode == DNS_OPT_REPORT_CHANNEL) { + INDENT(style); + ADD_STRING(target, "Report-Channel:"); + if (optlen > 0U) { + isc_buffer_t sb = optbuf; + isc_buffer_setactive(&optbuf, optlen); + result = render_reportchan(&optbuf, + target); + if (result == ISC_R_SUCCESS) { + continue; + } + optbuf = sb; + } } else { INDENT(style); ADD_STRING(target, "OPT="); @@ -4249,6 +4282,19 @@ dns_message_pseudosectiontotext(dns_message_t *msg, dns_pseudosection_t section, ADD_STRING(target, buf); continue; } + } else if (optcode == DNS_OPT_REPORT_CHANNEL) { + ADD_STRING(target, "; Report-Channel:"); + if (optlen > 0U) { + isc_buffer_t sb = optbuf; + isc_buffer_setactive(&optbuf, optlen); + result = render_reportchan(&optbuf, + target); + if (result == ISC_R_SUCCESS) { + ADD_STRING(target, "\n"); + continue; + } + optbuf = sb; + } } else { ADD_STRING(target, "; OPT="); snprintf(buf, sizeof(buf), "%u:", optcode); diff --git a/lib/dns/name.c b/lib/dns/name.c index 4c1390e719..32770402ba 100644 --- a/lib/dns/name.c +++ b/lib/dns/name.c @@ -2281,3 +2281,72 @@ dns_name_isdnssvcb(const dns_name_t *name) { return (false); } + +bool +dns_name_israd(const dns_name_t *name, const dns_name_t *rad) { + dns_name_t suffix; + dns_offsets_t offsets; + char labelbuf[64]; + unsigned long v, last = ULONG_MAX; + char *end, *l; + + REQUIRE(DNS_NAME_VALID(name)); + REQUIRE(DNS_NAME_VALID(rad)); + + if (name->labels < rad->labels + 4U || name->length < 4U) { + return (false); + } + + if (name->ndata[0] != 3 || name->ndata[1] != '_' || + tolower(name->ndata[2]) != 'e' || tolower(name->ndata[3]) != 'r') + { + return (false); + } + + dns_name_init(&suffix, offsets); + dns_name_split(name, rad->labels + 1, NULL, &suffix); + + if (suffix.ndata[0] != 3 || suffix.ndata[1] != '_' || + tolower(suffix.ndata[2]) != 'e' || tolower(suffix.ndata[3]) != 'r') + { + return (false); + } + + /* type list */ + dns_name_split(name, name->labels - 1, NULL, &suffix); + INSIST(*suffix.ndata < sizeof(labelbuf)); + memmove(labelbuf, suffix.ndata + 1, *suffix.ndata); + labelbuf[*suffix.ndata] = 0; + if (strlen(labelbuf) != *suffix.ndata) { + return (false); + } + l = labelbuf; + do { + v = strtoul(l, &end, 10); + if (v > 0xffff || (*end != 0 && *end != '-') || end == l) { + return (false); + } + if (last != ULONG_MAX && v <= last) { + return (false); + } + last = v; + if (*end == '-') { + l = end + 1; + } + } while (*end != 0); + + /* extended error code */ + dns_name_split(name, rad->labels + 2, NULL, &suffix); + INSIST(*suffix.ndata < sizeof(labelbuf)); + memmove(labelbuf, suffix.ndata + 1, *suffix.ndata); + labelbuf[*suffix.ndata] = 0; + if (strlen(labelbuf) != *suffix.ndata) { + return (false); + } + v = strtoul(labelbuf, &end, 10); + if (v > 0xfff || *end != 0) { + return (false); + } + + return (dns_name_issubdomain(name, rad)); +} diff --git a/lib/dns/rdata/generic/opt_41.c b/lib/dns/rdata/generic/opt_41.c index 433df37299..84bbe1cc7f 100644 --- a/lib/dns/rdata/generic/opt_41.c +++ b/lib/dns/rdata/generic/opt_41.c @@ -93,17 +93,22 @@ totext_opt(ARGS_TOTEXT) { static isc_result_t fromwire_opt(ARGS_FROMWIRE) { + dns_fixedname_t fixed; + dns_name_t *name; + isc_buffer_t b; isc_region_t sregion; isc_region_t tregion; - uint16_t opt; + isc_result_t result; uint16_t length; + uint16_t opt; unsigned int total; REQUIRE(type == dns_rdatatype_opt); UNUSED(type); UNUSED(rdclass); - UNUSED(dctx); + + dctx = dns_decompress_setpermitted(dctx, false); isc_buffer_activeregion(source, &sregion); if (sregion.length == 0) { @@ -245,6 +250,22 @@ fromwire_opt(ARGS_FROMWIRE) { } isc_region_consume(&sregion, length); break; + case DNS_OPT_REPORT_CHANNEL: + /* A domain name in wire format. RFC 9567 */ + if (length == 0 || length > DNS_NAME_MAXWIRE) { + return (DNS_R_OPTERR); + } + isc_buffer_init(&b, sregion.base, length); + isc_buffer_add(&b, length); + name = dns_fixedname_initname(&fixed); + result = dns_name_fromwire(name, &b, dctx, NULL); + if (result != ISC_R_SUCCESS || name->length != length || + !dns_name_isabsolute(name)) + { + return (DNS_R_OPTERR); + } + isc_region_consume(&sregion, length); + break; default: isc_region_consume(&sregion, length); break; diff --git a/lib/dns/view.c b/lib/dns/view.c index 2be324156c..fd261a9919 100644 --- a/lib/dns/view.c +++ b/lib/dns/view.c @@ -376,6 +376,10 @@ destroy(dns_view_t *view) { dns_dns64_unlink(&view->dns64, dns64); dns_dns64_destroy(&dns64); } + if (view->rad != NULL) { + dns_name_free(view->rad, view->mctx); + isc_mem_put(view->mctx, view->rad, sizeof(*view->rad)); + } if (view->managed_keys != NULL) { dns_zone_detach(&view->managed_keys); } diff --git a/lib/isc/include/isc/log.h b/lib/isc/include/isc/log.h index 665add8d65..f1c1a47576 100644 --- a/lib/isc/include/isc/log.h +++ b/lib/isc/include/isc/log.h @@ -149,6 +149,7 @@ enum isc_logcategory { NS_LOGCATEGORY_TAT, NS_LOGCATEGORY_SERVE_STALE, NS_LOGCATEGORY_RESPONSES, + NS_LOGCATEGORY_DRA, /* cfg categories */ CFG_LOGCATEGORY_CONFIG, /* named categories */ diff --git a/lib/isc/log.c b/lib/isc/log.c index 8551b34fdc..04be355ae2 100644 --- a/lib/isc/log.c +++ b/lib/isc/log.c @@ -197,6 +197,7 @@ static const char *categories_description[] = { [NS_LOGCATEGORY_UPDATE_SECURITY] = "update-security", [NS_LOGCATEGORY_QUERY_ERRORS] = "query-errors", [NS_LOGCATEGORY_TAT] = "trust-anchor-telemetry", + [NS_LOGCATEGORY_DRA] = "dns-reporting-agent", [NS_LOGCATEGORY_SERVE_STALE] = "serve-stale", [NS_LOGCATEGORY_RESPONSES] = "responses", /* cfg categories */ diff --git a/lib/isccfg/check.c b/lib/isccfg/check.c index 31546f3b67..524c2b122d 100644 --- a/lib/isccfg/check.c +++ b/lib/isccfg/check.c @@ -1617,6 +1617,23 @@ check_options(const cfg_obj_t *options, const cfg_obj_t *config, } } + /* + * Check send-report-channel. + */ + obj = NULL; + (void)cfg_map_get(options, "send-report-channel", &obj); + if (obj != NULL) { + str = cfg_obj_asstring(obj); + tresult = check_name(str); + if (tresult != ISC_R_SUCCESS) { + cfg_obj_log(obj, ISC_LOG_ERROR, + "'%s' is not a valid name", str); + if (result == ISC_R_SUCCESS) { + result = tresult; + } + } + } + /* * Check dnssec-must-be-secure. */ @@ -5053,28 +5070,73 @@ cleanup: typedef enum { special_zonetype_rpz, special_zonetype_catz } special_zonetype_t; +static bool +iszone(const cfg_obj_t *nameobj, const char *what, const char *forview, + const char *viewname, int level, isc_symtab_t *symtab) { + char namebuf[DNS_NAME_FORMATSIZE]; + const cfg_obj_t *obj = NULL; + const char *zonename = cfg_obj_asstring(nameobj); + const char *zonetype = ""; + dns_fixedname_t fixed; + dns_name_t *name = dns_fixedname_initname(&fixed); + isc_result_t result; + isc_symvalue_t value; + + if (viewname == NULL) { + viewname = ""; + forview = ""; + } + + result = dns_name_fromstring(name, zonename, dns_rootname, 0, NULL); + if (result != ISC_R_SUCCESS) { + cfg_obj_log(nameobj, ISC_LOG_ERROR, "bad domain name '%s'", + zonename); + return (false); + } + + dns_name_format(name, namebuf, sizeof(namebuf)); + result = isc_symtab_lookup(symtab, namebuf, 3, &value); + if (result == ISC_R_SUCCESS) { + const cfg_obj_t *zoneobj = value.as_cpointer; + if (zoneobj != NULL && cfg_obj_istuple(zoneobj)) { + zoneobj = cfg_tuple_get(zoneobj, "options"); + } + if (zoneobj != NULL && cfg_obj_ismap(zoneobj)) { + (void)cfg_map_get(zoneobj, "type", &obj); + } + if (obj != NULL) { + zonetype = cfg_obj_asstring(obj); + } + } + + if (strcasecmp(zonetype, "primary") != 0 && + strcasecmp(zonetype, "master") != 0 && + strcasecmp(zonetype, "secondary") != 0 && + strcasecmp(zonetype, "slave") != 0) + { + cfg_obj_log(nameobj, level, + "%s '%s'%s%s is not a primary or secondary zone", + what, zonename, forview, viewname); + return (false); + } + return (true); +} + static isc_result_t check_rpz_catz(const char *rpz_catz, const cfg_obj_t *rpz_obj, const char *viewname, isc_symtab_t *symtab, special_zonetype_t specialzonetype) { const cfg_listelt_t *element; - const cfg_obj_t *obj, *nameobj, *zoneobj; - const char *zonename, *zonetype; + const cfg_obj_t *obj, *nameobj; const char *forview = " for view "; - isc_symvalue_t value; - isc_result_t result, tresult; - dns_fixedname_t fixed; - dns_name_t *name; - char namebuf[DNS_NAME_FORMATSIZE]; + isc_result_t result = ISC_R_SUCCESS; unsigned int num_zones = 0; if (viewname == NULL) { viewname = ""; forview = ""; } - result = ISC_R_SUCCESS; - name = dns_fixedname_initname(&fixed); obj = cfg_tuple_get(rpz_obj, "zone list"); for (element = cfg_list_first(obj); element != NULL; @@ -5082,56 +5144,21 @@ check_rpz_catz(const char *rpz_catz, const cfg_obj_t *rpz_obj, { obj = cfg_listelt_value(element); nameobj = cfg_tuple_get(obj, "zone name"); - zonename = cfg_obj_asstring(nameobj); - zonetype = ""; if (specialzonetype == special_zonetype_rpz) { if (++num_zones > 64) { cfg_obj_log(nameobj, ISC_LOG_ERROR, "more than 64 response policy " - "zones in view '%s'", - viewname); + "zones%s'%s'", + forview, viewname); return (ISC_R_FAILURE); } } - tresult = dns_name_fromstring(name, zonename, dns_rootname, 0, - NULL); - if (tresult != ISC_R_SUCCESS) { - cfg_obj_log(nameobj, ISC_LOG_ERROR, - "bad domain name '%s'", zonename); - if (result == ISC_R_SUCCESS) { - result = tresult; - } - continue; - } - dns_name_format(name, namebuf, sizeof(namebuf)); - tresult = isc_symtab_lookup(symtab, namebuf, 3, &value); - if (tresult == ISC_R_SUCCESS) { - obj = NULL; - zoneobj = value.as_cpointer; - if (zoneobj != NULL && cfg_obj_istuple(zoneobj)) { - zoneobj = cfg_tuple_get(zoneobj, "options"); - } - if (zoneobj != NULL && cfg_obj_ismap(zoneobj)) { - (void)cfg_map_get(zoneobj, "type", &obj); - } - if (obj != NULL) { - zonetype = cfg_obj_asstring(obj); - } - } - if (strcasecmp(zonetype, "primary") != 0 && - strcasecmp(zonetype, "master") != 0 && - strcasecmp(zonetype, "secondary") != 0 && - strcasecmp(zonetype, "slave") != 0) + if (!iszone(nameobj, rpz_catz, forview, viewname, ISC_LOG_ERROR, + symtab)) { - cfg_obj_log(nameobj, ISC_LOG_ERROR, - "%s '%s'%s%s is not a primary or secondary " - "zone", - rpz_catz, zonename, forview, viewname); - if (result == ISC_R_SUCCESS) { - result = ISC_R_FAILURE; - } + result = ISC_R_FAILURE; } } return (result); @@ -5411,7 +5438,8 @@ check_viewconf(const cfg_obj_t *config, const cfg_obj_t *voptions, /* * Check that the response-policy and catalog-zones options - * refer to zones that exist. + * refer to zones that exist. Also warn if send-report-channel + * is not a zone. */ if (opts != NULL) { obj = NULL; @@ -5433,6 +5461,14 @@ check_viewconf(const cfg_obj_t *config, const cfg_obj_t *voptions, { result = ISC_R_FAILURE; } + + obj = NULL; + if ((cfg_map_get(opts, "send-report-channel", &obj) == + ISC_R_SUCCESS)) + { + (void)iszone(obj, "send-report-channel", " for view ", + viewname, ISC_LOG_WARNING, symtab); + } } /* diff --git a/lib/isccfg/namedconf.c b/lib/isccfg/namedconf.c index cee0036810..d951101525 100644 --- a/lib/isccfg/namedconf.c +++ b/lib/isccfg/namedconf.c @@ -2132,6 +2132,7 @@ static cfg_clausedef_t view_clauses[] = { { "queryport-pool-updateinterval", NULL, CFG_CLAUSEFLAG_ANCIENT }, { "rate-limit", &cfg_type_rrl, 0 }, { "recursion", &cfg_type_boolean, 0 }, + { "send-report-channel", &cfg_type_astring, 0 }, { "request-nsid", &cfg_type_boolean, 0 }, { "request-sit", NULL, CFG_CLAUSEFLAG_ANCIENT }, { "require-server-cookie", &cfg_type_boolean, 0 }, diff --git a/lib/ns/client.c b/lib/ns/client.c index df20be335b..25bb7e5a8a 100644 --- a/lib/ns/client.c +++ b/lib/ns/client.c @@ -97,13 +97,11 @@ #define COOKIE_SIZE 24U /* 8 + 4 + 4 + 8 */ #define ECS_SIZE 20U /* 2 + 1 + 1 + [0..16] */ -#define TCPBUFFERS_FILLCOUNT 1U -#define TCPBUFFERS_FREEMAX 8U - -#define WANTNSID(x) (((x)->attributes & NS_CLIENTATTR_WANTNSID) != 0) -#define WANTEXPIRE(x) (((x)->attributes & NS_CLIENTATTR_WANTEXPIRE) != 0) -#define WANTPAD(x) (((x)->attributes & NS_CLIENTATTR_WANTPAD) != 0) #define USEKEEPALIVE(x) (((x)->attributes & NS_CLIENTATTR_USEKEEPALIVE) != 0) +#define WANTEXPIRE(x) (((x)->attributes & NS_CLIENTATTR_WANTEXPIRE) != 0) +#define WANTNSID(x) (((x)->attributes & NS_CLIENTATTR_WANTNSID) != 0) +#define WANTPAD(x) (((x)->attributes & NS_CLIENTATTR_WANTPAD) != 0) +#define WANTRC(x) (((x)->attributes & NS_CLIENTATTR_WANTRC) != 0) #define MANAGER_MAGIC ISC_MAGIC('N', 'S', 'C', 'm') #define VALID_MANAGER(m) ISC_MAGIC_VALID(m, MANAGER_MAGIC) @@ -1238,6 +1236,16 @@ no_nsid: count++; } + if (WANTRC(client) && view != NULL && view->rad != NULL && + !dns_name_equal(view->rad, dns_rootname)) + { + INSIST(count < DNS_EDNSOPTIONS); + ednsopts[count].code = DNS_OPT_REPORT_CHANNEL; + ednsopts[count].length = view->rad->length; + ednsopts[count].value = view->rad->ndata; + count++; + } + /* Padding must be added last */ if ((view != NULL) && (view->padding > 0) && WANTPAD(client) && (TCP_CLIENT(client) || diff --git a/lib/ns/include/ns/client.h b/lib/ns/include/ns/client.h index 2a27df8c20..e03bc11e6d 100644 --- a/lib/ns/include/ns/client.h +++ b/lib/ns/include/ns/client.h @@ -247,7 +247,7 @@ struct ns_client { #define NS_CLIENTATTR_WANTNSID 0x00020 /*%< include nameserver ID */ #define NS_CLIENTATTR_BADCOOKIE \ 0x00040 /*%< Presented cookie is bad/out-of-date */ -/* Obsolete: NS_CLIENTATTR_FILTER_AAAA_RC 0x00080 */ +#define NS_CLIENTATTR_WANTRC 0x00080 /*%< include Report-Channel */ #define NS_CLIENTATTR_WANTAD 0x00100 /*%< want AD in response if possible */ #define NS_CLIENTATTR_WANTCOOKIE 0x00200 /*%< return a COOKIE */ #define NS_CLIENTATTR_HAVECOOKIE 0x00400 /*%< has a valid COOKIE */ @@ -257,8 +257,8 @@ struct ns_client { #define NS_CLIENTATTR_HAVEECS 0x04000 /*%< received an ECS option */ #define NS_CLIENTATTR_WANTPAD 0x08000 /*%< pad reply */ #define NS_CLIENTATTR_USEKEEPALIVE 0x10000 /*%< use TCP keepalive */ - -#define NS_CLIENTATTR_NOSETFC 0x20000 /*%< don't set servfail cache */ +#define NS_CLIENTATTR_NOSETFC 0x20000 /*%< don't set servfail cache */ +#define NS_CLIENTATTR_NEEDTCP 0x40000 /*%< send TC=1 */ /* * Flag to use with the SERVFAIL cache to indicate diff --git a/lib/ns/query.c b/lib/ns/query.c index 107c6ef23c..4a11ca7857 100644 --- a/lib/ns/query.c +++ b/lib/ns/query.c @@ -107,6 +107,8 @@ (((c)->query.attributes & NS_QUERYATTR_WANTRECURSION) != 0) /*% Is TCP? */ #define TCP(c) (((c)->attributes & NS_CLIENTATTR_TCP) != 0) +/*% This query needs to have been sent over TCP. Return TC=1. */ +#define NEEDTCP(c) (((c)->attributes & NS_CLIENTATTR_NEEDTCP) != 0) /*% Want DNSSEC? */ #define WANTDNSSEC(c) (((c)->attributes & NS_CLIENTATTR_WANTDNSSEC) != 0) @@ -5363,6 +5365,17 @@ ns__query_start(query_ctx_t *qctx) { qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AA; qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD; qctx->client->message->rcode = dns_rcode_badcookie; + qctx->client->attributes &= ~NS_CLIENTATTR_WANTRC; + return (ns_query_done(qctx)); + } + + /* + * Respond with TC=1 if we need TCP for this request. + */ + if (!TCP(qctx->client) && NEEDTCP(qctx->client)) { + qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AA; + qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD; + qctx->client->message->flags |= DNS_MESSAGEFLAG_TC; return (ns_query_done(qctx)); } @@ -6891,6 +6904,8 @@ query_checkrrl(query_ctx_t *qctx, isc_result_t result) { ~DNS_MESSAGEFLAG_AD; qctx->client->message->rcode = dns_rcode_badcookie; + qctx->client->attributes &= + ~NS_CLIENTATTR_WANTRC; } else { qctx->client->message->flags |= DNS_MESSAGEFLAG_TC; @@ -11323,6 +11338,7 @@ ns_query_done(query_ctx_t *qctx) { */ if (qctx->client->query.restarts == 0 && !qctx->authoritative) { qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AA; + qctx->client->attributes &= ~NS_CLIENTATTR_WANTRC; } /* @@ -11459,6 +11475,54 @@ cleanup: return (result); } +static void +log_reportchannel(ns_client_t *client) { + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + char namebuf[DNS_NAME_FORMATSIZE]; + + client->attributes |= NS_CLIENTATTR_WANTRC; + + if (client->view->rad != NULL && + dns_name_issubdomain(client->query.qname, client->view->rad)) + { + /* + * Don't add Report-Channel to responses at or below the + * reporting agent domain to prevent infinite loops. + */ + client->attributes &= ~NS_CLIENTATTR_WANTRC; + } + + if (client->query.qtype != dns_rdatatype_txt || + client->view->rad == NULL || + !dns_name_israd(client->query.qname, client->view->rad)) + { + return; + } + + /* + * Check for TCP or a good server cookie. If neither send + * back BADCOOKIE or TC=1. + */ + if (!TCP(client) && !HAVECOOKIE(client)) { + if (WANTCOOKIE(client)) { + client->attributes |= NS_CLIENTATTR_BADCOOKIE; + } else { + client->attributes |= NS_CLIENTATTR_NEEDTCP; + } + } + + if (!isc_log_wouldlog(ISC_LOG_INFO)) { + return; + } + + dns_name_format(client->query.qname, namebuf, sizeof(namebuf)); + dns_rdataclass_format(client->view->rdclass, classbuf, + sizeof(classbuf)); + + isc_log_write(NS_LOGCATEGORY_DRA, NS_LOGMODULE_QUERY, ISC_LOG_INFO, + "dns-reporting-agent '%s/%s'", namebuf, classbuf); +} + static void log_tat(ns_client_t *client) { char namebuf[DNS_NAME_FORMATSIZE]; @@ -11714,6 +11778,7 @@ ns_query_start(ns_client_t *client, isc_nmhandle_t *handle) { dns_rdatatypestats_increment(client->manager->sctx->rcvquerystats, qtype); + log_reportchannel(client); log_tat(client); if (dns_rdatatype_ismeta(qtype)) { diff --git a/tests/dns/rdata_test.c b/tests/dns/rdata_test.c index f61c7ea400..57be904cbc 100644 --- a/tests/dns/rdata_test.c +++ b/tests/dns/rdata_test.c @@ -1848,6 +1848,45 @@ ISC_RUN_TEST_IMPL(edns_client_subnet) { dns_rdatatype_opt, sizeof(dns_rdata_opt_t)); } +ISC_RUN_TEST_IMPL(edns_rad) { + wire_ok_t wire_ok[] = { + /* + * Option code with no content. + */ + WIRE_INVALID(0x00, 0x12, 0x00, 0x00), + /* + * Agent Domain = "." + */ + WIRE_VALID(0x00, 0x12, 0x00, 0x01, 0x00), + /* + * Data after name. + */ + WIRE_INVALID(0x00, 0x12, 0x00, 0x02, 0x00, 0x00), + /* + * Agent Domain = "example.com." + */ + WIRE_VALID(0x00, 0x12, 0x00, 13, 7, 'e', 'x', 'a', 'm', 'p', + 'l', 'e', 3, 'c', 'o', 'm', 0x00), + /* + * No root label at end. + */ + WIRE_INVALID(0x00, 0x12, 0x00, 12, 7, 'e', 'x', 'a', 'm', 'p', + 'l', 'e', 3, 'c', 'o', 'm'), + /* + * Truncated label. + */ + WIRE_INVALID(0x00, 0x12, 0x00, 11, 7, 'e', 'x', 'a', 'm', 'p', + 'l', 'e', 3, 'c', 'o'), + /* + * Sentinel. + */ + WIRE_SENTINEL() + }; + + check_rdata(NULL, wire_ok, NULL, true, dns_rdataclass_in, + dns_rdatatype_opt, sizeof(dns_rdata_opt_t)); +} + /* * http://ana-3.lcs.mit.edu/~jnc/nimrod/dns.txt * @@ -3171,6 +3210,7 @@ ISC_TEST_ENTRY(zonemd) /* other tests */ ISC_TEST_ENTRY(edns_client_subnet) +ISC_TEST_ENTRY(edns_rad) ISC_TEST_ENTRY(atcname) ISC_TEST_ENTRY(atparent) ISC_TEST_ENTRY(iszonecutauth) diff --git a/util/check-categories.sh b/util/check-categories.sh index e45dafe33f..a4ad7d7913 100644 --- a/util/check-categories.sh +++ b/util/check-categories.sh @@ -16,6 +16,7 @@ list1=$( | grep -E "^[[:space:]]+[^[:space:]]+_LOGCATEGORY_[^[:space:]]+([[:space:]]+=[[:space:]]+[-0-9]+)?," \ | grep -Ev "ISC_LOGCATEGORY_(MAX|INVALID)" \ | sed -e 's/.*LOGCATEGORY_\([A-Z_]*\).*/\1/' -e 's/^RRL$/rate-limit/' \ + -e 's/DRA/dns-reporting-agent/' \ | tr 'A-Z' 'a-z' \ | tr _ - \ | sed 's/^tat$/trust-anchor-telemetry/' \