diff --git a/daemon/remote.c b/daemon/remote.c index df1f0d4e6..50bdefd68 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -824,6 +824,8 @@ print_stats(RES* ssl, const char* nm, struct ub_stats_info* s) if(!ssl_printf(ssl, "%s.num.dnscrypt.malformed"SQ"%lu\n", nm, (unsigned long)s->svr.num_query_dnscrypt_crypted_malformed)) return 0; #endif + if(!ssl_printf(ssl, "%s.num.dns_error_reports"SQ"%lu\n", nm, + (unsigned long)s->svr.num_dns_error_reports)) return 0; if(!ssl_printf(ssl, "%s.requestlist.avg"SQ"%g\n", nm, (s->svr.num_queries_missed_cache+s->svr.num_queries_prefetch)? (double)s->svr.sum_query_list_size/ @@ -5639,6 +5641,7 @@ fr_atomic_copy_cfg(struct config_file* oldcfg, struct config_file* cfg, COPY_VAR_int(serve_expired_reply_ttl); COPY_VAR_int(serve_expired_client_timeout); COPY_VAR_int(ede_serve_expired); + COPY_VAR_int(dns_error_reporting); COPY_VAR_int(serve_original_ttl); COPY_VAR_ptr(val_nsec3_key_iterations); COPY_VAR_int(zonemd_permissive_mode); diff --git a/daemon/stats.c b/daemon/stats.c index 3f2d848b3..7efb83a0b 100644 --- a/daemon/stats.c +++ b/daemon/stats.c @@ -285,6 +285,8 @@ server_stats_compile(struct worker* worker, struct ub_stats_info* s, int reset) (long long)worker->env.mesh->num_queries_discard_timeout; s->svr.num_queries_wait_limit += (long long)worker->env.mesh->num_queries_wait_limit; + s->svr.num_dns_error_reports += + (long long)worker->env.mesh->num_dns_error_reports; /* values from outside network */ s->svr.unwanted_replies = (long long)worker->back->unwanted_replies; s->svr.qtcp_outgoing = (long long)worker->back->num_tcp_outgoing; @@ -446,6 +448,7 @@ void server_stats_add(struct ub_stats_info* total, struct ub_stats_info* a) total->svr.num_queries_discard_timeout += a->svr.num_queries_discard_timeout; total->svr.num_queries_wait_limit += a->svr.num_queries_wait_limit; + total->svr.num_dns_error_reports += a->svr.num_dns_error_reports; total->svr.num_queries_missed_cache += a->svr.num_queries_missed_cache; total->svr.num_queries_prefetch += a->svr.num_queries_prefetch; total->svr.num_queries_timed_out += a->svr.num_queries_timed_out; @@ -458,9 +461,9 @@ void server_stats_add(struct ub_stats_info* total, struct ub_stats_info* a) #ifdef USE_DNSCRYPT total->svr.num_query_dnscrypt_crypted += a->svr.num_query_dnscrypt_crypted; total->svr.num_query_dnscrypt_cert += a->svr.num_query_dnscrypt_cert; - total->svr.num_query_dnscrypt_cleartext += \ + total->svr.num_query_dnscrypt_cleartext += a->svr.num_query_dnscrypt_cleartext; - total->svr.num_query_dnscrypt_crypted_malformed += \ + total->svr.num_query_dnscrypt_crypted_malformed += a->svr.num_query_dnscrypt_crypted_malformed; #endif /* USE_DNSCRYPT */ /* the max size reached is upped to higher of both */ diff --git a/doc/example.conf.in b/doc/example.conf.in index 6eabbe5fd..fdef9ef37 100644 --- a/doc/example.conf.in +++ b/doc/example.conf.in @@ -1086,6 +1086,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-control.8.in b/doc/unbound-control.8.in index 22a121414..6c0cdc217 100644 --- a/doc/unbound-control.8.in +++ b/doc/unbound-control.8.in @@ -534,6 +534,9 @@ request for certificates. .I threadX.num.dnscrypt.malformed number of request that were neither cleartext, not valid dnscrypt messages. .TP +.I threadX.num.dns_error_reports +number of DNS Error Reports generated by thread +.TP .I threadX.num.prefetch number of cache prefetches performed. This number is included in cachehits, as the original query had the unprefetched answer from cache, @@ -628,6 +631,9 @@ summed over threads. .I total.num.dnscrypt.malformed summed over threads. .TP +.I total.num.dns_error_reports +summed over threads. +.TP .I total.num.prefetch summed over threads. .TP diff --git a/doc/unbound.conf.5.in b/doc/unbound.conf.5.in index e65125a63..21dbd73e6 100644 --- a/doc/unbound.conf.5.in +++ b/doc/unbound.conf.5.in @@ -2089,17 +2089,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/iterator/iterator.c b/iterator/iterator.c index 8c0703e9e..e64dfa61b 100644 --- a/iterator/iterator.c +++ b/iterator/iterator.c @@ -4332,6 +4332,7 @@ process_response(struct module_qstate* qstate, struct iter_qstate* iq, } /* Copy the edns options we may got from the back end */ + qstate->edns_opts_back_in = NULL; if(edns.opt_list_in) { qstate->edns_opts_back_in = edns_opt_copy_region(edns.opt_list_in, qstate->region); diff --git a/libunbound/unbound.h b/libunbound/unbound.h index 8a1625b9f..bdcf4edec 100644 --- a/libunbound/unbound.h +++ b/libunbound/unbound.h @@ -853,6 +853,8 @@ struct ub_server_stats { long long num_queries_discard_timeout; /** number of queries removed due to wait-limit */ long long num_queries_wait_limit; + /** number of dns error reports generated */ + long long num_dns_error_reports; }; /** diff --git a/services/mesh.c b/services/mesh.c index b62aa5c17..1d19e7c7d 100644 --- a/services/mesh.c +++ b/services/mesh.c @@ -232,6 +232,7 @@ mesh_create(struct module_stack* stack, struct module_env* env) mesh->ans_cachedb = 0; mesh->num_queries_discard_timeout = 0; mesh->num_queries_wait_limit = 0; + mesh->num_dns_error_reports = 0; mesh->max_reply_states = env->cfg->num_queries_per_thread; mesh->max_forever_states = (mesh->max_reply_states+1)/2; #ifndef S_SPLINT_S @@ -1582,6 +1583,117 @@ 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; + + /* We need a valid reporting agent; + * this is based on qstate->edns_opts_back_in that will probably have + * the latest reporting agent we found while iterating */ + 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; + } + + /* Get the EDE generated from the mesh state, these are mostly + * validator errors. If other errors are produced in the future (e.g., + * RPZ) we would not want them to result in error reports. */ + 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 || + /* other, does not make sense without the text that comes + * with it */ + reason_bogus == LDNS_EDE_OTHER) 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); + if(mesh_add_sub(qstate, &qinfo, BIT_RD, 0, 0, &newq, &sub)) { + qstate->env->mesh->num_dns_error_reports++; + } + 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; @@ -1610,6 +1722,10 @@ void mesh_query_done(struct mesh_state* mstate) if(err) { log_err("%s", err); } } } + + if(mstate->reply_list && 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); @@ -2156,6 +2272,7 @@ mesh_stats_clear(struct mesh_area* mesh) mesh->ans_nodata = 0; mesh->num_queries_discard_timeout = 0; mesh->num_queries_wait_limit = 0; + mesh->num_dns_error_reports = 0; } size_t diff --git a/services/mesh.h b/services/mesh.h index 0b01d4ef8..fd17c05da 100644 --- a/services/mesh.h +++ b/services/mesh.h @@ -141,6 +141,8 @@ struct mesh_area { size_t num_queries_discard_timeout; /** stats, number of queries removed due to wait-limit */ size_t num_queries_wait_limit; + /** stats, number of dns error reports generated */ + size_t num_dns_error_reports; /** backup of query if other operations recurse and need the * network buffers */ diff --git a/sldns/rrdef.h b/sldns/rrdef.h index 24abec622..540468889 100644 --- a/sldns/rrdef.h +++ b/sldns/rrdef.h @@ -443,6 +443,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/smallapp/unbound-control.c b/smallapp/unbound-control.c index dcbe66030..0136b5e4e 100644 --- a/smallapp/unbound-control.c +++ b/smallapp/unbound-control.c @@ -244,12 +244,13 @@ static void pr_stats(const char* nm, struct ub_stats_info* s) PR_UL_NM("num.expired", s->svr.ans_expired); PR_UL_NM("num.recursivereplies", s->mesh_replies_sent); #ifdef USE_DNSCRYPT - PR_UL_NM("num.dnscrypt.crypted", s->svr.num_query_dnscrypt_crypted); - PR_UL_NM("num.dnscrypt.cert", s->svr.num_query_dnscrypt_cert); - PR_UL_NM("num.dnscrypt.cleartext", s->svr.num_query_dnscrypt_cleartext); - PR_UL_NM("num.dnscrypt.malformed", - s->svr.num_query_dnscrypt_crypted_malformed); + PR_UL_NM("num.dnscrypt.crypted", s->svr.num_query_dnscrypt_crypted); + PR_UL_NM("num.dnscrypt.cert", s->svr.num_query_dnscrypt_cert); + PR_UL_NM("num.dnscrypt.cleartext", s->svr.num_query_dnscrypt_cleartext); + PR_UL_NM("num.dnscrypt.malformed", + s->svr.num_query_dnscrypt_crypted_malformed); #endif /* USE_DNSCRYPT */ + PR_UL_NM("num.dns_error_reports", s->svr.num_dns_error_reports); printf("%s.requestlist.avg"SQ"%g\n", nm, (s->svr.num_queries_missed_cache+s->svr.num_queries_prefetch)? (double)s->svr.sum_query_list_size/ diff --git a/testdata/dns_error_reporting.rpl b/testdata/dns_error_reporting.rpl new file mode 100644 index 000000000..f1fac12a2 --- /dev/null +++ b/testdata/dns_error_reporting.rpl @@ -0,0 +1,200 @@ +; 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 + do-ip6: no + +stub-zone: + name: domain + stub-addr: 0.0.0.0 +stub-zone: + name: an.agent + stub-addr: 0.0.0.2 +CONFIG_END + +SCENARIO_BEGIN Test DNS Error Reporting + +; domain +RANGE_BEGIN 0 100 + ADDRESS 0.0.0.0 + ENTRY_BEGIN + MATCH opcode qtype qname + ADJUST copy_id + REPLY QR NOERROR + SECTION QUESTION + a.domain. IN A + SECTION AUTHORITY + a.domain. IN NS ns.a.domain. + SECTION ADDITIONAL + ns.a.domain. IN A 0.0.0.1 + 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 + +; 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; +; 'domain' did give an error reporting agent, but the latest upstream +; 'a.domain' did not) +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/testdata/stat_values.tdir/stat_values.conf b/testdata/stat_values.tdir/stat_values.conf index d1adff58c..312a7e174 100644 --- a/testdata/stat_values.tdir/stat_values.conf +++ b/testdata/stat_values.tdir/stat_values.conf @@ -15,6 +15,9 @@ server: root-key-sentinel: no trust-anchor-signaling: no serve-expired-client-timeout: 0 + dns-error-reporting: yes + + trust-anchor: "bogusdnssec. DS 1444 8 2 5224fb17d630a2e3efdc863a05a4032c5db415b5de3f32472ee9abed42e10146" local-zone: local.zone static local-data: "www.local.zone A 192.0.2.1" @@ -30,6 +33,12 @@ remote-control: stub-zone: name: "example.com." stub-addr: "127.0.0.1@@TOPORT@" +stub-zone: + name: "bogusdnssec." + stub-addr: "127.0.0.1@@TOPORT@" +stub-zone: + name: "an.agent." + stub-addr: "127.0.0.1@@TOPORT@" stub-zone: name: "expired." stub-addr: "127.0.0.1@@EXPIREDPORT@" diff --git a/testdata/stat_values.tdir/stat_values.test b/testdata/stat_values.tdir/stat_values.test index 456d27cb8..d538e4d60 100644 --- a/testdata/stat_values.tdir/stat_values.test +++ b/testdata/stat_values.tdir/stat_values.test @@ -426,6 +426,35 @@ rrset.cache.count=3 infra.cache.count=2" +teststep "Check dns-error-reporting." +echo "> dig www.bogusdnssec." +dig @127.0.0.1 -p $UNBOUND_PORT www.bogusdnssec. | tee outfile +echo "> check answer" +if grep "SERVFAIL" outfile; then + echo "OK" +else + end 1 +fi +check_stats "\ +infra.cache.count=4 +key.cache.count=1 +msg.cache.count=7 +num.answer.bogus=1 +num.answer.rcode.SERVFAIL=1 +num.query.class.IN=1 +num.query.edns.present=1 +num.query.flags.AD=1 +num.query.flags.RD=1 +num.query.opcode.QUERY=1 +num.query.type.A=1 +num.query.udpout=9 +rrset.cache.count=4 +total.num.cachemiss=1 +total.num.dns_error_reports=1 +total.num.queries=1 +total.num.recursivereplies=1" + + ### # # Bring the discard-timeout, wait-limit configured Unbound up @@ -436,8 +465,8 @@ bring_up_alternate_configuration ub_discard_wait_limit.conf teststep "Check discard-timeout and wait-limit" -echo "> dig www.slow" -dig @127.0.0.1 -p $UNBOUND_PORT +retry=2 +timeout=1 www.slow. | tee outfile +echo "> dig www.unresponsive" +dig @127.0.0.1 -p $UNBOUND_PORT +retry=2 +timeout=1 www.unresponsive. | tee outfile echo "> check answer" if grep "no servers could be reached" outfile; then echo "OK" diff --git a/testdata/stat_values.tdir/stat_values.testns b/testdata/stat_values.tdir/stat_values.testns index 906c49f2b..a5c0ae92b 100644 --- a/testdata/stat_values.tdir/stat_values.testns +++ b/testdata/stat_values.tdir/stat_values.testns @@ -32,14 +32,51 @@ SECTION ANSWER 0ttl 0 IN A 0.0.0.1 ENTRY_END -$ORIGIN slow. + + +$ORIGIN bogusdnssec. ENTRY_BEGIN MATCH opcode qtype qname REPLY QR AA NOERROR -ADJUST copy_id sleep=2 +ADJUST copy_id SECTION QUESTION -www. IN A +@ IN DNSKEY SECTION ANSWER -www. 0 IN A 10.20.30.40 ENTRY_END + +ENTRY_BEGIN +MATCH opcode qtype qname +REPLY QR AA NOERROR +ADJUST copy_id +SECTION QUESTION +www IN A +SECTION ANSWER +www 0 IN A 10.20.30.40 +; bogus signature to not trigger LAME DNSSEC and continue with validation +www 0 IN RRSIG A 8 2 240 20250429005000 20250401005000 42393 bogusdnssec. ob6ddTJkdeOUn92cxx1NPGneV7rhOp2zKBv8FXQjJ/Wso8LJJnzRHW9p 3sTatlzi+UdRi7BOrcxwjUG38lgO+TS5vRFGAiTRmOezm6xJVNTg8lIb RJGCD5bRtRRstwt31Qt6Gda+6sAyvDebpUB/opkQpevv6xohdrhr0g8+ Q4w= +SECTION ADDITIONAL +; dns error reporting agent +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 + + + +$ORIGIN an.agent. +;just give an answer back to anything +ENTRY_BEGIN +MATCH opcode subdomain +REPLY QR AA NXDOMAIN +ADJUST copy_id copy_query +SECTION QUESTION +an.agent. IN ANY +ENTRY_END + + + +$ORIGIN unresponsive. +;; no entry for 'unresponsive.', we rely on timeouts. diff --git a/testdata/stat_values.tdir/stat_values_discard_wait_limit.conf b/testdata/stat_values.tdir/stat_values_discard_wait_limit.conf index d4a3459b8..b6f63cf17 100644 --- a/testdata/stat_values.tdir/stat_values_discard_wait_limit.conf +++ b/testdata/stat_values.tdir/stat_values_discard_wait_limit.conf @@ -32,5 +32,5 @@ remote-control: control-key-file: "unbound_control.key" control-cert-file: "unbound_control.pem" stub-zone: - name: "slow." + name: "unresponsive." stub-addr: "127.0.0.1@@TOPORT@" diff --git a/util/config_file.c b/util/config_file.c index a24067060..81bffa8d8 100644 --- a/util/config_file.c +++ b/util/config_file.c @@ -284,7 +284,6 @@ config_create(void) cfg->serve_expired_ttl_reset = 0; cfg->serve_expired_reply_ttl = 30; cfg->serve_expired_client_timeout = 1800; - cfg->ede_serve_expired = 0; cfg->serve_original_ttl = 0; cfg->zonemd_permissive_mode = 0; cfg->add_holddown = 30*24*3600; @@ -418,6 +417,8 @@ config_create(void) cfg->ipset_name_v6 = NULL; #endif cfg->ede = 0; + cfg->ede_serve_expired = 0; + cfg->dns_error_reporting = 0; cfg->iter_scrub_ns = 20; cfg->iter_scrub_cname = 11; cfg->max_global_quota = 200; @@ -756,6 +757,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_NUMBER_OR_ZERO("iter-scrub-ns:", iter_scrub_ns) else S_NUMBER_OR_ZERO("iter-scrub-cname:", iter_scrub_cname) else S_NUMBER_OR_ZERO("max-global-quota:", max_global_quota) @@ -1231,6 +1233,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_DEC(opt, "iter-scrub-ns", iter_scrub_ns) else O_DEC(opt, "iter-scrub-cname", iter_scrub_cname) else O_DEC(opt, "max-global-quota", max_global_quota) diff --git a/util/config_file.h b/util/config_file.h index a5d73f4c6..89bbc1c7d 100644 --- a/util/config_file.h +++ b/util/config_file.h @@ -438,8 +438,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 */ @@ -784,6 +782,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; /** limit on NS RRs in RRset for the iterator scrubber. */ size_t iter_scrub_ns; /** limit on CNAME, DNAME RRs in answer for the iterator scrubber. */ diff --git a/util/configlexer.lex b/util/configlexer.lex index 1b9eaa35b..bc258673d 100644 --- a/util/configlexer.lex +++ b/util/configlexer.lex @@ -601,6 +601,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) } iter-scrub-ns{COLON} { YDVAR(1, VAR_ITER_SCRUB_NS) } iter-scrub-cname{COLON} { YDVAR(1, VAR_ITER_SCRUB_CNAME) } diff --git a/util/configparser.y b/util/configparser.y index af47b0eb7..ebb23f41c 100644 --- a/util/configparser.y +++ b/util/configparser.y @@ -206,6 +206,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_QUIC_PORT VAR_QUIC_SIZE @@ -350,6 +351,7 @@ content_server: server_num_threads | server_verbosity | server_port | server_tcp_reuse_timeout | server_tcp_auth_query_timeout | server_quic_port | server_quic_size | 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 | server_cookie_secret_file | @@ -3073,6 +3075,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));