diff options
Diffstat (limited to '')
-rw-r--r-- | src/knot/nameserver/axfr.c | 225 | ||||
-rw-r--r-- | src/knot/nameserver/axfr.h | 27 | ||||
-rw-r--r-- | src/knot/nameserver/chaos.c | 145 | ||||
-rw-r--r-- | src/knot/nameserver/chaos.h | 24 | ||||
-rw-r--r-- | src/knot/nameserver/internet.c | 728 | ||||
-rw-r--r-- | src/knot/nameserver/internet.h | 79 | ||||
-rw-r--r-- | src/knot/nameserver/ixfr.c | 332 | ||||
-rw-r--r-- | src/knot/nameserver/ixfr.h | 63 | ||||
-rw-r--r-- | src/knot/nameserver/log.h | 88 | ||||
-rw-r--r-- | src/knot/nameserver/notify.c | 92 | ||||
-rw-r--r-- | src/knot/nameserver/notify.h | 28 | ||||
-rw-r--r-- | src/knot/nameserver/nsec_proofs.c | 677 | ||||
-rw-r--r-- | src/knot/nameserver/nsec_proofs.h | 38 | ||||
-rw-r--r-- | src/knot/nameserver/process_query.c | 978 | ||||
-rw-r--r-- | src/knot/nameserver/process_query.h | 107 | ||||
-rw-r--r-- | src/knot/nameserver/query_module.c | 791 | ||||
-rw-r--r-- | src/knot/nameserver/query_module.h | 99 | ||||
-rw-r--r-- | src/knot/nameserver/tsig_ctx.c | 189 | ||||
-rw-r--r-- | src/knot/nameserver/tsig_ctx.h | 97 | ||||
-rw-r--r-- | src/knot/nameserver/update.c | 107 | ||||
-rw-r--r-- | src/knot/nameserver/update.h | 27 | ||||
-rw-r--r-- | src/knot/nameserver/xfr.c | 96 | ||||
-rw-r--r-- | src/knot/nameserver/xfr.h | 69 |
23 files changed, 5106 insertions, 0 deletions
diff --git a/src/knot/nameserver/axfr.c b/src/knot/nameserver/axfr.c new file mode 100644 index 0000000..dac4a43 --- /dev/null +++ b/src/knot/nameserver/axfr.c @@ -0,0 +1,225 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <urcu.h> + +#include "contrib/mempattern.h" +#include "contrib/sockaddr.h" +#include "knot/nameserver/axfr.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/log.h" +#include "knot/nameserver/xfr.h" +#include "libknot/libknot.h" + +#define ZONE_NAME(qdata) knot_pkt_qname((qdata)->query) +#define REMOTE(qdata) (struct sockaddr *)knotd_qdata_remote_addr(qdata) + +#define AXFROUT_LOG(priority, qdata, fmt...) \ + ns_log(priority, ZONE_NAME(qdata), LOG_OPERATION_AXFR, \ + LOG_DIRECTION_OUT, REMOTE(qdata), false, fmt) + +/* AXFR context. @note aliasing the generic xfr_proc */ +struct axfr_proc { + struct xfr_proc proc; + trie_it_t *i; + zone_tree_it_t it; + unsigned cur_rrset; +}; + +static int axfr_put_rrsets(knot_pkt_t *pkt, zone_node_t *node, + struct axfr_proc *state) +{ + assert(node != NULL); + + /* Append all RRs. */ + for (unsigned i = state->cur_rrset; i < node->rrset_count; ++i) { + knot_rrset_t rrset = node_rrset_at(node, i); + if (rrset.type == KNOT_RRTYPE_SOA) { + continue; + } + + int ret = knot_pkt_put(pkt, 0, &rrset, KNOT_PF_NOTRUNC | KNOT_PF_ORIGTTL); + if (ret != KNOT_EOK) { + /* If something failed, remember the current RR for later. */ + state->cur_rrset = i; + return ret; + } + if (pkt->size > KNOT_WIRE_PTR_MAX) { + // optimization: once the XFR DNS message is > 16 KiB, compression + // is limited. Better wrap to next message. + state->cur_rrset = i + 1; + return KNOT_ESPACE; + } + } + + state->cur_rrset = 0; + + return KNOT_EOK; +} + +static int axfr_process_node_tree(knot_pkt_t *pkt, const void *item, + struct xfr_proc *state) +{ + assert(item != NULL); + + struct axfr_proc *axfr = (struct axfr_proc*)state; + + int ret = zone_tree_it_begin((zone_tree_t *)item, &axfr->it); // does nothing if already iterating + + /* Put responses. */ + while (ret == KNOT_EOK && !zone_tree_it_finished(&axfr->it)) { + zone_node_t *node = zone_tree_it_val(&axfr->it); + ret = axfr_put_rrsets(pkt, node, axfr); + if (ret == KNOT_EOK) { + zone_tree_it_next(&axfr->it); + } + } + + /* Finished all nodes. */ + if (ret == KNOT_EOK) { + zone_tree_it_free(&axfr->it); + } + return ret; +} + +static void axfr_query_cleanup(knotd_qdata_t *qdata) +{ + struct axfr_proc *axfr = (struct axfr_proc *)qdata->extra->ext; + + zone_tree_it_free(&axfr->it); + ptrlist_free(&axfr->proc.nodes, qdata->mm); + mm_free(qdata->mm, axfr); + + /* Allow zone changes (finished). */ + rcu_read_unlock(); +} + +static int axfr_query_check(knotd_qdata_t *qdata) +{ + NS_NEED_ZONE(qdata, KNOT_RCODE_NOTAUTH); + NS_NEED_AUTH(qdata, ACL_ACTION_TRANSFER); + NS_NEED_ZONE_CONTENTS(qdata); + + return KNOT_STATE_DONE; +} + +static int axfr_query_init(knotd_qdata_t *qdata) +{ + assert(qdata); + + /* Check AXFR query validity. */ + if (axfr_query_check(qdata) == KNOT_STATE_FAIL) { + if (qdata->rcode == KNOT_RCODE_FORMERR) { + return KNOT_EMALF; + } else { + return KNOT_EDENIED; + } + } + + if (zone_get_flag(qdata->extra->zone, ZONE_XFR_FROZEN, false)) { + qdata->rcode = KNOT_RCODE_REFUSED; + qdata->rcode_ede = KNOT_EDNS_EDE_NOT_READY; + return KNOT_EAGAIN; + } + + /* Create transfer processing context. */ + knot_mm_t *mm = qdata->mm; + struct axfr_proc *axfr = mm_alloc(mm, sizeof(struct axfr_proc)); + if (axfr == NULL) { + return KNOT_ENOMEM; + } + memset(axfr, 0, sizeof(struct axfr_proc)); + init_list(&axfr->proc.nodes); + + /* Put data to process. */ + xfr_stats_begin(&axfr->proc.stats); + const zone_contents_t *contents = qdata->extra->contents; + /* Must be non-NULL for the first message. */ + assert(contents); + ptrlist_add(&axfr->proc.nodes, contents->nodes, mm); + /* Put NSEC3 data if exists. */ + if (!zone_tree_is_empty(contents->nsec3_nodes)) { + ptrlist_add(&axfr->proc.nodes, contents->nsec3_nodes, mm); + } + + /* Set up cleanup callback. */ + qdata->extra->ext = axfr; + qdata->extra->ext_cleanup = &axfr_query_cleanup; + + /* No zone changes during multipacket answer (unlocked in axfr_answer_cleanup) */ + rcu_read_lock(); + + return KNOT_EOK; +} + +int axfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt == NULL || qdata == NULL) { + return KNOT_STATE_FAIL; + } + + /* AXFR over UDP isn't allowed, respond with NOTIMPL. */ + if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) { + qdata->rcode = KNOT_RCODE_NOTIMPL; + return KNOT_STATE_FAIL; + } + + /* Initialize on first call. */ + struct axfr_proc *axfr = qdata->extra->ext; + if (axfr == NULL) { + int ret = axfr_query_init(qdata); + axfr = qdata->extra->ext; + switch (ret) { + case KNOT_EOK: /* OK */ + AXFROUT_LOG(LOG_INFO, qdata, "started, serial %u", + zone_contents_serial(qdata->extra->contents)); + break; + case KNOT_EDENIED: /* Not authorized, already logged. */ + return KNOT_STATE_FAIL; + case KNOT_EMALF: /* Malformed query. */ + AXFROUT_LOG(LOG_DEBUG, qdata, "malformed query"); + return KNOT_STATE_FAIL; + case KNOT_EAGAIN: /* Outgoing AXFR temporarily disabled. */ + AXFROUT_LOG(LOG_INFO, qdata, "outgoing AXFR frozen"); + return KNOT_STATE_FAIL; + default: + AXFROUT_LOG(LOG_ERR, qdata, "failed to start (%s)", + knot_strerror(ret)); + return KNOT_STATE_FAIL; + } + } + + /* Reserve space for TSIG. */ + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + /* Answer current packet (or continue). */ + ret = xfr_process_list(pkt, &axfr_process_node_tree, qdata); + switch (ret) { + case KNOT_ESPACE: /* Couldn't write more, send packet and continue. */ + return KNOT_STATE_PRODUCE; /* Check for more. */ + case KNOT_EOK: /* Last response. */ + xfr_stats_end(&axfr->proc.stats); + xfr_log_finished(ZONE_NAME(qdata), LOG_OPERATION_AXFR, LOG_DIRECTION_OUT, + REMOTE(qdata), false, &axfr->proc.stats); + return KNOT_STATE_DONE; + default: /* Generic error. */ + AXFROUT_LOG(LOG_ERR, qdata, "failed (%s)", knot_strerror(ret)); + return KNOT_STATE_FAIL; + } +} diff --git a/src/knot/nameserver/axfr.h b/src/knot/nameserver/axfr.h new file mode 100644 index 0000000..81fcad8 --- /dev/null +++ b/src/knot/nameserver/axfr.h @@ -0,0 +1,27 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/nameserver/process_query.h" +#include "libknot/packet/pkt.h" + +/*! + * \brief Process an AXFR query message. + * + * \return KNOT_STATE_* processing states + */ +int axfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata); diff --git a/src/knot/nameserver/chaos.c b/src/knot/nameserver/chaos.c new file mode 100644 index 0000000..b83e2f5 --- /dev/null +++ b/src/knot/nameserver/chaos.c @@ -0,0 +1,145 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <strings.h> +#include <stdlib.h> + +#include "knot/nameserver/chaos.h" +#include "knot/conf/conf.h" +#include "libknot/libknot.h" + +#define WISH "Knot DNS developers wish you " +#define HOPE "Knot DNS developers hope you " + +static const char *wishes[] = { + HOPE "have all your important life questions answered without SERVFAIL.", + WISH "many wonderful people in your domain.", + WISH "non-empty lymph nodes.", + HOPE "resolve the . of your problems.", + WISH "long enough TTL.", + HOPE "become authoritative master in your domain.", + HOPE "always find useful PTR in CHAOS.", + "Canonical name is known to both DNS experts and Ubuntu users.", + HOPE "never forget both your name and address.", + "Don't fix broken CNAME chains with glue!", + WISH "no Additional section in your TODO list.", + HOPE "won't find surprising news in today's journal.", + HOPE "perform rollover often just when playing roulette.", + HOPE "get notified before your domain registration expires.", +}; + +#undef WISH +#undef HOPE + +static const char *get_txt_response_string(knot_pkt_t *response) +{ + char qname[32]; + if (knot_dname_to_str(qname, knot_pkt_qname(response), sizeof(qname)) == NULL) { + return NULL; + } + + const char *response_str = NULL; + + /* Allow hostname.bind. for compatibility. */ + if (strcasecmp("id.server.", qname) == 0 || + strcasecmp("hostname.bind.", qname) == 0) { + conf_val_t val = conf_get(conf(), C_SRV, C_IDENT); + if (val.code == KNOT_EOK) { + response_str = conf_str(&val); // Can be NULL! + } else { + response_str = conf()->hostname; + } + /* Allow version.bind. for compatibility. */ + } else if (strcasecmp("version.server.", qname) == 0 || + strcasecmp("version.bind.", qname) == 0) { + conf_val_t val = conf_get(conf(), C_SRV, C_VERSION); + if (val.code == KNOT_EOK) { + response_str = conf_str(&val); // Can be NULL! + } else { + response_str = "Knot DNS " PACKAGE_VERSION; + } + } else if (strcasecmp("fortune.", qname) == 0) { + conf_val_t val = conf_get(conf(), C_SRV, C_VERSION); + if (val.code != KNOT_EOK) { + uint16_t wishno = knot_wire_get_id(response->wire) % + (sizeof(wishes) / sizeof(wishes[0])); + response_str = wishes[wishno]; + } + } + + return response_str; +} + +static int create_txt_rrset(knot_rrset_t *rrset, const knot_dname_t *owner, + const char *response_str, knot_mm_t *mm) +{ + /* Truncate response to one TXT label. */ + size_t response_len = strlen(response_str); + if (response_len > UINT8_MAX) { + response_len = UINT8_MAX; + } + + knot_dname_t *rowner = knot_dname_copy(owner, mm); + if (rowner == NULL) { + return KNOT_ENOMEM; + } + + knot_rrset_init(rrset, rowner, KNOT_RRTYPE_TXT, KNOT_CLASS_CH, 0); + uint8_t rdata[response_len + 1]; + + rdata[0] = response_len; + memcpy(&rdata[1], response_str, response_len); + + int ret = knot_rrset_add_rdata(rrset, rdata, response_len + 1, mm); + if (ret != KNOT_EOK) { + knot_dname_free(rrset->owner, mm); + return ret; + } + + return KNOT_EOK; +} + +static int answer_txt(knot_pkt_t *response) +{ + const char *response_str = get_txt_response_string(response); + if (response_str == NULL || response_str[0] == '\0') { + return KNOT_RCODE_REFUSED; + } + + knot_rrset_t rrset; + int ret = create_txt_rrset(&rrset, knot_pkt_qname(response), + response_str, &response->mm); + if (ret != KNOT_EOK) { + return KNOT_RCODE_SERVFAIL; + } + + int result = knot_pkt_put(response, 0, &rrset, KNOT_PF_FREE); + if (result != KNOT_EOK) { + knot_rrset_clear(&rrset, &response->mm); + return KNOT_RCODE_SERVFAIL; + } + + return KNOT_RCODE_NOERROR; +} + +int knot_chaos_answer(knot_pkt_t *pkt) +{ + if (knot_pkt_qtype(pkt) != KNOT_RRTYPE_TXT) { + return KNOT_RCODE_REFUSED; + } + + return answer_txt(pkt); +} diff --git a/src/knot/nameserver/chaos.h b/src/knot/nameserver/chaos.h new file mode 100644 index 0000000..f875abe --- /dev/null +++ b/src/knot/nameserver/chaos.h @@ -0,0 +1,24 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" + +/*! + * \brief Create a response for a given query in the CHAOS class. + */ +int knot_chaos_answer(knot_pkt_t *pkt); diff --git a/src/knot/nameserver/internet.c b/src/knot/nameserver/internet.c new file mode 100644 index 0000000..51bde97 --- /dev/null +++ b/src/knot/nameserver/internet.c @@ -0,0 +1,728 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "libknot/libknot.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-nsec.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/nsec_proofs.h" +#include "knot/nameserver/query_module.h" +#include "knot/zone/serial.h" +#include "contrib/mempattern.h" + +/*! \brief Check if given node was already visited. */ +static int wildcard_has_visited(knotd_qdata_t *qdata, const zone_node_t *node) +{ + struct wildcard_hit *item; + WALK_LIST(item, qdata->extra->wildcards) { + if (item->node == node) { + return true; + } + } + return false; +} + +/*! \brief Mark given node as visited. */ +static int wildcard_visit(knotd_qdata_t *qdata, const zone_node_t *node, + const zone_node_t *prev, const knot_dname_t *sname) +{ + assert(qdata); + assert(node); + + if (node->flags & NODE_FLAGS_NONAUTH) { + return KNOT_EOK; + } + + knot_mm_t *mm = qdata->mm; + struct wildcard_hit *item = mm_alloc(mm, sizeof(struct wildcard_hit)); + item->node = node; + item->prev = prev; + item->sname = sname; + add_tail(&qdata->extra->wildcards, (node_t *)item); + return KNOT_EOK; +} + +/*! \brief Synthesizes a CNAME RR from a DNAME. */ +static int dname_cname_synth(const knot_rrset_t *dname_rr, + const knot_dname_t *qname, + knot_rrset_t *cname_rrset, + knot_mm_t *mm) +{ + if (cname_rrset == NULL) { + return KNOT_EINVAL; + } + knot_dname_t *owner_copy = knot_dname_copy(qname, mm); + if (owner_copy == NULL) { + return KNOT_ENOMEM; + } + knot_rrset_init(cname_rrset, owner_copy, KNOT_RRTYPE_CNAME, dname_rr->rclass, + dname_rr->ttl); + + /* Replace last labels of qname with DNAME. */ + const knot_dname_t *dname_wire = dname_rr->owner; + const knot_dname_t *dname_tgt = knot_dname_target(dname_rr->rrs.rdata); + size_t labels = knot_dname_labels(dname_wire, NULL); + knot_dname_t *cname = knot_dname_replace_suffix(qname, labels, dname_tgt, mm); + if (cname == NULL) { + knot_dname_free(owner_copy, mm); + return KNOT_ENOMEM; + } + + /* Store DNAME into RDATA. */ + size_t cname_size = knot_dname_size(cname); + uint8_t cname_rdata[cname_size]; + memcpy(cname_rdata, cname, cname_size); + knot_dname_free(cname, mm); + + int ret = knot_rrset_add_rdata(cname_rrset, cname_rdata, cname_size, mm); + if (ret != KNOT_EOK) { + knot_dname_free(owner_copy, mm); + return ret; + } + + return KNOT_EOK; +} + +/*! + * \brief Checks if the name created by replacing the owner of \a dname_rrset + * in the \a qname by the DNAME's target would be longer than allowed. + */ +static bool dname_cname_cannot_synth(const knot_rrset_t *rrset, const knot_dname_t *qname) +{ + if (knot_dname_labels(qname, NULL) - knot_dname_labels(rrset->owner, NULL) + + knot_dname_labels(knot_dname_target(rrset->rrs.rdata), NULL) > KNOT_DNAME_MAXLABELS) { + return true; + } else if (knot_dname_size(qname) - knot_dname_size(rrset->owner) + + knot_dname_size(knot_dname_target(rrset->rrs.rdata)) > KNOT_DNAME_MAXLEN) { + return true; + } else { + return false; + } +} + +/*! \brief DNSSEC both requested & available. */ +static bool have_dnssec(knotd_qdata_t *qdata) +{ + return knot_pkt_has_dnssec(qdata->query) && + qdata->extra->contents->dnssec; +} + +/*! \brief This is a wildcard-covered or any other terminal node for QNAME. + * e.g. positive answer. + */ +static int put_answer(knot_pkt_t *pkt, uint16_t type, knotd_qdata_t *qdata) +{ + /* Wildcard expansion or exact match, either way RRSet owner is + * is QNAME. We can fake name synthesis by setting compression hint to + * QNAME position. Just need to check if we're answering QNAME and not + * a CNAME target. + */ + uint16_t compr_hint = KNOT_COMPR_HINT_NONE; + if (pkt->rrset_count == 0) { /* Guaranteed first answer. */ + compr_hint = KNOT_COMPR_HINT_QNAME; + } + + unsigned put_rr_flags = (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) ? + KNOT_PF_NULL : KNOT_PF_NOTRUNC; + put_rr_flags |= KNOT_PF_ORIGTTL; + + knot_rrset_t rrsigs = node_rrset(qdata->extra->node, KNOT_RRTYPE_RRSIG); + knot_rrset_t rrset; + switch (type) { + case KNOT_RRTYPE_ANY: /* Put one RRSet, not all. */ + rrset = node_rrset_at(qdata->extra->node, 0); + break; + case KNOT_RRTYPE_RRSIG: /* Put some RRSIGs, not all. */ + if (!knot_rrset_empty(&rrsigs)) { + knot_rrset_init(&rrset, rrsigs.owner, rrsigs.type, rrsigs.rclass, rrsigs.ttl); + int ret = knot_synth_rrsig(KNOT_RRTYPE_ANY, &rrsigs.rrs, &rrset.rrs, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } else { + knot_rrset_init_empty(&rrset); + } + break; + default: /* Single RRSet of given type. */ + rrset = node_rrset(qdata->extra->node, type); + break; + } + + if (knot_rrset_empty(&rrset)) { + return KNOT_EOK; + } + + return process_query_put_rr(pkt, qdata, &rrset, &rrsigs, compr_hint, put_rr_flags); +} + +/*! \brief Puts optional SOA RRSet to the Authority section of the response. */ +static int put_authority_soa(knot_pkt_t *pkt, knotd_qdata_t *qdata, + const zone_contents_t *zone) +{ + knot_rrset_t soa = node_rrset(zone->apex, KNOT_RRTYPE_SOA); + knot_rrset_t rrsigs = node_rrset(zone->apex, KNOT_RRTYPE_RRSIG); + return process_query_put_rr(pkt, qdata, &soa, &rrsigs, + KNOT_COMPR_HINT_NONE, + KNOT_PF_NOTRUNC | KNOT_PF_SOAMINTTL); +} + +/*! \brief Put the delegation NS RRSet to the Authority section. */ +static int put_delegation(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + /* Find closest delegation point. */ + while (!(qdata->extra->node->flags & NODE_FLAGS_DELEG)) { + qdata->extra->node = node_parent(qdata->extra->node); + } + + /* Insert NS record. */ + knot_rrset_t rrset = node_rrset(qdata->extra->node, KNOT_RRTYPE_NS); + knot_rrset_t rrsigs = node_rrset(qdata->extra->node, KNOT_RRTYPE_RRSIG); + return process_query_put_rr(pkt, qdata, &rrset, &rrsigs, + KNOT_COMPR_HINT_NONE, 0); +} + +static int put_nsec3_bitmap(const zone_node_t *for_node, knot_pkt_t *pkt, + knotd_qdata_t *qdata, uint32_t flags) +{ + const zone_node_t *node = node_nsec3_get(for_node); + if (node == NULL) { + return KNOT_EOK; + } + + knot_rrset_t nsec3 = node_rrset(node, KNOT_RRTYPE_NSEC3); + if (knot_rrset_empty(&nsec3)) { + return KNOT_EOK; + } + + knot_rrset_t rrsig = node_rrset(node, KNOT_RRTYPE_RRSIG); + return process_query_put_rr(pkt, qdata, &nsec3, &rrsig, + KNOT_COMPR_HINT_NONE, flags); +} + +/*! \brief Put additional records for given RR. */ +static int put_additional(knot_pkt_t *pkt, const knot_rrset_t *rr, + knotd_qdata_t *qdata, knot_rrinfo_t *info, int state) +{ + if (rr->additional == NULL) { + return KNOT_EOK; + } + + /* Valid types for ADDITIONALS insertion. */ + /* \note Not resolving CNAMEs as MX/NS name must not be an alias. (RFC2181/10.3) */ + static uint16_t ar_type_list[] = { KNOT_RRTYPE_A, KNOT_RRTYPE_AAAA, KNOT_RRTYPE_SVCB }; + static const int ar_type_count_default = 2; + + int ret = KNOT_EOK; + + additional_t *additional = (additional_t *)rr->additional; + + /* Iterate over the additionals. */ + for (uint16_t i = 0; i < additional->count; i++) { + glue_t *glue = &additional->glues[i]; + uint32_t flags = KNOT_PF_NULL; + + /* Optional glue doesn't cause truncation. (RFC 1034/4.3.2 step 3b). */ + if (state != KNOTD_IN_STATE_DELEG || glue->optional) { + flags |= KNOT_PF_NOTRUNC; + } + + int ar_type_count = ar_type_count_default, ar_present = 0; + if (rr->type == KNOT_RRTYPE_SVCB || rr->type == KNOT_RRTYPE_HTTPS) { + ar_type_list[ar_type_count++] = rr->type; + } + + uint16_t hint = knot_compr_hint(info, KNOT_COMPR_HINT_RDATA + + glue->ns_pos); + const zone_node_t *gluenode = glue_node(glue, qdata->extra->node); + knot_rrset_t rrsigs = node_rrset(gluenode, KNOT_RRTYPE_RRSIG); + for (int k = 0; k < ar_type_count; ++k) { + knot_rrset_t rrset = node_rrset(gluenode, ar_type_list[k]); + if (knot_rrset_empty(&rrset)) { + continue; + } + ret = process_query_put_rr(pkt, qdata, &rrset, &rrsigs, + hint, flags); + if (ret != KNOT_EOK) { + break; + } + ar_present++; + } + + if ((rr->type == KNOT_RRTYPE_SVCB || rr->type == KNOT_RRTYPE_HTTPS) && + ar_present < ar_type_count && have_dnssec(qdata)) { + // it would be nicer to have this in solve_additional_dnssec, but + // it seems infeasible to transfer all the context there + + // adding an NSEC(3) record proving non-existence of some of the + // glue with its bitmap + if (knot_is_nsec3_enabled(qdata->extra->contents)) { + ret = put_nsec3_bitmap(gluenode, pkt, qdata, flags); + } else { + knot_rrset_t nsec = node_rrset(gluenode, KNOT_RRTYPE_NSEC); + if (!knot_rrset_empty(&nsec)) { + ret = process_query_put_rr(pkt, qdata, &nsec, &rrsigs, + KNOT_COMPR_HINT_NONE, flags); + } + } + if (ret != KNOT_EOK) { + break; + } + } + } + + return ret; +} + +static int follow_cname(knot_pkt_t *pkt, uint16_t rrtype, knotd_qdata_t *qdata) +{ + /* CNAME chain processing limit. */ + if (++qdata->extra->cname_chain > CNAME_CHAIN_MAX) { + qdata->extra->node = NULL; + return KNOTD_IN_STATE_HIT; + } + + const zone_node_t *cname_node = qdata->extra->node; + knot_rrset_t cname_rr = node_rrset(qdata->extra->node, rrtype); + knot_rrset_t rrsigs = node_rrset(qdata->extra->node, KNOT_RRTYPE_RRSIG); + + assert(!knot_rrset_empty(&cname_rr)); + + /* Check whether RR is already in the packet. */ + uint16_t flags = KNOT_PF_CHECKDUP; + + /* Now, try to put CNAME to answer. */ + uint16_t rr_count_before = pkt->rrset_count; + int ret = process_query_put_rr(pkt, qdata, &cname_rr, &rrsigs, 0, flags); + switch (ret) { + case KNOT_EOK: break; + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; + default: return KNOTD_IN_STATE_ERROR; + } + + /* Synthesize CNAME if followed DNAME. */ + if (rrtype == KNOT_RRTYPE_DNAME) { + if (dname_cname_cannot_synth(&cname_rr, qdata->name)) { + qdata->rcode = KNOT_RCODE_YXDOMAIN; + } else { + knot_rrset_t dname_rr = cname_rr; + ret = dname_cname_synth(&dname_rr, qdata->name, + &cname_rr, &pkt->mm); + if (ret != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_SERVFAIL; + return KNOTD_IN_STATE_ERROR; + } + ret = process_query_put_rr(pkt, qdata, &cname_rr, NULL, 0, KNOT_PF_FREE); + switch (ret) { + case KNOT_EOK: break; + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; + default: return KNOTD_IN_STATE_ERROR; + } + if (knot_pkt_qtype(pkt) == KNOT_RRTYPE_CNAME) { + /* Synthesized CNAME is a perfect answer to query. */ + return KNOTD_IN_STATE_HIT; + } + } + } + + /* Check if RR count increased. */ + if (pkt->rrset_count <= rr_count_before) { + qdata->extra->node = NULL; /* Act as if the name leads to nowhere. */ + return KNOTD_IN_STATE_HIT; + } + + /* If node is a wildcard, follow only if we didn't visit the same node + * earlier, as that would mean a CNAME loop. */ + if (knot_dname_is_wildcard(cname_node->owner)) { + + /* Check if is not in wildcard nodes (loop). */ + if (wildcard_has_visited(qdata, cname_node)) { + qdata->extra->node = NULL; /* Act as if the name leads to nowhere. */ + + if (wildcard_visit(qdata, cname_node, qdata->extra->previous, qdata->name) != KNOT_EOK) { // in case of loop, re-add this cname_node because it might have different qdata->name + return KNOTD_IN_STATE_ERROR; + } + return KNOTD_IN_STATE_HIT; + } + + /* Put to wildcard node list. */ + if (wildcard_visit(qdata, cname_node, qdata->extra->previous, qdata->name) != KNOT_EOK) { + return KNOTD_IN_STATE_ERROR; + } + } + + /* Now follow the next CNAME TARGET. */ + qdata->name = knot_cname_name(cname_rr.rrs.rdata); + + return KNOTD_IN_STATE_FOLLOW; +} + +static int name_found(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + uint16_t qtype = knot_pkt_qtype(pkt); + + /* DS query at DP is answered normally, but everything else at/below DP + * triggers referral response. */ + if (((qdata->extra->node->flags & NODE_FLAGS_DELEG) && qtype != KNOT_RRTYPE_DS) || + (qdata->extra->node->flags & NODE_FLAGS_NONAUTH)) { + return KNOTD_IN_STATE_DELEG; + } + + if (node_rrtype_exists(qdata->extra->node, KNOT_RRTYPE_CNAME) + && qtype != KNOT_RRTYPE_CNAME + && qtype != KNOT_RRTYPE_RRSIG + && qtype != KNOT_RRTYPE_NSEC + && qtype != KNOT_RRTYPE_ANY) { + return follow_cname(pkt, KNOT_RRTYPE_CNAME, qdata); + } + + uint16_t old_rrcount = pkt->rrset_count; + int ret = put_answer(pkt, qtype, qdata); + if (ret != KNOT_EOK) { + if (ret == KNOT_ESPACE && (qdata->params->proto == KNOTD_QUERY_PROTO_UDP)) { + return KNOTD_IN_STATE_TRUNC; + } else { + return KNOTD_IN_STATE_ERROR; + } + } + + /* Check for NODATA (=0 RRs added). */ + if (old_rrcount == pkt->rrset_count) { + return KNOTD_IN_STATE_NODATA; + } else { + return KNOTD_IN_STATE_HIT; + } +} + +static int name_not_found(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + /* Name is covered by wildcard. */ + if (qdata->extra->encloser->flags & NODE_FLAGS_WILDCARD_CHILD) { + /* Find wildcard child in the zone. */ + const zone_node_t *wildcard_node = + zone_contents_find_wildcard_child( + qdata->extra->contents, qdata->extra->encloser); + + qdata->extra->node = wildcard_node; + assert(qdata->extra->node != NULL); + + /* Follow expanded wildcard. */ + int next_state = name_found(pkt, qdata); + + /* Put to wildcard node list. */ + if (wildcard_has_visited(qdata, wildcard_node)) { + return next_state; + } + if (wildcard_visit(qdata, wildcard_node, qdata->extra->previous, qdata->name) != KNOT_EOK) { + next_state = KNOTD_IN_STATE_ERROR; + } + + return next_state; + } + + /* Name is under DNAME, use it for substitution. */ + bool encloser_auth = !(qdata->extra->encloser->flags & (NODE_FLAGS_NONAUTH | NODE_FLAGS_DELEG)); + knot_rrset_t dname_rrset = node_rrset(qdata->extra->encloser, KNOT_RRTYPE_DNAME); + if (encloser_auth && !knot_rrset_empty(&dname_rrset)) { + qdata->extra->node = qdata->extra->encloser; /* Follow encloser as new node. */ + return follow_cname(pkt, KNOT_RRTYPE_DNAME, qdata); + } + + /* Look up an authoritative encloser or its parent. */ + const zone_node_t *node = qdata->extra->encloser; + while (node->rrset_count == 0 || node->flags & NODE_FLAGS_NONAUTH) { + node = node_parent(node); + assert(node); + } + + /* Name is below delegation. */ + if ((node->flags & NODE_FLAGS_DELEG)) { + qdata->extra->node = node; + return KNOTD_IN_STATE_DELEG; + } + + return KNOTD_IN_STATE_MISS; +} + +static int solve_name(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + int ret = zone_contents_find_dname(qdata->extra->contents, qdata->name, + &qdata->extra->node, &qdata->extra->encloser, + &qdata->extra->previous); + + switch (ret) { + case ZONE_NAME_FOUND: + return name_found(pkt, qdata); + case ZONE_NAME_NOT_FOUND: + return name_not_found(pkt, qdata); + case KNOT_EOUTOFZONE: + assert(state == KNOTD_IN_STATE_FOLLOW); /* CNAME/DNAME chain only. */ + return KNOTD_IN_STATE_HIT; + default: + return KNOTD_IN_STATE_ERROR; + } +} + +static int solve_answer(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, void *ctx) +{ + int old_state = state; + + /* Do not solve if already solved, e.g. in a module. */ + if (state == KNOTD_IN_STATE_HIT) { + return state; + } + + /* Get answer to QNAME. */ + state = solve_name(state, pkt, qdata); + + /* Promote NODATA from a module if nothing found in zone. */ + if (state == KNOTD_IN_STATE_MISS && old_state == KNOTD_IN_STATE_NODATA) { + state = old_state; + } + + /* Is authoritative answer unless referral. + * Must check before we chase the CNAME chain. */ + if (state != KNOTD_IN_STATE_DELEG) { + knot_wire_set_aa(pkt->wire); + } + + /* Additional resolving for CNAME/DNAME chain. */ + while (state == KNOTD_IN_STATE_FOLLOW) { + state = solve_name(state, pkt, qdata); + } + + return state; +} + +static int solve_answer_dnssec(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, void *ctx) +{ + /* RFC4035, section 3.1 RRSIGs for RRs in ANSWER are mandatory. */ + int ret = nsec_append_rrsigs(pkt, qdata, false); + switch (ret) { + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; + case KNOT_EOK: return state; + default: return KNOTD_IN_STATE_ERROR; + } +} + +static int solve_authority(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, void *ctx) +{ + int ret = KNOT_ERROR; + const zone_contents_t *zone_contents = qdata->extra->contents; + + switch (state) { + case KNOTD_IN_STATE_HIT: /* Positive response. */ + ret = KNOT_EOK; + break; + case KNOTD_IN_STATE_MISS: /* MISS, set NXDOMAIN RCODE. */ + qdata->rcode = KNOT_RCODE_NXDOMAIN; + ret = put_authority_soa(pkt, qdata, zone_contents); + break; + case KNOTD_IN_STATE_NODATA: /* NODATA append AUTHORITY SOA. */ + ret = put_authority_soa(pkt, qdata, zone_contents); + break; + case KNOTD_IN_STATE_DELEG: /* Referral response. */ + ret = put_delegation(pkt, qdata); + break; + case KNOTD_IN_STATE_TRUNC: /* Truncated ANSWER. */ + ret = KNOT_ESPACE; + break; + case KNOTD_IN_STATE_ERROR: /* Error resolving ANSWER. */ + break; + default: + assert(0); + break; + } + + /* Evaluate final state. */ + switch (ret) { + case KNOT_EOK: return state; /* Keep current state. */ + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; /* Truncated. */ + default: return KNOTD_IN_STATE_ERROR; /* Error. */ + } +} + +static int solve_authority_dnssec(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, void *ctx) +{ + int ret = KNOT_ERROR; + + /* Authenticated denial of existence. */ + switch (state) { + case KNOTD_IN_STATE_HIT: ret = KNOT_EOK; break; + case KNOTD_IN_STATE_MISS: ret = nsec_prove_nxdomain(pkt, qdata); break; + case KNOTD_IN_STATE_NODATA: ret = nsec_prove_nodata(pkt, qdata); break; + case KNOTD_IN_STATE_DELEG: ret = nsec_prove_dp_security(pkt, qdata); break; + case KNOTD_IN_STATE_TRUNC: ret = KNOT_ESPACE; break; + case KNOTD_IN_STATE_ERROR: ret = KNOT_ERROR; break; + default: + assert(0); + break; + } + + /* RFC4035 3.1.3 Prove visited wildcards. + * Wildcard expansion applies for Name Error, Wildcard Answer and + * No Data proofs if at one point the search expanded a wildcard node. */ + if (ret == KNOT_EOK) { + ret = nsec_prove_wildcards(pkt, qdata); + } + + /* RFC4035, section 3.1 RRSIGs for RRs in AUTHORITY are mandatory. */ + if (ret == KNOT_EOK) { + ret = nsec_append_rrsigs(pkt, qdata, false); + } + + /* Evaluate final state. */ + switch (ret) { + case KNOT_EOK: return state; /* Keep current state. */ + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; /* Truncated. */ + default: return KNOTD_IN_STATE_ERROR; /* Error. */ + } +} + +static int solve_additional(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, + void *ctx) +{ + int ret = KNOT_EOK, rrset_count = pkt->rrset_count; + + /* Scan all RRs in ANSWER/AUTHORITY. */ + for (int i = 0; i < rrset_count; ++i) { + knot_rrset_t *rr = &pkt->rr[i]; + knot_rrinfo_t *info = &pkt->rr_info[i]; + + /* Skip types for which it doesn't apply. */ + if (!knot_rrtype_additional_needed(rr->type)) { + continue; + } + + /* Put additional records for given type. */ + ret = put_additional(pkt, rr, qdata, info, state); + if (ret != KNOT_EOK) { + break; + } + } + + /* Evaluate final state. */ + switch (ret) { + case KNOT_EOK: return state; /* Keep current state. */ + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; /* Truncated. */ + default: return KNOTD_IN_STATE_ERROR; /* Error. */ + } +} + +static int solve_additional_dnssec(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, void *ctx) +{ + /* RFC4035, section 3.1 RRSIGs for RRs in ADDITIONAL are optional. */ + int ret = nsec_append_rrsigs(pkt, qdata, true); + switch (ret) { + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; + case KNOT_EOK: return state; + default: return KNOTD_IN_STATE_ERROR; + } +} + +/*! \brief Helper for internet_query repetitive code. */ +#define SOLVE_STEP(solver, state, context) \ + state = (solver)(state, pkt, qdata, context); \ + if (state == KNOTD_IN_STATE_TRUNC) { \ + return KNOT_STATE_DONE; \ + } else if (state == KNOTD_IN_STATE_ERROR) { \ + return KNOT_STATE_FAIL; \ + } + +static int answer_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + int state = KNOTD_IN_STATE_BEGIN; + struct query_plan *plan = qdata->extra->zone->query_plan; + struct query_step *step; + + bool with_dnssec = have_dnssec(qdata); + + /* Resolve PREANSWER. */ + if (plan != NULL) { + WALK_LIST(step, plan->stage[KNOTD_STAGE_PREANSWER]) { + SOLVE_STEP(step->process, state, step->ctx); + } + } + + /* Resolve ANSWER. */ + knot_pkt_begin(pkt, KNOT_ANSWER); + SOLVE_STEP(solve_answer, state, NULL); + if (with_dnssec) { + SOLVE_STEP(solve_answer_dnssec, state, NULL); + } + if (plan != NULL) { + WALK_LIST(step, plan->stage[KNOTD_STAGE_ANSWER]) { + SOLVE_STEP(step->process, state, step->ctx); + } + } + + /* Resolve AUTHORITY. */ + knot_pkt_begin(pkt, KNOT_AUTHORITY); + SOLVE_STEP(solve_authority, state, NULL); + if (with_dnssec) { + SOLVE_STEP(solve_authority_dnssec, state, NULL); + } + if (plan != NULL) { + WALK_LIST(step, plan->stage[KNOTD_STAGE_AUTHORITY]) { + SOLVE_STEP(step->process, state, step->ctx); + } + } + + /* Resolve ADDITIONAL. */ + knot_pkt_begin(pkt, KNOT_ADDITIONAL); + SOLVE_STEP(solve_additional, state, NULL); + if (with_dnssec) { + SOLVE_STEP(solve_additional_dnssec, state, NULL); + } + if (plan != NULL) { + WALK_LIST(step, plan->stage[KNOTD_STAGE_ADDITIONAL]) { + SOLVE_STEP(step->process, state, step->ctx); + } + } + + /* Write resulting RCODE. */ + knot_wire_set_rcode(pkt->wire, qdata->rcode); + + return KNOT_STATE_DONE; +} + +int internet_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt == NULL || qdata == NULL) { + return KNOT_STATE_FAIL; + } + + /* Check if valid zone. */ + NS_NEED_ZONE(qdata, KNOT_RCODE_REFUSED); + + /* Check if a TSIG is present. */ + if (knot_pkt_has_tsig(qdata->query)) { + NS_NEED_AUTH(qdata, ACL_ACTION_QUERY); + + /* Reserve space for TSIG. */ + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + } + + /* Check if the zone is not empty or expired. */ + NS_NEED_ZONE_CONTENTS(qdata); + + /* Get answer to QNAME. */ + qdata->name = knot_pkt_qname(qdata->query); + + return answer_query(pkt, qdata); +} diff --git a/src/knot/nameserver/internet.h b/src/knot/nameserver/internet.h new file mode 100644 index 0000000..52afe62 --- /dev/null +++ b/src/knot/nameserver/internet.h @@ -0,0 +1,79 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" +#include "knot/include/module.h" +#include "knot/nameserver/process_query.h" + +/*! \brief Don't follow CNAME/DNAME chain beyond this depth. */ +#define CNAME_CHAIN_MAX 5 + +/*! + * \brief Answer query from an IN class zone. + * + * \retval KNOT_STATE_FAIL if it encountered an error. + * \retval KNOT_STATE_DONE if finished. + */ +int internet_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! \brief Require given QUERY TYPE or return error code. */ +#define NS_NEED_QTYPE(qdata, qtype_want, error_rcode) \ + if (knot_pkt_qtype((qdata)->query) != (qtype_want)) { \ + qdata->rcode = (error_rcode); \ + return KNOT_STATE_FAIL; \ + } + +/*! \brief Require given QUERY NAME or return error code. */ +#define NS_NEED_QNAME(qdata, qname_want, error_rcode) \ + if (!knot_dname_is_equal(knot_pkt_qname((qdata)->query), (qname_want))) { \ + qdata->rcode = (error_rcode); \ + return KNOT_STATE_FAIL; \ + } + +/*! \brief Require existing zone or return failure. */ +#define NS_NEED_ZONE(qdata, error_rcode) \ + if ((qdata)->extra->zone == NULL) { \ + qdata->rcode = (error_rcode); \ + if ((error_rcode) == KNOT_RCODE_REFUSED) { \ + qdata->rcode_ede = KNOT_EDNS_EDE_NOTAUTH; \ + } \ + return KNOT_STATE_FAIL; \ + } + +/*! \brief Require existing zone contents or return failure. */ +#define NS_NEED_ZONE_CONTENTS(qdata) \ + if ((qdata)->extra->contents == NULL) { \ + qdata->rcode = KNOT_RCODE_SERVFAIL; \ + qdata->rcode_ede = KNOT_EDNS_EDE_INV_DATA; \ + return KNOT_STATE_FAIL; \ + } + +/*! \brief Require authentication. */ +#define NS_NEED_AUTH(qdata, action) \ + if (!process_query_acl_check(conf(), (action), (qdata)) || \ + process_query_verify(qdata) != KNOT_EOK) { \ + return KNOT_STATE_FAIL; \ + } + +/*! \brief Require the zone not to be frozen. */ +#define NS_NEED_NOT_FROZEN(qdata) \ + if ((qdata)->extra->zone->events.ufrozen) { \ + (qdata)->rcode = KNOT_RCODE_REFUSED; \ + (qdata)->rcode_ede = KNOT_EDNS_EDE_NOT_READY; \ + return KNOT_STATE_FAIL; \ + } diff --git a/src/knot/nameserver/ixfr.c b/src/knot/nameserver/ixfr.c new file mode 100644 index 0000000..03a9fdf --- /dev/null +++ b/src/knot/nameserver/ixfr.c @@ -0,0 +1,332 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <urcu.h> + +#include "contrib/mempattern.h" +#include "contrib/sockaddr.h" +#include "knot/journal/journal_metadata.h" +#include "knot/nameserver/axfr.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/ixfr.h" +#include "knot/nameserver/log.h" +#include "knot/nameserver/xfr.h" +#include "knot/zone/serial.h" +#include "libknot/libknot.h" + +#define ZONE_NAME(qdata) knot_pkt_qname((qdata)->query) +#define REMOTE(qdata) (struct sockaddr *)knotd_qdata_remote_addr(qdata) + +#define IXFROUT_LOG(priority, qdata, fmt...) \ + ns_log(priority, ZONE_NAME(qdata), LOG_OPERATION_IXFR, \ + LOG_DIRECTION_OUT, REMOTE(qdata), false, fmt) + +/*! \brief Helper macro for putting RRs into packet. */ +#define IXFR_SAFE_PUT(pkt, rr) \ + int ret = knot_pkt_put((pkt), 0, (rr), KNOT_PF_NOTRUNC | KNOT_PF_ORIGTTL); \ + if (ret != KNOT_EOK) { \ + return ret; \ + } + +/*! \brief Puts current RR into packet, stores state for retries. */ +static int ixfr_put_chg_part(knot_pkt_t *pkt, struct ixfr_proc *ixfr, + journal_read_t *read) +{ + assert(pkt); + assert(ixfr); + assert(read); + + if (!knot_rrset_empty(&ixfr->cur_rr)) { + IXFR_SAFE_PUT(pkt, &ixfr->cur_rr); + journal_read_clear_rrset(&ixfr->cur_rr); + } + + while (journal_read_rrset(read, &ixfr->cur_rr, true)) { + if (ixfr->cur_rr.type == KNOT_RRTYPE_SOA) { + ixfr->in_remove_section = !ixfr->in_remove_section; + + if (ixfr->in_remove_section) { + if (knot_soa_serial(ixfr->cur_rr.rrs.rdata) == ixfr->soa_to) { + break; + } + } else { + ixfr->soa_last = knot_soa_serial(ixfr->cur_rr.rrs.rdata); + } + } + + if (pkt->size > KNOT_WIRE_PTR_MAX) { + // optimization: once the XFR DNS message is > 16 KiB, compression + // is limited. Better wrap to next message. + return KNOT_ESPACE; + } + + IXFR_SAFE_PUT(pkt, &ixfr->cur_rr); + journal_read_clear_rrset(&ixfr->cur_rr); + } + + return journal_read_get_error(read, KNOT_EOK); +} + +/*! + * \brief Process the changes from journal. + * \note Keep in mind that this function must be able to resume processing, + * for example if it fills a packet and returns ESPACE, it is called again + * with next empty answer and it must resume the processing exactly where + * it's left off. + */ +static int ixfr_process_journal(knot_pkt_t *pkt, const void *item, + struct xfr_proc *xfer) +{ + int ret = KNOT_EOK; + struct ixfr_proc *ixfr = (struct ixfr_proc *)xfer; + journal_read_t *read = (journal_read_t *)item; + + ret = ixfr_put_chg_part(pkt, ixfr, read); + + return ret; +} + +#undef IXFR_SAFE_PUT + +static int ixfr_load_chsets(journal_read_t **journal_read, zone_t *zone, + const zone_contents_t *contents, const knot_rrset_t *their_soa) +{ + assert(journal_read); + assert(zone); + + /* Compare serials. */ + uint32_t serial_to = zone_contents_serial(contents), j_serial_to; + uint32_t serial_from = knot_soa_serial(their_soa->rrs.rdata); + if (serial_compare(serial_to, serial_from) & SERIAL_MASK_LEQ) { /* We have older/same age zone. */ + return KNOT_EUPTODATE; + } + + zone_journal_t j = zone_journal(zone); + bool j_exists = false; + int ret = journal_info(j, &j_exists, NULL, NULL, &j_serial_to, NULL, NULL, NULL, NULL); + if (ret != KNOT_EOK) { + return ret; + } else if (!j_exists) { + return KNOT_ENOENT; + } + + // please note that the journal serial_to might differ from zone SOA serial + // it is because RCU lock is made at different moment than LMDB txn begin + return journal_read_begin(zone_journal(zone), false, serial_from, journal_read); +} + +static int ixfr_query_check(knotd_qdata_t *qdata) +{ + NS_NEED_ZONE(qdata, KNOT_RCODE_NOTAUTH); + NS_NEED_AUTH(qdata, ACL_ACTION_TRANSFER); + NS_NEED_ZONE_CONTENTS(qdata); + + /* Need SOA authority record. */ + const knot_pktsection_t *authority = knot_pkt_section(qdata->query, KNOT_AUTHORITY); + const knot_rrset_t *their_soa = knot_pkt_rr(authority, 0); + if (authority->count < 1 || their_soa->type != KNOT_RRTYPE_SOA) { + qdata->rcode = KNOT_RCODE_FORMERR; + return KNOT_STATE_FAIL; + } + /* SOA needs to match QNAME. */ + NS_NEED_QNAME(qdata, their_soa->owner, KNOT_RCODE_FORMERR); + + return KNOT_STATE_DONE; +} + +static void ixfr_answer_cleanup(knotd_qdata_t *qdata) +{ + struct ixfr_proc *ixfr = (struct ixfr_proc *)qdata->extra->ext; + knot_mm_t *mm = qdata->mm; + + knot_rrset_clear(&ixfr->cur_rr, NULL); + ptrlist_free(&ixfr->proc.nodes, mm); + journal_read_end(ixfr->journal_ctx); + mm_free(mm, qdata->extra->ext); + + /* Allow zone changes (finished). */ + rcu_read_unlock(); +} + +static int ixfr_answer_init(knotd_qdata_t *qdata, uint32_t *serial_from) +{ + assert(qdata); + + if (ixfr_query_check(qdata) == KNOT_STATE_FAIL) { + if (qdata->rcode == KNOT_RCODE_FORMERR) { + return KNOT_EMALF; + } else { + return KNOT_EDENIED; + } + } + + if (zone_get_flag(qdata->extra->zone, ZONE_XFR_FROZEN, false)) { + qdata->rcode = KNOT_RCODE_REFUSED; + qdata->rcode_ede = KNOT_EDNS_EDE_NOT_READY; + return KNOT_EAGAIN; + } + + conf_val_t provide = conf_zone_get(conf(), C_PROVIDE_IXFR, + qdata->extra->zone->name); + if (!conf_bool(&provide)) { + return KNOT_ENOTSUP; + } + + const knot_pktsection_t *authority = knot_pkt_section(qdata->query, KNOT_AUTHORITY); + const knot_rrset_t *their_soa = knot_pkt_rr(authority, 0); + *serial_from = knot_soa_serial(their_soa->rrs.rdata); + + knot_mm_t *mm = qdata->mm; + struct ixfr_proc *xfer = mm_alloc(mm, sizeof(*xfer)); + if (xfer == NULL) { + return KNOT_ENOMEM; + } + memset(xfer, 0, sizeof(*xfer)); + + int ret = ixfr_load_chsets(&xfer->journal_ctx, (zone_t *)qdata->extra->zone, + qdata->extra->contents, their_soa); + if (ret != KNOT_EOK) { + mm_free(mm, xfer); + return ret; + } + + xfr_stats_begin(&xfer->proc.stats); + xfer->state = IXFR_SOA_DEL; + init_list(&xfer->proc.nodes); + knot_rrset_init_empty(&xfer->cur_rr); + xfer->qdata = qdata; + + ptrlist_add(&xfer->proc.nodes, xfer->journal_ctx, mm); + + xfer->soa_from = knot_soa_serial(their_soa->rrs.rdata); + xfer->soa_to = zone_contents_serial(qdata->extra->contents); + xfer->soa_last = xfer->soa_from; + + qdata->extra->ext = xfer; + qdata->extra->ext_cleanup = &ixfr_answer_cleanup; + + /* No zone changes during multipacket answer (unlocked in ixfr_answer_cleanup) */ + rcu_read_lock(); + + return KNOT_EOK; +} + +static int ixfr_answer_soa(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + assert(pkt); + assert(qdata); + + /* Check query. */ + int state = ixfr_query_check(qdata); + if (state == KNOT_STATE_FAIL) { + return state; /* Malformed query. */ + } + + /* Reserve space for TSIG. */ + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + /* Guaranteed to have zone contents. */ + const zone_node_t *apex = qdata->extra->contents->apex; + knot_rrset_t soa_rr = node_rrset(apex, KNOT_RRTYPE_SOA); + if (knot_rrset_empty(&soa_rr)) { + return KNOT_STATE_FAIL; + } + ret = knot_pkt_put(pkt, 0, &soa_rr, 0); + if (ret != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_SERVFAIL; + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_DONE; +} + +int ixfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt == NULL || qdata == NULL) { + return KNOT_STATE_FAIL; + } + + /* IXFR over UDP is responded with SOA. */ + if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) { + return ixfr_answer_soa(pkt, qdata); + } + + /* Initialize on first call. */ + struct ixfr_proc *ixfr = qdata->extra->ext; + if (ixfr == NULL) { + uint32_t soa_from = 0; + int ret = ixfr_answer_init(qdata, &soa_from); + ixfr = qdata->extra->ext; + switch (ret) { + case KNOT_EOK: /* OK */ + IXFROUT_LOG(LOG_INFO, qdata, "started, serial %u -> %u", + ixfr->soa_from, ixfr->soa_to); + break; + case KNOT_EUPTODATE: /* Our zone is same age/older, send SOA. */ + IXFROUT_LOG(LOG_INFO, qdata, "zone is up-to-date, serial %u", soa_from); + return ixfr_answer_soa(pkt, qdata); + case KNOT_ENOTSUP: + IXFROUT_LOG(LOG_INFO, qdata, "cannot provide, fallback to AXFR"); + qdata->type = KNOTD_QUERY_TYPE_AXFR; /* Solve as AXFR. */ + return axfr_process_query(pkt, qdata); + case KNOT_ERANGE: /* No history -> AXFR. */ + case KNOT_ENOENT: + IXFROUT_LOG(LOG_INFO, qdata, "incomplete history, serial %u, fallback to AXFR", soa_from); + qdata->type = KNOTD_QUERY_TYPE_AXFR; /* Solve as AXFR. */ + return axfr_process_query(pkt, qdata); + case KNOT_EDENIED: /* Not authorized, already logged. */ + return KNOT_STATE_FAIL; + case KNOT_EMALF: /* Malformed query. */ + IXFROUT_LOG(LOG_DEBUG, qdata, "malformed query"); + return KNOT_STATE_FAIL; + case KNOT_EAGAIN: /* Outgoing IXFR temporarily disabled. */ + IXFROUT_LOG(LOG_INFO, qdata, "outgoing IXFR frozen"); + return KNOT_STATE_FAIL; + default: /* Server errors. */ + IXFROUT_LOG(LOG_ERR, qdata, "failed to start (%s)", + knot_strerror(ret)); + return KNOT_STATE_FAIL; + } + } + + /* Reserve space for TSIG. */ + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + /* Answer current packet (or continue). */ + ret = xfr_process_list(pkt, &ixfr_process_journal, qdata); + switch (ret) { + case KNOT_ESPACE: /* Couldn't write more, send packet and continue. */ + return KNOT_STATE_PRODUCE; /* Check for more. */ + case KNOT_EOK: /* Last response. */ + if (ixfr->soa_last != ixfr->soa_to) { + IXFROUT_LOG(LOG_ERR, qdata, "failed (inconsistent history)"); + return KNOT_STATE_FAIL; + } + xfr_stats_end(&ixfr->proc.stats); + xfr_log_finished(ZONE_NAME(qdata), LOG_OPERATION_IXFR, LOG_DIRECTION_OUT, + REMOTE(qdata), false, &ixfr->proc.stats); + return KNOT_STATE_DONE; + default: /* Generic error. */ + IXFROUT_LOG(LOG_ERR, qdata, "failed (%s)", knot_strerror(ret)); + return KNOT_STATE_FAIL; + } +} diff --git a/src/knot/nameserver/ixfr.h b/src/knot/nameserver/ixfr.h new file mode 100644 index 0000000..3012be1 --- /dev/null +++ b/src/knot/nameserver/ixfr.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/journal/journal_read.h" +#include "knot/nameserver/process_query.h" +#include "knot/nameserver/xfr.h" +#include "libknot/packet/pkt.h" + +/*! \brief IXFR-in processing states. */ +enum ixfr_state { + IXFR_INVALID = 0, + IXFR_START, /* IXFR-in starting, expecting final SOA. */ + IXFR_SOA_DEL, /* Expecting starting SOA. */ + IXFR_SOA_ADD, /* Expecting ending SOA. */ + IXFR_DEL, /* Expecting RR to delete. */ + IXFR_ADD, /* Expecting RR to add. */ + IXFR_DONE /* Processing done, IXFR-in complete. */ +}; + +/*! \brief Extended structure for IXFR-in/IXFR-out processing. */ +struct ixfr_proc { + /* Processing state. */ + struct xfr_proc proc; + enum ixfr_state state; + bool in_remove_section; + + /* Changes to be sent. */ + journal_read_t *journal_ctx; + + /* Currently processed RRSet. */ + knot_rrset_t cur_rr; + + /* Processing context. */ + knotd_qdata_t *qdata; + knot_mm_t *mm; + uint32_t soa_from; + uint32_t soa_to; + uint32_t soa_last; +}; + +/*! + * \brief IXFR query processing module. + * + * \retval PRODUCE if it has an answer, but not yet finished. + * \retval FAIL if it encountered an error. + * \retval DONE if finished. + */ +int ixfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata); diff --git a/src/knot/nameserver/log.h b/src/knot/nameserver/log.h new file mode 100644 index 0000000..fc79bd3 --- /dev/null +++ b/src/knot/nameserver/log.h @@ -0,0 +1,88 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/sockaddr.h" +#include "knot/common/log.h" +#include "libknot/dname.h" + +typedef enum { + LOG_OPERATION_AXFR, + LOG_OPERATION_IXFR, + LOG_OPERATION_NOTIFY, + LOG_OPERATION_REFRESH, + LOG_OPERATION_UPDATE, + LOG_OPERATION_DS_CHECK, + LOG_OPERATION_DS_PUSH, +} log_operation_t; + +typedef enum { + LOG_DIRECTION_NONE, + LOG_DIRECTION_IN, + LOG_DIRECTION_OUT, +} log_direction_t; + +static inline const char *log_operation_name(log_operation_t operation) +{ + switch (operation) { + case LOG_OPERATION_AXFR: + return "AXFR"; + case LOG_OPERATION_IXFR: + return "IXFR"; + case LOG_OPERATION_NOTIFY: + return "notify"; + case LOG_OPERATION_REFRESH: + return "refresh"; + case LOG_OPERATION_UPDATE: + return "DDNS"; + case LOG_OPERATION_DS_CHECK: + return "DS check"; + case LOG_OPERATION_DS_PUSH: + return "DS push"; + default: + return "?"; + } +} + +static inline const char *log_direction_name(log_direction_t direction) +{ + switch (direction) { + case LOG_DIRECTION_IN: + return ", incoming"; + case LOG_DIRECTION_OUT: + return ", outgoing"; + case LOG_DIRECTION_NONE: + default: + return ""; + } +} + +/*! + * \brief Generate log message for server communication. + * + * Example output: + * + * [example.com] NOTIFY, outgoing, remote 2001:db8::1@53, serial 123 + */ +#define ns_log(priority, zone, op, dir, remote, pool, fmt, ...) \ + do { \ + char address[SOCKADDR_STRLEN] = ""; \ + sockaddr_tostr(address, sizeof(address), (const struct sockaddr_storage *)remote); \ + log_fmt_zone(priority, LOG_SOURCE_ZONE, zone, NULL, "%s%s, remote %s%s, " fmt, \ + log_operation_name(op), log_direction_name(dir), address, \ + (pool) ? " pool" : "", ## __VA_ARGS__); \ + } while (0) diff --git a/src/knot/nameserver/notify.c b/src/knot/nameserver/notify.c new file mode 100644 index 0000000..82fce70 --- /dev/null +++ b/src/knot/nameserver/notify.c @@ -0,0 +1,92 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/nameserver/notify.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/log.h" +#include "knot/nameserver/tsig_ctx.h" +#include "knot/zone/serial.h" +#include "libdnssec/random.h" +#include "libknot/libknot.h" + +#define NOTIFY_IN_LOG(priority, qdata, fmt...) \ + ns_log(priority, knot_pkt_qname(qdata->query), LOG_OPERATION_NOTIFY, \ + LOG_DIRECTION_IN, knotd_qdata_remote_addr(qdata), false, fmt) + +static int notify_check_query(knotd_qdata_t *qdata) +{ + NS_NEED_ZONE(qdata, KNOT_RCODE_NOTAUTH); + NS_NEED_AUTH(qdata, ACL_ACTION_NOTIFY); + /* RFC1996 requires SOA question. */ + NS_NEED_QTYPE(qdata, KNOT_RRTYPE_SOA, KNOT_RCODE_FORMERR); + + return KNOT_STATE_DONE; +} + +int notify_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt == NULL || qdata == NULL) { + return KNOT_STATE_FAIL; + } + + /* Validate notification query. */ + int state = notify_check_query(qdata); + if (state == KNOT_STATE_FAIL) { + switch (qdata->rcode) { + case KNOT_RCODE_NOTAUTH: /* Not authorized, already logged. */ + break; + default: /* Other errors. */ + NOTIFY_IN_LOG(LOG_DEBUG, qdata, "invalid query"); + break; + } + return state; + } + + /* Reserve space for TSIG. */ + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + /* SOA RR in answer may be included, recover serial. */ + zone_t *zone = (zone_t *)qdata->extra->zone; + const knot_pktsection_t *answer = knot_pkt_section(qdata->query, KNOT_ANSWER); + if (answer->count > 0) { + const knot_rrset_t *soa = knot_pkt_rr(answer, 0); + if (soa->type == KNOT_RRTYPE_SOA) { + uint32_t zone_serial, serial = knot_soa_serial(soa->rrs.rdata); + NOTIFY_IN_LOG(LOG_INFO, qdata, "serial %u", serial); + if (zone->contents != NULL && + slave_zone_serial(zone, conf(), &zone_serial) == KNOT_EOK && + serial_equal(serial, zone_serial)) { + // NOTIFY serial == zone serial => ignore, keep timers + return KNOT_STATE_DONE; + } + } else { /* Complain, but accept N/A record. */ + NOTIFY_IN_LOG(LOG_NOTICE, qdata, "bad record in answer section"); + } + } else { + NOTIFY_IN_LOG(LOG_INFO, qdata, "serial none"); + } + + /* Incoming NOTIFY expires REFRESH timer and renews EXPIRE timer. */ + zone_set_preferred_master(zone, knotd_qdata_remote_addr(qdata)); + zone_events_schedule_now(zone, ZONE_EVENT_REFRESH); + + return KNOT_STATE_DONE; +} diff --git a/src/knot/nameserver/notify.h b/src/knot/nameserver/notify.h new file mode 100644 index 0000000..d0bff14 --- /dev/null +++ b/src/knot/nameserver/notify.h @@ -0,0 +1,28 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" +#include "knot/nameserver/process_query.h" + +/*! + * \brief Answer IN class zone NOTIFY message (RFC1996). + * + * \retval FAIL if it encountered an error. + * \retval DONE if finished. + */ +int notify_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata); diff --git a/src/knot/nameserver/nsec_proofs.c b/src/knot/nameserver/nsec_proofs.c new file mode 100644 index 0000000..71944b1 --- /dev/null +++ b/src/knot/nameserver/nsec_proofs.c @@ -0,0 +1,677 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "libknot/libknot.h" +#include "knot/nameserver/nsec_proofs.h" +#include "knot/nameserver/internet.h" +#include "knot/dnssec/zone-nsec.h" + +/*! + * \brief Check if node is empty non-terminal. + */ +static bool empty_nonterminal(const zone_node_t *node) +{ + return node && node->rrset_count == 0; +} + +/*! + * \brief Check if wildcard expansion happened for given node and QNAME. + */ +static bool wildcard_expanded(const zone_node_t *node, const knot_dname_t *qname) +{ + return !knot_dname_is_wildcard(qname) && knot_dname_is_wildcard(node->owner); +} + +/*! + * \brief Check if opt-out can take an effect. + */ +static bool ds_optout(const zone_node_t *node) +{ + return node_nsec3_get(node) == NULL && !(node->flags & NODE_FLAGS_SUBTREE_AUTH); +} + +/*! + * \brief Check if node is part of the NSEC chain. + * + * NSEC is created for each node with authoritative data or delegation. + * + * \see https://tools.ietf.org/html/rfc4035#section-2.3 + */ +static bool node_in_nsec(const zone_node_t *node) +{ + return (node->flags & NODE_FLAGS_NONAUTH) == 0 && !empty_nonterminal(node); +} + +/*! + * \brief Check if node is part of the NSEC3 chain. + * + * NSEC3 is created for each node with authoritative data, empty-non terminal, + * and delegation (unless opt-out is in effect). + * + * \see https://tools.ietf.org/html/rfc5155#section-7.1 + */ +static bool node_in_nsec3(const zone_node_t *node) +{ + return (node->flags & NODE_FLAGS_NONAUTH) == 0 && !ds_optout(node); +} + +/*! + * \brief Walk previous names until we reach a node in NSEC chain. + * + */ +static const zone_node_t *nsec_previous(const zone_node_t *previous) +{ + assert(previous); + + while (!node_in_nsec(previous)) { + previous = node_prev(previous); + assert(previous); + } + + return previous; +} + +/*! + * \brief Get closest provable encloser from closest matching parent node. + */ +static const zone_node_t *nsec3_encloser(const zone_node_t *closest) +{ + assert(closest); + + while (!node_in_nsec3(closest)) { + closest = node_parent(closest); + assert(closest); + } + + return closest; +} + +/*! + * \brief Create a 'next closer name' to the given domain name. + * + * Next closer is the name one label longer than the closest provable encloser + * of a name. + * + * \see https://tools.ietf.org/html/rfc5155#section-1.3 + * + * \param closest_encloser Closest provable encloser of \a name. + * \param name Domain name to create the 'next closer' name to. + * + * \return Next closer name, NULL on error. + */ +static const knot_dname_t *get_next_closer(const knot_dname_t *closest_encloser, + const knot_dname_t *name) +{ + // make name only one label longer than closest_encloser + size_t ce_labels = knot_dname_labels(closest_encloser, NULL); + size_t qname_labels = knot_dname_labels(name, NULL); + for (int i = 0; i < (qname_labels - ce_labels - 1); ++i) { + name = knot_wire_next_label(name, NULL); + } + + // the common labels should match + assert(knot_dname_is_equal(knot_wire_next_label(name, NULL), closest_encloser)); + + return name; +} + +/*! + * \brief Put NSEC/NSEC3 record with corresponding RRSIG into the response. + */ +static int put_nxt_from_node(const zone_node_t *node, + uint16_t type, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + assert(type == KNOT_RRTYPE_NSEC || type == KNOT_RRTYPE_NSEC3); + + knot_rrset_t rrset = node_rrset(node, type); + if (knot_rrset_empty(&rrset)) { + return KNOT_EOK; + } + + knot_rrset_t rrsigs = node_rrset(node, KNOT_RRTYPE_RRSIG); + + return process_query_put_rr(resp, qdata, &rrset, &rrsigs, + KNOT_COMPR_HINT_NONE, KNOT_PF_CHECKDUP); +} + +/*! + * \brief Put NSEC record with corresponding RRSIG into the response. + */ +static int put_nsec_from_node(const zone_node_t *node, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + return put_nxt_from_node(node, KNOT_RRTYPE_NSEC, qdata, resp); +} + +/*! + * \brief Put NSEC3 record with corresponding RRSIG into the response. + */ +static int put_nsec3_from_node(const zone_node_t *node, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + return put_nxt_from_node(node, KNOT_RRTYPE_NSEC3, qdata, resp); +} + +/*! + * \brief Find NSEC for given name and put it into the response. + * + * Note this function allows the name to match the QNAME. The NODATA proof + * for empty non-terminal is equivalent to NXDOMAIN proof, except that the + * names may exist. This is why. + */ +static int put_covering_nsec(const zone_contents_t *zone, + const knot_dname_t *name, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *match = NULL; + const zone_node_t *closest = NULL; + const zone_node_t *prev = NULL; + + const zone_node_t *proof = NULL; + + int ret = zone_contents_find_dname(zone, name, &match, &closest, &prev); + if (ret == ZONE_NAME_FOUND) { + proof = match; + } else if (ret == ZONE_NAME_NOT_FOUND) { + proof = nsec_previous(prev); + } else { + assert(ret < 0); + return ret; + } + + return put_nsec_from_node(proof, qdata, resp); +} + +/*! + * \brief Find NSEC3 covering the given name and put it into the response. + */ +static int put_covering_nsec3(const zone_contents_t *zone, + const knot_dname_t *name, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *prev = NULL; + const zone_node_t *node = NULL; + + int match = zone_contents_find_nsec3_for_name(zone, name, &node, &prev); + if (match < 0) { + // ignore if missing + return KNOT_EOK; + } + + if (match == ZONE_NAME_FOUND || prev == NULL){ + return KNOT_ERROR; + } + + return put_nsec3_from_node(prev, qdata, resp); +} + +/*! + * \brief Add NSEC3 covering the next closer name to closest encloser. + * + * \param cpe Closest provable encloser of \a qname. + * \param qname Source QNAME. + * \param zone Source zone. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec3_next_closer(const zone_node_t *cpe, + const knot_dname_t *qname, + const zone_contents_t *zone, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const knot_dname_t *next_closer = get_next_closer(cpe->owner, qname); + + return put_covering_nsec3(zone, next_closer, qdata, resp); +} + +/*! + * \brief Add NSEC3s for closest encloser proof. + * + * Adds up to two NSEC3 records. The first one proves that closest encloser + * of the queried name exists, the second one proves that the name bellow the + * encloser doesn't. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.1 + * + * \param qname Source QNAME. + * \param zone Source zone. + * \param cpe Closest provable encloser of \a qname. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_closest_encloser_proof(const knot_dname_t *qname, + const zone_contents_t *zone, + const zone_node_t *cpe, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + // An NSEC3 RR that matches the closest (provable) encloser. + + int ret = put_nsec3_from_node(node_nsec3_get(cpe), qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + + // An NSEC3 RR that covers the "next closer" name to the closest encloser. + + return put_nsec3_next_closer(cpe, qname, zone, qdata, resp); +} + +/*! + * \brief Put NSEC for wildcard answer into the response. + * + * Add NSEC record proving that no better match on QNAME exists. + * + * \see https://tools.ietf.org/html/rfc4035#section-3.1.3.3 + * + * \param previous Previous name for QNAME. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec_wildcard(const zone_node_t *previous, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + return put_nsec_from_node(previous, qdata, resp); +} + +/*! + * \brief Put NSEC3s for wildcard answer into the response. + * + * Add NSEC3 record proving that no better match on QNAME exists. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.6 + * + * \param wildcard Wildcard node that was used for expansion. + * \param qname Source QNAME. + * \param zone Source zone. + * \param qdata Query processing data. + * \param resp Response packet. + */ +static int put_nsec3_wildcard(const zone_node_t *wildcard, + const knot_dname_t *qname, + const zone_contents_t *zone, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *cpe = nsec3_encloser(node_parent(wildcard)); + + return put_nsec3_next_closer(cpe, qname, zone, qdata, resp); +} + +/*! + * \brief Put NSECs or NSEC3s for wildcard expansion in the response. + * + * \return KNOT_E* + */ +static int put_wildcard_answer(const zone_node_t *wildcard, + const zone_node_t *previous, + const zone_contents_t *zone, + const knot_dname_t *qname, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (!wildcard_expanded(wildcard, qname)) { + return KNOT_EOK; + } + + int ret = 0; + + if (knot_is_nsec3_enabled(zone)) { + ret = put_nsec3_wildcard(wildcard, qname, zone, qdata, resp); + } else { + previous = nsec_previous(previous); + ret = put_nsec_wildcard(previous, qdata, resp); + } + + return ret; +} + +/*! + * \brief Put NSECs for NXDOMAIN error into the response. + * + * Adds up to two NSEC records. We have to prove that the queried name doesn't + * exist and that no wildcard expansion is possible for that name. + * + * \see https://tools.ietf.org/html/rfc4035#section-3.1.3.2 + * + * \param zone Source zone. + * \param previous Previous node to QNAME. + * \param closest Closest matching parent of QNAME. + * \param qdata Query data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec_nxdomain(const zone_contents_t *zone, + const zone_node_t *previous, + const zone_node_t *closest, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + assert(previous); + assert(closest); + + // An NSEC RR proving that there is no exact match for <SNAME, SCLASS>. + + previous = nsec_previous(previous); + int ret = put_nsec_from_node(previous, qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + + // An NSEC RR proving that the zone contains no RRsets that would match + // <SNAME, SCLASS> via wildcard name expansion. + + // NOTE: closest may be empty non-terminal and thus not authoritative. + + size_t size = knot_dname_size(closest->owner); + if (size > KNOT_DNAME_MAXLEN - 2) { + return KNOT_EINVAL; + } + assert(size > 0); + uint8_t wildcard[2 + size]; + memcpy(wildcard, "\x01""*", 2); + memcpy(wildcard + 2, closest->owner, size); + + return put_covering_nsec(zone, wildcard, qdata, resp); +} + +/*! + * \brief Put NSEC3s for NXDOMAIN error into the response. + * + * Adds up to three NSEC3 records. We have to prove that some parent name + * exists (closest encloser proof) and that no wildcard expansion is possible + * bellow that closest encloser. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.2 + * + * \param qname Source QNAME. + * \param zone Source zone. + * \param closest Closest matching parent of \a qname. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \retval KNOT_E* + */ +static int put_nsec3_nxdomain(const knot_dname_t *qname, + const zone_contents_t *zone, + const zone_node_t *closest, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *cpe = nsec3_encloser(closest); + + // Closest encloser proof. + + int ret = put_closest_encloser_proof(qname, zone, cpe, qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + + // NSEC3 covering the (nonexistent) wildcard at the closest encloser. + + const zone_node_t *nsec3_wildcard_prev, *ignored; + if (cpe->nsec3_wildcard_name == NULL || + zone_contents_find_nsec3(zone, cpe->nsec3_wildcard_name, &ignored, &nsec3_wildcard_prev) == ZONE_NAME_FOUND) { + return KNOT_ERROR; + } + + return put_nsec3_from_node(nsec3_wildcard_prev, qdata, resp); +} + +/*! + * \brief Put NSECs or NSEC3s for the NXDOMAIN error into the response. + * + * \param zone Zone used for answering. + * \param previous Previous node to \a qname. + * \param closest Closest matching parent name for \a qname. + * \param qname Source QNAME. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nxdomain(const zone_contents_t *zone, + const zone_node_t *previous, + const zone_node_t *closest, + const knot_dname_t *qname, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (knot_is_nsec3_enabled(zone)) { + return put_nsec3_nxdomain(qname, zone, closest, qdata, resp); + } else { + return put_nsec_nxdomain(zone, previous, closest, qdata, resp); + } +} + +/*! + * \brief Put NSEC for NODATA error into the response. + * + * Then NSEC matching the QNAME must be added into the response and the bitmap + * will indicate that the QTYPE doesn't exist. As NSECs for empty non-terminals + * don't exist, the proof for NODATA match on non-terminal is proved like + * non-existence of the queried name. + * + * \see https://tools.ietf.org/html/rfc4035#section-3.1.3.1 + * + * \param match Node matching QNAME. + * \param previous Previous node to QNAME in the zone. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec_nodata(const zone_node_t *match, + const zone_node_t *previous, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (empty_nonterminal(match)) { + return put_nsec_from_node(nsec_previous(previous), qdata, resp); + } else { + return put_nsec_from_node(match, qdata, resp); + } +} + +/*! + * \brief Put NSEC3 for NODATA error into the response. + * + * The NSEC3 matching the QNAME is added into the response and the bitmap + * will indicate that the QTYPE doesn't exist. For QTYPE==DS, the server + * may alternatively serve a closest encloser proof with opt-out. For wildcard + * expansion, the closest encloser proof must included as well. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.3 + * \see https://tools.ietf.org/html/rfc5155#section-7.2.4 + * \see https://tools.ietf.org/html/rfc5155#section-7.2.5 + */ +static int put_nsec3_nodata(const knot_dname_t *qname, + const zone_contents_t *zone, + const zone_node_t *match, + const zone_node_t *closest, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + int ret = KNOT_EOK; + + // NSEC3 matching QNAME is always included. + + zone_node_t *nsec3_match = node_nsec3_get(match); + if (nsec3_match != NULL) { + ret = put_nsec3_from_node(nsec3_match, qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + } + + // Closest encloser proof for wildcard effect or NSEC3 opt-out. + + if (wildcard_expanded(match, qname) || ds_optout(match)) { + const zone_node_t *cpe = nsec3_encloser(closest); + ret = put_closest_encloser_proof(qname, zone, cpe, qdata, resp); + } + + return ret; +} + +/*! + * \brief Put NSECs or NSEC3s for the NODATA error into the response. + * + * \param node Source node. + * \param qdata Query processing data. + * \param resp Response packet. + */ +static int put_nodata(const zone_node_t *node, + const zone_node_t *closest, + const zone_node_t *previous, + const zone_contents_t *zone, + const knot_dname_t *qname, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (knot_is_nsec3_enabled(zone)) { + return put_nsec3_nodata(qname, zone, node, closest, qdata, resp); + } else { + return put_nsec_nodata(node, previous, qdata, resp); + } +} + +int nsec_prove_wildcards(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->contents == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + struct wildcard_hit *item; + + WALK_LIST(item, qdata->extra->wildcards) { + if (item->node == NULL) { + return KNOT_EINVAL; + } + ret = put_wildcard_answer(item->node, item->prev, + qdata->extra->contents, + item->sname, qdata, pkt); + if (ret != KNOT_EOK) { + break; + } + } + + return ret; +} + +int nsec_prove_nodata(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->contents == NULL || qdata->extra->node == NULL) { + return KNOT_EINVAL; + } + + return put_nodata(qdata->extra->node, qdata->extra->encloser, qdata->extra->previous, + qdata->extra->contents, qdata->name, qdata, pkt); +} + +int nsec_prove_nxdomain(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->contents == NULL) { + return KNOT_EINVAL; + } + + return put_nxdomain(qdata->extra->contents, + qdata->extra->previous, qdata->extra->encloser, + qdata->name, qdata, pkt); +} + +int nsec_prove_dp_security(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->node == NULL || qdata->extra->encloser == NULL || + qdata->extra->contents == NULL) { + return KNOT_EINVAL; + } + + // Add DS into the response. + + knot_rrset_t rrset = node_rrset(qdata->extra->node, KNOT_RRTYPE_DS); + if (!knot_rrset_empty(&rrset)) { + knot_rrset_t rrsigs = node_rrset(qdata->extra->node, KNOT_RRTYPE_RRSIG); + return process_query_put_rr(pkt, qdata, &rrset, &rrsigs, + KNOT_COMPR_HINT_NONE, 0); + } + + // Alternatively prove that DS doesn't exist. + + return put_nodata(qdata->extra->node, qdata->extra->encloser, qdata->extra->previous, + qdata->extra->contents, qdata->name, qdata, pkt); +} + +int nsec_append_rrsigs(knot_pkt_t *pkt, knotd_qdata_t *qdata, bool optional) +{ + int ret = KNOT_EOK; + uint16_t flags = optional ? KNOT_PF_NOTRUNC : KNOT_PF_NULL; + flags |= KNOT_PF_FREE; // Free all RRSIGs, they are synthesized + flags |= KNOT_PF_ORIGTTL; + + /* Append RRSIGs for section. */ + struct rrsig_info *info; + WALK_LIST(info, qdata->extra->rrsigs) { + knot_rrset_t *rrsig = &info->synth_rrsig; + uint16_t compr_hint = info->rrinfo->compress_ptr[KNOT_COMPR_HINT_OWNER]; + uint16_t flags_mask = (info->rrinfo->flags & KNOT_PF_SOAMINTTL) ? KNOT_PF_ORIGTTL : 0; + ret = knot_pkt_put(pkt, compr_hint, rrsig, flags & ~flags_mask); + if (ret != KNOT_EOK) { + break; + } + /* RRSIG is owned by packet now. */ + knot_rdataset_init(&info->synth_rrsig.rrs); + }; + + /* Clear the list. */ + nsec_clear_rrsigs(qdata); + + return ret; +} + +void nsec_clear_rrsigs(knotd_qdata_t *qdata) +{ + if (qdata == NULL) { + return; + } + + struct rrsig_info *info; + WALK_LIST(info, qdata->extra->rrsigs) { + knot_rrset_t *rrsig = &info->synth_rrsig; + knot_rrset_clear(rrsig, qdata->mm); + }; + + ptrlist_free(&qdata->extra->rrsigs, qdata->mm); + init_list(&qdata->extra->rrsigs); +} diff --git a/src/knot/nameserver/nsec_proofs.h b/src/knot/nameserver/nsec_proofs.h new file mode 100644 index 0000000..09d5f2a --- /dev/null +++ b/src/knot/nameserver/nsec_proofs.h @@ -0,0 +1,38 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" +#include "knot/nameserver/process_query.h" + +/*! \brief Prove wildcards visited during answer resolution. */ +int nsec_prove_wildcards(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! \brief Prove answer leading to non-existent name. */ +int nsec_prove_nxdomain(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! \brief Prove empty answer. */ +int nsec_prove_nodata(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! \brief Prove delegation point security. */ +int nsec_prove_dp_security(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! \brief Append missing RRSIGs for current processing section. */ +int nsec_append_rrsigs(knot_pkt_t *pkt, knotd_qdata_t *qdata, bool optional); + +/*! \brief Clear RRSIG list. */ +void nsec_clear_rrsigs(knotd_qdata_t *qdata); diff --git a/src/knot/nameserver/process_query.c b/src/knot/nameserver/process_query.c new file mode 100644 index 0000000..34590df --- /dev/null +++ b/src/knot/nameserver/process_query.c @@ -0,0 +1,978 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <urcu.h> + +#include "libdnssec/tsig.h" +#include "knot/common/log.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/nameserver/process_query.h" +#include "knot/nameserver/query_module.h" +#include "knot/nameserver/chaos.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/axfr.h" +#include "knot/nameserver/ixfr.h" +#include "knot/nameserver/update.h" +#include "knot/nameserver/nsec_proofs.h" +#include "knot/nameserver/notify.h" +#include "knot/server/server.h" +#include "libknot/libknot.h" +#include "contrib/macros.h" +#include "contrib/mempattern.h" + +/*! \brief Accessor to query-specific data. */ +#define QUERY_DATA(ctx) ((knotd_qdata_t *)(ctx)->data) + +static knotd_query_type_t query_type(const knot_pkt_t *pkt) +{ + switch (knot_wire_get_opcode(pkt->wire)) { + case KNOT_OPCODE_QUERY: + switch (knot_pkt_qtype(pkt)) { + case 0 /* RESERVED */: return KNOTD_QUERY_TYPE_INVALID; + case KNOT_RRTYPE_AXFR: return KNOTD_QUERY_TYPE_AXFR; + case KNOT_RRTYPE_IXFR: return KNOTD_QUERY_TYPE_IXFR; + default: return KNOTD_QUERY_TYPE_NORMAL; + } + case KNOT_OPCODE_NOTIFY: return KNOTD_QUERY_TYPE_NOTIFY; + case KNOT_OPCODE_UPDATE: return KNOTD_QUERY_TYPE_UPDATE; + default: return KNOTD_QUERY_TYPE_INVALID; + } +} + +/*! \brief Reinitialize query data structure. */ +static void query_data_init(knot_layer_t *ctx, knotd_qdata_params_t *params, + knotd_qdata_extra_t *extra) +{ + /* Initialize persistent data. */ + knotd_qdata_t *data = QUERY_DATA(ctx); + memset(data, 0, sizeof(*data)); + data->mm = ctx->mm; + data->params = params; + data->extra = extra; + data->rcode_ede = KNOT_EDNS_EDE_NONE; + + /* Initialize lists. */ + memset(extra, 0, sizeof(*extra)); + init_list(&extra->wildcards); + init_list(&extra->rrsigs); +} + +static int process_query_begin(knot_layer_t *ctx, void *params) +{ + /* Initialize context. */ + assert(ctx); + ctx->data = mm_alloc(ctx->mm, sizeof(knotd_qdata_t)); + knotd_qdata_extra_t *extra = mm_alloc(ctx->mm, sizeof(*extra)); + + /* Initialize persistent data. */ + query_data_init(ctx, params, extra); + + /* Await packet. */ + return KNOT_STATE_CONSUME; +} + +static int process_query_reset(knot_layer_t *ctx) +{ + assert(ctx); + knotd_qdata_t *qdata = QUERY_DATA(ctx); + + /* Remember persistent parameters. */ + knotd_qdata_params_t *params = qdata->params; + knotd_qdata_extra_t *extra = qdata->extra; + + /* Free allocated data. */ + knot_rrset_clear(&qdata->opt_rr, qdata->mm); + ptrlist_free(&extra->wildcards, qdata->mm); + nsec_clear_rrsigs(qdata); + if (extra->ext_cleanup != NULL) { + extra->ext_cleanup(qdata); + } + + /* Initialize persistent data. */ + query_data_init(ctx, params, extra); + + /* Await packet. */ + return KNOT_STATE_CONSUME; +} + +static int process_query_finish(knot_layer_t *ctx) +{ + process_query_reset(ctx); + mm_free(ctx->mm, ctx->data); + ctx->data = NULL; + + return KNOT_STATE_NOOP; +} + +static int process_query_in(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + assert(pkt && ctx); + knotd_qdata_t *qdata = QUERY_DATA(ctx); + + /* Check if at least header is parsed. */ + if (pkt->parsed < KNOT_WIRE_HEADER_SIZE) { + return KNOT_STATE_NOOP; /* Ignore. */ + } + + /* Accept only queries. */ + if (knot_wire_get_qr(pkt->wire)) { + return KNOT_STATE_NOOP; /* Ignore. */ + } + + /* Store for processing. */ + qdata->query = pkt; + qdata->type = query_type(pkt); + + /* Declare having response. */ + return KNOT_STATE_PRODUCE; +} + +/*! + * \brief Create a response for a given query in the INTERNET class. + */ +static int query_internet(knot_pkt_t *pkt, knot_layer_t *ctx) +{ + knotd_qdata_t *data = QUERY_DATA(ctx); + + switch (data->type) { + case KNOTD_QUERY_TYPE_NORMAL: return internet_process_query(pkt, data); + case KNOTD_QUERY_TYPE_NOTIFY: return notify_process_query(pkt, data); + case KNOTD_QUERY_TYPE_AXFR: return axfr_process_query(pkt, data); + case KNOTD_QUERY_TYPE_IXFR: return ixfr_process_query(pkt, data); + case KNOTD_QUERY_TYPE_UPDATE: return update_process_query(pkt, data); + default: + /* Nothing else is supported. */ + data->rcode = KNOT_RCODE_NOTIMPL; + return KNOT_STATE_FAIL; + } +} + +/*! + * \brief Create a response for a given query in the CHAOS class. + */ +static int query_chaos(knot_pkt_t *pkt, knot_layer_t *ctx) +{ + knotd_qdata_t *data = QUERY_DATA(ctx); + + /* Nothing except normal queries is supported. */ + if (data->type != KNOTD_QUERY_TYPE_NORMAL) { + data->rcode = KNOT_RCODE_NOTIMPL; + return KNOT_STATE_FAIL; + } + + data->rcode = knot_chaos_answer(pkt); + if (data->rcode != KNOT_RCODE_NOERROR) { + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_DONE; +} + +/*! \brief Find zone for given question. */ +static zone_t *answer_zone_find(const knot_pkt_t *query, knot_zonedb_t *zonedb) +{ + uint16_t qtype = knot_pkt_qtype(query); + uint16_t qclass = knot_pkt_qclass(query); + const knot_dname_t *qname = knot_pkt_qname(query); + zone_t *zone = NULL; + + // search for zone only for IN and ANY classes + if (qclass != KNOT_CLASS_IN && qclass != KNOT_CLASS_ANY) { + return NULL; + } + + /* In case of DS query, we strip the leftmost label when searching for + * the zone (but use whole qname in search for the record), as the DS + * records are only present in a parent zone. + */ + if (qtype == KNOT_RRTYPE_DS) { + const knot_dname_t *parent = knot_wire_next_label(qname, NULL); + zone = knot_zonedb_find_suffix(zonedb, parent); + /* If zone does not exist, search for its parent zone, + this will later result to NODATA answer. */ + /*! \note This is not 100% right, it may lead to DS name for example + * when following a CNAME chain, that should also be answered + * from the parent zone (if it exists). + */ + } + + if (zone == NULL) { + if (query_type(query) == KNOTD_QUERY_TYPE_NORMAL) { + zone = knot_zonedb_find_suffix(zonedb, qname); + } else { + // Direct match required. + zone = knot_zonedb_find(zonedb, qname); + } + } + + return zone; +} + +static int answer_edns_reserve(knot_pkt_t *resp, knotd_qdata_t *qdata) +{ + if (knot_rrset_empty(&qdata->opt_rr)) { + return KNOT_EOK; + } + + /* Reserve size in the response. */ + return knot_pkt_reserve(resp, knot_edns_wire_size(&qdata->opt_rr)); +} + +static int answer_edns_init(const knot_pkt_t *query, knot_pkt_t *resp, + knotd_qdata_t *qdata) +{ + if (!knot_pkt_has_edns(query)) { + return KNOT_EOK; + } + + /* Initialize OPT record. */ + uint16_t max_payload; + switch (knotd_qdata_remote_addr(qdata)->ss_family) { + case AF_INET: + max_payload = conf()->cache.srv_udp_max_payload_ipv4; + break; + case AF_INET6: + max_payload = conf()->cache.srv_udp_max_payload_ipv6; + break; + case AF_UNIX: + max_payload = MIN(conf()->cache.srv_udp_max_payload_ipv4, + conf()->cache.srv_udp_max_payload_ipv6); + break; + default: + return KNOT_ERROR; + } + int ret = knot_edns_init(&qdata->opt_rr, max_payload, 0, + KNOT_EDNS_VERSION, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + + /* Check supported version. */ + if (knot_edns_get_version(query->opt_rr) != KNOT_EDNS_VERSION) { + qdata->rcode = KNOT_RCODE_BADVERS; + } + + /* Set DO bit if set (DNSSEC requested). */ + if (knot_pkt_has_dnssec(query)) { + knot_edns_set_do(&qdata->opt_rr); + } + + /* Append NSID if requested and available. */ + if (knot_pkt_edns_option(query, KNOT_EDNS_OPTION_NSID) != NULL) { + size_t nsid_len = conf()->cache.srv_nsid_len; + const uint8_t *nsid_data = conf()->cache.srv_nsid_data; + + if (nsid_len > 0) { + ret = knot_edns_add_option(&qdata->opt_rr, + KNOT_EDNS_OPTION_NSID, + nsid_len, nsid_data, + qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + /* Initialize EDNS Client Subnet if configured and present in query. */ + if (conf()->cache.srv_ecs) { + uint8_t *ecs_opt = knot_pkt_edns_option(query, KNOT_EDNS_OPTION_CLIENT_SUBNET); + if (ecs_opt != NULL) { + qdata->ecs = mm_alloc(qdata->mm, sizeof(knot_edns_client_subnet_t)); + if (qdata->ecs == NULL) { + return KNOT_ENOMEM; + } + const uint8_t *ecs_data = knot_edns_opt_get_data(ecs_opt); + uint16_t ecs_len = knot_edns_opt_get_length(ecs_opt); + ret = knot_edns_client_subnet_parse(qdata->ecs, ecs_data, ecs_len); + if (ret != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_FORMERR; + return ret; + } + qdata->ecs->scope_len = 0; + + /* Reserve space for the option in the answer. */ + ret = knot_edns_reserve_option(&qdata->opt_rr, KNOT_EDNS_OPTION_CLIENT_SUBNET, + ecs_len, NULL, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } else { + qdata->ecs = NULL; + } + + return answer_edns_reserve(resp, qdata); +} + +static int answer_edns_put(knot_pkt_t *resp, knotd_qdata_t *qdata) +{ + if (knot_rrset_empty(&qdata->opt_rr)) { + return KNOT_EOK; + } + + /* Add ECS if present. */ + int ret = KNOT_EOK; + if (qdata->ecs != NULL) { + uint8_t *ecs_opt = knot_edns_get_option(&qdata->opt_rr, KNOT_EDNS_OPTION_CLIENT_SUBNET, NULL); + if (ecs_opt != NULL) { + uint8_t *ecs_data = knot_edns_opt_get_data(ecs_opt); + uint16_t ecs_len = knot_edns_opt_get_length(ecs_opt); + ret = knot_edns_client_subnet_write(ecs_data, ecs_len, qdata->ecs); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + size_t opt_wire_size = knot_edns_wire_size(&qdata->opt_rr); + + /* Add EDE. Pragmatic: only if space in pkt. */ + if (qdata->rcode_ede != KNOT_EDNS_EDE_NONE && + knot_pkt_reserve(resp, KNOT_EDNS_EDE_MIN_LENGTH) == KNOT_EOK) { + ret = knot_pkt_reclaim(resp, KNOT_EDNS_EDE_MIN_LENGTH); + assert(ret == KNOT_EOK); + + uint16_t ede_code = (uint16_t)qdata->rcode_ede; + assert((int)ede_code == qdata->rcode_ede); + ede_code = htobe16(ede_code); + + ret = knot_edns_add_option(&qdata->opt_rr, KNOT_EDNS_OPTION_EDE, + sizeof(ede_code), (uint8_t *)&ede_code, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + + /* Add EXPIRE if space and not catalog zone, which cannot expire. */ + if (knot_pkt_edns_option(qdata->query, KNOT_EDNS_OPTION_EXPIRE) != NULL && + qdata->extra->contents != NULL && !qdata->extra->zone->is_catalog_flag) { + int64_t timer = qdata->extra->zone->timers.next_expire == 0 + ? zone_soa_expire(qdata->extra->zone) + : qdata->extra->zone->timers.next_expire - time(NULL); + timer = MAX(timer, 0); + uint32_t timer_be; + knot_wire_write_u32((uint8_t *)&timer_be, (uint32_t)timer); + + uint16_t expire_size = KNOT_EDNS_OPTION_HDRLEN + sizeof(timer_be); + if (knot_pkt_reserve(resp, expire_size) == KNOT_EOK) { + ret = knot_pkt_reclaim(resp, expire_size); + assert(ret == KNOT_EOK); + + ret = knot_edns_add_option(&qdata->opt_rr, KNOT_EDNS_OPTION_EXPIRE, + sizeof(timer_be), (uint8_t *)&timer_be, + qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + /* Align the response if QUIC with EDNS. */ + if (qdata->params->proto == KNOTD_QUERY_PROTO_QUIC) { + int pad_len = knot_pkt_default_padding_size(resp, &qdata->opt_rr); + if (pad_len > -1) { + ret = knot_edns_reserve_option(&qdata->opt_rr, KNOT_EDNS_OPTION_PADDING, + pad_len, NULL, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + /* Reclaim reserved size. */ + ret = knot_pkt_reclaim(resp, opt_wire_size); + if (ret != KNOT_EOK) { + return ret; + } + + uint8_t *wire_end = resp->wire + resp->size; + + /* Write to packet. */ + assert(resp->current == KNOT_ADDITIONAL); + ret = knot_pkt_put(resp, KNOT_COMPR_HINT_NONE, &qdata->opt_rr, 0); + if (ret == KNOT_EOK) { + /* Save position of the OPT RR. */ + qdata->extra->opt_rr_pos = wire_end; + } + + return ret; +} + +/*! \brief Initialize response, sizes and find zone from which we're going to answer. */ +static int prepare_answer(knot_pkt_t *query, knot_pkt_t *resp, knot_layer_t *ctx) +{ + knotd_qdata_t *qdata = QUERY_DATA(ctx); + server_t *server = qdata->params->server; + + /* Initialize response. */ + int ret = knot_pkt_init_response(resp, query); + if (ret != KNOT_EOK) { + return ret; + } + knot_wire_clear_cd(resp->wire); + + /* Setup EDNS. */ + ret = answer_edns_init(query, resp, qdata); + if (ret != KNOT_EOK || qdata->rcode != 0) { + return KNOT_ERROR; + } + + /* Update maximal answer size. */ + if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) { + resp->max_size = KNOT_WIRE_MIN_PKTSIZE; + if (knot_pkt_has_edns(query)) { + uint16_t server_size; + switch (knotd_qdata_remote_addr(qdata)->ss_family) { + case AF_INET: + server_size = conf()->cache.srv_udp_max_payload_ipv4; + break; + case AF_INET6: + server_size = conf()->cache.srv_udp_max_payload_ipv6; + break; + default: + return KNOT_ERROR; + } + uint16_t client_size = knot_edns_get_payload(query->opt_rr); + uint16_t transfer = MIN(client_size, server_size); + resp->max_size = MAX(resp->max_size, transfer); + } + } else { + resp->max_size = KNOT_WIRE_MAX_PKTSIZE; + } + + /* All supported OPCODEs require a question. */ + const knot_dname_t *qname = knot_pkt_qname(query); + if (qname == NULL) { + switch (knot_wire_get_opcode(query->wire)) { + case KNOT_OPCODE_QUERY: + case KNOT_OPCODE_NOTIFY: + case KNOT_OPCODE_UPDATE: + qdata->rcode = KNOT_RCODE_FORMERR; + break; + default: + qdata->rcode = KNOT_RCODE_NOTIMPL; + } + return KNOT_ENOTSUP; + } + + /* Find zone for QNAME. */ + qdata->extra->zone = answer_zone_find(query, server->zone_db); + if (qdata->extra->zone != NULL && qdata->extra->contents == NULL) { + qdata->extra->contents = qdata->extra->zone->contents; + } + + /* Allow normal queries to catalog only if not UDP and if allowed by ACL. */ + if (qdata->extra->zone != NULL && qdata->extra->zone->is_catalog_flag && + query_type(query) == KNOTD_QUERY_TYPE_NORMAL) { + if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP || + !process_query_acl_check(conf(), ACL_ACTION_TRANSFER, qdata)) { + qdata->extra->zone = NULL; + qdata->extra->contents = NULL; + } + } + + return KNOT_EOK; +} + +static void set_rcode_to_packet(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + uint8_t ext_rcode = KNOT_EDNS_RCODE_HI(qdata->rcode); + + if (ext_rcode != 0) { + /* No OPT RR and Ext RCODE results in SERVFAIL. */ + if (qdata->extra->opt_rr_pos == NULL) { + knot_wire_set_rcode(pkt->wire, KNOT_RCODE_SERVFAIL); + return; + } + + knot_edns_set_ext_rcode_wire(qdata->extra->opt_rr_pos, ext_rcode); + } + + knot_wire_set_rcode(pkt->wire, KNOT_EDNS_RCODE_LO(qdata->rcode)); +} + +static int process_query_err(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + assert(ctx && pkt); + + knotd_qdata_t *qdata = QUERY_DATA(ctx); + + /* Initialize response from query packet. */ + knot_pkt_t *query = qdata->query; + (void)knot_pkt_init_response(pkt, query); + knot_wire_clear_cd(pkt->wire); + + /* Set TC bit if required. */ + if (qdata->err_truncated) { + knot_wire_set_aa(pkt->wire); + knot_wire_set_tc(pkt->wire); + } + + /* Move to Additionals to add OPT and TSIG. */ + if (pkt->current != KNOT_ADDITIONAL) { + (void)knot_pkt_begin(pkt, KNOT_ADDITIONAL); + } + + /* Put OPT RR to the additional section. */ + if (answer_edns_reserve(pkt, qdata) != KNOT_EOK || + answer_edns_put(pkt, qdata) != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_FORMERR; + } + + /* Set final RCODE to packet. */ + if (qdata->rcode == KNOT_RCODE_NOERROR && !qdata->err_truncated) { + /* Default RCODE is SERVFAIL if not otherwise specified. */ + qdata->rcode = KNOT_RCODE_SERVFAIL; + } + set_rcode_to_packet(pkt, qdata); + + /* Transaction security (if applicable). */ + if (process_query_sign_response(pkt, qdata) != KNOT_EOK) { + set_rcode_to_packet(pkt, qdata); + } + + return KNOT_STATE_DONE; +} + +#define PROCESS_BEGIN(plan, step, next_state, qdata) \ + if (plan != NULL) { \ + WALK_LIST(step, plan->stage[KNOTD_STAGE_BEGIN]) { \ + next_state = step->process(next_state, pkt, qdata, step->ctx); \ + if (next_state == KNOT_STATE_FAIL) { \ + goto finish; \ + } \ + } \ + } + +#define PROCESS_END(plan, step, next_state, qdata) \ + if (plan != NULL) { \ + WALK_LIST(step, plan->stage[KNOTD_STAGE_END]) { \ + next_state = step->process(next_state, pkt, qdata, step->ctx); \ + if (next_state == KNOT_STATE_FAIL) { \ + next_state = process_query_err(ctx, pkt); \ + } \ + } \ + } + +static int process_query_out(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + assert(pkt && ctx); + + rcu_read_lock(); + + knotd_qdata_t *qdata = QUERY_DATA(ctx); + struct query_plan *plan = conf()->query_plan; + struct query_plan *zone_plan = NULL; + struct query_step *step; + + int next_state = KNOT_STATE_PRODUCE; + + /* Check parse state. */ + knot_pkt_t *query = qdata->query; + if (query->parsed < query->size) { + qdata->rcode = KNOT_RCODE_FORMERR; + next_state = KNOT_STATE_FAIL; + goto finish; + } + + /* Preprocessing. */ + if (prepare_answer(query, pkt, ctx) != KNOT_EOK) { + next_state = KNOT_STATE_FAIL; + goto finish; + } + + if (qdata->extra->zone != NULL && qdata->extra->zone->query_plan != NULL) { + zone_plan = qdata->extra->zone->query_plan; + } + + /* Before query processing code. */ + PROCESS_BEGIN(plan, step, next_state, qdata); + PROCESS_BEGIN(zone_plan, step, next_state, qdata); + + /* Answer based on qclass. */ + if (next_state == KNOT_STATE_PRODUCE) { + switch (knot_pkt_qclass(pkt)) { + case KNOT_CLASS_CH: + next_state = query_chaos(pkt, ctx); + break; + case KNOT_CLASS_ANY: + case KNOT_CLASS_IN: + next_state = query_internet(pkt, ctx); + break; + default: + qdata->rcode = KNOT_RCODE_REFUSED; + next_state = KNOT_STATE_FAIL; + break; + } + } + + /* Postprocessing. */ + if (next_state == KNOT_STATE_DONE || next_state == KNOT_STATE_PRODUCE) { + /* Move to Additionals to add OPT and TSIG. */ + if (pkt->current != KNOT_ADDITIONAL) { + (void)knot_pkt_begin(pkt, KNOT_ADDITIONAL); + } + + /* Put OPT RR to the additional section. */ + if (answer_edns_put(pkt, qdata) != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_FORMERR; + next_state = KNOT_STATE_FAIL; + goto finish; + } + + /* Transaction security (if applicable). */ + if (process_query_sign_response(pkt, qdata) != KNOT_EOK) { + next_state = KNOT_STATE_FAIL; + goto finish; + } + } + +finish: + switch (next_state) { + case KNOT_STATE_NOOP: + break; + case KNOT_STATE_FAIL: + /* Error processing. */ + next_state = process_query_err(ctx, pkt); + break; + case KNOT_STATE_FINAL: + /* Just skipped postprocessing. */ + next_state = KNOT_STATE_DONE; + break; + default: + set_rcode_to_packet(pkt, qdata); + } + + /* After query processing code. */ + PROCESS_END(plan, step, next_state, qdata); + PROCESS_END(zone_plan, step, next_state, qdata); + + rcu_read_unlock(); + + return next_state; +} + +bool process_query_acl_check(conf_t *conf, acl_action_t action, + knotd_qdata_t *qdata) +{ + const knot_dname_t *zone_name = qdata->extra->zone->name; + knot_pkt_t *query = qdata->query; + const struct sockaddr_storage *query_source = knotd_qdata_remote_addr(qdata); + knot_tsig_key_t tsig = { 0 }; + + /* Skip if already checked and valid. */ + if (qdata->sign.tsig_key.name != NULL) { + return true; + } + + /* Authenticate with NOKEY if the packet isn't signed. */ + if (query->tsig_rr) { + tsig.name = query->tsig_rr->owner; + tsig.algorithm = knot_tsig_rdata_alg(query->tsig_rr); + } + + /* Log ACL details. */ + char addr_str[SOCKADDR_STRLEN]; + if (sockaddr_tostr(addr_str, sizeof(addr_str), query_source) <= 0) { + addr_str[0] = '\0'; + } + knot_dname_txt_storage_t key_name; + if (knot_dname_to_str(key_name, tsig.name, sizeof(key_name)) == NULL) { + key_name[0] = '\0'; + } + const knot_lookup_t *act = knot_lookup_by_id((knot_lookup_t *)acl_actions, action); + + bool automatic = false; + bool allowed = false; + + if (action != ACL_ACTION_UPDATE) { + // ACL_ACTION_QUERY is used for SOA/refresh query. + assert(action == ACL_ACTION_QUERY || action == ACL_ACTION_NOTIFY || + action == ACL_ACTION_TRANSFER); + const yp_name_t *item = (action == ACL_ACTION_NOTIFY) ? C_MASTER : C_NOTIFY; + conf_val_t rmts = conf_zone_get(conf, item, zone_name); + allowed = rmt_allowed(conf, &rmts, query_source, &tsig); + automatic = allowed; + } + if (!allowed) { + conf_val_t acl = conf_zone_get(conf, C_ACL, zone_name); + allowed = acl_allowed(conf, &acl, action, query_source, &tsig, zone_name, query); + } + + log_zone_debug(zone_name, + "ACL, %s, action %s, remote %s, key %s%s%s%s", + allowed ? "allowed" : "denied", + (act != NULL) ? act->name : "query", + addr_str, + (key_name[0] != '\0') ? "'" : "", + (key_name[0] != '\0') ? key_name : "none", + (key_name[0] != '\0') ? "'" : "", + automatic ? ", automatic" : ""); + + /* Check if authorized. */ + if (!allowed) { + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADKEY; + return false; + } + + /* Remember used TSIG key. */ + qdata->sign.tsig_key = tsig; + + return true; +} + +int process_query_verify(knotd_qdata_t *qdata) +{ + knot_pkt_t *query = qdata->query; + knot_sign_context_t *ctx = &qdata->sign; + + /* NOKEY => no verification. */ + if (query->tsig_rr == NULL) { + return KNOT_EOK; + } + + /* Keep digest for signing response. */ + /*! \note This memory will be rewritten for multi-pkt answers. */ + ctx->tsig_digest = (uint8_t *)knot_tsig_rdata_mac(query->tsig_rr); + ctx->tsig_digestlen = knot_tsig_rdata_mac_length(query->tsig_rr); + + /* Checking query. */ + int ret = knot_tsig_server_check(query->tsig_rr, query->wire, + query->size, &ctx->tsig_key); + + /* Evaluate TSIG check results. */ + switch(ret) { + case KNOT_EOK: + qdata->rcode = KNOT_RCODE_NOERROR; + break; + case KNOT_TSIG_EBADKEY: + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADKEY; + break; + case KNOT_TSIG_EBADSIG: + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADSIG; + break; + case KNOT_TSIG_EBADTIME: + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADTIME; + ctx->tsig_time_signed = knot_tsig_rdata_time_signed(query->tsig_rr); + break; + case KNOT_EMALF: + qdata->rcode = KNOT_RCODE_FORMERR; + break; + default: + qdata->rcode = KNOT_RCODE_SERVFAIL; + break; + } + + /* Log possible error. */ + if (qdata->rcode == KNOT_RCODE_SERVFAIL) { + log_zone_error(qdata->extra->zone->name, + "TSIG, verification failed (%s)", knot_strerror(ret)); + } else if (qdata->rcode != KNOT_RCODE_NOERROR) { + const knot_lookup_t *item = NULL; + if (qdata->rcode_tsig != KNOT_RCODE_NOERROR) { + item = knot_lookup_by_id(knot_tsig_rcode_names, qdata->rcode_tsig); + if (item == NULL) { + item = knot_lookup_by_id(knot_rcode_names, qdata->rcode_tsig); + } + } else { + item = knot_lookup_by_id(knot_rcode_names, qdata->rcode); + } + + char *key_name = knot_dname_to_str_alloc(ctx->tsig_key.name); + log_zone_debug(qdata->extra->zone->name, + "TSIG, key '%s', verification failed '%s'", + (key_name != NULL) ? key_name : "", + (item != NULL) ? item->name : ""); + free(key_name); + } + + return ret; +} + +int process_query_sign_response(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt->size == 0) { + // Nothing to sign. + return KNOT_EOK; + } + + int ret = KNOT_EOK; + knot_pkt_t *query = qdata->query; + knot_sign_context_t *ctx = &qdata->sign; + + /* KEY provided and verified TSIG or BADTIME allows signing. */ + if (ctx->tsig_key.name != NULL && knot_tsig_can_sign(qdata->rcode_tsig)) { + /* Sign query response. */ + size_t new_digest_len = dnssec_tsig_algorithm_size(ctx->tsig_key.algorithm); + if (ctx->pkt_count == 0) { + ret = knot_tsig_sign(pkt->wire, &pkt->size, pkt->max_size, + ctx->tsig_digest, ctx->tsig_digestlen, + ctx->tsig_digest, &new_digest_len, + &ctx->tsig_key, qdata->rcode_tsig, + ctx->tsig_time_signed); + } else { + ret = knot_tsig_sign_next(pkt->wire, &pkt->size, pkt->max_size, + ctx->tsig_digest, ctx->tsig_digestlen, + ctx->tsig_digest, &new_digest_len, + &ctx->tsig_key, + pkt->wire, pkt->size); + } + if (ret != KNOT_EOK) { + goto fail; /* Failed to sign. */ + } else { + ++ctx->pkt_count; + } + } else { + /* Copy TSIG from query and set RCODE. */ + if (query->tsig_rr && qdata->rcode_tsig != KNOT_RCODE_NOERROR) { + ret = knot_tsig_add(pkt->wire, &pkt->size, pkt->max_size, + qdata->rcode_tsig, query->tsig_rr); + if (ret != KNOT_EOK) { + goto fail; /* Whatever it is, it's server fail. */ + } + } + } + + return KNOT_EOK; + + /* Server failure in signing. */ +fail: + qdata->rcode = KNOT_RCODE_SERVFAIL; + qdata->rcode_tsig = KNOT_RCODE_NOERROR; /* Don't sign again. */ + return ret; +} + +/*! \brief Synthesize RRSIG for given parameters, store in 'qdata' for later use */ +static int put_rrsig(const knot_dname_t *sig_owner, uint16_t type, + const knot_rrset_t *rrsigs, knot_rrinfo_t *rrinfo, + uint32_t ttl_limit, knotd_qdata_t *qdata) +{ + knot_rdataset_t synth_rrs; + knot_rdataset_init(&synth_rrs); + assert(type != KNOT_RRTYPE_ANY); + int ret = knot_synth_rrsig(type, &rrsigs->rrs, &synth_rrs, qdata->mm); + if (ret == KNOT_ENOENT) { + // No signature + return KNOT_EOK; + } + if (ret != KNOT_EOK) { + return ret; + } + + /* Create rrsig info structure. */ + struct rrsig_info *info = mm_alloc(qdata->mm, sizeof(struct rrsig_info)); + if (info == NULL) { + knot_rdataset_clear(&synth_rrs, qdata->mm); + return KNOT_ENOMEM; + } + + /* Store RRSIG into info structure. */ + knot_dname_t *owner_copy = knot_dname_copy(sig_owner, qdata->mm); + if (owner_copy == NULL) { + mm_free(qdata->mm, info); + knot_rdataset_clear(&synth_rrs, qdata->mm); + return KNOT_ENOMEM; + } + uint32_t orig_ttl = knot_rrsig_original_ttl(synth_rrs.rdata); + knot_rrset_init(&info->synth_rrsig, owner_copy, rrsigs->type, + rrsigs->rclass, MIN(orig_ttl, ttl_limit)); + /* Store filtered signature. */ + info->synth_rrsig.rrs = synth_rrs; + + info->rrinfo = rrinfo; + add_tail(&qdata->extra->rrsigs, &info->n); + + return KNOT_EOK; +} + +int process_query_put_rr(knot_pkt_t *pkt, knotd_qdata_t *qdata, + const knot_rrset_t *rr, const knot_rrset_t *rrsigs, + uint16_t compr_hint, uint32_t flags) +{ + if (rr->rrs.count < 1) { + return KNOT_EMALF; + } + + /* Wildcard expansion applies only for answers. */ + bool expand = false; + if (pkt->current == KNOT_ANSWER) { + /* Expand if RR is wildcard. TRICK: if the asterix node is queried directly, we behave like if wildcard would be expanded. It's the same. */ + expand = knot_dname_is_wildcard(rr->owner); + } + + int ret = KNOT_EOK; + + /* If we already have compressed name on the wire and compression hint, + * we can just insert RRSet and fake synthesis by using compression + * hint. */ + knot_rrset_t to_add; + if (compr_hint == KNOT_COMPR_HINT_NONE && expand) { + knot_dname_t *qname_cpy = knot_dname_copy(qdata->name, &pkt->mm); + if (qname_cpy == NULL) { + return KNOT_ENOMEM; + } + knot_rrset_init(&to_add, qname_cpy, rr->type, rr->rclass, rr->ttl); + ret = knot_rdataset_copy(&to_add.rrs, &rr->rrs, &pkt->mm); + if (ret != KNOT_EOK) { + knot_dname_free(qname_cpy, &pkt->mm); + return ret; + } + to_add.additional = rr->additional; + flags |= KNOT_PF_FREE; + } else { + to_add = *rr; + } + + uint16_t rotate = conf()->cache.srv_ans_rotate ? knot_wire_get_id(qdata->query->wire) : 0; + uint16_t prev_count = pkt->rrset_count; + ret = knot_pkt_put_rotate(pkt, compr_hint, &to_add, rotate, flags); + if (ret != KNOT_EOK && (flags & KNOT_PF_FREE)) { + knot_rrset_clear(&to_add, &pkt->mm); + return ret; + } + + uint32_t rrsig_ttl_limit = UINT32_MAX; + if ((flags & KNOT_PF_SOAMINTTL) && to_add.type == KNOT_RRTYPE_SOA) { + rrsig_ttl_limit = knot_soa_minimum(to_add.rrs.rdata); + } + + const bool inserted = (prev_count != pkt->rrset_count); + if (inserted && + !knot_rrset_empty(rrsigs) && rr->type != KNOT_RRTYPE_RRSIG) { + // Get rrinfo of just inserted RR. + knot_rrinfo_t *rrinfo = &pkt->rr_info[pkt->rrset_count - 1]; + ret = put_rrsig(rr->owner, rr->type, rrsigs, rrinfo, rrsig_ttl_limit, qdata); + } + + return ret; +} + +/*! \brief Module implementation. */ +const knot_layer_api_t *process_query_layer(void) +{ + static const knot_layer_api_t api = { + .begin = &process_query_begin, + .reset = &process_query_reset, + .finish = &process_query_finish, + .consume = &process_query_in, + .produce = &process_query_out, + }; + return &api; +} diff --git a/src/knot/nameserver/process_query.h b/src/knot/nameserver/process_query.h new file mode 100644 index 0000000..bd7d42a --- /dev/null +++ b/src/knot/nameserver/process_query.h @@ -0,0 +1,107 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/include/module.h" +#include "knot/query/layer.h" +#include "knot/updates/acl.h" +#include "knot/zone/zone.h" + +/* Query processing module implementation. */ +const knot_layer_api_t *process_query_layer(void); + +/*! \brief Query processing intermediate data. */ +typedef struct knotd_qdata_extra { + zone_t *zone; /*!< Zone from which is answered. */ + const zone_contents_t *contents; /*!< Zone contents from which is answered. */ + list_t wildcards; /*!< Visited wildcards. */ + list_t rrsigs; /*!< Section RRSIGs. */ + uint8_t *opt_rr_pos; /*!< Place of the OPT RR in wire. */ + + /* Currently processed nodes. */ + const zone_node_t *node, *encloser, *previous; + + uint8_t cname_chain; /*!< Length of the CNAME chain so far. */ + + /* Extensions. */ + void *ext; + void (*ext_cleanup)(knotd_qdata_t *); /*!< Extensions cleanup callback. */ +} knotd_qdata_extra_t; + +/*! \brief Visited wildcard node list. */ +struct wildcard_hit { + node_t n; + const zone_node_t *node; /* Visited node. */ + const zone_node_t *prev; /* Previous node from the SNAME. */ + const knot_dname_t *sname; /* Name leading to this node. */ +}; + +/*! \brief RRSIG info node list. */ +struct rrsig_info { + node_t n; + knot_rrset_t synth_rrsig; /* Synthesized RRSIG. */ + knot_rrinfo_t *rrinfo; /* RR info. */ +}; + +/*! + * \brief Check current query against ACL. + * + * \param conf Configuration. + * \param action ACL action. + * \param qdata Query data. + * \return true if accepted, false if denied. + */ +bool process_query_acl_check(conf_t *conf, acl_action_t action, + knotd_qdata_t *qdata); + +/*! + * \brief Verify current query transaction security and update query data. + * + * \param qdata + * \retval KNOT_EOK + * \retval KNOT_TSIG_EBADKEY + * \retval KNOT_TSIG_EBADSIG + * \retval KNOT_TSIG_EBADTIME + * \retval (other generic errors) + */ +int process_query_verify(knotd_qdata_t *qdata); + +/*! + * \brief Sign current query using configured TSIG keys. + * + * \param pkt Outgoing message. + * \param qdata Query data. + * + * \retval KNOT_E* + */ +int process_query_sign_response(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! + * \brief Puts RRSet to packet, will store its RRSIG for later use. + * + * \param pkt Packet to store RRSet into. + * \param qdata Query data structure. + * \param rr RRSet to be stored. + * \param rrsigs RRSIGs to be stored. + * \param compr_hint Compression hint. + * \param flags Flags. + * + * \return KNOT_E* + */ +int process_query_put_rr(knot_pkt_t *pkt, knotd_qdata_t *qdata, + const knot_rrset_t *rr, const knot_rrset_t *rrsigs, + uint16_t compr_hint, uint32_t flags); diff --git a/src/knot/nameserver/query_module.c b/src/knot/nameserver/query_module.c new file mode 100644 index 0000000..2837135 --- /dev/null +++ b/src/knot/nameserver/query_module.c @@ -0,0 +1,791 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "contrib/sockaddr.h" +#include "libknot/attribute.h" +#include "libknot/probe/data.h" +#include "libknot/xdp.h" +#include "knot/common/log.h" +#include "knot/conf/module.h" +#include "knot/conf/tools.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-sign.h" +#include "knot/nameserver/query_module.h" +#include "knot/nameserver/process_query.h" + +#ifdef HAVE_ATOMIC + #define ATOMIC_ADD(dst, val) __atomic_add_fetch(&(dst), (val), __ATOMIC_RELAXED) + #define ATOMIC_SUB(dst, val) __atomic_sub_fetch(&(dst), (val), __ATOMIC_RELAXED) + #define ATOMIC_SET(dst, val) __atomic_store_n(&(dst), (val), __ATOMIC_RELAXED) +#else + #warning "Statistics data can be inaccurate" + #define ATOMIC_ADD(dst, val) ((dst) += (val)) + #define ATOMIC_SUB(dst, val) ((dst) -= (val)) + #define ATOMIC_SET(dst, val) ((dst) = (val)) +#endif + +_public_ +int knotd_conf_check_ref(knotd_conf_check_args_t *args) +{ + return check_ref(args); +} + +struct query_plan *query_plan_create(void) +{ + struct query_plan *plan = malloc(sizeof(struct query_plan)); + if (plan == NULL) { + return NULL; + } + + for (unsigned i = 0; i < KNOTD_STAGES; ++i) { + init_list(&plan->stage[i]); + } + + return plan; +} + +void query_plan_free(struct query_plan *plan) +{ + if (plan == NULL) { + return; + } + + for (unsigned i = 0; i < KNOTD_STAGES; ++i) { + struct query_step *step, *next; + WALK_LIST_DELSAFE(step, next, plan->stage[i]) { + free(step); + } + } + + free(plan); +} + +static struct query_step *make_step(query_step_process_f process, void *ctx) +{ + struct query_step *step = calloc(1, sizeof(struct query_step)); + if (step == NULL) { + return NULL; + } + + step->process = process; + step->ctx = ctx; + + return step; +} + +int query_plan_step(struct query_plan *plan, knotd_stage_t stage, + query_step_process_f process, void *ctx) +{ + struct query_step *step = make_step(process, ctx); + if (step == NULL) { + return KNOT_ENOMEM; + } + + add_tail(&plan->stage[stage], &step->node); + + return KNOT_EOK; +} + +_public_ +int knotd_mod_hook(knotd_mod_t *mod, knotd_stage_t stage, knotd_mod_hook_f hook) +{ + if (stage != KNOTD_STAGE_BEGIN && stage != KNOTD_STAGE_END) { + return KNOT_EINVAL; + } + + return query_plan_step(mod->plan, stage, hook, mod); +} + +_public_ +int knotd_mod_in_hook(knotd_mod_t *mod, knotd_stage_t stage, knotd_mod_in_hook_f hook) +{ + if (stage == KNOTD_STAGE_BEGIN || stage == KNOTD_STAGE_END) { + return KNOT_EINVAL; + } + + return query_plan_step(mod->plan, stage, hook, mod); +} + +knotd_mod_t *query_module_open(conf_t *conf, server_t *server, conf_mod_id_t *mod_id, + struct query_plan *plan, const knot_dname_t *zone) +{ + if (conf == NULL || server == NULL || mod_id == NULL || plan == NULL) { + return NULL; + } + + /* Locate the module. */ + const module_t *mod = conf_mod_find(conf, mod_id->name + 1, + mod_id->name[0], false); + if (mod == NULL) { + return NULL; + } + + /* Create query module. */ + knotd_mod_t *module = calloc(1, sizeof(knotd_mod_t)); + if (module == NULL) { + return NULL; + } + + module->plan = plan; + module->config = conf; + module->server = server; + module->zone = zone; + module->id = mod_id; + module->api = mod->api; + + return module; +} + +static void module_reset(conf_t *conf, knotd_mod_t *module, struct query_plan *new_plan) +{ + // Keep ->node + module->config = conf; + // Keep ->server + // Keep ->id + module->plan = new_plan; + // Keep ->zone + // Keep ->api + + // Reset DNSSEC + zone_sign_ctx_free(module->sign_ctx); + free_zone_keys(module->keyset); + free(module->keyset); + if (module->dnssec != NULL) { + kdnssec_ctx_deinit(module->dnssec); + free(module->dnssec); + } + module->dnssec = NULL; + module->keyset = NULL; + module->sign_ctx = NULL; + + // Reset statistics + knotd_mod_stats_free(module); + module->stats_info = NULL; + module->stats_vals = NULL; + module->stats_count = 0; + + // Keep ->ctx +} + +void query_module_close(knotd_mod_t *module) +{ + if (module == NULL) { + return; + } + + module_reset(NULL, module, NULL); + conf_free_mod_id(module->id); + free(module); +} + +void query_module_reset(conf_t *conf, knotd_mod_t *module, struct query_plan *new_plan) +{ + if (module == NULL) { + return; + } + + module_reset(conf, module, new_plan); +} + +_public_ +void *knotd_mod_ctx(knotd_mod_t *mod) +{ + return (mod != NULL) ? mod->ctx : NULL; +} + +_public_ +void knotd_mod_ctx_set(knotd_mod_t *mod, void *ctx) +{ + if (mod != NULL) mod->ctx = ctx; +} + +_public_ +const knot_dname_t *knotd_mod_zone(knotd_mod_t *mod) +{ + return (mod != NULL) ? mod->zone : NULL; +} + +_public_ +void knotd_mod_log(knotd_mod_t *mod, int priority, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + knotd_mod_vlog(mod, priority, fmt, args); + va_end(args); +} + +_public_ +void knotd_mod_vlog(knotd_mod_t *mod, int priority, const char *fmt, va_list args) +{ + if (mod == NULL || fmt == NULL) { + return; + } + + char msg[512]; + + if (vsnprintf(msg, sizeof(msg), fmt, args) < 0) { + msg[0] = '\0'; + } + + #define LOG_ARGS(mod_id, msg) "module '%s%s%.*s', %s", \ + mod_id->name + 1, (mod_id->len > 0) ? "/" : "", (int)mod_id->len, \ + mod_id->data, msg + + if (mod->zone == NULL) { + log_fmt(priority, LOG_SOURCE_SERVER, LOG_ARGS(mod->id, msg)); + } else { + log_fmt_zone(priority, LOG_SOURCE_ZONE, mod->zone, NULL, + LOG_ARGS(mod->id, msg)); + } + + #undef LOG_ARGS +} + +_public_ +int knotd_mod_stats_add(knotd_mod_t *mod, const char *ctr_name, uint32_t idx_count, + knotd_mod_idx_to_str_f idx_to_str) +{ + if (mod == NULL || idx_count == 0) { + return KNOT_EINVAL; + } + + unsigned threads = knotd_mod_threads(mod); + + mod_ctr_t *stats = NULL; + uint32_t offset = 0; + if (mod->stats_info == NULL) { + assert(mod->stats_count == 0); + stats = malloc(sizeof(*stats)); + if (stats == NULL) { + return KNOT_ENOMEM; + } + mod->stats_info = stats; + + assert(mod->stats_vals == NULL); + mod->stats_vals = calloc(threads, sizeof(*mod->stats_vals)); + if (mod->stats_vals == NULL) { + knotd_mod_stats_free(mod); + return KNOT_ENOMEM; + } + + for (unsigned i = 0; i < threads; i++) { + mod->stats_vals[i] = calloc(idx_count, sizeof(**mod->stats_vals)); + if (mod->stats_vals[i] == NULL) { + knotd_mod_stats_free(mod); + return KNOT_ENOMEM; + } + } + } else { + for (uint32_t i = 0; i < mod->stats_count; i++) { + offset += mod->stats_info[i].count; + } + assert(offset == mod->stats_info[mod->stats_count - 1].offset + + mod->stats_info[mod->stats_count - 1].count); + + assert(mod->stats_count > 0); + size_t old_size = mod->stats_count * sizeof(*stats); + size_t new_size = old_size + sizeof(*stats); + stats = realloc(mod->stats_info, new_size); + if (stats == NULL) { + knotd_mod_stats_free(mod); + return KNOT_ENOMEM; + } + mod->stats_info = stats; + stats += mod->stats_count; + + for (unsigned i = 0; i < threads; i++) { + uint64_t *new_vals = realloc(mod->stats_vals[i], + (offset + idx_count) * sizeof(*new_vals)); + if (new_vals == NULL) { + knotd_mod_stats_free(mod); + return KNOT_ENOMEM; + } + mod->stats_vals[i] = new_vals; + new_vals += offset; + for (uint32_t j = 0; j < idx_count; j++) { + *new_vals++ = 0; + } + } + } + + stats->name = ctr_name; + stats->count = idx_count; + stats->idx_to_str = idx_to_str; + stats->offset = offset; + + mod->stats_count++; + + return KNOT_EOK; +} + +_public_ +void knotd_mod_stats_free(knotd_mod_t *mod) +{ + if (mod == NULL || mod->stats_info == NULL) { + return; + } + + if (mod->stats_vals != NULL) { + unsigned threads = knotd_mod_threads(mod); + for (unsigned i = 0; i < threads; i++) { + free(mod->stats_vals[i]); + } + } + + free(mod->stats_vals); + free(mod->stats_info); +} + +#define STATS_BODY(OPERATION) { \ + if (mod == NULL) return; \ + \ + mod_ctr_t *ctr = mod->stats_info + ctr_id; \ + assert(idx < ctr->count); \ + OPERATION(mod->stats_vals[thr_id][ctr->offset + idx], val); \ +} + +_public_ +void knotd_mod_stats_incr(knotd_mod_t *mod, unsigned thr_id, uint32_t ctr_id, + uint32_t idx, uint64_t val) +{ + STATS_BODY(ATOMIC_ADD) +} + +_public_ +void knotd_mod_stats_decr(knotd_mod_t *mod, unsigned thr_id, uint32_t ctr_id, + uint32_t idx, uint64_t val) +{ + STATS_BODY(ATOMIC_SUB) +} + +_public_ +void knotd_mod_stats_store(knotd_mod_t *mod, unsigned thr_id, uint32_t ctr_id, + uint32_t idx, uint64_t val) +{ + STATS_BODY(ATOMIC_SET) +} + +_public_ +knotd_conf_t knotd_conf_env(knotd_mod_t *mod, knotd_conf_env_t env) +{ + static const char *version = "Knot DNS " PACKAGE_VERSION; + + knotd_conf_t out = { { 0 } }; + + if (mod == NULL) { + return out; + } + + conf_t *config = (mod->config != NULL) ? mod->config : conf(); + + switch (env) { + case KNOTD_CONF_ENV_VERSION: + out.single.string = version; + break; + case KNOTD_CONF_ENV_HOSTNAME: + out.single.string = config->hostname; + break; + case KNOTD_CONF_ENV_WORKERS_UDP: + out.single.integer = config->cache.srv_udp_threads; + break; + case KNOTD_CONF_ENV_WORKERS_TCP: + out.single.integer = config->cache.srv_tcp_threads; + break; + case KNOTD_CONF_ENV_WORKERS_XDP: + out.single.integer = config->cache.srv_xdp_threads; + break; + default: + return out; + } + + out.count = 1; + + return out; +} + +_public_ +unsigned knotd_mod_threads(knotd_mod_t *mod) +{ + knotd_conf_t udp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_UDP); + knotd_conf_t xdp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_XDP); + knotd_conf_t tcp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_TCP); + return udp.single.integer + xdp.single.integer + tcp.single.integer; +} + +static void set_val(yp_type_t type, knotd_conf_val_t *item, conf_val_t *val) +{ + switch (type) { + case YP_TINT: + item->integer = conf_int(val); + break; + case YP_TBOOL: + item->boolean = conf_bool(val); + break; + case YP_TOPT: + item->option = conf_opt(val); + break; + case YP_TSTR: + item->string = conf_str(val); + break; + case YP_TDNAME: + item->dname = conf_dname(val); + break; + case YP_TADDR: + item->addr = conf_addr(val, NULL); + break; + case YP_TNET: + item->addr = conf_addr_range(val, &item->addr_max, + &item->addr_mask); + break; + case YP_TREF: + if (val->code == KNOT_EOK) { + conf_val(val); + item->data_len = val->len; + item->data = val->data; + } + break; + case YP_THEX: + case YP_TB64: + item->data = conf_bin(val, &item->data_len); + break; + case YP_TDATA: + item->data = conf_data(val, &item->data_len); + break; + default: + return; + } +} + +static void set_conf_out(knotd_conf_t *out, conf_val_t *val) +{ + if (!(val->item->flags & YP_FMULTI)) { + out->count = (val->code == KNOT_EOK) ? 1 : 0; + set_val(val->item->type, &out->single, val); + } else { + size_t count = conf_val_count(val); + if (count == 0) { + return; + } + + out->multi = malloc(count * sizeof(*out->multi)); + if (out->multi == NULL) { + return; + } + memset(out->multi, 0, count * sizeof(*out->multi)); + + for (size_t i = 0; i < count; i++) { + set_val(val->item->type, &out->multi[i], val); + conf_val_next(val); + } + out->count = count; + } +} + +_public_ +knotd_conf_t knotd_conf(knotd_mod_t *mod, const yp_name_t *section_name, + const yp_name_t *item_name, const knotd_conf_t *id) +{ + knotd_conf_t out = { { 0 } }; + + if (mod == NULL || section_name == NULL || item_name == NULL) { + return out; + } + + conf_t *config = (mod->config != NULL) ? mod->config : conf(); + + conf_val_t val; + if (id != NULL) { + val = conf_rawid_get(config, section_name, item_name, + id->single.data, id->single.data_len); + } else { + val = conf_get(config, section_name, item_name); + } + + set_conf_out(&out, &val); + + return out; +} + +_public_ +knotd_conf_t knotd_conf_mod(knotd_mod_t *mod, const yp_name_t *item_name) +{ + knotd_conf_t out = { { 0 } }; + + if (mod == NULL || item_name == NULL) { + return out; + } + + conf_t *config = (mod->config != NULL) ? mod->config : conf(); + + conf_val_t val = conf_mod_get(config, item_name, mod->id); + if (val.item == NULL) { + return out; + } + + set_conf_out(&out, &val); + + return out; +} + +_public_ +knotd_conf_t knotd_conf_zone(knotd_mod_t *mod, const yp_name_t *item_name, + const knot_dname_t *zone) +{ + knotd_conf_t out = { { 0 } }; + + if (mod == NULL || item_name == NULL || zone == NULL) { + return out; + } + + conf_t *config = (mod->config != NULL) ? mod->config : conf(); + + conf_val_t val = conf_zone_get(config, item_name, zone); + + set_conf_out(&out, &val); + + return out; +} + +_public_ +knotd_conf_t knotd_conf_check_item(knotd_conf_check_args_t *args, + const yp_name_t *item_name) +{ + knotd_conf_t out = { { 0 } }; + + conf_val_t val = conf_rawid_get_txn(args->extra->conf, args->extra->txn, + args->item->name, item_name, + args->id, args->id_len); + + set_conf_out(&out, &val); + + return out; +} + +_public_ +bool knotd_conf_addr_range_match(const knotd_conf_t *range, + const struct sockaddr_storage *addr) +{ + if (range == NULL || addr == NULL) { + return false; + } + + for (size_t i = 0; i < range->count; i++) { + knotd_conf_val_t *val = &range->multi[i]; + if (val->addr_max.ss_family == AF_UNSPEC) { + if (sockaddr_net_match(addr, &val->addr, val->addr_mask)) { + return true; + } + } else { + if (sockaddr_range_match(addr, &val->addr, &val->addr_max)) { + return true; + } + } + } + + return false; +} + +_public_ +void knotd_conf_free(knotd_conf_t *conf) +{ + if (conf == NULL) { + return; + } + + if (conf->count > 0 && conf->multi != NULL) { + memset(conf->multi, 0, conf->count * sizeof(*conf->multi)); + free(conf->multi); + } + memset(conf, 0, sizeof(*conf)); +} + +_public_ +const struct sockaddr_storage *knotd_qdata_local_addr(knotd_qdata_t *qdata, + struct sockaddr_storage *buff) +{ + if (qdata == NULL) { + return NULL; + } + + if (qdata->params->xdp_msg != NULL) { +#ifdef ENABLE_XDP + return (struct sockaddr_storage *)&qdata->params->xdp_msg->ip_to; +#else + assert(0); + return NULL; +#endif + } else { + socklen_t buff_len = sizeof(*buff); + if (getsockname(qdata->params->socket, (struct sockaddr *)buff, + &buff_len) != 0) { + return NULL; + } + return buff; + } +} + +_public_ +const struct sockaddr_storage *knotd_qdata_remote_addr(knotd_qdata_t *qdata) +{ + if (qdata == NULL) { + return NULL; + } + + if (qdata->params->xdp_msg != NULL) { +#ifdef ENABLE_XDP + return (struct sockaddr_storage *)&qdata->params->xdp_msg->ip_from; +#else + assert(0); + return NULL; +#endif + } else { + return qdata->params->remote; + } +} + +_public_ +uint32_t knotd_qdata_rtt(knotd_qdata_t *qdata) +{ + if (qdata == NULL) { + return 0; + } + + switch (qdata->params->proto) { + case KNOTD_QUERY_PROTO_TCP: + if (qdata->params->xdp_msg != NULL) { +#ifdef ENABLE_XDP + return qdata->params->measured_rtt; +#else + assert(0); + return 0; +#endif + } else { + return knot_probe_tcp_rtt(qdata->params->socket); + } + case KNOTD_QUERY_PROTO_QUIC: + return qdata->params->measured_rtt; + case KNOTD_QUERY_PROTO_UDP: + default: + return 0; + } +} + +_public_ +const knot_dname_t *knotd_qdata_zone_name(knotd_qdata_t *qdata) +{ + if (qdata == NULL || qdata->extra->zone == NULL) { + return NULL; + } + + return qdata->extra->zone->name; +} + +_public_ +knot_rrset_t knotd_qdata_zone_apex_rrset(knotd_qdata_t *qdata, uint16_t type) +{ + if (qdata == NULL || qdata->extra->contents == NULL) { + return node_rrset(NULL, type); + } + + return node_rrset(qdata->extra->contents->apex, type); +} + +_public_ +int knotd_mod_dnssec_init(knotd_mod_t *mod) +{ + if (mod == NULL || mod->dnssec != NULL) { + return KNOT_EINVAL; + } + + knot_lmdb_db_t *kaspdb = &mod->server->kaspdb; + kasp_db_ensure_init(kaspdb, mod->config); // probably redundant + + mod->dnssec = calloc(1, sizeof(*(mod->dnssec))); + if (mod->dnssec == NULL) { + return KNOT_ENOMEM; + } + + conf_val_t conf = conf_zone_get(mod->config, C_DNSSEC_SIGNING, mod->zone); + int ret = kdnssec_ctx_init(mod->config, mod->dnssec, mod->zone, kaspdb, + conf_bool(&conf) ? NULL : mod->id); + if (ret != KNOT_EOK) { + free(mod->dnssec); + mod->dnssec = NULL; + return ret; + } + + return KNOT_EOK; +} + +_public_ +int knotd_mod_dnssec_load_keyset(knotd_mod_t *mod, bool verbose) +{ + if (mod == NULL || mod->dnssec == NULL) { + return KNOT_EINVAL; + } + + mod->keyset = calloc(1, sizeof(*(mod->keyset))); + if (mod->keyset == NULL) { + return KNOT_ENOMEM; + } + + int ret = load_zone_keys(mod->dnssec, mod->keyset, verbose); + if (ret != KNOT_EOK) { + free(mod->keyset); + mod->keyset = NULL; + return ret; + } + + mod->sign_ctx = zone_sign_ctx(mod->keyset, mod->dnssec); + if (mod->sign_ctx == NULL) { + free_zone_keys(mod->keyset); + free(mod->keyset); + mod->keyset = NULL; + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +_public_ +void knotd_mod_dnssec_unload_keyset(knotd_mod_t *mod) +{ + if (mod != NULL && mod->keyset != NULL) { + zone_sign_ctx_free(mod->sign_ctx); + mod->sign_ctx = NULL; + + free_zone_keys(mod->keyset); + free(mod->keyset); + mod->keyset = NULL; + } +} + +_public_ +int knotd_mod_dnssec_sign_rrset(knotd_mod_t *mod, knot_rrset_t *rrsigs, + const knot_rrset_t *rrset, knot_mm_t *mm) +{ + if (mod == NULL || rrsigs == NULL || rrset == NULL) { + return KNOT_EINVAL; + } + + return knot_sign_rrset2(rrsigs, rrset, mod->sign_ctx, mm); +} diff --git a/src/knot/nameserver/query_module.h b/src/knot/nameserver/query_module.h new file mode 100644 index 0000000..5cc905b --- /dev/null +++ b/src/knot/nameserver/query_module.h @@ -0,0 +1,99 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/libknot.h" +#include "knot/conf/conf.h" +#include "knot/dnssec/context.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/include/module.h" +#include "knot/server/server.h" +#include "contrib/ucw/lists.h" + +#ifdef HAVE_ATOMIC + #define ATOMIC_GET(src) __atomic_load_n(&(src), __ATOMIC_RELAXED) +#else + #define ATOMIC_GET(src) (src) +#endif + +#define KNOTD_STAGES (KNOTD_STAGE_END + 1) + +typedef unsigned (*query_step_process_f) + (unsigned state, knot_pkt_t *pkt, knotd_qdata_t *qdata, knotd_mod_t *mod); + +/*! \brief Single processing step in query processing. */ +struct query_step { + node_t node; + void *ctx; + query_step_process_f process; +}; + +/*! Query plan represents a sequence of steps needed for query processing + * divided into several stages, where each stage represents a current response + * assembly phase, for example 'before processing', 'answer section' and so on. + */ +struct query_plan { + list_t stage[KNOTD_STAGES]; +}; + +/*! \brief Create an empty query plan. */ +struct query_plan *query_plan_create(void); + +/*! \brief Free query plan and all planned steps. */ +void query_plan_free(struct query_plan *plan); + +/*! \brief Plan another step for given stage. */ +int query_plan_step(struct query_plan *plan, knotd_stage_t stage, + query_step_process_f process, void *ctx); + +/*! \brief Open query module identified by name. */ +knotd_mod_t *query_module_open(conf_t *conf, server_t *server, conf_mod_id_t *mod_id, + struct query_plan *plan, const knot_dname_t *zone); + +/*! \brief Close query module. */ +void query_module_close(knotd_mod_t *module); + +/*! \brief Close and open existing query module. */ +void query_module_reset(conf_t *conf, knotd_mod_t *module, struct query_plan *new_plan); + +typedef char* (*mod_idx_to_str_f)(uint32_t idx, uint32_t count); + +typedef struct { + const char *name; + mod_idx_to_str_f idx_to_str; // unused if count == 1 + uint32_t offset; // offset of counters in stats_vals[thread_id] + uint32_t count; +} mod_ctr_t; + +struct knotd_mod { + node_t node; + conf_t *config; + server_t *server; + conf_mod_id_t *id; + struct query_plan *plan; + const knot_dname_t *zone; + const knotd_mod_api_t *api; + kdnssec_ctx_t *dnssec; + zone_keyset_t *keyset; + zone_sign_ctx_t *sign_ctx; + mod_ctr_t *stats_info; + uint64_t **stats_vals; + uint32_t stats_count; + void *ctx; +}; + +void knotd_mod_stats_free(knotd_mod_t *mod); diff --git a/src/knot/nameserver/tsig_ctx.c b/src/knot/nameserver/tsig_ctx.c new file mode 100644 index 0000000..05383b1 --- /dev/null +++ b/src/knot/nameserver/tsig_ctx.c @@ -0,0 +1,189 @@ +/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/nameserver/tsig_ctx.h" +#include "contrib/string.h" +#include "libknot/libknot.h" + +/*! + * Maximal total size for unsigned messages. + */ +static const size_t TSIG_BUFFER_MAX_SIZE = (UINT16_MAX * 100); + +void tsig_init(tsig_ctx_t *ctx, const knot_tsig_key_t *key) +{ + if (!ctx) { + return; + } + + memzero(ctx, sizeof(*ctx)); + ctx->key = key; +} + +void tsig_cleanup(tsig_ctx_t *ctx) +{ + if (!ctx) { + return; + } + + free(ctx->buffer); + memzero(ctx, sizeof(*ctx)); +} + +void tsig_reset(tsig_ctx_t *ctx) +{ + if (!ctx) { + return; + } + + const knot_tsig_key_t *backup = ctx->key; + tsig_cleanup(ctx); + tsig_init(ctx, backup); +} + +int tsig_sign_packet(tsig_ctx_t *ctx, knot_pkt_t *packet) +{ + if (!ctx || !packet) { + return KNOT_EINVAL; + } + + if (ctx->key == NULL) { + return KNOT_EOK; + } + + int ret = KNOT_ERROR; + if (ctx->digest_size == 0) { + ctx->digest_size = dnssec_tsig_algorithm_size(ctx->key->algorithm); + ret = knot_tsig_sign(packet->wire, &packet->size, packet->max_size, + NULL, 0, + ctx->digest, &ctx->digest_size, + ctx->key, 0, 0); + } else { + uint8_t previous_digest[ctx->digest_size]; + memcpy(previous_digest, ctx->digest, ctx->digest_size); + + ret = knot_tsig_sign_next(packet->wire, &packet->size, packet->max_size, + previous_digest, ctx->digest_size, + ctx->digest, &ctx->digest_size, + ctx->key, packet->wire, packet->size); + } + + return ret; +} + +static int update_ctx_after_verify(tsig_ctx_t *ctx, knot_rrset_t *tsig_rr) +{ + assert(ctx); + assert(tsig_rr); + + if (ctx->digest_size != knot_tsig_rdata_mac_length(tsig_rr)) { + return KNOT_EMALF; + } + + memcpy(ctx->digest, knot_tsig_rdata_mac(tsig_rr), ctx->digest_size); + ctx->prev_signed_time = knot_tsig_rdata_time_signed(tsig_rr); + ctx->unsigned_count = 0; + ctx->buffer_used = 0; + + return KNOT_EOK; +} + +static int buffer_add_packet(tsig_ctx_t *ctx, knot_pkt_t *packet) +{ + size_t need = ctx->buffer_used + packet->size; + + // Inflate the buffer if necessary. + + if (need > TSIG_BUFFER_MAX_SIZE) { + return KNOT_ENOMEM; + } + + if (need > ctx->buffer_size) { + uint8_t *buffer = realloc(ctx->buffer, need); + if (!buffer) { + return KNOT_ENOMEM; + } + + ctx->buffer = buffer; + ctx->buffer_size = need; + } + + // Buffer the packet. + + uint8_t *write = ctx->buffer + ctx->buffer_used; + memcpy(write, packet->wire, packet->size); + ctx->buffer_used = need; + + return KNOT_EOK; +} + +int tsig_verify_packet(tsig_ctx_t *ctx, knot_pkt_t *packet) +{ + if (!ctx || !packet) { + return KNOT_EINVAL; + } + + if (ctx->key == NULL) { + return KNOT_EOK; + } + + int ret = buffer_add_packet(ctx, packet); + if (ret != KNOT_EOK) { + return ret; + } + + // Unsigned packet. + + if (packet->tsig_rr == NULL) { + ctx->unsigned_count += 1; + return KNOT_EOK; + } + + // Signed packet. + + if (ctx->prev_signed_time == 0) { + ret = knot_tsig_client_check(packet->tsig_rr, ctx->buffer, + ctx->buffer_used, ctx->digest, + ctx->digest_size, ctx->key, 0); + } else { + ret = knot_tsig_client_check_next(packet->tsig_rr, ctx->buffer, + ctx->buffer_used, ctx->digest, + ctx->digest_size, ctx->key, + ctx->prev_signed_time); + } + + if (ret != KNOT_EOK) { + return ret; + } + + ret = update_ctx_after_verify(ctx, packet->tsig_rr); + if (ret != KNOT_EOK) { + return ret; + } + + return KNOT_EOK; +} + +unsigned tsig_unsigned_count(tsig_ctx_t *ctx) +{ + if (!ctx) { + return -1; + } + + return ctx->unsigned_count; +} diff --git a/src/knot/nameserver/tsig_ctx.h b/src/knot/nameserver/tsig_ctx.h new file mode 100644 index 0000000..3e91671 --- /dev/null +++ b/src/knot/nameserver/tsig_ctx.h @@ -0,0 +1,97 @@ +/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> + +#include "libknot/packet/pkt.h" +#include "libknot/tsig.h" + +#define TSIG_MAX_DIGEST_SIZE 64 + +/*! + \brief TSIG context. + */ +typedef struct tsig_ctx { + const knot_tsig_key_t *key; + uint64_t prev_signed_time; + + uint8_t digest[TSIG_MAX_DIGEST_SIZE]; + size_t digest_size; + + /* Unsigned packets handling. */ + unsigned unsigned_count; + uint8_t *buffer; + size_t buffer_used; + size_t buffer_size; +} tsig_ctx_t; + +/*! + * \brief Initialize TSIG context. + * + * \param ctx TSIG context to be initialized. + * \param key Key to be used for signing. If NULL, all performed operations + * will do nothing and always successful. + */ +void tsig_init(tsig_ctx_t *ctx, const knot_tsig_key_t *key); + +/*! + * \brief Cleanup TSIG context. + * + * \param ctx TSIG context to be cleaned up. + */ +void tsig_cleanup(tsig_ctx_t *ctx); + +/*! + * \brief Reset TSIG context for new message exchange. + */ +void tsig_reset(tsig_ctx_t *ctx); + +/*! + * \brief Sign outgoing packet. + * + * \param ctx TSIG signing context. + * \param packet Packet to be signed. + * + * \return Error code, KNOT_EOK if successful. + */ +int tsig_sign_packet(tsig_ctx_t *ctx, knot_pkt_t *packet); + +/*! + * \brief Verify incoming packet. + * + * If the packet is not signed, the function will succeed, but an internal + * counter of unsigned packets is increased. When a packet is signed, the + * same counter is reset to zero. + * + * \see tsig_unsigned_count + * + * \param ctx TSIG signing context. + * \param packet Packet to be verified. + * + * \return Error code, KNOT_EOK if successful. + */ +int tsig_verify_packet(tsig_ctx_t *ctx, knot_pkt_t *packet); + +/*! + * \brief Get number of unsigned packets since the last signed one. + * + * \param ctx TSIG signing context. + * + * \return Number of unsigned packets since the last signed one. + */ +unsigned tsig_unsigned_count(tsig_ctx_t *ctx); diff --git a/src/knot/nameserver/update.c b/src/knot/nameserver/update.c new file mode 100644 index 0000000..f43e1af --- /dev/null +++ b/src/knot/nameserver/update.c @@ -0,0 +1,107 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <unistd.h> + +#include "knot/dnssec/zone-events.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/update.h" +#include "knot/query/requestor.h" +#include "libknot/libknot.h" + +static int update_enqueue(zone_t *zone, knotd_qdata_t *qdata) +{ + assert(zone); + assert(qdata); + + /* Create serialized request. */ + knot_request_t *req = calloc(1, sizeof(*req)); + if (req == NULL) { + return KNOT_ENOMEM; + } + + /* Store socket and remote address. */ + req->fd = dup(qdata->params->socket); + memcpy(&req->remote, knotd_qdata_remote_addr(qdata), sizeof(req->remote)); + + /* Store update request. */ + req->query = knot_pkt_new(NULL, qdata->query->max_size, NULL); + int ret = knot_pkt_copy(req->query, qdata->query); + if (ret != KNOT_EOK) { + knot_pkt_free(req->query); + free(req); + return ret; + } + + /* Store and update possible TSIG context (see NS_NEED_AUTH). */ + if (qdata->sign.tsig_key.name != NULL) { + req->sign = qdata->sign; + req->sign.tsig_digest = (uint8_t *)knot_tsig_rdata_mac(req->query->tsig_rr); + req->sign.tsig_key.name = req->query->tsig_rr->owner; + ret = dnssec_binary_dup(&qdata->sign.tsig_key.secret, &req->sign.tsig_key.secret); + if (ret != KNOT_EOK) { + knot_pkt_free(req->query); + free(req); + return ret; + } + assert(req->sign.tsig_digestlen == knot_tsig_rdata_mac_length(req->query->tsig_rr)); + assert(req->sign.tsig_key.algorithm == knot_tsig_rdata_alg(req->query->tsig_rr)); + } + + pthread_mutex_lock(&zone->ddns_lock); + + /* Enqueue created request. */ + ptrlist_add(&zone->ddns_queue, req, NULL); + ++zone->ddns_queue_size; + + pthread_mutex_unlock(&zone->ddns_lock); + + /* Schedule UPDATE event. */ + zone_events_schedule_now(zone, ZONE_EVENT_UPDATE); + + return KNOT_EOK; +} + +int update_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + /* DDNS over XDP not supported. */ + if (qdata->params->xdp_msg != NULL) { + qdata->rcode = KNOT_RCODE_SERVFAIL; + return KNOT_STATE_FAIL; + } + + /* RFC1996 require SOA question. */ + NS_NEED_QTYPE(qdata, KNOT_RRTYPE_SOA, KNOT_RCODE_FORMERR); + + /* Check valid zone. */ + NS_NEED_ZONE(qdata, KNOT_RCODE_NOTAUTH); + + /* Need valid transaction security. */ + NS_NEED_AUTH(qdata, ACL_ACTION_UPDATE); + /* Check expiration. */ + NS_NEED_ZONE_CONTENTS(qdata); + /* Check frozen zone. */ + NS_NEED_NOT_FROZEN(qdata); + + /* Store update into DDNS queue. */ + int ret = update_enqueue((zone_t *)qdata->extra->zone, qdata); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + /* No immediate response. */ + return KNOT_STATE_NOOP; +} diff --git a/src/knot/nameserver/update.h b/src/knot/nameserver/update.h new file mode 100644 index 0000000..609acd9 --- /dev/null +++ b/src/knot/nameserver/update.h @@ -0,0 +1,27 @@ +/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" +#include "knot/nameserver/process_query.h" + +/*! + * \brief UPDATE query processing module. + * + * \return KNOT_STATE_* processing states + */ +int update_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata); diff --git a/src/knot/nameserver/xfr.c b/src/knot/nameserver/xfr.c new file mode 100644 index 0000000..b54a4ff --- /dev/null +++ b/src/knot/nameserver/xfr.c @@ -0,0 +1,96 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "knot/nameserver/xfr.h" +#include "contrib/mempattern.h" + +int xfr_process_list(knot_pkt_t *pkt, xfr_put_cb put, knotd_qdata_t *qdata) +{ + if (pkt == NULL || qdata == NULL || qdata->extra->ext == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + knot_mm_t *mm = qdata->mm; + struct xfr_proc *xfer = qdata->extra->ext; + + /* Check if the zone wasn't expired during multi-message transfer. */ + const zone_contents_t *contents = qdata->extra->contents; + if (contents == NULL) { + return KNOT_ENOZONE; + } + knot_rrset_t soa_rr = node_rrset(contents->apex, KNOT_RRTYPE_SOA); + + /* Prepend SOA on first packet. */ + if (xfer->stats.messages == 0) { + ret = knot_pkt_put(pkt, 0, &soa_rr, KNOT_PF_NOTRUNC); + if (ret != KNOT_EOK) { + return ret; + } + } + + /* Process all items in the list. */ + while (!EMPTY_LIST(xfer->nodes)) { + ptrnode_t *head = HEAD(xfer->nodes); + ret = put(pkt, head->d, xfer); + if (ret == KNOT_EOK) { /* Finished. */ + /* Complete change set. */ + rem_node((node_t *)head); + mm_free(mm, head); + } else { /* Packet full or other error. */ + break; + } + } + + /* Append SOA on last packet. */ + if (ret == KNOT_EOK) { + ret = knot_pkt_put(pkt, 0, &soa_rr, KNOT_PF_NOTRUNC); + } + + /* Update counters. */ + xfr_stats_add(&xfer->stats, pkt->size + knot_rrset_size(&qdata->opt_rr)); + + /* If a rrset is larger than the message, + * fail to avoid infinite loop of empty messages */ + if (ret == KNOT_ESPACE && pkt->rrset_count < 1) { + return KNOT_ENOXFR; + } + + return ret; +} + +void xfr_stats_begin(struct xfr_stats *stats) +{ + assert(stats); + + memset(stats, 0, sizeof(*stats)); + stats->begin = time_now(); +} + +void xfr_stats_add(struct xfr_stats *stats, unsigned bytes) +{ + assert(stats); + + stats->messages += 1; + stats->bytes += bytes; +} + +void xfr_stats_end(struct xfr_stats *stats) +{ + assert(stats); + + stats->end = time_now(); +} diff --git a/src/knot/nameserver/xfr.h b/src/knot/nameserver/xfr.h new file mode 100644 index 0000000..3347304 --- /dev/null +++ b/src/knot/nameserver/xfr.h @@ -0,0 +1,69 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/time.h" +#include "contrib/ucw/lists.h" +#include "knot/nameserver/log.h" +#include "knot/nameserver/process_query.h" +#include "knot/zone/contents.h" +#include "libknot/packet/pkt.h" + +struct xfr_stats { + unsigned messages; + unsigned bytes; + struct timespec begin; + struct timespec end; +}; + +void xfr_stats_begin(struct xfr_stats *stats); +void xfr_stats_add(struct xfr_stats *stats, unsigned bytes); +void xfr_stats_end(struct xfr_stats *stats); + +static inline +void xfr_log_finished(const knot_dname_t *zone, log_operation_t op, + log_direction_t dir, const struct sockaddr *remote, + bool reused, const struct xfr_stats *stats) +{ + ns_log(LOG_INFO, zone, op, dir, remote, reused, + "finished, %0.2f seconds, %u messages, %u bytes", + time_diff_ms(&stats->begin, &stats->end) / 1000.0, + stats->messages, stats->bytes); +} + +/*! + * \brief Generic transfer processing state. + */ +struct xfr_proc { + list_t nodes; //!< Items to process (ptrnode_t). + zone_contents_t *contents; //!< Processed zone. + struct xfr_stats stats; //!< Packet transfer statistics. +}; + +/*! + * \brief Generic transfer processing. + * + * \return KNOT_EOK or an error + */ +typedef int (*xfr_put_cb)(knot_pkt_t *pkt, const void *item, struct xfr_proc *xfer); + +/*! + * \brief Put all items from xfr_proc.nodes to packet using a callback function. + * + * \note qdata->extra->ext points to struct xfr_proc* (this is xfer-specific context) + */ +int xfr_process_list(knot_pkt_t *pkt, xfr_put_cb put, knotd_qdata_t *qdata); |