diff --git a/bin/tests/system/auth/ns1/named.conf.in b/bin/tests/system/auth/ns1/named.conf.in index 0f40bbbf4a..92fd707dbd 100644 --- a/bin/tests/system/auth/ns1/named.conf.in +++ b/bin/tests/system/auth/ns1/named.conf.in @@ -38,6 +38,12 @@ view main in { file "example.com.db"; send-report-channel "rad.example.com"; }; + + zone rad.example.net { + type primary; + file "rad.db"; + log-report-channel yes; + }; }; view alt chaos { diff --git a/bin/tests/system/auth/ns1/rad.db b/bin/tests/system/auth/ns1/rad.db new file mode 100644 index 0000000000..08d3197011 --- /dev/null +++ b/bin/tests/system/auth/ns1/rad.db @@ -0,0 +1,23 @@ +; Copyright (C) Internet Systems Consortium, Inc. ("ISC") +; +; SPDX-License-Identifier: MPL-2.0 +; +; This Source Code Form 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 ; 5 minutes +@ IN SOA ns root ( + 2018010100 ; serial + 1800 ; refresh (30 minutes) + 1800 ; retry (30 minutes) + 1814400 ; expire (3 weeks) + 3600 ; minimum (1 hour) + ) + NS ns +ns A 10.53.0.1 +server A 10.53.0.100 +*._er TXT "Report received" diff --git a/bin/tests/system/auth/tests.sh b/bin/tests/system/auth/tests.sh index 7900ae0010..38fb85d76f 100644 --- a/bin/tests/system/auth/tests.sh +++ b/bin/tests/system/auth/tests.sh @@ -212,5 +212,15 @@ 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 to non-logging zones are not logged ($n)" +ret=0 +nextpart ns1/named.run >/dev/null +$DIG $DIGOPTS @10.53.0.1 _er.0.example.1._er.example.com TXT >dig.out.test$n +nextpart ns1/named.run | grep "dns-reporting-agent '_er.0.example.1._er.example.com/IN'" >/dev/null && ret=1 +grep "; Report-Channel: rad.example.com" 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/warn-rad.conf b/bin/tests/system/checkconf/bad-log-rc.conf similarity index 82% rename from bin/tests/system/checkconf/warn-rad.conf rename to bin/tests/system/checkconf/bad-log-rc.conf index a6bfc33973..9834f70a6c 100644 --- a/bin/tests/system/checkconf/warn-rad.conf +++ b/bin/tests/system/checkconf/bad-log-rc.conf @@ -11,7 +11,8 @@ * information regarding copyright ownership. */ -options { - /* nonexistent zone used as agent-domain */ - send-report-channel example.com; +zone "." { + type primary; + file "root.db"; + log-report-channel yes; }; diff --git a/bin/tests/system/checkconf/tests.sh b/bin/tests/system/checkconf/tests.sh index 3f83a13100..461e4ef451 100644 --- a/bin/tests/system/checkconf/tests.sh +++ b/bin/tests/system/checkconf/tests.sh @@ -772,16 +772,5 @@ 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/doc/arm/logging-categories.inc.rst b/doc/arm/logging-categories.inc.rst index 91f82b36d2..88d4f8b180 100644 --- a/doc/arm/logging-categories.inc.rst +++ b/doc/arm/logging-categories.inc.rst @@ -44,7 +44,7 @@ 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. + Reports from clients indicating that 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 ef2d15277a..4813ccd546 100644 --- a/doc/arm/reference.rst +++ b/doc/arm/reference.rst @@ -1680,44 +1680,6 @@ 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`, - :namedconf:ref:`view`, or :namedconf:ref:`zone`, :iscman:`named` adds a - Report-Channel option to authoritative responses, using the specified - domain name as the Agent-Domain. - - When it is set in :namedconf:ref:`options` or :namedconf:ref:`view` (but - *not* :namedconf:ref:`zone`), :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). @@ -1957,6 +1919,32 @@ default is used. :namedconf:ref:`zone` blocks, setting :any:`max-zone-ttl` to zero is equivalent to "unlimited". +.. 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`, + :namedconf:ref:`view`, or :namedconf:ref:`zone`, :iscman:`named` adds a + Report-Channel option to authoritative responses, using the specified + domain name as the Agent-Domain. + + If :any:`send-report-channel` is not set, or is set to ``.``, then + the EDNS option is not added to responses. + + A client that wishes to report an error can send a query for type TXT + to a name matching the prescribed RFC 9567 error-reporting format: + ``_er...._er.``. + + There should be an authoritative zone configured to respond to such + queries, with the :any:`log-report-channel` option set to ``yes``. + .. namedconf:statement:: stale-answer-ttl :tags: query :short: Specifies the time to live (TTL) to be returned on stale answers, in seconds. @@ -7317,6 +7305,30 @@ Zone Options The use of this option in :any:`zone` blocks is deprecated and will be rendered non-operational in a future release. +.. namedconf:statement:: log-report-channel + :tags: logging + :short: Specifies whether to log error-report queries. + + When this option is set to ``yes``, TXT queries for names + matching the prescribed RFC 9567 error-reporting format + (``_er...._er.``) + are logged to the ``dns-reporting-agent`` logging category at + level ``info``. + + The zone should have a wildcard record in place to respond to such + queries. For example: + + :: + + $ORIGIN + @ 600 SOA 0 0 0 0 600 + @ 600 NS + @ 600 NS + *._er 600 TXT "Report received" + +:any:`send-report-channel` + See the description of :any:`send-report-channel` in :namedconf:ref:`options`. + .. _dynamic_update_policies: Dynamic Update Policies diff --git a/lib/isccfg/check.c b/lib/isccfg/check.c index 524c2b122d..a24297290b 100644 --- a/lib/isccfg/check.c +++ b/lib/isccfg/check.c @@ -3910,6 +3910,22 @@ check_zoneconf(const cfg_obj_t *zconfig, const cfg_obj_t *voptions, } } + /* + * "log-report-channel" cannot be set for the root zone. + */ + if (ztype == CFG_ZONE_PRIMARY || ztype == CFG_ZONE_SECONDARY) { + obj = NULL; + tresult = cfg_map_get(zoptions, "log-report-channel", &obj); + if (tresult == ISC_R_SUCCESS && cfg_obj_asboolean(obj) && + dns_name_equal(zname, dns_rootname)) + { + cfg_obj_log(zconfig, ISC_LOG_ERROR, + "'log-report-channel' cannot be set in " + "the root zone"); + result = ISC_R_FAILURE; + } + } + /* * Check various options. */ @@ -5070,73 +5086,28 @@ 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; + const cfg_obj_t *obj, *nameobj, *zoneobj; + const char *zonename, *zonetype; const char *forview = " for view "; - isc_result_t result = ISC_R_SUCCESS; + isc_symvalue_t value; + isc_result_t result, tresult; + dns_fixedname_t fixed; + dns_name_t *name; + char namebuf[DNS_NAME_FORMATSIZE]; 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; @@ -5144,21 +5115,56 @@ 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%s'%s'", - forview, viewname); + "zones in view '%s'", + viewname); return (ISC_R_FAILURE); } } - if (!iszone(nameobj, rpz_catz, forview, viewname, ISC_LOG_ERROR, - symtab)) + 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) { - result = ISC_R_FAILURE; + 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; + } } } return (result); @@ -5438,8 +5444,7 @@ 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. Also warn if send-report-channel - * is not a zone. + * refer to zones that exist. */ if (opts != NULL) { obj = NULL; @@ -5461,14 +5466,6 @@ 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/ns/query.c b/lib/ns/query.c index ed2662b0a7..6dc2048fc0 100644 --- a/lib/ns/query.c +++ b/lib/ns/query.c @@ -5332,6 +5332,75 @@ root_key_sentinel_detect(query_ctx_t *qctx) { } } +static void +qctx_reportquery(query_ctx_t *qctx) { + ns_client_t *client = qctx->client; + + client->attributes |= NS_CLIENTATTR_WANTRC; + + /* If this isn't a report-logging zone, there's no more to do */ + dns_zoneopt_t opts = dns_zone_getoptions(qctx->zone); + if ((opts & DNS_ZONEOPT_LOGREPORTS) == 0) { + return; + } + + /* + * Suppress EDNS Report-Channel in responses from report- + * logging zones; this prevents infinite loops. + */ + client->attributes &= ~NS_CLIENTATTR_WANTRC; + + /* If this isn't an error-report query, there's nothing more to do */ + if (client->query.qtype != dns_rdatatype_txt || + !dns_name_israd(client->query.qname, + dns_zone_getorigin(qctx->zone))) + { + 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)) { + char classbuf[DNS_RDATACLASS_FORMATSIZE]; + char namebuf[DNS_NAME_FORMATSIZE]; + + 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 +qctx_setrad(query_ctx_t *qctx) { + ns_client_t *client = qctx->client; + + /* Set the client to send a Report-Channel option when replying */ + if ((client->attributes & NS_CLIENTATTR_WANTRC) != 0) { + dns_fixedname_t fixed; + dns_name_t *rad = dns_fixedname_initname(&fixed); + + if (!dns_name_dynamic(&client->rad) && + dns_zone_getrad(qctx->zone, rad) == ISC_R_SUCCESS) + { + dns_name_dup(rad, client->manager->mctx, &client->rad); + } + } +} + /*% * Starting point for a client query or a chaining query. * @@ -5518,8 +5587,6 @@ ns__query_start(query_ctx_t *qctx) { if (qctx->is_zone) { qctx->authoritative = true; if (qctx->zone != NULL) { - dns_fixedname_t fixed; - dns_name_t *rad; switch (dns_zone_gettype(qctx->zone)) { case dns_zone_mirror: qctx->authoritative = false; @@ -5529,16 +5596,8 @@ ns__query_start(query_ctx_t *qctx) { break; case dns_zone_primary: case dns_zone_secondary: - rad = dns_fixedname_initname(&fixed); - if (!dns_name_dynamic(&qctx->client->rad) && - dns_zone_getrad(qctx->zone, rad) == - ISC_R_SUCCESS) - { - dns_name_dup( - rad, - qctx->client->manager->mctx, - &qctx->client->rad); - } + qctx_reportquery(qctx); + qctx_setrad(qctx); break; default: break; @@ -11493,54 +11552,6 @@ 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]; @@ -11796,7 +11807,6 @@ 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)) {