summaryrefslogtreecommitdiffstats
path: root/src/knot/nameserver
diff options
context:
space:
mode:
Diffstat (limited to 'src/knot/nameserver')
-rw-r--r--src/knot/nameserver/axfr.c225
-rw-r--r--src/knot/nameserver/axfr.h27
-rw-r--r--src/knot/nameserver/chaos.c145
-rw-r--r--src/knot/nameserver/chaos.h24
-rw-r--r--src/knot/nameserver/internet.c728
-rw-r--r--src/knot/nameserver/internet.h79
-rw-r--r--src/knot/nameserver/ixfr.c332
-rw-r--r--src/knot/nameserver/ixfr.h63
-rw-r--r--src/knot/nameserver/log.h88
-rw-r--r--src/knot/nameserver/notify.c92
-rw-r--r--src/knot/nameserver/notify.h28
-rw-r--r--src/knot/nameserver/nsec_proofs.c677
-rw-r--r--src/knot/nameserver/nsec_proofs.h38
-rw-r--r--src/knot/nameserver/process_query.c978
-rw-r--r--src/knot/nameserver/process_query.h107
-rw-r--r--src/knot/nameserver/query_module.c791
-rw-r--r--src/knot/nameserver/query_module.h99
-rw-r--r--src/knot/nameserver/tsig_ctx.c189
-rw-r--r--src/knot/nameserver/tsig_ctx.h97
-rw-r--r--src/knot/nameserver/update.c107
-rw-r--r--src/knot/nameserver/update.h27
-rw-r--r--src/knot/nameserver/xfr.c96
-rw-r--r--src/knot/nameserver/xfr.h69
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);