summaryrefslogtreecommitdiffstats
path: root/src/knot/updates/zone-update.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/knot/updates/zone-update.c')
-rw-r--r--src/knot/updates/zone-update.c1088
1 files changed, 1088 insertions, 0 deletions
diff --git a/src/knot/updates/zone-update.c b/src/knot/updates/zone-update.c
new file mode 100644
index 0000000..64ce179
--- /dev/null
+++ b/src/knot/updates/zone-update.c
@@ -0,0 +1,1088 @@
+/* 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;
+ }
+
+ int ret = KNOT_EOK;
+
+ if (update->flags & UPDATE_INCREMENTAL) {
+ if (rrset->type == KNOT_RRTYPE_SOA) {
+ // replace previous SOA
+ ret = apply_replace_soa(update->a_ctx, rrset);
+ } else {
+ ret = apply_add_rr(update->a_ctx, rrset);
+ if (ret == KNOT_EOK) {
+ update_affected_rrtype(update, rrset->type);
+ }
+ }
+ } else if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) {
+ if (rrset->type == KNOT_RRTYPE_SOA) {
+ /* replace previous SOA */
+ ret = replace_soa(update->new_cont, rrset);
+ goto chset_add;
+ }
+
+ zone_node_t *n = NULL;
+ 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);
+ ret = KNOT_EOK;
+ }
+ } else {
+ return KNOT_EINVAL;
+ }
+
+chset_add:
+ if ((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && ret == KNOT_EOK) {
+ 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);
+ }
+ }
+
+ return ret;
+}
+
+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;
+ }
+
+ /* Copy the rrset as sometimes rrset may be what is actually being removed thus
+ * its contents might be already freed memory when we get to changeset removal. */
+ knot_rrset_t *rrset_copy = knot_rrset_copy(rrset, NULL);
+ if (rrset_copy == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ int ret = KNOT_EOK;
+
+ if (update->flags & UPDATE_INCREMENTAL) {
+ if (rrset->type == KNOT_RRTYPE_SOA) {
+ /* SOA is replaced with addition */
+ } else {
+ ret = apply_remove_rr(update->a_ctx, rrset);
+ if (ret == KNOT_EOK) {
+ update_affected_rrtype(update, rrset->type);
+ }
+ }
+ } else if (update->flags & (UPDATE_FULL | UPDATE_HYBRID)) {
+ zone_node_t *n = NULL;
+ ret = zone_contents_remove_rr(update->new_cont, rrset, &n);
+ } else {
+ ret = KNOT_EINVAL;
+ }
+
+ if ((update->flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) &&
+ rrset_copy->type != KNOT_RRTYPE_SOA && !(update->flags & UPDATE_NO_CHSET) &&
+ ret == KNOT_EOK) {
+ ret = changeset_add_removal(&update->change, rrset_copy, 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_copy, CHANGESET_CHECK);
+ }
+ }
+
+ knot_rrset_free(rrset_copy, NULL);
+ return ret;
+}
+
+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);
+}
+
+int zone_update_increment_soa(zone_update_t *update, conf_t *conf)
+{
+ if (update == NULL || conf == NULL) {
+ return KNOT_EINVAL;
+ }
+
+ 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, conf, update->zone->name,
+ SERIAL_POLICY_AUTO, 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;
+}
+
+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, "enqueued %zd catalog 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) {
+ log_zone_error(update->zone->name, "journal update failed (%s)", knot_strerror(ret));
+ 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");
+ }
+
+ zone_local_notify(update->zone);
+
+ /* 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));
+ }
+}