diff options
Diffstat (limited to '')
-rw-r--r-- | src/knot/zone/contents.c | 1197 | ||||
-rw-r--r-- | src/knot/zone/contents.h | 288 | ||||
-rw-r--r-- | src/knot/zone/node.c | 312 | ||||
-rw-r--r-- | src/knot/zone/node.h | 276 | ||||
-rw-r--r-- | src/knot/zone/semantic-check.c | 1193 | ||||
-rw-r--r-- | src/knot/zone/semantic-check.h | 123 | ||||
-rw-r--r-- | src/knot/zone/serial.c | 84 | ||||
-rw-r--r-- | src/knot/zone/serial.h | 72 | ||||
-rw-r--r-- | src/knot/zone/timers.c | 288 | ||||
-rw-r--r-- | src/knot/zone/timers.h | 119 | ||||
-rw-r--r-- | src/knot/zone/zone-diff.c | 389 | ||||
-rw-r--r-- | src/knot/zone/zone-diff.h | 31 | ||||
-rw-r--r-- | src/knot/zone/zone-dump.c | 226 | ||||
-rw-r--r-- | src/knot/zone/zone-dump.h | 41 | ||||
-rw-r--r-- | src/knot/zone/zone-load.c | 154 | ||||
-rw-r--r-- | src/knot/zone/zone-load.h | 63 | ||||
-rw-r--r-- | src/knot/zone/zone-tree.c | 209 | ||||
-rw-r--r-- | src/knot/zone/zone-tree.h | 142 | ||||
-rw-r--r-- | src/knot/zone/zone.c | 731 | ||||
-rw-r--r-- | src/knot/zone/zone.h | 177 | ||||
-rw-r--r-- | src/knot/zone/zonedb-load.c | 346 | ||||
-rw-r--r-- | src/knot/zone/zonedb-load.h | 28 | ||||
-rw-r--r-- | src/knot/zone/zonedb.c | 167 | ||||
-rw-r--r-- | src/knot/zone/zonedb.h | 123 | ||||
-rw-r--r-- | src/knot/zone/zonefile.c | 343 | ||||
-rw-r--r-- | src/knot/zone/zonefile.h | 104 |
26 files changed, 7226 insertions, 0 deletions
diff --git a/src/knot/zone/contents.c b/src/knot/zone/contents.c new file mode 100644 index 0000000..e99cb48 --- /dev/null +++ b/src/knot/zone/contents.c @@ -0,0 +1,1197 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include "libdnssec/error.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" +#include "contrib/macros.h" + +typedef struct { + zone_contents_apply_cb_t func; + void *data; +} zone_tree_func_t; + +typedef struct { + zone_node_t *first_node; + zone_contents_t *zone; + zone_node_t *previous_node; +} zone_adjust_arg_t; + +static int tree_apply_cb(zone_node_t **node, void *data) +{ + if (node == NULL || data == NULL) { + return KNOT_EINVAL; + } + + zone_tree_func_t *f = (zone_tree_func_t *)data; + return f->func(*node, f->data); +} + +/*! + * \brief Checks if the given node can be inserted into the given zone. + * + * Checks if both the arguments are non-NULL and if the owner of the node + * belongs to the zone (i.e. is a subdomain of the zone apex). + * + * \param zone Zone to which the node is going to be inserted. + * \param node Node to check. + * + * \retval KNOT_EOK if both arguments are non-NULL and the node belongs to the + * zone. + * \retval KNOT_EINVAL if either of the arguments is NULL. + * \retval KNOT_EOUTOFZONE if the node does not belong to the zone. + */ +static int check_node(const zone_contents_t *contents, const zone_node_t *node) +{ + assert(contents); + assert(contents->apex != NULL); + assert(node); + + if (knot_dname_in_bailiwick(node->owner, contents->apex->owner) <= 0) { + return KNOT_EOUTOFZONE; + } + + return KNOT_EOK; +} + +/*! + * \brief Destroys all RRSets in a node. + * + * This function is designed to be used in the tree-iterating functions. + * + * \param node Node to destroy RRSets from. + * \param data Unused parameter. + */ +static int destroy_node_rrsets_from_tree(zone_node_t **node, void *data) +{ + assert(node); + UNUSED(data); + + if (*node != NULL) { + node_free_rrsets(*node, NULL); + node_free(*node, NULL); + } + + return KNOT_EOK; +} + +static int create_nsec3_name(uint8_t *out, size_t out_size, + const zone_contents_t *zone, + const knot_dname_t *name) +{ + assert(out); + assert(zone); + assert(name); + + if (!knot_is_nsec3_enabled(zone)) { + return KNOT_ENSEC3PAR; + } + + return knot_create_nsec3_owner(out, out_size, name, zone->apex->owner, + &zone->nsec3_params); +} + +/*! \brief Link pointers to additional nodes for this RRSet. */ +static int discover_additionals(const knot_dname_t *owner, struct rr_data *rr_data, + zone_contents_t *zone) +{ + assert(rr_data != NULL); + + /* Drop possible previous additional nodes. */ + additional_clear(rr_data->additional); + rr_data->additional = NULL; + + const knot_rdataset_t *rrs = &rr_data->rrs; + 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++) { + knot_rdata_t *rdata = knot_rdataset_at(rrs, i); + const knot_dname_t *dname = knot_rdata_name(rdata, rr_data->type); + const zone_node_t *node = NULL, *encloser = NULL, *prev = NULL; + + /* Try to find node for the dname in the RDATA. */ + zone_contents_find_dname(zone, dname, &node, &encloser, &prev); + if (node == NULL && encloser != NULL + && (encloser->flags & NODE_FLAGS_WILDCARD_CHILD)) { + /* Find wildcard child in the zone. */ + node = zone_contents_find_wildcard_child(zone, encloser); + assert(node != NULL); + } + + if (node == NULL) { + 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, owner) >= 0) { + glue = &mandatory[mandatory_count++]; + glue->optional = false; + } else { + glue = &others[others_count++]; + glue->optional = true; + } + glue->node = node; + glue->ns_pos = i; + } + + /* Store sorted additionals by the type, mandatory first. */ + size_t total_count = mandatory_count + others_count; + if (total_count > 0) { + rr_data->additional = malloc(sizeof(additional_t)); + if (rr_data->additional == NULL) { + return KNOT_ENOMEM; + } + rr_data->additional->count = total_count; + + size_t size = total_count * sizeof(glue_t); + rr_data->additional->glues = malloc(size); + if (rr_data->additional->glues == NULL) { + free(rr_data->additional); + return KNOT_ENOMEM; + } + + size_t mandatory_size = mandatory_count * sizeof(glue_t); + memcpy(rr_data->additional->glues, mandatory, mandatory_size); + memcpy(rr_data->additional->glues + mandatory_count, others, + size - mandatory_size); + } + + return KNOT_EOK; +} + +static int adjust_pointers(zone_node_t **tnode, void *data) +{ + assert(tnode != NULL); + assert(data != NULL); + + zone_adjust_arg_t *args = (zone_adjust_arg_t *)data; + zone_node_t *node = *tnode; + + // remember first node + if (args->first_node == NULL) { + args->first_node = node; + } + + // check if this node is not a wildcard child of its parent + if (knot_dname_is_wildcard(node->owner)) { + assert(node->parent != NULL); + node->parent->flags |= NODE_FLAGS_WILDCARD_CHILD; + } + + // set flags (delegation point, non-authoritative) + if (node->parent && + (node->parent->flags & NODE_FLAGS_DELEG || + node->parent->flags & NODE_FLAGS_NONAUTH)) { + node->flags |= NODE_FLAGS_NONAUTH; + } else if (node_rrtype_exists(node, KNOT_RRTYPE_NS) && node != args->zone->apex) { + node->flags |= NODE_FLAGS_DELEG; + } else { + // Default. + node->flags = NODE_FLAGS_AUTH; + } + + // set pointer to previous 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 KNOT_EOK; +} + +static int adjust_nsec3_pointers(zone_node_t **tnode, void *data) +{ + assert(data != NULL); + assert(tnode != NULL); + + zone_adjust_arg_t *args = (zone_adjust_arg_t *)data; + zone_node_t *node = *tnode; + const zone_node_t *ignored; + + // Connect to NSEC3 node (only if NSEC3 tree is not empty) + node->nsec3_wildcard_prev = NULL; + uint8_t nsec3_name[KNOT_DNAME_MAXLEN]; + int ret = create_nsec3_name(nsec3_name, sizeof(nsec3_name), args->zone, + node->owner); + if (ret == KNOT_EOK) { + node->nsec3_node = zone_tree_get(args->zone->nsec3_nodes, nsec3_name); + + // Connect to NSEC3 node proving nonexistence of wildcard. + size_t wildcard_size = knot_dname_size(node->owner) + 2; + if (wildcard_size <= KNOT_DNAME_MAXLEN) { + assert(wildcard_size > 2); + knot_dname_t wildcard[wildcard_size]; + memcpy(wildcard, "\x01""*", 2); + memcpy(wildcard + 2, node->owner, wildcard_size - 2); + ret = zone_contents_find_nsec3_for_name(args->zone, wildcard, &ignored, + (const zone_node_t **)&node->nsec3_wildcard_prev); + if (ret == ZONE_NAME_FOUND) { + node->nsec3_wildcard_prev = NULL; + ret = KNOT_EOK; + } + } + } else if (ret == KNOT_ENSEC3PAR) { + node->nsec3_node = NULL; + ret = KNOT_EOK; + } + + return ret; +} + +static int measure_size(zone_node_t *node, void *data){ + + size_t *size = data; + int rrset_count = node->rrset_count; + for (int i = 0; i < rrset_count; i++) { + knot_rrset_t rrset = node_rrset_at(node, i); + *size += knot_rrset_size(&rrset); + } + return KNOT_EOK; +} + +static int measure_max_ttl(zone_node_t *node, void *data){ + + uint32_t *max = data; + int rrset_count = node->rrset_count; + for (int i = 0; i < rrset_count; i++) { + *max = MAX(*max, node->rrs[i].ttl); + } + return KNOT_EOK; +} + +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); +} + +/*! + * \brief Adjust normal (non NSEC3) node. + * + * Set: + * - pointer to wildcard childs in parent nodes if applicable + * - flags (delegation point, non-authoritative) + * - pointer to previous node + * - parent pointers + * + * \param tnode Zone node to adjust. + * \param data Adjusting parameters (zone_adjust_arg_t *). + */ +static int adjust_normal_node(zone_node_t **tnode, void *data) +{ + assert(tnode != NULL && *tnode); + assert(data != NULL); + + // Do cheap operations first + int ret = adjust_pointers(tnode, data); + if (ret != KNOT_EOK) { + return ret; + } + + zone_adjust_arg_t *arg = data; + measure_size(*tnode, &arg->zone->size); + measure_max_ttl(*tnode, &arg->zone->max_ttl); + + // Connect nodes to their NSEC3 nodes + return adjust_nsec3_pointers(tnode, data); +} + +/*! + * \brief Adjust NSEC3 node. + * + * Set: + * - pointer to previous node + * - pointer to node stored in owner dname + * + * \param tnode Zone node to adjust. + * \param data Adjusting parameters (zone_adjust_arg_t *). + */ +static int adjust_nsec3_node(zone_node_t **tnode, void *data) +{ + assert(data != NULL); + assert(tnode != NULL); + + zone_adjust_arg_t *args = (zone_adjust_arg_t *)data; + zone_node_t *node = *tnode; + + // remember first node + if (args->first_node == NULL) { + args->first_node = node; + } + + // set previous node + node->prev = args->previous_node; + args->previous_node = node; + + measure_size(*tnode, &args->zone->size); + measure_max_ttl(*tnode, &args->zone->max_ttl); + + // check if this node belongs to correct 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, &args->zone->nsec3_params, i)) { + node->flags |= NODE_FLAGS_IN_NSEC3_CHAIN; + } + } + + return KNOT_EOK; +} + +/*! \brief Discover additional records for affected nodes. */ +static int adjust_additional(zone_node_t **tnode, void *data) +{ + assert(data != NULL); + assert(tnode != NULL); + + zone_adjust_arg_t *args = (zone_adjust_arg_t *)data; + zone_node_t *node = *tnode; + + /* 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->owner, rr_data, args->zone); + if (ret != KNOT_EOK) { + return ret; + } + } + } + + 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; +} + +zone_contents_t *zone_contents_new(const knot_dname_t *apex_name) +{ + if (apex_name == NULL) { + return NULL; + } + + zone_contents_t *contents = malloc(sizeof(zone_contents_t)); + if (contents == NULL) { + return NULL; + } + + memset(contents, 0, sizeof(zone_contents_t)); + contents->apex = node_new(apex_name, NULL); + if (contents->apex == NULL) { + goto cleanup; + } + + contents->nodes = zone_tree_create(); + if (contents->nodes == NULL) { + goto cleanup; + } + + if (zone_tree_insert(contents->nodes, contents->apex) != KNOT_EOK) { + goto cleanup; + } + + return contents; + +cleanup: + free(contents->nodes); + free(contents->nsec3_nodes); + free(contents); + return 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 int add_node(zone_contents_t *zone, zone_node_t *node, bool create_parents) +{ + if (zone == NULL || node == NULL) { + return KNOT_EINVAL; + } + + int ret = check_node(zone, node); + if (ret != KNOT_EOK) { + return ret; + } + + ret = zone_tree_insert(zone->nodes, node); + if (ret != KNOT_EOK) { + return ret; + } + + if (!create_parents) { + return KNOT_EOK; + } + + /* No parents for root domain. */ + if (*node->owner == '\0') { + return KNOT_EOK; + } + + zone_node_t *next_node = NULL; + const uint8_t *parent = knot_wire_next_label(node->owner, NULL); + + if (knot_dname_is_equal(zone->apex->owner, parent)) { + node_set_parent(node, zone->apex); + + // check if the node is not wildcard child of the parent + if (knot_dname_is_wildcard(node->owner)) { + zone->apex->flags |= NODE_FLAGS_WILDCARD_CHILD; + } + } else { + while (parent != NULL && !(next_node = get_node(zone, parent))) { + + /* Create a new node. */ + next_node = node_new(parent, NULL); + if (next_node == NULL) { + return KNOT_ENOMEM; + } + + /* Insert node to a tree. */ + ret = zone_tree_insert(zone->nodes, next_node); + if (ret != KNOT_EOK) { + node_free(next_node, NULL); + return ret; + } + + /* Update node pointers. */ + node_set_parent(node, next_node); + if (knot_dname_is_wildcard(node->owner)) { + next_node->flags |= NODE_FLAGS_WILDCARD_CHILD; + } + + node = next_node; + parent = knot_wire_next_label(parent, NULL); + } + + // set the found parent (in the zone) as the parent of the last + // inserted node + assert(node->parent == NULL); + node_set_parent(node, next_node); + } + + return KNOT_EOK; +} + +static int add_nsec3_node(zone_contents_t *zone, zone_node_t *node) +{ + if (zone == NULL || node == NULL) { + return KNOT_EINVAL; + } + + int ret = check_node(zone, node); + if (ret != KNOT_EOK) { + return ret; + } + + /* Create NSEC3 tree if not exists. */ + if (zone->nsec3_nodes == NULL) { + zone->nsec3_nodes = zone_tree_create(); + if (zone->nsec3_nodes == NULL) { + return KNOT_ENOMEM; + } + } + + // how to know if this is successful?? + ret = zone_tree_insert(zone->nsec3_nodes, node); + if (ret != KNOT_EOK) { + return ret; + } + + // no parents to be created, the only parent is the zone apex + // set the apex as the parent of the node + node_set_parent(node, zone->apex); + + // cannot be wildcard child, so nothing to be done + + return KNOT_EOK; +} + +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, 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; + } + + if (*n == NULL) { + *n = nsec3 ? get_nsec3_node(z, rr->owner) : get_node(z, rr->owner); + if (*n == NULL) { + // Create new, insert + *n = node_new(rr->owner, NULL); + if (*n == NULL) { + return KNOT_ENOMEM; + } + int ret = nsec3 ? add_nsec3_node(z, *n) : add_node(z, *n, true); + if (ret != KNOT_EOK) { + node_free(*n, NULL); + *n = NULL; + } + } + } + + 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; + } + + knot_rdataset_t *node_rrs = node_rdataset(node, rr->type); + // Subtract changeset RRS from node RRS. + int ret = knot_rdataset_subtract(node_rrs, &rr->rrs, NULL); + if (ret != KNOT_EOK) { + return ret; + } + + if (node_rrs->count == 0) { + // RRSet is empty now, remove it from node, all data freed. + node_remove_rdataset(node, rr->type); + // If node is empty now, delete it from zone tree. + if (node->rrset_count == 0 && node != z->apex) { + zone_tree_delete_empty(nsec3 ? z->nsec3_nodes : z->nodes, node); + } + } + + *n = node; + return KNOT_EOK; +} + +static int recreate_normal_tree(const zone_contents_t *z, zone_contents_t *out) +{ + out->nodes = trie_create(NULL); + if (out->nodes == NULL) { + return KNOT_ENOMEM; + } + + // Insert APEX first. + zone_node_t *apex_cpy = node_shallow_copy(z->apex, NULL); + if (apex_cpy == NULL) { + return KNOT_ENOMEM; + } + + // Normal additions need apex ... so we need to insert directly. + int ret = zone_tree_insert(out->nodes, apex_cpy); + if (ret != KNOT_EOK) { + node_free(apex_cpy, NULL); + return ret; + } + + out->apex = apex_cpy; + + trie_it_t *itt = trie_it_begin(z->nodes); + if (itt == NULL) { + return KNOT_ENOMEM; + } + + while (!trie_it_finished(itt)) { + const zone_node_t *to_cpy = (zone_node_t *)*trie_it_val(itt); + if (to_cpy == z->apex) { + // Inserted already. + trie_it_next(itt); + continue; + } + zone_node_t *to_add = node_shallow_copy(to_cpy, NULL); + if (to_add == NULL) { + trie_it_free(itt); + return KNOT_ENOMEM; + } + + int ret = add_node(out, to_add, true); + if (ret != KNOT_EOK) { + node_free(to_add, NULL); + trie_it_free(itt); + return ret; + } + trie_it_next(itt); + } + + trie_it_free(itt); + + return KNOT_EOK; +} + +static int recreate_nsec3_tree(const zone_contents_t *z, zone_contents_t *out) +{ + out->nsec3_nodes = trie_create(NULL); + if (out->nsec3_nodes == NULL) { + return KNOT_ENOMEM; + } + + trie_it_t *itt = trie_it_begin(z->nsec3_nodes); + if (itt == NULL) { + return KNOT_ENOMEM; + } + while (!trie_it_finished(itt)) { + const zone_node_t *to_cpy = (zone_node_t *)*trie_it_val(itt); + zone_node_t *to_add = node_shallow_copy(to_cpy, NULL); + if (to_add == NULL) { + trie_it_free(itt); + return KNOT_ENOMEM; + } + + int ret = add_nsec3_node(out, to_add); + if (ret != KNOT_EOK) { + trie_it_free(itt); + node_free(to_add, NULL); + return ret; + } + + trie_it_next(itt); + } + + trie_it_free(itt); + + return KNOT_EOK; +} + +// Public API + +int zone_contents_add_rr(zone_contents_t *z, const knot_rrset_t *rr, + zone_node_t **n) +{ + if (z == NULL || rr == NULL || n == NULL) { + return KNOT_EINVAL; + } + + return insert_rr(z, rr, n, knot_rrset_is_nsec3rel(rr)); +} + +int zone_contents_remove_rr(zone_contents_t *z, const knot_rrset_t *rr, + zone_node_t **n) +{ + if (z == NULL || rr == NULL || n == NULL) { + return KNOT_EINVAL; + } + + return remove_rr(z, rr, n, knot_rrset_is_nsec3rel(rr)); +} + +zone_node_t *zone_contents_get_node_for_rr(zone_contents_t *zone, const knot_rrset_t *rrset) +{ + if (zone == NULL || rrset == NULL) { + return NULL; + } + + const bool nsec3 = knot_rrset_is_nsec3rel(rrset); + zone_node_t *node = nsec3 ? get_nsec3_node(zone, rrset->owner) : + get_node(zone, rrset->owner); + if (node == NULL) { + node = node_new(rrset->owner, NULL); + int ret = nsec3 ? add_nsec3_node(zone, node) : add_node(zone, node, true); + if (ret != KNOT_EOK) { + node_free(node, NULL); + return NULL; + } + + return node; + } else { + return node; + } +} + +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); +} + +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 (!zone || !name || !match || !closest || !previous) { + return KNOT_EINVAL; + } + + 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) { + // exact match + + assert(node && prev); + + *match = node; + *closest = node; + *previous = prev; + + 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; + assert(node); + } + + *match = NULL; + *closest = node; + *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 (zone == NULL || name == NULL || nsec3_node == NULL || + nsec3_previous == NULL) { + return KNOT_EINVAL; + } + + // check if the NSEC3 tree is not empty + if (zone_tree_is_empty(zone->nsec3_nodes)) { + return KNOT_ENSEC3CHAIN; + } + + uint8_t nsec3_name[KNOT_DNAME_MAXLEN]; + int ret = create_nsec3_name(nsec3_name, sizeof(nsec3_name), zone, name); + if (ret != KNOT_EOK) { + return ret; + } + + 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 = (*nsec3_node)->prev; + } 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 = (*nsec3_previous)->prev; + 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_t wildcard[KNOT_DNAME_MAXLEN] = { 0x01, '*' }; + knot_dname_to_wire(wildcard + 2, parent->owner, KNOT_DNAME_MAXLEN - 2); + + return zone_contents_find_node(contents, wildcard); +} + +static int adjust_nodes(zone_tree_t *nodes, zone_adjust_arg_t *adjust_arg, + zone_tree_apply_cb_t callback) +{ + assert(adjust_arg); + assert(callback); + + if (zone_tree_is_empty(nodes)) { + return KNOT_EOK; + } + + adjust_arg->first_node = NULL; + adjust_arg->previous_node = NULL; + + int ret = zone_tree_apply(nodes, callback, adjust_arg); + + if (adjust_arg->first_node) { + adjust_arg->first_node->prev = adjust_arg->previous_node; + } + + return ret; +} + +static int load_nsec3param(zone_contents_t *contents) +{ + assert(contents); + assert(contents->apex); + + 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; +} + +static int contents_adjust(zone_contents_t *contents, bool normal) +{ + if (contents == NULL || contents->apex == NULL) { + return KNOT_EINVAL; + } + + int ret = load_nsec3param(contents); + if (ret != KNOT_EOK) { + log_zone_error(contents->apex->owner, + "failed to load NSEC3 parameters (%s)", + knot_strerror(ret)); + return ret; + } + + zone_adjust_arg_t arg = { + .zone = contents + }; + + contents->size = 0; + contents->dnssec = node_rrtype_is_signed(contents->apex, KNOT_RRTYPE_SOA); + + // NSEC3 nodes must be adjusted first, because we already need the NSEC3 chain + // to be closed before we adjust NSEC3 pointers in adjust_normal_node + ret = adjust_nodes(contents->nsec3_nodes, &arg, adjust_nsec3_node); + if (ret != KNOT_EOK) { + return ret; + } + + ret = adjust_nodes(contents->nodes, &arg, + normal ? adjust_normal_node : adjust_pointers); + if (ret != KNOT_EOK) { + return ret; + } + + return adjust_nodes(contents->nodes, &arg, adjust_additional); +} + +int zone_contents_adjust_pointers(zone_contents_t *contents) +{ + return contents_adjust(contents, false); +} + +int zone_contents_adjust_full(zone_contents_t *contents) +{ + return contents_adjust(contents, true); +} + +int zone_contents_apply(zone_contents_t *contents, + zone_contents_apply_cb_t function, void *data) +{ + if (contents == NULL) { + return KNOT_EINVAL; + } + + zone_tree_func_t f = { + .func = function, + .data = data + }; + + return zone_tree_apply(contents->nodes, tree_apply_cb, &f); +} + +int zone_contents_nsec3_apply(zone_contents_t *contents, + zone_contents_apply_cb_t function, void *data) +{ + if (contents == NULL) { + return KNOT_EINVAL; + } + + zone_tree_func_t f = { + .func = function, + .data = data + }; + + return zone_tree_apply(contents->nsec3_nodes, tree_apply_cb, &f); +} + +int zone_contents_shallow_copy(const zone_contents_t *from, zone_contents_t **to) +{ + if (from == NULL || to == NULL) { + return KNOT_EINVAL; + } + + /* 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; + } + + int ret = recreate_normal_tree(from, contents); + if (ret != KNOT_EOK) { + zone_tree_free(&contents->nodes); + free(contents); + return ret; + } + + if (from->nsec3_nodes) { + ret = recreate_nsec3_tree(from, contents); + if (ret != KNOT_EOK) { + zone_tree_free(&contents->nodes); + zone_tree_free(&contents->nsec3_nodes); + free(contents); + return ret; + } + } else { + contents->nsec3_nodes = NULL; + } + + *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); + + 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); + } +} + +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); +} + +size_t zone_contents_measure_size(zone_contents_t *zone) +{ + zone->size = 0; + zone_contents_apply(zone, measure_size, &zone->size); + return zone->size; +} + +uint32_t zone_contents_max_ttl(zone_contents_t *zone) +{ + zone->max_ttl = 0; + zone_contents_apply(zone, measure_max_ttl, &zone->size); + return zone->max_ttl; +} diff --git a/src/knot/zone/contents.h b/src/knot/zone/contents.h new file mode 100644 index 0000000..9ccf3c0 --- /dev/null +++ b/src/knot/zone/contents.h @@ -0,0 +1,288 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +/*! + * \file + * + * Zone contents structure and API for manipulating it. + * + * \addtogroup zone + * @{ + */ + +#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; + + dnssec_nsec3_params_t nsec3_params; + size_t size; + uint32_t max_ttl; + bool dnssec; +} zone_contents_t; + +/*! + * \brief Signature of callback for zone contents apply functions. + */ +typedef int (*zone_contents_apply_cb_t)(zone_node_t *node, void *data); + +/*! + * \brief Allocate and create new zone contents. + * + * \param apex_name Name of the root node. + * + * \return New contents or NULL on error. + */ +zone_contents_t *zone_contents_new(const knot_dname_t *apex_name); + +/*! + * \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 Get the node with this RR (the RR's owner). + * + * \param zone Contents to add to. + * \param rrset The RR to add. + * + * \return The searched node if it exists, a new added empty node or NULL on error. + */ +zone_node_t *zone_contents_get_node_for_rr(zone_contents_t *zone, const knot_rrset_t *rrset); + +/*! + * \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); + +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_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. + * + * \note This function is identical to zone_contents_get_nsec3_node(), only it + * returns constant reference. + * + * \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_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); + +const zone_node_t *zone_contents_find_wildcard_child(const zone_contents_t *contents, + const zone_node_t *parent); + +/*! + * \brief Sets parent and previous pointers and node flags. (cheap operation) + * For both normal and NSEC3 tree + * + * \param contents Zone contents to be adjusted. + */ +int zone_contents_adjust_pointers(zone_contents_t *contents); + +/*! + * \brief Sets parent and previous pointers, sets node flags and NSEC3 links. + * This has to be called before the zone can be served. + * + * \param contents Zone contents to be adjusted. + */ +int zone_contents_adjust_full(zone_contents_t *contents); + +/*! + * \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_contents_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_contents_apply_cb_t function, void *data); + +/*! + * \brief Creates a shallow copy of the zone (no stored data are copied). + * + * This function creates a new zone structure in \a to, creates new trees for + * regular nodes and for NSEC3 nodes, creates new hash table and a new domain + * table. It also fills these structures with the exact same data as the + * original zone is - no copying of stored data is done, just pointers are + * copied. + * + * \param from Original zone. + * \param to Copy of the zone. + * + * \retval KNOT_EOK + * \retval KNOT_EINVAL + * \retval KNOT_ENOMEM + */ +int zone_contents_shallow_copy(const 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 Return true if zone is empty. + */ +bool zone_contents_is_empty(const zone_contents_t *zone); + +/*! + * \brief Measure zone contents size. + * + * Size is measured in uncompressed wire format. Measured size is saved into + * zone contents structure. + * \return Measured size + */ +size_t zone_contents_measure_size(zone_contents_t *zone); + +/*! + * \brief Obtain maximal TTL above all the records in zone. + * + * The value is also stored in zone_contents structure. + * + * \param zone Zone in question. + * \return Maximal TTL. + */ +uint32_t zone_contents_max_ttl(zone_contents_t *zone); + +/*! @} */ diff --git a/src/knot/zone/node.c b/src/knot/zone/node.c new file mode 100644 index 0000000..2d593b9 --- /dev/null +++ b/src/knot/zone/node.c @@ -0,0 +1,312 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "knot/zone/node.h" +#include "libknot/libknot.h" +#include "contrib/mempattern.h" + +void additional_clear(additional_t *additional) +{ + if (additional == NULL) { + return; + } + + free(additional->glues); + free(additional); +} + +/*! \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); + additional_clear(data->additional); +} + +/*! \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; + int ret = rr_data_from(rrset, node->rrs + node->rrset_count, 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, knot_mm_t *mm) +{ + zone_node_t *ret = mm_alloc(mm, 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; + + return ret; +} + +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) { + 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); + + if (node->rrs != NULL) { + mm_free(mm, node->rrs); + } + + mm_free(mm, node); +} + +zone_node_t *node_shallow_copy(const zone_node_t *src, knot_mm_t *mm) +{ + if (src == NULL) { + return NULL; + } + + // create new node + zone_node_t *dst = node_new(src->owner, mm); + if (dst == NULL) { + return NULL; + } + + dst->flags = src->flags; + + // copy RRSets + dst->rrset_count = src->rrset_count; + size_t rrlen = sizeof(struct rr_data) * src->rrset_count; + dst->rrs = mm_alloc(mm, rrlen); + if (dst->rrs == NULL) { + node_free(dst, mm); + return NULL; + } + memcpy(dst->rrs, src->rrs, rrlen); + + for (uint16_t i = 0; i < src->rrset_count; ++i) { + // Clear additionals in the copy. + dst->rrs[i].additional = NULL; + } + + return dst; +} + +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; + } + + 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; + } + + for (int i = 0; i < node->rrset_count; ++i) { + if (node->rrs[i].type == type) { + // We need to free additionals from this rr_data before it gets overwritten. + additional_clear(node->rrs[i].additional); + memmove(node->rrs + i, node->rrs + i + 1, + (node->rrset_count - i - 1) * sizeof(struct rr_data)); + --node->rrset_count; + return; + } + } +} + +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; +} + +void node_set_parent(zone_node_t *node, zone_node_t *parent) +{ + if (node == NULL || node->parent == parent) { + return; + } + + // decrease number of children of previous parent + if (node->parent != NULL) { + --node->parent->children; + } + // set the parent + node->parent = parent; + + // increase the count of children of the new parent + if (parent != NULL) { + ++parent->children; + } +} + +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..07b4678 --- /dev/null +++ b/src/knot/zone/node.h @@ -0,0 +1,276 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#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; + struct zone_node *nsec3_node; /*! NSEC3 node corresponding to this node. */ + struct zone_node *nsec3_wildcard_prev; /*! NSEC3 node for proof of wildcard non-existence. */ + uint32_t children; /*!< Count of children nodes in DNS hierarchy. */ + uint16_t rrset_count; /*!< Number of RRSets stored in the node. */ + uint8_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 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 Clears additional structure. + * + * \param additional Additional to clear. + */ +void additional_clear(additional_t *additional); + +/*! + * \brief Creates and initializes new node structure. + * + * \param owner Node's owner, will be duplicated. + * \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, knot_mm_t *mm); + +/*! + * \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 Creates a shallow copy of node structure, RR data are shared. + * + * \param src Source of the copy. + * \param mm Memory context to use. + * + * \return Copied node if success, NULL otherwise. + */ +zone_node_t *node_shallow_copy(const zone_node_t *src, 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 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 Sets the parent of the node. Also adjusts children count of parent. + * + * \param node Node to set the parent of. + * \param parent Parent to set to the node. + */ +void node_set_parent(zone_node_t *node, zone_node_t *parent); + +/*! + * \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; +} diff --git a/src/knot/zone/semantic-check.c b/src/knot/zone/semantic-check.c new file mode 100644 index 0000000..e7e9129 --- /dev/null +++ b/src/knot/zone/semantic-check.c @@ -0,0 +1,1193 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <stdio.h> +#include <sys/types.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> + +#include "libdnssec/error.h" +#include "contrib/base32hex.h" +#include "contrib/string.h" +#include "libknot/libknot.h" +#include "knot/zone/semantic-check.h" +#include "knot/dnssec/rrset-sign.h" +#include "knot/dnssec/zone-nsec.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] = + "more records exist at CNAME", + [SEM_ERR_CNAME_MULTIPLE] = + "multiple CNAME records", + + [SEM_ERR_DNAME_CHILDREN] = + "child record exists under DNAME", + + [SEM_ERR_NS_APEX] = + "missing NS at the zone apex", + [SEM_ERR_NS_GLUE] = + "missing glue record", + + [SEM_ERR_RRSIG_RDATA_TYPE_COVERED] = + "wrong type covered in RRSIG", + [SEM_ERR_RRSIG_RDATA_TTL] = + "wrong original TTL in RRSIG", + [SEM_ERR_RRSIG_RDATA_EXPIRATION] = + "expired RRSIG", + [SEM_ERR_RRSIG_RDATA_INCEPTION] = + "RRSIG inception in the future", + [SEM_ERR_RRSIG_RDATA_LABELS] = + "wrong labels in RRSIG", + [SEM_ERR_RRSIG_RDATA_OWNER] = + "wrong signer's name in RRSIG", + [SEM_ERR_RRSIG_NO_RRSIG] = + "missing RRSIG", + [SEM_ERR_RRSIG_SIGNED] = + "signed RRSIG", + [SEM_ERR_RRSIG_UNVERIFIABLE] = + "unverifiable signature", + + [SEM_ERR_NSEC_NONE] = + "missing NSEC", + [SEM_ERR_NSEC_RDATA_BITMAP] = + "incorrect type bitmap in NSEC", + [SEM_ERR_NSEC_RDATA_MULTIPLE] = + "multiple NSEC records", + [SEM_ERR_NSEC_RDATA_CHAIN] = + "incoherent NSEC chain", + + [SEM_ERR_NSEC3_NONE] = + "missing NSEC3", + [SEM_ERR_NSEC3_INSECURE_DELEGATION_OPT] = + "insecure delegation outside NSEC3 opt-out", + [SEM_ERR_NSEC3_EXTRA_RECORD] = + "invalid record type in NSEC3 chain", + [SEM_ERR_NSEC3_RDATA_TTL] = + "inconsistent TTL for NSEC3 and minimum TTL in SOA", + [SEM_ERR_NSEC3_RDATA_CHAIN] = + "incoherent NSEC3 chain", + [SEM_ERR_NSEC3_RDATA_BITMAP] = + "incorrect type bitmap in NSEC3", + [SEM_ERR_NSEC3_RDATA_FLAGS] = + "incorrect flags in NSEC3", + [SEM_ERR_NSEC3_RDATA_SALT] = + "incorrect salt in NSEC3", + [SEM_ERR_NSEC3_RDATA_ALG] = + "incorrect algorithm in NSEC3", + [SEM_ERR_NSEC3_RDATA_ITERS] = + "incorrect number of iterations in NSEC3", + + [SEM_ERR_NSEC3PARAM_RDATA_FLAGS] = + "invalid flags in NSEC3PARAM", + [SEM_ERR_NSEC3PARAM_RDATA_ALG] = + "invalid algorithm in NSEC3PARAM", + + [SEM_ERR_DS_RDATA_ALG] = + "invalid algorithm in DS", + [SEM_ERR_DS_RDATA_DIGLEN] = + "invalid digest length in DS", + + [SEM_ERR_DNSKEY_NONE] = + "missing DNSKEY", + [SEM_ERR_DNSKEY_INVALID] = + "invalid DNSKEY", + [SEM_ERR_DNSKEY_RDATA_PROTOCOL] = + "invalid protocol in DNSKEY", + + [SEM_ERR_CDS_NONE] = + "missing CDS", + [SEM_ERR_CDS_MULTIPLE] = + "multiple CDS records", + [SEM_ERR_CDS_NOT_MATCH] = + "CDS not match CDNSKEY", + + [SEM_ERR_CDNSKEY_NONE] = + "missing CDNSKEY", + [SEM_ERR_CDNSKEY_MULTIPLE] = + "multiple CDNSKEY records", + [SEM_ERR_CDNSKEY_NO_DNSKEY] = + "CDNSKEY not match DNSKEY", + + [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, + OPTIONAL = 1 << 1, + NSEC = 1 << 2, + NSEC3 = 1 << 3, +} check_level_t; + +typedef struct { + zone_contents_t *zone; + sem_handler_t *handler; + const zone_node_t *next_nsec; + check_level_t level; + time_t time; +} semchecks_data_t; + +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_submission(const zone_node_t *node, semchecks_data_t *data); +static int check_ds(const zone_node_t *node, semchecks_data_t *data); +static int check_nsec(const zone_node_t *node, semchecks_data_t *data); +static int check_nsec3(const zone_node_t *node, semchecks_data_t *data); +static int check_nsec3_opt_out(const zone_node_t *node, semchecks_data_t *data); +static int check_rrsig(const zone_node_t *node, semchecks_data_t *data); +static int check_rrsig_signed(const zone_node_t *node, semchecks_data_t *data); +static int check_nsec_bitmap(const zone_node_t *node, semchecks_data_t *data); +static int check_nsec3_presence(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; +}; + +/* List of function callbacks for defined check_level */ +static const struct check_function CHECK_FUNCTIONS[] = { + { check_cname, MANDATORY }, + { check_dname, MANDATORY }, + { check_delegation, OPTIONAL }, + { check_submission, OPTIONAL }, + { check_ds, OPTIONAL }, + { check_rrsig, NSEC | NSEC3 }, + { check_rrsig_signed, NSEC | NSEC3 }, + { check_nsec_bitmap, NSEC | NSEC3 }, + { check_nsec, NSEC }, + { check_nsec3, NSEC3 }, + { check_nsec3_presence, NSEC3 }, + { check_nsec3_opt_out, NSEC3 }, +}; + +static const int CHECK_FUNCTIONS_LEN = sizeof(CHECK_FUNCTIONS) + / sizeof(struct check_function); + +static int dnssec_key_from_rdata(dnssec_key_t **key, const knot_dname_t *owner, + const uint8_t *rdata, size_t rdlen) +{ + if (!key || !rdata || rdlen == 0) { + return KNOT_EINVAL; + } + + const dnssec_binary_t binary_key = { + .size = rdlen, + .data = (uint8_t *)rdata + }; + + dnssec_key_t *new_key = NULL; + int ret = dnssec_key_new(&new_key); + if (ret != DNSSEC_EOK) { + return KNOT_ENOMEM; + } + ret = dnssec_key_set_rdata(new_key, &binary_key); + if (ret != DNSSEC_EOK) { + dnssec_key_free(new_key); + return KNOT_ENOMEM; + } + if (owner) { + ret = dnssec_key_set_dname(new_key, owner); + if (ret != DNSSEC_EOK) { + dnssec_key_free(new_key); + return KNOT_ENOMEM; + } + } + + *key = new_key; + return KNOT_EOK; +} + +static int check_signature(const knot_rdata_t *rrsig, const dnssec_key_t *key, + const knot_rrset_t *covered) +{ + if (!rrsig || !key || !dnssec_key_can_verify(key)) { + return KNOT_EINVAL; + } + + int ret = KNOT_EOK; + dnssec_sign_ctx_t *sign_ctx = NULL; + + dnssec_binary_t signature = { + .size = knot_rrsig_signature_len(rrsig), + .data = (uint8_t *)knot_rrsig_signature(rrsig) + }; + if (!signature.data || !signature.size) { + ret = KNOT_EINVAL; + goto fail; + } + + if (dnssec_sign_new(&sign_ctx, key) != KNOT_EOK) { + ret = KNOT_ENOMEM; + goto fail; + } + + if (knot_sign_ctx_add_data(sign_ctx, rrsig->data, covered) != KNOT_EOK) { + ret = KNOT_ENOMEM; + goto fail; + } + + if (dnssec_sign_verify(sign_ctx, &signature) != KNOT_EOK) { + ret = KNOT_EINVAL; + goto fail; + } + +fail: + dnssec_sign_free(sign_ctx); + return ret; +} + +/*! + * \brief Semantic check - RRSIG rdata. + * + * \param handler Pointer on function to be called in case of negative check. + * \param zone The zone the rrset is in. + * \param node The node in the zone contents. + * \param rrsig RRSIG rdata. + * \param rrset RRSet signed by the RRSIG. + * \param context The time stamp we check the rrsig validity according to. + * \param level Level of the check. + * \param verified Out: the RRSIG has been verified to be signed by existing DNSKEY. + * + * \retval KNOT_EOK on success. + * \return Appropriate error code if error was found. + */ +static int check_rrsig_rdata(sem_handler_t *handler, + const zone_contents_t *zone, + const zone_node_t *node, + const knot_rdata_t *rrsig, + const knot_rrset_t *rrset, + time_t context, + check_level_t level, + bool *verified) +{ + /* Prepare additional info string. */ + char info_str[50] = { '\0' }; + char type_str[16] = { '\0' }; + knot_rrtype_to_string(rrset->type, type_str, sizeof(type_str)); + int ret = snprintf(info_str, sizeof(info_str), "(record type %s)", type_str); + if (ret < 0 || ret >= sizeof(info_str)) { + return KNOT_ENOMEM; + } + + if (knot_rrsig_type_covered(rrsig) != rrset->type) { + handler->cb(handler, zone, node, SEM_ERR_RRSIG_RDATA_TYPE_COVERED, + info_str); + } + + /* label number at the 2nd index should be same as owner's */ + uint8_t labels_rdata = knot_rrsig_labels(rrsig); + + size_t tmp = knot_dname_labels(rrset->owner, NULL) - labels_rdata; + if (tmp != 0) { + /* if name has wildcard, label must not be included */ + if (!knot_dname_is_wildcard(rrset->owner)) { + handler->cb(handler, zone, node, SEM_ERR_RRSIG_RDATA_LABELS, + info_str); + } else if (tmp != 1) { + handler->cb(handler, zone, node, SEM_ERR_RRSIG_RDATA_LABELS, + info_str); + } + } + + /* Check original TTL. */ + uint32_t original_ttl = knot_rrsig_original_ttl(rrsig); + if (original_ttl != rrset->ttl) { + handler->cb(handler, zone, node, SEM_ERR_RRSIG_RDATA_TTL, + info_str); + } + + /* Check for expired signature. */ + if (knot_rrsig_sig_expiration(rrsig) < context) { + handler->cb(handler, zone, node, SEM_ERR_RRSIG_RDATA_EXPIRATION, + info_str); + } + + /* Check inception */ + if (knot_rrsig_sig_inception(rrsig) > context) { + handler->cb(handler, zone, node, SEM_ERR_RRSIG_RDATA_INCEPTION, + info_str); + } + + /* Check signer name. */ + const knot_dname_t *signer = knot_rrsig_signer_name(rrsig); + if (!knot_dname_is_equal(signer, zone->apex->owner)) { + handler->cb(handler, zone, node, SEM_ERR_RRSIG_RDATA_OWNER, + info_str); + } + + /* Verify with public key - only one RRSIG of covered record needed */ + if (level & OPTIONAL && !*verified) { + const knot_rdataset_t *dnskeys = node_rdataset(zone->apex, KNOT_RRTYPE_DNSKEY); + if (dnskeys == NULL) { + return KNOT_EOK; + } + + for (int i = 0; i < dnskeys->count; i++) { + knot_rdata_t *dnskey = knot_rdataset_at(dnskeys, i); + uint16_t flags = knot_dnskey_flags(dnskey); + uint8_t proto = knot_dnskey_proto(dnskey); + /* RFC 4034 2.1.1 & 2.1.2 */ + if (flags & DNSKEY_FLAGS_ZSK && proto == 3) { + dnssec_key_t *key; + + ret = dnssec_key_from_rdata(&key, zone->apex->owner, + dnskey->data, dnskey->len); + if (ret != KNOT_EOK) { + continue; + } + + ret = check_signature(rrsig, key, rrset); + dnssec_key_free(key); + if (ret == KNOT_EOK) { + *verified = true; + break; + } + } + } + } + + return KNOT_EOK; +} + +static int check_rrsig_signed(const zone_node_t *node, semchecks_data_t *data) +{ + /* signed rrsig - nonsense */ + if (node_rrtype_is_signed(node, KNOT_RRTYPE_RRSIG)) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_RRSIG_SIGNED, NULL); + } + + return KNOT_EOK; +} +/*! + * \brief Semantic check - RRSet's RRSIG. + * + * \param handler Pointer on function to be called in case of negative check. + * \param zone The zone the rrset is in. + * \param node The node in the zone contents. + * \param rrset RRSet signed by the RRSIG. + * \param context The time stamp we check the rrsig validity according to. + * \param level Level of the check. + * + * \retval KNOT_EOK on success. + * \return Appropriate error code if error was found. + */ +static int check_rrsig_in_rrset(sem_handler_t *handler, + const zone_contents_t *zone, + const zone_node_t *node, + const knot_rrset_t *rrset, + time_t context, + check_level_t level) +{ + if (handler == NULL || node == NULL || rrset == NULL) { + return KNOT_EINVAL; + } + /* Prepare additional info string. */ + char info_str[50] = { '\0' }; + char type_str[16] = { '\0' }; + knot_rrtype_to_string(rrset->type, type_str, sizeof(type_str)); + int ret = snprintf(info_str, sizeof(info_str), "(record type %s)", type_str); + if (ret < 0 || ret >= sizeof(info_str)) { + return KNOT_ENOMEM; + } + + knot_rrset_t node_rrsigs = node_rrset(node, KNOT_RRTYPE_RRSIG); + + knot_rdataset_t rrsigs; + knot_rdataset_init(&rrsigs); + ret = knot_synth_rrsig(rrset->type, &node_rrsigs.rrs, &rrsigs, NULL); + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + return ret; + } + if (ret == KNOT_ENOENT) { + handler->cb(handler, zone, node, SEM_ERR_RRSIG_NO_RRSIG, info_str); + return KNOT_EOK; + } + + bool verified = false; + knot_rdata_t *rrsig = rrsigs.rdata; + for (uint16_t i = 0; ret == KNOT_EOK && i < rrsigs.count; ++i) { + ret = check_rrsig_rdata(handler, zone, node, rrsig, rrset, + context, level, &verified); + rrsig = knot_rdataset_next(rrsig); + } + /* Only one rrsig of covered record needs to be verified by DNSKEY. */ + if (!verified) { + handler->cb(handler, zone, node, SEM_ERR_RRSIG_UNVERIFIABLE, + info_str); + } + + knot_rdataset_clear(&rrsigs, NULL); + return KNOT_EOK;; +} + +/*! + * \brief Check if glue record for delegation is present. + * + * Also check if there is NS record in the zone. + * \param node Node to check + * \param data Semantic checks context data + */ +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; + } + + 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, + 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); + if (knot_dname_in_bailiwick(ns_dname, node->owner) <= 0) { + continue; + } + + const zone_node_t *glue_node = + zone_contents_find_node(data->zone, ns_dname); + + if (glue_node == NULL) { + /* Try wildcard ([1]* + suffix). */ + knot_dname_t wildcard[KNOT_DNAME_MAXLEN]; + memcpy(wildcard, "\x1""*", 2); + knot_dname_to_wire(wildcard + 2, + knot_wire_next_label(ns_dname, NULL), + sizeof(wildcard) - 2); + glue_node = zone_contents_find_node(data->zone, wildcard); + } + 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, + SEM_ERR_NS_GLUE, NULL); + } + } + + return KNOT_EOK; +} + +/*! + * \brief check_submission_records Check CDS and CDNSKEY + */ +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, + SEM_ERR_CDS_NONE, NULL); + return KNOT_EOK; + } else if (cdnskeys == NULL) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_CDNSKEY_NONE, NULL); + return KNOT_EOK; + } + + if (cdss->count != 1) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_CDS_MULTIPLE, NULL); + } + if (cdnskeys->count != 1) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_CDNSKEY_MULTIPLE, NULL); + } + + knot_rdata_t *cdnskey = cdnskeys->rdata; + knot_rdata_t *cds = cdss->rdata; + uint8_t digest_type = knot_ds_digest_type(cdss->rdata); + + const knot_rdataset_t *dnskeys = node_rdataset(data->zone->apex, + KNOT_RRTYPE_DNSKEY); + for (int i = 0; dnskeys != NULL && i < dnskeys->count; i++) { + knot_rdata_t *dnskey = knot_rdataset_at(dnskeys, i); + if (knot_rdata_cmp(dnskey, cdnskey) != 0) { + continue; + } + + dnssec_key_t *key; + int ret = dnssec_key_from_rdata(&key, data->zone->apex->owner, + dnskey->data, dnskey->len); + if (ret != KNOT_EOK) { + return ret; + } + + 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) { + return KNOT_EOK; + } else { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_CDS_NOT_MATCH, NULL); + } + } + + if (dnskeys != NULL) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_CDNSKEY_NO_DNSKEY, NULL); + } + return KNOT_EOK; +} + +/*! + * \brief Semantic check - DS record. + * + * \param node Node to check + * \param data Semantic checks context data + * + * \retval KNOT_EOK on success. + * \return Appropriate error code if error was found. + */ +static int check_ds(const zone_node_t *node, semchecks_data_t *data) +{ + const knot_rdataset_t *dss = node_rdataset(node, KNOT_RRTYPE_DS); + if (dss == NULL) { + return KNOT_EOK; + } + + for (int i = 0; i < dss->count; i++) { + knot_rdata_t *ds = knot_rdataset_at(dss, i); + uint16_t keytag = knot_ds_key_tag(ds); + uint8_t digest_type = knot_ds_digest_type(ds); + + char info[100] = ""; + (void)snprintf(info, sizeof(info), "(keytag %d)", keytag); + + if (!dnssec_algorithm_digest_support(digest_type)) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_DS_RDATA_ALG, info); + } else { + // Sizes for different digest algorithms. + const uint16_t digest_sizes [] = { 0, 20, 32, 32, 48}; + + uint16_t digest_size = knot_ds_digest_len(ds); + + if (digest_sizes[digest_type] != digest_size) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_DS_RDATA_DIGLEN, info); + } + } + } + + return KNOT_EOK; +} + +/*! + * \brief Run all semantic check related to RRSIG record + * + * \param node Node to check + * \param data Semantic checks context data + */ +static int check_rrsig(const zone_node_t *node, semchecks_data_t *data) +{ + assert(node); + if (node->flags & NODE_FLAGS_NONAUTH) { + return KNOT_EOK; + } + + bool deleg = node->flags & NODE_FLAGS_DELEG; + + int ret = KNOT_EOK; + + int rrset_count = node->rrset_count; + for (int i = 0; ret == KNOT_EOK && i < rrset_count; i++) { + knot_rrset_t rrset = node_rrset_at(node, i); + if (rrset.type == KNOT_RRTYPE_RRSIG) { + continue; + } + if (deleg && rrset.type != KNOT_RRTYPE_NSEC && + rrset.type != KNOT_RRTYPE_DS ) { + continue; + } + + ret = check_rrsig_in_rrset(data->handler, data->zone, node, &rrset, + data->time, data->level); + } + return ret; +} + +/*! + * \brief Add all RR types from a node into the bitmap. + */ +static void bitmap_add_all_node_rrsets(dnssec_nsec_bitmap_t *bitmap, + const zone_node_t *node) +{ + bool deleg = node->flags & NODE_FLAGS_DELEG; + for (int i = 0; i < node->rrset_count; i++) { + knot_rrset_t rr = node_rrset_at(node, i); + if (deleg && (rr.type != KNOT_RRTYPE_NS && + rr.type != KNOT_RRTYPE_DS && + rr.type != KNOT_RRTYPE_NSEC && + rr.type != KNOT_RRTYPE_RRSIG)) { + continue; + } + dnssec_nsec_bitmap_add(bitmap, rr.type); + } +} + +static char *nsec3_info(const knot_dname_t *owner, char *out, size_t out_len) +{ + char buff[KNOT_DNAME_TXT_MAXLEN + 1]; + char *str = knot_dname_to_str(buff, owner, sizeof(buff)); + if (str == NULL) { + return NULL; + } + + int ret = snprintf(out, out_len, "(NSEC3 owner=%s)", str); + if (ret <= 0 || ret >= out_len) { + return NULL; + } + + return out; +} + +/*! + * \brief Check NSEC and NSEC3 type bitmap + * + * \param node Node to check + * \param data Semantic checks context data + */ +static int check_nsec_bitmap(const zone_node_t *node, semchecks_data_t *data) +{ + assert(node); + if (node->flags & NODE_FLAGS_NONAUTH) { + return KNOT_EOK; + } + + bool nsec = data->level & NSEC; + knot_rdataset_t *nsec_rrs = NULL; + + if (nsec) { + nsec_rrs = node_rdataset(node, KNOT_RRTYPE_NSEC); + } else if (node->nsec3_node != NULL) { + nsec_rrs = node_rdataset(node->nsec3_node, KNOT_RRTYPE_NSEC3); + } + if (nsec_rrs == NULL) { + return KNOT_EOK; + } + + // create NSEC bitmap from node + dnssec_nsec_bitmap_t *node_bitmap = dnssec_nsec_bitmap_new(); + if (node_bitmap == NULL) { + return KNOT_ENOMEM; + } + bitmap_add_all_node_rrsets(node_bitmap, node); + + uint16_t node_wire_size = dnssec_nsec_bitmap_size(node_bitmap); + uint8_t *node_wire = malloc(node_wire_size); + if (node_wire == NULL) { + dnssec_nsec_bitmap_free(node_bitmap); + return KNOT_ENOMEM; + } + dnssec_nsec_bitmap_write(node_bitmap, node_wire); + dnssec_nsec_bitmap_free(node_bitmap); + + // get NSEC bitmap from NSEC node + const uint8_t *nsec_wire = NULL; + uint16_t nsec_wire_size = 0; + if (nsec) { + nsec_wire = knot_nsec_bitmap(nsec_rrs->rdata); + nsec_wire_size = knot_nsec_bitmap_len(nsec_rrs->rdata); + } else { + nsec_wire = knot_nsec3_bitmap(nsec_rrs->rdata); + nsec_wire_size = knot_nsec3_bitmap_len(nsec_rrs->rdata); + } + + if (node_wire_size != nsec_wire_size || + memcmp(node_wire, nsec_wire, node_wire_size) != 0) { + char buff[50 + KNOT_DNAME_TXT_MAXLEN]; + char *info = nsec ? NULL : nsec3_info(node->nsec3_node->owner, + buff, sizeof(buff)); + data->handler->cb(data->handler, data->zone, node, + (nsec ? SEM_ERR_NSEC_RDATA_BITMAP : SEM_ERR_NSEC3_RDATA_BITMAP), + info); + } + + free(node_wire); + return KNOT_EOK; +} + +/*! + * \brief Run NSEC related semantic checks + * + * \param node Node to check + * \param data Semantic checks context data + */ +static int check_nsec(const zone_node_t *node, semchecks_data_t *data) +{ + assert(node); + if (node->flags & NODE_FLAGS_NONAUTH) { + return KNOT_EOK; + } + + if (node->rrset_count == 0) { // empty nonterminal + return KNOT_EOK; + } + + /* check for NSEC record */ + const knot_rdataset_t *nsec_rrs = node_rdataset(node, KNOT_RRTYPE_NSEC); + if (nsec_rrs == NULL) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC_NONE, NULL); + return KNOT_EOK; + } + + /* Test that only one record is in the NSEC RRSet */ + if (nsec_rrs->count != 1) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC_RDATA_MULTIPLE, NULL); + } + + if (data->next_nsec != node) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC_RDATA_CHAIN, NULL); + } + + /* + * Test that NSEC chain is coherent. + * We have already checked that every + * authoritative node contains NSEC record + * so checking should only be matter of testing + * the next link in each node. + */ + const knot_dname_t *next_domain = knot_nsec_next(nsec_rrs->rdata); + + data->next_nsec = zone_contents_find_node(data->zone, next_domain); + if (data->next_nsec == NULL) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC_RDATA_CHAIN, NULL); + } + + return KNOT_EOK; +} + +/*! + * \brief Check if node has NSEC3 node. + * + * \param node Node to check + * \param data Semantic checks context data + */ +static int check_nsec3_presence(const zone_node_t *node, semchecks_data_t *data) +{ + bool auth = (node->flags & NODE_FLAGS_NONAUTH) == 0; + bool deleg = (node->flags & NODE_FLAGS_DELEG) != 0; + + if ((deleg && node_rrtype_exists(node, KNOT_RRTYPE_DS)) || (auth && !deleg)) { + if (node->nsec3_node == NULL) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC3_NONE, NULL); + } + } + + return KNOT_EOK; +} + +/*! + * \brief Check NSEC3 opt-out. + * + * \param node Node to check + * \param data Semantic checks context data + */ +static int check_nsec3_opt_out(const zone_node_t *node, semchecks_data_t *data) +{ + if (!(node->nsec3_node == NULL && node->flags & NODE_FLAGS_DELEG)) { + return KNOT_EOK; + } + /* Insecure delegation, check whether it is part of opt-out span. */ + + const zone_node_t *nsec3_previous = NULL; + const zone_node_t *nsec3_node; + zone_contents_find_nsec3_for_name(data->zone, node->owner, &nsec3_node, + &nsec3_previous); + + if (nsec3_previous == NULL) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC3_NONE, NULL); + return KNOT_EOK; + } + + const knot_rdataset_t *previous_rrs; + previous_rrs = node_rdataset(nsec3_previous, KNOT_RRTYPE_NSEC3); + assert(previous_rrs); + + /* Check for opt-out flag. */ + uint8_t flags = knot_nsec3_flags(previous_rrs->rdata); + if (!(flags & 1)) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC3_INSECURE_DELEGATION_OPT, NULL); + } + + return KNOT_EOK; +} + +/*! + * \brief Run checks related to NSEC3. + * + * Check NSEC3 node for given node. + * Check if NSEC3 chain is coherent and cyclic. + * \param node Node to check + * \param data Semantic checks context data + */ +static int check_nsec3(const zone_node_t *node, semchecks_data_t *data) +{ + assert(node); + bool auth = (node->flags & NODE_FLAGS_NONAUTH) == 0; + bool deleg = (node->flags & NODE_FLAGS_DELEG) != 0; + + if (!auth && !deleg) { + return KNOT_EOK; + } + if (node->nsec3_node == NULL) { + return KNOT_EOK; + } + + dnssec_nsec3_params_t params_apex = { 0 }; + int ret = KNOT_EOK; + + char buff[50 + KNOT_DNAME_TXT_MAXLEN]; + char *info = nsec3_info(node->nsec3_node->owner, buff, sizeof(buff)); + + knot_rrset_t nsec3_rrs = node_rrset(node->nsec3_node, KNOT_RRTYPE_NSEC3); + if (knot_rrset_empty(&nsec3_rrs)) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC3_NONE, info); + goto nsec3_cleanup; + } + + const knot_rdataset_t *soa_rrs = node_rdataset(data->zone->apex, KNOT_RRTYPE_SOA); + assert(soa_rrs); + uint32_t minimum_ttl = knot_soa_minimum(soa_rrs->rdata); + if (nsec3_rrs.ttl != minimum_ttl) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC3_RDATA_TTL, info); + } + + // Check parameters. + const knot_rdataset_t *nsec3param = node_rdataset(data->zone->apex, + KNOT_RRTYPE_NSEC3PARAM); + dnssec_binary_t rdata = { + .size = nsec3param->rdata->len, + .data = nsec3param->rdata->data + }; + ret = dnssec_nsec3_params_from_rdata(¶ms_apex, &rdata); + if (ret != DNSSEC_EOK) { + ret = knot_error_from_libdnssec(ret); + goto nsec3_cleanup; + } + + if (knot_nsec3_flags(nsec3_rrs.rrs.rdata) > 1) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC3_RDATA_FLAGS, info); + } + + dnssec_binary_t salt = { + .size = knot_nsec3_salt_len(nsec3_rrs.rrs.rdata), + .data = (uint8_t *)knot_nsec3_salt(nsec3_rrs.rrs.rdata), + }; + + if (dnssec_binary_cmp(&salt, ¶ms_apex.salt)) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC3_RDATA_SALT, info); + } + + if (knot_nsec3_alg(nsec3_rrs.rrs.rdata) != params_apex.algorithm) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC3_RDATA_ALG, info); + } + + if (knot_nsec3_iters(nsec3_rrs.rrs.rdata) != params_apex.iterations) { + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC3_RDATA_ITERS, info); + } + + // Get next nsec3 node. + const zone_node_t *apex = data->zone->apex; + const uint8_t *next_dname_str = knot_nsec3_next(nsec3_rrs.rrs.rdata); + uint8_t next_dname_str_size = knot_nsec3_next_len(nsec3_rrs.rrs.rdata); + uint8_t next_dname[KNOT_DNAME_MAXLEN]; + ret = knot_nsec3_hash_to_dname(next_dname, sizeof(next_dname), + next_dname_str, next_dname_str_size, + apex->owner); + if (ret != KNOT_EOK) { + goto nsec3_cleanup; + } + + const zone_node_t *next_nsec3 = zone_contents_find_nsec3_node(data->zone, + next_dname); + if (next_nsec3 == NULL || next_nsec3->prev != node->nsec3_node) { + uint8_t *next = NULL; + int32_t next_len = base32hex_encode_alloc(next_dname_str, + next_dname_str_size, + &next); + char *hash_info = NULL; + if (next != NULL) { + hash_info = sprintf_alloc("(next hash %.*s)", next_len, next); + free(next); + } + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_NSEC3_RDATA_CHAIN, hash_info); + free(hash_info); + } + + ret = check_rrsig(node->nsec3_node, data); + if (ret != KNOT_EOK) { + goto nsec3_cleanup; + } + + // Check that the node only contains NSEC3 and RRSIG. + for (int i = 0; ret == KNOT_EOK && i < node->nsec3_node->rrset_count; i++) { + knot_rrset_t rrset = node_rrset_at(node->nsec3_node, i); + uint16_t type = rrset.type; + if (type != KNOT_RRTYPE_NSEC3 && type != KNOT_RRTYPE_RRSIG) { + data->handler->cb(data->handler, data->zone, node->nsec3_node, + SEM_ERR_NSEC3_EXTRA_RECORD, NULL); + } + } + +nsec3_cleanup: + dnssec_nsec3_params_free(¶ms_apex); + + return ret; +} + +/*! + * \brief Check if CNAME record contains other records + * + * \param node Node to check + * \param data Semantic checks context data + */ +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->fatal_error = true; + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_CNAME_EXTRA_RECORDS, NULL); + } + if (cname_rrs->count != 1) { + data->handler->fatal_error = true; + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_CNAME_MULTIPLE, NULL); + } + + return KNOT_EOK; +} + +/*! + * \brief Check if DNAME record has children. + * + * \param node Node to check + * \param data Semantic checks context data + */ +static int check_dname(const zone_node_t *node, semchecks_data_t *data) +{ + if (node->parent != NULL && node_rrtype_exists(node->parent, KNOT_RRTYPE_DNAME)) { + data->handler->fatal_error = true; + data->handler->cb(data->handler, data->zone, node, + SEM_ERR_DNAME_CHILDREN, NULL); + } + + return KNOT_EOK; +} + +/*! + * \brief Check that NSEC chain is cyclic. + * + * Run only once per zone. Check that last NSEC node points to first one. + * \param data Semantic checks context data + */ +static int check_nsec_cyclic(semchecks_data_t *data) +{ + if (data->next_nsec == NULL) { + data->handler->cb(data->handler, data->zone, data->zone->apex, + SEM_ERR_NSEC_RDATA_CHAIN, NULL); + return KNOT_EOK; + } + if (!knot_dname_is_equal(data->next_nsec->owner, data->zone->apex->owner)) { + data->handler->cb(data->handler, data->zone, data->next_nsec, + SEM_ERR_NSEC_RDATA_CHAIN, NULL); + } + + return KNOT_EOK; +} + +/*! + * \brief Call all semantic checks for each node. + * + * This function is called as callback from zone_contents_tree_apply_inorder. + * Checks are functions from global const array check_functions. + * + * \param node Node to be checked + * \param data Semantic checks context data + */ +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); + } + } + + + return ret; +} + +static void check_nsec3param(knot_rdataset_t *nsec3param, zone_contents_t *zone, + sem_handler_t *handler, semchecks_data_t *data) +{ + assert(nsec3param); + + data->level |= NSEC3; + uint8_t param = knot_nsec3param_flags(nsec3param->rdata); + if ((param & ~1) != 0) { + handler->cb(handler, zone, zone->apex, SEM_ERR_NSEC3PARAM_RDATA_FLAGS, + NULL); + } + + param = knot_nsec3param_alg(nsec3param->rdata); + if (param != DNSSEC_NSEC3_ALGORITHM_SHA1) { + handler->cb(handler, zone, zone->apex, SEM_ERR_NSEC3PARAM_RDATA_ALG, + NULL); + } +} + +static void check_dnskey(zone_contents_t *zone, sem_handler_t *handler) +{ + const knot_rdataset_t *dnskeys = node_rdataset(zone->apex, KNOT_RRTYPE_DNSKEY); + if (dnskeys == NULL) { + handler->cb(handler, zone, zone->apex, SEM_ERR_DNSKEY_NONE, NULL); + return; + } + + for (int i = 0; i < dnskeys->count; i++) { + knot_rdata_t *dnskey = knot_rdataset_at(dnskeys, i); + dnssec_key_t *key; + int ret = dnssec_key_from_rdata(&key, zone->apex->owner, + dnskey->data, dnskey->len); + if (ret == KNOT_EOK) { + dnssec_key_free(key); + } else { + handler->cb(handler, zone, zone->apex, SEM_ERR_DNSKEY_INVALID, NULL); + } + + if (knot_dnskey_proto(dnskey) != 3) { + handler->cb(handler, zone, zone->apex, SEM_ERR_DNSKEY_RDATA_PROTOCOL, + NULL); + } + + dnssec_key_algorithm_t alg = knot_dnskey_alg(dnskey); + if (!dnssec_algorithm_key_support(alg)) { + char *info = sprintf_alloc("(unsupported algorithm %d)", alg); + handler->cb(handler, zone, zone->apex, SEM_ERR_DNSKEY_INVALID, info); + free(info); + } + } +} + +int sem_checks_process(zone_contents_t *zone, bool optional, sem_handler_t *handler, + time_t time) +{ + if (zone == NULL || handler == NULL) { + return KNOT_EINVAL; + } + + semchecks_data_t data = { + .handler = handler, + .zone = zone, + .next_nsec = zone->apex, + .level = MANDATORY, + .time = time, + }; + + if (optional) { + data.level |= OPTIONAL; + if (zone->dnssec) { + knot_rdataset_t *nsec3param = node_rdataset(zone->apex, + KNOT_RRTYPE_NSEC3PARAM); + if (nsec3param != NULL) { + data.level |= NSEC3; + check_nsec3param(nsec3param, zone, handler, &data); + } else { + data.level |= NSEC; + } + check_dnskey(zone, handler); + } + } + + 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; + } + + // check cyclic chain after every node was checked + if (data.level & NSEC) { + check_nsec_cyclic(&data); + } + if (data.handler->fatal_error) { + return KNOT_ESEMCHECK; + } + + return KNOT_EOK; +} diff --git a/src/knot/zone/semantic-check.h b/src/knot/zone/semantic-check.h new file mode 100644 index 0000000..2010ec9 --- /dev/null +++ b/src/knot/zone/semantic-check.h @@ -0,0 +1,123 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/zone/node.h" +#include "knot/zone/contents.h" + +/*! + *\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, + + // Optional checks. + SEM_ERR_NS_APEX, + SEM_ERR_NS_GLUE, + + SEM_ERR_RRSIG_RDATA_TYPE_COVERED, + SEM_ERR_RRSIG_RDATA_TTL, + SEM_ERR_RRSIG_RDATA_EXPIRATION, + SEM_ERR_RRSIG_RDATA_INCEPTION, + SEM_ERR_RRSIG_RDATA_LABELS, + SEM_ERR_RRSIG_RDATA_OWNER, + SEM_ERR_RRSIG_NO_RRSIG, + SEM_ERR_RRSIG_SIGNED, + SEM_ERR_RRSIG_TTL, + SEM_ERR_RRSIG_UNVERIFIABLE, + + SEM_ERR_NSEC_NONE, + SEM_ERR_NSEC_RDATA_BITMAP, + SEM_ERR_NSEC_RDATA_MULTIPLE, + SEM_ERR_NSEC_RDATA_CHAIN, + + SEM_ERR_NSEC3_NONE, + SEM_ERR_NSEC3_INSECURE_DELEGATION_OPT, + SEM_ERR_NSEC3_EXTRA_RECORD, + SEM_ERR_NSEC3_RDATA_TTL, + SEM_ERR_NSEC3_RDATA_CHAIN, + SEM_ERR_NSEC3_RDATA_BITMAP, + SEM_ERR_NSEC3_RDATA_FLAGS, + SEM_ERR_NSEC3_RDATA_SALT, + SEM_ERR_NSEC3_RDATA_ALG, + SEM_ERR_NSEC3_RDATA_ITERS, + + SEM_ERR_NSEC3PARAM_RDATA_FLAGS, + SEM_ERR_NSEC3PARAM_RDATA_ALG, + + SEM_ERR_DS_RDATA_ALG, + SEM_ERR_DS_RDATA_DIGLEN, + + SEM_ERR_DNSKEY_NONE, + SEM_ERR_DNSKEY_INVALID, + SEM_ERR_DNSKEY_RDATA_PROTOCOL, + + SEM_ERR_CDS_NONE, + SEM_ERR_CDS_MULTIPLE, + SEM_ERR_CDS_NOT_MATCH, + + SEM_ERR_CDNSKEY_NONE, + SEM_ERR_CDNSKEY_MULTIPLE, + SEM_ERR_CDNSKEY_NO_DNSKEY, + + // 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. + * + * Return KNOT_EOK to continue in semantic checks. + * Return other KNOT_E* to stop semantic check with error. + */ +typedef void (*sem_callback) (sem_handler_t *ctx, const zone_contents_t *zone, + const zone_node_t *node, sem_error_t error, const char *data); + +struct sem_handler { + sem_callback cb; + bool fatal_error; +}; + +/*! + * \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_EINVAL or other error + */ +int sem_checks_process(zone_contents_t *zone, bool 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..9846f92 --- /dev/null +++ b/src/knot/zone/serial.c @@ -0,0 +1,84 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <assert.h> +#include <time.h> + +#include "knot/conf/conf.h" +#include "knot/zone/serial.h" + +static const serial_cmp_result_t diffbrief2result[4] = { + [0] = SERIAL_EQUAL, + [1] = SERIAL_GREATER, + [2] = SERIAL_INCOMPARABLE, + [3] = SERIAL_LOWER, +}; + +serial_cmp_result_t serial_compare(uint32_t s1, uint32_t s2) +{ + uint64_t diff = ((uint64_t)s1 + ((uint64_t)1 << 32) - s2) & 0xffffffff; + int diffbrief = (diff >> 31 << 1) | ((diff & 0x7fffffff) ? 1 : 0); + assert(diffbrief > -1 && diffbrief < 4); + return diffbrief2result[diffbrief]; +} + +static uint32_t serial_next_date(uint32_t current) +{ + uint32_t next = current + 1; + + struct tm now; + time_t current_time = time(NULL); + struct tm *gmtime_result = gmtime_r(¤t_time, &now); + if (gmtime_result == NULL) { + return next; + } + + uint32_t yyyyMMdd00 = (1900 + now.tm_year) * 1000000 + + ( 1 + now.tm_mon ) * 10000 + + ( now.tm_mday) * 100; + + if (next < yyyyMMdd00) { + next = yyyyMMdd00; + } + + return next; +} + +uint32_t serial_next(uint32_t current, int policy) +{ + uint32_t candidate; + switch (policy) { + case SERIAL_POLICY_INCREMENT: + return current + 1; + case SERIAL_POLICY_UNIXTIME: + candidate = time(NULL); + if (serial_compare(candidate, current) != SERIAL_GREATER) { + return current + 1; + } else { + return candidate; + } + case SERIAL_POLICY_DATESERIAL: + return serial_next_date(current); + default: + assert(0); + return 0; + } +} + +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..3453de8 --- /dev/null +++ b/src/knot/zone/serial.h @@ -0,0 +1,72 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include <stdbool.h> +#include <stdint.h> + +/*! + * \brief result of serial comparison. LOWER means that the first serial is lower that the second. + * + * Example: (serial_compare(a, b) & SERIAL_MASK_LEQ) means "a <= b". + */ +typedef enum { + SERIAL_INCOMPARABLE = 0x0, + SERIAL_LOWER = 0x1, + SERIAL_GREATER = 0x2, + SERIAL_EQUAL = 0x3, + SERIAL_MASK_LEQ = SERIAL_LOWER, + SERIAL_MASK_GEQ = SERIAL_GREATER, +} serial_cmp_result_t; + +/*! + * \brief Compares two zone serials. + */ +serial_cmp_result_t serial_compare(uint32_t s1, uint32_t s2); + +inline static bool serial_equal(uint32_t a, uint32_t b) +{ + return serial_compare(a, b) == SERIAL_EQUAL; +} + +/*! + * \brief Get next serial for given serial update policy. + * + * \param current Current SOA serial. + * \param policy SERIAL_POLICY_INCREMENT, SERIAL_POLICY_UNIXTIME or + * SERIAL_POLICY_DATESERIAL. + * + * \return New serial. + */ +uint32_t serial_next(uint32_t current, int policy); + +typedef struct { + uint32_t serial; + bool valid; +} kserial_t; + +/*! + * \brief Compares two kserials. + * + * If any of them is invalid, they are INCOMPARABLE. + */ +serial_cmp_result_t kserial_cmp(kserial_t a, kserial_t b); + +inline static bool kserial_equal(kserial_t a, kserial_t b) +{ + return kserial_cmp(a, b) == SERIAL_EQUAL; +} diff --git a/src/knot/zone/timers.c b/src/knot/zone/timers.c new file mode 100644 index 0000000..0ff8aed --- /dev/null +++ b/src/knot/zone/timers.c @@ -0,0 +1,288 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "knot/zone/timers.h" + +#include "contrib/wire_ctx.h" +#include "libknot/db/db.h" +#include "libknot/db/db_lmdb.h" + +/* + * # Timer database + * + * Timer database stores timestaps 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, + TIMER_LAST_FLUSH, + TIMER_LAST_REFRESH, + TIMER_NEXT_REFRESH, + TIMER_LAST_RESALT, + TIMER_NEXT_PARENT_DS_Q +}; + +#define TIMER_COUNT 6 +#define TIMER_SIZE (sizeof(uint8_t) + sizeof(uint64_t)) +#define SERIALIZED_SIZE (TIMER_COUNT * TIMER_SIZE) + +/*! + * \brief Serialize timers into a binary buffer. + */ +static int serialize_timers(const zone_timers_t *timers, uint8_t *data, size_t size) +{ + if (!timers || !data || size != SERIALIZED_SIZE) { + return KNOT_EINVAL; + } + + wire_ctx_t wire = wire_ctx_init(data, size); + + wire_ctx_write_u8(&wire, TIMER_SOA_EXPIRE); + wire_ctx_write_u64(&wire, timers->soa_expire); + wire_ctx_write_u8(&wire, TIMER_LAST_FLUSH); + wire_ctx_write_u64(&wire, timers->last_flush); + wire_ctx_write_u8(&wire, TIMER_LAST_REFRESH); + wire_ctx_write_u64(&wire, timers->last_refresh); + wire_ctx_write_u8(&wire, TIMER_NEXT_REFRESH); + wire_ctx_write_u64(&wire, timers->next_refresh); + wire_ctx_write_u8(&wire, TIMER_LAST_RESALT); + wire_ctx_write_u64(&wire, timers->last_resalt); + wire_ctx_write_u8(&wire, TIMER_NEXT_PARENT_DS_Q); + wire_ctx_write_u64(&wire, timers->next_parent_ds_q); + + assert(wire.error == KNOT_EOK); + assert(wire_ctx_available(&wire) == 0); + + return KNOT_EOK; +} + +/*! + * \brief Deserialize timers from a binary buffer. + * + * \note Unknown timers are ignored. + */ +static int deserialize_timers(zone_timers_t *timers_ptr, + const uint8_t *data, size_t size) +{ + if (!timers_ptr || !data) { + return KNOT_EINVAL; + } + + zone_timers_t timers = { 0 }; + + wire_ctx_t wire = wire_ctx_init_const(data, size); + while (wire_ctx_available(&wire) >= TIMER_SIZE) { + uint8_t id = wire_ctx_read_u8(&wire); + uint64_t value = wire_ctx_read_u64(&wire); + switch (id) { + case TIMER_SOA_EXPIRE: timers.soa_expire = value; break; + case TIMER_LAST_FLUSH: timers.last_flush = value; break; + case TIMER_LAST_REFRESH: timers.last_refresh = value; break; + case TIMER_NEXT_REFRESH: timers.next_refresh = value; break; + case TIMER_LAST_RESALT: timers.last_resalt = value; break; + case TIMER_NEXT_PARENT_DS_Q: timers.next_parent_ds_q = 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 int txn_write_timers(knot_db_txn_t *txn, const knot_dname_t *zone, + const zone_timers_t *timers) +{ + uint8_t data[SERIALIZED_SIZE] = { 0 }; + int ret = serialize_timers(timers, data, sizeof(data)); + if (ret != KNOT_EOK) { + return ret; + } + + knot_db_val_t key = { (uint8_t *)zone, knot_dname_size(zone) }; + knot_db_val_t val = { data, sizeof(data) }; + + return knot_db_lmdb_api()->insert(txn, &key, &val, 0); +} + +static int txn_read_timers(knot_db_txn_t *txn, const knot_dname_t *zone, + zone_timers_t *timers) +{ + knot_db_val_t key = { (uint8_t *)zone, knot_dname_size(zone) }; + knot_db_val_t val = { 0 }; + int ret = knot_db_lmdb_api()->find(txn, &key, &val, 0); + if (ret != KNOT_EOK) { + return ret; + } + + return deserialize_timers(timers, val.data, val.len); +} + +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_db_t *db, const knot_dname_t *zone, + zone_timers_t *timers) +{ + if (!db || !zone || !timers) { + return KNOT_EINVAL; + } + + const knot_db_api_t *db_api = knot_db_lmdb_api(); + assert(db_api); + + knot_db_txn_t txn = { 0 }; + int ret = db_api->txn_begin(db, &txn, KNOT_DB_RDONLY); + if (ret != KNOT_EOK) { + return ret; + } + + ret = txn_read_timers(&txn, zone, timers); + db_api->txn_abort(&txn); + + return ret; +} + +int zone_timers_write_begin(knot_db_t *db, knot_db_txn_t *txn) +{ + memset(txn, 0, sizeof(*txn)); + return knot_db_lmdb_api()->txn_begin(db, txn, KNOT_DB_SORTED); +} + +int zone_timers_write_end(knot_db_txn_t *txn) +{ + return knot_db_lmdb_api()->txn_commit(txn); +} + +int zone_timers_write(knot_db_t *db, const knot_dname_t *zone, + const zone_timers_t *timers, knot_db_txn_t *txn) +{ + if (!zone || !timers || (!db && !txn)) { + return KNOT_EINVAL; + } + + const knot_db_api_t *db_api = knot_db_lmdb_api(); + assert(db_api); + + knot_db_txn_t static_txn, *mytxn = txn; + if (txn == NULL) { + mytxn = &static_txn; + int ret = db_api->txn_begin(db, mytxn, KNOT_DB_SORTED); + if (ret != KNOT_EOK) { + return ret; + } + } + + int ret = txn_write_timers(mytxn, zone, timers); + if (ret != KNOT_EOK) { + db_api->txn_abort(mytxn); + return ret; + } + + if (txn == NULL) { + db_api->txn_commit(mytxn); + } + + return KNOT_EOK; +} + +int zone_timers_sweep(knot_db_t *db, sweep_cb keep_zone, void *cb_data) +{ + if (!db || !keep_zone) { + return KNOT_EINVAL; + } + + const knot_db_api_t *db_api = knot_db_lmdb_api(); + assert(db_api); + + knot_db_txn_t txn = { 0 }; + int ret = db_api->txn_begin(db, &txn, KNOT_DB_SORTED); + if (ret != KNOT_EOK) { + return ret; + } + + knot_db_iter_t *it = NULL; + for (it = db_api->iter_begin(&txn, 0); it != NULL; it = db_api->iter_next(it)) { + knot_db_val_t key = { 0 }; + ret = db_api->iter_key(it, &key); + if (ret != KNOT_EOK) { + break; + } + + const knot_dname_t *zone = (const knot_dname_t *)key.data; + if (!keep_zone(zone, cb_data)) { + ret = db_api->del(&txn, &key); + if (ret != KNOT_EOK) { + break; + } + } + } + db_api->iter_finish(it); + + if (ret != KNOT_EOK) { + db_api->txn_abort(&txn); + return ret; + } + + return db_api->txn_commit(&txn); +} diff --git a/src/knot/zone/timers.h b/src/knot/zone/timers.h new file mode 100644 index 0000000..4c8d0fa --- /dev/null +++ b/src/knot/zone/timers.h @@ -0,0 +1,119 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include <stdint.h> +#include <time.h> + +#include "libknot/db/db.h" +#include "libknot/dname.h" + +/*! + * \brief Persistent zone timers. + */ +struct zone_timers { + uint32_t soa_expire; //!< SOA expire value. + time_t last_flush; //!< Last zone file synchronization. + time_t last_refresh; //!< Last successful zone refresh attempt. + time_t next_refresh; //!< Next zone refresh attempt. + time_t last_resalt; //!< Last NSEC3 resalt + time_t next_parent_ds_q; //!< Next parent ds query +}; + +typedef struct zone_timers zone_timers_t; + +/*! + * \brief Open zone timers database. + * + * \param[in] path Path to a directory with the database. + * \param[out] db Created database. + * \param[in] mapsize LMDB mapsize. + * + * \return KNOT_E* + */ +int zone_timers_open(const char *path, knot_db_t **db, size_t mapsize); + +/*! + * \brief Closes zone timers database. + * + * \param db Timer database. + */ +void zone_timers_close(knot_db_t *db); + +/*! + * \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_db_t *db, const knot_dname_t *zone, + zone_timers_t *timers); + +/*! + * \brief Init txn for zone_timers_write() + * + * \param db Timer database. + * \param txn Handler to be initialized. + * + * \return KNOT_E* + */ +int zone_timers_write_begin(knot_db_t *db, knot_db_txn_t *txn); + +/*! + * \brief Close txn for zone_timers_write() + * + * \param txn Handler to be closed. + * + * \return KNOT_E* + */ +int zone_timers_write_end(knot_db_txn_t *txn); + +/*! + * \brief Write timers for one zone. + * + * \param db Timer database. + * \param zone Zone name. + * \param timers Loaded timers + * \param txn Transaction handler obtained from zone_timers_write_begin() + * + * \return KNOT_E* + */ +int zone_timers_write(knot_db_t *db, const knot_dname_t *zone, + const zone_timers_t *timers, knot_db_txn_t *txn); + +/*! + * \brief Callback used in \ref zone_timers_sweep. + * + * \retval true for zones to preserve. + * \retval false for zones to remove. + */ +typedef bool (*sweep_cb)(const knot_dname_t *zone, void *data); + +/*! + * \brief Selectively delete zones from the database. + * + * \param db Timer dababase. + * \param keep_zone Filtering callback. + * \param cb_data Data passed to callback function. + * + * \return KNOT_E* + */ +int zone_timers_sweep(knot_db_t *db, sweep_cb keep_zone, void *cb_data); diff --git a/src/knot/zone/zone-diff.c b/src/knot/zone/zone-diff.c new file mode 100644 index 0000000..6022d4e --- /dev/null +++ b/src/knot/zone/zone-diff.c @@ -0,0 +1,389 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://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; +}; + +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) +{ + /* 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)) { + 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) +{ + /* 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)) { + 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; + knot_rrset_t to_add; + 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_ptr, void *data) +{ + if (node_ptr == NULL || *node_ptr == NULL || data == NULL) { + return KNOT_EINVAL; + } + + zone_node_t *node = *node_ptr; + + 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); + } + + 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); + } + + 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)) { + 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)) { + 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_ptr, void *data) +{ + if (node_ptr == NULL || *node_ptr == NULL || data == NULL) { + return KNOT_EINVAL; + } + + zone_node_t *node = *node_ptr; + + 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. Differencies 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); + } + + return KNOT_EOK; +} + +static int load_trees(zone_tree_t *nodes1, zone_tree_t *nodes2, + changeset_t *changeset, bool ignore_dnssec) +{ + assert(changeset); + + struct zone_diff_param param = { + .changeset = changeset, + .ignore_dnssec = ignore_dnssec, + }; + + // 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) +{ + if (zone1 == NULL || zone2 == NULL || changeset == NULL) { + return KNOT_EINVAL; + } + + 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); + if (ret != KNOT_EOK) { + return ret; + } + + ret = load_trees(zone1->nsec3_nodes, zone2->nsec3_nodes, changeset, ignore_dnssec); + 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); +} diff --git a/src/knot/zone/zone-diff.h b/src/knot/zone/zone-diff.h new file mode 100644 index 0000000..5f467aa --- /dev/null +++ b/src/knot/zone/zone-diff.h @@ -0,0 +1,31 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://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); + +/*! + * \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..9729cbc --- /dev/null +++ b/src/knot/zone/zone-dump.c @@ -0,0 +1,226 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://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); + knot_dump_style_t soa_style = *params->style; + + // Dump SOA record as a first. + if (!params->dump_nsec) { + int ret = knot_rrset_txt_dump(&soa, ¶ms->buf, ¶ms->buflen, + &soa_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) +{ + if (zone == NULL || file == NULL) { + return KNOT_EINVAL; + } + + // 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. + zone_node_t *apex = zone->apex; + dump_params_t params = { + .file = file, + .buf = buf, + .buflen = DUMP_BUF_LEN, + .rr_count = 0, + .origin = apex->owner, + .style = &KNOT_DUMP_STYLE_DEFAULT, + .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) { + 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) { + 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) { + 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) { + 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) { + 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..e9d8be8 --- /dev/null +++ b/src/knot/zone/zone-dump.h @@ -0,0 +1,41 @@ +/* Copyright (C) 2016 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +/*! + * \file + * + * \brief Zone text dump facility. + * + * \addtogroup zone + * @{ + */ + +#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. + * + * \retval KNOT_EOK on success. + * \retval < 0 if error. + */ +int zone_dump_text(zone_contents_t *zone, FILE *file, bool comments); + +/*! @} */ diff --git a/src/knot/zone/zone-load.c b/src/knot/zone/zone-load.c new file mode 100644 index 0000000..6b1c68e --- /dev/null +++ b/src/knot/zone/zone-load.c @@ -0,0 +1,154 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "knot/common/log.h" +#include "knot/journal/journal.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 "knot/updates/apply.h" +#include "libknot/libknot.h" + +int zone_load_contents(conf_t *conf, const knot_dname_t *zone_name, + zone_contents_t **contents) +{ + if (conf == NULL || zone_name == NULL || contents == NULL) { + return KNOT_EINVAL; + } + + char *zonefile = conf_zonefile(conf, zone_name); + conf_val_t val = conf_zone_get(conf, C_SEM_CHECKS, zone_name); + + zloader_t zl; + int ret = zonefile_open(&zl, zonefile, zone_name, conf_bool(&val), 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; + } + + return KNOT_EOK; +} + +int zone_load_journal(conf_t *conf, zone_t *zone, zone_contents_t *contents) +{ + if (conf == NULL || zone == NULL || contents == 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; + } + + /* Fetch SOA serial. */ + uint32_t serial = zone_contents_serial(contents); + + /* Load journal */ + list_t chgs; + init_list(&chgs); + int ret = zone_changes_load(conf, zone, &chgs, serial); + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + changesets_free(&chgs); + return ret; + } + if (EMPTY_LIST(chgs)) { + return KNOT_EOK; + } + + /* Apply changesets. */ + apply_ctx_t a_ctx = { 0 }; + apply_init_ctx(&a_ctx, contents, 0); + + ret = apply_changesets_directly(&a_ctx, &chgs); + if (ret == KNOT_EOK) { + log_zone_info(zone->name, "changes from journal applied %u -> %u", + serial, zone_contents_serial(contents)); + } else { + log_zone_error(zone->name, "failed to apply journal changes %u -> %u (%s)", + serial, zone_contents_serial(contents), + knot_strerror(ret)); + } + + update_cleanup(&a_ctx); + changesets_free(&chgs); + + 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; + } + + list_t chgs; + init_list(&chgs); + int ret = zone_in_journal_load(conf, zone, &chgs); + if (ret != KNOT_EOK) { + changesets_free(&chgs); + return ret; // include ENOENT, which is normal operation + } + + changeset_t *boo_ch = (changeset_t *)HEAD(chgs); + rem_node(&boo_ch->n); + ret = changeset_to_contents(boo_ch, contents); + if (ret != KNOT_EOK) { + changesets_free(&chgs); + return ret; + } + + apply_ctx_t a_ctx = { 0 }; + apply_init_ctx(&a_ctx, *contents, 0); + ret = apply_changesets_directly(&a_ctx, &chgs); + 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 (%s)", + knot_strerror(ret)); + } + update_cleanup(&a_ctx); + changesets_free(&chgs); + + 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..3210476 --- /dev/null +++ b/src/knot/zone/zone-load.h @@ -0,0 +1,63 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include "knot/conf/conf.h" +#include "knot/zone/zone.h" +#include "knot/dnssec/zone-events.h" // zone_sign_reschedule_t + +/*! + * \brief Load zone contents according to the configuration. + * + * \param conf + * \param zone_name + * \param contents + * \return KNOT_EOK or an error + */ +int zone_load_contents(conf_t *conf, const knot_dname_t *zone_name, + zone_contents_t **contents); + +/*! + * \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..d46b5ae --- /dev/null +++ b/src/knot/zone/zone-tree.c @@ -0,0 +1,209 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://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 "contrib/macros.h" + +zone_tree_t *zone_tree_create(void) +{ + return trie_create(NULL); +} + +int zone_tree_insert(zone_tree_t *tree, zone_node_t *node) +{ + if (tree == 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); + + *trie_get_ins(tree, (char *)lf + 1, *lf) = node; + + return KNOT_EOK; +} + +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, (char *)lf + 1, *lf); + if (val == NULL) { + return NULL; + } + + return *val; +} + +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, (char *)lf + 1, *lf, &fval); + if (fval != NULL) { + *found = (zone_node_t *)(*fval); + } + + int exact_match = 0; + if (ret == KNOT_EOK) { + if (fval != NULL) { + *previous = (*found)->prev; + } + 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. */ + trie_it_t *i = trie_it_begin(tree); + *previous = *(zone_node_t **)trie_it_val(i); /* leftmost */ + *previous = (*previous)->prev; /* rightmost */ + *found = NULL; + trie_it_free(i); + } + + return exact_match; +} + +/*! \brief Removes node with the given owner from the zone tree. */ +static void remove_node(zone_tree_t *tree, const knot_dname_t *owner) +{ + assert(owner); + + if (zone_tree_is_empty(tree)) { + 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, (char *)lf + 1, *lf); + if (rval != NULL) { + trie_del(tree, (char *)lf + 1, *lf, NULL); + } +} + +/*! \brief Clears wildcard child if set in parent node. */ +static void fix_wildcard_child(zone_node_t *node, const knot_dname_t *owner) +{ + if ((node->flags & NODE_FLAGS_WILDCARD_CHILD) + && knot_dname_is_wildcard(owner)) { + node->flags &= ~NODE_FLAGS_WILDCARD_CHILD; + } +} + +void zone_tree_delete_empty(zone_tree_t *tree, zone_node_t *node) +{ + if (tree == NULL || node == NULL) { + return; + } + + if (node->rrset_count == 0 && node->children == 0) { + zone_node_t *parent_node = node->parent; + if (parent_node != NULL) { + parent_node->children--; + fix_wildcard_child(parent_node, node->owner); + if (parent_node->parent != NULL) { /* Is not apex */ + // Recurse using the parent node, do not delete possibly empty parent. + zone_tree_delete_empty(tree, parent_node); + } + } + + // Delete node + remove_node(tree, node->owner); + node_free(node, NULL); + } +} + +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; + } + + return trie_apply(tree, (int (*)(trie_val_t *, void *))function, data); +} + +void zone_tree_free(zone_tree_t **tree) +{ + if (tree == NULL || *tree == NULL) { + return; + } + + trie_free(*tree); + *tree = NULL; +} + +static int zone_tree_free_node(zone_node_t **node, void *data) +{ + UNUSED(data); + + if (node) { + node_free(*node, NULL); + } + + return KNOT_EOK; +} + +void zone_tree_deep_free(zone_tree_t **tree) +{ + if (tree == NULL || *tree == NULL) { + return; + } + + (void)zone_tree_apply(*tree, zone_tree_free_node, NULL); + zone_tree_free(tree); +} diff --git a/src/knot/zone/zone-tree.h b/src/knot/zone/zone-tree.h new file mode 100644 index 0000000..d3cc909 --- /dev/null +++ b/src/knot/zone/zone-tree.h @@ -0,0 +1,142 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "contrib/qp-trie/trie.h" +#include "knot/zone/node.h" + +typedef trie_t zone_tree_t; + +/*! + * \brief Signature of callback for zone apply functions. + */ +typedef int (*zone_tree_apply_cb_t)(zone_node_t **node, void *data); + +/*! + * \brief Creates the zone tree. + * + * \return created zone tree structure. + */ +zone_tree_t *zone_tree_create(void); + +/*! + * \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) { + return 0; + } + + return trie_weight(tree); +} + +/*! + * \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; +} + +/*! + * \brief Inserts the given node into the zone tree. + * + * \param tree Zone tree to insert the node into. + * \param node Node to insert. + * + * \retval KNOT_EOK + * \retval KNOT_EINVAL + * \retval KNOT_ENOMEM + */ +int zone_tree_insert(zone_tree_t *tree, zone_node_t *node); + +/*! + * \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 Delete a node that has no RRSets and no children. + * + * \param tree The tree to remove from. + * \param node The node to remove. + */ +void zone_tree_delete_empty(zone_tree_t *tree, zone_node_t *node); + +/*! + * \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 Destroys the zone tree, not touching the saved data. + * + * \param tree Zone tree to be destroyed. + */ +void zone_tree_free(zone_tree_t **tree); + +/*! + * \brief Destroys the zone tree, together with the saved data. + * + * \param tree Zone tree to be destroyed. + */ +void zone_tree_deep_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..efc0caa --- /dev/null +++ b/src/knot/zone/zone.c @@ -0,0 +1,731 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <urcu.h> + +#include "knot/common/log.h" +#include "knot/conf/module.h" +#include "knot/dnssec/kasp/kasp_db.h" +#include "knot/nameserver/process_query.h" +#include "knot/query/requestor.h" +#include "knot/updates/zone-update.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/trim.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); + +static void free_ddns_queue(zone_t *zone) +{ + ptrnode_t *node = NULL, *nxt = NULL; + WALK_LIST_DELSAFE(node, nxt, zone->ddns_queue) { + knot_request_free(node->d, NULL); + } + ptrlist_free(&zone->ddns_queue, NULL); +} + +/*! \brief Open journal for zone. */ +static int open_journal(zone_t *zone) +{ + assert(zone); + + int ret = journal_open(zone->journal, zone->journal_db, zone->name); + if (ret != KNOT_EOK) { + log_zone_error(zone->name, "failed to open journal '%s'", + (*zone->journal_db)->path); + } + + return ret; +} + +/*! \brief Close the zone journal. */ +static void close_journal(zone_t *zone) +{ + assert(zone); + journal_close(zone->journal); +} + +/*! + * \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) +{ + /*! @note Function expects nobody will change zone contents meanwile. */ + + assert(zone); + + int ret = KNOT_EOK; + + bool force = zone->flags & ZONE_FORCE_FLUSH; + zone->flags &= ~ZONE_FORCE_FLUSH; + + if (zone_contents_is_empty(zone->contents)) { + if (allow_empty_zone && zone->journal && journal_exists(zone->journal_db, zone->name)) { + ret = journal_flush(zone->journal); + } else { + ret = KNOT_EINVAL; + } + goto flush_journal_replan; + } + + /* Check for disabled zonefile synchronization. */ + conf_val_t val = conf_zone_get(conf, C_ZONEFILE_SYNC, zone->name); + if (conf_int(&val) < 0 && !force) { + 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 && zone->zonefile.exists && zone->zonefile.serial == serial_to && + !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_mtime; + zone->zonefile.serial = serial_to; + zone->zonefile.resigned = false; + + /* Flush journal. */ + if (zone->journal && journal_exists(zone->journal_db, zone->name)) { + ret = open_journal(zone); + if (ret != KNOT_EOK) { + goto flush_journal_replan; + } + + ret = journal_flush(zone->journal); + if (ret != KNOT_EOK) { + goto flush_journal_replan; + } + } + + /* Trim extra heap. */ + mem_trim(); + +flush_journal_replan: + /* Plan next journal flush after proper period. */ + zone->timers.last_flush = time(NULL); + val = conf_zone_get(conf, C_ZONEFILE_SYNC, zone->name); + int64_t sync_timeout = conf_int(&val); + if (sync_timeout > 0) { + time_t next_flush = zone->timers.last_flush + sync_timeout; + zone_events_schedule_at(zone, ZONE_EVENT_FLUSH, 0, + ZONE_EVENT_FLUSH, next_flush); + } + + return ret; +} + +zone_t* zone_new(const knot_dname_t *name) +{ + zone_t *zone = malloc(sizeof(zone_t)); + if (zone == NULL) { + return NULL; + } + memset(zone, 0, sizeof(zone_t)); + + zone->name = knot_dname_copy(name, NULL); + if (zone->name == NULL) { + free(zone); + return NULL; + } + + // Journal + zone->journal = journal_new(); + if (zone->journal == NULL) { + knot_dname_free(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); + + // Journal lock + pthread_mutex_init(&zone->journal_lock, NULL); + + // Preferred master lock + pthread_mutex_init(&zone->preferred_lock, NULL); + + // Initialize events + zone_events_init(zone); + + // Initialize query modules list. + init_list(&zone->query_modules); + + return zone; +} + +void zone_control_clear(zone_t *zone) +{ + if (zone == NULL) { + return; + } + + zone_update_clear(zone->control_update); + free(zone->control_update); + zone->control_update = NULL; +} + +void zone_free(zone_t **zone_ptr) +{ + if (zone_ptr == NULL || *zone_ptr == NULL) { + return; + } + + zone_t *zone = *zone_ptr; + + close_journal(zone); + + zone_events_deinit(zone); + + knot_dname_free(zone->name, NULL); + + journal_free(&zone->journal); + + free_ddns_queue(zone); + pthread_mutex_destroy(&zone->ddns_lock); + pthread_mutex_destroy(&zone->journal_lock); + + /* Control update. */ + zone_control_clear(zone); + + /* Free preferred master. */ + pthread_mutex_destroy(&zone->preferred_lock); + free(zone->preferred_master); + + /* Free zone contents. */ + zone_contents_deep_free(zone->contents); + + conf_deactivate_modules(&zone->query_modules, &zone->query_plan); + + free(zone); + *zone_ptr = NULL; +} + +int zone_change_store(conf_t *conf, zone_t *zone, changeset_t *change) +{ + if (conf == NULL || zone == NULL || change == NULL) { + return KNOT_EINVAL; + } + + JOURNAL_LOCK_RW + + int ret = open_journal(zone); + if (ret == KNOT_EOK) { + ret = journal_store_changeset(zone->journal, change); + if (ret == KNOT_EBUSY) { + log_zone_notice(zone->name, "journal is full, flushing"); + + /* Transaction rolled back, journal released, we may flush. */ + ret = flush_journal(conf, zone, true); + if (ret == KNOT_EOK) { + ret = journal_store_changeset(zone->journal, change); + } + } + } + + JOURNAL_UNLOCK_RW + + return ret; +} + +int zone_changes_clear(conf_t *conf, zone_t *zone) +{ + if (conf == NULL || zone == NULL) { + return KNOT_EINVAL; + } + + JOURNAL_LOCK_RW + + int ret = KNOT_EOK; + + if (journal_exists(zone->journal_db, zone->name)) { + ret = open_journal(zone); + if (ret == KNOT_EOK) { + ret = journal_drop_changesets(zone->journal); + } + } + + JOURNAL_UNLOCK_RW + + return ret; +} + +int zone_changes_load(conf_t *conf, zone_t *zone, list_t *dst, uint32_t from) +{ + if (conf == NULL || zone == NULL || dst == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_ENOENT; + + if (journal_exists(zone->journal_db, zone->name)) { + ret = open_journal(zone); + } + + if (ret == KNOT_EOK) { + ret = journal_load_changesets(zone->journal, dst, from); + } + + return ret; +} + +int zone_chgset_ctx_load(conf_t *conf, zone_t *zone, chgset_ctx_list_t *dst, uint32_t from) +{ + if (conf == NULL || zone == NULL || dst == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_ENOENT; + + if (journal_exists(zone->journal_db, zone->name)) { + ret = open_journal(zone); + } + + if (ret == KNOT_EOK) { + ret = journal_load_chgset_ctx(zone->journal, dst, from); + } + + return ret; +} + +int zone_in_journal_load(conf_t *conf, zone_t *zone, list_t *dst) +{ + if (conf == NULL || zone == NULL || dst == NULL) { + return KNOT_EINVAL; + } + + int ret = KNOT_ENOENT; + + if (journal_exists(zone->journal_db, zone->name)) { + ret = open_journal(zone); + } + + if (ret == KNOT_EOK) { + ret = journal_load_bootstrap(zone->journal, dst); + } + + return ret; +} + +int zone_in_journal_store(conf_t *conf, zone_t *zone, zone_contents_t *new_contents) +{ + if (conf == NULL || zone == NULL || new_contents == NULL) { + return KNOT_EINVAL; + } + + changeset_t *co_ch = changeset_from_contents(new_contents); + int ret = co_ch ? zone_change_store(conf, zone, co_ch) : KNOT_ENOMEM; + changeset_from_contents_free(co_ch); + + 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) +{ + if (conf == NULL || zone == NULL) { + return KNOT_EINVAL; + } + + JOURNAL_LOCK_RW + + // NO open_journal() here. + + int ret = flush_journal(conf, zone, false); + + JOURNAL_UNLOCK_RW + + return ret; +} + +int zone_journal_serial(conf_t *conf, zone_t *zone, bool *is_empty, uint32_t *serial_to) +{ + if (conf == NULL || zone == NULL || is_empty == NULL || serial_to == NULL) { + return KNOT_EINVAL; + } + + int ret = open_journal(zone); + if (ret == KNOT_EOK) { + kserial_t ks; + journal_metadata_info(zone->journal, is_empty, NULL, NULL, NULL, &ks, NULL, NULL); + *serial_to = (ks.valid ? ks.serial : 0); + } + + return ret; +} + +zone_contents_t *zone_switch_contents(zone_t *zone, zone_contents_t *new_contents) +{ + if (zone == NULL) { + return NULL; + } + + zone_contents_t *old_contents; + zone_contents_t **current_contents = &zone->contents; + old_contents = rcu_xchg_pointer(current_contents, new_contents); + + return old_contents; +} + +bool zone_is_slave(conf_t *conf, const zone_t *zone) +{ + if (conf == NULL || zone == NULL) { + return false; + } + + conf_val_t val = conf_zone_get(conf, C_MASTER, zone->name); + return conf_val_count(&val) > 0 ? true : false; +} + +void zone_set_preferred_master(zone_t *zone, const struct sockaddr_storage *addr) +{ + if (zone == NULL || addr == NULL) { + return; + } + + pthread_mutex_lock(&zone->preferred_lock); + free(zone->preferred_master); + zone->preferred_master = malloc(sizeof(struct sockaddr_storage)); + *zone->preferred_master = *addr; + pthread_mutex_unlock(&zone->preferred_lock); +} + +void zone_clear_preferred_master(zone_t *zone) +{ + if (zone == NULL) { + return; + } + + pthread_mutex_lock(&zone->preferred_lock); + free(zone->preferred_master); + zone->preferred_master = NULL; + pthread_mutex_unlock(&zone->preferred_lock); +} + +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); +} + +bool zone_expired(const zone_t *zone) +{ + if (!zone) { + return false; + } + + const zone_timers_t *timers = &zone->timers; + + return timers->last_refresh > 0 && timers->soa_expire > 0 && + timers->last_refresh + timers->soa_expire <= time(NULL); +} + +/*! + * \brief Get preferred zone master while checking its existence. + */ +int static preferred_master(conf_t *conf, zone_t *zone, conf_remote_t *master) +{ + pthread_mutex_lock(&zone->preferred_lock); + + if (zone->preferred_master == NULL) { + pthread_mutex_unlock(&zone->preferred_lock); + return KNOT_ENOENT; + } + + conf_val_t masters = conf_zone_get(conf, C_MASTER, zone->name); + while (masters.code == KNOT_EOK) { + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, &masters); + size_t addr_count = conf_val_count(&addr); + + for (size_t i = 0; i < addr_count; i++) { + conf_remote_t remote = conf_remote(conf, &masters, i); + if (sockaddr_net_match((struct sockaddr *)&remote.addr, + (struct sockaddr *)zone->preferred_master, + -1)) { + *master = remote; + pthread_mutex_unlock(&zone->preferred_lock); + return KNOT_EOK; + } + } + + conf_val_next(&masters); + } + + pthread_mutex_unlock(&zone->preferred_lock); + + return KNOT_ENOENT; +} + +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; + } + + /* Try the preferred server. */ + + conf_remote_t preferred = { { AF_UNSPEC } }; + if (preferred_master(conf, zone, &preferred) == KNOT_EOK) { + int ret = callback(conf, zone, &preferred, callback_data); + if (ret == KNOT_EOK) { + return ret; + } + } + + /* Try all the other servers. */ + + bool success = false; + + conf_val_t masters = conf_zone_get(conf, C_MASTER, zone->name); + while (masters.code == KNOT_EOK) { + conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, &masters); + size_t addr_count = conf_val_count(&addr); + + for (size_t i = 0; i < addr_count; i++) { + conf_remote_t master = conf_remote(conf, &masters, i); + if (preferred.addr.ss_family != AF_UNSPEC && + sockaddr_net_match((struct sockaddr *)&master.addr, + (struct sockaddr *)&preferred.addr, + -1)) { + preferred.addr.ss_family = AF_UNSPEC; + continue; + } + + int ret = callback(conf, zone, &master, callback_data); + if (ret == KNOT_EOK) { + success = true; + break; + } + + char addr_str[SOCKADDR_STRLEN] = { 0 }; + sockaddr_tostr(addr_str, sizeof(addr_str), + (struct sockaddr *)&master.addr); + log_zone_debug(zone->name, "%s, remote %s, address %s, failed (%s)", + err_str, conf_str(&masters), addr_str, + knot_strerror(ret)); + } + + if (!success) { + log_zone_warning(zone->name, "%s, remote %s not usable", + err_str, conf_str(&masters)); + } + + conf_val_next(&masters); + } + + return success ? KNOT_EOK : KNOT_ENOMASTER; +} + +int zone_update_enqueue(zone_t *zone, knot_pkt_t *pkt, knotd_qdata_params_t *params) +{ + if (zone == NULL || pkt == NULL || params == NULL) { + return KNOT_EINVAL; + } + + /* Create serialized request. */ + struct knot_request *req = malloc(sizeof(struct knot_request)); + if (req == NULL) { + return KNOT_ENOMEM; + } + memset(req, 0, sizeof(struct knot_request)); + + /* Copy socket and request. */ + req->fd = dup(params->socket); + memcpy(&req->remote, params->remote, sizeof(struct sockaddr_storage)); + + req->query = knot_pkt_new(NULL, pkt->max_size, NULL); + int ret = knot_pkt_copy(req->query, pkt); + if (ret != KNOT_EOK) { + knot_pkt_free(req->query); + free(req); + return ret; + } + + pthread_mutex_lock(&zone->ddns_lock); + + /* Enqueue created request. */ + ptrlist_add(&zone->ddns_queue, req, NULL); + ++zone->ddns_queue_size; + + pthread_mutex_unlock(&zone->ddns_lock); + + /* Schedule UPDATE event. */ + zone_events_schedule_now(zone, ZONE_EVENT_UPDATE); + + return KNOT_EOK; +} + +size_t zone_update_dequeue(zone_t *zone, list_t *updates) +{ + if (zone == NULL || updates == NULL) { + return 0; + } + + pthread_mutex_lock(&zone->ddns_lock); + if (EMPTY_LIST(zone->ddns_queue)) { + /* Lost race during reload. */ + pthread_mutex_unlock(&zone->ddns_lock); + return 0; + } + + *updates = zone->ddns_queue; + size_t update_count = zone->ddns_queue_size; + init_list(&zone->ddns_queue); + zone->ddns_queue_size = 0; + + pthread_mutex_unlock(&zone->ddns_lock); + + return update_count; +} + +int zone_dump_to_dir(conf_t *conf, zone_t *zone, const char *dir) +{ + if (zone == NULL || dir == NULL) { + return KNOT_EINVAL; + } + + size_t dir_len = strlen(dir); + if (dir_len == 0) { + return KNOT_EINVAL; + } + + char *zonefile = conf_zonefile(conf, zone->name); + char *zonefile_basename = strrchr(zonefile, '/'); + if (zonefile_basename == NULL) { + zonefile_basename = zonefile; + } + + size_t target_length = strlen(zonefile_basename) + dir_len + 2; + char target[target_length]; + (void)snprintf(target, target_length, "%s/%s", dir, zonefile_basename); + if (strcmp(target, zonefile) == 0) { + free(zonefile); + return KNOT_EDENIED; + } + free(zonefile); + + return zonefile_write(target, zone->contents); +} + +int zone_set_master_serial(zone_t *zone, uint32_t serial) +{ + int ret = kasp_db_open(*kaspdb()); + if (ret == KNOT_EOK) { + ret = kasp_db_store_serial(*kaspdb(), zone->name, KASPDB_SERIAL_MASTER, serial); + } + return ret; +} + +int zone_get_master_serial(zone_t *zone, uint32_t *serial) +{ + if (!kasp_db_exists(*kaspdb())) { + *serial = zone_contents_serial(zone->contents); + return KNOT_EOK; + } + int ret = kasp_db_open(*kaspdb()); + if (ret != KNOT_EOK) { + return ret; + } + ret = kasp_db_load_serial(*kaspdb(), zone->name, KASPDB_SERIAL_MASTER, serial); + if (ret == KNOT_ENOENT) { + *serial = zone_contents_serial(zone->contents); + return KNOT_EOK; + } + return ret; +} + +int zone_set_lastsigned_serial(zone_t *zone, uint32_t serial) +{ + int ret = kasp_db_open(*kaspdb()); + if (ret == KNOT_EOK) { + ret = kasp_db_store_serial(*kaspdb(), zone->name, KASPDB_SERIAL_LASTSIGNED, serial); + } + return ret; +} + +bool zone_get_lastsigned_serial(zone_t *zone, uint32_t *serial) +{ + if (!kasp_db_exists(*kaspdb())) { + return false; + } + int ret = kasp_db_open(*kaspdb()); + if (ret == KNOT_EOK) { + ret = kasp_db_load_serial(*kaspdb(), zone->name, KASPDB_SERIAL_LASTSIGNED, serial); + } + return (ret == KNOT_EOK); +} diff --git a/src/knot/zone/zone.h b/src/knot/zone/zone.h new file mode 100644 index 0000000..360e222 --- /dev/null +++ b/src/knot/zone/zone.h @@ -0,0 +1,177 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "knot/conf/conf.h" +#include "knot/conf/confio.h" +#include "knot/journal/journal.h" +#include "knot/events/events.h" +#include "knot/zone/contents.h" +#include "knot/zone/timers.h" +#include "libknot/dname.h" +#include "libknot/packet/pkt.h" + +struct zone_update; + +/*! + * \brief Zone flags. + */ +typedef enum zone_flag_t { + 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_flag_t; + +/*! + * \brief Structure for holding DNS zone. + */ +typedef struct zone +{ + knot_dname_t *name; + zone_contents_t *contents; + zone_flag_t flags; + + /*! \brief Dynamic configuration zone change type. */ + conf_io_type_t change_type; + + /*! \brief Zonefile parameters. */ + struct { + time_t mtime; + uint32_t serial; + bool exists; + bool resigned; + } zonefile; + + /*! \brief Zone events. */ + zone_timers_t timers; //!< Persistent zone timers. + zone_events_t events; //!< Zone events timers. + + /*! \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 Journal structure. */ + journal_t *journal; + + /*! \brief Journal access lock. */ + pthread_mutex_t journal_lock; + + /*! \brief Ptr to journal DB (in struct server) */ + journal_db_t **journal_db; + + /*! \brief Preferred master lock. */ + 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 emtpy 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 Clears possible control update transaction. + * + * \param zone Zone to be cleared. + */ +void zone_control_clear(zone_t *zone); + +int zone_change_store(conf_t *conf, zone_t *zone, changeset_t *change); +int zone_changes_clear(conf_t *conf, zone_t *zone); +int zone_changes_load(conf_t *conf, zone_t *zone, list_t *dst, uint32_t from); +int zone_chgset_ctx_load(conf_t *conf, zone_t *zone, chgset_ctx_list_t *dst, uint32_t from); +int zone_in_journal_load(conf_t *conf, zone_t *zone, list_t *dst); +int zone_in_journal_store(conf_t *conf, zone_t *zone, zone_contents_t *new_contents); +int zone_journal_serial(conf_t *conf, zone_t *zone, bool *is_empty, uint32_t *serial_to); + +/*! \brief Synchronize zone file with journal. */ +int zone_flush_journal(conf_t *conf, zone_t *zone); + +/*! + * \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 Get zone SOA RR. */ +const knot_rdataset_t *zone_soa(const zone_t *zone); + +/*! \brief Check if zone is expired according to timers. */ +bool zone_expired(const zone_t *zone); + +typedef int (*zone_master_cb)(conf_t *conf, zone_t *zone, const conf_remote_t *remote, + void *data); + +/*! + * \brief Perform an action with a first working master server. + * + * The function iterates over available masters. For each master, the callback + * function is called. If the callback function succeeds (\ref KNOT_EOK is + * returned), the iteration is terminated. + * + * \return Error code from the last callback. + */ +int zone_master_try(conf_t *conf, zone_t *zone, zone_master_cb callback, + void *callback_data, const char *err_str); + + +/*! \brief Enqueue UPDATE request for processing. */ +int zone_update_enqueue(zone_t *zone, knot_pkt_t *pkt, knotd_qdata_params_t *params); + +/*! \brief Dequeue UPDATE request. Returns number of queued updates. */ +size_t zone_update_dequeue(zone_t *zone, list_t *updates); + +/*! \brief Write zone contents to zonefile, but into different directory. */ +int zone_dump_to_dir(conf_t *conf, zone_t *zone, const char *dir); + +int zone_set_master_serial(zone_t *zone, uint32_t serial); + +int zone_get_master_serial(zone_t *zone, uint32_t *serial); + +int zone_set_lastsigned_serial(zone_t *zone, uint32_t serial); + +bool zone_get_lastsigned_serial(zone_t *zone, 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..a6e9834 --- /dev/null +++ b/src/knot/zone/zonedb-load.c @@ -0,0 +1,346 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include <assert.h> +#include <urcu.h> + +#include "knot/common/log.h" +#include "knot/conf/module.h" +#include "knot/events/replan.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); + + char *zonefile = conf_zonefile(conf, zone_name); + time_t mtime; + int ret = zonefile_exists(zonefile, &mtime); + free(zonefile); + + return (ret == KNOT_EOK && old_zone != NULL && + !(old_zone->zonefile.exists && old_zone->zonefile.mtime == mtime)); +} + +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->journal_db = &server->journal_db; + + int result = zone_events_setup(zone, server->workers, &server->sched, + server->timers_db); + if (result != KNOT_EOK) { + zone_free(&zone); + return NULL; + } + + return zone; +} + +/*! + * \brief Set timer if unset (value is 0). + */ +static void time_set_default(time_t *time, time_t value) +{ + assert(time); + + if (*time == 0) { + *time = value; + } +} + +/*! + * \brief Set default timers for new zones or invalidate if not valid. + */ +static void timers_sanitize(conf_t *conf, zone_t *zone) +{ + assert(conf); + assert(zone); + + time_t now = time(NULL); + + // replace SOA expire if we have better knowledge + if (!zone_contents_is_empty(zone->contents)) { + const knot_rdataset_t *soa = zone_soa(zone); + zone->timers.soa_expire = knot_soa_expire(soa->rdata); + } + + // 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.last_refresh, now); + time_set_default(&zone->timers.next_refresh, now); + } else { + // invalidate if we don't have a master + zone->timers.last_refresh = 0; + zone->timers.next_refresh = 0; + } +} + +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->timers = old_zone->timers; + timers_sanitize(conf, zone); + + if (zone_file_updated(conf, old_zone, name) && !zone_expired(zone)) { + replan_load_updated(zone, old_zone); + } else { + zone->zonefile = old_zone->zonefile; + replan_load_current(conf, zone, old_zone); + } + + if (old_zone->control_update != NULL) { + log_zone_warning(old_zone->name, "control transaction aborted"); + zone_control_clear(old_zone); + } + + 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->timers_db, name, &zone->timers); + if (ret != KNOT_EOK && ret != KNOT_ENOENT) { + log_zone_error(zone->name, "failed to load persistent timers (%s)", + knot_strerror(ret)); + zone_free(&zone); + return NULL; + } + + timers_sanitize(conf, zone); + + 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"); + replan_load_new(zone); // if load fails, fallback to bootstrap + } + + 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); + + if (old_zone) { + return create_zone_reload(conf, name, server, old_zone); + } else { + return create_zone_new(conf, name, server); + } +} + +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_type_t)(*trie_it_val(it)); + assert(!(type & CONF_IO_TSET)); + zone->change_type = type; + } + } + trie_it_free(it); +} + +/*! + * \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. + * + * \return New zone database. + */ +static knot_zonedb_t *create_zonedb(conf_t *conf, server_t *server) +{ + 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; + } + + bool full = !(conf->io.flags & CONF_IO_FACTIVE) || + (conf->io.flags & CONF_IO_FRLD_ZONES); + + /* Mark changed zones. */ + if (!full) { + mark_changed_zones(server->zone_db, conf->io.zones); + } + + 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 && !full) { + /* 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, zone->name, &zone->query_modules, + &zone->query_plan); + + knot_zonedb_insert(db_new, zone); + } + + 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 db_new New zone database for comparison if full reload. + */ +static void remove_old_zonedb(conf_t *conf, knot_zonedb_t *db_old, + knot_zonedb_t *db_new) +{ + if (db_old == NULL) { + return; + } + + bool full = !(conf->io.flags & CONF_IO_FACTIVE) || + (conf->io.flags & CONF_IO_FRLD_ZONES); + + 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 (full) { + /* Check if reloaded (reused contents). */ + if (knot_zonedb_find(db_new, zone->name)) { + zone->contents = NULL; + } + /* Completely new zone. */ + } else { + /* Check if reloaded (reused contents). */ + if (zone->change_type & CONF_IO_TRELOAD) { + 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); + + if (full) { + knot_zonedb_deep_free(&db_old); + } else { + knot_zonedb_free(&db_old); + } +} + +void zonedb_reload(conf_t *conf, server_t *server) +{ + if (conf == NULL || server == NULL) { + return; + } + + /* Insert all required zones to the new zone DB. */ + knot_zonedb_t *db_new = create_zonedb(conf, server); + if (db_new == NULL) { + log_error("failed to create new zone database"); + return; + } + + /* 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(); + + /* Remove old zone DB. */ + remove_old_zonedb(conf, db_old, db_new); +} diff --git a/src/knot/zone/zonedb-load.h b/src/knot/zone/zonedb-load.h new file mode 100644 index 0000000..fda12d8 --- /dev/null +++ b/src/knot/zone/zonedb-load.h @@ -0,0 +1,28 @@ +/* Copyright (C) 2015 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +#pragma once + +#include "knot/conf/conf.h" +#include "knot/server/server.h" + +/*! + * \brief Update zone database according to configuration. + * + * \param[in] conf Configuration. + * \param[in] server Server instance. + */ +void zonedb_reload(conf_t *conf, server_t *server); diff --git a/src/knot/zone/zonedb.c b/src/knot/zone/zonedb.c new file mode 100644 index 0000000..d949a59 --- /dev/null +++ b/src/knot/zone/zonedb.c @@ -0,0 +1,167 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include <stdlib.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) +{ + // Don't flush if removed zone (no previous configuration available). + if (conf_rawid_exists(conf(), C_ZONE, zone->name, knot_dname_size(zone->name))) { + // Flush if bootstrapped or if the journal doesn't exist. + if (!zone->zonefile.exists || !journal_exists(zone->journal_db, zone->name)) { + zone_flush_journal(conf(), zone); + } else { + bool empty; + uint32_t journal_serial, zone_serial = zone_contents_serial(zone->contents); + int ret = zone_journal_serial(conf(), zone, &empty, &journal_serial); + if (ret != KNOT_EOK || empty || journal_serial != zone_serial) { + zone_flush_journal(conf(), 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, (char *)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, (char *)lf + 1, *lf); + if (rval == NULL) { + return KNOT_ENOENT; + } + + return trie_del(db->trie, (char *)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, (char *)lf + 1, *lf); + if (val == NULL) { + return NULL; + } + + return *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, (char *)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) +{ + if (db == NULL || *db == NULL) { + return; + } + + knot_zonedb_foreach(*db, discard_zone); + knot_zonedb_free(db); +} diff --git a/src/knot/zone/zonedb.h b/src/knot/zone/zonedb.h new file mode 100644 index 0000000..c5fab4d --- /dev/null +++ b/src/knot/zone/zonedb.h @@ -0,0 +1,123 @@ +/* Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://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" + +typedef struct { + trie_t *trie; + knot_mm_t mm; +} knot_zonedb_t; + +/* + * 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 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. + */ +void knot_zonedb_deep_free(knot_zonedb_t **db); diff --git a/src/knot/zone/zonefile.c b/src/knot/zone/zonefile.c new file mode 100644 index 0000000..37fc90b --- /dev/null +++ b/src/knot/zone/zonefile.c @@ -0,0 +1,343 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://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/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; + + char buff[KNOT_DNAME_TXT_MAXLEN + 1]; + 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] = { '\0' }; + 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, bool 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_EACCES; + } + + /* 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); + 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->fatal_error = true; + loader->err_handler->cb(loader->err_handler, zc->z, NULL, + SEM_ERR_SOA_NONE, NULL); + goto fail; + } + + ret = zone_contents_adjust_full(zc->z); + 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; + } + + return zc->z; + +fail: + zone_contents_deep_free(zc->z); + return NULL; +} + +int zonefile_exists(const char *path, time_t *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_mtime; + } + + return KNOT_EOK; +} + +int zonefile_write(const char *path, zone_contents_t *zone) +{ + if (!zone || !path) { + return KNOT_EINVAL; + } + + 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); + 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 zone_node_t *node, sem_error_t error, const char *data) +{ + assert(handler != NULL); + assert(zone != NULL); + + char buff[KNOT_DNAME_TXT_MAXLEN + 1] = ""; + if (node != NULL) { + (void)knot_dname_to_str(buff, node->owner, sizeof(buff)); + } + + log_fmt_zone(handler->fatal_error ? LOG_ERR : LOG_WARNING, + LOG_SOURCE_ZONE, zone->apex->owner, NULL, + "check%s%s, %s%s%s", + (node != NULL ? ", node " : ""), + (node != NULL ? buff : ""), + sem_error_msg(error), + (data != NULL ? " " : ""), + (data != NULL ? data : "")); +} + +#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..90283ee --- /dev/null +++ b/src/knot/zone/zonefile.h @@ -0,0 +1,104 @@ +/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://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. */ + bool 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 zone_node_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, bool 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, time_t *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); |