summaryrefslogtreecommitdiffstats
path: root/src/knot/updates
diff options
context:
space:
mode:
Diffstat (limited to 'src/knot/updates')
-rw-r--r--src/knot/updates/acl.c361
-rw-r--r--src/knot/updates/acl.h83
-rw-r--r--src/knot/updates/apply.c379
-rw-r--r--src/knot/updates/apply.h101
-rw-r--r--src/knot/updates/changesets.c628
-rw-r--r--src/knot/updates/changesets.h290
-rw-r--r--src/knot/updates/ddns.c701
-rw-r--r--src/knot/updates/ddns.h47
-rw-r--r--src/knot/updates/zone-update.c1098
-rw-r--r--src/knot/updates/zone-update.h299
10 files changed, 3987 insertions, 0 deletions
diff --git a/src/knot/updates/acl.c b/src/knot/updates/acl.c
new file mode 100644
index 0000000..b46c893
--- /dev/null
+++ b/src/knot/updates/acl.c
@@ -0,0 +1,361 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "knot/updates/acl.h"
+#include "contrib/wire_ctx.h"
+
+static bool match_type(uint16_t type, conf_val_t *types)
+{
+ if (types == NULL) {
+ return true;
+ }
+
+ conf_val_reset(types);
+ while (types->code == KNOT_EOK) {
+ if (type == knot_wire_read_u64(types->data)) {
+ return true;
+ }
+ conf_val_next(types);
+ }
+
+ return false;
+}
+
+static bool match_name(const knot_dname_t *rr_owner, const knot_dname_t *name,
+ acl_update_owner_match_t match)
+{
+ if (name == NULL) {
+ return true;
+ }
+
+ int ret = knot_dname_in_bailiwick(rr_owner, name);
+ switch (match) {
+ case ACL_UPDATE_MATCH_SUBEQ:
+ return (ret >= 0);
+ case ACL_UPDATE_MATCH_EQ:
+ return (ret == 0);
+ case ACL_UPDATE_MATCH_SUB:
+ return (ret > 0);
+ default:
+ return false;
+ }
+}
+
+static bool match_names(const knot_dname_t *rr_owner, const knot_dname_t *zone_name,
+ conf_val_t *names, acl_update_owner_match_t match)
+{
+ if (names == NULL) {
+ return true;
+ }
+
+ conf_val_reset(names);
+ while (names->code == KNOT_EOK) {
+ knot_dname_storage_t full_name;
+ size_t len;
+ const uint8_t *name = conf_data(names, &len);
+ if (name[len - 1] != '\0') {
+ // Append zone name if non-FQDN.
+ wire_ctx_t ctx = wire_ctx_init(full_name, sizeof(full_name));
+ wire_ctx_write(&ctx, name, len);
+ wire_ctx_write(&ctx, zone_name, knot_dname_size(zone_name));
+ if (ctx.error != KNOT_EOK) {
+ return false;
+ }
+ name = full_name;
+ }
+ if (match_name(rr_owner, name, match)) {
+ return true;
+ }
+ conf_val_next(names);
+ }
+
+ return false;
+}
+
+static bool update_match(conf_t *conf, conf_val_t *acl, knot_dname_t *key_name,
+ const knot_dname_t *zone_name, knot_pkt_t *query)
+{
+ if (query == NULL) {
+ return true;
+ }
+
+ conf_val_t val_types = conf_id_get(conf, C_ACL, C_UPDATE_TYPE, acl);
+ conf_val_t *types = (conf_val_count(&val_types) > 0) ? &val_types : NULL;
+
+ conf_val_t val = conf_id_get(conf, C_ACL, C_UPDATE_OWNER, acl);
+ acl_update_owner_t owner = conf_opt(&val);
+
+ /* Return if no specific requirements configured. */
+ if (types == NULL && owner == ACL_UPDATE_OWNER_NONE) {
+ return true;
+ }
+
+ acl_update_owner_match_t match = ACL_UPDATE_MATCH_SUBEQ;
+ if (owner != ACL_UPDATE_OWNER_NONE) {
+ val = conf_id_get(conf, C_ACL, C_UPDATE_OWNER_MATCH, acl);
+ match = conf_opt(&val);
+ }
+
+ conf_val_t *names = NULL;
+ conf_val_t val_names;
+ if (owner == ACL_UPDATE_OWNER_NAME) {
+ val_names = conf_id_get(conf, C_ACL, C_UPDATE_OWNER_NAME, acl);
+ if (conf_val_count(&val_names) > 0) {
+ names = &val_names;
+ }
+ }
+
+ /* Updated RRs are contained in the Authority section of the query
+ * (RFC 2136 Section 2.2)
+ */
+ uint16_t pos = query->sections[KNOT_AUTHORITY].pos;
+ uint16_t count = query->sections[KNOT_AUTHORITY].count;
+
+ for (int i = pos; i < pos + count; i++) {
+ knot_rrset_t *rr = &query->rr[i];
+ if (!match_type(rr->type, types)) {
+ return false;
+ }
+
+ switch (owner) {
+ case ACL_UPDATE_OWNER_NAME:
+ if (!match_names(rr->owner, zone_name, names, match)) {
+ return false;
+ }
+ break;
+ case ACL_UPDATE_OWNER_KEY:
+ if (!match_name(rr->owner, key_name, match)) {
+ return false;
+ }
+ break;
+ case ACL_UPDATE_OWNER_ZONE:
+ if (!match_name(rr->owner, zone_name, match)) {
+ return false;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ return true;
+}
+
+static bool check_addr_key(conf_t *conf, conf_val_t *addr_val, conf_val_t *key_val,
+ bool remote, const struct sockaddr_storage *addr,
+ const knot_tsig_key_t *tsig, bool deny)
+{
+ /* Check if the address matches the acl address list or remote addresses. */
+ if (addr_val->code != KNOT_ENOENT) {
+ if (remote) {
+ if (!conf_addr_match(addr_val, addr)) {
+ return false;
+ }
+ } else {
+ if (!conf_addr_range_match(addr_val, addr)) {
+ return false;
+ }
+ }
+ }
+
+ /* Check if the key matches the acl key list or remote key. */
+ while (key_val->code == KNOT_EOK) {
+ /* No key provided, but required. */
+ if (tsig->name == NULL) {
+ goto next_key;
+ }
+
+ /* Compare key names (both in lower-case). */
+ const knot_dname_t *key_name = conf_dname(key_val);
+ if (!knot_dname_is_equal(key_name, tsig->name)) {
+ goto next_key;
+ }
+
+ /* Compare key algorithms. */
+ conf_val_t alg_val = conf_id_get(conf, C_KEY, C_ALG, key_val);
+ if (conf_opt(&alg_val) != tsig->algorithm) {
+ goto next_key;
+ }
+
+ break;
+ next_key:
+ if (remote) {
+ assert(!(key_val->item->flags & YP_FMULTI));
+ key_val->code = KNOT_EOF;
+ break;
+ } else {
+ assert(key_val->item->flags & YP_FMULTI);
+ conf_val_next(key_val);
+ }
+ }
+ switch (key_val->code) {
+ case KNOT_EOK:
+ // Key match.
+ break;
+ case KNOT_ENOENT:
+ // Empty list without key provided or denied.
+ if (tsig->name == NULL || deny) {
+ break;
+ }
+ // FALLTHROUGH
+ default:
+ return false;
+ }
+
+ return true;
+}
+
+bool acl_allowed(conf_t *conf, conf_val_t *acl, acl_action_t action,
+ const struct sockaddr_storage *addr, knot_tsig_key_t *tsig,
+ const knot_dname_t *zone_name, knot_pkt_t *query)
+{
+ if (acl == NULL || addr == NULL || tsig == NULL) {
+ return false;
+ }
+
+ while (acl->code == KNOT_EOK) {
+ conf_val_t rmt_val = conf_id_get(conf, C_ACL, C_RMT, acl);
+ bool remote = (rmt_val.code == KNOT_EOK);
+ conf_val_t deny_val = conf_id_get(conf, C_ACL, C_DENY, acl);
+ bool deny = conf_bool(&deny_val);
+
+ /* Check if a remote matches given address and key. */
+ conf_val_t addr_val, key_val;
+ conf_mix_iter_t iter;
+ conf_mix_iter_init(conf, &rmt_val, &iter);
+ while (iter.id->code == KNOT_EOK) {
+ addr_val = conf_id_get(conf, C_RMT, C_ADDR, iter.id);
+ key_val = conf_id_get(conf, C_RMT, C_KEY, iter.id);
+ if (check_addr_key(conf, &addr_val, &key_val, remote, addr, tsig, deny)) {
+ break;
+ }
+ conf_mix_iter_next(&iter);
+ }
+ if (iter.id->code == KNOT_EOF) {
+ goto next_acl;
+ }
+ /* Or check if acl address/key matches given address and key. */
+ if (!remote) {
+ addr_val = conf_id_get(conf, C_ACL, C_ADDR, acl);
+ key_val = conf_id_get(conf, C_ACL, C_KEY, acl);
+ if (!check_addr_key(conf, &addr_val, &key_val, remote, addr, tsig, deny)) {
+ goto next_acl;
+ }
+ }
+
+ /* Check if the action is allowed. */
+ if (action != ACL_ACTION_QUERY) {
+ conf_val_t val = conf_id_get(conf, C_ACL, C_ACTION, acl);
+ while (val.code == KNOT_EOK) {
+ if (conf_opt(&val) != action) {
+ conf_val_next(&val);
+ continue;
+ }
+
+ break;
+ }
+ switch (val.code) {
+ case KNOT_EOK: /* Check for action match. */
+ break;
+ case KNOT_ENOENT: /* Empty action list allowed with deny only. */
+ return false;
+ default: /* No match. */
+ goto next_acl;
+ }
+ }
+
+ /* If the action is update, check for update rule match. */
+ if (action == ACL_ACTION_UPDATE &&
+ !update_match(conf, acl, tsig->name, zone_name, query)) {
+ goto next_acl;
+ }
+
+ /* Check if denied. */
+ if (deny) {
+ return false;
+ }
+
+ /* Fill the output with tsig secret if provided. */
+ if (tsig->name != NULL) {
+ conf_val_t val = conf_id_get(conf, C_KEY, C_SECRET, &key_val);
+ tsig->secret.data = (uint8_t *)conf_bin(&val, &tsig->secret.size);
+ }
+
+ return true;
+next_acl:
+ conf_val_next(acl);
+ }
+
+ return false;
+}
+
+bool rmt_allowed(conf_t *conf, conf_val_t *rmts, const struct sockaddr_storage *addr,
+ knot_tsig_key_t *tsig)
+{
+ if (!conf->cache.srv_auto_acl) {
+ return false;
+ }
+
+ conf_mix_iter_t iter;
+ conf_mix_iter_init(conf, rmts, &iter);
+ while (iter.id->code == KNOT_EOK) {
+ conf_val_t val = conf_id_get(conf, C_RMT, C_AUTO_ACL, iter.id);
+ if (!conf_bool(&val)) {
+ goto next_remote;
+ }
+
+ conf_val_t key_id = conf_id_get(conf, C_RMT, C_KEY, iter.id);
+ if (key_id.code == KNOT_EOK) {
+ /* No key provided, but required. */
+ if (tsig->name == NULL) {
+ goto next_remote;
+ }
+
+ /* Compare key names (both in lower-case). */
+ const knot_dname_t *key_name = conf_dname(&key_id);
+ if (!knot_dname_is_equal(key_name, tsig->name)) {
+ goto next_remote;
+ }
+
+ /* Compare key algorithms. */
+ val = conf_id_get(conf, C_KEY, C_ALG, &key_id);
+ if (conf_opt(&val) != tsig->algorithm) {
+ goto next_remote;
+ }
+ } else if (key_id.code == KNOT_ENOENT && tsig->name != NULL) {
+ /* Key provided but no key configured. */
+ goto next_remote;
+ }
+
+ /* Check if the address matches. */
+ val = conf_id_get(conf, C_RMT, C_ADDR, iter.id);
+ if (!conf_addr_match(&val, addr)) {
+ goto next_remote;
+ }
+
+ /* Fill out the output with tsig secret if provided. */
+ if (tsig->name != NULL) {
+ val = conf_id_get(conf, C_KEY, C_SECRET, &key_id);
+ tsig->secret.data = (uint8_t *)conf_bin(&val, &tsig->secret.size);
+ }
+
+ return true;
+next_remote:
+ conf_mix_iter_next(&iter);
+ }
+
+ return false;
+}
diff --git a/src/knot/updates/acl.h b/src/knot/updates/acl.h
new file mode 100644
index 0000000..8c15acf
--- /dev/null
+++ b/src/knot/updates/acl.h
@@ -0,0 +1,83 @@
+/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <sys/socket.h>
+
+#include "libknot/tsig.h"
+#include "knot/conf/conf.h"
+
+/*! \brief ACL actions. */
+typedef enum {
+ ACL_ACTION_QUERY = 0,
+ ACL_ACTION_NOTIFY = 1,
+ ACL_ACTION_TRANSFER = 2,
+ ACL_ACTION_UPDATE = 3
+} acl_action_t;
+
+/*! \brief ACL update owner matching options. */
+typedef enum {
+ ACL_UPDATE_OWNER_NONE = 0,
+ ACL_UPDATE_OWNER_KEY = 1,
+ ACL_UPDATE_OWNER_ZONE = 2,
+ ACL_UPDATE_OWNER_NAME = 3,
+} acl_update_owner_t;
+
+/*! \bref ACL update owner comparison options. */
+typedef enum {
+ ACL_UPDATE_MATCH_SUBEQ = 0,
+ ACL_UPDATE_MATCH_EQ = 1,
+ ACL_UPDATE_MATCH_SUB = 2,
+} acl_update_owner_match_t;
+
+/*!
+ * \brief Checks if the address and/or tsig key matches given ACL list.
+ *
+ * If a proper ACL rule is found and tsig.name is not empty, tsig.secret is filled.
+ *
+ * \param conf Configuration.
+ * \param acl Pointer to ACL config multivalued identifier.
+ * \param action ACL action.
+ * \param addr IP address.
+ * \param tsig TSIG parameters.
+ * \param zone_name Zone name.
+ * \param query Update query.
+ *
+ * \retval True if authenticated.
+ */
+bool acl_allowed(conf_t *conf, conf_val_t *acl, acl_action_t action,
+ const struct sockaddr_storage *addr, knot_tsig_key_t *tsig,
+ const knot_dname_t *zone_name, knot_pkt_t *query);
+
+/*!
+ * \brief Checks if the address and/or tsig key matches a remote from the list.
+ *
+ * Global (server.automatic-acl) and per remote automatic ACL functionality
+ * must be enabled in order to decide the remote is allowed.
+ *
+ * If a proper REMOTE is found and tsig.name is not empty, tsig.secret is filled.
+ *
+ * \param conf Configuration.
+ * \param rmts Pointer to REMOTE config multivalued identifier.
+ * \param addr IP address.
+ * \param tsig TSIG parameters.
+ *
+ * \retval True if authenticated.
+ */
+bool rmt_allowed(conf_t *conf, conf_val_t *rmts, const struct sockaddr_storage *addr,
+ knot_tsig_key_t *tsig);
diff --git a/src/knot/updates/apply.c b/src/knot/updates/apply.c
new file mode 100644
index 0000000..b96432e
--- /dev/null
+++ b/src/knot/updates/apply.c
@@ -0,0 +1,379 @@
+/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "knot/common/log.h"
+#include "knot/updates/apply.h"
+#include "libknot/libknot.h"
+#include "contrib/macros.h"
+#include "contrib/mempattern.h"
+
+/*! \brief Replaces rdataset of given type with a copy. */
+static int replace_rdataset_with_copy(zone_node_t *node, uint16_t type)
+{
+ int ret = binode_prepare_change(node, NULL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Find data to copy.
+ struct rr_data *data = NULL;
+ for (uint16_t i = 0; i < node->rrset_count; ++i) {
+ if (node->rrs[i].type == type) {
+ data = &node->rrs[i];
+ break;
+ }
+ }
+ if (data == NULL) {
+ return KNOT_EOK;
+ }
+
+ // Create new data.
+ knot_rdataset_t *rrs = &data->rrs;
+ void *copy = malloc(rrs->size);
+ if (copy == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ memcpy(copy, rrs->rdata, rrs->size);
+
+ // Store new data into node RRS.
+ rrs->rdata = copy;
+
+ return KNOT_EOK;
+}
+
+/*! \brief Frees RR dataset. For use when a copy was made. */
+static void clear_new_rrs(zone_node_t *node, uint16_t type)
+{
+ knot_rdataset_t *new_rrs = node_rdataset(node, type);
+ if (new_rrs) {
+ knot_rdataset_clear(new_rrs, NULL);
+ }
+}
+
+/*! \brief Logs redundant rrset operation. */
+static void can_log_rrset(const knot_rrset_t *rrset, int pos, apply_ctx_t *ctx, bool remove)
+{
+ if (!(ctx->flags & APPLY_STRICT)) {
+ return;
+ }
+
+ char type[16];
+ char data[1024];
+ const char *msg = remove ? "cannot remove nonexisting RR" :
+ "cannot add existing RR";
+
+ char *owner = knot_dname_to_str_alloc(rrset->owner);
+ if (owner != NULL && knot_rrtype_to_string(rrset->type, type, sizeof(type)) > 0 &&
+ knot_rrset_txt_dump_data(rrset, pos, data, sizeof(data), &KNOT_DUMP_STYLE_DEFAULT) > 0) {
+ log_zone_debug(ctx->contents->apex->owner,
+ "node %s, type %s, data '%s', %s", owner, type, data, msg);
+ }
+ free(owner);
+}
+
+/*! \brief Returns true if given RR is present in node and can be removed. */
+static bool can_remove(const zone_node_t *node, const knot_rrset_t *rrset, apply_ctx_t *ctx)
+{
+ if (node == NULL) {
+ // Node does not exist, cannot remove anything.
+ can_log_rrset(rrset, 0, ctx, true);
+ return false;
+ }
+
+ const knot_rdataset_t *node_rrs = node_rdataset(node, rrset->type);
+ if (node_rrs == NULL) {
+ // Node does not have this type at all.
+ can_log_rrset(rrset, 0, ctx, true);
+ return false;
+ }
+
+ knot_rdata_t *rr_cmp = rrset->rrs.rdata;
+ for (uint16_t i = 0; i < rrset->rrs.count; ++i) {
+ if (!knot_rdataset_member(node_rrs, rr_cmp)) {
+ // At least one RR doesnt' match.
+ can_log_rrset(rrset, i, ctx, true);
+ return false;
+ }
+ rr_cmp = knot_rdataset_next(rr_cmp);
+ }
+
+ return true;
+}
+
+/*! \brief Returns true if given RR is not present in node and can be added. */
+static bool can_add(const zone_node_t *node, const knot_rrset_t *rrset, apply_ctx_t *ctx)
+{
+ if (node == NULL) {
+ // Node does not exist, can add anything.
+ return true;
+ }
+ const knot_rdataset_t *node_rrs = node_rdataset(node, rrset->type);
+ if (node_rrs == NULL) {
+ // Node does not have this type at all.
+ return true;
+ }
+
+ knot_rdata_t *rr_cmp = rrset->rrs.rdata;
+ for (uint16_t i = 0; i < rrset->rrs.count; ++i) {
+ if (knot_rdataset_member(node_rrs, rr_cmp)) {
+ // No RR must match.
+ can_log_rrset(rrset, i, ctx, false);
+ return false;
+ }
+ rr_cmp = knot_rdataset_next(rr_cmp);
+ }
+
+ return true;
+}
+
+int apply_init_ctx(apply_ctx_t *ctx, zone_contents_t *contents, uint32_t flags)
+{
+ if (ctx == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ ctx->contents = contents;
+
+ ctx->node_ptrs = zone_tree_create(true);
+ if (ctx->node_ptrs == NULL) {
+ return KNOT_ENOMEM;
+ }
+ ctx->node_ptrs->flags = contents->nodes->flags;
+
+ ctx->nsec3_ptrs = zone_tree_create(true);
+ if (ctx->nsec3_ptrs == NULL) {
+ zone_tree_free(&ctx->node_ptrs);
+ return KNOT_ENOMEM;
+ }
+ ctx->nsec3_ptrs->flags = contents->nodes->flags;
+
+ ctx->adjust_ptrs = zone_tree_create(true);
+ if (ctx->adjust_ptrs == NULL) {
+ zone_tree_free(&ctx->nsec3_ptrs);
+ zone_tree_free(&ctx->node_ptrs);
+ return KNOT_ENOMEM;
+ }
+ ctx->adjust_ptrs->flags = contents->nodes->flags;
+
+ ctx->flags = flags;
+
+ return KNOT_EOK;
+}
+
+static zone_node_t *add_node_cb(const knot_dname_t *owner, void *ctx)
+{
+ zone_tree_t *tree = ctx;
+ zone_node_t *node = zone_tree_get(tree, owner);
+ if (node == NULL) {
+ node = node_new_for_tree(owner, tree, NULL);
+ } else {
+ node->flags &= ~NODE_FLAGS_DELETED;
+ }
+ return node;
+}
+
+int apply_add_rr(apply_ctx_t *ctx, const knot_rrset_t *rr)
+{
+ zone_contents_t *contents = ctx->contents;
+ bool nsec3rel = knot_rrset_is_nsec3rel(rr);
+ zone_tree_t *ptrs = nsec3rel ? ctx->nsec3_ptrs : ctx->node_ptrs;
+ zone_tree_t *tree = zone_contents_tree_for_rr(contents, rr);
+ if (tree == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Get or create node with this owner, search changes first
+ zone_node_t *node = NULL;
+ int ret = zone_tree_add_node(tree, contents->apex, rr->owner, add_node_cb, ptrs, &node);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (!can_add(node, rr, ctx)) {
+ return (ctx->flags & APPLY_STRICT) ? KNOT_EISRECORD : KNOT_EOK;
+ }
+
+ ret = zone_tree_insert_with_parents(ptrs, node, nsec3rel);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (binode_rdata_shared(node, rr->type)) {
+ // Modifying existing RRSet.
+ ret = replace_rdataset_with_copy(node, rr->type);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ // Insert new RR to RRSet, data will be copied.
+ ret = node_add_rrset(node, rr, NULL);
+ if (ret == KNOT_ETTL) {
+ // this shall not happen except applying journal created before this bugfix
+ return KNOT_EOK;
+ }
+ return ret;
+}
+
+int apply_remove_rr(apply_ctx_t *ctx, const knot_rrset_t *rr)
+{
+ zone_contents_t *contents = ctx->contents;
+ bool nsec3rel = knot_rrset_is_nsec3rel(rr);
+ zone_tree_t *ptrs = nsec3rel ? ctx->nsec3_ptrs : ctx->node_ptrs;
+ zone_tree_t *tree = zone_contents_tree_for_rr(contents, rr);
+ if (tree == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Find node for this owner
+ zone_node_t *node = zone_contents_find_node_for_rr(contents, rr);
+ if (!can_remove(node, rr, ctx)) {
+ return (ctx->flags & APPLY_STRICT) ? KNOT_ENORECORD : KNOT_EOK;
+ }
+
+ int ret = zone_tree_insert_with_parents(ptrs, node, nsec3rel);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (binode_rdata_shared(node, rr->type)) {
+ ret = replace_rdataset_with_copy(node, rr->type);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ ret = node_remove_rrset(node, rr, NULL);
+ if (ret != KNOT_EOK) {
+ clear_new_rrs(node, rr->type);
+ return ret;
+ }
+
+ if (node->rrset_count == 0 && node->children == 0 && node != contents->apex) {
+ zone_tree_del_node(tree, node, false);
+ }
+
+ return KNOT_EOK;
+}
+
+int apply_replace_soa(apply_ctx_t *ctx, const knot_rrset_t *rr)
+{
+ zone_contents_t *contents = ctx->contents;
+
+ if (!knot_dname_is_equal(rr->owner, contents->apex->owner)) {
+ return KNOT_EDENIED;
+ }
+
+ knot_rrset_t old_soa = node_rrset(contents->apex, KNOT_RRTYPE_SOA);
+
+ int ret = apply_remove_rr(ctx, &old_soa);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Check for SOA with proper serial but different rdata.
+ if (node_rrtype_exists(contents->apex, KNOT_RRTYPE_SOA)) {
+ return KNOT_ESOAINVAL;
+ }
+
+ return apply_add_rr(ctx, rr);
+}
+
+void apply_cleanup(apply_ctx_t *ctx)
+{
+ if (ctx == NULL) {
+ return;
+ }
+
+ if (ctx->flags & APPLY_UNIFY_FULL) {
+ zone_trees_unify_binodes(ctx->contents->nodes, ctx->contents->nsec3_nodes, true);
+ } else {
+ zone_trees_unify_binodes(ctx->adjust_ptrs, NULL, false); // beware there might be duplicities in ctx->adjust_ptrs and ctx->node_ptrs, so we don't free here
+ zone_trees_unify_binodes(ctx->node_ptrs, ctx->nsec3_ptrs, true);
+ }
+
+ zone_tree_free(&ctx->node_ptrs);
+ zone_tree_free(&ctx->nsec3_ptrs);
+ zone_tree_free(&ctx->adjust_ptrs);
+
+ if (ctx->cow_mutex != NULL) {
+ knot_sem_post(ctx->cow_mutex);
+ }
+}
+
+void apply_rollback(apply_ctx_t *ctx)
+{
+ if (ctx == NULL) {
+ return;
+ }
+
+ if (ctx->node_ptrs != NULL) {
+ ctx->node_ptrs->flags ^= ZONE_TREE_BINO_SECOND;
+ }
+ if (ctx->nsec3_ptrs != NULL) {
+ ctx->nsec3_ptrs->flags ^= ZONE_TREE_BINO_SECOND;
+ }
+ zone_trees_unify_binodes(ctx->node_ptrs, ctx->nsec3_ptrs, true);
+
+ zone_tree_free(&ctx->node_ptrs);
+ zone_tree_free(&ctx->nsec3_ptrs);
+ zone_tree_free(&ctx->adjust_ptrs);
+
+ trie_cow_rollback(ctx->contents->nodes->cow, NULL, NULL);
+ ctx->contents->nodes->cow = NULL;
+ if (ctx->contents->nsec3_nodes != NULL && ctx->contents->nsec3_nodes->cow != NULL) {
+ trie_cow_rollback(ctx->contents->nsec3_nodes->cow, NULL, NULL);
+ ctx->contents->nsec3_nodes->cow = NULL;
+ } else if (ctx->contents->nsec3_nodes != NULL) {
+ zone_tree_free(&ctx->contents->nsec3_nodes);
+ ctx->contents->nsec3_nodes = NULL;
+ }
+
+ free(ctx->contents->nodes);
+ free(ctx->contents->nsec3_nodes);
+
+ dnssec_nsec3_params_free(&ctx->contents->nsec3_params);
+
+ free(ctx->contents);
+
+ if (ctx->cow_mutex != NULL) {
+ knot_sem_post(ctx->cow_mutex);
+ }
+}
+
+void update_free_zone(zone_contents_t *contents)
+{
+ if (contents == NULL) {
+ return;
+ }
+
+ trie_cow_commit(contents->nodes->cow, NULL, NULL);
+ contents->nodes->cow = NULL;
+ if (contents->nsec3_nodes != NULL && contents->nsec3_nodes->cow != NULL) {
+ trie_cow_commit(contents->nsec3_nodes->cow, NULL, NULL);
+ contents->nsec3_nodes->cow = NULL;
+ }
+
+ free(contents->nodes);
+ free(contents->nsec3_nodes);
+
+ dnssec_nsec3_params_free(&contents->nsec3_params);
+
+ free(contents);
+}
diff --git a/src/knot/updates/apply.h b/src/knot/updates/apply.h
new file mode 100644
index 0000000..2d3588b
--- /dev/null
+++ b/src/knot/updates/apply.h
@@ -0,0 +1,101 @@
+/* Copyright (C) 2021 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "contrib/semaphore.h"
+#include "knot/zone/contents.h"
+#include "knot/updates/changesets.h"
+#include "contrib/ucw/lists.h"
+
+enum {
+ APPLY_STRICT = 1 << 0, /*!< Apply strictly, don't ignore removing non-existent RRs. */
+ APPLY_UNIFY_FULL = 1 << 1, /*!< When cleaning up successful update, perform full trees nodes unify. */
+};
+
+struct apply_ctx {
+ zone_contents_t *contents;
+ zone_tree_t *node_ptrs; /*!< Just pointers to the affected nodes in contents. */
+ zone_tree_t *nsec3_ptrs; /*!< The same for NSEC3 nodes. */
+ zone_tree_t *adjust_ptrs; /*!< Pointers to nodes affected by adjusting. */
+ uint32_t flags;
+ knot_sem_t *cow_mutex;
+};
+
+typedef struct apply_ctx apply_ctx_t;
+
+/*!
+ * \brief Initialize a new context structure.
+ *
+ * \param ctx Context to be initialized.
+ * \param contents Zone contents to apply changes onto.
+ * \param flags Flags to control the application process.
+ *
+ * \return KNOT_E*
+ */
+int apply_init_ctx(apply_ctx_t *ctx, zone_contents_t *contents, uint32_t flags);
+
+/*!
+ * \brief Adds a single RR into zone contents.
+ *
+ * \param ctx Apply context.
+ * \param rr RRSet to add.
+ *
+ * \return KNOT_E*
+ */
+int apply_add_rr(apply_ctx_t *ctx, const knot_rrset_t *rr);
+
+/*!
+ * \brief Removes single RR from zone contents.
+ *
+ * \param ctx Apply context.
+ * \param rr RRSet to remove.
+ *
+ * \return KNOT_E*
+ */
+int apply_remove_rr(apply_ctx_t *ctx, const knot_rrset_t *rr);
+
+/*!
+ * \brief Remove SOA and add a new SOA.
+ *
+ * \param ctx Apply context.
+ * \param rr New SOA to be added.
+ *
+ * \return KNOT_E*
+ */
+int apply_replace_soa(apply_ctx_t *ctx, const knot_rrset_t *rr);
+
+/*!
+ * \brief Cleanups successful zone update.
+ *
+ * \param ctx Context used to create the update.
+ */
+void apply_cleanup(apply_ctx_t *ctx);
+
+/*!
+ * \brief Rollbacks failed zone update.
+ *
+ * \param ctx Context used to create the update.
+ */
+void apply_rollback(apply_ctx_t *ctx);
+
+/*!
+ * \brief Shallow frees zone contents - either shallow copy after failed update
+ * or original zone contents after successful update.
+ *
+ * \param contents Contents to free.
+ */
+void update_free_zone(zone_contents_t *contents);
diff --git a/src/knot/updates/changesets.c b/src/knot/updates/changesets.c
new file mode 100644
index 0000000..1d1a0d3
--- /dev/null
+++ b/src/knot/updates/changesets.c
@@ -0,0 +1,628 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+#include <stdarg.h>
+
+#include "knot/updates/changesets.h"
+#include "knot/updates/apply.h"
+#include "knot/zone/zone-dump.h"
+#include "contrib/color.h"
+#include "contrib/time.h"
+#include "libknot/libknot.h"
+
+static int handle_soa(knot_rrset_t **soa, const knot_rrset_t *rrset)
+{
+ assert(soa);
+ assert(rrset);
+
+ if (*soa != NULL) {
+ knot_rrset_free(*soa, NULL);
+ }
+
+ *soa = knot_rrset_copy(rrset, NULL);
+ if (*soa == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ return KNOT_EOK;
+}
+
+/*! \brief Adds RRSet to given zone. */
+static int add_rr_to_contents(zone_contents_t *z, const knot_rrset_t *rrset)
+{
+ _unused_ zone_node_t *n = NULL;
+ int ret = zone_contents_add_rr(z, rrset, &n);
+
+ // We don't care of TTLs.
+ return ret == KNOT_ETTL ? KNOT_EOK : ret;
+}
+
+/*! \brief Inits changeset iterator with given tries. */
+static int changeset_iter_init(changeset_iter_t *ch_it, size_t tries, ...)
+{
+ memset(ch_it, 0, sizeof(*ch_it));
+
+ va_list args;
+ va_start(args, tries);
+
+ assert(tries <= sizeof(ch_it->trees) / sizeof(*ch_it->trees));
+ for (size_t i = 0; i < tries; ++i) {
+ zone_tree_t *t = va_arg(args, zone_tree_t *);
+ if (t == NULL) {
+ continue;
+ }
+
+ ch_it->trees[ch_it->n_trees++] = t;
+ }
+
+ va_end(args);
+
+ assert(ch_it->n_trees);
+ return zone_tree_it_begin(ch_it->trees[0], &ch_it->it);
+}
+
+// removes from counterpart what is in rr.
+// fixed_rr is an output parameter, holding a copy of rr without what has been removed from counterpart
+static void check_redundancy(zone_contents_t *counterpart, const knot_rrset_t *rr, knot_rrset_t **fixed_rr)
+{
+ if (fixed_rr != NULL) {
+ *fixed_rr = knot_rrset_copy(rr, NULL);
+ }
+
+ zone_node_t *node = zone_contents_find_node_for_rr(counterpart, rr);
+ if (node == NULL) {
+ return;
+ }
+
+ if (!node_rrtype_exists(node, rr->type)) {
+ return;
+ }
+
+ uint32_t rrs_ttl = node_rrset(node, rr->type).ttl;
+
+ if (fixed_rr != NULL && *fixed_rr != NULL &&
+ ((*fixed_rr)->ttl == rrs_ttl || rr->type == KNOT_RRTYPE_RRSIG)) {
+ int ret = knot_rdataset_subtract(&(*fixed_rr)->rrs, node_rdataset(node, rr->type), NULL);
+ if (ret != KNOT_EOK) {
+ return;
+ }
+ }
+
+ // TTL of RRSIGs is better determined by original_ttl field, which is compared as part of rdata anyway
+ if (rr->ttl == rrs_ttl || rr->type == KNOT_RRTYPE_RRSIG) {
+ int ret = node_remove_rrset(node, rr, NULL);
+ if (ret != KNOT_EOK) {
+ return;
+ }
+ }
+
+ if (node->rrset_count == 0 && node->children == 0 && node != counterpart->apex) {
+ zone_tree_t *t = knot_rrset_is_nsec3rel(rr) ?
+ counterpart->nsec3_nodes : counterpart->nodes;
+ zone_tree_del_node(t, node, true);
+ }
+
+ return;
+}
+
+int changeset_init(changeset_t *ch, const knot_dname_t *apex)
+{
+ memset(ch, 0, sizeof(changeset_t));
+
+ // Init local changes
+ ch->add = zone_contents_new(apex, false);
+ if (ch->add == NULL) {
+ return KNOT_ENOMEM;
+ }
+ ch->remove = zone_contents_new(apex, false);
+ if (ch->remove == NULL) {
+ zone_contents_free(ch->add);
+ return KNOT_ENOMEM;
+ }
+
+ return KNOT_EOK;
+}
+
+changeset_t *changeset_new(const knot_dname_t *apex)
+{
+ changeset_t *ret = malloc(sizeof(changeset_t));
+ if (ret == NULL) {
+ return NULL;
+ }
+
+ if (changeset_init(ret, apex) == KNOT_EOK) {
+ return ret;
+ } else {
+ free(ret);
+ return NULL;
+ }
+}
+
+bool changeset_empty(const changeset_t *ch)
+{
+ if (ch == NULL) {
+ return true;
+ }
+
+ if (zone_contents_is_empty(ch->remove) &&
+ zone_contents_is_empty(ch->add)) {
+ if (ch->soa_to == NULL) {
+ return true;
+ }
+ if (ch->soa_from != NULL && ch->soa_to != NULL &&
+ knot_rrset_equal(ch->soa_from, ch->soa_to, false)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+size_t changeset_size(const changeset_t *ch)
+{
+ if (ch == NULL) {
+ return 0;
+ }
+
+ changeset_iter_t itt;
+ changeset_iter_all(&itt, ch);
+
+ size_t size = 0;
+ knot_rrset_t rr = changeset_iter_next(&itt);
+ while(!knot_rrset_empty(&rr)) {
+ ++size;
+ rr = changeset_iter_next(&itt);
+ }
+ changeset_iter_clear(&itt);
+
+ if (!knot_rrset_empty(ch->soa_from)) {
+ size += 1;
+ }
+ if (!knot_rrset_empty(ch->soa_to)) {
+ size += 1;
+ }
+
+ return size;
+}
+
+int changeset_add_addition(changeset_t *ch, const knot_rrset_t *rrset, changeset_flag_t flags)
+{
+ if (!ch || !rrset) {
+ return KNOT_EINVAL;
+ }
+
+ if (rrset->type == KNOT_RRTYPE_SOA) {
+ /* Do not add SOAs into actual contents. */
+ return handle_soa(&ch->soa_to, rrset);
+ }
+
+ knot_rrset_t *rrset_cancelout = NULL;
+
+ /* Check if there's any removal and remove that, then add this
+ * addition anyway. Required to change TTLs. */
+ if (flags & CHANGESET_CHECK) {
+ /* If we delete the rrset, we need to hold a copy to add it later */
+ rrset = knot_rrset_copy(rrset, NULL);
+ if (rrset == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ check_redundancy(ch->remove, rrset, &rrset_cancelout);
+ }
+
+ const knot_rrset_t *to_add = (rrset_cancelout == NULL ? rrset : rrset_cancelout);
+ int ret = knot_rrset_empty(to_add) ? KNOT_EOK : add_rr_to_contents(ch->add, to_add);
+
+ if (flags & CHANGESET_CHECK) {
+ knot_rrset_free((knot_rrset_t *)rrset, NULL);
+ }
+ knot_rrset_free(rrset_cancelout, NULL);
+
+ return ret;
+}
+
+int changeset_add_removal(changeset_t *ch, const knot_rrset_t *rrset, changeset_flag_t flags)
+{
+ if (!ch || !rrset) {
+ return KNOT_EINVAL;
+ }
+
+ if (rrset->type == KNOT_RRTYPE_SOA) {
+ /* Do not add SOAs into actual contents. */
+ return handle_soa(&ch->soa_from, rrset);
+ }
+
+ knot_rrset_t *rrset_cancelout = NULL;
+
+ /* Check if there's any addition and remove that, then add this
+ * removal anyway. */
+ if (flags & CHANGESET_CHECK) {
+ /* If we delete the rrset, we need to hold a copy to add it later */
+ rrset = knot_rrset_copy(rrset, NULL);
+ if (rrset == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ check_redundancy(ch->add, rrset, &rrset_cancelout);
+ }
+
+ const knot_rrset_t *to_remove = (rrset_cancelout == NULL ? rrset : rrset_cancelout);
+ int ret = (knot_rrset_empty(to_remove) || ch->remove == NULL) ? KNOT_EOK : add_rr_to_contents(ch->remove, to_remove);
+
+ if (flags & CHANGESET_CHECK) {
+ knot_rrset_free((knot_rrset_t *)rrset, NULL);
+ }
+ knot_rrset_free(rrset_cancelout, NULL);
+
+ return ret;
+}
+
+int changeset_remove_addition(changeset_t *ch, const knot_rrset_t *rrset)
+{
+ if (rrset->type == KNOT_RRTYPE_SOA) {
+ /* Do not add SOAs into actual contents. */
+ if (ch->soa_to != NULL) {
+ knot_rrset_free(ch->soa_to, NULL);
+ ch->soa_to = NULL;
+ }
+ return KNOT_EOK;
+ }
+
+ zone_node_t *n = NULL;
+ return zone_contents_remove_rr(ch->add, rrset, &n);
+}
+
+int changeset_remove_removal(changeset_t *ch, const knot_rrset_t *rrset)
+{
+ if (rrset->type == KNOT_RRTYPE_SOA) {
+ /* Do not add SOAs into actual contents. */
+ if (ch->soa_from != NULL) {
+ knot_rrset_free(ch->soa_from, NULL);
+ ch->soa_from = NULL;
+ }
+ return KNOT_EOK;
+ }
+
+ zone_node_t *n = NULL;
+ return zone_contents_remove_rr(ch->remove, rrset, &n);
+}
+
+int changeset_merge(changeset_t *ch1, const changeset_t *ch2, int flags)
+{
+ changeset_iter_t itt;
+ changeset_iter_rem(&itt, ch2);
+
+ knot_rrset_t rrset = changeset_iter_next(&itt);
+ while (!knot_rrset_empty(&rrset)) {
+ int ret = changeset_add_removal(ch1, &rrset, CHANGESET_CHECK | flags);
+ if (ret != KNOT_EOK) {
+ changeset_iter_clear(&itt);
+ return ret;
+ }
+ rrset = changeset_iter_next(&itt);
+ }
+ changeset_iter_clear(&itt);
+
+ changeset_iter_add(&itt, ch2);
+
+ rrset = changeset_iter_next(&itt);
+ while (!knot_rrset_empty(&rrset)) {
+ int ret = changeset_add_addition(ch1, &rrset, CHANGESET_CHECK | flags);
+ if (ret != KNOT_EOK) {
+ changeset_iter_clear(&itt);
+ return ret;
+ }
+ rrset = changeset_iter_next(&itt);
+ }
+ changeset_iter_clear(&itt);
+
+ // Use soa_to and serial from the second changeset
+ // soa_to from the first changeset is redundant, delete it
+ if (ch2->soa_to == NULL && ch2->soa_from == NULL) {
+ // but not if ch2 has no soa change
+ return KNOT_EOK;
+ }
+ knot_rrset_t *soa_copy = knot_rrset_copy(ch2->soa_to, NULL);
+ if (soa_copy == NULL && ch2->soa_to) {
+ return KNOT_ENOMEM;
+ }
+ knot_rrset_free(ch1->soa_to, NULL);
+ ch1->soa_to = soa_copy;
+
+ return KNOT_EOK;
+}
+
+uint32_t changeset_from(const changeset_t *ch)
+{
+ return ch->soa_from == NULL ? 0 : knot_soa_serial(ch->soa_from->rrs.rdata);
+}
+
+uint32_t changeset_to(const changeset_t *ch)
+{
+ return ch->soa_to == NULL ? 0 : knot_soa_serial(ch->soa_to->rrs.rdata);
+}
+
+bool changeset_differs_just_serial(const changeset_t *ch, bool ignore_zonemd)
+{
+ if (ch == NULL || ch->soa_from == NULL || ch->soa_to == NULL) {
+ return false;
+ }
+
+ knot_rrset_t *soa_to_cpy = knot_rrset_copy(ch->soa_to, NULL);
+ knot_soa_serial_set(soa_to_cpy->rrs.rdata, knot_soa_serial(ch->soa_from->rrs.rdata));
+
+ bool ret = knot_rrset_equal(ch->soa_from, soa_to_cpy, true);
+ knot_rrset_free(soa_to_cpy, NULL);
+
+ changeset_iter_t itt;
+ changeset_iter_all(&itt, ch);
+
+ knot_rrset_t rrset = changeset_iter_next(&itt);
+ while (!knot_rrset_empty(&rrset) && ret) {
+ switch (rrset.type) {
+ case KNOT_RRTYPE_ZONEMD:
+ ret = ignore_zonemd;
+ break;
+ case KNOT_RRTYPE_RRSIG:
+ ; uint16_t covered = knot_rrsig_type_covered(rrset.rrs.rdata);
+ if (covered == KNOT_RRTYPE_SOA ||
+ (covered == KNOT_RRTYPE_ZONEMD && ignore_zonemd)) {
+ break;
+ }
+ // FALLTHROUGH
+ default:
+ ret = false;
+ break;
+ }
+ rrset = changeset_iter_next(&itt);
+ }
+ changeset_iter_clear(&itt);
+
+ return ret;
+}
+
+void changesets_clear(list_t *chgs)
+{
+ if (chgs) {
+ changeset_t *chg, *nxt;
+ WALK_LIST_DELSAFE(chg, nxt, *chgs) {
+ changeset_clear(chg);
+ rem_node(&chg->n);
+ }
+ init_list(chgs);
+ }
+}
+
+void changesets_free(list_t *chgs)
+{
+ if (chgs) {
+ changeset_t *chg, *nxt;
+ WALK_LIST_DELSAFE(chg, nxt, *chgs) {
+ rem_node(&chg->n);
+ changeset_free(chg);
+ }
+ init_list(chgs);
+ }
+}
+
+void changeset_clear(changeset_t *ch)
+{
+ if (ch == NULL) {
+ return;
+ }
+
+ // Delete RRSets in lists, in case there are any left
+ zone_contents_deep_free(ch->add);
+ zone_contents_deep_free(ch->remove);
+ ch->add = NULL;
+ ch->remove = NULL;
+
+ knot_rrset_free(ch->soa_from, NULL);
+ knot_rrset_free(ch->soa_to, NULL);
+ ch->soa_from = NULL;
+ ch->soa_to = NULL;
+
+ // Delete binary data
+ free(ch->data);
+}
+
+changeset_t *changeset_clone(const changeset_t *ch)
+{
+ if (ch == NULL) {
+ return NULL;
+ }
+
+ changeset_t *res = changeset_new(ch->add->apex->owner);
+ if (res == NULL) {
+ return NULL;
+ }
+
+ res->soa_from = knot_rrset_copy(ch->soa_from, NULL);
+ res->soa_to = knot_rrset_copy(ch->soa_to, NULL);
+
+ int ret = KNOT_EOK;
+ changeset_iter_t itt;
+
+ changeset_iter_rem(&itt, ch);
+ knot_rrset_t rr = changeset_iter_next(&itt);
+ while (!knot_rrset_empty(&rr) && ret == KNOT_EOK) {
+ ret = changeset_add_removal(res, &rr, 0);
+ rr = changeset_iter_next(&itt);
+ }
+ changeset_iter_clear(&itt);
+
+ changeset_iter_add(&itt, ch);
+ rr = changeset_iter_next(&itt);
+ while (!knot_rrset_empty(&rr) && ret == KNOT_EOK) {
+ ret = changeset_add_addition(res, &rr, 0);
+ rr = changeset_iter_next(&itt);
+ }
+ changeset_iter_clear(&itt);
+
+ if ((ch->soa_from != NULL && res->soa_from == NULL) ||
+ (ch->soa_to != NULL && res->soa_to == NULL) ||
+ ret != KNOT_EOK) {
+ changeset_free(res);
+ return NULL;
+ }
+
+ return res;
+}
+
+void changeset_free(changeset_t *ch)
+{
+ changeset_clear(ch);
+ free(ch);
+}
+
+int changeset_iter_add(changeset_iter_t *itt, const changeset_t *ch)
+{
+ return changeset_iter_init(itt, 2, ch->add->nodes, ch->add->nsec3_nodes);
+}
+
+int changeset_iter_rem(changeset_iter_t *itt, const changeset_t *ch)
+{
+ return changeset_iter_init(itt, 2, ch->remove->nodes, ch->remove->nsec3_nodes);
+}
+
+int changeset_iter_all(changeset_iter_t *itt, const changeset_t *ch)
+{
+ return changeset_iter_init(itt, 4, ch->add->nodes, ch->add->nsec3_nodes,
+ ch->remove->nodes, ch->remove->nsec3_nodes);
+}
+
+knot_rrset_t changeset_iter_next(changeset_iter_t *it)
+{
+ assert(it);
+
+ knot_rrset_t rr;
+ while (it->node == NULL || it->node_pos >= it->node->rrset_count) {
+ if (it->node != NULL) {
+ zone_tree_it_next(&it->it);
+ }
+ while (zone_tree_it_finished(&it->it)) {
+ zone_tree_it_free(&it->it);
+ if (--it->n_trees > 0) {
+ for (size_t i = 0; i < it->n_trees; i++) {
+ it->trees[i] = it->trees[i + 1];
+ }
+ (void)zone_tree_it_begin(it->trees[0], &it->it);
+ } else {
+ knot_rrset_init_empty(&rr);
+ return rr;
+ }
+ }
+ it->node = zone_tree_it_val(&it->it);
+ it->node_pos = 0;
+ }
+ rr = node_rrset_at(it->node, it->node_pos++);
+ assert(!knot_rrset_empty(&rr));
+ return rr;
+}
+
+void changeset_iter_clear(changeset_iter_t *it)
+{
+ if (it) {
+ zone_tree_it_free(&it->it);
+ it->node = NULL;
+ it->node_pos = 0;
+ }
+}
+
+int changeset_walk(const changeset_t *changeset, changeset_walk_callback callback, void *ctx)
+{
+ changeset_iter_t it;
+ int ret = changeset_iter_rem(&it, changeset);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ knot_rrset_t rrset = changeset_iter_next(&it);
+ while (!knot_rrset_empty(&rrset)) {
+ ret = callback(&rrset, false, ctx);
+ if (ret != KNOT_EOK) {
+ changeset_iter_clear(&it);
+ return ret;
+ }
+ rrset = changeset_iter_next(&it);
+ }
+ changeset_iter_clear(&it);
+
+ if (changeset->soa_from != NULL) {
+ ret = callback(changeset->soa_from, false, ctx);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ ret = changeset_iter_add(&it, changeset);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ rrset = changeset_iter_next(&it);
+ while (!knot_rrset_empty(&rrset)) {
+ ret = callback(&rrset, true, ctx);
+ if (ret != KNOT_EOK) {
+ changeset_iter_clear(&it);
+ return ret;
+ }
+ rrset = changeset_iter_next(&it);
+ }
+ changeset_iter_clear(&it);
+
+ if (changeset->soa_to != NULL) {
+ ret = callback(changeset->soa_to, true, ctx);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+void changeset_print(const changeset_t *changeset, FILE *outfile, bool color)
+{
+ size_t buflen = 1024;
+ char *buff = malloc(buflen);
+
+ knot_dump_style_t style = KNOT_DUMP_STYLE_DEFAULT;
+ style.now = knot_time();
+
+ style.color = COL_RED(color);
+ if (changeset->soa_from != NULL || !zone_contents_is_empty(changeset->remove)) {
+ fprintf(outfile, "%s;; Removed%s\n", style.color, COL_RST(color));
+ }
+ if (changeset->soa_from != NULL && buff != NULL) {
+ (void)knot_rrset_txt_dump(changeset->soa_from, &buff, &buflen, &style);
+ fprintf(outfile, "%s%s%s", style.color, buff, COL_RST(color));
+ }
+ (void)zone_dump_text(changeset->remove, outfile, false, style.color);
+
+ style.color = COL_GRN(color);
+ if (changeset->soa_to != NULL || !zone_contents_is_empty(changeset->add)) {
+ fprintf(outfile, "%s;; Added%s\n", style.color, COL_RST(color));
+ }
+ if (changeset->soa_to != NULL && buff != NULL) {
+ (void)knot_rrset_txt_dump(changeset->soa_to, &buff, &buflen, &style);
+ fprintf(outfile, "%s%s%s", style.color, buff, COL_RST(color));
+ }
+ (void)zone_dump_text(changeset->add, outfile, false, style.color);
+
+ free(buff);
+}
diff --git a/src/knot/updates/changesets.h b/src/knot/updates/changesets.h
new file mode 100644
index 0000000..1234cb9
--- /dev/null
+++ b/src/knot/updates/changesets.h
@@ -0,0 +1,290 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdio.h>
+
+#include "libknot/rrset.h"
+#include "knot/zone/contents.h"
+#include "contrib/ucw/lists.h"
+
+/*! \brief Changeset addition/removal flags */
+typedef enum {
+ CHANGESET_NONE = 0,
+ CHANGESET_CHECK = 1 << 0, /*! Perform redundancy check on additions/removals */
+} changeset_flag_t;
+
+/*! \brief One zone change, from 'soa_from' to 'soa_to'. */
+typedef struct {
+ node_t n; /*!< List node. */
+ knot_rrset_t *soa_from; /*!< Start SOA. */
+ knot_rrset_t *soa_to; /*!< Destination SOA. */
+ zone_contents_t *add; /*!< Change additions. */
+ zone_contents_t *remove; /*!< Change removals. */
+ size_t size; /*!< Size of serialized changeset. \todo Remove after old_journal removal! */
+ uint8_t *data; /*!< Serialized changeset. */
+} changeset_t;
+
+/*! \brief Changeset iteration structure. */
+typedef struct {
+ list_t iters; /*!< List of pending zone iterators. */
+ zone_tree_t *trees[4]; /*!< Pointers to zone trees to iterate over. */
+ size_t n_trees; /*!< Their count. */
+ zone_tree_it_t it; /*!< Zone tree iterator. */
+ const zone_node_t *node; /*!< Current zone node. */
+ uint16_t node_pos; /*!< Position in node. */
+} changeset_iter_t;
+
+/*!
+ * \brief Inits changeset structure.
+ *
+ * \param ch Changeset to init.
+ * \param apex Zone apex DNAME.
+ *
+ * \return KNOT_E*
+ */
+int changeset_init(changeset_t *ch, const knot_dname_t *apex);
+
+/*!
+ * \brief Creates new changeset structure and inits it.
+ *
+ * \param apex Zone apex DNAME.
+ *
+ * \return Changeset structure on success, NULL on errors.
+ */
+changeset_t *changeset_new(const knot_dname_t *apex);
+
+/*!
+ * \brief Checks whether changeset is empty, i.e. no change will happen after its application.
+ *
+ * \param ch Changeset to be checked.
+ *
+ * \retval true if changeset is empty.
+ * \retval false if changeset is not empty.
+ */
+bool changeset_empty(const changeset_t *ch);
+
+/*!
+ * \brief Get number of changes (additions and removals) in the changeset.
+ *
+ * \param ch Changeset to be checked.
+ *
+ * \return Number of changes in the changeset.
+ */
+size_t changeset_size(const changeset_t *ch);
+
+/*!
+ * \brief Add RRSet to 'add' part of changeset.
+ *
+ * \param ch Changeset to add RRSet into.
+ * \param rrset RRSet to be added.
+ * \param flags Changeset flags.
+ *
+ * \return KNOT_E*
+ */
+int changeset_add_addition(changeset_t *ch, const knot_rrset_t *rrset, changeset_flag_t flags);
+
+/*!
+ * \brief Add RRSet to 'remove' part of changeset.
+ *
+ * \param ch Changeset to add RRSet into.
+ * \param rrset RRSet to be added.
+ * \param flags Changeset flags.
+ *
+ * \return KNOT_E*
+ */
+int changeset_add_removal(changeset_t *ch, const knot_rrset_t *rrset, changeset_flag_t flags);
+
+
+/*!
+ * \brief Remove an RRSet from the 'add' part of changeset.
+ *
+ * \param ch Changeset to add RRSet into.
+ * \param rrset RRSet to be added.
+ *
+ * \return KNOT_E*
+ */
+int changeset_remove_addition(changeset_t *ch, const knot_rrset_t *rrset);
+
+/*!
+ * \brief Remove an RRSet from the 'remove' part of changeset.
+ *
+ * \param ch Changeset to add RRSet into.
+ * \param rrset RRSet to be added.
+ *
+ * \return KNOT_E*
+ */
+int changeset_remove_removal(changeset_t *ch, const knot_rrset_t *rrset);
+
+/*!
+ * \brief Merges two changesets together.
+ *
+ * \param ch1 Merge into this changeset.
+ * \param ch2 Merge this changeset.
+ * \param flags Flags how to handle redundancies.
+ *
+ * \return KNOT_E*
+ */
+int changeset_merge(changeset_t *ch1, const changeset_t *ch2, int flags);
+
+/*!
+ * \brief Get serial "from" of the changeset.
+ *
+ * \param ch Changeset in question.
+ *
+ * \return Its serial "from", or 0 if none.
+ */
+uint32_t changeset_from(const changeset_t *ch);
+
+/*!
+ * \brief Get serial "to" of the changeset.
+ *
+ * \param ch Changeset in question.
+ *
+ * \return Its serial "to", or 0 if none.
+ */
+uint32_t changeset_to(const changeset_t *ch);
+
+/*!
+ * \brief Check the changes and SOA, ignoring possibly updated SOA serial and ZONEMD.
+ *
+ * \note Also tolerates changed RRSIG of SOA or ZONEMD.
+ *
+ * \param ch Changeset in question.
+ * \param ignore_zonemd If enabled, possible ZONEMD records are ignored.
+ *
+ * \retval false If the changeset changes other records than SOA, or some SOA field
+ * other than serial changed or optionally ZONEMD.
+ * \retval true Otherwise.
+ */
+bool changeset_differs_just_serial(const changeset_t *ch, bool ignore_zonemd);
+
+/*!
+ * \brief Clears changesets in list. Changesets are not free'd. Legacy.
+ *
+ * \param chgs Changeset list to clear.
+ */
+void changesets_clear(list_t *chgs);
+
+/*!
+ * \brief Free changesets in list. Legacy.
+ *
+ * \param chgs Changeset list to free.
+ */
+void changesets_free(list_t *chgs);
+
+/*!
+ * \brief Clear single changeset.
+ *
+ * \param ch Changeset to clear.
+ */
+void changeset_clear(changeset_t *ch);
+
+/*!
+ * \brief Copy changeset to newly allocated space, all rrsigs are copied.
+ *
+ * \param ch Changeset to be copied.
+ *
+ * \return a copy, or NULL if error.
+ */
+changeset_t *changeset_clone(const changeset_t *ch);
+
+/*!
+ * \brief Frees single changeset.
+ *
+ * \param ch Changeset to free.
+ */
+void changeset_free(changeset_t *ch);
+
+/*!
+ * \brief Inits changeset iteration structure with changeset additions.
+ *
+ * \param itt Iterator to init.
+ * \param ch Changeset to use.
+ *
+ * \return KNOT_E*
+ */
+int changeset_iter_add(changeset_iter_t *itt, const changeset_t *ch);
+
+/*!
+ * \brief Inits changeset iteration structure with changeset removals.
+ *
+ * \param itt Iterator to init.
+ * \param ch Changeset to use.
+ *
+ * \return KNOT_E*
+ */
+int changeset_iter_rem(changeset_iter_t *itt, const changeset_t *ch);
+
+/*!
+ * \brief Inits changeset iteration structure with changeset additions and removals.
+ *
+ * \param itt Iterator to init.
+ * \param ch Changeset to use.
+ *
+ * \return KNOT_E*
+ */
+int changeset_iter_all(changeset_iter_t *itt, const changeset_t *ch);
+
+/*!
+ * \brief Gets next RRSet from changeset iterator.
+ *
+ * \param it Changeset iterator.
+ *
+ * \return Next RRSet in iterator, empty RRSet if iteration done.
+ */
+knot_rrset_t changeset_iter_next(changeset_iter_t *it);
+
+/*!
+ * \brief Free resources allocated by changeset iterator.
+ *
+ * \param it Iterator to clear.
+ */
+void changeset_iter_clear(changeset_iter_t *it);
+
+/*!
+ * \brief A pointer type for callback for changeset_walk() function.
+ *
+ * \param rrset An actual removal/addition inside the changeset.
+ * \param addition Indicates addition against removal.
+ * \param ctx A context passed to the changeset_walk() function.
+ *
+ * \retval KNOT_EOK if all ok, iteration will continue
+ * \return KNOT_E* if error, iteration will stop immediately and changeset_walk() returns this error.
+ */
+typedef int (*changeset_walk_callback)(const knot_rrset_t *rrset, bool addition, void *ctx);
+
+/*!
+ * \brief Calls a callback for each removal/addition in the changeset.
+ *
+ * \param changeset Changeset.
+ * \param callback Callback.
+ * \param ctx Arbitrary context passed to the callback.
+ *
+ * \return KNOT_E*
+ */
+int changeset_walk(const changeset_t *changeset, changeset_walk_callback callback, void *ctx);
+
+/*!
+ *
+ * \brief Dumps the changeset into text file.
+ *
+ * \param changeset Changeset.
+ * \param outfile File to write into.
+ * \param color Use unix tty color metacharacters.
+ */
+void changeset_print(const changeset_t *changeset, FILE *outfile, bool color);
diff --git a/src/knot/updates/ddns.c b/src/knot/updates/ddns.c
new file mode 100644
index 0000000..eb75317
--- /dev/null
+++ b/src/knot/updates/ddns.c
@@ -0,0 +1,701 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "knot/common/log.h"
+#include "knot/updates/ddns.h"
+#include "knot/updates/changesets.h"
+#include "knot/updates/zone-update.h"
+#include "knot/zone/serial.h"
+#include "libknot/libknot.h"
+#include "contrib/ucw/lists.h"
+
+/*!< \brief Clears prereq RRSet list. */
+static void rrset_list_clear(list_t *l)
+{
+ node_t *n, *nxt;
+ WALK_LIST_DELSAFE(n, nxt, *l) {
+ ptrnode_t *ptr_n = (ptrnode_t *)n;
+ knot_rrset_t *rrset = (knot_rrset_t *)ptr_n->d;
+ knot_rrset_free(rrset, NULL);
+ free(n);
+ };
+}
+
+/*!< \brief Adds RR to prereq RRSet list, merges RRs into RRSets. */
+static int add_rr_to_list(list_t *l, const knot_rrset_t *rr)
+{
+ node_t *n;
+ WALK_LIST(n, *l) {
+ ptrnode_t *ptr_n = (ptrnode_t *)n;
+ knot_rrset_t *rrset = (knot_rrset_t *)ptr_n->d;
+ if (rrset->type == rr->type && knot_dname_is_equal(rrset->owner, rr->owner)) {
+ return knot_rdataset_merge(&rrset->rrs, &rr->rrs, NULL);
+ }
+ };
+
+ knot_rrset_t *rr_copy = knot_rrset_copy(rr, NULL);
+ if (rr_copy == NULL) {
+ return KNOT_ENOMEM;
+ }
+ return ptrlist_add(l, rr_copy, NULL) != NULL ? KNOT_EOK : KNOT_ENOMEM;
+}
+
+/*!< \brief Checks whether RRSet exists in the zone. */
+static int check_rrset_exists(zone_update_t *update, const knot_rrset_t *rrset,
+ uint16_t *rcode)
+{
+ assert(rrset->type != KNOT_RRTYPE_ANY);
+
+ const zone_node_t *node = zone_update_get_node(update, rrset->owner);
+ if (node == NULL || !node_rrtype_exists(node, rrset->type)) {
+ *rcode = KNOT_RCODE_NXRRSET;
+ return KNOT_EPREREQ;
+ } else {
+ knot_rrset_t found = node_rrset(node, rrset->type);
+ assert(!knot_rrset_empty(&found));
+ if (knot_rrset_equal(&found, rrset, false)) {
+ return KNOT_EOK;
+ } else {
+ *rcode = KNOT_RCODE_NXRRSET;
+ return KNOT_EPREREQ;
+ }
+ }
+}
+
+/*!< \brief Checks whether RRSets in the list exist in the zone. */
+static int check_stored_rrsets(list_t *l, zone_update_t *update,
+ uint16_t *rcode)
+{
+ node_t *n;
+ WALK_LIST(n, *l) {
+ ptrnode_t *ptr_n = (ptrnode_t *)n;
+ knot_rrset_t *rrset = (knot_rrset_t *)ptr_n->d;
+ int ret = check_rrset_exists(update, rrset, rcode);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ };
+
+ return KNOT_EOK;
+}
+
+/*!< \brief Checks whether node of given owner, with given type exists. */
+static bool check_type(zone_update_t *update, const knot_rrset_t *rrset)
+{
+ assert(rrset->type != KNOT_RRTYPE_ANY);
+ const zone_node_t *node = zone_update_get_node(update, rrset->owner);
+ if (node == NULL || !node_rrtype_exists(node, rrset->type)) {
+ return false;
+ }
+
+ return true;
+}
+
+/*!< \brief Checks whether RR type exists in the zone. */
+static int check_type_exist(zone_update_t *update,
+ const knot_rrset_t *rrset, uint16_t *rcode)
+{
+ assert(rrset->rclass == KNOT_CLASS_ANY);
+ if (check_type(update, rrset)) {
+ return KNOT_EOK;
+ } else {
+ *rcode = KNOT_RCODE_NXRRSET;
+ return KNOT_EPREREQ;
+ }
+}
+
+/*!< \brief Checks whether RR type is not in the zone. */
+static int check_type_not_exist(zone_update_t *update,
+ const knot_rrset_t *rrset, uint16_t *rcode)
+{
+ assert(rrset->rclass == KNOT_CLASS_NONE);
+ if (check_type(update, rrset)) {
+ *rcode = KNOT_RCODE_YXRRSET;
+ return KNOT_EPREREQ;
+ } else {
+ return KNOT_EOK;
+ }
+}
+
+/*!< \brief Checks whether DNAME is in the zone. */
+static int check_in_use(zone_update_t *update,
+ const knot_dname_t *dname, uint16_t *rcode)
+{
+ const zone_node_t *node = zone_update_get_node(update, dname);
+ if (node == NULL || node->rrset_count == 0) {
+ *rcode = KNOT_RCODE_NXDOMAIN;
+ return KNOT_EPREREQ;
+ } else {
+ return KNOT_EOK;
+ }
+}
+
+/*!< \brief Checks whether DNAME is not in the zone. */
+static int check_not_in_use(zone_update_t *update,
+ const knot_dname_t *dname, uint16_t *rcode)
+{
+ const zone_node_t *node = zone_update_get_node(update, dname);
+ if (node == NULL || node->rrset_count == 0) {
+ return KNOT_EOK;
+ } else {
+ *rcode = KNOT_RCODE_YXDOMAIN;
+ return KNOT_EPREREQ;
+ }
+}
+
+/*!< \brief Returns true if rrset has 0 data or RDATA of size 0 (we need TTL). */
+static bool rrset_empty(const knot_rrset_t *rrset)
+{
+ switch (rrset->rrs.count) {
+ case 0:
+ return true;
+ case 1:
+ return rrset->rrs.rdata->len == 0;
+ default:
+ return false;
+ }
+}
+
+/*!< \brief Checks prereq for given packet RR. */
+static int process_prereq(const knot_rrset_t *rrset, uint16_t qclass,
+ zone_update_t *update, uint16_t *rcode,
+ list_t *rrset_list)
+{
+ if (rrset->ttl != 0) {
+ *rcode = KNOT_RCODE_FORMERR;
+ return KNOT_EMALF;
+ }
+
+ if (knot_dname_in_bailiwick(rrset->owner, update->zone->name) < 0) {
+ *rcode = KNOT_RCODE_NOTZONE;
+ return KNOT_EOUTOFZONE;
+ }
+
+ if (rrset->rclass == KNOT_CLASS_ANY) {
+ if (!rrset_empty(rrset)) {
+ *rcode = KNOT_RCODE_FORMERR;
+ return KNOT_EMALF;
+ }
+ if (rrset->type == KNOT_RRTYPE_ANY) {
+ return check_in_use(update, rrset->owner, rcode);
+ } else {
+ return check_type_exist(update, rrset, rcode);
+ }
+ } else if (rrset->rclass == KNOT_CLASS_NONE) {
+ if (!rrset_empty(rrset)) {
+ *rcode = KNOT_RCODE_FORMERR;
+ return KNOT_EMALF;
+ }
+ if (rrset->type == KNOT_RRTYPE_ANY) {
+ return check_not_in_use(update, rrset->owner, rcode);
+ } else {
+ return check_type_not_exist(update, rrset, rcode);
+ }
+ } else if (rrset->rclass == qclass) {
+ // Store RRs for full check into list
+ int ret = add_rr_to_list(rrset_list, rrset);
+ if (ret != KNOT_EOK) {
+ *rcode = KNOT_RCODE_SERVFAIL;
+ }
+ return ret;
+ } else {
+ *rcode = KNOT_RCODE_FORMERR;
+ return KNOT_EMALF;
+ }
+}
+
+static inline bool is_addition(const knot_rrset_t *rr)
+{
+ return rr->rclass == KNOT_CLASS_IN;
+}
+
+static inline bool is_removal(const knot_rrset_t *rr)
+{
+ return rr->rclass == KNOT_CLASS_NONE || rr->rclass == KNOT_CLASS_ANY;
+}
+
+static inline bool is_rr_removal(const knot_rrset_t *rr)
+{
+ return rr->rclass == KNOT_CLASS_NONE;
+}
+
+static inline bool is_rrset_removal(const knot_rrset_t *rr)
+{
+ return rr->rclass == KNOT_CLASS_ANY && rr->type != KNOT_RRTYPE_ANY;
+}
+
+static inline bool is_node_removal(const knot_rrset_t *rr)
+{
+ return rr->rclass == KNOT_CLASS_ANY && rr->type == KNOT_RRTYPE_ANY;
+}
+
+/*!< \brief Returns true if last addition of certain types is to be replaced. */
+static bool should_replace(const knot_rrset_t *rrset)
+{
+ return rrset->type == KNOT_RRTYPE_CNAME ||
+ rrset->type == KNOT_RRTYPE_DNAME ||
+ rrset->type == KNOT_RRTYPE_NSEC3PARAM;
+}
+
+/*!< \brief Returns true if node contains given RR in its RRSets. */
+static bool node_contains_rr(const zone_node_t *node,
+ const knot_rrset_t *rrset)
+{
+ const knot_rdataset_t *zone_rrs = node_rdataset(node, rrset->type);
+ if (zone_rrs != NULL) {
+ assert(rrset->rrs.count == 1);
+ return knot_rdataset_member(zone_rrs, rrset->rrs.rdata);
+ } else {
+ return false;
+ }
+}
+
+/*!< \brief Returns true if CNAME is in this node. */
+static bool adding_to_cname(const knot_dname_t *owner,
+ const zone_node_t *node)
+{
+ if (node == NULL) {
+ // Node did not exist before update.
+ return false;
+ }
+
+ knot_rrset_t cname = node_rrset(node, KNOT_RRTYPE_CNAME);
+ if (knot_rrset_empty(&cname)) {
+ // Node did not contain CNAME before update.
+ return false;
+ }
+
+ // CNAME present
+ return true;
+}
+
+/*!< \brief Used to ignore SOA deletions and SOAs with lower serial than zone. */
+static bool skip_soa(const knot_rrset_t *rr, int64_t sn)
+{
+ if (rr->type == KNOT_RRTYPE_SOA &&
+ (rr->rclass == KNOT_CLASS_NONE || rr->rclass == KNOT_CLASS_ANY ||
+ (serial_compare(knot_soa_serial(rr->rrs.rdata), sn) != SERIAL_GREATER))) {
+ return true;
+ }
+
+ return false;
+}
+
+/*!< \brief Replaces possible singleton RR type in changeset. */
+static bool singleton_replaced(zone_update_t *update, const knot_rrset_t *rr)
+{
+ if (!should_replace(rr)) {
+ return false;
+ }
+
+ return zone_update_remove_rrset(update, rr->owner, rr->type) == KNOT_EOK;
+}
+
+/*!< \brief Adds RR into add section of changeset if it is deemed worthy. */
+static int add_rr_to_changeset(const knot_rrset_t *rr, zone_update_t *update)
+{
+ if (singleton_replaced(update, rr)) {
+ return KNOT_EOK;
+ }
+
+ return zone_update_add(update, rr);
+}
+
+/*!< \brief Processes CNAME addition (replace or ignore) */
+static int process_add_cname(const zone_node_t *node,
+ const knot_rrset_t *rr,
+ zone_update_t *update)
+{
+ knot_rrset_t cname = node_rrset(node, KNOT_RRTYPE_CNAME);
+ if (!knot_rrset_empty(&cname)) {
+ // If they are identical, ignore.
+ if (knot_rrset_equal(&cname, rr, true)) {
+ return KNOT_EOK;
+ }
+
+ int ret = zone_update_remove(update, &cname);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ return add_rr_to_changeset(rr, update);
+ } else if (!node_empty(node)) {
+ // Other occupied node => ignore.
+ return KNOT_EOK;
+ } else {
+ // Can add.
+ return add_rr_to_changeset(rr, update);
+ }
+}
+
+/*!< \brief Processes NSEC3PARAM addition (ignore when not removed, or non-apex) */
+static int process_add_nsec3param(const zone_node_t *node,
+ const knot_rrset_t *rr,
+ zone_update_t *update)
+{
+ if (node == NULL || !node_rrtype_exists(node, KNOT_RRTYPE_SOA)) {
+ // Ignore non-apex additions
+ char *owner = knot_dname_to_str_alloc(rr->owner);
+ log_warning("DDNS, refusing to add NSEC3PARAM to non-apex "
+ "node '%s'", owner);
+ free(owner);
+ return KNOT_EDENIED;
+ }
+ knot_rrset_t param = node_rrset(node, KNOT_RRTYPE_NSEC3PARAM);
+ if (knot_rrset_empty(&param)) {
+ return add_rr_to_changeset(rr, update);
+ }
+
+ char *owner = knot_dname_to_str_alloc(rr->owner);
+ log_warning("DDNS, refusing to add second NSEC3PARAM to node '%s'", owner);
+ free(owner);
+
+ return KNOT_EOK;
+}
+
+/*!
+ * \brief Processes SOA addition (ignore when non-apex), lower serials
+ * dropped before.
+ */
+static int process_add_soa(const zone_node_t *node,
+ const knot_rrset_t *rr,
+ zone_update_t *update)
+{
+ if (node == NULL || !node_rrtype_exists(node, KNOT_RRTYPE_SOA)) {
+ // Adding SOA to non-apex node, ignore.
+ return KNOT_EOK;
+ }
+
+ // Get current SOA RR.
+ knot_rrset_t removed = node_rrset(node, KNOT_RRTYPE_SOA);
+ if (knot_rrset_equal(&removed, rr, true)) {
+ // If they are identical, ignore.
+ return KNOT_EOK;
+ }
+
+ return add_rr_to_changeset(rr, update);
+}
+
+/*!< \brief Adds normal RR, ignores when CNAME exists in node. */
+static int process_add_normal(const zone_node_t *node,
+ const knot_rrset_t *rr,
+ zone_update_t *update)
+{
+ if (adding_to_cname(rr->owner, node)) {
+ // Adding RR to CNAME node, ignore.
+ return KNOT_EOK;
+ }
+
+ if (node && node_contains_rr(node, rr)) {
+ // Adding existing RR, ignore.
+ return KNOT_EOK;
+ }
+
+ return add_rr_to_changeset(rr, update);
+}
+
+/*!< \brief Decides what to do with RR addition. */
+static int process_add(const knot_rrset_t *rr,
+ const zone_node_t *node,
+ zone_update_t *update)
+{
+ switch(rr->type) {
+ case KNOT_RRTYPE_CNAME:
+ return process_add_cname(node, rr, update);
+ case KNOT_RRTYPE_SOA:
+ return process_add_soa(node, rr, update);
+ case KNOT_RRTYPE_NSEC3PARAM:
+ return process_add_nsec3param(node, rr, update);
+ default:
+ return process_add_normal(node, rr, update);
+ }
+}
+
+/*!< \brief Removes single RR from zone. */
+static int process_rem_rr(const knot_rrset_t *rr,
+ const zone_node_t *node,
+ zone_update_t *update)
+{
+ if (node == NULL) {
+ // Removing from node that does not exist
+ return KNOT_EOK;
+ }
+
+ const bool apex_ns = node_rrtype_exists(node, KNOT_RRTYPE_SOA) &&
+ rr->type == KNOT_RRTYPE_NS;
+ if (apex_ns) {
+ const knot_rdataset_t *ns_rrs =
+ node_rdataset(node, KNOT_RRTYPE_NS);
+ if (ns_rrs == NULL) {
+ // Zone without apex NS.
+ return KNOT_EOK;
+ }
+ if (ns_rrs->count == 1) {
+ // Cannot remove last apex NS RR.
+ return KNOT_EOK;
+ }
+ }
+
+ knot_rrset_t to_modify = node_rrset(node, rr->type);
+ if (knot_rrset_empty(&to_modify)) {
+ // No such RRSet
+ return KNOT_EOK;
+ }
+
+ knot_rdataset_t *rrs = node_rdataset(node, rr->type);
+ if (!knot_rdataset_member(rrs, rr->rrs.rdata)) {
+ // Node does not contain this RR
+ return KNOT_EOK;
+ }
+
+ knot_rrset_t rr_ttl = *rr;
+ rr_ttl.ttl = to_modify.ttl;
+
+ return zone_update_remove(update, &rr_ttl);
+}
+
+/*!< \brief Removes RRSet from zone. */
+static int process_rem_rrset(const knot_rrset_t *rrset,
+ const zone_node_t *node,
+ zone_update_t *update)
+{
+ bool is_apex = node_rrtype_exists(node, KNOT_RRTYPE_SOA);
+
+ if (is_apex && rrset->type == KNOT_RRTYPE_NS) {
+ // Ignore NS apex RRSet removals.
+ return KNOT_EOK;
+ }
+
+ if (node == NULL) {
+ // no such node in zone, ignore
+ return KNOT_EOK;
+ }
+
+ if (!node_rrtype_exists(node, rrset->type)) {
+ // no such RR, ignore
+ return KNOT_EOK;
+ }
+
+ knot_rrset_t to_remove = node_rrset(node, rrset->type);
+ return zone_update_remove(update, &to_remove);
+}
+
+/*!< \brief Removes node from zone. */
+static int process_rem_node(const knot_rrset_t *rr,
+ const zone_node_t *node, zone_update_t *update)
+{
+ if (node == NULL) {
+ return KNOT_EOK;
+ }
+
+ // Remove all RRSets from node
+ size_t rrset_count = node->rrset_count;
+ for (int i = 0; i < rrset_count; ++i) {
+ knot_rrset_t rrset = node_rrset_at(node, rrset_count - i - 1);
+ int ret = process_rem_rrset(&rrset, node, update);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+/*!< \brief Decides what to with removal. */
+static int process_remove(const knot_rrset_t *rr,
+ const zone_node_t *node,
+ zone_update_t *update)
+{
+ if (is_rr_removal(rr)) {
+ return process_rem_rr(rr, node, update);
+ } else if (is_rrset_removal(rr)) {
+ return process_rem_rrset(rr, node, update);
+ } else if (is_node_removal(rr)) {
+ return process_rem_node(rr, node, update);
+ } else {
+ return KNOT_EINVAL;
+ }
+}
+
+/*!< \brief Checks whether addition has not violated DNAME rules. */
+static bool sem_check(const knot_rrset_t *rr, const zone_node_t *zone_node,
+ zone_update_t *update)
+{
+ const zone_node_t *added_node = zone_contents_find_node(update->new_cont, rr->owner);
+
+ // we do this sem check AFTER adding the RR, so the node must exist
+ assert(added_node != NULL);
+
+ for (const zone_node_t *parent = added_node->parent;
+ parent != NULL; parent = parent->parent) {
+ if (node_rrtype_exists(parent, KNOT_RRTYPE_DNAME)) {
+ // Parent has DNAME RRSet, refuse update
+ return false;
+ }
+ }
+
+ if (rr->type != KNOT_RRTYPE_DNAME || zone_node == NULL) {
+ return true;
+ }
+
+ // Check that we have not created node with DNAME children.
+ if (zone_node->children > 0) {
+ // Updated node has children and DNAME was added, refuse update
+ return false;
+ }
+
+ return true;
+}
+
+/*!< \brief Checks whether we can accept this RR. */
+static int check_update(const knot_rrset_t *rrset, const knot_pkt_t *query,
+ uint16_t *rcode)
+{
+ /* Accept both subdomain and dname match. */
+ const knot_dname_t *owner = rrset->owner;
+ const knot_dname_t *qname = knot_pkt_qname(query);
+ const int in_bailiwick = knot_dname_in_bailiwick(owner, qname);
+ if (in_bailiwick < 0) {
+ *rcode = KNOT_RCODE_NOTZONE;
+ return KNOT_EOUTOFZONE;
+ }
+
+ if (rrset->rclass == knot_pkt_qclass(query)) {
+ if (knot_rrtype_is_metatype(rrset->type)) {
+ *rcode = KNOT_RCODE_FORMERR;
+ return KNOT_EMALF;
+ }
+ } else if (rrset->rclass == KNOT_CLASS_ANY) {
+ if (!rrset_empty(rrset) ||
+ (knot_rrtype_is_metatype(rrset->type) &&
+ rrset->type != KNOT_RRTYPE_ANY)) {
+ *rcode = KNOT_RCODE_FORMERR;
+ return KNOT_EMALF;
+ }
+ } else if (rrset->rclass == KNOT_CLASS_NONE) {
+ if (rrset->ttl != 0 || knot_rrtype_is_metatype(rrset->type)) {
+ *rcode = KNOT_RCODE_FORMERR;
+ return KNOT_EMALF;
+ }
+ } else {
+ *rcode = KNOT_RCODE_FORMERR;
+ return KNOT_EMALF;
+ }
+
+ return KNOT_EOK;
+}
+
+/*!< \brief Checks RR and decides what to do with it. */
+static int process_rr(const knot_rrset_t *rr, zone_update_t *update)
+{
+ const zone_node_t *node = zone_update_get_node(update, rr->owner);
+
+ if (is_addition(rr)) {
+ int ret = process_add(rr, node, update);
+ if (ret == KNOT_EOK) {
+ if (!sem_check(rr, node, update)) {
+ return KNOT_EDENIED;
+ }
+ }
+ return ret;
+ } else if (is_removal(rr)) {
+ return process_remove(rr, node, update);
+ } else {
+ return KNOT_EMALF;
+ }
+}
+
+/*!< \brief Maps Knot return code to RCODE. */
+static uint16_t ret_to_rcode(int ret)
+{
+ if (ret == KNOT_EMALF) {
+ return KNOT_RCODE_FORMERR;
+ } else if (ret == KNOT_EDENIED) {
+ return KNOT_RCODE_REFUSED;
+ } else {
+ return KNOT_RCODE_SERVFAIL;
+ }
+}
+
+int ddns_process_prereqs(const knot_pkt_t *query, zone_update_t *update,
+ uint16_t *rcode)
+{
+ if (query == NULL || rcode == NULL || update == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = KNOT_EOK;
+ list_t rrset_list; // List used to store merged RRSets
+ init_list(&rrset_list);
+
+ const knot_pktsection_t *answer = knot_pkt_section(query, KNOT_ANSWER);
+ const knot_rrset_t *answer_rr = (answer->count > 0) ? knot_pkt_rr(answer, 0) : NULL;
+ for (int i = 0; i < answer->count; ++i) {
+ // Check what can be checked, store full RRs into list
+ ret = process_prereq(&answer_rr[i], knot_pkt_qclass(query),
+ update, rcode, &rrset_list);
+ if (ret != KNOT_EOK) {
+ rrset_list_clear(&rrset_list);
+ return ret;
+ }
+ }
+
+ // Check stored RRSets
+ ret = check_stored_rrsets(&rrset_list, update, rcode);
+ rrset_list_clear(&rrset_list);
+ return ret;
+}
+
+int ddns_process_update(const zone_t *zone, const knot_pkt_t *query,
+ zone_update_t *update, uint16_t *rcode)
+{
+ if (zone == NULL || query == NULL || update == NULL || rcode == NULL) {
+ if (rcode) {
+ *rcode = ret_to_rcode(KNOT_EINVAL);
+ }
+ return KNOT_EINVAL;
+ }
+
+ uint32_t sn_old = knot_soa_serial(zone_update_from(update)->rdata);
+
+ // Process all RRs in the authority section.
+ const knot_pktsection_t *authority = knot_pkt_section(query, KNOT_AUTHORITY);
+ const knot_rrset_t *authority_rr = (authority->count > 0) ? knot_pkt_rr(authority, 0) : NULL;
+ for (uint16_t i = 0; i < authority->count; ++i) {
+ const knot_rrset_t *rr = &authority_rr[i];
+ // Check if RR is correct.
+ int ret = check_update(rr, query, rcode);
+ if (ret != KNOT_EOK) {
+ assert(*rcode != KNOT_RCODE_NOERROR);
+ return ret;
+ }
+
+ if (skip_soa(rr, sn_old)) {
+ continue;
+ }
+
+ ret = process_rr(rr, update);
+ if (ret != KNOT_EOK) {
+ *rcode = ret_to_rcode(ret);
+ return ret;
+ }
+ }
+
+ *rcode = KNOT_RCODE_NOERROR;
+ return KNOT_EOK;
+}
diff --git a/src/knot/updates/ddns.h b/src/knot/updates/ddns.h
new file mode 100644
index 0000000..1d79218
--- /dev/null
+++ b/src/knot/updates/ddns.h
@@ -0,0 +1,47 @@
+/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/updates/zone-update.h"
+#include "knot/zone/zone.h"
+#include "libknot/packet/pkt.h"
+
+/*!
+ * \brief Checks update prerequisite section.
+ *
+ * \param query DNS message containing the update.
+ * \param update Zone to be checked.
+ * \param rcode Returned DNS RCODE.
+ *
+ * \return KNOT_E*
+ */
+int ddns_process_prereqs(const knot_pkt_t *query, zone_update_t *update,
+ uint16_t *rcode);
+
+/*!
+ * \brief Processes DNS update and creates a changeset out of it. Zone is left
+ * intact.
+ *
+ * \param zone Zone to be updated.
+ * \param query DNS message containing the update.
+ * \param update Output changeset.
+ * \param rcode Output DNS RCODE.
+ *
+ * \return KNOT_E*
+ */
+int ddns_process_update(const zone_t *zone, const knot_pkt_t *query,
+ zone_update_t *update, uint16_t *rcode);
diff --git a/src/knot/updates/zone-update.c b/src/knot/updates/zone-update.c
new file mode 100644
index 0000000..81f3465
--- /dev/null
+++ b/src/knot/updates/zone-update.c
@@ -0,0 +1,1098 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <signal.h>
+#include <unistd.h>
+#include <urcu.h>
+
+#include "knot/catalog/interpret.h"
+#include "knot/common/log.h"
+#include "knot/common/systemd.h"
+#include "knot/dnssec/zone-events.h"
+#include "knot/server/server.h"
+#include "knot/updates/zone-update.h"
+#include "knot/zone/adds_tree.h"
+#include "knot/zone/adjust.h"
+#include "knot/zone/digest.h"
+#include "knot/zone/serial.h"
+#include "knot/zone/zone-diff.h"
+#include "knot/zone/zonefile.h"
+#include "contrib/trim.h"
+#include "contrib/ucw/lists.h"
+
+// Call mem_trim() whenever accumulated size of updated zones reaches this size.
+#define UPDATE_MEMTRIM_AT (10 * 1024 * 1024)
+
+static int init_incremental(zone_update_t *update, zone_t *zone, zone_contents_t *old_contents)
+{
+ if (old_contents == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = changeset_init(&update->change, zone->name);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (update->flags & UPDATE_HYBRID) {
+ update->new_cont = old_contents;
+ } else {
+ ret = zone_contents_cow(old_contents, &update->new_cont);
+ if (ret != KNOT_EOK) {
+ changeset_clear(&update->change);
+ return ret;
+ }
+ }
+
+ uint32_t apply_flags = (update->flags & UPDATE_STRICT) ? APPLY_STRICT : 0;
+ apply_flags |= (update->flags & UPDATE_HYBRID) ? APPLY_UNIFY_FULL : 0;
+ ret = apply_init_ctx(update->a_ctx, update->new_cont, apply_flags);
+ if (ret != KNOT_EOK) {
+ changeset_clear(&update->change);
+ return ret;
+ }
+
+ /* Copy base SOA RR. */
+ update->change.soa_from =
+ node_create_rrset(old_contents->apex, KNOT_RRTYPE_SOA);
+ if (update->change.soa_from == NULL) {
+ zone_contents_free(update->new_cont);
+ changeset_clear(&update->change);
+ return KNOT_ENOMEM;
+ }
+
+ return KNOT_EOK;
+}
+
+static int init_full(zone_update_t *update, zone_t *zone)
+{
+ update->new_cont = zone_contents_new(zone->name, true);
+ if (update->new_cont == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ int ret = apply_init_ctx(update->a_ctx, update->new_cont, APPLY_UNIFY_FULL);
+ if (ret != KNOT_EOK) {
+ zone_contents_free(update->new_cont);
+ return ret;
+ }
+
+ return KNOT_EOK;
+}
+
+static int replace_soa(zone_contents_t *contents, const knot_rrset_t *rr)
+{
+ /* SOA possible only within apex. */
+ if (!knot_dname_is_equal(rr->owner, contents->apex->owner)) {
+ return KNOT_EDENIED;
+ }
+
+ knot_rrset_t old_soa = node_rrset(contents->apex, KNOT_RRTYPE_SOA);
+ zone_node_t *n = contents->apex;
+ int ret = zone_contents_remove_rr(contents, &old_soa, &n);
+ if (ret != KNOT_EOK && ret != KNOT_EINVAL) {
+ return ret;
+ }
+
+ ret = zone_contents_add_rr(contents, rr, &n);
+ if (ret == KNOT_ETTL) {
+ return KNOT_EOK;
+ }
+
+ return ret;
+}
+
+static int init_base(zone_update_t *update, zone_t *zone, zone_contents_t *old_contents,
+ zone_update_flags_t flags)
+{
+ if (update == NULL || zone == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ memset(update, 0, sizeof(*update));
+ update->zone = zone;
+ update->flags = flags;
+
+ update->a_ctx = calloc(1, sizeof(*update->a_ctx));
+ if (update->a_ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ if (zone->control_update != NULL && zone->control_update != update) {
+ log_zone_warning(zone->name, "blocked zone update due to open control transaction");
+ }
+
+ knot_sem_wait(&zone->cow_lock);
+ update->a_ctx->cow_mutex = &zone->cow_lock;
+
+ if (old_contents == NULL) {
+ old_contents = zone->contents; // don't obtain this pointer before any other zone_update ceased to exist!
+ }
+
+ int ret = KNOT_EINVAL;
+ if (flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
+ ret = init_incremental(update, zone, old_contents);
+ } else if (flags & UPDATE_FULL) {
+ ret = init_full(update, zone);
+ }
+ if (ret != KNOT_EOK) {
+ knot_sem_post(&zone->cow_lock);
+ free(update->a_ctx);
+ }
+
+ return ret;
+}
+
+/* ------------------------------- API -------------------------------------- */
+
+int zone_update_init(zone_update_t *update, zone_t *zone, zone_update_flags_t flags)
+{
+ return init_base(update, zone, NULL, flags);
+}
+
+int zone_update_from_differences(zone_update_t *update, zone_t *zone, zone_contents_t *old_cont,
+ zone_contents_t *new_cont, zone_update_flags_t flags,
+ bool ignore_dnssec, bool ignore_zonemd)
+{
+ if (update == NULL || zone == NULL || new_cont == NULL ||
+ !(flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) || (flags & UPDATE_FULL)) {
+ return KNOT_EINVAL;
+ }
+
+ changeset_t diff;
+ int ret = changeset_init(&diff, zone->name);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = init_base(update, zone, old_cont, flags);
+ if (ret != KNOT_EOK) {
+ changeset_clear(&diff);
+ return ret;
+ }
+
+ if (old_cont == NULL) {
+ old_cont = zone->contents;
+ }
+
+ ret = zone_contents_diff(old_cont, new_cont, &diff, ignore_dnssec, ignore_zonemd);
+ switch (ret) {
+ case KNOT_ENODIFF:
+ case KNOT_ESEMCHECK:
+ case KNOT_EOK:
+ break;
+ case KNOT_ERANGE:
+ additionals_tree_free(update->new_cont->adds_tree);
+ update->new_cont->adds_tree = NULL;
+ update->new_cont = NULL; // Prevent deep_free as old_cont will be used later.
+ update->a_ctx->flags &= ~APPLY_UNIFY_FULL; // Prevent Unify of old_cont that will be used later.
+ // FALLTHROUGH
+ default:
+ changeset_clear(&diff);
+ zone_update_clear(update);
+ return ret;
+ }
+
+ ret = zone_update_apply_changeset(update, &diff);
+ changeset_clear(&diff);
+ if (ret != KNOT_EOK) {
+ zone_update_clear(update);
+ return ret;
+ }
+
+ update->init_cont = new_cont;
+ return KNOT_EOK;
+}
+
+int zone_update_from_contents(zone_update_t *update, zone_t *zone_without_contents,
+ zone_contents_t *new_cont, zone_update_flags_t flags)
+{
+ if (update == NULL || zone_without_contents == NULL || new_cont == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ memset(update, 0, sizeof(*update));
+ update->zone = zone_without_contents;
+ update->flags = flags;
+ update->new_cont = new_cont;
+
+ update->a_ctx = calloc(1, sizeof(*update->a_ctx));
+ if (update->a_ctx == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ if (zone_without_contents->control_update != NULL) {
+ log_zone_warning(zone_without_contents->name,
+ "blocked zone update due to open control transaction");
+ }
+
+ knot_sem_wait(&update->zone->cow_lock);
+ update->a_ctx->cow_mutex = &update->zone->cow_lock;
+
+ if (flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
+ int ret = changeset_init(&update->change, zone_without_contents->name);
+ if (ret != KNOT_EOK) {
+ free(update->a_ctx);
+ update->a_ctx = NULL;
+ knot_sem_post(&update->zone->cow_lock);
+ return ret;
+ }
+
+ update->change.soa_from = node_create_rrset(new_cont->apex, KNOT_RRTYPE_SOA);
+ if (update->change.soa_from == NULL) {
+ changeset_clear(&update->change);
+ free(update->a_ctx);
+ update->a_ctx = NULL;
+ knot_sem_post(&update->zone->cow_lock);
+ return KNOT_ENOMEM;
+ }
+ }
+
+ uint32_t apply_flags = (update->flags & UPDATE_STRICT) ? APPLY_STRICT : 0;
+ int ret = apply_init_ctx(update->a_ctx, update->new_cont, apply_flags | APPLY_UNIFY_FULL);
+ if (ret != KNOT_EOK) {
+ changeset_clear(&update->change);
+ free(update->a_ctx);
+ update->a_ctx = NULL;
+ knot_sem_post(&update->zone->cow_lock);
+ return ret;
+ }
+
+ return KNOT_EOK;
+}
+
+int zone_update_start_extra(zone_update_t *update, conf_t *conf)
+{
+ assert((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)));
+
+ int ret = changeset_init(&update->extra_ch, update->new_cont->apex->owner);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (update->init_cont != NULL) {
+ ret = zone_update_increment_soa(update, conf);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = zone_contents_diff(update->init_cont, update->new_cont,
+ &update->extra_ch, false, false);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ } else {
+ update->extra_ch.soa_from = node_create_rrset(update->new_cont->apex, KNOT_RRTYPE_SOA);
+ if (update->extra_ch.soa_from == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ ret = zone_update_increment_soa(update, conf);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ update->extra_ch.soa_to = node_create_rrset(update->new_cont->apex, KNOT_RRTYPE_SOA);
+ if (update->extra_ch.soa_to == NULL) {
+ return KNOT_ENOMEM;
+ }
+ }
+
+ update->flags |= UPDATE_EXTRA_CHSET;
+ return KNOT_EOK;
+}
+
+const zone_node_t *zone_update_get_node(zone_update_t *update, const knot_dname_t *dname)
+{
+ if (update == NULL || dname == NULL) {
+ return NULL;
+ }
+
+ return zone_contents_node_or_nsec3(update->new_cont, dname);
+}
+
+uint32_t zone_update_current_serial(zone_update_t *update)
+{
+ const zone_node_t *apex = update->new_cont->apex;
+ if (apex != NULL) {
+ return knot_soa_serial(node_rdataset(apex, KNOT_RRTYPE_SOA)->rdata);
+ } else {
+ return 0;
+ }
+}
+
+bool zone_update_changed_nsec3param(const zone_update_t *update)
+{
+ if (update->zone->contents == NULL) {
+ return true;
+ }
+
+ dnssec_nsec3_params_t *orig = &update->zone->contents->nsec3_params;
+ dnssec_nsec3_params_t *upd = &update->new_cont->nsec3_params;
+ return !dnssec_nsec3_params_match(orig, upd);
+}
+
+const knot_rdataset_t *zone_update_from(zone_update_t *update)
+{
+ if (update == NULL) {
+ return NULL;
+ }
+
+ if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
+ const zone_node_t *apex = update->zone->contents->apex;
+ return node_rdataset(apex, KNOT_RRTYPE_SOA);
+ }
+
+ return NULL;
+}
+
+const knot_rdataset_t *zone_update_to(zone_update_t *update)
+{
+ if (update == NULL) {
+ return NULL;
+ }
+
+ if (update->flags & UPDATE_NO_CHSET) {
+ zone_diff_t diff = { .apex = update->new_cont->apex };
+ return zone_diff_to(&diff) == zone_diff_from(&diff) ?
+ NULL : node_rdataset(update->new_cont->apex, KNOT_RRTYPE_SOA);
+ } else if (update->flags & UPDATE_FULL) {
+ const zone_node_t *apex = update->new_cont->apex;
+ return node_rdataset(apex, KNOT_RRTYPE_SOA);
+ } else {
+ if (update->change.soa_to == NULL) {
+ return NULL;
+ }
+ return &update->change.soa_to->rrs;
+ }
+
+ return NULL;
+}
+
+void zone_update_clear(zone_update_t *update)
+{
+ if (update == NULL || update->zone == NULL) {
+ return;
+ }
+
+ if (update->new_cont != NULL) {
+ additionals_tree_free(update->new_cont->adds_tree);
+ update->new_cont->adds_tree = NULL;
+ }
+
+ if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
+ changeset_clear(&update->change);
+ changeset_clear(&update->extra_ch);
+ }
+
+ zone_contents_deep_free(update->init_cont);
+
+ if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) {
+ apply_cleanup(update->a_ctx);
+ zone_contents_deep_free(update->new_cont);
+ } else {
+ apply_rollback(update->a_ctx);
+ }
+
+ free(update->a_ctx);
+ memset(update, 0, sizeof(*update));
+}
+
+inline static void update_affected_rrtype(zone_update_t *update, uint16_t rrtype)
+{
+ switch (rrtype) {
+ case KNOT_RRTYPE_NSEC:
+ case KNOT_RRTYPE_NSEC3:
+ update->flags |= UPDATE_CHANGED_NSEC;
+ break;
+ }
+}
+
+static int solve_add_different_ttl(zone_update_t *update, const knot_rrset_t *add)
+{
+ if (add->type == KNOT_RRTYPE_RRSIG || add->type == KNOT_RRTYPE_SOA) {
+ return KNOT_EOK;
+ }
+
+ const zone_node_t *exist_node = zone_contents_find_node(update->new_cont, add->owner);
+ const knot_rrset_t exist_rr = node_rrset(exist_node, add->type);
+ if (knot_rrset_empty(&exist_rr) || exist_rr.ttl == add->ttl) {
+ return KNOT_EOK;
+ }
+
+ knot_dname_txt_storage_t buff;
+ char *owner = knot_dname_to_str(buff, add->owner, sizeof(buff));
+ if (owner == NULL) {
+ owner = "";
+ }
+ char type[16] = "";
+ knot_rrtype_to_string(add->type, type, sizeof(type));
+ log_zone_notice(update->zone->name, "TTL mismatch, owner %s, type %s, "
+ "TTL set to %u", owner, type, add->ttl);
+
+ knot_rrset_t *exist_copy = knot_rrset_copy(&exist_rr, NULL);
+ if (exist_copy == NULL) {
+ return KNOT_ENOMEM;
+ }
+ int ret = zone_update_remove(update, exist_copy);
+ if (ret == KNOT_EOK) {
+ exist_copy->ttl = add->ttl;
+ ret = zone_update_add(update, exist_copy);
+ }
+ knot_rrset_free(exist_copy, NULL);
+ return ret;
+}
+
+int zone_update_add(zone_update_t *update, const knot_rrset_t *rrset)
+{
+ if (update == NULL || rrset == NULL) {
+ return KNOT_EINVAL;
+ }
+ if (knot_rrset_empty(rrset)) {
+ return KNOT_EOK;
+ }
+
+ if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
+ int ret = solve_add_different_ttl(update, rrset);
+ if (ret == KNOT_EOK && !(update->flags & UPDATE_NO_CHSET)) {
+ ret = changeset_add_addition(&update->change, rrset, CHANGESET_CHECK);
+ }
+ if (ret == KNOT_EOK && (update->flags & UPDATE_EXTRA_CHSET)) {
+ assert(!(update->flags & UPDATE_NO_CHSET));
+ ret = changeset_add_addition(&update->extra_ch, rrset, CHANGESET_CHECK);
+ }
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ if (update->flags & UPDATE_INCREMENTAL) {
+ if (rrset->type == KNOT_RRTYPE_SOA) {
+ // replace previous SOA
+ int ret = apply_replace_soa(update->a_ctx, rrset);
+ if (ret != KNOT_EOK && !(update->flags & UPDATE_NO_CHSET)) {
+ changeset_remove_addition(&update->change, rrset);
+ }
+ return ret;
+ }
+
+ int ret = apply_add_rr(update->a_ctx, rrset);
+ if (ret != KNOT_EOK) {
+ if (!(update->flags & UPDATE_NO_CHSET)) {
+ changeset_remove_addition(&update->change, rrset);
+ }
+ return ret;
+ }
+
+ update_affected_rrtype(update, rrset->type);
+ return KNOT_EOK;
+ } else if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) {
+ if (rrset->type == KNOT_RRTYPE_SOA) {
+ /* replace previous SOA */
+ return replace_soa(update->new_cont, rrset);
+ }
+
+ zone_node_t *n = NULL;
+ int ret = zone_contents_add_rr(update->new_cont, rrset, &n);
+ if (ret == KNOT_ETTL) {
+ knot_dname_txt_storage_t buff;
+ char *owner = knot_dname_to_str(buff, rrset->owner, sizeof(buff));
+ if (owner == NULL) {
+ owner = "";
+ }
+ char type[16] = "";
+ knot_rrtype_to_string(rrset->type, type, sizeof(type));
+ log_zone_notice(update->new_cont->apex->owner,
+ "TTL mismatch, owner %s, type %s, "
+ "TTL set to %u", owner, type, rrset->ttl);
+ return KNOT_EOK;
+ }
+
+ return ret;
+ } else {
+ return KNOT_EINVAL;
+ }
+}
+
+int zone_update_remove(zone_update_t *update, const knot_rrset_t *rrset)
+{
+ if (update == NULL || rrset == NULL) {
+ return KNOT_EINVAL;
+ }
+ if (knot_rrset_empty(rrset)) {
+ return KNOT_EOK;
+ }
+
+ if ((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) &&
+ rrset->type != KNOT_RRTYPE_SOA && !(update->flags & UPDATE_NO_CHSET)) {
+ int ret = changeset_add_removal(&update->change, rrset, CHANGESET_CHECK);
+ if (ret == KNOT_EOK && (update->flags & UPDATE_EXTRA_CHSET)) {
+ assert(!(update->flags & UPDATE_NO_CHSET));
+ ret = changeset_add_removal(&update->extra_ch, rrset, CHANGESET_CHECK);
+ }
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ if (update->flags & UPDATE_INCREMENTAL) {
+ if (rrset->type == KNOT_RRTYPE_SOA) {
+ /* SOA is replaced with addition */
+ return KNOT_EOK;
+ }
+
+ int ret = apply_remove_rr(update->a_ctx, rrset);
+ if (ret != KNOT_EOK) {
+ if (!(update->flags & UPDATE_NO_CHSET)) {
+ changeset_remove_removal(&update->change, rrset);
+ }
+ return ret;
+ }
+
+ update_affected_rrtype(update, rrset->type);
+ return KNOT_EOK;
+ } else if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) {
+ zone_node_t *n = NULL;
+ return zone_contents_remove_rr(update->new_cont, rrset, &n);
+ } else {
+ return KNOT_EINVAL;
+ }
+}
+
+int zone_update_remove_rrset(zone_update_t *update, knot_dname_t *owner, uint16_t type)
+{
+ if (update == NULL || owner == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ const zone_node_t *node = zone_contents_node_or_nsec3(update->new_cont, owner);
+ if (node == NULL) {
+ return KNOT_ENONODE;
+ }
+
+ knot_rrset_t rrset = node_rrset(node, type);
+ if (rrset.owner == NULL) {
+ return KNOT_ENOENT;
+ }
+
+ return zone_update_remove(update, &rrset);
+}
+
+int zone_update_remove_node(zone_update_t *update, const knot_dname_t *owner)
+{
+ if (update == NULL || owner == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ const zone_node_t *node = zone_contents_node_or_nsec3(update->new_cont, owner);
+ if (node == NULL) {
+ return KNOT_ENONODE;
+ }
+
+ size_t rrset_count = node->rrset_count;
+ for (int i = 0; i < rrset_count; ++i) {
+ knot_rrset_t rrset = node_rrset_at(node, rrset_count - 1 - i);
+ int ret = zone_update_remove(update, &rrset);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int update_chset_step(const knot_rrset_t *rrset, bool addition, void *ctx)
+{
+ zone_update_t *update = ctx;
+ if (addition) {
+ return zone_update_add(update, rrset);
+ } else {
+ return zone_update_remove(update, rrset);
+ }
+}
+
+int zone_update_apply_changeset(zone_update_t *update, const changeset_t *changes)
+{
+ return changeset_walk(changes, update_chset_step, update);
+}
+
+int zone_update_apply_changeset_reverse(zone_update_t *update, const changeset_t *changes)
+{
+ changeset_t reverse;
+ reverse.remove = changes->add;
+ reverse.add = changes->remove;
+ reverse.soa_from = changes->soa_to;
+ reverse.soa_to = changes->soa_from;
+ return zone_update_apply_changeset(update, &reverse);
+}
+
+static int set_new_soa(zone_update_t *update, unsigned serial_policy)
+{
+ assert(update);
+
+ knot_rrset_t *soa_cpy = node_create_rrset(update->new_cont->apex,
+ KNOT_RRTYPE_SOA);
+ if (soa_cpy == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ int ret = zone_update_remove(update, soa_cpy);
+ if (ret != KNOT_EOK) {
+ knot_rrset_free(soa_cpy, NULL);
+ return ret;
+ }
+
+ uint32_t old_serial = knot_soa_serial(soa_cpy->rrs.rdata);
+ uint32_t new_serial = serial_next(old_serial, serial_policy, 1);
+ if (serial_compare(old_serial, new_serial) != SERIAL_LOWER) {
+ log_zone_warning(update->zone->name, "updated SOA serial is lower "
+ "than current, serial %u -> %u",
+ old_serial, new_serial);
+ ret = KNOT_ESOAINVAL;
+ } else {
+ knot_soa_serial_set(soa_cpy->rrs.rdata, new_serial);
+
+ ret = zone_update_add(update, soa_cpy);
+ }
+ knot_rrset_free(soa_cpy, NULL);
+
+ return ret;
+}
+
+int zone_update_increment_soa(zone_update_t *update, conf_t *conf)
+{
+ if (update == NULL || conf == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ conf_val_t val = conf_zone_get(conf, C_SERIAL_POLICY, update->zone->name);
+ return set_new_soa(update, conf_opt(&val));
+}
+
+static void get_zone_diff(zone_diff_t *zdiff, zone_update_t *up)
+{
+ zdiff->nodes = *up->a_ctx->node_ptrs;
+ zdiff->nsec3s = *up->a_ctx->nsec3_ptrs;
+ zdiff->apex = up->new_cont->apex;
+}
+
+static int commit_journal(conf_t *conf, zone_update_t *update)
+{
+ conf_val_t val = conf_zone_get(conf, C_JOURNAL_CONTENT, update->zone->name);
+ unsigned content = conf_opt(&val);
+ int ret = KNOT_EOK;
+ if (update->flags & UPDATE_NO_CHSET) {
+ zone_diff_t diff;
+ get_zone_diff(&diff, update);
+ if (content != JOURNAL_CONTENT_NONE && !zone_update_no_change(update)) {
+ ret = zone_diff_store(conf, update->zone, &diff);
+ }
+ } else if ((update->flags & UPDATE_INCREMENTAL) ||
+ (update->flags & UPDATE_HYBRID)) {
+ changeset_t *extra = (update->flags & UPDATE_EXTRA_CHSET) ? &update->extra_ch : NULL;
+ if (content != JOURNAL_CONTENT_NONE && !zone_update_no_change(update)) {
+ ret = zone_change_store(conf, update->zone, &update->change, extra);
+ }
+ } else {
+ if (content == JOURNAL_CONTENT_ALL) {
+ return zone_in_journal_store(conf, update->zone, update->new_cont);
+ } else if (content != JOURNAL_CONTENT_NONE) { // zone_in_journal_store does this automatically
+ return zone_changes_clear(conf, update->zone);
+ }
+ }
+ return ret;
+}
+
+static int commit_incremental(conf_t *conf, zone_update_t *update)
+{
+ assert(update);
+
+ if (zone_update_to(update) == NULL && !zone_update_no_change(update)) {
+ /* No SOA in the update, create one according to the current policy */
+ int ret = zone_update_increment_soa(update, conf);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+ }
+
+ return KNOT_EOK;
+}
+
+static int commit_full(conf_t *conf, zone_update_t *update)
+{
+ assert(update);
+
+ /* Check if we have SOA. We might consider adding full semantic check here.
+ * But if we wanted full sem-check I'd consider being it controlled by a flag
+ * - to enable/disable it on demand. */
+ if (!node_rrtype_exists(update->new_cont->apex, KNOT_RRTYPE_SOA)) {
+ return KNOT_ESEMCHECK;
+ }
+
+ return KNOT_EOK;
+}
+
+static int update_catalog(conf_t *conf, zone_update_t *update)
+{
+ conf_val_t val = conf_zone_get(conf, C_CATALOG_TPL, update->zone->name);
+ if (val.code != KNOT_EOK) {
+ return (val.code == KNOT_ENOENT || val.code == KNOT_YP_EINVAL_ID) ? KNOT_EOK : val.code;
+ }
+
+ int ret = catalog_zone_verify(update->new_cont);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ssize_t upd_count = 0;
+ if ((update->flags & UPDATE_NO_CHSET)) {
+ zone_diff_t diff;
+ get_zone_diff(&diff, update);
+ ret = catalog_update_from_zone(zone_catalog_upd(update->zone),
+ NULL, &diff, update->new_cont,
+ false, zone_catalog(update->zone), &upd_count);
+ } else if ((update->flags & UPDATE_INCREMENTAL)) {
+ ret = catalog_update_from_zone(zone_catalog_upd(update->zone),
+ update->change.remove, NULL, update->new_cont,
+ true, zone_catalog(update->zone), &upd_count);
+ if (ret == KNOT_EOK) {
+ ret = catalog_update_from_zone(zone_catalog_upd(update->zone),
+ update->change.add, NULL, update->new_cont,
+ false, NULL, &upd_count);
+ }
+ } else {
+ ret = catalog_update_del_all(zone_catalog_upd(update->zone),
+ zone_catalog(update->zone),
+ update->zone->name, &upd_count);
+ if (ret == KNOT_EOK) {
+ ret = catalog_update_from_zone(zone_catalog_upd(update->zone),
+ update->new_cont, NULL, update->new_cont,
+ false, NULL, &upd_count);
+ }
+ }
+
+ if (ret == KNOT_EOK) {
+ log_zone_info(update->zone->name, "catalog reloaded, %zd updates", upd_count);
+ update->zone->server->catalog_upd_signal = true;
+ if (kill(getpid(), SIGUSR1) != 0) {
+ ret = knot_map_errno();
+ }
+ } else {
+ // this cant normally happen, just some ENOMEM or so
+ (void)catalog_update_del_all(zone_catalog_upd(update->zone),
+ zone_catalog(update->zone),
+ update->zone->name, &upd_count);
+ }
+
+ return ret;
+}
+
+typedef struct {
+ pthread_mutex_t lock;
+ size_t counter;
+} counter_reach_t;
+
+static bool counter_reach(counter_reach_t *counter, size_t increment, size_t limit)
+{
+ bool reach = false;
+ pthread_mutex_lock(&counter->lock);
+ counter->counter += increment;
+ if (counter->counter >= limit) {
+ counter->counter = 0;
+ reach = true;
+ }
+ pthread_mutex_unlock(&counter->lock);
+ return reach;
+}
+
+/*! \brief Struct for what needs to be cleared after RCU.
+ *
+ * This can't be zone_update_t structure as this might be already freed at that time.
+ */
+typedef struct {
+ struct rcu_head rcuhead;
+
+ zone_contents_t *free_contents;
+ void (*free_method)(zone_contents_t *);
+
+ apply_ctx_t *cleanup_apply;
+
+ size_t new_cont_size;
+} update_clear_ctx_t;
+
+static void update_clear(struct rcu_head *param)
+{
+ static counter_reach_t counter = { PTHREAD_MUTEX_INITIALIZER, 0 };
+
+ update_clear_ctx_t *ctx = (update_clear_ctx_t *)param;
+
+ ctx->free_method(ctx->free_contents);
+ apply_cleanup(ctx->cleanup_apply);
+ free(ctx->cleanup_apply);
+
+ if (counter_reach(&counter, ctx->new_cont_size, UPDATE_MEMTRIM_AT)) {
+ mem_trim();
+ }
+
+ free(ctx);
+}
+
+static void discard_adds_tree(zone_update_t *update)
+{
+ additionals_tree_free(update->new_cont->adds_tree);
+ update->new_cont->adds_tree = NULL;
+}
+
+int zone_update_semcheck(conf_t *conf, zone_update_t *update)
+{
+ if (update == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ zone_tree_t *node_ptrs = (update->flags & UPDATE_INCREMENTAL) ?
+ update->a_ctx->node_ptrs : NULL;
+
+ // adjust_cb_nsec3_pointer not needed as we don't check DNSSEC here
+ int ret = zone_adjust_contents(update->new_cont, adjust_cb_flags, NULL,
+ false, false, 1, node_ptrs);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ sem_handler_t handler = {
+ .cb = err_handler_logger
+ };
+
+ conf_val_t val = conf_zone_get(conf, C_SEM_CHECKS, update->zone->name);
+ semcheck_optional_t mode = (conf_opt(&val) == SEMCHECKS_SOFT) ?
+ SEMCHECK_MANDATORY_SOFT : SEMCHECK_MANDATORY_ONLY;
+
+ ret = sem_checks_process(update->new_cont, mode, &handler, time(NULL));
+ if (ret != KNOT_EOK) {
+ // error is logged by the error handler
+ return ret;
+ }
+
+ return KNOT_EOK;
+}
+
+int zone_update_verify_digest(conf_t *conf, zone_update_t *update)
+{
+ conf_val_t val = conf_zone_get(conf, C_ZONEMD_VERIFY, update->zone->name);
+ if (!conf_bool(&val)) {
+ return KNOT_EOK;
+ }
+
+ int ret = zone_contents_digest_verify(update->new_cont);
+ if (ret != KNOT_EOK) {
+ log_zone_error(update->zone->name, "ZONEMD, verification failed (%s)",
+ knot_strerror(ret));
+ } else {
+ log_zone_info(update->zone->name, "ZONEMD, verification successful");
+ }
+
+ return ret;
+}
+
+int zone_update_commit(conf_t *conf, zone_update_t *update)
+{
+ if (conf == NULL || update == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ int ret = KNOT_EOK;
+
+ if ((update->flags & UPDATE_INCREMENTAL) && zone_update_no_change(update)) {
+ zone_update_clear(update);
+ return KNOT_EOK;
+ }
+
+ if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
+ ret = commit_incremental(conf, update);
+ } else {
+ ret = commit_full(conf, update);
+ }
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ conf_val_t val = conf_zone_get(conf, C_DNSSEC_SIGNING, update->zone->name);
+ bool dnssec = conf_bool(&val);
+
+ conf_val_t thr = conf_zone_get(conf, C_ADJUST_THR, update->zone->name);
+ if ((update->flags & (UPDATE_HYBRID | UPDATE_FULL))) {
+ ret = zone_adjust_full(update->new_cont, conf_int(&thr));
+ } else {
+ ret = zone_adjust_incremental_update(update, conf_int(&thr));
+ }
+ if (ret != KNOT_EOK) {
+ discard_adds_tree(update);
+ return ret;
+ }
+
+ /* Check the zone size. */
+ val = conf_zone_get(conf, C_ZONE_MAX_SIZE, update->zone->name);
+ size_t size_limit = conf_int(&val);
+
+ if (update->new_cont->size > size_limit) {
+ discard_adds_tree(update);
+ return KNOT_EZONESIZE;
+ }
+
+ val = conf_zone_get(conf, C_DNSSEC_VALIDATION, update->zone->name);
+ if (conf_bool(&val)) {
+ bool incr_valid = update->flags & UPDATE_INCREMENTAL;
+ const char *msg_valid = incr_valid ? "incremental " : "";
+
+ ret = knot_dnssec_validate_zone(update, conf, 0, incr_valid);
+ if (ret != KNOT_EOK) {
+ log_zone_error(update->zone->name, "DNSSEC, %svalidation failed (%s)",
+ msg_valid, knot_strerror(ret));
+ char type_str[16];
+ knot_dname_txt_storage_t name_str;
+ if (knot_dname_to_str(name_str, update->validation_hint.node, sizeof(name_str)) != NULL &&
+ knot_rrtype_to_string(update->validation_hint.rrtype, type_str, sizeof(type_str)) >= 0) {
+ log_zone_error(update->zone->name, "DNSSEC, validation hint: %s %s",
+ name_str, type_str);
+ }
+ discard_adds_tree(update);
+ if (conf->cache.srv_dbus_event & DBUS_EVENT_ZONE_INVALID) {
+ systemd_emit_zone_invalid(update->zone->name);
+ }
+ return ret;
+ } else {
+ log_zone_info(update->zone->name, "DNSSEC, %svalidation successful", msg_valid);
+ }
+ }
+
+ ret = update_catalog(conf, update);
+ if (ret != KNOT_EOK) {
+ log_zone_error(update->zone->name, "failed to process catalog zone (%s)", knot_strerror(ret));
+ discard_adds_tree(update);
+ return ret;
+ }
+
+ ret = commit_journal(conf, update);
+ if (ret != KNOT_EOK) {
+ discard_adds_tree(update);
+ return ret;
+ }
+
+ if (dnssec && zone_is_slave(conf, update->zone)) {
+ ret = zone_set_lastsigned_serial(update->zone,
+ zone_contents_serial(update->new_cont));
+ if (ret != KNOT_EOK) {
+ log_zone_warning(update->zone->name,
+ "unable to save lastsigned serial, "
+ "future transfers might be broken");
+ }
+ }
+
+ /* Switch zone contents. */
+ zone_contents_t *old_contents;
+ old_contents = zone_switch_contents(update->zone, update->new_cont);
+
+ if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
+ changeset_clear(&update->change);
+ changeset_clear(&update->extra_ch);
+ }
+ zone_contents_deep_free(update->init_cont);
+
+ update_clear_ctx_t *clear_ctx = calloc(1, sizeof(*clear_ctx));
+ if (clear_ctx != NULL) {
+ clear_ctx->free_contents = old_contents;
+ clear_ctx->free_method = (
+ (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) ?
+ zone_contents_deep_free : update_free_zone
+ );
+ clear_ctx->cleanup_apply = update->a_ctx;
+ clear_ctx->new_cont_size = update->new_cont->size;
+
+ call_rcu((struct rcu_head *)clear_ctx, update_clear);
+ } else {
+ log_zone_error(update->zone->name, "failed to deallocate unused memory");
+ }
+
+ /* Sync zonefile immediately if configured. */
+ val = conf_zone_get(conf, C_ZONEFILE_SYNC, update->zone->name);
+ if (conf_int(&val) == 0) {
+ zone_events_schedule_now(update->zone, ZONE_EVENT_FLUSH);
+ }
+
+ if (conf->cache.srv_dbus_event & DBUS_EVENT_ZONE_UPDATED) {
+ systemd_emit_zone_updated(update->zone->name,
+ zone_contents_serial(update->zone->contents));
+ }
+
+ memset(update, 0, sizeof(*update));
+
+ return KNOT_EOK;
+}
+
+bool zone_update_no_change(zone_update_t *update)
+{
+ if (update == NULL) {
+ return true;
+ }
+
+ if (update->flags & UPDATE_NO_CHSET) {
+ zone_diff_t diff;
+ get_zone_diff(&diff, update);
+ return (zone_diff_serialized_size(diff) == 0);
+ } else if (update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) {
+ return changeset_empty(&update->change);
+ } else {
+ /* This branch does not make much sense and FULL update will most likely
+ * be a change every time anyway, just return false. */
+ return false;
+ }
+}
+
+static bool zone_diff_rdataset(const zone_contents_t *c, uint16_t rrtype)
+{
+ const knot_rdataset_t *a = node_rdataset(binode_counterpart(c->apex), rrtype);
+ const knot_rdataset_t *b = node_rdataset(c->apex, rrtype);
+ if ((a == NULL && b == NULL) || (a != NULL && b != NULL && a->rdata == b->rdata)) {
+ return false;
+ } else {
+ return !knot_rdataset_eq(a, b);
+ }
+}
+
+static bool contents_have_dnskey(const zone_contents_t *contents)
+{
+ if (contents == NULL) {
+ return false;
+ }
+ assert(contents->apex != NULL);
+ return (node_rrtype_exists(contents->apex, KNOT_RRTYPE_DNSKEY) ||
+ node_rrtype_exists(contents->apex, KNOT_RRTYPE_CDNSKEY) ||
+ node_rrtype_exists(contents->apex, KNOT_RRTYPE_CDS));
+}
+
+bool zone_update_changes_dnskey(zone_update_t *update)
+{
+ if (update->flags & UPDATE_NO_CHSET) {
+ return (zone_diff_rdataset(update->new_cont, KNOT_RRTYPE_DNSKEY) ||
+ zone_diff_rdataset(update->new_cont, KNOT_RRTYPE_CDNSKEY) ||
+ zone_diff_rdataset(update->new_cont, KNOT_RRTYPE_CDS));
+ } else if (update->flags & UPDATE_FULL) {
+ return contents_have_dnskey(update->new_cont);
+ } else {
+ return (contents_have_dnskey(update->change.remove) ||
+ contents_have_dnskey(update->change.add));
+ }
+}
diff --git a/src/knot/updates/zone-update.h b/src/knot/updates/zone-update.h
new file mode 100644
index 0000000..0499d72
--- /dev/null
+++ b/src/knot/updates/zone-update.h
@@ -0,0 +1,299 @@
+/* Copyright (C) 2023 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include "knot/updates/apply.h"
+#include "knot/conf/conf.h"
+#include "knot/updates/changesets.h"
+#include "knot/zone/contents.h"
+#include "knot/zone/zone.h"
+
+typedef struct {
+ knot_dname_storage_t next;
+ const knot_dname_t *node;
+ uint16_t rrtype;
+} dnssec_validation_hint_t;
+
+/*! \brief Structure for zone contents updating / querying. */
+typedef struct zone_update {
+ zone_t *zone; /*!< Zone being updated. */
+ zone_contents_t *new_cont; /*!< New zone contents for full updates. */
+ changeset_t change; /*!< Changes we want to apply. */
+ zone_contents_t *init_cont; /*!< Exact contents of the zonefile. */
+ changeset_t extra_ch; /*!< Extra changeset to store just diff btwn zonefile and result. */
+ apply_ctx_t *a_ctx; /*!< Context for applying changesets. */
+ uint32_t flags; /*!< Zone update flags. */
+ dnssec_validation_hint_t validation_hint;
+} zone_update_t;
+
+typedef struct {
+ zone_update_t *update; /*!< The update we're iterating over. */
+ zone_tree_it_t tree_it; /*!< Iterator for the new zone. */
+ const zone_node_t *cur_node; /*!< Current node in the new zone. */
+ bool nsec3; /*!< Set when we're using the NSEC3 node tree. */
+} zone_update_iter_t;
+
+typedef enum {
+ // Mutually exclusive flags
+ UPDATE_FULL = 1 << 0, /*!< Replace the old zone by a complete new one. */
+ UPDATE_HYBRID = 1 << 1, /*!< Changeset like for incremental, adjusting like full. */
+ UPDATE_INCREMENTAL = 1 << 2, /*!< Apply changes to the old zone. */
+ // Additional flags
+ UPDATE_STRICT = 1 << 4, /*!< Apply changes strictly, i.e. fail when removing nonexistent RR. */
+ UPDATE_EXTRA_CHSET = 1 << 6, /*!< Extra changeset in use, to store diff btwn zonefile and final contents. */
+ UPDATE_CHANGED_NSEC = 1 << 7, /*!< This incremental update affects NSEC or NSEC3 nodes in zone. */
+ UPDATE_NO_CHSET = 1 << 8, /*!< Avoid using changeset and serialize to journal from diff of bi-nodes. */
+} zone_update_flags_t;
+
+/*!
+ * \brief Inits given zone update structure, new memory context is created.
+ *
+ * \param update Zone update structure to init.
+ * \param zone Init with this zone.
+ * \param flags Flags to control the behavior of the update.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_init(zone_update_t *update, zone_t *zone, zone_update_flags_t flags);
+
+/*!
+ * \brief Inits update structure, the update is built like IXFR from differences.
+ *
+ * The existing zone with its own contents is taken as a base,
+ * the new candidate zone contents are taken as new contents,
+ * the diff is calculated, so that this update is INCREMENTAL.
+ *
+ * \param update Zone update structure to init.
+ * \param zone Init with this zone.
+ * \param old_cont The current zone contents the diff will be against. Probably zone->contents.
+ * \param new_cont New zone contents. Will be taken over (and later freed) by zone update.
+ * \param flags Flags for update. Must be UPDATE_INCREMENTAL or UPDATE_HYBRID.
+ * \param ignore_dnssec Ignore DNSSEC records.
+ * \param ignore_zonemd Ignore ZONEMD records.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_from_differences(zone_update_t *update, zone_t *zone, zone_contents_t *old_cont,
+ zone_contents_t *new_cont, zone_update_flags_t flags,
+ bool ignore_dnssec, bool ignore_zonemd);
+
+/*!
+ * \brief Inits a zone update based on new zone contents.
+ *
+ * \param update Zone update structure to init.
+ * \param zone_without_contents Init with this zone. Its contents may be NULL.
+ * \param new_cont New zone contents. Will be taken over (and later freed) by zone update.
+ * \param flags Flags for update.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_from_contents(zone_update_t *update, zone_t *zone_without_contents,
+ zone_contents_t *new_cont, zone_update_flags_t flags);
+
+/*!
+ * \brief Inits using extra changeset, increments SOA serial.
+ *
+ * This shall be used after from_differences, to start tracking changes that are against the loaded zonefile.
+ *
+ * \param update Zone update.
+ * \param conf Configuration.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_start_extra(zone_update_t *update, conf_t *conf);
+
+/*!
+ * \brief Returns node that would be in the zone after updating it.
+ *
+ * \note Returned node is either zone original or synthesized, do *not* free
+ * or modify. Returned node is allocated on local mempool.
+ *
+ * \param update Zone update.
+ * \param dname Dname to search for.
+ *
+ * \return Node after zone update.
+ */
+const zone_node_t *zone_update_get_node(zone_update_t *update,
+ const knot_dname_t *dname);
+
+/*!
+ * \brief Returns the serial from the current apex.
+ *
+ * \param update Zone update.
+ *
+ * \return 0 if no apex was found, its serial otherwise.
+ */
+uint32_t zone_update_current_serial(zone_update_t *update);
+
+/*! \brief Return true if NSEC3PARAM has been changed in this update. */
+bool zone_update_changed_nsec3param(const zone_update_t *update);
+
+/*!
+ * \brief Returns the SOA rdataset we're updating from.
+ *
+ * \param update Zone update.
+ *
+ * \return The original SOA rdataset.
+ */
+const knot_rdataset_t *zone_update_from(zone_update_t *update);
+
+/*!
+ * \brief Returns the SOA rdataset we're updating to.
+ *
+ * \param update Zone update.
+ *
+ * \return NULL if no new SOA has been added, new SOA otherwise.
+ *
+ * \todo Refactor this function according to its use.
+ */
+const knot_rdataset_t *zone_update_to(zone_update_t *update);
+
+/*!
+ * \brief Clear data allocated by given zone update structure.
+ *
+ * \param update Zone update to clear.
+ */
+void zone_update_clear(zone_update_t *update);
+
+/*!
+ * \brief Adds an RRSet to the zone.
+ *
+ * \warning Do not edit the zone_update when any iterator is active. Any
+ * zone_update modifications will invalidate the trie iterators
+ * in the zone_update iterator(s).
+ *
+ * \param update Zone update.
+ * \param rrset RRSet to add.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_add(zone_update_t *update, const knot_rrset_t *rrset);
+
+/*!
+ * \brief Removes an RRSet from the zone.
+ *
+ * \warning Do not edit the zone_update when any iterator is active. Any
+ * zone_update modifications will invalidate the trie iterators
+ * in the zone_update iterator(s).
+ *
+ * \param update Zone update.
+ * \param rrset RRSet to remove.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_remove(zone_update_t *update, const knot_rrset_t *rrset);
+
+/*!
+ * \brief Removes a whole RRSet of specified type from the zone.
+ *
+ * \warning Do not edit the zone_update when any iterator is active. Any
+ * zone_update modifications will invalidate the trie iterators
+ * in the zone_update iterator(s).
+ *
+ * \param update Zone update.
+ * \param owner Node name to remove.
+ * \param type RRSet type to remove.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_remove_rrset(zone_update_t *update, knot_dname_t *owner, uint16_t type);
+
+/*!
+ * \brief Removes a whole node from the zone.
+ *
+ * \warning Do not edit the zone_update when any iterator is active. Any
+ * zone_update modifications will invalidate the trie iterators
+ * in the zone_update iterator(s).
+ *
+ * \param update Zone update.
+ * \param owner Node name to remove.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_remove_node(zone_update_t *update, const knot_dname_t *owner);
+
+/*!
+ * \brief Adds and removes RRsets to/from the zone according to the changeset.
+ *
+ * \param update Zone update.
+ * \param changes Changes to be made in zone.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_apply_changeset(zone_update_t *update, const changeset_t *changes);
+
+/*!
+ * \brief Applies the changeset in reverse, rsets from REM section are added and from ADD section removed.
+ *
+ * \param update Zone update.
+ * \param changes Changes to be un-done.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_apply_changeset_reverse(zone_update_t *update, const changeset_t *changes);
+
+/*!
+ * \brief Increment SOA serial (according to configured policy) in the update.
+ *
+ * \param update Update to be modified.
+ * \param conf Configuration.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_increment_soa(zone_update_t *update, conf_t *conf);
+
+/*!
+ * \brief Executes mandatory semantic checks on the zone contents.
+ *
+ * \param conf Configuration.
+ * \param update Update to be checked.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_semcheck(conf_t *conf, zone_update_t *update);
+
+/*!
+ * \brief If configured, verify ZONEMD and log the result.
+ *
+ * \param conf Configuration.
+ * \param update Zone update.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_verify_digest(conf_t *conf, zone_update_t *update);
+
+/*!
+ * \brief Commits all changes to the zone, signs it, saves changes to journal.
+ *
+ * \param conf Configuration.
+ * \param update Zone update.
+ *
+ * \return KNOT_E*
+ */
+int zone_update_commit(conf_t *conf, zone_update_t *update);
+
+/*!
+ * \brief Returns bool whether there are any changes at all.
+ *
+ * \param update Zone update.
+ */
+bool zone_update_no_change(zone_update_t *update);
+
+/*!
+ * \brief Return whether apex DNSKEY, CDNSKEY, or CDS is updated.
+ */
+bool zone_update_changes_dnskey(zone_update_t *update);