summaryrefslogtreecommitdiffstats
path: root/lib/layer
diff options
context:
space:
mode:
Diffstat (limited to 'lib/layer')
-rw-r--r--lib/layer/cache.c20
-rw-r--r--lib/layer/iterate.c1254
-rw-r--r--lib/layer/iterate.h25
-rw-r--r--lib/layer/mode.rst26
-rw-r--r--lib/layer/test.integr/deckard.yaml13
-rw-r--r--lib/layer/test.integr/iter_cname_length.rpl226
-rw-r--r--lib/layer/test.integr/iter_limit_bad_glueless.rpl220
-rw-r--r--lib/layer/test.integr/iter_limit_refuse.rpl150
-rw-r--r--lib/layer/test.integr/kresd_config.j2107
-rw-r--r--lib/layer/validate.c1396
-rw-r--r--lib/layer/validate.test.integr/deckard.yaml10
-rw-r--r--lib/layer/validate.test.integr/fwd_insecure_but_rrsig_signer_invalid.rpl294
-rw-r--r--lib/layer/validate.test.integr/kresd_config.j252
13 files changed, 3793 insertions, 0 deletions
diff --git a/lib/layer/cache.c b/lib/layer/cache.c
new file mode 100644
index 0000000..2f1ba60
--- /dev/null
+++ b/lib/layer/cache.c
@@ -0,0 +1,20 @@
+/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "lib/module.h"
+#include "lib/cache/api.h"
+
+/** Module implementation. */
+int cache_init(struct kr_module *self)
+{
+ static const kr_layer_api_t layer = {
+ .produce = &cache_peek,
+ .consume = &cache_stash,
+ };
+ self->layer = &layer;
+ return kr_ok();
+}
+
+KR_MODULE_EXPORT(cache) /* useless for builtin module, but let's be consistent */
+
diff --git a/lib/layer/iterate.c b/lib/layer/iterate.c
new file mode 100644
index 0000000..dfb7c87
--- /dev/null
+++ b/lib/layer/iterate.c
@@ -0,0 +1,1254 @@
+/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+/** @file iterate.c
+ *
+ * This builtin module is mainly active in the consume phase.
+ * Primary responsibilities:
+ * - Classify the packet as auth/nonauth and change its AA flag accordingly.
+ * - Pick interesting RRs to kr_request::answ_selected and ::auth_selected,
+ * NEW: and classify their rank, except for validation status.
+ * - Update kr_query::zone_cut (in case of referral).
+ * - Interpret CNAMEs.
+ * - Prepare the followup query - either inline or as another kr_query
+ * (CNAME jumps create a new "sibling" query).
+ */
+
+#include <sys/time.h>
+#include <arpa/inet.h>
+
+#include <contrib/cleanup.h>
+#include <libknot/descriptor.h>
+#include <libknot/rrtype/rdname.h>
+#include <libknot/rrtype/rrsig.h>
+
+#include "lib/layer/iterate.h"
+#include "lib/resolve.h"
+#include "lib/rplan.h"
+#include "lib/defines.h"
+#include "lib/selection.h"
+#include "lib/module.h"
+#include "lib/dnssec/ta.h"
+
+#define VERBOSE_MSG(...) kr_log_q(req->current_query, ITERATOR, __VA_ARGS__)
+#define QVERBOSE_MSG(qry, ...) kr_log_q(qry, ITERATOR, __VA_ARGS__)
+#define WITH_VERBOSE(qry) if (kr_log_is_debug_qry(ITERATOR, (qry)))
+
+/* Iterator often walks through packet section, this is an abstraction. */
+typedef int (*rr_callback_t)(const knot_rrset_t *, unsigned, struct kr_request *);
+
+/** Return minimized QNAME/QTYPE for current zone cut. */
+static const knot_dname_t *minimized_qname(struct kr_query *query, uint16_t *qtype)
+{
+ /* Minimization disabled. */
+ const knot_dname_t *qname = query->sname;
+ if (qname[0] == '\0' || query->flags.NO_MINIMIZE || query->flags.STUB) {
+ return qname;
+ }
+
+ /* Minimize name to contain current zone cut + 1 label. */
+ int cut_labels = knot_dname_labels(query->zone_cut.name, NULL);
+ int qname_labels = knot_dname_labels(qname, NULL);
+ while(qname[0] && qname_labels > cut_labels + 1) {
+ qname = knot_wire_next_label(qname, NULL);
+ qname_labels -= 1;
+ }
+
+ /* Hide QTYPE if minimized. */
+ if (qname != query->sname) {
+ *qtype = KNOT_RRTYPE_NS;
+ }
+
+ return qname;
+}
+
+/** Answer is paired to query. */
+static bool is_paired_to_query(const knot_pkt_t *answer, struct kr_query *query)
+{
+ uint16_t qtype = query->stype;
+ const knot_dname_t *qname = minimized_qname(query, &qtype);
+
+ /* ID should already match, thanks to session_tasklist_del_msgid()
+ * in worker_submit(), but it won't hurt to check again. */
+ return query->id == knot_wire_get_id(answer->wire) &&
+ knot_wire_get_qdcount(answer->wire) == 1 &&
+ query->sclass == knot_pkt_qclass(answer) &&
+ qtype == knot_pkt_qtype(answer) &&
+ /* qry->secret had been xor-applied to answer already,
+ * so this also checks for correctness of case randomization */
+ knot_dname_is_equal(qname, kr_pkt_qname_raw(answer));
+}
+
+/** Relaxed rule for AA, either AA=1 or SOA matching zone cut is required. */
+static bool is_authoritative(const knot_pkt_t *answer, struct kr_query *query)
+{
+ if (knot_wire_get_aa(answer->wire)) {
+ return true;
+ }
+
+ const knot_pktsection_t *ns = knot_pkt_section(answer, KNOT_AUTHORITY);
+ for (unsigned i = 0; i < ns->count; ++i) {
+ const knot_rrset_t *rr = knot_pkt_rr(ns, i);
+ if (rr->type == KNOT_RRTYPE_SOA
+ && knot_dname_in_bailiwick(rr->owner, query->zone_cut.name) >= 0) {
+ return true;
+ }
+ }
+
+#ifndef STRICT_MODE
+ /* Last resort to work around broken auths, if the zone cut is at the QNAME. */
+ if (knot_dname_is_equal(query->zone_cut.name, knot_pkt_qname(answer))) {
+ return true;
+ }
+#endif
+
+ /* Some authoritative servers are hopelessly broken, allow lame answers in permissive mode. */
+ if (query->flags.PERMISSIVE) {
+ return true;
+ }
+
+ return false;
+}
+
+int kr_response_classify(const knot_pkt_t *pkt)
+{
+ const knot_pktsection_t *an = knot_pkt_section(pkt, KNOT_ANSWER);
+ switch (knot_wire_get_rcode(pkt->wire)) {
+ case KNOT_RCODE_NOERROR:
+ return (an->count == 0) ? PKT_NODATA : PKT_NOERROR;
+ case KNOT_RCODE_NXDOMAIN:
+ return PKT_NXDOMAIN;
+ case KNOT_RCODE_REFUSED:
+ return PKT_REFUSED;
+ default:
+ return PKT_ERROR;
+ }
+}
+
+/** @internal Filter ANY or loopback addresses. */
+static bool is_valid_addr(const uint8_t *addr, size_t len)
+{
+ if (len == sizeof(struct in_addr)) {
+ /* Filter ANY and 127.0.0.0/8 */
+ uint32_t ip_host; /* Memcpy is safe for unaligned case (on non-x86) */
+ memcpy(&ip_host, addr, sizeof(ip_host));
+ ip_host = ntohl(ip_host);
+ if (ip_host == 0 || (ip_host & 0xff000000) == 0x7f000000) {
+ return false;
+ }
+ } else if (len == sizeof(struct in6_addr)) {
+ struct in6_addr ip6_mask;
+ memset(&ip6_mask, 0, sizeof(ip6_mask));
+ /* All except last byte are zeroed, last byte defines ANY/::1 */
+ if (memcmp(addr, ip6_mask.s6_addr, sizeof(ip6_mask.s6_addr) - 1) == 0) {
+ return (addr[len - 1] > 1);
+ }
+ }
+ return true;
+}
+
+/** @internal Update NS address from record \a rr. Return _FAIL on error. */
+static int update_nsaddr(const knot_rrset_t *rr, struct kr_query *query, int *glue_cnt)
+{
+ if (rr->type == KNOT_RRTYPE_A || rr->type == KNOT_RRTYPE_AAAA) {
+ const knot_rdata_t *rdata = rr->rrs.rdata;
+ const int a_len = rr->type == KNOT_RRTYPE_A
+ ? sizeof(struct in_addr) : sizeof(struct in6_addr);
+ if (a_len != rdata->len) {
+ QVERBOSE_MSG(query, "<= ignoring invalid glue, length %d != %d\n",
+ (int)rdata->len, a_len);
+ return KR_STATE_FAIL;
+ }
+ char name_str[KR_DNAME_STR_MAXLEN];
+ char addr_str[INET6_ADDRSTRLEN];
+ WITH_VERBOSE(query) {
+ const int af = (rr->type == KNOT_RRTYPE_A) ? AF_INET : AF_INET6;
+ knot_dname_to_str(name_str, rr->owner, sizeof(name_str));
+ name_str[sizeof(name_str) - 1] = 0;
+ inet_ntop(af, rdata->data, addr_str, sizeof(addr_str));
+ }
+ if (!(query->flags.ALLOW_LOCAL) &&
+ !is_valid_addr(rdata->data, rdata->len)) {
+ QVERBOSE_MSG(query, "<= ignoring invalid glue for "
+ "'%s': '%s'\n", name_str, addr_str);
+ return KR_STATE_CONSUME; /* Ignore invalid addresses */
+ }
+ int ret = kr_zonecut_add(&query->zone_cut, rr->owner, rdata->data, rdata->len);
+ if (ret != 0) {
+ return KR_STATE_FAIL;
+ }
+
+ ++*glue_cnt; /* reduced verbosity */
+ /* QVERBOSE_MSG(query, "<= using glue for "
+ "'%s': '%s'\n", name_str, addr_str);
+ */
+ }
+ return KR_STATE_CONSUME;
+}
+
+enum { GLUE_COUNT_THROTTLE = 26 };
+
+/** @internal From \a pkt, fetch glue records for name \a ns, and update the cut etc.
+ *
+ * \param glue_cnt the number of accepted addresses (to be incremented)
+ */
+static void fetch_glue(knot_pkt_t *pkt, const knot_dname_t *ns, bool in_bailiwick,
+ struct kr_request *req, const struct kr_query *qry, int *glue_cnt)
+{
+ ranked_rr_array_t *selected[] = kr_request_selected(req);
+ for (knot_section_t i = KNOT_ANSWER; i <= KNOT_ADDITIONAL; ++i) {
+ const knot_pktsection_t *sec = knot_pkt_section(pkt, i);
+ for (unsigned k = 0; k < sec->count; ++k) {
+ const knot_rrset_t *rr = knot_pkt_rr(sec, k);
+ if (!knot_dname_is_equal(ns, rr->owner)) {
+ continue;
+ }
+ if ((rr->type != KNOT_RRTYPE_A) &&
+ (rr->type != KNOT_RRTYPE_AAAA)) {
+ continue;
+ }
+
+ uint8_t rank = (in_bailiwick && i == KNOT_ANSWER)
+ ? (KR_RANK_INITIAL | KR_RANK_AUTH) : KR_RANK_OMIT;
+ (void) kr_ranked_rrarray_add(selected[i], rr, rank,
+ false, qry->uid, &req->pool);
+
+ if ((rr->type == KNOT_RRTYPE_A) &&
+ (req->ctx->options.NO_IPV4)) {
+ QVERBOSE_MSG(qry, "<= skipping IPv4 glue due to network settings\n");
+ continue;
+ }
+ if ((rr->type == KNOT_RRTYPE_AAAA) &&
+ (req->ctx->options.NO_IPV6)) {
+ QVERBOSE_MSG(qry, "<= skipping IPv6 glue due to network settings\n");
+ continue;
+ }
+ (void) update_nsaddr(rr, req->current_query, glue_cnt);
+ /* If we reach limit on total glue addresses,
+ * we only load the first one per NS name (the one just above). */
+ if (*glue_cnt > GLUE_COUNT_THROTTLE)
+ break;
+ }
+ }
+}
+
+/** Attempt to find glue for given nameserver name (best effort). */
+static bool has_glue(knot_pkt_t *pkt, const knot_dname_t *ns)
+{
+ for (knot_section_t i = KNOT_ANSWER; i <= KNOT_ADDITIONAL; ++i) {
+ const knot_pktsection_t *sec = knot_pkt_section(pkt, i);
+ for (unsigned k = 0; k < sec->count; ++k) {
+ const knot_rrset_t *rr = knot_pkt_rr(sec, k);
+ if (knot_dname_is_equal(ns, rr->owner) &&
+ (rr->type == KNOT_RRTYPE_A || rr->type == KNOT_RRTYPE_AAAA)) {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+/** @internal Update the cut with another NS(+glue) record.
+ * @param current_cut is cut name before this packet.
+ * @return _DONE if cut->name changes, _FAIL on error, and _CONSUME otherwise. */
+static int update_cut(knot_pkt_t *pkt, const knot_rrset_t *rr,
+ struct kr_request *req, const knot_dname_t *current_cut,
+ int *glue_cnt)
+{
+ struct kr_query *qry = req->current_query;
+ struct kr_zonecut *cut = &qry->zone_cut;
+ int state = KR_STATE_CONSUME;
+
+ /* New authority MUST be at/below the authority of the current cut;
+ * also qname must be below new authority;
+ * otherwise it's a possible cache injection attempt. */
+ const bool ok = knot_dname_in_bailiwick(rr->owner, current_cut) >= 0
+ && knot_dname_in_bailiwick(qry->sname, rr->owner) >= 0;
+ if (!ok) {
+ VERBOSE_MSG("<= authority: ns outside bailiwick\n");
+ qry->server_selection.error(qry, req->upstream.transport, KR_SELECTION_LAME_DELEGATION);
+#ifdef STRICT_MODE
+ return KR_STATE_FAIL;
+#else
+ /* Workaround: ignore out-of-bailiwick NSs for authoritative answers,
+ * but fail for referrals. This is important to detect lame answers. */
+ if (knot_pkt_section(pkt, KNOT_ANSWER)->count == 0) {
+ state = KR_STATE_FAIL;
+ }
+ return state;
+#endif
+ }
+
+ /* Update zone cut name */
+ if (!knot_dname_is_equal(rr->owner, cut->name)) {
+ /* Remember parent cut and descend to new (keep keys and TA). */
+ struct kr_zonecut *parent = mm_alloc(&req->pool, sizeof(*parent));
+ if (parent) {
+ memcpy(parent, cut, sizeof(*parent));
+ kr_zonecut_init(cut, rr->owner, &req->pool);
+ cut->key = parent->key;
+ cut->trust_anchor = parent->trust_anchor;
+ cut->parent = parent;
+ } else {
+ kr_zonecut_set(cut, rr->owner);
+ }
+ state = KR_STATE_DONE;
+ }
+
+ /* Fetch glue for each NS */
+ knot_rdata_t *rdata_i = rr->rrs.rdata;
+ for (unsigned i = 0; i < rr->rrs.count;
+ ++i, rdata_i = knot_rdataset_next(rdata_i)) {
+ const knot_dname_t *ns_name = knot_ns_name(rdata_i);
+ /* Glue is mandatory for NS below zone */
+ if (knot_dname_in_bailiwick(ns_name, rr->owner) >= 0
+ && !has_glue(pkt, ns_name)) {
+ const char *msg =
+ "<= authority: missing mandatory glue, skipping NS";
+ WITH_VERBOSE(qry) {
+ auto_free char *ns_str = kr_dname_text(ns_name);
+ VERBOSE_MSG("%s %s\n", msg, ns_str);
+ }
+ continue;
+ }
+ int ret = kr_zonecut_add(cut, ns_name, NULL, 0);
+ kr_assert(!ret);
+
+ /* Choose when to use glue records. */
+ const bool in_bailiwick =
+ knot_dname_in_bailiwick(ns_name, current_cut) >= 0;
+ bool do_fetch;
+ if (qry->flags.PERMISSIVE) {
+ do_fetch = true;
+ } else if (qry->flags.STRICT) {
+ /* Strict mode uses only mandatory glue. */
+ do_fetch = knot_dname_in_bailiwick(ns_name, cut->name) >= 0;
+ } else {
+ /* Normal mode uses in-bailiwick glue. */
+ do_fetch = in_bailiwick;
+ }
+ if (do_fetch) {
+ fetch_glue(pkt, ns_name, in_bailiwick, req, qry, glue_cnt);
+ }
+ }
+
+ return state;
+}
+
+/** Compute rank appropriate for RRs present in the packet.
+ * @param answer whether the RR is from answer or authority section
+ * @param is_nonauth: from referral or forwarding (etc.) */
+static uint8_t get_initial_rank(const knot_rrset_t *rr, const struct kr_query *qry,
+ const bool answer, const bool is_nonauth)
+{
+ /* For RRSIGs, ensure the KR_RANK_AUTH flag corresponds to the signed RR. */
+ uint16_t type = kr_rrset_type_maysig(rr);
+
+ if (qry->flags.CACHED) {
+ return rr->additional ? *(uint8_t *)rr->additional : KR_RANK_OMIT;
+ /* ^^ Current use case for "cached" RRs without rank: hints module. */
+ }
+ if (answer || type == KNOT_RRTYPE_DS
+ || type == KNOT_RRTYPE_SOA /* needed for aggressive negative caching */
+ || type == KNOT_RRTYPE_NSEC || type == KNOT_RRTYPE_NSEC3) {
+ /* We almost always want these validated, and it should be possible. */
+ return KR_RANK_INITIAL | KR_RANK_AUTH;
+ }
+ /* Be aggressive: try to validate anything else (almost never extra latency). */
+ return KR_RANK_TRY;
+ /* TODO: this classifier of authoritativity may not be perfect yet. */
+}
+
+static int pick_authority(knot_pkt_t *pkt, struct kr_request *req, bool to_wire)
+{
+ struct kr_query *qry = req->current_query;
+ const knot_pktsection_t *ns = knot_pkt_section(pkt, KNOT_AUTHORITY);
+
+ const knot_dname_t *zonecut_name = qry->zone_cut.name;
+ bool referral = !knot_wire_get_aa(pkt->wire);
+ if (referral) {
+ /* zone cut already updated by process_authority()
+ * use parent zonecut name */
+ zonecut_name = qry->zone_cut.parent ? qry->zone_cut.parent->name : qry->zone_cut.name;
+ to_wire = false;
+ }
+
+ for (unsigned i = 0; i < ns->count; ++i) {
+ const knot_rrset_t *rr = knot_pkt_rr(ns, i);
+ if (rr->rclass != KNOT_CLASS_IN
+ || knot_dname_in_bailiwick(rr->owner, zonecut_name) < 0) {
+ continue;
+ }
+ uint8_t rank = get_initial_rank(rr, qry, false,
+ qry->flags.FORWARD || referral);
+ int ret = kr_ranked_rrarray_add(&req->auth_selected, rr,
+ rank, to_wire, qry->uid, &req->pool);
+ if (ret < 0) {
+ return ret;
+ }
+ }
+
+ return kr_ok();
+}
+
+static int process_authority(knot_pkt_t *pkt, struct kr_request *req)
+{
+ struct kr_query *qry = req->current_query;
+ if (kr_fails_assert(!qry->flags.STUB))
+ return KR_STATE_FAIL;
+
+ int result = KR_STATE_CONSUME;
+ if (qry->flags.FORWARD) {
+ return result;
+ }
+
+ const knot_pktsection_t *ns = knot_pkt_section(pkt, KNOT_AUTHORITY);
+ const knot_pktsection_t *an = knot_pkt_section(pkt, KNOT_ANSWER);
+
+#ifdef STRICT_MODE
+ /* AA, terminate resolution chain. */
+ if (knot_wire_get_aa(pkt->wire)) {
+ return KR_STATE_CONSUME;
+ }
+#else
+ /* Work around servers sending back CNAME with different delegation and no AA. */
+ if (an->count > 0 && ns->count > 0) {
+ const knot_rrset_t *rr = knot_pkt_rr(an, 0);
+ if (rr->type == KNOT_RRTYPE_CNAME) {
+ return KR_STATE_CONSUME;
+ }
+ /* Work around for these NSs which are authoritative both for
+ * parent and child and mixes data from both zones in single answer */
+ if (knot_wire_get_aa(pkt->wire) &&
+ (rr->type == qry->stype) &&
+ (knot_dname_is_equal(rr->owner, qry->sname))) {
+ return KR_STATE_CONSUME;
+ }
+ }
+#endif
+ /* Remember current bailiwick for NS processing. */
+ const knot_dname_t *current_zone_cut = qry->zone_cut.name;
+ bool ns_record_exists = false;
+ int glue_cnt = 0;
+ int ns_count = 0;
+ /* Update zone cut information. */
+ for (unsigned i = 0; i < ns->count; ++i) {
+ const knot_rrset_t *rr = knot_pkt_rr(ns, i);
+ if (rr->type == KNOT_RRTYPE_NS) {
+ ns_record_exists = true;
+ int state = update_cut(pkt, rr, req, current_zone_cut, &glue_cnt);
+ switch(state) {
+ case KR_STATE_DONE: result = state; break;
+ case KR_STATE_FAIL: return state; break;
+ default: /* continue */ break;
+ }
+
+ if (++ns_count >= 13) {
+ VERBOSE_MSG("<= authority: many glue NSs, skipping the rest\n");
+ break;
+ }
+ } else if (rr->type == KNOT_RRTYPE_SOA
+ && knot_dname_in_bailiwick(rr->owner, qry->zone_cut.name) > 0) {
+ /* SOA below cut in authority indicates different authority,
+ * but same NS set. */
+ qry->zone_cut.name = knot_dname_copy(rr->owner, &req->pool);
+ }
+ }
+
+ /* Nameserver is authoritative for both parent side and the child side of the
+ * delegation may respond with an NS record in the answer section, and still update
+ * the zone cut (this e.g. happens on the `nrl.navy.mil.` zone cut).
+ * By updating the zone cut, we can continue with QNAME minimization,
+ * as the current code is only able to minimize one label below a zone cut. */
+ if (!ns_record_exists && knot_wire_get_aa(pkt->wire)) {
+ for (unsigned i = 0; i < an->count; ++i) {
+ const knot_rrset_t *rr = knot_pkt_rr(an, i);
+ if (rr->type == KNOT_RRTYPE_NS
+ && knot_dname_in_bailiwick(rr->owner, qry->zone_cut.name) > 0
+ /* "confusing" NS records can happen e.g. on a CNAME chain */
+ && knot_dname_in_bailiwick(qry->sname, rr->owner) >= 0) {
+ /* NS below cut in authority indicates different authority,
+ * but same NS set. */
+ qry->zone_cut.name = knot_dname_copy(rr->owner, &req->pool);
+ }
+ }
+ }
+
+ if (glue_cnt) {
+ VERBOSE_MSG("<= loaded %d glue addresses\n", glue_cnt);
+ }
+ if (glue_cnt > GLUE_COUNT_THROTTLE) {
+ VERBOSE_MSG("<= (some may have been omitted due to being too many)\n");
+ }
+
+
+ if ((qry->flags.DNSSEC_WANT) && (result == KR_STATE_CONSUME)) {
+ if (knot_wire_get_aa(pkt->wire) == 0 &&
+ knot_wire_get_ancount(pkt->wire) == 0 &&
+ ns_record_exists) {
+ /* Unhelpful referral
+ Prevent from validating as an authoritative answer */
+ result = KR_STATE_DONE;
+ }
+ }
+
+ /* CONSUME => Unhelpful referral.
+ * DONE => Zone cut updated. */
+ return result;
+}
+
+static int finalize_answer(knot_pkt_t *pkt, struct kr_request *req)
+{
+ /* Finalize header */
+ knot_pkt_t *answer = kr_request_ensure_answer(req);
+ if (answer) {
+ knot_wire_set_rcode(answer->wire, knot_wire_get_rcode(pkt->wire));
+ req->state = KR_STATE_DONE;
+ }
+ return req->state;
+}
+
+static int unroll_cname(knot_pkt_t *pkt, struct kr_request *req, bool referral, const knot_dname_t **cname_ret)
+{
+ struct kr_query *query = req->current_query;
+ if (kr_fails_assert(!query->flags.STUB))
+ return KR_STATE_FAIL;
+ /* Process answer type */
+ const knot_pktsection_t *an = knot_pkt_section(pkt, KNOT_ANSWER);
+ const knot_dname_t *cname = NULL;
+ const knot_dname_t *pending_cname = query->sname;
+ bool is_final = (query->parent == NULL);
+ bool strict_mode = (query->flags.STRICT);
+
+ query->cname_depth = query->cname_parent ? query->cname_parent->cname_depth : 1;
+
+ do {
+ /* CNAME was found at previous iteration, but records may not follow the correct order.
+ * Try to find records for pending_cname owner from section start. */
+ cname = pending_cname;
+ size_t cname_answ_selected_i = -1;
+ bool cname_is_occluded = false; /* whether `cname` is in a DNAME's bailiwick */
+ pending_cname = NULL;
+ const int cname_labels = knot_dname_labels(cname, NULL);
+ for (unsigned i = 0; i < an->count; ++i) {
+ const knot_rrset_t *rr = knot_pkt_rr(an, i);
+
+ /* Skip the RR if its owner+type doesn't interest us. */
+ const uint16_t type = kr_rrset_type_maysig(rr);
+ const bool type_OK = rr->type == query->stype || type == query->stype
+ || type == KNOT_RRTYPE_CNAME;
+ if (rr->rclass != KNOT_CLASS_IN
+ || knot_dname_in_bailiwick(rr->owner, query->zone_cut.name) < 0) {
+ continue;
+ }
+ const bool all_OK = type_OK && knot_dname_is_equal(rr->owner, cname);
+
+ const bool to_wire = is_final && !referral;
+
+ if (!all_OK && type == KNOT_RRTYPE_DNAME
+ && knot_dname_in_bailiwick(cname, rr->owner) >= 1) {
+ /* This DNAME (or RRSIGs) cover the current target (`cname`),
+ * so it is interesting and will occlude its CNAME.
+ * We rely on CNAME being sent along with DNAME
+ * (mandatory unless YXDOMAIN). */
+ cname_is_occluded = true;
+ uint8_t rank = get_initial_rank(rr, query, true,
+ query->flags.FORWARD || referral);
+ int ret = kr_ranked_rrarray_add(&req->answ_selected, rr,
+ rank, to_wire, query->uid, &req->pool);
+ if (ret < 0) {
+ return KR_STATE_FAIL;
+ }
+ }
+ if (!all_OK) {
+ continue;
+ }
+
+ if (rr->type == KNOT_RRTYPE_RRSIG) {
+ int rrsig_labels = knot_rrsig_labels(rr->rrs.rdata);
+ if (rrsig_labels > cname_labels) {
+ /* clearly wrong RRSIG, don't pick it.
+ * don't fail immediately,
+ * let validator work. */
+ continue;
+ }
+ if (rrsig_labels < cname_labels) {
+ query->flags.DNSSEC_WEXPAND = true;
+ }
+ }
+
+ /* Process records matching current SNAME */
+ if (!is_final) {
+ int cnt_ = 0;
+ int state = update_nsaddr(rr, query->parent, &cnt_);
+ if (state & KR_STATE_FAIL) {
+ return state;
+ }
+ }
+ uint8_t rank = get_initial_rank(rr, query, true,
+ query->flags.FORWARD || referral);
+ int ret = kr_ranked_rrarray_add(&req->answ_selected, rr,
+ rank, to_wire, query->uid, &req->pool);
+ if (ret < 0) {
+ return KR_STATE_FAIL;
+ }
+ cname_answ_selected_i = ret;
+
+ /* Select the next CNAME target, but don't jump immediately.
+ * There can be records for "old" cname (RRSIGs are interesting);
+ * more importantly there might be a DNAME for `cname_is_occluded`. */
+ if (query->stype != KNOT_RRTYPE_CNAME && rr->type == KNOT_RRTYPE_CNAME) {
+ pending_cname = knot_cname_name(rr->rrs.rdata);
+ if (!pending_cname) {
+ break;
+ }
+ }
+ }
+ if (!pending_cname) {
+ break;
+ }
+ if (cname_is_occluded) {
+ req->answ_selected.at[cname_answ_selected_i]->dont_cache = true;
+ }
+ if (++(query->cname_depth) > KR_CNAME_CHAIN_LIMIT) {
+ VERBOSE_MSG("<= error: CNAME chain exceeded max length %d\n",
+ /* people count objects from 0, no CNAME = 0 */
+ (int)KR_CNAME_CHAIN_LIMIT - 1);
+ return KR_STATE_FAIL;
+ }
+
+ if (knot_dname_is_equal(cname, pending_cname)) {
+ VERBOSE_MSG("<= error: CNAME chain loop detected\n");
+ return KR_STATE_FAIL;
+ }
+ /* In strict mode, explicitly fetch each CNAME target. */
+ if (strict_mode) {
+ cname = pending_cname;
+ break;
+ }
+ /* Information outside bailiwick is not trusted. */
+ if (knot_dname_in_bailiwick(pending_cname, query->zone_cut.name) < 0) {
+ cname = pending_cname;
+ break;
+ }
+ /* The validator still can't handle multiple zones in one answer,
+ * so we only follow if a single label is replaced.
+ * Forwarding appears to be even more sensitive to this.
+ * TODO: iteration can probably handle the remaining cases,
+ * but overall it would be better to have a smarter validator
+ * (and thus save roundtrips).*/
+ const int pending_labels = knot_dname_labels(pending_cname, NULL);
+ if (pending_labels != cname_labels) {
+ cname = pending_cname;
+ break;
+ }
+ if (knot_dname_matched_labels(pending_cname, cname) != cname_labels - 1
+ || query->flags.FORWARD) {
+ cname = pending_cname;
+ break;
+ }
+ } while (true);
+ *cname_ret = cname;
+ return kr_ok();
+}
+
+static int process_referral_answer(knot_pkt_t *pkt, struct kr_request *req)
+{
+ const knot_dname_t *cname = NULL;
+ int state = unroll_cname(pkt, req, true, &cname);
+ struct kr_query *query = req->current_query;
+ if (state != kr_ok()) {
+ query->server_selection.error(query, req->upstream.transport, KR_SELECTION_BAD_CNAME);
+ return KR_STATE_FAIL;
+ }
+ if (!(query->flags.CACHED)) {
+ /* If not cached (i.e. got from upstream)
+ * make sure that this is not an authoritative answer
+ * (even with AA=1) for other layers.
+ * There can be answers with AA=1,
+ * empty answer section and NS in authority.
+ * Clearing of AA prevents them from
+ * caching in the packet cache.
+ * If packet already cached, don't touch him. */
+ knot_wire_clear_aa(pkt->wire);
+ }
+ state = pick_authority(pkt, req, false);
+ return state == kr_ok() ? KR_STATE_DONE : KR_STATE_FAIL;
+}
+
+static int process_final(knot_pkt_t *pkt, struct kr_request *req,
+ const knot_dname_t *cname)
+{
+ const int pkt_class = kr_response_classify(pkt);
+ struct kr_query *query = req->current_query;
+ ranked_rr_array_t *array = &req->answ_selected;
+ for (size_t i = 0; i < array->len; ++i) {
+ const knot_rrset_t *rr = array->at[i]->rr;
+ if (!knot_dname_is_equal(rr->owner, cname)) {
+ continue;
+ }
+ if ((rr->rclass != query->sclass) ||
+ (rr->type != query->stype)) {
+ continue;
+ }
+ const bool to_wire = ((pkt_class & (PKT_NXDOMAIN|PKT_NODATA)) != 0);
+ const int state = pick_authority(pkt, req, to_wire);
+ if (state != kr_ok()) {
+ return KR_STATE_FAIL;
+ }
+ if (!array->at[i]->to_wire) {
+ const size_t last_idx = array->len - 1;
+ size_t j = i;
+ ranked_rr_array_entry_t *entry = array->at[i];
+ /* Relocate record to the end, after current cname */
+ while (j < last_idx) {
+ array->at[j] = array->at[j + 1];
+ ++j;
+ }
+ array->at[last_idx] = entry;
+ entry->to_wire = true;
+ }
+ return finalize_answer(pkt, req);
+ }
+ return kr_ok();
+}
+
+static int process_answer(knot_pkt_t *pkt, struct kr_request *req)
+{
+ struct kr_query *query = req->current_query;
+
+ /* Response for minimized QNAME. Note that current iterator's minimization
+ * is only able ask one label below a zone cut.
+ * NODATA => may be empty non-terminal, retry (found zone cut)
+ * NOERROR => found zone cut, retry, except the case described below
+ * NXDOMAIN => parent is zone cut, retry as a workaround for bad authoritatives
+ */
+ const bool is_final = (query->parent == NULL);
+ const int pkt_class = kr_response_classify(pkt);
+ const knot_dname_t * pkt_qname = knot_pkt_qname(pkt);
+ if (!knot_dname_is_equal(pkt_qname, query->sname) &&
+ (pkt_class & (PKT_NOERROR|PKT_NXDOMAIN|PKT_REFUSED|PKT_NODATA))) {
+ /* Check for parent server that is authoritative for child zone,
+ * several CCTLDs where the SLD and TLD have the same name servers */
+ const knot_pktsection_t *ans = knot_pkt_section(pkt, KNOT_ANSWER);
+ if ((pkt_class & (PKT_NOERROR)) && ans->count > 0 &&
+ knot_dname_is_equal(pkt_qname, query->zone_cut.name)) {
+ VERBOSE_MSG("<= continuing with qname minimization\n");
+ } else {
+ /* fall back to disabling minimization */
+ VERBOSE_MSG("<= retrying with non-minimized name\n");
+ query->flags.NO_MINIMIZE = true;
+ }
+ return KR_STATE_CONSUME;
+ }
+
+ /* This answer didn't improve resolution chain, therefore must be authoritative (relaxed to negative). */
+ if (!is_authoritative(pkt, query)) {
+ if (!(query->flags.FORWARD) &&
+ pkt_class & (PKT_NXDOMAIN|PKT_NODATA)) {
+ query->server_selection.error(query, req->upstream.transport, KR_SELECTION_LAME_DELEGATION);
+ VERBOSE_MSG("<= lame response: non-auth sent negative response\n");
+ return KR_STATE_FAIL;
+ }
+ }
+
+ const knot_dname_t *cname = NULL;
+ /* Process answer type */
+ int state = unroll_cname(pkt, req, false, &cname);
+ if (state != kr_ok()) {
+ query->server_selection.error(query, req->upstream.transport, KR_SELECTION_BAD_CNAME);
+ return state;
+ }
+ /* Make sure that this is an authoritative answer (even with AA=0) for other layers */
+ knot_wire_set_aa(pkt->wire);
+ /* Either way it resolves current query. */
+ query->flags.RESOLVED = true;
+ /* Follow canonical name as next SNAME. */
+ if (!knot_dname_is_equal(cname, query->sname)) {
+ /* Check if target record has been already copied */
+ query->flags.CNAME = true;
+ if (is_final) {
+ state = process_final(pkt, req, cname);
+ if (state != kr_ok()) {
+ return state;
+ }
+ } else if ((query->flags.FORWARD) &&
+ ((query->stype == KNOT_RRTYPE_DS) ||
+ (query->stype == KNOT_RRTYPE_NS))) {
+ /* CNAME'ed answer for DS or NS subquery.
+ * Treat it as proof of zonecut nonexistence. */
+ return KR_STATE_DONE;
+ }
+ VERBOSE_MSG("<= cname chain, following\n");
+ /* Check if the same query was followed in the same CNAME chain. */
+ for (const struct kr_query *q = query->cname_parent; q != NULL;
+ q = q->cname_parent) {
+ if (q->sclass == query->sclass &&
+ q->stype == query->stype &&
+ knot_dname_is_equal(q->sname, cname)) {
+ VERBOSE_MSG("<= cname chain loop\n");
+ query->server_selection.error(query, req->upstream.transport, KR_SELECTION_BAD_CNAME);
+ return KR_STATE_FAIL;
+ }
+ }
+ struct kr_query *next = kr_rplan_push(&req->rplan, query->parent, cname, query->sclass, query->stype);
+ if (!next) {
+ return KR_STATE_FAIL;
+ }
+ next->flags.AWAIT_CUT = true;
+
+ /* Copy transitive flags from original query to CNAME followup. */
+ next->flags.TRACE = query->flags.TRACE;
+ next->flags.ALWAYS_CUT = query->flags.ALWAYS_CUT;
+
+ /* Original query might have turned minimization off, revert. */
+ next->flags.NO_MINIMIZE = req->options.NO_MINIMIZE;
+
+ if (query->flags.FORWARD) {
+ next->forward_flags.CNAME = true;
+ }
+ next->cname_parent = query;
+ /* Want DNSSEC if and only if it's possible to secure
+ * this name (i.e. iff it is covered by a TA) */
+ if (kr_ta_closest(req->ctx, cname, query->stype)) {
+ next->flags.DNSSEC_WANT = true;
+ } else {
+ next->flags.DNSSEC_WANT = false;
+ }
+ if (!(query->flags.FORWARD) ||
+ (query->flags.DNSSEC_WEXPAND)) {
+ state = pick_authority(pkt, req, false);
+ if (state != kr_ok()) {
+ return KR_STATE_FAIL;
+ }
+ }
+ } else if (!query->parent) {
+ /* Answer for initial query */
+ const bool to_wire = ((pkt_class & (PKT_NXDOMAIN|PKT_NODATA)) != 0);
+ state = pick_authority(pkt, req, to_wire);
+ if (state != kr_ok()) {
+ return KR_STATE_FAIL;
+ }
+ return finalize_answer(pkt, req);
+ } else {
+ /* Answer for sub-query; DS, IP for NS etc.
+ * It may contains NSEC \ NSEC3 records for
+ * data non-existence or wc expansion proving.
+ * If yes, they must be validated by validator.
+ * If no, authority section is unuseful.
+ * dnssec\nsec.c & dnssec\nsec3.c use
+ * rrsets from incoming packet.
+ * validator uses answer_selected & auth_selected.
+ * So, if nsec\nsec3 records are present in authority,
+ * pick_authority() must be called.
+ * TODO refactor nsec\nsec3 modules to work with
+ * answer_selected & auth_selected instead of incoming pkt. */
+ bool auth_is_unuseful = true;
+ const knot_pktsection_t *ns = knot_pkt_section(pkt, KNOT_AUTHORITY);
+ for (unsigned i = 0; i < ns->count; ++i) {
+ const knot_rrset_t *rr = knot_pkt_rr(ns, i);
+ if (rr->type == KNOT_RRTYPE_NSEC ||
+ rr->type == KNOT_RRTYPE_NSEC3) {
+ auth_is_unuseful = false;
+ break;
+ }
+ }
+ if (!auth_is_unuseful) {
+ state = pick_authority(pkt, req, false);
+ if (state != kr_ok()) {
+ return KR_STATE_FAIL;
+ }
+ }
+ }
+ return KR_STATE_DONE;
+}
+
+/** @internal like process_answer() but for the STUB mode. */
+static int process_stub(knot_pkt_t *pkt, struct kr_request *req)
+{
+ struct kr_query *query = req->current_query;
+ if (kr_fails_assert(query->flags.STUB))
+ return KR_STATE_FAIL;
+ /* Pick all answer RRs. */
+ const knot_pktsection_t *an = knot_pkt_section(pkt, KNOT_ANSWER);
+ for (unsigned i = 0; i < an->count; ++i) {
+ const knot_rrset_t *rr = knot_pkt_rr(an, i);
+ int err = kr_ranked_rrarray_add(&req->answ_selected, rr,
+ KR_RANK_OMIT | KR_RANK_AUTH, true, query->uid, &req->pool);
+ /* KR_RANK_AUTH: we don't have the records directly from
+ * an authoritative source, but we do trust the server and it's
+ * supposed to only send us authoritative records. */
+ if (err < 0) {
+ return KR_STATE_FAIL;
+ }
+ }
+
+ knot_wire_set_aa(pkt->wire);
+ query->flags.RESOLVED = true;
+ /* Pick authority RRs. */
+ int pkt_class = kr_response_classify(pkt);
+ const bool to_wire = ((pkt_class & (PKT_NXDOMAIN|PKT_NODATA)) != 0);
+ int err = pick_authority(pkt, req, to_wire);
+ if (err != kr_ok()) {
+ return KR_STATE_FAIL;
+ }
+
+ return finalize_answer(pkt, req);
+}
+
+/* State-less single resolution iteration step, not needed. */
+static int reset(kr_layer_t *ctx) { return KR_STATE_PRODUCE; }
+
+/* Set resolution context and parameters. */
+static int begin(kr_layer_t *ctx)
+{
+ if (ctx->state & (KR_STATE_DONE|KR_STATE_FAIL)) {
+ return ctx->state;
+ }
+ /*
+ * RFC7873 5.4 extends the QUERY operation code behaviour in order to
+ * be able to generate requests for server cookies. Such requests have
+ * QDCOUNT equal to zero and must contain a cookie option.
+ * Server cookie queries must be handled by the cookie module/layer
+ * before this layer.
+ */
+ const knot_pkt_t *pkt = ctx->req->qsource.packet;
+ if (!pkt || knot_wire_get_qdcount(pkt->wire) == 0) {
+ return KR_STATE_FAIL;
+ }
+
+ struct kr_query *qry = ctx->req->current_query;
+ /* Avoid any other classes, and avoid any meta-types ~~except for ANY~~. */
+ if (qry->sclass != KNOT_CLASS_IN
+ || (knot_rrtype_is_metatype(qry->stype)
+ /* && qry->stype != KNOT_RRTYPE_ANY hmm ANY seems broken ATM */)) {
+ knot_pkt_t *ans = kr_request_ensure_answer(ctx->req);
+ if (!ans) return ctx->req->state;
+ knot_wire_set_rcode(ans->wire, KNOT_RCODE_NOTIMPL);
+ return KR_STATE_FAIL;
+ }
+
+ return reset(ctx);
+}
+
+int kr_make_query(struct kr_query *query, knot_pkt_t *pkt)
+{
+ /* Minimize QNAME (if possible). */
+ uint16_t qtype = query->stype;
+ const knot_dname_t *qname = minimized_qname(query, &qtype);
+
+ /* Form a query for the authoritative. */
+ knot_pkt_clear(pkt);
+ int ret = knot_pkt_put_question(pkt, qname, query->sclass, qtype);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ /* Query built, expect answer. */
+ query->id = kr_rand_bytes(2);
+ /* We must respect https://tools.ietf.org/html/rfc7766#section-6.2.1
+ * - When sending multiple queries over a TCP connection, clients MUST NOT
+ * reuse the DNS Message ID of an in-flight query on that connection.
+ *
+ * So, if query is going to be sent over TCP connection
+ * this id can be changed to avoid duplication with query that already was sent
+ * but didn't receive answer yet.
+ */
+ knot_wire_set_id(pkt->wire, query->id);
+ pkt->parsed = pkt->size;
+
+ return kr_ok();
+}
+
+static int prepare_query(kr_layer_t *ctx, knot_pkt_t *pkt)
+{
+ if (kr_fails_assert(pkt && ctx))
+ return KR_STATE_FAIL;
+ struct kr_request *req = ctx->req;
+ struct kr_query *query = req->current_query;
+ if (!query || ctx->state & (KR_STATE_DONE|KR_STATE_FAIL)) {
+ return ctx->state;
+ }
+
+ /* Make query */
+ int ret = kr_make_query(query, pkt);
+ if (ret != 0) {
+ return KR_STATE_FAIL;
+ }
+
+ WITH_VERBOSE(query) {
+ KR_DNAME_GET_STR(name_str, query->sname);
+ KR_RRTYPE_GET_STR(type_str, query->stype);
+ QVERBOSE_MSG(query, "'%s' type '%s' new uid was assigned .%02u, parent uid .%02u\n",
+ name_str, type_str, req->rplan.next_uid,
+ query->parent ? query->parent->uid : 0);
+ }
+
+ query->uid = req->rplan.next_uid;
+ req->rplan.next_uid += 1;
+ query->flags.CACHED = false; // in case it got left from earlier (unknown edge case)
+
+ return KR_STATE_CONSUME;
+}
+
+static bool satisfied_by_additional(const struct kr_query *qry)
+{
+ const bool prereq = !qry->flags.STUB && !qry->flags.FORWARD && qry->flags.NONAUTH;
+ if (!prereq)
+ return false;
+ const struct kr_request *req = qry->request;
+ for (ssize_t i = req->add_selected.len - 1; i >= 0; --i) {
+ ranked_rr_array_entry_t *entry = req->add_selected.at[i];
+ if (entry->qry_uid != qry->uid)
+ break;
+ if (entry->rr->type == qry->stype
+ && knot_dname_is_equal(entry->rr->owner, qry->sname)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/** Restrict all RRset TTLs to the specified bounds (if matching qry_uid). */
+static void bound_ttls(ranked_rr_array_t *array, uint32_t qry_uid,
+ uint32_t ttl_min, uint32_t ttl_max)
+{
+ for (ssize_t i = 0; i < array->len; ++i) {
+ if (array->at[i]->qry_uid != qry_uid)
+ continue;
+ uint32_t *ttl = &array->at[i]->rr->ttl;
+ if (*ttl < ttl_min) {
+ *ttl = ttl_min;
+ } else if (*ttl > ttl_max) {
+ *ttl = ttl_max;
+ }
+ }
+}
+
+static void ede_passthru(const knot_pkt_t *pkt, struct kr_request *req)
+{
+ const uint8_t *ede_raw = pkt->edns_opts ?
+ pkt->edns_opts->ptr[KNOT_EDNS_OPTION_EDE] : NULL;
+ if (!ede_raw) return;
+ kr_require(ede_raw[0] * 256 + ede_raw[1] == KNOT_EDNS_OPTION_EDE);
+ uint16_t ede_len = ede_raw[2] * 256 + ede_raw[3];
+ if (ede_len < 2) return;
+ uint16_t ede_code = ede_raw[4] * 256 + ede_raw[5];
+ if (ede_code >= KNOT_EDNS_EDE_INDETERMINATE // long range of DNSSEC codes
+ && ede_code <= KNOT_EDNS_EDE_NSEC_MISS) {
+ kr_request_set_extended_error(req, ede_code, "V5T7: forwarded EDE code");
+ }
+}
+
+/** Resolve input query or continue resolution with followups.
+ *
+ * This roughly corresponds to RFC1034, 5.3.3 4a-d.
+ */
+static int resolve(kr_layer_t *ctx, knot_pkt_t *pkt)
+{
+ if (kr_fails_assert(pkt && ctx))
+ return KR_STATE_FAIL;
+ struct kr_request *req = ctx->req;
+ struct kr_query *query = req->current_query;
+ if (!query) {
+ return ctx->state;
+ }
+ query->flags.PKT_IS_SANE = false;
+
+ WITH_VERBOSE(query) {
+ if (query->flags.TRACE) {
+ auto_free char *pkt_text = kr_pkt_text(pkt);
+ VERBOSE_MSG("<= answer received:\n%s\n", pkt_text);
+ }
+ }
+
+ if (query->flags.RESOLVED || query->flags.BADCOOKIE_AGAIN) {
+ return ctx->state;
+ }
+
+ /* Check for packet processing errors first.
+ * Note - we *MUST* check if it has at least a QUESTION,
+ * otherwise it would crash on accessing QNAME. */
+ /* TODO: some of these erros are probably unreachable
+ * thanks to getting caught earlier, in particular in worker_submit() */
+ if (pkt->parsed <= KNOT_WIRE_HEADER_SIZE) {
+ if (pkt->parsed == KNOT_WIRE_HEADER_SIZE && knot_wire_get_rcode(pkt->wire) == KNOT_RCODE_FORMERR) {
+ /* This is a special case where we get valid header with FORMERR and nothing else.
+ * This happens on some authoritatives which don't support EDNS and don't
+ * bother copying the SECTION QUESTION. */
+ query->server_selection.error(query, req->upstream.transport, KR_SELECTION_FORMERR);
+ return KR_STATE_FAIL;
+ }
+ VERBOSE_MSG("<= malformed response (parsed %d)\n", (int)pkt->parsed);
+ query->server_selection.error(query, req->upstream.transport, KR_SELECTION_MALFORMED);
+ return KR_STATE_FAIL;
+ } else if (!is_paired_to_query(pkt, query)) {
+ WITH_VERBOSE(query) {
+ const char *ns_str =
+ req->upstream.transport ? kr_straddr(&req->upstream.transport->address.ip) : "(internal)";
+ VERBOSE_MSG("<= ignoring mismatching response from %s\n",
+ ns_str ? ns_str : "(kr_straddr failed)");
+ }
+ query->server_selection.error(query, req->upstream.transport, KR_SELECTION_MISMATCHED);
+ return KR_STATE_FAIL;
+ } else if (knot_wire_get_tc(pkt->wire)) {
+ VERBOSE_MSG("<= truncated response, failover to TCP\n");
+ if (query) {
+ /* Fail if already on TCP. */
+ if (req->upstream.transport->protocol != KR_TRANSPORT_UDP) {
+ VERBOSE_MSG("<= TC=1 with TCP, bailing out\n");
+ query->server_selection.error(query, req->upstream.transport, KR_SELECTION_TRUNCATED);
+ return KR_STATE_FAIL;
+ }
+ query->server_selection.error(query, req->upstream.transport, KR_SELECTION_TRUNCATED);
+ }
+ return KR_STATE_CONSUME;
+ }
+
+ /* If exiting above here, there's no sense to put it into packet cache.
+ * Having "extra bytes" at the end of DNS message is considered SANE here.
+ * The most important part is to check for spoofing: is_paired_to_query() */
+ query->flags.PKT_IS_SANE = true;
+
+ const knot_lookup_t *rcode = // just for logging but cheaper than a condition
+ knot_lookup_by_id(knot_rcode_names, knot_wire_get_rcode(pkt->wire));
+
+ // We can't return directly from the switch because we have to give feedback to server selection first
+ int ret = 0;
+ int selection_error = KR_SELECTION_OK;
+
+ /* Check response code. */
+ switch(knot_wire_get_rcode(pkt->wire)) {
+ case KNOT_RCODE_NOERROR:
+ case KNOT_RCODE_NXDOMAIN:
+ break; /* OK */
+ case KNOT_RCODE_YXDOMAIN: /* Basically a successful answer; name just doesn't fit. */
+ if (!kr_request_ensure_answer(req)) {
+ ret = req->state;
+ }
+ knot_wire_set_rcode(req->answer->wire, KNOT_RCODE_YXDOMAIN);
+ break;
+ case KNOT_RCODE_REFUSED:
+ if (query->flags.STUB) {
+ /* just pass answer through if in stub mode */
+ break;
+ }
+ ret = KR_STATE_FAIL;
+ selection_error = KR_SELECTION_REFUSED;
+ break;
+ case KNOT_RCODE_SERVFAIL:
+ if (query->flags.STUB) {
+ /* just pass answer through if in stub mode */
+ break;
+ }
+ ret = KR_STATE_FAIL;
+ selection_error = KR_SELECTION_SERVFAIL;
+ if (query->flags.FORWARD) {
+ /* additionally pass some of the EDE codes through */
+ ede_passthru(pkt, req);
+ }
+ break;
+ case KNOT_RCODE_FORMERR:
+ ret = KR_STATE_FAIL;
+ if (knot_pkt_has_edns(pkt)) {
+ selection_error = KR_SELECTION_FORMERR_EDNS;
+ } else {
+ selection_error = KR_SELECTION_FORMERR;
+ }
+ break;
+ case KNOT_RCODE_NOTIMPL:
+ ret = KR_STATE_FAIL;
+ selection_error = KR_SELECTION_NOTIMPL;
+ break;
+ default:
+ ret = KR_STATE_FAIL;
+ selection_error = KR_SELECTION_OTHER_RCODE;
+ break;
+ }
+
+ /* Check for "extra bytes" is deferred, so that RCODE-based failures take priority. */
+ if (ret != KR_STATE_FAIL && pkt->parsed < pkt->size) {
+ VERBOSE_MSG("<= malformed response with %zu extra bytes\n",
+ pkt->size - pkt->parsed);
+ ret = KR_STATE_FAIL;
+ if (selection_error == KR_SELECTION_OK)
+ selection_error = KR_SELECTION_MALFORMED;
+ }
+
+ if (query->server_selection.initialized) {
+ query->server_selection.error(query, req->upstream.transport, selection_error);
+ }
+
+ if (ret) {
+ VERBOSE_MSG("<= rcode: %s\n", rcode ? rcode->name : "??");
+ return ret;
+ }
+
+ int state;
+ /* Forwarding/stub mode is special. */
+ if (query->flags.STUB) {
+ state = process_stub(pkt, req);
+ goto rrarray_finalize;
+ }
+
+ /* Resolve authority to see if it's referral or authoritative. */
+ state = process_authority(pkt, req);
+ switch(state) {
+ case KR_STATE_CONSUME: /* Not referral, process answer. */
+ VERBOSE_MSG("<= rcode: %s\n", rcode ? rcode->name : "??");
+ state = process_answer(pkt, req);
+ break;
+ case KR_STATE_DONE: /* Referral */
+ state = process_referral_answer(pkt,req);
+ if (satisfied_by_additional(query)) { /* This is a little hacky.
+ * We found sufficient information in ADDITIONAL section
+ * and it was selected for caching in this CONSUME round.
+ * To make iterator accept the record in a simple way,
+ * we trigger another cache *reading* attempt
+ * for the subsequent PRODUCE round.
+ */
+ kr_assert(query->flags.NONAUTH);
+ query->flags.CACHE_TRIED = false;
+ VERBOSE_MSG("<= referral response, but cache should stop us short now\n");
+ } else {
+ VERBOSE_MSG("<= referral response, follow\n");
+ }
+ break;
+ default:
+ break;
+ }
+
+rrarray_finalize:
+ /* Finish construction of libknot-format RRsets.
+ * We do this even if dropping the answer, though it's probably useless. */
+ (void)0;
+ const struct kr_cache *cache = &req->ctx->cache;
+ ranked_rr_array_t *selected[] = kr_request_selected(req);
+ for (knot_section_t i = KNOT_ANSWER; i <= KNOT_ADDITIONAL; ++i) {
+ ret = kr_ranked_rrarray_finalize(selected[i], query->uid, &req->pool);
+ if (unlikely(ret))
+ return KR_STATE_FAIL;
+ if (!query->flags.CACHED)
+ bound_ttls(selected[i], query->uid, cache->ttl_min, cache->ttl_max);
+ }
+
+ return state;
+}
+
+/** Module implementation. */
+int iterate_init(struct kr_module *self)
+{
+ static const kr_layer_api_t layer = {
+ .begin = &begin,
+ .reset = &reset,
+ .consume = &resolve,
+ .produce = &prepare_query
+ };
+ self->layer = &layer;
+ return kr_ok();
+}
+
+KR_MODULE_EXPORT(iterate) /* useless for builtin module, but let's be consistent */
+
+#undef VERBOSE_MSG
diff --git a/lib/layer/iterate.h b/lib/layer/iterate.h
new file mode 100644
index 0000000..4ea4351
--- /dev/null
+++ b/lib/layer/iterate.h
@@ -0,0 +1,25 @@
+/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include "lib/layer.h"
+#include "lib/rplan.h"
+
+/* Packet classification. */
+enum {
+ PKT_NOERROR = 1 << 0, /* Positive response */
+ PKT_NODATA = 1 << 1, /* No data response */
+ PKT_NXDOMAIN = 1 << 2, /* Negative response */
+ PKT_REFUSED = 1 << 3, /* Refused response */
+ PKT_ERROR = 1 << 4 /* Bad message */
+};
+
+/** Classify response by type. */
+KR_EXPORT
+int kr_response_classify(const knot_pkt_t *pkt);
+
+/** Make next iterative query. */
+KR_EXPORT
+int kr_make_query(struct kr_query *query, knot_pkt_t *pkt);
diff --git a/lib/layer/mode.rst b/lib/layer/mode.rst
new file mode 100644
index 0000000..d64257e
--- /dev/null
+++ b/lib/layer/mode.rst
@@ -0,0 +1,26 @@
+.. SPDX-License-Identifier: GPL-3.0-or-later
+
+.. function:: mode(['strict' | 'normal' | 'permissive'])
+
+ :param: New checking level specified as string (*optional*).
+ :return: Current checking level.
+
+ Get or change resolver strictness checking level.
+
+ By default, resolver runs in *normal* mode. There are possibly many small adjustments
+ hidden behind the mode settings, but the main idea is that in *permissive* mode, the resolver
+ tries to resolve a name with as few lookups as possible, while in *strict* mode it spends much
+ more effort resolving and checking referral path. However, if majority of the traffic is covered
+ by DNSSEC, some of the strict checking actions are counter-productive.
+
+ .. csv-table::
+ :header: "Glue type", "Modes when it is accepted", "Example glue [#example_glue]_"
+
+ "mandatory glue", "strict, normal, permissive", "ns1.example.org"
+ "in-bailiwick glue", "normal, permissive", "ns1.example2.org"
+ "any glue records", "permissive", "ns1.example3.net"
+
+ .. [#example_glue] The examples show glue records acceptable from servers
+ authoritative for `org` zone when delegating to `example.org` zone.
+ Unacceptable or missing glue records trigger resolution of names listed
+ in NS records before following respective delegation.
diff --git a/lib/layer/test.integr/deckard.yaml b/lib/layer/test.integr/deckard.yaml
new file mode 100644
index 0000000..d2d62d0
--- /dev/null
+++ b/lib/layer/test.integr/deckard.yaml
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+programs:
+- name: kresd
+ binary: kresd
+ additional:
+ - --noninteractive
+ templates:
+ - lib/layer/test.integr/kresd_config.j2
+ - tests/integration/hints_zone.j2
+ configs:
+ - config
+ - hints
+noclean: True
diff --git a/lib/layer/test.integr/iter_cname_length.rpl b/lib/layer/test.integr/iter_cname_length.rpl
new file mode 100644
index 0000000..39f48a8
--- /dev/null
+++ b/lib/layer/test.integr/iter_cname_length.rpl
@@ -0,0 +1,226 @@
+do-ip6: no
+; config options
+; SPDX-License-Identifier: GPL-3.0-or-later
+ stub-addr: 193.0.14.129 # k.root-servers.net.
+CONFIG_END
+
+SCENARIO_BEGIN Test restriction on CNAME chain length.
+
+
+; k.root-servers.net.
+RANGE_BEGIN 0 100
+ ADDRESS 193.0.14.129
+
+ENTRY_BEGIN
+MATCH opcode qname
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+n1.tld. IN NS
+SECTION ANSWER
+n1.tld. IN CNAME n2.tld.
+n2.tld. IN CNAME n3.tld.
+n3.tld. IN CNAME n4.tld.
+n4.tld. IN CNAME n5.tld.
+n5.tld. IN CNAME n6.tld.
+n6.tld. IN CNAME n7.sub.
+SECTION AUTHORITY
+sub. IN NS ns.sub.
+SECTION ADDITIONAL
+ns.sub. IN A 194.0.14.1
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qname
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+n2.tld. IN NS
+SECTION ANSWER
+n2.tld. IN CNAME n3.tld.
+n3.tld. IN CNAME n4.tld.
+n4.tld. IN CNAME n5.tld.
+n5.tld. IN CNAME n6.tld.
+n6.tld. IN CNAME n7.sub.
+SECTION AUTHORITY
+sub. IN NS ns.sub.
+SECTION ADDITIONAL
+ns.sub. IN A 194.0.14.1
+ENTRY_END
+
+
+; empty non-terminal for query name minimization
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR AA NOERROR
+SECTION QUESTION
+tld. IN NS
+SECTION ANSWER
+ENTRY_END
+
+
+
+; sub. subdomains
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+sub. IN NS
+SECTION AUTHORITY
+sub. IN NS ns.sub.
+SECTION ADDITIONAL
+ns.sub. IN A 194.0.14.1
+ENTRY_END
+
+RANGE_END
+
+
+; ns.sub.
+RANGE_BEGIN 0 100
+ ADDRESS 194.0.14.1
+
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+ns.sub. IN A
+SECTION ANSWER
+ns.sub. IN A 194.0.14.1
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+ns.sub. IN AAAA
+SECTION ANSWER
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+n7.sub. IN A
+SECTION ANSWER
+n7.sub. IN CNAME n8.sub.
+n8.sub. IN CNAME n9.sub.
+n9.sub. IN CNAME n10.sub.
+n10.sub. IN CNAME n11.sub.
+n11.sub. IN CNAME n12.sub.
+n12.sub. IN CNAME n13.sub.
+n13.sub. IN CNAME n14.sub.
+n14.sub. IN A 198.18.0.1
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qname qtype
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+loop7.sub. IN A
+SECTION ANSWER
+loop7.sub. IN CNAME loop8.sub.
+loop8.sub. IN CNAME loop9.sub.
+loop9.sub. IN CNAME loop10.sub.
+loop10.sub. IN CNAME loop11.sub.
+; loop11 -> loop7 -> ... -> loop11
+loop11.sub. IN CNAME loop7.sub.
+loop12.sub. IN CNAME loop13.sub.
+loop13.sub. IN CNAME loop14.sub.
+loop14.sub. IN A 198.18.0.1
+ENTRY_END
+
+RANGE_END
+
+; maximum allowed chain length
+STEP 10 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+n2.tld. IN A
+ENTRY_END
+
+STEP 11 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+n2.tld. IN A
+SECTION ANSWER
+n2.tld. IN CNAME n3.tld.
+n3.tld. IN CNAME n4.tld.
+n4.tld. IN CNAME n5.tld.
+n5.tld. IN CNAME n6.tld.
+n6.tld. IN CNAME n7.sub.
+n7.sub. IN CNAME n8.sub.
+n8.sub. IN CNAME n9.sub.
+n9.sub. IN CNAME n10.sub.
+n10.sub. IN CNAME n11.sub.
+n11.sub. IN CNAME n12.sub.
+n12.sub. IN CNAME n13.sub.
+n13.sub. IN CNAME n14.sub.
+n14.sub. IN A 198.18.0.1
+ENTRY_END
+
+
+; too long CNAME chain across two zones
+STEP 20 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+n1.tld. IN A
+ENTRY_END
+
+STEP 21 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA SERVFAIL
+SECTION QUESTION
+n1.tld. IN A
+SECTION ANSWER
+n1.tld. IN CNAME n2.tld.
+n2.tld. IN CNAME n3.tld.
+n3.tld. IN CNAME n4.tld.
+n4.tld. IN CNAME n5.tld.
+n5.tld. IN CNAME n6.tld.
+n6.tld. IN CNAME n7.sub.
+n7.sub. IN CNAME n8.sub.
+n8.sub. IN CNAME n9.sub.
+n9.sub. IN CNAME n10.sub.
+n10.sub. IN CNAME n11.sub.
+n11.sub. IN CNAME n12.sub.
+n12.sub. IN CNAME n13.sub.
+n13.sub. IN CNAME n14.sub.
+; This chain is too long (> 13):
+; n14.sub. IN A 198.18.0.1
+ENTRY_END
+
+
+; CNAME loop detection
+STEP 30 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+loop7.sub. IN A
+ENTRY_END
+
+STEP 31 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA SERVFAIL
+SECTION QUESTION
+loop7.sub. IN A
+SECTION ANSWER
+loop7.sub. IN CNAME loop8.sub.
+loop8.sub. IN CNAME loop9.sub.
+loop9.sub. IN CNAME loop10.sub.
+loop10.sub. IN CNAME loop11.sub.
+loop11.sub. IN CNAME loop7.sub.
+ENTRY_END
+
+SCENARIO_END
diff --git a/lib/layer/test.integr/iter_limit_bad_glueless.rpl b/lib/layer/test.integr/iter_limit_bad_glueless.rpl
new file mode 100644
index 0000000..73d4627
--- /dev/null
+++ b/lib/layer/test.integr/iter_limit_bad_glueless.rpl
@@ -0,0 +1,220 @@
+do-ip6: no
+; config options
+; target-fetch-policy: "0 0 0 0 0"
+; name: "."
+ stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET.
+CONFIG_END
+
+SCENARIO_BEGIN Test resolution with lame reply looks like nodata with noSOA
+
+; K.ROOT-SERVERS.NET.
+RANGE_BEGIN 0 100
+ ADDRESS 193.0.14.129
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+. IN NS K.ROOT-SERVERS.NET.
+SECTION ADDITIONAL
+K.ROOT-SERVERS.NET. IN A 193.0.14.129
+ENTRY_END
+
+; com
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+com. IN NS
+SECTION AUTHORITY
+com. IN NS a.gtld-servers.net.
+SECTION ADDITIONAL
+a.gtld-servers.net. IN A 192.5.6.30
+ENTRY_END
+
+; net
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+net. IN NS
+SECTION AUTHORITY
+net. IN NS e.gtld-servers.net.
+SECTION ADDITIONAL
+e.gtld-servers.net. IN A 192.12.94.30
+ENTRY_END
+
+RANGE_END
+
+; a.gtld-servers.net. - com
+RANGE_BEGIN 0 100
+ ADDRESS 192.5.6.30
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+com. IN NS
+SECTION ANSWER
+com. IN NS a.gtld-servers.net.
+SECTION ADDITIONAL
+a.gtld-servers.net. IN A 192.5.6.30
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+victim.com. IN NS
+SECTION AUTHORITY
+victim.com. IN NS ns.victim.com.
+SECTION ADDITIONAL
+ns.victim.com. IN A 1.2.3.55
+ENTRY_END
+RANGE_END
+
+; e.gtld-servers.net. - net
+RANGE_BEGIN 0 100
+ ADDRESS 192.12.94.30
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+net. IN NS
+SECTION ANSWER
+net. IN NS e.gtld-servers.net.
+SECTION ADDITIONAL
+e.gtld-servers.net. IN A 192.12.94.30
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+attacker.net. IN NS
+SECTION AUTHORITY
+attacker.net. IN NS ns.attacker.net.
+SECTION ADDITIONAL
+ns.attacker.net. IN A 1.2.3.44
+ENTRY_END
+RANGE_END
+
+; ns.attacker.net.
+RANGE_BEGIN 0 100
+ ADDRESS 1.2.3.44
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+attacker.net. IN NS
+SECTION ANSWER
+attacker.net. IN NS ns.attacker.net.
+SECTION ADDITIONAL
+ns.attacker.net. IN A 1.2.3.44
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+ns.attacker.net. IN A
+SECTION ANSWER
+ns.attacker.net. IN A 1.2.3.44
+SECTION AUTHORITY
+attacker.net. IN NS ns.attacker.net.
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+ns.attacker.net. IN AAAA
+SECTION AUTHORITY
+SECTION ADDITIONAL
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+sub.attacker.net. IN NS
+SECTION AUTHORITY
+sub.attacker.net. IN NS ns1.victim.com.
+sub.attacker.net. IN NS ns2.victim.com.
+sub.attacker.net. IN NS ns3.victim.com.
+sub.attacker.net. IN NS ns4.victim.com.
+sub.attacker.net. IN NS ns5.victim.com.
+sub.attacker.net. IN NS ns6.victim.com.
+sub.attacker.net. IN NS ns7.victim.com.
+sub.attacker.net. IN NS ns8.victim.com.
+sub.attacker.net. IN NS ns9.victim.com.
+ENTRY_END
+RANGE_END
+
+; ns.victim.com.
+; returns NXDOMAIN for all queries (attacker generated NS names are not present)
+RANGE_BEGIN 0 100
+ ADDRESS 1.2.3.55
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NXDOMAIN
+SECTION QUESTION
+victim.com. IN NS
+SECTION AUTHORITY
+victim.com. 0 IN SOA . . 1 1 1 1 1
+SECTION ADDITIONAL
+ENTRY_END
+RANGE_END
+
+
+STEP 10 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+www.sub.attacker.net. IN A
+ENTRY_END
+
+; in any case we must get SERVFAIL, no deleation works
+STEP 11 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA SERVFAIL
+SECTION QUESTION
+www.sub.attacker.net. IN A
+SECTION ANSWER
+ENTRY_END
+
+; recursion happens here
+STEP 20 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+glueless.trigger.check.max.number.of.upstream.queries. IN TXT
+ENTRY_END
+
+STEP 21 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR AA RD RA NOERROR
+SECTION QUESTION
+glueless.trigger.check.max.number.of.upstream.queries. IN TXT
+SECTION ANSWER
+glueless.trigger.check.max.number.of.upstream.queries. IN TXT "pass"
+SECTION AUTHORITY
+SECTION ADDITIONAL
+ENTRY_END
+
+
+SCENARIO_END
diff --git a/lib/layer/test.integr/iter_limit_refuse.rpl b/lib/layer/test.integr/iter_limit_refuse.rpl
new file mode 100644
index 0000000..285b5af
--- /dev/null
+++ b/lib/layer/test.integr/iter_limit_refuse.rpl
@@ -0,0 +1,150 @@
+do-ip6: no
+; config options
+;server:
+ stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET.
+CONFIG_END
+
+SCENARIO_BEGIN Outrageous number of auth servers return REFUSED. Simulates NXNSAttack misusing wildcard which points to victim's DNS server. Lua config checks if number of outgoing queries is within limits.
+
+; K.ROOT-SERVERS.NET.
+RANGE_BEGIN 0 100
+ ADDRESS 193.0.14.129
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+. IN NS
+SECTION ANSWER
+. IN NS K.ROOT-SERVERS.NET.
+SECTION ADDITIONAL
+K.ROOT-SERVERS.NET. IN A 193.0.14.129
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+com. IN A
+SECTION AUTHORITY
+com. IN NS a.gtld-servers.net.
+SECTION ADDITIONAL
+a.gtld-servers.net. IN A 192.5.6.30
+ENTRY_END
+RANGE_END
+
+; a.gtld-servers.net.
+RANGE_BEGIN 0 100
+ ADDRESS 192.5.6.30
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR NOERROR
+SECTION QUESTION
+com. IN NS
+SECTION ANSWER
+com. IN NS a.gtld-servers.net.
+SECTION ADDITIONAL
+a.gtld-servers.net. IN A 192.5.6.30
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode subdomain
+ADJUST copy_id copy_query
+REPLY QR NOERROR
+SECTION QUESTION
+example.com. IN A
+SECTION AUTHORITY
+example.com. IN NS ns10.example.com.
+example.com. IN NS ns11.example.com.
+example.com. IN NS ns12.example.com.
+example.com. IN NS ns13.example.com.
+example.com. IN NS ns14.example.com.
+example.com. IN NS ns15.example.com.
+example.com. IN NS ns16.example.com.
+example.com. IN NS ns17.example.com.
+example.com. IN NS ns18.example.com.
+example.com. IN NS ns19.example.com.
+SECTION ADDITIONAL
+ns10.example.com. IN A 1.2.3.10
+ns11.example.com. IN A 1.2.3.11
+ns12.example.com. IN A 1.2.3.12
+ns13.example.com. IN A 1.2.3.13
+ns14.example.com. IN A 1.2.3.14
+ns15.example.com. IN A 1.2.3.15
+ns16.example.com. IN A 1.2.3.16
+ns17.example.com. IN A 1.2.3.17
+ns18.example.com. IN A 1.2.3.18
+ns19.example.com. IN A 1.2.3.19
+
+ENTRY_END
+RANGE_END
+
+; ns1.example.com.
+RANGE_BEGIN 0 100
+ ADDRESS 1.2.3.10
+ ADDRESS 1.2.3.11
+ ADDRESS 1.2.3.12
+ ADDRESS 1.2.3.13
+ ADDRESS 1.2.3.14
+ ADDRESS 1.2.3.15
+ ADDRESS 1.2.3.16
+ ADDRESS 1.2.3.17
+ ADDRESS 1.2.3.18
+ ADDRESS 1.2.3.19
+ENTRY_BEGIN
+MANDATORY
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA REFUSED
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+SECTION AUTHORITY
+SECTION ADDITIONAL
+ENTRY_END
+RANGE_END
+
+
+; recursion happens here
+STEP 10 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+www.example.com. IN A
+ENTRY_END
+
+; in any case we must get SERVFAIL, no auth works
+STEP 11 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA DO SERVFAIL
+SECTION QUESTION
+www.example.com. IN A
+SECTION ANSWER
+SECTION AUTHORITY
+SECTION ADDITIONAL
+ENTRY_END
+
+; recursion happens here
+STEP 20 QUERY
+ENTRY_BEGIN
+REPLY RD DO
+SECTION QUESTION
+refused.trigger.check.max.number.of.upstream.queries. IN TXT
+ENTRY_END
+
+STEP 21 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR AA RD RA NOERROR
+SECTION QUESTION
+refused.trigger.check.max.number.of.upstream.queries. IN TXT
+SECTION ANSWER
+refused.trigger.check.max.number.of.upstream.queries. IN TXT "pass"
+SECTION AUTHORITY
+SECTION ADDITIONAL
+ENTRY_END
+
+SCENARIO_END
diff --git a/lib/layer/test.integr/kresd_config.j2 b/lib/layer/test.integr/kresd_config.j2
new file mode 100644
index 0000000..dc18a1b
--- /dev/null
+++ b/lib/layer/test.integr/kresd_config.j2
@@ -0,0 +1,107 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local ffi = require('ffi')
+
+-- hook for iter_refuse_toomany.rpl
+local function check_max_number_of_upstream_queries(maxcnt)
+ return function (state, req)
+ local vals = worker.stats()
+ local upstream_packets = vals.ipv4 + vals.ipv6
+ log_info(ffi.C.LOG_GRP_TESTS, '%d packets sent to upstream', upstream_packets)
+ local answ_f
+ if upstream_packets > maxcnt then -- . + com. + ????
+ answ_f = policy.ANSWER(
+ { [kres.type.TXT] = { ttl=300, rdata='\4fail' } })
+
+ else
+ answ_f = policy.ANSWER(
+ { [kres.type.TXT] = { ttl=300, rdata='\4pass' } })
+ end
+ return answ_f(state, req)
+ end
+end
+
+policy.add(
+ policy.suffix(check_max_number_of_upstream_queries(8),
+ policy.todnames({'refused.trigger.check.max.number.of.upstream.queries.'})
+ )
+)
+policy.add(
+ policy.suffix(check_max_number_of_upstream_queries(16),
+ policy.todnames({'glueless.trigger.check.max.number.of.upstream.queries.'})
+ )
+)
+
+-- hook end iter_refuse_toomany.rpl
+
+
+trust_anchors.remove('.')
+{% for TAF in TRUST_ANCHOR_FILES %}
+-- trust_anchors.add_file('{{TAF}}')
+{% endfor %}
+
+{% raw %}
+-- Disable RFC5011 TA update
+if ta_update then
+ modules.unload('ta_update')
+end
+
+-- Disable RFC8145 signaling, scenario doesn't provide expected answers
+if ta_signal_query then
+ modules.unload('ta_signal_query')
+end
+
+-- Disable RFC8109 priming, scenario doesn't provide expected answers
+if priming then
+ modules.unload('priming')
+end
+
+-- Disable this module because it make one priming query
+if detect_time_skew then
+ modules.unload('detect_time_skew')
+end
+
+_hint_root_file('hints')
+cache.size = 2*MB
+log_level('debug')
+policy.add(policy.all(policy.DEBUG_ALWAYS))
+{% endraw %}
+
+net = { '{{SELF_ADDR}}' }
+
+{% if DO_IP6 == "true" %}
+net.ipv6 = true
+{% else %}
+net.ipv6 = false
+{% endif %}
+
+{% if DO_IP4 == "true" %}
+net.ipv4 = true
+{% else %}
+net.ipv4 = false
+{% endif %}
+
+
+{% if QMIN == "false" %}
+option('NO_MINIMIZE', true)
+{% else %}
+option('NO_MINIMIZE', false)
+{% endif %}
+
+
+-- Self-checks on globals
+assert(help() ~= nil)
+assert(worker.id ~= nil)
+-- Self-checks on facilities
+assert(cache.count() == 0)
+assert(cache.stats() ~= nil)
+assert(cache.backends() ~= nil)
+assert(worker.stats() ~= nil)
+assert(net.interfaces() ~= nil)
+-- Self-checks on loaded stuff
+assert(net.list()[1].transport.ip == '{{SELF_ADDR}}')
+assert(#modules.list() > 0)
+-- Self-check timers
+ev = event.recurrent(1 * sec, function (ev) return 1 end)
+event.cancel(ev)
+ev = event.after(0, function (ev) return 1 end)
diff --git a/lib/layer/validate.c b/lib/layer/validate.c
new file mode 100644
index 0000000..3bdb205
--- /dev/null
+++ b/lib/layer/validate.c
@@ -0,0 +1,1396 @@
+/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz>
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include <errno.h>
+#include <sys/time.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <contrib/cleanup.h>
+#include <libknot/packet/wire.h>
+#include <libknot/rrtype/rdname.h>
+#include <libknot/rrtype/rrsig.h>
+#include <libdnssec/error.h>
+
+#include "lib/dnssec/nsec.h"
+#include "lib/dnssec/nsec3.h"
+#include "lib/dnssec/ta.h"
+#include "lib/dnssec.h"
+#include "lib/layer.h"
+#include "lib/resolve.h"
+#include "lib/rplan.h"
+#include "lib/utils.h"
+#include "lib/defines.h"
+#include "lib/module.h"
+#include "lib/selection.h"
+
+#define VERBOSE_MSG(qry, ...) kr_log_q(qry, VALIDATOR, __VA_ARGS__)
+
+#define MAX_REVALIDATION_CNT 2
+
+/**
+ * Search in section for given type.
+ * @param sec Packet section.
+ * @param type Type to search for.
+ * @return True if found.
+ */
+static bool section_has_type(const knot_pktsection_t *sec, uint16_t type)
+{
+ if (!sec) {
+ return false;
+ }
+
+ for (unsigned i = 0; i < sec->count; ++i) {
+ const knot_rrset_t *rr = knot_pkt_rr(sec, i);
+ if (rr->type == type) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool pkt_has_type(const knot_pkt_t *pkt, uint16_t type)
+{
+ if (!pkt) {
+ return false;
+ }
+
+ if (section_has_type(knot_pkt_section(pkt, KNOT_ANSWER), type)) {
+ return true;
+ }
+ if (section_has_type(knot_pkt_section(pkt, KNOT_AUTHORITY), type)) {
+ return true;
+ }
+ return section_has_type(knot_pkt_section(pkt, KNOT_ADDITIONAL), type);
+}
+
+static void log_bogus_rrsig(kr_rrset_validation_ctx_t *vctx,
+ const knot_rrset_t *rr, const char *msg) {
+ if (kr_log_is_debug_qry(VALIDATOR, vctx->log_qry)) {
+ auto_free char *name_text = kr_dname_text(rr->owner);
+ auto_free char *type_text = kr_rrtype_text(rr->type);
+ VERBOSE_MSG(vctx->log_qry, ">< %s: %s %s "
+ "(%u matching RRSIGs, %u expired, %u not yet valid, "
+ "%u invalid signer, %u invalid label count, %u invalid key, "
+ "%u invalid crypto, %u invalid NSEC)\n",
+ msg, name_text, type_text, vctx->rrs_counters.matching_name_type,
+ vctx->rrs_counters.expired, vctx->rrs_counters.notyet,
+ vctx->rrs_counters.signer_invalid, vctx->rrs_counters.labels_invalid,
+ vctx->rrs_counters.key_invalid, vctx->rrs_counters.crypto_invalid,
+ vctx->rrs_counters.nsec_invalid);
+ }
+}
+
+/** Check that given CNAME could be generated by given DNAME (no DNSSEC validation). */
+static bool cname_matches_dname(const knot_rrset_t *rr_cn, const knot_rrset_t *rr_dn)
+{
+ if (kr_fails_assert(rr_cn->type == KNOT_RRTYPE_CNAME && rr_dn->type == KNOT_RRTYPE_DNAME))
+ return false;
+ /* When DNAME substitution happens, let's consider the "prefix"
+ * that is carried over and the "suffix" that is replaced.
+ * (Here we consider the label order used in wire and presentation.) */
+ const int prefix_labels = knot_dname_in_bailiwick(rr_cn->owner, rr_dn->owner);
+ if (prefix_labels < 1)
+ return false;
+ const knot_dname_t *cn_target = knot_cname_name(rr_cn->rrs.rdata);
+ const knot_dname_t *dn_target = knot_dname_target(rr_dn->rrs.rdata);
+ /* ^ We silently use the first RR in each RRset. Could be e.g. logged. */
+ /* Check that the suffixes are correct - and even prefix label counts. */
+ if (knot_dname_in_bailiwick(cn_target, dn_target) != prefix_labels)
+ return false;
+ /* Check that prefixes match. Find end of the first one and compare. */
+ const knot_dname_t *cn_se = rr_cn->owner;
+ for (int i = 0; i < prefix_labels; ++i)
+ cn_se += 1 + *cn_se;
+ return strncmp((const char *)rr_cn->owner, (const char *)cn_target,
+ cn_se - rr_cn->owner) == 0;
+ /* ^ We use the fact that dnames are always zero-terminated
+ * to avoid any possible over-read in cn_target. */
+}
+
+static void mark_insecure_parents(const struct kr_query *qry);
+static void rank_records(struct kr_query *qry, bool any_rank, enum kr_rank rank_to_set,
+ const knot_dname_t *bailiwick);
+
+static bool maybe_downgrade_nsec3(const ranked_rr_array_entry_t *e, struct kr_query *qry,
+ const kr_rrset_validation_ctx_t *vctx)
+{
+ bool required_conditions =
+ e->rr->type == KNOT_RRTYPE_NSEC3
+ && kr_rank_test(e->rank, KR_RANK_SECURE)
+ // extra careful: avoid downgrade if SNAME isn't in bailiwick of signer
+ && knot_dname_in_bailiwick(qry->sname, vctx->zone_name) >= 0;
+ if (!required_conditions)
+ return false;
+
+ const knot_rdataset_t *rrs = &e->rr->rrs;
+ knot_rdata_t *rd = rrs->rdata;
+ for (int j = 0; j < rrs->count; ++j, rd = knot_rdataset_next(rd)) {
+ if (kr_nsec3_limited_rdata(rd))
+ goto do_downgrade;
+ }
+ return false;
+
+do_downgrade: // we do this deep inside calls because of having signer name available
+ VERBOSE_MSG(qry,
+ "<= DNSSEC downgraded due to expensive NSEC3: %d iterations, %d salt length\n",
+ (int)knot_nsec3_iters(rd), (int)knot_nsec3_salt_len(rd));
+ qry->flags.DNSSEC_WANT = false;
+ qry->flags.DNSSEC_INSECURE = true;
+ rank_records(qry, true, KR_RANK_INSECURE, vctx->zone_name);
+ mark_insecure_parents(qry);
+ return true;
+}
+
+#define KNOT_EDOWNGRADED (KNOT_ERROR_MIN - 1)
+
+static int validate_section(kr_rrset_validation_ctx_t *vctx, struct kr_query *qry,
+ knot_mm_t *pool)
+{
+ struct kr_request *req = qry->request;
+ if (!vctx) {
+ return kr_error(EINVAL);
+ }
+
+ /* Can't use qry->zone_cut.name directly, as this name can
+ * change when updating cut information before validation.
+ */
+ vctx->zone_name = vctx->keys ? vctx->keys->owner : NULL;
+
+ for (ssize_t i = 0; i < vctx->rrs->len; ++i) {
+ ranked_rr_array_entry_t *entry = vctx->rrs->at[i];
+ knot_rrset_t * const rr = entry->rr;
+
+ if (entry->yielded || vctx->qry_uid != entry->qry_uid) {
+ continue;
+ }
+
+ if (kr_rank_test(entry->rank, KR_RANK_OMIT)
+ || kr_rank_test(entry->rank, KR_RANK_SECURE)) {
+ continue; /* these are already OK */
+ }
+
+ if (!knot_dname_is_equal(qry->zone_cut.name, rr->owner)/*optim.*/
+ && !kr_ta_closest(qry->request->ctx, rr->owner, rr->type)) {
+ /* We have NTA "between" our (perceived) zone cut and the RR. */
+ kr_rank_set(&entry->rank, KR_RANK_INSECURE);
+ continue;
+ }
+
+ if (rr->type == KNOT_RRTYPE_RRSIG) {
+ const knot_dname_t *signer_name = knot_rrsig_signer_name(rr->rrs.rdata);
+ if (!knot_dname_is_equal(vctx->zone_name, signer_name)) {
+ kr_rank_set(&entry->rank, KR_RANK_MISMATCH);
+ vctx->err_cnt += 1;
+ break;
+ }
+ if (!kr_rank_test(entry->rank, KR_RANK_BOGUS))
+ kr_rank_set(&entry->rank, KR_RANK_OMIT);
+ continue;
+ }
+
+ uint8_t rank_orig = entry->rank;
+ int validation_result = kr_rrset_validate(vctx, rr);
+
+ /* Handle the case of CNAMEs synthesized from DNAMEs (they don't have RRSIGs). */
+ if (rr->type == KNOT_RRTYPE_CNAME && validation_result == kr_error(ENOENT)) {
+ for (ssize_t j = 0; j < vctx->rrs->len; ++j) {
+ ranked_rr_array_entry_t *e_dname = vctx->rrs->at[j];
+ if ((e_dname->rr->type == KNOT_RRTYPE_DNAME)
+ /* If the order is wrong, we will need two passes. */
+ && kr_rank_test(e_dname->rank, KR_RANK_SECURE)
+ && cname_matches_dname(rr, e_dname->rr)) {
+ /* Now we believe the CNAME is OK. */
+ validation_result = kr_ok();
+ break;
+ }
+ }
+ if (validation_result != kr_ok()) {
+ vctx->cname_norrsig_cnt += 1;
+ }
+ }
+
+ if (validation_result == kr_ok()) {
+ kr_rank_set(&entry->rank, KR_RANK_SECURE);
+
+ /* Downgrade zone to insecure if certain NSEC3 record occurs. */
+ if (unlikely(maybe_downgrade_nsec3(entry, qry, vctx)))
+ return kr_error(KNOT_EDOWNGRADED);
+
+ } else if (kr_rank_test(rank_orig, KR_RANK_TRY)) {
+ /* RFC 4035 section 2.2:
+ * NS RRsets that appear at delegation points (...)
+ * MUST NOT be signed */
+ if (vctx->rrs_counters.matching_name_type > 0)
+ log_bogus_rrsig(vctx, rr,
+ "found unexpected signatures for non-authoritative data which failed to validate, continuing");
+ vctx->result = kr_ok();
+ kr_rank_set(&entry->rank, KR_RANK_TRY);
+ /* ^^ BOGUS would be more accurate, but it might change
+ * to MISMATCH on revalidation, e.g. in test val_referral_nods :-/
+ */
+
+ } else if (validation_result == kr_error(ENOENT)
+ && vctx->rrs_counters.matching_name_type == 0) {
+ /* no RRSIGs found */
+ kr_rank_set(&entry->rank, KR_RANK_MISSING);
+ vctx->err_cnt += 1;
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_RRSIG_MISS, "JZAJ");
+ log_bogus_rrsig(vctx, rr, "no valid RRSIGs found");
+ } else {
+ kr_rank_set(&entry->rank, KR_RANK_BOGUS);
+ vctx->err_cnt += 1;
+ if (vctx->rrs_counters.expired > 0)
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_EXPIRED, "YFJ2");
+ else if (vctx->rrs_counters.notyet > 0)
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_NOTYET, "UBBS");
+ else
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "I74V");
+ log_bogus_rrsig(vctx, rr, "bogus signatures");
+ }
+ }
+ return kr_ok();
+}
+
+static int validate_records(struct kr_request *req, knot_pkt_t *answer, knot_mm_t *pool, bool has_nsec3)
+{
+ struct kr_query *qry = req->current_query;
+ if (!qry->zone_cut.key) {
+ VERBOSE_MSG(qry, "<= no DNSKEY, can't validate\n");
+ return kr_error(EBADMSG);
+ }
+
+ kr_rrset_validation_ctx_t vctx = {
+ .pkt = answer,
+ .rrs = &req->answ_selected,
+ .section_id = KNOT_ANSWER,
+ .keys = qry->zone_cut.key,
+ .zone_name = qry->zone_cut.name,
+ .timestamp = qry->timestamp.tv_sec,
+ .ttl_min = req->ctx->cache.ttl_min,
+ .qry_uid = qry->uid,
+ .has_nsec3 = has_nsec3,
+ .flags = 0,
+ .err_cnt = 0,
+ .cname_norrsig_cnt = 0,
+ .result = 0,
+ .limit_crypto_remains = &qry->vld_limit_crypto_remains,
+ .log_qry = qry,
+ };
+
+ int ret = validate_section(&vctx, qry, pool);
+ if (vctx.err_cnt && vctx.err_cnt == vctx.cname_norrsig_cnt) {
+ VERBOSE_MSG(qry, ">< all validation errors are missing RRSIGs on CNAMES, trying again in hope for DNAMEs\n");
+ vctx.err_cnt = vctx.cname_norrsig_cnt = vctx.result = 0;
+ ret = validate_section(&vctx, qry, pool);
+ }
+ req->answ_validated = (vctx.err_cnt == 0);
+ if (ret != kr_ok()) {
+ return ret;
+ }
+
+ uint32_t an_flags = vctx.flags;
+ vctx.rrs = &req->auth_selected;
+ vctx.section_id = KNOT_AUTHORITY;
+ vctx.flags = 0;
+ vctx.err_cnt = 0;
+ vctx.result = 0;
+
+ ret = validate_section(&vctx, qry, pool);
+ req->auth_validated = (vctx.err_cnt == 0);
+ if (ret != kr_ok()) {
+ return ret;
+ }
+
+ /* Records were validated.
+ * If there is wildcard expansion in answer,
+ * or optout - flag the query.
+ */
+ if (an_flags & KR_DNSSEC_VFLG_WEXPAND) {
+ qry->flags.DNSSEC_WEXPAND = true;
+ }
+ if (an_flags & KR_DNSSEC_VFLG_OPTOUT) {
+ qry->flags.DNSSEC_OPTOUT = true;
+ }
+
+ return ret;
+}
+
+static int validate_keyset(struct kr_request *req, knot_pkt_t *answer, bool has_nsec3)
+{
+ /* Merge DNSKEY records from answer that are below/at current cut. */
+ struct kr_query *qry = req->current_query;
+ bool updated_key = false;
+ const knot_pktsection_t *an = knot_pkt_section(answer, KNOT_ANSWER);
+ for (unsigned i = 0; i < an->count; ++i) {
+ const knot_rrset_t *rr = knot_pkt_rr(an, i);
+ if (rr->type != KNOT_RRTYPE_DNSKEY
+ || knot_dname_in_bailiwick(rr->owner, qry->zone_cut.name) < 0) {
+ continue;
+ }
+ /* Merge with zone cut (or replace ancestor key). */
+ if (!qry->zone_cut.key || !knot_dname_is_equal(qry->zone_cut.key->owner, rr->owner)) {
+ qry->zone_cut.key = knot_rrset_copy(rr, qry->zone_cut.pool);
+ if (!qry->zone_cut.key) {
+ return kr_error(ENOMEM);
+ }
+ updated_key = true;
+ } else {
+ int ret = knot_rdataset_merge(&qry->zone_cut.key->rrs,
+ &rr->rrs, qry->zone_cut.pool);
+ if (ret != 0) {
+ knot_rrset_free(qry->zone_cut.key, qry->zone_cut.pool);
+ qry->zone_cut.key = NULL;
+ return ret;
+ }
+ updated_key = true;
+ }
+ }
+
+ /* Check if there's a key for current TA. */
+ if (updated_key && !(qry->flags.CACHED)) {
+ /* Find signatures for the DNSKEY; selected by iterator from ANSWER. */
+ int sig_index = -1;
+ for (int i = req->answ_selected.len - 1; i >= 0; --i) {
+ const knot_rrset_t *rrsig = req->answ_selected.at[i]->rr;
+ const bool ok = req->answ_selected.at[i]->qry_uid == qry->uid
+ && rrsig->type == KNOT_RRTYPE_RRSIG
+ && knot_rrsig_type_covered(rrsig->rrs.rdata)
+ == KNOT_RRTYPE_DNSKEY
+ && rrsig->rclass == KNOT_CLASS_IN
+ && knot_dname_is_equal(rrsig->owner,
+ qry->zone_cut.key->owner);
+ if (ok) {
+ sig_index = i;
+ break;
+ }
+ }
+ if (sig_index < 0) {
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_RRSIG_MISS, "EZDC");
+ return kr_error(ENOENT);
+ }
+ const knot_rdataset_t *sig_rds = &req->answ_selected.at[sig_index]->rr->rrs;
+
+ kr_rrset_validation_ctx_t vctx = {
+ .pkt = answer,
+ .rrs = &req->answ_selected,
+ .section_id = KNOT_ANSWER,
+ .keys = qry->zone_cut.key,
+ .zone_name = qry->zone_cut.name,
+ .timestamp = qry->timestamp.tv_sec,
+ .ttl_min = req->ctx->cache.ttl_min,
+ .qry_uid = qry->uid,
+ .has_nsec3 = has_nsec3,
+ .flags = 0,
+ .result = 0,
+ .limit_crypto_remains = &qry->vld_limit_crypto_remains,
+ .log_qry = qry,
+ };
+ int ret = kr_dnskeys_trusted(&vctx, sig_rds, qry->zone_cut.trust_anchor);
+ /* Set rank of the RRSIG. This may be needed, but I don't know why.
+ * In particular, black_ent.rpl may get broken otherwise. */
+ kr_rank_set(&req->answ_selected.at[sig_index]->rank,
+ ret == 0 ? KR_RANK_SECURE : KR_RANK_BOGUS);
+
+ if (ret != 0) {
+ log_bogus_rrsig(&vctx, qry->zone_cut.key, "bogus key");
+ knot_rrset_free(qry->zone_cut.key, qry->zone_cut.pool);
+ qry->zone_cut.key = NULL;
+ if (vctx.rrs_counters.expired > 0)
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_EXPIRED, "6GJV");
+ else if (vctx.rrs_counters.notyet > 0)
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_NOTYET, "4DJQ");
+ else
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "EXRU");
+ return ret;
+ }
+
+ if (vctx.flags & KR_DNSSEC_VFLG_WEXPAND) {
+ qry->flags.DNSSEC_WEXPAND = true;
+ }
+ if (vctx.flags & KR_DNSSEC_VFLG_OPTOUT) {
+ qry->flags.DNSSEC_OPTOUT = true;
+ }
+
+ }
+ return kr_ok();
+}
+
+static knot_rrset_t *update_ds(struct kr_zonecut *cut, const knot_pktsection_t *sec)
+{
+ /* Aggregate DS records (if using multiple keys) */
+ knot_rrset_t *new_ds = NULL;
+ for (unsigned i = 0; i < sec->count; ++i) {
+ const knot_rrset_t *rr = knot_pkt_rr(sec, i);
+ if (rr->type != KNOT_RRTYPE_DS) {
+ continue;
+ }
+ int ret = 0;
+ if (new_ds) {
+ ret = knot_rdataset_merge(&new_ds->rrs, &rr->rrs, cut->pool);
+ } else {
+ new_ds = knot_rrset_copy(rr, cut->pool);
+ if (!new_ds) {
+ return NULL;
+ }
+ }
+ if (ret != 0) {
+ knot_rrset_free(new_ds, cut->pool);
+ return NULL;
+ }
+ }
+ return new_ds;
+}
+
+static void mark_insecure_parents(const struct kr_query *qry)
+{
+ /* If there is a chain of DS queries mark all of them,
+ * then mark first non-DS parent.
+ * Stop if parent is waiting for ns address.
+ * NS can be located at unsigned zone, but still will return
+ * valid DNSSEC records for initial query. */
+ struct kr_query *parent = qry->parent;
+ while (parent && !parent->flags.AWAIT_IPV4 && !parent->flags.AWAIT_IPV6) {
+ parent->flags.DNSSEC_WANT = false;
+ parent->flags.DNSSEC_INSECURE = true;
+ if (parent->stype != KNOT_RRTYPE_DS &&
+ parent->stype != KNOT_RRTYPE_RRSIG) {
+ break;
+ }
+ parent = parent->parent;
+ }
+}
+
+static int update_parent_keys(struct kr_request *req, uint16_t answer_type)
+{
+ struct kr_query *qry = req->current_query;
+ struct kr_query *parent = qry->parent;
+ if (kr_fails_assert(parent))
+ return KR_STATE_FAIL;
+ switch(answer_type) {
+ case KNOT_RRTYPE_DNSKEY:
+ VERBOSE_MSG(qry, "<= parent: updating DNSKEY\n");
+ parent->zone_cut.key = knot_rrset_copy(qry->zone_cut.key, parent->zone_cut.pool);
+ if (!parent->zone_cut.key) {
+ return KR_STATE_FAIL;
+ }
+ break;
+ case KNOT_RRTYPE_DS:
+ VERBOSE_MSG(qry, "<= parent: updating DS\n");
+ if (qry->flags.DNSSEC_INSECURE) { /* DS non-existence proven. */
+ mark_insecure_parents(qry);
+ } else if (qry->flags.DNSSEC_NODS && !qry->flags.FORWARD) {
+ if (qry->flags.DNSSEC_OPTOUT) {
+ mark_insecure_parents(qry);
+ } else {
+ int ret = kr_dnssec_matches_name_and_type(&req->auth_selected, qry->uid,
+ qry->sname, KNOT_RRTYPE_NS);
+ if (ret == kr_ok()) {
+ mark_insecure_parents(qry);
+ }
+ }
+ } else if (qry->flags.DNSSEC_NODS && qry->flags.FORWARD) {
+ int ret = kr_dnssec_matches_name_and_type(&req->auth_selected, qry->uid,
+ qry->sname, KNOT_RRTYPE_NS);
+ if (ret == kr_ok()) {
+ mark_insecure_parents(qry);
+ }
+ } else { /* DS existence proven. */
+ parent->zone_cut.trust_anchor = knot_rrset_copy(qry->zone_cut.trust_anchor, parent->zone_cut.pool);
+ if (!parent->zone_cut.trust_anchor) {
+ return KR_STATE_FAIL;
+ }
+ }
+ break;
+ default: break;
+ }
+ return kr_ok();
+}
+
+static int update_delegation(struct kr_request *req, struct kr_query *qry, knot_pkt_t *answer, bool has_nsec3)
+{
+ struct kr_zonecut *cut = &qry->zone_cut;
+
+ /* RFC4035 3.1.4. authoritative must send either DS or proof of non-existence.
+ * If it contains neither, resolver must query the parent for the DS (RFC4035 5.2.).
+ * If DS exists, the referral is OK,
+ * otherwise referral is bogus (or an attempted downgrade attack).
+ */
+
+
+ unsigned section = KNOT_ANSWER;
+ const bool referral = !knot_wire_get_aa(answer->wire);
+ if (referral) {
+ section = KNOT_AUTHORITY;
+ } else if (knot_pkt_qtype(answer) == KNOT_RRTYPE_DS &&
+ !(qry->flags.CNAME) &&
+ (knot_wire_get_rcode(answer->wire) != KNOT_RCODE_NXDOMAIN)) {
+ section = KNOT_ANSWER;
+ } else { /* N/A */
+ return kr_ok();
+ }
+
+ int ret = 0;
+ const knot_dname_t *proved_name = knot_pkt_qname(answer);
+ /* Aggregate DS records (if using multiple keys) */
+ knot_rrset_t *new_ds = update_ds(cut, knot_pkt_section(answer, section));
+ if (!new_ds) {
+ /* No DS provided, check for proof of non-existence. */
+ if (!has_nsec3) {
+ if (referral) {
+ /* Check if it is referral to unsigned, rfc4035 5.2 */
+ ret = kr_nsec_ref_to_unsigned(&req->auth_selected,
+ qry->uid, proved_name);
+ } else {
+ /* No-data answer */
+ ret = kr_nsec_negative(&req->auth_selected, qry->uid,
+ proved_name, KNOT_RRTYPE_DS);
+ if (ret >= 0) {
+ if (ret == PKT_NODATA) {
+ ret = kr_ok();
+ } else {
+ ret = kr_error(ENOENT); // suspicious
+ }
+ }
+ }
+ } else {
+ if (referral) {
+ /* Check if it is referral to unsigned, rfc5155 8.9 */
+ ret = kr_nsec3_ref_to_unsigned(answer);
+ } else {
+ /* No-data answer, QTYPE is DS, rfc5155 8.6 */
+ ret = kr_nsec3_no_data(answer, KNOT_AUTHORITY, proved_name, KNOT_RRTYPE_DS);
+ }
+ if (ret == kr_error(KNOT_ERANGE)) {
+ /* Not bogus, going insecure due to optout */
+ ret = 0;
+ }
+ }
+
+ if (referral && qry->stype != KNOT_RRTYPE_DS &&
+ ret == DNSSEC_NOT_FOUND) {
+ /* referral,
+ * qtype is not KNOT_RRTYPE_DS, NSEC\NSEC3 were not found.
+ * Check if DS already was fetched. */
+ knot_rrset_t *ta = cut->trust_anchor;
+ if (knot_dname_is_equal(cut->name, ta->owner)) {
+ /* DS is OK */
+ ret = 0;
+ }
+ } else if (ret != 0) {
+ VERBOSE_MSG(qry, "<= bogus proof of DS non-existence\n");
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "Z4I6");
+ qry->flags.DNSSEC_BOGUS = true;
+ } else if (proved_name[0] != '\0') { /* don't go to insecure for . DS */
+ qry->flags.DNSSEC_NODS = true;
+ /* Rank the corresponding nonauth NS as insecure. */
+ for (int i = 0; i < req->auth_selected.len; ++i) {
+ ranked_rr_array_entry_t *ns = req->auth_selected.at[i];
+ if (ns->qry_uid != qry->uid
+ || !ns->rr
+ || ns->rr->type != KNOT_RRTYPE_NS) {
+ continue;
+ }
+ if (!referral && !knot_dname_is_equal(qry->sname, ns->rr->owner)) {
+ continue;
+ }
+ /* Found the record. Note: this is slightly fragile
+ * in case there were more NS records in the packet.
+ * As it is now for referrals, kr_nsec*_ref_to_unsigned consider
+ * (only) the first NS record in the packet. */
+ if (!kr_rank_test(ns->rank, KR_RANK_AUTH)) { /* sanity */
+ ns->rank = KR_RANK_INSECURE;
+ }
+ break;
+ }
+ }
+ return ret;
+ } else if (qry->flags.FORWARD && qry->parent) {
+ struct kr_query *parent = qry->parent;
+ parent->zone_cut.name = knot_dname_copy(qry->sname, parent->zone_cut.pool);
+ }
+
+ /* Extend trust anchor */
+ VERBOSE_MSG(qry, "<= DS: OK\n");
+ cut->trust_anchor = new_ds;
+ return ret;
+}
+
+static const knot_dname_t *find_first_signer(ranked_rr_array_t *arr, struct kr_query *qry)
+{
+ for (size_t i = 0; i < arr->len; ++i) {
+ ranked_rr_array_entry_t *entry = arr->at[i];
+ const knot_rrset_t *rr = entry->rr;
+ if (entry->yielded ||
+ (!kr_rank_test(entry->rank, KR_RANK_INITIAL) &&
+ !kr_rank_test(entry->rank, KR_RANK_TRY) &&
+ !kr_rank_test(entry->rank, KR_RANK_MISMATCH))) {
+ continue;
+ }
+ if (rr->type != KNOT_RRTYPE_RRSIG) {
+ continue;
+ }
+ const knot_dname_t *signame = knot_rrsig_signer_name(rr->rrs.rdata);
+ if (knot_dname_in_bailiwick(rr->owner, signame) >= 0) {
+ return signame;
+ } else {
+ /* otherwise it's some nonsense, so we skip it */
+ kr_log_q(qry, VALIDATOR, "protocol violation: "
+ "out-of-bailiwick RRSIG signer, skipping\n");
+ }
+ }
+ return NULL;
+}
+
+static const knot_dname_t *signature_authority(struct kr_request *req)
+{
+ const knot_dname_t *signer_name = find_first_signer(&req->answ_selected, req->current_query);
+ if (!signer_name) {
+ signer_name = find_first_signer(&req->auth_selected, req->current_query);
+ }
+ return signer_name;
+}
+
+static int rrsig_not_found(const kr_layer_t * const ctx, const knot_pkt_t * const pkt,
+ const knot_rrset_t * const rr)
+{
+ /* Signatures are missing. There might be a zone cut that we've skipped
+ * and transitions to insecure. That can commonly happen when iterating
+ * and both sides of that cut are served by the same IP address(es).
+ * We'll try proving that the name truly is insecure - by spawning
+ * a DS sub-query on a suitable QNAME.
+ */
+ struct kr_request * const req = ctx->req;
+ struct kr_query * const qry = req->current_query;
+
+ if (qry->flags.FORWARD || qry->flags.STUB) {
+ /* Undiscovered signed cuts can't happen in the current forwarding
+ * algorithm, so this function shouldn't be able to help. */
+ return KR_STATE_FAIL;
+ }
+
+ /* Find cut_next: the name at which to try finding the "missing" zone cut. */
+ const knot_dname_t * const cut_top = qry->zone_cut.name;
+ const int next_depth = knot_dname_in_bailiwick(rr->owner, cut_top);
+ if (next_depth <= 0) {
+ return KR_STATE_FAIL; // shouldn't happen, I think
+ }
+ /* Add one extra label to cur_top, i.e. descend one level below current zone cut */
+ const knot_dname_t * const cut_next = rr->owner +
+ kr_dname_prefixlen(rr->owner, next_depth - 1);
+
+ /* Spawn that DS sub-query. */
+ struct kr_query * const next = kr_rplan_push(&req->rplan, qry, cut_next,
+ rr->rclass, KNOT_RRTYPE_DS);
+ if (!next) {
+ return KR_STATE_FAIL;
+ }
+ kr_zonecut_init(&next->zone_cut, qry->zone_cut.name, &req->pool);
+ kr_zonecut_copy(&next->zone_cut, &qry->zone_cut);
+ kr_zonecut_copy_trust(&next->zone_cut, &qry->zone_cut);
+ next->flags.DNSSEC_WANT = true;
+ return KR_STATE_YIELD;
+}
+
+static int check_validation_result(kr_layer_t *ctx, const knot_pkt_t *pkt, ranked_rr_array_t *arr)
+{
+ int ret = KR_STATE_DONE;
+ struct kr_request *req = ctx->req;
+ struct kr_query *qry = req->current_query;
+ ranked_rr_array_entry_t *invalid_entry = NULL;
+ for (size_t i = 0; i < arr->len; ++i) {
+ ranked_rr_array_entry_t *entry = arr->at[i];
+ if (entry->yielded || entry->qry_uid != qry->uid) {
+ continue;
+ }
+ if (kr_rank_test(entry->rank, KR_RANK_MISMATCH)) {
+ invalid_entry = entry;
+ break;
+ } else if (kr_rank_test(entry->rank, KR_RANK_MISSING) &&
+ !invalid_entry) {
+ invalid_entry = entry;
+ } else if (kr_rank_test(entry->rank, KR_RANK_OMIT)) {
+ continue;
+ } else if (!kr_rank_test(entry->rank, KR_RANK_SECURE) &&
+ !invalid_entry) {
+ invalid_entry = entry;
+ }
+ }
+
+ if (!invalid_entry) {
+ return ret;
+ }
+
+ if (!kr_rank_test(invalid_entry->rank, KR_RANK_SECURE) &&
+ (++(invalid_entry->revalidation_cnt) > MAX_REVALIDATION_CNT)) {
+ VERBOSE_MSG(qry, "<= continuous revalidation, fails\n");
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_OTHER,
+ "4T4L: continuous revalidation");
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ }
+
+ const knot_rrset_t *rr = invalid_entry->rr;
+ if (kr_rank_test(invalid_entry->rank, KR_RANK_MISMATCH)) {
+ const knot_dname_t *signer_name = knot_rrsig_signer_name(rr->rrs.rdata);
+ if (knot_dname_in_bailiwick(signer_name, qry->zone_cut.name) > 0) {
+ qry->zone_cut.name = knot_dname_copy(signer_name, &req->pool);
+ qry->flags.AWAIT_CUT = true;
+ } else if (!knot_dname_is_equal(signer_name, qry->zone_cut.name)) {
+ if (qry->zone_cut.parent) {
+ memcpy(&qry->zone_cut, qry->zone_cut.parent, sizeof(qry->zone_cut));
+ } else {
+ qry->flags.AWAIT_CUT = true;
+ }
+ qry->zone_cut.name = knot_dname_copy(signer_name, &req->pool);
+ }
+ VERBOSE_MSG(qry, ">< cut changed (new signer), needs revalidation\n");
+ ret = KR_STATE_YIELD;
+ } else if (kr_rank_test(invalid_entry->rank, KR_RANK_MISSING)) {
+ ret = rrsig_not_found(ctx, pkt, rr);
+ } else if (!kr_rank_test(invalid_entry->rank, KR_RANK_SECURE)) {
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "NXJA");
+ qry->flags.DNSSEC_BOGUS = true;
+ ret = KR_STATE_FAIL;
+ }
+
+ return ret;
+}
+
+static bool check_empty_answer(kr_layer_t *ctx, knot_pkt_t *pkt)
+{
+ struct kr_request *req = ctx->req;
+ struct kr_query *qry = req->current_query;
+ ranked_rr_array_t *arr = &req->answ_selected;
+ size_t num_entries = 0;
+ for (size_t i = 0; i < arr->len; ++i) {
+ ranked_rr_array_entry_t *entry = arr->at[i];
+ const knot_rrset_t *rr = entry->rr;
+ if (rr->type == KNOT_RRTYPE_RRSIG && qry->stype != KNOT_RRTYPE_RRSIG) {
+ continue;
+ }
+ if (entry->qry_uid == qry->uid) {
+ ++num_entries;
+ }
+ }
+ const knot_pktsection_t *an = knot_pkt_section(pkt, KNOT_ANSWER);
+ return ((an->count != 0) && (num_entries == 0)) ? false : true;
+}
+
+static int unsigned_forward(kr_layer_t *ctx, knot_pkt_t *pkt)
+{
+ struct kr_request *req = ctx->req;
+ struct kr_query *qry = req->current_query;
+ const uint16_t qtype = knot_pkt_qtype(pkt);
+ const uint8_t pkt_rcode = knot_wire_get_rcode(pkt->wire);
+ bool nods = false;
+ bool ns_exist = true;
+ for (int i = 0; i < req->rplan.resolved.len; ++i) {
+ struct kr_query *q = req->rplan.resolved.at[i];
+ if (q->sclass == qry->sclass &&
+ q->stype == KNOT_RRTYPE_DS &&
+ knot_dname_is_equal(q->sname, qry->sname)) {
+ nods = true;
+ if (!(q->flags.DNSSEC_OPTOUT)) {
+ int ret = kr_dnssec_matches_name_and_type(&req->auth_selected, q->uid,
+ qry->sname, KNOT_RRTYPE_NS);
+ ns_exist = (ret == kr_ok());
+ }
+ }
+ }
+
+ if (nods && ns_exist && qtype == KNOT_RRTYPE_NS) {
+ qry->flags.DNSSEC_WANT = false;
+ qry->flags.DNSSEC_INSECURE = true;
+ if (qry->forward_flags.CNAME) {
+ if (kr_fails_assert(qry->cname_parent))
+ return KR_STATE_FAIL;
+ qry->cname_parent->flags.DNSSEC_WANT = false;
+ qry->cname_parent->flags.DNSSEC_INSECURE = true;
+ } else if (pkt_rcode == KNOT_RCODE_NOERROR && qry->parent != NULL) {
+ const knot_pktsection_t *sec = knot_pkt_section(pkt, KNOT_ANSWER);
+ const knot_rrset_t *rr = knot_pkt_rr(sec, 0);
+ if (rr->type == KNOT_RRTYPE_NS) {
+ qry->parent->zone_cut.name = knot_dname_copy(rr->owner, &req->pool);
+ qry->parent->flags.DNSSEC_WANT = false;
+ qry->parent->flags.DNSSEC_INSECURE = true;
+ }
+ }
+ while (qry->parent) {
+ qry = qry->parent;
+ qry->flags.DNSSEC_WANT = false;
+ qry->flags.DNSSEC_INSECURE = true;
+ if (qry->forward_flags.CNAME) {
+ if (kr_fails_assert(qry->cname_parent))
+ return KR_STATE_FAIL;
+ qry->cname_parent->flags.DNSSEC_WANT = false;
+ qry->cname_parent->flags.DNSSEC_INSECURE = true;
+ }
+ }
+ return KR_STATE_DONE;
+ }
+
+ if (ctx->state == KR_STATE_YIELD) {
+ return KR_STATE_DONE;
+ }
+
+ if (!nods && qtype != KNOT_RRTYPE_DS) {
+ struct kr_rplan *rplan = &req->rplan;
+ struct kr_query *next = kr_rplan_push(rplan, qry, qry->sname, qry->sclass, KNOT_RRTYPE_DS);
+ if (!next) {
+ return KR_STATE_FAIL;
+ }
+ kr_zonecut_set(&next->zone_cut, qry->zone_cut.name);
+ kr_zonecut_copy_trust(&next->zone_cut, &qry->zone_cut);
+ next->flags.DNSSEC_WANT = true;
+ }
+
+ return KR_STATE_YIELD;
+}
+
+static int check_signer(kr_layer_t *ctx, knot_pkt_t *pkt)
+{
+ struct kr_request *req = ctx->req;
+ struct kr_query *qry = req->current_query;
+ const knot_dname_t *ta_name = qry->zone_cut.trust_anchor ? qry->zone_cut.trust_anchor->owner : NULL;
+ const knot_dname_t *signer = signature_authority(req);
+ if (ta_name && (!signer || !knot_dname_is_equal(ta_name, signer))) {
+ /* check all newly added RRSIGs */
+ if (!signer) {
+ if (qry->flags.FORWARD) {
+ return unsigned_forward(ctx, pkt);
+ }
+ /* Not a DNSSEC-signed response. */
+ if (ctx->state == KR_STATE_YIELD) {
+ /* Already yielded for revalidation.
+ * It means that trust chain is OK and
+ * transition to INSECURE hasn't occurred.
+ * Let the validation logic ask about RRSIG. */
+ return KR_STATE_DONE;
+ }
+ /* Ask parent for DS
+ * to prove transition to INSECURE. */
+ const uint16_t qtype = knot_pkt_qtype(pkt);
+ const knot_dname_t *qname = knot_pkt_qname(pkt);
+ if (qtype == KNOT_RRTYPE_NS &&
+ knot_dname_in_bailiwick(qname, qry->zone_cut.name) > 0) {
+ /* Server is authoritative
+ * for both parent and child,
+ * and child zone is not signed. */
+ qry->zone_cut.name = knot_dname_copy(qname, &req->pool);
+ }
+ } else if (knot_dname_in_bailiwick(signer, qry->zone_cut.name) > 0) {
+ if (!(qry->flags.FORWARD)) {
+ /* Key signer is below current cut, advance and refetch keys. */
+ qry->zone_cut.name = knot_dname_copy(signer, &req->pool);
+ } else {
+ /* Check if DS does not exist. */
+ struct kr_query *q = kr_rplan_find_resolved(&req->rplan, NULL,
+ signer, qry->sclass, KNOT_RRTYPE_DS);
+ if (q && q->flags.DNSSEC_NODS) {
+ qry->flags.DNSSEC_WANT = false;
+ qry->flags.DNSSEC_INSECURE = true;
+ if (qry->parent) {
+ qry->parent->flags.DNSSEC_WANT = false;
+ qry->parent->flags.DNSSEC_INSECURE = true;
+ }
+ } else if (qry->stype != KNOT_RRTYPE_DS) {
+ struct kr_rplan *rplan = &req->rplan;
+ struct kr_query *next = kr_rplan_push(rplan, qry, qry->sname,
+ qry->sclass, KNOT_RRTYPE_DS);
+ if (!next) {
+ return KR_STATE_FAIL;
+ }
+ kr_zonecut_set(&next->zone_cut, qry->zone_cut.name);
+ kr_zonecut_copy_trust(&next->zone_cut, &qry->zone_cut);
+ next->flags.DNSSEC_WANT = true;
+ }
+ }
+ } else if (!knot_dname_is_equal(signer, qry->zone_cut.name)) {
+ /* Key signer is above the current cut, so we can't validate it. This happens when
+ a server is authoritative for both grandparent, parent and child zone.
+ Ascend to parent cut, and refetch authority for signer. */
+ if (qry->zone_cut.parent) {
+ memcpy(&qry->zone_cut, qry->zone_cut.parent, sizeof(qry->zone_cut));
+ } else {
+ qry->flags.AWAIT_CUT = true;
+ }
+ qry->zone_cut.name = knot_dname_copy(signer, &req->pool);
+ }
+
+ /* zone cut matches, but DS/DNSKEY doesn't => refetch. */
+ VERBOSE_MSG(qry, ">< cut changed, needs revalidation\n");
+ if ((qry->flags.FORWARD) && qry->stype != KNOT_RRTYPE_DS) {
+ struct kr_rplan *rplan = &req->rplan;
+ struct kr_query *next = kr_rplan_push(rplan, qry, signer,
+ qry->sclass, KNOT_RRTYPE_DS);
+ if (!next) {
+ return KR_STATE_FAIL;
+ }
+ kr_zonecut_set(&next->zone_cut, qry->zone_cut.name);
+ kr_zonecut_copy_trust(&next->zone_cut, &qry->zone_cut);
+ next->flags.DNSSEC_WANT = true;
+ return KR_STATE_YIELD;
+ }
+ if (!(qry->flags.FORWARD)) {
+ return KR_STATE_YIELD;
+ }
+ }
+ return KR_STATE_DONE;
+}
+
+/** Change ranks of RRs from this single iteration:
+ * _INITIAL or _TRY or _MISSING -> rank_to_set. Or any rank, if any_rank == true.
+ *
+ * Optionally do this only in a `bailiwick` (if not NULL).
+ * Iterator shouldn't have selected such records, but we check to be sure. */
+static void rank_records(struct kr_query *qry, bool any_rank, enum kr_rank rank_to_set,
+ const knot_dname_t *bailiwick)
+{
+ struct kr_request *req = qry->request;
+ ranked_rr_array_t *ptrs[2] = { &req->answ_selected, &req->auth_selected };
+ for (size_t i = 0; i < 2; ++i) {
+ ranked_rr_array_t *arr = ptrs[i];
+ for (size_t j = 0; j < arr->len; ++j) {
+ ranked_rr_array_entry_t *entry = arr->at[j];
+ if (entry->qry_uid != qry->uid) {
+ continue;
+ }
+ if (bailiwick && knot_dname_in_bailiwick(entry->rr->owner,
+ bailiwick) < 0) {
+ continue;
+ }
+ if (any_rank
+ || kr_rank_test(entry->rank, KR_RANK_INITIAL)
+ || kr_rank_test(entry->rank, KR_RANK_TRY)
+ || kr_rank_test(entry->rank, KR_RANK_MISSING)) {
+ kr_rank_set(&entry->rank, rank_to_set);
+ }
+ }
+ }
+}
+
+static void check_wildcard(kr_layer_t *ctx)
+{
+ struct kr_request *req = ctx->req;
+ struct kr_query *qry = req->current_query;
+ ranked_rr_array_t *ptrs[2] = { &req->answ_selected, &req->auth_selected };
+
+ for (int i = 0; i < 2; ++i) {
+ ranked_rr_array_t *arr = ptrs[i];
+ for (ssize_t j = 0; j < arr->len; ++j) {
+ ranked_rr_array_entry_t *entry = arr->at[j];
+ const knot_rrset_t *rrsigs = entry->rr;
+
+ if (qry->uid != entry->qry_uid) {
+ continue;
+ }
+
+ if (rrsigs->type != KNOT_RRTYPE_RRSIG) {
+ continue;
+ }
+
+ int owner_labels = knot_dname_labels(rrsigs->owner, NULL);
+
+ knot_rdata_t *rdata_k = rrsigs->rrs.rdata;
+ for (int k = 0; k < rrsigs->rrs.count;
+ ++k, rdata_k = knot_rdataset_next(rdata_k)) {
+ if (knot_rrsig_labels(rdata_k) != owner_labels) {
+ qry->flags.DNSSEC_WEXPAND = true;
+ }
+ }
+ }
+ }
+}
+
+/** Just for wildcard_adjust_to_wire() */
+static bool rr_is_for_wildcard(const ranked_rr_array_entry_t *entry)
+{
+ switch (kr_rrset_type_maysig(entry->rr)) {
+ case KNOT_RRTYPE_NSEC:
+ case KNOT_RRTYPE_NSEC3:
+ return true;
+ default:
+ return false;
+ }
+}
+/** In case of wildcard expansion, mark required authority RRs by to_wire. */
+static int wildcard_adjust_to_wire(struct kr_request *req, const struct kr_query *qry)
+{
+ if (!qry->parent && qry->flags.DNSSEC_WEXPAND) {
+ return kr_ranked_rrarray_set_wire(&req->auth_selected, true,
+ qry->uid, true, &rr_is_for_wildcard);
+ }
+ return kr_ok();
+}
+
+static int validate(kr_layer_t *ctx, knot_pkt_t *pkt)
+{
+ int ret = 0;
+ struct kr_request *req = ctx->req;
+ struct kr_query *qry = req->current_query;
+
+ if (qry->vld_limit_uid != qry->uid) {
+ qry->vld_limit_uid = qry->uid;
+ qry->vld_limit_crypto_remains = req->ctx->vld_limit_crypto;
+ }
+
+ /* Ignore faulty or unprocessed responses. */
+ if (ctx->state & (KR_STATE_FAIL|KR_STATE_CONSUME)) {
+ return ctx->state;
+ }
+
+ /* Pass-through if user doesn't want secure answer or stub. */
+ if (qry->flags.STUB) {
+ rank_records(qry, false, KR_RANK_OMIT, NULL);
+ return ctx->state;
+ }
+ uint8_t pkt_rcode = knot_wire_get_rcode(pkt->wire);
+ if ((qry->flags.FORWARD) &&
+ pkt_rcode != KNOT_RCODE_NOERROR &&
+ pkt_rcode != KNOT_RCODE_NXDOMAIN) {
+ do {
+ qry->flags.DNSSEC_BOGUS = true;
+ if (qry->cname_parent) {
+ qry->cname_parent->flags.DNSSEC_BOGUS = true;
+ }
+ qry = qry->parent;
+ } while (qry);
+ ctx->state = KR_STATE_DONE;
+ return ctx->state;
+ }
+
+ if (!(qry->flags.DNSSEC_WANT)) {
+ const bool is_insec = qry->flags.CACHED && qry->flags.DNSSEC_INSECURE;
+ if ((qry->flags.DNSSEC_INSECURE)) {
+ rank_records(qry, true, KR_RANK_INSECURE, qry->zone_cut.name);
+ }
+ if (is_insec && qry->parent != NULL) {
+ /* We have got insecure answer from cache.
+ * Mark parent(s) as insecure. */
+ mark_insecure_parents(qry);
+ VERBOSE_MSG(qry, "<= cached insecure response, going insecure\n");
+ ctx->state = KR_STATE_DONE;
+ } else if (ctx->state == KR_STATE_YIELD) {
+ /* Transition to insecure state
+ occurred during revalidation.
+ if state remains YIELD, answer will not be cached.
+ Let cache layers to work. */
+ ctx->state = KR_STATE_DONE;
+ }
+ return ctx->state;
+ }
+
+ /* Pass-through if CD bit is set. */
+ if (knot_wire_get_cd(req->qsource.packet->wire)) {
+ check_wildcard(ctx);
+ wildcard_adjust_to_wire(req, qry);
+ rank_records(qry, false, KR_RANK_OMIT, NULL);
+ return ctx->state;
+ }
+ /* Answer for RRSIG may not set DO=1, but all records MUST still validate. */
+ bool use_signatures = (knot_pkt_qtype(pkt) != KNOT_RRTYPE_RRSIG);
+ if (!(qry->flags.CACHED) && !knot_pkt_has_dnssec(pkt) && !use_signatures) {
+ VERBOSE_MSG(qry, "<= got insecure response\n");
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "MISQ");
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ }
+
+ /* Check if this is a DNSKEY answer, check trust chain and store. */
+ uint16_t qtype = knot_pkt_qtype(pkt);
+ bool has_nsec3 = pkt_has_type(pkt, KNOT_RRTYPE_NSEC3);
+ const knot_pktsection_t *an = knot_pkt_section(pkt, KNOT_ANSWER);
+ const bool referral = (an->count == 0 && !knot_wire_get_aa(pkt->wire));
+
+ if (!(qry->flags.CACHED) && knot_wire_get_aa(pkt->wire)) {
+ /* Check if answer if not empty,
+ * but iterator has not selected any records. */
+ if (!check_empty_answer(ctx, pkt)) {
+ VERBOSE_MSG(qry, "<= no useful RR in authoritative answer\n");
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "MJX6");
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ }
+ /* Track difference between current TA and signer name.
+ * This indicates that the NS is auth for both parent-child,
+ * and we must update DS/DNSKEY to validate it.
+ */
+ ret = check_signer(ctx, pkt);
+ if (ret != KR_STATE_DONE) {
+ return ret;
+ }
+ if (qry->flags.FORWARD && qry->flags.DNSSEC_INSECURE) {
+ return KR_STATE_DONE;
+ }
+ }
+
+ /* Check for too many NSEC3 records. That's an issue, as some parts of validation
+ * are quadratic in their count, doing nontrivial computations inside.
+ * Also there seems to be no use in sending many NSEC3 records. */
+ if (!qry->flags.CACHED) {
+ const knot_pktsection_t *sec = knot_pkt_section(pkt, KNOT_AUTHORITY);
+ int count = 0;
+ for (int i = 0; i < sec->count; ++i)
+ count += (knot_pkt_rr(sec, i)->type == KNOT_RRTYPE_NSEC3);
+ if (count > 8) {
+ VERBOSE_MSG(qry, "<= too many NSEC3 records in AUTHORITY (%d)\n", count);
+ kr_request_set_extended_error(req, 27/*KNOT_EDNS_EDE_NSEC3_ITERS*/,
+ /* It's not about iteration values per se, but close enough. */
+ "DYRH: too many NSEC3 records");
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ }
+ }
+
+ if (knot_wire_get_aa(pkt->wire) && qtype == KNOT_RRTYPE_DNSKEY) {
+ const knot_rrset_t *ds = qry->zone_cut.trust_anchor;
+ if (ds && !kr_ds_algo_support(ds)) {
+ VERBOSE_MSG(qry, ">< all DS entries use unsupported algorithm pairs, going insecure\n");
+ /* ^ the message is a bit imprecise to avoid being too verbose */
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_OTHER, "LSLC: unsupported digest/key");
+ qry->flags.DNSSEC_WANT = false;
+ qry->flags.DNSSEC_INSECURE = true;
+ rank_records(qry, true, KR_RANK_INSECURE, qry->zone_cut.name);
+ mark_insecure_parents(qry);
+ return KR_STATE_DONE;
+ }
+
+ ret = validate_keyset(req, pkt, has_nsec3);
+ if (ret == kr_error(EAGAIN)) {
+ VERBOSE_MSG(qry, ">< cut changed, needs revalidation\n");
+ return KR_STATE_YIELD;
+ } else if (ret != 0) {
+ VERBOSE_MSG(qry, "<= bad keys, broken trust chain\n");
+ /* EDE code already set in validate_keyset() */
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ }
+ }
+
+ /* Validate all records, fail as bogus if it doesn't match.
+ * Do not revalidate data from cache, as it's already trusted.
+ * TTLs of RRsets may get lowered. */
+ if (!(qry->flags.CACHED)) {
+ ret = validate_records(req, pkt, req->rplan.pool, has_nsec3);
+ if (ret == KNOT_EDOWNGRADED) {
+ return KR_STATE_DONE;
+ } else if (ret == kr_error(E2BIG)) {
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+
+ } else if (ret != 0) {
+ /* something exceptional - no DNS key, empty pointers etc
+ * normally it shouldn't happen */
+ VERBOSE_MSG(qry, "<= couldn't validate RRSIGs\n");
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_OTHER,
+ "O4TP: couldn't validate RRSIGs");
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ }
+ /* check validation state and spawn subrequests */
+ if (!req->answ_validated) {
+ ret = check_validation_result(ctx, pkt, &req->answ_selected);
+ if (ret != KR_STATE_DONE) {
+ return ret;
+ }
+ }
+ if (!req->auth_validated) {
+ ret = check_validation_result(ctx, pkt, &req->auth_selected);
+ if (ret != KR_STATE_DONE) {
+ return ret;
+ }
+ }
+ }
+
+ /* Validate non-existence proof if not positive answer.
+ * In case of CNAME, iterator scheduled a sibling query for the target,
+ * so we just drop the negative piece of information and don't try to prove it.
+ * TODO: not ideal; with aggressive cache we'll at least avoid the extra packet. */
+ if (!qry->flags.CACHED && pkt_rcode == KNOT_RCODE_NXDOMAIN && !qry->flags.CNAME) {
+ /* @todo If knot_pkt_qname(pkt) is used instead of qry->sname then the tests crash. */
+ if (!has_nsec3) {
+ ret = kr_nsec_negative(&req->auth_selected, qry->uid,
+ qry->sname, KNOT_RRTYPE_NULL);
+ if (ret >= 0) {
+ if (ret & PKT_NXDOMAIN) {
+ ret = kr_ok();
+ } else {
+ ret = kr_error(ENOENT); // probably proved NODATA
+ }
+ }
+ } else {
+ ret = kr_nsec3_name_error_response_check(pkt, KNOT_AUTHORITY, qry->sname);
+ }
+ if (has_nsec3 && (ret == kr_error(KNOT_ERANGE))) {
+ /* NXDOMAIN proof is OK,
+ * but NSEC3 that covers next closer name
+ * (or wildcard at next closer name) has opt-out flag.
+ * RFC5155 9.2; AD flag can not be set */
+ qry->flags.DNSSEC_OPTOUT = true;
+ VERBOSE_MSG(qry, "<= can't prove NXDOMAIN due to optout, going insecure\n");
+ } else if (ret != 0) {
+ VERBOSE_MSG(qry, "<= bad NXDOMAIN proof\n");
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_NSEC_MISS, "3WKM");
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ }
+ }
+
+ /* @todo WTH, this needs API that just tries to find a proof and the caller
+ * doesn't have to worry about NSEC/NSEC3
+ * @todo rework this
+ * CNAME: same as the NXDOMAIN case above */
+ if (!qry->flags.CACHED && pkt_rcode == KNOT_RCODE_NOERROR && !qry->flags.CNAME) {
+ bool no_data = (an->count == 0 && knot_wire_get_aa(pkt->wire));
+ if (no_data) {
+ /* @todo
+ * ? quick mechanism to determine which check to preform first
+ * ? merge the functionality together to share code/resources
+ */
+ if (!has_nsec3) {
+ ret = kr_nsec_negative(&req->auth_selected, qry->uid,
+ knot_pkt_qname(pkt), knot_pkt_qtype(pkt));
+ if (ret >= 0) {
+ if (ret == PKT_NODATA) {
+ ret = kr_ok();
+ } else {
+ ret = kr_error(ENOENT); // suspicious
+ }
+ }
+ } else {
+ ret = kr_nsec3_no_data(pkt, KNOT_AUTHORITY, knot_pkt_qname(pkt), knot_pkt_qtype(pkt));
+ }
+ if (ret != 0) {
+ if (has_nsec3 && (ret == kr_error(KNOT_ERANGE))) {
+ VERBOSE_MSG(qry, "<= can't prove NODATA due to optout, going insecure\n");
+ qry->flags.DNSSEC_OPTOUT = true;
+ /* Could not return from here,
+ * we must continue, validate NSEC\NSEC3 and
+ * call update_parent_keys() to mark
+ * parent queries as insecure */
+ } else {
+ VERBOSE_MSG(qry, "<= bad NODATA proof\n");
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_NSEC_MISS, "AHXI");
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ }
+ }
+ }
+ }
+
+ wildcard_adjust_to_wire(req, qry);
+
+ /* Check and update current delegation point security status. */
+ ret = update_delegation(req, qry, pkt, has_nsec3);
+ if (ret == DNSSEC_NOT_FOUND && qry->stype != KNOT_RRTYPE_DS) {
+ if (ctx->state == KR_STATE_YIELD) {
+ VERBOSE_MSG(qry, "<= can't validate referral\n");
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "XLE4");
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ } else {
+ /* Check the trust chain and query DS\DNSKEY if needed. */
+ VERBOSE_MSG(qry, "<= DS\\NSEC was not found, querying for DS\n");
+ return KR_STATE_YIELD;
+ }
+ } else if (ret != 0) {
+ return KR_STATE_FAIL;
+ } else if (pkt_rcode == KNOT_RCODE_NOERROR &&
+ referral &&
+ ((!qry->flags.DNSSEC_WANT && qry->flags.DNSSEC_INSECURE) ||
+ (qry->flags.DNSSEC_NODS))) {
+ /* referral with proven DS non-existence */
+ qtype = KNOT_RRTYPE_DS;
+ }
+ /* Update parent query zone cut */
+ if (qry->parent) {
+ if (update_parent_keys(req, qtype) != 0) {
+ return KR_STATE_FAIL;
+ }
+ }
+
+ if (qry->flags.FORWARD && qry->parent) {
+ if (pkt_rcode == KNOT_RCODE_NXDOMAIN) {
+ qry->parent->forward_flags.NO_MINIMIZE = true;
+ }
+ }
+ VERBOSE_MSG(qry, "<= answer valid, OK\n");
+ return KR_STATE_DONE;
+}
+
+/** Hide RRsets which did not validate from clients. */
+static int hide_bogus(kr_layer_t *ctx) {
+ if (knot_wire_get_cd(ctx->req->qsource.packet->wire)) {
+ return ctx->state;
+ }
+ /* We don't want to send bogus answers to clients, not even in SERVFAIL
+ * answers, but we cannot drop whole sections. If a CNAME chain
+ * SERVFAILs somewhere, the steps that were OK should be put into
+ * answer.
+ *
+ * There is one specific issue: currently we follow CNAME *before*
+ * we validate it, because... iterator comes before validator.
+ * Therefore some rrsets might be added into req->*_selected before
+ * we detected failure in validator.
+ * TODO: better approach, probably during work on parallel queries.
+ */
+ const ranked_rr_array_t *sel[] = kr_request_selected(ctx->req);
+ for (knot_section_t sect = KNOT_ANSWER; sect <= KNOT_ADDITIONAL; ++sect) {
+ for (size_t i = 0; i < sel[sect]->len; ++i) {
+ ranked_rr_array_entry_t *e = sel[sect]->at[i];
+ e->to_wire = e->to_wire
+ && !kr_rank_test(e->rank, KR_RANK_INDET)
+ && !kr_rank_test(e->rank, KR_RANK_BOGUS)
+ && !kr_rank_test(e->rank, KR_RANK_MISMATCH)
+ && !kr_rank_test(e->rank, KR_RANK_MISSING);
+ }
+ }
+ return ctx->state;
+}
+
+static int validate_wrapper(kr_layer_t *ctx, knot_pkt_t *pkt) {
+ // Wrapper for now.
+ int ret = validate(ctx, pkt);
+ struct kr_request *req = ctx->req;
+ struct kr_query *qry = req->current_query;
+ if (ret & KR_STATE_FAIL && qry->flags.DNSSEC_BOGUS)
+ qry->server_selection.error(qry, req->upstream.transport, KR_SELECTION_DNSSEC_ERROR);
+ if (ret & KR_STATE_DONE && !qry->flags.DNSSEC_BOGUS) {
+ /* Don't report extended DNS errors related to validation
+ * when it managed to succeed (e.g. by trying different auth). */
+ switch (req->extended_error.info_code) {
+ case KNOT_EDNS_EDE_BOGUS:
+ case KNOT_EDNS_EDE_NSEC_MISS:
+ case KNOT_EDNS_EDE_RRSIG_MISS:
+ case KNOT_EDNS_EDE_SIG_EXPIRED:
+ case KNOT_EDNS_EDE_SIG_NOTYET:
+ kr_request_set_extended_error(req, KNOT_EDNS_EDE_NONE, NULL);
+ break;
+ case KNOT_EDNS_EDE_DNSKEY_MISS:
+ case KNOT_EDNS_EDE_DNSKEY_BIT:
+ kr_assert(false); /* These EDE codes aren't used. */
+ break;
+ default: break; /* Remaining codes don't indicate hard DNSSEC failure. */
+ }
+ }
+ return ret;
+}
+
+
+/** Module implementation. */
+int validate_init(struct kr_module *self)
+{
+ static const kr_layer_api_t layer = {
+ .consume = &validate_wrapper,
+ .answer_finalize = &hide_bogus,
+ };
+ self->layer = &layer;
+ return kr_ok();
+}
+
+KR_MODULE_EXPORT(validate) /* useless for builtin module, but let's be consistent */
+
+#undef VERBOSE_MSG
diff --git a/lib/layer/validate.test.integr/deckard.yaml b/lib/layer/validate.test.integr/deckard.yaml
new file mode 100644
index 0000000..aac7b20
--- /dev/null
+++ b/lib/layer/validate.test.integr/deckard.yaml
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+programs:
+- name: kresd
+ binary: kresd
+ additional:
+ - -n
+ templates:
+ - lib/layer/validate.test.integr/kresd_config.j2
+ configs:
+ - config
diff --git a/lib/layer/validate.test.integr/fwd_insecure_but_rrsig_signer_invalid.rpl b/lib/layer/validate.test.integr/fwd_insecure_but_rrsig_signer_invalid.rpl
new file mode 100644
index 0000000..3cc0968
--- /dev/null
+++ b/lib/layer/validate.test.integr/fwd_insecure_but_rrsig_signer_invalid.rpl
@@ -0,0 +1,294 @@
+; SPDX-License-Identifier: GPL-3.0-or-later
+ val-override-date: "20200722144207"
+ trust-anchor: ". IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"
+CONFIG_END
+
+SCENARIO_BEGIN Forwarding: forwarder sent RRSIGs with invalid signed for an insecure zone, it should not cause SERVFAIL because the zone is insecure
+
+
+; forwarder
+RANGE_BEGIN 0 100
+ ADDRESS 8.8.8.8
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA AD CD NOERROR
+SECTION QUESTION
+. IN DNSKEY
+SECTION ANSWER
+. 86913 IN DNSKEY 256 3 8 AwEAAdauOGxLhfAKFTTZwGhBXbk793QK dWIQRjiSftWdusCwkPhNyJrIjwtNffCW XGLlZAbpcs414RE3oS1qVwV+AdXsO92S Bu5haGlxMUk0NqZO7Xlf84/wrzGZVRRo uPo5pNX/CKS8Mv9UOi0olKGCu31dNfh8 qCszWZcloLDgeLzSnQSkvFoGe69vNCfh 7feESKedkBC2qRz0BZv9+oJI0IY/3D7W EnV0NOlf8gSHozhfJFJ/ZAKtvw/Q3ogr VJFk0LyVaU/NVtVA5FM4pVMIRID7pfrP i78aAzG7b/Wh/Pce4jPAIpS3dApq25Yk vMuPvfB91NMf9FemKwlp78PBVcM=
+. 86913 IN DNSKEY 257 3 8 AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexT BAvkMgJzkKTOiW1vkIbzxeF3+/4RgWOq 7HrxRixHlFlExOLAJr5emLvN7SWXgnLh 4+B5xQlNVz8Og8kvArMtNROxVQuCaSnI DdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLr jyBxWezF0jLHwVN8efS3rCj/EWgvIWgb 9tarpVUDK/b58Da+sqqls3eNbuv7pr+e oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTId sIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6 +cn8HFRm+2hM8AnXGXws9555KrUB5qih ylGa8subX2Nn6UwNR1AkUTV74bU=
+. 86913 IN RRSIG DNSKEY 8 0 172800 20200811000000 20200721000000 20326 . IcMH/yNRoNKkCPmOo8MDcMEZO4sF8p0A 8xgASRnD1c0t+VSU5NRzh05eME7RJrRP 31T/E4eUh+jyI18Gz/O5Lg02Zu1wmcOy Mnkr+bfU+Al7pCztj+6aGTUl34HFyWtM cChKkeJQDeJoBtyVDVa4oL1FQs4Ml6HC OOjzoOKIHakrfCLyaktN82G+uNFXt0CB SGR2xQSWDKnzqSJqCep9X8NtNjjAFus2 g8weAXomG2+gRlrNfQAFqcGPjLHeVtZv yco3u0u8ZOp+8PC8fnlLhtpJ3DBXgwFp wY3V7uM7Zfabcio64st79wu4zNwZR5uf IEpKciMtUh8J8LfVWFM62w==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA AD CD NXDOMAIN
+SECTION QUESTION
+_TA-4f66. IN NULL
+SECTION AUTHORITY
+. 86161 IN NSEC aaa. NS SOA RRSIG NSEC DNSKEY
+. 86161 IN RRSIG NSEC 8 0 86400 20200804050000 20200722040000 46594 . zNoDySatAPoT5YV6XmQcz3qs60+GxuEo yITrzrWMEU1keQNU+6663BM6N/F6G2nd b5Bj5cY5m7MN7iZnBFN/cXQd7EWjdLTC Fw7FxBPc3J51vqShIaM2xxpm9RRdpaB/ vfei+x2e+4YRCJJdw81qmrUBBohANyNW 119JAEufoTgAVY9jPd8699lJ4svMY10E avFAL+PTfC/la9KKNPlOgDgbVRjpUZcU wLeooyqggterm4kXcnWahk3PtG3tmB7A zz1qccY6rOeQALloUQ3Im1Wl9s6pbExZ XI/6qBXYetdexB6DpebnsAk1P4wTprhQ iIxvZpHCppz/jMAx/KDRjQ==
+. 86161 IN RRSIG SOA 8 0 86400 20200804050000 20200722040000 46594 . qI98OcNtjSzHbTbiNg6MZRopwcTAW0Cm JiHKdcEOzGY+Tabxyl76YDeVWEZuKrYG pzeqFLKC05W+4nQrUKTmCoI89YHFNdAc f8uO7zbSEM6dFlS4ksCZFkZZHb2hjp4g KI1MEkI56EsojYqEDa1fXakpytEucvKO 2qJgcqPVkb29lk2lePIicO4YIddI38M4 BNdKsCnEhdMYC76lKls7EYMfJeHAr+FX c3SZIAG/r3ov2KR0u/CNqDDlKcfsGCVt wRCWE/EYUMuZbCNW7/cZ8fCWps1Xn7fu I8N7YLgMTqAsAmXx4I86zf8TAUTslgYN 5IR69n16/l2h6QP9vJPS3g==
+. 86161 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2020072200 1800 900 604800 86400
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA AD CD NOERROR
+SECTION QUESTION
+fi. IN DS
+SECTION ANSWER
+fi. 48962 IN DS 44855 8 2 80cd184a31c50d5dc44b4f98811f298315986b44edcc0666455c6aef7dbd997e
+fi. 48962 IN RRSIG DS 8 1 86400 20200803200000 20200721190000 46594 . HhKwynZK3BYIdOYlwFp6wLacu8uci+ig va3NggxT/LdJqCXlIpuBV5EBGFnEowA9 dYJMdJkkbizYmqkTLbAqaomS5nH3Juz4 5rvjtqi2/viyZjy9VfIPtWx5T5+xfSHH tLac4rJk8ieKSxhpjcc9tITGFT0cnU7U kpWD8OO/0toJF94diLleqS7M/uCDIYyj zgvwIuvx0WxwgxG6bG45EvwfWQbIMRTi R2XSzKtOTRSBbXtI0XKoGrTblOkVxCei uQka3Jqpm/HvmFa9rsspylXTC/6VkKwC u+/TogoJ9vobAr9vXpLx0LiYTkh3GKzZ QL0uw/36jcqplPHlddICPg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA AD CD NOERROR
+SECTION QUESTION
+fi. IN DNSKEY
+SECTION ANSWER
+fi. 793 IN DNSKEY 256 3 8 AwEAAbfXUBTwb2oWNVvv3kCwgaER6vZ6 76rm5DsLmQjOuClDEHXRtH4JmX+nbjmX yFfeNRBasI0jr2sLIYSPB2TF7ctF/FLB a4zgEKIOTiwM2Q58bBxzUhXtU2KAgqEo V5X7kHYau899uJSW+CUJhKAPYpb/sb41 VqSUrsxkACf5ocZ4k4K57H81B2OKZSHC u6C6h4h7D59l8fwYg7F+RAr2vJENfbSM EjHfIWt54x7WfDuCKGDYvpoKmdgg/ACg 6CyLMttbml5dCZ9XJyWyFbs7eJZYGOgN Uw/3ezxmouChVSqqOeVzZAwfsUAsjb1l 1GSSj3YSiRVY4FWewrdw7XL09v0=
+fi. 793 IN DNSKEY 257 3 8 AwEAAb9AMR4NV2YxZH9E6ELMFY5DOszk dTd5AxhSg1YZWi8B9cruHXjghFCmApd1 VfUyQ4MX3DZbskfML/ToxumeSv9OhfA4 I6Fao9bN9UxsBbFlkqwqhAGmuJapSgNu yMWArSoUG4XB/dykPNdyFt+3t1dEH/S3 hS5JmZccSI6YAjnUfG+Pd45R8ljO8ERI 1wSa0IJjDArkuFaLcGrtjR9GJluVmM15 0gWVUIiUkBfDUz8pFjqAWewk0QY9TX22 Z8gfl3yKhO9TYPVHN1oMHTydVqjQKbdb s/BvGXx/GPh63OOxE2ICmxXcz2Ma+082 eA0DfwKLo86PFOre8YK7pEWuw4U=
+fi. 793 IN RRSIG DNSKEY 8 1 900 20200805163544 20200722030600 44855 fi. p9isNtIsiD4yhCQkiVeyfPva4iXV8+oE hhU/SEec2ykGjs22yiscFKppV/4s738y Tl58F682IXAq5jolf/6ShAMOUqxzMD1j 5nphLZ62O5B/r6Ah9JiJf9l748mXHCAW kOnwep4IpkEJkiS1lOHgcxd0hw/rsRNZ QAAgiDhYBGcTIWLw6FFsWL/sMJTEnMim 4QzkDLrF4MlFRduQSffC2N7mBbtQpwYt BFS38tZHQZ7w48or3GGJw7ejwR7IQQQJ VYO7yWr13eLZiGPnne51tvltO7LVeDbl cskxbGWYdtTaB+klcukrVlApqUopqjbT mgzA60/IMKe4PXk+QnzZeA==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+dIGiTrAnSIT.FI. IN DS
+SECTION AUTHORITY
+FI. 1334 IN SOA a.FI. fi-domain-tech.ficora.FI. 2020072239 3605 1800 2419200 86400
+FI. 21134 IN RRSIG SOA 8 1 86400 20200805082122 20200722074635 20436 fi. mXyNMPJRTLMLOKDjZ2ET/z1SOO9g0a8o o1dsHchts2X0uPvYu6Hc8quBCmUbmjhe qu9dRDiDtjCyddFScwn9FTI9CRMJKytB 1LVcrlBrbuWFlmDilRLkhrK2gmm6MOYU /VGgj+kgEtIQE7NSGF35NQHTJfDw1Jmo FxGAo7I//QBAHJemx5Q4YmbVMDiyTBjz FCLSdAjCGouJTRU9jFgweoEivGk+yD+H vsaaG7NTumzK4miU3SL4sMPb9NYr+HIR 2j+6pUjD05x9UOKAfIQzFnRcToDB1KUo ZZr8teWnLH3fbHCq9JGfz338Eju0wdfe 0E2CGL6Cc77+VThotyTrAg==
+es7p5jquq6ng6hd9vn5q3el38s2vuoqh.FI. 21134 IN NSEC3 1 1 5 7298d6895c6c0415 escp8o5mqifge1u5itme7ju27k5hf19a NS DS RRSIG
+es7p5jquq6ng6hd9vn5q3el38s2vuoqh.FI. 21134 IN RRSIG NSEC3 8 2 86400 20200805124944 20200722030600 20436 fi. S/DGd4jI4zndv50oMGp2BmB/aH9/M3AX /My9hwZ4zi2r88DrYiNyd2ghyuUvZO9a lvY2NcB9cX9sjAdQAM/xmoHsoraB5YWV v53YBTJDF+kY7BzO5mqImNWmkpe/Kcxt Mpp9Gz02ySpPp37dot4FbdK9A0RWERVw XkoEaFvLkHRf3QGMJUBnCjKJ7r448axs OAKdHIIQb6aG6OATML6mJx6xHOeI8CFl giTrgixGOR/qGP92i9ErcP9iQ2lvHlfK A7SuOLi6uSPBYWIgZzkqGJOpJ0o+cPHV 69QX/SP7m0qr00shV+rxfyppHCLUpKBx iXacRghILlVV5mUgV/25lw==
+ml0llbvj7rbbtgbp31q2j3d3qv89643r.FI. 21134 IN NSEC3 1 1 5 7298d6895c6c0415 ml0mjk4qs3b070682obd52l1p9v07cl6 NS SOA TXT RRSIG DNSKEY NSEC3PARAM
+ml0llbvj7rbbtgbp31q2j3d3qv89643r.FI. 21134 IN RRSIG NSEC3 8 2 86400 20200804210456 20200722030600 20436 fi. bLLvaTmn2WXk8RxKz7Kb28FoDSgNuIoj ZPrpnwyYVRltfYvOe8wOtzzPQtDYj9F8 bqZgPmZFIAQfsKJk86NMsEWvArGYhX8z 1w6z4qkfpgXpLGeV6fNjLMi/YHZRHQJn cELLNcxh/U9e+xuURCr55XzgzpVVnXpk fm2848LbLO/9p2ZltOGd+GWcyQxxt/aK FlSHUHz2Zp27/9wNCxRyQ8EKtR4eIic8 T9p4kgu5w6302GPSAlfbFCX8Yf+ikMvE Fy7XdbLiE+Uyt0PjEnayT51kqxL2DJFe vtY6Y+MSKazC5xBJudtB6S3owmCDd4PL OhOtxu8lwuuU/FVLteZwvg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+diGITrANsiT.fI. IN NS
+SECTION ANSWER
+diGITrANsiT.fI. 3134 IN NS dns1.ficora.fI.
+diGITrANsiT.fI. 3134 IN NS dns2.ficora.fI.
+diGITrANsiT.fI. 3134 IN NS ns-secondary.funet.fI.
+diGITrANsiT.fI. 3134 IN NS ns1.z.fI.
+diGITrANsiT.fI. 3134 IN NS ns2.z.fI.
+; following RRSIG signer is out-of-bailiwick, i.e. nonsense
+diGITrANsiT.fI. 3134 IN RRSIG NS 10 2 3600 20190517083644 20190117083646 28100 droneinfo.fi. XFNrVGseG3exPEFC58o3tgRckNghA9+G Psc8w8lUgJW7NGPuWHM4aSuhgMWL3nxk kCqTYI60RerzNV1PNxFLlfyBzuwi6rqe dOjpud7Nr9giKQc2r2YsOSLrSfcRN4Wq KGllqTVXZAYIbF5+QYA+2x3sY1StATQS mb2qqYPPB6iR/EPuHOn/1DA9gzJKXdQS wWwRWSuzBtO89q/e/zhSlCsWhk96POR7 du1KpJb58wPdNm6+Jznwj7E7KphZaTID mexL5S9Sf6VwDNDHkSOGT9x182tjIXTs kZcpFZgJTDgrR+vwVwjXmvAs/CKQmMax D0ZuRbMjvgvaMkCsBRjtDQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+Api.dIGiTRaNsit.Fi. IN A
+SECTION ANSWER
+Api.dIGiTRaNsit.Fi. 3357 IN CNAME digitransit-prod.trafficmanager.net.
+digitransit-aks.westeurope.cloudapp.azure.com. 9 IN A 40.119.148.209
+digitransit-prod.trafficmanager.net. 299 IN CNAME digitransit-aks.westeurope.cloudapp.azure.com.
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA AD CD NOERROR
+SECTION QUESTION
+nET. IN DS
+SECTION ANSWER
+nET. 56393 IN DS 35886 8 2 7862b27f5f516ebe19680444d4ce5e762981931842c465f00236401d8bd973ee
+nET. 56393 IN RRSIG DS 8 1 86400 20200803200000 20200721190000 46594 . PtZ4PuUSHtwvVksquoCtgL5ylNkYaBJk uXVY0Xx4FOyJ8U5kJwlQzScXS8/W7/4m NMLRJWvDIfEMTwpRtFpgd71THg5w3M+O He4GoQ5dGaaSuREvpYCHY+O6aeO/t3DX P3mTcps+CJlIOJckiRirvv3V1u7jmTGB t4jZ6Gn27CX9lPXGUkhWrDx9EOW1p7ky ZYtFGtkVxGmqnMqoNMSz+JaFmN43uaJU grJt6B8aKFIw3MR1Z4xCX3oYzd5jDdQt sS1h8frflyyN/dF/aIl5BW58sc8qkgGS kv8iUgHW1/E24chcMQFngnOuAAF6hjaA K4SJCdrCXUNiduL8H9Q5MQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA AD CD NOERROR
+SECTION QUESTION
+nEt. IN DNSKEY
+SECTION ANSWER
+nEt. 21146 IN DNSKEY 256 3 8 AQPeYYme8NvhAl+0XjyGqHVep4Y1T2Or RmO+L3QGULBlOe571PnxI+gRyXCQmtN7 WpoJxzALFSVBPsggqwOP+wnmCx8DZ49N HrfS7WbMtoYHTtiaIvHTjZZ88leuCtNL qfIH8N1Ax68Xnf4uKobYFgZXj0M2Zi7Y I84iFkCpIyZk6VIiJpvpNgyCK5mWetPF 2zmO2jXC8M045JIPam38reXD
+nEt. 21146 IN DNSKEY 257 3 8 AQOYBnzqWXIEj6mlgXg4LWC0HP2n8eK8 XqgHlmJ/69iuIHsa1TrHDG6TcOra/pye GKwH0nKZhTmXSuUFGh9BCNiwVDuyyb6O BGy2Nte9Kr8NwWg4q+zhSoOf4D+gC9dE zg0yFdwT0DKEvmNPt0K4jbQDS4Yimb+u PKuF6yieWWrPYYCrv8C9KC8JMze2uT6N uWBfsl2fDUoV4l65qMww06D7n+p7Rbdw WkAZ0fA63mXVXBZF6kpDtsYD7SUB9jhh fLQE/r85bvg3FaSs5Wi2BaqN06SzGWI1 DHu7axthIOeHwg00zxlhTpoYCH0ldoQz +S65zWYi/fRJiyLSBb6JZOvn
+nEt. 21146 IN RRSIG DNSKEY 8 1 86400 20200728162830 20200713162330 35886 net. BsxHqTUrVNqYdQdv5uriiUd/p+Dh5F12 /01oniA3F1keMZU1V+pbELcih+1gfLs3 i+f88p/9r3kM8gghxQtInzyJl3lPdeBM 7LjWuonQR5CzvfnM4WAgkVZZxmFB7l6b bj+ey9mwocAMR1ht9502MgB4eQLOHVve 3mdCXYuilxwQ9vOrVsFDLiELVoCDVtQi csatJy3Z/LU31IWtR8c6Ta/ItApgqsWg fU8Br3uyHesiDbA2FSfBb9qWFfGcNrDJ ZGF9dLCxvMeHSU5AKpEwX8flOKWKRVwM nB/+LB1owMmcyao82yJlL7y7vxfrQDb0 V0uJaQMEiQl7XGBsT+8Hgg==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+TRAFficManaGer.NEt. IN DS
+SECTION AUTHORITY
+A1RT98BS5QGC9NFI51S9HCI47ULJG6JH.NEt. 21540 IN NSEC3 1 1 0 - a1ruuffjkct2q54p78f8ejgj8jbk7i8b NS SOA RRSIG DNSKEY NSEC3PARAM
+A1RT98BS5QGC9NFI51S9HCI47ULJG6JH.NEt. 21540 IN RRSIG NSEC3 8 2 86400 20200728064417 20200721053417 56519 net. B4N1ZLUcMCAkfgGNrmJfXylkUAbPnqxO 31ZQ5ZwU0AecyChOkGGTelH/87eM7RtG M7mqrM6zU9CnGwsgheqg2HCbpX7n5Fvl 1AclnJZBuFlF0hvejSO99rrLrLRaTWQt LyJwVQJWkMfgztCO5Mh2ngJfK6ZB1PLS xOFqVz0j5MTYYp9QwdL1PvLIaBAw0kTg cm3o476wB0glMo6yzDbK2A==
+CS431SS8CTI7M6JHMN592GRLI9T17OK7.NEt. 21540 IN NSEC3 1 1 0 - cs49egn3atpo6m7fblhased3j00k92nb NS DS RRSIG
+CS431SS8CTI7M6JHMN592GRLI9T17OK7.NEt. 21540 IN RRSIG NSEC3 8 2 86400 20200727063544 20200720052544 56519 net. FB73aPYNlR3FqN1gmxYeIccL+ybiv+8A ymQkJDevhRITdI76YqbobqbvAKQg9Knm lSe5OtWZEmI6h+qbYXtXfnAGl+GayzbL LsSyUABJdWp8ZuGooatayzGqjWUpIGZr qgAZIK+twkCKUicgi2XhCztnVr1wVf2z L0tRuctiZQRdoDlcRfFzXBsg21Kn3eVy ivjnZUzp93vL1ZrBhhZfMg==
+NEt. 840 IN RRSIG SOA 8 1 900 20200729095953 20200722084953 56519 net. GEY1baT1zShB6uxFAyHlg6EPfEbCxp3E 3C2l8OeVCBwgzBdP9TwDeXH+//RcfT9w Q++Wan2q/0W5I9fJRWZKGDPkdmhOcvh9 VjJRlb3DwZdJEeDc3Rj3dip3MvL7OyZt 8lfv42/WoBYHXeduQwGknz0KbNpeSqU0 BjV4C9O/w5rUSjvk3z6Qka7jz3/0sz7H 4FfXqv5CxkUCKYyl9PzxuA==
+NEt. 840 IN SOA a.gtld-servers.NEt. nstld.verisign-grs.com. 1595411993 1800 900 604800 86400
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+TrAfFicMANAger.net. IN NS
+SECTION ANSWER
+TrAfFicMANAger.net. 21041 IN NS tm1.dns-tm.com.
+TrAfFicMANAger.net. 21041 IN NS tm1.edgedns-tm.info.
+TrAfFicMANAger.net. 21041 IN NS tm2.dns-tm.com.
+TrAfFicMANAger.net. 21041 IN NS tm2.edgedns-tm.info.
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+Api.dIGiTRaNsit.Fi. IN A
+SECTION ANSWER
+Api.dIGiTRaNsit.Fi. 3585 IN CNAME digitransit-prod.trafficmanager.net.
+digitransit-aks.westeurope.cloudapp.azure.com. 9 IN A 40.119.148.209
+digitransit-prod.trafficmanager.net. 285 IN CNAME digitransit-aks.westeurope.cloudapp.azure.com.
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+DigiTranSiT-pROd.tRaffiCmanAgeR.Net. IN A
+SECTION ANSWER
+DigiTranSiT-pROd.tRaffiCmanAgeR.Net. 299 IN CNAME digitransit-aks.westeurope.cloudapp.azure.com.
+digitransit-aks.westeurope.cloudapp.azure.com. 9 IN A 40.119.148.209
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+DigiTranSiT-pROd.tRaffiCmanAgeR.Net. IN A
+SECTION ANSWER
+DigiTranSiT-pROd.tRaffiCmanAgeR.Net. 299 IN CNAME digitransit-aks.westeurope.cloudapp.azure.com.
+digitransit-aks.westeurope.cloudapp.azure.com. 9 IN A 40.119.148.209
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA AD CD NOERROR
+SECTION QUESTION
+coM. IN DS
+SECTION ANSWER
+coM. 86251 IN DS 30909 8 2 e2d3c916f6deeac73294e8268fb5885044a833fc5459588f4a9184cfc41a5766
+coM. 86251 IN RRSIG DS 8 1 86400 20200803200000 20200721190000 46594 . F3sngBM8xYQ10Z3iIVWUqlMFLvecizXH 2d5kM4iguQL088Cv6xz1Ep3d4wUVEzlL YBBrsCQB627WztctcVPFiXUn22cWwzky 7yxjII8YY72V2x2/758hmZMCSHdSzJph By9Wv5Av5O8qqLt1QyYq8r6cZK7352Vk ICENa/OhKWX3dUvQ+EQe+3JUN5q/Xfeb WnCiuG2LgaWIgl/f+yjpMEkg808EUCCz 41Dbe81gwIadN+vvpjvpb3j5cBfozwqk thyCWlXt2+ZUUH4aSr5q5WsS8nD4gb70 6YC+A08woNmeYms8t40irruVMdlUzrXC Fz7+azz0zCEAc046A9v5fQ==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA AD CD NOERROR
+SECTION QUESTION
+coM. IN DNSKEY
+SECTION ANSWER
+coM. 20944 IN DNSKEY 256 3 8 AwEAAdVECVAFFKnwlH7lDYpsSvv50Z7E JP518luWdiN7X5igYJo6dLij/noOhYO0 ppmTghphtSqHn75/qMmETK9NiUfLW4M9 X8j/IvIr1xrTPEb6+dipDE9xKjhMGFUu fOeXHiBoMQiKLNzlssYuz90oQrEwCKpa 5R4cYYFiZaoeezi2NQeIAY82dh/8auvF zqCOewWx/J2zVh8YHqfkGeXyzsM=
+coM. 20944 IN DNSKEY 257 3 8 AQPDzldNmMvZFX4NcNJ0uEnKDg7tmv/F 3MyQR0lpBmVcNcsIszxNFxsBfKNW9JYC Yqpik8366LE7VbIcNRzfp2h9OO8HRl+H +E08zauK8k7evWEmu/6od+2boggPoiEf GNyvNPaSI7FOIroDsnw/taggzHRX1Z7S OiOiPWPNIwSUyWOZ79VmcQ1GLkC6NlYv G3HwYmynQv6oFwGv/KELSw7ZSdrbTQ0H XvZbqMUI7BaMskmvgm1G7oKZ1YiF7O9i oVNc0+7ASbqmZN7Z98EGU/Qh2K/BgUe8 Hs0XVcdPKrtyYnoQHd2ynKPcMMlTEih2 /2HDHjRPJ2aywIpKNnv4oPo/
+coM. 20944 IN RRSIG DNSKEY 8 1 86400 20200729182421 20200714181921 30909 com. kWt6r1qzHb1LABToPGz61aVgRBHMIkS+ x1FbuHxh+Ha9nYtlnl1AOju4CjMC6gje qBXYhhwpZWC4VFSE+hVXuI2NNEPvtcD1 Om9eu69KEK8d1rXmho+PBzJyzXSSUpM6 KtapTL0NDdjg+uCt6YmWNli/e3QdRAoI u5eNnmFBK5viaGcnIP5l8/QdXH+dBmfi qrrs+z0oJv89euCCjH0UeMfVJUetHTox MLiB4GlMyQnPWsNXNZPzQEWk8CeLEhVu e8QVvMQmq+GfcvRF/jFnTsL9ILTMYiJR 8gYp5YnFtBgtrqPoekmAaJ0dig2bXFVV /x8RzR/wFd8yUHTaT+AWkA==
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+AZURe.com. IN DS
+SECTION AUTHORITY
+2VPE4QITM4PLV486G56AB7OUDVIO1U3D.com. 21574 IN NSEC3 1 1 0 - 2vpf134m9p90k8k9jmtdjemi4anqnsaf NS DS RRSIG
+2VPE4QITM4PLV486G56AB7OUDVIO1U3D.com. 21574 IN RRSIG NSEC3 8 2 86400 20200726043358 20200719032358 24966 com. SpLKFkZ534rLzf6WTHf+JdBCanfiUhX1 BbmjpFQRI+l1qRN9po9mtA95jfw8AGjw S0n36LK9MBNF4pRo4zcmxny3/K7lfDwf 6HhDlNMp3KQGtLMGcSCqw7StxA3b3qsg 0NhxPtHl818RsBKueIR88LPX88x5jkzC Gr11WJoVlq682pCJlmkVOze2JCmRvjYY m/mUHv4y5+mh3h2AbHc+zA==
+CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 21574 IN NSEC3 1 1 0 - ck0q1gin43n1arrc9osm6qpqr81h5m9a NS SOA RRSIG DNSKEY NSEC3PARAM
+CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 21574 IN RRSIG NSEC3 8 2 86400 20200728044213 20200721033213 24966 com. qoAss2VVxsBWG9+MAQ3giAmCnzf4dz7G ppZsQfkt3b7cFIVgO3TiWPWkusb3BEbp sma2Q+x/TBLeeHYm6l1HWXMIl7zcYLsA XY0ZzoWaqNPTPH6YNDAreZYP21UekBL7 g710cndRk4oaJUNz5t8sGi3JaOJF046Q cUz6gGg7NLMvyGlJWzmftGbxgp9ovdOg wmirddESGOj33kuCfSJvWA==
+com. 874 IN RRSIG SOA 8 1 900 20200729100807 20200722085807 24966 com. kDvHo8x7ut5Lu66MSUOUTPvxfYJLXMcu aKPCu9I+jTYZOzAH4KquPm+765a/gnp5 2okPhEUJTO2JYumhfkjyG04kE4HCnqfR bWtjjfIyqXo34km+CR8rG8RGZ6QilLWZ 0yxux5+izvuji4L4KLeTxPwUJQFgAmVA 59unj2IqysGWRc2ETSofHOPFrydduuyc DJdQLN6Dq1fMFh849Qr6nw==
+com. 874 IN SOA a.gtld-servers.net. nstld.verisign-grs.com. 1595412487 1800 900 604800 86400
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+AZURe.COM. IN NS
+SECTION ANSWER
+AZURe.COM. 21462 IN NS ns1-205.azure-dns.COM.
+AZURe.COM. 21462 IN NS ns2-205.azure-dns.net.
+AZURe.COM. 21462 IN NS ns3-205.azure-dns.org.
+AZURe.COM. 21462 IN NS ns4-205.azure-dns.info.
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+DigitRaNsit-aKS.WESTEuROPe.CloUDApp.aZuRe.cOm. IN A
+SECTION ANSWER
+DigitRaNsit-aKS.WESTEuROPe.CloUDApp.aZuRe.cOm. 9 IN A 40.119.148.209
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+api.digitransit.fi. IN A
+SECTION ANSWER
+api.digitransit.fi. 3357 IN CNAME digitransit-prod.trafficmanager.net.
+digitransit-aks.westeurope.cloudapp.azure.com. 9 IN A 40.119.148.209
+digitransit-prod.trafficmanager.net. 299 IN CNAME digitransit-aks.westeurope.cloudapp.azure.com.
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH qname qtype
+ADJUST copy_id
+REPLY QR RD RA CD NOERROR
+SECTION QUESTION
+DigitRaNsit-aKS.WESTEuROPe.CloUDApp.aZuRe.cOm. IN A
+SECTION ANSWER
+DigitRaNsit-aKS.WESTEuROPe.CloUDApp.aZuRe.cOm. 9 IN A 40.119.148.209
+ENTRY_END
+
+RANGE_END
+
+
+STEP 10 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+api.digitransit.fi. IN A
+ENTRY_END
+
+STEP 11 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH all
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+api.digitransit.fi. IN A
+SECTION ANSWER
+api.digitransit.fi. IN CNAME digitransit-prod.trafficmanager.net.
+digitransit-prod.trafficmanager.net. IN CNAME digitransit-aks.westeurope.cloudapp.azure.com.
+digitransit-aks.westeurope.cloudapp.azure.com. IN A 40.119.148.209
+ENTRY_END
+
+SCENARIO_END
diff --git a/lib/layer/validate.test.integr/kresd_config.j2 b/lib/layer/validate.test.integr/kresd_config.j2
new file mode 100644
index 0000000..cc0dbd5
--- /dev/null
+++ b/lib/layer/validate.test.integr/kresd_config.j2
@@ -0,0 +1,52 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+trust_anchors.remove('.')
+{% for TAF in TRUST_ANCHOR_FILES %}
+trust_anchors.add_file('{{TAF}}')
+{% endfor %}
+
+{% raw %}
+-- Disable RFC5011 TA update
+if ta_update then
+ modules.unload('ta_update')
+end
+
+-- Disable RFC8145 signaling, scenario doesn't provide expected answers
+if ta_signal_query then
+ modules.unload('ta_signal_query')
+end
+
+-- Disable RFC8109 priming, scenario doesn't provide expected answers
+if priming then
+ modules.unload('priming')
+end
+
+-- Disable this module because it make one priming query
+if detect_time_skew then
+ modules.unload('detect_time_skew')
+end
+
+cache.size = 2*MB
+log_level('debug')
+policy.add(policy.all(policy.DEBUG_ALWAYS))
+policy.add(policy.all(policy.FORWARD('8.8.8.8')))
+{% endraw %}
+
+net = { '{{SELF_ADDR}}' }
+
+-- Self-checks on globals
+assert(help() ~= nil)
+assert(worker.id ~= nil)
+-- Self-checks on facilities
+assert(cache.count() == 0)
+assert(cache.stats() ~= nil)
+assert(cache.backends() ~= nil)
+assert(worker.stats() ~= nil)
+assert(net.interfaces() ~= nil)
+-- Self-checks on loaded stuff
+assert(net.list()[1].transport.ip == '{{SELF_ADDR}}')
+assert(#modules.list() > 0)
+-- Self-check timers
+ev = event.recurrent(1 * sec, function (ev) return 1 end)
+event.cancel(ev)
+ev = event.after(0, function (ev) return 1 end)