diff options
Diffstat (limited to 'src/knot/modules')
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, ¤t_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, ¶ms); + 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, ¶ms) != 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, ¶ms) != 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(©->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. |