diff --git a/doc/example.conf.in b/doc/example.conf.in index 329bed150..0c1cfd0d9 100644 --- a/doc/example.conf.in +++ b/doc/example.conf.in @@ -1052,6 +1052,11 @@ server: # Note that the ede option above needs to be enabled for this to work. # ede-serve-expired: no + # Enable DNS Error Reporting (RFC9567). + # qname-minimisation is advised to be turned on as well to increase + # privacy on the outgoing reports. + # dns-error-reporting: no + # Specific options for ipsecmod. Unbound needs to be configured with # --enable-ipsecmod for these to take effect. # diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in index ab146d5e4..e76def298 100644 --- a/doc/unbound.conf.5.in +++ b/doc/unbound.conf.5.in @@ -1996,17 +1996,30 @@ be used. Default is 65001. .TP 5 .B ede: \fI If enabled, Unbound will respond with Extended DNS Error codes (RFC8914). -These EDEs attach informative error messages to a response for various -errors. Default is "no". +These EDEs provide additional information with a response mainly for, but not +limited to, DNS and DNSSEC errors. When the \fBval-log-level\fR option is also set to \fB2\fR, responses with -Extended DNS Errors concerning DNSSEC failures that are not served from cache, -will also contain a descriptive text message about the reason for the failure. +Extended DNS Errors concerning DNSSEC failures will also contain a descriptive +text message about the reason for the failure. +Default is "no". .TP 5 .B ede\-serve\-expired: \fI If enabled, Unbound will attach an Extended DNS Error (RFC8914) Code 3 - Stale -Answer as EDNS0 option to the expired response. Note that this will not attach -the EDE code without setting the global \fBede\fR option to "yes" as well. +Answer as EDNS0 option to the expired response. +The \fBede\fR option needs to be enabled as well for this to work. +Default is "no". +.TP 5 +.B dns\-error\-reporting: \fI +If enabled, Unbound will send DNS Error Reports (RFC9567). +The name servers need to express support by attaching the Report-Channel EDNS0 +option on their replies specifying the reporting agent for the zone. +Any errors encountered during resolution that would result in Unbound +generating an Extended DNS Error (RFC8914) will be reported to the zone's +reporting agent. +The \fBede\fR option does not need to be enabled for this to work. +It is advised that the \fBqname\-minimisation\fR option is also enabled to +increase privacy on the outgoing reports. Default is "no". .SS "Remote Control Options" In the diff --git a/services/mesh.c b/services/mesh.c index 522118844..5f9a0dfc9 100644 --- a/services/mesh.c +++ b/services/mesh.c @@ -1491,6 +1491,106 @@ mesh_send_reply(struct mesh_state* m, int rcode, struct reply_info* rep, } } +/** + * Generate the DNS Error Report (RFC9567). + * If there is an EDE attached for this reply and there was a Report-Channel + * EDNS0 option from the upstream, fire up a report query. + * @param qstate: module qstate. + * @param rep: prepared reply to be sent. + */ +static void dns_error_reporting(struct module_qstate* qstate, + struct reply_info* rep) +{ + struct query_info qinfo; + struct mesh_state* sub; + struct module_qstate* newq; + uint8_t buf[LDNS_MAX_DOMAINLEN]; + size_t count = 0; + int written; + size_t expected_length; + struct edns_option* opt; + sldns_ede_code reason_bogus = LDNS_EDE_NONE; + sldns_rr_type qtype = qstate->qinfo.qtype; + uint8_t* qname = qstate->qinfo.qname; + size_t qname_len = qstate->qinfo.qname_len-1; /* skip the trailing \0 */ + uint8_t* agent_domain; + size_t agent_domain_len; + + reason_bogus = errinf_to_reason_bogus(qstate); + if(rep && ((reason_bogus == LDNS_EDE_DNSSEC_BOGUS && + rep->reason_bogus != LDNS_EDE_NONE) || + reason_bogus == LDNS_EDE_NONE)) { + reason_bogus = rep->reason_bogus; + } + if(reason_bogus == LDNS_EDE_NONE) return; + + opt = edns_opt_list_find(qstate->edns_opts_back_in, + LDNS_EDNS_REPORT_CHANNEL); + if(!opt) return; + agent_domain_len = opt->opt_len; + agent_domain = opt->opt_data; + if(dname_valid(agent_domain, agent_domain_len) < 3) { + /* The agent domain needs to be a valid dname that is not the + * root; from RFC9567. */ + return; + } + + /* Synthesize the error report query in the format: + * "_er.$qtype.$qname.$ede._er.$reporting-agent-domain" */ + /* First check if the static length parts fit in the buffer. + * That is everything except for qtype and ede that need to be + * converted to decimal and checked further on. */ + expected_length = 4/*_er*/+qname_len+4/*_er*/+agent_domain_len; + if(expected_length > LDNS_MAX_DOMAINLEN) goto skip; + + memmove(buf+count, "\3_er", 4); + count += 4; + + written = snprintf((char*)buf+count, LDNS_MAX_DOMAINLEN-count, + "X%d", qtype); + expected_length += written; + /* Skip on error, truncation or long expected length */ + if(written < 0 || (size_t)written >= LDNS_MAX_DOMAINLEN-count || + expected_length > LDNS_MAX_DOMAINLEN ) goto skip; + /* Put in the label length */ + *(buf+count) = (char)(written - 1); + count += written; + + memmove(buf+count, qname, qname_len); + count += qname_len; + + written = snprintf((char*)buf+count, LDNS_MAX_DOMAINLEN-count, + "X%d", reason_bogus); + expected_length += written; + /* Skip on error, truncation or long expected length */ + if(written < 0 || (size_t)written >= LDNS_MAX_DOMAINLEN-count || + expected_length > LDNS_MAX_DOMAINLEN ) goto skip; + *(buf+count) = (char)(written - 1); + count += written; + + memmove(buf+count, "\3_er", 4); + count += 4; + + /* Copy the agent domain */ + memmove(buf+count, agent_domain, agent_domain_len); + count += agent_domain_len; + + qinfo.qname = buf; + qinfo.qname_len = count; + qinfo.qtype = LDNS_RR_TYPE_TXT; + qinfo.qclass = qstate->qinfo.qclass; + qinfo.local_alias = NULL; + + log_query_info(VERB_ALGO, "DNS Error Reporting: generating report " + "query for", &qinfo); + mesh_add_sub(qstate, &qinfo, BIT_RD, 0, 0, &newq, &sub); + return; +skip: + verbose(VERB_ALGO, "DNS Error Reporting: report query qname too long; " + "skip"); + return; +} + void mesh_query_done(struct mesh_state* mstate) { struct mesh_reply* r; @@ -1517,6 +1617,10 @@ void mesh_query_done(struct mesh_state* mstate) if(err) { log_err("%s", err); } } } + + if(mstate->s.env->cfg->dns_error_reporting) + dns_error_reporting(&mstate->s, rep); + for(r = mstate->reply_list; r; r = r->next) { struct timeval old; timeval_subtract(&old, mstate->s.env->now_tv, &r->start_time); diff --git a/sldns/rrdef.h b/sldns/rrdef.h index 7cadf7beb..c4e7cef6d 100644 --- a/sldns/rrdef.h +++ b/sldns/rrdef.h @@ -438,6 +438,7 @@ enum sldns_enum_edns_option LDNS_EDNS_PADDING = 12, /* RFC7830 */ LDNS_EDNS_EDE = 15, /* RFC8914 */ LDNS_EDNS_CLIENT_TAG = 16, /* draft-bellis-dnsop-edns-tags-01 */ + LDNS_EDNS_REPORT_CHANNEL = 18, /* RFC9567 */ LDNS_EDNS_UNBOUND_CACHEDB_TESTFRAME_TEST = 65534 }; typedef enum sldns_enum_edns_option sldns_edns_option; diff --git a/testdata/dns_error_reporting.rpl b/testdata/dns_error_reporting.rpl new file mode 100644 index 000000000..ad6edfa47 --- /dev/null +++ b/testdata/dns_error_reporting.rpl @@ -0,0 +1,176 @@ +; Test DNS Error Reporting. + +server: + module-config: "validator iterator" + trust-anchor-signaling: no + target-fetch-policy: "0 0 0 0 0" + verbosity: 4 + qname-minimisation: no + minimal-responses: no + rrset-roundrobin: no + trust-anchor: "a.domain DS 50602 8 2 FA8EE175C47325F4BD46D8A4083C3EBEB11C977D689069F2B41F1A29B22446B1" + ede: no # It is not needed for dns-error-reporting; only for clients to receive EDEs + dns-error-reporting: yes + +stub-zone: + name: a.domain + stub-addr: 0.0.0.1 +stub-zone: + name: an.agent + stub-addr: 0.0.0.2 +CONFIG_END + +SCENARIO_BEGIN Test DNS Error Reporting + +; a.domain +RANGE_BEGIN 0 9 + ADDRESS 0.0.0.1 + ENTRY_BEGIN + MATCH opcode qtype qname + ADJUST copy_id + REPLY QR NOERROR + SECTION QUESTION + a.domain. IN DNSKEY + ENTRY_END + ENTRY_BEGIN + MATCH opcode qtype qname + ADJUST copy_id + REPLY QR NOERROR + SECTION QUESTION + a.domain. IN A + SECTION ANSWER + a.domain. 5 IN A 0.0.0.0 + ; No RRSIG to trigger validation error (and EDE) + SECTION ADDITIONAL + ; No Report-Channel here + ENTRY_END +RANGE_END + +; a.domain +RANGE_BEGIN 10 100 + ADDRESS 0.0.0.1 + ENTRY_BEGIN + MATCH opcode qtype qname + ADJUST copy_id + REPLY QR NOERROR + SECTION QUESTION + a.domain. IN DNSKEY + ENTRY_END + ENTRY_BEGIN + MATCH opcode qtype qname + ADJUST copy_id + REPLY QR NOERROR + SECTION QUESTION + a.domain. IN A + SECTION ANSWER + a.domain. 5 IN A 0.0.0.0 + ; No RRSIG to trigger validator error and EDE + SECTION ADDITIONAL + HEX_EDNSDATA_BEGIN + 00 12 ; opt-code (Report-Channel) + 00 0A ; opt-len + 02 61 6E 05 61 67 65 6E 74 00 ; an.agent. + HEX_EDNSDATA_END + ENTRY_END +RANGE_END + +; an.agent +RANGE_BEGIN 10 20 + ADDRESS 0.0.0.2 + ENTRY_BEGIN + MATCH opcode qtype qname + ADJUST copy_id + REPLY QR NOERROR + SECTION QUESTION + _er.1.a.domain.9._er.an.agent. IN TXT + SECTION ANSWER + _er.1.a.domain.9._er.an.agent. IN TXT "OK" + ENTRY_END +RANGE_END + +; Query +STEP 0 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +a.domain. IN A +ENTRY_END + +; Check that validation failed (no DNS error reporting at this state) +STEP 1 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA SERVFAIL +SECTION QUESTION +a.domain. IN A +ENTRY_END + +; Wait for the a.domain query to expire (TTL 5) +STEP 3 TIME_PASSES ELAPSE 6 + +; Query again +STEP 10 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +a.domain. IN A +ENTRY_END + +; Check that validation failed +; (a DNS Error Report query should have been generated) +STEP 11 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA SERVFAIL +SECTION QUESTION +a.domain. IN A +ENTRY_END + +; Check explicitly that the DNS Error Report query is cached. +STEP 20 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +_er.1.a.domain.9._er.an.agent. IN TXT +ENTRY_END + +; At this range there are no configured agents to answer this. +; If the DNS Error Report query is not answered from the cache the test will +; fail with pending messages. +STEP 21 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY RD QR RA NOERROR +SECTION QUESTION +_er.1.a.domain.9._er.an.agent. IN TXT +SECTION ANSWER +_er.1.a.domain.9._er.an.agent. IN TXT "OK" +ENTRY_END + +; Wait for the a.domain query to expire (5 TTL). +; The DNS Error Report query should still be cached (SOA negative). +STEP 30 TIME_PASSES ELAPSE 6 + +; Force a DNS Error Report query generation again. +STEP 31 QUERY +ENTRY_BEGIN +REPLY RD +SECTION QUESTION +a.domain. IN A +ENTRY_END + +; Check that validation failed +STEP 32 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR RD RA SERVFAIL +SECTION QUESTION +a.domain. IN A +ENTRY_END + +; The same DNS Error Report query will be generated as above. +; No agent is configured at this range to answer the DNS Error Report query. +; If the DNS Error Report query is not used from the cache the test will fail +; with pending messages. + +SCENARIO_END diff --git a/util/config_file.c b/util/config_file.c index 6c42a80d0..50a6df41e 100644 --- a/util/config_file.c +++ b/util/config_file.c @@ -280,7 +280,6 @@ config_create(void) cfg->serve_expired_ttl_reset = 0; cfg->serve_expired_reply_ttl = 30; cfg->serve_expired_client_timeout = 0; - cfg->ede_serve_expired = 0; cfg->serve_original_ttl = 0; cfg->zonemd_permissive_mode = 0; cfg->add_holddown = 30*24*3600; @@ -407,6 +406,8 @@ config_create(void) cfg->ipset_name_v6 = NULL; #endif cfg->ede = 0; + cfg->ede_serve_expired = 0; + cfg->dns_error_reporting = 0; return cfg; error_exit: config_delete(cfg); @@ -717,6 +718,7 @@ int config_set_option(struct config_file* cfg, const char* opt, else S_NUMBER_OR_ZERO("serve-expired-client-timeout:", serve_expired_client_timeout) else S_YNO("ede:", ede) else S_YNO("ede-serve-expired:", ede_serve_expired) + else S_YNO("dns-error-reporting:", dns_error_reporting) else S_YNO("serve-original-ttl:", serve_original_ttl) else S_STR("val-nsec3-keysize-iterations:", val_nsec3_key_iterations) else S_YNO("zonemd-permissive-mode:", zonemd_permissive_mode) @@ -1183,6 +1185,7 @@ config_get_option(struct config_file* cfg, const char* opt, else O_DEC(opt, "serve-expired-client-timeout", serve_expired_client_timeout) else O_YNO(opt, "ede", ede) else O_YNO(opt, "ede-serve-expired", ede_serve_expired) + else O_YNO(opt, "dns-error-reporting", dns_error_reporting) else O_YNO(opt, "serve-original-ttl", serve_original_ttl) else O_STR(opt, "val-nsec3-keysize-iterations",val_nsec3_key_iterations) else O_YNO(opt, "zonemd-permissive-mode", zonemd_permissive_mode) diff --git a/util/config_file.h b/util/config_file.h index cca714127..a98d87956 100644 --- a/util/config_file.h +++ b/util/config_file.h @@ -426,8 +426,6 @@ struct config_file { /** serve expired entries only after trying to update the entries and this * timeout (in milliseconds) is reached */ int serve_expired_client_timeout; - /** serve EDE code 3 - Stale Answer (RFC8914) for expired entries */ - int ede_serve_expired; /** serve original TTLs rather than decrementing ones */ int serve_original_ttl; /** nsec3 maximum iterations per key size, string */ @@ -758,6 +756,10 @@ struct config_file { #endif /** respond with Extended DNS Errors (RFC8914) */ int ede; + /** serve EDE code 3 - Stale Answer (RFC8914) for expired entries */ + int ede_serve_expired; + /** send DNS Error Reports to upstream reporting agent (RFC9567) */ + int dns_error_reporting; }; /** from cfg username, after daemonize setup performed */ diff --git a/util/configlexer.lex b/util/configlexer.lex index 31a37d50d..da4ce6ff1 100644 --- a/util/configlexer.lex +++ b/util/configlexer.lex @@ -586,6 +586,7 @@ edns-client-string{COLON} { YDVAR(2, VAR_EDNS_CLIENT_STRING) } edns-client-string-opcode{COLON} { YDVAR(1, VAR_EDNS_CLIENT_STRING_OPCODE) } nsid{COLON} { YDVAR(1, VAR_NSID ) } ede{COLON} { YDVAR(1, VAR_EDE ) } +dns-error-reporting{COLON} { YDVAR(1, VAR_DNS_ERROR_REPORTING ) } proxy-protocol-port{COLON} { YDVAR(1, VAR_PROXY_PROTOCOL_PORT) } {NEWLINE} { LEXOUT(("NL\n")); cfg_parser->line++; } diff --git a/util/configparser.y b/util/configparser.y index cf026bdad..c62197598 100644 --- a/util/configparser.y +++ b/util/configparser.y @@ -200,6 +200,7 @@ extern struct config_parser_state* cfg_parser; %token VAR_EDNS_CLIENT_STRING_OPCODE VAR_NSID %token VAR_ZONEMD_PERMISSIVE_MODE VAR_ZONEMD_CHECK VAR_ZONEMD_REJECT_ABSENCE %token VAR_RPZ_SIGNAL_NXDOMAIN_RA VAR_INTERFACE_AUTOMATIC_PORTS VAR_EDE +%token VAR_DNS_ERROR_REPORTING %token VAR_INTERFACE_ACTION VAR_INTERFACE_VIEW VAR_INTERFACE_TAG %token VAR_INTERFACE_TAG_ACTION VAR_INTERFACE_TAG_DATA %token VAR_PROXY_PROTOCOL_PORT VAR_STATISTICS_INHIBIT_ZERO @@ -340,6 +341,7 @@ content_server: server_num_threads | server_verbosity | server_port | server_zonemd_permissive_mode | server_max_reuse_tcp_queries | server_tcp_reuse_timeout | server_tcp_auth_query_timeout | server_interface_automatic_ports | server_ede | + server_dns_error_reporting | server_proxy_protocol_port | server_statistics_inhibit_zero | server_harden_unknown_additional | server_disable_edns_do | server_log_destaddr @@ -3016,6 +3018,15 @@ server_ede: VAR_EDE STRING_ARG free($2); } ; +server_dns_error_reporting: VAR_DNS_ERROR_REPORTING STRING_ARG + { + OUTYY(("P(server_dns_error_reporting:%s)\n", $2)); + if(strcmp($2, "yes") != 0 && strcmp($2, "no") != 0) + yyerror("expected yes or no."); + else cfg_parser->cfg->dns_error_reporting = (strcmp($2, "yes")==0); + free($2); + } + ; server_proxy_protocol_port: VAR_PROXY_PROTOCOL_PORT STRING_ARG { OUTYY(("P(server_proxy_protocol_port:%s)\n", $2));