]> git.ipfire.org Git - thirdparty/unbound.git/commitdiff
DNS Error Reporting (RFC 9567) (#902)
authorWillem Toorop <willem@nlnetlabs.nl>
Mon, 7 Apr 2025 08:25:10 +0000 (10:25 +0200)
committerGitHub <noreply@github.com>
Mon, 7 Apr 2025 08:25:10 +0000 (10:25 +0200)
* v1 EDER poc

* remove superfluous edns_list_get_option function

* create an EDER configurable

* Hackathon 114

* Fixes for version -04

* Generated configparser and configlexer are not versioned in master anymore

* Remove NOERROR DNS Error Reporting; not part of final RFC.
* Use assigned IANA EDNS0 Option Code for Report-Channel.

* Fix buffer protection and agent domain validity

* Use DNS Error Reporting instead of the eder nickname

* Update documentation.

* Fix typo.

* Bail out early if ede is not present.

* Forget previous EDNS options from upstream; this is what was
  implicitly happening but not deterministacally.

* Don't report LDNS_EDE_OTHER and bail early if there is no reporting
  agent.

* Only do DNS error reporting when a client asked for something that
  went wrong.

* Add an error reporting agent in the parent that should be ignored.

* review feedback.

* fixup for fast reload

* Add 'num.dns_error_reports' to stats and test for it.

---------

Co-authored-by: TCY16 <tom@nlnetlabs.nl>
Co-authored-by: Yorgos Thessalonikefs <yorgos@nlnetlabs.nl>
20 files changed:
daemon/remote.c
daemon/stats.c
doc/example.conf.in
doc/unbound-control.8.in
doc/unbound.conf.5.in
iterator/iterator.c
libunbound/unbound.h
services/mesh.c
services/mesh.h
sldns/rrdef.h
smallapp/unbound-control.c
testdata/dns_error_reporting.rpl [new file with mode: 0644]
testdata/stat_values.tdir/stat_values.conf
testdata/stat_values.tdir/stat_values.test
testdata/stat_values.tdir/stat_values.testns
testdata/stat_values.tdir/stat_values_discard_wait_limit.conf
util/config_file.c
util/config_file.h
util/configlexer.lex
util/configparser.y

index df1f0d4e69b7529f12f2a926c005500590fc56ea..50bdefd68b26d6a6215a956b027b04abd15d4bca 100644 (file)
@@ -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);
index 3f2d848b33135c0b68752c5ffceb74a218916c54..7efb83a0bc3f6fb71dd96f4420b904da16899978 100644 (file)
@@ -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 */
index 6eabbe5fd970faf597d0f55b475831713866b9be..fdef9ef37917658796f55113f6a35b5466412a14 100644 (file)
@@ -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.
        #
index 22a1214144922318eddc4d703b03fda1c75f85e0..6c0cdc21732b229199d5a34f0a5f05fcde9c9135 100644 (file)
@@ -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
index e65125a63721b58f5cffa15cb53c390b6d31cacd..21dbd73e6f8c6d85ee5f7df63880fe06fff71c2c 100644 (file)
@@ -2089,17 +2089,30 @@ be used.  Default is 65001.
 .TP 5
 .B ede: \fI<yes or no>
 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<yes or no>
 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<yes or no>
+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
index 8c0703e9e44f5fd817b6cdefcee2ef5369765343..e64dfa61ba2dc8bca5ff458d47b40beb4562808e 100644 (file)
@@ -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);
index 8a1625b9f9e4c34f2d593cd4c0bd1256c610d10e..bdcf4edeca5f42cba335aae5e77d90b8a8d3fc94 100644 (file)
@@ -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;
 };
 
 /**
index b62aa5c17c59a85ad2ec98036fa20be65ec720ea..1d19e7c7db50bb8b4a38842d4ae708424007a4f1 100644 (file)
@@ -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
index 0b01d4ef816e0ad884aab2299e81f178f9db0e9a..fd17c05da6d424dfa9a52144431af461985b1fb0 100644 (file)
@@ -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 */
index 24abec622e1f6c36f9784d70d4746ed69bf394ff..5404688898801e9f8c50d0e46b2b9308adeaac42 100644 (file)
@@ -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;
index dcbe6603028edc1040cfc899758f11ff081991c5..0136b5e4eb67c5d0bdb538afac2fe4f29d61f2de 100644 (file)
@@ -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 (file)
index 0000000..f1fac12
--- /dev/null
@@ -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
index d1adff58c1ddccd12680c5941d9a7b44d91a27a1..312a7e17494f775569af72110e8b510feb6945eb 100644 (file)
@@ -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@"
index 456d27cb8299d46fa242f92b5ebc03180e904aed..d538e4d60ec296fc95eebc1b74240893d5ddd6e1 100644 (file)
@@ -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"
index 906c49f2bd8e5ced96f0fe212ebcd01289615fd0..a5c0ae92b5997892d08ad5f56027caebbf664a34 100644 (file)
@@ -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.
index d4a3459b8dad22a3ccba29b3aa2c4b24f09dd4ca..b6f63cf17cfdf9d4daca471d26901daca4fa9722 100644 (file)
@@ -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@"
index a24067060e8a998da6b4724351595d282ae4280b..81bffa8d88aab1f72920f5080039767810163c2c 100644 (file)
@@ -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)
index a5d73f4c6069dd2e8df6c0181c8c5ba8a95fa8b6..89bbc1c7d856cc3c35799f50d22fdc2cd2874117 100644 (file)
@@ -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. */
index 1b9eaa35bb80aef22383c962344305722a061914..bc258673d712c0e1e23b4d91cae635484dc71acb 100644 (file)
@@ -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) }
index af47b0eb71dc65d5e942a4dbdd8ac00468ea9d7c..ebb23f41cbd359044c4f94f7cc91389a2fb81f53 100644 (file)
@@ -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));