summaryrefslogtreecommitdiffstats
path: root/lib/layer
diff options
context:
space:
mode:
Diffstat (limited to 'lib/layer')
-rw-r--r--lib/layer/cache.c31
-rw-r--r--lib/layer/iterate.c1135
-rw-r--r--lib/layer/iterate.h36
-rw-r--r--lib/layer/validate.c1133
4 files changed, 2335 insertions, 0 deletions
diff --git a/lib/layer/cache.c b/lib/layer/cache.c
new file mode 100644
index 0000000..c7bbc1a
--- /dev/null
+++ b/lib/layer/cache.c
@@ -0,0 +1,31 @@
+/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "lib/module.h"
+#include "lib/cache/api.h"
+
+/** Module implementation. */
+const kr_layer_api_t *cache_layer(struct kr_module *module)
+{
+ static const kr_layer_api_t _layer = {
+ .produce = &cache_peek,
+ .consume = &cache_stash,
+ };
+
+ return &_layer;
+}
+
+KR_MODULE_EXPORT(cache)
diff --git a/lib/layer/iterate.c b/lib/layer/iterate.c
new file mode 100644
index 0000000..cf57cc5
--- /dev/null
+++ b/lib/layer/iterate.c
@@ -0,0 +1,1135 @@
+/* Copyright (C) 2014-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/** @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 <assert.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/nsrep.h"
+#include "lib/module.h"
+#include "lib/dnssec/ta.h"
+
+#define VERBOSE_MSG(...) QRVERBOSE(req->current_query, "iter", __VA_ARGS__)
+#define QVERBOSE_MSG(qry, ...) QRVERBOSE(qry, "iter", __VA_ARGS__)
+
+/* 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);
+
+ return query->id == knot_wire_get_id(answer->wire) &&
+ knot_wire_get_qdcount(answer->wire) > 0 &&
+ query->sclass == knot_pkt_qclass(answer) &&
+ qtype == knot_pkt_qtype(answer) &&
+ knot_dname_is_equal(qname, knot_pkt_qname(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/parent of 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 = ntohl(*(const uint32_t *)(addr));
+ 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;
+}
+
+/** @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)) {
+ continue;
+ }
+ if ((rr->type == KNOT_RRTYPE_AAAA) &&
+ (req->ctx->options.NO_IPV6)) {
+ continue;
+ }
+ (void) update_nsaddr(rr, req->current_query, glue_cnt);
+ }
+ }
+}
+
+/** 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");
+#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);
+ assert(!ret); (void)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 != kr_ok()) {
+ return ret;
+ }
+ }
+
+ return kr_ok();
+}
+
+static int process_authority(knot_pkt_t *pkt, struct kr_request *req)
+{
+ struct kr_query *qry = req->current_query;
+ assert(!(qry->flags.STUB));
+
+ 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;
+ /* 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;
+ }
+ } 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 (e.g. what a.gtld-servers.net would respond for `com NS`) */
+ 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) {
+ /* 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 ((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 void finalize_answer(knot_pkt_t *pkt, struct kr_query *qry, struct kr_request *req)
+{
+ /* Finalize header */
+ knot_pkt_t *answer = req->answer;
+ knot_wire_set_rcode(answer->wire, knot_wire_get_rcode(pkt->wire));
+}
+
+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;
+ assert(!(query->flags.STUB));
+ /* 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;
+ unsigned cname_chain_len = 0;
+ bool is_final = (query->parent == NULL);
+ uint32_t iter_count = 0;
+ bool strict_mode = (query->flags.STRICT);
+ 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;
+ 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 || type == KNOT_RRTYPE_DNAME;
+ /* TODO: actually handle DNAMEs */
+ if (rr->rclass != KNOT_CLASS_IN || !type_OK
+ || !knot_dname_is_equal(rr->owner, cname)
+ || knot_dname_in_bailiwick(rr->owner, query->zone_cut.name) < 0) {
+ 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 */
+ int state = KR_STATE_FAIL;
+ bool to_wire = false;
+ if (is_final) {
+ /* if not referral, mark record to be written to final answer */
+ to_wire = !referral;
+ } else {
+ int cnt_ = 0;
+ 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);
+ state = kr_ranked_rrarray_add(&req->answ_selected, rr,
+ rank, to_wire, query->uid, &req->pool);
+ if (state != kr_ok()) {
+ return KR_STATE_FAIL;
+ }
+ /* Jump to next CNAME target */
+ if ((query->stype == KNOT_RRTYPE_CNAME) || (rr->type != KNOT_RRTYPE_CNAME)) {
+ continue;
+ }
+ cname_chain_len += 1;
+ pending_cname = knot_cname_name(rr->rrs.rdata);
+ if (!pending_cname) {
+ break;
+ }
+ if (cname_chain_len > an->count || cname_chain_len > KR_CNAME_CHAIN_LIMIT) {
+ VERBOSE_MSG("<= too long cname chain\n");
+ return KR_STATE_FAIL;
+ }
+ /* Don't use pending_cname immediately.
+ * There are can be records for "old" cname. */
+ }
+ if (!pending_cname) {
+ break;
+ }
+ if (knot_dname_is_equal(cname, pending_cname)) {
+ VERBOSE_MSG("<= cname chain loop\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.
+ * TODO: this still isn't 100%, as the target might have a NS+DS,
+ * possibly leading to a SERVFAIL for the in-bailiwick name. */
+ 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)) {
+ cname = pending_cname;
+ break;
+ }
+ } while (++iter_count < KR_CNAME_CHAIN_LIMIT);
+ if (iter_count >= KR_CNAME_CHAIN_LIMIT) {
+ VERBOSE_MSG("<= too long cname chain\n");
+ return KR_STATE_FAIL;
+ }
+ *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);
+ if (state != kr_ok()) {
+ return KR_STATE_FAIL;
+ }
+ struct kr_query *query = req->current_query;
+ 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;
+ }
+ finalize_answer(pkt, query, req);
+ return KR_STATE_DONE;
+ }
+ 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)) {
+ 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()) {
+ 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 nonexistance. */
+ 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");
+ 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;
+ next->flags.NO_MINIMIZE = query->flags.NO_MINIMIZE;
+ next->flags.NO_THROTTLE = query->flags.NO_THROTTLE;
+
+ if (query->flags.FORWARD) {
+ next->forward_flags.CNAME = true;
+ if (query->parent == NULL) {
+ state = kr_nsrep_copy_set(&next->ns, &query->ns);
+ if (state != kr_ok()) {
+ return KR_STATE_FAIL;
+ }
+ }
+ }
+ next->cname_parent = query;
+ /* Want DNSSEC if and only if it's posible to secure
+ * this name (i.e. iff it is covered by a TA) */
+ if (kr_ta_covers_qry(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;
+ }
+ finalize_answer(pkt, query, 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;
+ assert(query->flags.STUB);
+ /* 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 != kr_ok()) {
+ 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;
+ }
+
+ finalize_answer(pkt, query, req);
+ return KR_STATE_DONE;
+}
+
+
+/** Error handling, RFC1034 5.3.3, 4d. */
+static int resolve_error(knot_pkt_t *pkt, struct kr_request *req)
+{
+ return KR_STATE_FAIL;
+}
+
+/* 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_wire_set_rcode(ctx->req->answer->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)
+{
+ assert(pkt && ctx);
+ 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;
+
+ return KR_STATE_CONSUME;
+}
+
+static int resolve_badmsg(knot_pkt_t *pkt, struct kr_request *req, struct kr_query *query)
+{
+
+#ifndef STRICT_MODE
+ /* Work around broken auths/load balancers */
+ if (query->flags.SAFEMODE) {
+ return resolve_error(pkt, req);
+ } else if (query->flags.NO_MINIMIZE) {
+ query->flags.SAFEMODE = true;
+ return KR_STATE_DONE;
+ } else {
+ query->flags.NO_MINIMIZE = true;
+ return KR_STATE_DONE;
+ }
+#else
+ return resolve_error(pkt, req);
+#endif
+}
+
+static int resolve_notimpl(knot_pkt_t *pkt, struct kr_request *req, struct kr_query *qry)
+{
+ if (qry->stype == KNOT_RRTYPE_RRSIG && qry->parent != NULL) {
+ /* RRSIG subquery have got NOTIMPL.
+ * Possible scenario - same NS is autoritative for child and parent,
+ * but child isn't signed.
+ * We got delegation to parent,
+ * then NS responded as NS for child zone.
+ * Answer contained record been requested, but no RRSIGs,
+ * Validator issued RRSIG query then. If qname is zone name,
+ * we can get NOTIMPL. Ask for DS to find out security status.
+ * TODO - maybe it would be better to do this in validator, when
+ * RRSIG revalidation occurs.
+ */
+ struct kr_rplan *rplan = &req->rplan;
+ struct kr_query *next = kr_rplan_push(rplan, qry->parent, qry->sname,
+ qry->sclass, KNOT_RRTYPE_DS);
+ if (!next) {
+ return KR_STATE_FAIL;
+ }
+ kr_zonecut_set(&next->zone_cut, qry->parent->zone_cut.name);
+ kr_zonecut_copy(&next->zone_cut, &qry->parent->zone_cut);
+ kr_zonecut_copy_trust(&next->zone_cut, &qry->parent->zone_cut);
+ next->flags.DNSSEC_WANT = true;
+ qry->flags.RESOLVED = true;
+ return KR_STATE_DONE;
+ }
+ return resolve_badmsg(pkt, req, qry);
+}
+
+/** 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)
+{
+ assert(pkt && ctx);
+ struct kr_request *req = ctx->req;
+ struct kr_query *query = req->current_query;
+ if (!query) {
+ return ctx->state;
+ }
+
+ 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. */
+#ifdef STRICT_MODE
+ if (pkt->parsed < pkt->size) {
+ VERBOSE_MSG("<= pkt contains excessive data\n");
+ return resolve_badmsg(pkt, req, query);
+ } else
+#endif
+ if (pkt->parsed <= KNOT_WIRE_HEADER_SIZE) {
+ VERBOSE_MSG("<= malformed response\n");
+ return resolve_badmsg(pkt, req, query);
+ } else if (!is_paired_to_query(pkt, query)) {
+ VERBOSE_MSG("<= ignoring mismatching response\n");
+ /* Force TCP, to work around authoritatives messing up question
+ * without yielding to spoofed responses. */
+ query->flags.TCP = true;
+ return resolve_badmsg(pkt, req, query);
+ } else if (knot_wire_get_tc(pkt->wire)) {
+ VERBOSE_MSG("<= truncated response, failover to TCP\n");
+ if (query) {
+ /* Fail if already on TCP. */
+ if (query->flags.TCP) {
+ VERBOSE_MSG("<= TC=1 with TCP, bailing out\n");
+ return resolve_error(pkt, req);
+ }
+ query->flags.TCP = true;
+ }
+ return KR_STATE_CONSUME;
+ }
+
+#ifndef NOVERBOSELOG
+ const knot_lookup_t *rcode = knot_lookup_by_id(knot_rcode_names, knot_wire_get_rcode(pkt->wire));
+#endif
+
+ /* Check response code. */
+ switch(knot_wire_get_rcode(pkt->wire)) {
+ case KNOT_RCODE_NOERROR:
+ case KNOT_RCODE_NXDOMAIN:
+ break; /* OK */
+ case KNOT_RCODE_REFUSED:
+ case KNOT_RCODE_SERVFAIL: {
+ if (query->flags.STUB) {
+ /* Pass through in stub mode */
+ break;
+ }
+ VERBOSE_MSG("<= rcode: %s\n", rcode ? rcode->name : "??");
+ query->fails += 1;
+ if (query->fails >= KR_QUERY_NSRETRY_LIMIT) {
+ query->fails = 0; /* Reset per-query counter. */
+ return resolve_error(pkt, req);
+ } else {
+ if (!query->flags.FORWARD) {
+ query->flags.NO_MINIMIZE = true; /* Drop minimisation as a safe-guard. */
+ }
+ return KR_STATE_CONSUME;
+ }
+ }
+ case KNOT_RCODE_FORMERR:
+ VERBOSE_MSG("<= rcode: %s\n", rcode ? rcode->name : "??");
+ return resolve_badmsg(pkt, req, query);
+ case KNOT_RCODE_NOTIMPL:
+ VERBOSE_MSG("<= rcode: %s\n", rcode ? rcode->name : "??");
+ return resolve_notimpl(pkt, req, query);
+ default:
+ VERBOSE_MSG("<= rcode: %s\n", rcode ? rcode->name : "??");
+ return resolve_error(pkt, req);
+ }
+
+ /* Forwarding/stub mode is special. */
+ if (query->flags.STUB) {
+ return process_stub(pkt, req);
+ }
+
+ /* Resolve authority to see if it's referral or authoritative. */
+ int 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);
+ VERBOSE_MSG("<= referral response, follow\n");
+ break;
+ default:
+ break;
+ }
+
+ return state;
+}
+
+/** Module implementation. */
+const kr_layer_api_t *iterate_layer(struct kr_module *module)
+{
+ static const kr_layer_api_t _layer = {
+ .begin = &begin,
+ .reset = &reset,
+ .consume = &resolve,
+ .produce = &prepare_query
+ };
+ return &_layer;
+}
+
+KR_MODULE_EXPORT(iterate)
+
+#undef VERBOSE_MSG
diff --git a/lib/layer/iterate.h b/lib/layer/iterate.h
new file mode 100644
index 0000000..a9395bf
--- /dev/null
+++ b/lib/layer/iterate.h
@@ -0,0 +1,36 @@
+/* Copyright (C) 2014-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#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. */
+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/validate.c b/lib/layer/validate.c
new file mode 100644
index 0000000..55c3ad7
--- /dev/null
+++ b/lib/layer/validate.c
@@ -0,0 +1,1133 @@
+/* Copyright (C) 2014-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#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.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"
+
+#define VERBOSE_MSG(qry, ...) QRVERBOSE(qry, "vldr", __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 struct kr_query *qry,
+ const knot_rrset_t *rr, const char *msg) {
+ WITH_VERBOSE(qry) {
+ auto_free char *name_text = kr_dname_text(rr->owner);
+ auto_free char *type_text = kr_rrtype_text(rr->type);
+ VERBOSE_MSG(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);
+ }
+}
+
+static int validate_section(kr_rrset_validation_ctx_t *vctx, const struct kr_query *qry,
+ knot_mm_t *pool)
+{
+ 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;
+
+ int validation_result = 0;
+ for (ssize_t i = 0; i < vctx->rrs->len; ++i) {
+ ranked_rr_array_entry_t *entry = vctx->rrs->at[i];
+ const knot_rrset_t *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 (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;
+ }
+ kr_rank_set(&entry->rank, KR_RANK_OMIT);
+ continue;
+ }
+
+ uint8_t rank_orig = entry->rank;
+ validation_result = kr_rrset_validate(vctx, rr);
+ if (validation_result == kr_ok()) {
+ kr_rank_set(&entry->rank, KR_RANK_SECURE);
+
+ } else if (kr_rank_test(rank_orig, KR_RANK_TRY)) {
+ log_bogus_rrsig(vctx, qry, rr,
+ "failed to validate non-authoritative data but 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)) {
+ /* no RRSIGs found */
+ kr_rank_set(&entry->rank, KR_RANK_MISSING);
+ vctx->err_cnt += 1;
+ log_bogus_rrsig(vctx, qry, rr, "no valid RRSIGs found");
+ } else {
+ kr_rank_set(&entry->rank, KR_RANK_BOGUS);
+ vctx->err_cnt += 1;
+ log_bogus_rrsig(vctx, qry, 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,
+ .qry_uid = qry->uid,
+ .has_nsec3 = has_nsec3,
+ .flags = 0,
+ .err_cnt = 0,
+ .result = 0
+ };
+
+ int 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)) {
+
+ 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,
+ .qry_uid = qry->uid,
+ .has_nsec3 = has_nsec3,
+ .flags = 0,
+ .result = 0
+ };
+ int ret = kr_dnskeys_trusted(&vctx, qry->zone_cut.trust_anchor);
+ if (ret != 0) {
+ if (ret != kr_error(DNSSEC_INVALID_DS_ALGORITHM) &&
+ ret != kr_error(EAGAIN)) {
+ log_bogus_rrsig(&vctx, qry, qry->zone_cut.key, "bogus key");
+ }
+ knot_rrset_free(qry->zone_cut.key, qry->zone_cut.pool);
+ qry->zone_cut.key = NULL;
+ 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;
+ assert(parent);
+ 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(answer);
+ } else {
+ /* No-data answer */
+ ret = kr_nsec_existence_denial(answer, KNOT_AUTHORITY, proved_name, KNOT_RRTYPE_DS);
+ }
+ } 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");
+ qry->flags.DNSSEC_BOGUS = true;
+ } else if (proved_name[0] != '\0') { /* don't go to insecure for . DS */
+ VERBOSE_MSG(qry, "<= DS doesn't exist, going insecure\n");
+ 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)
+{
+ 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) {
+ return knot_rrsig_signer_name(rr->rrs.rdata);
+ }
+ }
+ 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);
+ if (!signer_name) {
+ signer_name = find_first_signer(&req->auth_selected);
+ }
+ return signer_name;
+}
+
+static int rrsig_not_found(kr_layer_t *ctx, const knot_rrset_t *rr)
+{
+ struct kr_request *req = ctx->req;
+ struct kr_query *qry = req->current_query;
+
+ /* Parent-side record, so don't ask for RRSIG.
+ * We won't receive it anyway. */
+ if (qry->stype == KNOT_RRTYPE_DS) {
+ return KR_STATE_FAIL;
+ }
+
+ struct kr_zonecut *cut = &qry->zone_cut;
+ const knot_dname_t *cut_name_start = qry->zone_cut.name;
+ bool use_cut = true;
+ if (knot_dname_in_bailiwick(rr->owner, cut_name_start) < 0) {
+ int zone_labels = knot_dname_labels(qry->zone_cut.name, NULL);
+ int matched_labels = knot_dname_matched_labels(qry->zone_cut.name, rr->owner);
+ int skip_labels = zone_labels - matched_labels;
+ while (skip_labels--) {
+ cut_name_start = knot_wire_next_label(cut_name_start, NULL);
+ }
+ /* try to find the name wanted among ancestors */
+ use_cut = false;
+ while (cut->parent) {
+ cut = cut->parent;
+ if (knot_dname_is_equal(cut_name_start, cut->name)) {
+ use_cut = true;
+ break;
+ }
+ };
+ }
+ struct kr_rplan *rplan = &req->rplan;
+ struct kr_query *next = kr_rplan_push(rplan, qry, rr->owner, rr->rclass, KNOT_RRTYPE_RRSIG);
+ if (!next) {
+ return KR_STATE_FAIL;
+ }
+ kr_zonecut_init(&next->zone_cut, cut_name_start, &req->pool);
+ if (use_cut) {
+ kr_zonecut_copy(&next->zone_cut, cut);
+ kr_zonecut_copy_trust(&next->zone_cut, cut);
+ } else {
+ next->flags.AWAIT_CUT = true;
+ }
+ if (qry->flags.FORWARD) {
+ next->flags.AWAIT_CUT = false;
+ }
+ next->flags.DNSSEC_WANT = true;
+ return KR_STATE_YIELD;
+}
+
+static int check_validation_result(kr_layer_t *ctx, 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");
+ 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, rr);
+ } else if (!kr_rank_test(invalid_entry->rank, KR_RANK_SECURE)) {
+ 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) {
+ assert(qry->cname_parent);
+ 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) {
+ assert(qry->cname_parent);
+ 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.
+ *
+ * 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(kr_layer_t *ctx, enum kr_rank rank_to_set,
+ const knot_dname_t *bailiwick)
+{
+ 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 (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 (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;
+
+ /* 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. */
+ /* @todo: Validating stub resolver mode. */
+ if (qry->flags.STUB) {
+ rank_records(ctx, 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(ctx, 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 unsecure 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(ctx, 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");
+ 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");
+ 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;
+ }
+ }
+
+ if (knot_wire_get_aa(pkt->wire) && qtype == KNOT_RRTYPE_DNSKEY) {
+ 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 == kr_error(DNSSEC_INVALID_DS_ALGORITHM)) {
+ VERBOSE_MSG(qry, ">< all DS entries use unsupported algorithm pairs, going insecure\n");
+ /* ^ the message is a bit imprecise to avoid being too verbose */
+ qry->flags.DNSSEC_WANT = false;
+ qry->flags.DNSSEC_INSECURE = true;
+ rank_records(ctx, KR_RANK_INSECURE, qry->zone_cut.name);
+ mark_insecure_parents(qry);
+ return KR_STATE_DONE;
+ } else if (ret != 0) {
+ VERBOSE_MSG(qry, "<= bad keys, broken trust chain\n");
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ }
+ }
+
+ /* 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_name_error_response_check(pkt, KNOT_AUTHORITY, qry->sname);
+ } 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");
+ 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_existence_denial(pkt, KNOT_AUTHORITY, knot_pkt_qname(pkt), knot_pkt_qtype(pkt));
+ } 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 insecured */
+ } else {
+ VERBOSE_MSG(qry, "<= bad NODATA proof\n");
+ 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. */
+ if (!(qry->flags.CACHED)) {
+ ret = validate_records(req, pkt, req->rplan.pool, has_nsec3);
+ if (ret != 0) {
+ /* something exceptional - no DNS key, empty pointers etc
+ * normally it shoudn't happen */
+ VERBOSE_MSG(qry, "<= couldn't validate RRSIGs\n");
+ qry->flags.DNSSEC_BOGUS = true;
+ return KR_STATE_FAIL;
+ }
+ /* check validation state and spawn subrequests */
+ if (!req->answ_validated) {
+ ret = check_validation_result(ctx, &req->answ_selected);
+ if (ret != KR_STATE_DONE) {
+ return ret;
+ }
+ }
+ if (!req->auth_validated) {
+ ret = check_validation_result(ctx, &req->auth_selected);
+ if (ret != KR_STATE_DONE) {
+ return ret;
+ }
+ }
+ }
+
+ 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");
+ 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-existance */
+ 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;
+}
+/** Module implementation. */
+const kr_layer_api_t *validate_layer(struct kr_module *module)
+{
+ static const kr_layer_api_t _layer = {
+ .consume = &validate,
+ };
+ /* Store module reference */
+ return &_layer;
+}
+
+int validate_init(struct kr_module *module)
+{
+ return kr_ok();
+}
+
+KR_MODULE_EXPORT(validate)
+
+#undef VERBOSE_MSG