summaryrefslogtreecommitdiffstats
path: root/src/knot/modules
diff options
context:
space:
mode:
Diffstat (limited to 'src/knot/modules')
-rw-r--r--src/knot/modules/cookies/Makefile.inc13
-rw-r--r--src/knot/modules/cookies/cookies.c308
-rw-r--r--src/knot/modules/cookies/cookies.rst110
-rw-r--r--src/knot/modules/dnsproxy/Makefile.inc13
-rw-r--r--src/knot/modules/dnsproxy/dnsproxy.c191
-rw-r--r--src/knot/modules/dnsproxy/dnsproxy.rst125
-rw-r--r--src/knot/modules/dnstap/Makefile.inc15
-rw-r--r--src/knot/modules/dnstap/dnstap.c338
-rw-r--r--src/knot/modules/dnstap/dnstap.rst113
-rw-r--r--src/knot/modules/geoip/Makefile.inc17
-rw-r--r--src/knot/modules/geoip/geodb.c216
-rw-r--r--src/knot/modules/geoip/geodb.h67
-rw-r--r--src/knot/modules/geoip/geoip.c1061
-rw-r--r--src/knot/modules/geoip/geoip.rst324
-rw-r--r--src/knot/modules/noudp/Makefile.inc12
-rw-r--r--src/knot/modules/noudp/noudp.c110
-rw-r--r--src/knot/modules/noudp/noudp.rst68
-rw-r--r--src/knot/modules/onlinesign/Makefile.inc15
-rw-r--r--src/knot/modules/onlinesign/nsec_next.c113
-rw-r--r--src/knot/modules/onlinesign/nsec_next.h29
-rw-r--r--src/knot/modules/onlinesign/onlinesign.c736
-rw-r--r--src/knot/modules/onlinesign/onlinesign.rst158
-rw-r--r--src/knot/modules/probe/Makefile.inc12
-rw-r--r--src/knot/modules/probe/probe.c190
-rw-r--r--src/knot/modules/probe/probe.rst89
-rw-r--r--src/knot/modules/queryacl/Makefile.inc12
-rw-r--r--src/knot/modules/queryacl/queryacl.c93
-rw-r--r--src/knot/modules/queryacl/queryacl.rst70
-rw-r--r--src/knot/modules/rrl/Makefile.inc15
-rw-r--r--src/knot/modules/rrl/functions.c554
-rw-r--r--src/knot/modules/rrl/functions.h111
-rw-r--r--src/knot/modules/rrl/rrl.c208
-rw-r--r--src/knot/modules/rrl/rrl.rst133
-rw-r--r--src/knot/modules/static_modules.h.in25
-rw-r--r--src/knot/modules/stats/Makefile.inc13
-rw-r--r--src/knot/modules/stats/stats.c676
-rw-r--r--src/knot/modules/stats/stats.rst274
-rw-r--r--src/knot/modules/synthrecord/Makefile.inc13
-rw-r--r--src/knot/modules/synthrecord/synthrecord.c625
-rw-r--r--src/knot/modules/synthrecord/synthrecord.rst170
-rw-r--r--src/knot/modules/whoami/Makefile.inc12
-rw-r--r--src/knot/modules/whoami/whoami.c114
-rw-r--r--src/knot/modules/whoami/whoami.rst97
43 files changed, 7658 insertions, 0 deletions
diff --git a/src/knot/modules/cookies/Makefile.inc b/src/knot/modules/cookies/Makefile.inc
new file mode 100644
index 0000000..0f0b342
--- /dev/null
+++ b/src/knot/modules/cookies/Makefile.inc
@@ -0,0 +1,13 @@
+knot_modules_cookies_la_SOURCES = knot/modules/cookies/cookies.c
+EXTRA_DIST += knot/modules/cookies/cookies.rst
+
+if STATIC_MODULE_cookies
+libknotd_la_SOURCES += $(knot_modules_cookies_la_SOURCES)
+endif
+
+if SHARED_MODULE_cookies
+knot_modules_cookies_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_cookies_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+knot_modules_cookies_la_LIBADD = $(libcontrib_LIBS)
+pkglib_LTLIBRARIES += knot/modules/cookies.la
+endif
diff --git a/src/knot/modules/cookies/cookies.c b/src/knot/modules/cookies/cookies.c
new file mode 100644
index 0000000..34c4b22
--- /dev/null
+++ b/src/knot/modules/cookies/cookies.c
@@ -0,0 +1,308 @@
+/* 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 <pthread.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "knot/include/module.h"
+#include "libknot/libknot.h"
+#include "contrib/string.h"
+#include "libdnssec/random.h"
+
+#ifdef HAVE_ATOMIC
+#define ATOMIC_SET(dst, val) __atomic_store_n(&(dst), (val), __ATOMIC_RELAXED)
+#define ATOMIC_GET(src) __atomic_load_n(&(src), __ATOMIC_RELAXED)
+#define ATOMIC_ADD(dst, val) __atomic_add_fetch(&(dst), (val), __ATOMIC_RELAXED)
+#else
+#define ATOMIC_SET(dst, val) ((dst) = (val))
+#define ATOMIC_GET(src) (src)
+#define ATOMIC_ADD(dst, val) ((dst) += (val))
+#endif
+
+#define BADCOOKIE_CTR_INIT 1
+
+#define MOD_SECRET_LIFETIME "\x0F""secret-lifetime"
+#define MOD_BADCOOKIE_SLIP "\x0E""badcookie-slip"
+#define MOD_SECRET "\x06""secret"
+
+const yp_item_t cookies_conf[] = {
+ { MOD_SECRET_LIFETIME, YP_TINT, YP_VINT = { 1, 36*24*3600, 26*3600, YP_STIME } },
+ { MOD_BADCOOKIE_SLIP, YP_TINT, YP_VINT = { 1, INT32_MAX, 1 } },
+ { MOD_SECRET, YP_THEX, YP_VNONE },
+ { NULL }
+};
+
+int cookies_conf_check(knotd_conf_check_args_t *args)
+{
+ knotd_conf_t conf = knotd_conf_check_item(args, MOD_SECRET);
+ if (conf.count == 1 && conf.single.data_len != KNOT_EDNS_COOKIE_SECRET_SIZE) {
+ args->err_str = "the length of the cookie secret "
+ "MUST BE 16 bytes (32 HEX characters)";
+ return KNOT_EINVAL;
+ }
+ return KNOT_EOK;
+}
+
+typedef struct {
+ struct {
+ uint64_t variable;
+ uint64_t constant;
+ } secret;
+ pthread_t update_secret;
+ uint32_t secret_lifetime;
+ uint32_t badcookie_slip;
+ uint16_t badcookie_ctr; // Counter for BADCOOKIE answers.
+} cookies_ctx_t;
+
+static void update_ctr(cookies_ctx_t *ctx)
+{
+ assert(ctx);
+
+ if (ATOMIC_GET(ctx->badcookie_ctr) < ctx->badcookie_slip) {
+ ATOMIC_ADD(ctx->badcookie_ctr, 1);
+ } else {
+ ATOMIC_SET(ctx->badcookie_ctr, BADCOOKIE_CTR_INIT);
+ }
+}
+
+static int generate_secret(cookies_ctx_t *ctx)
+{
+ assert(ctx);
+
+ // Generate a new variable part of the server secret.
+ uint64_t new_secret;
+ int ret = dnssec_random_buffer((uint8_t *)&new_secret, sizeof(new_secret));
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ATOMIC_SET(ctx->secret.variable, new_secret);
+
+ return KNOT_EOK;
+}
+
+static void *update_secret(void *data)
+{
+ knotd_mod_t *mod = (knotd_mod_t *)data;
+ cookies_ctx_t *ctx = knotd_mod_ctx(mod);
+
+ while (true) {
+ pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
+ int ret = generate_secret(ctx);
+ if (ret != KNOT_EOK) {
+ knotd_mod_log(mod, LOG_ERR, "failed to generate a secret (%s)",
+ knot_strerror(ret));
+ }
+ pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
+ sleep(ctx->secret_lifetime);
+ }
+
+ return NULL;
+}
+
+// Inserts the current cookie option into the answer's OPT RR.
+static int put_cookie(knotd_qdata_t *qdata, knot_pkt_t *pkt,
+ const knot_edns_cookie_t *cc, const knot_edns_cookie_t *sc)
+{
+ assert(qdata && pkt && cc && sc);
+
+ uint8_t *option = NULL;
+ uint16_t option_size = knot_edns_cookie_size(cc, sc);
+ int ret = knot_edns_reserve_option(&qdata->opt_rr, KNOT_EDNS_OPTION_COOKIE,
+ option_size, &option, qdata->mm);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = knot_edns_cookie_write(option, option_size, cc, sc);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Reserve extra space for the cookie option.
+ ret = knot_pkt_reserve(pkt, KNOT_EDNS_OPTION_HDRLEN + option_size);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ return KNOT_EOK;
+}
+
+static knotd_state_t cookies_process(knotd_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata && mod);
+
+ cookies_ctx_t *ctx = knotd_mod_ctx(mod);
+
+ // Check if the cookie option is present.
+ uint8_t *cookie_opt = knot_pkt_edns_option(qdata->query,
+ KNOT_EDNS_OPTION_COOKIE);
+ if (cookie_opt == NULL) {
+ return state;
+ }
+
+ // Increment the statistics counter.
+ knotd_mod_stats_incr(mod, qdata->params->thread_id, 0, 0, 1);
+
+ knot_edns_cookie_t cc;
+ knot_edns_cookie_t sc;
+
+ // Parse the cookie from wireformat.
+ const uint8_t *data = knot_edns_opt_get_data(cookie_opt);
+ uint16_t data_len = knot_edns_opt_get_length(cookie_opt);
+ int ret = knot_edns_cookie_parse(&cc, &sc, data, data_len);
+ if (ret != KNOT_EOK) {
+ qdata->rcode = KNOT_RCODE_FORMERR;
+ return KNOTD_STATE_FAIL;
+ }
+
+ // Prepare data for server cookie computation.
+ knot_edns_cookie_params_t params = {
+ .version = KNOT_EDNS_COOKIE_VERSION,
+ .timestamp = (uint32_t)time(NULL),
+ .lifetime_before = 3600,
+ .lifetime_after = 300,
+ .client_addr = knotd_qdata_remote_addr(qdata)
+ };
+ uint64_t current_secret = ATOMIC_GET(ctx->secret.variable);
+ memcpy(params.secret, &current_secret, sizeof(current_secret));
+ memcpy(params.secret + sizeof(current_secret), &ctx->secret.constant,
+ sizeof(ctx->secret.constant));
+
+ // Compare server cookie.
+ ret = knot_edns_cookie_server_check(&sc, &cc, &params);
+ if (ret != KNOT_EOK) {
+ // Established connection (TCP or QUIC) is taken into account,
+ // so a normal response is provided.
+ if (qdata->params->proto != KNOTD_QUERY_PROTO_UDP) {
+ if (knot_edns_cookie_server_generate(&sc, &cc, &params) != KNOT_EOK ||
+ put_cookie(qdata, pkt, &cc, &sc) != KNOT_EOK)
+ {
+ return KNOTD_STATE_FAIL;
+ }
+
+ return state;
+ } else if (ATOMIC_GET(ctx->badcookie_ctr) > BADCOOKIE_CTR_INIT) {
+ // Silently drop the response.
+ update_ctr(ctx);
+ knotd_mod_stats_incr(mod, qdata->params->thread_id, 1, 0, 1);
+ return KNOTD_STATE_NOOP;
+ } else {
+ if (ctx->badcookie_slip > 1) {
+ update_ctr(ctx);
+ }
+
+ if (knot_edns_cookie_server_generate(&sc, &cc, &params) != KNOT_EOK ||
+ put_cookie(qdata, pkt, &cc, &sc) != KNOT_EOK)
+ {
+ return KNOTD_STATE_FAIL;
+ }
+
+ qdata->rcode = KNOT_RCODE_BADCOOKIE;
+ return KNOTD_STATE_FAIL;
+ }
+ }
+
+ // Reuse valid server cookie.
+ ret = put_cookie(qdata, pkt, &cc, &sc);
+ if (ret != KNOT_EOK) {
+ return KNOTD_STATE_FAIL;
+ }
+
+ // Set the valid cookie flag.
+ qdata->params->flags |= KNOTD_QUERY_FLAG_COOKIE;
+
+ return state;
+}
+
+int cookies_load(knotd_mod_t *mod)
+{
+ // Create module context.
+ cookies_ctx_t *ctx = calloc(1, sizeof(cookies_ctx_t));
+ if (ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Initialize BADCOOKIE counter.
+ ctx->badcookie_ctr = BADCOOKIE_CTR_INIT;
+
+ // Set up configurable items.
+ knotd_conf_t conf = knotd_conf_mod(mod, MOD_BADCOOKIE_SLIP);
+ ctx->badcookie_slip = conf.single.integer;
+
+ // Set up statistics counters.
+ int ret = knotd_mod_stats_add(mod, "presence", 1, NULL);
+ if (ret != KNOT_EOK) {
+ free(ctx);
+ return ret;
+ }
+
+ ret = knotd_mod_stats_add(mod, "dropped", 1, NULL);
+ if (ret != KNOT_EOK) {
+ free(ctx);
+ return ret;
+ }
+
+ // Store module context before rollover thread is created.
+ knotd_mod_ctx_set(mod, ctx);
+
+ // Initialize the server secret.
+ conf = knotd_conf_mod(mod, MOD_SECRET);
+ if (conf.count == 1) {
+ assert(conf.single.data_len == KNOT_EDNS_COOKIE_SECRET_SIZE);
+ memcpy(&ctx->secret, conf.single.data, conf.single.data_len);
+ assert(ctx->secret_lifetime == 0);
+ } else {
+ ret = dnssec_random_buffer((uint8_t *)&ctx->secret, sizeof(ctx->secret));
+ if (ret != KNOT_EOK) {
+ free(ctx);
+ return ret;
+ }
+
+ conf = knotd_conf_mod(mod, MOD_SECRET_LIFETIME);
+ ctx->secret_lifetime = conf.single.integer;
+
+ // Start the secret rollover thread.
+ if (pthread_create(&ctx->update_secret, NULL, update_secret, (void *)mod)) {
+ knotd_mod_log(mod, LOG_ERR, "failed to create the secret rollover thread");
+ free(ctx);
+ return KNOT_ERROR;
+ }
+ }
+
+#ifndef HAVE_ATOMIC
+ knotd_mod_log(mod, LOG_WARNING, "the module might work slightly wrong on this platform");
+ ctx->badcookie_slip = 1;
+#endif
+
+ return knotd_mod_hook(mod, KNOTD_STAGE_BEGIN, cookies_process);
+}
+
+void cookies_unload(knotd_mod_t *mod)
+{
+ cookies_ctx_t *ctx = knotd_mod_ctx(mod);
+ if (ctx->secret_lifetime > 0) {
+ (void)pthread_cancel(ctx->update_secret);
+ (void)pthread_join(ctx->update_secret, NULL);
+ }
+ memzero(&ctx->secret, sizeof(ctx->secret));
+ free(ctx);
+}
+
+KNOTD_MOD_API(cookies, KNOTD_MOD_FLAG_SCOPE_ANY | KNOTD_MOD_FLAG_OPT_CONF,
+ cookies_load, cookies_unload, cookies_conf, cookies_conf_check);
diff --git a/src/knot/modules/cookies/cookies.rst b/src/knot/modules/cookies/cookies.rst
new file mode 100644
index 0000000..74bffe5
--- /dev/null
+++ b/src/knot/modules/cookies/cookies.rst
@@ -0,0 +1,110 @@
+.. _mod-cookies:
+
+``cookies`` — DNS Cookies
+=========================
+
+DNS Cookies (:rfc:`7873`) is a lightweight security mechanism against
+denial-of-service and amplification attacks. The server keeps a secret value
+(the Server Secret), which is used to generate a cookie, which is sent to
+the client in the OPT RR. The server then verifies the authenticity of the client
+by the presence of a correct cookie. Both the server and the client have to
+support DNS Cookies, otherwise they are not used.
+
+.. NOTE::
+ This module introduces two statistics counters:
+
+ - ``presence`` – The number of queries containing the COOKIE option.
+ - ``dropped`` – The number of dropped queries due to the slip limit.
+
+.. WARNING::
+ For effective module operation the :ref:`RRL<mod-rrl>` module must also
+ be enabled and configured after :ref:`Cookies<mod-cookies>`. See
+ :ref:`query-modules` how to configure modules.
+
+Example
+-------
+
+It is recommended to enable DNS Cookies globally, not per zone. The module may be used without any further configuration.
+
+::
+
+ template:
+ - id: default
+ global-module: mod-cookies # Enable DNS Cookies globally
+
+Module configuration may be supplied if necessary.
+
+::
+
+ mod-cookies:
+ - id: default
+ secret-lifetime: 30h # The Server Secret is regenerated every 30 hours
+ badcookie-slip: 3 # The server replies only to every third query with a wrong cookie
+
+ template:
+ - id: default
+ global-module: mod-cookies/default # Enable DNS Cookies globally
+
+The value of the Server Secret may also be managed manually using the :ref:`mod-cookies_secret` option. In this case
+the server does not automatically regenerate the Server Secret.
+
+::
+
+ mod-cookies:
+ - id: default
+ secret: 0xdeadbeefdeadbeefdeadbeefdeadbeef
+
+Module reference
+----------------
+
+::
+
+ mod-cookies:
+ - id: STR
+ secret-lifetime: TIME
+ badcookie-slip: INT
+ secret: STR | HEXSTR
+
+.. _mod-cookies_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-cookies_secret-lifetime:
+
+secret-lifetime
+...............
+
+This option configures in seconds how often the Server Secret is regenerated.
+The maximum allowed value is 36 days (:rfc:`7873#section-7.1`).
+
+*Default:* ``26h`` (26 hours)
+
+.. _mod-cookies_badcookie-slip:
+
+badcookie-slip
+..............
+
+This option configures how often the server responds to queries containing
+an invalid cookie by sending them the correct cookie.
+
+- The value **1** means that the server responds to every query.
+- The value **2** means that the server responds to every second query with
+ an invalid cookie, the rest of the queries is dropped.
+- The value **N > 2** means that the server responds to every N\ :sup:`th`
+ query with an invalid cookie, the rest of the queries is dropped.
+
+*Default:* ``1``
+
+.. _mod-cookies_secret:
+
+secret
+......
+
+Use this option to set the Server Secret manually. If this option is used, the
+Server Secret remains the same until changed manually and the :ref:`mod-cookies_secret-lifetime` option is ignored.
+The size of the Server Secret currently MUST BE 16 bytes, or 32 hexadecimal characters.
+
+*Default:* not set
diff --git a/src/knot/modules/dnsproxy/Makefile.inc b/src/knot/modules/dnsproxy/Makefile.inc
new file mode 100644
index 0000000..86f1577
--- /dev/null
+++ b/src/knot/modules/dnsproxy/Makefile.inc
@@ -0,0 +1,13 @@
+knot_modules_dnsproxy_la_SOURCES = knot/modules/dnsproxy/dnsproxy.c
+EXTRA_DIST += knot/modules/dnsproxy/dnsproxy.rst
+
+if STATIC_MODULE_dnsproxy
+libknotd_la_SOURCES += $(knot_modules_dnsproxy_la_SOURCES)
+endif
+
+if SHARED_MODULE_dnsproxy
+knot_modules_dnsproxy_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_dnsproxy_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+knot_modules_dnsproxy_la_LIBADD = $(libcontrib_LIBS)
+pkglib_LTLIBRARIES += knot/modules/dnsproxy.la
+endif
diff --git a/src/knot/modules/dnsproxy/dnsproxy.c b/src/knot/modules/dnsproxy/dnsproxy.c
new file mode 100644
index 0000000..b44b136
--- /dev/null
+++ b/src/knot/modules/dnsproxy/dnsproxy.c
@@ -0,0 +1,191 @@
+/* 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 "contrib/net.h"
+#include "knot/include/module.h"
+#include "knot/conf/schema.h"
+#include "knot/query/capture.h" // Forces static module!
+#include "knot/query/requestor.h" // Forces static module!
+
+#define MOD_REMOTE "\x06""remote"
+#define MOD_ADDRESS "\x07""address"
+#define MOD_TCP_FASTOPEN "\x0C""tcp-fastopen"
+#define MOD_TIMEOUT "\x07""timeout"
+#define MOD_FALLBACK "\x08""fallback"
+#define MOD_CATCH_NXDOMAIN "\x0E""catch-nxdomain"
+
+const yp_item_t dnsproxy_conf[] = {
+ { MOD_REMOTE, YP_TREF, YP_VREF = { C_RMT }, YP_FNONE,
+ { knotd_conf_check_ref } },
+ { MOD_TIMEOUT, YP_TINT, YP_VINT = { 0, INT32_MAX, 500 } },
+ { MOD_ADDRESS, YP_TNET, YP_VNONE, YP_FMULTI },
+ { MOD_FALLBACK, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_TCP_FASTOPEN, YP_TBOOL, YP_VNONE },
+ { MOD_CATCH_NXDOMAIN, YP_TBOOL, YP_VNONE },
+ { NULL }
+};
+
+int dnsproxy_conf_check(knotd_conf_check_args_t *args)
+{
+ knotd_conf_t rmt = knotd_conf_check_item(args, MOD_REMOTE);
+ if (rmt.count == 0) {
+ args->err_str = "no remote server specified";
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+typedef struct {
+ struct sockaddr_storage remote;
+ struct sockaddr_storage via;
+ knotd_conf_t addr;
+ bool fallback;
+ bool tfo;
+ bool catch_nxdomain;
+ int timeout;
+} dnsproxy_t;
+
+static knotd_state_t dnsproxy_fwd(knotd_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata && mod);
+
+ dnsproxy_t *proxy = knotd_mod_ctx(mod);
+
+ /* Forward only queries ending with REFUSED (no zone) or NXDOMAIN (if configured) */
+ if (proxy->fallback && !(qdata->rcode == KNOT_RCODE_REFUSED ||
+ (qdata->rcode == KNOT_RCODE_NXDOMAIN && proxy->catch_nxdomain))) {
+ return state;
+ }
+
+ /* Forward from specified addresses only if configured. */
+ if (proxy->addr.count > 0) {
+ const struct sockaddr_storage *addr = knotd_qdata_remote_addr(qdata);
+ if (!knotd_conf_addr_range_match(&proxy->addr, addr)) {
+ return state;
+ }
+ }
+
+ /* Forward also original TSIG. */
+ if (qdata->query->tsig_rr != NULL && !proxy->fallback) {
+ knot_tsig_append(qdata->query->wire, &qdata->query->size,
+ qdata->query->max_size, qdata->query->tsig_rr);
+ }
+
+ /* Capture layer context. */
+ const knot_layer_api_t *capture = query_capture_api();
+ struct capture_param capture_param = {
+ .sink = pkt
+ };
+
+ /* Create a forwarding request. */
+ knot_requestor_t re;
+ int ret = knot_requestor_init(&re, capture, &capture_param, qdata->mm);
+ if (ret != KNOT_EOK) {
+ return state; /* Ignore, not enough memory. */
+ }
+
+ knot_request_flag_t flags = KNOT_REQUEST_NONE;
+ if (!net_is_stream(qdata->params->socket)) {
+ flags = KNOT_REQUEST_UDP;
+ } else if (proxy->tfo) {
+ flags = KNOT_REQUEST_TFO;
+ }
+ const struct sockaddr_storage *dst = &proxy->remote;
+ const struct sockaddr_storage *src = &proxy->via;
+ knot_request_t *req = knot_request_make(re.mm, dst, src, qdata->query, NULL,
+ flags);
+ if (req == NULL) {
+ knot_requestor_clear(&re);
+ return state; /* Ignore, not enough memory. */
+ }
+
+ /* Forward request. */
+ ret = knot_requestor_exec(&re, req, proxy->timeout);
+
+ knot_request_free(req, re.mm);
+ knot_requestor_clear(&re);
+
+ /* Check result. */
+ if (ret != KNOT_EOK) {
+ qdata->rcode = KNOT_RCODE_SERVFAIL;
+ return KNOTD_STATE_FAIL; /* Forwarding failed, SERVFAIL. */
+ } else {
+ qdata->rcode = knot_pkt_ext_rcode(pkt);
+ }
+
+ /* Respond also with TSIG. */
+ if (pkt->tsig_rr != NULL && !proxy->fallback) {
+ knot_tsig_append(pkt->wire, &pkt->size, pkt->max_size, pkt->tsig_rr);
+ }
+
+ return (proxy->fallback ? KNOTD_STATE_DONE : KNOTD_STATE_FINAL);
+}
+
+int dnsproxy_load(knotd_mod_t *mod)
+{
+ dnsproxy_t *proxy = calloc(1, sizeof(*proxy));
+ if (proxy == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ knotd_conf_t remote = knotd_conf_mod(mod, MOD_REMOTE);
+ knotd_conf_t conf = knotd_conf(mod, C_RMT, C_ADDR, &remote);
+ if (conf.count > 0) {
+ proxy->remote = conf.multi[0].addr;
+ knotd_conf_free(&conf);
+ }
+ conf = knotd_conf(mod, C_RMT, C_VIA, &remote);
+ if (conf.count > 0) {
+ proxy->via = conf.multi[0].addr;
+ knotd_conf_free(&conf);
+ }
+
+ proxy->addr = knotd_conf_mod(mod, MOD_ADDRESS);
+
+ conf = knotd_conf_mod(mod, MOD_TIMEOUT);
+ proxy->timeout = conf.single.integer;
+
+ conf = knotd_conf_mod(mod, MOD_FALLBACK);
+ proxy->fallback = conf.single.boolean;
+
+ conf = knotd_conf_mod(mod, MOD_TCP_FASTOPEN);
+ proxy->tfo = conf.single.boolean;
+
+ conf = knotd_conf_mod(mod, MOD_CATCH_NXDOMAIN);
+ proxy->catch_nxdomain = conf.single.boolean;
+
+ knotd_mod_ctx_set(mod, proxy);
+
+ if (proxy->fallback) {
+ return knotd_mod_hook(mod, KNOTD_STAGE_END, dnsproxy_fwd);
+ } else {
+ return knotd_mod_hook(mod, KNOTD_STAGE_BEGIN, dnsproxy_fwd);
+ }
+}
+
+void dnsproxy_unload(knotd_mod_t *mod)
+{
+ dnsproxy_t *ctx = knotd_mod_ctx(mod);
+ if (ctx != NULL) {
+ knotd_conf_free(&ctx->addr);
+ }
+ free(ctx);
+}
+
+KNOTD_MOD_API(dnsproxy, KNOTD_MOD_FLAG_SCOPE_ANY,
+ dnsproxy_load, dnsproxy_unload, dnsproxy_conf, dnsproxy_conf_check);
diff --git a/src/knot/modules/dnsproxy/dnsproxy.rst b/src/knot/modules/dnsproxy/dnsproxy.rst
new file mode 100644
index 0000000..9493738
--- /dev/null
+++ b/src/knot/modules/dnsproxy/dnsproxy.rst
@@ -0,0 +1,125 @@
+.. _mod-dnsproxy:
+
+``dnsproxy`` – Tiny DNS proxy
+=============================
+
+The module forwards all queries, or all specific zone queries if configured
+per zone, to the indicated server for resolution. If configured in the fallback
+mode, only locally unsatisfied queries are forwarded. I.e. a tiny DNS proxy.
+There are several uses of this feature:
+
+* A substitute public-facing server in front of the real one
+* Local zones (poor man's "views"), rest is forwarded to the public-facing server
+* Using the fallback to forward queries to a resolver
+* etc.
+
+.. NOTE::
+ The module does not alter the query/response as the resolver would,
+ and the original transport protocol is kept as well.
+
+Example
+-------
+
+The configuration is straightforward and just a single remote server is
+required::
+
+ remote:
+ - id: hidden
+ address: 10.0.1.1
+
+ mod-dnsproxy:
+ - id: default
+ remote: hidden
+ fallback: on
+
+ template:
+ - id: default
+ global-module: mod-dnsproxy/default
+
+ zone:
+ - domain: local.zone
+
+When clients query for anything in the ``local.zone``, they will be
+responded to locally. The rest of the requests will be forwarded to the
+specified server (``10.0.1.1`` in this case).
+
+Module reference
+----------------
+
+::
+
+ mod-dnsproxy:
+ - id: STR
+ remote: remote_id
+ timeout: INT
+ address: ADDR[/INT] | ADDR-ADDR ...
+ fallback: BOOL
+ tcp-fastopen: BOOL
+ catch-nxdomain: BOOL
+
+.. _mod-dnsproxy_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-dnsproxy_remote:
+
+remote
+......
+
+A :ref:`reference<remote_id>` to a remote server where the queries are
+forwarded to.
+
+*Required*
+
+.. _mod-dnsproxy_timeout:
+
+timeout
+.......
+
+A remote response timeout in milliseconds.
+
+*Default:* ``500`` (milliseconds)
+
+.. _mod-dnsproxy_address:
+
+address
+.......
+
+An optional list of allowed ranges and/or subnets for query's source address.
+If the query's address does not fall into any of the configured ranges, the
+query isn't forwarded.
+
+*Default:* not set
+
+.. _mod-dnsproxy_fallback:
+
+fallback
+........
+
+If enabled, locally unsatisfied queries leading to REFUSED (no zone) are forwarded.
+If disabled, all queries are directly forwarded without any local attempts
+to resolve them.
+
+*Default:* ``on``
+
+.. _mod-dnsproxy_tcp-fastopen:
+
+tcp-fastopen
+............
+
+If enabled, TCP Fast Open is used when forwarding TCP queries.
+
+*Default:* ``off``
+
+.. _mod-dnsproxy_catch-nxdomain:
+
+catch-nxdomain
+..............
+
+If enabled, locally unsatisfied queries leading to NXDOMAIN are forwarded.
+This option is only relevant in the fallback mode.
+
+*Default:* ``off``
diff --git a/src/knot/modules/dnstap/Makefile.inc b/src/knot/modules/dnstap/Makefile.inc
new file mode 100644
index 0000000..e69b56c
--- /dev/null
+++ b/src/knot/modules/dnstap/Makefile.inc
@@ -0,0 +1,15 @@
+knot_modules_dnstap_la_SOURCES = knot/modules/dnstap/dnstap.c
+EXTRA_DIST += knot/modules/dnstap/dnstap.rst
+
+if STATIC_MODULE_dnstap
+libknotd_la_SOURCES += $(knot_modules_dnstap_la_SOURCES)
+libknotd_la_CPPFLAGS += $(DNSTAP_CFLAGS)
+libknotd_la_LIBADD += $(libdnstap_LIBS)
+endif
+
+if SHARED_MODULE_dnstap
+knot_modules_dnstap_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_dnstap_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS) $(DNSTAP_CFLAGS)
+knot_modules_dnstap_la_LIBADD = $(libdnstap_LIBS)
+pkglib_LTLIBRARIES += knot/modules/dnstap.la
+endif
diff --git a/src/knot/modules/dnstap/dnstap.c b/src/knot/modules/dnstap/dnstap.c
new file mode 100644
index 0000000..6119ccd
--- /dev/null
+++ b/src/knot/modules/dnstap/dnstap.c
@@ -0,0 +1,338 @@
+/* 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 <netinet/in.h>
+#include <sys/socket.h>
+
+#include "contrib/dnstap/dnstap.h"
+#include "contrib/dnstap/dnstap.pb-c.h"
+#include "contrib/dnstap/message.h"
+#include "contrib/dnstap/writer.h"
+#include "contrib/time.h"
+#include "knot/include/module.h"
+
+#define MOD_SINK "\x04""sink"
+#define MOD_IDENTITY "\x08""identity"
+#define MOD_VERSION "\x07""version"
+#define MOD_QUERIES "\x0B""log-queries"
+#define MOD_RESPONSES "\x0D""log-responses"
+#define MOD_WITH_QUERIES "\x16""responses-with-queries"
+
+const yp_item_t dnstap_conf[] = {
+ { MOD_SINK, YP_TSTR, YP_VNONE },
+ { MOD_IDENTITY, YP_TSTR, YP_VNONE },
+ { MOD_VERSION, YP_TSTR, YP_VNONE },
+ { MOD_QUERIES, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_RESPONSES, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_WITH_QUERIES, YP_TBOOL, YP_VBOOL = { false } },
+ { NULL }
+};
+
+int dnstap_conf_check(knotd_conf_check_args_t *args)
+{
+ knotd_conf_t sink = knotd_conf_check_item(args, MOD_SINK);
+ if (sink.count == 0 || sink.single.string[0] == '\0') {
+ args->err_str = "no sink specified";
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+typedef struct {
+ struct fstrm_iothr *iothread;
+ char *identity;
+ size_t identity_len;
+ char *version;
+ size_t version_len;
+ bool with_queries;
+} dnstap_ctx_t;
+
+static knotd_state_t log_message(knotd_state_t state, const knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata && mod);
+
+ /* Skip empty packet. */
+ if (state == KNOTD_STATE_NOOP) {
+ return state;
+ }
+
+ dnstap_ctx_t *ctx = knotd_mod_ctx(mod);
+
+ struct fstrm_iothr_queue *ioq =
+ fstrm_iothr_get_input_queue_idx(ctx->iothread, qdata->params->thread_id);
+
+ /* Unless we want to measure the time it takes to process each query,
+ * we can treat Q/R times the same. */
+ struct timespec tv = { 0 };
+ clock_gettime(CLOCK_REALTIME, &tv);
+
+ /* Determine query / response. */
+ Dnstap__Message__Type msgtype = DNSTAP__MESSAGE__TYPE__AUTH_QUERY;
+ if (knot_wire_get_qr(pkt->wire)) {
+ msgtype = DNSTAP__MESSAGE__TYPE__AUTH_RESPONSE;
+ }
+
+ /* Determine whether we run on UDP/TCP. */
+ /* TODO: distinguish QUIC. */
+ int protocol = IPPROTO_UDP;
+ if (qdata->params->proto == KNOTD_QUERY_PROTO_TCP) {
+ protocol = IPPROTO_TCP;
+ }
+
+ /* Create a dnstap message. */
+ struct sockaddr_storage buff;
+ Dnstap__Message msg;
+ int ret = dt_message_fill(&msg, msgtype,
+ (const struct sockaddr *)knotd_qdata_remote_addr(qdata),
+ (const struct sockaddr *)knotd_qdata_local_addr(qdata, &buff),
+ protocol, pkt->wire, pkt->size, &tv);
+ if (ret != KNOT_EOK) {
+ return state;
+ }
+
+ Dnstap__Dnstap dnstap = DNSTAP__DNSTAP__INIT;
+ dnstap.type = DNSTAP__DNSTAP__TYPE__MESSAGE;
+ dnstap.message = &msg;
+
+ /* Set message version and identity. */
+ if (ctx->identity_len > 0) {
+ dnstap.identity.data = (uint8_t *)ctx->identity;
+ dnstap.identity.len = ctx->identity_len;
+ dnstap.has_identity = 1;
+ }
+ if (ctx->version_len > 0) {
+ dnstap.version.data = (uint8_t *)ctx->version;
+ dnstap.version.len = ctx->version_len;
+ dnstap.has_version = 1;
+ }
+
+ /* Also add query message if 'responses-with-queries' is enabled and this is a response. */
+ if (ctx->with_queries &&
+ msgtype == DNSTAP__MESSAGE__TYPE__AUTH_RESPONSE &&
+ qdata->query != NULL)
+ {
+ msg.query_message.len = qdata->query->size;
+ msg.query_message.data = qdata->query->wire;
+ msg.has_query_message = 1;
+ }
+
+ /* Pack the message. */
+ uint8_t *frame = NULL;
+ size_t size = 0;
+ dt_pack(&dnstap, &frame, &size);
+ if (frame == NULL) {
+ return state;
+ }
+
+ /* Submit a request. */
+ fstrm_res res = fstrm_iothr_submit(ctx->iothread, ioq, frame, size,
+ fstrm_free_wrapper, NULL);
+ if (res != fstrm_res_success) {
+ free(frame);
+ return state;
+ }
+
+ return state;
+}
+
+/*! \brief Submit message - query. */
+static knotd_state_t dnstap_message_log_query(knotd_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(qdata);
+
+ return log_message(state, qdata->query, qdata, mod);
+}
+
+/*! \brief Submit message - response. */
+static knotd_state_t dnstap_message_log_response(knotd_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ return log_message(state, pkt, qdata, mod);
+}
+
+/*! \brief Create a UNIX socket sink. */
+static struct fstrm_writer* dnstap_unix_writer(const char *path)
+{
+ struct fstrm_unix_writer_options *opt = NULL;
+ struct fstrm_writer_options *wopt = NULL;
+ struct fstrm_writer *writer = NULL;
+
+ opt = fstrm_unix_writer_options_init();
+ if (opt == NULL) {
+ goto finish;
+ }
+ fstrm_unix_writer_options_set_socket_path(opt, path);
+
+ wopt = fstrm_writer_options_init();
+ if (wopt == NULL) {
+ goto finish;
+ }
+ fstrm_writer_options_add_content_type(wopt, DNSTAP_CONTENT_TYPE,
+ strlen(DNSTAP_CONTENT_TYPE));
+ writer = fstrm_unix_writer_init(opt, wopt);
+
+finish:
+ fstrm_unix_writer_options_destroy(&opt);
+ fstrm_writer_options_destroy(&wopt);
+ return writer;
+}
+
+/*! \brief Create a basic file writer sink. */
+static struct fstrm_writer* dnstap_file_writer(const char *path)
+{
+ struct fstrm_file_options *fopt = NULL;
+ struct fstrm_writer_options *wopt = NULL;
+ struct fstrm_writer *writer = NULL;
+
+ fopt = fstrm_file_options_init();
+ if (fopt == NULL) {
+ goto finish;
+ }
+ fstrm_file_options_set_file_path(fopt, path);
+
+ wopt = fstrm_writer_options_init();
+ if (wopt == NULL) {
+ goto finish;
+ }
+ fstrm_writer_options_add_content_type(wopt, DNSTAP_CONTENT_TYPE,
+ strlen(DNSTAP_CONTENT_TYPE));
+ writer = fstrm_file_writer_init(fopt, wopt);
+
+finish:
+ fstrm_file_options_destroy(&fopt);
+ fstrm_writer_options_destroy(&wopt);
+ return writer;
+}
+
+/*! \brief Create a log sink according to the path string. */
+static struct fstrm_writer* dnstap_writer(const char *path)
+{
+ const char *prefix = "unix:";
+ const size_t prefix_len = strlen(prefix);
+
+ /* UNIX socket prefix. */
+ if (strlen(path) > prefix_len && strncmp(path, prefix, prefix_len) == 0) {
+ return dnstap_unix_writer(path + prefix_len);
+ }
+
+ return dnstap_file_writer(path);
+}
+
+int dnstap_load(knotd_mod_t *mod)
+{
+ /* Create dnstap context. */
+ dnstap_ctx_t *ctx = calloc(1, sizeof(*ctx));
+ if (ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ /* Set identity. */
+ knotd_conf_t conf = knotd_conf_mod(mod, MOD_IDENTITY);
+ if (conf.count == 1) {
+ ctx->identity = (conf.single.string != NULL) ?
+ strdup(conf.single.string) : NULL;
+ } else {
+ knotd_conf_t host = knotd_conf_env(mod, KNOTD_CONF_ENV_HOSTNAME);
+ ctx->identity = strdup(host.single.string);
+ }
+ ctx->identity_len = (ctx->identity != NULL) ? strlen(ctx->identity) : 0;
+
+ /* Set version. */
+ conf = knotd_conf_mod(mod, MOD_VERSION);
+ if (conf.count == 1) {
+ ctx->version = (conf.single.string != NULL) ?
+ strdup(conf.single.string) : NULL;
+ } else {
+ knotd_conf_t version = knotd_conf_env(mod, KNOTD_CONF_ENV_VERSION);
+ ctx->version = strdup(version.single.string);
+ }
+ ctx->version_len = (ctx->version != NULL) ? strlen(ctx->version) : 0;
+
+ /* Set responses-with-queries. */
+ conf = knotd_conf_mod(mod, MOD_WITH_QUERIES);
+ ctx->with_queries = conf.single.boolean;
+
+ /* Set sink. */
+ conf = knotd_conf_mod(mod, MOD_SINK);
+ const char *sink = conf.single.string;
+
+ /* Set log_queries. */
+ conf = knotd_conf_mod(mod, MOD_QUERIES);
+ const bool log_queries = conf.single.boolean;
+
+ /* Set log_responses. */
+ conf = knotd_conf_mod(mod, MOD_RESPONSES);
+ const bool log_responses = conf.single.boolean;
+
+ /* Initialize the writer and the options. */
+ struct fstrm_writer *writer = dnstap_writer(sink);
+ if (writer == NULL) {
+ goto fail;
+ }
+
+ struct fstrm_iothr_options *opt = fstrm_iothr_options_init();
+ if (opt == NULL) {
+ fstrm_writer_destroy(&writer);
+ goto fail;
+ }
+
+ /* Initialize queues. */
+ fstrm_iothr_options_set_num_input_queues(opt, knotd_mod_threads(mod));
+
+ /* Create the I/O thread. */
+ ctx->iothread = fstrm_iothr_init(opt, &writer);
+ fstrm_iothr_options_destroy(&opt);
+ if (ctx->iothread == NULL) {
+ fstrm_writer_destroy(&writer);
+ goto fail;
+ }
+
+ knotd_mod_ctx_set(mod, ctx);
+
+ /* Hook to the query plan. */
+ if (log_queries) {
+ knotd_mod_hook(mod, KNOTD_STAGE_BEGIN, dnstap_message_log_query);
+ }
+ if (log_responses) {
+ knotd_mod_hook(mod, KNOTD_STAGE_END, dnstap_message_log_response);
+ }
+
+ return KNOT_EOK;
+fail:
+ knotd_mod_log(mod, LOG_ERR, "failed to init sink '%s'", sink);
+
+ free(ctx->identity);
+ free(ctx->version);
+ free(ctx);
+
+ return KNOT_ENOMEM;
+}
+
+void dnstap_unload(knotd_mod_t *mod)
+{
+ dnstap_ctx_t *ctx = knotd_mod_ctx(mod);
+
+ fstrm_iothr_destroy(&ctx->iothread);
+ free(ctx->identity);
+ free(ctx->version);
+ free(ctx);
+}
+
+KNOTD_MOD_API(dnstap, KNOTD_MOD_FLAG_SCOPE_ANY,
+ dnstap_load, dnstap_unload, dnstap_conf, dnstap_conf_check);
diff --git a/src/knot/modules/dnstap/dnstap.rst b/src/knot/modules/dnstap/dnstap.rst
new file mode 100644
index 0000000..591bda5
--- /dev/null
+++ b/src/knot/modules/dnstap/dnstap.rst
@@ -0,0 +1,113 @@
+.. _mod-dnstap:
+
+``dnstap`` – Dnstap traffic logging
+===================================
+
+A module for query and response logging based on the dnstap_ library.
+You can capture either all or zone-specific queries and responses; usually
+you want to do the former.
+
+Example
+-------
+
+The configuration comprises only a :ref:`mod-dnstap_sink` path parameter,
+which can be either a file or a UNIX socket::
+
+ mod-dnstap:
+ - id: capture_all
+ sink: /tmp/capture.tap
+
+ template:
+ - id: default
+ global-module: mod-dnstap/capture_all
+
+.. NOTE::
+ To be able to use a Unix socket you need an external program to create it.
+ Knot DNS connects to it as a client using the libfstrm library. It operates
+ exactly like syslog.
+
+.. NOTE::
+ Dnstap log files can also be created or read using :doc:`kdig<man_kdig>`.
+
+.. _dnstap: https://dnstap.info/
+
+Module reference
+----------------
+
+For all queries logging, use this module in the *default* template. For
+zone-specific logging, use this module in the proper zone configuration.
+
+::
+
+ mod-dnstap:
+ - id: STR
+ sink: STR
+ identity: STR
+ version: STR
+ log-queries: BOOL
+ log-responses: BOOL
+ responses-with-queries: BOOL
+
+.. _mod-dnstap_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-dnstap_sink:
+
+sink
+....
+
+A sink path, which can be either a file or a UNIX socket when prefixed with
+``unix:``.
+
+*Required*
+
+.. WARNING::
+ File is overwritten on server startup or reload.
+
+.. _mod-dnstap_identity:
+
+identity
+........
+
+A DNS server identity. Set empty value to disable.
+
+*Default:* FQDN hostname
+
+.. _mod-dnstap_version:
+
+version
+.......
+
+A DNS server version. Set empty value to disable.
+
+*Default:* server version
+
+.. _mod-dnstap_log-queries:
+
+log-queries
+...........
+
+If enabled, query messages will be logged.
+
+*Default:* ``on``
+
+.. _mod-dnstap_log-responses:
+
+log-responses
+.............
+
+If enabled, response messages will be logged.
+
+*Default:* ``on``
+
+responses-with-queries
+......................
+
+If enabled, dnstap ``AUTH_RESPONSE`` messages will also include the original
+query message as well as the response message sent by the server.
+
+*Default:* ``off``
diff --git a/src/knot/modules/geoip/Makefile.inc b/src/knot/modules/geoip/Makefile.inc
new file mode 100644
index 0000000..9bf65ae
--- /dev/null
+++ b/src/knot/modules/geoip/Makefile.inc
@@ -0,0 +1,17 @@
+knot_modules_geoip_la_SOURCES = knot/modules/geoip/geoip.c \
+ knot/modules/geoip/geodb.c \
+ knot/modules/geoip/geodb.h
+EXTRA_DIST += knot/modules/geoip/geoip.rst
+
+if STATIC_MODULE_geoip
+libknotd_la_SOURCES += $(knot_modules_geoip_la_SOURCES)
+libknotd_la_CPPFLAGS += $(libmaxminddb_CFLAGS)
+libknotd_la_LIBADD += $(libmaxminddb_LIBS)
+endif
+
+if SHARED_MODULE_geoip
+knot_modules_geoip_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_geoip_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS) $(libmaxminddb_CFLAGS)
+knot_modules_geoip_la_LIBADD = $(libcontrib_LIBS) $(libmaxminddb_LIBS)
+pkglib_LTLIBRARIES += knot/modules/geoip.la
+endif
diff --git a/src/knot/modules/geoip/geodb.c b/src/knot/modules/geoip/geodb.c
new file mode 100644
index 0000000..97b6609
--- /dev/null
+++ b/src/knot/modules/geoip/geodb.c
@@ -0,0 +1,216 @@
+/* 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 "knot/modules/geoip/geodb.h"
+#include "contrib/strtonum.h"
+#include "contrib/string.h"
+
+#if HAVE_MAXMINDDB
+static const uint16_t type_map[] = {
+ [GEODB_KEY_ID] = MMDB_DATA_TYPE_UINT32,
+ [GEODB_KEY_TXT] = MMDB_DATA_TYPE_UTF8_STRING
+};
+#endif
+
+int parse_geodb_path(geodb_path_t *path, const char *input)
+{
+ if (path == NULL || input == NULL) {
+ return -1;
+ }
+
+ // Parse optional type of key.
+ path->type = GEODB_KEY_TXT;
+ const char *delim = input;
+ if (input[0] == '(') {
+ delim = strchr(input, ')');
+ if (delim == NULL) {
+ return -1;
+ }
+ input++;
+ char *type = sprintf_alloc("%.*s", (int)(delim - input), input);
+ const knot_lookup_t *table = knot_lookup_by_name(geodb_key_types, type);
+ free(type);
+ if (table == NULL) {
+ return -1;
+ }
+ path->type = table->id;
+ input = delim + 1;
+ }
+
+ // Parse the path.
+ uint16_t len = 0;
+ while (1) {
+ delim = strchr(input, '/');
+ if (delim == NULL) {
+ delim = input + strlen(input);
+ }
+ path->path[len] = malloc(delim - input + 1);
+ if (path->path[len] == NULL) {
+ return -1;
+ }
+ memcpy(path->path[len], input, delim - input);
+ path->path[len][delim - input] = '\0';
+ len++;
+ if (*delim == 0 || len == GEODB_MAX_PATH_LEN) {
+ break;
+ }
+ input = delim + 1;
+ }
+
+ return 0;
+}
+
+int parse_geodb_data(const char *input, void **geodata, uint32_t *geodata_len,
+ uint8_t *geodepth, geodb_path_t *path, uint16_t path_cnt)
+{
+ for (uint16_t i = 0; i < path_cnt; i++) {
+ const char *delim = strchr(input, ';');
+ if (delim == NULL) {
+ delim = input + strlen(input);
+ }
+ uint16_t key_len = delim - input;
+ if (key_len > 0 && !(key_len == 1 && *input == '*')) {
+ *geodepth = i + 1;
+ switch (path[i].type) {
+ case GEODB_KEY_TXT:
+ geodata[i] = malloc(key_len + 1);
+ if (geodata[i] == NULL) {
+ return -1;
+ }
+ memcpy(geodata[i], input, key_len);
+ ((char *)geodata[i])[key_len] = '\0';
+ geodata_len[i] = key_len;
+ break;
+ case GEODB_KEY_ID:
+ geodata[i] = malloc(sizeof(uint32_t));
+ if (geodata[i] == NULL) {
+ return -1;
+ }
+ if (str_to_u32(input, (uint32_t *)geodata[i]) != KNOT_EOK) {
+ return -1;
+ }
+ geodata_len[i] = sizeof(uint32_t);
+ break;
+ default:
+ assert(0);
+ return -1;
+ }
+ }
+ if (*delim == '\0') {
+ break;
+ }
+ input = delim + 1;
+ }
+
+ return 0;
+}
+
+bool geodb_available(void)
+{
+#if HAVE_MAXMINDDB
+ return true;
+#else
+ return false;
+#endif
+}
+
+geodb_t *geodb_open(const char *filename)
+{
+#if HAVE_MAXMINDDB
+ MMDB_s *db = calloc(1, sizeof(MMDB_s));
+ if (db == NULL) {
+ return NULL;
+ }
+ int mmdb_error = MMDB_open(filename, MMDB_MODE_MMAP, db);
+ if (mmdb_error != MMDB_SUCCESS) {
+ free(db);
+ return NULL;
+ }
+ return db;
+#else
+ return NULL;
+#endif
+}
+
+void geodb_close(geodb_t *geodb)
+{
+#if HAVE_MAXMINDDB
+ MMDB_close(geodb);
+#endif
+}
+
+int geodb_query(geodb_t *geodb, geodb_data_t *entries, struct sockaddr *remote,
+ geodb_path_t *paths, uint16_t path_cnt, uint16_t *netmask)
+{
+#if HAVE_MAXMINDDB
+ int mmdb_error = 0;
+ MMDB_lookup_result_s res;
+ res = MMDB_lookup_sockaddr(geodb, remote, &mmdb_error);
+ if (mmdb_error != MMDB_SUCCESS || !res.found_entry) {
+ return -1;
+ }
+
+ // Save netmask.
+ *netmask = res.netmask;
+
+ for (uint16_t i = 0; i < path_cnt; i++) {
+ // Get the value of the next key.
+ mmdb_error = MMDB_aget_value(&res.entry, &entries[i], (const char *const*)paths[i].path);
+ if (mmdb_error != MMDB_SUCCESS && mmdb_error != MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR) {
+ return -1;
+ }
+ if (mmdb_error == MMDB_LOOKUP_PATH_DOES_NOT_MATCH_DATA_ERROR || !entries[i].has_data) {
+ entries[i].has_data = false;
+ continue;
+ }
+ // Check the type.
+ if (entries[i].type != type_map[paths[i].type]) {
+ entries[i].has_data = false;
+ continue;
+ }
+ }
+ return 0;
+#else
+ return -1;
+#endif
+}
+
+void geodb_fill_geodata(geodb_data_t *entries, uint16_t path_cnt,
+ void **geodata, uint32_t *geodata_len, uint8_t *geodepth)
+{
+#if HAVE_MAXMINDDB
+ for (int i = 0; i < path_cnt; i++) {
+ if (entries[i].has_data) {
+ *geodepth = i + 1;
+ switch (entries[i].type) {
+ case MMDB_DATA_TYPE_UTF8_STRING:
+ geodata[i] = (void *)entries[i].utf8_string;
+ geodata_len[i] = entries[i].data_size;
+ break;
+ case MMDB_DATA_TYPE_UINT32:
+ geodata[i] = (void *)&entries[i].uint32;
+ geodata_len[i] = sizeof(uint32_t);
+ break;
+ default:
+ assert(0);
+ break;
+ }
+ }
+ }
+#else
+ return;
+#endif
+}
diff --git a/src/knot/modules/geoip/geodb.h b/src/knot/modules/geoip/geodb.h
new file mode 100644
index 0000000..2ec8701
--- /dev/null
+++ b/src/knot/modules/geoip/geodb.h
@@ -0,0 +1,67 @@
+/* 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/libknot.h>
+#if HAVE_MAXMINDDB
+#include <maxminddb.h>
+#endif
+
+#if HAVE_MAXMINDDB
+#define geodb_t MMDB_s
+#define geodb_data_t MMDB_entry_data_s
+#else
+#define geodb_t void
+#define geodb_data_t char
+#endif
+
+// MaxMind DB related constants.
+#define GEODB_MAX_PATH_LEN 8
+#define GEODB_MAX_DEPTH 8
+
+typedef enum {
+ GEODB_KEY_ID,
+ GEODB_KEY_TXT
+} geodb_key_type_t;
+
+static const knot_lookup_t geodb_key_types[] = {
+ { GEODB_KEY_ID, "id" },
+ { GEODB_KEY_TXT, "" },
+ { 0, NULL }
+};
+
+typedef struct {
+ geodb_key_type_t type;
+ char *path[GEODB_MAX_PATH_LEN + 1]; // MMDB_aget_value() requires last member to be NULL.
+} geodb_path_t;
+
+int parse_geodb_path(geodb_path_t *path, const char *input);
+
+int parse_geodb_data(const char *input, void **geodata, uint32_t *geodata_len,
+ uint8_t *geodepth, geodb_path_t *path, uint16_t path_cnt);
+
+bool geodb_available(void);
+
+geodb_t *geodb_open(const char *filename);
+
+void geodb_close(geodb_t *geodb);
+
+int geodb_query(geodb_t *geodb, geodb_data_t *entries, struct sockaddr *remote,
+ geodb_path_t *paths, uint16_t path_cnt, uint16_t *netmask);
+
+void geodb_fill_geodata(geodb_data_t *entries, uint16_t path_cnt,
+ void **geodata, uint32_t *geodata_len, uint8_t *geodepth);
diff --git a/src/knot/modules/geoip/geoip.c b/src/knot/modules/geoip/geoip.c
new file mode 100644
index 0000000..4a8a2e3
--- /dev/null
+++ b/src/knot/modules/geoip/geoip.c
@@ -0,0 +1,1061 @@
+/* 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 <stdio.h>
+#include <string.h>
+#include <arpa/inet.h>
+
+#include "knot/conf/schema.h"
+#include "knot/include/module.h"
+#include "knot/modules/geoip/geodb.h"
+#include "libknot/libknot.h"
+#include "contrib/qp-trie/trie.h"
+#include "contrib/ucw/lists.h"
+#include "contrib/macros.h"
+#include "contrib/sockaddr.h"
+#include "contrib/string.h"
+#include "contrib/strtonum.h"
+#include "libdnssec/random.h"
+#include "libzscanner/scanner.h"
+
+#define MOD_CONFIG_FILE "\x0B""config-file"
+#define MOD_TTL "\x03""ttl"
+#define MOD_MODE "\x04""mode"
+#define MOD_DNSSEC "\x06""dnssec"
+#define MOD_POLICY "\x06""policy"
+#define MOD_GEODB_FILE "\x0A""geodb-file"
+#define MOD_GEODB_KEY "\x09""geodb-key"
+
+enum operation_mode {
+ MODE_SUBNET,
+ MODE_GEODB,
+ MODE_WEIGHTED
+};
+
+static const knot_lookup_t modes[] = {
+ { MODE_SUBNET, "subnet" },
+ { MODE_GEODB, "geodb" },
+ { MODE_WEIGHTED, "weighted" },
+ { 0, NULL }
+};
+
+static const char* mode_key[] = {
+ [MODE_SUBNET] = "net",
+ [MODE_GEODB] = "geo",
+ [MODE_WEIGHTED] = "weight"
+};
+
+const yp_item_t geoip_conf[] = {
+ { MOD_CONFIG_FILE, YP_TSTR, YP_VNONE },
+ { MOD_TTL, YP_TINT, YP_VINT = { 0, UINT32_MAX, 60, YP_STIME } },
+ { MOD_MODE, YP_TOPT, YP_VOPT = { modes, MODE_SUBNET} },
+ { MOD_DNSSEC, YP_TBOOL, YP_VNONE },
+ { MOD_POLICY, YP_TREF, YP_VREF = { C_POLICY }, YP_FNONE, { knotd_conf_check_ref } },
+ { MOD_GEODB_FILE, YP_TSTR, YP_VNONE },
+ { MOD_GEODB_KEY, YP_TSTR, YP_VSTR = { "country/iso_code" }, YP_FMULTI },
+ { NULL }
+};
+
+char geoip_check_str[1024];
+
+typedef struct {
+ knotd_conf_check_args_t *args; // Set for a dry run.
+ knotd_mod_t *mod; // Set for a real module load.
+} check_ctx_t;
+
+static int load_module(check_ctx_t *ctx);
+
+int geoip_conf_check(knotd_conf_check_args_t *args)
+{
+ knotd_conf_t conf = knotd_conf_check_item(args, MOD_CONFIG_FILE);
+ if (conf.count == 0) {
+ args->err_str = "no configuration file specified";
+ return KNOT_EINVAL;
+ }
+ conf = knotd_conf_check_item(args, MOD_MODE);
+ if (conf.count == 1 && conf.single.option == MODE_GEODB) {
+ if (!geodb_available()) {
+ args->err_str = "geodb mode not available";
+ return KNOT_EINVAL;
+ }
+
+ conf = knotd_conf_check_item(args, MOD_GEODB_FILE);
+ if (conf.count == 0) {
+ args->err_str = "no geodb file specified while in geodb mode";
+ return KNOT_EINVAL;
+ }
+
+ conf = knotd_conf_check_item(args, MOD_GEODB_KEY);
+ if (conf.count > GEODB_MAX_DEPTH) {
+ args->err_str = "maximal number of geodb-key items exceeded";
+ knotd_conf_free(&conf);
+ return KNOT_EINVAL;
+ }
+ for (size_t i = 0; i < conf.count; i++) {
+ geodb_path_t path = { 0 };
+ if (parse_geodb_path(&path, (char *)conf.multi[i].string) != 0) {
+ args->err_str = "unrecognized geodb-key format";
+ knotd_conf_free(&conf);
+ return KNOT_EINVAL;
+ }
+ for (int j = 0; j < GEODB_MAX_PATH_LEN; j++) {
+ free(path.path[j]);
+ }
+ }
+ knotd_conf_free(&conf);
+ }
+
+ check_ctx_t check = { .args = args };
+ return load_module(&check);
+}
+
+typedef struct {
+ enum operation_mode mode;
+ uint32_t ttl;
+ trie_t *geo_trie;
+ bool dnssec;
+ bool rotate;
+
+ geodb_t *geodb;
+ geodb_path_t paths[GEODB_MAX_DEPTH];
+ uint16_t path_count;
+} geoip_ctx_t;
+
+typedef struct {
+ struct sockaddr_storage *subnet;
+ uint8_t subnet_prefix;
+
+ void *geodata[GEODB_MAX_DEPTH]; // NULL if '*' is specified in config.
+ uint32_t geodata_len[GEODB_MAX_DEPTH];
+ uint8_t geodepth;
+
+ uint16_t weight;
+
+ // Index of the "parent" in the sorted view list.
+ // Equal to its own index if there is no parent.
+ size_t prev;
+
+ size_t count, avail;
+ knot_rrset_t *rrsets;
+ knot_rrset_t *rrsigs;
+
+ knot_dname_t *cname;
+} geo_view_t;
+
+typedef struct {
+ size_t count, avail;
+ geo_view_t *views;
+ uint16_t total_weight;
+} geo_trie_val_t;
+
+typedef int (*view_cmp_t)(const void *a, const void *b);
+
+int geodb_view_cmp(const void *a, const void *b)
+{
+ geo_view_t *va = (geo_view_t *)a;
+ geo_view_t *vb = (geo_view_t *)b;
+
+ int i = 0;
+ while (i < va->geodepth && i < vb->geodepth) {
+ if (va->geodata[i] == NULL) {
+ if (vb->geodata[i] != NULL) {
+ return -1;
+ }
+ } else {
+ if (vb->geodata[i] == NULL) {
+ return 1;
+ }
+ int len = MIN(va->geodata_len[i], vb->geodata_len[i]);
+ int ret = memcmp(va->geodata[i], vb->geodata[i], len);
+ if (ret < 0 || (ret == 0 && vb->geodata_len[i] > len)) {
+ return -1;
+ } else if (ret > 0 || (ret == 0 && va->geodata_len[i] > len)) {
+ return 1;
+ }
+ }
+ i++;
+ }
+ if (i < va->geodepth) {
+ return 1;
+ }
+ if (i < vb->geodepth) {
+ return -1;
+ }
+ return 0;
+}
+
+int subnet_view_cmp(const void *a, const void *b)
+{
+ geo_view_t *va = (geo_view_t *)a;
+ geo_view_t *vb = (geo_view_t *)b;
+
+ if (va->subnet->ss_family != vb->subnet->ss_family) {
+ return va->subnet->ss_family - vb->subnet->ss_family;
+ }
+
+ int ret = 0;
+ switch (va->subnet->ss_family) {
+ case AF_INET:
+ ret = memcmp(&((struct sockaddr_in *)va->subnet)->sin_addr,
+ &((struct sockaddr_in *)vb->subnet)->sin_addr,
+ sizeof(struct in_addr));
+ break;
+ case AF_INET6:
+ ret = memcmp(&((struct sockaddr_in6 *)va->subnet)->sin6_addr,
+ &((struct sockaddr_in6 *)vb->subnet)->sin6_addr,
+ sizeof(struct in6_addr));
+ }
+ if (ret == 0) {
+ return va->subnet_prefix - vb->subnet_prefix;
+ }
+ return ret;
+}
+
+int weighted_view_cmp(const void *a, const void *b)
+{
+ geo_view_t *va = (geo_view_t *)a;
+ geo_view_t *vb = (geo_view_t *)b;
+
+ return (int)va->weight - (int)vb->weight;
+}
+
+static view_cmp_t cmp_fct[] = {
+ [MODE_SUBNET] = &subnet_view_cmp,
+ [MODE_GEODB] = &geodb_view_cmp,
+ [MODE_WEIGHTED] = &weighted_view_cmp
+};
+
+static int add_view_to_trie(knot_dname_t *owner, geo_view_t *view, geoip_ctx_t *ctx)
+{
+ int ret = KNOT_EOK;
+
+ // Find the node belonging to the owner.
+ knot_dname_storage_t lf_storage;
+ uint8_t *lf = knot_dname_lf(owner, lf_storage);
+ assert(lf);
+ trie_val_t *val = trie_get_ins(ctx->geo_trie, lf + 1, *lf);
+ geo_trie_val_t *cur_val = *val;
+
+ if (cur_val == NULL) {
+ // Create new node value.
+ geo_trie_val_t *new_val = calloc(1, sizeof(geo_trie_val_t));
+ new_val->avail = 1;
+ new_val->count = 1;
+ new_val->views = malloc(sizeof(geo_view_t));
+ if (ctx->mode == MODE_WEIGHTED) {
+ new_val->total_weight = view->weight;
+ view->weight = 0; // because it is the first view
+ }
+ new_val->views[0] = *view;
+
+ // Add new value to trie.
+ *val = new_val;
+ } else {
+ // Double the views array in size if necessary.
+ if (cur_val->avail == cur_val->count) {
+ void *alloc_ret = realloc(cur_val->views,
+ 2 * cur_val->avail * sizeof(geo_view_t));
+ if (alloc_ret == NULL) {
+ return KNOT_ENOMEM;
+ }
+ cur_val->views = alloc_ret;
+ cur_val->avail *= 2;
+ }
+
+ // Insert new element.
+ if (ctx->mode == MODE_WEIGHTED) {
+ cur_val->total_weight += view->weight;
+ view->weight = cur_val->total_weight - view->weight;
+ }
+ cur_val->views[cur_val->count++] = *view;
+ }
+
+ return ret;
+}
+
+static void geo_log(check_ctx_t *check, int priority, const char *fmt, ...)
+{
+ va_list vargs;
+ va_start(vargs, fmt);
+
+ if (check->args != NULL) {
+ if (vsnprintf(geoip_check_str, sizeof(geoip_check_str), fmt, vargs) < 0) {
+ geoip_check_str[0] = '\0';
+ }
+ check->args->err_str = geoip_check_str;
+ } else {
+ knotd_mod_vlog(check->mod, priority, fmt, vargs);
+ }
+
+ va_end(vargs);
+}
+
+static knotd_conf_t geo_conf(check_ctx_t *check, const yp_name_t *item_name)
+{
+ if (check->args != NULL) {
+ return knotd_conf_check_item(check->args, item_name);
+ } else {
+ return knotd_conf_mod(check->mod, item_name);
+ }
+}
+
+static int finalize_geo_view(check_ctx_t *check, geo_view_t *view, knot_dname_t *owner,
+ geoip_ctx_t *ctx)
+{
+ if (view == NULL || view->count == 0) {
+ return KNOT_EOK;
+ }
+
+ int ret = KNOT_EOK;
+ if (ctx->dnssec) {
+ assert(check->mod != NULL);
+ view->rrsigs = malloc(sizeof(knot_rrset_t) * view->count);
+ if (view->rrsigs == NULL) {
+ return KNOT_ENOMEM;
+ }
+ for (size_t i = 0; i < view->count; i++) {
+ knot_dname_t *owner_cpy = knot_dname_copy(owner, NULL);
+ if (owner_cpy == NULL) {
+ return KNOT_ENOMEM;
+ }
+ knot_rrset_init(&view->rrsigs[i], owner_cpy, KNOT_RRTYPE_RRSIG,
+ KNOT_CLASS_IN, ctx->ttl);
+ ret = knotd_mod_dnssec_sign_rrset(check->mod, &view->rrsigs[i],
+ &view->rrsets[i], NULL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+ }
+
+ ret = add_view_to_trie(owner, view, ctx);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ memset(view, 0, sizeof(*view));
+ return ret;
+}
+
+static int init_geo_view(geo_view_t *view)
+{
+ if (view == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ view->count = 0;
+ view->avail = 1;
+ view->rrsigs = NULL;
+ view->rrsets = malloc(sizeof(knot_rrset_t));
+ if (view->rrsets == NULL) {
+ return KNOT_ENOMEM;
+ }
+ view->cname = NULL;
+ return KNOT_EOK;
+}
+
+static void clear_geo_view(geo_view_t *view)
+{
+ if (view == NULL) {
+ return;
+ }
+ for (int i = 0; i < GEODB_MAX_DEPTH; i++) {
+ free(view->geodata[i]);
+ }
+ free(view->subnet);
+ for (int j = 0; j < view->count; j++) {
+ knot_rrset_clear(&view->rrsets[j], NULL);
+ if (view->rrsigs != NULL) {
+ knot_rrset_clear(&view->rrsigs[j], NULL);
+ }
+ }
+ free(view->rrsets);
+ view->rrsets = NULL;
+ free(view->rrsigs);
+ view->rrsigs = NULL;
+ free(view->cname);
+ view->cname = NULL;
+}
+
+static int parse_origin(yp_parser_t *yp, zs_scanner_t *scanner)
+{
+ char *set_origin = sprintf_alloc("$ORIGIN %s%s\n", yp->key,
+ (yp->key[yp->key_len - 1] == '.') ? "" : ".");
+ if (set_origin == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Set owner as origin for future record parses.
+ if (zs_set_input_string(scanner, set_origin, strlen(set_origin)) != 0 ||
+ zs_parse_record(scanner) != 0) {
+ free(set_origin);
+ return KNOT_EPARSEFAIL;
+ }
+ free(set_origin);
+ return KNOT_EOK;
+}
+
+static int parse_view(check_ctx_t *check, geoip_ctx_t *ctx, yp_parser_t *yp, geo_view_t *view)
+{
+ // Initialize new geo view.
+ memset(view, 0, sizeof(*view));
+ int ret = init_geo_view(view);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Check view type syntax.
+ int key_len = strlen(mode_key[ctx->mode]);
+ if (yp->key_len != key_len || memcmp(yp->key, mode_key[ctx->mode], key_len) != 0) {
+ geo_log(check, LOG_ERR, "invalid key type '%s' on line %zu",
+ yp->key, yp->line_count);
+ return KNOT_EINVAL;
+ }
+
+ // Parse geodata/subnet.
+ if (ctx->mode == MODE_GEODB) {
+ if (parse_geodb_data((char *)yp->data, view->geodata, view->geodata_len,
+ &view->geodepth, ctx->paths, ctx->path_count) != 0) {
+ geo_log(check, LOG_ERR, "invalid geo format '%s' on line %zu",
+ yp->data, yp->line_count);
+ return KNOT_EINVAL;
+ }
+ } else if (ctx->mode == MODE_SUBNET) {
+ // Locate the optional slash in the subnet string.
+ char *slash = strchr(yp->data, '/');
+ if (slash == NULL) {
+ slash = yp->data + yp->data_len;
+ }
+ *slash = '\0';
+
+ // Parse address.
+ view->subnet = calloc(1, sizeof(struct sockaddr_storage));
+ if (view->subnet == NULL) {
+ return KNOT_ENOMEM;
+ }
+ // Try to parse as IPv4.
+ ret = sockaddr_set(view->subnet, AF_INET, yp->data, 0);
+ view->subnet_prefix = 32;
+ if (ret != KNOT_EOK) {
+ // Try to parse as IPv6.
+ ret = sockaddr_set(view->subnet, AF_INET6 ,yp->data, 0);
+ view->subnet_prefix = 128;
+ }
+ if (ret != KNOT_EOK) {
+ geo_log(check, LOG_ERR, "invalid address format '%s' on line %zu",
+ yp->data, yp->line_count);
+ return KNOT_EINVAL;
+ }
+
+ // Parse subnet prefix.
+ if (slash < yp->data + yp->data_len - 1) {
+ ret = str_to_u8(slash + 1, &view->subnet_prefix);
+ if (ret != KNOT_EOK) {
+ geo_log(check, LOG_ERR, "invalid prefix '%s' on line %zu",
+ slash + 1, yp->line_count);
+ return ret;
+ }
+ if (view->subnet->ss_family == AF_INET && view->subnet_prefix > 32) {
+ view->subnet_prefix = 32;
+ geo_log(check, LOG_WARNING, "IPv4 prefix too large on line %zu, set to 32",
+ yp->line_count);
+ }
+ if (view->subnet->ss_family == AF_INET6 && view->subnet_prefix > 128) {
+ view->subnet_prefix = 128;
+ geo_log(check, LOG_WARNING, "IPv6 prefix too large on line %zu, set to 128",
+ yp->line_count);
+ }
+ }
+ } else if (ctx->mode == MODE_WEIGHTED) {
+ uint8_t weight;
+ ret = str_to_u8(yp->data, &weight);
+ if (ret != KNOT_EOK) {
+ geo_log(check, LOG_ERR, "invalid weight '%s' on line %zu",
+ yp->data, yp->line_count);
+ return ret;
+ }
+ view->weight = weight;
+ }
+
+ return KNOT_EOK;
+}
+
+static int parse_rr(check_ctx_t *check, yp_parser_t *yp, zs_scanner_t *scanner,
+ knot_dname_t *owner, geo_view_t *view, uint32_t ttl)
+{
+ uint16_t rr_type = KNOT_RRTYPE_A;
+ if (knot_rrtype_from_string(yp->key, &rr_type) != 0) {
+ geo_log(check, LOG_ERR, "invalid RR type '%s' on line %zu",
+ yp->key, yp->line_count);
+ return KNOT_EINVAL;
+ }
+
+ if (rr_type == KNOT_RRTYPE_CNAME && view->count > 0) {
+ geo_log(check, LOG_ERR, "cannot add CNAME to view with other RRs on line %zu",
+ yp->line_count);
+ return KNOT_EINVAL;
+ }
+
+ if (view->cname != NULL) {
+ geo_log(check, LOG_ERR, "cannot add RR to view with CNAME on line %zu",
+ yp->line_count);
+ return KNOT_EINVAL;
+ }
+
+ if (knot_rrtype_is_dnssec(rr_type)) {
+ geo_log(check, LOG_ERR, "DNSSEC record '%s' not allowed on line %zu",
+ yp->key, yp->line_count);
+ return KNOT_EINVAL;
+ }
+
+ knot_rrset_t *add_rr = NULL;
+ for (size_t i = 0; i < view->count; i++) {
+ if (view->rrsets[i].type == rr_type) {
+ add_rr = &view->rrsets[i];
+ break;
+ }
+ }
+
+ if (add_rr == NULL) {
+ if (view->count == view->avail) {
+ void *alloc_ret = realloc(view->rrsets,
+ 2 * view->avail * sizeof(knot_rrset_t));
+ if (alloc_ret == NULL) {
+ return KNOT_ENOMEM;
+ }
+ view->rrsets = alloc_ret;
+ view->avail *= 2;
+ }
+ add_rr = &view->rrsets[view->count++];
+ knot_dname_t *owner_cpy = knot_dname_copy(owner, NULL);
+ if (owner_cpy == NULL) {
+ return KNOT_ENOMEM;
+ }
+ knot_rrset_init(add_rr, owner_cpy, rr_type, KNOT_CLASS_IN, ttl);
+ }
+
+ // Parse record.
+ char *input_string = sprintf_alloc("@ %s %s\n", yp->key, yp->data);
+ if (input_string == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ if (zs_set_input_string(scanner, input_string, strlen(input_string)) != 0 ||
+ zs_parse_record(scanner) != 0 ||
+ scanner->state != ZS_STATE_DATA) {
+ free(input_string);
+ return KNOT_EPARSEFAIL;
+ }
+ free(input_string);
+
+ if (rr_type == KNOT_RRTYPE_CNAME) {
+ view->cname = knot_dname_from_str_alloc(yp->data);
+ }
+
+ // Add new rdata to current rrset.
+ return knot_rrset_add_rdata(add_rr, scanner->r_data, scanner->r_data_length, NULL);
+}
+
+static int geo_conf_yparse(check_ctx_t *check, geoip_ctx_t *ctx)
+{
+ int ret = KNOT_EOK;
+ yp_parser_t *yp = NULL;
+ zs_scanner_t *scanner = NULL;
+ knot_dname_storage_t owner_buff;
+ knot_dname_t *owner = NULL;
+ geo_view_t *view = calloc(1, sizeof(geo_view_t));
+ if (view == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Initialize yparser.
+ yp = malloc(sizeof(yp_parser_t));
+ if (yp == NULL) {
+ ret = KNOT_ENOMEM;
+ goto cleanup;
+ }
+ yp_init(yp);
+ knotd_conf_t conf = geo_conf(check, MOD_CONFIG_FILE);
+ ret = yp_set_input_file(yp, conf.single.string);
+ if (ret != KNOT_EOK) {
+ geo_log(check, LOG_ERR, "failed to load module config file '%s' (%s)",
+ conf.single.string, knot_strerror(ret));
+ goto cleanup;
+ }
+
+ // Initialize zscanner.
+ scanner = malloc(sizeof(zs_scanner_t));
+ if (scanner == NULL) {
+ ret = KNOT_ENOMEM;
+ goto cleanup;
+ }
+ if (zs_init(scanner, NULL, KNOT_CLASS_IN, ctx->ttl) != 0) {
+ ret = KNOT_EPARSEFAIL;
+ goto cleanup;
+ }
+
+ // Main loop.
+ while (1) {
+ // Get the next item in config.
+ ret = yp_parse(yp);
+ if (ret == KNOT_EOF) {
+ ret = finalize_geo_view(check, view, owner, ctx);
+ goto cleanup;
+ }
+ if (ret != KNOT_EOK) {
+ geo_log(check, LOG_ERR,
+ "failed to parse module config file on line %zu (%s)",
+ yp->line_count, knot_strerror(ret));
+ goto cleanup;
+ }
+
+ // If the next item is not a rrset, the current view is finished.
+ if (yp->event != YP_EKEY1) {
+ ret = finalize_geo_view(check, view, owner, ctx);
+ if (ret != KNOT_EOK) {
+ goto cleanup;
+ }
+ }
+
+ // Next domain.
+ if (yp->event == YP_EKEY0) {
+ owner = knot_dname_from_str(owner_buff, yp->key, sizeof(owner_buff));
+ if (owner == NULL) {
+ geo_log(check, LOG_ERR,
+ "invalid domain name in module config file on line %zu",
+ yp->line_count);
+ ret = KNOT_EINVAL;
+ goto cleanup;
+ }
+ ret = parse_origin(yp, scanner);
+ if (ret != KNOT_EOK) {
+ goto cleanup;
+ }
+ }
+
+ // Next view.
+ if (yp->event == YP_EID) {
+ ret = parse_view(check, ctx, yp, view);
+ if (ret != KNOT_EOK) {
+ goto cleanup;
+ }
+ }
+
+ // Next RR of the current view.
+ if (yp->event == YP_EKEY1) {
+ // Check whether we really are in a view.
+ if (view->avail <= 0) {
+ const char *err_str[] = {
+ [MODE_SUBNET] = "- net: SUBNET",
+ [MODE_GEODB] = "- geo: LOCATION",
+ [MODE_WEIGHTED] = "- weight: WEIGHT"
+ };
+ geo_log(check, LOG_ERR,
+ "missing '%s' in module config file before line %zu",
+ err_str[ctx->mode], yp->line_count);
+ ret = KNOT_EINVAL;
+ goto cleanup;
+ }
+ ret = parse_rr(check, yp, scanner, owner, view, ctx->ttl);
+ if (ret != KNOT_EOK) {
+ goto cleanup;
+ }
+ }
+ }
+
+cleanup:
+ if (ret != KNOT_EOK) {
+ clear_geo_view(view);
+ }
+ free(view);
+ zs_deinit(scanner);
+ free(scanner);
+ yp_deinit(yp);
+ free(yp);
+ return ret;
+}
+
+static void clear_geo_trie(trie_t *trie)
+{
+ trie_it_t *it = trie_it_begin(trie);
+ while (!trie_it_finished(it)) {
+ geo_trie_val_t *val = (geo_trie_val_t *) (*trie_it_val(it));
+ for (int i = 0; i < val->count; i++) {
+ clear_geo_view(&val->views[i]);
+ }
+ free(val->views);
+ free(val);
+ trie_it_next(it);
+ }
+ trie_it_free(it);
+ trie_clear(trie);
+}
+
+static void free_geoip_ctx(geoip_ctx_t *ctx)
+{
+ geodb_close(ctx->geodb);
+ free(ctx->geodb);
+ clear_geo_trie(ctx->geo_trie);
+ trie_free(ctx->geo_trie);
+ for (int i = 0; i < ctx->path_count; i++) {
+ for (int j = 0; j < GEODB_MAX_PATH_LEN; j++) {
+ free(ctx->paths[i].path[j]);
+ }
+ }
+ free(ctx);
+}
+
+static bool view_strictly_in_view(geo_view_t *view, geo_view_t *in,
+ enum operation_mode mode)
+{
+ switch (mode) {
+ case MODE_GEODB:
+ if (in->geodepth >= view->geodepth) {
+ return false;
+ }
+ for (int i = 0; i < in->geodepth; i++) {
+ if (in->geodata[i] != NULL) {
+ if (in->geodata_len[i] != view->geodata_len[i]) {
+ return false;
+ }
+ if (memcmp(in->geodata[i], view->geodata[i],
+ in->geodata_len[i]) != 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+ case MODE_SUBNET:
+ if (in->subnet_prefix >= view->subnet_prefix) {
+ return false;
+ }
+ return sockaddr_net_match(view->subnet, in->subnet, in->subnet_prefix);
+ case MODE_WEIGHTED:
+ return true;
+ default:
+ assert(0);
+ return false;
+ }
+}
+
+static void geo_sort_and_link(geoip_ctx_t *ctx)
+{
+ trie_it_t *it = trie_it_begin(ctx->geo_trie);
+ while (!trie_it_finished(it)) {
+ geo_trie_val_t *val = (geo_trie_val_t *) (*trie_it_val(it));
+ qsort(val->views, val->count, sizeof(geo_view_t), cmp_fct[ctx->mode]);
+
+ for (int i = 1; i < val->count; i++) {
+ geo_view_t *cur_view = &val->views[i];
+ geo_view_t *prev_view = &val->views[i - 1];
+ cur_view->prev = i;
+ int prev = i - 1;
+ do {
+ if (view_strictly_in_view(cur_view, prev_view, ctx->mode)) {
+ cur_view->prev = prev;
+ break;
+ }
+ if (prev == prev_view->prev) {
+ break;
+ }
+ prev = prev_view->prev;
+ prev_view = &val->views[prev];
+ } while (1);
+ }
+ trie_it_next(it);
+ }
+ trie_it_free(it);
+}
+
+// Return the index of the last lower or equal element or -1 of not exists.
+static int geo_bin_search(geo_view_t *arr, int count, geo_view_t *x, view_cmp_t cmp)
+{
+ int l = 0, r = count;
+ while (l < r) {
+ int m = (l + r) / 2;
+ if (cmp(&arr[m], x) <= 0) {
+ l = m + 1;
+ } else {
+ r = m;
+ }
+ }
+ return l - 1; // l is the index of first greater element or N if not exists.
+}
+
+static geo_view_t *find_best_view(geo_view_t *dummy, geo_trie_val_t *data, geoip_ctx_t *ctx)
+{
+ view_cmp_t cmp = cmp_fct[ctx->mode];
+ int idx = geo_bin_search(data->views, data->count, dummy, cmp);
+ if (idx == -1) { // There is no suitable view.
+ return NULL;
+ }
+ if (cmp(dummy, &data->views[idx]) != 0 &&
+ !view_strictly_in_view(dummy, &data->views[idx], ctx->mode)) {
+ idx = data->views[idx].prev;
+ while (!view_strictly_in_view(dummy, &data->views[idx], ctx->mode)) {
+ if (idx == data->views[idx].prev) {
+ // We are at a root and we have found no suitable view.
+ return NULL;
+ }
+ idx = data->views[idx].prev;
+ }
+ }
+ return &data->views[idx];
+}
+
+static void find_rr_in_view(uint16_t qtype, geo_view_t *view,
+ knot_rrset_t **rr, knot_rrset_t **rrsig)
+{
+ knot_rrset_t *cname = NULL;
+ knot_rrset_t *cnamesig = NULL;
+ for (int i = 0; i < view->count; i++) {
+ if (view->rrsets[i].type == qtype) {
+ *rr = &view->rrsets[i];
+ *rrsig = (view->rrsigs) ? &view->rrsigs[i] : NULL;
+ } else if (view->rrsets[i].type == KNOT_RRTYPE_CNAME) {
+ cname = &view->rrsets[i];
+ cnamesig = (view->rrsigs) ? &view->rrsigs[i] : NULL;
+ }
+ }
+
+ // Return CNAME if only CNAME is found.
+ if (*rr == NULL && cname != NULL) {
+ *rr = cname;
+ *rrsig = cnamesig;
+ }
+}
+
+static knotd_in_state_t geoip_process(knotd_in_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata && mod);
+
+ // Nothing to do if the query was already resolved by a previous module.
+ if (state == KNOTD_IN_STATE_HIT || state == KNOTD_IN_STATE_FOLLOW) {
+ return state;
+ }
+
+ geoip_ctx_t *ctx = (geoip_ctx_t *)knotd_mod_ctx(mod);
+
+ // Save the query type.
+ uint16_t qtype = knot_pkt_qtype(qdata->query);
+
+ // Check if geolocation is available for given query.
+ knot_dname_storage_t lf_storage;
+ uint8_t *lf = knot_dname_lf(knot_pkt_qname(qdata->query), lf_storage);
+ // Exit if no qname.
+ if (lf == NULL) {
+ return state;
+ }
+ trie_val_t *val = trie_get_try_wildcard(ctx->geo_trie, lf + 1, *lf);
+ if (val == NULL) {
+ // Nothing to do in this module.
+ return state;
+ }
+
+ geo_trie_val_t *data = *val;
+
+ // Check if EDNS Client Subnet is available.
+ struct sockaddr_storage ecs_addr = { 0 };
+ const struct sockaddr_storage *remote = knotd_qdata_remote_addr(qdata);
+ if (knot_edns_client_subnet_get_addr(&ecs_addr, qdata->ecs) == KNOT_EOK) {
+ remote = &ecs_addr;
+ }
+
+ uint16_t netmask = 0;
+ geodb_data_t entries[GEODB_MAX_DEPTH];
+
+ // Create dummy view and fill it with data about the current remote.
+ geo_view_t dummy = { 0 };
+ switch(ctx->mode) {
+ case MODE_SUBNET:
+ dummy.subnet = (struct sockaddr_storage *)remote;
+ dummy.subnet_prefix = (remote->ss_family == AF_INET) ? 32 : 128;
+ break;
+ case MODE_GEODB:
+ if (geodb_query(ctx->geodb, entries, (struct sockaddr *)remote,
+ ctx->paths, ctx->path_count, &netmask) != 0) {
+ return state;
+ }
+ // MMDB may supply IPv6 prefixes even for IPv4 address, see man libmaxminddb.
+ if (remote->ss_family == AF_INET && netmask > 32) {
+ netmask -= 96;
+ }
+ geodb_fill_geodata(entries, ctx->path_count,
+ dummy.geodata, dummy.geodata_len, &dummy.geodepth);
+ break;
+ case MODE_WEIGHTED:
+ dummy.weight = dnssec_random_uint16_t() % data->total_weight;
+ break;
+ default:
+ assert(0);
+ break;
+ }
+
+ // Find last lower or equal view.
+ geo_view_t *view = find_best_view(&dummy, data, ctx);
+ if (view == NULL) { // No suitable view was found.
+ return state;
+ }
+
+ // Save netmask for ECS if in subnet mode.
+ if (ctx->mode == MODE_SUBNET) {
+ netmask = view->subnet_prefix;
+ }
+
+ // Fetch the correct rrset from found view.
+ knot_rrset_t *rr = NULL;
+ knot_rrset_t *rrsig = NULL;
+ find_rr_in_view(qtype, view, &rr, &rrsig);
+
+ // Answer the query if possible.
+ if (rr != NULL) {
+ // Update ECS if used.
+ if (qdata->ecs != NULL && netmask > 0) {
+ qdata->ecs->scope_len = netmask;
+ }
+
+ uint16_t rotate = ctx->rotate ? knot_wire_get_id(qdata->query->wire) : 0;
+ knot_pkt_put_rotate(pkt, KNOT_COMPR_HINT_QNAME, rr, rotate, 0);
+ if (ctx->dnssec && knot_pkt_has_dnssec(qdata->query) && rrsig != NULL) {
+ knot_pkt_put_rotate(pkt, KNOT_COMPR_HINT_QNAME, rrsig, rotate, 0);
+ }
+
+ // We've got an answer, set the AA bit.
+ knot_wire_set_aa(pkt->wire);
+
+ if (rr->type == KNOT_RRTYPE_CNAME && view->cname != NULL) {
+ // Trigger CNAME chain resolution
+ qdata->name = view->cname;
+ return KNOTD_IN_STATE_FOLLOW;
+ }
+
+ return KNOTD_IN_STATE_HIT;
+ } else {
+ // view was found, but no suitable rrtype
+ return KNOTD_IN_STATE_NODATA;
+ }
+}
+
+static int load_module(check_ctx_t *check)
+{
+ assert((check->args != NULL) != (check->mod != NULL));
+ knotd_mod_t *mod = check->mod;
+
+ // Create module context.
+ geoip_ctx_t *ctx = calloc(1, sizeof(geoip_ctx_t));
+ if (ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ knotd_conf_t conf = geo_conf(check, MOD_TTL);
+ ctx->ttl = conf.single.integer;
+ conf = geo_conf(check, MOD_MODE);
+ ctx->mode = conf.single.option;
+
+ // Initialize the dname trie.
+ ctx->geo_trie = trie_create(NULL);
+ if (ctx->geo_trie == NULL) {
+ free_geoip_ctx(ctx);
+ return KNOT_ENOMEM;
+ }
+
+ if (ctx->mode == MODE_GEODB) {
+ // Initialize geodb.
+ conf = geo_conf(check, MOD_GEODB_FILE);
+ ctx->geodb = geodb_open(conf.single.string);
+ if (ctx->geodb == NULL) {
+ geo_log(check, LOG_ERR, "failed to open geo DB");
+ free_geoip_ctx(ctx);
+ return KNOT_EINVAL;
+ }
+
+ // Load configured geodb keys.
+ conf = geo_conf(check, MOD_GEODB_KEY);
+ assert(conf.count <= GEODB_MAX_DEPTH);
+ ctx->path_count = conf.count;
+ for (size_t i = 0; i < conf.count; i++) {
+ (void)parse_geodb_path(&ctx->paths[i], (char *)conf.multi[i].string);
+ }
+ knotd_conf_free(&conf);
+ }
+
+ if (mod != NULL) {
+ // Is DNSSEC used on this zone?
+ conf = knotd_conf_mod(mod, MOD_DNSSEC);
+ if (conf.count == 0) {
+ conf = knotd_conf_zone(mod, C_DNSSEC_SIGNING, knotd_mod_zone(mod));
+ }
+ ctx->dnssec = conf.single.boolean;
+ if (ctx->dnssec) {
+ int ret = knotd_mod_dnssec_init(mod);
+ if (ret != KNOT_EOK) {
+ knotd_mod_log(mod, LOG_ERR, "failed to initialize DNSSEC");
+ free_geoip_ctx(ctx);
+ return ret;
+ }
+ ret = knotd_mod_dnssec_load_keyset(mod, false);
+ if (ret != KNOT_EOK) {
+ knotd_mod_log(mod, LOG_ERR, "failed to load DNSSEC keys");
+ free_geoip_ctx(ctx);
+ return ret;
+ }
+ }
+
+ conf = knotd_conf(mod, C_SRV, C_ANS_ROTATION, NULL);
+ ctx->rotate = conf.single.boolean;
+ }
+
+ // Parse geo configuration file.
+ int ret = geo_conf_yparse(check, ctx);
+ if (ret != KNOT_EOK) {
+ free_geoip_ctx(ctx);
+ return ret;
+ }
+
+ if (mod != NULL) {
+ // Prepare geo views for faster search.
+ geo_sort_and_link(ctx);
+
+ knotd_mod_ctx_set(mod, ctx);
+ } else {
+ free_geoip_ctx(ctx);
+ }
+
+ return ret;
+}
+
+int geoip_load(knotd_mod_t *mod)
+{
+ check_ctx_t check = { .mod = mod };
+ int ret = load_module(&check);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ return knotd_mod_in_hook(mod, KNOTD_STAGE_PREANSWER, geoip_process);
+}
+
+void geoip_unload(knotd_mod_t *mod)
+{
+ geoip_ctx_t *ctx = knotd_mod_ctx(mod);
+ if (ctx != NULL) {
+ free_geoip_ctx(ctx);
+ }
+}
+
+KNOTD_MOD_API(geoip, KNOTD_MOD_FLAG_SCOPE_ZONE,
+ geoip_load, geoip_unload, geoip_conf, geoip_conf_check);
diff --git a/src/knot/modules/geoip/geoip.rst b/src/knot/modules/geoip/geoip.rst
new file mode 100644
index 0000000..d65c1cb
--- /dev/null
+++ b/src/knot/modules/geoip/geoip.rst
@@ -0,0 +1,324 @@
+.. _mod-geoip:
+
+``geoip`` — Geography-based responses
+=====================================
+
+This module offers response tailoring based on client's
+subnet, geographic location, or a statistical weight. It supports GeoIP databases
+in the MaxMind DB format, such as `GeoIP2 <https://dev.maxmind.com/geoip/geoip2/downloadable/>`_
+or the free version `GeoLite2 <https://dev.maxmind.com/geoip/geoip2/geolite2/>`_.
+
+The module can be enabled only per zone.
+
+.. NOTE::
+ If :ref:`EDNS Client Subnet<server_edns-client-subnet>` support is enabled
+ and if a query contains this option, the module takes advantage of this
+ information to provide a more accurate response.
+
+DNSSEC support
+--------------
+
+There are several ways to enable DNSSEC signing of tailored responses.
+
+Full zone signing
+.................
+
+If :ref:`automatic DNSSEC signing <zone_dnssec-signing>` is enabled,
+the whole zone is signed by the server and all alternative RRsets, which are responded
+by the module, are pre-signed when the module is loaded.
+
+This has a speed benefit, however note that every RRset configured in the module should
+have a **default** RRset of the same type contained in the zone, so that the NSEC(3)
+chain can be built correctly. Also, it is STRONGLY RECOMMENDED to use
+:ref:`manual key management <dnssec-manual-key-management>` in this setting,
+as the corresponding zone has to be reloaded when the signing key changes and to
+have better control over key synchronization to all instances of the server.
+
+.. NOTE::
+ DNSSEC keys for computing record signatures MUST exist in the KASP database
+ or be generated before the module is launched, otherwise the module fails to
+ compute the signatures and does not load.
+
+Module signing
+..............
+
+If :ref:`automatic DNSSEC signing <zone_dnssec-signing>` is disabled,
+it's possible to combine externally pre-signed zone with module pre-signing
+of the alternative RRsets when the module is loaded. In this mode, only ZSK
+has to be present in the KASP database. Also in this mode every RRset configured
+in the module should have a **default** RRset of the same type contained in the zone.
+
+Example:
+
+::
+
+ policy:
+ - id: presigned_zone
+ manual: on
+ unsafe-operation: no-check-keyset
+
+ mod-geoip:
+ - id: geo_dnssec
+ ...
+ dnssec: on
+ policy: presigned_zone
+
+ zone:
+ - domain: example.com.
+ module: mod-geoip/geo_dnssec
+
+Online signing
+..............
+
+Alternatively, the :ref:`geoip<mod-geoip>` module may be combined with the
+:ref:`onlinesign<mod-onlinesign>` module and the tailored responses can be signed
+on the fly. This approach is much more computationally demanding for the server.
+
+.. NOTE::
+ If the GeoIP module is used with online signing, it is recommended to set the :ref:`nsec-bitmap<mod-onlinesign_nsec-bitmap>`
+ option of the onlinesign module to contain all Resource Record types potentially generated by the module.
+
+Example
+-------
+
+An example configuration:
+
+::
+
+ mod-geoip:
+ - id: default
+ config-file: /path/to/geo.conf
+ ttl: 20
+ mode: geodb
+ geodb-file: /path/to/GeoLite2-City.mmdb
+ geodb-key: [ country/iso_code, city/names/en ]
+
+ zone:
+ - domain: example.com.
+ module: mod-geoip/default
+
+
+Configuration file
+------------------
+
+Every instance of the module requires an additional :ref:`mod-geoip_config-file`
+in which the desired responses to queries from various locations are configured.
+This file has the following simple format:
+
+::
+
+ domain-name1:
+ - geo|net|weight: value1
+ RR-Type1: RDATA
+ RR-Type2: RDATA
+ ...
+ - geo|net|weight: value2
+ RR-Type1: RDATA
+ ...
+ domain-name2:
+ ...
+
+
+Module configuration examples
+-----------------------------
+
+This section contains some examples for the module's :ref:`mod-geoip_config-file`.
+
+Using subnets
+.............
+
+::
+
+ foo.example.com:
+ - net: 10.0.0.0/24
+ A: [ 192.168.1.1, 192.168.1.2 ]
+ AAAA: [ 2001:DB8::1, 2001:DB8::2 ]
+ TXT: "subnet\ 10.0.0.0/24"
+ ...
+ bar.example.com:
+ - net: 2001:DB8::/32
+ A: 192.168.1.3
+ AAAA: 2001:DB8::3
+ TXT: "subnet\ 2001:DB8::/32"
+ ...
+
+Clients from the specified subnets will receive the responses defined in the
+module config. Others will receive the default records defined in the zone (if any).
+
+.. NOTE::
+ If a space or a quotation mark is a part of record data, such a character
+ must be prefixed with a backslash. The following notations are equivalent::
+
+ Multi-word\ string
+ "Multi-word\ string"
+ "\"Multi-word string\""
+
+Using geographic locations
+..........................
+
+::
+
+ foo.example.com:
+ - geo: "CZ;Prague"
+ CNAME: cz.foo.example.com.
+ - geo: "US;Las Vegas"
+ CNAME: vegas.foo.example.net.
+ - geo: "US;*"
+ CNAME: us.foo.example.net.
+ ...
+
+Clients from the specified geographic locations will receive the responses defined in the
+module config. Others will receive the default records defined in the zone (if any). See
+:ref:`mod-geoip_geodb-key` for the syntax and semantics of the location definitions.
+
+Using weighted records
+......................
+
+::
+
+ foo.example.com:
+ - weight: 1
+ CNAME: canary.foo.example.com.
+ - weight: 10
+ CNAME: prod1.foo.example.com.
+ - weight: 10
+ CNAME: prod2.foo.example.com.
+ ...
+
+Each response is generated through a random pick where each defined record has a likelihood
+of its weight over the sum of all weights for the requested name to. Records defined in the
+zone itself (if any) will never be served.
+
+Result:
+
+.. code-block:: console
+
+ $ for i in $(seq 1 100); do kdig @192.168.1.242 CNAME foo.example.com +short; done | sort | uniq -c
+ 3 canary.foo.example.com.foo.example.com.
+ 52 prod1.foo.example.net.foo.example.com.
+ 45 prod2.foo.example.net.foo.example.com.
+
+Module reference
+----------------
+
+::
+
+ mod-geoip:
+ - id: STR
+ config-file: STR
+ ttl: TIME
+ mode: geodb | subnet | weighted
+ dnssec: BOOL
+ policy: policy_id
+ geodb-file: STR
+ geodb-key: STR ...
+
+.. _mod-geoip_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-geoip_config-file:
+
+config-file
+...........
+
+Full path to the response configuration file as described above.
+
+*Required*
+
+.. _mod-geoip_ttl:
+
+ttl
+...
+
+The time to live of Resource Records returned by the module, in seconds.
+
+*Default:* ``60``
+
+.. _mod-geoip_mode:
+
+mode
+....
+
+The mode of operation of the module.
+
+Possible values:
+
+- ``subnet`` – Responses are tailored according to subnets.
+- ``geodb`` – Responses are tailored according to geographic data retrieved
+ from the configured database.
+- ``weighted`` – Responses are tailored according to a statistical weight.
+
+*Default:* ``subnet``
+
+.. _mod-geoip_dnssec:
+
+dnssec
+......
+
+If explicitly enabled, the module signs positive responses based on the module policy
+(:ref:`mod-geoip_policy`). If explicitly disabled, positive responses from the
+module are not signed even if the zone is pre-signed or signed by the server
+(:ref:`zone_dnssec-signing`).
+
+.. WARNING::
+ This configuration must be used carefully. Otherwise the zone responses
+ can be bogus.
+ DNSKEY rotation isn't supported. So :ref:`policy_manual` mode is highly
+ recommended.
+
+*Default:* current value of :ref:`zone_dnssec-signing` with :ref:`zone_dnssec-policy`
+
+.. _mod-geoip_policy:
+
+policy
+......
+
+A :ref:`reference<policy_id>` to DNSSEC signing policy which is used if
+:ref:`mod-geoip_dnssec` is enabled.
+
+*Default:* an imaginary policy with all default values
+
+.. _mod-geoip_geodb-file:
+
+geodb-file
+..........
+
+Full path to a .mmdb file containing the GeoIP database.
+
+*Required if* :ref:`mod-geoip_mode` *is set to* **geodb**
+
+.. _mod-geoip_geodb-key:
+
+geodb-key
+.........
+
+Multi-valued item, can be specified up to **8** times. Each **geodb-key** specifies
+a path to a key in a node in the supplied GeoIP database. The module currently supports
+two types of values: **string** or **32-bit unsigned int**. In the latter
+case, the key has to be prefixed with **(id)**. Common choices of keys include:
+
+* **continent/code**
+
+* **country/iso_code**
+
+* **(id)country/geoname_id**
+
+* **city/names/en**
+
+* **(id)city/geoname_id**
+
+* **isp**
+
+* ...
+
+The exact keys available depend on the database being used. To get the full list
+of keys available, you can e.g. do a sample lookup on your database with the
+`mmdblookup <https://maxmind.github.io/libmaxminddb/mmdblookup.html>`_ tool.
+
+In the zone's config file for the module the values of the keys are entered in the same order
+as the keys in the module's configuration, separated by a semicolon. Enter the value **"*"**
+if the key is allowed to have any value.
diff --git a/src/knot/modules/noudp/Makefile.inc b/src/knot/modules/noudp/Makefile.inc
new file mode 100644
index 0000000..cf26a35
--- /dev/null
+++ b/src/knot/modules/noudp/Makefile.inc
@@ -0,0 +1,12 @@
+knot_modules_noudp_la_SOURCES = knot/modules/noudp/noudp.c
+EXTRA_DIST += knot/modules/noudp/noudp.rst
+
+if STATIC_MODULE_noudp
+libknotd_la_SOURCES += $(knot_modules_noudp_la_SOURCES)
+endif
+
+if SHARED_MODULE_noudp
+knot_modules_noudp_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_noudp_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+pkglib_LTLIBRARIES += knot/modules/noudp.la
+endif
diff --git a/src/knot/modules/noudp/noudp.c b/src/knot/modules/noudp/noudp.c
new file mode 100644
index 0000000..e8f456b
--- /dev/null
+++ b/src/knot/modules/noudp/noudp.c
@@ -0,0 +1,110 @@
+/* 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 "knot/include/module.h"
+
+#define MOD_UDP_ALLOW_RATE "\x0e""udp-allow-rate"
+#define MOD_UDP_TRUNC_RATE "\x11""udp-truncate-rate"
+
+const yp_item_t noudp_conf[] = {
+ { MOD_UDP_ALLOW_RATE, YP_TINT, YP_VINT = { 0, UINT32_MAX, 0 } },
+ { MOD_UDP_TRUNC_RATE, YP_TINT, YP_VINT = { 1, UINT32_MAX, 0 } },
+ { NULL }
+};
+
+int noudp_conf_check(knotd_conf_check_args_t *args)
+{
+ knotd_conf_t allow = knotd_conf_check_item(args, MOD_UDP_ALLOW_RATE);
+ knotd_conf_t trunc = knotd_conf_check_item(args, MOD_UDP_TRUNC_RATE);
+ if (allow.count == 1 && trunc.count == 1) {
+ args->err_str = "udp-allow-rate and udp-truncate-rate cannot be specified together";
+ return KNOT_EINVAL;
+ }
+ return KNOT_EOK;
+}
+
+typedef struct {
+ uint32_t rate;
+ uint32_t *counters;
+ bool trunc_mode;
+} noudp_ctx_t;
+
+static knotd_state_t noudp_begin(knotd_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ if (qdata->params->proto != KNOTD_QUERY_PROTO_UDP) {
+ return state;
+ }
+
+ bool truncate = true;
+
+ noudp_ctx_t *ctx = knotd_mod_ctx(mod);
+ if (ctx->rate > 0) {
+ bool apply = false;
+ if (++ctx->counters[qdata->params->thread_id] >= ctx->rate) {
+ ctx->counters[qdata->params->thread_id] = 0;
+ apply = true;
+ }
+ truncate = (apply == ctx->trunc_mode);
+ }
+
+ if (truncate) {
+ knot_wire_set_tc(pkt->wire);
+ return KNOTD_STATE_DONE;
+ } else {
+ return state;
+ }
+}
+
+int noudp_load(knotd_mod_t *mod)
+{
+ noudp_ctx_t *ctx = calloc(1, sizeof(noudp_ctx_t));
+ if (ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ knotd_conf_t allow = knotd_conf_mod(mod, MOD_UDP_ALLOW_RATE);
+ knotd_conf_t trunc = knotd_conf_mod(mod, MOD_UDP_TRUNC_RATE);
+
+ if (allow.count == 1) {
+ ctx->rate = allow.single.integer;
+ } else if (trunc.count == 1) {
+ ctx->rate = trunc.single.integer;
+ ctx->trunc_mode = true;
+ }
+
+ if (ctx->rate > 0) {
+ ctx->counters = calloc(knotd_mod_threads(mod), sizeof(uint32_t));
+ if (ctx->counters == NULL) {
+ free(ctx);
+ return KNOT_ENOMEM;
+ }
+ }
+
+ knotd_mod_ctx_set(mod, ctx);
+
+ return knotd_mod_hook(mod, KNOTD_STAGE_BEGIN, noudp_begin);
+}
+
+void noudp_unload(knotd_mod_t *mod)
+{
+ noudp_ctx_t *ctx = knotd_mod_ctx(mod);
+ free(ctx->counters);
+ free(ctx);
+}
+
+KNOTD_MOD_API(noudp, KNOTD_MOD_FLAG_SCOPE_ANY | KNOTD_MOD_FLAG_OPT_CONF,
+ noudp_load, noudp_unload, noudp_conf, noudp_conf_check);
diff --git a/src/knot/modules/noudp/noudp.rst b/src/knot/modules/noudp/noudp.rst
new file mode 100644
index 0000000..e430395
--- /dev/null
+++ b/src/knot/modules/noudp/noudp.rst
@@ -0,0 +1,68 @@
+.. _mod-noudp:
+
+``noudp`` — No UDP response
+===========================
+
+The module sends empty truncated reply to a query over UDP. Replies over TCP
+are not affected.
+
+Example
+-------
+
+To enable this module for all configured zones and every UDP reply::
+
+ template:
+ - id: default
+ global-module: mod-noudp
+
+Or with specified UDP allow rate::
+
+ mod-noudp:
+ - id: sometimes
+ udp-allow-rate: 1000 # Don't truncate every 1000th UDP reply
+
+ template:
+ - id: default
+ module: mod-noudp/sometimes
+
+Module reference
+----------------
+
+::
+
+ mod-noudp:
+ - id: STR
+ udp-allow-rate: INT
+ udp-truncate-rate: INT
+
+.. NOTE::
+ Both *udp-allow-rate* and *udp-truncate-rate* cannot be specified together.
+
+.. _mod-noudp_udp-allow-rate:
+
+udp-allow-rate
+..............
+
+Specifies frequency of UDP replies that are not truncated. A non-zero value means
+that every N\ :sup:`th` UDP reply is not truncated.
+
+.. NOTE::
+ The rate value is associated with one UDP worker. If more UDP workers are
+ configured, the specified value may not be obvious to clients.
+
+*Default:* not set
+
+.. _mod-noudp_udp-truncate-rate:
+
+udp-truncate-rate
+.................
+
+Specifies frequency of UDP replies that are truncated (opposite of
+:ref:`udp-allow-rate <mod-noudp_udp-allow-rate>`). A non-zero value means that
+every N\ :sup:`th` UDP reply is truncated.
+
+.. NOTE::
+ The rate value is associated with one UDP worker. If more UDP workers are
+ configured, the specified value may not be obvious to clients.
+
+*Default:* ``1``
diff --git a/src/knot/modules/onlinesign/Makefile.inc b/src/knot/modules/onlinesign/Makefile.inc
new file mode 100644
index 0000000..e7289fb
--- /dev/null
+++ b/src/knot/modules/onlinesign/Makefile.inc
@@ -0,0 +1,15 @@
+knot_modules_onlinesign_la_SOURCES = knot/modules/onlinesign/onlinesign.c \
+ knot/modules/onlinesign/nsec_next.c \
+ knot/modules/onlinesign/nsec_next.h
+EXTRA_DIST += knot/modules/onlinesign/onlinesign.rst
+
+if STATIC_MODULE_onlinesign
+libknotd_la_SOURCES += $(knot_modules_onlinesign_la_SOURCES)
+endif
+
+if SHARED_MODULE_onlinesign
+knot_modules_onlinesign_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_onlinesign_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+knot_modules_onlinesign_la_LIBADD = $(libcontrib_LIBS)
+pkglib_LTLIBRARIES += knot/modules/onlinesign.la
+endif
diff --git a/src/knot/modules/onlinesign/nsec_next.c b/src/knot/modules/onlinesign/nsec_next.c
new file mode 100644
index 0000000..2205f6b
--- /dev/null
+++ b/src/knot/modules/onlinesign/nsec_next.c
@@ -0,0 +1,113 @@
+/* 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 <stdbool.h>
+#include <stdlib.h>
+
+#include "knot/modules/onlinesign/nsec_next.h"
+#include "libknot/libknot.h"
+
+static bool inc_label(const uint8_t *buffer, uint8_t **label_ptr)
+{
+ assert(buffer);
+ assert(label_ptr && *label_ptr);
+ assert(buffer <= *label_ptr && *label_ptr < buffer + KNOT_DNAME_MAXLEN);
+
+ const uint8_t *label = *label_ptr;
+ const uint8_t len = *label;
+ const uint8_t *first = *label_ptr + 1;
+ const uint8_t *last = *label_ptr + len;
+
+ assert(len <= KNOT_DNAME_MAXLABELLEN);
+
+ // jump over trailing 0xff chars
+ uint8_t *scan = (uint8_t *)last;
+ while (scan >= first && *scan == 0xff) {
+ scan -= 1;
+ }
+
+ // increase in place
+ if (scan >= first) {
+ if (*scan == 'A' - 1) {
+ *scan = 'Z' + 1;
+ } else {
+ *scan += 1;
+ }
+ memset(scan + 1, 0x00, last - scan);
+ return true;
+ }
+
+ // check name and label boundaries
+ if (scan - 1 < buffer || len == KNOT_DNAME_MAXLABELLEN) {
+ return false;
+ }
+
+ // append a zero byte at the end of the label
+ scan -= 1;
+ scan[0] = len + 1;
+ memmove(scan + 1, first, len);
+ scan[len + 1] = 0x00;
+
+ *label_ptr = scan;
+
+ return true;
+}
+
+static void strip_label(uint8_t **name_ptr)
+{
+ assert(name_ptr && *name_ptr);
+
+ uint8_t len = **name_ptr;
+ *name_ptr += 1 + len;
+}
+
+knot_dname_t *online_nsec_next(const knot_dname_t *dname, const knot_dname_t *apex)
+{
+ assert(dname);
+ assert(apex);
+
+ // right aligned copy of the domain name
+ knot_dname_storage_t copy = { 0 };
+ const size_t dname_len = knot_dname_size(dname);
+ const size_t empty_len = sizeof(copy) - dname_len;
+ memmove(copy + empty_len, dname, dname_len);
+
+ // add new zero-byte label
+ if (empty_len >= 2) {
+ uint8_t *pos = copy + empty_len - 2;
+ pos[0] = 0x01;
+ pos[1] = 0x00;
+ return knot_dname_copy(pos, NULL);
+ }
+
+ // find apex position in the buffer
+ size_t apex_len = knot_dname_size(apex);
+ const uint8_t *apex_pos = copy + sizeof(copy) - apex_len;
+ assert(knot_dname_is_equal(apex, apex_pos));
+
+ // find first label which can be incremented
+ uint8_t *pos = copy + empty_len;
+ while (pos != apex_pos) {
+ if (inc_label(copy, &pos)) {
+ return knot_dname_copy(pos, NULL);
+ }
+ strip_label(&pos);
+ }
+
+ // apex completes the chain
+ return knot_dname_copy(pos, NULL);
+}
diff --git a/src/knot/modules/onlinesign/nsec_next.h b/src/knot/modules/onlinesign/nsec_next.h
new file mode 100644
index 0000000..428b993
--- /dev/null
+++ b/src/knot/modules/onlinesign/nsec_next.h
@@ -0,0 +1,29 @@
+/* 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 "libknot/dname.h"
+
+/*!
+ * \brief Get the very next possible name in NSEC chain.
+ *
+ * \param dname Current dname in the NSEC chain.
+ * \param apex Zone apex name, used when we reach the end of the chain.
+ *
+ * \return Successor of dname in the NSEC chain.
+ */
+knot_dname_t *online_nsec_next(const knot_dname_t *dname, const knot_dname_t *apex);
diff --git a/src/knot/modules/onlinesign/onlinesign.c b/src/knot/modules/onlinesign/onlinesign.c
new file mode 100644
index 0000000..56b1c03
--- /dev/null
+++ b/src/knot/modules/onlinesign/onlinesign.c
@@ -0,0 +1,736 @@
+/* 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 <stddef.h>
+#include <string.h>
+
+#include "contrib/string.h"
+#include "libdnssec/error.h"
+#include "knot/include/module.h"
+#include "knot/modules/onlinesign/nsec_next.h"
+// Next dependencies force static module!
+#include "knot/dnssec/ds_query.h"
+#include "knot/dnssec/key-events.h"
+#include "knot/dnssec/policy.h"
+#include "knot/dnssec/rrset-sign.h"
+#include "knot/dnssec/zone-events.h"
+#include "knot/dnssec/zone-sign.h"
+#include "knot/nameserver/query_module.h"
+#include "knot/nameserver/process_query.h"
+
+#define MOD_POLICY "\x06""policy"
+#define MOD_NSEC_BITMAP "\x0B""nsec-bitmap"
+
+int policy_check(knotd_conf_check_args_t *args)
+{
+ int ret = knotd_conf_check_ref(args);
+ if (ret != KNOT_EOK && strcmp((const char *)args->data, "default") == 0) {
+ return KNOT_EOK;
+ }
+
+ return ret;
+}
+
+int bitmap_check(knotd_conf_check_args_t *args)
+{
+ uint16_t num;
+ int ret = knot_rrtype_from_string((const char *)args->data, &num);
+ if (ret != 0) {
+ args->err_str = "invalid RR type";
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+const yp_item_t online_sign_conf[] = {
+ { MOD_POLICY, YP_TREF, YP_VREF = { C_POLICY }, YP_FNONE, { policy_check } },
+ { MOD_NSEC_BITMAP, YP_TSTR, YP_VNONE, YP_FMULTI, { bitmap_check } },
+ { NULL }
+};
+
+/*!
+ * We cannot determine the true NSEC bitmap because of dynamic modules which
+ * can synthesize some types on-the-fly. The base NSEC map will be determined
+ * from zone content and this list of types.
+ *
+ * The types in the NSEC bitmap really don't have to exist. Only the QTYPE
+ * must not be present. This will make the validation work with resolvers
+ * performing negative caching.
+ */
+
+static const uint16_t NSEC_FORCE_TYPES[] = {
+ KNOT_RRTYPE_A,
+ KNOT_RRTYPE_AAAA,
+ 0
+};
+
+typedef struct {
+ knot_time_t event_rollover;
+ knot_time_t event_parent_ds_q;
+ pthread_mutex_t event_mutex;
+ pthread_rwlock_t signing_mutex;
+
+ uint16_t *nsec_force_types;
+
+ bool zone_doomed;
+} online_sign_ctx_t;
+
+static bool want_dnssec(knotd_qdata_t *qdata)
+{
+ return knot_pkt_has_dnssec(qdata->query);
+}
+
+static uint32_t dnskey_ttl(knotd_qdata_t *qdata)
+{
+ knot_rrset_t soa = knotd_qdata_zone_apex_rrset(qdata, KNOT_RRTYPE_SOA);
+ return soa.ttl;
+}
+
+static uint32_t nsec_ttl(knotd_qdata_t *qdata)
+{
+ knot_rrset_t soa = knotd_qdata_zone_apex_rrset(qdata, KNOT_RRTYPE_SOA);
+ return knot_soa_minimum(soa.rrs.rdata);
+}
+
+/*!
+ * \brief Add bitmap records synthesized by online-signing.
+ */
+static void bitmap_add_synth(dnssec_nsec_bitmap_t *map, bool is_apex)
+{
+ dnssec_nsec_bitmap_add(map, KNOT_RRTYPE_NSEC);
+ dnssec_nsec_bitmap_add(map, KNOT_RRTYPE_RRSIG);
+ if (is_apex) {
+ dnssec_nsec_bitmap_add(map, KNOT_RRTYPE_DNSKEY);
+ //dnssec_nsec_bitmap_add(map, KNOT_RRTYPE_CDS);
+ }
+}
+
+/*!
+ * \brief Add bitmap records present in the zone.
+ */
+static void bitmap_add_zone(dnssec_nsec_bitmap_t *map, const zone_node_t *node)
+{
+ if (!node) {
+ return;
+ }
+
+ for (int i = 0; i < node->rrset_count; i++) {
+ dnssec_nsec_bitmap_add(map, node->rrs[i].type);
+ }
+}
+
+/*!
+ * \brief Add bitmap records which can be synthesized by other modules.
+ *
+ * \param qtype Current QTYPE, will never be added into the map.
+ */
+static void bitmap_add_forced(dnssec_nsec_bitmap_t *map, uint16_t qtype,
+ const uint16_t *force_types)
+{
+ for (int i = 0; force_types[i] > 0; i++) {
+ if (force_types[i] != qtype) {
+ dnssec_nsec_bitmap_add(map, force_types[i]);
+ }
+ }
+}
+
+/*!
+ * \brief Synthesize NSEC type bitmap.
+ *
+ * - The bitmap will contain types synthesized by this module.
+ * - The bitmap will contain types from zone and forced
+ * types which can be potentially synthesized by other query modules.
+ */
+static dnssec_nsec_bitmap_t *synth_bitmap(const knotd_qdata_t *qdata,
+ const uint16_t *force_types)
+{
+ dnssec_nsec_bitmap_t *map = dnssec_nsec_bitmap_new();
+ if (!map) {
+ return NULL;
+ }
+
+ uint16_t qtype = knot_pkt_qtype(qdata->query);
+ bool is_apex = (qdata->extra->contents != NULL &&
+ qdata->extra->node == qdata->extra->contents->apex);
+
+ bitmap_add_synth(map, is_apex);
+
+ bitmap_add_zone(map, qdata->extra->node);
+ if (force_types != NULL && !node_rrtype_exists(qdata->extra->node, KNOT_RRTYPE_CNAME)) {
+ bitmap_add_forced(map, qtype, force_types);
+ }
+
+ return map;
+}
+
+static bool is_deleg(const knot_pkt_t *pkt)
+{
+ return !knot_wire_get_aa(pkt->wire);
+}
+
+static knot_rrset_t *synth_nsec(knot_pkt_t *pkt, knotd_qdata_t *qdata, knotd_mod_t *mod,
+ knot_mm_t *mm)
+{
+ const knot_dname_t *nsec_owner = is_deleg(pkt) ? qdata->extra->encloser->owner : qdata->name;
+ knot_rrset_t *nsec = knot_rrset_new(nsec_owner, KNOT_RRTYPE_NSEC,
+ KNOT_CLASS_IN, nsec_ttl(qdata), mm);
+ if (!nsec) {
+ return NULL;
+ }
+
+ knot_dname_t *next = online_nsec_next(nsec_owner, knotd_qdata_zone_name(qdata));
+ if (!next) {
+ knot_rrset_free(nsec, mm);
+ return NULL;
+ }
+
+ // If necessary, prepare types to force into NSEC bitmap.
+ uint16_t *force_types = NULL;
+ if (!is_deleg(pkt)) {
+ online_sign_ctx_t *ctx = knotd_mod_ctx(mod);
+ force_types = ctx->nsec_force_types;
+ }
+
+ dnssec_nsec_bitmap_t *bitmap = synth_bitmap(qdata, force_types);
+ if (!bitmap) {
+ free(next);
+ knot_rrset_free(nsec, mm);
+ return NULL;
+ }
+
+ size_t size = knot_dname_size(next) + dnssec_nsec_bitmap_size(bitmap);
+ uint8_t rdata[size];
+
+ int written = knot_dname_to_wire(rdata, next, size);
+ dnssec_nsec_bitmap_write(bitmap, rdata + written);
+
+ knot_dname_free(next, NULL);
+ dnssec_nsec_bitmap_free(bitmap);
+
+ if (knot_rrset_add_rdata(nsec, rdata, size, mm) != KNOT_EOK) {
+ knot_rrset_free(nsec, mm);
+ return NULL;
+ }
+
+ return nsec;
+}
+
+static knot_rrset_t *sign_rrset(const knot_dname_t *owner,
+ const knot_rrset_t *cover,
+ knotd_mod_t *mod,
+ zone_sign_ctx_t *sign_ctx,
+ knot_mm_t *mm)
+{
+ // copy of RR set with replaced owner name
+
+ knot_rrset_t *copy = knot_rrset_new(owner, cover->type, cover->rclass,
+ cover->ttl, NULL);
+ if (!copy) {
+ return NULL;
+ }
+
+ if (knot_rdataset_copy(&copy->rrs, &cover->rrs, NULL) != KNOT_EOK) {
+ knot_rrset_free(copy, NULL);
+ return NULL;
+ }
+
+ // resulting RRSIG
+
+ knot_rrset_t *rrsig = knot_rrset_new(owner, KNOT_RRTYPE_RRSIG, copy->rclass,
+ copy->ttl, mm);
+ if (!rrsig) {
+ knot_rrset_free(copy, NULL);
+ return NULL;
+ }
+
+ online_sign_ctx_t *ctx = knotd_mod_ctx(mod);
+ pthread_rwlock_rdlock(&ctx->signing_mutex);
+ int ret = knot_sign_rrset2(rrsig, copy, sign_ctx, mm);
+ pthread_rwlock_unlock(&ctx->signing_mutex);
+ if (ret != KNOT_EOK) {
+ knot_rrset_free(copy, NULL);
+ knot_rrset_free(rrsig, mm);
+ return NULL;
+ }
+
+ knot_rrset_free(copy, NULL);
+
+ return rrsig;
+}
+
+static glue_t *find_glue_for(const knot_rrset_t *rr, const knot_pkt_t *pkt)
+{
+ for (int i = KNOT_ANSWER; i <= KNOT_AUTHORITY; i++) {
+ const knot_pktsection_t *section = knot_pkt_section(pkt, i);
+ for (int j = 0; j < section->count; j++) {
+ const knot_rrset_t *attempt = knot_pkt_rr(section, j);
+ const additional_t *a = attempt->additional;
+ for (int k = 0; a != NULL && k < a->count; k++) {
+ // no need for knot_dname_cmp because the pointers are assigned
+ if (a->glues[k].node->owner == rr->owner) {
+ return &a->glues[k];
+ }
+ }
+ }
+ }
+ return NULL;
+}
+
+static bool shall_sign_rr(const knot_rrset_t *rr, const knot_pkt_t *pkt, knotd_qdata_t *qdata)
+{
+ if (pkt->current == KNOT_ADDITIONAL) {
+ glue_t *g = find_glue_for(rr, pkt);
+ assert(g); // finds actually the node which is rr in
+ const zone_node_t *gn = glue_node(g, qdata->extra->node);
+ return !(gn->flags & NODE_FLAGS_NONAUTH);
+ } else {
+ return !is_deleg(pkt) || rr->type == KNOT_RRTYPE_NSEC;
+ }
+}
+
+static knotd_in_state_t sign_section(knotd_in_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ if (!want_dnssec(qdata)) {
+ return state;
+ }
+
+ const knot_pktsection_t *section = knot_pkt_section(pkt, pkt->current);
+ assert(section);
+
+ zone_sign_ctx_t *sign_ctx = zone_sign_ctx(mod->keyset, mod->dnssec);
+ if (sign_ctx == NULL) {
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ uint16_t count_unsigned = section->count;
+ for (int i = 0; i < count_unsigned; i++) {
+ const knot_rrset_t *rr = knot_pkt_rr(section, i);
+ if (!shall_sign_rr(rr, pkt, qdata)) {
+ continue;
+ }
+
+ uint16_t rr_pos = knot_pkt_rr_offset(section, i);
+
+ knot_dname_storage_t owner;
+ knot_dname_unpack(owner, pkt->wire + rr_pos, sizeof(owner), pkt->wire);
+ knot_dname_to_lower(owner);
+
+ knot_rrset_t *rrsig = sign_rrset(owner, rr, mod, sign_ctx, &pkt->mm);
+ if (!rrsig) {
+ state = KNOTD_IN_STATE_ERROR;
+ break;
+ }
+
+ int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_NONE, rrsig, KNOT_PF_FREE);
+ if (r != KNOT_EOK) {
+ knot_rrset_free(rrsig, &pkt->mm);
+ state = KNOTD_IN_STATE_ERROR;
+ break;
+ }
+ }
+
+ zone_sign_ctx_free(sign_ctx);
+
+ return state;
+}
+
+static knotd_in_state_t synth_authority(knotd_in_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ if (state == KNOTD_IN_STATE_HIT) {
+ return state;
+ }
+
+ // synthesise NSEC
+
+ if (want_dnssec(qdata)) {
+ knot_rrset_t *nsec = synth_nsec(pkt, qdata, mod, &pkt->mm);
+ int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_NONE, nsec, KNOT_PF_FREE);
+ if (r != DNSSEC_EOK) {
+ knot_rrset_free(nsec, &pkt->mm);
+ return KNOTD_IN_STATE_ERROR;
+ }
+ }
+
+ // promote NXDOMAIN to NODATA
+
+ if (want_dnssec(qdata) && state == KNOTD_IN_STATE_MISS) {
+ //! \todo Override RCODE set in solver_authority. Review.
+ qdata->rcode = KNOT_RCODE_NOERROR;
+ return KNOTD_IN_STATE_NODATA;
+ }
+
+ return state;
+}
+
+static knot_rrset_t *synth_dnskey(knotd_qdata_t *qdata, knotd_mod_t *mod,
+ knot_mm_t *mm)
+{
+ knot_rrset_t *dnskey = knot_rrset_new(knotd_qdata_zone_name(qdata),
+ KNOT_RRTYPE_DNSKEY, KNOT_CLASS_IN,
+ dnskey_ttl(qdata), mm);
+ if (!dnskey) {
+ return 0;
+ }
+
+ dnssec_binary_t rdata = { 0 };
+ online_sign_ctx_t *ctx = knotd_mod_ctx(mod);
+ pthread_rwlock_rdlock(&ctx->signing_mutex);
+ for (size_t i = 0; i < mod->keyset->count; i++) {
+ if (!mod->keyset->keys[i].is_public) {
+ continue;
+ }
+
+ dnssec_key_get_rdata(mod->keyset->keys[i].key, &rdata);
+ assert(rdata.size > 0 && rdata.data);
+
+ int r = knot_rrset_add_rdata(dnskey, rdata.data, rdata.size, mm);
+ if (r != KNOT_EOK) {
+ knot_rrset_free(dnskey, mm);
+ pthread_rwlock_unlock(&ctx->signing_mutex);
+ return NULL;
+ }
+ }
+
+ pthread_rwlock_unlock(&ctx->signing_mutex);
+ return dnskey;
+}
+
+static knot_rrset_t *synth_cdnskey(knotd_qdata_t *qdata, knotd_mod_t *mod,
+ knot_mm_t *mm)
+{
+ knot_rrset_t *dnskey = knot_rrset_new(knotd_qdata_zone_name(qdata),
+ KNOT_RRTYPE_CDNSKEY, KNOT_CLASS_IN,
+ 0, mm);
+ if (dnskey == NULL) {
+ return 0;
+ }
+
+ dnssec_binary_t rdata = { 0 };
+ online_sign_ctx_t *ctx = knotd_mod_ctx(mod);
+ pthread_rwlock_rdlock(&ctx->signing_mutex);
+ keyptr_dynarray_t kcdnskeys = knot_zone_sign_get_cdnskeys(mod->dnssec, mod->keyset);
+ knot_dynarray_foreach(keyptr, zone_key_t *, ksk_for_cdnskey, kcdnskeys) {
+ dnssec_key_get_rdata((*ksk_for_cdnskey)->key, &rdata);
+ assert(rdata.size > 0 && rdata.data);
+ (void)knot_rrset_add_rdata(dnskey, rdata.data, rdata.size, mm);
+ }
+ pthread_rwlock_unlock(&ctx->signing_mutex);
+
+ return dnskey;
+}
+
+static knot_rrset_t *synth_cds(knotd_qdata_t *qdata, knotd_mod_t *mod,
+ knot_mm_t *mm)
+{
+ knot_rrset_t *ds = knot_rrset_new(knotd_qdata_zone_name(qdata),
+ KNOT_RRTYPE_CDS, KNOT_CLASS_IN,
+ 0, mm);
+ if (ds == NULL) {
+ return 0;
+ }
+
+ dnssec_binary_t rdata = { 0 };
+ online_sign_ctx_t *ctx = knotd_mod_ctx(mod);
+ pthread_rwlock_rdlock(&ctx->signing_mutex);
+ keyptr_dynarray_t kcdnskeys = knot_zone_sign_get_cdnskeys(mod->dnssec, mod->keyset);
+ knot_dynarray_foreach(keyptr, zone_key_t *, ksk_for_cds, kcdnskeys) {
+ zone_key_calculate_ds(*ksk_for_cds, mod->dnssec->policy->cds_dt, &rdata);
+ assert(rdata.size > 0 && rdata.data);
+ (void)knot_rrset_add_rdata(ds, rdata.data, rdata.size, mm);
+ }
+ pthread_rwlock_unlock(&ctx->signing_mutex);
+
+ return ds;
+}
+
+static bool qtype_match(knotd_qdata_t *qdata, uint16_t type)
+{
+ uint16_t qtype = knot_pkt_qtype(qdata->query);
+ return (qtype == type);
+}
+
+static bool is_apex_query(knotd_qdata_t *qdata)
+{
+ return knot_dname_is_equal(qdata->name, knotd_qdata_zone_name(qdata));
+}
+
+static knotd_in_state_t pre_routine(knotd_in_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ online_sign_ctx_t *ctx = knotd_mod_ctx(mod);
+ zone_sign_reschedule_t resch = { 0 };
+
+ (void)pkt, (void)qdata;
+
+ pthread_mutex_lock(&ctx->event_mutex);
+ if (ctx->zone_doomed) {
+ pthread_mutex_unlock(&ctx->event_mutex);
+ return KNOTD_IN_STATE_ERROR;
+ }
+ mod->dnssec->now = time(NULL);
+ int ret = KNOT_ESEMCHECK;
+ if (knot_time_cmp(ctx->event_parent_ds_q, mod->dnssec->now) <= 0) {
+ pthread_rwlock_rdlock(&ctx->signing_mutex);
+ ret = knot_parent_ds_query(conf(), mod->dnssec, 1000);
+ pthread_rwlock_unlock(&ctx->signing_mutex);
+ if (ret != KNOT_EOK && ret != KNOT_NO_READY_KEY && mod->dnssec->policy->ksk_sbm_check_interval > 0) {
+ ctx->event_parent_ds_q = mod->dnssec->now + mod->dnssec->policy->ksk_sbm_check_interval;
+ } else {
+ ctx->event_parent_ds_q = 0;
+ }
+ }
+ if (ret == KNOT_EOK || knot_time_cmp(ctx->event_rollover, mod->dnssec->now) <= 0) {
+ update_policy_from_zone(mod->dnssec->policy, qdata->extra->contents);
+ ret = knot_dnssec_key_rollover(mod->dnssec, KEY_ROLL_ALLOW_KSK_ROLL | KEY_ROLL_ALLOW_ZSK_ROLL, &resch);
+ if (ret != KNOT_EOK) {
+ ctx->event_rollover = knot_dnssec_failover_delay(mod->dnssec);
+ }
+ }
+ if (ret == KNOT_EOK) {
+ if (resch.plan_ds_check && mod->dnssec->policy->ksk_sbm_check_interval > 0) {
+ ctx->event_parent_ds_q = mod->dnssec->now + mod->dnssec->policy->ksk_sbm_check_interval;
+ } else {
+ ctx->event_parent_ds_q = 0;
+ }
+
+ ctx->event_rollover = resch.next_rollover;
+
+ pthread_rwlock_wrlock(&ctx->signing_mutex);
+ knotd_mod_dnssec_unload_keyset(mod);
+ ret = knotd_mod_dnssec_load_keyset(mod, true);
+ if (ret != KNOT_EOK) {
+ ctx->zone_doomed = true;
+ state = KNOTD_IN_STATE_ERROR;
+ } else {
+ ctx->event_rollover = knot_time_min(ctx->event_rollover, knot_get_next_zone_key_event(mod->keyset));
+ }
+ pthread_rwlock_unlock(&ctx->signing_mutex);
+ }
+ pthread_mutex_unlock(&ctx->event_mutex);
+
+ return state;
+}
+
+static knotd_in_state_t synth_answer(knotd_in_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ // disallowed queries
+
+ if (knot_pkt_qtype(pkt) == KNOT_RRTYPE_RRSIG) {
+ qdata->rcode = KNOT_RCODE_REFUSED;
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ // synthesized DNSSEC answers
+
+ if (qtype_match(qdata, KNOT_RRTYPE_DNSKEY) && is_apex_query(qdata)) {
+ knot_rrset_t *dnskey = synth_dnskey(qdata, mod, &pkt->mm);
+ if (!dnskey) {
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, dnskey, KNOT_PF_FREE);
+ if (r != DNSSEC_EOK) {
+ knot_rrset_free(dnskey, &pkt->mm);
+ return KNOTD_IN_STATE_ERROR;
+ }
+ state = KNOTD_IN_STATE_HIT;
+ }
+
+ if (qtype_match(qdata, KNOT_RRTYPE_CDNSKEY) && is_apex_query(qdata)) {
+ knot_rrset_t *dnskey = synth_cdnskey(qdata, mod, &pkt->mm);
+ if (!dnskey) {
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, dnskey, KNOT_PF_FREE);
+ if (r != DNSSEC_EOK) {
+ knot_rrset_free(dnskey, &pkt->mm);
+ return KNOTD_IN_STATE_ERROR;
+ }
+ state = KNOTD_IN_STATE_HIT;
+ }
+
+ if (qtype_match(qdata, KNOT_RRTYPE_CDS) && is_apex_query(qdata)) {
+ knot_rrset_t *ds = synth_cds(qdata, mod, &pkt->mm);
+ if (!ds) {
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, ds, KNOT_PF_FREE);
+ if (r != DNSSEC_EOK) {
+ knot_rrset_free(ds, &pkt->mm);
+ return KNOTD_IN_STATE_ERROR;
+ }
+ state = KNOTD_IN_STATE_HIT;
+ }
+
+ if (qtype_match(qdata, KNOT_RRTYPE_NSEC)) {
+ knot_rrset_t *nsec = synth_nsec(pkt, qdata, mod, &pkt->mm);
+ if (!nsec) {
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ int r = knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, nsec, KNOT_PF_FREE);
+ if (r != DNSSEC_EOK) {
+ knot_rrset_free(nsec, &pkt->mm);
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ state = KNOTD_IN_STATE_HIT;
+ }
+
+ return state;
+}
+
+static void online_sign_ctx_free(online_sign_ctx_t *ctx)
+{
+ pthread_mutex_destroy(&ctx->event_mutex);
+ pthread_rwlock_destroy(&ctx->signing_mutex);
+
+ free(ctx->nsec_force_types);
+ free(ctx);
+}
+
+static int online_sign_ctx_new(online_sign_ctx_t **ctx_ptr, knotd_mod_t *mod)
+{
+ online_sign_ctx_t *ctx = calloc(1, sizeof(*ctx));
+ if (!ctx) {
+ return KNOT_ENOMEM;
+ }
+
+ int ret = knotd_mod_dnssec_init(mod);
+ if (ret != KNOT_EOK) {
+ free(ctx);
+ return ret;
+ }
+
+ // Historically, the default scheme is Single-Type signing.
+ if (mod->dnssec->policy->sts_default) {
+ mod->dnssec->policy->single_type_signing = true;
+ }
+
+ zone_sign_reschedule_t resch = { 0 };
+ ret = knot_dnssec_key_rollover(mod->dnssec, KEY_ROLL_ALLOW_KSK_ROLL | KEY_ROLL_ALLOW_ZSK_ROLL, &resch);
+ if (ret != KNOT_EOK) {
+ free(ctx);
+ return ret;
+ }
+
+ if (resch.plan_ds_check) {
+ ctx->event_parent_ds_q = time(NULL);
+ }
+ ctx->event_rollover = resch.next_rollover;
+
+ ret = knotd_mod_dnssec_load_keyset(mod, true);
+ if (ret != KNOT_EOK) {
+ free(ctx);
+ return ret;
+ }
+
+ ctx->event_rollover = knot_time_min(ctx->event_rollover, knot_get_next_zone_key_event(mod->keyset));
+
+ pthread_mutex_init(&ctx->event_mutex, NULL);
+ pthread_rwlock_init(&ctx->signing_mutex, NULL);
+
+ *ctx_ptr = ctx;
+
+ return KNOT_EOK;
+}
+
+int load_nsec_bitmap(online_sign_ctx_t *ctx, knotd_conf_t *conf)
+{
+ int count = (conf->count > 0) ? conf->count : sizeof(NSEC_FORCE_TYPES) / sizeof(uint16_t);
+ ctx->nsec_force_types = calloc(count + 1, sizeof(uint16_t));
+ if (ctx->nsec_force_types == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ if (conf->count == 0) {
+ // Use the default list.
+ for (int i = 0; NSEC_FORCE_TYPES[i] > 0; i++) {
+ ctx->nsec_force_types[i] = NSEC_FORCE_TYPES[i];
+ }
+ } else {
+ for (int i = 0; i < conf->count; i++) {
+ int ret = knot_rrtype_from_string(conf->multi[i].string,
+ &ctx->nsec_force_types[i]);
+ if (ret != 0) {
+ return KNOT_EINVAL;
+ }
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+int online_sign_load(knotd_mod_t *mod)
+{
+ knotd_conf_t conf = knotd_conf_zone(mod, C_DNSSEC_SIGNING,
+ knotd_mod_zone(mod));
+ if (conf.single.boolean) {
+ knotd_mod_log(mod, LOG_ERR, "incompatible with automatic signing");
+ return KNOT_ENOTSUP;
+ }
+
+ online_sign_ctx_t *ctx = NULL;
+ int ret = online_sign_ctx_new(&ctx, mod);
+ if (ret != KNOT_EOK) {
+ knotd_mod_log(mod, LOG_ERR, "failed to initialize signing key (%s)",
+ knot_strerror(ret));
+ return KNOT_ERROR;
+ }
+
+ if (mod->dnssec->policy->offline_ksk) {
+ knotd_mod_log(mod, LOG_ERR, "incompatible with offline KSK mode");
+ online_sign_ctx_free(ctx);
+ return KNOT_ENOTSUP;
+ }
+
+ conf = knotd_conf_mod(mod, MOD_NSEC_BITMAP);
+ ret = load_nsec_bitmap(ctx, &conf);
+ knotd_conf_free(&conf);
+ if (ret != KNOT_EOK) {
+ online_sign_ctx_free(ctx);
+ return ret;
+ }
+
+ knotd_mod_ctx_set(mod, ctx);
+
+ knotd_mod_in_hook(mod, KNOTD_STAGE_ANSWER, pre_routine);
+
+ knotd_mod_in_hook(mod, KNOTD_STAGE_ANSWER, synth_answer);
+ knotd_mod_in_hook(mod, KNOTD_STAGE_ANSWER, sign_section);
+
+ knotd_mod_in_hook(mod, KNOTD_STAGE_AUTHORITY, synth_authority);
+ knotd_mod_in_hook(mod, KNOTD_STAGE_AUTHORITY, sign_section);
+
+ knotd_mod_in_hook(mod, KNOTD_STAGE_ADDITIONAL, sign_section);
+
+ return KNOT_EOK;
+}
+
+void online_sign_unload(knotd_mod_t *mod)
+{
+ online_sign_ctx_free(knotd_mod_ctx(mod));
+}
+
+KNOTD_MOD_API(onlinesign, KNOTD_MOD_FLAG_SCOPE_ZONE | KNOTD_MOD_FLAG_OPT_CONF,
+ online_sign_load, online_sign_unload, online_sign_conf, NULL);
diff --git a/src/knot/modules/onlinesign/onlinesign.rst b/src/knot/modules/onlinesign/onlinesign.rst
new file mode 100644
index 0000000..c1859e2
--- /dev/null
+++ b/src/knot/modules/onlinesign/onlinesign.rst
@@ -0,0 +1,158 @@
+.. _mod-onlinesign:
+
+``onlinesign`` — Online DNSSEC signing
+======================================
+
+The module provides online DNSSEC signing. Instead of pre-computing the zone
+signatures when the zone is loaded into the server or instead of loading an
+externally signed zone, the signatures are computed on-the-fly during
+answering.
+
+The main purpose of the module is to enable authenticated responses with
+zones which use other dynamic module (e.g., automatic reverse record
+synthesis) because these zones cannot be pre-signed. However, it can be also
+used as a simple signing solution for zones with low traffic and also as
+a protection against zone content enumeration (zone walking).
+
+In order to minimize the number of computed signatures per query, the module
+produces a bit different responses from the responses that would be sent if
+the zone was pre-signed. Still, the responses should be perfectly valid for
+a DNSSEC validating resolver.
+
+.. rubric:: Differences from statically signed zones:
+
+* The NSEC records are constructed as Minimally Covering NSEC Records
+ (:rfc:`7129#appendix-A`). Therefore the generated domain names cover
+ the complete domain name space in the zone's authority.
+
+* NXDOMAIN responses are promoted to NODATA responses. The module proves
+ that the query type does not exist rather than that the domain name does not
+ exist.
+
+* Domain names matching a wildcard are expanded. The module pretends and proves
+ that the domain name exists rather than proving a presence of the wildcard.
+
+.. rubric:: Records synthesized by the module:
+
+* DNSKEY record is synthesized in the zone apex and includes public key
+ material for the active signing key.
+
+* NSEC records are synthesized as needed.
+
+* RRSIG records are synthesized for authoritative content of the zone.
+
+* CDNSKEY and CDS records are generated as usual to publish valid Secure Entry Point.
+
+.. rubric:: Limitations:
+
+* Due to limited interaction between the server and the module,
+ after any change to KASP DB (including `knotc zone-ksk-submitted` command)
+ or when a scheduled DNSSEC event shall be processed (e.g. transition to next
+ DNSKEY rollover state) the server must be reloaded or queried to the zone
+ (with the DO bit set) to apply the change or to trigger the event. For optimal
+ operation, the recommended query frequency is at least ones per second for
+ each zone configured.
+
+* The NSEC records may differ for one domain name if queried for different
+ types. This is an implementation shortcoming as the dynamic modules
+ cooperate loosely. Possible synthesis of a type by other module cannot
+ be predicted. This dissimilarity should not affect response validation,
+ even with validators performing aggressive negative caching (:rfc:`8198`).
+
+* The module isn't compatible with the Offline KSK mode yet.
+
+.. rubric:: Recommendations:
+
+* Configure the module with an explicit signing policy which has the
+ :ref:`policy_rrsig-lifetime` value in the order of hours.
+
+* Note that :ref:`policy_single-type-signing` should be set explicitly to
+ avoid fallback to backward-compatible default.
+
+Example
+-------
+
+* Enable the module in the zone configuration with the default signing policy::
+
+ zone:
+ - domain: example.com
+ module: mod-onlinesign
+
+ Or with an explicit signing policy::
+
+ policy:
+ - id: rsa
+ algorithm: RSASHA256
+ ksk-size: 2048
+ rrsig-lifetime: 25h
+ rrsig-refresh: 20h
+
+ mod-onlinesign:
+ - id: explicit
+ policy: rsa
+
+ zone:
+ - domain: example.com
+ module: mod-onlinesign/explicit
+
+ Or use manual policy in an analogous manner, see
+ :ref:`Manual key management<dnssec-manual-key-management>`.
+
+* Make sure the zone is not signed and also that the automatic signing is
+ disabled. All is set, you are good to go. Reload (or start) the server:
+
+ .. code-block:: console
+
+ $ knotc reload
+
+The following example stacks the online signing with reverse record synthesis
+module::
+
+ mod-synthrecord:
+ - id: lan-forward
+ type: forward
+ prefix: ip-
+ ttl: 1200
+ network: 192.168.100.0/24
+
+ zone:
+ - domain: corp.example.net
+ module: [mod-synthrecord/lan-forward, mod-onlinesign]
+
+Module reference
+----------------
+
+::
+
+ mod-onlinesign:
+ - id: STR
+ policy: policy_id
+ nsec-bitmap: STR ...
+
+.. _mod-onlinesign_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-onlinesign_policy:
+
+policy
+......
+
+A :ref:`reference<policy_id>` to DNSSEC signing policy. A special *default*
+value can be used for the default policy setting.
+
+*Default:* an imaginary policy with all default values
+
+.. _mod-onlinesign_nsec-bitmap:
+
+nsec-bitmap
+...........
+
+A list of Resource Record types included in an NSEC bitmap generated by the module.
+This option should reflect zone contents or synthesized responses by modules,
+such as :ref:`synthrecord<mod-synthrecord>` and :ref:`GeoIP<mod-geoip>`.
+
+*Default:* ``[A, AAAA]``
diff --git a/src/knot/modules/probe/Makefile.inc b/src/knot/modules/probe/Makefile.inc
new file mode 100644
index 0000000..db14fc4
--- /dev/null
+++ b/src/knot/modules/probe/Makefile.inc
@@ -0,0 +1,12 @@
+knot_modules_probe_la_SOURCES = knot/modules/probe/probe.c
+EXTRA_DIST += knot/modules/probe/probe.rst
+
+if STATIC_MODULE_probe
+libknotd_la_SOURCES += $(knot_modules_probe_la_SOURCES)
+endif
+
+if SHARED_MODULE_probe
+knot_modules_probe_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_probe_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+pkglib_LTLIBRARIES += knot/modules/probe.la
+endif
diff --git a/src/knot/modules/probe/probe.c b/src/knot/modules/probe/probe.c
new file mode 100644
index 0000000..bcaa707
--- /dev/null
+++ b/src/knot/modules/probe/probe.c
@@ -0,0 +1,190 @@
+/* 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 <stdio.h>
+#include <stdint.h>
+
+#include "knot/conf/schema.h"
+#include "knot/include/module.h"
+#include "contrib/string.h"
+#include "contrib/time.h"
+#include "libknot/libknot.h"
+
+#ifdef HAVE_ATOMIC
+#define ATOMIC_SET(dst, val) __atomic_store_n(&(dst), (val), __ATOMIC_RELAXED)
+#define ATOMIC_GET(src) __atomic_load_n(&(src), __ATOMIC_RELAXED)
+#else
+#define ATOMIC_SET(dst, val) ((dst) = (val))
+#define ATOMIC_GET(src) (src)
+#endif
+
+#define MOD_PATH "\x04""path"
+#define MOD_CHANNELS "\x08""channels"
+#define MOD_MAX_RATE "\x08""max-rate"
+
+const yp_item_t probe_conf[] = {
+ { MOD_PATH, YP_TSTR, YP_VNONE },
+ { MOD_CHANNELS, YP_TINT, YP_VINT = { 1, UINT16_MAX, 1 } },
+ { MOD_MAX_RATE, YP_TINT, YP_VINT = { 0, UINT32_MAX, 100000 } },
+ { NULL }
+};
+
+typedef struct {
+ knot_probe_t **probes;
+ size_t probe_count;
+ uint64_t *last_times;
+ uint64_t min_diff_ns;
+ char *path;
+} probe_ctx_t;
+
+static void free_probe_ctx(probe_ctx_t *ctx)
+{
+ for (int i = 0; ctx->probes != NULL && i < ctx->probe_count; ++i) {
+ knot_probe_free(ctx->probes[i]);
+ }
+ free(ctx->probes);
+ free(ctx->last_times);
+ free(ctx->path);
+ free(ctx);
+}
+
+static knotd_state_t export(knotd_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata);
+
+ probe_ctx_t *ctx = knotd_mod_ctx(mod);
+ uint16_t idx = qdata->params->thread_id % ctx->probe_count;
+ knot_probe_t *probe = ctx->probes[idx];
+
+ // Check the rate limit if enabled.
+ if (ctx->min_diff_ns > 0) {
+ struct timespec now = time_now();
+ uint64_t now_ns = 1000000000 * now.tv_sec + now.tv_nsec;
+ uint64_t last_ns = ATOMIC_GET(ctx->last_times[idx]);
+ if (now_ns - last_ns < ctx->min_diff_ns) {
+ return state;
+ }
+ ATOMIC_SET(ctx->last_times[idx], now_ns);
+ }
+
+ // Prepare data sources.
+ struct sockaddr_storage buff;
+ const struct sockaddr_storage *local = knotd_qdata_local_addr(qdata, &buff);
+ const struct sockaddr_storage *remote = knotd_qdata_remote_addr(qdata);
+
+ knot_probe_proto_t proto = (knot_probe_proto_t)qdata->params->proto;
+ const knot_pkt_t *reply = (state != KNOTD_STATE_NOOP ? pkt : NULL);
+
+ uint16_t rcode = qdata->rcode;
+ if (qdata->rcode_tsig != KNOT_RCODE_NOERROR) {
+ rcode = qdata->rcode_tsig;
+ }
+
+ // Fill out and export the data structure.
+ knot_probe_data_t d;
+ int ret = knot_probe_data_set(&d, proto, local, remote, qdata->query, reply, rcode);
+ if (ret == KNOT_EOK) {
+ d.tcp_rtt = knotd_qdata_rtt(qdata);
+ if (qdata->query->opt_rr != NULL) {
+ d.reply.ede = qdata->rcode_ede;
+ }
+ (void)knot_probe_produce(probe, &d, 1);
+ }
+
+ return state;
+}
+
+int probe_load(knotd_mod_t *mod)
+{
+ probe_ctx_t *ctx = calloc(1, sizeof(*ctx));
+ if (ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ knotd_conf_t conf = knotd_conf_mod(mod, MOD_CHANNELS);
+ ctx->probe_count = conf.single.integer;
+
+ conf = knotd_conf_mod(mod, MOD_PATH);
+ if (conf.count == 0) {
+ conf = knotd_conf(mod, C_SRV, C_RUNDIR, NULL);
+ }
+ if (conf.single.string[0] != '/') {
+ char *cwd = realpath("./", NULL);
+ ctx->path = sprintf_alloc("%s/%s", cwd, conf.single.string);
+ free(cwd);
+ } else {
+ ctx->path = strdup(conf.single.string);
+ }
+ if (ctx->path == NULL) {
+ free_probe_ctx(ctx);
+ return KNOT_ENOMEM;
+ }
+
+ ctx->probes = calloc(ctx->probe_count, sizeof(knot_probe_t *));
+ if (ctx->probes == NULL) {
+ free_probe_ctx(ctx);
+ return KNOT_ENOMEM;
+ }
+
+ ctx->last_times = calloc(ctx->probe_count, sizeof(uint64_t));
+ if (ctx->last_times == NULL) {
+ free_probe_ctx(ctx);
+ return KNOT_ENOMEM;
+ }
+
+ ctx->min_diff_ns = 0;
+ conf = knotd_conf_mod(mod, MOD_MAX_RATE);
+ if (conf.single.integer > 0) {
+ ctx->min_diff_ns = ctx->probe_count * 1000000000 / conf.single.integer;
+ }
+
+ for (int i = 0; i < ctx->probe_count; i++) {
+ knot_probe_t *probe = knot_probe_alloc();
+ if (probe == NULL) {
+ free_probe_ctx(ctx);
+ return KNOT_ENOMEM;
+ }
+
+ int ret = knot_probe_set_producer(probe, ctx->path, i + 1);
+ switch (ret) {
+ case KNOT_ECONN:
+ knotd_mod_log(mod, LOG_NOTICE, "channel %i not connected", i + 1);
+ case KNOT_EOK:
+ break;
+ default:
+ free_probe_ctx(ctx);
+ return ret;
+ }
+
+ ctx->probes[i] = probe;
+ }
+
+ knotd_mod_ctx_set(mod, ctx);
+
+ return knotd_mod_hook(mod, KNOTD_STAGE_END, export);
+}
+
+void probe_unload(knotd_mod_t *mod)
+{
+ probe_ctx_t *ctx = knotd_mod_ctx(mod);
+ if (ctx != NULL) {
+ free_probe_ctx(ctx);
+ }
+}
+
+KNOTD_MOD_API(probe, KNOTD_MOD_FLAG_SCOPE_ANY | KNOTD_MOD_FLAG_OPT_CONF,
+ probe_load, probe_unload, probe_conf, NULL);
diff --git a/src/knot/modules/probe/probe.rst b/src/knot/modules/probe/probe.rst
new file mode 100644
index 0000000..e3657b9
--- /dev/null
+++ b/src/knot/modules/probe/probe.rst
@@ -0,0 +1,89 @@
+.. _mod-probe:
+
+``probe`` — DNS traffic probe
+=============================
+
+The module allows the server to send simplified information about regular DNS
+traffic through *UNIX* sockets. The exported information consists of data blocks
+where each data block (datagram) describes one query/response pair. The response
+part can be empty. The receiver can be an arbitrary program using *libknot* interface
+(C or Python). In case of high traffic, more channels (sockets) can be configured
+to allow parallel processing.
+
+.. NOTE::
+ A simple `probe client <https://gitlab.nic.cz/knot/knot-dns/-/blob/master/scripts/probe_dump.py>`_ in Python.
+
+Example
+-------
+
+Default module configuration::
+
+ template:
+ - id: default
+ global-module: mod-probe
+
+Per zone probe with 8 channels and maximum 1M logs per second limit::
+
+ mod-probe:
+ - id: custom
+ path: /tmp/knot-probe
+ channels: 8
+ max-rate: 1000000
+
+ zone:
+ - domain: example.com.
+ module: mod-probe/custom
+
+
+Module reference
+----------------
+
+::
+
+ mod-probe:
+ - id: STR
+ path: STR
+ channels: INT
+ max-rate: INT
+
+.. _mod-probe_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-probe_path:
+
+path
+....
+
+A directory path the UNIX sockets are located.
+
+.. NOTE::
+ It's recommended to use a directory with the execute permission restricted
+ to the intended probe consumer process owner only.
+
+*Default:* :ref:`rundir<server_rundir>`
+
+.. _mod-probe_channels:
+
+channels
+........
+
+Number of channels (UNIX sockets) the traffic is distributed to. In case of
+high DNS traffic which is beeing processed by many UDP/XDP/TCP workers,
+using more channels reduces the module overhead.
+
+*Default:* ``1``
+
+.. _mod-probe_max-rate:
+
+max-rate
+........
+
+Maximum number of queries/replies per second the probe is allowed to transfer.
+If the limit is exceeded, the over-limit traffic is ignored. Zero value means
+no limit.
+
+*Default:* ``100000`` (one hundred thousand)
diff --git a/src/knot/modules/queryacl/Makefile.inc b/src/knot/modules/queryacl/Makefile.inc
new file mode 100644
index 0000000..25dcc38
--- /dev/null
+++ b/src/knot/modules/queryacl/Makefile.inc
@@ -0,0 +1,12 @@
+knot_modules_queryacl_la_SOURCES = knot/modules/queryacl/queryacl.c
+EXTRA_DIST += knot/modules/queryacl/queryacl.rst
+
+if STATIC_MODULE_queryacl
+libknotd_la_SOURCES += $(knot_modules_queryacl_la_SOURCES)
+endif
+
+if SHARED_MODULE_queryacl
+knot_modules_queryacl_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_queryacl_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+pkglib_LTLIBRARIES += knot/modules/queryacl.la
+endif
diff --git a/src/knot/modules/queryacl/queryacl.c b/src/knot/modules/queryacl/queryacl.c
new file mode 100644
index 0000000..e787083
--- /dev/null
+++ b/src/knot/modules/queryacl/queryacl.c
@@ -0,0 +1,93 @@
+/* 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 "knot/include/module.h"
+#include "contrib/sockaddr.h"
+
+#define MOD_ADDRESS "\x07""address"
+#define MOD_INTERFACE "\x09""interface"
+
+const yp_item_t queryacl_conf[] = {
+ { MOD_ADDRESS, YP_TNET, YP_VNONE, YP_FMULTI },
+ { MOD_INTERFACE, YP_TNET, YP_VNONE, YP_FMULTI },
+ { NULL }
+};
+
+typedef struct {
+ knotd_conf_t allow_addr;
+ knotd_conf_t allow_iface;
+} queryacl_ctx_t;
+
+static knotd_state_t queryacl_process(knotd_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata && mod);
+
+ queryacl_ctx_t *ctx = knotd_mod_ctx(mod);
+
+ // Continue only for regular queries.
+ if (qdata->type != KNOTD_QUERY_TYPE_NORMAL) {
+ return state;
+ }
+
+ if (ctx->allow_addr.count > 0) {
+ const struct sockaddr_storage *addr = knotd_qdata_remote_addr(qdata);
+ if (!knotd_conf_addr_range_match(&ctx->allow_addr, addr)) {
+ qdata->rcode = KNOT_RCODE_NOTAUTH;
+ return KNOTD_STATE_FAIL;
+ }
+ }
+
+ if (ctx->allow_iface.count > 0) {
+ struct sockaddr_storage buff;
+ const struct sockaddr_storage *addr = knotd_qdata_local_addr(qdata, &buff);
+ if (!knotd_conf_addr_range_match(&ctx->allow_iface, addr)) {
+ qdata->rcode = KNOT_RCODE_NOTAUTH;
+ return KNOTD_STATE_FAIL;
+ }
+ }
+
+ return state;
+}
+
+int queryacl_load(knotd_mod_t *mod)
+{
+ // Create module context.
+ queryacl_ctx_t *ctx = calloc(1, sizeof(queryacl_ctx_t));
+ if (ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ ctx->allow_addr = knotd_conf_mod(mod, MOD_ADDRESS);
+ ctx->allow_iface = knotd_conf_mod(mod, MOD_INTERFACE);
+
+ knotd_mod_ctx_set(mod, ctx);
+
+ return knotd_mod_hook(mod, KNOTD_STAGE_BEGIN, queryacl_process);
+}
+
+void queryacl_unload(knotd_mod_t *mod)
+{
+ queryacl_ctx_t *ctx = knotd_mod_ctx(mod);
+ if (ctx != NULL) {
+ knotd_conf_free(&ctx->allow_addr);
+ knotd_conf_free(&ctx->allow_iface);
+ }
+ free(ctx);
+}
+
+KNOTD_MOD_API(queryacl, KNOTD_MOD_FLAG_SCOPE_ANY,
+ queryacl_load, queryacl_unload, queryacl_conf, NULL);
diff --git a/src/knot/modules/queryacl/queryacl.rst b/src/knot/modules/queryacl/queryacl.rst
new file mode 100644
index 0000000..1a402f6
--- /dev/null
+++ b/src/knot/modules/queryacl/queryacl.rst
@@ -0,0 +1,70 @@
+.. _mod-queryacl:
+
+``queryacl`` — Limit queries by remote address or target interface
+==================================================================
+
+This module provides a simple way to whitelist incoming queries
+according to the query's source address or target interface.
+It can be used e.g. to create a restricted-access subzone with delegations from the corresponding public zone.
+The module may be enabled both globally and per-zone.
+
+.. NOTE::
+ The module limits only regular queries. Notify, transfer and update are handled by :ref:`ACL<ACL>`.
+
+Example
+-------
+
+::
+
+ mod-queryacl:
+ - id: default
+ address: [192.0.2.73-192.0.2.90, 203.0.113.0/24]
+ interface: 198.51.100
+
+ zone:
+ - domain: example.com
+ module: mod-queryacl/default
+
+Module reference
+----------------
+
+::
+
+ mod-queryacl:
+ - id: STR
+ address: ADDR[/INT] | ADDR-ADDR ...
+ interface: ADDR[/INT] | ADDR-ADDR ...
+
+.. _mod-queryacl_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-queryacl_address:
+
+address
+.......
+
+An optional list of allowed ranges and/or subnets for query's source address.
+If the query's address does not fall into any
+of the configured ranges, NOTAUTH rcode is returned.
+
+*Default:* not set
+
+.. _mod-queryacl_interface:
+
+interface
+.........
+
+An optional list of allowed ranges and/or subnets for query's target interface.
+If the interface does not fall into any
+of the configured ranges, NOTAUTH rcode is returned. Note that every interface
+used has to be configured in :ref:`listen<server_listen>`.
+
+.. NOTE::
+ Don't use values *0.0.0.0* and *::0*. These values are redundant and don't
+ work as expected.
+
+*Default:* not set
diff --git a/src/knot/modules/rrl/Makefile.inc b/src/knot/modules/rrl/Makefile.inc
new file mode 100644
index 0000000..d82edf9
--- /dev/null
+++ b/src/knot/modules/rrl/Makefile.inc
@@ -0,0 +1,15 @@
+knot_modules_rrl_la_SOURCES = knot/modules/rrl/rrl.c \
+ knot/modules/rrl/functions.c \
+ knot/modules/rrl/functions.h
+EXTRA_DIST += knot/modules/rrl/rrl.rst
+
+if STATIC_MODULE_rrl
+libknotd_la_SOURCES += $(knot_modules_rrl_la_SOURCES)
+endif
+
+if SHARED_MODULE_rrl
+knot_modules_rrl_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_rrl_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+knot_modules_rrl_la_LIBADD = $(libcontrib_LIBS)
+pkglib_LTLIBRARIES += knot/modules/rrl.la
+endif
diff --git a/src/knot/modules/rrl/functions.c b/src/knot/modules/rrl/functions.c
new file mode 100644
index 0000000..df35394
--- /dev/null
+++ b/src/knot/modules/rrl/functions.c
@@ -0,0 +1,554 @@
+/* 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 <assert.h>
+#include <time.h>
+
+#include "knot/modules/rrl/functions.h"
+#include "contrib/musl/inet_ntop.h"
+#include "contrib/openbsd/strlcat.h"
+#include "contrib/sockaddr.h"
+#include "contrib/time.h"
+#include "libdnssec/error.h"
+#include "libdnssec/random.h"
+
+/* Hopscotch defines. */
+#define HOP_LEN (sizeof(unsigned)*8)
+/* Limits (class, ipv6 remote, dname) */
+#define RRL_CLSBLK_MAXLEN (1 + 8 + 255)
+/* CIDR block prefix lengths for v4/v6 */
+#define RRL_V4_PREFIX_LEN 3 /* /24 */
+#define RRL_V6_PREFIX_LEN 7 /* /56 */
+/* Defaults */
+#define RRL_SSTART 2 /* 1/Nth of the rate for slow start */
+#define RRL_PSIZE_LARGE 1024
+#define RRL_CAPACITY 4 /* Window size in seconds */
+#define RRL_LOCK_GRANULARITY 32 /* Last digit granularity */
+
+/* Classification */
+enum {
+ CLS_NULL = 0 << 0, /* Empty bucket. */
+ CLS_NORMAL = 1 << 0, /* Normal response. */
+ CLS_ERROR = 1 << 1, /* Error response. */
+ CLS_NXDOMAIN = 1 << 2, /* NXDOMAIN (special case of error). */
+ CLS_EMPTY = 1 << 3, /* Empty response. */
+ CLS_LARGE = 1 << 4, /* Response size over threshold (1024k). */
+ CLS_WILDCARD = 1 << 5, /* Wildcard query. */
+ CLS_ANY = 1 << 6, /* ANY query (spec. class). */
+ CLS_DNSSEC = 1 << 7 /* DNSSEC related RR query (spec. class) */
+};
+
+/* Classification string. */
+struct cls_name {
+ int code;
+ const char *name;
+};
+
+static const struct cls_name rrl_cls_names[] = {
+ { CLS_NORMAL, "POSITIVE" },
+ { CLS_ERROR, "ERROR" },
+ { CLS_NXDOMAIN, "NXDOMAIN"},
+ { CLS_EMPTY, "EMPTY"},
+ { CLS_LARGE, "LARGE"},
+ { CLS_WILDCARD, "WILDCARD"},
+ { CLS_ANY, "ANY"},
+ { CLS_DNSSEC, "DNSSEC"},
+ { CLS_NULL, "NULL"},
+ { CLS_NULL, NULL}
+};
+
+static inline const char *rrl_clsstr(int code)
+{
+ for (const struct cls_name *c = rrl_cls_names; c->name; c++) {
+ if (c->code == code) {
+ return c->name;
+ }
+ }
+
+ return "unknown class";
+}
+
+/* Bucket flags. */
+enum {
+ RRL_BF_NULL = 0 << 0, /* No flags. */
+ RRL_BF_SSTART = 1 << 0, /* Bucket in slow-start after collision. */
+ RRL_BF_ELIMIT = 1 << 1 /* Bucket is rate-limited. */
+};
+
+static uint8_t rrl_clsid(rrl_req_t *p)
+{
+ /* Check error code */
+ int ret = CLS_NULL;
+ switch (knot_wire_get_rcode(p->wire)) {
+ case KNOT_RCODE_NOERROR: ret = CLS_NORMAL; break;
+ case KNOT_RCODE_NXDOMAIN: return CLS_NXDOMAIN; break;
+ default: return CLS_ERROR; break;
+ }
+
+ /* Check if answered from a qname */
+ if (ret == CLS_NORMAL && p->flags & RRL_REQ_WILDCARD) {
+ return CLS_WILDCARD;
+ }
+
+ /* Check query type for spec. classes. */
+ if (p->query) {
+ switch(knot_pkt_qtype(p->query)) {
+ case KNOT_RRTYPE_ANY: /* ANY spec. class */
+ return CLS_ANY;
+ break;
+ case KNOT_RRTYPE_DNSKEY:
+ case KNOT_RRTYPE_RRSIG:
+ case KNOT_RRTYPE_DS: /* DNSSEC-related RR class. */
+ return CLS_DNSSEC;
+ break;
+ default:
+ break;
+ }
+ }
+
+ /* Check packet size for threshold. */
+ if (p->len >= RRL_PSIZE_LARGE) {
+ return CLS_LARGE;
+ }
+
+ /* Check ancount */
+ if (knot_wire_get_ancount(p->wire) == 0) {
+ return CLS_EMPTY;
+ }
+
+ return ret;
+}
+
+static int rrl_clsname(uint8_t *dst, size_t maxlen, uint8_t cls, rrl_req_t *req,
+ const knot_dname_t *name)
+{
+ if (name == NULL) {
+ /* Fallback for errors etc. */
+ name = (const knot_dname_t *)"\x00";
+ }
+
+ switch (cls) {
+ case CLS_ERROR: /* Could be a non-existent zone or garbage. */
+ case CLS_NXDOMAIN: /* Queries to non-existent names in zone. */
+ case CLS_WILDCARD: /* Queries to names covered by a wildcard. */
+ break;
+ default:
+ /* Use QNAME */
+ if (req->query) {
+ name = knot_pkt_qname(req->query);
+ }
+ break;
+ }
+
+ /* Write to wire */
+ return knot_dname_to_wire(dst, name, maxlen);
+}
+
+static int rrl_classify(uint8_t *dst, size_t maxlen, const struct sockaddr_storage *remote,
+ rrl_req_t *req, const knot_dname_t *name)
+{
+ /* Class */
+ uint8_t cls = rrl_clsid(req);
+ *dst = cls;
+ int blklen = sizeof(cls);
+
+ /* Address (in network byteorder, adjust masks). */
+ uint64_t netblk = 0;
+ if (remote->ss_family == AF_INET6) {
+ struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)remote;
+ memcpy(&netblk, &ipv6->sin6_addr, RRL_V6_PREFIX_LEN);
+ } else {
+ struct sockaddr_in *ipv4 = (struct sockaddr_in *)remote;
+ memcpy(&netblk, &ipv4->sin_addr, RRL_V4_PREFIX_LEN);
+ }
+ memcpy(dst + blklen, &netblk, sizeof(netblk));
+ blklen += sizeof(netblk);
+
+ /* Name */
+ int ret = rrl_clsname(dst + blklen, maxlen - blklen, cls, req, name);
+ if (ret < 0) {
+ return ret;
+ }
+ uint8_t len = ret;
+ blklen += len;
+
+ return blklen;
+}
+
+static int bucket_free(rrl_item_t *bucket, uint32_t now)
+{
+ return bucket->cls == CLS_NULL || (bucket->time + 1 < now);
+}
+
+static int bucket_match(rrl_item_t *bucket, rrl_item_t *match)
+{
+ return bucket->cls == match->cls &&
+ bucket->netblk == match->netblk &&
+ bucket->qname == match->qname;
+}
+
+static int find_free(rrl_table_t *tbl, unsigned id, uint32_t now)
+{
+ for (int i = id; i < tbl->size; i++) {
+ if (bucket_free(&tbl->arr[i], now)) {
+ return i - id;
+ }
+ }
+ for (int i = 0; i < id; i++) {
+ if (bucket_free(&tbl->arr[i], now)) {
+ return i + (tbl->size - id);
+ }
+ }
+
+ /* this happens if table is full... force vacate current elm */
+ return id;
+}
+
+static inline unsigned find_match(rrl_table_t *tbl, uint32_t id, rrl_item_t *m)
+{
+ unsigned new_id = 0;
+ unsigned hop = 0;
+ unsigned match_bitmap = tbl->arr[id].hop;
+ while (match_bitmap != 0) {
+ hop = __builtin_ctz(match_bitmap); /* offset of next potential match */
+ new_id = (id + hop) % tbl->size;
+ if (bucket_match(&tbl->arr[new_id], m)) {
+ return hop;
+ } else {
+ match_bitmap &= ~(1 << hop); /* clear potential match */
+ }
+ }
+
+ return HOP_LEN + 1;
+}
+
+static inline unsigned reduce_dist(rrl_table_t *tbl, unsigned id, unsigned dist, unsigned *free_id)
+{
+ unsigned rd = HOP_LEN - 1;
+ while (rd > 0) {
+ unsigned vacate_id = (tbl->size + *free_id - rd) % tbl->size; /* bucket to be vacated */
+ if (tbl->arr[vacate_id].hop != 0) {
+ unsigned hop = __builtin_ctz(tbl->arr[vacate_id].hop); /* offset of first valid bucket */
+ if (hop < rd) { /* only offsets in <vacate_id, free_id> are interesting */
+ unsigned new_id = (vacate_id + hop) % tbl->size; /* this item will be displaced to [free_id] */
+ unsigned keep_hop = tbl->arr[*free_id].hop; /* unpredictable padding */
+ memcpy(tbl->arr + *free_id, tbl->arr + new_id, sizeof(rrl_item_t));
+ tbl->arr[*free_id].hop = keep_hop;
+ tbl->arr[new_id].cls = CLS_NULL;
+ tbl->arr[vacate_id].hop &= ~(1 << hop);
+ tbl->arr[vacate_id].hop |= 1 << rd;
+ *free_id = new_id;
+ return dist - (rd - hop);
+ }
+ }
+ --rd;
+ }
+
+ assert(rd == 0); /* this happens with p=1/fact(HOP_LEN) */
+ *free_id = id;
+ dist = 0; /* force vacate initial element */
+ return dist;
+}
+
+static void subnet_tostr(char *dst, size_t maxlen, const struct sockaddr_storage *ss)
+{
+ const void *addr;
+ const char *suffix;
+
+ if (ss->ss_family == AF_INET6) {
+ addr = &((struct sockaddr_in6 *)ss)->sin6_addr;
+ suffix = "/56";
+ } else {
+ addr = &((struct sockaddr_in *)ss)->sin_addr;
+ suffix = "/24";
+ }
+
+ if (knot_inet_ntop(ss->ss_family, addr, dst, maxlen) != NULL) {
+ strlcat(dst, suffix, maxlen);
+ } else {
+ dst[0] = '\0';
+ }
+}
+
+static void rrl_log_state(knotd_mod_t *mod, const struct sockaddr_storage *ss,
+ uint16_t flags, uint8_t cls, const knot_dname_t *qname)
+{
+ if (mod == NULL || ss == NULL) {
+ return;
+ }
+
+ char addr_str[SOCKADDR_STRLEN];
+ subnet_tostr(addr_str, sizeof(addr_str), ss);
+
+ const char *what = "leaves";
+ if (flags & RRL_BF_ELIMIT) {
+ what = "enters";
+ }
+
+ knot_dname_txt_storage_t buf;
+ char *qname_str = knot_dname_to_str(buf, qname, sizeof(buf));
+ if (qname_str == NULL) {
+ qname_str = "?";
+ }
+
+ knotd_mod_log(mod, LOG_NOTICE, "address/subnet %s, class %s, qname %s, %s limiting",
+ addr_str, rrl_clsstr(cls), qname_str, what);
+}
+
+static void rrl_lock(rrl_table_t *tbl, int lk_id)
+{
+ assert(lk_id > -1);
+ pthread_mutex_lock(tbl->lk + lk_id);
+}
+
+static void rrl_unlock(rrl_table_t *tbl, int lk_id)
+{
+ assert(lk_id > -1);
+ pthread_mutex_unlock(tbl->lk + lk_id);
+}
+
+static int rrl_setlocks(rrl_table_t *tbl, uint32_t granularity)
+{
+ assert(!tbl->lk); /* Cannot change while locks are used. */
+ assert(granularity <= tbl->size / 10); /* Due to int. division err. */
+
+ if (pthread_mutex_init(&tbl->ll, NULL) < 0) {
+ return KNOT_ENOMEM;
+ }
+
+ /* Alloc new locks. */
+ tbl->lk = malloc(granularity * sizeof(pthread_mutex_t));
+ if (!tbl->lk) {
+ return KNOT_ENOMEM;
+ }
+ memset(tbl->lk, 0, granularity * sizeof(pthread_mutex_t));
+
+ /* Initialize. */
+ for (size_t i = 0; i < granularity; ++i) {
+ if (pthread_mutex_init(tbl->lk + i, NULL) < 0) {
+ break;
+ }
+ ++tbl->lk_count;
+ }
+
+ /* Incomplete initialization */
+ if (tbl->lk_count != granularity) {
+ for (size_t i = 0; i < tbl->lk_count; ++i) {
+ pthread_mutex_destroy(tbl->lk + i);
+ }
+ free(tbl->lk);
+ tbl->lk_count = 0;
+ return KNOT_ERROR;
+ }
+
+ return KNOT_EOK;
+}
+
+rrl_table_t *rrl_create(size_t size, uint32_t rate)
+{
+ if (size == 0) {
+ return NULL;
+ }
+
+ const size_t tbl_len = sizeof(rrl_table_t) + size * sizeof(rrl_item_t);
+ rrl_table_t *tbl = calloc(1, tbl_len);
+ if (!tbl) {
+ return NULL;
+ }
+ tbl->size = size;
+ tbl->rate = rate;
+
+ if (dnssec_random_buffer((uint8_t *)&tbl->key, sizeof(tbl->key)) != DNSSEC_EOK) {
+ free(tbl);
+ return NULL;
+ }
+
+ if (rrl_setlocks(tbl, RRL_LOCK_GRANULARITY) != KNOT_EOK) {
+ free(tbl);
+ return NULL;
+ }
+
+ return tbl;
+}
+
+static knot_dname_t *buf_qname(uint8_t *buf)
+{
+ return buf + sizeof(uint8_t) + sizeof(uint64_t);
+}
+
+/*! \brief Get bucket for current combination of parameters. */
+static rrl_item_t *rrl_hash(rrl_table_t *tbl, const struct sockaddr_storage *remote,
+ rrl_req_t *req, const knot_dname_t *zone, uint32_t stamp,
+ int *lock, uint8_t *buf, size_t buf_len)
+{
+ int len = rrl_classify(buf, buf_len, remote, req, zone);
+ if (len < 0) {
+ return NULL;
+ }
+
+ uint32_t id = SipHash24(&tbl->key, buf, len) % tbl->size;
+
+ /* Lock for lookup. */
+ pthread_mutex_lock(&tbl->ll);
+
+ /* Find an exact match in <id, id + HOP_LEN). */
+ knot_dname_t *qname = buf_qname(buf);
+ uint64_t netblk;
+ memcpy(&netblk, buf + sizeof(uint8_t), sizeof(netblk));
+ rrl_item_t match = {
+ .hop = 0,
+ .netblk = netblk,
+ .ntok = tbl->rate * RRL_CAPACITY,
+ .cls = buf[0],
+ .flags = RRL_BF_NULL,
+ .qname = SipHash24(&tbl->key, qname, knot_dname_size(qname)),
+ .time = stamp
+ };
+
+ unsigned dist = find_match(tbl, id, &match);
+ if (dist > HOP_LEN) { /* not an exact match, find free element [f] */
+ dist = find_free(tbl, id, stamp);
+ }
+
+ /* Reduce distance to fit <id, id + HOP_LEN) */
+ unsigned free_id = (id + dist) % tbl->size;
+ while (dist >= HOP_LEN) {
+ dist = reduce_dist(tbl, id, dist, &free_id);
+ }
+
+ /* Assign granular lock and unlock lookup. */
+ *lock = free_id % tbl->lk_count;
+ rrl_lock(tbl, *lock);
+ pthread_mutex_unlock(&tbl->ll);
+
+ /* found free bucket which is in <id, id + HOP_LEN) */
+ tbl->arr[id].hop |= (1 << dist);
+ rrl_item_t *bucket = &tbl->arr[free_id];
+ assert(free_id == (id + dist) % tbl->size);
+
+ /* Inspect bucket state. */
+ unsigned hop = bucket->hop;
+ if (bucket->cls == CLS_NULL) {
+ memcpy(bucket, &match, sizeof(rrl_item_t));
+ bucket->hop = hop;
+ }
+ /* Check for collisions. */
+ if (!bucket_match(bucket, &match)) {
+ if (!(bucket->flags & RRL_BF_SSTART)) {
+ memcpy(bucket, &match, sizeof(rrl_item_t));
+ bucket->hop = hop;
+ bucket->ntok = tbl->rate + tbl->rate / RRL_SSTART;
+ bucket->flags |= RRL_BF_SSTART;
+ }
+ }
+
+ return bucket;
+}
+
+int rrl_query(rrl_table_t *rrl, const struct sockaddr_storage *remote,
+ rrl_req_t *req, const knot_dname_t *zone, knotd_mod_t *mod)
+{
+ if (!rrl || !req || !remote) {
+ return KNOT_EINVAL;
+ }
+
+ uint8_t buf[RRL_CLSBLK_MAXLEN];
+
+ /* Calculate hash and fetch */
+ int ret = KNOT_EOK;
+ int lock = -1;
+ uint32_t now = time_now().tv_sec;
+ rrl_item_t *bucket = rrl_hash(rrl, remote, req, zone, now, &lock, buf, sizeof(buf));
+ if (!bucket) {
+ if (lock > -1) {
+ rrl_unlock(rrl, lock);
+ }
+ return KNOT_ERROR;
+ }
+
+ /* Calculate rate for dT */
+ uint32_t dt = now - bucket->time;
+ if (dt > RRL_CAPACITY) {
+ dt = RRL_CAPACITY;
+ }
+ /* Visit bucket. */
+ bucket->time = now;
+ if (dt > 0) { /* Window moved. */
+
+ /* Check state change. */
+ if ((bucket->ntok > 0 || dt > 1) && (bucket->flags & RRL_BF_ELIMIT)) {
+ bucket->flags &= ~RRL_BF_ELIMIT;
+ rrl_log_state(mod, remote, bucket->flags, bucket->cls,
+ knot_pkt_qname(req->query));
+ }
+
+ /* Add new tokens. */
+ uint32_t dn = rrl->rate * dt;
+ if (bucket->flags & RRL_BF_SSTART) { /* Bucket in slow-start. */
+ bucket->flags &= ~RRL_BF_SSTART;
+ }
+ bucket->ntok += dn;
+ if (bucket->ntok > RRL_CAPACITY * rrl->rate) {
+ bucket->ntok = RRL_CAPACITY * rrl->rate;
+ }
+ }
+
+ /* Last item taken. */
+ if (bucket->ntok == 1 && !(bucket->flags & RRL_BF_ELIMIT)) {
+ bucket->flags |= RRL_BF_ELIMIT;
+ rrl_log_state(mod, remote, bucket->flags, bucket->cls,
+ knot_pkt_qname(req->query));
+ }
+
+ /* Decay current bucket. */
+ if (bucket->ntok > 0) {
+ --bucket->ntok;
+ } else if (bucket->ntok == 0) {
+ ret = KNOT_ELIMIT;
+ }
+
+ if (lock > -1) {
+ rrl_unlock(rrl, lock);
+ }
+ return ret;
+}
+
+bool rrl_slip_roll(int n_slip)
+{
+ switch (n_slip) {
+ case 0:
+ return false;
+ case 1:
+ return true;
+ default:
+ return (dnssec_random_uint16_t() % n_slip == 0);
+ }
+}
+
+void rrl_destroy(rrl_table_t *rrl)
+{
+ if (rrl) {
+ if (rrl->lk_count > 0) {
+ pthread_mutex_destroy(&rrl->ll);
+ }
+ for (size_t i = 0; i < rrl->lk_count; ++i) {
+ pthread_mutex_destroy(rrl->lk + i);
+ }
+ free(rrl->lk);
+ }
+
+ free(rrl);
+}
diff --git a/src/knot/modules/rrl/functions.h b/src/knot/modules/rrl/functions.h
new file mode 100644
index 0000000..0f09234
--- /dev/null
+++ b/src/knot/modules/rrl/functions.h
@@ -0,0 +1,111 @@
+/* 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 <stdint.h>
+#include <pthread.h>
+#include <sys/socket.h>
+
+#include "libknot/libknot.h"
+#include "knot/include/module.h"
+#include "contrib/openbsd/siphash.h"
+
+/*!
+ * \brief RRL hash bucket.
+ */
+typedef struct {
+ unsigned hop; /* Hop bitmap. */
+ uint64_t netblk; /* Prefix associated. */
+ uint16_t ntok; /* Tokens available. */
+ uint8_t cls; /* Bucket class. */
+ uint8_t flags; /* Flags. */
+ uint32_t qname; /* imputed(QNAME) hash. */
+ uint32_t time; /* Timestamp. */
+} rrl_item_t;
+
+/*!
+ * \brief RRL hash bucket table.
+ *
+ * Table is fixed size, so collisions may occur and are dealt with
+ * in a way, that hashbucket rate is reset and enters slow-start for 1 dt.
+ * When a bucket is in a slow-start mode, it cannot reset again for the time
+ * period.
+ *
+ * To avoid lock contention, N locks are created and distributed amongst buckets.
+ * As of now lock K for bucket N is calculated as K = N % (num_buckets).
+ */
+
+typedef struct {
+ SIPHASH_KEY key; /* Siphash key. */
+ uint32_t rate; /* Configured RRL limit. */
+ pthread_mutex_t ll;
+ pthread_mutex_t *lk; /* Table locks. */
+ unsigned lk_count; /* Table lock count (granularity). */
+ size_t size; /* Number of buckets. */
+ rrl_item_t arr[]; /* Buckets. */
+} rrl_table_t;
+
+/*! \brief RRL request flags. */
+typedef enum {
+ RRL_REQ_NOFLAG = 0 << 0, /*!< No flags. */
+ RRL_REQ_WILDCARD = 1 << 1 /*!< Query to wildcard name. */
+} rrl_req_flag_t;
+
+/*!
+ * \brief RRL request descriptor.
+ */
+typedef struct {
+ const uint8_t *wire;
+ uint16_t len;
+ rrl_req_flag_t flags;
+ knot_pkt_t *query;
+} rrl_req_t;
+
+/*!
+ * \brief Create a RRL table.
+ * \param size Fixed hashtable size (reasonable large prime is recommended).
+ * \param rate Rate (in pkts/sec).
+ * \return created table or NULL.
+ */
+rrl_table_t *rrl_create(size_t size, uint32_t rate);
+
+/*!
+ * \brief Query the RRL table for accept or deny, when the rate limit is reached.
+ *
+ * \param rrl RRL table.
+ * \param remote Source address.
+ * \param req RRL request (containing resp., flags and question).
+ * \param zone Zone name related to the response (or NULL).
+ * \param mod Query module (needed for logging).
+ * \retval KNOT_EOK if passed.
+ * \retval KNOT_ELIMIT when the limit is reached.
+ */
+int rrl_query(rrl_table_t *rrl, const struct sockaddr_storage *remote,
+ rrl_req_t *req, const knot_dname_t *zone, knotd_mod_t *mod);
+
+/*!
+ * \brief Roll a dice whether answer slips or not.
+ * \param n_slip Number represents every Nth answer that is slipped.
+ * \return true or false
+ */
+bool rrl_slip_roll(int n_slip);
+
+/*!
+ * \brief Destroy RRL table.
+ * \param rrl RRL table.
+ */
+void rrl_destroy(rrl_table_t *rrl);
diff --git a/src/knot/modules/rrl/rrl.c b/src/knot/modules/rrl/rrl.c
new file mode 100644
index 0000000..64f6cbf
--- /dev/null
+++ b/src/knot/modules/rrl/rrl.c
@@ -0,0 +1,208 @@
+/* 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 "knot/include/module.h"
+#include "knot/nameserver/process_query.h" // Dependency on qdata->extra!
+#include "knot/modules/rrl/functions.h"
+
+#define MOD_RATE_LIMIT "\x0A""rate-limit"
+#define MOD_SLIP "\x04""slip"
+#define MOD_TBL_SIZE "\x0A""table-size"
+#define MOD_WHITELIST "\x09""whitelist"
+
+const yp_item_t rrl_conf[] = {
+ { MOD_RATE_LIMIT, YP_TINT, YP_VINT = { 1, INT32_MAX } },
+ { MOD_SLIP, YP_TINT, YP_VINT = { 0, 100, 1 } },
+ { MOD_TBL_SIZE, YP_TINT, YP_VINT = { 1, INT32_MAX, 393241 } },
+ { MOD_WHITELIST, YP_TNET, YP_VNONE, YP_FMULTI },
+ { NULL }
+};
+
+int rrl_conf_check(knotd_conf_check_args_t *args)
+{
+ knotd_conf_t limit = knotd_conf_check_item(args, MOD_RATE_LIMIT);
+ if (limit.count == 0) {
+ args->err_str = "no rate limit specified";
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+typedef struct {
+ rrl_table_t *rrl;
+ int slip;
+ knotd_conf_t whitelist;
+} rrl_ctx_t;
+
+static const knot_dname_t *name_from_rrsig(const knot_rrset_t *rr)
+{
+ if (rr == NULL) {
+ return NULL;
+ }
+ if (rr->type != KNOT_RRTYPE_RRSIG) {
+ return NULL;
+ }
+
+ // This is a signature.
+ return knot_rrsig_signer_name(rr->rrs.rdata);
+}
+
+static const knot_dname_t *name_from_authrr(const knot_rrset_t *rr)
+{
+ if (rr == NULL) {
+ return NULL;
+ }
+ if (rr->type != KNOT_RRTYPE_NS && rr->type != KNOT_RRTYPE_SOA) {
+ return NULL;
+ }
+
+ // This is a valid authority RR.
+ return rr->owner;
+}
+
+static knotd_state_t ratelimit_apply(knotd_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata && mod);
+
+ rrl_ctx_t *ctx = knotd_mod_ctx(mod);
+
+ // Rate limit is applied to pure UDP only.
+ if (qdata->params->proto != KNOTD_QUERY_PROTO_UDP) {
+ return state;
+ }
+
+ // Rate limit is not applied to responses with a valid cookie.
+ if (qdata->params->flags & KNOTD_QUERY_FLAG_COOKIE) {
+ return state;
+ }
+
+ // Exempt clients.
+ if (knotd_conf_addr_range_match(&ctx->whitelist, knotd_qdata_remote_addr(qdata))) {
+ return state;
+ }
+
+ rrl_req_t req = {
+ .wire = pkt->wire,
+ .query = qdata->query
+ };
+
+ if (!EMPTY_LIST(qdata->extra->wildcards)) {
+ req.flags = RRL_REQ_WILDCARD;
+ }
+
+ // Take the zone name if known.
+ const knot_dname_t *zone_name = knotd_qdata_zone_name(qdata);
+
+ // Take the signer name as zone name if there is an RRSIG.
+ if (zone_name == NULL) {
+ const knot_pktsection_t *ans = knot_pkt_section(pkt, KNOT_ANSWER);
+ for (int i = 0; i < ans->count; i++) {
+ zone_name = name_from_rrsig(knot_pkt_rr(ans, i));
+ if (zone_name != NULL) {
+ break;
+ }
+ }
+ }
+
+ // Take the NS or SOA owner name if there is no RRSIG.
+ if (zone_name == NULL) {
+ const knot_pktsection_t *auth = knot_pkt_section(pkt, KNOT_AUTHORITY);
+ for (int i = 0; i < auth->count; i++) {
+ zone_name = name_from_authrr(knot_pkt_rr(auth, i));
+ if (zone_name != NULL) {
+ break;
+ }
+ }
+ }
+
+ if (rrl_query(ctx->rrl, knotd_qdata_remote_addr(qdata), &req, zone_name, mod) == KNOT_EOK) {
+ // Rate limiting not applied.
+ return state;
+ }
+
+ if (rrl_slip_roll(ctx->slip)) {
+ // Slip the answer.
+ knotd_mod_stats_incr(mod, qdata->params->thread_id, 0, 0, 1);
+ qdata->err_truncated = true;
+ return KNOTD_STATE_FAIL;
+ } else {
+ // Drop the answer.
+ knotd_mod_stats_incr(mod, qdata->params->thread_id, 1, 0, 1);
+ return KNOTD_STATE_NOOP;
+ }
+}
+
+static void ctx_free(rrl_ctx_t *ctx)
+{
+ assert(ctx);
+
+ rrl_destroy(ctx->rrl);
+ free(ctx);
+}
+
+int rrl_load(knotd_mod_t *mod)
+{
+ // Create RRL context.
+ rrl_ctx_t *ctx = calloc(1, sizeof(rrl_ctx_t));
+ if (ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Create table.
+ uint32_t rate = knotd_conf_mod(mod, MOD_RATE_LIMIT).single.integer;
+ size_t size = knotd_conf_mod(mod, MOD_TBL_SIZE).single.integer;
+ ctx->rrl = rrl_create(size, rate);
+ if (ctx->rrl == NULL) {
+ ctx_free(ctx);
+ return KNOT_ENOMEM;
+ }
+
+ // Get slip.
+ ctx->slip = knotd_conf_mod(mod, MOD_SLIP).single.integer;
+
+ // Get whitelist.
+ ctx->whitelist = knotd_conf_mod(mod, MOD_WHITELIST);
+
+ // Set up statistics counters.
+ int ret = knotd_mod_stats_add(mod, "slipped", 1, NULL);
+ if (ret != KNOT_EOK) {
+ ctx_free(ctx);
+ return ret;
+ }
+
+ ret = knotd_mod_stats_add(mod, "dropped", 1, NULL);
+ if (ret != KNOT_EOK) {
+ ctx_free(ctx);
+ return ret;
+ }
+
+ knotd_mod_ctx_set(mod, ctx);
+
+ return knotd_mod_hook(mod, KNOTD_STAGE_END, ratelimit_apply);
+}
+
+void rrl_unload(knotd_mod_t *mod)
+{
+ rrl_ctx_t *ctx = knotd_mod_ctx(mod);
+
+ knotd_conf_free(&ctx->whitelist);
+ ctx_free(ctx);
+}
+
+KNOTD_MOD_API(rrl, KNOTD_MOD_FLAG_SCOPE_ANY,
+ rrl_load, rrl_unload, rrl_conf, rrl_conf_check);
diff --git a/src/knot/modules/rrl/rrl.rst b/src/knot/modules/rrl/rrl.rst
new file mode 100644
index 0000000..3fc7892
--- /dev/null
+++ b/src/knot/modules/rrl/rrl.rst
@@ -0,0 +1,133 @@
+.. _mod-rrl:
+
+``rrl`` — Response rate limiting
+================================
+
+Response rate limiting (RRL) is a method to combat DNS reflection amplification
+attacks. These attacks rely on the fact that source address of a UDP query
+can be forged, and without a worldwide deployment of `BCP38
+<https://tools.ietf.org/html/bcp38>`_, such a forgery cannot be prevented.
+An attacker can use a DNS server (or multiple servers) as an amplification
+source and can flood a victim with a large number of unsolicited DNS responses.
+The RRL lowers the amplification factor of these attacks by sending some of
+the responses as truncated or by dropping them altogether.
+
+.. NOTE::
+ The module introduces two statistics counters. The number of slipped and
+ dropped responses.
+
+.. NOTE::
+ If the :ref:`Cookies<mod-cookies>` module is active, RRL is not applied
+ for responses with a valid DNS cookie.
+
+Example
+-------
+
+You can enable RRL by setting the module globally or per zone.
+
+::
+
+ mod-rrl:
+ - id: default
+ rate-limit: 200 # Allow 200 resp/s for each flow
+ slip: 2 # Approximately every other response slips
+
+ template:
+ - id: default
+ global-module: mod-rrl/default # Enable RRL globally
+
+Module reference
+----------------
+
+::
+
+ mod-rrl:
+ - id: STR
+ rate-limit: INT
+ slip: INT
+ table-size: INT
+ whitelist: ADDR[/INT] | ADDR-ADDR ...
+
+.. _mod-rrl_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-rrl_rate-limit:
+
+rate-limit
+..........
+
+Rate limiting is based on the token bucket scheme. A rate basically
+represents a number of tokens available each second. Each response is
+processed and classified (based on several discriminators, e.g.
+source netblock, query type, zone name, rcode, etc.). Classified responses are
+then hashed and assigned to a bucket containing number of available
+tokens, timestamp and metadata. When available tokens are exhausted,
+response is dropped or sent as truncated (see :ref:`mod-rrl_slip`).
+Number of available tokens is recalculated each second.
+
+*Required*
+
+.. _mod-rrl_table-size:
+
+table-size
+..........
+
+Size of the hash table in a number of buckets. The larger the hash table, the lesser
+the probability of a hash collision, but at the expense of additional memory costs.
+Each bucket is estimated roughly to 32 bytes. The size should be selected as
+a reasonably large prime due to better hash function distribution properties.
+Hash table is internally chained and works well up to a fill rate of 90 %, general
+rule of thumb is to select a prime near 1.2 * maximum_qps.
+
+*Default:* ``393241``
+
+.. _mod-rrl_slip:
+
+slip
+....
+
+As attacks using DNS/UDP are usually based on a forged source address,
+an attacker could deny services to the victim's netblock if all
+responses would be completely blocked. The idea behind SLIP mechanism
+is to send each N\ :sup:`th` response as truncated, thus allowing client to
+reconnect via TCP for at least some degree of service. It is worth
+noting, that some responses can't be truncated (e.g. SERVFAIL).
+
+- Setting the value to **0** will cause that all rate-limited responses will
+ be dropped. The outbound bandwidth and packet rate will be strictly capped
+ by the :ref:`mod-rrl_rate-limit` option. All legitimate requestors affected
+ by the limit will face denial of service and will observe excessive timeouts.
+ Therefore this setting is not recommended.
+
+- Setting the value to **1** will cause that all rate-limited responses will
+ be sent as truncated. The amplification factor of the attack will be reduced,
+ but the outbound data bandwidth won't be lower than the incoming bandwidth.
+ Also the outbound packet rate will be the same as without RRL.
+
+- Setting the value to **2** will cause that approximately half of the rate-limited responses
+ will be dropped, the other half will be sent as truncated. With this
+ configuration, both outbound bandwidth and packet rate will be lower than the
+ inbound. On the other hand, the dropped responses enlarge the time window
+ for possible cache poisoning attack on the resolver.
+
+- Setting the value to anything **larger than 2** will keep on decreasing
+ the outgoing rate-limited bandwidth, packet rate, and chances to notify
+ legitimate requestors to reconnect using TCP. These attributes are inversely
+ proportional to the configured value. Setting the value high is not advisable.
+
+*Default:* ``1``
+
+.. _mod-rrl_whitelist:
+
+whitelist
+.........
+
+A list of IP addresses, network subnets, or network ranges to exempt from
+rate limiting. Empty list means that no incoming connection will be
+white-listed.
+
+*Default:* not set
diff --git a/src/knot/modules/static_modules.h.in b/src/knot/modules/static_modules.h.in
new file mode 100644
index 0000000..1e1713e
--- /dev/null
+++ b/src/knot/modules/static_modules.h.in
@@ -0,0 +1,25 @@
+/* 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 "knot/include/module.h"
+
+// Forward declarations of static modules (generated by configure).
+@STATIC_MODULES_DECLARS@
+
+// STATIC_MODULES initializer (generated by configure).
+#define STATIC_MODULES_INIT @STATIC_MODULES_INIT@
diff --git a/src/knot/modules/stats/Makefile.inc b/src/knot/modules/stats/Makefile.inc
new file mode 100644
index 0000000..8952d49
--- /dev/null
+++ b/src/knot/modules/stats/Makefile.inc
@@ -0,0 +1,13 @@
+knot_modules_stats_la_SOURCES = knot/modules/stats/stats.c
+EXTRA_DIST += knot/modules/stats/stats.rst
+
+if STATIC_MODULE_stats
+libknotd_la_SOURCES += $(knot_modules_stats_la_SOURCES)
+endif
+
+if SHARED_MODULE_stats
+knot_modules_stats_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_stats_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+knot_modules_stats_la_LIBADD = $(libcontrib_LIBS)
+pkglib_LTLIBRARIES += knot/modules/stats.la
+endif
diff --git a/src/knot/modules/stats/stats.c b/src/knot/modules/stats/stats.c
new file mode 100644
index 0000000..26262ac
--- /dev/null
+++ b/src/knot/modules/stats/stats.c
@@ -0,0 +1,676 @@
+/* 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 "contrib/macros.h"
+#include "contrib/wire_ctx.h"
+#include "knot/include/module.h"
+#include "knot/nameserver/xfr.h" // Dependency on qdata->extra!
+
+#define MOD_PROTOCOL "\x10""request-protocol"
+#define MOD_OPERATION "\x10""server-operation"
+#define MOD_REQ_BYTES "\x0D""request-bytes"
+#define MOD_RESP_BYTES "\x0E""response-bytes"
+#define MOD_EDNS "\x0D""edns-presence"
+#define MOD_FLAG "\x0D""flag-presence"
+#define MOD_RCODE "\x0D""response-code"
+#define MOD_REQ_EOPT "\x13""request-edns-option"
+#define MOD_RESP_EOPT "\x14""response-edns-option"
+#define MOD_NODATA "\x0C""reply-nodata"
+#define MOD_QTYPE "\x0A""query-type"
+#define MOD_QSIZE "\x0A""query-size"
+#define MOD_RSIZE "\x0A""reply-size"
+
+#define OTHER "other"
+
+const yp_item_t stats_conf[] = {
+ { MOD_PROTOCOL, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_OPERATION, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_REQ_BYTES, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_RESP_BYTES, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_EDNS, YP_TBOOL, YP_VNONE },
+ { MOD_FLAG, YP_TBOOL, YP_VNONE },
+ { MOD_RCODE, YP_TBOOL, YP_VBOOL = { true } },
+ { MOD_REQ_EOPT, YP_TBOOL, YP_VNONE },
+ { MOD_RESP_EOPT, YP_TBOOL, YP_VNONE },
+ { MOD_NODATA, YP_TBOOL, YP_VNONE },
+ { MOD_QTYPE, YP_TBOOL, YP_VNONE },
+ { MOD_QSIZE, YP_TBOOL, YP_VNONE },
+ { MOD_RSIZE, YP_TBOOL, YP_VNONE },
+ { NULL }
+};
+
+enum {
+ CTR_PROTOCOL,
+ CTR_OPERATION,
+ CTR_REQ_BYTES,
+ CTR_RESP_BYTES,
+ CTR_EDNS,
+ CTR_FLAG,
+ CTR_RCODE,
+ CTR_REQ_EOPT,
+ CTR_RESP_EOPT,
+ CTR_NODATA,
+ CTR_QTYPE,
+ CTR_QSIZE,
+ CTR_RSIZE,
+};
+
+typedef struct {
+ bool protocol;
+ bool operation;
+ bool req_bytes;
+ bool resp_bytes;
+ bool edns;
+ bool flag;
+ bool rcode;
+ bool req_eopt;
+ bool resp_eopt;
+ bool nodata;
+ bool qtype;
+ bool qsize;
+ bool rsize;
+} stats_t;
+
+typedef struct {
+ yp_name_t *conf_name;
+ size_t conf_offset;
+ uint32_t count;
+ knotd_mod_idx_to_str_f fcn;
+} ctr_desc_t;
+
+enum {
+ OPERATION_QUERY = 0,
+ OPERATION_UPDATE,
+ OPERATION_NOTIFY,
+ OPERATION_AXFR,
+ OPERATION_IXFR,
+ OPERATION_INVALID,
+ OPERATION__COUNT
+};
+
+static char *operation_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case OPERATION_QUERY: return strdup("query");
+ case OPERATION_UPDATE: return strdup("update");
+ case OPERATION_NOTIFY: return strdup("notify");
+ case OPERATION_AXFR: return strdup("axfr");
+ case OPERATION_IXFR: return strdup("ixfr");
+ case OPERATION_INVALID: return strdup("invalid");
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ PROTOCOL_UDP4 = 0,
+ PROTOCOL_TCP4,
+ PROTOCOL_QUIC4,
+ PROTOCOL_UDP6,
+ PROTOCOL_TCP6,
+ PROTOCOL_QUIC6,
+ PROTOCOL_UDP4_XDP,
+ PROTOCOL_TCP4_XDP,
+ PROTOCOL_QUIC4_XDP,
+ PROTOCOL_UDP6_XDP,
+ PROTOCOL_TCP6_XDP,
+ PROTOCOL_QUIC6_XDP,
+ PROTOCOL__COUNT
+};
+
+static char *protocol_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case PROTOCOL_UDP4: return strdup("udp4");
+ case PROTOCOL_TCP4: return strdup("tcp4");
+ case PROTOCOL_QUIC4: return strdup("quic4");
+ case PROTOCOL_UDP6: return strdup("udp6");
+ case PROTOCOL_TCP6: return strdup("tcp6");
+ case PROTOCOL_QUIC6: return strdup("quic6");
+ case PROTOCOL_UDP4_XDP: return strdup("udp4-xdp");
+ case PROTOCOL_TCP4_XDP: return strdup("tcp4-xdp");
+ case PROTOCOL_QUIC4_XDP: return strdup("quic4-xdp");
+ case PROTOCOL_UDP6_XDP: return strdup("udp6-xdp");
+ case PROTOCOL_TCP6_XDP: return strdup("tcp6-xdp");
+ case PROTOCOL_QUIC6_XDP: return strdup("quic6-xdp");
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ REQ_BYTES_QUERY = 0,
+ REQ_BYTES_UPDATE,
+ REQ_BYTES_OTHER,
+ REQ_BYTES__COUNT
+};
+
+static char *req_bytes_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case REQ_BYTES_QUERY: return strdup("query");
+ case REQ_BYTES_UPDATE: return strdup("update");
+ case REQ_BYTES_OTHER: return strdup(OTHER);
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ RESP_BYTES_REPLY = 0,
+ RESP_BYTES_TRANSFER,
+ RESP_BYTES_OTHER,
+ RESP_BYTES__COUNT
+};
+
+static char *resp_bytes_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case RESP_BYTES_REPLY: return strdup("reply");
+ case RESP_BYTES_TRANSFER: return strdup("transfer");
+ case RESP_BYTES_OTHER: return strdup(OTHER);
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ EDNS_REQ = 0,
+ EDNS_RESP,
+ EDNS__COUNT
+};
+
+static char *edns_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case EDNS_REQ: return strdup("request");
+ case EDNS_RESP: return strdup("response");
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ FLAG_DO = 0,
+ FLAG_TC,
+ FLAG__COUNT
+};
+
+static char *flag_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case FLAG_TC: return strdup("TC");
+ case FLAG_DO: return strdup("DO");
+ default: assert(0); return NULL;
+ }
+}
+
+enum {
+ NODATA_A = 0,
+ NODATA_AAAA,
+ NODATA_OTHER,
+ NODATA__COUNT
+};
+
+static char *nodata_to_str(uint32_t idx, uint32_t count)
+{
+ switch (idx) {
+ case NODATA_A: return strdup("A");
+ case NODATA_AAAA: return strdup("AAAA");
+ case NODATA_OTHER: return strdup(OTHER);
+ default: assert(0); return NULL;
+ }
+}
+
+#define RCODE_BADSIG 15 // Unassigned code internally used for BADSIG.
+#define RCODE_OTHER (KNOT_RCODE_BADCOOKIE + 1) // Other RCODES.
+
+static char *rcode_to_str(uint32_t idx, uint32_t count)
+{
+ const knot_lookup_t *rcode = NULL;
+
+ switch (idx) {
+ case RCODE_BADSIG:
+ rcode = knot_lookup_by_id(knot_tsig_rcode_names, KNOT_RCODE_BADSIG);
+ break;
+ case RCODE_OTHER:
+ return strdup(OTHER);
+ default:
+ rcode = knot_lookup_by_id(knot_rcode_names, idx);
+ break;
+ }
+
+ if (rcode != NULL) {
+ return strdup(rcode->name);
+ } else {
+ return NULL;
+ }
+}
+
+#define EOPT_OTHER (KNOT_EDNS_MAX_OPTION_CODE + 1)
+#define req_eopt_to_str eopt_to_str
+#define resp_eopt_to_str eopt_to_str
+
+static char *eopt_to_str(uint32_t idx, uint32_t count)
+{
+ if (idx >= EOPT_OTHER) {
+ return strdup(OTHER);
+ }
+
+ char str[32];
+ if (knot_opt_code_to_string(idx, str, sizeof(str)) < 0) {
+ return NULL;
+ } else {
+ return strdup(str);
+ }
+}
+
+enum {
+ QTYPE_OTHER = 0,
+ QTYPE_MIN1 = 1,
+ QTYPE_MAX1 = 65,
+ QTYPE_MIN2 = 99,
+ QTYPE_MAX2 = 110,
+ QTYPE_MIN3 = 255,
+ QTYPE_MAX3 = 260,
+ QTYPE_SHIFT2 = QTYPE_MIN2 - QTYPE_MAX1 - 1,
+ QTYPE_SHIFT3 = QTYPE_SHIFT2 + QTYPE_MIN3 - QTYPE_MAX2 - 1,
+ QTYPE__COUNT = QTYPE_MAX3 - QTYPE_SHIFT3 + 1
+};
+
+static char *qtype_to_str(uint32_t idx, uint32_t count)
+{
+ if (idx == QTYPE_OTHER) {
+ return strdup(OTHER);
+ }
+
+ uint16_t qtype;
+
+ if (idx <= QTYPE_MAX1) {
+ qtype = idx;
+ assert(qtype >= QTYPE_MIN1 && qtype <= QTYPE_MAX1);
+ } else if (idx <= QTYPE_MAX2 - QTYPE_SHIFT2) {
+ qtype = idx + QTYPE_SHIFT2;
+ assert(qtype >= QTYPE_MIN2 && qtype <= QTYPE_MAX2);
+ } else {
+ qtype = idx + QTYPE_SHIFT3;
+ assert(qtype >= QTYPE_MIN3 && qtype <= QTYPE_MAX3);
+ }
+
+ char str[32];
+ if (knot_rrtype_to_string(qtype, str, sizeof(str)) < 0) {
+ return NULL;
+ } else {
+ return strdup(str);
+ }
+}
+
+#define BUCKET_SIZE 16
+#define QSIZE_MAX_IDX (288 / BUCKET_SIZE)
+#define RSIZE_MAX_IDX (4096 / BUCKET_SIZE)
+
+static char *size_to_str(uint32_t idx, uint32_t count)
+{
+ char str[16];
+
+ int ret;
+ if (idx < count - 1) {
+ ret = snprintf(str, sizeof(str), "%u-%u", idx * BUCKET_SIZE,
+ (idx + 1) * BUCKET_SIZE - 1);
+ } else {
+ ret = snprintf(str, sizeof(str), "%u-65535", idx * BUCKET_SIZE);
+ }
+
+ if (ret <= 0 || (size_t)ret >= sizeof(str)) {
+ return NULL;
+ } else {
+ return strdup(str);
+ }
+}
+
+static char *qsize_to_str(uint32_t idx, uint32_t count)
+{
+ return size_to_str(idx, count);
+}
+
+static char *rsize_to_str(uint32_t idx, uint32_t count)
+{
+ return size_to_str(idx, count);
+}
+
+static const ctr_desc_t ctr_descs[] = {
+ #define item(macro, name, count) \
+ [CTR_##macro] = { MOD_##macro, offsetof(stats_t, name), (count), name##_to_str }
+ item(PROTOCOL, protocol, PROTOCOL__COUNT),
+ item(OPERATION, operation, OPERATION__COUNT),
+ item(REQ_BYTES, req_bytes, REQ_BYTES__COUNT),
+ item(RESP_BYTES, resp_bytes, RESP_BYTES__COUNT),
+ item(EDNS, edns, EDNS__COUNT),
+ item(FLAG, flag, FLAG__COUNT),
+ item(RCODE, rcode, RCODE_OTHER + 1),
+ item(REQ_EOPT, req_eopt, EOPT_OTHER + 1),
+ item(RESP_EOPT, resp_eopt, EOPT_OTHER + 1),
+ item(NODATA, nodata, NODATA__COUNT),
+ item(QTYPE, qtype, QTYPE__COUNT),
+ item(QSIZE, qsize, QSIZE_MAX_IDX + 1),
+ item(RSIZE, rsize, RSIZE_MAX_IDX + 1),
+ { NULL }
+};
+
+static void incr_edns_option(knotd_mod_t *mod, unsigned thr_id, const knot_pkt_t *pkt, unsigned ctr_name)
+{
+ if (!knot_pkt_has_edns(pkt)) {
+ return;
+ }
+
+ knot_rdata_t *rdata = pkt->opt_rr->rrs.rdata;
+ if (rdata == NULL || rdata->len == 0) {
+ return;
+ }
+
+ wire_ctx_t wire = wire_ctx_init_const(rdata->data, rdata->len);
+ while (wire_ctx_available(&wire) > 0) {
+ uint16_t opt_code = wire_ctx_read_u16(&wire);
+ uint16_t opt_len = wire_ctx_read_u16(&wire);
+ wire_ctx_skip(&wire, opt_len);
+ if (wire.error != KNOT_EOK) {
+ break;
+ }
+ knotd_mod_stats_incr(mod, thr_id, ctr_name, MIN(opt_code, EOPT_OTHER), 1);
+ }
+}
+
+static knotd_state_t update_counters(knotd_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata);
+
+ stats_t *stats = knotd_mod_ctx(mod);
+
+ uint16_t operation;
+ unsigned xfr_packets = 0;
+ unsigned tid = qdata->params->thread_id;
+
+ // Get the server operation.
+ switch (qdata->type) {
+ case KNOTD_QUERY_TYPE_NORMAL:
+ operation = OPERATION_QUERY;
+ break;
+ case KNOTD_QUERY_TYPE_UPDATE:
+ operation = OPERATION_UPDATE;
+ break;
+ case KNOTD_QUERY_TYPE_NOTIFY:
+ operation = OPERATION_NOTIFY;
+ break;
+ case KNOTD_QUERY_TYPE_AXFR:
+ operation = OPERATION_AXFR;
+ if (qdata->extra->ext != NULL) {
+ xfr_packets = ((struct xfr_proc *)qdata->extra->ext)->stats.messages;
+ }
+ break;
+ case KNOTD_QUERY_TYPE_IXFR:
+ operation = OPERATION_IXFR;
+ if (qdata->extra->ext != NULL) {
+ xfr_packets = ((struct xfr_proc *)qdata->extra->ext)->stats.messages;
+ }
+ break;
+ default:
+ operation = OPERATION_INVALID;
+ break;
+ }
+
+ // Count request bytes.
+ if (stats->req_bytes) {
+ switch (operation) {
+ case OPERATION_QUERY:
+ knotd_mod_stats_incr(mod, tid, CTR_REQ_BYTES, REQ_BYTES_QUERY,
+ knot_pkt_size(qdata->query));
+ break;
+ case OPERATION_UPDATE:
+ knotd_mod_stats_incr(mod, tid, CTR_REQ_BYTES, REQ_BYTES_UPDATE,
+ knot_pkt_size(qdata->query));
+ break;
+ default:
+ if (xfr_packets <= 1) {
+ knotd_mod_stats_incr(mod, tid, CTR_REQ_BYTES, REQ_BYTES_OTHER,
+ knot_pkt_size(qdata->query));
+ }
+ break;
+ }
+ }
+
+ // Count response bytes.
+ if (stats->resp_bytes && state != KNOTD_STATE_NOOP) {
+ switch (operation) {
+ case OPERATION_QUERY:
+ knotd_mod_stats_incr(mod, tid, CTR_RESP_BYTES, RESP_BYTES_REPLY,
+ knot_pkt_size(pkt));
+ break;
+ case OPERATION_AXFR:
+ case OPERATION_IXFR:
+ knotd_mod_stats_incr(mod, tid, CTR_RESP_BYTES, RESP_BYTES_TRANSFER,
+ knot_pkt_size(pkt));
+ break;
+ default:
+ knotd_mod_stats_incr(mod, tid, CTR_RESP_BYTES, RESP_BYTES_OTHER,
+ knot_pkt_size(pkt));
+ break;
+ }
+ }
+
+ // Get the extended response code.
+ uint16_t rcode = qdata->rcode;
+ if (qdata->rcode_tsig != KNOT_RCODE_NOERROR) {
+ rcode = qdata->rcode_tsig;
+ }
+
+ // Count the response code.
+ if (stats->rcode && state != KNOTD_STATE_NOOP) {
+ if (xfr_packets <= 1 || rcode != KNOT_RCODE_NOERROR) {
+ if (xfr_packets > 1) {
+ assert(rcode != KNOT_RCODE_NOERROR);
+ // Ignore the leading XFR message NOERROR.
+ knotd_mod_stats_decr(mod, tid, CTR_RCODE,
+ KNOT_RCODE_NOERROR, 1);
+ }
+
+ if (qdata->rcode_tsig == KNOT_RCODE_BADSIG) {
+ knotd_mod_stats_incr(mod, tid, CTR_RCODE, RCODE_BADSIG, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_RCODE,
+ MIN(rcode, RCODE_OTHER), 1);
+ }
+ }
+ }
+
+ // Return if non-first transfer message.
+ if (xfr_packets > 1) {
+ return state;
+ }
+
+ // Count the server operation.
+ if (stats->operation) {
+ knotd_mod_stats_incr(mod, tid, CTR_OPERATION, operation, 1);
+ }
+
+ // Count the request protocol.
+ if (stats->protocol) {
+ bool xdp = qdata->params->xdp_msg != NULL;
+ if (knotd_qdata_remote_addr(qdata)->ss_family == AF_INET) {
+ if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_UDP4_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_UDP4, 1);
+ }
+ } else if (qdata->params->proto == KNOTD_QUERY_PROTO_QUIC) {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_QUIC4_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_QUIC4, 1);
+ }
+ } else {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_TCP4_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_TCP4, 1);
+ }
+ }
+ } else {
+ if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_UDP6_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_UDP6, 1);
+ }
+ } else if (qdata->params->proto == KNOTD_QUERY_PROTO_QUIC) {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_QUIC6_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_QUIC6, 1);
+ }
+ } else {
+ if (xdp) {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_TCP6_XDP, 1);
+ } else {
+ knotd_mod_stats_incr(mod, tid, CTR_PROTOCOL,
+ PROTOCOL_TCP6, 1);
+ }
+ }
+ }
+ }
+
+ // Count EDNS occurrences.
+ if (stats->edns) {
+ if (knot_pkt_has_edns(qdata->query)) {
+ knotd_mod_stats_incr(mod, tid, CTR_EDNS, EDNS_REQ, 1);
+ }
+ if (knot_pkt_has_edns(pkt) && state != KNOTD_STATE_NOOP) {
+ knotd_mod_stats_incr(mod, tid, CTR_EDNS, EDNS_RESP, 1);
+ }
+ }
+
+ // Count interesting message header flags.
+ if (stats->flag) {
+ if (state != KNOTD_STATE_NOOP && knot_wire_get_tc(pkt->wire)) {
+ knotd_mod_stats_incr(mod, tid, CTR_FLAG, FLAG_TC, 1);
+ }
+ if (knot_pkt_has_dnssec(pkt)) {
+ knotd_mod_stats_incr(mod, tid, CTR_FLAG, FLAG_DO, 1);
+ }
+ }
+
+ // Count EDNS options.
+ if (stats->req_eopt) {
+ incr_edns_option(mod, tid, qdata->query, CTR_REQ_EOPT);
+ }
+ if (stats->resp_eopt) {
+ incr_edns_option(mod, tid, pkt, CTR_RESP_EOPT);
+ }
+
+ // Return if not query operation.
+ if (operation != OPERATION_QUERY) {
+ return state;
+ }
+
+ // Count NODATA reply (RFC 2308, Section 2.2).
+ if (stats->nodata && rcode == KNOT_RCODE_NOERROR && state != KNOTD_STATE_NOOP &&
+ knot_wire_get_ancount(pkt->wire) == 0 && !knot_wire_get_tc(pkt->wire) &&
+ (knot_wire_get_nscount(pkt->wire) == 0 ||
+ knot_pkt_rr(knot_pkt_section(pkt, KNOT_AUTHORITY), 0)->type == KNOT_RRTYPE_SOA)) {
+ switch (knot_pkt_qtype(qdata->query)) {
+ case KNOT_RRTYPE_A:
+ knotd_mod_stats_incr(mod, tid, CTR_NODATA, NODATA_A, 1);
+ break;
+ case KNOT_RRTYPE_AAAA:
+ knotd_mod_stats_incr(mod, tid, CTR_NODATA, NODATA_AAAA, 1);
+ break;
+ default:
+ knotd_mod_stats_incr(mod, tid, CTR_NODATA, NODATA_OTHER, 1);
+ break;
+ }
+ }
+
+ // Count the query type.
+ if (stats->qtype) {
+ uint16_t qtype = knot_pkt_qtype(qdata->query);
+
+ uint16_t idx;
+ switch (qtype) {
+ case QTYPE_MIN1 ... QTYPE_MAX1: idx = qtype; break;
+ case QTYPE_MIN2 ... QTYPE_MAX2: idx = qtype - QTYPE_SHIFT2; break;
+ case QTYPE_MIN3 ... QTYPE_MAX3: idx = qtype - QTYPE_SHIFT3; break;
+ default: idx = QTYPE_OTHER; break;
+ }
+
+ knotd_mod_stats_incr(mod, tid, CTR_QTYPE, idx, 1);
+ }
+
+ // Count the query size.
+ if (stats->qsize) {
+ uint64_t idx = knot_pkt_size(qdata->query) / BUCKET_SIZE;
+ knotd_mod_stats_incr(mod, tid, CTR_QSIZE, MIN(idx, QSIZE_MAX_IDX), 1);
+ }
+
+ // Count the reply size.
+ if (stats->rsize && state != KNOTD_STATE_NOOP) {
+ uint64_t idx = knot_pkt_size(pkt) / BUCKET_SIZE;
+ knotd_mod_stats_incr(mod, tid, CTR_RSIZE, MIN(idx, RSIZE_MAX_IDX), 1);
+ }
+
+ return state;
+}
+
+int stats_load(knotd_mod_t *mod)
+{
+ stats_t *stats = calloc(1, sizeof(*stats));
+ if (stats == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ for (const ctr_desc_t *desc = ctr_descs; desc->conf_name != NULL; desc++) {
+ knotd_conf_t conf = knotd_conf_mod(mod, desc->conf_name);
+ bool enabled = conf.single.boolean;
+
+ // Initialize corresponding configuration item.
+ *(bool *)((uint8_t *)stats + desc->conf_offset) = enabled;
+
+ int ret = knotd_mod_stats_add(mod, enabled ? desc->conf_name + 1 : NULL,
+ enabled ? desc->count : 1, desc->fcn);
+ if (ret != KNOT_EOK) {
+ free(stats);
+ return ret;
+ }
+ }
+
+ knotd_mod_ctx_set(mod, stats);
+
+ return knotd_mod_hook(mod, KNOTD_STAGE_END, update_counters);
+}
+
+void stats_unload(knotd_mod_t *mod)
+{
+ free(knotd_mod_ctx(mod));
+}
+
+KNOTD_MOD_API(stats, KNOTD_MOD_FLAG_SCOPE_ANY | KNOTD_MOD_FLAG_OPT_CONF,
+ stats_load, stats_unload, stats_conf, NULL);
diff --git a/src/knot/modules/stats/stats.rst b/src/knot/modules/stats/stats.rst
new file mode 100644
index 0000000..8acf1aa
--- /dev/null
+++ b/src/knot/modules/stats/stats.rst
@@ -0,0 +1,274 @@
+.. _mod-stats:
+
+``stats`` — Query statistics
+============================
+
+The module extends server statistics with incoming DNS request and corresponding
+response counters, such as used network protocol, total number of responded bytes,
+etc (see module reference for full list of supported counters).
+This module should be configured as the last module.
+
+.. NOTE::
+ Server initiated communication (outgoing NOTIFY, incoming \*XFR,...) is not
+ counted by this module.
+
+.. NOTE::
+ Leading 16-bit message size over TCP is not considered.
+
+Example
+-------
+
+Common statistics with default module configuration::
+
+ template:
+ - id: default
+ global-module: mod-stats
+
+Per zone statistics with explicit module configuration::
+
+ mod-stats:
+ - id: custom
+ edns-presence: on
+ query-type: on
+
+ template:
+ - id: default
+ module: mod-stats/custom
+
+Module reference
+----------------
+
+::
+
+ mod-stats:
+ - id: STR
+ request-protocol: BOOL
+ server-operation: BOOL
+ request-bytes: BOOL
+ response-bytes: BOOL
+ edns-presence: BOOL
+ flag-presence: BOOL
+ response-code: BOOL
+ request-edns-option: BOOL
+ response-edns-option: BOOL
+ reply-nodata: BOOL
+ query-type: BOOL
+ query-size: BOOL
+ reply-size: BOOL
+
+.. _mod-stats_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-stats_request-protocol:
+
+request-protocol
+................
+
+If enabled, all incoming requests are counted by the network protocol:
+
+* udp4 - UDP over IPv4
+* tcp4 - TCP over IPv4
+* quic4 - QUIC over IPv4
+* udp6 - UDP over IPv6
+* tcp6 - TCP over IPv6
+* quic6 - QUIC over IPv6
+* udp4-xdp - UDP over IPv4 through XDP
+* tcp4-xdp - TCP over IPv4 through XDP
+* quic4-xdp - QUIC over IPv4 through XDP
+* udp6-xdp - UDP over IPv6 through XDP
+* tcp6-xdp - TCP over IPv6 through XDP
+* quic6-xdp - QUIC over IPv6 through XDP
+
+*Default:* ``on``
+
+.. _mod-stats_server-operation:
+
+server-operation
+................
+
+If enabled, all incoming requests are counted by the server operation. The
+server operation is based on message header OpCode and message query (meta) type:
+
+* query - Normal query operation
+* update - Dynamic update operation
+* notify - NOTIFY request operation
+* axfr - Full zone transfer operation
+* ixfr - Incremental zone transfer operation
+* invalid - Invalid server operation
+
+*Default:* ``on``
+
+.. _mod-stats_request-bytes:
+
+request-bytes
+.............
+
+If enabled, all incoming request bytes are counted by the server operation:
+
+* query - Normal query bytes
+* update - Dynamic update bytes
+* other - Other request bytes
+
+*Default:* ``on``
+
+.. _mod-stats_response-bytes:
+
+response-bytes
+..............
+
+If enabled, outgoing response bytes are counted by the server operation:
+
+* reply - Normal response bytes
+* transfer - Zone transfer bytes
+* other - Other response bytes
+
+.. WARNING::
+ Dynamic update response bytes are not counted by this module.
+
+*Default:* ``on``
+
+.. _mod-stats_edns-presence:
+
+edns-presence
+.............
+
+If enabled, EDNS pseudo section presence is counted by the message direction:
+
+* request - EDNS present in request
+* response - EDNS present in response
+
+*Default:* ``off``
+
+.. _mod-stats_flag-presence:
+
+flag-presence
+.............
+
+If enabled, some message header flags are counted:
+
+* TC - Truncated Answer in response
+* DO - DNSSEC OK in request
+
+*Default:* ``off``
+
+.. _mod-stats_response-code:
+
+response-code
+.............
+
+If enabled, outgoing response code is counted:
+
+* NOERROR
+* ...
+* NOTZONE
+* BADVERS
+* ...
+* BADCOOKIE
+* other - All other codes
+
+.. NOTE::
+ In the case of multi-message zone transfer response, just one counter is
+ incremented.
+
+.. WARNING::
+ Dynamic update response code is not counted by this module.
+
+*Default:* ``on``
+
+.. _mod-stats_request-edns-option:
+
+request-edns-option
+...................
+
+If enabled, EDNS options in requests are counted by their code:
+
+* CODE0
+* ...
+* EDNS-KEY-TAG (CODE14)
+* other - All other codes
+
+*Default:* ``off``
+
+.. _mod-stats_response-edns-option:
+
+response-edns-option
+....................
+
+If enabled, EDNS options in responses are counted by their code. See
+:ref:`mod-stats_request-edns-option`.
+
+*Default:* ``off``
+
+.. _mod-stats_reply-nodata:
+
+reply-nodata
+............
+
+If enabled, NODATA pseudo RCODE (:rfc:`2308#section-2.2`) is counted by the
+query type:
+
+* A
+* AAAA
+* other - All other types
+
+*Default:* ``off``
+
+.. _mod-stats_query-type:
+
+query-type
+..........
+
+If enabled, normal query type is counted:
+
+* A (TYPE1)
+* ...
+* TYPE65
+* SPF (TYPE99)
+* ...
+* TYPE110
+* ANY (TYPE255)
+* ...
+* TYPE260
+* other - All other types
+
+.. NOTE::
+ Not all assigned meta types (IXFR, AXFR,...) have their own counters,
+ because such types are not processed as normal query.
+
+*Default:* ``off``
+
+.. _mod-stats_query-size:
+
+query-size
+..........
+
+If enabled, normal query message size distribution is counted by the size range
+in bytes:
+
+* 0-15
+* 16-31
+* ...
+* 272-287
+* 288-65535
+
+*Default:* ``off``
+
+.. _mod-stats_reply-size:
+
+reply-size
+..........
+
+If enabled, normal reply message size distribution is counted by the size range
+in bytes:
+
+* 0-15
+* 16-31
+* ...
+* 4080-4095
+* 4096-65535
+
+*Default:* ``off``
diff --git a/src/knot/modules/synthrecord/Makefile.inc b/src/knot/modules/synthrecord/Makefile.inc
new file mode 100644
index 0000000..9fae495
--- /dev/null
+++ b/src/knot/modules/synthrecord/Makefile.inc
@@ -0,0 +1,13 @@
+knot_modules_synthrecord_la_SOURCES = knot/modules/synthrecord/synthrecord.c
+EXTRA_DIST += knot/modules/synthrecord/synthrecord.rst
+
+if STATIC_MODULE_synthrecord
+libknotd_la_SOURCES += $(knot_modules_synthrecord_la_SOURCES)
+endif
+
+if SHARED_MODULE_synthrecord
+knot_modules_synthrecord_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_synthrecord_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+knot_modules_synthrecord_la_LIBADD = $(libcontrib_LIBS)
+pkglib_LTLIBRARIES += knot/modules/synthrecord.la
+endif
diff --git a/src/knot/modules/synthrecord/synthrecord.c b/src/knot/modules/synthrecord/synthrecord.c
new file mode 100644
index 0000000..d7af9a1
--- /dev/null
+++ b/src/knot/modules/synthrecord/synthrecord.c
@@ -0,0 +1,625 @@
+/* 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 "contrib/ctype.h"
+#include "contrib/macros.h"
+#include "contrib/net.h"
+#include "contrib/sockaddr.h"
+#include "contrib/wire_ctx.h"
+#include "knot/include/module.h"
+
+#define MOD_NET "\x07""network"
+#define MOD_ORIGIN "\x06""origin"
+#define MOD_PREFIX "\x06""prefix"
+#define MOD_TTL "\x03""ttl"
+#define MOD_TYPE "\x04""type"
+#define MOD_SHORT "\x0d""reverse-short"
+
+/*! \brief Supported answer synthesis template types. */
+enum synth_template_type {
+ SYNTH_NULL = 0,
+ SYNTH_FORWARD = 1,
+ SYNTH_REVERSE = 2
+};
+
+static const knot_lookup_t synthetic_types[] = {
+ { SYNTH_FORWARD, "forward" },
+ { SYNTH_REVERSE, "reverse" },
+ { 0, NULL }
+};
+
+int check_prefix(knotd_conf_check_args_t *args)
+{
+ if (strchr((const char *)args->data, '.') != NULL) {
+ args->err_str = "dot '.' is not allowed";
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+const yp_item_t synth_record_conf[] = {
+ { MOD_TYPE, YP_TOPT, YP_VOPT = { synthetic_types, SYNTH_NULL } },
+ { MOD_PREFIX, YP_TSTR, YP_VSTR = { "" }, YP_FNONE, { check_prefix } },
+ { MOD_ORIGIN, YP_TDNAME, YP_VNONE },
+ { MOD_TTL, YP_TINT, YP_VINT = { 0, UINT32_MAX, 3600, YP_STIME } },
+ { MOD_NET, YP_TNET, YP_VNONE, YP_FMULTI },
+ { MOD_SHORT, YP_TBOOL, YP_VBOOL = { true } },
+ { NULL }
+};
+
+int synth_record_conf_check(knotd_conf_check_args_t *args)
+{
+ // Check type.
+ knotd_conf_t type = knotd_conf_check_item(args, MOD_TYPE);
+ if (type.count == 0) {
+ args->err_str = "no synthesis type specified";
+ return KNOT_EINVAL;
+ }
+
+ // Check origin.
+ knotd_conf_t origin = knotd_conf_check_item(args, MOD_ORIGIN);
+ if (origin.count == 0 && type.single.option == SYNTH_REVERSE) {
+ args->err_str = "no origin specified";
+ return KNOT_EINVAL;
+ }
+ if (origin.count != 0 && type.single.option == SYNTH_FORWARD) {
+ args->err_str = "origin not allowed with forward type";
+ return KNOT_EINVAL;
+ }
+
+ // Check network subnet.
+ knotd_conf_t net = knotd_conf_check_item(args, MOD_NET);
+ if (net.count == 0) {
+ args->err_str = "no network subnet specified";
+ return KNOT_EINVAL;
+ }
+ knotd_conf_free(&net);
+
+ // Check reverse-short parameter is only for reverse synthrecord.
+ knotd_conf_t reverse_short = knotd_conf_check_item(args, MOD_SHORT);
+ if (reverse_short.count != 0 && type.single.option == SYNTH_FORWARD) {
+ args->err_str = "reverse-short not allowed with forward type";
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+#define ARPA_ZONE_LABELS 2
+#define IPV4_ADDR_LABELS 4
+#define IPV6_ADDR_LABELS 32
+#define IPV4_ARPA_DNAME (uint8_t *)"\x07""in-addr""\x04""arpa"
+#define IPV6_ARPA_DNAME (uint8_t *)"\x03""ip6""\x04""arpa"
+#define IPV4_ARPA_LEN 14
+#define IPV6_ARPA_LEN 10
+
+/*!
+ * \brief Synthetic response template.
+ */
+typedef struct {
+ struct sockaddr_storage addr;
+ struct sockaddr_storage addr_max;
+ int addr_mask;
+} synth_templ_addr_t;
+
+typedef struct {
+ enum synth_template_type type;
+ char *prefix;
+ size_t prefix_len;
+ char *zone;
+ size_t zone_len;
+ uint32_t ttl;
+ size_t addr_count;
+ synth_templ_addr_t *addr;
+ bool reverse_short;
+} synth_template_t;
+
+typedef union {
+ uint32_t b32;
+ uint8_t b4[4];
+} addr_block_t;
+
+/*! \brief Write one IPV4 address block without redundant leading zeros. */
+static unsigned block_write(addr_block_t *block, char *addr_str)
+{
+ unsigned len = 0;
+
+ if (block->b4[0] != '0') {
+ addr_str[len++] = block->b4[0];
+ }
+ if (len > 0 || block->b4[1] != '0') {
+ addr_str[len++] = block->b4[1];
+ }
+ if (len > 0 || block->b4[2] != '0') {
+ addr_str[len++] = block->b4[2];
+ }
+ addr_str[len++] = block->b4[3];
+
+ return len;
+}
+
+/*! \brief Substitute all occurrences of given character. */
+static void str_subst(char *str, size_t len, char from, char to)
+{
+ for (int i = 0; i < len; ++i) {
+ if (str[i] == from) {
+ str[i] = to;
+ }
+ }
+}
+
+/*! \brief Separator character for address family. */
+static char str_separator(int addr_family)
+{
+ return (addr_family == AF_INET6) ? ':' : '.';
+}
+
+/*! \brief Return true if query type is satisfied with provided address family. */
+static bool query_satisfied_by_family(uint16_t qtype, int family)
+{
+ switch (qtype) {
+ case KNOT_RRTYPE_A: return family == AF_INET;
+ case KNOT_RRTYPE_AAAA: return family == AF_INET6;
+ case KNOT_RRTYPE_ANY: return true;
+ default: return false;
+ }
+}
+
+/*! \brief Parse address from reverse query QNAME and return address family. */
+static int reverse_addr_parse(knotd_qdata_t *qdata, const synth_template_t *tpl,
+ char *addr_str, int *addr_family, bool *parent)
+{
+ /* QNAME required format is [address].[subnet/zone]
+ * f.e. [1.0...0].[h.g.f.e.0.0.0.0.d.c.b.a.ip6.arpa] represents
+ * [abcd:0:efgh::1] */
+ const knot_dname_t *label = qdata->name; // uncompressed name
+
+ static const char ipv4_zero[] = "0.0.0.0";
+
+ bool can_ipv4 = true;
+ bool can_ipv6 = true;
+ unsigned labels = 0;
+
+ uint8_t buf4[16], *buf4_end = buf4 + sizeof(buf4), *buf4_pos = buf4_end;
+ uint8_t buf6[32], *buf6_end = buf6 + sizeof(buf6), *buf6_pos = buf6_end;
+
+ for ( ; labels < IPV6_ADDR_LABELS; labels++) {
+ if (unlikely(*label == 0)) {
+ return KNOT_EINVAL;
+ }
+ if (label[1] == 'i') {
+ break;
+ }
+ if (labels < IPV4_ADDR_LABELS) {
+ switch (*label) {
+ case 1:
+ assert(buf4 + 1 < buf4_pos && buf6 < buf6_pos);
+ *--buf6_pos = label[1];
+ *--buf4_pos = label[1];
+ *--buf4_pos = '.';
+ break;
+ case 2:
+ case 3:
+ assert(buf4 + *label < buf4_pos);
+ can_ipv6 = false;
+ buf4_pos -= *label;
+ memcpy(buf4_pos, label + 1, *label);
+ *--buf4_pos = '.';
+ break;
+ case 4:
+ case 5:
+ case 6: // Ignore second possibly classless label (e.g. 0/25, 193/26).
+ if (labels-- != 1) {
+ return KNOT_EINVAL;
+ }
+ can_ipv6 = false;
+ break;
+ default:
+ return KNOT_EINVAL;
+ }
+ } else {
+ can_ipv4 = false;
+ if (!can_ipv6 || *label != 1) {
+ return KNOT_EINVAL;
+ }
+ assert(buf6 < buf6_pos);
+ *--buf6_pos = label[1];
+
+ }
+ label += *label + sizeof(*label);
+ }
+
+ if (can_ipv4 && knot_dname_is_equal(label, IPV4_ARPA_DNAME)) {
+ *addr_family = AF_INET;
+ *parent = (labels < IPV4_ADDR_LABELS);
+ int buf4_overweight = (buf4_end - buf4_pos) - (2 * labels);
+ assert(buf4_overweight >= 0);
+ memcpy(addr_str + buf4_overweight, ipv4_zero, sizeof(ipv4_zero));
+ if (labels > 0) {
+ buf4_pos++; // skip leading '.'
+ memcpy(addr_str, buf4_pos, buf4_end - buf4_pos);
+ }
+ return KNOT_EOK;
+ } else if (can_ipv6 && knot_dname_is_equal(label, IPV6_ARPA_DNAME)) {
+ *addr_family = AF_INET6;
+ *parent = (labels < IPV6_ADDR_LABELS);
+
+ addr_block_t blocks[8] = { { 0 } };
+ int compr_start = -1, compr_end = -1;
+
+ unsigned buf6_len = buf6_end - buf6_pos;
+ memcpy(blocks, buf6_pos, buf6_len);
+ memset(((uint8_t *)blocks) + buf6_len, 0x30, sizeof(blocks) - buf6_len);
+
+ for (int i = 0; i < 8; i++) {
+ addr_block_t *block = &blocks[i];
+
+ /* The Unicode string MUST NOT contain "--" in the third and fourth
+ character positions and MUST NOT start or end with a "-".
+ So we will not compress first, second, and last address blocks
+ for simplicity. And we will not compress a single block.
+
+ i: 0 1 2 3 4 5 6 7
+ label block: H:G:F:E:D:C:B:A
+ address block: A B C D E F G H
+ compressibles: 0 0 0 0 0
+ 0 0 0 0
+ 0 0 0
+ 0 0
+ */
+ // Check for trailing zero dual-blocks.
+ if (tpl->reverse_short && i > 1 && i < 6 &&
+ block[0].b32 == 0x30303030UL && block[1].b32 == 0x30303030UL) {
+ if (compr_start == -1) {
+ compr_start = i;
+ }
+ } else {
+ if (compr_start != -1 && compr_end == -1) {
+ compr_end = i;
+ }
+ }
+ }
+
+ // Write address blocks.
+ unsigned addr_len = 0;
+ for (int i = 0; i < 8; i++) {
+ if (compr_start == -1 || i < compr_start || i > compr_end) {
+ // Write regular address block.
+ if (tpl->reverse_short) {
+ addr_len += block_write(&blocks[i], addr_str + addr_len);
+ } else {
+ assert(sizeof(blocks[i]) == 4);
+ memcpy(addr_str + addr_len, &blocks[i], 4);
+ addr_len += 4;
+ }
+ // Write separator
+ if (i < 7) {
+ addr_str[addr_len++] = ':';
+ }
+ } else if (compr_start != -1 && compr_end == i) {
+ // Write compression double colon.
+ addr_str[addr_len++] = ':';
+ }
+ }
+ addr_str[addr_len] = '\0';
+
+ return KNOT_EOK;
+ }
+
+ return KNOT_EINVAL;
+}
+
+static int forward_addr_parse(knotd_qdata_t *qdata, const synth_template_t *tpl,
+ char *addr_str, int *addr_family)
+{
+ const knot_dname_t *label = qdata->name;
+
+ // Check for prefix mismatch.
+ if (label[0] <= tpl->prefix_len ||
+ memcmp(label + 1, tpl->prefix, tpl->prefix_len) != 0) {
+ return KNOT_EINVAL;
+ }
+
+ // Copy address part.
+ unsigned addr_len = label[0] - tpl->prefix_len;
+ memcpy(addr_str, label + 1 + tpl->prefix_len, addr_len);
+ addr_str[addr_len] = '\0';
+
+ // Determine address family.
+ unsigned hyphen_cnt = 0;
+ const char *ch = addr_str;
+ while (hyphen_cnt < 4 && ch < addr_str + addr_len) {
+ if (*ch == '-') {
+ hyphen_cnt++;
+ if (*++ch == '-') { // Check for shortened IPv6 notation.
+ hyphen_cnt = 4;
+ break;
+ }
+ }
+ ch++;
+ }
+ // Valid IPv4 address looks like A-B-C-D.
+ *addr_family = (hyphen_cnt == 3) ? AF_INET : AF_INET6;
+
+ // Restore correct address format.
+ const char sep = str_separator(*addr_family);
+ str_subst(addr_str, addr_len, '-', sep);
+
+ return KNOT_EOK;
+}
+
+static int addr_parse(knotd_qdata_t *qdata, const synth_template_t *tpl, char *addr_str,
+ int *addr_family, bool *parent)
+{
+ switch (tpl->type) {
+ case SYNTH_REVERSE: return reverse_addr_parse(qdata, tpl, addr_str, addr_family, parent);
+ case SYNTH_FORWARD: return forward_addr_parse(qdata, tpl, addr_str, addr_family);
+ default: return KNOT_EINVAL;
+ }
+}
+
+static knot_dname_t *synth_ptrname(uint8_t *out, const char *addr_str,
+ const synth_template_t *tpl, int addr_family)
+{
+ knot_dname_txt_storage_t ptrname;
+ int addr_len = strlen(addr_str);
+ const char sep = str_separator(addr_family);
+
+ // PTR right-hand value is [prefix][address][zone]
+ wire_ctx_t ctx = wire_ctx_init((uint8_t *)ptrname, sizeof(ptrname));
+ wire_ctx_write(&ctx, tpl->prefix, tpl->prefix_len);
+ wire_ctx_write(&ctx, addr_str, addr_len);
+ wire_ctx_write_u8(&ctx, '.');
+ wire_ctx_write(&ctx, tpl->zone, tpl->zone_len);
+ wire_ctx_write_u8(&ctx, '\0');
+ if (ctx.error != KNOT_EOK) {
+ return NULL;
+ }
+
+ // Substitute address separator by '-'.
+ str_subst(ptrname + tpl->prefix_len, addr_len, sep, '-');
+
+ // Convert to domain name.
+ return knot_dname_from_str(out, ptrname, KNOT_DNAME_MAXLEN);
+}
+
+static int reverse_rr(char *addr_str, const synth_template_t *tpl, knot_pkt_t *pkt,
+ knot_rrset_t *rr, int addr_family)
+{
+ // Synthesize PTR record data.
+ knot_dname_storage_t ptrname;
+ if (synth_ptrname(ptrname, addr_str, tpl, addr_family) == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ rr->type = KNOT_RRTYPE_PTR;
+ knot_rrset_add_rdata(rr, ptrname, knot_dname_size(ptrname), &pkt->mm);
+
+ return KNOT_EOK;
+}
+
+static int forward_rr(char *addr_str, const synth_template_t *tpl, knot_pkt_t *pkt,
+ knot_rrset_t *rr, int addr_family)
+{
+ struct sockaddr_storage query_addr;
+ sockaddr_set(&query_addr, addr_family, addr_str, 0);
+
+ // Specify address type and data.
+ if (addr_family == AF_INET6) {
+ rr->type = KNOT_RRTYPE_AAAA;
+ const struct sockaddr_in6* ip = (const struct sockaddr_in6*)&query_addr;
+ knot_rrset_add_rdata(rr, (const uint8_t *)&ip->sin6_addr,
+ sizeof(struct in6_addr), &pkt->mm);
+ } else if (addr_family == AF_INET) {
+ rr->type = KNOT_RRTYPE_A;
+ const struct sockaddr_in* ip = (const struct sockaddr_in*)&query_addr;
+ knot_rrset_add_rdata(rr, (const uint8_t *)&ip->sin_addr,
+ sizeof(struct in_addr), &pkt->mm);
+ } else {
+ return KNOT_EINVAL;
+ }
+
+ return KNOT_EOK;
+}
+
+static knot_rrset_t *synth_rr(char *addr_str, const synth_template_t *tpl, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, int addr_family)
+{
+ knot_rrset_t *rr = knot_rrset_new(qdata->name, 0, KNOT_CLASS_IN, tpl->ttl,
+ &pkt->mm);
+ if (rr == NULL) {
+ return NULL;
+ }
+
+ // Fill in the specific data.
+ int ret = KNOT_ERROR;
+ switch (tpl->type) {
+ case SYNTH_REVERSE: ret = reverse_rr(addr_str, tpl, pkt, rr, addr_family); break;
+ case SYNTH_FORWARD: ret = forward_rr(addr_str, tpl, pkt, rr, addr_family); break;
+ default: break;
+ }
+
+ if (ret != KNOT_EOK) {
+ knot_rrset_free(rr, &pkt->mm);
+ return NULL;
+ }
+
+ return rr;
+}
+
+/*! \brief Check if query fits the template requirements. */
+static knotd_in_state_t template_match(knotd_in_state_t state, const synth_template_t *tpl,
+ knot_pkt_t *pkt, knotd_qdata_t *qdata)
+{
+ int provided_af = AF_UNSPEC;
+ struct sockaddr_storage query_addr;
+ char addr_str[SOCKADDR_STRLEN];
+ assert(SOCKADDR_STRLEN > KNOT_DNAME_MAXLABELLEN);
+ bool parent = false; // querying empty-non-terminal being (possibly indirect) parent of synthesized name
+
+ // Parse address from query name.
+ if (addr_parse(qdata, tpl, addr_str, &provided_af, &parent) != KNOT_EOK ||
+ sockaddr_set(&query_addr, provided_af, addr_str, 0) != KNOT_EOK) {
+ return state;
+ }
+
+ // Try all available addresses.
+ int i;
+ for (i = 0; i < tpl->addr_count; i++) {
+ if (tpl->addr[i].addr_max.ss_family == AF_UNSPEC) {
+ if (sockaddr_net_match(&query_addr, &tpl->addr[i].addr,
+ tpl->addr[i].addr_mask)) {
+ break;
+ }
+ } else {
+ if (sockaddr_range_match(&query_addr, &tpl->addr[i].addr,
+ &tpl->addr[i].addr_max)) {
+ break;
+ }
+ }
+ }
+ if (i >= tpl->addr_count) {
+ return state;
+ }
+
+ // Check if the request is for an available query type.
+ uint16_t qtype = knot_pkt_qtype(qdata->query);
+ switch (tpl->type) {
+ case SYNTH_FORWARD:
+ assert(!parent);
+ if (!query_satisfied_by_family(qtype, provided_af)) {
+ qdata->rcode = KNOT_RCODE_NOERROR;
+ return KNOTD_IN_STATE_NODATA;
+ }
+ break;
+ case SYNTH_REVERSE:
+ if (parent || (qtype != KNOT_RRTYPE_PTR && qtype != KNOT_RRTYPE_ANY)) {
+ qdata->rcode = KNOT_RCODE_NOERROR;
+ return KNOTD_IN_STATE_NODATA;
+ }
+ break;
+ default:
+ return state;
+ }
+
+ // Synthesize record from template.
+ knot_rrset_t *rr = synth_rr(addr_str, tpl, pkt, qdata, provided_af);
+ if (rr == NULL) {
+ qdata->rcode = KNOT_RCODE_SERVFAIL;
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ // Insert synthetic response into packet.
+ if (knot_pkt_put(pkt, 0, rr, KNOT_PF_FREE) != KNOT_EOK) {
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ // Authoritative response.
+ knot_wire_set_aa(pkt->wire);
+
+ return KNOTD_IN_STATE_HIT;
+}
+
+static knotd_in_state_t solve_synth_record(knotd_in_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata && mod);
+
+ // Applicable when search in zone fails.
+ if (state != KNOTD_IN_STATE_MISS) {
+ return state;
+ }
+
+ // Check if template fits.
+ return template_match(state, knotd_mod_ctx(mod), pkt, qdata);
+}
+
+int synth_record_load(knotd_mod_t *mod)
+{
+ // Create synthesis template.
+ synth_template_t *tpl = calloc(1, sizeof(*tpl));
+ if (tpl == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Set type.
+ knotd_conf_t conf = knotd_conf_mod(mod, MOD_TYPE);
+ tpl->type = conf.single.option;
+
+ /* Set prefix. */
+ conf = knotd_conf_mod(mod, MOD_PREFIX);
+ tpl->prefix = strdup(conf.single.string);
+ tpl->prefix_len = strlen(tpl->prefix);
+
+ // Set origin if generating reverse record.
+ if (tpl->type == SYNTH_REVERSE) {
+ conf = knotd_conf_mod(mod, MOD_ORIGIN);
+ tpl->zone = knot_dname_to_str_alloc(conf.single.dname);
+ if (tpl->zone == NULL) {
+ free(tpl->prefix);
+ free(tpl);
+ return KNOT_ENOMEM;
+ }
+ tpl->zone_len = strlen(tpl->zone);
+ }
+
+ // Set ttl.
+ conf = knotd_conf_mod(mod, MOD_TTL);
+ tpl->ttl = conf.single.integer;
+
+ // Set address.
+ conf = knotd_conf_mod(mod, MOD_NET);
+ tpl->addr_count = conf.count;
+ tpl->addr = calloc(conf.count, sizeof(*tpl->addr));
+ if (tpl->addr == NULL) {
+ knotd_conf_free(&conf);
+ free(tpl->zone);
+ free(tpl->prefix);
+ free(tpl);
+ return KNOT_ENOMEM;
+ }
+ for (size_t i = 0; i < conf.count; i++) {
+ tpl->addr[i].addr = conf.multi[i].addr;
+ tpl->addr[i].addr_max = conf.multi[i].addr_max;
+ tpl->addr[i].addr_mask = conf.multi[i].addr_mask;
+ }
+ knotd_conf_free(&conf);
+
+ // Set address shortening.
+ if (tpl->type == SYNTH_REVERSE) {
+ conf = knotd_conf_mod(mod, MOD_SHORT);
+ tpl->reverse_short = conf.single.boolean;
+ }
+
+ knotd_mod_ctx_set(mod, tpl);
+
+ return knotd_mod_in_hook(mod, KNOTD_STAGE_ANSWER, solve_synth_record);
+}
+
+void synth_record_unload(knotd_mod_t *mod)
+{
+ synth_template_t *tpl = knotd_mod_ctx(mod);
+
+ free(tpl->addr);
+ free(tpl->zone);
+ free(tpl->prefix);
+ free(tpl);
+}
+
+KNOTD_MOD_API(synthrecord, KNOTD_MOD_FLAG_SCOPE_ZONE,
+ synth_record_load, synth_record_unload, synth_record_conf,
+ synth_record_conf_check);
diff --git a/src/knot/modules/synthrecord/synthrecord.rst b/src/knot/modules/synthrecord/synthrecord.rst
new file mode 100644
index 0000000..4ad0a4b
--- /dev/null
+++ b/src/knot/modules/synthrecord/synthrecord.rst
@@ -0,0 +1,170 @@
+.. _mod-synthrecord:
+
+``synthrecord`` – Automatic forward/reverse records
+===================================================
+
+This module is able to synthesize either forward or reverse records for
+a given prefix and subnet.
+
+Records are synthesized only if the query can't be satisfied from the zone.
+Both IPv4 and IPv6 are supported.
+
+Example
+-------
+
+Automatic forward records
+.........................
+
+::
+
+ mod-synthrecord:
+ - id: test1
+ type: forward
+ prefix: dynamic-
+ ttl: 400
+ network: 2620:0:b61::/52
+
+ zone:
+ - domain: test.
+ file: test.zone # Must exist
+ module: mod-synthrecord/test1
+
+Result:
+
+.. code-block:: console
+
+ $ kdig AAAA dynamic-2620-0-b61-100--1.test.
+ ...
+ ;; QUESTION SECTION:
+ ;; dynamic-2620-0-b61-100--1.test. IN AAAA
+
+ ;; ANSWER SECTION:
+ dynamic-2620-0-b61-100--1.test. 400 IN AAAA 2620:0:b61:100::1
+
+You can also have CNAME aliases to the dynamic records, which are going to be
+further resolved:
+
+.. code-block:: console
+
+ $ kdig AAAA alias.test.
+ ...
+ ;; QUESTION SECTION:
+ ;; alias.test. IN AAAA
+
+ ;; ANSWER SECTION:
+ alias.test. 3600 IN CNAME dynamic-2620-0-b61-100--2.test.
+ dynamic-2620-0-b61-100--2.test. 400 IN AAAA 2620:0:b61:100::2
+
+Automatic reverse records
+.........................
+
+::
+
+ mod-synthrecord:
+ - id: test2
+ type: reverse
+ prefix: dynamic-
+ origin: test
+ ttl: 400
+ network: 2620:0:b61::/52
+
+ zone:
+ - domain: 1.6.b.0.0.0.0.0.0.2.6.2.ip6.arpa.
+ file: 1.6.b.0.0.0.0.0.0.2.6.2.ip6.arpa.zone # Must exist
+ module: mod-synthrecord/test2
+
+Result:
+
+.. code-block:: console
+
+ $ kdig -x 2620:0:b61::1
+ ...
+ ;; QUESTION SECTION:
+ ;; 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.6.b.0.0.0.0.0.0.2.6.2.ip6.arpa. IN PTR
+
+ ;; ANSWER SECTION:
+ 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.6.b.0.0.0.0.0.0.2.6.2.ip6.arpa. 400 IN PTR dynamic-2620-0-b61--1.test.
+
+Module reference
+----------------
+
+::
+
+ mod-synthrecord:
+ - id: STR
+ type: forward | reverse
+ prefix: STR
+ origin: DNAME
+ ttl: INT
+ network: ADDR[/INT] | ADDR-ADDR ...
+ reverse-short: BOOL
+
+.. _mod-synthrecord_id:
+
+id
+..
+
+A module identifier.
+
+.. _mod-synthrecord_type:
+
+type
+....
+
+The type of generated records.
+
+Possible values:
+
+- ``forward`` – Forward records
+- ``reverse`` – Reverse records
+
+*Required*
+
+.. _mod-synthrecord_prefix:
+
+prefix
+......
+
+A record owner prefix.
+
+.. NOTE::
+ The value doesn’t allow dots, address parts in the synthetic names are
+ separated with a dash.
+
+*Default:* empty
+
+.. _mod-synthrecord_origin:
+
+origin
+......
+
+A zone origin (only valid for the :ref:`reverse type<mod-synthrecord_type>`).
+
+*Required*
+
+.. _mod-synthrecord_ttl:
+
+ttl
+...
+
+Time to live of the generated records.
+
+*Default:* ``3600``
+
+.. _mod-synthrecord_network:
+
+network
+.......
+
+An IP address, a network subnet, or a network range the query must match.
+
+*Required*
+
+.. _mod-synthrecord_reverse-short:
+
+reverse-short
+.............
+
+If enabled, a shortened IPv6 address can be used for reverse record rdata synthesis.
+
+*Default:* ``on``
diff --git a/src/knot/modules/whoami/Makefile.inc b/src/knot/modules/whoami/Makefile.inc
new file mode 100644
index 0000000..4d20fcb
--- /dev/null
+++ b/src/knot/modules/whoami/Makefile.inc
@@ -0,0 +1,12 @@
+knot_modules_whoami_la_SOURCES = knot/modules/whoami/whoami.c
+EXTRA_DIST += knot/modules/whoami/whoami.rst
+
+if STATIC_MODULE_whoami
+libknotd_la_SOURCES += $(knot_modules_whoami_la_SOURCES)
+endif
+
+if SHARED_MODULE_whoami
+knot_modules_whoami_la_LDFLAGS = $(KNOTD_MOD_LDFLAGS)
+knot_modules_whoami_la_CPPFLAGS = $(KNOTD_MOD_CPPFLAGS)
+pkglib_LTLIBRARIES += knot/modules/whoami.la
+endif
diff --git a/src/knot/modules/whoami/whoami.c b/src/knot/modules/whoami/whoami.c
new file mode 100644
index 0000000..99c4372
--- /dev/null
+++ b/src/knot/modules/whoami/whoami.c
@@ -0,0 +1,114 @@
+/* Copyright (C) 2017 Fastly, Inc.
+
+ 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 <netinet/in.h>
+
+#include "knot/include/module.h"
+
+static knotd_in_state_t whoami_query(knotd_in_state_t state, knot_pkt_t *pkt,
+ knotd_qdata_t *qdata, knotd_mod_t *mod)
+{
+ assert(pkt && qdata);
+
+ const knot_dname_t *zone_name = knotd_qdata_zone_name(qdata);
+ if (zone_name == NULL) {
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ /* Retrieve the query tuple. */
+ const knot_dname_t *qname = knot_pkt_qname(qdata->query);
+ const uint16_t qtype = knot_pkt_qtype(qdata->query);
+ const uint16_t qclass = knot_pkt_qclass(qdata->query);
+
+ /* We only generate A and AAAA records, which are Internet class. */
+ if (qclass != KNOT_CLASS_IN) {
+ return state;
+ }
+
+ /* Only handle queries with qname set to the zone name. */
+ if (!knot_dname_is_equal(qname, zone_name)) {
+ return state;
+ }
+
+ /* Only handle A and AAAA queries. */
+ if (qtype != KNOT_RRTYPE_A && qtype != KNOT_RRTYPE_AAAA) {
+ return state;
+ }
+
+ /* Retrieve the IP address that sent the query. */
+ const struct sockaddr_storage *query_source = knotd_qdata_remote_addr(qdata);
+ if (query_source == NULL) {
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ /* If the socket address family corresponds to the query type (i.e.,
+ * AF_INET <-> A and AF_INET6 <-> AAAA), put the socket address and
+ * length into 'rdata' and 'len_rdata'.
+ */
+ const void *rdata = NULL;
+ uint16_t len_rdata = 0;
+ if (query_source->ss_family == AF_INET && qtype == KNOT_RRTYPE_A) {
+ const struct sockaddr_in *sai = (struct sockaddr_in *)query_source;
+ rdata = &sai->sin_addr.s_addr;
+ len_rdata = sizeof(sai->sin_addr.s_addr);
+ } else if (query_source->ss_family == AF_INET6 && qtype == KNOT_RRTYPE_AAAA) {
+ const struct sockaddr_in6 *sai6 = (struct sockaddr_in6 *)query_source;
+ rdata = &sai6->sin6_addr;
+ len_rdata = sizeof(sai6->sin6_addr);
+ } else {
+ /* Query type didn't match address family. */
+ return state;
+ }
+
+ /* Synthesize the response RRset. */
+
+ /* TTL is taken from the TTL of the SOA record. */
+ knot_rrset_t soa = knotd_qdata_zone_apex_rrset(qdata, KNOT_RRTYPE_SOA);
+
+ /* Owner name, type, and class are taken from the question. */
+ knot_rrset_t *rrset = knot_rrset_new(qname, qtype, qclass, soa.ttl, &pkt->mm);
+ if (rrset == NULL) {
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ /* Record data is the query source address. */
+ int ret = knot_rrset_add_rdata(rrset, rdata, len_rdata, &pkt->mm);
+ if (ret != KNOT_EOK) {
+ knot_rrset_free(rrset, &pkt->mm);
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ /* Add the new RRset to the response packet. */
+ ret = knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, rrset, KNOT_PF_FREE);
+ if (ret != KNOT_EOK) {
+ knot_rrset_free(rrset, &pkt->mm);
+ return KNOTD_IN_STATE_ERROR;
+ }
+
+ /* Success. */
+ return KNOTD_IN_STATE_HIT;
+}
+
+int whoami_load(knotd_mod_t *mod)
+{
+ /* Hook to the query plan. */
+ knotd_mod_in_hook(mod, KNOTD_STAGE_ANSWER, whoami_query);
+
+ return KNOT_EOK;
+}
+
+KNOTD_MOD_API(whoami, KNOTD_MOD_FLAG_SCOPE_ZONE | KNOTD_MOD_FLAG_OPT_CONF,
+ whoami_load, NULL, NULL, NULL);
diff --git a/src/knot/modules/whoami/whoami.rst b/src/knot/modules/whoami/whoami.rst
new file mode 100644
index 0000000..25d0174
--- /dev/null
+++ b/src/knot/modules/whoami/whoami.rst
@@ -0,0 +1,97 @@
+.. _mod-whoami:
+
+``whoami`` — Whoami response
+============================
+
+The module synthesizes an A or AAAA record containing the query source IP address,
+at the apex of the zone being served. It makes sure to allow Knot DNS to generate
+cacheable negative responses, and to allow fallback to extra records defined in the
+underlying zone file. The TTL of the synthesized record is copied from
+the TTL of the SOA record in the zone file.
+
+Because a DNS query for type A or AAAA has nothing to do with whether
+the query occurs over IPv4 or IPv6, this module requires a special
+zone configuration to support both address families. For A queries, the
+underlying zone must have a set of nameservers that only have IPv4
+addresses, and for AAAA queries, the underlying zone must have a set of
+nameservers that only have IPv6 addresses.
+
+Example
+-------
+
+To enable this module, you need to add something like the following to
+the Knot DNS configuration file::
+
+ zone:
+ - domain: whoami.domain.example
+ file: "/path/to/whoami.domain.example"
+ module: mod-whoami
+
+ zone:
+ - domain: whoami6.domain.example
+ file: "/path/to/whoami6.domain.example"
+ module: mod-whoami
+
+The whoami.domain.example zone file example:
+
+ .. code-block:: none
+
+ $TTL 1
+
+ @ SOA (
+ whoami.domain.example. ; MNAME
+ hostmaster.domain.example. ; RNAME
+ 2016051300 ; SERIAL
+ 86400 ; REFRESH
+ 86400 ; RETRY
+ 86400 ; EXPIRE
+ 1 ; MINIMUM
+ )
+
+ $TTL 86400
+
+ @ NS ns1.whoami.domain.example.
+ @ NS ns2.whoami.domain.example.
+ @ NS ns3.whoami.domain.example.
+ @ NS ns4.whoami.domain.example.
+
+ ns1 A 198.51.100.53
+ ns2 A 192.0.2.53
+ ns3 A 203.0.113.53
+ ns4 A 198.19.123.53
+
+The whoami6.domain.example zone file example:
+
+ .. code-block:: none
+
+ $TTL 1
+
+ @ SOA (
+ whoami6.domain.example. ; MNAME
+ hostmaster.domain.example. ; RNAME
+ 2016051300 ; SERIAL
+ 86400 ; REFRESH
+ 86400 ; RETRY
+ 86400 ; EXPIRE
+ 1 ; MINIMUM
+ )
+
+ $TTL 86400
+
+ @ NS ns1.whoami6.domain.example.
+ @ NS ns2.whoami6.domain.example.
+ @ NS ns3.whoami6.domain.example.
+ @ NS ns4.whoami6.domain.example.
+
+ ns1 AAAA 2001:db8:100::53
+ ns2 AAAA 2001:db8:200::53
+ ns3 AAAA 2001:db8:300::53
+ ns4 AAAA 2001:db8:400::53
+
+The parent domain would then delegate whoami.domain.example to
+ns[1-4].whoami.domain.example and whoami6.domain.example to
+ns[1-4].whoami6.domain.example, and include the corresponding A-only or
+AAAA-only glue records.
+
+.. NOTE::
+ This module is not configurable.