diff options
Diffstat (limited to 'src/knot/zone')
40 files changed, 10483 insertions, 0 deletions
diff --git a/src/knot/zone/adds_tree.c b/src/knot/zone/adds_tree.c new file mode 100644 index 0000000..5a3a9fa --- /dev/null +++ b/src/knot/zone/adds_tree.c @@ -0,0 +1,320 @@ +/* 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 "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; +} + +static int reverse_apply_nodes(a_t_node_t *nodes, node_apply_cb_t cb, void *ctx) +{ + 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(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; + } + + return reverse_apply_nodes(*(a_t_node_t **)val, cb, ctx); +} + +static bool key_in_bailiwick(const uint8_t *root_key, size_t root_len, + const uint8_t *sub_key, size_t sub_len) +{ + return sub_len >= root_len && + memcmp(sub_key, root_key, root_len) == 0; +} + +static bool it_in_bailiwick(trie_it_t *it, const uint8_t *root_key, size_t root_len) +{ + size_t cur_len = 0; + const uint8_t *cur_key = trie_it_key(it, &cur_len); + return key_in_bailiwick(root_key, root_len, cur_key, cur_len); +} + +static int reverse_apply_subtree(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_it_t *it = trie_it_begin(a_t); + if (it == NULL) { + return KNOT_ENOMEM; + } + int ret = trie_it_get_leq(it, lf + 1, *lf); + if (ret == KNOT_ENOENT) { + trie_it_free(it); + it = trie_it_begin(a_t); // no node "less or equal", start at the beginning + assert(it != NULL); + ret = KNOT_EOK; + } else if (ret == KNOT_EOK || ret == 1) { + trie_it_next(it); // skip name itself, only iterate on subtree + ret = KNOT_EOK; + } else { + trie_it_free(it); + return ret; + } + + while (!trie_it_finished(it) && it_in_bailiwick(it, lf + 1, *lf) && ret == KNOT_EOK) { + trie_val_t *val = trie_it_val(it); + ret = reverse_apply_nodes(*(a_t_node_t **)val, cb, ctx); + trie_it_next(it); + } + trie_it_free(it); + + return ret; +} + +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) { + const knot_dname_t *owner = zone_tree_it_val(&it)->owner; + if (knot_dname_is_wildcard(owner)) { + // this skips the subtree root, but includes the wildcard node itself as it's part of the subtree + ret = reverse_apply_subtree(a_t, owner + 2 /* strip wildcard label */, cb, ctx); + } else { + ret = additionals_reverse_apply(a_t, 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..fb96e0a --- /dev/null +++ b/src/knot/zone/adjust.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 "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; + node_update_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..bfe977b --- /dev/null +++ b/src/knot/zone/backup.c @@ -0,0 +1,616 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + 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/ctl/commands.h" +#include "knot/dnssec/kasp/kasp_zone.h" +#include "knot/dnssec/kasp/keystore.h" +#include "knot/journal/journal_metadata.h" +#include "knot/server/server.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. + +const backup_filter_list_t backup_filters[] = { + { "zonefile", BACKUP_PARAM_ZONEFILE, CTL_FILTER_BACKUP_ZONEFILE, CTL_FILTER_BACKUP_NOZONEFILE }, + { "journal", BACKUP_PARAM_JOURNAL, CTL_FILTER_BACKUP_JOURNAL, CTL_FILTER_BACKUP_NOJOURNAL }, + { "timers", BACKUP_PARAM_TIMERS, CTL_FILTER_BACKUP_TIMERS, CTL_FILTER_BACKUP_NOTIMERS }, + { "kaspdb", BACKUP_PARAM_KASPDB, CTL_FILTER_BACKUP_KASPDB, CTL_FILTER_BACKUP_NOKASPDB }, + { "keysonly", BACKUP_PARAM_KEYSONLY, CTL_FILTER_BACKUP_KEYSONLY, CTL_FILTER_BACKUP_NOKEYSONLY }, + { "catalog", BACKUP_PARAM_CATALOG, CTL_FILTER_BACKUP_CATALOG, CTL_FILTER_BACKUP_NOCATALOG }, + { "quic", BACKUP_PARAM_QUIC, CTL_FILTER_BACKUP_QUIC, CTL_FILTER_BACKUP_NOQUIC }, + { NULL }, +}; + +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)) + +#define MISSING_FROM_BACKUP(request, stored) (((request) ^ (stored)) & (request)) + +int zone_backup_init(bool restore_mode, knot_backup_params_t filters, 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->backup_params = filters; + ctx->in_backup = 0; // Just to be sure. + 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 and available data. + int ret = backupdir_init(ctx); + if (ret != KNOT_EOK) { + free(ctx); + return ret; + } + + // For restore, check that there are all required data components in the backup. + if (restore_mode) { + // '+kaspdb' in backup provides data also for '+keysonly' restore. + knot_backup_params_t available = ctx->in_backup | + ((bool)(ctx->in_backup & BACKUP_PARAM_KASPDB) * BACKUP_PARAM_KEYSONLY); + + filters = MISSING_FROM_BACKUP(filters, available); + if (filters) { + // ctx needed for logging, will be freed later. + *out_ctx = ctx; + ctx->backup_params = filters; + return KNOT_EBACKUPDATA; + } + } + + 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; +} + +static int backup_file(char *dst, char *src) +{ + struct stat st; + int ret; + + if (stat(src, &st) == 0) { + ret = make_path(dst, S_IRWXU | S_IRWXG); + if (ret == KNOT_EOK) { + ret = copy_file(dst, src); + } + } else { + ret = knot_map_errno(); + // If there's no src file, remove any old dst file. + if (ret == KNOT_ENOENT) { + unlink(dst); + } + } + + return ret; +} + +#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) { + ret = backup_file(local_zf, backup_zf); + ret = ret == KNOT_ENOENT ? KNOT_EFILE : ret; + + } 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_notice(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; +} + +static int backup_kaspdb(zone_backup_ctx_t *ctx, conf_t *conf, zone_t *zone, + int (*kaspdb_backup_cb)(const knot_dname_t *zone, + knot_lmdb_db_t *db, knot_lmdb_db_t *backup_db)) +{ + 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) { + int ret = kaspdb_backup_cb(zone->name, kasp_from, kasp_to); + if (ret != KNOT_EOK) { + LOG_MARK_FAIL("KASP database"); + return ret; + } + + ret = backup_keystore(conf, zone, ctx); + if (ret != KNOT_EOK) { + // Errors already logged in detail. + ctx->failed = true; + return ret; + } + } + + return KNOT_EOK; +} + +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; + + if (ctx->backup_params & BACKUP_PARAM_ZONEFILE) { + ret = backup_zonefile(conf, zone, ctx); + if (ret != KNOT_EOK) { + LOG_MARK_FAIL("zone file"); + return ret; + } + } + + if (ctx->backup_params & BACKUP_PARAM_KASPDB) { + ret = backup_kaspdb(ctx, conf, zone, kasp_db_backup); + if (ret != KNOT_EOK) { + // Errors already logged in detail. + return ret; + } + } + + if (ctx->backup_params & BACKUP_PARAM_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_params & BACKUP_PARAM_ZONEFILE)) { + ret = journal_scrape_with_md(zone_journal(zone), true); + } + if (ret != KNOT_EOK) { + LOG_MARK_FAIL("journal"); + return ret; + } + + if (ctx->backup_params & BACKUP_PARAM_TIMERS) { + ret = knot_lmdb_open(&ctx->bck_timer_db); + if (ret != KNOT_EOK) { + LOG_MARK_FAIL("timers open"); + return ret; + } + 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"); + return ret; + } + } + + return ret; +} + +int global_backup(zone_backup_ctx_t *ctx, catalog_t *catalog, + const knot_dname_t *zone_only) +{ + if (!(ctx->backup_params & BACKUP_PARAM_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; +} + +int zone_backup_keysonly(zone_backup_ctx_t *ctx, conf_t *conf, zone_t *zone) +{ + return backup_kaspdb(ctx, conf, zone, kasp_db_backup_keys); +} + +static int backup_quic_file(zone_backup_ctx_t *ctx, char *file, char *subdir, + const char *desc, bool required, bool *success) +{ + char *backup_quic_dir = NULL, *backup_orig = NULL, *backup; + int ret; + + backup_quic_dir = dir_file(ctx->backup_dir, subdir); + ABORT_IF_ENOMEM(backup_quic_dir); + backup_orig = backup = dir_file(backup_quic_dir, file); + ABORT_IF_ENOMEM(backup); + + BACKUP_SWAP(ctx, backup, file); + ret = backup_file(backup, file); + if (ret == KNOT_EOK) { + *success = true; + } else if (!required && ret == KNOT_ENOENT) { + ret = KNOT_EOK; + } else { + log_ctl_error("control, QUIC %s file %s failed (%s)", desc, + ctx->restore_mode ? "restore" : "backup", + knot_strerror(ret)); + } +done: + free(backup_orig); + free(backup_quic_dir); + return ret; +} + +#define DONE_ON_ERROR if (ret != KNOT_EOK) { \ + goto done; \ + } + +int backup_quic(zone_backup_ctx_t *ctx, bool quic_on) +{ + if (!(ctx->backup_params & BACKUP_PARAM_QUIC)) { + return KNOT_EOK; + } + + const char *str_auto = "auto-generated key"; + const char *str_key = "configured key"; + const char *str_cert = "certificate"; + + bool log_auto = false; + bool log_key = false; + bool log_cert = false; + int ret; + + char *cert_file = conf_tls(conf(), C_CERT_FILE); + char *key_file = conf_tls(conf(), C_KEY_FILE); + bool user_keys = (key_file != NULL); + + char *kasp_dir = conf_db(conf(), C_KASP_DB); + char *auto_file = abs_path(DFLT_QUIC_KEY_FILE, kasp_dir); + free(kasp_dir); + ABORT_IF_ENOMEM(auto_file); + + // Backup/restore of auto-generated key is required if it's in active use, + // otherwise use it if the file is found (no fail if missing). + ret = backup_quic_file(ctx, auto_file, "keys", str_auto, + quic_on && !user_keys, &log_auto); + DONE_ON_ERROR; + + // If QUIC isn't configured, backup of configured key and cert is possible, + // but it isn't required (no fail if missing). + if (user_keys && !ctx->restore_mode) { + char *quic_subdir = "quic"; + ret = backup_quic_file(ctx, key_file, quic_subdir, str_key, + quic_on, &log_key); + DONE_ON_ERROR; + + ret = backup_quic_file(ctx, cert_file, quic_subdir, str_cert, + quic_on, &log_cert); + DONE_ON_ERROR; + } + + if (log_auto || log_key) { + log_ctl_info("control, QUIC %s%s%s%s%s %s '%s'", + log_auto ? str_auto : "", + (log_auto && log_key) ? ", " : "", + log_key ? str_key : "", + log_cert ? " and " : "", + log_cert ? str_cert : "", + ctx->restore_mode ? "restored from" : "backed up to", + ctx->backup_dir); + } + +done: + free(auto_file); + free(key_file); + free(cert_file); + + 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..d3ae07d --- /dev/null +++ b/src/knot/zone/backup.h @@ -0,0 +1,109 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + 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; + +/*! \bref Backup components list. */ +typedef enum { + BACKUP_PARAM_ZONEFILE = 1 << 0, // backup zone contents to a zonefile + BACKUP_PARAM_JOURNAL = 1 << 1, // backup journal + BACKUP_PARAM_TIMERS = 1 << 2, // backup timers + BACKUP_PARAM_KASPDB = 1 << 3, // backup KASP database (incl. keys) + BACKUP_PARAM_KEYSONLY = 1 << 4, // backup keys (without KASP db) + BACKUP_PARAM_CATALOG = 1 << 5, // backup zone catalog + BACKUP_PARAM_QUIC = 1 << 6, // backup QUIC server key and certificate +} knot_backup_params_t; + +/*! \bref Default set of components for backup. */ +#define BACKUP_PARAM_DFLT_B (BACKUP_PARAM_ZONEFILE | BACKUP_PARAM_TIMERS | \ + BACKUP_PARAM_KASPDB | BACKUP_PARAM_CATALOG | \ + BACKUP_PARAM_QUIC) + +/*! \bref Default set of components for restore. */ +#define BACKUP_PARAM_DFLT_R (BACKUP_PARAM_ZONEFILE | BACKUP_PARAM_TIMERS | \ + BACKUP_PARAM_KASPDB | BACKUP_PARAM_CATALOG) + +/*! \bref Backup components done in event. */ +#define BACKUP_PARAM_EVENT (BACKUP_PARAM_ZONEFILE | BACKUP_PARAM_JOURNAL | \ + BACKUP_PARAM_TIMERS | BACKUP_PARAM_KASPDB | \ + BACKUP_PARAM_CATALOG) + +typedef struct { + const char *name; + knot_backup_params_t param; + char filter; + char neg_filter; +} backup_filter_list_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 + knot_backup_params_t backup_params; // bit-mapped list of backup components + knot_backup_params_t in_backup; // bit-mapped list of components available in backup + 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; + +extern const backup_filter_list_t backup_filters[]; + +int zone_backup_init(bool restore_mode, knot_backup_params_t filters, 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); +int zone_backup_keysonly(zone_backup_ctx_t *ctx, conf_t *conf, zone_t *zone); + +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); + +int backup_quic(zone_backup_ctx_t *ctx, bool quic_on); diff --git a/src/knot/zone/backup_dir.c b/src/knot/zone/backup_dir.c new file mode 100644 index 0000000..7bf9fd5 --- /dev/null +++ b/src/knot/zone/backup_dir.c @@ -0,0 +1,303 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + 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_PARAMS "parameters: " +#define LABEL_FILE_BACKUPDIR "backupdir " +#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) \ + size_t var_size = path_size(ctx); \ + char var[var_size]; \ + get_full_path(ctx, file, var, var_size); + +#define PARAMS_MAX_LENGTH 128 // At least longest params string without + // '+backupdir' ... (incl. \0) plus 1 for assert(). + +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, size_t full_path_size) +{ + (void)snprintf(full_path, full_path_size, "%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 void print_params(char *buf, knot_backup_params_t params) +{ + int remain = PARAMS_MAX_LENGTH; + for (const backup_filter_list_t *item = backup_filters; + item->name != NULL; item++) { + int n = snprintf(buf, remain, "+%s%s ", + (params & item->param) ? "" : "no", + item->name); + buf += n; + remain -= n; + } + assert(remain > 1); +} + +static knot_backup_params_t parse_params(const char *str) +{ + knot_backup_params_t params = 0; + + // Checking for positive filters only, negative assumed otherwise. + while ((str = strchr(str, '+')) != NULL) { + str++; + for (const backup_filter_list_t *item = backup_filters; + item->name != NULL; item++) { + if (strncmp(str, item->name, + strlen(item->name)) == 0) { + params |= item->param; + break; + } + } + // Avoid getting fooled by the backup directory path. + if (strncmp(str, LABEL_FILE_BACKUPDIR, + sizeof(LABEL_FILE_BACKUPDIR) - 1) == 0) { + break; + } + } + + return params; +} + +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. + const char *ident = conf()->cache.srv_ident; + + // 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. + char params_str[PARAMS_MAX_LENGTH]; + print_params(params_str, ctx->backup_params); + int ret = fprintf(file, + "%s" + LABEL_FILE_FORMAT + "server_identity: %s\n" + "started_time: %s\n" + "finished_time: %s\n" + "knot_version: %s\n" + LABEL_FILE_PARAMS "%s+" LABEL_FILE_BACKUPDIR "%s\n" + "zone_count: %d\n", + label_file_head, + ctx->backup_format, ident, started_time, finished_time, PACKAGE_VERSION, + params_str, 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; + // No contents info available, it's user's responsibility here. + // Set backup components existing in BACKUP_FORMAT_1 only. + ctx->in_backup = BACKUP_PARAM_ZONEFILE | BACKUP_PARAM_JOURNAL | + BACKUP_PARAM_TIMERS | BACKUP_PARAM_KASPDB | + BACKUP_PARAM_CATALOG; + 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 KNOT_EMALF; + } + + 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; + } + + unsigned int remain = 3; // Bit-mapped "punch card" for lines to get data from. + while (remain > 0 && 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; + goto done; + } else if (value <= BACKUP_FORMAT_1) { + ret = KNOT_EMALF; + goto done; + } else { + ctx->backup_format = value; + remain &= ~1; + continue; + } + } + if (strncmp(line, LABEL_FILE_PARAMS, sizeof(LABEL_FILE_PARAMS) - 1) == 0) { + ctx->in_backup = parse_params(line + sizeof(LABEL_FILE_PARAMS) - 1); + remain &= ~2; + } + } + + ret = (remain == 0) ? KNOT_EOK : KNOT_EMALF; + +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; + } + } + + size_t full_path_size = path_size(ctx); + char full_path[full_path_size]; + + // Check for existence of a label file, the backup format used, and available data. + 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, full_path_size); + 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, full_path_size); + 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..8fef21d --- /dev/null +++ b/src/knot/zone/backup_dir.h @@ -0,0 +1,40 @@ +/* Copyright (C) 2024 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + 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 + * and ctx->in_backup. + * + * \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. It does 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..b961f15 --- /dev/null +++ b/src/knot/zone/digest.c @@ -0,0 +1,303 @@ +/* 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 + +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; + } + } + + size_t buf_req = knot_rrset_size(rrset); + if (buf_req > ctx->buf_size) { + uint8_t *newbuf = realloc(ctx->buf, buf_req); + if (newbuf == NULL) { + return KNOT_ENOMEM; + } + ctx->buf = newbuf; + ctx->buf_size = buf_req; + } + + int ret = knot_rrset_to_wire_extra(rrset, ctx->buf, ctx->buf_size, 0, + NULL, KNOT_PF_ORIGTTL | KNOT_PF_BUFENOUGH); + + // 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/reverse.c b/src/knot/zone/reverse.c new file mode 100644 index 0000000..760afda --- /dev/null +++ b/src/knot/zone/reverse.c @@ -0,0 +1,137 @@ +/* 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/zone/reverse.h" + +static const uint8_t *reverse4postfix = (const uint8_t *)"\x07""in-addr""\x04""arpa"; +static const uint8_t *reverse6postfix = (const uint8_t *)"\x03""ip6""\x04""arpa"; +static const size_t reverse4pf_len = 14; +static const size_t reverse6pf_len = 10; + +static const uint8_t hex_chars[] = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' +}; + +static void reverse_owner4(knot_dname_storage_t out, uint8_t *in_addr_raw) +{ + uint8_t *pos = out; + uint8_t *end = pos + sizeof(knot_dname_storage_t) - 1; + for (int i = 3; i >= 0; i--) { + pos[0] = snprintf((char *)(pos + 1), end - pos, "%d", (int)in_addr_raw[i]); + pos += pos[0] + 1; + assert(pos[0] <= 3); + } + memcpy(pos, reverse4postfix, reverse4pf_len); +} + +static void reverse_owner6(knot_dname_storage_t out, uint8_t *in6_addr_raw) +{ + uint8_t *pos = out; + for (int i = 15; i >= 0; i--) { + uint8_t ip6_byte = in6_addr_raw[i]; + pos[0] = 1; + pos[1] = hex_chars[ip6_byte & 0xF]; + pos[2] = 1; + pos[3] = hex_chars[ip6_byte >> 4]; + pos += 4; + } + memcpy(pos, reverse6postfix, reverse6pf_len); +} + +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); + rrset->rrs.count = 1; +} + +typedef struct { + const knot_dname_t *rev_zone; + zone_contents_t *rev_conts; + zone_update_t *rev_upd; + bool upd_rem; + bool ipv6; +} rev_ctx_t; + +static int reverse_from_node(zone_node_t *node, void *data) +{ + rev_ctx_t *ctx = data; + + knot_rrset_t forw = node_rrset(node, ctx->ipv6 ? KNOT_RRTYPE_AAAA : KNOT_RRTYPE_A); + + knot_rrset_t rev; + knot_dname_storage_t rev_owner; // Will be filled later. + uint8_t rev_data[knot_rdata_size(KNOT_DNAME_MAXLEN)]; // Will be filled later. + knot_rrset_init(&rev, rev_owner, KNOT_RRTYPE_PTR, forw.rclass, forw.ttl); + rev.rrs.rdata = (knot_rdata_t *)rev_data; + + int ret = KNOT_EOK; + + knot_rdata_t *rd = forw.rrs.rdata; + for (int i = 0; i < forw.rrs.count && ret == KNOT_EOK; i++) { + if (ctx->ipv6) { + reverse_owner6(rev_owner, rd->data); + } else { + reverse_owner4(rev_owner, rd->data); + } + + if (knot_dname_in_bailiwick(rev_owner, ctx->rev_zone) < 0) { + rd = knot_rdataset_next(rd); + continue; + } + + set_rdata(&rev, node->owner, knot_dname_size(node->owner)); + + if (ctx->rev_upd != NULL) { + if (ctx->upd_rem) { + ret = zone_update_remove(ctx->rev_upd, &rev); + } else { + ret = zone_update_add(ctx->rev_upd, &rev); + } + } else { + zone_node_t *unused = NULL; + ret = zone_contents_add_rr(ctx->rev_conts, &rev, &unused); + } + + rd = knot_rdataset_next(rd); + } + + return ret; +} + +int zone_reverse(zone_contents_t *from, zone_contents_t *to_conts, + zone_update_t *to_upd, bool to_upd_rem) +{ + const knot_dname_t *to_name; + if (to_upd != NULL) { + to_name = to_upd->zone->name; + } else { + to_name = to_conts->apex->owner; + } + + rev_ctx_t ctx = { + .rev_zone = to_name, + .rev_conts = to_conts, + .rev_upd = to_upd, + .upd_rem = to_upd_rem, + .ipv6 = (knot_dname_in_bailiwick(to_name, reverse6postfix) >= 0) + }; + + return zone_contents_apply(from, reverse_from_node, &ctx); +} diff --git a/src/knot/zone/reverse.h b/src/knot/zone/reverse.h new file mode 100644 index 0000000..b05e0cc --- /dev/null +++ b/src/knot/zone/reverse.h @@ -0,0 +1,41 @@ +/* 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/zone-update.h" + +/*! + * \brief Create/update reverse zone based on forward zone. + * + * \param from Forward zone to be reversed. + * \param to_conts Out/optional: resulting reverse zone. + * \param to_upd Out/optional: resulting update of reverse zone. + * \param to_upd_rem Trigger removal from reverse zone. + * + * \return KNOT_E* + */ +int zone_reverse(zone_contents_t *from, zone_contents_t *to_conts, + zone_update_t *to_upd, bool to_upd_rem); + +inline static int changeset_reverse(changeset_t *from, zone_update_t *to) +{ + int ret = zone_reverse(from->remove, NULL, to, true); + if (ret == KNOT_EOK) { + ret = zone_reverse(from->add, NULL, to, false); + } + return ret; +} diff --git a/src/knot/zone/semantic-check.c b/src/knot/zone/semantic-check.c new file mode 100644 index 0000000..2360728 --- /dev/null +++ b/src/knot/zone/semantic-check.c @@ -0,0 +1,578 @@ +/* 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 "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] = + "unknown algorithm in DS", + [SEM_ERR_DS_RDATA_DIGLEN] = + "invalid digest length in DS", + [SEM_ERR_DS_APEX] = + "DS at the zone apex", + + [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, MANDATORY | SOFT }, // mandatory for apex, optional for others + { 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; + } + + if (data->zone->apex == node) { + data->handler->error = true; + data->handler->cb(data->handler, data->zone, node->owner, + SEM_ERR_DS_APEX, NULL); + return KNOT_EOK; + } + + if (!(data->level & OPTIONAL)) { + return KNOT_EOK; + } + + for (int i = 0; i < dss->count; i++) { + knot_rdata_t *ds = knot_rdataset_at(dss, i); + uint8_t digest_type = knot_ds_digest_type(ds); + uint16_t digest_size = knot_ds_digest_len(ds); + + // Sizes of known digest algorithms. + const uint16_t digest_sizes[] = { 0, 20, 32, 32, 48 }; + + sem_error_t err; + if (digest_type == 0 || + digest_type >= sizeof(digest_sizes) / sizeof(digest_sizes[0])) { + err = SEM_ERR_DS_RDATA_ALG; + } else if (digest_sizes[digest_type] != digest_size) { + err = SEM_ERR_DS_RDATA_DIGLEN; + } else { + continue; + } + + char info[64] = ""; + uint16_t keytag = knot_ds_key_tag(ds); + (void)snprintf(info, sizeof(info), "(keytag %d, algorithm %d)", + keytag, digest_type); + + data->handler->cb(data->handler, data->zone, node->owner, + err, 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..f92639b --- /dev/null +++ b/src/knot/zone/semantic-check.h @@ -0,0 +1,117 @@ +/* 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 <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_DS_APEX, + + 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..0e768c8 --- /dev/null +++ b/src/knot/zone/serial.c @@ -0,0 +1,110 @@ +/* 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/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_generic(uint32_t current, unsigned policy, uint32_t must_increment, + uint8_t rem, uint8_t mod) +{ + uint32_t minimum, result; + + 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) { + result = current + must_increment; + } else { + result = minimum; + } + + if (mod > 1) { + assert(rem < mod); + uint32_t incr = ((rem + mod) - (result % mod)) % mod; + assert(incr == 0 || result % mod != rem); + result += incr; + assert(result % mod == rem); + } + + return result; +} + +uint32_t serial_next(uint32_t current, conf_t *conf, const knot_dname_t *zone, + unsigned policy, uint32_t must_increment) +{ + assert(conf); + assert(zone); + + if (policy == SERIAL_POLICY_AUTO) { + conf_val_t val = conf_zone_get(conf, C_SERIAL_POLICY, zone); + policy = conf_opt(&val); + } + + uint32_t rem, mod; + conf_val_t val = conf_zone_get(conf, C_SERIAL_MODULO, zone); + if (serial_modulo_parse(conf_str(&val), &rem, &mod) != KNOT_EOK) { + assert(0); // cannot happen - ensured by conf check + return 0; + } + + return serial_next_generic(current, policy, must_increment, rem, mod); +} + +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..5eca16e --- /dev/null +++ b/src/knot/zone/serial.h @@ -0,0 +1,117 @@ +/* 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 <inttypes.h> +#include <stdio.h> + +#include "knot/conf/conf.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 Specific policy to use instead of configured one. + * \param must_increment The minimum difference to the current value. + * 0 only ensures policy; 1 also increments. + * \param rem Requested remainder after division by the modulus. + * \param mod Modulus of the given congruency. + * + * \return New serial. + */ +uint32_t serial_next_generic(uint32_t current, unsigned policy, uint32_t must_increment, + uint8_t rem, uint8_t mod); + +/*! + * \brief Get (next) serial for given serial update policy. + * + * This function is similar to serial_next_generic() but policy and parameters + * of the congruency are taken from the server configuration. + * + * \param current Current SOA serial. + * \param conf Configuration to get serial-policy from. + * \param zone Zone to read out configured policy of. + * \param policy Specific policy to use instead of configured one. + * \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, conf_t *conf, const knot_dname_t *zone, + unsigned 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; +} + +/*! + * Gets the tuple value (remainder, modulus) of a string in the format "#/#". + * + * \param[in] str String value to parse. + * \param[out] rem Parsed remainder value. + * \param[out] mod Parsed modulus value. + * + * \return KNOT_EOK if OK, KNOT_E* otherwise. + */ +inline static int serial_modulo_parse(const char *str, uint32_t *rem, uint32_t *mod) +{ + if (str == NULL) { + return KNOT_EINVAL; + } + + char c; // Possible first trailing character. + return sscanf(str, "%"SCNu32"/%"SCNu32"%c", rem, mod, &c) == 2 ? KNOT_EOK : KNOT_EMALF; +} diff --git a/src/knot/zone/timers.c b/src/knot/zone/timers.c new file mode 100644 index 0000000..a12a88a --- /dev/null +++ b/src/knot/zone/timers.c @@ -0,0 +1,242 @@ +/* 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/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, + TIMER_LAST_MASTER = 0x8b, + TIMER_MASTER_PIN_HIT = 0x8c, +}; + +#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); + if (id == TIMER_LAST_MASTER) { + wire_ctx_read(&wire, &timers.last_master, sizeof(timers.last_master)); + continue; + } + 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; + case TIMER_MASTER_PIN_HIT: timers.master_pin_hit = 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) +{ + const char *format = (timers->last_master.sin6_family == AF_INET || + timers->last_master.sin6_family == AF_INET6) ? + "BLBLBLBLBLBLBLBLBDBL" : + "BLBLBLBLBLBLBLBL"; + + MDB_val k = { knot_dname_size(zone), (void *)zone }; + MDB_val v = knot_lmdb_make_key(format, + 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, + TIMER_LAST_MASTER, &timers->last_master, sizeof(timers->last_master), + TIMER_MASTER_PIN_HIT,(uint64_t)timers->master_pin_hit); + 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..67a560b --- /dev/null +++ b/src/knot/zone/timers.h @@ -0,0 +1,102 @@ +/* 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 <stdint.h> +#include <time.h> + +#include "contrib/sockaddr.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. + struct sockaddr_in6 last_master; //!< Address of pinned master (used last time). + time_t master_pin_hit; //!< Fist occurence of another master more updated than the pinned one. +}; + +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..d0fbde4 --- /dev/null +++ b/src/knot/zone/zone.c @@ -0,0 +1,821 @@ +/* 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 <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, (time_t)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); + + init_list(&zone->internal_notify); + + 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); + + ptrlist_free(&zone->internal_notify, NULL); + + 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; +} + +static int journal_insert_flush(conf_t *conf, zone_t *zone, + changeset_t *change, changeset_t *extra, + const zone_diff_t *diff) +{ + zone_journal_t j = { zone_journaldb(zone), zone->name, conf }; + + int ret = journal_insert(j, change, extra, diff); + if (ret == KNOT_EBUSY) { + log_zone_notice(zone->name, "journal, flushing the zone to allow old changesets cleanup to free space"); + + /* 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, diff); + } + } + + return ret; +} + +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; + } + + return journal_insert_flush(conf, zone, change, extra, NULL); +} + +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; + } + + return journal_insert_flush(conf, zone, NULL, NULL, diff); +} + +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 (val.code == KNOT_EOK) ? true : false; // Reference item cannot be empty. +} + +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(*zone->preferred_master)); + memcpy(zone->preferred_master, addr, sockaddr_len(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); +} + +void zone_set_last_master(zone_t *zone, const struct sockaddr_storage *addr) +{ + if (zone == NULL) { + return; + } + + if (addr == NULL) { + memset(&zone->timers.last_master, 0, sizeof(zone->timers.last_master)); + } else { + memcpy(&zone->timers.last_master, addr, sizeof(zone->timers.last_master)); + } + zone->timers.master_pin_hit = 0; +} + +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; + } +} + +static int try_remote(conf_t *conf, zone_t *zone, zone_master_cb callback, + void *callback_data, const char *err_str, const char *remote_id, + conf_val_t *remote, zone_master_fallback_t *fallback, + const char *remote_prefix) +{ + int ret = KNOT_ERROR; + + 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); + assert(fallback->address); + + for (size_t i = 0; i < addr_count && fallback->address; i++) { + conf_remote_t master = conf_remote(conf, remote, i); + ret = callback(conf, zone, &master, callback_data, fallback); + if (ret == KNOT_EOK) { + return ret; + } + + char addr_str[SOCKADDR_STRLEN] = { 0 }; + sockaddr_tostr(addr_str, sizeof(addr_str), &master.addr); + log_zone_info(zone->name, "%s, %sremote %s, address %s, failed (%s)", + err_str, remote_prefix, remote_id, addr_str, knot_strerror(ret)); + } + + log_zone_warning(zone->name, "%s, %sremote %s not usable", + err_str, remote_prefix, remote_id); + + return 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; + } + + conf_val_t val = conf_zone_get(conf, C_MASTER_PIN_TOL, zone->name); + uint32_t pin_tolerance = conf_int(&val); + + /* Find last and preferred master in conf. */ + + const char *last_id = NULL, *preferred_id = NULL; + conf_val_t last = { 0 }, preferred = { 0 }; + int idx = 0, last_idx = -1, preferred_idx = -1; + + conf_val_t masters = conf_zone_get(conf, C_MASTER, zone->name); + conf_mix_iter_t iter; + conf_mix_iter_init(conf, &masters, &iter); + pthread_mutex_lock(&zone->preferred_lock); + 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 (zone->preferred_master != NULL && + sockaddr_net_match(&remote.addr, zone->preferred_master, -1)) { + preferred_id = conf_str(iter.id); + preferred = *iter.id; + preferred_idx = idx; + } + if (pin_tolerance > 0 && + sockaddr_net_match(&remote.addr, (struct sockaddr_storage *)&zone->timers.last_master, -1)) { + last_id = conf_str(iter.id); + last = *iter.id; + last_idx = idx; + } + } + + idx++; + conf_mix_iter_next(&iter); + } + pthread_mutex_unlock(&zone->preferred_lock); + + int ret = KNOT_EOK; + + /* Try the preferred server. */ + + if (preferred_idx >= 0) { + zone_master_fallback_t fallback = { + true, true, preferred_idx == last_idx, pin_tolerance + }; + ret = try_remote(conf, zone, callback, callback_data, err_str, + preferred_id, &preferred, &fallback, "notifying "); + if (ret == KNOT_EOK || !fallback.remote) { + return ret; // Success or local error. + } + } + + /* Try the last server. */ + + if (last_idx >= 0 && last_idx != preferred_idx) { + zone_master_fallback_t fallback = { + true, true, true, pin_tolerance + }; + ret = try_remote(conf, zone, callback, callback_data, err_str, + last_id, &last, &fallback, "pinned "); + if (!fallback.remote) { + return ret; // Local error. + } + } + + /* Try all the other servers. */ + + conf_val_reset(&masters); + conf_mix_iter_init(conf, &masters, &iter); + zone_master_fallback_t fallback = { true, true, false, pin_tolerance }; + for (idx = 0; iter.id->code == KNOT_EOK && fallback.remote; idx++) { + if (idx != last_idx && idx != preferred_idx) { + fallback.address = true; + conf_val_t remote = *iter.id; + ret = try_remote(conf, zone, callback, callback_data, err_str, + conf_str(&remote), &remote, &fallback, ""); + if (!fallback.remote) { + break; // Local error. + } + } + conf_mix_iter_next(&iter); + } + + return ret == KNOT_EOK ? 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); +} + +void zone_local_notify_subscribe(zone_t *zone, zone_t *subscribe) +{ + ptrlist_add(&zone->internal_notify, subscribe, NULL); +} + +void zone_local_notify(zone_t *zone) +{ + ptrnode_t *n; + WALK_LIST(n, zone->internal_notify) { + zone_events_schedule_now(n->d, ZONE_EVENT_LOAD); + } +} + +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..0527c85 --- /dev/null +++ b/src/knot/zone/zone.h @@ -0,0 +1,302 @@ +/* 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 "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 Auto-generated reverse zones... */ + struct zone *reverse_from; + list_t internal_notify; + + /*! \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 Updates the last master address used. */ +void zone_set_last_master(zone_t *zone, const struct sockaddr_storage *addr); + +/*! \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. + bool trying_last; //!< This master try is for the same server as last time; + uint32_t pin_tol; //!< Configured mster pin tolerance (0 for no pin). +} 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); + +void zone_local_notify_subscribe(zone_t *zone, zone_t *subscribe); +void zone_local_notify(zone_t *zone); + +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..d8acd0b --- /dev/null +++ b/src/knot/zone/zonedb-load.c @@ -0,0 +1,664 @@ +/* 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); + + it = knot_zonedb_iter_begin(db_new); + while (!knot_zonedb_iter_finished(it)) { + zone_t *z = knot_zonedb_iter_val(it); + conf_val_t val = conf_zone_get(conf, C_REVERSE_GEN, z->name); + if (val.code == KNOT_EOK) { + const knot_dname_t *forw_name = conf_dname(&val); + zone_t *forw = knot_zonedb_find(db_new, forw_name); + if (forw == NULL) { + knot_dname_txt_storage_t forw_str; + (void)knot_dname_to_str(forw_str, forw_name, sizeof(forw_str)); + log_zone_warning(z->name, "zone to reverse %s doesn't exist", + forw_str); + } else { + z->reverse_from = forw; + zone_local_notify_subscribe(forw, z); + } + } + knot_zonedb_iter_next(it); + } + knot_zonedb_iter_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..d104820 --- /dev/null +++ b/src/knot/zone/zonefile.c @@ -0,0 +1,373 @@ +/* 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); |