diff options
Diffstat (limited to 'src/knot/updates')
-rw-r--r-- | src/knot/updates/acl.c | 361 | ||||
-rw-r--r-- | src/knot/updates/acl.h | 83 | ||||
-rw-r--r-- | src/knot/updates/apply.c | 379 | ||||
-rw-r--r-- | src/knot/updates/apply.h | 101 | ||||
-rw-r--r-- | src/knot/updates/changesets.c | 628 | ||||
-rw-r--r-- | src/knot/updates/changesets.h | 290 | ||||
-rw-r--r-- | src/knot/updates/ddns.c | 701 | ||||
-rw-r--r-- | src/knot/updates/ddns.h | 47 | ||||
-rw-r--r-- | src/knot/updates/zone-update.c | 1098 | ||||
-rw-r--r-- | src/knot/updates/zone-update.h | 299 |
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(¶m)) { + 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); |