diff options
Diffstat (limited to '')
243 files changed, 67614 insertions, 0 deletions
diff --git a/src/knot/Makefile.inc b/src/knot/Makefile.inc new file mode 100644 index 0000000..c28e6a8 --- /dev/null +++ b/src/knot/Makefile.inc @@ -0,0 +1,240 @@ +libknotd_la_CPPFLAGS = $(AM_CPPFLAGS) $(CFLAG_VISIBILITY) $(libkqueue_CFLAGS) \ + $(liburcu_CFLAGS) $(lmdb_CFLAGS) $(systemd_CFLAGS) \ + $(gnutls_CFLAGS) $(libngtcp2_CFLAGS) -DKNOTD_MOD_STATIC +libknotd_la_LDFLAGS = $(AM_LDFLAGS) -export-symbols-regex '^knotd_' +libknotd_la_LIBADD = $(dlopen_LIBS) $(libkqueue_LIBS) $(pthread_LIBS) \ + $(libngtcp2_LIBS) +libknotd_LIBS = libknotd.la libknot.la libdnssec.la libzscanner.la \ + $(libcontrib_LIBS) $(liburcu_LIBS) $(lmdb_LIBS) \ + $(systemd_LIBS) $(gnutls_LIBS) + +if EMBEDDED_LIBNGTCP2 +libknotd_la_LIBADD += $(libembngtcp2_LIBS) +endif EMBEDDED_LIBNGTCP2 + +include_libknotddir = $(includedir)/knot +include_libknotd_HEADERS = \ + knot/include/module.h + +libknotd_la_SOURCES = \ + knot/catalog/catalog_db.c \ + knot/catalog/catalog_db.h \ + knot/catalog/catalog_update.c \ + knot/catalog/catalog_update.h \ + knot/catalog/generate.c \ + knot/catalog/generate.h \ + knot/catalog/interpret.c \ + knot/catalog/interpret.h \ + knot/conf/base.c \ + knot/conf/base.h \ + knot/conf/conf.c \ + knot/conf/conf.h \ + knot/conf/confdb.c \ + knot/conf/confdb.h \ + knot/conf/confio.c \ + knot/conf/confio.h \ + knot/conf/migration.c \ + knot/conf/migration.h \ + knot/conf/module.h \ + knot/conf/module.c \ + knot/conf/schema.c \ + knot/conf/schema.h \ + knot/conf/tools.c \ + knot/conf/tools.h \ + knot/ctl/commands.c \ + knot/ctl/commands.h \ + knot/ctl/process.c \ + knot/ctl/process.h \ + knot/dnssec/context.c \ + knot/dnssec/context.h \ + knot/dnssec/ds_query.c \ + knot/dnssec/ds_query.h \ + knot/dnssec/kasp/kasp_db.c \ + knot/dnssec/kasp/kasp_db.h \ + knot/dnssec/kasp/kasp_zone.c \ + knot/dnssec/kasp/kasp_zone.h \ + knot/dnssec/kasp/keystate.c \ + knot/dnssec/kasp/keystate.h \ + knot/dnssec/kasp/keystore.c \ + knot/dnssec/kasp/keystore.h \ + knot/dnssec/kasp/policy.h \ + knot/dnssec/key-events.c \ + knot/dnssec/key-events.h \ + knot/dnssec/key_records.c \ + knot/dnssec/key_records.h \ + knot/dnssec/nsec-chain.c \ + knot/dnssec/nsec-chain.h \ + knot/dnssec/nsec3-chain.c \ + knot/dnssec/nsec3-chain.h \ + knot/dnssec/policy.c \ + knot/dnssec/policy.h \ + knot/dnssec/rrset-sign.c \ + knot/dnssec/rrset-sign.h \ + knot/dnssec/zone-events.c \ + knot/dnssec/zone-events.h \ + knot/dnssec/zone-keys.c \ + knot/dnssec/zone-keys.h \ + knot/dnssec/zone-nsec.c \ + knot/dnssec/zone-nsec.h \ + knot/dnssec/zone-sign.c \ + knot/dnssec/zone-sign.h \ + knot/events/events.c \ + knot/events/events.h \ + knot/events/handlers.h \ + knot/events/handlers/backup.c \ + knot/events/handlers/dnssec.c \ + knot/events/handlers/ds_check.c \ + knot/events/handlers/ds_push.c \ + knot/events/handlers/expire.c \ + knot/events/handlers/flush.c \ + knot/events/handlers/freeze_thaw.c \ + knot/events/handlers/load.c \ + knot/events/handlers/notify.c \ + knot/events/handlers/refresh.c \ + knot/events/handlers/update.c \ + knot/events/replan.c \ + knot/events/replan.h \ + knot/nameserver/axfr.c \ + knot/nameserver/axfr.h \ + knot/nameserver/chaos.c \ + knot/nameserver/chaos.h \ + knot/nameserver/internet.c \ + knot/nameserver/internet.h \ + knot/nameserver/ixfr.c \ + knot/nameserver/ixfr.h \ + knot/nameserver/log.h \ + knot/nameserver/notify.c \ + knot/nameserver/notify.h \ + knot/nameserver/nsec_proofs.c \ + knot/nameserver/nsec_proofs.h \ + knot/nameserver/process_query.c \ + knot/nameserver/process_query.h \ + knot/nameserver/query_module.c \ + knot/nameserver/query_module.h \ + knot/nameserver/tsig_ctx.c \ + knot/nameserver/tsig_ctx.h \ + knot/nameserver/update.c \ + knot/nameserver/update.h \ + knot/nameserver/xfr.c \ + knot/nameserver/xfr.h \ + knot/query/capture.c \ + knot/query/capture.h \ + knot/query/layer.h \ + knot/query/query.c \ + knot/query/query.h \ + knot/query/requestor.c \ + knot/query/requestor.h \ + knot/common/evsched.c \ + knot/common/evsched.h \ + knot/common/fdset.c \ + knot/common/fdset.h \ + knot/common/log.c \ + knot/common/log.h \ + knot/common/process.c \ + knot/common/process.h \ + knot/common/stats.c \ + knot/common/stats.h \ + knot/common/systemd.c \ + knot/common/systemd.h \ + knot/common/unreachable.c \ + knot/common/unreachable.h \ + knot/journal/journal_basic.c \ + knot/journal/journal_basic.h \ + knot/journal/journal_metadata.c \ + knot/journal/journal_metadata.h \ + knot/journal/journal_read.c \ + knot/journal/journal_read.h \ + knot/journal/journal_write.c \ + knot/journal/journal_write.h \ + knot/journal/knot_lmdb.c \ + knot/journal/knot_lmdb.h \ + knot/journal/serialization.c \ + knot/journal/serialization.h \ + knot/server/dthreads.c \ + knot/server/dthreads.h \ + knot/server/proxyv2.c \ + knot/server/proxyv2.h \ + knot/server/server.c \ + knot/server/server.h \ + knot/server/tcp-handler.c \ + knot/server/tcp-handler.h \ + knot/server/udp-handler.c \ + knot/server/udp-handler.h \ + knot/server/xdp-handler.c \ + knot/server/xdp-handler.h \ + knot/updates/acl.c \ + knot/updates/acl.h \ + knot/updates/apply.c \ + knot/updates/apply.h \ + knot/updates/changesets.c \ + knot/updates/changesets.h \ + knot/updates/ddns.c \ + knot/updates/ddns.h \ + knot/updates/zone-update.c \ + knot/updates/zone-update.h \ + knot/worker/pool.c \ + knot/worker/pool.h \ + knot/worker/queue.c \ + knot/worker/queue.h \ + knot/zone/adds_tree.c \ + knot/zone/adds_tree.h \ + knot/zone/adjust.c \ + knot/zone/adjust.h \ + knot/zone/backup.c \ + knot/zone/backup.h \ + knot/zone/backup_dir.c \ + knot/zone/backup_dir.h \ + knot/zone/contents.c \ + knot/zone/contents.h \ + knot/zone/digest.c \ + knot/zone/digest.h \ + knot/zone/measure.h \ + knot/zone/measure.c \ + knot/zone/node.c \ + knot/zone/node.h \ + knot/zone/semantic-check.c \ + knot/zone/semantic-check.h \ + knot/zone/serial.c \ + knot/zone/serial.h \ + knot/zone/timers.c \ + knot/zone/timers.h \ + knot/zone/zone-diff.c \ + knot/zone/zone-diff.h \ + knot/zone/zone-dump.c \ + knot/zone/zone-dump.h \ + knot/zone/zone-load.c \ + knot/zone/zone-load.h \ + knot/zone/zone-tree.c \ + knot/zone/zone-tree.h \ + knot/zone/zone.c \ + knot/zone/zone.h \ + knot/zone/zonedb-load.c \ + knot/zone/zonedb-load.h \ + knot/zone/zonedb.c \ + knot/zone/zonedb.h \ + knot/zone/zonefile.c \ + knot/zone/zonefile.h + +if HAVE_DAEMON +noinst_LTLIBRARIES += libknotd.la +pkgconfig_DATA += knotd.pc +endif HAVE_DAEMON + +KNOTD_MOD_CPPFLAGS = $(AM_CPPFLAGS) $(CFLAG_VISIBILITY) +KNOTD_MOD_LDFLAGS = $(AM_LDFLAGS) -module -shared -avoid-version + +pkglibdir = $(module_instdir) +pkglib_LTLIBRARIES = + +include $(srcdir)/knot/modules/cookies/Makefile.inc +include $(srcdir)/knot/modules/dnsproxy/Makefile.inc +include $(srcdir)/knot/modules/dnstap/Makefile.inc +include $(srcdir)/knot/modules/geoip/Makefile.inc +include $(srcdir)/knot/modules/noudp/Makefile.inc +include $(srcdir)/knot/modules/onlinesign/Makefile.inc +include $(srcdir)/knot/modules/probe/Makefile.inc +include $(srcdir)/knot/modules/queryacl/Makefile.inc +include $(srcdir)/knot/modules/rrl/Makefile.inc +include $(srcdir)/knot/modules/stats/Makefile.inc +include $(srcdir)/knot/modules/synthrecord/Makefile.inc +include $(srcdir)/knot/modules/whoami/Makefile.inc diff --git a/src/knot/catalog/catalog_db.c b/src/knot/catalog/catalog_db.c new file mode 100644 index 0000000..b483f4d --- /dev/null +++ b/src/knot/catalog/catalog_db.c @@ -0,0 +1,347 @@ +/* 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 <stdio.h> +#include <string.h> +#include <sys/stat.h> +#include <urcu.h> + +#include "contrib/files.h" +#include "knot/catalog/catalog_db.h" +#include "knot/common/log.h" + +static const MDB_val catalog_iter_prefix = { 1, "" }; + +size_t catalog_dname_append(knot_dname_storage_t storage, const knot_dname_t *name) +{ + size_t old_len = knot_dname_size(storage); + size_t name_len = knot_dname_size(name); + size_t new_len = old_len - 1 + name_len; + if (old_len == 0 || name_len == 0 || new_len > KNOT_DNAME_MAXLEN) { + return 0; + } + memcpy(storage + old_len - 1, name, name_len); + return new_len; +} + +int catalog_bailiwick_shift(const knot_dname_t *subname, const knot_dname_t *name) +{ + const knot_dname_t *res = subname; + while (!knot_dname_is_equal(res, name)) { + if (*res == '\0') { + return -1; + } + res = knot_wire_next_label(res, NULL); + } + return res - subname; +} + +void catalog_init(catalog_t *cat, const char *path, size_t mapsize) +{ + knot_lmdb_init(&cat->db, path, mapsize, MDB_NOTLS, NULL); +} + +static void ensure_cat_version(knot_lmdb_txn_t *ro_txn, knot_lmdb_txn_t *rw_txn) +{ + MDB_val key = { 8, "\x01version" }; + if (knot_lmdb_find(ro_txn, &key, KNOT_LMDB_EXACT)) { + if (strncmp(CATALOG_VERSION, ro_txn->cur_val.mv_data, + ro_txn->cur_val.mv_size) != 0) { + log_warning("catalog version mismatch"); + } + } else if (rw_txn != NULL) { + MDB_val val = { strlen(CATALOG_VERSION), CATALOG_VERSION }; + knot_lmdb_insert(rw_txn, &key, &val); + } +} + +// does NOT check for catalog zone version by RFC, this is Knot-specific in the cat LMDB ! +static void check_cat_version(catalog_t *cat) +{ + if (cat->ro_txn->ret == KNOT_EOK) { + ensure_cat_version(cat->ro_txn, cat->rw_txn); + } +} + +int catalog_open(catalog_t *cat) +{ + if (!knot_lmdb_is_open(&cat->db)) { + int ret = knot_lmdb_open(&cat->db); + if (ret != KNOT_EOK) { + return ret; + } + } + if (cat->ro_txn == NULL) { + knot_lmdb_txn_t *ro_txn = calloc(1, sizeof(*ro_txn)); + if (ro_txn == NULL) { + return KNOT_ENOMEM; + } + knot_lmdb_begin(&cat->db, ro_txn, false); + cat->ro_txn = ro_txn; + } + check_cat_version(cat); + return cat->ro_txn->ret; +} + +int catalog_begin(catalog_t *cat) +{ + int ret = catalog_open(cat); + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t *rw_txn = calloc(1, sizeof(*rw_txn)); + if (rw_txn == NULL) { + return KNOT_ENOMEM; + } + knot_lmdb_begin(&cat->db, rw_txn, true); + if (rw_txn->ret != KNOT_EOK) { + ret = rw_txn->ret; + free(rw_txn); + return ret; + } + assert(cat->rw_txn == NULL); // LMDB prevents two existing RW txns at a time + cat->rw_txn = rw_txn; + check_cat_version(cat); + return cat->rw_txn->ret; +} + +int catalog_commit(catalog_t *cat) +{ + knot_lmdb_txn_t *rw_txn = rcu_xchg_pointer(&cat->rw_txn, NULL); + knot_lmdb_commit(rw_txn); + int ret = rw_txn->ret; + free(rw_txn); + if (ret != KNOT_EOK) { + return ret; + } + + // now refresh RO txn + knot_lmdb_txn_t *ro_txn = calloc(1, sizeof(*ro_txn)); + if (ro_txn == NULL) { + return KNOT_ENOMEM; + } + knot_lmdb_begin(&cat->db, ro_txn, false); + cat->old_ro_txn = rcu_xchg_pointer(&cat->ro_txn, ro_txn); + + return KNOT_EOK; +} + +void catalog_abort(catalog_t *cat) +{ + knot_lmdb_txn_t *rw_txn = rcu_xchg_pointer(&cat->rw_txn, NULL); + if (rw_txn != NULL) { + knot_lmdb_abort(rw_txn); + free(rw_txn); + } +} + +void catalog_commit_cleanup(catalog_t *cat) +{ + knot_lmdb_txn_t *old_ro_txn = rcu_xchg_pointer(&cat->old_ro_txn, NULL); + if (old_ro_txn != NULL) { + knot_lmdb_abort(old_ro_txn); + free(old_ro_txn); + } +} + +void catalog_deinit(catalog_t *cat) +{ + assert(cat->rw_txn == NULL); + if (cat->ro_txn != NULL) { + knot_lmdb_abort(cat->ro_txn); + free(cat->ro_txn); + } + if (cat->old_ro_txn != NULL) { + knot_lmdb_abort(cat->old_ro_txn); + free(cat->old_ro_txn); + } + knot_lmdb_deinit(&cat->db); +} + +int catalog_add(catalog_t *cat, const knot_dname_t *member, + const knot_dname_t *owner, const knot_dname_t *catzone, + const char *group) +{ + if (cat->rw_txn == NULL) { + return KNOT_EINVAL; + } + int bail = catalog_bailiwick_shift(owner, catzone); + if (bail < 0) { + return KNOT_EOUTOFZONE; + } + assert(bail >= 0 && bail < 256); + MDB_val key = knot_lmdb_make_key("BN", 0, member); // 0 for future purposes + MDB_val val = knot_lmdb_make_key("BBNS", 0, bail, owner, group); + + knot_lmdb_insert(cat->rw_txn, &key, &val); + free(key.mv_data); + free(val.mv_data); + return cat->rw_txn->ret; +} + +int catalog_del(catalog_t *cat, const knot_dname_t *member) +{ + if (cat->rw_txn == NULL) { + return KNOT_EINVAL; + } + MDB_val key = knot_lmdb_make_key("BN", 0, member); + knot_lmdb_del_prefix(cat->rw_txn, &key); // deletes one record + free(key.mv_data); + return cat->rw_txn->ret; +} + +static void unmake_val(MDB_val *val, const knot_dname_t **owner, + const knot_dname_t **catz, const char **group) +{ + uint8_t zero, shift; + *group = ""; // backward compatibility with Knot 3.0 + knot_lmdb_unmake_key(val->mv_data, val->mv_size, "BBNS", &zero, &shift, + owner, group); + *catz = *owner + shift; +} + +static int find_threadsafe(catalog_t *cat, const knot_dname_t *member, + const knot_dname_t **owner, const knot_dname_t **catz, + const char **group, void **tofree) +{ + *tofree = NULL; + if (cat->ro_txn == NULL) { + return KNOT_ENOENT; + } + + MDB_val key = knot_lmdb_make_key("BN", 0, member), val = { 0 }; + + int ret = knot_lmdb_find_threadsafe(cat->ro_txn, &key, &val, KNOT_LMDB_EXACT); + if (ret == KNOT_EOK) { + unmake_val(&val, owner, catz, group); + *tofree = val.mv_data; + } + free(key.mv_data); + return ret; +} + +int catalog_get_catz(catalog_t *cat, const knot_dname_t *member, + const knot_dname_t **catz, const char **group, void **tofree) +{ + const knot_dname_t *unused; + return find_threadsafe(cat, member, &unused, catz, group, tofree); +} + +bool catalog_has_member(catalog_t *cat, const knot_dname_t *member) +{ + const knot_dname_t *catz; + const char *group; + void *tofree = NULL; + int ret = catalog_get_catz(cat, member, &catz, &group, &tofree); + free(tofree); + return (ret == KNOT_EOK); +} + +bool catalog_contains_exact(catalog_t *cat, const knot_dname_t *member, + const knot_dname_t *owner, const knot_dname_t *catz) +{ + const knot_dname_t *found_owner, *found_catz; + const char *found_group; + void *tofree = NULL; + int ret = find_threadsafe(cat, member, &found_owner, &found_catz, &found_group, &tofree); + if (ret == KNOT_EOK && (!knot_dname_is_equal(owner, found_owner) || + !knot_dname_is_equal(catz, found_catz))) { + ret = KNOT_ENOENT; + } + free(tofree); + return (ret == KNOT_EOK); +} + +typedef struct { + catalog_apply_cb_t cb; + void *ctx; +} catalog_apply_ctx_t; + +static int catalog_apply_cb(MDB_val *key, MDB_val *val, void *ctx) +{ + catalog_apply_ctx_t *iter_ctx = ctx; + uint8_t zero; + const knot_dname_t *mem = NULL, *ow = NULL, *cz = NULL; + const char *gr = NULL; + knot_lmdb_unmake_key(key->mv_data, key->mv_size, "BN", &zero, &mem); + unmake_val(val, &ow, &cz, &gr); + if (mem == NULL || ow == NULL || cz == NULL) { + return KNOT_EMALF; + } + return iter_ctx->cb(mem, ow, cz, gr, iter_ctx->ctx); +} + +int catalog_apply(catalog_t *cat, const knot_dname_t *for_member, + catalog_apply_cb_t cb, void *ctx, bool rw) +{ + MDB_val prefix = knot_lmdb_make_key(for_member == NULL ? "B" : "BN", 0, for_member); + catalog_apply_ctx_t iter_ctx = { cb, ctx }; + knot_lmdb_txn_t *use_txn = rw ? cat->rw_txn : cat->ro_txn; + int ret = knot_lmdb_apply_threadsafe(use_txn, &prefix, true, catalog_apply_cb, &iter_ctx); + free(prefix.mv_data); + return ret; +} + +static bool same_catalog(knot_lmdb_txn_t *txn, const knot_dname_t *catalog) +{ + if (catalog == NULL) { + return true; + } + const knot_dname_t *txn_cat = NULL, *unused; + const char *grunused; + unmake_val(&txn->cur_val, &unused, &txn_cat, &grunused); + return knot_dname_is_equal(txn_cat, catalog); +} + +int catalog_copy(knot_lmdb_db_t *from, knot_lmdb_db_t *to, + const knot_dname_t *cat_only, bool read_rw_txn) +{ + if (knot_lmdb_exists(from) == KNOT_ENODB) { + return KNOT_EOK; + } + int ret = knot_lmdb_open(from); + if (ret == KNOT_EOK) { + ret = make_path(to->path, S_IRWXU | S_IRWXG); + if (ret == KNOT_EOK) { + ret = knot_lmdb_open(to); + } + } + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t txn_r = { 0 }, txn_w = { 0 }; + knot_lmdb_begin(from, &txn_r, read_rw_txn); // using RW txn not to conflict with still-open RO txn + knot_lmdb_begin(to, &txn_w, true); + knot_lmdb_foreach(&txn_w, (MDB_val *)&catalog_iter_prefix) { + if (same_catalog(&txn_w, cat_only)) { + knot_lmdb_del_cur(&txn_w); + } + } + knot_lmdb_foreach(&txn_r, (MDB_val *)&catalog_iter_prefix) { + if (same_catalog(&txn_r, cat_only)) { + knot_lmdb_insert(&txn_w, &txn_r.cur_key, &txn_r.cur_val); + } + } + ensure_cat_version(&txn_w, &txn_w); + if (txn_r.ret != KNOT_EOK) { + knot_lmdb_abort(&txn_r); + knot_lmdb_abort(&txn_w); + return txn_r.ret; + } + knot_lmdb_commit(&txn_r); + knot_lmdb_commit(&txn_w); + return txn_w.ret; +} diff --git a/src/knot/catalog/catalog_db.h b/src/knot/catalog/catalog_db.h new file mode 100644 index 0000000..d0abd3b --- /dev/null +++ b/src/knot/catalog/catalog_db.h @@ -0,0 +1,187 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/journal/knot_lmdb.h" +#include "libknot/libknot.h" + +#define CATALOG_VERSION "1.0" +#define CATALOG_ZONE_VERSION "2" // must be just one char long +#define CATALOG_ZONES_LABEL "\x05""zones" +#define CATALOG_GROUP_LABEL "\x05""group" +#define CATALOG_GROUP_MAXLEN 255 + +typedef struct catalog { + knot_lmdb_db_t db; + knot_lmdb_txn_t *ro_txn; // persistent RO transaction + knot_lmdb_txn_t *rw_txn; // temporary RW transaction + + // private + knot_lmdb_txn_t *old_ro_txn; +} catalog_t; + +/*! + * \brief Append a prefix dname to a dname in a storage. + * + * \return New dname length. + */ +size_t catalog_dname_append(knot_dname_storage_t storage, const knot_dname_t *name); + +/*! + * \brief Return the number of bytes that subname has more than name. + * + * \return -1 if subname is not subname of name + */ +int catalog_bailiwick_shift(const knot_dname_t *subname, const knot_dname_t *name); + +/*! + * \brief Initialize catalog structure. + * + * \param cat Catalog structure. + * \param path Path to LMDB for catalog. + * \param mapsize Mapsize of the LMDB. + */ +void catalog_init(catalog_t *cat, const char *path, size_t mapsize); + +/*! + * \brief Open the catalog LMDB, create it if not exists. + * + * \param cat Catlog to be opened. + * + * \return KNOT_E* + */ +int catalog_open(catalog_t *cat); + +/*! + * \brief Start a temporary RW transaction in the catalog. + * + * \param cat Catalog in question. + * + * \return KNOT_E* + */ +int catalog_begin(catalog_t *cat); + +/*! + * \brief End using the temporary RW txn, refresh the persistent RO txn. + * + * \param cat Catalog in question. + * + * \return KNOT_E* + */ +int catalog_commit(catalog_t *cat); + +/*! + * \brief Abort temporary RW txn. + */ +void catalog_abort(catalog_t *cat); + +/*! + * \brief Free up old txns. + * + * \note This must be called after catalog_commit() with a delay of synchronize_rcu(). + * + * \param cat Catalog. + */ +void catalog_commit_cleanup(catalog_t *cat); + +/*! + * \brief Close the catalog and de-init the structure. + * + * \param cat Catalog to be closed. + */ +void catalog_deinit(catalog_t *cat); + +/*! + * \brief Add a member zone to the catalog database. + * + * \param cat Catalog to be augmented. + * \param member Member zone name. + * \param owner Owner of the PTR record in catalog zone, respective to the member zone. + * \param catzone Name of the catalog zone whose it's the member. + * \param group Configuration group of the member. + * + * \return KNOT_E* + */ +int catalog_add(catalog_t *cat, const knot_dname_t *member, + const knot_dname_t *owner, const knot_dname_t *catzone, + const char *group); + +/*! + * \brief Delete a member zone from the catalog database. + * + * \param cat Catalog to be removed from. + * \param member Member zone to be removed. + * + * \return KNOT_E* + */ +int catalog_del(catalog_t *cat, const knot_dname_t *member); + +/*! + * \brief Find catz name of the catalog owning this member. + * + * \note This function may be called in multithreaded operation. + * + * \param cat Catalog database. + * \param member Member to search for. + * \param catz Out: name of catalog zone it resides in. + * \param group Out: configuration group the member resides in. + * \param tofree Out: a pointer that has to be freed later. + * + * \return KNOT_E* + */ +int catalog_get_catz(catalog_t *cat, const knot_dname_t *member, + const knot_dname_t **catz, const char **group, void **tofree); + +/*! + * \brief Check if this member exists in any catalog zone. + */ +bool catalog_has_member(catalog_t *cat, const knot_dname_t *member); + +/*! + * \brief Check if exactly this record (member, owner, catz) is in catalog DB. + */ +bool catalog_contains_exact(catalog_t *cat, const knot_dname_t *member, + const knot_dname_t *owner, const knot_dname_t *catz); + +typedef int (*catalog_apply_cb_t)(const knot_dname_t *member, const knot_dname_t *owner, + const knot_dname_t *catz, const char *group, void *ctx); +/*! + * \brief Iterate through catalog database, applying callback. + * + * \param cat Catalog to be iterated. + * \param for_member (Optional) Iterate only on records for this member name. + * \param cb Callback to be called. + * \param ctx Context for this callback. + * \param rw Use read-write transaction. + * + * \return KNOT_E* + */ +int catalog_apply(catalog_t *cat, const knot_dname_t *for_member, + catalog_apply_cb_t cb, void *ctx, bool rw); + +/*! + * \brief Copy records from one catalog database to other. + * + * \param from Catalog DB to copy from. + * \param to Catalog DB to copy to. + * \param cat_only Optional: copy only records for this catalog zone. + * \param read_rw_txn Use RW txn for read operations. + * + * \return KNOT_E* + */ +int catalog_copy(knot_lmdb_db_t *from, knot_lmdb_db_t *to, + const knot_dname_t *cat_only, bool read_rw_txn); diff --git a/src/knot/catalog/catalog_update.c b/src/knot/catalog/catalog_update.c new file mode 100644 index 0000000..50f38cb --- /dev/null +++ b/src/knot/catalog/catalog_update.c @@ -0,0 +1,407 @@ +/* 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 <signal.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> + +#include "knot/catalog/catalog_update.h" +#include "knot/common/log.h" +#include "knot/conf/base.h" +#include "knot/server/server.h" + +int catalog_update_init(catalog_update_t *u) +{ + u->upd = trie_create(NULL); + if (u->upd == NULL) { + return KNOT_ENOMEM; + } + pthread_mutex_init(&u->mutex, 0); + u->error = KNOT_EOK; + return KNOT_EOK; +} + +catalog_update_t *catalog_update_new(void) +{ + catalog_update_t *u = calloc(1, sizeof(*u)); + if (u != NULL) { + int ret = catalog_update_init(u); + if (ret != KNOT_EOK) { + free(u); + u = NULL; + } + } + return u; +} + +static void catalog_upd_val_free(catalog_upd_val_t *val) +{ + free(val->add_owner); + free(val->rem_owner); + free(val->new_group); + free(val); +} + +static int freecb(trie_val_t *tval, _unused_ void *unused) +{ + catalog_upd_val_t *val = *tval; + if (val != NULL) { + catalog_upd_val_free(val); + } + return 0; +} + +void catalog_update_clear(catalog_update_t *u) +{ + trie_apply(u->upd, freecb, NULL); + trie_clear(u->upd); + u->error = KNOT_EOK; +} + +void catalog_update_deinit(catalog_update_t *u) +{ + pthread_mutex_destroy(&u->mutex); + trie_free(u->upd); +} + +void catalog_update_free(catalog_update_t *u) +{ + if (u != NULL) { + catalog_update_deinit(u); + free(u); + } +} + +static catalog_upd_val_t *upd_val_new(const knot_dname_t *member, int bail, + const knot_dname_t *owner, catalog_upd_type_t type) +{ + assert(bail <= (int)knot_dname_size(owner)); + size_t member_size = knot_dname_size(member); + + catalog_upd_val_t *val = malloc(sizeof(*val) + member_size); + if (val == NULL) { + return NULL; + } + val->member = (knot_dname_t *)(val + 1); + memcpy(val->member, member, member_size); + knot_dname_t *owner_cpy = knot_dname_copy(owner, NULL); + if (owner_cpy == NULL) { + free(val); + return NULL; + } + val->type = type; + val->new_group = NULL; + if (type == CAT_UPD_REM) { + val->add_owner = NULL; + val->add_catz = NULL; + val->rem_owner = owner_cpy; + val->rem_catz = owner_cpy + bail; + } else { + val->add_owner = owner_cpy; + val->add_catz = owner_cpy + bail; + val->rem_owner = NULL; + val->rem_catz = NULL; + } + return val; +} + +static const knot_dname_t *get_uniq(const knot_dname_t *ptr_owner, + const knot_dname_t *catz) +{ + int labels = knot_dname_labels(ptr_owner, NULL); + labels -= knot_dname_labels(catz, NULL); + assert(labels >= 2); + return ptr_owner + knot_dname_prefixlen(ptr_owner, labels - 2, NULL); +} + +static bool same_uniq(const knot_dname_t *owner1, const knot_dname_t *catz1, + const knot_dname_t *owner2, const knot_dname_t *catz2) +{ + const knot_dname_t *uniq1 = get_uniq(owner1, catz1), *uniq2 = get_uniq(owner2, catz2); + if (*uniq1 != *uniq2) { + return false; + } + return memcmp(uniq1 + 1, uniq2 + 1, *uniq1) == 0; +} + +static int upd_val_update(catalog_upd_val_t *val, int bail, + const knot_dname_t *owner, bool rem) +{ + if ((rem && val->type != CAT_UPD_ADD) || + (!rem && val->type != CAT_UPD_REM)) { + log_zone_error(val->member, "duplicate addition/removal of the member node, ignoring"); + return KNOT_EOK; + } + knot_dname_t *owner_cpy = knot_dname_copy(owner, NULL); + if (owner_cpy == NULL) { + return KNOT_ENOMEM; + } + if (rem) { + val->rem_owner = owner_cpy; + val->rem_catz = owner_cpy + bail; + } else { + val->add_owner = owner_cpy; + val->add_catz = owner_cpy + bail; + } + if (same_uniq(val->rem_owner, val->rem_catz, val->add_owner, val->add_catz)) { + val->type = CAT_UPD_MINOR; + } else { + val->type = CAT_UPD_UNIQ; + } + return KNOT_EOK; +} + +static int upd_val_set_prop(catalog_upd_val_t *val, const knot_dname_t *check_ow, + const knot_dname_t *check_catz, const char *group, + size_t group_len) +{ + if (check_catz != NULL) { + if (val->type == CAT_UPD_REM || + !knot_dname_is_equal(check_ow, val->add_owner) || // TODO consider removing those checks. Are they worth the performance? + !knot_dname_is_equal(check_catz, val->add_catz)) { + return KNOT_EOK; // ignore invalid property set + } + } + if (val->new_group != NULL) { + free(val->new_group); + } + val->new_group = strndup(group, group_len); + return val->new_group == NULL ? KNOT_ENOMEM : KNOT_EOK; +} + +int catalog_update_add(catalog_update_t *u, const knot_dname_t *member, + const knot_dname_t *owner, const knot_dname_t *catzone, + catalog_upd_type_t type, const char *group, + size_t group_len, catalog_t *check_rem) +{ + int bail = catalog_bailiwick_shift(owner, catzone); + if (bail < 0) { + return KNOT_EOUTOFZONE; + } + assert(bail >= 0 && bail <= KNOT_DNAME_MAXLEN); + + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(member, lf_storage); + + trie_val_t *found = trie_get_try(u->upd, lf + 1, lf[0]); + + if ((type == CAT_UPD_REM || type == CAT_UPD_PROP) && check_rem != NULL && + !catalog_contains_exact(check_rem, member, owner, catzone)) { + if (found == NULL) { + // we need to perform this check immediately because + // garbage removal would block legitimate removal + return KNOT_EOK; + } + if (type == CAT_UPD_REM) { + catalog_upd_val_t *val = *found; + catalog_upd_val_free(val); + trie_del(u->upd, lf + 1, lf[0], NULL); + return KNOT_EOK; + } + } + + if (found != NULL) { + catalog_upd_val_t *val = *found; + assert(knot_dname_is_equal(val->member, member)); + if (type == CAT_UPD_PROP) { + return upd_val_set_prop(val, owner, catzone, group, group_len); + } else { + return upd_val_update(val, bail, owner, type == CAT_UPD_REM); + } + } + + catalog_upd_val_t *val = upd_val_new(member, bail, owner, type); + if (val == NULL) { + return KNOT_ENOMEM; + } + if (group_len > 0) { + int ret = upd_val_set_prop(val, NULL, NULL, group, group_len); + if (ret != KNOT_EOK) { + catalog_upd_val_free(val); + return ret; + } + } + trie_val_t *added = trie_get_ins(u->upd, lf + 1, lf[0]); + if (added == NULL) { + catalog_upd_val_free(val); + return KNOT_ENOMEM; + } + assert(*added == NULL); + *added = val; + return KNOT_EOK; +} + +catalog_upd_val_t *catalog_update_get(catalog_update_t *u, const knot_dname_t *member) +{ + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(member, lf_storage); + + trie_val_t *found = trie_get_try(u->upd, lf + 1, lf[0]); + return found == NULL ? NULL : *(catalog_upd_val_t **)found; +} + +static bool check_member(catalog_upd_val_t *val, conf_t *conf, catalog_t *cat) +{ + if (val->type == CAT_UPD_REM || val->type == CAT_UPD_INVALID || val->type == CAT_UPD_PROP) { + return true; + } + if (!conf_rawid_exists(conf, C_ZONE, val->add_catz, knot_dname_size(val->add_catz))) { + knot_dname_txt_storage_t cat_str; + (void)knot_dname_to_str(cat_str, val->add_catz, sizeof(cat_str)); + log_zone_error(val->member, "catalog template zone '%s' not configured, ignoring", cat_str); + return false; + } + if (conf_rawid_exists(conf, C_ZONE, val->member, knot_dname_size(val->member))) { + log_zone_error(val->member, "member zone already configured, ignoring"); + return false; + } + if (val->type == CAT_UPD_ADD && catalog_has_member(cat, val->member)) { + log_zone_error(val->member, "member zone already configured by catalog, ignoring"); + return false; + } + return true; +} + +typedef struct { + conf_t *conf; + catalog_update_t *cup; +} rem_conflict_ctx_t; + +static int rem_conf_conflict(const knot_dname_t *mem, const knot_dname_t *ow, + const knot_dname_t *cz, _unused_ const char *gr, void *ctx) +{ + rem_conflict_ctx_t *rcctx = ctx; + + if (conf_rawid_exists(rcctx->conf, C_ZONE, mem, knot_dname_size(mem))) { + return catalog_update_add(rcctx->cup, mem, ow, cz, CAT_UPD_REM, NULL, 0, NULL); + } + return KNOT_EOK; +} + +void catalog_update_finalize(catalog_update_t *u, catalog_t *cat, conf_t *conf) +{ + catalog_it_t *it = catalog_it_begin(u); + while (!catalog_it_finished(it)) { + catalog_upd_val_t *val = catalog_it_val(it); + if (!check_member(val, conf, cat)) { + val->type = (val->type == CAT_UPD_ADD ? CAT_UPD_INVALID : CAT_UPD_REM); + } + catalog_it_next(it); + } + catalog_it_free(it); + + // This checks if the configuration file has not changed in the way + // it conflicts with existing member zone and let config take precedence. + if (cat->ro_txn != NULL) { + rem_conflict_ctx_t rcctx = { conf, u }; + (void)catalog_apply(cat, NULL, rem_conf_conflict, &rcctx, false); + } +} + +int catalog_update_commit(catalog_update_t *u, catalog_t *cat) +{ + catalog_it_t *it = catalog_it_begin(u); + if (catalog_it_finished(it)) { + catalog_it_free(it); + return KNOT_EOK; + } + int ret = catalog_begin(cat); + while (!catalog_it_finished(it) && ret == KNOT_EOK) { + catalog_upd_val_t *val = catalog_it_val(it); + switch (val->type) { + case CAT_UPD_ADD: + case CAT_UPD_MINOR: // catalog_add will simply update/overwrite existing data + case CAT_UPD_UNIQ: + case CAT_UPD_PROP: + ret = catalog_add(cat, val->member, val->add_owner, val->add_catz, + val->new_group == NULL ? "" : val->new_group); + break; + case CAT_UPD_REM: + ret = catalog_del(cat, val->member); + break; + case CAT_UPD_INVALID: + break; // no action + default: + assert(0); + ret = KNOT_ERROR; + } + catalog_it_next(it); + } + catalog_it_free(it); + if (ret == KNOT_EOK) { + ret = catalog_commit(cat); + } else { + catalog_abort(cat); + } + return ret; +} + +typedef struct { + const knot_dname_t *zone; + catalog_update_t *u; +} del_all_ctx_t; + +static int del_all_cb(const knot_dname_t *member, const knot_dname_t *owner, + const knot_dname_t *catz, _unused_ const char *group, void *dactx) +{ + del_all_ctx_t *ctx = dactx; + if (knot_dname_is_equal(catz, ctx->zone)) { + // TODO possible speedup by indexing which member zones belong to a catalog zone + return catalog_update_add(ctx->u, member, owner, catz, CAT_UPD_REM, NULL, 0, NULL); + } else { + return KNOT_EOK; + } +} + +int catalog_update_del_all(catalog_update_t *u, catalog_t *cat, const knot_dname_t *zone, ssize_t *upd_count) +{ + pthread_mutex_lock(&u->mutex); + del_all_ctx_t ctx = { zone, u }; + *upd_count -= trie_weight(u->upd); + int ret = catalog_apply(cat, NULL, del_all_cb, &ctx, false); + *upd_count += trie_weight(u->upd); + pthread_mutex_unlock(&u->mutex); + return ret; +} + +int catalog_zone_purge(server_t *server, conf_t *conf, const knot_dname_t *zone) +{ + assert(server); + assert(zone); + + if (server->catalog.ro_txn == NULL) { + return KNOT_EOK; // no catalog at all + } + + if (conf != NULL) { + conf_val_t role = conf_zone_get(conf, C_CATALOG_ROLE, zone); + if (conf_opt(&role) != CATALOG_ROLE_INTERPRET) { + return KNOT_EOK; + } + } + + ssize_t members = 0; + int ret = catalog_update_del_all(&server->catalog_upd, &server->catalog, zone, &members); + if (ret == KNOT_EOK && members > 0) { + log_zone_info(zone, "catalog zone purged, %zd member zones deconfigured", members); + server->catalog_upd_signal = true; + if (kill(getpid(), SIGUSR1) != 0) { + ret = knot_map_errno(); + } + } + return ret; +} diff --git a/src/knot/catalog/catalog_update.h b/src/knot/catalog/catalog_update.h new file mode 100644 index 0000000..3726372 --- /dev/null +++ b/src/knot/catalog/catalog_update.h @@ -0,0 +1,171 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/qp-trie/trie.h" +#include "knot/catalog/catalog_db.h" +#include "knot/conf/conf.h" + +struct server; // "knot/server/server.h" causes preprocessor problems when included. + +typedef enum { + CAT_UPD_INVALID, // invalid value + CAT_UPD_ADD, // member addition + CAT_UPD_REM, // member removal + CAT_UPD_MINOR, // owner or catzone change, uniqID preserved + CAT_UPD_UNIQ, // uniqID change + CAT_UPD_PROP, // ONLY change of properties of existing member + CAT_UPD_MAX, // number of options in the enum +} catalog_upd_type_t; + +typedef struct catalog_upd_val { + knot_dname_t *member; // name of catalog member zone + catalog_upd_type_t type; // what kind of update this is + + knot_dname_t *rem_owner; // owner of PTR record being removed + knot_dname_t *rem_catz; // catalog zone the member being removed from + knot_dname_t *add_owner; // owner of PTR record being added + knot_dname_t *add_catz; // catalog zone the member being added to + + char *new_group; // the desired configuration group for the member +} catalog_upd_val_t; + +typedef struct { + trie_t *upd; // tree of catalog_upd_val_t, that gonna be changed in catalog + int error; // error occurred during generating of upd + pthread_mutex_t mutex; // lock for accessing this struct +} catalog_update_t; + +/*! + * \brief Initialize catalog update structure. + * + * \param u Catalog update to be initialized. + * + * \return KNOT_EOK, KNOT_ENOMEM + */ +int catalog_update_init(catalog_update_t *u); +catalog_update_t *catalog_update_new(void); + +/*! + * \brief Clear contents of catalog update structure. + * + * \param u Catalog update structure to be cleared. + */ +void catalog_update_clear(catalog_update_t *u); + +/*! + * \brief Free catalog update structure. + * + * \param u Catalog update structure. + */ +void catalog_update_deinit(catalog_update_t *u); +void catalog_update_free(catalog_update_t *u); + +/*! + * \brief Add a new record to catalog update structure. + * + * \param u Catalog update. + * \param member Member zone name to be added. + * \param owner Owner of respective PTR record. + * \param catzone Catalog zone holding the member. + * \param type CAT_UPD_REM, CAT_UPD_ADD, CAT_UPD_PROP. + * \param group Optional: member group property value. + * \param group_len Length of 'group' string (if not NULL). + * \param check_rem Check catalog DB for existing record to be removed. + * + * \return KNOT_E* + */ +int catalog_update_add(catalog_update_t *u, const knot_dname_t *member, + const knot_dname_t *owner, const knot_dname_t *catzone, + catalog_upd_type_t type, const char *group, + size_t group_len, catalog_t *check_rem); + +/*! + * \brief Read catalog update record for given member zone. + * + * \param u Catalog update. + * \param member Member zone name. + * \param remove Search in remove section. + * + * \return Found update record for given member zone; or NULL. + */ +catalog_upd_val_t *catalog_update_get(catalog_update_t *u, const knot_dname_t *member); + +/*! + * \brief Catalog update iteration. + */ +typedef trie_it_t catalog_it_t; + +inline static catalog_it_t *catalog_it_begin(catalog_update_t *u) +{ + return trie_it_begin(u->upd); +} + +inline static catalog_upd_val_t *catalog_it_val(catalog_it_t *it) +{ + return *(catalog_upd_val_t **)trie_it_val(it); +} + +inline static bool catalog_it_finished(catalog_it_t *it) +{ + return it == NULL || trie_it_finished(it); +} + +#define catalog_it_next trie_it_next +#define catalog_it_free trie_it_free + +/*! + * \brief Check Catalog update for conflicts with conf or other catalogs. + * + * \param u Catalog update to be aligned in-place. + * \param cat Catalog DB to check against. + * \param conf Relevant configuration. + */ +void catalog_update_finalize(catalog_update_t *u, catalog_t *cat, conf_t *conf); + +/*! + * \brief Put changes from Catalog Update into persistent Catalog database. + * + * \param u Catalog update to be committed. + * \param cat Catalog to be updated. + * + * \return KNOT_E* + */ +int catalog_update_commit(catalog_update_t *u, catalog_t *cat); + +/*! + * \brief Add to catalog update removals of all member zones of a single catalog zone. + * + * \param u Catalog update to be updated. + * \param cat Catalog database to be iterated. + * \param zone Name of catalog zone whose members gonna be removed. + * \param upd_count Output: number of resulting updates to catalog database. + * + * \return KNOT_E* + */ +int catalog_update_del_all(catalog_update_t *u, catalog_t *cat, const knot_dname_t *zone, ssize_t *upd_count); + +/*! + * \brief Destroy all members of specified catalog zone. + * + * \param server Server with catalog DB. + * \param conf Optional: check conf to skip if zone not catalog. + * \param zone Catalog zone name. + * + * \return KNOT_E* + */ +int catalog_zone_purge(struct server *server, conf_t *conf, const knot_dname_t *zone); diff --git a/src/knot/catalog/generate.c b/src/knot/catalog/generate.c new file mode 100644 index 0000000..e05442b --- /dev/null +++ b/src/knot/catalog/generate.c @@ -0,0 +1,346 @@ +/* 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 <string.h> + +#include "knot/catalog/generate.h" +#include "knot/common/log.h" +#include "knot/updates/zone-update.h" +#include "knot/zone/zonedb.h" +#include "contrib/openbsd/siphash.h" +#include "contrib/wire_ctx.h" + +static knot_dname_t *catalog_member_owner(const knot_dname_t *member, + const knot_dname_t *catzone, + time_t member_time) +{ + SIPHASH_CTX hash; + SIPHASH_KEY shkey = { 0 }; // only used for hashing -> zero key + SipHash24_Init(&hash, &shkey); + SipHash24_Update(&hash, member, knot_dname_size(member)); + uint64_t u64time = htobe64(member_time); + SipHash24_Update(&hash, &u64time, sizeof(u64time)); + uint64_t hashres = SipHash24_End(&hash); + + char *hexhash = bin_to_hex((uint8_t *)&hashres, sizeof(hashres), false); + if (hexhash == NULL) { + return NULL; + } + size_t hexlen = strlen(hexhash); + assert(hexlen == 16); + size_t zoneslen = knot_dname_size((uint8_t *)CATALOG_ZONES_LABEL); + assert(hexlen <= KNOT_DNAME_MAXLABELLEN && zoneslen <= KNOT_DNAME_MAXLABELLEN); + size_t catzlen = knot_dname_size(catzone); + + size_t outlen = hexlen + zoneslen + catzlen; + knot_dname_t *out; + if (outlen > KNOT_DNAME_MAXLEN || (out = malloc(outlen)) == NULL) { + free(hexhash); + return NULL; + } + + wire_ctx_t wire = wire_ctx_init(out, outlen); + wire_ctx_write_u8(&wire, hexlen); + wire_ctx_write(&wire, hexhash, hexlen); + wire_ctx_write(&wire, CATALOG_ZONES_LABEL, zoneslen); + wire_ctx_skip(&wire, -1); + wire_ctx_write(&wire, catzone, catzlen); + assert(wire.error == KNOT_EOK); + + free(hexhash); + return out; +} + +static bool same_group(zone_t *old_z, zone_t *new_z) +{ + if (old_z->catalog_group == NULL || new_z->catalog_group == NULL) { + return (old_z->catalog_group == new_z->catalog_group); + } else { + return (strcmp(old_z->catalog_group, new_z->catalog_group) == 0); + } +} + +void catalogs_generate(struct knot_zonedb *db_new, struct knot_zonedb *db_old) +{ + // general comment: catz->contents!=NULL means incremental update of catalog + + if (db_old != NULL) { + knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_old); + while (!knot_zonedb_iter_finished(it)) { + zone_t *zone = knot_zonedb_iter_val(it); + knot_dname_t *cg = zone->catalog_gen; + if (cg != NULL && knot_zonedb_find(db_new, zone->name) == NULL) { + zone_t *catz = knot_zonedb_find(db_new, cg); + if (catz != NULL && catz->contents != NULL) { + assert(catz->cat_members != NULL); // if this failed to allocate, catz wasn't added to zonedb + knot_dname_t *owner = catalog_member_owner(zone->name, cg, zone->timers.catalog_member); + if (owner == NULL) { + catz->cat_members->error = KNOT_ENOENT; + knot_zonedb_iter_next(it); + continue; + } + int ret = catalog_update_add(catz->cat_members, zone->name, owner, + cg, CAT_UPD_REM, NULL, 0, NULL); + free(owner); + if (ret != KNOT_EOK) { + catz->cat_members->error = ret; + } else { + zone_events_schedule_now(catz, ZONE_EVENT_LOAD); + } + } + } + knot_zonedb_iter_next(it); + } + knot_zonedb_iter_free(it); + } + + knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_new); + while (!knot_zonedb_iter_finished(it)) { + zone_t *zone = knot_zonedb_iter_val(it); + knot_dname_t *cg = zone->catalog_gen; + if (cg == NULL) { + knot_zonedb_iter_next(it); + continue; + } + zone_t *catz = knot_zonedb_find(db_new, cg); + zone_t *old = knot_zonedb_find(db_old, zone->name); + knot_dname_t *owner = catalog_member_owner(zone->name, cg, zone->timers.catalog_member); + size_t cgroup_size = zone->catalog_group == NULL ? 0 : strlen(zone->catalog_group); + if (catz == NULL) { + log_zone_warning(zone->name, "member zone belongs to non-existing catalog zone"); + } else if (catz->contents == NULL || old == NULL) { + assert(catz->cat_members != NULL); + if (owner == NULL) { + catz->cat_members->error = KNOT_ENOENT; + knot_zonedb_iter_next(it); + continue; + } + int ret = catalog_update_add(catz->cat_members, zone->name, owner, + cg, CAT_UPD_ADD, zone->catalog_group, + cgroup_size, NULL); + if (ret != KNOT_EOK) { + catz->cat_members->error = ret; + } else { + zone_events_schedule_now(catz, ZONE_EVENT_LOAD); + } + } else if (!same_group(zone, old)) { + int ret = catalog_update_add(catz->cat_members, zone->name, owner, + cg, CAT_UPD_PROP, zone->catalog_group, + cgroup_size, NULL); + if (ret != KNOT_EOK) { + catz->cat_members->error = ret; + } else { + zone_events_schedule_now(catz, ZONE_EVENT_LOAD); + } + } + free(owner); + knot_zonedb_iter_next(it); + } + knot_zonedb_iter_free(it); +} + +static void set_rdata(knot_rrset_t *rrset, uint8_t *data, uint16_t len) +{ + knot_rdata_init(rrset->rrs.rdata, len, data); + rrset->rrs.size = knot_rdata_size(len); +} + +#define def_txt_owner(ptr_owner) \ + knot_dname_storage_t txt_owner = "\x05""group"; \ + size_t _ptr_ow_len = knot_dname_size(ptr_owner); \ + size_t _ptr_ow_ind = strlen((const char *)txt_owner); \ + if (_ptr_ow_ind + _ptr_ow_len > sizeof(txt_owner)) { \ + return KNOT_ERANGE; \ + } \ + memcpy(txt_owner + _ptr_ow_ind, (ptr_owner), _ptr_ow_len); + +static int add_group_txt(const knot_dname_t *ptr_owner, const char *group, + zone_contents_t *conts, zone_update_t *up) +{ + assert((conts == NULL) != (up == NULL)); + size_t group_len; + if (group == NULL || (group_len = strlen(group)) < 1) { + return KNOT_EOK; + } + assert(group_len <= 255); + + def_txt_owner(ptr_owner); + + uint8_t data[256] = { group_len }; + memcpy(data + 1, group, group_len); + + knot_rrset_t txt; + knot_rrset_init(&txt, txt_owner, KNOT_RRTYPE_TXT, KNOT_CLASS_IN, 0); + uint8_t txt_rd[256] = { 0 }; + txt.rrs.rdata = (knot_rdata_t *)txt_rd; + txt.rrs.count = 1; + set_rdata(&txt, data, 1 + group_len ); + + int ret; + if (conts != NULL) { + zone_node_t *unused = NULL; + ret = zone_contents_add_rr(conts, &txt, &unused); + } else { + ret = zone_update_add(up, &txt); + } + + return ret; +} + +static int rem_group_txt(const knot_dname_t *ptr_owner, zone_update_t *up) +{ + def_txt_owner(ptr_owner); + + int ret = zone_update_remove_rrset(up, txt_owner, KNOT_RRTYPE_TXT); + if (ret == KNOT_ENOENT || ret == KNOT_ENONODE) { + ret = KNOT_EOK; + } + + return ret; +} + +struct zone_contents *catalog_update_to_zone(catalog_update_t *u, const knot_dname_t *catzone, + uint32_t soa_serial) +{ + if (u->error != KNOT_EOK) { + return NULL; + } + zone_contents_t *c = zone_contents_new(catzone, true); + if (c == NULL) { + return c; + } + + zone_node_t *unused = NULL; + uint8_t invalid[9] = "\x07""invalid"; + uint8_t version[9] = "\x07""version"; + uint8_t cat_version[2] = "\x01" CATALOG_ZONE_VERSION; + + // prepare common rrset with one rdata item + uint8_t rdata[256] = { 0 }; + knot_rrset_t rrset; + knot_rrset_init(&rrset, (knot_dname_t *)catzone, KNOT_RRTYPE_SOA, KNOT_CLASS_IN, 0); + rrset.rrs.rdata = (knot_rdata_t *)rdata; + rrset.rrs.count = 1; + + // set catalog zone's SOA + uint8_t data[250]; + assert(sizeof(knot_rdata_t) + sizeof(data) <= sizeof(rdata)); + wire_ctx_t wire = wire_ctx_init(data, sizeof(data)); + wire_ctx_write(&wire, invalid, sizeof(invalid)); + wire_ctx_write(&wire, invalid, sizeof(invalid)); + wire_ctx_write_u32(&wire, soa_serial); + wire_ctx_write_u32(&wire, CATALOG_SOA_REFRESH); + wire_ctx_write_u32(&wire, CATALOG_SOA_RETRY); + wire_ctx_write_u32(&wire, CATALOG_SOA_EXPIRE); + wire_ctx_write_u32(&wire, 0); + set_rdata(&rrset, data, wire_ctx_offset(&wire)); + if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) { + goto fail; + } + + // set catalog zone's NS + unused = NULL; + rrset.type = KNOT_RRTYPE_NS; + set_rdata(&rrset, invalid, sizeof(invalid)); + if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) { + goto fail; + } + + // set catalog zone's version TXT + unused = NULL; + knot_dname_storage_t owner; + if (knot_dname_store(owner, version) == 0 || catalog_dname_append(owner, catzone) == 0) { + goto fail; + } + rrset.owner = owner; + rrset.type = KNOT_RRTYPE_TXT; + set_rdata(&rrset, cat_version, sizeof(cat_version)); + if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK) { + goto fail; + } + + // insert member zone PTR records + rrset.type = KNOT_RRTYPE_PTR; + catalog_it_t *it = catalog_it_begin(u); + while (!catalog_it_finished(it)) { + catalog_upd_val_t *val = catalog_it_val(it); + if (val->add_owner == NULL) { + continue; + } + rrset.owner = val->add_owner; + set_rdata(&rrset, val->member, knot_dname_size(val->member)); + unused = NULL; + if (zone_contents_add_rr(c, &rrset, &unused) != KNOT_EOK || + add_group_txt(val->add_owner, val->new_group, c, NULL) != KNOT_EOK) { + catalog_it_free(it); + goto fail; + } + catalog_it_next(it); + } + catalog_it_free(it); + + return c; +fail: + zone_contents_deep_free(c); + return NULL; +} + +int catalog_update_to_update(catalog_update_t *u, struct zone_update *zu) +{ + knot_rrset_t ptr; + knot_rrset_init(&ptr, NULL, KNOT_RRTYPE_PTR, KNOT_CLASS_IN, 0); + uint8_t tmp[KNOT_DNAME_MAXLEN + sizeof(knot_rdata_t)]; + ptr.rrs.rdata = (knot_rdata_t *)tmp; + ptr.rrs.count = 1; + + int ret = u->error; + catalog_it_t *it = catalog_it_begin(u); + while (!catalog_it_finished(it) && ret == KNOT_EOK) { + catalog_upd_val_t *val = catalog_it_val(it); + if (val->type == CAT_UPD_INVALID) { + catalog_it_next(it); + continue; + } + + if (val->type == CAT_UPD_PROP && knot_dname_is_equal(zu->zone->name, val->add_catz)) { + ret = rem_group_txt(val->add_owner, zu); + if (ret == KNOT_EOK) { + ret = add_group_txt(val->add_owner, val->new_group, NULL, zu); + } + catalog_it_next(it); + continue; + } + + set_rdata(&ptr, val->member, knot_dname_size(val->member)); + if (val->type == CAT_UPD_REM && knot_dname_is_equal(zu->zone->name, val->rem_catz)) { + ptr.owner = val->rem_owner; + ret = zone_update_remove(zu, &ptr); + if (ret == KNOT_EOK) { + ret = rem_group_txt(val->rem_owner, zu); + } + } + if (val->type == CAT_UPD_ADD && knot_dname_is_equal(zu->zone->name, val->add_catz)) { + ptr.owner = val->add_owner; + ret = zone_update_add(zu, &ptr); + if (ret == KNOT_EOK) { + ret = add_group_txt(val->add_owner, val->new_group, NULL, zu); + } + } + catalog_it_next(it); + } + catalog_it_free(it); + return ret; +} diff --git a/src/knot/catalog/generate.h b/src/knot/catalog/generate.h new file mode 100644 index 0000000..721c1ef --- /dev/null +++ b/src/knot/catalog/generate.h @@ -0,0 +1,56 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/catalog/catalog_update.h" + +#define CATALOG_SOA_REFRESH 3600 +#define CATALOG_SOA_RETRY 600 +#define CATALOG_SOA_EXPIRE (INT32_MAX - 1) + +struct knot_zonedb; + +/*! + * \brief Compare old and new zonedb, create incremental catalog upd in each catz->cat_members + */ +void catalogs_generate(struct knot_zonedb *db_new, struct knot_zonedb *db_old); + +struct zone_contents; + +/*! + * \brief Generate catalog zone contents from (full) catalog update. + * + * \param u Catalog update to read. + * \param catzone Catalog zone name. + * \param soa_serial SOA serial of the generated zone. + * + * \return Catalog zone contents, or NULL if ENOMEM. + */ +struct zone_contents *catalog_update_to_zone(catalog_update_t *u, const knot_dname_t *catzone, + uint32_t soa_serial); + +struct zone_update; + +/*! + * \brief Incrementally update catalog zone from catalog update. + * + * \param u Catalog update to read. + * \param zu Zone update to be updated. + * + * \return KNOT_E* + */ +int catalog_update_to_update(catalog_update_t *u, struct zone_update *zu); diff --git a/src/knot/catalog/interpret.c b/src/knot/catalog/interpret.c new file mode 100644 index 0000000..e7a5cf0 --- /dev/null +++ b/src/knot/catalog/interpret.c @@ -0,0 +1,257 @@ +/* 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 <stdio.h> + +#include "knot/catalog/interpret.h" +#include "knot/journal/serialization.h" + +struct cat_upd_ctx; +typedef int (*cat_interpret_cb_t)(zone_node_t *node, struct cat_upd_ctx *ctx); + +typedef struct cat_upd_ctx { + catalog_update_t *u; + const zone_contents_t *complete_conts; + int apex_labels; + bool remove; + bool zone_diff; + catalog_t *check; + cat_interpret_cb_t member_cb; + cat_interpret_cb_t property_cb; +} cat_upd_ctx_t; + +static bool label_eq(const knot_dname_t *a, const char *_b) +{ + const knot_dname_t *b = (const knot_dname_t *)_b; + return a[0] == b[0] && memcmp(a + 1, b + 1, a[0]) == 0; +} + +static bool check_zone_version(const zone_contents_t *zone) +{ + size_t zone_size = knot_dname_size(zone->apex->owner); + knot_dname_t sub[zone_size + 8]; + memcpy(sub, "\x07""version", 8); + memcpy(sub + 8, zone->apex->owner, zone_size); + + const zone_node_t *ver_node = zone_contents_find_node(zone, sub); + knot_rdataset_t *ver_rr = node_rdataset(ver_node, KNOT_RRTYPE_TXT); + if (ver_rr == NULL) { + return false; + } + + knot_rdata_t *rdata = ver_rr->rdata; + for (int i = 0; i < ver_rr->count; i++) { + if (rdata->len == 2 && rdata->data[1] == CATALOG_ZONE_VERSION[0]) { + return true; + } + rdata = knot_rdataset_next(rdata); + } + return false; +} + +static int interpret_node(zone_node_t *node, void * _ctx) +{ + cat_upd_ctx_t *ctx = _ctx; + + int labels_diff = knot_dname_labels(node->owner, NULL) - ctx->apex_labels + - 1 /* "zones" label */ - 1 /* unique-N label */; + assert(labels_diff >= 0); + + switch (labels_diff) { + case 0: + return ctx->member_cb(node, ctx); + case 1: + return ctx->property_cb(node, ctx); + default: + return KNOT_EOK; + } +} + +static int interpret_zone(zone_diff_t *zdiff, cat_upd_ctx_t *ctx) +{ + knot_dname_storage_t sub; + if (knot_dname_store(sub, (uint8_t *)CATALOG_ZONES_LABEL) == 0 || + catalog_dname_append(sub, zdiff->apex->owner) == 0) { + return KNOT_EINVAL; + } + + if (zone_tree_get(&zdiff->nodes, sub) == NULL) { + return KNOT_EOK; + } + + return zone_tree_sub_apply(&zdiff->nodes, sub, true, interpret_node, ctx); +} + +static const knot_dname_t *property_get_member(const zone_node_t *prop_node, + const zone_contents_t *complete_conts, + const knot_dname_t **owner) +{ + assert(prop_node != NULL); + knot_rdataset_t *ptr = node_rdataset(prop_node->parent, KNOT_RRTYPE_PTR); + if (ptr == NULL) { + // fallback: search in provided complete zone contents + const knot_dname_t *memb_name = knot_wire_next_label(prop_node->owner, NULL); + const zone_node_t *memb_node = zone_contents_find_node(complete_conts, memb_name); + ptr = node_rdataset(memb_node, KNOT_RRTYPE_PTR); + if (memb_node != NULL) { + *owner = memb_node->owner; + } + } else { + *owner = prop_node->parent->owner; + } + if (*owner == NULL || ptr == NULL || ptr->count != 1) { + return NULL; + } + return knot_ptr_name(ptr->rdata); +} + +static int cat_update_add_memb(zone_node_t *node, cat_upd_ctx_t *ctx) +{ + const knot_rdataset_t *ptr = node_rdataset(node, KNOT_RRTYPE_PTR); + if (ptr == NULL) { + return KNOT_EOK; + } else if (ptr->count != 1) { + return KNOT_ERROR; + } + + const knot_rdataset_t *counter_ptr = node_rdataset(binode_counterpart(node), KNOT_RRTYPE_PTR); + if (knot_rdataset_subset(ptr, counter_ptr)) { + return KNOT_EOK; + } + + knot_rdata_t *rdata = ptr->rdata; + int ret = KNOT_EOK; + for (int i = 0; ret == KNOT_EOK && i < ptr->count; i++) { + const knot_dname_t *member = knot_ptr_name(rdata); + ret = catalog_update_add(ctx->u, member, node->owner, ctx->complete_conts->apex->owner, + ctx->remove ? CAT_UPD_REM : CAT_UPD_ADD, + NULL, 0, ctx->check); + rdata = knot_rdataset_next(rdata); + } + return ret; +} + +static int cat_update_add_grp(zone_node_t *node, cat_upd_ctx_t *ctx) +{ + if (!label_eq(node->owner, CATALOG_GROUP_LABEL)) { + return KNOT_EOK; + } + + const knot_dname_t *owner = NULL; + const knot_dname_t *member = property_get_member(node, ctx->complete_conts, &owner); + if (member == NULL) { + return KNOT_EOK; // just ignore property w/o member + } + + const knot_rdataset_t *txt = node_rdataset(node, KNOT_RRTYPE_TXT); + if (txt == NULL) { + return KNOT_EOK; + } else if (txt->count != 1) { + return KNOT_ERROR; + } + + const knot_rdataset_t *counter_txt = node_rdataset(binode_counterpart(node), KNOT_RRTYPE_TXT); + if (knot_rdataset_subset(txt, counter_txt)) { + return KNOT_EOK; + } + + const char *newgr = ""; + size_t grlen = 0; + if (!ctx->remove) { + assert(txt->count == 1); + // TXT rdata consists of one or more 1-byte prefixed strings. + if (txt->rdata->len != txt->rdata->data[0] + 1) { + return KNOT_EMALF; + } + newgr = (const char *)txt->rdata->data + 1; + grlen = txt->rdata->data[0]; + assert(grlen <= CATALOG_GROUP_MAXLEN); + } + + return catalog_update_add(ctx->u, member, owner, ctx->complete_conts->apex->owner, + CAT_UPD_PROP, newgr, grlen, ctx->check); +} + +int catalog_update_from_zone(catalog_update_t *u, struct zone_contents *zone, + const zone_diff_t *zone_diff, + const struct zone_contents *complete_contents, + bool remove, catalog_t *check, ssize_t *upd_count) +{ + int ret = KNOT_EOK; + zone_diff_t zdiff; + assert(zone == NULL || zone_diff == NULL); + if (zone != NULL) { + zone_diff_from_zone(&zdiff, zone); + } else { + zdiff = *zone_diff; + } + cat_upd_ctx_t ctx = { u, complete_contents, knot_dname_labels(zdiff.apex->owner, NULL), + remove, zone_diff != NULL, check, cat_update_add_memb, cat_update_add_grp }; + + pthread_mutex_lock(&u->mutex); + *upd_count -= trie_weight(u->upd); + if (zone_diff != NULL) { + zone_diff_reverse(&zdiff); + ctx.remove = true; + ret = interpret_zone(&zdiff, &ctx); + zone_diff_reverse(&zdiff); + ctx.remove = false; + ctx.check = NULL; + } + if (ret == KNOT_EOK) { + ret = interpret_zone(&zdiff, &ctx); + } + *upd_count += trie_weight(u->upd); + pthread_mutex_unlock(&u->mutex); + return ret; +} + +static int rr_count(const zone_node_t *node, uint16_t type) +{ + const knot_rdataset_t *rd = node_rdataset(node, type); + return rd == NULL ? 0 : rd->count; +} + +static int member_verify(zone_node_t *node, cat_upd_ctx_t *ctx) +{ + return rr_count(node, KNOT_RRTYPE_PTR) > 1 ? KNOT_EISRECORD : KNOT_EOK; +} + +static int prop_verify(zone_node_t *node, cat_upd_ctx_t *ctx) +{ + if (label_eq(node->owner, CATALOG_GROUP_LABEL) && + rr_count(node, KNOT_RRTYPE_TXT) > 1) { + return KNOT_EISRECORD; + } + + return KNOT_EOK; +} + +int catalog_zone_verify(const struct zone_contents *zone) +{ + cat_upd_ctx_t ctx = { NULL, zone, knot_dname_labels(zone->apex->owner, NULL), + false, false, NULL, member_verify, prop_verify }; + + if (!check_zone_version(zone)) { + return KNOT_EZONEINVAL; + } + + zone_diff_t zdiff; + zone_diff_from_zone(&zdiff, zone); + + return interpret_zone(&zdiff, &ctx); +} diff --git a/src/knot/catalog/interpret.h b/src/knot/catalog/interpret.h new file mode 100644 index 0000000..20928b7 --- /dev/null +++ b/src/knot/catalog/interpret.h @@ -0,0 +1,52 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/catalog/catalog_update.h" + +struct zone_contents; +struct zone_diff; + +/*! + * \brief Validate if given zone is valid catalog. + * + * \param zone Catalog zone in question. + * + * \retval KNOT_EZONEINVAL Invalid version record. + * \retval KNOT_EISRECORD Some of single-record RRSets has multiple RRs. + * \return KNOT_EOK All OK. + */ +int catalog_zone_verify(const struct zone_contents *zone); + +/*! + * \brief Iterate over PTR records in given zone contents and add members to catalog update. + * + * \param u Catalog update to be updated. + * \param zone Zone contents to be searched for member PTR records. + * \param zone_diff Zone diff to interpret for removals and additions. + * \param complete_contents Complete zone contents (zone might be from a changeset). + * \param remove Add removals of found member zones. + * \param check Optional: existing catalog database to be checked for existence + * of such record (useful for removals). + * \param upd_count Output: number of resulting updates to catalog database. + * + * \return KNOT_E* + */ +int catalog_update_from_zone(catalog_update_t *u, struct zone_contents *zone, + const struct zone_diff *zone_diff, + const struct zone_contents *complete_contents, + bool remove, catalog_t *check, ssize_t *upd_count); diff --git a/src/knot/common/evsched.c b/src/knot/common/evsched.c new file mode 100644 index 0000000..0d65c6a --- /dev/null +++ b/src/knot/common/evsched.c @@ -0,0 +1,268 @@ +/* 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 <sys/time.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <assert.h> + +#include "libknot/libknot.h" +#include "knot/server/dthreads.h" +#include "knot/common/evsched.h" + +/*! \brief Some implementations of timercmp >= are broken, this is for compat.*/ +static inline int timercmp_ge(struct timeval *a, struct timeval *b) { + return !timercmp(a, b, <); +} + +static int compare_event_heap_nodes(void *e1, void *e2) +{ + if (timercmp(&((event_t *)e1)->tv, &((event_t *)e2)->tv, <)) return -1; + if (timercmp(&((event_t *)e1)->tv, &((event_t *)e2)->tv, >)) return 1; + return 0; +} + +/*! + * \brief Get time T (now) + dt milliseconds. + */ +static struct timeval timeval_in(uint32_t dt) +{ + struct timeval tv = { 0 }; + gettimeofday(&tv, NULL); + + /* Add number of seconds. */ + tv.tv_sec += dt / 1000; + + /* Add the number of microseconds. */ + tv.tv_usec += (dt % 1000) * 1000; + + /* Check for overflow. */ + while (tv.tv_usec > 999999) { + tv.tv_sec += 1; + tv.tv_usec -= 1 * 1000 * 1000; + } + + return tv; +} + +/*! \brief Event scheduler loop. */ +static int evsched_run(dthread_t *thread) +{ + evsched_t *sched = (evsched_t*)thread->data; + if (sched == NULL) { + return KNOT_EINVAL; + } + + /* Run event loop. */ + pthread_mutex_lock(&sched->heap_lock); + while (!dt_is_cancelled(thread)) { + if (!!EMPTY_HEAP(&sched->heap) || sched->paused) { + pthread_cond_wait(&sched->notify, &sched->heap_lock); + continue; + } + + /* Get current time. */ + struct timeval dt; + gettimeofday(&dt, 0); + + /* Get next event. */ + event_t *ev = *((event_t**)HHEAD(&sched->heap)); + assert(ev != NULL); + + if (timercmp_ge(&dt, &ev->tv)) { + heap_delmin(&sched->heap); + ev->cb(ev); + } else { + /* Wait for next event or interrupt. Unlock calendar. */ + struct timespec ts; + ts.tv_sec = ev->tv.tv_sec; + ts.tv_nsec = ev->tv.tv_usec * 1000L; + pthread_cond_timedwait(&sched->notify, &sched->heap_lock, &ts); + } + } + pthread_mutex_unlock(&sched->heap_lock); + + return KNOT_EOK; +} + +int evsched_init(evsched_t *sched, void *ctx) +{ + memset(sched, 0, sizeof(evsched_t)); + sched->ctx = ctx; + + /* Initialize event calendar. */ + pthread_mutex_init(&sched->heap_lock, 0); + pthread_cond_init(&sched->notify, 0); + heap_init(&sched->heap, compare_event_heap_nodes, 0); + + sched->thread = dt_create(1, evsched_run, NULL, sched); + + if (sched->thread == NULL) { + evsched_deinit(sched); + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +void evsched_deinit(evsched_t *sched) +{ + if (sched == NULL) { + return; + } + + /* Deinitialize event calendar. */ + pthread_mutex_destroy(&sched->heap_lock); + pthread_cond_destroy(&sched->notify); + + while (!EMPTY_HEAP(&sched->heap)) { + event_t *e = (event_t *)*HHEAD(&sched->heap); + heap_delmin(&sched->heap); + evsched_event_free(e); + } + + heap_deinit(&sched->heap); + + if (sched->thread != NULL) { + dt_delete(&sched->thread); + } + + /* Clear the structure. */ + memset(sched, 0, sizeof(evsched_t)); +} + +event_t *evsched_event_create(evsched_t *sched, event_cb_t cb, void *data) +{ + /* Create event. */ + if (sched == NULL) { + return NULL; + } + + /* Allocate. */ + event_t *e = malloc(sizeof(event_t)); + if (e == NULL) { + return NULL; + } + + /* Initialize. */ + memset(e, 0, sizeof(event_t)); + e->sched = sched; + e->cb = cb; + e->data = data; + e->hpos.pos = 0; + + return e; +} + +void evsched_event_free(event_t *ev) +{ + if (ev == NULL) { + return; + } + + free(ev); +} + +int evsched_schedule(event_t *ev, uint32_t dt) +{ + if (ev == NULL || ev->sched == NULL) { + return KNOT_EINVAL; + } + + struct timeval new_time = timeval_in(dt); + + evsched_t *sched = ev->sched; + + /* Lock calendar. */ + pthread_mutex_lock(&sched->heap_lock); + + ev->tv = new_time; + + /* Make sure it's not already enqueued. */ + int found = heap_find(&sched->heap, (heap_val_t *)ev); + if (found > 0) { + /* "Replacing" with itself -- just repositioning it. */ + heap_replace(&sched->heap, found, (heap_val_t *)ev); + } else { + heap_insert(&sched->heap, (heap_val_t *)ev); + } + + /* Unlock calendar. */ + pthread_cond_signal(&sched->notify); + pthread_mutex_unlock(&sched->heap_lock); + + return KNOT_EOK; +} + +int evsched_cancel(event_t *ev) +{ + if (ev == NULL || ev->sched == NULL) { + return KNOT_EINVAL; + } + + evsched_t *sched = ev->sched; + + /* Lock calendar. */ + pthread_mutex_lock(&sched->heap_lock); + + int found = heap_find(&sched->heap, (heap_val_t *)ev); + if (found > 0) { + heap_delete(&sched->heap, found); + pthread_cond_signal(&sched->notify); + } + + /* Unlock calendar. */ + pthread_mutex_unlock(&sched->heap_lock); + + /* Reset event timer. */ + memset(&ev->tv, 0, sizeof(struct timeval)); + + return KNOT_EOK; +} + +void evsched_start(evsched_t *sched) +{ + dt_start(sched->thread); +} + +void evsched_stop(evsched_t *sched) +{ + pthread_mutex_lock(&sched->heap_lock); + dt_stop(sched->thread); + pthread_cond_signal(&sched->notify); + pthread_mutex_unlock(&sched->heap_lock); +} + +void evsched_join(evsched_t *sched) +{ + dt_join(sched->thread); +} + +void evsched_pause(evsched_t *sched) +{ + pthread_mutex_lock(&sched->heap_lock); + sched->paused = true; + pthread_mutex_unlock(&sched->heap_lock); +} + +void evsched_resume(evsched_t *sched) +{ + pthread_mutex_lock(&sched->heap_lock); + sched->paused = false; + pthread_cond_signal(&sched->notify); + pthread_mutex_unlock(&sched->heap_lock); +} diff --git a/src/knot/common/evsched.h b/src/knot/common/evsched.h new file mode 100644 index 0000000..762c3f8 --- /dev/null +++ b/src/knot/common/evsched.h @@ -0,0 +1,154 @@ +/* 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/>. + */ + +/*! + * \brief Event scheduler. + */ + +#pragma once + +#include <pthread.h> +#include <stdbool.h> +#include <stdint.h> +#include <sys/time.h> + +#include "knot/server/dthreads.h" +#include "contrib/ucw/heap.h" + +/* Forward decls. */ +struct evsched; +struct event; + +/*! + * \brief Event callback. + * + * Pointer to whole event structure is passed to the callback. + * Callback should return 0 on success and negative integer on error. + * + * Example callback: + * \code + * void print_callback(event_t *t) { + * printf("Callback: %s\n", t->data); + * } + * \endcode + */ +typedef void (*event_cb_t)(struct event *); + +/*! + * \brief Event structure. + */ +typedef struct event { + struct heap_val hpos; + struct timeval tv; /*!< Event scheduled time. */ + void *data; /*!< Usable data ptr. */ + event_cb_t cb; /*!< Event callback. */ + struct evsched *sched; /*!< Scheduler for this event. */ +} event_t; + +/*! + * \brief Event scheduler structure. + */ +typedef struct evsched { + volatile bool paused; /*!< Temporarily stop processing events. */ + pthread_mutex_t heap_lock; /*!< Event heap locking. */ + pthread_cond_t notify; /*!< Event heap notification. */ + struct heap heap; /*!< Event heap. */ + void *ctx; /*!< Scheduler context. */ + dt_unit_t *thread; +} evsched_t; + +/*! + * \brief Initialize event scheduler instance. + * + * \retval New instance on success. + * \retval NULL on error. + */ +int evsched_init(evsched_t *sched, void *ctx); + +/*! + * \brief Deinitialize and free event scheduler instance. + * + * \param sched Pointer to event scheduler instance. + */ +void evsched_deinit(evsched_t *sched); + +/*! + * \brief Create a callback event. + * + * \note Scheduler takes ownership of scheduled events. Created, but unscheduled + * events are in the ownership of the caller. + * + * \param sched Pointer to event scheduler instance. + * \param cb Callback handler. + * \param data Data for callback. + * + * \retval New instance on success. + * \retval NULL on error. + */ +event_t *evsched_event_create(evsched_t *sched, event_cb_t cb, void *data); + +/*! + * \brief Dispose event instance. + * + * \param ev Event instance. + */ +void evsched_event_free(event_t *ev); + +/*! + * \brief Schedule an event. + * + * \note This function checks if the event was already scheduled, if it was + * then it replaces this timer with the newer value. + * Running events are not canceled or waited for. + * + * \param ev Prepared event. + * \param dt Time difference in milliseconds from now (dt is relative). + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL + */ +int evsched_schedule(event_t *ev, uint32_t dt); + +/*! + * \brief Cancel a scheduled event. + * + * \warning May block until current running event is finished (as it cannot + * interrupt running event). + * + * \warning Never cancel event in it's callback. As it never finishes, + * it deadlocks. + * + * \param ev Scheduled event. + * + * \retval KNOT_EOK + * \retval KNOT_EINVAL + */ +int evsched_cancel(event_t *ev); + +/*! \brief Start event processing threads. */ +void evsched_start(evsched_t *sched); + +/*! \brief Stop event processing threads. */ +void evsched_stop(evsched_t *sched); + +/*! \brief Join event processing threads. */ +void evsched_join(evsched_t *sched); + +/*! \brief Temporarily stop processing events. */ +void evsched_pause(evsched_t *sched); + +/*! \brief Resume processing events. */ +void evsched_resume(evsched_t *sched); diff --git a/src/knot/common/fdset.c b/src/knot/common/fdset.c new file mode 100644 index 0000000..a4b37d9 --- /dev/null +++ b/src/knot/common/fdset.c @@ -0,0 +1,336 @@ +/* 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 <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "knot/common/fdset.h" +#include "contrib/time.h" +#include "contrib/macros.h" + +#define MEM_RESIZE(p, n) { \ + void *tmp = NULL; \ + if ((tmp = realloc((p), (n) * sizeof(*p))) == NULL) { \ + return KNOT_ENOMEM; \ + } \ + (p) = tmp; \ +} + +static int fdset_resize(fdset_t *set, const unsigned size) +{ + assert(set); + + MEM_RESIZE(set->ctx, size); + MEM_RESIZE(set->timeout, size); +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + MEM_RESIZE(set->ev, size); +#else + MEM_RESIZE(set->pfd, size); +#endif + set->size = size; + return KNOT_EOK; +} + +int fdset_init(fdset_t *set, const unsigned size) +{ + if (set == NULL) { + return KNOT_EINVAL; + } + + memset(set, 0, sizeof(*set)); + +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) +#ifdef HAVE_EPOLL + set->pfd = epoll_create1(0); +#elif HAVE_KQUEUE + set->pfd = kqueue(); +#endif + if (set->pfd < 0) { + return knot_map_errno(); + } +#endif + int ret = fdset_resize(set, size); +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + if (ret != KNOT_EOK) { + close(set->pfd); + } +#endif + return ret; +} + +void fdset_clear(fdset_t *set) +{ + if (set == NULL) { + return; + } + + free(set->ctx); + free(set->timeout); +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + free(set->ev); + free(set->recv_ev); + close(set->pfd); +#else + free(set->pfd); +#endif + memset(set, 0, sizeof(*set)); +} + +int fdset_add(fdset_t *set, const int fd, const fdset_event_t events, void *ctx) +{ + if (set == NULL || fd < 0) { + return KNOT_EINVAL; + } + + if (set->n == set->size && + fdset_resize(set, set->size + FDSET_RESIZE_STEP) != KNOT_EOK) { + return KNOT_ENOMEM; + } + + const int idx = set->n++; + set->ctx[idx] = ctx; + set->timeout[idx] = 0; +#ifdef HAVE_EPOLL + set->ev[idx].data.fd = fd; + set->ev[idx].events = events; + struct epoll_event ev = { + .data.u64 = idx, + .events = events + }; + if (epoll_ctl(set->pfd, EPOLL_CTL_ADD, fd, &ev) != 0) { + return knot_map_errno(); + } +#elif HAVE_KQUEUE + EV_SET(&set->ev[idx], fd, events, EV_ADD, 0, 0, (void *)(intptr_t)idx); + if (kevent(set->pfd, &set->ev[idx], 1, NULL, 0, NULL) < 0) { + return knot_map_errno(); + } +#else + set->pfd[idx].fd = fd; + set->pfd[idx].events = events; + set->pfd[idx].revents = 0; +#endif + + return idx; +} + +int fdset_remove(fdset_t *set, const unsigned idx) +{ + if (set == NULL || idx >= set->n) { + return KNOT_EINVAL; + } + + const int fd = fdset_get_fd(set, idx); +#ifdef HAVE_EPOLL + /* This is necessary as DDNS duplicates file descriptors! */ + if (epoll_ctl(set->pfd, EPOLL_CTL_DEL, fd, NULL) != 0) { + close(fd); + return knot_map_errno(); + } +#elif HAVE_KQUEUE + /* Return delete flag back to original filter number. */ +#if defined(__NetBSD__) + if ((signed short)set->ev[idx].filter < 0) +#else + if (set->ev[idx].filter >= 0) +#endif + { + set->ev[idx].filter = ~set->ev[idx].filter; + } + set->ev[idx].flags = EV_DELETE; + if (kevent(set->pfd, &set->ev[idx], 1, NULL, 0, NULL) < 0) { + close(fd); + return knot_map_errno(); + } +#endif + close(fd); + + const unsigned last = --set->n; + /* Nothing else if it is the last one. Move last -> i if some remain. */ + if (idx < last) { + set->ctx[idx] = set->ctx[last]; + set->timeout[idx] = set->timeout[last]; +#if defined(HAVE_EPOLL) || defined (HAVE_KQUEUE) + set->ev[idx] = set->ev[last]; +#ifdef HAVE_EPOLL + struct epoll_event ev = { + .data.u64 = idx, + .events = set->ev[idx].events + }; + if (epoll_ctl(set->pfd, EPOLL_CTL_MOD, set->ev[last].data.fd, &ev) != 0) { + return knot_map_errno(); + } +#elif HAVE_KQUEUE + EV_SET(&set->ev[idx], set->ev[last].ident, set->ev[last].filter, + EV_ADD, 0, 0, (void *)(intptr_t)idx); + if (kevent(set->pfd, &set->ev[idx], 1, NULL, 0, NULL) < 0) { + return knot_map_errno(); + } +#endif +#else + set->pfd[idx] = set->pfd[last]; +#endif + } + + return KNOT_EOK; +} + +int fdset_poll(fdset_t *set, fdset_it_t *it, const unsigned offset, const int timeout_ms) +{ + if (it == NULL) { + return KNOT_EINVAL; + } + it->unprocessed = 0; + + if (set == NULL) { + return KNOT_EINVAL; + } + + it->set = set; + it->idx = offset; +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + if (set->recv_size != set->size) { + MEM_RESIZE(set->recv_ev, set->size); + set->recv_size = set->size; + } + it->ptr = set->recv_ev; + it->dirty = 0; +#ifdef HAVE_EPOLL + if (set->n == 0) { + return 0; + } + if ((it->unprocessed = epoll_wait(set->pfd, set->recv_ev, set->recv_size, + timeout_ms)) == -1) { + return knot_map_errno(); + } +#ifndef NDEBUG + /* In specific circumstances with valgrind, it sometimes happens that + * `set->n < it->unprocessed`. */ + if (it->unprocessed > 0 && unlikely(it->unprocessed > set->n)) { + assert(it->unprocessed == 232); + it->unprocessed = 0; + } +#endif +#elif HAVE_KQUEUE + struct timespec timeout = { + .tv_sec = timeout_ms / 1000, + .tv_nsec = (timeout_ms % 1000) * 1000000 + }; + if ((it->unprocessed = kevent(set->pfd, NULL, 0, set->recv_ev, set->recv_size, + (timeout_ms >= 0) ? &timeout : NULL)) == -1) { + return knot_map_errno(); + } +#endif + /* + * NOTE: Can't skip offset without bunch of syscalls! + * Because of that it waits for `ctx->n` (every socket). Offset is set when TCP + * throttling is ON. Sometimes it can return with sockets where none of them is + * connected socket, but it should not be common. + */ + while (it->unprocessed > 0 && fdset_it_get_idx(it) < it->idx) { + it->ptr++; + it->unprocessed--; + } + return it->unprocessed; +#else + it->unprocessed = poll(&set->pfd[offset], set->n - offset, timeout_ms); +#ifndef NDEBUG + /* In specific circumstances with valgrind, it sometimes happens that + * `set->n < it->unprocessed`. */ + if (it->unprocessed > 0 && unlikely(it->unprocessed > set->n - offset)) { + assert(it->unprocessed == 7); + it->unprocessed = 0; + } +#endif + while (it->unprocessed > 0 && set->pfd[it->idx].revents == 0) { + it->idx++; + } + return it->unprocessed; +#endif +} + +void fdset_it_commit(fdset_it_t *it) +{ + if (it == NULL) { + return; + } +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + /* NOTE: reverse iteration to avoid as much "remove last" operations + * as possible. I'm not sure about performance improvement. It + * will skip some syscalls at begin of iteration, but what + * performance increase do we get is a question. + */ + fdset_t *set = it->set; + for (int i = set->n - 1; it->dirty > 0 && i >= 0; --i) { +#ifdef HAVE_EPOLL + if (set->ev[i].events == FDSET_REMOVE_FLAG) +#else +#if defined(__NetBSD__) + if ((signed short)set->ev[i].filter < 0) +#else + if (set->ev[i].filter >= 0) +#endif +#endif + { + (void)fdset_remove(set, i); + it->dirty--; + } + } + assert(it->dirty == 0); +#endif +} + +int fdset_set_watchdog(fdset_t *set, const unsigned idx, const int interval) +{ + if (set == NULL || idx >= set->n) { + return KNOT_EINVAL; + } + + /* Lift watchdog if interval is negative. */ + if (interval < 0) { + set->timeout[idx] = 0; + return KNOT_EOK; + } + + /* Update clock. */ + const struct timespec now = time_now(); + set->timeout[idx] = now.tv_sec + interval; /* Only seconds precision. */ + + return KNOT_EOK; +} + +void fdset_sweep(fdset_t *set, const fdset_sweep_cb_t cb, void *data) +{ + if (set == NULL || cb == NULL) { + return; + } + + /* Get time threshold. */ + const struct timespec now = time_now(); + unsigned idx = 0; + while (idx < set->n) { + /* Check sweep state, remove if requested. */ + if (set->timeout[idx] > 0 && set->timeout[idx] <= now.tv_sec) { + const int fd = fdset_get_fd(set, idx); + if (cb(set, fd, data) == FDSET_SWEEP) { + (void)fdset_remove(set, idx); + continue; + } + } + ++idx; + } +} diff --git a/src/knot/common/fdset.h b/src/knot/common/fdset.h new file mode 100644 index 0000000..95a5c61 --- /dev/null +++ b/src/knot/common/fdset.h @@ -0,0 +1,382 @@ +/* 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/>. + */ + +/*! + * \brief I/O multiplexing with context and timeouts for each fd. + */ + +#pragma once + +#include <assert.h> +#include <stdbool.h> +#include <stddef.h> +#include <time.h> + +#ifdef HAVE_EPOLL +#include <sys/epoll.h> +#elif HAVE_KQUEUE +#include <sys/event.h> +#else +#include <poll.h> +#endif + +#include "libknot/errcode.h" + +#define FDSET_RESIZE_STEP 256 +#ifdef HAVE_EPOLL +#define FDSET_REMOVE_FLAG ~0U +#endif + +/*! \brief Set of file descriptors with associated context and timeouts. */ +typedef struct { + unsigned n; /*!< Active fds. */ + unsigned size; /*!< Array size (allocated). */ + void **ctx; /*!< Context for each fd. */ + time_t *timeout; /*!< Timeout for each fd (seconds precision). */ +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) +#ifdef HAVE_EPOLL + struct epoll_event *ev; /*!< Epoll event storage for each fd. */ + struct epoll_event *recv_ev; /*!< Array for polled events. */ +#elif HAVE_KQUEUE + struct kevent *ev; /*!< Kqueue event storage for each fd. */ + struct kevent *recv_ev; /*!< Array for polled events. */ +#endif + unsigned recv_size; /*!< Size of array for polled events. */ + int pfd; /*!< File descriptor of kernel polling structure (epoll or kqueue). */ +#else + struct pollfd *pfd; /*!< Poll state for each fd. */ +#endif +} fdset_t; + +/*! \brief State of iterator over received events */ +typedef struct { + fdset_t *set; /*!< Source fdset_t. */ + unsigned idx; /*!< Event index offset. */ + int unprocessed; /*!< Unprocessed events left. */ +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) +#ifdef HAVE_EPOLL + struct epoll_event *ptr; /*!< Pointer on processed event. */ +#elif HAVE_KQUEUE + struct kevent *ptr; /*!< Pointer on processed event. */ +#endif + unsigned dirty; /*!< Number of fd to be removed on commit. */ +#endif +} fdset_it_t; + +typedef enum { +#ifdef HAVE_EPOLL + FDSET_POLLIN = EPOLLIN, + FDSET_POLLOUT = EPOLLOUT, +#elif HAVE_KQUEUE + FDSET_POLLIN = EVFILT_READ, + FDSET_POLLOUT = EVFILT_WRITE, +#else + FDSET_POLLIN = POLLIN, + FDSET_POLLOUT = POLLOUT, +#endif +} fdset_event_t; + +/*! \brief Mark-and-sweep state. */ +typedef enum { + FDSET_KEEP, + FDSET_SWEEP +} fdset_sweep_state_t; + +/*! \brief Sweep callback (set, index, data) */ +typedef fdset_sweep_state_t (*fdset_sweep_cb_t)(fdset_t *, int, void *); + +/*! + * \brief Initialize fdset to given size. + * + * \param set Target set. + * \param size Initial set size. + * + * \return Error code, KNOT_EOK if success. + */ +int fdset_init(fdset_t *set, const unsigned size); + +/*! + * \brief Clear whole context of the fdset. + * + * \param set Target set. + */ +void fdset_clear(fdset_t *set); + +/*! + * \brief Add file descriptor to watched set. + * + * \param set Target set. + * \param fd Added file descriptor. + * \param events Mask of watched events. + * \param ctx Context (optional). + * + * \retval ret >= 0 is index of the added fd. + * \retval ret < 0 on error. + */ +int fdset_add(fdset_t *set, const int fd, const fdset_event_t events, void *ctx); + +/*! + * \brief Remove and close file descriptor from watched set. + * + * \param set Target set. + * \param idx Index of the removed fd. + * + * \return Error code, KNOT_EOK if success. + */ +int fdset_remove(fdset_t *set, const unsigned idx); + +/*! + * \brief Wait for receive events. + * + * Skip events based on offset and set iterator on first event. + * + * \param set Target set. + * \param it Event iterator storage. + * \param offset Index of first event. + * \param timeout_ms Timeout of operation in milliseconds (use -1 for unlimited). + * + * \retval ret >= 0 represents number of events received. + * \retval ret < 0 on error. + */ +int fdset_poll(fdset_t *set, fdset_it_t *it, const unsigned offset, const int timeout_ms); + +/*! + * \brief Set file descriptor watchdog interval. + * + * Set time (interval from now) after which the associated file descriptor + * should be sweeped (see fdset_sweep). Good example is setting a grace period + * of N seconds between socket activity. If socket is not active within + * <now, now + interval>, it is sweeped and closed. + * + * \param set Target set. + * \param idx Index of the file descriptor. + * \param interval Allowed interval without activity (seconds). + * -1 disables watchdog timer. + * + * \return Error code, KNOT_EOK if success. + */ +int fdset_set_watchdog(fdset_t *set, const unsigned idx, const int interval); + +/*! + * \brief Sweep file descriptors with exceeding inactivity period. + * + * \param set Target set. + * \param cb Callback for sweeped descriptors. + * \param data Pointer to extra data. + */ +void fdset_sweep(fdset_t *set, const fdset_sweep_cb_t cb, void *data); + +/*! + * \brief Returns file descriptor based on index. + * + * \param set Target set. + * \param idx Index of the file descriptor. + * + * \retval ret >= 0 for file descriptor. + * \retval ret < 0 on errors. + */ +inline static int fdset_get_fd(const fdset_t *set, const unsigned idx) +{ + assert(set && idx < set->n); + +#ifdef HAVE_EPOLL + return set->ev[idx].data.fd; +#elif HAVE_KQUEUE + return set->ev[idx].ident; +#else + return set->pfd[idx].fd; +#endif +} + +/*! + * \brief Returns number of file descriptors stored in set. + * + * \param set Target set. + * + * \retval Number of descriptors stored + */ +inline static unsigned fdset_get_length(const fdset_t *set) +{ + assert(set); + + return set->n; +} + +/*! + * \brief Get index of event in set referenced by iterator. + * + * \param it Target iterator. + * + * \retval Index of event. + */ +inline static unsigned fdset_it_get_idx(const fdset_it_t *it) +{ + assert(it); + +#ifdef HAVE_EPOLL + return it->ptr->data.u64; +#elif HAVE_KQUEUE + return (unsigned)(intptr_t)it->ptr->udata; +#else + return it->idx; +#endif +} + +/*! + * \brief Get file descriptor of event referenced by iterator. + * + * \param it Target iterator. + * + * \retval ret >= 0 for file descriptor. + * \retval ret < 0 on errors. + */ +inline static int fdset_it_get_fd(const fdset_it_t *it) +{ + assert(it); + +#ifdef HAVE_EPOLL + return it->set->ev[fdset_it_get_idx(it)].data.fd; +#elif HAVE_KQUEUE + return it->ptr->ident; +#else + return it->set->pfd[it->idx].fd; +#endif +} + +/*! + * \brief Move iterator on next received event. + * + * \param it Target iterator. + */ +inline static void fdset_it_next(fdset_it_t *it) +{ + assert(it); + +#if defined(HAVE_EPOLL) || defined(HAVE_KQUEUE) + do { + it->ptr++; + it->unprocessed--; + } while (it->unprocessed > 0 && fdset_it_get_idx(it) < it->idx); +#else + if (--it->unprocessed > 0) { + while (it->set->pfd[++it->idx].revents == 0); /* nop */ + } +#endif +} + +/*! + * \brief Remove file descriptor referenced by iterator from watched set. + * + * \param it Target iterator. + * + * \return Error code, KNOT_EOK if success. + */ +inline static void fdset_it_remove(fdset_it_t *it) +{ + assert(it); + +#ifdef HAVE_EPOLL + const int idx = fdset_it_get_idx(it); + it->set->ev[idx].events = FDSET_REMOVE_FLAG; + it->dirty++; +#elif HAVE_KQUEUE + const int idx = fdset_it_get_idx(it); + /* Bitwise negated filter marks event for delete. */ + /* Filters become: */ + /* [FreeBSD] */ + /* EVFILT_READ (-1) -> 0 */ + /* EVFILT_WRITE (-2) -> 1 */ + /* [NetBSD] */ + /* EVFILT_READ (0) -> -1 */ + /* EVFILT_WRITE (1) -> -2 */ + /* If not marked for delete then mark for delete. */ +#if defined(__NetBSD__) + if ((signed short)it->set->ev[idx].filter >= 0) +#else + if (it->set->ev[idx].filter < 0) +#endif + { + it->set->ev[idx].filter = ~it->set->ev[idx].filter; + } + it->dirty++; +#else + (void)fdset_remove(it->set, fdset_it_get_idx(it)); + /* Iterator should return on last valid already processed element. */ + /* On `next` call (in for-loop) will point on first unprocessed. */ + it->idx--; +#endif +} + +/*! + * \brief Commit changes made in fdset using iterator. + * + * \param it Target iterator. + */ +void fdset_it_commit(fdset_it_t *it); + +/*! + * \brief Decide if there is more received events. + * + * \param it Target iterator. + * + * \retval Logical flag representing 'done' state. + */ +inline static bool fdset_it_is_done(const fdset_it_t *it) +{ + assert(it); + + return it->unprocessed <= 0; +} + +/*! + * \brief Decide if event referenced by iterator is POLLIN event. + * + * \param it Target iterator. + * + * \retval Logical flag represents 'POLLIN' event received. + */ +inline static bool fdset_it_is_pollin(const fdset_it_t *it) +{ + assert(it); + +#ifdef HAVE_EPOLL + return it->ptr->events & EPOLLIN; +#elif HAVE_KQUEUE + return it->ptr->filter == EVFILT_READ; +#else + return it->set->pfd[it->idx].revents & POLLIN; +#endif +} + +/*! + * \brief Decide if event referenced by iterator is error event. + * + * \param it Target iterator. + * + * \retval Logical flag represents error event received. + */ +inline static bool fdset_it_is_error(const fdset_it_t *it) +{ + assert(it); + +#ifdef HAVE_EPOLL + return it->ptr->events & (EPOLLERR | EPOLLHUP); +#elif HAVE_KQUEUE + return it->ptr->flags & EV_ERROR; +#else + return it->set->pfd[it->idx].revents & (POLLERR | POLLHUP | POLLNVAL); +#endif +} diff --git a/src/knot/common/log.c b/src/knot/common/log.c new file mode 100644 index 0000000..8bbdc51 --- /dev/null +++ b/src/knot/common/log.c @@ -0,0 +1,491 @@ +/* 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 <stdarg.h> +#include <stdio.h> +#include <string.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/time.h> +#include <time.h> +#include <urcu.h> + +#ifdef ENABLE_SYSTEMD +#define SD_JOURNAL_SUPPRESS_LOCATION 1 +#include <systemd/sd-journal.h> +#include <systemd/sd-daemon.h> +#endif + +#include "knot/common/log.h" +#include "libknot/libknot.h" +#include "contrib/ucw/lists.h" + +/*! Single log message buffer length (one line). */ +#define LOG_BUFLEN 512 +#define NULL_ZONE_STR "?" + +#ifdef ENABLE_SYSTEMD +int use_journal = 0; +#endif + +/*! Log context. */ +typedef struct { + size_t target_count; /*!< Log target count. */ + int *target; /*!< Log targets. */ + size_t file_count; /*!< Open files count. */ + FILE **file; /*!< Open files. */ + log_flag_t flags; /*!< Formatting flags. */ +} log_t; + +/*! Log singleton. */ +log_t *s_log = NULL; + +static bool log_isopen(void) +{ + return s_log != NULL; +} + +static void sink_free(log_t *log) +{ + if (log == NULL) { + return; + } + + // Close open log files. + for (int i = 0; i < log->file_count; ++i) { + fclose(log->file[i]); + } + free(log->target); + free(log->file); + free(log); +} + +/*! + * \brief Create logging targets respecting their canonical order. + * + * Facilities ordering: Syslog, Stderr, Stdout, File0... + */ +static log_t *sink_setup(size_t file_count) +{ + log_t *log = malloc(sizeof(*log)); + if (log == NULL) { + return NULL; + } + memset(log, 0, sizeof(*log)); + + // Reserve space for targets. + log->target_count = LOG_TARGET_FILE + file_count; + log->target = malloc(LOG_SOURCE_ANY * sizeof(int) * log->target_count); + if (!log->target) { + free(log); + return NULL; + } + memset(log->target, 0, LOG_SOURCE_ANY * sizeof(int) * log->target_count); + + // Reserve space for log files. + if (file_count > 0) { + log->file = malloc(sizeof(FILE *) * file_count); + if (!log->file) { + free(log->target); + free(log); + return NULL; + } + memset(log->file, 0, sizeof(FILE *) * file_count); + } + + return log; +} + +static void sink_publish(log_t *log) +{ + log_t **current_log = &s_log; + log_t *old_log = rcu_xchg_pointer(current_log, log); + synchronize_rcu(); + sink_free(old_log); +} + +static int *src_levels(log_t *log, log_target_t target, log_source_t src) +{ + assert(src < LOG_SOURCE_ANY); + return &log->target[LOG_SOURCE_ANY * target + src]; +} + +static void sink_levels_set(log_t *log, log_target_t target, log_source_t src, int levels) +{ + // Assign levels to the specified source. + if (src != LOG_SOURCE_ANY) { + *src_levels(log, target, src) = levels; + } else { + // ANY ~ set levels to all sources. + for (int i = 0; i < LOG_SOURCE_ANY; ++i) { + *src_levels(log, target, i) = levels; + } + } +} + +static void sink_levels_add(log_t *log, log_target_t target, log_source_t src, int levels) +{ + // Add levels to the specified source. + if (src != LOG_SOURCE_ANY) { + *src_levels(log, target, src) |= levels; + } else { + // ANY ~ add levels to all sources. + for (int i = 0; i < LOG_SOURCE_ANY; ++i) { + *src_levels(log, target, i) |= levels; + } + } +} + +void log_init(void) +{ + // Setup initial state. + int emask = LOG_MASK(LOG_CRIT) | LOG_MASK(LOG_ERR) | LOG_MASK(LOG_WARNING); + int imask = LOG_MASK(LOG_NOTICE) | LOG_MASK(LOG_INFO); + + // Publish base log sink. + log_t *log = sink_setup(0); + if (log == NULL) { + fprintf(stderr, "Failed to setup logging\n"); + return; + } + +#ifdef ENABLE_SYSTEMD + // Should only use the journal if system was booted with systemd. + use_journal = sd_booted(); +#endif + + sink_levels_set(log, LOG_TARGET_SYSLOG, LOG_SOURCE_ANY, emask); + sink_levels_set(log, LOG_TARGET_STDERR, LOG_SOURCE_ANY, emask); + sink_levels_set(log, LOG_TARGET_STDOUT, LOG_SOURCE_ANY, imask); + sink_publish(log); + + setlogmask(LOG_UPTO(LOG_DEBUG)); + openlog(PACKAGE_NAME, LOG_PID, LOG_DAEMON); +} + +void log_close(void) +{ + sink_publish(NULL); + + fflush(stdout); + fflush(stderr); + + closelog(); +} + +void log_flag_set(log_flag_t flag) +{ + if (log_isopen()) { + s_log->flags |= flag; + } +} + +void log_levels_set(log_target_t target, log_source_t src, int levels) +{ + if (log_isopen()) { + sink_levels_set(s_log, target, src, levels); + } +} + +void log_levels_add(log_target_t target, log_source_t src, int levels) +{ + if (log_isopen()) { + sink_levels_add(s_log, target, src, levels); + } +} + +static void emit_log_msg(int level, log_source_t src, const char *zone, + size_t zone_len, const char *msg, const char *param) +{ + log_t *log = s_log; + + // Syslog target. + if (*src_levels(log, LOG_TARGET_SYSLOG, src) & LOG_MASK(level)) { +#ifdef ENABLE_SYSTEMD + if (use_journal) { + char *zone_fmt = zone ? "ZONE=%.*s." : NULL; + sd_journal_send("PRIORITY=%d", level, + "MESSAGE=%s", msg, + zone_fmt, zone_len, zone, + param, NULL); + } else +#endif + { + syslog(level, "%s", msg); + } + } + + // Prefix date and time. + char tstr[LOG_BUFLEN] = { 0 }; + if (!(s_log->flags & LOG_FLAG_NOTIMESTAMP)) { + struct tm lt; + struct timeval tv; + gettimeofday(&tv, NULL); + time_t sec = tv.tv_sec; + if (localtime_r(&sec, <) != NULL) { + strftime(tstr, sizeof(tstr), KNOT_LOG_TIME_FORMAT " ", <); + } + } + + // Other log targets. + for (int i = LOG_TARGET_STDERR; i < LOG_TARGET_FILE + log->file_count; ++i) { + if (*src_levels(log, i, src) & LOG_MASK(level)) { + FILE *stream; + switch (i) { + case LOG_TARGET_STDERR: stream = stderr; break; + case LOG_TARGET_STDOUT: stream = stdout; break; + default: stream = log->file[i - LOG_TARGET_FILE]; break; + } + + // Print the message. + fprintf(stream, "%s%s\n", tstr, msg); + if (stream == stdout) { + fflush(stream); + } + } + } +} + +static const char *level_prefix(int level) +{ + switch (level) { + case LOG_DEBUG: return "debug"; + case LOG_INFO: return "info"; + case LOG_NOTICE: return "notice"; + case LOG_WARNING: return "warning"; + case LOG_ERR: return "error"; + case LOG_CRIT: return "critical"; + default: return NULL; + }; +} + +static int log_msg_add(char **write, size_t *capacity, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + int written = vsnprintf(*write, *capacity, fmt, args); + va_end(args); + + if (written < 0 || written >= *capacity) { + return KNOT_ESPACE; + } + + *write += written; + *capacity -= written; + + return KNOT_EOK; +} + +static void log_msg_text(int level, log_source_t src, const char *zone, + const char *fmt, va_list args, const char *param) +{ + if (!log_isopen() || src == LOG_SOURCE_ANY) { + return; + } + + // Buffer for log message. + char buff[LOG_BUFLEN]; + char *write = buff; + size_t capacity = sizeof(buff); + + rcu_read_lock(); + + // Prefix error level. + if (level != LOG_INFO || !(s_log->flags & LOG_FLAG_NOINFO)) { + const char *prefix = level_prefix(level); + int ret = log_msg_add(&write, &capacity, "%s: ", prefix); + if (ret != KNOT_EOK) { + rcu_read_unlock(); + return; + } + } + + // Prefix zone name. + size_t zone_len = 0; + if (zone != NULL) { + zone_len = strlen(zone); + if (zone_len > 0 && zone[zone_len - 1] == '.') { + zone_len--; + } + + int ret = log_msg_add(&write, &capacity, "[%.*s.] ", (int)zone_len, zone); + if (ret != KNOT_EOK) { + rcu_read_unlock(); + return; + } + } + + // Compile log message. + int ret = vsnprintf(write, capacity, fmt, args); + if (ret >= 0) { + // Send to logging targets. + emit_log_msg(level, src, zone, zone_len, buff, param); + } + + rcu_read_unlock(); +} + +void log_fmt(int priority, log_source_t src, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + log_msg_text(priority, src, NULL, fmt, args, NULL); + va_end(args); +} + +void log_fmt_zone(int priority, log_source_t src, const knot_dname_t *zone, + const char *param, const char *fmt, ...) +{ + knot_dname_txt_storage_t buff; + char *zone_str = knot_dname_to_str(buff, zone, sizeof(buff)); + if (zone_str == NULL) { + zone_str = NULL_ZONE_STR; + } + + va_list args; + va_start(args, fmt); + log_msg_text(priority, src, zone_str, fmt, args, param); + va_end(args); +} + +void log_fmt_zone_str(int priority, log_source_t src, const char *zone, + const char *fmt, ...) +{ + if (zone == NULL) { + zone = NULL_ZONE_STR; + } + + va_list args; + va_start(args, fmt); + log_msg_text(priority, src, zone, fmt, args, NULL); + va_end(args); +} + +int log_update_privileges(int uid, int gid) +{ + if (!log_isopen()) { + return KNOT_EOK; + } + + for (int i = 0; i < s_log->file_count; ++i) { + if (fchown(fileno(s_log->file[i]), uid, gid) < 0) { + log_error("failed to change log file owner"); + } + } + + return KNOT_EOK; +} + +static log_target_t get_logtype(const char *logname) +{ + assert(logname); + + if (strcasecmp(logname, "syslog") == 0) { + return LOG_TARGET_SYSLOG; + } else if (strcasecmp(logname, "stderr") == 0) { + return LOG_TARGET_STDERR; + } else if (strcasecmp(logname, "stdout") == 0) { + return LOG_TARGET_STDOUT; + } else { + return LOG_TARGET_FILE; + } +} + +static int log_open_file(log_t *log, const char *filename) +{ + assert(LOG_TARGET_FILE + log->file_count < log->target_count); + + // Open the file. + log->file[log->file_count] = fopen(filename, "a"); + if (log->file[log->file_count] == NULL) { + return knot_map_errno(); + } + + // Disable buffering. + setvbuf(log->file[log->file_count], NULL, _IONBF, 0); + + return LOG_TARGET_FILE + log->file_count++; +} + +void log_reconfigure(conf_t *conf) +{ + // Use defaults if no 'log' section is configured. + if (conf_id_count(conf, C_LOG) == 0) { + log_close(); + log_init(); + return; + } + + // Find maximum log target id. + unsigned files = 0; + for (conf_iter_t iter = conf_iter(conf, C_LOG); iter.code == KNOT_EOK; + conf_iter_next(conf, &iter)) { + conf_val_t id = conf_iter_id(conf, &iter); + if (get_logtype(conf_str(&id)) == LOG_TARGET_FILE) { + ++files; + } + } + + // Initialize logsystem. + log_t *log = sink_setup(files); + if (log == NULL) { + fprintf(stderr, "Failed to setup logging\n"); + return; + } + + // Setup logs. + for (conf_iter_t iter = conf_iter(conf, C_LOG); iter.code == KNOT_EOK; + conf_iter_next(conf, &iter)) { + conf_val_t id = conf_iter_id(conf, &iter); + const char *logname = conf_str(&id); + + // Get target. + int target = get_logtype(logname); + if (target == LOG_TARGET_FILE) { + target = log_open_file(log, logname); + if (target < 0) { + log_error("failed to open log, file '%s' (%s)", + logname, knot_strerror(target)); + continue; + } + } + + conf_val_t levels_val; + unsigned levels; + + // Set SERVER logging. + levels_val = conf_id_get(conf, C_LOG, C_SERVER, &id); + levels = conf_opt(&levels_val); + sink_levels_add(log, target, LOG_SOURCE_SERVER, levels); + + // Set CONTROL logging. + levels_val = conf_id_get(conf, C_LOG, C_CTL, &id); + levels = conf_opt(&levels_val); + sink_levels_add(log, target, LOG_SOURCE_CONTROL, levels); + + // Set ZONE logging. + levels_val = conf_id_get(conf, C_LOG, C_ZONE, &id); + levels = conf_opt(&levels_val); + sink_levels_add(log, target, LOG_SOURCE_ZONE, levels); + + // Set ANY logging. + levels_val = conf_id_get(conf, C_LOG, C_ANY, &id); + levels = conf_opt(&levels_val); + sink_levels_add(log, target, LOG_SOURCE_ANY, levels); + } + + sink_publish(log); +} diff --git a/src/knot/common/log.h b/src/knot/common/log.h new file mode 100644 index 0000000..49a8375 --- /dev/null +++ b/src/knot/common/log.h @@ -0,0 +1,187 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/*! + * \brief Logging facility. + * + * Supported log levels/priorities: + * LOG_CRIT, LOG_ERR, LOG_WARNING, LOG_NOTICE, LOG_INFO, and LOG_DEBUG. + * + * \see syslog.h + */ + +#pragma once + +#include <assert.h> +#include <syslog.h> +#include <stdint.h> +#include <stdbool.h> + +#include "libknot/dname.h" +#include "knot/conf/conf.h" + +/*! \brief Format for timestamps in log files. */ +#define KNOT_LOG_TIME_FORMAT "%Y-%m-%dT%H:%M:%S%z" + +/*! \brief Logging targets. */ +typedef enum { + LOG_TARGET_SYSLOG = 0, /*!< System log. */ + LOG_TARGET_STDERR = 1, /*!< Standard error stream. */ + LOG_TARGET_STDOUT = 2, /*!< Standard output stream. */ + LOG_TARGET_FILE = 3 /*!< Generic logging to a file (unbuffered). */ +} log_target_t; + +/*! \brief Logging sources. */ +typedef enum { + LOG_SOURCE_SERVER = 0, /*!< Server module. */ + LOG_SOURCE_CONTROL = 1, /*!< Server control module. */ + LOG_SOURCE_ZONE = 2, /*!< Zone manipulation module. */ + LOG_SOURCE_ANY = 3 /*!< Any module. */ +} log_source_t; + +/*! \brief Logging format flags. */ +typedef enum { + LOG_FLAG_NOTIMESTAMP = 1 << 0, /*!< Don't print timestamp prefix. */ + LOG_FLAG_NOINFO = 1 << 1 /*!< Don't print info level prefix. */ +} log_flag_t; + +/*! + * \brief Setup logging subsystem. + */ +void log_init(void); + +/*! + * \brief Close and deinitialize log. + */ +void log_close(void); + +/*! + * \brief Set logging format flag. + */ +void log_flag_set(log_flag_t flag); + +/*! + * \brief Set log levels for given target. + * + * \param target Logging target index (LOG_TARGET_SYSLOG...). + * \param src Logging source (LOG_SOURCE_SERVER...LOG_SOURCE_ANY). + * \param levels Bitmask of specified log levels. + */ +void log_levels_set(log_target_t target, log_source_t src, int levels); + +/*! + * \brief Add log levels to a given target. + * + * New levels are added on top of existing, the resulting levels set is + * "old_levels OR new_levels". + * + * \param target Logging target index (LOG_TARGET_SYSLOG...). + * \param src Logging source (LOG_SOURCE_SERVER...LOG_SOURCE_ANY). + * \param levels Bitmask of specified log levels. + */ +void log_levels_add(log_target_t target, log_source_t src, int levels); + +/*! + * \brief Log message into server category. + * + * Function follows printf() format. + * + * \note LOG_SOURCE_ANY is not a valid value for the src parameter. + * + * \param priority Message priority. + * \param src Message source (LOG_SOURCE_SERVER...LOG_SOURCE_ZONE). + * \param fmt Content of the logged message. + */ +void log_fmt(int priority, log_source_t src, const char *fmt, ...) +__attribute__((format(printf, 3, 4))); + +/*! + * \brief Log message into zone category. + * + * \see log_fmt + * + * \param priority Message priority. + * \param src Message source (LOG_SOURCE_SERVER...LOG_SOURCE_ZONE). + * \param zone Zone name in wire format. + * \param param Optional key-value parameter for structured logging. + * \param fmt Content of the logged message. + */ +void log_fmt_zone(int priority, log_source_t src, const knot_dname_t *zone, + const char *param, const char *fmt, ...) +__attribute__((format(printf, 5, 6))); + +/*! + * \brief Log message into zone category. + * + * \see log_fmt + * + * \param zone Zone name as an ASCII string. + * \param priority Message priority. + * \param src Message source (LOG_SOURCE_SERVER...LOG_SOURCE_ZONE). + * \param fmt Content of the logged message. + */ +void log_fmt_zone_str(int priority, log_source_t src, const char *zone, const char *fmt, ...) +__attribute__((format(printf, 4, 5))); + +/*! + * \brief Convenient logging macros. + */ +#define log_fatal(msg, ...) log_fmt(LOG_CRIT, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) +#define log_error(msg, ...) log_fmt(LOG_ERR, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) +#define log_warning(msg, ...) log_fmt(LOG_WARNING, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) +#define log_notice(msg, ...) log_fmt(LOG_NOTICE, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) +#define log_info(msg, ...) log_fmt(LOG_INFO, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) +#define log_debug(msg, ...) log_fmt(LOG_DEBUG, LOG_SOURCE_SERVER, msg, ##__VA_ARGS__) + +#define log_ctl_fatal(msg, ...) log_fmt(LOG_CRIT, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) +#define log_ctl_error(msg, ...) log_fmt(LOG_ERR, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) +#define log_ctl_warning(msg, ...) log_fmt(LOG_WARNING, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) +#define log_ctl_notice(msg, ...) log_fmt(LOG_NOTICE, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) +#define log_ctl_info(msg, ...) log_fmt(LOG_INFO, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) +#define log_ctl_debug(msg, ...) log_fmt(LOG_DEBUG, LOG_SOURCE_CONTROL, msg, ##__VA_ARGS__) + +#define log_ctl_zone_str_error(zone, msg, ...) log_fmt_zone_str(LOG_ERR, LOG_SOURCE_CONTROL, zone, msg, ##__VA_ARGS__) +#define log_ctl_zone_str_info(zone, msg, ...) log_fmt_zone_str(LOG_INFO, LOG_SOURCE_CONTROL, zone, msg, ##__VA_ARGS__) +#define log_ctl_zone_str_debug(zone, msg, ...) log_fmt_zone_str(LOG_DEBUG, LOG_SOURCE_CONTROL, zone, msg, ##__VA_ARGS__) + +#define log_zone_fatal(zone, msg, ...) log_fmt_zone(LOG_CRIT, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) +#define log_zone_error(zone, msg, ...) log_fmt_zone(LOG_ERR, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) +#define log_zone_warning(zone, msg, ...) log_fmt_zone(LOG_WARNING, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) +#define log_zone_notice(zone, msg, ...) log_fmt_zone(LOG_NOTICE, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) +#define log_zone_info(zone, msg, ...) log_fmt_zone(LOG_INFO, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) +#define log_zone_debug(zone, msg, ...) log_fmt_zone(LOG_DEBUG, LOG_SOURCE_ZONE, zone, NULL, msg, ##__VA_ARGS__) + +#define log_zone_str_fatal(zone, msg, ...) log_fmt_zone_str(LOG_CRIT, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) +#define log_zone_str_error(zone, msg, ...) log_fmt_zone_str(LOG_ERR, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) +#define log_zone_str_warning(zone, msg, ...) log_fmt_zone_str(LOG_WARNING, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) +#define log_zone_str_notice(zone, msg, ...) log_fmt_zone_str(LOG_NOTICE, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) +#define log_zone_str_info(zone, msg, ...) log_fmt_zone_str(LOG_INFO, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) +#define log_zone_str_debug(zone, msg, ...) log_fmt_zone_str(LOG_DEBUG, LOG_SOURCE_ZONE, zone, msg, ##__VA_ARGS__) + +/*! + * \brief Update open files ownership. + * + * \param uid New owner id. + * \param gid New group id. + * + * \return Error code, KNOT_EOK if success. + */ +int log_update_privileges(int uid, int gid); + +/*! + * \brief Setup logging facilities from config. + */ +void log_reconfigure(conf_t *conf); diff --git a/src/knot/common/process.c b/src/knot/common/process.c new file mode 100644 index 0000000..bdec1d4 --- /dev/null +++ b/src/knot/common/process.c @@ -0,0 +1,194 @@ +/* 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 <errno.h> +#include <fcntl.h> +#include <grp.h> +#include <pwd.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#include "knot/common/log.h" +#include "knot/common/process.h" +#include "knot/conf/conf.h" +#include "libknot/errcode.h" + +static char* pid_filename(void) +{ + conf_val_t val = conf_get(conf(), C_SRV, C_RUNDIR); + char *rundir = conf_abs_path(&val, NULL); + val = conf_get(conf(), C_SRV, C_PIDFILE); + char *pidfile = conf_abs_path(&val, rundir); + free(rundir); + + return pidfile; +} + +static pid_t pid_read(const char *filename) +{ + if (filename == NULL) { + return 0; + } + + size_t len = 0; + char buf[64] = { 0 }; + + FILE *fp = fopen(filename, "r"); + if (fp == NULL) { + return 0; + } + + /* Read the content of the file. */ + len = fread(buf, 1, sizeof(buf) - 1, fp); + fclose(fp); + if (len < 1) { + return 0; + } + + /* Convert pid. */ + errno = 0; + char *end = 0; + unsigned long pid = strtoul(buf, &end, 10); + if (end == buf || *end != '\0'|| errno != 0) { + return 0; + } + + return (pid_t)pid; +} + +static int pid_write(const char *filename, pid_t pid) +{ + if (filename == NULL) { + return KNOT_EINVAL; + } + + /* Convert. */ + char buf[64]; + int len = 0; + len = snprintf(buf, sizeof(buf), "%lu", (unsigned long)pid); + if (len < 0 || len >= sizeof(buf)) { + return KNOT_ENOMEM; + } + + /* Create file. */ + int ret = KNOT_EOK; + int fd = open(filename, O_RDWR|O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP); + if (fd >= 0) { + if (write(fd, buf, len) != len) { + ret = knot_map_errno(); + } + close(fd); + } else { + ret = knot_map_errno(); + } + + return ret; +} + +unsigned long pid_check_and_create(void) +{ + struct stat st; + char *pidfile = pid_filename(); + pid_t pid = pid_read(pidfile); + + /* Check PID for existence and liveness. */ + if (pid > 0 && pid_running(pid)) { + log_fatal("server PID found, already running"); + free(pidfile); + return 0; + } else if (stat(pidfile, &st) == 0) { + assert(pidfile); + log_warning("removing stale PID file '%s'", pidfile); + pid_cleanup(); + } + + /* Get current PID. */ + pid = getpid(); + + /* Create a PID file. */ + int ret = pid_write(pidfile, pid); + if (ret != KNOT_EOK) { + log_fatal("failed to create a PID file '%s' (%s)", pidfile, + knot_strerror(ret)); + free(pidfile); + return 0; + } + free(pidfile); + + return (unsigned long)pid; +} + +void pid_cleanup(void) +{ + char *pidfile = pid_filename(); + if (pidfile != NULL) { + (void)unlink(pidfile); + free(pidfile); + } +} + +bool pid_running(pid_t pid) +{ + return kill(pid, 0) == 0; +} + +int proc_update_privileges(int uid, int gid) +{ +#ifdef HAVE_SETGROUPS + /* Drop supplementary groups. */ + if ((uid_t)uid != getuid() || (gid_t)gid != getgid()) { + if (setgroups(0, NULL) < 0) { + log_warning("failed to drop supplementary groups for " + "UID %d (%s)", getuid(), strerror(errno)); + } +# ifdef HAVE_INITGROUPS + struct passwd *pw; + if ((pw = getpwuid(uid)) == NULL) { + log_warning("failed to get passwd entry for UID %d (%s)", + uid, strerror(errno)); + } else { + if (initgroups(pw->pw_name, gid) < 0) { + log_warning("failed to set supplementary groups " + "for UID %d (%s)", uid, strerror(errno)); + } + } +# endif /* HAVE_INITGROUPS */ + } +#endif /* HAVE_SETGROUPS */ + + /* Watch uid/gid. */ + if ((gid_t)gid != getgid()) { + log_info("changing GID to %d", gid); + if (setregid(gid, gid) < 0) { + log_error("failed to change GID to %d", gid); + return KNOT_ERROR; + } + } + if ((uid_t)uid != getuid()) { + log_info("changing UID to %d", uid); + if (setreuid(uid, uid) < 0) { + log_error("failed to change UID to %d", uid); + return KNOT_ERROR; + } + } + + return KNOT_EOK; +} diff --git a/src/knot/common/process.h b/src/knot/common/process.h new file mode 100644 index 0000000..14ca34e --- /dev/null +++ b/src/knot/common/process.h @@ -0,0 +1,60 @@ +/* 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/>. + */ + +/*! + * \brief Functions for POSIX process handling. + */ + +#pragma once + +#include <stdbool.h> +#include <unistd.h> + +/*! + * \brief Check if PID file exists and create it if possible. + * + * \retval 0 if failed. + * \retval Current PID. + */ +unsigned long pid_check_and_create(void); + +/*! + * \brief Remove PID file. + * + * \warning PID file content won't be checked. + */ +void pid_cleanup(void); + +/*! + * \brief Return true if the PID is running. + * + * \param pid Process ID. + * + * \retval 1 if running. + * \retval 0 if not running (or error). + */ +bool pid_running(pid_t pid); + +/*! + * \brief Update process privileges to new UID/GID. + * + * \param uid New user ID. + * \param gid New group ID. + * + * \retval KNOT_EOK on success. + * \retval KNOT_ERROR if UID or GID change failed. + */ +int proc_update_privileges(int uid, int gid); diff --git a/src/knot/common/stats.c b/src/knot/common/stats.c new file mode 100644 index 0000000..2b8cb09 --- /dev/null +++ b/src/knot/common/stats.c @@ -0,0 +1,309 @@ +/* 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 <inttypes.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> +#include <urcu.h> + +#include "contrib/files.h" +#include "knot/common/stats.h" +#include "knot/common/log.h" +#include "knot/nameserver/query_module.h" + +struct { + bool active_dumper; + pthread_t dumper; + uint32_t timer; + server_t *server; +} stats = { 0 }; + +typedef struct { + FILE *fd; + const list_t *query_modules; + const knot_dname_t *zone; + bool zone_emitted; +} dump_ctx_t; + +#define DUMP_STR(fd, level, name, ...) do { \ + fprintf(fd, "%-.*s"name": %s\n", level, " ", ##__VA_ARGS__); \ + } while (0) +#define DUMP_CTR(fd, level, name, ...) do { \ + fprintf(fd, "%-.*s"name": %"PRIu64"\n", level, " ", ##__VA_ARGS__); \ + } while (0) + +uint64_t server_zone_count(server_t *server) +{ + return knot_zonedb_size(server->zone_db); +} + +const stats_item_t server_stats[] = { + { "zone-count", server_zone_count }, + { 0 } +}; + +uint64_t stats_get_counter(uint64_t **stats_vals, uint32_t offset, unsigned threads) +{ + uint64_t res = 0; + for (unsigned i = 0; i < threads; i++) { + res += ATOMIC_GET(stats_vals[i][offset]); + } + return res; +} + +static void dump_counters(FILE *fd, int level, mod_ctr_t *ctr, uint64_t **stats_vals, unsigned threads) +{ + for (uint32_t j = 0; j < ctr->count; j++) { + uint64_t counter = stats_get_counter(stats_vals, ctr->offset + j, threads); + + // Skip empty counters. + if (counter == 0) { + continue; + } + + if (ctr->idx_to_str != NULL) { + char *str = ctr->idx_to_str(j, ctr->count); + if (str != NULL) { + DUMP_CTR(fd, level, "%s", str, counter); + free(str); + } + } else { + DUMP_CTR(fd, level, "%u", j, counter); + } + } +} + +static void dump_modules(dump_ctx_t *ctx) +{ + int level = 0; + knotd_mod_t *mod; + WALK_LIST(mod, *ctx->query_modules) { + // Skip modules without statistics. + if (mod->stats_count == 0) { + continue; + } + + // Dump zone name. + if (ctx->zone != NULL) { + // Prevent from zone section override. + if (!ctx->zone_emitted) { + DUMP_STR(ctx->fd, 0, "zone", ""); + ctx->zone_emitted = true; + } + level = 1; + + knot_dname_txt_storage_t name; + if (knot_dname_to_str(name, ctx->zone, sizeof(name)) == NULL) { + return; + } + DUMP_STR(ctx->fd, level++, "\"%s\"", name, ""); + } else { + level = 0; + } + + unsigned threads = knotd_mod_threads(mod); + + // Dump module counters. + DUMP_STR(ctx->fd, level, "%s", mod->id->name + 1, ""); + for (int i = 0; i < mod->stats_count; i++) { + mod_ctr_t *ctr = mod->stats_info + i; + if (ctr->name == NULL) { + // Empty counter. + continue; + } + if (ctr->count == 1) { + // Simple counter. + uint64_t counter = stats_get_counter(mod->stats_vals, + ctr->offset, threads); + DUMP_CTR(ctx->fd, level + 1, "%s", ctr->name, counter); + } else { + // Array of counters. + DUMP_STR(ctx->fd, level + 1, "%s", ctr->name, ""); + dump_counters(ctx->fd, level + 2, ctr, mod->stats_vals, threads); + } + } + } +} + +static void zone_stats_dump(zone_t *zone, dump_ctx_t *ctx) +{ + if (EMPTY_LIST(zone->query_modules)) { + return; + } + + ctx->query_modules = &zone->query_modules; + ctx->zone = zone->name; + + dump_modules(ctx); +} + +static void dump_to_file(FILE *fd, server_t *server) +{ + char date[64] = ""; + + // Get formatted current time string. + struct tm tm; + time_t now = time(NULL); + localtime_r(&now, &tm); + strftime(date, sizeof(date), KNOT_LOG_TIME_FORMAT, &tm); + + // Get the server identity. + conf_val_t val = conf_get(conf(), C_SRV, C_IDENT); + const char *ident = conf_str(&val); + if (ident == NULL || ident[0] == '\0') { + ident = conf()->hostname; + } + + // Dump record header. + fprintf(fd, + "---\n" + "time: %s\n" + "identity: %s\n", + date, ident); + + // Dump server statistics. + DUMP_STR(fd, 0, "server", ""); + for (const stats_item_t *item = server_stats; item->name != NULL; item++) { + DUMP_CTR(fd, 1, "%s", item->name, item->val(server)); + } + + dump_ctx_t ctx = { + .fd = fd, + .query_modules = conf()->query_modules, + }; + + // Dump global statistics. + dump_modules(&ctx); + + // Dump zone statistics. + knot_zonedb_foreach(server->zone_db, zone_stats_dump, &ctx); +} + +static void dump_stats(server_t *server) +{ + conf_t *pconf = conf(); + conf_val_t val = conf_get(pconf, C_SRV, C_RUNDIR); + char *rundir = conf_abs_path(&val, NULL); + val = conf_get(pconf, C_STATS, C_FILE); + char *file_name = conf_abs_path(&val, rundir); + free(rundir); + + val = conf_get(pconf, C_STATS, C_APPEND); + bool append = conf_bool(&val); + + // Open or create output file. + FILE *fd = NULL; + char *tmp_name = NULL; + if (append) { + fd = fopen(file_name, "a"); + if (fd == NULL) { + log_error("stats, failed to append file '%s' (%s)", + file_name, knot_strerror(knot_map_errno())); + free(file_name); + return; + } + } else { + int ret = open_tmp_file(file_name, &tmp_name, &fd, + S_IRUSR | S_IWUSR | S_IRGRP); + if (ret != KNOT_EOK) { + log_error("stats, failed to open file '%s' (%s)", + file_name, knot_strerror(ret)); + free(file_name); + return; + } + } + assert(fd); + + // Dump stats into the file. + dump_to_file(fd, server); + + fflush(fd); + fclose(fd); + + // Switch the file contents. + if (!append) { + int ret = rename(tmp_name, file_name); + if (ret != 0) { + log_error("stats, failed to access file '%s' (%s)", + file_name, knot_strerror(knot_map_errno())); + unlink(tmp_name); + } + free(tmp_name); + } + + log_debug("stats, dumped into file '%s'", file_name); + free(file_name); +} + +static void *dumper(void *data) +{ + rcu_register_thread(); + while (true) { + assert(stats.timer > 0); + sleep(stats.timer); + + pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); + rcu_read_lock(); + dump_stats(stats.server); + rcu_read_unlock(); + pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); + } + rcu_unregister_thread(); + return NULL; +} + +void stats_reconfigure(conf_t *conf, server_t *server) +{ + if (conf == NULL || server == NULL) { + return; + } + + // Update server context. + stats.server = server; + + conf_val_t val = conf_get(conf, C_STATS, C_TIMER); + stats.timer = conf_int(&val); + if (stats.timer > 0) { + // Check if dumping is already running. + if (stats.active_dumper) { + return; + } + + int ret = pthread_create(&stats.dumper, NULL, dumper, NULL); + if (ret != 0) { + log_error("stats, failed to launch periodic dumping (%s)", + knot_strerror(knot_map_errno_code(ret))); + } else { + stats.active_dumper = true; + } + // Stop current dumping. + } else if (stats.active_dumper) { + pthread_cancel(stats.dumper); + pthread_join(stats.dumper, NULL); + stats.active_dumper = false; + } +} + +void stats_deinit(void) +{ + if (stats.active_dumper) { + pthread_cancel(stats.dumper); + pthread_join(stats.dumper, NULL); + } + + memset(&stats, 0, sizeof(stats)); +} diff --git a/src/knot/common/stats.h b/src/knot/common/stats.h new file mode 100644 index 0000000..bd6df6d --- /dev/null +++ b/src/knot/common/stats.h @@ -0,0 +1,53 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +/*! + * \brief Server statistics general API. + */ + +#pragma once + +#include "knot/server/server.h" + +typedef uint64_t (*stats_val_f)(server_t *server); + +/*! + * \brief Statistics metrics item. + */ +typedef struct { + const char *name; /*!< Metrics name. */ + stats_val_f val; /*!< Metrics value getter. */ +} stats_item_t; + +/*! + * \brief Basic server metrics. + */ +extern const stats_item_t server_stats[]; + +/*! + * \brief Read out value of single counter summed across threads. + */ +uint64_t stats_get_counter(uint64_t **stats_vals, uint32_t offset, unsigned threads); + +/*! + * \brief Reconfigures the statistics facility. + */ +void stats_reconfigure(conf_t *conf, server_t *server); + +/*! + * \brief Deinitializes the statistics facility. + */ +void stats_deinit(void); diff --git a/src/knot/common/systemd.c b/src/knot/common/systemd.c new file mode 100644 index 0000000..13c83e6 --- /dev/null +++ b/src/knot/common/systemd.c @@ -0,0 +1,168 @@ +/* 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 <stdarg.h> +#include <stdio.h> +#include <stdlib.h> + +#include "knot/common/systemd.h" +#include "contrib/strtonum.h" + +#ifdef ENABLE_SYSTEMD +#include <systemd/sd-daemon.h> + +#define ZONE_LOAD_TIMEOUT_DEFAULT 60 + +static int zone_load_timeout_s; + +static int systemd_zone_load_timeout(void) +{ + const char *timeout = getenv("KNOT_ZONE_LOAD_TIMEOUT_SEC"); + + int out; + if (timeout != NULL && timeout[0] != '\0' && + str_to_int(timeout, &out, 0, 24 * 3600) == KNOT_EOK) { + return out; + } else { + return ZONE_LOAD_TIMEOUT_DEFAULT; + } +} +#endif + +#ifdef ENABLE_DBUS +#include <systemd/sd-bus.h> + +static sd_bus *_dbus = NULL; +#endif + +void systemd_zone_load_timeout_notify(void) +{ +#ifdef ENABLE_SYSTEMD + if (zone_load_timeout_s == 0) { + zone_load_timeout_s = systemd_zone_load_timeout(); + } + sd_notifyf(0, "EXTEND_TIMEOUT_USEC=%d000000", zone_load_timeout_s); +#endif +} + +void systemd_tasks_status_notify(int tasks) +{ +#ifdef ENABLE_SYSTEMD + if (tasks > 0) { + sd_notifyf(0, "STATUS=Waiting for %d tasks to finish...", tasks); + } else { + sd_notify(0, "STATUS="); + } +#endif +} + +void systemd_ready_notify(void) +{ +#ifdef ENABLE_SYSTEMD + sd_notify(0, "READY=1\nSTATUS="); +#endif +} + +void systemd_reloading_notify(void) +{ +#ifdef ENABLE_SYSTEMD + sd_notify(0, "RELOADING=1\nSTATUS="); +#endif +} + +void systemd_stopping_notify(void) +{ +#ifdef ENABLE_SYSTEMD + sd_notify(0, "STOPPING=1\nSTATUS="); +#endif +} + +int systemd_dbus_open(void) +{ +#ifdef ENABLE_DBUS + if (_dbus != NULL) { + return KNOT_EOK; + } + + int ret = sd_bus_open_system(&_dbus); + if (ret < 0) { + return ret; + } + + /* Take a well-known service name so that clients can find us. */ + ret = sd_bus_request_name(_dbus, KNOT_DBUS_NAME, 0); + if (ret < 0) { + systemd_dbus_close(); + return ret; + } + + return KNOT_EOK; +#else + return KNOT_ENOTSUP; +#endif +} + +void systemd_dbus_close(void) +{ +#ifdef ENABLE_DBUS + _dbus = sd_bus_unref(_dbus); +#endif +} + +#define emit_event(event, ...) \ + sd_bus_emit_signal(_dbus, KNOT_DBUS_PATH, KNOT_DBUS_NAME".events", \ + event, __VA_ARGS__) + +void systemd_emit_running(bool up) +{ +#ifdef ENABLE_DBUS + emit_event(up ? KNOT_BUS_EVENT_STARTED : KNOT_BUS_EVENT_STOPPED, ""); +#endif +} + +void systemd_emit_zone_updated(const knot_dname_t *zone_name, uint32_t serial) +{ +#ifdef ENABLE_DBUS + knot_dname_txt_storage_t buff; + char *zone_str = knot_dname_to_str(buff, zone_name, sizeof(buff)); + if (zone_str != NULL) { + emit_event(KNOT_BUS_EVENT_ZONE_UPD, "su", zone_str, serial); + } +#endif +} + +void systemd_emit_zone_submission(const knot_dname_t *zone_name, uint16_t keytag, + const char *keyid) +{ +#ifdef ENABLE_DBUS + knot_dname_txt_storage_t buff; + char *zone_str = knot_dname_to_str(buff, zone_name, sizeof(buff)); + if (zone_str != NULL) { + emit_event(KNOT_BUS_EVENT_ZONE_KSK_SUBM, "sqs", zone_str, keytag, keyid); + } +#endif +} + +void systemd_emit_zone_invalid(const knot_dname_t *zone_name) +{ +#ifdef ENABLE_DBUS + knot_dname_txt_storage_t buff; + char *zone_str = knot_dname_to_str(buff, zone_name, sizeof(buff)); + if (zone_str != NULL) { + emit_event(KNOT_BUS_EVENT_ZONE_INVALID, "s", zone_str); + } +#endif +} diff --git a/src/knot/common/systemd.h b/src/knot/common/systemd.h new file mode 100644 index 0000000..1cefd9c --- /dev/null +++ b/src/knot/common/systemd.h @@ -0,0 +1,105 @@ +/* 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/>. + */ + +/*! + * \brief Systemd API wrappers. + */ + +#pragma once + +#include "libknot/libknot.h" + +#define KNOT_DBUS_NAME "cz.nic.knotd" +#define KNOT_DBUS_PATH "/cz/nic/knotd" + +#define KNOT_BUS_EVENT_STARTED "started" +#define KNOT_BUS_EVENT_STOPPED "stopped" +#define KNOT_BUS_EVENT_ZONE_UPD "zone_updated" +#define KNOT_BUS_EVENT_ZONE_KSK_SUBM "zone_ksk_submission" +#define KNOT_BUS_EVENT_ZONE_INVALID "zone_dnssec_invalid" + +/*! + * \brief Notify systemd about zone loading start. + */ +void systemd_zone_load_timeout_notify(void); + +/*! + * \brief Update systemd service status with information about number + * of scheduled tasks. + * + * \param tasks Number of tasks to be done. + */ +void systemd_tasks_status_notify(int tasks); + +/*! + * \brief Notify systemd about service is ready. + */ +void systemd_ready_notify(void); + +/*! + * \brief Notify systemd about service is reloading. + */ +void systemd_reloading_notify(void); + +/*! + * \brief Notify systemd about service is stopping. + */ +void systemd_stopping_notify(void); + +/*! + * \brief Creates unique D-Bus sender reference (common for whole process). + * + * \retval KNOT_EOK on successful create of reference. + * \retval Negative value on error. + */ +int systemd_dbus_open(void); + +/*! + * \brief Closes D-Bus. + */ +void systemd_dbus_close(void); + +/*! + * \brief Emit event signal for started daemon. + * + * \param up Indication if the server has been started. + */ +void systemd_emit_running(bool up); + +/*! + * \brief Emit event signal for updated zones. + * + * \param zone_name Zone name. + * \param serial Current zone SOA serial. + */ +void systemd_emit_zone_updated(const knot_dname_t *zone_name, uint32_t serial); + +/*! + * \brief Emit event signal for KSK submission. + * + * \param zone_name Zone name. + * \param keytag Keytag of the ready key. + * \param keyid KASP id of the ready key. + */ +void systemd_emit_zone_submission(const knot_dname_t *zone_name, uint16_t keytag, + const char *keyid); + +/*! + * \brief Emit event signal for failed DNSSEC validation. + * + * \param zone_name Zone name. + */ +void systemd_emit_zone_invalid(const knot_dname_t *zone_name); diff --git a/src/knot/common/unreachable.c b/src/knot/common/unreachable.c new file mode 100644 index 0000000..e137f3d --- /dev/null +++ b/src/knot/common/unreachable.c @@ -0,0 +1,148 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <stdlib.h> +#include <time.h> + +#include "unreachable.h" + +knot_unreachables_t *global_unreachables = NULL; + +static uint32_t get_timestamp(void) +{ + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + uint64_t res = (uint64_t)t.tv_sec * 1000; + res += (uint64_t)t.tv_nsec / 1000000; + return res & 0xffffffff; // overflow does not matter since we are working with differences +} + +knot_unreachables_t *knot_unreachables_init(uint32_t ttl_ms) +{ + knot_unreachables_t *res = calloc(1, sizeof(*res)); + if (res != NULL) { + pthread_mutex_init(&res->mutex, NULL); + res->ttl_ms = ttl_ms; + init_list(&res->urs); + } + return res; +} + +uint32_t knot_unreachables_ttl(knot_unreachables_t *urs, uint32_t new_ttl_ms) +{ + if (urs == NULL) { + return 0; + } + + pthread_mutex_lock(&urs->mutex); + + uint32_t prev = urs->ttl_ms; + urs->ttl_ms = new_ttl_ms; + + pthread_mutex_unlock(&urs->mutex); + + return prev; +} + +void knot_unreachables_deinit(knot_unreachables_t **urs) +{ + if (urs != NULL && *urs != NULL) { + knot_unreachable_t *ur, *nxt; + WALK_LIST_DELSAFE(ur, nxt, (*urs)->urs) { + rem_node((node_t *)ur); + free(ur); + } + pthread_mutex_destroy(&(*urs)->mutex); + free(*urs); + *urs = NULL; + } +} + +static bool clear_old(knot_unreachable_t *ur, uint32_t now, uint32_t ttl_ms) +{ + if (ur->time_ms != 0 && now - ur->time_ms > ttl_ms) { + rem_node((node_t *)ur); + free(ur); + return true; + } + return false; +} + +// also clears up (some) expired unreachables +static knot_unreachable_t *get_ur(knot_unreachables_t *urs, + const struct sockaddr_storage *addr, + const struct sockaddr_storage *via) +{ + assert(urs != NULL); + + uint32_t now = get_timestamp(); + knot_unreachable_t *ur, *nxt; + WALK_LIST_DELSAFE(ur, nxt, urs->urs) { + if (clear_old(ur, now, urs->ttl_ms)) { + continue; + } + + if (sockaddr_cmp(&ur->addr, addr, false) == 0 && + sockaddr_cmp(&ur->via, via, true) == 0) { + return ur; + } + } + + return NULL; +} + +bool knot_unreachable_is(knot_unreachables_t *urs, + const struct sockaddr_storage *addr, + const struct sockaddr_storage *via) +{ + if (urs == NULL) { + return false; + } + assert(addr); + assert(via); + + pthread_mutex_lock(&urs->mutex); + + bool res = (get_ur(urs, addr, via) != NULL); + + pthread_mutex_unlock(&urs->mutex); + + return res; +} + +void knot_unreachable_add(knot_unreachables_t *urs, + const struct sockaddr_storage *addr, + const struct sockaddr_storage *via) +{ + if (urs == NULL) { + return; + } + assert(addr); + assert(via); + + pthread_mutex_lock(&urs->mutex); + + knot_unreachable_t *ur = malloc(sizeof(*ur)); + if (ur != NULL) { + memcpy(&ur->addr, addr, sizeof(ur->addr)); + memcpy(&ur->via, via, sizeof(ur->via)); + ur->time_ms = get_timestamp(); + add_head(&urs->urs, (node_t *)ur); + } + + pthread_mutex_unlock(&urs->mutex); +} diff --git a/src/knot/common/unreachable.h b/src/knot/common/unreachable.h new file mode 100644 index 0000000..40094f9 --- /dev/null +++ b/src/knot/common/unreachable.h @@ -0,0 +1,87 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pthread.h> +#include <stdbool.h> +#include <stdint.h> + +#include "contrib/sockaddr.h" +#include "contrib/ucw/lists.h" + +typedef struct { + node_t n; + struct sockaddr_storage addr; + struct sockaddr_storage via; + uint32_t time_ms; +} knot_unreachable_t; + +typedef struct { + pthread_mutex_t mutex; + uint32_t ttl_ms; + list_t urs; +} knot_unreachables_t; + +extern knot_unreachables_t *global_unreachables; + +/*! + * \brief Allocate Unreachables structure. + * + * \param ttl TTL for unreachable in milliseconds. + * + * \return Allocated structure, or NULL. + */ +knot_unreachables_t *knot_unreachables_init(uint32_t ttl_ms); + +/*! + * \brief Free Unreachables structure. + */ +void knot_unreachables_deinit(knot_unreachables_t **urs); + +/*! + * \brief Get and/or set the TTL. + * + * \param urs Unreachables structure. + * \param new_ttl_ms New TTL value in milliseconds. + * + * \return Previous value of TTL. + */ +uint32_t knot_unreachables_ttl(knot_unreachables_t *urs, uint32_t new_ttl_ms); + +/*! + * \brief Determine if given address is unreachable. + * + * \param urs Unreachables structure. + * \param addr Address and port in question. + * \param via Local outgoing address. + * + * \return True iff unreachable within TTL. + */ +bool knot_unreachable_is(knot_unreachables_t *urs, + const struct sockaddr_storage *addr, + const struct sockaddr_storage *via); + +/*! + * \brief Add an unreachable into Unreachables structure. + * + * \param urs Unreachables structure. + * \param addr Address and port being unreachable. + * \param via Local outgoing address. + */ +void knot_unreachable_add(knot_unreachables_t *urs, + const struct sockaddr_storage *addr, + const struct sockaddr_storage *via); diff --git a/src/knot/conf/base.c b/src/knot/conf/base.c new file mode 100644 index 0000000..1670929 --- /dev/null +++ b/src/knot/conf/base.c @@ -0,0 +1,1056 @@ +/* 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 <string.h> +#include <urcu.h> + +#include "knot/conf/base.h" +#include "knot/conf/confdb.h" +#include "knot/conf/module.h" +#include "knot/conf/tools.h" +#include "knot/common/log.h" +#include "knot/nameserver/query_module.h" +#include "libknot/libknot.h" +#include "libknot/yparser/ypformat.h" +#include "libknot/yparser/yptrafo.h" +#include "contrib/files.h" +#include "contrib/sockaddr.h" +#include "contrib/string.h" + +// The active configuration. +conf_t *s_conf; + +conf_t* conf(void) { + return s_conf; +} + +static int init_and_check( + conf_t *conf, + conf_flag_t flags) +{ + if (conf == NULL) { + return KNOT_EINVAL; + } + + knot_db_txn_t txn; + unsigned txn_flags = (flags & CONF_FREADONLY) ? KNOT_DB_RDONLY : 0; + int ret = conf->api->txn_begin(conf->db, &txn, txn_flags); + if (ret != KNOT_EOK) { + return ret; + } + + // Initialize the database. + if (!(flags & CONF_FREADONLY)) { + ret = conf_db_init(conf, &txn, false); + if (ret != KNOT_EOK) { + conf->api->txn_abort(&txn); + return ret; + } + } + + // Check the database. + if (!(flags & CONF_FNOCHECK)) { + ret = conf_db_check(conf, &txn); + if (ret < KNOT_EOK) { + conf->api->txn_abort(&txn); + return ret; + } + } + + if (flags & CONF_FREADONLY) { + conf->api->txn_abort(&txn); + return KNOT_EOK; + } else { + return conf->api->txn_commit(&txn); + } +} + +int conf_refresh_txn( + conf_t *conf) +{ + if (conf == NULL) { + return KNOT_EINVAL; + } + + // Close previously opened transaction. + conf->api->txn_abort(&conf->read_txn); + + return conf->api->txn_begin(conf->db, &conf->read_txn, KNOT_DB_RDONLY); +} + +static void refresh_hostname( + conf_t *conf) +{ + if (conf == NULL) { + return; + } + + free(conf->hostname); + conf->hostname = sockaddr_hostname(); + if (conf->hostname == NULL) { + // Empty hostname fallback, NULL cannot be passed to strlen! + conf->hostname = strdup(""); + } +} + +static int infinite_adjust( + int timeout) +{ + return (timeout > 0) ? timeout : -1; +} + +static void init_cache( + conf_t *conf, + bool reinit_cache) +{ + /* + * For UDP, TCP, XDP, and background workers, cache the number of running + * workers. Cache the setting of TCP reuseport too. These values + * can't change in runtime, while config data can. + */ + + static bool first_init = true; + static bool running_tcp_reuseport; + static bool running_socket_affinity; + static bool running_xdp_udp; + static bool running_xdp_tcp; + static uint16_t running_xdp_quic; + static bool running_route_check; + static size_t running_udp_threads; + static size_t running_tcp_threads; + static size_t running_xdp_threads; + static size_t running_bg_threads; + static size_t running_quic_clients; + static size_t running_quic_outbufs; + static size_t running_quic_idle; + + if (first_init || reinit_cache) { + running_tcp_reuseport = conf_get_bool(conf, C_SRV, C_TCP_REUSEPORT); + running_socket_affinity = conf_get_bool(conf, C_SRV, C_SOCKET_AFFINITY); + running_xdp_udp = conf_get_bool(conf, C_XDP, C_UDP); + running_xdp_tcp = conf_get_bool(conf, C_XDP, C_TCP); + running_xdp_quic = 0; + if (conf_get_bool(conf, C_XDP, C_QUIC)) { + running_xdp_quic = conf_get_int(conf, C_XDP, C_QUIC_PORT); + } + running_route_check = conf_get_bool(conf, C_XDP, C_ROUTE_CHECK); + running_udp_threads = conf_udp_threads(conf); + running_tcp_threads = conf_tcp_threads(conf); + running_xdp_threads = conf_xdp_threads(conf); + running_bg_threads = conf_bg_threads(conf); + running_quic_clients = conf_get_int(conf, C_SRV, C_QUIC_MAX_CLIENTS); + running_quic_outbufs = conf_get_int(conf, C_SRV, C_QUIC_OUTBUF_MAX_SIZE); + running_quic_idle = conf_get_int(conf, C_SRV, C_QUIC_IDLE_CLOSE); + + first_init = false; + } + + conf_val_t val = conf_get(conf, C_SRV, C_UDP_MAX_PAYLOAD_IPV4); + if (val.code != KNOT_EOK) { + val = conf_get(conf, C_SRV, C_UDP_MAX_PAYLOAD); + } + conf->cache.srv_udp_max_payload_ipv4 = conf_int(&val); + + val = conf_get(conf, C_SRV, C_UDP_MAX_PAYLOAD_IPV6); + if (val.code != KNOT_EOK) { + val = conf_get(conf, C_SRV, C_UDP_MAX_PAYLOAD); + } + conf->cache.srv_udp_max_payload_ipv6 = conf_int(&val); + + val = conf_get(conf, C_SRV, C_TCP_IDLE_TIMEOUT); + conf->cache.srv_tcp_idle_timeout = conf_int(&val); + + val = conf_get(conf, C_SRV, C_TCP_IO_TIMEOUT); + conf->cache.srv_tcp_io_timeout = infinite_adjust(conf_int(&val)); + + val = conf_get(conf, C_SRV, C_TCP_RMT_IO_TIMEOUT); + conf->cache.srv_tcp_remote_io_timeout = infinite_adjust(conf_int(&val)); + + val = conf_get(conf, C_SRV, C_TCP_FASTOPEN); + conf->cache.srv_tcp_fastopen = conf_bool(&val); + + conf->cache.srv_quic_max_clients = running_quic_clients; + + conf->cache.srv_quic_idle_close = running_quic_idle; + + conf->cache.srv_quic_obuf_max_size = running_quic_outbufs; + + conf->cache.srv_tcp_reuseport = running_tcp_reuseport; + + conf->cache.srv_socket_affinity = running_socket_affinity; + + val = conf_get(conf, C_SRV, C_DBUS_EVENT); + while (val.code == KNOT_EOK) { + conf->cache.srv_dbus_event |= conf_opt(&val); + conf_val_next(&val); + } + + conf->cache.srv_udp_threads = running_udp_threads; + + conf->cache.srv_tcp_threads = running_tcp_threads; + + conf->cache.srv_xdp_threads = running_xdp_threads; + + conf->cache.srv_bg_threads = running_bg_threads; + + conf->cache.srv_tcp_max_clients = conf_tcp_max_clients(conf); + + val = conf_get(conf, C_XDP, C_TCP_MAX_CLIENTS); + conf->cache.xdp_tcp_max_clients = conf_int(&val); + + val = conf_get(conf, C_XDP, C_TCP_INBUF_MAX_SIZE); + conf->cache.xdp_tcp_inbuf_max_size = conf_int(&val); + + val = conf_get(conf, C_XDP, C_TCP_OUTBUF_MAX_SIZE); + conf->cache.xdp_tcp_outbuf_max_size = conf_int(&val); + + val = conf_get(conf, C_XDP, C_TCP_IDLE_CLOSE); + conf->cache.xdp_tcp_idle_close = conf_int(&val); + + val = conf_get(conf, C_XDP, C_TCP_IDLE_RESET); + conf->cache.xdp_tcp_idle_reset = conf_int(&val); + + val = conf_get(conf, C_XDP, C_TCP_RESEND); + conf->cache.xdp_tcp_idle_resend = conf_int(&val); + + conf->cache.xdp_udp = running_xdp_udp; + + conf->cache.xdp_tcp = running_xdp_tcp; + + conf->cache.xdp_quic = running_xdp_quic; + + conf->cache.xdp_route_check = running_route_check; + + val = conf_get(conf, C_CTL, C_TIMEOUT); + conf->cache.ctl_timeout = conf_int(&val) * 1000; + /* infinite_adjust() call isn't needed, 0 is adjusted later anyway. */ + + val = conf_get(conf, C_SRV, C_NSID); + if (val.code != KNOT_EOK) { + if (conf->hostname == NULL) { + conf->cache.srv_nsid_data = (const uint8_t *)""; + conf->cache.srv_nsid_len = 0; + } else { + conf->cache.srv_nsid_data = (const uint8_t *)conf->hostname; + conf->cache.srv_nsid_len = strlen(conf->hostname); + } + } else { + conf->cache.srv_nsid_data = conf_bin(&val, &conf->cache.srv_nsid_len); + } + + val = conf_get(conf, C_SRV, C_ECS); + conf->cache.srv_ecs = conf_bool(&val); + + val = conf_get(conf, C_SRV, C_ANS_ROTATION); + conf->cache.srv_ans_rotate = conf_bool(&val); + + val = conf_get(conf, C_SRV, C_AUTO_ACL); + conf->cache.srv_auto_acl = conf_bool(&val); + + val = conf_get(conf, C_SRV, C_PROXY_ALLOWLIST); + conf->cache.srv_proxy_enabled = (conf_val_count(&val) > 0); +} + +int conf_new( + conf_t **conf, + const yp_item_t *schema, + const char *db_dir, + size_t max_conf_size, + conf_flag_t flags) +{ + if (conf == NULL) { + return KNOT_EINVAL; + } + + conf_t *out = malloc(sizeof(conf_t)); + if (out == NULL) { + return KNOT_ENOMEM; + } + memset(out, 0, sizeof(conf_t)); + + // Initialize config schema. + int ret = yp_schema_copy(&out->schema, schema); + if (ret != KNOT_EOK) { + goto new_error; + } + + // Initialize query modules list. + out->query_modules = malloc(sizeof(list_t)); + if (out->query_modules == NULL) { + ret = KNOT_ENOMEM; + goto new_error; + } + init_list(out->query_modules); + + // Set the DB api. + out->mapsize = max_conf_size; + out->api = knot_db_lmdb_api(); + struct knot_db_lmdb_opts lmdb_opts = KNOT_DB_LMDB_OPTS_INITIALIZER; + lmdb_opts.mapsize = out->mapsize; + lmdb_opts.maxreaders = CONF_MAX_DB_READERS; + lmdb_opts.flags.env = KNOT_DB_LMDB_NOTLS; + + // Open the database. + if (db_dir == NULL) { + // Prepare a temporary database. + char tpl[] = "/tmp/knot-confdb.XXXXXX"; + lmdb_opts.path = mkdtemp(tpl); + if (lmdb_opts.path == NULL) { + CONF_LOG(LOG_ERR, "failed to create temporary directory (%s)", + knot_strerror(knot_map_errno())); + ret = KNOT_ENOMEM; + goto new_error; + } + + ret = out->api->init(&out->db, NULL, &lmdb_opts); + + // Remove the database to ensure it is temporary. + if (!remove_path(lmdb_opts.path)) { + CONF_LOG(LOG_WARNING, "failed to purge temporary directory '%s'", + lmdb_opts.path); + } + } else { + // Set the specified database. + lmdb_opts.path = db_dir; + + // Set the read-only mode. + if (flags & CONF_FREADONLY) { + lmdb_opts.flags.env |= KNOT_DB_LMDB_RDONLY; + } + + ret = out->api->init(&out->db, NULL, &lmdb_opts); + } + if (ret != KNOT_EOK) { + goto new_error; + } + + // Initialize and check the database. + ret = init_and_check(out, flags); + if (ret != KNOT_EOK) { + goto new_error; + } + + // Open common read-only transaction. + ret = conf_refresh_txn(out); + if (ret != KNOT_EOK) { + goto new_error; + } + + // Cache the current hostname. + if (!(flags & CONF_FNOHOSTNAME)) { + refresh_hostname(out); + } + + // Initialize cached values. + init_cache(out, false); + + // Load module schemas. + if (flags & (CONF_FREQMODULES | CONF_FOPTMODULES)) { + ret = conf_mod_load_common(out); + if (ret != KNOT_EOK && (flags & CONF_FREQMODULES)) { + goto new_error; + } + + for (conf_iter_t iter = conf_iter(out, C_MODULE); + iter.code == KNOT_EOK; conf_iter_next(out, &iter)) { + conf_val_t id = conf_iter_id(out, &iter); + conf_val_t file = conf_id_get(out, C_MODULE, C_FILE, &id); + ret = conf_mod_load_extra(out, conf_str(&id), conf_str(&file), + MOD_EXPLICIT); + if (ret != KNOT_EOK && (flags & CONF_FREQMODULES)) { + conf_iter_finish(out, &iter); + goto new_error; + } + } + + conf_mod_load_purge(out, false); + } + + *conf = out; + + return KNOT_EOK; +new_error: + conf_free(out); + + return ret; +} + +int conf_clone( + conf_t **conf) +{ + if (conf == NULL || s_conf == NULL) { + return KNOT_EINVAL; + } + + conf_t *out = malloc(sizeof(conf_t)); + if (out == NULL) { + return KNOT_ENOMEM; + } + memset(out, 0, sizeof(conf_t)); + + // Initialize config schema. + int ret = yp_schema_copy(&out->schema, s_conf->schema); + if (ret != KNOT_EOK) { + free(out); + return ret; + } + + // Set shared items. + out->api = s_conf->api; + out->db = s_conf->db; + + // Initialize query modules list. + out->query_modules = malloc(sizeof(list_t)); + if (out->query_modules == NULL) { + yp_schema_free(out->schema); + free(out); + return KNOT_ENOMEM; + } + init_list(out->query_modules); + + // Open common read-only transaction. + ret = conf_refresh_txn(out); + if (ret != KNOT_EOK) { + free(out->query_modules); + yp_schema_free(out->schema); + free(out); + return ret; + } + + // Copy the filename. + if (s_conf->filename != NULL) { + out->filename = strdup(s_conf->filename); + } + + // Copy the hostname. + if (s_conf->hostname != NULL) { + out->hostname = strdup(s_conf->hostname); + } + + out->catalog = s_conf->catalog; + + // Initialize cached values. + init_cache(out, false); + + out->is_clone = true; + + *conf = out; + + return KNOT_EOK; +} + +conf_t *conf_update( + conf_t *conf, + conf_update_flag_t flags) +{ + // Remove the clone flag for new master configuration. + if (conf != NULL) { + conf->is_clone = false; + + if ((flags & CONF_UPD_FCONFIO) && s_conf != NULL) { + conf->io.flags = s_conf->io.flags; + conf->io.zones = s_conf->io.zones; + } + if ((flags & CONF_UPD_FMODULES) && s_conf != NULL) { + free(conf->query_modules); + conf->query_modules = s_conf->query_modules; + conf->query_plan = s_conf->query_plan; + } + } + + conf_t **current_conf = &s_conf; + conf_t *old_conf = rcu_xchg_pointer(current_conf, conf); + + synchronize_rcu(); + + if (old_conf != NULL) { + // Remove the clone flag if a single configuration. + old_conf->is_clone = (conf != NULL) ? true : false; + + if (flags & CONF_UPD_FCONFIO) { + old_conf->io.zones = NULL; + } + if (flags & CONF_UPD_FMODULES) { + old_conf->query_modules = NULL; + old_conf->query_plan = NULL; + } + if (!(flags & CONF_UPD_FNOFREE)) { + conf_free(old_conf); + old_conf = NULL; + } + } + + return old_conf; +} + +void conf_free( + conf_t *conf) +{ + if (conf == NULL) { + return; + } + + yp_schema_free(conf->schema); + free(conf->filename); + free(conf->hostname); + if (conf->api != NULL) { + conf->api->txn_abort(&conf->read_txn); + } + + if (conf->io.txn != NULL && conf->api != NULL) { + conf->api->txn_abort(conf->io.txn_stack); + } + if (conf->io.zones != NULL) { + trie_free(conf->io.zones); + } + + conf_mod_load_purge(conf, false); + conf_deactivate_modules(conf->query_modules, &conf->query_plan); + free(conf->query_modules); + conf_mod_unload_shared(conf); + + if (!conf->is_clone) { + if (conf->api != NULL) { + conf->api->deinit(conf->db); + } + } + + free(conf); +} + +#define CONF_LOG_LINE(file, line, msg, ...) do { \ + CONF_LOG(LOG_ERR, "%s%s%sline %zu" msg, \ + (file != NULL ? "file '" : ""), (file != NULL ? file : ""), \ + (file != NULL ? "', " : ""), line, ##__VA_ARGS__); \ + } while (0) + +static void log_parser_err( + yp_parser_t *parser, + int ret) +{ + if (parser->event == YP_ENULL) { + CONF_LOG_LINE(parser->file.name, parser->line_count, + " (%s)", knot_strerror(ret)); + } else { + CONF_LOG_LINE(parser->file.name, parser->line_count, + ", item '%s'%s%.*s%s (%s)", parser->key, + (parser->data_len > 0) ? ", value '" : "", + (int)parser->data_len, + (parser->data_len > 0) ? parser->data : "", + (parser->data_len > 0) ? "'" : "", + knot_strerror(ret)); + } +} + +static void log_parser_schema_err( + yp_parser_t *parser, + int ret) +{ + // Emit better message for 'unknown module' error. + if (ret == KNOT_YP_EINVAL_ITEM && parser->event == YP_EKEY0 && + strncmp(parser->key, KNOTD_MOD_NAME_PREFIX, strlen(KNOTD_MOD_NAME_PREFIX)) == 0) { + CONF_LOG_LINE(parser->file.name, parser->line_count, + ", unknown module '%s'", parser->key); + } else { + log_parser_err(parser, ret); + } +} + +static void log_call_err( + yp_parser_t *parser, + knotd_conf_check_args_t *args, + int ret) +{ + CONF_LOG_LINE(args->extra->file_name, args->extra->line, + ", item '%s'%s%s%s (%s)", args->item->name + 1, + (parser->data_len > 0) ? ", value '" : "", + (parser->data_len > 0) ? parser->data : "", + (parser->data_len > 0) ? "'" : "", + (args->err_str != NULL) ? args->err_str : knot_strerror(ret)); +} + +static void log_prev_err( + knotd_conf_check_args_t *args, + int ret) +{ + char buff[512] = { 0 }; + size_t len = sizeof(buff); + + // Get the previous textual identifier. + if ((args->item->flags & YP_FMULTI) != 0) { + if (yp_item_to_txt(args->item->var.g.id, args->id, args->id_len, + buff, &len, YP_SNOQUOTE) != KNOT_EOK) { + buff[0] = '\0'; + } + } + + CONF_LOG_LINE(args->extra->file_name, args->extra->line - 1, + ", section '%s%s%s%s' (%s)", args->item->name + 1, + (buff[0] != '\0') ? "[" : "", + buff, + (buff[0] != '\0') ? "]" : "", + args->err_str != NULL ? args->err_str : knot_strerror(ret)); +} + +static int finalize_previous_section( + conf_t *conf, + knot_db_txn_t *txn, + yp_parser_t *parser, + yp_check_ctx_t *ctx) +{ + yp_node_t *node = &ctx->nodes[0]; + + // Return if no previous section or include or empty multi-section. + if (node->item == NULL || node->item->type != YP_TGRP || + (node->id_len == 0 && (node->item->flags & YP_FMULTI) != 0)) { + return KNOT_EOK; + } + + knotd_conf_check_extra_t extra = { + .conf = conf, + .txn = txn, + .file_name = parser->file.name, + .line = parser->line_count + }; + knotd_conf_check_args_t args = { + .item = node->item, + .id = node->id, + .id_len = node->id_len, + .data = node->data, + .data_len = node->data_len, + .extra = &extra + }; + + int ret = conf_exec_callbacks(&args); + if (ret != KNOT_EOK) { + log_prev_err(&args, ret); + } + + return ret; +} + +static int finalize_item( + conf_t *conf, + knot_db_txn_t *txn, + yp_parser_t *parser, + yp_check_ctx_t *ctx) +{ + yp_node_t *node = &ctx->nodes[ctx->current]; + + // Section callbacks are executed before another section. + if (node->item->type == YP_TGRP && node->id_len == 0) { + return KNOT_EOK; + } + + knotd_conf_check_extra_t extra = { + .conf = conf, + .txn = txn, + .file_name = parser->file.name, + .line = parser->line_count + }; + knotd_conf_check_args_t args = { + .item = (parser->event == YP_EID) ? node->item->var.g.id : node->item, + .id = node->id, + .id_len = node->id_len, + .data = node->data, + .data_len = node->data_len, + .extra = &extra + }; + + int ret = conf_exec_callbacks(&args); + if (ret != KNOT_EOK) { + log_call_err(parser, &args, ret); + } + + return ret; +} + +int conf_parse( + conf_t *conf, + knot_db_txn_t *txn, + const char *input, + bool is_file) +{ + if (conf == NULL || txn == NULL || input == NULL) { + return KNOT_EINVAL; + } + + yp_parser_t *parser = malloc(sizeof(yp_parser_t)); + if (parser == NULL) { + return KNOT_ENOMEM; + } + yp_init(parser); + + int ret; + + // Set parser source. + if (is_file) { + ret = yp_set_input_file(parser, input); + } else { + ret = yp_set_input_string(parser, input, strlen(input)); + } + if (ret != KNOT_EOK) { + CONF_LOG(LOG_ERR, "failed to load file '%s' (%s)", + input, knot_strerror(ret)); + goto parse_error; + } + + // Initialize parser check context. + yp_check_ctx_t *ctx = yp_schema_check_init(&conf->schema); + if (ctx == NULL) { + ret = KNOT_ENOMEM; + goto parse_error; + } + + int check_ret = KNOT_EOK; + + // Parse the configuration. + while ((ret = yp_parse(parser)) == KNOT_EOK) { + if (parser->event == YP_EKEY0 || parser->event == YP_EID) { + check_ret = finalize_previous_section(conf, txn, parser, ctx); + if (check_ret != KNOT_EOK) { + break; + } + } + + check_ret = yp_schema_check_parser(ctx, parser); + if (check_ret != KNOT_EOK) { + log_parser_schema_err(parser, check_ret); + break; + } + + yp_node_t *node = &ctx->nodes[ctx->current]; + yp_node_t *parent = node->parent; + + if (parent == NULL) { + check_ret = conf_db_set(conf, txn, node->item->name, + NULL, node->id, node->id_len, + node->data, node->data_len); + } else { + check_ret = conf_db_set(conf, txn, parent->item->name, + node->item->name, parent->id, + parent->id_len, node->data, + node->data_len); + } + if (check_ret != KNOT_EOK) { + log_parser_err(parser, check_ret); + break; + } + + check_ret = finalize_item(conf, txn, parser, ctx); + if (check_ret != KNOT_EOK) { + break; + } + } + + if (ret == KNOT_EOF) { + ret = finalize_previous_section(conf, txn, parser, ctx); + } else if (ret != KNOT_EOK) { + log_parser_err(parser, ret); + } else { + ret = check_ret; + } + + yp_schema_check_deinit(ctx); +parse_error: + yp_deinit(parser); + free(parser); + + return ret; +} + +int conf_import( + conf_t *conf, + const char *input, + bool is_file, + bool reinit_cache) +{ + if (conf == NULL || input == NULL) { + return KNOT_EINVAL; + } + + int ret; + + knot_db_txn_t txn; + ret = conf->api->txn_begin(conf->db, &txn, 0); + if (ret != KNOT_EOK) { + goto import_error; + } + + // Initialize the DB. + ret = conf_db_init(conf, &txn, true); + if (ret != KNOT_EOK) { + conf->api->txn_abort(&txn); + goto import_error; + } + + // Parse and import given file. + ret = conf_parse(conf, &txn, input, is_file); + if (ret != KNOT_EOK) { + conf->api->txn_abort(&txn); + goto import_error; + } + // Load purge must be here as conf_parse may be called recursively! + conf_mod_load_purge(conf, false); + + // Commit new configuration. + ret = conf->api->txn_commit(&txn); + if (ret != KNOT_EOK) { + goto import_error; + } + + // Update read-only transaction. + ret = conf_refresh_txn(conf); + if (ret != KNOT_EOK) { + goto import_error; + } + + // Update cached values. + init_cache(conf, reinit_cache); + + // Reset the filename. + free(conf->filename); + conf->filename = NULL; + if (is_file) { + conf->filename = strdup(input); + } + + ret = KNOT_EOK; +import_error: + + return ret; +} + +static int export_group_name( + FILE *fp, + const yp_item_t *group, + char *out, + size_t out_len, + yp_style_t style) +{ + int ret = yp_format_key0(group, NULL, 0, out, out_len, style, true, true); + if (ret != KNOT_EOK) { + return ret; + } + + fprintf(fp, "%s", out); + + return KNOT_EOK; +} + +static int export_group( + conf_t *conf, + FILE *fp, + const yp_item_t *group, + const uint8_t *id, + size_t id_len, + char *out, + size_t out_len, + yp_style_t style, + bool *exported) +{ + // Export the multi-group name. + if ((group->flags & YP_FMULTI) != 0 && !(*exported)) { + int ret = export_group_name(fp, group, out, out_len, style); + if (ret != KNOT_EOK) { + return ret; + } + *exported = true; + } + + // Iterate through all possible group items. + for (yp_item_t *item = group->sub_items; item->name != NULL; item++) { + // Export the identifier. + if (group->var.g.id == item && (group->flags & YP_FMULTI) != 0) { + int ret = yp_format_id(group->var.g.id, id, id_len, out, + out_len, style); + if (ret != KNOT_EOK) { + return ret; + } + fprintf(fp, "%s", out); + continue; + } + + conf_val_t bin; + conf_db_get(conf, &conf->read_txn, group->name, item->name, + id, id_len, &bin); + if (bin.code == KNOT_ENOENT) { + continue; + } else if (bin.code != KNOT_EOK) { + return bin.code; + } + + // Export the single-group name if an item is set. + if ((group->flags & YP_FMULTI) == 0 && !(*exported)) { + int ret = export_group_name(fp, group, out, out_len, style); + if (ret != KNOT_EOK) { + return ret; + } + *exported = true; + } + + // Format single/multiple-valued item. + size_t values = conf_val_count(&bin); + for (size_t i = 1; i <= values; i++) { + conf_val(&bin); + int ret = yp_format_key1(item, bin.data, bin.len, out, + out_len, style, i == 1, + i == values); + if (ret != KNOT_EOK) { + return ret; + } + fprintf(fp, "%s", out); + + if (values > 1) { + conf_val_next(&bin); + } + } + } + + if (*exported) { + fprintf(fp, "\n"); + } + + return KNOT_EOK; +} + +static int export_item( + conf_t *conf, + FILE *fp, + const yp_item_t *item, + char *buff, + size_t buff_len, + yp_style_t style) +{ + bool exported = false; + + // Skip non-group items (include). + if (item->type != YP_TGRP) { + return KNOT_EOK; + } + + // Export simple group without identifiers. + if (!(item->flags & YP_FMULTI)) { + return export_group(conf, fp, item, NULL, 0, buff, buff_len, + style, &exported); + } + + // Iterate over all identifiers. + conf_iter_t iter; + int ret = conf_db_iter_begin(conf, &conf->read_txn, item->name, &iter); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + return KNOT_EOK; + default: + return ret; + } + + while (ret == KNOT_EOK) { + const uint8_t *id; + size_t id_len; + ret = conf_db_iter_id(conf, &iter, &id, &id_len); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf, &iter); + return ret; + } + + // Export group with identifiers. + ret = export_group(conf, fp, item, id, id_len, buff, buff_len, + style, &exported); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf, &iter); + return ret; + } + + ret = conf_db_iter_next(conf, &iter); + } + if (ret != KNOT_EOF) { + return ret; + } + + return KNOT_EOK; +} + +int conf_export( + conf_t *conf, + const char *file_name, + yp_style_t style) +{ + if (conf == NULL) { + return KNOT_EINVAL; + } + + // Prepare common buffer; + const size_t buff_len = 2 * CONF_MAX_DATA_LEN; // Rough limit. + char *buff = malloc(buff_len); + if (buff == NULL) { + return KNOT_ENOMEM; + } + + FILE *fp = (file_name != NULL) ? fopen(file_name, "w") : stdout; + if (fp == NULL) { + free(buff); + return knot_map_errno(); + } + + fprintf(fp, "# Configuration export (Knot DNS %s)\n\n", PACKAGE_VERSION); + + const char *mod_prefix = KNOTD_MOD_NAME_PREFIX; + const size_t mod_prefix_len = strlen(mod_prefix); + + int ret; + + // Iterate over the schema. + for (yp_item_t *item = conf->schema; item->name != NULL; item++) { + // Don't export module sections again. + if (strncmp(item->name + 1, mod_prefix, mod_prefix_len) == 0) { + break; + } + + // Export module sections before the template section. + if (strcmp(&item->name[1], &C_TPL[1]) == 0) { + for (yp_item_t *mod = item + 1; mod->name != NULL; mod++) { + // Skip non-module sections. + if (strncmp(mod->name + 1, mod_prefix, mod_prefix_len) != 0) { + continue; + } + + // Export module section. + ret = export_item(conf, fp, mod, buff, buff_len, style); + if (ret != KNOT_EOK) { + goto export_error; + } + } + } + + // Export non-module section. + ret = export_item(conf, fp, item, buff, buff_len, style); + if (ret != KNOT_EOK) { + goto export_error; + } + } + + ret = KNOT_EOK; +export_error: + if (file_name != NULL) { + fclose(fp); + } + free(buff); + + return ret; +} diff --git a/src/knot/conf/base.h b/src/knot/conf/base.h new file mode 100644 index 0000000..693ffd6 --- /dev/null +++ b/src/knot/conf/base.h @@ -0,0 +1,322 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/libknot.h" +#include "libknot/yparser/ypschema.h" +#include "contrib/qp-trie/trie.h" +#include "contrib/ucw/lists.h" +#include "libknot/dynarray.h" +#include "knot/include/module.h" + +/*! Default template identifier. */ +#define CONF_DEFAULT_ID ((uint8_t *)"\x08""default\0") +/*! Default configuration file. */ +#define CONF_DEFAULT_FILE (CONFIG_DIR "/knot.conf") +/*! Default configuration database. */ +#define CONF_DEFAULT_DBDIR (STORAGE_DIR "/confdb") +/*! Maximum depth of nested transactions. */ +#define CONF_MAX_TXN_DEPTH 5 + +/*! Maximum number of UDP workers. */ +#define CONF_MAX_UDP_WORKERS 256 +/*! Maximum number of TCP workers. */ +#define CONF_MAX_TCP_WORKERS 256 +/*! Maximum number of background workers. */ +#define CONF_MAX_BG_WORKERS 512 +/*! Maximum number of concurrent DB readers. */ +#define CONF_MAX_DB_READERS (CONF_MAX_UDP_WORKERS + CONF_MAX_TCP_WORKERS + \ + CONF_MAX_BG_WORKERS + 10 + 128 /* Utils, XDP workers */) + +/*! Configuration specific logging. */ +#define CONF_LOG(severity, msg, ...) do { \ + log_fmt(severity, LOG_SOURCE_SERVER, "config, " msg, ##__VA_ARGS__); \ + } while (0) + +#define CONF_LOG_ZONE(severity, zone, msg, ...) do { \ + log_fmt_zone(severity, LOG_SOURCE_ZONE, zone, NULL, "config, " msg, ##__VA_ARGS__); \ + } while (0) + +/*! Configuration getter output. */ +typedef struct { + /*! Item description. */ + const yp_item_t *item; + /*! Whole data (can be array). */ + const uint8_t *blob; + /*! Whole data length. */ + size_t blob_len; + // Public items. + /*! Current single data. */ + const uint8_t *data; + /*! Current single data length. */ + size_t len; + /*! Value getter return code. */ + int code; +} conf_val_t; + +/*! Shared module types. */ +typedef enum { + /*! Static module. */ + MOD_STATIC = 0, + /*! Implicit shared module which is always loaded. */ + MOD_IMPLICIT, + /*! Explicit shared module which is currently loaded. */ + MOD_EXPLICIT, + /*! Explicit shared temporary module which is loaded during config check. */ + MOD_TEMPORARY +} module_type_t; + +/*! Query module context. */ +typedef struct { + /*! Module interface. */ + const knotd_mod_api_t *api; + /*! Shared library dlopen handler. */ + void *lib_handle; + /*! Module type. */ + module_type_t type; +} module_t; + +knot_dynarray_declare(mod, module_t *, DYNARRAY_VISIBILITY_NORMAL, 16) +knot_dynarray_declare(old_schema, yp_item_t *, DYNARRAY_VISIBILITY_NORMAL, 16) + +struct knot_catalog; + +/*! Configuration context. */ +typedef struct { + /*! Cloned configuration indicator. */ + bool is_clone; + /*! Currently used namedb api. */ + const struct knot_db_api *api; + /*! Configuration schema. */ + yp_item_t *schema; + /*! Configuration database. */ + knot_db_t *db; + /*! LMDB mapsize. */ + size_t mapsize; + + /*! Read-only transaction for config access. */ + knot_db_txn_t read_txn; + + struct { + /*! The current writing transaction. */ + knot_db_txn_t *txn; + /*! Stack of nested writing transactions. */ + knot_db_txn_t txn_stack[CONF_MAX_TXN_DEPTH]; + /*! Master transaction flags. */ + yp_flag_t flags; + /*! Changed zones. */ + trie_t *zones; + } io; + + /*! Current config file (for reload if started with config file). */ + char *filename; + + /*! Prearranged hostname string (for automatic NSID or CH ident value). */ + char *hostname; + + /*! Cached critical confdb items. */ + struct { + uint16_t srv_udp_max_payload_ipv4; + uint16_t srv_udp_max_payload_ipv6; + int srv_tcp_idle_timeout; + int srv_tcp_io_timeout; + int srv_tcp_remote_io_timeout; + bool srv_tcp_reuseport; + bool srv_tcp_fastopen; + bool srv_socket_affinity; + unsigned srv_dbus_event; + size_t srv_udp_threads; + size_t srv_tcp_threads; + size_t srv_xdp_threads; + size_t srv_bg_threads; + size_t srv_tcp_max_clients; + size_t xdp_tcp_max_clients; + size_t xdp_tcp_inbuf_max_size; + size_t xdp_tcp_outbuf_max_size; + uint32_t xdp_tcp_idle_close; + uint32_t xdp_tcp_idle_reset; + uint32_t xdp_tcp_idle_resend; + size_t srv_quic_max_clients; + size_t srv_quic_obuf_max_size; + uint32_t srv_quic_idle_close; + bool xdp_udp; + bool xdp_tcp; + uint16_t xdp_quic; + bool xdp_route_check; + int ctl_timeout; + const uint8_t *srv_nsid_data; + size_t srv_nsid_len; + bool srv_ecs; + bool srv_ans_rotate; + bool srv_auto_acl; + bool srv_proxy_enabled; + } cache; + + /*! List of dynamically loaded modules. */ + mod_dynarray_t modules; + /*! List of old schemas (lazy freed). */ + old_schema_dynarray_t old_schemas; + /*! List of active query modules. */ + list_t *query_modules; + /*! Default query modules plan. */ + struct query_plan *query_plan; + /*! Zone catalog database. */ + struct catalog *catalog; +} conf_t; + +/*! + * Configuration access flags. + */ +typedef enum { + CONF_FNONE = 0, /*!< Empty flag. */ + CONF_FREADONLY = 1 << 0, /*!< Read only access. */ + CONF_FNOCHECK = 1 << 1, /*!< Disabled confdb check. */ + CONF_FNOHOSTNAME = 1 << 2, /*!< Don't set the hostname. */ + CONF_FREQMODULES = 1 << 3, /*!< Load module schemas (must succeed). */ + CONF_FOPTMODULES = 1 << 4, /*!< Load module schemas (may fail). */ +} conf_flag_t; + +/*! + * Configuration update flags. + */ +typedef enum { + CONF_UPD_FNONE = 0, /*!< Empty flag. */ + CONF_UPD_FNOFREE = 1 << 0, /*!< Disable auto-free of previous config. */ + CONF_UPD_FMODULES = 1 << 1, /*!< Reuse previous global modules. */ + CONF_UPD_FCONFIO = 1 << 2, /*!< Reuse previous confio reload context. */ +} conf_update_flag_t; + +/*! + * Returns the active configuration. + */ +conf_t* conf(void); + +/*! + * Refreshes common read-only transaction. + * + * \param[in] conf Configuration. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_refresh_txn( + conf_t *conf +); + +/*! + * Creates new or opens old configuration database. + * + * \param[out] conf Configuration. + * \param[in] schema Configuration schema. + * \param[in] db_dir Database path or NULL. + * \param[in] max_conf_size Maximum configuration DB size in bytes (LMDB mapsize). + * \param[in] flags Access flags. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_new( + conf_t **conf, + const yp_item_t *schema, + const char *db_dir, + size_t max_conf_size, + conf_flag_t flags +); + +/*! + * Creates a partial copy of the active configuration. + * + * Shared objects: api, mm, db, filename. + * + * \param[out] conf Configuration. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_clone( + conf_t **conf +); + +/*! + * Replaces the active configuration with the specified one. + * + * \param[in] conf New configuration. + * \param[in] flags Update flags. + * + * \return Previous config if CONF_UPD_FNOFREE, else NULL. + */ +conf_t *conf_update( + conf_t *conf, + conf_update_flag_t flags +); + +/*! + * Removes the specified configuration. + * + * \param[in] conf Configuration. + */ +void conf_free( + conf_t *conf +); + +/*! + * Parses textual configuration from the string or from the file. + * + * This function is not for direct using, just for includes processing! + * + * \param[in] conf Configuration. + * \param[in] txn Transaction. + * \param[in] input Configuration string or filename. + * \param[in] is_file Specifies if the input is string or input filename. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_parse( + conf_t *conf, + knot_db_txn_t *txn, + const char *input, + bool is_file +); + +/*! + * Imports textual configuration. + * + * \param[in] conf Configuration. + * \param[in] input Configuration string or input filename. + * \param[in] is_file Specifies if the input is string or filename. + * \param[in] reinit_cache Indication if cache reinitialization needed. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_import( + conf_t *conf, + const char *input, + bool is_file, + bool reinit_cache +); + +/*! + * Exports configuration to textual file. + * + * \param[in] conf Configuration. + * \param[in] file_name Output filename (stdout is used if NULL). + * \param[in] style Formatting style. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_export( + conf_t *conf, + const char *file_name, + yp_style_t style +); diff --git a/src/knot/conf/conf.c b/src/knot/conf/conf.c new file mode 100644 index 0000000..016f01e --- /dev/null +++ b/src/knot/conf/conf.c @@ -0,0 +1,1469 @@ +/* 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 <grp.h> +#include <pwd.h> +#include <stdio.h> +#include <sys/resource.h> +#include <sys/stat.h> + +#include "knot/conf/base.h" +#include "knot/conf/confdb.h" +#include "knot/catalog/catalog_db.h" +#include "knot/common/log.h" +#include "knot/server/dthreads.h" +#include "libknot/libknot.h" +#include "libknot/yparser/yptrafo.h" +#include "libknot/xdp.h" +#include "contrib/files.h" +#include "contrib/macros.h" +#include "contrib/sockaddr.h" +#include "contrib/strtonum.h" +#include "contrib/string.h" +#include "contrib/wire_ctx.h" +#include "contrib/openbsd/strlcat.h" +#include "contrib/openbsd/strlcpy.h" + +#define DBG_LOG(err) CONF_LOG(LOG_DEBUG, "%s (%s)", __func__, knot_strerror((err))); + +#define DFLT_MIN_TCP_WORKERS 10 +#define DFLT_MAX_BG_WORKERS 10 +#define FALLBACK_MAX_TCP_CLIENTS 100 + +bool conf_db_exists( + const char *db_dir) +{ + if (db_dir == NULL) { + return false; + } + + struct stat st; + char data_mdb[strlen(db_dir) + 10]; + (void)snprintf(data_mdb, sizeof(data_mdb), "%s/data.mdb", db_dir); + return (stat(data_mdb, &st) == 0 && st.st_size > 0); +} + +conf_val_t conf_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name, + const yp_name_t *key1_name) +{ + conf_val_t val = { NULL }; + + if (key0_name == NULL || key1_name == NULL) { + val.code = KNOT_EINVAL; + DBG_LOG(val.code); + return val; + } + + conf_db_get(conf, txn, key0_name, key1_name, NULL, 0, &val); + switch (val.code) { + default: + CONF_LOG(LOG_ERR, "failed to read '%s/%s' (%s)", + key0_name + 1, key1_name + 1, knot_strerror(val.code)); + // FALLTHROUGH + case KNOT_EOK: + case KNOT_ENOENT: + return val; + } +} + +conf_val_t conf_rawid_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name, + const yp_name_t *key1_name, + const uint8_t *id, + size_t id_len) +{ + conf_val_t val = { NULL }; + + if (key0_name == NULL || key1_name == NULL || id == NULL) { + val.code = KNOT_EINVAL; + DBG_LOG(val.code); + return val; + } + + conf_db_get(conf, txn, key0_name, key1_name, id, id_len, &val); + switch (val.code) { + default: + CONF_LOG(LOG_ERR, "failed to read '%s/%s' with identifier (%s)", + key0_name + 1, key1_name + 1, knot_strerror(val.code)); + // FALLTHROUGH + case KNOT_EOK: + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + return val; + } +} + +conf_val_t conf_id_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name, + const yp_name_t *key1_name, + conf_val_t *id) +{ + conf_val_t val = { NULL }; + + if (key0_name == NULL || key1_name == NULL || id == NULL || + id->code != KNOT_EOK) { + val.code = KNOT_EINVAL; + DBG_LOG(val.code); + return val; + } + + conf_val(id); + + conf_db_get(conf, txn, key0_name, key1_name, id->data, id->len, &val); + switch (val.code) { + default: + CONF_LOG(LOG_ERR, "failed to read '%s/%s' with identifier (%s)", + key0_name + 1, key1_name + 1, knot_strerror(val.code)); + // FALLTHROUGH + case KNOT_EOK: + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + return val; + } +} + +conf_val_t conf_mod_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key1_name, + const conf_mod_id_t *mod_id) +{ + conf_val_t val = { NULL }; + + if (key1_name == NULL || mod_id == NULL) { + val.code = KNOT_EINVAL; + DBG_LOG(val.code); + return val; + } + + conf_db_get(conf, txn, mod_id->name, key1_name, mod_id->data, mod_id->len, + &val); + switch (val.code) { + default: + CONF_LOG(LOG_ERR, "failed to read '%s/%s' (%s)", + mod_id->name + 1, key1_name + 1, knot_strerror(val.code)); + // FALLTHROUGH + case KNOT_EOK: + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + return val; + } +} + +conf_val_t conf_zone_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key1_name, + const knot_dname_t *dname) +{ + conf_val_t val = { NULL }; + + if (key1_name == NULL || dname == NULL) { + val.code = KNOT_EINVAL; + DBG_LOG(val.code); + return val; + } + + size_t dname_size = knot_dname_size(dname); + + // Try to get explicit value. + conf_db_get(conf, txn, C_ZONE, key1_name, dname, dname_size, &val); + switch (val.code) { + case KNOT_EOK: + return val; + default: + CONF_LOG_ZONE(LOG_ERR, dname, "failed to read '%s/%s' (%s)", + &C_ZONE[1], &key1_name[1], knot_strerror(val.code)); + // FALLTHROUGH + case KNOT_YP_EINVAL_ID: + case KNOT_ENOENT: + break; + } + + // Check if a template is available. + conf_db_get(conf, txn, C_ZONE, C_TPL, dname, dname_size, &val); + switch (val.code) { + case KNOT_EOK: + // Use the specified template. + conf_val(&val); + conf_db_get(conf, txn, C_TPL, key1_name, val.data, val.len, &val); + goto got_template; + default: + CONF_LOG_ZONE(LOG_ERR, dname, "failed to read '%s/%s' (%s)", + &C_ZONE[1], &C_TPL[1], knot_strerror(val.code)); + // FALLTHROUGH + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + break; + } + + // Check if this is a catalog member zone. + if (conf->catalog != NULL) { + void *tofree = NULL; + const knot_dname_t *catalog; + const char *group; + int ret = catalog_get_catz(conf->catalog, dname, &catalog, &group, &tofree); + if (ret == KNOT_EOK) { + val = conf_zone_get_txn(conf, txn, C_CATALOG_TPL, catalog); + if (val.code == KNOT_EOK) { + conf_val(&val); + while (val.code == KNOT_EOK) { + if (strmemcmp(group, val.data, val.len) == 0) { + break; + } + conf_val_next(&val); + } + conf_val(&val); // Use first value if no match. + free(tofree); + + conf_db_get(conf, txn, C_TPL, key1_name, val.data, + val.len, &val); + goto got_template; + } else { + CONF_LOG_ZONE(LOG_ERR, catalog, + "orphaned catalog database record (%s)", + knot_strerror(val.code)); + free(tofree); + } + } + } + + // Use the default template. + conf_db_get(conf, txn, C_TPL, key1_name, CONF_DEFAULT_ID + 1, + CONF_DEFAULT_ID[0], &val); + +got_template: + switch (val.code) { + default: + CONF_LOG_ZONE(LOG_ERR, dname, "failed to read '%s/%s' (%s)", + &C_TPL[1], &key1_name[1], knot_strerror(val.code)); + // FALLTHROUGH + case KNOT_EOK: + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + break; + } + + return val; +} + +conf_val_t conf_default_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key1_name) +{ + conf_val_t val = { NULL }; + + if (key1_name == NULL) { + val.code = KNOT_EINVAL; + DBG_LOG(val.code); + return val; + } + + conf_db_get(conf, txn, C_TPL, key1_name, CONF_DEFAULT_ID + 1, + CONF_DEFAULT_ID[0], &val); + switch (val.code) { + default: + CONF_LOG(LOG_ERR, "failed to read default '%s/%s' (%s)", + &C_TPL[1], &key1_name[1], knot_strerror(val.code)); + // FALLTHROUGH + case KNOT_EOK: + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + break; + } + + return val; +} + +bool conf_rawid_exists_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name, + const uint8_t *id, + size_t id_len) +{ + if (key0_name == NULL || id == NULL) { + DBG_LOG(KNOT_EINVAL); + return false; + } + + int ret = conf_db_get(conf, txn, key0_name, NULL, id, id_len, NULL); + switch (ret) { + case KNOT_EOK: + return true; + default: + CONF_LOG(LOG_ERR, "failed to check '%s' for identifier (%s)", + key0_name + 1, knot_strerror(ret)); + // FALLTHROUGH + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + return false; + } +} + +bool conf_id_exists_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name, + conf_val_t *id) +{ + if (key0_name == NULL || id == NULL || id->code != KNOT_EOK) { + DBG_LOG(KNOT_EINVAL); + return false; + } + + conf_val(id); + + int ret = conf_db_get(conf, txn, key0_name, NULL, id->data, id->len, NULL); + switch (ret) { + case KNOT_EOK: + return true; + default: + CONF_LOG(LOG_ERR, "failed to check '%s' for identifier (%s)", + key0_name + 1, knot_strerror(ret)); + // FALLTHROUGH + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + return false; + } +} + +size_t conf_id_count_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name) +{ + size_t count = 0; + + for (conf_iter_t iter = conf_iter_txn(conf, txn, key0_name); + iter.code == KNOT_EOK; conf_iter_next(conf, &iter)) { + count++; + } + + return count; +} + +conf_iter_t conf_iter_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name) +{ + conf_iter_t iter = { NULL }; + + (void)conf_db_iter_begin(conf, txn, key0_name, &iter); + switch (iter.code) { + default: + CONF_LOG(LOG_ERR, "failed to iterate through '%s' (%s)", + key0_name + 1, knot_strerror(iter.code)); + // FALLTHROUGH + case KNOT_EOK: + case KNOT_ENOENT: + return iter; + } +} + +void conf_iter_next( + conf_t *conf, + conf_iter_t *iter) +{ + (void)conf_db_iter_next(conf, iter); + switch (iter->code) { + default: + CONF_LOG(LOG_ERR, "failed to read next item (%s)", + knot_strerror(iter->code)); + // FALLTHROUGH + case KNOT_EOK: + case KNOT_EOF: + return; + } +} + +conf_val_t conf_iter_id( + conf_t *conf, + conf_iter_t *iter) +{ + conf_val_t val = { NULL }; + + val.code = conf_db_iter_id(conf, iter, &val.blob, &val.blob_len); + switch (val.code) { + default: + CONF_LOG(LOG_ERR, "failed to read identifier (%s)", + knot_strerror(val.code)); + // FALLTHROUGH + case KNOT_EOK: + val.item = iter->item; + return val; + } +} + +void conf_iter_finish( + conf_t *conf, + conf_iter_t *iter) +{ + conf_db_iter_finish(conf, iter); +} + +size_t conf_val_count( + conf_val_t *val) +{ + if (val == NULL || val->code != KNOT_EOK) { + return 0; + } + + if (!(val->item->flags & YP_FMULTI)) { + return 1; + } + + size_t count = 0; + conf_val(val); + while (val->code == KNOT_EOK) { + count++; + conf_val_next(val); + } + if (val->code != KNOT_EOF) { + return 0; + } + + // Reset to the initial state. + conf_val(val); + + return count; +} + +void conf_val( + conf_val_t *val) +{ + assert(val != NULL); + assert(val->code == KNOT_EOK || val->code == KNOT_EOF); + + if (val->item->flags & YP_FMULTI) { + // Check if already called and not at the end. + if (val->data != NULL && val->code != KNOT_EOF) { + return; + } + // Otherwise set to the first value. + conf_val_reset(val); + } else { + // Check for empty data. + if (val->blob_len == 0) { + val->data = NULL; + val->len = 0; + val->code = KNOT_EOK; + return; + } else { + assert(val->blob != NULL); + val->data = val->blob; + val->len = val->blob_len; + val->code = KNOT_EOK; + } + } +} + +void conf_val_next( + conf_val_t *val) +{ + assert(val != NULL); + assert(val->code == KNOT_EOK); + assert(val->item->flags & YP_FMULTI); + + // Check for the 'zero' call. + if (val->data == NULL) { + conf_val(val); + return; + } + + if (val->data + val->len < val->blob + val->blob_len) { + wire_ctx_t ctx = wire_ctx_init_const(val->blob, val->blob_len); + size_t offset = val->data + val->len - val->blob; + wire_ctx_skip(&ctx, offset); + uint16_t len = wire_ctx_read_u16(&ctx); + assert(ctx.error == KNOT_EOK); + + val->data = ctx.position; + val->len = len; + val->code = KNOT_EOK; + } else { + val->data = NULL; + val->len = 0; + val->code = KNOT_EOF; + } +} + +void conf_val_reset(conf_val_t *val) +{ + assert(val != NULL); + assert(val->code == KNOT_EOK || val->code == KNOT_EOF); + assert(val->item->flags & YP_FMULTI); + + assert(val->blob != NULL); + wire_ctx_t ctx = wire_ctx_init_const(val->blob, val->blob_len); + uint16_t len = wire_ctx_read_u16(&ctx); + assert(ctx.error == KNOT_EOK); + + val->data = ctx.position; + val->len = len; + val->code = KNOT_EOK; +} + +bool conf_val_equal( + conf_val_t *val1, + conf_val_t *val2) +{ + if (val1->blob_len == val2->blob_len && + memcmp(val1->blob, val2->blob, val1->blob_len) == 0) { + return true; + } + + return false; +} + +void conf_mix_iter_init( + conf_t *conf, + conf_val_t *mix_id, + conf_mix_iter_t *iter) +{ + assert(mix_id != NULL && mix_id->item != NULL); + assert(mix_id->item->type == YP_TREF && + mix_id->item->var.r.ref != NULL && + mix_id->item->var.r.grp_ref != NULL && + mix_id->item->var.r.ref->var.g.id->type == YP_TSTR && + mix_id->item->var.r.grp_ref->var.g.id->type == YP_TSTR); + + iter->conf = conf; + iter->mix_id = mix_id; + iter->id = mix_id; + iter->nested = false; + + if (mix_id->code != KNOT_EOK) { + return; + } + + iter->sub_id = conf_id_get_txn(conf, &conf->read_txn, + mix_id->item->var.r.grp_ref_name, + mix_id->item->var.r.ref_name, + mix_id); + if (iter->sub_id.code == KNOT_EOK) { + conf_val(&iter->sub_id); + iter->id = &iter->sub_id; + iter->nested = true; + } +} + +void conf_mix_iter_next( + conf_mix_iter_t *iter) +{ + conf_val_next(iter->id); + if (iter->nested) { + if (iter->id->code == KNOT_EOK) { + return; + } + assert(iter->id->code == KNOT_EOF); + conf_val_next(iter->mix_id); + if (iter->mix_id->code != KNOT_EOK) { + return; + } + } else if (iter->id->code != KNOT_EOK){ + return; + } + + iter->sub_id = conf_id_get_txn(iter->conf, &iter->conf->read_txn, + iter->mix_id->item->var.r.grp_ref_name, + iter->mix_id->item->var.r.ref_name, + iter->mix_id); + if (iter->sub_id.code == KNOT_EOK) { + conf_val(&iter->sub_id); + iter->id = &iter->sub_id; + iter->nested = true; + } else { + iter->id = iter->mix_id; + iter->nested = false; + } +} + +int64_t conf_int( + conf_val_t *val) +{ + assert(val != NULL && val->item != NULL); + assert(val->item->type == YP_TINT || + (val->item->type == YP_TREF && + val->item->var.r.ref->var.g.id->type == YP_TINT)); + + if (val->code == KNOT_EOK) { + conf_val(val); + return yp_int(val->data); + } else { + return val->item->var.i.dflt; + } +} + +bool conf_bool( + conf_val_t *val) +{ + assert(val != NULL && val->item != NULL); + assert(val->item->type == YP_TBOOL || + (val->item->type == YP_TREF && + val->item->var.r.ref->var.g.id->type == YP_TBOOL)); + + if (val->code == KNOT_EOK) { + conf_val(val); + return yp_bool(val->data); + } else { + return val->item->var.b.dflt; + } +} + +unsigned conf_opt( + conf_val_t *val) +{ + assert(val != NULL && val->item != NULL); + assert(val->item->type == YP_TOPT || + (val->item->type == YP_TREF && + val->item->var.r.ref->var.g.id->type == YP_TOPT)); + + if (val->code == KNOT_EOK) { + conf_val(val); + return yp_opt(val->data); + } else { + return val->item->var.o.dflt; + } +} + +const char* conf_str( + conf_val_t *val) +{ + assert(val != NULL && val->item != NULL); + assert(val->item->type == YP_TSTR || + (val->item->type == YP_TREF && + val->item->var.r.ref->var.g.id->type == YP_TSTR)); + + if (val->code == KNOT_EOK) { + conf_val(val); + return yp_str(val->data); + } else { + return val->item->var.s.dflt; + } +} + +const knot_dname_t* conf_dname( + conf_val_t *val) +{ + assert(val != NULL && val->item != NULL); + assert(val->item->type == YP_TDNAME || + (val->item->type == YP_TREF && + val->item->var.r.ref->var.g.id->type == YP_TDNAME)); + + if (val->code == KNOT_EOK) { + conf_val(val); + return yp_dname(val->data); + } else { + return (const knot_dname_t *)val->item->var.d.dflt; + } +} + +const uint8_t* conf_bin( + conf_val_t *val, + size_t *len) +{ + assert(val != NULL && val->item != NULL && len != NULL); + assert(val->item->type == YP_THEX || val->item->type == YP_TB64 || + (val->item->type == YP_TREF && + (val->item->var.r.ref->var.g.id->type == YP_THEX || + val->item->var.r.ref->var.g.id->type == YP_TB64))); + + if (val->code == KNOT_EOK) { + conf_val(val); + *len = yp_bin_len(val->data); + return yp_bin(val->data); + } else { + *len = val->item->var.d.dflt_len; + return val->item->var.d.dflt; + } +} + +const uint8_t* conf_data( + conf_val_t *val, + size_t *len) +{ + assert(val != NULL && val->item != NULL); + assert(val->item->type == YP_TDATA || + (val->item->type == YP_TREF && + val->item->var.r.ref->var.g.id->type == YP_TDATA)); + + if (val->code == KNOT_EOK) { + conf_val(val); + *len = val->len; + return val->data; + } else { + *len = val->item->var.d.dflt_len; + return val->item->var.d.dflt; + } +} + +struct sockaddr_storage conf_addr( + conf_val_t *val, + const char *sock_base_dir) +{ + assert(val != NULL && val->item != NULL); + assert(val->item->type == YP_TADDR || + (val->item->type == YP_TREF && + val->item->var.r.ref->var.g.id->type == YP_TADDR)); + + struct sockaddr_storage out = { AF_UNSPEC }; + + if (val->code == KNOT_EOK) { + bool no_port; + conf_val(val); + assert(val->data); + out = yp_addr(val->data, &no_port); + + if (out.ss_family == AF_UNIX) { + // val->data[0] is socket type identifier! + if (val->data[1] != '/' && sock_base_dir != NULL) { + char *tmp = sprintf_alloc("%s/%s", sock_base_dir, + val->data + 1); + val->code = sockaddr_set(&out, AF_UNIX, tmp, 0); + free(tmp); + } + } else if (no_port) { + sockaddr_port_set(&out, val->item->var.a.dflt_port); + } + } else { + const char *dflt_socket = val->item->var.a.dflt_socket; + if (dflt_socket != NULL) { + if (dflt_socket[0] == '/' || sock_base_dir == NULL) { + val->code = sockaddr_set(&out, AF_UNIX, + dflt_socket, 0); + } else { + char *tmp = sprintf_alloc("%s/%s", sock_base_dir, + dflt_socket); + val->code = sockaddr_set(&out, AF_UNIX, tmp, 0); + free(tmp); + } + } + } + + return out; +} + +bool conf_addr_match( + conf_val_t *match, + const struct sockaddr_storage *addr) +{ + if (match == NULL || addr == NULL) { + return false; + } + + while (match->code == KNOT_EOK) { + struct sockaddr_storage maddr = conf_addr(match, NULL); + if (sockaddr_cmp(&maddr, addr, true) == 0) { + return true; + } + + conf_val_next(match); + } + + return false; +} + +struct sockaddr_storage conf_addr_range( + conf_val_t *val, + struct sockaddr_storage *max_ss, + int *prefix_len) +{ + assert(val != NULL && val->item != NULL && max_ss != NULL && + prefix_len != NULL); + assert(val->item->type == YP_TNET || + (val->item->type == YP_TREF && + val->item->var.r.ref->var.g.id->type == YP_TNET)); + + struct sockaddr_storage out = { AF_UNSPEC }; + + if (val->code == KNOT_EOK) { + conf_val(val); + assert(val->data); + out = yp_addr_noport(val->data); + // addr_type, addr, format, formatted_data (port| addr| empty). + const uint8_t *format = val->data + sizeof(uint8_t) + + ((out.ss_family == AF_INET) ? + IPV4_PREFIXLEN / 8 : IPV6_PREFIXLEN / 8); + // See addr_range_to_bin. + switch (*format) { + case 1: + max_ss->ss_family = AF_UNSPEC; + *prefix_len = yp_int(format + sizeof(uint8_t)); + break; + case 2: + *max_ss = yp_addr_noport(format + sizeof(uint8_t)); + *prefix_len = -1; + break; + default: + max_ss->ss_family = AF_UNSPEC; + *prefix_len = -1; + break; + } + } else { + max_ss->ss_family = AF_UNSPEC; + *prefix_len = -1; + } + + return out; +} + +bool conf_addr_range_match( + conf_val_t *range, + const struct sockaddr_storage *addr) +{ + if (range == NULL || addr == NULL) { + return false; + } + + while (range->code == KNOT_EOK) { + int mask; + struct sockaddr_storage min, max; + + min = conf_addr_range(range, &max, &mask); + if (max.ss_family == AF_UNSPEC) { + if (sockaddr_net_match(addr, &min, mask)) { + return true; + } + } else { + if (sockaddr_range_match(addr, &min, &max)) { + return true; + } + } + + conf_val_next(range); + } + + return false; +} + +char* conf_abs_path( + conf_val_t *val, + const char *base_dir) +{ + const char *path = conf_str(val); + return abs_path(path, base_dir); +} + +conf_mod_id_t* conf_mod_id( + conf_val_t *val) +{ + assert(val != NULL && val->item != NULL); + assert(val->item->type == YP_TDATA || + (val->item->type == YP_TREF && + val->item->var.r.ref->var.g.id->type == YP_TDATA)); + + conf_mod_id_t *mod_id = NULL; + + if (val->code == KNOT_EOK) { + conf_val(val); + assert(val->data); + + mod_id = malloc(sizeof(conf_mod_id_t)); + if (mod_id == NULL) { + return NULL; + } + + // Set module name in yp_name_t format + add zero termination. + size_t name_len = 1 + val->data[0]; + mod_id->name = malloc(name_len + 1); + if (mod_id->name == NULL) { + free(mod_id); + return NULL; + } + memcpy(mod_id->name, val->data, name_len); + mod_id->name[name_len] = '\0'; + + // Set module identifier. + mod_id->len = val->len - name_len; + mod_id->data = malloc(mod_id->len); + if (mod_id->data == NULL) { + free(mod_id->name); + free(mod_id); + return NULL; + } + memcpy(mod_id->data, val->data + name_len, mod_id->len); + } + + return mod_id; +} + +void conf_free_mod_id( + conf_mod_id_t *mod_id) +{ + if (mod_id == NULL) { + return; + } + + free(mod_id->name); + free(mod_id->data); + free(mod_id); +} + +static int get_index( + const char **start, + const char *end, + unsigned *index1, + unsigned *index2) +{ + char c, *p; + if (sscanf(*start, "[%u%c", index1, &c) != 2) { + return KNOT_EINVAL; + } + switch (c) { + case '-': + p = strchr(*start, '-') + 1; + if (end - p < 2 || index2 == NULL || + sscanf(p, "%u%c", index2, &c) != 2 || c != ']') { + return KNOT_EINVAL; + } + break; + case ']': + if (index2 != NULL) { + *index2 = *index1; + } + break; + default: + return KNOT_EINVAL; + } + + *start = strchr(*start, ']') + 1; + return ((*index1 < 256 && (index2 == NULL || *index2 < 256) + && end - *start >= 0 && (index2 == NULL || *index2 >= *index1)) + ? KNOT_EOK : KNOT_EINVAL); +} + +static void replace_slashes( + char *name, + bool remove_dot) +{ + // Replace possible slashes with underscores. + char *ch; + for (ch = name; *ch != '\0'; ch++) { + if (*ch == '/') { + *ch = '_'; + } + } + + // Remove trailing dot. + if (remove_dot && ch > name) { + assert(*(ch - 1) == '.'); + *(ch - 1) = '\0'; + } +} + +static int str_char( + const knot_dname_t *zone, + char *buff, + size_t buff_len, + unsigned index1, + unsigned index2) +{ + assert(buff); + + if (knot_dname_to_str(buff, zone, buff_len) == NULL) { + return KNOT_EINVAL; + } + + size_t zone_len = strlen(buff); + assert(zone_len > 0); + + // Get the block length. + size_t len = index2 - index1 + 1; + + // Check for out of scope block. + if (index1 >= zone_len) { + buff[0] = '\0'; + return KNOT_EOK; + } + // Check for partial block. + if (index2 >= zone_len) { + len = zone_len - index1; + } + + // Copy the block. + memmove(buff, buff + index1, len); + buff[len] = '\0'; + + // Replace possible slashes with underscores. + replace_slashes(buff, false); + + return KNOT_EOK; +} + +static int str_zone( + const knot_dname_t *zone, + char *buff, + size_t buff_len) +{ + assert(buff); + + if (knot_dname_to_str(buff, zone, buff_len) == NULL) { + return KNOT_EINVAL; + } + + // Replace possible slashes with underscores. + replace_slashes(buff, true); + + return KNOT_EOK; +} + +static int str_label( + const knot_dname_t *zone, + char *buff, + size_t buff_len, + size_t right_index) +{ + size_t labels = knot_dname_labels(zone, NULL); + + // Check for root label of the root zone. + if (labels == 0 && right_index == 0) { + return str_zone(zone, buff, buff_len); + // Check for labels error or for an exceeded index. + } else if (labels < 1 || labels <= right_index) { + buff[0] = '\0'; + return KNOT_EOK; + } + + // ~ Label length + label + root label. + knot_dname_t label[1 + KNOT_DNAME_MAXLABELLEN + 1]; + + // Compute the index from the left. + assert(labels > right_index); + size_t index = labels - right_index - 1; + + // Create a dname from the single label. + size_t prefix_len = knot_dname_prefixlen(zone, index, NULL); + size_t label_len = *(zone + prefix_len); + memcpy(label, zone + prefix_len, 1 + label_len); + label[1 + label_len] = '\0'; + + return str_zone(label, buff, buff_len); +} + +static char* get_filename( + conf_t *conf, + knot_db_txn_t *txn, + const knot_dname_t *zone, + const char *name) +{ + assert(name); + + const char *end = name + strlen(name); + char out[1024] = ""; + + do { + // Search for a formatter. + const char *pos = strchr(name, '%'); + + // If no formatter, copy the rest of the name. + if (pos == NULL) { + if (strlcat(out, name, sizeof(out)) >= sizeof(out)) { + CONF_LOG_ZONE(LOG_WARNING, zone, "too long zonefile name"); + return NULL; + } + break; + } + + // Copy constant block. + char *block = strndup(name, pos - name); + if (block == NULL || + strlcat(out, block, sizeof(out)) >= sizeof(out)) { + CONF_LOG_ZONE(LOG_WARNING, zone, "too long zonefile name"); + free(block); + return NULL; + } + free(block); + + // Move name pointer behind the formatter. + name = pos + 2; + + char buff[512] = ""; + unsigned idx1, idx2; + bool failed = false; + + const char type = *(pos + 1); + switch (type) { + case '%': + strlcat(buff, "%", sizeof(buff)); + break; + case 'c': + if (get_index(&name, end, &idx1, &idx2) != KNOT_EOK || + str_char(zone, buff, sizeof(buff), idx1, idx2) != KNOT_EOK) { + failed = true; + } + break; + case 'l': + if (get_index(&name, end, &idx1, NULL) != KNOT_EOK || + str_label(zone, buff, sizeof(buff), idx1) != KNOT_EOK) { + failed = true; + } + break; + case 's': + if (str_zone(zone, buff, sizeof(buff)) != KNOT_EOK) { + failed = true; + } + break; + case '\0': + CONF_LOG_ZONE(LOG_WARNING, zone, "ignoring missing " + "trailing zonefile formatter"); + continue; + default: + CONF_LOG_ZONE(LOG_WARNING, zone, "ignoring zonefile " + "formatter '%%%c'", type); + continue; + } + + if (failed) { + CONF_LOG_ZONE(LOG_WARNING, zone, "failed to process " + "zonefile formatter '%%%c'", type); + return NULL; + } + + if (strlcat(out, buff, sizeof(out)) >= sizeof(out)) { + CONF_LOG_ZONE(LOG_WARNING, zone, "too long zonefile name"); + return NULL; + } + } while (name < end); + + // Use storage prefix if not absolute path. + if (out[0] == '/') { + return strdup(out); + } else { + conf_val_t val = conf_zone_get_txn(conf, txn, C_STORAGE, zone); + char *storage = conf_abs_path(&val, NULL); + if (storage == NULL) { + return NULL; + } + char *abs = sprintf_alloc("%s/%s", storage, out); + free(storage); + return abs; + } +} + +char* conf_zonefile_txn( + conf_t *conf, + knot_db_txn_t *txn, + const knot_dname_t *zone) +{ + if (zone == NULL) { + return NULL; + } + + conf_val_t val = conf_zone_get_txn(conf, txn, C_FILE, zone); + const char *file = conf_str(&val); + + // Use default zonefile name pattern if not specified. + if (file == NULL) { + file = "%s.zone"; + } + + return get_filename(conf, txn, zone, file); +} + +char* conf_db_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *db_type) +{ + conf_val_t storage_val = conf_get_txn(conf, txn, C_DB, C_STORAGE); + char *storage = conf_abs_path(&storage_val, NULL); + + if (db_type == NULL) { + return storage; + } + + conf_val_t db_val = conf_get_txn(conf, txn, C_DB, db_type); + char *dbdir = conf_abs_path(&db_val, storage); + free(storage); + + return dbdir; +} + +char *conf_tls_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *tls_item) +{ + conf_val_t tls_val = conf_get_txn(conf, txn, C_SRV, tls_item); + if (conf_str(&tls_val) == NULL) { + return NULL; + } + + return conf_abs_path(&tls_val, CONFIG_DIR); +} + +size_t conf_udp_threads_txn( + conf_t *conf, + knot_db_txn_t *txn) +{ + conf_val_t val = conf_get_txn(conf, txn, C_SRV, C_UDP_WORKERS); + int64_t workers = conf_int(&val); + assert(workers <= CONF_MAX_UDP_WORKERS); + if (workers == YP_NIL) { + return MIN(dt_optimal_size(), CONF_MAX_UDP_WORKERS); + } + + return workers; +} + +size_t conf_tcp_threads_txn( + conf_t *conf, + knot_db_txn_t *txn) +{ + conf_val_t val = conf_get_txn(conf, txn, C_SRV, C_TCP_WORKERS); + int64_t workers = conf_int(&val); + assert(workers <= CONF_MAX_TCP_WORKERS); + if (workers == YP_NIL) { + size_t optimal = MAX(dt_optimal_size(), DFLT_MIN_TCP_WORKERS); + return MIN(optimal, CONF_MAX_TCP_WORKERS); + } + + return workers; +} + +size_t conf_xdp_threads_txn( + conf_t *conf, + knot_db_txn_t *txn) +{ + size_t workers = 0; + + conf_val_t val = conf_get_txn(conf, txn, C_XDP, C_LISTEN); + while (val.code == KNOT_EOK) { + struct sockaddr_storage addr = conf_addr(&val, NULL); + conf_xdp_iface_t iface; + int ret = conf_xdp_iface(&addr, &iface); + if (ret == KNOT_EOK) { + workers += iface.queues; + } + conf_val_next(&val); + } + + return workers; +} + +size_t conf_bg_threads_txn( + conf_t *conf, + knot_db_txn_t *txn) +{ + conf_val_t val = conf_get_txn(conf, txn, C_SRV, C_BG_WORKERS); + int64_t workers = conf_int(&val); + assert(workers <= CONF_MAX_BG_WORKERS); + if (workers == YP_NIL) { + assert(DFLT_MAX_BG_WORKERS <= CONF_MAX_BG_WORKERS); + return MIN(dt_optimal_size(), DFLT_MAX_BG_WORKERS); + } + + return workers; +} + +size_t conf_tcp_max_clients_txn( + conf_t *conf, + knot_db_txn_t *txn) +{ + conf_val_t val = conf_get_txn(conf, txn, C_SRV, C_TCP_MAX_CLIENTS); + int64_t clients = conf_int(&val); + if (clients == YP_NIL) { + static size_t permval = 0; + if (permval == 0) { + struct rlimit numfiles; + if (getrlimit(RLIMIT_NOFILE, &numfiles) == 0) { + permval = (size_t)numfiles.rlim_cur / 2; + } else { + permval = FALLBACK_MAX_TCP_CLIENTS; + } + } + return permval; + } + + return clients; +} + +int conf_user_txn( + conf_t *conf, + knot_db_txn_t *txn, + int *uid, + int *gid) +{ + if (uid == NULL || gid == NULL) { + return KNOT_EINVAL; + } + + conf_val_t val = conf_get_txn(conf, txn, C_SRV, C_USER); + if (val.code == KNOT_EOK) { + char *user = strdup(conf_str(&val)); + + // Search for user:group separator. + char *sep_pos = strchr(user, ':'); + if (sep_pos != NULL) { + // Process group name. + struct group *grp = getgrnam(sep_pos + 1); + if (grp != NULL) { + *gid = grp->gr_gid; + } else { + CONF_LOG(LOG_ERR, "invalid group name '%s'", + sep_pos + 1); + free(user); + return KNOT_EINVAL; + } + + // Cut off group part. + *sep_pos = '\0'; + } else { + *gid = getgid(); + } + + // Process user name. + struct passwd *pwd = getpwnam(user); + if (pwd != NULL) { + *uid = pwd->pw_uid; + } else { + CONF_LOG(LOG_ERR, "invalid user name '%s'", user); + free(user); + return KNOT_EINVAL; + } + + free(user); + return KNOT_EOK; + } else if (val.code == KNOT_ENOENT) { + *uid = getuid(); + *gid = getgid(); + return KNOT_EOK; + } else { + return val.code; + } +} + +conf_remote_t conf_remote_txn( + conf_t *conf, + knot_db_txn_t *txn, + conf_val_t *id, + size_t index) +{ + assert(id != NULL && id->item != NULL); + assert(id->item->type == YP_TSTR || + (id->item->type == YP_TREF && + id->item->var.r.ref->var.g.id->type == YP_TSTR)); + + conf_remote_t out = { { AF_UNSPEC } }; + + conf_val_t rundir_val = conf_get_txn(conf, txn, C_SRV, C_RUNDIR); + char *rundir = conf_abs_path(&rundir_val, NULL); + + // Get indexed remote address. + conf_val_t val = conf_id_get_txn(conf, txn, C_RMT, C_ADDR, id); + for (size_t i = 0; val.code == KNOT_EOK && i < index; i++) { + if (i == 0) { + conf_val(&val); + } + conf_val_next(&val); + } + // Index overflow causes empty socket. + out.addr = conf_addr(&val, rundir); + + // Get outgoing address if family matches (optional). + val = conf_id_get_txn(conf, txn, C_RMT, C_VIA, id); + while (val.code == KNOT_EOK) { + struct sockaddr_storage via = conf_addr(&val, rundir); + if (via.ss_family == out.addr.ss_family) { + out.via = conf_addr(&val, rundir); + break; + } + conf_val_next(&val); + } + + // Get TSIG key (optional). + conf_val_t key_id = conf_id_get_txn(conf, txn, C_RMT, C_KEY, id); + if (key_id.code == KNOT_EOK) { + out.key.name = (knot_dname_t *)conf_dname(&key_id); + + val = conf_id_get_txn(conf, txn, C_KEY, C_ALG, &key_id); + out.key.algorithm = conf_opt(&val); + + val = conf_id_get_txn(conf, txn, C_KEY, C_SECRET, &key_id); + out.key.secret.data = (uint8_t *)conf_bin(&val, &out.key.secret.size); + } + + free(rundir); + + val = conf_id_get_txn(conf, txn, C_RMT, C_BLOCK_NOTIFY_XFR, id); + out.block_notify_after_xfr = conf_bool(&val); + + val = conf_id_get_txn(conf, txn, C_RMT, C_NO_EDNS, id); + out.no_edns = conf_bool(&val); + + return out; +} + +int conf_xdp_iface( + struct sockaddr_storage *addr, + conf_xdp_iface_t *iface) +{ +#ifndef ENABLE_XDP + return KNOT_ENOTSUP; +#else + if (addr == NULL || iface == NULL) { + return KNOT_EINVAL; + } + + if (addr->ss_family == AF_UNIX) { + const char *addr_str = ((struct sockaddr_un *)addr)->sun_path; + strlcpy(iface->name, addr_str, sizeof(iface->name)); + + const char *port = strchr(addr_str, '@'); + if (port != NULL) { + iface->name[port - addr_str] = '\0'; + int ret = str_to_u16(port + 1, &iface->port); + if (ret != KNOT_EOK) { + return ret; + } else if (iface->port == 0) { + return KNOT_EINVAL; + } + } else { + iface->port = 53; + } + } else { + int ret = knot_eth_name_from_addr(addr, iface->name, sizeof(iface->name)); + if (ret != KNOT_EOK) { + return ret; + } + ret = sockaddr_port(addr); + if (ret < 0) { // Cannot check for 0 as don't know if port specified. + return KNOT_EINVAL; + } + iface->port = ret; + } + + int queues = knot_eth_queues(iface->name); + if (queues <= 0) { + assert(queues != 0); + return queues; + } + iface->queues = queues; + + return KNOT_EOK; +#endif +} diff --git a/src/knot/conf/conf.h b/src/knot/conf/conf.h new file mode 100644 index 0000000..83dfd1d --- /dev/null +++ b/src/knot/conf/conf.h @@ -0,0 +1,939 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <sys/socket.h> + +#include "knot/conf/base.h" +#include "knot/conf/schema.h" + +/*! Configuration remote getter output. */ +typedef struct { + /*! Target socket address. */ + struct sockaddr_storage addr; + /*! Local outgoing socket address. */ + struct sockaddr_storage via; + /*! TSIG key. */ + knot_tsig_key_t key; + /*! Suppress sending NOTIFY after zone transfer from this master. */ + bool block_notify_after_xfr; + /*! Disable EDNS on XFR queries. */ + bool no_edns; +} conf_remote_t; + +/*! Configuration section iterator. */ +typedef struct { + /*! Item description. */ + const yp_item_t *item; + /*! Namedb iterator. */ + knot_db_iter_t *iter; + /*! Key0 database code. */ + uint8_t key0_code; + // Public items. + /*! Iterator return code. */ + int code; +} conf_iter_t; + +/*! Configuration iterator over mixed references (e.g. remote and remotes). */ +typedef struct { + /*! Configuration context. */ + conf_t *conf; + /*! Mixed references. */ + conf_val_t *mix_id; + /*! Temporary nested references. */ + conf_val_t sub_id; + /*! Current (possibly expanded) reference to use. */ + conf_val_t *id; + /*! Nested references in use indication. */ + bool nested; +} conf_mix_iter_t; + +/*! Configuration module getter output. */ +typedef struct { + /*! Module name. */ + yp_name_t *name; + /*! Module id data. */ + uint8_t *data; + /*! Module id data length. */ + size_t len; +} conf_mod_id_t; + +/*! + * Check if the configuration database exists on the filesystem. + * + * \param[in] db_dir Database path. + * + * \return True if it already exists. + */ + +bool conf_db_exists( + const char *db_dir +); + +/*! + * Gets the configuration item value of the section without identifiers. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0_name Section name. + * \param[in] key1_name Item name. + * + * \return Item value. + */ +conf_val_t conf_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name, + const yp_name_t *key1_name +); +static inline conf_val_t conf_get( + conf_t *conf, + const yp_name_t *key0_name, + const yp_name_t *key1_name) +{ + return conf_get_txn(conf, &conf->read_txn, key0_name, key1_name); +} + +/*! + * Gets the configuration item value of the section with identifiers (raw version). + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0_name Section name. + * \param[in] key1_name Item name. + * \param[in] id Section identifier (raw value). + * \param[in] id_len Length of the section identifier. + * + * \return Item value. + */ +conf_val_t conf_rawid_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name, + const yp_name_t *key1_name, + const uint8_t *id, + size_t id_len +); +static inline conf_val_t conf_rawid_get( + conf_t *conf, + const yp_name_t *key0_name, + const yp_name_t *key1_name, + const uint8_t *id, + size_t id_len) +{ + return conf_rawid_get_txn(conf, &conf->read_txn, key0_name, key1_name, + id, id_len); +} + +/*! + * Gets the configuration item value of the section with identifiers. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0_name Section name. + * \param[in] key1_name Item name. + * \param[in] id Section identifier (output of a config getter). + * + * \return Item value. + */ +conf_val_t conf_id_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name, + const yp_name_t *key1_name, + conf_val_t *id +); +static inline conf_val_t conf_id_get( + conf_t *conf, + const yp_name_t *key0_name, + const yp_name_t *key1_name, + conf_val_t *id) +{ + return conf_id_get_txn(conf, &conf->read_txn, key0_name, key1_name, id); +} + +/*! + * Gets the configuration item value of the module section. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key1_name Item name. + * \param[in] mod_id Module identifier. + * + * \return Item value. + */ +conf_val_t conf_mod_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key1_name, + const conf_mod_id_t *mod_id +); +static inline conf_val_t conf_mod_get( + conf_t *conf, + const yp_name_t *key1_name, + const conf_mod_id_t *mod_id) +{ + return conf_mod_get_txn(conf, &conf->read_txn, key1_name, mod_id); +} + +/*! + * Gets the configuration item value of the zone section. + * + * \note A possibly associated template is taken into account. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key1_name Item name. + * \param[in] dname Zone name. + * + * \return Item value. + */ +conf_val_t conf_zone_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key1_name, + const knot_dname_t *dname +); +static inline conf_val_t conf_zone_get( + conf_t *conf, + const yp_name_t *key1_name, + const knot_dname_t *dname) +{ + return conf_zone_get_txn(conf, &conf->read_txn, key1_name, dname); +} + +/*! + * Gets the configuration item value of the default template. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key1_name Item name. + * + * \return Item value. + */ +conf_val_t conf_default_get_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key1_name +); +static inline conf_val_t conf_default_get( + conf_t *conf, + const yp_name_t *key1_name) +{ + return conf_default_get_txn(conf, &conf->read_txn, key1_name); +} + +/*! + * Checks the configuration section for the identifier (raw version). + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0_name Section name. + * \param[in] id Section identifier (raw value). + * \param[in] id_len Length of the section identifier. + * + * \return True if exists. + */ +bool conf_rawid_exists_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name, + const uint8_t *id, + size_t id_len +); +static inline bool conf_rawid_exists( + conf_t *conf, + const yp_name_t *key0_name, + const uint8_t *id, + size_t id_len) +{ + return conf_rawid_exists_txn(conf, &conf->read_txn, key0_name, id, id_len); +} + +/*! + * Checks the configuration section for the identifier. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0_name Section name. + * \param[in] id Section identifier (output of a config getter). + * + * \return True if exists. + */ +bool conf_id_exists_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name, + conf_val_t *id +); +static inline bool conf_id_exists( + conf_t *conf, + const yp_name_t *key0_name, + conf_val_t *id) +{ + return conf_id_exists_txn(conf, &conf->read_txn, key0_name, id); +} + +/*! + * Gets the number of section identifiers. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0_name Section name. + * + * \return Number of identifiers. + */ +size_t conf_id_count_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name +); +static inline size_t conf_id_count( + conf_t *conf, + const yp_name_t *key0_name) +{ + return conf_id_count_txn(conf, &conf->read_txn, key0_name); +} + +/*! + * Gets a configuration section iterator. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0_name Section name. + * + * \return Section iterator. + */ +conf_iter_t conf_iter_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0_name +); +static inline conf_iter_t conf_iter( + conf_t *conf, + const yp_name_t *key0_name) +{ + return conf_iter_txn(conf, &conf->read_txn, key0_name); +} + +/*! + * Moves the configuration section iterator to the next identifier. + * + * \param[in] conf Configuration. + * \param[in] iter Configuration iterator. + */ +void conf_iter_next( + conf_t *conf, + conf_iter_t *iter +); + +/*! + * Gets the current iterator value (identifier). + * + * \param[in] conf Configuration. + * \param[in] iter Configuration iterator. + * + * \return Section identifier. + */ +conf_val_t conf_iter_id( + conf_t *conf, + conf_iter_t *iter +); + +/*! + * Deletes the section iterator. + * + * This function should be called when the iterating is early interrupted, + * otherwise this is done automatically at KNOT_EOF. + * + * \param[in] conf Configuration. + * \param[in] iter Configuration iterator. + */ +void conf_iter_finish( + conf_t *conf, + conf_iter_t *iter +); + +/*! + * Prepares the value for the direct access. + * + * The following access is through val->len and val->data. + * + * \param[in] val Item value. + */ +void conf_val( + conf_val_t *val +); + +/*! + * Moves to the next value of a multi-valued item. + * + * \param[in] val Item value. + */ +void conf_val_next( + conf_val_t *val +); + +/*! + * Resets to the first value of a multi-valued item. + * + * \param[in] val Item value. + */ +void conf_val_reset( + conf_val_t *val +); + +/*! + * Gets the number of values if multivalued item. + * + * \param[in] val Item value. + * + * \return Number of values. + */ +size_t conf_val_count( + conf_val_t *val +); + +/*! + * Checks if two item values are equal. + * + * \param[in] val1 First item value. + * \param[in] val2 Second item value. + * + * \return true if equal, false if not. + */ +bool conf_val_equal( + conf_val_t *val1, + conf_val_t *val2 +); + +/*! + * Initializes a mixed reference iterator. + * + * The following access is through iter->id. + * + * \param[in] conf Configuration. + * \param[in] mix_id First mixed reference. + * \param[out] iter Iterator to be initialized. + */ +void conf_mix_iter_init( + conf_t *conf, + conf_val_t *mix_id, + conf_mix_iter_t *iter +); + +/*! + * Increments the mixed iterator. + * + * \param[in] iter Mixed reference iterator. + */ +void conf_mix_iter_next( + conf_mix_iter_t *iter +); + +/*! + * Gets the numeric value of the item. + * + * \param[in] val Item value. + * + * \return Integer. + */ +int64_t conf_int( + conf_val_t *val +); + +/*! + * Gets the boolean value of the item. + * + * \param[in] val Item value. + * + * \return Boolean. + */ +bool conf_bool( + conf_val_t *val +); + +/*! + * Gets the option value of the item. + * + * \param[in] val Item value. + * + * \return Option id. + */ +unsigned conf_opt( + conf_val_t *val +); + +/*! + * Gets the string value of the item. + * + * \param[in] val Item value. + * + * \return String pointer. + */ +const char* conf_str( + conf_val_t *val +); + +/*! + * Gets the dname value of the item. + * + * \param[in] val Item value. + * + * \return Dname pointer. + */ +const knot_dname_t* conf_dname( + conf_val_t *val +); + +/*! + * Gets the length-prefixed data value of the item. + * + * \param[in] val Item value. + * \param[out] len Output length. + * + * \return Data pointer. + */ +const uint8_t* conf_bin( + conf_val_t *val, + size_t *len +); + +/*! + * Gets the generic data value of the item. + * + * \param[in] val Item value. + * \param[out] len Output length. + * + * \return Data pointer. + */ +const uint8_t* conf_data( + conf_val_t *val, + size_t *len +); + +/*! + * Gets the socket address value of the item. + * + * \param[in] val Item value. + * \param[in] sock_base_dir Path prefix for a relative UNIX socket location. + * + * \return Socket address. + */ +struct sockaddr_storage conf_addr( + conf_val_t *val, + const char *sock_base_dir +); + +/*! + * Checks the configured address if equal to given one (except port). + * + * \param[in] match Configured address. + * \param[in] addr Address to check. + * + * \return True if matches. + */ +bool conf_addr_match( + conf_val_t *match, + const struct sockaddr_storage *addr +); + +/*! + * Gets the socket address range value of the item. + * + * \param[in] val Item value. + * \param[out] max_ss Upper address bound or AF_UNSPEC family if not specified. + * \param[out] prefix_len Network subnet prefix length or -1 if not specified. + * + * \return Socket address. + */ +struct sockaddr_storage conf_addr_range( + conf_val_t *val, + struct sockaddr_storage *max_ss, + int *prefix_len +); + +/*! + * Checks the address if matches given address range/network block. + * + * \param[in] range Address range/network block. + * \param[in] addr Address to check. + * + * \return True if matches. + */ +bool conf_addr_range_match( + conf_val_t *range, + const struct sockaddr_storage *addr +); + +/*! + * Gets the absolute string value of the item. + * + * \note The result must be explicitly deallocated. + * + * \param[in] val Item value. + * \param[in] base_dir Path prefix for a relative string. + * + * \return Absolute path string pointer. + */ +char* conf_abs_path( + conf_val_t *val, + const char *base_dir +); + +/*! + * Ensures empty 'default' identifier value. + * + * \param[in] val Item value. + * + * \return Empty item value. + */ +static inline void conf_id_fix_default(conf_val_t *val) +{ + if (val->code != KNOT_EOK) { + conf_val_t empty = { + .item = val->item, + .code = KNOT_EOK + }; + + *val = empty; + } +} + +/*! + * Gets the module identifier value of the item. + * + * \param[in] val Item value. + * + * \return Module identifier. + */ +conf_mod_id_t* conf_mod_id( + conf_val_t *val +); + +/*! + * Destroys the module identifier. + * + * \param[in] mod_id Module identifier. + */ +void conf_free_mod_id( + conf_mod_id_t *mod_id +); + +/*! + * Gets the absolute zone file path. + * + * \note The result must be explicitly deallocated. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] zone Zone name. + * + * \return Absolute zone file path string pointer. + */ +char* conf_zonefile_txn( + conf_t *conf, + knot_db_txn_t *txn, + const knot_dname_t *zone +); +static inline char* conf_zonefile( + conf_t *conf, + const knot_dname_t *zone) +{ + return conf_zonefile_txn(conf, &conf->read_txn, zone); +} + +/*! + * Gets the absolute directory path for a database. + * + * e.g. Journal, KASP db, Timers + * + * \note The result must be explicitly deallocated. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] db_type Database name. + * + * \return Absolute database path string pointer. + */ +char* conf_db_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *db_type +); +static inline char* conf_db( + conf_t *conf, + const yp_name_t *db_type) +{ + return conf_db_txn(conf, &conf->read_txn, db_type); +} + +/*! + * Gets the absolute directory path for a TLS key/cert file. + * + * \note The result must be explicitly deallocated. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] db_type TLS configuration option. + * + * \return Absolute path string pointer. + */ +char *conf_tls_txn( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *tls_item); +static inline char* conf_tls( + conf_t *conf, + const yp_name_t *tls_item) +{ + return conf_tls_txn(conf, &conf->read_txn, tls_item); +} + +/*! + * Gets database-specific parameter. + * + * \param[in] conf Configuration. + * \param[in] param Parameter name. + * + * \return Item value. + */ +static inline conf_val_t conf_db_param( + conf_t *conf, + const yp_name_t *param) +{ + return conf_get_txn(conf, &conf->read_txn, C_DB, param); +} + +/*! + * Gets the configured setting of the bool option in the specified section. + * + * \param[in] conf Configuration. + * \param[in] section Section name. + * \param[in] param Parameter name. + * + * \return True if enabled, false otherwise. + */ +static inline bool conf_get_bool( + conf_t *conf, + const yp_name_t *section, + const yp_name_t *param) +{ + conf_val_t val = conf_get_txn(conf, &conf->read_txn, section, param); + return conf_bool(&val); +} + +/*! + * Gets the configured setting of the int option in the specified section. + * + * \param[in] conf Configuration. + * \param[in] section Section name. + * \param[in] param Parameter name. + * + * \return Configured integer value. + */ +static inline int64_t conf_get_int( + conf_t *conf, + const yp_name_t *section, + const yp_name_t *param) +{ + conf_val_t val = conf_get_txn(conf, &conf->read_txn, section, param); + return conf_int(&val); +} + +/*! + * Gets the configured number of UDP threads. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * + * \return Number of threads. + */ +size_t conf_udp_threads_txn( + conf_t *conf, + knot_db_txn_t *txn +); +static inline size_t conf_udp_threads( + conf_t *conf) +{ + return conf_udp_threads_txn(conf, &conf->read_txn); +} + +/*! + * Gets the configured number of TCP threads. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * + * \return Number of threads. + */ +size_t conf_tcp_threads_txn( + conf_t *conf, + knot_db_txn_t *txn +); +static inline size_t conf_tcp_threads( + conf_t *conf) +{ + return conf_tcp_threads_txn(conf, &conf->read_txn); +} + +/*! + * Gets the number of used XDP threads. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * + * \return Number of threads. + */ +size_t conf_xdp_threads_txn( + conf_t *conf, + knot_db_txn_t *txn +); +static inline size_t conf_xdp_threads( + conf_t *conf) +{ + return conf_xdp_threads_txn(conf, &conf->read_txn); +} + +/*! + * Gets the configured number of worker threads. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * + * \return Number of threads. + */ +size_t conf_bg_threads_txn( + conf_t *conf, + knot_db_txn_t *txn +); +static inline size_t conf_bg_threads( + conf_t *conf) +{ + return conf_bg_threads_txn(conf, &conf->read_txn); +} + +/*! + * Gets the required LMDB readers limit based on the current configuration. + * + * \note The resulting value is a common limit to journal, kasp, timers, + * and catalog databases. So it's over-estimated for simplicity reasons. + * + * \note This function cannot be used for the configuration database setting :-/ + * + * \param[in] conf Configuration. + * + * \return Number of readers. + */ +static inline size_t conf_lmdb_readers( + conf_t *conf) +{ + if (conf == NULL) { // Return default in tests. + return 126; + } + return conf_udp_threads(conf) + conf_tcp_threads(conf) + + conf_bg_threads(conf) + conf_xdp_threads(conf) + 2; // Main thread, utils. +} + +/*! + * Gets the configured maximum number of TCP clients. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * + * \return Maximum number of TCP clients. + */ +size_t conf_tcp_max_clients_txn( + conf_t *conf, + knot_db_txn_t *txn +); +static inline size_t conf_tcp_max_clients( + conf_t *conf) +{ + return conf_tcp_max_clients_txn(conf, &conf->read_txn); +} + +/*! + * Gets the configured user and group identifiers. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[out] uid User identifier. + * \param[out] gid Group identifier. + * + * \return Knot error code. + */ +int conf_user_txn( + conf_t *conf, + knot_db_txn_t *txn, + int *uid, + int *gid +); +static inline int conf_user( + conf_t *conf, + int *uid, + int *gid) +{ + return conf_user_txn(conf, &conf->read_txn, uid, gid); +} + +/*! + * Gets the remote parameters for the given identifier. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] id Remote identifier. + * \param[in] index Remote index (counted from 0). + * + * \return Remote parameters. + */ +conf_remote_t conf_remote_txn( + conf_t *conf, + knot_db_txn_t *txn, + conf_val_t *id, + size_t index +); +static inline conf_remote_t conf_remote( + conf_t *conf, + conf_val_t *id, + size_t index) +{ + return conf_remote_txn(conf, &conf->read_txn, id, index); +} + +/*! XDP interface parameters. */ +typedef struct { + /*! Interface name. */ + char name[32]; + /*! UDP port to listen on. */ + uint16_t port; + /*! Number of active IO queues. */ + uint16_t queues; +} conf_xdp_iface_t; + +/*! + * Gets the XDP interface parameters for a given configuration value. + * + * \param[in] addr XDP interface name stored in the configuration. + * \param[out] iface Interface parameters. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_xdp_iface( + struct sockaddr_storage *addr, + conf_xdp_iface_t *iface +); diff --git a/src/knot/conf/confdb.c b/src/knot/conf/confdb.c new file mode 100644 index 0000000..e1262c2 --- /dev/null +++ b/src/knot/conf/confdb.c @@ -0,0 +1,951 @@ +/* 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 <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +#include "knot/conf/confdb.h" +#include "libknot/errcode.h" +#include "libknot/yparser/yptrafo.h" +#include "contrib/openbsd/strlcpy.h" +#include "contrib/wire_ctx.h" + +/* + * A simple configuration: + * + * server.identity: "knot" + * server.version: "version" + * template[tpl1].storage: "directory1" + * template[tpl2].storage: "directory2" + * template[tpl2].master: [ "master1", "master2" ] + * + * And the corresponding configuration DB content: + * + * # DB structure version. + * [00][FF]: [02] + * # Sections codes. + * [00][00]server: [02] + * [00][00]template: [03] + * # Server section items codes. + * [02][00]identity: [02] + * [02][00]version: [03] + * # Server items values. + * [02][02]: knot\0 + * [02][03]: version\0 + * # Template section items codes. + * [03][00]master: [03] + * [03][00]storage: [02] + * # Template identifiers. + * [03][01]tpl1\0 + * [03][01]tpl2\0 + * # Template items values. + * [03][02]tpl1\0: directory1\0 + * [03][02]tpl2\0: directory2\0 + * [03][03]tpl2\0: [00][08]master1\0 [00][08]master2\0 + */ + +typedef enum { + KEY0_ROOT = 0, + KEY1_ITEMS = 0, + KEY1_ID = 1, + KEY1_FIRST = 2, + KEY1_LAST = 200, + KEY1_VERSION = 255 +} db_code_t; + +typedef enum { + KEY0_POS = 0, + KEY1_POS = 1, + NAME_POS = 2 +} db_code_pos_t; + +typedef enum { + DB_GET, + DB_SET, + DB_DEL +} db_action_t; + +static int db_check_version( + conf_t *conf, + knot_db_txn_t *txn) +{ + uint8_t k[2] = { KEY0_ROOT, KEY1_VERSION }; + knot_db_val_t key = { k, sizeof(k) }; + knot_db_val_t data; + + // Get conf-DB version. + int ret = conf->api->find(txn, &key, &data, 0); + if (ret != KNOT_EOK) { + return ret; + } + + // Check conf-DB version. + if (data.len != 1 || ((uint8_t *)data.data)[0] != CONF_DB_VERSION) { + return KNOT_CONF_EVERSION; + } + + return KNOT_EOK; +} + +int conf_db_init( + conf_t *conf, + knot_db_txn_t *txn, + bool purge) +{ + if (conf == NULL || txn == NULL) { + return KNOT_EINVAL; + } + + uint8_t k[2] = { KEY0_ROOT, KEY1_VERSION }; + knot_db_val_t key = { k, sizeof(k) }; + + int ret = conf->api->count(txn); + if (ret == 0) { // Initialize empty DB with DB version. + uint8_t d[1] = { CONF_DB_VERSION }; + knot_db_val_t data = { d, sizeof(d) }; + return conf->api->insert(txn, &key, &data, 0); + } else if (ret > 0) { // Non-empty DB. + if (purge) { + // Purge the DB. + ret = conf->api->clear(txn); + if (ret != KNOT_EOK) { + return ret; + } + return conf_db_init(conf, txn, false); + } + return KNOT_EOK; + } else { // DB error. + return ret; + } +} + +int conf_db_check( + conf_t *conf, + knot_db_txn_t *txn) +{ + int ret = conf->api->count(txn); + if (ret == 0) { // Not initialized DB. + return KNOT_CONF_ENOTINIT; + } else if (ret > 0) { // Check the DB. + int count = ret; + + ret = db_check_version(conf, txn); + if (ret != KNOT_EOK) { + return ret; + } else if (count == 1) { + return KNOT_EOK; // Empty but initialized DB. + } else { + return count - 1; // Non-empty DB. + } + } else { // DB error. + return ret; + } +} + +static int db_code( + conf_t *conf, + knot_db_txn_t *txn, + uint8_t section_code, + const yp_name_t *name, + db_action_t action, + uint8_t *code) +{ + if (name == NULL) { + return KNOT_EINVAL; + } + + knot_db_val_t key; + uint8_t k[CONF_MIN_KEY_LEN + YP_MAX_ITEM_NAME_LEN]; + k[KEY0_POS] = section_code; + k[KEY1_POS] = KEY1_ITEMS; + memcpy(k + NAME_POS, name + 1, name[0]); + key.data = k; + key.len = CONF_MIN_KEY_LEN + name[0]; + + // Check if the item is already registered. + knot_db_val_t data; + int ret = conf->api->find(txn, &key, &data, 0); + switch (ret) { + case KNOT_EOK: + if (action == DB_DEL) { + return conf->api->del(txn, &key); + } + if (code != NULL) { + *code = ((uint8_t *)data.data)[0]; + } + return KNOT_EOK; + case KNOT_ENOENT: + if (action != DB_SET) { + return KNOT_ENOENT; + } + break; + default: + return ret; + } + + // Reduce the key to common prefix only. + key.len = CONF_MIN_KEY_LEN; + + bool codes[KEY1_LAST + 1] = { false }; + + // Find all used item codes. + knot_db_iter_t *it = conf->api->iter_begin(txn, KNOT_DB_NOOP); + it = conf->api->iter_seek(it, &key, KNOT_DB_GEQ); + while (it != NULL) { + knot_db_val_t iter_key; + ret = conf->api->iter_key(it, &iter_key); + if (ret != KNOT_EOK) { + conf->api->iter_finish(it); + return ret; + } + uint8_t *key_data = (uint8_t *)iter_key.data; + + // Check for database prefix end. + if (key_data[KEY0_POS] != k[KEY0_POS] || + key_data[KEY1_POS] != k[KEY1_POS]) { + break; + } + + knot_db_val_t iter_val; + ret = conf->api->iter_val(it, &iter_val); + if (ret != KNOT_EOK) { + conf->api->iter_finish(it); + return ret; + } + uint8_t used_code = ((uint8_t *)iter_val.data)[0]; + codes[used_code] = true; + + it = conf->api->iter_next(it); + } + conf->api->iter_finish(it); + + // Find the smallest unused item code. + uint8_t new_code = KEY1_FIRST; + while (codes[new_code]) { + new_code++; + if (new_code > KEY1_LAST) { + return KNOT_ESPACE; + } + } + + // Restore the full key. + key.len = CONF_MIN_KEY_LEN + name[0]; + + // Fill the data with a new code. + data.data = &new_code; + data.len = sizeof(new_code); + + // Register new item code. + ret = conf->api->insert(txn, &key, &data, 0); + if (ret != KNOT_EOK) { + return ret; + } + + if (code != NULL) { + *code = new_code; + } + + return KNOT_EOK; +} + +static uint8_t *find_data( + const knot_db_val_t *value, + const knot_db_val_t *current) +{ + wire_ctx_t ctx = wire_ctx_init_const(current->data, current->len); + + // Loop over the data array. Each item has 2B length prefix. + while (wire_ctx_available(&ctx) > 0) { + uint16_t len = wire_ctx_read_u16(&ctx); + assert(ctx.error == KNOT_EOK); + + // Check for the same data. + if (len == value->len && + (len == 0 || memcmp(ctx.position, value->data, value->len) == 0)) { + wire_ctx_skip(&ctx, -sizeof(uint16_t)); + assert(ctx.error == KNOT_EOK); + return ctx.position; + } + wire_ctx_skip(&ctx, len); + } + + assert(ctx.error == KNOT_EOK && wire_ctx_available(&ctx) == 0); + + return NULL; +} + +static int db_set( + conf_t *conf, + knot_db_txn_t *txn, + knot_db_val_t *key, + knot_db_val_t *data, + bool multi) +{ + if (!multi) { + if (data->len > CONF_MAX_DATA_LEN) { + return KNOT_ERANGE; + } + + // Insert new (overwrite old) data. + return conf->api->insert(txn, key, data, 0); + } + + knot_db_val_t d; + + if (data->len > UINT16_MAX) { + return KNOT_ERANGE; + } + + int ret = conf->api->find(txn, key, &d, 0); + if (ret == KNOT_ENOENT) { + d.len = 0; + } else if (ret == KNOT_EOK) { + // Check for duplicate data. + if (find_data(data, &d) != NULL) { + return KNOT_EOK; + } + } else { + return ret; + } + + // Prepare buffer for all data. + size_t new_len = d.len + sizeof(uint16_t) + data->len; + if (new_len > CONF_MAX_DATA_LEN) { + return KNOT_ESPACE; + } + + uint8_t *new_data = malloc(new_len); + if (new_data == NULL) { + return KNOT_ENOMEM; + } + + wire_ctx_t ctx = wire_ctx_init(new_data, new_len); + + // Copy current data array. + wire_ctx_write(&ctx, d.data, d.len); + // Copy length prefix for the new data item. + wire_ctx_write_u16(&ctx, data->len); + // Copy the new data item. + wire_ctx_write(&ctx, data->data, data->len); + + assert(ctx.error == KNOT_EOK && wire_ctx_available(&ctx) == 0); + + d.data = new_data; + d.len = new_len; + + // Insert new (or append) data. + ret = conf->api->insert(txn, key, &d, 0); + + free(new_data); + + return ret; +} + +int conf_db_set( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0, + const yp_name_t *key1, + const uint8_t *id, + size_t id_len, + const uint8_t *data, + size_t data_len) +{ + if (conf == NULL || txn == NULL || key0 == NULL || + (id == NULL && id_len > 0) || (data == NULL && data_len > 0)) { + return KNOT_EINVAL; + } + + // Check for valid keys. + const yp_item_t *item = yp_schema_find(key1 != NULL ? key1 : key0, + key1 != NULL ? key0 : NULL, + conf->schema); + if (item == NULL) { + return KNOT_YP_EINVAL_ITEM; + } + + // Ignore alone key0 insertion. + if (key1 == NULL && id_len == 0) { + return KNOT_EOK; + } + + // Ignore group id as a key1. + if (item->parent != NULL && (item->parent->flags & YP_FMULTI) != 0 && + item->parent->var.g.id == item) { + key1 = NULL; + } + + uint8_t k[CONF_MAX_KEY_LEN] = { 0 }; + knot_db_val_t key = { k, CONF_MIN_KEY_LEN }; + + // Set key0 code. + int ret = db_code(conf, txn, KEY0_ROOT, key0, DB_SET, &k[KEY0_POS]); + if (ret != KNOT_EOK) { + return ret; + } + + // Set id part. + if (id_len > 0) { + if (id_len > YP_MAX_ID_LEN) { + return KNOT_YP_EINVAL_ID; + } + memcpy(k + CONF_MIN_KEY_LEN, id, id_len); + key.len += id_len; + + k[KEY1_POS] = KEY1_ID; + knot_db_val_t val = { NULL }; + + // Insert id. + if (key1 == NULL) { + ret = conf->api->find(txn, &key, &val, 0); + if (ret == KNOT_EOK) { + return KNOT_CONF_EREDEFINE; + } + ret = db_set(conf, txn, &key, &val, false); + if (ret != KNOT_EOK) { + return ret; + } + // Check for existing id. + } else { + ret = conf->api->find(txn, &key, &val, 0); + if (ret != KNOT_EOK) { + return KNOT_YP_EINVAL_ID; + } + } + } + + // Insert key1 data. + if (key1 != NULL) { + // Set key1 code. + ret = db_code(conf, txn, k[KEY0_POS], key1, DB_SET, &k[KEY1_POS]); + if (ret != KNOT_EOK) { + return ret; + } + + knot_db_val_t val = { (uint8_t *)data, data_len }; + ret = db_set(conf, txn, &key, &val, item->flags & YP_FMULTI); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int db_unset( + conf_t *conf, + knot_db_txn_t *txn, + knot_db_val_t *key, + knot_db_val_t *data, + bool multi) +{ + // No item data can be zero length. + if (data->len == 0) { + return conf->api->del(txn, key); + } + + knot_db_val_t d; + + int ret = conf->api->find(txn, key, &d, 0); + if (ret != KNOT_EOK) { + return ret; + } + + // Process singlevalued data. + if (!multi) { + if (d.len != data->len || + memcmp((uint8_t *)d.data, data->data, d.len) != 0) { + return KNOT_ENOENT; + } + return conf->api->del(txn, key); + } + + // Check if the data exists. + uint8_t *pos = find_data(data, &d); + if (pos == NULL) { + return KNOT_ENOENT; + } + + // Prepare buffer for reduced data. + size_t total_len = d.len - sizeof(uint16_t) - data->len; + if (total_len == 0) { + return conf->api->del(txn, key); + } + + uint8_t *new_data = malloc(total_len); + if (new_data == NULL) { + return KNOT_ENOMEM; + } + + size_t new_len = 0; + + // Copy leading data block. + assert(pos >= (uint8_t *)d.data); + size_t head_len = pos - (uint8_t *)d.data; + if (head_len > 0) { + memcpy(new_data, d.data, head_len); + new_len += head_len; + } + + pos += sizeof(uint16_t) + data->len; + + // Copy trailing data block. + assert(pos <= (uint8_t *)d.data + d.len); + size_t tail_len = (uint8_t *)d.data + d.len - pos; + if (tail_len > 0) { + memcpy(new_data + new_len, pos, tail_len); + new_len += tail_len; + } + + d.data = new_data; + d.len = new_len; + + // Insert reduced data. + ret = conf->api->insert(txn, key, &d, 0); + + free(new_data); + + return ret; +} + +int conf_db_unset( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0, + const yp_name_t *key1, + const uint8_t *id, + size_t id_len, + const uint8_t *data, + size_t data_len, + bool delete_key1) +{ + if (conf == NULL || txn == NULL || key0 == NULL || + (id == NULL && id_len > 0) || (data == NULL && data_len > 0)) { + return KNOT_EINVAL; + } + + // Check for valid keys. + const yp_item_t *item = yp_schema_find(key1 != NULL ? key1 : key0, + key1 != NULL ? key0 : NULL, + conf->schema); + if (item == NULL) { + return KNOT_YP_EINVAL_ITEM; + } + + // Delete the key0. + if (key1 == NULL && id_len == 0) { + return db_code(conf, txn, KEY0_ROOT, key0, DB_DEL, NULL); + } + + // Ignore group id as a key1. + if (item->parent != NULL && (item->parent->flags & YP_FMULTI) != 0 && + item->parent->var.g.id == item) { + key1 = NULL; + } + + uint8_t k[CONF_MAX_KEY_LEN] = { 0 }; + knot_db_val_t key = { k, CONF_MIN_KEY_LEN }; + + // Set the key0 code. + int ret = db_code(conf, txn, KEY0_ROOT, key0, DB_GET, &k[KEY0_POS]); + if (ret != KNOT_EOK) { + return ret; + } + + // Set the id part. + if (id_len > 0) { + if (id_len > YP_MAX_ID_LEN) { + return KNOT_YP_EINVAL_ID; + } + memcpy(k + CONF_MIN_KEY_LEN, id, id_len); + key.len += id_len; + + k[KEY1_POS] = KEY1_ID; + knot_db_val_t val = { NULL }; + + // Delete the id. + if (key1 == NULL) { + return conf->api->del(txn, &key); + // Check for existing id. + } else { + ret = conf->api->find(txn, &key, &val, 0); + if (ret != KNOT_EOK) { + return KNOT_YP_EINVAL_ID; + } + } + } + + if (key1 != NULL) { + // Set the key1 code. + ret = db_code(conf, txn, k[KEY0_POS], key1, DB_GET, &k[KEY1_POS]); + if (ret != KNOT_EOK) { + return ret; + } + + // Delete the key1. + if (data_len == 0 && delete_key1) { + ret = db_code(conf, txn, k[KEY0_POS], key1, DB_DEL, NULL); + if (ret != KNOT_EOK) { + return ret; + } + // Delete the item data. + } else { + knot_db_val_t val = { (uint8_t *)data, data_len }; + ret = db_unset(conf, txn, &key, &val, item->flags & YP_FMULTI); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + return KNOT_EOK; +} + +int conf_db_get( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0, + const yp_name_t *key1, + const uint8_t *id, + size_t id_len, + conf_val_t *data) +{ + conf_val_t out = { NULL }; + + if (conf == NULL || txn == NULL || key0 == NULL || + (id == NULL && id_len > 0)) { + out.code = KNOT_EINVAL; + goto get_error; + } + + // Check for valid keys. + out.item = yp_schema_find(key1 != NULL ? key1 : key0, + key1 != NULL ? key0 : NULL, + conf->schema); + if (out.item == NULL) { + out.code = KNOT_YP_EINVAL_ITEM; + goto get_error; + } + + // At least key1 or id must be specified. + if (key1 == NULL && id_len == 0) { + out.code = KNOT_EINVAL; + goto get_error; + } + + // Ignore group id as a key1. + if (out.item->parent != NULL && (out.item->parent->flags & YP_FMULTI) != 0 && + out.item->parent->var.g.id == out.item) { + key1 = NULL; + } + + uint8_t k[CONF_MAX_KEY_LEN] = { 0 }; + knot_db_val_t key = { k, CONF_MIN_KEY_LEN }; + knot_db_val_t val = { NULL }; + + // Set the key0 code. + out.code = db_code(conf, txn, KEY0_ROOT, key0, DB_GET, &k[KEY0_POS]); + if (out.code != KNOT_EOK) { + if (id_len > 0) { + out.code = KNOT_YP_EINVAL_ID; + } + goto get_error; + } + + // Set the id part. + if (id_len > 0) { + if (id_len > YP_MAX_ID_LEN) { + out.code = KNOT_YP_EINVAL_ID; + goto get_error; + } + memcpy(k + CONF_MIN_KEY_LEN, id, id_len); + key.len += id_len; + + k[KEY1_POS] = KEY1_ID; + + // Check for existing id. + out.code = conf->api->find(txn, &key, &val, 0); + if (out.code != KNOT_EOK) { + out.code = KNOT_YP_EINVAL_ID; + goto get_error; + } + } + + // Set the key1 code. + if (key1 != NULL) { + out.code = db_code(conf, txn, k[KEY0_POS], key1, DB_GET, &k[KEY1_POS]); + if (out.code != KNOT_EOK) { + goto get_error; + } + } + + // Get the data. + out.code = conf->api->find(txn, &key, &val, 0); + if (out.code == KNOT_EOK) { + out.blob = val.data; + out.blob_len = val.len; + } +get_error: + // Set the output. + if (data != NULL) { + *data = out; + } + + return out.code; +} + +static int check_iter( + conf_t *conf, + conf_iter_t *iter) +{ + knot_db_val_t key; + + // Get the current key. + int ret = conf->api->iter_key(iter->iter, &key); + if (ret != KNOT_EOK) { + return KNOT_ENOENT; + } + uint8_t *key_data = (uint8_t *)key.data; + + // Check for key overflow. + if (key_data[KEY0_POS] != iter->key0_code || key_data[KEY1_POS] != KEY1_ID) { + return KNOT_EOF; + } + + return KNOT_EOK; +} + +int conf_db_iter_begin( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0, + conf_iter_t *iter) +{ + conf_iter_t out = { NULL }; + + if (conf == NULL || txn == NULL || key0 == NULL || iter == NULL) { + out.code = KNOT_EINVAL; + goto iter_begin_error; + } + + // Look-up group id item in the schema. + const yp_item_t *grp = yp_schema_find(key0, NULL, conf->schema); + if (grp == NULL) { + out.code = KNOT_YP_EINVAL_ITEM; + goto iter_begin_error; + } + if (grp->type != YP_TGRP || (grp->flags & YP_FMULTI) == 0) { + out.code = KNOT_ENOTSUP; + goto iter_begin_error; + } + out.item = grp->var.g.id; + + // Get key0 code. + out.code = db_code(conf, txn, KEY0_ROOT, key0, DB_GET, &out.key0_code); + if (out.code != KNOT_EOK) { + goto iter_begin_error; + } + + // Prepare key prefix. + uint8_t k[2] = { out.key0_code, KEY1_ID }; + knot_db_val_t key = { k, sizeof(k) }; + + // Get the data. + out.iter = conf->api->iter_begin(txn, KNOT_DB_NOOP); + out.iter = conf->api->iter_seek(out.iter, &key, KNOT_DB_GEQ); + + // Check for no section id. + out.code = check_iter(conf, &out); + if (out.code != KNOT_EOK) { + out.code = KNOT_ENOENT; // Treat all errors as no entry. + conf_db_iter_finish(conf, &out); + goto iter_begin_error; + } + +iter_begin_error: + // Set the output. + if (iter != NULL) { + *iter = out; + } + + return out.code; +} + +int conf_db_iter_next( + conf_t *conf, + conf_iter_t *iter) +{ + if (conf == NULL || iter == NULL) { + return KNOT_EINVAL; + } + + if (iter->code != KNOT_EOK) { + return iter->code; + } + assert(iter->iter != NULL); + + // Move to the next key-value. + iter->iter = conf->api->iter_next(iter->iter); + if (iter->iter == NULL) { + conf_db_iter_finish(conf, iter); + iter->code = KNOT_EOF; + return iter->code; + } + + // Check for key overflow. + iter->code = check_iter(conf, iter); + if (iter->code != KNOT_EOK) { + conf_db_iter_finish(conf, iter); + return iter->code; + } + + return KNOT_EOK; +} + +int conf_db_iter_id( + conf_t *conf, + conf_iter_t *iter, + const uint8_t **data, + size_t *data_len) +{ + if (conf == NULL || iter == NULL || iter->iter == NULL || + data == NULL || data_len == NULL) { + return KNOT_EINVAL; + } + + knot_db_val_t key; + int ret = conf->api->iter_key(iter->iter, &key); + if (ret != KNOT_EOK) { + return ret; + } + + *data = (uint8_t *)key.data + CONF_MIN_KEY_LEN; + *data_len = key.len - CONF_MIN_KEY_LEN; + + return KNOT_EOK; +} + +int conf_db_iter_del( + conf_t *conf, + conf_iter_t *iter) +{ + if (conf == NULL || iter == NULL || iter->iter == NULL) { + return KNOT_EINVAL; + } + + return knot_db_lmdb_iter_del(iter->iter); +} + +void conf_db_iter_finish( + conf_t *conf, + conf_iter_t *iter) +{ + if (conf == NULL || iter == NULL) { + return; + } + + if (iter->iter != NULL) { + conf->api->iter_finish(iter->iter); + iter->iter = NULL; + } +} + +int conf_db_raw_dump( + conf_t *conf, + knot_db_txn_t *txn, + const char *file_name) +{ + if (conf == NULL) { + return KNOT_EINVAL; + } + + // Use the current config read transaction if not specified. + if (txn == NULL) { + txn = &conf->read_txn; + } + + FILE *fp = stdout; + if (file_name != NULL) { + fp = fopen(file_name, "w"); + if (fp == NULL) { + return KNOT_ERROR; + } + } + + int ret = KNOT_EOK; + + knot_db_iter_t *it = conf->api->iter_begin(txn, KNOT_DB_FIRST); + while (it != NULL) { + knot_db_val_t key; + ret = conf->api->iter_key(it, &key); + if (ret != KNOT_EOK) { + break; + } + + knot_db_val_t data; + ret = conf->api->iter_val(it, &data); + if (ret != KNOT_EOK) { + break; + } + + uint8_t *k = (uint8_t *)key.data; + uint8_t *d = (uint8_t *)data.data; + if (k[1] == KEY1_ITEMS) { + fprintf(fp, "[%i][%i]%.*s", k[0], k[1], + (int)key.len - 2, k + 2); + fprintf(fp, ": %u\n", d[0]); + } else if (k[1] == KEY1_ID) { + fprintf(fp, "[%i][%i](%zu){", k[0], k[1], key.len - 2); + for (size_t i = 2; i < key.len; i++) { + fprintf(fp, "%02x", (uint8_t)k[i]); + } + fprintf(fp, "}\n"); + } else { + fprintf(fp, "[%i][%i]", k[0], k[1]); + if (key.len > 2) { + fprintf(fp, "(%zu){", key.len - 2); + for (size_t i = 2; i < key.len; i++) { + fprintf(fp, "%02x", (uint8_t)k[i]); + } + fprintf(fp, "}"); + } + fprintf(fp, ": (%zu)<", data.len); + for (size_t i = 0; i < data.len; i++) { + fprintf(fp, "%02x", (uint8_t)d[i]); + } + fprintf(fp, ">\n"); + } + + it = conf->api->iter_next(it); + } + conf->api->iter_finish(it); + + if (file_name != NULL) { + fclose(fp); + } else { + fflush(fp); + } + + return ret; +} diff --git a/src/knot/conf/confdb.h b/src/knot/conf/confdb.h new file mode 100644 index 0000000..927200e --- /dev/null +++ b/src/knot/conf/confdb.h @@ -0,0 +1,230 @@ +/* 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 <stdbool.h> +#include <stdint.h> + +#include "knot/conf/conf.h" +#include "libknot/libknot.h" +#include "libknot/yparser/ypschema.h" + +/*! Current version of the configuration database structure. */ +#define CONF_DB_VERSION 2 +/*! Minimum length of a database key ([category_id, item_id]. */ +#define CONF_MIN_KEY_LEN (2 * sizeof(uint8_t)) +/*! Maximum length of a database key ([category_id, item_id, identifier]. */ +#define CONF_MAX_KEY_LEN (CONF_MIN_KEY_LEN + YP_MAX_ID_LEN) +/*! Maximum size of database data. */ +#define CONF_MAX_DATA_LEN 65536 + +/*! + * Initializes the configuration DB if empty. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] purge Purge the DB indicator. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_db_init( + conf_t *conf, + knot_db_txn_t *txn, + bool purge +); + +/*! + * Checks the configuration DB and returns the number of items. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * + * \return Error code, KNOT_EOK if ok and empty, > 0 number of records. + */ +int conf_db_check( + conf_t *conf, + knot_db_txn_t *txn +); + +/*! + * Sets the item with data in the configuration DB. + * + * Singlevalued data is rewritten, multivalued data is appended. + * + * \note Setting of key0 without key1 has no effect. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0 Section name. + * \param[in] key1 Item name. + * \param[in] id Section identifier. + * \param[in] id_len Length of the section identifier. + * \param[in] data Item data. + * \param[in] data_len Length of the item data. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_db_set( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0, + const yp_name_t *key1, + const uint8_t *id, + size_t id_len, + const uint8_t *data, + size_t data_len +); + +/*! + * Unsets the item data in the configuration DB. + * + * If no data is provided, the whole item is remove. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0 Section name. + * \param[in] key1 Item name. + * \param[in] id Section identifier. + * \param[in] id_len Length of the section identifier. + * \param[in] data Item data. + * \param[in] data_len Length of the item data. + * \param[in] delete_key1 Set to unregister the item from the DB. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_db_unset( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0, + const yp_name_t *key1, + const uint8_t *id, + size_t id_len, + const uint8_t *data, + size_t data_len, + bool delete_key1 +); + +/*! + * Gets the item data from the configuration DB. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0 Section name. + * \param[in] key1 Item name. + * \param[in] id Section identifier. + * \param[in] id_len Length of the section identifier. + * \param[out] data Item data. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_db_get( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0, + const yp_name_t *key1, + const uint8_t *id, + size_t id_len, + conf_val_t *data +); + +/*! + * Gets a configuration DB section iterator. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] key0 Section name. + * \param[out] iter Section iterator. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_db_iter_begin( + conf_t *conf, + knot_db_txn_t *txn, + const yp_name_t *key0, + conf_iter_t *iter +); + +/*! + * Moves the section iterator to the next identifier. + * + * \param[in] conf Configuration. + * \param[in,out] iter Section iterator. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_db_iter_next( + conf_t *conf, + conf_iter_t *iter +); + +/*! + * Gets the current section iterator value (identifier). + * + * \param[in] conf Configuration. + * \param[in] iter Section iterator. + * \param[out] data Identifier. + * \param[out] data_len Length of the identifier. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_db_iter_id( + conf_t *conf, + conf_iter_t *iter, + const uint8_t **data, + size_t *data_len +); + +/*! + * Deletes the current section iterator value (identifier). + * + * \param[in] conf Configuration. + * \param[in,out] iter Section iterator. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_db_iter_del( + conf_t *conf, + conf_iter_t *iter +); + +/*! + * Deletes the section iterator. + * + * \param[in] conf Configuration. + * \param[in,out] iter Section iterator. + */ +void conf_db_iter_finish( + conf_t *conf, + conf_iter_t *iter +); + +/*! + * Dumps the configuration DB in the textual form. + * + * \note This function is intended for debugging. + * + * \param[in] conf Configuration. + * \param[in] txn Configuration DB transaction. + * \param[in] file_name File name to dump to (NULL to dump to stdout). + * + * \return Error code, KNOT_EOK if success. + */ +int conf_db_raw_dump( + conf_t *conf, + knot_db_txn_t *txn, + const char *file_name +); diff --git a/src/knot/conf/confio.c b/src/knot/conf/confio.c new file mode 100644 index 0000000..817f693 --- /dev/null +++ b/src/knot/conf/confio.c @@ -0,0 +1,1612 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/common/log.h" +#include "knot/conf/confdb.h" +#include "knot/conf/confio.h" +#include "knot/conf/module.h" +#include "knot/conf/tools.h" + +#define FCN(io) (io->fcn != NULL) ? io->fcn(io) : KNOT_EOK; + +static void io_reset_val( + conf_io_t *io, + const yp_item_t *key0, + const yp_item_t *key1, + const uint8_t *id, + size_t id_len, + bool id_as_data, + conf_val_t *val) +{ + io->key0 = key0; + io->key1 = key1; + io->id = id; + io->id_len = id_len; + io->id_as_data = id_as_data; + io->data.val = val; + io->data.bin = NULL; +} + +static void io_reset_bin( + conf_io_t *io, + const yp_item_t *key0, + const yp_item_t *key1, + const uint8_t *id, + size_t id_len, + const uint8_t *bin, + size_t bin_len) +{ + io_reset_val(io, key0, key1, id, id_len, false, NULL); + io->data.bin = bin; + io->data.bin_len = bin_len; +} + +int conf_io_begin( + bool child) +{ + assert(conf() != NULL); + + if (conf()->io.txn != NULL && !child) { + return KNOT_TXN_EEXISTS; + } else if (conf()->io.txn == NULL && child) { + return KNOT_TXN_ENOTEXISTS; + } + + knot_db_txn_t *parent = conf()->io.txn; + knot_db_txn_t *txn = (parent == NULL) ? conf()->io.txn_stack : parent + 1; + if (txn >= conf()->io.txn_stack + CONF_MAX_TXN_DEPTH) { + return KNOT_TXN_EEXISTS; + } + + if (conf()->filename != NULL && !child) { + log_ctl_notice("control, persistent configuration database " + "not available"); + } + + // Start the writing transaction. + int ret = knot_db_lmdb_txn_begin(conf()->db, txn, parent, 0); + if (ret != KNOT_EOK) { + return ret; + } + + conf()->io.txn = txn; + + // Reset master transaction flags. + if (!child) { + conf()->io.flags = CONF_IO_FACTIVE; + if (conf()->io.zones != NULL) { + trie_clear(conf()->io.zones); + } + } + + return KNOT_EOK; +} + +int conf_io_commit( + bool child) +{ + assert(conf() != NULL); + + if (conf()->io.txn == NULL || + (child && conf()->io.txn == conf()->io.txn_stack)) { + return KNOT_TXN_ENOTEXISTS; + } + + knot_db_txn_t *txn = child ? conf()->io.txn : conf()->io.txn_stack; + + // Commit the writing transaction. + int ret = conf()->api->txn_commit(txn); + + conf()->io.txn = child ? txn - 1 : NULL; + + return ret; +} + +void conf_io_abort( + bool child) +{ + assert(conf() != NULL); + + if (conf()->io.txn == NULL || + (child && conf()->io.txn == conf()->io.txn_stack)) { + return; + } + + knot_db_txn_t *txn = child ? conf()->io.txn : conf()->io.txn_stack; + + // Abort the writing transaction. + conf()->api->txn_abort(txn); + conf()->io.txn = child ? txn - 1 : NULL; + + // Reset master transaction flags. + if (!child) { + conf()->io.flags = YP_FNONE; + if (conf()->io.zones != NULL) { + trie_clear(conf()->io.zones); + } + } +} + +static int list_section( + const yp_item_t *items, + const yp_item_t **item, + conf_io_t *io) +{ + for (*item = items; (*item)->name != NULL; (*item)++) { + int ret = FCN(io); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +int conf_io_list( + const char *key0, + const char *key1, + const char *id, + bool list_schema, + bool get_current, + conf_io_t *io) +{ + if (io == NULL) { + return KNOT_EINVAL; + } + + assert(conf() != NULL); + + if (conf()->io.txn == NULL && !get_current) { + return KNOT_TXN_ENOTEXISTS; + } + + // List schema sections by default. + if (key0 == NULL) { + io_reset_val(io, NULL, NULL, NULL, 0, false, NULL); + + return list_section(conf()->schema, &io->key0, io); + } + + yp_check_ctx_t *ctx = yp_schema_check_init(&conf()->schema); + if (ctx == NULL) { + return KNOT_ENOMEM; + } + + // Check the input. + int ret = yp_schema_check_str(ctx, key0, key1, id, NULL); + if (ret != KNOT_EOK) { + goto list_error; + } + + knot_db_txn_t *txn = get_current ? &conf()->read_txn : conf()->io.txn; + + yp_node_t *node = &ctx->nodes[ctx->current]; + yp_node_t *parent = node->parent; + + // List group items. + if (list_schema && key1 == NULL) { + if (node->item->type != YP_TGRP) { // Ignore non-group section. + ret = KNOT_EOK; + goto list_error; + } + + io_reset_val(io, node->item, NULL, NULL, 0, false, NULL); + + ret = list_section(node->item->sub_items, &io->key1, io); + // List option item values. + } else if (list_schema) { + if (node->item->type != YP_TOPT) { // Ignore non-option item. + ret = KNOT_EOK; + goto list_error; + } + + for (const knot_lookup_t *o = node->item->var.o.opts; o->name != NULL; o++) { + uint8_t val = o->id; + io_reset_bin(io, parent->item, node->item, parent->id, + parent->id_len, &val, sizeof(val)); + ret = FCN(io); + if (ret != KNOT_EOK) { + goto list_error; + } + } + // List group identifiers. + } else if (parent == NULL) { + if (node->item->type != YP_TGRP) { // Ignore non-group section. + ret = KNOT_EOK; + goto list_error; + } + + // If key1 != NULL, it's used for a value completion (zone.domain). + io_reset_val(io, node->item, NULL, NULL, 0, key1 != NULL, NULL); + + conf_iter_t iter; + ret = conf_db_iter_begin(conf(), txn, io->key0->name, &iter); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + ret = KNOT_EOK; + goto list_error; + default: + goto list_error; + } + + while (ret == KNOT_EOK) { + // Set the section identifier. + ret = conf_db_iter_id(conf(), &iter, &io->id, &io->id_len); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + goto list_error; + } + + ret = FCN(io); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + goto list_error; + } + + ret = conf_db_iter_next(conf(), &iter); + } + ret = KNOT_EOK; + // List item values. + } else { + io_reset_val(io, parent->item, node->item, parent->id, + parent->id_len, false, NULL); + + // Get the item value. + conf_val_t data; + ret = conf_db_get(conf(), txn, io->key0->name, io->key1->name, + io->id, io->id_len, &data); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + ret = KNOT_EOK; + goto list_error; + default: + goto list_error; + } + + io->data.val = &data; + + ret = FCN(io); + if (ret != KNOT_EOK) { + goto list_error; + } + } +list_error: + yp_schema_check_deinit(ctx); + + return ret; +} + +static int diff_item( + conf_io_t *io) +{ + // Process an identifier item. + if ((io->key0->flags & YP_FMULTI) != 0 && io->key0->var.g.id == io->key1) { + bool old_id, new_id; + + // Check if a removed identifier. + int ret = conf_db_get(conf(), &conf()->read_txn, io->key0->name, + NULL, io->id, io->id_len, NULL); + switch (ret) { + case KNOT_EOK: + old_id = true; + break; + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + old_id = false; + break; + default: + return ret; + } + + // Check if an added identifier. + ret = conf_db_get(conf(), conf()->io.txn, io->key0->name, NULL, + io->id, io->id_len, NULL); + switch (ret) { + case KNOT_EOK: + new_id = true; + break; + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + new_id = false; + break; + default: + return ret; + } + + // Check if valid identifier. + if (!old_id && !new_id) { + return KNOT_YP_EINVAL_ID; + } + + if (old_id != new_id) { + io->id_as_data = true; + io->type = old_id ? OLD : NEW; + + // Process the callback. + ret = FCN(io); + + // Reset the modified parameters. + io->id_as_data = false; + io->type = NONE; + + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; + } + + conf_val_t old_val, new_val; + + // Get the old item value. + conf_db_get(conf(), &conf()->read_txn, io->key0->name, io->key1->name, + io->id, io->id_len, &old_val); + switch (old_val.code) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + break; + default: + return old_val.code; + } + + // Get the new item value. + conf_db_get(conf(), conf()->io.txn, io->key0->name, io->key1->name, + io->id, io->id_len, &new_val); + switch (new_val.code) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + if (old_val.code != KNOT_EOK) { + return KNOT_EOK; + } + break; + default: + return new_val.code; + } + + // Process the value difference. + if (old_val.code != KNOT_EOK) { + io->data.val = &new_val; + io->type = NEW; + int ret = FCN(io); + if (ret != KNOT_EOK) { + return ret; + } + } else if (new_val.code != KNOT_EOK) { + io->data.val = &old_val; + io->type = OLD; + int ret = FCN(io); + if (ret != KNOT_EOK) { + return ret; + } + } else if (!conf_val_equal(&old_val, &new_val)) { + io->data.val = &old_val; + io->type = OLD; + int ret = FCN(io); + if (ret != KNOT_EOK) { + return ret; + } + + io->data.val = &new_val; + io->type = NEW; + ret = FCN(io); + if (ret != KNOT_EOK) { + return ret; + } + } + + // Reset the modified parameters. + io->data.val = NULL; + io->type = NONE; + + return KNOT_EOK; +} + +static int diff_section( + conf_io_t *io) +{ + // Get the value for the specified item. + if (io->key1 != NULL) { + return diff_item(io); + } + + // Get the values for all items. + for (yp_item_t *i = io->key0->sub_items; i->name != NULL; i++) { + io->key1 = i; + + int ret = diff_item(io); + + // Reset the modified parameters. + io->key1 = NULL; + + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int diff_iter_section( + conf_io_t *io) +{ + // First compare the section with the old and common identifiers. + conf_iter_t iter; + int ret = conf_db_iter_begin(conf(), &conf()->read_txn, io->key0->name, + &iter); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + // Continue to the second step. + ret = KNOT_EOF; + break; + default: + return ret; + } + + while (ret == KNOT_EOK) { + ret = conf_db_iter_id(conf(), &iter, &io->id, &io->id_len); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + return ret; + } + + ret = diff_section(io); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + return ret; + } + + ret = conf_db_iter_next(conf(), &iter); + } + if (ret != KNOT_EOF) { + return ret; + } + + // Second compare the section with the new identifiers. + ret = conf_db_iter_begin(conf(), conf()->io.txn, io->key0->name, &iter); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + return KNOT_EOK; + default: + return ret; + } + + while (ret == KNOT_EOK) { + ret = conf_db_iter_id(conf(), &iter, &io->id, &io->id_len); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + return ret; + } + + // Ignore old and common identifiers. + ret = conf_db_get(conf(), &conf()->read_txn, io->key0->name, + NULL, io->id, io->id_len, NULL); + switch (ret) { + case KNOT_EOK: + ret = conf_db_iter_next(conf(), &iter); + continue; + case KNOT_ENOENT: + case KNOT_YP_EINVAL_ID: + break; + default: + conf_db_iter_finish(conf(), &iter); + return ret; + } + + ret = diff_section(io); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + return ret; + } + + ret = conf_db_iter_next(conf(), &iter); + } + if (ret != KNOT_EOF) { + return ret; + } + + return KNOT_EOK; +} + +static int diff_zone_section( + conf_io_t *io) +{ + assert(io->key0->flags & CONF_IO_FZONE); + + if (conf()->io.zones == NULL) { + return KNOT_EOK; + } + + trie_it_t *it = trie_it_begin(conf()->io.zones); + for (; !trie_it_finished(it); trie_it_next(it)) { + io->id = (const uint8_t *)trie_it_key(it, &io->id_len); + + // Get the difference for specific zone. + int ret = diff_section(io); + if (ret != KNOT_EOK) { + trie_it_free(it); + return ret; + } + } + trie_it_free(it); + + return KNOT_EOK; +} + +int conf_io_diff( + const char *key0, + const char *key1, + const char *id, + conf_io_t *io) +{ + if (io == NULL) { + return KNOT_EINVAL; + } + + assert(conf() != NULL); + + if (conf()->io.txn == NULL) { + return KNOT_TXN_ENOTEXISTS; + } + + // Compare all sections by default. + if (key0 == NULL) { + for (yp_item_t *i = conf()->schema; i->name != NULL; i++) { + // Skip non-group item. + if (i->type != YP_TGRP) { + continue; + } + + int ret = conf_io_diff(i->name + 1, key1, NULL, io); + + // Reset parameters after each section. + io_reset_val(io, NULL, NULL, NULL, 0, false, NULL); + + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; + } + + yp_check_ctx_t *ctx = yp_schema_check_init(&conf()->schema); + if (ctx == NULL) { + return KNOT_ENOMEM; + } + + // Check the input. + int ret = yp_schema_check_str(ctx, key0, key1, id, NULL); + if (ret != KNOT_EOK) { + goto diff_error; + } + + yp_node_t *node = &ctx->nodes[ctx->current]; + yp_node_t *parent = node->parent; + + // Key1 is not a group identifier. + if (parent != NULL) { + io_reset_val(io, parent->item, node->item, parent->id, + parent->id_len, false, NULL); + // Key1 is a group identifier. + } else if (key1 != NULL && strlen(key1) != 0) { + assert(node->item->type == YP_TGRP && + (node->item->flags & YP_FMULTI) != 0); + + io_reset_val(io, node->item, node->item->var.g.id, node->id, + node->id_len, true, NULL); + // No key1 specified. + } else { + io_reset_val(io, node->item, NULL, node->id, node->id_len, + false, NULL); + } + + // Check for a non-group item. + if (io->key0->type != YP_TGRP) { + ret = KNOT_ENOTSUP; + goto diff_error; + } + + // Compare the section with all identifiers by default. + if ((io->key0->flags & YP_FMULTI) != 0 && io->id_len == 0) { + // The zone section has an optimized diff. + if (io->key0->flags & CONF_IO_FZONE) { + // Full diff by default. + if (!(conf()->io.flags & CONF_IO_FACTIVE)) { + ret = diff_iter_section(io); + // Full diff if all zones changed. + } else if (conf()->io.flags & CONF_IO_FDIFF_ZONES) { + ret = diff_iter_section(io); + // Optimized diff for specific zones. + } else { + ret = diff_zone_section(io); + } + } else { + ret = diff_iter_section(io); + } + + goto diff_error; + } + + // Compare the section with a possible identifier. + ret = diff_section(io); +diff_error: + yp_schema_check_deinit(ctx); + + return ret; +} + +static int get_section( + knot_db_txn_t *txn, + conf_io_t *io) +{ + conf_val_t data; + + // Get the value for the specified item. + if (io->key1 != NULL) { + if (!io->id_as_data) { + // Get the item value. + conf_db_get(conf(), txn, io->key0->name, io->key1->name, + io->id, io->id_len, &data); + switch (data.code) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + return KNOT_EOK; + default: + return data.code; + } + + io->data.val = &data; + } + + // Process the callback. + int ret = FCN(io); + + // Reset the modified parameters. + io->data.val = NULL; + + return ret; + } + + // Get the values for all section items by default. + for (yp_item_t *i = io->key0->sub_items; i->name != NULL; i++) { + // Process the (first) identifier item. + if ((io->key0->flags & YP_FMULTI) != 0 && io->key0->var.g.id == i) { + // Check if existing identifier. + conf_db_get(conf(), txn, io->key0->name, NULL, io->id, + io->id_len, &data); + switch (data.code) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + continue; + default: + return data.code; + } + + io->key1 = i; + io->id_as_data = true; + + // Process the callback. + int ret = FCN(io); + + // Reset the modified parameters. + io->key1 = NULL; + io->id_as_data = false; + + if (ret != KNOT_EOK) { + return ret; + } + + continue; + } + + // Get the item value. + conf_db_get(conf(), txn, io->key0->name, i->name, io->id, + io->id_len, &data); + switch (data.code) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + continue; + default: + return data.code; + } + + io->key1 = i; + io->data.val = &data; + + // Process the callback. + int ret = FCN(io); + + // Reset the modified parameters. + io->key1 = NULL; + io->data.val = NULL; + + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +int conf_io_get( + const char *key0, + const char *key1, + const char *id, + bool get_current, + conf_io_t *io) +{ + if (io == NULL) { + return KNOT_EINVAL; + } + + assert(conf() != NULL); + + if (conf()->io.txn == NULL && !get_current) { + return KNOT_TXN_ENOTEXISTS; + } + + // List all sections by default. + if (key0 == NULL) { + for (yp_item_t *i = conf()->schema; i->name != NULL; i++) { + // Skip non-group item. + if (i->type != YP_TGRP) { + continue; + } + + int ret = conf_io_get(i->name + 1, key1, NULL, + get_current, io); + // Reset parameters after each section. + io_reset_val(io, NULL, NULL, NULL, 0, false, NULL); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; + } + + yp_check_ctx_t *ctx = yp_schema_check_init(&conf()->schema); + if (ctx == NULL) { + return KNOT_ENOMEM; + } + + // Check the input. + int ret = yp_schema_check_str(ctx, key0, key1, id, NULL); + if (ret != KNOT_EOK) { + goto get_error; + } + + yp_node_t *node = &ctx->nodes[ctx->current]; + yp_node_t *parent = node->parent; + + // Key1 is not a group identifier. + if (parent != NULL) { + io_reset_val(io, parent->item, node->item, parent->id, + parent->id_len, false, NULL); + // Key1 is a group identifier. + } else if (key1 != NULL && strlen(key1) != 0) { + assert(node->item->type == YP_TGRP && + (node->item->flags & YP_FMULTI) != 0); + + io_reset_val(io, node->item, node->item->var.g.id, node->id, + node->id_len, true, NULL); + // No key1 specified. + } else { + io_reset_val(io, node->item, NULL, node->id, node->id_len, false, + NULL); + } + + knot_db_txn_t *txn = get_current ? &conf()->read_txn : conf()->io.txn; + + // Check for a non-group item. + if (io->key0->type != YP_TGRP) { + ret = KNOT_ENOTSUP; + goto get_error; + } + + // List the section with all identifiers by default. + if ((io->key0->flags & YP_FMULTI) != 0 && io->id_len == 0) { + conf_iter_t iter; + ret = conf_db_iter_begin(conf(), txn, io->key0->name, &iter); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + ret = KNOT_EOK; + goto get_error; + default: + goto get_error; + } + + while (ret == KNOT_EOK) { + // Set the section identifier. + ret = conf_db_iter_id(conf(), &iter, &io->id, &io->id_len); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + goto get_error; + } + + ret = get_section(txn, io); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + goto get_error; + } + + ret = conf_db_iter_next(conf(), &iter); + } + + ret = KNOT_EOK; + goto get_error; + } + + // List the section with a possible identifier. + ret = get_section(txn, io); +get_error: + yp_schema_check_deinit(ctx); + + return ret; +} + +static void upd_changes( + const conf_io_t *io, + conf_io_type_t type, + yp_flag_t flags, + bool any_id) +{ + // Update common flags. + conf()->io.flags |= flags; + + // Return if not important change. + if (type == CONF_IO_TNONE) { + return; + } + + // Update reference item. + if (flags & CONF_IO_FREF) { + // Expected an identifier, which cannot be changed. + assert(type != CONF_IO_TCHANGE); + + // Re-check and reload all zones if a reference has been removed. + if (type == CONF_IO_TUNSET) { + conf()->io.flags |= CONF_IO_FCHECK_ZONES | CONF_IO_FRLD_ZONES; + } + return; + // Return if no specific zone operation. + } else if (!(flags & CONF_IO_FZONE)) { + return; + } + + // Don't process each zone individually, process all instead. + if (any_id) { + // Diff all zone changes. + conf()->io.flags |= CONF_IO_FCHECK_ZONES | CONF_IO_FDIFF_ZONES; + + // Reload just with important changes. + if (flags & CONF_IO_FRLD_ZONE) { + conf()->io.flags |= CONF_IO_FRLD_ZONES; + } + return; + } + + // Prepare zone changes storage if it doesn't exist. + trie_t *zones = conf()->io.zones; + if (zones == NULL) { + zones = trie_create(NULL); + if (zones == NULL) { + return; + } + conf()->io.zones = zones; + } + + // Get zone status or create new. + trie_val_t *val = trie_get_ins(zones, io->id, io->id_len); + conf_io_type_t *current = (conf_io_type_t *)val; + + switch (type) { + case CONF_IO_TSET: + // Revert remove zone, but don't remove (probably changed). + if (*current & CONF_IO_TUNSET) { + *current &= ~CONF_IO_TUNSET; + } else { + // Must be a new zone. + assert(*current == CONF_IO_TNONE); + // Mark added zone. + *current = type; + } + break; + case CONF_IO_TUNSET: + if (*current & CONF_IO_TSET) { + // Remove inserted zone -> no change. + trie_del(zones, io->id, io->id_len, NULL); + } else { + // Remove existing zone. + *current |= type; + } + break; + case CONF_IO_TCHANGE: + *current |= type; + // Mark zone to reload if required. + if (flags & CONF_IO_FRLD_ZONE) { + *current |= CONF_IO_TRELOAD; + } + break; + case CONF_IO_TRELOAD: + default: + assert(0); + } +} + +static int set_item( + conf_io_t *io) +{ + int ret = conf_db_set(conf(), conf()->io.txn, io->key0->name, + (io->key1 != NULL) ? io->key1->name : NULL, + io->id, io->id_len, io->data.bin, io->data.bin_len); + if (ret != KNOT_EOK) { + return ret; + } + + // Postpone group callbacks to config check. + if (io->key0->type == YP_TGRP && io->id_len == 0) { + return KNOT_EOK; + } + + knotd_conf_check_extra_t extra = { + .conf = conf(), + .txn = conf()->io.txn + }; + knotd_conf_check_args_t args = { + .item = (io->key1 != NULL) ? io->key1 : + ((io->id_len == 0) ? io->key0 : io->key0->var.g.id), + .id = io->id, + .id_len = io->id_len, + .data = io->data.bin, + .data_len = io->data.bin_len, + .extra = &extra + }; + + // Call the item callbacks (include, item check, mod-id check). + ret = conf_exec_callbacks(&args); + if (ret != KNOT_EOK) { + CONF_LOG(LOG_DEBUG, "item '%s' (%s)", args.item->name + 1, + args.err_str != NULL ? args.err_str : knot_strerror(ret)); + } + + return ret; +} + +int conf_io_set( + const char *key0, + const char *key1, + const char *id, + const char *data) +{ + assert(conf() != NULL); + + if (conf()->io.txn == NULL) { + return KNOT_TXN_ENOTEXISTS; + } + + // At least key0 must be specified. + if (key0 == NULL) { + return KNOT_EINVAL; + } + + yp_check_ctx_t *ctx = yp_schema_check_init(&conf()->schema); + if (ctx == NULL) { + return KNOT_ENOMEM; + } + + // Check the input. + int ret = yp_schema_check_str(ctx, key0, key1, id, data); + if (ret != KNOT_EOK) { + goto set_error; + } + + yp_node_t *node = &ctx->nodes[ctx->current]; + yp_node_t *parent = node->parent; + + yp_flag_t upd_flags = node->item->flags; + conf_io_type_t upd_type = CONF_IO_TNONE; + + conf_io_t io = { NULL }; + + // Key1 is not a group identifier. + if (parent != NULL) { + if (node->data_len == 0) { + ret = KNOT_YP_ENODATA; + goto set_error; + } + upd_type = CONF_IO_TCHANGE; + upd_flags |= parent->item->flags; + io_reset_bin(&io, parent->item, node->item, parent->id, + parent->id_len, node->data, node->data_len); + // A group identifier or whole group. + } else if (node->item->type == YP_TGRP) { + upd_type = CONF_IO_TSET; + if ((node->item->flags & YP_FMULTI) != 0) { + if (node->id_len == 0) { + ret = KNOT_YP_ENOID; + goto set_error; + } + upd_flags |= node->item->var.g.id->flags; + } else { + ret = KNOT_ENOTSUP; + goto set_error; + } + assert(node->data_len == 0); + io_reset_bin(&io, node->item, NULL, node->id, node->id_len, + NULL, 0); + // A non-group item with data (include). + } else if (node->data_len > 0) { + io_reset_bin(&io, node->item, NULL, NULL, 0, node->data, + node->data_len); + } else { + ret = KNOT_YP_ENODATA; + goto set_error; + } + + // Set the item for all identifiers by default. + if (io.key0->type == YP_TGRP && io.key1 != NULL && + (io.key0->flags & YP_FMULTI) != 0 && io.id_len == 0) { + conf_iter_t iter; + ret = conf_db_iter_begin(conf(), conf()->io.txn, io.key0->name, + &iter); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + ret = KNOT_EOK; + goto set_error; + default: + goto set_error; + } + + uint8_t copied_id[YP_MAX_ID_LEN]; + io.id = copied_id; + while (ret == KNOT_EOK) { + // Get the identifier and copy it because of next DB update. + const uint8_t *tmp_id; + ret = conf_db_iter_id(conf(), &iter, &tmp_id, &io.id_len); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + goto set_error; + } + memcpy(copied_id, tmp_id, io.id_len); + + // Set the data. + ret = set_item(&io); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + goto set_error; + } + + ret = conf_db_iter_next(conf(), &iter); + } + if (ret != KNOT_EOF) { + goto set_error; + } + + upd_changes(&io, upd_type, upd_flags, true); + + ret = KNOT_EOK; + goto set_error; + } + + // Set the item with a possible identifier. + ret = set_item(&io); + + if (ret == KNOT_EOK) { + upd_changes(&io, upd_type, upd_flags, false); + } +set_error: + yp_schema_check_deinit(ctx); + + return ret; +} + +static int unset_section_data( + conf_io_t *io) +{ + // Unset the value for the specified item. + if (io->key1 != NULL) { + return conf_db_unset(conf(), conf()->io.txn, io->key0->name, + io->key1->name, io->id, io->id_len, + io->data.bin, io->data.bin_len, false); + } + + // Unset the whole section by default. + for (yp_item_t *i = io->key0->sub_items; i->name != NULL; i++) { + // Skip the identifier item. + if ((io->key0->flags & YP_FMULTI) != 0 && io->key0->var.g.id == i) { + continue; + } + + int ret = conf_db_unset(conf(), conf()->io.txn, io->key0->name, + i->name, io->id, io->id_len, io->data.bin, + io->data.bin_len, false); + switch (ret) { + case KNOT_EOK: + case KNOT_ENOENT: + continue; + default: + return ret; + } + } + + return KNOT_EOK; +} + +static int unset_section( + const yp_item_t *key0) +{ + // Unset the section items. + for (yp_item_t *i = key0->sub_items; i->name != NULL; i++) { + // Skip the identifier item. + if ((key0->flags & YP_FMULTI) != 0 && key0->var.g.id == i) { + continue; + } + + int ret = conf_db_unset(conf(), conf()->io.txn, key0->name, + i->name, NULL, 0, NULL, 0, true); + switch (ret) { + case KNOT_EOK: + case KNOT_ENOENT: + continue; + default: + return ret; + } + } + + // Unset the section. + int ret = conf_db_unset(conf(), conf()->io.txn, key0->name, NULL, NULL, + 0, NULL, 0, false); + switch (ret) { + case KNOT_EOK: + case KNOT_ENOENT: + return KNOT_EOK; + default: + return ret; + } +} + +int conf_io_unset( + const char *key0, + const char *key1, + const char *id, + const char *data) +{ + assert(conf() != NULL); + + if (conf()->io.txn == NULL) { + return KNOT_TXN_ENOTEXISTS; + } + + // Unset all sections by default. + if (key0 == NULL) { + for (yp_item_t *i = conf()->schema; i->name != NULL; i++) { + // Skip non-group item. + if (i->type != YP_TGRP) { + continue; + } + + int ret = conf_io_unset(i->name + 1, key1, NULL, NULL); + switch (ret) { + case KNOT_EOK: + case KNOT_ENOENT: + break; + default: + return ret; + } + } + + return KNOT_EOK; + } + + yp_check_ctx_t *ctx = yp_schema_check_init(&conf()->schema); + if (ctx == NULL) { + return KNOT_ENOMEM; + } + + // Check the input. + int ret = yp_schema_check_str(ctx, key0, key1, id, data); + if (ret != KNOT_EOK) { + goto unset_error; + } + + yp_node_t *node = &ctx->nodes[ctx->current]; + yp_node_t *parent = node->parent; + + yp_flag_t upd_flags = node->item->flags; + conf_io_type_t upd_type = CONF_IO_TNONE; + + conf_io_t io = { NULL }; + + // Key1 is not a group identifier. + if (parent != NULL) { + upd_type = CONF_IO_TCHANGE; + upd_flags |= parent->item->flags; + io_reset_bin(&io, parent->item, node->item, parent->id, + parent->id_len, node->data, node->data_len); + // A group identifier or whole group. + } else if (node->item->type == YP_TGRP) { + upd_type = CONF_IO_TUNSET; + if ((node->item->flags & YP_FMULTI) != 0) { + upd_flags |= node->item->var.g.id->flags; + } + assert(node->data_len == 0); + io_reset_bin(&io, node->item, NULL, node->id, node->id_len, + NULL, 0); + // A non-group item (include). + } else { + ret = KNOT_ENOTSUP; + goto unset_error; + } + + // Unset the section with all identifiers by default. + if ((io.key0->flags & YP_FMULTI) != 0 && io.id_len == 0) { + conf_iter_t iter; + ret = conf_db_iter_begin(conf(), conf()->io.txn, io.key0->name, + &iter); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + ret = KNOT_EOK; + goto unset_error; + default: + goto unset_error; + } + + uint8_t copied_id[YP_MAX_ID_LEN]; + io.id = copied_id; + while (ret == KNOT_EOK) { + // Get the identifier and copy it because of next DB update. + const uint8_t *tmp_id; + ret = conf_db_iter_id(conf(), &iter, &tmp_id, &io.id_len); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + goto unset_error; + } + memcpy(copied_id, tmp_id, io.id_len); + + // Unset the section data. + ret = unset_section_data(&io); + switch (ret) { + case KNOT_EOK: + case KNOT_ENOENT: + break; + default: + conf_db_iter_finish(conf(), &iter); + goto unset_error; + } + + ret = conf_db_iter_next(conf(), &iter); + } + if (ret != KNOT_EOF) { + goto unset_error; + } + + if (io.key1 == NULL) { + // Unset all identifiers. + ret = conf_db_iter_begin(conf(), conf()->io.txn, + io.key0->name, &iter); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + ret = KNOT_EOK; + goto unset_error; + default: + goto unset_error; + } + + while (ret == KNOT_EOK) { + ret = conf_db_iter_del(conf(), &iter); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + goto unset_error; + } + + ret = conf_db_iter_next(conf(), &iter); + } + if (ret != KNOT_EOF) { + goto unset_error; + } + + // Unset the section. + ret = unset_section(io.key0); + if (ret != KNOT_EOK) { + goto unset_error; + } + } + + upd_changes(&io, upd_type, upd_flags, true); + + ret = KNOT_EOK; + goto unset_error; + } + + // Unset the section data. + ret = unset_section_data(&io); + if (ret != KNOT_EOK) { + goto unset_error; + } + + if (io.key1 == NULL) { + // Unset the identifier. + if (io.id_len != 0) { + ret = conf_db_unset(conf(), conf()->io.txn, io.key0->name, + NULL, io.id, io.id_len, NULL, 0, false); + if (ret != KNOT_EOK) { + goto unset_error; + } + // Unset the section. + } else { + ret = unset_section(io.key0); + if (ret != KNOT_EOK) { + goto unset_error; + } + } + } + + if (ret == KNOT_EOK) { + upd_changes(&io, upd_type, upd_flags, false); + } +unset_error: + yp_schema_check_deinit(ctx); + + return ret; +} + +static int check_section( + const yp_item_t *group, + const uint8_t *id, + size_t id_len, + conf_io_t *io) +{ + knotd_conf_check_extra_t extra = { + .conf = conf(), + .txn = conf()->io.txn, + .check = true + }; + knotd_conf_check_args_t args = { + .id = id, + .id_len = id_len, + .extra = &extra + }; + + bool non_empty = false; + + conf_val_t bin; // Must be in the scope of the error processing. + for (yp_item_t *item = group->sub_items; item->name != NULL; item++) { + args.item = item; + + // Check the identifier. + if ((group->flags & YP_FMULTI) != 0 && group->var.g.id == item) { + io->error.code = conf_exec_callbacks(&args); + if (io->error.code != KNOT_EOK) { + io_reset_val(io, group, item, NULL, 0, false, NULL); + goto check_section_error; + } + continue; + } + + // Get the item value. + conf_db_get(conf(), conf()->io.txn, group->name, item->name, id, + id_len, &bin); + if (bin.code == KNOT_ENOENT) { + continue; + } else if (bin.code != KNOT_EOK) { + return bin.code; + } + + non_empty = true; + + // Check the item value(s). + size_t values = conf_val_count(&bin); + for (size_t i = 1; i <= values; i++) { + conf_val(&bin); + args.data = bin.data; + args.data_len = bin.len; + + io->error.code = conf_exec_callbacks(&args); + if (io->error.code != KNOT_EOK) { + io_reset_val(io, group, item, id, id_len, false, + &bin); + io->data.index = i; + goto check_section_error; + } + + if (values > 1) { + conf_val_next(&bin); + } + } + } + + // Check the whole section if not empty. + if (id != NULL || non_empty) { + args.item = group; + args.data = NULL; + args.data_len = 0; + + io->error.code = conf_exec_callbacks(&args); + if (io->error.code != KNOT_EOK) { + io_reset_val(io, group, NULL, id, id_len, false, NULL); + goto check_section_error; + } + } + + return KNOT_EOK; + +check_section_error: + io->error.str = args.err_str; + int ret = FCN(io); + if (ret == KNOT_EOK) { + return io->error.code; + } + return ret; +} + +static int check_iter_section( + const yp_item_t *item, + conf_io_t *io) +{ + // Iterate over all identifiers. + conf_iter_t iter; + int ret = conf_db_iter_begin(conf(), conf()->io.txn, item->name, &iter); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + return KNOT_EOK; + default: + return ret; + } + + while (ret == KNOT_EOK) { + size_t id_len; + const uint8_t *id; + ret = conf_db_iter_id(conf(), &iter, &id, &id_len); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + return ret; + } + + // Check specific section item. + ret = check_section(item, id, id_len, io); + if (ret != KNOT_EOK) { + conf_db_iter_finish(conf(), &iter); + return ret; + } + + ret = conf_db_iter_next(conf(), &iter); + } + if (ret != KNOT_EOF) { + return ret; + } + + return KNOT_EOK; +} + +static int check_zone_section( + const yp_item_t *item, + conf_io_t *io) +{ + assert(item->flags & CONF_IO_FZONE); + + if (conf()->io.zones == NULL) { + return KNOT_EOK; + } + + trie_it_t *it = trie_it_begin(conf()->io.zones); + for (; !trie_it_finished(it); trie_it_next(it)) { + size_t id_len; + const uint8_t *id = (const uint8_t *)trie_it_key(it, &id_len); + + conf_io_type_t type = conf_io_trie_val(it); + if (type == CONF_IO_TUNSET) { + // Nothing to check. + continue; + } + + // Check specific zone. + int ret = check_section(item, id, id_len, io); + if (ret != KNOT_EOK) { + trie_it_free(it); + return ret; + } + } + trie_it_free(it); + + return KNOT_EOK; +} + +int conf_io_check( + conf_io_t *io) +{ + if (io == NULL) { + return KNOT_EINVAL; + } + + assert(conf() != NULL); + + if (conf()->io.txn == NULL) { + return KNOT_TXN_ENOTEXISTS; + } + + int ret; + + // Iterate over the schema. + for (yp_item_t *item = conf()->schema; item->name != NULL; item++) { + // Skip non-group items (include). + if (item->type != YP_TGRP) { + continue; + } + + // Check simple group without identifiers. + if ((item->flags & YP_FMULTI) == 0) { + ret = check_section(item, NULL, 0, io); + if (ret != KNOT_EOK) { + goto check_error; + } + continue; + } + + // The zone section has an optimized check. + if (item->flags & CONF_IO_FZONE) { + // Full check by default. + if (!(conf()->io.flags & CONF_IO_FACTIVE)) { + ret = check_iter_section(item, io); + // Full check if all zones changed. + } else if (conf()->io.flags & CONF_IO_FCHECK_ZONES) { + ret = check_iter_section(item, io); + // Optimized check for specific zones. + } else { + ret = check_zone_section(item, io); + } + } else { + ret = check_iter_section(item, io); + } + if (ret != KNOT_EOK) { + goto check_error; + } + } + + ret = KNOT_EOK; +check_error: + conf_mod_load_purge(conf(), true); + + return ret; +} diff --git a/src/knot/conf/confio.h b/src/knot/conf/confio.h new file mode 100644 index 0000000..be08e97 --- /dev/null +++ b/src/knot/conf/confio.h @@ -0,0 +1,231 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/conf/conf.h" + +/*! Configuration schema additional flags for dynamic changes. */ +#define CONF_IO_FACTIVE YP_FUSR1 /*!< Active confio transaction indicator. */ +#define CONF_IO_FZONE YP_FUSR2 /*!< Zone section indicator. */ +#define CONF_IO_FREF YP_FUSR3 /*!< Possibly referenced id from a zone. */ +#define CONF_IO_FDIFF_ZONES YP_FUSR4 /*!< All zones config has changed. */ +#define CONF_IO_FCHECK_ZONES YP_FUSR5 /*!< All zones config needs to check. */ +#define CONF_IO_FRLD_SRV YP_FUSR6 /*!< Reload server. */ +#define CONF_IO_FRLD_LOG YP_FUSR7 /*!< Reload logging. */ +#define CONF_IO_FRLD_MOD YP_FUSR8 /*!< Reload global modules. */ +#define CONF_IO_FRLD_ZONE YP_FUSR9 /*!< Reload a specific zone. */ +#define CONF_IO_FRLD_ZONES YP_FUSR10 /*!< Reload all zones. */ +#define CONF_IO_FRLD_ALL (CONF_IO_FRLD_SRV | CONF_IO_FRLD_LOG | \ + CONF_IO_FRLD_MOD | CONF_IO_FRLD_ZONES) + +/*! Zone configuration change type. */ +typedef enum { + CONF_IO_TNONE = 0, /*!< Unspecified. */ + CONF_IO_TSET = 1 << 0, /*!< Zone added. */ + CONF_IO_TUNSET = 1 << 1, /*!< Zone removed. */ + CONF_IO_TCHANGE = 1 << 2, /*!< Zone has changed configuration. */ + CONF_IO_TRELOAD = 1 << 3, /*!< Zone must be reloaded. */ +} conf_io_type_t; + +/*! Configuration interface output. */ +typedef struct conf_io conf_io_t; +struct conf_io { + /*! Section. */ + const yp_item_t *key0; + /*! Section item. */ + const yp_item_t *key1; + /*! Section identifier. */ + const uint8_t *id; + /*! Section identifier length. */ + size_t id_len; + /*! Consider item identifier as item data. */ + bool id_as_data; + + enum { + /*! Default item state. */ + NONE, + /*! New item indicator. */ + NEW, + /*! Old item indicator. */ + OLD + } type; + + struct { + /*! Section item data (NULL if not used). */ + conf_val_t *val; + /*! Index of data value to format (counted from 1, 0 means all). */ + size_t index; + /*! Binary data value (NULL if not used). */ + const uint8_t *bin; + /*! Length of the binary data value. */ + size_t bin_len; + } data; + + struct { + /*! Edit operation return code. */ + int code; + /*! Edit operation return error message. */ + const char *str; + } error; + + /*! Optional processing callback. */ + int (*fcn)(conf_io_t *); + /*! Miscellaneous data useful for the callback. */ + void *misc; +}; + +inline static conf_io_type_t conf_io_trie_val(trie_it_t *it) +{ + return (conf_io_type_t)(uintptr_t)(*trie_it_val(it)); +} + +/*! + * Starts new writing transaction. + * + * \param[in] child Nested transaction indicator. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_io_begin( + bool child +); + +/*! + * Commits the current writing transaction. + * + * \note Remember to call conf_refresh to publish the changes into the common + * configuration. + * + * \param[in] child Nested transaction indicator. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_io_commit( + bool child +); + +/*! + * Aborts the current writing transaction. + * + * \param[in] child Nested transaction indicator. + */ +void conf_io_abort( + bool child +); + +/*! + * Gets the configuration sections list or section items list. + * + * \param[in] key0 Section name (NULL to get section list). + * \param[in] key1 Item name (non-NULL to get value list). + * \param[in] id Section identifier name if needed for value list. + * \param[in] list_schema List schema items or option values. + * \param[in] get_current The current configuration or the active transaction switch. + * \param[out] io Operation output. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_io_list( + const char *key0, + const char *key1, + const char *id, + bool list_schema, + bool get_current, + conf_io_t *io +); + +/*! + * Gets the configuration difference between the current configuration and + * the active transaction. + * + * \param[in] key0 Section name (NULL to diff all sections). + * \param[in] key1 Item name (NULL to diff all section items). + * \param[in] id Section identifier name (NULL to consider all section identifiers). + * \param[out] io Operation output. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_io_diff( + const char *key0, + const char *key1, + const char *id, + conf_io_t *io +); + +/*! + * Gets the configuration item(s) value(s). + * + * \param[in] key0 Section name (NULL to get all sections). + * \param[in] key1 Item name (NULL to get all section items). + * \param[in] id Section identifier name (NULL to consider all section identifiers). + * \param[in] get_current The current configuration or the active transaction switch. + * \param[out] io Operation output. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_io_get( + const char *key0, + const char *key1, + const char *id, + bool get_current, + conf_io_t *io +); + +/*! + * Sets the configuration item(s) value. + * + * \param[in] key0 Section name. + * \param[in] key1 Item name (NULL to add identifier only). + * \param[in] id Section identifier name (NULL to consider all section identifiers). + * \param[in] data Item data to set/add. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_io_set( + const char *key0, + const char *key1, + const char *id, + const char *data +); + +/*! + * Unsets the configuration item(s) value(s). + * + * \param[in] key0 Section name (NULL to unset all sections). + * \param[in] key1 Item name (NULL to unset the whole section). + * \param[in] id Section identifier name (NULL to consider all section identifiers). + * \param[in] data Item data (NULL to unset all data). + * + * \return Error code, KNOT_EOK if success. + */ +int conf_io_unset( + const char *key0, + const char *key1, + const char *id, + const char *data +); + +/*! + * Checks the configuration database semantics in the current writing transaction. + * + * \param[out] io Operation output. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_io_check( + conf_io_t *io +); diff --git a/src/knot/conf/migration.c b/src/knot/conf/migration.c new file mode 100644 index 0000000..7e881b6 --- /dev/null +++ b/src/knot/conf/migration.c @@ -0,0 +1,81 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "knot/common/log.h" +#include "knot/conf/migration.h" +#include "knot/conf/confdb.h" + +/* +static void try_unset(conf_t *conf, knot_db_txn_t *txn, yp_name_t *key0, yp_name_t *key1) +{ + int ret = conf_db_unset(conf, txn, key0, key1, NULL, 0, NULL, 0, true); + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + log_warning("conf, migration, failed to unset '%s%s%s' (%s)", + key0 + 1, + (key1 != NULL) ? "/" : "", + (key1 != NULL) ? key1 + 1 : "", + knot_strerror(ret)); + } +} + +#define check_set(conf, txn, key0, key1, id, id_len, data, data_len) \ + ret = conf_db_set(conf, txn, key0, key1, id, id_len, data, data_len); \ + if (ret != KNOT_EOK && ret != KNOT_CONF_EREDEFINE) { \ + log_error("conf, migration, failed to set '%s%s%s' (%s)", \ + key0 + 1, \ + (key1 != NULL) ? "/" : "", \ + (key1 != NULL) ? key1 + 1 : "", \ + knot_strerror(ret)); \ + return ret; \ + } + +static int migrate_( + conf_t *conf, + knot_db_txn_t *txn) +{ + return KNOT_EOK; +} +*/ + +int conf_migrate( + conf_t *conf) +{ + return KNOT_EOK; + /* + if (conf == NULL) { + return KNOT_EINVAL; + } + + knot_db_txn_t txn; + int ret = conf->api->txn_begin(conf->db, &txn, 0); + if (ret != KNOT_EOK) { + return ret; + } + + ret = migrate_(conf, &txn); + if (ret != KNOT_EOK) { + conf->api->txn_abort(&txn); + return ret; + } + + ret = conf->api->txn_commit(&txn); + if (ret != KNOT_EOK) { + return ret; + } + + return conf_refresh_txn(conf); + */ +} diff --git a/src/knot/conf/migration.h b/src/knot/conf/migration.h new file mode 100644 index 0000000..f8d0793 --- /dev/null +++ b/src/knot/conf/migration.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/conf/base.h" + +/*! + * Migrates from an old configuration schema. + * + * \param[in] conf Configuration. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_migrate( + conf_t *conf +); diff --git a/src/knot/conf/module.c b/src/knot/conf/module.c new file mode 100644 index 0000000..d5d9642 --- /dev/null +++ b/src/knot/conf/module.c @@ -0,0 +1,509 @@ +/* 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 <dlfcn.h> +#include <fcntl.h> +#include <glob.h> +#include <sys/stat.h> +#include <unistd.h> +#include <urcu.h> + +#include "knot/conf/conf.h" +#include "knot/conf/confio.h" +#include "knot/conf/module.h" +#include "knot/common/log.h" +#include "knot/modules/static_modules.h" +#include "knot/nameserver/query_module.h" +#include "contrib/openbsd/strlcat.h" +#include "contrib/string.h" + +#define LIB_EXTENSION ".so" + +knot_dynarray_define(mod, module_t *, DYNARRAY_VISIBILITY_NORMAL) +knot_dynarray_define(old_schema, yp_item_t *, DYNARRAY_VISIBILITY_NORMAL) + +static module_t STATIC_MODULES[] = { + STATIC_MODULES_INIT + { NULL } +}; + +module_t *conf_mod_find( + conf_t *conf, + const char *name, + size_t len, + bool temporary) +{ + if (conf == NULL || name == NULL) { + return NULL; + } + + // First, search in static modules. + for (module_t *mod = STATIC_MODULES; mod->api != NULL; mod++) { + if (strncmp(name, mod->api->name, len) == 0) { + return mod; + } + } + + module_type_t excluded_type = temporary ? MOD_EXPLICIT : MOD_TEMPORARY; + + // Second, search in dynamic modules. + knot_dynarray_foreach(mod, module_t *, module, conf->modules) { + if ((*module) != NULL && (*module)->type != excluded_type && + strncmp(name, (*module)->api->name, len) == 0) { + return (*module); + } + } + + return NULL; +} + +static int mod_load( + conf_t *conf, + module_t *mod) +{ + static const yp_item_t module_common[] = { + { C_ID, YP_TSTR, YP_VNONE, CONF_IO_FREF }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } + }; + + yp_item_t *sub_items = NULL; + + int ret; + if (mod->api->config != NULL) { + ret = yp_schema_merge(&sub_items, module_common, mod->api->config); + } else { + ret = yp_schema_copy(&sub_items, module_common); + } + if (ret != KNOT_EOK) { + return ret; + } + + /* Synthesise module config section name. */ + const size_t name_len = strlen(mod->api->name); + if (name_len > YP_MAX_ITEM_NAME_LEN) { + return KNOT_YP_EINVAL_ITEM; + } + char name[1 + YP_MAX_ITEM_NAME_LEN + 1]; + name[0] = name_len; + memcpy(name + 1, mod->api->name, name_len + 1); + + const yp_item_t schema[] = { + { name, YP_TGRP, YP_VGRP = { sub_items }, + YP_FALLOC | YP_FMULTI | CONF_IO_FRLD_MOD | CONF_IO_FRLD_ZONES, + { mod->api->config_check } }, + { NULL } + }; + + yp_item_t *merged = NULL; + ret = yp_schema_merge(&merged, conf->schema, schema); + yp_schema_free(sub_items); + if (ret != KNOT_EOK) { + return ret; + } + + // Update configuration schema (with lazy free). + yp_item_t **current_schema = &conf->schema; + yp_item_t *old_schema = rcu_xchg_pointer(current_schema, merged); + synchronize_rcu(); + old_schema_dynarray_add(&conf->old_schemas, &old_schema); + + return KNOT_EOK; +} + +int conf_mod_load_common( + conf_t *conf) +{ + if (conf == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + + // First, load static modules. + for (module_t *mod = STATIC_MODULES; mod->api != NULL; mod++) { + ret = mod_load(conf, mod); + if (ret != KNOT_EOK) { + log_error("module '%s', failed to load (%s)", + mod->api->name, knot_strerror(ret)); + break; + } + + log_debug("module '%s', loaded static", mod->api->name); + } + + // Second, try to load implicit shared modules if configured. + if (strlen(MODULE_DIR) > 0) { + struct stat path_stat; + glob_t glob_buf = { 0 }; + + char *path = sprintf_alloc("%s/*%s", MODULE_DIR, LIB_EXTENSION); + if (path == NULL) { + ret = KNOT_ENOMEM; + } else if (stat(MODULE_DIR, &path_stat) != 0 || + !S_ISDIR(path_stat.st_mode)) { + if (errno == ENOENT) { + // Module directory doesn't exist. + ret = KNOT_EOK; + } else { + log_error("module, invalid directory '%s'", + MODULE_DIR); + ret = KNOT_EINVAL; + } + } else if (access(MODULE_DIR, F_OK | R_OK) != 0) { + log_error("module, failed to access directory '%s'", + MODULE_DIR); + ret = KNOT_EACCES; + } else { + ret = glob(path, 0, NULL, &glob_buf); + if (ret != 0 && ret != GLOB_NOMATCH) { + log_error("module, failed to read directory '%s'", + MODULE_DIR); + ret = KNOT_EACCES; + } else { + ret = KNOT_EOK; + } + } + + // Process each module in the directory. + for (size_t i = 0; i < glob_buf.gl_pathc; i++) { + (void)conf_mod_load_extra(conf, NULL, glob_buf.gl_pathv[i], + MOD_IMPLICIT); + } + + globfree(&glob_buf); + free(path); + } + + conf_mod_load_purge(conf, false); + + return ret; +} + +int conf_mod_load_extra( + conf_t *conf, + const char *mod_name, + const char *file_name, + module_type_t type) +{ + if (conf == NULL || (mod_name == NULL && file_name == NULL)) { + return KNOT_EINVAL; + } + + // Synthesize module file name if not specified. + char *tmp_name = NULL; + if (file_name == NULL) { + tmp_name = sprintf_alloc("%s/%s%s", MODULE_INSTDIR, + mod_name + strlen(KNOTD_MOD_NAME_PREFIX), + LIB_EXTENSION); + if (tmp_name == NULL) { + return KNOT_ENOMEM; + } + file_name = tmp_name; + } + + void *handle = dlopen(file_name, RTLD_NOW | RTLD_LOCAL); + if (handle == NULL) { + log_error("module, failed to open '%s' (%s)", file_name, dlerror()); + free(tmp_name); + return KNOT_ENOENT; + } + (void)dlerror(); + + knotd_mod_api_t *api = dlsym(handle, "knotd_mod_api"); + if (api == NULL) { + char *err = dlerror(); + if (err == NULL) { + err = "empty symbol"; + } + log_error("module, invalid library '%s' (%s)", file_name, err); + dlclose(handle); + free(tmp_name); + return KNOT_ENOENT; + } + free(tmp_name); + + if (api->version != KNOTD_MOD_ABI_VERSION) { + log_error("module '%s', incompatible version", api->name); + dlclose(handle); + return KNOT_ENOTSUP; + } + + if (api->name == NULL || (mod_name != NULL && strcmp(api->name, mod_name) != 0)) { + log_error("module '%s', module name mismatch", api->name); + dlclose(handle); + return KNOT_ENOTSUP; + } + + // Check if the module is already loaded. + module_t *found = conf_mod_find(conf, api->name, strlen(api->name), + type == MOD_TEMPORARY); + if (found != NULL) { + log_error("module '%s', duplicate module", api->name); + dlclose(handle); + return KNOT_EEXIST; + } + + module_t *mod = calloc(1, sizeof(*mod)); + if (mod == NULL) { + dlclose(handle); + return KNOT_ENOMEM; + } + mod->api = api; + mod->lib_handle = handle; + mod->type = type; + + int ret = mod_load(conf, mod); + if (ret != KNOT_EOK) { + log_error("module '%s', failed to load (%s)", api->name, + knot_strerror(ret)); + dlclose(handle); + free(mod); + return ret; + } + + mod_dynarray_add(&conf->modules, &mod); + + log_debug("module '%s', loaded shared", api->name); + + return KNOT_EOK; +} + +static void unload_shared( + module_t *mod) +{ + if (mod != NULL) { + assert(mod->lib_handle); + (void)dlclose(mod->lib_handle); + free(mod); + } +} + +void conf_mod_load_purge( + conf_t *conf, + bool temporary) +{ + if (conf == NULL) { + return; + } + + // Switch the current temporary schema with the initial one. + if (temporary && conf->old_schemas.size > 0) { + yp_item_t **current_schema = &conf->schema; + yp_item_t **initial = &(conf->old_schemas.arr(&conf->old_schemas))[0]; + + yp_item_t *old_schema = rcu_xchg_pointer(current_schema, *initial); + synchronize_rcu(); + *initial = old_schema; + } + + knot_dynarray_foreach(old_schema, yp_item_t *, schema, conf->old_schemas) { + yp_schema_free(*schema); + } + old_schema_dynarray_free(&conf->old_schemas); + + knot_dynarray_foreach(mod, module_t *, module, conf->modules) { + if ((*module) != NULL && (*module)->type == MOD_TEMPORARY) { + unload_shared((*module)); + *module = NULL; // Cannot remove from dynarray. + } + } +} + +void conf_mod_unload_shared( + conf_t *conf) +{ + if (conf == NULL) { + return; + } + + knot_dynarray_foreach(mod, module_t *, module, conf->modules) { + unload_shared((*module)); + } + mod_dynarray_free(&conf->modules); +} + +#define LOG_ARGS(mod_id, msg) "module '%s%s%.*s', " msg, \ + mod_id->name + 1, (mod_id->len > 0) ? "/" : "", (int)mod_id->len, \ + mod_id->data + +#define MOD_ID_LOG(zone, level, mod_id, msg, ...) \ + if (zone != NULL) \ + log_zone_##level(zone, LOG_ARGS(mod_id, msg), ##__VA_ARGS__); \ + else \ + log_##level(LOG_ARGS(mod_id, msg), ##__VA_ARGS__); + +void conf_activate_modules( + conf_t *conf, + struct server *server, + const knot_dname_t *zone_name, + list_t *query_modules, + struct query_plan **query_plan) +{ + int ret = KNOT_EOK; + + if (conf == NULL || query_modules == NULL || query_plan == NULL) { + ret = KNOT_EINVAL; + goto activate_error; + } + + conf_val_t val; + + // Get list of associated modules. + if (zone_name != NULL) { + val = conf_zone_get(conf, C_MODULE, zone_name); + } else { + val = conf_default_get(conf, C_GLOBAL_MODULE); + } + + switch (val.code) { + case KNOT_EOK: + break; + case KNOT_ENOENT: // Check if a module is configured at all. + case KNOT_YP_EINVAL_ID: + return; + default: + ret = val.code; + goto activate_error; + } + + // Create query plan. + *query_plan = query_plan_create(); + if (*query_plan == NULL) { + ret = KNOT_ENOMEM; + goto activate_error; + } + + // Initialize query modules list. + init_list(query_modules); + + // Open the modules. + while (val.code == KNOT_EOK) { + conf_mod_id_t *mod_id = conf_mod_id(&val); + if (mod_id == NULL) { + ret = KNOT_ENOMEM; + goto activate_error; + } + + // Open the module. + knotd_mod_t *mod = query_module_open(conf, server, mod_id, *query_plan, + zone_name); + if (mod == NULL) { + MOD_ID_LOG(zone_name, error, mod_id, "failed to open"); + conf_free_mod_id(mod_id); + goto skip_module; + } + + // Check the module scope. + if ((zone_name == NULL && !(mod->api->flags & KNOTD_MOD_FLAG_SCOPE_GLOBAL)) || + (zone_name != NULL && !(mod->api->flags & KNOTD_MOD_FLAG_SCOPE_ZONE))) { + MOD_ID_LOG(zone_name, error, mod_id, "out of scope"); + query_module_close(mod); + goto skip_module; + } + + // Check if the module is loadable. + if (mod->api->load == NULL) { + MOD_ID_LOG(zone_name, debug, mod_id, "empty module, not loaded"); + query_module_close(mod); + goto skip_module; + } + + // Load the module. + ret = mod->api->load(mod); + if (ret != KNOT_EOK) { + MOD_ID_LOG(zone_name, error, mod_id, "failed to load (%s)", + knot_strerror(ret)); + query_module_close(mod); + goto skip_module; + } + mod->config = NULL; // Invalidate the current config. + + add_tail(query_modules, &mod->node); +skip_module: + conf_val_next(&val); + } + + return; +activate_error: + CONF_LOG(LOG_ERR, "failed to activate modules (%s)", knot_strerror(ret)); +} + +void conf_deactivate_modules( + list_t *query_modules, + struct query_plan **query_plan) +{ + if (query_modules == NULL || query_plan == NULL) { + return; + } + + // Free query plan. + query_plan_free(*query_plan); + *query_plan = NULL; + + // Free query modules list. + knotd_mod_t *mod, *next; + WALK_LIST_DELSAFE(mod, next, *query_modules) { + if (mod->api->unload != NULL) { + mod->api->unload(mod); + } + query_module_close(mod); + } + init_list(query_modules); +} + +void conf_reset_modules( + conf_t *conf, + list_t *query_modules, + struct query_plan **query_plan) +{ + if (query_modules == NULL || query_plan == NULL) { + return; + } + + struct query_plan *new_plan = query_plan_create(); + if (new_plan == NULL) { + CONF_LOG(LOG_ERR, "failed to activate modules (%s)", knot_strerror(KNOT_ENOMEM)); + return; + } + + struct query_plan *old_plan = rcu_xchg_pointer(query_plan, NULL); + synchronize_rcu(); + query_plan_free(old_plan); + + knotd_mod_t *mod; + WALK_LIST(mod, *query_modules) { + if (mod->api->unload != NULL) { + mod->api->unload(mod); + } + query_module_reset(conf, mod, new_plan); + } + + knotd_mod_t *next; + WALK_LIST_DELSAFE(mod, next, *query_modules) { + int ret = mod->api->load(mod); + if (ret != KNOT_EOK) { + MOD_ID_LOG(mod->zone, error, mod->id, "failed to load (%s)", + knot_strerror(ret)); + rem_node(&mod->node); + query_module_close(mod); + continue; + } + mod->config = NULL; // Invalidate the current config. + } + + (void)rcu_xchg_pointer(query_plan, new_plan); +} diff --git a/src/knot/conf/module.h b/src/knot/conf/module.h new file mode 100644 index 0000000..a821792 --- /dev/null +++ b/src/knot/conf/module.h @@ -0,0 +1,126 @@ +/* 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/>. + */ + +#pragma once + +#include "knot/conf/base.h" + +struct server; + +/*! + * Finds specific module in static or dynamic modules. + * + * \param[in] conf Configuration. + * \param[in] name Module name. + * \param[in] len Module name length. + * \param[in] temporary Find only a temporary module indication. + * + * \return Module, NULL if not found. + */ +module_t *conf_mod_find( + conf_t *conf, + const char *name, + size_t len, + bool temporary +); + +/*! + * Loads common static and shared modules. + * + * \param[in] conf Configuration. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_mod_load_common( + conf_t *conf +); + +/*! + * Loads extra shared module. + * + * \param[in] conf Configuration. + * \param[in] mod_name Module name. + * \param[in] file_name Shared library file name. + * \param[in] type Type of module. + * + * \return Error code, KNOT_EOK if success. + */ +int conf_mod_load_extra( + conf_t *conf, + const char *mod_name, + const char *file_name, + module_type_t type +); + +/*! + * Purges temporary schemas and modules after all modules loading. + * + * \param[in] conf Configuration. + * \param[in] temporary Purge only temporary modules indication. + */ +void conf_mod_load_purge( + conf_t *conf, + bool temporary +); + +/*! + * Unloads all shared modules. + * + * \param[in] conf Configuration. + */ +void conf_mod_unload_shared( + conf_t *conf +); + +/*! + * Activates configured query modules for the specified zone or for all zones. + * + * \param[in] conf Configuration. + * \param[in] zone_name Zone name, NULL for all zones. + * \param[in] query_modules Destination query modules list. + * \param[in] query_plan Destination query plan. + */ +void conf_activate_modules( + conf_t *conf, + struct server *server, + const knot_dname_t *zone_name, + list_t *query_modules, + struct query_plan **query_plan +); + +/*! + * Deactivates query modules list. + * + * \param[in] query_modules Destination query modules list. + * \param[in] query_plan Destination query plan. + */ +void conf_deactivate_modules( + list_t *query_modules, + struct query_plan **query_plan +); + +/*! + * Re-activates query modules in list. + * + * \param[in] conf Configuration. + * \param[in] query_modules Query module list. + * \param[in] query_plan Query plan. + */ +void conf_reset_modules( + conf_t *conf, + list_t *query_modules, + struct query_plan **query_plan +); diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c new file mode 100644 index 0000000..d8472b3 --- /dev/null +++ b/src/knot/conf/schema.c @@ -0,0 +1,530 @@ +/* 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 <limits.h> +#include <stdbool.h> +#include <stdlib.h> +#include <stdint.h> + +#include "knot/conf/schema.h" +#include "knot/conf/confio.h" +#include "knot/conf/tools.h" +#include "knot/common/log.h" +#include "knot/updates/acl.h" +#include "libknot/rrtype/opt.h" +#include "libdnssec/tsig.h" +#include "libdnssec/key.h" + +#define HOURS(x) ((x) * 3600) +#define DAYS(x) ((x) * HOURS(24)) + +#define KILO(x) (1024LLU * (x)) +#define MEGA(x) (KILO(1024) * (x)) +#define GIGA(x) (MEGA(1024) * (x)) +#define TERA(x) (GIGA(1024) * (x)) + +#define VIRT_MEM_TOP_32BIT MEGA(500) +#define VIRT_MEM_LIMIT(x) (((sizeof(void *) < 8) && ((x) > VIRT_MEM_TOP_32BIT)) \ + ? VIRT_MEM_TOP_32BIT : (x)) + +static const knot_lookup_t keystore_backends[] = { + { KEYSTORE_BACKEND_PEM, "pem" }, + { KEYSTORE_BACKEND_PKCS11, "pkcs11" }, + { 0, NULL } +}; + +static const knot_lookup_t tsig_key_algs[] = { + { DNSSEC_TSIG_HMAC_MD5, "hmac-md5" }, + { DNSSEC_TSIG_HMAC_SHA1, "hmac-sha1" }, + { DNSSEC_TSIG_HMAC_SHA224, "hmac-sha224" }, + { DNSSEC_TSIG_HMAC_SHA256, "hmac-sha256" }, + { DNSSEC_TSIG_HMAC_SHA384, "hmac-sha384" }, + { DNSSEC_TSIG_HMAC_SHA512, "hmac-sha512" }, + { 0, NULL } +}; + +static const knot_lookup_t dnssec_key_algs[] = { + { DNSSEC_KEY_ALGORITHM_RSA_SHA1, "rsasha1" }, + { DNSSEC_KEY_ALGORITHM_RSA_SHA1_NSEC3, "rsasha1-nsec3-sha1" }, + { DNSSEC_KEY_ALGORITHM_RSA_SHA256, "rsasha256" }, + { DNSSEC_KEY_ALGORITHM_RSA_SHA512, "rsasha512" }, + { DNSSEC_KEY_ALGORITHM_ECDSA_P256_SHA256, "ecdsap256sha256" }, + { DNSSEC_KEY_ALGORITHM_ECDSA_P384_SHA384, "ecdsap384sha384" }, +#ifdef HAVE_ED25519 + { DNSSEC_KEY_ALGORITHM_ED25519, "ed25519" }, +#endif +#ifdef HAVE_ED448 + { DNSSEC_KEY_ALGORITHM_ED448, "ed448" }, +#endif + { 0, NULL } +}; + +static const knot_lookup_t unsafe_operation[] = { + { UNSAFE_NONE, "none" }, + { UNSAFE_KEYSET, "no-check-keyset" }, + { UNSAFE_DNSKEY, "no-update-dnskey" }, + { UNSAFE_NSEC, "no-update-nsec" }, + { UNSAFE_EXPIRED, "no-update-expired" }, + { 0, NULL } +}; + +static const knot_lookup_t cds_cdnskey[] = { + { CDS_CDNSKEY_NONE, "none" }, + { CDS_CDNSKEY_EMPTY, "delete-dnssec" }, + { CDS_CDNSKEY_ROLLOVER, "rollover" }, + { CDS_CDNSKEY_ALWAYS, "always" }, + { CDS_CDNSKEY_DOUBLE_DS, "double-ds" }, + { 0, NULL } +}; + +static const knot_lookup_t dnskey_mgmt[] = { + { DNSKEY_MGMT_FULL, "full" }, + { DNSKEY_MGMT_INCREMENTAL, "incremental" }, + { 0, NULL } +}; + +static const knot_lookup_t cds_digesttype[] = { + { DNSSEC_KEY_DIGEST_SHA256, "sha256" }, + { DNSSEC_KEY_DIGEST_SHA384, "sha384" }, + { 0, NULL } +}; + +const knot_lookup_t acl_actions[] = { + { ACL_ACTION_QUERY, "query" }, + { ACL_ACTION_NOTIFY, "notify" }, + { ACL_ACTION_TRANSFER, "transfer" }, + { ACL_ACTION_UPDATE, "update" }, + { 0, NULL } +}; + +static const knot_lookup_t acl_update_owner[] = { + { ACL_UPDATE_OWNER_KEY, "key" }, + { ACL_UPDATE_OWNER_ZONE, "zone" }, + { ACL_UPDATE_OWNER_NAME, "name" }, + { 0, NULL } +}; + +static const knot_lookup_t acl_update_owner_match[] = { + { ACL_UPDATE_MATCH_SUBEQ, "sub-or-equal" }, + { ACL_UPDATE_MATCH_EQ, "equal" }, + { ACL_UPDATE_MATCH_SUB, "sub" }, + { 0, NULL } +}; + +static const knot_lookup_t serial_policies[] = { + { SERIAL_POLICY_INCREMENT, "increment" }, + { SERIAL_POLICY_UNIXTIME, "unixtime" }, + { SERIAL_POLICY_DATESERIAL, "dateserial" }, + { 0, NULL } +}; + +static const knot_lookup_t semantic_checks[] = { + { SEMCHECKS_OFF, "off" }, + { SEMCHECKS_OFF, "false" }, + { SEMCHECKS_ON, "on" }, + { SEMCHECKS_ON, "true" }, + { SEMCHECKS_SOFT, "soft" }, + { 0, NULL } +}; + +static const knot_lookup_t zone_digest[] = { + { ZONE_DIGEST_NONE, "none" }, + { ZONE_DIGEST_SHA384, "zonemd-sha384" }, + { ZONE_DIGEST_SHA512, "zonemd-sha512" }, + { ZONE_DIGEST_REMOVE, "remove" }, + { 0, NULL } +}; + +static const knot_lookup_t journal_content[] = { + { JOURNAL_CONTENT_NONE, "none" }, + { JOURNAL_CONTENT_CHANGES, "changes" }, + { JOURNAL_CONTENT_ALL, "all" }, + { 0, NULL } +}; + +static const knot_lookup_t zonefile_load[] = { + { ZONEFILE_LOAD_NONE, "none" }, + { ZONEFILE_LOAD_DIFF, "difference" }, + { ZONEFILE_LOAD_DIFSE, "difference-no-serial" }, + { ZONEFILE_LOAD_WHOLE, "whole" }, + { 0, NULL } +}; + +static const knot_lookup_t log_severities[] = { + { LOG_UPTO(LOG_CRIT), "critical" }, + { LOG_UPTO(LOG_ERR), "error" }, + { LOG_UPTO(LOG_WARNING), "warning" }, + { LOG_UPTO(LOG_NOTICE), "notice" }, + { LOG_UPTO(LOG_INFO), "info" }, + { LOG_UPTO(LOG_DEBUG), "debug" }, + { 0, NULL } +}; + +static const knot_lookup_t journal_modes[] = { + { JOURNAL_MODE_ROBUST, "robust" }, + { JOURNAL_MODE_ASYNC, "asynchronous" }, + { 0, NULL } +}; + +static const knot_lookup_t catalog_roles[] = { + { CATALOG_ROLE_NONE, "none" }, + { CATALOG_ROLE_INTERPRET, "interpret" }, + { CATALOG_ROLE_GENERATE, "generate" }, + { CATALOG_ROLE_MEMBER, "member" }, + { 0, NULL } +}; + +static const knot_lookup_t dbus_events[] = { + { DBUS_EVENT_NONE, "none" }, + { DBUS_EVENT_RUNNING, "running" }, + { DBUS_EVENT_ZONE_UPDATED, "zone-updated" }, + { DBUS_EVENT_ZONE_SUBMISSION, "ksk-submission" }, + { DBUS_EVENT_ZONE_INVALID, "dnssec-invalid" }, + { 0, NULL } +}; + +static const yp_item_t desc_module[] = { + { C_ID, YP_TSTR, YP_VNONE, YP_FNONE, { check_module_id } }, + { C_FILE, YP_TSTR, YP_VNONE }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_server[] = { + { C_IDENT, YP_TSTR, YP_VNONE }, + { C_VERSION, YP_TSTR, YP_VNONE }, + { C_NSID, YP_THEX, YP_VNONE }, + { C_RUNDIR, YP_TSTR, YP_VSTR = { RUN_DIR } }, + { C_USER, YP_TSTR, YP_VNONE }, + { C_PIDFILE, YP_TSTR, YP_VSTR = { "knot.pid" } }, + { C_UDP_WORKERS, YP_TINT, YP_VINT = { 1, CONF_MAX_UDP_WORKERS, YP_NIL } }, + { C_TCP_WORKERS, YP_TINT, YP_VINT = { 1, CONF_MAX_TCP_WORKERS, YP_NIL } }, + { C_BG_WORKERS, YP_TINT, YP_VINT = { 1, CONF_MAX_BG_WORKERS, YP_NIL } }, + { C_ASYNC_START, YP_TBOOL, YP_VNONE }, + { C_TCP_IDLE_TIMEOUT, YP_TINT, YP_VINT = { 1, INT32_MAX, 10, YP_STIME } }, + { C_TCP_IO_TIMEOUT, YP_TINT, YP_VINT = { 0, INT32_MAX, 500 } }, + { C_TCP_RMT_IO_TIMEOUT, YP_TINT, YP_VINT = { 0, INT32_MAX, 5000 } }, + { C_TCP_MAX_CLIENTS, YP_TINT, YP_VINT = { 0, INT32_MAX, YP_NIL } }, + { C_TCP_REUSEPORT, YP_TBOOL, YP_VNONE }, + { C_TCP_FASTOPEN, YP_TBOOL, YP_VNONE }, + { C_QUIC_MAX_CLIENTS, YP_TINT, YP_VINT = { 128, INT32_MAX, 10000 } }, + { C_QUIC_OUTBUF_MAX_SIZE, YP_TINT, YP_VINT = { MEGA(1), SSIZE_MAX, MEGA(100), YP_SSIZE } }, + { C_QUIC_IDLE_CLOSE, YP_TINT, YP_VINT = { 1, INT32_MAX, 4, YP_STIME } }, + { C_RMT_POOL_LIMIT, YP_TINT, YP_VINT = { 0, INT32_MAX, 0 } }, + { C_RMT_POOL_TIMEOUT, YP_TINT, YP_VINT = { 1, INT32_MAX, 5, YP_STIME } }, + { C_RMT_RETRY_DELAY, YP_TINT, YP_VINT = { 0, INT32_MAX, 0 } }, + { C_SOCKET_AFFINITY, YP_TBOOL, YP_VNONE }, + { C_UDP_MAX_PAYLOAD, YP_TINT, YP_VINT = { KNOT_EDNS_MIN_DNSSEC_PAYLOAD, + KNOT_EDNS_MAX_UDP_PAYLOAD, + 1232, YP_SSIZE } }, + { C_UDP_MAX_PAYLOAD_IPV4, YP_TINT, YP_VINT = { KNOT_EDNS_MIN_DNSSEC_PAYLOAD, + KNOT_EDNS_MAX_UDP_PAYLOAD, + 1232, YP_SSIZE } }, + { C_UDP_MAX_PAYLOAD_IPV6, YP_TINT, YP_VINT = { KNOT_EDNS_MIN_DNSSEC_PAYLOAD, + KNOT_EDNS_MAX_UDP_PAYLOAD, + 1232, YP_SSIZE } }, + { C_CERT_FILE, YP_TSTR, YP_VNONE, YP_FNONE, { check_file } }, + { C_KEY_FILE, YP_TSTR, YP_VNONE, YP_FNONE, { check_file } }, + { C_ECS, YP_TBOOL, YP_VNONE }, + { C_ANS_ROTATION, YP_TBOOL, YP_VNONE }, + { C_AUTO_ACL, YP_TBOOL, YP_VNONE }, + { C_PROXY_ALLOWLIST, YP_TNET, YP_VNONE, YP_FMULTI}, + { C_DBUS_EVENT, YP_TOPT, YP_VOPT = { dbus_events, DBUS_EVENT_NONE }, YP_FMULTI }, + { C_DBUS_INIT_DELAY, YP_TINT, YP_VINT = { 0, INT32_MAX, 1, YP_STIME } }, + { C_LISTEN, YP_TADDR, YP_VADDR = { 53 }, YP_FMULTI, { check_listen } }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + // Legacy items. + { C_LISTEN_XDP, YP_TADDR, YP_VADDR = { 0 }, YP_FMULTI, { legacy_item } }, + { C_MAX_TCP_CLIENTS, YP_TINT, YP_VINT = { 0, INT32_MAX, 0 }, YP_FNONE, { legacy_item } }, + { C_TCP_HSHAKE_TIMEOUT, YP_TINT, YP_VINT = { 0, INT32_MAX, 0, YP_STIME }, YP_FNONE, { legacy_item } }, + { C_TCP_REPLY_TIMEOUT, YP_TINT, YP_VINT = { 0, INT32_MAX, 0, YP_STIME }, YP_FNONE, { legacy_item } }, + { C_MAX_UDP_PAYLOAD, YP_TINT, YP_VINT = { 0, INT32_MAX, 0, YP_SSIZE }, YP_FNONE, { legacy_item } }, + { C_MAX_IPV4_UDP_PAYLOAD, YP_TINT, YP_VINT = { 0, INT32_MAX, 0, YP_SSIZE }, YP_FNONE, { legacy_item } }, + { C_MAX_IPV6_UDP_PAYLOAD, YP_TINT, YP_VINT = { 0, INT32_MAX, 0, YP_SSIZE }, YP_FNONE, { legacy_item } }, + { NULL } +}; + +static const yp_item_t desc_xdp[] = { + { C_LISTEN, YP_TADDR, YP_VADDR = { 53 }, YP_FMULTI, { check_xdp_listen } }, + { C_UDP, YP_TBOOL, YP_VBOOL = { true } }, + { C_TCP, YP_TBOOL, YP_VNONE }, + { C_QUIC, YP_TBOOL, YP_VNONE }, + { C_QUIC_PORT, YP_TINT, YP_VINT = { 1, 65535, 853 } }, + { C_QUIC_LOG, YP_TBOOL, YP_VNONE }, + { C_TCP_MAX_CLIENTS, YP_TINT, YP_VINT = { 1024, INT32_MAX, 1000000 } }, + { C_TCP_INBUF_MAX_SIZE, YP_TINT, YP_VINT = { MEGA(1), SSIZE_MAX, MEGA(100), YP_SSIZE } }, + { C_TCP_OUTBUF_MAX_SIZE, YP_TINT, YP_VINT = { MEGA(1), SSIZE_MAX, MEGA(100), YP_SSIZE } }, + { C_TCP_IDLE_CLOSE, YP_TINT, YP_VINT = { 1, INT32_MAX, 10, YP_STIME } }, + { C_TCP_IDLE_RESET, YP_TINT, YP_VINT = { 1, INT32_MAX, 20, YP_STIME } }, + { C_TCP_RESEND, YP_TINT, YP_VINT = { 1, INT32_MAX, 5, YP_STIME } }, + { C_ROUTE_CHECK, YP_TBOOL, YP_VNONE }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_control[] = { + { C_LISTEN, YP_TSTR, YP_VSTR = { "knot.sock" } }, + { C_TIMEOUT, YP_TINT, YP_VINT = { 0, INT32_MAX / 1000, 5, YP_STIME } }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_log[] = { + { C_TARGET, YP_TSTR, YP_VNONE }, + { C_SERVER, YP_TOPT, YP_VOPT = { log_severities, 0 } }, + { C_CTL, YP_TOPT, YP_VOPT = { log_severities, 0 } }, + { C_ZONE, YP_TOPT, YP_VOPT = { log_severities, 0 } }, + { C_ANY, YP_TOPT, YP_VOPT = { log_severities, 0 } }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_stats[] = { + { C_TIMER, YP_TINT, YP_VINT = { 1, UINT32_MAX, 0, YP_STIME } }, + { C_FILE, YP_TSTR, YP_VSTR = { "stats.yaml" } }, + { C_APPEND, YP_TBOOL, YP_VNONE }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_database[] = { + { C_STORAGE, YP_TSTR, YP_VSTR = { STORAGE_DIR } }, + { C_JOURNAL_DB, YP_TSTR, YP_VSTR = { "journal" } }, + { C_JOURNAL_DB_MODE, YP_TOPT, YP_VOPT = { journal_modes, JOURNAL_MODE_ROBUST } }, + { C_JOURNAL_DB_MAX_SIZE, YP_TINT, YP_VINT = { MEGA(1), VIRT_MEM_LIMIT(TERA(100)), + VIRT_MEM_LIMIT(GIGA(20)), YP_SSIZE } }, + { C_KASP_DB, YP_TSTR, YP_VSTR = { "keys" } }, + { C_KASP_DB_MAX_SIZE, YP_TINT, YP_VINT = { MEGA(5), VIRT_MEM_LIMIT(GIGA(100)), + MEGA(500), YP_SSIZE } }, + { C_TIMER_DB, YP_TSTR, YP_VSTR = { "timers" } }, + { C_TIMER_DB_MAX_SIZE, YP_TINT, YP_VINT = { MEGA(1), VIRT_MEM_LIMIT(GIGA(100)), + MEGA(100), YP_SSIZE } }, + { C_CATALOG_DB, YP_TSTR, YP_VSTR = { "catalog" } }, + { C_CATALOG_DB_MAX_SIZE, YP_TINT, YP_VINT = { MEGA(5), VIRT_MEM_LIMIT(GIGA(100)), + VIRT_MEM_LIMIT(GIGA(20)), YP_SSIZE } }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_keystore[] = { + { C_ID, YP_TSTR, YP_VNONE }, + { C_BACKEND, YP_TOPT, YP_VOPT = { keystore_backends, KEYSTORE_BACKEND_PEM }, + CONF_IO_FRLD_ZONES }, + { C_CONFIG, YP_TSTR, YP_VSTR = { "keys" }, CONF_IO_FRLD_ZONES }, + { C_KEY_LABEL, YP_TBOOL, YP_VNONE }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_key[] = { + { C_ID, YP_TDNAME, YP_VNONE }, + { C_ALG, YP_TOPT, YP_VOPT = { tsig_key_algs, DNSSEC_TSIG_UNKNOWN } }, + { C_SECRET, YP_TB64, YP_VNONE }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_remote[] = { + { C_ID, YP_TSTR, YP_VNONE, CONF_IO_FREF }, + { C_ADDR, YP_TADDR, YP_VADDR = { 53 }, YP_FMULTI }, + { C_VIA, YP_TADDR, YP_VNONE, YP_FMULTI }, + { C_KEY, YP_TREF, YP_VREF = { C_KEY }, YP_FNONE, { check_ref } }, + { C_BLOCK_NOTIFY_XFR, YP_TBOOL, YP_VNONE }, + { C_NO_EDNS, YP_TBOOL, YP_VNONE }, + { C_AUTO_ACL, YP_TBOOL, YP_VBOOL = { true } }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_remotes[] = { + { C_ID, YP_TSTR, YP_VNONE, CONF_IO_FREF }, + { C_RMT, YP_TREF, YP_VREF = { C_RMT }, YP_FMULTI, { check_ref } }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_acl[] = { + { C_ID, YP_TSTR, YP_VNONE, CONF_IO_FREF }, + { C_ADDR, YP_TNET, YP_VNONE, YP_FMULTI }, + { C_KEY, YP_TREF, YP_VREF = { C_KEY }, YP_FMULTI, { check_ref } }, + { C_RMT, YP_TREF, YP_VREF = { C_RMT, C_RMTS }, YP_FMULTI, { check_ref } }, + { C_ACTION, YP_TOPT, YP_VOPT = { acl_actions, ACL_ACTION_QUERY }, YP_FMULTI }, + { C_DENY, YP_TBOOL, YP_VNONE }, + { C_UPDATE_TYPE, YP_TDATA, YP_VDATA = { 0, NULL, rrtype_to_bin, rrtype_to_txt }, + YP_FMULTI, }, + { C_UPDATE_OWNER, YP_TOPT, YP_VOPT = { acl_update_owner, ACL_UPDATE_OWNER_NONE } }, + { C_UPDATE_OWNER_MATCH, YP_TOPT, YP_VOPT = { acl_update_owner_match, ACL_UPDATE_MATCH_SUBEQ } }, + { C_UPDATE_OWNER_NAME, YP_TDATA, YP_VDATA = { 0, NULL, rdname_to_bin, rdname_to_txt }, + YP_FMULTI, }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_submission[] = { + { C_ID, YP_TSTR, YP_VNONE }, + { C_PARENT, YP_TREF, YP_VREF = { C_RMT, C_RMTS }, YP_FMULTI, { check_ref } }, + { C_CHK_INTERVAL, YP_TINT, YP_VINT = { 1, UINT32_MAX, HOURS(1), YP_STIME } }, + { C_TIMEOUT, YP_TINT, YP_VINT = { 0, UINT32_MAX, 0, YP_STIME }, + CONF_IO_FRLD_ZONES }, + { C_PARENT_DELAY, YP_TINT, YP_VINT = { 0, UINT32_MAX, 0, YP_STIME } }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +static const yp_item_t desc_policy[] = { + { C_ID, YP_TSTR, YP_VNONE, CONF_IO_FREF }, + { C_KEYSTORE, YP_TREF, YP_VREF = { C_KEYSTORE }, CONF_IO_FRLD_ZONES, + { check_ref_dflt } }, + { C_MANUAL, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES }, + { C_SINGLE_TYPE_SIGNING, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES }, + { C_ALG, YP_TOPT, YP_VOPT = { dnssec_key_algs, + DNSSEC_KEY_ALGORITHM_ECDSA_P256_SHA256 }, + CONF_IO_FRLD_ZONES }, + { C_KSK_SIZE, YP_TINT, YP_VINT = { 0, UINT16_MAX, YP_NIL, YP_SSIZE }, + CONF_IO_FRLD_ZONES }, + { C_ZSK_SIZE, YP_TINT, YP_VINT = { 0, UINT16_MAX, YP_NIL, YP_SSIZE }, + CONF_IO_FRLD_ZONES }, + { C_KSK_SHARED, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES }, + { C_DNSKEY_TTL, YP_TINT, YP_VINT = { 0, INT32_MAX, YP_NIL, YP_STIME }, + CONF_IO_FRLD_ZONES }, + { C_ZONE_MAX_TTL, YP_TINT, YP_VINT = { 0, INT32_MAX, YP_NIL, YP_STIME }, + CONF_IO_FRLD_ZONES }, + { C_KSK_LIFETIME, YP_TINT, YP_VINT = { 0, UINT32_MAX, 0, YP_STIME }, + CONF_IO_FRLD_ZONES }, + { C_ZSK_LIFETIME, YP_TINT, YP_VINT = { 0, UINT32_MAX, DAYS(30), YP_STIME }, + CONF_IO_FRLD_ZONES }, + { C_DELETE_DELAY, YP_TINT, YP_VINT = { 0, UINT32_MAX, 0, YP_STIME } }, + { C_PROPAG_DELAY, YP_TINT, YP_VINT = { 0, INT32_MAX, HOURS(1), YP_STIME }, + CONF_IO_FRLD_ZONES }, + { C_RRSIG_LIFETIME, YP_TINT, YP_VINT = { 1, INT32_MAX, DAYS(14), YP_STIME }, + CONF_IO_FRLD_ZONES }, + { C_RRSIG_REFRESH, YP_TINT, YP_VINT = { 1, INT32_MAX, YP_NIL, YP_STIME }, + CONF_IO_FRLD_ZONES }, + { C_RRSIG_PREREFRESH, YP_TINT, YP_VINT = { 0, INT32_MAX, HOURS(1), YP_STIME }, + CONF_IO_FRLD_ZONES }, + { C_REPRO_SIGNING, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES }, + { C_NSEC3, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES }, + { C_NSEC3_ITER, YP_TINT, YP_VINT = { 0, UINT16_MAX, 0 }, CONF_IO_FRLD_ZONES }, + { C_NSEC3_OPT_OUT, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES }, + { C_NSEC3_SALT_LEN, YP_TINT, YP_VINT = { 0, UINT8_MAX, 8 }, CONF_IO_FRLD_ZONES }, + { C_NSEC3_SALT_LIFETIME, YP_TINT, YP_VINT = { -1, UINT32_MAX, DAYS(30), YP_STIME }, + CONF_IO_FRLD_ZONES }, + { C_SIGNING_THREADS, YP_TINT, YP_VINT = { 1, UINT16_MAX, 1 } }, + { C_KSK_SBM, YP_TREF, YP_VREF = { C_SBM }, CONF_IO_FRLD_ZONES, + { check_ref } }, + { C_DS_PUSH, YP_TREF, YP_VREF = { C_RMT, C_RMTS }, YP_FMULTI | CONF_IO_FRLD_ZONES, + { check_ref } }, + { C_CDS_CDNSKEY, YP_TOPT, YP_VOPT = { cds_cdnskey, CDS_CDNSKEY_ROLLOVER }, + CONF_IO_FRLD_ZONES }, + { C_CDS_DIGESTTYPE, YP_TOPT, YP_VOPT = { cds_digesttype, DNSSEC_KEY_DIGEST_SHA256 }, + CONF_IO_FRLD_ZONES }, + { C_DNSKEY_MGMT, YP_TOPT, YP_VOPT = { dnskey_mgmt, DNSKEY_MGMT_FULL }, + CONF_IO_FRLD_ZONES }, + { C_OFFLINE_KSK, YP_TBOOL, YP_VNONE, CONF_IO_FRLD_ZONES }, + { C_UNSAFE_OPERATION, YP_TOPT, YP_VOPT = { unsafe_operation, UNSAFE_NONE }, YP_FMULTI }, + { C_COMMENT, YP_TSTR, YP_VNONE }, + { NULL } +}; + +#define ZONE_ITEMS(FLAGS) \ + { C_STORAGE, YP_TSTR, YP_VSTR = { STORAGE_DIR }, FLAGS }, \ + { C_FILE, YP_TSTR, YP_VNONE, FLAGS }, \ + { C_MASTER, YP_TREF, YP_VREF = { C_RMT, C_RMTS }, YP_FMULTI, { check_ref } }, \ + { C_DDNS_MASTER, YP_TREF, YP_VREF = { C_RMT }, YP_FNONE, { check_ref } }, \ + { C_NOTIFY, YP_TREF, YP_VREF = { C_RMT, C_RMTS }, YP_FMULTI, { check_ref } }, \ + { C_ACL, YP_TREF, YP_VREF = { C_ACL }, YP_FMULTI, { check_ref } }, \ + { C_PROVIDE_IXFR, YP_TBOOL, YP_VBOOL = { true } }, \ + { C_SEM_CHECKS, YP_TOPT, YP_VOPT = { semantic_checks, SEMCHECKS_OFF }, FLAGS }, \ + { C_ZONEFILE_SYNC, YP_TINT, YP_VINT = { -1, INT32_MAX, 0, YP_STIME } }, \ + { C_ZONEFILE_LOAD, YP_TOPT, YP_VOPT = { zonefile_load, ZONEFILE_LOAD_WHOLE } }, \ + { C_JOURNAL_CONTENT, YP_TOPT, YP_VOPT = { journal_content, JOURNAL_CONTENT_CHANGES }, FLAGS }, \ + { C_JOURNAL_MAX_USAGE, YP_TINT, YP_VINT = { KILO(40), SSIZE_MAX, MEGA(100), YP_SSIZE } }, \ + { C_JOURNAL_MAX_DEPTH, YP_TINT, YP_VINT = { 2, SSIZE_MAX, 20 } }, \ + { C_ZONE_MAX_SIZE, YP_TINT, YP_VINT = { 0, SSIZE_MAX, SSIZE_MAX, YP_SSIZE }, FLAGS }, \ + { C_ADJUST_THR, YP_TINT, YP_VINT = { 1, UINT16_MAX, 1 } }, \ + { C_DNSSEC_SIGNING, YP_TBOOL, YP_VNONE, FLAGS }, \ + { C_DNSSEC_VALIDATION, YP_TBOOL, YP_VNONE, FLAGS }, \ + { C_DNSSEC_POLICY, YP_TREF, YP_VREF = { C_POLICY }, FLAGS, { check_ref_dflt } }, \ + { C_DS_PUSH, YP_TREF, YP_VREF = { C_RMT, C_RMTS }, YP_FMULTI | FLAGS, \ + { check_ref } }, \ + { C_SERIAL_POLICY, YP_TOPT, YP_VOPT = { serial_policies, SERIAL_POLICY_INCREMENT } }, \ + { C_ZONEMD_GENERATE, YP_TOPT, YP_VOPT = { zone_digest, ZONE_DIGEST_NONE }, FLAGS }, \ + { C_ZONEMD_VERIFY, YP_TBOOL, YP_VNONE, FLAGS }, \ + { C_REFRESH_MIN_INTERVAL,YP_TINT, YP_VINT = { 2, UINT32_MAX, 2, YP_STIME } }, \ + { C_REFRESH_MAX_INTERVAL,YP_TINT, YP_VINT = { 2, UINT32_MAX, UINT32_MAX, YP_STIME } }, \ + { C_RETRY_MIN_INTERVAL, YP_TINT, YP_VINT = { 1, UINT32_MAX, 1, YP_STIME } }, \ + { C_RETRY_MAX_INTERVAL, YP_TINT, YP_VINT = { 1, UINT32_MAX, UINT32_MAX, YP_STIME } }, \ + { C_EXPIRE_MIN_INTERVAL, YP_TINT, YP_VINT = { 3, UINT32_MAX, 3, YP_STIME } }, \ + { C_EXPIRE_MAX_INTERVAL, YP_TINT, YP_VINT = { 3, UINT32_MAX, UINT32_MAX, YP_STIME } }, \ + { C_CATALOG_ROLE, YP_TOPT, YP_VOPT = { catalog_roles, CATALOG_ROLE_NONE }, FLAGS }, \ + { C_CATALOG_TPL, YP_TREF, YP_VREF = { C_TPL }, YP_FMULTI | FLAGS, { check_ref } }, \ + { C_CATALOG_ZONE, YP_TDNAME,YP_VNONE, FLAGS | CONF_IO_FRLD_ZONES }, \ + { C_CATALOG_GROUP, YP_TSTR, YP_VNONE, FLAGS | CONF_IO_FRLD_ZONES, { check_catalog_group } }, \ + { C_MODULE, YP_TDATA, YP_VDATA = { 0, NULL, mod_id_to_bin, mod_id_to_txt }, \ + YP_FMULTI | FLAGS, { check_modref } }, \ + { C_COMMENT, YP_TSTR, YP_VNONE }, \ + /* Legacy items.*/ \ + { C_DISABLE_ANY, YP_TBOOL, YP_VNONE, YP_FNONE, { legacy_item } }, \ + { C_MAX_ZONE_SIZE, YP_TINT, YP_VINT = { 0, SSIZE_MAX, 0, YP_SSIZE }, YP_FNONE, { legacy_item } }, \ + { C_MAX_JOURNAL_USAGE, YP_TINT, YP_VINT = { 0, SSIZE_MAX, 0, YP_SSIZE }, YP_FNONE, { legacy_item } }, \ + { C_MAX_JOURNAL_DEPTH, YP_TINT, YP_VINT = { 0, SSIZE_MAX, 0 }, YP_FNONE, { legacy_item } }, \ + { C_MAX_REFRESH_INTERVAL,YP_TINT, YP_VINT = { 0, SSIZE_MAX, 0, YP_STIME }, YP_FNONE, { legacy_item } }, \ + { C_MIN_REFRESH_INTERVAL,YP_TINT, YP_VINT = { 0, SSIZE_MAX, 0, YP_STIME }, YP_FNONE, { legacy_item } }, \ + +static const yp_item_t desc_template[] = { + { C_ID, YP_TSTR, YP_VNONE, CONF_IO_FREF }, + { C_GLOBAL_MODULE, YP_TDATA, YP_VDATA = { 0, NULL, mod_id_to_bin, mod_id_to_txt }, + YP_FMULTI | CONF_IO_FRLD_MOD, { check_modref } }, + ZONE_ITEMS(CONF_IO_FRLD_ZONES) + // Legacy items. + { C_TIMER_DB, YP_TSTR, YP_VSTR = { "" }, YP_FNONE, { legacy_item } }, + { C_MAX_TIMER_DB_SIZE, YP_TINT, YP_VINT = { 0, SSIZE_MAX, 0, YP_SSIZE }, YP_FNONE, { legacy_item } }, + { C_JOURNAL_DB, YP_TSTR, YP_VSTR = { "" }, YP_FNONE, { legacy_item } }, + { C_JOURNAL_DB_MODE, YP_TOPT, YP_VOPT = { journal_modes, 0 }, YP_FNONE, { legacy_item } }, + { C_MAX_JOURNAL_DB_SIZE, YP_TINT, YP_VINT = { 0, SSIZE_MAX, 0, YP_SSIZE }, YP_FNONE, { legacy_item } }, + { C_KASP_DB, YP_TSTR, YP_VSTR = { "" }, YP_FNONE, { legacy_item } }, + { C_MAX_KASP_DB_SIZE, YP_TINT, YP_VINT = { 0, SSIZE_MAX, 0, YP_SSIZE }, YP_FNONE, { legacy_item } }, + { NULL } +}; + +static const yp_item_t desc_zone[] = { + { C_DOMAIN, YP_TDNAME, YP_VNONE, CONF_IO_FRLD_ZONE }, + { C_TPL, YP_TREF, YP_VREF = { C_TPL }, CONF_IO_FRLD_ZONE, { check_ref } }, + ZONE_ITEMS(CONF_IO_FRLD_ZONE) + { NULL } +}; + +const yp_item_t conf_schema[] = { + { C_MODULE, YP_TGRP, YP_VGRP = { desc_module }, YP_FMULTI | CONF_IO_FRLD_ALL | + CONF_IO_FCHECK_ZONES, { load_module } }, + { C_SRV, YP_TGRP, YP_VGRP = { desc_server }, CONF_IO_FRLD_SRV, { check_server } }, + { C_XDP, YP_TGRP, YP_VGRP = { desc_xdp }, CONF_IO_FRLD_SRV, { check_xdp } }, + { C_CTL, YP_TGRP, YP_VGRP = { desc_control } }, + { C_LOG, YP_TGRP, YP_VGRP = { desc_log }, YP_FMULTI | CONF_IO_FRLD_LOG }, + { C_STATS, YP_TGRP, YP_VGRP = { desc_stats }, CONF_IO_FRLD_SRV }, + { C_DB, YP_TGRP, YP_VGRP = { desc_database }, CONF_IO_FRLD_SRV, { check_database } }, + { C_KEYSTORE, YP_TGRP, YP_VGRP = { desc_keystore }, YP_FMULTI, { check_keystore } }, + { C_KEY, YP_TGRP, YP_VGRP = { desc_key }, YP_FMULTI, { check_key } }, + { C_RMT, YP_TGRP, YP_VGRP = { desc_remote }, YP_FMULTI, { check_remote } }, + { C_RMTS, YP_TGRP, YP_VGRP = { desc_remotes }, YP_FMULTI, { check_remotes } }, + { C_ACL, YP_TGRP, YP_VGRP = { desc_acl }, YP_FMULTI, { check_acl } }, + { C_SBM, YP_TGRP, YP_VGRP = { desc_submission }, YP_FMULTI }, + { C_POLICY, YP_TGRP, YP_VGRP = { desc_policy }, YP_FMULTI, { check_policy } }, + { C_TPL, YP_TGRP, YP_VGRP = { desc_template }, YP_FMULTI, { check_template } }, + { C_ZONE, YP_TGRP, YP_VGRP = { desc_zone }, YP_FMULTI | CONF_IO_FZONE, { check_zone } }, + { C_INCL, YP_TSTR, YP_VNONE, CONF_IO_FDIFF_ZONES | CONF_IO_FRLD_ALL, { include_file } }, + { NULL } +}; diff --git a/src/knot/conf/schema.h b/src/knot/conf/schema.h new file mode 100644 index 0000000..5850acc --- /dev/null +++ b/src/knot/conf/schema.h @@ -0,0 +1,279 @@ +/* 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/>. + */ + +#pragma once + +#include "libknot/lookup.h" +#include "libknot/yparser/ypschema.h" + +#define C_ACL "\x03""acl" +#define C_ACTION "\x06""action" +#define C_ADDR "\x07""address" +#define C_ADJUST_THR "\x0E""adjust-threads" +#define C_ALG "\x09""algorithm" +#define C_ANS_ROTATION "\x0F""answer-rotation" +#define C_ANY "\x03""any" +#define C_APPEND "\x06""append" +#define C_ASYNC_START "\x0B""async-start" +#define C_AUTO_ACL "\x0D""automatic-acl" +#define C_BACKEND "\x07""backend" +#define C_BG_WORKERS "\x12""background-workers" +#define C_BLOCK_NOTIFY_XFR "\x1B""block-notify-after-transfer" +#define C_CATALOG_DB "\x0A""catalog-db" +#define C_CATALOG_DB_MAX_SIZE "\x13""catalog-db-max-size" +#define C_CATALOG_GROUP "\x0D""catalog-group" +#define C_CATALOG_ROLE "\x0C""catalog-role" +#define C_CATALOG_TPL "\x10""catalog-template" +#define C_CATALOG_ZONE "\x0C""catalog-zone" +#define C_CDS_CDNSKEY "\x13""cds-cdnskey-publish" +#define C_CDS_DIGESTTYPE "\x0F""cds-digest-type" +#define C_CERT_FILE "\x09""cert-file" +#define C_CHK_INTERVAL "\x0E""check-interval" +#define C_COMMENT "\x07""comment" +#define C_CONFIG "\x06""config" +#define C_CTL "\x07""control" +#define C_DB "\x08""database" +#define C_DBUS_EVENT "\x0A""dbus-event" +#define C_DBUS_INIT_DELAY "\x0F""dbus-init-delay" +#define C_DDNS_MASTER "\x0B""ddns-master" +#define C_DENY "\x04""deny" +#define C_DNSKEY_MGMT "\x11""dnskey-management" +#define C_DNSKEY_TTL "\x0A""dnskey-ttl" +#define C_DNSSEC_POLICY "\x0D""dnssec-policy" +#define C_DNSSEC_SIGNING "\x0E""dnssec-signing" +#define C_DNSSEC_VALIDATION "\x11""dnssec-validation" +#define C_DOMAIN "\x06""domain" +#define C_DS_PUSH "\x07""ds-push" +#define C_ECS "\x12""edns-client-subnet" +#define C_EXPIRE_MAX_INTERVAL "\x13""expire-max-interval" +#define C_EXPIRE_MIN_INTERVAL "\x13""expire-min-interval" +#define C_FILE "\x04""file" +#define C_GLOBAL_MODULE "\x0D""global-module" +#define C_ID "\x02""id" +#define C_IDENT "\x08""identity" +#define C_INCL "\x07""include" +#define C_JOURNAL_CONTENT "\x0F""journal-content" +#define C_JOURNAL_DB "\x0A""journal-db" +#define C_JOURNAL_DB_MAX_SIZE "\x13""journal-db-max-size" +#define C_JOURNAL_DB_MODE "\x0F""journal-db-mode" +#define C_JOURNAL_MAX_DEPTH "\x11""journal-max-depth" +#define C_JOURNAL_MAX_USAGE "\x11""journal-max-usage" +#define C_KASP_DB "\x07""kasp-db" +#define C_KASP_DB_MAX_SIZE "\x10""kasp-db-max-size" +#define C_DELETE_DELAY "\x0C""delete-delay" +#define C_KEY "\x03""key" +#define C_KEYSTORE "\x08""keystore" +#define C_KEY_FILE "\x08""key-file" +#define C_KEY_LABEL "\x09""key-label" +#define C_KSK_LIFETIME "\x0C""ksk-lifetime" +#define C_KSK_SBM "\x0E""ksk-submission" +#define C_KSK_SHARED "\x0a""ksk-shared" +#define C_KSK_SIZE "\x08""ksk-size" +#define C_LISTEN "\x06""listen" +#define C_LOG "\x03""log" +#define C_MANUAL "\x06""manual" +#define C_MASTER "\x06""master" +#define C_MODULE "\x06""module" +#define C_NO_EDNS "\x07""no-edns" +#define C_NOTIFY "\x06""notify" +#define C_NSEC3 "\x05""nsec3" +#define C_NSEC3_ITER "\x10""nsec3-iterations" +#define C_NSEC3_OPT_OUT "\x0D""nsec3-opt-out" +#define C_NSEC3_SALT_LEN "\x11""nsec3-salt-length" +#define C_NSEC3_SALT_LIFETIME "\x13""nsec3-salt-lifetime" +#define C_NSID "\x04""nsid" +#define C_OFFLINE_KSK "\x0B""offline-ksk" +#define C_PARENT "\x06""parent" +#define C_PARENT_DELAY "\x0C""parent-delay" +#define C_PIDFILE "\x07""pidfile" +#define C_POLICY "\x06""policy" +#define C_PROPAG_DELAY "\x11""propagation-delay" +#define C_PROVIDE_IXFR "\x0C""provide-ixfr" +#define C_PROXY_ALLOWLIST "\x0F""proxy-allowlist" +#define C_QUIC "\x04""quic" +#define C_QUIC_IDLE_CLOSE "\x17""quic-idle-close-timeout" +#define C_QUIC_LOG "\x08""quic-log" +#define C_QUIC_MAX_CLIENTS "\x10""quic-max-clients" +#define C_QUIC_OUTBUF_MAX_SIZE "\x14""quic-outbuf-max-size" +#define C_QUIC_PORT "\x09""quic-port" +#define C_REFRESH_MAX_INTERVAL "\x14""refresh-max-interval" +#define C_REFRESH_MIN_INTERVAL "\x14""refresh-min-interval" +#define C_REPRO_SIGNING "\x14""reproducible-signing" +#define C_RETRY_MAX_INTERVAL "\x12""retry-max-interval" +#define C_RETRY_MIN_INTERVAL "\x12""retry-min-interval" +#define C_RMT "\x06""remote" +#define C_RMTS "\x07""remotes" +#define C_RMT_POOL_LIMIT "\x11""remote-pool-limit" +#define C_RMT_POOL_TIMEOUT "\x13""remote-pool-timeout" +#define C_RMT_RETRY_DELAY "\x12""remote-retry-delay" +#define C_ROUTE_CHECK "\x0B""route-check" +#define C_RRSIG_LIFETIME "\x0E""rrsig-lifetime" +#define C_RRSIG_PREREFRESH "\x11""rrsig-pre-refresh" +#define C_RRSIG_REFRESH "\x0D""rrsig-refresh" +#define C_RUNDIR "\x06""rundir" +#define C_SBM "\x0A""submission" +#define C_SECRET "\x06""secret" +#define C_SEM_CHECKS "\x0F""semantic-checks" +#define C_SERIAL_POLICY "\x0D""serial-policy" +#define C_SERVER "\x06""server" +#define C_SIGNING_THREADS "\x0F""signing-threads" +#define C_SINGLE_TYPE_SIGNING "\x13""single-type-signing" +#define C_SOCKET_AFFINITY "\x0F""socket-affinity" +#define C_SRV "\x06""server" +#define C_STATS "\x0A""statistics" +#define C_STORAGE "\x07""storage" +#define C_TARGET "\x06""target" +#define C_TCP "\x03""tcp" +#define C_TCP_FASTOPEN "\x0C""tcp-fastopen" +#define C_TCP_IDLE_CLOSE "\x16""tcp-idle-close-timeout" +#define C_TCP_IDLE_RESET "\x16""tcp-idle-reset-timeout" +#define C_TCP_IDLE_TIMEOUT "\x10""tcp-idle-timeout" +#define C_TCP_INBUF_MAX_SIZE "\x12""tcp-inbuf-max-size" +#define C_TCP_IO_TIMEOUT "\x0E""tcp-io-timeout" +#define C_TCP_MAX_CLIENTS "\x0F""tcp-max-clients" +#define C_TCP_OUTBUF_MAX_SIZE "\x13""tcp-outbuf-max-size" +#define C_TCP_RESEND "\x12""tcp-resend-timeout" +#define C_TCP_REUSEPORT "\x0D""tcp-reuseport" +#define C_TCP_RMT_IO_TIMEOUT "\x15""tcp-remote-io-timeout" +#define C_TCP_WORKERS "\x0B""tcp-workers" +#define C_TIMEOUT "\x07""timeout" +#define C_TIMER "\x05""timer" +#define C_TIMER_DB "\x08""timer-db" +#define C_TIMER_DB_MAX_SIZE "\x11""timer-db-max-size" +#define C_TPL "\x08""template" +#define C_UDP "\x03""udp" +#define C_UDP_MAX_PAYLOAD "\x0F""udp-max-payload" +#define C_UDP_MAX_PAYLOAD_IPV4 "\x14""udp-max-payload-ipv4" +#define C_UDP_MAX_PAYLOAD_IPV6 "\x14""udp-max-payload-ipv6" +#define C_UDP_WORKERS "\x0B""udp-workers" +#define C_UNSAFE_OPERATION "\x10""unsafe-operation" +#define C_UPDATE_OWNER "\x0C""update-owner" +#define C_UPDATE_OWNER_MATCH "\x12""update-owner-match" +#define C_UPDATE_OWNER_NAME "\x11""update-owner-name" +#define C_UPDATE_TYPE "\x0B""update-type" +#define C_USER "\x04""user" +#define C_VERSION "\x07""version" +#define C_VIA "\x03""via" +#define C_XDP "\x03""xdp" +#define C_ZONE "\x04""zone" +#define C_ZONEFILE_LOAD "\x0D""zonefile-load" +#define C_ZONEFILE_SYNC "\x0D""zonefile-sync" +#define C_ZONEMD_GENERATE "\x0F""zonemd-generate" +#define C_ZONEMD_VERIFY "\x0D""zonemd-verify" +#define C_ZONE_MAX_SIZE "\x0D""zone-max-size" +#define C_ZONE_MAX_TTL "\x0C""zone-max-ttl" +#define C_ZSK_LIFETIME "\x0C""zsk-lifetime" +#define C_ZSK_SIZE "\x08""zsk-size" + +// Legacy items. +#define C_DISABLE_ANY "\x0B""disable-any" +#define C_LISTEN_XDP "\x0A""listen-xdp" +#define C_MAX_TIMER_DB_SIZE "\x11""max-timer-db-size" +#define C_MAX_JOURNAL_DB_SIZE "\x13""max-journal-db-size" +#define C_MAX_KASP_DB_SIZE "\x10""max-kasp-db-size" +#define C_TCP_HSHAKE_TIMEOUT "\x15""tcp-handshake-timeout" +#define C_TCP_REPLY_TIMEOUT "\x11""tcp-reply-timeout" +#define C_MAX_TCP_CLIENTS "\x0F""max-tcp-clients" +#define C_MAX_UDP_PAYLOAD "\x0F""max-udp-payload" +#define C_MAX_IPV4_UDP_PAYLOAD "\x14""max-ipv4-udp-payload" +#define C_MAX_IPV6_UDP_PAYLOAD "\x14""max-ipv6-udp-payload" +#define C_MAX_ZONE_SIZE "\x0D""max-zone-size" +#define C_MAX_REFRESH_INTERVAL "\x14""max-refresh-interval" +#define C_MIN_REFRESH_INTERVAL "\x14""min-refresh-interval" +#define C_MAX_JOURNAL_DEPTH "\x11""max-journal-depth" +#define C_MAX_JOURNAL_USAGE "\x11""max-journal-usage" + +enum { + KEYSTORE_BACKEND_PEM = 1, + KEYSTORE_BACKEND_PKCS11 = 2, +}; + +enum { + UNSAFE_NONE = 0, + UNSAFE_KEYSET = (1 << 0), + UNSAFE_DNSKEY = (1 << 1), + UNSAFE_NSEC = (1 << 2), + UNSAFE_EXPIRED = (1 << 3), +}; + +enum { + CDS_CDNSKEY_NONE = 0, + CDS_CDNSKEY_EMPTY = 1, + CDS_CDNSKEY_ROLLOVER = 2, + CDS_CDNSKEY_ALWAYS = 3, + CDS_CDNSKEY_DOUBLE_DS = 4, +}; + +enum { + DNSKEY_MGMT_FULL = 0, + DNSKEY_MGMT_INCREMENTAL = 1, +}; + +enum { + SERIAL_POLICY_INCREMENT = 1, + SERIAL_POLICY_UNIXTIME = 2, + SERIAL_POLICY_DATESERIAL = 3, +}; + +enum { + SEMCHECKS_OFF = 0, + SEMCHECKS_ON = 1, + SEMCHECKS_SOFT = 2, +}; + +enum { + ZONE_DIGEST_NONE = 0, + ZONE_DIGEST_SHA384 = 1, + ZONE_DIGEST_SHA512 = 2, + ZONE_DIGEST_REMOVE = 255, +}; + +enum { + JOURNAL_CONTENT_NONE = 0, + JOURNAL_CONTENT_CHANGES = 1, + JOURNAL_CONTENT_ALL = 2, +}; + +enum { + JOURNAL_MODE_ROBUST = 0, // Robust journal DB disk synchronization. + JOURNAL_MODE_ASYNC = 1, // Asynchronous journal DB disk synchronization. +}; + +enum { + ZONEFILE_LOAD_NONE = 0, + ZONEFILE_LOAD_DIFF = 1, + ZONEFILE_LOAD_WHOLE = 2, + ZONEFILE_LOAD_DIFSE = 3, +}; + +enum { + CATALOG_ROLE_NONE = 0, + CATALOG_ROLE_INTERPRET = 1, + CATALOG_ROLE_GENERATE = 2, + CATALOG_ROLE_MEMBER = 3, +}; + +enum { + DBUS_EVENT_NONE = 0, + DBUS_EVENT_RUNNING = (1 << 0), + DBUS_EVENT_ZONE_UPDATED = (1 << 1), + DBUS_EVENT_ZONE_SUBMISSION = (1 << 2), + DBUS_EVENT_ZONE_INVALID = (1 << 3), +}; + +extern const knot_lookup_t acl_actions[]; + +extern const yp_item_t conf_schema[]; diff --git a/src/knot/conf/tools.c b/src/knot/conf/tools.c new file mode 100644 index 0000000..6823a05 --- /dev/null +++ b/src/knot/conf/tools.c @@ -0,0 +1,1069 @@ +/* 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 <glob.h> +#include <inttypes.h> +#include <libgen.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> +#ifdef ENABLE_XDP +#include <netinet/in.h> +#include <linux/ip.h> +#include <linux/ipv6.h> +#include <linux/udp.h> +#endif + +#include "libdnssec/key.h" +#include "knot/catalog/catalog_db.h" +#include "knot/conf/tools.h" +#include "knot/conf/conf.h" +#include "knot/conf/module.h" +#include "knot/conf/schema.h" +#include "knot/common/log.h" +#include "libknot/errcode.h" +#include "libknot/yparser/yptrafo.h" +#include "libknot/xdp.h" +#include "contrib/files.h" +#include "contrib/sockaddr.h" +#include "contrib/string.h" +#include "contrib/wire_ctx.h" + +#define MAX_INCLUDE_DEPTH 5 + +char check_str[1024]; + +int legacy_item( + knotd_conf_check_args_t *args) +{ + CONF_LOG(LOG_NOTICE, "line %zu, option '%s.%s' is obsolete and has no effect", + args->extra->line, args->item->parent->name + 1, + args->item->name + 1); + + return KNOT_EOK; +} + +static bool is_default_id( + const uint8_t *id, + size_t id_len) +{ + return id_len == CONF_DEFAULT_ID[0] && + memcmp(id, CONF_DEFAULT_ID + 1, id_len) == 0; +} + +int conf_exec_callbacks( + knotd_conf_check_args_t *args) +{ + if (args == NULL) { + return KNOT_EINVAL; + } + + for (size_t i = 0; i < YP_MAX_MISC_COUNT; i++) { + int (*fcn)(knotd_conf_check_args_t *) = args->item->misc[i]; + if (fcn == NULL) { + break; + } + + int ret = fcn(args); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +int mod_id_to_bin( + YP_TXT_BIN_PARAMS) +{ + YP_CHECK_PARAMS_BIN; + + // Check for "mod_name/mod_id" format. + const uint8_t *pos = (uint8_t *)strchr((char *)in->position, '/'); + if (pos == in->position) { + // Missing module name. + return KNOT_EINVAL; + } else if (pos >= stop - 1) { + // Missing module identifier after slash. + return KNOT_EINVAL; + } + + // Write mod_name in the yp_name_t format. + uint8_t name_len = (pos != NULL) ? (pos - in->position) : + wire_ctx_available(in); + wire_ctx_write_u8(out, name_len); + wire_ctx_write(out, in->position, name_len); + wire_ctx_skip(in, name_len); + + // Check for mod_id. + if (pos != NULL) { + // Skip the separator. + wire_ctx_skip(in, sizeof(uint8_t)); + + // Write mod_id as a zero terminated string. + int ret = yp_str_to_bin(in, out, stop); + if (ret != KNOT_EOK) { + return ret; + } + } + + YP_CHECK_RET; +} + +int mod_id_to_txt( + YP_BIN_TXT_PARAMS) +{ + YP_CHECK_PARAMS_TXT; + + // Write mod_name. + uint8_t name_len = wire_ctx_read_u8(in); + wire_ctx_write(out, in->position, name_len); + wire_ctx_skip(in, name_len); + + // Check for mod_id. + if (wire_ctx_available(in) > 0) { + // Write the separator. + wire_ctx_write_u8(out, '/'); + + // Write mod_id. + int ret = yp_str_to_txt(in, out); + if (ret != KNOT_EOK) { + return ret; + } + } + + YP_CHECK_RET; +} + +int rrtype_to_bin( + YP_TXT_BIN_PARAMS) +{ + YP_CHECK_PARAMS_BIN; + + uint16_t type; + int ret = knot_rrtype_from_string((char *)in->position, &type); + if (ret != 0) { + return KNOT_EINVAL; + } + wire_ctx_write_u64(out, type); + + YP_CHECK_RET; +} + +int rrtype_to_txt( + YP_BIN_TXT_PARAMS) +{ + YP_CHECK_PARAMS_TXT; + + uint16_t type = (uint16_t)wire_ctx_read_u64(in); + int ret = knot_rrtype_to_string(type, (char *)out->position, out->size); + if (ret < 0) { + return KNOT_EINVAL; + } + wire_ctx_skip(out, ret); + + YP_CHECK_RET; +} + +int rdname_to_bin( + YP_TXT_BIN_PARAMS) +{ + YP_CHECK_PARAMS_BIN; + + int ret = yp_dname_to_bin(in, out, stop); + if (ret == KNOT_EOK && in->wire[in->size - 1] != '.') { + // If non-FQDN, trim off the zero label. + wire_ctx_skip(out, -1); + } + + YP_CHECK_RET; +} + +int rdname_to_txt( + YP_BIN_TXT_PARAMS) +{ + YP_CHECK_PARAMS_TXT; + + // Temporarily normalize the input. + if (in->wire[in->size - 1] == '\0') { + return yp_dname_to_txt(in, out); + } + + knot_dname_storage_t full_name; + wire_ctx_t ctx = wire_ctx_init(full_name, sizeof(full_name)); + wire_ctx_write(&ctx, in->wire, in->size); + wire_ctx_write(&ctx, "\0", 1); + wire_ctx_set_offset(&ctx, 0); + + int ret = yp_dname_to_txt(&ctx, out); + if (ret != KNOT_EOK) { + return ret; + } + + // Trim off the trailing dot. + wire_ctx_skip(out, -1); + + YP_CHECK_RET; +} + +int check_ref( + knotd_conf_check_args_t *args) +{ + const yp_item_t *ref = args->item->var.r.ref; + const yp_item_t *ref2 = args->item->var.r.grp_ref; + + bool found1 = false, found2 = false; + + // Try to find the id in the first section. + found1 = conf_rawid_exists_txn(args->extra->conf, args->extra->txn, + ref->name, args->data, args->data_len); + if (ref2 != NULL) { + // Try to find the id in the second section if supported. + found2 = conf_rawid_exists_txn(args->extra->conf, args->extra->txn, + ref2->name, args->data, args->data_len); + } + + if (found1 == found2) { + if (found1) { + args->err_str = "ambiguous reference"; + return KNOT_ENOENT; + } else { + args->err_str = "invalid reference"; + return KNOT_ENOENT; + } + } + + return KNOT_EOK; +} + +int check_ref_dflt( + knotd_conf_check_args_t *args) +{ + if (check_ref(args) != KNOT_EOK && !is_default_id(args->data, args->data_len)) { + args->err_str = "invalid reference"; + return KNOT_ENOENT; + } + + return KNOT_EOK; +} + +int check_listen( + knotd_conf_check_args_t *args) +{ + bool no_port; + struct sockaddr_storage ss = yp_addr(args->data, &no_port); + if (!no_port && sockaddr_port(&ss) == 0) { + args->err_str = "invalid port"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +int check_xdp_listen( + knotd_conf_check_args_t *args) +{ +#ifndef ENABLE_XDP + args->err_str = "XDP is not available"; + return KNOT_ENOTSUP; +#else + bool no_port; + struct sockaddr_storage ss = yp_addr(args->data, &no_port); + conf_xdp_iface_t if_new; + int ret = conf_xdp_iface(&ss, &if_new); + if (ret != KNOT_EOK) { + args->err_str = "invalid XDP interface specification"; + return ret; + } else if (!no_port && if_new.port == 0) { + args->err_str = "invalid port"; + return KNOT_EINVAL; + } + + conf_val_t xdp = conf_get_txn(args->extra->conf, args->extra->txn, C_XDP, + C_LISTEN); + size_t count = conf_val_count(&xdp); + while (xdp.code == KNOT_EOK && count-- > 1) { + struct sockaddr_storage addr = conf_addr(&xdp, NULL); + conf_xdp_iface_t if_prev; + ret = conf_xdp_iface(&addr, &if_prev); + if (ret != KNOT_EOK) { + return ret; + } + if (strcmp(if_new.name, if_prev.name) == 0) { + args->err_str = "duplicate XDP interface specification"; + return KNOT_EINVAL; + } + conf_val_next(&xdp); + } + + return KNOT_EOK; +#endif +} + +static int dir_exists(const char *dir) +{ + struct stat st; + if (stat(dir, &st) != 0) { + return knot_map_errno(); + } else if (!S_ISDIR(st.st_mode)) { + return KNOT_ENOTDIR; + } else if (access(dir, W_OK) != 0) { + return knot_map_errno(); + } else { + return KNOT_EOK; + } +} + +static int dir_can_create(const char *dir) +{ + int ret = dir_exists(dir); + if (ret == KNOT_ENOENT) { + return KNOT_EOK; + } else { + return ret; + } +} + +static void check_db( + knotd_conf_check_args_t *args, + const yp_name_t *db_type, + int (*check_fun)(const char *), + const char *desc) +{ + if (db_type != NULL) { + conf_val_t val = conf_get_txn(args->extra->conf, args->extra->txn, + C_DB, db_type); + if (val.code != KNOT_EOK) { + // Don't check implicit database values. + return; + } + } + + char *db = conf_db_txn(args->extra->conf, args->extra->txn, db_type); + int ret = check_fun(db); + if (ret != KNOT_EOK) { + CONF_LOG(LOG_WARNING, "%s '%s' %s", desc, db, + (ret == KNOT_EACCES ? "not writable" : knot_strerror(ret))); + } + free(db); +} + +int check_database( + knotd_conf_check_args_t *args) +{ + check_db(args, NULL, dir_exists, "database storage"); + check_db(args, C_TIMER_DB, dir_can_create, "timer database"); + check_db(args, C_JOURNAL_DB, dir_can_create, "journal database"); + check_db(args, C_KASP_DB, dir_can_create, "KASP database"); + check_db(args, C_CATALOG_DB, dir_can_create, "catalog database"); + + return KNOT_EOK; +} + +int check_modref( + knotd_conf_check_args_t *args) +{ + const yp_name_t *mod_name = (const yp_name_t *)args->data; + const uint8_t *id = args->data + 1 + args->data[0]; + size_t id_len = args->data_len - 1 - args->data[0]; + + // Check if the module is ever available. + const module_t *mod = conf_mod_find(args->extra->conf, mod_name + 1, + mod_name[0], args->extra->check); + if (mod == NULL) { + args->err_str = "unknown module"; + return KNOT_EINVAL; + } + + // Check if the module requires some configuration. + if (id_len == 0) { + if (mod->api->flags & KNOTD_MOD_FLAG_OPT_CONF) { + return KNOT_EOK; + } else { + args->err_str = "missing module configuration"; + return KNOT_YP_ENOID; + } + } + + // Try to find a module with the id. + if (!conf_rawid_exists_txn(args->extra->conf, args->extra->txn, mod_name, + id, id_len)) { + args->err_str = "invalid module reference"; + return KNOT_ENOENT; + } + + return KNOT_EOK; +} + +int check_module_id( + knotd_conf_check_args_t *args) +{ + const size_t len = strlen(KNOTD_MOD_NAME_PREFIX); + + if (strncmp((const char *)args->id, KNOTD_MOD_NAME_PREFIX, len) != 0) { + args->err_str = "required 'mod-' prefix"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +int check_file( + knotd_conf_check_args_t *args) +{ + char *path = abs_path((const char *)args->data, CONFIG_DIR); + + struct stat st; + int ret = stat(path, &st); + free(path); + + if (ret != 0) { + args->err_str = "invalid file"; + return KNOT_EINVAL; + } else if(!S_ISREG(st.st_mode)) { + args->err_str = "not a file"; + return KNOT_EINVAL; + } else { + return KNOT_EOK; + } +} + +#define CHECK_LEGACY_NAME(section, old_item, new_item) { \ + conf_val_t val = conf_get_txn(args->extra->conf, args->extra->txn, \ + section, old_item); \ + if (val.code == KNOT_EOK) { \ + CONF_LOG(LOG_NOTICE, "option '%s.%s' has no effect, " \ + "use option '%s.%s' instead", \ + §ion[1], &old_item[1], \ + §ion[1], &new_item[1]); \ + } \ +} + +#define CHECK_LEGACY_NAME_ID(section, old_item, new_item) { \ + conf_val_t val = conf_rawid_get_txn(args->extra->conf, args->extra->txn, \ + section, old_item, args->id, args->id_len); \ + if (val.code == KNOT_EOK) { \ + CONF_LOG(LOG_NOTICE, "option '%s.%s' has no effect, " \ + "use option '%s.%s' instead", \ + §ion[1], &old_item[1], \ + §ion[1], &new_item[1]); \ + } \ +} + +static void check_mtu(knotd_conf_check_args_t *args, conf_val_t *xdp_listen) +{ +#ifdef ENABLE_XDP + conf_val_t val = conf_get_txn(args->extra->conf, args->extra->txn, + C_SRV, C_UDP_MAX_PAYLOAD_IPV4); + if (val.code != KNOT_EOK) { + val = conf_get_txn(args->extra->conf, args->extra->txn, + C_SRV, C_UDP_MAX_PAYLOAD); + } + int64_t ipv4_max = conf_int(&val) + sizeof(struct udphdr) + 4 + // Eth. CRC + sizeof(struct iphdr) + sizeof(struct ethhdr); + + val = conf_get_txn(args->extra->conf, args->extra->txn, + C_SRV, C_UDP_MAX_PAYLOAD_IPV6); + if (val.code != KNOT_EOK) { + val = conf_get_txn(args->extra->conf, args->extra->txn, + C_SRV, C_UDP_MAX_PAYLOAD); + } + int64_t ipv6_max = conf_int(&val) + sizeof(struct udphdr) + 4 + // Eth. CRC + sizeof(struct ipv6hdr) + sizeof(struct ethhdr); + + if (ipv6_max > KNOT_XDP_MAX_MTU || ipv4_max > KNOT_XDP_MAX_MTU) { + CONF_LOG(LOG_WARNING, "maximum UDP payload not compatible with XDP MTU (%u)", + KNOT_XDP_MAX_MTU); + } + + while (xdp_listen->code == KNOT_EOK) { + struct sockaddr_storage addr = conf_addr(xdp_listen, NULL); + conf_xdp_iface_t iface; + int ret = conf_xdp_iface(&addr, &iface); + if (ret != KNOT_EOK) { + CONF_LOG(LOG_WARNING, "failed to check XDP interface MTU"); + return; + } + int mtu = knot_eth_mtu(iface.name); + if (mtu < 0) { + CONF_LOG(LOG_WARNING, "failed to read MTU of interface %s", + iface.name); + continue; + } + mtu += sizeof(struct ethhdr) + 4; + if (ipv6_max > mtu || ipv4_max > mtu) { + CONF_LOG(LOG_WARNING, "maximum UDP payload not compatible " + "with MTU of interface %s", iface.name); + } + conf_val_next(xdp_listen); + } +#endif +} + +int check_server( + knotd_conf_check_args_t *args) +{ + conf_val_t key_file = conf_get_txn(args->extra->conf, args->extra->txn, + C_SRV, C_KEY_FILE); + conf_val_t crt_file = conf_get_txn(args->extra->conf, args->extra->txn, + C_SRV, C_CERT_FILE); + if (key_file.code != crt_file.code) { + args->err_str = "both server certificate and key must be set"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +int check_xdp( + knotd_conf_check_args_t *args) +{ + conf_val_t xdp_listen = conf_get_txn(args->extra->conf, args->extra->txn, + C_XDP, C_LISTEN); + conf_val_t srv_listen = conf_get_txn(args->extra->conf, args->extra->txn, + C_SRV, C_LISTEN); + conf_val_t udp = conf_get_txn(args->extra->conf, args->extra->txn, C_XDP, + C_UDP); + conf_val_t tcp = conf_get_txn(args->extra->conf, args->extra->txn, C_XDP, + C_TCP); + conf_val_t quic = conf_get_txn(args->extra->conf, args->extra->txn, C_XDP, + C_QUIC); + if (xdp_listen.code == KNOT_EOK) { + if (!conf_bool(&udp) && !conf_bool(&tcp) && !conf_bool(&quic)) { + args->err_str = "XDP processing requires UDP, TCP, or QUIC enabled"; + return KNOT_EINVAL; + } + + if (srv_listen.code != KNOT_EOK && tcp.code != KNOT_EOK) { + CONF_LOG(LOG_WARNING, "TCP processing not available"); + } + check_mtu(args, &xdp_listen); + } + + if (conf_bool(&quic)) { +#ifdef ENABLE_QUIC + conf_val_t port = conf_get_txn(args->extra->conf, args->extra->txn, C_XDP, + C_QUIC_PORT); + uint16_t quic_port = conf_int(&port); + + while (xdp_listen.code == KNOT_EOK) { + conf_xdp_iface_t iface; + struct sockaddr_storage udp_addr = conf_addr(&xdp_listen, NULL); + if (conf_xdp_iface(&udp_addr, &iface) == KNOT_EOK && iface.port == quic_port) { + args->err_str = "QUIC has to listen on different port than UDP"; + return KNOT_EINVAL; + } + conf_val_next(&xdp_listen); + } +#else + args->err_str = "QUIC processing not available"; + return KNOT_EINVAL; +#endif // ENABLE_QUIC + } + + return KNOT_EOK; +} + +int check_keystore( + knotd_conf_check_args_t *args) +{ + conf_val_t backend = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_KEYSTORE, + C_BACKEND, args->id, args->id_len); + conf_val_t config = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_KEYSTORE, + C_CONFIG, args->id, args->id_len); + conf_val_t key_label = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_KEYSTORE, + C_KEY_LABEL, args->id, args->id_len); + if (conf_opt(&backend) == KEYSTORE_BACKEND_PKCS11 && conf_str(&config) == NULL) { + args->err_str = "no PKCS #11 configuration defined"; + return KNOT_EINVAL; + } + if (conf_opt(&backend) != KEYSTORE_BACKEND_PKCS11 && conf_bool(&key_label)) { + args->err_str = "key labels not supported with the specified keystore"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +int check_policy( + knotd_conf_check_args_t *args) +{ + conf_val_t sts = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_SINGLE_TYPE_SIGNING, args->id, args->id_len); + conf_val_t alg = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_ALG, args->id, args->id_len); + conf_val_t ksk = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_KSK_SIZE, args->id, args->id_len); + conf_val_t zsk = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_ZSK_SIZE, args->id, args->id_len); + conf_val_t lifetime = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_RRSIG_LIFETIME, args->id, args->id_len); + conf_val_t refresh = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_RRSIG_REFRESH, args->id, args->id_len); + conf_val_t prerefresh = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_RRSIG_PREREFRESH, args->id, args->id_len); + conf_val_t prop_del = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_PROPAG_DELAY, args->id, args->id_len); + conf_val_t zsk_life = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_ZSK_LIFETIME, args->id, args->id_len); + conf_val_t ksk_life = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_KSK_LIFETIME, args->id, args->id_len); + conf_val_t dnskey_ttl = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_DNSKEY_TTL, args->id, args->id_len); + conf_val_t zone_max_ttl = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_ZONE_MAX_TTL, args->id, args->id_len); + conf_val_t nsec3 = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_NSEC3, args->id, args->id_len); + conf_val_t nsec3_iters = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_NSEC3_ITER, args->id, args->id_len); + + unsigned algorithm = conf_opt(&alg); + if (algorithm < DNSSEC_KEY_ALGORITHM_RSA_SHA256) { + CONF_LOG(LOG_NOTICE, "algorithm %u is deprecated and shouldn't be used for DNSSEC signing", + algorithm); + } + + int64_t ksk_size = conf_int(&ksk); + if (ksk_size != YP_NIL && !dnssec_algorithm_key_size_check(algorithm, ksk_size)) { + args->err_str = "KSK key size not compatible with the algorithm"; + return KNOT_EINVAL; + } + + int64_t zsk_size = conf_int(&zsk); + if (zsk_size != YP_NIL && !dnssec_algorithm_key_size_check(algorithm, zsk_size)) { + args->err_str = "ZSK key size not compatible with the algorithm"; + return KNOT_EINVAL; + } + + int64_t lifetime_val = conf_int(&lifetime); + int64_t refresh_val = conf_int(&refresh); + int64_t preref_val = conf_int(&prerefresh); + if (lifetime_val <= refresh_val + preref_val) { + args->err_str = "RRSIG refresh + pre-refresh has to be lower than RRSIG lifetime"; + return KNOT_EINVAL; + } + + bool sts_val = conf_bool(&sts); + int64_t prop_del_val = conf_int(&prop_del); + int64_t zsk_life_val = conf_int(&zsk_life); + int64_t ksk_life_val = conf_int(&ksk_life); + int64_t dnskey_ttl_val = conf_int(&dnskey_ttl); + if (dnskey_ttl_val == YP_NIL) { + dnskey_ttl_val = 0; + } + int64_t zone_max_ttl_val = conf_int(&zone_max_ttl); + if (zone_max_ttl_val == YP_NIL) { + zone_max_ttl_val = dnskey_ttl_val; // Better than 0. + } + + if (sts_val) { + if (ksk_life_val != 0 && ksk_life_val < 2 * prop_del_val + dnskey_ttl_val + zone_max_ttl_val) { + args->err_str = "CSK lifetime too low according to propagation delay, DNSKEY TTL, " + "and maximum zone TTL"; + return KNOT_EINVAL; + } + } else { + if (ksk_life_val != 0 && ksk_life_val < 2 * prop_del_val + 2 * dnskey_ttl_val) { + args->err_str = "KSK lifetime too low according to propagation delay and DNSKEY TTL"; + return KNOT_EINVAL; + } + if (zsk_life_val != 0 && zsk_life_val < 2 * prop_del_val + dnskey_ttl_val + zone_max_ttl_val) { + args->err_str = "ZSK lifetime too low according to propagation delay, DNSKEY TTL, " + "and maximum zone TTL"; + return KNOT_EINVAL; + } + } + + conf_val_t cds_cdnskey = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_CDS_CDNSKEY, args->id, args->id_len); + conf_val_t ds_push = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_DS_PUSH, args->id, args->id_len); + + if (conf_val_count(&ds_push) > 0 && conf_opt(&cds_cdnskey) == CDS_CDNSKEY_NONE) { + args->err_str = "DS push requires enabled CDS/CDNSKEY publication"; + return KNOT_EINVAL; + } + +#ifndef HAVE_GNUTLS_REPRODUCIBLE + conf_val_t repro_sign = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_REPRO_SIGNING, args->id, args->id_len); + if (conf_bool(&repro_sign)) { + CONF_LOG(LOG_WARNING, "reproducible signing not available, signing normally"); + } +#endif + + if (conf_bool(&nsec3)) { + uint16_t iters = conf_int(&nsec3_iters); + if (nsec3_iters.code != KNOT_EOK && iters != 0) { + CONF_LOG(LOG_WARNING, "policy[%s].nsec3-iterations defaults to %u, " + "since version 3.2 the default becomes 0", args->id, iters); + } + if (iters > 20) { + CONF_LOG(LOG_NOTICE, "policy[%s].nsec3-iterations=%u is too high, " + "the recommended value is 0", args->id, iters); + } + } + + conf_val_t dnskey_mgmt = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_DNSKEY_MGMT, args->id, args->id_len); + conf_val_t offline_ksk = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_OFFLINE_KSK, args->id, args->id_len); + conf_val_t delete_dely = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_POLICY, + C_DELETE_DELAY, args->id, args->id_len); + if (conf_opt(&dnskey_mgmt) != DNSKEY_MGMT_FULL) { + if (conf_bool(&offline_ksk)) { + args->err_str = "incremental DNSKEY management can't be used with offline-ksk"; + return KNOT_EINVAL; + } + if (conf_int(&delete_dely) <= 0) { + args->err_str = "incremental DNSKEY management requires configured delete-delay"; + return KNOT_EINVAL; + } + } + + return KNOT_EOK; +} + +int check_key( + knotd_conf_check_args_t *args) +{ + conf_val_t alg = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_KEY, + C_ALG, args->id, args->id_len); + if (conf_val_count(&alg) == 0) { + args->err_str = "no key algorithm defined"; + return KNOT_EINVAL; + } + + conf_val_t secret = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_KEY, + C_SECRET, args->id, args->id_len); + if (conf_val_count(&secret) == 0) { + args->err_str = "no key secret defined"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +int check_acl( + knotd_conf_check_args_t *args) +{ + conf_val_t addr = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_ACL, + C_ADDR, args->id, args->id_len); + conf_val_t key = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_ACL, + C_KEY, args->id, args->id_len); + conf_val_t remote = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_ACL, + C_RMT, args->id, args->id_len); + if (remote.code != KNOT_ENOENT && + (addr.code != KNOT_ENOENT || key.code != KNOT_ENOENT)) { + args->err_str = "specified ACL/remote together with address or key"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +int check_remote( + knotd_conf_check_args_t *args) +{ + conf_val_t addr = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_RMT, + C_ADDR, args->id, args->id_len); + if (conf_val_count(&addr) == 0) { + args->err_str = "no remote address defined"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +int check_remotes( + knotd_conf_check_args_t *args) +{ + conf_val_t remote = conf_rawid_get_txn(args->extra->conf, args->extra->txn, C_RMTS, + C_RMT, args->id, args->id_len); + if (remote.code != KNOT_EOK) { + args->err_str = "no remote defined"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +#define CHECK_DFLT(item, name) { \ + conf_val_t val = conf_rawid_get_txn(args->extra->conf, args->extra->txn, \ + C_TPL, item, args->id, args->id_len); \ + if (val.code == KNOT_EOK) { \ + args->err_str = name " in non-default template"; \ + return KNOT_EINVAL; \ + } \ +} + +int check_catalog_group( + knotd_conf_check_args_t *args) +{ + assert(args->data_len > 0); + if (args->data_len - 1 > CATALOG_GROUP_MAXLEN) { + args->err_str = "group name longer than 255 characters"; + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +int check_template( + knotd_conf_check_args_t *args) +{ + if (!is_default_id(args->id, args->id_len)) { + CHECK_DFLT(C_GLOBAL_MODULE, "global module"); + } + + return KNOT_EOK; +} + +#define CHECK_ZONE_INTERVALS(low_item, high_item) { \ + conf_val_t high = conf_zone_get_txn(args->extra->conf, args->extra->txn, \ + high_item, yp_dname(args->id)); \ + if (high.code == KNOT_EOK) { \ + conf_val_t low = conf_zone_get_txn(args->extra->conf, args->extra->txn, \ + low_item, yp_dname(args->id)); \ + if (low.code == KNOT_EOK && conf_int(&low) > conf_int(&high)) { \ + if (snprintf(check_str, sizeof(check_str), "'%s' is higher than '%s'", \ + &low_item[1], &high_item[1]) < 0) { \ + check_str[0] = '\0'; \ + } \ + args->err_str = check_str; \ + return KNOT_EINVAL; \ + } \ + } \ +} + +#define CHECK_CATZ_TPL(option, option_string) \ +{ \ + conf_val_t val = conf_rawid_get_txn(args->extra->conf, args->extra->txn, \ + C_TPL, option, catalog_tpl.data, \ + catalog_tpl.len); \ + if (val.code == KNOT_EOK) { \ + args->err_str = "'" option_string "' in a catalog template"; \ + return KNOT_EINVAL; \ + } \ +} + +int check_zone( + knotd_conf_check_args_t *args) +{ + CHECK_ZONE_INTERVALS(C_REFRESH_MIN_INTERVAL, C_REFRESH_MAX_INTERVAL); + CHECK_ZONE_INTERVALS(C_RETRY_MIN_INTERVAL, C_RETRY_MAX_INTERVAL); + CHECK_ZONE_INTERVALS(C_EXPIRE_MIN_INTERVAL, C_EXPIRE_MAX_INTERVAL); + + conf_val_t zf_load = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_ZONEFILE_LOAD, yp_dname(args->id)); + if (conf_opt(&zf_load) == ZONEFILE_LOAD_DIFSE) { + conf_val_t journal = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_JOURNAL_CONTENT, yp_dname(args->id)); + if (conf_opt(&journal) != JOURNAL_CONTENT_ALL) { + args->err_str = "'zonefile-load: difference-no-serial' requires 'journal-content: all'"; + return KNOT_EINVAL; + } + } + + conf_val_t validation = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_DNSSEC_VALIDATION, yp_dname(args->id)); + if (conf_bool(&validation)) { + conf_val_t signing = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_DNSSEC_SIGNING, yp_dname(args->id)); + if (conf_bool(&signing)) { + args->err_str = "'dnssec-validation' is not compatible with 'dnssec-signing'"; + return KNOT_EINVAL; + } + } + + conf_val_t catalog_role = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_CATALOG_ROLE, yp_dname(args->id)); + conf_val_t catalog_tpl = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_CATALOG_TPL, yp_dname(args->id)); + conf_val_t catalog_zone = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_CATALOG_ZONE, yp_dname(args->id)); + conf_val_t catalog_serial = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_SERIAL_POLICY, yp_dname(args->id)); + + unsigned role = conf_opt(&catalog_role); + if ((bool)(role == CATALOG_ROLE_INTERPRET) != (bool)(catalog_tpl.code == KNOT_EOK)) { + args->err_str = "'catalog-role' must correspond to configured 'catalog-template'"; + return KNOT_EINVAL; + } + if ((bool)(role == CATALOG_ROLE_MEMBER) != (bool)(catalog_zone.code == KNOT_EOK)) { + args->err_str = "'catalog-role' must correspond to configured 'catalog-zone'"; + return KNOT_EINVAL; + } + if (role == CATALOG_ROLE_GENERATE && + conf_opt(&catalog_serial) != SERIAL_POLICY_UNIXTIME && // Default doesn't harm. + catalog_serial.code == KNOT_EOK) { + args->err_str = "'serial-policy' must be 'unixtime' for generated catalog zones"; + return KNOT_EINVAL; + } + if (role == CATALOG_ROLE_INTERPRET) { + conf_val(&catalog_tpl); + while (catalog_tpl.code == KNOT_EOK) { + CHECK_CATZ_TPL(C_CATALOG_TPL, "catalog-template"); + CHECK_CATZ_TPL(C_CATALOG_ROLE, "catalog-role"); + CHECK_CATZ_TPL(C_CATALOG_ZONE, "catalog-zone"); + CHECK_CATZ_TPL(C_CATALOG_GROUP, "catalog-group"); + conf_val_next(&catalog_tpl); + } + } + + conf_val_t ds_push = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_DS_PUSH, yp_dname(args->id)); + if (ds_push.code == KNOT_EOK) { + conf_val_t policy_id = conf_zone_get_txn(args->extra->conf, args->extra->txn, + C_DNSSEC_POLICY, yp_dname(args->id)); + if (policy_id.code == KNOT_EOK) { + conf_val_t cds_cdnskey = conf_id_get_txn(args->extra->conf, args->extra->txn, + C_POLICY, C_CDS_CDNSKEY, + &policy_id); + if (conf_val_count(&ds_push) > 0 && conf_opt(&cds_cdnskey) == CDS_CDNSKEY_NONE) { + args->err_str = "DS push requires enabled CDS/CDNSKEY publication"; + return KNOT_EINVAL; + } + } + } + + return KNOT_EOK; +} + +static int glob_error( + const char *epath, + int eerrno) +{ + CONF_LOG(LOG_WARNING, "failed to access '%s' (%s)", epath, + knot_strerror(knot_map_errno_code(eerrno))); + + return 0; +} + +int include_file( + knotd_conf_check_args_t *args) +{ + if (args->data_len == 0) { + return KNOT_YP_ENODATA; + } + + // This function should not be called in more threads. + static int depth = 0; + glob_t glob_buf = { 0 }; + char *path = NULL; + int ret; + + // Check for include loop. + if (depth++ > MAX_INCLUDE_DEPTH) { + CONF_LOG(LOG_ERR, "include loop detected"); + ret = KNOT_EPARSEFAIL; + goto include_error; + } + + // Prepare absolute include path. + if (args->data[0] == '/') { + path = sprintf_alloc("%.*s", (int)args->data_len, args->data); + } else { + const char *file_name = args->extra->file_name != NULL ? + args->extra->file_name : "./"; + char *full_current_name = realpath(file_name, NULL); + if (full_current_name == NULL) { + ret = KNOT_ENOMEM; + goto include_error; + } + + path = sprintf_alloc("%s/%.*s", dirname(full_current_name), + (int)args->data_len, args->data); + free(full_current_name); + } + if (path == NULL) { + ret = KNOT_ESPACE; + goto include_error; + } + + // Evaluate include pattern (empty wildcard match is also valid). + ret = glob(path, 0, glob_error, &glob_buf); + if (ret != 0 && (ret != GLOB_NOMATCH || strchr(path, '*') == NULL)) { + ret = KNOT_EFILE; + goto include_error; + } + + // Process glob result. + for (size_t i = 0; i < glob_buf.gl_pathc; i++) { + // Get file status. + struct stat file_stat; + if (stat(glob_buf.gl_pathv[i], &file_stat) != 0) { + CONF_LOG(LOG_WARNING, "failed to get file status for '%s'", + glob_buf.gl_pathv[i]); + continue; + } + + // Ignore directory or non-regular file. + if (S_ISDIR(file_stat.st_mode)) { + continue; + } else if (!S_ISREG(file_stat.st_mode)) { + CONF_LOG(LOG_WARNING, "invalid include file '%s'", + glob_buf.gl_pathv[i]); + continue; + } + + // Include regular file. + ret = conf_parse(args->extra->conf, args->extra->txn, + glob_buf.gl_pathv[i], true); + if (ret != KNOT_EOK) { + goto include_error; + } + } + + ret = KNOT_EOK; +include_error: + globfree(&glob_buf); + free(path); + depth--; + + return ret; +} + +int load_module( + knotd_conf_check_args_t *args) +{ + conf_val_t val = conf_rawid_get_txn(args->extra->conf, args->extra->txn, + C_MODULE, C_FILE, args->id, args->id_len); + const char *file_name = conf_str(&val); + + char *mod_name = strndup((const char *)args->id, args->id_len); + if (mod_name == NULL) { + return KNOT_ENOMEM; + } + + int ret = conf_mod_load_extra(args->extra->conf, mod_name, file_name, + args->extra->check ? MOD_TEMPORARY : MOD_EXPLICIT); + free(mod_name); + if (ret != KNOT_EOK) { + return ret; + } + + // Update currently iterating item. + const yp_item_t *section = yp_schema_find(C_MODULE, NULL, args->extra->conf->schema); + assert(section); + args->item = section->var.g.id; + + return ret; +} diff --git a/src/knot/conf/tools.h b/src/knot/conf/tools.h new file mode 100644 index 0000000..a8875bd --- /dev/null +++ b/src/knot/conf/tools.h @@ -0,0 +1,147 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> +#include <stdint.h> + +#include "knot/conf/conf.h" +#include "libknot/yparser/ypschema.h" + +typedef struct knotd_conf_check_extra { + conf_t *conf; + knot_db_txn_t *txn; + const char *file_name; + size_t line; + bool check; /*!< Indication of the confio check mode. */ +} knotd_conf_check_extra_t; + +int legacy_item( + knotd_conf_check_args_t *args +); + +int conf_exec_callbacks( + knotd_conf_check_args_t *args +); + +int mod_id_to_bin( + YP_TXT_BIN_PARAMS +); + +int mod_id_to_txt( + YP_BIN_TXT_PARAMS +); + +int rrtype_to_bin( + YP_TXT_BIN_PARAMS +); + +int rrtype_to_txt( + YP_BIN_TXT_PARAMS +); + +int rdname_to_bin( + YP_TXT_BIN_PARAMS +); + +int rdname_to_txt( + YP_BIN_TXT_PARAMS +); + +int check_ref( + knotd_conf_check_args_t *args +); + +int check_ref_dflt( + knotd_conf_check_args_t *args +); + +int check_listen( + knotd_conf_check_args_t *args +); + +int check_xdp_listen( + knotd_conf_check_args_t *args +); + +int check_database( + knotd_conf_check_args_t *args +); + +int check_modref( + knotd_conf_check_args_t *args +); + +int check_module_id( + knotd_conf_check_args_t *args +); + +int check_file( + knotd_conf_check_args_t *args +); + +int check_server( + knotd_conf_check_args_t *args +); + +int check_xdp( + knotd_conf_check_args_t *args +); + +int check_keystore( + knotd_conf_check_args_t *args +); + +int check_policy( + knotd_conf_check_args_t *args +); + +int check_key( + knotd_conf_check_args_t *args +); + +int check_acl( + knotd_conf_check_args_t *args +); + +int check_remote( + knotd_conf_check_args_t *args +); + +int check_remotes( + knotd_conf_check_args_t *args +); + +int check_catalog_group( + knotd_conf_check_args_t *args +); + +int check_template( + knotd_conf_check_args_t *args +); + +int check_zone( + knotd_conf_check_args_t *args +); + +int include_file( + knotd_conf_check_args_t *args +); + +int load_module( + knotd_conf_check_args_t *args +); diff --git a/src/knot/ctl/commands.c b/src/knot/ctl/commands.c new file mode 100644 index 0000000..7d4c592 --- /dev/null +++ b/src/knot/ctl/commands.c @@ -0,0 +1,2331 @@ +/* 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 <string.h> +#include <unistd.h> +#include <sys/time.h> +#include <urcu.h> + +#include "knot/common/log.h" +#include "knot/common/stats.h" +#include "knot/conf/confio.h" +#include "knot/ctl/commands.h" +#include "knot/dnssec/key-events.h" +#include "knot/events/events.h" +#include "knot/events/handlers.h" +#include "knot/journal/journal_metadata.h" +#include "knot/nameserver/query_module.h" +#include "knot/updates/zone-update.h" +#include "knot/zone/backup.h" +#include "knot/zone/digest.h" +#include "knot/zone/timers.h" +#include "knot/zone/zonedb-load.h" +#include "knot/zone/zonefile.h" +#include "libknot/libknot.h" +#include "libknot/yparser/yptrafo.h" +#include "contrib/files.h" +#include "contrib/string.h" +#include "contrib/strtonum.h" +#include "contrib/openbsd/strlcat.h" +#include "contrib/ucw/lists.h" +#include "libzscanner/scanner.h" + +#define MATCH_OR_FILTER(args, code) ((args)->data[KNOT_CTL_IDX_FILTER] == NULL || \ + strchr((args)->data[KNOT_CTL_IDX_FILTER], (code)) != NULL) + +#define MATCH_AND_FILTER(args, code) ((args)->data[KNOT_CTL_IDX_FILTER] != NULL && \ + strchr((args)->data[KNOT_CTL_IDX_FILTER], (code)) != NULL) + +typedef struct { + ctl_args_t *args; + int type_filter; // -1: no specific type, [0, 2^16]: specific type. + knot_dump_style_t style; + knot_ctl_data_t data; + knot_dname_txt_storage_t zone; + knot_dname_txt_storage_t owner; + char ttl[16]; + char type[32]; + char rdata[2 * 65536]; +} send_ctx_t; + +static struct { + send_ctx_t send_ctx; + zs_scanner_t scanner; + char txt_rr[sizeof(((send_ctx_t *)0)->owner) + + sizeof(((send_ctx_t *)0)->ttl) + + sizeof(((send_ctx_t *)0)->type) + + sizeof(((send_ctx_t *)0)->rdata)]; +} ctl_globals; + +/*! + * Evaluates a filter pair and checks for conflicting filters. + * + * \param[in] args Command arguments. + * \param[out] param The filter to be set. + * \param[in] dflt Default filter value. + * \param[in] filter Name of the filter. + * \param[in] neg_filter Name of the negative filter. + * + * \return false if there is a filter conflict, true otherwise. + */ + +static bool eval_opposite_filters(ctl_args_t *args, bool *param, bool dflt, + int filter, int neg_filter) +{ + bool set = MATCH_AND_FILTER(args, filter); + bool unset = MATCH_AND_FILTER(args, neg_filter); + + *param = dflt ? (set || !unset) : (set && !unset); + return !(set && unset); +} + +static int schedule_trigger(zone_t *zone, ctl_args_t *args, zone_event_type_t event, + bool user) +{ + int ret = KNOT_EOK; + + if (ctl_has_flag(args->data[KNOT_CTL_IDX_FLAGS], CTL_FLAG_BLOCKING)) { + ret = zone_events_schedule_blocking(zone, event, user); + } else if (user) { + zone_events_schedule_user(zone, event); + } else { + zone_events_schedule_now(zone, event); + } + + return ret; +} + +static void ctl_log_conf_data(knot_ctl_data_t *data) +{ + if (data == NULL) { + return; + } + + const char *section = (*data)[KNOT_CTL_IDX_SECTION]; + const char *item = (*data)[KNOT_CTL_IDX_ITEM]; + const char *id = (*data)[KNOT_CTL_IDX_ID]; + + if (section != NULL) { + log_ctl_debug("control, config item '%s%s%s%s%s%s'", section, + (id != NULL ? "[" : ""), + (id != NULL ? id : ""), + (id != NULL ? "]" : ""), + (item != NULL ? "." : ""), + (item != NULL ? item : "")); + } +} + +static void send_error(ctl_args_t *args, const char *msg) +{ + knot_ctl_data_t data; + memcpy(&data, args->data, sizeof(data)); + + data[KNOT_CTL_IDX_ERROR] = msg; + + int ret = knot_ctl_send(args->ctl, KNOT_CTL_TYPE_DATA, &data); + if (ret != KNOT_EOK) { + log_ctl_debug("control, failed to send error (%s)", knot_strerror(ret)); + } +} + +static int get_zone(ctl_args_t *args, zone_t **zone) +{ + const char *name = args->data[KNOT_CTL_IDX_ZONE]; + assert(name != NULL); + + knot_dname_storage_t buff; + knot_dname_t *dname = knot_dname_from_str(buff, name, sizeof(buff)); + if (dname == NULL) { + return KNOT_EINVAL; + } + knot_dname_to_lower(dname); + + *zone = knot_zonedb_find(args->server->zone_db, dname); + if (*zone == NULL) { + return KNOT_ENOZONE; + } + + return KNOT_EOK; +} + +static int zones_apply(ctl_args_t *args, int (*fcn)(zone_t *, ctl_args_t *)) +{ + int ret; + + // Process all configured zones if none is specified. + if (args->data[KNOT_CTL_IDX_ZONE] == NULL) { + bool failed = false; + knot_zonedb_iter_t *it = knot_zonedb_iter_begin(args->server->zone_db); + while (!knot_zonedb_iter_finished(it)) { + args->suppress = false; + ret = fcn((zone_t *)knot_zonedb_iter_val(it), args); + if (ret != KNOT_EOK && !args->suppress) { + failed = true; + } + knot_zonedb_iter_next(it); + } + knot_zonedb_iter_free(it); + + if (failed) { + ret = KNOT_CTL_EZONE; + log_ctl_error("control, error (%s)", knot_strerror(ret)); + send_error(args, knot_strerror(ret)); + } + + return KNOT_EOK; + } + + while (true) { + zone_t *zone; + ret = get_zone(args, &zone); + if (ret == KNOT_EOK) { + ret = fcn(zone, args); + } + if (ret != KNOT_EOK) { + log_ctl_zone_str_error(args->data[KNOT_CTL_IDX_ZONE], + "control, error (%s)", knot_strerror(ret)); + send_error(args, knot_strerror(ret)); + } + + // Get next zone name. + ret = knot_ctl_receive(args->ctl, &args->type, &args->data); + if (ret != KNOT_EOK || args->type != KNOT_CTL_TYPE_DATA) { + break; + } + strtolower((char *)args->data[KNOT_CTL_IDX_ZONE]); + + // Log the other zones the same way as the first one from process.c. + log_ctl_zone_str_info(args->data[KNOT_CTL_IDX_ZONE], + "control, received command '%s'", + args->data[KNOT_CTL_IDX_CMD]); + } + + return ret; +} + +static int zone_status(zone_t *zone, ctl_args_t *args) +{ + knot_dname_txt_storage_t name; + if (knot_dname_to_str(name, zone->name, sizeof(name)) == NULL) { + return KNOT_EINVAL; + } + + char flags[16] = ""; + knot_ctl_data_t data = { + [KNOT_CTL_IDX_ZONE] = name, + [KNOT_CTL_IDX_FLAGS] = flags + }; + + const bool slave = zone_is_slave(conf(), zone); + if (slave) { + strlcat(flags, CTL_FLAG_STATUS_SLAVE, sizeof(flags)); + } + const bool empty = (zone->contents == NULL); + if (empty) { + strlcat(flags, CTL_FLAG_STATUS_EMPTY, sizeof(flags)); + } + const bool member = (zone->flags & ZONE_IS_CAT_MEMBER); + if (member) { + strlcat(flags, CTL_FLAG_STATUS_MEMBER, sizeof(flags)); + } + + int ret; + char buff[128]; + knot_ctl_type_t type = KNOT_CTL_TYPE_DATA; + + if (MATCH_OR_FILTER(args, CTL_FILTER_STATUS_ROLE)) { + data[KNOT_CTL_IDX_TYPE] = "role"; + + if (slave) { + data[KNOT_CTL_IDX_DATA] = "slave"; + } else { + data[KNOT_CTL_IDX_DATA] = "master"; + } + + ret = knot_ctl_send(args->ctl, type, &data); + if (ret != KNOT_EOK) { + return ret; + } else { + type = KNOT_CTL_TYPE_EXTRA; + } + } + + if (MATCH_OR_FILTER(args, CTL_FILTER_STATUS_SERIAL)) { + data[KNOT_CTL_IDX_TYPE] = "serial"; + + if (empty) { + ret = snprintf(buff, sizeof(buff), STATUS_EMPTY); + } else { + knot_rdataset_t *soa = node_rdataset(zone->contents->apex, + KNOT_RRTYPE_SOA); + ret = snprintf(buff, sizeof(buff), "%u", knot_soa_serial(soa->rdata)); + } + if (ret < 0 || ret >= sizeof(buff)) { + return KNOT_ESPACE; + } + + data[KNOT_CTL_IDX_DATA] = buff; + + ret = knot_ctl_send(args->ctl, type, &data); + if (ret != KNOT_EOK) { + return ret; + } else { + type = KNOT_CTL_TYPE_EXTRA; + } + } + + if (MATCH_OR_FILTER(args, CTL_FILTER_STATUS_TRANSACTION)) { + data[KNOT_CTL_IDX_TYPE] = "transaction"; + data[KNOT_CTL_IDX_DATA] = (zone->control_update != NULL) ? "open" : STATUS_EMPTY; + ret = knot_ctl_send(args->ctl, type, &data); + if (ret != KNOT_EOK) { + return ret; + } else { + type = KNOT_CTL_TYPE_EXTRA; + } + } + + const bool ufrozen = zone->events.ufrozen; + if (MATCH_OR_FILTER(args, CTL_FILTER_STATUS_FREEZE)) { + data[KNOT_CTL_IDX_TYPE] = "freeze"; + if (ufrozen) { + if (zone_events_get_time(zone, ZONE_EVENT_UTHAW) < time(NULL)) { + data[KNOT_CTL_IDX_DATA] = "yes"; + } else { + data[KNOT_CTL_IDX_DATA] = "thawing"; + } + } else { + if (zone_events_get_time(zone, ZONE_EVENT_UFREEZE) < time(NULL)) { + data[KNOT_CTL_IDX_DATA] = STATUS_EMPTY; + } else { + data[KNOT_CTL_IDX_DATA] = "freezing"; + } + } + ret = knot_ctl_send(args->ctl, type, &data); + if (ret != KNOT_EOK) { + return ret; + } else { + type = KNOT_CTL_TYPE_EXTRA; + } + + data[KNOT_CTL_IDX_TYPE] = "XFR-freeze"; + if (zone_get_flag(zone, ZONE_XFR_FROZEN, false)) { + data[KNOT_CTL_IDX_DATA] = "yes"; + } else { + data[KNOT_CTL_IDX_DATA] = STATUS_EMPTY; + } + ret = knot_ctl_send(args->ctl, type, &data); + if (ret != KNOT_EOK) { + return ret; + } + } + + if (MATCH_OR_FILTER(args, CTL_FILTER_STATUS_CATALOG)) { + char buf[1 + KNOT_DNAME_TXT_MAXLEN + 1 + CATALOG_GROUP_MAXLEN + 1] = ""; + data[KNOT_CTL_IDX_TYPE] = "catalog"; + data[KNOT_CTL_IDX_DATA] = buf; + + if (member) { + const knot_dname_t *catz; + const char *group; + void *to_free; + ret = catalog_get_catz(zone_catalog(zone), zone->name, + &catz, &group, &to_free); + if (ret == KNOT_EOK) { + if (knot_dname_to_str(buf, catz, sizeof(buf)) == NULL) { + buf[0] = '\0'; + } + if (group[0] != '\0') { + size_t idx = strlcat(buf, "#", sizeof(buf)); + (void)strlcat(buf + idx, group, sizeof(buf) - idx); + } + free(to_free); + } + } else { + conf_val_t val = conf_zone_get(conf(), C_CATALOG_ROLE, zone->name); + switch (conf_opt(&val)) { + case CATALOG_ROLE_INTERPRET: + data[KNOT_CTL_IDX_DATA] = "interpret"; + break; + case CATALOG_ROLE_GENERATE: + data[KNOT_CTL_IDX_DATA] = "generate"; + break; + case CATALOG_ROLE_MEMBER: + buf[0] = '@'; + val = conf_zone_get(conf(), C_CATALOG_ZONE, zone->name); + if (knot_dname_to_str(buf + 1, conf_dname(&val), sizeof(buf) - 1) == NULL) { + buf[1] = '\0'; + } + val = conf_zone_get(conf(), C_CATALOG_GROUP, zone->name); + if (val.code == KNOT_EOK) { + size_t idx = strlcat(buf, "#", sizeof(buf)); + (void)strlcat(buf + idx, conf_str(&val), sizeof(buf) - idx); + } + break; + default: + data[KNOT_CTL_IDX_DATA] = STATUS_EMPTY; + } + } + + ret = knot_ctl_send(args->ctl, type, &data); + if (ret != KNOT_EOK) { + return ret; + } else { + type = KNOT_CTL_TYPE_EXTRA; + } + } + + if (MATCH_OR_FILTER(args, CTL_FILTER_STATUS_EVENTS)) { + for (zone_event_type_t i = 0; i < ZONE_EVENT_COUNT; i++) { + // Events not worth showing or used elsewhere. + if (i == ZONE_EVENT_UFREEZE || i == ZONE_EVENT_UTHAW) { + continue; + } + + data[KNOT_CTL_IDX_TYPE] = zone_events_get_name(i); + time_t ev_time = zone_events_get_time(zone, i); + if (zone->events.running && zone->events.type == i) { + ret = snprintf(buff, sizeof(buff), "running"); + } else if (ev_time <= 0) { + ret = snprintf(buff, sizeof(buff), STATUS_EMPTY); + } else if (ev_time <= time(NULL)) { + bool frozen = ufrozen && ufreeze_applies(i); + ret = snprintf(buff, sizeof(buff), frozen ? "frozen" : "pending"); + } else { + ret = knot_time_print(TIME_PRINT_HUMAN_MIXED, + ev_time, buff, sizeof(buff)); + } + if (ret < 0 || ret >= sizeof(buff)) { + return KNOT_ESPACE; + } + data[KNOT_CTL_IDX_DATA] = buff; + + ret = knot_ctl_send(args->ctl, type, &data); + if (ret != KNOT_EOK) { + return ret; + } else { + type = KNOT_CTL_TYPE_EXTRA; + } + } + } + + return KNOT_EOK; +} + +static int zone_reload(zone_t *zone, _unused_ ctl_args_t *args) +{ + if (zone_expired(zone)) { + args->suppress = true; + return KNOT_ENOTSUP; + } + + if (ctl_has_flag(args->data[KNOT_CTL_IDX_FLAGS], CTL_FLAG_FORCE)) { + return zone_reload_modules(conf(), args->server, zone->name); + } + + return schedule_trigger(zone, args, ZONE_EVENT_LOAD, true); +} + +static int zone_refresh(zone_t *zone, _unused_ ctl_args_t *args) +{ + if (!zone_is_slave(conf(), zone)) { + args->suppress = true; + return KNOT_ENOTSUP; + } + + zone->zonefile.bootstrap_cnt = 0; // restart delays + return schedule_trigger(zone, args, ZONE_EVENT_REFRESH, true); +} + +static int zone_retransfer(zone_t *zone, _unused_ ctl_args_t *args) +{ + if (!zone_is_slave(conf(), zone)) { + args->suppress = true; + return KNOT_ENOTSUP; + } + + zone_set_flag(zone, ZONE_FORCE_AXFR); + zone->zonefile.bootstrap_cnt = 0; // restart delays + return schedule_trigger(zone, args, ZONE_EVENT_REFRESH, true); +} + +static int zone_notify(zone_t *zone, _unused_ ctl_args_t *args) +{ + zone_notifailed_clear(zone); + return schedule_trigger(zone, args, ZONE_EVENT_NOTIFY, true); +} + +static int zone_flush(zone_t *zone, ctl_args_t *args) +{ + if (MATCH_AND_FILTER(args, CTL_FILTER_FLUSH_OUTDIR)) { + rcu_read_lock(); + int ret = zone_dump_to_dir(conf(), zone, args->data[KNOT_CTL_IDX_DATA]); + rcu_read_unlock(); + if (ret != KNOT_EOK) { + log_zone_warning(zone->name, "failed to update zone file (%s)", + knot_strerror(ret)); + } + return ret; + } + + zone_set_flag(zone, ZONE_USER_FLUSH); + if (ctl_has_flag(args->data[KNOT_CTL_IDX_FLAGS], CTL_FLAG_FORCE)) { + zone_set_flag(zone, ZONE_FORCE_FLUSH); + } + + return schedule_trigger(zone, args, ZONE_EVENT_FLUSH, true); +} + +static int init_backup(ctl_args_t *args, bool restore_mode) +{ + if (!MATCH_AND_FILTER(args, CTL_FILTER_BACKUP_OUTDIR)) { + return KNOT_ENOPARAM; + } + + // Make sure that the backup outdir is not the same as the server DB storage. + conf_val_t db_storage_val = conf_db_param(conf(), C_STORAGE); + const char *db_storage = conf_str(&db_storage_val); + + const char *backup_dir = args->data[KNOT_CTL_IDX_DATA]; + + if (same_path(backup_dir, db_storage)) { + char *msg = sprintf_alloc("%s the database storage directory not allowed", + restore_mode ? "restore from" : "backup to"); + + if (args->data[KNOT_CTL_IDX_ZONE] == NULL) { + log_ctl_error("%s", msg); + } else { + log_ctl_zone_str_error(args->data[KNOT_CTL_IDX_ZONE], "%s", msg); + } + free(msg); + return KNOT_EINVAL; + } + + // Evaluate filters (and possibly fail) before writing to the filesystem. + bool filter_zonefile, filter_journal, filter_timers, filter_kaspdb, filter_catalog; + + // The default filter values are set just in this paragraph. + if (!(eval_opposite_filters(args, &filter_zonefile, true, + CTL_FILTER_BACKUP_ZONEFILE, CTL_FILTER_BACKUP_NOZONEFILE) && + eval_opposite_filters(args, &filter_journal, false, + CTL_FILTER_BACKUP_JOURNAL, CTL_FILTER_BACKUP_NOJOURNAL) && + eval_opposite_filters(args, &filter_timers, true, + CTL_FILTER_BACKUP_TIMERS, CTL_FILTER_BACKUP_NOTIMERS) && + eval_opposite_filters(args, &filter_kaspdb, true, + CTL_FILTER_BACKUP_KASPDB, CTL_FILTER_BACKUP_NOKASPDB) && + eval_opposite_filters(args, &filter_catalog, true, + CTL_FILTER_BACKUP_CATALOG, CTL_FILTER_BACKUP_NOCATALOG))) { + return KNOT_EXPARAM; + } + + bool forced = ctl_has_flag(args->data[KNOT_CTL_IDX_FLAGS], CTL_FLAG_FORCE); + + zone_backup_ctx_t *ctx; + + // The present timer db size is not up-to-date, use the maximum one. + conf_val_t timer_db_size = conf_db_param(conf(), C_TIMER_DB_MAX_SIZE); + + int ret = zone_backup_init(restore_mode, forced, + args->data[KNOT_CTL_IDX_DATA], + knot_lmdb_copy_size(&args->server->kaspdb), + conf_int(&timer_db_size), + knot_lmdb_copy_size(&args->server->journaldb), + knot_lmdb_copy_size(&args->server->catalog.db), + &ctx); + if (ret != KNOT_EOK) { + return ret; + } + + assert(ctx != NULL); + ctx->backup_zonefile = filter_zonefile; + ctx->backup_journal = filter_journal; + ctx->backup_timers = filter_timers; + ctx->backup_kaspdb = filter_kaspdb; + ctx->backup_catalog = filter_catalog; + + zone_backups_add(&args->server->backup_ctxs, ctx); + + return ret; +} + +static zone_backup_ctx_t *latest_backup_ctx(ctl_args_t *args) +{ + // no need to mutex in this case + return (zone_backup_ctx_t *)TAIL(args->server->backup_ctxs.ctxs); +} + +static int deinit_backup(ctl_args_t *args) +{ + return zone_backup_deinit(latest_backup_ctx(args)); +} + +static int zone_backup_cmd(zone_t *zone, ctl_args_t *args) +{ + zone_backup_ctx_t *ctx = latest_backup_ctx(args); + if (!ctx->restore_mode && ctx->failed) { + // No need to proceed with already faulty backup. + return KNOT_EOK; + } + + if (zone->backup_ctx != NULL) { + log_zone_warning(zone->name, "backup or restore already in progress, skipping zone"); + ctx->failed = true; + return KNOT_EPROGRESS; + } + + zone->backup_ctx = ctx; + pthread_mutex_lock(&ctx->readers_mutex); + ctx->readers++; + pthread_mutex_unlock(&ctx->readers_mutex); + ctx->zone_count++; + + int ret = schedule_trigger(zone, args, ZONE_EVENT_BACKUP, true); + + if (ret == KNOT_EOK && !ctx->backup_global && (ctx->restore_mode || !ctx->failed)) { + ret = global_backup(ctx, zone_catalog(zone), zone->name); + } + + return ret; +} + +static int zones_apply_backup(ctl_args_t *args, bool restore_mode) +{ + int ret_deinit; + int ret = init_backup(args, restore_mode); + + if (ret != KNOT_EOK) { + char *msg = sprintf_alloc("%s init failed (%s)", + restore_mode ? "restore" : "backup", + knot_strerror(ret)); + + if (args->data[KNOT_CTL_IDX_ZONE] == NULL) { + log_ctl_error("%s", msg); + } else { + log_ctl_zone_str_error(args->data[KNOT_CTL_IDX_ZONE], + "%s", msg); + } + free (msg); + + /* Warning: zone name in the control command params discarded here. */ + args->data[KNOT_CTL_IDX_ZONE] = NULL; + send_error(args, knot_strerror(ret)); + return KNOT_CTL_EZONE; + } + + /* Global catalog zones backup. */ + if (args->data[KNOT_CTL_IDX_ZONE] == NULL) { + zone_backup_ctx_t *ctx = latest_backup_ctx(args); + ctx->backup_global = true; + ret = global_backup(ctx, &args->server->catalog, NULL); + if (ret != KNOT_EOK) { + log_ctl_error("control, error (%s)", knot_strerror(ret)); + send_error(args, knot_strerror(ret)); + ret = KNOT_EOK; + goto done; + } + } + + ret = zones_apply(args, zone_backup_cmd); + +done: + ret_deinit = deinit_backup(args); + return ret != KNOT_EOK ? ret : ret_deinit; +} + +static int zone_sign(zone_t *zone, _unused_ ctl_args_t *args) +{ + conf_val_t val = conf_zone_get(conf(), C_DNSSEC_SIGNING, zone->name); + if (!conf_bool(&val)) { + args->suppress = true; + return KNOT_ENOTSUP; + } + + zone_set_flag(zone, ZONE_FORCE_RESIGN); + return schedule_trigger(zone, args, ZONE_EVENT_DNSSEC, true); +} + +static int zone_keys_load(zone_t *zone, _unused_ ctl_args_t *args) +{ + conf_val_t val = conf_zone_get(conf(), C_DNSSEC_SIGNING, zone->name); + if (!conf_bool(&val)) { + args->suppress = true; + return KNOT_ENOTSUP; + } + + return schedule_trigger(zone, args, ZONE_EVENT_DNSSEC, true); +} + +static int zone_key_roll(zone_t *zone, ctl_args_t *args) +{ + conf_val_t val = conf_zone_get(conf(), C_DNSSEC_SIGNING, zone->name); + if (!conf_bool(&val)) { + args->suppress = true; + return KNOT_ENOTSUP; + } + + const char *key_type = args->data[KNOT_CTL_IDX_TYPE]; + if (strncasecmp(key_type, "ksk", 3) == 0) { + zone_set_flag(zone, ZONE_FORCE_KSK_ROLL); + } else if (strncasecmp(key_type, "zsk", 3) == 0) { + zone_set_flag(zone, ZONE_FORCE_ZSK_ROLL); + } else { + return KNOT_EINVAL; + } + + return schedule_trigger(zone, args, ZONE_EVENT_DNSSEC, true); +} + +static int zone_ksk_sbm_confirm(zone_t *zone, _unused_ ctl_args_t *args) +{ + kdnssec_ctx_t ctx = { 0 }; + + int ret = kdnssec_ctx_init(conf(), &ctx, zone->name, zone_kaspdb(zone), NULL); + if (ret != KNOT_EOK) { + return ret; + } + + ret = knot_dnssec_ksk_sbm_confirm(&ctx, 0); + kdnssec_ctx_deinit(&ctx); + + conf_val_t val = conf_zone_get(conf(), C_DNSSEC_SIGNING, zone->name); + if (ret == KNOT_EOK && conf_bool(&val)) { + // NOT zone_events_schedule_user(), intentionally! + ret = schedule_trigger(zone, args, ZONE_EVENT_DNSSEC, false); + } + + return ret; +} + +static int zone_freeze(zone_t *zone, _unused_ ctl_args_t *args) +{ + return schedule_trigger(zone, args, ZONE_EVENT_UFREEZE, false); +} + +static int zone_thaw(zone_t *zone, _unused_ ctl_args_t *args) +{ + return schedule_trigger(zone, args, ZONE_EVENT_UTHAW, false); +} + +static int zone_xfr_freeze(zone_t *zone, _unused_ ctl_args_t *args) +{ + zone_set_flag(zone, ZONE_XFR_FROZEN); + + log_zone_info(zone->name, "outgoing XFR frozen"); + + return KNOT_EOK; +} + +static int zone_xfr_thaw(zone_t *zone, _unused_ ctl_args_t *args) +{ + zone_unset_flag(zone, ZONE_XFR_FROZEN); + + log_zone_info(zone->name, "outgoing XFR unfrozen"); + + return KNOT_EOK; +} + +static int zone_txn_begin(zone_t *zone, _unused_ ctl_args_t *args) +{ + if (zone->control_update != NULL) { + return KNOT_TXN_EEXISTS; + } + + zone->control_update = malloc(sizeof(zone_update_t)); + if (zone->control_update == NULL) { + return KNOT_ENOMEM; + } + + zone_update_flags_t type = (zone->contents == NULL) ? UPDATE_FULL : UPDATE_INCREMENTAL; + int ret = zone_update_init(zone->control_update, zone, type | UPDATE_STRICT); + if (ret != KNOT_EOK) { + free(zone->control_update); + zone->control_update = NULL; + } + + return ret; +} + +static int zone_txn_commit(zone_t *zone, _unused_ ctl_args_t *args) +{ + if (zone->control_update == NULL) { + args->suppress = true; + return KNOT_TXN_ENOTEXISTS; + } + + int ret = zone_update_semcheck(conf(), zone->control_update); + if (ret != KNOT_EOK) { + return ret; // Recoverable error. + } + + // NOOP if empty changeset/contents. + if (((zone->control_update->flags & UPDATE_INCREMENTAL) && + changeset_empty(&zone->control_update->change)) || + ((zone->control_update->flags & UPDATE_FULL) && + zone_contents_is_empty(zone->control_update->new_cont))) { + zone_control_clear(zone); + return KNOT_EOK; + } + + // Sign update. + conf_val_t val = conf_zone_get(conf(), C_DNSSEC_SIGNING, zone->name); + bool dnssec_enable = conf_bool(&val); + val = conf_zone_get(conf(), C_ZONEMD_GENERATE, zone->name); + unsigned digest_alg = conf_opt(&val); + if (dnssec_enable) { + if (zone->control_update->flags & UPDATE_FULL) { + zone_sign_reschedule_t resch = { 0 }; + zone_sign_roll_flags_t rflags = KEY_ROLL_ALLOW_ALL; + ret = knot_dnssec_zone_sign(zone->control_update, conf(), 0, rflags, 0, &resch); + event_dnssec_reschedule(conf(), zone, &resch, false); + } else { + ret = knot_dnssec_sign_update(zone->control_update, conf()); + } + } else if (digest_alg != ZONE_DIGEST_NONE) { + if (zone_update_to(zone->control_update) == NULL) { + ret = zone_update_increment_soa(zone->control_update, conf()); + } + if (ret == KNOT_EOK) { + ret = zone_update_add_digest(zone->control_update, digest_alg, false); + } + } + if (ret != KNOT_EOK) { + zone_control_clear(zone); + return ret; + } + + ret = zone_update_commit(conf(), zone->control_update); + if (ret != KNOT_EOK) { + zone_control_clear(zone); + return ret; + } + + free(zone->control_update); + zone->control_update = NULL; + + zone_schedule_notify(zone, 0); + + return KNOT_EOK; +} + +static int zone_txn_abort(zone_t *zone, _unused_ ctl_args_t *args) +{ + if (zone->control_update == NULL) { + args->suppress = true; + return KNOT_TXN_ENOTEXISTS; + } + + zone_control_clear(zone); + + return KNOT_EOK; +} + +static int init_send_ctx(send_ctx_t *ctx, const knot_dname_t *zone_name, + ctl_args_t *args) +{ + memset(ctx, 0, sizeof(*ctx)); + + ctx->args = args; + + // Set the dump style. + ctx->style.show_ttl = true; + ctx->style.original_ttl = true; + ctx->style.human_timestamp = true; + + // Set the output data buffers. + ctx->data[KNOT_CTL_IDX_ZONE] = ctx->zone; + ctx->data[KNOT_CTL_IDX_OWNER] = ctx->owner; + ctx->data[KNOT_CTL_IDX_TTL] = ctx->ttl; + ctx->data[KNOT_CTL_IDX_TYPE] = ctx->type; + ctx->data[KNOT_CTL_IDX_DATA] = ctx->rdata; + + // Set the ZONE. + if (knot_dname_to_str(ctx->zone, zone_name, sizeof(ctx->zone)) == NULL) { + return KNOT_EINVAL; + } + + // Set the TYPE filter. + if (args->data[KNOT_CTL_IDX_TYPE] != NULL) { + uint16_t type; + if (knot_rrtype_from_string(args->data[KNOT_CTL_IDX_TYPE], &type) != 0) { + return KNOT_EINVAL; + } + ctx->type_filter = type; + } else { + ctx->type_filter = -1; + } + + return KNOT_EOK; +} + +static int send_rrset(knot_rrset_t *rrset, send_ctx_t *ctx) +{ + if (rrset->type != KNOT_RRTYPE_RRSIG) { + int ret = snprintf(ctx->ttl, sizeof(ctx->ttl), "%u", rrset->ttl); + if (ret <= 0 || ret >= sizeof(ctx->ttl)) { + return KNOT_ESPACE; + } + } + + if (knot_rrtype_to_string(rrset->type, ctx->type, sizeof(ctx->type)) < 0) { + return KNOT_ESPACE; + } + + for (size_t i = 0; i < rrset->rrs.count; ++i) { + if (rrset->type == KNOT_RRTYPE_RRSIG) { + int ret = snprintf(ctx->ttl, sizeof(ctx->ttl), "%u", + knot_rrsig_original_ttl(knot_rdataset_at(&rrset->rrs, i))); + if (ret <= 0 || ret >= sizeof(ctx->ttl)) { + return KNOT_ESPACE; + } + } + + int ret = knot_rrset_txt_dump_data(rrset, i, ctx->rdata, + sizeof(ctx->rdata), &ctx->style); + if (ret < 0) { + return ret; + } + + ret = knot_ctl_send(ctx->args->ctl, KNOT_CTL_TYPE_DATA, &ctx->data); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int send_node(zone_node_t *node, void *ctx_void) +{ + send_ctx_t *ctx = ctx_void; + if (knot_dname_to_str(ctx->owner, node->owner, sizeof(ctx->owner)) == NULL) { + return KNOT_EINVAL; + } + + for (size_t i = 0; i < node->rrset_count; ++i) { + knot_rrset_t rrset = node_rrset_at(node, i); + + // Check for requested TYPE. + if (ctx->type_filter != -1 && rrset.type != ctx->type_filter) { + continue; + } + + int ret = send_rrset(&rrset, ctx); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int get_owner(uint8_t *out, size_t out_len, knot_dname_t *origin, + ctl_args_t *args) +{ + const char *owner = args->data[KNOT_CTL_IDX_OWNER]; + assert(owner != NULL); + + bool fqdn = false; + size_t prefix_len = 0; + + size_t owner_len = strlen(owner); + if (owner_len > 0 && (owner_len != 1 || owner[0] != '@')) { + // Check if the owner is FQDN. + if (owner[owner_len - 1] == '.') { + fqdn = true; + } + + if (knot_dname_from_str(out, owner, out_len) == NULL) { + return KNOT_EINVAL; + } + knot_dname_to_lower(out); + + prefix_len = knot_dname_size(out); + if (prefix_len == 0) { + return KNOT_EINVAL; + } + + // Ignore trailing dot. + prefix_len--; + } + + // Append the origin. + if (!fqdn) { + size_t origin_len = knot_dname_size(origin); + if (origin_len == 0 || origin_len > out_len - prefix_len) { + return KNOT_EINVAL; + } + memcpy(out + prefix_len, origin, origin_len); + } + + return KNOT_EOK; +} + +static int zone_read(zone_t *zone, ctl_args_t *args) +{ + send_ctx_t *ctx = &ctl_globals.send_ctx; + int ret = init_send_ctx(ctx, zone->name, args); + if (ret != KNOT_EOK) { + return ret; + } + + if (args->data[KNOT_CTL_IDX_OWNER] != NULL) { + knot_dname_storage_t owner; + + ret = get_owner(owner, sizeof(owner), zone->name, args); + if (ret != KNOT_EOK) { + return ret; + } + + const zone_node_t *node = zone_contents_node_or_nsec3(zone->contents, owner); + if (node == NULL) { + return KNOT_ENONODE; + } + + ret = send_node((zone_node_t *)node, ctx); + } else if (zone->contents != NULL) { + ret = zone_contents_apply(zone->contents, send_node, ctx); + if (ret == KNOT_EOK) { + ret = zone_contents_nsec3_apply(zone->contents, send_node, ctx); + } + } + + return ret; +} + +static int zone_flag_txn_get(zone_t *zone, ctl_args_t *args, const char *flag) +{ + if (zone->control_update == NULL) { + args->suppress = true; + return KNOT_TXN_ENOTEXISTS; + } + + send_ctx_t *ctx = &ctl_globals.send_ctx; + int ret = init_send_ctx(ctx, zone->name, args); + if (ret != KNOT_EOK) { + return ret; + } + ctx->data[KNOT_CTL_IDX_FLAGS] = flag; + + if (args->data[KNOT_CTL_IDX_OWNER] != NULL) { + knot_dname_storage_t owner; + + ret = get_owner(owner, sizeof(owner), zone->name, args); + if (ret != KNOT_EOK) { + return ret; + } + + const zone_node_t *node = zone_contents_node_or_nsec3(zone->control_update->new_cont, owner); + if (node == NULL) { + return KNOT_ENONODE; + + } + + ret = send_node((zone_node_t *)node, ctx); + } else { + zone_tree_it_t it = { 0 }; + ret = zone_tree_it_double_begin(zone->control_update->new_cont->nodes, + zone->control_update->new_cont->nsec3_nodes, + &it); + while (ret == KNOT_EOK && !zone_tree_it_finished(&it)) { + ret = send_node(zone_tree_it_val(&it), ctx); + zone_tree_it_next(&it); + } + zone_tree_it_free(&it); + } + + return ret; +} + +static int zone_txn_get(zone_t *zone, ctl_args_t *args) +{ + return zone_flag_txn_get(zone, args, NULL); +} + +static int send_changeset_part(changeset_t *ch, send_ctx_t *ctx, bool from) +{ + ctx->data[KNOT_CTL_IDX_FLAGS] = from ? CTL_FLAG_DIFF_REM : CTL_FLAG_DIFF_ADD; + + // Send SOA only if explicitly changed. + if (ch->soa_to != NULL) { + knot_rrset_t *soa = from ? ch->soa_from : ch->soa_to; + assert(soa); + + char *owner = knot_dname_to_str(ctx->owner, soa->owner, sizeof(ctx->owner)); + if (owner == NULL) { + return KNOT_EINVAL; + } + + int ret = send_rrset(soa, ctx); + if (ret != KNOT_EOK) { + return ret; + } + } + + // Send other records. + changeset_iter_t it; + int ret = from ? changeset_iter_rem(&it, ch) : changeset_iter_add(&it, ch); + if (ret != KNOT_EOK) { + return ret; + } + + knot_rrset_t rrset = changeset_iter_next(&it); + while (!knot_rrset_empty(&rrset)) { + char *owner = knot_dname_to_str(ctx->owner, rrset.owner, sizeof(ctx->owner)); + if (owner == NULL) { + changeset_iter_clear(&it); + return KNOT_EINVAL; + } + + ret = send_rrset(&rrset, ctx); + if (ret != KNOT_EOK) { + changeset_iter_clear(&it); + return ret; + } + + rrset = changeset_iter_next(&it); + } + changeset_iter_clear(&it); + + return KNOT_EOK; +} + +static int send_changeset(changeset_t *ch, send_ctx_t *ctx) +{ + // First send 'from' changeset part. + int ret = send_changeset_part(ch, ctx, true); + if (ret != KNOT_EOK) { + return ret; + } + + // Second send 'to' changeset part. + return send_changeset_part(ch, ctx, false); +} + +static int zone_txn_diff(zone_t *zone, ctl_args_t *args) +{ + if (zone->control_update == NULL) { + args->suppress = true; + return KNOT_TXN_ENOTEXISTS; + } + + // FULL update has no changeset to print, do a 'get' instead. + if (zone->control_update->flags & UPDATE_FULL) { + return zone_flag_txn_get(zone, args, CTL_FLAG_DIFF_ADD); + } + + send_ctx_t *ctx = &ctl_globals.send_ctx; + int ret = init_send_ctx(ctx, zone->name, args); + if (ret != KNOT_EOK) { + return ret; + } + + return send_changeset(&zone->control_update->change, ctx); +} + +static int get_ttl(zone_t *zone, ctl_args_t *args, uint32_t *ttl) +{ + knot_dname_storage_t owner; + + int ret = get_owner(owner, sizeof(owner), zone->name, args); + if (ret != KNOT_EOK) { + return ret; + } + + const zone_node_t *node = zone_contents_node_or_nsec3(zone->control_update->new_cont, owner); + if (node == NULL) { + return KNOT_ENOTTL; + } + + uint16_t type; + if (knot_rrtype_from_string(args->data[KNOT_CTL_IDX_TYPE], &type) != 0) { + return KNOT_EINVAL; + } + + knot_rrset_t rrset = node_rrset(node, type); + if (knot_rrset_empty(&rrset)) { + return KNOT_ENOTTL; + } + *ttl = rrset.ttl; + + return KNOT_EOK; +} + +static int create_rrset(knot_rrset_t **rrset, zone_t *zone, ctl_args_t *args, + bool need_ttl) +{ + knot_dname_txt_storage_t origin_buff; + char *origin = knot_dname_to_str(origin_buff, zone->name, sizeof(origin_buff)); + if (origin == NULL) { + return KNOT_EINVAL; + } + + const char *owner = args->data[KNOT_CTL_IDX_OWNER]; + const char *type = args->data[KNOT_CTL_IDX_TYPE]; + const char *data = args->data[KNOT_CTL_IDX_DATA]; + const char *ttl = need_ttl ? args->data[KNOT_CTL_IDX_TTL] : NULL; + + // Prepare a buffer for a reconstructed record. + const size_t buff_len = sizeof(ctl_globals.txt_rr); + char *buff = ctl_globals.txt_rr; + + uint32_t default_ttl = 0; + if (ttl == NULL) { + int ret = get_ttl(zone, args, &default_ttl); + if (need_ttl && ret != KNOT_EOK) { + return ret; + } + } + + // Reconstruct the record. + int ret = snprintf(buff, buff_len, "%s %s %s %s\n", + (owner != NULL ? owner : ""), + (ttl != NULL ? ttl : ""), + (type != NULL ? type : ""), + (data != NULL ? data : "")); + if (ret <= 0 || ret >= buff_len) { + return KNOT_ESPACE; + } + size_t rdata_len = ret; + + // Parse the record. + zs_scanner_t *scanner = &ctl_globals.scanner; + if (zs_init(scanner, origin, KNOT_CLASS_IN, default_ttl) != 0 || + zs_set_input_string(scanner, buff, rdata_len) != 0 || + zs_parse_record(scanner) != 0 || + scanner->state != ZS_STATE_DATA) { + ret = KNOT_EPARSEFAIL; + goto parser_failed; + } + knot_dname_to_lower(scanner->r_owner); + + // Create output rrset. + *rrset = knot_rrset_new(scanner->r_owner, scanner->r_type, + scanner->r_class, scanner->r_ttl, NULL); + if (*rrset == NULL) { + ret = KNOT_ENOMEM; + goto parser_failed; + } + + ret = knot_rrset_add_rdata(*rrset, scanner->r_data, scanner->r_data_length, + NULL); +parser_failed: + zs_deinit(scanner); + + return ret; +} + +static int zone_txn_set(zone_t *zone, ctl_args_t *args) +{ + if (zone->control_update == NULL) { + args->suppress = true; + return KNOT_TXN_ENOTEXISTS; + } + + if (args->data[KNOT_CTL_IDX_OWNER] == NULL || + args->data[KNOT_CTL_IDX_TYPE] == NULL) { + return KNOT_EINVAL; + } + + knot_rrset_t *rrset; + int ret = create_rrset(&rrset, zone, args, true); + if (ret != KNOT_EOK) { + return ret; + } + + ret = zone_update_add(zone->control_update, rrset); + knot_rrset_free(rrset, NULL); + + return ret; +} + +static int zone_txn_unset(zone_t *zone, ctl_args_t *args) +{ + if (zone->control_update == NULL) { + args->suppress = true; + return KNOT_TXN_ENOTEXISTS; + } + + if (args->data[KNOT_CTL_IDX_OWNER] == NULL) { + return KNOT_EINVAL; + } + + // Remove specific record. + if (args->data[KNOT_CTL_IDX_DATA] != NULL) { + if (args->data[KNOT_CTL_IDX_TYPE] == NULL) { + return KNOT_EINVAL; + } + + knot_rrset_t *rrset; + int ret = create_rrset(&rrset, zone, args, false); + if (ret != KNOT_EOK) { + return ret; + } + + ret = zone_update_remove(zone->control_update, rrset); + knot_rrset_free(rrset, NULL); + return ret; + } else { + knot_dname_storage_t owner; + + int ret = get_owner(owner, sizeof(owner), zone->name, args); + if (ret != KNOT_EOK) { + return ret; + } + + // Remove whole rrset. + if (args->data[KNOT_CTL_IDX_TYPE] != NULL) { + uint16_t type; + if (knot_rrtype_from_string(args->data[KNOT_CTL_IDX_TYPE], + &type) != 0) { + return KNOT_EINVAL; + } + + return zone_update_remove_rrset(zone->control_update, owner, type); + // Remove whole node. + } else { + return zone_update_remove_node(zone->control_update, owner); + } + } +} + +static bool zone_exists(const knot_dname_t *zone, void *data) +{ + assert(zone); + assert(data); + + knot_zonedb_t *db = data; + + return knot_zonedb_find(db, zone) != NULL; +} + +static bool zone_names_distinct(const knot_dname_t *zone, void *data) +{ + assert(zone); + assert(data); + + knot_dname_t *zone_to_purge = data; + + return !knot_dname_is_equal(zone, zone_to_purge); +} + +static int drop_journal_if_orphan(const knot_dname_t *for_zone, void *ctx) +{ + server_t *server = ctx; + zone_journal_t j = { &server->journaldb, for_zone }; + if (!zone_exists(for_zone, server->zone_db)) { + return journal_scrape_with_md(j, false); + } + return KNOT_EOK; +} + +static int purge_orphan_member_cb(const knot_dname_t *member, const knot_dname_t *owner, + const knot_dname_t *catz, const char *group, void *ctx) +{ + server_t *server = ctx; + if (zone_exists(member, server->zone_db)) { + return KNOT_EOK; + } + + const char *err_str = NULL; + + rcu_read_lock(); + zone_t *cat_z = knot_zonedb_find(server->zone_db, catz); + if (cat_z == NULL) { + err_str = "existing"; + } else if (!cat_z->is_catalog_flag) { + err_str = "catalog"; + } + rcu_read_unlock(); + + if (err_str == NULL) { + return KNOT_EOK; + } + + knot_dname_txt_storage_t catz_str; + (void)knot_dname_to_str(catz_str, catz, sizeof(catz_str)); + log_zone_info(member, "member of a non-%s zone %s", + err_str, catz_str); + + // Single-purpose fake zone_t containing only minimal data. + // malloc() should suffice here, but clean zone_t is more mishandling-proof. + zone_t *orphan = calloc(1, sizeof(zone_t)); + if (orphan == NULL) { + return KNOT_ENOMEM; + } + + orphan->name = (knot_dname_t *)member; + orphan->server = server; + + purge_flag_t params = + PURGE_ZONE_TIMERS | PURGE_ZONE_JOURNAL | PURGE_ZONE_KASPDB | + PURGE_ZONE_BEST | PURGE_ZONE_LOG; + + int ret = selective_zone_purge(conf(), orphan, params); + free(orphan); + if (ret != KNOT_EOK) { + log_zone_error(member, "purge of an orphaned zone failed (%s)", + knot_strerror(ret)); + } + + // this deleting inside catalog DB iteration is OK, since + // the deletion happens in RW txn, while the iteration in persistent RO txn + ret = catalog_del(&server->catalog, member); + if (ret != KNOT_EOK) { + log_zone_error(member, "remove of an orphan from catalog failed (%s)", + knot_strerror(ret)); + } + + return KNOT_EOK; +} + +static int catalog_orphans_sweep(server_t *server) +{ + catalog_t *cat = &server->catalog; + int ret2 = KNOT_EOK; + int ret = catalog_begin(cat); + if (ret == KNOT_EOK) { + ret = catalog_apply(cat, NULL, + purge_orphan_member_cb, + server, false); + if (ret != KNOT_EOK) { + log_error("failed to purge orphan members data (%s)", + knot_strerror(ret)); + } + ret2 = catalog_commit(cat); + synchronize_rcu(); + catalog_commit_cleanup(cat); + if (ret2 != KNOT_EOK) { + log_error("failed to update catalog (%s)", + knot_strerror(ret)); + } + } else { + log_error("can't open catalog for purging (%s)", + knot_strerror(ret)); + } + + return (ret == KNOT_EOK) ? ret2 : ret; +} + +static void log_if_orphans_error(knot_dname_t *zone_name, int err, char *db_type, + bool *failed) +{ + if (err == KNOT_EOK || err == KNOT_ENOENT || err == KNOT_EFILE) { + return; + } + + *failed = true; + const char *error = knot_strerror(err); + + char *msg = sprintf_alloc("control, failed to purge orphan from %s database (%s)", + db_type, error); + if (msg == NULL) { + return; + } + + if (zone_name == NULL) { + log_error("%s", msg); + } else { + log_zone_error(zone_name, "%s", msg); + } + free(msg); +} + +static int orphans_purge(ctl_args_t *args) +{ + assert(args->data[KNOT_CTL_IDX_FILTER] != NULL); + bool only_orphan = (strlen(args->data[KNOT_CTL_IDX_FILTER]) == 1); + int ret; + bool failed = false; + + if (args->data[KNOT_CTL_IDX_ZONE] == NULL) { + // Purge KASP DB. + if (only_orphan || MATCH_AND_FILTER(args, CTL_FILTER_PURGE_KASPDB)) { + ret = kasp_db_sweep(&args->server->kaspdb, + zone_exists, args->server->zone_db); + log_if_orphans_error(NULL, ret, "KASP", &failed); + } + + // Purge zone journals of unconfigured zones. + if (only_orphan || MATCH_AND_FILTER(args, CTL_FILTER_PURGE_JOURNAL)) { + ret = journals_walk(&args->server->journaldb, + drop_journal_if_orphan, args->server); + log_if_orphans_error(NULL, ret, "journal", &failed); + } + + // Purge timers of unconfigured zones. + if (only_orphan || MATCH_AND_FILTER(args, CTL_FILTER_PURGE_TIMERS)) { + ret = zone_timers_sweep(&args->server->timerdb, + zone_exists, args->server->zone_db); + log_if_orphans_error(NULL, ret, "timer", &failed); + } + + // Purge and remove orphan members of non-existing/non-catalog zones. + if (only_orphan || MATCH_AND_FILTER(args, CTL_FILTER_PURGE_CATALOG)) { + ret = catalog_orphans_sweep(args->server); + log_if_orphans_error(NULL, ret, "catalog", &failed); + } + + if (failed) { + send_error(args, knot_strerror(KNOT_CTL_EZONE)); + } + } else { + knot_dname_storage_t buff; + while (true) { + knot_dname_t *zone_name = + knot_dname_from_str(buff, args->data[KNOT_CTL_IDX_ZONE], + sizeof(buff)); + if (zone_name == NULL) { + log_ctl_zone_str_error(args->data[KNOT_CTL_IDX_ZONE], + "control, error (%s)", + knot_strerror(KNOT_EINVAL)); + send_error(args, knot_strerror(KNOT_EINVAL)); + return KNOT_EINVAL; + } + knot_dname_to_lower(zone_name); + + if (!zone_exists(zone_name, args->server->zone_db)) { + // Purge KASP DB. + if (only_orphan || MATCH_AND_FILTER(args, CTL_FILTER_PURGE_KASPDB)) { + if (knot_lmdb_open(&args->server->kaspdb) == KNOT_EOK) { + ret = kasp_db_delete_all(&args->server->kaspdb, zone_name); + log_if_orphans_error(zone_name, ret, "KASP", &failed); + } + } + + // Purge zone journal. + if (only_orphan || MATCH_AND_FILTER(args, CTL_FILTER_PURGE_JOURNAL)) { + zone_journal_t j = { &args->server->journaldb, zone_name }; + ret = journal_scrape_with_md(j, true); + log_if_orphans_error(zone_name, ret, "journal", &failed); + } + + // Purge zone timers. + if (only_orphan || MATCH_AND_FILTER(args, CTL_FILTER_PURGE_TIMERS)) { + ret = zone_timers_sweep(&args->server->timerdb, + zone_names_distinct, zone_name); + log_if_orphans_error(zone_name, ret, "timer", &failed); + } + + // Purge Catalog. + if (only_orphan || MATCH_AND_FILTER(args, CTL_FILTER_PURGE_CATALOG)) { + ret = catalog_zone_purge(args->server, NULL, zone_name); + log_if_orphans_error(zone_name, ret, "catalog", &failed); + } + + if (failed) { + send_error(args, knot_strerror(KNOT_ERROR)); + failed = false; + } + } + + // Get next zone name. + ret = knot_ctl_receive(args->ctl, &args->type, &args->data); + if (ret != KNOT_EOK || args->type != KNOT_CTL_TYPE_DATA) { + break; + } + strtolower((char *)args->data[KNOT_CTL_IDX_ZONE]); + + // Log the other zones the same way as the first one from process.c. + log_ctl_zone_str_info(args->data[KNOT_CTL_IDX_ZONE], + "control, received command '%s'", + args->data[KNOT_CTL_IDX_CMD]); + } + } + + return KNOT_EOK; +} + +static int zone_purge(zone_t *zone, ctl_args_t *args) +{ + if (MATCH_OR_FILTER(args, CTL_FILTER_PURGE_EXPIRE)) { + // Abort possible editing transaction. + int ret = zone_txn_abort(zone, args); + if (ret != KNOT_EOK && ret != KNOT_TXN_ENOTEXISTS) { + log_zone_error(zone->name, + "failed to abort pending transaction (%s)", + knot_strerror(ret)); + return ret; + } + + // Expire the zone. + // KNOT_EOK is the only return value from event_expire(). + (void)schedule_trigger(zone, args, ZONE_EVENT_EXPIRE, true); + } + + purge_flag_t params = + MATCH_OR_FILTER(args, CTL_FILTER_PURGE_TIMERS) * PURGE_ZONE_TIMERS | + MATCH_OR_FILTER(args, CTL_FILTER_PURGE_ZONEFILE) * PURGE_ZONE_ZONEFILE | + MATCH_OR_FILTER(args, CTL_FILTER_PURGE_JOURNAL) * PURGE_ZONE_JOURNAL | + MATCH_OR_FILTER(args, CTL_FILTER_PURGE_KASPDB) * PURGE_ZONE_KASPDB | + MATCH_OR_FILTER(args, CTL_FILTER_PURGE_CATALOG) * PURGE_ZONE_CATALOG | + PURGE_ZONE_NOSYNC; // Purge even zonefiles with disabled syncing. + + // Purge the requested zone data. + return selective_zone_purge(conf(), zone, params); +} + +static int send_stats_ctr(mod_ctr_t *ctr, uint64_t **stats_vals, unsigned threads, + ctl_args_t *args, knot_ctl_data_t *data) +{ + char index[128]; + char value[32]; + + if (ctr->count == 1) { + uint64_t counter = stats_get_counter(stats_vals, ctr->offset, threads); + int ret = snprintf(value, sizeof(value), "%"PRIu64, counter); + if (ret <= 0 || ret >= sizeof(value)) { + return KNOT_ESPACE; + } + + (*data)[KNOT_CTL_IDX_ID] = NULL; + (*data)[KNOT_CTL_IDX_DATA] = value; + + ret = knot_ctl_send(args->ctl, KNOT_CTL_TYPE_DATA, data); + if (ret != KNOT_EOK) { + return ret; + } + } else { + bool force = ctl_has_flag(args->data[KNOT_CTL_IDX_FLAGS], + CTL_FLAG_FORCE); + + for (uint32_t i = 0; i < ctr->count; i++) { + uint64_t counter = stats_get_counter(stats_vals, ctr->offset + i, threads); + + // Skip empty counters. + if (counter == 0 && !force) { + continue; + } + + int ret; + if (ctr->idx_to_str) { + char *str = ctr->idx_to_str(i, ctr->count); + if (str == NULL) { + continue; + } + ret = snprintf(index, sizeof(index), "%s", str); + free(str); + } else { + ret = snprintf(index, sizeof(index), "%u", i); + } + if (ret <= 0 || ret >= sizeof(index)) { + return KNOT_ESPACE; + } + + ret = snprintf(value, sizeof(value), "%"PRIu64, counter); + if (ret <= 0 || ret >= sizeof(value)) { + return KNOT_ESPACE; + } + + (*data)[KNOT_CTL_IDX_ID] = index; + (*data)[KNOT_CTL_IDX_DATA] = value; + + knot_ctl_type_t type = (i == 0) ? KNOT_CTL_TYPE_DATA : + KNOT_CTL_TYPE_EXTRA; + ret = knot_ctl_send(args->ctl, type, data); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + return KNOT_EOK; +} + +static int modules_stats(list_t *query_modules, ctl_args_t *args, knot_dname_t *zone) +{ + if (query_modules == NULL) { + return KNOT_EOK; + } + + const char *section = args->data[KNOT_CTL_IDX_SECTION]; + const char *item = args->data[KNOT_CTL_IDX_ITEM]; + + knot_dname_txt_storage_t name = ""; + knot_ctl_data_t data = { 0 }; + + bool section_found = (section == NULL) ? true : false; + bool item_found = (item == NULL) ? true : false; + + knotd_mod_t *mod; + WALK_LIST(mod, *query_modules) { + // Skip modules without statistics. + if (mod->stats_count == 0) { + continue; + } + + // Check for specific module. + if (section != NULL) { + if (section_found) { + break; + } else if (strcasecmp(mod->id->name + 1, section) == 0) { + section_found = true; + } else { + continue; + } + } + + data[KNOT_CTL_IDX_SECTION] = mod->id->name + 1; + + unsigned threads = knotd_mod_threads(mod); + + for (int i = 0; i < mod->stats_count; i++) { + mod_ctr_t *ctr = mod->stats_info + i; + + // Skip empty counter. + if (ctr->name == NULL) { + continue; + } + + // Check for specific counter. + if (item != NULL) { + if (item_found) { + break; + } else if (strcasecmp(ctr->name, item) == 0) { + item_found = true; + } else { + continue; + } + } + + // Prepare zone name if not already prepared. + if (zone != NULL && name[0] == '\0') { + if (knot_dname_to_str(name, zone, sizeof(name)) == NULL) { + return KNOT_EINVAL; + } + data[KNOT_CTL_IDX_ZONE] = name; + } + + data[KNOT_CTL_IDX_ITEM] = ctr->name; + + // Send the counters. + int ret = send_stats_ctr(ctr, mod->stats_vals, threads, args, &data); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + return (section_found && item_found) ? KNOT_EOK : KNOT_ENOENT; +} + +static int zone_stats(zone_t *zone, ctl_args_t *args) +{ + return modules_stats(&zone->query_modules, args, zone->name); +} + +static int ctl_zone(ctl_args_t *args, ctl_cmd_t cmd) +{ + switch (cmd) { + case CTL_ZONE_STATUS: + return zones_apply(args, zone_status); + case CTL_ZONE_RELOAD: + return zones_apply(args, zone_reload); + case CTL_ZONE_REFRESH: + return zones_apply(args, zone_refresh); + case CTL_ZONE_RETRANSFER: + return zones_apply(args, zone_retransfer); + case CTL_ZONE_NOTIFY: + return zones_apply(args, zone_notify); + case CTL_ZONE_FLUSH: + return zones_apply(args, zone_flush); + case CTL_ZONE_BACKUP: + return zones_apply_backup(args, false); + case CTL_ZONE_RESTORE: + return zones_apply_backup(args, true); + case CTL_ZONE_SIGN: + return zones_apply(args, zone_sign); + case CTL_ZONE_KEYS_LOAD: + return zones_apply(args, zone_keys_load); + case CTL_ZONE_KEY_ROLL: + return zones_apply(args, zone_key_roll); + case CTL_ZONE_KSK_SBM: + return zones_apply(args, zone_ksk_sbm_confirm); + case CTL_ZONE_FREEZE: + return zones_apply(args, zone_freeze); + case CTL_ZONE_THAW: + return zones_apply(args, zone_thaw); + case CTL_ZONE_XFR_FREEZE: + return zones_apply(args, zone_xfr_freeze); + case CTL_ZONE_XFR_THAW: + return zones_apply(args, zone_xfr_thaw); + case CTL_ZONE_READ: + return zones_apply(args, zone_read); + case CTL_ZONE_BEGIN: + return zones_apply(args, zone_txn_begin); + case CTL_ZONE_COMMIT: + return zones_apply(args, zone_txn_commit); + case CTL_ZONE_ABORT: + return zones_apply(args, zone_txn_abort); + case CTL_ZONE_DIFF: + return zones_apply(args, zone_txn_diff); + case CTL_ZONE_GET: + return zones_apply(args, zone_txn_get); + case CTL_ZONE_SET: + return zones_apply(args, zone_txn_set); + case CTL_ZONE_UNSET: + return zones_apply(args, zone_txn_unset); + case CTL_ZONE_PURGE: + if (MATCH_AND_FILTER(args, CTL_FILTER_PURGE_ORPHAN)) { + return orphans_purge(args); + } else { + return zones_apply(args, zone_purge); + } + case CTL_ZONE_STATS: + return zones_apply(args, zone_stats); + default: + assert(0); + return KNOT_EINVAL; + } +} + +static int server_status(ctl_args_t *args) +{ + const char *type = args->data[KNOT_CTL_IDX_TYPE]; + + if (type == NULL || strlen(type) == 0) { + return KNOT_EOK; + } + + char buff[4096] = ""; + + int ret; + if (strcasecmp(type, "version") == 0) { + ret = snprintf(buff, sizeof(buff), "Version: %s", PACKAGE_VERSION); + } else if (strcasecmp(type, "workers") == 0) { + int running_bkg_wrk, wrk_queue; + worker_pool_status(args->server->workers, false, &running_bkg_wrk, &wrk_queue); + ret = snprintf(buff, sizeof(buff), "UDP workers: %zu, TCP workers: %zu, " + "XDP workers: %zu, background workers: %zu (running: %d, pending: %d)", + conf()->cache.srv_udp_threads, conf()->cache.srv_tcp_threads, + conf()->cache.srv_xdp_threads, conf()->cache.srv_bg_threads, + running_bkg_wrk, wrk_queue); + } else if (strcasecmp(type, "configure") == 0) { + ret = snprintf(buff, sizeof(buff), "%s", CONFIGURE_SUMMARY); + } else { + return KNOT_EINVAL; + } + if (ret <= 0 || ret >= sizeof(buff)) { + return KNOT_ESPACE; + } + + args->data[KNOT_CTL_IDX_DATA] = buff; + + return knot_ctl_send(args->ctl, KNOT_CTL_TYPE_DATA, &args->data); +} + +static int ctl_server(ctl_args_t *args, ctl_cmd_t cmd) +{ + int ret = KNOT_EOK; + + switch (cmd) { + case CTL_STATUS: + ret = server_status(args); + if (ret != KNOT_EOK) { + send_error(args, knot_strerror(ret)); + } + break; + case CTL_STOP: + ret = KNOT_CTL_ESTOP; + break; + case CTL_RELOAD: + ret = server_reload(args->server, RELOAD_FULL); + if (ret != KNOT_EOK) { + send_error(args, knot_strerror(ret)); + } + break; + default: + assert(0); + ret = KNOT_EINVAL; + } + + return ret; +} + +static int ctl_stats(ctl_args_t *args, ctl_cmd_t cmd) +{ + const char *section = args->data[KNOT_CTL_IDX_SECTION]; + const char *item = args->data[KNOT_CTL_IDX_ITEM]; + + bool found = (section == NULL) ? true : false; + + // Process server metrics. + if (section == NULL || strcasecmp(section, "server") == 0) { + char value[32]; + knot_ctl_data_t data = { + [KNOT_CTL_IDX_SECTION] = "server", + [KNOT_CTL_IDX_DATA] = value + }; + + for (const stats_item_t *i = server_stats; i->name != NULL; i++) { + if (item != NULL) { + if (found) { + break; + } else if (strcmp(i->name, item) == 0) { + found = true; + } else { + continue; + } + } else { + found = true; + } + + data[KNOT_CTL_IDX_ITEM] = i->name; + int ret = snprintf(value, sizeof(value), "%"PRIu64, + i->val(args->server)); + if (ret <= 0 || ret >= sizeof(value)) { + ret = KNOT_ESPACE; + send_error(args, knot_strerror(ret)); + return ret; + } + + ret = knot_ctl_send(args->ctl, KNOT_CTL_TYPE_DATA, &data); + if (ret != KNOT_EOK) { + send_error(args, knot_strerror(ret)); + return ret; + } + } + } + + // Process modules metrics. + if (section == NULL || strncasecmp(section, "mod-", strlen("mod-")) == 0) { + int ret = modules_stats(conf()->query_modules, args, NULL); + if (ret != KNOT_EOK) { + send_error(args, knot_strerror(ret)); + return ret; + } + + found = true; + } + + if (!found) { + send_error(args, knot_strerror(KNOT_EINVAL)); + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +static int send_block_data(conf_io_t *io, knot_ctl_data_t *data) +{ + knot_ctl_t *ctl = (knot_ctl_t *)io->misc; + + const yp_item_t *item = (io->key1 != NULL) ? io->key1 : io->key0; + assert(item != NULL); + + char buff[YP_MAX_TXT_DATA_LEN + 1] = "\0"; + + (*data)[KNOT_CTL_IDX_DATA] = buff; + + // Format explicit binary data value. + if (io->data.bin != NULL) { + size_t buff_len = sizeof(buff); + int ret = yp_item_to_txt(item, io->data.bin, io->data.bin_len, buff, + &buff_len, YP_SNOQUOTE); + if (ret != KNOT_EOK) { + return ret; + } + return knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, data); + // Format all multivalued item data if no specified index. + } else if ((item->flags & YP_FMULTI) && io->data.index == 0) { + size_t values = conf_val_count(io->data.val); + for (size_t i = 0; i < values; i++) { + conf_val(io->data.val); + size_t buff_len = sizeof(buff); + int ret = yp_item_to_txt(item, io->data.val->data, + io->data.val->len, buff,&buff_len, + YP_SNOQUOTE); + if (ret != KNOT_EOK) { + return ret; + } + + knot_ctl_type_t type = (i == 0) ? KNOT_CTL_TYPE_DATA : + KNOT_CTL_TYPE_EXTRA; + ret = knot_ctl_send(ctl, type, data); + if (ret != KNOT_EOK) { + return ret; + } + + conf_val_next(io->data.val); + } + return KNOT_EOK; + // Format singlevalued item data or a specified one from multivalued. + } else { + conf_val(io->data.val); + size_t buff_len = sizeof(buff); + int ret = yp_item_to_txt(item, io->data.val->data, io->data.val->len, + buff, &buff_len, YP_SNOQUOTE); + if (ret != KNOT_EOK) { + return ret; + } + return knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, data); + } +} + +static int send_block(conf_io_t *io) +{ + knot_ctl_t *ctl = (knot_ctl_t *)io->misc; + + // Get possible error message. + const char *err = io->error.str; + if (err == NULL && io->error.code != KNOT_EOK) { + err = knot_strerror(io->error.code); + } + + knot_ctl_data_t data = { + [KNOT_CTL_IDX_ERROR] = err, + }; + + if (io->key0 != NULL) { + data[KNOT_CTL_IDX_SECTION] = io->key0->name + 1; + } + if (io->key1 != NULL) { + data[KNOT_CTL_IDX_ITEM] = io->key1->name + 1; + } + + // Get the item prefix. + switch (io->type) { + case NEW: data[KNOT_CTL_IDX_FLAGS] = CTL_FLAG_DIFF_ADD; break; + case OLD: data[KNOT_CTL_IDX_FLAGS] = CTL_FLAG_DIFF_REM; break; + default: break; + } + + knot_dname_txt_storage_t id; + + // Get the textual item id. + if (io->id_len > 0 && io->key0 != NULL) { + size_t id_len = sizeof(id); + int ret = yp_item_to_txt(io->key0->var.g.id, io->id, io->id_len, + id, &id_len, YP_SNOQUOTE); + if (ret != KNOT_EOK) { + return ret; + } + if (io->id_as_data) { + data[KNOT_CTL_IDX_DATA] = id; + } else { + data[KNOT_CTL_IDX_ID] = id; + } + } + + if (io->data.val == NULL && io->data.bin == NULL) { + return knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, &data); + } else { + return send_block_data(io, &data); + } +} + +static int ctl_conf_txn(ctl_args_t *args, ctl_cmd_t cmd) +{ + conf_io_t io = { + .fcn = send_block, + .misc = args->ctl + }; + + int ret = KNOT_EOK; + + switch (cmd) { + case CTL_CONF_BEGIN: + ret = conf_io_begin(false); + break; + case CTL_CONF_ABORT: + conf_io_abort(false); + ret = KNOT_EOK; + break; + case CTL_CONF_COMMIT: + // First check the database. + ret = conf_io_check(&io); + if (ret != KNOT_EOK) { + // A semantic error is already sent by the check function. + if (io.error.code != KNOT_EOK) { + return KNOT_EOK; + } + // No transaction abort! + break; + } + + ret = conf_io_commit(false); + if (ret != KNOT_EOK) { + conf_io_abort(false); + break; + } + + ret = server_reload(args->server, RELOAD_COMMIT); + break; + default: + assert(0); + ret = KNOT_EINVAL; + } + + if (ret != KNOT_EOK) { + send_error(args, knot_strerror(ret)); + } + + return ret; +} + +static void list_zone(zone_t *zone, knot_ctl_t *ctl) +{ + knot_dname_txt_storage_t buff; + knot_dname_to_str(buff, zone->name, sizeof(buff)); + + knot_ctl_data_t data = { + [KNOT_CTL_IDX_SECTION] = "zone", + [KNOT_CTL_IDX_ID] = buff + }; + + (void)knot_ctl_send(ctl, KNOT_CTL_TYPE_DATA, &data); +} + +static int list_zones(knot_zonedb_t *zonedb, knot_ctl_t *ctl) +{ + assert(zonedb != NULL && ctl != NULL); + + knot_zonedb_foreach(zonedb, list_zone, ctl); + + return KNOT_EOK; +} + +static int ctl_conf_list(ctl_args_t *args, ctl_cmd_t cmd) +{ + conf_io_t io = { + .fcn = send_block, + .misc = args->ctl + }; + + int ret = KNOT_EOK; + + while (true) { + const char *key0 = args->data[KNOT_CTL_IDX_SECTION]; + const char *key1 = args->data[KNOT_CTL_IDX_ITEM]; + const char *id = args->data[KNOT_CTL_IDX_ID]; + const char *flags = args->data[KNOT_CTL_IDX_FLAGS]; + + bool schema = ctl_has_flag(flags, CTL_FLAG_LIST_SCHEMA); + bool current = !ctl_has_flag(flags, CTL_FLAG_LIST_TXN); + bool zones = ctl_has_flag(flags, CTL_FLAG_LIST_ZONES); + + if (zones) { + ret = list_zones(args->server->zone_db, args->ctl); + } else { + ret = conf_io_list(key0, key1, id, schema, current, &io); + } + if (ret != KNOT_EOK) { + send_error(args, knot_strerror(ret)); + break; + } + + // Get next data unit. + ret = knot_ctl_receive(args->ctl, &args->type, &args->data); + if (ret != KNOT_EOK || args->type != KNOT_CTL_TYPE_DATA) { + break; + } + } + + return ret; +} + +static int ctl_conf_read(ctl_args_t *args, ctl_cmd_t cmd) +{ + conf_io_t io = { + .fcn = send_block, + .misc = args->ctl + }; + + int ret = KNOT_EOK; + + while (true) { + const char *key0 = args->data[KNOT_CTL_IDX_SECTION]; + const char *key1 = args->data[KNOT_CTL_IDX_ITEM]; + const char *id = args->data[KNOT_CTL_IDX_ID]; + + ctl_log_conf_data(&args->data); + + switch (cmd) { + case CTL_CONF_READ: + ret = conf_io_get(key0, key1, id, true, &io); + break; + case CTL_CONF_DIFF: + ret = conf_io_diff(key0, key1, id, &io); + break; + case CTL_CONF_GET: + ret = conf_io_get(key0, key1, id, false, &io); + break; + default: + assert(0); + ret = KNOT_EINVAL; + } + if (ret != KNOT_EOK) { + send_error(args, knot_strerror(ret)); + break; + } + + // Get next data unit. + ret = knot_ctl_receive(args->ctl, &args->type, &args->data); + if (ret != KNOT_EOK || args->type != KNOT_CTL_TYPE_DATA) { + break; + } + } + + return ret; +} + +static int ctl_conf_modify(ctl_args_t *args, ctl_cmd_t cmd) +{ + // Start child transaction. + int ret = conf_io_begin(true); + if (ret != KNOT_EOK) { + send_error(args, knot_strerror(ret)); + return ret; + } + + while (true) { + const char *key0 = args->data[KNOT_CTL_IDX_SECTION]; + const char *key1 = args->data[KNOT_CTL_IDX_ITEM]; + const char *id = args->data[KNOT_CTL_IDX_ID]; + const char *data = args->data[KNOT_CTL_IDX_DATA]; + + ctl_log_conf_data(&args->data); + + switch (cmd) { + case CTL_CONF_SET: + ret = conf_io_set(key0, key1, id, data); + break; + case CTL_CONF_UNSET: + ret = conf_io_unset(key0, key1, id, data); + break; + default: + assert(0); + ret = KNOT_EINVAL; + } + if (ret != KNOT_EOK) { + send_error(args, knot_strerror(ret)); + break; + } + + // Get next data unit. + ret = knot_ctl_receive(args->ctl, &args->type, &args->data); + if (ret != KNOT_EOK || args->type != KNOT_CTL_TYPE_DATA) { + break; + } + } + + // Finish child transaction. + if (ret == KNOT_EOK) { + ret = conf_io_commit(true); + if (ret != KNOT_EOK) { + send_error(args, knot_strerror(ret)); + } + } else { + conf_io_abort(true); + } + + return ret; +} + +typedef struct { + const char *name; + int (*fcn)(ctl_args_t *, ctl_cmd_t); +} desc_t; + +static const desc_t cmd_table[] = { + [CTL_NONE] = { "" }, + + [CTL_STATUS] = { "status", ctl_server }, + [CTL_STOP] = { "stop", ctl_server }, + [CTL_RELOAD] = { "reload", ctl_server }, + [CTL_STATS] = { "stats", ctl_stats }, + + [CTL_ZONE_STATUS] = { "zone-status", ctl_zone }, + [CTL_ZONE_RELOAD] = { "zone-reload", ctl_zone }, + [CTL_ZONE_REFRESH] = { "zone-refresh", ctl_zone }, + [CTL_ZONE_RETRANSFER] = { "zone-retransfer", ctl_zone }, + [CTL_ZONE_NOTIFY] = { "zone-notify", ctl_zone }, + [CTL_ZONE_FLUSH] = { "zone-flush", ctl_zone }, + [CTL_ZONE_BACKUP] = { "zone-backup", ctl_zone }, + [CTL_ZONE_RESTORE] = { "zone-restore", ctl_zone }, + [CTL_ZONE_SIGN] = { "zone-sign", ctl_zone }, + [CTL_ZONE_KEYS_LOAD] = { "zone-keys-load", ctl_zone }, + [CTL_ZONE_KEY_ROLL] = { "zone-key-rollover", ctl_zone }, + [CTL_ZONE_KSK_SBM] = { "zone-ksk-submitted", ctl_zone }, + [CTL_ZONE_FREEZE] = { "zone-freeze", ctl_zone }, + [CTL_ZONE_THAW] = { "zone-thaw", ctl_zone }, + [CTL_ZONE_XFR_FREEZE] = { "zone-xfr-freeze", ctl_zone }, + [CTL_ZONE_XFR_THAW] = { "zone-xfr-thaw", ctl_zone }, + + [CTL_ZONE_READ] = { "zone-read", ctl_zone }, + [CTL_ZONE_BEGIN] = { "zone-begin", ctl_zone }, + [CTL_ZONE_COMMIT] = { "zone-commit", ctl_zone }, + [CTL_ZONE_ABORT] = { "zone-abort", ctl_zone }, + [CTL_ZONE_DIFF] = { "zone-diff", ctl_zone }, + [CTL_ZONE_GET] = { "zone-get", ctl_zone }, + [CTL_ZONE_SET] = { "zone-set", ctl_zone }, + [CTL_ZONE_UNSET] = { "zone-unset", ctl_zone }, + [CTL_ZONE_PURGE] = { "zone-purge", ctl_zone }, + [CTL_ZONE_STATS] = { "zone-stats", ctl_zone }, + + [CTL_CONF_LIST] = { "conf-list", ctl_conf_list }, + [CTL_CONF_READ] = { "conf-read", ctl_conf_read }, + [CTL_CONF_BEGIN] = { "conf-begin", ctl_conf_txn }, + [CTL_CONF_COMMIT] = { "conf-commit", ctl_conf_txn }, + [CTL_CONF_ABORT] = { "conf-abort", ctl_conf_txn }, + [CTL_CONF_DIFF] = { "conf-diff", ctl_conf_read }, + [CTL_CONF_GET] = { "conf-get", ctl_conf_read }, + [CTL_CONF_SET] = { "conf-set", ctl_conf_modify }, + [CTL_CONF_UNSET] = { "conf-unset", ctl_conf_modify }, +}; + +#define MAX_CTL_CODE (sizeof(cmd_table) / sizeof(desc_t) - 1) + +const char *ctl_cmd_to_str(ctl_cmd_t cmd) +{ + if (cmd <= CTL_NONE || cmd > MAX_CTL_CODE) { + return NULL; + } + + return cmd_table[cmd].name; +} + +ctl_cmd_t ctl_str_to_cmd(const char *cmd_str) +{ + if (cmd_str == NULL) { + return CTL_NONE; + } + + for (ctl_cmd_t cmd = CTL_NONE + 1; cmd <= MAX_CTL_CODE; cmd++) { + if (strcmp(cmd_str, cmd_table[cmd].name) == 0) { + return cmd; + } + } + + return CTL_NONE; +} + +int ctl_exec(ctl_cmd_t cmd, ctl_args_t *args) +{ + if (args == NULL) { + return KNOT_EINVAL; + } + + return cmd_table[cmd].fcn(args, cmd); +} + +bool ctl_has_flag(const char *flags, const char *flag) +{ + if (flags == NULL || flag == NULL) { + return false; + } + + return strstr(flags, flag) != NULL; +} diff --git a/src/knot/ctl/commands.h b/src/knot/ctl/commands.h new file mode 100644 index 0000000..ab7984e --- /dev/null +++ b/src/knot/ctl/commands.h @@ -0,0 +1,160 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/libknot.h" +#include "knot/server/server.h" + +#define CTL_FLAG_FORCE "F" +#define CTL_FLAG_BLOCKING "B" + +#define CTL_FLAG_DIFF_ADD "+" +#define CTL_FLAG_DIFF_REM "-" + +#define CTL_FLAG_LIST_SCHEMA "s" +#define CTL_FLAG_LIST_TXN "t" +#define CTL_FLAG_LIST_ZONES "z" + +#define CTL_FLAG_STATUS_EMPTY "e" +#define CTL_FLAG_STATUS_SLAVE "s" +#define CTL_FLAG_STATUS_MEMBER "m" + +#define CTL_FILTER_FLUSH_OUTDIR 'd' + +#define CTL_FILTER_STATUS_ROLE 'r' +#define CTL_FILTER_STATUS_SERIAL 's' +#define CTL_FILTER_STATUS_TRANSACTION 't' +#define CTL_FILTER_STATUS_FREEZE 'f' +#define CTL_FILTER_STATUS_CATALOG 'c' +#define CTL_FILTER_STATUS_EVENTS 'e' + +#define CTL_FILTER_PURGE_EXPIRE 'e' +#define CTL_FILTER_PURGE_ZONEFILE 'f' +#define CTL_FILTER_PURGE_JOURNAL 'j' +#define CTL_FILTER_PURGE_TIMERS 't' +#define CTL_FILTER_PURGE_KASPDB 'k' +#define CTL_FILTER_PURGE_CATALOG 'c' +#define CTL_FILTER_PURGE_ORPHAN 'o' + +#define CTL_FILTER_BACKUP_OUTDIR 'd' +#define CTL_FILTER_BACKUP_ZONEFILE 'z' +#define CTL_FILTER_BACKUP_NOZONEFILE 'Z' +#define CTL_FILTER_BACKUP_JOURNAL 'j' +#define CTL_FILTER_BACKUP_NOJOURNAL 'J' +#define CTL_FILTER_BACKUP_TIMERS 't' +#define CTL_FILTER_BACKUP_NOTIMERS 'T' +#define CTL_FILTER_BACKUP_KASPDB 'k' +#define CTL_FILTER_BACKUP_NOKASPDB 'K' +#define CTL_FILTER_BACKUP_CATALOG 'c' +#define CTL_FILTER_BACKUP_NOCATALOG 'C' + +#define STATUS_EMPTY "-" + +/*! Control commands. */ +typedef enum { + CTL_NONE, + + CTL_STATUS, + CTL_STOP, + CTL_RELOAD, + CTL_STATS, + + CTL_ZONE_STATUS, + CTL_ZONE_RELOAD, + CTL_ZONE_REFRESH, + CTL_ZONE_RETRANSFER, + CTL_ZONE_NOTIFY, + CTL_ZONE_FLUSH, + CTL_ZONE_BACKUP, + CTL_ZONE_RESTORE, + CTL_ZONE_SIGN, + CTL_ZONE_KEYS_LOAD, + CTL_ZONE_KEY_ROLL, + CTL_ZONE_KSK_SBM, + CTL_ZONE_FREEZE, + CTL_ZONE_THAW, + CTL_ZONE_XFR_FREEZE, + CTL_ZONE_XFR_THAW, + + CTL_ZONE_READ, + CTL_ZONE_BEGIN, + CTL_ZONE_COMMIT, + CTL_ZONE_ABORT, + CTL_ZONE_DIFF, + CTL_ZONE_GET, + CTL_ZONE_SET, + CTL_ZONE_UNSET, + CTL_ZONE_PURGE, + CTL_ZONE_STATS, + + CTL_CONF_LIST, + CTL_CONF_READ, + CTL_CONF_BEGIN, + CTL_CONF_COMMIT, + CTL_CONF_ABORT, + CTL_CONF_DIFF, + CTL_CONF_GET, + CTL_CONF_SET, + CTL_CONF_UNSET, +} ctl_cmd_t; + +/*! Control command parameters. */ +typedef struct { + knot_ctl_t *ctl; + knot_ctl_type_t type; + knot_ctl_data_t data; + server_t *server; + bool suppress; // Suppress error reporting in the "all zones" ctl commands. +} ctl_args_t; + +/*! + * Returns a string equivalent of the command. + * + * \param[in] cmd Command. + * + * \return Command string or NULL. + */ +const char *ctl_cmd_to_str(ctl_cmd_t cmd); + +/*! + * Returns a command corresponding to the string. + * + * \param[in] cmd_str Command string. + * + * \return Command. + */ +ctl_cmd_t ctl_str_to_cmd(const char *cmd_str); + +/*! + * Executes a control command. + * + * \param[in] cmd Control command. + * \param[in] args Command arguments. + * + * \return Error code, KNOT_EOK if successful. + */ +int ctl_exec(ctl_cmd_t cmd, ctl_args_t *args); + +/*! + * Checks flag presence in flags. + * + * \param[in] flags Flags to check presence in. + * \param[in] flag Checked flag. + * + * \return True if presented. + */ +bool ctl_has_flag(const char *flags, const char *flag); diff --git a/src/knot/ctl/process.c b/src/knot/ctl/process.c new file mode 100644 index 0000000..50fde21 --- /dev/null +++ b/src/knot/ctl/process.c @@ -0,0 +1,128 @@ +/* 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/common/log.h" +#include "knot/ctl/commands.h" +#include "knot/ctl/process.h" +#include "libknot/error.h" +#include "contrib/string.h" + +int ctl_process(knot_ctl_t *ctl, server_t *server) +{ + if (ctl == NULL || server == NULL) { + return KNOT_EINVAL; + } + + ctl_args_t args = { + .ctl = ctl, + .type = KNOT_CTL_TYPE_END, + .server = server + }; + + // Strip redundant/unprocessed data units in the current block. + bool strip = false; + + while (true) { + // Receive data unit. + int ret = knot_ctl_receive(args.ctl, &args.type, &args.data); + if (ret != KNOT_EOK) { + log_ctl_debug("control, failed to receive (%s)", + knot_strerror(ret)); + return ret; + } + + // Decide what to do. + switch (args.type) { + case KNOT_CTL_TYPE_DATA: + // Leading data unit with a command name. + if (!strip) { + // Set to strip unprocessed data unit. + strip = true; + break; + } + // FALLTHROUGH + case KNOT_CTL_TYPE_EXTRA: + // All non-first data units should be parsed in a callback. + // Ignore if probable previous error. + continue; + case KNOT_CTL_TYPE_BLOCK: + strip = false; + continue; + case KNOT_CTL_TYPE_END: + return KNOT_EOF; + default: + assert(0); + } + + strtolower((char *)args.data[KNOT_CTL_IDX_ZONE]); + + const char *cmd_name = args.data[KNOT_CTL_IDX_CMD]; + const char *zone_name = args.data[KNOT_CTL_IDX_ZONE]; + + ctl_cmd_t cmd = ctl_str_to_cmd(cmd_name); + if (cmd == CTL_CONF_LIST) { + log_ctl_debug("control, received command '%s'", cmd_name); + } else if (cmd != CTL_NONE) { + if (zone_name != NULL) { + log_ctl_zone_str_info(zone_name, + "control, received command '%s'", cmd_name); + } else { + log_ctl_info("control, received command '%s'", cmd_name); + } + } else if (cmd_name != NULL){ + log_ctl_debug("control, invalid command '%s'", cmd_name); + continue; + } else { + log_ctl_debug("control, empty command"); + continue; + } + + // Execute the command. + int cmd_ret = ctl_exec(cmd, &args); + switch (cmd_ret) { + case KNOT_EOK: + strip = false; + case KNOT_CTL_ESTOP: + case KNOT_CTL_EZONE: + // KNOT_CTL_EZONE - don't change strip, but don't be reported + // as a ctl/communication error either. + break; + default: + log_ctl_debug("control, command '%s' (%s)", cmd_name, + knot_strerror(cmd_ret)); + break; + } + + // Finalize the answer block. + ret = knot_ctl_send(ctl, KNOT_CTL_TYPE_BLOCK, NULL); + if (ret != KNOT_EOK) { + log_ctl_debug("control, failed to reply (%s)", + knot_strerror(ret)); + } + + // Stop if required. + if (cmd_ret == KNOT_CTL_ESTOP) { + // Finalize the answer message. + ret = knot_ctl_send(ctl, KNOT_CTL_TYPE_END, NULL); + if (ret != KNOT_EOK) { + log_ctl_debug("control, failed to reply (%s)", + knot_strerror(ret)); + } + + return cmd_ret; + } + } +} diff --git a/src/knot/ctl/process.h b/src/knot/ctl/process.h new file mode 100644 index 0000000..ab0f75f --- /dev/null +++ b/src/knot/ctl/process.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/libknot.h" +#include "knot/server/server.h" + +/*! + * Processes incoming control commands. + * + * \param[in] ctl Control context. + * \param[in] server Server instance. + * + * \return Error code, KNOT_EOK if successful. + */ +int ctl_process(knot_ctl_t *ctl, server_t *server); diff --git a/src/knot/dnssec/context.c b/src/knot/dnssec/context.c new file mode 100644 index 0000000..f2a3685 --- /dev/null +++ b/src/knot/dnssec/context.c @@ -0,0 +1,351 @@ +/* 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 <string.h> + +#include "contrib/macros.h" +#include "contrib/time.h" +#include "libknot/libknot.h" +#include "knot/dnssec/context.h" +#include "knot/dnssec/kasp/keystore.h" +#include "knot/dnssec/key_records.h" +#include "knot/server/dthreads.h" + +knot_dynarray_define(parent, knot_kasp_parent_t, DYNARRAY_VISIBILITY_NORMAL) + +static void policy_load(knot_kasp_policy_t *policy, conf_t *conf, conf_val_t *id, + const knot_dname_t *zone_name) +{ + if (conf_str(id) == NULL) { + policy->string = strdup("default"); + } else { + policy->string = strdup(conf_str(id)); + } + + conf_val_t val = conf_id_get(conf, C_POLICY, C_MANUAL, id); + policy->manual = conf_bool(&val); + + val = conf_id_get(conf, C_POLICY, C_SINGLE_TYPE_SIGNING, id); + policy->single_type_signing = conf_bool(&val); + policy->sts_default = (val.code != KNOT_EOK); + + val = conf_id_get(conf, C_POLICY, C_ALG, id); + policy->algorithm = conf_opt(&val); + + val = conf_id_get(conf, C_POLICY, C_KSK_SHARED, id); + policy->ksk_shared = conf_bool(&val); + + val = conf_id_get(conf, C_POLICY, C_KSK_SIZE, id); + int64_t num = conf_int(&val); + policy->ksk_size = (num != YP_NIL) ? num : + dnssec_algorithm_key_size_default(policy->algorithm); + + val = conf_id_get(conf, C_POLICY, C_ZSK_SIZE, id); + num = conf_int(&val); + policy->zsk_size = (num != YP_NIL) ? num : + dnssec_algorithm_key_size_default(policy->algorithm); + + val = conf_id_get(conf, C_POLICY, C_DNSKEY_TTL, id); + int64_t ttl = conf_int(&val); + policy->dnskey_ttl = (ttl != YP_NIL) ? ttl : UINT32_MAX; + + val = conf_id_get(conf, C_POLICY, C_ZONE_MAX_TTL, id); + ttl = conf_int(&val); + policy->zone_maximal_ttl = (ttl != YP_NIL) ? ttl : UINT32_MAX; + + val = conf_id_get(conf, C_POLICY, C_ZSK_LIFETIME, id); + policy->zsk_lifetime = conf_int(&val); + + val = conf_id_get(conf, C_POLICY, C_KSK_LIFETIME, id); + policy->ksk_lifetime = conf_int(&val); + + val = conf_id_get(conf, C_POLICY, C_DELETE_DELAY, id); + policy->delete_delay = conf_int(&val); + + val = conf_id_get(conf, C_POLICY, C_PROPAG_DELAY, id); + policy->propagation_delay = conf_int(&val); + + val = conf_id_get(conf, C_POLICY, C_RRSIG_LIFETIME, id); + policy->rrsig_lifetime = conf_int(&val); + + val = conf_id_get(conf, C_POLICY, C_RRSIG_REFRESH, id); + num = conf_int(&val); + policy->rrsig_refresh_before = (num != YP_NIL) ? num : UINT32_MAX; + if (policy->rrsig_refresh_before == UINT32_MAX && policy->zone_maximal_ttl != UINT32_MAX) { + policy->rrsig_refresh_before = policy->propagation_delay + policy->zone_maximal_ttl; + } + + val = conf_id_get(conf, C_POLICY, C_RRSIG_PREREFRESH, id); + policy->rrsig_prerefresh = conf_int(&val); + + val = conf_id_get(conf, C_POLICY, C_REPRO_SIGNING, id); + policy->reproducible_sign = conf_bool(&val); + + val = conf_id_get(conf, C_POLICY, C_NSEC3, id); + policy->nsec3_enabled = conf_bool(&val); + + val = conf_id_get(conf, C_POLICY, C_NSEC3_OPT_OUT, id); + policy->nsec3_opt_out = conf_bool(&val); + + val = conf_id_get(conf, C_POLICY, C_NSEC3_ITER, id); + policy->nsec3_iterations = conf_int(&val); + + val = conf_id_get(conf, C_POLICY, C_NSEC3_SALT_LEN, id); + policy->nsec3_salt_length = conf_int(&val); + + val = conf_id_get(conf, C_POLICY, C_NSEC3_SALT_LIFETIME, id); + policy->nsec3_salt_lifetime = conf_int(&val); + + val = conf_id_get(conf, C_POLICY, C_CDS_CDNSKEY, id); + policy->cds_cdnskey_publish = conf_opt(&val); + + val = conf_id_get(conf, C_POLICY, C_CDS_DIGESTTYPE, id); + policy->cds_dt = conf_opt(&val); + + val = conf_id_get(conf, C_POLICY, C_DNSKEY_MGMT, id); + policy->incremental = (conf_opt(&val) == DNSKEY_MGMT_INCREMENTAL); + + conf_val_t ksk_sbm = conf_id_get(conf, C_POLICY, C_KSK_SBM, id); + if (ksk_sbm.code == KNOT_EOK) { + val = conf_id_get(conf, C_SBM, C_CHK_INTERVAL, &ksk_sbm); + policy->ksk_sbm_check_interval = conf_int(&val); + + val = conf_id_get(conf, C_SBM, C_TIMEOUT, &ksk_sbm); + policy->ksk_sbm_timeout = conf_int(&val); + + val = conf_id_get(conf, C_SBM, C_PARENT, &ksk_sbm); + conf_mix_iter_t iter; + conf_mix_iter_init(conf, &val, &iter); + while (iter.id->code == KNOT_EOK) { + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, iter.id); + knot_kasp_parent_t p = { .addrs = conf_val_count(&addr) }; + p.addr = p.addrs ? malloc(p.addrs * sizeof(*p.addr)) : NULL; + if (p.addr != NULL) { + for (size_t i = 0; i < p.addrs; i++) { + p.addr[i] = conf_remote(conf, iter.id, i); + } + parent_dynarray_add(&policy->parents, &p); + } + conf_mix_iter_next(&iter); + } + + val = conf_id_get(conf, C_SBM, C_PARENT_DELAY, &ksk_sbm); + policy->ksk_sbm_delay = conf_int(&val); + } + + val = conf_id_get(conf, C_POLICY, C_SIGNING_THREADS, id); + policy->signing_threads = conf_int(&val); + + val = conf_zone_get(conf, C_DS_PUSH, zone_name); + if (val.code != KNOT_EOK) { + val = conf_id_get(conf, C_POLICY, C_DS_PUSH, id); + } + policy->ds_push = conf_val_count(&val) > 0; + + val = conf_id_get(conf, C_POLICY, C_OFFLINE_KSK, id); + policy->offline_ksk = conf_bool(&val); + + policy->unsafe = 0; + val = conf_id_get(conf, C_POLICY, C_UNSAFE_OPERATION, id); + while (val.code == KNOT_EOK) { + policy->unsafe |= conf_opt(&val); + conf_val_next(&val); + } +} + +int kdnssec_ctx_init(conf_t *conf, kdnssec_ctx_t *ctx, const knot_dname_t *zone_name, + knot_lmdb_db_t *kaspdb, const conf_mod_id_t *from_module) +{ + if (ctx == NULL || zone_name == NULL) { + return KNOT_EINVAL; + } + + int ret; + + memset(ctx, 0, sizeof(*ctx)); + + ctx->zone = calloc(1, sizeof(*ctx->zone)); + if (ctx->zone == NULL) { + ret = KNOT_ENOMEM; + goto init_error; + } + + ctx->kasp_db = kaspdb; + ret = knot_lmdb_open(ctx->kasp_db); + if (ret != KNOT_EOK) { + goto init_error; + } + + ret = kasp_zone_load(ctx->zone, zone_name, ctx->kasp_db, + &ctx->keytag_conflict); + if (ret != KNOT_EOK) { + goto init_error; + } + + ctx->kasp_zone_path = conf_db(conf, C_KASP_DB); + if (ctx->kasp_zone_path == NULL) { + ret = KNOT_ENOMEM; + goto init_error; + } + + ctx->policy = calloc(1, sizeof(*ctx->policy)); + if (ctx->policy == NULL) { + ret = KNOT_ENOMEM; + goto init_error; + } + + ret = kasp_db_get_saved_ttls(ctx->kasp_db, zone_name, + &ctx->policy->saved_max_ttl, + &ctx->policy->saved_key_ttl); + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + return ret; + } + + conf_val_t policy_id; + if (from_module == NULL) { + policy_id = conf_zone_get(conf, C_DNSSEC_POLICY, zone_name); + } else { + policy_id = conf_mod_get(conf, C_POLICY, from_module); + } + conf_id_fix_default(&policy_id); + policy_load(ctx->policy, conf, &policy_id, ctx->zone->dname); + + ret = zone_init_keystore(conf, &policy_id, &ctx->keystore, NULL, + &ctx->policy->key_label); + if (ret != KNOT_EOK) { + goto init_error; + } + + ctx->dbus_event = conf->cache.srv_dbus_event; + + ctx->now = knot_time(); + + key_records_init(ctx, &ctx->offline_records); + if (ctx->policy->offline_ksk) { + ret = kasp_db_load_offline_records(ctx->kasp_db, ctx->zone->dname, + &ctx->now, &ctx->offline_next_time, + &ctx->offline_records); + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + goto init_error; + } + } + + return KNOT_EOK; +init_error: + kdnssec_ctx_deinit(ctx); + return ret; +} + +int kdnssec_ctx_commit(kdnssec_ctx_t *ctx) +{ + if (ctx == NULL || ctx->kasp_zone_path == NULL) { + return KNOT_EINVAL; + } + + if (ctx->policy->dnskey_ttl != UINT32_MAX && + ctx->policy->zone_maximal_ttl != UINT32_MAX) { + int ret = kasp_db_set_saved_ttls(ctx->kasp_db, ctx->zone->dname, + ctx->policy->zone_maximal_ttl, + ctx->policy->dnskey_ttl); + if (ret != KNOT_EOK) { + return ret; + } + } + + return kasp_zone_save(ctx->zone, ctx->zone->dname, ctx->kasp_db); +} + +void kdnssec_ctx_deinit(kdnssec_ctx_t *ctx) +{ + if (ctx == NULL) { + return; + } + + if (ctx->policy != NULL) { + free(ctx->policy->string); + knot_dynarray_foreach(parent, knot_kasp_parent_t, i, ctx->policy->parents) { + free(i->addr); + } + free(ctx->policy); + } + key_records_clear(&ctx->offline_records); + dnssec_keystore_deinit(ctx->keystore); + kasp_zone_free(&ctx->zone); + free(ctx->kasp_zone_path); + + memset(ctx, 0, sizeof(*ctx)); +} + +// expects policy struct to be zeroed +static void policy_from_zone(knot_kasp_policy_t *policy, const zone_contents_t *zone) +{ + knot_rdataset_t *dnskey = node_rdataset(zone->apex, KNOT_RRTYPE_DNSKEY); + knot_rdataset_t *n3p = node_rdataset(zone->apex, KNOT_RRTYPE_NSEC3PARAM); + + policy->manual = true; + policy->single_type_signing = (dnskey != NULL && dnskey->count == 1); + + if (n3p != NULL) { + policy->nsec3_enabled = true; + policy->nsec3_iterations = knot_nsec3param_iters(n3p->rdata); + policy->nsec3_salt_length = knot_nsec3param_salt_len(n3p->rdata); + } + policy->signing_threads = 1; +} + +int kdnssec_validation_ctx(conf_t *conf, kdnssec_ctx_t *ctx, const zone_contents_t *zone) +{ + if (ctx == NULL || zone == NULL) { + return KNOT_EINVAL; + } + + memset(ctx, 0, sizeof(*ctx)); + + ctx->zone = calloc(1, sizeof(*ctx->zone)); + if (ctx->zone == NULL) { + return KNOT_ENOMEM; + } + + ctx->policy = calloc(1, sizeof(*ctx->policy)); + if (ctx->policy == NULL) { + free(ctx->zone); + return KNOT_ENOMEM; + } + + policy_from_zone(ctx->policy, zone); + if (conf != NULL) { + conf_val_t policy_id = conf_zone_get(conf, C_DNSSEC_POLICY, zone->apex->owner); + conf_id_fix_default(&policy_id); + conf_val_t num_threads = conf_id_get(conf, C_POLICY, C_SIGNING_THREADS, &policy_id); + ctx->policy->signing_threads = conf_int(&num_threads); + } else { + ctx->policy->signing_threads = MAX(dt_optimal_size(), 1); + } + + int ret = kasp_zone_from_contents(ctx->zone, zone, ctx->policy->single_type_signing, + ctx->policy->nsec3_enabled, &ctx->policy->nsec3_iterations, + &ctx->keytag_conflict); + if (ret != KNOT_EOK) { + memset(ctx->zone, 0, sizeof(*ctx->zone)); + kdnssec_ctx_deinit(ctx); + return ret; + } + + ctx->now = knot_time(); + ctx->validation_mode = true; + return KNOT_EOK; +} diff --git a/src/knot/dnssec/context.h b/src/knot/dnssec/context.h new file mode 100644 index 0000000..55a2f3c --- /dev/null +++ b/src/knot/dnssec/context.h @@ -0,0 +1,82 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <time.h> + +#include "libdnssec/keystore.h" + +#include "knot/conf/conf.h" +#include "knot/dnssec/kasp/kasp_zone.h" +#include "knot/dnssec/kasp/policy.h" + +/*! + * \brief DNSSEC signing context. + */ +typedef struct { + knot_time_t now; + + knot_lmdb_db_t *kasp_db; + knot_kasp_zone_t *zone; + knot_kasp_policy_t *policy; + dnssec_keystore_t *keystore; + + char *kasp_zone_path; + + bool rrsig_drop_existing; + bool keep_deleted_keys; + bool keytag_conflict; + bool validation_mode; + + unsigned dbus_event; + + key_records_t offline_records; + knot_time_t offline_next_time; +} kdnssec_ctx_t; + +/*! + * \brief Initialize DNSSEC signing context. + * + * \param conf Configuration. + * \param ctx Signing context to be initialized. + * \param zone_name Name of the zone. + * \param kaspdb Key and signature policy database. + * \param from_module Module identifier if initialized from a module. + */ +int kdnssec_ctx_init(conf_t *conf, kdnssec_ctx_t *ctx, const knot_dname_t *zone_name, + knot_lmdb_db_t *kaspdb, const conf_mod_id_t *from_module); + +/*! + * \brief Initialize DNSSEC validating context. + * + * \param conf Configuration. + * \param ctx Signing context to be initialized. + * \param zone Zone contents to be validated. + * + * \return KNOT_E* + */ +int kdnssec_validation_ctx(conf_t *conf, kdnssec_ctx_t *ctx, const zone_contents_t *zone); + +/*! + * \brief Save the changes in ctx (in kasp zone). + */ +int kdnssec_ctx_commit(kdnssec_ctx_t *ctx); + +/*! + * \brief Cleanup DNSSEC signing context. + */ +void kdnssec_ctx_deinit(kdnssec_ctx_t *ctx); diff --git a/src/knot/dnssec/ds_query.c b/src/knot/dnssec/ds_query.c new file mode 100644 index 0000000..918ae5d --- /dev/null +++ b/src/knot/dnssec/ds_query.c @@ -0,0 +1,288 @@ +/* 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 "contrib/macros.h" +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/dnssec/ds_query.h" +#include "knot/dnssec/key-events.h" +#include "knot/query/layer.h" +#include "knot/query/query.h" +#include "knot/query/requestor.h" + +static bool match_key_ds(knot_kasp_key_t *key, knot_rdata_t *ds) +{ + assert(key); + assert(ds); + + dnssec_binary_t ds_rdata = { + .size = ds->len, + .data = ds->data, + }; + + dnssec_binary_t cds_rdata = { 0 }; + + int ret = dnssec_key_create_ds(key->key, knot_ds_digest_type(ds), &cds_rdata); + if (ret != KNOT_EOK) { + return false; + } + + ret = (dnssec_binary_cmp(&cds_rdata, &ds_rdata) == 0); + dnssec_binary_free(&cds_rdata); + return ret; +} + +static bool match_key_ds_rrset(knot_kasp_key_t *key, const knot_rrset_t *rr) +{ + if (key == NULL) { + return false; + } + knot_rdata_t *rd = rr->rrs.rdata; + for (int i = 0; i < rr->rrs.count; i++) { + if (match_key_ds(key, rd)) { + return true; + } + rd = knot_rdataset_next(rd); + } + return false; +} + +struct ds_query_data { + conf_t *conf; + + const knot_dname_t *zone_name; + const struct sockaddr *remote; + + knot_kasp_key_t *key; + knot_kasp_key_t *not_key; + + query_edns_data_t edns; + + bool ds_ok; + bool result_logged; + + uint32_t ttl; +}; + +static int ds_query_begin(knot_layer_t *layer, void *params) +{ + layer->data = params; + + return KNOT_STATE_PRODUCE; +} + +static int ds_query_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct ds_query_data *data = layer->data; + + query_init_pkt(pkt); + + int r = knot_pkt_put_question(pkt, data->zone_name, KNOT_CLASS_IN, KNOT_RRTYPE_DS); + if (r != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + r = query_put_edns(pkt, &data->edns); + if (r != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + knot_wire_set_rd(pkt->wire); + + return KNOT_STATE_CONSUME; +} + +static int ds_query_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct ds_query_data *data = layer->data; + data->result_logged = true; + + uint16_t rcode = knot_pkt_ext_rcode(pkt); + if (rcode != KNOT_RCODE_NOERROR) { + ns_log((rcode == KNOT_RCODE_NXDOMAIN ? LOG_NOTICE : LOG_WARNING), + data->zone_name, LOG_OPERATION_DS_CHECK, + LOG_DIRECTION_OUT, data->remote, + layer->flags & KNOT_REQUESTOR_REUSED, + "failed (%s)", knot_pkt_ext_rcode_name(pkt)); + return KNOT_STATE_FAIL; + } + + const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER); + + bool match = false, match_not = false; + + for (size_t j = 0; j < answer->count; j++) { + const knot_rrset_t *rr = knot_pkt_rr(answer, j); + switch ((rr && rr->rrs.count > 0) ? rr->type : 0) { + case KNOT_RRTYPE_DS: + if (match_key_ds_rrset(data->key, rr)) { + match = true; + if (data->ttl == 0) { // fallback: if there is no RRSIG + data->ttl = rr->ttl; + } + } + if (match_key_ds_rrset(data->not_key, rr)) { + match_not = true; + } + break; + case KNOT_RRTYPE_RRSIG: + data->ttl = knot_rrsig_original_ttl(rr->rrs.rdata); + break; + default: + break; + } + } + + if (match_not) { + match = false; + } + + ns_log(LOG_INFO, data->zone_name, LOG_OPERATION_DS_CHECK, + LOG_DIRECTION_OUT, data->remote, layer->flags & KNOT_REQUESTOR_REUSED, + "KSK submission check: %s", (match ? "positive" : "negative")); + + if (match) { + data->ds_ok = true; + } + return KNOT_STATE_DONE; +} + +static const knot_layer_api_t ds_query_api = { + .begin = ds_query_begin, + .produce = ds_query_produce, + .consume = ds_query_consume, + .reset = NULL, + .finish = NULL, +}; + +static int try_ds(conf_t *conf, const knot_dname_t *zone_name, const conf_remote_t *parent, + knot_kasp_key_t *key, knot_kasp_key_t *not_key, size_t timeout, uint32_t *ds_ttl) +{ + // TODO: Abstract interface to issue DNS queries. This is almost copy-pasted. + + assert(zone_name); + assert(parent); + + struct ds_query_data data = { + .zone_name = zone_name, + .remote = (struct sockaddr *)&parent->addr, + .key = key, + .not_key = not_key, + .edns = query_edns_data_init(conf, parent->addr.ss_family, + QUERY_EDNS_OPT_DO), + .ds_ok = false, + .result_logged = false, + .ttl = 0, + }; + + knot_requestor_t requestor; + knot_requestor_init(&requestor, &ds_query_api, &data, NULL); + + knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL); + if (!pkt) { + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + const struct sockaddr_storage *dst = &parent->addr; + const struct sockaddr_storage *src = &parent->via; + knot_request_t *req = knot_request_make(NULL, dst, src, pkt, &parent->key, 0); + if (!req) { + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + int ret = knot_requestor_exec(&requestor, req, timeout); + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + + // alternative: we could put answer back through ctx instead of errcode + if (ret == KNOT_EOK && !data.ds_ok) { + ret = KNOT_ENORECORD; + } + + if (ret != KNOT_EOK && !data.result_logged) { + ns_log(LOG_WARNING, zone_name, LOG_OPERATION_DS_CHECK, + LOG_DIRECTION_OUT, data.remote, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "failed (%s)", knot_strerror(ret)); + } + + *ds_ttl = data.ttl; + + return ret; +} + +static knot_kasp_key_t *get_not_key(kdnssec_ctx_t *kctx, knot_kasp_key_t *key) +{ + knot_kasp_key_t *not_key = knot_dnssec_key2retire(kctx, key); + + if (not_key == NULL || dnssec_key_get_algorithm(not_key->key) == dnssec_key_get_algorithm(key->key)) { + return NULL; + } + + return not_key; +} + +static bool parents_have_ds(conf_t *conf, kdnssec_ctx_t *kctx, knot_kasp_key_t *key, + size_t timeout, uint32_t *max_ds_ttl) +{ + bool success = false; + knot_dynarray_foreach(parent, knot_kasp_parent_t, i, kctx->policy->parents) { + success = false; + for (size_t j = 0; j < i->addrs; j++) { + uint32_t ds_ttl = 0; + int ret = try_ds(conf, kctx->zone->dname, &i->addr[j], key, + get_not_key(kctx, key), timeout, &ds_ttl); + if (ret == KNOT_EOK) { + *max_ds_ttl = MAX(*max_ds_ttl, ds_ttl); + success = true; + break; + } else if (ret == KNOT_ENORECORD) { + // parent was queried successfully, answer was negative + break; + } + } + // Each parent must succeed. + if (!success) { + return false; + } + } + return success; +} + +int knot_parent_ds_query(conf_t *conf, kdnssec_ctx_t *kctx, size_t timeout) +{ + uint32_t max_ds_ttl = 0; + + for (size_t i = 0; i < kctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &kctx->zone->keys[i]; + if (!key->is_pub_only && + knot_time_cmp(key->timing.ready, kctx->now) <= 0 && + knot_time_cmp(key->timing.active, kctx->now) > 0) { + assert(key->is_ksk); + if (parents_have_ds(conf, kctx, key, timeout, &max_ds_ttl)) { + return knot_dnssec_ksk_sbm_confirm(kctx, max_ds_ttl + kctx->policy->ksk_sbm_delay); + } else { + return KNOT_ENOENT; + } + } + } + return KNOT_NO_READY_KEY; +} diff --git a/src/knot/dnssec/ds_query.h b/src/knot/dnssec/ds_query.h new file mode 100644 index 0000000..1144d21 --- /dev/null +++ b/src/knot/dnssec/ds_query.h @@ -0,0 +1,22 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/dnssec/zone-keys.h" +#include "knot/dnssec/context.h" + +int knot_parent_ds_query(conf_t *conf, kdnssec_ctx_t *kctx, size_t timeout); diff --git a/src/knot/dnssec/kasp/kasp_db.c b/src/knot/dnssec/kasp/kasp_db.c new file mode 100644 index 0000000..29c6a7d --- /dev/null +++ b/src/knot/dnssec/kasp/kasp_db.c @@ -0,0 +1,610 @@ +/* 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/dnssec/kasp/kasp_db.h" + +#include <inttypes.h> +#include <stdio.h> + +#include "contrib/strtonum.h" +#include "contrib/wire_ctx.h" +#include "knot/dnssec/key_records.h" + +typedef enum { + KASPDBKEY_PARAMS = 0x1, + KASPDBKEY_POLICYLAST = 0x2, + KASPDBKEY_NSEC3SALT = 0x3, + KASPDBKEY_NSEC3TIME = 0x4, + KASPDBKEY_MASTERSERIAL = 0x5, + KASPDBKEY_LASTSIGNEDSERIAL = 0x6, + KASPDBKEY_OFFLINE_RECORDS = 0x7, + KASPDBKEY_SAVED_TTLS = 0x8, +} keyclass_t; + +static const keyclass_t zone_related_classes[] = { + KASPDBKEY_PARAMS, + KASPDBKEY_NSEC3SALT, + KASPDBKEY_NSEC3TIME, + KASPDBKEY_MASTERSERIAL, + KASPDBKEY_LASTSIGNEDSERIAL, + KASPDBKEY_OFFLINE_RECORDS, + KASPDBKEY_SAVED_TTLS, +}; +static const size_t zone_related_classes_size = sizeof(zone_related_classes) / sizeof(*zone_related_classes); + +static bool is_zone_related_class(uint8_t class) +{ + for (size_t i = 0; i < zone_related_classes_size; i++) { + if (zone_related_classes[i] == class) { + return true; + } + } + return false; +} + +static bool is_zone_related(const MDB_val *key) +{ + return is_zone_related_class(*(uint8_t *)key->mv_data); +} + +static MDB_val make_key_str(keyclass_t kclass, const knot_dname_t *dname, const char *str) +{ + switch (kclass) { + case KASPDBKEY_POLICYLAST: + assert(dname == NULL && str != NULL); + return knot_lmdb_make_key("BS", (int)kclass, str); + case KASPDBKEY_NSEC3SALT: + case KASPDBKEY_NSEC3TIME: + case KASPDBKEY_LASTSIGNEDSERIAL: + case KASPDBKEY_MASTERSERIAL: + case KASPDBKEY_SAVED_TTLS: + assert(dname != NULL && str == NULL); + return knot_lmdb_make_key("BN", (int)kclass, dname); + case KASPDBKEY_PARAMS: + case KASPDBKEY_OFFLINE_RECORDS: + assert(dname != NULL); + if (str == NULL) { + return knot_lmdb_make_key("BN", (int)kclass, dname); + } else { + return knot_lmdb_make_key("BNS", (int)kclass, dname, str); + } + default: + assert(0); + MDB_val empty = { 0 }; + return empty; + } +} + +static MDB_val make_key_time(keyclass_t kclass, const knot_dname_t *dname, knot_time_t time) +{ + char tmp[21]; + (void)snprintf(tmp, sizeof(tmp), "%0*"PRIu64, (int)(sizeof(tmp) - 1), time); + return make_key_str(kclass, dname, tmp); +} + +static bool unmake_key_str(const MDB_val *keyv, char **str) +{ + uint8_t kclass; + const knot_dname_t *dname; + const char *s; + return (knot_lmdb_unmake_key(keyv->mv_data, keyv->mv_size, "BNS", &kclass, &dname, &s) && + ((*str = strdup(s)) != NULL)); +} + +static bool unmake_key_time(const MDB_val *keyv, knot_time_t *time) +{ + uint8_t kclass; + const knot_dname_t *dname; + const char *s; + return (knot_lmdb_unmake_key(keyv->mv_data, keyv->mv_size, "BNS", &kclass, &dname, &s) && + str_to_u64(s, time) == KNOT_EOK); +} + +static MDB_val params_serialize(const key_params_t *params) +{ + uint8_t flags = 0x02; + flags |= (params->is_ksk ? 0x01 : 0); + flags |= (params->is_pub_only ? 0x04 : 0); + flags |= (params->is_csk ? 0x08 : 0); + + return knot_lmdb_make_key("LLHBBLLLLLLLLLDL", (uint64_t)params->public_key.size, + (uint64_t)sizeof(params->timing.revoke), params->keytag, params->algorithm, flags, + params->timing.created, params->timing.pre_active, params->timing.publish, + params->timing.ready, params->timing.active, params->timing.retire_active, + params->timing.retire, params->timing.post_active, params->timing.remove, + params->public_key.data, params->public_key.size, params->timing.revoke); +} + +// this is no longer compatible with keys created by Knot 2.5.x (and unmodified since) +static bool params_deserialize(const MDB_val *val, key_params_t *params) +{ + if (val->mv_size < 2 * sizeof(uint64_t)) { + return false; + } + uint64_t keylen = knot_wire_read_u64(val->mv_data); + uint64_t future = knot_wire_read_u64(val->mv_data + sizeof(keylen)); + uint8_t flags; + + if ((params->public_key.data = malloc(keylen)) == NULL) { + return false; + } + + if (knot_lmdb_unmake_key(val->mv_data, val->mv_size - future, "LLHBBLLLLLLLLLD", + &keylen, &future, ¶ms->keytag, ¶ms->algorithm, &flags, + ¶ms->timing.created, ¶ms->timing.pre_active, ¶ms->timing.publish, + ¶ms->timing.ready, ¶ms->timing.active, ¶ms->timing.retire_active, + ¶ms->timing.retire, ¶ms->timing.post_active, ¶ms->timing.remove, + params->public_key.data, (size_t)keylen)) { + + params->public_key.size = keylen; + params->is_ksk = ((flags & 0x01) ? true : false); + params->is_pub_only = ((flags & 0x04) ? true : false); + params->is_csk = ((flags & 0x08) ? true : false); + + if (future > 0) { + if (future < sizeof(params->timing.revoke)) { + free(params->public_key.data); + params->public_key.data = NULL; + return false; + } + // 'revoked' timer is part of 'future' section since it was added later + params->timing.revoke = knot_wire_read_u64(val->mv_data + val->mv_size - future); + } + + if ((flags & 0x02) && (params->is_ksk || !params->is_csk)) { + return true; + } + } + free(params->public_key.data); + params->public_key.data = NULL; + return false; +} + +static key_params_t *txn2params(knot_lmdb_txn_t *txn) +{ + key_params_t *p = calloc(1, sizeof(*p)); + if (p == NULL) { + txn->ret = KNOT_ENOMEM; + } else { + if (!params_deserialize(&txn->cur_val, p) || + !unmake_key_str(&txn->cur_key, &p->id)) { + txn->ret = KNOT_EMALF; + free(p); + p = NULL; + } + } + return p; +} + +int kasp_db_list_keys(knot_lmdb_db_t *db, const knot_dname_t *zone_name, list_t *dst) +{ + init_list(dst); + knot_lmdb_txn_t txn = { 0 }; + MDB_val prefix = make_key_str(KASPDBKEY_PARAMS, zone_name, NULL); + knot_lmdb_begin(db, &txn, false); + knot_lmdb_foreach(&txn, &prefix) { + key_params_t *p = txn2params(&txn); + if (p != NULL) { + ptrlist_add(dst, p, NULL); + } + } + knot_lmdb_abort(&txn); + free(prefix.mv_data); + if (txn.ret != KNOT_EOK) { + ptrlist_deep_free(dst, NULL); + return txn.ret; + } + return (EMPTY_LIST(*dst) ? KNOT_ENOENT : KNOT_EOK); +} + +int kasp_db_get_key_algorithm(knot_lmdb_db_t *db, const knot_dname_t *zone_name, + const char *key_id) +{ + knot_lmdb_txn_t txn = { 0 }; + MDB_val search = make_key_str(KASPDBKEY_PARAMS, zone_name, key_id); + knot_lmdb_begin(db, &txn, false); + int ret = txn.ret == KNOT_EOK ? KNOT_ENOENT : txn.ret; + if (knot_lmdb_find(&txn, &search, KNOT_LMDB_EXACT)) { + key_params_t p = { 0 }; + ret = params_deserialize(&txn.cur_val, &p) ? p.algorithm : KNOT_EMALF; + free(p.public_key.data); + } + knot_lmdb_abort(&txn); + free(search.mv_data); + return ret; +} + +static bool keyid_inuse(knot_lmdb_txn_t *txn, const char *key_id, key_params_t **params) +{ + uint8_t pf = KASPDBKEY_PARAMS; + MDB_val prefix = { sizeof(pf), &pf }; + knot_lmdb_foreach(txn, &prefix) { + char *found_id = NULL; + if (unmake_key_str(&txn->cur_key, &found_id) && + strcmp(found_id, key_id) == 0) { + if (params != NULL) { + *params = txn2params(txn); + } + free(found_id); + return true; + } + free(found_id); + } + return false; +} + + +int kasp_db_delete_key(knot_lmdb_db_t *db, const knot_dname_t *zone_name, const char *key_id, bool *still_used) +{ + MDB_val search = make_key_str(KASPDBKEY_PARAMS, zone_name, key_id); + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + knot_lmdb_del_prefix(&txn, &search); + if (still_used != NULL) { + *still_used = keyid_inuse(&txn, key_id, NULL); + } + knot_lmdb_commit(&txn); + free(search.mv_data); + return txn.ret; +} + + +int kasp_db_delete_all(knot_lmdb_db_t *db, const knot_dname_t *zone) +{ + MDB_val prefix = make_key_str(KASPDBKEY_PARAMS, zone, NULL); + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + for (size_t i = 0; i < zone_related_classes_size && prefix.mv_data != NULL; i++) { + *(uint8_t *)prefix.mv_data = zone_related_classes[i]; + knot_lmdb_del_prefix(&txn, &prefix); + } + knot_lmdb_commit(&txn); + free(prefix.mv_data); + return txn.ret; +} + +int kasp_db_sweep(knot_lmdb_db_t *db, sweep_cb keep_zone, void *cb_data) +{ + if (knot_lmdb_exists(db) == KNOT_ENODB) { + return KNOT_EOK; + } + int ret = knot_lmdb_open(db); + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + knot_lmdb_forwhole(&txn) { + if (is_zone_related(&txn.cur_key) && + !keep_zone((const knot_dname_t *)txn.cur_key.mv_data + 1, cb_data)) { + knot_lmdb_del_cur(&txn); + } + } + knot_lmdb_commit(&txn); + return txn.ret; +} + +int kasp_db_list_zones(knot_lmdb_db_t *db, list_t *zones) +{ + if (knot_lmdb_exists(db) == KNOT_ENODB) { + return KNOT_EOK; + } + int ret = knot_lmdb_open(db); + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, false); + + uint8_t prefix_data = KASPDBKEY_PARAMS; + MDB_val prefix = { sizeof(prefix_data), &prefix_data }; + knot_lmdb_foreach(&txn, &prefix) { + const knot_dname_t *found = txn.cur_key.mv_data + sizeof(prefix_data); + if (!knot_dname_is_equal(found, ((ptrnode_t *)TAIL(*zones))->d)) { + knot_dname_t *copy = knot_dname_copy(found, NULL); + if (copy == NULL || ptrlist_add(zones, copy, NULL) == NULL) { + free(copy); + ptrlist_deep_free(zones, NULL); + return KNOT_ENOMEM; + } + } + } + knot_lmdb_abort(&txn); + if (txn.ret != KNOT_EOK) { + ptrlist_deep_free(zones, NULL); + } + return txn.ret; +} + +int kasp_db_add_key(knot_lmdb_db_t *db, const knot_dname_t *zone_name, const key_params_t *params) +{ + MDB_val v = params_serialize(params); + MDB_val k = make_key_str(KASPDBKEY_PARAMS, zone_name, params->id); + return knot_lmdb_quick_insert(db, k, v); +} + +int kasp_db_share_key(knot_lmdb_db_t *db, const knot_dname_t *zone_from, + const knot_dname_t *zone_to, const char *key_id) +{ + MDB_val from = make_key_str(KASPDBKEY_PARAMS, zone_from, key_id); + MDB_val to = make_key_str(KASPDBKEY_PARAMS, zone_to, key_id); + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + if (knot_lmdb_find(&txn, &from, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) { + knot_lmdb_insert(&txn, &to, &txn.cur_val); + } + knot_lmdb_commit(&txn); + free(from.mv_data); + free(to.mv_data); + return txn.ret; +} + +int kasp_db_store_nsec3salt(knot_lmdb_db_t *db, const knot_dname_t *zone_name, + const dnssec_binary_t *nsec3salt, knot_time_t salt_created) +{ + MDB_val key = make_key_str(KASPDBKEY_NSEC3SALT, zone_name, NULL); + MDB_val val1 = { nsec3salt->size, nsec3salt->data }; + uint64_t tmp = htobe64(salt_created); + MDB_val val2 = { sizeof(tmp), &tmp }; + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + knot_lmdb_insert(&txn, &key, &val1); + if (key.mv_data != NULL) { + *(uint8_t *)key.mv_data = KASPDBKEY_NSEC3TIME; + } + knot_lmdb_insert(&txn, &key, &val2); + knot_lmdb_commit(&txn); + free(key.mv_data); + return txn.ret; +} + +int kasp_db_load_nsec3salt(knot_lmdb_db_t *db, const knot_dname_t *zone_name, + dnssec_binary_t *nsec3salt, knot_time_t *salt_created) +{ + MDB_val key = make_key_str(KASPDBKEY_NSEC3SALT, zone_name, NULL); + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, false); + if (nsec3salt != NULL) { + memset(nsec3salt, 0, sizeof(*nsec3salt)); + if (knot_lmdb_find(&txn, &key, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) { + nsec3salt->size = txn.cur_val.mv_size; + nsec3salt->data = malloc(txn.cur_val.mv_size + 1); // +1 because it can be zero + if (nsec3salt->data == NULL) { + txn.ret = KNOT_ENOMEM; + } else { + memcpy(nsec3salt->data, txn.cur_val.mv_data, txn.cur_val.mv_size); + } + } + } + *(uint8_t *)key.mv_data = KASPDBKEY_NSEC3TIME; + if (knot_lmdb_find(&txn, &key, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) { + knot_lmdb_unmake_curval(&txn, "L", salt_created); + } + knot_lmdb_abort(&txn); + free(key.mv_data); + if (txn.ret != KNOT_EOK && nsec3salt != NULL) { + free(nsec3salt->data); + } + return txn.ret; +} + +int kasp_db_store_serial(knot_lmdb_db_t *db, const knot_dname_t *zone_name, + kaspdb_serial_t serial_type, uint32_t serial) +{ + int ret = knot_lmdb_open(db); + if (ret != KNOT_EOK) { + return ret; + } + MDB_val k = make_key_str((keyclass_t)serial_type, zone_name, NULL); + MDB_val v = knot_lmdb_make_key("I", serial); + return knot_lmdb_quick_insert(db, k, v); +} + +int kasp_db_load_serial(knot_lmdb_db_t *db, const knot_dname_t *zone_name, + kaspdb_serial_t serial_type, uint32_t *serial) +{ + if (knot_lmdb_exists(db) == KNOT_ENODB) { + return KNOT_ENOENT; + } + int ret = knot_lmdb_open(db); + if (ret != KNOT_EOK) { + return ret; + } + MDB_val k = make_key_str((keyclass_t)serial_type, zone_name, NULL); + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, false); + if (knot_lmdb_find(&txn, &k, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) { + knot_lmdb_unmake_curval(&txn, "I", serial); + } + knot_lmdb_abort(&txn); + free(k.mv_data); + return txn.ret; +} + +int kasp_db_get_policy_last(knot_lmdb_db_t *db, const char *policy_string, + knot_dname_t **lp_zone, char **lp_keyid) +{ + MDB_val k = make_key_str(KASPDBKEY_POLICYLAST, NULL, policy_string); + uint8_t kclass = 0; + knot_lmdb_txn_t txn = { 0 }; + *lp_zone = NULL; + *lp_keyid = NULL; + knot_lmdb_begin(db, &txn, false); + if (knot_lmdb_find(&txn, &k, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE) && + knot_lmdb_unmake_curval(&txn, "BNS", &kclass, lp_zone, lp_keyid)) { + assert(*lp_zone != NULL && *lp_keyid != NULL); + *lp_zone = knot_dname_copy(*lp_zone, NULL); + *lp_keyid = strdup(*lp_keyid); + if (kclass != KASPDBKEY_PARAMS) { + txn.ret = KNOT_EMALF; + } else if (*lp_keyid == NULL || *lp_zone == NULL) { + txn.ret = KNOT_ENOMEM; + } else { + // check that the referenced key really exists + knot_lmdb_find(&txn, &txn.cur_val, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE); + } + } + knot_lmdb_abort(&txn); + free(k.mv_data); + + return txn.ret; +} + +int kasp_db_set_policy_last(knot_lmdb_db_t *db, const char *policy_string, const char *last_lp_keyid, + const knot_dname_t *new_lp_zone, const char *new_lp_keyid) +{ + MDB_val k = make_key_str(KASPDBKEY_POLICYLAST, NULL, policy_string); + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + if (knot_lmdb_find(&txn, &k, KNOT_LMDB_EXACT)) { + // check that the last_lp_keyid matches + uint8_t unuse1, *unuse2; + const char *real_last_keyid; + if (knot_lmdb_unmake_curval(&txn, "BNS", &unuse1, &unuse2, &real_last_keyid) && + (last_lp_keyid == NULL || strcmp(last_lp_keyid, real_last_keyid) != 0)) { + txn.ret = KNOT_ESEMCHECK; + } + } + MDB_val v = make_key_str(KASPDBKEY_PARAMS, new_lp_zone, new_lp_keyid); + knot_lmdb_insert(&txn, &k, &v); + free(k.mv_data); + free(v.mv_data); + knot_lmdb_commit(&txn); + return txn.ret; +} + +int kasp_db_store_offline_records(knot_lmdb_db_t *db, knot_time_t for_time, const key_records_t *r) +{ + MDB_val k = make_key_time(KASPDBKEY_OFFLINE_RECORDS, r->rrsig.owner, for_time); + MDB_val v = { key_records_serialized_size(r), NULL }; + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + if (knot_lmdb_insert(&txn, &k, &v)) { + wire_ctx_t wire = wire_ctx_init(v.mv_data, v.mv_size); + txn.ret = key_records_serialize(&wire, r); + } + knot_lmdb_commit(&txn); + free(k.mv_data); + return txn.ret; +} + +int kasp_db_load_offline_records(knot_lmdb_db_t *db, const knot_dname_t *for_dname, + knot_time_t *for_time, knot_time_t *next_time, + key_records_t *r) +{ + MDB_val prefix = make_key_str(KASPDBKEY_OFFLINE_RECORDS, for_dname, NULL); + if (prefix.mv_data == NULL) { + return KNOT_ENOMEM; + } + unsigned operator = KNOT_LMDB_GEQ; + MDB_val search = prefix; + bool zero_for_time = (*for_time == 0); + if (!zero_for_time) { + operator = KNOT_LMDB_LEQ; + search = make_key_time(KASPDBKEY_OFFLINE_RECORDS, for_dname, *for_time); + } + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, false); + if (knot_lmdb_find(&txn, &search, operator) && + knot_lmdb_is_prefix_of(&prefix, &txn.cur_key)) { + wire_ctx_t wire = wire_ctx_init(txn.cur_val.mv_data, txn.cur_val.mv_size); + txn.ret = key_records_deserialize(&wire, r); + if (zero_for_time) { + unmake_key_time(&txn.cur_key, for_time); + } + if (!knot_lmdb_next(&txn) || !knot_lmdb_is_prefix_of(&prefix, &txn.cur_key) || + !unmake_key_time(&txn.cur_key, next_time)) { + *next_time = 0; + } + } else if (txn.ret == KNOT_EOK) { + txn.ret = KNOT_ENOENT; + } + knot_lmdb_abort(&txn); + if (!zero_for_time) { + free(search.mv_data); + } + free(prefix.mv_data); + return txn.ret; +} + +int kasp_db_delete_offline_records(knot_lmdb_db_t *db, const knot_dname_t *zone, + knot_time_t from_time, knot_time_t to_time) +{ + MDB_val prefix = make_key_str(KASPDBKEY_OFFLINE_RECORDS, zone, NULL); + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + knot_lmdb_foreach(&txn, &prefix) { + knot_time_t found; + if (unmake_key_time(&txn.cur_key, &found) && + knot_time_cmp(found, from_time) >= 0 && + knot_time_cmp(found, to_time) <= 0) { + knot_lmdb_del_cur(&txn); + } + } + knot_lmdb_commit(&txn); + free(prefix.mv_data); + return txn.ret; +} + +int kasp_db_get_saved_ttls(knot_lmdb_db_t *db, const knot_dname_t *zone, + uint32_t *max_ttl, uint32_t *key_ttl) +{ + MDB_val key = make_key_str(KASPDBKEY_SAVED_TTLS, zone, NULL); + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, false); + if (knot_lmdb_find(&txn, &key, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) { + knot_lmdb_unmake_curval(&txn, "II", max_ttl, key_ttl); + } + knot_lmdb_abort(&txn); + free(key.mv_data); + return txn.ret; +} + +int kasp_db_set_saved_ttls(knot_lmdb_db_t *db, const knot_dname_t *zone, + uint32_t max_ttl, uint32_t key_ttl) +{ + MDB_val key = make_key_str(KASPDBKEY_SAVED_TTLS, zone, NULL); + MDB_val val = knot_lmdb_make_key("II", max_ttl, key_ttl); + return knot_lmdb_quick_insert(db, key, val); +} + +void kasp_db_ensure_init(knot_lmdb_db_t *db, conf_t *conf) +{ + if (db->path == NULL) { + char *kasp_dir = conf_db(conf, C_KASP_DB); + conf_val_t kasp_size = conf_db_param(conf, C_KASP_DB_MAX_SIZE); + knot_lmdb_init(db, kasp_dir, conf_int(&kasp_size), 0, "keys_db"); + free(kasp_dir); + assert(db->path != NULL); + } +} + +int kasp_db_backup(const knot_dname_t *zone, knot_lmdb_db_t *db, knot_lmdb_db_t *backup_db) +{ + size_t n_prefs = zone_related_classes_size + 1; // NOTE: this and following must match number of record types + MDB_val prefixes[n_prefs]; + prefixes[0] = knot_lmdb_make_key("B", KASPDBKEY_POLICYLAST); // we copy all policy-last records, that doesn't harm + for (size_t i = 1; i < n_prefs; i++) { + prefixes[i] = make_key_str(zone_related_classes[i - 1], zone, NULL); + } + + int ret = knot_lmdb_copy_prefixes(db, backup_db, prefixes, n_prefs); + + for (int i = 0; i < n_prefs; i++) { + free(prefixes[i].mv_data); + } + return ret; +} diff --git a/src/knot/dnssec/kasp/kasp_db.h b/src/knot/dnssec/kasp/kasp_db.h new file mode 100644 index 0000000..e9eea4f --- /dev/null +++ b/src/knot/dnssec/kasp/kasp_db.h @@ -0,0 +1,296 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <time.h> + +#include "contrib/time.h" +#include "contrib/ucw/lists.h" +#include "libknot/db/db_lmdb.h" +#include "libknot/dname.h" +#include "knot/dnssec/kasp/policy.h" +#include "knot/journal/knot_lmdb.h" + +typedef struct kasp_db kasp_db_t; + +typedef enum { // the enum values MUST match those from keyclass_t !! + KASPDB_SERIAL_MASTER = 0x5, + KASPDB_SERIAL_LASTSIGNED = 0x6, +} kaspdb_serial_t; + +/*! + * \brief For given zone, list all keys (their IDs) belonging to it. + * + * \param db KASP db + * \param zone_name name of the zone in question + * \param dst output if KNOT_EOK: ptrlist of keys' params + * + * \return KNOT_E* (KNOT_ENOENT if no keys) + */ +int kasp_db_list_keys(knot_lmdb_db_t *db, const knot_dname_t *zone_name, list_t *dst); + +/*! + * \brief Obtain the algorithm of a key. + * + * \param db KASP db. + * \param zone_name name of the zone + * \param key_id ID of the key in question + * + * \retval KNOT_E* if error + * \return >0 The algorithm of the key. + */ +int kasp_db_get_key_algorithm(knot_lmdb_db_t *db, const knot_dname_t *zone_name, + const char *key_id); + +/*! + * \brief Remove a key from zone. Delete the key if no zone has it anymore. + * + * \param db KASP db + * \param zone_name zone to be removed from + * \param key_id ID of key to be removed + * \param still_used output if KNOT_EOK: is the key still in use by other zones? + * + * \return KNOT_E* + */ +int kasp_db_delete_key(knot_lmdb_db_t *db, const knot_dname_t *zone_name, const char *key_id, bool *still_used); + +/*! + * \brief Remove all zone's keys from DB, including nsec3param + * + * \param db KASP db + * \param zone_name zone to be removed + * + * \return KNOT_E* + */ +int kasp_db_delete_all(knot_lmdb_db_t *db, const knot_dname_t *zone_name); + +/*! + * \brief Selectively delete zones from the database. + * + * \param db KASP database. + * \param keep_zone Filtering callback. + * \param cb_data Data passed to callback function. + * + * \return KNOT_E* + */ +int kasp_db_sweep(knot_lmdb_db_t *db, sweep_cb keep_zone, void *cb_data); + +/*! + * \brief List all zones that have at least one key in KASP db. + * + * \param db KASP database. + * \param zones Output: ptrlist with zone names. + * + * \return KNOT_E* + */ +int kasp_db_list_zones(knot_lmdb_db_t *db, list_t *zones); + +/*! + * \brief Add a key to the DB (possibly overwrite) and link it to a zone. + * + * Stores new key with given params into KASP db. If a key with the same ID had been present + * in KASP db already, its params get silently overwritten by those new params. + * Moreover, the key ID is linked to the zone. + * + * \param db KASP db + * \param zone_name name of the zone the new key shall belong to + * \param params key params, incl. ID + * + * \return KNOT_E* + */ +int kasp_db_add_key(knot_lmdb_db_t *db, const knot_dname_t *zone_name, const key_params_t *params); + +/*! + * \brief Link a key from another zone. + * + * \param db KASP db + * \param zone_from name of the zone the key belongs to + * \param zone_to name of the zone the key shall belong to as well + * \param key_id ID of the key in question + * + * \return KNOT_E* + */ +int kasp_db_share_key(knot_lmdb_db_t *db, const knot_dname_t *zone_from, + const knot_dname_t *zone_to, const char *key_id); + +/*! + * \brief Store NSEC3 salt for given zone (possibly overwrites old salt). + * + * \param db KASP db + * \param zone_name zone name + * \param nsec3salt new NSEC3 salt + * \param salt_created timestamp when the salt was created + * + * \return KNOT_E* + */ +int kasp_db_store_nsec3salt(knot_lmdb_db_t *db, const knot_dname_t *zone_name, + const dnssec_binary_t *nsec3salt, knot_time_t salt_created); + +/*! + * \brief Load NSEC3 salt for given zone. + * + * \param db KASP db + * \param zone_name zone name + * \param nsec3salt output if KNOT_EOK: the zone's NSEC3 salt + * \param salt_created output if KNOT_EOK: timestamp when the salt was created + * + * \return KNOT_E* (KNOT_ENOENT if not stored before) + */ +int kasp_db_load_nsec3salt(knot_lmdb_db_t *db, const knot_dname_t *zone_name, + dnssec_binary_t *nsec3salt, knot_time_t *salt_created); + +/*! + * \brief Store SOA serial number of master or last signed serial. + * + * \param db KASP db + * \param zone_name zone name + * \param serial_type kind of serial to be stored + * \param serial new serial to be stored + * + * \return KNOT_E* + */ +int kasp_db_store_serial(knot_lmdb_db_t *db, const knot_dname_t *zone_name, + kaspdb_serial_t serial_type, uint32_t serial); + +/*! + * \brief Load saved SOA serial number of master or last signed serial. + * + * \param db KASP db + * \param zone_name zone name + * \param serial_type kind of serial to be loaded + * \param serial output if KNOT_EOK: desired serial number + * + * \return KNOT_E* (KNOT_ENOENT if not stored before) + */ +int kasp_db_load_serial(knot_lmdb_db_t *db, const knot_dname_t *zone_name, + kaspdb_serial_t serial_type, uint32_t *serial); + +/*! + * \brief For given policy name, obtain last generated key. + * + * \param db KASP db + * \param policy_string a name identifying the signing policy with shared keys + * \param lp_zone out: the zone owning the last generated key + * \param lp_keyid out: the ID of the last generated key + * + * \note lp_zone and lp_keyid must be freed even when an error is returned + * + * \return KNOT_E* + */ +int kasp_db_get_policy_last(knot_lmdb_db_t *db, const char *policy_string, + knot_dname_t **lp_zone, char **lp_keyid); + +/*! + * \brief For given policy name, try to reset last generated key. + * + * \param db KASP db + * \param policy_string a name identifying the signing policy with shared keys + * \param last_lp_keyid just for check: ID of the key the caller thinks is the policy-last + * \param new_lp_zone zone name of the new policy-last key + * \param new_lp_keyid ID of the new policy-last key + * + * \retval KNOT_ESEMCHECK lasp_lp_keyid does not correspond to real last key. Probably another zone + * changed policy-last key in the meantime. Re-run kasp_db_get_policy_last() + * \retval KNOT_EOK policy-last key set up successfully to given zone/ID + * \return KNOT_E* common error + */ +int kasp_db_set_policy_last(knot_lmdb_db_t *db, const char *policy_string, const char *last_lp_keyid, + const knot_dname_t *new_lp_zone, const char *new_lp_keyid); + +/*! + * \brief Store pre-generated records for offline KSK usage. + * + * \param db KASP db. + * \param for_time Timestamp in future in which the RRSIG shall be used. + * \param r Records to be stored. + * + * \return KNOT_E* + */ +int kasp_db_store_offline_records(knot_lmdb_db_t *db, knot_time_t for_time, const key_records_t *r); + +/*! + * \brief Load pregenerated records for offline signing. + * + * \param db KASP db. + * \param for_dname Name of the related zone. + * \param for_time Now. Closest RRSIG (timestamp equals or is closest lower). + * If zero, the first record is returned and its time is stored. + * \param next_time Out: timestamp of next saved RRSIG (for easy "iteration"). + * \param r Out: offline records. + * + * \return KNOT_E* + */ +int kasp_db_load_offline_records(knot_lmdb_db_t *db, const knot_dname_t *for_dname, + knot_time_t *for_time, knot_time_t *next_time, + key_records_t *r); + +/*! + * \brief Delete pregenerated records for specified time interval. + * + * \param db KASP db. + * \param zone Zone in question. + * \param from_time Lower bound of the time interval (0 = infinity). + * \param to_time Upper bound of the time interval (0 = infinity). + * + * \return KNOT_E* + */ +int kasp_db_delete_offline_records(knot_lmdb_db_t *db, const knot_dname_t *zone, + knot_time_t from_time, knot_time_t to_time); + +/*! + * \brief Load saved zone-max-TTL and DNSKEY-TTL. + * + * \param db KASP db. + * \param max_ttl Out: saved zone max TTL. + * \param key_ttl Out: saved DNSKEY TTL. + * + * \retval KNOT_ENOENT If not saved yet. + * \return KNOT_E* + */ +int kasp_db_get_saved_ttls(knot_lmdb_db_t *db, const knot_dname_t *zone, + uint32_t *max_ttl, uint32_t *key_ttl); + +/*! + * \brief Save current zone-max-TTL and DNSKEY-TTL. + * + * \param db KASP db. + * \param max_ttl Current zone max TTL. + * \param key_ttl Current DNSKEY TTL. + * + * \return KNOT_E* + */ +int kasp_db_set_saved_ttls(knot_lmdb_db_t *db, const knot_dname_t *zone, + uint32_t max_ttl, uint32_t key_ttl); + +/*! + * \brief Initialize KASP database according to conf, if not already. + * + * \param db KASP DB to be initialized. + * \param conf COnfiguration to take options from. + */ +void kasp_db_ensure_init(knot_lmdb_db_t *db, conf_t *conf); + +/*! + * \brief Backup KASP DB for one zone with keys and all metadata to backup location. + * + * \param zone Name of the zone to be backed up. + * \param db DB to backup from. + * \param backup_db DB to backup to. + * + * \return KNOT_E* + */ +int kasp_db_backup(const knot_dname_t *zone, knot_lmdb_db_t *db, knot_lmdb_db_t *backup_db); diff --git a/src/knot/dnssec/kasp/kasp_zone.c b/src/knot/dnssec/kasp/kasp_zone.c new file mode 100644 index 0000000..58925fa --- /dev/null +++ b/src/knot/dnssec/kasp/kasp_zone.c @@ -0,0 +1,447 @@ +/* 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/dnssec/kasp/kasp_zone.h" +#include "knot/dnssec/kasp/keystore.h" +#include "knot/dnssec/zone-keys.h" +#include "libdnssec/binary.h" + +// FIXME DNSSEC errors versus knot errors + +/*! + * Check if key parameters allow to create a key. + */ +static int key_params_check(key_params_t *params) +{ + assert(params); + + if (params->algorithm == 0) { + return KNOT_INVALID_KEY_ALGORITHM; + } + + if (params->public_key.size == 0) { + return KNOT_NO_PUBLIC_KEY; + } + + return KNOT_EOK; +} + +/*! \brief Determine presence of SEP bit by trial-end-error using known keytag. */ +static int dnskey_guess_flags(dnssec_key_t *key, uint16_t keytag) +{ + dnssec_key_set_flags(key, DNSKEY_FLAGS_KSK); + if (dnssec_key_get_keytag(key) == keytag) { + return KNOT_EOK; + } + + dnssec_key_set_flags(key, DNSKEY_FLAGS_ZSK); + if (dnssec_key_get_keytag(key) == keytag) { + return KNOT_EOK; + } + + dnssec_key_set_flags(key, DNSKEY_FLAGS_REVOKED); + if (dnssec_key_get_keytag(key) == keytag) { + return KNOT_EOK; + } + + return KNOT_EMALF; +} + +static int params2dnskey(const knot_dname_t *dname, key_params_t *params, + dnssec_key_t **key_ptr) +{ + assert(dname); + assert(params); + assert(key_ptr); + + int ret = key_params_check(params); + if (ret != KNOT_EOK) { + return ret; + } + + dnssec_key_t *key = NULL; + ret = dnssec_key_new(&key); + if (ret != KNOT_EOK) { + return knot_error_from_libdnssec(ret); + } + + ret = dnssec_key_set_dname(key, dname); + if (ret != KNOT_EOK) { + dnssec_key_free(key); + return knot_error_from_libdnssec(ret); + } + + dnssec_key_set_algorithm(key, params->algorithm); + + ret = dnssec_key_set_pubkey(key, ¶ms->public_key); + if (ret != KNOT_EOK) { + dnssec_key_free(key); + return knot_error_from_libdnssec(ret); + } + + ret = dnskey_guess_flags(key, params->keytag); + if (ret != KNOT_EOK) { + dnssec_key_free(key); + return ret; + } + + *key_ptr = key; + + return KNOT_EOK; +} + +static int params2kaspkey(const knot_dname_t *dname, key_params_t *params, + knot_kasp_key_t *key) +{ + assert(dname != NULL); + assert(params != NULL); + assert(key != NULL); + + int ret = params2dnskey(dname, params, &key->key); + if (ret != KNOT_EOK) { + return ret; + } + + key->id = strdup(params->id); + if (key->id == NULL) { + dnssec_key_free(key->key); + return KNOT_ENOMEM; + } + + key->timing = params->timing; + key->is_pub_only = params->is_pub_only; + assert(params->is_ksk || !params->is_csk); + key->is_ksk = params->is_ksk; + key->is_zsk = (params->is_csk || !params->is_ksk); + return KNOT_EOK; +} + +static void kaspkey2params(knot_kasp_key_t *key, key_params_t *params) +{ + assert(key); + assert(params); + + params->id = key->id; + params->keytag = dnssec_key_get_keytag(key->key); + dnssec_key_get_pubkey(key->key, ¶ms->public_key); + params->algorithm = dnssec_key_get_algorithm(key->key); + params->is_ksk = key->is_ksk; + params->is_csk = (key->is_ksk && key->is_zsk); + params->timing = key->timing; + params->is_pub_only = key->is_pub_only; +} + +static void detect_keytag_conflict(knot_kasp_zone_t *zone, bool *kt_cfl) +{ + *kt_cfl = false; + if (zone->num_keys == 0) { + return; + } + uint16_t keytags[zone->num_keys]; + for (size_t i = 0; i < zone->num_keys; i++) { + keytags[i] = dnssec_key_get_keytag(zone->keys[i].key); + for (size_t j = 0; j < i; j++) { + if (keytags[j] == keytags[i]) { + *kt_cfl = true; + return; + } + } + } +} + +int kasp_zone_load(knot_kasp_zone_t *zone, + const knot_dname_t *zone_name, + knot_lmdb_db_t *kdb, + bool *kt_cfl) +{ + if (zone == NULL || zone_name == NULL || kdb == NULL) { + return KNOT_EINVAL; + } + + knot_kasp_key_t *dkeys = NULL; + size_t num_dkeys = 0; + dnssec_binary_t salt = { 0 }; + knot_time_t sc = 0; + + list_t key_params; + init_list(&key_params); + int ret = kasp_db_list_keys(kdb, zone_name, &key_params); + if (ret == KNOT_ENOENT) { + zone->keys = NULL; + zone->num_keys = 0; + ret = KNOT_EOK; + goto kzl_salt; + } else if (ret != KNOT_EOK) { + goto kzl_end; + } + + num_dkeys = list_size(&key_params); + dkeys = calloc(num_dkeys, sizeof(*dkeys)); + if (dkeys == NULL) { + goto kzl_end; + } + + ptrnode_t *n; + int i = 0; + WALK_LIST(n, key_params) { + key_params_t *parm = n->d; + ret = params2kaspkey(zone_name, parm, &dkeys[i++]); + free_key_params(parm); + if (ret != KNOT_EOK) { + goto kzl_end; + } + } + +kzl_salt: + (void)kasp_db_load_nsec3salt(kdb, zone_name, &salt, &sc); + // if error, salt was probably not present, no problem to have zero ? + + zone->dname = knot_dname_copy(zone_name, NULL); + if (zone->dname == NULL) { + ret = KNOT_ENOMEM; + goto kzl_end; + } + zone->keys = dkeys; + zone->num_keys = num_dkeys; + zone->nsec3_salt = salt; + zone->nsec3_salt_created = sc; + + detect_keytag_conflict(zone, kt_cfl); + +kzl_end: + ptrlist_deep_free(&key_params, NULL); + if (ret != KNOT_EOK) { + free(dkeys); + } + return ret; +} + +int kasp_zone_append(knot_kasp_zone_t *zone, const knot_kasp_key_t *appkey) +{ + if (zone == NULL || appkey == NULL || (zone->keys == NULL && zone->num_keys > 0)) { + return KNOT_EINVAL; + } + + size_t new_num_keys = zone->num_keys + 1; + knot_kasp_key_t *new_keys = calloc(new_num_keys, sizeof(*new_keys)); + if (!new_keys) { + return KNOT_ENOMEM; + } + if (zone->num_keys > 0) { + memcpy(new_keys, zone->keys, zone->num_keys * sizeof(*new_keys)); + } + memcpy(&new_keys[new_num_keys - 1], appkey, sizeof(*appkey)); + free(zone->keys); + zone->keys = new_keys; + zone->num_keys = new_num_keys; + return KNOT_EOK; +} + +int kasp_zone_save(const knot_kasp_zone_t *zone, + const knot_dname_t *zone_name, + knot_lmdb_db_t *kdb) +{ + if (zone == NULL || zone_name == NULL || kdb == NULL) { + return KNOT_EINVAL; + } + + key_params_t parm; + for (size_t i = 0; i < zone->num_keys; i++) { + kaspkey2params(&zone->keys[i], &parm); + + // Force overwrite already existing key-val pairs. + int ret = kasp_db_add_key(kdb, zone_name, &parm); + if (ret != KNOT_EOK) { + return ret; + } + } + + + return kasp_db_store_nsec3salt(kdb, zone_name, &zone->nsec3_salt, + zone->nsec3_salt_created); +} + +static void kasp_zone_clear_keys(knot_kasp_zone_t *zone) +{ + for (size_t i = 0; i < zone->num_keys; i++) { + dnssec_key_free(zone->keys[i].key); + free(zone->keys[i].id); + } + free(zone->keys); + zone->keys = NULL; + zone->num_keys = 0; +} + +void kasp_zone_clear(knot_kasp_zone_t *zone) +{ + if (zone == NULL) { + return; + } + knot_dname_free(zone->dname, NULL); + kasp_zone_clear_keys(zone); + free(zone->nsec3_salt.data); + memset(zone, 0, sizeof(*zone)); +} + +void kasp_zone_free(knot_kasp_zone_t **zone) +{ + if (zone != NULL) { + kasp_zone_clear(*zone); + free(*zone); + *zone = NULL; + } +} + +void free_key_params(key_params_t *parm) +{ + if (parm != NULL) { + free(parm->id); + dnssec_binary_free(&parm->public_key); + memset(parm, 0 , sizeof(*parm)); + } +} + +int zone_init_keystore(conf_t *conf, conf_val_t *policy_id, + dnssec_keystore_t **keystore, unsigned *backend, bool *key_label) +{ + char *zone_path = conf_db(conf, C_KASP_DB); + if (zone_path == NULL) { + return KNOT_ENOMEM; + } + + conf_id_fix_default(policy_id); + + conf_val_t keystore_id = conf_id_get(conf, C_POLICY, C_KEYSTORE, policy_id); + conf_id_fix_default(&keystore_id); + + conf_val_t val = conf_id_get(conf, C_KEYSTORE, C_BACKEND, &keystore_id); + unsigned _backend = conf_opt(&val); + + val = conf_id_get(conf, C_KEYSTORE, C_CONFIG, &keystore_id); + const char *config = conf_str(&val); + + if (key_label != NULL) { + val = conf_id_get(conf, C_KEYSTORE, C_KEY_LABEL, &keystore_id); + *key_label = conf_bool(&val); + } + + int ret = keystore_load(config, _backend, zone_path, keystore); + + if (backend != NULL) { + *backend = _backend; + } + + free(zone_path); + return ret; +} + +int kasp_zone_keys_from_rr(knot_kasp_zone_t *zone, + const knot_rdataset_t *zone_dnskey, + bool policy_single_type_signing, + bool *keytag_conflict) +{ + if (zone == NULL || zone_dnskey == NULL || keytag_conflict == NULL) { + return KNOT_EINVAL; + } + + kasp_zone_clear_keys(zone); + + zone->num_keys = zone_dnskey->count; + zone->keys = calloc(zone->num_keys, sizeof(*zone->keys)); + if (zone->keys == NULL) { + zone->num_keys = 0; + return KNOT_ENOMEM; + } + + knot_rdata_t *zkey = zone_dnskey->rdata; + for (int i = 0; i < zone->num_keys; i++) { + int ret = dnssec_key_from_rdata(&zone->keys[i].key, zone->dname, + zkey->data, zkey->len); + if (ret == KNOT_EOK) { + ret = dnssec_key_get_keyid(zone->keys[i].key, &zone->keys[i].id); + } + if (ret != KNOT_EOK) { + free(zone->keys); + zone->keys = NULL; + zone->num_keys = 0; + return ret; + } + zone->keys[i].is_pub_only = true; + + zone->keys[i].is_ksk = (knot_dnskey_flags(zkey) == DNSKEY_FLAGS_KSK); + zone->keys[i].is_zsk = policy_single_type_signing || !zone->keys[i].is_ksk; + + zone->keys[i].timing.publish = 1; + zone->keys[i].timing.active = 1; + + zkey = knot_rdataset_next(zkey); + } + + detect_keytag_conflict(zone, keytag_conflict); + return KNOT_EOK; +} + +int kasp_zone_from_contents(knot_kasp_zone_t *zone, + const zone_contents_t *contents, + bool policy_single_type_signing, + bool policy_nsec3, + uint16_t *policy_nsec3_iters, + bool *keytag_conflict) +{ + if (zone == NULL || contents == NULL || contents->apex == NULL) { + return KNOT_EINVAL; + } + + memset(zone, 0, sizeof(*zone)); + zone->dname = knot_dname_copy(contents->apex->owner, NULL); + if (zone->dname == NULL) { + return KNOT_ENOMEM; + } + + knot_rdataset_t *zone_dnskey = node_rdataset(contents->apex, KNOT_RRTYPE_DNSKEY); + if (zone_dnskey == NULL || zone_dnskey->count < 1) { + free(zone->dname); + return KNOT_DNSSEC_ENOKEY; + } + + int ret = kasp_zone_keys_from_rr(zone, zone_dnskey, policy_single_type_signing, keytag_conflict); + if (ret != KNOT_EOK) { + free(zone->dname); + return ret; + } + + zone->nsec3_salt_created = 0; + if (policy_nsec3) { + knot_rdataset_t *zone_ns3p = node_rdataset(contents->apex, KNOT_RRTYPE_NSEC3PARAM); + if (zone_ns3p == NULL || zone_ns3p->count != 1) { + kasp_zone_clear(zone); + return KNOT_ENSEC3PAR; + } + zone->nsec3_salt.size = knot_nsec3param_salt_len(zone_ns3p->rdata); + zone->nsec3_salt.data = malloc(zone->nsec3_salt.size); + if (zone->nsec3_salt.data == NULL) { + kasp_zone_clear(zone); + return KNOT_ENOMEM; + } + memcpy(zone->nsec3_salt.data, + knot_nsec3param_salt(zone_ns3p->rdata), + zone->nsec3_salt.size); + + *policy_nsec3_iters = knot_nsec3param_iters(zone_ns3p->rdata); + } + + return KNOT_EOK; +} diff --git a/src/knot/dnssec/kasp/kasp_zone.h b/src/knot/dnssec/kasp/kasp_zone.h new file mode 100644 index 0000000..c4df282 --- /dev/null +++ b/src/knot/dnssec/kasp/kasp_zone.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/dnssec/kasp/kasp_db.h" +#include "knot/zone/contents.h" +#include "libdnssec/keystore.h" + +typedef struct { + knot_dname_t *dname; + + knot_kasp_key_t *keys; + size_t num_keys; + + dnssec_binary_t nsec3_salt; + knot_time_t nsec3_salt_created; +} knot_kasp_zone_t; + +int kasp_zone_load(knot_kasp_zone_t *zone, + const knot_dname_t *zone_name, + knot_lmdb_db_t *kdb, + bool *kt_cfl); + +int kasp_zone_save(const knot_kasp_zone_t *zone, + const knot_dname_t *zone_name, + knot_lmdb_db_t *kdb); + +int kasp_zone_append(knot_kasp_zone_t *zone, + const knot_kasp_key_t *appkey); + +void kasp_zone_clear(knot_kasp_zone_t *zone); +void kasp_zone_free(knot_kasp_zone_t **zone); + +void free_key_params(key_params_t *parm); + +int zone_init_keystore(conf_t *conf, conf_val_t *policy_id, + dnssec_keystore_t **keystore, unsigned *backend, bool *key_label); + +int kasp_zone_keys_from_rr(knot_kasp_zone_t *zone, + const knot_rdataset_t *zone_dnskey, + bool policy_single_type_signing, + bool *keytag_conflict); + +int kasp_zone_from_contents(knot_kasp_zone_t *zone, + const zone_contents_t *contents, + bool policy_single_type_signing, + bool policy_nsec3, + uint16_t *policy_nsec3_iters, + bool *keytag_conflict); diff --git a/src/knot/dnssec/kasp/keystate.c b/src/knot/dnssec/kasp/keystate.c new file mode 100644 index 0000000..f1eaa54 --- /dev/null +++ b/src/knot/dnssec/kasp/keystate.c @@ -0,0 +1,74 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "knot/dnssec/kasp/keystate.h" + +key_state_t get_key_state(const knot_kasp_key_t *key, knot_time_t moment) +{ + if (!key || moment <= 0) { + return DNSSEC_KEY_STATE_INVALID; + } + + const knot_kasp_key_timing_t *t = &key->timing; + + bool removed = (knot_time_cmp(t->remove, moment) <= 0); + bool revoked = (knot_time_cmp(t->revoke, moment) <= 0); + bool post_active = (knot_time_cmp(t->post_active, moment) <= 0); + bool retired = (knot_time_cmp(t->retire, moment) <= 0); + bool retire_active = (knot_time_cmp(t->retire_active, moment) <= 0); + bool active = (knot_time_cmp(t->active, moment) <= 0); + bool ready = (knot_time_cmp(t->ready, moment) <= 0); + bool published = (knot_time_cmp(t->publish, moment) <= 0); + bool pre_active = (knot_time_cmp(t->pre_active, moment) <= 0); + bool created = (knot_time_cmp(t->created, moment) <= 0); + + if (removed) { + return DNSSEC_KEY_STATE_REMOVED; + } + if (revoked) { + return DNSSEC_KEY_STATE_REVOKED; + } + if (post_active) { + if (retired) { + return DNSSEC_KEY_STATE_INVALID; + } else { + return DNSSEC_KEY_STATE_POST_ACTIVE; + } + } + if (retired) { + return DNSSEC_KEY_STATE_RETIRED; + } + if (retire_active) { + return DNSSEC_KEY_STATE_RETIRE_ACTIVE; + } + if (active) { + return DNSSEC_KEY_STATE_ACTIVE; + } + if (ready) { + return DNSSEC_KEY_STATE_READY; + } + if (published) { + return DNSSEC_KEY_STATE_PUBLISHED; + } + if (pre_active) { + return DNSSEC_KEY_STATE_PRE_ACTIVE; + } + if (created) { + // don't care + } + + return DNSSEC_KEY_STATE_INVALID; +} diff --git a/src/knot/dnssec/kasp/keystate.h b/src/knot/dnssec/kasp/keystate.h new file mode 100644 index 0000000..6b7d398 --- /dev/null +++ b/src/knot/dnssec/kasp/keystate.h @@ -0,0 +1,35 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/time.h" +#include "knot/dnssec/kasp/policy.h" + +typedef enum { + DNSSEC_KEY_STATE_INVALID = 0, + DNSSEC_KEY_STATE_PRE_ACTIVE, + DNSSEC_KEY_STATE_PUBLISHED, + DNSSEC_KEY_STATE_READY, + DNSSEC_KEY_STATE_ACTIVE, + DNSSEC_KEY_STATE_RETIRE_ACTIVE, + DNSSEC_KEY_STATE_RETIRED, + DNSSEC_KEY_STATE_POST_ACTIVE, + DNSSEC_KEY_STATE_REVOKED, + DNSSEC_KEY_STATE_REMOVED, +} key_state_t; + +key_state_t get_key_state(const knot_kasp_key_t *key, knot_time_t moment); diff --git a/src/knot/dnssec/kasp/keystore.c b/src/knot/dnssec/kasp/keystore.c new file mode 100644 index 0000000..2ec5cd1 --- /dev/null +++ b/src/knot/dnssec/kasp/keystore.c @@ -0,0 +1,89 @@ +/* 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 <stdio.h> +#include <string.h> + +#include "libdnssec/error.h" +#include "knot/dnssec/kasp/keystore.h" +#include "knot/conf/schema.h" +#include "libknot/error.h" + +static char *fix_path(const char *config, const char *base_path) +{ + assert(config); + assert(base_path); + + char *path = NULL; + + if (config[0] == '/') { + path = strdup(config); + } else { + if (asprintf(&path, "%s/%s", base_path, config) == -1) { + path = NULL; + } + } + + return path; +} + +int keystore_load(const char *config, unsigned backend, + const char *kasp_base_path, dnssec_keystore_t **keystore) +{ + int ret = DNSSEC_EINVAL; + char *fixed_config = NULL; + + switch (backend) { + case KEYSTORE_BACKEND_PEM: + ret = dnssec_keystore_init_pkcs8(keystore); + fixed_config = fix_path(config, kasp_base_path); + break; + case KEYSTORE_BACKEND_PKCS11: + ret = dnssec_keystore_init_pkcs11(keystore); + fixed_config = strdup(config); + break; + default: + assert(0); + } + if (ret != DNSSEC_EOK) { + free(fixed_config); + return knot_error_from_libdnssec(ret); + } + if (fixed_config == NULL) { + dnssec_keystore_deinit(*keystore); + *keystore = NULL; + return KNOT_ENOMEM; + } + + ret = dnssec_keystore_init(*keystore, fixed_config); + if (ret != DNSSEC_EOK) { + free(fixed_config); + dnssec_keystore_deinit(*keystore); + *keystore = NULL; + return knot_error_from_libdnssec(ret); + } + + ret = dnssec_keystore_open(*keystore, fixed_config); + free(fixed_config); + if (ret != DNSSEC_EOK) { + dnssec_keystore_deinit(*keystore); + *keystore = NULL; + return knot_error_from_libdnssec(ret); + } + + return KNOT_EOK; +} diff --git a/src/knot/dnssec/kasp/keystore.h b/src/knot/dnssec/kasp/keystore.h new file mode 100644 index 0000000..bd62347 --- /dev/null +++ b/src/knot/dnssec/kasp/keystore.h @@ -0,0 +1,22 @@ +/* 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 "libdnssec/keystore.h" + +int keystore_load(const char *config, unsigned backend, + const char *kasp_base_path, dnssec_keystore_t **keystore); diff --git a/src/knot/dnssec/kasp/policy.h b/src/knot/dnssec/kasp/policy.h new file mode 100644 index 0000000..4354b95 --- /dev/null +++ b/src/knot/dnssec/kasp/policy.h @@ -0,0 +1,135 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> + +#include "contrib/time.h" +#include "libdnssec/key.h" +#include "knot/conf/conf.h" + +/*! + * KASP key timing information. + */ +typedef struct { + knot_time_t created; /*!< Time the key was generated/imported. */ + knot_time_t pre_active; /*!< Signing start with new algorithm. */ + knot_time_t publish; /*!< Time of DNSKEY record publication. */ + knot_time_t ready; /*!< Start of RRSIG generation, waiting for parent zone. */ + knot_time_t active; /*!< RRSIG records generating, other keys can be retired */ + knot_time_t retire_active; /*!< Still active, but obsoleted. */ + knot_time_t retire; /*!< End of RRSIG records generating. */ + knot_time_t post_active; /*!< Still signing with old algorithm, not published. */ + knot_time_t revoke; /*!< RFC 5011 state of KSK with 'revoked' flag and signed by self. */ + knot_time_t remove; /*!< Time of DNSKEY record removal. */ +} knot_kasp_key_timing_t; + +/*! + * Key parameters as writing in zone config file. + */ +typedef struct { + char *id; + bool is_ksk; + bool is_csk; + bool is_pub_only; + uint16_t keytag; + uint8_t algorithm; + dnssec_binary_t public_key; + knot_kasp_key_timing_t timing; +} key_params_t; + +/*! + * Zone key. + */ +typedef struct { + char *id; /*!< Keystore unique key ID. */ + dnssec_key_t *key; /*!< Instance of the key. */ + knot_kasp_key_timing_t timing; /*!< Key timing information. */ + bool is_pub_only; + bool is_ksk; + bool is_zsk; +} knot_kasp_key_t; + +/*! + * Parent for DS checks. + */ +typedef struct { + conf_remote_t *addr; + size_t addrs; +} knot_kasp_parent_t; + +knot_dynarray_declare(parent, knot_kasp_parent_t, DYNARRAY_VISIBILITY_NORMAL, 3) + +/*! + * Set of DNSSEC key related records. + */ +typedef struct { + knot_rrset_t dnskey; + knot_rrset_t cdnskey; + knot_rrset_t cds; + knot_rrset_t rrsig; +} key_records_t; + +/*! + * Key and signature policy. + */ +typedef struct { + bool manual; + char *string; + // DNSKEY + dnssec_key_algorithm_t algorithm; + uint16_t ksk_size; + uint16_t zsk_size; + uint32_t dnskey_ttl; + uint32_t zsk_lifetime; // like knot_time_t + uint32_t ksk_lifetime; // like knot_time_t + uint32_t delete_delay; // like knot_timediff_t + bool ksk_shared; + bool single_type_signing; + bool sts_default; // single-type-signing was set to default value + // RRSIG + bool reproducible_sign; // (EC)DSA creates reproducible signatures + uint32_t rrsig_lifetime; // like knot_time_t + uint32_t rrsig_refresh_before; // like knot_timediff_t + uint32_t rrsig_prerefresh; // like knot_timediff_t + // NSEC3 + bool nsec3_enabled; + bool nsec3_opt_out; + int64_t nsec3_salt_lifetime; // like knot_time_t + uint16_t nsec3_iterations; + uint8_t nsec3_salt_length; + // zone + uint32_t zone_maximal_ttl; // like knot_timediff_t + uint32_t saved_max_ttl; + uint32_t saved_key_ttl; + // data propagation delay + uint32_t propagation_delay; // like knot_timediff_t + // various + uint32_t ksk_sbm_timeout; // like knot_time_t + uint32_t ksk_sbm_check_interval; // like knot_time_t + uint32_t ksk_sbm_delay; + unsigned cds_cdnskey_publish; + dnssec_key_digest_t cds_dt; // digest type for CDS + parent_dynarray_t parents; + uint16_t signing_threads; + bool ds_push; + bool offline_ksk; + bool incremental; + bool key_label; + unsigned unsafe; +} knot_kasp_policy_t; +// TODO make the time parameters knot_timediff_t ?? diff --git a/src/knot/dnssec/key-events.c b/src/knot/dnssec/key-events.c new file mode 100644 index 0000000..170f5a9 --- /dev/null +++ b/src/knot/dnssec/key-events.c @@ -0,0 +1,863 @@ +/* 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 "contrib/macros.h" +#include "knot/common/log.h" +#include "knot/common/systemd.h" +#include "knot/dnssec/kasp/keystate.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/policy.h" +#include "knot/dnssec/zone-keys.h" + +static bool key_present(const kdnssec_ctx_t *ctx, bool ksk, bool zsk) +{ + assert(ctx); + assert(ctx->zone); + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + const knot_kasp_key_t *key = &ctx->zone->keys[i]; + if (key->is_ksk == ksk && key->is_zsk == zsk && !key->is_pub_only && + get_key_state(key, ctx->now) != DNSSEC_KEY_STATE_REMOVED) { + return true; + } + } + return false; +} + +static bool key_id_present(const kdnssec_ctx_t *ctx, const char *keyid, bool want_ksk) +{ + assert(ctx); + assert(ctx->zone); + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + const knot_kasp_key_t *key = &ctx->zone->keys[i]; + if (strcmp(keyid, key->id) == 0 && + key->is_ksk == want_ksk && + get_key_state(key, ctx->now) != DNSSEC_KEY_STATE_REMOVED) { + return true; + } + } + return false; +} + +static unsigned algorithm_present(const kdnssec_ctx_t *ctx, uint8_t alg) +{ + assert(ctx); + assert(ctx->zone); + unsigned ret = 0; + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + const knot_kasp_key_t *key = &ctx->zone->keys[i]; + knot_time_t activated = knot_time_min(key->timing.pre_active, key->timing.ready); + if (knot_time_cmp(knot_time_min(activated, key->timing.active), ctx->now) <= 0 && + get_key_state(key, ctx->now) != DNSSEC_KEY_STATE_REMOVED && + dnssec_key_get_algorithm(key->key) == alg && !key->is_pub_only) { + ret++; + } + } + return ret; +} + +static bool signing_scheme_present(const kdnssec_ctx_t *ctx) +{ + if (ctx->policy->single_type_signing) { + return (!key_present(ctx, true, false) || !key_present(ctx, false, true) || key_present(ctx, true, true)); + } else { + return (key_present(ctx, true, false) && key_present(ctx, false, true)); + } +} + +static knot_kasp_key_t *key_get_by_id(kdnssec_ctx_t *ctx, const char *keyid) +{ + assert(ctx); + assert(ctx->zone); + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + if (strcmp(keyid, key->id) == 0) { + return key; + } + } + return NULL; +} + +static int generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags, + knot_time_t when_active, bool pre_active) +{ + assert(!pre_active || when_active == 0); + + knot_kasp_key_t *key = NULL; + int ret = kdnssec_generate_key(ctx, flags, &key); + if (ret != KNOT_EOK) { + return ret; + } + + key->timing.remove = 0; + key->timing.retire = 0; + key->timing.active = ((flags & DNSKEY_GENERATE_KSK) ? 0 : when_active); + key->timing.ready = ((flags & DNSKEY_GENERATE_KSK) ? when_active : 0); + key->timing.publish = (pre_active ? 0 : ctx->now); + key->timing.pre_active = (pre_active ? ctx->now : 0); + + return KNOT_EOK; +} + +static int share_or_generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags, + knot_time_t when_active, bool pre_active) +{ + assert(!pre_active || when_active == 0); + + knot_dname_t *borrow_zone = NULL; + char *borrow_key = NULL; + + if (!(flags & DNSKEY_GENERATE_KSK)) { + return KNOT_EINVAL; + } // for now not designed for rotating shared ZSK + + int ret = kasp_db_get_policy_last(ctx->kasp_db, ctx->policy->string, + &borrow_zone, &borrow_key); + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + free(borrow_zone); + free(borrow_key); + return ret; + } + + // if we already have the policy-last key, we have to generate new one + if (ret == KNOT_ENOENT || key_id_present(ctx, borrow_key, true) || + kasp_db_get_key_algorithm(ctx->kasp_db, borrow_zone, borrow_key) != (int)ctx->policy->algorithm) { + knot_kasp_key_t *key = NULL; + ret = kdnssec_generate_key(ctx, flags, &key); + if (ret != KNOT_EOK) { + return ret; + } + key->timing.remove = 0; + key->timing.retire = 0; + key->timing.active = ((flags & DNSKEY_GENERATE_KSK) ? 0 : when_active); + key->timing.ready = ((flags & DNSKEY_GENERATE_KSK) ? when_active : 0); + key->timing.publish = (pre_active ? 0 : ctx->now); + key->timing.pre_active = (pre_active ? ctx->now : 0); + + ret = kdnssec_ctx_commit(ctx); + if (ret != KNOT_EOK) { + return ret; + } + + ret = kasp_db_set_policy_last(ctx->kasp_db, ctx->policy->string, + borrow_key, ctx->zone->dname, key->id); + free(borrow_zone); + free(borrow_key); + borrow_zone = NULL; + borrow_key = NULL; + if (ret != KNOT_ESEMCHECK) { + // all ok, we generated new kay and updated policy-last + return ret; + } else { + // another zone updated policy-last key in the meantime + ret = kdnssec_delete_key(ctx, key); + if (ret == KNOT_EOK) { + ret = kdnssec_ctx_commit(ctx); + } + if (ret != KNOT_EOK) { + return ret; + } + + ret = kasp_db_get_policy_last(ctx->kasp_db, ctx->policy->string, + &borrow_zone, &borrow_key); + } + } + + if (ret == KNOT_EOK) { + ret = kdnssec_share_key(ctx, borrow_zone, borrow_key); + if (ret == KNOT_EOK) { + knot_kasp_key_t *newkey = key_get_by_id(ctx, borrow_key); + assert(newkey != NULL); + newkey->timing.remove = 0; + newkey->timing.retire = 0; + newkey->timing.active = ((flags & DNSKEY_GENERATE_KSK) ? 0 : when_active); + newkey->timing.ready = ((flags & DNSKEY_GENERATE_KSK) ? when_active : 0); + newkey->timing.publish = (pre_active ? 0 : ctx->now); + newkey->timing.pre_active = (pre_active ? ctx->now : 0); + newkey->is_ksk = (flags & DNSKEY_GENERATE_KSK); + newkey->is_zsk = (flags & DNSKEY_GENERATE_ZSK); + } + } + free(borrow_zone); + free(borrow_key); + return ret; +} + +#define GEN_KSK_FLAGS (DNSKEY_GENERATE_KSK | (ctx->policy->single_type_signing ? DNSKEY_GENERATE_ZSK : 0)) + +static int generate_ksk(kdnssec_ctx_t *ctx, knot_time_t when_active, bool pre_active) +{ + if (ctx->policy->ksk_shared) { + return share_or_generate_key(ctx, GEN_KSK_FLAGS, when_active, pre_active); + } else { + return generate_key(ctx, GEN_KSK_FLAGS, when_active, pre_active); + } +} + +static bool running_rollover(const kdnssec_ctx_t *ctx) +{ + bool res = false; + bool ready_ksk = false, active_ksk = false; + + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + if (key->is_pub_only) { + continue; + } + switch (get_key_state(key, ctx->now)) { + case DNSSEC_KEY_STATE_PRE_ACTIVE: + res = true; + break; + case DNSSEC_KEY_STATE_PUBLISHED: + res = true; + break; + case DNSSEC_KEY_STATE_READY: + ready_ksk = (ready_ksk || key->is_ksk); + break; + case DNSSEC_KEY_STATE_ACTIVE: + active_ksk = (active_ksk || key->is_ksk); + break; + case DNSSEC_KEY_STATE_RETIRE_ACTIVE: + case DNSSEC_KEY_STATE_POST_ACTIVE: + res = true; + break; + case DNSSEC_KEY_STATE_RETIRED: + case DNSSEC_KEY_STATE_REMOVED: + default: + break; + } + } + if (ready_ksk && active_ksk) { + res = true; + } + return res; +} + +typedef enum { + INVALID = 0, + GENERATE = 1, + PUBLISH, + SUBMIT, + REPLACE, + RETIRE, + REMOVE, + REALLY_REMOVE, +} roll_action_type_t; + +typedef struct { + roll_action_type_t type; + bool ksk; + knot_time_t time; + knot_kasp_key_t *key; + uint16_t ready_keytag; + const char *ready_keyid; +} roll_action_t; + +static const char *roll_action_name(roll_action_type_t type) +{ + switch (type) { + case GENERATE: return "generate"; + case PUBLISH: return "publish"; + case SUBMIT: return "submit"; + case REPLACE: return "replace"; + case RETIRE: return "retire"; + case REMOVE: return "remove"; + case INVALID: + // FALLTHROUGH + default: return "invalid"; + } +} + +static knot_time_t zsk_rollover_time(knot_time_t active_time, const kdnssec_ctx_t *ctx) +{ + if (active_time <= 0 || ctx->policy->zsk_lifetime == 0) { + return 0; + } + return knot_time_plus(active_time, ctx->policy->zsk_lifetime); +} + +static knot_time_t zsk_active_time(knot_time_t publish_time, const kdnssec_ctx_t *ctx) +{ + if (publish_time <= 0) { + return 0; + } + return knot_time_add(publish_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl); +} + +static knot_time_t zsk_remove_time(knot_time_t retire_time, const kdnssec_ctx_t *ctx) +{ + if (retire_time <= 0) { + return 0; + } + return knot_time_add(retire_time, ctx->policy->propagation_delay + ctx->policy->saved_max_ttl); +} + +static knot_time_t ksk_rollover_time(knot_time_t created_time, const kdnssec_ctx_t *ctx) +{ + if (created_time <= 0 || ctx->policy->ksk_lifetime == 0) { + return 0; + } + return knot_time_plus(created_time, ctx->policy->ksk_lifetime); +} + +static knot_time_t ksk_ready_time(knot_time_t publish_time, const kdnssec_ctx_t *ctx) +{ + if (publish_time <= 0) { + return 0; + } + return knot_time_add(publish_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl); +} + +static knot_time_t ksk_sbm_max_time(knot_time_t ready_time, const kdnssec_ctx_t *ctx) +{ + if (ready_time <= 0 || ctx->policy->ksk_sbm_timeout == 0) { + return 0; + } + return knot_time_plus(ready_time, ctx->policy->ksk_sbm_timeout); +} + +static knot_time_t ksk_retire_time(knot_time_t retire_active_time, const kdnssec_ctx_t *ctx) +{ + if (retire_active_time <= 0) { + return 0; + } + // this is not correct! It should be parent DS TTL. + return knot_time_add(retire_active_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl); +} + +static knot_time_t ksk_remove_time(knot_time_t retire_time, bool is_csk, const kdnssec_ctx_t *ctx) +{ + if (retire_time <= 0) { + return 0; + } + knot_timediff_t use_ttl = ctx->policy->saved_key_ttl; + if (is_csk) { + use_ttl = ctx->policy->saved_max_ttl; + } + return knot_time_add(retire_time, ctx->policy->propagation_delay + use_ttl); +} + +static knot_time_t ksk_really_remove_time(knot_time_t remove_time, const kdnssec_ctx_t *ctx) +{ + if (ctx->keep_deleted_keys) { + return 0; + } + return knot_time_add(remove_time, ctx->policy->delete_delay); +} + +static knot_time_t zsk_really_remove_time(knot_time_t remove_time, const kdnssec_ctx_t *ctx) +{ + if (ctx->keep_deleted_keys) { + return 0; + } + return knot_time_add(remove_time, ctx->policy->delete_delay); +} + +// algorithm rollover related timers must be the same for KSK and ZSK + +static knot_time_t alg_publish_time(knot_time_t pre_active_time, const kdnssec_ctx_t *ctx) +{ + if (pre_active_time <= 0) { + return 0; + } + return knot_time_add(pre_active_time, ctx->policy->propagation_delay + ctx->policy->saved_max_ttl); +} + +static knot_time_t alg_remove_time(knot_time_t post_active_time, const kdnssec_ctx_t *ctx) +{ + return knot_time_add(post_active_time, ctx->policy->propagation_delay + ctx->policy->saved_key_ttl); +} + +static roll_action_t next_action(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags) +{ + roll_action_t res = { 0 }; + + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + knot_time_t keytime = 0; + roll_action_type_t restype = INVALID; + if (key->is_pub_only || + (key->is_ksk && !(flags & KEY_ROLL_ALLOW_KSK_ROLL)) || + (key->is_zsk && !(flags & KEY_ROLL_ALLOW_ZSK_ROLL))) { + continue; + } + if (key->is_ksk) { + switch (get_key_state(key, ctx->now)) { + case DNSSEC_KEY_STATE_PRE_ACTIVE: + keytime = alg_publish_time(key->timing.pre_active, ctx); + restype = PUBLISH; + break; + case DNSSEC_KEY_STATE_PUBLISHED: + keytime = ksk_ready_time(key->timing.publish, ctx); + restype = SUBMIT; + break; + case DNSSEC_KEY_STATE_READY: + keytime = ksk_sbm_max_time(key->timing.ready, ctx); + restype = REPLACE; + res.ready_keyid = key->id; + res.ready_keytag = dnssec_key_get_keytag(key->key); + break; + case DNSSEC_KEY_STATE_ACTIVE: + if (!running_rollover(ctx) && + dnssec_key_get_algorithm(key->key) == ctx->policy->algorithm) { + knot_time_t ksk_created = key->timing.created == 0 ? + key->timing.active : + key->timing.created; + keytime = ksk_rollover_time(ksk_created, ctx); + restype = GENERATE; + } + break; + case DNSSEC_KEY_STATE_RETIRE_ACTIVE: + if (key->timing.retire == 0 && key->timing.post_active == 0 && key->timing.remove == 0) { // this shouldn't normally happen + // when a KSK is retire_active, it has already some following timer set + keytime = ksk_retire_time(key->timing.retire_active, ctx); + restype = RETIRE; + } + break; + case DNSSEC_KEY_STATE_POST_ACTIVE: + keytime = alg_remove_time(key->timing.post_active, ctx); + restype = REMOVE; + break; + case DNSSEC_KEY_STATE_RETIRED: + keytime = knot_time_min(key->timing.retire, key->timing.remove); + keytime = ksk_remove_time(keytime, key->is_zsk, ctx); + restype = REMOVE; + break; + case DNSSEC_KEY_STATE_REMOVED: + keytime = ksk_really_remove_time(key->timing.remove, ctx); + if (knot_time_cmp(keytime, ctx->now) > 0) { + keytime = 0; + } + restype = REALLY_REMOVE; + break; + default: + continue; + } + } else { + switch (get_key_state(key, ctx->now)) { + case DNSSEC_KEY_STATE_PRE_ACTIVE: + keytime = alg_publish_time(key->timing.pre_active, ctx); + restype = PUBLISH; + break; + case DNSSEC_KEY_STATE_PUBLISHED: + keytime = zsk_active_time(key->timing.publish, ctx); + restype = REPLACE; + break; + case DNSSEC_KEY_STATE_ACTIVE: + if (!running_rollover(ctx) && + dnssec_key_get_algorithm(key->key) == ctx->policy->algorithm) { + keytime = zsk_rollover_time(key->timing.active, ctx); + restype = GENERATE; + } + break; + case DNSSEC_KEY_STATE_RETIRE_ACTIVE: + // simply waiting for submitted KSK to retire me. + break; + case DNSSEC_KEY_STATE_POST_ACTIVE: + keytime = alg_remove_time(key->timing.post_active, ctx); + restype = REMOVE; + break; + case DNSSEC_KEY_STATE_RETIRED: + keytime = knot_time_min(key->timing.retire, key->timing.remove); + keytime = zsk_remove_time(keytime, ctx); + restype = REMOVE; + break; + case DNSSEC_KEY_STATE_REMOVED: + keytime = zsk_really_remove_time(key->timing.remove, ctx); + if (knot_time_cmp(keytime, ctx->now) > 0) { + keytime = 0; + } + restype = REALLY_REMOVE; + break; + case DNSSEC_KEY_STATE_READY: + default: + continue; + } + } + if (knot_time_cmp(keytime, res.time) < 0) { + res.key = key; + res.ksk = key->is_ksk; + res.time = keytime; + res.type = restype; + } + } + + return res; +} + +static int submit_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey) +{ + assert(get_key_state(newkey, ctx->now) == DNSSEC_KEY_STATE_PUBLISHED); + assert(newkey->is_ksk); + + // pushing from READY into ACTIVE decreases the other key's cds_priority + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + if (key->is_ksk && !key->is_pub_only && + get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_READY) { + key->timing.active = ctx->now; + } + } + + newkey->timing.ready = ctx->now; + return KNOT_EOK; +} + +knot_kasp_key_t *knot_dnssec_key2retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey) +{ + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + key_state_t keystate = get_key_state(key, ctx->now); + if (((newkey->is_ksk && key->is_ksk) || (!newkey->is_ksk && !key->is_ksk)) + && (keystate == DNSSEC_KEY_STATE_ACTIVE)) { + return key; + } + } + return NULL; +} + +static knot_kasp_key_t *zsk2retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *newksk) +{ + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + key_state_t keystate = get_key_state(key, ctx->now); + uint8_t keyalg = dnssec_key_get_algorithm(key->key); + bool algdiff = (keyalg != dnssec_key_get_algorithm(newksk->key)); + + if (key->is_zsk && !key->is_ksk && + (algdiff || newksk->is_zsk) && + (keystate == DNSSEC_KEY_STATE_ACTIVE || + keystate == DNSSEC_KEY_STATE_RETIRE_ACTIVE)) { + return key; + } + } + return NULL; +} + +static int exec_new_signatures(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey, uint32_t active_retire_delay) +{ + if (newkey->is_ksk) { + log_zone_notice(ctx->zone->dname, "DNSSEC, KSK submission, confirmed"); + } + + knot_kasp_key_t *oldkey = knot_dnssec_key2retire(ctx, newkey), *oldzsk = NULL; + if (oldkey != NULL) { + uint8_t keyalg = dnssec_key_get_algorithm(oldkey->key); + bool algdiff = (keyalg != dnssec_key_get_algorithm(newkey->key)); + + if (algdiff) { + oldkey->timing.retire_active = ctx->now; + if (oldkey->is_ksk) { + oldkey->timing.post_active = ctx->now + active_retire_delay; + } + } else if (oldkey->is_ksk) { + oldkey->timing.retire_active = ctx->now; + if (oldkey->is_zsk) { // CSK + oldkey->timing.retire = ctx->now + active_retire_delay; + } else { + oldkey->timing.remove = ctx->now + active_retire_delay; + } + } else { + oldkey->timing.retire = ctx->now; + } + + if (newkey->is_ksk && (oldzsk = zsk2retire(ctx, newkey)) != NULL) { + if (algdiff) { + oldzsk->timing.post_active = ctx->now + active_retire_delay; + } else { + oldzsk->timing.retire = ctx->now; + } + } + } + + if (newkey->is_ksk) { + assert(get_key_state(newkey, ctx->now) == DNSSEC_KEY_STATE_READY); + } else { + assert(get_key_state(newkey, ctx->now) == DNSSEC_KEY_STATE_PUBLISHED); + } + newkey->timing.active = knot_time_min(ctx->now, newkey->timing.active); + + return KNOT_EOK; +} + +static int exec_publish(kdnssec_ctx_t *ctx, knot_kasp_key_t *key) +{ + assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_PRE_ACTIVE); + key->timing.publish = ctx->now; + + return KNOT_EOK; +} + +static int exec_ksk_retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *key) +{ + bool alg_rollover = false; + knot_kasp_key_t *alg_rollover_friend = NULL; + + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *k = &ctx->zone->keys[i]; + int magic = (k->is_ksk && k->is_zsk ? 2 : 3); // :( + if (k->is_zsk && get_key_state(k, ctx->now) == DNSSEC_KEY_STATE_RETIRE_ACTIVE && + algorithm_present(ctx, dnssec_key_get_algorithm(k->key)) < magic) { + alg_rollover = true; + alg_rollover_friend = k; + } + } + + assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_RETIRE_ACTIVE); + + if (alg_rollover) { + key->timing.post_active = ctx->now; + alg_rollover_friend->timing.post_active = ctx->now; + } else { + key->timing.retire = ctx->now; + } + + return KNOT_EOK; +} + +static int exec_remove_old_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key) +{ + assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_RETIRED || + get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_POST_ACTIVE || + get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_REMOVED); + key->timing.remove = ctx->now; + return KNOT_EOK; +} + +static int exec_really_remove(kdnssec_ctx_t *ctx, knot_kasp_key_t *key) +{ + assert(get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_REMOVED); + assert(!ctx->keep_deleted_keys); + return kdnssec_delete_key(ctx, key); +} + +int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags, + zone_sign_reschedule_t *reschedule) +{ + if (ctx == NULL || reschedule == NULL) { + return KNOT_EINVAL; + } + if (ctx->policy->manual) { + if ((flags & (KEY_ROLL_FORCE_KSK_ROLL | KEY_ROLL_FORCE_ZSK_ROLL))) { + log_zone_notice(ctx->zone->dname, "DNSSEC, ignoring forced key rollover " + "due to manual policy"); + } + return KNOT_EOK; + } + int ret = KNOT_EOK; + uint16_t ready_keytag = 0; + const char *ready_keyid = NULL; + bool allowed_general_roll = ((flags & KEY_ROLL_ALLOW_KSK_ROLL) && (flags & KEY_ROLL_ALLOW_ZSK_ROLL)); + // generate initial keys if missing + if (!key_present(ctx, true, false) && !key_present(ctx, true, true)) { + if ((flags & KEY_ROLL_ALLOW_KSK_ROLL)) { + if (ctx->policy->ksk_shared) { + ret = share_or_generate_key(ctx, GEN_KSK_FLAGS, ctx->now, false); + } else { + ret = generate_key(ctx, GEN_KSK_FLAGS, ctx->now, false); + } + if (ret == KNOT_EOK) { + reschedule->plan_ds_check = true; + ready_keyid = ctx->zone->keys[0].id; + ready_keytag = dnssec_key_get_keytag(ctx->zone->keys[0].key); + } + } + if (ret == KNOT_EOK && (flags & KEY_ROLL_ALLOW_ZSK_ROLL)) { + reschedule->keys_changed = true; + if (!ctx->policy->single_type_signing && + !key_present(ctx, false, true)) { + ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, ctx->now, false); + } + } + } + // forced KSK rollover + if ((flags & KEY_ROLL_FORCE_KSK_ROLL) && ret == KNOT_EOK && (flags & KEY_ROLL_ALLOW_KSK_ROLL)) { + flags &= ~KEY_ROLL_FORCE_KSK_ROLL; + if (running_rollover(ctx)) { + log_zone_warning(ctx->zone->dname, "DNSSEC, ignoring forced KSK rollover " + "due to running rollover"); + } else { + ret = generate_ksk(ctx, 0, false); + if (ret == KNOT_EOK) { + reschedule->keys_changed = true; + log_zone_info(ctx->zone->dname, "DNSSEC, KSK rollover started"); + } + } + } + // forced ZSK rollover + if ((flags & KEY_ROLL_FORCE_ZSK_ROLL) && ret == KNOT_EOK && (flags & KEY_ROLL_ALLOW_ZSK_ROLL)) { + flags &= ~KEY_ROLL_FORCE_ZSK_ROLL; + if (running_rollover(ctx)) { + log_zone_warning(ctx->zone->dname, "DNSSEC, ignoring forced ZSK rollover " + "due to running rollover"); + } else { + ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, false); + if (ret == KNOT_EOK) { + reschedule->keys_changed = true; + log_zone_info(ctx->zone->dname, "DNSSEC, ZSK rollover started"); + } + } + } + // algorithm rollover + if (algorithm_present(ctx, ctx->policy->algorithm) == 0 && + !running_rollover(ctx) && allowed_general_roll && ret == KNOT_EOK) { + ret = generate_ksk(ctx, 0, true); + if (!ctx->policy->single_type_signing && ret == KNOT_EOK) { + ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, true); + } + log_zone_info(ctx->zone->dname, "DNSSEC, algorithm rollover started"); + if (ret == KNOT_EOK) { + reschedule->keys_changed = true; + } + } + // scheme rollover + if (!signing_scheme_present(ctx) && allowed_general_roll && + !running_rollover(ctx) && ret == KNOT_EOK) { + ret = generate_ksk(ctx, 0, false); + if (!ctx->policy->single_type_signing && ret == KNOT_EOK) { + ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, false); + } + log_zone_info(ctx->zone->dname, "DNSSEC, signing scheme rollover started"); + if (ret == KNOT_EOK) { + reschedule->keys_changed = true; + } + } + if (ret != KNOT_EOK) { + return ret; + } + + roll_action_t next = next_action(ctx, flags); + + reschedule->next_rollover = next.time; + + if (knot_time_cmp(reschedule->next_rollover, ctx->now) <= 0) { + bool log_keytag = true; + switch (next.type) { + case GENERATE: + if (next.ksk) { + ret = generate_ksk(ctx, 0, false); + } else { + ret = generate_key(ctx, DNSKEY_GENERATE_ZSK, 0, false); + } + if (ret == KNOT_EOK) { + log_zone_info(ctx->zone->dname, "DNSSEC, %cSK rollover started", + (next.ksk ? 'K' : 'Z')); + } + log_keytag = false; + break; + case PUBLISH: + ret = exec_publish(ctx, next.key); + break; + case SUBMIT: + ret = submit_key(ctx, next.key); + if (ret == KNOT_EOK) { + reschedule->plan_ds_check = true; + ready_keyid = next.key->id; + ready_keytag = dnssec_key_get_keytag(next.key->key); + } + break; + case REPLACE: + ret = exec_new_signatures(ctx, next.key, 0); + break; + case RETIRE: + ret = exec_ksk_retire(ctx, next.key); + break; + case REMOVE: + ret = exec_remove_old_key(ctx, next.key); + break; + case REALLY_REMOVE: + ret = exec_really_remove(ctx, next.key); + break; + default: + log_keytag = false; + ret = KNOT_EINVAL; + } + + if (ret == KNOT_EOK) { + reschedule->keys_changed = true; + next = next_action(ctx, flags); + reschedule->next_rollover = next.time; + } else { + if (log_keytag) { + log_zone_warning(ctx->zone->dname, "DNSSEC, key rollover, tag %5d, action %s (%s)", + dnssec_key_get_keytag(next.key->key), + roll_action_name(next.type), knot_strerror(ret)); + } else { + log_zone_warning(ctx->zone->dname, "DNSSEC, key rollover, action %s (%s)", + roll_action_name(next.type), knot_strerror(ret)); + } + } + } + + if (ret == KNOT_EOK && next.ready_keyid != NULL) { + // just to make sure DS check is scheduled + reschedule->plan_ds_check = true; + ready_keyid = next.ready_keyid; + ready_keytag = next.ready_keytag; + } + + if (ret == KNOT_EOK && knot_time_cmp(reschedule->next_rollover, ctx->now) <= 0) { + return knot_dnssec_key_rollover(ctx, flags, reschedule); + } + + if (ret == KNOT_EOK && reschedule->keys_changed) { + ret = kdnssec_ctx_commit(ctx); + } + + if (ret == KNOT_EOK && reschedule->plan_ds_check) { + char param[32]; + (void)snprintf(param, sizeof(param), "KEY_SUBMISSION=%hu", ready_keytag); + log_fmt_zone(LOG_NOTICE, LOG_SOURCE_ZONE, ctx->zone->dname, param, + "DNSSEC, KSK submission, waiting for confirmation"); + if (ctx->dbus_event & DBUS_EVENT_ZONE_SUBMISSION) { + systemd_emit_zone_submission(ctx->zone->dname, ready_keytag, ready_keyid); + } + } + + return ret; +} + +int knot_dnssec_ksk_sbm_confirm(kdnssec_ctx_t *ctx, uint32_t retire_delay) +{ + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + if (key->is_ksk && !key->is_pub_only && + get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_READY) { + int ret = exec_new_signatures(ctx, key, retire_delay); + if (ret == KNOT_EOK) { + ret = kdnssec_ctx_commit(ctx); + } + return ret; + } + } + return KNOT_NO_READY_KEY; +} + +bool zone_has_key_sbm(const kdnssec_ctx_t *ctx) +{ + assert(ctx->zone); + + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *key = &ctx->zone->keys[i]; + if (key->is_ksk && !key->is_pub_only && + (get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_READY || + get_key_state(key, ctx->now) == DNSSEC_KEY_STATE_ACTIVE)) { + return true; + } + } + return false; +} diff --git a/src/knot/dnssec/key-events.h b/src/knot/dnssec/key-events.h new file mode 100644 index 0000000..d216f90 --- /dev/null +++ b/src/knot/dnssec/key-events.h @@ -0,0 +1,69 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/dnssec/context.h" +#include "knot/dnssec/zone-events.h" + +/*! + * \brief Perform correct ZSK and KSK rollover action and plan next one. + * + * For given zone, check keys in KASP db and decide what shall be done + * according to their timers. Perform the action if they shall be done now, + * and tell the user the next time it shall be called. + * + * This function is optimized to be called from KEY_ROLLOVER_EVENT, + * but also during zone load so that the zone gets loaded already with + * proper DNSSEC chain. + * + * \param ctx Zone signing context + * \param flags Determine if some actions are forced + * \param reschedule Out: timestamp of desired next invoke + * + * \return KNOT_E* + */ +int knot_dnssec_key_rollover(kdnssec_ctx_t *ctx, zone_sign_roll_flags_t flags, + zone_sign_reschedule_t *reschedule); + +/*! + * \brief Get the key that ought to be retired by activating given new key. + * + * \param ctx DNSSEC context. + * \param newkey New key being rolled in. + * + * \return Old key being rolled out. + */ +knot_kasp_key_t *knot_dnssec_key2retire(kdnssec_ctx_t *ctx, knot_kasp_key_t *newkey); + +/*! + * \brief Set the submitted KSK to active state and the active one to retired + * + * \param ctx Zone signing context. + * \param retire_delay Retire event delay. + * + * \return KNOT_E* + */ +int knot_dnssec_ksk_sbm_confirm(kdnssec_ctx_t *ctx, uint32_t retire_delay); + +/*! + * \brief Is there a key in submission phase? + * + * \param ctx zone signing context + * + * \return False if there is no submitted key or if error; True otherwise + */ +bool zone_has_key_sbm(const kdnssec_ctx_t *ctx); diff --git a/src/knot/dnssec/key_records.c b/src/knot/dnssec/key_records.c new file mode 100644 index 0000000..9b22f7a --- /dev/null +++ b/src/knot/dnssec/key_records.c @@ -0,0 +1,300 @@ +/* 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/dnssec/key_records.h" + +#include "libdnssec/error.h" +#include "libdnssec/sign.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-sign.h" +#include "knot/journal/serialization.h" + +void key_records_init(const kdnssec_ctx_t *ctx, key_records_t *r) +{ + knot_rrset_init(&r->dnskey, knot_dname_copy(ctx->zone->dname, NULL), + KNOT_RRTYPE_DNSKEY, KNOT_CLASS_IN, ctx->policy->dnskey_ttl); + knot_rrset_init(&r->cdnskey, knot_dname_copy(ctx->zone->dname, NULL), + KNOT_RRTYPE_CDNSKEY, KNOT_CLASS_IN, 0); + knot_rrset_init(&r->cds, knot_dname_copy(ctx->zone->dname, NULL), + KNOT_RRTYPE_CDS, KNOT_CLASS_IN, 0); + knot_rrset_init(&r->rrsig, knot_dname_copy(ctx->zone->dname, NULL), + KNOT_RRTYPE_RRSIG, KNOT_CLASS_IN, ctx->policy->dnskey_ttl); +} + +void key_records_from_apex(const zone_node_t *apex, key_records_t *r) +{ + r->dnskey = node_rrset(apex, KNOT_RRTYPE_DNSKEY); + r->cdnskey = node_rrset(apex, KNOT_RRTYPE_CDNSKEY); + r->cds = node_rrset(apex, KNOT_RRTYPE_CDS); + knot_rrset_init_empty(&r->rrsig); +} + +int key_records_add_rdata(key_records_t *r, uint16_t rrtype, uint8_t *rdata, uint16_t rdlen, uint32_t ttl) +{ + knot_rrset_t *to_add; + switch(rrtype) { + case KNOT_RRTYPE_DNSKEY: + to_add = &r->dnskey; + break; + case KNOT_RRTYPE_CDNSKEY: + to_add = &r->cdnskey; + break; + case KNOT_RRTYPE_CDS: + to_add = &r->cds; + break; + case KNOT_RRTYPE_RRSIG: + to_add = &r->rrsig; + break; + default: + return KNOT_EINVAL; + } + + int ret = knot_rrset_add_rdata(to_add, rdata, rdlen, NULL); + if (ret == KNOT_EOK) { + to_add->ttl = ttl; + } + return ret; +} + +void key_records_clear(key_records_t *r) +{ + knot_rrset_clear(&r->dnskey, NULL); + knot_rrset_clear(&r->cdnskey, NULL); + knot_rrset_clear(&r->cds, NULL); + knot_rrset_clear(&r->rrsig, NULL); +} + +void key_records_clear_rdatasets(key_records_t *r) +{ + knot_rdataset_clear(&r->dnskey.rrs, NULL); + knot_rdataset_clear(&r->cdnskey.rrs, NULL); + knot_rdataset_clear(&r->cds.rrs, NULL); + knot_rdataset_clear(&r->rrsig.rrs, NULL); +} + +static int add_one(const knot_rrset_t *rr, changeset_t *ch, + bool rem, changeset_flag_t fl, int ret) +{ + if (ret == KNOT_EOK && !knot_rrset_empty(rr)) { + if (rem) { + ret = changeset_add_removal(ch, rr, fl); + } else { + ret = changeset_add_addition(ch, rr, fl); + } + } + return ret; +} + +int key_records_to_changeset(const key_records_t *r, changeset_t *ch, + bool rem, changeset_flag_t chfl) +{ + int ret = KNOT_EOK; + ret = add_one(&r->dnskey, ch, rem, chfl, ret); + ret = add_one(&r->cdnskey, ch, rem, chfl, ret); + ret = add_one(&r->cds, ch, rem, chfl, ret); + return ret; +} + +static int subtract_one(knot_rrset_t *from, const knot_rrset_t *what, + int (*fcn)(knot_rdataset_t *, const knot_rdataset_t *, knot_mm_t *), + int ret) +{ + if (ret == KNOT_EOK && !knot_rrset_empty(from)) { + ret = fcn(&from->rrs, &what->rrs, NULL); + } + return ret; +} + +int key_records_subtract(key_records_t *r, const key_records_t *against) +{ + int ret = KNOT_EOK; + ret = subtract_one(&r->dnskey, &against->dnskey, knot_rdataset_subtract, ret); + ret = subtract_one(&r->cdnskey, &against->cdnskey, knot_rdataset_subtract, ret); + ret = subtract_one(&r->cds, &against->cds, knot_rdataset_subtract, ret); + return ret; +} + +int key_records_intersect(key_records_t *r, const key_records_t *against) +{ + int ret = KNOT_EOK; + ret = subtract_one(&r->dnskey, &against->dnskey, knot_rdataset_intersect2, ret); + ret = subtract_one(&r->cdnskey, &against->cdnskey, knot_rdataset_intersect2, ret); + ret = subtract_one(&r->cds, &against->cds, knot_rdataset_intersect2, ret); + return ret; +} + +int key_records_dump(char **buf, size_t *buf_size, const key_records_t *r, bool verbose) +{ + if (*buf == NULL) { + if (*buf_size == 0) { + *buf_size = 512; + } + *buf = malloc(*buf_size); + if (*buf == NULL) { + return KNOT_ENOMEM; + } + } + + const knot_dump_style_t verb_style = { + .wrap = true, + .show_ttl = true, + .verbose = true, + .original_ttl = true, + .human_timestamp = true + }; + const knot_dump_style_t *style = verbose ? &verb_style : &KNOT_DUMP_STYLE_DEFAULT; + + int ret = 0; + size_t total = 1; + const knot_rrset_t *all_rr[4] = { &r->dnskey, &r->cdnskey, &r->cds, &r->rrsig }; + // first go: just detect the size + for (int i = 0; i < 4; i++) { + if (ret >= 0 && !knot_rrset_empty(all_rr[i])) { + ret = knot_rrset_txt_dump(all_rr[i], buf, buf_size, style); + (void)buf; + total += ret; + } + } + if (ret >= 0 && total > *buf_size) { + free(*buf); + *buf_size = total; + *buf = malloc(*buf_size); + if (*buf == NULL) { + return KNOT_ENOMEM; + } + } + char *fake_buf = *buf; + size_t fake_size = *buf_size; + //second go: do it + for (int i = 0; i < 4; i++) { + if (ret >= 0 && !knot_rrset_empty(all_rr[i])) { + ret = knot_rrset_txt_dump(all_rr[i], &fake_buf, &fake_size, style); + fake_buf += ret, fake_size -= ret; + } + } + assert(fake_buf - *buf == total - 1); + return ret >= 0 ? KNOT_EOK : ret; +} + +int key_records_sign(const zone_key_t *key, key_records_t *r, const kdnssec_ctx_t *kctx, knot_time_t *expires) +{ + dnssec_sign_ctx_t *sign_ctx; + int ret = dnssec_sign_new(&sign_ctx, key->key); + if (ret != DNSSEC_EOK) { + ret = knot_error_from_libdnssec(ret); + } + + if (!knot_rrset_empty(&r->dnskey) && knot_zone_sign_use_key(key, &r->dnskey)) { + ret = knot_sign_rrset(&r->rrsig, &r->dnskey, key->key, sign_ctx, kctx, NULL, expires); + } + if (ret == KNOT_EOK && !knot_rrset_empty(&r->cdnskey) && knot_zone_sign_use_key(key, &r->cdnskey)) { + ret = knot_sign_rrset(&r->rrsig, &r->cdnskey, key->key, sign_ctx, kctx, NULL, expires); + } + if (ret == KNOT_EOK && !knot_rrset_empty(&r->cds) && knot_zone_sign_use_key(key, &r->cds)) { + ret = knot_sign_rrset(&r->rrsig, &r->cds, key->key, sign_ctx, kctx, NULL, expires); + } + + dnssec_sign_free(sign_ctx); + return ret; +} + +int key_records_verify(key_records_t *r, kdnssec_ctx_t *kctx, knot_time_t timestamp) +{ + kctx->now = timestamp; + int ret = kasp_zone_keys_from_rr(kctx->zone, &r->dnskey.rrs, false, &kctx->keytag_conflict); + if (ret != KNOT_EOK) { + return ret; + } + + zone_sign_ctx_t *sign_ctx = zone_validation_ctx(kctx); + if (sign_ctx == NULL) { + return KNOT_ENOMEM; + } + + ret = knot_validate_rrsigs(&r->dnskey, &r->rrsig, sign_ctx, false); + if (ret == KNOT_EOK && !knot_rrset_empty(&r->cdnskey)) { + ret = knot_validate_rrsigs(&r->cdnskey, &r->rrsig, sign_ctx, false); + } + if (ret == KNOT_EOK && !knot_rrset_empty(&r->cds)) { + ret = knot_validate_rrsigs(&r->cds, &r->rrsig, sign_ctx, false); + } + + zone_sign_ctx_free(sign_ctx); + return ret; +} + +size_t key_records_serialized_size(const key_records_t *r) +{ + return rrset_serialized_size(&r->dnskey) + rrset_serialized_size(&r->cdnskey) + + rrset_serialized_size(&r->cds) + rrset_serialized_size(&r->rrsig); +} + +int key_records_serialize(wire_ctx_t *wire, const key_records_t *r) +{ + int ret = serialize_rrset(wire, &r->dnskey); + if (ret == KNOT_EOK) { + ret = serialize_rrset(wire, &r->cdnskey); + } + if (ret == KNOT_EOK) { + ret = serialize_rrset(wire, &r->cds); + } + if (ret == KNOT_EOK) { + ret = serialize_rrset(wire, &r->rrsig); + } + return ret; +} + +int key_records_deserialize(wire_ctx_t *wire, key_records_t *r) +{ + int ret = deserialize_rrset(wire, &r->dnskey); + if (ret == KNOT_EOK) { + ret = deserialize_rrset(wire, &r->cdnskey); + } + if (ret == KNOT_EOK) { + ret = deserialize_rrset(wire, &r->cds); + } + if (ret == KNOT_EOK) { + ret = deserialize_rrset(wire, &r->rrsig); + } + return ret; +} + +int key_records_last_timestamp(kdnssec_ctx_t *ctx, knot_time_t *last) +{ + knot_time_t from = 0; + while (true) { + knot_time_t next; + key_records_t r = { { 0 } }; + int ret = kasp_db_load_offline_records(ctx->kasp_db, ctx->zone->dname, + &from, &next, &r); + key_records_clear(&r); + if (ret == KNOT_ENOENT) { + break; + } else if (ret != KNOT_EOK) { + return ret; + } + + if (next == 0) { + break; + } + from = next; + } + if (from == 0) { + from = knot_time(); + } + *last = from; + return KNOT_EOK; +} diff --git a/src/knot/dnssec/key_records.h b/src/knot/dnssec/key_records.h new file mode 100644 index 0000000..b53ed86 --- /dev/null +++ b/src/knot/dnssec/key_records.h @@ -0,0 +1,54 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/wire_ctx.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/updates/changesets.h" + +void key_records_init(const kdnssec_ctx_t *ctx, key_records_t *r); + +void key_records_from_apex(const zone_node_t *apex, key_records_t *r); + +int key_records_add_rdata(key_records_t *r, uint16_t rrtype, uint8_t *rdata, uint16_t rdlen, uint32_t ttl); + +void key_records_clear(key_records_t *r); + +void key_records_clear_rdatasets(key_records_t *r); + +int key_records_to_changeset(const key_records_t *r, changeset_t *ch, + bool rem, changeset_flag_t chfl); + +int key_records_subtract(key_records_t *r, const key_records_t *against); + +int key_records_intersect(key_records_t *r, const key_records_t *against); + +int key_records_dump(char **buf, size_t *buf_size, const key_records_t *r, bool verbose); + +int key_records_sign(const zone_key_t *key, key_records_t *r, const kdnssec_ctx_t *kctx, knot_time_t *expires); + +// WARNING this modifies 'kctx' with updated timestamp and with zone_keys from r->dnskey +int key_records_verify(key_records_t *r, kdnssec_ctx_t *kctx, knot_time_t timestamp); + +size_t key_records_serialized_size(const key_records_t *r); + +int key_records_serialize(wire_ctx_t *wire, const key_records_t *r); + +int key_records_deserialize(wire_ctx_t *wire, key_records_t *r); + +// Returns now if no records available. +int key_records_last_timestamp(kdnssec_ctx_t *ctx, knot_time_t *last); diff --git a/src/knot/dnssec/nsec-chain.c b/src/knot/dnssec/nsec-chain.c new file mode 100644 index 0000000..dc35097 --- /dev/null +++ b/src/knot/dnssec/nsec-chain.c @@ -0,0 +1,797 @@ +/* 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 "contrib/base32hex.h" +#include "knot/dnssec/nsec-chain.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-nsec.h" +#include "knot/dnssec/zone-sign.h" +#include "knot/zone/adjust.h" + +void bitmap_add_node_rrsets(dnssec_nsec_bitmap_t *bitmap, const zone_node_t *node, + bool exact) +{ + bool deleg = node->flags & NODE_FLAGS_DELEG; + for (int i = 0; i < node->rrset_count; i++) { + knot_rrset_t rr = node_rrset_at(node, i); + if (deleg && (rr.type != KNOT_RRTYPE_NS && rr.type != KNOT_RRTYPE_DS && + rr.type != KNOT_RRTYPE_NSEC)) { + if (rr.type != KNOT_RRTYPE_RRSIG) { + continue; + } + if (!rrsig_covers_type(&rr, KNOT_RRTYPE_DS) && + !rrsig_covers_type(&rr, KNOT_RRTYPE_NSEC)) { + continue; + } + } + if (!exact && (rr.type == KNOT_RRTYPE_NSEC || rr.type == KNOT_RRTYPE_RRSIG)) { + continue; + } + + dnssec_nsec_bitmap_add(bitmap, rr.type); + } +} + +/* - NSEC chain construction ------------------------------------------------ */ + +static int create_nsec_base(knot_rrset_t *rrset, knot_dname_t *from_owner, + const knot_dname_t *to_owner, uint32_t ttl, + size_t bitmap_size, uint8_t **bitmap_writeto) +{ + knot_rrset_init(rrset, from_owner, KNOT_RRTYPE_NSEC, KNOT_CLASS_IN, ttl); + + size_t next_owner_size = knot_dname_size(to_owner); + size_t rdsize = next_owner_size + bitmap_size; + uint8_t rdata[rdsize]; + memcpy(rdata, to_owner, next_owner_size); + + int ret = knot_rrset_add_rdata(rrset, rdata, rdsize, NULL); + + assert(ret != KNOT_EOK || rrset->rrs.rdata->len == rdsize); + *bitmap_writeto = rrset->rrs.rdata->data + next_owner_size; + + return ret; +} + +/*! + * \brief Create NSEC RR set. + * + * \param rrset RRSet to be initialized. + * \param from Node that should contain the new RRSet. + * \param to Node that should be pointed to from 'from'. + * \param ttl Record TTL (SOA's minimum TTL). + * + * \return Error code, KNOT_EOK if successful. + */ +static int create_nsec_rrset(knot_rrset_t *rrset, const zone_node_t *from, + const knot_dname_t *to, uint32_t ttl) +{ + assert(from); + assert(to); + + dnssec_nsec_bitmap_t *rr_types = dnssec_nsec_bitmap_new(); + if (!rr_types) { + return KNOT_ENOMEM; + } + + bitmap_add_node_rrsets(rr_types, from, false); + dnssec_nsec_bitmap_add(rr_types, KNOT_RRTYPE_NSEC); + dnssec_nsec_bitmap_add(rr_types, KNOT_RRTYPE_RRSIG); + + uint8_t *bitmap_write; + int ret = create_nsec_base(rrset, from->owner, to, ttl, + dnssec_nsec_bitmap_size(rr_types), &bitmap_write); + if (ret == KNOT_EOK) { + dnssec_nsec_bitmap_write(rr_types, bitmap_write); + } + dnssec_nsec_bitmap_free(rr_types); + + return ret; +} + +/*! + * \brief Connect two nodes by adding a NSEC RR into the first node. + * + * Callback function, signature chain_iterate_cb. + * + * \param a First node. + * \param b Second node (immediate follower of a). + * \param data Pointer to nsec_chain_iterate_data_t holding parameters + * including changeset. + * + * \return Error code, KNOT_EOK if successful. + */ +static int connect_nsec_nodes(zone_node_t *a, zone_node_t *b, + nsec_chain_iterate_data_t *data) +{ + assert(a); + assert(b); + assert(data); + + if (b->rrset_count == 0 || b->flags & NODE_FLAGS_NONAUTH) { + return NSEC_NODE_SKIP; + } + + int ret = KNOT_EOK; + + /*! + * If the node has no other RRSets than NSEC (and possibly RRSIGs), + * just remove the NSEC and its RRSIG, they are redundant + */ + if (node_rrtype_exists(b, KNOT_RRTYPE_NSEC) + && knot_nsec_empty_nsec_and_rrsigs_in_node(b)) { + ret = knot_nsec_changeset_remove(b, data->update); + if (ret != KNOT_EOK) { + return ret; + } + // Skip the 'b' node + return NSEC_NODE_SKIP; + } + + // create new NSEC + knot_rrset_t new_nsec; + ret = create_nsec_rrset(&new_nsec, a, b->owner, data->ttl); + if (ret != KNOT_EOK) { + return ret; + } + + knot_rrset_t old_nsec = node_rrset(a, KNOT_RRTYPE_NSEC); + + if (!knot_rrset_empty(&old_nsec)) { + /* Convert old NSEC to lowercase, just in case it's not. */ + knot_rrset_t *old_nsec_lc = knot_rrset_copy(&old_nsec, NULL); + ret = knot_rrset_rr_to_canonical(old_nsec_lc); + if (ret != KNOT_EOK) { + knot_rrset_free(old_nsec_lc, NULL); + return ret; + } + + bool equal = knot_rrset_equal(&new_nsec, old_nsec_lc, true); + knot_rrset_free(old_nsec_lc, NULL); + + if (equal) { + // current NSEC is valid, do nothing + knot_rdataset_clear(&new_nsec.rrs, NULL); + return KNOT_EOK; + } + + ret = knot_nsec_changeset_remove(a, data->update); + if (ret != KNOT_EOK) { + knot_rdataset_clear(&new_nsec.rrs, NULL); + return ret; + } + } + + // Add new NSEC to the changeset (no matter if old was removed) + ret = zone_update_add(data->update, &new_nsec); + knot_rdataset_clear(&new_nsec.rrs, NULL); + return ret; +} + +/*! + * \brief Replace b's NSEC "next" field with a's, keeping the NSEC bitmap. + * + * \param a Node to take the NSEC "next" field from. + * \param b Node to update the NSEC "next" field in. + * \param data Contains changeset to be updated. + * + * \return KNOT_E* + */ +static int reconnect_nsec_nodes(zone_node_t *a, zone_node_t *b, + nsec_chain_iterate_data_t *data) +{ + assert(a); + assert(b); + assert(data); + + knot_rrset_t an = node_rrset(a, KNOT_RRTYPE_NSEC); + assert(!knot_rrset_empty(&an)); + + knot_rrset_t bnorig = node_rrset(b, KNOT_RRTYPE_NSEC); + assert(!knot_rrset_empty(&bnorig)); + + size_t b_bitmap_len = knot_nsec_bitmap_len(bnorig.rrs.rdata); + + knot_rrset_t bnnew; + uint8_t *bitmap_write; + int ret = create_nsec_base(&bnnew, bnorig.owner, knot_nsec_next(an.rrs.rdata), + bnorig.ttl, b_bitmap_len, &bitmap_write); + if (ret == KNOT_EOK) { + memcpy(bitmap_write, knot_nsec_bitmap(bnorig.rrs.rdata), b_bitmap_len); + } + + ret = zone_update_remove(data->update, &bnorig); + if (ret == KNOT_EOK) { + ret = zone_update_add(data->update, &bnnew); + } + + knot_rdataset_clear(&bnnew.rrs, NULL); + return ret; +} + +static bool node_no_nsec(zone_node_t *node) +{ + return ((node->flags & NODE_FLAGS_DELETED) || + (node->flags & NODE_FLAGS_NONAUTH) || + node->rrset_count == 0); +} + +/*! + * \brief Create or fix the node's NSEC record with correct bitmap. + * + * \param node Node to fix the NSEC bitmap in. + * \param data_voidp NSEC creation data. + * + * \return KNOT_E* + */ +static int nsec_update_bitmap(zone_node_t *node, + nsec_chain_iterate_data_t *data) +{ + if (node_no_nsec(node) || knot_nsec_empty_nsec_and_rrsigs_in_node(node)) { + return knot_nsec_changeset_remove(node, data->update); + } + + knot_rrset_t old_nsec = node_rrset(node, KNOT_RRTYPE_NSEC); + const knot_dname_t *next = knot_rrset_empty(&old_nsec) ? + (const knot_dname_t *)"" : + knot_nsec_next(old_nsec.rrs.rdata); + knot_rrset_t new_nsec; + int ret = create_nsec_rrset(&new_nsec, node, next, data->ttl); + + if (ret == KNOT_EOK && !knot_rrset_empty(&old_nsec)) { + ret = zone_update_remove(data->update, &old_nsec); + } + if (ret == KNOT_EOK) { + ret = zone_update_add(data->update, &new_nsec); + } + knot_rdataset_clear(&new_nsec.rrs, NULL); + return ret; +} + +static int nsec_update_bitmaps(zone_tree_t *node_ptrs, + nsec_chain_iterate_data_t *data) +{ + zone_tree_delsafe_it_t it = { 0 }; + int ret = zone_tree_delsafe_it_begin(node_ptrs, &it, false); + if (ret != KNOT_EOK) { + return ret; + } + while (!zone_tree_delsafe_it_finished(&it) && ret == KNOT_EOK) { + ret = nsec_update_bitmap(zone_tree_delsafe_it_val(&it), data); + zone_tree_delsafe_it_next(&it); + } + zone_tree_delsafe_it_free(&it); + return ret; +} + +static bool node_nsec3_unmatching(const zone_node_t *node, const dnssec_nsec3_params_t *params) +{ + knot_rdataset_t *nsec3 = node_rdataset(node, KNOT_RRTYPE_NSEC3); + if (nsec3 == NULL || nsec3->count < 1 || params == NULL) { + return false; + } + knot_rdata_t *rdata = nsec3->rdata; + for (int i = 0; i < nsec3->count; i++) { + if (knot_nsec3_alg(rdata) == params->algorithm && + knot_nsec3_iters(rdata) == params->iterations && + knot_nsec3_salt_len(rdata) == params->salt.size && + memcmp(knot_nsec3_salt(rdata), params->salt.data, params->salt.size) == 0) { + return false; + } + rdata = knot_rdataset_next(rdata); + } + return true; +} + +int nsec_check_connect_nodes(zone_node_t *a, zone_node_t *b, + nsec_chain_iterate_data_t *data) +{ + if (node_no_nsec(b) || node_nsec3_unmatching(b, data->nsec3_params)) { + return NSEC_NODE_SKIP; + } + knot_rdataset_t *nsec = node_rdataset(a, data->nsec_type); + if (nsec == NULL || nsec->count != 1) { + data->update->validation_hint.node = a->owner; + data->update->validation_hint.rrtype = KNOT_RRTYPE_ANY; + return KNOT_DNSSEC_ENSEC_CHAIN; + } + if (data->nsec_type == KNOT_RRTYPE_NSEC) { + const knot_dname_t *a_next = knot_nsec_next(nsec->rdata); + if (!knot_dname_is_case_equal(a_next, b->owner)) { + data->update->validation_hint.node = a->owner; + data->update->validation_hint.rrtype = data->nsec_type; + return KNOT_DNSSEC_ENSEC_CHAIN; + } + } else { + uint8_t next_len = knot_nsec3_next_len(nsec->rdata); + uint8_t bdecoded[next_len]; + int len = knot_base32hex_decode(b->owner + 1, b->owner[0], bdecoded, next_len); + if (len != next_len || + memcmp(knot_nsec3_next(nsec->rdata), bdecoded, len) != 0) { + data->update->validation_hint.node = a->owner; + data->update->validation_hint.rrtype = data->nsec_type; + return KNOT_DNSSEC_ENSEC_CHAIN; + } + } + return KNOT_EOK; +} + +static zone_node_t *nsec_prev(zone_node_t *node, const dnssec_nsec3_params_t *matching_params) +{ + zone_node_t *res = node; + do { + res = node_prev(res); + } while (res != NULL && ((res->flags & NODE_FLAGS_NONAUTH) || + res->rrset_count == 0 || + node_nsec3_unmatching(res, matching_params))); + assert(res == NULL || !knot_nsec_empty_nsec_and_rrsigs_in_node(res)); + return res; +} + +static int nsec_check_prev_next(zone_node_t *node, void *ctx) +{ + if (node_no_nsec(node)) { + return KNOT_EOK; + } + + nsec_chain_iterate_data_t *data = ctx; + int ret = nsec_check_connect_nodes(nsec_prev(node, data->nsec3_params), node, data); + if (ret == NSEC_NODE_SKIP) { + return KNOT_EOK; + } + if (ret != KNOT_EOK) { + return ret; + } + + dnssec_validation_hint_t *hint = &data->update->validation_hint; + knot_rdataset_t *nsec = node_rdataset(node, data->nsec_type); + if (nsec == NULL || nsec->count != 1) { + hint->node = node->owner; + hint->rrtype = KNOT_RRTYPE_ANY; + return KNOT_DNSSEC_ENSEC_CHAIN; + } + + const zone_node_t *nn; + if (data->nsec_type == KNOT_RRTYPE_NSEC) { + if (knot_dname_store(hint->next, knot_nsec_next(nsec->rdata)) == 0) { + return KNOT_EINVAL; + } + knot_dname_to_lower(hint->next); + nn = zone_contents_find_node(data->update->new_cont, hint->next); + } else { + ret = knot_nsec3_hash_to_dname(hint->next, sizeof(hint->next), + knot_nsec3_next(nsec->rdata), + knot_nsec3_next_len(nsec->rdata), + data->update->new_cont->apex->owner); + if (ret != KNOT_EOK) { + return ret; + } + nn = zone_contents_find_nsec3_node(data->update->new_cont, hint->next); + } + if (nn == NULL) { + hint->node = hint->next; + hint->rrtype = KNOT_RRTYPE_ANY; + return KNOT_DNSSEC_ENSEC_CHAIN; + } + if (nsec_prev((zone_node_t *)nn, data->nsec3_params) != node) { + hint->node = node->owner; + hint->rrtype = data->nsec_type; + return KNOT_DNSSEC_ENSEC_CHAIN; + } + return KNOT_EOK; +} + +int nsec_check_new_connects(zone_tree_t *tree, nsec_chain_iterate_data_t *data) +{ + return zone_tree_apply(tree, nsec_check_prev_next, data); +} + +static int check_subtree_optout(zone_node_t *node, void *ctx) +{ + bool *res = ctx; + if ((node->flags & NODE_FLAGS_NONAUTH) || !*res) { + return KNOT_EOK; + } + if (node_nsec3_get(node) != NULL && + node_rdataset(node_nsec3_get(node), KNOT_RRTYPE_NSEC3) != NULL) { + *res = false; + } + return KNOT_EOK; +} + +static int check_nsec_bitmap(zone_node_t *node, void *ctx) +{ + nsec_chain_iterate_data_t *data = ctx; + assert((bool)(data->nsec_type == KNOT_RRTYPE_NSEC3) == (bool)(data->nsec3_params != NULL)); + const zone_node_t *nsec_node = node; + bool shall_no_nsec = node_no_nsec(node); + if (data->nsec3_params != NULL) { + if ((node->flags & NODE_FLAGS_DELETED) || + node_rrtype_exists(node, KNOT_RRTYPE_NSEC3)) { + // this can happen when checking nodes from adjust_ptrs + return KNOT_EOK; + } + nsec_node = node_nsec3_get(node); + shall_no_nsec = (node->flags & NODE_FLAGS_DELETED) || + (node->flags & NODE_FLAGS_NONAUTH); + } + bool may_no_nsec = (data->nsec3_params != NULL && !(node->flags & NODE_FLAGS_SUBTREE_AUTH)); + knot_rdataset_t *nsec = node_rdataset(nsec_node, data->nsec_type); + if (may_no_nsec && nsec == NULL) { + int ret = zone_tree_sub_apply(data->update->new_cont->nodes, node->owner, + true, check_subtree_optout, &may_no_nsec); + if (ret != KNOT_EOK) { + return ret; + } + } + if ((nsec == NULL || nsec->count != 1) && !shall_no_nsec && !may_no_nsec) { + data->update->validation_hint.node = (nsec_node == NULL ? node->owner : nsec_node->owner); + data->update->validation_hint.rrtype = KNOT_RRTYPE_ANY; + return KNOT_DNSSEC_ENONSEC; + } + if (shall_no_nsec && nsec != NULL && nsec->count > 0) { + data->update->validation_hint.node = nsec_node->owner; + data->update->validation_hint.rrtype = data->nsec_type; + return KNOT_DNSSEC_ENSEC_BITMAP; + } + if (shall_no_nsec) { + return KNOT_EOK; + } + if (may_no_nsec && nsec == NULL) { + assert(data->nsec_type == KNOT_RRTYPE_NSEC3); + const zone_node_t *found_nsec3 = NULL, *prev_nsec3 = NULL; + if (node->nsec3_hash == NULL || + zone_contents_find_nsec3(data->update->new_cont, node->nsec3_hash, &found_nsec3, &prev_nsec3) != ZONE_NAME_NOT_FOUND || + found_nsec3 != NULL) { + return KNOT_ERROR; + } + if (prev_nsec3 == NULL) { + data->update->validation_hint.node = (nsec_node == NULL ? node->owner : nsec_node->owner); + data->update->validation_hint.rrtype = KNOT_RRTYPE_ANY; + return KNOT_DNSSEC_ENONSEC; + } + knot_rdataset_t *nsec3 = node_rdataset(prev_nsec3, KNOT_RRTYPE_NSEC3); + if (nsec3 == NULL) { + return KNOT_ERROR; + } + if (nsec3->count != 1 || !(knot_nsec3param_flags(nsec3->rdata) & KNOT_NSEC3_FLAG_OPT_OUT)) { + data->update->validation_hint.node = prev_nsec3->owner; + data->update->validation_hint.rrtype = data->nsec_type; + return KNOT_DNSSEC_ENSEC3_OPTOUT; + } + return KNOT_EOK; + } + + dnssec_nsec_bitmap_t *rr_types = dnssec_nsec_bitmap_new(); + if (rr_types == NULL) { + return KNOT_ENOMEM; + } + bitmap_add_node_rrsets(rr_types, node, true); + + uint16_t node_wire_size = dnssec_nsec_bitmap_size(rr_types); + uint8_t *node_wire = malloc(node_wire_size); + if (node_wire == NULL) { + dnssec_nsec_bitmap_free(rr_types); + return KNOT_ENOMEM; + } + dnssec_nsec_bitmap_write(rr_types, node_wire); + dnssec_nsec_bitmap_free(rr_types); + + const uint8_t *nsec_wire = NULL; + uint16_t nsec_wire_size = 0; + if (data->nsec3_params == NULL) { + nsec_wire = knot_nsec_bitmap(nsec->rdata); + nsec_wire_size = knot_nsec_bitmap_len(nsec->rdata); + } else { + nsec_wire = knot_nsec3_bitmap(nsec->rdata); + nsec_wire_size = knot_nsec3_bitmap_len(nsec->rdata); + } + + if (node_wire_size != nsec_wire_size || + memcmp(node_wire, nsec_wire, node_wire_size) != 0) { + free(node_wire); + data->update->validation_hint.node = node->owner; + data->update->validation_hint.rrtype = data->nsec_type; + return KNOT_DNSSEC_ENSEC_BITMAP; + } + free(node_wire); + return KNOT_EOK; +} + +int nsec_check_bitmaps(zone_tree_t *nsec_ptrs, nsec_chain_iterate_data_t *data) +{ + return zone_tree_apply(nsec_ptrs, check_nsec_bitmap, data); +} + +/*! \brief Return the one from those nodes which has + * closest lower (lexicographically) owner name to ref. */ +static zone_node_t *node_nearer(zone_node_t *a, zone_node_t *b, zone_node_t *ref) +{ + if (a == NULL || a == b) { + return b; + } else if (b == NULL) { + return a; + } else { + int abigger = knot_dname_cmp(a->owner, ref->owner) >= 0 ? 1 : 0; + int bbigger = knot_dname_cmp(b->owner, ref->owner) >= 0 ? 1 : 0; + int cmp = knot_dname_cmp(a->owner, b->owner); + if (abigger != bbigger) { + cmp = -cmp; + } + return cmp < 0 ? b : a; + } +} + +/* - API - iterations ------------------------------------------------------- */ + +/*! + * \brief Call a function for each piece of the chain formed by sorted nodes. + */ +int knot_nsec_chain_iterate_create(zone_tree_t *nodes, + chain_iterate_create_cb callback, + nsec_chain_iterate_data_t *data) +{ + assert(nodes); + assert(callback); + + zone_tree_delsafe_it_t it = { 0 }; + int result = zone_tree_delsafe_it_begin(nodes, &it, false); + if (result != KNOT_EOK) { + return result; + } + + if (zone_tree_delsafe_it_finished(&it)) { + zone_tree_delsafe_it_free(&it); + return KNOT_EINVAL; + } + + zone_node_t *first = zone_tree_delsafe_it_val(&it); + zone_node_t *previous = first; + zone_node_t *current = first; + + zone_tree_delsafe_it_next(&it); + + while (!zone_tree_delsafe_it_finished(&it)) { + current = zone_tree_delsafe_it_val(&it); + + result = callback(previous, current, data); + if (result == NSEC_NODE_SKIP) { + // No NSEC should be created for 'current' node, skip + ; + } else if (result == KNOT_EOK) { + previous = current; + } else { + zone_tree_delsafe_it_free(&it); + return result; + } + zone_tree_delsafe_it_next(&it); + } + + zone_tree_delsafe_it_free(&it); + + return result == NSEC_NODE_SKIP ? callback(previous, first, data) : + callback(current, first, data); +} + +int knot_nsec_chain_iterate_fix(zone_tree_t *node_ptrs, + chain_iterate_create_cb callback, + chain_iterate_create_cb cb_reconn, + nsec_chain_iterate_data_t *data) +{ + zone_tree_delsafe_it_t it = { 0 }; + int ret = zone_tree_delsafe_it_begin(node_ptrs, &it, true); + if (ret != KNOT_EOK) { + return ret; + } + + zone_node_t *prev_it = NULL; + zone_node_t *started_with = NULL; + while (ret == KNOT_EOK) { + if (zone_tree_delsafe_it_finished(&it)) { + assert(started_with != NULL); + zone_tree_delsafe_it_restart(&it); + } + + zone_node_t *curr_new = zone_tree_delsafe_it_val(&it); + zone_node_t *curr_old = binode_counterpart(curr_new); + bool del_new = node_no_nsec(curr_new); + bool del_old = node_no_nsec(curr_old); + + if (started_with == curr_new) { + assert(started_with != NULL); + break; + } + if (!del_old && !del_new && started_with == NULL) { + // Once this must happen since the NSEC(3) node belonging + // to zone apex is always present. + started_with = curr_new; + } + + if (!del_old && del_new && started_with != NULL) { + zone_node_t *prev_old = curr_old, *prev_new; + do { + prev_old = nsec_prev(prev_old, NULL); + prev_new = binode_counterpart(prev_old); + } while (node_no_nsec(prev_new)); + + zone_node_t *prev_near = node_nearer(prev_new, prev_it, curr_old); + ret = cb_reconn(curr_old, prev_near, data); + } + if (del_old && !del_new && started_with != NULL) { + zone_node_t *prev_new = nsec_prev(curr_new, NULL); + ret = cb_reconn(prev_new, curr_new, data); + if (ret == KNOT_EOK) { + ret = callback(prev_new, curr_new, data); + } + prev_it = curr_new; + } + + zone_tree_delsafe_it_next(&it); + } + zone_tree_delsafe_it_free(&it); + return ret; +} + +/* - API - utility functions ------------------------------------------------ */ + +/*! + * \brief Add entry for removed NSEC to the changeset. + */ +int knot_nsec_changeset_remove(const zone_node_t *n, zone_update_t *update) +{ + if (update == NULL) { + return KNOT_EINVAL; + } + + int result = KNOT_EOK; + knot_rrset_t nsec_rem = node_rrset(n, KNOT_RRTYPE_NSEC); + knot_rrset_t nsec3_rem = node_rrset(n, KNOT_RRTYPE_NSEC3); + knot_rrset_t rrsigs = node_rrset(n, KNOT_RRTYPE_RRSIG); + + if (!knot_rrset_empty(&nsec_rem)) { + result = zone_update_remove(update, &nsec_rem); + } + if (result == KNOT_EOK && !knot_rrset_empty(&nsec3_rem)) { + result = zone_update_remove(update, &nsec3_rem); + } + if (!knot_rrset_empty(&rrsigs) && result == KNOT_EOK) { + knot_rrset_t synth_rrsigs; + knot_rrset_init(&synth_rrsigs, n->owner, KNOT_RRTYPE_RRSIG, + KNOT_CLASS_IN, rrsigs.ttl); + result = knot_synth_rrsig(KNOT_RRTYPE_NSEC, &rrsigs.rrs, + &synth_rrsigs.rrs, NULL); + if (result == KNOT_ENOENT) { + // Try removing NSEC3 RRSIGs + result = knot_synth_rrsig(KNOT_RRTYPE_NSEC3, &rrsigs.rrs, + &synth_rrsigs.rrs, NULL); + } + + if (result != KNOT_EOK) { + knot_rdataset_clear(&synth_rrsigs.rrs, NULL); + if (result != KNOT_ENOENT) { + return result; + } + return KNOT_EOK; + } + + // store RRSIG + result = zone_update_remove(update, &synth_rrsigs); + knot_rdataset_clear(&synth_rrsigs.rrs, NULL); + } + + return result; +} + +/*! + * \brief Checks whether the node is empty or eventually contains only NSEC and + * RRSIGs. + */ +bool knot_nsec_empty_nsec_and_rrsigs_in_node(const zone_node_t *n) +{ + assert(n); + for (int i = 0; i < n->rrset_count; ++i) { + knot_rrset_t rrset = node_rrset_at(n, i); + if (rrset.type != KNOT_RRTYPE_NSEC && + rrset.type != KNOT_RRTYPE_RRSIG) { + return false; + } + } + + return true; +} + +/* - API - Chain creation --------------------------------------------------- */ + +/*! + * \brief Create new NSEC chain, add differences from current into a changeset. + */ +int knot_nsec_create_chain(zone_update_t *update, uint32_t ttl) +{ + assert(update); + assert(update->new_cont->nodes); + + nsec_chain_iterate_data_t data = { ttl, update, KNOT_RRTYPE_NSEC }; + + return knot_nsec_chain_iterate_create(update->new_cont->nodes, + connect_nsec_nodes, &data); +} + +int knot_nsec_fix_chain(zone_update_t *update, uint32_t ttl) +{ + assert(update); + assert(update->zone->contents->nodes); + assert(update->new_cont->nodes); + + nsec_chain_iterate_data_t data = { ttl, update, KNOT_RRTYPE_NSEC }; + + int ret = nsec_update_bitmaps(update->a_ctx->node_ptrs, &data); + if (ret != KNOT_EOK) { + return ret; + } + + ret = zone_adjust_contents(update->new_cont, adjust_cb_void, NULL, false, true, 1, update->a_ctx->node_ptrs); + if (ret != KNOT_EOK) { + return ret; + } + + // ensure that zone root is in list of changed nodes + ret = zone_tree_insert(update->a_ctx->node_ptrs, &update->new_cont->apex); + if (ret != KNOT_EOK) { + return ret; + } + + return knot_nsec_chain_iterate_fix(update->a_ctx->node_ptrs, + connect_nsec_nodes, reconnect_nsec_nodes, &data); +} + +int knot_nsec_check_chain(zone_update_t *update) +{ + if (!zone_tree_is_empty(update->new_cont->nsec3_nodes)) { + update->validation_hint.node = update->zone->name; + update->validation_hint.rrtype = KNOT_RRTYPE_NSEC3; + return KNOT_DNSSEC_ENSEC_BITMAP; + } + + nsec_chain_iterate_data_t data = { 0, update, KNOT_RRTYPE_NSEC }; + + int ret = nsec_check_bitmaps(update->new_cont->nodes, &data); + if (ret != KNOT_EOK) { + return ret; + } + + return knot_nsec_chain_iterate_create(update->new_cont->nodes, + nsec_check_connect_nodes, &data); +} + +int knot_nsec_check_chain_fix(zone_update_t *update) +{ + if (!zone_tree_is_empty(update->new_cont->nsec3_nodes)) { + update->validation_hint.node = update->zone->name; + update->validation_hint.rrtype = KNOT_RRTYPE_NSEC3; + return KNOT_DNSSEC_ENSEC_BITMAP; + } + + nsec_chain_iterate_data_t data = { 0, update, KNOT_RRTYPE_NSEC }; + + int ret = nsec_check_bitmaps(update->a_ctx->node_ptrs, &data); + if (ret != KNOT_EOK) { + return ret; + } + + return nsec_check_new_connects(update->a_ctx->node_ptrs, &data); +} diff --git a/src/knot/dnssec/nsec-chain.h b/src/knot/dnssec/nsec-chain.h new file mode 100644 index 0000000..362780e --- /dev/null +++ b/src/knot/dnssec/nsec-chain.h @@ -0,0 +1,174 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <assert.h> +#include <stdbool.h> +#include <stdint.h> + +#include "knot/zone/contents.h" +#include "knot/updates/zone-update.h" +#include "libdnssec/nsec.h" + +/*! + * \brief Parameters to be used in connect_nsec_nodes callback. + */ +typedef struct { + uint32_t ttl; // TTL for NSEC(3) records + zone_update_t *update; // The zone update for NSECs + uint16_t nsec_type; // NSEC or NSEC3 + const dnssec_nsec3_params_t *nsec3_params; +} nsec_chain_iterate_data_t; + +/*! + * \brief Used to control changeset iteration functions. + */ +enum { + NSEC_NODE_SKIP = 1, +}; + +/*! + * \brief Callback used when creating NSEC chains. + */ +typedef int (*chain_iterate_create_cb)(zone_node_t *, zone_node_t *, + nsec_chain_iterate_data_t *); + +/*! + * \brief Add all RR types from a node into the bitmap. + */ +void bitmap_add_node_rrsets(dnssec_nsec_bitmap_t *bitmap, const zone_node_t *node, + bool exact); + +/*! + * \brief Check that the NSEC(3) record in node A points to B. + * + * \param a Node A. + * \param b Node B. + * \param data Validation context. + * + * \retval NSEC_NODE_SKIP Node B is not part of NSEC chain, call again with A and B->next. + * \retval KNOT_DNSSEC_ENSEC_CHAIN The NSEC(3) chain is broken. + * \return KNOT_E* + */ +int nsec_check_connect_nodes(zone_node_t *a, zone_node_t *b, + nsec_chain_iterate_data_t *data); + +/*! + * \brief Check NSEC connections of updated nodes. + * + * \param tree Trie with updated nodes. + * \param data Validation context. + * + * \return KNOT_DNSSEC_ENSEC_CHAIN, KNOT_E* + */ +int nsec_check_new_connects(zone_tree_t *tree, nsec_chain_iterate_data_t *data); + +/*! + * \brief Check NSEC(3) bitmaps for updated nodes. + * + * \param nsec_ptrs Trie with nodes to be checked. + * \param data Validation context. + * + * \return KNOT_DNSSEC_ENSEC_BITMAP, KNOT_E* + */ +int nsec_check_bitmaps(zone_tree_t *nsec_ptrs, nsec_chain_iterate_data_t *data); + +/*! + * \brief Call a function for each piece of the chain formed by sorted nodes. + * + * \note If the callback function returns anything other than KNOT_EOK, the + * iteration is terminated and the error code is propagated. + * + * \param nodes Zone nodes. + * \param callback Callback function. + * \param data Custom data supplied to the callback function. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_nsec_chain_iterate_create(zone_tree_t *nodes, + chain_iterate_create_cb callback, + nsec_chain_iterate_data_t *data); + +/*! + * \brief Call the chain-connecting function for modified records and their neighbours. + * + * \param node_ptrs Tree of those nodes that have ben changed by the update. + * \param callback Callback function. + * \param cb_reconn Callback for re-connecting "next" link to another node. + * \param data Custom data supplied, incl. changeset to be updated. + * + * \retval KNOT_ENORECORD if the chain must be recreated from scratch. + * \return KNOT_E* + */ +int knot_nsec_chain_iterate_fix(zone_tree_t *node_ptrs, + chain_iterate_create_cb callback, + chain_iterate_create_cb cb_reconn, + nsec_chain_iterate_data_t *data); + +/*! + * \brief Add entry for removed NSEC(3) and its RRSIG to the changeset. + * + * \param n Node to extract NSEC(3) from. + * \param update Update to add the old RR removal into. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_nsec_changeset_remove(const zone_node_t *n, zone_update_t *update); + +/*! + * \brief Checks whether the node is empty or eventually contains only NSEC and + * RRSIGs. + * + * \param n Node to check. + * + * \retval true if the node is empty or contains only NSEC and RRSIGs. + * \retval false otherwise. + */ +bool knot_nsec_empty_nsec_and_rrsigs_in_node(const zone_node_t *n); + +/*! + * \brief Create new NSEC chain. + * + * \param update Zone update to create NSEC chain for. + * \param ttl TTL for created NSEC records. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_nsec_create_chain(zone_update_t *update, uint32_t ttl); + +/*! + * \brief Fix existing NSEC chain to cover the changes in zone contents. + * + * \param update Zone update to update NSEC chain for. + * \param ttl TTL for created NSEC records. + * + * \retval KNOT_ENORECORD if the chain must be recreated from scratch. + * \return KNOT_E* + */ +int knot_nsec_fix_chain(zone_update_t *update, uint32_t ttl); + +/*! + * \brief Validate NSEC chain in new_cont as whole. + * + * \note new_cont must have been adjusted already! + */ +int knot_nsec_check_chain(zone_update_t *update); + +/*! + * \brief Validate NSEC chain in new_cont incrementally. + */ +int knot_nsec_check_chain_fix(zone_update_t *update); diff --git a/src/knot/dnssec/nsec3-chain.c b/src/knot/dnssec/nsec3-chain.c new file mode 100644 index 0000000..97010be --- /dev/null +++ b/src/knot/dnssec/nsec3-chain.c @@ -0,0 +1,733 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "libknot/dname.h" +#include "knot/dnssec/nsec-chain.h" +#include "knot/dnssec/nsec3-chain.h" +#include "knot/dnssec/zone-sign.h" +#include "knot/dnssec/zone-nsec.h" +#include "knot/zone/adjust.h" +#include "knot/zone/zone-diff.h" +#include "contrib/base32hex.h" +#include "contrib/wire_ctx.h" + +static bool nsec3_empty(const zone_node_t *node, const dnssec_nsec3_params_t *params) +{ + bool opt_out = (params->flags & KNOT_NSEC3_FLAG_OPT_OUT); + return opt_out ? !(node->flags & NODE_FLAGS_SUBTREE_AUTH) : !(node->flags & NODE_FLAGS_SUBTREE_DATA); +} + +/*! + * \brief Check whether at least one RR type in node should be signed, + * used when signing with NSEC3. + * + * \param node Node for which the check is done. + * + * \return true/false. + */ +static bool node_should_be_signed_nsec3(const zone_node_t *n) +{ + for (int i = 0; i < n->rrset_count; i++) { + knot_rrset_t rrset = node_rrset_at(n, i); + if (rrset.type == KNOT_RRTYPE_NSEC || + rrset.type == KNOT_RRTYPE_RRSIG) { + continue; + } + + if (knot_zone_sign_rr_should_be_signed(n, &rrset)) { + return true; + } + } + + return false; +} + +/*! + * \brief Custom NSEC3 tree free function. + * + */ +static void free_nsec3_tree(zone_tree_t *nodes) +{ + assert(nodes); + + zone_tree_it_t it = { 0 }; + for ((void)zone_tree_it_begin(nodes, &it); !zone_tree_it_finished(&it); zone_tree_it_next(&it)) { + zone_node_t *node = zone_tree_it_val(&it); + // newly allocated NSEC3 nodes + knot_rdataset_t *nsec3 = node_rdataset(node, KNOT_RRTYPE_NSEC3); + knot_rdataset_t *rrsig = node_rdataset(node, KNOT_RRTYPE_RRSIG); + knot_rdataset_clear(nsec3, NULL); + knot_rdataset_clear(rrsig, NULL); + node_free(node, NULL); + } + + zone_tree_it_free(&it); + zone_tree_free(&nodes); +} + +/* - NSEC3 nodes construction ----------------------------------------------- */ + +/*! + * \brief Get NSEC3 RDATA size. + */ +static size_t nsec3_rdata_size(const dnssec_nsec3_params_t *params, + const dnssec_nsec_bitmap_t *rr_types) +{ + assert(params); + assert(rr_types); + + return 6 + params->salt.size + + dnssec_nsec3_hash_length(params->algorithm) + + dnssec_nsec_bitmap_size(rr_types); +} + +/*! + * \brief Fill NSEC3 RDATA. + * + * \note Content of next hash field is not changed. + */ +static int nsec3_fill_rdata(uint8_t *rdata, size_t rdata_len, + const dnssec_nsec3_params_t *params, + const dnssec_nsec_bitmap_t *rr_types, + const uint8_t *next_hashed) +{ + assert(rdata); + assert(params); + assert(rr_types); + + uint8_t hash_length = dnssec_nsec3_hash_length(params->algorithm); + + wire_ctx_t wire = wire_ctx_init(rdata, rdata_len); + + wire_ctx_write_u8(&wire, params->algorithm); + wire_ctx_write_u8(&wire, params->flags); + wire_ctx_write_u16(&wire, params->iterations); + wire_ctx_write_u8(&wire, params->salt.size); + wire_ctx_write(&wire, params->salt.data, params->salt.size); + wire_ctx_write_u8(&wire, hash_length); + + if (next_hashed != NULL) { + wire_ctx_write(&wire, next_hashed, hash_length); + } else { + wire_ctx_skip(&wire, hash_length); + } + + if (wire.error != KNOT_EOK) { + return wire.error; + } + + dnssec_nsec_bitmap_write(rr_types, wire.position); + + return KNOT_EOK; +} + +/*! + * \brief Creates NSEC3 RRSet. + * + * \param owner Owner for the RRSet. + * \param params Parsed NSEC3PARAM. + * \param rr_types Bitmap. + * \param next_hashed Next hashed. + * \param ttl TTL for the RRSet. + * + * \return Pointer to created RRSet on success, NULL on errors. + */ +static int create_nsec3_rrset(knot_rrset_t *rrset, + const knot_dname_t *owner, + const dnssec_nsec3_params_t *params, + const dnssec_nsec_bitmap_t *rr_types, + const uint8_t *next_hashed, + uint32_t ttl) +{ + assert(rrset); + assert(owner); + assert(params); + assert(rr_types); + + knot_dname_t *owner_copy = knot_dname_copy(owner, NULL); + if (owner_copy == NULL) { + return KNOT_ENOMEM; + } + knot_rrset_init(rrset, owner_copy, KNOT_RRTYPE_NSEC3, KNOT_CLASS_IN, ttl); + + size_t rdata_size = nsec3_rdata_size(params, rr_types); + uint8_t rdata[rdata_size]; + memset(rdata, 0, rdata_size); + int ret = nsec3_fill_rdata(rdata, rdata_size, params, rr_types, + next_hashed); + if (ret != KNOT_EOK) { + knot_dname_free(owner_copy, NULL); + return ret; + } + + ret = knot_rrset_add_rdata(rrset, rdata, rdata_size, NULL); + if (ret != KNOT_EOK) { + knot_dname_free(owner_copy, NULL); + return ret; + } + + return KNOT_EOK; +} + +/*! + * \brief Create NSEC3 node. + */ +static zone_node_t *create_nsec3_node(const knot_dname_t *owner, + const dnssec_nsec3_params_t *nsec3_params, + zone_node_t *apex_node, + const dnssec_nsec_bitmap_t *rr_types, + uint32_t ttl) +{ + assert(owner); + assert(nsec3_params); + assert(apex_node); + assert(rr_types); + + zone_node_t *new_node = node_new(owner, false, false, NULL); + if (!new_node) { + return NULL; + } + + knot_rrset_t nsec3_rrset; + int ret = create_nsec3_rrset(&nsec3_rrset, owner, nsec3_params, + rr_types, NULL, ttl); + if (ret != KNOT_EOK) { + node_free(new_node, NULL); + return NULL; + } + + ret = node_add_rrset(new_node, &nsec3_rrset, NULL); + knot_rrset_clear(&nsec3_rrset, NULL); + if (ret != KNOT_EOK) { + node_free(new_node, NULL); + return NULL; + } + + return new_node; +} + +/*! + * \brief Create new NSEC3 node for given regular node. + * + * \param node Node for which the NSEC3 node is created. + * \param apex Zone apex node. + * \param params NSEC3 hash function parameters. + * \param ttl TTL of the new NSEC3 node. + * + * \return Error code, KNOT_EOK if successful. + */ +static zone_node_t *create_nsec3_node_for_node(const zone_node_t *node, + zone_node_t *apex, + const dnssec_nsec3_params_t *params, + uint32_t ttl) +{ + assert(node); + assert(apex); + assert(params); + + knot_dname_storage_t nsec3_owner; + int ret = knot_create_nsec3_owner(nsec3_owner, sizeof(nsec3_owner), + node->owner, apex->owner, params); + if (ret != KNOT_EOK) { + return NULL; + } + + dnssec_nsec_bitmap_t *rr_types = dnssec_nsec_bitmap_new(); + if (!rr_types) { + return NULL; + } + + bitmap_add_node_rrsets(rr_types, node, false); + if (node->rrset_count > 0 && node_should_be_signed_nsec3(node)) { + dnssec_nsec_bitmap_add(rr_types, KNOT_RRTYPE_RRSIG); + } + if (node == apex) { + dnssec_nsec_bitmap_add(rr_types, KNOT_RRTYPE_NSEC3PARAM); + } + + zone_node_t *nsec3_node = create_nsec3_node(nsec3_owner, params, apex, + rr_types, ttl); + dnssec_nsec_bitmap_free(rr_types); + + return nsec3_node; +} + +/* - NSEC3 chain creation --------------------------------------------------- */ + +// see connect_nsec3_nodes() for what this function does +static int connect_nsec3_base(knot_rdataset_t *a_rrs, const knot_dname_t *b_name) +{ + assert(a_rrs); + uint8_t algorithm = knot_nsec3_alg(a_rrs->rdata); + if (algorithm == 0) { + return KNOT_EINVAL; + } + + uint8_t raw_length = knot_nsec3_next_len(a_rrs->rdata); + assert(raw_length == dnssec_nsec3_hash_length(algorithm)); + uint8_t *raw_hash = (uint8_t *)knot_nsec3_next(a_rrs->rdata); + if (raw_hash == NULL) { + return KNOT_EINVAL; + } + + assert(b_name); + uint8_t b32_length = b_name[0]; + const uint8_t *b32_hash = &(b_name[1]); + int32_t written = knot_base32hex_decode(b32_hash, b32_length, raw_hash, raw_length); + if (written != raw_length) { + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +/*! + * \brief Connect two nodes by filling 'hash' field of NSEC3 RDATA of the first node. + * + * \param a First node. Gets modified in-place! + * \param b Second node (immediate follower of a). + * \param data Unused parameter. + * + * \return Error code, KNOT_EOK if successful. + */ +static int connect_nsec3_nodes(zone_node_t *a, zone_node_t *b, + _unused_ nsec_chain_iterate_data_t *data) +{ + assert(a); + assert(b); + assert(a->rrset_count == 1); + + return connect_nsec3_base(node_rdataset(a, KNOT_RRTYPE_NSEC3), b->owner); +} + +/*! + * \brief Connect two nodes by updating the changeset. + * + * \param a First node. + * \param b Second node. + * \param data Contains the changeset to be updated. + * + * \return Error code, KNOT_EOK if successful. + */ +static int connect_nsec3_nodes2(zone_node_t *a, zone_node_t *b, + nsec_chain_iterate_data_t *data) +{ + assert(data); + + knot_rrset_t aorig = node_rrset(a, KNOT_RRTYPE_NSEC3); + assert(!knot_rrset_empty(&aorig)); + + // prepare a copy of NSEC3 rrsets in question + knot_rrset_t *acopy = knot_rrset_copy(&aorig, NULL); + if (acopy == NULL) { + return KNOT_ENOMEM; + } + + // connect the copied rrset + int ret = connect_nsec3_base(&acopy->rrs, b->owner); + if (ret != KNOT_EOK || knot_rrset_equal(&aorig, acopy, true)) { + knot_rrset_free(acopy, NULL); + return ret; + } + + // add the removed original and the updated copy to changeset + ret = zone_update_remove(data->update, &aorig); + if (ret == KNOT_EOK) { + ret = zone_update_add(data->update, acopy); + } + knot_rrset_free(acopy, NULL); + return ret; +} + +/*! + * \brief Replace the "next hash" field in b's NSEC3 by that in a's NSEC3, by updating the changeset. + * + * \param a A node to take the "next hash" from. + * \param b A node to put the "next hash" into. + * \param data Contains the changeset to be updated. + * + * \return KNOT_E* + */ +static int reconnect_nsec3_nodes2(zone_node_t *a, zone_node_t *b, + nsec_chain_iterate_data_t *data) +{ + assert(data); + + knot_rrset_t an = node_rrset(a, KNOT_RRTYPE_NSEC3); + assert(!knot_rrset_empty(&an)); + + knot_rrset_t bnorig = node_rrset(b, KNOT_RRTYPE_NSEC3); + assert(!knot_rrset_empty(&bnorig)); + + // prepare a copy of NSEC3 rrsets in question + knot_rrset_t *bnnew = knot_rrset_copy(&bnorig, NULL); + if (bnnew == NULL) { + return KNOT_ENOMEM; + } + + uint8_t raw_length = knot_nsec3_next_len(an.rrs.rdata); + uint8_t *a_hash = (uint8_t *)knot_nsec3_next(an.rrs.rdata); + uint8_t *bnew_hash = (uint8_t *)knot_nsec3_next(bnnew->rrs.rdata); + if (a_hash == NULL || bnew_hash == NULL || + raw_length != knot_nsec3_next_len(bnnew->rrs.rdata)) { + knot_rrset_free(bnnew, NULL); + return KNOT_ERROR; + } + memcpy(bnew_hash, a_hash, raw_length); + + int ret = zone_update_remove(data->update, &bnorig); + if (ret == KNOT_EOK) { + ret = zone_update_add(data->update, bnnew); + } + knot_rrset_free(bnnew, NULL); + return ret; +} + +/*! + * \brief Create NSEC3 node for each regular node in the zone. + * + * \param zone Zone. + * \param params NSEC3 params. + * \param ttl TTL for the created NSEC records. + * \param cds_in_apex Hint to guess apex node type bitmap: false=just DNSKEY, true=DNSKEY,CDS,CDNSKEY. + * \param nsec3_nodes Tree whereto new NSEC3 nodes will be added. + * \param update Zone update for possible NSEC removals + * + * \return Error code, KNOT_EOK if successful. + */ +static int create_nsec3_nodes(const zone_contents_t *zone, + const dnssec_nsec3_params_t *params, + uint32_t ttl, + zone_tree_t *nsec3_nodes, + zone_update_t *update) +{ + assert(zone); + assert(nsec3_nodes); + assert(update); + + zone_tree_delsafe_it_t it = { 0 }; + int result = zone_tree_delsafe_it_begin(zone->nodes, &it, false); // delsafe - removing nodes that contain only NSEC+RRSIG + + while (!zone_tree_delsafe_it_finished(&it)) { + zone_node_t *node = zone_tree_delsafe_it_val(&it); + + /*! + * Remove possible NSEC from the node. (Do not allow both NSEC + * and NSEC3 in the zone at once.) + */ + result = knot_nsec_changeset_remove(node, update); + if (result != KNOT_EOK) { + break; + } + if (node->flags & NODE_FLAGS_NONAUTH || nsec3_empty(node, params) || node->flags & NODE_FLAGS_DELETED) { + zone_tree_delsafe_it_next(&it); + continue; + } + + zone_node_t *nsec3_node; + nsec3_node = create_nsec3_node_for_node(node, zone->apex, + params, ttl); + if (!nsec3_node) { + result = KNOT_ENOMEM; + break; + } + + result = zone_tree_insert(nsec3_nodes, &nsec3_node); + if (result != KNOT_EOK) { + break; + } + + zone_tree_delsafe_it_next(&it); + } + + zone_tree_delsafe_it_free(&it); + + return result; +} + +/*! + * \brief For given dname, check if anything changed in zone_update, and recreate (possibly unconnected) NSEC3 nodes appropriately. + * + * \param update Zone update structure holding zone contents changes. + * \param params NSEC3 params. + * \param ttl TTL for newly created NSEC3 records. + * \param for_node Domain name of the node in question. + * + * \retval KNOT_ENORECORD if the NSEC3 chain shall be rather recreated completely. + * \return KNOT_EOK, KNOT_E* if any error. + */ +static int fix_nsec3_for_node(zone_update_t *update, const dnssec_nsec3_params_t *params, + uint32_t ttl, const knot_dname_t *for_node) +{ + // check if we need to do something + const zone_node_t *old_n = zone_contents_find_node(update->zone->contents, for_node); + const zone_node_t *new_n = zone_contents_find_node(update->new_cont, for_node); + + bool had_no_nsec = (old_n == NULL || old_n->nsec3_node == NULL || !(old_n->flags & NODE_FLAGS_NSEC3_NODE)); + bool shall_no_nsec = (new_n == NULL || new_n->flags & NODE_FLAGS_NONAUTH || nsec3_empty(new_n, params) || new_n->flags & NODE_FLAGS_DELETED); + + if (had_no_nsec == shall_no_nsec && node_bitmap_equal(old_n, new_n)) { + return KNOT_EOK; + } + + knot_dname_storage_t for_node_hashed; + int ret = knot_create_nsec3_owner(for_node_hashed, sizeof(for_node_hashed), + for_node, update->new_cont->apex->owner, params); + if (ret != KNOT_EOK) { + return ret; + } + + // saved hash of next node + uint8_t *next_hash = NULL; + uint8_t next_length = 0; + + // remove (all) existing NSEC3 + const zone_node_t *old_nsec3_n = zone_contents_find_nsec3_node(update->new_cont, for_node_hashed); + assert((bool)(old_nsec3_n == NULL) == had_no_nsec); + if (old_nsec3_n != NULL) { + knot_rrset_t rem_nsec3 = node_rrset(old_nsec3_n, KNOT_RRTYPE_NSEC3); + if (!knot_rrset_empty(&rem_nsec3)) { + knot_rrset_t rem_rrsig = node_rrset(old_nsec3_n, KNOT_RRTYPE_RRSIG); + ret = zone_update_remove(update, &rem_nsec3); + if (ret == KNOT_EOK && !knot_rrset_empty(&rem_rrsig)) { + ret = zone_update_remove(update, &rem_rrsig); + } + assert(update->flags & UPDATE_INCREMENTAL); // to make sure the following pointer remains valid + next_hash = (uint8_t *)knot_nsec3_next(rem_nsec3.rrs.rdata); + next_length = knot_nsec3_next_len(rem_nsec3.rrs.rdata); + } + } + + // add NSEC3 with correct bitmap + if (!shall_no_nsec && ret == KNOT_EOK) { + zone_node_t *new_nsec3_n = create_nsec3_node_for_node(new_n, update->new_cont->apex, params, ttl); + if (new_nsec3_n == NULL) { + return KNOT_ENOMEM; + } + knot_rrset_t nsec3 = node_rrset(new_nsec3_n, KNOT_RRTYPE_NSEC3); + assert(!knot_rrset_empty(&nsec3)); + + // copy hash of next element from removed record + if (next_hash != NULL) { + uint8_t *raw_hash = (uint8_t *)knot_nsec3_next(nsec3.rrs.rdata); + uint8_t raw_length = knot_nsec3_next_len(nsec3.rrs.rdata); + assert(raw_hash != NULL); + if (raw_length != next_length) { + ret = KNOT_EMALF; + } else { + memcpy(raw_hash, next_hash, raw_length); + } + } + if (ret == KNOT_EOK) { + ret = zone_update_add(update, &nsec3); + } + binode_unify(new_nsec3_n, false, NULL); + node_free_rrsets(new_nsec3_n, NULL); + node_free(new_nsec3_n, NULL); + } + + return ret; +} + +static int fix_nsec3_nodes(zone_update_t *update, const dnssec_nsec3_params_t *params, + uint32_t ttl) +{ + assert(update); + + zone_tree_it_t it = { 0 }; + int ret = zone_tree_it_begin(update->a_ctx->node_ptrs, &it); + + while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) { + zone_node_t *n = zone_tree_it_val(&it); + ret = fix_nsec3_for_node(update, params, ttl, n->owner); + zone_tree_it_next(&it); + } + zone_tree_it_free(&it); + + return ret; +} + +static int zone_update_nsec3_nodes(zone_update_t *up, zone_tree_t *nsec3n) +{ + int ret = KNOT_EOK; + zone_tree_delsafe_it_t dit = { 0 }; + zone_tree_it_t it = { 0 }; + if (up->new_cont->nsec3_nodes == NULL) { + goto add_nsec3n; + } + ret = zone_tree_delsafe_it_begin(up->new_cont->nsec3_nodes, &dit, false); + while (ret == KNOT_EOK && !zone_tree_delsafe_it_finished(&dit)) { + zone_node_t *nold = zone_tree_delsafe_it_val(&dit); + knot_rrset_t ns3old = node_rrset(nold, KNOT_RRTYPE_NSEC3); + zone_node_t *nnew = zone_tree_get(nsec3n, nold->owner); + if (!knot_rrset_empty(&ns3old)) { + knot_rrset_t ns3new = node_rrset(nnew, KNOT_RRTYPE_NSEC3); + if (knot_rrset_equal(&ns3old, &ns3new, true)) { + node_remove_rdataset(nnew, KNOT_RRTYPE_NSEC3); + } else { + ret = knot_nsec_changeset_remove(nold, up); + } + } else if (node_rrtype_exists(nold, KNOT_RRTYPE_RRSIG)) { + ret = knot_nsec_changeset_remove(nold, up); + } + zone_tree_delsafe_it_next(&dit); + } + zone_tree_delsafe_it_free(&dit); + if (ret != KNOT_EOK) { + return ret; + } + +add_nsec3n: + ret = zone_tree_it_begin(nsec3n, &it); + while (ret == KNOT_EOK && !zone_tree_it_finished(&it)) { + zone_node_t *nnew = zone_tree_it_val(&it); + knot_rrset_t ns3new = node_rrset(nnew, KNOT_RRTYPE_NSEC3); + if (!knot_rrset_empty(&ns3new)) { + ret = zone_update_add(up, &ns3new); + } + zone_tree_it_next(&it); + } + zone_tree_it_free(&it); + return ret; +} + +/* - Public API ------------------------------------------------------------- */ + +int delete_nsec3_chain(zone_update_t *up) +{ + zone_tree_t *empty = zone_tree_create(false); + if (empty == NULL) { + return KNOT_ENOMEM; + } + int ret = zone_update_nsec3_nodes(up, empty); + zone_tree_free(&empty); + return ret; +} + +/*! + * \brief Create new NSEC3 chain, add differences from current into a changeset. + */ +int knot_nsec3_create_chain(const zone_contents_t *zone, + const dnssec_nsec3_params_t *params, + uint32_t ttl, + zone_update_t *update) +{ + assert(zone); + assert(params); + + zone_tree_t *nsec3_nodes = zone_tree_create(false); + if (!nsec3_nodes) { + return KNOT_ENOMEM; + } + + int result = create_nsec3_nodes(zone, params, ttl, nsec3_nodes, update); + if (result != KNOT_EOK) { + free_nsec3_tree(nsec3_nodes); + return result; + } + + result = knot_nsec_chain_iterate_create(nsec3_nodes, + connect_nsec3_nodes, NULL); + if (result != KNOT_EOK) { + free_nsec3_tree(nsec3_nodes); + return result; + } + + result = zone_update_nsec3_nodes(update, nsec3_nodes); + + free_nsec3_tree(nsec3_nodes); + + return result; +} + +int knot_nsec3_fix_chain(zone_update_t *update, + const dnssec_nsec3_params_t *params, + uint32_t ttl) +{ + assert(update); + assert(params); + + // ensure that the salt has not changed + if (!knot_nsec3param_uptodate(update->new_cont, params)) { + int ret = knot_nsec3param_update(update, params, ttl); + if (ret != KNOT_EOK) { + return ret; + } + return knot_nsec3_create_chain(update->new_cont, params, ttl, update); + } + + int ret = fix_nsec3_nodes(update, params, ttl); + if (ret != KNOT_EOK) { + return ret; + } + + ret = zone_adjust_contents(update->new_cont, NULL, adjust_cb_void, false, true, 1, update->a_ctx->nsec3_ptrs); + if (ret != KNOT_EOK) { + return ret; + } + + // ensure that nsec3 node for zone root is in list of changed nodes + const zone_node_t *nsec3_for_root = NULL, *unused; + ret = zone_contents_find_nsec3_for_name(update->new_cont, update->zone->name, &nsec3_for_root, &unused); + if (ret >= 0) { + assert(ret == ZONE_NAME_FOUND); + assert(!(nsec3_for_root->flags & NODE_FLAGS_DELETED)); + assert(!(binode_counterpart((zone_node_t *)nsec3_for_root)->flags & NODE_FLAGS_DELETED)); + ret = zone_tree_insert(update->a_ctx->nsec3_ptrs, (zone_node_t **)&nsec3_for_root); + } + if (ret != KNOT_EOK) { + return ret; + } + + nsec_chain_iterate_data_t data = { ttl, update, KNOT_RRTYPE_NSEC3 }; + + ret = knot_nsec_chain_iterate_fix(update->a_ctx->nsec3_ptrs, + connect_nsec3_nodes2, reconnect_nsec3_nodes2, &data); + + return ret; +} + +int knot_nsec3_check_chain(zone_update_t *update, const dnssec_nsec3_params_t *params) +{ + nsec_chain_iterate_data_t data = { 0, update, KNOT_RRTYPE_NSEC3, params }; + + int ret = nsec_check_bitmaps(update->new_cont->nodes, &data); + if (ret != KNOT_EOK) { + return ret; + } + + return knot_nsec_chain_iterate_create(update->new_cont->nsec3_nodes, + nsec_check_connect_nodes, &data); +} + +int knot_nsec3_check_chain_fix(zone_update_t *update, const dnssec_nsec3_params_t *params) +{ + nsec_chain_iterate_data_t data = { 0, update, KNOT_RRTYPE_NSEC3, params }; + + int ret = nsec_check_bitmaps(update->a_ctx->node_ptrs, &data); + if (ret != KNOT_EOK) { + return ret; + } + + ret = nsec_check_bitmaps(update->a_ctx->adjust_ptrs, &data); // adjust_ptrs contain also NSEC3-nodes. See check_nsec_bitmap() how this is handled. + if (ret != KNOT_EOK) { + return ret; + } + + return nsec_check_new_connects(update->a_ctx->nsec3_ptrs, &data); +} diff --git a/src/knot/dnssec/nsec3-chain.h b/src/knot/dnssec/nsec3-chain.h new file mode 100644 index 0000000..5b3708f --- /dev/null +++ b/src/knot/dnssec/nsec3-chain.h @@ -0,0 +1,69 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> +#include "libdnssec/nsec.h" +#include "knot/updates/changesets.h" +#include "knot/updates/zone-update.h" +#include "knot/zone/contents.h" + +/*! + * \brief delete_nsec3_chain Delete all NSEC3 records and their RRSIGs. + */ +int delete_nsec3_chain(zone_update_t *up); + +/*! + * \brief Creates new NSEC3 chain, add differences from current into a changeset. + * + * \param zone Zone to be checked. + * \param params NSEC3 parameters. + * \param ttl TTL for new records. + * \param update Zone update to stare immediate changes into. + * + * \return KNOT_E* + */ +int knot_nsec3_create_chain(const zone_contents_t *zone, + const dnssec_nsec3_params_t *params, + uint32_t ttl, + zone_update_t *update); + +/*! + * \brief Updates zone's NSEC3 chain to follow the differences in zone update. + * + * \param update Zone Update structure holding the zone and its update. Also modified! + * \param params NSEC3 parameters. + * \param ttl TTL for new records. + * + * \retval KNOT_ENORECORD if the chain must be recreated from scratch. + * \return KNOT_E* + */ +int knot_nsec3_fix_chain(zone_update_t *update, + const dnssec_nsec3_params_t *params, + uint32_t ttl); + +/*! + * \brief Validate NSEC3 chain in new_cont as whole. + * + * \note new_cont must have been adjusted already! + */ +int knot_nsec3_check_chain(zone_update_t *update, const dnssec_nsec3_params_t *params); + +/*! + * \brief Validate NSEC3 chain in new_cont incrementally. + */ +int knot_nsec3_check_chain_fix(zone_update_t *update, const dnssec_nsec3_params_t *params); diff --git a/src/knot/dnssec/policy.c b/src/knot/dnssec/policy.c new file mode 100644 index 0000000..2589ae6 --- /dev/null +++ b/src/knot/dnssec/policy.c @@ -0,0 +1,51 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/dnssec/policy.h" +#include "libknot/rrtype/soa.h" + +static uint32_t zone_soa_ttl(const zone_contents_t *zone) +{ + knot_rrset_t soa = node_rrset(zone->apex, KNOT_RRTYPE_SOA); + return soa.ttl; +} + +void update_policy_from_zone(knot_kasp_policy_t *policy, + const zone_contents_t *zone) +{ + assert(policy); + assert(zone); + + if (policy->dnskey_ttl == UINT32_MAX) { + policy->dnskey_ttl = zone_soa_ttl(zone); + } + if (policy->saved_key_ttl == 0) { // possibly not set yet + policy->saved_key_ttl = policy->dnskey_ttl; + } + + if (policy->zone_maximal_ttl == UINT32_MAX) { + policy->zone_maximal_ttl = zone->max_ttl; + if (policy->rrsig_refresh_before == UINT32_MAX) { + policy->rrsig_refresh_before = policy->propagation_delay + + policy->zone_maximal_ttl; + } + } + if (policy->saved_max_ttl == 0) { // possibly not set yet + policy->saved_max_ttl = policy->zone_maximal_ttl; + } +} diff --git a/src/knot/dnssec/policy.h b/src/knot/dnssec/policy.h new file mode 100644 index 0000000..8c0149b --- /dev/null +++ b/src/knot/dnssec/policy.h @@ -0,0 +1,26 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/dnssec/context.h" +#include "knot/zone/contents.h" + +/*! + * \brief Update policy parameters depending on zone content. + */ +void update_policy_from_zone(knot_kasp_policy_t *policy, + const zone_contents_t *zone); diff --git a/src/knot/dnssec/rrset-sign.c b/src/knot/dnssec/rrset-sign.c new file mode 100644 index 0000000..3522a24 --- /dev/null +++ b/src/knot/dnssec/rrset-sign.c @@ -0,0 +1,425 @@ +/* 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 "contrib/wire_ctx.h" +#include "libdnssec/error.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-sign.h" +#include "knot/zone/serial.h" // DNS uint32 arithmetics +#include "libknot/libknot.h" + +#define RRSIG_RDATA_SIGNER_OFFSET 18 + +#define RRSIG_INCEPT_IN_PAST (90 * 60) + +/*- Creating of RRSIGs -------------------------------------------------------*/ + +/*! + * \brief Get size of RRSIG RDATA for a given key without signature. + */ +static size_t rrsig_rdata_header_size(const dnssec_key_t *key) +{ + if (!key) { + return 0; + } + + size_t size; + + // static part + + size = sizeof(uint16_t) // type covered + + sizeof(uint8_t) // algorithm + + sizeof(uint8_t) // labels + + sizeof(uint32_t) // original TTL + + sizeof(uint32_t) // signature expiration + + sizeof(uint32_t) // signature inception + + sizeof(uint16_t); // key tag (footprint) + + assert(size == RRSIG_RDATA_SIGNER_OFFSET); + + // variable part + + size += knot_dname_size(dnssec_key_get_dname(key)); + + return size; +} + +/*! + * \brief Write RRSIG RDATA except signature. + * + * \note This can be also used for SIG(0) if proper parameters are supplied. + * + * \param rdata_len Length of RDATA. + * \param rdata Pointer to RDATA. + * \param key Key used for signing. + * \param covered_type Type of the covered RR. + * \param owner_labels Number of labels covered by the signature. + * \param sig_incepted Timestamp of signature inception. + * \param sig_expires Timestamp of signature expiration. + */ +static int rrsig_write_rdata(uint8_t *rdata, size_t rdata_len, + const dnssec_key_t *key, + uint16_t covered_type, uint8_t owner_labels, + uint32_t owner_ttl, uint32_t sig_incepted, + uint32_t sig_expires) +{ + if (!rdata || !key || serial_compare(sig_incepted, sig_expires) != SERIAL_LOWER) { + return KNOT_EINVAL; + } + + uint8_t algorithm = dnssec_key_get_algorithm(key); + uint16_t keytag = dnssec_key_get_keytag(key); + const uint8_t *signer = dnssec_key_get_dname(key); + assert(signer); + + wire_ctx_t wire = wire_ctx_init(rdata, rdata_len); + + wire_ctx_write_u16(&wire, covered_type); // type covered + wire_ctx_write_u8(&wire, algorithm); // algorithm + wire_ctx_write_u8(&wire, owner_labels); // labels + wire_ctx_write_u32(&wire, owner_ttl); // original TTL + wire_ctx_write_u32(&wire, sig_expires); // signature expiration + wire_ctx_write_u32(&wire, sig_incepted); // signature inception + wire_ctx_write_u16(&wire, keytag); // key fingerprint + assert(wire_ctx_offset(&wire) == RRSIG_RDATA_SIGNER_OFFSET); + wire_ctx_write(&wire, signer, knot_dname_size(signer)); // signer + + return wire.error; +} + +/*- Computation of signatures ------------------------------------------------*/ + +/*! + * \brief Add RRSIG RDATA without signature to signing context. + * + * Requires signer name in RDATA in canonical form. + * + * \param ctx Signing context. + * \param rdata Pointer to RRSIG RDATA. + * + * \return Error code, KNOT_EOK if successful. + */ +static int sign_ctx_add_self(dnssec_sign_ctx_t *ctx, const uint8_t *rdata) +{ + assert(ctx); + assert(rdata); + + int result; + + // static header + + dnssec_binary_t header = { 0 }; + header.data = (uint8_t *)rdata; + header.size = RRSIG_RDATA_SIGNER_OFFSET; + + result = dnssec_sign_add(ctx, &header); + if (result != DNSSEC_EOK) { + return result; + } + + // signer name + + const uint8_t *rdata_signer = rdata + RRSIG_RDATA_SIGNER_OFFSET; + dnssec_binary_t signer = { 0 }; + signer.data = knot_dname_copy(rdata_signer, NULL); + signer.size = knot_dname_size(signer.data); + + result = dnssec_sign_add(ctx, &signer); + free(signer.data); + + return result; +} + +/*! + * \brief Add covered RRs to signing context. + * + * Requires all DNAMEs in canonical form and all RRs ordered canonically. + * + * \param ctx Signing context. + * \param covered Covered RRs. + * + * \return Error code, KNOT_EOK if successful. + */ +static int sign_ctx_add_records(dnssec_sign_ctx_t *ctx, const knot_rrset_t *covered) +{ + // huge block of rrsets can be optionally created + uint8_t *rrwf = malloc(KNOT_WIRE_MAX_PKTSIZE); + if (!rrwf) { + return KNOT_ENOMEM; + } + + int written = knot_rrset_to_wire(covered, rrwf, KNOT_WIRE_MAX_PKTSIZE, NULL); + if (written < 0) { + free(rrwf); + return written; + } + + dnssec_binary_t rrset_wire = { 0 }; + rrset_wire.size = written; + rrset_wire.data = rrwf; + int result = dnssec_sign_add(ctx, &rrset_wire); + free(rrwf); + + return result; +} + +int knot_sign_ctx_add_data(dnssec_sign_ctx_t *ctx, + const uint8_t *rrsig_rdata, + const knot_rrset_t *covered) +{ + if (!ctx || !rrsig_rdata || knot_rrset_empty(covered)) { + return KNOT_EINVAL; + } + + int result = sign_ctx_add_self(ctx, rrsig_rdata); + if (result != KNOT_EOK) { + return result; + } + + return sign_ctx_add_records(ctx, covered); +} + +/*! + * \brief Create RRSIG RDATA. + * + * \param[in] rrsigs RR set with RRSIGS. + * \param[in] ctx DNSSEC signing context. + * \param[in] covered RR covered by the signature. + * \param[in] key Key used for signing. + * \param[in] sig_incepted Timestamp of signature inception. + * \param[in] sig_expires Timestamp of signature expiration. + * \param[in] sign_flags Signing flags. + * \param[in] mm Memory context. + * + * \return Error code, KNOT_EOK if successful. + */ +static int rrsigs_create_rdata(knot_rrset_t *rrsigs, dnssec_sign_ctx_t *ctx, + const knot_rrset_t *covered, + const dnssec_key_t *key, + uint32_t sig_incepted, uint32_t sig_expires, + dnssec_sign_flags_t sign_flags, + knot_mm_t *mm) +{ + assert(rrsigs); + assert(rrsigs->type == KNOT_RRTYPE_RRSIG); + assert(!knot_rrset_empty(covered)); + assert(key); + + size_t header_size = rrsig_rdata_header_size(key); + assert(header_size != 0); + + uint8_t owner_labels = knot_dname_labels(covered->owner, NULL); + if (knot_dname_is_wildcard(covered->owner)) { + owner_labels -= 1; + } + + uint8_t header[header_size]; + int res = rrsig_write_rdata(header, header_size, + key, covered->type, owner_labels, + covered->ttl, sig_incepted, sig_expires); + assert(res == KNOT_EOK); + + res = dnssec_sign_init(ctx); + if (res != KNOT_EOK) { + return res; + } + + res = knot_sign_ctx_add_data(ctx, header, covered); + if (res != KNOT_EOK) { + return res; + } + + dnssec_binary_t signature = { 0 }; + res = dnssec_sign_write(ctx, sign_flags, &signature); + if (res != DNSSEC_EOK) { + return res; + } + assert(signature.size > 0); + + size_t rrsig_size = header_size + signature.size; + uint8_t rrsig[rrsig_size]; + memcpy(rrsig, header, header_size); + memcpy(rrsig + header_size, signature.data, signature.size); + + dnssec_binary_free(&signature); + + return knot_rrset_add_rdata(rrsigs, rrsig, rrsig_size, mm); +} + +int knot_sign_rrset(knot_rrset_t *rrsigs, const knot_rrset_t *covered, + const dnssec_key_t *key, dnssec_sign_ctx_t *sign_ctx, + const kdnssec_ctx_t *dnssec_ctx, knot_mm_t *mm, knot_time_t *expires) +{ + if (knot_rrset_empty(covered) || !key || !sign_ctx || !dnssec_ctx || + rrsigs->type != KNOT_RRTYPE_RRSIG || + !knot_dname_is_equal(rrsigs->owner, covered->owner) + ) { + return KNOT_EINVAL; + } + + uint64_t sig_incept = dnssec_ctx->now - RRSIG_INCEPT_IN_PAST; + uint64_t sig_expire = dnssec_ctx->now + dnssec_ctx->policy->rrsig_lifetime; + dnssec_sign_flags_t sign_flags = dnssec_ctx->policy->reproducible_sign ? + DNSSEC_SIGN_REPRODUCIBLE : DNSSEC_SIGN_NORMAL; + + int ret = rrsigs_create_rdata(rrsigs, sign_ctx, covered, key, (uint32_t)sig_incept, + (uint32_t)sig_expire, sign_flags, mm); + if (ret == KNOT_EOK && expires != NULL) { + *expires = knot_time_min(*expires, sig_expire); + } + return ret; +} + +int knot_sign_rrset2(knot_rrset_t *rrsigs, const knot_rrset_t *rrset, + zone_sign_ctx_t *sign_ctx, knot_mm_t *mm) +{ + if (rrsigs == NULL || rrset == NULL || sign_ctx == NULL) { + return KNOT_EINVAL; + } + + for (size_t i = 0; i < sign_ctx->count; i++) { + zone_key_t *key = &sign_ctx->keys[i]; + + if (!knot_zone_sign_use_key(key, rrset)) { + continue; + } + + int ret = knot_sign_rrset(rrsigs, rrset, key->key, sign_ctx->sign_ctxs[i], + sign_ctx->dnssec_ctx, mm, NULL); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +int knot_synth_rrsig(uint16_t type, const knot_rdataset_t *rrsig_rrs, + knot_rdataset_t *out_sig, knot_mm_t *mm) +{ + if (rrsig_rrs == NULL) { + return KNOT_ENOENT; + } + + if (out_sig == NULL || out_sig->count > 0) { + return KNOT_EINVAL; + } + + knot_rdata_t *rr_to_copy = rrsig_rrs->rdata; + for (int i = 0; i < rrsig_rrs->count; ++i) { + if (type == KNOT_RRTYPE_ANY) { + type = knot_rrsig_type_covered(rr_to_copy); + } + if (type == knot_rrsig_type_covered(rr_to_copy)) { + int ret = knot_rdataset_add(out_sig, rr_to_copy, mm); + if (ret != KNOT_EOK) { + knot_rdataset_clear(out_sig, mm); + return ret; + } + } + rr_to_copy = knot_rdataset_next(rr_to_copy); + } + + return out_sig->count > 0 ? KNOT_EOK : KNOT_ENOENT; +} + +bool knot_synth_rrsig_exists(uint16_t type, const knot_rdataset_t *rrsig_rrs) +{ + if (rrsig_rrs == NULL) { + return false; + } + + knot_rdata_t *rr = rrsig_rrs->rdata; + for (int i = 0; i < rrsig_rrs->count; ++i) { + if (type == knot_rrsig_type_covered(rr)) { + return true; + } + rr = knot_rdataset_next(rr); + } + + return false; +} + +/*- Verification of signatures -----------------------------------------------*/ + +static bool is_expired_signature(const knot_rdata_t *rrsig, knot_time_t now, + uint32_t refresh_before) +{ + assert(rrsig); + + uint32_t expire32 = knot_rrsig_sig_expiration(rrsig); + uint32_t incept32 = knot_rrsig_sig_inception(rrsig); + knot_time_t expire64 = knot_time_from_u32(expire32, now); + knot_time_t incept64 = knot_time_from_u32(incept32, now); + + return now >= expire64 - refresh_before || now < incept64; +} + +int knot_check_signature(const knot_rrset_t *covered, + const knot_rrset_t *rrsigs, size_t pos, + const dnssec_key_t *key, + dnssec_sign_ctx_t *sign_ctx, + const kdnssec_ctx_t *dnssec_ctx, + knot_timediff_t refresh, + bool skip_crypto) +{ + if (knot_rrset_empty(covered) || knot_rrset_empty(rrsigs) || !key || + !sign_ctx || !dnssec_ctx) { + return KNOT_EINVAL; + } + + knot_rdata_t *rrsig = knot_rdataset_at(&rrsigs->rrs, pos); + assert(rrsig); + + if (!(dnssec_ctx->policy->unsafe & UNSAFE_EXPIRED) && + is_expired_signature(rrsig, dnssec_ctx->now, refresh)) { + return DNSSEC_INVALID_SIGNATURE; + } + + if (skip_crypto) { + return KNOT_EOK; + } + + // identify fields in the signature being validated + + dnssec_binary_t signature = { + .size = knot_rrsig_signature_len(rrsig), + .data = (uint8_t *)knot_rrsig_signature(rrsig) + }; + if (signature.data == NULL) { + return KNOT_EINVAL; + } + + // perform the validation + + int result = dnssec_sign_init(sign_ctx); + if (result != KNOT_EOK) { + return result; + } + + result = knot_sign_ctx_add_data(sign_ctx, rrsig->data, covered); + if (result != KNOT_EOK) { + return result; + } + + bool sign_cmp = dnssec_algorithm_reproducible( + dnssec_ctx->policy->algorithm, + dnssec_ctx->policy->reproducible_sign); + + return dnssec_sign_verify(sign_ctx, sign_cmp, &signature); +} diff --git a/src/knot/dnssec/rrset-sign.h b/src/knot/dnssec/rrset-sign.h new file mode 100644 index 0000000..8e00402 --- /dev/null +++ b/src/knot/dnssec/rrset-sign.h @@ -0,0 +1,123 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libdnssec/key.h" +#include "libdnssec/sign.h" +#include "knot/dnssec/context.h" +#include "knot/dnssec/zone-keys.h" +#include "libknot/rrset.h" + +/*! + * \brief Create RRSIG RR for given RR set. + * + * \param rrsigs RR set with RRSIGs into which the result will be added. + * \param covered RR set to create a new signature for. + * \param key Signing key. + * \param sign_ctx Signing context. + * \param dnssec_ctx DNSSEC context. + * \param mm Memory context. + * \param expires Out: When will the new RRSIG expire. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_sign_rrset(knot_rrset_t *rrsigs, + const knot_rrset_t *covered, + const dnssec_key_t *key, + dnssec_sign_ctx_t *sign_ctx, + const kdnssec_ctx_t *dnssec_ctx, + knot_mm_t *mm, + knot_time_t *expires); + +/*! + * \brief Create RRSIG RR for given RR set, choose which key to use. + * + * \param rrsigs RR set with RRSIGs into which the result will be added. + * \param rrset RR set to create a new signature for. + * \param sign_ctx Zone signing context. + * \param mm Memory context. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_sign_rrset2(knot_rrset_t *rrsigs, + const knot_rrset_t *rrset, + zone_sign_ctx_t *sign_ctx, + knot_mm_t *mm); + +/*! + * \brief Add all data covered by signature into signing context. + * + * RFC 4034: The signature covers RRSIG RDATA field (excluding the signature) + * and all matching RR records, which are ordered canonically. + * + * Requires all DNAMEs in canonical form and all RRs ordered canonically. + * + * \param ctx Signing context. + * \param rrsig_rdata RRSIG RDATA with populated fields except signature. + * \param covered Covered RRs. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_sign_ctx_add_data(dnssec_sign_ctx_t *ctx, + const uint8_t *rrsig_rdata, + const knot_rrset_t *covered); + +/*! + * \brief Creates new RRS using \a rrsig_rrs as a source. Only those RRs that + * cover given \a type are copied into \a out_sig + * + * \note If given \a type is ANY, put a random subset, not all. + * + * \param type Covered type. + * \param rrsig_rrs Source RRS. + * \param out_sig Output RRS. + * \param mm Memory context. + * + * \retval KNOT_EOK if some RRSIG was found. + * \retval KNOT_EINVAL if no RRSIGs were found. + * \retval Error code other than EINVAL on error. + */ +int knot_synth_rrsig(uint16_t type, const knot_rdataset_t *rrsig_rrs, + knot_rdataset_t *out_sig, knot_mm_t *mm); + +/*! + * \brief Determines if a RRSIG exists, covering the specified type. + */ +bool knot_synth_rrsig_exists(uint16_t type, const knot_rdataset_t *rrsig_rrs); + +/*! + * \brief Check if RRSIG signature is valid. + * + * \param covered RRs covered by the signature. + * \param rrsigs RR set with RRSIGs. + * \param pos Number of RRSIG RR in 'rrsigs' to be validated. + * \param key Signing key. + * \param sign_ctx Signing context. + * \param dnssec_ctx DNSSEC context. + * \param refresh Consider RRSIG expired when gonna expire this soon. + * \param skip_crypto All RRSIGs in this node have been verified, just check validity. + * + * \return Error code, KNOT_EOK if successful and the signature is valid. + * \retval KNOT_DNSSEC_EINVALID_SIGNATURE The signature is invalid. + */ +int knot_check_signature(const knot_rrset_t *covered, + const knot_rrset_t *rrsigs, size_t pos, + const dnssec_key_t *key, + dnssec_sign_ctx_t *sign_ctx, + const kdnssec_ctx_t *dnssec_ctx, + knot_timediff_t refresh, + bool skip_crypto); diff --git a/src/knot/dnssec/zone-events.c b/src/knot/dnssec/zone-events.c new file mode 100644 index 0000000..22a2a76 --- /dev/null +++ b/src/knot/dnssec/zone-events.c @@ -0,0 +1,470 @@ +/* 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 "libdnssec/error.h" +#include "libdnssec/random.h" +#include "libknot/libknot.h" +#include "knot/conf/conf.h" +#include "knot/common/log.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/key_records.h" +#include "knot/dnssec/policy.h" +#include "knot/dnssec/zone-events.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/dnssec/zone-nsec.h" +#include "knot/dnssec/zone-sign.h" +#include "knot/zone/adjust.h" +#include "knot/zone/digest.h" + +static knot_time_t schedule_next(kdnssec_ctx_t *kctx, const zone_keyset_t *keyset, + knot_time_t keys_expire, knot_time_t rrsigs_expire) +{ + knot_time_t rrsigs_refresh = knot_time_add(rrsigs_expire, -(knot_timediff_t)kctx->policy->rrsig_refresh_before); + knot_time_t zone_refresh = knot_time_min(keys_expire, rrsigs_refresh); + + knot_time_t dnskey_update = knot_get_next_zone_key_event(keyset); + knot_time_t next = knot_time_min(zone_refresh, dnskey_update); + + return next; +} + +static int generate_salt(dnssec_binary_t *salt, uint16_t length) +{ + assert(salt); + dnssec_binary_t new_salt = { 0 }; + + if (length > 0) { + int r = dnssec_binary_alloc(&new_salt, length); + if (r != KNOT_EOK) { + return knot_error_from_libdnssec(r); + } + + r = dnssec_random_binary(&new_salt); + if (r != KNOT_EOK) { + dnssec_binary_free(&new_salt); + return knot_error_from_libdnssec(r); + } + } + + dnssec_binary_free(salt); + *salt = new_salt; + + return KNOT_EOK; +} + +int knot_dnssec_nsec3resalt(kdnssec_ctx_t *ctx, bool soa_rrsigs_ok, + knot_time_t *salt_changed, knot_time_t *when_resalt) +{ + int ret = KNOT_EOK; + + if (!ctx->policy->nsec3_enabled) { + return KNOT_EOK; + } + + if (ctx->policy->nsec3_salt_lifetime < 0 && !soa_rrsigs_ok) { + *when_resalt = ctx->now; + } else if (ctx->zone->nsec3_salt.size != ctx->policy->nsec3_salt_length || ctx->zone->nsec3_salt_created == 0) { + *when_resalt = ctx->now; + } else if (knot_time_cmp(ctx->now, ctx->zone->nsec3_salt_created) < 0) { + return KNOT_EINVAL; + } else if (ctx->policy->nsec3_salt_lifetime > 0) { + *when_resalt = knot_time_plus(ctx->zone->nsec3_salt_created, ctx->policy->nsec3_salt_lifetime); + } + + if (knot_time_cmp(*when_resalt, ctx->now) <= 0) { + if (ctx->policy->nsec3_salt_length == 0) { + ctx->zone->nsec3_salt.size = 0; + ctx->zone->nsec3_salt_created = ctx->now; + *salt_changed = ctx->now; + *when_resalt = 0; + return kdnssec_ctx_commit(ctx); + } + + ret = generate_salt(&ctx->zone->nsec3_salt, ctx->policy->nsec3_salt_length); + if (ret == KNOT_EOK) { + ctx->zone->nsec3_salt_created = ctx->now; + ret = kdnssec_ctx_commit(ctx); + *salt_changed = ctx->now; + } + // continue to planning next resalt even if NOK + if (ctx->policy->nsec3_salt_lifetime > 0) { + *when_resalt = knot_time_plus(ctx->now, ctx->policy->nsec3_salt_lifetime); + } + } + + return ret; +} + +static int check_offline_records(kdnssec_ctx_t *ctx) +{ + if (!ctx->policy->offline_ksk) { + return KNOT_EOK; + } + + if (ctx->offline_records.dnskey.rrs.count == 0 || + ctx->offline_records.rrsig.rrs.count == 0) { + log_zone_error(ctx->zone->dname, + "DNSSEC, no offline KSK records available"); + return KNOT_ENOENT; + } + + int ret; + knot_time_t last; + if (ctx->offline_next_time == 0) { + log_zone_warning(ctx->zone->dname, + "DNSSEC, using last offline KSK records available, " + "import new SKR before RRSIGs expire"); + } else if ((ret = key_records_last_timestamp(ctx, &last)) != KNOT_EOK) { + log_zone_error(ctx->zone->dname, + "DNSSEC, failed to load offline KSK records (%s)", + knot_strerror(ret)); + } else if (knot_time_diff(last, ctx->now) < 7 * 24 * 3600) { + log_zone_notice(ctx->zone->dname, + "DNSSEC, having offline KSK records for less than " + "a week, import new SKR"); + } + + return KNOT_EOK; +} + +int knot_dnssec_zone_sign(zone_update_t *update, + conf_t *conf, + zone_sign_flags_t flags, + zone_sign_roll_flags_t roll_flags, + knot_time_t adjust_now, + zone_sign_reschedule_t *reschedule) +{ + if (!update || !reschedule) { + return KNOT_EINVAL; + } + + const knot_dname_t *zone_name = update->new_cont->apex->owner; + kdnssec_ctx_t ctx = { 0 }; + zone_keyset_t keyset = { 0 }; + knot_time_t zone_expire = 0; + + int result = kdnssec_ctx_init(conf, &ctx, zone_name, zone_kaspdb(update->zone), NULL); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to initialize signing context (%s)", + knot_strerror(result)); + return result; + } + if (adjust_now) { + ctx.now = adjust_now; + } + + // update policy based on the zone content + update_policy_from_zone(ctx.policy, update->new_cont); + + if (ctx.policy->rrsig_refresh_before < ctx.policy->zone_maximal_ttl + ctx.policy->propagation_delay) { + log_zone_error(zone_name, "DNSSEC, rrsig-refresh too low to prevent expired RRSIGs in resolver caches"); + result = KNOT_EINVAL; + goto done; + } + if (ctx.policy->rrsig_lifetime <= ctx.policy->rrsig_refresh_before) { + log_zone_error(zone_name, "DNSSEC, rrsig-lifetime lower than rrsig-refresh"); + result = KNOT_EINVAL; + goto done; + } + + // perform key rollover if needed + result = knot_dnssec_key_rollover(&ctx, roll_flags, reschedule); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to update key set (%s)", + knot_strerror(result)); + goto done; + } + + ctx.rrsig_drop_existing = flags & ZONE_SIGN_DROP_SIGNATURES; + + conf_val_t val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone_name); + unsigned zonemd_alg = conf_opt(&val); + if (zonemd_alg != ZONE_DIGEST_NONE) { + result = zone_update_add_digest(update, zonemd_alg, true); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to reserve dummy ZONEMD (%s)", + knot_strerror(result)); + goto done; + } + } + + uint32_t ms; + if (zone_is_slave(conf, update->zone) && zone_get_master_serial(update->zone, &ms) == KNOT_ENOENT) { + // zone had been XFRed before on-slave-signing turned on + zone_set_master_serial(update->zone, zone_contents_serial(update->new_cont)); + } + + result = load_zone_keys(&ctx, &keyset, true); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to load keys (%s)", + knot_strerror(result)); + goto done; + } + + // perform nsec3resalt if pending + if (roll_flags & KEY_ROLL_ALLOW_NSEC3RESALT) { + knot_rdataset_t *rrsig = node_rdataset(update->new_cont->apex, KNOT_RRTYPE_RRSIG); + bool issbaz = is_soa_signed_by_all_zsks(&keyset, rrsig); + result = knot_dnssec_nsec3resalt(&ctx, issbaz, &reschedule->last_nsec3resalt, &reschedule->next_nsec3resalt); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to update NSEC3 salt (%s)", + knot_strerror(result)); + goto done; + } + } + + log_zone_info(zone_name, "DNSSEC, signing started"); + + result = knot_zone_sign_update_dnskeys(update, &keyset, &ctx); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to update DNSKEY records (%s)", + knot_strerror(result)); + goto done; + } + + result = zone_adjust_contents(update->new_cont, adjust_cb_flags, NULL, + false, false, 1, update->a_ctx->node_ptrs); + if (result != KNOT_EOK) { + return result; + } + + result = knot_zone_create_nsec_chain(update, &ctx); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to create NSEC%s chain (%s)", + ctx.policy->nsec3_enabled ? "3" : "", + knot_strerror(result)); + goto done; + } + + result = check_offline_records(&ctx); + if (result != KNOT_EOK) { + goto done; + } + + result = knot_zone_sign(update, &keyset, &ctx, &zone_expire); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to sign zone content (%s)", + knot_strerror(result)); + goto done; + } + + // SOA finishing + + if (zone_update_no_change(update)) { + log_zone_info(zone_name, "DNSSEC, zone is up-to-date"); + update->zone->zonefile.resigned = false; + goto done; + } else { + update->zone->zonefile.resigned = true; + } + + if (!(flags & ZONE_SIGN_KEEP_SERIAL) && zone_update_to(update) == NULL) { + result = zone_update_increment_soa(update, conf); + if (result == KNOT_EOK) { + result = knot_zone_sign_apex_rr(update, KNOT_RRTYPE_SOA, &keyset, &ctx); + } + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to update SOA record (%s)", + knot_strerror(result)); + goto done; + } + } + + if (zonemd_alg != ZONE_DIGEST_NONE) { + result = zone_update_add_digest(update, zonemd_alg, false); + if (result == KNOT_EOK) { + result = knot_zone_sign_apex_rr(update, KNOT_RRTYPE_ZONEMD, &keyset, &ctx); + } + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to update ZONEMD record (%s)", + knot_strerror(result)); + goto done; + } + } + + log_zone_info(zone_name, "DNSSEC, successfully signed"); + +done: + if (result == KNOT_EOK) { + reschedule->next_sign = schedule_next(&ctx, &keyset, ctx.offline_next_time, zone_expire); + } else { + reschedule->next_sign = knot_dnssec_failover_delay(&ctx); + reschedule->next_rollover = 0; + } + + free_zone_keys(&keyset); + kdnssec_ctx_deinit(&ctx); + + return result; +} + +int knot_dnssec_sign_update(zone_update_t *update, conf_t *conf) +{ + if (update == NULL || conf == NULL) { + return KNOT_EINVAL; + } + + const knot_dname_t *zone_name = update->new_cont->apex->owner; + kdnssec_ctx_t ctx = { 0 }; + zone_keyset_t keyset = { 0 }; + knot_time_t zone_expire = 0; + + int result = kdnssec_ctx_init(conf, &ctx, zone_name, zone_kaspdb(update->zone), NULL); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to initialize signing context (%s)", + knot_strerror(result)); + return result; + } + + update_policy_from_zone(ctx.policy, update->new_cont); + + conf_val_t val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone_name); + unsigned zonemd_alg = conf_opt(&val); + if (zonemd_alg != ZONE_DIGEST_NONE) { + result = zone_update_add_digest(update, zonemd_alg, true); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to reserve dummy ZONEMD (%s)", + knot_strerror(result)); + goto done; + } + } + + result = load_zone_keys(&ctx, &keyset, false); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to load keys (%s)", + knot_strerror(result)); + goto done; + } + + if (zone_update_changes_dnskey(update)) { + result = knot_zone_sign_update_dnskeys(update, &keyset, &ctx); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to update DNSKEY records (%s)", + knot_strerror(result)); + goto done; + } + } + + result = zone_adjust_contents(update->new_cont, adjust_cb_flags, NULL, + false, false, 1, update->a_ctx->node_ptrs); + if (result != KNOT_EOK) { + goto done; + } + + result = check_offline_records(&ctx); + if (result != KNOT_EOK) { + goto done; + } + + result = knot_zone_sign_update(update, &keyset, &ctx, &zone_expire); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to sign changeset (%s)", + knot_strerror(result)); + goto done; + } + + result = knot_zone_fix_nsec_chain(update, &keyset, &ctx); + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to fix NSEC%s chain (%s)", + ctx.policy->nsec3_enabled ? "3" : "", + knot_strerror(result)); + goto done; + } + + bool soa_changed = (knot_soa_serial(node_rdataset(update->zone->contents->apex, KNOT_RRTYPE_SOA)->rdata) != + knot_soa_serial(node_rdataset(update->new_cont->apex, KNOT_RRTYPE_SOA)->rdata)); + + if (zone_update_no_change(update) && !soa_changed) { + log_zone_info(zone_name, "DNSSEC, zone is up-to-date"); + update->zone->zonefile.resigned = false; + goto done; + } else { + update->zone->zonefile.resigned = true; + } + + if (!soa_changed) { + // incrementing SOA just of it has not been modified by the update + result = zone_update_increment_soa(update, conf); + } + if (result == KNOT_EOK) { + result = knot_zone_sign_apex_rr(update, KNOT_RRTYPE_SOA, &keyset, &ctx); + } + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to update SOA record (%s)", + knot_strerror(result)); + goto done; + } + + if (zonemd_alg != ZONE_DIGEST_NONE) { + result = zone_update_add_digest(update, zonemd_alg, false); + if (result == KNOT_EOK) { + result = knot_zone_sign_apex_rr(update, KNOT_RRTYPE_ZONEMD, &keyset, &ctx); + } + if (result != KNOT_EOK) { + log_zone_error(zone_name, "DNSSEC, failed to update ZONEMD record (%s)", + knot_strerror(result)); + goto done; + } + } + + log_zone_info(zone_name, "DNSSEC, incrementally signed"); + +done: + if (result == KNOT_EOK) { + knot_time_t next = knot_time_min(ctx.offline_next_time, zone_expire); + // NOTE: this is usually NOOP since signing planned earlier + zone_events_schedule_at(update->zone, ZONE_EVENT_DNSSEC, next ? next : -1); + } + + free_zone_keys(&keyset); + kdnssec_ctx_deinit(&ctx); + + return result; +} + +knot_time_t knot_dnssec_failover_delay(const kdnssec_ctx_t *ctx) +{ + if (ctx->policy == NULL) { + return ctx->now + 3600; // failed before allocating ctx->policy, use default + } else { + return ctx->now + ctx->policy->rrsig_prerefresh; + } +} + +int knot_dnssec_validate_zone(zone_update_t *update, conf_t *conf, knot_time_t now, bool incremental) +{ + kdnssec_ctx_t ctx = { 0 }; + int ret = kdnssec_validation_ctx(conf, &ctx, update->new_cont); + if (now != 0) { + ctx.now = now; + } + if (ret == KNOT_EOK) { + ret = knot_zone_check_nsec_chain(update, &ctx, incremental); + } + if (ret == KNOT_EOK) { + knot_time_t unused = 0; + assert(ctx.validation_mode); + if (incremental) { + ret = knot_zone_sign_update(update, NULL, &ctx, &unused); + } else { + ret = knot_zone_sign(update, NULL, &ctx, &unused); + } + } + kdnssec_ctx_deinit(&ctx); + return ret; +} diff --git a/src/knot/dnssec/zone-events.h b/src/knot/dnssec/zone-events.h new file mode 100644 index 0000000..d3667f3 --- /dev/null +++ b/src/knot/dnssec/zone-events.h @@ -0,0 +1,134 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <time.h> + +#include "knot/zone/zone.h" +#include "knot/updates/changesets.h" +#include "knot/updates/zone-update.h" +#include "knot/dnssec/context.h" + +enum zone_sign_flags { + ZONE_SIGN_NONE = 0, + ZONE_SIGN_DROP_SIGNATURES = (1 << 0), + ZONE_SIGN_KEEP_SERIAL = (1 << 1), +}; + +typedef enum zone_sign_flags zone_sign_flags_t; + +typedef enum { + KEY_ROLL_ALLOW_KSK_ROLL = (1 << 0), + KEY_ROLL_FORCE_KSK_ROLL = (1 << 1), + KEY_ROLL_ALLOW_ZSK_ROLL = (1 << 2), + KEY_ROLL_FORCE_ZSK_ROLL = (1 << 3), + KEY_ROLL_ALLOW_NSEC3RESALT = (1 << 4), + KEY_ROLL_ALLOW_ALL = KEY_ROLL_ALLOW_KSK_ROLL | + KEY_ROLL_ALLOW_ZSK_ROLL | + KEY_ROLL_ALLOW_NSEC3RESALT +} zone_sign_roll_flags_t; + +typedef struct { + knot_time_t next_sign; + knot_time_t next_rollover; + knot_time_t next_nsec3resalt; + knot_time_t last_nsec3resalt; + bool keys_changed; + bool plan_ds_check; +} zone_sign_reschedule_t; + +/*! + * \brief Generate/rollover keys in keystore as needed. + * + * \param kctx Pointers to the keytore, policy, etc. + * \param zone_name Zone name. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_dnssec_sign_process_events(const kdnssec_ctx_t *kctx, + const knot_dname_t *zone_name); + +/*! + * \brief DNSSEC re-sign zone, store new records into changeset. Valid signatures + * and NSEC(3) records will not be changed. + * + * \param update Zone Update structure with current zone contents to be updated by signing. + * \param conf Knot configuration. + * \param flags Zone signing flags. + * \param roll_flags Key rollover flags. + * \param adjust_now If not zero: adjust "now" to this timestamp. + * \param reschedule Signature refresh time of the oldest signature in zone. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_dnssec_zone_sign(zone_update_t *update, + conf_t *conf, + zone_sign_flags_t flags, + zone_sign_roll_flags_t roll_flags, + knot_time_t adjust_now, + zone_sign_reschedule_t *reschedule); + +/*! + * \brief Sign changeset (inside incremental Zone Update) created by DDNS or so... + * + * \param update Zone Update structure with current zone contents, changes to be signed and to be updated with signatures. + * \param conf Knot configuration. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_dnssec_sign_update(zone_update_t *update, conf_t *conf); + +/*! + * \brief Create new NCES3 salt if the old one is too old, and plan next resalt. + * + * For given zone, check NSEC3 salt in KASP db and decide if it shall be recreated + * and tell the user the next time it shall be called. + * + * This function is optimized to be called from NSEC3RESALT_EVENT, + * but also during zone load so that the zone gets loaded already with + * proper DNSSEC chain. + * + * \param ctx zone signing context + * \param soa_rrsigs_ok Zone is signed by current active ZSKs. + * \param salt_changed output if KNOT_EOK: when was the salt last changed? (either ctx->now or 0) + * \param when_resalt output: timestamp when next resalt takes place + * + * \return KNOT_E* + */ +int knot_dnssec_nsec3resalt(kdnssec_ctx_t *ctx, bool soa_rrsigs_ok, + knot_time_t *salt_changed, knot_time_t *when_resalt); + +/*! + * \brief When DNSSEC signing failed, re-plan on this time. + * + * \param ctx zone signing context + * + * \return Timestamp of next signing attempt. + */ +knot_time_t knot_dnssec_failover_delay(const kdnssec_ctx_t *ctx); + +/*! + * \brief Validate zone DNSSEC based on its contents. + * + * \param update Zone update with contents. + * \param conf Knot configuration. + * \param now If not zero: adjust "now" to this timestamp. + * \param incremental Try to validate incrementally. + * + * \return KNOT_E* + */ +int knot_dnssec_validate_zone(zone_update_t *update, conf_t *conf, knot_time_t now, bool incremental); diff --git a/src/knot/dnssec/zone-keys.c b/src/knot/dnssec/zone-keys.c new file mode 100644 index 0000000..a76c4be --- /dev/null +++ b/src/knot/dnssec/zone-keys.c @@ -0,0 +1,767 @@ +/* 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 <limits.h> +#include <stdio.h> + +#include "libdnssec/error.h" +#include "knot/common/log.h" +#include "knot/dnssec/zone-keys.h" +#include "libknot/libknot.h" +#include "contrib/openbsd/strlcat.h" + +#define MAX_KEY_INFO 128 + +typedef struct { + char msg[MAX_KEY_INFO]; + knot_time_t key_time; +} key_info_t; + +knot_dynarray_define(keyptr, zone_key_t *, DYNARRAY_VISIBILITY_NORMAL) + +void normalize_generate_flags(kdnssec_generate_flags_t *flags) +{ + if (!(*flags & DNSKEY_GENERATE_KSK) && !(*flags & DNSKEY_GENERATE_ZSK)) { + *flags |= DNSKEY_GENERATE_ZSK; + } + if (!(*flags & DNSKEY_GENERATE_SEP_SPEC)) { + if ((*flags & DNSKEY_GENERATE_KSK)) { + *flags |= DNSKEY_GENERATE_SEP_ON; + } else { + *flags &= ~DNSKEY_GENERATE_SEP_ON; + } + } +} + +static int generate_dnssec_key(dnssec_keystore_t *keystore, + const knot_dname_t *zone_name, + const char *key_label, + dnssec_key_algorithm_t alg, + unsigned size, + kdnssec_generate_flags_t flags, + char **id, + dnssec_key_t **key) +{ + *key = NULL; + *id = NULL; + + int ret = dnssec_keystore_generate(keystore, alg, size, key_label, id); + if (ret != KNOT_EOK) { + return ret; + } + + ret = dnssec_key_new(key); + if (ret != KNOT_EOK) { + goto fail; + } + + ret = dnssec_key_set_dname(*key, zone_name); + if (ret != KNOT_EOK) { + goto fail; + } + + dnssec_key_set_flags(*key, dnskey_flags(flags & DNSKEY_GENERATE_SEP_ON)); + dnssec_key_set_algorithm(*key, alg); + + ret = dnssec_keystore_get_private(keystore, *id, *key); + if (ret != KNOT_EOK) { + goto fail; + } + + return KNOT_EOK; + +fail: + dnssec_key_free(*key); + *key = NULL; + free(*id); + *id = NULL; + return ret; +} + +static bool keytag_in_use(kdnssec_ctx_t *ctx, uint16_t keytag) +{ + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + uint16_t used = dnssec_key_get_keytag(ctx->zone->keys[i].key); + if (used == keytag) { + return true; + } + } + return false; +} + +#define GENERATE_KEYTAG_ATTEMPTS (20) + +static int generate_keytag_unconflict(kdnssec_ctx_t *ctx, + kdnssec_generate_flags_t flags, + char **id, + dnssec_key_t **key) +{ + unsigned size = (flags & DNSKEY_GENERATE_KSK) ? ctx->policy->ksk_size : + ctx->policy->zsk_size; + + const char *label = NULL; + + char label_buf[sizeof(knot_dname_txt_storage_t) + 16]; + if (ctx->policy->key_label && + knot_dname_to_str(label_buf, ctx->zone->dname, sizeof(label_buf)) != NULL) { + const char *key_type = (flags & DNSKEY_GENERATE_KSK) ? " KSK" : " ZSK" ; + strlcat(label_buf, key_type, sizeof(label_buf)); + label = label_buf; + } + + for (size_t i = 0; i < GENERATE_KEYTAG_ATTEMPTS; i++) { + dnssec_key_free(*key); + free(*id); + + int ret = generate_dnssec_key(ctx->keystore, ctx->zone->dname, label, + ctx->policy->algorithm, size, flags, + id, key); + if (ret != KNOT_EOK) { + return ret; + } + if (!keytag_in_use(ctx, dnssec_key_get_keytag(*key))) { + return KNOT_EOK; + } + } + + log_zone_notice(ctx->zone->dname, "generated key with conflicting keytag %hu", + dnssec_key_get_keytag(*key)); + return KNOT_EOK; +} + +int kdnssec_generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags, + knot_kasp_key_t **key_ptr) +{ + assert(ctx); + assert(ctx->zone); + assert(ctx->keystore); + assert(ctx->policy); + + normalize_generate_flags(&flags); + + // generate key in the keystore + + char *id = NULL; + dnssec_key_t *dnskey = NULL; + + int r = generate_keytag_unconflict(ctx, flags, &id, &dnskey); + if (r != KNOT_EOK) { + return r; + } + + knot_kasp_key_t *key = calloc(1, sizeof(*key)); + if (!key) { + dnssec_key_free(dnskey); + free(id); + return KNOT_ENOMEM; + } + + key->id = id; + key->key = dnskey; + key->is_ksk = (flags & DNSKEY_GENERATE_KSK); + key->is_zsk = (flags & DNSKEY_GENERATE_ZSK); + key->timing.created = ctx->now; + + r = kasp_zone_append(ctx->zone, key); + free(key); + if (r != KNOT_EOK) { + dnssec_key_free(dnskey); + free(id); + return r; + } + + if (key_ptr) { + *key_ptr = &ctx->zone->keys[ctx->zone->num_keys - 1]; + } + + return KNOT_EOK; +} + +int kdnssec_share_key(kdnssec_ctx_t *ctx, const knot_dname_t *from_zone, const char *key_id) +{ + knot_dname_t *to_zone = knot_dname_copy(ctx->zone->dname, NULL); + if (to_zone == NULL) { + return KNOT_ENOMEM; + } + + int ret = kdnssec_ctx_commit(ctx); + if (ret != KNOT_EOK) { + free(to_zone); + return ret; + } + + ret = kasp_db_share_key(ctx->kasp_db, from_zone, ctx->zone->dname, key_id); + if (ret != KNOT_EOK) { + free(to_zone); + return ret; + } + + kasp_zone_clear(ctx->zone); + ret = kasp_zone_load(ctx->zone, to_zone, ctx->kasp_db, + &ctx->keytag_conflict); + free(to_zone); + return ret; +} + +int kdnssec_delete_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key_ptr) +{ + assert(ctx); + assert(ctx->zone); + assert(ctx->keystore); + assert(ctx->policy); + + ssize_t key_index = key_ptr - ctx->zone->keys; + + if (key_index < 0 || key_index >= ctx->zone->num_keys) { + return KNOT_EINVAL; + } + + bool key_still_used_in_keystore = false; + int ret = kasp_db_delete_key(ctx->kasp_db, ctx->zone->dname, key_ptr->id, &key_still_used_in_keystore); + if (ret != KNOT_EOK) { + return ret; + } + + if (!key_still_used_in_keystore && !key_ptr->is_pub_only) { + ret = dnssec_keystore_remove(ctx->keystore, key_ptr->id); + if (ret != KNOT_EOK) { + return ret; + } + } + + dnssec_key_free(key_ptr->key); + free(key_ptr->id); + memmove(key_ptr, key_ptr + 1, (ctx->zone->num_keys - key_index - 1) * sizeof(*key_ptr)); + ctx->zone->num_keys--; + return KNOT_EOK; +} + +static bool is_published(knot_kasp_key_timing_t *timing, knot_time_t now) +{ + return (knot_time_cmp(timing->publish, now) <= 0 && + knot_time_cmp(timing->post_active, now) > 0 && + knot_time_cmp(timing->remove, now) > 0); +} + +static bool is_ready(knot_kasp_key_timing_t *timing, knot_time_t now) +{ + return (knot_time_cmp(timing->ready, now) <= 0 && + knot_time_cmp(timing->active, now) > 0); +} + +static bool is_active(knot_kasp_key_timing_t *timing, knot_time_t now) +{ + return (knot_time_cmp(timing->active, now) <= 0 && + knot_time_cmp(timing->retire, now) > 0 && + knot_time_cmp(timing->retire_active, now) > 0 && + knot_time_cmp(timing->remove, now) > 0); +} + +static bool alg_has_active_zsk(kdnssec_ctx_t *ctx, uint8_t alg) +{ + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *k = &ctx->zone->keys[i]; + if (dnssec_key_get_algorithm(k->key) == alg && + k->is_zsk && is_active(&k->timing, ctx->now)) { + return true; + } + } + return false; +} + +static void fix_revoked_flag(knot_kasp_key_t *key) +{ + uint16_t flags = dnssec_key_get_flags(key->key); + if ((flags & DNSKEY_FLAGS_REVOKED) != DNSKEY_FLAGS_REVOKED) { + dnssec_key_set_flags(key->key, flags | DNSKEY_FLAGS_REVOKED); // FYI leading to change of keytag + } +} + +/*! + * \brief Get key feature flags from key parameters. + */ +static void set_key(knot_kasp_key_t *kasp_key, knot_time_t now, + zone_key_t *zone_key, bool same_alg_act_zsk) +{ + assert(kasp_key); + assert(zone_key); + + knot_kasp_key_timing_t *timing = &kasp_key->timing; + + zone_key->id = kasp_key->id; + zone_key->key = kasp_key->key; + + // next event computation + + knot_time_t next = 0; + knot_time_t timestamps[] = { + timing->pre_active, + timing->publish, + timing->ready, + timing->active, + timing->retire_active, + timing->retire, + timing->post_active, + timing->revoke, + timing->remove, + }; + + for (int i = 0; i < sizeof(timestamps) / sizeof(knot_time_t); i++) { + knot_time_t ts = timestamps[i]; + if (knot_time_cmp(now, ts) < 0 && knot_time_cmp(ts, next) < 0) { + next = ts; + } + } + + zone_key->next_event = next; + + zone_key->is_ksk = kasp_key->is_ksk; + zone_key->is_zsk = kasp_key->is_zsk; + + zone_key->is_public = is_published(timing, now); + zone_key->is_ready = (zone_key->is_ksk && is_ready(timing, now)); + zone_key->is_active = is_active(timing, now); + + zone_key->is_ksk_active_plus = zone_key->is_public && zone_key->is_ksk && !zone_key->is_active; // KSK is active+ whenever published + zone_key->is_zsk_active_plus = zone_key->is_ready && !same_alg_act_zsk; + if (knot_time_cmp(timing->pre_active, now) <= 0 && + knot_time_cmp(timing->ready, now) > 0 && + knot_time_cmp(timing->active, now) > 0 && + knot_time_cmp(timing->remove, now) > 0) { + zone_key->is_zsk_active_plus = zone_key->is_zsk; + // zone_key->is_ksk_active_plus = (knot_time_cmp(timing->publish, now) <= 0 && zone_key->is_ksk); // redundant, but helps understand + } + if (knot_time_cmp(timing->retire, now) <= 0 && + knot_time_cmp(timing->remove, now) > 0) { + zone_key->is_ksk_active_plus = false; + zone_key->is_public = zone_key->is_zsk; + } + if (knot_time_cmp(timing->retire_active, now) <= 0 && + knot_time_cmp(timing->retire, now) > 0 && + knot_time_cmp(timing->remove, now) > 0) { + zone_key->is_ksk_active_plus = zone_key->is_ksk; + zone_key->is_zsk_active_plus = !same_alg_act_zsk; + } // not "else" ! + if (knot_time_cmp(timing->post_active, now) <= 0 && + knot_time_cmp(timing->remove, now) > 0) { + zone_key->is_ksk_active_plus = false; + zone_key->is_zsk_active_plus = zone_key->is_zsk; + } + if (zone_key->is_ksk && + knot_time_cmp(timing->revoke, now) <= 0 && + knot_time_cmp(timing->remove, now) > 0) { + zone_key->is_ready = false; + zone_key->is_active = false; + zone_key->is_ksk_active_plus = true; + zone_key->is_public = true; + zone_key->is_revoked = true; + fix_revoked_flag(kasp_key); + } + if (kasp_key->is_pub_only) { + zone_key->is_active = false; + zone_key->is_ksk_active_plus = false; + zone_key->is_zsk_active_plus = false; + zone_key->is_pub_only = true; + } +} + +/*! + * \brief Check if algorithm is allowed with NSEC3. + */ +static bool is_nsec3_allowed(uint8_t algorithm) +{ + switch (algorithm) { + case DNSSEC_KEY_ALGORITHM_RSA_SHA1: + return false; + default: + return true; + } +} + +static int walk_algorithms(kdnssec_ctx_t *ctx, zone_keyset_t *keyset) +{ + if (ctx->policy->unsafe & UNSAFE_KEYSET) { + return KNOT_EOK; + } + + uint8_t alg_usage[256] = { 0 }; + bool have_active_alg = false; + + for (size_t i = 0; i < keyset->count; i++) { + zone_key_t *key = &keyset->keys[i]; + if (key->is_pub_only) { + continue; + } + uint8_t alg = dnssec_key_get_algorithm(key->key); + + if (ctx->policy->nsec3_enabled && !is_nsec3_allowed(alg)) { + log_zone_warning(ctx->zone->dname, "DNSSEC, key %d " + "cannot be used with NSEC3", + dnssec_key_get_keytag(key->key)); + key->is_public = false; + key->is_active = false; + key->is_ready = false; + key->is_ksk_active_plus = false; + key->is_zsk_active_plus = false; + continue; + } + + if (key->is_ksk && key->is_public) { alg_usage[alg] |= 1; } + if (key->is_zsk && key->is_public) { alg_usage[alg] |= 2; } + if (key->is_ksk && (key->is_active || key->is_ksk_active_plus)) { alg_usage[alg] |= 4; } + if (key->is_zsk && (key->is_active || key->is_zsk_active_plus)) { alg_usage[alg] |= 8; } + } + + for (size_t i = 0; i < sizeof(alg_usage); i++) { + if (!(alg_usage[i] & 3)) { + continue; // no public keys, ignore + } + switch (alg_usage[i]) { + case 15: // all keys ready for signing + have_active_alg = true; + break; + case 5: + case 10: + if (ctx->policy->offline_ksk) { + have_active_alg = true; + break; + } + // else FALLTHROUGH + default: + return KNOT_DNSSEC_EMISSINGKEYTYPE; + } + } + + if (!have_active_alg) { + return KNOT_DNSSEC_ENOKEY; + } + + return KNOT_EOK; +} + +/*! + * \brief Load private keys for active keys. + */ +static int load_private_keys(dnssec_keystore_t *keystore, zone_keyset_t *keyset) +{ + assert(keystore); + assert(keyset); + + for (size_t i = 0; i < keyset->count; i++) { + zone_key_t *key = &keyset->keys[i]; + if (!key->is_active && !key->is_ksk_active_plus && !key->is_zsk_active_plus) { + continue; + } + int r = dnssec_keystore_get_private(keystore, key->id, key->key); + switch (r) { + case DNSSEC_EOK: + case DNSSEC_KEY_ALREADY_PRESENT: + break; + default: + return r; + } + } + + return DNSSEC_EOK; +} + +/*! + * \brief Log information about zone keys. + */ +static void log_key_info(const zone_key_t *key, char *out, size_t out_len) +{ + assert(key); + assert(out); + + uint8_t alg_code = dnssec_key_get_algorithm(key->key); + const knot_lookup_t *alg = knot_lookup_by_id(knot_dnssec_alg_names, alg_code); + + char alg_code_str[8] = ""; + if (alg == NULL) { + (void)snprintf(alg_code_str, sizeof(alg_code_str), "%d", alg_code); + } + + (void)snprintf(out, out_len, "DNSSEC, key, tag %5d, algorithm %s%s%s%s%s%s", + dnssec_key_get_keytag(key->key), + (alg != NULL ? alg->name : alg_code_str), + (key->is_ksk ? (key->is_zsk ? ", CSK" : ", KSK") : ""), + (key->is_public ? ", public" : ""), + (key->is_ready ? ", ready" : ""), + (key->is_active ? ", active" : ""), + (key->is_ksk_active_plus || key->is_zsk_active_plus ? ", active+" : "")); +} + +static int log_key_sort(const void *a, const void *b) +{ + const key_info_t *x = a, *y = b; + return knot_time_cmp(x->key_time, y->key_time); +} + +/*! + * \brief Load zone keys and init cryptographic context. + */ +int load_zone_keys(kdnssec_ctx_t *ctx, zone_keyset_t *keyset_ptr, bool verbose) +{ + if (!ctx || !keyset_ptr) { + return KNOT_EINVAL; + } + + zone_keyset_t keyset = { 0 }; + + if (ctx->zone->num_keys < 1) { + log_zone_error(ctx->zone->dname, "DNSSEC, no keys are available"); + return KNOT_DNSSEC_ENOKEY; + } + + keyset.count = ctx->zone->num_keys; + keyset.keys = calloc(keyset.count, sizeof(zone_key_t)); + if (!keyset.keys) { + free_zone_keys(&keyset); + return KNOT_ENOMEM; + } + + key_info_t key_info[ctx->zone->num_keys]; + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + knot_kasp_key_t *kasp_key = &ctx->zone->keys[i]; + uint8_t kk_alg = dnssec_key_get_algorithm(kasp_key->key); + bool same_alg_zsk = alg_has_active_zsk(ctx, kk_alg); + set_key(kasp_key, ctx->now, &keyset.keys[i], same_alg_zsk); + if (verbose) { + log_key_info(&keyset.keys[i], key_info[i].msg, MAX_KEY_INFO); + if (knot_time_cmp(kasp_key->timing.pre_active, kasp_key->timing.publish) < 0) { + key_info[i].key_time = kasp_key->timing.pre_active; + } else { + key_info[i].key_time = kasp_key->timing.publish; + } + } + } + + // Sort the keys by publish/pre_active timestamps. + if (verbose) { + qsort(key_info, ctx->zone->num_keys, sizeof(key_info[0]), log_key_sort); + for (size_t i = 0; i < ctx->zone->num_keys; i++) { + log_zone_info(ctx->zone->dname, "%s", key_info[i].msg); + } + } + + int ret = walk_algorithms(ctx, &keyset); + if (ret != KNOT_EOK) { + log_zone_error(ctx->zone->dname, "DNSSEC, keys validation failed (%s)", + knot_strerror(ret)); + free_zone_keys(&keyset); + return ret; + } + + ret = load_private_keys(ctx->keystore, &keyset); + ret = knot_error_from_libdnssec(ret); + if (ret != KNOT_EOK) { + log_zone_error(ctx->zone->dname, "DNSSEC, failed to load private " + "keys (%s)", knot_strerror(ret)); + free_zone_keys(&keyset); + return ret; + } + + *keyset_ptr = keyset; + + return KNOT_EOK; +} + +/*! + * \brief Free structure with zone keys and associated DNSSEC contexts. + */ +void free_zone_keys(zone_keyset_t *keyset) +{ + if (!keyset) { + return; + } + + for (size_t i = 0; i < keyset->count; i++) { + dnssec_binary_free(&keyset->keys[i].precomputed_ds); + } + + free(keyset->keys); + + memset(keyset, '\0', sizeof(*keyset)); +} + +/*! + * \brief Get timestamp of next key event. + */ +knot_time_t knot_get_next_zone_key_event(const zone_keyset_t *keyset) +{ + assert(keyset); + + knot_time_t result = 0; + + for (size_t i = 0; i < keyset->count; i++) { + zone_key_t *key = &keyset->keys[i]; + if (knot_time_cmp(key->next_event, result) < 0) { + result = key->next_event; + } + } + + return result; +} + +/*! + * \brief Compute DS record rdata from key + cache it. + */ +int zone_key_calculate_ds(zone_key_t *for_key, dnssec_key_digest_t digesttype, + dnssec_binary_t *out_donotfree) +{ + assert(for_key); + assert(out_donotfree); + + int ret = KNOT_EOK; + + if (for_key->precomputed_ds.data == NULL || for_key->precomputed_digesttype != digesttype) { + dnssec_binary_free(&for_key->precomputed_ds); + ret = dnssec_key_create_ds(for_key->key, digesttype, &for_key->precomputed_ds); + ret = knot_error_from_libdnssec(ret); + for_key->precomputed_digesttype = digesttype; + } + + *out_donotfree = for_key->precomputed_ds; + return ret; +} + +zone_sign_ctx_t *zone_sign_ctx(const zone_keyset_t *keyset, const kdnssec_ctx_t *dnssec_ctx) +{ + zone_sign_ctx_t *ctx = calloc(1, sizeof(*ctx) + keyset->count * sizeof(*ctx->sign_ctxs)); + if (ctx == NULL) { + return NULL; + } + + ctx->sign_ctxs = (dnssec_sign_ctx_t **)(ctx + 1); + ctx->count = keyset->count; + ctx->keys = keyset->keys; + ctx->dnssec_ctx = dnssec_ctx; + for (size_t i = 0; i < ctx->count; i++) { + int ret = dnssec_sign_new(&ctx->sign_ctxs[i], ctx->keys[i].key); + if (ret != DNSSEC_EOK) { + zone_sign_ctx_free(ctx); + return NULL; + } + } + + return ctx; +} + +zone_sign_ctx_t *zone_validation_ctx(const kdnssec_ctx_t *dnssec_ctx) +{ + size_t count = dnssec_ctx->zone->num_keys; + zone_sign_ctx_t *ctx = calloc(1, sizeof(*ctx) + count * sizeof(*ctx->sign_ctxs)); + if (ctx == NULL) { + return NULL; + } + + ctx->sign_ctxs = (dnssec_sign_ctx_t **)(ctx + 1); + ctx->count = count; + ctx->keys = NULL; + ctx->dnssec_ctx = dnssec_ctx; + for (size_t i = 0; i < ctx->count; i++) { + int ret = dnssec_sign_new(&ctx->sign_ctxs[i], dnssec_ctx->zone->keys[i].key); + if (ret != DNSSEC_EOK) { + zone_sign_ctx_free(ctx); + return NULL; + } + } + + return ctx; +} + +void zone_sign_ctx_free(zone_sign_ctx_t *ctx) +{ + if (ctx != NULL) { + for (size_t i = 0; i < ctx->count; i++) { + dnssec_sign_free(ctx->sign_ctxs[i]); + } + free(ctx); + } +} + +int dnssec_key_from_rdata(dnssec_key_t **key, const knot_dname_t *owner, + const uint8_t *rdata, size_t rdlen) +{ + if (key == NULL || rdata == NULL || rdlen == 0) { + return KNOT_EINVAL; + } + + const dnssec_binary_t binary_key = { + .size = rdlen, + .data = (uint8_t *)rdata + }; + + dnssec_key_t *new_key = NULL; + int ret = dnssec_key_new(&new_key); + if (ret != DNSSEC_EOK) { + return knot_error_from_libdnssec(ret); + } + ret = dnssec_key_set_rdata(new_key, &binary_key); + if (ret != DNSSEC_EOK) { + dnssec_key_free(new_key); + return knot_error_from_libdnssec(ret); + } + if (owner != NULL) { + ret = dnssec_key_set_dname(new_key, owner); + if (ret != DNSSEC_EOK) { + dnssec_key_free(new_key); + return knot_error_from_libdnssec(ret); + } + } + + *key = new_key; + return KNOT_EOK; +} + +static bool soa_signed_by_key(const zone_key_t *key, const knot_rdataset_t *apex_rrsig) +{ + assert(key != NULL); + if (apex_rrsig == NULL) { + return false; + } + uint16_t keytag = dnssec_key_get_keytag(key->key); + + knot_rdata_t *rr = apex_rrsig->rdata; + for (int i = 0; i < apex_rrsig->count; i++) { + if (knot_rrsig_type_covered(rr) == KNOT_RRTYPE_SOA && + knot_rrsig_key_tag(rr) == keytag) { + return true; + } + rr = knot_rdataset_next(rr); + } + + return false; +} + +int is_soa_signed_by_all_zsks(const zone_keyset_t *keyset, + const knot_rdataset_t *apex_rrsig) +{ + if (keyset == NULL || keyset->count == 0) { + return false; + } + + for (size_t i = 0; i < keyset->count; i++) { + const zone_key_t *key = &keyset->keys[i]; + if (key->is_zsk && key->is_active && + !soa_signed_by_key(key, apex_rrsig)) { + return false; + } + } + + return true; +} diff --git a/src/knot/dnssec/zone-keys.h b/src/knot/dnssec/zone-keys.h new file mode 100644 index 0000000..6d72572 --- /dev/null +++ b/src/knot/dnssec/zone-keys.h @@ -0,0 +1,213 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/dynarray.h" +#include "libdnssec/keystore.h" +#include "libdnssec/sign.h" +#include "knot/dnssec/kasp/kasp_zone.h" +#include "knot/dnssec/kasp/policy.h" +#include "knot/dnssec/context.h" + +/*! + * \brief Zone key context used during signing. + */ +typedef struct { + const char *id; + dnssec_key_t *key; + + dnssec_binary_t precomputed_ds; + dnssec_key_digest_t precomputed_digesttype; + + knot_time_t next_event; + + bool is_ksk; + bool is_zsk; + bool is_active; + bool is_public; + bool is_ready; + bool is_zsk_active_plus; + bool is_ksk_active_plus; + bool is_pub_only; + bool is_revoked; +} zone_key_t; + +knot_dynarray_declare(keyptr, zone_key_t *, DYNARRAY_VISIBILITY_NORMAL, 1) + +typedef struct { + size_t count; + zone_key_t *keys; +} zone_keyset_t; + +/*! + * \brief Signing context used for single signing thread. + */ +typedef struct { + size_t count; // number of keys in keyset + zone_key_t *keys; // keys in keyset + dnssec_sign_ctx_t **sign_ctxs; // signing buffers for keys in keyset + const kdnssec_ctx_t *dnssec_ctx; // dnssec context +} zone_sign_ctx_t; + +/*! + * \brief Flags determining key type + */ +enum { + DNSKEY_FLAGS_ZSK = KNOT_DNSKEY_FLAG_ZONE, + DNSKEY_FLAGS_KSK = KNOT_DNSKEY_FLAG_ZONE | KNOT_DNSKEY_FLAG_SEP, + DNSKEY_FLAGS_REVOKED = KNOT_DNSKEY_FLAG_ZONE | KNOT_DNSKEY_FLAG_SEP | KNOT_DNSKEY_FLAG_REVOKE, +}; + +inline static uint16_t dnskey_flags(bool is_ksk) +{ + return is_ksk ? DNSKEY_FLAGS_KSK : DNSKEY_FLAGS_ZSK; +} + +typedef enum { + DNSKEY_GENERATE_KSK = (1 << 0), // KSK flag in metadata + DNSKEY_GENERATE_ZSK = (1 << 1), // ZSK flag in metadata + DNSKEY_GENERATE_SEP_SPEC = (1 << 2), // not (SEP bit set iff KSK) + DNSKEY_GENERATE_SEP_ON = (1 << 3), // SEP bit set on +} kdnssec_generate_flags_t; + +void normalize_generate_flags(kdnssec_generate_flags_t *flags); + +/*! + * \brief Generate new key, store all details in new kasp key structure. + * + * \param ctx kasp context + * \param flags determine if to use the key as KSK and/or ZSK and SEP flag + * \param key_ptr output if KNOT_EOK: new pointer to generated key + * + * \return KNOT_E* + */ +int kdnssec_generate_key(kdnssec_ctx_t *ctx, kdnssec_generate_flags_t flags, + knot_kasp_key_t **key_ptr); + +/*! + * \brief Take a key from another zone (copying info, sharing privkey). + * + * \param ctx kasp context + * \param from_zone name of the zone to take from + * \param key_id ID of the key to take + * + * \return KNOT_E* + */ +int kdnssec_share_key(kdnssec_ctx_t *ctx, const knot_dname_t *from_zone, const char *key_id); + +/*! + * \brief Remove key from zone. + * + * Deletes the key in keystore, unlinks the key from the zone in KASP db, + * moreover if no more zones use this key in KASP db, deletes it completely there + * and deletes it also from key storage (PKCS8dir/PKCS11). + * + * \param ctx kasp context (zone, keystore, kaspdb) to be modified + * \param key_ptr pointer to key to be removed, must be inside keystore structure, NOT a copy of it! + * + * \return KNOT_E* + */ +int kdnssec_delete_key(kdnssec_ctx_t *ctx, knot_kasp_key_t *key_ptr); + +/*! + * \brief Load zone keys and init cryptographic context. + * + * \param ctx Zone signing context. + * \param keyset_ptr Resulting zone keyset. + * \param verbose Print key summary into log. + * + * \return Error code, KNOT_EOK if successful. + */ +int load_zone_keys(kdnssec_ctx_t *ctx, zone_keyset_t *keyset_ptr, bool verbose); + +/*! + * \brief Free structure with zone keys and associated DNSSEC contexts. + * + * \param keyset Zone keys. + */ +void free_zone_keys(zone_keyset_t *keyset); + +/*! + * \brief Get timestamp of next key event. + * + * \param keyset Zone keys. + * + * \return Timestamp of next key event. + */ +knot_time_t knot_get_next_zone_key_event(const zone_keyset_t *keyset); + +/*! + * \brief Returns DS record rdata for given key. + * + * This function caches the results, so calling again with the same key returns immediately. + * + * \param for_key The key to compute DS for. + * \param digesttype DS digest algorithm. + * \param out_donotfree Output: the DS record rdata. Do not call dnssec_binary_free() on this ever. + * + * \return Error code, KNOT_EOK if successful. + */ +int zone_key_calculate_ds(zone_key_t *for_key, dnssec_key_digest_t digesttype, + dnssec_binary_t *out_donotfree); + +/*! + * \brief Initialize local signing context. + * + * \param keyset Key set. + * \param dnssec_ctx DNSSEC context. + * + * \return New local signing context or NULL. + */ +zone_sign_ctx_t *zone_sign_ctx(const zone_keyset_t *keyset, const kdnssec_ctx_t *dnssec_ctx); + +/*! + * \brief Initialize local validating context. + * \param dnssec_ctx DNSSEC context. + * \return New local validating context or NULL. + */ +zone_sign_ctx_t *zone_validation_ctx(const kdnssec_ctx_t *dnssec_ctx); + +/*! + * \brief Free local signing context. + * + * \note This doesn't free the underlying keyset. + * + * \param ctx Local context to be freed. + */ +void zone_sign_ctx_free(zone_sign_ctx_t *ctx); + +/*! + * \brief Create key signing structure from DNSKEY zone record. + * + * \param key Dnssec key to be allocated. + * \param owner Zone name. + * \param rdata DNSKEY rdata. + * \param rdlen DNSKEY rdata length. + * + * \return KNOT_E* + */ +int dnssec_key_from_rdata(dnssec_key_t **key, const knot_dname_t *owner, + const uint8_t *rdata, size_t rdlen); + +/*! + * \brief Tell if apex SOA is signed by all active ZSKs. + * + * \param keyset Zone key set. + * \param apex_rrsig Apex RRSIG RRSet. + */ +int is_soa_signed_by_all_zsks(const zone_keyset_t *keyset, + const knot_rdataset_t *apex_rrsig); diff --git a/src/knot/dnssec/zone-nsec.c b/src/knot/dnssec/zone-nsec.c new file mode 100644 index 0000000..e952061 --- /dev/null +++ b/src/knot/dnssec/zone-nsec.c @@ -0,0 +1,429 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "libdnssec/error.h" +#include "libknot/descriptor.h" +#include "libknot/rrtype/nsec3.h" +#include "libknot/rrtype/soa.h" +#include "knot/common/log.h" +#include "knot/dnssec/nsec-chain.h" +#include "knot/dnssec/nsec3-chain.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-nsec.h" +#include "knot/dnssec/zone-sign.h" +#include "knot/zone/zone-diff.h" +#include "contrib/base32hex.h" +#include "contrib/wire_ctx.h" + +int knot_nsec3_hash_to_dname(uint8_t *out, size_t out_size, const uint8_t *hash, + size_t hash_size, const knot_dname_t *zone_apex) + +{ + if (out == NULL || hash == NULL || zone_apex == NULL) { + return KNOT_EINVAL; + } + + // Encode raw hash to the first label. + uint8_t label[KNOT_DNAME_MAXLABELLEN]; + int32_t label_size = knot_base32hex_encode(hash, hash_size, label, sizeof(label)); + if (label_size <= 0) { + return label_size; + } + + // Write the result, which already is in lower-case. + wire_ctx_t wire = wire_ctx_init(out, out_size); + + wire_ctx_write_u8(&wire, label_size); + wire_ctx_write(&wire, label, label_size); + wire_ctx_write(&wire, zone_apex, knot_dname_size(zone_apex)); + + return wire.error; +} + +int knot_create_nsec3_owner(uint8_t *out, size_t out_size, + const knot_dname_t *owner, const knot_dname_t *zone_apex, + const dnssec_nsec3_params_t *params) +{ + if (out == NULL || owner == NULL || zone_apex == NULL || params == NULL) { + return KNOT_EINVAL; + } + + dnssec_binary_t data = { + .data = (uint8_t *)owner, + .size = knot_dname_size(owner) + }; + + dnssec_binary_t hash = { 0 }; + + int ret = dnssec_nsec3_hash(&data, params, &hash); + if (ret != DNSSEC_EOK) { + return knot_error_from_libdnssec(ret); + } + + ret = knot_nsec3_hash_to_dname(out, out_size, hash.data, hash.size, zone_apex); + + dnssec_binary_free(&hash); + + return ret; +} + +knot_dname_t *node_nsec3_hash(zone_node_t *node, const zone_contents_t *zone) +{ + if (node->nsec3_hash == NULL && knot_is_nsec3_enabled(zone)) { + assert(!(node->flags & NODE_FLAGS_NSEC3_NODE)); + size_t hash_size = zone_nsec3_name_len(zone); + knot_dname_t *hash = malloc(hash_size); + if (hash == NULL) { + return NULL; + } + if (knot_create_nsec3_owner(hash, hash_size, node->owner, zone->apex->owner, + &zone->nsec3_params) != KNOT_EOK) { + free(hash); + return NULL; + } + node->nsec3_hash = hash; + } + + if (node->flags & NODE_FLAGS_NSEC3_NODE) { + return node->nsec3_node->owner; + } else { + return node->nsec3_hash; + } +} + +zone_node_t *node_nsec3_node(zone_node_t *node, const zone_contents_t *zone) +{ + if (!(node->flags & NODE_FLAGS_NSEC3_NODE) && knot_is_nsec3_enabled(zone)) { + knot_dname_t *hash = node_nsec3_hash(node, zone); + zone_node_t *nsec3 = zone_tree_get(zone->nsec3_nodes, hash); + if (nsec3 != NULL) { + if (node->nsec3_hash != binode_counterpart(node)->nsec3_hash) { + free(node->nsec3_hash); + } + node->nsec3_node = binode_first(nsec3); + node->flags |= NODE_FLAGS_NSEC3_NODE; + } + } + + return node_nsec3_get(node); +} + +int binode_fix_nsec3_pointer(zone_node_t *node, const zone_contents_t *zone) +{ + zone_node_t *counter = binode_counterpart(node); + if (counter->nsec3_hash == NULL) { + (void)node_nsec3_node(node, zone); + return KNOT_EOK; + } + assert(counter->nsec3_node != NULL); // shut up cppcheck + + zone_node_t *nsec3_counter = (counter->flags & NODE_FLAGS_NSEC3_NODE) ? + counter->nsec3_node : NULL; + if (nsec3_counter != NULL && !(binode_node_as(nsec3_counter, node)->flags & NODE_FLAGS_DELETED)) { + assert(node->flags & NODE_FLAGS_NSEC3_NODE); + node->flags |= NODE_FLAGS_NSEC3_NODE; + assert(!(nsec3_counter->flags & NODE_FLAGS_SECOND)); + node->nsec3_node = nsec3_counter; + } else { + node->flags &= ~NODE_FLAGS_NSEC3_NODE; + if (counter->flags & NODE_FLAGS_NSEC3_NODE) { + // downgrade the NSEC3 node pointer to NSEC3 name + node->nsec3_hash = knot_dname_copy(counter->nsec3_node->owner, NULL); + } else { + node->nsec3_hash = counter->nsec3_hash; + } + (void)node_nsec3_node(node, zone); + } + return KNOT_EOK; +} + +static bool nsec3param_valid(const knot_rdataset_t *rrs, + const dnssec_nsec3_params_t *params) +{ + assert(rrs); + assert(params); + + // NSEC3 disabled + if (params->algorithm == 0) { + return false; + } + + // multiple NSEC3 records + if (rrs->count != 1) { + return false; + } + + dnssec_binary_t rdata = { + .size = rrs->rdata->len, + .data = rrs->rdata->data, + }; + + dnssec_nsec3_params_t parsed = { 0 }; + int r = dnssec_nsec3_params_from_rdata(&parsed, &rdata); + if (r != DNSSEC_EOK) { + return false; + } + + bool equal = parsed.algorithm == params->algorithm && + parsed.flags == 0 && // opt-out flag is always 0 in NSEC3PARAM + parsed.iterations == params->iterations && + dnssec_binary_cmp(&parsed.salt, ¶ms->salt) == 0; + + dnssec_nsec3_params_free(&parsed); + + return equal; +} + +static int remove_nsec3param(zone_update_t *update, bool also_rrsig) +{ + knot_rrset_t rrset = node_rrset(update->new_cont->apex, KNOT_RRTYPE_NSEC3PARAM); + int ret = zone_update_remove(update, &rrset); + + rrset = node_rrset(update->new_cont->apex, KNOT_RRTYPE_RRSIG); + if (!knot_rrset_empty(&rrset) && ret == KNOT_EOK && also_rrsig) { + knot_rrset_t rrsig; + knot_rrset_init(&rrsig, update->new_cont->apex->owner, + KNOT_RRTYPE_RRSIG, KNOT_CLASS_IN, 0); + ret = knot_synth_rrsig(KNOT_RRTYPE_NSEC3PARAM, &rrset.rrs, &rrsig.rrs, NULL); + if (ret == KNOT_EOK) { + ret = zone_update_remove(update, &rrsig); + } + knot_rdataset_clear(&rrsig.rrs, NULL); + } + + return ret; +} + +static int set_nsec3param(knot_rrset_t *rrset, const dnssec_nsec3_params_t *params) +{ + assert(rrset); + assert(params); + + // Prepare wire rdata. + size_t rdata_len = 3 * sizeof(uint8_t) + sizeof(uint16_t) + params->salt.size; + uint8_t rdata[rdata_len]; + wire_ctx_t wire = wire_ctx_init(rdata, rdata_len); + + wire_ctx_write_u8(&wire, params->algorithm); + wire_ctx_write_u8(&wire, 0); // (RFC 5155 Section 4.1.2) + wire_ctx_write_u16(&wire, params->iterations); + wire_ctx_write_u8(&wire, params->salt.size); + wire_ctx_write(&wire, params->salt.data, params->salt.size); + + if (wire.error != KNOT_EOK) { + return wire.error; + } + + assert(wire_ctx_available(&wire) == 0); + + return knot_rrset_add_rdata(rrset, rdata, rdata_len, NULL); +} + +static int add_nsec3param(zone_update_t *update, + const dnssec_nsec3_params_t *params, + uint32_t ttl) +{ + assert(update); + assert(params); + + knot_rrset_t *rrset = NULL; + rrset = knot_rrset_new(update->new_cont->apex->owner, KNOT_RRTYPE_NSEC3PARAM, + KNOT_CLASS_IN, ttl, NULL); + if (rrset == NULL) { + return KNOT_ENOMEM; + } + + int r = set_nsec3param(rrset, params); + if (r == KNOT_EOK) { + r = zone_update_add(update, rrset); + } + knot_rrset_free(rrset, NULL); + return r; +} + +bool knot_nsec3param_uptodate(const zone_contents_t *zone, + const dnssec_nsec3_params_t *params) +{ + assert(zone); + assert(params); + + knot_rdataset_t *nsec3param = node_rdataset(zone->apex, KNOT_RRTYPE_NSEC3PARAM); + + return (nsec3param != NULL && nsec3param_valid(nsec3param, params)); +} + +int knot_nsec3param_update(zone_update_t *update, + const dnssec_nsec3_params_t *params, + uint32_t ttl) +{ + assert(update); + assert(params); + + knot_rdataset_t *nsec3param = node_rdataset(update->new_cont->apex, KNOT_RRTYPE_NSEC3PARAM); + bool valid = nsec3param && nsec3param_valid(nsec3param, params); + + if (nsec3param && !valid) { + int r = remove_nsec3param(update, params->algorithm == 0); + if (r != KNOT_EOK) { + return r; + } + } + + if (params->algorithm != 0 && !valid) { + return add_nsec3param(update, params, ttl); + } + + return KNOT_EOK; +} + +/*! + * \brief Initialize NSEC3PARAM based on the signing policy. + * + * \note For NSEC, the algorithm number is set to 0. + */ +static dnssec_nsec3_params_t nsec3param_init(const knot_kasp_policy_t *policy, + const knot_kasp_zone_t *zone) +{ + assert(policy); + assert(zone); + + dnssec_nsec3_params_t params = { 0 }; + if (policy->nsec3_enabled) { + params.algorithm = DNSSEC_NSEC3_ALGORITHM_SHA1; + params.iterations = policy->nsec3_iterations; + params.salt = zone->nsec3_salt; + params.flags = (policy->nsec3_opt_out ? KNOT_NSEC3_FLAG_OPT_OUT : 0); + } + + return params; +} + +// int: returns KNOT_E* if error +static int zone_nsec_ttl(zone_contents_t *zone) +{ + knot_rrset_t soa = node_rrset(zone->apex, KNOT_RRTYPE_SOA); + if (knot_rrset_empty(&soa)) { + return KNOT_EINVAL; + } + + return MIN(knot_soa_minimum(soa.rrs.rdata), soa.ttl); +} + +int knot_zone_create_nsec_chain(zone_update_t *update, const kdnssec_ctx_t *ctx) +{ + if (update == NULL || ctx == NULL) { + return KNOT_EINVAL; + } + + if (ctx->policy->unsafe & UNSAFE_NSEC) { + return KNOT_EOK; + } + + int nsec_ttl = zone_nsec_ttl(update->new_cont); + if (nsec_ttl < 0) { + return nsec_ttl; + } + + dnssec_nsec3_params_t params = nsec3param_init(ctx->policy, ctx->zone); + + int ret = knot_nsec3param_update(update, ¶ms, nsec_ttl); + if (ret != KNOT_EOK) { + return ret; + } + + if (ctx->policy->nsec3_enabled) { + ret = knot_nsec3_create_chain(update->new_cont, ¶ms, nsec_ttl, + update); + } else { + ret = knot_nsec_create_chain(update, nsec_ttl); + if (ret == KNOT_EOK) { + ret = delete_nsec3_chain(update); + } + } + return ret; +} + +int knot_zone_fix_nsec_chain(zone_update_t *update, + const zone_keyset_t *zone_keys, + const kdnssec_ctx_t *ctx) +{ + if (update == NULL || ctx == NULL) { + return KNOT_EINVAL; + } + + if (ctx->policy->unsafe & UNSAFE_NSEC) { + return KNOT_EOK; + } + + int nsec_ttl_old = zone_nsec_ttl(update->zone->contents); + int nsec_ttl_new = zone_nsec_ttl(update->new_cont); + if (nsec_ttl_old < 0 || nsec_ttl_new < 0) { + return MIN(nsec_ttl_old, nsec_ttl_new); + } + + dnssec_nsec3_params_t params = nsec3param_init(ctx->policy, ctx->zone); + + int ret; + if (nsec_ttl_old != nsec_ttl_new || (update->flags & UPDATE_CHANGED_NSEC)) { + ret = KNOT_ENORECORD; + } else if (ctx->policy->nsec3_enabled) { + ret = knot_nsec3_fix_chain(update, ¶ms, nsec_ttl_new); + } else { + ret = knot_nsec_fix_chain(update, nsec_ttl_new); + } + if (ret == KNOT_ENORECORD) { + log_zone_info(update->zone->name, "DNSSEC, re-creating whole NSEC%s chain", + (ctx->policy->nsec3_enabled ? "3" : "")); + if (ctx->policy->nsec3_enabled) { + ret = knot_nsec3_create_chain(update->new_cont, ¶ms, + nsec_ttl_new, update); + } else { + ret = knot_nsec_create_chain(update, nsec_ttl_new); + } + } + if (ret == KNOT_EOK) { + ret = knot_zone_sign_nsecs_in_changeset(zone_keys, ctx, update); + } + return ret; +} + +int knot_zone_check_nsec_chain(zone_update_t *update, const kdnssec_ctx_t *ctx, + bool incremental) +{ + int ret = KNOT_EOK; + dnssec_nsec3_params_t params = nsec3param_init(ctx->policy, ctx->zone); + + if (incremental) { + ret = ctx->policy->nsec3_enabled + ? knot_nsec3_check_chain_fix(update, ¶ms) + : knot_nsec_check_chain_fix(update); + } + if (ret == KNOT_ENORECORD) { + log_zone_info(update->zone->name, "DNSSEC, re-validating whole NSEC%s chain", + (ctx->policy->nsec3_enabled ? "3" : "")); + incremental = false; + } + + if (incremental) { + return ret; + } + + return ctx->policy->nsec3_enabled ? knot_nsec3_check_chain(update, ¶ms) : + knot_nsec_check_chain(update); +} diff --git a/src/knot/dnssec/zone-nsec.h b/src/knot/dnssec/zone-nsec.h new file mode 100644 index 0000000..c43b658 --- /dev/null +++ b/src/knot/dnssec/zone-nsec.h @@ -0,0 +1,163 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> + +#include "knot/dnssec/context.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/updates/zone-update.h" +#include "knot/zone/contents.h" + +/*! + * Check if NSEC3 is enabled for the given zone. + * + * \param zone Zone to be checked. + * + * \return NSEC3 is enabled. + */ +inline static bool knot_is_nsec3_enabled(const zone_contents_t *zone) +{ + return zone != NULL && zone->nsec3_params.algorithm != 0; +} + +inline static size_t zone_nsec3_hash_len(const zone_contents_t *zone) +{ + return knot_is_nsec3_enabled(zone) ? dnssec_nsec3_hash_length(zone->nsec3_params.algorithm) : 0; +} + +inline static size_t zone_nsec3_name_len(const zone_contents_t *zone) +{ + return 1 + ((zone_nsec3_hash_len(zone) + 4) / 5) * 8 + knot_dname_size(zone->apex->owner); +} + +/*! + * \brief Create NSEC3 owner name from hash and zone apex. + * + * \param out Output buffer. + * \param out_size Size of the output buffer. + * \param hash Raw hash. + * \param hash_size Size of the hash. + * \param zone_apex Zone apex. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_nsec3_hash_to_dname(uint8_t *out, size_t out_size, const uint8_t *hash, + size_t hash_size, const knot_dname_t *zone_apex); + +/*! + * \brief Create NSEC3 owner name from regular owner name. + * + * \param out Output buffer. + * \param out_size Size of the output buffer. + * \param owner Node owner name. + * \param zone_apex Zone apex name. + * \param params Params for NSEC3 hashing function. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_create_nsec3_owner(uint8_t *out, size_t out_size, + const knot_dname_t *owner, const knot_dname_t *zone_apex, + const dnssec_nsec3_params_t *params); + +/*! + * \brief Return (and compute of needed) the corresponding NSEC3 node's name. + * + * \param node Normal node. + * \param zone Optional: zone contents with NSEC3 params. + * + * \return NSEC3 node owner. + * + * \note The result is also stored in (node), unless zone == NULL; + */ +knot_dname_t *node_nsec3_hash(zone_node_t *node, const zone_contents_t *zone); + +/*! + * \brief Return (and compute if needed) the corresponding NSEC3 node. + * + * \param node Normal node. + * \param zone Optional: zone contents with NSEC3 params and NSEC3 tree. + * + * \return NSEC3 node. + * + * \note The result is also stored in (node), unless zone == NULL; + */ +zone_node_t *node_nsec3_node(zone_node_t *node, const zone_contents_t *zone); + +/*! + * \brief Update node's NSEC3 pointer (or hash), taking it from bi-node counterpart if possible. + * + * \param node Bi-node with this node to be updated. + * \param zone Zone contents the node is in. + * + * \return KNOT_EOK :) + */ +int binode_fix_nsec3_pointer(zone_node_t *node, const zone_contents_t *zone); + +/*! + * \brief Check if NSEC3 record in zone is consistent with configured params. + */ +bool knot_nsec3param_uptodate(const zone_contents_t *zone, + const dnssec_nsec3_params_t *params); + +/*! + * \brief Update NSEC3PARAM in zone to be consistent with configured params. + * + * \param update Zone to be updated. + * \param params NSEC3 params. + * \param ttl Desired TTL for NSEC3PARAM. + * + * \return KNOT_E* + */ +int knot_nsec3param_update(zone_update_t *update, + const dnssec_nsec3_params_t *params, + uint32_t ttl); + +/*! + * \brief Create NSEC or NSEC3 chain in the zone. + * + * \param update Zone Update with current zone contents and to be updated with NSEC chain. + * \param ctx Signing context. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_zone_create_nsec_chain(zone_update_t *update, const kdnssec_ctx_t *ctx); + +/*! + * \brief Fix NSEC or NSEC3 chain after zone was updated, and sign the changed NSECs. + * + * \param update Zone Update with the update and to be update with NSEC chain. + * \param zone_keys Zone keys used for NSEC(3) creation. + * \param ctx Signing context. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_zone_fix_nsec_chain(zone_update_t *update, + const zone_keyset_t *zone_keys, + const kdnssec_ctx_t *ctx); + +/*! + * \brief Validate NSEC or NSEC3 chain in the zone. + * + * \param update Zone update with current/previous contents. + * \param ctx Signing context. + * \param incremental Validate incremental update. + * + * \return KNOT_E* + */ +int knot_zone_check_nsec_chain(zone_update_t *update, const kdnssec_ctx_t *ctx, + bool incremental); diff --git a/src/knot/dnssec/zone-sign.c b/src/knot/dnssec/zone-sign.c new file mode 100644 index 0000000..ffa10c4 --- /dev/null +++ b/src/knot/dnssec/zone-sign.c @@ -0,0 +1,1081 @@ +/* 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 <pthread.h> +#include <sys/types.h> + +#include "libdnssec/error.h" +#include "libdnssec/key.h" +#include "libdnssec/keytag.h" +#include "libdnssec/sign.h" +#include "knot/common/log.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/key_records.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-sign.h" +#include "libknot/libknot.h" +#include "libknot/dynarray.h" +#include "contrib/wire_ctx.h" + +typedef struct { + node_t n; + uint16_t type; +} type_node_t; + +typedef struct { + knot_dname_t *dname; + knot_dname_t *hashed_dname; + list_t *type_list; +} signed_info_t; + +/*- private API - common functions -------------------------------------------*/ + +/*! + * \brief Initializes RR set and set owner and rclass from template RR set. + */ +static knot_rrset_t rrset_init_from(const knot_rrset_t *src, uint16_t type) +{ + assert(src); + knot_rrset_t rrset; + knot_rrset_init(&rrset, src->owner, type, src->rclass, src->ttl); + return rrset; +} + +/*! + * \brief Create empty RRSIG RR set for a given RR set to be covered. + */ +static knot_rrset_t create_empty_rrsigs_for(const knot_rrset_t *covered) +{ + assert(!knot_rrset_empty(covered)); + return rrset_init_from(covered, KNOT_RRTYPE_RRSIG); +} + +static bool apex_rr_changed(const zone_node_t *old_apex, + const zone_node_t *new_apex, + uint16_t type) +{ + assert(old_apex); + assert(new_apex); + knot_rrset_t old_rr = node_rrset(old_apex, type); + knot_rrset_t new_rr = node_rrset(new_apex, type); + + return !knot_rrset_equal(&old_rr, &new_rr, false); +} + +static bool apex_dnssec_changed(zone_update_t *update) +{ + if (update->zone->contents == NULL || update->new_cont == NULL) { + return false; + } + return apex_rr_changed(update->zone->contents->apex, + update->new_cont->apex, KNOT_RRTYPE_DNSKEY) || + apex_rr_changed(update->zone->contents->apex, + update->new_cont->apex, KNOT_RRTYPE_NSEC3PARAM); +} + +/*- private API - signing of in-zone nodes -----------------------------------*/ + +/*! + * \brief Check if there is a valid signature for a given RR set and key. + * + * \param covered RR set with covered records. + * \param rrsigs RR set with RRSIGs. + * \param key Signing key. + * \param ctx Signing context. + * \param policy DNSSEC policy. + * \param skip_crypto All RRSIGs in this node have been verified, just check validity. + * \param refresh Consider RRSIG expired when gonna expire this soon. + * \param found_invalid Out: some matching but expired%invalid RRSIG found. + * \param at Out: RRSIG position. + * + * \return The signature exists and is valid. + */ +static bool valid_signature_exists(const knot_rrset_t *covered, + const knot_rrset_t *rrsigs, + const dnssec_key_t *key, + dnssec_sign_ctx_t *ctx, + const kdnssec_ctx_t *dnssec_ctx, + knot_timediff_t refresh, + bool skip_crypto, + int *found_invalid, + uint16_t *at) +{ + assert(key); + + if (knot_rrset_empty(rrsigs)) { + return false; + } + + uint16_t rrsigs_rdata_count = rrsigs->rrs.count; + knot_rdata_t *rdata = rrsigs->rrs.rdata; + bool found_valid = false; + for (uint16_t i = 0; i < rrsigs_rdata_count; i++) { + uint16_t rr_keytag = knot_rrsig_key_tag(rdata); + uint16_t rr_covered = knot_rrsig_type_covered(rdata); + uint8_t rr_algo = knot_rrsig_alg(rdata); + rdata = knot_rdataset_next(rdata); + + uint16_t keytag = dnssec_key_get_keytag(key); + uint8_t algo = dnssec_key_get_algorithm(key); + if (rr_keytag != keytag || rr_algo != algo || rr_covered != covered->type) { + continue; + } + + int ret = knot_check_signature(covered, rrsigs, i, key, ctx, + dnssec_ctx, refresh, skip_crypto); + if (ret == KNOT_EOK) { + if (at != NULL) { + *at = i; + } + if (found_invalid == NULL) { + return true; + } else { + found_valid = true; // continue searching for invalid RRSIG + } + } else if (found_invalid != NULL) { + *found_invalid = ret; + } + } + + return found_valid; +} + +/*! + * \brief Note earliest expiration of a signature. + * + * \param rrsig RRSIG rdata. + * \param now Current 64-bit timestamp. + * \param expires_at Current earliest expiration, will be updated. + */ +static void note_earliest_expiration(const knot_rdata_t *rrsig, knot_time_t now, + knot_time_t *expires_at) +{ + assert(rrsig); + if (expires_at == NULL) { + return; + } + + uint32_t curr_rdata = knot_rrsig_sig_expiration(rrsig); + knot_time_t current = knot_time_from_u32(curr_rdata, now); + + *expires_at = knot_time_min(current, *expires_at); +} + +bool rrsig_covers_type(const knot_rrset_t *rrsig, uint16_t type) +{ + if (knot_rrset_empty(rrsig)) { + return false; + } + assert(rrsig->type == KNOT_RRTYPE_RRSIG); + knot_rdata_t *one_rr = rrsig->rrs.rdata; + for (int i = 0; i < rrsig->rrs.count; i++) { + if (type == knot_rrsig_type_covered(one_rr)) { + return true; + } + one_rr = knot_rdataset_next(one_rr); + } + return false; +} + +/*! + * \brief Add missing RRSIGs into the changeset for adding. + * + * \note Also removes invalid RRSIGs. + * + * \param covered RR set with covered records. + * \param rrsigs RR set with RRSIGs. + * \param sign_ctx Local zone signing context. + * \param skip_crypto All RRSIGs in this node have been verified, just check validity. + * \param changeset Changeset to be updated. + * \param update Zone update to be updated. Exactly one of "changeset" and "update" must be NULL! + * \param expires_at Earliest RRSIG expiration. + * + * \return Error code, KNOT_EOK if successful. + */ +static int add_missing_rrsigs(const knot_rrset_t *covered, + const knot_rrset_t *rrsigs, + zone_sign_ctx_t *sign_ctx, + bool skip_crypto, + changeset_t *changeset, + zone_update_t *update, + knot_time_t *expires_at) +{ + assert(!knot_rrset_empty(covered)); + assert(sign_ctx); + assert((bool)changeset != (bool)update); + + knot_rrset_t to_add = create_empty_rrsigs_for(covered); + knot_rrset_t to_remove = create_empty_rrsigs_for(covered); + int result = (!rrsig_covers_type(rrsigs, covered->type) ? KNOT_EOK : + knot_synth_rrsig(covered->type, &rrsigs->rrs, &to_remove.rrs, NULL)); + + if (result == KNOT_EOK && sign_ctx->dnssec_ctx->offline_records.rrsig.rrs.count > 0 && + knot_dname_cmp(sign_ctx->dnssec_ctx->offline_records.rrsig.owner, covered->owner) == 0 && + rrsig_covers_type(&sign_ctx->dnssec_ctx->offline_records.rrsig, covered->type)) { + result = knot_synth_rrsig(covered->type, + &sign_ctx->dnssec_ctx->offline_records.rrsig.rrs, &to_add.rrs, NULL); + if (result == KNOT_EOK) { + // don't remove what shall be added + result = knot_rdataset_subtract(&to_remove.rrs, &to_add.rrs, NULL); + } + if (result == KNOT_EOK && !knot_rrset_empty(rrsigs)) { + // don't add what's already present + result = knot_rdataset_subtract(&to_add.rrs, &rrsigs->rrs, NULL); + } + } + + for (size_t i = 0; i < sign_ctx->count && result == KNOT_EOK; i++) { + const zone_key_t *key = &sign_ctx->keys[i]; + if (!knot_zone_sign_use_key(key, covered)) { + continue; + } + + uint16_t valid_at; + knot_timediff_t refresh = sign_ctx->dnssec_ctx->policy->rrsig_refresh_before + + sign_ctx->dnssec_ctx->policy->rrsig_prerefresh; + if (valid_signature_exists(covered, rrsigs, key->key, sign_ctx->sign_ctxs[i], + sign_ctx->dnssec_ctx, refresh, skip_crypto, NULL, &valid_at)) { + knot_rdata_t *valid_rr = knot_rdataset_at(&rrsigs->rrs, valid_at); + result = knot_rdataset_remove(&to_remove.rrs, valid_rr, NULL); + note_earliest_expiration(valid_rr, sign_ctx->dnssec_ctx->now, expires_at); + continue; + } + result = knot_sign_rrset(&to_add, covered, key->key, sign_ctx->sign_ctxs[i], + sign_ctx->dnssec_ctx, NULL, expires_at); + } + + if (!knot_rrset_empty(&to_remove) && result == KNOT_EOK) { + if (changeset != NULL) { + result = changeset_add_removal(changeset, &to_remove, 0); + } else { + result = zone_update_remove(update, &to_remove); + } + } + + if (!knot_rrset_empty(&to_add) && result == KNOT_EOK) { + if (changeset != NULL) { + result = changeset_add_addition(changeset, &to_add, 0); + } else { + result = zone_update_add(update, &to_add); + } + } + + knot_rdataset_clear(&to_add.rrs, NULL); + knot_rdataset_clear(&to_remove.rrs, NULL); + + return result; +} + +static bool key_used(bool ksk, bool zsk, uint16_t type, + const knot_dname_t *owner, const knot_dname_t *zone_apex) +{ + if (knot_dname_cmp(owner, zone_apex) != 0) { + return zsk; + } + switch (type) { + case KNOT_RRTYPE_DNSKEY: + case KNOT_RRTYPE_CDNSKEY: + case KNOT_RRTYPE_CDS: + return ksk; + default: + return zsk; + } +} + +int knot_validate_rrsigs(const knot_rrset_t *covered, + const knot_rrset_t *rrsigs, + zone_sign_ctx_t *sign_ctx, + bool skip_crypto) +{ + if (covered == NULL || rrsigs == NULL || sign_ctx == NULL) { + return KNOT_EINVAL; + } + + bool valid_exists = false; + int ret = KNOT_EOK; + for (size_t i = 0; i < sign_ctx->count; i++) { + const knot_kasp_key_t *key = &sign_ctx->dnssec_ctx->zone->keys[i]; + if (!key_used(key->is_ksk, key->is_zsk, covered->type, + covered->owner, sign_ctx->dnssec_ctx->zone->dname)) { + continue; + } + + uint16_t valid_at; + if (valid_signature_exists(covered, rrsigs, key->key, sign_ctx->sign_ctxs[i], + sign_ctx->dnssec_ctx, 0, skip_crypto, &ret, &valid_at)) { + valid_exists = true; + } + } + + return valid_exists ? ret : KNOT_DNSSEC_ENOSIG; +} + +/*! + * \brief Add all RRSIGs into the changeset for removal. + * + * \param covered RR set with covered records. + * \param changeset Changeset to be updated. + * + * \return Error code, KNOT_EOK if successful. + */ +static int remove_rrset_rrsigs(const knot_dname_t *owner, uint16_t type, + const knot_rrset_t *rrsigs, + changeset_t *changeset) +{ + assert(owner); + assert(changeset); + knot_rrset_t synth_rrsig; + knot_rrset_init(&synth_rrsig, (knot_dname_t *)owner, + KNOT_RRTYPE_RRSIG, rrsigs->rclass, rrsigs->ttl); + int ret = knot_synth_rrsig(type, &rrsigs->rrs, &synth_rrsig.rrs, NULL); + if (ret != KNOT_EOK) { + if (ret != KNOT_ENOENT) { + return ret; + } + return KNOT_EOK; + } + + ret = changeset_add_removal(changeset, &synth_rrsig, 0); + knot_rdataset_clear(&synth_rrsig.rrs, NULL); + + return ret; +} + +/*! + * \brief Drop all existing and create new RRSIGs for covered records. + * + * \param covered RR set with covered records. + * \param rrsigs Existing RRSIGs for covered RR set. + * \param sign_ctx Local zone signing context. + * \param changeset Changeset to be updated. + * + * \return Error code, KNOT_EOK if successful. + */ +static int force_resign_rrset(const knot_rrset_t *covered, + const knot_rrset_t *rrsigs, + zone_sign_ctx_t *sign_ctx, + changeset_t *changeset) +{ + assert(!knot_rrset_empty(covered)); + + if (!knot_rrset_empty(rrsigs)) { + int result = remove_rrset_rrsigs(covered->owner, covered->type, + rrsigs, changeset); + if (result != KNOT_EOK) { + return result; + } + } + + return add_missing_rrsigs(covered, NULL, sign_ctx, false, changeset, NULL, NULL); +} + +/*! + * \brief Drop all expired and create new RRSIGs for covered records. + * + * \param covered RR set with covered records. + * \param rrsigs Existing RRSIGs for covered RR set. + * \param sign_ctx Local zone signing context. + * \param skip_crypto All RRSIGs in this node have been verified, just check validity. + * \param changeset Changeset to be updated. + * \param expires_at Current earliest expiration, will be updated. + * + * \return Error code, KNOT_EOK if successful. + */ +static int resign_rrset(const knot_rrset_t *covered, + const knot_rrset_t *rrsigs, + zone_sign_ctx_t *sign_ctx, + bool skip_crypto, + changeset_t *changeset, + knot_time_t *expires_at) +{ + assert(!knot_rrset_empty(covered)); + + return add_missing_rrsigs(covered, rrsigs, sign_ctx, skip_crypto, changeset, NULL, expires_at); +} + +static int remove_standalone_rrsigs(const zone_node_t *node, + const knot_rrset_t *rrsigs, + changeset_t *changeset) +{ + if (rrsigs == NULL) { + return KNOT_EOK; + } + + uint16_t rrsigs_rdata_count = rrsigs->rrs.count; + knot_rdata_t *rdata = rrsigs->rrs.rdata; + for (uint16_t i = 0; i < rrsigs_rdata_count; ++i) { + uint16_t type_covered = knot_rrsig_type_covered(rdata); + if (!node_rrtype_exists(node, type_covered)) { + knot_rrset_t to_remove; + knot_rrset_init(&to_remove, rrsigs->owner, rrsigs->type, + rrsigs->rclass, rrsigs->ttl); + int ret = knot_rdataset_add(&to_remove.rrs, rdata, NULL); + if (ret != KNOT_EOK) { + return ret; + } + ret = changeset_add_removal(changeset, &to_remove, 0); + knot_rdataset_clear(&to_remove.rrs, NULL); + if (ret != KNOT_EOK) { + return ret; + } + } + rdata = knot_rdataset_next(rdata); + } + + return KNOT_EOK; +} + +/*! + * \brief Update RRSIGs in a given node by updating changeset. + * + * \param node Node to be signed. + * \param sign_ctx Local zone signing context. + * \param changeset Changeset to be updated. + * \param expires_at Current earliest expiration, will be updated. + * + * \return Error code, KNOT_EOK if successful. + */ +static int sign_node_rrsets(const zone_node_t *node, + zone_sign_ctx_t *sign_ctx, + changeset_t *changeset, + knot_time_t *expires_at, + dnssec_validation_hint_t *hint) +{ + assert(node); + assert(sign_ctx); + + int result = KNOT_EOK; + knot_rrset_t rrsigs = node_rrset(node, KNOT_RRTYPE_RRSIG); + bool skip_crypto = (node->flags & NODE_FLAGS_RRSIGS_VALID) && + !sign_ctx->dnssec_ctx->keytag_conflict; + + for (int i = 0; result == KNOT_EOK && i < node->rrset_count; i++) { + knot_rrset_t rrset = node_rrset_at(node, i); + assert(rrset.type != KNOT_RRTYPE_ANY); + + if (!knot_zone_sign_rr_should_be_signed(node, &rrset)) { + if (!sign_ctx->dnssec_ctx->validation_mode) { + result = remove_rrset_rrsigs(rrset.owner, rrset.type, &rrsigs, changeset); + } else { + if (knot_synth_rrsig_exists(rrset.type, &rrsigs.rrs)) { + hint->node = node->owner; + hint->rrtype = rrset.type; + result = KNOT_DNSSEC_ENOSIG; + } + } + continue; + } + + if (sign_ctx->dnssec_ctx->validation_mode) { + result = knot_validate_rrsigs(&rrset, &rrsigs, sign_ctx, skip_crypto); + if (result != KNOT_EOK) { + hint->node = node->owner; + hint->rrtype = rrset.type; + } + } else if (sign_ctx->dnssec_ctx->rrsig_drop_existing) { + result = force_resign_rrset(&rrset, &rrsigs, + sign_ctx, changeset); + } else { + result = resign_rrset(&rrset, &rrsigs, sign_ctx, skip_crypto, + changeset, expires_at); + } + } + + if (result == KNOT_EOK) { + result = remove_standalone_rrsigs(node, &rrsigs, changeset); + } + return result; +} + +/*! + * \brief Struct to carry data for 'sign_data' callback function. + */ +typedef struct { + zone_tree_t *tree; + zone_sign_ctx_t *sign_ctx; + changeset_t changeset; + knot_time_t expires_at; + dnssec_validation_hint_t *hint; + size_t num_threads; + size_t thread_index; + size_t rrset_index; + int errcode; + int thread_init_errcode; + pthread_t thread; +} node_sign_args_t; + +/*! + * \brief Sign node (callback function). + * + * \param node Node to be signed. + * \param data Callback data, node_sign_args_t. + */ +static int sign_node(zone_node_t *node, void *data) +{ + assert(node); + assert(data); + + node_sign_args_t *args = (node_sign_args_t *)data; + + if (node->rrset_count == 0) { + return KNOT_EOK; + } + + if (args->rrset_index++ % args->num_threads != args->thread_index) { + return KNOT_EOK; + } + + int result = sign_node_rrsets(node, args->sign_ctx, + &args->changeset, &args->expires_at, + args->hint); + + return result; +} + +static void *tree_sign_thread(void *_arg) +{ + node_sign_args_t *arg = _arg; + arg->errcode = zone_tree_apply(arg->tree, sign_node, _arg); + return NULL; +} + +static int set_signed(zone_node_t *node, _unused_ void *data) +{ + node->flags |= NODE_FLAGS_RRSIGS_VALID; + return KNOT_EOK; +} + +/*! + * \brief Update RRSIGs in a given zone tree by updating changeset. + * + * \param tree Zone tree to be signed. + * \param num_threads Number of threads to use for parallel signing. + * \param zone_keys Zone keys. + * \param policy DNSSEC policy. + * \param update Zone update structure to be updated. + * \param expires_at Expiration time of the oldest signature in zone. + * + * \return Error code, KNOT_EOK if successful. + */ +static int zone_tree_sign(zone_tree_t *tree, + size_t num_threads, + zone_keyset_t *zone_keys, + const kdnssec_ctx_t *dnssec_ctx, + zone_update_t *update, + knot_time_t *expires_at) +{ + assert(zone_keys || dnssec_ctx->validation_mode); + assert(dnssec_ctx); + assert(update || dnssec_ctx->validation_mode); + + int ret = KNOT_EOK; + node_sign_args_t args[num_threads]; + memset(args, 0, sizeof(args)); + *expires_at = knot_time_plus(dnssec_ctx->now, dnssec_ctx->policy->rrsig_lifetime); + + // init context structures + for (size_t i = 0; i < num_threads; i++) { + args[i].tree = tree; + args[i].sign_ctx = dnssec_ctx->validation_mode + ? zone_validation_ctx(dnssec_ctx) + : zone_sign_ctx(zone_keys, dnssec_ctx); + if (args[i].sign_ctx == NULL) { + ret = KNOT_ENOMEM; + break; + } + ret = changeset_init(&args[i].changeset, dnssec_ctx->zone->dname); + if (ret != KNOT_EOK) { + break; + } + args[i].expires_at = 0; + args[i].hint = &update->validation_hint; + args[i].num_threads = num_threads; + args[i].thread_index = i; + args[i].rrset_index = 0; + args[i].errcode = KNOT_EOK; + args[i].thread_init_errcode = -1; + } + if (ret != KNOT_EOK) { + for (size_t i = 0; i < num_threads; i++) { + changeset_clear(&args[i].changeset); + zone_sign_ctx_free(args[i].sign_ctx); + } + return ret; + } + + if (num_threads == 1) { + args[0].thread_init_errcode = 0; + tree_sign_thread(&args[0]); + } else { + // start working threads + for (size_t i = 0; i < num_threads; i++) { + args[i].thread_init_errcode = + pthread_create(&args[i].thread, NULL, tree_sign_thread, &args[i]); + } + + // join those threads that have been really started + for (size_t i = 0; i < num_threads; i++) { + if (args[i].thread_init_errcode == 0) { + args[i].thread_init_errcode = pthread_join(args[i].thread, NULL); + } + } + } + + // collect return code and results + for (size_t i = 0; i < num_threads; i++) { + if (ret == KNOT_EOK) { + if (args[i].thread_init_errcode != 0) { + ret = knot_map_errno_code(args[i].thread_init_errcode); + } else { + ret = args[i].errcode; + if (ret == KNOT_EOK && !dnssec_ctx->validation_mode) { + ret = zone_update_apply_changeset(update, &args[i].changeset); // _fix not needed + *expires_at = knot_time_min(*expires_at, args[i].expires_at); + } + } + } + assert(!dnssec_ctx->validation_mode || changeset_empty(&args[i].changeset)); + changeset_clear(&args[i].changeset); + zone_sign_ctx_free(args[i].sign_ctx); + } + + return ret; +} + +/*- private API - signing of NSEC(3) in changeset ----------------------------*/ + +/*! + * \brief Struct to carry data for changeset signing callback functions. + */ +typedef struct { + const zone_contents_t *zone; + changeset_iter_t itt; + zone_sign_ctx_t *sign_ctx; + changeset_t changeset; + knot_time_t expires_at; + size_t num_threads; + size_t thread_index; + size_t rrset_index; + int errcode; + int thread_init_errcode; + pthread_t thread; +} changeset_signing_data_t; + +int rrset_add_zone_key(knot_rrset_t *rrset, zone_key_t *zone_key) +{ + if (rrset == NULL || zone_key == NULL) { + return KNOT_EINVAL; + } + + dnssec_binary_t dnskey_rdata = { 0 }; + dnssec_key_get_rdata(zone_key->key, &dnskey_rdata); + + return knot_rrset_add_rdata(rrset, dnskey_rdata.data, dnskey_rdata.size, NULL); +} + +static int rrset_add_zone_ds(knot_rrset_t *rrset, zone_key_t *zone_key, dnssec_key_digest_t dt) +{ + assert(rrset); + assert(zone_key); + + dnssec_binary_t cds_rdata = { 0 }; + zone_key_calculate_ds(zone_key, dt, &cds_rdata); + + return knot_rrset_add_rdata(rrset, cds_rdata.data, cds_rdata.size, NULL); +} + +int knot_zone_sign(zone_update_t *update, + zone_keyset_t *zone_keys, + const kdnssec_ctx_t *dnssec_ctx, + knot_time_t *expire_at) +{ + if (!update || !dnssec_ctx || !expire_at || + dnssec_ctx->policy->signing_threads < 1 || + (zone_keys == NULL && !dnssec_ctx->validation_mode)) { + return KNOT_EINVAL; + } + + int result; + + knot_time_t normal_expire = 0; + result = zone_tree_sign(update->new_cont->nodes, dnssec_ctx->policy->signing_threads, + zone_keys, dnssec_ctx, update, &normal_expire); + if (result != KNOT_EOK) { + return result; + } + + knot_time_t nsec3_expire = 0; + result = zone_tree_sign(update->new_cont->nsec3_nodes, dnssec_ctx->policy->signing_threads, + zone_keys, dnssec_ctx, update, &nsec3_expire); + if (result != KNOT_EOK) { + return result; + } + + bool whole = !(update->flags & UPDATE_INCREMENTAL); + result = zone_tree_apply(whole ? update->new_cont->nodes : update->a_ctx->node_ptrs, set_signed, NULL); + if (result == KNOT_EOK) { + result = zone_tree_apply(whole ? update->new_cont->nsec3_nodes : update->a_ctx->nsec3_ptrs, set_signed, NULL); + } + + *expire_at = knot_time_min(normal_expire, nsec3_expire); + + return result; +} + +keyptr_dynarray_t knot_zone_sign_get_cdnskeys(const kdnssec_ctx_t *ctx, + zone_keyset_t *zone_keys) +{ + keyptr_dynarray_t r = { 0 }; + unsigned crp = ctx->policy->cds_cdnskey_publish; + unsigned cds_published = 0; + uint8_t ready_alg = 0; + + if (crp == CDS_CDNSKEY_ROLLOVER || crp == CDS_CDNSKEY_ALWAYS || + crp == CDS_CDNSKEY_DOUBLE_DS) { + // first, add strictly-ready keys + for (int i = 0; i < zone_keys->count; i++) { + zone_key_t *key = &zone_keys->keys[i]; + if (key->is_ready) { + assert(key->is_ksk); + ready_alg = dnssec_key_get_algorithm(key->key); + keyptr_dynarray_add(&r, &key); + if (!key->is_pub_only) { + cds_published++; + } + } + } + + // second, add active keys + if ((crp == CDS_CDNSKEY_ALWAYS && cds_published == 0) || + (crp == CDS_CDNSKEY_DOUBLE_DS)) { + for (int i = 0; i < zone_keys->count; i++) { + zone_key_t *key = &zone_keys->keys[i]; + if (key->is_ksk && key->is_active && !key->is_ready && + (cds_published == 0 || ready_alg == dnssec_key_get_algorithm(key->key))) { + keyptr_dynarray_add(&r, &key); + } + } + } + + if ((crp != CDS_CDNSKEY_DOUBLE_DS && cds_published > 1) || + (cds_published > 2)) { + log_zone_warning(ctx->zone->dname, "DNSSEC, published CDS/CDNSKEY records for too many (%u) keys", cds_published); + } + } + + return r; +} + +int knot_zone_sign_add_dnskeys(zone_keyset_t *zone_keys, const kdnssec_ctx_t *dnssec_ctx, + key_records_t *add_r, key_records_t *rem_r, key_records_t *orig_r) +{ + if (add_r == NULL || (rem_r != NULL && orig_r == NULL)) { + return KNOT_EINVAL; + } + + bool incremental = (dnssec_ctx->policy->incremental && rem_r != NULL); + dnssec_key_digest_t cds_dt = dnssec_ctx->policy->cds_dt; + int ret = KNOT_EOK; + + for (int i = 0; i < zone_keys->count; i++) { + zone_key_t *key = &zone_keys->keys[i]; + if (key->is_public) { + ret = rrset_add_zone_key(&add_r->dnskey, key); + } else if (incremental) { + ret = rrset_add_zone_key(&rem_r->dnskey, key); + } + + // add all possible known CDNSKEYs and CDSs to removals. Sort it out later + if (incremental && ret == KNOT_EOK) { + ret = rrset_add_zone_key(&rem_r->cdnskey, key); + } + if (incremental && ret == KNOT_EOK) { + ret = rrset_add_zone_ds(&rem_r->cds, key, cds_dt); + } + + if (ret != KNOT_EOK) { + return ret; + } + } + + keyptr_dynarray_t kcdnskeys = knot_zone_sign_get_cdnskeys(dnssec_ctx, zone_keys); + knot_dynarray_foreach(keyptr, zone_key_t *, ksk_for_cds, kcdnskeys) { + ret = rrset_add_zone_key(&add_r->cdnskey, *ksk_for_cds); + if (ret == KNOT_EOK) { + ret = rrset_add_zone_ds(&add_r->cds, *ksk_for_cds, cds_dt); + } + } + + if (incremental && ret == KNOT_EOK) { // else rem_r is empty + ret = key_records_subtract(rem_r, add_r); + if (ret == KNOT_EOK) { + ret = key_records_intersect(rem_r, orig_r); + } + if (ret == KNOT_EOK) { + ret = key_records_subtract(add_r, orig_r); + } + } + + if (dnssec_ctx->policy->cds_cdnskey_publish == CDS_CDNSKEY_EMPTY && ret == KNOT_EOK) { + const uint8_t cdnskey_empty[5] = { 0, 0, 3, 0, 0 }; + const uint8_t cds_empty[5] = { 0, 0, 0, 0, 0 }; + ret = knot_rrset_add_rdata(&add_r->cdnskey, cdnskey_empty, sizeof(cdnskey_empty), NULL); + if (ret == KNOT_EOK) { + ret = knot_rrset_add_rdata(&add_r->cds, cds_empty, sizeof(cds_empty), NULL); + } + } + + keyptr_dynarray_free(&kcdnskeys); + return ret; +} + +int knot_zone_sign_update_dnskeys(zone_update_t *update, + zone_keyset_t *zone_keys, + kdnssec_ctx_t *dnssec_ctx) +{ + if (update == NULL || zone_keys == NULL || dnssec_ctx == NULL) { + return KNOT_EINVAL; + } + + if (dnssec_ctx->policy->unsafe & UNSAFE_DNSKEY) { + return KNOT_EOK; + } + + const zone_node_t *apex = update->new_cont->apex; + knot_rrset_t soa = node_rrset(apex, KNOT_RRTYPE_SOA); + if (knot_rrset_empty(&soa)) { + return KNOT_EINVAL; + } + + key_records_t orig_r; + key_records_from_apex(apex, &orig_r); + + changeset_t ch; + int ret = changeset_init(&ch, apex->owner); + if (ret != KNOT_EOK) { + return ret; + } + + if (!dnssec_ctx->policy->incremental) { + // remove all. This will cancel out with additions later + ret = key_records_to_changeset(&orig_r, &ch, true, 0); + if (ret != KNOT_EOK) { + return ret; + } + } + + key_records_t add_r, rem_r; + key_records_init(dnssec_ctx, &add_r); + key_records_init(dnssec_ctx, &rem_r); + +#define CHECK_RET if (ret != KNOT_EOK) goto cleanup + + if (dnssec_ctx->policy->offline_ksk) { + key_records_t *r = &dnssec_ctx->offline_records; + log_zone_info(dnssec_ctx->zone->dname, + "DNSSEC, using offline records, DNSKEYs %hu, CDNSKEYs %hu, CDs %hu, RRSIGs %hu", + r->dnskey.rrs.count, r->cdnskey.rrs.count, r->cds.rrs.count, r->rrsig.rrs.count); + ret = key_records_to_changeset(r, &ch, false, CHANGESET_CHECK); + CHECK_RET; + } else { + ret = knot_zone_sign_add_dnskeys(zone_keys, dnssec_ctx, &add_r, &rem_r, &orig_r); + CHECK_RET; + ret = key_records_to_changeset(&rem_r, &ch, true, CHANGESET_CHECK); + CHECK_RET; + ret = key_records_to_changeset(&add_r, &ch, false, CHANGESET_CHECK); + CHECK_RET; + } + + if (dnssec_ctx->policy->ds_push && node_rrtype_exists(ch.add->apex, KNOT_RRTYPE_CDS)) { + // there is indeed a change to CDS + update->zone->timers.next_ds_push = time(NULL) + dnssec_ctx->policy->propagation_delay; + zone_events_schedule_at(update->zone, ZONE_EVENT_DS_PUSH, update->zone->timers.next_ds_push); + } + + ret = zone_update_apply_changeset(update, &ch); + +#undef CHECK_RET + +cleanup: + key_records_clear(&add_r); + key_records_clear(&rem_r); + changeset_clear(&ch); + return ret; +} + +bool knot_zone_sign_use_key(const zone_key_t *key, const knot_rrset_t *covered) +{ + if (key == NULL || covered == NULL) { + return false; + } + + bool active_ksk = ((key->is_active || key->is_ksk_active_plus) && key->is_ksk); + bool active_zsk = ((key->is_active || key->is_zsk_active_plus) && key->is_zsk);; + + // this may be a problem with offline KSK + bool cds_sign_by_ksk = true; + + assert(key->is_zsk || key->is_ksk); + bool is_apex = knot_dname_is_equal(covered->owner, + dnssec_key_get_dname(key->key)); + if (!is_apex) { + return active_zsk; + } + + switch (covered->type) { + case KNOT_RRTYPE_DNSKEY: + return active_ksk; + case KNOT_RRTYPE_CDS: + case KNOT_RRTYPE_CDNSKEY: + return (cds_sign_by_ksk ? active_ksk : active_zsk); + default: + return active_zsk; + } +} + +static int sign_in_changeset(zone_node_t *node, uint16_t rrtype, knot_rrset_t *rrsigs, + zone_sign_ctx_t *sign_ctx, int ret_prev, + bool skip_crypto, zone_update_t *up) +{ + if (ret_prev != KNOT_EOK) { + return ret_prev; + } + knot_rrset_t rr = node_rrset(node, rrtype); + if (knot_rrset_empty(&rr)) { + return KNOT_EOK; + } + return add_missing_rrsigs(&rr, rrsigs, sign_ctx, skip_crypto, NULL, up, NULL); +} + +int knot_zone_sign_nsecs_in_changeset(const zone_keyset_t *zone_keys, + const kdnssec_ctx_t *dnssec_ctx, + zone_update_t *update) +{ + if (zone_keys == NULL || dnssec_ctx == NULL || update == NULL) { + return KNOT_EINVAL; + } + + zone_sign_ctx_t *sign_ctx = zone_sign_ctx(zone_keys, dnssec_ctx); + if (sign_ctx == NULL) { + return KNOT_ENOMEM; + } + + zone_tree_it_t it = { 0 }; + int ret = zone_tree_it_double_begin(update->a_ctx->node_ptrs, update->a_ctx->nsec3_ptrs, &it); + + while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) { + zone_node_t *n = zone_tree_it_val(&it); + bool skip_crypto = (n->flags & NODE_FLAGS_RRSIGS_VALID) && !dnssec_ctx->keytag_conflict; + + knot_rrset_t rrsigs = node_rrset(n, KNOT_RRTYPE_RRSIG); + ret = sign_in_changeset(n, KNOT_RRTYPE_NSEC, &rrsigs, sign_ctx, ret, skip_crypto, update); + ret = sign_in_changeset(n, KNOT_RRTYPE_NSEC3, &rrsigs, sign_ctx, ret, skip_crypto, update); + ret = sign_in_changeset(n, KNOT_RRTYPE_NSEC3PARAM, &rrsigs, sign_ctx, ret, skip_crypto, update); + + if (ret == KNOT_EOK) { + n->flags |= NODE_FLAGS_RRSIGS_VALID; // non-NSEC RRSIGs had been validated in knot_dnssec_sign_update() + } + + zone_tree_it_next(&it); + } + zone_tree_it_free(&it); + zone_sign_ctx_free(sign_ctx); + + return ret; +} + +bool knot_zone_sign_rr_should_be_signed(const zone_node_t *node, + const knot_rrset_t *rrset) +{ + if (node == NULL || knot_rrset_empty(rrset)) { + return false; + } + + if (rrset->type == KNOT_RRTYPE_RRSIG || (node->flags & NODE_FLAGS_NONAUTH)) { + return false; + } + + // At delegation points we only want to sign NSECs and DSs + if (node->flags & NODE_FLAGS_DELEG) { + if (!(rrset->type == KNOT_RRTYPE_NSEC || + rrset->type == KNOT_RRTYPE_DS)) { + return false; + } + } + + return true; +} + +int knot_zone_sign_update(zone_update_t *update, + zone_keyset_t *zone_keys, + const kdnssec_ctx_t *dnssec_ctx, + knot_time_t *expire_at) +{ + if (update == NULL || dnssec_ctx == NULL || expire_at == NULL || + dnssec_ctx->policy->signing_threads < 1 || + (zone_keys == NULL && !dnssec_ctx->validation_mode)) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + + /* Check if the UPDATE changed DNSKEYs or NSEC3PARAM. + * If so, we have to sign the whole zone. */ + const bool full_sign = apex_dnssec_changed(update); + if (full_sign) { + ret = knot_zone_sign(update, zone_keys, dnssec_ctx, expire_at); + } else { + ret = zone_tree_sign(update->a_ctx->node_ptrs, dnssec_ctx->policy->signing_threads, + zone_keys, dnssec_ctx, update, expire_at); + if (ret == KNOT_EOK) { + ret = zone_tree_apply(update->a_ctx->node_ptrs, set_signed, NULL); + } + if (ret == KNOT_EOK && dnssec_ctx->validation_mode) { + ret = zone_tree_sign(update->a_ctx->nsec3_ptrs, dnssec_ctx->policy->signing_threads, + zone_keys, dnssec_ctx, update, expire_at); + } + if (ret == KNOT_EOK && dnssec_ctx->validation_mode) { + ret = zone_tree_apply(update->a_ctx->nsec3_ptrs, set_signed, NULL); + } + } + + return ret; +} + +int knot_zone_sign_apex_rr(zone_update_t *update, uint16_t rrtype, + const zone_keyset_t *zone_keys, + const kdnssec_ctx_t *dnssec_ctx) +{ + knot_rrset_t rr = node_rrset(update->new_cont->apex, rrtype); + knot_rrset_t rrsig = node_rrset(update->new_cont->apex, KNOT_RRTYPE_RRSIG); + changeset_t ch; + int ret = changeset_init(&ch, update->zone->name); + if (ret == KNOT_EOK) { + zone_sign_ctx_t *sign_ctx = zone_sign_ctx(zone_keys, dnssec_ctx); + if (sign_ctx == NULL) { + changeset_clear(&ch); + return KNOT_ENOMEM; + } + ret = force_resign_rrset(&rr, &rrsig, sign_ctx, &ch); + if (ret == KNOT_EOK) { + ret = zone_update_apply_changeset(update, &ch); + } + zone_sign_ctx_free(sign_ctx); + } + changeset_clear(&ch); + return ret; +} diff --git a/src/knot/dnssec/zone-sign.h b/src/knot/dnssec/zone-sign.h new file mode 100644 index 0000000..ba6e2b2 --- /dev/null +++ b/src/knot/dnssec/zone-sign.h @@ -0,0 +1,162 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/updates/changesets.h" +#include "knot/updates/zone-update.h" +#include "knot/zone/contents.h" +#include "knot/dnssec/context.h" +#include "knot/dnssec/zone-keys.h" + +int rrset_add_zone_key(knot_rrset_t *rrset, zone_key_t *zone_key); + +bool rrsig_covers_type(const knot_rrset_t *rrsig, uint16_t type); + +/*! + * \brief Prepare DNSKEYs, CDNSKEYs and CDSs to be added to the zone into rrsets. + * + * \param zone_keys Zone keyset. + * \param dnssec_ctx KASP context. + * \param add_r RRSets to be added. + * \param rem_r RRSets to be removed (only for incremental policy). + * \param orig_r RRSets that was originally in zone (only for incremental policy). + * + * \return KNOT_E* + */ +int knot_zone_sign_add_dnskeys(zone_keyset_t *zone_keys, const kdnssec_ctx_t *dnssec_ctx, + key_records_t *add_r, key_records_t *rem_r, key_records_t *orig_r); + +/*! + * \brief Adds/removes DNSKEY (and CDNSKEY, CDS) records to zone according to zone keyset. + * + * \param update Structure holding zone contents and to be updated with changes. + * \param zone_keys Keyset with private keys. + * \param dnssec_ctx KASP context. + * + * \return KNOT_E* + */ +int knot_zone_sign_update_dnskeys(zone_update_t *update, + zone_keyset_t *zone_keys, + kdnssec_ctx_t *dnssec_ctx); + +/*! + * \brief Check if key can be used to sign given RR. + * + * \param key Zone key. + * \param covered RR to be checked. + * + * \return The RR should be signed. + */ +bool knot_zone_sign_use_key(const zone_key_t *key, const knot_rrset_t *covered); + +/*! + * \brief Return those keys for whose the CDNSKEY/CDS records shall be created. + * + * \param ctx DNSSEC context. + * \param zone_keys Zone keyset, including ZSKs. + * + * \return Dynarray containing pointers on some KSKs in keyset. + */ +keyptr_dynarray_t knot_zone_sign_get_cdnskeys(const kdnssec_ctx_t *ctx, + zone_keyset_t *zone_keys); + +/*! + * \brief Check that at least one correct signature exists to at least one DNSKEY and that none incorrect exists. + * + * \param covered RRSet bein validated. + * \param rrsigs RRSIG with signatures. + * \param sign_ctx Signing context (with keys == NULL) + * \param skip_crypto Crypto operations might be skipped as they had been successful earlier. + * + * \return KNOT_E* + */ +int knot_validate_rrsigs(const knot_rrset_t *covered, + const knot_rrset_t *rrsigs, + zone_sign_ctx_t *sign_ctx, + bool skip_crypto); + +/*! + * \brief Update zone signatures and store performed changes in update. + * + * Updates RRSIGs, NSEC(3)s, and DNSKEYs. + * + * \param update Zone Update containing the zone and to be updated with new DNSKEYs and RRSIGs. + * \param zone_keys Zone keys. + * \param dnssec_ctx DNSSEC context. + * \param expire_at Time, when the oldest signature in the zone expires. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_zone_sign(zone_update_t *update, + zone_keyset_t *zone_keys, + const kdnssec_ctx_t *dnssec_ctx, + knot_time_t *expire_at); + +/*! + * \brief Sign NSEC/NSEC3 nodes in changeset and update the changeset. + * + * \param zone_keys Zone keys. + * \param dnssec_ctx DNSSEC context. + * \param changeset Changeset to be updated. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_zone_sign_nsecs_in_changeset(const zone_keyset_t *zone_keys, + const kdnssec_ctx_t *dnssec_ctx, + zone_update_t *update); + +/*! + * \brief Checks whether RRSet in a node has to be signed. Will not return + * true for all types that should be signed, do not use this as an + * universal function, it is implementation specific. + * + * \param node Node containing the RRSet. + * \param rrset RRSet we are checking for. + * + * \retval true if should be signed. + */ +bool knot_zone_sign_rr_should_be_signed(const zone_node_t *node, + const knot_rrset_t *rrset); + +/*! + * \brief Sign updates of the zone, storing new RRSIGs in this update again. + * + * \param update Zone Update structure. + * \param zone_keys Zone keys. + * \param dnssec_ctx DNSSEC context. + * \param expire_at Time, when the oldest signature in the update expires. + * + * \return Error code, KNOT_EOK if successful. + */ +int knot_zone_sign_update(zone_update_t *update, + zone_keyset_t *zone_keys, + const kdnssec_ctx_t *dnssec_ctx, + knot_time_t *expire_at); + +/*! + * \brief Force re-sign of a RRSet in zone apex. + * + * \param update Zone update to be updated. + * \param rrtype Type of the apex RR. + * \param zone_keys Zone keyset. + * \param dnssec_ctx DNSSEC context. + * + * \return KNOT_E* + */ +int knot_zone_sign_apex_rr(zone_update_t *update, uint16_t rrtype, + const zone_keyset_t *zone_keys, + const kdnssec_ctx_t *dnssec_ctx); diff --git a/src/knot/events/events.c b/src/knot/events/events.c new file mode 100644 index 0000000..4dba950 --- /dev/null +++ b/src/knot/events/events.c @@ -0,0 +1,564 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <stdarg.h> +#include <time.h> +#include <unistd.h> +#include <urcu.h> + +#include "libknot/libknot.h" +#include "knot/common/log.h" +#include "knot/events/events.h" +#include "knot/events/handlers.h" +#include "knot/events/replan.h" +#include "knot/zone/zone.h" + +#define ZONE_EVENT_IMMEDIATE 1 /* Fast-track to worker queue. */ + +typedef int (*zone_event_cb)(conf_t *conf, zone_t *zone); + +typedef struct event_info { + zone_event_type_t type; + const zone_event_cb callback; + const char *name; +} event_info_t; + +static const event_info_t EVENT_INFO[] = { + { ZONE_EVENT_LOAD, event_load, "load" }, + { ZONE_EVENT_REFRESH, event_refresh, "refresh" }, + { ZONE_EVENT_UPDATE, event_update, "update" }, + { ZONE_EVENT_EXPIRE, event_expire, "expiration" }, + { ZONE_EVENT_FLUSH, event_flush, "flush" }, + { ZONE_EVENT_BACKUP, event_backup, "backup/restore" }, + { ZONE_EVENT_NOTIFY, event_notify, "notify" }, + { ZONE_EVENT_DNSSEC, event_dnssec, "re-sign" }, + { ZONE_EVENT_UFREEZE, event_ufreeze, "update-freeze" }, + { ZONE_EVENT_UTHAW, event_uthaw, "update-thaw" }, + { ZONE_EVENT_DS_CHECK, event_ds_check, "DS-check" }, + { ZONE_EVENT_DS_PUSH, event_ds_push, "DS-push" }, + { 0 } +}; + +static const event_info_t *get_event_info(zone_event_type_t type) +{ + const event_info_t *info; + for (info = EVENT_INFO; info->callback != NULL; info++) { + if (info->type == type) { + return info; + } + } + + assert(0); + return NULL; +} + +static bool valid_event(zone_event_type_t type) +{ + return (type > ZONE_EVENT_INVALID && type < ZONE_EVENT_COUNT); +} + +bool ufreeze_applies(zone_event_type_t type) +{ + switch (type) { + case ZONE_EVENT_LOAD: + case ZONE_EVENT_REFRESH: + case ZONE_EVENT_UPDATE: + case ZONE_EVENT_FLUSH: + case ZONE_EVENT_DNSSEC: + case ZONE_EVENT_DS_CHECK: + return true; + default: + return false; + } +} + +/*! \brief Return remaining time to planned event (seconds). */ +static time_t time_until(time_t planned) +{ + time_t now = time(NULL); + return now < planned ? (planned - now) : 0; +} + +/*! + * \brief Set time of a given event type. + */ +static void event_set_time(zone_events_t *events, zone_event_type_t type, time_t time) +{ + assert(events); + assert(valid_event(type)); + + events->time[type] = time; +} + +/*! + * \brief Get time of a given event type. + */ +static time_t event_get_time(zone_events_t *events, zone_event_type_t type) +{ + assert(events); + assert(valid_event(type)); + + return events->time[type]; +} + +/*! + * \brief Find next scheduled zone event. + * + * \note Afer the UTHAW event, get_next_event() is also invoked. In that situation, + * all the events are suddenly allowed, and those which were planned into + * the ufrozen interval, start to be performed one-by-one sorted by their times. + * + * \param events Zone events. + * + * \return Zone event type, or ZONE_EVENT_INVALID if no event is scheduled. + */ +static zone_event_type_t get_next_event(zone_events_t *events) +{ + if (!events) { + return ZONE_EVENT_INVALID; + } + + zone_event_type_t next_type = ZONE_EVENT_INVALID; + time_t next = 0; + + for (int i = 0; i < ZONE_EVENT_COUNT; i++) { + time_t current = events->time[i]; + + if ((next == 0 || current < next) && (current != 0) && + (events->forced[i] || !events->ufrozen || !ufreeze_applies(i))) { + next = current; + next_type = i; + } + } + + return next_type; +} + +/*! + * \brief Fined time of next scheduled event. + */ +static time_t get_next_time(zone_events_t *events) +{ + zone_event_type_t type = get_next_event(events); + return valid_event(type) ? event_get_time(events, type) : 0; +} + +/*! + * \brief Cancel scheduled item, schedule first enqueued item. + * + * \param mx_handover events->mx already locked. Take it over and unlock when done. + */ +static void reschedule(zone_events_t *events, bool mx_handover) +{ + assert(events); + + if (!mx_handover) { + pthread_mutex_lock(&events->reschedule_lock); + pthread_mutex_lock(&events->mx); + } + + if (!events->event || events->running || events->frozen) { + pthread_mutex_unlock(&events->mx); + pthread_mutex_unlock(&events->reschedule_lock); + return; + } + + zone_event_type_t type = get_next_event(events); + if (!valid_event(type)) { + pthread_mutex_unlock(&events->mx); + pthread_mutex_unlock(&events->reschedule_lock); + return; + } + + time_t diff = time_until(event_get_time(events, type)); + + pthread_mutex_unlock(&events->mx); + + evsched_schedule(events->event, diff * 1000); + + pthread_mutex_unlock(&events->reschedule_lock); +} + +/*! + * \brief Zone event wrapper, expected to be called from a worker thread. + * + * 1. Takes the next planned event. + * 2. Resets the event's scheduled time (and forced flag). + * 3. Perform the event's callback. + * 4. Schedule next event planned event. + */ +static void event_wrap(worker_task_t *task) +{ + assert(task); + assert(task->ctx); + + zone_t *zone = task->ctx; + zone_events_t *events = &zone->events; + + pthread_mutex_lock(&events->mx); + zone_event_type_t type = get_next_event(events); + pthread_cond_t *blocking = events->blocking[type]; + if (!valid_event(type)) { + events->running = false; + pthread_mutex_unlock(&events->mx); + return; + } + events->type = type; + event_set_time(events, type, 0); + events->forced[type] = false; + pthread_mutex_unlock(&events->mx); + + const event_info_t *info = get_event_info(type); + + /* Create a configuration copy just for this event. */ + conf_t *conf; + rcu_read_lock(); + int ret = conf_clone(&conf); + rcu_read_unlock(); + if (ret == KNOT_EOK) { + /* Execute the event callback. */ + ret = info->callback(conf, zone); + conf_free(conf); + } + + if (ret != KNOT_EOK) { + log_zone_error(zone->name, "zone event '%s' failed (%s)", + info->name, knot_strerror(ret)); + } + + pthread_mutex_lock(&events->reschedule_lock); + pthread_mutex_lock(&events->mx); + events->running = false; + events->type = ZONE_EVENT_INVALID; + + if (blocking != NULL) { + events->blocking[type] = NULL; + events->result[type] = ret; + pthread_cond_broadcast(blocking); + } + + if (events->run_end != NULL) { + pthread_cond_broadcast(events->run_end); + } + + reschedule(events, true); // unlocks events->mx +} + +/*! + * \brief Called by scheduler thread if the event occurs. + */ +static void event_dispatch(event_t *event) +{ + assert(event); + assert(event->data); + + zone_events_t *events = event->data; + + pthread_mutex_lock(&events->mx); + if (!events->running && !events->frozen) { + events->running = true; + worker_pool_assign(events->pool, &events->task); + } + pthread_mutex_unlock(&events->mx); +} + +int zone_events_init(zone_t *zone) +{ + if (!zone) { + return KNOT_EINVAL; + } + + zone_events_t *events = &zone->events; + + memset(&zone->events, 0, sizeof(zone->events)); + pthread_mutex_init(&events->mx, NULL); + pthread_mutex_init(&events->reschedule_lock, NULL); + events->task.ctx = zone; + events->task.run = event_wrap; + + return KNOT_EOK; +} + +int zone_events_setup(struct zone *zone, worker_pool_t *workers, + evsched_t *scheduler) +{ + if (!zone || !workers || !scheduler) { + return KNOT_EINVAL; + } + + event_t *event; + event = evsched_event_create(scheduler, event_dispatch, &zone->events); + if (!event) { + return KNOT_ENOMEM; + } + + zone->events.event = event; + zone->events.pool = workers; + + return KNOT_EOK; +} + +void zone_events_deinit(zone_t *zone) +{ + if (!zone) { + return; + } + + zone_events_t *events = &zone->events; + + pthread_mutex_lock(&events->reschedule_lock); + pthread_mutex_lock(&events->mx); + + evsched_cancel(events->event); + evsched_event_free(events->event); + + pthread_mutex_unlock(&events->mx); + pthread_mutex_destroy(&events->mx); + pthread_mutex_unlock(&events->reschedule_lock); + pthread_mutex_destroy(&events->reschedule_lock); + + memset(events, 0, sizeof(*events)); +} + +void _zone_events_schedule_at(zone_t *zone, ...) +{ + zone_events_t *events = &zone->events; + va_list args; + va_start(args, zone); + + pthread_mutex_lock(&events->reschedule_lock); + pthread_mutex_lock(&events->mx); + + time_t old_next = get_next_time(events); + + // update timers + for (int type = va_arg(args, int); valid_event(type); type = va_arg(args, int)) { + time_t planned = va_arg(args, time_t); + if (planned < 0) { + continue; + } + + time_t current = event_get_time(events, type); + if (current == 0 || (planned == 0 && !events->forced[type]) || + (planned > 0 && planned < current)) { + event_set_time(events, type, planned); + } + } + + // reschedule if changed + time_t next = get_next_time(events); + if (old_next != next) { + reschedule(events, true); // unlocks events->mx + } else { + pthread_mutex_unlock(&events->mx); + pthread_mutex_unlock(&events->reschedule_lock); + } + + va_end(args); +} + +void zone_events_schedule_user(zone_t *zone, zone_event_type_t type) +{ + if (!zone || !valid_event(type)) { + return; + } + + zone_events_t *events = &zone->events; + pthread_mutex_lock(&events->mx); + events->forced[type] = true; + pthread_mutex_unlock(&events->mx); + + zone_events_schedule_now(zone, type); + + // reschedule because get_next_event result changed outside of _zone_events_schedule_at + reschedule(events, false); +} + +int zone_events_schedule_blocking(zone_t *zone, zone_event_type_t type, bool user) +{ + if (!zone || !valid_event(type)) { + return KNOT_EINVAL; + } + + zone_events_t *events = &zone->events; + pthread_cond_t local_cond; + pthread_cond_init(&local_cond, NULL); + + pthread_mutex_lock(&events->mx); + while (events->blocking[type] != NULL) { + pthread_cond_wait(events->blocking[type], &events->mx); + } + events->blocking[type] = &local_cond; + pthread_mutex_unlock(&events->mx); + + if (user) { + zone_events_schedule_user(zone, type); + } else { + zone_events_schedule_now(zone, type); + } + + pthread_mutex_lock(&events->mx); + while (events->blocking[type] == &local_cond) { + pthread_cond_wait(&local_cond, &events->mx); + } + int ret = events->result[type]; + pthread_mutex_unlock(&events->mx); + pthread_cond_destroy(&local_cond); + + return ret; +} + +void zone_events_enqueue(zone_t *zone, zone_event_type_t type) +{ + if (!zone || !valid_event(type)) { + return; + } + + zone_events_t *events = &zone->events; + + pthread_mutex_lock(&events->mx); + + /* Bypass scheduler if no event is running. */ + if (!events->running && !events->frozen && + (!events->ufrozen || !ufreeze_applies(type))) { + events->running = true; + events->type = type; + event_set_time(events, type, ZONE_EVENT_IMMEDIATE); + worker_pool_assign(events->pool, &events->task); + pthread_mutex_unlock(&events->mx); + return; + } + + pthread_mutex_unlock(&events->mx); + + /* Execute as soon as possible. */ + zone_events_schedule_now(zone, type); +} + +void zone_events_freeze(zone_t *zone) +{ + if (!zone) { + return; + } + + zone_events_t *events = &zone->events; + + /* Prevent new events being enqueued. */ + pthread_mutex_lock(&events->reschedule_lock); + pthread_mutex_lock(&events->mx); + events->frozen = true; + pthread_mutex_unlock(&events->mx); + + /* Cancel current event. */ + evsched_cancel(events->event); + pthread_mutex_unlock(&events->reschedule_lock); +} + +void zone_events_freeze_blocking(zone_t *zone) +{ + if (!zone) { + return; + } + + zone_events_freeze(zone); + + zone_events_t *events = &zone->events; + + /* Wait for running event to finish. */ + pthread_cond_t cond; + pthread_cond_init(&cond, NULL); + pthread_mutex_lock(&events->mx); + while (events->running) { + events->run_end = &cond; + pthread_cond_wait(&cond, &events->mx); + } + events->run_end = NULL; + pthread_mutex_unlock(&events->mx); + pthread_cond_destroy(&cond); +} + +void zone_events_start(zone_t *zone) +{ + if (!zone) { + return; + } + + zone_events_t *events = &zone->events; + + /* Unlock the events queue. */ + pthread_mutex_lock(&events->reschedule_lock); + pthread_mutex_lock(&events->mx); + events->frozen = false; + + reschedule(events, true); //unlocks events->mx +} + +time_t zone_events_get_time(const struct zone *zone, zone_event_type_t type) +{ + if (zone == NULL) { + return KNOT_EINVAL; + } + + time_t event_time = KNOT_ENOENT; + zone_events_t *events = (zone_events_t *)&zone->events; + + pthread_mutex_lock(&events->mx); + + /* Get next valid event. */ + if (valid_event(type)) { + event_time = event_get_time(events, type); + } + + pthread_mutex_unlock(&events->mx); + + return event_time; +} + +const char *zone_events_get_name(zone_event_type_t type) +{ + /* Get information about the event and time. */ + const event_info_t *info = get_event_info(type); + if (info == NULL) { + return NULL; + } + + return info->name; +} + +time_t zone_events_get_next(const struct zone *zone, zone_event_type_t *type) +{ + if (zone == NULL || type == NULL) { + return KNOT_EINVAL; + } + + time_t next_time = KNOT_ENOENT; + zone_events_t *events = (zone_events_t *)&zone->events; + + pthread_mutex_lock(&events->mx); + + /* Get time of next valid event. */ + *type = get_next_event(events); + if (valid_event(*type)) { + next_time = event_get_time(events, *type); + } else { + *type = ZONE_EVENT_INVALID; + } + + pthread_mutex_unlock(&events->mx); + + return next_time; +} diff --git a/src/knot/events/events.h b/src/knot/events/events.h new file mode 100644 index 0000000..8ede5fb --- /dev/null +++ b/src/knot/events/events.h @@ -0,0 +1,214 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pthread.h> +#include <stdbool.h> +#include <sys/time.h> + +#include "knot/conf/conf.h" +#include "knot/common/evsched.h" +#include "knot/worker/pool.h" +#include "libknot/db/db.h" + +struct zone; + +typedef enum zone_event_type { + ZONE_EVENT_INVALID = -1, + // supported event types + ZONE_EVENT_LOAD = 0, + ZONE_EVENT_REFRESH, + ZONE_EVENT_UPDATE, + ZONE_EVENT_EXPIRE, + ZONE_EVENT_FLUSH, + ZONE_EVENT_BACKUP, + ZONE_EVENT_NOTIFY, + ZONE_EVENT_DNSSEC, + ZONE_EVENT_UFREEZE, + ZONE_EVENT_UTHAW, + ZONE_EVENT_DS_CHECK, + ZONE_EVENT_DS_PUSH, + // terminator + ZONE_EVENT_COUNT, +} zone_event_type_t; + +typedef struct zone_events { + pthread_mutex_t mx; //!< Mutex protecting the struct. + pthread_mutex_t reschedule_lock;//!< Prevent concurrent reschedule() making mess. + + zone_event_type_t type; //!< Type of running event. + bool running; //!< Some zone event is being run. + pthread_cond_t *run_end; //!< Notify this one after finishing a job. + + bool frozen; //!< Terminated, don't schedule new events. + bool ufrozen; //!< Updates to the zone temporarily frozen by user. + + event_t *event; //!< Scheduler event. + worker_pool_t *pool; //!< Server worker pool. + + worker_task_t task; //!< Event execution context. + time_t time[ZONE_EVENT_COUNT]; //!< Event execution times. + bool forced[ZONE_EVENT_COUNT]; //!< Flag that the event was invoked by user ctl. + pthread_cond_t *blocking[ZONE_EVENT_COUNT]; //!< For blocking events: dispatching cond. + int result[ZONE_EVENT_COUNT]; //!< Event return values (in blocking operations). +} zone_events_t; + +/*! + * \brief Initialize zone events. + * + * The function will not set up the scheduling, use \ref zone_events_setup + * to do that. + * + * \param zone Pointer to zone (context of execution). + * + * \return KNOT_E* + */ +int zone_events_init(struct zone *zone); + +/*! + * \brief Set up zone events execution. + * + * \param zone Zone to setup. + * \param workers Worker thread pool. + * \param scheduler Event scheduler. + * + * \return KNOT_E* + */ +int zone_events_setup(struct zone *zone, worker_pool_t *workers, + evsched_t *scheduler); + +/*! + * \brief Deinitialize zone events. + * + * \param zone Zone whose events we want to deinitialize. + */ +void zone_events_deinit(struct zone *zone); + +/*! + * \brief Enqueue event type for asynchronous execution. + * + * \note This is similar to the scheduling an event for NOW, but it can + * bypass the event scheduler if no event is running at the moment. + * + * \param zone Zone to schedule new event for. + * \param type Type of event. + */ +void zone_events_enqueue(struct zone *zone, zone_event_type_t type); + +/*! + * \brief Schedule new zone event. + * + * The function allows to set multiple events at once. + * + * The function interprets time values (t) as follows: + * + * t > 0: schedule timer for a given time + * t = 0: cancel the timer + * t < 0: ignore change in the timer + * + * If the event is already scheduled, the new time will be set only if the + * new time is earlier than the currently scheduled one. To override the + * check, cancel and schedule the event in a single function call. + * + * \param zone Zone to schedule new event for. + * \param ... Sequence of zone_event_type_t and time_t terminated with + * ZONE_EVENT_INVALID. + */ +void _zone_events_schedule_at(struct zone *zone, ...); + +#define zone_events_schedule_at(zone, events...) \ + _zone_events_schedule_at(zone, events, ZONE_EVENT_INVALID) + +#define zone_events_schedule_now(zone, type) \ + zone_events_schedule_at(zone, type, time(NULL)) + +/*! + * \brief Schedule zone event to now, with forced flag. + */ +void zone_events_schedule_user(struct zone *zone, zone_event_type_t type); + +/*! + * \brief Schedule new zone event as soon as possible and wait for it's + * completion (end of task run), with optional forced flag. + * + * \param zone Zone to schedule new event for. + * \param type Zone event type. + * \param user Forced flag indication. + * + * \return KNOT_E* + */ +int zone_events_schedule_blocking(struct zone *zone, zone_event_type_t type, bool user); + +/*! + * \brief Freeze all zone events and prevent new events from running. + * + * \param zone Zone to freeze events for. + */ +void zone_events_freeze(struct zone *zone); + +/*! + * \brief Freeze zone events and wait for running event to finish. + * + * \param zone Zone to freeze events for. + */ +void zone_events_freeze_blocking(struct zone *zone); + +/*! + * \brief ufreeze_applies + * \param type Type of event to be checked + * \return true / false if user freeze applies + */ +bool ufreeze_applies(zone_event_type_t type); + +/*! + * \brief Start the events processing. + * + * \param zone Zone to start processing for. + */ +void zone_events_start(struct zone *zone); + +/*! + * \brief Return time of the occurrence of the given event. + * + * \param zone Zone to get event time from. + * \param type Event type. + * + * \retval time of the event when event found + * \retval 0 when the event is not planned + * \retval negative value if event is invalid + */ +time_t zone_events_get_time(const struct zone *zone, zone_event_type_t type); + +/*! + * \brief Return text name of the event. + * + * \param type Type of event. + * + * \retval String with event name if it exists. + * \retval NULL if the event does not exist. + */ +const char *zone_events_get_name(zone_event_type_t type); + +/*! + * \brief Return time and type of the next event. + * + * \param zone Zone to get next event from. + * \param type [out] Type of the next event will be stored in the parameter. + * + * \return time of the next event or an error (negative number) + */ +time_t zone_events_get_next(const struct zone *zone, zone_event_type_t *type); diff --git a/src/knot/events/handlers.h b/src/knot/events/handlers.h new file mode 100644 index 0000000..e6dfd6c --- /dev/null +++ b/src/knot/events/handlers.h @@ -0,0 +1,49 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/conf/conf.h" +#include "knot/zone/zone.h" +#include "knot/dnssec/zone-events.h" // zone_sign_reschedule_t + +/*! \brief Loads or reloads potentially changed zone. */ +int event_load(conf_t *conf, zone_t *zone); +/*! \brief Refresh a zone from a master. */ +int event_refresh(conf_t *conf, zone_t *zone); +/*! \brief Processes DDNS updates in the zone's DDNS queue. */ +int event_update(conf_t *conf, zone_t *zone); +/*! \brief Empties in-memory zone contents. */ +int event_expire(conf_t *conf, zone_t *zone); +/*! \brief Flushes zone contents into text file. */ +int event_flush(conf_t *conf, zone_t *zone); +/*! \brief Backs up zone contents, metadata, keys, etc to a directory. */ +int event_backup(conf_t *conf, zone_t *zone); +/*! \brief Sends notify to slaves. */ +int event_notify(conf_t *conf, zone_t *zone); +/*! \brief Signs the zone using its DNSSEC keys, perform key rollovers. */ +int event_dnssec(conf_t *conf, zone_t *zone); +/*! \brief NOT A HANDLER, just a helper function to reschedule based on reschedule_t */ +void event_dnssec_reschedule(conf_t *conf, zone_t *zone, + const zone_sign_reschedule_t *refresh, bool zone_changed); +/*! \brief Freeze those events causing zone contents change. */ +int event_ufreeze(conf_t *conf, zone_t *zone); +/*! \brief Unfreeze zone updates. */ +int event_uthaw(conf_t *conf, zone_t *zone); +/*! \brief When CDS/CDNSKEY published, look for matching DS */ +int event_ds_check(conf_t *conf, zone_t *zone); +/*! \brief After change of CDS/CDNSKEY, push the new DS to parent zone as DDNS. */ +int event_ds_push(conf_t *conf, zone_t *zone); diff --git a/src/knot/events/handlers/backup.c b/src/knot/events/handlers/backup.c new file mode 100644 index 0000000..a6b258c --- /dev/null +++ b/src/knot/events/handlers/backup.c @@ -0,0 +1,71 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <urcu.h> + +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/events/handlers.h" +#include "knot/zone/backup.h" + +int event_backup(conf_t *conf, zone_t *zone) +{ + assert(zone); + + zone_backup_ctx_t *ctx = zone->backup_ctx; + if (ctx == NULL) { + return KNOT_EINVAL; + } + + bool restore = ctx->restore_mode; + + if (!restore && ctx->failed) { + // No need to proceed with already faulty backup. + return KNOT_EOK; + } + + char *back_dir = strdup(ctx->backup_dir); + if (back_dir == NULL) { + return KNOT_ENOMEM; + } + + if (restore) { + // expire zone + zone_contents_t *expired = zone_switch_contents(zone, NULL); + synchronize_rcu(); + knot_sem_wait(&zone->cow_lock); + zone_contents_deep_free(expired); + knot_sem_post(&zone->cow_lock); + zone->zonefile.exists = false; + } + + int ret = zone_backup(conf, zone); + if (ret == KNOT_EOK) { + log_zone_info(zone->name, "zone %s '%s'", + restore ? "restored from" : "backed up to", back_dir); + } else { + log_zone_warning(zone->name, "zone %s failed (%s)", + restore ? "restore" : "backup", knot_strerror(ret)); + } + + if (restore && ret == KNOT_EOK) { + zone_reset(conf, zone); + } + + free(back_dir); + return ret; +} diff --git a/src/knot/events/handlers/dnssec.c b/src/knot/events/handlers/dnssec.c new file mode 100644 index 0000000..8263b0d --- /dev/null +++ b/src/knot/events/handlers/dnssec.c @@ -0,0 +1,116 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/dnssec/zone-events.h" +#include "knot/updates/apply.h" +#include "knot/zone/zone.h" +#include "libknot/errcode.h" + +static void log_dnssec_next(const knot_dname_t *zone, knot_time_t refresh_at) +{ + char time_str[64] = { 0 }; + struct tm time_gm = { 0 }; + time_t refresh = refresh_at; + localtime_r(&refresh, &time_gm); + strftime(time_str, sizeof(time_str), KNOT_LOG_TIME_FORMAT, &time_gm); + if (refresh_at == 0) { + log_zone_warning(zone, "DNSSEC, next signing not scheduled"); + } else { + log_zone_info(zone, "DNSSEC, next signing at %s", time_str); + } +} + +void event_dnssec_reschedule(conf_t *conf, zone_t *zone, + const zone_sign_reschedule_t *refresh, bool zone_changed) +{ + time_t now = time(NULL); + time_t ignore = -1; + knot_time_t refresh_at = refresh->next_sign; + + refresh_at = knot_time_min(refresh_at, refresh->next_rollover); + refresh_at = knot_time_min(refresh_at, refresh->next_nsec3resalt); + + log_dnssec_next(zone->name, (time_t)refresh_at); + + if (refresh->plan_ds_check) { + zone->timers.next_ds_check = now; + } + + zone_events_schedule_at(zone, + ZONE_EVENT_DNSSEC, refresh_at ? (time_t)refresh_at : ignore, + ZONE_EVENT_DS_CHECK, refresh->plan_ds_check ? now : ignore + ); + if (zone_changed) { + zone_schedule_notify(zone, 0); + } +} + +int event_dnssec(conf_t *conf, zone_t *zone) +{ + assert(zone); + + zone_sign_reschedule_t resch = { 0 }; + zone_sign_roll_flags_t r_flags = KEY_ROLL_ALLOW_ALL; + int sign_flags = 0; + bool zone_changed = false; + + if (zone_get_flag(zone, ZONE_FORCE_RESIGN, true)) { + log_zone_info(zone->name, "DNSSEC, dropping previous " + "signatures, re-signing zone"); + sign_flags = ZONE_SIGN_DROP_SIGNATURES; + } else { + log_zone_info(zone->name, "DNSSEC, signing zone"); + sign_flags = 0; + } + + if (zone_get_flag(zone, ZONE_FORCE_KSK_ROLL, true)) { + r_flags |= KEY_ROLL_FORCE_KSK_ROLL; + } + if (zone_get_flag(zone, ZONE_FORCE_ZSK_ROLL, true)) { + r_flags |= KEY_ROLL_FORCE_ZSK_ROLL; + } + + zone_update_t up; + int ret = zone_update_init(&up, zone, UPDATE_INCREMENTAL | UPDATE_NO_CHSET); + if (ret != KNOT_EOK) { + return ret; + } + + ret = knot_dnssec_zone_sign(&up, conf, sign_flags, r_flags, 0, &resch); + if (ret != KNOT_EOK) { + goto done; + } + + zone_changed = !zone_update_no_change(&up); + + ret = zone_update_commit(conf, &up); + if (ret != KNOT_EOK) { + goto done; + } + +done: + // Schedule dependent events + event_dnssec_reschedule(conf, zone, &resch, zone_changed); + + if (ret != KNOT_EOK) { + zone_update_clear(&up); + } + return ret; +} diff --git a/src/knot/events/handlers/ds_check.c b/src/knot/events/handlers/ds_check.c new file mode 100644 index 0000000..0138bed --- /dev/null +++ b/src/knot/events/handlers/ds_check.c @@ -0,0 +1,49 @@ +/* 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/dnssec/ds_query.h" +#include "knot/zone/zone.h" + +int event_ds_check(conf_t *conf, zone_t *zone) +{ + kdnssec_ctx_t ctx = { 0 }; + + int ret = kdnssec_ctx_init(conf, &ctx, zone->name, zone_kaspdb(zone), NULL); + if (ret != KNOT_EOK) { + return ret; + } + + ret = knot_parent_ds_query(conf, &ctx, conf->cache.srv_tcp_remote_io_timeout); + + zone->timers.next_ds_check = 0; + switch (ret) { + case KNOT_NO_READY_KEY: + break; + case KNOT_EOK: + zone_events_schedule_now(zone, ZONE_EVENT_DNSSEC); + break; + default: + if (ctx.policy->ksk_sbm_check_interval > 0) { + time_t next_check = time(NULL) + ctx.policy->ksk_sbm_check_interval; + zone->timers.next_ds_check = next_check; + zone_events_schedule_at(zone, ZONE_EVENT_DS_CHECK, next_check); + } + } + + kdnssec_ctx_deinit(&ctx); + + return KNOT_EOK; // allways ok, if failure it has been rescheduled +} diff --git a/src/knot/events/handlers/ds_push.c b/src/knot/events/handlers/ds_push.c new file mode 100644 index 0000000..11aef75 --- /dev/null +++ b/src/knot/events/handlers/ds_push.c @@ -0,0 +1,277 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/query/query.h" +#include "knot/query/requestor.h" +#include "knot/zone/zone.h" +#include "libknot/errcode.h" + +struct ds_push_data { + const knot_dname_t *zone; + const knot_dname_t *parent_query; + knot_dname_t *parent_soa; + knot_rrset_t del_old_ds; + knot_rrset_t new_ds; + const struct sockaddr *remote; + query_edns_data_t edns; +}; + +#define DS_PUSH_RETRY 600 + +#define DS_PUSH_LOG(priority, zone, remote, reused, fmt, ...) \ + ns_log(priority, zone, LOG_OPERATION_DS_PUSH, LOG_DIRECTION_OUT, remote, \ + reused, fmt, ## __VA_ARGS__) + +static const knot_rdata_t remove_cds = { 5, { 0, 0, 0, 0, 0 } }; + +static int ds_push_begin(knot_layer_t *layer, void *params) +{ + layer->data = params; + + return KNOT_STATE_PRODUCE; +} + +static int parent_soa_produce(struct ds_push_data *data, knot_pkt_t *pkt) +{ + assert(data->parent_query[0] != '\0'); + data->parent_query = knot_wire_next_label(data->parent_query, NULL); + + int ret = knot_pkt_put_question(pkt, data->parent_query, KNOT_CLASS_IN, KNOT_RRTYPE_SOA); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + ret = query_put_edns(pkt, &data->edns); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_CONSUME; +} + +static int ds_push_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct ds_push_data *data = layer->data; + + query_init_pkt(pkt); + + if (data->parent_soa == NULL) { + return parent_soa_produce(data, pkt); + } + + knot_wire_set_opcode(pkt->wire, KNOT_OPCODE_UPDATE); + int ret = knot_pkt_put_question(pkt, data->parent_soa, KNOT_CLASS_IN, KNOT_RRTYPE_SOA); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + knot_pkt_begin(pkt, KNOT_AUTHORITY); + + assert(data->del_old_ds.type == KNOT_RRTYPE_DS); + ret = knot_pkt_put(pkt, KNOT_COMPR_HINT_NONE, &data->del_old_ds, 0); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + assert(data->new_ds.type == KNOT_RRTYPE_DS); + assert(!knot_rrset_empty(&data->new_ds)); + if (knot_rdata_cmp(data->new_ds.rrs.rdata, &remove_cds) != 0) { + // Otherwise only remove DS - it was a special "remove CDS". + ret = knot_pkt_put(pkt, KNOT_COMPR_HINT_NONE, &data->new_ds, 0); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + } + + query_put_edns(pkt, &data->edns); + + return KNOT_STATE_CONSUME; +} + +static const knot_rrset_t *sect_soa(const knot_pkt_t *pkt, knot_section_t sect) +{ + const knot_pktsection_t *s = knot_pkt_section(pkt, sect); + const knot_rrset_t *rr = s->count > 0 ? knot_pkt_rr(s, 0) : NULL; + if (rr == NULL || rr->type != KNOT_RRTYPE_SOA || rr->rrs.count != 1) { + return NULL; + } + return rr; +} + +static int ds_push_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct ds_push_data *data = layer->data; + + if (data->parent_soa != NULL) { + // DS push has already been sent, just finish the action. + return KNOT_STATE_DONE; + } + + const knot_rrset_t *parent_soa = sect_soa(pkt, KNOT_ANSWER); + if (parent_soa != NULL) { + // parent SOA obtained, continue with DS push + data->parent_soa = knot_dname_copy(parent_soa->owner, NULL); + return KNOT_STATE_RESET; + } + + if (data->parent_query[0] == '\0') { + // query for parent SOA systematically fails + DS_PUSH_LOG(LOG_WARNING, data->zone, data->remote, + layer->flags & KNOT_REQUESTOR_REUSED, + "unable to query parent SOA"); + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_RESET; // cut off one more label and re-query +} + +static int ds_push_reset(knot_layer_t *layer) +{ + (void)layer; + return KNOT_STATE_PRODUCE; +} + +static int ds_push_finish(knot_layer_t *layer) +{ + struct ds_push_data *data = layer->data; + free(data->parent_soa); + data->parent_soa = NULL; + return layer->state; +} + +static const knot_layer_api_t DS_PUSH_API = { + .begin = ds_push_begin, + .produce = ds_push_produce, + .reset = ds_push_reset, + .consume = ds_push_consume, + .finish = ds_push_finish, +}; + +static int send_ds_push(conf_t *conf, zone_t *zone, + const conf_remote_t *parent, int timeout) +{ + knot_rrset_t zone_cds = node_rrset(zone->contents->apex, KNOT_RRTYPE_CDS); + if (knot_rrset_empty(&zone_cds)) { + return KNOT_EOK; // No CDS, do nothing. + } + zone_cds.type = KNOT_RRTYPE_DS; + zone_cds.ttl = node_rrset(zone->contents->apex, KNOT_RRTYPE_DNSKEY).ttl; + + struct ds_push_data data = { + .zone = zone->name, + .parent_query = zone->name, + .new_ds = zone_cds, + .remote = (struct sockaddr *)&parent->addr, + .edns = query_edns_data_init(conf, parent->addr.ss_family, 0) + }; + + knot_rrset_init(&data.del_old_ds, zone->name, KNOT_RRTYPE_DS, KNOT_CLASS_ANY, 0); + int ret = knot_rrset_add_rdata(&data.del_old_ds, NULL, 0, NULL); + if (ret != KNOT_EOK) { + return ret; + } + + knot_requestor_t requestor; + knot_requestor_init(&requestor, &DS_PUSH_API, &data, NULL); + + knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL); + if (pkt == NULL) { + knot_rdataset_clear(&data.del_old_ds.rrs, NULL); + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + const struct sockaddr_storage *dst = &parent->addr; + const struct sockaddr_storage *src = &parent->via; + knot_request_t *req = knot_request_make(NULL, dst, src, pkt, &parent->key, 0); + if (req == NULL) { + knot_rdataset_clear(&data.del_old_ds.rrs, NULL); + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + ret = knot_requestor_exec(&requestor, req, timeout); + + if (ret == KNOT_EOK && knot_pkt_ext_rcode(req->resp) == 0) { + DS_PUSH_LOG(LOG_INFO, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "success"); + } else if (knot_pkt_ext_rcode(req->resp) == 0) { + DS_PUSH_LOG(LOG_WARNING, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "failed (%s)", knot_strerror(ret)); + } else { + DS_PUSH_LOG(LOG_WARNING, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "server responded with error '%s'", + knot_pkt_ext_rcode_name(req->resp)); + } + + knot_rdataset_clear(&data.del_old_ds.rrs, NULL); + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + + return ret; +} + +int event_ds_push(conf_t *conf, zone_t *zone) +{ + assert(zone); + + if (zone_contents_is_empty(zone->contents)) { + return KNOT_EOK; + } + + int timeout = conf->cache.srv_tcp_remote_io_timeout; + + conf_val_t ds_push = conf_zone_get(conf, C_DS_PUSH, zone->name); + if (ds_push.code != KNOT_EOK) { + conf_val_t policy_id = conf_zone_get(conf, C_DNSSEC_POLICY, zone->name); + conf_id_fix_default(&policy_id); + ds_push = conf_id_get(conf, C_POLICY, C_DS_PUSH, &policy_id); + } + conf_mix_iter_t iter; + conf_mix_iter_init(conf, &ds_push, &iter); + while (iter.id->code == KNOT_EOK) { + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, iter.id); + size_t addr_count = conf_val_count(&addr); + + int ret = KNOT_EOK; + for (int i = 0; i < addr_count; i++) { + conf_remote_t parent = conf_remote(conf, iter.id, i); + ret = send_ds_push(conf, zone, &parent, timeout); + if (ret == KNOT_EOK) { + zone->timers.next_ds_push = 0; + break; + } + } + + if (ret != KNOT_EOK) { + time_t next_push = time(NULL) + DS_PUSH_RETRY; + zone_events_schedule_at(zone, ZONE_EVENT_DS_PUSH, next_push); + zone->timers.next_ds_push = next_push; + } + + conf_mix_iter_next(&iter); + } + + return KNOT_EOK; +} diff --git a/src/knot/events/handlers/expire.c b/src/knot/events/handlers/expire.c new file mode 100644 index 0000000..d7deedd --- /dev/null +++ b/src/knot/events/handlers/expire.c @@ -0,0 +1,46 @@ +/* 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 <urcu.h> + +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/events/handlers.h" +#include "knot/events/replan.h" +#include "knot/zone/contents.h" +#include "knot/zone/zone.h" + +int event_expire(conf_t *conf, zone_t *zone) +{ + assert(zone); + + zone_contents_t *expired = zone_switch_contents(zone, NULL); + log_zone_info(zone->name, "zone expired"); + + synchronize_rcu(); + knot_sem_wait(&zone->cow_lock); + zone_contents_deep_free(expired); + knot_sem_post(&zone->cow_lock); + + zone->zonefile.exists = false; + + zone->timers.next_expire = time(NULL); + zone->timers.next_refresh = zone->timers.next_expire; + replan_from_timers(conf, zone); + + return KNOT_EOK; +} diff --git a/src/knot/events/handlers/flush.c b/src/knot/events/handlers/flush.c new file mode 100644 index 0000000..65663cb --- /dev/null +++ b/src/knot/events/handlers/flush.c @@ -0,0 +1,33 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <time.h> + +#include "knot/conf/conf.h" +#include "knot/zone/zone.h" + +int event_flush(conf_t *conf, zone_t *zone) +{ + assert(conf); + assert(zone); + + if (zone_contents_is_empty(zone->contents)) { + return KNOT_EOK; + } + + return zone_flush_journal(conf, zone, true); +} diff --git a/src/knot/events/handlers/freeze_thaw.c b/src/knot/events/handlers/freeze_thaw.c new file mode 100644 index 0000000..dfa867f --- /dev/null +++ b/src/knot/events/handlers/freeze_thaw.c @@ -0,0 +1,46 @@ +/* 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/common/log.h" +#include "knot/conf/conf.h" +#include "knot/events/events.h" +#include "knot/zone/zone.h" + +int event_ufreeze(conf_t *conf, zone_t *zone) +{ + assert(zone); + + pthread_mutex_lock(&zone->events.mx); + zone->events.ufrozen = true; + pthread_mutex_unlock(&zone->events.mx); + + log_zone_info(zone->name, "zone updates frozen"); + + return KNOT_EOK; +} + +int event_uthaw(conf_t *conf, zone_t *zone) +{ + assert(zone); + + pthread_mutex_lock(&zone->events.mx); + zone->events.ufrozen = false; + pthread_mutex_unlock(&zone->events.mx); + + log_zone_info(zone->name, "zone updates unfrozen"); + + return KNOT_EOK; +} diff --git a/src/knot/events/handlers/load.c b/src/knot/events/handlers/load.c new file mode 100644 index 0000000..13e3298 --- /dev/null +++ b/src/knot/events/handlers/load.c @@ -0,0 +1,406 @@ +/* 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 "knot/catalog/generate.h" +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/zone-events.h" +#include "knot/events/handlers.h" +#include "knot/events/replan.h" +#include "knot/zone/digest.h" +#include "knot/zone/serial.h" +#include "knot/zone/zone-diff.h" +#include "knot/zone/zone-load.h" +#include "knot/zone/zone.h" +#include "knot/zone/zonefile.h" +#include "knot/updates/acl.h" + +static bool dontcare_load_error(conf_t *conf, const zone_t *zone) +{ + return (zone->contents == NULL && zone_load_can_bootstrap(conf, zone->name)); +} + +static bool allowed_xfr(conf_t *conf, const zone_t *zone) +{ + conf_val_t acl = conf_zone_get(conf, C_ACL, zone->name); + while (acl.code == KNOT_EOK) { + conf_val_t action = conf_id_get(conf, C_ACL, C_ACTION, &acl); + while (action.code == KNOT_EOK) { + if (conf_opt(&action) == ACL_ACTION_TRANSFER) { + return true; + } + conf_val_next(&action); + } + conf_val_next(&acl); + } + + return false; +} + +int event_load(conf_t *conf, zone_t *zone) +{ + zone_update_t up = { 0 }; + zone_contents_t *journal_conts = NULL, *zf_conts = NULL; + bool old_contents_exist = (zone->contents != NULL), zone_in_journal_exists = false; + + conf_val_t val = conf_zone_get(conf, C_JOURNAL_CONTENT, zone->name); + unsigned load_from = conf_opt(&val); + + val = conf_zone_get(conf, C_ZONEFILE_LOAD, zone->name); + unsigned zf_from = conf_opt(&val); + + int ret = KNOT_EOK; + + // If configured, load journal contents. + if (!old_contents_exist && + ((load_from == JOURNAL_CONTENT_ALL && zf_from != ZONEFILE_LOAD_WHOLE) || + zone->cat_members != NULL)) { + ret = zone_load_from_journal(conf, zone, &journal_conts); + switch (ret) { + case KNOT_EOK: + zone_in_journal_exists = true; + break; + case KNOT_ENOENT: + zone_in_journal_exists = false; + break; + default: + goto cleanup; + } + } else { + zone_in_journal_exists = zone_journal_has_zij(zone); + } + + // If configured, attempt to load zonefile. + if (zf_from != ZONEFILE_LOAD_NONE && zone->cat_members == NULL) { + struct timespec mtime; + char *filename = conf_zonefile(conf, zone->name); + ret = zonefile_exists(filename, &mtime); + if (ret == KNOT_EOK) { + conf_val_t semchecks = conf_zone_get(conf, C_SEM_CHECKS, zone->name); + semcheck_optional_t mode = conf_opt(&semchecks); + if (mode == SEMCHECK_DNSSEC_AUTO) { + conf_val_t validation = conf_zone_get(conf, C_DNSSEC_VALIDATION, zone->name); + if (conf_bool(&validation)) { + /* Disable duplicate DNSSEC checks, which are the + same as DNSSEC validation in zone update commit. */ + mode = SEMCHECK_DNSSEC_OFF; + } + } + + ret = zone_load_contents(conf, zone->name, &zf_conts, mode, false); + } + if (ret != KNOT_EOK) { + assert(!zf_conts); + if (dontcare_load_error(conf, zone)) { + log_zone_info(zone->name, "failed to parse zone file '%s' (%s)", + filename, knot_strerror(ret)); + } else { + log_zone_error(zone->name, "failed to parse zone file '%s' (%s)", + filename, knot_strerror(ret)); + } + free(filename); + goto load_end; + } + free(filename); + + // Save zonefile information. + zone->zonefile.serial = zone_contents_serial(zf_conts); + zone->zonefile.exists = (zf_conts != NULL); + zone->zonefile.mtime = mtime; + + // If configured and possible, fix the SOA serial of zonefile. + zone_contents_t *relevant = (zone->contents != NULL ? zone->contents : journal_conts); + if (zf_conts != NULL && zf_from == ZONEFILE_LOAD_DIFSE && relevant != NULL) { + uint32_t serial = zone_contents_serial(relevant); + conf_val_t policy = conf_zone_get(conf, C_SERIAL_POLICY, zone->name); + uint32_t set = serial_next(serial, conf_opt(&policy), 1); + zone_contents_set_soa_serial(zf_conts, set); + log_zone_info(zone->name, "zone file parsed, serial updated %u -> %u", + zone->zonefile.serial, set); + zone->zonefile.serial = set; + } else { + log_zone_info(zone->name, "zone file parsed, serial %u", + zone->zonefile.serial); + } + + // If configured and appliable to zonefile, load journal changes. + if (load_from != JOURNAL_CONTENT_NONE) { + ret = zone_load_journal(conf, zone, zf_conts); + if (ret != KNOT_EOK) { + zone_contents_deep_free(zf_conts); + zf_conts = NULL; + log_zone_warning(zone->name, "failed to load journal (%s)", + knot_strerror(ret)); + } + } + } + if (zone->cat_members != NULL && !old_contents_exist) { + uint32_t serial = journal_conts == NULL ? 1 : zone_contents_serial(journal_conts); + serial = serial_next(serial, SERIAL_POLICY_UNIXTIME, 1); // unixtime hardcoded + zf_conts = catalog_update_to_zone(zone->cat_members, zone->name, serial); + if (zf_conts == NULL) { + ret = zone->cat_members->error == KNOT_EOK ? KNOT_ENOMEM : zone->cat_members->error; + goto cleanup; + } + } + + // If configured contents=all, but not present, store zonefile. + if ((load_from == JOURNAL_CONTENT_ALL || zone->cat_members != NULL) && + !zone_in_journal_exists && (zf_conts != NULL || old_contents_exist)) { + zone_contents_t *store_c = old_contents_exist ? zone->contents : zf_conts; + ret = zone_in_journal_store(conf, zone, store_c); + if (ret != KNOT_EOK) { + log_zone_warning(zone->name, "failed to write zone-in-journal (%s)", + knot_strerror(ret)); + } else { + zone_in_journal_exists = true; + } + } + + val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name); + bool dnssec_enable = (conf_bool(&val) && zone->cat_members == NULL), zu_from_zf_conts = false; + bool do_diff = (zf_from == ZONEFILE_LOAD_DIFF || zf_from == ZONEFILE_LOAD_DIFSE || zone->cat_members != NULL); + bool ignore_dnssec = (do_diff && dnssec_enable); + + val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name); + unsigned digest_alg = conf_opt(&val); + bool update_zonemd = (digest_alg != ZONE_DIGEST_NONE); + + // Create zone_update structure according to current state. + if (old_contents_exist) { + if (zone->cat_members != NULL) { + ret = zone_update_init(&up, zone, UPDATE_INCREMENTAL); + if (ret == KNOT_EOK) { + ret = catalog_update_to_update(zone->cat_members, &up); + } + if (ret == KNOT_EOK) { + ret = zone_update_increment_soa(&up, conf); + } + } else if (zf_conts == NULL) { + // nothing to be re-loaded + ret = KNOT_EOK; + goto cleanup; + } else if (zf_from == ZONEFILE_LOAD_WHOLE) { + // throw old zone contents and load new from ZF + ret = zone_update_from_contents(&up, zone, zf_conts, + (load_from == JOURNAL_CONTENT_NONE ? + UPDATE_FULL : UPDATE_HYBRID)); + zu_from_zf_conts = true; + } else { + // compute ZF diff and if success, apply it + ret = zone_update_from_differences(&up, zone, NULL, zf_conts, UPDATE_INCREMENTAL, + ignore_dnssec, update_zonemd); + } + } else { + if (journal_conts != NULL && (zf_from != ZONEFILE_LOAD_WHOLE || zone->cat_members != NULL)) { + if (zf_conts == NULL) { + // load zone-in-journal + ret = zone_update_from_contents(&up, zone, journal_conts, UPDATE_HYBRID); + } else { + // load zone-in-journal, compute ZF diff and if success, apply it + ret = zone_update_from_differences(&up, zone, journal_conts, zf_conts, + UPDATE_HYBRID, ignore_dnssec, update_zonemd); + if (ret == KNOT_ESEMCHECK || ret == KNOT_ERANGE) { + log_zone_warning(zone->name, + "zone file changed with SOA serial %s, " + "ignoring zone file and loading from journal", + (ret == KNOT_ESEMCHECK ? "unupdated" : "decreased")); + zone_contents_deep_free(zf_conts); + zf_conts = NULL; + ret = zone_update_from_contents(&up, zone, journal_conts, UPDATE_HYBRID); + } + } + } else { + if (zf_conts == NULL) { + // nothing to be loaded + ret = KNOT_ENOENT; + } else { + // load from ZF + ret = zone_update_from_contents(&up, zone, zf_conts, + (load_from == JOURNAL_CONTENT_NONE ? + UPDATE_FULL : UPDATE_HYBRID)); + if (zf_from == ZONEFILE_LOAD_WHOLE) { + zu_from_zf_conts = true; + } + } + } + } + +load_end: + if (ret != KNOT_EOK) { + switch (ret) { + case KNOT_ENOENT: + if (zone_load_can_bootstrap(conf, zone->name)) { + log_zone_info(zone->name, "zone will be bootstrapped"); + } else { + log_zone_info(zone->name, "zone not found"); + } + break; + case KNOT_ESEMCHECK: + log_zone_warning(zone->name, "zone file changed without SOA serial update"); + break; + case KNOT_ERANGE: + if (serial_compare(zone->zonefile.serial, zone_contents_serial(zone->contents)) == SERIAL_INCOMPARABLE) { + log_zone_warning(zone->name, "zone file changed with incomparable SOA serial"); + } else { + log_zone_warning(zone->name, "zone file changed with decreased SOA serial"); + } + break; + } + goto cleanup; + } + + bool zf_serial_updated = (zf_conts != NULL && zone_contents_serial(zf_conts) != zone_contents_serial(zone->contents)); + + // The contents are already part of zone_update. + zf_conts = NULL; + journal_conts = NULL; + + ret = zone_update_verify_digest(conf, &up); + if (ret != KNOT_EOK) { + goto cleanup; + } + + uint32_t middle_serial = zone_contents_serial(up.new_cont); + + if (do_diff && old_contents_exist && dnssec_enable && zf_serial_updated && + !zone_in_journal_exists) { + ret = zone_update_start_extra(&up, conf); + if (ret != KNOT_EOK) { + goto cleanup; + } + } + + // Sign zone using DNSSEC if configured. + zone_sign_reschedule_t dnssec_refresh = { 0 }; + if (dnssec_enable) { + ret = knot_dnssec_zone_sign(&up, conf, 0, KEY_ROLL_ALLOW_ALL, 0, &dnssec_refresh); + if (ret != KNOT_EOK) { + goto cleanup; + } + if (zu_from_zf_conts && (up.flags & UPDATE_HYBRID) && allowed_xfr(conf, zone)) { + log_zone_warning(zone->name, + "with automatic DNSSEC signing and outgoing transfers enabled, " + "'zonefile-load: difference' should be set to avoid malformed " + "IXFR after manual zone file update"); + } + } else if (update_zonemd) { + /* Don't update ZONEMD if no change and ZONEMD is up-to-date. + * If ZONEFILE_LOAD_DIFSE, the change is non-empty and ZONEMD + * is directly updated without its verification. */ + if (!zone_update_no_change(&up) || !zone_contents_digest_exists(up.new_cont, digest_alg, false)) { + if (zone_update_to(&up) == NULL || middle_serial == zone->zonefile.serial) { + ret = zone_update_increment_soa(&up, conf); + } + if (ret == KNOT_EOK) { + ret = zone_update_add_digest(&up, digest_alg, false); + } + if (ret != KNOT_EOK) { + goto cleanup; + } + } + } + + // If the change is only automatically incremented SOA serial, make it no change. + if ((zf_from == ZONEFILE_LOAD_DIFSE || zone->cat_members != NULL) && + (up.flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && + changeset_differs_just_serial(&up.change, update_zonemd)) { + changeset_t *cpy = changeset_clone(&up.change); + if (cpy == NULL) { + ret = KNOT_ENOMEM; + goto cleanup; + } + ret = zone_update_apply_changeset_reverse(&up, cpy); + if (ret != KNOT_EOK) { + changeset_free(cpy); + goto cleanup; + } + + // If the original ZONEMD is outdated, use the reverted changeset again. + if (update_zonemd && !zone_contents_digest_exists(up.new_cont, digest_alg, false)) { + ret = zone_update_apply_changeset(&up, cpy); + changeset_free(cpy); + if (ret != KNOT_EOK) { + goto cleanup; + } + } else { + changeset_free(cpy); + // Revert automatic zone serial increment. + zone->zonefile.serial = zone_contents_serial(up.new_cont); + /* Reset possibly set the resigned flag. Note that dnssec + * reschedule isn't reverted, but shouldn't be a problem + * for non-empty zones as SOA, ZONEMD, and their RRSIGs + * are always updated with other changes in the zone. */ + zone->zonefile.resigned = false; + } + } + + uint32_t old_serial = 0, new_serial = zone_contents_serial(up.new_cont); + char old_serial_str[11] = "none", new_serial_str[15] = ""; + if (old_contents_exist) { + old_serial = zone_contents_serial(zone->contents); + (void)snprintf(old_serial_str, sizeof(old_serial_str), "%u", old_serial); + } + if (new_serial != middle_serial) { + (void)snprintf(new_serial_str, sizeof(new_serial_str), " -> %u", new_serial); + } + + // Commit zone_update back to zone (including journal update, rcu,...). + ret = zone_update_commit(conf, &up); + if (ret != KNOT_EOK) { + goto cleanup; + } + + char expires_in[32] = ""; + if (zone->timers.next_expire > 0) { + (void)snprintf(expires_in, sizeof(expires_in), + ", expires in %u seconds", + (uint32_t)MAX(zone->timers.next_expire - time(NULL), 0)); + } + + log_zone_info(zone->name, "loaded, serial %s -> %u%s, %zu bytes%s", + old_serial_str, middle_serial, new_serial_str, zone->contents->size, expires_in); + + if (zone->cat_members != NULL) { + catalog_update_clear(zone->cat_members); + } + + // Schedule dependent events. + if (dnssec_enable) { + event_dnssec_reschedule(conf, zone, &dnssec_refresh, false); // false since we handle NOTIFY below + } + + replan_from_timers(conf, zone); + + if (!zone_timers_serial_notified(&zone->timers, new_serial)) { + zone_schedule_notify(zone, 0); + } + + return KNOT_EOK; + +cleanup: + // Try to bootstrap the zone if local error. + replan_from_timers(conf, zone); + + zone_update_clear(&up); + zone_contents_deep_free(zf_conts); + zone_contents_deep_free(journal_conts); + + return (dontcare_load_error(conf, zone) ? KNOT_EOK : ret); +} diff --git a/src/knot/events/handlers/notify.c b/src/knot/events/handlers/notify.c new file mode 100644 index 0000000..dc3965d --- /dev/null +++ b/src/knot/events/handlers/notify.c @@ -0,0 +1,212 @@ +/* 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 "contrib/openbsd/siphash.h" +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/query/query.h" +#include "knot/query/requestor.h" +#include "knot/zone/zone.h" +#include "libknot/errcode.h" + +static notifailed_rmt_hash notifailed_hash(conf_val_t *rmt_id) +{ + SIPHASH_KEY zero_key = { 0, 0 }; + SIPHASH_CTX ctx; + SipHash24_Init(&ctx, &zero_key); + SipHash24_Update(&ctx, rmt_id->data, rmt_id->len); + return SipHash24_End(&ctx); +} + +/*! + * \brief NOTIFY message processing data. + */ +struct notify_data { + const knot_dname_t *zone; + const knot_rrset_t *soa; + const struct sockaddr *remote; + query_edns_data_t edns; +}; + +static int notify_begin(knot_layer_t *layer, void *params) +{ + layer->data = params; + + return KNOT_STATE_PRODUCE; +} + +static int notify_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct notify_data *data = layer->data; + + // mandatory: NOTIFY opcode, AA flag, SOA qtype + query_init_pkt(pkt); + knot_wire_set_opcode(pkt->wire, KNOT_OPCODE_NOTIFY); + knot_wire_set_aa(pkt->wire); + knot_pkt_put_question(pkt, data->zone, KNOT_CLASS_IN, KNOT_RRTYPE_SOA); + + // unsecure hint: new SOA + if (data->soa) { + knot_pkt_begin(pkt, KNOT_ANSWER); + knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, data->soa, 0); + } + + query_put_edns(pkt, &data->edns); + + return KNOT_STATE_CONSUME; +} + +static int notify_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + return KNOT_STATE_DONE; +} + +static const knot_layer_api_t NOTIFY_API = { + .begin = notify_begin, + .produce = notify_produce, + .consume = notify_consume, +}; + +#define NOTIFY_OUT_LOG(priority, zone, remote, reused, fmt, ...) \ + ns_log(priority, zone, LOG_OPERATION_NOTIFY, LOG_DIRECTION_OUT, remote, \ + (reused), fmt, ## __VA_ARGS__) + +static int send_notify(conf_t *conf, zone_t *zone, const knot_rrset_t *soa, + const conf_remote_t *slave, int timeout, bool retry) +{ + struct notify_data data = { + .zone = zone->name, + .soa = soa, + .remote = (struct sockaddr *)&slave->addr, + .edns = query_edns_data_init(conf, slave->addr.ss_family, 0) + }; + + knot_requestor_t requestor; + knot_requestor_init(&requestor, &NOTIFY_API, &data, NULL); + + knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL); + if (!pkt) { + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + const struct sockaddr_storage *dst = &slave->addr; + const struct sockaddr_storage *src = &slave->via; + knot_request_flag_t flags = conf->cache.srv_tcp_fastopen ? KNOT_REQUEST_TFO : 0; + knot_request_t *req = knot_request_make(NULL, dst, src, pkt, &slave->key, flags); + if (!req) { + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + int ret = knot_requestor_exec(&requestor, req, timeout); + + const char *log_retry = retry ? "retry, " : ""; + + if (ret == KNOT_EOK && knot_pkt_ext_rcode(req->resp) == 0) { + NOTIFY_OUT_LOG(LOG_INFO, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "%sserial %u", log_retry, knot_soa_serial(soa->rrs.rdata)); + zone->timers.last_notified_serial = (knot_soa_serial(soa->rrs.rdata) | LAST_NOTIFIED_SERIAL_VALID); + } else if (knot_pkt_ext_rcode(req->resp) == 0) { + NOTIFY_OUT_LOG(LOG_WARNING, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "%sfailed (%s)", log_retry, knot_strerror(ret)); + } else { + NOTIFY_OUT_LOG(LOG_WARNING, zone->name, dst, + requestor.layer.flags & KNOT_REQUESTOR_REUSED, + "%sserver responded with error '%s'", + log_retry, knot_pkt_ext_rcode_name(req->resp)); + } + + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + + return ret; +} + +int event_notify(conf_t *conf, zone_t *zone) +{ + assert(zone); + + bool failed = false; + + if (zone_contents_is_empty(zone->contents)) { + return KNOT_EOK; + } + + // NOTIFY content + int timeout = conf->cache.srv_tcp_remote_io_timeout; + knot_rrset_t soa = node_rrset(zone->contents->apex, KNOT_RRTYPE_SOA); + + // in case of re-try, NOTIFY only failed remotes + pthread_mutex_lock(&zone->preferred_lock); + bool retry = (zone->notifailed.size > 0); + + // send NOTIFY to each remote, use working address + conf_val_t notify = conf_zone_get(conf, C_NOTIFY, zone->name); + conf_mix_iter_t iter; + conf_mix_iter_init(conf, ¬ify, &iter); + while (iter.id->code == KNOT_EOK) { + notifailed_rmt_hash rmt_hash = notifailed_hash(iter.id); + if (retry && notifailed_rmt_dynarray_bsearch(&zone->notifailed, &rmt_hash) == NULL) { + conf_mix_iter_next(&iter); + continue; + } + pthread_mutex_unlock(&zone->preferred_lock); + + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, iter.id); + size_t addr_count = conf_val_count(&addr); + + int ret = KNOT_EOK; + + for (int i = 0; i < addr_count; i++) { + conf_remote_t slave = conf_remote(conf, iter.id, i); + ret = send_notify(conf, zone, &soa, &slave, timeout, retry); + if (ret == KNOT_EOK) { + break; + } + } + + pthread_mutex_lock(&zone->preferred_lock); + if (ret != KNOT_EOK) { + failed = true; + notifailed_rmt_dynarray_add(&zone->notifailed, &rmt_hash); + } else { + notifailed_rmt_dynarray_remove(&zone->notifailed, &rmt_hash); + } + + conf_mix_iter_next(&iter); + } + + if (failed) { + notifailed_rmt_dynarray_sort_dedup(&zone->notifailed); + + uint32_t retry_in = knot_soa_retry(soa.rrs.rdata); + conf_val_t val = conf_zone_get(conf, C_RETRY_MIN_INTERVAL, zone->name); + retry_in = MAX(retry_in, conf_int(&val)); + val = conf_zone_get(conf, C_RETRY_MAX_INTERVAL, zone->name); + retry_in = MIN(retry_in, conf_int(&val)); + + zone_events_schedule_at(zone, ZONE_EVENT_NOTIFY, time(NULL) + retry_in); + } + pthread_mutex_unlock(&zone->preferred_lock); + + return failed ? KNOT_ERROR : KNOT_EOK; +} diff --git a/src/knot/events/handlers/refresh.c b/src/knot/events/handlers/refresh.c new file mode 100644 index 0000000..9125aac --- /dev/null +++ b/src/knot/events/handlers/refresh.c @@ -0,0 +1,1391 @@ +/* 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 <stdint.h> + +#include "contrib/mempattern.h" +#include "libdnssec/random.h" +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/dnssec/zone-events.h" +#include "knot/events/handlers.h" +#include "knot/events/replan.h" +#include "knot/nameserver/ixfr.h" +#include "knot/query/layer.h" +#include "knot/query/query.h" +#include "knot/query/requestor.h" +#include "knot/updates/changesets.h" +#include "knot/zone/adjust.h" +#include "knot/zone/digest.h" +#include "knot/zone/serial.h" +#include "knot/zone/zone.h" +#include "knot/zone/zonefile.h" +#include "libknot/errcode.h" + +/*! + * \brief Refresh event processing. + * + * The following diagram represents refresh event processing. + * + * \verbatim + * O + * | + * +-----v-----+ + * | BEGIN | + * +---+---+---+ + * has SOA | | no SOA + * +-------------------+ +------------------------------+ + * | | + * +------v------+ outdated +--------------+ error +-------v------+ + * | SOA query +------------> IXFR query +-----------> AXFR query | + * +-----+---+---+ +------+-------+ +----+----+----+ + * error | | current | success success | | error + * | +-----+ +---------------+ | | + * | | | +--------------------------------------+ | + * | | | | +----------+ +--------------+ + * | | | | | | | + * | +--v-v-v--+ | +--v--v--+ + * | | DONE | | | FAIL | + * | +---------+ | +--------+ + * +----------------------------+ + * + * \endverbatim + */ + +#define REFRESH_LOG(priority, data, direction, msg...) \ + ns_log(priority, (data)->zone->name, LOG_OPERATION_REFRESH, direction, \ + (data)->remote, (data)->layer->flags & KNOT_REQUESTOR_REUSED, msg) + +#define AXFRIN_LOG(priority, data, msg...) \ + ns_log(priority, (data)->zone->name, LOG_OPERATION_AXFR, LOG_DIRECTION_IN, \ + (data)->remote, (data)->layer->flags & KNOT_REQUESTOR_REUSED, msg) + +#define IXFRIN_LOG(priority, data, msg...) \ + ns_log(priority, (data)->zone->name, LOG_OPERATION_IXFR, LOG_DIRECTION_IN, \ + (data)->remote, (data)->layer->flags & KNOT_REQUESTOR_REUSED, msg) + +enum state { + REFRESH_STATE_INVALID = 0, + STATE_SOA_QUERY, + STATE_TRANSFER, +}; + +enum xfr_type { + XFR_TYPE_NOTIMP = -2, + XFR_TYPE_ERROR = -1, + XFR_TYPE_UNDETERMINED = 0, + XFR_TYPE_UPTODATE, + XFR_TYPE_AXFR, + XFR_TYPE_IXFR, +}; + +struct refresh_data { + knot_layer_t *layer; //!< Used for reading requestor flags. + + // transfer configuration, initialize appropriately: + + zone_t *zone; //!< Zone to eventually updated. + conf_t *conf; //!< Server configuration. + const struct sockaddr *remote; //!< Remote endpoint. + const knot_rrset_t *soa; //!< Local SOA (NULL for AXFR). + const size_t max_zone_size; //!< Maximal zone size. + bool use_edns; //!< Allow EDNS in SOA/AXFR/IXFR queries. + query_edns_data_t edns; //!< EDNS data to be used in queries. + zone_master_fallback_t *fallback; //!< Flags allowing zone_master_try() fallbacks. + bool fallback_axfr; //!< Flag allowing fallback to AXFR, + uint32_t expire_timer; //!< Result: expire timer from answer EDNS. + + // internal state, initialize with zeroes: + + int ret; //!< Error code. + enum state state; //!< Event processing state. + enum xfr_type xfr_type; //!< Transer type (mostly IXFR versus AXFR). + knot_rrset_t *initial_soa_copy; //!< Copy of the received initial SOA. + struct xfr_stats stats; //!< Transfer statistics. + struct timespec started; //!< When refresh started. + size_t change_size; //!< Size of added and removed RRs. + + struct { + zone_contents_t *zone; //!< AXFR result, new zone. + } axfr; + + struct { + struct ixfr_proc *proc; //!< IXFR processing context. + knot_rrset_t *final_soa; //!< SOA denoting end of transfer. + list_t changesets; //!< IXFR result, zone updates. + } ixfr; + + bool updated; // TODO: Can we fid a better way to check if zone was updated? + knot_mm_t *mm; // TODO: This used to be used in IXFR. Remove or reuse. +}; + +static const uint32_t EXPIRE_TIMER_INVALID = ~0U; + +static bool serial_is_current(uint32_t local_serial, uint32_t remote_serial) +{ + return (serial_compare(local_serial, remote_serial) & SERIAL_MASK_GEQ); +} + +static time_t bootstrap_next(uint8_t *count) +{ + // Let the increment gradually grow in a sensible way. + time_t increment = 5 * (*count) * (*count); + + if (increment < 7200) { // two hours + (*count)++; + } else { + increment = 7200; + } + + // Add a random delay to prevent burst refresh. + return increment + dnssec_random_uint16_t() % 30; +} + +static void limit_timer(conf_t *conf, const knot_dname_t *zone, uint32_t *timer, + const char *tm_name, const yp_name_t *low, const yp_name_t *upp) +{ + uint32_t tlow = 0; + if (low > 0) { + conf_val_t val1 = conf_zone_get(conf, low, zone); + tlow = conf_int(&val1); + } + conf_val_t val2 = conf_zone_get(conf, upp, zone); + uint32_t tupp = conf_int(&val2); + + const char *msg = "%s timer trimmed to '%s-%s-interval'"; + if (*timer < tlow) { + *timer = tlow; + log_zone_debug(zone, msg, tm_name, tm_name, "min"); + } else if (*timer > tupp) { + *timer = tupp; + log_zone_debug(zone, msg, tm_name, tm_name, "max"); + } +} + +/*! + * \brief Modify the expire timer wrt the received EDNS EXPIRE (RFC 7314, section 4) + * + * \param data The refresh data. + * \param pkt A received packet to parse. + * \param strictly_follow Strictly use EDNS EXPIRE as the expire timer value. + * (false == RFC 7314, section 4, second paragraph, + * true == third paragraph) + */ +static void consume_edns_expire(struct refresh_data *data, knot_pkt_t *pkt, bool strictly_follow) +{ + if (data->zone->is_catalog_flag) { + data->expire_timer = EXPIRE_TIMER_INVALID; + return; + } + + uint8_t *expire_opt = knot_pkt_edns_option(pkt, KNOT_EDNS_OPTION_EXPIRE); + if (expire_opt != NULL && knot_edns_opt_get_length(expire_opt) == sizeof(uint32_t)) { + uint32_t edns_expire = knot_wire_read_u32(knot_edns_opt_get_data(expire_opt)); + data->expire_timer = strictly_follow ? edns_expire : + MAX(edns_expire, data->zone->timers.next_expire - time(NULL)); + } +} + +static void finalize_timers(struct refresh_data *data) +{ + conf_t *conf = data->conf; + zone_t *zone = data->zone; + + // EDNS EXPIRE -- RFC 7314, section 4, fourth paragraph. + data->expire_timer = MIN(data->expire_timer, zone_soa_expire(data->zone)); + assert(data->expire_timer != EXPIRE_TIMER_INVALID); + + time_t now = time(NULL); + const knot_rdataset_t *soa = zone_soa(zone); + + uint32_t soa_refresh = knot_soa_refresh(soa->rdata); + limit_timer(conf, zone->name, &soa_refresh, "refresh", + C_REFRESH_MIN_INTERVAL, C_REFRESH_MAX_INTERVAL); + zone->timers.next_refresh = now + soa_refresh; + zone->timers.last_refresh_ok = true; + + if (zone->is_catalog_flag) { + // It's already zero in most cases. + zone->timers.next_expire = 0; + } else { + limit_timer(conf, zone->name, &data->expire_timer, "expire", + // Limit min if not received as EDNS Expire. + data->expire_timer == knot_soa_expire(soa->rdata) ? + C_EXPIRE_MIN_INTERVAL : 0, + C_EXPIRE_MAX_INTERVAL); + zone->timers.next_expire = now + data->expire_timer; + } +} + +static void fill_expires_in(char *expires_in, size_t size, const struct refresh_data *data) +{ + assert(!data->zone->is_catalog_flag || data->zone->timers.next_expire == 0); + if (data->zone->timers.next_expire > 0) { + (void)snprintf(expires_in, size, + ", expires in %u seconds", data->expire_timer); + } +} + +static void xfr_log_publish(const struct refresh_data *data, + const uint32_t old_serial, + const uint32_t new_serial, + const uint32_t master_serial, + bool has_master_serial, + bool axfr_bootstrap) +{ + struct timespec finished = time_now(); + double duration = time_diff_ms(&data->started, &finished) / 1000.0; + + char old_info[32] = "none"; + if (!axfr_bootstrap) { + (void)snprintf(old_info, sizeof(old_info), "%u", old_serial); + } + + char master_info[32] = ""; + if (has_master_serial) { + (void)snprintf(master_info, sizeof(master_info), + ", remote serial %u", master_serial); + } + + char expires_in[32] = ""; + fill_expires_in(expires_in, sizeof(expires_in), data); + + REFRESH_LOG(LOG_INFO, data, LOG_DIRECTION_NONE, + "zone updated, %0.2f seconds, serial %s -> %u%s%s", + duration, old_info, new_serial, master_info, expires_in); +} + +static void xfr_log_read_ms(const knot_dname_t *zone, int ret) +{ + log_zone_error(zone, "failed reading master serial from KASP DB (%s)", knot_strerror(ret)); +} + +static int axfr_init(struct refresh_data *data) +{ + zone_contents_t *new_zone = zone_contents_new(data->zone->name, true); + if (new_zone == NULL) { + return KNOT_ENOMEM; + } + + data->axfr.zone = new_zone; + return KNOT_EOK; +} + +static void axfr_cleanup(struct refresh_data *data) +{ + zone_contents_deep_free(data->axfr.zone); + data->axfr.zone = NULL; +} + +static void axfr_slave_sign_serial(zone_contents_t *new_contents, zone_t *zone, + conf_t *conf, uint32_t *master_serial) +{ + // Update slave's serial to ensure it's growing and consistent with + // its serial policy. + conf_val_t val = conf_zone_get(conf, C_SERIAL_POLICY, zone->name); + unsigned serial_policy = conf_opt(&val); + + *master_serial = zone_contents_serial(new_contents); + + uint32_t new_serial, lastsigned_serial; + if (zone->contents != NULL) { + // Retransfer or AXFR-fallback - increment current serial. + new_serial = serial_next(zone_contents_serial(zone->contents), serial_policy, 1); + } else if (zone_get_lastsigned_serial(zone, &lastsigned_serial) == KNOT_EOK) { + // Bootstrap - increment stored serial. + new_serial = serial_next(lastsigned_serial, serial_policy, 1); + } else { + // Bootstrap - try to reuse master serial, considering policy. + new_serial = serial_next(*master_serial, serial_policy, 0); + } + zone_contents_set_soa_serial(new_contents, new_serial); +} + +static int axfr_finalize(struct refresh_data *data) +{ + zone_contents_t *new_zone = data->axfr.zone; + + conf_val_t val = conf_zone_get(data->conf, C_DNSSEC_SIGNING, data->zone->name); + bool dnssec_enable = conf_bool(&val); + uint32_t old_serial = zone_contents_serial(data->zone->contents), master_serial = 0; + bool bootstrap = (data->zone->contents == NULL); + + if (dnssec_enable) { + axfr_slave_sign_serial(new_zone, data->zone, data->conf, &master_serial); + } + + zone_update_t up = { 0 }; + int ret = zone_update_from_contents(&up, data->zone, new_zone, UPDATE_FULL); + if (ret != KNOT_EOK) { + data->fallback->remote = false; + return ret; + } + // Seized by zone_update. Don't free the contents again in axfr_cleanup. + data->axfr.zone = NULL; + + ret = zone_update_semcheck(data->conf, &up); + if (ret == KNOT_EOK) { + ret = zone_update_verify_digest(data->conf, &up); + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + return ret; + } + + val = conf_zone_get(data->conf, C_ZONEMD_GENERATE, data->zone->name); + unsigned digest_alg = conf_opt(&val); + + if (dnssec_enable) { + zone_sign_reschedule_t resch = { 0 }; + ret = knot_dnssec_zone_sign(&up, data->conf, ZONE_SIGN_KEEP_SERIAL, KEY_ROLL_ALLOW_ALL, 0, &resch); + event_dnssec_reschedule(data->conf, data->zone, &resch, true); + } else if (digest_alg != ZONE_DIGEST_NONE) { + assert(zone_update_to(&up) != NULL); + ret = zone_update_add_digest(&up, digest_alg, false); + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + data->fallback->remote = false; + return ret; + } + + ret = zone_update_commit(data->conf, &up); + if (ret != KNOT_EOK) { + zone_update_clear(&up); + AXFRIN_LOG(LOG_WARNING, data, + "failed to store changes (%s)", knot_strerror(ret)); + data->fallback->remote = false; + return ret; + } + + if (dnssec_enable) { + ret = zone_set_master_serial(data->zone, master_serial); + if (ret != KNOT_EOK) { + log_zone_warning(data->zone->name, + "unable to save master serial, future transfers might be broken"); + } + } + + finalize_timers(data); + xfr_log_publish(data, old_serial, zone_contents_serial(new_zone), + master_serial, dnssec_enable, bootstrap); + + return KNOT_EOK; +} + +static int axfr_consume_rr(const knot_rrset_t *rr, struct refresh_data *data) +{ + assert(rr); + assert(data); + assert(data->axfr.zone); + + // zc is stateless structure which can be initialized for each rr + // the changes are stored only in data->axfr.zone (aka zc.z) + zcreator_t zc = { + .z = data->axfr.zone, + .master = false, + .ret = KNOT_EOK + }; + + if (rr->type == KNOT_RRTYPE_SOA && + node_rrtype_exists(zc.z->apex, KNOT_RRTYPE_SOA)) { + return KNOT_STATE_DONE; + } + + data->ret = zcreator_step(&zc, rr); + if (data->ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + data->change_size += knot_rrset_size(rr); + if (data->change_size > data->max_zone_size) { + AXFRIN_LOG(LOG_WARNING, data, + "zone size exceeded"); + data->ret = KNOT_EZONESIZE; + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_CONSUME; +} + +static int axfr_consume_packet(knot_pkt_t *pkt, struct refresh_data *data) +{ + assert(pkt); + assert(data); + + const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER); + int ret = KNOT_STATE_CONSUME; + for (uint16_t i = 0; i < answer->count && ret == KNOT_STATE_CONSUME; ++i) { + ret = axfr_consume_rr(knot_pkt_rr(answer, i), data); + } + return ret; +} + +static int axfr_consume(knot_pkt_t *pkt, struct refresh_data *data, bool reuse_soa) +{ + assert(pkt); + assert(data); + + // Check RCODE + if (knot_pkt_ext_rcode(pkt) != KNOT_RCODE_NOERROR) { + AXFRIN_LOG(LOG_WARNING, data, + "server responded with error '%s'", + knot_pkt_ext_rcode_name(pkt)); + data->ret = KNOT_EDENIED; + return KNOT_STATE_FAIL; + } + + // Initialize with first packet + if (data->axfr.zone == NULL) { + data->ret = axfr_init(data); + if (data->ret != KNOT_EOK) { + AXFRIN_LOG(LOG_WARNING, data, + "failed to initialize (%s)", + knot_strerror(data->ret)); + data->fallback->remote = false; + return KNOT_STATE_FAIL; + } + + AXFRIN_LOG(LOG_INFO, data, "started"); + xfr_stats_begin(&data->stats); + data->change_size = 0; + } + + int next; + // Process saved SOA if fallback from IXFR + if (data->initial_soa_copy != NULL) { + next = reuse_soa ? axfr_consume_rr(data->initial_soa_copy, data) : + KNOT_STATE_CONSUME; + knot_rrset_free(data->initial_soa_copy, data->mm); + data->initial_soa_copy = NULL; + if (next != KNOT_STATE_CONSUME) { + return next; + } + } + + // Process answer packet + xfr_stats_add(&data->stats, pkt->size); + next = axfr_consume_packet(pkt, data); + + // Finalize + if (next == KNOT_STATE_DONE) { + xfr_stats_end(&data->stats); + } + + return next; +} + +/*! \brief Initialize IXFR-in processing context. */ +static int ixfr_init(struct refresh_data *data) +{ + struct ixfr_proc *proc = mm_alloc(data->mm, sizeof(*proc)); + if (proc == NULL) { + return KNOT_ENOMEM; + } + + memset(proc, 0, sizeof(struct ixfr_proc)); + proc->state = IXFR_START; + proc->mm = data->mm; + + data->ixfr.proc = proc; + data->ixfr.final_soa = NULL; + + init_list(&data->ixfr.changesets); + + return KNOT_EOK; +} + +/*! \brief Clean up data allocated by IXFR-in processing. */ +static void ixfr_cleanup(struct refresh_data *data) +{ + if (data->ixfr.proc == NULL) { + return; + } + + knot_rrset_free(data->ixfr.final_soa, data->mm); + data->ixfr.final_soa = NULL; + mm_free(data->mm, data->ixfr.proc); + data->ixfr.proc = NULL; + + changesets_free(&data->ixfr.changesets); +} + +static bool ixfr_serial_once(changeset_t *ch, int policy, uint32_t *master_serial, uint32_t *local_serial) +{ + uint32_t ch_from = changeset_from(ch), ch_to = changeset_to(ch); + + if (ch_from != *master_serial || (serial_compare(ch_from, ch_to) & SERIAL_MASK_GEQ)) { + return false; + } + + uint32_t new_from = *local_serial; + uint32_t new_to = serial_next(new_from, policy, 1); + knot_soa_serial_set(ch->soa_from->rrs.rdata, new_from); + knot_soa_serial_set(ch->soa_to->rrs.rdata, new_to); + + *master_serial = ch_to; + *local_serial = new_to; + + return true; +} + +static int ixfr_slave_sign_serial(list_t *changesets, zone_t *zone, + conf_t *conf, uint32_t *master_serial) +{ + uint32_t local_serial = zone_contents_serial(zone->contents), lastsigned; + + if (zone_get_lastsigned_serial(zone, &lastsigned) != KNOT_EOK || lastsigned != local_serial) { + // this is kind of assert + return KNOT_ERROR; + } + + conf_val_t val = conf_zone_get(conf, C_SERIAL_POLICY, zone->name); + unsigned serial_policy = conf_opt(&val); + + int ret = zone_get_master_serial(zone, master_serial); + if (ret != KNOT_EOK) { + log_zone_error(zone->name, "failed to read master serial" + "from KASP DB (%s)", knot_strerror(ret)); + return ret; + } + changeset_t *chs; + WALK_LIST(chs, *changesets) { + if (!ixfr_serial_once(chs, serial_policy, master_serial, &local_serial)) { + return KNOT_EINVAL; + } + } + + return KNOT_EOK; +} + +static int ixfr_finalize(struct refresh_data *data) +{ + conf_val_t val = conf_zone_get(data->conf, C_DNSSEC_SIGNING, data->zone->name); + bool dnssec_enable = conf_bool(&val); + uint32_t master_serial = 0, old_serial = zone_contents_serial(data->zone->contents); + + if (dnssec_enable) { + int ret = ixfr_slave_sign_serial(&data->ixfr.changesets, data->zone, data->conf, &master_serial); + if (ret != KNOT_EOK) { + IXFRIN_LOG(LOG_WARNING, data, + "failed to adjust SOA serials from unsigned remote (%s)", + knot_strerror(ret)); + data->fallback_axfr = false; + data->fallback->remote = false; + return ret; + } + } + + zone_update_t up = { 0 }; + int ret = zone_update_init(&up, data->zone, UPDATE_INCREMENTAL | UPDATE_STRICT | UPDATE_NO_CHSET); + if (ret != KNOT_EOK) { + data->fallback_axfr = false; + data->fallback->remote = false; + return ret; + } + + changeset_t *set; + WALK_LIST(set, data->ixfr.changesets) { + ret = zone_update_apply_changeset(&up, set); + if (ret != KNOT_EOK) { + uint32_t serial_from = knot_soa_serial(set->soa_from->rrs.rdata); + uint32_t serial_to = knot_soa_serial(set->soa_to->rrs.rdata); + zone_update_clear(&up); + IXFRIN_LOG(LOG_WARNING, data, + "serial %u -> %u, failed to apply changes to zone (%s)", + serial_from, serial_to, knot_strerror(ret)); + return ret; + } + } + + ret = zone_update_semcheck(data->conf, &up); + if (ret == KNOT_EOK) { + ret = zone_update_verify_digest(data->conf, &up); + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + data->fallback_axfr = false; + return ret; + } + + val = conf_zone_get(data->conf, C_ZONEMD_GENERATE, data->zone->name); + unsigned digest_alg = conf_opt(&val); + + if (dnssec_enable) { + ret = knot_dnssec_sign_update(&up, data->conf); + } else if (digest_alg != ZONE_DIGEST_NONE) { + assert(zone_update_to(&up) != NULL); + ret = zone_update_add_digest(&up, digest_alg, false); + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + data->fallback_axfr = false; + data->fallback->remote = false; + return ret; + } + + ret = zone_update_commit(data->conf, &up); + if (ret != KNOT_EOK) { + zone_update_clear(&up); + IXFRIN_LOG(LOG_WARNING, data, + "failed to store changes (%s)", knot_strerror(ret)); + return ret; + } + + if (dnssec_enable && !EMPTY_LIST(data->ixfr.changesets)) { + ret = zone_set_master_serial(data->zone, master_serial); + if (ret != KNOT_EOK) { + log_zone_warning(data->zone->name, + "unable to save master serial, future transfers might be broken"); + } + } + + finalize_timers(data); + xfr_log_publish(data, old_serial, zone_contents_serial(data->zone->contents), + master_serial, dnssec_enable, false); + + return KNOT_EOK; +} + +/*! \brief Stores starting SOA into changesets structure. */ +static int ixfr_solve_start(const knot_rrset_t *rr, struct refresh_data *data) +{ + assert(data->ixfr.final_soa == NULL); + if (rr->type != KNOT_RRTYPE_SOA) { + return KNOT_EMALF; + } + + // Store terminal SOA + data->ixfr.final_soa = knot_rrset_copy(rr, data->mm); + if (data->ixfr.final_soa == NULL) { + return KNOT_ENOMEM; + } + + // Initialize list for changes + init_list(&data->ixfr.changesets); + + return KNOT_EOK; +} + +/*! \brief Decides what to do with a starting SOA (deletions). */ +static int ixfr_solve_soa_del(const knot_rrset_t *rr, struct refresh_data *data) +{ + if (rr->type != KNOT_RRTYPE_SOA) { + return KNOT_EMALF; + } + + // Create new changeset. + changeset_t *change = changeset_new(data->zone->name); + if (change == NULL) { + return KNOT_ENOMEM; + } + + // Store SOA into changeset. + change->soa_from = knot_rrset_copy(rr, NULL); + if (change->soa_from == NULL) { + changeset_free(change); + return KNOT_ENOMEM; + } + + // Add changeset. + add_tail(&data->ixfr.changesets, &change->n); + + return KNOT_EOK; +} + +/*! \brief Stores ending SOA into changeset. */ +static int ixfr_solve_soa_add(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm) +{ + if (rr->type != KNOT_RRTYPE_SOA) { + return KNOT_EMALF; + } + + change->soa_to = knot_rrset_copy(rr, NULL); + if (change->soa_to == NULL) { + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +/*! \brief Adds single RR into remove section of changeset. */ +static int ixfr_solve_del(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm) +{ + return changeset_add_removal(change, rr, 0); +} + +/*! \brief Adds single RR into add section of changeset. */ +static int ixfr_solve_add(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm) +{ + return changeset_add_addition(change, rr, 0); +} + +/*! \brief Decides what the next IXFR-in state should be. */ +static int ixfr_next_state(struct refresh_data *data, const knot_rrset_t *rr) +{ + const bool soa = (rr->type == KNOT_RRTYPE_SOA); + enum ixfr_state state = data->ixfr.proc->state; + + if ((state == IXFR_SOA_ADD || state == IXFR_ADD) && + knot_rrset_equal(rr, data->ixfr.final_soa, true)) { + return IXFR_DONE; + } + + switch (state) { + case IXFR_START: + // Final SOA already stored or transfer start. + return data->ixfr.final_soa ? IXFR_SOA_DEL : IXFR_START; + case IXFR_SOA_DEL: + // Empty delete section or start of delete section. + return soa ? IXFR_SOA_ADD : IXFR_DEL; + case IXFR_SOA_ADD: + // Empty add section or start of add section. + return soa ? IXFR_SOA_DEL : IXFR_ADD; + case IXFR_DEL: + // End of delete section or continue. + return soa ? IXFR_SOA_ADD : IXFR_DEL; + case IXFR_ADD: + // End of add section or continue. + return soa ? IXFR_SOA_DEL : IXFR_ADD; + default: + assert(0); + return IXFR_INVALID; + } +} + +/*! + * \brief Processes single RR according to current IXFR-in state. The states + * correspond with IXFR-in message structure, in the order they are + * mentioned in the code. + * + * \param rr RR to process. + * \param proc Processing context. + * + * \return KNOT_E* + */ +static int ixfr_step(const knot_rrset_t *rr, struct refresh_data *data) +{ + data->ixfr.proc->state = ixfr_next_state(data, rr); + changeset_t *change = TAIL(data->ixfr.changesets); + + switch (data->ixfr.proc->state) { + case IXFR_START: + return ixfr_solve_start(rr, data); + case IXFR_SOA_DEL: + return ixfr_solve_soa_del(rr, data); + case IXFR_DEL: + return ixfr_solve_del(rr, change, data->mm); + case IXFR_SOA_ADD: + return ixfr_solve_soa_add(rr, change, data->mm); + case IXFR_ADD: + return ixfr_solve_add(rr, change, data->mm); + case IXFR_DONE: + return KNOT_EOK; + default: + return KNOT_ERROR; + } +} + +static int ixfr_consume_rr(const knot_rrset_t *rr, struct refresh_data *data) +{ + if (knot_dname_in_bailiwick(rr->owner, data->zone->name) < 0) { + return KNOT_STATE_CONSUME; + } + + data->ret = ixfr_step(rr, data); + if (data->ret != KNOT_EOK) { + IXFRIN_LOG(LOG_WARNING, data, + "failed (%s)", knot_strerror(data->ret)); + return KNOT_STATE_FAIL; + } + + data->change_size += knot_rrset_size(rr); + if (data->change_size / 2 > data->max_zone_size) { + IXFRIN_LOG(LOG_WARNING, data, + "transfer size exceeded"); + data->ret = KNOT_EZONESIZE; + return KNOT_STATE_FAIL; + } + + if (data->ixfr.proc->state == IXFR_DONE) { + return KNOT_STATE_DONE; + } + + return KNOT_STATE_CONSUME; +} + +/*! + * \brief Processes IXFR reply packet and fills in the changesets structure. + * + * \param pkt Packet containing the IXFR reply in wire format. + * \param adata Answer data, including processing context. + * + * \return KNOT_STATE_CONSUME, KNOT_STATE_DONE, KNOT_STATE_FAIL + */ +static int ixfr_consume_packet(knot_pkt_t *pkt, struct refresh_data *data) +{ + // Process RRs in the message. + const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER); + int ret = KNOT_STATE_CONSUME; + for (uint16_t i = 0; i < answer->count && ret == KNOT_STATE_CONSUME; ++i) { + ret = ixfr_consume_rr(knot_pkt_rr(answer, i), data); + } + return ret; +} + +static enum xfr_type determine_xfr_type(const knot_pktsection_t *answer, + uint32_t zone_serial, const knot_rrset_t *initial_soa) +{ + if (answer->count < 1) { + return XFR_TYPE_NOTIMP; + } + + const knot_rrset_t *rr_one = knot_pkt_rr(answer, 0); + if (initial_soa != NULL) { + if (rr_one->type == KNOT_RRTYPE_SOA) { + return knot_rrset_equal(initial_soa, rr_one, true) ? + XFR_TYPE_AXFR : XFR_TYPE_IXFR; + } + return XFR_TYPE_AXFR; + } + + if (answer->count == 1) { + if (rr_one->type == KNOT_RRTYPE_SOA) { + return serial_is_current(zone_serial, knot_soa_serial(rr_one->rrs.rdata)) ? + XFR_TYPE_UPTODATE : XFR_TYPE_UNDETERMINED; + } + return XFR_TYPE_ERROR; + } + + const knot_rrset_t *rr_two = knot_pkt_rr(answer, 1); + if (answer->count == 2 && rr_one->type == KNOT_RRTYPE_SOA && + knot_rrset_equal(rr_one, rr_two, true)) { + return XFR_TYPE_AXFR; + } + + return (rr_one->type == KNOT_RRTYPE_SOA && rr_two->type != KNOT_RRTYPE_SOA) ? + XFR_TYPE_AXFR : XFR_TYPE_IXFR; +} + +static int ixfr_consume(knot_pkt_t *pkt, struct refresh_data *data) +{ + assert(pkt); + assert(data); + + // Check RCODE + if (knot_pkt_ext_rcode(pkt) != KNOT_RCODE_NOERROR) { + IXFRIN_LOG(LOG_WARNING, data, + "server responded with error '%s'", + knot_pkt_ext_rcode_name(pkt)); + data->ret = KNOT_EDENIED; + return KNOT_STATE_FAIL; + } + + // Initialize with first packet + if (data->ixfr.proc == NULL) { + const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER); + + uint32_t master_serial; + data->ret = slave_zone_serial(data->zone, data->conf, &master_serial); + if (data->ret != KNOT_EOK) { + xfr_log_read_ms(data->zone->name, data->ret); + data->fallback_axfr = false; + data->fallback->remote = false; + return KNOT_STATE_FAIL; + } + data->xfr_type = determine_xfr_type(answer, master_serial, + data->initial_soa_copy); + switch (data->xfr_type) { + case XFR_TYPE_ERROR: + IXFRIN_LOG(LOG_WARNING, data, + "malformed response SOA"); + data->ret = KNOT_EMALF; + data->xfr_type = XFR_TYPE_IXFR; // unrecognisable IXFR type is the same as failed IXFR + return KNOT_STATE_FAIL; + case XFR_TYPE_NOTIMP: + IXFRIN_LOG(LOG_WARNING, data, + "not supported by remote"); + data->ret = KNOT_ENOTSUP; + data->xfr_type = XFR_TYPE_IXFR; + return KNOT_STATE_FAIL; + case XFR_TYPE_UNDETERMINED: + // Store the SOA and check with next packet + data->initial_soa_copy = knot_rrset_copy(knot_pkt_rr(answer, 0), data->mm); + if (data->initial_soa_copy == NULL) { + data->ret = KNOT_ENOMEM; + return KNOT_STATE_FAIL; + } + xfr_stats_add(&data->stats, pkt->size); + return KNOT_STATE_CONSUME; + case XFR_TYPE_AXFR: + IXFRIN_LOG(LOG_INFO, data, + "receiving AXFR-style IXFR"); + return axfr_consume(pkt, data, true); + case XFR_TYPE_UPTODATE: + consume_edns_expire(data, pkt, false); + finalize_timers(data); + char expires_in[32] = ""; + fill_expires_in(expires_in, sizeof(expires_in), data); + IXFRIN_LOG(LOG_INFO, data, + "zone is up-to-date%s", expires_in); + xfr_stats_begin(&data->stats); + xfr_stats_add(&data->stats, pkt->size); + xfr_stats_end(&data->stats); + return KNOT_STATE_DONE; + case XFR_TYPE_IXFR: + break; + default: + assert(0); + data->ret = KNOT_EPROCESSING; + return KNOT_STATE_FAIL; + } + + data->ret = ixfr_init(data); + if (data->ret != KNOT_EOK) { + IXFRIN_LOG(LOG_WARNING, data, + "failed to initialize (%s)", knot_strerror(data->ret)); + data->fallback_axfr = false; + data->fallback->remote = false; + return KNOT_STATE_FAIL; + } + + IXFRIN_LOG(LOG_INFO, data, "started"); + xfr_stats_begin(&data->stats); + data->change_size = 0; + } + + int next; + // Process saved SOA if existing + if (data->initial_soa_copy != NULL) { + next = ixfr_consume_rr(data->initial_soa_copy, data); + knot_rrset_free(data->initial_soa_copy, data->mm); + data->initial_soa_copy = NULL; + if (next != KNOT_STATE_CONSUME) { + return next; + } + } + + // Process answer packet + xfr_stats_add(&data->stats, pkt->size); + next = ixfr_consume_packet(pkt, data); + + // Finalize + if (next == KNOT_STATE_DONE) { + xfr_stats_end(&data->stats); + } + + return next; +} + +static int soa_query_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + + query_init_pkt(pkt); + + data->ret = knot_pkt_put_question(pkt, data->zone->name, KNOT_CLASS_IN, + KNOT_RRTYPE_SOA); + if (data->ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + if (data->use_edns) { + data->ret = query_put_edns(pkt, &data->edns); + if (data->ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + } + + return KNOT_STATE_CONSUME; +} + +static int soa_query_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + + if (knot_pkt_ext_rcode(pkt) != KNOT_RCODE_NOERROR) { + REFRESH_LOG(LOG_WARNING, data, LOG_DIRECTION_IN, + "server responded with error '%s'", + knot_pkt_ext_rcode_name(pkt)); + data->ret = KNOT_EDENIED; + return KNOT_STATE_FAIL; + } + + const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER); + const knot_rrset_t *rr = answer->count == 1 ? knot_pkt_rr(answer, 0) : NULL; + if (!rr || rr->type != KNOT_RRTYPE_SOA || rr->rrs.count != 1) { + REFRESH_LOG(LOG_WARNING, data, LOG_DIRECTION_IN, + "malformed message"); + conf_val_t val = conf_zone_get(data->conf, C_SEM_CHECKS, data->zone->name); + if (conf_opt(&val) == SEMCHECKS_SOFT) { + data->xfr_type = XFR_TYPE_AXFR; + data->state = STATE_TRANSFER; + return KNOT_STATE_RESET; + } else { + data->ret = KNOT_EMALF; + return KNOT_STATE_FAIL; + } + } + + uint32_t local_serial; + data->ret = slave_zone_serial(data->zone, data->conf, &local_serial); + if (data->ret != KNOT_EOK) { + xfr_log_read_ms(data->zone->name, data->ret); + data->fallback->remote = false; + return KNOT_STATE_FAIL; + } + uint32_t remote_serial = knot_soa_serial(rr->rrs.rdata); + bool current = serial_is_current(local_serial, remote_serial); + bool master_uptodate = serial_is_current(remote_serial, local_serial); + + if (!current) { + REFRESH_LOG(LOG_INFO, data, LOG_DIRECTION_NONE, + "remote serial %u, zone is outdated", remote_serial); + data->state = STATE_TRANSFER; + return KNOT_STATE_RESET; // continue with transfer + } else if (master_uptodate) { + consume_edns_expire(data, pkt, false); + finalize_timers(data); + char expires_in[32] = ""; + fill_expires_in(expires_in, sizeof(expires_in), data); + REFRESH_LOG(LOG_INFO, data, LOG_DIRECTION_NONE, + "remote serial %u, zone is up-to-date%s", + remote_serial, expires_in); + return KNOT_STATE_DONE; + } else { + REFRESH_LOG(LOG_INFO, data, LOG_DIRECTION_NONE, + "remote serial %u, remote is outdated", remote_serial); + return KNOT_STATE_FAIL; + } +} + +static int transfer_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + + query_init_pkt(pkt); + + bool ixfr = (data->xfr_type == XFR_TYPE_IXFR); + + data->ret = knot_pkt_put_question(pkt, data->zone->name, KNOT_CLASS_IN, + ixfr ? KNOT_RRTYPE_IXFR : KNOT_RRTYPE_AXFR); + if (data->ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + if (ixfr) { + assert(data->soa); + knot_rrset_t *sending_soa = knot_rrset_copy(data->soa, data->mm); + uint32_t master_serial; + data->ret = slave_zone_serial(data->zone, data->conf, &master_serial); + if (data->ret != KNOT_EOK) { + data->fallback->remote = false; + xfr_log_read_ms(data->zone->name, data->ret); + } + if (sending_soa == NULL || data->ret != KNOT_EOK) { + knot_rrset_free(sending_soa, data->mm); + return KNOT_STATE_FAIL; + } + knot_soa_serial_set(sending_soa->rrs.rdata, master_serial); + knot_pkt_begin(pkt, KNOT_AUTHORITY); + knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, sending_soa, 0); + knot_rrset_free(sending_soa, data->mm); + } + + if (data->use_edns) { + data->ret = query_put_edns(pkt, &data->edns); + if (data->ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + } + + return KNOT_STATE_CONSUME; +} + +static int transfer_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + + consume_edns_expire(data, pkt, true); + if (data->expire_timer < 2) { + REFRESH_LOG(LOG_WARNING, data, LOG_DIRECTION_NONE, + "remote is expired, ignoring"); + return KNOT_STATE_IGNORE; + } + + data->fallback_axfr = (data->xfr_type == XFR_TYPE_IXFR); + + int next = (data->xfr_type == XFR_TYPE_AXFR) ? axfr_consume(pkt, data, false) : + ixfr_consume(pkt, data); + + // Transfer completed + if (next == KNOT_STATE_DONE) { + // Log transfer even if we still can fail + xfr_log_finished(data->zone->name, + data->xfr_type == XFR_TYPE_IXFR || + data->xfr_type == XFR_TYPE_UPTODATE ? + LOG_OPERATION_IXFR : LOG_OPERATION_AXFR, + LOG_DIRECTION_IN, data->remote, + layer->flags & KNOT_REQUESTOR_REUSED, + &data->stats); + + /* + * TODO: Move finialization into finish + * callback. And update requestor to allow reset from fallback + * as we need IXFR to AXFR failover. + */ + if (tsig_unsigned_count(layer->tsig) != 0) { + data->ret = KNOT_EMALF; + return KNOT_STATE_FAIL; + } + + // Finalize and publish the zone + switch (data->xfr_type) { + case XFR_TYPE_IXFR: + data->ret = ixfr_finalize(data); + break; + case XFR_TYPE_AXFR: + data->ret = axfr_finalize(data); + break; + default: + return next; + } + if (data->ret == KNOT_EOK) { + data->updated = true; + } else { + next = KNOT_STATE_FAIL; + } + } + + return next; +} + +static int refresh_begin(knot_layer_t *layer, void *_data) +{ + layer->data = _data; + struct refresh_data *data = _data; + data->layer = layer; + + if (data->soa) { + data->state = STATE_SOA_QUERY; + data->xfr_type = XFR_TYPE_IXFR; + data->initial_soa_copy = NULL; + } else { + data->state = STATE_TRANSFER; + data->xfr_type = XFR_TYPE_AXFR; + data->initial_soa_copy = NULL; + } + + data->started = time_now(); + + return KNOT_STATE_PRODUCE; +} + +static int refresh_produce(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + data->layer = layer; + + switch (data->state) { + case STATE_SOA_QUERY: return soa_query_produce(layer, pkt); + case STATE_TRANSFER: return transfer_produce(layer, pkt); + default: + return KNOT_STATE_FAIL; + } +} + +static int refresh_consume(knot_layer_t *layer, knot_pkt_t *pkt) +{ + struct refresh_data *data = layer->data; + data->layer = layer; + + data->fallback->address = false; // received something, other address not needed + + switch (data->state) { + case STATE_SOA_QUERY: return soa_query_consume(layer, pkt); + case STATE_TRANSFER: return transfer_consume(layer, pkt); + default: + return KNOT_STATE_FAIL; + } +} + +static int refresh_reset(knot_layer_t *layer) +{ + return KNOT_STATE_PRODUCE; +} + +static int refresh_finish(knot_layer_t *layer) +{ + struct refresh_data *data = layer->data; + data->layer = layer; + + // clean processing context + axfr_cleanup(data); + ixfr_cleanup(data); + + return KNOT_STATE_NOOP; +} + +static const knot_layer_api_t REFRESH_API = { + .begin = refresh_begin, + .produce = refresh_produce, + .consume = refresh_consume, + .reset = refresh_reset, + .finish = refresh_finish, +}; + +static size_t max_zone_size(conf_t *conf, const knot_dname_t *zone) +{ + conf_val_t val = conf_zone_get(conf, C_ZONE_MAX_SIZE, zone); + return conf_int(&val); +} + +typedef struct { + bool force_axfr; + bool send_notify; +} try_refresh_ctx_t; + +static int try_refresh(conf_t *conf, zone_t *zone, const conf_remote_t *master, + void *ctx, zone_master_fallback_t *fallback) +{ + // TODO: Abstract interface to issue DNS queries. This is almost copy-pasted. + + assert(zone); + assert(master); + assert(ctx); + assert(fallback); + + try_refresh_ctx_t *trctx = ctx; + + knot_rrset_t soa = { 0 }; + if (zone->contents) { + soa = node_rrset(zone->contents->apex, KNOT_RRTYPE_SOA); + } + + struct refresh_data data = { + .zone = zone, + .conf = conf, + .remote = (struct sockaddr *)&master->addr, + .soa = zone->contents && !trctx->force_axfr ? &soa : NULL, + .max_zone_size = max_zone_size(conf, zone->name), + .use_edns = !master->no_edns, + .edns = query_edns_data_init(conf, master->addr.ss_family, + QUERY_EDNS_OPT_EXPIRE), + .expire_timer = EXPIRE_TIMER_INVALID, + .fallback = fallback, + .fallback_axfr = false, // will be set upon IXFR consume + }; + + knot_requestor_t requestor; + knot_requestor_init(&requestor, &REFRESH_API, &data, NULL); + + knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL); + if (!pkt) { + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + const struct sockaddr_storage *dst = &master->addr; + const struct sockaddr_storage *src = &master->via; + knot_request_flag_t flags = conf->cache.srv_tcp_fastopen ? KNOT_REQUEST_TFO : 0; + knot_request_t *req = knot_request_make(NULL, dst, src, pkt, &master->key, flags); + if (!req) { + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + return KNOT_ENOMEM; + } + + int timeout = conf->cache.srv_tcp_remote_io_timeout; + + int ret; + + // while loop runs 0x or 1x; IXFR to AXFR failover + while (ret = knot_requestor_exec(&requestor, req, timeout), + ret = (data.ret == KNOT_EOK ? ret : data.ret), + data.fallback_axfr && ret != KNOT_EOK) { + REFRESH_LOG(LOG_WARNING, &data, LOG_DIRECTION_IN, + "fallback to AXFR (%s)", knot_strerror(ret)); + ixfr_cleanup(&data); + data.ret = KNOT_EOK; + data.xfr_type = XFR_TYPE_AXFR; + data.fallback_axfr = false, + requestor.layer.state = KNOT_STATE_RESET; + requestor.layer.flags |= KNOT_REQUESTOR_CLOSE; + } + knot_request_free(req, NULL); + knot_requestor_clear(&requestor); + + if (ret == KNOT_EOK) { + trctx->send_notify = data.updated && !master->block_notify_after_xfr; + trctx->force_axfr = false; + } + + return ret; +} + +int event_refresh(conf_t *conf, zone_t *zone) +{ + assert(zone); + + if (!zone_is_slave(conf, zone)) { + return KNOT_ENOTSUP; + } + + try_refresh_ctx_t trctx = { 0 }; + + // TODO: Flag on zone is ugly. Event specific parameters would be nice. + if (zone_get_flag(zone, ZONE_FORCE_AXFR, true)) { + trctx.force_axfr = true; + zone->zonefile.retransfer = true; + } + + int ret = zone_master_try(conf, zone, try_refresh, &trctx, "refresh"); + zone_clear_preferred_master(zone); + if (ret != KNOT_EOK) { + const knot_rdataset_t *soa = zone_soa(zone); + uint32_t next; + + if (soa) { + next = knot_soa_retry(soa->rdata); + } else { + next = bootstrap_next(&zone->zonefile.bootstrap_cnt); + } + + limit_timer(conf, zone->name, &next, "retry", + C_RETRY_MIN_INTERVAL, C_RETRY_MAX_INTERVAL); + zone->timers.next_refresh = time(NULL) + next; + zone->timers.last_refresh_ok = false; + + char time_str[64] = { 0 }; + struct tm time_gm = { 0 }; + localtime_r(&zone->timers.next_refresh, &time_gm); + strftime(time_str, sizeof(time_str), KNOT_LOG_TIME_FORMAT, &time_gm); + + log_zone_error(zone->name, "refresh, failed (%s), next retry at %s", + knot_strerror(ret), time_str); + } else { + zone->zonefile.bootstrap_cnt = 0; + } + + /* Reschedule events. */ + replan_from_timers(conf, zone); + if (trctx.send_notify) { + zone_schedule_notify(zone, 1); + } + + return ret; +} diff --git a/src/knot/events/handlers/update.c b/src/knot/events/handlers/update.c new file mode 100644 index 0000000..f337eb5 --- /dev/null +++ b/src/knot/events/handlers/update.c @@ -0,0 +1,433 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/events/handlers.h" +#include "knot/nameserver/log.h" +#include "knot/nameserver/process_query.h" +#include "knot/query/capture.h" +#include "knot/query/requestor.h" +#include "knot/updates/ddns.h" +#include "knot/zone/digest.h" +#include "knot/zone/zone.h" +#include "libdnssec/random.h" +#include "libknot/libknot.h" +#include "contrib/net.h" +#include "contrib/time.h" + +#define UPDATE_LOG(priority, qdata, fmt...) \ + ns_log(priority, knot_pkt_qname(qdata->query), LOG_OPERATION_UPDATE, \ + LOG_DIRECTION_IN, (struct sockaddr *)knotd_qdata_remote_addr(qdata), \ + false, fmt) + +static void init_qdata_from_request(knotd_qdata_t *qdata, + zone_t *zone, + knot_request_t *req, + knotd_qdata_params_t *params, + knotd_qdata_extra_t *extra) +{ + memset(qdata, 0, sizeof(*qdata)); + qdata->params = params; + qdata->query = req->query; + qdata->sign = req->sign; + qdata->extra = extra; + memset(extra, 0, sizeof(*extra)); + qdata->extra->zone = zone; +} + +static int check_prereqs(knot_request_t *request, + const zone_t *zone, zone_update_t *update, + knotd_qdata_t *qdata) +{ + uint16_t rcode = KNOT_RCODE_NOERROR; + int ret = ddns_process_prereqs(request->query, update, &rcode); + if (ret != KNOT_EOK) { + UPDATE_LOG(LOG_WARNING, qdata, "prerequisites not met (%s)", + knot_strerror(ret)); + assert(rcode != KNOT_RCODE_NOERROR); + knot_wire_set_rcode(request->resp->wire, rcode); + return ret; + } + + return KNOT_EOK; +} + +static int process_single_update(knot_request_t *request, + const zone_t *zone, zone_update_t *update, + knotd_qdata_t *qdata) +{ + uint16_t rcode = KNOT_RCODE_NOERROR; + int ret = ddns_process_update(zone, request->query, update, &rcode); + if (ret != KNOT_EOK) { + UPDATE_LOG(LOG_WARNING, qdata, "failed to apply (%s)", + knot_strerror(ret)); + assert(rcode != KNOT_RCODE_NOERROR); + knot_wire_set_rcode(request->resp->wire, rcode); + return ret; + } + + return KNOT_EOK; +} + +static void set_rcodes(list_t *requests, const uint16_t rcode) +{ + ptrnode_t *node; + WALK_LIST(node, *requests) { + knot_request_t *req = node->d; + if (knot_wire_get_rcode(req->resp->wire) == KNOT_RCODE_NOERROR) { + knot_wire_set_rcode(req->resp->wire, rcode); + } + } +} + +static int process_bulk(zone_t *zone, list_t *requests, zone_update_t *up) +{ + // Walk all the requests and process. + ptrnode_t *node; + WALK_LIST(node, *requests) { + knot_request_t *req = node->d; + // Init qdata structure for logging (unique per-request). + knotd_qdata_params_t params = { + .remote = &req->remote + }; + knotd_qdata_t qdata; + knotd_qdata_extra_t extra; + init_qdata_from_request(&qdata, zone, req, ¶ms, &extra); + + int ret = check_prereqs(req, zone, up, &qdata); + if (ret != KNOT_EOK) { + // Skip updates with failed prereqs. + continue; + } + + ret = process_single_update(req, zone, up, &qdata); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int process_normal(conf_t *conf, zone_t *zone, list_t *requests) +{ + assert(requests); + + // Init zone update structure + zone_update_t up; + int ret = zone_update_init(&up, zone, UPDATE_INCREMENTAL | UPDATE_NO_CHSET); + if (ret != KNOT_EOK) { + set_rcodes(requests, KNOT_RCODE_SERVFAIL); + return ret; + } + + // Process all updates. + ret = process_bulk(zone, requests, &up); + if (ret == KNOT_EOK) { + ret = zone_update_verify_digest(conf, &up); + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + set_rcodes(requests, KNOT_RCODE_SERVFAIL); + return ret; + } + + // Sign update. + conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name); + bool dnssec_enable = conf_bool(&val); + val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name); + unsigned digest_alg = conf_opt(&val); + if (dnssec_enable) { + ret = knot_dnssec_sign_update(&up, conf); + } else if (digest_alg != ZONE_DIGEST_NONE) { + if (zone_update_to(&up) == NULL) { + ret = zone_update_increment_soa(&up, conf); + } + if (ret == KNOT_EOK) { + ret = zone_update_add_digest(&up, digest_alg, false); + } + } + if (ret != KNOT_EOK) { + zone_update_clear(&up); + set_rcodes(requests, KNOT_RCODE_SERVFAIL); + return ret; + } + + // Apply changes. + ret = zone_update_commit(conf, &up); + if (ret != KNOT_EOK) { + zone_update_clear(&up); + if (ret == KNOT_EZONESIZE) { + set_rcodes(requests, KNOT_RCODE_REFUSED); + } else { + set_rcodes(requests, KNOT_RCODE_SERVFAIL); + } + return ret; + } + + return KNOT_EOK; +} + +static void process_requests(conf_t *conf, zone_t *zone, list_t *requests) +{ + assert(zone); + assert(requests); + + /* Keep original state. */ + struct timespec t_start = time_now(); + const uint32_t old_serial = zone_contents_serial(zone->contents); + + /* Process authenticated packet. */ + int ret = process_normal(conf, zone, requests); + if (ret != KNOT_EOK) { + log_zone_error(zone->name, "DDNS, processing failed (%s)", + knot_strerror(ret)); + return; + } + + /* Evaluate response. */ + const uint32_t new_serial = zone_contents_serial(zone->contents); + if (new_serial == old_serial) { + log_zone_info(zone->name, "DDNS, finished, no changes to the zone were made"); + return; + } + + struct timespec t_end = time_now(); + log_zone_info(zone->name, "DDNS, finished, serial %u -> %u, " + "%.02f seconds", old_serial, new_serial, + time_diff_ms(&t_start, &t_end) / 1000.0); + + zone_schedule_notify(zone, 1); +} + +static int remote_forward(conf_t *conf, knot_request_t *request, conf_remote_t *remote) +{ + /* Copy request and assign new ID. */ + knot_pkt_t *query = knot_pkt_new(NULL, request->query->max_size, NULL); + int ret = knot_pkt_copy(query, request->query); + if (ret != KNOT_EOK) { + knot_pkt_free(query); + return ret; + } + knot_wire_set_id(query->wire, dnssec_random_uint16_t()); + knot_tsig_append(query->wire, &query->size, query->max_size, query->tsig_rr); + + /* Prepare packet capture layer. */ + const knot_layer_api_t *capture = query_capture_api(); + struct capture_param capture_param = { + .sink = request->resp + }; + + /* Create requestor instance. */ + knot_requestor_t re; + ret = knot_requestor_init(&re, capture, &capture_param, NULL); + if (ret != KNOT_EOK) { + knot_pkt_free(query); + return ret; + } + + /* Create a request. */ + const struct sockaddr_storage *dst = &remote->addr; + const struct sockaddr_storage *src = &remote->via; + knot_request_flag_t flags = conf->cache.srv_tcp_fastopen ? KNOT_REQUEST_TFO : 0; + knot_request_t *req = knot_request_make(re.mm, dst, src, query, NULL, flags); + if (req == NULL) { + knot_requestor_clear(&re); + knot_pkt_free(query); + return KNOT_ENOMEM; + } + + /* Execute the request. */ + int timeout = conf->cache.srv_tcp_remote_io_timeout; + ret = knot_requestor_exec(&re, req, timeout); + + knot_request_free(req, re.mm); + knot_requestor_clear(&re); + + return ret; +} + +static void forward_request(conf_t *conf, zone_t *zone, knot_request_t *request) +{ + /* Read the ddns master or the first master. */ + conf_val_t remote = conf_zone_get(conf, C_DDNS_MASTER, zone->name); + if (remote.code != KNOT_EOK) { + remote = conf_zone_get(conf, C_MASTER, zone->name); + } + + /* Get the number of remote addresses. */ + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, &remote); + size_t addr_count = conf_val_count(&addr); + assert(addr_count > 0); + + /* Try all remote addresses to forward the request to. */ + int ret = KNOT_EOK; + for (size_t i = 0; i < addr_count; i++) { + conf_remote_t master = conf_remote(conf, &remote, i); + + ret = remote_forward(conf, request, &master); + if (ret == KNOT_EOK) { + break; + } + } + + /* Restore message ID and TSIG. */ + knot_wire_set_id(request->resp->wire, knot_wire_get_id(request->query->wire)); + knot_tsig_append(request->resp->wire, &request->resp->size, + request->resp->max_size, request->resp->tsig_rr); + + /* Set RCODE if forwarding failed. */ + if (ret != KNOT_EOK) { + knot_wire_set_rcode(request->resp->wire, KNOT_RCODE_SERVFAIL); + log_zone_error(zone->name, "DDNS, failed to forward updates to the master (%s)", + knot_strerror(ret)); + } else { + log_zone_info(zone->name, "DDNS, updates forwarded to the master"); + } +} + +static void forward_requests(conf_t *conf, zone_t *zone, list_t *requests) +{ + assert(zone); + assert(requests); + + ptrnode_t *node; + WALK_LIST(node, *requests) { + knot_request_t *req = node->d; + forward_request(conf, zone, req); + } +} + +static void send_update_response(conf_t *conf, zone_t *zone, knot_request_t *req) +{ + if (req->resp) { + if (!zone_is_slave(conf, zone)) { + // Sign the response with TSIG where applicable + knotd_qdata_t qdata; + knotd_qdata_extra_t extra; + init_qdata_from_request(&qdata, zone, req, NULL, &extra); + + (void)process_query_sign_response(req->resp, &qdata); + } + + if (net_is_stream(req->fd)) { + net_dns_tcp_send(req->fd, req->resp->wire, req->resp->size, + conf->cache.srv_tcp_remote_io_timeout, NULL); + } else { + net_dgram_send(req->fd, req->resp->wire, req->resp->size, + &req->remote); + } + } +} + +static void free_request(knot_request_t *req) +{ + close(req->fd); + knot_pkt_free(req->query); + knot_pkt_free(req->resp); + dnssec_binary_free(&req->sign.tsig_key.secret); + free(req); +} + +static void send_update_responses(conf_t *conf, zone_t *zone, list_t *updates) +{ + ptrnode_t *node, *nxt; + WALK_LIST_DELSAFE(node, nxt, *updates) { + knot_request_t *req = node->d; + send_update_response(conf, zone, req); + free_request(req); + } + ptrlist_free(updates, NULL); +} + +static int init_update_responses(list_t *updates) +{ + ptrnode_t *node, *nxt; + WALK_LIST_DELSAFE(node, nxt, *updates) { + knot_request_t *req = node->d; + req->resp = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL); + if (req->resp == NULL) { + return KNOT_ENOMEM; + } + + assert(req->query); + knot_pkt_init_response(req->resp, req->query); + } + + return KNOT_EOK; +} + +static size_t update_dequeue(zone_t *zone, list_t *updates) +{ + assert(zone); + assert(updates); + + pthread_mutex_lock(&zone->ddns_lock); + + if (EMPTY_LIST(zone->ddns_queue)) { + /* Lost race during reload. */ + pthread_mutex_unlock(&zone->ddns_lock); + return 0; + } + + *updates = zone->ddns_queue; + size_t update_count = zone->ddns_queue_size; + init_list(&zone->ddns_queue); + zone->ddns_queue_size = 0; + + pthread_mutex_unlock(&zone->ddns_lock); + + return update_count; +} + +int event_update(conf_t *conf, zone_t *zone) +{ + assert(zone); + + /* Get list of pending updates. */ + list_t updates; + size_t update_count = update_dequeue(zone, &updates); + if (update_count == 0) { + return KNOT_EOK; + } + + /* Init updates responses. */ + int ret = init_update_responses(&updates); + if (ret != KNOT_EOK) { + /* Send what responses we can. */ + set_rcodes(&updates, KNOT_RCODE_SERVFAIL); + send_update_responses(conf, zone, &updates); + return ret; + } + + /* Process update list - forward if zone has master, or execute. + RCODEs are set. */ + if (zone_is_slave(conf, zone)) { + log_zone_info(zone->name, + "DDNS, forwarding %zu updates", update_count); + forward_requests(conf, zone, &updates); + } else { + log_zone_info(zone->name, + "DDNS, processing %zu updates", update_count); + process_requests(conf, zone, &updates); + } + + /* Send responses. */ + send_update_responses(conf, zone, &updates); + + return KNOT_EOK; +} diff --git a/src/knot/events/replan.c b/src/knot/events/replan.c new file mode 100644 index 0000000..ed03fe1 --- /dev/null +++ b/src/knot/events/replan.c @@ -0,0 +1,210 @@ +/* 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/dnssec/kasp/kasp_db.h" +#include "knot/events/replan.h" + +#define TIME_CANCEL 0 +#define TIME_IGNORE (-1) + +/*! + * \brief Move DDNS queue from old zone to new zone and replan if necessary. + * + * New zone will contain references from the old zone. New zone will free + * the data. + */ +static void replan_ddns(zone_t *zone, zone_t *old_zone) +{ + if (old_zone->ddns_queue_size == 0) { + return; + } + + ptrnode_t *node; + WALK_LIST(node, old_zone->ddns_queue) { + ptrlist_add(&zone->ddns_queue, node->d, NULL); + } + zone->ddns_queue_size = old_zone->ddns_queue_size; + + ptrlist_free(&old_zone->ddns_queue, NULL); + + zone_events_schedule_now(zone, ZONE_EVENT_UPDATE); +} + +/*! + * \brief Replan events that are already planned for the old zone. + * + * \notice Preserves notifailed. + */ +static void replan_from_zone(zone_t *zone, zone_t *old_zone) +{ + assert(zone); + assert(old_zone); + + replan_ddns(zone, old_zone); + + const zone_event_type_t types[] = { + ZONE_EVENT_REFRESH, + ZONE_EVENT_FLUSH, + ZONE_EVENT_BACKUP, + ZONE_EVENT_NOTIFY, + ZONE_EVENT_UFREEZE, + ZONE_EVENT_UTHAW, + ZONE_EVENT_INVALID + }; + + for (const zone_event_type_t *type = types; *type != ZONE_EVENT_INVALID; type++) { + time_t when = zone_events_get_time(old_zone, *type); + if (when > 0) { + zone_events_schedule_at(zone, *type, when); + } + } +} + +/*! + * \brief Replan DNSSEC if automatic signing enabled. + * + * This is required as the configuration could have changed. + */ +static void replan_dnssec(conf_t *conf, zone_t *zone) +{ + assert(conf); + assert(zone); + + conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name); + if (conf_bool(&val)) { + zone_events_schedule_now(zone, ZONE_EVENT_DNSSEC); + } +} + +/*! + * \brief Replan events that depend on zone timers (REFRESH, EXPIRE, FLUSH, RESALT, PARENT DS QUERY). + */ +void replan_from_timers(conf_t *conf, zone_t *zone) +{ + assert(conf); + assert(zone); + + time_t now = time(NULL); + + time_t refresh = TIME_CANCEL; + if (zone_is_slave(conf, zone)) { + refresh = zone->timers.next_refresh; + if (zone->contents == NULL && zone->timers.last_refresh_ok) { // zone disappeared w/o expiry + refresh = now; + } + assert(refresh > 0); + } + + time_t expire_pre = TIME_IGNORE; + time_t expire = TIME_IGNORE; + if (zone_is_slave(conf, zone) && zone->contents != NULL) { + expire_pre = TIME_CANCEL; + expire = zone->timers.next_expire; + } + + time_t flush = TIME_IGNORE; + if (!zone_is_slave(conf, zone) || zone->contents != NULL) { + conf_val_t val = conf_zone_get(conf, C_ZONEFILE_SYNC, zone->name); + int64_t sync_timeout = conf_int(&val); + if (sync_timeout > 0) { + flush = zone->timers.last_flush + sync_timeout; + } + } + + time_t resalt = TIME_IGNORE; + time_t ds_check = TIME_CANCEL; + time_t ds_push = TIME_CANCEL; + conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name); + if (conf_bool(&val)) { + conf_val_t policy = conf_zone_get(conf, C_DNSSEC_POLICY, zone->name); + conf_id_fix_default(&policy); + val = conf_id_get(conf, C_POLICY, C_NSEC3, &policy); + if (conf_bool(&val)) { + knot_time_t last_resalt = 0; + if (knot_lmdb_open(zone_kaspdb(zone)) == KNOT_EOK) { + (void)kasp_db_load_nsec3salt(zone_kaspdb(zone), zone->name, NULL, &last_resalt); + } + if (last_resalt == 0) { + resalt = now; + } else { + val = conf_id_get(conf, C_POLICY, C_NSEC3_SALT_LIFETIME, &policy); + if (conf_int(&val) > 0) { + resalt = last_resalt + conf_int(&val); + } + } + } + + ds_check = zone->timers.next_ds_check; + if (ds_check == 0) { + ds_check = TIME_IGNORE; + } + ds_push = zone->timers.next_ds_push; + if (ds_push == 0) { + ds_push = TIME_IGNORE; + } + } + + zone_events_schedule_at(zone, + ZONE_EVENT_REFRESH, refresh, + ZONE_EVENT_EXPIRE, expire_pre, + ZONE_EVENT_EXPIRE, expire, + ZONE_EVENT_FLUSH, flush, + ZONE_EVENT_DNSSEC, resalt, + ZONE_EVENT_DS_CHECK, ds_check, + ZONE_EVENT_DS_PUSH, ds_push); +} + +void replan_load_new(zone_t *zone, bool gen_catalog) +{ + if (gen_catalog) { + /* Catalog generation must wait until the zonedb + * is fully created. */ + zone_events_schedule_now(zone, ZONE_EVENT_LOAD); + } else { + /* Enqueue directly, make first load waitable, + * other events will cascade from load. */ + zone_events_enqueue(zone, ZONE_EVENT_LOAD); + } +} + +void replan_load_bootstrap(conf_t *conf, zone_t *zone) +{ + replan_from_timers(conf, zone); +} + +void replan_load_current(conf_t *conf, zone_t *zone, zone_t *old_zone) +{ + replan_from_zone(zone, old_zone); + + if (zone->contents != NULL || zone_expired(zone)) { + replan_from_timers(conf, zone); + replan_dnssec(conf, zone); + } else { + zone_events_schedule_now(zone, ZONE_EVENT_LOAD); + } +} + +void replan_load_updated(zone_t *zone, zone_t *old_zone) +{ + zone_notifailed_clear(zone); + replan_from_zone(zone, old_zone); + + // other events will cascade from load + zone_events_schedule_now(zone, ZONE_EVENT_LOAD); +} diff --git a/src/knot/events/replan.h b/src/knot/events/replan.h new file mode 100644 index 0000000..62ebeb2 --- /dev/null +++ b/src/knot/events/replan.h @@ -0,0 +1,35 @@ +/* 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/>. + */ + +#pragma once + +#include "knot/conf/conf.h" +#include "knot/zone/zone.h" + +/*! + * \brief Replan timer dependent refresh, expire, and flush. + */ +void replan_from_timers(conf_t *conf, zone_t *zone); + +/*! + * \defgroup replan_load Replan timers after zone load or reload. + * @{ + */ +void replan_load_new(zone_t *zone, bool gen_catalog); +void replan_load_bootstrap(conf_t *conf, zone_t *zone); +void replan_load_current(conf_t *conf, zone_t *zone, zone_t *old_zone); +void replan_load_updated(zone_t *zone, zone_t *old_zone); +/*! @} */ diff --git a/src/knot/include/module.h b/src/knot/include/module.h new file mode 100644 index 0000000..8190828 --- /dev/null +++ b/src/knot/include/module.h @@ -0,0 +1,602 @@ +/* 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/>. + */ + +/*! + * \file + * + * \brief Knot DNS module interface. + * + * \addtogroup module + * @{ + */ + +#pragma once + +#include <stdarg.h> +#include <stdint.h> +#include <syslog.h> +#include <sys/socket.h> + +#include <libknot/libknot.h> +#include <libknot/yparser/ypschema.h> + +/*** Query module API. ***/ + +/*! Current module ABI version. */ +#define KNOTD_MOD_ABI_VERSION 400 +/*! Module configuration name prefix. */ +#define KNOTD_MOD_NAME_PREFIX "mod-" + +/*! Configuration check function context. */ +typedef struct { + const yp_item_t *item; /*!< Current item descriptor. */ + const uint8_t *id; /*!< Current section identifier. */ + size_t id_len; /*!< Current section identifier length. */ + const uint8_t *data; /*!< Current item data. */ + size_t data_len; /*!< Current item data length. */ + const char *err_str; /*!< Output error message. */ + struct knotd_conf_check_extra *extra; /*!< Private items (conf/tools.h). */ +} knotd_conf_check_args_t; + +/*! Module context. */ +typedef struct knotd_mod knotd_mod_t; + +/*! + * Module load callback. + * + * Responsibilities: + * - Query processing hooks registration + * - Optional module specific context initialization + * - Module configuration processing + * - Query statistics counters registration + * + * \param[in] mod Module context. + * + * \return Error code, KNOT_EOK if success. + */ +typedef int (*knotd_mod_load_f)(knotd_mod_t *mod); + +/*! + * Module unload callback. + * + * Responsibilities: + * - Optional module specific context deinitialization + * + * \param[in] mod Module context. + */ +typedef void (*knotd_mod_unload_f)(knotd_mod_t *mod); + +/*! + * Module configuration section check callback. + * + * Responsibilities: + * - Optional module configuration section items checks. + * + * \note Set args.err_str to proper error message if error. + * + * \param[in] args Configuration check arguments. + * + * \return Error code, KNOT_EOK if success. + */ +typedef int (*knotd_conf_check_f)(knotd_conf_check_args_t *args); + +/*! Module flags. */ +typedef enum { + KNOTD_MOD_FLAG_NONE = 0, /*!< Unspecified. */ + KNOTD_MOD_FLAG_OPT_CONF = 1 << 0, /*!< Optional module configuration. */ + KNOTD_MOD_FLAG_SCOPE_GLOBAL = 1 << 1, /*!< Can be specified as global module. */ + KNOTD_MOD_FLAG_SCOPE_ZONE = 1 << 2, /*!< Can be specified as zone module. */ + KNOTD_MOD_FLAG_SCOPE_ANY = KNOTD_MOD_FLAG_SCOPE_GLOBAL | + KNOTD_MOD_FLAG_SCOPE_ZONE, +} knotd_mod_flag_t; + +/*! Module API. */ +typedef struct { + uint32_t version; /*!< Embedded version of the module ABI. */ + const char *name; /*!< Module name. */ + knotd_mod_flag_t flags; /*!< Module flags. */ + knotd_mod_load_f load; /*!< Module load callback. */ + knotd_mod_unload_f unload; /*!< Module unload callback. */ + const yp_item_t *config; /*!< Module configuration schema. */ + knotd_conf_check_f config_check; /*!< Module configuration check callback. */ +} knotd_mod_api_t; + +/*! Static module API symbol must have a unique name. */ +#ifdef KNOTD_MOD_STATIC + #define KNOTD_MOD_API_NAME(mod_name) knotd_mod_api_##mod_name +#else + #define KNOTD_MOD_API_NAME(mod_name) knotd_mod_api +#endif + +/*! Module API instance initialization helper macro. */ +#define KNOTD_MOD_API(mod_name, mod_flags, mod_load, mod_unload, mod_conf, mod_conf_check) \ + __attribute__((visibility("default"))) \ + const knotd_mod_api_t KNOTD_MOD_API_NAME(mod_name) = { \ + .version = KNOTD_MOD_ABI_VERSION, \ + .name = KNOTD_MOD_NAME_PREFIX #mod_name, \ + .flags = mod_flags, \ + .load = mod_load, \ + .unload = mod_unload, \ + .config = mod_conf, \ + .config_check = mod_conf_check, \ + } + +/*** Configuration, statistics, logging,... API. ***/ + +/*! + * Checks reference item (YP_TREF) value if the destination exists. + * + * \note This function is intended to be used in module schema. + * + * \param[in] args Configuration check arguments. + * + * \return Error code, KNOT_EOK if success. + */ +int knotd_conf_check_ref(knotd_conf_check_args_t *args); + +/*! + * Gets optional module context. + * + * \param[in] mod Module context. + * + * \return Pointer to optional module context. + */ +void *knotd_mod_ctx(knotd_mod_t *mod); + +/*! + * Sets optional module context. + * + * \param[in] mod Module context. + * \param[in] ctx Optional module context. + */ +void knotd_mod_ctx_set(knotd_mod_t *mod, void *ctx); + +/*! + * Gets the zone name the module is configured for. + * + * \param[in] mod Module context. + * + * \return Zone name. + */ +const knot_dname_t *knotd_mod_zone(knotd_mod_t *mod); + +/*! + * Emits a module specific log message. + * + * \param[in] mod Module context. + * \param[in] priority Message priority (LOG_DEBUG...LOG_CRIT). + * \param[in] fmt Content of the message. + */ +void knotd_mod_log(knotd_mod_t *mod, int priority, const char *fmt, ...); + +/*! + * Emits a module specific log message (va_list variant). + * + * \param[in] mod Module context. + * \param[in] priority Message priority (LOG_DEBUG...LOG_CRIT). + * \param[in] fmt Content of the message. + * \param[in] args Variable argument list. + */ +void knotd_mod_vlog(knotd_mod_t *mod, int priority, const char *fmt, va_list args); + +/*! + * Statistics multi-counter index to name transformation callback. + * + * \param[in] idx Multi-counter index. + * \param[in] idx_count Number of subcounters. + * + * \return Index name string. + */ +typedef char* (*knotd_mod_idx_to_str_f)(uint32_t idx, uint32_t idx_count); + +/*! + * Registers a statistics counter. + * + * \param[in] mod Module context. + * \param[in] ctr_name Counter name + * \param[in] idx_count Number of subcounters (set 1 for single-counter). + * \param[in] idx_to_str Subcounter index to name transformation callback + * (set NULL for single-counter). + * + * \return Error code, KNOT_EOK if success. + */ +int knotd_mod_stats_add(knotd_mod_t *mod, const char *ctr_name, uint32_t idx_count, + knotd_mod_idx_to_str_f idx_to_str); + +/*! + * Increments a statistics counter. + * + * \param[in] mod Module context. + * \param[in] thr_id Index of worker thread. + * \param[in] ctr_id Counter id (counted in the order the counters were registered). + * \param[in] idx Subcounter index (set 0 for single-counter). + * \param[in] val Value increment. + */ +void knotd_mod_stats_incr(knotd_mod_t *mod, unsigned thr_id, uint32_t ctr_id, + uint32_t idx, uint64_t val); + +/*! + * Decrements a statistics counter. + * + * \param[in] mod Module context. + * \param[in] thr_id Index of worker thread. + * \param[in] ctr_id Counter id (counted in the order the counters were registered). + * \param[in] idx Subcounter index (set 0 for single-counter). + * \param[in] val Value decrement. + */ +void knotd_mod_stats_decr(knotd_mod_t *mod, unsigned thr_id, uint32_t ctr_id, + uint32_t idx, uint64_t val); + +/*! + * Sets a statistics counter value. + * + * \param[in] mod Module context. + * \param[in] thr_id Index of worker thread. + * \param[in] ctr_id Counter id (counted in the order the counters were registered). + * \param[in] idx Subcounter index (set 0 for single-counter). + * \param[in] val Value. + */ +void knotd_mod_stats_store(knotd_mod_t *mod, unsigned thr_id, uint32_t ctr_id, + uint32_t idx, uint64_t val); + +/*! Configuration single-value abstraction. */ +typedef union { + int64_t integer; + unsigned option; + bool boolean; + const char *string; + const knot_dname_t *dname; + struct { + struct sockaddr_storage addr; + struct sockaddr_storage addr_max; + int addr_mask; + }; + struct { + const uint8_t *data; + size_t data_len; + }; +} knotd_conf_val_t; + +/*! Configuration value. */ +typedef struct { + knotd_conf_val_t single; /*!< Single-valued item data. */ + knotd_conf_val_t *multi; /*!< Multi-valued item data. */ + size_t count; /*!< Number of items (0 if default single value). */ +} knotd_conf_t; + +/*! Environment items. */ +typedef enum { + KNOTD_CONF_ENV_VERSION = 0, /*!< Software version. */ + KNOTD_CONF_ENV_HOSTNAME = 1, /*!< Current hostname. */ + KNOTD_CONF_ENV_WORKERS_UDP = 2, /*!< Current number of UDP workers. */ + KNOTD_CONF_ENV_WORKERS_TCP = 3, /*!< Current number of TCP workers. */ + KNOTD_CONF_ENV_WORKERS_XDP = 4, /*!< Current number of UDP-over-XDP workers. */ +} knotd_conf_env_t; + +/*! + * Gets general configuration value. + * + * \param[in] mod Module context. + * \param[in] section_name Section name. + * \param[in] item_name Section item name. + * \param[in] id Section identifier (NULL for simple section). + * + * \return Configuration value. + */ +knotd_conf_t knotd_conf(knotd_mod_t *mod, const yp_name_t *section_name, + const yp_name_t *item_name, const knotd_conf_t *id); + +/*! + * Gets environment value. + * + * \param[in] mod Module context. + * \param[in] env Environment item. + * + * \return Configuration value. + */ +knotd_conf_t knotd_conf_env(knotd_mod_t *mod, knotd_conf_env_t env); + +/*! + * Gets number of answering threads. + * + * \param[in] mod Module context. + * + * \return Number of worker threads. + */ +unsigned knotd_mod_threads(knotd_mod_t *mod); + +/*! + * Gets module configuration value. + * + * \param[in] mod Module context. + * \param[in] item_name Module section item name. + * + * \return Configuration value. + */ +knotd_conf_t knotd_conf_mod(knotd_mod_t *mod, const yp_name_t *item_name); + +/*! + * Gets zone configuration value. + * + * \param[in] mod Module context. + * \param[in] item_name Zone section item name. + * \param[in] zone Zone name. + * + * \return Configuration value. + */ +knotd_conf_t knotd_conf_zone(knotd_mod_t *mod, const yp_name_t *item_name, + const knot_dname_t *zone); + +/*! + * Gets module configuration value during the checking phase. + * + * \note This function is intended to be used in 'knotd_conf_check_f' callbacks. + * + * \param[in] args + * \param[in] item_name + * + * \return Configuration value. + */ +knotd_conf_t knotd_conf_check_item(knotd_conf_check_args_t *args, + const yp_name_t *item_name); + +/*! + * \brief Checks if address is in at least one of given ranges. + * + * \param[in] range + * \param[in] addr + * + * \return true if addr is in at least one range, false otherwise. + */ +bool knotd_conf_addr_range_match(const knotd_conf_t *range, + const struct sockaddr_storage *addr); + +/*! + * Deallocates multi-valued configuration values. + * + * \param[in] conf Configuration value. + */ +void knotd_conf_free(knotd_conf_t *conf); + +/*** Query processing API. ***/ + +/*! + * DNS query type. + * + * This type encompasses the different query types distinguished by both the + * OPCODE and the QTYPE. + */ +typedef enum { + KNOTD_QUERY_TYPE_INVALID, /*!< Invalid query. */ + KNOTD_QUERY_TYPE_NORMAL, /*!< Normal query. */ + KNOTD_QUERY_TYPE_AXFR, /*!< Request for AXFR transfer. */ + KNOTD_QUERY_TYPE_IXFR, /*!< Request for IXFR transfer. */ + KNOTD_QUERY_TYPE_NOTIFY, /*!< NOTIFY query. */ + KNOTD_QUERY_TYPE_UPDATE, /*!< Dynamic update. */ +} knotd_query_type_t; + +/*! Supported transport protocols. */ +typedef enum { + KNOTD_QUERY_PROTO_UDP = KNOT_PROBE_PROTO_UDP, /*!< Pure UDP. */ + KNOTD_QUERY_PROTO_TCP = KNOT_PROBE_PROTO_TCP, /*!< Pure TCP. */ + KNOTD_QUERY_PROTO_QUIC = KNOT_PROBE_PROTO_QUIC, /*!< QUIC/UDP. */ +} knotd_query_proto_t; + +/*! Query processing specific flags. */ +typedef enum { + KNOTD_QUERY_FLAG_COOKIE = 1 << 0, /*!< Valid DNS Cookie indication. */ +} knotd_query_flag_t; + +/*! Query processing data context parameters. */ +typedef struct { + knotd_query_proto_t proto; /*!< Transport protocol used. */ + knotd_query_flag_t flags; /*!< Current query flags. */ + const struct sockaddr_storage *remote; /*!< Current remote address. */ + int socket; /*!< Current network socket. */ + unsigned thread_id; /*!< Current thread id. */ + void *server; /*!< Server object private item. */ + const struct knot_xdp_msg *xdp_msg; /*!< Possible XDP message context. */ + uint32_t measured_rtt; /*!< Measured RTT in usecs: QUIC or TCP-XDP. */ +} knotd_qdata_params_t; + +/*! Query processing data context. */ +typedef struct { + knot_pkt_t *query; /*!< Query to be solved. */ + knotd_query_type_t type; /*!< Query packet type. */ + const knot_dname_t *name; /*!< Currently processed name. */ + uint16_t rcode; /*!< Resulting RCODE (Whole extended RCODE). */ + uint16_t rcode_tsig; /*!< Resulting TSIG RCODE. */ + int rcode_ede; /*!< Resulting Extended (EDE) RCODE. */ + knot_rrset_t opt_rr; /*!< OPT record. */ + knot_sign_context_t sign; /*!< Signing context. */ + knot_edns_client_subnet_t *ecs; /*!< EDNS Client Subnet option. */ + bool err_truncated; /*!< Set TC and AA bits if an error reply. */ + + /*! Persistent items on processing reset. */ + knot_mm_t *mm; /*!< Memory context. */ + knotd_qdata_params_t *params; /*!< Low-level processing parameters. */ + + struct knotd_qdata_extra *extra; /*!< Private items (process_query.h). */ +} knotd_qdata_t; + +/*! + * Gets the local (destination) address of the query. + * + * \param[in] qdata Query data. + * \param[out] buff Auxiliary buffer (not used for XDP). + * + * \return Local address or NULL if error. + */ +const struct sockaddr_storage *knotd_qdata_local_addr(knotd_qdata_t *qdata, + struct sockaddr_storage *buff); + +/*! + * Gets the remote (source) address of the query. + * + * \param[in] qdata Query data. + * + * \return Remote address or NULL if error. + */ +const struct sockaddr_storage *knotd_qdata_remote_addr(knotd_qdata_t *qdata); + +/*! + * Gets the measured TCP round-trip-time. + * + * \param[in] qdata Query data. + * + * \return RTT in microseconds or 0 if error or not available. + */ +uint32_t knotd_qdata_rtt(knotd_qdata_t *qdata); + +/*! + * Gets the current zone name. + * + * \param[in] qdata Query data. + * + * \return Zone name. + */ +const knot_dname_t *knotd_qdata_zone_name(knotd_qdata_t *qdata); + +/*! + * Gets the current zone apex rrset of the given type. + * + * \param[in] qdata Query data. + * \param[in] type Rrset type. + * + * \return A copy of the zone apex rrset. + */ +knot_rrset_t knotd_qdata_zone_apex_rrset(knotd_qdata_t *qdata, uint16_t type); + +/*! General query processing states. */ +typedef enum { + KNOTD_STATE_NOOP = 0, /*!< No response. */ + KNOTD_STATE_DONE = 4, /*!< Finished. */ + KNOTD_STATE_FAIL = 5, /*!< Error. */ + KNOTD_STATE_FINAL = 6, /*!< Finished and finalized (QNAME, EDNS, TSIG). */ +} knotd_state_t; + +/*! brief Internet query processing states. */ +typedef enum { + KNOTD_IN_STATE_BEGIN, /*!< Begin name resolution. */ + KNOTD_IN_STATE_NODATA, /*!< Positive result with NO data. */ + KNOTD_IN_STATE_HIT, /*!< Positive result. */ + KNOTD_IN_STATE_MISS, /*!< Negative result. */ + KNOTD_IN_STATE_DELEG, /*!< Result is delegation. */ + KNOTD_IN_STATE_FOLLOW, /*!< Resolution not complete (CNAME/DNAME chain). */ + KNOTD_IN_STATE_TRUNC, /*!< Finished, packet size limit encountered. */ + KNOTD_IN_STATE_ERROR, /*!< Resolution failed. */ +} knotd_in_state_t; + +/*! Query module processing stages. */ +typedef enum { + KNOTD_STAGE_BEGIN = 0, /*!< Before query processing. */ + KNOTD_STAGE_PREANSWER, /*!< Before section processing. */ + KNOTD_STAGE_ANSWER, /*!< Answer section processing. */ + KNOTD_STAGE_AUTHORITY, /*!< Authority section processing. */ + KNOTD_STAGE_ADDITIONAL, /*!< Additional section processing. */ + KNOTD_STAGE_END, /*!< After query processing. */ +} knotd_stage_t; + +/*! + * General processing hook. + * + * \param[in] state Current processing state. + * \param[in,out] pkt Response packet. + * \param[in] qdata Query data. + * \param[in] mod Module context. + * + * \return Next processing state. + */ +typedef knotd_state_t (*knotd_mod_hook_f) + (knotd_state_t state, knot_pkt_t *pkt, knotd_qdata_t *qdata, knotd_mod_t *mod); + +/*! + * Internet class processing hook. + * + * \param[in] state Current processing state. + * \param[in,out] pkt Response packet. + * \param[in] qdata Query data. + * \param[in] mod Module context. + * + * \return Next processing state. + */ +typedef knotd_in_state_t (*knotd_mod_in_hook_f) + (knotd_in_state_t state, knot_pkt_t *pkt, knotd_qdata_t *qdata, knotd_mod_t *mod); + +/*! + * Registers general processing module hook. + * + * \param[in] mod Module context. + * \param[in] stage Processing stage (KNOTD_STAGE_BEGIN or KNOTD_STAGE_END). + * \param[in] hook Module hook. + * + * \return Error code, KNOT_EOK if success. + */ +int knotd_mod_hook(knotd_mod_t *mod, knotd_stage_t stage, knotd_mod_hook_f hook); + +/*! + * Registers Internet class module hook. + * + * \param[in] mod Module context. + * \param[in] stage Processing stage (KNOTD_STAGE_ANSWER..KNOTD_STAGE_ADDITIONAL). + * \param[in] hook Module hook. + * + * \return Error code, KNOT_EOK if success. + */ +int knotd_mod_in_hook(knotd_mod_t *mod, knotd_stage_t stage, knotd_mod_in_hook_f hook); + +/*** DNSSEC API. ***/ + +/*! + * Initializes DNSSEC signing context. + * + * \param[in] mod Module context. + * + * \return Error code, KNOT_EOK if success. + */ +int knotd_mod_dnssec_init(knotd_mod_t *mod); + +/*! + * Loads available DNSSEC signing keys. + * + * \param[in] mod Module context. + * \param[in] verbose Print key summary into log indication. + * + * \return Error code, KNOT_EOK if success. + */ +int knotd_mod_dnssec_load_keyset(knotd_mod_t *mod, bool verbose); + +/*! + * Frees up resources before re-loading DNSSEC signing keys. + * + * \param[in] mod Module context. + */ +void knotd_mod_dnssec_unload_keyset(knotd_mod_t *mod); + +/*! + * Generates RRSIGs for given RRSet. + * + * \param[in] mod Module context. + * \param[out] rrsigs Output RRSIG RRSet. + * \param[in] rrset Input RRSet to generate RRSIGs for. + * \param[in] mm Memory context. + * + * \return Error code, KNOT_EOK if success. + */ +int knotd_mod_dnssec_sign_rrset(knotd_mod_t *mod, knot_rrset_t *rrsigs, + const knot_rrset_t *rrset, knot_mm_t *mm); + +/*! @} */ diff --git a/src/knot/journal/journal_basic.c b/src/knot/journal/journal_basic.c new file mode 100644 index 0000000..825130a --- /dev/null +++ b/src/knot/journal/journal_basic.c @@ -0,0 +1,92 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "knot/journal/journal_basic.h" +#include "knot/journal/journal_metadata.h" +#include "libknot/error.h" + +MDB_val journal_changeset_id_to_key(bool zone_in_journal, uint32_t serial, const knot_dname_t *zone) +{ + if (zone_in_journal) { + return knot_lmdb_make_key("NIS", zone, (uint32_t)0, "bootstrap"); + } else { + return knot_lmdb_make_key("NII", zone, (uint32_t)0, serial); + } +} + +MDB_val journal_make_chunk_key(const knot_dname_t *apex, uint32_t ch_from, bool zij, uint32_t chunk_id) +{ + if (zij) { + return knot_lmdb_make_key("NISI", apex, (uint32_t)0, "bootstrap", chunk_id); + } else { + return knot_lmdb_make_key("NIII", apex, (uint32_t)0, ch_from, chunk_id); + } +} + +MDB_val journal_zone_prefix(const knot_dname_t *zone) +{ + return knot_lmdb_make_key("NI", zone, (uint32_t)0); +} + +void journal_del_zone(knot_lmdb_txn_t *txn, const knot_dname_t *zone) +{ + assert(txn->is_rw); + MDB_val prefix = journal_zone_prefix(zone); + knot_lmdb_del_prefix(txn, &prefix); + free(prefix.mv_data); +} + +void journal_make_header(void *chunk, uint32_t ch_serial_to) +{ + knot_lmdb_make_key_part(chunk, JOURNAL_HEADER_SIZE, "IILLL", ch_serial_to, + (uint32_t)0 /* we no longer care for # of chunks */, + (uint64_t)0, (uint64_t)0, (uint64_t)0); +} + +uint32_t journal_next_serial(const MDB_val *chunk) +{ + return knot_wire_read_u32(chunk->mv_data); +} + +bool journal_serial_to(knot_lmdb_txn_t *txn, bool zij, uint32_t serial, + const knot_dname_t *zone, uint32_t *serial_to) +{ + MDB_val key = journal_changeset_id_to_key(zij, serial, zone); + bool found = knot_lmdb_find_prefix(txn, &key); + if (found && serial_to != NULL) { + *serial_to = journal_next_serial(&txn->cur_val); + } + free(key.mv_data); + return found; +} + +bool journal_allow_flush(zone_journal_t j) +{ + conf_val_t val = conf_zone_get(j.conf, C_ZONEFILE_SYNC, j.zone); + return conf_int(&val) >= 0; +} + +size_t journal_conf_max_usage(zone_journal_t j) +{ + conf_val_t val = conf_zone_get(j.conf, C_JOURNAL_MAX_USAGE, j.zone); + return conf_int(&val); +} + +size_t journal_conf_max_changesets(zone_journal_t j) +{ + conf_val_t val = conf_zone_get(j.conf, C_JOURNAL_MAX_DEPTH, j.zone); + return conf_int(&val); +} diff --git a/src/knot/journal/journal_basic.h b/src/knot/journal/journal_basic.h new file mode 100644 index 0000000..8804d7b --- /dev/null +++ b/src/knot/journal/journal_basic.h @@ -0,0 +1,118 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/conf/conf.h" +#include "knot/journal/knot_lmdb.h" +#include "knot/updates/changesets.h" +#include "libknot/dname.h" + +typedef struct { + knot_lmdb_db_t *db; + const knot_dname_t *zone; + void *conf; // needed only for journal write operations +} zone_journal_t; + +#define JOURNAL_CHUNK_MAX (70 * 1024) // must be at least 64k + 6B +#define JOURNAL_CHUNK_THRESH (15 * 1024) +#define JOURNAL_HEADER_SIZE (32) + +/*! \brief Convert journal_mode to LMDB environment flags. */ +inline static unsigned journal_env_flags(int journal_mode, bool readonly) +{ + return (journal_mode == JOURNAL_MODE_ASYNC ? (MDB_WRITEMAP | MDB_MAPASYNC) : 0) | + (readonly ? MDB_RDONLY : 0); +} + +/*! + * \brief Create a database key prefix to search for a changeset. + * + * \param zone_in_journal True if searching for zone-in-journal special changeset. + * \param serial Serial-from of the changeset to be searched for. Ignored if 'zone_in_journal'. + * \param zone Name of the zone. + * + * \return DB key. 'mv_data' shall be freed later. 'mv_data' is NULL on failure. + */ +MDB_val journal_changeset_id_to_key(bool zone_in_journal, uint32_t serial, const knot_dname_t *zone); + +/*! + * \brief Create a database key for changeset chunk. + * + * \param apex Zone apex owner name. + * \param ch_from Serial "from" of the stored changeset. + * \param zij Zone-in-journal is stored. + * \param chunk_id Ordinal number of this changeset's chunk. + * + * \return DB key. 'mv_data' shall be freed later. 'mv_data' is NULL on failure. + */ +MDB_val journal_make_chunk_key(const knot_dname_t *apex, uint32_t ch_from, bool zij, uint32_t chunk_id); + +/*! + * \brief Return a key prefix to operate with all zone-related records. + */ +MDB_val journal_zone_prefix(const knot_dname_t *zone); + +/*! + * \brief Delete all zone-related records from journal with open read-write txn. + */ +void journal_del_zone(knot_lmdb_txn_t *txn, const knot_dname_t *zone); + +/*! + * \brief Initialise chunk header. + * + * \param chunk Pointer to the changeset chunk. It must be at least JOURNAL_HEADER_SIZE, perhaps more. + * \param ch Serial-to of the changeset being serialized. + */ +void journal_make_header(void *chunk, uint32_t ch_serial_to); + +/*! + * \brief Obtain serial-to of the serialized changeset. + * + * \param chunk Any chunk of a serialized changeset. + * + * \return The changeset's serial-to. + */ +uint32_t journal_next_serial(const MDB_val *chunk); + +/*! + * \brief Obtain serial-to of a changeset stored in journal. + * + * \param txn Journal DB transaction. + * \param zij True if changeset in question is zone-in-journal. + * \param serial Serial-from of the changeset in question. + * \param zone Zone name. + * \param serial_to Output: serial-to of the changeset in question. + * + * \return True if the changeset exists in the journal. + */ +bool journal_serial_to(knot_lmdb_txn_t *txn, bool zij, uint32_t serial, + const knot_dname_t *zone, uint32_t *serial_to); + +/*! \brief Return true if the changeset in question exists in the journal. */ +inline static bool journal_contains(knot_lmdb_txn_t *txn, bool zone, uint32_t serial, const knot_dname_t *zone_name) +{ + return journal_serial_to(txn, zone, serial, zone_name, NULL); +} + +/*! \brief Return true if the journal may be flushed according to conf. */ +bool journal_allow_flush(zone_journal_t j); + +/*! \brief Return configured maximal per-zone usage of journal DB. */ +size_t journal_conf_max_usage(zone_journal_t j); + +/*! \brief Return configured maximal depth of journal. */ +size_t journal_conf_max_changesets(zone_journal_t j); diff --git a/src/knot/journal/journal_metadata.c b/src/knot/journal/journal_metadata.c new file mode 100644 index 0000000..b133534 --- /dev/null +++ b/src/knot/journal/journal_metadata.c @@ -0,0 +1,422 @@ +/* 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/journal/journal_metadata.h" + +#include "libknot/endian.h" +#include "libknot/error.h" + +static void fix_endian(void *data, size_t data_size, bool in) +{ + union { + uint8_t u8; + uint16_t u16; + uint32_t u32; + uint64_t u64; + } before, after; + + memcpy(&before, data, data_size); + switch (data_size) { + case sizeof(uint8_t): + return; + case sizeof(uint16_t): + after.u16 = in ? be16toh(before.u16) : htobe16(before.u16); + break; + case sizeof(uint32_t): + after.u32 = in ? be32toh(before.u32) : htobe32(before.u32); + break; + case sizeof(uint64_t): + after.u64 = in ? be64toh(before.u64) : htobe64(before.u64); + break; + default: + assert(0); + } + memcpy(data, &after, data_size); +} + +static MDB_val metadata_key(const knot_dname_t *zone, const char *metadata) +{ + if (zone == NULL) { + return knot_lmdb_make_key("IS", (uint32_t)0, metadata); + } else { + return knot_lmdb_make_key("NIS", zone, (uint32_t)0, metadata); + } +} + +static bool del_metadata(knot_lmdb_txn_t *txn, const knot_dname_t *zone, const char *metadata) +{ + MDB_val key = metadata_key(zone, metadata); + if (key.mv_data != NULL) { + knot_lmdb_del_prefix(txn, &key); + free(key.mv_data); + } + return (key.mv_data != NULL); +} + +static bool get_metadata(knot_lmdb_txn_t *txn, const knot_dname_t *zone, const char *metadata) +{ + MDB_val key = metadata_key(zone, metadata); + bool ret = knot_lmdb_find(txn, &key, KNOT_LMDB_EXACT); // not FORCE + free(key.mv_data); + return ret; +} + +static bool get_metadata_numeric(knot_lmdb_txn_t *txn, const knot_dname_t *zone, + const char *metadata, void *result, size_t result_size) +{ + if (get_metadata(txn, zone, metadata)) { + if (txn->cur_val.mv_size == result_size) { + memcpy(result, txn->cur_val.mv_data, result_size); + fix_endian(result, result_size, true); + return true; + } else { + txn->ret = KNOT_EMALF; + } + } + return false; +} + +bool get_metadata32(knot_lmdb_txn_t *txn, const knot_dname_t *zone, + const char *metadata, uint32_t *result) +{ + return get_metadata_numeric(txn, zone, metadata, result, sizeof(*result)); +} + +bool get_metadata64(knot_lmdb_txn_t *txn, const knot_dname_t *zone, + const char *metadata, uint64_t *result) +{ + return get_metadata_numeric(txn, zone, metadata, result, sizeof(*result)); +} + +bool get_metadata64or32(knot_lmdb_txn_t *txn, const knot_dname_t *zone, + const char *metadata, uint64_t *result) +{ + if (txn->ret != KNOT_EOK) { + return false; + } + bool ret = get_metadata64(txn, zone, metadata, result); + if (txn->ret == KNOT_EMALF) { + uint32_t res32 = 0; + txn->ret = KNOT_EOK; + ret = get_metadata32(txn, zone, metadata, &res32); + *result = res32; + } + return ret; +} + +void set_metadata(knot_lmdb_txn_t *txn, const knot_dname_t *zone, const char *metadata, + const void *valp, size_t val_size, bool numeric) +{ + MDB_val key = metadata_key(zone, metadata); + MDB_val val = { val_size, NULL }; + if (knot_lmdb_insert(txn, &key, &val)) { + memcpy(val.mv_data, valp, val_size); + if (numeric) { + fix_endian(val.mv_data, val_size, false); + } + } + free(key.mv_data); +} + +static int64_t last_occupied_diff(knot_lmdb_txn_t *txn) +{ + uint64_t occupied_now = knot_lmdb_usage(txn), occupied_last = 0; + (void)get_metadata64(txn, NULL, "last_total_occupied", &occupied_last); + return (int64_t)occupied_now - (int64_t)occupied_last; +} + +void update_last_inserter(knot_lmdb_txn_t *txn, const knot_dname_t *new_inserter) +{ + uint64_t occupied_now = knot_lmdb_usage(txn), lis_occupied = 0; + int64_t occupied_diff = last_occupied_diff(txn); + knot_dname_t *last_inserter = get_metadata(txn, NULL, "last_inserter_zone") ? + knot_dname_copy(txn->cur_val.mv_data, NULL) : NULL; + if (occupied_diff == 0 || last_inserter == NULL) { + goto update_inserter; + } + (void)get_metadata64(txn, last_inserter, "occupied", &lis_occupied); + lis_occupied = MAX(0, (int64_t)lis_occupied + occupied_diff); + set_metadata(txn, last_inserter, "occupied", &lis_occupied, sizeof(lis_occupied), true); + +update_inserter: + if (new_inserter == NULL) { + del_metadata(txn, NULL, "last_inserter_zone"); + } else if (last_inserter == NULL || !knot_dname_is_equal(last_inserter, new_inserter)) { + set_metadata(txn, NULL, "last_inserter_zone", new_inserter, knot_dname_size(new_inserter), false); + } + free(last_inserter); + set_metadata(txn, NULL, "last_total_occupied", &occupied_now, sizeof(occupied_now), true); +} + +uint64_t journal_get_occupied(knot_lmdb_txn_t *txn, const knot_dname_t *zone) +{ + uint64_t res = 0; + get_metadata64(txn, zone, "occupied", &res); + return res; +} + +static int first_digit(char * of) +{ + unsigned maj, min; + return sscanf(of, "%u.%u", &maj, &min) == 2 ? maj : -1; +} + +void journal_load_metadata(knot_lmdb_txn_t *txn, const knot_dname_t *zone, journal_metadata_t *md) +{ + memset(md, 0, sizeof(*md)); + if (get_metadata(txn, NULL, "version")) { + switch (first_digit(txn->cur_val.mv_data)) { + case 3: + // TODO warning about downgrade + // FALLTHROUGH + case 1: + // still supported + // FALLTHROUGH + case 2: + // normal operation + break; + case 0: + // failed to read version + txn->ret = KNOT_ENOENT; + return; + default: + txn->ret = KNOT_ENOTSUP; + return; + } + } + md->_new_zone = !get_metadata32(txn, zone, "flags", &md->flags); + (void)get_metadata32(txn, zone, "first_serial", &md->first_serial); + (void)get_metadata32(txn, zone, "last_serial_to", &md->serial_to); + (void)get_metadata32(txn, zone, "merged_serial", &md->merged_serial); + (void)get_metadata32(txn, zone, "changeset_count", &md->changeset_count); + if (!get_metadata32(txn, zone, "flushed_upto", &md->flushed_upto)) { + // importing from version 1.0 + if ((md->flags & JOURNAL_LAST_FLUSHED_VALID)) { + uint32_t last_flushed = 0; + if (!get_metadata32(txn, zone, "last_flushed", &last_flushed) || + !journal_serial_to(txn, false, last_flushed, zone, &md->flushed_upto)) { + txn->ret = KNOT_EMALF; + } else { + md->flags &= ~JOURNAL_LAST_FLUSHED_VALID; + } + } else { + md->flushed_upto = md->first_serial; + } + } + +} + +void journal_store_metadata(knot_lmdb_txn_t *txn, const knot_dname_t *zone, const journal_metadata_t *md) +{ + set_metadata(txn, zone, "first_serial", &md->first_serial, sizeof(md->first_serial), true); + set_metadata(txn, zone, "last_serial_to", &md->serial_to, sizeof(md->serial_to), true); + set_metadata(txn, zone, "flushed_upto", &md->flushed_upto, sizeof(md->flushed_upto), true); + set_metadata(txn, zone, "merged_serial", &md->merged_serial, sizeof(md->merged_serial), true); + set_metadata(txn, zone, "changeset_count", &md->changeset_count, sizeof(md->changeset_count), true); + set_metadata(txn, zone, "flags", &md->flags, sizeof(md->flags), true); + set_metadata(txn, NULL, "version", "2.0", 4, false); + if (md->_new_zone) { + uint64_t journal_count = 0; + (void)get_metadata64or32(txn, NULL, "journal_count", &journal_count); + ++journal_count; + set_metadata(txn, NULL, "journal_count", &journal_count, sizeof(journal_count), true); + } +} + +void journal_metadata_after_delete(journal_metadata_t *md, uint32_t deleted_upto, + size_t deleted_count) +{ + if (deleted_count == 0) { + return; + } + assert((md->flags & JOURNAL_SERIAL_TO_VALID)); + if (deleted_upto == md->serial_to) { + assert(md->flushed_upto == md->serial_to); + assert(md->changeset_count == deleted_count); + md->flags &= ~JOURNAL_SERIAL_TO_VALID; + } + md->first_serial = deleted_upto; + md->changeset_count -= deleted_count; +} + +void journal_metadata_after_merge(journal_metadata_t *md, bool merged_zij, uint32_t merged_serial, + uint32_t merged_serial_to, uint32_t original_serial_to) +{ + md->flushed_upto = merged_serial_to; + if ((md->flags & JOURNAL_MERGED_SERIAL_VALID)) { + assert(!merged_zij); + assert(merged_serial == md->merged_serial); + } else if (!merged_zij) { + md->merged_serial = merged_serial; + md->flags |= JOURNAL_MERGED_SERIAL_VALID; + assert(merged_serial == md->first_serial); + journal_metadata_after_delete(md, original_serial_to, 1); // the merged changeset writes itself instead of first one + } +} + +void journal_metadata_after_insert(journal_metadata_t *md, uint32_t serial, uint32_t serial_to) +{ + if (md->first_serial == md->serial_to) { // no changesets yet + md->first_serial = serial; + md->flushed_upto = serial; + } + md->serial_to = serial_to; + md->flags |= JOURNAL_SERIAL_TO_VALID; + md->changeset_count++; +} + +void journal_metadata_after_extra(journal_metadata_t *md, uint32_t serial, uint32_t serial_to) +{ + assert(!(md->flags & JOURNAL_MERGED_SERIAL_VALID)); + md->merged_serial = serial; + md->flushed_upto = serial_to; + md->flags |= (JOURNAL_MERGED_SERIAL_VALID | JOURNAL_LAST_FLUSHED_VALID); +} + +void journal_del_zone_txn(knot_lmdb_txn_t *txn, const knot_dname_t *zone) +{ + uint64_t md_occupied = 0; + (void)get_metadata64(txn, zone, "occupied", &md_occupied); + journal_del_zone(txn, zone); + set_metadata(txn, zone, "occupied", &md_occupied, sizeof(md_occupied), true); +} + +int journal_scrape_with_md(zone_journal_t j, bool check_existence) +{ + if (check_existence && !journal_is_existing(j)) { + return KNOT_EOK; + } + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(j.db, &txn, true); + + update_last_inserter(&txn, NULL); + journal_del_zone(&txn, j.zone); + + knot_lmdb_commit(&txn); + return txn.ret; +} + +int journal_copy_with_md(knot_lmdb_db_t *from, knot_lmdb_db_t *to, const knot_dname_t *zone) +{ + knot_lmdb_txn_t tr = { 0 }, tw = { 0 }; + tr.ret = knot_lmdb_open(from); + tw.ret = knot_lmdb_open(to); + if (tr.ret != KNOT_EOK || tw.ret != KNOT_EOK) { + goto done; + } + knot_lmdb_begin(from, &tr, true); + knot_lmdb_begin(to, &tw, true); + update_last_inserter(&tr, NULL); + MDB_val prefix = journal_zone_prefix(zone); + knot_lmdb_copy_prefix(&tr, &tw, &prefix); + free(prefix.mv_data); + knot_lmdb_commit(&tw); + knot_lmdb_commit(&tr); +done: + return tr.ret == KNOT_EOK ? tw.ret : tr.ret; +} + +int journal_set_flushed(zone_journal_t j) +{ + knot_lmdb_txn_t txn = { 0 }; + journal_metadata_t md = { 0 }; + knot_lmdb_begin(j.db, &txn, true); + journal_load_metadata(&txn, j.zone, &md); + + md.flushed_upto = md.serial_to; + + journal_store_metadata(&txn, j.zone, &md); + knot_lmdb_commit(&txn); + return txn.ret; +} + +int journal_info(zone_journal_t j, bool *exists, uint32_t *first_serial, bool *has_zij, + uint32_t *serial_to, bool *has_merged, uint32_t *merged_serial, + uint64_t *occupied, uint64_t *occupied_total) +{ + if (knot_lmdb_exists(j.db) == KNOT_ENODB) { + *exists = false; + return KNOT_EOK; + } + int ret = knot_lmdb_open(j.db); + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t txn = { 0 }; + journal_metadata_t md = { 0 }; + knot_lmdb_begin(j.db, &txn, false); + journal_load_metadata(&txn, j.zone, &md); + *exists = (md.flags & JOURNAL_SERIAL_TO_VALID); + if (first_serial != NULL) { + *first_serial = md.first_serial; + } + if (has_zij != NULL) { + *has_zij = journal_contains(&txn, true, 0, j.zone); + } + if (serial_to != NULL) { + *serial_to = md.serial_to; + } + if (has_merged != NULL) { + *has_merged = (md.flags & JOURNAL_MERGED_SERIAL_VALID); + } + if (merged_serial != NULL) { + *merged_serial = md.merged_serial; + } + if (occupied != NULL) { + *occupied = 0; + get_metadata64(&txn, j.zone, "occupied", occupied); + + if (get_metadata(&txn, NULL, "last_inserter_zone") && + knot_dname_is_equal(j.zone, txn.cur_val.mv_data)) { + *occupied = MAX(0, (int64_t)*occupied + last_occupied_diff(&txn)); + } + } + if (occupied_total != NULL) { + *occupied_total = knot_lmdb_usage(&txn); + } + knot_lmdb_abort(&txn); + return txn.ret; +} + +int journals_walk(knot_lmdb_db_t *db, journals_walk_cb_t cb, void *ctx) +{ + int ret = knot_lmdb_exists(db); + if (ret == KNOT_EOK) { + ret = knot_lmdb_open(db); + } + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, false); + knot_dname_storage_t search_data = { 0 }; + MDB_val search = { 1, search_data }; + while (knot_lmdb_find(&txn, &search, KNOT_LMDB_GEQ)) { + knot_dname_t *found = txn.cur_key.mv_data; + uint32_t unused_flags; + if (get_metadata32(&txn, found, "flags", &unused_flags)) { + // matched journal DB key appears to be a zone name + txn.ret = cb(found, ctx); + } + + // update searched key to next after found zone + search.mv_size = knot_dname_size(found); + memcpy(search.mv_data, found, search.mv_size); + ((uint8_t *)search.mv_data)[search.mv_size - 1]++; + } + knot_lmdb_abort(&txn); + return txn.ret; +} diff --git a/src/knot/journal/journal_metadata.h b/src/knot/journal/journal_metadata.h new file mode 100644 index 0000000..246d899 --- /dev/null +++ b/src/knot/journal/journal_metadata.h @@ -0,0 +1,187 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/journal/journal_basic.h" + +typedef struct { + uint32_t first_serial; + uint32_t serial_to; + uint32_t flushed_upto; + uint32_t merged_serial; + uint32_t changeset_count; + uint32_t flags; // a bitmap of flags, see enum below + bool _new_zone; // private: if there were no metadata at all previously +} journal_metadata_t; + +enum journal_metadata_flags { + JOURNAL_LAST_FLUSHED_VALID = (1 << 0), // deprecated + JOURNAL_SERIAL_TO_VALID = (1 << 1), + JOURNAL_MERGED_SERIAL_VALID = (1 << 2), +}; + +typedef int (*journals_walk_cb_t)(const knot_dname_t *zone, void *ctx); + +/*! + * \brief Update the computation of DB resources used by each zone. + * + * Because the amount of used space is bigger than sum of changesets' serialized_sizes, + * journal uses a complicated way to compute each zone's used space: there is a metadata + * showing always the previously-inserting zone. Before the next insert, it is computed + * how the total usage of the DB changed during the previous insert (or delete), and the + * usage increase (or decrease) is accounted on the bill of the previous inserter. + * + * \param txn Journal DB transaction. + * \param new_inserter Name of the zone that is going to insert now. Might be NULL if no insert nor delete will be done. + */ +void update_last_inserter(knot_lmdb_txn_t *txn, const knot_dname_t *new_inserter); + +/* \brief Return the journal database usage by given zone. */ +uint64_t journal_get_occupied(knot_lmdb_txn_t *txn, const knot_dname_t *zone); + +/*! + * \brief Load the metadata from DB into structure. + * + * \param txn Journal DB transaction. + * \param zone Zone name. + * \param md Output: metadata structure. + */ +void journal_load_metadata(knot_lmdb_txn_t *txn, const knot_dname_t *zone, journal_metadata_t *md); + +/*! + * \brief Store the metadata from structure into DB. + * + * \param txn Journal DB transaction. + * \param zone Zone name. + * \param md Metadata structure. + */ +void journal_store_metadata(knot_lmdb_txn_t *txn, const knot_dname_t *zone, const journal_metadata_t *md); + +/*! + * \brief Update metadata according to what was deleted. + * + * \param md Metadata structure to be updated. + * \param deleted_upto Serial-to of the last deleted changeset. + * \param deleted_count Number of deleted changesets. + */ +void journal_metadata_after_delete(journal_metadata_t *md, uint32_t deleted_upto, + size_t deleted_count); + +/*! + * \brief Update metadata according to what was merged. + * + * \param md Metadata structure to be updated. + * \param merged_zij True if it was a merge into zone-in-journal. + * \param merged_serial Serial-from of the merged changeset (ignored if 'merged_zij'). + * \param merged_serial_to Serial-to of the merged changeset. + * \param original_serial_to Previous serial-to of the merged changeset before the merge. + */ +void journal_metadata_after_merge(journal_metadata_t *md, bool merged_zij, uint32_t merged_serial, + uint32_t merged_serial_to, uint32_t original_serial_to); + +/*! + * \brief Update metadata according to what was inserted. + * + * \param md Metadata structure to be updated. + * \param serial Serial-from of the inserted changeset. + * \param serial_to Serial-to of the inserted changeset. + */ +void journal_metadata_after_insert(journal_metadata_t *md, uint32_t serial, uint32_t serial_to); + +/*! + * \brief Update metadata according to inserted extra changeset. + * + * \param md Metadata structure to be updated. + * \param serial Serial-from of the inserted changeset. + * \param serial_to Serial-to of the inserted changeset. + */ +void journal_metadata_after_extra(journal_metadata_t *md, uint32_t serial, uint32_t serial_to); + +/*! + * \brief Delete all zone records in a txn that will later write to the same zone. + * + * \note The difference against journal_del_zone(), which purges even metadata, incl "occupied". + * \note This preserves keeping track of space occupied/freed by this zone. + */ +void journal_del_zone_txn(knot_lmdb_txn_t *txn, const knot_dname_t *zone); + +/*! + * \brief Completely delete all journal records belonging to this zone, including metadata. + * + * \param j Journal to be scraped. + * \param check_existence Don't operate if the journal seems not to exist. + * + * \return KNOT_E* + */ +int journal_scrape_with_md(zone_journal_t j, bool check_existence); + +/*! + * \brief Copy all records related to this zone from one journal DB to another. + * + * \param from DB to copy from. + * \param to DB to copy to. + * \param zone Journal zone. + * + * \return KNOT_E* + */ +int journal_copy_with_md(knot_lmdb_db_t *from, knot_lmdb_db_t *to, const knot_dname_t *zone); + +/*! + * \brief Update the metadata stored in journal DB after a zone flush. + * + * \param j Journal to be notified about flush. + * + * \return KNOT_E* + */ +int journal_set_flushed(zone_journal_t j); + +/*! + * \brief Obtain information about the zone's journal from the DB (mostly metadata). + * + * \param j Zone journal. + * \param exists Output: bool if the zone exists in the journal. + * \param first_serial Optional output: serial-from of the first changeset in journal. + * \param has_zij Optional output: bool if there is zone-in-journal. + * \param serial_to Optional output: serial.to of the last changeset in journal. + * \param has_merged Optional output: bool if there is a special (non zone-in-journal) merged changeset. + * \param merged_serial Optional output: serial-from of the merged changeset. + * \param occupied Optional output: DB space occupied by this zones. + * \param occupied_total Optional output: DB space occupied in total by all zones. + * + * \return KNOT_E* + */ +int journal_info(zone_journal_t j, bool *exists, uint32_t *first_serial, bool *has_zij, + uint32_t *serial_to, bool *has_merged, uint32_t *merged_serial, + uint64_t *occupied, uint64_t *occupied_total); + +/*! \brief Return true if this zone exists in journal DB. */ +inline static bool journal_is_existing(zone_journal_t j) { + bool ex = false; + (void)journal_info(j, &ex, NULL, NULL, NULL, NULL, NULL, NULL, NULL); + return ex; +} + +/*! + * \brief Call a function for each zone being in the journal DB. + * + * \param db Journal database. + * \param cb Callback to be called for each zone-name found. + * \param ctx Arbitrary context to be passed to the callback. + * + * \return An error code from either journal operations or from the callback. + */ +int journals_walk(knot_lmdb_db_t *db, journals_walk_cb_t cb, void *ctx); diff --git a/src/knot/journal/journal_read.c b/src/knot/journal/journal_read.c new file mode 100644 index 0000000..6c4fc32 --- /dev/null +++ b/src/knot/journal/journal_read.c @@ -0,0 +1,436 @@ +/* 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/journal/journal_read.h" + +#include "knot/journal/journal_metadata.h" +#include "knot/journal/knot_lmdb.h" + +#include "contrib/macros.h" +#include "contrib/ucw/lists.h" +#include "contrib/wire_ctx.h" +#include "libknot/error.h" + +#include <stdlib.h> + +struct journal_read { + knot_lmdb_txn_t txn; + MDB_val key_prefix; + const knot_dname_t *zone; + wire_ctx_t wire; + uint32_t next; +}; + +int journal_read_get_error(const journal_read_t *ctx, int another_error) +{ + return (ctx == NULL || ctx->txn.ret == KNOT_EOK ? another_error : ctx->txn.ret); +} + +static void update_ctx_wire(journal_read_t *ctx) +{ + ctx->wire = wire_ctx_init_const(ctx->txn.cur_val.mv_data, ctx->txn.cur_val.mv_size); + wire_ctx_skip(&ctx->wire, JOURNAL_HEADER_SIZE); +} + +static bool go_next_changeset(journal_read_t *ctx, bool go_zone, const knot_dname_t *zone) +{ + free(ctx->key_prefix.mv_data); + ctx->key_prefix = journal_changeset_id_to_key(go_zone, ctx->next, zone); + if (!knot_lmdb_find_prefix(&ctx->txn, &ctx->key_prefix)) { + return false; + } + if (!go_zone && ctx->next == journal_next_serial(&ctx->txn.cur_val)) { + ctx->txn.ret = KNOT_ELOOP; + return false; + } + ctx->next = journal_next_serial(&ctx->txn.cur_val); + update_ctx_wire(ctx); + return true; +} + +int journal_read_begin(zone_journal_t j, bool read_zone, uint32_t serial_from, journal_read_t **ctx) +{ + *ctx = NULL; + if (!journal_is_existing(j)) { // this also opens the LMDB if not already + return KNOT_ENOENT; + } + + journal_read_t *newctx = calloc(1, sizeof(*newctx)); + if (newctx == NULL) { + return KNOT_ENOMEM; + } + + newctx->zone = j.zone; + newctx->next = serial_from; + + knot_lmdb_begin(j.db, &newctx->txn, false); + + if (go_next_changeset(newctx, read_zone, j.zone)) { + *ctx = newctx; + return KNOT_EOK; + } else { + journal_read_end(newctx); + return KNOT_ENOENT; + } +} + +void journal_read_end(journal_read_t *ctx) +{ + if (ctx != NULL) { + free(ctx->key_prefix.mv_data); + knot_lmdb_abort(&ctx->txn); + free(ctx); + } +} + +static bool make_data_available(journal_read_t *ctx) +{ + if (wire_ctx_available(&ctx->wire) == 0) { + if (!knot_lmdb_next(&ctx->txn)) { + return false; + } + if (!knot_lmdb_is_prefix_of(&ctx->key_prefix, &ctx->txn.cur_key)) { + return false; + } + if (ctx->next != journal_next_serial(&ctx->txn.cur_val)) { + // consistency check, see also MR !1270 + ctx->txn.ret = KNOT_EMALF; + return false; + } + update_ctx_wire(ctx); + } + return true; +} + +// thoughts for next design of journal serialization: +// - one TTL per rrset +// - endian +// - optionally storing whole rdataset at once? + +bool journal_read_rrset(journal_read_t *ctx, knot_rrset_t *rrset, bool allow_next_changeset) +{ + //knot_rdataset_clear(&rrset->rrs, NULL); + //memset(rrset, 0, sizeof(*rrset)); + if (!make_data_available(ctx)) { + if (!allow_next_changeset || !go_next_changeset(ctx, false, ctx->zone)) { + return false; + } + } + rrset->owner = knot_dname_copy(ctx->wire.position, NULL); + wire_ctx_skip(&ctx->wire, knot_dname_size(rrset->owner)); + rrset->type = wire_ctx_read_u16(&ctx->wire); + rrset->rclass = wire_ctx_read_u16(&ctx->wire); + uint16_t rrs_count = wire_ctx_read_u16(&ctx->wire); + for (int i = 0; i < rrs_count && ctx->wire.error == KNOT_EOK; i++) { + if (!make_data_available(ctx)) { + ctx->wire.error = KNOT_EFEWDATA; + } + // TODO think of how to export serialized rr directly to knot_rdataset_add + // focus on: even address aligning + uint32_t ttl = wire_ctx_read_u32(&ctx->wire); + if (i == 0) { + rrset->ttl = ttl; + } + uint16_t len = wire_ctx_read_u16(&ctx->wire); + if (ctx->wire.error == KNOT_EOK) { + ctx->wire.error = knot_rrset_add_rdata(rrset, ctx->wire.position, len, NULL); + } + wire_ctx_skip(&ctx->wire, len); + } + if (ctx->txn.ret == KNOT_EOK) { + ctx->txn.ret = ctx->wire.error == KNOT_ERANGE ? KNOT_EMALF : ctx->wire.error; + } + if (ctx->txn.ret == KNOT_EOK) { + return true; + } else { + journal_read_clear_rrset(rrset); + return false; + } +} + +void journal_read_clear_rrset(knot_rrset_t *rr) +{ + knot_rrset_clear(rr, NULL); +} + +int journal_read_rrsets(journal_read_t *read, journal_read_cb_t cb, void *ctx) +{ + knot_rrset_t rr = { 0 }; + bool in_remove_section = false; + int ret = KNOT_EOK; + while (ret == KNOT_EOK && journal_read_rrset(read, &rr, true)) { + if (rr_is_apex_soa(&rr, read->zone)) { + in_remove_section = !in_remove_section; + } + ret = cb(in_remove_section, &rr, ctx); + journal_read_clear_rrset(&rr); + } + ret = journal_read_get_error(read, ret); + journal_read_end(read); + return ret; +} + +static int add_rr_to_contents(zone_contents_t *z, const knot_rrset_t *rrset) +{ + zone_node_t *n = NULL; + return zone_contents_add_rr(z, rrset, &n); + // Shall we ignore ETTL ? +} + +bool journal_read_changeset(journal_read_t *ctx, changeset_t *ch) +{ + zone_contents_t *tree = zone_contents_new(ctx->zone, false); + knot_rrset_t *soa = calloc(1, sizeof(*soa)), rr = { 0 }; + if (tree == NULL || soa == NULL) { + ctx->txn.ret = KNOT_ENOMEM; + goto fail; + } + memset(ch, 0, sizeof(*ch)); + + if (!journal_read_rrset(ctx, soa, true)) { + goto fail; + } + while (journal_read_rrset(ctx, &rr, false)) { + if (rr_is_apex_soa(&rr, ctx->zone)) { + if (ch->soa_from != NULL) { + ctx->txn.ret = KNOT_EMALF; + goto fail; + } + ch->soa_from = soa; + ch->remove = tree; + soa = malloc(sizeof(*soa)); + tree = zone_contents_new(ctx->zone, false); + if (tree == NULL || soa == NULL) { + ctx->txn.ret = KNOT_ENOMEM; + goto fail; + } + *soa = rr; // note this tricky assignment + memset(&rr, 0, sizeof(rr)); + } else { + ctx->txn.ret = add_rr_to_contents(tree, &rr); + journal_read_clear_rrset(&rr); + } + } + + if (ctx->txn.ret == KNOT_EOK) { + ch->soa_to = soa; + ch->add = tree; + return true; + } else { +fail: + journal_read_clear_rrset(&rr); + journal_read_clear_rrset(soa); + free(soa); + changeset_clear(ch); + zone_contents_deep_free(tree); + return false; + } +} + +void journal_read_clear_changeset(changeset_t *ch) +{ + changeset_clear(ch); + memset(ch, 0, sizeof(*ch)); +} + +static int just_load_md(zone_journal_t j, journal_metadata_t *md, bool *has_zij) +{ + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(j.db, &txn, false); + journal_load_metadata(&txn, j.zone, md); + if (has_zij != NULL) { + *has_zij = journal_contains(&txn, true, 0, j.zone); + } + knot_lmdb_abort(&txn); + return txn.ret; +} + +int journal_walk_from(zone_journal_t j, uint32_t from, + journal_walk_cb_t cb, void *ctx) +{ + bool at_least_one = false; + journal_metadata_t md = { 0 }; + journal_read_t *read = NULL; + changeset_t ch; + + int ret = just_load_md(j, &md, NULL); + if (ret != KNOT_EOK) { + return ret; + } + + if ((md.flags & JOURNAL_SERIAL_TO_VALID) && from != md.serial_to && + ret == KNOT_EOK) { + ret = journal_read_begin(j, false, from, &read); + while (ret == KNOT_EOK && journal_read_changeset(read, &ch)) { + ret = cb(false, &ch, ctx); + at_least_one = true; + journal_read_clear_changeset(&ch); + } + ret = journal_read_get_error(read, ret); + journal_read_end(read); + } + if (!at_least_one && ret == KNOT_EOK) { + ret = cb(false, NULL, ctx); + } + return ret; +} + +// beware, this function does not operate in single txn! +int journal_walk(zone_journal_t j, journal_walk_cb_t cb, void *ctx) +{ + int ret = knot_lmdb_exists(j.db); + if (ret == KNOT_ENODB) { + ret = cb(true, NULL, ctx); + if (ret == KNOT_EOK) { + ret = cb(false, NULL, ctx); + } + return ret; + } else if (ret == KNOT_EOK) { + ret = knot_lmdb_open(j.db); + } + if (ret != KNOT_EOK) { + return ret; + } + journal_metadata_t md = { 0 }; + journal_read_t *read = NULL; + changeset_t ch; + bool zone_in_j = false; + ret = just_load_md(j, &md, &zone_in_j); + if (ret != KNOT_EOK) { + return ret; + } + if (zone_in_j) { + ret = journal_read_begin(j, true, 0, &read); + goto read_one_special; + } else if ((md.flags & JOURNAL_MERGED_SERIAL_VALID)) { + ret = journal_read_begin(j, false, md.merged_serial, &read); +read_one_special: + if (ret == KNOT_EOK && journal_read_changeset(read, &ch)) { + ret = cb(true, &ch, ctx); + journal_read_clear_changeset(&ch); + } + ret = journal_read_get_error(read, ret); + journal_read_end(read); + read = NULL; + } else { + ret = cb(true, NULL, ctx); + } + + if (ret == KNOT_EOK) { + ret = journal_walk_from(j, md.first_serial, cb, ctx); + } + return ret; +} + +typedef struct { + size_t observed_count; + size_t observed_merged; + uint32_t merged_serial; + size_t observed_zij; + uint32_t first_serial; + bool first_serial_valid; + uint32_t last_serial; + bool last_serial_valid; +} check_ctx_t; + +static int check_cb(bool special, const changeset_t *ch, void *vctx) +{ + check_ctx_t *ctx = vctx; + if (special && ch != NULL) { + if (ch->remove == NULL) { + ctx->observed_zij++; + ctx->last_serial = changeset_to(ch); + ctx->last_serial_valid = true; + } else { + ctx->merged_serial = changeset_from(ch); + ctx->observed_merged++; + } + } else if (ch != NULL) { + if (!ctx->first_serial_valid) { + ctx->first_serial = changeset_from(ch); + ctx->first_serial_valid = true; + } + ctx->last_serial = changeset_to(ch); + ctx->last_serial_valid = true; + ctx->observed_count++; + } + return KNOT_EOK; +} + +static bool eq(bool a, bool b) +{ + return a ? b : !b; +} + +int journal_sem_check(zone_journal_t j) +{ + check_ctx_t ctx = { 0 }; + journal_metadata_t md = { 0 }; + bool has_zij = false; + + if (!journal_is_existing(j)) { + return KNOT_EOK; + } + + int ret = just_load_md(j, &md, &has_zij); + if (ret == KNOT_EOK) { + ret = journal_walk(j, check_cb, &ctx); + } + if (ret != KNOT_EOK) { + return ret; + } + + if (!eq((md.flags & JOURNAL_SERIAL_TO_VALID), ctx.last_serial_valid)) { + return 101; + } + if (ctx.last_serial_valid && ctx.last_serial != md.serial_to) { + return 102; + } + if (!eq((md.flags & JOURNAL_MERGED_SERIAL_VALID), (ctx.observed_merged > 0))) { + return 103; + } + if (ctx.observed_merged > 1) { + return 104; + } + if (ctx.observed_merged == 1 && ctx.merged_serial != md.merged_serial) { + return 105; + } + if (!eq(has_zij, (ctx.observed_zij > 0))) { + return 106; + } + if (ctx.observed_zij > 1) { + return 107; + } + if (ctx.observed_zij + ctx.observed_merged > 1) { + return 108; + } + if (!eq(((md.flags & JOURNAL_SERIAL_TO_VALID) && md.first_serial != md.serial_to), ctx.first_serial_valid)) { + return 109; + } + if (!eq(ctx.first_serial_valid, (ctx.observed_count > 0))) { + return 110; + } + if (ctx.first_serial_valid && ctx.first_serial != md.first_serial) { + return 111; + } + if (ctx.observed_count != md.changeset_count) { + return 112; + } + if (ctx.observed_merged > 0 && ctx.observed_count == 0) { + return 113; + } + return KNOT_EOK; +} diff --git a/src/knot/journal/journal_read.h b/src/knot/journal/journal_read.h new file mode 100644 index 0000000..92cad9f --- /dev/null +++ b/src/knot/journal/journal_read.h @@ -0,0 +1,158 @@ +/* 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 "knot/journal/journal_basic.h" + +typedef struct journal_read journal_read_t; + +typedef int (*journal_read_cb_t)(bool in_remove_section, const knot_rrset_t *rr, void *ctx); + +typedef int (*journal_walk_cb_t)(bool special, const changeset_t *ch, void *ctx); + +/*! + * \brief Start reading journal from specified changeset. + * + * \param j Journal to be read. + * \param read_zone True if reading shall start with zone-in-journal. + * \param serial_from Serial-from of the changeset to be started at (ignored if 'read_zone'). + * \param ctx Output: journal reading context initialised. + * + * \return KNOT_E* + */ +int journal_read_begin(zone_journal_t j, bool read_zone, uint32_t serial_from, journal_read_t **ctx); + +/*! + * \brief Read a single RRSet from a journal changeset. + * + * \param ctx Journal reading context. + * \param rr Output: RRSet to be filled with serialized data. + * \param allow_next_changeset True to allow jumping to next changeset. + * + * \return False if no more RRSet in this changeset/journal, or failure. + */ +bool journal_read_rrset(journal_read_t *ctx, knot_rrset_t *rr, bool allow_next_changeset); + +/*! + * \brief Free up heap allocations by journal_read_rrset(). + * + * \param rr RRSet initialised by journal_read_rrset(). + */ +void journal_read_clear_rrset(knot_rrset_t *rr); + +// TODO move somewhere. Libknot? +inline static bool rr_is_apex_soa(const knot_rrset_t *rr, const knot_dname_t *apex) +{ + return (rr->type == KNOT_RRTYPE_SOA && knot_dname_is_equal(rr->owner, apex)); +} + +/*! + * \brief Read all RRSets up to the end of journal, calling a function for each. + * + * \note Closes reading context at the end. + * + * \param read Journal reading context. + * \param cb Callback to be called on each read. + * \param ctx Arbitrary context to be passed to the callback. + * + * \return An error code from either journal operations or from the callback. + */ +int journal_read_rrsets(journal_read_t *read, journal_read_cb_t cb, void *ctx); + +/*! + * \brief Read a single changeset from journal. + * + * \param ctx Journal reading context. + * \param ch Output: changeset to be filled with serialized data. + * + * \return False if no more changesets in the journal, or failure. + */ +bool journal_read_changeset(journal_read_t *ctx, changeset_t *ch); + +/*! + * \brief Free up heap allocations by journal_read_changeset(). + * + * \param ch Changeset initialised by journal_read_changeset(). + */ +void journal_read_clear_changeset(changeset_t *ch); + +/*! + * \brief Obtain error code from the journal_read operations previously performed. + * + * \param ctx Journal reading context. + * \param another_error An error code from outside the reading operations to be combined. + * + * \return KNOT_EOK if completely every operation succeeded, KNOT_E* + */ +int journal_read_get_error(const journal_read_t *ctx, int another_error); + +/*! + * \brief Finalise journal reading. + * + * \param ctx Journal reading context (will be freed). + */ +void journal_read_end(journal_read_t *ctx); + +/*! + * \brief Call a function for each changeset in journal. + * + * This is a variant of journal_walk() see below. + * The difference is that iteration starts at specified serial. + * Similarly to how IXFR works. + * The callback is called for each found changeset, or just once + * with ch=NULL if none is found. + * + * \param j Zone journal to be read. + * \param from SOA serial to start at. + * \param cb Callback to be called for each changeset (or its non-existence). + * \param ctx Arbitrary context to be passed to the callback. + * + * \return An error code from either journal operations or from the callback. + * \retval KNOT_ENOENT if the journal is not empty, but the requested serial not present. + */ +int journal_walk_from(zone_journal_t j, uint32_t from, + journal_walk_cb_t cb, void *ctx); + +/*! + * \brief Call a function for each changeset stored in journal. + * + * First, the callback will be called for the special changeset - + * either zone-in-journal or merged changeset, with special=true. + * If there is no such, it will be called anyway with ch=NULL. + * + * Than, the callback will be called for each regular changeset + * with special=false. If there is none, it will be called once + * with ch=NULL. + * + * \param j Zone journal to be read. + * \param cb Callback to be called for each changeset (or its non-existence). + * \param ctx Arbitrary context to be passed to the callback. + * + * \return An error code from either journal operations or from the callback. + */ +int journal_walk(zone_journal_t j, journal_walk_cb_t cb, void *ctx); + +/*! + * \brief Perform semantic check of the zone journal (consistency, metadata...). + * + * \param j Zone journal to be checked. + * + * \retval KNOT_E* ( < 0 ) if an error during journal operation. + * \retval > 100 if some inconsistency found. + * \return KNOT_EOK of all ok. + */ +int journal_sem_check(zone_journal_t j); diff --git a/src/knot/journal/journal_write.c b/src/knot/journal/journal_write.c new file mode 100644 index 0000000..ad1247b --- /dev/null +++ b/src/knot/journal/journal_write.c @@ -0,0 +1,333 @@ +/* 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/journal/journal_write.h" + +#include "contrib/macros.h" +#include "knot/journal/journal_metadata.h" +#include "knot/journal/journal_read.h" +#include "knot/journal/serialization.h" +#include "libknot/error.h" + +static void journal_write_serialize(knot_lmdb_txn_t *txn, serialize_ctx_t *ser, + const knot_dname_t *apex, bool zij, uint32_t ch_from, uint32_t ch_to) +{ + MDB_val chunk; + uint32_t i = 0; + while (serialize_unfinished(ser) && txn->ret == KNOT_EOK) { + serialize_prepare(ser, JOURNAL_CHUNK_THRESH - JOURNAL_HEADER_SIZE, + JOURNAL_CHUNK_MAX - JOURNAL_HEADER_SIZE, &chunk.mv_size); + if (chunk.mv_size == 0) { + break; // beware! If this is omitted, it creates empty chunk => EMALF when reading. + } + chunk.mv_size += JOURNAL_HEADER_SIZE; + chunk.mv_data = NULL; + MDB_val key = journal_make_chunk_key(apex, ch_from, zij, i); + if (knot_lmdb_insert(txn, &key, &chunk)) { + journal_make_header(chunk.mv_data, ch_to); + serialize_chunk(ser, chunk.mv_data + JOURNAL_HEADER_SIZE, chunk.mv_size - JOURNAL_HEADER_SIZE); + } + free(key.mv_data); + i++; + } + int ret = serialize_deinit(ser); + if (txn->ret == KNOT_EOK) { + txn->ret = ret; + } +} + +void journal_write_changeset(knot_lmdb_txn_t *txn, const changeset_t *ch) +{ + serialize_ctx_t *ser = serialize_init(ch); + if (ser == NULL) { + txn->ret = KNOT_ENOMEM; + return; + } + if (ch->remove == NULL) { + journal_write_serialize(txn, ser, ch->soa_to->owner, true, 0, changeset_to(ch)); + } else { + journal_write_serialize(txn, ser, ch->soa_to->owner, false, changeset_from(ch), changeset_to(ch)); + } +} + +void journal_write_zone(knot_lmdb_txn_t *txn, const zone_contents_t *z) +{ + serialize_ctx_t *ser = serialize_zone_init(z); + if (ser == NULL) { + txn->ret = KNOT_ENOMEM; + return; + } + journal_write_serialize(txn, ser, z->apex->owner, true, 0, zone_contents_serial(z)); +} + +void journal_write_zone_diff(knot_lmdb_txn_t *txn, const zone_diff_t *z) +{ + serialize_ctx_t *ser = serialize_zone_diff_init(z); + if (ser == NULL) { + txn->ret = KNOT_ENOMEM; + return; + } + journal_write_serialize(txn, ser, z->apex->owner, false, zone_diff_from(z), zone_diff_to(z)); +} + +static bool delete_one(knot_lmdb_txn_t *txn, bool del_zij, uint32_t del_serial, + const knot_dname_t *zone, uint64_t *freed, uint32_t *next_serial) +{ + *freed = 0; + MDB_val prefix = journal_changeset_id_to_key(del_zij, del_serial, zone); + knot_lmdb_foreach(txn, &prefix) { + *freed += txn->cur_val.mv_size; + *next_serial = journal_next_serial(&txn->cur_val); + knot_lmdb_del_cur(txn); + } + free(prefix.mv_data); + return (*freed > 0); +} + +static int merge_cb(bool remove, const knot_rrset_t *rr, void *ctx) +{ + changeset_t *ch = ctx; + return remove ? (rr_is_apex_soa(rr, ch->soa_to->owner) ? + KNOT_EOK : changeset_add_removal(ch, rr, CHANGESET_CHECK)) + : changeset_add_addition(ch, rr, CHANGESET_CHECK); +} + +void journal_merge(zone_journal_t j, knot_lmdb_txn_t *txn, bool merge_zij, + uint32_t merge_serial, uint32_t *original_serial_to) +{ + changeset_t merge; + memset(&merge, 0, sizeof(merge)); + journal_read_t *read = NULL; + txn->ret = journal_read_begin(j, merge_zij, merge_serial, &read); + if (txn->ret != KNOT_EOK) { + return; + } + if (journal_read_changeset(read, &merge)) { + *original_serial_to = changeset_to(&merge); + } + txn->ret = journal_read_rrsets(read, merge_cb, &merge); + + // deleting seems redundant since the merge changeset will be overwritten + // but it would cause EMALF or invalid data if the new merged has less chunks than before + uint32_t del_next_serial; + uint64_t del_freed; + delete_one(txn, merge_zij, merge_serial, j.zone, &del_freed, &del_next_serial); + assert(del_freed > 0 && del_next_serial == *original_serial_to); + + journal_write_changeset(txn, &merge); + journal_read_clear_changeset(&merge); +} + +static void delete_merged(knot_lmdb_txn_t *txn, const knot_dname_t *zone, + journal_metadata_t *md, uint64_t *freed) +{ + if (!(md->flags & JOURNAL_MERGED_SERIAL_VALID)) { + return; + } + uint32_t unused = 0; + delete_one(txn, false, md->merged_serial, zone, freed, &unused); + md->merged_serial = 0; + md->flags &= ~JOURNAL_MERGED_SERIAL_VALID; +} + +bool journal_delete(knot_lmdb_txn_t *txn, uint32_t from, const knot_dname_t *zone, + uint64_t tofree_size, size_t tofree_count, uint32_t stop_at_serial, + uint64_t *freed_size, size_t *freed_count, uint32_t *stopped_at) +{ + *freed_size = 0; + *freed_count = 0; + uint64_t freed_now; + while (from != stop_at_serial && + (*freed_size < tofree_size || *freed_count < tofree_count) && + delete_one(txn, false, from, zone, &freed_now, stopped_at)) { + *freed_size += freed_now; + ++(*freed_count); + from = *stopped_at; + } + return (*freed_count > 0); +} + +void journal_try_flush(zone_journal_t j, knot_lmdb_txn_t *txn, journal_metadata_t *md) +{ + bool flush = journal_allow_flush(j); + uint32_t merge_orig = 0; + if (journal_contains(txn, true, 0, j.zone)) { + journal_merge(j, txn, true, 0, &merge_orig); + if (!flush) { + journal_metadata_after_merge(md, true, 0, md->serial_to, merge_orig); + } + } else if (!flush) { + uint32_t merge_serial = ((md->flags & JOURNAL_MERGED_SERIAL_VALID) ? md->merged_serial : md->first_serial); + journal_merge(j, txn, false, merge_serial, &merge_orig); + journal_metadata_after_merge(md, false, merge_serial, md->serial_to, merge_orig); + } + + if (flush) { + // delete merged serial if (very unlikely) exists + if ((md->flags & JOURNAL_MERGED_SERIAL_VALID)) { + uint64_t unused64; + uint32_t unused32; + (void)delete_one(txn, false, md->merged_serial, j.zone, &unused64, &unused32); + md->flags &= ~JOURNAL_MERGED_SERIAL_VALID; + } + + // commit partial job and ask zone to flush itself + journal_store_metadata(txn, j.zone, md); + knot_lmdb_commit(txn); + if (txn->ret == KNOT_EOK) { + txn->ret = KNOT_EBUSY; + } + } +} + +#define U_MINUS(minuend, subtrahend) ((minuend) - MIN((minuend), (subtrahend))) + +void journal_fix_occupation(zone_journal_t j, knot_lmdb_txn_t *txn, journal_metadata_t *md, + int64_t max_usage, ssize_t max_count) +{ + uint64_t occupied = journal_get_occupied(txn, j.zone), freed; + uint64_t need_tofree = U_MINUS(occupied, max_usage); + size_t count = md->changeset_count, removed; + size_t need_todel = U_MINUS(count, max_count); + + while ((need_tofree > 0 || need_todel > 0) && txn->ret == KNOT_EOK) { + uint32_t del_from = md->first_serial; // don't move this line outside of the loop + uint32_t del_upto = md->flushed_upto; + (void)journal_serial_to(txn, true, 0, j.zone, &del_upto); // in case zij present and wrong flushed_upto, avoid discontinuity + freed = 0; + removed = 0; + journal_delete(txn, del_from, j.zone, need_tofree, need_todel, + del_upto, &freed, &removed, &del_from); + if (freed == 0) { + if (del_upto != md->serial_to) { + journal_try_flush(j, txn, md); + } else { + txn->ret = KNOT_ESPACE; + break; + } + } else { + journal_metadata_after_delete(md, del_from, removed); + need_tofree = U_MINUS(need_tofree, freed); + need_todel = U_MINUS(need_todel, removed); + } + } +} + +int journal_insert_zone(zone_journal_t j, const zone_contents_t *z) +{ + changeset_t fake_ch = { .add = (zone_contents_t *)z }; + size_t ch_size = changeset_serialized_size(&fake_ch); + size_t max_usage = journal_conf_max_usage(j); + if (ch_size >= max_usage) { + return KNOT_ESPACE; + } + int ret = knot_lmdb_open(j.db); + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(j.db, &txn, true); + + update_last_inserter(&txn, j.zone); + journal_del_zone_txn(&txn, j.zone); + + journal_write_zone(&txn, z); + + journal_metadata_t md = { 0 }; + md.flags = JOURNAL_SERIAL_TO_VALID; + md.serial_to = zone_contents_serial(z); + md.first_serial = md.serial_to; + journal_store_metadata(&txn, j.zone, &md); + + knot_lmdb_commit(&txn); + return txn.ret; +} + +int journal_insert(zone_journal_t j, const changeset_t *ch, const changeset_t *extra, + const zone_diff_t *zdiff) +{ + assert(zdiff == NULL || (ch == NULL && extra == NULL)); + + size_t ch_size = zdiff == NULL ? changeset_serialized_size(ch) : + zone_diff_serialized_size(*zdiff); + size_t max_usage = journal_conf_max_usage(j); + if (ch_size >= max_usage) { + return KNOT_ESPACE; + } + + uint32_t ch_from = zdiff == NULL ? changeset_from(ch) : zone_diff_from(zdiff); + uint32_t ch_to = zdiff == NULL ? changeset_to(ch) : zone_diff_to(zdiff); + if (extra != NULL && (changeset_to(extra) != ch_to || + changeset_from(extra) == ch_from)) { + return KNOT_EINVAL; + } + int ret = knot_lmdb_open(j.db); + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t txn = { 0 }; + journal_metadata_t md = { 0 }; + knot_lmdb_begin(j.db, &txn, true); + journal_load_metadata(&txn, j.zone, &md); + + update_last_inserter(&txn, j.zone); + + if (extra != NULL) { + if (journal_contains(&txn, true, 0, j.zone)) { + txn.ret = KNOT_ESEMCHECK; + } + uint64_t merged_freed = 0; + delete_merged(&txn, j.zone, &md, &merged_freed); + ch_size += changeset_serialized_size(extra); + ch_size -= merged_freed; + md.flushed_upto = md.serial_to; // set temporarily + md.flags |= JOURNAL_LAST_FLUSHED_VALID; + } + + size_t chs_limit = journal_conf_max_changesets(j); + journal_fix_occupation(j, &txn, &md, max_usage - ch_size, chs_limit - 1); + + // avoid discontinuity + if ((md.flags & JOURNAL_SERIAL_TO_VALID) && md.serial_to != ch_from) { + if (journal_contains(&txn, true, 0, j.zone)) { + txn.ret = KNOT_ESEMCHECK; + } else { + journal_del_zone_txn(&txn, j.zone); + memset(&md, 0, sizeof(md)); + } + } + + // avoid cycle + if (journal_contains(&txn, false, ch_to, j.zone)) { + journal_fix_occupation(j, &txn, &md, INT64_MAX, 1); + } + + if (zdiff == NULL) { + journal_write_changeset(&txn, ch); + } else { + journal_write_zone_diff(&txn, zdiff); + } + journal_metadata_after_insert(&md, ch_from, ch_to); + + if (extra != NULL) { + journal_write_changeset(&txn, extra); + journal_metadata_after_extra(&md, changeset_from(extra), changeset_to(extra)); + } + + journal_store_metadata(&txn, j.zone, &md); + knot_lmdb_commit(&txn); + return txn.ret; +} diff --git a/src/knot/journal/journal_write.h b/src/knot/journal/journal_write.h new file mode 100644 index 0000000..a55fd34 --- /dev/null +++ b/src/knot/journal/journal_write.h @@ -0,0 +1,121 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/journal/journal_basic.h" +#include "knot/journal/journal_metadata.h" +#include "knot/journal/serialization.h" + +/*! + * \brief Serialize a changeset into chunks and write it into DB with no checks and metadata update. + * + * \param txn Journal DB transaction. + * \param ch Changeset to be written. + */ +void journal_write_changeset(knot_lmdb_txn_t *txn, const changeset_t *ch); + +/*! + * \brief Serialize zone contents aka "bootstrap" changeset into journal, no checks. + * + * \param txn Journal DB transaction. + * \param z Zone contents to be written. + */ +void journal_write_zone(knot_lmdb_txn_t *txn, const zone_contents_t *z); + +/*! + * \brief Merge all following changeset into one of journal changeset. + * + * \param j Zone journal. + * \param txn Journal DB transaction. + * \param merge_zij True if we shall merge into zone-in-journal. + * \param merge_serial Serial-from of the changeset to be merged into (ignored if 'merge_zij'). + * \param original_serial_to Output: previous serial-to of the merged changeset before merge. + * + * \note The error code will be in thx->ret. + */ +void journal_merge(zone_journal_t j, knot_lmdb_txn_t *txn, bool merge_zij, + uint32_t merge_serial, uint32_t *original_serial_to); + +/*! + * \brief Delete some journal changesets in attempt to fulfill usage quotas. + * + * \param txn Journal DB transaction. + * \param from Serial-from of the first changeset to be deleted. + * \param zone Zone name. + * \param tofree_size Amount of data (in bytes) to be at least deleted. + * \param tofree_count Number of changesets to be at least deleted. + * \param stop_at_serial Must not delete the changeset with this serial-from. + * \param freed_size Output: amount of data really deleted. + * \param freed_count Output: number of changesets really freed. + * \param stopped_at Output: serial-to of the last deleted changeset. + * + * \return True if something was deleted (not necessarily fulfilling tofree_*). + */ +bool journal_delete(knot_lmdb_txn_t *txn, uint32_t from, const knot_dname_t *zone, + uint64_t tofree_size, size_t tofree_count, uint32_t stop_at_serial, + uint64_t *freed_size, size_t *freed_count, uint32_t *stopped_at); + +/*! + * \brief Perform a merge or zone flush in order to enable deleting more changesets. + * + * \param j Zone journal. + * \param txn Journal DB transaction. + * \param md Journal metadata. + * + * \note It might set txn->ret to KNOT_EBUSY to fail out from this operation and let the zone flush itself. + */ +void journal_try_flush(zone_journal_t j, knot_lmdb_txn_t *txn, journal_metadata_t *md); + +/*! + * \brief Perform delete/merge/flush operations to fulfill configured journal quotas. + * + * \param j Zone journal. + * \param txn Journal DB transaction. + * \param md Journal metadata. + * \param max_usage Configured maximum usage (in bytes) of journal DB by this zone. + * \param max_count Configured maximum number of changesets. + */ +void journal_fix_occupation(zone_journal_t j, knot_lmdb_txn_t *txn, journal_metadata_t *md, + int64_t max_usage, ssize_t max_count); + +/*! + * \brief Store zone-in-journal into the journal, update metadata. + * + * \param j Zone journal. + * \param z Zone contents to be stored. + * + * \return KNOT_E* + */ +int journal_insert_zone(zone_journal_t j, const zone_contents_t *z); + +/*! + * \brief Store changeset into journal, fulfilling quotas and updating metadata. + * + * \param j Zone journal. + * \param ch Changeset to be stored. + * \param extra Extra changeset to be stored in the role of merged changeset. + * \param zdiff Zone diff to be stored instead of changeset. + * + * \note The extra changesetis being stored on zone load, it is basically the diff + * between zonefile and loaded zone contents. Afterwards, it will be treated + * the same like merged changeset. Inserting it requires no zone-in-journal + * present and leads to deleting any previous merged changeset. + * + * \return KNOT_E* + */ +int journal_insert(zone_journal_t j, const changeset_t *ch, const changeset_t *extra, + const zone_diff_t *zdiff); diff --git a/src/knot/journal/knot_lmdb.c b/src/knot/journal/knot_lmdb.c new file mode 100644 index 0000000..bc17462 --- /dev/null +++ b/src/knot/journal/knot_lmdb.c @@ -0,0 +1,770 @@ +/* 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 <stdarg.h> +#include <stdio.h> // snprintf +#include <stdlib.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "knot/journal/knot_lmdb.h" + +#include "knot/conf/conf.h" +#include "contrib/files.h" +#include "contrib/wire_ctx.h" +#include "libknot/dname.h" +#include "libknot/endian.h" +#include "libknot/error.h" + +#define LMDB_DIR_MODE 0770 +#define LMDB_FILE_MODE 0660 + +static void err_to_knot(int *err) +{ + switch (*err) { + case MDB_SUCCESS: + *err = KNOT_EOK; + break; + case MDB_NOTFOUND: + *err = KNOT_ENOENT; + break; + case MDB_TXN_FULL: + *err = KNOT_ELIMIT; + break; + case MDB_MAP_FULL: + case ENOSPC: + *err = KNOT_ESPACE; + break; + default: + *err = (*err < 0 ? *err : -*err); + } +} + +void knot_lmdb_init(knot_lmdb_db_t *db, const char *path, size_t mapsize, unsigned env_flags, const char *dbname) +{ +#ifdef __OpenBSD__ + /* + * Enforce that MDB_WRITEMAP is set. + * + * MDB assumes a unified buffer cache. + * + * See https://www.openldap.org/pub/hyc/mdm-paper.pdf section 3.1, + * references 17, 18, and 19. + * + * From Howard Chu: "This requirement can be relaxed in the + * current version of the library. If you create the environment + * with the MDB_WRITEMAP option then all reads and writes are + * performed using mmap, so the file buffer cache is irrelevant. + * Of course then you lose the protection that the read-only + * map offers." + */ + env_flags |= MDB_WRITEMAP; +#endif + db->env = NULL; + db->path = strdup(path); + db->mapsize = mapsize; + db->env_flags = env_flags; + db->dbname = dbname; + pthread_mutex_init(&db->opening_mutex, NULL); + db->maxdbs = 2; + db->maxreaders = conf_lmdb_readers(conf()); +} + +static int lmdb_stat(const char *lmdb_path, struct stat *st) +{ + char data_mdb[strlen(lmdb_path) + 10]; + (void)snprintf(data_mdb, sizeof(data_mdb), "%s/data.mdb", lmdb_path); + if (stat(data_mdb, st) == 0) { + return st->st_size > 0 ? KNOT_EOK : KNOT_ENODB; + } else if (errno == ENOENT) { + return KNOT_ENODB; + } else { + return knot_map_errno(); + } +} + +int knot_lmdb_exists(knot_lmdb_db_t *db) +{ + if (db->env != NULL) { + return KNOT_EOK; + } + if (db->path == NULL) { + return KNOT_ENODB; + } + struct stat unused; + return lmdb_stat(db->path, &unused); +} + +static int fix_mapsize(knot_lmdb_db_t *db) +{ + if (db->mapsize == 0) { + struct stat st; + int ret = lmdb_stat(db->path, &st); + if (ret != KNOT_EOK) { + return ret; + } + db->mapsize = st.st_size * 2; // twice the size as DB might grow while we read it + db->env_flags |= MDB_RDONLY; + } + return KNOT_EOK; +} + +size_t knot_lmdb_copy_size(knot_lmdb_db_t *to_copy) +{ + size_t copy_size = 1048576; + struct stat st; + if (lmdb_stat(to_copy->path, &st) == KNOT_EOK) { + copy_size += st.st_size * 2; + } + return copy_size; +} + +static int lmdb_open(knot_lmdb_db_t *db) +{ + MDB_txn *init_txn = NULL; + + if (db->env != NULL) { + return KNOT_EOK; + } + + if (db->path == NULL) { + return KNOT_ENOMEM; + } + + int ret = fix_mapsize(db); + if (ret != KNOT_EOK) { + return ret; + } + + ret = make_dir(db->path, LMDB_DIR_MODE, true); + if (ret != KNOT_EOK) { + return ret; + } + + long page_size = sysconf(_SC_PAGESIZE); + if (page_size <= 0) { + return KNOT_ERROR; + } + size_t mapsize = (db->mapsize / page_size + 1) * page_size; + + ret = mdb_env_create(&db->env); + if (ret != MDB_SUCCESS) { + err_to_knot(&ret); + return ret; + } + + ret = mdb_env_set_mapsize(db->env, mapsize); + if (ret == MDB_SUCCESS) { + ret = mdb_env_set_maxdbs(db->env, db->maxdbs); + } + if (ret == MDB_SUCCESS) { + ret = mdb_env_set_maxreaders(db->env, db->maxreaders); + } + if (ret == MDB_SUCCESS) { + ret = mdb_env_open(db->env, db->path, db->env_flags, LMDB_FILE_MODE); + } + if (ret == MDB_SUCCESS) { + unsigned init_txn_flags = (db->env_flags & MDB_RDONLY); + ret = mdb_txn_begin(db->env, NULL, init_txn_flags, &init_txn); + if (ret == MDB_READERS_FULL) { + int cleared = 0; + ret = mdb_reader_check(db->env, &cleared); + if (ret == MDB_SUCCESS) { + ret = mdb_txn_begin(db->env, NULL, init_txn_flags, &init_txn); + } + } + } + if (ret == MDB_SUCCESS) { + ret = mdb_dbi_open(init_txn, db->dbname, MDB_CREATE, &db->dbi); + } + if (ret == MDB_SUCCESS) { + ret = mdb_txn_commit(init_txn); + } + + if (ret != MDB_SUCCESS) { + if (init_txn != NULL) { + mdb_txn_abort(init_txn); + } + mdb_env_close(db->env); + db->env = NULL; + } + err_to_knot(&ret); + return ret; +} + +int knot_lmdb_open(knot_lmdb_db_t *db) +{ + pthread_mutex_lock(&db->opening_mutex); + int ret = lmdb_open(db); + pthread_mutex_unlock(&db->opening_mutex); + return ret; +} + +static void lmdb_close(knot_lmdb_db_t *db) +{ + if (db->env != NULL) { + mdb_dbi_close(db->env, db->dbi); + mdb_env_close(db->env); + db->env = NULL; + } +} + +void knot_lmdb_close(knot_lmdb_db_t *db) +{ + pthread_mutex_lock(&db->opening_mutex); + lmdb_close(db); + pthread_mutex_unlock(&db->opening_mutex); +} + +static int lmdb_reinit(knot_lmdb_db_t *db, const char *path, size_t mapsize, unsigned env_flags) +{ +#ifdef __OpenBSD__ + env_flags |= MDB_WRITEMAP; +#endif + if (strcmp(db->path, path) == 0 && db->mapsize == mapsize && db->env_flags == env_flags) { + return KNOT_EOK; + } + if (db->env != NULL) { + return KNOT_EISCONN; + } + free(db->path); + db->path = strdup(path); + db->mapsize = mapsize; + db->env_flags = env_flags; + return KNOT_EOK; +} + +int knot_lmdb_reinit(knot_lmdb_db_t *db, const char *path, size_t mapsize, unsigned env_flags) +{ + pthread_mutex_lock(&db->opening_mutex); + int ret = lmdb_reinit(db, path, mapsize, env_flags); + pthread_mutex_unlock(&db->opening_mutex); + return ret; +} + +int knot_lmdb_reconfigure(knot_lmdb_db_t *db, const char *path, size_t mapsize, unsigned env_flags) +{ + pthread_mutex_lock(&db->opening_mutex); + int ret = lmdb_reinit(db, path, mapsize, env_flags); + if (ret != KNOT_EOK) { + lmdb_close(db); + ret = lmdb_reinit(db, path, mapsize, env_flags); + if (ret == KNOT_EOK) { + ret = lmdb_open(db); + } + } + pthread_mutex_unlock(&db->opening_mutex); + return ret; +} + +void knot_lmdb_deinit(knot_lmdb_db_t *db) +{ + knot_lmdb_close(db); + pthread_mutex_destroy(&db->opening_mutex); + free(db->path); +} + +void knot_lmdb_begin(knot_lmdb_db_t *db, knot_lmdb_txn_t *txn, bool rw) +{ + txn->ret = mdb_txn_begin(db->env, NULL, rw ? 0 : MDB_RDONLY, &txn->txn); + err_to_knot(&txn->ret); + if (txn->ret == KNOT_EOK) { + txn->opened = true; + txn->db = db; + txn->is_rw = rw; + } +} + +void knot_lmdb_abort(knot_lmdb_txn_t *txn) +{ + if (txn->opened) { + if (txn->cursor != NULL) { + mdb_cursor_close(txn->cursor); + txn->cursor = NULL; + } + mdb_txn_abort(txn->txn); + txn->opened = false; + } +} + +static bool txn_semcheck(knot_lmdb_txn_t *txn) +{ + if (!txn->opened && txn->ret == KNOT_EOK) { + txn->ret = KNOT_ESEMCHECK; + } + if (txn->ret != KNOT_EOK) { + knot_lmdb_abort(txn); + return false; + } + return true; +} + +void knot_lmdb_commit(knot_lmdb_txn_t *txn) +{ + if (!txn_semcheck(txn)) { + return; + } + if (txn->cursor != NULL) { + mdb_cursor_close(txn->cursor); + txn->cursor = NULL; + } + txn->ret = mdb_txn_commit(txn->txn); + err_to_knot(&txn->ret); + txn->opened = false; +} + +// save the programmer's frequent checking for ENOMEM when creating search keys +static bool txn_enomem(knot_lmdb_txn_t *txn, const MDB_val *tocheck) +{ + if (tocheck->mv_data == NULL) { + txn->ret = KNOT_ENOMEM; + knot_lmdb_abort(txn); + return false; + } + return true; +} + +static bool init_cursor(knot_lmdb_txn_t *txn) +{ + if (txn->cursor == NULL) { + txn->ret = mdb_cursor_open(txn->txn, txn->db->dbi, &txn->cursor); + err_to_knot(&txn->ret); + if (txn->ret != KNOT_EOK) { + knot_lmdb_abort(txn); + return false; + } + } + return true; +} + +static bool curget(knot_lmdb_txn_t *txn, MDB_cursor_op op) +{ + txn->ret = mdb_cursor_get(txn->cursor, &txn->cur_key, &txn->cur_val, op); + err_to_knot(&txn->ret); + if (txn->ret == KNOT_ENOENT) { + txn->ret = KNOT_EOK; + return false; + } + return (txn->ret == KNOT_EOK); +} + +static int mdb_val_clone(const MDB_val *orig, MDB_val *clone) +{ + clone->mv_data = malloc(orig->mv_size); + if (clone->mv_data == NULL) { + return KNOT_ENOMEM; + } + clone->mv_size = orig->mv_size; + memcpy(clone->mv_data, orig->mv_data, clone->mv_size); + return KNOT_EOK; +} + +bool knot_lmdb_find(knot_lmdb_txn_t *txn, MDB_val *what, knot_lmdb_find_t how) +{ + if (!txn_semcheck(txn) || !init_cursor(txn) || !txn_enomem(txn, what)) { + return false; + } + txn->cur_key.mv_size = what->mv_size; + txn->cur_key.mv_data = what->mv_data; + txn->cur_val.mv_size = 0; + txn->cur_val.mv_data = NULL; + knot_lmdb_find_t cmp = (how & 3); + bool succ = curget(txn, cmp == KNOT_LMDB_EXACT ? MDB_SET : MDB_SET_RANGE); + if (cmp == KNOT_LMDB_LEQ && txn->ret == KNOT_EOK) { + // LEQ is not supported by LMDB, we use GEQ and go back + if (succ) { + if (txn->cur_key.mv_size != what->mv_size || + memcmp(txn->cur_key.mv_data, what->mv_data, what->mv_size) != 0) { + succ = curget(txn, MDB_PREV); + } + } else { + succ = curget(txn, MDB_LAST); + } + } + + if ((how & KNOT_LMDB_FORCE) && !succ && txn->ret == KNOT_EOK) { + txn->ret = KNOT_ENOENT; + } + + return succ; +} + +// this is not bulletproof thread-safe (in case of LMDB fail-teardown, but mostly OK +int knot_lmdb_find_threadsafe(knot_lmdb_txn_t *txn, MDB_val *key, MDB_val *val, knot_lmdb_find_t how) +{ + assert(how == KNOT_LMDB_EXACT); + if (key->mv_data == NULL) { + return KNOT_ENOMEM; + } + if (!txn->opened) { + return KNOT_EINVAL; + } + if (txn->ret != KNOT_EOK) { + return txn->ret; + } + MDB_val tmp = { 0 }; + int ret = mdb_get(txn->txn, txn->db->dbi, key, &tmp); + err_to_knot(&ret); + if (ret == KNOT_EOK) { + ret = mdb_val_clone(&tmp, val); + } + return ret; +} + +bool knot_lmdb_first(knot_lmdb_txn_t *txn) +{ + return txn_semcheck(txn) && init_cursor(txn) && curget(txn, MDB_FIRST); +} + +bool knot_lmdb_next(knot_lmdb_txn_t *txn) +{ + if (txn->cursor == NULL && txn->ret == KNOT_EOK) { + txn->ret = KNOT_EINVAL; + } + if (!txn_semcheck(txn)) { + return false; + } + return curget(txn, MDB_NEXT); +} + +bool knot_lmdb_is_prefix_of(const MDB_val *prefix, const MDB_val *of) +{ + return prefix->mv_size <= of->mv_size && + memcmp(prefix->mv_data, of->mv_data, prefix->mv_size) == 0; +} + +void knot_lmdb_del_cur(knot_lmdb_txn_t *txn) +{ + if (txn_semcheck(txn)) { + txn->ret = mdb_cursor_del(txn->cursor, 0); + err_to_knot(&txn->ret); + } +} + +void knot_lmdb_del_prefix(knot_lmdb_txn_t *txn, MDB_val *prefix) +{ + knot_lmdb_foreach(txn, prefix) { + knot_lmdb_del_cur(txn); + } +} + +int knot_lmdb_apply_threadsafe(knot_lmdb_txn_t *txn, const MDB_val *key, bool prefix, lmdb_apply_cb cb, void *ctx) +{ + MDB_cursor *cursor; + int ret = mdb_cursor_open(txn->txn, txn->db->dbi, &cursor); + err_to_knot(&ret); + if (ret != KNOT_EOK) { + return ret; + } + + MDB_val getkey = *key, getval = { 0 }; + ret = mdb_cursor_get(cursor, &getkey, &getval, prefix ? MDB_SET_RANGE : MDB_SET); + err_to_knot(&ret); + if (ret != KNOT_EOK) { + mdb_cursor_close(cursor); + if (prefix && ret == KNOT_ENOENT) { + return KNOT_EOK; + } + return ret; + } + + if (prefix) { + while (knot_lmdb_is_prefix_of(key, &getkey) && ret == KNOT_EOK) { + ret = cb(&getkey, &getval, ctx); + if (ret == KNOT_EOK) { + ret = mdb_cursor_get(cursor, &getkey, &getval, MDB_NEXT); + err_to_knot(&ret); + } + } + if (ret == KNOT_ENOENT) { + ret = KNOT_EOK; + } + } else { + ret = cb(&getkey, &getval, ctx); + } + mdb_cursor_close(cursor); + return ret; +} + +bool knot_lmdb_insert(knot_lmdb_txn_t *txn, MDB_val *key, MDB_val *val) +{ + if (txn_semcheck(txn) && txn_enomem(txn, key)) { + unsigned flags = (val->mv_size > 0 && val->mv_data == NULL ? MDB_RESERVE : 0); + txn->ret = mdb_put(txn->txn, txn->db->dbi, key, val, flags); + err_to_knot(&txn->ret); + } + return (txn->ret == KNOT_EOK); +} + +int knot_lmdb_quick_insert(knot_lmdb_db_t *db, MDB_val key, MDB_val val) +{ + if (val.mv_data == NULL) { + free(key.mv_data); + return KNOT_ENOMEM; + } + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + knot_lmdb_insert(&txn, &key, &val); + free(key.mv_data); + free(val.mv_data); + knot_lmdb_commit(&txn); + return txn.ret; +} + +int knot_lmdb_copy_prefix(knot_lmdb_txn_t *from, knot_lmdb_txn_t *to, MDB_val *prefix) +{ + knot_lmdb_foreach(to, prefix) { + knot_lmdb_del_cur(to); + } + if (to->ret != KNOT_EOK) { + return to->ret; + } + knot_lmdb_foreach(from, prefix) { + knot_lmdb_insert(to, &from->cur_key, &from->cur_val); + } + return from->ret == KNOT_EOK ? to->ret : from->ret; +} + +int knot_lmdb_copy_prefixes(knot_lmdb_db_t *from, knot_lmdb_db_t *to, + MDB_val *prefixes, size_t n_prefixes) +{ + if (n_prefixes < 1) { + return KNOT_EOK; + } + if (from == NULL || to == NULL || prefixes == NULL) { + return KNOT_EINVAL; + } + int ret = knot_lmdb_open(from); + if (ret == KNOT_EOK) { + ret = knot_lmdb_open(to); + } + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t tr = { 0 }, tw = { 0 }; + knot_lmdb_begin(from, &tr, false); + knot_lmdb_begin(to, &tw, true); + for (size_t i = 0; i < n_prefixes && ret == KNOT_EOK; i++) { + ret = knot_lmdb_copy_prefix(&tr, &tw, &prefixes[i]); + } + knot_lmdb_commit(&tw); + knot_lmdb_commit(&tr); + return ret == KNOT_EOK ? tw.ret : ret; +} + +size_t knot_lmdb_usage(knot_lmdb_txn_t *txn) +{ + if (!txn_semcheck(txn)) { + return 0; + } + MDB_stat st = { 0 }; + txn->ret = mdb_stat(txn->txn, txn->db->dbi, &st); + err_to_knot(&txn->ret); + + size_t pgs_used = st.ms_branch_pages + st.ms_leaf_pages + st.ms_overflow_pages; + return (pgs_used * st.ms_psize); +} + +static bool make_key_part(void *key_data, size_t key_len, const char *format, va_list arg) +{ + wire_ctx_t wire = wire_ctx_init(key_data, key_len); + const char *tmp_s; + const knot_dname_t *tmp_d; + const void *tmp_v; + size_t tmp; + + for (const char *f = format; *f != '\0'; f++) { + switch (*f) { + case 'B': + wire_ctx_write_u8(&wire, va_arg(arg, int)); + break; + case 'H': + wire_ctx_write_u16(&wire, va_arg(arg, int)); + break; + case 'I': + wire_ctx_write_u32(&wire, va_arg(arg, uint32_t)); + break; + case 'L': + wire_ctx_write_u64(&wire, va_arg(arg, uint64_t)); + break; + case 'S': + tmp_s = va_arg(arg, const char *); + wire_ctx_write(&wire, tmp_s, strlen(tmp_s) + 1); + break; + case 'N': + tmp_d = va_arg(arg, const knot_dname_t *); + wire_ctx_write(&wire, tmp_d, knot_dname_size(tmp_d)); + break; + case 'D': + tmp_v = va_arg(arg, const void *); + tmp = va_arg(arg, size_t); + wire_ctx_write(&wire, tmp_v, tmp); + break; + } + } + + return wire.error == KNOT_EOK && wire_ctx_available(&wire) == 0; +} + +MDB_val knot_lmdb_make_key(const char *format, ...) +{ + MDB_val key = { 0 }; + va_list arg; + const char *tmp_s; + const knot_dname_t *tmp_d; + + // first, just determine the size of the key + va_start(arg, format); + for (const char *f = format; *f != '\0'; f++) { + switch (*f) { + case 'B': + key.mv_size += sizeof(uint8_t); + (void)va_arg(arg, int); // uint8_t will be promoted to int + break; + case 'H': + key.mv_size += sizeof(uint16_t); + (void)va_arg(arg, int); // uint16_t will be promoted to int + break; + case 'I': + key.mv_size += sizeof(uint32_t); + (void)va_arg(arg, uint32_t); + break; + case 'L': + key.mv_size += sizeof(uint64_t); + (void)va_arg(arg, uint64_t); + break; + case 'S': + tmp_s = va_arg(arg, const char *); + key.mv_size += strlen(tmp_s) + 1; + break; + case 'N': + tmp_d = va_arg(arg, const knot_dname_t *); + key.mv_size += knot_dname_size(tmp_d); + break; + case 'D': + (void)va_arg(arg, const void *); + key.mv_size += va_arg(arg, size_t); + break; + } + } + va_end(arg); + + // second, alloc the key and fill it + if (key.mv_size > 0) { + key.mv_data = malloc(key.mv_size); + } + if (key.mv_data == NULL) { + return key; + } + va_start(arg, format); + bool succ = make_key_part(key.mv_data, key.mv_size, format, arg); + assert(succ); + (void)succ; + va_end(arg); + return key; +} + +bool knot_lmdb_make_key_part(void *key_data, size_t key_len, const char *format, ...) +{ + va_list arg; + va_start(arg, format); + bool succ = make_key_part(key_data, key_len, format, arg); + va_end(arg); + return succ; +} + +static bool unmake_key_part(const void *key_data, size_t key_len, const char *format, va_list arg) +{ + if (key_data == NULL) { + return false; + } + wire_ctx_t wire = wire_ctx_init_const(key_data, key_len); + for (const char *f = format; *f != '\0' && wire.error == KNOT_EOK && wire_ctx_available(&wire) > 0; f++) { + void *tmp = va_arg(arg, void *); + size_t tmsize; + switch (*f) { + case 'B': + if (tmp == NULL) { + wire_ctx_skip(&wire, sizeof(uint8_t)); + } else { + *(uint8_t *)tmp = wire_ctx_read_u8(&wire); + } + break; + case 'H': + if (tmp == NULL) { + wire_ctx_skip(&wire, sizeof(uint16_t)); + } else { + *(uint16_t *)tmp = wire_ctx_read_u16(&wire); + } + break; + case 'I': + if (tmp == NULL) { + wire_ctx_skip(&wire, sizeof(uint32_t)); + } else { + *(uint32_t *)tmp = wire_ctx_read_u32(&wire); + } + break; + case 'L': + if (tmp == NULL) { + wire_ctx_skip(&wire, sizeof(uint64_t)); + } else { + *(uint64_t *)tmp = wire_ctx_read_u64(&wire); + } + break; + case 'S': + if (tmp != NULL) { + *(const char **)tmp = (const char *)wire.position; + } + wire_ctx_skip(&wire, strlen((const char *)wire.position) + 1); + break; + case 'N': + if (tmp != NULL) { + *(const knot_dname_t **)tmp = (const knot_dname_t *)wire.position; + } + wire_ctx_skip(&wire, knot_dname_size((const knot_dname_t *)wire.position)); + break; + case 'D': + tmsize = va_arg(arg, size_t); + if (tmp != NULL) { + memcpy(tmp, wire.position, tmsize); + } + wire_ctx_skip(&wire, tmsize); + break; + } + } + return (wire.error == KNOT_EOK && wire_ctx_available(&wire) == 0); +} + +bool knot_lmdb_unmake_key(const void *key_data, size_t key_len, const char *format, ...) +{ + va_list arg; + va_start(arg, format); + bool succ = unmake_key_part(key_data, key_len, format, arg); + va_end(arg); + return succ; +} + +bool knot_lmdb_unmake_curval(knot_lmdb_txn_t *txn, const char *format, ...) +{ + va_list arg; + va_start(arg, format); + bool succ = unmake_key_part(txn->cur_val.mv_data, txn->cur_val.mv_size, format, arg); + va_end(arg); + if (!succ && txn->ret == KNOT_EOK) { + txn->ret = KNOT_EMALF; + } + return succ; +} diff --git a/src/knot/journal/knot_lmdb.h b/src/knot/journal/knot_lmdb.h new file mode 100644 index 0000000..6214a10 --- /dev/null +++ b/src/knot/journal/knot_lmdb.h @@ -0,0 +1,446 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <lmdb.h> + +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <pthread.h> + +typedef struct knot_lmdb_db { + MDB_dbi dbi; + MDB_env *env; + pthread_mutex_t opening_mutex; + + // those are static options. Set them after knot_lmdb_init(). + unsigned maxdbs; + unsigned maxreaders; + + // those are internal options. Please don't touch them directly. + size_t mapsize; + unsigned env_flags; // MDB_NOTLS, MDB_RDONLY, MDB_WRITEMAP, MDB_DUPSORT, MDB_NOSYNC, MDB_MAPASYNC + const char *dbname; + char *path; +} knot_lmdb_db_t; + +typedef struct { + MDB_txn *txn; + MDB_cursor *cursor; + MDB_val cur_key; + MDB_val cur_val; + + bool opened; + bool is_rw; + int ret; + knot_lmdb_db_t *db; +} knot_lmdb_txn_t; + +typedef enum { + KNOT_LMDB_EXACT = 3, /*! \brief Search for exactly matching key. */ + KNOT_LMDB_LEQ = 1, /*! \brief Search lexicographically lower or equal key. */ + KNOT_LMDB_GEQ = 2, /*! \brief Search lexicographically greater or equal key. */ + KNOT_LMDB_FORCE = 4, /*! \brief If no matching key found, consider it a transaction failure (KNOT_ENOENT). */ +} knot_lmdb_find_t; + +/*! + * \brief Callback used in sweep functions. + * + * \retval true for zones to preserve. + * \retval false for zones to remove. + */ +typedef bool (*sweep_cb)(const uint8_t *zone, void *data); + +/*! + * \brief Callback used in copy functions. + * + * \retval true if the current record shall be copied + * \retval false if the current record shall be skipped + */ +typedef bool (*knot_lmdb_copy_cb)(MDB_val *cur_key, MDB_val *cur_val); + +/*! + * \brief Initialise the DB handling structure. + * + * \param db DB handling structure. + * \param path Path to LMDB database on filesystem. + * \param mapsize Maximum size of the DB on FS. + * \param env_flags LMDB environment flags (e.g. MDB_RDONLY) + * \param dbname Optional: name of the sub-database. + */ +void knot_lmdb_init(knot_lmdb_db_t *db, const char *path, size_t mapsize, unsigned env_flags, const char *dbname); + +/*! + * \brief Check if the database exists on the filesystem. + * + * \param db The DB in question. + * + * \retval KNOT_EOK The database exists (and is accessible for stat() ). + * \retval KNOT_ENODB The database doesn't exist. + * \return KNOT_E* explaining why stat() failed. + */ +int knot_lmdb_exists(knot_lmdb_db_t *db); + +/*! + * \brief Big enough mapsize for new database to hold a copy of to_copy. + */ +size_t knot_lmdb_copy_size(knot_lmdb_db_t *to_copy); + +/*! + * \brief Open the previously initialised DB. + * + * \param db The DB to be opened. + * + * \note If db->mapsize is zero, it will be set to twice the current size, and DB opened read-only! + * + * \return KNOT_E* + */ +int knot_lmdb_open(knot_lmdb_db_t *db); + +/*! + * \brief Close the database, but keep it initialised. + * + * \param db The DB to be closed. + */ +void knot_lmdb_close(knot_lmdb_db_t *db); + +/*! + * \brief Re-initialise existing DB with modified parameters. + * + * \note If the parameters differ and DB is open, it will be refused. + * + * \param db The DB to be modified. + * \param path New path to the DB. + * \param mapsize New mapsize. + * \param env_flags New LMDB environment flags. + * + * \return KNOT_EOK on success, KNOT_EISCONN if not possible. + */ +int knot_lmdb_reinit(knot_lmdb_db_t *db, const char *path, size_t mapsize, unsigned env_flags); + +/*! + * \brief Re-open opened DB with modified parameters. + * + * \note The DB will be first closed, re-initialised and finally opened again. + * + * \note There must not be any DB transaction during this process. + * + * \param db The DB to be modified. + * \param path New path to the DB. + * \param mapsize New mapsize. + * \param env_flags New LMDB environment flags. + * + * \return KNOT_E* + */ +int knot_lmdb_reconfigure(knot_lmdb_db_t *db, const char *path, size_t mapsize, unsigned env_flags); + +/*! + * \brief Close and de-initialise DB. + * + * \param db DB to be deinitialized. + */ +void knot_lmdb_deinit(knot_lmdb_db_t *db); + +/*! + * \brief Return true if DB is open. + */ +inline static bool knot_lmdb_is_open(knot_lmdb_db_t *db) { return db != NULL && db->env != NULL; } + +/*! + * \brief Start a DB transaction. + * + * \param db The database. + * \param txn Transaction handling structure to be initialised. + * \param rw True for read-write transaction, false for read-only. + * + * \note The error code will be stored in txn->ret. + */ +void knot_lmdb_begin(knot_lmdb_db_t *db, knot_lmdb_txn_t *txn, bool rw); + +/*! + * \brief Abort a transaction. + * + * \param txn Transaction to be aborted. + */ +void knot_lmdb_abort(knot_lmdb_txn_t *txn); + +/*! + * \brief Commit a transaction, or abort it if id had failured. + * + * \param txn Transaction to be committed. + * + * \note If txn->ret equals KNOT_EOK afterwards, whole DB transaction was successful. + */ +void knot_lmdb_commit(knot_lmdb_txn_t *txn); + +/*! + * \brief Find a key in database. The matched key will be in txn->cur_key and its value in txn->cur_val. + * + * \param txn DB transaction. + * \param what Key to be searched for. + * \param how Method of comparing keys. See comments at knot_lmdb_find_t. + * + * \note It's possible to use knot_lmdb_next() subsequently to iterate over following keys. + * + * \return True if a key found, false if none or failure. + */ +bool knot_lmdb_find(knot_lmdb_txn_t *txn, MDB_val *what, knot_lmdb_find_t how); + +/*! + * \brief Simple database lookup in case txn shared among threads. + * + * \param txn DB transaction share among threads. + * \param key Key to be searched for. + * \param val Output: database value. + * \param how Must be KNOT_LMDB_EXACT. + * + * \note Free val->mv_data afterwards! + * + * \retval KNOT_ENOENT no such key in DB. + * \return KNOT_E* + */ +int knot_lmdb_find_threadsafe(knot_lmdb_txn_t *txn, MDB_val *key, MDB_val *val, knot_lmdb_find_t how); + +/*! + * \brief Start iteration the whole DB from lexicographically first key. + * + * \note The first DB record will be in txn->cur_key and txn->cur_val. + * + * \param txn DB transaction. + * + * \return True if ok, false if no key at all or failure. + */ +bool knot_lmdb_first(knot_lmdb_txn_t *txn); + +/*! + * \brief Iterate to the lexicographically next key (sets txn->cur_key and txn->cur_val). + * + * \param txn DB transaction. + * + * \return True if ok, false if behind the end of DB or failure. + */ +bool knot_lmdb_next(knot_lmdb_txn_t *txn); + +/*! + * \brief Check if one DB key is a prefix of another, + * + * \param prefix DB key prefix. + * \param of Another DB key. + * + * \return True iff 'prefix' is a prefix of 'of'. + */ +bool knot_lmdb_is_prefix_of(const MDB_val *prefix, const MDB_val *of); + +/*! + * \brief Find leftmost key in DB matching given prefix. + * + * \param txn DB transaction. + * \param prefix Prefix searched for. + * + * \return True if found, false if none or failure. + */ +inline static bool knot_lmdb_find_prefix(knot_lmdb_txn_t *txn, MDB_val *prefix) +{ + return knot_lmdb_find(txn, prefix, KNOT_LMDB_GEQ) && + knot_lmdb_is_prefix_of(prefix, &txn->cur_key); +} + +/*! + * \brief Execute following block of commands for every key in DB matching given prefix. + * + * \param txn DB transaction. + * \param prefix Prefix searched for. + */ +#define knot_lmdb_foreach(txn, prefix) \ + for (bool _knot_lmdb_foreach_found = knot_lmdb_find((txn), (prefix), KNOT_LMDB_GEQ); \ + _knot_lmdb_foreach_found && knot_lmdb_is_prefix_of((prefix), &(txn)->cur_key); \ + _knot_lmdb_foreach_found = knot_lmdb_next((txn))) + +/*! + * \brief Execute following block of commands for every key in DB. + * + * \param txn DB transaction. + */ +#define knot_lmdb_forwhole(txn) \ + for (bool _knot_lmdb_forwhole_any = knot_lmdb_first((txn)); \ + _knot_lmdb_forwhole_any; \ + _knot_lmdb_forwhole_any = knot_lmdb_next((txn))) + +/*! + * \brief Delete the one DB record, that the iteration is currently pointing to. + * + * \note It's safe to delete during an uncomplicated iteration, e.g. knot_lmdb_foreach(). + * + * \param txn DB transaction. + */ +void knot_lmdb_del_cur(knot_lmdb_txn_t *txn); + +/*! + * \brief Delete all DB records matching given key prefix. + * + * \param txn DB transaction. + * \param prefix Prefix to be deleted. + */ +void knot_lmdb_del_prefix(knot_lmdb_txn_t *txn, MDB_val *prefix); + +typedef int (*lmdb_apply_cb)(MDB_val *key, MDB_val *val, void *ctx); + +/*! + * \brief Call a callback for any item matching given key. + * + * \note This function does not affect fields within txn struct, + * thus can be used on txn shared between threads. + * + * \param txn DB transaction. + * \param key Key to be searched for. + * \param prefix The 'key' is in fact prefix, apply on all items matching prefix. + * \param cb Callback to be called. + * \param ctx Arbitrary context for the callback. + * + * \return KNOT_E* + */ +int knot_lmdb_apply_threadsafe(knot_lmdb_txn_t *txn, const MDB_val *key, bool prefix, lmdb_apply_cb cb, void *ctx); + +/*! + * \brief Insert a new record into the DB. + * + * \note If a record with equal key already exists in the DB, its value will be quietly overwritten. + * + * \param txn DB transaction. + * \param key Inserted key. + * \param val Inserted value. + * + * \return False if failure. + */ +bool knot_lmdb_insert(knot_lmdb_txn_t *txn, MDB_val *key, MDB_val *val); + +/*! + * \brief Open a transaction, insert a record, commit and free key's and val's mv_data. + * + * \param db DB to be inserted into. + * \param key Inserted key. + * \param val Inserted val. + * + * \return KNOT_E* + */ +int knot_lmdb_quick_insert(knot_lmdb_db_t *db, MDB_val key, MDB_val val); + +/*! + * \brief Copy all records matching given key prefix. + * + * \param from Open RO/RW transaction in the database to copy from. + * \param to Open RW txn in the DB to copy to. + * \param prefix Prefix for matching records to be copied. + * + * \note Prior to copying, all records from the target DB, matching the prefix, will be deleted! + * + * \return KNOT_E* + * + * \note KNOT_EOK even if none records matched the prefix (and were copied). + */ +int knot_lmdb_copy_prefix(knot_lmdb_txn_t *from, knot_lmdb_txn_t *to, MDB_val *prefix); + +/*! + * \brief Copy all records matching any of multiple prefixes. + * + * \param from DB to copy from. + * \param to DB to copy to. + * \param prefixes List of prefixes to match. + * \param n_prefixes Number of prefixes in the list. + * + * \note Prior to copying, all records from the target DB, matching any of the prefixes, will be deleted! + * + * \return KNOT_E* + */ +int knot_lmdb_copy_prefixes(knot_lmdb_db_t *from, knot_lmdb_db_t *to, + MDB_val *prefixes, size_t n_prefixes); + +/*! + * \brief Amount of bytes used by the DB storage. + * + * \note According to LMDB design, it will be a multiple of page size, which is usually 4096. + * + * \param txn DB transaction. + * + * \return DB usage. + */ +size_t knot_lmdb_usage(knot_lmdb_txn_t *txn); + +/*! + * \brief Serialize various parameters into a DB key. + * + * \param format Specifies the number and type of parameters. + * \param ... For each character in 'format', one or two parameters with the actual values. + * + * \return DB key structure. 'mv_data' needs to be freed later. 'mv_data' is NULL on failure. + * + * Possible format characters are: + * - B for a byte + * - H for uint16 + * - I for uint32 + * - L for uint64, like H and I, the serialization converts them to big endian + * - S for zero-terminated string + * - N for a domain name (in knot_dname_t* format) + * - D for fixed-size data (takes two params: void* and size_t) + */ +MDB_val knot_lmdb_make_key(const char *format, ...); + +/*! + * \brief Serialize various parameters into prepared buffer. + * + * \param key_data Pointer to the buffer. + * \param key_len Size of the buffer. + * \param format Specifies the number and type of parameters. + * \param ... For each character in 'format', one or two parameters with the actual values. + * + * \note See comment at knot_lmdb_make_key(). + * + * \return True if ok and the serialization took exactly 'key_len', false on failure. + */ +bool knot_lmdb_make_key_part(void *key_data, size_t key_len, const char *format, ...); + +/*! + * \brief Deserialize various parameters from a buffer. + * + * \note 'format' must exactly correspond with what the data in buffer actually are. + * + * \param key_data Pointer to the buffer. + * \param key_len Size of the buffer. + * \param format Specifies the number and type of parameters. + * \param ... For each character in 'format', pointer to where the values will be stored. + * + * \note For B, H, I, L; provide simply pointers to variables of corresponding type. + * \note For S, N; provide pointer to pointer - it will be set to pointing inside the buffer, so no allocation here. + * \note For D, provide void* and size_t, the data will be copied. + * + * \return True if no failure. + */ +bool knot_lmdb_unmake_key(const void *key_data, size_t key_len, const char *format, ...); + +/*! + * \brief Deserialize various parameters from txn->cur_val. Set txn->ret to KNOT_EMALF if failure. + * + * \param txn DB transaction. + * \param format Specifies the number and type of parameters. + * \param ... For each character in 'format', pointer to where the values will be stored. + * + * \note See comment at knot_lmdb_unmake_key(). + * + * \return True if no failure. + */ +bool knot_lmdb_unmake_curval(knot_lmdb_txn_t *txn, const char *format, ...); diff --git a/src/knot/journal/serialization.c b/src/knot/journal/serialization.c new file mode 100644 index 0000000..5758481 --- /dev/null +++ b/src/knot/journal/serialization.c @@ -0,0 +1,501 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/journal/serialization.h" +#include "knot/zone/zone-tree.h" + +#define SERIALIZE_RRSET_INIT (-1) +#define SERIALIZE_RRSET_DONE ((1L<<16)+1) + +typedef enum { + PHASE_ZONE_SOA, + PHASE_ZONE_NODES, + PHASE_ZONE_NSEC3, + PHASE_SOA_1, + PHASE_REM, + PHASE_SOA_2, + PHASE_ADD, + PHASE_END, +} serialize_phase_t; + +#define RRSET_BUF_MAXSIZE 256 + +struct serialize_ctx { + zone_diff_t zdiff; + zone_tree_it_t zit; + zone_node_t *n; + uint16_t node_pos; + bool zone_diff; + bool zone_diff_add; + int ret; + + const changeset_t *ch; + changeset_iter_t it; + serialize_phase_t changeset_phase; + long rrset_phase; + knot_rrset_t rrset_buf[RRSET_BUF_MAXSIZE]; + size_t rrset_buf_size; + list_t free_rdatasets; +}; + +serialize_ctx_t *serialize_init(const changeset_t *ch) +{ + serialize_ctx_t *ctx = calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + return NULL; + } + + ctx->ch = ch; + ctx->changeset_phase = ch->soa_from != NULL ? PHASE_SOA_1 : PHASE_SOA_2; + ctx->rrset_phase = SERIALIZE_RRSET_INIT; + ctx->rrset_buf_size = 0; + init_list(&ctx->free_rdatasets); + + return ctx; +} + +serialize_ctx_t *serialize_zone_init(const zone_contents_t *z) +{ + serialize_ctx_t *ctx = calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + return NULL; + } + + zone_diff_from_zone(&ctx->zdiff, z); + ctx->changeset_phase = PHASE_ZONE_SOA; + ctx->rrset_phase = SERIALIZE_RRSET_INIT; + ctx->rrset_buf_size = 0; + init_list(&ctx->free_rdatasets); + + return ctx; +} + +serialize_ctx_t *serialize_zone_diff_init(const zone_diff_t *z) +{ + serialize_ctx_t *ctx = calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + return NULL; + } + + ctx->zone_diff = true; + ctx->zdiff = *z; + zone_diff_reverse(&ctx->zdiff); // start with removals of counterparts + + ctx->changeset_phase = PHASE_ZONE_SOA; + ctx->rrset_phase = SERIALIZE_RRSET_INIT; + ctx->rrset_buf_size = 0; + init_list(&ctx->free_rdatasets); + + return ctx; +} + +static knot_rrset_t get_next_rrset(serialize_ctx_t *ctx) +{ + knot_rrset_t res; + knot_rrset_init_empty(&res); + switch (ctx->changeset_phase) { + case PHASE_ZONE_SOA: + zone_tree_it_begin(&ctx->zdiff.nodes, &ctx->zit); + ctx->changeset_phase = PHASE_ZONE_NODES; + return node_rrset(ctx->zdiff.apex, KNOT_RRTYPE_SOA); + case PHASE_ZONE_NODES: + case PHASE_ZONE_NSEC3: + while (ctx->n == NULL || ctx->node_pos >= ctx->n->rrset_count) { + if (zone_tree_it_finished(&ctx->zit)) { + zone_tree_it_free(&ctx->zit); + if (ctx->changeset_phase == PHASE_ZONE_NSEC3 || + zone_tree_is_empty(&ctx->zdiff.nsec3s)) { + if (ctx->zone_diff && !ctx->zone_diff_add) { + ctx->zone_diff_add = true; + zone_diff_reverse(&ctx->zdiff); + zone_tree_it_begin(&ctx->zdiff.nodes, &ctx->zit); + ctx->changeset_phase = PHASE_ZONE_NODES; + return node_rrset(ctx->zdiff.apex, KNOT_RRTYPE_SOA); + } else { + ctx->changeset_phase = PHASE_END; + return res; + } + } else { + zone_tree_it_begin(&ctx->zdiff.nsec3s, &ctx->zit); + ctx->changeset_phase = PHASE_ZONE_NSEC3; + } + } + ctx->n = zone_tree_it_val(&ctx->zit); + zone_tree_it_next(&ctx->zit); + ctx->node_pos = 0; + } + res = node_rrset_at(ctx->n, ctx->node_pos++); + if (ctx->n == ctx->zdiff.apex && res.type == KNOT_RRTYPE_SOA) { + return get_next_rrset(ctx); + } + if (ctx->zone_diff) { + knot_rrset_t counter_rr = node_rrset(binode_counterpart(ctx->n), res.type); + if (counter_rr.ttl == res.ttl && !knot_rrset_empty(&counter_rr)) { + if (knot_rdataset_subset(&res.rrs, &counter_rr.rrs)) { + return get_next_rrset(ctx); + } + knot_rdataset_t rd_copy; + ctx->ret = knot_rdataset_copy(&rd_copy, &res.rrs, NULL); + if (ctx->ret == KNOT_EOK) { + knot_rdataset_subtract(&rd_copy, &counter_rr.rrs, NULL); + ptrlist_add(&ctx->free_rdatasets, rd_copy.rdata, NULL); + res.rrs = rd_copy; + assert(!knot_rrset_empty(&res)); + } else { + ctx->changeset_phase = PHASE_END; + } + } + } + return res; + case PHASE_SOA_1: + changeset_iter_rem(&ctx->it, ctx->ch); + ctx->changeset_phase = PHASE_REM; + return *ctx->ch->soa_from; + case PHASE_REM: + res = changeset_iter_next(&ctx->it); + if (knot_rrset_empty(&res)) { + changeset_iter_clear(&ctx->it); + changeset_iter_add(&ctx->it, ctx->ch); + ctx->changeset_phase = PHASE_ADD; + return *ctx->ch->soa_to; + } + return res; + case PHASE_SOA_2: + if (ctx->it.node != NULL) { + changeset_iter_clear(&ctx->it); + } + changeset_iter_add(&ctx->it, ctx->ch); + ctx->changeset_phase = PHASE_ADD; + return *ctx->ch->soa_to; + case PHASE_ADD: + res = changeset_iter_next(&ctx->it); + if (knot_rrset_empty(&res)) { + changeset_iter_clear(&ctx->it); + ctx->changeset_phase = PHASE_END; + } + return res; + default: + return res; + } +} + +void serialize_prepare(serialize_ctx_t *ctx, size_t thresh_size, + size_t max_size, size_t *realsize) +{ + *realsize = 0; + + // check if we are in middle of a rrset + if (ctx->rrset_buf_size > 0) { + ctx->rrset_buf[0] = ctx->rrset_buf[ctx->rrset_buf_size - 1]; + ctx->rrset_buf_size = 1; + + // memory optimization: free all buffered rrsets except last one + ptrnode_t *n, *next; + WALK_LIST_DELSAFE(n, next, ctx->free_rdatasets) { + if (n != TAIL(ctx->free_rdatasets)) { + free(n->d); + rem_node(&n->n); + free(n); + } + } + } else { + ctx->rrset_buf[0] = get_next_rrset(ctx); + if (ctx->changeset_phase == PHASE_END) { + ctx->rrset_buf_size = 0; + return; + } + ctx->rrset_buf_size = 1; + } + + size_t candidate = 0; + long tmp_phase = ctx->rrset_phase; + while (1) { + if (tmp_phase >= ctx->rrset_buf[ctx->rrset_buf_size - 1].rrs.count) { + if (ctx->rrset_buf_size >= RRSET_BUF_MAXSIZE) { + return; + } + ctx->rrset_buf[ctx->rrset_buf_size++] = get_next_rrset(ctx); + if (ctx->changeset_phase == PHASE_END) { + ctx->rrset_buf_size--; + return; + } + tmp_phase = SERIALIZE_RRSET_INIT; + } + if (tmp_phase == SERIALIZE_RRSET_INIT) { + candidate += 3 * sizeof(uint16_t) + + knot_dname_size(ctx->rrset_buf[ctx->rrset_buf_size - 1].owner); + } else { + candidate += sizeof(uint32_t) + sizeof(uint16_t) + + knot_rdataset_at(&ctx->rrset_buf[ctx->rrset_buf_size - 1].rrs, tmp_phase)->len; + } + if (candidate > max_size) { + return; + } + *realsize = candidate; + if (candidate >= thresh_size) { + return; + } + tmp_phase++; + } +} + +void serialize_chunk(serialize_ctx_t *ctx, uint8_t *dst_chunk, size_t chunk_size) +{ + wire_ctx_t wire = wire_ctx_init(dst_chunk, chunk_size); + + for (size_t i = 0; ; ) { + if (ctx->rrset_phase >= ctx->rrset_buf[i].rrs.count) { + if (++i >= ctx->rrset_buf_size) { + break; + } + ctx->rrset_phase = SERIALIZE_RRSET_INIT; + } + if (ctx->rrset_phase == SERIALIZE_RRSET_INIT) { + int size = knot_dname_to_wire(wire.position, ctx->rrset_buf[i].owner, + wire_ctx_available(&wire)); + if (size < 0 || wire_ctx_available(&wire) < size + 3 * sizeof(uint16_t)) { + break; + } + wire_ctx_skip(&wire, size); + wire_ctx_write_u16(&wire, ctx->rrset_buf[i].type); + wire_ctx_write_u16(&wire, ctx->rrset_buf[i].rclass); + wire_ctx_write_u16(&wire, ctx->rrset_buf[i].rrs.count); + } else { + const knot_rdata_t *rr = knot_rdataset_at(&ctx->rrset_buf[i].rrs, + ctx->rrset_phase); + assert(rr); + uint16_t rdlen = rr->len; + if (wire_ctx_available(&wire) < sizeof(uint32_t) + sizeof(uint16_t) + rdlen) { + break; + } + // Compatibility, but one TTL per rrset would be enough. + wire_ctx_write_u32(&wire, ctx->rrset_buf[i].ttl); + wire_ctx_write_u16(&wire, rdlen); + wire_ctx_write(&wire, rr->data, rdlen); + } + ctx->rrset_phase++; + } + assert(wire.error == KNOT_EOK); +} + +bool serialize_unfinished(serialize_ctx_t *ctx) +{ + return ctx->changeset_phase < PHASE_END; +} + +int serialize_deinit(serialize_ctx_t *ctx) +{ + if (ctx->it.node != NULL) { + changeset_iter_clear(&ctx->it); + } + if (ctx->zit.tree != NULL) { + zone_tree_it_free(&ctx->zit); + } + ptrnode_t *n, *next; + WALK_LIST_DELSAFE(n, next, ctx->free_rdatasets) { + free(n->d); + rem_node(&n->n); + free(n); + } + int ret = ctx->ret; + free(ctx); + return ret; +} + +static uint64_t rrset_binary_size(const knot_rrset_t *rrset) +{ + if (rrset == NULL || rrset->rrs.count == 0) { + return 0; + } + + // Owner size + type + class + RR count. + uint64_t size = knot_dname_size(rrset->owner) + 3 * sizeof(uint16_t); + + // RRs. + knot_rdata_t *rr = rrset->rrs.rdata; + for (uint16_t i = 0; i < rrset->rrs.count; i++) { + // TTL + RR size + RR. + size += sizeof(uint32_t) + sizeof(uint16_t) + rr->len; + rr = knot_rdataset_next(rr); + } + + return size; +} + +static size_t node_diff_size(zone_node_t *node) +{ + size_t res = 0; + knot_rrset_t rr, counter_rr; + for (int i = 0; i < node->rrset_count; i++) { + rr = node_rrset_at(node, i); + counter_rr = node_rrset(binode_counterpart(node), rr.type); + if (!knot_rrset_equal(&rr, &counter_rr, true)) { + res += rrset_binary_size(&rr); + } + } + return res; +} + +size_t zone_diff_serialized_size(zone_diff_t diff) +{ + size_t res = 0; + for (int i = 0; i < 2; i++) { + zone_diff_reverse(&diff); + zone_tree_it_t it = { 0 }; + int ret = zone_tree_it_double_begin(&diff.nodes, diff.nsec3s.trie != NULL ? + &diff.nsec3s : NULL, &it); + if (ret != KNOT_EOK) { + return 0; + } + while (!zone_tree_it_finished(&it)) { + res += node_diff_size(zone_tree_it_val(&it)); + zone_tree_it_next(&it); + } + zone_tree_it_free(&it); + } + return res; +} + +size_t changeset_serialized_size(const changeset_t *ch) +{ + if (ch == NULL) { + return 0; + } + + size_t soa_from_size = rrset_binary_size(ch->soa_from); + size_t soa_to_size = rrset_binary_size(ch->soa_to); + + changeset_iter_t it; + if (ch->remove == NULL) { + changeset_iter_add(&it, ch); + } else { + changeset_iter_all(&it, ch); + } + + size_t change_size = 0; + knot_rrset_t rrset = changeset_iter_next(&it); + while (!knot_rrset_empty(&rrset)) { + change_size += rrset_binary_size(&rrset); + rrset = changeset_iter_next(&it); + } + + changeset_iter_clear(&it); + + return soa_from_size + soa_to_size + change_size; +} + +int serialize_rrset(wire_ctx_t *wire, const knot_rrset_t *rrset) +{ + assert(wire != NULL && rrset != NULL); + + // write owner, type, class, rrcnt + int size = knot_dname_to_wire(wire->position, rrset->owner, + wire_ctx_available(wire)); + if (size < 0 || wire_ctx_available(wire) < size + 3 * sizeof(uint16_t)) { + assert(0); + } + wire_ctx_skip(wire, size); + wire_ctx_write_u16(wire, rrset->type); + wire_ctx_write_u16(wire, rrset->rclass); + wire_ctx_write_u16(wire, rrset->rrs.count); + + for (size_t phase = 0; phase < rrset->rrs.count; phase++) { + const knot_rdata_t *rr = knot_rdataset_at(&rrset->rrs, phase); + assert(rr); + uint16_t rdlen = rr->len; + if (wire_ctx_available(wire) < sizeof(uint32_t) + sizeof(uint16_t) + rdlen) { + assert(0); + } + wire_ctx_write_u32(wire, rrset->ttl); + wire_ctx_write_u16(wire, rdlen); + wire_ctx_write(wire, rr->data, rdlen); + assert(wire->error == KNOT_EOK); + } + + return KNOT_EOK; +} + +int deserialize_rrset(wire_ctx_t *wire, knot_rrset_t *rrset) +{ + assert(wire != NULL && rrset != NULL); + + // Read owner, rtype, rclass and RR count. + int size = knot_dname_size(wire->position); + if (size < 0) { + assert(0); + } + knot_dname_t *owner = knot_dname_copy(wire->position, NULL); + if (owner == NULL || wire_ctx_available(wire) < size + 3 * sizeof(uint16_t)) { + knot_dname_free(owner, NULL); + return KNOT_EMALF; + } + wire_ctx_skip(wire, size); + uint16_t type = wire_ctx_read_u16(wire); + uint16_t rclass = wire_ctx_read_u16(wire); + uint16_t rrcount = wire_ctx_read_u16(wire); + if (wire->error != KNOT_EOK) { + knot_dname_free(owner, NULL); + return wire->error; + } + if (rrset->owner != NULL) { + if (knot_dname_cmp(owner, rrset->owner) != 0) { + knot_dname_free(owner, NULL); + return KNOT_ESEMCHECK; + } + knot_rrset_clear(rrset, NULL); + } + knot_rrset_init(rrset, owner, type, rclass, 0); + + for (size_t phase = 0; phase < rrcount && wire_ctx_available(wire) > 0; phase++) { + uint32_t ttl = wire_ctx_read_u32(wire); + uint32_t rdata_size = wire_ctx_read_u16(wire); + if (phase == 0) { + rrset->ttl = ttl; + } + if (wire->error != KNOT_EOK || + wire_ctx_available(wire) < rdata_size || + knot_rrset_add_rdata(rrset, wire->position, rdata_size, + NULL) != KNOT_EOK) { + knot_rrset_clear(rrset, NULL); + return KNOT_EMALF; + } + wire_ctx_skip(wire, rdata_size); + assert(wire->error == KNOT_EOK); + } + + return KNOT_EOK; +} + +size_t rrset_serialized_size(const knot_rrset_t *rrset) +{ + if (rrset == NULL) { + return 0; + } + + // Owner size + type + class + RR count. + size_t size = knot_dname_size(rrset->owner) + 3 * sizeof(uint16_t); + + for (uint16_t i = 0; i < rrset->rrs.count; i++) { + const knot_rdata_t *rr = knot_rdataset_at(&rrset->rrs, i); + assert(rr); + // TTL + RR size + RR. + size += sizeof(uint32_t) + sizeof(uint16_t) + rr->len; + } + + return size; +} diff --git a/src/knot/journal/serialization.h b/src/knot/journal/serialization.h new file mode 100644 index 0000000..621dcdb --- /dev/null +++ b/src/knot/journal/serialization.h @@ -0,0 +1,169 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> + +#include "libknot/rrset.h" +#include "libknot/rrtype/soa.h" +#include "knot/updates/changesets.h" +#include "contrib/wire_ctx.h" + +typedef struct zone_diff { + zone_tree_t nodes; + zone_tree_t nsec3s; + zone_node_t *apex; +} zone_diff_t; + +inline static void zone_diff_reverse(zone_diff_t *diff) +{ + diff->nodes.flags ^= ZONE_TREE_BINO_SECOND; + diff->nsec3s.flags ^= ZONE_TREE_BINO_SECOND; + diff->apex = binode_counterpart(diff->apex); +} + +inline static void zone_diff_from_zone(zone_diff_t *diff, const zone_contents_t *z) +{ + diff->nodes = *z->nodes; + if (z->nsec3_nodes != NULL) { + diff->nsec3s = *z->nsec3_nodes; + } else { + memset(&diff->nsec3s, 0, sizeof(diff->nsec3s)); + } + diff->apex = z->apex; +} + +inline static uint32_t zone_diff_to(const zone_diff_t *diff) +{ + return knot_soa_serial(node_rdataset(diff->apex, KNOT_RRTYPE_SOA)->rdata); +} + +inline static uint32_t zone_diff_from(const zone_diff_t *diff) +{ + return knot_soa_serial(node_rdataset(binode_counterpart(diff->apex), KNOT_RRTYPE_SOA)->rdata); +} + +typedef struct serialize_ctx serialize_ctx_t; + +/*! + * \brief Init serialization context. + * + * \param ch Changeset to be serialized. + * + * \return Context. + */ +serialize_ctx_t *serialize_init(const changeset_t *ch); + +/*! + * \brief Init serialization context. + * + * \param z Zone to be serialized like zone-in-journal changeset. + * + * \return Context. + */ +serialize_ctx_t *serialize_zone_init(const zone_contents_t *z); + +/*! + * \brief Init serialization context. + * + * \param z Zone with binodes being updated. + * + * \return Context. + */ +serialize_ctx_t *serialize_zone_diff_init(const zone_diff_t *z); + +/*! + * \brief Pre-check and space computation before serializing a chunk. + * + * \note This MUST be called before each serialize_chunk() ! + * + * \param ctx Serializing context. + * \param thresh_size Optimal size of next chunk. + * \param max_size Maximum size of next chunk. + * \param realsize Output: real exact size of next chunk. + */ +void serialize_prepare(serialize_ctx_t *ctx, size_t thresh_size, + size_t max_size, size_t *realsize); + +/*! + * \brief Perform one step of serializiation: fill one chunk. + * + * \param ctx Serializing context. + * \param chunk Pointer on allocated memory to be serialized into. + * \param chunk_size Its size. It MUST be the same as returned from serialize_prepare(). + */ +void serialize_chunk(serialize_ctx_t *ctx, uint8_t *chunk, size_t chunk_size); + +/*! \brief Tells if there remains something of the changeset + * to be serialized into next chunk(s) yet. */ +bool serialize_unfinished(serialize_ctx_t *ctx); + +/*! + * \brief Free serialization context. + * + * \return KNOT_E* if there were errors during serialization. + */ +int serialize_deinit(serialize_ctx_t *ctx); + +/*! + * \brief Returns size of serialized changeset from zone diff. + * + * \warning Not accurate! This is an upper bound, suitable for policy enforcement etc. + * + * \param[in] diff Zone diff structure to create changeset from. + * + * \return Size of the resulting changeset. + */ +size_t zone_diff_serialized_size(zone_diff_t diff); + +/*! + * \brief Returns size of changeset in serialized form. + * + * \param[in] ch Changeset whose size we want to compute. + * + * \return Size of the changeset. + */ +size_t changeset_serialized_size(const changeset_t *ch); + +/*! + * \brief Simply serialize RRset w/o any chunking. + * + * \param wire + * \param rrset + * + * \return KNOT_E* + */ +int serialize_rrset(wire_ctx_t *wire, const knot_rrset_t *rrset); + +/*! + * \brief Simply deserialize RRset w/o any chunking. + * + * \param wire + * \param rrset + * + * \return KNOT_E* + */ +int deserialize_rrset(wire_ctx_t *wire, knot_rrset_t *rrset); + +/*! + * \brief Space needed to serialize RRset. + * + * \param rrset RRset. + * + * \return RRset binary size. + */ +size_t rrset_serialized_size(const knot_rrset_t *rrset); 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. diff --git a/src/knot/nameserver/axfr.c b/src/knot/nameserver/axfr.c new file mode 100644 index 0000000..dac4a43 --- /dev/null +++ b/src/knot/nameserver/axfr.c @@ -0,0 +1,225 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <urcu.h> + +#include "contrib/mempattern.h" +#include "contrib/sockaddr.h" +#include "knot/nameserver/axfr.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/log.h" +#include "knot/nameserver/xfr.h" +#include "libknot/libknot.h" + +#define ZONE_NAME(qdata) knot_pkt_qname((qdata)->query) +#define REMOTE(qdata) (struct sockaddr *)knotd_qdata_remote_addr(qdata) + +#define AXFROUT_LOG(priority, qdata, fmt...) \ + ns_log(priority, ZONE_NAME(qdata), LOG_OPERATION_AXFR, \ + LOG_DIRECTION_OUT, REMOTE(qdata), false, fmt) + +/* AXFR context. @note aliasing the generic xfr_proc */ +struct axfr_proc { + struct xfr_proc proc; + trie_it_t *i; + zone_tree_it_t it; + unsigned cur_rrset; +}; + +static int axfr_put_rrsets(knot_pkt_t *pkt, zone_node_t *node, + struct axfr_proc *state) +{ + assert(node != NULL); + + /* Append all RRs. */ + for (unsigned i = state->cur_rrset; i < node->rrset_count; ++i) { + knot_rrset_t rrset = node_rrset_at(node, i); + if (rrset.type == KNOT_RRTYPE_SOA) { + continue; + } + + int ret = knot_pkt_put(pkt, 0, &rrset, KNOT_PF_NOTRUNC | KNOT_PF_ORIGTTL); + if (ret != KNOT_EOK) { + /* If something failed, remember the current RR for later. */ + state->cur_rrset = i; + return ret; + } + if (pkt->size > KNOT_WIRE_PTR_MAX) { + // optimization: once the XFR DNS message is > 16 KiB, compression + // is limited. Better wrap to next message. + state->cur_rrset = i + 1; + return KNOT_ESPACE; + } + } + + state->cur_rrset = 0; + + return KNOT_EOK; +} + +static int axfr_process_node_tree(knot_pkt_t *pkt, const void *item, + struct xfr_proc *state) +{ + assert(item != NULL); + + struct axfr_proc *axfr = (struct axfr_proc*)state; + + int ret = zone_tree_it_begin((zone_tree_t *)item, &axfr->it); // does nothing if already iterating + + /* Put responses. */ + while (ret == KNOT_EOK && !zone_tree_it_finished(&axfr->it)) { + zone_node_t *node = zone_tree_it_val(&axfr->it); + ret = axfr_put_rrsets(pkt, node, axfr); + if (ret == KNOT_EOK) { + zone_tree_it_next(&axfr->it); + } + } + + /* Finished all nodes. */ + if (ret == KNOT_EOK) { + zone_tree_it_free(&axfr->it); + } + return ret; +} + +static void axfr_query_cleanup(knotd_qdata_t *qdata) +{ + struct axfr_proc *axfr = (struct axfr_proc *)qdata->extra->ext; + + zone_tree_it_free(&axfr->it); + ptrlist_free(&axfr->proc.nodes, qdata->mm); + mm_free(qdata->mm, axfr); + + /* Allow zone changes (finished). */ + rcu_read_unlock(); +} + +static int axfr_query_check(knotd_qdata_t *qdata) +{ + NS_NEED_ZONE(qdata, KNOT_RCODE_NOTAUTH); + NS_NEED_AUTH(qdata, ACL_ACTION_TRANSFER); + NS_NEED_ZONE_CONTENTS(qdata); + + return KNOT_STATE_DONE; +} + +static int axfr_query_init(knotd_qdata_t *qdata) +{ + assert(qdata); + + /* Check AXFR query validity. */ + if (axfr_query_check(qdata) == KNOT_STATE_FAIL) { + if (qdata->rcode == KNOT_RCODE_FORMERR) { + return KNOT_EMALF; + } else { + return KNOT_EDENIED; + } + } + + if (zone_get_flag(qdata->extra->zone, ZONE_XFR_FROZEN, false)) { + qdata->rcode = KNOT_RCODE_REFUSED; + qdata->rcode_ede = KNOT_EDNS_EDE_NOT_READY; + return KNOT_EAGAIN; + } + + /* Create transfer processing context. */ + knot_mm_t *mm = qdata->mm; + struct axfr_proc *axfr = mm_alloc(mm, sizeof(struct axfr_proc)); + if (axfr == NULL) { + return KNOT_ENOMEM; + } + memset(axfr, 0, sizeof(struct axfr_proc)); + init_list(&axfr->proc.nodes); + + /* Put data to process. */ + xfr_stats_begin(&axfr->proc.stats); + const zone_contents_t *contents = qdata->extra->contents; + /* Must be non-NULL for the first message. */ + assert(contents); + ptrlist_add(&axfr->proc.nodes, contents->nodes, mm); + /* Put NSEC3 data if exists. */ + if (!zone_tree_is_empty(contents->nsec3_nodes)) { + ptrlist_add(&axfr->proc.nodes, contents->nsec3_nodes, mm); + } + + /* Set up cleanup callback. */ + qdata->extra->ext = axfr; + qdata->extra->ext_cleanup = &axfr_query_cleanup; + + /* No zone changes during multipacket answer (unlocked in axfr_answer_cleanup) */ + rcu_read_lock(); + + return KNOT_EOK; +} + +int axfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt == NULL || qdata == NULL) { + return KNOT_STATE_FAIL; + } + + /* AXFR over UDP isn't allowed, respond with NOTIMPL. */ + if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) { + qdata->rcode = KNOT_RCODE_NOTIMPL; + return KNOT_STATE_FAIL; + } + + /* Initialize on first call. */ + struct axfr_proc *axfr = qdata->extra->ext; + if (axfr == NULL) { + int ret = axfr_query_init(qdata); + axfr = qdata->extra->ext; + switch (ret) { + case KNOT_EOK: /* OK */ + AXFROUT_LOG(LOG_INFO, qdata, "started, serial %u", + zone_contents_serial(qdata->extra->contents)); + break; + case KNOT_EDENIED: /* Not authorized, already logged. */ + return KNOT_STATE_FAIL; + case KNOT_EMALF: /* Malformed query. */ + AXFROUT_LOG(LOG_DEBUG, qdata, "malformed query"); + return KNOT_STATE_FAIL; + case KNOT_EAGAIN: /* Outgoing AXFR temporarily disabled. */ + AXFROUT_LOG(LOG_INFO, qdata, "outgoing AXFR frozen"); + return KNOT_STATE_FAIL; + default: + AXFROUT_LOG(LOG_ERR, qdata, "failed to start (%s)", + knot_strerror(ret)); + return KNOT_STATE_FAIL; + } + } + + /* Reserve space for TSIG. */ + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + /* Answer current packet (or continue). */ + ret = xfr_process_list(pkt, &axfr_process_node_tree, qdata); + switch (ret) { + case KNOT_ESPACE: /* Couldn't write more, send packet and continue. */ + return KNOT_STATE_PRODUCE; /* Check for more. */ + case KNOT_EOK: /* Last response. */ + xfr_stats_end(&axfr->proc.stats); + xfr_log_finished(ZONE_NAME(qdata), LOG_OPERATION_AXFR, LOG_DIRECTION_OUT, + REMOTE(qdata), false, &axfr->proc.stats); + return KNOT_STATE_DONE; + default: /* Generic error. */ + AXFROUT_LOG(LOG_ERR, qdata, "failed (%s)", knot_strerror(ret)); + return KNOT_STATE_FAIL; + } +} diff --git a/src/knot/nameserver/axfr.h b/src/knot/nameserver/axfr.h new file mode 100644 index 0000000..81fcad8 --- /dev/null +++ b/src/knot/nameserver/axfr.h @@ -0,0 +1,27 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/nameserver/process_query.h" +#include "libknot/packet/pkt.h" + +/*! + * \brief Process an AXFR query message. + * + * \return KNOT_STATE_* processing states + */ +int axfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata); diff --git a/src/knot/nameserver/chaos.c b/src/knot/nameserver/chaos.c new file mode 100644 index 0000000..b83e2f5 --- /dev/null +++ b/src/knot/nameserver/chaos.c @@ -0,0 +1,145 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <strings.h> +#include <stdlib.h> + +#include "knot/nameserver/chaos.h" +#include "knot/conf/conf.h" +#include "libknot/libknot.h" + +#define WISH "Knot DNS developers wish you " +#define HOPE "Knot DNS developers hope you " + +static const char *wishes[] = { + HOPE "have all your important life questions answered without SERVFAIL.", + WISH "many wonderful people in your domain.", + WISH "non-empty lymph nodes.", + HOPE "resolve the . of your problems.", + WISH "long enough TTL.", + HOPE "become authoritative master in your domain.", + HOPE "always find useful PTR in CHAOS.", + "Canonical name is known to both DNS experts and Ubuntu users.", + HOPE "never forget both your name and address.", + "Don't fix broken CNAME chains with glue!", + WISH "no Additional section in your TODO list.", + HOPE "won't find surprising news in today's journal.", + HOPE "perform rollover often just when playing roulette.", + HOPE "get notified before your domain registration expires.", +}; + +#undef WISH +#undef HOPE + +static const char *get_txt_response_string(knot_pkt_t *response) +{ + char qname[32]; + if (knot_dname_to_str(qname, knot_pkt_qname(response), sizeof(qname)) == NULL) { + return NULL; + } + + const char *response_str = NULL; + + /* Allow hostname.bind. for compatibility. */ + if (strcasecmp("id.server.", qname) == 0 || + strcasecmp("hostname.bind.", qname) == 0) { + conf_val_t val = conf_get(conf(), C_SRV, C_IDENT); + if (val.code == KNOT_EOK) { + response_str = conf_str(&val); // Can be NULL! + } else { + response_str = conf()->hostname; + } + /* Allow version.bind. for compatibility. */ + } else if (strcasecmp("version.server.", qname) == 0 || + strcasecmp("version.bind.", qname) == 0) { + conf_val_t val = conf_get(conf(), C_SRV, C_VERSION); + if (val.code == KNOT_EOK) { + response_str = conf_str(&val); // Can be NULL! + } else { + response_str = "Knot DNS " PACKAGE_VERSION; + } + } else if (strcasecmp("fortune.", qname) == 0) { + conf_val_t val = conf_get(conf(), C_SRV, C_VERSION); + if (val.code != KNOT_EOK) { + uint16_t wishno = knot_wire_get_id(response->wire) % + (sizeof(wishes) / sizeof(wishes[0])); + response_str = wishes[wishno]; + } + } + + return response_str; +} + +static int create_txt_rrset(knot_rrset_t *rrset, const knot_dname_t *owner, + const char *response_str, knot_mm_t *mm) +{ + /* Truncate response to one TXT label. */ + size_t response_len = strlen(response_str); + if (response_len > UINT8_MAX) { + response_len = UINT8_MAX; + } + + knot_dname_t *rowner = knot_dname_copy(owner, mm); + if (rowner == NULL) { + return KNOT_ENOMEM; + } + + knot_rrset_init(rrset, rowner, KNOT_RRTYPE_TXT, KNOT_CLASS_CH, 0); + uint8_t rdata[response_len + 1]; + + rdata[0] = response_len; + memcpy(&rdata[1], response_str, response_len); + + int ret = knot_rrset_add_rdata(rrset, rdata, response_len + 1, mm); + if (ret != KNOT_EOK) { + knot_dname_free(rrset->owner, mm); + return ret; + } + + return KNOT_EOK; +} + +static int answer_txt(knot_pkt_t *response) +{ + const char *response_str = get_txt_response_string(response); + if (response_str == NULL || response_str[0] == '\0') { + return KNOT_RCODE_REFUSED; + } + + knot_rrset_t rrset; + int ret = create_txt_rrset(&rrset, knot_pkt_qname(response), + response_str, &response->mm); + if (ret != KNOT_EOK) { + return KNOT_RCODE_SERVFAIL; + } + + int result = knot_pkt_put(response, 0, &rrset, KNOT_PF_FREE); + if (result != KNOT_EOK) { + knot_rrset_clear(&rrset, &response->mm); + return KNOT_RCODE_SERVFAIL; + } + + return KNOT_RCODE_NOERROR; +} + +int knot_chaos_answer(knot_pkt_t *pkt) +{ + if (knot_pkt_qtype(pkt) != KNOT_RRTYPE_TXT) { + return KNOT_RCODE_REFUSED; + } + + return answer_txt(pkt); +} diff --git a/src/knot/nameserver/chaos.h b/src/knot/nameserver/chaos.h new file mode 100644 index 0000000..f875abe --- /dev/null +++ b/src/knot/nameserver/chaos.h @@ -0,0 +1,24 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" + +/*! + * \brief Create a response for a given query in the CHAOS class. + */ +int knot_chaos_answer(knot_pkt_t *pkt); diff --git a/src/knot/nameserver/internet.c b/src/knot/nameserver/internet.c new file mode 100644 index 0000000..51bde97 --- /dev/null +++ b/src/knot/nameserver/internet.c @@ -0,0 +1,728 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "libknot/libknot.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-nsec.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/nsec_proofs.h" +#include "knot/nameserver/query_module.h" +#include "knot/zone/serial.h" +#include "contrib/mempattern.h" + +/*! \brief Check if given node was already visited. */ +static int wildcard_has_visited(knotd_qdata_t *qdata, const zone_node_t *node) +{ + struct wildcard_hit *item; + WALK_LIST(item, qdata->extra->wildcards) { + if (item->node == node) { + return true; + } + } + return false; +} + +/*! \brief Mark given node as visited. */ +static int wildcard_visit(knotd_qdata_t *qdata, const zone_node_t *node, + const zone_node_t *prev, const knot_dname_t *sname) +{ + assert(qdata); + assert(node); + + if (node->flags & NODE_FLAGS_NONAUTH) { + return KNOT_EOK; + } + + knot_mm_t *mm = qdata->mm; + struct wildcard_hit *item = mm_alloc(mm, sizeof(struct wildcard_hit)); + item->node = node; + item->prev = prev; + item->sname = sname; + add_tail(&qdata->extra->wildcards, (node_t *)item); + return KNOT_EOK; +} + +/*! \brief Synthesizes a CNAME RR from a DNAME. */ +static int dname_cname_synth(const knot_rrset_t *dname_rr, + const knot_dname_t *qname, + knot_rrset_t *cname_rrset, + knot_mm_t *mm) +{ + if (cname_rrset == NULL) { + return KNOT_EINVAL; + } + knot_dname_t *owner_copy = knot_dname_copy(qname, mm); + if (owner_copy == NULL) { + return KNOT_ENOMEM; + } + knot_rrset_init(cname_rrset, owner_copy, KNOT_RRTYPE_CNAME, dname_rr->rclass, + dname_rr->ttl); + + /* Replace last labels of qname with DNAME. */ + const knot_dname_t *dname_wire = dname_rr->owner; + const knot_dname_t *dname_tgt = knot_dname_target(dname_rr->rrs.rdata); + size_t labels = knot_dname_labels(dname_wire, NULL); + knot_dname_t *cname = knot_dname_replace_suffix(qname, labels, dname_tgt, mm); + if (cname == NULL) { + knot_dname_free(owner_copy, mm); + return KNOT_ENOMEM; + } + + /* Store DNAME into RDATA. */ + size_t cname_size = knot_dname_size(cname); + uint8_t cname_rdata[cname_size]; + memcpy(cname_rdata, cname, cname_size); + knot_dname_free(cname, mm); + + int ret = knot_rrset_add_rdata(cname_rrset, cname_rdata, cname_size, mm); + if (ret != KNOT_EOK) { + knot_dname_free(owner_copy, mm); + return ret; + } + + return KNOT_EOK; +} + +/*! + * \brief Checks if the name created by replacing the owner of \a dname_rrset + * in the \a qname by the DNAME's target would be longer than allowed. + */ +static bool dname_cname_cannot_synth(const knot_rrset_t *rrset, const knot_dname_t *qname) +{ + if (knot_dname_labels(qname, NULL) - knot_dname_labels(rrset->owner, NULL) + + knot_dname_labels(knot_dname_target(rrset->rrs.rdata), NULL) > KNOT_DNAME_MAXLABELS) { + return true; + } else if (knot_dname_size(qname) - knot_dname_size(rrset->owner) + + knot_dname_size(knot_dname_target(rrset->rrs.rdata)) > KNOT_DNAME_MAXLEN) { + return true; + } else { + return false; + } +} + +/*! \brief DNSSEC both requested & available. */ +static bool have_dnssec(knotd_qdata_t *qdata) +{ + return knot_pkt_has_dnssec(qdata->query) && + qdata->extra->contents->dnssec; +} + +/*! \brief This is a wildcard-covered or any other terminal node for QNAME. + * e.g. positive answer. + */ +static int put_answer(knot_pkt_t *pkt, uint16_t type, knotd_qdata_t *qdata) +{ + /* Wildcard expansion or exact match, either way RRSet owner is + * is QNAME. We can fake name synthesis by setting compression hint to + * QNAME position. Just need to check if we're answering QNAME and not + * a CNAME target. + */ + uint16_t compr_hint = KNOT_COMPR_HINT_NONE; + if (pkt->rrset_count == 0) { /* Guaranteed first answer. */ + compr_hint = KNOT_COMPR_HINT_QNAME; + } + + unsigned put_rr_flags = (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) ? + KNOT_PF_NULL : KNOT_PF_NOTRUNC; + put_rr_flags |= KNOT_PF_ORIGTTL; + + knot_rrset_t rrsigs = node_rrset(qdata->extra->node, KNOT_RRTYPE_RRSIG); + knot_rrset_t rrset; + switch (type) { + case KNOT_RRTYPE_ANY: /* Put one RRSet, not all. */ + rrset = node_rrset_at(qdata->extra->node, 0); + break; + case KNOT_RRTYPE_RRSIG: /* Put some RRSIGs, not all. */ + if (!knot_rrset_empty(&rrsigs)) { + knot_rrset_init(&rrset, rrsigs.owner, rrsigs.type, rrsigs.rclass, rrsigs.ttl); + int ret = knot_synth_rrsig(KNOT_RRTYPE_ANY, &rrsigs.rrs, &rrset.rrs, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } else { + knot_rrset_init_empty(&rrset); + } + break; + default: /* Single RRSet of given type. */ + rrset = node_rrset(qdata->extra->node, type); + break; + } + + if (knot_rrset_empty(&rrset)) { + return KNOT_EOK; + } + + return process_query_put_rr(pkt, qdata, &rrset, &rrsigs, compr_hint, put_rr_flags); +} + +/*! \brief Puts optional SOA RRSet to the Authority section of the response. */ +static int put_authority_soa(knot_pkt_t *pkt, knotd_qdata_t *qdata, + const zone_contents_t *zone) +{ + knot_rrset_t soa = node_rrset(zone->apex, KNOT_RRTYPE_SOA); + knot_rrset_t rrsigs = node_rrset(zone->apex, KNOT_RRTYPE_RRSIG); + return process_query_put_rr(pkt, qdata, &soa, &rrsigs, + KNOT_COMPR_HINT_NONE, + KNOT_PF_NOTRUNC | KNOT_PF_SOAMINTTL); +} + +/*! \brief Put the delegation NS RRSet to the Authority section. */ +static int put_delegation(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + /* Find closest delegation point. */ + while (!(qdata->extra->node->flags & NODE_FLAGS_DELEG)) { + qdata->extra->node = node_parent(qdata->extra->node); + } + + /* Insert NS record. */ + knot_rrset_t rrset = node_rrset(qdata->extra->node, KNOT_RRTYPE_NS); + knot_rrset_t rrsigs = node_rrset(qdata->extra->node, KNOT_RRTYPE_RRSIG); + return process_query_put_rr(pkt, qdata, &rrset, &rrsigs, + KNOT_COMPR_HINT_NONE, 0); +} + +static int put_nsec3_bitmap(const zone_node_t *for_node, knot_pkt_t *pkt, + knotd_qdata_t *qdata, uint32_t flags) +{ + const zone_node_t *node = node_nsec3_get(for_node); + if (node == NULL) { + return KNOT_EOK; + } + + knot_rrset_t nsec3 = node_rrset(node, KNOT_RRTYPE_NSEC3); + if (knot_rrset_empty(&nsec3)) { + return KNOT_EOK; + } + + knot_rrset_t rrsig = node_rrset(node, KNOT_RRTYPE_RRSIG); + return process_query_put_rr(pkt, qdata, &nsec3, &rrsig, + KNOT_COMPR_HINT_NONE, flags); +} + +/*! \brief Put additional records for given RR. */ +static int put_additional(knot_pkt_t *pkt, const knot_rrset_t *rr, + knotd_qdata_t *qdata, knot_rrinfo_t *info, int state) +{ + if (rr->additional == NULL) { + return KNOT_EOK; + } + + /* Valid types for ADDITIONALS insertion. */ + /* \note Not resolving CNAMEs as MX/NS name must not be an alias. (RFC2181/10.3) */ + static uint16_t ar_type_list[] = { KNOT_RRTYPE_A, KNOT_RRTYPE_AAAA, KNOT_RRTYPE_SVCB }; + static const int ar_type_count_default = 2; + + int ret = KNOT_EOK; + + additional_t *additional = (additional_t *)rr->additional; + + /* Iterate over the additionals. */ + for (uint16_t i = 0; i < additional->count; i++) { + glue_t *glue = &additional->glues[i]; + uint32_t flags = KNOT_PF_NULL; + + /* Optional glue doesn't cause truncation. (RFC 1034/4.3.2 step 3b). */ + if (state != KNOTD_IN_STATE_DELEG || glue->optional) { + flags |= KNOT_PF_NOTRUNC; + } + + int ar_type_count = ar_type_count_default, ar_present = 0; + if (rr->type == KNOT_RRTYPE_SVCB || rr->type == KNOT_RRTYPE_HTTPS) { + ar_type_list[ar_type_count++] = rr->type; + } + + uint16_t hint = knot_compr_hint(info, KNOT_COMPR_HINT_RDATA + + glue->ns_pos); + const zone_node_t *gluenode = glue_node(glue, qdata->extra->node); + knot_rrset_t rrsigs = node_rrset(gluenode, KNOT_RRTYPE_RRSIG); + for (int k = 0; k < ar_type_count; ++k) { + knot_rrset_t rrset = node_rrset(gluenode, ar_type_list[k]); + if (knot_rrset_empty(&rrset)) { + continue; + } + ret = process_query_put_rr(pkt, qdata, &rrset, &rrsigs, + hint, flags); + if (ret != KNOT_EOK) { + break; + } + ar_present++; + } + + if ((rr->type == KNOT_RRTYPE_SVCB || rr->type == KNOT_RRTYPE_HTTPS) && + ar_present < ar_type_count && have_dnssec(qdata)) { + // it would be nicer to have this in solve_additional_dnssec, but + // it seems infeasible to transfer all the context there + + // adding an NSEC(3) record proving non-existence of some of the + // glue with its bitmap + if (knot_is_nsec3_enabled(qdata->extra->contents)) { + ret = put_nsec3_bitmap(gluenode, pkt, qdata, flags); + } else { + knot_rrset_t nsec = node_rrset(gluenode, KNOT_RRTYPE_NSEC); + if (!knot_rrset_empty(&nsec)) { + ret = process_query_put_rr(pkt, qdata, &nsec, &rrsigs, + KNOT_COMPR_HINT_NONE, flags); + } + } + if (ret != KNOT_EOK) { + break; + } + } + } + + return ret; +} + +static int follow_cname(knot_pkt_t *pkt, uint16_t rrtype, knotd_qdata_t *qdata) +{ + /* CNAME chain processing limit. */ + if (++qdata->extra->cname_chain > CNAME_CHAIN_MAX) { + qdata->extra->node = NULL; + return KNOTD_IN_STATE_HIT; + } + + const zone_node_t *cname_node = qdata->extra->node; + knot_rrset_t cname_rr = node_rrset(qdata->extra->node, rrtype); + knot_rrset_t rrsigs = node_rrset(qdata->extra->node, KNOT_RRTYPE_RRSIG); + + assert(!knot_rrset_empty(&cname_rr)); + + /* Check whether RR is already in the packet. */ + uint16_t flags = KNOT_PF_CHECKDUP; + + /* Now, try to put CNAME to answer. */ + uint16_t rr_count_before = pkt->rrset_count; + int ret = process_query_put_rr(pkt, qdata, &cname_rr, &rrsigs, 0, flags); + switch (ret) { + case KNOT_EOK: break; + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; + default: return KNOTD_IN_STATE_ERROR; + } + + /* Synthesize CNAME if followed DNAME. */ + if (rrtype == KNOT_RRTYPE_DNAME) { + if (dname_cname_cannot_synth(&cname_rr, qdata->name)) { + qdata->rcode = KNOT_RCODE_YXDOMAIN; + } else { + knot_rrset_t dname_rr = cname_rr; + ret = dname_cname_synth(&dname_rr, qdata->name, + &cname_rr, &pkt->mm); + if (ret != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_SERVFAIL; + return KNOTD_IN_STATE_ERROR; + } + ret = process_query_put_rr(pkt, qdata, &cname_rr, NULL, 0, KNOT_PF_FREE); + switch (ret) { + case KNOT_EOK: break; + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; + default: return KNOTD_IN_STATE_ERROR; + } + if (knot_pkt_qtype(pkt) == KNOT_RRTYPE_CNAME) { + /* Synthesized CNAME is a perfect answer to query. */ + return KNOTD_IN_STATE_HIT; + } + } + } + + /* Check if RR count increased. */ + if (pkt->rrset_count <= rr_count_before) { + qdata->extra->node = NULL; /* Act as if the name leads to nowhere. */ + return KNOTD_IN_STATE_HIT; + } + + /* If node is a wildcard, follow only if we didn't visit the same node + * earlier, as that would mean a CNAME loop. */ + if (knot_dname_is_wildcard(cname_node->owner)) { + + /* Check if is not in wildcard nodes (loop). */ + if (wildcard_has_visited(qdata, cname_node)) { + qdata->extra->node = NULL; /* Act as if the name leads to nowhere. */ + + if (wildcard_visit(qdata, cname_node, qdata->extra->previous, qdata->name) != KNOT_EOK) { // in case of loop, re-add this cname_node because it might have different qdata->name + return KNOTD_IN_STATE_ERROR; + } + return KNOTD_IN_STATE_HIT; + } + + /* Put to wildcard node list. */ + if (wildcard_visit(qdata, cname_node, qdata->extra->previous, qdata->name) != KNOT_EOK) { + return KNOTD_IN_STATE_ERROR; + } + } + + /* Now follow the next CNAME TARGET. */ + qdata->name = knot_cname_name(cname_rr.rrs.rdata); + + return KNOTD_IN_STATE_FOLLOW; +} + +static int name_found(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + uint16_t qtype = knot_pkt_qtype(pkt); + + /* DS query at DP is answered normally, but everything else at/below DP + * triggers referral response. */ + if (((qdata->extra->node->flags & NODE_FLAGS_DELEG) && qtype != KNOT_RRTYPE_DS) || + (qdata->extra->node->flags & NODE_FLAGS_NONAUTH)) { + return KNOTD_IN_STATE_DELEG; + } + + if (node_rrtype_exists(qdata->extra->node, KNOT_RRTYPE_CNAME) + && qtype != KNOT_RRTYPE_CNAME + && qtype != KNOT_RRTYPE_RRSIG + && qtype != KNOT_RRTYPE_NSEC + && qtype != KNOT_RRTYPE_ANY) { + return follow_cname(pkt, KNOT_RRTYPE_CNAME, qdata); + } + + uint16_t old_rrcount = pkt->rrset_count; + int ret = put_answer(pkt, qtype, qdata); + if (ret != KNOT_EOK) { + if (ret == KNOT_ESPACE && (qdata->params->proto == KNOTD_QUERY_PROTO_UDP)) { + return KNOTD_IN_STATE_TRUNC; + } else { + return KNOTD_IN_STATE_ERROR; + } + } + + /* Check for NODATA (=0 RRs added). */ + if (old_rrcount == pkt->rrset_count) { + return KNOTD_IN_STATE_NODATA; + } else { + return KNOTD_IN_STATE_HIT; + } +} + +static int name_not_found(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + /* Name is covered by wildcard. */ + if (qdata->extra->encloser->flags & NODE_FLAGS_WILDCARD_CHILD) { + /* Find wildcard child in the zone. */ + const zone_node_t *wildcard_node = + zone_contents_find_wildcard_child( + qdata->extra->contents, qdata->extra->encloser); + + qdata->extra->node = wildcard_node; + assert(qdata->extra->node != NULL); + + /* Follow expanded wildcard. */ + int next_state = name_found(pkt, qdata); + + /* Put to wildcard node list. */ + if (wildcard_has_visited(qdata, wildcard_node)) { + return next_state; + } + if (wildcard_visit(qdata, wildcard_node, qdata->extra->previous, qdata->name) != KNOT_EOK) { + next_state = KNOTD_IN_STATE_ERROR; + } + + return next_state; + } + + /* Name is under DNAME, use it for substitution. */ + bool encloser_auth = !(qdata->extra->encloser->flags & (NODE_FLAGS_NONAUTH | NODE_FLAGS_DELEG)); + knot_rrset_t dname_rrset = node_rrset(qdata->extra->encloser, KNOT_RRTYPE_DNAME); + if (encloser_auth && !knot_rrset_empty(&dname_rrset)) { + qdata->extra->node = qdata->extra->encloser; /* Follow encloser as new node. */ + return follow_cname(pkt, KNOT_RRTYPE_DNAME, qdata); + } + + /* Look up an authoritative encloser or its parent. */ + const zone_node_t *node = qdata->extra->encloser; + while (node->rrset_count == 0 || node->flags & NODE_FLAGS_NONAUTH) { + node = node_parent(node); + assert(node); + } + + /* Name is below delegation. */ + if ((node->flags & NODE_FLAGS_DELEG)) { + qdata->extra->node = node; + return KNOTD_IN_STATE_DELEG; + } + + return KNOTD_IN_STATE_MISS; +} + +static int solve_name(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + int ret = zone_contents_find_dname(qdata->extra->contents, qdata->name, + &qdata->extra->node, &qdata->extra->encloser, + &qdata->extra->previous); + + switch (ret) { + case ZONE_NAME_FOUND: + return name_found(pkt, qdata); + case ZONE_NAME_NOT_FOUND: + return name_not_found(pkt, qdata); + case KNOT_EOUTOFZONE: + assert(state == KNOTD_IN_STATE_FOLLOW); /* CNAME/DNAME chain only. */ + return KNOTD_IN_STATE_HIT; + default: + return KNOTD_IN_STATE_ERROR; + } +} + +static int solve_answer(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, void *ctx) +{ + int old_state = state; + + /* Do not solve if already solved, e.g. in a module. */ + if (state == KNOTD_IN_STATE_HIT) { + return state; + } + + /* Get answer to QNAME. */ + state = solve_name(state, pkt, qdata); + + /* Promote NODATA from a module if nothing found in zone. */ + if (state == KNOTD_IN_STATE_MISS && old_state == KNOTD_IN_STATE_NODATA) { + state = old_state; + } + + /* Is authoritative answer unless referral. + * Must check before we chase the CNAME chain. */ + if (state != KNOTD_IN_STATE_DELEG) { + knot_wire_set_aa(pkt->wire); + } + + /* Additional resolving for CNAME/DNAME chain. */ + while (state == KNOTD_IN_STATE_FOLLOW) { + state = solve_name(state, pkt, qdata); + } + + return state; +} + +static int solve_answer_dnssec(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, void *ctx) +{ + /* RFC4035, section 3.1 RRSIGs for RRs in ANSWER are mandatory. */ + int ret = nsec_append_rrsigs(pkt, qdata, false); + switch (ret) { + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; + case KNOT_EOK: return state; + default: return KNOTD_IN_STATE_ERROR; + } +} + +static int solve_authority(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, void *ctx) +{ + int ret = KNOT_ERROR; + const zone_contents_t *zone_contents = qdata->extra->contents; + + switch (state) { + case KNOTD_IN_STATE_HIT: /* Positive response. */ + ret = KNOT_EOK; + break; + case KNOTD_IN_STATE_MISS: /* MISS, set NXDOMAIN RCODE. */ + qdata->rcode = KNOT_RCODE_NXDOMAIN; + ret = put_authority_soa(pkt, qdata, zone_contents); + break; + case KNOTD_IN_STATE_NODATA: /* NODATA append AUTHORITY SOA. */ + ret = put_authority_soa(pkt, qdata, zone_contents); + break; + case KNOTD_IN_STATE_DELEG: /* Referral response. */ + ret = put_delegation(pkt, qdata); + break; + case KNOTD_IN_STATE_TRUNC: /* Truncated ANSWER. */ + ret = KNOT_ESPACE; + break; + case KNOTD_IN_STATE_ERROR: /* Error resolving ANSWER. */ + break; + default: + assert(0); + break; + } + + /* Evaluate final state. */ + switch (ret) { + case KNOT_EOK: return state; /* Keep current state. */ + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; /* Truncated. */ + default: return KNOTD_IN_STATE_ERROR; /* Error. */ + } +} + +static int solve_authority_dnssec(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, void *ctx) +{ + int ret = KNOT_ERROR; + + /* Authenticated denial of existence. */ + switch (state) { + case KNOTD_IN_STATE_HIT: ret = KNOT_EOK; break; + case KNOTD_IN_STATE_MISS: ret = nsec_prove_nxdomain(pkt, qdata); break; + case KNOTD_IN_STATE_NODATA: ret = nsec_prove_nodata(pkt, qdata); break; + case KNOTD_IN_STATE_DELEG: ret = nsec_prove_dp_security(pkt, qdata); break; + case KNOTD_IN_STATE_TRUNC: ret = KNOT_ESPACE; break; + case KNOTD_IN_STATE_ERROR: ret = KNOT_ERROR; break; + default: + assert(0); + break; + } + + /* RFC4035 3.1.3 Prove visited wildcards. + * Wildcard expansion applies for Name Error, Wildcard Answer and + * No Data proofs if at one point the search expanded a wildcard node. */ + if (ret == KNOT_EOK) { + ret = nsec_prove_wildcards(pkt, qdata); + } + + /* RFC4035, section 3.1 RRSIGs for RRs in AUTHORITY are mandatory. */ + if (ret == KNOT_EOK) { + ret = nsec_append_rrsigs(pkt, qdata, false); + } + + /* Evaluate final state. */ + switch (ret) { + case KNOT_EOK: return state; /* Keep current state. */ + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; /* Truncated. */ + default: return KNOTD_IN_STATE_ERROR; /* Error. */ + } +} + +static int solve_additional(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, + void *ctx) +{ + int ret = KNOT_EOK, rrset_count = pkt->rrset_count; + + /* Scan all RRs in ANSWER/AUTHORITY. */ + for (int i = 0; i < rrset_count; ++i) { + knot_rrset_t *rr = &pkt->rr[i]; + knot_rrinfo_t *info = &pkt->rr_info[i]; + + /* Skip types for which it doesn't apply. */ + if (!knot_rrtype_additional_needed(rr->type)) { + continue; + } + + /* Put additional records for given type. */ + ret = put_additional(pkt, rr, qdata, info, state); + if (ret != KNOT_EOK) { + break; + } + } + + /* Evaluate final state. */ + switch (ret) { + case KNOT_EOK: return state; /* Keep current state. */ + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; /* Truncated. */ + default: return KNOTD_IN_STATE_ERROR; /* Error. */ + } +} + +static int solve_additional_dnssec(int state, knot_pkt_t *pkt, knotd_qdata_t *qdata, void *ctx) +{ + /* RFC4035, section 3.1 RRSIGs for RRs in ADDITIONAL are optional. */ + int ret = nsec_append_rrsigs(pkt, qdata, true); + switch (ret) { + case KNOT_ESPACE: return KNOTD_IN_STATE_TRUNC; + case KNOT_EOK: return state; + default: return KNOTD_IN_STATE_ERROR; + } +} + +/*! \brief Helper for internet_query repetitive code. */ +#define SOLVE_STEP(solver, state, context) \ + state = (solver)(state, pkt, qdata, context); \ + if (state == KNOTD_IN_STATE_TRUNC) { \ + return KNOT_STATE_DONE; \ + } else if (state == KNOTD_IN_STATE_ERROR) { \ + return KNOT_STATE_FAIL; \ + } + +static int answer_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + int state = KNOTD_IN_STATE_BEGIN; + struct query_plan *plan = qdata->extra->zone->query_plan; + struct query_step *step; + + bool with_dnssec = have_dnssec(qdata); + + /* Resolve PREANSWER. */ + if (plan != NULL) { + WALK_LIST(step, plan->stage[KNOTD_STAGE_PREANSWER]) { + SOLVE_STEP(step->process, state, step->ctx); + } + } + + /* Resolve ANSWER. */ + knot_pkt_begin(pkt, KNOT_ANSWER); + SOLVE_STEP(solve_answer, state, NULL); + if (with_dnssec) { + SOLVE_STEP(solve_answer_dnssec, state, NULL); + } + if (plan != NULL) { + WALK_LIST(step, plan->stage[KNOTD_STAGE_ANSWER]) { + SOLVE_STEP(step->process, state, step->ctx); + } + } + + /* Resolve AUTHORITY. */ + knot_pkt_begin(pkt, KNOT_AUTHORITY); + SOLVE_STEP(solve_authority, state, NULL); + if (with_dnssec) { + SOLVE_STEP(solve_authority_dnssec, state, NULL); + } + if (plan != NULL) { + WALK_LIST(step, plan->stage[KNOTD_STAGE_AUTHORITY]) { + SOLVE_STEP(step->process, state, step->ctx); + } + } + + /* Resolve ADDITIONAL. */ + knot_pkt_begin(pkt, KNOT_ADDITIONAL); + SOLVE_STEP(solve_additional, state, NULL); + if (with_dnssec) { + SOLVE_STEP(solve_additional_dnssec, state, NULL); + } + if (plan != NULL) { + WALK_LIST(step, plan->stage[KNOTD_STAGE_ADDITIONAL]) { + SOLVE_STEP(step->process, state, step->ctx); + } + } + + /* Write resulting RCODE. */ + knot_wire_set_rcode(pkt->wire, qdata->rcode); + + return KNOT_STATE_DONE; +} + +int internet_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt == NULL || qdata == NULL) { + return KNOT_STATE_FAIL; + } + + /* Check if valid zone. */ + NS_NEED_ZONE(qdata, KNOT_RCODE_REFUSED); + + /* Check if a TSIG is present. */ + if (knot_pkt_has_tsig(qdata->query)) { + NS_NEED_AUTH(qdata, ACL_ACTION_QUERY); + + /* Reserve space for TSIG. */ + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + } + + /* Check if the zone is not empty or expired. */ + NS_NEED_ZONE_CONTENTS(qdata); + + /* Get answer to QNAME. */ + qdata->name = knot_pkt_qname(qdata->query); + + return answer_query(pkt, qdata); +} diff --git a/src/knot/nameserver/internet.h b/src/knot/nameserver/internet.h new file mode 100644 index 0000000..52afe62 --- /dev/null +++ b/src/knot/nameserver/internet.h @@ -0,0 +1,79 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" +#include "knot/include/module.h" +#include "knot/nameserver/process_query.h" + +/*! \brief Don't follow CNAME/DNAME chain beyond this depth. */ +#define CNAME_CHAIN_MAX 5 + +/*! + * \brief Answer query from an IN class zone. + * + * \retval KNOT_STATE_FAIL if it encountered an error. + * \retval KNOT_STATE_DONE if finished. + */ +int internet_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! \brief Require given QUERY TYPE or return error code. */ +#define NS_NEED_QTYPE(qdata, qtype_want, error_rcode) \ + if (knot_pkt_qtype((qdata)->query) != (qtype_want)) { \ + qdata->rcode = (error_rcode); \ + return KNOT_STATE_FAIL; \ + } + +/*! \brief Require given QUERY NAME or return error code. */ +#define NS_NEED_QNAME(qdata, qname_want, error_rcode) \ + if (!knot_dname_is_equal(knot_pkt_qname((qdata)->query), (qname_want))) { \ + qdata->rcode = (error_rcode); \ + return KNOT_STATE_FAIL; \ + } + +/*! \brief Require existing zone or return failure. */ +#define NS_NEED_ZONE(qdata, error_rcode) \ + if ((qdata)->extra->zone == NULL) { \ + qdata->rcode = (error_rcode); \ + if ((error_rcode) == KNOT_RCODE_REFUSED) { \ + qdata->rcode_ede = KNOT_EDNS_EDE_NOTAUTH; \ + } \ + return KNOT_STATE_FAIL; \ + } + +/*! \brief Require existing zone contents or return failure. */ +#define NS_NEED_ZONE_CONTENTS(qdata) \ + if ((qdata)->extra->contents == NULL) { \ + qdata->rcode = KNOT_RCODE_SERVFAIL; \ + qdata->rcode_ede = KNOT_EDNS_EDE_INV_DATA; \ + return KNOT_STATE_FAIL; \ + } + +/*! \brief Require authentication. */ +#define NS_NEED_AUTH(qdata, action) \ + if (!process_query_acl_check(conf(), (action), (qdata)) || \ + process_query_verify(qdata) != KNOT_EOK) { \ + return KNOT_STATE_FAIL; \ + } + +/*! \brief Require the zone not to be frozen. */ +#define NS_NEED_NOT_FROZEN(qdata) \ + if ((qdata)->extra->zone->events.ufrozen) { \ + (qdata)->rcode = KNOT_RCODE_REFUSED; \ + (qdata)->rcode_ede = KNOT_EDNS_EDE_NOT_READY; \ + return KNOT_STATE_FAIL; \ + } diff --git a/src/knot/nameserver/ixfr.c b/src/knot/nameserver/ixfr.c new file mode 100644 index 0000000..03a9fdf --- /dev/null +++ b/src/knot/nameserver/ixfr.c @@ -0,0 +1,332 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <urcu.h> + +#include "contrib/mempattern.h" +#include "contrib/sockaddr.h" +#include "knot/journal/journal_metadata.h" +#include "knot/nameserver/axfr.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/ixfr.h" +#include "knot/nameserver/log.h" +#include "knot/nameserver/xfr.h" +#include "knot/zone/serial.h" +#include "libknot/libknot.h" + +#define ZONE_NAME(qdata) knot_pkt_qname((qdata)->query) +#define REMOTE(qdata) (struct sockaddr *)knotd_qdata_remote_addr(qdata) + +#define IXFROUT_LOG(priority, qdata, fmt...) \ + ns_log(priority, ZONE_NAME(qdata), LOG_OPERATION_IXFR, \ + LOG_DIRECTION_OUT, REMOTE(qdata), false, fmt) + +/*! \brief Helper macro for putting RRs into packet. */ +#define IXFR_SAFE_PUT(pkt, rr) \ + int ret = knot_pkt_put((pkt), 0, (rr), KNOT_PF_NOTRUNC | KNOT_PF_ORIGTTL); \ + if (ret != KNOT_EOK) { \ + return ret; \ + } + +/*! \brief Puts current RR into packet, stores state for retries. */ +static int ixfr_put_chg_part(knot_pkt_t *pkt, struct ixfr_proc *ixfr, + journal_read_t *read) +{ + assert(pkt); + assert(ixfr); + assert(read); + + if (!knot_rrset_empty(&ixfr->cur_rr)) { + IXFR_SAFE_PUT(pkt, &ixfr->cur_rr); + journal_read_clear_rrset(&ixfr->cur_rr); + } + + while (journal_read_rrset(read, &ixfr->cur_rr, true)) { + if (ixfr->cur_rr.type == KNOT_RRTYPE_SOA) { + ixfr->in_remove_section = !ixfr->in_remove_section; + + if (ixfr->in_remove_section) { + if (knot_soa_serial(ixfr->cur_rr.rrs.rdata) == ixfr->soa_to) { + break; + } + } else { + ixfr->soa_last = knot_soa_serial(ixfr->cur_rr.rrs.rdata); + } + } + + if (pkt->size > KNOT_WIRE_PTR_MAX) { + // optimization: once the XFR DNS message is > 16 KiB, compression + // is limited. Better wrap to next message. + return KNOT_ESPACE; + } + + IXFR_SAFE_PUT(pkt, &ixfr->cur_rr); + journal_read_clear_rrset(&ixfr->cur_rr); + } + + return journal_read_get_error(read, KNOT_EOK); +} + +/*! + * \brief Process the changes from journal. + * \note Keep in mind that this function must be able to resume processing, + * for example if it fills a packet and returns ESPACE, it is called again + * with next empty answer and it must resume the processing exactly where + * it's left off. + */ +static int ixfr_process_journal(knot_pkt_t *pkt, const void *item, + struct xfr_proc *xfer) +{ + int ret = KNOT_EOK; + struct ixfr_proc *ixfr = (struct ixfr_proc *)xfer; + journal_read_t *read = (journal_read_t *)item; + + ret = ixfr_put_chg_part(pkt, ixfr, read); + + return ret; +} + +#undef IXFR_SAFE_PUT + +static int ixfr_load_chsets(journal_read_t **journal_read, zone_t *zone, + const zone_contents_t *contents, const knot_rrset_t *their_soa) +{ + assert(journal_read); + assert(zone); + + /* Compare serials. */ + uint32_t serial_to = zone_contents_serial(contents), j_serial_to; + uint32_t serial_from = knot_soa_serial(their_soa->rrs.rdata); + if (serial_compare(serial_to, serial_from) & SERIAL_MASK_LEQ) { /* We have older/same age zone. */ + return KNOT_EUPTODATE; + } + + zone_journal_t j = zone_journal(zone); + bool j_exists = false; + int ret = journal_info(j, &j_exists, NULL, NULL, &j_serial_to, NULL, NULL, NULL, NULL); + if (ret != KNOT_EOK) { + return ret; + } else if (!j_exists) { + return KNOT_ENOENT; + } + + // please note that the journal serial_to might differ from zone SOA serial + // it is because RCU lock is made at different moment than LMDB txn begin + return journal_read_begin(zone_journal(zone), false, serial_from, journal_read); +} + +static int ixfr_query_check(knotd_qdata_t *qdata) +{ + NS_NEED_ZONE(qdata, KNOT_RCODE_NOTAUTH); + NS_NEED_AUTH(qdata, ACL_ACTION_TRANSFER); + NS_NEED_ZONE_CONTENTS(qdata); + + /* Need SOA authority record. */ + const knot_pktsection_t *authority = knot_pkt_section(qdata->query, KNOT_AUTHORITY); + const knot_rrset_t *their_soa = knot_pkt_rr(authority, 0); + if (authority->count < 1 || their_soa->type != KNOT_RRTYPE_SOA) { + qdata->rcode = KNOT_RCODE_FORMERR; + return KNOT_STATE_FAIL; + } + /* SOA needs to match QNAME. */ + NS_NEED_QNAME(qdata, their_soa->owner, KNOT_RCODE_FORMERR); + + return KNOT_STATE_DONE; +} + +static void ixfr_answer_cleanup(knotd_qdata_t *qdata) +{ + struct ixfr_proc *ixfr = (struct ixfr_proc *)qdata->extra->ext; + knot_mm_t *mm = qdata->mm; + + knot_rrset_clear(&ixfr->cur_rr, NULL); + ptrlist_free(&ixfr->proc.nodes, mm); + journal_read_end(ixfr->journal_ctx); + mm_free(mm, qdata->extra->ext); + + /* Allow zone changes (finished). */ + rcu_read_unlock(); +} + +static int ixfr_answer_init(knotd_qdata_t *qdata, uint32_t *serial_from) +{ + assert(qdata); + + if (ixfr_query_check(qdata) == KNOT_STATE_FAIL) { + if (qdata->rcode == KNOT_RCODE_FORMERR) { + return KNOT_EMALF; + } else { + return KNOT_EDENIED; + } + } + + if (zone_get_flag(qdata->extra->zone, ZONE_XFR_FROZEN, false)) { + qdata->rcode = KNOT_RCODE_REFUSED; + qdata->rcode_ede = KNOT_EDNS_EDE_NOT_READY; + return KNOT_EAGAIN; + } + + conf_val_t provide = conf_zone_get(conf(), C_PROVIDE_IXFR, + qdata->extra->zone->name); + if (!conf_bool(&provide)) { + return KNOT_ENOTSUP; + } + + const knot_pktsection_t *authority = knot_pkt_section(qdata->query, KNOT_AUTHORITY); + const knot_rrset_t *their_soa = knot_pkt_rr(authority, 0); + *serial_from = knot_soa_serial(their_soa->rrs.rdata); + + knot_mm_t *mm = qdata->mm; + struct ixfr_proc *xfer = mm_alloc(mm, sizeof(*xfer)); + if (xfer == NULL) { + return KNOT_ENOMEM; + } + memset(xfer, 0, sizeof(*xfer)); + + int ret = ixfr_load_chsets(&xfer->journal_ctx, (zone_t *)qdata->extra->zone, + qdata->extra->contents, their_soa); + if (ret != KNOT_EOK) { + mm_free(mm, xfer); + return ret; + } + + xfr_stats_begin(&xfer->proc.stats); + xfer->state = IXFR_SOA_DEL; + init_list(&xfer->proc.nodes); + knot_rrset_init_empty(&xfer->cur_rr); + xfer->qdata = qdata; + + ptrlist_add(&xfer->proc.nodes, xfer->journal_ctx, mm); + + xfer->soa_from = knot_soa_serial(their_soa->rrs.rdata); + xfer->soa_to = zone_contents_serial(qdata->extra->contents); + xfer->soa_last = xfer->soa_from; + + qdata->extra->ext = xfer; + qdata->extra->ext_cleanup = &ixfr_answer_cleanup; + + /* No zone changes during multipacket answer (unlocked in ixfr_answer_cleanup) */ + rcu_read_lock(); + + return KNOT_EOK; +} + +static int ixfr_answer_soa(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + assert(pkt); + assert(qdata); + + /* Check query. */ + int state = ixfr_query_check(qdata); + if (state == KNOT_STATE_FAIL) { + return state; /* Malformed query. */ + } + + /* Reserve space for TSIG. */ + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + /* Guaranteed to have zone contents. */ + const zone_node_t *apex = qdata->extra->contents->apex; + knot_rrset_t soa_rr = node_rrset(apex, KNOT_RRTYPE_SOA); + if (knot_rrset_empty(&soa_rr)) { + return KNOT_STATE_FAIL; + } + ret = knot_pkt_put(pkt, 0, &soa_rr, 0); + if (ret != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_SERVFAIL; + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_DONE; +} + +int ixfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt == NULL || qdata == NULL) { + return KNOT_STATE_FAIL; + } + + /* IXFR over UDP is responded with SOA. */ + if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) { + return ixfr_answer_soa(pkt, qdata); + } + + /* Initialize on first call. */ + struct ixfr_proc *ixfr = qdata->extra->ext; + if (ixfr == NULL) { + uint32_t soa_from = 0; + int ret = ixfr_answer_init(qdata, &soa_from); + ixfr = qdata->extra->ext; + switch (ret) { + case KNOT_EOK: /* OK */ + IXFROUT_LOG(LOG_INFO, qdata, "started, serial %u -> %u", + ixfr->soa_from, ixfr->soa_to); + break; + case KNOT_EUPTODATE: /* Our zone is same age/older, send SOA. */ + IXFROUT_LOG(LOG_INFO, qdata, "zone is up-to-date, serial %u", soa_from); + return ixfr_answer_soa(pkt, qdata); + case KNOT_ENOTSUP: + IXFROUT_LOG(LOG_INFO, qdata, "cannot provide, fallback to AXFR"); + qdata->type = KNOTD_QUERY_TYPE_AXFR; /* Solve as AXFR. */ + return axfr_process_query(pkt, qdata); + case KNOT_ERANGE: /* No history -> AXFR. */ + case KNOT_ENOENT: + IXFROUT_LOG(LOG_INFO, qdata, "incomplete history, serial %u, fallback to AXFR", soa_from); + qdata->type = KNOTD_QUERY_TYPE_AXFR; /* Solve as AXFR. */ + return axfr_process_query(pkt, qdata); + case KNOT_EDENIED: /* Not authorized, already logged. */ + return KNOT_STATE_FAIL; + case KNOT_EMALF: /* Malformed query. */ + IXFROUT_LOG(LOG_DEBUG, qdata, "malformed query"); + return KNOT_STATE_FAIL; + case KNOT_EAGAIN: /* Outgoing IXFR temporarily disabled. */ + IXFROUT_LOG(LOG_INFO, qdata, "outgoing IXFR frozen"); + return KNOT_STATE_FAIL; + default: /* Server errors. */ + IXFROUT_LOG(LOG_ERR, qdata, "failed to start (%s)", + knot_strerror(ret)); + return KNOT_STATE_FAIL; + } + } + + /* Reserve space for TSIG. */ + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + /* Answer current packet (or continue). */ + ret = xfr_process_list(pkt, &ixfr_process_journal, qdata); + switch (ret) { + case KNOT_ESPACE: /* Couldn't write more, send packet and continue. */ + return KNOT_STATE_PRODUCE; /* Check for more. */ + case KNOT_EOK: /* Last response. */ + if (ixfr->soa_last != ixfr->soa_to) { + IXFROUT_LOG(LOG_ERR, qdata, "failed (inconsistent history)"); + return KNOT_STATE_FAIL; + } + xfr_stats_end(&ixfr->proc.stats); + xfr_log_finished(ZONE_NAME(qdata), LOG_OPERATION_IXFR, LOG_DIRECTION_OUT, + REMOTE(qdata), false, &ixfr->proc.stats); + return KNOT_STATE_DONE; + default: /* Generic error. */ + IXFROUT_LOG(LOG_ERR, qdata, "failed (%s)", knot_strerror(ret)); + return KNOT_STATE_FAIL; + } +} diff --git a/src/knot/nameserver/ixfr.h b/src/knot/nameserver/ixfr.h new file mode 100644 index 0000000..3012be1 --- /dev/null +++ b/src/knot/nameserver/ixfr.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/journal/journal_read.h" +#include "knot/nameserver/process_query.h" +#include "knot/nameserver/xfr.h" +#include "libknot/packet/pkt.h" + +/*! \brief IXFR-in processing states. */ +enum ixfr_state { + IXFR_INVALID = 0, + IXFR_START, /* IXFR-in starting, expecting final SOA. */ + IXFR_SOA_DEL, /* Expecting starting SOA. */ + IXFR_SOA_ADD, /* Expecting ending SOA. */ + IXFR_DEL, /* Expecting RR to delete. */ + IXFR_ADD, /* Expecting RR to add. */ + IXFR_DONE /* Processing done, IXFR-in complete. */ +}; + +/*! \brief Extended structure for IXFR-in/IXFR-out processing. */ +struct ixfr_proc { + /* Processing state. */ + struct xfr_proc proc; + enum ixfr_state state; + bool in_remove_section; + + /* Changes to be sent. */ + journal_read_t *journal_ctx; + + /* Currently processed RRSet. */ + knot_rrset_t cur_rr; + + /* Processing context. */ + knotd_qdata_t *qdata; + knot_mm_t *mm; + uint32_t soa_from; + uint32_t soa_to; + uint32_t soa_last; +}; + +/*! + * \brief IXFR query processing module. + * + * \retval PRODUCE if it has an answer, but not yet finished. + * \retval FAIL if it encountered an error. + * \retval DONE if finished. + */ +int ixfr_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata); diff --git a/src/knot/nameserver/log.h b/src/knot/nameserver/log.h new file mode 100644 index 0000000..fc79bd3 --- /dev/null +++ b/src/knot/nameserver/log.h @@ -0,0 +1,88 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/sockaddr.h" +#include "knot/common/log.h" +#include "libknot/dname.h" + +typedef enum { + LOG_OPERATION_AXFR, + LOG_OPERATION_IXFR, + LOG_OPERATION_NOTIFY, + LOG_OPERATION_REFRESH, + LOG_OPERATION_UPDATE, + LOG_OPERATION_DS_CHECK, + LOG_OPERATION_DS_PUSH, +} log_operation_t; + +typedef enum { + LOG_DIRECTION_NONE, + LOG_DIRECTION_IN, + LOG_DIRECTION_OUT, +} log_direction_t; + +static inline const char *log_operation_name(log_operation_t operation) +{ + switch (operation) { + case LOG_OPERATION_AXFR: + return "AXFR"; + case LOG_OPERATION_IXFR: + return "IXFR"; + case LOG_OPERATION_NOTIFY: + return "notify"; + case LOG_OPERATION_REFRESH: + return "refresh"; + case LOG_OPERATION_UPDATE: + return "DDNS"; + case LOG_OPERATION_DS_CHECK: + return "DS check"; + case LOG_OPERATION_DS_PUSH: + return "DS push"; + default: + return "?"; + } +} + +static inline const char *log_direction_name(log_direction_t direction) +{ + switch (direction) { + case LOG_DIRECTION_IN: + return ", incoming"; + case LOG_DIRECTION_OUT: + return ", outgoing"; + case LOG_DIRECTION_NONE: + default: + return ""; + } +} + +/*! + * \brief Generate log message for server communication. + * + * Example output: + * + * [example.com] NOTIFY, outgoing, remote 2001:db8::1@53, serial 123 + */ +#define ns_log(priority, zone, op, dir, remote, pool, fmt, ...) \ + do { \ + char address[SOCKADDR_STRLEN] = ""; \ + sockaddr_tostr(address, sizeof(address), (const struct sockaddr_storage *)remote); \ + log_fmt_zone(priority, LOG_SOURCE_ZONE, zone, NULL, "%s%s, remote %s%s, " fmt, \ + log_operation_name(op), log_direction_name(dir), address, \ + (pool) ? " pool" : "", ## __VA_ARGS__); \ + } while (0) diff --git a/src/knot/nameserver/notify.c b/src/knot/nameserver/notify.c new file mode 100644 index 0000000..82fce70 --- /dev/null +++ b/src/knot/nameserver/notify.c @@ -0,0 +1,92 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/nameserver/notify.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/log.h" +#include "knot/nameserver/tsig_ctx.h" +#include "knot/zone/serial.h" +#include "libdnssec/random.h" +#include "libknot/libknot.h" + +#define NOTIFY_IN_LOG(priority, qdata, fmt...) \ + ns_log(priority, knot_pkt_qname(qdata->query), LOG_OPERATION_NOTIFY, \ + LOG_DIRECTION_IN, knotd_qdata_remote_addr(qdata), false, fmt) + +static int notify_check_query(knotd_qdata_t *qdata) +{ + NS_NEED_ZONE(qdata, KNOT_RCODE_NOTAUTH); + NS_NEED_AUTH(qdata, ACL_ACTION_NOTIFY); + /* RFC1996 requires SOA question. */ + NS_NEED_QTYPE(qdata, KNOT_RRTYPE_SOA, KNOT_RCODE_FORMERR); + + return KNOT_STATE_DONE; +} + +int notify_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt == NULL || qdata == NULL) { + return KNOT_STATE_FAIL; + } + + /* Validate notification query. */ + int state = notify_check_query(qdata); + if (state == KNOT_STATE_FAIL) { + switch (qdata->rcode) { + case KNOT_RCODE_NOTAUTH: /* Not authorized, already logged. */ + break; + default: /* Other errors. */ + NOTIFY_IN_LOG(LOG_DEBUG, qdata, "invalid query"); + break; + } + return state; + } + + /* Reserve space for TSIG. */ + int ret = knot_pkt_reserve(pkt, knot_tsig_wire_size(&qdata->sign.tsig_key)); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + /* SOA RR in answer may be included, recover serial. */ + zone_t *zone = (zone_t *)qdata->extra->zone; + const knot_pktsection_t *answer = knot_pkt_section(qdata->query, KNOT_ANSWER); + if (answer->count > 0) { + const knot_rrset_t *soa = knot_pkt_rr(answer, 0); + if (soa->type == KNOT_RRTYPE_SOA) { + uint32_t zone_serial, serial = knot_soa_serial(soa->rrs.rdata); + NOTIFY_IN_LOG(LOG_INFO, qdata, "serial %u", serial); + if (zone->contents != NULL && + slave_zone_serial(zone, conf(), &zone_serial) == KNOT_EOK && + serial_equal(serial, zone_serial)) { + // NOTIFY serial == zone serial => ignore, keep timers + return KNOT_STATE_DONE; + } + } else { /* Complain, but accept N/A record. */ + NOTIFY_IN_LOG(LOG_NOTICE, qdata, "bad record in answer section"); + } + } else { + NOTIFY_IN_LOG(LOG_INFO, qdata, "serial none"); + } + + /* Incoming NOTIFY expires REFRESH timer and renews EXPIRE timer. */ + zone_set_preferred_master(zone, knotd_qdata_remote_addr(qdata)); + zone_events_schedule_now(zone, ZONE_EVENT_REFRESH); + + return KNOT_STATE_DONE; +} diff --git a/src/knot/nameserver/notify.h b/src/knot/nameserver/notify.h new file mode 100644 index 0000000..d0bff14 --- /dev/null +++ b/src/knot/nameserver/notify.h @@ -0,0 +1,28 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" +#include "knot/nameserver/process_query.h" + +/*! + * \brief Answer IN class zone NOTIFY message (RFC1996). + * + * \retval FAIL if it encountered an error. + * \retval DONE if finished. + */ +int notify_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata); diff --git a/src/knot/nameserver/nsec_proofs.c b/src/knot/nameserver/nsec_proofs.c new file mode 100644 index 0000000..71944b1 --- /dev/null +++ b/src/knot/nameserver/nsec_proofs.c @@ -0,0 +1,677 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "libknot/libknot.h" +#include "knot/nameserver/nsec_proofs.h" +#include "knot/nameserver/internet.h" +#include "knot/dnssec/zone-nsec.h" + +/*! + * \brief Check if node is empty non-terminal. + */ +static bool empty_nonterminal(const zone_node_t *node) +{ + return node && node->rrset_count == 0; +} + +/*! + * \brief Check if wildcard expansion happened for given node and QNAME. + */ +static bool wildcard_expanded(const zone_node_t *node, const knot_dname_t *qname) +{ + return !knot_dname_is_wildcard(qname) && knot_dname_is_wildcard(node->owner); +} + +/*! + * \brief Check if opt-out can take an effect. + */ +static bool ds_optout(const zone_node_t *node) +{ + return node_nsec3_get(node) == NULL && !(node->flags & NODE_FLAGS_SUBTREE_AUTH); +} + +/*! + * \brief Check if node is part of the NSEC chain. + * + * NSEC is created for each node with authoritative data or delegation. + * + * \see https://tools.ietf.org/html/rfc4035#section-2.3 + */ +static bool node_in_nsec(const zone_node_t *node) +{ + return (node->flags & NODE_FLAGS_NONAUTH) == 0 && !empty_nonterminal(node); +} + +/*! + * \brief Check if node is part of the NSEC3 chain. + * + * NSEC3 is created for each node with authoritative data, empty-non terminal, + * and delegation (unless opt-out is in effect). + * + * \see https://tools.ietf.org/html/rfc5155#section-7.1 + */ +static bool node_in_nsec3(const zone_node_t *node) +{ + return (node->flags & NODE_FLAGS_NONAUTH) == 0 && !ds_optout(node); +} + +/*! + * \brief Walk previous names until we reach a node in NSEC chain. + * + */ +static const zone_node_t *nsec_previous(const zone_node_t *previous) +{ + assert(previous); + + while (!node_in_nsec(previous)) { + previous = node_prev(previous); + assert(previous); + } + + return previous; +} + +/*! + * \brief Get closest provable encloser from closest matching parent node. + */ +static const zone_node_t *nsec3_encloser(const zone_node_t *closest) +{ + assert(closest); + + while (!node_in_nsec3(closest)) { + closest = node_parent(closest); + assert(closest); + } + + return closest; +} + +/*! + * \brief Create a 'next closer name' to the given domain name. + * + * Next closer is the name one label longer than the closest provable encloser + * of a name. + * + * \see https://tools.ietf.org/html/rfc5155#section-1.3 + * + * \param closest_encloser Closest provable encloser of \a name. + * \param name Domain name to create the 'next closer' name to. + * + * \return Next closer name, NULL on error. + */ +static const knot_dname_t *get_next_closer(const knot_dname_t *closest_encloser, + const knot_dname_t *name) +{ + // make name only one label longer than closest_encloser + size_t ce_labels = knot_dname_labels(closest_encloser, NULL); + size_t qname_labels = knot_dname_labels(name, NULL); + for (int i = 0; i < (qname_labels - ce_labels - 1); ++i) { + name = knot_wire_next_label(name, NULL); + } + + // the common labels should match + assert(knot_dname_is_equal(knot_wire_next_label(name, NULL), closest_encloser)); + + return name; +} + +/*! + * \brief Put NSEC/NSEC3 record with corresponding RRSIG into the response. + */ +static int put_nxt_from_node(const zone_node_t *node, + uint16_t type, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + assert(type == KNOT_RRTYPE_NSEC || type == KNOT_RRTYPE_NSEC3); + + knot_rrset_t rrset = node_rrset(node, type); + if (knot_rrset_empty(&rrset)) { + return KNOT_EOK; + } + + knot_rrset_t rrsigs = node_rrset(node, KNOT_RRTYPE_RRSIG); + + return process_query_put_rr(resp, qdata, &rrset, &rrsigs, + KNOT_COMPR_HINT_NONE, KNOT_PF_CHECKDUP); +} + +/*! + * \brief Put NSEC record with corresponding RRSIG into the response. + */ +static int put_nsec_from_node(const zone_node_t *node, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + return put_nxt_from_node(node, KNOT_RRTYPE_NSEC, qdata, resp); +} + +/*! + * \brief Put NSEC3 record with corresponding RRSIG into the response. + */ +static int put_nsec3_from_node(const zone_node_t *node, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + return put_nxt_from_node(node, KNOT_RRTYPE_NSEC3, qdata, resp); +} + +/*! + * \brief Find NSEC for given name and put it into the response. + * + * Note this function allows the name to match the QNAME. The NODATA proof + * for empty non-terminal is equivalent to NXDOMAIN proof, except that the + * names may exist. This is why. + */ +static int put_covering_nsec(const zone_contents_t *zone, + const knot_dname_t *name, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *match = NULL; + const zone_node_t *closest = NULL; + const zone_node_t *prev = NULL; + + const zone_node_t *proof = NULL; + + int ret = zone_contents_find_dname(zone, name, &match, &closest, &prev); + if (ret == ZONE_NAME_FOUND) { + proof = match; + } else if (ret == ZONE_NAME_NOT_FOUND) { + proof = nsec_previous(prev); + } else { + assert(ret < 0); + return ret; + } + + return put_nsec_from_node(proof, qdata, resp); +} + +/*! + * \brief Find NSEC3 covering the given name and put it into the response. + */ +static int put_covering_nsec3(const zone_contents_t *zone, + const knot_dname_t *name, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *prev = NULL; + const zone_node_t *node = NULL; + + int match = zone_contents_find_nsec3_for_name(zone, name, &node, &prev); + if (match < 0) { + // ignore if missing + return KNOT_EOK; + } + + if (match == ZONE_NAME_FOUND || prev == NULL){ + return KNOT_ERROR; + } + + return put_nsec3_from_node(prev, qdata, resp); +} + +/*! + * \brief Add NSEC3 covering the next closer name to closest encloser. + * + * \param cpe Closest provable encloser of \a qname. + * \param qname Source QNAME. + * \param zone Source zone. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec3_next_closer(const zone_node_t *cpe, + const knot_dname_t *qname, + const zone_contents_t *zone, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const knot_dname_t *next_closer = get_next_closer(cpe->owner, qname); + + return put_covering_nsec3(zone, next_closer, qdata, resp); +} + +/*! + * \brief Add NSEC3s for closest encloser proof. + * + * Adds up to two NSEC3 records. The first one proves that closest encloser + * of the queried name exists, the second one proves that the name bellow the + * encloser doesn't. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.1 + * + * \param qname Source QNAME. + * \param zone Source zone. + * \param cpe Closest provable encloser of \a qname. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_closest_encloser_proof(const knot_dname_t *qname, + const zone_contents_t *zone, + const zone_node_t *cpe, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + // An NSEC3 RR that matches the closest (provable) encloser. + + int ret = put_nsec3_from_node(node_nsec3_get(cpe), qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + + // An NSEC3 RR that covers the "next closer" name to the closest encloser. + + return put_nsec3_next_closer(cpe, qname, zone, qdata, resp); +} + +/*! + * \brief Put NSEC for wildcard answer into the response. + * + * Add NSEC record proving that no better match on QNAME exists. + * + * \see https://tools.ietf.org/html/rfc4035#section-3.1.3.3 + * + * \param previous Previous name for QNAME. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec_wildcard(const zone_node_t *previous, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + return put_nsec_from_node(previous, qdata, resp); +} + +/*! + * \brief Put NSEC3s for wildcard answer into the response. + * + * Add NSEC3 record proving that no better match on QNAME exists. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.6 + * + * \param wildcard Wildcard node that was used for expansion. + * \param qname Source QNAME. + * \param zone Source zone. + * \param qdata Query processing data. + * \param resp Response packet. + */ +static int put_nsec3_wildcard(const zone_node_t *wildcard, + const knot_dname_t *qname, + const zone_contents_t *zone, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *cpe = nsec3_encloser(node_parent(wildcard)); + + return put_nsec3_next_closer(cpe, qname, zone, qdata, resp); +} + +/*! + * \brief Put NSECs or NSEC3s for wildcard expansion in the response. + * + * \return KNOT_E* + */ +static int put_wildcard_answer(const zone_node_t *wildcard, + const zone_node_t *previous, + const zone_contents_t *zone, + const knot_dname_t *qname, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (!wildcard_expanded(wildcard, qname)) { + return KNOT_EOK; + } + + int ret = 0; + + if (knot_is_nsec3_enabled(zone)) { + ret = put_nsec3_wildcard(wildcard, qname, zone, qdata, resp); + } else { + previous = nsec_previous(previous); + ret = put_nsec_wildcard(previous, qdata, resp); + } + + return ret; +} + +/*! + * \brief Put NSECs for NXDOMAIN error into the response. + * + * Adds up to two NSEC records. We have to prove that the queried name doesn't + * exist and that no wildcard expansion is possible for that name. + * + * \see https://tools.ietf.org/html/rfc4035#section-3.1.3.2 + * + * \param zone Source zone. + * \param previous Previous node to QNAME. + * \param closest Closest matching parent of QNAME. + * \param qdata Query data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec_nxdomain(const zone_contents_t *zone, + const zone_node_t *previous, + const zone_node_t *closest, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + assert(previous); + assert(closest); + + // An NSEC RR proving that there is no exact match for <SNAME, SCLASS>. + + previous = nsec_previous(previous); + int ret = put_nsec_from_node(previous, qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + + // An NSEC RR proving that the zone contains no RRsets that would match + // <SNAME, SCLASS> via wildcard name expansion. + + // NOTE: closest may be empty non-terminal and thus not authoritative. + + size_t size = knot_dname_size(closest->owner); + if (size > KNOT_DNAME_MAXLEN - 2) { + return KNOT_EINVAL; + } + assert(size > 0); + uint8_t wildcard[2 + size]; + memcpy(wildcard, "\x01""*", 2); + memcpy(wildcard + 2, closest->owner, size); + + return put_covering_nsec(zone, wildcard, qdata, resp); +} + +/*! + * \brief Put NSEC3s for NXDOMAIN error into the response. + * + * Adds up to three NSEC3 records. We have to prove that some parent name + * exists (closest encloser proof) and that no wildcard expansion is possible + * bellow that closest encloser. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.2 + * + * \param qname Source QNAME. + * \param zone Source zone. + * \param closest Closest matching parent of \a qname. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \retval KNOT_E* + */ +static int put_nsec3_nxdomain(const knot_dname_t *qname, + const zone_contents_t *zone, + const zone_node_t *closest, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + const zone_node_t *cpe = nsec3_encloser(closest); + + // Closest encloser proof. + + int ret = put_closest_encloser_proof(qname, zone, cpe, qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + + // NSEC3 covering the (nonexistent) wildcard at the closest encloser. + + const zone_node_t *nsec3_wildcard_prev, *ignored; + if (cpe->nsec3_wildcard_name == NULL || + zone_contents_find_nsec3(zone, cpe->nsec3_wildcard_name, &ignored, &nsec3_wildcard_prev) == ZONE_NAME_FOUND) { + return KNOT_ERROR; + } + + return put_nsec3_from_node(nsec3_wildcard_prev, qdata, resp); +} + +/*! + * \brief Put NSECs or NSEC3s for the NXDOMAIN error into the response. + * + * \param zone Zone used for answering. + * \param previous Previous node to \a qname. + * \param closest Closest matching parent name for \a qname. + * \param qname Source QNAME. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nxdomain(const zone_contents_t *zone, + const zone_node_t *previous, + const zone_node_t *closest, + const knot_dname_t *qname, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (knot_is_nsec3_enabled(zone)) { + return put_nsec3_nxdomain(qname, zone, closest, qdata, resp); + } else { + return put_nsec_nxdomain(zone, previous, closest, qdata, resp); + } +} + +/*! + * \brief Put NSEC for NODATA error into the response. + * + * Then NSEC matching the QNAME must be added into the response and the bitmap + * will indicate that the QTYPE doesn't exist. As NSECs for empty non-terminals + * don't exist, the proof for NODATA match on non-terminal is proved like + * non-existence of the queried name. + * + * \see https://tools.ietf.org/html/rfc4035#section-3.1.3.1 + * + * \param match Node matching QNAME. + * \param previous Previous node to QNAME in the zone. + * \param qdata Query processing data. + * \param resp Response packet. + * + * \return KNOT_E* + */ +static int put_nsec_nodata(const zone_node_t *match, + const zone_node_t *previous, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (empty_nonterminal(match)) { + return put_nsec_from_node(nsec_previous(previous), qdata, resp); + } else { + return put_nsec_from_node(match, qdata, resp); + } +} + +/*! + * \brief Put NSEC3 for NODATA error into the response. + * + * The NSEC3 matching the QNAME is added into the response and the bitmap + * will indicate that the QTYPE doesn't exist. For QTYPE==DS, the server + * may alternatively serve a closest encloser proof with opt-out. For wildcard + * expansion, the closest encloser proof must included as well. + * + * \see https://tools.ietf.org/html/rfc5155#section-7.2.3 + * \see https://tools.ietf.org/html/rfc5155#section-7.2.4 + * \see https://tools.ietf.org/html/rfc5155#section-7.2.5 + */ +static int put_nsec3_nodata(const knot_dname_t *qname, + const zone_contents_t *zone, + const zone_node_t *match, + const zone_node_t *closest, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + int ret = KNOT_EOK; + + // NSEC3 matching QNAME is always included. + + zone_node_t *nsec3_match = node_nsec3_get(match); + if (nsec3_match != NULL) { + ret = put_nsec3_from_node(nsec3_match, qdata, resp); + if (ret != KNOT_EOK) { + return ret; + } + } + + // Closest encloser proof for wildcard effect or NSEC3 opt-out. + + if (wildcard_expanded(match, qname) || ds_optout(match)) { + const zone_node_t *cpe = nsec3_encloser(closest); + ret = put_closest_encloser_proof(qname, zone, cpe, qdata, resp); + } + + return ret; +} + +/*! + * \brief Put NSECs or NSEC3s for the NODATA error into the response. + * + * \param node Source node. + * \param qdata Query processing data. + * \param resp Response packet. + */ +static int put_nodata(const zone_node_t *node, + const zone_node_t *closest, + const zone_node_t *previous, + const zone_contents_t *zone, + const knot_dname_t *qname, + knotd_qdata_t *qdata, + knot_pkt_t *resp) +{ + if (knot_is_nsec3_enabled(zone)) { + return put_nsec3_nodata(qname, zone, node, closest, qdata, resp); + } else { + return put_nsec_nodata(node, previous, qdata, resp); + } +} + +int nsec_prove_wildcards(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->contents == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + struct wildcard_hit *item; + + WALK_LIST(item, qdata->extra->wildcards) { + if (item->node == NULL) { + return KNOT_EINVAL; + } + ret = put_wildcard_answer(item->node, item->prev, + qdata->extra->contents, + item->sname, qdata, pkt); + if (ret != KNOT_EOK) { + break; + } + } + + return ret; +} + +int nsec_prove_nodata(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->contents == NULL || qdata->extra->node == NULL) { + return KNOT_EINVAL; + } + + return put_nodata(qdata->extra->node, qdata->extra->encloser, qdata->extra->previous, + qdata->extra->contents, qdata->name, qdata, pkt); +} + +int nsec_prove_nxdomain(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->contents == NULL) { + return KNOT_EINVAL; + } + + return put_nxdomain(qdata->extra->contents, + qdata->extra->previous, qdata->extra->encloser, + qdata->name, qdata, pkt); +} + +int nsec_prove_dp_security(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (qdata->extra->node == NULL || qdata->extra->encloser == NULL || + qdata->extra->contents == NULL) { + return KNOT_EINVAL; + } + + // Add DS into the response. + + knot_rrset_t rrset = node_rrset(qdata->extra->node, KNOT_RRTYPE_DS); + if (!knot_rrset_empty(&rrset)) { + knot_rrset_t rrsigs = node_rrset(qdata->extra->node, KNOT_RRTYPE_RRSIG); + return process_query_put_rr(pkt, qdata, &rrset, &rrsigs, + KNOT_COMPR_HINT_NONE, 0); + } + + // Alternatively prove that DS doesn't exist. + + return put_nodata(qdata->extra->node, qdata->extra->encloser, qdata->extra->previous, + qdata->extra->contents, qdata->name, qdata, pkt); +} + +int nsec_append_rrsigs(knot_pkt_t *pkt, knotd_qdata_t *qdata, bool optional) +{ + int ret = KNOT_EOK; + uint16_t flags = optional ? KNOT_PF_NOTRUNC : KNOT_PF_NULL; + flags |= KNOT_PF_FREE; // Free all RRSIGs, they are synthesized + flags |= KNOT_PF_ORIGTTL; + + /* Append RRSIGs for section. */ + struct rrsig_info *info; + WALK_LIST(info, qdata->extra->rrsigs) { + knot_rrset_t *rrsig = &info->synth_rrsig; + uint16_t compr_hint = info->rrinfo->compress_ptr[KNOT_COMPR_HINT_OWNER]; + uint16_t flags_mask = (info->rrinfo->flags & KNOT_PF_SOAMINTTL) ? KNOT_PF_ORIGTTL : 0; + ret = knot_pkt_put(pkt, compr_hint, rrsig, flags & ~flags_mask); + if (ret != KNOT_EOK) { + break; + } + /* RRSIG is owned by packet now. */ + knot_rdataset_init(&info->synth_rrsig.rrs); + }; + + /* Clear the list. */ + nsec_clear_rrsigs(qdata); + + return ret; +} + +void nsec_clear_rrsigs(knotd_qdata_t *qdata) +{ + if (qdata == NULL) { + return; + } + + struct rrsig_info *info; + WALK_LIST(info, qdata->extra->rrsigs) { + knot_rrset_t *rrsig = &info->synth_rrsig; + knot_rrset_clear(rrsig, qdata->mm); + }; + + ptrlist_free(&qdata->extra->rrsigs, qdata->mm); + init_list(&qdata->extra->rrsigs); +} diff --git a/src/knot/nameserver/nsec_proofs.h b/src/knot/nameserver/nsec_proofs.h new file mode 100644 index 0000000..09d5f2a --- /dev/null +++ b/src/knot/nameserver/nsec_proofs.h @@ -0,0 +1,38 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" +#include "knot/nameserver/process_query.h" + +/*! \brief Prove wildcards visited during answer resolution. */ +int nsec_prove_wildcards(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! \brief Prove answer leading to non-existent name. */ +int nsec_prove_nxdomain(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! \brief Prove empty answer. */ +int nsec_prove_nodata(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! \brief Prove delegation point security. */ +int nsec_prove_dp_security(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! \brief Append missing RRSIGs for current processing section. */ +int nsec_append_rrsigs(knot_pkt_t *pkt, knotd_qdata_t *qdata, bool optional); + +/*! \brief Clear RRSIG list. */ +void nsec_clear_rrsigs(knotd_qdata_t *qdata); diff --git a/src/knot/nameserver/process_query.c b/src/knot/nameserver/process_query.c new file mode 100644 index 0000000..34590df --- /dev/null +++ b/src/knot/nameserver/process_query.c @@ -0,0 +1,978 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <urcu.h> + +#include "libdnssec/tsig.h" +#include "knot/common/log.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/nameserver/process_query.h" +#include "knot/nameserver/query_module.h" +#include "knot/nameserver/chaos.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/axfr.h" +#include "knot/nameserver/ixfr.h" +#include "knot/nameserver/update.h" +#include "knot/nameserver/nsec_proofs.h" +#include "knot/nameserver/notify.h" +#include "knot/server/server.h" +#include "libknot/libknot.h" +#include "contrib/macros.h" +#include "contrib/mempattern.h" + +/*! \brief Accessor to query-specific data. */ +#define QUERY_DATA(ctx) ((knotd_qdata_t *)(ctx)->data) + +static knotd_query_type_t query_type(const knot_pkt_t *pkt) +{ + switch (knot_wire_get_opcode(pkt->wire)) { + case KNOT_OPCODE_QUERY: + switch (knot_pkt_qtype(pkt)) { + case 0 /* RESERVED */: return KNOTD_QUERY_TYPE_INVALID; + case KNOT_RRTYPE_AXFR: return KNOTD_QUERY_TYPE_AXFR; + case KNOT_RRTYPE_IXFR: return KNOTD_QUERY_TYPE_IXFR; + default: return KNOTD_QUERY_TYPE_NORMAL; + } + case KNOT_OPCODE_NOTIFY: return KNOTD_QUERY_TYPE_NOTIFY; + case KNOT_OPCODE_UPDATE: return KNOTD_QUERY_TYPE_UPDATE; + default: return KNOTD_QUERY_TYPE_INVALID; + } +} + +/*! \brief Reinitialize query data structure. */ +static void query_data_init(knot_layer_t *ctx, knotd_qdata_params_t *params, + knotd_qdata_extra_t *extra) +{ + /* Initialize persistent data. */ + knotd_qdata_t *data = QUERY_DATA(ctx); + memset(data, 0, sizeof(*data)); + data->mm = ctx->mm; + data->params = params; + data->extra = extra; + data->rcode_ede = KNOT_EDNS_EDE_NONE; + + /* Initialize lists. */ + memset(extra, 0, sizeof(*extra)); + init_list(&extra->wildcards); + init_list(&extra->rrsigs); +} + +static int process_query_begin(knot_layer_t *ctx, void *params) +{ + /* Initialize context. */ + assert(ctx); + ctx->data = mm_alloc(ctx->mm, sizeof(knotd_qdata_t)); + knotd_qdata_extra_t *extra = mm_alloc(ctx->mm, sizeof(*extra)); + + /* Initialize persistent data. */ + query_data_init(ctx, params, extra); + + /* Await packet. */ + return KNOT_STATE_CONSUME; +} + +static int process_query_reset(knot_layer_t *ctx) +{ + assert(ctx); + knotd_qdata_t *qdata = QUERY_DATA(ctx); + + /* Remember persistent parameters. */ + knotd_qdata_params_t *params = qdata->params; + knotd_qdata_extra_t *extra = qdata->extra; + + /* Free allocated data. */ + knot_rrset_clear(&qdata->opt_rr, qdata->mm); + ptrlist_free(&extra->wildcards, qdata->mm); + nsec_clear_rrsigs(qdata); + if (extra->ext_cleanup != NULL) { + extra->ext_cleanup(qdata); + } + + /* Initialize persistent data. */ + query_data_init(ctx, params, extra); + + /* Await packet. */ + return KNOT_STATE_CONSUME; +} + +static int process_query_finish(knot_layer_t *ctx) +{ + process_query_reset(ctx); + mm_free(ctx->mm, ctx->data); + ctx->data = NULL; + + return KNOT_STATE_NOOP; +} + +static int process_query_in(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + assert(pkt && ctx); + knotd_qdata_t *qdata = QUERY_DATA(ctx); + + /* Check if at least header is parsed. */ + if (pkt->parsed < KNOT_WIRE_HEADER_SIZE) { + return KNOT_STATE_NOOP; /* Ignore. */ + } + + /* Accept only queries. */ + if (knot_wire_get_qr(pkt->wire)) { + return KNOT_STATE_NOOP; /* Ignore. */ + } + + /* Store for processing. */ + qdata->query = pkt; + qdata->type = query_type(pkt); + + /* Declare having response. */ + return KNOT_STATE_PRODUCE; +} + +/*! + * \brief Create a response for a given query in the INTERNET class. + */ +static int query_internet(knot_pkt_t *pkt, knot_layer_t *ctx) +{ + knotd_qdata_t *data = QUERY_DATA(ctx); + + switch (data->type) { + case KNOTD_QUERY_TYPE_NORMAL: return internet_process_query(pkt, data); + case KNOTD_QUERY_TYPE_NOTIFY: return notify_process_query(pkt, data); + case KNOTD_QUERY_TYPE_AXFR: return axfr_process_query(pkt, data); + case KNOTD_QUERY_TYPE_IXFR: return ixfr_process_query(pkt, data); + case KNOTD_QUERY_TYPE_UPDATE: return update_process_query(pkt, data); + default: + /* Nothing else is supported. */ + data->rcode = KNOT_RCODE_NOTIMPL; + return KNOT_STATE_FAIL; + } +} + +/*! + * \brief Create a response for a given query in the CHAOS class. + */ +static int query_chaos(knot_pkt_t *pkt, knot_layer_t *ctx) +{ + knotd_qdata_t *data = QUERY_DATA(ctx); + + /* Nothing except normal queries is supported. */ + if (data->type != KNOTD_QUERY_TYPE_NORMAL) { + data->rcode = KNOT_RCODE_NOTIMPL; + return KNOT_STATE_FAIL; + } + + data->rcode = knot_chaos_answer(pkt); + if (data->rcode != KNOT_RCODE_NOERROR) { + return KNOT_STATE_FAIL; + } + + return KNOT_STATE_DONE; +} + +/*! \brief Find zone for given question. */ +static zone_t *answer_zone_find(const knot_pkt_t *query, knot_zonedb_t *zonedb) +{ + uint16_t qtype = knot_pkt_qtype(query); + uint16_t qclass = knot_pkt_qclass(query); + const knot_dname_t *qname = knot_pkt_qname(query); + zone_t *zone = NULL; + + // search for zone only for IN and ANY classes + if (qclass != KNOT_CLASS_IN && qclass != KNOT_CLASS_ANY) { + return NULL; + } + + /* In case of DS query, we strip the leftmost label when searching for + * the zone (but use whole qname in search for the record), as the DS + * records are only present in a parent zone. + */ + if (qtype == KNOT_RRTYPE_DS) { + const knot_dname_t *parent = knot_wire_next_label(qname, NULL); + zone = knot_zonedb_find_suffix(zonedb, parent); + /* If zone does not exist, search for its parent zone, + this will later result to NODATA answer. */ + /*! \note This is not 100% right, it may lead to DS name for example + * when following a CNAME chain, that should also be answered + * from the parent zone (if it exists). + */ + } + + if (zone == NULL) { + if (query_type(query) == KNOTD_QUERY_TYPE_NORMAL) { + zone = knot_zonedb_find_suffix(zonedb, qname); + } else { + // Direct match required. + zone = knot_zonedb_find(zonedb, qname); + } + } + + return zone; +} + +static int answer_edns_reserve(knot_pkt_t *resp, knotd_qdata_t *qdata) +{ + if (knot_rrset_empty(&qdata->opt_rr)) { + return KNOT_EOK; + } + + /* Reserve size in the response. */ + return knot_pkt_reserve(resp, knot_edns_wire_size(&qdata->opt_rr)); +} + +static int answer_edns_init(const knot_pkt_t *query, knot_pkt_t *resp, + knotd_qdata_t *qdata) +{ + if (!knot_pkt_has_edns(query)) { + return KNOT_EOK; + } + + /* Initialize OPT record. */ + uint16_t max_payload; + switch (knotd_qdata_remote_addr(qdata)->ss_family) { + case AF_INET: + max_payload = conf()->cache.srv_udp_max_payload_ipv4; + break; + case AF_INET6: + max_payload = conf()->cache.srv_udp_max_payload_ipv6; + break; + case AF_UNIX: + max_payload = MIN(conf()->cache.srv_udp_max_payload_ipv4, + conf()->cache.srv_udp_max_payload_ipv6); + break; + default: + return KNOT_ERROR; + } + int ret = knot_edns_init(&qdata->opt_rr, max_payload, 0, + KNOT_EDNS_VERSION, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + + /* Check supported version. */ + if (knot_edns_get_version(query->opt_rr) != KNOT_EDNS_VERSION) { + qdata->rcode = KNOT_RCODE_BADVERS; + } + + /* Set DO bit if set (DNSSEC requested). */ + if (knot_pkt_has_dnssec(query)) { + knot_edns_set_do(&qdata->opt_rr); + } + + /* Append NSID if requested and available. */ + if (knot_pkt_edns_option(query, KNOT_EDNS_OPTION_NSID) != NULL) { + size_t nsid_len = conf()->cache.srv_nsid_len; + const uint8_t *nsid_data = conf()->cache.srv_nsid_data; + + if (nsid_len > 0) { + ret = knot_edns_add_option(&qdata->opt_rr, + KNOT_EDNS_OPTION_NSID, + nsid_len, nsid_data, + qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + /* Initialize EDNS Client Subnet if configured and present in query. */ + if (conf()->cache.srv_ecs) { + uint8_t *ecs_opt = knot_pkt_edns_option(query, KNOT_EDNS_OPTION_CLIENT_SUBNET); + if (ecs_opt != NULL) { + qdata->ecs = mm_alloc(qdata->mm, sizeof(knot_edns_client_subnet_t)); + if (qdata->ecs == NULL) { + return KNOT_ENOMEM; + } + const uint8_t *ecs_data = knot_edns_opt_get_data(ecs_opt); + uint16_t ecs_len = knot_edns_opt_get_length(ecs_opt); + ret = knot_edns_client_subnet_parse(qdata->ecs, ecs_data, ecs_len); + if (ret != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_FORMERR; + return ret; + } + qdata->ecs->scope_len = 0; + + /* Reserve space for the option in the answer. */ + ret = knot_edns_reserve_option(&qdata->opt_rr, KNOT_EDNS_OPTION_CLIENT_SUBNET, + ecs_len, NULL, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } else { + qdata->ecs = NULL; + } + + return answer_edns_reserve(resp, qdata); +} + +static int answer_edns_put(knot_pkt_t *resp, knotd_qdata_t *qdata) +{ + if (knot_rrset_empty(&qdata->opt_rr)) { + return KNOT_EOK; + } + + /* Add ECS if present. */ + int ret = KNOT_EOK; + if (qdata->ecs != NULL) { + uint8_t *ecs_opt = knot_edns_get_option(&qdata->opt_rr, KNOT_EDNS_OPTION_CLIENT_SUBNET, NULL); + if (ecs_opt != NULL) { + uint8_t *ecs_data = knot_edns_opt_get_data(ecs_opt); + uint16_t ecs_len = knot_edns_opt_get_length(ecs_opt); + ret = knot_edns_client_subnet_write(ecs_data, ecs_len, qdata->ecs); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + size_t opt_wire_size = knot_edns_wire_size(&qdata->opt_rr); + + /* Add EDE. Pragmatic: only if space in pkt. */ + if (qdata->rcode_ede != KNOT_EDNS_EDE_NONE && + knot_pkt_reserve(resp, KNOT_EDNS_EDE_MIN_LENGTH) == KNOT_EOK) { + ret = knot_pkt_reclaim(resp, KNOT_EDNS_EDE_MIN_LENGTH); + assert(ret == KNOT_EOK); + + uint16_t ede_code = (uint16_t)qdata->rcode_ede; + assert((int)ede_code == qdata->rcode_ede); + ede_code = htobe16(ede_code); + + ret = knot_edns_add_option(&qdata->opt_rr, KNOT_EDNS_OPTION_EDE, + sizeof(ede_code), (uint8_t *)&ede_code, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + + /* Add EXPIRE if space and not catalog zone, which cannot expire. */ + if (knot_pkt_edns_option(qdata->query, KNOT_EDNS_OPTION_EXPIRE) != NULL && + qdata->extra->contents != NULL && !qdata->extra->zone->is_catalog_flag) { + int64_t timer = qdata->extra->zone->timers.next_expire == 0 + ? zone_soa_expire(qdata->extra->zone) + : qdata->extra->zone->timers.next_expire - time(NULL); + timer = MAX(timer, 0); + uint32_t timer_be; + knot_wire_write_u32((uint8_t *)&timer_be, (uint32_t)timer); + + uint16_t expire_size = KNOT_EDNS_OPTION_HDRLEN + sizeof(timer_be); + if (knot_pkt_reserve(resp, expire_size) == KNOT_EOK) { + ret = knot_pkt_reclaim(resp, expire_size); + assert(ret == KNOT_EOK); + + ret = knot_edns_add_option(&qdata->opt_rr, KNOT_EDNS_OPTION_EXPIRE, + sizeof(timer_be), (uint8_t *)&timer_be, + qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + /* Align the response if QUIC with EDNS. */ + if (qdata->params->proto == KNOTD_QUERY_PROTO_QUIC) { + int pad_len = knot_pkt_default_padding_size(resp, &qdata->opt_rr); + if (pad_len > -1) { + ret = knot_edns_reserve_option(&qdata->opt_rr, KNOT_EDNS_OPTION_PADDING, + pad_len, NULL, qdata->mm); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + /* Reclaim reserved size. */ + ret = knot_pkt_reclaim(resp, opt_wire_size); + if (ret != KNOT_EOK) { + return ret; + } + + uint8_t *wire_end = resp->wire + resp->size; + + /* Write to packet. */ + assert(resp->current == KNOT_ADDITIONAL); + ret = knot_pkt_put(resp, KNOT_COMPR_HINT_NONE, &qdata->opt_rr, 0); + if (ret == KNOT_EOK) { + /* Save position of the OPT RR. */ + qdata->extra->opt_rr_pos = wire_end; + } + + return ret; +} + +/*! \brief Initialize response, sizes and find zone from which we're going to answer. */ +static int prepare_answer(knot_pkt_t *query, knot_pkt_t *resp, knot_layer_t *ctx) +{ + knotd_qdata_t *qdata = QUERY_DATA(ctx); + server_t *server = qdata->params->server; + + /* Initialize response. */ + int ret = knot_pkt_init_response(resp, query); + if (ret != KNOT_EOK) { + return ret; + } + knot_wire_clear_cd(resp->wire); + + /* Setup EDNS. */ + ret = answer_edns_init(query, resp, qdata); + if (ret != KNOT_EOK || qdata->rcode != 0) { + return KNOT_ERROR; + } + + /* Update maximal answer size. */ + if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP) { + resp->max_size = KNOT_WIRE_MIN_PKTSIZE; + if (knot_pkt_has_edns(query)) { + uint16_t server_size; + switch (knotd_qdata_remote_addr(qdata)->ss_family) { + case AF_INET: + server_size = conf()->cache.srv_udp_max_payload_ipv4; + break; + case AF_INET6: + server_size = conf()->cache.srv_udp_max_payload_ipv6; + break; + default: + return KNOT_ERROR; + } + uint16_t client_size = knot_edns_get_payload(query->opt_rr); + uint16_t transfer = MIN(client_size, server_size); + resp->max_size = MAX(resp->max_size, transfer); + } + } else { + resp->max_size = KNOT_WIRE_MAX_PKTSIZE; + } + + /* All supported OPCODEs require a question. */ + const knot_dname_t *qname = knot_pkt_qname(query); + if (qname == NULL) { + switch (knot_wire_get_opcode(query->wire)) { + case KNOT_OPCODE_QUERY: + case KNOT_OPCODE_NOTIFY: + case KNOT_OPCODE_UPDATE: + qdata->rcode = KNOT_RCODE_FORMERR; + break; + default: + qdata->rcode = KNOT_RCODE_NOTIMPL; + } + return KNOT_ENOTSUP; + } + + /* Find zone for QNAME. */ + qdata->extra->zone = answer_zone_find(query, server->zone_db); + if (qdata->extra->zone != NULL && qdata->extra->contents == NULL) { + qdata->extra->contents = qdata->extra->zone->contents; + } + + /* Allow normal queries to catalog only if not UDP and if allowed by ACL. */ + if (qdata->extra->zone != NULL && qdata->extra->zone->is_catalog_flag && + query_type(query) == KNOTD_QUERY_TYPE_NORMAL) { + if (qdata->params->proto == KNOTD_QUERY_PROTO_UDP || + !process_query_acl_check(conf(), ACL_ACTION_TRANSFER, qdata)) { + qdata->extra->zone = NULL; + qdata->extra->contents = NULL; + } + } + + return KNOT_EOK; +} + +static void set_rcode_to_packet(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + uint8_t ext_rcode = KNOT_EDNS_RCODE_HI(qdata->rcode); + + if (ext_rcode != 0) { + /* No OPT RR and Ext RCODE results in SERVFAIL. */ + if (qdata->extra->opt_rr_pos == NULL) { + knot_wire_set_rcode(pkt->wire, KNOT_RCODE_SERVFAIL); + return; + } + + knot_edns_set_ext_rcode_wire(qdata->extra->opt_rr_pos, ext_rcode); + } + + knot_wire_set_rcode(pkt->wire, KNOT_EDNS_RCODE_LO(qdata->rcode)); +} + +static int process_query_err(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + assert(ctx && pkt); + + knotd_qdata_t *qdata = QUERY_DATA(ctx); + + /* Initialize response from query packet. */ + knot_pkt_t *query = qdata->query; + (void)knot_pkt_init_response(pkt, query); + knot_wire_clear_cd(pkt->wire); + + /* Set TC bit if required. */ + if (qdata->err_truncated) { + knot_wire_set_aa(pkt->wire); + knot_wire_set_tc(pkt->wire); + } + + /* Move to Additionals to add OPT and TSIG. */ + if (pkt->current != KNOT_ADDITIONAL) { + (void)knot_pkt_begin(pkt, KNOT_ADDITIONAL); + } + + /* Put OPT RR to the additional section. */ + if (answer_edns_reserve(pkt, qdata) != KNOT_EOK || + answer_edns_put(pkt, qdata) != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_FORMERR; + } + + /* Set final RCODE to packet. */ + if (qdata->rcode == KNOT_RCODE_NOERROR && !qdata->err_truncated) { + /* Default RCODE is SERVFAIL if not otherwise specified. */ + qdata->rcode = KNOT_RCODE_SERVFAIL; + } + set_rcode_to_packet(pkt, qdata); + + /* Transaction security (if applicable). */ + if (process_query_sign_response(pkt, qdata) != KNOT_EOK) { + set_rcode_to_packet(pkt, qdata); + } + + return KNOT_STATE_DONE; +} + +#define PROCESS_BEGIN(plan, step, next_state, qdata) \ + if (plan != NULL) { \ + WALK_LIST(step, plan->stage[KNOTD_STAGE_BEGIN]) { \ + next_state = step->process(next_state, pkt, qdata, step->ctx); \ + if (next_state == KNOT_STATE_FAIL) { \ + goto finish; \ + } \ + } \ + } + +#define PROCESS_END(plan, step, next_state, qdata) \ + if (plan != NULL) { \ + WALK_LIST(step, plan->stage[KNOTD_STAGE_END]) { \ + next_state = step->process(next_state, pkt, qdata, step->ctx); \ + if (next_state == KNOT_STATE_FAIL) { \ + next_state = process_query_err(ctx, pkt); \ + } \ + } \ + } + +static int process_query_out(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + assert(pkt && ctx); + + rcu_read_lock(); + + knotd_qdata_t *qdata = QUERY_DATA(ctx); + struct query_plan *plan = conf()->query_plan; + struct query_plan *zone_plan = NULL; + struct query_step *step; + + int next_state = KNOT_STATE_PRODUCE; + + /* Check parse state. */ + knot_pkt_t *query = qdata->query; + if (query->parsed < query->size) { + qdata->rcode = KNOT_RCODE_FORMERR; + next_state = KNOT_STATE_FAIL; + goto finish; + } + + /* Preprocessing. */ + if (prepare_answer(query, pkt, ctx) != KNOT_EOK) { + next_state = KNOT_STATE_FAIL; + goto finish; + } + + if (qdata->extra->zone != NULL && qdata->extra->zone->query_plan != NULL) { + zone_plan = qdata->extra->zone->query_plan; + } + + /* Before query processing code. */ + PROCESS_BEGIN(plan, step, next_state, qdata); + PROCESS_BEGIN(zone_plan, step, next_state, qdata); + + /* Answer based on qclass. */ + if (next_state == KNOT_STATE_PRODUCE) { + switch (knot_pkt_qclass(pkt)) { + case KNOT_CLASS_CH: + next_state = query_chaos(pkt, ctx); + break; + case KNOT_CLASS_ANY: + case KNOT_CLASS_IN: + next_state = query_internet(pkt, ctx); + break; + default: + qdata->rcode = KNOT_RCODE_REFUSED; + next_state = KNOT_STATE_FAIL; + break; + } + } + + /* Postprocessing. */ + if (next_state == KNOT_STATE_DONE || next_state == KNOT_STATE_PRODUCE) { + /* Move to Additionals to add OPT and TSIG. */ + if (pkt->current != KNOT_ADDITIONAL) { + (void)knot_pkt_begin(pkt, KNOT_ADDITIONAL); + } + + /* Put OPT RR to the additional section. */ + if (answer_edns_put(pkt, qdata) != KNOT_EOK) { + qdata->rcode = KNOT_RCODE_FORMERR; + next_state = KNOT_STATE_FAIL; + goto finish; + } + + /* Transaction security (if applicable). */ + if (process_query_sign_response(pkt, qdata) != KNOT_EOK) { + next_state = KNOT_STATE_FAIL; + goto finish; + } + } + +finish: + switch (next_state) { + case KNOT_STATE_NOOP: + break; + case KNOT_STATE_FAIL: + /* Error processing. */ + next_state = process_query_err(ctx, pkt); + break; + case KNOT_STATE_FINAL: + /* Just skipped postprocessing. */ + next_state = KNOT_STATE_DONE; + break; + default: + set_rcode_to_packet(pkt, qdata); + } + + /* After query processing code. */ + PROCESS_END(plan, step, next_state, qdata); + PROCESS_END(zone_plan, step, next_state, qdata); + + rcu_read_unlock(); + + return next_state; +} + +bool process_query_acl_check(conf_t *conf, acl_action_t action, + knotd_qdata_t *qdata) +{ + const knot_dname_t *zone_name = qdata->extra->zone->name; + knot_pkt_t *query = qdata->query; + const struct sockaddr_storage *query_source = knotd_qdata_remote_addr(qdata); + knot_tsig_key_t tsig = { 0 }; + + /* Skip if already checked and valid. */ + if (qdata->sign.tsig_key.name != NULL) { + return true; + } + + /* Authenticate with NOKEY if the packet isn't signed. */ + if (query->tsig_rr) { + tsig.name = query->tsig_rr->owner; + tsig.algorithm = knot_tsig_rdata_alg(query->tsig_rr); + } + + /* Log ACL details. */ + char addr_str[SOCKADDR_STRLEN]; + if (sockaddr_tostr(addr_str, sizeof(addr_str), query_source) <= 0) { + addr_str[0] = '\0'; + } + knot_dname_txt_storage_t key_name; + if (knot_dname_to_str(key_name, tsig.name, sizeof(key_name)) == NULL) { + key_name[0] = '\0'; + } + const knot_lookup_t *act = knot_lookup_by_id((knot_lookup_t *)acl_actions, action); + + bool automatic = false; + bool allowed = false; + + if (action != ACL_ACTION_UPDATE) { + // ACL_ACTION_QUERY is used for SOA/refresh query. + assert(action == ACL_ACTION_QUERY || action == ACL_ACTION_NOTIFY || + action == ACL_ACTION_TRANSFER); + const yp_name_t *item = (action == ACL_ACTION_NOTIFY) ? C_MASTER : C_NOTIFY; + conf_val_t rmts = conf_zone_get(conf, item, zone_name); + allowed = rmt_allowed(conf, &rmts, query_source, &tsig); + automatic = allowed; + } + if (!allowed) { + conf_val_t acl = conf_zone_get(conf, C_ACL, zone_name); + allowed = acl_allowed(conf, &acl, action, query_source, &tsig, zone_name, query); + } + + log_zone_debug(zone_name, + "ACL, %s, action %s, remote %s, key %s%s%s%s", + allowed ? "allowed" : "denied", + (act != NULL) ? act->name : "query", + addr_str, + (key_name[0] != '\0') ? "'" : "", + (key_name[0] != '\0') ? key_name : "none", + (key_name[0] != '\0') ? "'" : "", + automatic ? ", automatic" : ""); + + /* Check if authorized. */ + if (!allowed) { + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADKEY; + return false; + } + + /* Remember used TSIG key. */ + qdata->sign.tsig_key = tsig; + + return true; +} + +int process_query_verify(knotd_qdata_t *qdata) +{ + knot_pkt_t *query = qdata->query; + knot_sign_context_t *ctx = &qdata->sign; + + /* NOKEY => no verification. */ + if (query->tsig_rr == NULL) { + return KNOT_EOK; + } + + /* Keep digest for signing response. */ + /*! \note This memory will be rewritten for multi-pkt answers. */ + ctx->tsig_digest = (uint8_t *)knot_tsig_rdata_mac(query->tsig_rr); + ctx->tsig_digestlen = knot_tsig_rdata_mac_length(query->tsig_rr); + + /* Checking query. */ + int ret = knot_tsig_server_check(query->tsig_rr, query->wire, + query->size, &ctx->tsig_key); + + /* Evaluate TSIG check results. */ + switch(ret) { + case KNOT_EOK: + qdata->rcode = KNOT_RCODE_NOERROR; + break; + case KNOT_TSIG_EBADKEY: + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADKEY; + break; + case KNOT_TSIG_EBADSIG: + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADSIG; + break; + case KNOT_TSIG_EBADTIME: + qdata->rcode = KNOT_RCODE_NOTAUTH; + qdata->rcode_tsig = KNOT_RCODE_BADTIME; + ctx->tsig_time_signed = knot_tsig_rdata_time_signed(query->tsig_rr); + break; + case KNOT_EMALF: + qdata->rcode = KNOT_RCODE_FORMERR; + break; + default: + qdata->rcode = KNOT_RCODE_SERVFAIL; + break; + } + + /* Log possible error. */ + if (qdata->rcode == KNOT_RCODE_SERVFAIL) { + log_zone_error(qdata->extra->zone->name, + "TSIG, verification failed (%s)", knot_strerror(ret)); + } else if (qdata->rcode != KNOT_RCODE_NOERROR) { + const knot_lookup_t *item = NULL; + if (qdata->rcode_tsig != KNOT_RCODE_NOERROR) { + item = knot_lookup_by_id(knot_tsig_rcode_names, qdata->rcode_tsig); + if (item == NULL) { + item = knot_lookup_by_id(knot_rcode_names, qdata->rcode_tsig); + } + } else { + item = knot_lookup_by_id(knot_rcode_names, qdata->rcode); + } + + char *key_name = knot_dname_to_str_alloc(ctx->tsig_key.name); + log_zone_debug(qdata->extra->zone->name, + "TSIG, key '%s', verification failed '%s'", + (key_name != NULL) ? key_name : "", + (item != NULL) ? item->name : ""); + free(key_name); + } + + return ret; +} + +int process_query_sign_response(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + if (pkt->size == 0) { + // Nothing to sign. + return KNOT_EOK; + } + + int ret = KNOT_EOK; + knot_pkt_t *query = qdata->query; + knot_sign_context_t *ctx = &qdata->sign; + + /* KEY provided and verified TSIG or BADTIME allows signing. */ + if (ctx->tsig_key.name != NULL && knot_tsig_can_sign(qdata->rcode_tsig)) { + /* Sign query response. */ + size_t new_digest_len = dnssec_tsig_algorithm_size(ctx->tsig_key.algorithm); + if (ctx->pkt_count == 0) { + ret = knot_tsig_sign(pkt->wire, &pkt->size, pkt->max_size, + ctx->tsig_digest, ctx->tsig_digestlen, + ctx->tsig_digest, &new_digest_len, + &ctx->tsig_key, qdata->rcode_tsig, + ctx->tsig_time_signed); + } else { + ret = knot_tsig_sign_next(pkt->wire, &pkt->size, pkt->max_size, + ctx->tsig_digest, ctx->tsig_digestlen, + ctx->tsig_digest, &new_digest_len, + &ctx->tsig_key, + pkt->wire, pkt->size); + } + if (ret != KNOT_EOK) { + goto fail; /* Failed to sign. */ + } else { + ++ctx->pkt_count; + } + } else { + /* Copy TSIG from query and set RCODE. */ + if (query->tsig_rr && qdata->rcode_tsig != KNOT_RCODE_NOERROR) { + ret = knot_tsig_add(pkt->wire, &pkt->size, pkt->max_size, + qdata->rcode_tsig, query->tsig_rr); + if (ret != KNOT_EOK) { + goto fail; /* Whatever it is, it's server fail. */ + } + } + } + + return KNOT_EOK; + + /* Server failure in signing. */ +fail: + qdata->rcode = KNOT_RCODE_SERVFAIL; + qdata->rcode_tsig = KNOT_RCODE_NOERROR; /* Don't sign again. */ + return ret; +} + +/*! \brief Synthesize RRSIG for given parameters, store in 'qdata' for later use */ +static int put_rrsig(const knot_dname_t *sig_owner, uint16_t type, + const knot_rrset_t *rrsigs, knot_rrinfo_t *rrinfo, + uint32_t ttl_limit, knotd_qdata_t *qdata) +{ + knot_rdataset_t synth_rrs; + knot_rdataset_init(&synth_rrs); + assert(type != KNOT_RRTYPE_ANY); + int ret = knot_synth_rrsig(type, &rrsigs->rrs, &synth_rrs, qdata->mm); + if (ret == KNOT_ENOENT) { + // No signature + return KNOT_EOK; + } + if (ret != KNOT_EOK) { + return ret; + } + + /* Create rrsig info structure. */ + struct rrsig_info *info = mm_alloc(qdata->mm, sizeof(struct rrsig_info)); + if (info == NULL) { + knot_rdataset_clear(&synth_rrs, qdata->mm); + return KNOT_ENOMEM; + } + + /* Store RRSIG into info structure. */ + knot_dname_t *owner_copy = knot_dname_copy(sig_owner, qdata->mm); + if (owner_copy == NULL) { + mm_free(qdata->mm, info); + knot_rdataset_clear(&synth_rrs, qdata->mm); + return KNOT_ENOMEM; + } + uint32_t orig_ttl = knot_rrsig_original_ttl(synth_rrs.rdata); + knot_rrset_init(&info->synth_rrsig, owner_copy, rrsigs->type, + rrsigs->rclass, MIN(orig_ttl, ttl_limit)); + /* Store filtered signature. */ + info->synth_rrsig.rrs = synth_rrs; + + info->rrinfo = rrinfo; + add_tail(&qdata->extra->rrsigs, &info->n); + + return KNOT_EOK; +} + +int process_query_put_rr(knot_pkt_t *pkt, knotd_qdata_t *qdata, + const knot_rrset_t *rr, const knot_rrset_t *rrsigs, + uint16_t compr_hint, uint32_t flags) +{ + if (rr->rrs.count < 1) { + return KNOT_EMALF; + } + + /* Wildcard expansion applies only for answers. */ + bool expand = false; + if (pkt->current == KNOT_ANSWER) { + /* Expand if RR is wildcard. TRICK: if the asterix node is queried directly, we behave like if wildcard would be expanded. It's the same. */ + expand = knot_dname_is_wildcard(rr->owner); + } + + int ret = KNOT_EOK; + + /* If we already have compressed name on the wire and compression hint, + * we can just insert RRSet and fake synthesis by using compression + * hint. */ + knot_rrset_t to_add; + if (compr_hint == KNOT_COMPR_HINT_NONE && expand) { + knot_dname_t *qname_cpy = knot_dname_copy(qdata->name, &pkt->mm); + if (qname_cpy == NULL) { + return KNOT_ENOMEM; + } + knot_rrset_init(&to_add, qname_cpy, rr->type, rr->rclass, rr->ttl); + ret = knot_rdataset_copy(&to_add.rrs, &rr->rrs, &pkt->mm); + if (ret != KNOT_EOK) { + knot_dname_free(qname_cpy, &pkt->mm); + return ret; + } + to_add.additional = rr->additional; + flags |= KNOT_PF_FREE; + } else { + to_add = *rr; + } + + uint16_t rotate = conf()->cache.srv_ans_rotate ? knot_wire_get_id(qdata->query->wire) : 0; + uint16_t prev_count = pkt->rrset_count; + ret = knot_pkt_put_rotate(pkt, compr_hint, &to_add, rotate, flags); + if (ret != KNOT_EOK && (flags & KNOT_PF_FREE)) { + knot_rrset_clear(&to_add, &pkt->mm); + return ret; + } + + uint32_t rrsig_ttl_limit = UINT32_MAX; + if ((flags & KNOT_PF_SOAMINTTL) && to_add.type == KNOT_RRTYPE_SOA) { + rrsig_ttl_limit = knot_soa_minimum(to_add.rrs.rdata); + } + + const bool inserted = (prev_count != pkt->rrset_count); + if (inserted && + !knot_rrset_empty(rrsigs) && rr->type != KNOT_RRTYPE_RRSIG) { + // Get rrinfo of just inserted RR. + knot_rrinfo_t *rrinfo = &pkt->rr_info[pkt->rrset_count - 1]; + ret = put_rrsig(rr->owner, rr->type, rrsigs, rrinfo, rrsig_ttl_limit, qdata); + } + + return ret; +} + +/*! \brief Module implementation. */ +const knot_layer_api_t *process_query_layer(void) +{ + static const knot_layer_api_t api = { + .begin = &process_query_begin, + .reset = &process_query_reset, + .finish = &process_query_finish, + .consume = &process_query_in, + .produce = &process_query_out, + }; + return &api; +} diff --git a/src/knot/nameserver/process_query.h b/src/knot/nameserver/process_query.h new file mode 100644 index 0000000..bd7d42a --- /dev/null +++ b/src/knot/nameserver/process_query.h @@ -0,0 +1,107 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/include/module.h" +#include "knot/query/layer.h" +#include "knot/updates/acl.h" +#include "knot/zone/zone.h" + +/* Query processing module implementation. */ +const knot_layer_api_t *process_query_layer(void); + +/*! \brief Query processing intermediate data. */ +typedef struct knotd_qdata_extra { + zone_t *zone; /*!< Zone from which is answered. */ + const zone_contents_t *contents; /*!< Zone contents from which is answered. */ + list_t wildcards; /*!< Visited wildcards. */ + list_t rrsigs; /*!< Section RRSIGs. */ + uint8_t *opt_rr_pos; /*!< Place of the OPT RR in wire. */ + + /* Currently processed nodes. */ + const zone_node_t *node, *encloser, *previous; + + uint8_t cname_chain; /*!< Length of the CNAME chain so far. */ + + /* Extensions. */ + void *ext; + void (*ext_cleanup)(knotd_qdata_t *); /*!< Extensions cleanup callback. */ +} knotd_qdata_extra_t; + +/*! \brief Visited wildcard node list. */ +struct wildcard_hit { + node_t n; + const zone_node_t *node; /* Visited node. */ + const zone_node_t *prev; /* Previous node from the SNAME. */ + const knot_dname_t *sname; /* Name leading to this node. */ +}; + +/*! \brief RRSIG info node list. */ +struct rrsig_info { + node_t n; + knot_rrset_t synth_rrsig; /* Synthesized RRSIG. */ + knot_rrinfo_t *rrinfo; /* RR info. */ +}; + +/*! + * \brief Check current query against ACL. + * + * \param conf Configuration. + * \param action ACL action. + * \param qdata Query data. + * \return true if accepted, false if denied. + */ +bool process_query_acl_check(conf_t *conf, acl_action_t action, + knotd_qdata_t *qdata); + +/*! + * \brief Verify current query transaction security and update query data. + * + * \param qdata + * \retval KNOT_EOK + * \retval KNOT_TSIG_EBADKEY + * \retval KNOT_TSIG_EBADSIG + * \retval KNOT_TSIG_EBADTIME + * \retval (other generic errors) + */ +int process_query_verify(knotd_qdata_t *qdata); + +/*! + * \brief Sign current query using configured TSIG keys. + * + * \param pkt Outgoing message. + * \param qdata Query data. + * + * \retval KNOT_E* + */ +int process_query_sign_response(knot_pkt_t *pkt, knotd_qdata_t *qdata); + +/*! + * \brief Puts RRSet to packet, will store its RRSIG for later use. + * + * \param pkt Packet to store RRSet into. + * \param qdata Query data structure. + * \param rr RRSet to be stored. + * \param rrsigs RRSIGs to be stored. + * \param compr_hint Compression hint. + * \param flags Flags. + * + * \return KNOT_E* + */ +int process_query_put_rr(knot_pkt_t *pkt, knotd_qdata_t *qdata, + const knot_rrset_t *rr, const knot_rrset_t *rrsigs, + uint16_t compr_hint, uint32_t flags); diff --git a/src/knot/nameserver/query_module.c b/src/knot/nameserver/query_module.c new file mode 100644 index 0000000..2837135 --- /dev/null +++ b/src/knot/nameserver/query_module.c @@ -0,0 +1,791 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> + +#include "contrib/sockaddr.h" +#include "libknot/attribute.h" +#include "libknot/probe/data.h" +#include "libknot/xdp.h" +#include "knot/common/log.h" +#include "knot/conf/module.h" +#include "knot/conf/tools.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-sign.h" +#include "knot/nameserver/query_module.h" +#include "knot/nameserver/process_query.h" + +#ifdef HAVE_ATOMIC + #define ATOMIC_ADD(dst, val) __atomic_add_fetch(&(dst), (val), __ATOMIC_RELAXED) + #define ATOMIC_SUB(dst, val) __atomic_sub_fetch(&(dst), (val), __ATOMIC_RELAXED) + #define ATOMIC_SET(dst, val) __atomic_store_n(&(dst), (val), __ATOMIC_RELAXED) +#else + #warning "Statistics data can be inaccurate" + #define ATOMIC_ADD(dst, val) ((dst) += (val)) + #define ATOMIC_SUB(dst, val) ((dst) -= (val)) + #define ATOMIC_SET(dst, val) ((dst) = (val)) +#endif + +_public_ +int knotd_conf_check_ref(knotd_conf_check_args_t *args) +{ + return check_ref(args); +} + +struct query_plan *query_plan_create(void) +{ + struct query_plan *plan = malloc(sizeof(struct query_plan)); + if (plan == NULL) { + return NULL; + } + + for (unsigned i = 0; i < KNOTD_STAGES; ++i) { + init_list(&plan->stage[i]); + } + + return plan; +} + +void query_plan_free(struct query_plan *plan) +{ + if (plan == NULL) { + return; + } + + for (unsigned i = 0; i < KNOTD_STAGES; ++i) { + struct query_step *step, *next; + WALK_LIST_DELSAFE(step, next, plan->stage[i]) { + free(step); + } + } + + free(plan); +} + +static struct query_step *make_step(query_step_process_f process, void *ctx) +{ + struct query_step *step = calloc(1, sizeof(struct query_step)); + if (step == NULL) { + return NULL; + } + + step->process = process; + step->ctx = ctx; + + return step; +} + +int query_plan_step(struct query_plan *plan, knotd_stage_t stage, + query_step_process_f process, void *ctx) +{ + struct query_step *step = make_step(process, ctx); + if (step == NULL) { + return KNOT_ENOMEM; + } + + add_tail(&plan->stage[stage], &step->node); + + return KNOT_EOK; +} + +_public_ +int knotd_mod_hook(knotd_mod_t *mod, knotd_stage_t stage, knotd_mod_hook_f hook) +{ + if (stage != KNOTD_STAGE_BEGIN && stage != KNOTD_STAGE_END) { + return KNOT_EINVAL; + } + + return query_plan_step(mod->plan, stage, hook, mod); +} + +_public_ +int knotd_mod_in_hook(knotd_mod_t *mod, knotd_stage_t stage, knotd_mod_in_hook_f hook) +{ + if (stage == KNOTD_STAGE_BEGIN || stage == KNOTD_STAGE_END) { + return KNOT_EINVAL; + } + + return query_plan_step(mod->plan, stage, hook, mod); +} + +knotd_mod_t *query_module_open(conf_t *conf, server_t *server, conf_mod_id_t *mod_id, + struct query_plan *plan, const knot_dname_t *zone) +{ + if (conf == NULL || server == NULL || mod_id == NULL || plan == NULL) { + return NULL; + } + + /* Locate the module. */ + const module_t *mod = conf_mod_find(conf, mod_id->name + 1, + mod_id->name[0], false); + if (mod == NULL) { + return NULL; + } + + /* Create query module. */ + knotd_mod_t *module = calloc(1, sizeof(knotd_mod_t)); + if (module == NULL) { + return NULL; + } + + module->plan = plan; + module->config = conf; + module->server = server; + module->zone = zone; + module->id = mod_id; + module->api = mod->api; + + return module; +} + +static void module_reset(conf_t *conf, knotd_mod_t *module, struct query_plan *new_plan) +{ + // Keep ->node + module->config = conf; + // Keep ->server + // Keep ->id + module->plan = new_plan; + // Keep ->zone + // Keep ->api + + // Reset DNSSEC + zone_sign_ctx_free(module->sign_ctx); + free_zone_keys(module->keyset); + free(module->keyset); + if (module->dnssec != NULL) { + kdnssec_ctx_deinit(module->dnssec); + free(module->dnssec); + } + module->dnssec = NULL; + module->keyset = NULL; + module->sign_ctx = NULL; + + // Reset statistics + knotd_mod_stats_free(module); + module->stats_info = NULL; + module->stats_vals = NULL; + module->stats_count = 0; + + // Keep ->ctx +} + +void query_module_close(knotd_mod_t *module) +{ + if (module == NULL) { + return; + } + + module_reset(NULL, module, NULL); + conf_free_mod_id(module->id); + free(module); +} + +void query_module_reset(conf_t *conf, knotd_mod_t *module, struct query_plan *new_plan) +{ + if (module == NULL) { + return; + } + + module_reset(conf, module, new_plan); +} + +_public_ +void *knotd_mod_ctx(knotd_mod_t *mod) +{ + return (mod != NULL) ? mod->ctx : NULL; +} + +_public_ +void knotd_mod_ctx_set(knotd_mod_t *mod, void *ctx) +{ + if (mod != NULL) mod->ctx = ctx; +} + +_public_ +const knot_dname_t *knotd_mod_zone(knotd_mod_t *mod) +{ + return (mod != NULL) ? mod->zone : NULL; +} + +_public_ +void knotd_mod_log(knotd_mod_t *mod, int priority, const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + knotd_mod_vlog(mod, priority, fmt, args); + va_end(args); +} + +_public_ +void knotd_mod_vlog(knotd_mod_t *mod, int priority, const char *fmt, va_list args) +{ + if (mod == NULL || fmt == NULL) { + return; + } + + char msg[512]; + + if (vsnprintf(msg, sizeof(msg), fmt, args) < 0) { + msg[0] = '\0'; + } + + #define LOG_ARGS(mod_id, msg) "module '%s%s%.*s', %s", \ + mod_id->name + 1, (mod_id->len > 0) ? "/" : "", (int)mod_id->len, \ + mod_id->data, msg + + if (mod->zone == NULL) { + log_fmt(priority, LOG_SOURCE_SERVER, LOG_ARGS(mod->id, msg)); + } else { + log_fmt_zone(priority, LOG_SOURCE_ZONE, mod->zone, NULL, + LOG_ARGS(mod->id, msg)); + } + + #undef LOG_ARGS +} + +_public_ +int knotd_mod_stats_add(knotd_mod_t *mod, const char *ctr_name, uint32_t idx_count, + knotd_mod_idx_to_str_f idx_to_str) +{ + if (mod == NULL || idx_count == 0) { + return KNOT_EINVAL; + } + + unsigned threads = knotd_mod_threads(mod); + + mod_ctr_t *stats = NULL; + uint32_t offset = 0; + if (mod->stats_info == NULL) { + assert(mod->stats_count == 0); + stats = malloc(sizeof(*stats)); + if (stats == NULL) { + return KNOT_ENOMEM; + } + mod->stats_info = stats; + + assert(mod->stats_vals == NULL); + mod->stats_vals = calloc(threads, sizeof(*mod->stats_vals)); + if (mod->stats_vals == NULL) { + knotd_mod_stats_free(mod); + return KNOT_ENOMEM; + } + + for (unsigned i = 0; i < threads; i++) { + mod->stats_vals[i] = calloc(idx_count, sizeof(**mod->stats_vals)); + if (mod->stats_vals[i] == NULL) { + knotd_mod_stats_free(mod); + return KNOT_ENOMEM; + } + } + } else { + for (uint32_t i = 0; i < mod->stats_count; i++) { + offset += mod->stats_info[i].count; + } + assert(offset == mod->stats_info[mod->stats_count - 1].offset + + mod->stats_info[mod->stats_count - 1].count); + + assert(mod->stats_count > 0); + size_t old_size = mod->stats_count * sizeof(*stats); + size_t new_size = old_size + sizeof(*stats); + stats = realloc(mod->stats_info, new_size); + if (stats == NULL) { + knotd_mod_stats_free(mod); + return KNOT_ENOMEM; + } + mod->stats_info = stats; + stats += mod->stats_count; + + for (unsigned i = 0; i < threads; i++) { + uint64_t *new_vals = realloc(mod->stats_vals[i], + (offset + idx_count) * sizeof(*new_vals)); + if (new_vals == NULL) { + knotd_mod_stats_free(mod); + return KNOT_ENOMEM; + } + mod->stats_vals[i] = new_vals; + new_vals += offset; + for (uint32_t j = 0; j < idx_count; j++) { + *new_vals++ = 0; + } + } + } + + stats->name = ctr_name; + stats->count = idx_count; + stats->idx_to_str = idx_to_str; + stats->offset = offset; + + mod->stats_count++; + + return KNOT_EOK; +} + +_public_ +void knotd_mod_stats_free(knotd_mod_t *mod) +{ + if (mod == NULL || mod->stats_info == NULL) { + return; + } + + if (mod->stats_vals != NULL) { + unsigned threads = knotd_mod_threads(mod); + for (unsigned i = 0; i < threads; i++) { + free(mod->stats_vals[i]); + } + } + + free(mod->stats_vals); + free(mod->stats_info); +} + +#define STATS_BODY(OPERATION) { \ + if (mod == NULL) return; \ + \ + mod_ctr_t *ctr = mod->stats_info + ctr_id; \ + assert(idx < ctr->count); \ + OPERATION(mod->stats_vals[thr_id][ctr->offset + idx], val); \ +} + +_public_ +void knotd_mod_stats_incr(knotd_mod_t *mod, unsigned thr_id, uint32_t ctr_id, + uint32_t idx, uint64_t val) +{ + STATS_BODY(ATOMIC_ADD) +} + +_public_ +void knotd_mod_stats_decr(knotd_mod_t *mod, unsigned thr_id, uint32_t ctr_id, + uint32_t idx, uint64_t val) +{ + STATS_BODY(ATOMIC_SUB) +} + +_public_ +void knotd_mod_stats_store(knotd_mod_t *mod, unsigned thr_id, uint32_t ctr_id, + uint32_t idx, uint64_t val) +{ + STATS_BODY(ATOMIC_SET) +} + +_public_ +knotd_conf_t knotd_conf_env(knotd_mod_t *mod, knotd_conf_env_t env) +{ + static const char *version = "Knot DNS " PACKAGE_VERSION; + + knotd_conf_t out = { { 0 } }; + + if (mod == NULL) { + return out; + } + + conf_t *config = (mod->config != NULL) ? mod->config : conf(); + + switch (env) { + case KNOTD_CONF_ENV_VERSION: + out.single.string = version; + break; + case KNOTD_CONF_ENV_HOSTNAME: + out.single.string = config->hostname; + break; + case KNOTD_CONF_ENV_WORKERS_UDP: + out.single.integer = config->cache.srv_udp_threads; + break; + case KNOTD_CONF_ENV_WORKERS_TCP: + out.single.integer = config->cache.srv_tcp_threads; + break; + case KNOTD_CONF_ENV_WORKERS_XDP: + out.single.integer = config->cache.srv_xdp_threads; + break; + default: + return out; + } + + out.count = 1; + + return out; +} + +_public_ +unsigned knotd_mod_threads(knotd_mod_t *mod) +{ + knotd_conf_t udp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_UDP); + knotd_conf_t xdp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_XDP); + knotd_conf_t tcp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_TCP); + return udp.single.integer + xdp.single.integer + tcp.single.integer; +} + +static void set_val(yp_type_t type, knotd_conf_val_t *item, conf_val_t *val) +{ + switch (type) { + case YP_TINT: + item->integer = conf_int(val); + break; + case YP_TBOOL: + item->boolean = conf_bool(val); + break; + case YP_TOPT: + item->option = conf_opt(val); + break; + case YP_TSTR: + item->string = conf_str(val); + break; + case YP_TDNAME: + item->dname = conf_dname(val); + break; + case YP_TADDR: + item->addr = conf_addr(val, NULL); + break; + case YP_TNET: + item->addr = conf_addr_range(val, &item->addr_max, + &item->addr_mask); + break; + case YP_TREF: + if (val->code == KNOT_EOK) { + conf_val(val); + item->data_len = val->len; + item->data = val->data; + } + break; + case YP_THEX: + case YP_TB64: + item->data = conf_bin(val, &item->data_len); + break; + case YP_TDATA: + item->data = conf_data(val, &item->data_len); + break; + default: + return; + } +} + +static void set_conf_out(knotd_conf_t *out, conf_val_t *val) +{ + if (!(val->item->flags & YP_FMULTI)) { + out->count = (val->code == KNOT_EOK) ? 1 : 0; + set_val(val->item->type, &out->single, val); + } else { + size_t count = conf_val_count(val); + if (count == 0) { + return; + } + + out->multi = malloc(count * sizeof(*out->multi)); + if (out->multi == NULL) { + return; + } + memset(out->multi, 0, count * sizeof(*out->multi)); + + for (size_t i = 0; i < count; i++) { + set_val(val->item->type, &out->multi[i], val); + conf_val_next(val); + } + out->count = count; + } +} + +_public_ +knotd_conf_t knotd_conf(knotd_mod_t *mod, const yp_name_t *section_name, + const yp_name_t *item_name, const knotd_conf_t *id) +{ + knotd_conf_t out = { { 0 } }; + + if (mod == NULL || section_name == NULL || item_name == NULL) { + return out; + } + + conf_t *config = (mod->config != NULL) ? mod->config : conf(); + + conf_val_t val; + if (id != NULL) { + val = conf_rawid_get(config, section_name, item_name, + id->single.data, id->single.data_len); + } else { + val = conf_get(config, section_name, item_name); + } + + set_conf_out(&out, &val); + + return out; +} + +_public_ +knotd_conf_t knotd_conf_mod(knotd_mod_t *mod, const yp_name_t *item_name) +{ + knotd_conf_t out = { { 0 } }; + + if (mod == NULL || item_name == NULL) { + return out; + } + + conf_t *config = (mod->config != NULL) ? mod->config : conf(); + + conf_val_t val = conf_mod_get(config, item_name, mod->id); + if (val.item == NULL) { + return out; + } + + set_conf_out(&out, &val); + + return out; +} + +_public_ +knotd_conf_t knotd_conf_zone(knotd_mod_t *mod, const yp_name_t *item_name, + const knot_dname_t *zone) +{ + knotd_conf_t out = { { 0 } }; + + if (mod == NULL || item_name == NULL || zone == NULL) { + return out; + } + + conf_t *config = (mod->config != NULL) ? mod->config : conf(); + + conf_val_t val = conf_zone_get(config, item_name, zone); + + set_conf_out(&out, &val); + + return out; +} + +_public_ +knotd_conf_t knotd_conf_check_item(knotd_conf_check_args_t *args, + const yp_name_t *item_name) +{ + knotd_conf_t out = { { 0 } }; + + conf_val_t val = conf_rawid_get_txn(args->extra->conf, args->extra->txn, + args->item->name, item_name, + args->id, args->id_len); + + set_conf_out(&out, &val); + + return out; +} + +_public_ +bool knotd_conf_addr_range_match(const knotd_conf_t *range, + const struct sockaddr_storage *addr) +{ + if (range == NULL || addr == NULL) { + return false; + } + + for (size_t i = 0; i < range->count; i++) { + knotd_conf_val_t *val = &range->multi[i]; + if (val->addr_max.ss_family == AF_UNSPEC) { + if (sockaddr_net_match(addr, &val->addr, val->addr_mask)) { + return true; + } + } else { + if (sockaddr_range_match(addr, &val->addr, &val->addr_max)) { + return true; + } + } + } + + return false; +} + +_public_ +void knotd_conf_free(knotd_conf_t *conf) +{ + if (conf == NULL) { + return; + } + + if (conf->count > 0 && conf->multi != NULL) { + memset(conf->multi, 0, conf->count * sizeof(*conf->multi)); + free(conf->multi); + } + memset(conf, 0, sizeof(*conf)); +} + +_public_ +const struct sockaddr_storage *knotd_qdata_local_addr(knotd_qdata_t *qdata, + struct sockaddr_storage *buff) +{ + if (qdata == NULL) { + return NULL; + } + + if (qdata->params->xdp_msg != NULL) { +#ifdef ENABLE_XDP + return (struct sockaddr_storage *)&qdata->params->xdp_msg->ip_to; +#else + assert(0); + return NULL; +#endif + } else { + socklen_t buff_len = sizeof(*buff); + if (getsockname(qdata->params->socket, (struct sockaddr *)buff, + &buff_len) != 0) { + return NULL; + } + return buff; + } +} + +_public_ +const struct sockaddr_storage *knotd_qdata_remote_addr(knotd_qdata_t *qdata) +{ + if (qdata == NULL) { + return NULL; + } + + if (qdata->params->xdp_msg != NULL) { +#ifdef ENABLE_XDP + return (struct sockaddr_storage *)&qdata->params->xdp_msg->ip_from; +#else + assert(0); + return NULL; +#endif + } else { + return qdata->params->remote; + } +} + +_public_ +uint32_t knotd_qdata_rtt(knotd_qdata_t *qdata) +{ + if (qdata == NULL) { + return 0; + } + + switch (qdata->params->proto) { + case KNOTD_QUERY_PROTO_TCP: + if (qdata->params->xdp_msg != NULL) { +#ifdef ENABLE_XDP + return qdata->params->measured_rtt; +#else + assert(0); + return 0; +#endif + } else { + return knot_probe_tcp_rtt(qdata->params->socket); + } + case KNOTD_QUERY_PROTO_QUIC: + return qdata->params->measured_rtt; + case KNOTD_QUERY_PROTO_UDP: + default: + return 0; + } +} + +_public_ +const knot_dname_t *knotd_qdata_zone_name(knotd_qdata_t *qdata) +{ + if (qdata == NULL || qdata->extra->zone == NULL) { + return NULL; + } + + return qdata->extra->zone->name; +} + +_public_ +knot_rrset_t knotd_qdata_zone_apex_rrset(knotd_qdata_t *qdata, uint16_t type) +{ + if (qdata == NULL || qdata->extra->contents == NULL) { + return node_rrset(NULL, type); + } + + return node_rrset(qdata->extra->contents->apex, type); +} + +_public_ +int knotd_mod_dnssec_init(knotd_mod_t *mod) +{ + if (mod == NULL || mod->dnssec != NULL) { + return KNOT_EINVAL; + } + + knot_lmdb_db_t *kaspdb = &mod->server->kaspdb; + kasp_db_ensure_init(kaspdb, mod->config); // probably redundant + + mod->dnssec = calloc(1, sizeof(*(mod->dnssec))); + if (mod->dnssec == NULL) { + return KNOT_ENOMEM; + } + + conf_val_t conf = conf_zone_get(mod->config, C_DNSSEC_SIGNING, mod->zone); + int ret = kdnssec_ctx_init(mod->config, mod->dnssec, mod->zone, kaspdb, + conf_bool(&conf) ? NULL : mod->id); + if (ret != KNOT_EOK) { + free(mod->dnssec); + mod->dnssec = NULL; + return ret; + } + + return KNOT_EOK; +} + +_public_ +int knotd_mod_dnssec_load_keyset(knotd_mod_t *mod, bool verbose) +{ + if (mod == NULL || mod->dnssec == NULL) { + return KNOT_EINVAL; + } + + mod->keyset = calloc(1, sizeof(*(mod->keyset))); + if (mod->keyset == NULL) { + return KNOT_ENOMEM; + } + + int ret = load_zone_keys(mod->dnssec, mod->keyset, verbose); + if (ret != KNOT_EOK) { + free(mod->keyset); + mod->keyset = NULL; + return ret; + } + + mod->sign_ctx = zone_sign_ctx(mod->keyset, mod->dnssec); + if (mod->sign_ctx == NULL) { + free_zone_keys(mod->keyset); + free(mod->keyset); + mod->keyset = NULL; + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +_public_ +void knotd_mod_dnssec_unload_keyset(knotd_mod_t *mod) +{ + if (mod != NULL && mod->keyset != NULL) { + zone_sign_ctx_free(mod->sign_ctx); + mod->sign_ctx = NULL; + + free_zone_keys(mod->keyset); + free(mod->keyset); + mod->keyset = NULL; + } +} + +_public_ +int knotd_mod_dnssec_sign_rrset(knotd_mod_t *mod, knot_rrset_t *rrsigs, + const knot_rrset_t *rrset, knot_mm_t *mm) +{ + if (mod == NULL || rrsigs == NULL || rrset == NULL) { + return KNOT_EINVAL; + } + + return knot_sign_rrset2(rrsigs, rrset, mod->sign_ctx, mm); +} diff --git a/src/knot/nameserver/query_module.h b/src/knot/nameserver/query_module.h new file mode 100644 index 0000000..5cc905b --- /dev/null +++ b/src/knot/nameserver/query_module.h @@ -0,0 +1,99 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/libknot.h" +#include "knot/conf/conf.h" +#include "knot/dnssec/context.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/include/module.h" +#include "knot/server/server.h" +#include "contrib/ucw/lists.h" + +#ifdef HAVE_ATOMIC + #define ATOMIC_GET(src) __atomic_load_n(&(src), __ATOMIC_RELAXED) +#else + #define ATOMIC_GET(src) (src) +#endif + +#define KNOTD_STAGES (KNOTD_STAGE_END + 1) + +typedef unsigned (*query_step_process_f) + (unsigned state, knot_pkt_t *pkt, knotd_qdata_t *qdata, knotd_mod_t *mod); + +/*! \brief Single processing step in query processing. */ +struct query_step { + node_t node; + void *ctx; + query_step_process_f process; +}; + +/*! Query plan represents a sequence of steps needed for query processing + * divided into several stages, where each stage represents a current response + * assembly phase, for example 'before processing', 'answer section' and so on. + */ +struct query_plan { + list_t stage[KNOTD_STAGES]; +}; + +/*! \brief Create an empty query plan. */ +struct query_plan *query_plan_create(void); + +/*! \brief Free query plan and all planned steps. */ +void query_plan_free(struct query_plan *plan); + +/*! \brief Plan another step for given stage. */ +int query_plan_step(struct query_plan *plan, knotd_stage_t stage, + query_step_process_f process, void *ctx); + +/*! \brief Open query module identified by name. */ +knotd_mod_t *query_module_open(conf_t *conf, server_t *server, conf_mod_id_t *mod_id, + struct query_plan *plan, const knot_dname_t *zone); + +/*! \brief Close query module. */ +void query_module_close(knotd_mod_t *module); + +/*! \brief Close and open existing query module. */ +void query_module_reset(conf_t *conf, knotd_mod_t *module, struct query_plan *new_plan); + +typedef char* (*mod_idx_to_str_f)(uint32_t idx, uint32_t count); + +typedef struct { + const char *name; + mod_idx_to_str_f idx_to_str; // unused if count == 1 + uint32_t offset; // offset of counters in stats_vals[thread_id] + uint32_t count; +} mod_ctr_t; + +struct knotd_mod { + node_t node; + conf_t *config; + server_t *server; + conf_mod_id_t *id; + struct query_plan *plan; + const knot_dname_t *zone; + const knotd_mod_api_t *api; + kdnssec_ctx_t *dnssec; + zone_keyset_t *keyset; + zone_sign_ctx_t *sign_ctx; + mod_ctr_t *stats_info; + uint64_t **stats_vals; + uint32_t stats_count; + void *ctx; +}; + +void knotd_mod_stats_free(knotd_mod_t *mod); diff --git a/src/knot/nameserver/tsig_ctx.c b/src/knot/nameserver/tsig_ctx.c new file mode 100644 index 0000000..05383b1 --- /dev/null +++ b/src/knot/nameserver/tsig_ctx.c @@ -0,0 +1,189 @@ +/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/nameserver/tsig_ctx.h" +#include "contrib/string.h" +#include "libknot/libknot.h" + +/*! + * Maximal total size for unsigned messages. + */ +static const size_t TSIG_BUFFER_MAX_SIZE = (UINT16_MAX * 100); + +void tsig_init(tsig_ctx_t *ctx, const knot_tsig_key_t *key) +{ + if (!ctx) { + return; + } + + memzero(ctx, sizeof(*ctx)); + ctx->key = key; +} + +void tsig_cleanup(tsig_ctx_t *ctx) +{ + if (!ctx) { + return; + } + + free(ctx->buffer); + memzero(ctx, sizeof(*ctx)); +} + +void tsig_reset(tsig_ctx_t *ctx) +{ + if (!ctx) { + return; + } + + const knot_tsig_key_t *backup = ctx->key; + tsig_cleanup(ctx); + tsig_init(ctx, backup); +} + +int tsig_sign_packet(tsig_ctx_t *ctx, knot_pkt_t *packet) +{ + if (!ctx || !packet) { + return KNOT_EINVAL; + } + + if (ctx->key == NULL) { + return KNOT_EOK; + } + + int ret = KNOT_ERROR; + if (ctx->digest_size == 0) { + ctx->digest_size = dnssec_tsig_algorithm_size(ctx->key->algorithm); + ret = knot_tsig_sign(packet->wire, &packet->size, packet->max_size, + NULL, 0, + ctx->digest, &ctx->digest_size, + ctx->key, 0, 0); + } else { + uint8_t previous_digest[ctx->digest_size]; + memcpy(previous_digest, ctx->digest, ctx->digest_size); + + ret = knot_tsig_sign_next(packet->wire, &packet->size, packet->max_size, + previous_digest, ctx->digest_size, + ctx->digest, &ctx->digest_size, + ctx->key, packet->wire, packet->size); + } + + return ret; +} + +static int update_ctx_after_verify(tsig_ctx_t *ctx, knot_rrset_t *tsig_rr) +{ + assert(ctx); + assert(tsig_rr); + + if (ctx->digest_size != knot_tsig_rdata_mac_length(tsig_rr)) { + return KNOT_EMALF; + } + + memcpy(ctx->digest, knot_tsig_rdata_mac(tsig_rr), ctx->digest_size); + ctx->prev_signed_time = knot_tsig_rdata_time_signed(tsig_rr); + ctx->unsigned_count = 0; + ctx->buffer_used = 0; + + return KNOT_EOK; +} + +static int buffer_add_packet(tsig_ctx_t *ctx, knot_pkt_t *packet) +{ + size_t need = ctx->buffer_used + packet->size; + + // Inflate the buffer if necessary. + + if (need > TSIG_BUFFER_MAX_SIZE) { + return KNOT_ENOMEM; + } + + if (need > ctx->buffer_size) { + uint8_t *buffer = realloc(ctx->buffer, need); + if (!buffer) { + return KNOT_ENOMEM; + } + + ctx->buffer = buffer; + ctx->buffer_size = need; + } + + // Buffer the packet. + + uint8_t *write = ctx->buffer + ctx->buffer_used; + memcpy(write, packet->wire, packet->size); + ctx->buffer_used = need; + + return KNOT_EOK; +} + +int tsig_verify_packet(tsig_ctx_t *ctx, knot_pkt_t *packet) +{ + if (!ctx || !packet) { + return KNOT_EINVAL; + } + + if (ctx->key == NULL) { + return KNOT_EOK; + } + + int ret = buffer_add_packet(ctx, packet); + if (ret != KNOT_EOK) { + return ret; + } + + // Unsigned packet. + + if (packet->tsig_rr == NULL) { + ctx->unsigned_count += 1; + return KNOT_EOK; + } + + // Signed packet. + + if (ctx->prev_signed_time == 0) { + ret = knot_tsig_client_check(packet->tsig_rr, ctx->buffer, + ctx->buffer_used, ctx->digest, + ctx->digest_size, ctx->key, 0); + } else { + ret = knot_tsig_client_check_next(packet->tsig_rr, ctx->buffer, + ctx->buffer_used, ctx->digest, + ctx->digest_size, ctx->key, + ctx->prev_signed_time); + } + + if (ret != KNOT_EOK) { + return ret; + } + + ret = update_ctx_after_verify(ctx, packet->tsig_rr); + if (ret != KNOT_EOK) { + return ret; + } + + return KNOT_EOK; +} + +unsigned tsig_unsigned_count(tsig_ctx_t *ctx) +{ + if (!ctx) { + return -1; + } + + return ctx->unsigned_count; +} diff --git a/src/knot/nameserver/tsig_ctx.h b/src/knot/nameserver/tsig_ctx.h new file mode 100644 index 0000000..3e91671 --- /dev/null +++ b/src/knot/nameserver/tsig_ctx.h @@ -0,0 +1,97 @@ +/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> + +#include "libknot/packet/pkt.h" +#include "libknot/tsig.h" + +#define TSIG_MAX_DIGEST_SIZE 64 + +/*! + \brief TSIG context. + */ +typedef struct tsig_ctx { + const knot_tsig_key_t *key; + uint64_t prev_signed_time; + + uint8_t digest[TSIG_MAX_DIGEST_SIZE]; + size_t digest_size; + + /* Unsigned packets handling. */ + unsigned unsigned_count; + uint8_t *buffer; + size_t buffer_used; + size_t buffer_size; +} tsig_ctx_t; + +/*! + * \brief Initialize TSIG context. + * + * \param ctx TSIG context to be initialized. + * \param key Key to be used for signing. If NULL, all performed operations + * will do nothing and always successful. + */ +void tsig_init(tsig_ctx_t *ctx, const knot_tsig_key_t *key); + +/*! + * \brief Cleanup TSIG context. + * + * \param ctx TSIG context to be cleaned up. + */ +void tsig_cleanup(tsig_ctx_t *ctx); + +/*! + * \brief Reset TSIG context for new message exchange. + */ +void tsig_reset(tsig_ctx_t *ctx); + +/*! + * \brief Sign outgoing packet. + * + * \param ctx TSIG signing context. + * \param packet Packet to be signed. + * + * \return Error code, KNOT_EOK if successful. + */ +int tsig_sign_packet(tsig_ctx_t *ctx, knot_pkt_t *packet); + +/*! + * \brief Verify incoming packet. + * + * If the packet is not signed, the function will succeed, but an internal + * counter of unsigned packets is increased. When a packet is signed, the + * same counter is reset to zero. + * + * \see tsig_unsigned_count + * + * \param ctx TSIG signing context. + * \param packet Packet to be verified. + * + * \return Error code, KNOT_EOK if successful. + */ +int tsig_verify_packet(tsig_ctx_t *ctx, knot_pkt_t *packet); + +/*! + * \brief Get number of unsigned packets since the last signed one. + * + * \param ctx TSIG signing context. + * + * \return Number of unsigned packets since the last signed one. + */ +unsigned tsig_unsigned_count(tsig_ctx_t *ctx); diff --git a/src/knot/nameserver/update.c b/src/knot/nameserver/update.c new file mode 100644 index 0000000..f43e1af --- /dev/null +++ b/src/knot/nameserver/update.c @@ -0,0 +1,107 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <unistd.h> + +#include "knot/dnssec/zone-events.h" +#include "knot/nameserver/internet.h" +#include "knot/nameserver/update.h" +#include "knot/query/requestor.h" +#include "libknot/libknot.h" + +static int update_enqueue(zone_t *zone, knotd_qdata_t *qdata) +{ + assert(zone); + assert(qdata); + + /* Create serialized request. */ + knot_request_t *req = calloc(1, sizeof(*req)); + if (req == NULL) { + return KNOT_ENOMEM; + } + + /* Store socket and remote address. */ + req->fd = dup(qdata->params->socket); + memcpy(&req->remote, knotd_qdata_remote_addr(qdata), sizeof(req->remote)); + + /* Store update request. */ + req->query = knot_pkt_new(NULL, qdata->query->max_size, NULL); + int ret = knot_pkt_copy(req->query, qdata->query); + if (ret != KNOT_EOK) { + knot_pkt_free(req->query); + free(req); + return ret; + } + + /* Store and update possible TSIG context (see NS_NEED_AUTH). */ + if (qdata->sign.tsig_key.name != NULL) { + req->sign = qdata->sign; + req->sign.tsig_digest = (uint8_t *)knot_tsig_rdata_mac(req->query->tsig_rr); + req->sign.tsig_key.name = req->query->tsig_rr->owner; + ret = dnssec_binary_dup(&qdata->sign.tsig_key.secret, &req->sign.tsig_key.secret); + if (ret != KNOT_EOK) { + knot_pkt_free(req->query); + free(req); + return ret; + } + assert(req->sign.tsig_digestlen == knot_tsig_rdata_mac_length(req->query->tsig_rr)); + assert(req->sign.tsig_key.algorithm == knot_tsig_rdata_alg(req->query->tsig_rr)); + } + + pthread_mutex_lock(&zone->ddns_lock); + + /* Enqueue created request. */ + ptrlist_add(&zone->ddns_queue, req, NULL); + ++zone->ddns_queue_size; + + pthread_mutex_unlock(&zone->ddns_lock); + + /* Schedule UPDATE event. */ + zone_events_schedule_now(zone, ZONE_EVENT_UPDATE); + + return KNOT_EOK; +} + +int update_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata) +{ + /* DDNS over XDP not supported. */ + if (qdata->params->xdp_msg != NULL) { + qdata->rcode = KNOT_RCODE_SERVFAIL; + return KNOT_STATE_FAIL; + } + + /* RFC1996 require SOA question. */ + NS_NEED_QTYPE(qdata, KNOT_RRTYPE_SOA, KNOT_RCODE_FORMERR); + + /* Check valid zone. */ + NS_NEED_ZONE(qdata, KNOT_RCODE_NOTAUTH); + + /* Need valid transaction security. */ + NS_NEED_AUTH(qdata, ACL_ACTION_UPDATE); + /* Check expiration. */ + NS_NEED_ZONE_CONTENTS(qdata); + /* Check frozen zone. */ + NS_NEED_NOT_FROZEN(qdata); + + /* Store update into DDNS queue. */ + int ret = update_enqueue((zone_t *)qdata->extra->zone, qdata); + if (ret != KNOT_EOK) { + return KNOT_STATE_FAIL; + } + + /* No immediate response. */ + return KNOT_STATE_NOOP; +} diff --git a/src/knot/nameserver/update.h b/src/knot/nameserver/update.h new file mode 100644 index 0000000..609acd9 --- /dev/null +++ b/src/knot/nameserver/update.h @@ -0,0 +1,27 @@ +/* Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" +#include "knot/nameserver/process_query.h" + +/*! + * \brief UPDATE query processing module. + * + * \return KNOT_STATE_* processing states + */ +int update_process_query(knot_pkt_t *pkt, knotd_qdata_t *qdata); diff --git a/src/knot/nameserver/xfr.c b/src/knot/nameserver/xfr.c new file mode 100644 index 0000000..b54a4ff --- /dev/null +++ b/src/knot/nameserver/xfr.c @@ -0,0 +1,96 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "knot/nameserver/xfr.h" +#include "contrib/mempattern.h" + +int xfr_process_list(knot_pkt_t *pkt, xfr_put_cb put, knotd_qdata_t *qdata) +{ + if (pkt == NULL || qdata == NULL || qdata->extra->ext == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + knot_mm_t *mm = qdata->mm; + struct xfr_proc *xfer = qdata->extra->ext; + + /* Check if the zone wasn't expired during multi-message transfer. */ + const zone_contents_t *contents = qdata->extra->contents; + if (contents == NULL) { + return KNOT_ENOZONE; + } + knot_rrset_t soa_rr = node_rrset(contents->apex, KNOT_RRTYPE_SOA); + + /* Prepend SOA on first packet. */ + if (xfer->stats.messages == 0) { + ret = knot_pkt_put(pkt, 0, &soa_rr, KNOT_PF_NOTRUNC); + if (ret != KNOT_EOK) { + return ret; + } + } + + /* Process all items in the list. */ + while (!EMPTY_LIST(xfer->nodes)) { + ptrnode_t *head = HEAD(xfer->nodes); + ret = put(pkt, head->d, xfer); + if (ret == KNOT_EOK) { /* Finished. */ + /* Complete change set. */ + rem_node((node_t *)head); + mm_free(mm, head); + } else { /* Packet full or other error. */ + break; + } + } + + /* Append SOA on last packet. */ + if (ret == KNOT_EOK) { + ret = knot_pkt_put(pkt, 0, &soa_rr, KNOT_PF_NOTRUNC); + } + + /* Update counters. */ + xfr_stats_add(&xfer->stats, pkt->size + knot_rrset_size(&qdata->opt_rr)); + + /* If a rrset is larger than the message, + * fail to avoid infinite loop of empty messages */ + if (ret == KNOT_ESPACE && pkt->rrset_count < 1) { + return KNOT_ENOXFR; + } + + return ret; +} + +void xfr_stats_begin(struct xfr_stats *stats) +{ + assert(stats); + + memset(stats, 0, sizeof(*stats)); + stats->begin = time_now(); +} + +void xfr_stats_add(struct xfr_stats *stats, unsigned bytes) +{ + assert(stats); + + stats->messages += 1; + stats->bytes += bytes; +} + +void xfr_stats_end(struct xfr_stats *stats) +{ + assert(stats); + + stats->end = time_now(); +} diff --git a/src/knot/nameserver/xfr.h b/src/knot/nameserver/xfr.h new file mode 100644 index 0000000..3347304 --- /dev/null +++ b/src/knot/nameserver/xfr.h @@ -0,0 +1,69 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/time.h" +#include "contrib/ucw/lists.h" +#include "knot/nameserver/log.h" +#include "knot/nameserver/process_query.h" +#include "knot/zone/contents.h" +#include "libknot/packet/pkt.h" + +struct xfr_stats { + unsigned messages; + unsigned bytes; + struct timespec begin; + struct timespec end; +}; + +void xfr_stats_begin(struct xfr_stats *stats); +void xfr_stats_add(struct xfr_stats *stats, unsigned bytes); +void xfr_stats_end(struct xfr_stats *stats); + +static inline +void xfr_log_finished(const knot_dname_t *zone, log_operation_t op, + log_direction_t dir, const struct sockaddr *remote, + bool reused, const struct xfr_stats *stats) +{ + ns_log(LOG_INFO, zone, op, dir, remote, reused, + "finished, %0.2f seconds, %u messages, %u bytes", + time_diff_ms(&stats->begin, &stats->end) / 1000.0, + stats->messages, stats->bytes); +} + +/*! + * \brief Generic transfer processing state. + */ +struct xfr_proc { + list_t nodes; //!< Items to process (ptrnode_t). + zone_contents_t *contents; //!< Processed zone. + struct xfr_stats stats; //!< Packet transfer statistics. +}; + +/*! + * \brief Generic transfer processing. + * + * \return KNOT_EOK or an error + */ +typedef int (*xfr_put_cb)(knot_pkt_t *pkt, const void *item, struct xfr_proc *xfer); + +/*! + * \brief Put all items from xfr_proc.nodes to packet using a callback function. + * + * \note qdata->extra->ext points to struct xfr_proc* (this is xfer-specific context) + */ +int xfr_process_list(knot_pkt_t *pkt, xfr_put_cb put, knotd_qdata_t *qdata); diff --git a/src/knot/query/capture.c b/src/knot/query/capture.c new file mode 100644 index 0000000..43f3e54 --- /dev/null +++ b/src/knot/query/capture.c @@ -0,0 +1,63 @@ +/* Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/query/capture.h" + +static int reset(knot_layer_t *ctx) +{ + return KNOT_STATE_PRODUCE; +} + +static int finish(knot_layer_t *ctx) +{ + return KNOT_STATE_NOOP; +} + +static int begin(knot_layer_t *ctx, void *module_param) +{ + ctx->data = module_param; /* struct capture_param */ + return reset(ctx); +} + +static int prepare_query(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + return KNOT_STATE_CONSUME; +} + +static int capture(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + assert(pkt && ctx && ctx->data); + struct capture_param *param = ctx->data; + + knot_pkt_copy(param->sink, pkt); + + return KNOT_STATE_DONE; +} + +const knot_layer_api_t *query_capture_api(void) +{ + static const knot_layer_api_t API = { + .begin = begin, + .reset = reset, + .finish = finish, + .consume = capture, + .produce = prepare_query, + }; + + return &API; +} diff --git a/src/knot/query/capture.h b/src/knot/query/capture.h new file mode 100644 index 0000000..41f8270 --- /dev/null +++ b/src/knot/query/capture.h @@ -0,0 +1,32 @@ +/* Copyright (C) 2016 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/query/layer.h" +#include "libknot/packet/pkt.h" + +/*! + * \brief Processing module for packet capture. + */ +const knot_layer_api_t *query_capture_api(void); + +/*! + * \brief Processing module parameters. + */ +struct capture_param { + knot_pkt_t *sink; /*!< Container for captured response. */ +}; diff --git a/src/knot/query/layer.h b/src/knot/query/layer.h new file mode 100644 index 0000000..119ae5d --- /dev/null +++ b/src/knot/query/layer.h @@ -0,0 +1,136 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" +#include "libknot/mm_ctx.h" +#include "knot/nameserver/tsig_ctx.h" + +/*! + * \brief Layer processing states. + * + * Each state represents the state machine transition, + * and determines readiness for the next action. + */ +typedef enum { + KNOT_STATE_NOOP = 0, //!< Invalid. + KNOT_STATE_CONSUME, //!< Consume data. + KNOT_STATE_PRODUCE, //!< Produce data. + KNOT_STATE_RESET, //!< Restart processing. + KNOT_STATE_DONE, //!< Finished. + KNOT_STATE_FAIL, //!< Error. + KNOT_STATE_FINAL, //!< Finished and finalized. + KNOT_STATE_IGNORE, //!< Data has been ignored. +} knot_layer_state_t; + +typedef struct knot_layer_api knot_layer_api_t; + +/*! \brief Packet processing context. */ +typedef struct { + const knot_layer_api_t *api; //!< Layer API. + knot_mm_t *mm; //!< Processing memory context. + knot_layer_state_t state; //!< Processing state. + void *data; //!< Module specific. + tsig_ctx_t *tsig; //!< TODO: remove + unsigned flags; //!< Custom flags. +} knot_layer_t; + +/*! \brief Packet processing module API. */ +struct knot_layer_api { + int (*begin)(knot_layer_t *ctx, void *params); + int (*reset)(knot_layer_t *ctx); + int (*finish)(knot_layer_t *ctx); + int (*consume)(knot_layer_t *ctx, knot_pkt_t *pkt); + int (*produce)(knot_layer_t *ctx, knot_pkt_t *pkt); +}; + +/*! \brief Helper for conditional layer call. */ +#define LAYER_CALL(layer, func, ...) \ + assert(layer->api); \ + if (layer->api->func) { \ + layer->state = layer->api->func(layer, ##__VA_ARGS__); \ + } + +/*! + * \brief Initialize packet processing context. + * + * \param ctx Layer context. + * \param mm Memory context. + * \param api Layer API. + */ +inline static void knot_layer_init(knot_layer_t *ctx, knot_mm_t *mm, + const knot_layer_api_t *api) +{ + memset(ctx, 0, sizeof(*ctx)); + + ctx->mm = mm; + ctx->api = api; + ctx->state = KNOT_STATE_NOOP; +} + +/*! + * \brief Prepare packet processing. + * + * \param ctx Layer context. + * \param params Initialization params. + */ +inline static void knot_layer_begin(knot_layer_t *ctx, void *params) +{ + LAYER_CALL(ctx, begin, params); +} + +/*! + * \brief Reset current packet processing context. + * + * \param ctx Layer context. + */ +inline static void knot_layer_reset(knot_layer_t *ctx) +{ + LAYER_CALL(ctx, reset); +} + +/*! + * \brief Finish and close packet processing context. + * + * \param ctx Layer context. + */ +inline static void knot_layer_finish(knot_layer_t *ctx) +{ + LAYER_CALL(ctx, finish); +} + +/*! + * \brief Add more data to layer processing. + * + * \param ctx Layer context. + * \param pkt Data packet. + */ +inline static void knot_layer_consume(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + LAYER_CALL(ctx, consume, pkt); +} + +/*! + * \brief Generate output from layer. + * + * \param ctx Layer context. + * \param pkt Data packet. + */ +inline static void knot_layer_produce(knot_layer_t *ctx, knot_pkt_t *pkt) +{ + LAYER_CALL(ctx, produce, pkt); +} diff --git a/src/knot/query/query.c b/src/knot/query/query.c new file mode 100644 index 0000000..877851a --- /dev/null +++ b/src/knot/query/query.c @@ -0,0 +1,85 @@ +/* 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/query/query.h" + +#include "contrib/wire_ctx.h" +#include "libdnssec/random.h" + +void query_init_pkt(knot_pkt_t *pkt) +{ + if (pkt == NULL) { + return; + } + + knot_pkt_clear(pkt); + knot_wire_set_id(pkt->wire, dnssec_random_uint16_t()); +} + +query_edns_data_t query_edns_data_init(conf_t *conf, int remote_family, + query_edns_opt_t opts) +{ + assert(conf); + + query_edns_data_t edns = { + .max_payload = remote_family == AF_INET ? + conf->cache.srv_udp_max_payload_ipv4 : + conf->cache.srv_udp_max_payload_ipv6, + .do_flag = (opts & QUERY_EDNS_OPT_DO), + .expire_option = (opts & QUERY_EDNS_OPT_EXPIRE) + }; + + return edns; +} + +int query_put_edns(knot_pkt_t *pkt, const query_edns_data_t *edns) +{ + if (!pkt || !edns) { + return KNOT_EINVAL; + } + + // Construct EDNS RR + + knot_rrset_t opt_rr = { 0 }; + int ret = knot_edns_init(&opt_rr, edns->max_payload, 0, KNOT_EDNS_VERSION, &pkt->mm); + if (ret != KNOT_EOK) { + return ret; + } + + if (edns->do_flag) { + knot_edns_set_do(&opt_rr); + } + + if (edns->expire_option) { + ret = knot_edns_add_option(&opt_rr, KNOT_EDNS_OPTION_EXPIRE, 0, NULL, &pkt->mm); + if (ret != KNOT_EOK) { + knot_rrset_clear(&opt_rr, &pkt->mm); + return ret; + } + } + + // Add result into the packet + + knot_pkt_begin(pkt, KNOT_ADDITIONAL); + + ret = knot_pkt_put(pkt, KNOT_COMPR_HINT_NOCOMP, &opt_rr, KNOT_PF_FREE); + if (ret != KNOT_EOK) { + knot_rrset_clear(&opt_rr, &pkt->mm); + return ret; + } + + return KNOT_EOK; +} diff --git a/src/knot/query/query.h b/src/knot/query/query.h new file mode 100644 index 0000000..fbf437d --- /dev/null +++ b/src/knot/query/query.h @@ -0,0 +1,66 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/conf/conf.h" +#include "knot/nameserver/log.h" +#include "libknot/packet/pkt.h" + +/*! + * \brief EDNS data. + */ +typedef struct { + uint16_t max_payload; + bool do_flag; + bool expire_option; +} query_edns_data_t; + +typedef enum { + QUERY_EDNS_OPT_DO = 1 << 0, + QUERY_EDNS_OPT_EXPIRE = 1 << 1, +} query_edns_opt_t; + +/*! + * \brief Initialize new packet. + * + * Clear the packet and generate random transaction ID. + * + * \param pkt Packet to initialize. + */ +void query_init_pkt(knot_pkt_t *pkt); + +/*! + * \brief Initialize EDNS parameters from server configuration. + * + * \param[in] conf Server configuration. + * \param[in] remote_family Address family for remote host. + * \param[in] opts EDNS options. + * + * \return EDNS parameters. + */ +query_edns_data_t query_edns_data_init(conf_t *conf, int remote_family, + query_edns_opt_t opts); + +/*! + * \brief Append EDNS into the packet. + * + * \param pkt Packet to add EDNS into. + * \param edns EDNS data. + * + * \return KNOT_E* + */ +int query_put_edns(knot_pkt_t *pkt, const query_edns_data_t *edns); diff --git a/src/knot/query/requestor.c b/src/knot/query/requestor.c new file mode 100644 index 0000000..8643f74 --- /dev/null +++ b/src/knot/query/requestor.c @@ -0,0 +1,378 @@ +/* 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 "libknot/attribute.h" +#include "knot/common/unreachable.h" +#include "knot/query/requestor.h" +#include "libknot/errcode.h" +#include "contrib/conn_pool.h" +#include "contrib/mempattern.h" +#include "contrib/net.h" +#include "contrib/sockaddr.h" + +static bool use_tcp(knot_request_t *request) +{ + return (request->flags & KNOT_REQUEST_UDP) == 0; +} + +static bool is_answer_to_query(const knot_pkt_t *query, const knot_pkt_t *answer) +{ + return knot_wire_get_id(query->wire) == knot_wire_get_id(answer->wire); +} + +/*! \brief Ensure a socket is connected. */ +static int request_ensure_connected(knot_request_t *request, bool *reused_fd) +{ + if (request->fd >= 0) { + return KNOT_EOK; + } + + int sock_type = use_tcp(request) ? SOCK_STREAM : SOCK_DGRAM; + + if (sock_type == SOCK_STREAM) { + request->fd = conn_pool_get(global_conn_pool, + &request->source, + &request->remote); + if (request->fd >= 0) { + if (reused_fd != NULL) { + *reused_fd = true; + } + return KNOT_EOK; + } + + if (knot_unreachable_is(global_unreachables, &request->remote, + &request->source)) { + return KNOT_EUNREACH; + } + } + + request->fd = net_connected_socket(sock_type, + &request->remote, + &request->source, + request->flags & KNOT_REQUEST_TFO); + if (request->fd < 0) { + if (request->fd == KNOT_ETIMEOUT) { + knot_unreachable_add(global_unreachables, &request->remote, + &request->source); + } + return request->fd; + } + + return KNOT_EOK; +} + +static int request_send(knot_request_t *request, int timeout_ms, bool *reused_fd) +{ + /* Initiate non-blocking connect if not connected. */ + *reused_fd = false; + int ret = request_ensure_connected(request, reused_fd); + if (ret != KNOT_EOK) { + return ret; + } + + /* Send query, construct if not exists. */ + knot_pkt_t *query = request->query; + uint8_t *wire = query->wire; + size_t wire_len = query->size; + struct sockaddr_storage *tfo_addr = (request->flags & KNOT_REQUEST_TFO) ? + &request->remote : NULL; + + /* Send query. */ + if (use_tcp(request)) { + ret = net_dns_tcp_send(request->fd, wire, wire_len, timeout_ms, + tfo_addr); + if (ret == KNOT_ETIMEOUT) { // Includes establishing conn which times out. + knot_unreachable_add(global_unreachables, &request->remote, + &request->source); + } + } else { + ret = net_dgram_send(request->fd, wire, wire_len, NULL); + } + if (ret < 0) { + return ret; + } else if (ret != wire_len) { + return KNOT_ECONN; + } + + return KNOT_EOK; +} + +static int request_recv(knot_request_t *request, int timeout_ms) +{ + knot_pkt_t *resp = request->resp; + knot_pkt_clear(resp); + + /* Wait for readability */ + int ret = request_ensure_connected(request, NULL); + if (ret != KNOT_EOK) { + return ret; + } + + /* Receive it */ + if (use_tcp(request)) { + ret = net_dns_tcp_recv(request->fd, resp->wire, resp->max_size, timeout_ms); + } else { + ret = net_dgram_recv(request->fd, resp->wire, resp->max_size, timeout_ms); + } + if (ret <= 0) { + resp->size = 0; + if (ret == 0) { + return KNOT_ECONN; + } + return ret; + } + + resp->size = ret; + return ret; +} + +knot_request_t *knot_request_make(knot_mm_t *mm, + const struct sockaddr_storage *remote, + const struct sockaddr_storage *source, + knot_pkt_t *query, + const knot_tsig_key_t *tsig_key, + knot_request_flag_t flags) +{ + if (remote == NULL || query == NULL) { + return NULL; + } + + knot_request_t *request = mm_calloc(mm, 1, sizeof(*request)); + if (request == NULL) { + return NULL; + } + + request->resp = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, mm); + if (request->resp == NULL) { + mm_free(mm, request); + return NULL; + } + + request->query = query; + request->fd = -1; + request->flags = flags; + memcpy(&request->remote, remote, sockaddr_len(remote)); + if (source) { + memcpy(&request->source, source, sockaddr_len(source)); + } else { + request->source.ss_family = AF_UNSPEC; + } + + if (tsig_key && tsig_key->algorithm == DNSSEC_TSIG_UNKNOWN) { + tsig_key = NULL; + } + tsig_init(&request->tsig, tsig_key); + + return request; +} + +void knot_request_free(knot_request_t *request, knot_mm_t *mm) +{ + if (request == NULL) { + return; + } + + if (request->fd >= 0 && use_tcp(request) && + (request->flags & KNOT_REQUEST_KEEP)) { + request->fd = conn_pool_put(global_conn_pool, + &request->source, + &request->remote, + request->fd); + } + if (request->fd >= 0) { + close(request->fd); + } + knot_pkt_free(request->query); + knot_pkt_free(request->resp); + tsig_cleanup(&request->tsig); + + mm_free(mm, request); +} + +int knot_requestor_init(knot_requestor_t *requestor, + const knot_layer_api_t *proc, void *proc_param, + knot_mm_t *mm) +{ + if (requestor == NULL || proc == NULL) { + return KNOT_EINVAL; + } + + memset(requestor, 0, sizeof(*requestor)); + + requestor->mm = mm; + knot_layer_init(&requestor->layer, mm, proc); + knot_layer_begin(&requestor->layer, proc_param); + + return KNOT_EOK; +} + +void knot_requestor_clear(knot_requestor_t *requestor) +{ + if (requestor == NULL) { + return; + } + + knot_layer_finish(&requestor->layer); + + memset(requestor, 0, sizeof(*requestor)); +} + +static int request_reset(knot_requestor_t *req, knot_request_t *last) +{ + knot_layer_reset(&req->layer); + tsig_reset(&last->tsig); + + if (req->layer.flags & KNOT_REQUESTOR_CLOSE) { + req->layer.flags &= ~KNOT_REQUESTOR_CLOSE; + if (last->fd >= 0) { + close(last->fd); + last->fd = -1; + } + } + + if (req->layer.state == KNOT_STATE_RESET) { + return KNOT_EPROCESSING; + } + + return KNOT_EOK; +} + +static int request_produce(knot_requestor_t *req, knot_request_t *last, + int timeout_ms) +{ + knot_layer_produce(&req->layer, last->query); + + int ret = tsig_sign_packet(&last->tsig, last->query); + if (ret != KNOT_EOK) { + return ret; + } + + // TODO: verify condition + if (req->layer.state == KNOT_STATE_CONSUME) { + bool reused_fd = false; + ret = request_send(last, timeout_ms, &reused_fd); + if (reused_fd) { + req->layer.flags |= KNOT_REQUESTOR_REUSED; + } else { + req->layer.flags &= ~KNOT_REQUESTOR_REUSED; + } + } + + return ret; +} + +static int request_consume(knot_requestor_t *req, knot_request_t *last, + int timeout_ms) +{ + int ret = request_recv(last, timeout_ms); + if (ret < 0) { + return ret; + } + + ret = knot_pkt_parse(last->resp, 0); + if (ret != KNOT_EOK) { + return ret; + } + + if (!is_answer_to_query(last->query, last->resp)) { + return KNOT_EMALF; + } + + ret = tsig_verify_packet(&last->tsig, last->resp); + if (ret != KNOT_EOK) { + return ret; + } + + if (tsig_unsigned_count(&last->tsig) >= 100) { + return KNOT_TSIG_EBADSIG; + } + + knot_layer_consume(&req->layer, last->resp); + + return KNOT_EOK; +} + +static bool layer_active(knot_layer_state_t state) +{ + switch (state) { + case KNOT_STATE_CONSUME: + case KNOT_STATE_PRODUCE: + case KNOT_STATE_RESET: + return true; + default: + return false; + } +} + +static int request_io(knot_requestor_t *req, knot_request_t *last, + int timeout_ms) +{ + switch (req->layer.state) { + case KNOT_STATE_CONSUME: + return request_consume(req, last, timeout_ms); + case KNOT_STATE_PRODUCE: + return request_produce(req, last, timeout_ms); + case KNOT_STATE_RESET: + return request_reset(req, last); + default: + return KNOT_EINVAL; + } +} + +int knot_requestor_exec(knot_requestor_t *requestor, knot_request_t *request, + int timeout_ms) +{ + if (requestor == NULL || request == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + + requestor->layer.tsig = &request->tsig; + + /* Do I/O until the processing is satisfied or fails. */ + while (layer_active(requestor->layer.state)) { + ret = request_io(requestor, request, timeout_ms); + if (ret != KNOT_EOK) { + knot_layer_finish(&requestor->layer); + return ret; + } + } + + /* Expect complete request. */ + switch (requestor->layer.state) { + case KNOT_STATE_DONE: + request->flags |= KNOT_REQUEST_KEEP; + break; + case KNOT_STATE_IGNORE: + ret = KNOT_ERROR; + break; + default: + ret = KNOT_EPROCESSING; + } + + /* Verify last TSIG */ + if (tsig_unsigned_count(&request->tsig) != 0) { + ret = KNOT_TSIG_EBADSIG; + } + + /* Finish current query processing. */ + knot_layer_finish(&requestor->layer); + + return ret; +} diff --git a/src/knot/query/requestor.h b/src/knot/query/requestor.h new file mode 100644 index 0000000..aa90cd5 --- /dev/null +++ b/src/knot/query/requestor.h @@ -0,0 +1,119 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <sys/socket.h> +#include <sys/time.h> + +#include "knot/nameserver/tsig_ctx.h" +#include "knot/query/layer.h" +#include "libknot/mm_ctx.h" +#include "libknot/rrtype/tsig.h" + +typedef enum { + KNOT_REQUEST_NONE = 0, /*!< Empty flag. */ + KNOT_REQUEST_UDP = 1 << 0, /*!< Use UDP for requests. */ + KNOT_REQUEST_TFO = 1 << 1, /*!< Enable TCP Fast Open for requests. */ + KNOT_REQUEST_KEEP = 1 << 2, /*!< Keep upstream TCP connection in pool for later reuse. */ +} knot_request_flag_t; + +typedef enum { + KNOT_REQUESTOR_CLOSE = 1 << 0, /*!< Close the connection indication. */ + KNOT_REQUESTOR_REUSED = 1 << 1, /*!< Reused FD indication. */ +} knot_requestor_flag_t; + +/*! \brief Requestor structure. + * + * Requestor holds a FIFO of pending queries. + */ +typedef struct { + knot_mm_t *mm; /*!< Memory context. */ + knot_layer_t layer; /*!< Response processing layer. */ +} knot_requestor_t; + +/*! \brief Request data (socket, payload, response, TSIG and endpoints). */ +typedef struct { + int fd; + knot_request_flag_t flags; + struct sockaddr_storage remote, source; + knot_pkt_t *query; + knot_pkt_t *resp; + tsig_ctx_t tsig; + + knot_sign_context_t sign; /*!< Required for async. DDNS processing. */ +} knot_request_t; + +/*! + * \brief Make request out of endpoints and query. + * + * \param mm Memory context. + * \param remote Remote endpoint address. + * \param source Source address (or NULL). + * \param query Query message. + * \param tsig_key TSIG key for authentication. + * \param flags Request flags. + * + * \return Prepared request or NULL in case of error. + */ +knot_request_t *knot_request_make(knot_mm_t *mm, + const struct sockaddr_storage *remote, + const struct sockaddr_storage *source, + knot_pkt_t *query, + const knot_tsig_key_t *tsig_key, + knot_request_flag_t flags); + +/*! + * \brief Free request and associated data. + * + * \param request Freed request. + * \param mm Memory context. + */ +void knot_request_free(knot_request_t *request, knot_mm_t *mm); + +/*! + * \brief Initialize requestor structure. + * + * \param requestor Requestor instance. + * \param proc Response processing module. + * \param proc_param Processing module context. + * \param mm Memory context. + * + * \return KNOT_EOK or error + */ +int knot_requestor_init(knot_requestor_t *requestor, + const knot_layer_api_t *proc, void *proc_param, + knot_mm_t *mm); + +/*! + * \brief Clear the requestor structure and close pending queries. + * + * \param requestor Requestor instance. + */ +void knot_requestor_clear(knot_requestor_t *requestor); + +/*! + * \brief Execute a request. + * + * \param requestor Requestor instance. + * \param request Request instance. + * \param timeout_ms Timeout of each operation in milliseconds (-1 for infinity). + * + * \return KNOT_EOK or error + */ +int knot_requestor_exec(knot_requestor_t *requestor, + knot_request_t *request, + int timeout_ms); diff --git a/src/knot/server/dthreads.c b/src/knot/server/dthreads.c new file mode 100644 index 0000000..74203ac --- /dev/null +++ b/src/knot/server/dthreads.c @@ -0,0 +1,767 @@ +/* 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 <signal.h> +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <unistd.h> +#include <errno.h> +#include <urcu.h> + +#ifdef HAVE_PTHREAD_NP_H +#include <pthread_np.h> +#endif /* HAVE_PTHREAD_NP_H */ + +#include "knot/server/dthreads.h" +#include "libknot/libknot.h" + +/* BSD cpu set compatibility. */ +#if defined(HAVE_CPUSET_BSD) +typedef cpuset_t cpu_set_t; +#endif + +/*! \brief Lock thread state for R/W. */ +static inline void lock_thread_rw(dthread_t *thread) +{ + pthread_mutex_lock(&thread->_mx); +} +/*! \brief Unlock thread state for R/W. */ +static inline void unlock_thread_rw(dthread_t *thread) +{ + pthread_mutex_unlock(&thread->_mx); +} + +/*! \brief Signalize thread state change. */ +static inline void unit_signalize_change(dt_unit_t *unit) +{ + pthread_mutex_lock(&unit->_report_mx); + pthread_cond_signal(&unit->_report); + pthread_mutex_unlock(&unit->_report_mx); +} + +/*! + * \brief Update thread state with notification. + * \param thread Given thread. + * \param state New state for thread. + * \retval 0 on success. + * \retval <0 on error (EINVAL, ENOTSUP). + */ +static inline int dt_update_thread(dthread_t *thread, int state) +{ + if (thread == 0) { + return KNOT_EINVAL; + } + + // Cancel with lone thread + dt_unit_t *unit = thread->unit; + if (unit == 0) { + return KNOT_ENOTSUP; + } + + // Cancel current runnable if running + pthread_mutex_lock(&unit->_notify_mx); + lock_thread_rw(thread); + if (thread->state & (ThreadIdle | ThreadActive)) { + + // Update state + thread->state = state; + unlock_thread_rw(thread); + + // Notify thread + pthread_cond_broadcast(&unit->_notify); + pthread_mutex_unlock(&unit->_notify_mx); + } else { + /* Unable to update thread, it is already dead. */ + unlock_thread_rw(thread); + pthread_mutex_unlock(&unit->_notify_mx); + return KNOT_EINVAL; + } + + return KNOT_EOK; +} + +/*! + * \brief Thread entrypoint function. + * + * When a thread is created and started, it immediately enters this function. + * Depending on thread state, it either enters runnable or + * blocks until it is awakened. + * + * This function also handles "ThreadIdle" state to quickly suspend and resume + * threads and mitigate thread creation costs. Also, thread runnable may + * be changed to alter the thread behavior on runtime + */ +static void *thread_ep(void *data) +{ + dthread_t *thread = (dthread_t *)data; + if (thread == 0) { + return 0; + } + + // Check if is a member of unit + dt_unit_t *unit = thread->unit; + if (unit == 0) { + return 0; + } + + // Unblock SIGALRM for synchronization + sigset_t mask; + (void)sigemptyset(&mask); + sigaddset(&mask, SIGALRM); + pthread_sigmask(SIG_UNBLOCK, &mask, NULL); + + rcu_register_thread(); + + // Run loop + for (;;) { + + // Check thread state + lock_thread_rw(thread); + if (thread->state == ThreadDead) { + unlock_thread_rw(thread); + break; + } + + // Update data + thread->data = thread->_adata; + runnable_t _run = thread->run; + + // Start runnable if thread is marked Active + if ((thread->state == ThreadActive) && (thread->run != 0)) { + unlock_thread_rw(thread); + _run(thread); + } else { + unlock_thread_rw(thread); + } + + // If the runnable was cancelled, start new iteration + lock_thread_rw(thread); + if (thread->state & ThreadCancelled) { + thread->state &= ~ThreadCancelled; + unlock_thread_rw(thread); + continue; + } + unlock_thread_rw(thread); + + // Runnable finished without interruption, mark as Idle + pthread_mutex_lock(&unit->_notify_mx); + lock_thread_rw(thread); + if (thread->state & ThreadActive) { + thread->state &= ~ThreadActive; + thread->state |= ThreadIdle; + } + + // Go to sleep if idle + if (thread->state & ThreadIdle) { + unlock_thread_rw(thread); + + // Signalize state change + unit_signalize_change(unit); + + // Wait for notification from unit + pthread_cond_wait(&unit->_notify, &unit->_notify_mx); + pthread_mutex_unlock(&unit->_notify_mx); + } else { + unlock_thread_rw(thread); + pthread_mutex_unlock(&unit->_notify_mx); + } + } + + // Thread destructor + if (thread->destruct) { + thread->destruct(thread); + } + + // Report thread state change + unit_signalize_change(unit); + lock_thread_rw(thread); + thread->state |= ThreadJoinable; + unlock_thread_rw(thread); + rcu_unregister_thread(); + + // Return + return 0; +} + +/*! + * \brief Create single thread. + * \retval New thread instance on success. + * \retval NULL on error. + */ +static dthread_t *dt_create_thread(dt_unit_t *unit) +{ + // Alloc thread + dthread_t *thread = malloc(sizeof(dthread_t)); + if (thread == 0) { + return 0; + } + + memset(thread, 0, sizeof(dthread_t)); + + // Blank thread state + thread->state = ThreadJoined; + pthread_mutex_init(&thread->_mx, 0); + + // Set membership in unit + thread->unit = unit; + + // Initialize attribute + pthread_attr_t *attr = &thread->_attr; + pthread_attr_init(attr); + //pthread_attr_setinheritsched(attr, PTHREAD_INHERIT_SCHED); + //pthread_attr_setschedpolicy(attr, SCHED_OTHER); + pthread_attr_setstacksize(attr, 1024*1024); + return thread; +} + +/*! \brief Delete single thread. */ +static void dt_delete_thread(dthread_t **thread) +{ + if (!thread || !*thread) { + return; + } + + dthread_t* thr = *thread; + thr->unit = 0; + *thread = 0; + + // Delete attribute + pthread_attr_destroy(&(thr)->_attr); + + // Delete mutex + pthread_mutex_destroy(&(thr)->_mx); + + // Free memory + free(thr); +} + +static dt_unit_t *dt_create_unit(int count) +{ + if (count <= 0) { + return 0; + } + + dt_unit_t *unit = malloc(sizeof(dt_unit_t)); + if (unit == 0) { + return 0; + } + + // Initialize conditions + if (pthread_cond_init(&unit->_notify, 0) != 0) { + free(unit); + return 0; + } + if (pthread_cond_init(&unit->_report, 0) != 0) { + pthread_cond_destroy(&unit->_notify); + free(unit); + return 0; + } + + // Initialize mutexes + if (pthread_mutex_init(&unit->_notify_mx, 0) != 0) { + pthread_cond_destroy(&unit->_notify); + pthread_cond_destroy(&unit->_report); + free(unit); + return 0; + } + if (pthread_mutex_init(&unit->_report_mx, 0) != 0) { + pthread_cond_destroy(&unit->_notify); + pthread_cond_destroy(&unit->_report); + pthread_mutex_destroy(&unit->_notify_mx); + free(unit); + return 0; + } + if (pthread_mutex_init(&unit->_mx, 0) != 0) { + pthread_cond_destroy(&unit->_notify); + pthread_cond_destroy(&unit->_report); + pthread_mutex_destroy(&unit->_notify_mx); + pthread_mutex_destroy(&unit->_report_mx); + free(unit); + return 0; + } + + // Save unit size + unit->size = count; + + // Alloc threads + unit->threads = calloc(count, sizeof(dthread_t *)); + if (unit->threads == 0) { + pthread_cond_destroy(&unit->_notify); + pthread_cond_destroy(&unit->_report); + pthread_mutex_destroy(&unit->_notify_mx); + pthread_mutex_destroy(&unit->_report_mx); + pthread_mutex_destroy(&unit->_mx); + free(unit); + return 0; + } + + // Initialize threads + int init_success = 1; + for (int i = 0; i < count; ++i) { + unit->threads[i] = dt_create_thread(unit); + if (unit->threads[i] == 0) { + init_success = 0; + break; + } + } + + // Check thread initialization + if (!init_success) { + + // Delete created threads + for (int i = 0; i < count; ++i) { + dt_delete_thread(&unit->threads[i]); + } + + // Free rest of the unit + pthread_cond_destroy(&unit->_notify); + pthread_cond_destroy(&unit->_report); + pthread_mutex_destroy(&unit->_notify_mx); + pthread_mutex_destroy(&unit->_report_mx); + pthread_mutex_destroy(&unit->_mx); + free(unit->threads); + free(unit); + return 0; + } + + return unit; +} + +dt_unit_t *dt_create(int count, runnable_t runnable, runnable_t destructor, void *data) +{ + if (count <= 0) { + return 0; + } + + // Create unit + dt_unit_t *unit = dt_create_unit(count); + if (unit == 0) { + return 0; + } + + // Set threads common purpose + pthread_mutex_lock(&unit->_notify_mx); + dt_unit_lock(unit); + + for (int i = 0; i < count; ++i) { + dthread_t *thread = unit->threads[i]; + lock_thread_rw(thread); + thread->run = runnable; + thread->destruct = destructor; + thread->_adata = data; + unlock_thread_rw(thread); + } + + dt_unit_unlock(unit); + pthread_mutex_unlock(&unit->_notify_mx); + + return unit; +} + +void dt_delete(dt_unit_t **unit) +{ + /* + * All threads must be stopped or idle at this point, + * or else the behavior is undefined. + * Sorry. + */ + + if (unit == 0) { + return; + } + if (*unit == 0) { + return; + } + + // Compact and reclaim idle threads + dt_unit_t *d_unit = *unit; + dt_compact(d_unit); + + // Delete threads + for (int i = 0; i < d_unit->size; ++i) { + dt_delete_thread(&d_unit->threads[i]); + } + + // Deinit mutexes + pthread_mutex_destroy(&d_unit->_notify_mx); + pthread_mutex_destroy(&d_unit->_report_mx); + pthread_mutex_destroy(&d_unit->_mx); + + // Deinit conditions + pthread_cond_destroy(&d_unit->_notify); + pthread_cond_destroy(&d_unit->_report); + + // Free memory + free(d_unit->threads); + free(d_unit); + *unit = 0; +} + +static int dt_start_id(dthread_t *thread) +{ + if (thread == 0) { + return KNOT_EINVAL; + } + + lock_thread_rw(thread); + + // Update state + int prev_state = thread->state; + thread->state |= ThreadActive; + thread->state &= ~ThreadIdle; + thread->state &= ~ThreadDead; + thread->state &= ~ThreadJoined; + thread->state &= ~ThreadJoinable; + + // Do not re-create running threads + if (prev_state != ThreadJoined) { + unlock_thread_rw(thread); + return 0; + } + + // Start thread + sigset_t mask_all, mask_old; + sigfillset(&mask_all); + sigdelset(&mask_all, SIGPROF); + pthread_sigmask(SIG_SETMASK, &mask_all, &mask_old); + int res = pthread_create(&thread->_thr, /* pthread_t */ + &thread->_attr, /* pthread_attr_t */ + thread_ep, /* routine: thread_ep */ + thread); /* passed object: dthread_t */ + pthread_sigmask(SIG_SETMASK, &mask_old, NULL); + + // Unlock thread + unlock_thread_rw(thread); + return res; +} + +int dt_start(dt_unit_t *unit) +{ + if (unit == 0) { + return KNOT_EINVAL; + } + + // Lock unit + pthread_mutex_lock(&unit->_notify_mx); + dt_unit_lock(unit); + for (int i = 0; i < unit->size; ++i) { + + dthread_t *thread = unit->threads[i]; + int res = dt_start_id(thread); + if (res != 0) { + dt_unit_unlock(unit); + pthread_mutex_unlock(&unit->_notify_mx); + return res; + } + } + + // Unlock unit + dt_unit_unlock(unit); + pthread_cond_broadcast(&unit->_notify); + pthread_mutex_unlock(&unit->_notify_mx); + return KNOT_EOK; +} + +int dt_signalize(dthread_t *thread, int signum) +{ + if (thread == 0) { + return KNOT_EINVAL; + } + + int ret = pthread_kill(thread->_thr, signum); + + /* Not thread id found or invalid signum. */ + if (ret == EINVAL || ret == ESRCH) { + return KNOT_EINVAL; + } + + /* Generic error. */ + if (ret < 0) { + return KNOT_ERROR; + } + + return KNOT_EOK; +} + +int dt_join(dt_unit_t *unit) +{ + if (unit == 0) { + return KNOT_EINVAL; + } + + for (;;) { + + // Lock unit + pthread_mutex_lock(&unit->_report_mx); + dt_unit_lock(unit); + + // Browse threads + int active_threads = 0; + for (int i = 0; i < unit->size; ++i) { + + // Count active or cancelled but pending threads + dthread_t *thread = unit->threads[i]; + lock_thread_rw(thread); + if (thread->state & (ThreadActive|ThreadCancelled)) { + ++active_threads; + } + + // Reclaim dead threads, but only fast + if (thread->state & ThreadJoinable) { + unlock_thread_rw(thread); + pthread_join(thread->_thr, 0); + lock_thread_rw(thread); + thread->state = ThreadJoined; + unlock_thread_rw(thread); + } else { + unlock_thread_rw(thread); + } + } + + // Unlock unit + dt_unit_unlock(unit); + + // Check result + if (active_threads == 0) { + pthread_mutex_unlock(&unit->_report_mx); + break; + } + + // Wait for a thread to finish + pthread_cond_wait(&unit->_report, &unit->_report_mx); + pthread_mutex_unlock(&unit->_report_mx); + } + + return KNOT_EOK; +} + +int dt_stop(dt_unit_t *unit) +{ + if (unit == 0) { + return KNOT_EINVAL; + } + + // Lock unit + pthread_mutex_lock(&unit->_notify_mx); + dt_unit_lock(unit); + + // Signalize all threads to stop + for (int i = 0; i < unit->size; ++i) { + + // Lock thread + dthread_t *thread = unit->threads[i]; + lock_thread_rw(thread); + if (thread->state & (ThreadIdle | ThreadActive)) { + thread->state = ThreadDead | ThreadCancelled; + dt_signalize(thread, SIGALRM); + } + unlock_thread_rw(thread); + } + + // Unlock unit + dt_unit_unlock(unit); + + // Broadcast notification + pthread_cond_broadcast(&unit->_notify); + pthread_mutex_unlock(&unit->_notify_mx); + + return KNOT_EOK; +} + +int dt_setaffinity(dthread_t *thread, unsigned* cpu_id, size_t cpu_count) +{ + if (thread == NULL) { + return KNOT_EINVAL; + } + +#ifdef HAVE_PTHREAD_SETAFFINITY_NP + int ret = -1; + +/* Linux, FreeBSD interface. */ +#if defined(HAVE_CPUSET_LINUX) || defined(HAVE_CPUSET_BSD) + cpu_set_t set; + CPU_ZERO(&set); + for (unsigned i = 0; i < cpu_count; ++i) { + CPU_SET(cpu_id[i], &set); + } + ret = pthread_setaffinity_np(thread->_thr, sizeof(cpu_set_t), &set); +/* NetBSD interface. */ +#elif defined(HAVE_CPUSET_NETBSD) + cpuset_t *set = cpuset_create(); + if (set == NULL) { + return KNOT_ENOMEM; + } + cpuset_zero(set); + for (unsigned i = 0; i < cpu_count; ++i) { + cpuset_set(cpu_id[i], set); + } + ret = pthread_setaffinity_np(thread->_thr, cpuset_size(set), set); + cpuset_destroy(set); +#endif /* interface */ + + if (ret < 0) { + return KNOT_ERROR; + } + +#else /* HAVE_PTHREAD_SETAFFINITY_NP */ + return KNOT_ENOTSUP; +#endif + + return KNOT_EOK; +} + +int dt_activate(dthread_t *thread) +{ + return dt_update_thread(thread, ThreadActive); +} + +int dt_cancel(dthread_t *thread) +{ + return dt_update_thread(thread, ThreadIdle | ThreadCancelled); +} + +int dt_compact(dt_unit_t *unit) +{ + if (unit == 0) { + return KNOT_EINVAL; + } + + // Lock unit + pthread_mutex_lock(&unit->_notify_mx); + dt_unit_lock(unit); + + // Reclaim all Idle threads + for (int i = 0; i < unit->size; ++i) { + + // Locked state update + dthread_t *thread = unit->threads[i]; + lock_thread_rw(thread); + if (thread->state & (ThreadIdle)) { + thread->state = ThreadDead | ThreadCancelled; + dt_signalize(thread, SIGALRM); + } + unlock_thread_rw(thread); + } + + // Notify all threads + pthread_cond_broadcast(&unit->_notify); + pthread_mutex_unlock(&unit->_notify_mx); + + // Join all threads + for (int i = 0; i < unit->size; ++i) { + + // Reclaim all dead threads + dthread_t *thread = unit->threads[i]; + lock_thread_rw(thread); + if (thread->state & (ThreadDead)) { + unlock_thread_rw(thread); + pthread_join(thread->_thr, 0); + lock_thread_rw(thread); + thread->state = ThreadJoined; + unlock_thread_rw(thread); + } else { + unlock_thread_rw(thread); + } + } + + // Unlock unit + dt_unit_unlock(unit); + + return KNOT_EOK; +} + +int dt_online_cpus(void) +{ + int ret = -1; +/* Linux, FreeBSD, NetBSD, OpenBSD, macOS/OS X 10.4+, Solaris */ +#ifdef _SC_NPROCESSORS_ONLN + ret = (int) sysconf(_SC_NPROCESSORS_ONLN); +#else +/* OS X < 10.4 and some other OS's (if not handled by sysconf() above) */ +/* hw.ncpu won't work on FreeBSD, OpenBSD, NetBSD, DragonFlyBSD, and recent macOS/OS X. */ +#if HAVE_SYSCTLBYNAME + size_t rlen = sizeof(int); + if (sysctlbyname("hw.ncpu", &ret, &rlen, NULL, 0) < 0) { + ret = -1; + } +#endif +#endif + return ret; +} + +int dt_optimal_size(void) +{ + int ret = dt_online_cpus(); + if (ret > 1) { + return ret; + } + + return DEFAULT_THR_COUNT; +} + +int dt_is_cancelled(dthread_t *thread) +{ + if (thread == 0) { + return 0; + } + + return thread->state & ThreadCancelled; /* No need to be locked. */ +} + +unsigned dt_get_id(dthread_t *thread) +{ + if (thread == NULL || thread->unit == NULL) { + return 0; + } + + dt_unit_t *unit = thread->unit; + for(int tid = 0; tid < unit->size; ++tid) { + if (thread == unit->threads[tid]) { + return tid; + } + } + + return 0; +} + +int dt_unit_lock(dt_unit_t *unit) +{ + if (unit == 0) { + return KNOT_EINVAL; + } + + int ret = pthread_mutex_lock(&unit->_mx); + if (ret < 0) { + return knot_map_errno(); + } + + return KNOT_EOK; +} + +int dt_unit_unlock(dt_unit_t *unit) +{ + if (unit == 0) { + return KNOT_EINVAL; + } + + int ret = pthread_mutex_unlock(&unit->_mx); + if (ret < 0) { + return knot_map_errno(); + } + + return KNOT_EOK; +} diff --git a/src/knot/server/dthreads.h b/src/knot/server/dthreads.h new file mode 100644 index 0000000..0c243a1 --- /dev/null +++ b/src/knot/server/dthreads.h @@ -0,0 +1,295 @@ +/* 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/>. + */ + +/*! + * \brief Threading API. + * + * Dynamic threads provide: + * - coherent and incoherent threading capabilities + * - thread repurposing + * - thread prioritization + * - on-the-fly changing of threading unit size + * + * Coherent threading unit is when all threads execute + * the same runnable function. + * + * Incoherent function is when at least one thread executes + * a different runnable than the others. + */ + +#pragma once + +#include <pthread.h> + +#define DEFAULT_THR_COUNT 2 /*!< Default thread count. */ + +/* Forward decls */ +struct dthread; +struct dt_unit; + +/*! + * \brief Thread state enumeration. + */ +typedef enum { + ThreadJoined = 1 << 0, /*!< Thread is finished and joined. */ + ThreadJoinable = 1 << 1, /*!< Thread is waiting to be reclaimed. */ + ThreadCancelled = 1 << 2, /*!< Thread is cancelled, finishing task. */ + ThreadDead = 1 << 3, /*!< Thread is finished, exiting. */ + ThreadIdle = 1 << 4, /*!< Thread is idle, waiting for purpose. */ + ThreadActive = 1 << 5 /*!< Thread is active, working on a task. */ +} dt_state_t; + +/*! + * \brief Thread runnable prototype. + * + * Runnable is basically a pointer to function which is called on active + * thread runtime. + * + * \note When implementing a runnable, keep in mind to check thread state as + * it may change, and implement a cooperative cancellation point. + * + * Implement this by checking dt_is_cancelled() and return + * as soon as possible. + */ +typedef int (*runnable_t)(struct dthread *); + +/*! + * \brief Single thread descriptor public API. + */ +typedef struct dthread { + volatile unsigned state; /*!< Bitfield of dt_flag flags. */ + runnable_t run; /*!< Runnable function or 0. */ + runnable_t destruct; /*!< Destructor function or 0. */ + void *data; /*!< Currently active data */ + struct dt_unit *unit; /*!< Reference to assigned unit. */ + void *_adata; /*!< Thread-specific data. */ + pthread_t _thr; /*!< Thread */ + pthread_attr_t _attr; /*!< Thread attributes */ + pthread_mutex_t _mx; /*!< Thread state change lock. */ +} dthread_t; + +/*! + * \brief Thread unit descriptor API. + * + * Thread unit consists of 1..N threads. + * Unit is coherent if all threads execute + * the same runnable. + */ +typedef struct dt_unit { + int size; /*!< Unit width (number of threads) */ + struct dthread **threads; /*!< Array of threads */ + pthread_cond_t _notify; /*!< Notify thread */ + pthread_mutex_t _notify_mx; /*!< Condition mutex */ + pthread_cond_t _report; /*!< Report thread state */ + pthread_mutex_t _report_mx; /*!< Condition mutex */ + pthread_mutex_t _mx; /*!< Unit lock */ +} dt_unit_t; + +/*! + * \brief Create a set of coherent threads. + * + * Coherent means, that the threads will share a common runnable and the data. + * + * \param count Requested thread count. + * \param runnable Runnable function for all threads. + * \param destructor Destructor for all threads. + * \param data Any data passed onto threads. + * + * \retval New instance if successful + * \retval NULL on error + */ +dt_unit_t *dt_create(int count, runnable_t runnable, runnable_t destructor, void *data); + +/*! + * \brief Free unit. + * + * \warning Behavior is undefined if threads are still active, make sure + * to call dt_join() first. + * + * \param unit Unit to be deleted. + */ +void dt_delete(dt_unit_t **unit); + +/*! + * \brief Start all threads in selected unit. + * + * \param unit Unit to be started. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters (unit is null). + */ +int dt_start(dt_unit_t *unit); + +/*! + * \brief Send given signal to thread. + * + * \note This is useful to interrupt some blocking I/O as well, for example + * with SIGALRM, which is handled by default. + * \note Signal handler may be overridden in runnable. + * + * \param thread Target thread instance. + * \param signum Signal code. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + * \retval KNOT_ERROR unspecified error. + */ +int dt_signalize(dthread_t *thread, int signum); + +/*! + * \brief Wait for all thread in unit to finish. + * + * \param unit Unit to be joined. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + */ +int dt_join(dt_unit_t *unit); + +/*! + * \brief Stop all threads in unit. + * + * Thread is interrupted at the nearest runnable cancellation point. + * + * \param unit Unit to be stopped. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + */ +int dt_stop(dt_unit_t *unit); + +/*! + * \brief Set thread affinity to masked CPU's. + * + * \param thread Target thread instance. + * \param cpu_id Array of CPU IDs to set affinity to. + * \param cpu_count Number of CPUs in the array, set to 0 for no CPU. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + */ +int dt_setaffinity(dthread_t *thread, unsigned* cpu_id, size_t cpu_count); + +/*! + * \brief Wake up thread from idle state. + * + * Thread is awoken from idle state and reenters runnable. + * This function only affects idle threads. + * + * \note Unit needs to be started with dt_start() first, as the function + * doesn't affect dead threads. + * + * \param thread Target thread instance. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + * \retval KNOT_ENOTSUP operation not supported. + */ +int dt_activate(dthread_t *thread); + +/*! + * \brief Put thread to idle state, cancels current runnable function. + * + * Thread is flagged with Cancel flag and returns from runnable at the nearest + * cancellation point, which requires complying runnable function. + * + * \note Thread isn't disposed, but put to idle state until it's requested + * again or collected by dt_compact(). + * + * \param thread Target thread instance. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + */ +int dt_cancel(dthread_t *thread); + +/*! + * \brief Collect and dispose idle threads. + * + * \param unit Target unit instance. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + */ +int dt_compact(dt_unit_t *unit); + +/*! + * \brief Return number of online processors. + * + * \retval Number of online CPU's if success. + * \retval <0 on failure. + */ +int dt_online_cpus(void); + +/*! + * \brief Return optimal number of threads for instance. + * + * It is estimated as NUM_CPUs + CONSTANT. + * Fallback is DEFAULT_THR_COUNT (\see common.h). + * + * \return Number of threads. + */ +int dt_optimal_size(void); + +/*! + * \brief Return true if thread is cancelled. + * + * Synchronously check for ThreadCancelled flag. + * + * \param thread Target thread instance. + * + * \retval 1 if cancelled. + * \retval 0 if not cancelled. + */ +int dt_is_cancelled(dthread_t *thread); + +/*! + * \brief Return thread index in threading unit. + * + * \note Returns 0 when thread doesn't have a unit. + * + * \param thread Target thread instance. + * + * \return Thread index. + */ +unsigned dt_get_id(dthread_t *thread); + +/*! + * \brief Lock unit to prevent parallel operations which could alter unit + * at the same time. + * + * \param unit Target unit instance. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + * \retval KNOT_EAGAIN lack of resources to lock unit, try again. + * \retval KNOT_ERROR unspecified error. + */ +int dt_unit_lock(dt_unit_t *unit); + +/*! + * \brief Unlock unit. + * + * \see dt_unit_lock() + * + * \param unit Target unit instance. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + * \retval KNOT_EAGAIN lack of resources to unlock unit, try again. + * \retval KNOT_ERROR unspecified error. + */ +int dt_unit_unlock(dt_unit_t *unit); diff --git a/src/knot/server/proxyv2.c b/src/knot/server/proxyv2.c new file mode 100644 index 0000000..ff92263 --- /dev/null +++ b/src/knot/server/proxyv2.c @@ -0,0 +1,69 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "knot/server/proxyv2.h" + +#include "contrib/proxyv2/proxyv2.h" +#include "knot/conf/conf.h" + +int proxyv2_header_strip(knot_pkt_t **query, + const struct sockaddr_storage *remote, + struct sockaddr_storage *new_remote) +{ + conf_t *pconf = conf(); + if (!pconf->cache.srv_proxy_enabled) { + return KNOT_EDENIED; + } + + uint8_t *pkt = (*query)->wire; + size_t pkt_len = (*query)->max_size; + + int offset = proxyv2_header_offset(pkt, pkt_len); + if (offset <= 0) { + return KNOT_EMALF; + } + + /* + * Check if the query was sent from an IP address authorized to send + * proxied DNS traffic. + */ + conf_val_t whitelist_val = conf_get(pconf, C_SRV, C_PROXY_ALLOWLIST); + if (!conf_addr_range_match(&whitelist_val, remote)) { + return KNOT_EDENIED; + } + + /* + * Store the provided remote address. + */ + int ret = proxyv2_addr_store(pkt, pkt_len, new_remote); + if (ret != KNOT_EOK) { + return ret; + } + + /* + * Re-parse the query message using the data in the + * packet following the PROXY v2 payload. And replace the original + * query with the decapsulated one. + */ + knot_pkt_t *q = knot_pkt_new(pkt + offset, pkt_len - offset, &(*query)->mm); + if (q == NULL) { + return KNOT_ENOMEM; + } + knot_pkt_free(*query); + *query = q; + + return knot_pkt_parse(q, 0); +} diff --git a/src/knot/server/proxyv2.h b/src/knot/server/proxyv2.h new file mode 100644 index 0000000..5cb1251 --- /dev/null +++ b/src/knot/server/proxyv2.h @@ -0,0 +1,23 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libknot/packet/pkt.h" + +int proxyv2_header_strip(knot_pkt_t **query, + const struct sockaddr_storage *remote, + struct sockaddr_storage *new_remote); diff --git a/src/knot/server/server.c b/src/knot/server/server.c new file mode 100644 index 0000000..684526d --- /dev/null +++ b/src/knot/server/server.c @@ -0,0 +1,1335 @@ +/* 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/>. + */ + +#define __APPLE_USE_RFC_3542 + +#include <assert.h> +#include <sys/types.h> // OpenBSD +#include <netinet/tcp.h> // TCP_FASTOPEN +#include <sys/resource.h> + +#include "libknot/libknot.h" +#include "libknot/yparser/ypschema.h" +#include "libknot/xdp.h" +#if defined ENABLE_XDP && ENABLE_QUIC +#include "libknot/xdp/quic.h" +#endif // ENABLE_XDP && ENABLE_QUIC +#include "knot/common/log.h" +#include "knot/common/stats.h" +#include "knot/common/systemd.h" +#include "knot/common/unreachable.h" +#include "knot/conf/confio.h" +#include "knot/conf/migration.h" +#include "knot/conf/module.h" +#include "knot/dnssec/kasp/kasp_db.h" +#include "knot/journal/journal_basic.h" +#include "knot/server/server.h" +#include "knot/server/udp-handler.h" +#include "knot/server/tcp-handler.h" +#include "knot/zone/timers.h" +#include "knot/zone/zonedb-load.h" +#include "knot/worker/pool.h" +#include "contrib/conn_pool.h" +#include "contrib/net.h" +#include "contrib/openbsd/strlcat.h" +#include "contrib/os.h" +#include "contrib/sockaddr.h" +#include "contrib/trim.h" + +#ifdef ENABLE_XDP +#include <net/if.h> +#endif + +#ifdef SO_ATTACH_REUSEPORT_CBPF +#include <linux/filter.h> +#endif + +/*! \brief Minimal send/receive buffer sizes. */ +enum { + UDP_MIN_RCVSIZE = 4096, + UDP_MIN_SNDSIZE = 4096, + TCP_MIN_RCVSIZE = 4096, + TCP_MIN_SNDSIZE = sizeof(uint16_t) + UINT16_MAX +}; + +/*! \brief Unbind interface and clear the structure. */ +static void server_deinit_iface(iface_t *iface, bool dealloc) +{ + assert(iface); + + /* Free UDP handler. */ + if (iface->fd_udp != NULL) { + for (int i = 0; i < iface->fd_udp_count; i++) { + if (iface->fd_udp[i] > -1) { + close(iface->fd_udp[i]); + } + } + free(iface->fd_udp); + } + + for (int i = 0; i < iface->fd_xdp_count; i++) { +#ifdef ENABLE_XDP + knot_xdp_deinit(iface->xdp_sockets[i]); +#else + assert(0); +#endif + } + free(iface->fd_xdp); + free(iface->xdp_sockets); + + /* Free TCP handler. */ + if (iface->fd_tcp != NULL) { + for (int i = 0; i < iface->fd_tcp_count; i++) { + if (iface->fd_tcp[i] > -1) { + close(iface->fd_tcp[i]); + } + } + free(iface->fd_tcp); + } + + if (dealloc) { + free(iface); + } +} + +/*! \brief Deinit server interface list. */ +static void server_deinit_iface_list(iface_t *ifaces, size_t n) +{ + if (ifaces != NULL) { + for (size_t i = 0; i < n; i++) { + server_deinit_iface(ifaces + i, false); + } + free(ifaces); + } +} + +/*! + * \brief Attach SO_REUSEPORT socket filter for perfect CPU locality. + * + * \param sock Socket where to attach the CBPF filter to. + * \param sock_count Number of sockets. + */ +static bool server_attach_reuseport_bpf(const int sock, const int sock_count) +{ +#ifdef SO_ATTACH_REUSEPORT_CBPF + struct sock_filter code[] = { + /* A = raw_smp_processor_id(). */ + { BPF_LD | BPF_W | BPF_ABS, 0, 0, SKF_AD_OFF + SKF_AD_CPU }, + /* Adjust the CPUID to socket group size. */ + { BPF_ALU | BPF_MOD | BPF_K, 0, 0, sock_count }, + /* Return A. */ + { BPF_RET | BPF_A, 0, 0, 0 }, + }; + + struct sock_fprog prog = { 0 }; + prog.len = sizeof(code) / sizeof(*code); + prog.filter = code; + + return setsockopt(sock, SOL_SOCKET, SO_ATTACH_REUSEPORT_CBPF, &prog, sizeof(prog)) == 0; +#else + return true; +#endif +} + +/*! \brief Set lower bound for socket option. */ +static bool setsockopt_min(int sock, int option, int min) +{ + int value = 0; + socklen_t len = sizeof(value); + + if (getsockopt(sock, SOL_SOCKET, option, &value, &len) != 0) { + return false; + } + + assert(len == sizeof(value)); + if (value >= min) { + return true; + } + + return setsockopt(sock, SOL_SOCKET, option, &min, sizeof(min)) == 0; +} + +/*! + * \brief Enlarge send/receive buffers. + */ +static bool enlarge_net_buffers(int sock, int min_recvsize, int min_sndsize) +{ + return setsockopt_min(sock, SO_RCVBUF, min_recvsize) && + setsockopt_min(sock, SO_SNDBUF, min_sndsize); +} + +/*! + * \brief Enable source packet information retrieval. + */ +static bool enable_pktinfo(int sock, int family) +{ + int level = 0; + int option = 0; + + switch (family) { + case AF_INET: + level = IPPROTO_IP; +#if defined(IP_PKTINFO) + option = IP_PKTINFO; /* Linux */ +#elif defined(IP_RECVDSTADDR) + option = IP_RECVDSTADDR; /* BSD */ +#else + return false; +#endif + break; + case AF_INET6: + level = IPPROTO_IPV6; + option = IPV6_RECVPKTINFO; + break; + default: + return false; + } + + const int on = 1; + return setsockopt(sock, level, option, &on, sizeof(on)) == 0; +} + +/*! + * Linux 3.15 has IP_PMTUDISC_OMIT which makes sockets + * ignore PMTU information and send packets with DF=0. + * Fragmentation is allowed if and only if the packet + * size exceeds the outgoing interface MTU or the packet + * encounters smaller MTU link in network. + * This mitigates DNS fragmentation attacks by preventing + * forged PMTU information. + * FreeBSD already has same semantics without setting + * the option. + */ +static int disable_pmtudisc(int sock, int family) +{ +#if defined(IP_MTU_DISCOVER) && defined(IP_PMTUDISC_OMIT) + if (family == AF_INET) { + int action_omit = IP_PMTUDISC_OMIT; + if (setsockopt(sock, IPPROTO_IP, IP_MTU_DISCOVER, &action_omit, + sizeof(action_omit)) != 0) { + return knot_map_errno(); + } + } +#endif + return KNOT_EOK; +} + +static iface_t *server_init_xdp_iface(struct sockaddr_storage *addr, bool route_check, + bool udp, bool tcp, uint16_t quic, unsigned *thread_id_start) +{ +#ifndef ENABLE_XDP + assert(0); + return NULL; +#else + conf_xdp_iface_t iface; + int ret = conf_xdp_iface(addr, &iface); + if (ret != KNOT_EOK) { + log_error("failed to initialize XDP interface (%s)", + knot_strerror(ret)); + return NULL; + } + + iface_t *new_if = calloc(1, sizeof(*new_if)); + if (new_if == NULL) { + log_error("failed to initialize XDP interface"); + return NULL; + } + memcpy(&new_if->addr, addr, sizeof(*addr)); + + new_if->fd_xdp = calloc(iface.queues, sizeof(int)); + new_if->xdp_sockets = calloc(iface.queues, sizeof(*new_if->xdp_sockets)); + if (new_if->fd_xdp == NULL || new_if->xdp_sockets == NULL) { + log_error("failed to initialize XDP interface"); + server_deinit_iface(new_if, true); + return NULL; + } + new_if->xdp_first_thread_id = *thread_id_start; + *thread_id_start += iface.queues; + + knot_xdp_filter_flag_t xdp_flags = udp ? KNOT_XDP_FILTER_UDP : 0; + if (tcp) { + xdp_flags |= KNOT_XDP_FILTER_TCP; + } + if (quic > 0) { + xdp_flags |= KNOT_XDP_FILTER_QUIC; + } + if (route_check) { + xdp_flags |= KNOT_XDP_FILTER_ROUTE; + } + + for (int i = 0; i < iface.queues; i++) { + knot_xdp_load_bpf_t mode = + (i == 0 ? KNOT_XDP_LOAD_BPF_ALWAYS : KNOT_XDP_LOAD_BPF_NEVER); + ret = knot_xdp_init(new_if->xdp_sockets + i, iface.name, i, + xdp_flags, iface.port, quic, mode, NULL); + if (ret == -EBUSY && i == 0) { + log_notice("XDP interface %s@%u is busy, retrying initialization", + iface.name, iface.port); + ret = knot_xdp_init(new_if->xdp_sockets + i, iface.name, i, + xdp_flags, iface.port, quic, + KNOT_XDP_LOAD_BPF_ALWAYS_UNLOAD, NULL); + } + if (ret != KNOT_EOK) { + log_warning("failed to initialize XDP interface %s@%u, queue %d (%s)", + iface.name, iface.port, i, knot_strerror(ret)); + server_deinit_iface(new_if, true); + new_if = NULL; + break; + } + new_if->fd_xdp[i] = knot_xdp_socket_fd(new_if->xdp_sockets[i]); + new_if->fd_xdp_count++; + } + + if (ret == KNOT_EOK) { + char msg[128]; + (void)snprintf(msg, sizeof(msg), "initialized XDP interface %s", iface.name); + if (udp || tcp) { + char buf[32] = ""; + (void)snprintf(buf, sizeof(buf), ", %s%s%s port %u", + (udp ? "UDP" : ""), + (udp && tcp ? "/" : ""), + (tcp ? "TCP" : ""), + iface.port); + strlcat(msg, buf, sizeof(msg)); + } + if (quic) { + char buf[32] = ""; + (void)snprintf(buf, sizeof(buf), ", QUIC port %u", quic); + strlcat(msg, buf, sizeof(msg)); + } + + knot_xdp_mode_t mode = knot_eth_xdp_mode(if_nametoindex(iface.name)); + log_info("%s, queues %d, %s mode%s", msg, iface.queues, + (mode == KNOT_XDP_MODE_FULL ? "native" : "emulated"), + route_check ? ", route check" : ""); + } + + return new_if; +#endif +} + +/*! + * \brief Create and initialize new interface. + * + * Both TCP and UDP sockets will be created for the interface. + * + * \param addr Socket address. + * \param udp_thread_count Number of created UDP workers. + * \param tcp_thread_count Number of created TCP workers. + * \param tcp_reuseport Indication if reuseport on TCP is enabled. + * \param socket_affinity Indication if CBPF should be attached. + * + * \retval Pointer to a new initialized interface. + * \retval NULL if error. + */ +static iface_t *server_init_iface(struct sockaddr_storage *addr, + int udp_thread_count, int tcp_thread_count, + bool tcp_reuseport, bool socket_affinity) +{ + iface_t *new_if = calloc(1, sizeof(*new_if)); + if (new_if == NULL) { + log_error("failed to initialize interface"); + return NULL; + } + memcpy(&new_if->addr, addr, sizeof(*addr)); + + /* Convert to string address format. */ + char addr_str[SOCKADDR_STRLEN] = { 0 }; + sockaddr_tostr(addr_str, sizeof(addr_str), addr); + + int udp_socket_count = 1; + int udp_bind_flags = 0; + int tcp_socket_count = 1; + int tcp_bind_flags = 0; + +#ifdef ENABLE_REUSEPORT + udp_socket_count = udp_thread_count; + udp_bind_flags |= NET_BIND_MULTIPLE; + + if (tcp_reuseport) { + tcp_socket_count = tcp_thread_count; + tcp_bind_flags |= NET_BIND_MULTIPLE; + } +#endif + + new_if->fd_udp = malloc(udp_socket_count * sizeof(int)); + new_if->fd_tcp = malloc(tcp_socket_count * sizeof(int)); + if (new_if->fd_udp == NULL || new_if->fd_tcp == NULL) { + log_error("failed to initialize interface"); + server_deinit_iface(new_if, true); + return NULL; + } + + const mode_t unix_mode = S_IWUSR | S_IWGRP | S_IWOTH; + + bool warn_bind = true; + bool warn_cbpf = true; + bool warn_bufsize = true; + bool warn_pktinfo = true; + bool warn_flag_misc = true; + + /* Create bound UDP sockets. */ + for (int i = 0; i < udp_socket_count; i++) { + int sock = net_bound_socket(SOCK_DGRAM, addr, udp_bind_flags, unix_mode); + if (sock == KNOT_EADDRNOTAVAIL) { + udp_bind_flags |= NET_BIND_NONLOCAL; + sock = net_bound_socket(SOCK_DGRAM, addr, udp_bind_flags, unix_mode); + if (sock >= 0 && warn_bind) { + log_warning("address %s UDP bound, but required nonlocal bind", addr_str); + warn_bind = false; + } + } + + if (sock < 0) { + log_error("cannot bind address %s UDP (%s)", addr_str, + knot_strerror(sock)); + server_deinit_iface(new_if, true); + return NULL; + } + + if ((udp_bind_flags & NET_BIND_MULTIPLE) && socket_affinity) { + if (!server_attach_reuseport_bpf(sock, udp_socket_count) && + warn_cbpf) { + log_warning("cannot ensure optimal CPU locality for UDP"); + warn_cbpf = false; + } + } + + if (!enlarge_net_buffers(sock, UDP_MIN_RCVSIZE, UDP_MIN_SNDSIZE) && + warn_bufsize) { + log_warning("failed to set network buffer sizes for UDP"); + warn_bufsize = false; + } + + if (sockaddr_is_any(addr) && !enable_pktinfo(sock, addr->ss_family) && + warn_pktinfo) { + log_warning("failed to enable received packet information retrieval"); + warn_pktinfo = false; + } + + int ret = disable_pmtudisc(sock, addr->ss_family); + if (ret != KNOT_EOK && warn_flag_misc) { + log_warning("failed to disable Path MTU discovery for IPv4/UDP (%s)", + knot_strerror(ret)); + warn_flag_misc = false; + } + + new_if->fd_udp[new_if->fd_udp_count] = sock; + new_if->fd_udp_count += 1; + } + + warn_bind = true; + warn_cbpf = true; + warn_bufsize = true; + warn_flag_misc = true; + + /* Create bound TCP sockets. */ + for (int i = 0; i < tcp_socket_count; i++) { + int sock = net_bound_socket(SOCK_STREAM, addr, tcp_bind_flags, unix_mode); + if (sock == KNOT_EADDRNOTAVAIL) { + tcp_bind_flags |= NET_BIND_NONLOCAL; + sock = net_bound_socket(SOCK_STREAM, addr, tcp_bind_flags, unix_mode); + if (sock >= 0 && warn_bind) { + log_warning("address %s TCP bound, but required nonlocal bind", addr_str); + warn_bind = false; + } + } + + if (sock < 0) { + log_error("cannot bind address %s TCP (%s)", addr_str, + knot_strerror(sock)); + server_deinit_iface(new_if, true); + return NULL; + } + + if (!enlarge_net_buffers(sock, TCP_MIN_RCVSIZE, TCP_MIN_SNDSIZE) && + warn_bufsize) { + log_warning("failed to set network buffer sizes for TCP"); + warn_bufsize = false; + } + + new_if->fd_tcp[new_if->fd_tcp_count] = sock; + new_if->fd_tcp_count += 1; + + /* Listen for incoming connections. */ + int ret = listen(sock, TCP_BACKLOG_SIZE); + if (ret < 0) { + log_error("failed to listen on TCP interface %s", addr_str); + server_deinit_iface(new_if, true); + return NULL; + } + + if ((tcp_bind_flags & NET_BIND_MULTIPLE) && socket_affinity) { + if (!server_attach_reuseport_bpf(sock, tcp_socket_count) && + warn_cbpf) { + log_warning("cannot ensure optimal CPU locality for TCP"); + warn_cbpf = false; + } + } + + /* Try to enable TCP Fast Open. */ + ret = net_bound_tfo(sock, TCP_BACKLOG_SIZE); + if (ret != KNOT_EOK && ret != KNOT_ENOTSUP && warn_flag_misc) { + log_warning("failed to enable TCP Fast Open on %s (%s)", + addr_str, knot_strerror(ret)); + warn_flag_misc = false; + } + } + + return new_if; +} + +static void log_sock_conf(conf_t *conf) +{ + char buf[128] = ""; +#if defined(ENABLE_REUSEPORT) + strlcat(buf, "UDP", sizeof(buf)); + if (conf->cache.srv_tcp_reuseport) { + strlcat(buf, "/TCP", sizeof(buf)); + } + strlcat(buf, " reuseport", sizeof(buf)); + if (conf->cache.srv_socket_affinity) { + strlcat(buf, ", socket affinity", sizeof(buf)); + } +#endif +#if defined(TCP_FASTOPEN) + if (buf[0] != '\0') { + strlcat(buf, ", ", sizeof(buf)); + } + strlcat(buf, "incoming", sizeof(buf)); + if (conf->cache.srv_tcp_fastopen) { + strlcat(buf, "/outgoing", sizeof(buf)); + } + strlcat(buf, " TCP Fast Open", sizeof(buf)); +#endif + if (buf[0] != '\0') { + log_info("using %s", buf); + } +} + +/*! \brief Initialize bound sockets according to configuration. */ +static int configure_sockets(conf_t *conf, server_t *s) +{ + if (s->state & ServerRunning) { + return KNOT_EOK; + } + + conf_val_t listen_val = conf_get(conf, C_SRV, C_LISTEN); + conf_val_t lisxdp_val = conf_get(conf, C_XDP, C_LISTEN); + conf_val_t rundir_val = conf_get(conf, C_SRV, C_RUNDIR); + + if (listen_val.code == KNOT_EOK) { + log_sock_conf(conf); + } else if (lisxdp_val.code != KNOT_EOK) { + log_warning("no network interface configured"); + return KNOT_EOK; + } + +#ifdef ENABLE_XDP + if (lisxdp_val.code == KNOT_EOK && !linux_at_least(5, 11)) { + struct rlimit min_limit = { RLIM_INFINITY, RLIM_INFINITY }; + struct rlimit cur_limit = { 0 }; + if (getrlimit(RLIMIT_MEMLOCK, &cur_limit) != 0 || + cur_limit.rlim_cur != min_limit.rlim_cur || + cur_limit.rlim_max != min_limit.rlim_max) { + int ret = setrlimit(RLIMIT_MEMLOCK, &min_limit); + if (ret != 0) { + log_error("failed to increase RLIMIT_MEMLOCK (%s)", + knot_strerror(errno)); + return KNOT_ESYSTEM; + } + } + } +#endif + + size_t real_nifs = 0; + size_t nifs = conf_val_count(&listen_val) + conf_val_count(&lisxdp_val); + iface_t *newlist = calloc(nifs, sizeof(*newlist)); + if (newlist == NULL) { + log_error("failed to allocate memory for network sockets"); + return KNOT_ENOMEM; + } + + /* Normal UDP and TCP sockets. */ + unsigned size_udp = s->handlers[IO_UDP].handler.unit->size; + unsigned size_tcp = s->handlers[IO_TCP].handler.unit->size; + bool tcp_reuseport = conf->cache.srv_tcp_reuseport; + bool socket_affinity = conf->cache.srv_socket_affinity; + char *rundir = conf_abs_path(&rundir_val, NULL); + while (listen_val.code == KNOT_EOK) { + struct sockaddr_storage addr = conf_addr(&listen_val, rundir); + char addr_str[SOCKADDR_STRLEN] = { 0 }; + sockaddr_tostr(addr_str, sizeof(addr_str), &addr); + log_info("binding to interface %s", addr_str); + + iface_t *new_if = server_init_iface(&addr, size_udp, size_tcp, + tcp_reuseport, socket_affinity); + if (new_if == NULL) { + server_deinit_iface_list(newlist, nifs); + free(rundir); + return KNOT_ERROR; + } + memcpy(&newlist[real_nifs++], new_if, sizeof(*newlist)); + free(new_if); + + conf_val_next(&listen_val); + } + free(rundir); + + /* XDP sockets. */ + bool xdp_udp = conf->cache.xdp_udp; + bool xdp_tcp = conf->cache.xdp_tcp; + uint16_t xdp_quic = conf->cache.xdp_quic; + bool route_check = conf->cache.xdp_route_check; + unsigned thread_id = s->handlers[IO_UDP].handler.unit->size + + s->handlers[IO_TCP].handler.unit->size; + while (lisxdp_val.code == KNOT_EOK) { + struct sockaddr_storage addr = conf_addr(&lisxdp_val, NULL); + char addr_str[SOCKADDR_STRLEN] = { 0 }; + sockaddr_tostr(addr_str, sizeof(addr_str), &addr); + log_info("binding to XDP interface %s", addr_str); + + iface_t *new_if = server_init_xdp_iface(&addr, route_check, xdp_udp, + xdp_tcp, xdp_quic, &thread_id); + if (new_if == NULL) { + server_deinit_iface_list(newlist, nifs); + return KNOT_ERROR; + } + memcpy(&newlist[real_nifs++], new_if, sizeof(*newlist)); + free(new_if); + + conf_val_next(&lisxdp_val); + } + assert(real_nifs <= nifs); + nifs = real_nifs; + +#if defined ENABLE_XDP && ENABLE_QUIC + if (xdp_quic > 0) { + char *tls_cert = conf_tls(conf, C_CERT_FILE); + char *tls_key = conf_tls(conf, C_KEY_FILE); + if (tls_cert == NULL) { + log_notice("QUIC, no server certificate configured, using one-time one"); + } + s->quic_creds = knot_xquic_init_creds(true, tls_cert, tls_key); + free(tls_cert); + free(tls_key); + if (s->quic_creds == NULL) { + log_error("QUIC, failed to initialize server credentials"); + server_deinit_iface_list(newlist, nifs); + return KNOT_ERROR; + } + } +#endif // ENABLE_XDP && ENABLE_QUIC + + /* Publish new list. */ + s->ifaces = newlist; + s->n_ifaces = nifs; + + /* Assign thread identifiers unique per all handlers. */ + unsigned thread_count = 0; + for (unsigned proto = IO_UDP; proto <= IO_XDP; ++proto) { + dt_unit_t *tu = s->handlers[proto].handler.unit; + for (unsigned i = 0; tu != NULL && i < tu->size; ++i) { + s->handlers[proto].handler.thread_id[i] = thread_count++; + } + } + + return KNOT_EOK; +} + +int server_init(server_t *server, int bg_workers) +{ + if (server == NULL) { + return KNOT_EINVAL; + } + + /* Clear the structure. */ + memset(server, 0, sizeof(server_t)); + + /* Initialize event scheduler. */ + if (evsched_init(&server->sched, server) != KNOT_EOK) { + return KNOT_ENOMEM; + } + + server->workers = worker_pool_create(bg_workers); + if (server->workers == NULL) { + evsched_deinit(&server->sched); + return KNOT_ENOMEM; + } + + int ret = catalog_update_init(&server->catalog_upd); + if (ret != KNOT_EOK) { + worker_pool_destroy(server->workers); + evsched_deinit(&server->sched); + return ret; + } + + zone_backups_init(&server->backup_ctxs); + + char *catalog_dir = conf_db(conf(), C_CATALOG_DB); + conf_val_t catalog_size = conf_db_param(conf(), C_CATALOG_DB_MAX_SIZE); + catalog_init(&server->catalog, catalog_dir, conf_int(&catalog_size)); + free(catalog_dir); + conf()->catalog = &server->catalog; + + char *journal_dir = conf_db(conf(), C_JOURNAL_DB); + conf_val_t journal_size = conf_db_param(conf(), C_JOURNAL_DB_MAX_SIZE); + conf_val_t journal_mode = conf_db_param(conf(), C_JOURNAL_DB_MODE); + knot_lmdb_init(&server->journaldb, journal_dir, conf_int(&journal_size), journal_env_flags(conf_opt(&journal_mode), false), NULL); + free(journal_dir); + + kasp_db_ensure_init(&server->kaspdb, conf()); + + char *timer_dir = conf_db(conf(), C_TIMER_DB); + conf_val_t timer_size = conf_db_param(conf(), C_TIMER_DB_MAX_SIZE); + knot_lmdb_init(&server->timerdb, timer_dir, conf_int(&timer_size), 0, NULL); + free(timer_dir); + + return KNOT_EOK; +} + +void server_deinit(server_t *server) +{ + if (server == NULL) { + return; + } + + zone_backups_deinit(&server->backup_ctxs); + + /* Save zone timers. */ + if (server->zone_db != NULL) { + log_info("updating persistent timer DB"); + int ret = zone_timers_write_all(&server->timerdb, server->zone_db); + if (ret != KNOT_EOK) { + log_warning("failed to update persistent timer DB (%s)", + knot_strerror(ret)); + } + } + + /* Free remaining interfaces. */ + server_deinit_iface_list(server->ifaces, server->n_ifaces); + + /* Free threads and event handlers. */ + worker_pool_destroy(server->workers); + + /* Free zone database. */ + knot_zonedb_deep_free(&server->zone_db, true); + + /* Free remaining events. */ + evsched_deinit(&server->sched); + + /* Free catalog zone context. */ + catalog_update_clear(&server->catalog_upd); + catalog_update_deinit(&server->catalog_upd); + catalog_deinit(&server->catalog); + + /* Close persistent timers DB. */ + knot_lmdb_deinit(&server->timerdb); + + /* Close kasp_db. */ + knot_lmdb_deinit(&server->kaspdb); + + /* Close journal database if open. */ + knot_lmdb_deinit(&server->journaldb); + + /* Close and deinit connection pool. */ + conn_pool_deinit(global_conn_pool); + global_conn_pool = NULL; + knot_unreachables_deinit(&global_unreachables); + +#if defined ENABLE_XDP && ENABLE_QUIC + knot_xquic_free_creds(server->quic_creds); +#endif // ENABLE_XDP && ENABLE_QUIC +} + +static int server_init_handler(server_t *server, int index, int thread_count, + runnable_t runnable, runnable_t destructor) +{ + /* Initialize */ + iohandler_t *h = &server->handlers[index].handler; + memset(h, 0, sizeof(iohandler_t)); + h->server = server; + h->unit = dt_create(thread_count, runnable, destructor, h); + if (h->unit == NULL) { + return KNOT_ENOMEM; + } + + h->thread_state = calloc(thread_count, sizeof(unsigned)); + if (h->thread_state == NULL) { + dt_delete(&h->unit); + return KNOT_ENOMEM; + } + + h->thread_id = calloc(thread_count, sizeof(unsigned)); + if (h->thread_id == NULL) { + free(h->thread_state); + dt_delete(&h->unit); + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +static void server_free_handler(iohandler_t *h) +{ + if (h == NULL || h->server == NULL) { + return; + } + + /* Wait for threads to finish */ + if (h->unit) { + dt_stop(h->unit); + dt_join(h->unit); + } + + /* Destroy worker context. */ + dt_delete(&h->unit); + free(h->thread_state); + free(h->thread_id); +} + +static void worker_wait_cb(worker_pool_t *pool) +{ + systemd_zone_load_timeout_notify(); + + static uint64_t last_ns = 0; + struct timespec now = time_now(); + uint64_t now_ns = 1000000000 * now.tv_sec + now.tv_nsec; + /* Too frequent worker_pool_status() call with many zones is expensive. */ + if (now_ns - last_ns > 1000000000) { + int running, queued; + worker_pool_status(pool, true, &running, &queued); + systemd_tasks_status_notify(running + queued); + last_ns = now_ns; + } +} + +int server_start(server_t *server, bool async) +{ + if (server == NULL) { + return KNOT_EINVAL; + } + + /* Start workers. */ + worker_pool_start(server->workers); + + /* Wait for enqueued events if not asynchronous. */ + if (!async) { + worker_pool_wait_cb(server->workers, worker_wait_cb); + systemd_tasks_status_notify(0); + } + + /* Start evsched handler. */ + evsched_start(&server->sched); + + /* Start I/O handlers. */ + server->state |= ServerRunning; + for (int proto = IO_UDP; proto <= IO_XDP; ++proto) { + if (server->handlers[proto].size > 0) { + int ret = dt_start(server->handlers[proto].handler.unit); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + return KNOT_EOK; +} + +void server_wait(server_t *server) +{ + if (server == NULL) { + return; + } + + evsched_join(&server->sched); + worker_pool_join(server->workers); + + for (int proto = IO_UDP; proto <= IO_XDP; ++proto) { + if (server->handlers[proto].size > 0) { + server_free_handler(&server->handlers[proto].handler); + } + } +} + +static int reload_conf(conf_t *new_conf) +{ + yp_schema_purge_dynamic(new_conf->schema); + + /* Re-load common modules. */ + int ret = conf_mod_load_common(new_conf); + if (ret != KNOT_EOK) { + return ret; + } + + /* Re-import config file if specified. */ + const char *filename = conf()->filename; + if (filename != NULL) { + log_info("reloading configuration file '%s'", filename); + + /* Import the configuration file. */ + ret = conf_import(new_conf, filename, true, false); + if (ret != KNOT_EOK) { + log_error("failed to load configuration file (%s)", + knot_strerror(ret)); + return ret; + } + } else { + log_info("reloading configuration database '%s'", + knot_db_lmdb_get_path(new_conf->db)); + + /* Re-load extra modules. */ + for (conf_iter_t iter = conf_iter(new_conf, C_MODULE); + iter.code == KNOT_EOK; conf_iter_next(new_conf, &iter)) { + conf_val_t id = conf_iter_id(new_conf, &iter); + conf_val_t file = conf_id_get(new_conf, C_MODULE, C_FILE, &id); + ret = conf_mod_load_extra(new_conf, conf_str(&id), conf_str(&file), + MOD_EXPLICIT); + if (ret != KNOT_EOK) { + conf_iter_finish(new_conf, &iter); + return ret; + } + } + } + + conf_mod_load_purge(new_conf, false); + + // Migrate from old schema. + ret = conf_migrate(new_conf); + if (ret != KNOT_EOK) { + log_error("failed to migrate configuration (%s)", knot_strerror(ret)); + } + + return KNOT_EOK; +} + +/*! \brief Check if parameter listen(-xdp) has been changed since knotd started. */ +static bool listen_changed(conf_t *conf, server_t *server) +{ + assert(server->ifaces); + + conf_val_t listen_val = conf_get(conf, C_SRV, C_LISTEN); + conf_val_t lisxdp_val = conf_get(conf, C_XDP, C_LISTEN); + size_t new_count = conf_val_count(&listen_val) + conf_val_count(&lisxdp_val); + size_t old_count = server->n_ifaces; + if (new_count != old_count) { + return true; + } + + conf_val_t rundir_val = conf_get(conf, C_SRV, C_RUNDIR); + char *rundir = conf_abs_path(&rundir_val, NULL); + size_t matches = 0; + + /* Find matching interfaces. */ + while (listen_val.code == KNOT_EOK) { + struct sockaddr_storage addr = conf_addr(&listen_val, rundir); + bool found = false; + for (size_t i = 0; i < server->n_ifaces; i++) { + if (sockaddr_cmp(&addr, &server->ifaces[i].addr, false) == 0) { + matches++; + found = true; + break; + } + } + if (!found) { + break; + } + conf_val_next(&listen_val); + } + free(rundir); + + while (lisxdp_val.code == KNOT_EOK) { + struct sockaddr_storage addr = conf_addr(&lisxdp_val, NULL); + bool found = false; + for (size_t i = 0; i < server->n_ifaces; i++) { + if (sockaddr_cmp(&addr, &server->ifaces[i].addr, false) == 0) { + matches++; + found = true; + break; + } + } + if (!found) { + break; + } + conf_val_next(&lisxdp_val); + } + + return matches != old_count; +} + +/*! \brief Log warnings if config change requires a restart. */ +static void warn_server_reconfigure(conf_t *conf, server_t *server) +{ + const char *msg = "changes of %s require restart to take effect"; + + static bool warn_tcp_reuseport = true; + static bool warn_socket_affinity = true; + static bool warn_udp = true; + static bool warn_tcp = true; + static bool warn_bg = true; + static bool warn_listen = true; + static bool warn_xdp_udp = true; + static bool warn_xdp_tcp = true; + static bool warn_xdp_quic = true; + static bool warn_route_check = true; + static bool warn_rmt_pool_limit = true; + + if (warn_tcp_reuseport && conf->cache.srv_tcp_reuseport != conf_get_bool(conf, C_SRV, C_TCP_REUSEPORT)) { + log_warning(msg, &C_TCP_REUSEPORT[1]); + warn_tcp_reuseport = false; + } + + if (warn_socket_affinity && conf->cache.srv_socket_affinity != conf_get_bool(conf, C_SRV, C_SOCKET_AFFINITY)) { + log_warning(msg, &C_SOCKET_AFFINITY[1]); + warn_socket_affinity = false; + } + + if (warn_udp && server->handlers[IO_UDP].size != conf_udp_threads(conf)) { + log_warning(msg, &C_UDP_WORKERS[1]); + warn_udp = false; + } + + if (warn_tcp && server->handlers[IO_TCP].size != conf_tcp_threads(conf)) { + log_warning(msg, &C_TCP_WORKERS[1]); + warn_tcp = false; + } + + if (warn_bg && conf->cache.srv_bg_threads != conf_bg_threads(conf)) { + log_warning(msg, &C_BG_WORKERS[1]); + warn_bg = false; + } + + if (warn_listen && server->ifaces != NULL && listen_changed(conf, server)) { + log_warning(msg, "listen(-xdp)"); + warn_listen = false; + } + + if (warn_xdp_udp && conf->cache.xdp_udp != conf_get_bool(conf, C_XDP, C_UDP)) { + log_warning(msg, &C_UDP[1]); + warn_xdp_udp = false; + } + + if (warn_xdp_tcp && conf->cache.xdp_tcp != conf_get_bool(conf, C_XDP, C_TCP)) { + log_warning(msg, &C_TCP[1]); + warn_xdp_tcp = false; + } + + if (warn_xdp_quic && (bool)conf->cache.xdp_quic != conf_get_bool(conf, C_XDP, C_QUIC)) { + log_warning(msg, &C_QUIC[1]); + warn_xdp_quic = false; + } + + if (warn_xdp_quic && conf->cache.xdp_quic > 0 && + conf->cache.xdp_quic != conf_get_int(conf, C_XDP, C_QUIC_PORT)) { + log_warning(msg, &C_QUIC_PORT[1]); + warn_xdp_quic = false; + } + + if (warn_route_check && conf->cache.xdp_route_check != conf_get_bool(conf, C_XDP, C_ROUTE_CHECK)) { + log_warning(msg, &C_ROUTE_CHECK[1]); + warn_route_check = false; + } + + if (warn_rmt_pool_limit && global_conn_pool != NULL && + global_conn_pool->capacity != conf_get_int(conf, C_SRV, C_RMT_POOL_LIMIT)) { + log_warning(msg, &C_RMT_POOL_LIMIT[1]); + warn_rmt_pool_limit = false; + } +} + +int server_reload(server_t *server, reload_t mode) +{ + if (server == NULL) { + return KNOT_EINVAL; + } + + systemd_reloading_notify(); + + /* Check for no edit mode. */ + if (conf()->io.txn != NULL) { + log_warning("reload aborted due to active configuration transaction"); + systemd_ready_notify(); + return KNOT_TXN_EEXISTS; + } + + conf_t *new_conf = NULL; + int ret = conf_clone(&new_conf); + if (ret != KNOT_EOK) { + log_error("failed to initialize configuration (%s)", + knot_strerror(ret)); + systemd_ready_notify(); + return ret; + } + + yp_flag_t flags = conf()->io.flags; + bool full = !(flags & CONF_IO_FACTIVE); + bool reuse_modules = !full && !(flags & CONF_IO_FRLD_MOD); + + /* Reload configuration and modules if full reload or a module change. */ + if (full || !reuse_modules) { + ret = reload_conf(new_conf); + if (ret != KNOT_EOK) { + conf_free(new_conf); + systemd_ready_notify(); + return ret; + } + + conf_activate_modules(new_conf, server, NULL, new_conf->query_modules, + &new_conf->query_plan); + } + + conf_update_flag_t upd_flags = CONF_UPD_FNOFREE; + if (!full) { + upd_flags |= CONF_UPD_FCONFIO; + } + if (reuse_modules) { + upd_flags |= CONF_UPD_FMODULES; + } + + /* Update to the new config. */ + conf_t *old_conf = conf_update(new_conf, upd_flags); + + /* Reload each component if full reload or a specific one if required. */ + if (full || (flags & CONF_IO_FRLD_LOG)) { + log_reconfigure(conf()); + } + if (full || (flags & CONF_IO_FRLD_SRV)) { + (void)server_reconfigure(conf(), server); + warn_server_reconfigure(conf(), server); + stats_reconfigure(conf(), server); + } + if (full || (flags & (CONF_IO_FRLD_ZONES | CONF_IO_FRLD_ZONE))) { + server_update_zones(conf(), server, mode); + } + + /* Free old config needed for module unload in zone reload. */ + conf_free(old_conf); + + if (full) { + log_info("configuration reloaded"); + } else { + // Reset confio reload context. + conf()->io.flags = YP_FNONE; + if (conf()->io.zones != NULL) { + trie_clear(conf()->io.zones); + } + } + + systemd_ready_notify(); + + return KNOT_EOK; +} + +void server_stop(server_t *server) +{ + log_info("stopping server"); + systemd_stopping_notify(); + + /* Stop scheduler. */ + evsched_stop(&server->sched); + /* Interrupt background workers. */ + worker_pool_stop(server->workers); + + /* Clear 'running' flag. */ + server->state &= ~ServerRunning; +} + +static int set_handler(server_t *server, int index, unsigned size, runnable_t run) +{ + /* Initialize I/O handlers. */ + int ret = server_init_handler(server, index, size, run, NULL); + if (ret != KNOT_EOK) { + return ret; + } + + server->handlers[index].size = size; + + return KNOT_EOK; +} + +static int configure_threads(conf_t *conf, server_t *server) +{ + int ret = set_handler(server, IO_UDP, conf->cache.srv_udp_threads, udp_master); + if (ret != KNOT_EOK) { + return ret; + } + + if (conf->cache.srv_xdp_threads > 0) { + ret = set_handler(server, IO_XDP, conf->cache.srv_xdp_threads, udp_master); + if (ret != KNOT_EOK) { + return ret; + } + } + + return set_handler(server, IO_TCP, conf->cache.srv_tcp_threads, tcp_master); +} + +static int reconfigure_journal_db(conf_t *conf, server_t *server) +{ + char *journal_dir = conf_db(conf, C_JOURNAL_DB); + conf_val_t journal_size = conf_db_param(conf, C_JOURNAL_DB_MAX_SIZE); + conf_val_t journal_mode = conf_db_param(conf, C_JOURNAL_DB_MODE); + int ret = knot_lmdb_reinit(&server->journaldb, journal_dir, conf_int(&journal_size), + journal_env_flags(conf_opt(&journal_mode), false)); + if (ret != KNOT_EOK) { + log_warning("ignored reconfiguration of journal DB (%s)", knot_strerror(ret)); + } + free(journal_dir); + + return KNOT_EOK; // not "ret" +} + +static int reconfigure_kasp_db(conf_t *conf, server_t *server) +{ + char *kasp_dir = conf_db(conf, C_KASP_DB); + conf_val_t kasp_size = conf_db_param(conf, C_KASP_DB_MAX_SIZE); + int ret = knot_lmdb_reinit(&server->kaspdb, kasp_dir, conf_int(&kasp_size), 0); + if (ret != KNOT_EOK) { + log_warning("ignored reconfiguration of KASP DB (%s)", knot_strerror(ret)); + } + free(kasp_dir); + + return KNOT_EOK; // not "ret" +} + +static int reconfigure_timer_db(conf_t *conf, server_t *server) +{ + char *timer_dir = conf_db(conf, C_TIMER_DB); + conf_val_t timer_size = conf_db_param(conf, C_TIMER_DB_MAX_SIZE); + int ret = knot_lmdb_reconfigure(&server->timerdb, timer_dir, conf_int(&timer_size), 0); + free(timer_dir); + return ret; +} + +static int reconfigure_remote_pool(conf_t *conf) +{ + conf_val_t val = conf_get(conf, C_SRV, C_RMT_POOL_LIMIT); + size_t limit = conf_int(&val); + val = conf_get(conf, C_SRV, C_RMT_POOL_TIMEOUT); + knot_timediff_t timeout = conf_int(&val); + if (global_conn_pool == NULL && limit > 0) { + conn_pool_t *new_pool = conn_pool_init(limit, timeout); + if (new_pool == NULL) { + return KNOT_ENOMEM; + } + global_conn_pool = new_pool; + } else { + (void)conn_pool_timeout(global_conn_pool, timeout); + } + + val = conf_get(conf, C_SRV, C_RMT_RETRY_DELAY); + int delay_ms = conf_int(&val); + if (global_unreachables == NULL && delay_ms > 0) { + global_unreachables = knot_unreachables_init(delay_ms); + } else { + (void)knot_unreachables_ttl(global_unreachables, delay_ms); + } + + return KNOT_EOK; +} + +int server_reconfigure(conf_t *conf, server_t *server) +{ + if (conf == NULL || server == NULL) { + return KNOT_EINVAL; + } + + int ret; + + /* First reconfiguration. */ + if (!(server->state & ServerRunning)) { + log_info("Knot DNS %s starting", PACKAGE_VERSION); + + size_t mapsize = conf->mapsize / (1024 * 1024); + if (conf->filename != NULL) { + log_info("loaded configuration file '%s', mapsize %zu MiB", + conf->filename, mapsize); + } else { + log_info("loaded configuration database '%s', mapsize %zu MiB", + knot_db_lmdb_get_path(conf->db), mapsize); + } + + /* Configure server threads. */ + if ((ret = configure_threads(conf, server)) != KNOT_EOK) { + log_error("failed to configure server threads (%s)", + knot_strerror(ret)); + return ret; + } + + /* Configure sockets. */ + if ((ret = configure_sockets(conf, server)) != KNOT_EOK) { + return ret; + } + + if (conf_lmdb_readers(conf) > CONF_MAX_DB_READERS) { + log_warning("config, exceeded number of database readers"); + } + } + + /* Reconfigure journal DB. */ + if ((ret = reconfigure_journal_db(conf, server)) != KNOT_EOK) { + log_error("failed to reconfigure journal DB (%s)", + knot_strerror(ret)); + } + + /* Reconfigure KASP DB. */ + if ((ret = reconfigure_kasp_db(conf, server)) != KNOT_EOK) { + log_error("failed to reconfigure KASP DB (%s)", + knot_strerror(ret)); + } + + /* Reconfigure Timer DB. */ + if ((ret = reconfigure_timer_db(conf, server)) != KNOT_EOK) { + log_error("failed to reconfigure Timer DB (%s)", + knot_strerror(ret)); + } + + /* Reconfigure connection pool. */ + if ((ret = reconfigure_remote_pool(conf)) != KNOT_EOK) { + log_error("failed to reconfigure remote pool (%s)", + knot_strerror(ret)); + } + + return KNOT_EOK; +} + +void server_update_zones(conf_t *conf, server_t *server, reload_t mode) +{ + if (conf == NULL || server == NULL) { + return; + } + + /* Prevent emitting of new zone events. */ + if (server->zone_db) { + knot_zonedb_foreach(server->zone_db, zone_events_freeze); + } + + /* Suspend adding events to worker pool queue, wait for queued events. */ + evsched_pause(&server->sched); + worker_pool_wait(server->workers); + + /* Reload zone database and free old zones. */ + zonedb_reload(conf, server, mode); + + /* Trim extra heap. */ + mem_trim(); + + /* Resume processing events on new zones. */ + evsched_resume(&server->sched); + if (server->zone_db) { + knot_zonedb_foreach(server->zone_db, zone_events_start); + } +} diff --git a/src/knot/server/server.h b/src/knot/server/server.h new file mode 100644 index 0000000..5adafdb --- /dev/null +++ b/src/knot/server/server.h @@ -0,0 +1,203 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdatomic.h> + +#include "knot/conf/conf.h" +#include "knot/catalog/catalog_update.h" +#include "knot/common/evsched.h" +#include "knot/common/fdset.h" +#include "knot/journal/knot_lmdb.h" +#include "knot/server/dthreads.h" +#include "knot/worker/pool.h" +#include "knot/zone/backup.h" +#include "knot/zone/zonedb.h" + +struct server; +struct knot_xdp_socket; +struct knot_quic_creds; + +/*! + * \brief I/O handler structure. + */ +typedef struct { + struct server *server; /*!< Reference to server. */ + dt_unit_t *unit; /*!< Threading unit. */ + unsigned *thread_state; /*!< Thread states. */ + unsigned *thread_id; /*!< Thread identifiers per all handlers. */ +} iohandler_t; + +/*! + * \brief Server state flags. + */ +typedef enum { + ServerIdle = 0 << 0, /*!< Server is idle. */ + ServerRunning = 1 << 0, /*!< Server is running. */ +} server_state_t; + +/*! + * \brief Server reload kinds. + */ +typedef enum { + RELOAD_NONE = 0, + RELOAD_FULL = 1 << 0, /*!< Reload the server and all zones. */ + RELOAD_COMMIT = 1 << 1, /*!< Process changes from dynamic configuration. */ + RELOAD_ZONES = 1 << 2, /*!< Reload all zones. */ + RELOAD_CATALOG = 1 << 3, /*!< Process catalog zone changes. */ +} reload_t; + +/*! + * \brief Server interface structure. + */ +typedef struct { + int *fd_udp; + unsigned fd_udp_count; + int *fd_tcp; + unsigned fd_tcp_count; + int *fd_xdp; + unsigned fd_xdp_count; + unsigned xdp_first_thread_id; + struct knot_xdp_socket **xdp_sockets; + struct sockaddr_storage addr; +} iface_t; + +/*! + * \brief Handler indexes. + */ +enum { + IO_UDP = 0, + IO_TCP = 1, + IO_XDP = 2, +}; + +/*! + * \brief Main server structure. + * + * Keeps references to all important structures needed for operation. + */ +typedef struct server { + /*! \brief Server state tracking. */ + volatile unsigned state; + + knot_zonedb_t *zone_db; + knot_lmdb_db_t timerdb; + knot_lmdb_db_t journaldb; + knot_lmdb_db_t kaspdb; + catalog_t catalog; + + /*! \brief I/O handlers. */ + struct { + unsigned size; + iohandler_t handler; + } handlers[3]; + + /*! \brief Background jobs. */ + worker_pool_t *workers; + + /*! \brief Event scheduler. */ + evsched_t sched; + + /*! \brief List of interfaces. */ + iface_t *ifaces; + size_t n_ifaces; + + /*! \brief Pending changes to catalog member zones, update indication. */ + catalog_update_t catalog_upd; + atomic_bool catalog_upd_signal; + + /*! \brief Context of pending zones' backup. */ + zone_backup_ctxs_t backup_ctxs; + + /*! \brief Crendentials context for QUIC. */ + struct knot_quic_creds *quic_creds; +} server_t; + +/*! + * \brief Initializes the server structure. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + */ +int server_init(server_t *server, int bg_workers); + +/*! + * \brief Properly destroys the server structure. + * + * \param server Server structure to be used for operation. + */ +void server_deinit(server_t *server); + +/*! + * \brief Starts the server. + * + * \param server Server structure to be used for operation. + * \param async Don't wait for zones to load if true. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL on invalid parameters. + * + */ +int server_start(server_t *server, bool async); + +/*! + * \brief Waits for the server to finish. + * + * \param server Server structure to be used for operation. + * + */ +void server_wait(server_t *server); + +/*! + * \brief Reload server configuration. + * + * \param server Server instance. + * \param mode Reload mode. + * + * \return Error code, KNOT_EOK if success. + */ +int server_reload(server_t *server, reload_t mode); + +/*! + * \brief Requests server to stop. + * + * \param server Server structure to be used for operation. + */ +void server_stop(server_t *server); + +/*! + * \brief Server reconfiguration routine. + * + * Routine for dynamic server reconfiguration. + * + * \param conf Configuration. + * \param server Server instance. + * + * \return Error code, KNOT_EOK if success. + */ +int server_reconfigure(conf_t *conf, server_t *server); + +/*! + * \brief Reconfigure zone database. + * + * Routine for dynamic server zones reconfiguration. + * + * \param conf Configuration. + * \param server Server instance. + * \param mode Reload mode. + */ +void server_update_zones(conf_t *conf, server_t *server, reload_t mode); diff --git a/src/knot/server/tcp-handler.c b/src/knot/server/tcp-handler.c new file mode 100644 index 0000000..433ca9b --- /dev/null +++ b/src/knot/server/tcp-handler.c @@ -0,0 +1,380 @@ +/* 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 <unistd.h> +#include <fcntl.h> +#include <errno.h> +#include <string.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/tcp.h> +#include <netinet/in.h> +#include <stdio.h> +#include <stdlib.h> +#include <urcu.h> +#ifdef HAVE_SYS_UIO_H // struct iovec (OpenBSD) +#include <sys/uio.h> +#endif // HAVE_SYS_UIO_H + +#include "knot/server/server.h" +#include "knot/server/tcp-handler.h" +#include "knot/common/log.h" +#include "knot/common/fdset.h" +#include "knot/nameserver/process_query.h" +#include "knot/query/layer.h" +#include "contrib/macros.h" +#include "contrib/mempattern.h" +#include "contrib/net.h" +#include "contrib/openbsd/strlcpy.h" +#include "contrib/sockaddr.h" +#include "contrib/time.h" +#include "contrib/ucw/mempool.h" + +/*! \brief TCP context data. */ +typedef struct tcp_context { + knot_layer_t layer; /*!< Query processing layer. */ + server_t *server; /*!< Name server structure. */ + struct iovec iov[2]; /*!< TX/RX buffers. */ + unsigned client_threshold; /*!< Index of first TCP client. */ + struct timespec last_poll_time; /*!< Time of the last socket poll. */ + bool is_throttled; /*!< TCP connections throttling switch. */ + fdset_t set; /*!< Set of server/client sockets. */ + unsigned thread_id; /*!< Thread identifier. */ + unsigned max_worker_fds; /*!< Max TCP clients per worker configuration + no. of ifaces. */ + int idle_timeout; /*!< [s] TCP idle timeout configuration. */ + int io_timeout; /*!< [ms] TCP send/recv timeout configuration. */ +} tcp_context_t; + +#define TCP_SWEEP_INTERVAL 2 /*!< [secs] granularity of connection sweeping. */ + +static void update_sweep_timer(struct timespec *timer) +{ + *timer = time_now(); + timer->tv_sec += TCP_SWEEP_INTERVAL; +} + +static void update_tcp_conf(tcp_context_t *tcp) +{ + rcu_read_lock(); + conf_t *pconf = conf(); + tcp->max_worker_fds = tcp->client_threshold + \ + MAX(pconf->cache.srv_tcp_max_clients / pconf->cache.srv_tcp_threads, 1); + tcp->idle_timeout = pconf->cache.srv_tcp_idle_timeout; + tcp->io_timeout = pconf->cache.srv_tcp_io_timeout; + rcu_read_unlock(); +} + +/*! \brief Sweep TCP connection. */ +static fdset_sweep_state_t tcp_sweep(fdset_t *set, int fd, _unused_ void *data) +{ + assert(set && fd >= 0); + + /* Best-effort, name and shame. */ + struct sockaddr_storage ss = { 0 }; + socklen_t len = sizeof(struct sockaddr_storage); + if (getpeername(fd, (struct sockaddr *)&ss, &len) == 0) { + char addr_str[SOCKADDR_STRLEN]; + sockaddr_tostr(addr_str, sizeof(addr_str), &ss); + log_notice("TCP, terminated inactive client, address %s", addr_str); + } + + return FDSET_SWEEP; +} + +static bool tcp_active_state(int state) +{ + return (state == KNOT_STATE_PRODUCE || state == KNOT_STATE_FAIL); +} + +static bool tcp_send_state(int state) +{ + return (state != KNOT_STATE_FAIL && state != KNOT_STATE_NOOP); +} + +static void tcp_log_error(struct sockaddr_storage *ss, const char *operation, int ret) +{ + /* Don't log ECONN as it usually means client closed the connection. */ + if (ret == KNOT_ETIMEOUT) { + char addr_str[SOCKADDR_STRLEN]; + sockaddr_tostr(addr_str, sizeof(addr_str), ss); + log_debug("TCP, failed to %s due to IO timeout, closing connection, address %s", + operation, addr_str); + } +} + +static unsigned tcp_set_ifaces(const iface_t *ifaces, size_t n_ifaces, + fdset_t *fds, int thread_id) +{ + if (n_ifaces == 0) { + return 0; + } + + for (const iface_t *i = ifaces; i != ifaces + n_ifaces; i++) { + if (i->fd_tcp_count == 0) { // Ignore XDP interface. + assert(i->fd_xdp_count > 0); + continue; + } + + int tcp_id = 0; +#ifdef ENABLE_REUSEPORT + if (conf()->cache.srv_tcp_reuseport) { + /* Note: thread_ids start with UDP threads, TCP threads follow. */ + assert((i->fd_udp_count <= thread_id) && + (thread_id < i->fd_tcp_count + i->fd_udp_count)); + + tcp_id = thread_id - i->fd_udp_count; + } +#endif + int ret = fdset_add(fds, i->fd_tcp[tcp_id], FDSET_POLLIN, NULL); + if (ret < 0) { + return 0; + } + } + + return fdset_get_length(fds); +} + +static int tcp_handle(tcp_context_t *tcp, int fd, struct iovec *rx, struct iovec *tx) +{ + /* Get peer name. */ + struct sockaddr_storage ss; + socklen_t addrlen = sizeof(struct sockaddr_storage); + if (getpeername(fd, (struct sockaddr *)&ss, &addrlen) != 0) { + return KNOT_EADDRNOTAVAIL; + } + + /* Create query processing parameter. */ + knotd_qdata_params_t params = { + .proto = KNOTD_QUERY_PROTO_TCP, + .remote = &ss, + .socket = fd, + .server = tcp->server, + .thread_id = tcp->thread_id + }; + + rx->iov_len = KNOT_WIRE_MAX_PKTSIZE; + tx->iov_len = KNOT_WIRE_MAX_PKTSIZE; + + /* Receive data. */ + int recv = net_dns_tcp_recv(fd, rx->iov_base, rx->iov_len, tcp->io_timeout); + if (recv > 0) { + rx->iov_len = recv; + } else { + tcp_log_error(&ss, "receive", recv); + return KNOT_EOF; + } + + /* Initialize processing layer. */ + knot_layer_begin(&tcp->layer, ¶ms); + + /* Create packets. */ + knot_pkt_t *ans = knot_pkt_new(tx->iov_base, tx->iov_len, tcp->layer.mm); + knot_pkt_t *query = knot_pkt_new(rx->iov_base, rx->iov_len, tcp->layer.mm); + + /* Input packet. */ + int ret = knot_pkt_parse(query, 0); + if (ret != KNOT_EOK && query->parsed > 0) { // parsing failed (e.g. 2x OPT) + query->parsed--; // artificially decreasing "parsed" leads to FORMERR + } + knot_layer_consume(&tcp->layer, query); + + /* Resolve until NOOP or finished. */ + while (tcp_active_state(tcp->layer.state)) { + knot_layer_produce(&tcp->layer, ans); + /* Send, if response generation passed and wasn't ignored. */ + if (ans->size > 0 && tcp_send_state(tcp->layer.state)) { + int sent = net_dns_tcp_send(fd, ans->wire, ans->size, + tcp->io_timeout, NULL); + if (sent != ans->size) { + tcp_log_error(&ss, "send", sent); + ret = KNOT_EOF; + break; + } + } + } + + /* Reset after processing. */ + knot_layer_finish(&tcp->layer); + + /* Flush per-query memory (including query and answer packets). */ + mp_flush(tcp->layer.mm->ctx); + + return ret; +} + +static void tcp_event_accept(tcp_context_t *tcp, unsigned i) +{ + /* Accept client. */ + int fd = fdset_get_fd(&tcp->set, i); + int client = net_accept(fd, NULL); + if (client >= 0) { + /* Assign to fdset. */ + int idx = fdset_add(&tcp->set, client, FDSET_POLLIN, NULL); + if (idx < 0) { + close(client); + return; + } + + /* Update watchdog timer. */ + (void)fdset_set_watchdog(&tcp->set, idx, tcp->idle_timeout); + } +} + +static int tcp_event_serve(tcp_context_t *tcp, unsigned i) +{ + int ret = tcp_handle(tcp, fdset_get_fd(&tcp->set, i), + &tcp->iov[0], &tcp->iov[1]); + if (ret == KNOT_EOK) { + /* Update socket activity timer. */ + (void)fdset_set_watchdog(&tcp->set, i, tcp->idle_timeout); + } + + return ret; +} + +static void tcp_wait_for_events(tcp_context_t *tcp) +{ + fdset_t *set = &tcp->set; + + /* Check if throttled with many open TCP connections. */ + assert(fdset_get_length(set) <= tcp->max_worker_fds); + tcp->is_throttled = fdset_get_length(set) == tcp->max_worker_fds; + + /* If throttled, temporarily ignore new TCP connections. */ + unsigned offset = tcp->is_throttled ? tcp->client_threshold : 0; + + /* Wait for events. */ + fdset_it_t it; + (void)fdset_poll(set, &it, offset, TCP_SWEEP_INTERVAL * 1000); + + /* Mark the time of last poll call. */ + tcp->last_poll_time = time_now(); + + /* Process events. */ + for (; !fdset_it_is_done(&it); fdset_it_next(&it)) { + bool should_close = false; + unsigned int idx = fdset_it_get_idx(&it); + if (fdset_it_is_error(&it)) { + should_close = (idx >= tcp->client_threshold); + } else if (fdset_it_is_pollin(&it)) { + /* Master sockets - new connection to accept. */ + if (idx < tcp->client_threshold) { + /* Don't accept more clients than configured. */ + if (fdset_get_length(set) < tcp->max_worker_fds) { + tcp_event_accept(tcp, idx); + } + /* Client sockets - already accepted connection or + closed connection :-( */ + } else if (tcp_event_serve(tcp, idx) != KNOT_EOK) { + should_close = true; + } + } + + /* Evaluate. */ + if (should_close) { + fdset_it_remove(&it); + } + } + fdset_it_commit(&it); +} + +int tcp_master(dthread_t *thread) +{ + if (thread == NULL || thread->data == NULL) { + return KNOT_EINVAL; + } + + iohandler_t *handler = (iohandler_t *)thread->data; + int thread_id = handler->thread_id[dt_get_id(thread)]; + +#ifdef ENABLE_REUSEPORT + /* Set thread affinity to CPU core (overlaps with UDP/XDP). */ + if (conf()->cache.srv_tcp_reuseport) { + unsigned cpu = dt_online_cpus(); + if (cpu > 1) { + unsigned cpu_mask = (dt_get_id(thread) % cpu); + dt_setaffinity(thread, &cpu_mask, 1); + } + } +#endif + + int ret = KNOT_EOK; + + /* Create big enough memory cushion. */ + knot_mm_t mm; + mm_ctx_mempool(&mm, 16 * MM_DEFAULT_BLKSIZE); + + /* Create TCP answering context. */ + tcp_context_t tcp = { + .server = handler->server, + .is_throttled = false, + .thread_id = thread_id, + }; + knot_layer_init(&tcp.layer, &mm, process_query_layer()); + + /* Create iovec abstraction. */ + for (unsigned i = 0; i < 2; ++i) { + tcp.iov[i].iov_len = KNOT_WIRE_MAX_PKTSIZE; + tcp.iov[i].iov_base = malloc(tcp.iov[i].iov_len); + if (tcp.iov[i].iov_base == NULL) { + ret = KNOT_ENOMEM; + goto finish; + } + } + + /* Initialize sweep interval and TCP configuration. */ + struct timespec next_sweep; + update_sweep_timer(&next_sweep); + update_tcp_conf(&tcp); + + /* Prepare initial buffer for listening and bound sockets. */ + if (fdset_init(&tcp.set, FDSET_RESIZE_STEP) != KNOT_EOK) { + goto finish; + } + + /* Set descriptors for the configured interfaces. */ + tcp.client_threshold = tcp_set_ifaces(handler->server->ifaces, + handler->server->n_ifaces, + &tcp.set, thread_id); + if (tcp.client_threshold == 0) { + goto finish; /* Terminate on zero interfaces. */ + } + + for (;;) { + /* Check for cancellation. */ + if (dt_is_cancelled(thread)) { + break; + } + + /* Serve client requests. */ + tcp_wait_for_events(&tcp); + + /* Sweep inactive clients and refresh TCP configuration. */ + if (tcp.last_poll_time.tv_sec >= next_sweep.tv_sec) { + fdset_sweep(&tcp.set, &tcp_sweep, NULL); + update_sweep_timer(&next_sweep); + update_tcp_conf(&tcp); + } + } + +finish: + free(tcp.iov[0].iov_base); + free(tcp.iov[1].iov_base); + mp_delete(mm.ctx); + fdset_clear(&tcp.set); + + return ret; +} diff --git a/src/knot/server/tcp-handler.h b/src/knot/server/tcp-handler.h new file mode 100644 index 0000000..b60ce8f --- /dev/null +++ b/src/knot/server/tcp-handler.h @@ -0,0 +1,43 @@ +/* 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/>. + */ + +/*! + * \brief TCP sockets threading model. + * + * The master socket distributes incoming connections among + * the worker threads ("buckets"). Each threads processes it's own + * set of sockets, and eliminates mutual exclusion problem by doing so. + */ + +#pragma once + +#include "knot/server/dthreads.h" + +#define TCP_BACKLOG_SIZE 10 /*!< TCP listen backlog size. */ + +/*! + * \brief TCP handler thread runnable. + * + * Listens to both bound TCP sockets for client connections and + * serves TCP clients. This runnable is designed to be used as coherent + * and implements cancellation point. + * + * \param thread Associated thread from DThreads unit. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL invalid parameters. + */ +int tcp_master(dthread_t *thread); diff --git a/src/knot/server/udp-handler.c b/src/knot/server/udp-handler.c new file mode 100644 index 0000000..1e309d6 --- /dev/null +++ b/src/knot/server/udp-handler.c @@ -0,0 +1,575 @@ +/* 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/>. + */ + +#define __APPLE_USE_RFC_3542 + +#include <assert.h> +#include <dlfcn.h> +#include <errno.h> +#include <string.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <sys/param.h> +#ifdef HAVE_SYS_UIO_H // struct iovec (OpenBSD) +#include <sys/uio.h> +#endif /* HAVE_SYS_UIO_H */ +#include <unistd.h> + +#include "contrib/macros.h" +#include "contrib/mempattern.h" +#include "contrib/sockaddr.h" +#include "contrib/ucw/mempool.h" +#include "knot/common/fdset.h" +#include "knot/nameserver/process_query.h" +#include "knot/query/layer.h" +#include "knot/server/proxyv2.h" +#include "knot/server/server.h" +#include "knot/server/udp-handler.h" +#include "knot/server/xdp-handler.h" + +/* Buffer identifiers. */ +enum { + RX = 0, + TX = 1, + NBUFS = 2 +}; + +/*! \brief UDP context data. */ +typedef struct { + knot_layer_t layer; /*!< Query processing layer. */ + server_t *server; /*!< Name server structure. */ + unsigned thread_id; /*!< Thread identifier. */ +} udp_context_t; + +static bool udp_state_active(int state) +{ + return (state == KNOT_STATE_PRODUCE || state == KNOT_STATE_FAIL); +} + +static void udp_handle(udp_context_t *udp, int fd, struct sockaddr_storage *ss, + struct iovec *rx, struct iovec *tx, struct knot_xdp_msg *xdp_msg) +{ + /* Create query processing parameter. */ + knotd_qdata_params_t params = { + .proto = KNOTD_QUERY_PROTO_UDP, + .remote = ss, + .socket = fd, + .server = udp->server, + .xdp_msg = xdp_msg, + .thread_id = udp->thread_id + }; + struct sockaddr_storage proxied_remote; + + /* Start query processing. */ + knot_layer_begin(&udp->layer, ¶ms); + + /* Create packets. */ + knot_pkt_t *query = knot_pkt_new(rx->iov_base, rx->iov_len, udp->layer.mm); + knot_pkt_t *ans = knot_pkt_new(tx->iov_base, tx->iov_len, udp->layer.mm); + + /* Input packet. */ + int ret = knot_pkt_parse(query, 0); + if (ret != KNOT_EOK && query->parsed > 0) { + ret = proxyv2_header_strip(&query, params.remote, &proxied_remote); + if (ret == KNOT_EOK) { + params.remote = &proxied_remote; + } else { + query->parsed--; // artificially decreasing "parsed" leads to FORMERR + } + } + knot_layer_consume(&udp->layer, query); + + /* Process answer. */ + while (udp_state_active(udp->layer.state)) { + knot_layer_produce(&udp->layer, ans); + } + + /* Send response only if finished successfully. */ + if (udp->layer.state == KNOT_STATE_DONE) { + tx->iov_len = ans->size; + } else { + tx->iov_len = 0; + } + + /* Reset after processing. */ + knot_layer_finish(&udp->layer); + + /* Flush per-query memory (including query and answer packets). */ + mp_flush(udp->layer.mm->ctx); +} + +typedef struct { + void* (*udp_init)(udp_context_t *, void *); + void (*udp_deinit)(void *); + int (*udp_recv)(int, void *); + void (*udp_handle)(udp_context_t *, void *); + void (*udp_send)(void *); + void (*udp_sweep)(void *); // Optional +} udp_api_t; + +/*! \brief Control message to fit IP_PKTINFO or IPv6_RECVPKTINFO. */ +typedef union { + struct cmsghdr cmsg; + uint8_t buf[CMSG_SPACE(sizeof(struct in6_pktinfo))]; +} cmsg_pktinfo_t; + +static void udp_pktinfo_handle(const struct msghdr *rx, struct msghdr *tx) +{ + tx->msg_controllen = rx->msg_controllen; + if (tx->msg_controllen > 0) { + tx->msg_control = rx->msg_control; + } else { + // BSD has problem with zero length and not-null pointer + tx->msg_control = NULL; + } + +#if defined(__linux__) || defined(__APPLE__) + struct cmsghdr *cmsg = CMSG_FIRSTHDR(tx); + if (cmsg == NULL) { + return; + } + + /* Unset the ifindex to not bypass the routing tables. */ + if (cmsg->cmsg_level == IPPROTO_IP && cmsg->cmsg_type == IP_PKTINFO) { + struct in_pktinfo *info = (struct in_pktinfo *)CMSG_DATA(cmsg); + info->ipi_spec_dst = info->ipi_addr; + info->ipi_ifindex = 0; + } else if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_PKTINFO) { + struct in6_pktinfo *info = (struct in6_pktinfo *)CMSG_DATA(cmsg); + info->ipi6_ifindex = 0; + } +#endif +} + +/* UDP recvfrom() request struct. */ +struct udp_recvfrom { + int fd; + struct sockaddr_storage addr; + struct msghdr msg[NBUFS]; + struct iovec iov[NBUFS]; + uint8_t buf[NBUFS][KNOT_WIRE_MAX_PKTSIZE]; + cmsg_pktinfo_t pktinfo; +}; + +static void *udp_recvfrom_init(_unused_ udp_context_t *ctx, _unused_ void *xdp_sock) +{ + struct udp_recvfrom *rq = malloc(sizeof(struct udp_recvfrom)); + if (rq == NULL) { + return NULL; + } + memset(rq, 0, sizeof(struct udp_recvfrom)); + + for (unsigned i = 0; i < NBUFS; ++i) { + rq->iov[i].iov_base = rq->buf + i; + rq->iov[i].iov_len = KNOT_WIRE_MAX_PKTSIZE; + rq->msg[i].msg_name = &rq->addr; + rq->msg[i].msg_namelen = sizeof(rq->addr); + rq->msg[i].msg_iov = &rq->iov[i]; + rq->msg[i].msg_iovlen = 1; + rq->msg[i].msg_control = &rq->pktinfo.cmsg; + rq->msg[i].msg_controllen = sizeof(rq->pktinfo); + } + return rq; +} + +static void udp_recvfrom_deinit(void *d) +{ + struct udp_recvfrom *rq = d; + free(rq); +} + +static int udp_recvfrom_recv(int fd, void *d) +{ + /* Reset max lengths. */ + struct udp_recvfrom *rq = (struct udp_recvfrom *)d; + rq->iov[RX].iov_len = KNOT_WIRE_MAX_PKTSIZE; + rq->msg[RX].msg_namelen = sizeof(struct sockaddr_storage); + rq->msg[RX].msg_controllen = sizeof(rq->pktinfo); + + int ret = recvmsg(fd, &rq->msg[RX], MSG_DONTWAIT); + if (ret > 0) { + rq->fd = fd; + rq->iov[RX].iov_len = ret; + return 1; + } + + return 0; +} + +static void udp_recvfrom_handle(udp_context_t *ctx, void *d) +{ + struct udp_recvfrom *rq = d; + + /* Prepare TX address. */ + rq->msg[TX].msg_namelen = rq->msg[RX].msg_namelen; + rq->iov[TX].iov_len = KNOT_WIRE_MAX_PKTSIZE; + + udp_pktinfo_handle(&rq->msg[RX], &rq->msg[TX]); + + /* Process received pkt. */ + udp_handle(ctx, rq->fd, &rq->addr, &rq->iov[RX], &rq->iov[TX], NULL); +} + +static void udp_recvfrom_send(void *d) +{ + struct udp_recvfrom *rq = d; + if (rq->iov[TX].iov_len > 0) { + (void)sendmsg(rq->fd, &rq->msg[TX], 0); + } +} + +_unused_ +static udp_api_t udp_recvfrom_api = { + udp_recvfrom_init, + udp_recvfrom_deinit, + udp_recvfrom_recv, + udp_recvfrom_handle, + udp_recvfrom_send, +}; + +#ifdef ENABLE_RECVMMSG +/* UDP recvmmsg() request struct. */ +struct udp_recvmmsg { + int fd; + struct sockaddr_storage addrs[RECVMMSG_BATCHLEN]; + char *iobuf[NBUFS]; + struct iovec *iov[NBUFS]; + struct mmsghdr *msgs[NBUFS]; + unsigned rcvd; + knot_mm_t mm; + cmsg_pktinfo_t pktinfo[RECVMMSG_BATCHLEN]; +}; + +static void *udp_recvmmsg_init(_unused_ udp_context_t *ctx, _unused_ void *xdp_sock) +{ + knot_mm_t mm; + mm_ctx_mempool(&mm, sizeof(struct udp_recvmmsg)); + + struct udp_recvmmsg *rq = mm_alloc(&mm, sizeof(struct udp_recvmmsg)); + memset(rq, 0, sizeof(*rq)); + memcpy(&rq->mm, &mm, sizeof(knot_mm_t)); + + /* Initialize buffers. */ + for (unsigned i = 0; i < NBUFS; ++i) { + rq->iobuf[i] = mm_alloc(&mm, KNOT_WIRE_MAX_PKTSIZE * RECVMMSG_BATCHLEN); + rq->iov[i] = mm_alloc(&mm, sizeof(struct iovec) * RECVMMSG_BATCHLEN); + rq->msgs[i] = mm_alloc(&mm, sizeof(struct mmsghdr) * RECVMMSG_BATCHLEN); + memset(rq->msgs[i], 0, sizeof(struct mmsghdr) * RECVMMSG_BATCHLEN); + for (unsigned k = 0; k < RECVMMSG_BATCHLEN; ++k) { + rq->iov[i][k].iov_base = rq->iobuf[i] + k * KNOT_WIRE_MAX_PKTSIZE; + rq->iov[i][k].iov_len = KNOT_WIRE_MAX_PKTSIZE; + rq->msgs[i][k].msg_hdr.msg_iov = rq->iov[i] + k; + rq->msgs[i][k].msg_hdr.msg_iovlen = 1; + rq->msgs[i][k].msg_hdr.msg_name = rq->addrs + k; + rq->msgs[i][k].msg_hdr.msg_namelen = sizeof(struct sockaddr_storage); + rq->msgs[i][k].msg_hdr.msg_control = &rq->pktinfo[k].cmsg; + rq->msgs[i][k].msg_hdr.msg_controllen = sizeof(cmsg_pktinfo_t); + } + } + + return rq; +} + +static void udp_recvmmsg_deinit(void *d) +{ + struct udp_recvmmsg *rq = d; + if (rq != NULL) { + mp_delete(rq->mm.ctx); + } +} + +static int udp_recvmmsg_recv(int fd, void *d) +{ + struct udp_recvmmsg *rq = d; + + int n = recvmmsg(fd, rq->msgs[RX], RECVMMSG_BATCHLEN, MSG_DONTWAIT, NULL); + if (n > 0) { + rq->fd = fd; + rq->rcvd = n; + } + return n; +} + +static void udp_recvmmsg_handle(udp_context_t *ctx, void *d) +{ + struct udp_recvmmsg *rq = d; + + /* Handle each received message. */ + unsigned j = 0; + for (unsigned i = 0; i < rq->rcvd; ++i) { + struct msghdr *rx = &rq->msgs[RX][i].msg_hdr; + struct msghdr *tx = &rq->msgs[TX][j].msg_hdr; + + /* Set received bytes. */ + rx->msg_iov->iov_len = rq->msgs[RX][i].msg_len; + /* Update mapping of address buffer. */ + tx->msg_name = rx->msg_name; + tx->msg_namelen = rx->msg_namelen; + + /* Update output message control buffer. */ + udp_pktinfo_handle(rx, tx); + + udp_handle(ctx, rq->fd, rq->addrs + i, rx->msg_iov, tx->msg_iov, NULL); + + if (tx->msg_iov->iov_len > 0) { + rq->msgs[TX][j].msg_len = tx->msg_iov->iov_len; + j++; + } else { + /* Reset tainted output context. */ + tx->msg_iov->iov_len = KNOT_WIRE_MAX_PKTSIZE; + } + + /* Reset input context. */ + rx->msg_iov->iov_len = KNOT_WIRE_MAX_PKTSIZE; + rx->msg_namelen = sizeof(struct sockaddr_storage); + rx->msg_controllen = sizeof(cmsg_pktinfo_t); + } + rq->rcvd = j; +} + +static void udp_recvmmsg_send(void *d) +{ + struct udp_recvmmsg *rq = d; + + (void)sendmmsg(rq->fd, rq->msgs[TX], rq->rcvd, 0); + for (unsigned i = 0; i < rq->rcvd; ++i) { + struct msghdr *tx = &rq->msgs[TX][i].msg_hdr; + + /* Reset output context. */ + tx->msg_iov->iov_len = KNOT_WIRE_MAX_PKTSIZE; + } +} + +static udp_api_t udp_recvmmsg_api = { + udp_recvmmsg_init, + udp_recvmmsg_deinit, + udp_recvmmsg_recv, + udp_recvmmsg_handle, + udp_recvmmsg_send, +}; +#endif /* ENABLE_RECVMMSG */ + +#ifdef ENABLE_XDP + +static void *xdp_recvmmsg_init(udp_context_t *ctx, void *xdp_sock) +{ + return xdp_handle_init(ctx->server, xdp_sock); +} + +static void xdp_recvmmsg_deinit(void *d) +{ + if (d != NULL) { + xdp_handle_free(d); + } +} + +static int xdp_recvmmsg_recv(_unused_ int fd, void *d) +{ + return xdp_handle_recv(d); +} + +static void xdp_recvmmsg_handle(udp_context_t *ctx, void *d) +{ + xdp_handle_msgs(d, &ctx->layer, ctx->server, ctx->thread_id); +} + +static void xdp_recvmmsg_send(void *d) +{ + xdp_handle_send(d); +} + +static void xdp_recvmmsg_sweep(void *d) +{ + xdp_handle_reconfigure(d); + xdp_handle_sweep(d); +} + +static udp_api_t xdp_recvmmsg_api = { + xdp_recvmmsg_init, + xdp_recvmmsg_deinit, + xdp_recvmmsg_recv, + xdp_recvmmsg_handle, + xdp_recvmmsg_send, + xdp_recvmmsg_sweep, +}; +#endif /* ENABLE_XDP */ + +static bool is_xdp_thread(const server_t *server, int thread_id) +{ + return server->handlers[IO_XDP].size > 0 && + server->handlers[IO_XDP].handler.thread_id[0] <= thread_id; +} + +static int iface_udp_fd(const iface_t *iface, int thread_id, bool xdp_thread, + void **xdp_socket) +{ + if (xdp_thread) { +#ifdef ENABLE_XDP + if (thread_id < iface->xdp_first_thread_id || + thread_id >= iface->xdp_first_thread_id + iface->fd_xdp_count) { + return -1; // Different XDP interface. + } + size_t xdp_wrk_id = thread_id - iface->xdp_first_thread_id; + assert(xdp_wrk_id < iface->fd_xdp_count); + *xdp_socket = iface->xdp_sockets[xdp_wrk_id]; + return iface->fd_xdp[xdp_wrk_id]; +#else + assert(0); + return -1; +#endif + } else { // UDP thread. + if (iface->fd_udp_count == 0) { // No UDP interfaces. + assert(iface->fd_xdp_count > 0); + return -1; + } +#ifdef ENABLE_REUSEPORT + assert(thread_id < iface->fd_udp_count); + return iface->fd_udp[thread_id]; +#else + return iface->fd_udp[0]; +#endif + } +} + +static unsigned udp_set_ifaces(const server_t *server, size_t n_ifaces, fdset_t *fds, + int thread_id, void **xdp_socket) +{ + if (n_ifaces == 0) { + return 0; + } + + bool xdp_thread = is_xdp_thread(server, thread_id); + const iface_t *ifaces = server->ifaces; + + for (const iface_t *i = ifaces; i != ifaces + n_ifaces; i++) { + int fd = iface_udp_fd(i, thread_id, xdp_thread, xdp_socket); + if (fd < 0) { + continue; + } + int ret = fdset_add(fds, fd, FDSET_POLLIN, NULL); + if (ret < 0) { + return 0; + } + } + + assert(!xdp_thread || fdset_get_length(fds) == 1); + return fdset_get_length(fds); +} + +int udp_master(dthread_t *thread) +{ + if (thread == NULL || thread->data == NULL) { + return KNOT_EINVAL; + } + + iohandler_t *handler = (iohandler_t *)thread->data; + int thread_id = handler->thread_id[dt_get_id(thread)]; + + if (handler->server->n_ifaces == 0) { + return KNOT_EOK; + } + + /* Set thread affinity to CPU core (same for UDP and XDP). */ + unsigned cpu = dt_online_cpus(); + if (cpu > 1) { + unsigned cpu_mask = (dt_get_id(thread) % cpu); + dt_setaffinity(thread, &cpu_mask, 1); + } + + /* Choose processing API. */ + udp_api_t *api = NULL; + if (is_xdp_thread(handler->server, thread_id)) { +#ifdef ENABLE_XDP + api = &xdp_recvmmsg_api; +#else + assert(0); +#endif + } else { +#ifdef ENABLE_RECVMMSG + api = &udp_recvmmsg_api; +#else + api = &udp_recvfrom_api; +#endif + } + void *api_ctx = NULL; + + /* Create big enough memory cushion. */ + knot_mm_t mm; + mm_ctx_mempool(&mm, 16 * MM_DEFAULT_BLKSIZE); + + /* Create UDP answering context. */ + udp_context_t udp = { + .server = handler->server, + .thread_id = thread_id, + }; + knot_layer_init(&udp.layer, &mm, process_query_layer()); + + /* Allocate descriptors for the configured interfaces. */ + void *xdp_socket = NULL; + size_t nifs = handler->server->n_ifaces; + fdset_t fds; + if (fdset_init(&fds, nifs) != KNOT_EOK) { + goto finish; + } + unsigned nfds = udp_set_ifaces(handler->server, nifs, &fds, + thread_id, &xdp_socket); + if (nfds == 0) { + goto finish; + } + + /* Initialize the networking API. */ + api_ctx = api->udp_init(&udp, xdp_socket); + if (api_ctx == NULL) { + goto finish; + } + + /* Loop until all data is read. */ + for (;;) { + /* Cancellation point. */ + if (dt_is_cancelled(thread)) { + break; + } + + /* Wait for events. */ + fdset_it_t it; + (void)fdset_poll(&fds, &it, 0, 1000); + + /* Process the events. */ + for (; !fdset_it_is_done(&it); fdset_it_next(&it)) { + if (!fdset_it_is_pollin(&it)) { + continue; + } + if (api->udp_recv(fdset_it_get_fd(&it), api_ctx) > 0) { + api->udp_handle(&udp, api_ctx); + api->udp_send(api_ctx); + } + } + + /* Regular maintenance (XDP-TCP only). */ + if (api->udp_sweep != NULL) { + api->udp_sweep(api_ctx); + } + } + +finish: + api->udp_deinit(api_ctx); + mp_delete(mm.ctx); + fdset_clear(&fds); + + return KNOT_EOK; +} diff --git a/src/knot/server/udp-handler.h b/src/knot/server/udp-handler.h new file mode 100644 index 0000000..b09e43e --- /dev/null +++ b/src/knot/server/udp-handler.h @@ -0,0 +1,43 @@ +/* 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/>. + */ + +/*! + * \brief UDP sockets threading model. + * + * The master socket locks one worker thread at a time + * and saves events in it's own backing store for asynchronous processing. + * The worker threads work asynchronously in thread pool. + */ + +#pragma once + +#include "knot/server/dthreads.h" + +#define RECVMMSG_BATCHLEN 10 /*!< Default recvmmsg() batch size. */ + +/*! + * \brief UDP handler thread runnable. + * + * Listen to DNS datagrams in a loop on a UDP socket and + * reply to them. This runnable is designed to be used as coherent + * and implements cancellation point. + * + * \param thread Associated thread from DThreads unit. + * + * \retval KNOT_EOK on success. + * \retval KNOT_EINVAL invalid parameters. + */ +int udp_master(dthread_t *thread); diff --git a/src/knot/server/xdp-handler.c b/src/knot/server/xdp-handler.c new file mode 100644 index 0000000..3c9f6d6 --- /dev/null +++ b/src/knot/server/xdp-handler.c @@ -0,0 +1,506 @@ +/* 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/>. + */ + +#ifdef ENABLE_XDP + +#include <assert.h> +#include <stdlib.h> +#include <urcu.h> + +#include "knot/server/xdp-handler.h" +#include "knot/common/log.h" +#include "knot/server/proxyv2.h" +#include "knot/server/server.h" +#include "contrib/sockaddr.h" +#include "contrib/time.h" +#include "contrib/ucw/mempool.h" +#include "libknot/endian.h" +#include "libknot/error.h" +#ifdef ENABLE_QUIC +#include "libknot/xdp/quic.h" +#endif // ENABLE_QUIC +#include "libknot/xdp/tcp.h" +#include "libknot/xdp/tcp_iobuf.h" + +#define QUIC_MAX_SEND_PER_RECV 4 +#define QUIC_IBUFS_PER_CONN 512 /* Heuristic value: this means that e.g. for 100k allowed + QUIC conns, we will limit total size of input buffers to 50 MiB. */ + +typedef struct { + uint64_t last_log; + knot_sweep_stats_t stats; +} closed_log_ctx_t; + +typedef struct xdp_handle_ctx { + knot_xdp_socket_t *sock; + knot_xdp_msg_t msg_recv[XDP_BATCHLEN]; + knot_xdp_msg_t msg_send_udp[XDP_BATCHLEN]; + knot_tcp_relay_t relays[XDP_BATCHLEN]; + uint32_t msg_recv_count; + uint32_t msg_udp_count; + knot_tcp_table_t *tcp_table; + knot_tcp_table_t *syn_table; + +#ifdef ENABLE_QUIC + knot_xquic_conn_t *quic_relays[XDP_BATCHLEN]; + int quic_rets[XDP_BATCHLEN]; + knot_xquic_table_t *quic_table; + closed_log_ctx_t quic_closed; +#endif // ENABLE_QUIC + + bool tcp; + size_t tcp_max_conns; + size_t tcp_syn_conns; + size_t tcp_max_inbufs; + size_t tcp_max_obufs; + uint32_t tcp_idle_close; // In microseconds. + uint32_t tcp_idle_reset; // In microseconds. + uint32_t tcp_idle_resend; // In microseconds. + + uint16_t quic_port; // Network-byte order! + size_t quic_max_conns; + uint64_t quic_idle_close; // In nanoseconds. + size_t quic_max_inbufs; + size_t quic_max_obufs; + + closed_log_ctx_t tcp_closed; +} xdp_handle_ctx_t; + +static bool udp_state_active(int state) +{ + return (state == KNOT_STATE_PRODUCE || state == KNOT_STATE_FAIL); +} + +static bool tcp_active_state(int state) +{ + return (state == KNOT_STATE_PRODUCE || state == KNOT_STATE_FAIL); +} + +static bool tcp_send_state(int state) +{ + return (state != KNOT_STATE_FAIL && state != KNOT_STATE_NOOP); +} + +static void log_closed(closed_log_ctx_t *ctx, bool tcp) +{ + struct timespec now = time_now(); + uint64_t sec = now.tv_sec + now.tv_nsec / 1000000000; + if (sec - ctx->last_log <= 9 || (ctx->stats.total == 0)) { + return; + } + + const char *proto = tcp ? "TCP" : "QUIC"; + + uint32_t timedout = ctx->stats.counters[KNOT_SWEEP_CTR_TIMEOUT]; + uint32_t limit_conn = ctx->stats.counters[KNOT_SWEEP_CTR_LIMIT_CONN]; + uint32_t limit_ibuf = ctx->stats.counters[KNOT_SWEEP_CTR_LIMIT_IBUF]; + uint32_t limit_obuf = ctx->stats.counters[KNOT_SWEEP_CTR_LIMIT_OBUF]; + + if (tcp || ctx->stats.total != timedout) { + log_notice("%s, connection sweep, closed %u, count limit %u, inbuf limit %u, outbuf limit %u", + proto, timedout, limit_conn, limit_ibuf, limit_obuf); + } else { + log_debug("%s, timed out connections %u", proto, timedout); + } + + ctx->last_log = sec; + knot_sweep_stats_reset(&ctx->stats); +} + +void xdp_handle_reconfigure(xdp_handle_ctx_t *ctx) +{ + rcu_read_lock(); + conf_t *pconf = conf(); + ctx->tcp = pconf->cache.xdp_tcp; + ctx->quic_port = htobe16(pconf->cache.xdp_quic); + ctx->tcp_max_conns = pconf->cache.xdp_tcp_max_clients / pconf->cache.srv_xdp_threads; + ctx->tcp_syn_conns = 2 * ctx->tcp_max_conns; + ctx->tcp_max_inbufs = pconf->cache.xdp_tcp_inbuf_max_size / pconf->cache.srv_xdp_threads; + ctx->tcp_max_obufs = pconf->cache.xdp_tcp_outbuf_max_size / pconf->cache.srv_xdp_threads; + ctx->tcp_idle_close = pconf->cache.xdp_tcp_idle_close * 1000000; + ctx->tcp_idle_reset = pconf->cache.xdp_tcp_idle_reset * 1000000; + ctx->tcp_idle_resend= pconf->cache.xdp_tcp_idle_resend * 1000000; + ctx->quic_max_conns = pconf->cache.srv_quic_max_clients / pconf->cache.srv_xdp_threads; + ctx->quic_idle_close= pconf->cache.srv_quic_idle_close * 1000000000LU; + ctx->quic_max_inbufs= ctx->quic_max_conns * QUIC_IBUFS_PER_CONN; + ctx->quic_max_obufs = pconf->cache.srv_quic_obuf_max_size; + rcu_read_unlock(); +} + +void xdp_handle_free(xdp_handle_ctx_t *ctx) +{ + knot_tcp_table_free(ctx->tcp_table); + knot_tcp_table_free(ctx->syn_table); +#ifdef ENABLE_QUIC + knot_xquic_table_free(ctx->quic_table); +#endif // ENABLE_QUIC + free(ctx); +} + +#ifdef ENABLE_QUIC +static void quic_log_cb(const char *line) +{ + log_debug("QUIC: %s", line); +} +#endif // ENABLE_QUIC + +xdp_handle_ctx_t *xdp_handle_init(struct server *server, knot_xdp_socket_t *xdp_sock) +{ + xdp_handle_ctx_t *ctx = calloc(1, sizeof(*ctx)); + if (ctx == NULL) { + return NULL; + } + ctx->sock = xdp_sock; + + xdp_handle_reconfigure(ctx); + + if (ctx->tcp) { + // NOTE: the table size don't have to equal its max usage! + ctx->tcp_table = knot_tcp_table_new(ctx->tcp_max_conns, NULL); + if (ctx->tcp_table == NULL) { + xdp_handle_free(ctx); + return NULL; + } + ctx->syn_table = knot_tcp_table_new(ctx->tcp_syn_conns, ctx->tcp_table); + if (ctx->syn_table == NULL) { + xdp_handle_free(ctx); + return NULL; + } + } + + if (ctx->quic_port > 0) { +#ifdef ENABLE_QUIC + conf_t *pconf = conf(); + size_t udp_pl = MIN(pconf->cache.srv_udp_max_payload_ipv4, pconf->cache.srv_udp_max_payload_ipv6); + ctx->quic_table = knot_xquic_table_new(ctx->quic_max_conns, ctx->quic_max_inbufs, + ctx->quic_max_obufs, udp_pl, server->quic_creds); + if (ctx->quic_table == NULL) { + xdp_handle_free(ctx); + return NULL; + } + if (conf_get_bool(pconf, C_XDP, C_QUIC_LOG)) { + ctx->quic_table->log_cb = quic_log_cb; + } +#else + assert(0); // verified in configuration checks +#endif // ENABLE_QUIC + } + + return ctx; +} + +int xdp_handle_recv(xdp_handle_ctx_t *ctx) +{ + int ret = knot_xdp_recv(ctx->sock, ctx->msg_recv, XDP_BATCHLEN, + &ctx->msg_recv_count, NULL); + return ret == KNOT_EOK ? ctx->msg_recv_count : ret; +} + +static void handle_init(knotd_qdata_params_t *params, knot_layer_t *layer, + knotd_query_proto_t proto, const knot_xdp_msg_t *msg, + const struct iovec *payload, struct sockaddr_storage *proxied_remote) +{ + params->proto = proto; + params->remote = (struct sockaddr_storage *)&msg->ip_from; + params->xdp_msg = msg; + + knot_layer_begin(layer, params); + + knot_pkt_t *query = knot_pkt_new(payload->iov_base, payload->iov_len, layer->mm); + int ret = knot_pkt_parse(query, 0); + if (ret != KNOT_EOK && query->parsed > 0) { // parsing failed (e.g. 2x OPT) + if (params->proto == KNOTD_QUERY_PROTO_UDP && + proxyv2_header_strip(&query, params->remote, proxied_remote) == KNOT_EOK) { + assert(proxied_remote); + params->remote = proxied_remote; + } else { + query->parsed--; // artificially decreasing "parsed" leads to FORMERR + } + } + knot_layer_consume(layer, query); +} + +static void handle_finish(knot_layer_t *layer) +{ + knot_layer_finish(layer); + + // Flush per-query memory (including query and answer packets). + mp_flush(layer->mm->ctx); +} + +static void handle_udp(xdp_handle_ctx_t *ctx, knot_layer_t *layer, + knotd_qdata_params_t *params) +{ + struct sockaddr_storage proxied_remote; + + ctx->msg_udp_count = 0; + + for (uint32_t i = 0; i < ctx->msg_recv_count; i++) { + knot_xdp_msg_t *msg_recv = &ctx->msg_recv[i]; + knot_xdp_msg_t *msg_send = &ctx->msg_send_udp[ctx->msg_udp_count]; + + // Skip TCP or QUIC or marked (zero length) message. + if ((msg_recv->flags & KNOT_XDP_MSG_TCP) || + msg_recv->ip_to.sin6_port == ctx->quic_port || + msg_recv->payload.iov_len == 0) { + continue; + } + + // Try to allocate a buffer for a reply. + if (knot_xdp_reply_alloc(ctx->sock, msg_recv, msg_send) != KNOT_EOK) { + log_notice("UDP, failed to send some packets"); + break; // Drop the rest of the messages. + } + ctx->msg_udp_count++; + + // Consume the query. + handle_init(params, layer, KNOTD_QUERY_PROTO_UDP, msg_recv, &msg_recv->payload, + &proxied_remote); + + // Process the reply. + knot_pkt_t *ans = knot_pkt_new(msg_send->payload.iov_base, + msg_send->payload.iov_len, layer->mm); + while (udp_state_active(layer->state)) { + knot_layer_produce(layer, ans); + } + if (layer->state == KNOT_STATE_DONE) { + msg_send->payload.iov_len = ans->size; + } else { + // If not success, don't send any reply. + msg_send->payload.iov_len = 0; + } + + // Reset the processing. + handle_finish(layer); + } +} + +static void handle_tcp(xdp_handle_ctx_t *ctx, knot_layer_t *layer, + knotd_qdata_params_t *params) +{ + int ret = knot_tcp_recv(ctx->relays, ctx->msg_recv, ctx->msg_recv_count, + ctx->tcp_table, ctx->syn_table, XDP_TCP_IGNORE_NONE); + if (ret != KNOT_EOK) { + log_notice("TCP, failed to process some packets (%s)", knot_strerror(ret)); + return; + } else if (knot_tcp_relay_empty(&ctx->relays[0])) { // no TCP traffic + return; + } + + uint8_t ans_buf[KNOT_WIRE_MAX_PKTSIZE]; + + for (uint32_t i = 0; i < ctx->msg_recv_count; i++) { + knot_tcp_relay_t *rl = &ctx->relays[i]; + + // Process all complete DNS queries in one TCP stream. + for (size_t j = 0; j < rl->inbufs_count; j++) { + // Consume the query. + handle_init(params, layer, KNOTD_QUERY_PROTO_TCP, rl->msg, &rl->inbufs[j], NULL); + params->measured_rtt = rl->conn->establish_rtt; + + // Process the reply. + knot_pkt_t *ans = knot_pkt_new(ans_buf, sizeof(ans_buf), layer->mm); + while (tcp_active_state(layer->state)) { + knot_layer_produce(layer, ans); + if (!tcp_send_state(layer->state)) { + continue; + } + + (void)knot_tcp_reply_data(rl, ctx->tcp_table, false, + ans->wire, ans->size); + } + + handle_finish(layer); + } + } +} + +#ifdef ENABLE_QUIC +static void handle_quic_stream(knot_xquic_conn_t *conn, int64_t stream_id, struct iovec *inbuf, + knot_layer_t *layer, knotd_qdata_params_t *params, uint8_t *ans_buf, + size_t ans_buf_size, const knot_xdp_msg_t *xdp_msg) +{ + // Consume the query. + handle_init(params, layer, KNOTD_QUERY_PROTO_QUIC, xdp_msg, inbuf, NULL); + params->measured_rtt = knot_xquic_conn_rtt(conn); + + // Process the reply. + knot_pkt_t *ans = knot_pkt_new(ans_buf, ans_buf_size, layer->mm); + while (tcp_active_state(layer->state)) { + knot_layer_produce(layer, ans); + if (!tcp_send_state(layer->state)) { + continue; + } + if (knot_xquic_stream_add_data(conn, stream_id, ans->wire, ans->size) == NULL) { + break; + } + } + + handle_finish(layer); +} +#endif // ENABLE_QUIC + +static void handle_quic(xdp_handle_ctx_t *ctx, knot_layer_t *layer, + knotd_qdata_params_t *params) +{ +#ifdef ENABLE_QUIC + if (ctx->quic_table == NULL) { + return; + } + + uint8_t ans_buf[KNOT_WIRE_MAX_PKTSIZE]; + + for (uint32_t i = 0; i < ctx->msg_recv_count; i++) { + knot_xdp_msg_t *msg_recv = &ctx->msg_recv[i]; + ctx->quic_relays[i] = NULL; + + if ((msg_recv->flags & KNOT_XDP_MSG_TCP) || + msg_recv->ip_to.sin6_port != ctx->quic_port || + msg_recv->payload.iov_len == 0) { + continue; + } + + ctx->quic_rets[i] = knot_xquic_handle(ctx->quic_table, msg_recv, + ctx->quic_idle_close, + &ctx->quic_relays[i]); + knot_xquic_conn_t *rl = ctx->quic_relays[i]; + + int64_t stream_id; + knot_xquic_stream_t *stream; + + while (rl != NULL && (stream = knot_xquic_stream_get_process(rl, &stream_id)) != NULL) { + assert(stream->inbuf_fin != NULL); + assert(stream->inbuf_fin->iov_len > 0); + handle_quic_stream(rl, stream_id, stream->inbuf_fin, layer, params, + ans_buf, sizeof(ans_buf), &ctx->msg_recv[i]); + free(stream->inbuf_fin); + stream->inbuf_fin = NULL; + } + } +#else + (void)(ctx); + (void)(layer); + (void)(params); +#endif // ENABLE_QUIC +} + +void xdp_handle_msgs(xdp_handle_ctx_t *ctx, knot_layer_t *layer, + server_t *server, unsigned thread_id) +{ + assert(ctx->msg_recv_count > 0); + + knotd_qdata_params_t params = { + .socket = knot_xdp_socket_fd(ctx->sock), + .server = server, + .thread_id = thread_id, + }; + + knot_xdp_send_prepare(ctx->sock); + + handle_udp(ctx, layer, ¶ms); + if (ctx->tcp) { + handle_tcp(ctx, layer, ¶ms); + } + handle_quic(ctx, layer, ¶ms); + + knot_xdp_recv_finish(ctx->sock, ctx->msg_recv, ctx->msg_recv_count); +} + +void xdp_handle_send(xdp_handle_ctx_t *ctx) +{ + uint32_t unused; + int ret = knot_xdp_send(ctx->sock, ctx->msg_send_udp, ctx->msg_udp_count, &unused); + if (ret != KNOT_EOK) { + log_notice("UDP, failed to send some packets"); + } + if (ctx->tcp) { + ret = knot_tcp_send(ctx->sock, ctx->relays, ctx->msg_recv_count, + XDP_BATCHLEN); + if (ret != KNOT_EOK) { + log_notice("TCP, failed to send some packets"); + } + } +#ifdef ENABLE_QUIC + for (uint32_t i = 0; i < ctx->msg_recv_count; i++) { + if (ctx->quic_relays[i] == NULL) { + continue; + } + + ret = knot_xquic_send(ctx->quic_table, ctx->quic_relays[i], ctx->sock, + &ctx->msg_recv[i], ctx->quic_rets[i], + QUIC_MAX_SEND_PER_RECV, false); + if (ret != KNOT_EOK) { + log_notice("QUIC, failed to send some packets"); + } + } + knot_xquic_cleanup(ctx->quic_relays, ctx->msg_recv_count); +#endif // ENABLE_QUIC + + (void)knot_xdp_send_finish(ctx->sock); + + if (ctx->tcp) { + knot_tcp_cleanup(ctx->tcp_table, ctx->relays, ctx->msg_recv_count); + } +} + +void xdp_handle_sweep(xdp_handle_ctx_t *ctx) +{ +#ifdef ENABLE_QUIC + if (ctx->quic_table != NULL) { + knot_xquic_table_sweep(ctx->quic_table, &ctx->quic_closed.stats); + log_closed(&ctx->quic_closed, false); + } +#endif // ENABLE_QUIC + + if (!ctx->tcp) { + return; + } + + int ret = KNOT_EOK; + uint32_t prev_total; + knot_tcp_relay_t sweep_relays[XDP_BATCHLEN]; + do { + knot_xdp_send_prepare(ctx->sock); + + prev_total = ctx->tcp_closed.stats.total; + + ret = knot_tcp_sweep(ctx->tcp_table, ctx->tcp_idle_close, ctx->tcp_idle_reset, + ctx->tcp_idle_resend, + ctx->tcp_max_conns, ctx->tcp_max_inbufs, ctx->tcp_max_obufs, + sweep_relays, XDP_BATCHLEN, &ctx->tcp_closed.stats); + if (ret == KNOT_EOK) { + ret = knot_tcp_send(ctx->sock, sweep_relays, XDP_BATCHLEN, XDP_BATCHLEN); + } + knot_tcp_cleanup(ctx->tcp_table, sweep_relays, XDP_BATCHLEN); + if (ret != KNOT_EOK) { + break; + } + + ret = knot_tcp_sweep(ctx->syn_table, UINT32_MAX, ctx->tcp_idle_reset, + UINT32_MAX, ctx->tcp_syn_conns, SIZE_MAX, SIZE_MAX, + sweep_relays, XDP_BATCHLEN, &ctx->tcp_closed.stats); + if (ret == KNOT_EOK) { + ret = knot_tcp_send(ctx->sock, sweep_relays, XDP_BATCHLEN, XDP_BATCHLEN); + } + knot_tcp_cleanup(ctx->syn_table, sweep_relays, XDP_BATCHLEN); + + (void)knot_xdp_send_finish(ctx->sock); + } while (ret == KNOT_EOK && prev_total < ctx->tcp_closed.stats.total); + + log_closed(&ctx->tcp_closed, true); +} + +#endif // ENABLE_XDP diff --git a/src/knot/server/xdp-handler.h b/src/knot/server/xdp-handler.h new file mode 100644 index 0000000..e6374ca --- /dev/null +++ b/src/knot/server/xdp-handler.h @@ -0,0 +1,67 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#ifdef ENABLE_XDP + +#include "knot/query/layer.h" +#include "libknot/xdp/xdp.h" + +#define XDP_BATCHLEN 32 /*!< XDP receive batch size. */ + +struct xdp_handle_ctx; +struct server; + +/*! + * \brief Initialize XDP packet handling context. + */ +struct xdp_handle_ctx *xdp_handle_init(struct server *server, knot_xdp_socket_t *sock); + +/*! + * \brief Deinitialize XDP packet handling context. + */ +void xdp_handle_free(struct xdp_handle_ctx *ctx); + +/*! + * \brief Receive packets thru XDP socket. + */ +int xdp_handle_recv(struct xdp_handle_ctx *ctx); + +/*! + * \brief Answer packets including DNS layers. + * + * \warning In case of TCP, this also sends some packets, e.g. ACK. + */ +void xdp_handle_msgs(struct xdp_handle_ctx *ctx, knot_layer_t *layer, + struct server *server, unsigned thread_id); + +/*! + * \brief Send packets thru XDP socket. + */ +void xdp_handle_send(struct xdp_handle_ctx *ctx); + +/*! + * \brief Check for old TCP connections and close/reset them. + */ +void xdp_handle_sweep(struct xdp_handle_ctx *ctx); + +/*! + * \brief Update configuration parameters of running ctx. + */ +void xdp_handle_reconfigure(struct xdp_handle_ctx *ctx); + +#endif // ENABLE_XDP diff --git a/src/knot/updates/acl.c b/src/knot/updates/acl.c new file mode 100644 index 0000000..b46c893 --- /dev/null +++ b/src/knot/updates/acl.c @@ -0,0 +1,361 @@ +/* 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/updates/acl.h" +#include "contrib/wire_ctx.h" + +static bool match_type(uint16_t type, conf_val_t *types) +{ + if (types == NULL) { + return true; + } + + conf_val_reset(types); + while (types->code == KNOT_EOK) { + if (type == knot_wire_read_u64(types->data)) { + return true; + } + conf_val_next(types); + } + + return false; +} + +static bool match_name(const knot_dname_t *rr_owner, const knot_dname_t *name, + acl_update_owner_match_t match) +{ + if (name == NULL) { + return true; + } + + int ret = knot_dname_in_bailiwick(rr_owner, name); + switch (match) { + case ACL_UPDATE_MATCH_SUBEQ: + return (ret >= 0); + case ACL_UPDATE_MATCH_EQ: + return (ret == 0); + case ACL_UPDATE_MATCH_SUB: + return (ret > 0); + default: + return false; + } +} + +static bool match_names(const knot_dname_t *rr_owner, const knot_dname_t *zone_name, + conf_val_t *names, acl_update_owner_match_t match) +{ + if (names == NULL) { + return true; + } + + conf_val_reset(names); + while (names->code == KNOT_EOK) { + knot_dname_storage_t full_name; + size_t len; + const uint8_t *name = conf_data(names, &len); + if (name[len - 1] != '\0') { + // Append zone name if non-FQDN. + wire_ctx_t ctx = wire_ctx_init(full_name, sizeof(full_name)); + wire_ctx_write(&ctx, name, len); + wire_ctx_write(&ctx, zone_name, knot_dname_size(zone_name)); + if (ctx.error != KNOT_EOK) { + return false; + } + name = full_name; + } + if (match_name(rr_owner, name, match)) { + return true; + } + conf_val_next(names); + } + + return false; +} + +static bool update_match(conf_t *conf, conf_val_t *acl, knot_dname_t *key_name, + const knot_dname_t *zone_name, knot_pkt_t *query) +{ + if (query == NULL) { + return true; + } + + conf_val_t val_types = conf_id_get(conf, C_ACL, C_UPDATE_TYPE, acl); + conf_val_t *types = (conf_val_count(&val_types) > 0) ? &val_types : NULL; + + conf_val_t val = conf_id_get(conf, C_ACL, C_UPDATE_OWNER, acl); + acl_update_owner_t owner = conf_opt(&val); + + /* Return if no specific requirements configured. */ + if (types == NULL && owner == ACL_UPDATE_OWNER_NONE) { + return true; + } + + acl_update_owner_match_t match = ACL_UPDATE_MATCH_SUBEQ; + if (owner != ACL_UPDATE_OWNER_NONE) { + val = conf_id_get(conf, C_ACL, C_UPDATE_OWNER_MATCH, acl); + match = conf_opt(&val); + } + + conf_val_t *names = NULL; + conf_val_t val_names; + if (owner == ACL_UPDATE_OWNER_NAME) { + val_names = conf_id_get(conf, C_ACL, C_UPDATE_OWNER_NAME, acl); + if (conf_val_count(&val_names) > 0) { + names = &val_names; + } + } + + /* Updated RRs are contained in the Authority section of the query + * (RFC 2136 Section 2.2) + */ + uint16_t pos = query->sections[KNOT_AUTHORITY].pos; + uint16_t count = query->sections[KNOT_AUTHORITY].count; + + for (int i = pos; i < pos + count; i++) { + knot_rrset_t *rr = &query->rr[i]; + if (!match_type(rr->type, types)) { + return false; + } + + switch (owner) { + case ACL_UPDATE_OWNER_NAME: + if (!match_names(rr->owner, zone_name, names, match)) { + return false; + } + break; + case ACL_UPDATE_OWNER_KEY: + if (!match_name(rr->owner, key_name, match)) { + return false; + } + break; + case ACL_UPDATE_OWNER_ZONE: + if (!match_name(rr->owner, zone_name, match)) { + return false; + } + break; + default: + break; + } + } + + return true; +} + +static bool check_addr_key(conf_t *conf, conf_val_t *addr_val, conf_val_t *key_val, + bool remote, const struct sockaddr_storage *addr, + const knot_tsig_key_t *tsig, bool deny) +{ + /* Check if the address matches the acl address list or remote addresses. */ + if (addr_val->code != KNOT_ENOENT) { + if (remote) { + if (!conf_addr_match(addr_val, addr)) { + return false; + } + } else { + if (!conf_addr_range_match(addr_val, addr)) { + return false; + } + } + } + + /* Check if the key matches the acl key list or remote key. */ + while (key_val->code == KNOT_EOK) { + /* No key provided, but required. */ + if (tsig->name == NULL) { + goto next_key; + } + + /* Compare key names (both in lower-case). */ + const knot_dname_t *key_name = conf_dname(key_val); + if (!knot_dname_is_equal(key_name, tsig->name)) { + goto next_key; + } + + /* Compare key algorithms. */ + conf_val_t alg_val = conf_id_get(conf, C_KEY, C_ALG, key_val); + if (conf_opt(&alg_val) != tsig->algorithm) { + goto next_key; + } + + break; + next_key: + if (remote) { + assert(!(key_val->item->flags & YP_FMULTI)); + key_val->code = KNOT_EOF; + break; + } else { + assert(key_val->item->flags & YP_FMULTI); + conf_val_next(key_val); + } + } + switch (key_val->code) { + case KNOT_EOK: + // Key match. + break; + case KNOT_ENOENT: + // Empty list without key provided or denied. + if (tsig->name == NULL || deny) { + break; + } + // FALLTHROUGH + default: + return false; + } + + return true; +} + +bool acl_allowed(conf_t *conf, conf_val_t *acl, acl_action_t action, + const struct sockaddr_storage *addr, knot_tsig_key_t *tsig, + const knot_dname_t *zone_name, knot_pkt_t *query) +{ + if (acl == NULL || addr == NULL || tsig == NULL) { + return false; + } + + while (acl->code == KNOT_EOK) { + conf_val_t rmt_val = conf_id_get(conf, C_ACL, C_RMT, acl); + bool remote = (rmt_val.code == KNOT_EOK); + conf_val_t deny_val = conf_id_get(conf, C_ACL, C_DENY, acl); + bool deny = conf_bool(&deny_val); + + /* Check if a remote matches given address and key. */ + conf_val_t addr_val, key_val; + conf_mix_iter_t iter; + conf_mix_iter_init(conf, &rmt_val, &iter); + while (iter.id->code == KNOT_EOK) { + addr_val = conf_id_get(conf, C_RMT, C_ADDR, iter.id); + key_val = conf_id_get(conf, C_RMT, C_KEY, iter.id); + if (check_addr_key(conf, &addr_val, &key_val, remote, addr, tsig, deny)) { + break; + } + conf_mix_iter_next(&iter); + } + if (iter.id->code == KNOT_EOF) { + goto next_acl; + } + /* Or check if acl address/key matches given address and key. */ + if (!remote) { + addr_val = conf_id_get(conf, C_ACL, C_ADDR, acl); + key_val = conf_id_get(conf, C_ACL, C_KEY, acl); + if (!check_addr_key(conf, &addr_val, &key_val, remote, addr, tsig, deny)) { + goto next_acl; + } + } + + /* Check if the action is allowed. */ + if (action != ACL_ACTION_QUERY) { + conf_val_t val = conf_id_get(conf, C_ACL, C_ACTION, acl); + while (val.code == KNOT_EOK) { + if (conf_opt(&val) != action) { + conf_val_next(&val); + continue; + } + + break; + } + switch (val.code) { + case KNOT_EOK: /* Check for action match. */ + break; + case KNOT_ENOENT: /* Empty action list allowed with deny only. */ + return false; + default: /* No match. */ + goto next_acl; + } + } + + /* If the action is update, check for update rule match. */ + if (action == ACL_ACTION_UPDATE && + !update_match(conf, acl, tsig->name, zone_name, query)) { + goto next_acl; + } + + /* Check if denied. */ + if (deny) { + return false; + } + + /* Fill the output with tsig secret if provided. */ + if (tsig->name != NULL) { + conf_val_t val = conf_id_get(conf, C_KEY, C_SECRET, &key_val); + tsig->secret.data = (uint8_t *)conf_bin(&val, &tsig->secret.size); + } + + return true; +next_acl: + conf_val_next(acl); + } + + return false; +} + +bool rmt_allowed(conf_t *conf, conf_val_t *rmts, const struct sockaddr_storage *addr, + knot_tsig_key_t *tsig) +{ + if (!conf->cache.srv_auto_acl) { + return false; + } + + conf_mix_iter_t iter; + conf_mix_iter_init(conf, rmts, &iter); + while (iter.id->code == KNOT_EOK) { + conf_val_t val = conf_id_get(conf, C_RMT, C_AUTO_ACL, iter.id); + if (!conf_bool(&val)) { + goto next_remote; + } + + conf_val_t key_id = conf_id_get(conf, C_RMT, C_KEY, iter.id); + if (key_id.code == KNOT_EOK) { + /* No key provided, but required. */ + if (tsig->name == NULL) { + goto next_remote; + } + + /* Compare key names (both in lower-case). */ + const knot_dname_t *key_name = conf_dname(&key_id); + if (!knot_dname_is_equal(key_name, tsig->name)) { + goto next_remote; + } + + /* Compare key algorithms. */ + val = conf_id_get(conf, C_KEY, C_ALG, &key_id); + if (conf_opt(&val) != tsig->algorithm) { + goto next_remote; + } + } else if (key_id.code == KNOT_ENOENT && tsig->name != NULL) { + /* Key provided but no key configured. */ + goto next_remote; + } + + /* Check if the address matches. */ + val = conf_id_get(conf, C_RMT, C_ADDR, iter.id); + if (!conf_addr_match(&val, addr)) { + goto next_remote; + } + + /* Fill out the output with tsig secret if provided. */ + if (tsig->name != NULL) { + val = conf_id_get(conf, C_KEY, C_SECRET, &key_id); + tsig->secret.data = (uint8_t *)conf_bin(&val, &tsig->secret.size); + } + + return true; +next_remote: + conf_mix_iter_next(&iter); + } + + return false; +} diff --git a/src/knot/updates/acl.h b/src/knot/updates/acl.h new file mode 100644 index 0000000..8c15acf --- /dev/null +++ b/src/knot/updates/acl.h @@ -0,0 +1,83 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> +#include <sys/socket.h> + +#include "libknot/tsig.h" +#include "knot/conf/conf.h" + +/*! \brief ACL actions. */ +typedef enum { + ACL_ACTION_QUERY = 0, + ACL_ACTION_NOTIFY = 1, + ACL_ACTION_TRANSFER = 2, + ACL_ACTION_UPDATE = 3 +} acl_action_t; + +/*! \brief ACL update owner matching options. */ +typedef enum { + ACL_UPDATE_OWNER_NONE = 0, + ACL_UPDATE_OWNER_KEY = 1, + ACL_UPDATE_OWNER_ZONE = 2, + ACL_UPDATE_OWNER_NAME = 3, +} acl_update_owner_t; + +/*! \bref ACL update owner comparison options. */ +typedef enum { + ACL_UPDATE_MATCH_SUBEQ = 0, + ACL_UPDATE_MATCH_EQ = 1, + ACL_UPDATE_MATCH_SUB = 2, +} acl_update_owner_match_t; + +/*! + * \brief Checks if the address and/or tsig key matches given ACL list. + * + * If a proper ACL rule is found and tsig.name is not empty, tsig.secret is filled. + * + * \param conf Configuration. + * \param acl Pointer to ACL config multivalued identifier. + * \param action ACL action. + * \param addr IP address. + * \param tsig TSIG parameters. + * \param zone_name Zone name. + * \param query Update query. + * + * \retval True if authenticated. + */ +bool acl_allowed(conf_t *conf, conf_val_t *acl, acl_action_t action, + const struct sockaddr_storage *addr, knot_tsig_key_t *tsig, + const knot_dname_t *zone_name, knot_pkt_t *query); + +/*! + * \brief Checks if the address and/or tsig key matches a remote from the list. + * + * Global (server.automatic-acl) and per remote automatic ACL functionality + * must be enabled in order to decide the remote is allowed. + * + * If a proper REMOTE is found and tsig.name is not empty, tsig.secret is filled. + * + * \param conf Configuration. + * \param rmts Pointer to REMOTE config multivalued identifier. + * \param addr IP address. + * \param tsig TSIG parameters. + * + * \retval True if authenticated. + */ +bool rmt_allowed(conf_t *conf, conf_val_t *rmts, const struct sockaddr_storage *addr, + knot_tsig_key_t *tsig); diff --git a/src/knot/updates/apply.c b/src/knot/updates/apply.c new file mode 100644 index 0000000..b96432e --- /dev/null +++ b/src/knot/updates/apply.c @@ -0,0 +1,379 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "knot/common/log.h" +#include "knot/updates/apply.h" +#include "libknot/libknot.h" +#include "contrib/macros.h" +#include "contrib/mempattern.h" + +/*! \brief Replaces rdataset of given type with a copy. */ +static int replace_rdataset_with_copy(zone_node_t *node, uint16_t type) +{ + int ret = binode_prepare_change(node, NULL); + if (ret != KNOT_EOK) { + return ret; + } + + // Find data to copy. + struct rr_data *data = NULL; + for (uint16_t i = 0; i < node->rrset_count; ++i) { + if (node->rrs[i].type == type) { + data = &node->rrs[i]; + break; + } + } + if (data == NULL) { + return KNOT_EOK; + } + + // Create new data. + knot_rdataset_t *rrs = &data->rrs; + void *copy = malloc(rrs->size); + if (copy == NULL) { + return KNOT_ENOMEM; + } + + memcpy(copy, rrs->rdata, rrs->size); + + // Store new data into node RRS. + rrs->rdata = copy; + + return KNOT_EOK; +} + +/*! \brief Frees RR dataset. For use when a copy was made. */ +static void clear_new_rrs(zone_node_t *node, uint16_t type) +{ + knot_rdataset_t *new_rrs = node_rdataset(node, type); + if (new_rrs) { + knot_rdataset_clear(new_rrs, NULL); + } +} + +/*! \brief Logs redundant rrset operation. */ +static void can_log_rrset(const knot_rrset_t *rrset, int pos, apply_ctx_t *ctx, bool remove) +{ + if (!(ctx->flags & APPLY_STRICT)) { + return; + } + + char type[16]; + char data[1024]; + const char *msg = remove ? "cannot remove nonexisting RR" : + "cannot add existing RR"; + + char *owner = knot_dname_to_str_alloc(rrset->owner); + if (owner != NULL && knot_rrtype_to_string(rrset->type, type, sizeof(type)) > 0 && + knot_rrset_txt_dump_data(rrset, pos, data, sizeof(data), &KNOT_DUMP_STYLE_DEFAULT) > 0) { + log_zone_debug(ctx->contents->apex->owner, + "node %s, type %s, data '%s', %s", owner, type, data, msg); + } + free(owner); +} + +/*! \brief Returns true if given RR is present in node and can be removed. */ +static bool can_remove(const zone_node_t *node, const knot_rrset_t *rrset, apply_ctx_t *ctx) +{ + if (node == NULL) { + // Node does not exist, cannot remove anything. + can_log_rrset(rrset, 0, ctx, true); + return false; + } + + const knot_rdataset_t *node_rrs = node_rdataset(node, rrset->type); + if (node_rrs == NULL) { + // Node does not have this type at all. + can_log_rrset(rrset, 0, ctx, true); + return false; + } + + knot_rdata_t *rr_cmp = rrset->rrs.rdata; + for (uint16_t i = 0; i < rrset->rrs.count; ++i) { + if (!knot_rdataset_member(node_rrs, rr_cmp)) { + // At least one RR doesnt' match. + can_log_rrset(rrset, i, ctx, true); + return false; + } + rr_cmp = knot_rdataset_next(rr_cmp); + } + + return true; +} + +/*! \brief Returns true if given RR is not present in node and can be added. */ +static bool can_add(const zone_node_t *node, const knot_rrset_t *rrset, apply_ctx_t *ctx) +{ + if (node == NULL) { + // Node does not exist, can add anything. + return true; + } + const knot_rdataset_t *node_rrs = node_rdataset(node, rrset->type); + if (node_rrs == NULL) { + // Node does not have this type at all. + return true; + } + + knot_rdata_t *rr_cmp = rrset->rrs.rdata; + for (uint16_t i = 0; i < rrset->rrs.count; ++i) { + if (knot_rdataset_member(node_rrs, rr_cmp)) { + // No RR must match. + can_log_rrset(rrset, i, ctx, false); + return false; + } + rr_cmp = knot_rdataset_next(rr_cmp); + } + + return true; +} + +int apply_init_ctx(apply_ctx_t *ctx, zone_contents_t *contents, uint32_t flags) +{ + if (ctx == NULL) { + return KNOT_EINVAL; + } + + ctx->contents = contents; + + ctx->node_ptrs = zone_tree_create(true); + if (ctx->node_ptrs == NULL) { + return KNOT_ENOMEM; + } + ctx->node_ptrs->flags = contents->nodes->flags; + + ctx->nsec3_ptrs = zone_tree_create(true); + if (ctx->nsec3_ptrs == NULL) { + zone_tree_free(&ctx->node_ptrs); + return KNOT_ENOMEM; + } + ctx->nsec3_ptrs->flags = contents->nodes->flags; + + ctx->adjust_ptrs = zone_tree_create(true); + if (ctx->adjust_ptrs == NULL) { + zone_tree_free(&ctx->nsec3_ptrs); + zone_tree_free(&ctx->node_ptrs); + return KNOT_ENOMEM; + } + ctx->adjust_ptrs->flags = contents->nodes->flags; + + ctx->flags = flags; + + return KNOT_EOK; +} + +static zone_node_t *add_node_cb(const knot_dname_t *owner, void *ctx) +{ + zone_tree_t *tree = ctx; + zone_node_t *node = zone_tree_get(tree, owner); + if (node == NULL) { + node = node_new_for_tree(owner, tree, NULL); + } else { + node->flags &= ~NODE_FLAGS_DELETED; + } + return node; +} + +int apply_add_rr(apply_ctx_t *ctx, const knot_rrset_t *rr) +{ + zone_contents_t *contents = ctx->contents; + bool nsec3rel = knot_rrset_is_nsec3rel(rr); + zone_tree_t *ptrs = nsec3rel ? ctx->nsec3_ptrs : ctx->node_ptrs; + zone_tree_t *tree = zone_contents_tree_for_rr(contents, rr); + if (tree == NULL) { + return KNOT_ENOMEM; + } + + // Get or create node with this owner, search changes first + zone_node_t *node = NULL; + int ret = zone_tree_add_node(tree, contents->apex, rr->owner, add_node_cb, ptrs, &node); + if (ret != KNOT_EOK) { + return ret; + } + + if (!can_add(node, rr, ctx)) { + return (ctx->flags & APPLY_STRICT) ? KNOT_EISRECORD : KNOT_EOK; + } + + ret = zone_tree_insert_with_parents(ptrs, node, nsec3rel); + if (ret != KNOT_EOK) { + return ret; + } + + if (binode_rdata_shared(node, rr->type)) { + // Modifying existing RRSet. + ret = replace_rdataset_with_copy(node, rr->type); + if (ret != KNOT_EOK) { + return ret; + } + } + + // Insert new RR to RRSet, data will be copied. + ret = node_add_rrset(node, rr, NULL); + if (ret == KNOT_ETTL) { + // this shall not happen except applying journal created before this bugfix + return KNOT_EOK; + } + return ret; +} + +int apply_remove_rr(apply_ctx_t *ctx, const knot_rrset_t *rr) +{ + zone_contents_t *contents = ctx->contents; + bool nsec3rel = knot_rrset_is_nsec3rel(rr); + zone_tree_t *ptrs = nsec3rel ? ctx->nsec3_ptrs : ctx->node_ptrs; + zone_tree_t *tree = zone_contents_tree_for_rr(contents, rr); + if (tree == NULL) { + return KNOT_ENOMEM; + } + + // Find node for this owner + zone_node_t *node = zone_contents_find_node_for_rr(contents, rr); + if (!can_remove(node, rr, ctx)) { + return (ctx->flags & APPLY_STRICT) ? KNOT_ENORECORD : KNOT_EOK; + } + + int ret = zone_tree_insert_with_parents(ptrs, node, nsec3rel); + if (ret != KNOT_EOK) { + return ret; + } + + if (binode_rdata_shared(node, rr->type)) { + ret = replace_rdataset_with_copy(node, rr->type); + if (ret != KNOT_EOK) { + return ret; + } + } + + ret = node_remove_rrset(node, rr, NULL); + if (ret != KNOT_EOK) { + clear_new_rrs(node, rr->type); + return ret; + } + + if (node->rrset_count == 0 && node->children == 0 && node != contents->apex) { + zone_tree_del_node(tree, node, false); + } + + return KNOT_EOK; +} + +int apply_replace_soa(apply_ctx_t *ctx, const knot_rrset_t *rr) +{ + zone_contents_t *contents = ctx->contents; + + if (!knot_dname_is_equal(rr->owner, contents->apex->owner)) { + return KNOT_EDENIED; + } + + knot_rrset_t old_soa = node_rrset(contents->apex, KNOT_RRTYPE_SOA); + + int ret = apply_remove_rr(ctx, &old_soa); + if (ret != KNOT_EOK) { + return ret; + } + + // Check for SOA with proper serial but different rdata. + if (node_rrtype_exists(contents->apex, KNOT_RRTYPE_SOA)) { + return KNOT_ESOAINVAL; + } + + return apply_add_rr(ctx, rr); +} + +void apply_cleanup(apply_ctx_t *ctx) +{ + if (ctx == NULL) { + return; + } + + if (ctx->flags & APPLY_UNIFY_FULL) { + zone_trees_unify_binodes(ctx->contents->nodes, ctx->contents->nsec3_nodes, true); + } else { + zone_trees_unify_binodes(ctx->adjust_ptrs, NULL, false); // beware there might be duplicities in ctx->adjust_ptrs and ctx->node_ptrs, so we don't free here + zone_trees_unify_binodes(ctx->node_ptrs, ctx->nsec3_ptrs, true); + } + + zone_tree_free(&ctx->node_ptrs); + zone_tree_free(&ctx->nsec3_ptrs); + zone_tree_free(&ctx->adjust_ptrs); + + if (ctx->cow_mutex != NULL) { + knot_sem_post(ctx->cow_mutex); + } +} + +void apply_rollback(apply_ctx_t *ctx) +{ + if (ctx == NULL) { + return; + } + + if (ctx->node_ptrs != NULL) { + ctx->node_ptrs->flags ^= ZONE_TREE_BINO_SECOND; + } + if (ctx->nsec3_ptrs != NULL) { + ctx->nsec3_ptrs->flags ^= ZONE_TREE_BINO_SECOND; + } + zone_trees_unify_binodes(ctx->node_ptrs, ctx->nsec3_ptrs, true); + + zone_tree_free(&ctx->node_ptrs); + zone_tree_free(&ctx->nsec3_ptrs); + zone_tree_free(&ctx->adjust_ptrs); + + trie_cow_rollback(ctx->contents->nodes->cow, NULL, NULL); + ctx->contents->nodes->cow = NULL; + if (ctx->contents->nsec3_nodes != NULL && ctx->contents->nsec3_nodes->cow != NULL) { + trie_cow_rollback(ctx->contents->nsec3_nodes->cow, NULL, NULL); + ctx->contents->nsec3_nodes->cow = NULL; + } else if (ctx->contents->nsec3_nodes != NULL) { + zone_tree_free(&ctx->contents->nsec3_nodes); + ctx->contents->nsec3_nodes = NULL; + } + + free(ctx->contents->nodes); + free(ctx->contents->nsec3_nodes); + + dnssec_nsec3_params_free(&ctx->contents->nsec3_params); + + free(ctx->contents); + + if (ctx->cow_mutex != NULL) { + knot_sem_post(ctx->cow_mutex); + } +} + +void update_free_zone(zone_contents_t *contents) +{ + if (contents == NULL) { + return; + } + + trie_cow_commit(contents->nodes->cow, NULL, NULL); + contents->nodes->cow = NULL; + if (contents->nsec3_nodes != NULL && contents->nsec3_nodes->cow != NULL) { + trie_cow_commit(contents->nsec3_nodes->cow, NULL, NULL); + contents->nsec3_nodes->cow = NULL; + } + + free(contents->nodes); + free(contents->nsec3_nodes); + + dnssec_nsec3_params_free(&contents->nsec3_params); + + free(contents); +} diff --git a/src/knot/updates/apply.h b/src/knot/updates/apply.h new file mode 100644 index 0000000..2d3588b --- /dev/null +++ b/src/knot/updates/apply.h @@ -0,0 +1,101 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/semaphore.h" +#include "knot/zone/contents.h" +#include "knot/updates/changesets.h" +#include "contrib/ucw/lists.h" + +enum { + APPLY_STRICT = 1 << 0, /*!< Apply strictly, don't ignore removing non-existent RRs. */ + APPLY_UNIFY_FULL = 1 << 1, /*!< When cleaning up successful update, perform full trees nodes unify. */ +}; + +struct apply_ctx { + zone_contents_t *contents; + zone_tree_t *node_ptrs; /*!< Just pointers to the affected nodes in contents. */ + zone_tree_t *nsec3_ptrs; /*!< The same for NSEC3 nodes. */ + zone_tree_t *adjust_ptrs; /*!< Pointers to nodes affected by adjusting. */ + uint32_t flags; + knot_sem_t *cow_mutex; +}; + +typedef struct apply_ctx apply_ctx_t; + +/*! + * \brief Initialize a new context structure. + * + * \param ctx Context to be initialized. + * \param contents Zone contents to apply changes onto. + * \param flags Flags to control the application process. + * + * \return KNOT_E* + */ +int apply_init_ctx(apply_ctx_t *ctx, zone_contents_t *contents, uint32_t flags); + +/*! + * \brief Adds a single RR into zone contents. + * + * \param ctx Apply context. + * \param rr RRSet to add. + * + * \return KNOT_E* + */ +int apply_add_rr(apply_ctx_t *ctx, const knot_rrset_t *rr); + +/*! + * \brief Removes single RR from zone contents. + * + * \param ctx Apply context. + * \param rr RRSet to remove. + * + * \return KNOT_E* + */ +int apply_remove_rr(apply_ctx_t *ctx, const knot_rrset_t *rr); + +/*! + * \brief Remove SOA and add a new SOA. + * + * \param ctx Apply context. + * \param rr New SOA to be added. + * + * \return KNOT_E* + */ +int apply_replace_soa(apply_ctx_t *ctx, const knot_rrset_t *rr); + +/*! + * \brief Cleanups successful zone update. + * + * \param ctx Context used to create the update. + */ +void apply_cleanup(apply_ctx_t *ctx); + +/*! + * \brief Rollbacks failed zone update. + * + * \param ctx Context used to create the update. + */ +void apply_rollback(apply_ctx_t *ctx); + +/*! + * \brief Shallow frees zone contents - either shallow copy after failed update + * or original zone contents after successful update. + * + * \param contents Contents to free. + */ +void update_free_zone(zone_contents_t *contents); diff --git a/src/knot/updates/changesets.c b/src/knot/updates/changesets.c new file mode 100644 index 0000000..1d1a0d3 --- /dev/null +++ b/src/knot/updates/changesets.c @@ -0,0 +1,628 @@ +/* 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 <stdlib.h> +#include <stdarg.h> + +#include "knot/updates/changesets.h" +#include "knot/updates/apply.h" +#include "knot/zone/zone-dump.h" +#include "contrib/color.h" +#include "contrib/time.h" +#include "libknot/libknot.h" + +static int handle_soa(knot_rrset_t **soa, const knot_rrset_t *rrset) +{ + assert(soa); + assert(rrset); + + if (*soa != NULL) { + knot_rrset_free(*soa, NULL); + } + + *soa = knot_rrset_copy(rrset, NULL); + if (*soa == NULL) { + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +/*! \brief Adds RRSet to given zone. */ +static int add_rr_to_contents(zone_contents_t *z, const knot_rrset_t *rrset) +{ + _unused_ zone_node_t *n = NULL; + int ret = zone_contents_add_rr(z, rrset, &n); + + // We don't care of TTLs. + return ret == KNOT_ETTL ? KNOT_EOK : ret; +} + +/*! \brief Inits changeset iterator with given tries. */ +static int changeset_iter_init(changeset_iter_t *ch_it, size_t tries, ...) +{ + memset(ch_it, 0, sizeof(*ch_it)); + + va_list args; + va_start(args, tries); + + assert(tries <= sizeof(ch_it->trees) / sizeof(*ch_it->trees)); + for (size_t i = 0; i < tries; ++i) { + zone_tree_t *t = va_arg(args, zone_tree_t *); + if (t == NULL) { + continue; + } + + ch_it->trees[ch_it->n_trees++] = t; + } + + va_end(args); + + assert(ch_it->n_trees); + return zone_tree_it_begin(ch_it->trees[0], &ch_it->it); +} + +// removes from counterpart what is in rr. +// fixed_rr is an output parameter, holding a copy of rr without what has been removed from counterpart +static void check_redundancy(zone_contents_t *counterpart, const knot_rrset_t *rr, knot_rrset_t **fixed_rr) +{ + if (fixed_rr != NULL) { + *fixed_rr = knot_rrset_copy(rr, NULL); + } + + zone_node_t *node = zone_contents_find_node_for_rr(counterpart, rr); + if (node == NULL) { + return; + } + + if (!node_rrtype_exists(node, rr->type)) { + return; + } + + uint32_t rrs_ttl = node_rrset(node, rr->type).ttl; + + if (fixed_rr != NULL && *fixed_rr != NULL && + ((*fixed_rr)->ttl == rrs_ttl || rr->type == KNOT_RRTYPE_RRSIG)) { + int ret = knot_rdataset_subtract(&(*fixed_rr)->rrs, node_rdataset(node, rr->type), NULL); + if (ret != KNOT_EOK) { + return; + } + } + + // TTL of RRSIGs is better determined by original_ttl field, which is compared as part of rdata anyway + if (rr->ttl == rrs_ttl || rr->type == KNOT_RRTYPE_RRSIG) { + int ret = node_remove_rrset(node, rr, NULL); + if (ret != KNOT_EOK) { + return; + } + } + + if (node->rrset_count == 0 && node->children == 0 && node != counterpart->apex) { + zone_tree_t *t = knot_rrset_is_nsec3rel(rr) ? + counterpart->nsec3_nodes : counterpart->nodes; + zone_tree_del_node(t, node, true); + } + + return; +} + +int changeset_init(changeset_t *ch, const knot_dname_t *apex) +{ + memset(ch, 0, sizeof(changeset_t)); + + // Init local changes + ch->add = zone_contents_new(apex, false); + if (ch->add == NULL) { + return KNOT_ENOMEM; + } + ch->remove = zone_contents_new(apex, false); + if (ch->remove == NULL) { + zone_contents_free(ch->add); + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +changeset_t *changeset_new(const knot_dname_t *apex) +{ + changeset_t *ret = malloc(sizeof(changeset_t)); + if (ret == NULL) { + return NULL; + } + + if (changeset_init(ret, apex) == KNOT_EOK) { + return ret; + } else { + free(ret); + return NULL; + } +} + +bool changeset_empty(const changeset_t *ch) +{ + if (ch == NULL) { + return true; + } + + if (zone_contents_is_empty(ch->remove) && + zone_contents_is_empty(ch->add)) { + if (ch->soa_to == NULL) { + return true; + } + if (ch->soa_from != NULL && ch->soa_to != NULL && + knot_rrset_equal(ch->soa_from, ch->soa_to, false)) { + return true; + } + } + + return false; +} + +size_t changeset_size(const changeset_t *ch) +{ + if (ch == NULL) { + return 0; + } + + changeset_iter_t itt; + changeset_iter_all(&itt, ch); + + size_t size = 0; + knot_rrset_t rr = changeset_iter_next(&itt); + while(!knot_rrset_empty(&rr)) { + ++size; + rr = changeset_iter_next(&itt); + } + changeset_iter_clear(&itt); + + if (!knot_rrset_empty(ch->soa_from)) { + size += 1; + } + if (!knot_rrset_empty(ch->soa_to)) { + size += 1; + } + + return size; +} + +int changeset_add_addition(changeset_t *ch, const knot_rrset_t *rrset, changeset_flag_t flags) +{ + if (!ch || !rrset) { + return KNOT_EINVAL; + } + + if (rrset->type == KNOT_RRTYPE_SOA) { + /* Do not add SOAs into actual contents. */ + return handle_soa(&ch->soa_to, rrset); + } + + knot_rrset_t *rrset_cancelout = NULL; + + /* Check if there's any removal and remove that, then add this + * addition anyway. Required to change TTLs. */ + if (flags & CHANGESET_CHECK) { + /* If we delete the rrset, we need to hold a copy to add it later */ + rrset = knot_rrset_copy(rrset, NULL); + if (rrset == NULL) { + return KNOT_ENOMEM; + } + + check_redundancy(ch->remove, rrset, &rrset_cancelout); + } + + const knot_rrset_t *to_add = (rrset_cancelout == NULL ? rrset : rrset_cancelout); + int ret = knot_rrset_empty(to_add) ? KNOT_EOK : add_rr_to_contents(ch->add, to_add); + + if (flags & CHANGESET_CHECK) { + knot_rrset_free((knot_rrset_t *)rrset, NULL); + } + knot_rrset_free(rrset_cancelout, NULL); + + return ret; +} + +int changeset_add_removal(changeset_t *ch, const knot_rrset_t *rrset, changeset_flag_t flags) +{ + if (!ch || !rrset) { + return KNOT_EINVAL; + } + + if (rrset->type == KNOT_RRTYPE_SOA) { + /* Do not add SOAs into actual contents. */ + return handle_soa(&ch->soa_from, rrset); + } + + knot_rrset_t *rrset_cancelout = NULL; + + /* Check if there's any addition and remove that, then add this + * removal anyway. */ + if (flags & CHANGESET_CHECK) { + /* If we delete the rrset, we need to hold a copy to add it later */ + rrset = knot_rrset_copy(rrset, NULL); + if (rrset == NULL) { + return KNOT_ENOMEM; + } + + check_redundancy(ch->add, rrset, &rrset_cancelout); + } + + const knot_rrset_t *to_remove = (rrset_cancelout == NULL ? rrset : rrset_cancelout); + int ret = (knot_rrset_empty(to_remove) || ch->remove == NULL) ? KNOT_EOK : add_rr_to_contents(ch->remove, to_remove); + + if (flags & CHANGESET_CHECK) { + knot_rrset_free((knot_rrset_t *)rrset, NULL); + } + knot_rrset_free(rrset_cancelout, NULL); + + return ret; +} + +int changeset_remove_addition(changeset_t *ch, const knot_rrset_t *rrset) +{ + if (rrset->type == KNOT_RRTYPE_SOA) { + /* Do not add SOAs into actual contents. */ + if (ch->soa_to != NULL) { + knot_rrset_free(ch->soa_to, NULL); + ch->soa_to = NULL; + } + return KNOT_EOK; + } + + zone_node_t *n = NULL; + return zone_contents_remove_rr(ch->add, rrset, &n); +} + +int changeset_remove_removal(changeset_t *ch, const knot_rrset_t *rrset) +{ + if (rrset->type == KNOT_RRTYPE_SOA) { + /* Do not add SOAs into actual contents. */ + if (ch->soa_from != NULL) { + knot_rrset_free(ch->soa_from, NULL); + ch->soa_from = NULL; + } + return KNOT_EOK; + } + + zone_node_t *n = NULL; + return zone_contents_remove_rr(ch->remove, rrset, &n); +} + +int changeset_merge(changeset_t *ch1, const changeset_t *ch2, int flags) +{ + changeset_iter_t itt; + changeset_iter_rem(&itt, ch2); + + knot_rrset_t rrset = changeset_iter_next(&itt); + while (!knot_rrset_empty(&rrset)) { + int ret = changeset_add_removal(ch1, &rrset, CHANGESET_CHECK | flags); + if (ret != KNOT_EOK) { + changeset_iter_clear(&itt); + return ret; + } + rrset = changeset_iter_next(&itt); + } + changeset_iter_clear(&itt); + + changeset_iter_add(&itt, ch2); + + rrset = changeset_iter_next(&itt); + while (!knot_rrset_empty(&rrset)) { + int ret = changeset_add_addition(ch1, &rrset, CHANGESET_CHECK | flags); + if (ret != KNOT_EOK) { + changeset_iter_clear(&itt); + return ret; + } + rrset = changeset_iter_next(&itt); + } + changeset_iter_clear(&itt); + + // Use soa_to and serial from the second changeset + // soa_to from the first changeset is redundant, delete it + if (ch2->soa_to == NULL && ch2->soa_from == NULL) { + // but not if ch2 has no soa change + return KNOT_EOK; + } + knot_rrset_t *soa_copy = knot_rrset_copy(ch2->soa_to, NULL); + if (soa_copy == NULL && ch2->soa_to) { + return KNOT_ENOMEM; + } + knot_rrset_free(ch1->soa_to, NULL); + ch1->soa_to = soa_copy; + + return KNOT_EOK; +} + +uint32_t changeset_from(const changeset_t *ch) +{ + return ch->soa_from == NULL ? 0 : knot_soa_serial(ch->soa_from->rrs.rdata); +} + +uint32_t changeset_to(const changeset_t *ch) +{ + return ch->soa_to == NULL ? 0 : knot_soa_serial(ch->soa_to->rrs.rdata); +} + +bool changeset_differs_just_serial(const changeset_t *ch, bool ignore_zonemd) +{ + if (ch == NULL || ch->soa_from == NULL || ch->soa_to == NULL) { + return false; + } + + knot_rrset_t *soa_to_cpy = knot_rrset_copy(ch->soa_to, NULL); + knot_soa_serial_set(soa_to_cpy->rrs.rdata, knot_soa_serial(ch->soa_from->rrs.rdata)); + + bool ret = knot_rrset_equal(ch->soa_from, soa_to_cpy, true); + knot_rrset_free(soa_to_cpy, NULL); + + changeset_iter_t itt; + changeset_iter_all(&itt, ch); + + knot_rrset_t rrset = changeset_iter_next(&itt); + while (!knot_rrset_empty(&rrset) && ret) { + switch (rrset.type) { + case KNOT_RRTYPE_ZONEMD: + ret = ignore_zonemd; + break; + case KNOT_RRTYPE_RRSIG: + ; uint16_t covered = knot_rrsig_type_covered(rrset.rrs.rdata); + if (covered == KNOT_RRTYPE_SOA || + (covered == KNOT_RRTYPE_ZONEMD && ignore_zonemd)) { + break; + } + // FALLTHROUGH + default: + ret = false; + break; + } + rrset = changeset_iter_next(&itt); + } + changeset_iter_clear(&itt); + + return ret; +} + +void changesets_clear(list_t *chgs) +{ + if (chgs) { + changeset_t *chg, *nxt; + WALK_LIST_DELSAFE(chg, nxt, *chgs) { + changeset_clear(chg); + rem_node(&chg->n); + } + init_list(chgs); + } +} + +void changesets_free(list_t *chgs) +{ + if (chgs) { + changeset_t *chg, *nxt; + WALK_LIST_DELSAFE(chg, nxt, *chgs) { + rem_node(&chg->n); + changeset_free(chg); + } + init_list(chgs); + } +} + +void changeset_clear(changeset_t *ch) +{ + if (ch == NULL) { + return; + } + + // Delete RRSets in lists, in case there are any left + zone_contents_deep_free(ch->add); + zone_contents_deep_free(ch->remove); + ch->add = NULL; + ch->remove = NULL; + + knot_rrset_free(ch->soa_from, NULL); + knot_rrset_free(ch->soa_to, NULL); + ch->soa_from = NULL; + ch->soa_to = NULL; + + // Delete binary data + free(ch->data); +} + +changeset_t *changeset_clone(const changeset_t *ch) +{ + if (ch == NULL) { + return NULL; + } + + changeset_t *res = changeset_new(ch->add->apex->owner); + if (res == NULL) { + return NULL; + } + + res->soa_from = knot_rrset_copy(ch->soa_from, NULL); + res->soa_to = knot_rrset_copy(ch->soa_to, NULL); + + int ret = KNOT_EOK; + changeset_iter_t itt; + + changeset_iter_rem(&itt, ch); + knot_rrset_t rr = changeset_iter_next(&itt); + while (!knot_rrset_empty(&rr) && ret == KNOT_EOK) { + ret = changeset_add_removal(res, &rr, 0); + rr = changeset_iter_next(&itt); + } + changeset_iter_clear(&itt); + + changeset_iter_add(&itt, ch); + rr = changeset_iter_next(&itt); + while (!knot_rrset_empty(&rr) && ret == KNOT_EOK) { + ret = changeset_add_addition(res, &rr, 0); + rr = changeset_iter_next(&itt); + } + changeset_iter_clear(&itt); + + if ((ch->soa_from != NULL && res->soa_from == NULL) || + (ch->soa_to != NULL && res->soa_to == NULL) || + ret != KNOT_EOK) { + changeset_free(res); + return NULL; + } + + return res; +} + +void changeset_free(changeset_t *ch) +{ + changeset_clear(ch); + free(ch); +} + +int changeset_iter_add(changeset_iter_t *itt, const changeset_t *ch) +{ + return changeset_iter_init(itt, 2, ch->add->nodes, ch->add->nsec3_nodes); +} + +int changeset_iter_rem(changeset_iter_t *itt, const changeset_t *ch) +{ + return changeset_iter_init(itt, 2, ch->remove->nodes, ch->remove->nsec3_nodes); +} + +int changeset_iter_all(changeset_iter_t *itt, const changeset_t *ch) +{ + return changeset_iter_init(itt, 4, ch->add->nodes, ch->add->nsec3_nodes, + ch->remove->nodes, ch->remove->nsec3_nodes); +} + +knot_rrset_t changeset_iter_next(changeset_iter_t *it) +{ + assert(it); + + knot_rrset_t rr; + while (it->node == NULL || it->node_pos >= it->node->rrset_count) { + if (it->node != NULL) { + zone_tree_it_next(&it->it); + } + while (zone_tree_it_finished(&it->it)) { + zone_tree_it_free(&it->it); + if (--it->n_trees > 0) { + for (size_t i = 0; i < it->n_trees; i++) { + it->trees[i] = it->trees[i + 1]; + } + (void)zone_tree_it_begin(it->trees[0], &it->it); + } else { + knot_rrset_init_empty(&rr); + return rr; + } + } + it->node = zone_tree_it_val(&it->it); + it->node_pos = 0; + } + rr = node_rrset_at(it->node, it->node_pos++); + assert(!knot_rrset_empty(&rr)); + return rr; +} + +void changeset_iter_clear(changeset_iter_t *it) +{ + if (it) { + zone_tree_it_free(&it->it); + it->node = NULL; + it->node_pos = 0; + } +} + +int changeset_walk(const changeset_t *changeset, changeset_walk_callback callback, void *ctx) +{ + changeset_iter_t it; + int ret = changeset_iter_rem(&it, changeset); + if (ret != KNOT_EOK) { + return ret; + } + + knot_rrset_t rrset = changeset_iter_next(&it); + while (!knot_rrset_empty(&rrset)) { + ret = callback(&rrset, false, ctx); + if (ret != KNOT_EOK) { + changeset_iter_clear(&it); + return ret; + } + rrset = changeset_iter_next(&it); + } + changeset_iter_clear(&it); + + if (changeset->soa_from != NULL) { + ret = callback(changeset->soa_from, false, ctx); + if (ret != KNOT_EOK) { + return ret; + } + } + + ret = changeset_iter_add(&it, changeset); + if (ret != KNOT_EOK) { + return ret; + } + + rrset = changeset_iter_next(&it); + while (!knot_rrset_empty(&rrset)) { + ret = callback(&rrset, true, ctx); + if (ret != KNOT_EOK) { + changeset_iter_clear(&it); + return ret; + } + rrset = changeset_iter_next(&it); + } + changeset_iter_clear(&it); + + if (changeset->soa_to != NULL) { + ret = callback(changeset->soa_to, true, ctx); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +void changeset_print(const changeset_t *changeset, FILE *outfile, bool color) +{ + size_t buflen = 1024; + char *buff = malloc(buflen); + + knot_dump_style_t style = KNOT_DUMP_STYLE_DEFAULT; + style.now = knot_time(); + + style.color = COL_RED(color); + if (changeset->soa_from != NULL || !zone_contents_is_empty(changeset->remove)) { + fprintf(outfile, "%s;; Removed%s\n", style.color, COL_RST(color)); + } + if (changeset->soa_from != NULL && buff != NULL) { + (void)knot_rrset_txt_dump(changeset->soa_from, &buff, &buflen, &style); + fprintf(outfile, "%s%s%s", style.color, buff, COL_RST(color)); + } + (void)zone_dump_text(changeset->remove, outfile, false, style.color); + + style.color = COL_GRN(color); + if (changeset->soa_to != NULL || !zone_contents_is_empty(changeset->add)) { + fprintf(outfile, "%s;; Added%s\n", style.color, COL_RST(color)); + } + if (changeset->soa_to != NULL && buff != NULL) { + (void)knot_rrset_txt_dump(changeset->soa_to, &buff, &buflen, &style); + fprintf(outfile, "%s%s%s", style.color, buff, COL_RST(color)); + } + (void)zone_dump_text(changeset->add, outfile, false, style.color); + + free(buff); +} diff --git a/src/knot/updates/changesets.h b/src/knot/updates/changesets.h new file mode 100644 index 0000000..1234cb9 --- /dev/null +++ b/src/knot/updates/changesets.h @@ -0,0 +1,290 @@ +/* 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/>. + */ + +#pragma once + +#include <stdio.h> + +#include "libknot/rrset.h" +#include "knot/zone/contents.h" +#include "contrib/ucw/lists.h" + +/*! \brief Changeset addition/removal flags */ +typedef enum { + CHANGESET_NONE = 0, + CHANGESET_CHECK = 1 << 0, /*! Perform redundancy check on additions/removals */ +} changeset_flag_t; + +/*! \brief One zone change, from 'soa_from' to 'soa_to'. */ +typedef struct { + node_t n; /*!< List node. */ + knot_rrset_t *soa_from; /*!< Start SOA. */ + knot_rrset_t *soa_to; /*!< Destination SOA. */ + zone_contents_t *add; /*!< Change additions. */ + zone_contents_t *remove; /*!< Change removals. */ + size_t size; /*!< Size of serialized changeset. \todo Remove after old_journal removal! */ + uint8_t *data; /*!< Serialized changeset. */ +} changeset_t; + +/*! \brief Changeset iteration structure. */ +typedef struct { + list_t iters; /*!< List of pending zone iterators. */ + zone_tree_t *trees[4]; /*!< Pointers to zone trees to iterate over. */ + size_t n_trees; /*!< Their count. */ + zone_tree_it_t it; /*!< Zone tree iterator. */ + const zone_node_t *node; /*!< Current zone node. */ + uint16_t node_pos; /*!< Position in node. */ +} changeset_iter_t; + +/*! + * \brief Inits changeset structure. + * + * \param ch Changeset to init. + * \param apex Zone apex DNAME. + * + * \return KNOT_E* + */ +int changeset_init(changeset_t *ch, const knot_dname_t *apex); + +/*! + * \brief Creates new changeset structure and inits it. + * + * \param apex Zone apex DNAME. + * + * \return Changeset structure on success, NULL on errors. + */ +changeset_t *changeset_new(const knot_dname_t *apex); + +/*! + * \brief Checks whether changeset is empty, i.e. no change will happen after its application. + * + * \param ch Changeset to be checked. + * + * \retval true if changeset is empty. + * \retval false if changeset is not empty. + */ +bool changeset_empty(const changeset_t *ch); + +/*! + * \brief Get number of changes (additions and removals) in the changeset. + * + * \param ch Changeset to be checked. + * + * \return Number of changes in the changeset. + */ +size_t changeset_size(const changeset_t *ch); + +/*! + * \brief Add RRSet to 'add' part of changeset. + * + * \param ch Changeset to add RRSet into. + * \param rrset RRSet to be added. + * \param flags Changeset flags. + * + * \return KNOT_E* + */ +int changeset_add_addition(changeset_t *ch, const knot_rrset_t *rrset, changeset_flag_t flags); + +/*! + * \brief Add RRSet to 'remove' part of changeset. + * + * \param ch Changeset to add RRSet into. + * \param rrset RRSet to be added. + * \param flags Changeset flags. + * + * \return KNOT_E* + */ +int changeset_add_removal(changeset_t *ch, const knot_rrset_t *rrset, changeset_flag_t flags); + + +/*! + * \brief Remove an RRSet from the 'add' part of changeset. + * + * \param ch Changeset to add RRSet into. + * \param rrset RRSet to be added. + * + * \return KNOT_E* + */ +int changeset_remove_addition(changeset_t *ch, const knot_rrset_t *rrset); + +/*! + * \brief Remove an RRSet from the 'remove' part of changeset. + * + * \param ch Changeset to add RRSet into. + * \param rrset RRSet to be added. + * + * \return KNOT_E* + */ +int changeset_remove_removal(changeset_t *ch, const knot_rrset_t *rrset); + +/*! + * \brief Merges two changesets together. + * + * \param ch1 Merge into this changeset. + * \param ch2 Merge this changeset. + * \param flags Flags how to handle redundancies. + * + * \return KNOT_E* + */ +int changeset_merge(changeset_t *ch1, const changeset_t *ch2, int flags); + +/*! + * \brief Get serial "from" of the changeset. + * + * \param ch Changeset in question. + * + * \return Its serial "from", or 0 if none. + */ +uint32_t changeset_from(const changeset_t *ch); + +/*! + * \brief Get serial "to" of the changeset. + * + * \param ch Changeset in question. + * + * \return Its serial "to", or 0 if none. + */ +uint32_t changeset_to(const changeset_t *ch); + +/*! + * \brief Check the changes and SOA, ignoring possibly updated SOA serial and ZONEMD. + * + * \note Also tolerates changed RRSIG of SOA or ZONEMD. + * + * \param ch Changeset in question. + * \param ignore_zonemd If enabled, possible ZONEMD records are ignored. + * + * \retval false If the changeset changes other records than SOA, or some SOA field + * other than serial changed or optionally ZONEMD. + * \retval true Otherwise. + */ +bool changeset_differs_just_serial(const changeset_t *ch, bool ignore_zonemd); + +/*! + * \brief Clears changesets in list. Changesets are not free'd. Legacy. + * + * \param chgs Changeset list to clear. + */ +void changesets_clear(list_t *chgs); + +/*! + * \brief Free changesets in list. Legacy. + * + * \param chgs Changeset list to free. + */ +void changesets_free(list_t *chgs); + +/*! + * \brief Clear single changeset. + * + * \param ch Changeset to clear. + */ +void changeset_clear(changeset_t *ch); + +/*! + * \brief Copy changeset to newly allocated space, all rrsigs are copied. + * + * \param ch Changeset to be copied. + * + * \return a copy, or NULL if error. + */ +changeset_t *changeset_clone(const changeset_t *ch); + +/*! + * \brief Frees single changeset. + * + * \param ch Changeset to free. + */ +void changeset_free(changeset_t *ch); + +/*! + * \brief Inits changeset iteration structure with changeset additions. + * + * \param itt Iterator to init. + * \param ch Changeset to use. + * + * \return KNOT_E* + */ +int changeset_iter_add(changeset_iter_t *itt, const changeset_t *ch); + +/*! + * \brief Inits changeset iteration structure with changeset removals. + * + * \param itt Iterator to init. + * \param ch Changeset to use. + * + * \return KNOT_E* + */ +int changeset_iter_rem(changeset_iter_t *itt, const changeset_t *ch); + +/*! + * \brief Inits changeset iteration structure with changeset additions and removals. + * + * \param itt Iterator to init. + * \param ch Changeset to use. + * + * \return KNOT_E* + */ +int changeset_iter_all(changeset_iter_t *itt, const changeset_t *ch); + +/*! + * \brief Gets next RRSet from changeset iterator. + * + * \param it Changeset iterator. + * + * \return Next RRSet in iterator, empty RRSet if iteration done. + */ +knot_rrset_t changeset_iter_next(changeset_iter_t *it); + +/*! + * \brief Free resources allocated by changeset iterator. + * + * \param it Iterator to clear. + */ +void changeset_iter_clear(changeset_iter_t *it); + +/*! + * \brief A pointer type for callback for changeset_walk() function. + * + * \param rrset An actual removal/addition inside the changeset. + * \param addition Indicates addition against removal. + * \param ctx A context passed to the changeset_walk() function. + * + * \retval KNOT_EOK if all ok, iteration will continue + * \return KNOT_E* if error, iteration will stop immediately and changeset_walk() returns this error. + */ +typedef int (*changeset_walk_callback)(const knot_rrset_t *rrset, bool addition, void *ctx); + +/*! + * \brief Calls a callback for each removal/addition in the changeset. + * + * \param changeset Changeset. + * \param callback Callback. + * \param ctx Arbitrary context passed to the callback. + * + * \return KNOT_E* + */ +int changeset_walk(const changeset_t *changeset, changeset_walk_callback callback, void *ctx); + +/*! + * + * \brief Dumps the changeset into text file. + * + * \param changeset Changeset. + * \param outfile File to write into. + * \param color Use unix tty color metacharacters. + */ +void changeset_print(const changeset_t *changeset, FILE *outfile, bool color); diff --git a/src/knot/updates/ddns.c b/src/knot/updates/ddns.c new file mode 100644 index 0000000..eb75317 --- /dev/null +++ b/src/knot/updates/ddns.c @@ -0,0 +1,701 @@ +/* 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 "knot/common/log.h" +#include "knot/updates/ddns.h" +#include "knot/updates/changesets.h" +#include "knot/updates/zone-update.h" +#include "knot/zone/serial.h" +#include "libknot/libknot.h" +#include "contrib/ucw/lists.h" + +/*!< \brief Clears prereq RRSet list. */ +static void rrset_list_clear(list_t *l) +{ + node_t *n, *nxt; + WALK_LIST_DELSAFE(n, nxt, *l) { + ptrnode_t *ptr_n = (ptrnode_t *)n; + knot_rrset_t *rrset = (knot_rrset_t *)ptr_n->d; + knot_rrset_free(rrset, NULL); + free(n); + }; +} + +/*!< \brief Adds RR to prereq RRSet list, merges RRs into RRSets. */ +static int add_rr_to_list(list_t *l, const knot_rrset_t *rr) +{ + node_t *n; + WALK_LIST(n, *l) { + ptrnode_t *ptr_n = (ptrnode_t *)n; + knot_rrset_t *rrset = (knot_rrset_t *)ptr_n->d; + if (rrset->type == rr->type && knot_dname_is_equal(rrset->owner, rr->owner)) { + return knot_rdataset_merge(&rrset->rrs, &rr->rrs, NULL); + } + }; + + knot_rrset_t *rr_copy = knot_rrset_copy(rr, NULL); + if (rr_copy == NULL) { + return KNOT_ENOMEM; + } + return ptrlist_add(l, rr_copy, NULL) != NULL ? KNOT_EOK : KNOT_ENOMEM; +} + +/*!< \brief Checks whether RRSet exists in the zone. */ +static int check_rrset_exists(zone_update_t *update, const knot_rrset_t *rrset, + uint16_t *rcode) +{ + assert(rrset->type != KNOT_RRTYPE_ANY); + + const zone_node_t *node = zone_update_get_node(update, rrset->owner); + if (node == NULL || !node_rrtype_exists(node, rrset->type)) { + *rcode = KNOT_RCODE_NXRRSET; + return KNOT_EPREREQ; + } else { + knot_rrset_t found = node_rrset(node, rrset->type); + assert(!knot_rrset_empty(&found)); + if (knot_rrset_equal(&found, rrset, false)) { + return KNOT_EOK; + } else { + *rcode = KNOT_RCODE_NXRRSET; + return KNOT_EPREREQ; + } + } +} + +/*!< \brief Checks whether RRSets in the list exist in the zone. */ +static int check_stored_rrsets(list_t *l, zone_update_t *update, + uint16_t *rcode) +{ + node_t *n; + WALK_LIST(n, *l) { + ptrnode_t *ptr_n = (ptrnode_t *)n; + knot_rrset_t *rrset = (knot_rrset_t *)ptr_n->d; + int ret = check_rrset_exists(update, rrset, rcode); + if (ret != KNOT_EOK) { + return ret; + } + }; + + return KNOT_EOK; +} + +/*!< \brief Checks whether node of given owner, with given type exists. */ +static bool check_type(zone_update_t *update, const knot_rrset_t *rrset) +{ + assert(rrset->type != KNOT_RRTYPE_ANY); + const zone_node_t *node = zone_update_get_node(update, rrset->owner); + if (node == NULL || !node_rrtype_exists(node, rrset->type)) { + return false; + } + + return true; +} + +/*!< \brief Checks whether RR type exists in the zone. */ +static int check_type_exist(zone_update_t *update, + const knot_rrset_t *rrset, uint16_t *rcode) +{ + assert(rrset->rclass == KNOT_CLASS_ANY); + if (check_type(update, rrset)) { + return KNOT_EOK; + } else { + *rcode = KNOT_RCODE_NXRRSET; + return KNOT_EPREREQ; + } +} + +/*!< \brief Checks whether RR type is not in the zone. */ +static int check_type_not_exist(zone_update_t *update, + const knot_rrset_t *rrset, uint16_t *rcode) +{ + assert(rrset->rclass == KNOT_CLASS_NONE); + if (check_type(update, rrset)) { + *rcode = KNOT_RCODE_YXRRSET; + return KNOT_EPREREQ; + } else { + return KNOT_EOK; + } +} + +/*!< \brief Checks whether DNAME is in the zone. */ +static int check_in_use(zone_update_t *update, + const knot_dname_t *dname, uint16_t *rcode) +{ + const zone_node_t *node = zone_update_get_node(update, dname); + if (node == NULL || node->rrset_count == 0) { + *rcode = KNOT_RCODE_NXDOMAIN; + return KNOT_EPREREQ; + } else { + return KNOT_EOK; + } +} + +/*!< \brief Checks whether DNAME is not in the zone. */ +static int check_not_in_use(zone_update_t *update, + const knot_dname_t *dname, uint16_t *rcode) +{ + const zone_node_t *node = zone_update_get_node(update, dname); + if (node == NULL || node->rrset_count == 0) { + return KNOT_EOK; + } else { + *rcode = KNOT_RCODE_YXDOMAIN; + return KNOT_EPREREQ; + } +} + +/*!< \brief Returns true if rrset has 0 data or RDATA of size 0 (we need TTL). */ +static bool rrset_empty(const knot_rrset_t *rrset) +{ + switch (rrset->rrs.count) { + case 0: + return true; + case 1: + return rrset->rrs.rdata->len == 0; + default: + return false; + } +} + +/*!< \brief Checks prereq for given packet RR. */ +static int process_prereq(const knot_rrset_t *rrset, uint16_t qclass, + zone_update_t *update, uint16_t *rcode, + list_t *rrset_list) +{ + if (rrset->ttl != 0) { + *rcode = KNOT_RCODE_FORMERR; + return KNOT_EMALF; + } + + if (knot_dname_in_bailiwick(rrset->owner, update->zone->name) < 0) { + *rcode = KNOT_RCODE_NOTZONE; + return KNOT_EOUTOFZONE; + } + + if (rrset->rclass == KNOT_CLASS_ANY) { + if (!rrset_empty(rrset)) { + *rcode = KNOT_RCODE_FORMERR; + return KNOT_EMALF; + } + if (rrset->type == KNOT_RRTYPE_ANY) { + return check_in_use(update, rrset->owner, rcode); + } else { + return check_type_exist(update, rrset, rcode); + } + } else if (rrset->rclass == KNOT_CLASS_NONE) { + if (!rrset_empty(rrset)) { + *rcode = KNOT_RCODE_FORMERR; + return KNOT_EMALF; + } + if (rrset->type == KNOT_RRTYPE_ANY) { + return check_not_in_use(update, rrset->owner, rcode); + } else { + return check_type_not_exist(update, rrset, rcode); + } + } else if (rrset->rclass == qclass) { + // Store RRs for full check into list + int ret = add_rr_to_list(rrset_list, rrset); + if (ret != KNOT_EOK) { + *rcode = KNOT_RCODE_SERVFAIL; + } + return ret; + } else { + *rcode = KNOT_RCODE_FORMERR; + return KNOT_EMALF; + } +} + +static inline bool is_addition(const knot_rrset_t *rr) +{ + return rr->rclass == KNOT_CLASS_IN; +} + +static inline bool is_removal(const knot_rrset_t *rr) +{ + return rr->rclass == KNOT_CLASS_NONE || rr->rclass == KNOT_CLASS_ANY; +} + +static inline bool is_rr_removal(const knot_rrset_t *rr) +{ + return rr->rclass == KNOT_CLASS_NONE; +} + +static inline bool is_rrset_removal(const knot_rrset_t *rr) +{ + return rr->rclass == KNOT_CLASS_ANY && rr->type != KNOT_RRTYPE_ANY; +} + +static inline bool is_node_removal(const knot_rrset_t *rr) +{ + return rr->rclass == KNOT_CLASS_ANY && rr->type == KNOT_RRTYPE_ANY; +} + +/*!< \brief Returns true if last addition of certain types is to be replaced. */ +static bool should_replace(const knot_rrset_t *rrset) +{ + return rrset->type == KNOT_RRTYPE_CNAME || + rrset->type == KNOT_RRTYPE_DNAME || + rrset->type == KNOT_RRTYPE_NSEC3PARAM; +} + +/*!< \brief Returns true if node contains given RR in its RRSets. */ +static bool node_contains_rr(const zone_node_t *node, + const knot_rrset_t *rrset) +{ + const knot_rdataset_t *zone_rrs = node_rdataset(node, rrset->type); + if (zone_rrs != NULL) { + assert(rrset->rrs.count == 1); + return knot_rdataset_member(zone_rrs, rrset->rrs.rdata); + } else { + return false; + } +} + +/*!< \brief Returns true if CNAME is in this node. */ +static bool adding_to_cname(const knot_dname_t *owner, + const zone_node_t *node) +{ + if (node == NULL) { + // Node did not exist before update. + return false; + } + + knot_rrset_t cname = node_rrset(node, KNOT_RRTYPE_CNAME); + if (knot_rrset_empty(&cname)) { + // Node did not contain CNAME before update. + return false; + } + + // CNAME present + return true; +} + +/*!< \brief Used to ignore SOA deletions and SOAs with lower serial than zone. */ +static bool skip_soa(const knot_rrset_t *rr, int64_t sn) +{ + if (rr->type == KNOT_RRTYPE_SOA && + (rr->rclass == KNOT_CLASS_NONE || rr->rclass == KNOT_CLASS_ANY || + (serial_compare(knot_soa_serial(rr->rrs.rdata), sn) != SERIAL_GREATER))) { + return true; + } + + return false; +} + +/*!< \brief Replaces possible singleton RR type in changeset. */ +static bool singleton_replaced(zone_update_t *update, const knot_rrset_t *rr) +{ + if (!should_replace(rr)) { + return false; + } + + return zone_update_remove_rrset(update, rr->owner, rr->type) == KNOT_EOK; +} + +/*!< \brief Adds RR into add section of changeset if it is deemed worthy. */ +static int add_rr_to_changeset(const knot_rrset_t *rr, zone_update_t *update) +{ + if (singleton_replaced(update, rr)) { + return KNOT_EOK; + } + + return zone_update_add(update, rr); +} + +/*!< \brief Processes CNAME addition (replace or ignore) */ +static int process_add_cname(const zone_node_t *node, + const knot_rrset_t *rr, + zone_update_t *update) +{ + knot_rrset_t cname = node_rrset(node, KNOT_RRTYPE_CNAME); + if (!knot_rrset_empty(&cname)) { + // If they are identical, ignore. + if (knot_rrset_equal(&cname, rr, true)) { + return KNOT_EOK; + } + + int ret = zone_update_remove(update, &cname); + if (ret != KNOT_EOK) { + return ret; + } + + return add_rr_to_changeset(rr, update); + } else if (!node_empty(node)) { + // Other occupied node => ignore. + return KNOT_EOK; + } else { + // Can add. + return add_rr_to_changeset(rr, update); + } +} + +/*!< \brief Processes NSEC3PARAM addition (ignore when not removed, or non-apex) */ +static int process_add_nsec3param(const zone_node_t *node, + const knot_rrset_t *rr, + zone_update_t *update) +{ + if (node == NULL || !node_rrtype_exists(node, KNOT_RRTYPE_SOA)) { + // Ignore non-apex additions + char *owner = knot_dname_to_str_alloc(rr->owner); + log_warning("DDNS, refusing to add NSEC3PARAM to non-apex " + "node '%s'", owner); + free(owner); + return KNOT_EDENIED; + } + knot_rrset_t param = node_rrset(node, KNOT_RRTYPE_NSEC3PARAM); + if (knot_rrset_empty(¶m)) { + return add_rr_to_changeset(rr, update); + } + + char *owner = knot_dname_to_str_alloc(rr->owner); + log_warning("DDNS, refusing to add second NSEC3PARAM to node '%s'", owner); + free(owner); + + return KNOT_EOK; +} + +/*! + * \brief Processes SOA addition (ignore when non-apex), lower serials + * dropped before. + */ +static int process_add_soa(const zone_node_t *node, + const knot_rrset_t *rr, + zone_update_t *update) +{ + if (node == NULL || !node_rrtype_exists(node, KNOT_RRTYPE_SOA)) { + // Adding SOA to non-apex node, ignore. + return KNOT_EOK; + } + + // Get current SOA RR. + knot_rrset_t removed = node_rrset(node, KNOT_RRTYPE_SOA); + if (knot_rrset_equal(&removed, rr, true)) { + // If they are identical, ignore. + return KNOT_EOK; + } + + return add_rr_to_changeset(rr, update); +} + +/*!< \brief Adds normal RR, ignores when CNAME exists in node. */ +static int process_add_normal(const zone_node_t *node, + const knot_rrset_t *rr, + zone_update_t *update) +{ + if (adding_to_cname(rr->owner, node)) { + // Adding RR to CNAME node, ignore. + return KNOT_EOK; + } + + if (node && node_contains_rr(node, rr)) { + // Adding existing RR, ignore. + return KNOT_EOK; + } + + return add_rr_to_changeset(rr, update); +} + +/*!< \brief Decides what to do with RR addition. */ +static int process_add(const knot_rrset_t *rr, + const zone_node_t *node, + zone_update_t *update) +{ + switch(rr->type) { + case KNOT_RRTYPE_CNAME: + return process_add_cname(node, rr, update); + case KNOT_RRTYPE_SOA: + return process_add_soa(node, rr, update); + case KNOT_RRTYPE_NSEC3PARAM: + return process_add_nsec3param(node, rr, update); + default: + return process_add_normal(node, rr, update); + } +} + +/*!< \brief Removes single RR from zone. */ +static int process_rem_rr(const knot_rrset_t *rr, + const zone_node_t *node, + zone_update_t *update) +{ + if (node == NULL) { + // Removing from node that does not exist + return KNOT_EOK; + } + + const bool apex_ns = node_rrtype_exists(node, KNOT_RRTYPE_SOA) && + rr->type == KNOT_RRTYPE_NS; + if (apex_ns) { + const knot_rdataset_t *ns_rrs = + node_rdataset(node, KNOT_RRTYPE_NS); + if (ns_rrs == NULL) { + // Zone without apex NS. + return KNOT_EOK; + } + if (ns_rrs->count == 1) { + // Cannot remove last apex NS RR. + return KNOT_EOK; + } + } + + knot_rrset_t to_modify = node_rrset(node, rr->type); + if (knot_rrset_empty(&to_modify)) { + // No such RRSet + return KNOT_EOK; + } + + knot_rdataset_t *rrs = node_rdataset(node, rr->type); + if (!knot_rdataset_member(rrs, rr->rrs.rdata)) { + // Node does not contain this RR + return KNOT_EOK; + } + + knot_rrset_t rr_ttl = *rr; + rr_ttl.ttl = to_modify.ttl; + + return zone_update_remove(update, &rr_ttl); +} + +/*!< \brief Removes RRSet from zone. */ +static int process_rem_rrset(const knot_rrset_t *rrset, + const zone_node_t *node, + zone_update_t *update) +{ + bool is_apex = node_rrtype_exists(node, KNOT_RRTYPE_SOA); + + if (is_apex && rrset->type == KNOT_RRTYPE_NS) { + // Ignore NS apex RRSet removals. + return KNOT_EOK; + } + + if (node == NULL) { + // no such node in zone, ignore + return KNOT_EOK; + } + + if (!node_rrtype_exists(node, rrset->type)) { + // no such RR, ignore + return KNOT_EOK; + } + + knot_rrset_t to_remove = node_rrset(node, rrset->type); + return zone_update_remove(update, &to_remove); +} + +/*!< \brief Removes node from zone. */ +static int process_rem_node(const knot_rrset_t *rr, + const zone_node_t *node, zone_update_t *update) +{ + if (node == NULL) { + return KNOT_EOK; + } + + // Remove all RRSets from node + size_t rrset_count = node->rrset_count; + for (int i = 0; i < rrset_count; ++i) { + knot_rrset_t rrset = node_rrset_at(node, rrset_count - i - 1); + int ret = process_rem_rrset(&rrset, node, update); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +/*!< \brief Decides what to with removal. */ +static int process_remove(const knot_rrset_t *rr, + const zone_node_t *node, + zone_update_t *update) +{ + if (is_rr_removal(rr)) { + return process_rem_rr(rr, node, update); + } else if (is_rrset_removal(rr)) { + return process_rem_rrset(rr, node, update); + } else if (is_node_removal(rr)) { + return process_rem_node(rr, node, update); + } else { + return KNOT_EINVAL; + } +} + +/*!< \brief Checks whether addition has not violated DNAME rules. */ +static bool sem_check(const knot_rrset_t *rr, const zone_node_t *zone_node, + zone_update_t *update) +{ + const zone_node_t *added_node = zone_contents_find_node(update->new_cont, rr->owner); + + // we do this sem check AFTER adding the RR, so the node must exist + assert(added_node != NULL); + + for (const zone_node_t *parent = added_node->parent; + parent != NULL; parent = parent->parent) { + if (node_rrtype_exists(parent, KNOT_RRTYPE_DNAME)) { + // Parent has DNAME RRSet, refuse update + return false; + } + } + + if (rr->type != KNOT_RRTYPE_DNAME || zone_node == NULL) { + return true; + } + + // Check that we have not created node with DNAME children. + if (zone_node->children > 0) { + // Updated node has children and DNAME was added, refuse update + return false; + } + + return true; +} + +/*!< \brief Checks whether we can accept this RR. */ +static int check_update(const knot_rrset_t *rrset, const knot_pkt_t *query, + uint16_t *rcode) +{ + /* Accept both subdomain and dname match. */ + const knot_dname_t *owner = rrset->owner; + const knot_dname_t *qname = knot_pkt_qname(query); + const int in_bailiwick = knot_dname_in_bailiwick(owner, qname); + if (in_bailiwick < 0) { + *rcode = KNOT_RCODE_NOTZONE; + return KNOT_EOUTOFZONE; + } + + if (rrset->rclass == knot_pkt_qclass(query)) { + if (knot_rrtype_is_metatype(rrset->type)) { + *rcode = KNOT_RCODE_FORMERR; + return KNOT_EMALF; + } + } else if (rrset->rclass == KNOT_CLASS_ANY) { + if (!rrset_empty(rrset) || + (knot_rrtype_is_metatype(rrset->type) && + rrset->type != KNOT_RRTYPE_ANY)) { + *rcode = KNOT_RCODE_FORMERR; + return KNOT_EMALF; + } + } else if (rrset->rclass == KNOT_CLASS_NONE) { + if (rrset->ttl != 0 || knot_rrtype_is_metatype(rrset->type)) { + *rcode = KNOT_RCODE_FORMERR; + return KNOT_EMALF; + } + } else { + *rcode = KNOT_RCODE_FORMERR; + return KNOT_EMALF; + } + + return KNOT_EOK; +} + +/*!< \brief Checks RR and decides what to do with it. */ +static int process_rr(const knot_rrset_t *rr, zone_update_t *update) +{ + const zone_node_t *node = zone_update_get_node(update, rr->owner); + + if (is_addition(rr)) { + int ret = process_add(rr, node, update); + if (ret == KNOT_EOK) { + if (!sem_check(rr, node, update)) { + return KNOT_EDENIED; + } + } + return ret; + } else if (is_removal(rr)) { + return process_remove(rr, node, update); + } else { + return KNOT_EMALF; + } +} + +/*!< \brief Maps Knot return code to RCODE. */ +static uint16_t ret_to_rcode(int ret) +{ + if (ret == KNOT_EMALF) { + return KNOT_RCODE_FORMERR; + } else if (ret == KNOT_EDENIED) { + return KNOT_RCODE_REFUSED; + } else { + return KNOT_RCODE_SERVFAIL; + } +} + +int ddns_process_prereqs(const knot_pkt_t *query, zone_update_t *update, + uint16_t *rcode) +{ + if (query == NULL || rcode == NULL || update == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + list_t rrset_list; // List used to store merged RRSets + init_list(&rrset_list); + + const knot_pktsection_t *answer = knot_pkt_section(query, KNOT_ANSWER); + const knot_rrset_t *answer_rr = (answer->count > 0) ? knot_pkt_rr(answer, 0) : NULL; + for (int i = 0; i < answer->count; ++i) { + // Check what can be checked, store full RRs into list + ret = process_prereq(&answer_rr[i], knot_pkt_qclass(query), + update, rcode, &rrset_list); + if (ret != KNOT_EOK) { + rrset_list_clear(&rrset_list); + return ret; + } + } + + // Check stored RRSets + ret = check_stored_rrsets(&rrset_list, update, rcode); + rrset_list_clear(&rrset_list); + return ret; +} + +int ddns_process_update(const zone_t *zone, const knot_pkt_t *query, + zone_update_t *update, uint16_t *rcode) +{ + if (zone == NULL || query == NULL || update == NULL || rcode == NULL) { + if (rcode) { + *rcode = ret_to_rcode(KNOT_EINVAL); + } + return KNOT_EINVAL; + } + + uint32_t sn_old = knot_soa_serial(zone_update_from(update)->rdata); + + // Process all RRs in the authority section. + const knot_pktsection_t *authority = knot_pkt_section(query, KNOT_AUTHORITY); + const knot_rrset_t *authority_rr = (authority->count > 0) ? knot_pkt_rr(authority, 0) : NULL; + for (uint16_t i = 0; i < authority->count; ++i) { + const knot_rrset_t *rr = &authority_rr[i]; + // Check if RR is correct. + int ret = check_update(rr, query, rcode); + if (ret != KNOT_EOK) { + assert(*rcode != KNOT_RCODE_NOERROR); + return ret; + } + + if (skip_soa(rr, sn_old)) { + continue; + } + + ret = process_rr(rr, update); + if (ret != KNOT_EOK) { + *rcode = ret_to_rcode(ret); + return ret; + } + } + + *rcode = KNOT_RCODE_NOERROR; + return KNOT_EOK; +} diff --git a/src/knot/updates/ddns.h b/src/knot/updates/ddns.h new file mode 100644 index 0000000..1d79218 --- /dev/null +++ b/src/knot/updates/ddns.h @@ -0,0 +1,47 @@ +/* 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/updates/zone-update.h" +#include "knot/zone/zone.h" +#include "libknot/packet/pkt.h" + +/*! + * \brief Checks update prerequisite section. + * + * \param query DNS message containing the update. + * \param update Zone to be checked. + * \param rcode Returned DNS RCODE. + * + * \return KNOT_E* + */ +int ddns_process_prereqs(const knot_pkt_t *query, zone_update_t *update, + uint16_t *rcode); + +/*! + * \brief Processes DNS update and creates a changeset out of it. Zone is left + * intact. + * + * \param zone Zone to be updated. + * \param query DNS message containing the update. + * \param update Output changeset. + * \param rcode Output DNS RCODE. + * + * \return KNOT_E* + */ +int ddns_process_update(const zone_t *zone, const knot_pkt_t *query, + zone_update_t *update, uint16_t *rcode); diff --git a/src/knot/updates/zone-update.c b/src/knot/updates/zone-update.c new file mode 100644 index 0000000..81f3465 --- /dev/null +++ b/src/knot/updates/zone-update.c @@ -0,0 +1,1098 @@ +/* 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 <signal.h> +#include <unistd.h> +#include <urcu.h> + +#include "knot/catalog/interpret.h" +#include "knot/common/log.h" +#include "knot/common/systemd.h" +#include "knot/dnssec/zone-events.h" +#include "knot/server/server.h" +#include "knot/updates/zone-update.h" +#include "knot/zone/adds_tree.h" +#include "knot/zone/adjust.h" +#include "knot/zone/digest.h" +#include "knot/zone/serial.h" +#include "knot/zone/zone-diff.h" +#include "knot/zone/zonefile.h" +#include "contrib/trim.h" +#include "contrib/ucw/lists.h" + +// Call mem_trim() whenever accumulated size of updated zones reaches this size. +#define UPDATE_MEMTRIM_AT (10 * 1024 * 1024) + +static int init_incremental(zone_update_t *update, zone_t *zone, zone_contents_t *old_contents) +{ + if (old_contents == NULL) { + return KNOT_EINVAL; + } + + int ret = changeset_init(&update->change, zone->name); + if (ret != KNOT_EOK) { + return ret; + } + + if (update->flags & UPDATE_HYBRID) { + update->new_cont = old_contents; + } else { + ret = zone_contents_cow(old_contents, &update->new_cont); + if (ret != KNOT_EOK) { + changeset_clear(&update->change); + return ret; + } + } + + uint32_t apply_flags = (update->flags & UPDATE_STRICT) ? APPLY_STRICT : 0; + apply_flags |= (update->flags & UPDATE_HYBRID) ? APPLY_UNIFY_FULL : 0; + ret = apply_init_ctx(update->a_ctx, update->new_cont, apply_flags); + if (ret != KNOT_EOK) { + changeset_clear(&update->change); + return ret; + } + + /* Copy base SOA RR. */ + update->change.soa_from = + node_create_rrset(old_contents->apex, KNOT_RRTYPE_SOA); + if (update->change.soa_from == NULL) { + zone_contents_free(update->new_cont); + changeset_clear(&update->change); + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +static int init_full(zone_update_t *update, zone_t *zone) +{ + update->new_cont = zone_contents_new(zone->name, true); + if (update->new_cont == NULL) { + return KNOT_ENOMEM; + } + + int ret = apply_init_ctx(update->a_ctx, update->new_cont, APPLY_UNIFY_FULL); + if (ret != KNOT_EOK) { + zone_contents_free(update->new_cont); + return ret; + } + + return KNOT_EOK; +} + +static int replace_soa(zone_contents_t *contents, const knot_rrset_t *rr) +{ + /* SOA possible only within apex. */ + if (!knot_dname_is_equal(rr->owner, contents->apex->owner)) { + return KNOT_EDENIED; + } + + knot_rrset_t old_soa = node_rrset(contents->apex, KNOT_RRTYPE_SOA); + zone_node_t *n = contents->apex; + int ret = zone_contents_remove_rr(contents, &old_soa, &n); + if (ret != KNOT_EOK && ret != KNOT_EINVAL) { + return ret; + } + + ret = zone_contents_add_rr(contents, rr, &n); + if (ret == KNOT_ETTL) { + return KNOT_EOK; + } + + return ret; +} + +static int init_base(zone_update_t *update, zone_t *zone, zone_contents_t *old_contents, + zone_update_flags_t flags) +{ + if (update == NULL || zone == NULL) { + return KNOT_EINVAL; + } + + memset(update, 0, sizeof(*update)); + update->zone = zone; + update->flags = flags; + + update->a_ctx = calloc(1, sizeof(*update->a_ctx)); + if (update->a_ctx == NULL) { + return KNOT_ENOMEM; + } + + if (zone->control_update != NULL && zone->control_update != update) { + log_zone_warning(zone->name, "blocked zone update due to open control transaction"); + } + + knot_sem_wait(&zone->cow_lock); + update->a_ctx->cow_mutex = &zone->cow_lock; + + if (old_contents == NULL) { + old_contents = zone->contents; // don't obtain this pointer before any other zone_update ceased to exist! + } + + int ret = KNOT_EINVAL; + if (flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) { + ret = init_incremental(update, zone, old_contents); + } else if (flags & UPDATE_FULL) { + ret = init_full(update, zone); + } + if (ret != KNOT_EOK) { + knot_sem_post(&zone->cow_lock); + free(update->a_ctx); + } + + return ret; +} + +/* ------------------------------- API -------------------------------------- */ + +int zone_update_init(zone_update_t *update, zone_t *zone, zone_update_flags_t flags) +{ + return init_base(update, zone, NULL, flags); +} + +int zone_update_from_differences(zone_update_t *update, zone_t *zone, zone_contents_t *old_cont, + zone_contents_t *new_cont, zone_update_flags_t flags, + bool ignore_dnssec, bool ignore_zonemd) +{ + if (update == NULL || zone == NULL || new_cont == NULL || + !(flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) || (flags & UPDATE_FULL)) { + return KNOT_EINVAL; + } + + changeset_t diff; + int ret = changeset_init(&diff, zone->name); + if (ret != KNOT_EOK) { + return ret; + } + + ret = init_base(update, zone, old_cont, flags); + if (ret != KNOT_EOK) { + changeset_clear(&diff); + return ret; + } + + if (old_cont == NULL) { + old_cont = zone->contents; + } + + ret = zone_contents_diff(old_cont, new_cont, &diff, ignore_dnssec, ignore_zonemd); + switch (ret) { + case KNOT_ENODIFF: + case KNOT_ESEMCHECK: + case KNOT_EOK: + break; + case KNOT_ERANGE: + additionals_tree_free(update->new_cont->adds_tree); + update->new_cont->adds_tree = NULL; + update->new_cont = NULL; // Prevent deep_free as old_cont will be used later. + update->a_ctx->flags &= ~APPLY_UNIFY_FULL; // Prevent Unify of old_cont that will be used later. + // FALLTHROUGH + default: + changeset_clear(&diff); + zone_update_clear(update); + return ret; + } + + ret = zone_update_apply_changeset(update, &diff); + changeset_clear(&diff); + if (ret != KNOT_EOK) { + zone_update_clear(update); + return ret; + } + + update->init_cont = new_cont; + return KNOT_EOK; +} + +int zone_update_from_contents(zone_update_t *update, zone_t *zone_without_contents, + zone_contents_t *new_cont, zone_update_flags_t flags) +{ + if (update == NULL || zone_without_contents == NULL || new_cont == NULL) { + return KNOT_EINVAL; + } + + memset(update, 0, sizeof(*update)); + update->zone = zone_without_contents; + update->flags = flags; + update->new_cont = new_cont; + + update->a_ctx = calloc(1, sizeof(*update->a_ctx)); + if (update->a_ctx == NULL) { + return KNOT_ENOMEM; + } + + if (zone_without_contents->control_update != NULL) { + log_zone_warning(zone_without_contents->name, + "blocked zone update due to open control transaction"); + } + + knot_sem_wait(&update->zone->cow_lock); + update->a_ctx->cow_mutex = &update->zone->cow_lock; + + if (flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) { + int ret = changeset_init(&update->change, zone_without_contents->name); + if (ret != KNOT_EOK) { + free(update->a_ctx); + update->a_ctx = NULL; + knot_sem_post(&update->zone->cow_lock); + return ret; + } + + update->change.soa_from = node_create_rrset(new_cont->apex, KNOT_RRTYPE_SOA); + if (update->change.soa_from == NULL) { + changeset_clear(&update->change); + free(update->a_ctx); + update->a_ctx = NULL; + knot_sem_post(&update->zone->cow_lock); + return KNOT_ENOMEM; + } + } + + uint32_t apply_flags = (update->flags & UPDATE_STRICT) ? APPLY_STRICT : 0; + int ret = apply_init_ctx(update->a_ctx, update->new_cont, apply_flags | APPLY_UNIFY_FULL); + if (ret != KNOT_EOK) { + changeset_clear(&update->change); + free(update->a_ctx); + update->a_ctx = NULL; + knot_sem_post(&update->zone->cow_lock); + return ret; + } + + return KNOT_EOK; +} + +int zone_update_start_extra(zone_update_t *update, conf_t *conf) +{ + assert((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID))); + + int ret = changeset_init(&update->extra_ch, update->new_cont->apex->owner); + if (ret != KNOT_EOK) { + return ret; + } + + if (update->init_cont != NULL) { + ret = zone_update_increment_soa(update, conf); + if (ret != KNOT_EOK) { + return ret; + } + + ret = zone_contents_diff(update->init_cont, update->new_cont, + &update->extra_ch, false, false); + if (ret != KNOT_EOK) { + return ret; + } + } else { + update->extra_ch.soa_from = node_create_rrset(update->new_cont->apex, KNOT_RRTYPE_SOA); + if (update->extra_ch.soa_from == NULL) { + return KNOT_ENOMEM; + } + + ret = zone_update_increment_soa(update, conf); + if (ret != KNOT_EOK) { + return ret; + } + + update->extra_ch.soa_to = node_create_rrset(update->new_cont->apex, KNOT_RRTYPE_SOA); + if (update->extra_ch.soa_to == NULL) { + return KNOT_ENOMEM; + } + } + + update->flags |= UPDATE_EXTRA_CHSET; + return KNOT_EOK; +} + +const zone_node_t *zone_update_get_node(zone_update_t *update, const knot_dname_t *dname) +{ + if (update == NULL || dname == NULL) { + return NULL; + } + + return zone_contents_node_or_nsec3(update->new_cont, dname); +} + +uint32_t zone_update_current_serial(zone_update_t *update) +{ + const zone_node_t *apex = update->new_cont->apex; + if (apex != NULL) { + return knot_soa_serial(node_rdataset(apex, KNOT_RRTYPE_SOA)->rdata); + } else { + return 0; + } +} + +bool zone_update_changed_nsec3param(const zone_update_t *update) +{ + if (update->zone->contents == NULL) { + return true; + } + + dnssec_nsec3_params_t *orig = &update->zone->contents->nsec3_params; + dnssec_nsec3_params_t *upd = &update->new_cont->nsec3_params; + return !dnssec_nsec3_params_match(orig, upd); +} + +const knot_rdataset_t *zone_update_from(zone_update_t *update) +{ + if (update == NULL) { + return NULL; + } + + if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) { + const zone_node_t *apex = update->zone->contents->apex; + return node_rdataset(apex, KNOT_RRTYPE_SOA); + } + + return NULL; +} + +const knot_rdataset_t *zone_update_to(zone_update_t *update) +{ + if (update == NULL) { + return NULL; + } + + if (update->flags & UPDATE_NO_CHSET) { + zone_diff_t diff = { .apex = update->new_cont->apex }; + return zone_diff_to(&diff) == zone_diff_from(&diff) ? + NULL : node_rdataset(update->new_cont->apex, KNOT_RRTYPE_SOA); + } else if (update->flags & UPDATE_FULL) { + const zone_node_t *apex = update->new_cont->apex; + return node_rdataset(apex, KNOT_RRTYPE_SOA); + } else { + if (update->change.soa_to == NULL) { + return NULL; + } + return &update->change.soa_to->rrs; + } + + return NULL; +} + +void zone_update_clear(zone_update_t *update) +{ + if (update == NULL || update->zone == NULL) { + return; + } + + if (update->new_cont != NULL) { + additionals_tree_free(update->new_cont->adds_tree); + update->new_cont->adds_tree = NULL; + } + + if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) { + changeset_clear(&update->change); + changeset_clear(&update->extra_ch); + } + + zone_contents_deep_free(update->init_cont); + + if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) { + apply_cleanup(update->a_ctx); + zone_contents_deep_free(update->new_cont); + } else { + apply_rollback(update->a_ctx); + } + + free(update->a_ctx); + memset(update, 0, sizeof(*update)); +} + +inline static void update_affected_rrtype(zone_update_t *update, uint16_t rrtype) +{ + switch (rrtype) { + case KNOT_RRTYPE_NSEC: + case KNOT_RRTYPE_NSEC3: + update->flags |= UPDATE_CHANGED_NSEC; + break; + } +} + +static int solve_add_different_ttl(zone_update_t *update, const knot_rrset_t *add) +{ + if (add->type == KNOT_RRTYPE_RRSIG || add->type == KNOT_RRTYPE_SOA) { + return KNOT_EOK; + } + + const zone_node_t *exist_node = zone_contents_find_node(update->new_cont, add->owner); + const knot_rrset_t exist_rr = node_rrset(exist_node, add->type); + if (knot_rrset_empty(&exist_rr) || exist_rr.ttl == add->ttl) { + return KNOT_EOK; + } + + knot_dname_txt_storage_t buff; + char *owner = knot_dname_to_str(buff, add->owner, sizeof(buff)); + if (owner == NULL) { + owner = ""; + } + char type[16] = ""; + knot_rrtype_to_string(add->type, type, sizeof(type)); + log_zone_notice(update->zone->name, "TTL mismatch, owner %s, type %s, " + "TTL set to %u", owner, type, add->ttl); + + knot_rrset_t *exist_copy = knot_rrset_copy(&exist_rr, NULL); + if (exist_copy == NULL) { + return KNOT_ENOMEM; + } + int ret = zone_update_remove(update, exist_copy); + if (ret == KNOT_EOK) { + exist_copy->ttl = add->ttl; + ret = zone_update_add(update, exist_copy); + } + knot_rrset_free(exist_copy, NULL); + return ret; +} + +int zone_update_add(zone_update_t *update, const knot_rrset_t *rrset) +{ + if (update == NULL || rrset == NULL) { + return KNOT_EINVAL; + } + if (knot_rrset_empty(rrset)) { + return KNOT_EOK; + } + + if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) { + int ret = solve_add_different_ttl(update, rrset); + if (ret == KNOT_EOK && !(update->flags & UPDATE_NO_CHSET)) { + ret = changeset_add_addition(&update->change, rrset, CHANGESET_CHECK); + } + if (ret == KNOT_EOK && (update->flags & UPDATE_EXTRA_CHSET)) { + assert(!(update->flags & UPDATE_NO_CHSET)); + ret = changeset_add_addition(&update->extra_ch, rrset, CHANGESET_CHECK); + } + if (ret != KNOT_EOK) { + return ret; + } + } + + if (update->flags & UPDATE_INCREMENTAL) { + if (rrset->type == KNOT_RRTYPE_SOA) { + // replace previous SOA + int ret = apply_replace_soa(update->a_ctx, rrset); + if (ret != KNOT_EOK && !(update->flags & UPDATE_NO_CHSET)) { + changeset_remove_addition(&update->change, rrset); + } + return ret; + } + + int ret = apply_add_rr(update->a_ctx, rrset); + if (ret != KNOT_EOK) { + if (!(update->flags & UPDATE_NO_CHSET)) { + changeset_remove_addition(&update->change, rrset); + } + return ret; + } + + update_affected_rrtype(update, rrset->type); + return KNOT_EOK; + } else if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) { + if (rrset->type == KNOT_RRTYPE_SOA) { + /* replace previous SOA */ + return replace_soa(update->new_cont, rrset); + } + + zone_node_t *n = NULL; + int ret = zone_contents_add_rr(update->new_cont, rrset, &n); + if (ret == KNOT_ETTL) { + knot_dname_txt_storage_t buff; + char *owner = knot_dname_to_str(buff, rrset->owner, sizeof(buff)); + if (owner == NULL) { + owner = ""; + } + char type[16] = ""; + knot_rrtype_to_string(rrset->type, type, sizeof(type)); + log_zone_notice(update->new_cont->apex->owner, + "TTL mismatch, owner %s, type %s, " + "TTL set to %u", owner, type, rrset->ttl); + return KNOT_EOK; + } + + return ret; + } else { + return KNOT_EINVAL; + } +} + +int zone_update_remove(zone_update_t *update, const knot_rrset_t *rrset) +{ + if (update == NULL || rrset == NULL) { + return KNOT_EINVAL; + } + if (knot_rrset_empty(rrset)) { + return KNOT_EOK; + } + + if ((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && + rrset->type != KNOT_RRTYPE_SOA && !(update->flags & UPDATE_NO_CHSET)) { + int ret = changeset_add_removal(&update->change, rrset, CHANGESET_CHECK); + if (ret == KNOT_EOK && (update->flags & UPDATE_EXTRA_CHSET)) { + assert(!(update->flags & UPDATE_NO_CHSET)); + ret = changeset_add_removal(&update->extra_ch, rrset, CHANGESET_CHECK); + } + if (ret != KNOT_EOK) { + return ret; + } + } + + if (update->flags & UPDATE_INCREMENTAL) { + if (rrset->type == KNOT_RRTYPE_SOA) { + /* SOA is replaced with addition */ + return KNOT_EOK; + } + + int ret = apply_remove_rr(update->a_ctx, rrset); + if (ret != KNOT_EOK) { + if (!(update->flags & UPDATE_NO_CHSET)) { + changeset_remove_removal(&update->change, rrset); + } + return ret; + } + + update_affected_rrtype(update, rrset->type); + return KNOT_EOK; + } else if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) { + zone_node_t *n = NULL; + return zone_contents_remove_rr(update->new_cont, rrset, &n); + } else { + return KNOT_EINVAL; + } +} + +int zone_update_remove_rrset(zone_update_t *update, knot_dname_t *owner, uint16_t type) +{ + if (update == NULL || owner == NULL) { + return KNOT_EINVAL; + } + + const zone_node_t *node = zone_contents_node_or_nsec3(update->new_cont, owner); + if (node == NULL) { + return KNOT_ENONODE; + } + + knot_rrset_t rrset = node_rrset(node, type); + if (rrset.owner == NULL) { + return KNOT_ENOENT; + } + + return zone_update_remove(update, &rrset); +} + +int zone_update_remove_node(zone_update_t *update, const knot_dname_t *owner) +{ + if (update == NULL || owner == NULL) { + return KNOT_EINVAL; + } + + const zone_node_t *node = zone_contents_node_or_nsec3(update->new_cont, owner); + if (node == NULL) { + return KNOT_ENONODE; + } + + size_t rrset_count = node->rrset_count; + for (int i = 0; i < rrset_count; ++i) { + knot_rrset_t rrset = node_rrset_at(node, rrset_count - 1 - i); + int ret = zone_update_remove(update, &rrset); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int update_chset_step(const knot_rrset_t *rrset, bool addition, void *ctx) +{ + zone_update_t *update = ctx; + if (addition) { + return zone_update_add(update, rrset); + } else { + return zone_update_remove(update, rrset); + } +} + +int zone_update_apply_changeset(zone_update_t *update, const changeset_t *changes) +{ + return changeset_walk(changes, update_chset_step, update); +} + +int zone_update_apply_changeset_reverse(zone_update_t *update, const changeset_t *changes) +{ + changeset_t reverse; + reverse.remove = changes->add; + reverse.add = changes->remove; + reverse.soa_from = changes->soa_to; + reverse.soa_to = changes->soa_from; + return zone_update_apply_changeset(update, &reverse); +} + +static int set_new_soa(zone_update_t *update, unsigned serial_policy) +{ + assert(update); + + knot_rrset_t *soa_cpy = node_create_rrset(update->new_cont->apex, + KNOT_RRTYPE_SOA); + if (soa_cpy == NULL) { + return KNOT_ENOMEM; + } + + int ret = zone_update_remove(update, soa_cpy); + if (ret != KNOT_EOK) { + knot_rrset_free(soa_cpy, NULL); + return ret; + } + + uint32_t old_serial = knot_soa_serial(soa_cpy->rrs.rdata); + uint32_t new_serial = serial_next(old_serial, serial_policy, 1); + if (serial_compare(old_serial, new_serial) != SERIAL_LOWER) { + log_zone_warning(update->zone->name, "updated SOA serial is lower " + "than current, serial %u -> %u", + old_serial, new_serial); + ret = KNOT_ESOAINVAL; + } else { + knot_soa_serial_set(soa_cpy->rrs.rdata, new_serial); + + ret = zone_update_add(update, soa_cpy); + } + knot_rrset_free(soa_cpy, NULL); + + return ret; +} + +int zone_update_increment_soa(zone_update_t *update, conf_t *conf) +{ + if (update == NULL || conf == NULL) { + return KNOT_EINVAL; + } + + conf_val_t val = conf_zone_get(conf, C_SERIAL_POLICY, update->zone->name); + return set_new_soa(update, conf_opt(&val)); +} + +static void get_zone_diff(zone_diff_t *zdiff, zone_update_t *up) +{ + zdiff->nodes = *up->a_ctx->node_ptrs; + zdiff->nsec3s = *up->a_ctx->nsec3_ptrs; + zdiff->apex = up->new_cont->apex; +} + +static int commit_journal(conf_t *conf, zone_update_t *update) +{ + conf_val_t val = conf_zone_get(conf, C_JOURNAL_CONTENT, update->zone->name); + unsigned content = conf_opt(&val); + int ret = KNOT_EOK; + if (update->flags & UPDATE_NO_CHSET) { + zone_diff_t diff; + get_zone_diff(&diff, update); + if (content != JOURNAL_CONTENT_NONE && !zone_update_no_change(update)) { + ret = zone_diff_store(conf, update->zone, &diff); + } + } else if ((update->flags & UPDATE_INCREMENTAL) || + (update->flags & UPDATE_HYBRID)) { + changeset_t *extra = (update->flags & UPDATE_EXTRA_CHSET) ? &update->extra_ch : NULL; + if (content != JOURNAL_CONTENT_NONE && !zone_update_no_change(update)) { + ret = zone_change_store(conf, update->zone, &update->change, extra); + } + } else { + if (content == JOURNAL_CONTENT_ALL) { + return zone_in_journal_store(conf, update->zone, update->new_cont); + } else if (content != JOURNAL_CONTENT_NONE) { // zone_in_journal_store does this automatically + return zone_changes_clear(conf, update->zone); + } + } + return ret; +} + +static int commit_incremental(conf_t *conf, zone_update_t *update) +{ + assert(update); + + if (zone_update_to(update) == NULL && !zone_update_no_change(update)) { + /* No SOA in the update, create one according to the current policy */ + int ret = zone_update_increment_soa(update, conf); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int commit_full(conf_t *conf, zone_update_t *update) +{ + assert(update); + + /* Check if we have SOA. We might consider adding full semantic check here. + * But if we wanted full sem-check I'd consider being it controlled by a flag + * - to enable/disable it on demand. */ + if (!node_rrtype_exists(update->new_cont->apex, KNOT_RRTYPE_SOA)) { + return KNOT_ESEMCHECK; + } + + return KNOT_EOK; +} + +static int update_catalog(conf_t *conf, zone_update_t *update) +{ + conf_val_t val = conf_zone_get(conf, C_CATALOG_TPL, update->zone->name); + if (val.code != KNOT_EOK) { + return (val.code == KNOT_ENOENT || val.code == KNOT_YP_EINVAL_ID) ? KNOT_EOK : val.code; + } + + int ret = catalog_zone_verify(update->new_cont); + if (ret != KNOT_EOK) { + return ret; + } + + ssize_t upd_count = 0; + if ((update->flags & UPDATE_NO_CHSET)) { + zone_diff_t diff; + get_zone_diff(&diff, update); + ret = catalog_update_from_zone(zone_catalog_upd(update->zone), + NULL, &diff, update->new_cont, + false, zone_catalog(update->zone), &upd_count); + } else if ((update->flags & UPDATE_INCREMENTAL)) { + ret = catalog_update_from_zone(zone_catalog_upd(update->zone), + update->change.remove, NULL, update->new_cont, + true, zone_catalog(update->zone), &upd_count); + if (ret == KNOT_EOK) { + ret = catalog_update_from_zone(zone_catalog_upd(update->zone), + update->change.add, NULL, update->new_cont, + false, NULL, &upd_count); + } + } else { + ret = catalog_update_del_all(zone_catalog_upd(update->zone), + zone_catalog(update->zone), + update->zone->name, &upd_count); + if (ret == KNOT_EOK) { + ret = catalog_update_from_zone(zone_catalog_upd(update->zone), + update->new_cont, NULL, update->new_cont, + false, NULL, &upd_count); + } + } + + if (ret == KNOT_EOK) { + log_zone_info(update->zone->name, "catalog reloaded, %zd updates", upd_count); + update->zone->server->catalog_upd_signal = true; + if (kill(getpid(), SIGUSR1) != 0) { + ret = knot_map_errno(); + } + } else { + // this cant normally happen, just some ENOMEM or so + (void)catalog_update_del_all(zone_catalog_upd(update->zone), + zone_catalog(update->zone), + update->zone->name, &upd_count); + } + + return ret; +} + +typedef struct { + pthread_mutex_t lock; + size_t counter; +} counter_reach_t; + +static bool counter_reach(counter_reach_t *counter, size_t increment, size_t limit) +{ + bool reach = false; + pthread_mutex_lock(&counter->lock); + counter->counter += increment; + if (counter->counter >= limit) { + counter->counter = 0; + reach = true; + } + pthread_mutex_unlock(&counter->lock); + return reach; +} + +/*! \brief Struct for what needs to be cleared after RCU. + * + * This can't be zone_update_t structure as this might be already freed at that time. + */ +typedef struct { + struct rcu_head rcuhead; + + zone_contents_t *free_contents; + void (*free_method)(zone_contents_t *); + + apply_ctx_t *cleanup_apply; + + size_t new_cont_size; +} update_clear_ctx_t; + +static void update_clear(struct rcu_head *param) +{ + static counter_reach_t counter = { PTHREAD_MUTEX_INITIALIZER, 0 }; + + update_clear_ctx_t *ctx = (update_clear_ctx_t *)param; + + ctx->free_method(ctx->free_contents); + apply_cleanup(ctx->cleanup_apply); + free(ctx->cleanup_apply); + + if (counter_reach(&counter, ctx->new_cont_size, UPDATE_MEMTRIM_AT)) { + mem_trim(); + } + + free(ctx); +} + +static void discard_adds_tree(zone_update_t *update) +{ + additionals_tree_free(update->new_cont->adds_tree); + update->new_cont->adds_tree = NULL; +} + +int zone_update_semcheck(conf_t *conf, zone_update_t *update) +{ + if (update == NULL) { + return KNOT_EINVAL; + } + + zone_tree_t *node_ptrs = (update->flags & UPDATE_INCREMENTAL) ? + update->a_ctx->node_ptrs : NULL; + + // adjust_cb_nsec3_pointer not needed as we don't check DNSSEC here + int ret = zone_adjust_contents(update->new_cont, adjust_cb_flags, NULL, + false, false, 1, node_ptrs); + if (ret != KNOT_EOK) { + return ret; + } + + sem_handler_t handler = { + .cb = err_handler_logger + }; + + conf_val_t val = conf_zone_get(conf, C_SEM_CHECKS, update->zone->name); + semcheck_optional_t mode = (conf_opt(&val) == SEMCHECKS_SOFT) ? + SEMCHECK_MANDATORY_SOFT : SEMCHECK_MANDATORY_ONLY; + + ret = sem_checks_process(update->new_cont, mode, &handler, time(NULL)); + if (ret != KNOT_EOK) { + // error is logged by the error handler + return ret; + } + + return KNOT_EOK; +} + +int zone_update_verify_digest(conf_t *conf, zone_update_t *update) +{ + conf_val_t val = conf_zone_get(conf, C_ZONEMD_VERIFY, update->zone->name); + if (!conf_bool(&val)) { + return KNOT_EOK; + } + + int ret = zone_contents_digest_verify(update->new_cont); + if (ret != KNOT_EOK) { + log_zone_error(update->zone->name, "ZONEMD, verification failed (%s)", + knot_strerror(ret)); + } else { + log_zone_info(update->zone->name, "ZONEMD, verification successful"); + } + + return ret; +} + +int zone_update_commit(conf_t *conf, zone_update_t *update) +{ + if (conf == NULL || update == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + + if ((update->flags & UPDATE_INCREMENTAL) && zone_update_no_change(update)) { + zone_update_clear(update); + return KNOT_EOK; + } + + if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) { + ret = commit_incremental(conf, update); + } else { + ret = commit_full(conf, update); + } + if (ret != KNOT_EOK) { + return ret; + } + + conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, update->zone->name); + bool dnssec = conf_bool(&val); + + conf_val_t thr = conf_zone_get(conf, C_ADJUST_THR, update->zone->name); + if ((update->flags & (UPDATE_HYBRID | UPDATE_FULL))) { + ret = zone_adjust_full(update->new_cont, conf_int(&thr)); + } else { + ret = zone_adjust_incremental_update(update, conf_int(&thr)); + } + if (ret != KNOT_EOK) { + discard_adds_tree(update); + return ret; + } + + /* Check the zone size. */ + val = conf_zone_get(conf, C_ZONE_MAX_SIZE, update->zone->name); + size_t size_limit = conf_int(&val); + + if (update->new_cont->size > size_limit) { + discard_adds_tree(update); + return KNOT_EZONESIZE; + } + + val = conf_zone_get(conf, C_DNSSEC_VALIDATION, update->zone->name); + if (conf_bool(&val)) { + bool incr_valid = update->flags & UPDATE_INCREMENTAL; + const char *msg_valid = incr_valid ? "incremental " : ""; + + ret = knot_dnssec_validate_zone(update, conf, 0, incr_valid); + if (ret != KNOT_EOK) { + log_zone_error(update->zone->name, "DNSSEC, %svalidation failed (%s)", + msg_valid, knot_strerror(ret)); + char type_str[16]; + knot_dname_txt_storage_t name_str; + if (knot_dname_to_str(name_str, update->validation_hint.node, sizeof(name_str)) != NULL && + knot_rrtype_to_string(update->validation_hint.rrtype, type_str, sizeof(type_str)) >= 0) { + log_zone_error(update->zone->name, "DNSSEC, validation hint: %s %s", + name_str, type_str); + } + discard_adds_tree(update); + if (conf->cache.srv_dbus_event & DBUS_EVENT_ZONE_INVALID) { + systemd_emit_zone_invalid(update->zone->name); + } + return ret; + } else { + log_zone_info(update->zone->name, "DNSSEC, %svalidation successful", msg_valid); + } + } + + ret = update_catalog(conf, update); + if (ret != KNOT_EOK) { + log_zone_error(update->zone->name, "failed to process catalog zone (%s)", knot_strerror(ret)); + discard_adds_tree(update); + return ret; + } + + ret = commit_journal(conf, update); + if (ret != KNOT_EOK) { + discard_adds_tree(update); + return ret; + } + + if (dnssec && zone_is_slave(conf, update->zone)) { + ret = zone_set_lastsigned_serial(update->zone, + zone_contents_serial(update->new_cont)); + if (ret != KNOT_EOK) { + log_zone_warning(update->zone->name, + "unable to save lastsigned serial, " + "future transfers might be broken"); + } + } + + /* Switch zone contents. */ + zone_contents_t *old_contents; + old_contents = zone_switch_contents(update->zone, update->new_cont); + + if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) { + changeset_clear(&update->change); + changeset_clear(&update->extra_ch); + } + zone_contents_deep_free(update->init_cont); + + update_clear_ctx_t *clear_ctx = calloc(1, sizeof(*clear_ctx)); + if (clear_ctx != NULL) { + clear_ctx->free_contents = old_contents; + clear_ctx->free_method = ( + (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) ? + zone_contents_deep_free : update_free_zone + ); + clear_ctx->cleanup_apply = update->a_ctx; + clear_ctx->new_cont_size = update->new_cont->size; + + call_rcu((struct rcu_head *)clear_ctx, update_clear); + } else { + log_zone_error(update->zone->name, "failed to deallocate unused memory"); + } + + /* Sync zonefile immediately if configured. */ + val = conf_zone_get(conf, C_ZONEFILE_SYNC, update->zone->name); + if (conf_int(&val) == 0) { + zone_events_schedule_now(update->zone, ZONE_EVENT_FLUSH); + } + + if (conf->cache.srv_dbus_event & DBUS_EVENT_ZONE_UPDATED) { + systemd_emit_zone_updated(update->zone->name, + zone_contents_serial(update->zone->contents)); + } + + memset(update, 0, sizeof(*update)); + + return KNOT_EOK; +} + +bool zone_update_no_change(zone_update_t *update) +{ + if (update == NULL) { + return true; + } + + if (update->flags & UPDATE_NO_CHSET) { + zone_diff_t diff; + get_zone_diff(&diff, update); + return (zone_diff_serialized_size(diff) == 0); + } else if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) { + return changeset_empty(&update->change); + } else { + /* This branch does not make much sense and FULL update will most likely + * be a change every time anyway, just return false. */ + return false; + } +} + +static bool zone_diff_rdataset(const zone_contents_t *c, uint16_t rrtype) +{ + const knot_rdataset_t *a = node_rdataset(binode_counterpart(c->apex), rrtype); + const knot_rdataset_t *b = node_rdataset(c->apex, rrtype); + if ((a == NULL && b == NULL) || (a != NULL && b != NULL && a->rdata == b->rdata)) { + return false; + } else { + return !knot_rdataset_eq(a, b); + } +} + +static bool contents_have_dnskey(const zone_contents_t *contents) +{ + if (contents == NULL) { + return false; + } + assert(contents->apex != NULL); + return (node_rrtype_exists(contents->apex, KNOT_RRTYPE_DNSKEY) || + node_rrtype_exists(contents->apex, KNOT_RRTYPE_CDNSKEY) || + node_rrtype_exists(contents->apex, KNOT_RRTYPE_CDS)); +} + +bool zone_update_changes_dnskey(zone_update_t *update) +{ + if (update->flags & UPDATE_NO_CHSET) { + return (zone_diff_rdataset(update->new_cont, KNOT_RRTYPE_DNSKEY) || + zone_diff_rdataset(update->new_cont, KNOT_RRTYPE_CDNSKEY) || + zone_diff_rdataset(update->new_cont, KNOT_RRTYPE_CDS)); + } else if (update->flags & UPDATE_FULL) { + return contents_have_dnskey(update->new_cont); + } else { + return (contents_have_dnskey(update->change.remove) || + contents_have_dnskey(update->change.add)); + } +} diff --git a/src/knot/updates/zone-update.h b/src/knot/updates/zone-update.h new file mode 100644 index 0000000..0499d72 --- /dev/null +++ b/src/knot/updates/zone-update.h @@ -0,0 +1,299 @@ +/* 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/>. + */ + +#pragma once + +#include "knot/updates/apply.h" +#include "knot/conf/conf.h" +#include "knot/updates/changesets.h" +#include "knot/zone/contents.h" +#include "knot/zone/zone.h" + +typedef struct { + knot_dname_storage_t next; + const knot_dname_t *node; + uint16_t rrtype; +} dnssec_validation_hint_t; + +/*! \brief Structure for zone contents updating / querying. */ +typedef struct zone_update { + zone_t *zone; /*!< Zone being updated. */ + zone_contents_t *new_cont; /*!< New zone contents for full updates. */ + changeset_t change; /*!< Changes we want to apply. */ + zone_contents_t *init_cont; /*!< Exact contents of the zonefile. */ + changeset_t extra_ch; /*!< Extra changeset to store just diff btwn zonefile and result. */ + apply_ctx_t *a_ctx; /*!< Context for applying changesets. */ + uint32_t flags; /*!< Zone update flags. */ + dnssec_validation_hint_t validation_hint; +} zone_update_t; + +typedef struct { + zone_update_t *update; /*!< The update we're iterating over. */ + zone_tree_it_t tree_it; /*!< Iterator for the new zone. */ + const zone_node_t *cur_node; /*!< Current node in the new zone. */ + bool nsec3; /*!< Set when we're using the NSEC3 node tree. */ +} zone_update_iter_t; + +typedef enum { + // Mutually exclusive flags + UPDATE_FULL = 1 << 0, /*!< Replace the old zone by a complete new one. */ + UPDATE_HYBRID = 1 << 1, /*!< Changeset like for incremental, adjusting like full. */ + UPDATE_INCREMENTAL = 1 << 2, /*!< Apply changes to the old zone. */ + // Additional flags + UPDATE_STRICT = 1 << 4, /*!< Apply changes strictly, i.e. fail when removing nonexistent RR. */ + UPDATE_EXTRA_CHSET = 1 << 6, /*!< Extra changeset in use, to store diff btwn zonefile and final contents. */ + UPDATE_CHANGED_NSEC = 1 << 7, /*!< This incremental update affects NSEC or NSEC3 nodes in zone. */ + UPDATE_NO_CHSET = 1 << 8, /*!< Avoid using changeset and serialize to journal from diff of bi-nodes. */ +} zone_update_flags_t; + +/*! + * \brief Inits given zone update structure, new memory context is created. + * + * \param update Zone update structure to init. + * \param zone Init with this zone. + * \param flags Flags to control the behavior of the update. + * + * \return KNOT_E* + */ +int zone_update_init(zone_update_t *update, zone_t *zone, zone_update_flags_t flags); + +/*! + * \brief Inits update structure, the update is built like IXFR from differences. + * + * The existing zone with its own contents is taken as a base, + * the new candidate zone contents are taken as new contents, + * the diff is calculated, so that this update is INCREMENTAL. + * + * \param update Zone update structure to init. + * \param zone Init with this zone. + * \param old_cont The current zone contents the diff will be against. Probably zone->contents. + * \param new_cont New zone contents. Will be taken over (and later freed) by zone update. + * \param flags Flags for update. Must be UPDATE_INCREMENTAL or UPDATE_HYBRID. + * \param ignore_dnssec Ignore DNSSEC records. + * \param ignore_zonemd Ignore ZONEMD records. + * + * \return KNOT_E* + */ +int zone_update_from_differences(zone_update_t *update, zone_t *zone, zone_contents_t *old_cont, + zone_contents_t *new_cont, zone_update_flags_t flags, + bool ignore_dnssec, bool ignore_zonemd); + +/*! + * \brief Inits a zone update based on new zone contents. + * + * \param update Zone update structure to init. + * \param zone_without_contents Init with this zone. Its contents may be NULL. + * \param new_cont New zone contents. Will be taken over (and later freed) by zone update. + * \param flags Flags for update. + * + * \return KNOT_E* + */ +int zone_update_from_contents(zone_update_t *update, zone_t *zone_without_contents, + zone_contents_t *new_cont, zone_update_flags_t flags); + +/*! + * \brief Inits using extra changeset, increments SOA serial. + * + * This shall be used after from_differences, to start tracking changes that are against the loaded zonefile. + * + * \param update Zone update. + * \param conf Configuration. + * + * \return KNOT_E* + */ +int zone_update_start_extra(zone_update_t *update, conf_t *conf); + +/*! + * \brief Returns node that would be in the zone after updating it. + * + * \note Returned node is either zone original or synthesized, do *not* free + * or modify. Returned node is allocated on local mempool. + * + * \param update Zone update. + * \param dname Dname to search for. + * + * \return Node after zone update. + */ +const zone_node_t *zone_update_get_node(zone_update_t *update, + const knot_dname_t *dname); + +/*! + * \brief Returns the serial from the current apex. + * + * \param update Zone update. + * + * \return 0 if no apex was found, its serial otherwise. + */ +uint32_t zone_update_current_serial(zone_update_t *update); + +/*! \brief Return true if NSEC3PARAM has been changed in this update. */ +bool zone_update_changed_nsec3param(const zone_update_t *update); + +/*! + * \brief Returns the SOA rdataset we're updating from. + * + * \param update Zone update. + * + * \return The original SOA rdataset. + */ +const knot_rdataset_t *zone_update_from(zone_update_t *update); + +/*! + * \brief Returns the SOA rdataset we're updating to. + * + * \param update Zone update. + * + * \return NULL if no new SOA has been added, new SOA otherwise. + * + * \todo Refactor this function according to its use. + */ +const knot_rdataset_t *zone_update_to(zone_update_t *update); + +/*! + * \brief Clear data allocated by given zone update structure. + * + * \param update Zone update to clear. + */ +void zone_update_clear(zone_update_t *update); + +/*! + * \brief Adds an RRSet to the zone. + * + * \warning Do not edit the zone_update when any iterator is active. Any + * zone_update modifications will invalidate the trie iterators + * in the zone_update iterator(s). + * + * \param update Zone update. + * \param rrset RRSet to add. + * + * \return KNOT_E* + */ +int zone_update_add(zone_update_t *update, const knot_rrset_t *rrset); + +/*! + * \brief Removes an RRSet from the zone. + * + * \warning Do not edit the zone_update when any iterator is active. Any + * zone_update modifications will invalidate the trie iterators + * in the zone_update iterator(s). + * + * \param update Zone update. + * \param rrset RRSet to remove. + * + * \return KNOT_E* + */ +int zone_update_remove(zone_update_t *update, const knot_rrset_t *rrset); + +/*! + * \brief Removes a whole RRSet of specified type from the zone. + * + * \warning Do not edit the zone_update when any iterator is active. Any + * zone_update modifications will invalidate the trie iterators + * in the zone_update iterator(s). + * + * \param update Zone update. + * \param owner Node name to remove. + * \param type RRSet type to remove. + * + * \return KNOT_E* + */ +int zone_update_remove_rrset(zone_update_t *update, knot_dname_t *owner, uint16_t type); + +/*! + * \brief Removes a whole node from the zone. + * + * \warning Do not edit the zone_update when any iterator is active. Any + * zone_update modifications will invalidate the trie iterators + * in the zone_update iterator(s). + * + * \param update Zone update. + * \param owner Node name to remove. + * + * \return KNOT_E* + */ +int zone_update_remove_node(zone_update_t *update, const knot_dname_t *owner); + +/*! + * \brief Adds and removes RRsets to/from the zone according to the changeset. + * + * \param update Zone update. + * \param changes Changes to be made in zone. + * + * \return KNOT_E* + */ +int zone_update_apply_changeset(zone_update_t *update, const changeset_t *changes); + +/*! + * \brief Applies the changeset in reverse, rsets from REM section are added and from ADD section removed. + * + * \param update Zone update. + * \param changes Changes to be un-done. + * + * \return KNOT_E* + */ +int zone_update_apply_changeset_reverse(zone_update_t *update, const changeset_t *changes); + +/*! + * \brief Increment SOA serial (according to configured policy) in the update. + * + * \param update Update to be modified. + * \param conf Configuration. + * + * \return KNOT_E* + */ +int zone_update_increment_soa(zone_update_t *update, conf_t *conf); + +/*! + * \brief Executes mandatory semantic checks on the zone contents. + * + * \param conf Configuration. + * \param update Update to be checked. + * + * \return KNOT_E* + */ +int zone_update_semcheck(conf_t *conf, zone_update_t *update); + +/*! + * \brief If configured, verify ZONEMD and log the result. + * + * \param conf Configuration. + * \param update Zone update. + * + * \return KNOT_E* + */ +int zone_update_verify_digest(conf_t *conf, zone_update_t *update); + +/*! + * \brief Commits all changes to the zone, signs it, saves changes to journal. + * + * \param conf Configuration. + * \param update Zone update. + * + * \return KNOT_E* + */ +int zone_update_commit(conf_t *conf, zone_update_t *update); + +/*! + * \brief Returns bool whether there are any changes at all. + * + * \param update Zone update. + */ +bool zone_update_no_change(zone_update_t *update); + +/*! + * \brief Return whether apex DNSKEY, CDNSKEY, or CDS is updated. + */ +bool zone_update_changes_dnskey(zone_update_t *update); diff --git a/src/knot/worker/pool.c b/src/knot/worker/pool.c new file mode 100644 index 0000000..ff74970 --- /dev/null +++ b/src/knot/worker/pool.c @@ -0,0 +1,254 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <pthread.h> +#include <stdbool.h> +#include <stdlib.h> +#include <string.h> + +#include "libknot/libknot.h" +#include "knot/server/dthreads.h" +#include "knot/worker/pool.h" + +/*! + * \brief Worker pool state. + */ +struct worker_pool { + dt_unit_t *threads; + + pthread_mutex_t lock; + pthread_cond_t wake; + + bool terminating; /*!< Is the pool terminating? .*/ + bool suspended; /*!< Is execution temporarily suspended? .*/ + int running; /*!< Number of running threads. */ + worker_queue_t tasks; +}; + +/*! + * \brief Worker thread. + * + * The thread takes a task from the tasks queue and runs it, while checking + * if the dispatching of new tasks is allowed by the thread pool. + * + * An execution of a running thread cannot be enforced. + * + */ +static int worker_main(dthread_t *thread) +{ + assert(thread); + + worker_pool_t *pool = thread->data; + + pthread_mutex_lock(&pool->lock); + + for (;;) { + if (pool->terminating) { + break; + } + + worker_task_t *task = NULL; + if (!pool->suspended) { + task = worker_queue_dequeue(&pool->tasks); + } + + if (task == NULL) { + pthread_cond_wait(&pool->wake, &pool->lock); + continue; + } + + assert(task->run); + pool->running += 1; + + pthread_mutex_unlock(&pool->lock); + task->run(task); + pthread_mutex_lock(&pool->lock); + + pool->running -= 1; + pthread_cond_broadcast(&pool->wake); + } + + pthread_mutex_unlock(&pool->lock); + + return KNOT_EOK; +} + +/* -- public API ------------------------------------------------------------ */ + +worker_pool_t *worker_pool_create(unsigned threads) +{ + worker_pool_t *pool = malloc(sizeof(worker_pool_t)); + if (pool == NULL) { + return NULL; + } + + memset(pool, 0, sizeof(worker_pool_t)); + pool->threads = dt_create(threads, worker_main, NULL, pool); + if (pool->threads == NULL) { + goto fail; + } + + if (pthread_mutex_init(&pool->lock, NULL) != 0) { + goto fail; + } + + if (pthread_cond_init(&pool->wake, NULL) != 0) { + goto fail; + } + + worker_queue_init(&pool->tasks); + + return pool; + +fail: + dt_delete(&pool->threads); + free(pool); + return NULL; +} + +void worker_pool_destroy(worker_pool_t *pool) +{ + if (!pool) { + return; + } + + dt_delete(&pool->threads); + + pthread_mutex_destroy(&pool->lock); + pthread_cond_destroy(&pool->wake); + + worker_queue_deinit(&pool->tasks); + + free(pool); +} + +void worker_pool_start(worker_pool_t *pool) +{ + if (!pool) { + return; + } + + dt_start(pool->threads); +} + +void worker_pool_stop(worker_pool_t *pool) +{ + if (!pool) { + return; + } + + pthread_mutex_lock(&pool->lock); + pool->terminating = true; + pthread_cond_broadcast(&pool->wake); + pthread_mutex_unlock(&pool->lock); + + dt_stop(pool->threads); +} + +void worker_pool_suspend(worker_pool_t *pool) +{ + if (!pool) { + return; + } + + pthread_mutex_lock(&pool->lock); + pool->suspended = true; + pthread_mutex_unlock(&pool->lock); +} + +void worker_pool_resume(worker_pool_t *pool) +{ + if (!pool) { + return; + } + + pthread_mutex_lock(&pool->lock); + pool->suspended = false; + pthread_cond_broadcast(&pool->wake); + pthread_mutex_unlock(&pool->lock); +} + +void worker_pool_join(worker_pool_t *pool) +{ + if (!pool) { + return; + } + + dt_join(pool->threads); +} + +void worker_pool_wait_cb(worker_pool_t *pool, wait_callback_t cb) +{ + if (!pool) { + return; + } + + pthread_mutex_lock(&pool->lock); + while (!EMPTY_LIST(pool->tasks.list) || pool->running > 0) { + if (cb != NULL) { + cb(pool); + } + pthread_cond_wait(&pool->wake, &pool->lock); + } + pthread_mutex_unlock(&pool->lock); +} + +void worker_pool_wait(worker_pool_t *pool) +{ + worker_pool_wait_cb(pool, NULL); +} + +void worker_pool_assign(worker_pool_t *pool, struct task *task) +{ + if (!pool || !task) { + return; + } + + pthread_mutex_lock(&pool->lock); + worker_queue_enqueue(&pool->tasks, task); + pthread_cond_signal(&pool->wake); + pthread_mutex_unlock(&pool->lock); +} + +void worker_pool_clear(worker_pool_t *pool) +{ + if (!pool) { + return; + } + + pthread_mutex_lock(&pool->lock); + worker_queue_deinit(&pool->tasks); + worker_queue_init(&pool->tasks); + pthread_mutex_unlock(&pool->lock); +} + +void worker_pool_status(worker_pool_t *pool, bool locked, int *running, int *queued) +{ + if (!pool) { + *running = *queued = 0; + return; + } + + if (!locked) { + pthread_mutex_lock(&pool->lock); + } + *running = pool->running; + *queued = worker_queue_length(&pool->tasks); + if (!locked) { + pthread_mutex_unlock(&pool->lock); + } +} diff --git a/src/knot/worker/pool.h b/src/knot/worker/pool.h new file mode 100644 index 0000000..f843ea7 --- /dev/null +++ b/src/knot/worker/pool.h @@ -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/>. + */ + +#pragma once + +#include <stdbool.h> + +#include "knot/worker/queue.h" + +struct worker_pool; +typedef struct worker_pool worker_pool_t; + +typedef void(*wait_callback_t)(worker_pool_t *); + +/*! + * \brief Initialize worker pool. + * + * \param threads Number of threads to be created. + * + * \return Thread pool or NULL in case of error. + */ +worker_pool_t *worker_pool_create(unsigned threads); + +/*! + * \brief Destroy the worker pool. + */ +void worker_pool_destroy(worker_pool_t *pool); + +/*! + * \brief Start all threads in the worker pool. + */ +void worker_pool_start(worker_pool_t *pool); + +/*! + * \brief Stop processing of new tasks, start stopping worker threads when possible. + */ +void worker_pool_stop(worker_pool_t *pool); + +/*! + * \brief Temporarily suspend the execution of worker pool. + */ +void worker_pool_suspend(worker_pool_t *pool); + +/*! + * \brief Resume the execution of worker pool. + */ +void worker_pool_resume(worker_pool_t *pool); + +/*! + * \brief Wait for all threads to terminate. + */ +void worker_pool_join(worker_pool_t *pool); + +/*! + * \brief Wait till the number of pending tasks is zero. + */ +void worker_pool_wait(worker_pool_t *pool); + +/*! + * \brief Wait till the number of pending tasks is zero. Callback emitted on + * thread wakeup can be specified. + */ +void worker_pool_wait_cb(worker_pool_t *pool, wait_callback_t cb); + +/*! + * \brief Assign a task to be performed by a worker in the pool. + */ +void worker_pool_assign(worker_pool_t *pool, struct task *task); + +/*! + * \brief Clear all tasks enqueued in pool processing queue. + */ +void worker_pool_clear(worker_pool_t *pool); + +/*! + * \brief Obtain info regarding how the pool is busy. + * + * \note Locked means if the mutex `pool->lock` is locked. + */ +void worker_pool_status(worker_pool_t *pool, bool locked, int *running, int *queued); diff --git a/src/knot/worker/queue.c b/src/knot/worker/queue.c new file mode 100644 index 0000000..d9fc2b6 --- /dev/null +++ b/src/knot/worker/queue.c @@ -0,0 +1,67 @@ +/* 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/worker/queue.h" +#include "contrib/mempattern.h" + +void worker_queue_init(worker_queue_t *queue) +{ + if (!queue) { + return; + } + + memset(queue, 0, sizeof(worker_queue_t)); + + init_list(&queue->list); + mm_ctx_init(&queue->mm_ctx); +} + +void worker_queue_deinit(worker_queue_t *queue) +{ + ptrlist_free(&queue->list, &queue->mm_ctx); +} + +void worker_queue_enqueue(worker_queue_t *queue, worker_task_t *task) +{ + if (!queue || !task) { + return; + } + + ptrlist_add(&queue->list, task, &queue->mm_ctx); +} + +worker_task_t *worker_queue_dequeue(worker_queue_t *queue) +{ + if (!queue) { + return NULL; + } + + worker_task_t *task = NULL; + + if (!EMPTY_LIST(queue->list)) { + ptrnode_t *node = HEAD(queue->list); + task = (void *)node->d; + rem_node(&node->n); + queue->mm_ctx.free(&node->n); + } + + return task; +} + +size_t worker_queue_length(worker_queue_t *queue) +{ + return queue ? list_size(&queue->list) : 0; +} diff --git a/src/knot/worker/queue.h b/src/knot/worker/queue.h new file mode 100644 index 0000000..0ade7ab --- /dev/null +++ b/src/knot/worker/queue.h @@ -0,0 +1,65 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/ucw/lists.h" + +struct task; +typedef void (*task_cb)(struct task *); + +/*! + * \brief Task executable by a worker. + */ +typedef struct task { + void *ctx; + task_cb run; +} worker_task_t; + +/*! + * \brief Worker queue. + */ +typedef struct worker_queue { + knot_mm_t mm_ctx; + list_t list; +} worker_queue_t; + +/*! + * \brief Initialize worker queue. + */ +void worker_queue_init(worker_queue_t *queue); + +/*! + * \brief Deinitialize worker queue. + */ +void worker_queue_deinit(worker_queue_t *queue); + +/*! + * \brief Insert new item into the queue. + */ +void worker_queue_enqueue(worker_queue_t *queue, worker_task_t *task); + +/*! + * \brief Remove item from the queue. + * + * \return Task or NULL if the queue is empty. + */ +worker_task_t *worker_queue_dequeue(worker_queue_t *queue); + +/*! + * \brief Return number of tasks in worker queue. + */ +size_t worker_queue_length(worker_queue_t *queue); diff --git a/src/knot/zone/adds_tree.c b/src/knot/zone/adds_tree.c new file mode 100644 index 0000000..6376724 --- /dev/null +++ b/src/knot/zone/adds_tree.c @@ -0,0 +1,262 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <stdlib.h> + +#include "knot/zone/adds_tree.h" + +#include "libknot/dynarray.h" +#include "libknot/error.h" +#include "libknot/rrtype/rdname.h" + +knot_dynarray_declare(nodeptr, zone_node_t *, DYNARRAY_VISIBILITY_STATIC, 2) +knot_dynarray_define(nodeptr, zone_node_t *, DYNARRAY_VISIBILITY_STATIC) + +typedef struct { + nodeptr_dynarray_t array; + bool deduplicated; +} a_t_node_t; + +static int free_a_t_node(trie_val_t *val, void *null) +{ + assert(null == NULL); + a_t_node_t *nodes = *(a_t_node_t **)val; + nodeptr_dynarray_free(&nodes->array); + free(nodes); + return 0; +} + +void additionals_tree_free(additionals_tree_t *a_t) +{ + if (a_t != NULL) { + trie_apply(a_t, free_a_t_node, NULL); + trie_free(a_t); + } +} + +int zone_node_additionals_foreach(const zone_node_t *node, const knot_dname_t *zone_apex, + zone_node_additionals_cb_t cb, void *ctx) +{ + int ret = KNOT_EOK; + for (int i = 0; ret == KNOT_EOK && i < node->rrset_count; i++) { + struct rr_data *rr_data = &node->rrs[i]; + if (!knot_rrtype_additional_needed(rr_data->type)) { + continue; + } + knot_rdata_t *rdata = knot_rdataset_at(&rr_data->rrs, 0); + for (int j = 0; ret == KNOT_EOK && j < rr_data->rrs.count; j++) { + const knot_dname_t *name = knot_rdata_name(rdata, rr_data->type); + if (knot_dname_in_bailiwick(name, zone_apex) > 0) { + ret = cb(name, ctx); + } + rdata = knot_rdataset_next(rdata); + } + } + return ret; +} + +typedef struct { + additionals_tree_t *a_t; + zone_node_t *node; +} a_t_node_ctx_t; + +static int remove_node_from_a_t(const knot_dname_t *name, void *a_ctx) +{ + a_t_node_ctx_t *ctx = a_ctx; + + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(name, lf_storage); + + trie_val_t *val = trie_get_try(ctx->a_t, lf + 1, *lf); + if (val == NULL) { + return KNOT_EOK; + } + + a_t_node_t *nodes = *(a_t_node_t **)val; + if (nodes == NULL) { + trie_del(ctx->a_t, lf + 1, *lf, NULL); + return KNOT_EOK; + } + + nodeptr_dynarray_remove(&nodes->array, &ctx->node); + + if (nodes->array.size == 0) { + nodeptr_dynarray_free(&nodes->array); + free(nodes); + trie_del(ctx->a_t, lf + 1, *lf, NULL); + } + + return KNOT_EOK; +} + +static int add_node_to_a_t(const knot_dname_t *name, void *a_ctx) +{ + a_t_node_ctx_t *ctx = a_ctx; + + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(name, lf_storage); + + trie_val_t *val = trie_get_ins(ctx->a_t, lf + 1, *lf); + if (*val == NULL) { + *val = calloc(1, sizeof(a_t_node_t)); + if (*val == NULL) { + return KNOT_ENOMEM; + } + } + + a_t_node_t *nodes = *(a_t_node_t **)val; + nodeptr_dynarray_add(&nodes->array, &ctx->node); + nodes->deduplicated = false; + return KNOT_EOK; +} + +int additionals_tree_update_node(additionals_tree_t *a_t, const knot_dname_t *zone_apex, + zone_node_t *old_node, zone_node_t *new_node) +{ + a_t_node_ctx_t ctx = { a_t, 0 }; + int ret = KNOT_EOK; + + if (a_t == NULL || zone_apex == NULL) { + return KNOT_EINVAL; + } + + if (binode_additionals_unchanged(old_node, new_node)) { + return KNOT_EOK; + } + + // for every additional in old_node rrsets, remove mentioning of this node in tree + if (old_node != NULL && !(old_node->flags & NODE_FLAGS_DELETED)) { + ctx.node = binode_first(old_node); + ret = zone_node_additionals_foreach(old_node, zone_apex, remove_node_from_a_t, &ctx); + } + + // for every additional in new_node rrsets, add reverse link into the tree + if (new_node != NULL && !(new_node->flags & NODE_FLAGS_DELETED) && ret == KNOT_EOK) { + ctx.node = binode_first(new_node); + ret = zone_node_additionals_foreach(new_node, zone_apex, add_node_to_a_t, &ctx); + } + return ret; +} + +int additionals_tree_update_nsec3(additionals_tree_t *a_t, const zone_contents_t *zone, + zone_node_t *old_node, zone_node_t *new_node) +{ + if (!knot_is_nsec3_enabled(zone)) { + return KNOT_EOK; + } + bool oldex = (old_node != NULL && !(old_node->flags & NODE_FLAGS_DELETED)); + bool newex = (new_node != NULL && !(new_node->flags & NODE_FLAGS_DELETED)); + bool addn = (!oldex && newex), remn = (oldex && !newex); + if (!addn && !remn) { + return KNOT_EOK; + } + const knot_dname_t *nsec3_name = node_nsec3_hash(addn ? new_node : old_node, zone); + if (nsec3_name == NULL) { + return KNOT_ENOMEM; + } + a_t_node_ctx_t ctx = { a_t, addn ? binode_first(new_node) : binode_first(old_node) }; + return (addn ? add_node_to_a_t : remove_node_from_a_t)(nsec3_name, &ctx); +} + +int additionals_tree_from_zone(additionals_tree_t **a_t, const zone_contents_t *zone) +{ + *a_t = additionals_tree_new(); + if (*a_t == NULL) { + return KNOT_ENOMEM; + } + + bool do_nsec3 = knot_is_nsec3_enabled(zone); + + zone_tree_it_t it = { 0 }; + int ret = zone_tree_it_begin(zone->nodes, &it); + while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) { + ret = additionals_tree_update_node(*a_t, zone->apex->owner, NULL, zone_tree_it_val(&it)); + if (do_nsec3 && ret == KNOT_EOK) { + ret = additionals_tree_update_nsec3(*a_t, zone, + NULL, zone_tree_it_val(&it)); + } + zone_tree_it_next(&it); + } + zone_tree_it_free(&it); + + if (ret != KNOT_EOK) { + additionals_tree_free(*a_t); + *a_t = NULL; + } + return ret; +} + +int additionals_tree_update_from_binodes(additionals_tree_t *a_t, const zone_tree_t *tree, + const zone_contents_t *zone) +{ + zone_tree_it_t it = { 0 }; + int ret = zone_tree_it_begin((zone_tree_t *)tree, &it); + while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) { + zone_node_t *node = zone_tree_it_val(&it), *counter = binode_counterpart(node); + ret = additionals_tree_update_node(a_t, zone->apex->owner, counter, node); + if (ret == KNOT_EOK) { + ret = additionals_tree_update_nsec3(a_t, zone, counter, node); + } + zone_tree_it_next(&it); + } + zone_tree_it_free(&it); + return ret; +} + +int additionals_reverse_apply(additionals_tree_t *a_t, const knot_dname_t *name, + node_apply_cb_t cb, void *ctx) +{ + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(name, lf_storage); + + trie_val_t *val = trie_get_try(a_t, lf + 1, *lf); + if (val == NULL) { + return KNOT_EOK; + } + + a_t_node_t *nodes = *(a_t_node_t **)val; + if (nodes == NULL) { + return KNOT_EOK; + } + + if (!nodes->deduplicated) { + nodeptr_dynarray_sort_dedup(&nodes->array); + nodes->deduplicated = true; + } + + knot_dynarray_foreach(nodeptr, zone_node_t *, node_in_arr, nodes->array) { + int ret = cb(*node_in_arr, ctx); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +int additionals_reverse_apply_multi(additionals_tree_t *a_t, const zone_tree_t *tree, + node_apply_cb_t cb, void *ctx) +{ + zone_tree_it_t it = { 0 }; + int ret = zone_tree_it_begin((zone_tree_t *)tree, &it); + while (!zone_tree_it_finished(&it) && ret == KNOT_EOK) { + ret = additionals_reverse_apply(a_t, zone_tree_it_val(&it)->owner, cb, ctx); + zone_tree_it_next(&it); + } + zone_tree_it_free(&it); + return ret; +} diff --git a/src/knot/zone/adds_tree.h b/src/knot/zone/adds_tree.h new file mode 100644 index 0000000..386d43b --- /dev/null +++ b/src/knot/zone/adds_tree.h @@ -0,0 +1,120 @@ +/* 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 "contrib/qp-trie/trie.h" +#include "knot/zone/contents.h" +#include "knot/dnssec/zone-nsec.h" + +typedef trie_t additionals_tree_t; + +inline static additionals_tree_t *additionals_tree_new(void) { return trie_create(NULL); } +void additionals_tree_free(additionals_tree_t *a_t); + +/*! + * \brief Foreach additional in all node RRSets, do sth. + * + * \note This is not too related to additionals_tree, might be moved. + * + * \param node Zone node with possibly NS, MX, etc rrsets. + * \param zone_apex Name of the zone apex. + * \param cb Callback to be performed. + * \param ctx Arbitrary context for the callback. + * + * \return KNOT_E* + */ +typedef int (*zone_node_additionals_cb_t)(const knot_dname_t *additional, void *ctx); +int zone_node_additionals_foreach(const zone_node_t *node, const knot_dname_t *zone_apex, + zone_node_additionals_cb_t cb, void *ctx); + +/*! + * \brief Update additionals tree according to changed RRsets in a zone node. + * + * \param a_t Additionals tree to be updated. + * \param zone_apex Zone apex owner. + * \param old_node Old state of the node (additionals will be removed). + * \param new_node New state of the node (additionals will be added). + * + * \return KNOT_E* + */ +int additionals_tree_update_node(additionals_tree_t *a_t, const knot_dname_t *zone_apex, + zone_node_t *old_node, zone_node_t *new_node); + +/*! + * \brief Update additionals tree with NSEC3 according to changed normal nodes. + * + * \param a_t Additionals tree to be updated. + * \param zone Zone contents with NSEC3PARAMS etc. + * \param old_node Old state of the node. + * \param new_node New state of the node. + * + * \return KNOT_E* + */ +int additionals_tree_update_nsec3(additionals_tree_t *a_t, const zone_contents_t *zone, + zone_node_t *old_node, zone_node_t *new_node); + +/*! + * \brief Create additionals tree from a zone (by scanning all additionals in zone RRsets). + * + * \param a_t Out: additionals tree to be created (NULL if error). + * \param zone Zone contents. + * + * \return KNOT_E* + */ +int additionals_tree_from_zone(additionals_tree_t **a_t, const zone_contents_t *zone); + +/*! + * \brief Update additionals tree according to changed RRsets in all nodes in a zone tree. + * + * \param a_t Additionals tree to be updated. + * \param tree Zone tree containing updated nodes as bi-nodes. + * \param zone Whole zone with some additional info. + * + * \return KNOT_E* + */ +int additionals_tree_update_from_binodes(additionals_tree_t *a_t, const zone_tree_t *tree, + const zone_contents_t *zone); + +/*! + * \brief Foreach node that has specified name in its additionals, do sth. + * + * \note The node passed to the callback might not be correct part of bi-node! + * + * \param a_t Additionals reverse tree. + * \param name Name to be looked up in the additionals. + * \param cb Callback to be called. + * \param ctx Arbitrary context for the callback. + * + * \return KNOT_E* + */ +typedef int (*node_apply_cb_t)(zone_node_t *node, void *ctx); +int additionals_reverse_apply(additionals_tree_t *a_t, const knot_dname_t *name, + node_apply_cb_t cb, void *ctx); + +/*! + * \brief Call additionals_reverse_apply() for every name in specified tree. + * + * \param a_t Additionals reverse tree. + * \param tree Zone tree with names to be looked up in additionals. + * \param cb Callback to be called for each affected node. + * \param ctx Arbitrary context for the callback. + * + * \return KNOT_E* + */ +int additionals_reverse_apply_multi(additionals_tree_t *a_t, const zone_tree_t *tree, + node_apply_cb_t cb, void *ctx); + diff --git a/src/knot/zone/adjust.c b/src/knot/zone/adjust.c new file mode 100644 index 0000000..1e014a6 --- /dev/null +++ b/src/knot/zone/adjust.c @@ -0,0 +1,628 @@ +/* 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/zone/adjust.h" +#include "knot/common/log.h" +#include "knot/dnssec/zone-nsec.h" +#include "knot/zone/adds_tree.h" +#include "knot/zone/measure.h" +#include "libdnssec/error.h" + +static bool node_non_dnssec_exists(const zone_node_t *node) +{ + assert(node); + + for (uint16_t i = 0; i < node->rrset_count; ++i) { + switch (node->rrs[i].type) { + case KNOT_RRTYPE_NSEC: + case KNOT_RRTYPE_NSEC3: + case KNOT_RRTYPE_RRSIG: + continue; + default: + return true; + } + } + + return false; +} + +int adjust_cb_flags(zone_node_t *node, adjust_ctx_t *ctx) +{ + zone_node_t *parent = node_parent(node); + uint16_t flags_orig = node->flags; + bool set_subt_auth = false; + bool has_data = node_non_dnssec_exists(node); + + assert(!(node->flags & NODE_FLAGS_DELETED)); + + node->flags &= ~(NODE_FLAGS_DELEG | NODE_FLAGS_NONAUTH | NODE_FLAGS_SUBTREE_AUTH | NODE_FLAGS_SUBTREE_DATA); + + if (parent && (parent->flags & NODE_FLAGS_DELEG || parent->flags & NODE_FLAGS_NONAUTH)) { + node->flags |= NODE_FLAGS_NONAUTH; + } else if (node_rrtype_exists(node, KNOT_RRTYPE_NS) && node != ctx->zone->apex) { + node->flags |= NODE_FLAGS_DELEG; + if (node_rrtype_exists(node, KNOT_RRTYPE_DS)) { + set_subt_auth = true; + } + } else if (has_data) { + set_subt_auth = true; + } + + if (set_subt_auth) { + node_set_flag_hierarch(node, NODE_FLAGS_SUBTREE_AUTH); + } + if (has_data) { + node_set_flag_hierarch(node, NODE_FLAGS_SUBTREE_DATA); + } + + if (node->flags != flags_orig && ctx->changed_nodes != NULL) { + return zone_tree_insert(ctx->changed_nodes, &node); + } + + return KNOT_EOK; +} + +int unadjust_cb_point_to_nsec3(zone_node_t *node, adjust_ctx_t *ctx) +{ + // downgrade the NSEC3 node pointer to NSEC3 name + if (node->flags & NODE_FLAGS_NSEC3_NODE) { + node->nsec3_hash = knot_dname_copy(node->nsec3_node->owner, NULL); + node->flags &= ~NODE_FLAGS_NSEC3_NODE; + } + assert(ctx->changed_nodes == NULL); + return KNOT_EOK; +} + +int adjust_cb_wildcard_nsec3(zone_node_t *node, adjust_ctx_t *ctx) +{ + if (!knot_is_nsec3_enabled(ctx->zone)) { + if (node->nsec3_wildcard_name != NULL && ctx->changed_nodes != NULL) { + zone_tree_insert(ctx->changed_nodes, &node); + } + node->nsec3_wildcard_name = NULL; + return KNOT_EOK; + } + + if (ctx->nsec3_param_changed) { + node->nsec3_wildcard_name = NULL; + } + + if (node->nsec3_wildcard_name != NULL) { + return KNOT_EOK; + } + + size_t wildcard_size = knot_dname_size(node->owner) + 2; + size_t wildcard_nsec3 = zone_nsec3_name_len(ctx->zone); + if (wildcard_size > KNOT_DNAME_MAXLEN) { + return KNOT_EOK; + } + + node->nsec3_wildcard_name = malloc(wildcard_nsec3); + if (node->nsec3_wildcard_name == NULL) { + return KNOT_ENOMEM; + } + + if (ctx->changed_nodes != NULL) { + zone_tree_insert(ctx->changed_nodes, &node); + } + + knot_dname_t wildcard[wildcard_size]; + assert(wildcard_size > 2); + memcpy(wildcard, "\x01""*", 2); + memcpy(wildcard + 2, node->owner, wildcard_size - 2); + return knot_create_nsec3_owner(node->nsec3_wildcard_name, wildcard_nsec3, + wildcard, ctx->zone->apex->owner, &ctx->zone->nsec3_params); +} + +static bool nsec3_params_match(const knot_rdataset_t *rrs, + const dnssec_nsec3_params_t *params, + size_t rdata_pos) +{ + assert(rrs != NULL); + assert(params != NULL); + + knot_rdata_t *rdata = knot_rdataset_at(rrs, rdata_pos); + + return (knot_nsec3_alg(rdata) == params->algorithm + && knot_nsec3_iters(rdata) == params->iterations + && knot_nsec3_salt_len(rdata) == params->salt.size + && memcmp(knot_nsec3_salt(rdata), params->salt.data, + params->salt.size) == 0); +} + +int adjust_cb_nsec3_flags(zone_node_t *node, adjust_ctx_t *ctx) +{ + uint16_t flags_orig = node->flags; + + // check if this node belongs to correct chain + node->flags &= ~NODE_FLAGS_IN_NSEC3_CHAIN; + const knot_rdataset_t *nsec3_rrs = node_rdataset(node, KNOT_RRTYPE_NSEC3); + for (uint16_t i = 0; nsec3_rrs != NULL && i < nsec3_rrs->count; i++) { + if (nsec3_params_match(nsec3_rrs, &ctx->zone->nsec3_params, i)) { + node->flags |= NODE_FLAGS_IN_NSEC3_CHAIN; + } + } + + if (node->flags != flags_orig && ctx->changed_nodes != NULL) { + return zone_tree_insert(ctx->changed_nodes, &node); + } + + return KNOT_EOK; +} + +int adjust_cb_nsec3_pointer(zone_node_t *node, adjust_ctx_t *ctx) +{ + uint16_t flags_orig = node->flags; + zone_node_t *ptr_orig = node->nsec3_node; + int ret = KNOT_EOK; + if (ctx->nsec3_param_changed) { + if (!(node->flags & NODE_FLAGS_NSEC3_NODE) && + node->nsec3_hash != binode_counterpart(node)->nsec3_hash) { + free(node->nsec3_hash); + } + node->nsec3_hash = NULL; + node->flags &= ~NODE_FLAGS_NSEC3_NODE; + (void)node_nsec3_node(node, ctx->zone); + } else { + ret = binode_fix_nsec3_pointer(node, ctx->zone); + } + if (ret == KNOT_EOK && ctx->changed_nodes != NULL && + (flags_orig != node->flags || ptr_orig != node->nsec3_node)) { + ret = zone_tree_insert(ctx->changed_nodes, &node); + } + return ret; +} + +/*! \brief Link pointers to additional nodes for this RRSet. */ +static int discover_additionals(zone_node_t *adjn, uint16_t rr_at, + adjust_ctx_t *ctx) +{ + struct rr_data *rr_data = &adjn->rrs[rr_at]; + assert(rr_data != NULL); + + const knot_rdataset_t *rrs = &rr_data->rrs; + knot_rdata_t *rdata = knot_rdataset_at(rrs, 0); + uint16_t rdcount = rrs->count; + + uint16_t mandatory_count = 0; + uint16_t others_count = 0; + glue_t mandatory[rdcount]; + glue_t others[rdcount]; + + /* Scan new additional nodes. */ + for (uint16_t i = 0; i < rdcount; i++) { + const knot_dname_t *dname = knot_rdata_name(rdata, rr_data->type); + const zone_node_t *node = NULL; + + if (!zone_contents_find_node_or_wildcard(ctx->zone, dname, &node)) { + rdata = knot_rdataset_next(rdata); + continue; + } + + glue_t *glue; + if ((node->flags & (NODE_FLAGS_DELEG | NODE_FLAGS_NONAUTH)) && + rr_data->type == KNOT_RRTYPE_NS && + knot_dname_in_bailiwick(node->owner, adjn->owner) >= 0) { + glue = &mandatory[mandatory_count++]; + glue->optional = false; + } else { + glue = &others[others_count++]; + glue->optional = true; + } + glue->node = node; + glue->ns_pos = i; + rdata = knot_rdataset_next(rdata); + } + + /* Store sorted additionals by the type, mandatory first. */ + size_t total_count = mandatory_count + others_count; + additional_t *new_addit = NULL; + if (total_count > 0) { + new_addit = malloc(sizeof(additional_t)); + if (new_addit == NULL) { + return KNOT_ENOMEM; + } + new_addit->count = total_count; + + size_t size = total_count * sizeof(glue_t); + new_addit->glues = malloc(size); + if (new_addit->glues == NULL) { + free(new_addit); + return KNOT_ENOMEM; + } + + size_t mandatory_size = mandatory_count * sizeof(glue_t); + memcpy(new_addit->glues, mandatory, mandatory_size); + memcpy(new_addit->glues + mandatory_count, others, + size - mandatory_size); + } + + /* If the result differs, shallow copy node and store additionals. */ + if (!additional_equal(rr_data->additional, new_addit)) { + if (ctx->changed_nodes != NULL) { + zone_tree_insert(ctx->changed_nodes, &adjn); + } + + if (!binode_additional_shared(adjn, adjn->rrs[rr_at].type)) { + // this happens when additionals are adjusted twice during one update, e.g. IXFR-from-diff + additional_clear(adjn->rrs[rr_at].additional); + } + + int ret = binode_prepare_change(adjn, NULL); + if (ret != KNOT_EOK) { + return ret; + } + rr_data = &adjn->rrs[rr_at]; + + rr_data->additional = new_addit; + } else { + additional_clear(new_addit); + } + + return KNOT_EOK; +} + +int adjust_cb_additionals(zone_node_t *node, adjust_ctx_t *ctx) +{ + /* Lookup additional records for specific nodes. */ + for(uint16_t i = 0; i < node->rrset_count; ++i) { + struct rr_data *rr_data = &node->rrs[i]; + if (knot_rrtype_additional_needed(rr_data->type)) { + int ret = discover_additionals(node, i, ctx); + if (ret != KNOT_EOK) { + return ret; + } + } + } + return KNOT_EOK; +} + +int adjust_cb_flags_and_nsec3(zone_node_t *node, adjust_ctx_t *ctx) +{ + int ret = adjust_cb_flags(node, ctx); + if (ret == KNOT_EOK) { + ret = adjust_cb_nsec3_pointer(node, ctx); + } + return ret; +} + +int adjust_cb_nsec3_and_additionals(zone_node_t *node, adjust_ctx_t *ctx) +{ + int ret = adjust_cb_nsec3_pointer(node, ctx); + if (ret == KNOT_EOK) { + ret = adjust_cb_wildcard_nsec3(node, ctx); + } + if (ret == KNOT_EOK) { + ret = adjust_cb_additionals(node, ctx); + } + return ret; +} + +int adjust_cb_nsec3_and_wildcard(zone_node_t *node, adjust_ctx_t *ctx) +{ + int ret = adjust_cb_wildcard_nsec3(node, ctx); + if (ret == KNOT_EOK) { + ret = adjust_cb_nsec3_pointer(node, ctx); + } + return ret; +} + +int adjust_cb_void(_unused_ zone_node_t *node, _unused_ adjust_ctx_t *ctx) +{ + return KNOT_EOK; +} + +typedef struct { + zone_node_t *first_node; + adjust_ctx_t ctx; + zone_node_t *previous_node; + adjust_cb_t adjust_cb; + bool adjust_prevs; + measure_t *m; + + // just for parallel + unsigned threads; + unsigned thr_id; + size_t i; + pthread_t thread; + int ret; + zone_tree_t *tree; +} zone_adjust_arg_t; + +static int adjust_single(zone_node_t *node, void *data) +{ + assert(node != NULL); + assert(data != NULL); + + zone_adjust_arg_t *args = (zone_adjust_arg_t *)data; + + // parallel adjust support + if (args->threads > 1) { + if (args->i++ % args->threads != args->thr_id) { + return KNOT_EOK; + } + } + + if (args->m != NULL) { + knot_measure_node(node, args->m); + } + + if ((node->flags & NODE_FLAGS_DELETED)) { + return KNOT_EOK; + } + + // remember first node + if (args->first_node == NULL) { + args->first_node = node; + } + + // set pointer to previous node + if (args->adjust_prevs && args->previous_node != NULL && + node->prev != args->previous_node && + node->prev != binode_counterpart(args->previous_node)) { + zone_tree_insert(args->ctx.changed_nodes, &node); + node->prev = args->previous_node; + } + + // update remembered previous pointer only if authoritative + if (!(node->flags & NODE_FLAGS_NONAUTH) && node->rrset_count > 0) { + args->previous_node = node; + } + + return args->adjust_cb(node, &args->ctx); +} + +static int zone_adjust_tree(zone_tree_t *tree, adjust_ctx_t *ctx, adjust_cb_t adjust_cb, + bool adjust_prevs, measure_t *measure_ctx) +{ + if (zone_tree_is_empty(tree)) { + return KNOT_EOK; + } + + zone_adjust_arg_t arg = { 0 }; + arg.ctx = *ctx; + arg.adjust_cb = adjust_cb; + arg.adjust_prevs = adjust_prevs; + arg.m = measure_ctx; + + int ret = zone_tree_apply(tree, adjust_single, &arg); + if (ret != KNOT_EOK) { + return ret; + } + + if (adjust_prevs && arg.first_node != NULL) { + zone_tree_insert(ctx->changed_nodes, &arg.first_node); + arg.first_node->prev = arg.previous_node; + } + + return KNOT_EOK; +} + +static void *adjust_tree_thread(void *ctx) +{ + zone_adjust_arg_t *arg = ctx; + + arg->ret = zone_tree_apply(arg->tree, adjust_single, ctx); + + return NULL; +} + +static int zone_adjust_tree_parallel(zone_tree_t *tree, adjust_ctx_t *ctx, + adjust_cb_t adjust_cb, unsigned threads) +{ + if (zone_tree_is_empty(tree)) { + return KNOT_EOK; + } + + zone_adjust_arg_t args[threads]; + memset(args, 0, sizeof(args)); + int ret = KNOT_EOK; + + for (unsigned i = 0; i < threads; i++) { + args[i].first_node = NULL; + args[i].ctx = *ctx; + args[i].adjust_cb = adjust_cb; + args[i].adjust_prevs = false; + args[i].m = NULL; + args[i].tree = tree; + args[i].threads = threads; + args[i].i = 0; + args[i].thr_id = i; + args[i].ret = -1; + if (ctx->changed_nodes != NULL) { + args[i].ctx.changed_nodes = zone_tree_create(true); + if (args[i].ctx.changed_nodes == NULL) { + ret = KNOT_ENOMEM; + break; + } + args[i].ctx.changed_nodes->flags = tree->flags; + } + } + if (ret != KNOT_EOK) { + for (unsigned i = 0; i < threads; i++) { + zone_tree_free(&args[i].ctx.changed_nodes); + } + return ret; + } + + for (unsigned i = 0; i < threads; i++) { + args[i].ret = pthread_create(&args[i].thread, NULL, adjust_tree_thread, &args[i]); + } + + for (unsigned i = 0; i < threads; i++) { + if (args[i].ret == 0) { + args[i].ret = pthread_join(args[i].thread, NULL); + } + if (args[i].ret != 0) { + ret = knot_map_errno_code(args[i].ret); + } + if (ret == KNOT_EOK && ctx->changed_nodes != NULL) { + ret = zone_tree_merge(ctx->changed_nodes, args[i].ctx.changed_nodes); + } + zone_tree_free(&args[i].ctx.changed_nodes); + } + + return ret; +} + +int zone_adjust_contents(zone_contents_t *zone, adjust_cb_t nodes_cb, adjust_cb_t nsec3_cb, + bool measure_zone, bool adjust_prevs, unsigned threads, + zone_tree_t *add_changed) +{ + int ret = zone_contents_load_nsec3param(zone); + if (ret != KNOT_EOK) { + log_zone_error(zone->apex->owner, + "failed to load NSEC3 parameters (%s)", + knot_strerror(ret)); + return ret; + } + zone->dnssec = node_rrtype_is_signed(zone->apex, KNOT_RRTYPE_SOA); + + measure_t m = knot_measure_init(measure_zone, false); + adjust_ctx_t ctx = { zone, add_changed, true }; + + if (threads > 1) { + assert(nodes_cb != adjust_cb_flags); // This cb demands parent to be adjusted before child + // => required sequential adjusting (also true for + // adjust_cb_flags_and_nsec3) !! + assert(!measure_zone); + assert(!adjust_prevs); + if (nsec3_cb != NULL) { + ret = zone_adjust_tree_parallel(zone->nsec3_nodes, &ctx, nsec3_cb, threads); + } + if (ret == KNOT_EOK && nodes_cb != NULL) { + ret = zone_adjust_tree_parallel(zone->nodes, &ctx, nodes_cb, threads); + } + } else { + if (nsec3_cb != NULL) { + ret = zone_adjust_tree(zone->nsec3_nodes, &ctx, nsec3_cb, adjust_prevs, &m); + } + if (ret == KNOT_EOK && nodes_cb != NULL) { + ret = zone_adjust_tree(zone->nodes, &ctx, nodes_cb, adjust_prevs, &m); + } + } + + if (ret == KNOT_EOK && measure_zone && nodes_cb != NULL && nsec3_cb != NULL) { + knot_measure_finish_zone(&m, zone); + } + return ret; +} + +int zone_adjust_update(zone_update_t *update, adjust_cb_t nodes_cb, adjust_cb_t nsec3_cb, bool measure_diff) +{ + int ret = KNOT_EOK; + measure_t m = knot_measure_init(false, measure_diff); + adjust_ctx_t ctx = { update->new_cont, update->a_ctx->adjust_ptrs, zone_update_changed_nsec3param(update) }; + + if (nsec3_cb != NULL) { + ret = zone_adjust_tree(update->a_ctx->nsec3_ptrs, &ctx, nsec3_cb, false, &m); + } + if (ret == KNOT_EOK && nodes_cb != NULL) { + ret = zone_adjust_tree(update->a_ctx->node_ptrs, &ctx, nodes_cb, false, &m); + } + if (ret == KNOT_EOK && measure_diff && nodes_cb != NULL && nsec3_cb != NULL) { + knot_measure_finish_update(&m, update); + } + return ret; +} + +int zone_adjust_full(zone_contents_t *zone, unsigned threads) +{ + int ret = zone_adjust_contents(zone, adjust_cb_flags, adjust_cb_nsec3_flags, + true, true, 1, NULL); + if (ret == KNOT_EOK) { + ret = zone_adjust_contents(zone, adjust_cb_nsec3_and_additionals, NULL, + false, false, threads, NULL); + } + if (ret == KNOT_EOK) { + additionals_tree_free(zone->adds_tree); + ret = additionals_tree_from_zone(&zone->adds_tree, zone); + } + return ret; +} + +static int adjust_additionals_cb(zone_node_t *node, void *ctx) +{ + adjust_ctx_t *actx = ctx; + zone_node_t *real_node = zone_tree_fix_get(node, actx->zone->nodes); + return adjust_cb_additionals(real_node, actx); +} + +static int adjust_point_to_nsec3_cb(zone_node_t *node, void *ctx) +{ + adjust_ctx_t *actx = ctx; + zone_node_t *real_node = zone_tree_fix_get(node, actx->zone->nodes); + return adjust_cb_nsec3_pointer(real_node, actx); +} + +int zone_adjust_incremental_update(zone_update_t *update, unsigned threads) +{ + int ret = zone_contents_load_nsec3param(update->new_cont); + if (ret != KNOT_EOK) { + return ret; + } + bool nsec3change = zone_update_changed_nsec3param(update); + adjust_ctx_t ctx = { update->new_cont, update->a_ctx->adjust_ptrs, nsec3change }; + + ret = zone_adjust_contents(update->new_cont, adjust_cb_flags, adjust_cb_nsec3_flags, + false, true, 1, update->a_ctx->adjust_ptrs); + if (ret == KNOT_EOK) { + if (nsec3change) { + ret = zone_adjust_contents(update->new_cont, adjust_cb_nsec3_and_wildcard, NULL, + false, false, threads, update->a_ctx->adjust_ptrs); + if (ret == KNOT_EOK) { + // just measure zone size + ret = zone_adjust_update(update, adjust_cb_void, adjust_cb_void, true); + } + } else { + ret = zone_adjust_update(update, adjust_cb_wildcard_nsec3, adjust_cb_void, true); + } + } + if (ret == KNOT_EOK) { + if (update->new_cont->adds_tree != NULL && !nsec3change) { + ret = additionals_tree_update_from_binodes( + update->new_cont->adds_tree, + update->a_ctx->node_ptrs, + update->new_cont + ); + } else { + additionals_tree_free(update->new_cont->adds_tree); + ret = additionals_tree_from_zone(&update->new_cont->adds_tree, update->new_cont); + } + } + if (ret == KNOT_EOK) { + ret = additionals_reverse_apply_multi( + update->new_cont->adds_tree, + update->a_ctx->node_ptrs, + adjust_additionals_cb, + &ctx + ); + } + if (ret == KNOT_EOK) { + ret = zone_adjust_update(update, adjust_cb_additionals, adjust_cb_void, false); + } + if (ret == KNOT_EOK) { + if (!nsec3change) { + ret = additionals_reverse_apply_multi( + update->new_cont->adds_tree, + update->a_ctx->nsec3_ptrs, + adjust_point_to_nsec3_cb, + &ctx + ); + } + } + return ret; +} diff --git a/src/knot/zone/adjust.h b/src/knot/zone/adjust.h new file mode 100644 index 0000000..5828e5a --- /dev/null +++ b/src/knot/zone/adjust.h @@ -0,0 +1,123 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/zone/contents.h" +#include "knot/updates/zone-update.h" + +typedef struct { + const zone_contents_t *zone; + zone_tree_t *changed_nodes; + bool nsec3_param_changed; +} adjust_ctx_t; + +typedef int (*adjust_cb_t)(zone_node_t *, adjust_ctx_t *); + +/* + * \brief Various callbacks for adjusting zone node's params and pointers. + * + * \param node Node to be adjusted. Must be already inside the zone contents! + * \param zone Zone being adjusted. + * + * \return KNOT_E* + */ + +// fix NORMAL node flags, like NODE_FLAGS_NONAUTH, NODE_FLAGS_DELEG etc. +int adjust_cb_flags(zone_node_t *node, adjust_ctx_t *ctx); + +// reset pointer to NSEC3 node +int unadjust_cb_point_to_nsec3(zone_node_t *node, adjust_ctx_t *ctx); + +// fix NORMAL node pointer to NSEC3 node proving nonexistence of wildcard +int adjust_cb_wildcard_nsec3(zone_node_t *node, adjust_ctx_t *ctx); + +// fix NSEC3 node flags: NODE_FLAGS_IN_NSEC3_CHAIN +int adjust_cb_nsec3_flags(zone_node_t *node, adjust_ctx_t *ctx); + +// fix pointer at corresponding NSEC3 node +int adjust_cb_nsec3_pointer(zone_node_t *node, adjust_ctx_t *ctx); + +// fix NORMAL node flags to additionals, like NS records and glue... +int adjust_cb_additionals(zone_node_t *node, adjust_ctx_t *ctx); + +// adjust_cb_flags and adjust_cb_nsec3_pointer at once +int adjust_cb_flags_and_nsec3(zone_node_t *node, adjust_ctx_t *ctx); + +// adjust_cb_nsec3_pointer, adjust_cb_wildcard_nsec3 and adjust_cb_additionals at once +int adjust_cb_nsec3_and_additionals(zone_node_t *node, adjust_ctx_t *ctx); + +// adjust_cb_wildcard_nsec3 and adjust_cb_nsec3_pointer at once +int adjust_cb_nsec3_and_wildcard(zone_node_t *node, adjust_ctx_t *ctx); + +// dummy callback, just make prev pointers adjusting and zone size measuring work +int adjust_cb_void(zone_node_t *node, adjust_ctx_t *ctx); + +/*! + * \brief Apply callback to NSEC3 and NORMAL nodes. Fix PREV pointers and measure zone size. + * + * \param zone Zone to be adjusted. + * \param nodes_cb Callback for NORMAL nodes. + * \param nsec3_cb Callback for NSEC3 nodes. + * \param measure_zone While adjusting, count the size and max TTL of the zone. + * \param adjust_prevs Also (re-)generate node->prev pointers. + * \param threads Operate in parallel using specified threads. + * \param add_changed Special tree to add any changed node (by adjusting) into. + * + * \return KNOT_E* + */ +int zone_adjust_contents(zone_contents_t *zone, adjust_cb_t nodes_cb, adjust_cb_t nsec3_cb, + bool measure_zone, bool adjust_prevs, unsigned threads, + zone_tree_t *add_changed); + +/*! + * \brief Apply callback to nodes affected by the zone update. + * + * \note Fixing PREV pointers and zone measurement does not make sense since we are not + * iterating over whole zone. The same applies for callback that reference other + * (unchanged, but indirectly affected) zone nodes. + * + * \param update Zone update being finalized. + * \param nodes_cb Callback for NORMAL nodes. + * \param nsec3_cb Callback for NSEC3 nodes. + * \param measure_diff While adjusting, count the size difference and max TTL change. + * + * \return KNOT_E* + */ +int zone_adjust_update(zone_update_t *update, adjust_cb_t nodes_cb, adjust_cb_t nsec3_cb, bool measure_diff); + +/*! + * \brief Do a general-purpose full update. + * + * This operates in two phases, first fix basic node flags and prev pointers, + * than nsec3-related pointers and additionals. + * + * \param zone Zone to be adjusted. + * \param threads Parallelize some adjusting using specified threads. + * + * \return KNOT_E* + */ +int zone_adjust_full(zone_contents_t *zone, unsigned threads); + +/*! + * \brief Do a generally approved adjust after incremental update. + * + * \param update Zone update to be adjusted incrementally. + * \param threads Parallelize some adjusting using specified threads. + * + * \return KNOT_E* + */ +int zone_adjust_incremental_update(zone_update_t *update, unsigned threads); diff --git a/src/knot/zone/backup.c b/src/knot/zone/backup.c new file mode 100644 index 0000000..704f2e2 --- /dev/null +++ b/src/knot/zone/backup.c @@ -0,0 +1,461 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <unistd.h> + +#include "knot/zone/backup.h" + +#include "contrib/files.h" +#include "contrib/getline.h" +#include "contrib/macros.h" +#include "contrib/string.h" +#include "knot/catalog/catalog_db.h" +#include "knot/common/log.h" +#include "knot/dnssec/kasp/kasp_zone.h" +#include "knot/dnssec/kasp/keystore.h" +#include "knot/journal/journal_metadata.h" +#include "knot/zone/backup_dir.h" +#include "knot/zone/zonefile.h" +#include "libdnssec/error.h" + +// Current backup format version for output. Don't decrease it. +#define BACKUP_VERSION BACKUP_FORMAT_2 // Starting with release 3.1.0. + +static void _backup_swap(zone_backup_ctx_t *ctx, void **local, void **remote) +{ + if (ctx->restore_mode) { + void *temp = *local; + *local = *remote; + *remote = temp; + } +} + +#define BACKUP_SWAP(ctx, from, to) _backup_swap((ctx), (void **)&(from), (void **)&(to)) + +int zone_backup_init(bool restore_mode, bool forced, const char *backup_dir, + size_t kasp_db_size, size_t timer_db_size, size_t journal_db_size, + size_t catalog_db_size, zone_backup_ctx_t **out_ctx) +{ + if (backup_dir == NULL || out_ctx == NULL) { + return KNOT_EINVAL; + } + + size_t backup_dir_len = strlen(backup_dir) + 1; + + zone_backup_ctx_t *ctx = malloc(sizeof(*ctx) + backup_dir_len); + if (ctx == NULL) { + return KNOT_ENOMEM; + } + ctx->restore_mode = restore_mode; + ctx->forced = forced; + ctx->backup_format = BACKUP_VERSION; + ctx->backup_global = false; + ctx->readers = 1; + ctx->failed = false; + ctx->init_time = time(NULL); + ctx->zone_count = 0; + ctx->backup_dir = (char *)(ctx + 1); + memcpy(ctx->backup_dir, backup_dir, backup_dir_len); + + // Backup directory, lock file, label file. + // In restore, set the backup format. + int ret = backupdir_init(ctx); + if (ret != KNOT_EOK) { + free(ctx); + return ret; + } + + pthread_mutex_init(&ctx->readers_mutex, NULL); + + char db_dir[backup_dir_len + 16]; + (void)snprintf(db_dir, sizeof(db_dir), "%s/keys", backup_dir); + knot_lmdb_init(&ctx->bck_kasp_db, db_dir, kasp_db_size, 0, "keys_db"); + + (void)snprintf(db_dir, sizeof(db_dir), "%s/timers", backup_dir); + knot_lmdb_init(&ctx->bck_timer_db, db_dir, timer_db_size, 0, NULL); + + (void)snprintf(db_dir, sizeof(db_dir), "%s/journal", backup_dir); + knot_lmdb_init(&ctx->bck_journal, db_dir, journal_db_size, 0, NULL); + + (void)snprintf(db_dir, sizeof(db_dir), "%s/catalog", backup_dir); + knot_lmdb_init(&ctx->bck_catalog, db_dir, catalog_db_size, 0, NULL); + + *out_ctx = ctx; + return KNOT_EOK; +} + +int zone_backup_deinit(zone_backup_ctx_t *ctx) +{ + if (ctx == NULL) { + return KNOT_ENOENT; + } + + int ret = KNOT_EOK; + + pthread_mutex_lock(&ctx->readers_mutex); + assert(ctx->readers > 0); + size_t left = --ctx->readers; + pthread_mutex_unlock(&ctx->readers_mutex); + + if (left == 0) { + knot_lmdb_deinit(&ctx->bck_catalog); + knot_lmdb_deinit(&ctx->bck_journal); + knot_lmdb_deinit(&ctx->bck_timer_db); + knot_lmdb_deinit(&ctx->bck_kasp_db); + pthread_mutex_destroy(&ctx->readers_mutex); + + ret = backupdir_deinit(ctx); + zone_backups_rem(ctx); + free(ctx); + } + + return ret; +} + +void zone_backups_init(zone_backup_ctxs_t *ctxs) +{ + init_list(&ctxs->ctxs); + pthread_mutex_init(&ctxs->mutex, NULL); +} + +void zone_backups_deinit(zone_backup_ctxs_t *ctxs) +{ + zone_backup_ctx_t *ctx, *nxt; + WALK_LIST_DELSAFE(ctx, nxt, ctxs->ctxs) { + log_warning("backup to '%s' in progress, terminating, will be incomplete", + ctx->backup_dir); + ctx->readers = 1; // ensure full deinit + ctx->failed = true; + (void)zone_backup_deinit(ctx); + } + pthread_mutex_destroy(&ctxs->mutex); +} + +void zone_backups_add(zone_backup_ctxs_t *ctxs, zone_backup_ctx_t *ctx) +{ + pthread_mutex_lock(&ctxs->mutex); + add_tail(&ctxs->ctxs, (node_t *)ctx); + pthread_mutex_unlock(&ctxs->mutex); +} + +static zone_backup_ctxs_t *get_ctxs_trick(zone_backup_ctx_t *ctx) +{ + node_t *n = (node_t *)ctx; + while (n->prev != NULL) { + n = n->prev; + } + return (zone_backup_ctxs_t *)n; +} + +void zone_backups_rem(zone_backup_ctx_t *ctx) +{ + zone_backup_ctxs_t *ctxs = get_ctxs_trick(ctx); + pthread_mutex_lock(&ctxs->mutex); + rem_node((node_t *)ctx); + pthread_mutex_unlock(&ctxs->mutex); +} + +static char *dir_file(const char *dir_name, const char *file_name) +{ + const char *basename = strrchr(file_name, '/'); + if (basename == NULL) { + basename = file_name; + } else { + basename++; + } + + return sprintf_alloc("%s/%s", dir_name, basename); +} + +static int backup_key(key_params_t *parm, dnssec_keystore_t *from, dnssec_keystore_t *to) +{ + dnssec_key_t *key = NULL; + int ret = dnssec_key_new(&key); + if (ret != DNSSEC_EOK) { + return knot_error_from_libdnssec(ret); + } + dnssec_key_set_algorithm(key, parm->algorithm); + + ret = dnssec_keystore_get_private(from, parm->id, key); + if (ret == DNSSEC_EOK) { + ret = dnssec_keystore_set_private(to, key); + } + + dnssec_key_free(key); + return knot_error_from_libdnssec(ret); +} + +static conf_val_t get_zone_policy(conf_t *conf, const knot_dname_t *zone) +{ + conf_val_t policy; + + // Global modules don't use DNSSEC policy so check zone modules only. + conf_val_t modules = conf_zone_get(conf, C_MODULE, zone); + while (modules.code == KNOT_EOK) { + conf_mod_id_t *mod_id = conf_mod_id(&modules); + if (mod_id != NULL && strcmp(mod_id->name + 1, "mod-onlinesign") == 0) { + policy = conf_mod_get(conf, C_POLICY, mod_id); + conf_id_fix_default(&policy); + conf_free_mod_id(mod_id); + return policy; + } + conf_free_mod_id(mod_id); + conf_val_next(&modules); + } + + // Use default policy if none is configured. + policy = conf_zone_get(conf, C_DNSSEC_POLICY, zone); + conf_id_fix_default(&policy); + return policy; +} + +#define LOG_FAIL(action) log_zone_warning(zone->name, "%s, %s failed (%s)", ctx->restore_mode ? \ + "restore" : "backup", (action), knot_strerror(ret)) +#define LOG_MARK_FAIL(action) LOG_FAIL(action); \ + ctx->failed = true + +#define ABORT_IF_ENOMEM(param) if (param == NULL) { \ + ret = KNOT_ENOMEM; \ + goto done; \ + } + +static int backup_zonefile(conf_t *conf, zone_t *zone, zone_backup_ctx_t *ctx) +{ + int ret = KNOT_EOK; + + char *local_zf = conf_zonefile(conf, zone->name); + char *backup_zfiles_dir = NULL, *backup_zf = NULL, *zone_name_str; + + switch (ctx->backup_format) { + case BACKUP_FORMAT_1: + backup_zf = dir_file(ctx->backup_dir, local_zf); + ABORT_IF_ENOMEM(backup_zf); + break; + case BACKUP_FORMAT_2: + default: + backup_zfiles_dir = dir_file(ctx->backup_dir, "zonefiles"); + ABORT_IF_ENOMEM(backup_zfiles_dir); + zone_name_str = knot_dname_to_str_alloc(zone->name); + ABORT_IF_ENOMEM(zone_name_str); + backup_zf = sprintf_alloc("%s/%szone", backup_zfiles_dir, zone_name_str); + free(zone_name_str); + ABORT_IF_ENOMEM(backup_zf); + } + + if (ctx->restore_mode) { + struct stat st; + if (stat(backup_zf, &st) == 0) { + ret = make_path(local_zf, S_IRWXU | S_IRWXG); + if (ret == KNOT_EOK) { + ret = copy_file(local_zf, backup_zf); + } + } else { + ret = errno == ENOENT ? KNOT_EFILE : knot_map_errno(); + /* If there's no zone file in the backup, remove any old zone file + * from the repository. + */ + if (ret == KNOT_EFILE) { + unlink(local_zf); + } + } + } else { + conf_val_t val = conf_zone_get(conf, C_ZONEFILE_SYNC, zone->name); + bool can_flush = (conf_int(&val) > -1); + + // The value of ctx->backup_format is always at least BACKUP_FORMAT_2 for + // the backup mode, therefore backup_zfiles_dir is always filled at this point. + assert(backup_zfiles_dir != NULL); + + ret = make_dir(backup_zfiles_dir, S_IRWXU | S_IRWXG, true); + if (ret == KNOT_EOK) { + if (can_flush) { + if (zone->contents != NULL) { + ret = zonefile_write(backup_zf, zone->contents); + } else { + log_zone_notice(zone->name, + "empty zone, skipping a zone file backup"); + } + } else { + ret = copy_file(backup_zf, local_zf); + } + } + } + +done: + free(backup_zf); + free(backup_zfiles_dir); + free(local_zf); + if (ret == KNOT_EFILE) { + log_zone_notice(zone->name, "no zone file, skipping a zone file %s", + ctx->restore_mode ? "restore" : "backup"); + ret = KNOT_EOK; + } + + return ret; +} + +static int backup_keystore(conf_t *conf, zone_t *zone, zone_backup_ctx_t *ctx) +{ + dnssec_keystore_t *from = NULL, *to = NULL; + + conf_val_t policy_id = get_zone_policy(conf, zone->name); + + unsigned backend_type = 0; + int ret = zone_init_keystore(conf, &policy_id, &from, &backend_type, NULL); + if (ret != KNOT_EOK) { + LOG_FAIL("keystore init"); + return ret; + } + if (backend_type == KEYSTORE_BACKEND_PKCS11) { + log_zone_warning(zone->name, "private keys from PKCS#11 aren't subject of backup/restore"); + (void)dnssec_keystore_deinit(from); + return KNOT_EOK; + } + + char kasp_dir[strlen(ctx->backup_dir) + 6]; + (void)snprintf(kasp_dir, sizeof(kasp_dir), "%s/keys", ctx->backup_dir); + ret = keystore_load("keys", KEYSTORE_BACKEND_PEM, kasp_dir, &to); + if (ret != KNOT_EOK) { + LOG_FAIL("keystore load"); + goto done; + } + + BACKUP_SWAP(ctx, from, to); + + list_t key_params; + init_list(&key_params); + ret = kasp_db_list_keys(zone_kaspdb(zone), zone->name, &key_params); + ret = (ret == KNOT_ENOENT ? KNOT_EOK : ret); + if (ret != KNOT_EOK) { + LOG_FAIL("keystore list"); + goto done; + } + ptrnode_t *n; + WALK_LIST(n, key_params) { + key_params_t *parm = n->d; + if (ret == KNOT_EOK && !parm->is_pub_only) { + ret = backup_key(parm, from, to); + } + free_key_params(parm); + } + if (ret != KNOT_EOK) { + LOG_FAIL("key copy"); + } + ptrlist_deep_free(&key_params, NULL); + +done: + (void)dnssec_keystore_deinit(to); + (void)dnssec_keystore_deinit(from); + return ret; +} + +int zone_backup(conf_t *conf, zone_t *zone) +{ + zone_backup_ctx_t *ctx = zone->backup_ctx; + if (ctx == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + int ret_deinit; + + if (ctx->backup_zonefile) { + ret = backup_zonefile(conf, zone, ctx); + if (ret != KNOT_EOK) { + LOG_MARK_FAIL("zone file"); + goto done; + } + } + + if (ctx->backup_kaspdb) { + knot_lmdb_db_t *kasp_from = zone_kaspdb(zone), *kasp_to = &ctx->bck_kasp_db; + BACKUP_SWAP(ctx, kasp_from, kasp_to); + + if (knot_lmdb_exists(kasp_from) != KNOT_ENODB) { + ret = kasp_db_backup(zone->name, kasp_from, kasp_to); + if (ret != KNOT_EOK) { + LOG_MARK_FAIL("KASP database"); + goto done; + } + + ret = backup_keystore(conf, zone, ctx); + if (ret != KNOT_EOK) { + ctx->failed = true; + goto done; + } + } + } + + if (ctx->backup_journal) { + knot_lmdb_db_t *j_from = zone_journaldb(zone), *j_to = &ctx->bck_journal; + BACKUP_SWAP(ctx, j_from, j_to); + + ret = journal_copy_with_md(j_from, j_to, zone->name); + } else if (ctx->restore_mode && ctx->backup_zonefile) { + ret = journal_scrape_with_md(zone_journal(zone), true); + } + if (ret != KNOT_EOK) { + LOG_MARK_FAIL("journal"); + goto done; + } + + if (ctx->backup_timers) { + ret = knot_lmdb_open(&ctx->bck_timer_db); + if (ret != KNOT_EOK) { + LOG_MARK_FAIL("timers open"); + goto done; + } + if (ctx->restore_mode) { + ret = zone_timers_read(&ctx->bck_timer_db, zone->name, &zone->timers); + zone_timers_sanitize(conf, zone); + zone->zonefile.bootstrap_cnt = 0; + } else { + ret = zone_timers_write(&ctx->bck_timer_db, zone->name, &zone->timers); + } + if (ret != KNOT_EOK) { + LOG_MARK_FAIL("timers"); + goto done; + } + } + +done: + ret_deinit = zone_backup_deinit(ctx); + zone->backup_ctx = NULL; + return (ret != KNOT_EOK) ? ret : ret_deinit; +} + +int global_backup(zone_backup_ctx_t *ctx, catalog_t *catalog, + const knot_dname_t *zone_only) +{ + if (!ctx->backup_catalog) { + return KNOT_EOK; + } + + knot_lmdb_db_t *cat_from = &catalog->db, *cat_to = &ctx->bck_catalog; + BACKUP_SWAP(ctx, cat_from, cat_to); + int ret = catalog_copy(cat_from, cat_to, zone_only, !ctx->restore_mode); + if (ret != KNOT_EOK) { + ctx->failed = true; + } + return ret; +} diff --git a/src/knot/zone/backup.h b/src/knot/zone/backup.h new file mode 100644 index 0000000..b1d0e3e --- /dev/null +++ b/src/knot/zone/backup.h @@ -0,0 +1,74 @@ +/* Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <pthread.h> +#include <stdint.h> + +#include "knot/dnssec/kasp/kasp_db.h" +#include "knot/zone/zone.h" + +/*! \bref Backup format versions. */ +typedef enum { + BACKUP_FORMAT_1 = 1, // in Knot DNS 3.0.x, no label file + BACKUP_FORMAT_2 = 2, // in Knot DNS 3.1.x + BACKUP_FORMAT_TERM, +} knot_backup_format_t; + +typedef struct zone_backup_ctx { + node_t n; // ability to be put into list_t + bool restore_mode; // if true, this is not a backup, but restore + bool forced; // if true, the force flag has been set + bool backup_zonefile; // if true, also backup zone contents to a zonefile (default on) + bool backup_journal; // if true, also backup journal (default off) + bool backup_timers; // if true, also backup timers (default on) + bool backup_kaspdb; // if true, also backup KASP database (default on) + bool backup_catalog; // if true, also backup zone catalog (default on) + bool backup_global; // perform global backup for all zones + ssize_t readers; // when decremented to 0, all zones done, free this context + pthread_mutex_t readers_mutex; // mutex covering readers counter + char *backup_dir; // path of directory to backup to / restore from + knot_lmdb_db_t bck_kasp_db; // backup KASP db + knot_lmdb_db_t bck_timer_db; // backup timer DB + knot_lmdb_db_t bck_journal; // backup journal DB + knot_lmdb_db_t bck_catalog; // backup catalog DB + bool failed; // true if an error occurred in processing of any zone + knot_backup_format_t backup_format; // the backup format version used + time_t init_time; // time when the current backup operation has started + int zone_count; // count of backed up zones +} zone_backup_ctx_t; + +typedef struct { + list_t ctxs; + pthread_mutex_t mutex; +} zone_backup_ctxs_t; + +int zone_backup_init(bool restore_mode, bool forced, const char *backup_dir, + size_t kasp_db_size, size_t timer_db_size, size_t journal_db_size, + size_t catalog_db_size, zone_backup_ctx_t **out_ctx); + +int zone_backup_deinit(zone_backup_ctx_t *ctx); + +int zone_backup(conf_t *conf, zone_t *zone); + +int global_backup(zone_backup_ctx_t *ctx, catalog_t *catalog, + const knot_dname_t *zone_only); + +void zone_backups_init(zone_backup_ctxs_t *ctxs); +void zone_backups_deinit(zone_backup_ctxs_t *ctxs); +void zone_backups_add(zone_backup_ctxs_t *ctxs, zone_backup_ctx_t *ctx); +void zone_backups_rem(zone_backup_ctx_t *ctx); diff --git a/src/knot/zone/backup_dir.c b/src/knot/zone/backup_dir.c new file mode 100644 index 0000000..7333b21 --- /dev/null +++ b/src/knot/zone/backup_dir.c @@ -0,0 +1,247 @@ +/* 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 <fcntl.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> + +#include "knot/zone/backup_dir.h" + +#include "contrib/files.h" +#include "contrib/getline.h" +#include "knot/common/log.h" + +#define LABEL_FILE "knot_backup.label" +#define LOCK_FILE "lock.knot_backup" + +#define LABEL_FILE_HEAD "label: Knot DNS Backup\n" +#define LABEL_FILE_FORMAT "backup_format: %d\n" +#define LABEL_FILE_TIME_FORMAT "%Y-%m-%d %H:%M:%S %Z" + +#define FNAME_MAX (MAX(sizeof(LABEL_FILE), sizeof(LOCK_FILE))) +#define PREPARE_PATH(var, file) \ + char var[path_size(ctx)]; \ + get_full_path(ctx, file, var); + +static const char *label_file_name = LABEL_FILE; +static const char *lock_file_name = LOCK_FILE; +static const char *label_file_head = LABEL_FILE_HEAD; + +static void get_full_path(zone_backup_ctx_t *ctx, const char *filename, char *full_path) +{ + (void)sprintf(full_path, "%s/%s", ctx->backup_dir, filename); +} + +static size_t path_size(zone_backup_ctx_t *ctx) +{ + // The \0 terminator is already included in the sizeof()/FNAME_MAX value, + // thus the sum covers one additional char for '/'. + return (strlen(ctx->backup_dir) + 1 + FNAME_MAX); +} + +static int make_label_file(zone_backup_ctx_t *ctx) +{ + PREPARE_PATH(label_path, label_file_name); + + FILE *file = fopen(label_path, "w"); + if (file == NULL) { + return knot_map_errno(); + } + + // Prepare the server identity. + conf_val_t val = conf_get(conf(), C_SRV, C_IDENT); + const char *ident = conf_str(&val); + if (ident == NULL || ident[0] == '\0') { + ident = conf()->hostname; + } + + // Prepare the timestamps. + char started_time[64], finished_time[64]; + struct tm tm; + + localtime_r(&ctx->init_time, &tm); + strftime(started_time, sizeof(started_time), LABEL_FILE_TIME_FORMAT, &tm); + + time_t now = time(NULL); + localtime_r(&now, &tm); + strftime(finished_time, sizeof(finished_time), LABEL_FILE_TIME_FORMAT, &tm); + + // Print the label contents. + int ret = fprintf(file, + "%s" + LABEL_FILE_FORMAT + "server_identity: %s\n" + "started_time: %s\n" + "finished_time: %s\n" + "knot_version: %s\n" + "parameters: +%szonefile +%sjournal +%stimers +%skaspdb +%scatalog " + "+backupdir %s\n" + "zone_count: %d\n", + label_file_head, + ctx->backup_format, ident, started_time, finished_time, PACKAGE_VERSION, + ctx->backup_zonefile ? "" : "no", + ctx->backup_journal ? "" : "no", + ctx->backup_timers ? "" : "no", + ctx->backup_kaspdb ? "" : "no", + ctx->backup_catalog ? "" : "no", + ctx->backup_dir, + ctx->zone_count); + + ret = (ret < 0) ? knot_map_errno() : KNOT_EOK; + + fclose(file); + return ret; +} + +static int get_backup_format(zone_backup_ctx_t *ctx) +{ + PREPARE_PATH(label_path, label_file_name); + + int ret = KNOT_EMALF; + + struct stat sb; + if (stat(label_path, &sb) != 0) { + ret = knot_map_errno(); + if (ret == KNOT_ENOENT) { + if (ctx->forced) { + ctx->backup_format = BACKUP_FORMAT_1; + ret = KNOT_EOK; + } else { + ret = KNOT_EMALF; + } + } + return ret; + } + + // getline() from an empty file results in EAGAIN, therefore avoid doing so. + if (!S_ISREG(sb.st_mode) || sb.st_size == 0) { + return ret; + } + + FILE *file = fopen(label_path, "r"); + if (file == NULL) { + return knot_map_errno(); + } + + char *line = NULL; + size_t line_size = 0; + + // Check for the header line first. + if (knot_getline(&line, &line_size, file) == -1) { + ret = knot_map_errno(); + goto done; + } + + if (strcmp(line, label_file_head) != 0) { + goto done; + } + + while (knot_getline(&line, &line_size, file) != -1) { + int value; + if (sscanf(line, LABEL_FILE_FORMAT, &value) != 0) { + if (value >= BACKUP_FORMAT_TERM) { + ret = KNOT_ENOTSUP; + } else if (value > BACKUP_FORMAT_1) { + ctx->backup_format = value; + ret = KNOT_EOK; + } + break; + } + } + +done: + free(line); + fclose(file); + return ret; +} + +int backupdir_init(zone_backup_ctx_t *ctx) +{ + int ret; + struct stat sb; + + // Make sure the source/target backup directory exists. + if (ctx->restore_mode) { + if (stat(ctx->backup_dir, &sb) != 0) { + return knot_map_errno(); + } + if (!S_ISDIR(sb.st_mode)) { + return KNOT_ENOTDIR; + } + } else { + ret = make_dir(ctx->backup_dir, S_IRWXU|S_IRWXG, true); + if (ret != KNOT_EOK) { + return ret; + } + } + + char full_path[path_size(ctx)]; + + // Check for existence of a label file and the backup format used. + if (ctx->restore_mode) { + ret = get_backup_format(ctx); + if (ret != KNOT_EOK) { + return ret; + } + } else { + get_full_path(ctx, label_file_name, full_path); + if (stat(full_path, &sb) == 0) { + return KNOT_EEXIST; + } + } + + // Make (or check for existence of) a lock file. + get_full_path(ctx, lock_file_name, full_path); + if (ctx->restore_mode) { + // Just check. + if (stat(full_path, &sb) == 0) { + return KNOT_EBUSY; + } + } else { + // Create it (which also checks for its existence). + int lock_file = open(full_path, O_CREAT|O_EXCL, + S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); + if (lock_file < 0) { + // Make the reported error better understandable than KNOT_EEXIST. + return errno == EEXIST ? KNOT_EBUSY : knot_map_errno(); + } + close(lock_file); + } + + return KNOT_EOK; +} + +int backupdir_deinit(zone_backup_ctx_t *ctx) +{ + int ret = KNOT_EOK; + + if (!ctx->restore_mode && !ctx->failed) { + // Create the label file first. + ret = make_label_file(ctx); + if (ret == KNOT_EOK) { + // Remove the lock file only when the label file has been created. + PREPARE_PATH(lock_path, lock_file_name); + unlink(lock_path); + } else { + log_error("failed to create a backup label in %s", (ctx)->backup_dir); + } + } + + return ret; +} diff --git a/src/knot/zone/backup_dir.h b/src/knot/zone/backup_dir.h new file mode 100644 index 0000000..7d19ffc --- /dev/null +++ b/src/knot/zone/backup_dir.h @@ -0,0 +1,39 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/zone/backup.h" + +/*! + * Prepares the backup directory - verifies it exists and creates it for backup + * if it's needed. Verifies existence/non-existence of a lock file and a label file, + * in the backup mode it creates them, in the restore mode it sets ctx->backup_format. + * + * \param[in/out] ctx Backup context. + * + * \return Error code, KNOT_EOK if successful. + */ +int backupdir_init(zone_backup_ctx_t *ctx); + +/*! + * If the backup has been successful, it creates the label file + * and removes the lock file. Do nothing in the restore mode. + * + * \param[in] ctx Backup context. + * + * \return Error code, KNOT_EOK if successful. + */ +int backupdir_deinit(zone_backup_ctx_t *ctx); diff --git a/src/knot/zone/contents.c b/src/knot/zone/contents.c new file mode 100644 index 0000000..cba13e8 --- /dev/null +++ b/src/knot/zone/contents.c @@ -0,0 +1,609 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "libdnssec/error.h" +#include "knot/zone/adds_tree.h" +#include "knot/zone/adjust.h" +#include "knot/zone/contents.h" +#include "knot/common/log.h" +#include "knot/dnssec/zone-nsec.h" +#include "libknot/libknot.h" +#include "contrib/qp-trie/trie.h" + +/*! + * \brief Destroys all RRSets in a node. + * + * \param node Node to destroy RRSets from. + * \param data Unused parameter. + */ +static int destroy_node_rrsets_from_tree(zone_node_t *node, _unused_ void *data) +{ + if (node != NULL) { + binode_unify(node, false, NULL); + node_free_rrsets(node, NULL); + node_free(node, NULL); + } + + return KNOT_EOK; +} + +/*! + * \brief Tries to find the given domain name in the zone tree. + * + * \param zone Zone to search in. + * \param name Domain name to find. + * \param node Found node. + * \param previous Previous node in canonical order (i.e. the one directly + * preceding \a name in canonical order, regardless if the name + * is in the zone or not). + * + * \retval true if the domain name was found. In such case \a node holds the + * zone node with \a name as its owner. \a previous is set + * properly. + * \retval false if the domain name was not found. \a node may hold any (or none) + * node. \a previous is set properly. + */ +static bool find_in_tree(zone_tree_t *tree, const knot_dname_t *name, + zone_node_t **node, zone_node_t **previous) +{ + assert(tree != NULL); + assert(name != NULL); + assert(node != NULL); + assert(previous != NULL); + + zone_node_t *found = NULL, *prev = NULL; + + int match = zone_tree_get_less_or_equal(tree, name, &found, &prev); + if (match < 0) { + assert(0); + return false; + } + + *node = found; + *previous = prev; + + return match > 0; +} + +/*! + * \brief Create a node suitable for inserting into this contents. + */ +static zone_node_t *node_new_for_contents(const knot_dname_t *owner, const zone_contents_t *contents) +{ + assert(contents->nsec3_nodes == NULL || contents->nsec3_nodes->flags == contents->nodes->flags); + return node_new_for_tree(owner, contents->nodes, NULL); +} + +static zone_node_t *get_node(const zone_contents_t *zone, const knot_dname_t *name) +{ + assert(zone); + assert(name); + + return zone_tree_get(zone->nodes, name); +} + +static zone_node_t *get_nsec3_node(const zone_contents_t *zone, + const knot_dname_t *name) +{ + assert(zone); + assert(name); + + return zone_tree_get(zone->nsec3_nodes, name); +} + +static int insert_rr(zone_contents_t *z, const knot_rrset_t *rr, zone_node_t **n) +{ + if (knot_rrset_empty(rr)) { + return KNOT_EINVAL; + } + + if (*n == NULL) { + int ret = zone_tree_add_node(zone_contents_tree_for_rr(z, rr), z->apex, rr->owner, + (zone_tree_new_node_cb_t)node_new_for_contents, z, n); + if (ret != KNOT_EOK) { + return ret; + } + } + + return node_add_rrset(*n, rr, NULL); +} + +static int remove_rr(zone_contents_t *z, const knot_rrset_t *rr, + zone_node_t **n, bool nsec3) +{ + if (knot_rrset_empty(rr)) { + return KNOT_EINVAL; + } + + // check if the RRSet belongs to the zone + if (knot_dname_in_bailiwick(rr->owner, z->apex->owner) < 0) { + return KNOT_EOUTOFZONE; + } + + zone_node_t *node; + if (*n == NULL) { + node = nsec3 ? get_nsec3_node(z, rr->owner) : get_node(z, rr->owner); + if (node == NULL) { + return KNOT_ENONODE; + } + } else { + node = *n; + } + + int ret = node_remove_rrset(node, rr, NULL); + if (ret != KNOT_EOK) { + return ret; + } + + if (node->rrset_count == 0 && node->children == 0 && node != z->apex) { + zone_tree_del_node(nsec3 ? z->nsec3_nodes : z->nodes, node, true); + } + + *n = node; + return KNOT_EOK; +} + +// Public API + +zone_contents_t *zone_contents_new(const knot_dname_t *apex_name, bool use_binodes) +{ + if (apex_name == NULL) { + return NULL; + } + + zone_contents_t *contents = calloc(1, sizeof(*contents)); + if (contents == NULL) { + return NULL; + } + + contents->nodes = zone_tree_create(use_binodes); + if (contents->nodes == NULL) { + goto cleanup; + } + + contents->apex = node_new_for_contents(apex_name, contents); + if (contents->apex == NULL) { + goto cleanup; + } + + if (zone_tree_insert(contents->nodes, &contents->apex) != KNOT_EOK) { + goto cleanup; + } + contents->apex->flags |= NODE_FLAGS_APEX; + contents->max_ttl = UINT32_MAX; + + return contents; + +cleanup: + node_free(contents->apex, NULL); + free(contents->nodes); + free(contents); + return NULL; +} + +zone_tree_t *zone_contents_tree_for_rr(zone_contents_t *contents, const knot_rrset_t *rr) +{ + bool nsec3rel = knot_rrset_is_nsec3rel(rr); + + if (nsec3rel && contents->nsec3_nodes == NULL) { + contents->nsec3_nodes = zone_tree_create((contents->nodes->flags & ZONE_TREE_USE_BINODES)); + if (contents->nsec3_nodes == NULL) { + return NULL; + } + contents->nsec3_nodes->flags = contents->nodes->flags; + } + + return nsec3rel ? contents->nsec3_nodes : contents->nodes; +} + +int zone_contents_add_rr(zone_contents_t *z, const knot_rrset_t *rr, zone_node_t **n) +{ + if (rr == NULL || n == NULL) { + return KNOT_EINVAL; + } + + if (z == NULL) { + return KNOT_EEMPTYZONE; + } + + return insert_rr(z, rr, n); +} + +int zone_contents_remove_rr(zone_contents_t *z, const knot_rrset_t *rr, zone_node_t **n) +{ + if (rr == NULL || n == NULL) { + return KNOT_EINVAL; + } + + if (z == NULL) { + return KNOT_EEMPTYZONE; + } + + return remove_rr(z, rr, n, knot_rrset_is_nsec3rel(rr)); +} + +const zone_node_t *zone_contents_find_node(const zone_contents_t *zone, const knot_dname_t *name) +{ + if (zone == NULL || name == NULL) { + return NULL; + } + + return get_node(zone, name); +} + +const zone_node_t *zone_contents_node_or_nsec3(const zone_contents_t *zone, const knot_dname_t *name) +{ + if (zone == NULL || name == NULL) { + return NULL; + } + + const zone_node_t *node = get_node(zone, name); + if (node == NULL) { + node = get_nsec3_node(zone, name); + } + return node; +} + +zone_node_t *zone_contents_find_node_for_rr(zone_contents_t *contents, const knot_rrset_t *rrset) +{ + if (contents == NULL || rrset == NULL) { + return NULL; + } + + const bool nsec3 = knot_rrset_is_nsec3rel(rrset); + return nsec3 ? get_nsec3_node(contents, rrset->owner) : + get_node(contents, rrset->owner); +} + +int zone_contents_find_dname(const zone_contents_t *zone, + const knot_dname_t *name, + const zone_node_t **match, + const zone_node_t **closest, + const zone_node_t **previous) +{ + if (name == NULL || match == NULL || closest == NULL) { + return KNOT_EINVAL; + } + + if (zone == NULL) { + return KNOT_EEMPTYZONE; + } + + if (knot_dname_in_bailiwick(name, zone->apex->owner) < 0) { + return KNOT_EOUTOFZONE; + } + + zone_node_t *node = NULL; + zone_node_t *prev = NULL; + + int found = zone_tree_get_less_or_equal(zone->nodes, name, &node, &prev); + if (found < 0) { + // error + return found; + } else if (found == 1 && previous != NULL) { + // exact match + + assert(node && prev); + + *match = node; + *closest = node; + *previous = prev; + + return ZONE_NAME_FOUND; + } else if (found == 1 && previous == NULL) { + // exact match, zone not adjusted yet + + assert(node); + *match = node; + *closest = node; + + return ZONE_NAME_FOUND; + } else { + // closest match + + assert(!node && prev); + + node = prev; + size_t matched_labels = knot_dname_matched_labels(node->owner, name); + while (matched_labels < knot_dname_labels(node->owner, NULL)) { + node = node_parent(node); + assert(node); + } + + *match = NULL; + *closest = node; + if (previous != NULL) { + *previous = prev; + } + + return ZONE_NAME_NOT_FOUND; + } +} + +const zone_node_t *zone_contents_find_nsec3_node(const zone_contents_t *zone, + const knot_dname_t *name) +{ + if (zone == NULL || name == NULL) { + return NULL; + } + + return get_nsec3_node(zone, name); +} + +int zone_contents_find_nsec3_for_name(const zone_contents_t *zone, + const knot_dname_t *name, + const zone_node_t **nsec3_node, + const zone_node_t **nsec3_previous) +{ + if (name == NULL || nsec3_node == NULL || nsec3_previous == NULL) { + return KNOT_EINVAL; + } + + if (zone == NULL) { + return KNOT_EEMPTYZONE; + } + + // check if the NSEC3 tree is not empty + if (zone_tree_is_empty(zone->nsec3_nodes)) { + return KNOT_ENSEC3CHAIN; + } + if (!knot_is_nsec3_enabled(zone)) { + return KNOT_ENSEC3PAR; + } + + knot_dname_storage_t nsec3_name; + int ret = knot_create_nsec3_owner(nsec3_name, sizeof(nsec3_name), + name, zone->apex->owner, &zone->nsec3_params); + if (ret != KNOT_EOK) { + return ret; + } + + return zone_contents_find_nsec3(zone, nsec3_name, nsec3_node, nsec3_previous); +} + +int zone_contents_find_nsec3(const zone_contents_t *zone, + const knot_dname_t *nsec3_name, + const zone_node_t **nsec3_node, + const zone_node_t **nsec3_previous) +{ + zone_node_t *found = NULL, *prev = NULL; + bool match = find_in_tree(zone->nsec3_nodes, nsec3_name, &found, &prev); + + *nsec3_node = found; + + if (prev == NULL) { + // either the returned node is the root of the tree, or it is + // the leftmost node in the tree; in both cases node was found + // set the previous node of the found node + assert(match); + assert(*nsec3_node != NULL); + *nsec3_previous = node_prev(*nsec3_node); + assert(*nsec3_previous != NULL); + } else { + *nsec3_previous = prev; + } + + // The previous may be from wrong NSEC3 chain. Search for previous from the right chain. + const zone_node_t *original_prev = *nsec3_previous; + while (!((*nsec3_previous)->flags & NODE_FLAGS_IN_NSEC3_CHAIN)) { + *nsec3_previous = node_prev(*nsec3_previous); + if (*nsec3_previous == original_prev || *nsec3_previous == NULL) { + // cycle + *nsec3_previous = NULL; + break; + } + } + + return (match ? ZONE_NAME_FOUND : ZONE_NAME_NOT_FOUND); +} + +const zone_node_t *zone_contents_find_wildcard_child(const zone_contents_t *contents, + const zone_node_t *parent) +{ + if (contents == NULL || parent == NULL || parent->owner == NULL) { + return NULL; + } + + knot_dname_storage_t wildcard = "\x01""*"; + knot_dname_to_wire(wildcard + 2, parent->owner, sizeof(wildcard) - 2); + + return zone_contents_find_node(contents, wildcard); +} + +bool zone_contents_find_node_or_wildcard(const zone_contents_t *contents, + const knot_dname_t *find, + const zone_node_t **found) +{ + const zone_node_t *encloser = NULL; + zone_contents_find_dname(contents, find, found, &encloser, NULL); + if (*found == NULL && encloser != NULL && (encloser->flags & NODE_FLAGS_WILDCARD_CHILD)) { + *found = zone_contents_find_wildcard_child(contents, encloser); + assert(*found != NULL); + } + return (*found != NULL); +} + +int zone_contents_apply(zone_contents_t *contents, + zone_tree_apply_cb_t function, void *data) +{ + if (contents == NULL) { + return KNOT_EEMPTYZONE; + } + return zone_tree_apply(contents->nodes, function, data); +} + +int zone_contents_nsec3_apply(zone_contents_t *contents, + zone_tree_apply_cb_t function, void *data) +{ + if (contents == NULL) { + return KNOT_EEMPTYZONE; + } + return zone_tree_apply(contents->nsec3_nodes, function, data); +} + +int zone_contents_cow(zone_contents_t *from, zone_contents_t **to) +{ + if (to == NULL) { + return KNOT_EINVAL; + } + + if (from == NULL) { + return KNOT_EEMPTYZONE; + } + + /* Copy to same destination as source. */ + if (from == *to) { + return KNOT_EINVAL; + } + + zone_contents_t *contents = calloc(1, sizeof(zone_contents_t)); + if (contents == NULL) { + return KNOT_ENOMEM; + } + + contents->nodes = zone_tree_cow(from->nodes); + if (contents->nodes == NULL) { + free(contents); + return KNOT_ENOMEM; + } + contents->apex = zone_tree_fix_get(from->apex, contents->nodes); + + if (from->nsec3_nodes) { + contents->nsec3_nodes = zone_tree_cow(from->nsec3_nodes); + if (contents->nsec3_nodes == NULL) { + trie_cow_rollback(contents->nodes->cow, NULL, NULL); + free(contents->nodes); + free(contents); + return KNOT_ENOMEM; + } + } + contents->adds_tree = from->adds_tree; + from->adds_tree = NULL; + contents->size = from->size; + contents->max_ttl = from->max_ttl; + + *to = contents; + return KNOT_EOK; +} + +void zone_contents_free(zone_contents_t *contents) +{ + if (contents == NULL) { + return; + } + + // free the zone tree, but only the structure + zone_tree_free(&contents->nodes); + zone_tree_free(&contents->nsec3_nodes); + + dnssec_nsec3_params_free(&contents->nsec3_params); + additionals_tree_free(contents->adds_tree); + + free(contents); +} + +void zone_contents_deep_free(zone_contents_t *contents) +{ + if (contents == NULL) { + return; + } + + if (contents != NULL) { + // Delete NSEC3 tree. + (void)zone_tree_apply(contents->nsec3_nodes, + destroy_node_rrsets_from_tree, NULL); + + // Delete the normal tree. + (void)zone_tree_apply(contents->nodes, + destroy_node_rrsets_from_tree, NULL); + } + + zone_contents_free(contents); +} + +uint32_t zone_contents_serial(const zone_contents_t *zone) +{ + if (zone == NULL) { + return 0; + } + + const knot_rdataset_t *soa = node_rdataset(zone->apex, KNOT_RRTYPE_SOA); + if (soa == NULL) { + return 0; + } + + return knot_soa_serial(soa->rdata); +} + +void zone_contents_set_soa_serial(zone_contents_t *zone, uint32_t new_serial) +{ + knot_rdataset_t *soa; + if (zone != NULL && (soa = node_rdataset(zone->apex, KNOT_RRTYPE_SOA)) != NULL) { + knot_soa_serial_set(soa->rdata, new_serial); + } +} + +int zone_contents_load_nsec3param(zone_contents_t *contents) +{ + if (contents == NULL) { + return KNOT_EEMPTYZONE; + } + + if (contents->apex == NULL) { + return KNOT_EINVAL; + } + + const knot_rdataset_t *rrs = NULL; + rrs = node_rdataset(contents->apex, KNOT_RRTYPE_NSEC3PARAM); + if (rrs == NULL) { + dnssec_nsec3_params_free(&contents->nsec3_params); + return KNOT_EOK; + } + + if (rrs->count != 1) { + return KNOT_EINVAL; + } + + dnssec_binary_t rdata = { + .size = rrs->rdata->len, + .data = rrs->rdata->data, + }; + + dnssec_nsec3_params_t new_params = { 0 }; + int r = dnssec_nsec3_params_from_rdata(&new_params, &rdata); + if (r != DNSSEC_EOK) { + return KNOT_EMALF; + } + + dnssec_nsec3_params_free(&contents->nsec3_params); + contents->nsec3_params = new_params; + return KNOT_EOK; +} + +bool zone_contents_is_empty(const zone_contents_t *zone) +{ + if (zone == NULL) { + return true; + } + + bool apex_empty = (zone->apex == NULL || zone->apex->rrset_count == 0); + bool no_non_apex = (zone_tree_count(zone->nodes) <= (zone->apex != NULL ? 1 : 0)); + bool no_nsec3 = zone_tree_is_empty(zone->nsec3_nodes); + + return (apex_empty && no_non_apex && no_nsec3); +} diff --git a/src/knot/zone/contents.h b/src/knot/zone/contents.h new file mode 100644 index 0000000..8f1f160 --- /dev/null +++ b/src/knot/zone/contents.h @@ -0,0 +1,291 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "libdnssec/nsec.h" +#include "libknot/rrtype/nsec3param.h" +#include "knot/zone/node.h" +#include "knot/zone/zone-tree.h" + +enum zone_contents_find_dname_result { + ZONE_NAME_NOT_FOUND = 0, + ZONE_NAME_FOUND = 1 +}; + +typedef struct zone_contents { + zone_node_t *apex; /*!< Apex node of the zone (holding SOA) */ + + zone_tree_t *nodes; + zone_tree_t *nsec3_nodes; + + trie_t *adds_tree; // "additionals tree" for reverse lookup of nodes affected by additionals + + dnssec_nsec3_params_t nsec3_params; + size_t size; + uint32_t max_ttl; + bool dnssec; +} zone_contents_t; + +/*! + * \brief Allocate and create new zone contents. + * + * \param apex_name Name of the root node. + * \param use_binodes Zone trees shall consist of bi-nodes to enable zone updates. + * + * \return New contents or NULL on error. + */ +zone_contents_t *zone_contents_new(const knot_dname_t *apex_name, bool use_binodes); + +/*! + * \brief Returns zone tree for inserting given RR. + */ +zone_tree_t *zone_contents_tree_for_rr(zone_contents_t *contents, const knot_rrset_t *rr); + +/*! + * \brief Add an RR to contents. + * + * \param z Contents to add to. + * \param rr The RR to add. + * \param n Node to which the RR has been added to on success, unchanged otherwise. + * + * \return KNOT_E* + */ +int zone_contents_add_rr(zone_contents_t *z, const knot_rrset_t *rr, zone_node_t **n); + +/*! + * \brief Remove an RR from contents. + * + * \param z Contents to remove from. + * \param rr The RR to remove. + * \param n Node from which the RR to be removed from on success, unchanged otherwise. + * + * \return KNOT_E* + */ +int zone_contents_remove_rr(zone_contents_t *z, const knot_rrset_t *rr, zone_node_t **n); + +/*! + * \brief Tries to find a node with the specified name in the zone. + * + * \param contents Zone where the name should be searched for. + * \param name Name to find. + * + * \return Corresponding node if found, NULL otherwise. + */ +const zone_node_t *zone_contents_find_node(const zone_contents_t *contents, const knot_dname_t *name); + +/*! + * \brief Tries to find a node in the zone, also searching in NSEC3 tree. + * + * \param zone Zone where the name should be searched for. + * \param name Name to find. + * + * \return Normal or NSEC3 node, or NULL. + */ +const zone_node_t *zone_contents_node_or_nsec3(const zone_contents_t *zone, const knot_dname_t *name); + +/*! + * \brief Find a node in which the given rrset may be inserted, + * + * \param contents Zone contents. + * \param rrset RRSet to be inserted later. + * + * \return Existing node in zone which the RRSet may be inserted in; or NULL if none present. + */ +zone_node_t *zone_contents_find_node_for_rr(zone_contents_t *contents, const knot_rrset_t *rrset); + +/*! + * \brief Tries to find a node by owner in the zone contents. + * + * \param[in] contents Zone to search for the name. + * \param[in] name Domain name to search for. + * \param[out] match Matching node or NULL. + * \param[out] closest Closest matching name in the zone. + * May match \a match if found exactly. + * \param[out] previous Previous domain name in canonical order. + * Always previous, won't match \a match. + * + * \note The encloser and previous mustn't be used directly for DNSSEC proofs. + * These nodes may be empty non-terminals or not authoritative. + * + * \retval ZONE_NAME_FOUND if node with owner \a name was found. + * \retval ZONE_NAME_NOT_FOUND if it was not found. + * \retval KNOT_EEMPTYZONE + * \retval KNOT_EINVAL + * \retval KNOT_EOUTOFZONE + */ +int zone_contents_find_dname(const zone_contents_t *contents, + const knot_dname_t *name, + const zone_node_t **match, + const zone_node_t **closest, + const zone_node_t **previous); + +/*! + * \brief Tries to find a node with the specified name among the NSEC3 nodes + * of the zone. + * + * \param contents Zone where the name should be searched for. + * \param name Name to find. + * + * \return Corresponding node if found, NULL otherwise. + */ +const zone_node_t *zone_contents_find_nsec3_node(const zone_contents_t *contents, + const knot_dname_t *name); + +/*! + * \brief Finds NSEC3 node and previous NSEC3 node in canonical order, + * corresponding to the given domain name. + * + * This functions creates a NSEC3 hash of \a name and tries to find NSEC3 node + * with the hashed domain name as owner. + * + * \param[in] contents Zone to search in. + * \param[in] name Domain name to get the corresponding NSEC3 nodes for. + * \param[out] nsec3_node NSEC3 node corresponding to \a name (if found, + * otherwise this may be an arbitrary NSEC3 node). + * \param[out] nsec3_previous The NSEC3 node immediately preceding hashed domain + * name corresponding to \a name in canonical order. + * + * \retval ZONE_NAME_FOUND if the corresponding NSEC3 node was found. + * \retval ZONE_NAME_NOT_FOUND if it was not found. + * \retval KNOT_EEMPTYZONE + * \retval KNOT_EINVAL + * \retval KNOT_ENSEC3PAR + * \retval KNOT_ECRYPTO + * \retval KNOT_ERROR + */ +int zone_contents_find_nsec3_for_name(const zone_contents_t *contents, + const knot_dname_t *name, + const zone_node_t **nsec3_node, + const zone_node_t **nsec3_previous); + +/*! + * \brief Finds NSEC3 node and previous NSEC3 node to specified NSEC3 name. + * + * Like previous function, but the NSEC3 hashed-name is already known. + * + * \param zone Zone contents to search in, + * \param nsec3_name NSEC3 name to be searched for. + * \param nsec3_node Out: NSEC3 node found. + * \param nsec3_previous Out: previous NSEC3 node. + * + * \return ZONE_NAME_FOUND, ZONE_NAME_NOT_FOUND, KNOT_E* + */ +int zone_contents_find_nsec3(const zone_contents_t *zone, + const knot_dname_t *nsec3_name, + const zone_node_t **nsec3_node, + const zone_node_t **nsec3_previous); + +/*! + * \brief For specified node, give a wildcard child if exists in zone. + * + * \param contents Zone contents. + * \param parent Given parent node. + * + * \return Node being a wildcard child; or NULL. + */ +const zone_node_t *zone_contents_find_wildcard_child(const zone_contents_t *contents, + const zone_node_t *parent); + +/*! + * \brief For given name, find either exactly matching node in zone, or a matching wildcard node. + * + * \param contents Zone contents to be searched in. + * \param find Name to be searched for. + * \param found Out: a node that either has owner "find" or is matching wildcard node. + * + * \return true iff found something + */ +bool zone_contents_find_node_or_wildcard(const zone_contents_t *contents, + const knot_dname_t *find, + const zone_node_t **found); + +/*! + * \brief Applies the given function to each regular node in the zone. + * + * \param contents Nodes of this zone will be used as parameters for the function. + * \param function Function to be applied to each node of the zone. + * \param data Arbitrary data to be passed to the function. + */ +int zone_contents_apply(zone_contents_t *contents, + zone_tree_apply_cb_t function, void *data); + +/*! + * \brief Applies the given function to each NSEC3 node in the zone. + * + * \param contents NSEC3 nodes of this zone will be used as parameters for the + * function. + * \param function Function to be applied to each node of the zone. + * \param data Arbitrary data to be passed to the function. + */ +int zone_contents_nsec3_apply(zone_contents_t *contents, + zone_tree_apply_cb_t function, void *data); + +/*! + * \brief Create new zone_contents by COW copy of zone trees. + * + * \param from Original zone. + * \param to Copy of the zone. + * + * \retval KNOT_EOK + * \retval KNOT_EEMPTYZONE + * \retval KNOT_EINVAL + * \retval KNOT_ENOMEM + */ +int zone_contents_cow(zone_contents_t *from, zone_contents_t **to); + +/*! + * \brief Deallocate directly owned data of zone contents. + * + * \param contents Zone contents to free. + */ +void zone_contents_free(zone_contents_t *contents); + +/*! + * \brief Deallocate node RRSets inside the trees, then call zone_contents_free. + * + * \param contents Zone contents to free. + */ +void zone_contents_deep_free(zone_contents_t *contents); + +/*! + * \brief Fetch zone serial. + * + * \param zone Zone. + * + * \return serial or 0 + */ +uint32_t zone_contents_serial(const zone_contents_t *zone); + +/*! + * \brief Adjust zone serial. + * + * Works only if there is a SOA in given contents. + * + * \param zone Zone. + * \param new_serial New serial to be set. + */ +void zone_contents_set_soa_serial(zone_contents_t *zone, uint32_t new_serial); + +/*! + * \brief Load parameters from NSEC3PARAM record into contents->nsec3param structure. + */ +int zone_contents_load_nsec3param(zone_contents_t *contents); + +/*! + * \brief Return true if zone is empty. + */ +bool zone_contents_is_empty(const zone_contents_t *zone); diff --git a/src/knot/zone/digest.c b/src/knot/zone/digest.c new file mode 100644 index 0000000..c3d40a4 --- /dev/null +++ b/src/knot/zone/digest.c @@ -0,0 +1,305 @@ +/* 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 "knot/zone/digest.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/updates/zone-update.h" +#include "contrib/wire_ctx.h" +#include "libdnssec/digest.h" +#include "libknot/libknot.h" + +#define DIGEST_BUF_MIN 4096 +#define DIGEST_BUF_MAX (40 * 1024 * 1024) + +typedef struct { + size_t buf_size; + uint8_t *buf; + struct dnssec_digest_ctx *digest_ctx; + const zone_node_t *apex; +} contents_digest_ctx_t; + +static int digest_rrset(knot_rrset_t *rrset, const zone_node_t *node, void *vctx) +{ + contents_digest_ctx_t *ctx = vctx; + + // ignore apex ZONEMD + if (node == ctx->apex && rrset->type == KNOT_RRTYPE_ZONEMD) { + return KNOT_EOK; + } + + // ignore RRSIGs of apex ZONEMD + if (node == ctx->apex && rrset->type == KNOT_RRTYPE_RRSIG) { + knot_rdataset_t cpy = rrset->rrs, zonemd_rrsig = { 0 }; + int ret = knot_rdataset_copy(&rrset->rrs, &cpy, NULL); + if (ret != KNOT_EOK) { + return ret; + } + + ret = knot_synth_rrsig(KNOT_RRTYPE_ZONEMD, &rrset->rrs, &zonemd_rrsig, NULL); + if (ret == KNOT_EOK) { + ret = knot_rdataset_subtract(&rrset->rrs, &zonemd_rrsig, NULL); + knot_rdataset_clear(&zonemd_rrsig, NULL); + } + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + knot_rdataset_clear(&rrset->rrs, NULL); + return ret; + } + } + + // serialize RRSet, expand buf as needed + int ret = knot_rrset_to_wire_extra(rrset, ctx->buf, ctx->buf_size, 0, + NULL, KNOT_PF_ORIGTTL); + while (ret == KNOT_ESPACE && ctx->buf_size < DIGEST_BUF_MAX) { + free(ctx->buf); + ctx->buf_size *= 2; + ctx->buf = malloc(ctx->buf_size); + if (ctx->buf == NULL) { + return KNOT_ENOMEM; + } + ret = knot_rrset_to_wire_extra(rrset, ctx->buf, ctx->buf_size, 0, + NULL, KNOT_PF_ORIGTTL); + } + + // cleanup apex RRSIGs mess + if (node == ctx->apex && rrset->type == KNOT_RRTYPE_RRSIG) { + knot_rdataset_clear(&rrset->rrs, NULL); + } + + if (ret < 0) { + return ret; + } + + // digest serialized RRSet + dnssec_binary_t bufbin = { ret, ctx->buf }; + return dnssec_digest(ctx->digest_ctx, &bufbin); +} + +static int digest_node(zone_node_t *node, void *ctx) +{ + int i = 0, ret = KNOT_EOK; + for ( ; i < node->rrset_count && ret == KNOT_EOK; i++) { + knot_rrset_t rrset = node_rrset_at(node, i); + ret = digest_rrset(&rrset, node, ctx); + } + return ret; +} + +int zone_contents_digest(const zone_contents_t *contents, int algorithm, + uint8_t **out_digest, size_t *out_size) +{ + if (out_digest == NULL || out_size == NULL) { + return KNOT_EINVAL; + } + + if (contents == NULL) { + return KNOT_EEMPTYZONE; + } + + contents_digest_ctx_t ctx = { + .buf_size = DIGEST_BUF_MIN, + .buf = malloc(DIGEST_BUF_MIN), + .apex = contents->apex, + }; + if (ctx.buf == NULL) { + return KNOT_ENOMEM; + } + + int ret = dnssec_digest_init(algorithm, &ctx.digest_ctx); + if (ret != DNSSEC_EOK) { + free(ctx.buf); + return knot_error_from_libdnssec(ret); + } + + zone_tree_t *conts = contents->nodes; + if (!zone_tree_is_empty(contents->nsec3_nodes)) { + conts = zone_tree_shallow_copy(conts); + if (conts == NULL) { + ret = KNOT_ENOMEM;; + } + if (ret == KNOT_EOK) { + ret = zone_tree_merge(conts, contents->nsec3_nodes); + } + } + + if (ret == KNOT_EOK) { + ret = zone_tree_apply(conts, digest_node, &ctx); + } + + if (conts != contents->nodes) { + zone_tree_free(&conts); + } + + dnssec_binary_t res = { 0 }; + if (ret == KNOT_EOK) { + ret = dnssec_digest_finish(ctx.digest_ctx, &res); + } + free(ctx.buf); + *out_digest = res.data; + *out_size = res.size; + return ret; +} + +static int verify_zonemd(const knot_rdata_t *zonemd, const zone_contents_t *contents) +{ + uint8_t *computed = NULL; + size_t comp_size = 0; + int ret = zone_contents_digest(contents, knot_zonemd_algorithm(zonemd), + &computed, &comp_size); + if (ret != KNOT_EOK) { + return ret; + } + assert(computed); + + if (comp_size != knot_zonemd_digest_size(zonemd)) { + ret = KNOT_EFEWDATA; + } else if (memcmp(knot_zonemd_digest(zonemd), computed, comp_size) != 0) { + ret = KNOT_EMALF; + } + free(computed); + return ret; +} + +bool zone_contents_digest_exists(const zone_contents_t *contents, int alg, bool no_verify) +{ + if (alg == 0) { + return true; + } + + knot_rdataset_t *zonemd = node_rdataset(contents->apex, KNOT_RRTYPE_ZONEMD); + + if (alg == ZONE_DIGEST_REMOVE) { + return (zonemd == NULL || zonemd->count == 0); + } + + if (zonemd == NULL || zonemd->count != 1 || knot_zonemd_algorithm(zonemd->rdata) != alg) { + return false; + } + + if (no_verify) { + return true; + } + + return verify_zonemd(zonemd->rdata, contents) == KNOT_EOK; +} + +static bool check_duplicate_schalg(const knot_rdataset_t *zonemd, int check_upto, + uint8_t scheme, uint8_t alg) +{ + knot_rdata_t *check = zonemd->rdata; + assert(check_upto <= zonemd->count); + for (int i = 0; i < check_upto; i++) { + if (knot_zonemd_scheme(check) == scheme && + knot_zonemd_algorithm(check) == alg) { + return false; + } + check = knot_rdataset_next(check); + } + return true; +} + +int zone_contents_digest_verify(const zone_contents_t *contents) +{ + if (contents == NULL) { + return KNOT_EEMPTYZONE; + } + + knot_rdataset_t *zonemd = node_rdataset(contents->apex, KNOT_RRTYPE_ZONEMD); + if (zonemd == NULL) { + return KNOT_ENOENT; + } + + uint32_t soa_serial = zone_contents_serial(contents); + + knot_rdata_t *rr = zonemd->rdata, *supported = NULL; + for (int i = 0; i < zonemd->count; i++) { + if (knot_zonemd_scheme(rr) == KNOT_ZONEMD_SCHEME_SIMPLE && + knot_zonemd_digest_size(rr) > 0 && + knot_zonemd_soa_serial(rr) == soa_serial) { + supported = rr; + } + if (!check_duplicate_schalg(zonemd, i, knot_zonemd_scheme(rr), + knot_zonemd_algorithm(rr))) { + return KNOT_ESEMCHECK; + } + rr = knot_rdataset_next(rr); + } + + return supported == NULL ? KNOT_ENOTSUP : verify_zonemd(supported, contents); +} + +static ptrdiff_t zonemd_hash_offs(void) +{ + knot_rdata_t fake = { 0 }; + return knot_zonemd_digest(&fake) - fake.data; +} + +int zone_update_add_digest(struct zone_update *update, int algorithm, bool placeholder) +{ + if (update == NULL) { + return KNOT_EINVAL; + } + + uint8_t *digest = NULL; + size_t dsize = 0; + + knot_rrset_t exists = node_rrset(update->new_cont->apex, KNOT_RRTYPE_ZONEMD); + if (algorithm == ZONE_DIGEST_REMOVE) { + return zone_update_remove(update, &exists); + } + if (placeholder) { + if (!knot_rrset_empty(&exists) && + !check_duplicate_schalg(&exists.rrs, exists.rrs.count, + KNOT_ZONEMD_SCHEME_SIMPLE, algorithm)) { + return KNOT_EOK; + } + } else { + int ret = zone_contents_digest(update->new_cont, algorithm, &digest, &dsize); + if (ret != KNOT_EOK) { + return ret; + } + + ret = zone_update_remove(update, &exists); + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + free(digest); + return ret; + } + } + + knot_rrset_t zonemd, soa = node_rrset(update->new_cont->apex, KNOT_RRTYPE_SOA); + + uint8_t rdata[zonemd_hash_offs() + dsize]; + wire_ctx_t wire = wire_ctx_init(rdata, sizeof(rdata)); + wire_ctx_write_u32(&wire, knot_soa_serial(soa.rrs.rdata)); + wire_ctx_write_u8(&wire, KNOT_ZONEMD_SCHEME_SIMPLE); + wire_ctx_write_u8(&wire, algorithm); + wire_ctx_write(&wire, digest, dsize); + assert(wire.error == KNOT_EOK && wire_ctx_available(&wire) == 0); + + free(digest); + + knot_rrset_init(&zonemd, update->new_cont->apex->owner, KNOT_RRTYPE_ZONEMD, + KNOT_CLASS_IN, soa.ttl); + int ret = knot_rrset_add_rdata(&zonemd, rdata, sizeof(rdata), NULL); + if (ret != KNOT_EOK) { + return ret; + } + + ret = zone_update_add(update, &zonemd); + knot_rdataset_clear(&zonemd.rrs, NULL); + return ret; +} diff --git a/src/knot/zone/digest.h b/src/knot/zone/digest.h new file mode 100644 index 0000000..81d1617 --- /dev/null +++ b/src/knot/zone/digest.h @@ -0,0 +1,72 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/zone/contents.h" + +/*! + * \brief Compute hash over whole zone by concatenating RRSets in wire format. + * + * \param contents Zone contents to digest. + * \param algorithm Algorithm to use. + * \param out_digest Output: buffer with computed hash (to be freed). + * \param out_size Output: size of the resulting hash. + * + * \return KNOT_E* + */ +int zone_contents_digest(const zone_contents_t *contents, int algorithm, + uint8_t **out_digest, size_t *out_size); + +/*! + * \brief Check whether exactly one ZONEMD exists in the zone, is valid and matches given algorithm. + * + * \note Special value 255 of algorithm means that ZONEMD shall not exist. + * + * \param contents Zone contents to be verified. + * \param alg Required algorithm of the ZONEMD. + * \param no_verify Don't verify the validness of the digest in ZONEMD. + */ +bool zone_contents_digest_exists(const zone_contents_t *contents, int alg, bool no_verify); + +/*! + * \brief Verify zone dgest in ZONEMD record. + * + * \param contents Zone contents ot be verified. + * + * \retval KNOT_EEMPTYZONE The zone is empty. + * \retval KNOT_ENOENT There is no ZONEMD in contents' apex. + * \retval KNOT_ENOTSUP None of present ZONEMD is supported (scheme+algorithm+SOAserial). + * \retval KNOT_ESEMCHECK Duplicate ZONEMD with identical scheme+algorithm pair. + * \retval KNOT_EFEWDATA Error in hash length. + * \retval KNOT_EMALF The computed hash differs from ZONEMD. + * \return KNOT_E* + */ +int zone_contents_digest_verify(const zone_contents_t *contents); + +struct zone_update; +/*! + * \brief Add ZONEMD record to zone_update. + * + * \param update Update with contents to be digested. + * \param algorithm ZONEMD algorithm. + * \param placeholder Don't calculate, just put placeholder (if ZONEMD not yet present). + * + * \note Special value 255 of algorithm means to remove ZONEMD. + * + * \return KNOT_E* + */ +int zone_update_add_digest(struct zone_update *update, int algorithm, bool placeholder); diff --git a/src/knot/zone/measure.c b/src/knot/zone/measure.c new file mode 100644 index 0000000..4c3ab5e --- /dev/null +++ b/src/knot/zone/measure.c @@ -0,0 +1,133 @@ +/* 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/zone/measure.h" + +measure_t knot_measure_init(bool measure_whole, bool measure_diff) +{ + assert(!measure_whole || !measure_diff); + measure_t m = { 0 }; + if (measure_whole) { + m.how_size = MEASURE_SIZE_WHOLE; + m.how_ttl = MEASURE_TTL_WHOLE; + } + if (measure_diff) { + m.how_size = MEASURE_SIZE_DIFF; + m.how_ttl = MEASURE_TTL_DIFF; + } + return m; +} + +bool knot_measure_node(zone_node_t *node, measure_t *m) +{ + if (m->how_size == MEASURE_SIZE_NONE && (m->how_ttl == MEASURE_TTL_NONE || + (m->how_ttl == MEASURE_TTL_LIMIT && m->max_ttl >= m->limit_max_ttl))) { + return false; + } + + int rrset_count = node->rrset_count; + for (int i = 0; i < rrset_count; i++) { + if (m->how_size != MEASURE_SIZE_NONE) { + knot_rrset_t rrset = node_rrset_at(node, i); + m->zone_size += knot_rrset_size(&rrset); + } + if (m->how_ttl != MEASURE_TTL_NONE) { + m->max_ttl = MAX(m->max_ttl, node->rrs[i].ttl); + } + } + + if (m->how_size != MEASURE_SIZE_DIFF && m->how_ttl != MEASURE_TTL_DIFF) { + return true; + } + + node = binode_counterpart(node); + rrset_count = node->rrset_count; + for (int i = 0; i < rrset_count; i++) { + if (m->how_size == MEASURE_SIZE_DIFF) { + knot_rrset_t rrset = node_rrset_at(node, i); + m->zone_size -= knot_rrset_size(&rrset); + } + if (m->how_ttl == MEASURE_TTL_DIFF) { + m->rem_max_ttl = MAX(m->rem_max_ttl, node->rrs[i].ttl); + } + } + + return true; +} + +static uint32_t re_measure_max_ttl(zone_contents_t *zone, uint32_t limit) +{ + measure_t m = {0 }; + m.how_ttl = MEASURE_TTL_LIMIT; + m.limit_max_ttl = limit; + + zone_tree_it_t it = { 0 }; + int ret = zone_tree_it_double_begin(zone->nodes, zone->nsec3_nodes, &it); + if (ret != KNOT_EOK) { + return limit; + } + + while (!zone_tree_it_finished(&it) && knot_measure_node(zone_tree_it_val(&it), &m)) { + zone_tree_it_next(&it); + } + zone_tree_it_free(&it); + + return m.max_ttl; +} + +void knot_measure_finish_zone(measure_t *m, zone_contents_t *zone) +{ + assert(m->how_size == MEASURE_SIZE_WHOLE || m->how_size == MEASURE_SIZE_NONE); + assert(m->how_ttl == MEASURE_TTL_WHOLE || m->how_ttl == MEASURE_TTL_NONE); + if (m->how_size == MEASURE_SIZE_WHOLE) { + zone->size = m->zone_size; + } + if (m->how_ttl == MEASURE_TTL_WHOLE) { + zone->max_ttl = m->max_ttl; + } +} + +void knot_measure_finish_update(measure_t *m, zone_update_t *update) +{ + switch (m->how_size) { + case MEASURE_SIZE_NONE: + break; + case MEASURE_SIZE_WHOLE: + update->new_cont->size = m->zone_size; + break; + case MEASURE_SIZE_DIFF: + update->new_cont->size = update->zone->contents->size + m->zone_size; + break; + } + + switch (m->how_ttl) { + case MEASURE_TTL_NONE: + break; + case MEASURE_TTL_WHOLE: + case MEASURE_TTL_LIMIT: + update->new_cont->max_ttl = m->max_ttl; + break; + case MEASURE_TTL_DIFF: + if (m->max_ttl >= update->zone->contents->max_ttl) { + update->new_cont->max_ttl = m->max_ttl; + } else if (update->zone->contents->max_ttl > m->rem_max_ttl) { + update->new_cont->max_ttl = update->zone->contents->max_ttl; + } else { + update->new_cont->max_ttl = re_measure_max_ttl(update->new_cont, update->zone->contents->max_ttl); + } + break; + } +} diff --git a/src/knot/zone/measure.h b/src/knot/zone/measure.h new file mode 100644 index 0000000..5c73c91 --- /dev/null +++ b/src/knot/zone/measure.h @@ -0,0 +1,71 @@ +/* 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 "knot/updates/zone-update.h" + +typedef enum { + MEASURE_SIZE_NONE = 0, // don't measure size of zone + MEASURE_SIZE_WHOLE, // measure complete size of zone nodes + MEASURE_SIZE_DIFF, // measure difference in size for bi-nodes in zone update +} measure_size_t; + +typedef enum { + MEASURE_TTL_NONE = 0, // don't measure max TTL of zone records + MEASURE_TTL_WHOLE, // measure max TTL among all zone records + MEASURE_TTL_DIFF, // check out zone update (bi-nodes) if the max TTL is affected + MEASURE_TTL_LIMIT, // measure max TTL whole; stop if a specific value is reached +} measure_ttl_t; + +typedef struct { + measure_size_t how_size; + measure_ttl_t how_ttl; + ssize_t zone_size; + uint32_t max_ttl; + uint32_t rem_max_ttl; + uint32_t limit_max_ttl; +} measure_t; + +/*! \brief Initialize measure struct. */ +measure_t knot_measure_init(bool measure_whole, bool measure_diff); + +/*! + * \brief Measure one node's size and max TTL, collecting into measure struct. + * + * \param node Node to be measured. + * \param m Measure context with instructions and results. + * + * \return False if no more measure is needed. + * \note You will probably ignore the return value. + */ +bool knot_measure_node(zone_node_t *node, measure_t *m); + +/*! + * \brief Collect the measured results and update the new zone with measured properties. + * + * \param zone Zone. + * \param m Measured results. + */ +void knot_measure_finish_zone(measure_t *m, zone_contents_t *zone); + +/*! + * \brief Collect the measured results and update the new zone with measured properties. + * + * \param update Zone update with the zone. + * \param m Measured results. + */ +void knot_measure_finish_update(measure_t *m, zone_update_t *update); diff --git a/src/knot/zone/node.c b/src/knot/zone/node.c new file mode 100644 index 0000000..291454b --- /dev/null +++ b/src/knot/zone/node.c @@ -0,0 +1,464 @@ +/* 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/zone/node.h" +#include "libknot/libknot.h" + +void additional_clear(additional_t *additional) +{ + if (additional == NULL) { + return; + } + + free(additional->glues); + free(additional); +} + +bool additional_equal(additional_t *a, additional_t *b) +{ + if (a == NULL || b == NULL || a->count != b->count) { + return false; + } + for (int i = 0; i < a->count; i++) { + glue_t *ag = &a->glues[i], *bg = &b->glues[i]; + if (ag->ns_pos != bg->ns_pos || ag->optional != bg->optional || + binode_first((zone_node_t *)ag->node) != binode_first((zone_node_t *)bg->node)) { + return false; + } + } + return true; +} + +/*! \brief Clears allocated data in RRSet entry. */ +static void rr_data_clear(struct rr_data *data, knot_mm_t *mm) +{ + knot_rdataset_clear(&data->rrs, mm); + memset(data, 0, sizeof(*data)); +} + +/*! \brief Clears allocated data in RRSet entry. */ +static int rr_data_from(const knot_rrset_t *rrset, struct rr_data *data, knot_mm_t *mm) +{ + int ret = knot_rdataset_copy(&data->rrs, &rrset->rrs, mm); + if (ret != KNOT_EOK) { + return ret; + } + data->ttl = rrset->ttl; + data->type = rrset->type; + data->additional = NULL; + + return KNOT_EOK; +} + +/*! \brief Adds RRSet to node directly. */ +static int add_rrset_no_merge(zone_node_t *node, const knot_rrset_t *rrset, + knot_mm_t *mm) +{ + if (node == NULL) { + return KNOT_EINVAL; + } + + const size_t prev_nlen = node->rrset_count * sizeof(struct rr_data); + const size_t nlen = (node->rrset_count + 1) * sizeof(struct rr_data); + void *p = mm_realloc(mm, node->rrs, nlen, prev_nlen); + if (p == NULL) { + return KNOT_ENOMEM; + } + node->rrs = p; + + // ensure rrsets are sorted by rrtype + struct rr_data *insert_pos = node->rrs, *end = node->rrs + node->rrset_count; + while (insert_pos != end && insert_pos->type < rrset->type) { + insert_pos++; + } + memmove(insert_pos + 1, insert_pos, (uint8_t *)end - (uint8_t *)insert_pos); + + int ret = rr_data_from(rrset, insert_pos, mm); + if (ret != KNOT_EOK) { + return ret; + } + ++node->rrset_count; + + return KNOT_EOK; +} + +/*! \brief Checks if the added RR has the same TTL as the first RR in the node. */ +static bool ttl_changed(struct rr_data *node_data, const knot_rrset_t *rrset) +{ + if (rrset->type == KNOT_RRTYPE_RRSIG || node_data->rrs.count == 0) { + return false; + } + + return rrset->ttl != node_data->ttl; +} + +zone_node_t *node_new(const knot_dname_t *owner, bool binode, bool second, knot_mm_t *mm) +{ + zone_node_t *ret = mm_alloc(mm, (binode ? 2 : 1) * sizeof(zone_node_t)); + if (ret == NULL) { + return NULL; + } + memset(ret, 0, sizeof(*ret)); + + if (owner) { + ret->owner = knot_dname_copy(owner, mm); + if (ret->owner == NULL) { + mm_free(mm, ret); + return NULL; + } + } + + // Node is authoritative by default. + ret->flags = NODE_FLAGS_AUTH; + + if (binode) { + ret->flags |= NODE_FLAGS_BINODE; + if (second) { + ret->flags |= NODE_FLAGS_DELETED; + } + memcpy(ret + 1, ret, sizeof(*ret)); + (ret + 1)->flags ^= NODE_FLAGS_SECOND | NODE_FLAGS_DELETED; + } + + return ret; +} + +zone_node_t *binode_counterpart(zone_node_t *node) +{ + zone_node_t *counterpart = NULL; + + assert(node == NULL || (node->flags & NODE_FLAGS_BINODE) || !(node->flags & NODE_FLAGS_SECOND)); + if (node != NULL && (node->flags & NODE_FLAGS_BINODE)) { + if ((node->flags & NODE_FLAGS_SECOND)) { + counterpart = node - 1; + assert(!(counterpart->flags & NODE_FLAGS_SECOND)); + } else { + counterpart = node + 1; + assert((counterpart->flags & NODE_FLAGS_SECOND)); + } + assert((counterpart->flags & NODE_FLAGS_BINODE)); + } + + return counterpart; +} + +void binode_unify(zone_node_t *node, bool free_deleted, knot_mm_t *mm) +{ + zone_node_t *counter = binode_counterpart(node); + if (counter != NULL) { + if (counter->rrs != node->rrs) { + for (uint16_t i = 0; i < counter->rrset_count; ++i) { + if (!binode_additional_shared(node, counter->rrs[i].type)) { + additional_clear(counter->rrs[i].additional); + } + if (!binode_rdata_shared(node, counter->rrs[i].type)) { + rr_data_clear(&counter->rrs[i], mm); + } + } + mm_free(mm, counter->rrs); + } + if (counter->nsec3_wildcard_name != node->nsec3_wildcard_name) { + free(counter->nsec3_wildcard_name); + } + if (!(counter->flags & NODE_FLAGS_NSEC3_NODE) && node->nsec3_hash != counter->nsec3_hash) { + free(counter->nsec3_hash); + } + assert(((node->flags ^ counter->flags) & NODE_FLAGS_SECOND)); + memcpy(counter, node, sizeof(*counter)); + counter->flags ^= NODE_FLAGS_SECOND; + + if (free_deleted && (node->flags & NODE_FLAGS_DELETED)) { + node_free(node, mm); + } + } +} + +int binode_prepare_change(zone_node_t *node, knot_mm_t *mm) +{ + zone_node_t *counter = binode_counterpart(node); + if (counter != NULL && counter->rrs == node->rrs && counter->rrs != NULL) { + size_t rrlen = sizeof(struct rr_data) * counter->rrset_count; + node->rrs = mm_alloc(mm, rrlen); + if (node->rrs == NULL) { + return KNOT_ENOMEM; + } + memcpy(node->rrs, counter->rrs, rrlen); + } + return KNOT_EOK; +} + +bool binode_rdata_shared(zone_node_t *node, uint16_t type) +{ + if (node == NULL || !(node->flags & NODE_FLAGS_BINODE)) { + return false; + } + zone_node_t *counterpart = ((node->flags & NODE_FLAGS_SECOND) ? node - 1 : node + 1); + if (counterpart->rrs == node->rrs) { + return true; + } + knot_rdataset_t *r1 = node_rdataset(node, type), *r2 = node_rdataset(counterpart, type); + return (r1 != NULL && r2 != NULL && r1->rdata == r2->rdata); +} + +static additional_t *node_type2addit(zone_node_t *node, uint16_t type) +{ + for (uint16_t i = 0; i < node->rrset_count; i++) { + if (node->rrs[i].type == type) { + return node->rrs[i].additional; + } + } + return NULL; +} + +bool binode_additional_shared(zone_node_t *node, uint16_t type) +{ + if (node == NULL || !(node->flags & NODE_FLAGS_BINODE)) { + return false; + } + zone_node_t *counter = ((node->flags & NODE_FLAGS_SECOND) ? node - 1 : node + 1); + if (counter->rrs == node->rrs) { + return true; + } + additional_t *a1 = node_type2addit(node, type), *a2 = node_type2addit(counter, type); + return (a1 == a2); +} + +bool binode_additionals_unchanged(zone_node_t *node, zone_node_t *counterpart) +{ + if (node == NULL || counterpart == NULL) { + return false; + } + if (counterpart->rrs == node->rrs) { + return true; + } + for (int i = 0; i < node->rrset_count; i++) { + struct rr_data *rr = &node->rrs[i]; + if (knot_rrtype_additional_needed(rr->type)) { + knot_rdataset_t *counterr = node_rdataset(counterpart, rr->type); + if (counterr == NULL || counterr->rdata != rr->rrs.rdata) { + return false; + } + } + } + for (int i = 0; i < counterpart->rrset_count; i++) { + struct rr_data *rr = &counterpart->rrs[i]; + if (knot_rrtype_additional_needed(rr->type)) { + knot_rdataset_t *counterr = node_rdataset(node, rr->type); + if (counterr == NULL || counterr->rdata != rr->rrs.rdata) { + return false; + } + } + } + return true; +} + +void node_free_rrsets(zone_node_t *node, knot_mm_t *mm) +{ + if (node == NULL) { + return; + } + + for (uint16_t i = 0; i < node->rrset_count; ++i) { + additional_clear(node->rrs[i].additional); + rr_data_clear(&node->rrs[i], mm); + } + + mm_free(mm, node->rrs); + node->rrs = NULL; + node->rrset_count = 0; +} + +void node_free(zone_node_t *node, knot_mm_t *mm) +{ + if (node == NULL) { + return; + } + + knot_dname_free(node->owner, mm); + + assert((node->flags & NODE_FLAGS_BINODE) || !(node->flags & NODE_FLAGS_SECOND)); + assert(binode_counterpart(node) == NULL || + binode_counterpart(node)->nsec3_wildcard_name == node->nsec3_wildcard_name); + + free(node->nsec3_wildcard_name); + if (!(node->flags & NODE_FLAGS_NSEC3_NODE)) { + free(node->nsec3_hash); + } + + if (node->rrs != NULL) { + mm_free(mm, node->rrs); + } + + mm_free(mm, binode_node(node, false)); +} + +int node_add_rrset(zone_node_t *node, const knot_rrset_t *rrset, knot_mm_t *mm) +{ + if (node == NULL || rrset == NULL) { + return KNOT_EINVAL; + } + + node->flags &= ~NODE_FLAGS_RRSIGS_VALID; + + for (uint16_t i = 0; i < node->rrset_count; ++i) { + if (node->rrs[i].type == rrset->type) { + struct rr_data *node_data = &node->rrs[i]; + const bool ttl_change = ttl_changed(node_data, rrset); + if (ttl_change) { + node_data->ttl = rrset->ttl; + } + + int ret = knot_rdataset_merge(&node_data->rrs, + &rrset->rrs, mm); + if (ret != KNOT_EOK) { + return ret; + } else { + return ttl_change ? KNOT_ETTL : KNOT_EOK; + } + } + } + + // New RRSet (with one RR) + return add_rrset_no_merge(node, rrset, mm); +} + +void node_remove_rdataset(zone_node_t *node, uint16_t type) +{ + if (node == NULL) { + return; + } + + node->flags &= ~NODE_FLAGS_RRSIGS_VALID; + + for (int i = 0; i < node->rrset_count; ++i) { + if (node->rrs[i].type == type) { + if (!binode_additional_shared(node, type)) { + additional_clear(node->rrs[i].additional); + } + if (!binode_rdata_shared(node, type)) { + rr_data_clear(&node->rrs[i], NULL); + } + memmove(node->rrs + i, node->rrs + i + 1, + (node->rrset_count - i - 1) * sizeof(struct rr_data)); + --node->rrset_count; + return; + } + } +} + +int node_remove_rrset(zone_node_t *node, const knot_rrset_t *rrset, knot_mm_t *mm) +{ + if (node == NULL || rrset == NULL) { + return KNOT_EINVAL; + } + + knot_rdataset_t *node_rrs = node_rdataset(node, rrset->type); + if (node_rrs == NULL) { + return KNOT_ENOENT; + } + + node->flags &= ~NODE_FLAGS_RRSIGS_VALID; + + int ret = knot_rdataset_subtract(node_rrs, &rrset->rrs, mm); + if (ret != KNOT_EOK) { + return ret; + } + + if (node_rrs->count == 0) { + node_remove_rdataset(node, rrset->type); + } + + return KNOT_EOK; +} + +knot_rrset_t *node_create_rrset(const zone_node_t *node, uint16_t type) +{ + if (node == NULL) { + return NULL; + } + + for (uint16_t i = 0; i < node->rrset_count; ++i) { + if (node->rrs[i].type == type) { + knot_rrset_t rrset = node_rrset_at(node, i); + return knot_rrset_copy(&rrset, NULL); + } + } + + return NULL; +} + +knot_rdataset_t *node_rdataset(const zone_node_t *node, uint16_t type) +{ + if (node == NULL) { + return NULL; + } + + for (uint16_t i = 0; i < node->rrset_count; ++i) { + if (node->rrs[i].type == type) { + return &node->rrs[i].rrs; + } + } + + return NULL; +} + +bool node_rrtype_is_signed(const zone_node_t *node, uint16_t type) +{ + if (node == NULL) { + return false; + } + + const knot_rdataset_t *rrsigs = node_rdataset(node, KNOT_RRTYPE_RRSIG); + if (rrsigs == NULL) { + return false; + } + + uint16_t rrsigs_rdata_count = rrsigs->count; + knot_rdata_t *rrsig = rrsigs->rdata; + for (uint16_t i = 0; i < rrsigs_rdata_count; ++i) { + if (knot_rrsig_type_covered(rrsig) == type) { + return true; + } + rrsig = knot_rdataset_next(rrsig); + } + + return false; +} + +bool node_bitmap_equal(const zone_node_t *a, const zone_node_t *b) +{ + if (a == NULL || b == NULL || a->rrset_count != b->rrset_count) { + return false; + } + + uint16_t i; + // heuristics: try if they are equal including order + for (i = 0; i < a->rrset_count; i++) { + if (a->rrs[i].type != b->rrs[i].type) { + break; + } + } + if (i == a->rrset_count) { + return true; + } + + for (i = 0; i < a->rrset_count; i++) { + if (node_rdataset(b, a->rrs[i].type) == NULL) { + return false; + } + } + return true; +} diff --git a/src/knot/zone/node.h b/src/knot/zone/node.h new file mode 100644 index 0000000..d30cc6e --- /dev/null +++ b/src/knot/zone/node.h @@ -0,0 +1,419 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/macros.h" +#include "contrib/mempattern.h" +#include "libknot/descriptor.h" +#include "libknot/dname.h" +#include "libknot/rrset.h" +#include "libknot/rdataset.h" + +struct rr_data; + +/*! + * \brief Structure representing one node in a domain name tree, i.e. one domain + * name in a zone. + */ +typedef struct zone_node { + knot_dname_t *owner; /*!< Domain name being the owner of this node. */ + struct zone_node *parent; /*!< Parent node in the name hierarchy. */ + + /*! \brief Array with data of RRSets belonging to this node. */ + struct rr_data *rrs; + + /*! + * \brief Previous node in canonical order. Only authoritative + * nodes or delegation points are referenced by this. + */ + struct zone_node *prev; + union { + knot_dname_t *nsec3_hash; /*! Name of the NSEC3 corresponding to this node. */ + struct zone_node *nsec3_node; /*! NSEC3 node corresponding to this node. + \warning This always points to first part of that bi-node! + assert(!(node->nsec3_node & NODE_FLAGS_SECOND)); */ + }; + knot_dname_t *nsec3_wildcard_name; /*! Name of NSEC3 node proving wildcard nonexistence. */ + uint32_t children; /*!< Count of children nodes in DNS hierarchy. */ + uint16_t rrset_count; /*!< Number of RRSets stored in the node. */ + uint16_t flags; /*!< \ref node_flags enum. */ +} zone_node_t; + +/*!< \brief Glue node context. */ +typedef struct { + const zone_node_t *node; /*!< Glue node. */ + uint16_t ns_pos; /*!< Corresponding NS record position (for compression). */ + bool optional; /*!< Optional glue indicator. */ +} glue_t; + +/*!< \brief Additional data. */ +typedef struct { + glue_t *glues; /*!< Glue data. */ + uint16_t count; /*!< Number of glue nodes. */ +} additional_t; + +/*!< \brief Structure storing RR data. */ +struct rr_data { + uint32_t ttl; /*!< RRSet TTL. */ + uint16_t type; /*!< RR type of data. */ + knot_rdataset_t rrs; /*!< Data of given type. */ + additional_t *additional; /*!< Additional nodes with glues. */ +}; + +/*! \brief Flags used to mark nodes with some property. */ +enum node_flags { + /*! \brief Node is authoritative, default. */ + NODE_FLAGS_AUTH = 0 << 0, + /*! \brief Node is a delegation point (i.e. marking a zone cut). */ + NODE_FLAGS_DELEG = 1 << 0, + /*! \brief Node is not authoritative (i.e. below a zone cut). */ + NODE_FLAGS_NONAUTH = 1 << 1, + /*! \brief RRSIGs in node have been cryptographically validated by Knot. */ + NODE_FLAGS_RRSIGS_VALID = 1 << 2, + /*! \brief Node is empty and will be deleted after update. */ + NODE_FLAGS_EMPTY = 1 << 3, + /*! \brief Node has a wildcard child. */ + NODE_FLAGS_WILDCARD_CHILD = 1 << 4, + /*! \brief Is this NSEC3 node compatible with zone's NSEC3PARAMS ? */ + NODE_FLAGS_IN_NSEC3_CHAIN = 1 << 5, + /*! \brief Node is the zone Apex. */ + NODE_FLAGS_APEX = 1 << 6, + /*! \brief The nsec3_node pointer is valid and and nsec3_hash pointer invalid. */ + NODE_FLAGS_NSEC3_NODE = 1 << 7, + /*! \brief Is this i bi-node? */ + NODE_FLAGS_BINODE = 1 << 8, // this value shall be fixed + /*! \brief Is this the second half of bi-node? */ + NODE_FLAGS_SECOND = 1 << 9, // this value shall be fixed + /*! \brief The node shall be deleted. It's just not because it's a bi-node and the counterpart still exists. */ + NODE_FLAGS_DELETED = 1 << 10, + /*! \brief The node or some node in subtree has some authoritative data in it (possibly also DS at deleg). */ + NODE_FLAGS_SUBTREE_AUTH = 1 << 11, + /*! \brief The node or some node in subtree has any data in it, possibly just insec deleg. */ + NODE_FLAGS_SUBTREE_DATA = 1 << 12, +}; + +typedef void (*node_addrem_cb)(zone_node_t *, void *); +typedef zone_node_t *(*node_new_cb)(const knot_dname_t *, void *); + +/*! + * \brief Clears additional structure. + * + * \param additional Additional to clear. + */ +void additional_clear(additional_t *additional); + +/*! + * \brief Compares additional structures on equivalency. + */ +bool additional_equal(additional_t *a, additional_t *b); + +/*! + * \brief Creates and initializes new node structure. + * + * \param owner Node's owner, will be duplicated. + * \param binode Create bi-node. + * \param second The second part of the bi-node shall be used now. + * \param mm Memory context to use. + * + * \return Newly created node or NULL if an error occurred. + */ +zone_node_t *node_new(const knot_dname_t *owner, bool binode, bool second, knot_mm_t *mm); + +/*! + * \brief Synchronize contents of both binode's nodes. + * + * \param node Pointer to either of nodes in a binode. + * \param free_deleted When the unified node has DELETED flag, free it afterwards. + * \param mm Memory context. + */ +void binode_unify(zone_node_t *node, bool free_deleted, knot_mm_t *mm); + +/*! + * \brief This must be called before any change to either of the bi-node's node's rdatasets. + */ +int binode_prepare_change(zone_node_t *node, knot_mm_t *mm); + +/*! + * \brief Get the correct node of a binode. + * + * \param node Pointer to either of nodes in a binode. + * \param second Get the second node (first otherwise). + * + * \return Pointer to correct node. + */ +inline static zone_node_t *binode_node(zone_node_t *node, bool second) +{ + if (unlikely(node == NULL || !(node->flags & NODE_FLAGS_BINODE))) { + assert(node == NULL || !(node->flags & NODE_FLAGS_SECOND)); + return node; + } + return node + (second - (int)((node->flags & NODE_FLAGS_SECOND) >> 9)); +} + +inline static zone_node_t *binode_first(zone_node_t *node) +{ + return binode_node(node, false); +} + +inline static zone_node_t *binode_node_as(zone_node_t *node, const zone_node_t *as) +{ + assert(node == NULL || (as->flags & NODE_FLAGS_BINODE) == (node->flags & NODE_FLAGS_BINODE)); + return binode_node(node, (as->flags & NODE_FLAGS_SECOND)); +} + +/*! + * \brief Return the other node from a bi-node. + * + * \param node A node in a bi-node. + * + * \return The counterpart node in the same bi-node. + */ +zone_node_t *binode_counterpart(zone_node_t *node); + +/*! + * \brief Return true if the rdataset of specified type is shared (shallow-copied) among both parts of bi-node. + */ +bool binode_rdata_shared(zone_node_t *node, uint16_t type); + +/*! + * \brief Return true if the additionals to rdataset of specified type are shared among both parts of bi-node. + */ +bool binode_additional_shared(zone_node_t *node, uint16_t type); + +/*! + * \brief Return true if the additionals are unchanged between two nodes (usually a bi-node). + */ +bool binode_additionals_unchanged(zone_node_t *node, zone_node_t *counterpart); + +/*! + * \brief Destroys allocated data within the node + * structure, but not the node itself. + * + * \param node Node that contains data to be destroyed. + * \param mm Memory context to use. + */ +void node_free_rrsets(zone_node_t *node, knot_mm_t *mm); + +/*! + * \brief Destroys the node structure. + * + * Does not destroy the data within the node. + * + * \param node Node to be destroyed. + * \param mm Memory context to use. + */ +void node_free(zone_node_t *node, knot_mm_t *mm); + +/*! + * \brief Adds an RRSet to the node. All data are copied. Owner and class are + * not used at all. + * + * \param node Node to add the RRSet to. + * \param rrset RRSet to add. + * \param mm Memory context to use. + * + * \return KNOT_E* + * \retval KNOT_ETTL RRSet TTL was updated. + */ +int node_add_rrset(zone_node_t *node, const knot_rrset_t *rrset, knot_mm_t *mm); + +/*! + * \brief Removes data for given RR type from node. + * + * \param node Node we want to delete from. + * \param type RR type to delete. + */ +void node_remove_rdataset(zone_node_t *node, uint16_t type); + +/*! + * \brief Remove all RRs from RRSet from the node. + * + * \param node Node to remove from. + * \param rrset RRSet with RRs to be removed. + * \param mm Memory context. + * + * \return KNOT_E* + */ +int node_remove_rrset(zone_node_t *node, const knot_rrset_t *rrset, knot_mm_t *mm); + +/*! + * \brief Returns the RRSet of the given type from the node. RRSet is allocated. + * + * \param node Node to get the RRSet from. + * \param type RR type of the RRSet to retrieve. + * + * \return RRSet from node \a node having type \a type, or NULL if no such + * RRSet exists in this node. + */ +knot_rrset_t *node_create_rrset(const zone_node_t *node, uint16_t type); + +/*! + * \brief Gets rdata set structure of given type from node. + * + * \param node Node to get data from. + * \param type RR type of data to get. + * + * \return Pointer to data if found, NULL otherwise. + */ +knot_rdataset_t *node_rdataset(const zone_node_t *node, uint16_t type); + +/*! + * \brief Returns parent node (fixing bi-node issue) of given node. + */ +inline static zone_node_t *node_parent(const zone_node_t *node) +{ + return binode_node_as(node->parent, node); +} + +/*! + * \brief Returns previous (lexicographically in same zone tree) node (fixing bi-node issue) of given node. + */ +inline static zone_node_t *node_prev(const zone_node_t *node) +{ + return binode_node_as(node->prev, node); +} + +/*! + * \brief Return node referenced by a glue. + * + * \param glue Glue in question. + * \param another_zone_node Another node from the same zone. + * + * \return Glue node. + */ +inline static const zone_node_t *glue_node(const glue_t *glue, const zone_node_t *another_zone_node) +{ + return binode_node_as((zone_node_t *)glue->node, another_zone_node); +} + +/*! + * \brief Add a flag to this node and all (grand-)parents until the flag is present. + */ +inline static void node_set_flag_hierarch(zone_node_t *node, uint16_t fl) +{ + for (zone_node_t *i = node; i != NULL && (i->flags & fl) != fl; i = node_parent(i)) { + i->flags |= fl; + } +} + +/*! + * \brief Checks whether node contains any RRSIG for given type. + * + * \param node Node to check in. + * \param type Type to check for. + * + * \return True/False. + */ +bool node_rrtype_is_signed(const zone_node_t *node, uint16_t type); + +/*! + * \brief Checks whether node contains RRSet for given type. + * + * \param node Node to check in. + * \param type Type to check for. + * + * \return True/False. + */ +inline static bool node_rrtype_exists(const zone_node_t *node, uint16_t type) +{ + return node_rdataset(node, type) != NULL; +} + +/*! + * \brief Checks whether node is empty. Node is empty when NULL or when no + * RRSets are in it. + * + * \param node Node to check in. + * + * \return True/False. + */ +inline static bool node_empty(const zone_node_t *node) +{ + return node == NULL || node->rrset_count == 0; +} + +/*! + * \brief Check whether two nodes have equal set of rrtypes. + * + * \param a A node. + * \param b Another node. + * + * \return True/False. + */ +bool node_bitmap_equal(const zone_node_t *a, const zone_node_t *b); + +/*! + * \brief Returns RRSet structure initialized with data from node. + * + * \param node Node containing RRSet. + * \param type RRSet type we want to get. + * + * \return RRSet structure with wanted type, or empty RRSet. + */ +static inline knot_rrset_t node_rrset(const zone_node_t *node, uint16_t type) +{ + knot_rrset_t rrset; + for (uint16_t i = 0; node && i < node->rrset_count; ++i) { + if (node->rrs[i].type == type) { + struct rr_data *rr_data = &node->rrs[i]; + knot_rrset_init(&rrset, node->owner, type, KNOT_CLASS_IN, + rr_data->ttl); + rrset.rrs = rr_data->rrs; + rrset.additional = rr_data->additional; + return rrset; + } + } + knot_rrset_init_empty(&rrset); + return rrset; +} + +/*! + * \brief Returns RRSet structure initialized with data from node at position + * equal to \a pos. + * + * \param node Node containing RRSet. + * \param pos RRSet position we want to get. + * + * \return RRSet structure with data from wanted position, or empty RRSet. + */ +static inline knot_rrset_t node_rrset_at(const zone_node_t *node, size_t pos) +{ + knot_rrset_t rrset; + if (node == NULL || pos >= node->rrset_count) { + knot_rrset_init_empty(&rrset); + return rrset; + } + + struct rr_data *rr_data = &node->rrs[pos]; + knot_rrset_init(&rrset, node->owner, rr_data->type, KNOT_CLASS_IN, + rr_data->ttl); + rrset.rrs = rr_data->rrs; + rrset.additional = rr_data->additional; + return rrset; +} + +/*! + * \brief Return the relevant NSEC3 node (if specified by adjusting), or NULL. + */ +static inline zone_node_t *node_nsec3_get(const zone_node_t *node) +{ + if (!(node->flags & NODE_FLAGS_NSEC3_NODE) || node->nsec3_node == NULL) { + return NULL; + } else { + return binode_node_as(node->nsec3_node, node); + } +} diff --git a/src/knot/zone/semantic-check.c b/src/knot/zone/semantic-check.c new file mode 100644 index 0000000..b3f1930 --- /dev/null +++ b/src/knot/zone/semantic-check.c @@ -0,0 +1,562 @@ +/* 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 "knot/zone/semantic-check.h" + +#include "libdnssec/error.h" +#include "libdnssec/key.h" +#include "contrib/string.h" +#include "libknot/libknot.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/zone-keys.h" +#include "knot/updates/zone-update.h" + +static const char *error_messages[SEM_ERR_UNKNOWN + 1] = { + [SEM_ERR_SOA_NONE] = + "missing SOA at the zone apex", + + [SEM_ERR_CNAME_EXTRA_RECORDS] = + "another record exists beside CNAME", + [SEM_ERR_CNAME_MULTIPLE] = + "multiple CNAME records", + + [SEM_ERR_DNAME_CHILDREN] = + "child record exists under DNAME", + [SEM_ERR_DNAME_MULTIPLE] = + "multiple DNAME records", + [SEM_ERR_DNAME_EXTRA_NS] = + "NS record exists beside DNAME", + + [SEM_ERR_NS_APEX] = + "missing NS at the zone apex", + [SEM_ERR_NS_GLUE] = + "missing glue record", + + [SEM_ERR_RRSIG_UNVERIFIABLE] = + "no valid signature for a record", + + [SEM_ERR_NSEC_NONE] = + "missing NSEC(3) record", + [SEM_ERR_NSEC_RDATA_BITMAP] = + "wrong NSEC(3) bitmap", + [SEM_ERR_NSEC_RDATA_CHAIN] = + "inconsistent NSEC(3) chain", + [SEM_ERR_NSEC3_INSECURE_DELEGATION_OPT] = + "wrong NSEC3 opt-out", + + [SEM_ERR_NSEC3PARAM_RDATA_FLAGS] = + "invalid flags in NSEC3PARAM", + [SEM_ERR_NSEC3PARAM_RDATA_ALG] = + "invalid algorithm in NSEC3PARAM", + + [SEM_ERR_DS_RDATA_ALG] = + "invalid algorithm in DS", + [SEM_ERR_DS_RDATA_DIGLEN] = + "invalid digest length in DS", + + [SEM_ERR_DNSKEY_NONE] = + "missing DNSKEY", + [SEM_ERR_DNSKEY_INVALID] = + "invalid DNSKEY", + + [SEM_ERR_CDS_NONE] = + "missing CDS", + [SEM_ERR_CDS_NOT_MATCH] = + "CDS not match CDNSKEY", + + [SEM_ERR_CDNSKEY_NONE] = + "missing CDNSKEY", + [SEM_ERR_CDNSKEY_NO_DNSKEY] = + "CDNSKEY not match DNSKEY", + [SEM_ERR_CDNSKEY_NO_CDS] = + "CDNSKEY without corresponding CDS", + [SEM_ERR_CDNSKEY_INVALID_DELETE] = + "invalid CDNSKEY/CDS for DNSSEC delete algorithm", + + [SEM_ERR_UNKNOWN] = + "unknown error" +}; + +const char *sem_error_msg(sem_error_t code) +{ + if (code > SEM_ERR_UNKNOWN) { + code = SEM_ERR_UNKNOWN; + } + return error_messages[code]; +} + +typedef enum { + MANDATORY = 1 << 0, + SOFT = 1 << 1, + OPTIONAL = 1 << 2, + DNSSEC = 1 << 3, +} check_level_t; + +typedef struct { + zone_contents_t *zone; + sem_handler_t *handler; + check_level_t level; + time_t time; +} semchecks_data_t; + +static int check_soa(const zone_node_t *node, semchecks_data_t *data); +static int check_cname(const zone_node_t *node, semchecks_data_t *data); +static int check_dname(const zone_node_t *node, semchecks_data_t *data); +static int check_delegation(const zone_node_t *node, semchecks_data_t *data); +static int check_nsec3param(const zone_node_t *node, semchecks_data_t *data); +static int check_submission(const zone_node_t *node, semchecks_data_t *data); +static int check_ds(const zone_node_t *node, semchecks_data_t *data); + +struct check_function { + int (*function)(const zone_node_t *, semchecks_data_t *); + check_level_t level; +}; + +static const struct check_function CHECK_FUNCTIONS[] = { + { check_soa, MANDATORY }, + { check_cname, MANDATORY | SOFT }, + { check_dname, MANDATORY | SOFT }, + { check_delegation, MANDATORY | SOFT }, // mandatory for apex, optional for others + { check_ds, OPTIONAL }, + { check_nsec3param, DNSSEC }, + { check_submission, DNSSEC }, +}; + +static const int CHECK_FUNCTIONS_LEN = sizeof(CHECK_FUNCTIONS) + / sizeof(struct check_function); + +static int check_delegation(const zone_node_t *node, semchecks_data_t *data) +{ + if (!((node->flags & NODE_FLAGS_DELEG) || data->zone->apex == node)) { + return KNOT_EOK; + } + + // always check zone apex + if (!(data->level & OPTIONAL) && data->zone->apex != node) { + return KNOT_EOK; + } + + const knot_rdataset_t *ns_rrs = node_rdataset(node, KNOT_RRTYPE_NS); + if (ns_rrs == NULL) { + assert(data->zone->apex == node); + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_NS_APEX, NULL); + return KNOT_EOK; + } + + // check glue record for delegation + for (int i = 0; i < ns_rrs->count; ++i) { + knot_rdata_t *ns_rr = knot_rdataset_at(ns_rrs, i); + const knot_dname_t *ns_dname = knot_ns_name(ns_rr); + const zone_node_t *glue_node = NULL, *glue_encloser = NULL; + int ret = zone_contents_find_dname(data->zone, ns_dname, &glue_node, + &glue_encloser, NULL); + switch (ret) { + case KNOT_EOUTOFZONE: + continue; // NS is out of bailiwick + case ZONE_NAME_NOT_FOUND: + if (glue_encloser != node && + glue_encloser->flags & (NODE_FLAGS_DELEG | NODE_FLAGS_NONAUTH)) { + continue; // NS is below another delegation + } + + // check if covered by wildcard + knot_dname_storage_t wildcard = "\x01""*"; + knot_dname_to_wire(wildcard + 2, glue_encloser->owner, + sizeof(wildcard) - 2); + glue_node = zone_contents_find_node(data->zone, wildcard); + break; // continue in checking glue existence + case ZONE_NAME_FOUND: + break; // continue in checking glue existence + default: + return ret; + } + if (!node_rrtype_exists(glue_node, KNOT_RRTYPE_A) && + !node_rrtype_exists(glue_node, KNOT_RRTYPE_AAAA)) { + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_NS_GLUE, NULL); + } + } + + return KNOT_EOK; +} + +static int check_submission(const zone_node_t *node, semchecks_data_t *data) +{ + const knot_rdataset_t *cdss = node_rdataset(node, KNOT_RRTYPE_CDS); + const knot_rdataset_t *cdnskeys = node_rdataset(node, KNOT_RRTYPE_CDNSKEY); + if (cdss == NULL && cdnskeys == NULL) { + return KNOT_EOK; + } else if (cdss == NULL) { + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_CDS_NONE, NULL); + return KNOT_EOK; + } else if (cdnskeys == NULL) { + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_CDNSKEY_NONE, NULL); + return KNOT_EOK; + } + + const knot_rdataset_t *dnskeys = node_rdataset(data->zone->apex, + KNOT_RRTYPE_DNSKEY); + if (dnskeys == NULL) { + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_DNSKEY_NONE, NULL); + } + + const uint8_t *empty_cds = (uint8_t *)"\x00\x00\x00\x00\x00"; + const uint8_t *empty_cdnskey = (uint8_t *)"\x00\x00\x03\x00\x00"; + bool delete_cds = false, delete_cdnskey = false; + + // check every CDNSKEY for corresponding DNSKEY + for (int i = 0; i < cdnskeys->count; i++) { + knot_rdata_t *cdnskey = knot_rdataset_at(cdnskeys, i); + + // skip delete-dnssec CDNSKEY + if (cdnskey->len == 5 && memcmp(cdnskey->data, empty_cdnskey, 5) == 0) { + delete_cdnskey = true; + continue; + } + + bool match = false; + for (int j = 0; dnskeys != NULL && j < dnskeys->count; j++) { + knot_rdata_t *dnskey = knot_rdataset_at(dnskeys, j); + + if (knot_rdata_cmp(dnskey, cdnskey) == 0) { + match = true; + break; + } + } + if (!match) { + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_CDNSKEY_NO_DNSKEY, NULL); + } + } + + // check every CDS for corresponding CDNSKEY + for (int i = 0; i < cdss->count; i++) { + knot_rdata_t *cds = knot_rdataset_at(cdss, i); + uint8_t digest_type = knot_ds_digest_type(cds); + + // skip delete-dnssec CDS + if (cds->len == 5 && memcmp(cds->data, empty_cds, 5) == 0) { + delete_cds = true; + continue; + } + + bool match = false; + for (int j = 0; j < cdnskeys->count; j++) { + knot_rdata_t *cdnskey = knot_rdataset_at(cdnskeys, j); + + dnssec_key_t *key; + int ret = dnssec_key_from_rdata(&key, data->zone->apex->owner, + cdnskey->data, cdnskey->len); + if (ret != KNOT_EOK) { + continue; + } + + dnssec_binary_t cds_calc = { 0 }; + dnssec_binary_t cds_orig = { .size = cds->len, .data = cds->data }; + ret = dnssec_key_create_ds(key, digest_type, &cds_calc); + if (ret != KNOT_EOK) { + dnssec_key_free(key); + return ret; + } + + ret = dnssec_binary_cmp(&cds_orig, &cds_calc); + dnssec_binary_free(&cds_calc); + dnssec_key_free(key); + if (ret == 0) { + match = true; + break; + } + } + if (!match) { + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_CDS_NOT_MATCH, NULL); + } + } + + // check delete-dnssec records + if ((delete_cds && (!delete_cdnskey || cdss->count > 1)) || + (delete_cdnskey && (!delete_cds || cdnskeys->count > 1))) { + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_CDNSKEY_INVALID_DELETE, NULL); + } + + // check orphaned CDS + if (cdss->count < cdnskeys->count) { + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_CDNSKEY_NO_CDS, NULL); + } + + return KNOT_EOK; +} + +static int check_ds(const zone_node_t *node, semchecks_data_t *data) +{ + const knot_rdataset_t *dss = node_rdataset(node, KNOT_RRTYPE_DS); + if (dss == NULL) { + return KNOT_EOK; + } + + for (int i = 0; i < dss->count; i++) { + knot_rdata_t *ds = knot_rdataset_at(dss, i); + uint16_t keytag = knot_ds_key_tag(ds); + uint8_t digest_type = knot_ds_digest_type(ds); + + char info[64] = ""; + (void)snprintf(info, sizeof(info), "(keytag %d)", keytag); + + if (!dnssec_algorithm_digest_support(digest_type)) { + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_DS_RDATA_ALG, info); + } else { + // Sizes for different digest algorithms. + const uint16_t digest_sizes [] = { 0, 20, 32, 32, 48}; + + uint16_t digest_size = knot_ds_digest_len(ds); + + if (digest_sizes[digest_type] != digest_size) { + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_DS_RDATA_DIGLEN, info); + } + } + } + + return KNOT_EOK; +} + +static int check_soa(const zone_node_t *node, semchecks_data_t *data) +{ + if (data->zone->apex != node) { + return KNOT_EOK; + } + + const knot_rdataset_t *soa_rrs = node_rdataset(node, KNOT_RRTYPE_SOA); + if (soa_rrs == NULL) { + data->handler->error = true; + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_SOA_NONE, NULL); + } + + return KNOT_EOK; +} + +static int check_cname(const zone_node_t *node, semchecks_data_t *data) +{ + const knot_rdataset_t *cname_rrs = node_rdataset(node, KNOT_RRTYPE_CNAME); + if (cname_rrs == NULL) { + return KNOT_EOK; + } + + unsigned rrset_limit = 1; + /* With DNSSEC node can contain RRSIGs or NSEC */ + if (node_rrtype_exists(node, KNOT_RRTYPE_NSEC)) { + rrset_limit += 1; + } + if (node_rrtype_exists(node, KNOT_RRTYPE_RRSIG)) { + rrset_limit += 1; + } + + if (node->rrset_count > rrset_limit) { + data->handler->error = true; + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_CNAME_EXTRA_RECORDS, NULL); + } + if (cname_rrs->count != 1) { + data->handler->error = true; + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_CNAME_MULTIPLE, NULL); + } + + return KNOT_EOK; +} + +static int check_dname(const zone_node_t *node, semchecks_data_t *data) +{ + const knot_rdataset_t *dname_rrs = node_rdataset(node, KNOT_RRTYPE_DNAME); + if (dname_rrs == NULL) { + return KNOT_EOK; + } + + /* RFC 6672 Section 2.3 Paragraph 3 */ + bool is_apex = (node->flags & NODE_FLAGS_APEX); + if (!is_apex && node_rrtype_exists(node, KNOT_RRTYPE_NS)) { + data->handler->error = true; + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_DNAME_EXTRA_NS, NULL); + } + /* RFC 6672 Section 2.4 Paragraph 1 */ + /* If the NSEC3 node of the apex is present, it is counted as apex's child. */ + unsigned allowed_children = (is_apex && node_nsec3_get(node) != NULL) ? 1 : 0; + if (node->children > allowed_children) { + data->handler->error = true; + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_DNAME_CHILDREN, NULL); + } + /* RFC 6672 Section 2.4 Paragraph 2 */ + if (dname_rrs->count != 1) { + data->handler->error = true; + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_DNAME_MULTIPLE, NULL); + } + + return KNOT_EOK; +} + +static int check_nsec3param(const zone_node_t *node, semchecks_data_t *data) +{ + if (data->zone->apex != node) { + return KNOT_EOK; + } + + const knot_rdataset_t *nsec3param_rrs = node_rdataset(node, KNOT_RRTYPE_NSEC3PARAM); + if (nsec3param_rrs == NULL) { + return KNOT_EOK; + } + + uint8_t param = knot_nsec3param_flags(nsec3param_rrs->rdata); + if ((param & ~1) != 0) { + data->handler->cb(data->handler, data->zone, data->zone->apex->owner, + SEM_ERR_NSEC3PARAM_RDATA_FLAGS, NULL); + } + + param = knot_nsec3param_alg(nsec3param_rrs->rdata); + if (param != DNSSEC_NSEC3_ALGORITHM_SHA1) { + data->handler->cb(data->handler, data->zone, data->zone->apex->owner, + SEM_ERR_NSEC3PARAM_RDATA_ALG, NULL); + } + + return KNOT_EOK; +} + +static int do_checks_in_tree(zone_node_t *node, void *data) +{ + semchecks_data_t *s_data = (semchecks_data_t *)data; + + int ret = KNOT_EOK; + + for (int i = 0; ret == KNOT_EOK && i < CHECK_FUNCTIONS_LEN; ++i) { + if (CHECK_FUNCTIONS[i].level & s_data->level) { + ret = CHECK_FUNCTIONS[i].function(node, s_data); + if (s_data->handler->fatal_error && + (CHECK_FUNCTIONS[i].level & SOFT) && + (s_data->level & SOFT)) { + s_data->handler->fatal_error = false; + } + } + } + + return ret; +} + +static sem_error_t err_dnssec2sem(int ret, uint16_t rrtype, char *info, size_t len) +{ + char type_str[16]; + + switch (ret) { + case KNOT_DNSSEC_ENOSIG: + if (knot_rrtype_to_string(rrtype, type_str, sizeof(type_str)) > 0) { + (void)snprintf(info, len, "(record type %s)", type_str); + } + return SEM_ERR_RRSIG_UNVERIFIABLE; + case KNOT_DNSSEC_ENONSEC: + return SEM_ERR_NSEC_NONE; + case KNOT_DNSSEC_ENSEC_BITMAP: + return SEM_ERR_NSEC_RDATA_BITMAP; + case KNOT_DNSSEC_ENSEC_CHAIN: + return SEM_ERR_NSEC_RDATA_CHAIN; + case KNOT_DNSSEC_ENSEC3_OPTOUT: + return SEM_ERR_NSEC3_INSECURE_DELEGATION_OPT; + default: + return SEM_ERR_UNKNOWN; + } +} + +static int verify_dnssec(zone_contents_t *zone, sem_handler_t *handler, time_t time) +{ + zone_update_t fake_up = { .new_cont = zone, }; + int ret = knot_dnssec_validate_zone(&fake_up, NULL, time, false); + if (fake_up.validation_hint.node != NULL) { // validation found an issue + char info[64] = ""; + sem_error_t err = err_dnssec2sem(ret, fake_up.validation_hint.rrtype, info, sizeof(info)); + handler->cb(handler, zone, fake_up.validation_hint.node, err, info); + return KNOT_EOK; + } else if (ret == KNOT_INVALID_PUBLIC_KEY) { // validation failed due to invalid DNSKEY + handler->cb(handler, zone, zone->apex->owner, SEM_ERR_DNSKEY_INVALID, NULL); + return KNOT_EOK; + } else { // validation failed by itself + return ret; + } +} + +int sem_checks_process(zone_contents_t *zone, semcheck_optional_t optional, sem_handler_t *handler, + time_t time) +{ + if (handler == NULL) { + return KNOT_EINVAL; + } + + if (zone == NULL) { + return KNOT_EEMPTYZONE; + } + + semchecks_data_t data = { + .handler = handler, + .zone = zone, + .level = MANDATORY, + .time = time, + }; + + switch (optional) { + case SEMCHECK_MANDATORY_SOFT: + data.level |= SOFT; + data.handler->soft_check = true; + break; + case SEMCHECK_DNSSEC_AUTO: + data.level |= OPTIONAL; + if (zone->dnssec) { + data.level |= DNSSEC; + } + break; + case SEMCHECK_DNSSEC_ON: + data.level |= OPTIONAL; + data.level |= DNSSEC; + break; + case SEMCHECK_DNSSEC_OFF: + data.level |= OPTIONAL; + break; + default: + break; + } + + int ret = zone_contents_apply(zone, do_checks_in_tree, &data); + if (ret != KNOT_EOK) { + return ret; + } + if (data.handler->fatal_error) { + return KNOT_ESEMCHECK; + } + + if (data.level & DNSSEC) { + ret = verify_dnssec(zone, handler, time); + } + + return ret; +} diff --git a/src/knot/zone/semantic-check.h b/src/knot/zone/semantic-check.h new file mode 100644 index 0000000..0318fc0 --- /dev/null +++ b/src/knot/zone/semantic-check.h @@ -0,0 +1,116 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <time.h> + +#include "knot/conf/schema.h" +#include "knot/zone/contents.h" + +typedef enum { + SEMCHECK_MANDATORY_ONLY = SEMCHECKS_OFF, + SEMCHECK_DNSSEC_AUTO = SEMCHECKS_ON, + SEMCHECK_MANDATORY_SOFT = SEMCHECKS_SOFT, + SEMCHECK_DNSSEC_OFF, + SEMCHECK_DNSSEC_ON, +} semcheck_optional_t; + +/*! + *\brief Internal error constants. + */ +typedef enum { + // Mandatory checks. + SEM_ERR_SOA_NONE, + + SEM_ERR_CNAME_EXTRA_RECORDS, + SEM_ERR_CNAME_MULTIPLE, + + SEM_ERR_DNAME_CHILDREN, + SEM_ERR_DNAME_MULTIPLE, + SEM_ERR_DNAME_EXTRA_NS, + + // Optional checks. + SEM_ERR_NS_APEX, + SEM_ERR_NS_GLUE, + + // DNSSEC checks. + SEM_ERR_RRSIG_UNVERIFIABLE, + + SEM_ERR_NSEC_NONE, + SEM_ERR_NSEC_RDATA_BITMAP, + SEM_ERR_NSEC_RDATA_CHAIN, + SEM_ERR_NSEC3_INSECURE_DELEGATION_OPT, + + SEM_ERR_NSEC3PARAM_RDATA_FLAGS, + SEM_ERR_NSEC3PARAM_RDATA_ALG, + + SEM_ERR_DS_RDATA_ALG, + SEM_ERR_DS_RDATA_DIGLEN, + + SEM_ERR_DNSKEY_NONE, + SEM_ERR_DNSKEY_INVALID, + + SEM_ERR_CDS_NONE, + SEM_ERR_CDS_NOT_MATCH, + + SEM_ERR_CDNSKEY_NONE, + SEM_ERR_CDNSKEY_NO_DNSKEY, + SEM_ERR_CDNSKEY_NO_CDS, + SEM_ERR_CDNSKEY_INVALID_DELETE, + + // General error! + SEM_ERR_UNKNOWN +} sem_error_t; + +const char *sem_error_msg(sem_error_t code); + +/*! + * \brief Structure for handling semantic errors. + */ +typedef struct sem_handler sem_handler_t; + +/*! + * \brief Callback for handle error. + */ +typedef void (*sem_callback) (sem_handler_t *ctx, const zone_contents_t *zone, + const knot_dname_t *node, sem_error_t error, const char *data); + +struct sem_handler { + sem_callback cb; + bool soft_check; + bool error; /* An error in the current check. */ + bool fatal_error; /* The checks detected at least one error. */ + bool warning; /* The checks detected at least one warning. */ +}; + +/*! + * \brief Check zone for semantic errors. + * + * Errors are logged in error handler. + * + * \param zone Zone to be searched / checked. + * \param optional To do also optional check. + * \param handler Semantic error handler. + * \param time Check zone at given time (rrsig expiration). + * + * \retval KNOT_EOK no error found + * \retval KNOT_ESEMCHECK found semantic error + * \retval KNOT_EEMPTYZONE the zone is empty + * \retval KNOT_EINVAL another error + */ +int sem_checks_process(zone_contents_t *zone, semcheck_optional_t optional, sem_handler_t *handler, + time_t time); diff --git a/src/knot/zone/serial.c b/src/knot/zone/serial.c new file mode 100644 index 0000000..0be5cbe --- /dev/null +++ b/src/knot/zone/serial.c @@ -0,0 +1,78 @@ +/* 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 <time.h> + +#include "knot/conf/conf.h" +#include "knot/zone/serial.h" + +static const serial_cmp_result_t diffbrief2result[4] = { + [0] = SERIAL_EQUAL, + [1] = SERIAL_GREATER, + [2] = SERIAL_INCOMPARABLE, + [3] = SERIAL_LOWER, +}; + +serial_cmp_result_t serial_compare(uint32_t s1, uint32_t s2) +{ + uint64_t diff = ((uint64_t)s1 + ((uint64_t)1 << 32) - s2) & 0xffffffff; + int diffbrief = (diff >> 31 << 1) | ((diff & 0x7fffffff) ? 1 : 0); + assert(diffbrief > -1 && diffbrief < 4); + return diffbrief2result[diffbrief]; +} + +static uint32_t serial_dateserial(uint32_t current) +{ + struct tm now; + time_t current_time = time(NULL); + struct tm *gmtime_result = gmtime_r(¤t_time, &now); + if (gmtime_result == NULL) { + return current; + } + return (1900 + now.tm_year) * 1000000 + + ( 1 + now.tm_mon ) * 10000 + + ( now.tm_mday) * 100; +} + +uint32_t serial_next(uint32_t current, int policy, uint32_t must_increment) +{ + uint32_t minimum; + switch (policy) { + case SERIAL_POLICY_INCREMENT: + minimum = current; + break; + case SERIAL_POLICY_UNIXTIME: + minimum = time(NULL); + break; + case SERIAL_POLICY_DATESERIAL: + minimum = serial_dateserial(current); + break; + default: + assert(0); + return 0; + } + if (serial_compare(minimum, current) != SERIAL_GREATER) { + return current + must_increment; + } else { + return minimum; + } +} + +serial_cmp_result_t kserial_cmp(kserial_t a, kserial_t b) +{ + return ((a.valid && b.valid) ? serial_compare(a.serial, b.serial) : SERIAL_INCOMPARABLE); +} diff --git a/src/knot/zone/serial.h b/src/knot/zone/serial.h new file mode 100644 index 0000000..effb1c6 --- /dev/null +++ b/src/knot/zone/serial.h @@ -0,0 +1,76 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> +#include <stdint.h> + +#define SERIAL_MAX_INCREMENT 2147483647 + +/*! + * \brief result of serial comparison. LOWER means that the first serial is lower that the second. + * + * Example: (serial_compare(a, b) & SERIAL_MASK_LEQ) means "a <= b". + */ +typedef enum { + SERIAL_INCOMPARABLE = 0x0, + SERIAL_LOWER = 0x1, + SERIAL_GREATER = 0x2, + SERIAL_EQUAL = 0x3, + SERIAL_MASK_LEQ = SERIAL_LOWER, + SERIAL_MASK_GEQ = SERIAL_GREATER, +} serial_cmp_result_t; + +/*! + * \brief Compares two zone serials. + */ +serial_cmp_result_t serial_compare(uint32_t s1, uint32_t s2); + +inline static bool serial_equal(uint32_t a, uint32_t b) +{ + return serial_compare(a, b) == SERIAL_EQUAL; +} + +/*! + * \brief Get (next) serial for given serial update policy. + * + * \param current Current SOA serial. + * \param policy SERIAL_POLICY_INCREMENT, SERIAL_POLICY_UNIXTIME or + * SERIAL_POLICY_DATESERIAL. + * \param must_increment The minimum difference to the current value. + * 0 only ensures policy; 1 also increments. + * + * \return New serial. + */ +uint32_t serial_next(uint32_t current, int policy, uint32_t must_increment); + +typedef struct { + uint32_t serial; + bool valid; +} kserial_t; + +/*! + * \brief Compares two kserials. + * + * If any of them is invalid, they are INCOMPARABLE. + */ +serial_cmp_result_t kserial_cmp(kserial_t a, kserial_t b); + +inline static bool kserial_equal(kserial_t a, kserial_t b) +{ + return kserial_cmp(a, b) == SERIAL_EQUAL; +} diff --git a/src/knot/zone/timers.c b/src/knot/zone/timers.c new file mode 100644 index 0000000..32c22b3 --- /dev/null +++ b/src/knot/zone/timers.c @@ -0,0 +1,228 @@ +/* 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/zone/timers.h" + +#include "contrib/wire_ctx.h" +#include "knot/zone/zonedb.h" + +/* + * # Timer database + * + * Timer database stores timestamps of events which need to be retained + * across server restarts. The key in the database is the zone name in + * wire format. The value contains serialized timers. + * + * # Serialization format + * + * The value is a sequence of timers. Each timer consists of the timer + * identifier (1 byte, unsigned integer) and timer value (8 bytes, unsigned + * integer, network order). + * + * For example, the following byte sequence: + * + * 81 00 00 00 00 57 e3 e8 0a 82 00 00 00 00 57 e3 e9 a1 + * + * Encodes the following timers: + * + * last_flush = 1474553866 + * last_refresh = 1474554273 + */ + +/*! + * \brief Timer database fields identifiers. + * + * Valid ID starts with '1' in MSB to avoid conflicts with "old timers". + */ +enum timer_id { + TIMER_INVALID = 0, + TIMER_SOA_EXPIRE = 0x80, // DEPRECATED + TIMER_LAST_FLUSH = 0x81, + TIMER_LAST_REFRESH = 0x82, // DEPRECATED + TIMER_NEXT_REFRESH = 0x83, + TIMER_NEXT_DS_CHECK = 0x85, + TIMER_NEXT_DS_PUSH = 0x86, + TIMER_CATALOG_MEMBER = 0x87, + TIMER_LAST_NOTIFIED = 0x88, + TIMER_LAST_REFR_OK = 0x89, + TIMER_NEXT_EXPIRE = 0x8a, +}; + +#define TIMER_SIZE (sizeof(uint8_t) + sizeof(uint64_t)) + +/*! + * \brief Deserialize timers from a binary buffer. + * + * \note Unknown timers are ignored. + */ +static int deserialize_timers(zone_timers_t *timers_ptr, + const uint8_t *data, size_t size) +{ + if (!timers_ptr || !data) { + return KNOT_EINVAL; + } + + zone_timers_t timers = { 0 }; + + wire_ctx_t wire = wire_ctx_init_const(data, size); + while (wire_ctx_available(&wire) >= TIMER_SIZE) { + uint8_t id = wire_ctx_read_u8(&wire); + uint64_t value = wire_ctx_read_u64(&wire); + switch (id) { + case TIMER_SOA_EXPIRE: timers.soa_expire = value; break; + case TIMER_LAST_FLUSH: timers.last_flush = value; break; + case TIMER_LAST_REFRESH: timers.last_refresh = value; break; + case TIMER_NEXT_REFRESH: timers.next_refresh = value; break; + case TIMER_LAST_REFR_OK: timers.last_refresh_ok = value; break; + case TIMER_LAST_NOTIFIED: timers.last_notified_serial = value; break; + case TIMER_NEXT_DS_CHECK: timers.next_ds_check = value; break; + case TIMER_NEXT_DS_PUSH: timers.next_ds_push = value; break; + case TIMER_CATALOG_MEMBER: timers.catalog_member = value; break; + case TIMER_NEXT_EXPIRE: timers.next_expire = value; break; + default: break; // ignore + } + } + + if (wire_ctx_available(&wire) != 0) { + return KNOT_EMALF; + } + + assert(wire.error == KNOT_EOK); + + *timers_ptr = timers; + return KNOT_EOK; +} + +static void txn_write_timers(knot_lmdb_txn_t *txn, const knot_dname_t *zone, + const zone_timers_t *timers) +{ + MDB_val k = { knot_dname_size(zone), (void *)zone }; + MDB_val v = knot_lmdb_make_key("BLBLBLBLBLBLBLBL", + TIMER_LAST_FLUSH, (uint64_t)timers->last_flush, + TIMER_NEXT_REFRESH, (uint64_t)timers->next_refresh, + TIMER_LAST_REFR_OK, (uint64_t)timers->last_refresh_ok, + TIMER_LAST_NOTIFIED, timers->last_notified_serial, + TIMER_NEXT_DS_CHECK, (uint64_t)timers->next_ds_check, + TIMER_NEXT_DS_PUSH, (uint64_t)timers->next_ds_push, + TIMER_CATALOG_MEMBER,(uint64_t)timers->catalog_member, + TIMER_NEXT_EXPIRE, (uint64_t)timers->next_expire); + knot_lmdb_insert(txn, &k, &v); + free(v.mv_data); +} + + +int zone_timers_open(const char *path, knot_db_t **db, size_t mapsize) +{ + if (path == NULL || db == NULL) { + return KNOT_EINVAL; + } + + struct knot_db_lmdb_opts opts = KNOT_DB_LMDB_OPTS_INITIALIZER; + opts.mapsize = mapsize; + opts.path = path; + + return knot_db_lmdb_api()->init(db, NULL, &opts); +} + +void zone_timers_close(knot_db_t *db) +{ + if (db == NULL) { + return; + } + + knot_db_lmdb_api()->deinit(db); +} + +int zone_timers_read(knot_lmdb_db_t *db, const knot_dname_t *zone, + zone_timers_t *timers) +{ + if (knot_lmdb_exists(db) == KNOT_ENODB) { + return KNOT_ENODB; + } + int ret = knot_lmdb_open(db); + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, false); + MDB_val k = { knot_dname_size(zone), (void *)zone }; + if (knot_lmdb_find(&txn, &k, KNOT_LMDB_EXACT | KNOT_LMDB_FORCE)) { + deserialize_timers(timers, txn.cur_val.mv_data, txn.cur_val.mv_size); + } + knot_lmdb_abort(&txn); + + // backward compatibility + // For catalog zones, next_expire is cleaned up later by zone_timers_sanitize(). + if (timers->next_expire == 0 && timers->last_refresh > 0) { + timers->next_expire = timers->last_refresh + timers->soa_expire; + } + + return txn.ret; +} + +int zone_timers_write(knot_lmdb_db_t *db, const knot_dname_t *zone, + const zone_timers_t *timers) +{ + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + txn_write_timers(&txn, zone, timers); + knot_lmdb_commit(&txn); + return txn.ret; +} + +static void txn_zone_write(zone_t *z, knot_lmdb_txn_t *txn) +{ + txn_write_timers(txn, z->name, &z->timers); +} + +int zone_timers_write_all(knot_lmdb_db_t *db, knot_zonedb_t *zonedb) +{ + int ret = knot_lmdb_open(db); + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + knot_zonedb_foreach(zonedb, txn_zone_write, &txn); + knot_lmdb_commit(&txn); + return txn.ret; +} + +int zone_timers_sweep(knot_lmdb_db_t *db, sweep_cb keep_zone, void *cb_data) +{ + if (knot_lmdb_exists(db) == KNOT_ENODB) { + return KNOT_EOK; + } + int ret = knot_lmdb_open(db); + if (ret != KNOT_EOK) { + return ret; + } + knot_lmdb_txn_t txn = { 0 }; + knot_lmdb_begin(db, &txn, true); + knot_lmdb_forwhole(&txn) { + if (!keep_zone((const knot_dname_t *)txn.cur_key.mv_data, cb_data)) { + knot_lmdb_del_cur(&txn); + } + } + knot_lmdb_commit(&txn); + return txn.ret; +} + +bool zone_timers_serial_notified(const zone_timers_t *timers, uint32_t serial) +{ + return (timers->last_notified_serial & LAST_NOTIFIED_SERIAL_VALID) && + ((uint32_t)timers->last_notified_serial == serial); +} diff --git a/src/knot/zone/timers.h b/src/knot/zone/timers.h new file mode 100644 index 0000000..d7bb05c --- /dev/null +++ b/src/knot/zone/timers.h @@ -0,0 +1,99 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> +#include <time.h> + +#include "libknot/dname.h" +#include "knot/journal/knot_lmdb.h" + +#define LAST_NOTIFIED_SERIAL_VALID (1LLU << 32) + +/*! + * \brief Persistent zone timers. + */ +struct zone_timers { + uint32_t soa_expire; //!< SOA expire value. DEPRECATED + time_t last_flush; //!< Last zone file synchronization. + time_t last_refresh; //!< Last successful zone refresh attempt. DEPRECATED + time_t next_refresh; //!< Next zone refresh attempt. + bool last_refresh_ok; //!< Last zone refresh attempt was successful. + uint64_t last_notified_serial; //!< SOA serial of last successful NOTIFY; (1<<32) if none. + time_t next_ds_check; //!< Next parent DS check. + time_t next_ds_push; //!< Next DDNS to parent zone with updated DS record. + time_t catalog_member; //!< This catalog member zone created. + time_t next_expire; //!< Timestamp of the zone to expire. +}; + +typedef struct zone_timers zone_timers_t; + +/*! + * \brief From zonedb.h + */ +typedef struct knot_zonedb knot_zonedb_t; + +/*! + * \brief Load timers for one zone. + * + * \param[in] db Timer database. + * \param[in] zone Zone name. + * \param[out] timers Loaded timers + * + * \return KNOT_E* + * \retval KNOT_ENOENT Zone not found in the database. + */ +int zone_timers_read(knot_lmdb_db_t *db, const knot_dname_t *zone, + zone_timers_t *timers); + +/*! + * \brief Write timers for one zone. + * + * \param db Timer database. + * \param zone Zone name. + * \param timers Loaded timers + * + * \return KNOT_E* + */ +int zone_timers_write(knot_lmdb_db_t *db, const knot_dname_t *zone, + const zone_timers_t *timers); + +/*! + * \brief Write timers for all zones. + * + * \param db Timer database. + * \param zonedb Zones database. + * + * \return KNOT_E* + */ +int zone_timers_write_all(knot_lmdb_db_t *db, knot_zonedb_t *zonedb); + +/*! + * \brief Selectively delete zones from the database. + * + * \param db Timer database. + * \param keep_zone Filtering callback. + * \param cb_data Data passed to callback function. + * + * \return KNOT_E* + */ +int zone_timers_sweep(knot_lmdb_db_t *db, sweep_cb keep_zone, void *cb_data); + +/*! + * \brief Tell if the specified serial has already been notified according to timers. + */ +bool zone_timers_serial_notified(const zone_timers_t *timers, uint32_t serial); diff --git a/src/knot/zone/zone-diff.c b/src/knot/zone/zone-diff.c new file mode 100644 index 0000000..9e6ecc6 --- /dev/null +++ b/src/knot/zone/zone-diff.c @@ -0,0 +1,402 @@ +/* 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 <stdlib.h> +#include <inttypes.h> + +#include "libknot/libknot.h" +#include "knot/zone/zone-diff.h" +#include "knot/zone/serial.h" + +struct zone_diff_param { + zone_tree_t *nodes; + changeset_t *changeset; + bool ignore_dnssec; + bool ignore_zonemd; +}; + +static bool rrset_is_dnssec(const knot_rrset_t *rrset) +{ + switch (rrset->type) { + case KNOT_RRTYPE_RRSIG: + case KNOT_RRTYPE_NSEC: + case KNOT_RRTYPE_NSEC3: + return true; + default: + return false; + } +} + +static int load_soas(const zone_contents_t *zone1, const zone_contents_t *zone2, + changeset_t *changeset) +{ + assert(zone1); + assert(zone2); + assert(changeset); + + const zone_node_t *apex1 = zone1->apex; + const zone_node_t *apex2 = zone2->apex; + if (apex1 == NULL || apex2 == NULL) { + return KNOT_EINVAL; + } + + knot_rrset_t soa_rrset1 = node_rrset(apex1, KNOT_RRTYPE_SOA); + knot_rrset_t soa_rrset2 = node_rrset(apex2, KNOT_RRTYPE_SOA); + if (knot_rrset_empty(&soa_rrset1) || knot_rrset_empty(&soa_rrset2)) { + return KNOT_EINVAL; + } + + if (soa_rrset1.rrs.count == 0 || + soa_rrset2.rrs.count == 0) { + return KNOT_EINVAL; + } + + uint32_t soa_serial1 = knot_soa_serial(soa_rrset1.rrs.rdata); + uint32_t soa_serial2 = knot_soa_serial(soa_rrset2.rrs.rdata); + + if (serial_compare(soa_serial1, soa_serial2) == SERIAL_EQUAL) { + return KNOT_ENODIFF; + } + + if (serial_compare(soa_serial1, soa_serial2) != SERIAL_LOWER) { + return KNOT_ERANGE; + } + + changeset->soa_from = knot_rrset_copy(&soa_rrset1, NULL); + if (changeset->soa_from == NULL) { + return KNOT_ENOMEM; + } + changeset->soa_to = knot_rrset_copy(&soa_rrset2, NULL); + if (changeset->soa_to == NULL) { + knot_rrset_free(changeset->soa_from, NULL); + return KNOT_ENOMEM; + } + + return KNOT_EOK; +} + +static int add_node(const zone_node_t *node, changeset_t *changeset, + bool ignore_dnssec, bool ignore_zonemd) +{ + /* Add all rrsets from node. */ + for (unsigned i = 0; i < node->rrset_count; i++) { + knot_rrset_t rrset = node_rrset_at(node, i); + + if ((ignore_dnssec && rrset_is_dnssec(&rrset)) || + (ignore_zonemd && rrset.type == KNOT_RRTYPE_ZONEMD)) { + continue; + } + + int ret = changeset_add_addition(changeset, &rrset, 0); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int remove_node(const zone_node_t *node, changeset_t *changeset, + bool ignore_dnssec, bool ignore_zonemd) +{ + /* Remove all the RRSets of the node. */ + for (unsigned i = 0; i < node->rrset_count; i++) { + knot_rrset_t rrset = node_rrset_at(node, i); + + if ((ignore_dnssec && rrset_is_dnssec(&rrset)) || + (ignore_zonemd && rrset.type == KNOT_RRTYPE_ZONEMD)) { + continue; + } + + int ret = changeset_add_removal(changeset, &rrset, 0); + if (ret != KNOT_EOK) { + return ret; + } + } + + return KNOT_EOK; +} + +static int rdata_return_changes(const knot_rrset_t *rrset1, + const knot_rrset_t *rrset2, + knot_rrset_t *changes) +{ + if (rrset1 == NULL || rrset2 == NULL) { + return KNOT_EINVAL; + } + + /* Create fake RRSet, it will be easier to handle. */ + knot_rrset_init(changes, rrset1->owner, rrset1->type, rrset1->rclass, rrset1->ttl); + + /* + * Take one rdata from first list and search through the second list + * looking for an exact match. If no match occurs, it means that this + * particular RR has changed. + * After the list has been traversed, we have a list of + * changed/removed rdatas. This has awful computation time. + */ + bool ttl_differ = rrset1->ttl != rrset2->ttl && rrset1->type != KNOT_RRTYPE_RRSIG; + knot_rdata_t *rr1 = rrset1->rrs.rdata; + for (uint16_t i = 0; i < rrset1->rrs.count; ++i) { + if (ttl_differ || !knot_rdataset_member(&rrset2->rrs, rr1)) { + /* + * No such RR is present in 'rrset2'. We'll copy + * index 'i' into 'changes' RRSet. + */ + int ret = knot_rdataset_add(&changes->rrs, rr1, NULL); + if (ret != KNOT_EOK) { + knot_rdataset_clear(&changes->rrs, NULL); + return ret; + } + } + rr1 = knot_rdataset_next(rr1); + } + + return KNOT_EOK; +} + +static int diff_rrsets(const knot_rrset_t *rrset1, const knot_rrset_t *rrset2, + changeset_t *changeset) +{ + if (changeset == NULL || (rrset1 == NULL && rrset2 == NULL)) { + return KNOT_EINVAL; + } + /* + * The easiest solution is to remove all the RRs that had no match and + * to add all RRs that had no match, but those from second RRSet. */ + + /* Get RRs to add to zone and to remove from zone. */ + knot_rrset_t to_remove = { 0 }; + knot_rrset_t to_add = { 0 }; + if (rrset1 != NULL && rrset2 != NULL) { + int ret = rdata_return_changes(rrset1, rrset2, &to_remove); + if (ret != KNOT_EOK) { + return ret; + } + + ret = rdata_return_changes(rrset2, rrset1, &to_add); + if (ret != KNOT_EOK) { + return ret; + } + } + + if (!knot_rrset_empty(&to_remove)) { + int ret = changeset_add_removal(changeset, &to_remove, 0); + knot_rdataset_clear(&to_remove.rrs, NULL); + if (ret != KNOT_EOK) { + knot_rdataset_clear(&to_add.rrs, NULL); + return ret; + } + } + + if (!knot_rrset_empty(&to_add)) { + int ret = changeset_add_addition(changeset, &to_add, 0); + knot_rdataset_clear(&to_add.rrs, NULL); + return ret; + } + + return KNOT_EOK; +} + +/*!< \todo this could be generic function for adding / removing. */ +static int knot_zone_diff_node(zone_node_t *node, void *data) +{ + if (node == NULL || data == NULL) { + return KNOT_EINVAL; + } + + struct zone_diff_param *param = (struct zone_diff_param *)data; + if (param->changeset == NULL) { + return KNOT_EINVAL; + } + + /* + * First, we have to search the second tree to see if there's according + * node, if not, the whole node has been removed. + */ + zone_node_t *node_in_second_tree = zone_tree_get(param->nodes, node->owner); + if (node_in_second_tree == NULL) { + return remove_node(node, param->changeset, param->ignore_dnssec, + param->ignore_zonemd); + } + + assert(node_in_second_tree != node); + + /* The nodes are in both trees, we have to diff each RRSet. */ + if (node->rrset_count == 0) { + /* + * If there are no RRs in the first tree, all of the RRs + * in the second tree will have to be inserted to ADD section. + */ + return add_node(node_in_second_tree, param->changeset, + param->ignore_dnssec, param->ignore_zonemd); + } + + for (unsigned i = 0; i < node->rrset_count; i++) { + /* Search for the RRSet in the node from the second tree. */ + knot_rrset_t rrset = node_rrset_at(node, i); + + /* SOAs are handled explicitly. */ + if (rrset.type == KNOT_RRTYPE_SOA) { + continue; + } + + if ((param->ignore_dnssec && rrset_is_dnssec(&rrset)) || + (param->ignore_zonemd && rrset.type == KNOT_RRTYPE_ZONEMD)) { + continue; + } + + knot_rrset_t rrset_from_second_node = + node_rrset(node_in_second_tree, rrset.type); + if (knot_rrset_empty(&rrset_from_second_node)) { + /* RRSet has been removed. Make a copy and remove. */ + int ret = changeset_add_removal( + param->changeset, &rrset, 0); + if (ret != KNOT_EOK) { + return ret; + } + } else { + /* Diff RRSets. */ + int ret = diff_rrsets(&rrset, &rrset_from_second_node, + param->changeset); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + for (unsigned i = 0; i < node_in_second_tree->rrset_count; i++) { + /* Search for the RRSet in the node from the second tree. */ + knot_rrset_t rrset = node_rrset_at(node_in_second_tree, i); + + /* SOAs are handled explicitly. */ + if (rrset.type == KNOT_RRTYPE_SOA) { + continue; + } + + if ((param->ignore_dnssec && rrset_is_dnssec(&rrset)) || + (param->ignore_zonemd && rrset.type == KNOT_RRTYPE_ZONEMD)) { + continue; + } + + knot_rrset_t rrset_from_first_node = node_rrset(node, rrset.type); + if (knot_rrset_empty(&rrset_from_first_node)) { + /* RRSet has been added. Make a copy and add. */ + int ret = changeset_add_addition( + param->changeset, &rrset, 0); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + return KNOT_EOK; +} + +/*!< \todo possibly not needed! */ +static int add_new_nodes(zone_node_t *node, void *data) +{ + if (node == NULL || data == NULL) { + return KNOT_EINVAL; + } + + struct zone_diff_param *param = (struct zone_diff_param *)data; + if (param->changeset == NULL) { + return KNOT_EINVAL; + } + + /* + * If a node is not present in the second zone, it is a new node + * and has to be added to changeset. Differences on the RRSet level are + * already handled. + */ + zone_node_t *new_node = zone_tree_get(param->nodes, node->owner); + if (new_node == NULL) { + assert(node); + return add_node(node, param->changeset, param->ignore_dnssec, + param->ignore_zonemd); + } + + return KNOT_EOK; +} + +static int load_trees(zone_tree_t *nodes1, zone_tree_t *nodes2, + changeset_t *changeset, bool ignore_dnssec, bool ignore_zonemd) +{ + assert(changeset); + + struct zone_diff_param param = { + .changeset = changeset, + .ignore_dnssec = ignore_dnssec, + .ignore_zonemd = ignore_zonemd, + }; + + // Traverse one tree, compare every node, each RRSet with its rdata. + param.nodes = nodes2; + int ret = zone_tree_apply(nodes1, knot_zone_diff_node, ¶m); + if (ret != KNOT_EOK) { + return ret; + } + + // Some nodes may have been added. Add missing nodes to changeset. + param.nodes = nodes1; + return zone_tree_apply(nodes2, add_new_nodes, ¶m); +} + +int zone_contents_diff(const zone_contents_t *zone1, const zone_contents_t *zone2, + changeset_t *changeset, bool ignore_dnssec, bool ignore_zonemd) +{ + if (changeset == NULL) { + return KNOT_EINVAL; + } + + if (zone1 == NULL || zone2 == NULL) { + return KNOT_EEMPTYZONE; + } + + int ret_soa = load_soas(zone1, zone2, changeset); + if (ret_soa != KNOT_EOK && ret_soa != KNOT_ENODIFF) { + return ret_soa; + } + + int ret = load_trees(zone1->nodes, zone2->nodes, changeset, + ignore_dnssec, ignore_zonemd); + if (ret != KNOT_EOK) { + return ret; + } + + ret = load_trees(zone1->nsec3_nodes, zone2->nsec3_nodes, changeset, + ignore_dnssec, ignore_zonemd); + if (ret != KNOT_EOK) { + return ret; + } + + if (ret_soa == KNOT_ENODIFF && !changeset_empty(changeset)) { + return KNOT_ESEMCHECK; + } + + return ret_soa; +} + +int zone_tree_add_diff(zone_tree_t *t1, zone_tree_t *t2, changeset_t *changeset) +{ + if (changeset == NULL) { + return KNOT_EINVAL; + } + + return load_trees(t1, t2, changeset, false, false); +} diff --git a/src/knot/zone/zone-diff.h b/src/knot/zone/zone-diff.h new file mode 100644 index 0000000..f31e214 --- /dev/null +++ b/src/knot/zone/zone-diff.h @@ -0,0 +1,31 @@ +/* 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/>. + */ + +#pragma once + +#include "knot/zone/contents.h" +#include "knot/updates/changesets.h" + +/*! + * \brief Create diff between two zone trees. + * */ +int zone_contents_diff(const zone_contents_t *zone1, const zone_contents_t *zone2, + changeset_t *changeset, bool ignore_dnssec, bool ignore_zonemd); + +/*! + * \brief Add diff between two zone trees into the changeset. + */ +int zone_tree_add_diff(zone_tree_t *t1, zone_tree_t *t2, changeset_t *changeset); diff --git a/src/knot/zone/zone-dump.c b/src/knot/zone/zone-dump.c new file mode 100644 index 0000000..41ec925 --- /dev/null +++ b/src/knot/zone/zone-dump.c @@ -0,0 +1,236 @@ +/* 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 <inttypes.h> + +#include "knot/dnssec/zone-nsec.h" +#include "knot/zone/zone-dump.h" +#include "libknot/libknot.h" + +/*! \brief Size of auxiliary buffer. */ +#define DUMP_BUF_LEN (70 * 1024) + +/*! \brief Dump parameters. */ +typedef struct { + FILE *file; + char *buf; + size_t buflen; + uint64_t rr_count; + bool dump_rrsig; + bool dump_nsec; + const knot_dname_t *origin; + const knot_dump_style_t *style; + const char *first_comment; +} dump_params_t; + +static int apex_node_dump_text(zone_node_t *node, dump_params_t *params) +{ + knot_rrset_t soa = node_rrset(node, KNOT_RRTYPE_SOA); + + // Dump SOA record as a first. + if (!params->dump_nsec) { + int ret = knot_rrset_txt_dump(&soa, ¶ms->buf, ¶ms->buflen, + params->style); + if (ret < 0) { + return ret; + } + params->rr_count += soa.rrs.count; + fprintf(params->file, "%s", params->buf); + params->buf[0] = '\0'; + } + + // Dump other records. + for (uint16_t i = 0; i < node->rrset_count; i++) { + knot_rrset_t rrset = node_rrset_at(node, i); + switch (rrset.type) { + case KNOT_RRTYPE_NSEC: + continue; + case KNOT_RRTYPE_RRSIG: + continue; + case KNOT_RRTYPE_SOA: + continue; + default: + break; + } + + int ret = knot_rrset_txt_dump(&rrset, ¶ms->buf, ¶ms->buflen, + params->style); + if (ret < 0) { + return ret; + } + params->rr_count += rrset.rrs.count; + fprintf(params->file, "%s", params->buf); + params->buf[0] = '\0'; + } + + return KNOT_EOK; +} + +static int node_dump_text(zone_node_t *node, void *data) +{ + dump_params_t *params = (dump_params_t *)data; + + // Zone apex rrsets. + if (node->owner == params->origin && !params->dump_rrsig && + !params->dump_nsec) { + apex_node_dump_text(node, params); + return KNOT_EOK; + } + + // Dump non-apex rrsets. + for (uint16_t i = 0; i < node->rrset_count; i++) { + knot_rrset_t rrset = node_rrset_at(node, i); + switch (rrset.type) { + case KNOT_RRTYPE_RRSIG: + if (params->dump_rrsig) { + break; + } + continue; + case KNOT_RRTYPE_NSEC: + if (params->dump_nsec) { + break; + } + continue; + case KNOT_RRTYPE_NSEC3: + if (params->dump_nsec) { + break; + } + continue; + default: + if (params->dump_nsec || params->dump_rrsig) { + continue; + } + break; + } + + // Dump block comment if available. + if (params->first_comment != NULL) { + fprintf(params->file, "%s", params->first_comment); + params->first_comment = NULL; + } + + int ret = knot_rrset_txt_dump(&rrset, ¶ms->buf, ¶ms->buflen, + params->style); + if (ret < 0) { + return ret; + } + params->rr_count += rrset.rrs.count; + fprintf(params->file, "%s", params->buf); + params->buf[0] = '\0'; + } + + return KNOT_EOK; +} + +int zone_dump_text(zone_contents_t *zone, FILE *file, bool comments, const char *color) +{ + if (file == NULL) { + return KNOT_EINVAL; + } + + if (zone == NULL) { + return KNOT_EEMPTYZONE; + } + + // Allocate auxiliary buffer for dumping operations. + char *buf = malloc(DUMP_BUF_LEN); + if (buf == NULL) { + return KNOT_ENOMEM; + } + + if (comments) { + fprintf(file, ";; Zone dump (Knot DNS %s)\n", PACKAGE_VERSION); + } + + // Set structure with parameters. + knot_dump_style_t style = KNOT_DUMP_STYLE_DEFAULT; + style.color = color; + style.now = knot_time(); + dump_params_t params = { + .file = file, + .buf = buf, + .buflen = DUMP_BUF_LEN, + .rr_count = 0, + .origin = zone->apex->owner, + .style = &style, + .dump_rrsig = false, + .dump_nsec = false + }; + + // Dump standard zone records without RRSIGS. + int ret = zone_contents_apply(zone, node_dump_text, ¶ms); + if (ret != KNOT_EOK) { + free(params.buf); + return ret; + } + + // Dump RRSIG records if available. + params.dump_rrsig = true; + params.dump_nsec = false; + params.first_comment = comments ? ";; DNSSEC signatures\n" : NULL; + ret = zone_contents_apply(zone, node_dump_text, ¶ms); + if (ret != KNOT_EOK) { + free(params.buf); + return ret; + } + + // Dump NSEC chain if available. + params.dump_rrsig = false; + params.dump_nsec = true; + params.first_comment = comments ? ";; DNSSEC NSEC chain\n" : NULL; + ret = zone_contents_apply(zone, node_dump_text, ¶ms); + if (ret != KNOT_EOK) { + free(params.buf); + return ret; + } + + // Dump NSEC3 chain if available. + params.dump_rrsig = false; + params.dump_nsec = true; + params.first_comment = comments ? ";; DNSSEC NSEC3 chain\n" : NULL; + ret = zone_contents_nsec3_apply(zone, node_dump_text, ¶ms); + if (ret != KNOT_EOK) { + free(params.buf); + return ret; + } + + params.dump_rrsig = true; + params.dump_nsec = false; + params.first_comment = comments ? ";; DNSSEC NSEC3 signatures\n" : NULL; + ret = zone_contents_nsec3_apply(zone, node_dump_text, ¶ms); + if (ret != KNOT_EOK) { + free(params.buf); + return ret; + } + + if (comments) { + // Create formatted date-time string. + time_t now = time(NULL); + struct tm tm; + localtime_r(&now, &tm); + char date[64]; + strftime(date, sizeof(date), "%Y-%m-%d %H:%M:%S %Z", &tm); + + // Dump trailing statistics. + fprintf(file, ";; Written %"PRIu64" records\n" + ";; Time %s\n", + params.rr_count, date); + } + + free(params.buf); // params.buf may be != buf because of knot_rrset_txt_dump_dynamic() + + return KNOT_EOK; +} diff --git a/src/knot/zone/zone-dump.h b/src/knot/zone/zone-dump.h new file mode 100644 index 0000000..a0290ef --- /dev/null +++ b/src/knot/zone/zone-dump.h @@ -0,0 +1,32 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/zone/zone.h" + +/*! + * \brief Dumps given zone to text file. + * + * \param zone Zone to be saved. + * \param file File to write to. + * \param comments Add separating comments indicator. + * \param color Optional color control sequence. + * + * \retval KNOT_EOK on success. + * \retval < 0 if error. + */ +int zone_dump_text(zone_contents_t *zone, FILE *file, bool comments, const char *color); diff --git a/src/knot/zone/zone-load.c b/src/knot/zone/zone-load.c new file mode 100644 index 0000000..11cba83 --- /dev/null +++ b/src/knot/zone/zone-load.c @@ -0,0 +1,173 @@ +/* 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 "knot/common/log.h" +#include "knot/journal/journal_metadata.h" +#include "knot/journal/journal_read.h" +#include "knot/zone/zone-diff.h" +#include "knot/zone/zone-load.h" +#include "knot/zone/zonefile.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/zone-events.h" +#include "libknot/libknot.h" + +int zone_load_contents(conf_t *conf, const knot_dname_t *zone_name, + zone_contents_t **contents, semcheck_optional_t semcheck_mode, + bool fail_on_warning) +{ + if (conf == NULL || zone_name == NULL || contents == NULL) { + return KNOT_EINVAL; + } + + char *zonefile = conf_zonefile(conf, zone_name); + + zloader_t zl; + int ret = zonefile_open(&zl, zonefile, zone_name, semcheck_mode, time(NULL)); + free(zonefile); + if (ret != KNOT_EOK) { + return ret; + } + + sem_handler_t handler = { + .cb = err_handler_logger + }; + + zl.err_handler = &handler; + zl.creator->master = !zone_load_can_bootstrap(conf, zone_name); + + *contents = zonefile_load(&zl); + zonefile_close(&zl); + if (*contents == NULL) { + return KNOT_ERROR; + } + if (handler.warning && fail_on_warning) { + zone_contents_deep_free(*contents); + *contents = NULL; + return KNOT_ESEMCHECK; + } + + return KNOT_EOK; +} + +static int apply_one_cb(bool remove, const knot_rrset_t *rr, void *ctx) +{ + zone_node_t *unused = NULL; + zone_contents_t *contents = ctx; + int ret = remove ? zone_contents_remove_rr(contents, rr, &unused) + : zone_contents_add_rr(contents, rr, &unused); + if (ret == KNOT_ENOENT && remove && knot_rrtype_is_dnssec(rr->type)) { + // Compatibility with imperfect journal contents (versions < 2.9) if + // 'zonefile-load: difference' and 'dnssec-signing: on`. + // Journal history can contain a changeset with removed DNSSEC records + // which are not present in the zonefile. + return KNOT_EOK; + } else { + return ret; + } +} + +int zone_load_journal(conf_t *conf, zone_t *zone, zone_contents_t *contents) +{ + if (conf == NULL || zone == NULL) { + return KNOT_EINVAL; + } + + // Check if journal is used (later in zone_changes_load() and zone is not empty. + if (zone_contents_is_empty(contents)) { + return KNOT_EOK; + } + uint32_t serial = zone_contents_serial(contents); + + journal_read_t *read = NULL; + int ret = journal_read_begin(zone_journal(zone), false, serial, &read); + switch (ret) { + case KNOT_EOK: + break; + case KNOT_ENOENT: + return KNOT_EOK; + default: + return ret; + } + + ret = journal_read_rrsets(read, apply_one_cb, contents); + if (ret == KNOT_EOK) { + log_zone_info(zone->name, "changes from journal applied, serial %u -> %u", + serial, zone_contents_serial(contents)); + } else { + log_zone_error(zone->name, "failed to apply journal changes, serial %u -> %u (%s)", + serial, zone_contents_serial(contents), + knot_strerror(ret)); + } + + return ret; +} + +int zone_load_from_journal(conf_t *conf, zone_t *zone, zone_contents_t **contents) +{ + if (conf == NULL || zone == NULL || contents == NULL) { + return KNOT_EINVAL; + } + + *contents = zone_contents_new(zone->name, true); + if (*contents == NULL) { + return KNOT_ENOMEM; + } + + journal_read_t *read = NULL; + int ret = journal_read_begin(zone_journal(zone), true, 0, &read); + if (ret == KNOT_ENOENT) { + zone_contents_deep_free(*contents); + *contents = NULL; + return ret; + } + + knot_rrset_t rr = { 0 }; + while (ret == KNOT_EOK && journal_read_rrset(read, &rr, false)) { + zone_node_t *unused = NULL; + ret = zone_contents_add_rr(*contents, &rr, &unused); + journal_read_clear_rrset(&rr); + } + + if (ret == KNOT_EOK) { + ret = journal_read_rrsets(read, apply_one_cb, *contents); + } else { + journal_read_end(read); + } + + if (ret == KNOT_EOK) { + log_zone_info(zone->name, "zone loaded from journal, serial %u", + zone_contents_serial(*contents)); + } else { + log_zone_error(zone->name, "failed to load zone from journal, serial %u (%s)", + zone_contents_serial(*contents), knot_strerror(ret)); + zone_contents_deep_free(*contents); + *contents = NULL; + } + + return ret; +} + +bool zone_load_can_bootstrap(conf_t *conf, const knot_dname_t *zone_name) +{ + if (conf == NULL || zone_name == NULL) { + return false; + } + + conf_val_t val = conf_zone_get(conf, C_MASTER, zone_name); + size_t count = conf_val_count(&val); + + return count > 0; +} diff --git a/src/knot/zone/zone-load.h b/src/knot/zone/zone-load.h new file mode 100644 index 0000000..c438903 --- /dev/null +++ b/src/knot/zone/zone-load.h @@ -0,0 +1,68 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/conf/conf.h" +#include "knot/zone/semantic-check.h" +#include "knot/zone/zone.h" + +/*! + * \brief Load zone contents according to the configuration. + * + * \param conf + * \param zone_name + * \param contents + * \param semcheck_mode + * \param fail_on_warning + * + * \retval KNOT_EOK if success. + * \retval KNOT_ESEMCHECK if any semantic check warning. + * \retval KNOT_E* if error. + */ +int zone_load_contents(conf_t *conf, const knot_dname_t *zone_name, + zone_contents_t **contents, semcheck_optional_t semcheck_mode, + bool fail_on_warning); + +/*! + * \brief Update zone contents from the journal. + * + * \warning If error, the zone is in inconsistent state and should be freed. + * + * \param conf + * \param zone + * \param contents + * \return KNOT_EOK or an error + */ +int zone_load_journal(conf_t *conf, zone_t *zone, zone_contents_t *contents); + +/*! + * \brief Load zone contents from journal (headless). + * + * \param conf + * \param zone + * \param contents + * \return KNOT_EOK or an error + */ +int zone_load_from_journal(conf_t *conf, zone_t *zone, zone_contents_t **contents); + +/*! + * \brief Check if zone can be bootstrapped. + * + * \param conf + * \param zone_name + */ +bool zone_load_can_bootstrap(conf_t *conf, const knot_dname_t *zone_name); diff --git a/src/knot/zone/zone-tree.c b/src/knot/zone/zone-tree.c new file mode 100644 index 0000000..87dde18 --- /dev/null +++ b/src/knot/zone/zone-tree.c @@ -0,0 +1,512 @@ +/* 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 <stdlib.h> + +#include "knot/zone/zone-tree.h" +#include "libknot/consts.h" +#include "libknot/errcode.h" +#include "libknot/packet/wire.h" + +typedef struct { + zone_tree_apply_cb_t func; + void *data; + int binode_second; +} zone_tree_func_t; + +static int tree_apply_cb(trie_val_t *node, void *data) +{ + zone_tree_func_t *f = (zone_tree_func_t *)data; + zone_node_t *n = (zone_node_t *)(*node) + f->binode_second; + assert(!f->binode_second || (n->flags & NODE_FLAGS_SECOND)); + return f->func(n, f->data); +} + +zone_tree_t *zone_tree_create(bool use_binodes) +{ + zone_tree_t *t = calloc(1, sizeof(*t)); + if (t != NULL) { + if (use_binodes) { + t->flags = ZONE_TREE_USE_BINODES; + } + t->trie = trie_create(NULL); + if (t->trie == NULL) { + free(t); + t = NULL; + } + } + return t; +} + +zone_tree_t *zone_tree_cow(zone_tree_t *from) +{ + zone_tree_t *to = calloc(1, sizeof(*to)); + if (to == NULL) { + return to; + } + to->flags = from->flags ^ ZONE_TREE_BINO_SECOND; + from->cow = trie_cow(from->trie, NULL, NULL); + to->cow = from->cow; + to->trie = trie_cow_new(to->cow); + if (to->trie == NULL) { + free(to); + to = NULL; + } + return to; +} + +static trie_val_t nocopy(const trie_val_t val, _unused_ knot_mm_t *mm) +{ + return val; +} + +zone_tree_t *zone_tree_shallow_copy(zone_tree_t *from) +{ + zone_tree_t *to = calloc(1, sizeof(*to)); + if (to == NULL) { + return to; + } + to->flags = from->flags; + to->trie = trie_dup(from->trie, nocopy, NULL); + if (to->trie == NULL) { + free(to); + to = NULL; + } + return to; +} + +int zone_tree_insert(zone_tree_t *tree, zone_node_t **node) +{ + if (tree == NULL || node == NULL || *node == NULL) { + return KNOT_EINVAL; + } + + assert((*node)->owner); + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf((*node)->owner, lf_storage); + assert(lf); + + if (tree->cow != NULL) { + *trie_get_cow(tree->cow, lf + 1, *lf) = binode_first(*node); + } else { + *trie_get_ins(tree->trie, lf + 1, *lf) = binode_first(*node); + } + + *node = zone_tree_fix_get(*node, tree); + + return KNOT_EOK; +} + +int zone_tree_insert_with_parents(zone_tree_t *tree, zone_node_t *node, bool without_parents) +{ + int ret = KNOT_EOK; + do { + ret = zone_tree_insert(tree, &node); + node = node->parent; + } while (node != NULL && ret == KNOT_EOK && !without_parents); + return ret; +} + +zone_node_t *zone_tree_get(zone_tree_t *tree, const knot_dname_t *owner) +{ + if (owner == NULL) { + return NULL; + } + + if (zone_tree_is_empty(tree)) { + return NULL; + } + + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(owner, lf_storage); + assert(lf); + + trie_val_t *val = trie_get_try(tree->trie, lf + 1, *lf); + if (val == NULL) { + return NULL; + } + + return zone_tree_fix_get(*val, tree); +} + +int zone_tree_get_less_or_equal(zone_tree_t *tree, + const knot_dname_t *owner, + zone_node_t **found, + zone_node_t **previous) +{ + if (owner == NULL || found == NULL || previous == NULL) { + return KNOT_EINVAL; + } + + if (zone_tree_is_empty(tree)) { + return KNOT_ENONODE; + } + + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(owner, lf_storage); + assert(lf); + + trie_val_t *fval = NULL; + int ret = trie_get_leq(tree->trie, lf + 1, *lf, &fval); + if (fval != NULL) { + *found = zone_tree_fix_get(*fval, tree); + } + + int exact_match = 0; + if (ret == KNOT_EOK) { + if (fval != NULL) { + *previous = node_prev(*found); + } + exact_match = 1; + } else if (ret == 1) { + *previous = *found; + *found = NULL; + } else { + /* Previous should be the rightmost node. + * For regular zone it is the node left of apex, but for some + * cases like NSEC3, there is no such sort of thing (name wise). + */ + /*! \todo We could store rightmost node in zonetree probably. */ + zone_tree_it_t it = { 0 }; + ret = zone_tree_it_begin(tree, &it); + if (ret != KNOT_EOK) { + return ret; + } + *previous = zone_tree_it_val(&it); /* leftmost */ + assert(*previous != NULL); // cppcheck + *previous = zone_tree_fix_get(*previous, tree); + *previous = node_prev(*previous); /* rightmost */ + *found = NULL; + zone_tree_it_free(&it); + } + + return exact_match; +} + +/*! \brief Removes node with the given owner from the zone tree. */ +void zone_tree_remove_node(zone_tree_t *tree, const knot_dname_t *owner) +{ + if (zone_tree_is_empty(tree) || owner == NULL) { + return; + } + + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(owner, lf_storage); + assert(lf); + + trie_val_t *rval = trie_get_try(tree->trie, lf + 1, *lf); + if (rval != NULL) { + if (tree->cow != NULL) { + trie_del_cow(tree->cow, lf + 1, *lf, NULL); + } else { + trie_del(tree->trie, lf + 1, *lf, NULL); + } + } +} + +int zone_tree_add_node(zone_tree_t *tree, zone_node_t *apex, const knot_dname_t *dname, + zone_tree_new_node_cb_t new_cb, void *new_cb_ctx, zone_node_t **new_node) +{ + int in_bailiwick = knot_dname_in_bailiwick(dname, apex->owner); + if (in_bailiwick == 0) { + *new_node = apex; + return KNOT_EOK; + } else if (in_bailiwick < 0) { + return KNOT_EOUTOFZONE; + } + + *new_node = zone_tree_get(tree, dname); + if (*new_node == NULL) { + *new_node = new_cb(dname, new_cb_ctx); + if (*new_node == NULL) { + return KNOT_ENOMEM; + } + int ret = zone_tree_insert(tree, new_node); + assert(!((*new_node)->flags & NODE_FLAGS_DELETED)); + if (ret != KNOT_EOK) { + return ret; + } + zone_node_t *parent = NULL; + ret = zone_tree_add_node(tree, apex, knot_wire_next_label(dname, NULL), new_cb, new_cb_ctx, &parent); + if (ret != KNOT_EOK) { + return ret; + } + (*new_node)->parent = parent; + if (parent != NULL) { + parent->children++; + if (knot_dname_is_wildcard(dname)) { + parent->flags |= NODE_FLAGS_WILDCARD_CHILD; + } + } + } + return KNOT_EOK; +} + +int zone_tree_del_node(zone_tree_t *tree, zone_node_t *node, bool free_deleted) +{ + zone_node_t *parent = node_parent(node); + bool wildcard = knot_dname_is_wildcard(node->owner); + + node->parent = NULL; + node->flags |= NODE_FLAGS_DELETED; + zone_tree_remove_node(tree, node->owner); + + if (free_deleted) { + node_free(node, NULL); + } + + int ret = KNOT_EOK; + if (ret == KNOT_EOK && parent != NULL) { + parent->children--; + if (wildcard) { + parent->flags &= ~NODE_FLAGS_WILDCARD_CHILD; + } + if (parent->children == 0 && parent->rrset_count == 0 && + !(parent->flags & NODE_FLAGS_APEX)) { + ret = zone_tree_del_node(tree, parent, free_deleted); + } + } + return ret; +} + +int zone_tree_apply(zone_tree_t *tree, zone_tree_apply_cb_t function, void *data) +{ + if (function == NULL) { + return KNOT_EINVAL; + } + + if (zone_tree_is_empty(tree)) { + return KNOT_EOK; + } + + zone_tree_func_t f = { + .func = function, + .data = data, + .binode_second = ((tree->flags & ZONE_TREE_BINO_SECOND) ? 1 : 0), + }; + + return trie_apply(tree->trie, tree_apply_cb, &f); +} + +int zone_tree_sub_apply(zone_tree_t *tree, const knot_dname_t *sub_root, + bool excl_root, zone_tree_apply_cb_t function, void *data) +{ + zone_tree_it_t it = { 0 }; + int ret = zone_tree_it_sub_begin(tree, sub_root, &it); + if (excl_root && ret == KNOT_EOK && !zone_tree_it_finished(&it)) { + zone_tree_it_next(&it); + } + while (ret == KNOT_EOK && !zone_tree_it_finished(&it)) { + ret = function(zone_tree_it_val(&it), data); + zone_tree_it_next(&it); + } + zone_tree_it_free(&it); + return ret; +} + +int zone_tree_it_begin(zone_tree_t *tree, zone_tree_it_t *it) +{ + return zone_tree_it_double_begin(tree, NULL, it); +} + +int zone_tree_it_sub_begin(zone_tree_t *tree, const knot_dname_t *sub_root, + zone_tree_it_t *it) +{ + if (tree == NULL || sub_root == NULL) { + return KNOT_EINVAL; + } + int ret = zone_tree_it_begin(tree, it); + if (ret != KNOT_EOK) { + return ret; + } + it->sub_root = knot_dname_copy(sub_root, NULL); + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(sub_root, lf_storage); + ret = trie_it_get_leq(it->it, lf + 1, *lf); + if ((ret != KNOT_EOK && ret != KNOT_ENOENT) || it->sub_root == NULL) { + zone_tree_it_free(it); + return ret == KNOT_EOK ? KNOT_ENOMEM : ret; + } + return KNOT_EOK; +} + +int zone_tree_it_double_begin(zone_tree_t *first, zone_tree_t *second, zone_tree_it_t *it) +{ + if (it->tree == NULL) { + it->it = trie_it_begin(first->trie); + if (it->it == NULL) { + return KNOT_ENOMEM; + } + if (trie_it_finished(it->it) && second != NULL) { // first tree is empty + trie_it_free(it->it); + it->it = trie_it_begin(second->trie); + it->tree = second; + it->next_tree = NULL; + } else { + it->tree = first; + it->next_tree = second; + } + it->binode_second = ((it->tree->flags & ZONE_TREE_BINO_SECOND) ? 1 : 0); + } + return KNOT_EOK; +} + +static bool sub_done(zone_tree_it_t *it) +{ + return it->sub_root != NULL && + knot_dname_in_bailiwick(zone_tree_it_val(it)->owner, it->sub_root) < 0; +} + +bool zone_tree_it_finished(zone_tree_it_t *it) +{ + return it->it == NULL || it->tree == NULL || trie_it_finished(it->it) || sub_done(it); +} + +zone_node_t *zone_tree_it_val(zone_tree_it_t *it) +{ + zone_node_t *node = (zone_node_t *)(*trie_it_val(it->it)) + it->binode_second; + assert(!it->binode_second || (node->flags & NODE_FLAGS_SECOND)); + return node; +} + +void zone_tree_it_del(zone_tree_it_t *it) +{ + trie_it_del(it->it); +} + +void zone_tree_it_next(zone_tree_it_t *it) +{ + trie_it_next(it->it); + if (it->next_tree != NULL && trie_it_finished(it->it)) { + trie_it_free(it->it); + it->tree = it->next_tree; + it->binode_second = ((it->tree->flags & ZONE_TREE_BINO_SECOND) ? 1 : 0); + it->next_tree = NULL; + it->it = trie_it_begin(it->tree->trie); + assert(it->sub_root == NULL); + } +} + +void zone_tree_it_free(zone_tree_it_t *it) +{ + trie_it_free(it->it); + knot_dname_free(it->sub_root, NULL); + memset(it, 0, sizeof(*it)); +} + +int zone_tree_delsafe_it_begin(zone_tree_t *tree, zone_tree_delsafe_it_t *it, bool include_deleted) +{ + it->incl_del = include_deleted; + it->total = zone_tree_count(tree); + if (it->total == 0) { + it->current = 0; + it->nodes = NULL; + return KNOT_EOK; + } + it->nodes = malloc(it->total * sizeof(*it->nodes)); + if (it->nodes == NULL) { + return KNOT_ENOMEM; + } + it->current = 0; + + zone_tree_it_t tmp = { 0 }; + int ret = zone_tree_it_begin(tree, &tmp); + if (ret != KNOT_EOK) { + return ret; + } + while (!zone_tree_it_finished(&tmp)) { + it->nodes[it->current++] = zone_tree_it_val(&tmp); + zone_tree_it_next(&tmp); + } + zone_tree_it_free(&tmp); + assert(it->total == it->current); + + zone_tree_delsafe_it_restart(it); + + return KNOT_EOK; +} + +bool zone_tree_delsafe_it_finished(zone_tree_delsafe_it_t *it) +{ + return (it->current >= it->total); +} + +void zone_tree_delsafe_it_restart(zone_tree_delsafe_it_t *it) +{ + it->current = 0; + + while (!it->incl_del && !zone_tree_delsafe_it_finished(it) && + (zone_tree_delsafe_it_val(it)->flags & NODE_FLAGS_DELETED)) { + it->current++; + } +} + +zone_node_t *zone_tree_delsafe_it_val(zone_tree_delsafe_it_t *it) +{ + return it->nodes[it->current]; +} + +void zone_tree_delsafe_it_next(zone_tree_delsafe_it_t *it) +{ + do { + it->current++; + } while (!it->incl_del && !zone_tree_delsafe_it_finished(it) && + (zone_tree_delsafe_it_val(it)->flags & NODE_FLAGS_DELETED)); +} + +void zone_tree_delsafe_it_free(zone_tree_delsafe_it_t *it) +{ + free(it->nodes); + memset(it, 0, sizeof(*it)); +} + +static int merge_cb(zone_node_t *node, void *ctx) +{ + return zone_tree_insert(ctx, &node); +} + +int zone_tree_merge(zone_tree_t *into, zone_tree_t *what) +{ + return zone_tree_apply(what, merge_cb, into); +} + +static int binode_unify_cb(zone_node_t *node, void *ctx) +{ + binode_unify(node, *(bool *)ctx, NULL); + return KNOT_EOK; +} + +void zone_trees_unify_binodes(zone_tree_t *nodes, zone_tree_t *nsec3_nodes, bool free_deleted) +{ + if (nodes != NULL) { + zone_tree_apply(nodes, binode_unify_cb, &free_deleted); + } + if (nsec3_nodes != NULL) { + zone_tree_apply(nsec3_nodes, binode_unify_cb, &free_deleted); + } +} + +void zone_tree_free(zone_tree_t **tree) +{ + if (tree == NULL || *tree == NULL) { + return; + } + + trie_free((*tree)->trie); + free(*tree); + *tree = NULL; +} diff --git a/src/knot/zone/zone-tree.h b/src/knot/zone/zone-tree.h new file mode 100644 index 0000000..384e87e --- /dev/null +++ b/src/knot/zone/zone-tree.h @@ -0,0 +1,337 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/qp-trie/trie.h" +#include "contrib/ucw/lists.h" +#include "knot/zone/node.h" + +enum { + /*! Indication of a zone tree with bi-nodes (two zone_node_t structures allocated for one node). */ + ZONE_TREE_USE_BINODES = (1 << 0), + /*! If set, from each bi-node in the zone tree, the second zone_node_t is valid. */ + ZONE_TREE_BINO_SECOND = (1 << 1), +}; + +typedef struct { + trie_t *trie; + trie_cow_t *cow; // non-NULL only during zone update + uint16_t flags; +} zone_tree_t; + +/*! + * \brief Signature of callback for zone apply functions. + */ +typedef int (*zone_tree_apply_cb_t)(zone_node_t *node, void *data); + +typedef zone_node_t *(*zone_tree_new_node_cb_t)(const knot_dname_t *dname, void *ctx); + +/*! + * \brief Zone tree iteration context. + */ +typedef struct { + zone_tree_t *tree; + trie_it_t *it; + int binode_second; + + zone_tree_t *next_tree; + knot_dname_t *sub_root; +} zone_tree_it_t; + +typedef struct { + zone_node_t **nodes; + size_t total; + size_t current; + bool incl_del; +} zone_tree_delsafe_it_t; + +/*! + * \brief Creates the zone tree. + * + * \return created zone tree structure. + */ +zone_tree_t *zone_tree_create(bool use_binodes); + +zone_tree_t *zone_tree_cow(zone_tree_t *from); + +/*! + * \brief Create a clone of existing zone_tree. + * + * \note Copies only the trie, not individual nodes. + * + * \warning Don't use COW in the duplicate. + */ +zone_tree_t *zone_tree_shallow_copy(zone_tree_t *from); + +/*! + * \brief Return number of nodes in the zone tree. + * + * \param tree Zone tree. + * + * \return number of nodes in tree. + */ +inline static size_t zone_tree_count(const zone_tree_t *tree) +{ + if (tree == NULL || tree->trie == NULL) { + return 0; + } + + return trie_weight(tree->trie); +} + +/*! + * \brief Checks if the zone tree is empty. + * + * \param tree Zone tree to check. + * + * \return Nonzero if the zone tree is empty. + */ +inline static bool zone_tree_is_empty(const zone_tree_t *tree) +{ + return zone_tree_count(tree) == 0; +} + +inline static zone_node_t *zone_tree_fix_get(zone_node_t *node, const zone_tree_t *tree) +{ + assert(((node->flags & NODE_FLAGS_BINODE) ? 1 : 0) == ((tree->flags & ZONE_TREE_USE_BINODES) ? 1 : 0)); + assert((tree->flags & ZONE_TREE_USE_BINODES) || !(tree->flags & ZONE_TREE_BINO_SECOND)); + return binode_node(node, (tree->flags & ZONE_TREE_BINO_SECOND)); +} + +inline static zone_node_t *node_new_for_tree(const knot_dname_t *owner, const zone_tree_t *tree, knot_mm_t *mm) +{ + assert((tree->flags & ZONE_TREE_USE_BINODES) || !(tree->flags & ZONE_TREE_BINO_SECOND)); + return node_new(owner, (tree->flags & ZONE_TREE_USE_BINODES), (tree->flags & ZONE_TREE_BINO_SECOND), mm); +} + +/*! + * \brief Inserts the given node into the zone tree. + * + * \param tree Zone tree to insert the node into. + * \param node Node to insert. If it's binode, the pointer will be adjusted to correct node. + * + * \retval KNOT_EOK + * \retval KNOT_EINVAL + * \retval KNOT_ENOMEM + */ +int zone_tree_insert(zone_tree_t *tree, zone_node_t **node); + +/*! + * \brief Insert a node together with its parents (iteratively node->parent). + * + * \param tree Zone tree to insert into. + * \param node Node to be inserted with parents. + * \param without_parents Actually, insert it without parents. + * + * \return KNOT_E* + */ +int zone_tree_insert_with_parents(zone_tree_t *tree, zone_node_t *node, bool without_parents); + +/*! + * \brief Finds node with the given owner in the zone tree. + * + * \param tree Zone tree to search in. + * \param owner Owner of the node to find. + * + * \retval Found node or NULL. + */ +zone_node_t *zone_tree_get(zone_tree_t *tree, const knot_dname_t *owner); + +/*! + * \brief Tries to find the given domain name in the zone tree and returns the + * associated node and previous node in canonical order. + * + * \param tree Zone to search in. + * \param owner Owner of the node to find. + * \param found Found node. + * \param previous Previous node in canonical order (i.e. the one directly + * preceding \a owner in canonical order, regardless if the name + * is in the zone or not). + * + * \retval > 0 if the domain name was found. In such case \a found holds the + * zone node with \a owner as its owner. + * \a previous is set properly. + * \retval 0 if the domain name was not found. \a found may hold any (or none) + * node. \a previous is set properly. + * \retval KNOT_EINVAL + * \retval KNOT_ENOMEM + */ +int zone_tree_get_less_or_equal(zone_tree_t *tree, + const knot_dname_t *owner, + zone_node_t **found, + zone_node_t **previous); + +/*! + * \brief Remove a node from a tree with no checks. + * + * \param tree The tree to remove from. + * \param owner The node to remove. + */ +void zone_tree_remove_node(zone_tree_t *tree, const knot_dname_t *owner); + +/*! + * \brief Create a node in zone tree if not already exists, and also all parent nodes. + * + * \param tree Zone tree to insert into. + * \param apex Zone contents apex node. + * \param dname Name of the node to be added. + * \param new_cb Callback for allocating new node. + * \param new_cb_ctx Context to be passed to allocating callback. + * \param new_node Output: pointer on added (or existing) node with specified dname. + * + * \return KNOT_E* + */ +int zone_tree_add_node(zone_tree_t *tree, zone_node_t *apex, const knot_dname_t *dname, + zone_tree_new_node_cb_t new_cb, void *new_cb_ctx, zone_node_t **new_node); + +/*! + * \brief Remove a node in zone tree, removing also empty parents. + * + * \param tree Zone tree to remove from. + * \param node Node to be removed. + * \param free_deleted Indication to free node. + * + * \return KNOT_E* + */ +int zone_tree_del_node(zone_tree_t *tree, zone_node_t *node, bool free_deleted); + +/*! + * \brief Applies the given function to each node in the zone in order. + * + * \param tree Zone tree to apply the function to. + * \param function Function to be applied to each node of the zone. + * \param data Arbitrary data to be passed to the function. + * + * \retval KNOT_EOK + * \retval KNOT_EINVAL + */ +int zone_tree_apply(zone_tree_t *tree, zone_tree_apply_cb_t function, void *data); + +/*! + * \brief Applies given function to each node in a subtree. + * + * \param tree Zone tree. + * \param sub_root Name denoting the subtree. + * \param excl_root Exclude the subtree root. + * \param function Callback to be applied. + * \param data Callback context. + * + * \return KNOT_E* + */ +int zone_tree_sub_apply(zone_tree_t *tree, const knot_dname_t *sub_root, + bool excl_root, zone_tree_apply_cb_t function, void *data); + +/*! + * \brief Start zone tree iteration. + * + * \param tree Zone tree to iterate over. + * \param it Out: iteration context. It shall be zeroed before. + * + * \return KNOT_OK, KNOT_ENOMEM + */ +int zone_tree_it_begin(zone_tree_t *tree, zone_tree_it_t *it); + +/*! + * \brief Start iteration over a subtree. + * + * \param tree Zone tree to iterate in. + * \param sub_root Iterate over node of this name and all children. + * \param it Out: iteration context, shall be zeroed before. + * + * \return KNOT_E* + */ +int zone_tree_it_sub_begin(zone_tree_t *tree, const knot_dname_t *sub_root, + zone_tree_it_t *it); + +/*! + * \brief Start iteration of two zone trees. + * + * This is useful e.g. for iteration over normal and NSEC3 nodes. + * + * \param first First tree to be iterated over. + * \param second Second tree to be iterated over. + * \param it Out: iteration context. It shall be zeroed before. + * + * \return KNOT_OK, KNOT_ENOMEM + */ +int zone_tree_it_double_begin(zone_tree_t *first, zone_tree_t *second, zone_tree_it_t *it); + +/*! + * \brief Return true iff iteration is finished. + * + * \note The iteration context needs to be freed afterwards nevertheless. + */ +bool zone_tree_it_finished(zone_tree_it_t *it); + +/*! + * \brief Return the node, zone iteration is currently pointing at. + * + * \note Don't call this when zone_tree_it_finished. + */ +zone_node_t *zone_tree_it_val(zone_tree_it_t *it); + +/*! + * \brief Remove from zone tree the node that iteration is pointing at. + * + * \note This doesn't free the node. + */ +void zone_tree_it_del(zone_tree_it_t *it); + +/*! + * \brief Move the iteration to next node. + */ +void zone_tree_it_next(zone_tree_it_t *it); + +/*! + * \brief Free zone iteration context. + */ +void zone_tree_it_free(zone_tree_it_t *it); + +/*! + * \brief Zone tree iteration allowing tree changes. + * + * The semantics is the same like for normal iteration. + * The set of iterated nodes is according to zone tree state on the beginning. + */ +int zone_tree_delsafe_it_begin(zone_tree_t *tree, zone_tree_delsafe_it_t *it, bool include_deleted); +bool zone_tree_delsafe_it_finished(zone_tree_delsafe_it_t *it); +void zone_tree_delsafe_it_restart(zone_tree_delsafe_it_t *it); +zone_node_t *zone_tree_delsafe_it_val(zone_tree_delsafe_it_t *it); +void zone_tree_delsafe_it_next(zone_tree_delsafe_it_t *it); +void zone_tree_delsafe_it_free(zone_tree_delsafe_it_t *it); + +/*! + * \brief Merge all nodes from 'what' to 'into'. + * + * \param into Zone tree to be inserted into.. + * \param what ...all nodes from this one. + * + * \return KNOT_E* + */ +int zone_tree_merge(zone_tree_t *into, zone_tree_t *what); + +/*! + * \brief Unify all bi-nodes in specified trees. + */ +void zone_trees_unify_binodes(zone_tree_t *nodes, zone_tree_t *nsec3_nodes, bool free_deleted); + +/*! + * \brief Destroys the zone tree, not touching the saved data. + * + * \param tree Zone tree to be destroyed. + */ +void zone_tree_free(zone_tree_t **tree); diff --git a/src/knot/zone/zone.c b/src/knot/zone/zone.c new file mode 100644 index 0000000..15a9c54 --- /dev/null +++ b/src/knot/zone/zone.c @@ -0,0 +1,792 @@ +/* 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 <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <time.h> +#include <urcu.h> + +#include "knot/common/log.h" +#include "knot/conf/module.h" +#include "knot/dnssec/kasp/kasp_db.h" +#include "knot/events/replan.h" +#include "knot/journal/journal_read.h" +#include "knot/journal/journal_write.h" +#include "knot/nameserver/process_query.h" +#include "knot/query/requestor.h" +#include "knot/updates/zone-update.h" +#include "knot/server/server.h" +#include "knot/zone/contents.h" +#include "knot/zone/serial.h" +#include "knot/zone/zone.h" +#include "knot/zone/zonefile.h" +#include "libknot/libknot.h" +#include "contrib/sockaddr.h" +#include "contrib/mempattern.h" +#include "contrib/ucw/lists.h" +#include "contrib/ucw/mempool.h" + +#define JOURNAL_LOCK_MUTEX (&zone->journal_lock) +#define JOURNAL_LOCK_RW pthread_mutex_lock(JOURNAL_LOCK_MUTEX); +#define JOURNAL_UNLOCK_RW pthread_mutex_unlock(JOURNAL_LOCK_MUTEX); + +knot_dynarray_define(notifailed_rmt, notifailed_rmt_hash, DYNARRAY_VISIBILITY_NORMAL); + +static void free_ddns_queue(zone_t *zone) +{ + ptrnode_t *node, *nxt; + WALK_LIST_DELSAFE(node, nxt, zone->ddns_queue) { + knot_request_free(node->d, NULL); + } + ptrlist_free(&zone->ddns_queue, NULL); +} + +/*! + * \param allow_empty_zone useful when need to flush journal but zone is not yet loaded + * ...in this case we actually don't have to do anything because the zonefile is current, + * but we must mark the journal as flushed + */ +static int flush_journal(conf_t *conf, zone_t *zone, bool allow_empty_zone, bool verbose) +{ + /*! @note Function expects nobody will change zone contents meanwhile. */ + + assert(zone); + + int ret = KNOT_EOK; + zone_journal_t j = zone_journal(zone); + + bool force = zone_get_flag(zone, ZONE_FORCE_FLUSH, true); + bool user_flush = zone_get_flag(zone, ZONE_USER_FLUSH, true); + + conf_val_t val = conf_zone_get(conf, C_ZONEFILE_SYNC, zone->name); + int64_t sync_timeout = conf_int(&val); + + if (zone_contents_is_empty(zone->contents)) { + if (allow_empty_zone && journal_is_existing(j)) { + ret = journal_set_flushed(j); + } else { + ret = KNOT_EEMPTYZONE; + } + goto flush_journal_replan; + } + + /* Check for disabled zonefile synchronization. */ + if (sync_timeout < 0 && !force) { + if (verbose) { + log_zone_warning(zone->name, "zonefile synchronization disabled, " + "use force command to override it"); + } + return KNOT_EOK; + } + + /* Check for updated zone. */ + zone_contents_t *contents = zone->contents; + uint32_t serial_to = zone_contents_serial(contents); + if (!force && !user_flush && + zone->zonefile.exists && zone->zonefile.serial == serial_to && + !zone->zonefile.retransfer && !zone->zonefile.resigned) { + ret = KNOT_EOK; /* No differences. */ + goto flush_journal_replan; + } + + char *zonefile = conf_zonefile(conf, zone->name); + + /* Synchronize journal. */ + ret = zonefile_write(zonefile, contents); + if (ret != KNOT_EOK) { + log_zone_warning(zone->name, "failed to update zone file (%s)", + knot_strerror(ret)); + free(zonefile); + goto flush_journal_replan; + } + + if (zone->zonefile.exists) { + log_zone_info(zone->name, "zone file updated, serial %u -> %u", + zone->zonefile.serial, serial_to); + } else { + log_zone_info(zone->name, "zone file updated, serial %u", + serial_to); + } + + /* Update zone version. */ + struct stat st; + if (stat(zonefile, &st) < 0) { + log_zone_warning(zone->name, "failed to update zone file (%s)", + knot_strerror(knot_map_errno())); + free(zonefile); + ret = KNOT_EACCES; + goto flush_journal_replan; + } + + free(zonefile); + + /* Update zone file attributes. */ + zone->zonefile.exists = true; + zone->zonefile.mtime = st.st_mtim; + zone->zonefile.serial = serial_to; + zone->zonefile.resigned = false; + zone->zonefile.retransfer = false; + + /* Flush journal. */ + if (journal_is_existing(j)) { + ret = journal_set_flushed(j); + } + +flush_journal_replan: + /* Plan next journal flush after proper period. */ + zone->timers.last_flush = time(NULL); + if (sync_timeout > 0) { + time_t next_flush = zone->timers.last_flush + sync_timeout; + zone_events_schedule_at(zone, ZONE_EVENT_FLUSH, 0, + ZONE_EVENT_FLUSH, next_flush); + } + + return ret; +} + +zone_t* zone_new(const knot_dname_t *name) +{ + zone_t *zone = malloc(sizeof(zone_t)); + if (zone == NULL) { + return NULL; + } + memset(zone, 0, sizeof(zone_t)); + + zone->name = knot_dname_copy(name, NULL); + if (zone->name == NULL) { + free(zone); + return NULL; + } + + // DDNS + pthread_mutex_init(&zone->ddns_lock, NULL); + zone->ddns_queue_size = 0; + init_list(&zone->ddns_queue); + + knot_sem_init(&zone->cow_lock, 1); + + // Preferred master lock + pthread_mutex_init(&zone->preferred_lock, NULL); + + // Initialize events + zone_events_init(zone); + + // Initialize query modules list. + init_list(&zone->query_modules); + + return zone; +} + +void zone_control_clear(zone_t *zone) +{ + if (zone == NULL) { + return; + } + + zone_update_clear(zone->control_update); + free(zone->control_update); + zone->control_update = NULL; +} + +void zone_free(zone_t **zone_ptr) +{ + if (zone_ptr == NULL || *zone_ptr == NULL) { + return; + } + + zone_t *zone = *zone_ptr; + + zone_events_deinit(zone); + + knot_dname_free(zone->name, NULL); + + free_ddns_queue(zone); + pthread_mutex_destroy(&zone->ddns_lock); + + knot_sem_destroy(&zone->cow_lock); + + /* Control update. */ + zone_control_clear(zone); + + free(zone->catalog_gen); + catalog_update_free(zone->cat_members); + + /* Free preferred master. */ + pthread_mutex_destroy(&zone->preferred_lock); + free(zone->preferred_master); + + /* Free zone contents. */ + zone_contents_deep_free(zone->contents); + + conf_deactivate_modules(&zone->query_modules, &zone->query_plan); + + free(zone); + *zone_ptr = NULL; +} + +void zone_reset(conf_t *conf, zone_t *zone) +{ + if (zone == NULL) { + return; + } + + zone_contents_t *old_contents = zone_switch_contents(zone, NULL); + conf_reset_modules(conf, &zone->query_modules, &zone->query_plan); // includes synchronize_rcu() + zone_contents_deep_free(old_contents); + if (zone_expired(zone)) { + replan_from_timers(conf, zone); + } else { + zone_events_schedule_now(zone, ZONE_EVENT_LOAD); + } +} + +#define RETURN_IF_FAILED(str, exception) \ +{ \ + if (ret != KNOT_EOK && ret != (exception)) { \ + errors = true; \ + log_zone_error(zone->name, \ + "failed to purge %s (%s)", (str), knot_strerror(ret)); \ + if (exit_immediately) { \ + return ret; \ + } \ + } \ +} + +int selective_zone_purge(conf_t *conf, zone_t *zone, purge_flag_t params) +{ + if (conf == NULL || zone == NULL) { + return KNOT_EINVAL; + } + + int ret; + bool errors = false; + bool exit_immediately = !(params & PURGE_ZONE_BEST); + + // Purge the zone timers. + if (params & PURGE_ZONE_TIMERS) { + zone->timers = (zone_timers_t) { + .catalog_member = zone->timers.catalog_member + }; + zone->zonefile.bootstrap_cnt = 0; + ret = zone_timers_sweep(&zone->server->timerdb, + (sweep_cb)knot_dname_cmp, zone->name); + RETURN_IF_FAILED("timers", KNOT_ENOENT); + } + + // Purge the zone file. + if (params & PURGE_ZONE_ZONEFILE) { + conf_val_t sync; + if ((params & PURGE_ZONE_NOSYNC) || + (sync = conf_zone_get(conf, C_ZONEFILE_SYNC, zone->name), + conf_int(&sync) > -1)) { + char *zonefile = conf_zonefile(conf, zone->name); + ret = (unlink(zonefile) == -1 ? knot_map_errno() : KNOT_EOK); + free(zonefile); + RETURN_IF_FAILED("zone file", KNOT_ENOENT); + } + } + + // Purge the zone journal. + if (params & PURGE_ZONE_JOURNAL) { + ret = journal_scrape_with_md(zone_journal(zone), true); + RETURN_IF_FAILED("journal", KNOT_ENOENT); + } + + // Purge KASP DB. + if (params & PURGE_ZONE_KASPDB) { + ret = knot_lmdb_open(zone_kaspdb(zone)); + if (ret == KNOT_EOK) { + ret = kasp_db_delete_all(zone_kaspdb(zone), zone->name); + } + RETURN_IF_FAILED("KASP DB", KNOT_ENOENT); + } + + // Purge Catalog. + if (params & PURGE_ZONE_CATALOG) { + zone->timers.catalog_member = 0; + ret = catalog_zone_purge(zone->server, conf, zone->name); + RETURN_IF_FAILED("catalog", KNOT_EOK); + } + + if (errors) { + return KNOT_ERROR; + } + + if ((params & PURGE_ZONE_LOG) || + (params & PURGE_ZONE_DATA) == PURGE_ZONE_DATA) { + log_zone_notice(zone->name, "zone purged"); + } + + return KNOT_EOK; +} + +knot_lmdb_db_t *zone_journaldb(const zone_t *zone) +{ + return &zone->server->journaldb; +} + +knot_lmdb_db_t *zone_kaspdb(const zone_t *zone) +{ + return &zone->server->kaspdb; +} + +catalog_t *zone_catalog(const zone_t *zone) +{ + return &zone->server->catalog; +} + +catalog_update_t *zone_catalog_upd(const zone_t *zone) +{ + return &zone->server->catalog_upd; +} + +int zone_change_store(conf_t *conf, zone_t *zone, changeset_t *change, changeset_t *extra) +{ + if (conf == NULL || zone == NULL || change == NULL) { + return KNOT_EINVAL; + } + + zone_journal_t j = { zone_journaldb(zone), zone->name, conf }; + + int ret = journal_insert(j, change, extra, NULL); + if (ret == KNOT_EBUSY) { + log_zone_notice(zone->name, "journal is full, flushing"); + + /* Transaction rolled back, journal released, we may flush. */ + ret = flush_journal(conf, zone, true, false); + if (ret == KNOT_EOK) { + ret = journal_insert(j, change, extra, NULL); + } + } + + return ret; +} + +int zone_diff_store(conf_t *conf, zone_t *zone, const zone_diff_t *diff) +{ + if (conf == NULL || zone == NULL || diff == NULL) { + return KNOT_EINVAL; + } + + zone_journal_t j = { zone_journaldb(zone), zone->name, conf }; + + int ret = journal_insert(j, NULL, NULL, diff); + if (ret == KNOT_EBUSY) { + log_zone_notice(zone->name, "journal is full, flushing"); + + /* Transaction rolled back, journal released, we may flush. */ + ret = flush_journal(conf, zone, true, false); + if (ret == KNOT_EOK) { + ret = journal_insert(j, NULL, NULL, diff); + } + } + + return ret; +} + +int zone_changes_clear(conf_t *conf, zone_t *zone) +{ + if (conf == NULL || zone == NULL) { + return KNOT_EINVAL; + } + + return journal_scrape_with_md(zone_journal(zone), true); +} + +int zone_in_journal_store(conf_t *conf, zone_t *zone, zone_contents_t *new_contents) +{ + if (conf == NULL || zone == NULL) { + return KNOT_EINVAL; + } + + if (new_contents == NULL) { + return KNOT_EEMPTYZONE; + } + + zone_journal_t j = { zone_journaldb(zone), zone->name, conf }; + + int ret = journal_insert_zone(j, new_contents); + if (ret == KNOT_EOK) { + log_zone_info(zone->name, "zone stored to journal, serial %u", + zone_contents_serial(new_contents)); + } + + return ret; +} + +int zone_flush_journal(conf_t *conf, zone_t *zone, bool verbose) +{ + if (conf == NULL || zone == NULL) { + return KNOT_EINVAL; + } + + return flush_journal(conf, zone, false, verbose); +} + +bool zone_journal_has_zij(zone_t *zone) +{ + bool exists = false, zij = false; + (void)journal_info(zone_journal(zone), &exists, NULL, &zij, NULL, NULL, NULL, NULL, NULL); + return exists && zij; +} + +void zone_notifailed_clear(zone_t *zone) +{ + pthread_mutex_lock(&zone->preferred_lock); + notifailed_rmt_dynarray_free(&zone->notifailed); + pthread_mutex_unlock(&zone->preferred_lock); +} + +void zone_schedule_notify(zone_t *zone, time_t delay) +{ + zone_notifailed_clear(zone); + zone_events_schedule_at(zone, ZONE_EVENT_NOTIFY, time(NULL) + delay); +} + +zone_contents_t *zone_switch_contents(zone_t *zone, zone_contents_t *new_contents) +{ + if (zone == NULL) { + return NULL; + } + + zone_contents_t *old_contents; + zone_contents_t **current_contents = &zone->contents; + old_contents = rcu_xchg_pointer(current_contents, new_contents); + + return old_contents; +} + +bool zone_is_slave(conf_t *conf, const zone_t *zone) +{ + if (conf == NULL || zone == NULL) { + return false; + } + + conf_val_t val = conf_zone_get(conf, C_MASTER, zone->name); + return conf_val_count(&val) > 0 ? true : false; +} + +void zone_set_preferred_master(zone_t *zone, const struct sockaddr_storage *addr) +{ + if (zone == NULL || addr == NULL) { + return; + } + + pthread_mutex_lock(&zone->preferred_lock); + free(zone->preferred_master); + zone->preferred_master = malloc(sizeof(struct sockaddr_storage)); + *zone->preferred_master = *addr; + pthread_mutex_unlock(&zone->preferred_lock); +} + +void zone_clear_preferred_master(zone_t *zone) +{ + if (zone == NULL) { + return; + } + + pthread_mutex_lock(&zone->preferred_lock); + free(zone->preferred_master); + zone->preferred_master = NULL; + pthread_mutex_unlock(&zone->preferred_lock); +} + +static void set_flag(zone_t *zone, zone_flag_t flag, bool remove) +{ + if (zone == NULL) { + return; + } + + pthread_mutex_lock(&zone->preferred_lock); // this mutex seems OK to be reused for this + zone->flags = remove ? (zone->flags & ~flag) : (zone->flags | flag); + pthread_mutex_unlock(&zone->preferred_lock); + + if (flag & ZONE_IS_CATALOG) { + zone->is_catalog_flag = !remove; + } +} + +void zone_set_flag(zone_t *zone, zone_flag_t flag) +{ + return set_flag(zone, flag, false); +} + +void zone_unset_flag(zone_t *zone, zone_flag_t flag) +{ + return set_flag(zone, flag, true); +} + +zone_flag_t zone_get_flag(zone_t *zone, zone_flag_t flag, bool clear) +{ + if (zone == NULL) { + return 0; + } + + pthread_mutex_lock(&zone->preferred_lock); + zone_flag_t res = (zone->flags & flag); + if (clear && res) { + zone->flags &= ~flag; + } + assert(((bool)(zone->flags & ZONE_IS_CATALOG)) == zone->is_catalog_flag); + pthread_mutex_unlock(&zone->preferred_lock); + + return res; +} + +const knot_rdataset_t *zone_soa(const zone_t *zone) +{ + if (!zone || zone_contents_is_empty(zone->contents)) { + return NULL; + } + + return node_rdataset(zone->contents->apex, KNOT_RRTYPE_SOA); +} + +uint32_t zone_soa_expire(const zone_t *zone) +{ + const knot_rdataset_t *soa = zone_soa(zone); + return soa == NULL ? 0 : knot_soa_expire(soa->rdata); +} + +bool zone_expired(const zone_t *zone) +{ + if (!zone) { + return false; + } + + const zone_timers_t *timers = &zone->timers; + + return timers->next_expire > 0 && timers->next_expire <= time(NULL); +} + +static void time_set_default(time_t *time, time_t value) +{ + assert(time); + + if (*time == 0) { + *time = value; + } +} + +void zone_timers_sanitize(conf_t *conf, zone_t *zone) +{ + assert(conf); + assert(zone); + + time_t now = time(NULL); + + // assume now if we don't know when we flushed + time_set_default(&zone->timers.last_flush, now); + + if (zone_is_slave(conf, zone)) { + // assume now if we don't know + time_set_default(&zone->timers.next_refresh, now); + if (zone->is_catalog_flag) { + zone->timers.next_expire = 0; + } + } else { + // invalidate if we don't have a master + zone->timers.last_refresh = 0; + zone->timers.next_refresh = 0; + zone->timers.last_refresh_ok = false; + zone->timers.next_expire = 0; + } +} + +/*! + * \brief Get preferred zone master while checking its existence. + */ +int static preferred_master(conf_t *conf, zone_t *zone, conf_remote_t *master) +{ + pthread_mutex_lock(&zone->preferred_lock); + + if (zone->preferred_master == NULL) { + pthread_mutex_unlock(&zone->preferred_lock); + return KNOT_ENOENT; + } + + conf_val_t masters = conf_zone_get(conf, C_MASTER, zone->name); + conf_mix_iter_t iter; + conf_mix_iter_init(conf, &masters, &iter); + while (iter.id->code == KNOT_EOK) { + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, iter.id); + size_t addr_count = conf_val_count(&addr); + + for (size_t i = 0; i < addr_count; i++) { + conf_remote_t remote = conf_remote(conf, iter.id, i); + if (sockaddr_net_match(&remote.addr, zone->preferred_master, -1)) { + *master = remote; + pthread_mutex_unlock(&zone->preferred_lock); + return KNOT_EOK; + } + } + + conf_mix_iter_next(&iter); + } + + pthread_mutex_unlock(&zone->preferred_lock); + + return KNOT_ENOENT; +} + +static void log_try_addr_error(const zone_t *zone, const char *remote_name, + const struct sockaddr_storage *remote_addr, + const char *err_str, int ret) +{ + char addr_str[SOCKADDR_STRLEN] = { 0 }; + sockaddr_tostr(addr_str, sizeof(addr_str), remote_addr); + log_zone_info(zone->name, "%s%s%s, address %s, failed (%s)", err_str, + (remote_name != NULL ? ", remote " : ""), + (remote_name != NULL ? remote_name : ""), + addr_str, knot_strerror(ret)); +} + +int zone_master_try(conf_t *conf, zone_t *zone, zone_master_cb callback, + void *callback_data, const char *err_str) +{ + if (conf == NULL || zone == NULL || callback == NULL || err_str == NULL) { + return KNOT_EINVAL; + } + + zone_master_fallback_t fallback = { true, true }; + + /* Try the preferred server. */ + + conf_remote_t preferred = { { AF_UNSPEC } }; + if (preferred_master(conf, zone, &preferred) == KNOT_EOK) { + int ret = callback(conf, zone, &preferred, callback_data, &fallback); + if (ret == KNOT_EOK) { + return ret; + } else if (!fallback.remote) { + return ret; // Local error. + } + + log_try_addr_error(zone, NULL, &preferred.addr, err_str, ret); + + char addr_str[SOCKADDR_STRLEN] = { 0 }; + sockaddr_tostr(addr_str, sizeof(addr_str), &preferred.addr); + log_zone_warning(zone->name, "%s, address %s not usable", + err_str, addr_str); + } + + /* Try all the other servers. */ + + bool success = false; + + conf_val_t masters = conf_zone_get(conf, C_MASTER, zone->name); + conf_mix_iter_t iter; + conf_mix_iter_init(conf, &masters, &iter); + while (iter.id->code == KNOT_EOK && fallback.remote) { + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, iter.id); + size_t addr_count = conf_val_count(&addr); + + bool tried = false; + fallback.address = true; + for (size_t i = 0; i < addr_count && fallback.address; i++) { + conf_remote_t master = conf_remote(conf, iter.id, i); + if (preferred.addr.ss_family != AF_UNSPEC && + sockaddr_net_match(&master.addr, &preferred.addr, -1)) { + preferred.addr.ss_family = AF_UNSPEC; + continue; + } + + tried = true; + int ret = callback(conf, zone, &master, callback_data, &fallback); + if (ret == KNOT_EOK) { + success = true; + break; + } else if (!fallback.remote) { + return ret; // Local error. + } + + log_try_addr_error(zone, conf_str(iter.id), &master.addr, + err_str, ret); + } + + if (!success && tried) { + log_zone_warning(zone->name, "%s, remote %s not usable", + err_str, conf_str(iter.id)); + } + + conf_mix_iter_next(&iter); + } + + return success ? KNOT_EOK : KNOT_ENOMASTER; +} + +int zone_dump_to_dir(conf_t *conf, zone_t *zone, const char *dir) +{ + if (zone == NULL || dir == NULL) { + return KNOT_EINVAL; + } + + size_t dir_len = strlen(dir); + if (dir_len == 0) { + return KNOT_EINVAL; + } + + char *zonefile = conf_zonefile(conf, zone->name); + char *zonefile_basename = strrchr(zonefile, '/'); + if (zonefile_basename == NULL) { + zonefile_basename = zonefile; + } + + size_t target_length = strlen(zonefile_basename) + dir_len + 2; + char target[target_length]; + (void)snprintf(target, target_length, "%s/%s", dir, zonefile_basename); + if (strcmp(target, zonefile) == 0) { + free(zonefile); + return KNOT_EDENIED; + } + free(zonefile); + + return zonefile_write(target, zone->contents); +} + +int zone_set_master_serial(zone_t *zone, uint32_t serial) +{ + return kasp_db_store_serial(zone_kaspdb(zone), zone->name, KASPDB_SERIAL_MASTER, serial); +} + +int zone_get_master_serial(zone_t *zone, uint32_t *serial) +{ + return kasp_db_load_serial(zone_kaspdb(zone), zone->name, KASPDB_SERIAL_MASTER, serial); +} + +int zone_set_lastsigned_serial(zone_t *zone, uint32_t serial) +{ + return kasp_db_store_serial(zone_kaspdb(zone), zone->name, KASPDB_SERIAL_LASTSIGNED, serial); +} + +int zone_get_lastsigned_serial(zone_t *zone, uint32_t *serial) +{ + return kasp_db_load_serial(zone_kaspdb(zone), zone->name, KASPDB_SERIAL_LASTSIGNED, serial); +} + +int slave_zone_serial(zone_t *zone, conf_t *conf, uint32_t *serial) +{ + int ret = KNOT_EOK; + assert(zone->contents != NULL); + *serial = zone_contents_serial(zone->contents); + + conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name); + if (conf_bool(&val)) { + ret = zone_get_master_serial(zone, serial); + } + + return ret; +} diff --git a/src/knot/zone/zone.h b/src/knot/zone/zone.h new file mode 100644 index 0000000..ae8991e --- /dev/null +++ b/src/knot/zone/zone.h @@ -0,0 +1,290 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/semaphore.h" +#include "knot/catalog/catalog_update.h" +#include "knot/conf/conf.h" +#include "knot/conf/confio.h" +#include "knot/journal/journal_basic.h" +#include "knot/journal/serialization.h" +#include "knot/events/events.h" +#include "knot/updates/changesets.h" +#include "knot/zone/contents.h" +#include "knot/zone/timers.h" +#include "libknot/dname.h" +#include "libknot/dynarray.h" +#include "libknot/packet/pkt.h" + +struct zone_update; +struct zone_backup_ctx; + +/*! + * \brief Zone flags. + * + * When updating check create_zone_reload() if the flag mask is ok. + */ +typedef enum { + ZONE_FORCE_AXFR = 1 << 0, /*!< Force AXFR as next transfer. */ + ZONE_FORCE_RESIGN = 1 << 1, /*!< Force zone re-sign. */ + ZONE_FORCE_FLUSH = 1 << 2, /*!< Force zone flush. */ + ZONE_FORCE_KSK_ROLL = 1 << 3, /*!< Force KSK/CSK rollover. */ + ZONE_FORCE_ZSK_ROLL = 1 << 4, /*!< Force ZSK rollover. */ + ZONE_IS_CATALOG = 1 << 5, /*!< This is a catalog. */ + ZONE_IS_CAT_MEMBER = 1 << 6, /*!< This zone exists according to a catalog. */ + ZONE_XFR_FROZEN = 1 << 7, /*!< Outgoing AXFR/IXFR temporarily disabled. */ + ZONE_USER_FLUSH = 1 << 8, /*!< User-triggered flush. */ +} zone_flag_t; + +/*! + * \brief Track unsuccessful NOTIFY targets. + */ +typedef uint64_t notifailed_rmt_hash; +knot_dynarray_declare(notifailed_rmt, notifailed_rmt_hash, DYNARRAY_VISIBILITY_NORMAL, 4); + +/*! + * \brief Zone purging parameter flags. + */ +typedef enum { + PURGE_ZONE_BEST = 1 << 0, /*!< Best effort -- continue on failures. */ + PURGE_ZONE_LOG = 1 << 1, /*!< Log a purged zone even if requested less. */ + PURGE_ZONE_NOSYNC = 1 << 2, /*!< Remove even zone files with disabled syncing. */ + PURGE_ZONE_TIMERS = 1 << 3, /*!< Purge the zone timers. */ + PURGE_ZONE_ZONEFILE = 1 << 4, /*!< Purge the zone file. */ + PURGE_ZONE_JOURNAL = 1 << 5, /*!< Purge the zone journal. */ + PURGE_ZONE_KASPDB = 1 << 6, /*!< Purge KASP DB. */ + PURGE_ZONE_CATALOG = 1 << 7, /*!< Purge the catalog. */ +} purge_flag_t; + +#define PURGE_ZONE_FULL ~0U /*!< Purge everything possible. */ + /*!< Standard purge (respect C_ZONEFILE_SYNC param). */ +#define PURGE_ZONE_ALL (PURGE_ZONE_FULL ^ PURGE_ZONE_NOSYNC) + /*!< All data. */ +#define PURGE_ZONE_DATA (PURGE_ZONE_TIMERS | PURGE_ZONE_ZONEFILE | PURGE_ZONE_JOURNAL | \ + PURGE_ZONE_KASPDB | PURGE_ZONE_CATALOG) + +/*! + * \brief Structure for holding DNS zone. + */ +typedef struct zone +{ + knot_dname_t *name; + zone_contents_t *contents; + zone_flag_t flags; + bool is_catalog_flag; //!< Lock-less indication of ZONE_IS_CATALOG flag. + + /*! \brief Dynamic configuration zone change type. */ + conf_io_type_t change_type; + + /*! \brief Zonefile parameters. */ + struct { + struct timespec mtime; + uint32_t serial; + bool exists; + bool resigned; + bool retransfer; + uint8_t bootstrap_cnt; //!< Rebootstrap count (not related to zonefile). + } zonefile; + + /*! \brief Zone events. */ + zone_timers_t timers; //!< Persistent zone timers. + zone_events_t events; //!< Zone events timers. + + /*! \brief Track unsuccessful NOTIFY targets. */ + notifailed_rmt_dynarray_t notifailed; + + /*! \brief DDNS queue and lock. */ + pthread_mutex_t ddns_lock; + size_t ddns_queue_size; + list_t ddns_queue; + + /*! \brief Control update context. */ + struct zone_update *control_update; + + /*! \brief Ensue one COW transaction on zone's trees at a time. */ + knot_sem_t cow_lock; + + /*! \brief Pointer on running server with e.g. KASP db, journal DB, catalog... */ + struct server *server; + + /*! \brief Zone backup context (NULL unless backup pending). */ + struct zone_backup_ctx *backup_ctx; + + /*! \brief Catalog-generate feature. */ + knot_dname_t *catalog_gen; + catalog_update_t *cat_members; + const char *catalog_group; + + /*! \brief Preferred master lock. Also used for flags access. */ + pthread_mutex_t preferred_lock; + /*! \brief Preferred master for remote operation. */ + struct sockaddr_storage *preferred_master; + + /*! \brief Query modules. */ + list_t query_modules; + struct query_plan *query_plan; +} zone_t; + +/*! + * \brief Creates new zone with empty zone content. + * + * \param name Zone name. + * + * \return The initialized zone structure or NULL if an error occurred. + */ +zone_t* zone_new(const knot_dname_t *name); + +/*! + * \brief Deallocates the zone structure. + * + * \note The function also deallocates all bound structures (contents, etc.). + * + * \param zone_ptr Zone to be freed. + */ +void zone_free(zone_t **zone_ptr); + +/*! + * \brief Clear zone contents (->SERVFAIL), reset modules, plan LOAD. + * + * \param conf Current configuration. + * \param zone Zone to be re-set. + */ +void zone_reset(conf_t *conf, zone_t *zone); + +/*! + * \brief Purges selected zone components. + * + * \param conf Current configuration. + * \param zone Zone to be purged. + * \param params Zone components to be purged and the purging mode + * (with PURGE_ZONE_BEST try to purge everything requested, + * otherwise exit on the first failure). + * + * \return KNOT_E* + */ +int selective_zone_purge(conf_t *conf, zone_t *zone, purge_flag_t params); + +/*! + * \brief Clears possible control update transaction. + * + * \param zone Zone to be cleared. + */ +void zone_control_clear(zone_t *zone); + +/*! + * \brief Common database getters. + */ +knot_lmdb_db_t *zone_journaldb(const zone_t *zone); +knot_lmdb_db_t *zone_kaspdb(const zone_t *zone); +catalog_t *zone_catalog(const zone_t *zone); +catalog_update_t *zone_catalog_upd(const zone_t *zone); + +/*! + * \brief Only for RO journal operations. + */ +inline static zone_journal_t zone_journal(zone_t *zone) +{ + zone_journal_t j = { zone_journaldb(zone), zone->name, NULL }; + return j; +} + +int zone_change_store(conf_t *conf, zone_t *zone, changeset_t *change, changeset_t *extra); +int zone_diff_store(conf_t *conf, zone_t *zone, const zone_diff_t *diff); +int zone_changes_clear(conf_t *conf, zone_t *zone); +int zone_in_journal_store(conf_t *conf, zone_t *zone, zone_contents_t *new_contents); + +/*! \brief Synchronize zone file with journal. */ +int zone_flush_journal(conf_t *conf, zone_t *zone, bool verbose); + +bool zone_journal_has_zij(zone_t *zone); + +/*! + * \brief Clear failed_notify list before planning new NOTIFY. + */ +void zone_notifailed_clear(zone_t *zone); +void zone_schedule_notify(zone_t *zone, time_t delay); + +/*! + * \brief Atomically switch the content of the zone. + */ +zone_contents_t *zone_switch_contents(zone_t *zone, zone_contents_t *new_contents); + +/*! \brief Checks if the zone is slave. */ +bool zone_is_slave(conf_t *conf, const zone_t *zone); + +/*! \brief Sets the address as a preferred master address. */ +void zone_set_preferred_master(zone_t *zone, const struct sockaddr_storage *addr); + +/*! \brief Clears the current preferred master address. */ +void zone_clear_preferred_master(zone_t *zone); + +/*! \brief Sets a zone flag. */ +void zone_set_flag(zone_t *zone, zone_flag_t flag); + +/*! \brief Unsets a zone flag. */ +void zone_unset_flag(zone_t *zone, zone_flag_t flag); + +/*! \brief Returns if a flag is set (and optionally clears it). */ +zone_flag_t zone_get_flag(zone_t *zone, zone_flag_t flag, bool clear); + +/*! \brief Get zone SOA RR. */ +const knot_rdataset_t *zone_soa(const zone_t *zone); + +/*! \brief Get zone SOA EXPIRE field, or 0 if empty zone. */ +uint32_t zone_soa_expire(const zone_t *zone); + +/*! \brief Check if zone is expired according to timers. */ +bool zone_expired(const zone_t *zone); + +/*! + * \brief Set default timers for new zones or invalidate if not valid. + */ +void zone_timers_sanitize(conf_t *conf, zone_t *zone); + +typedef struct { + bool address; //!< Fallback to next remote address is required. + bool remote; //!< Fallback to next remote server is required. +} zone_master_fallback_t; + +typedef int (*zone_master_cb)(conf_t *conf, zone_t *zone, const conf_remote_t *remote, + void *data, zone_master_fallback_t *fallback); + +/*! + * \brief Perform an action with all configured master servers. + * + * The function iterates over available masters. For each master, the callback + * function is called once for its every adresses until the callback function + * succeeds (\ref KNOT_EOK is returned) and then the iteration continues with + * the next master. + * + * \return Error code from the last callback or KNOT_ENOMASTER. + */ +int zone_master_try(conf_t *conf, zone_t *zone, zone_master_cb callback, + void *callback_data, const char *err_str); + +/*! \brief Write zone contents to zonefile, but into different directory. */ +int zone_dump_to_dir(conf_t *conf, zone_t *zone, const char *dir); + +int zone_set_master_serial(zone_t *zone, uint32_t serial); + +int zone_get_master_serial(zone_t *zone, uint32_t *serial); + +int zone_set_lastsigned_serial(zone_t *zone, uint32_t serial); + +int zone_get_lastsigned_serial(zone_t *zone, uint32_t *serial); + +int slave_zone_serial(zone_t *zone, conf_t *conf, uint32_t *serial); diff --git a/src/knot/zone/zonedb-load.c b/src/knot/zone/zonedb-load.c new file mode 100644 index 0000000..58b17ab --- /dev/null +++ b/src/knot/zone/zonedb-load.c @@ -0,0 +1,643 @@ +/* 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 <unistd.h> +#include <urcu.h> + +#include "knot/catalog/generate.h" +#include "knot/common/log.h" +#include "knot/conf/module.h" +#include "knot/events/replan.h" +#include "knot/journal/journal_metadata.h" +#include "knot/zone/digest.h" +#include "knot/zone/timers.h" +#include "knot/zone/zone-load.h" +#include "knot/zone/zone.h" +#include "knot/zone/zonedb-load.h" +#include "knot/zone/zonedb.h" +#include "knot/zone/zonefile.h" +#include "libknot/libknot.h" + +static bool zone_file_updated(conf_t *conf, const zone_t *old_zone, + const knot_dname_t *zone_name) +{ + assert(conf); + assert(zone_name); + + if (old_zone == NULL) { + return false; + } + + char *zonefile = conf_zonefile(conf, zone_name); + struct timespec mtime; + int ret = zonefile_exists(zonefile, &mtime); + free(zonefile); + + if (ret == KNOT_EOK) { + return !(old_zone->zonefile.exists && + old_zone->zonefile.mtime.tv_sec == mtime.tv_sec && + old_zone->zonefile.mtime.tv_nsec == mtime.tv_nsec); + } else { + return old_zone->zonefile.exists; + } +} + +static void zone_get_catalog_group(conf_t *conf, zone_t *zone) +{ + conf_val_t val = conf_zone_get(conf, C_CATALOG_GROUP, zone->name); + if (val.code == KNOT_EOK) { + zone->catalog_group = conf_str(&val); + } +} + +static zone_t *create_zone_from(const knot_dname_t *name, server_t *server) +{ + zone_t *zone = zone_new(name); + if (!zone) { + return NULL; + } + + zone->server = server; + + int result = zone_events_setup(zone, server->workers, &server->sched); + if (result != KNOT_EOK) { + zone_free(&zone); + return NULL; + } + + return zone; +} + +static void replan_events(conf_t *conf, zone_t *zone, zone_t *old_zone) +{ + bool conf_updated = (old_zone->change_type & CONF_IO_TRELOAD); + + conf_val_t digest = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name); + if (zone->contents != NULL && !zone_contents_digest_exists(zone->contents, conf_opt(&digest), true)) { + conf_updated = true; + } + + zone->events.ufrozen = old_zone->events.ufrozen; + if ((zone_file_updated(conf, old_zone, zone->name) || conf_updated) && !zone_expired(zone)) { + replan_load_updated(zone, old_zone); + } else { + zone->zonefile = old_zone->zonefile; + memcpy(&zone->notifailed, &old_zone->notifailed, sizeof(zone->notifailed)); + memset(&old_zone->notifailed, 0, sizeof(zone->notifailed)); + replan_load_current(conf, zone, old_zone); + } +} + +static zone_t *create_zone_reload(conf_t *conf, const knot_dname_t *name, + server_t *server, zone_t *old_zone) +{ + zone_t *zone = create_zone_from(name, server); + if (!zone) { + return NULL; + } + + zone->contents = old_zone->contents; + zone_set_flag(zone, zone_get_flag(old_zone, ~0, false)); + + zone->timers = old_zone->timers; + zone_timers_sanitize(conf, zone); + + if (old_zone->control_update != NULL) { + log_zone_warning(old_zone->name, "control transaction aborted"); + zone_control_clear(old_zone); + } + + zone->cat_members = old_zone->cat_members; + old_zone->cat_members = NULL; + + zone->catalog_gen = old_zone->catalog_gen; + old_zone->catalog_gen = NULL; + + return zone; +} + +static zone_t *create_zone_new(conf_t *conf, const knot_dname_t *name, + server_t *server) +{ + zone_t *zone = create_zone_from(name, server); + if (!zone) { + return NULL; + } + + int ret = zone_timers_read(&server->timerdb, name, &zone->timers); + if (ret != KNOT_EOK && ret != KNOT_ENODB && ret != KNOT_ENOENT) { + log_zone_error(zone->name, "failed to load persistent timers (%s)", + knot_strerror(ret)); + zone_free(&zone); + return NULL; + } + + zone_timers_sanitize(conf, zone); + + conf_val_t role_val = conf_zone_get(conf, C_CATALOG_ROLE, name); + unsigned role = conf_opt(&role_val); + if (role == CATALOG_ROLE_MEMBER) { + conf_val_t catz = conf_zone_get(conf, C_CATALOG_ZONE, name); + assert(catz.code == KNOT_EOK); // conf consistency checked in conf/tools.c + zone->catalog_gen = knot_dname_copy(conf_dname(&catz), NULL); + if (zone->timers.catalog_member == 0) { + zone->timers.catalog_member = time(NULL); + } + if (zone->catalog_gen == NULL) { + log_zone_error(zone->name, "failed to initialize catalog member zone (%s)", + knot_strerror(KNOT_ENOMEM)); + zone_free(&zone); + return NULL; + } + } else if (role == CATALOG_ROLE_GENERATE) { + zone->cat_members = catalog_update_new(); + if (zone->cat_members == NULL) { + log_zone_error(zone->name, "failed to initialize catalog zone (%s)", + knot_strerror(KNOT_ENOMEM)); + zone_free(&zone); + return NULL; + } + zone_set_flag(zone, ZONE_IS_CATALOG); + } else if (role == CATALOG_ROLE_INTERPRET) { + ret = catalog_open(&server->catalog); + if (ret != KNOT_EOK) { + log_error("failed to open catalog database (%s)", knot_strerror(ret)); + } + zone_set_flag(zone, ZONE_IS_CATALOG); + } + + if (zone_expired(zone)) { + // expired => force bootstrap, no load attempt + log_zone_info(zone->name, "zone will be bootstrapped"); + assert(zone_is_slave(conf, zone)); + replan_load_bootstrap(conf, zone); + } else { + log_zone_info(zone->name, "zone will be loaded"); + // if load fails, fallback to bootstrap + replan_load_new(zone, role == CATALOG_ROLE_GENERATE); + } + + return zone; +} + +/*! + * \brief Load or reload the zone. + * + * \param conf Configuration. + * \param server Server. + * \param old_zone Already loaded zone (can be NULL). + * + * \return Error code, KNOT_EOK if successful. + */ +static zone_t *create_zone(conf_t *conf, const knot_dname_t *name, server_t *server, + zone_t *old_zone) +{ + assert(conf); + assert(name); + assert(server); + + zone_t *z; + + if (old_zone) { + z = create_zone_reload(conf, name, server, old_zone); + } else { + z = create_zone_new(conf, name, server); + } + + if (z != NULL) { + zone_get_catalog_group(conf, z); + } + + return z; +} + +static void mark_changed_zones(knot_zonedb_t *zonedb, trie_t *changed) +{ + if (changed == NULL) { + return; + } + + trie_it_t *it = trie_it_begin(changed); + for (; !trie_it_finished(it); trie_it_next(it)) { + const knot_dname_t *name = + (const knot_dname_t *)trie_it_key(it, NULL); + + zone_t *zone = knot_zonedb_find(zonedb, name); + if (zone != NULL) { + conf_io_type_t type = conf_io_trie_val(it); + assert(!(type & CONF_IO_TSET)); + zone->change_type = type; + } + } + trie_it_free(it); +} + +static void zone_purge(conf_t *conf, zone_t *zone) +{ + (void)selective_zone_purge(conf, zone, PURGE_ZONE_ALL); +} + +static zone_contents_t *zone_expire(zone_t *zone) +{ + zone->timers.next_expire = time(NULL); + zone->timers.next_refresh = zone->timers.next_expire; + return zone_switch_contents(zone, NULL); +} + +static bool check_open_catalog(catalog_t *cat) +{ + int ret = knot_lmdb_exists(&cat->db); + switch (ret) { + case KNOT_ENODB: + return false; + case KNOT_EOK: + ret = catalog_open(cat); + if (ret == KNOT_EOK) { + return true; + } + // FALLTHROUGH + default: + log_error("failed to open persistent zone catalog"); + } + return false; +} + +static zone_t *reuse_member_zone(zone_t *zone, server_t *server, conf_t *conf, + reload_t mode, list_t *expired_contents) +{ + if (!zone_get_flag(zone, ZONE_IS_CAT_MEMBER, false)) { + return NULL; + } + + catalog_upd_val_t *upd = catalog_update_get(&server->catalog_upd, zone->name); + if (upd != NULL) { + switch (upd->type) { + case CAT_UPD_UNIQ: + zone_purge(conf, zone); + knot_sem_wait(&zone->cow_lock); + ptrlist_add(expired_contents, zone_expire(zone), NULL); + knot_sem_post(&zone->cow_lock); + // FALLTHROUGH + case CAT_UPD_PROP: + zone->change_type = CONF_IO_TRELOAD; + break; // reload the member zone + case CAT_UPD_INVALID: + case CAT_UPD_MINOR: + return zone; // reuse the member zone + case CAT_UPD_REM: + return NULL; // remove the member zone + case CAT_UPD_ADD: // cannot add existing member + default: + assert(0); + return NULL; + } + } else if (mode & (RELOAD_COMMIT | RELOAD_CATALOG)) { + return zone; // reuse the member zone + } + + zone_t *newzone = create_zone(conf, zone->name, server, zone); + if (newzone == NULL) { + log_zone_error(zone->name, "zone cannot be created"); + } else { + assert(zone_get_flag(newzone, ZONE_IS_CAT_MEMBER, false)); + conf_activate_modules(conf, server, newzone->name, &newzone->query_modules, + &newzone->query_plan); + } + + return newzone; +} + +// cold start of knot: add unchanged member zone to zonedb +static zone_t *reuse_cold_zone(const knot_dname_t *zname, server_t *server, conf_t *conf) +{ + catalog_upd_val_t *upd = catalog_update_get(&server->catalog_upd, zname); + if (upd != NULL && upd->type == CAT_UPD_REM) { + return NULL; // zone will be removed immediately + } + + zone_t *zone = create_zone(conf, zname, server, NULL); + if (zone == NULL) { + log_zone_error(zname, "zone cannot be created"); + } else { + zone_set_flag(zone, ZONE_IS_CAT_MEMBER); + conf_activate_modules(conf, server, zone->name, &zone->query_modules, + &zone->query_plan); + } + return zone; +} + +typedef struct { + knot_zonedb_t *zonedb; + server_t *server; + conf_t *conf; +} reuse_cold_zone_ctx_t; + +static int reuse_cold_zone_cb(const knot_dname_t *member, _unused_ const knot_dname_t *owner, + const knot_dname_t *catz, _unused_ const char *group, + void *ctx) +{ + reuse_cold_zone_ctx_t *rcz = ctx; + + zone_t *catz_z = knot_zonedb_find(rcz->zonedb, catz); + if (catz_z == NULL || !(catz_z->flags & ZONE_IS_CATALOG)) { + log_zone_warning(member, "orphaned catalog member zone, ignoring"); + return KNOT_EOK; + } + + zone_t *zone = reuse_cold_zone(member, rcz->server, rcz->conf); + if (zone == NULL) { + return KNOT_ENOMEM; + } + return knot_zonedb_insert(rcz->zonedb, zone); +} + +static zone_t *add_member_zone(catalog_upd_val_t *val, knot_zonedb_t *check, + server_t *server, conf_t *conf) +{ + if (val->type != CAT_UPD_ADD) { + return NULL; + } + + if (knot_zonedb_find(check, val->member) != NULL) { + log_zone_error(val->member, "zone already configured, ignoring"); + return NULL; + } + + zone_t *zone = create_zone(conf, val->member, server, NULL); + if (zone == NULL) { + log_zone_error(val->member, "zone cannot be created"); + } else { + zone_set_flag(zone, ZONE_IS_CAT_MEMBER); + conf_activate_modules(conf, server, zone->name, &zone->query_modules, + &zone->query_plan); + log_zone_info(val->member, "zone added from catalog"); + } + return zone; +} + +/*! + * \brief Create new zone database. + * + * Zones that should be retained are just added from the old database to the + * new. New zones are loaded. + * + * \param conf New server configuration. + * \param server Server instance. + * \param mode Reload mode. + * \param expired_contents Out: ptrlist of zone_contents_t to be deep freed after sync RCU. + * + * \return New zone database. + */ +static knot_zonedb_t *create_zonedb(conf_t *conf, server_t *server, reload_t mode, + list_t *expired_contents) +{ + assert(conf); + assert(server); + + knot_zonedb_t *db_old = server->zone_db; + knot_zonedb_t *db_new = knot_zonedb_new(); + if (!db_new) { + return NULL; + } + + /* Mark changed zones during dynamic configuration. */ + if (mode == RELOAD_COMMIT) { + mark_changed_zones(db_old, conf->io.zones); + } + + /* Process regular zones from the configuration. */ + for (conf_iter_t iter = conf_iter(conf, C_ZONE); iter.code == KNOT_EOK; + conf_iter_next(conf, &iter)) { + conf_val_t id = conf_iter_id(conf, &iter); + const knot_dname_t *name = conf_dname(&id); + + zone_t *old_zone = knot_zonedb_find(db_old, name); + if (old_zone != NULL && (mode & (RELOAD_COMMIT | RELOAD_CATALOG))) { + /* Reuse unchanged zone. */ + if (!(old_zone->change_type & CONF_IO_TRELOAD)) { + knot_zonedb_insert(db_new, old_zone); + continue; + } + } + + zone_t *zone = create_zone(conf, name, server, old_zone); + if (zone == NULL) { + log_zone_error(name, "zone cannot be created"); + continue; + } + + conf_activate_modules(conf, server, zone->name, &zone->query_modules, + &zone->query_plan); + + knot_zonedb_insert(db_new, zone); + } + + /* Purge decataloged zones before catalog removals are commited. */ + catalog_it_t *cat_it = catalog_it_begin(&server->catalog_upd); + while (!catalog_it_finished(cat_it)) { + catalog_upd_val_t *upd = catalog_it_val(cat_it); + if (upd->type == CAT_UPD_REM) { + zone_t *zone = knot_zonedb_find(db_old, upd->member); + if (zone != NULL) { + zone->change_type = CONF_IO_TUNSET; + zone_purge(conf, zone); + } + } + catalog_it_next(cat_it); + } + catalog_it_free(cat_it); + + int ret = catalog_update_commit(&server->catalog_upd, &server->catalog); + if (ret != KNOT_EOK) { + log_error("catalog, failed to apply changes (%s)", knot_strerror(ret)); + return db_new; + } + + /* Process existing catalog member zones. */ + if (db_old != NULL) { + knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_old); + while (!knot_zonedb_iter_finished(it)) { + zone_t *newzone = reuse_member_zone(knot_zonedb_iter_val(it), + server, conf, mode, + expired_contents); + if (newzone != NULL) { + knot_zonedb_insert(db_new, newzone); + } + knot_zonedb_iter_next(it); + } + knot_zonedb_iter_free(it); + } else if (check_open_catalog(&server->catalog)) { + reuse_cold_zone_ctx_t rcz = { db_new, server, conf }; + ret = catalog_apply(&server->catalog, NULL, reuse_cold_zone_cb, &rcz, false); + if (ret != KNOT_EOK) { + log_error("catalog, failed to load member zones (%s)", knot_strerror(ret)); + } + } + + /* Process new catalog member zones. */ + catalog_it_t *it = catalog_it_begin(&server->catalog_upd); + while (!catalog_it_finished(it)) { + catalog_upd_val_t *val = catalog_it_val(it); + zone_t *zone = add_member_zone(val, db_new, server, conf); + if (zone != NULL) { + knot_zonedb_insert(db_new, zone); + } + catalog_it_next(it); + } + catalog_it_free(it); + + return db_new; +} + +/*! + * \brief Schedule deletion of old zones, and free the zone db structure. + * + * \note Zone content may be preserved in the new zone database, in this case + * new and old zone share the contents. Shared content is not freed. + * + * \param conf New server configuration. + * \param db_old Old zone database to remove. + * \param server Server context. + */ +static void remove_old_zonedb(conf_t *conf, knot_zonedb_t *db_old, + server_t *server, reload_t mode) +{ + catalog_commit_cleanup(&server->catalog); + + knot_zonedb_t *db_new = server->zone_db; + + if (db_old == NULL) { + goto catalog_only; + } + + knot_zonedb_iter_t *it = knot_zonedb_iter_begin(db_old); + while (!knot_zonedb_iter_finished(it)) { + zone_t *zone = knot_zonedb_iter_val(it); + if (mode & (RELOAD_FULL | RELOAD_ZONES)) { + /* Check if reloaded (reused contents). */ + zone_t *new_zone = knot_zonedb_find(db_new, zone->name); + if (new_zone != NULL) { + replan_events(conf, new_zone, zone); + zone->contents = NULL; + } + /* Completely new zone. */ + } else { + /* Check if reloaded (reused contents). */ + if (zone->change_type & CONF_IO_TRELOAD) { + zone_t *new_zone = knot_zonedb_find(db_new, zone->name); + assert(new_zone); + replan_events(conf, new_zone, zone); + zone->contents = NULL; + zone_free(&zone); + /* Check if removed (drop also contents). */ + } else if (zone->change_type & CONF_IO_TUNSET) { + zone_free(&zone); + } + /* Completely reused zone. */ + } + knot_zonedb_iter_next(it); + } + knot_zonedb_iter_free(it); + +catalog_only: + + /* Clear catalog changes. No need to use mutex as this is done from main + * thread while all zone events are paused. */ + catalog_update_clear(&server->catalog_upd); + + if (mode & (RELOAD_FULL | RELOAD_ZONES)) { + knot_zonedb_deep_free(&db_old, false); + } else { + knot_zonedb_free(&db_old); + } +} + +void zonedb_reload(conf_t *conf, server_t *server, reload_t mode) +{ + if (conf == NULL || server == NULL) { + return; + } + + if (mode == RELOAD_COMMIT) { + assert(conf->io.flags & CONF_IO_FACTIVE); + if (conf->io.flags & CONF_IO_FRLD_ZONES) { + mode = RELOAD_ZONES; + } + } + + list_t contents_tofree; + init_list(&contents_tofree); + + catalog_update_finalize(&server->catalog_upd, &server->catalog, conf); + size_t cat_upd_size = trie_weight(server->catalog_upd.upd); + if (cat_upd_size > 0) { + log_info("catalog, updating, %zu changes", cat_upd_size); + } + + /* Insert all required zones to the new zone DB. */ + knot_zonedb_t *db_new = create_zonedb(conf, server, mode, &contents_tofree); + if (db_new == NULL) { + log_error("failed to create new zone database"); + return; + } + + catalogs_generate(db_new, server->zone_db); + + /* Switch the databases. */ + knot_zonedb_t **db_current = &server->zone_db; + knot_zonedb_t *db_old = rcu_xchg_pointer(db_current, db_new); + + /* Wait for readers to finish reading old zone database. */ + synchronize_rcu(); + + ptrlist_free_custom(&contents_tofree, NULL, (ptrlist_free_cb)zone_contents_deep_free); + + /* Remove old zone DB. */ + remove_old_zonedb(conf, db_old, server, mode); +} + +int zone_reload_modules(conf_t *conf, server_t *server, const knot_dname_t *zone_name) +{ + zone_t **zone = knot_zonedb_find_ptr(server->zone_db, zone_name); + if (zone == NULL) { + return KNOT_ENOENT; + } + assert(knot_dname_is_equal((*zone)->name, zone_name)); + + zone_events_freeze_blocking(*zone); + knot_sem_wait(&(*zone)->cow_lock); + + zone_t *newzone = create_zone(conf, zone_name, server, *zone); + if (newzone == NULL) { + return KNOT_ENOMEM; + } + conf_activate_modules(conf, server, newzone->name, &newzone->query_modules, + &newzone->query_plan); + + zone_t *oldzone = rcu_xchg_pointer(zone, newzone); + synchronize_rcu(); + + replan_events(conf, newzone, oldzone); + + assert(newzone->contents == oldzone->contents); + oldzone->contents = NULL; // contents have been re-used by newzone + + knot_sem_post(&oldzone->cow_lock); + zone_free(&oldzone); + + return KNOT_EOK; +} diff --git a/src/knot/zone/zonedb-load.h b/src/knot/zone/zonedb-load.h new file mode 100644 index 0000000..c69b831 --- /dev/null +++ b/src/knot/zone/zonedb-load.h @@ -0,0 +1,40 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/conf/conf.h" +#include "knot/server/server.h" + +/*! + * \brief Update zone database according to configuration. + * + * \param conf Configuration. + * \param server Server instance. + * \param mode Reload mode. + */ +void zonedb_reload(conf_t *conf, server_t *server, reload_t mode); + +/*! + * \brief Re-create zone_t struct in zoneDB so that the zone is reloaded incl modules. + * + * \param conf Configuration. + * \param server Server instance. + * \param zone_name Name of zone to be reloaded. + * + * \return KNOT_E* + */ +int zone_reload_modules(conf_t *conf, server_t *server, const knot_dname_t *zone_name); diff --git a/src/knot/zone/zonedb.c b/src/knot/zone/zonedb.c new file mode 100644 index 0000000..98cade5 --- /dev/null +++ b/src/knot/zone/zonedb.c @@ -0,0 +1,188 @@ +/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <stdlib.h> + +#include "knot/journal/journal_metadata.h" +#include "knot/zone/zonedb.h" +#include "libknot/packet/wire.h" +#include "contrib/mempattern.h" +#include "contrib/ucw/mempool.h" + +/*! \brief Discard zone in zone database. */ +static void discard_zone(zone_t *zone, bool abort_txn) +{ + // Don't flush if removed zone (no previous configuration available). + if (conf_rawid_exists(conf(), C_ZONE, zone->name, knot_dname_size(zone->name)) || + catalog_has_member(conf()->catalog, zone->name)) { + uint32_t journal_serial, zone_serial = zone_contents_serial(zone->contents); + bool exists; + + // Flush if bootstrapped or if the journal doesn't exist. + if (!zone->zonefile.exists || journal_info( + zone_journal(zone), &exists, NULL, NULL, &journal_serial, NULL, NULL, NULL, NULL + ) != KNOT_EOK || !exists || journal_serial != zone_serial) { + zone_flush_journal(conf(), zone, false); + } + } + + if (abort_txn) { + zone_control_clear(zone); + } + zone_free(&zone); +} + +knot_zonedb_t *knot_zonedb_new(void) +{ + knot_zonedb_t *db = calloc(1, sizeof(knot_zonedb_t)); + if (db == NULL) { + return NULL; + } + + mm_ctx_mempool(&db->mm, MM_DEFAULT_BLKSIZE); + + db->trie = trie_create(&db->mm); + if (db->trie == NULL) { + mp_delete(db->mm.ctx); + free(db); + return NULL; + } + + return db; +} + +int knot_zonedb_insert(knot_zonedb_t *db, zone_t *zone) +{ + if (db == NULL || zone == NULL) { + return KNOT_EINVAL; + } + + assert(zone->name); + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(zone->name, lf_storage); + assert(lf); + + *trie_get_ins(db->trie, lf + 1, *lf) = zone; + + return KNOT_EOK; +} + +int knot_zonedb_del(knot_zonedb_t *db, const knot_dname_t *zone_name) +{ + if (db == NULL || zone_name == NULL) { + return KNOT_EINVAL; + } + + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(zone_name, lf_storage); + assert(lf); + + trie_val_t *rval = trie_get_try(db->trie, lf + 1, *lf); + if (rval == NULL) { + return KNOT_ENOENT; + } + + return trie_del(db->trie, lf + 1, *lf, NULL); +} + +zone_t *knot_zonedb_find(knot_zonedb_t *db, const knot_dname_t *zone_name) +{ + if (db == NULL) { + return NULL; + } + + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(zone_name, lf_storage); + assert(lf); + + trie_val_t *val = trie_get_try(db->trie, lf + 1, *lf); + if (val == NULL) { + return NULL; + } + + return *val; +} + +zone_t **knot_zonedb_find_ptr(knot_zonedb_t *db, const knot_dname_t *zone_name) +{ + if (db == NULL) { + return NULL; + } + + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(zone_name, lf_storage); + assert(lf); + + trie_val_t *val = trie_get_try(db->trie, lf + 1, *lf); + if (val == NULL) { + return NULL; + } + + return (zone_t **)val; +} + +zone_t *knot_zonedb_find_suffix(knot_zonedb_t *db, const knot_dname_t *zone_name) +{ + if (db == NULL || zone_name == NULL) { + return NULL; + } + + while (true) { + knot_dname_storage_t lf_storage; + uint8_t *lf = knot_dname_lf(zone_name, lf_storage); + assert(lf); + + trie_val_t *val = trie_get_try(db->trie, lf + 1, *lf); + if (val != NULL) { + return *val; + } else if (zone_name[0] == 0) { + return NULL; + } + + zone_name = knot_wire_next_label(zone_name, NULL); + } +} + +size_t knot_zonedb_size(const knot_zonedb_t *db) +{ + if (db == NULL) { + return 0; + } + + return trie_weight(db->trie); +} + +void knot_zonedb_free(knot_zonedb_t **db) +{ + if (db == NULL || *db == NULL) { + return; + } + + mp_delete((*db)->mm.ctx); + free(*db); + *db = NULL; +} + +void knot_zonedb_deep_free(knot_zonedb_t **db, bool abort_txn) +{ + if (db == NULL || *db == NULL) { + return; + } + + knot_zonedb_foreach(*db, discard_zone, abort_txn); + knot_zonedb_free(db); +} diff --git a/src/knot/zone/zonedb.h b/src/knot/zone/zonedb.h new file mode 100644 index 0000000..de934d5 --- /dev/null +++ b/src/knot/zone/zonedb.h @@ -0,0 +1,135 @@ +/* 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/>. + */ + +/*! + * \file + * + * \brief Zone database represents a list of managed zones. + */ + +#pragma once + +#include "knot/zone/zone.h" +#include "libknot/dname.h" +#include "contrib/qp-trie/trie.h" + +struct knot_zonedb { + trie_t *trie; + knot_mm_t mm; +}; + +/* + * Mapping of iterators to internal data structure. + */ +typedef trie_it_t knot_zonedb_iter_t; +#define knot_zonedb_iter_begin(db) trie_it_begin((db)->trie) +#define knot_zonedb_iter_finished(it) trie_it_finished(it) +#define knot_zonedb_iter_next(it) trie_it_next(it) +#define knot_zonedb_iter_free(it) trie_it_free(it) +#define knot_zonedb_iter_val(it) *trie_it_val(it) + +/* + * Simple foreach() access with callback and variable number of callback params. + */ +#define knot_zonedb_foreach(db, callback, ...) \ +{ \ + knot_zonedb_iter_t *it = knot_zonedb_iter_begin((db)); \ + while(!knot_zonedb_iter_finished(it)) { \ + callback((zone_t *)knot_zonedb_iter_val(it), ##__VA_ARGS__); \ + knot_zonedb_iter_next(it); \ + } \ + knot_zonedb_iter_free(it); \ +} + +/*! + * \brief Allocates and initializes the zone database structure. + * + * \return Pointer to the created zone database structure or NULL if an error + * occurred. + */ +knot_zonedb_t *knot_zonedb_new(void); + +/*! + * \brief Adds new zone to the database. + * + * \param db Zone database to store the zone. + * \param zone Parsed zone. + * + * \retval KNOT_EOK + * \retval KNOT_EZONEIN + */ +int knot_zonedb_insert(knot_zonedb_t *db, zone_t *zone); + +/*! + * \brief Removes the given zone from the database if it exists. + * + * \param db Zone database to remove from. + * \param zone_name Name of the zone to be removed. + * + * \retval KNOT_EOK + * \retval KNOT_ENOZONE + */ +int knot_zonedb_del(knot_zonedb_t *db, const knot_dname_t *zone_name); + +/*! + * \brief Finds zone exactly matching the given zone name. + * + * \param db Zone database to search in. + * \param zone_name Domain name representing the zone name. + * + * \return Zone with \a zone_name being the owner of the zone apex or NULL if + * not found. + */ +zone_t *knot_zonedb_find(knot_zonedb_t *db, const knot_dname_t *zone_name); + +/*! + * \brief Finds pointer to zone exactly matching the given zone name. + * + * \param db Zone database to search in. + * \param zone_name Domain name representing the zone name. + * + * \return Pointer in zoneDB pointing at the zone structure, or NULL. + */ +zone_t **knot_zonedb_find_ptr(knot_zonedb_t *db, const knot_dname_t *zone_name); + +/*! + * \brief Finds zone the given domain name should belong to. + * + * \param db Zone database to search in. + * \param zone_name Domain name to find zone for. + * + * \retval Zone in which the domain name should be present or NULL if no such + * zone is found. + */ +zone_t *knot_zonedb_find_suffix(knot_zonedb_t *db, const knot_dname_t *zone_name); + +size_t knot_zonedb_size(const knot_zonedb_t *db); + +/*! + * \brief Destroys and deallocates the zone database structure (but not the + * zones within). + * + * \param db Zone database to be destroyed. + */ +void knot_zonedb_free(knot_zonedb_t **db); + +/*! + * \brief Destroys and deallocates the whole zone database including the zones. + * + * \param db Zone database to be destroyed. + * \param abort_txn Indication that possible zone transactions are aborted. + */ +void knot_zonedb_deep_free(knot_zonedb_t **db, bool abort_txn); diff --git a/src/knot/zone/zonefile.c b/src/knot/zone/zonefile.c new file mode 100644 index 0000000..e545497 --- /dev/null +++ b/src/knot/zone/zonefile.c @@ -0,0 +1,371 @@ +/* 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 <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <time.h> +#include <unistd.h> +#include <inttypes.h> + +#include "libknot/libknot.h" +#include "contrib/files.h" +#include "knot/common/log.h" +#include "knot/dnssec/zone-nsec.h" +#include "knot/zone/semantic-check.h" +#include "knot/zone/adjust.h" +#include "knot/zone/contents.h" +#include "knot/zone/zonefile.h" +#include "knot/zone/zone-dump.h" + +#define ERROR(zone, fmt, ...) log_zone_error(zone, "zone loader, " fmt, ##__VA_ARGS__) +#define WARNING(zone, fmt, ...) log_zone_warning(zone, "zone loader, " fmt, ##__VA_ARGS__) +#define NOTICE(zone, fmt, ...) log_zone_notice(zone, "zone loader, " fmt, ##__VA_ARGS__) + +static void process_error(zs_scanner_t *s) +{ + zcreator_t *zc = s->process.data; + const knot_dname_t *zname = zc->z->apex->owner; + + ERROR(zname, "%s in zone, file '%s', line %"PRIu64" (%s)", + s->error.fatal ? "fatal error" : "error", + s->file.name, s->line_counter, + zs_strerror(s->error.code)); +} + +static bool handle_err(zcreator_t *zc, const knot_rrset_t *rr, int ret, bool master) +{ + const knot_dname_t *zname = zc->z->apex->owner; + + knot_dname_txt_storage_t buff; + char *owner = knot_dname_to_str(buff, rr->owner, sizeof(buff)); + if (owner == NULL) { + owner = ""; + } + + if (ret == KNOT_EOUTOFZONE) { + WARNING(zname, "ignoring out-of-zone data, owner %s", owner); + return true; + } else if (ret == KNOT_ETTL) { + char type[16] = ""; + knot_rrtype_to_string(rr->type, type, sizeof(type)); + NOTICE(zname, "TTL mismatch, owner %s, type %s, TTL set to %u", + owner, type, rr->ttl); + return true; + } else { + ERROR(zname, "failed to process record, owner %s", owner); + return false; + } +} + +int zcreator_step(zcreator_t *zc, const knot_rrset_t *rr) +{ + if (zc == NULL || rr == NULL || rr->rrs.count != 1) { + return KNOT_EINVAL; + } + + if (rr->type == KNOT_RRTYPE_SOA && + node_rrtype_exists(zc->z->apex, KNOT_RRTYPE_SOA)) { + // Ignore extra SOA + return KNOT_EOK; + } + + zone_node_t *node = NULL; + int ret = zone_contents_add_rr(zc->z, rr, &node); + if (ret != KNOT_EOK) { + if (!handle_err(zc, rr, ret, zc->master)) { + // Fatal error + return ret; + } + } + + return KNOT_EOK; +} + +/*! \brief Creates RR from parser input, passes it to handling function. */ +static void process_data(zs_scanner_t *scanner) +{ + zcreator_t *zc = scanner->process.data; + if (zc->ret != KNOT_EOK) { + scanner->state = ZS_STATE_STOP; + return; + } + + knot_dname_t *owner = knot_dname_copy(scanner->r_owner, NULL); + if (owner == NULL) { + zc->ret = KNOT_ENOMEM; + return; + } + + knot_rrset_t rr; + knot_rrset_init(&rr, owner, scanner->r_type, scanner->r_class, scanner->r_ttl); + + int ret = knot_rrset_add_rdata(&rr, scanner->r_data, scanner->r_data_length, NULL); + if (ret != KNOT_EOK) { + knot_rrset_clear(&rr, NULL); + zc->ret = ret; + return; + } + + /* Convert RDATA dnames to lowercase before adding to zone. */ + ret = knot_rrset_rr_to_canonical(&rr); + if (ret != KNOT_EOK) { + knot_rrset_clear(&rr, NULL); + zc->ret = ret; + return; + } + + zc->ret = zcreator_step(zc, &rr); + knot_rrset_clear(&rr, NULL); +} + +int zonefile_open(zloader_t *loader, const char *source, + const knot_dname_t *origin, semcheck_optional_t semantic_checks, time_t time) +{ + if (!loader) { + return KNOT_EINVAL; + } + + memset(loader, 0, sizeof(zloader_t)); + + /* Check zone file. */ + if (access(source, F_OK | R_OK) != 0) { + return knot_map_errno(); + } + + /* Create context. */ + zcreator_t *zc = malloc(sizeof(zcreator_t)); + if (zc == NULL) { + return KNOT_ENOMEM; + } + memset(zc, 0, sizeof(zcreator_t)); + + zc->z = zone_contents_new(origin, true); + if (zc->z == NULL) { + free(zc); + return KNOT_ENOMEM; + } + + /* Prepare textual owner for zone scanner. */ + char *origin_str = knot_dname_to_str_alloc(origin); + if (origin_str == NULL) { + zone_contents_deep_free(zc->z); + free(zc); + return KNOT_ENOMEM; + } + + if (zs_init(&loader->scanner, origin_str, KNOT_CLASS_IN, 3600) != 0 || + zs_set_input_file(&loader->scanner, source) != 0 || + zs_set_processing(&loader->scanner, process_data, process_error, zc) != 0) { + zs_deinit(&loader->scanner); + free(origin_str); + zone_contents_deep_free(zc->z); + free(zc); + return KNOT_EFILE; + } + free(origin_str); + + loader->source = strdup(source); + loader->creator = zc; + loader->semantic_checks = semantic_checks; + loader->time = time; + + return KNOT_EOK; +} + +zone_contents_t *zonefile_load(zloader_t *loader) +{ + if (!loader) { + return NULL; + } + + zcreator_t *zc = loader->creator; + const knot_dname_t *zname = zc->z->apex->owner; + + assert(zc); + int ret = zs_parse_all(&loader->scanner); + if (ret != 0 && loader->scanner.error.counter == 0) { + ERROR(zname, "failed to load zone, file '%s' (%s)", + loader->source, zs_strerror(loader->scanner.error.code)); + goto fail; + } + + if (zc->ret != KNOT_EOK) { + ERROR(zname, "failed to load zone, file '%s' (%s)", + loader->source, knot_strerror(zc->ret)); + goto fail; + } + + if (loader->scanner.error.counter > 0) { + ERROR(zname, "failed to load zone, file '%s', %"PRIu64" errors", + loader->source, loader->scanner.error.counter); + goto fail; + } + + if (!node_rrtype_exists(loader->creator->z->apex, KNOT_RRTYPE_SOA)) { + loader->err_handler->error = true; + loader->err_handler->cb(loader->err_handler, zc->z, NULL, + SEM_ERR_SOA_NONE, NULL); + goto fail; + } + + ret = zone_adjust_contents(zc->z, adjust_cb_flags_and_nsec3, adjust_cb_nsec3_flags, + true, true, 1, NULL); + if (ret != KNOT_EOK) { + ERROR(zname, "failed to finalize zone contents (%s)", + knot_strerror(ret)); + goto fail; + } + + ret = sem_checks_process(zc->z, loader->semantic_checks, + loader->err_handler, loader->time); + + if (ret != KNOT_EOK) { + ERROR(zname, "failed to load zone, file '%s' (%s)", + loader->source, knot_strerror(ret)); + goto fail; + } + + /* The contents will now change possibly messing up NSEC3 tree, it will + be adjusted again at zone_update_commit. */ + ret = zone_adjust_contents(zc->z, unadjust_cb_point_to_nsec3, NULL, + false, false, 1, NULL); + if (ret != KNOT_EOK) { + ERROR(zname, "failed to finalize zone contents (%s)", + knot_strerror(ret)); + goto fail; + } + + return zc->z; + +fail: + zone_contents_deep_free(zc->z); + return NULL; +} + +int zonefile_exists(const char *path, struct timespec *mtime) +{ + if (path == NULL) { + return KNOT_EINVAL; + } + + struct stat zonefile_st = { 0 }; + if (stat(path, &zonefile_st) < 0) { + return knot_map_errno(); + } + + if (mtime != NULL) { + *mtime = zonefile_st.st_mtim; + } + + return KNOT_EOK; +} + +int zonefile_write(const char *path, zone_contents_t *zone) +{ + if (path == NULL) { + return KNOT_EINVAL; + } + + if (zone == NULL) { + return KNOT_EEMPTYZONE; + } + + int ret = make_path(path, S_IRUSR|S_IWUSR|S_IXUSR|S_IRGRP|S_IWGRP|S_IXGRP); + if (ret != KNOT_EOK) { + return ret; + } + + FILE *file = NULL; + char *tmp_name = NULL; + ret = open_tmp_file(path, &tmp_name, &file, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP); + if (ret != KNOT_EOK) { + return ret; + } + + ret = zone_dump_text(zone, file, true, NULL); + fclose(file); + if (ret != KNOT_EOK) { + unlink(tmp_name); + free(tmp_name); + return ret; + } + + /* Swap temporary zonefile and new zonefile. */ + ret = rename(tmp_name, path); + if (ret != 0) { + ret = knot_map_errno(); + unlink(tmp_name); + free(tmp_name); + return ret; + } + + free(tmp_name); + + return KNOT_EOK; +} + +void zonefile_close(zloader_t *loader) +{ + if (!loader) { + return; + } + + zs_deinit(&loader->scanner); + free(loader->source); + free(loader->creator); +} + +void err_handler_logger(sem_handler_t *handler, const zone_contents_t *zone, + const knot_dname_t *node, sem_error_t error, const char *data) +{ + assert(handler != NULL); + assert(zone != NULL); + + if (handler->error) { + handler->fatal_error = true; + } else { + handler->warning = true; + } + + knot_dname_txt_storage_t owner; + if (node != NULL) { + if (knot_dname_to_str(owner, node, sizeof(owner)) == NULL) { + owner[0] = '\0'; + } + } + + int level = handler->soft_check ? LOG_NOTICE : + (handler->error ? LOG_ERR : LOG_WARNING); + + log_fmt_zone(level, LOG_SOURCE_ZONE, zone->apex->owner, NULL, + "check%s%s, %s%s%s", + (node != NULL ? ", node " : ""), + (node != NULL ? owner : ""), + sem_error_msg(error), + (data != NULL ? " " : ""), + (data != NULL ? data : "")); + + handler->error = false; +} + +#undef ERROR +#undef WARNING +#undef NOTICE diff --git a/src/knot/zone/zonefile.h b/src/knot/zone/zonefile.h new file mode 100644 index 0000000..c8dbfad --- /dev/null +++ b/src/knot/zone/zonefile.h @@ -0,0 +1,104 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdbool.h> +#include <stdio.h> + +#include "knot/zone/zone.h" +#include "knot/zone/semantic-check.h" +#include "libzscanner/scanner.h" +/*! + * \brief Zone creator structure. + */ +typedef struct zcreator { + zone_contents_t *z; /*!< Created zone. */ + bool master; /*!< True if server is a primary master for the zone. */ + int ret; /*!< Return value. */ +} zcreator_t; + +/*! + * \brief Zone loader structure. + */ +typedef struct { + char *source; /*!< Zone source file. */ + semcheck_optional_t semantic_checks; /*!< Do semantic checks. */ + sem_handler_t *err_handler; /*!< Semantic checks error handler. */ + zcreator_t *creator; /*!< Loader context. */ + zs_scanner_t scanner; /*!< Zone scanner. */ + time_t time; /*!< time for zone check. */ +} zloader_t; + +void err_handler_logger(sem_handler_t *handler, const zone_contents_t *zone, + const knot_dname_t *node, sem_error_t error, const char *data); + +/*! + * \brief Open zone file for loading. + * + * \param loader Output zone loader. + * \param source Source file name. + * \param origin Zone origin. + * \param semantic_checks Perform semantic checks. + * \param time Time for semantic check. + * + * \retval Initialized loader on success. + * \retval NULL on error. + */ +int zonefile_open(zloader_t *loader, const char *source, + const knot_dname_t *origin, semcheck_optional_t semantic_checks, time_t time); + +/*! + * \brief Loads zone from a zone file. + * + * \param loader Zone loader instance. + * + * \retval Loaded zone contents on success. + * \retval NULL otherwise. + */ +zone_contents_t *zonefile_load(zloader_t *loader); + +/*! + * \brief Checks if zonefile exists. + * + * \param path Zonefile path. + * \param mtime Zonefile mtime if exists (can be NULL). + * + * \return KNOT_E* + */ +int zonefile_exists(const char *path, struct timespec *mtime); + +/*! + * \brief Write zone contents to zone file. + */ +int zonefile_write(const char *path, zone_contents_t *zone); + +/*! + * \brief Close zone file loader. + * + * \param loader Zone loader instance. + */ +void zonefile_close(zloader_t *loader); + +/*! + * \brief Adds one RR into zone. + * + * \param zl Zone loader. + * \param rr RR to add. + * + * \return KNOT_E* + */ +int zcreator_step(zcreator_t *zl, const knot_rrset_t *rr); diff --git a/src/knotd.pc.in b/src/knotd.pc.in new file mode 100644 index 0000000..6db74ca --- /dev/null +++ b/src/knotd.pc.in @@ -0,0 +1,9 @@ +prefix=@prefix@ +exec_prefix=@prefix@ +libdir=@libdir@ +module_instdir=@module_instdir@ + +Name: knotd +Description: Knot DNS daemon +URL: https://www.knot-dns.cz +Version: @PACKAGE_VERSION@ |