From f449f278dd3c70e479a035f50a9bb817a9b433ba Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 17:24:08 +0200 Subject: Adding upstream version 3.2.6. Signed-off-by: Daniel Baumann --- src/knot/events/handlers/load.c | 406 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 src/knot/events/handlers/load.c (limited to 'src/knot/events/handlers/load.c') diff --git a/src/knot/events/handlers/load.c b/src/knot/events/handlers/load.c new file mode 100644 index 0000000..13e3298 --- /dev/null +++ b/src/knot/events/handlers/load.c @@ -0,0 +1,406 @@ +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. + + 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 . + */ + +#include + +#include "knot/catalog/generate.h" +#include "knot/common/log.h" +#include "knot/conf/conf.h" +#include "knot/dnssec/key-events.h" +#include "knot/dnssec/zone-events.h" +#include "knot/events/handlers.h" +#include "knot/events/replan.h" +#include "knot/zone/digest.h" +#include "knot/zone/serial.h" +#include "knot/zone/zone-diff.h" +#include "knot/zone/zone-load.h" +#include "knot/zone/zone.h" +#include "knot/zone/zonefile.h" +#include "knot/updates/acl.h" + +static bool dontcare_load_error(conf_t *conf, const zone_t *zone) +{ + return (zone->contents == NULL && zone_load_can_bootstrap(conf, zone->name)); +} + +static bool allowed_xfr(conf_t *conf, const zone_t *zone) +{ + conf_val_t acl = conf_zone_get(conf, C_ACL, zone->name); + while (acl.code == KNOT_EOK) { + conf_val_t action = conf_id_get(conf, C_ACL, C_ACTION, &acl); + while (action.code == KNOT_EOK) { + if (conf_opt(&action) == ACL_ACTION_TRANSFER) { + return true; + } + conf_val_next(&action); + } + conf_val_next(&acl); + } + + return false; +} + +int event_load(conf_t *conf, zone_t *zone) +{ + zone_update_t up = { 0 }; + zone_contents_t *journal_conts = NULL, *zf_conts = NULL; + bool old_contents_exist = (zone->contents != NULL), zone_in_journal_exists = false; + + conf_val_t val = conf_zone_get(conf, C_JOURNAL_CONTENT, zone->name); + unsigned load_from = conf_opt(&val); + + val = conf_zone_get(conf, C_ZONEFILE_LOAD, zone->name); + unsigned zf_from = conf_opt(&val); + + int ret = KNOT_EOK; + + // If configured, load journal contents. + if (!old_contents_exist && + ((load_from == JOURNAL_CONTENT_ALL && zf_from != ZONEFILE_LOAD_WHOLE) || + zone->cat_members != NULL)) { + ret = zone_load_from_journal(conf, zone, &journal_conts); + switch (ret) { + case KNOT_EOK: + zone_in_journal_exists = true; + break; + case KNOT_ENOENT: + zone_in_journal_exists = false; + break; + default: + goto cleanup; + } + } else { + zone_in_journal_exists = zone_journal_has_zij(zone); + } + + // If configured, attempt to load zonefile. + if (zf_from != ZONEFILE_LOAD_NONE && zone->cat_members == NULL) { + struct timespec mtime; + char *filename = conf_zonefile(conf, zone->name); + ret = zonefile_exists(filename, &mtime); + if (ret == KNOT_EOK) { + conf_val_t semchecks = conf_zone_get(conf, C_SEM_CHECKS, zone->name); + semcheck_optional_t mode = conf_opt(&semchecks); + if (mode == SEMCHECK_DNSSEC_AUTO) { + conf_val_t validation = conf_zone_get(conf, C_DNSSEC_VALIDATION, zone->name); + if (conf_bool(&validation)) { + /* Disable duplicate DNSSEC checks, which are the + same as DNSSEC validation in zone update commit. */ + mode = SEMCHECK_DNSSEC_OFF; + } + } + + ret = zone_load_contents(conf, zone->name, &zf_conts, mode, false); + } + if (ret != KNOT_EOK) { + assert(!zf_conts); + if (dontcare_load_error(conf, zone)) { + log_zone_info(zone->name, "failed to parse zone file '%s' (%s)", + filename, knot_strerror(ret)); + } else { + log_zone_error(zone->name, "failed to parse zone file '%s' (%s)", + filename, knot_strerror(ret)); + } + free(filename); + goto load_end; + } + free(filename); + + // Save zonefile information. + zone->zonefile.serial = zone_contents_serial(zf_conts); + zone->zonefile.exists = (zf_conts != NULL); + zone->zonefile.mtime = mtime; + + // If configured and possible, fix the SOA serial of zonefile. + zone_contents_t *relevant = (zone->contents != NULL ? zone->contents : journal_conts); + if (zf_conts != NULL && zf_from == ZONEFILE_LOAD_DIFSE && relevant != NULL) { + uint32_t serial = zone_contents_serial(relevant); + conf_val_t policy = conf_zone_get(conf, C_SERIAL_POLICY, zone->name); + uint32_t set = serial_next(serial, conf_opt(&policy), 1); + zone_contents_set_soa_serial(zf_conts, set); + log_zone_info(zone->name, "zone file parsed, serial updated %u -> %u", + zone->zonefile.serial, set); + zone->zonefile.serial = set; + } else { + log_zone_info(zone->name, "zone file parsed, serial %u", + zone->zonefile.serial); + } + + // If configured and appliable to zonefile, load journal changes. + if (load_from != JOURNAL_CONTENT_NONE) { + ret = zone_load_journal(conf, zone, zf_conts); + if (ret != KNOT_EOK) { + zone_contents_deep_free(zf_conts); + zf_conts = NULL; + log_zone_warning(zone->name, "failed to load journal (%s)", + knot_strerror(ret)); + } + } + } + if (zone->cat_members != NULL && !old_contents_exist) { + uint32_t serial = journal_conts == NULL ? 1 : zone_contents_serial(journal_conts); + serial = serial_next(serial, SERIAL_POLICY_UNIXTIME, 1); // unixtime hardcoded + zf_conts = catalog_update_to_zone(zone->cat_members, zone->name, serial); + if (zf_conts == NULL) { + ret = zone->cat_members->error == KNOT_EOK ? KNOT_ENOMEM : zone->cat_members->error; + goto cleanup; + } + } + + // If configured contents=all, but not present, store zonefile. + if ((load_from == JOURNAL_CONTENT_ALL || zone->cat_members != NULL) && + !zone_in_journal_exists && (zf_conts != NULL || old_contents_exist)) { + zone_contents_t *store_c = old_contents_exist ? zone->contents : zf_conts; + ret = zone_in_journal_store(conf, zone, store_c); + if (ret != KNOT_EOK) { + log_zone_warning(zone->name, "failed to write zone-in-journal (%s)", + knot_strerror(ret)); + } else { + zone_in_journal_exists = true; + } + } + + val = conf_zone_get(conf, C_DNSSEC_SIGNING, zone->name); + bool dnssec_enable = (conf_bool(&val) && zone->cat_members == NULL), zu_from_zf_conts = false; + bool do_diff = (zf_from == ZONEFILE_LOAD_DIFF || zf_from == ZONEFILE_LOAD_DIFSE || zone->cat_members != NULL); + bool ignore_dnssec = (do_diff && dnssec_enable); + + val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name); + unsigned digest_alg = conf_opt(&val); + bool update_zonemd = (digest_alg != ZONE_DIGEST_NONE); + + // Create zone_update structure according to current state. + if (old_contents_exist) { + if (zone->cat_members != NULL) { + ret = zone_update_init(&up, zone, UPDATE_INCREMENTAL); + if (ret == KNOT_EOK) { + ret = catalog_update_to_update(zone->cat_members, &up); + } + if (ret == KNOT_EOK) { + ret = zone_update_increment_soa(&up, conf); + } + } else if (zf_conts == NULL) { + // nothing to be re-loaded + ret = KNOT_EOK; + goto cleanup; + } else if (zf_from == ZONEFILE_LOAD_WHOLE) { + // throw old zone contents and load new from ZF + ret = zone_update_from_contents(&up, zone, zf_conts, + (load_from == JOURNAL_CONTENT_NONE ? + UPDATE_FULL : UPDATE_HYBRID)); + zu_from_zf_conts = true; + } else { + // compute ZF diff and if success, apply it + ret = zone_update_from_differences(&up, zone, NULL, zf_conts, UPDATE_INCREMENTAL, + ignore_dnssec, update_zonemd); + } + } else { + if (journal_conts != NULL && (zf_from != ZONEFILE_LOAD_WHOLE || zone->cat_members != NULL)) { + if (zf_conts == NULL) { + // load zone-in-journal + ret = zone_update_from_contents(&up, zone, journal_conts, UPDATE_HYBRID); + } else { + // load zone-in-journal, compute ZF diff and if success, apply it + ret = zone_update_from_differences(&up, zone, journal_conts, zf_conts, + UPDATE_HYBRID, ignore_dnssec, update_zonemd); + if (ret == KNOT_ESEMCHECK || ret == KNOT_ERANGE) { + log_zone_warning(zone->name, + "zone file changed with SOA serial %s, " + "ignoring zone file and loading from journal", + (ret == KNOT_ESEMCHECK ? "unupdated" : "decreased")); + zone_contents_deep_free(zf_conts); + zf_conts = NULL; + ret = zone_update_from_contents(&up, zone, journal_conts, UPDATE_HYBRID); + } + } + } else { + if (zf_conts == NULL) { + // nothing to be loaded + ret = KNOT_ENOENT; + } else { + // load from ZF + ret = zone_update_from_contents(&up, zone, zf_conts, + (load_from == JOURNAL_CONTENT_NONE ? + UPDATE_FULL : UPDATE_HYBRID)); + if (zf_from == ZONEFILE_LOAD_WHOLE) { + zu_from_zf_conts = true; + } + } + } + } + +load_end: + if (ret != KNOT_EOK) { + switch (ret) { + case KNOT_ENOENT: + if (zone_load_can_bootstrap(conf, zone->name)) { + log_zone_info(zone->name, "zone will be bootstrapped"); + } else { + log_zone_info(zone->name, "zone not found"); + } + break; + case KNOT_ESEMCHECK: + log_zone_warning(zone->name, "zone file changed without SOA serial update"); + break; + case KNOT_ERANGE: + if (serial_compare(zone->zonefile.serial, zone_contents_serial(zone->contents)) == SERIAL_INCOMPARABLE) { + log_zone_warning(zone->name, "zone file changed with incomparable SOA serial"); + } else { + log_zone_warning(zone->name, "zone file changed with decreased SOA serial"); + } + break; + } + goto cleanup; + } + + bool zf_serial_updated = (zf_conts != NULL && zone_contents_serial(zf_conts) != zone_contents_serial(zone->contents)); + + // The contents are already part of zone_update. + zf_conts = NULL; + journal_conts = NULL; + + ret = zone_update_verify_digest(conf, &up); + if (ret != KNOT_EOK) { + goto cleanup; + } + + uint32_t middle_serial = zone_contents_serial(up.new_cont); + + if (do_diff && old_contents_exist && dnssec_enable && zf_serial_updated && + !zone_in_journal_exists) { + ret = zone_update_start_extra(&up, conf); + if (ret != KNOT_EOK) { + goto cleanup; + } + } + + // Sign zone using DNSSEC if configured. + zone_sign_reschedule_t dnssec_refresh = { 0 }; + if (dnssec_enable) { + ret = knot_dnssec_zone_sign(&up, conf, 0, KEY_ROLL_ALLOW_ALL, 0, &dnssec_refresh); + if (ret != KNOT_EOK) { + goto cleanup; + } + if (zu_from_zf_conts && (up.flags & UPDATE_HYBRID) && allowed_xfr(conf, zone)) { + log_zone_warning(zone->name, + "with automatic DNSSEC signing and outgoing transfers enabled, " + "'zonefile-load: difference' should be set to avoid malformed " + "IXFR after manual zone file update"); + } + } else if (update_zonemd) { + /* Don't update ZONEMD if no change and ZONEMD is up-to-date. + * If ZONEFILE_LOAD_DIFSE, the change is non-empty and ZONEMD + * is directly updated without its verification. */ + if (!zone_update_no_change(&up) || !zone_contents_digest_exists(up.new_cont, digest_alg, false)) { + if (zone_update_to(&up) == NULL || middle_serial == zone->zonefile.serial) { + ret = zone_update_increment_soa(&up, conf); + } + if (ret == KNOT_EOK) { + ret = zone_update_add_digest(&up, digest_alg, false); + } + if (ret != KNOT_EOK) { + goto cleanup; + } + } + } + + // If the change is only automatically incremented SOA serial, make it no change. + if ((zf_from == ZONEFILE_LOAD_DIFSE || zone->cat_members != NULL) && + (up.flags & (UPDATE_INCREMENTAL | UPDATE_HYBRID)) && + changeset_differs_just_serial(&up.change, update_zonemd)) { + changeset_t *cpy = changeset_clone(&up.change); + if (cpy == NULL) { + ret = KNOT_ENOMEM; + goto cleanup; + } + ret = zone_update_apply_changeset_reverse(&up, cpy); + if (ret != KNOT_EOK) { + changeset_free(cpy); + goto cleanup; + } + + // If the original ZONEMD is outdated, use the reverted changeset again. + if (update_zonemd && !zone_contents_digest_exists(up.new_cont, digest_alg, false)) { + ret = zone_update_apply_changeset(&up, cpy); + changeset_free(cpy); + if (ret != KNOT_EOK) { + goto cleanup; + } + } else { + changeset_free(cpy); + // Revert automatic zone serial increment. + zone->zonefile.serial = zone_contents_serial(up.new_cont); + /* Reset possibly set the resigned flag. Note that dnssec + * reschedule isn't reverted, but shouldn't be a problem + * for non-empty zones as SOA, ZONEMD, and their RRSIGs + * are always updated with other changes in the zone. */ + zone->zonefile.resigned = false; + } + } + + uint32_t old_serial = 0, new_serial = zone_contents_serial(up.new_cont); + char old_serial_str[11] = "none", new_serial_str[15] = ""; + if (old_contents_exist) { + old_serial = zone_contents_serial(zone->contents); + (void)snprintf(old_serial_str, sizeof(old_serial_str), "%u", old_serial); + } + if (new_serial != middle_serial) { + (void)snprintf(new_serial_str, sizeof(new_serial_str), " -> %u", new_serial); + } + + // Commit zone_update back to zone (including journal update, rcu,...). + ret = zone_update_commit(conf, &up); + if (ret != KNOT_EOK) { + goto cleanup; + } + + char expires_in[32] = ""; + if (zone->timers.next_expire > 0) { + (void)snprintf(expires_in, sizeof(expires_in), + ", expires in %u seconds", + (uint32_t)MAX(zone->timers.next_expire - time(NULL), 0)); + } + + log_zone_info(zone->name, "loaded, serial %s -> %u%s, %zu bytes%s", + old_serial_str, middle_serial, new_serial_str, zone->contents->size, expires_in); + + if (zone->cat_members != NULL) { + catalog_update_clear(zone->cat_members); + } + + // Schedule dependent events. + if (dnssec_enable) { + event_dnssec_reschedule(conf, zone, &dnssec_refresh, false); // false since we handle NOTIFY below + } + + replan_from_timers(conf, zone); + + if (!zone_timers_serial_notified(&zone->timers, new_serial)) { + zone_schedule_notify(zone, 0); + } + + return KNOT_EOK; + +cleanup: + // Try to bootstrap the zone if local error. + replan_from_timers(conf, zone); + + zone_update_clear(&up); + zone_contents_deep_free(zf_conts); + zone_contents_deep_free(journal_conts); + + return (dontcare_load_error(conf, zone) ? KNOT_EOK : ret); +} -- cgit v1.2.3