diff options
Diffstat (limited to '')
-rw-r--r-- | lib/layer.h | 107 | ||||
-rw-r--r-- | lib/layer/cache.c | 20 | ||||
-rw-r--r-- | lib/layer/iterate.c | 1254 | ||||
-rw-r--r-- | lib/layer/iterate.h | 25 | ||||
-rw-r--r-- | lib/layer/mode.rst | 26 | ||||
-rw-r--r-- | lib/layer/test.integr/deckard.yaml | 13 | ||||
-rw-r--r-- | lib/layer/test.integr/iter_cname_length.rpl | 226 | ||||
-rw-r--r-- | lib/layer/test.integr/iter_limit_bad_glueless.rpl | 220 | ||||
-rw-r--r-- | lib/layer/test.integr/iter_limit_refuse.rpl | 150 | ||||
-rw-r--r-- | lib/layer/test.integr/kresd_config.j2 | 107 | ||||
-rw-r--r-- | lib/layer/validate.c | 1396 | ||||
-rw-r--r-- | lib/layer/validate.test.integr/deckard.yaml | 10 | ||||
-rw-r--r-- | lib/layer/validate.test.integr/fwd_insecure_but_rrsig_signer_invalid.rpl | 294 | ||||
-rw-r--r-- | lib/layer/validate.test.integr/kresd_config.j2 | 52 |
14 files changed, 3900 insertions, 0 deletions
diff --git a/lib/layer.h b/lib/layer.h new file mode 100644 index 0000000..7721560 --- /dev/null +++ b/lib/layer.h @@ -0,0 +1,107 @@ +/* Copyright (C) CZ.NIC, z.s.p.o. <knot-resolver@labs.nic.cz> + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "kresconfig.h" +#include "lib/defines.h" +#include "lib/utils.h" + +/** Layer processing states. Only one value at a time (but see TODO). + * + * Each state represents the state machine transition, + * and determines readiness for the next action. + * See struct kr_layer_api for the actions. + * + * TODO: the cookie module sometimes sets (_FAIL | _DONE) on purpose (!) + */ +enum kr_layer_state { + KR_STATE_CONSUME = 1 << 0, /*!< Consume data. */ + KR_STATE_PRODUCE = 1 << 1, /*!< Produce data. */ + + /*! Finished successfully or a special case: in CONSUME phase this can + * be used (by iterator) to do a transition to PRODUCE phase again, + * in which case the packet wasn't accepted for some reason. */ + KR_STATE_DONE = 1 << 2, + + KR_STATE_FAIL = 1 << 3, /*!< Error. */ + KR_STATE_YIELD = 1 << 4, /*!< Paused, waiting for a sub-query. */ +}; + +/** Check that a kr_layer_state makes sense. We're not very strict ATM. */ +static inline bool kr_state_consistent(enum kr_layer_state s) +{ +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare" +#pragma clang diagnostic ignored "-Wtautological-unsigned-enum-zero-compare" +#endif + return s >= 0 && s < (1 << 5); +#ifdef __clang__ +#pragma clang diagnostic pop +#endif +} + +/* Forward declarations. */ +struct kr_layer_api; + +/** Packet processing context. */ +typedef struct kr_layer { + int state; /*!< The current state; bitmap of enum kr_layer_state. */ + struct kr_request *req; /*!< The corresponding request. */ + const struct kr_layer_api *api; + knot_pkt_t *pkt; /*!< In glue for lua kr_layer_api it's used to pass the parameter. */ + struct sockaddr *dst; /*!< In glue for checkout layer it's used to pass the parameter. */ + bool is_stream; /*!< In glue for checkout layer it's used to pass the parameter. */ +} kr_layer_t; + +/** Packet processing module API. All functions return the new kr_layer_state. + * + * Lua modules are allowed to return nil/nothing, meaning the state shall not change. + */ +struct kr_layer_api { + /** Start of processing the DNS request. */ + int (*begin)(kr_layer_t *ctx); + + int (*reset)(kr_layer_t *ctx); + + /** Paired to begin, called both on successes and failures. */ + int (*finish)(kr_layer_t *ctx); + + /** Process an answer from upstream or from cache. + * Lua API: call is omitted iff (state & KR_STATE_FAIL). */ + int (*consume)(kr_layer_t *ctx, knot_pkt_t *pkt); + + /** Produce either an answer to the request or a query for upstream (or fail). + * Lua API: call is omitted iff (state & KR_STATE_FAIL). */ + int (*produce)(kr_layer_t *ctx, knot_pkt_t *pkt); + + /** Finalises the outbound query packet with the knowledge of the IP addresses. + * The checkout layer doesn't persist the state, so canceled subrequests + * don't affect the resolution or rest of the processing. + * Lua API: call is omitted iff (state & KR_STATE_FAIL). */ + int (*checkout)(kr_layer_t *ctx, knot_pkt_t *packet, struct sockaddr *dst, int type); + + /** Finalises the answer. + * Last chance to affect what will get into the answer, including EDNS. + * Not called if the packet is being dropped. */ + int (*answer_finalize)(kr_layer_t *ctx); + + /** The C module can store anything in here. */ + void *data; + + /** Internal to ./daemon/ffimodule.c. */ + int cb_slots[]; +}; + +typedef struct kr_layer_api kr_layer_api_t; + +/** Pickled layer state (api, input, state). */ +struct kr_layer_pickle { + struct kr_layer_pickle *next; + const struct kr_layer_api *api; + knot_pkt_t *pkt; + unsigned state; +}; + 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) |