summaryrefslogtreecommitdiffstats
path: root/src/knot/zone
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/knot/zone/contents.c1197
-rw-r--r--src/knot/zone/contents.h288
-rw-r--r--src/knot/zone/node.c312
-rw-r--r--src/knot/zone/node.h276
-rw-r--r--src/knot/zone/semantic-check.c1193
-rw-r--r--src/knot/zone/semantic-check.h123
-rw-r--r--src/knot/zone/serial.c84
-rw-r--r--src/knot/zone/serial.h72
-rw-r--r--src/knot/zone/timers.c288
-rw-r--r--src/knot/zone/timers.h119
-rw-r--r--src/knot/zone/zone-diff.c389
-rw-r--r--src/knot/zone/zone-diff.h31
-rw-r--r--src/knot/zone/zone-dump.c226
-rw-r--r--src/knot/zone/zone-dump.h41
-rw-r--r--src/knot/zone/zone-load.c154
-rw-r--r--src/knot/zone/zone-load.h63
-rw-r--r--src/knot/zone/zone-tree.c209
-rw-r--r--src/knot/zone/zone-tree.h142
-rw-r--r--src/knot/zone/zone.c731
-rw-r--r--src/knot/zone/zone.h177
-rw-r--r--src/knot/zone/zonedb-load.c346
-rw-r--r--src/knot/zone/zonedb-load.h28
-rw-r--r--src/knot/zone/zonedb.c167
-rw-r--r--src/knot/zone/zonedb.h123
-rw-r--r--src/knot/zone/zonefile.c343
-rw-r--r--src/knot/zone/zonefile.h104
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(&params_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, &params_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(&params_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(&current_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, &param);
+ 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, &param);
+}
+
+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, &params->buf, &params->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, &params->buf, &params->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, &params->buf, &params->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, &params);
+ 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, &params);
+ 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, &params);
+ 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, &params);
+ 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, &params);
+ 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);