From: Libor Peltan Date: Tue, 10 Jun 2025 12:42:55 +0000 (+0200) Subject: external validation: implemented basic functionality X-Git-Tag: v3.5.0~38^2~11 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8ccc39d8dc551190e6e9fa8bf4e2abf905a60545;p=thirdparty%2Fknot-dns.git external validation: implemented basic functionality --- diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c index 0c4e8e954d..72644dbfb0 100644 --- a/src/knot/conf/schema.c +++ b/src/knot/conf/schema.c @@ -454,6 +454,11 @@ static const yp_item_t desc_policy[] = { { NULL } }; +static const yp_item_t desc_external[] = { + { C_ID, YP_TSTR, YP_VNONE, CONF_IO_FREF }, + { NULL } +}; + #define ZONE_ITEMS(FLAGS) \ { C_STORAGE, YP_TSTR, YP_VSTR = { STORAGE_DIR }, FLAGS }, \ { C_FILE, YP_TSTR, YP_VNONE, FLAGS }, \ @@ -479,6 +484,7 @@ static const yp_item_t desc_policy[] = { { C_IXFR_FROM_AXFR, YP_TBOOL, YP_VNONE }, \ { C_ZONE_MAX_SIZE, YP_TINT, YP_VINT = { 0, SSIZE_MAX, SSIZE_MAX, YP_SSIZE }, FLAGS }, \ { C_ADJUST_THR, YP_TINT, YP_VINT = { 1, UINT16_MAX, 1 } }, \ + { C_EXTERNAL_VLDT, YP_TREF, YP_VREF = { C_EXTERNAL }, FLAGS, { check_ref_dflt } }, \ { C_DNSSEC_SIGNING, YP_TBOOL, YP_VNONE, FLAGS }, \ { C_DNSSEC_VALIDATION, YP_TBOOL, YP_VNONE, FLAGS }, \ { C_DNSSEC_POLICY, YP_TREF, YP_VREF = { C_POLICY }, FLAGS, { check_ref_dflt } }, \ @@ -535,6 +541,7 @@ const yp_item_t conf_schema[] = { { C_SBM, YP_TGRP, YP_VGRP = { desc_submission }, YP_FMULTI }, { C_DNSKEY_SYNC, YP_TGRP, YP_VGRP = { desc_dnskey_sync }, YP_FMULTI, { check_dnskey_sync } }, { C_POLICY, YP_TGRP, YP_VGRP = { desc_policy }, YP_FMULTI, { check_policy } }, + { C_EXTERNAL, YP_TGRP, YP_VGRP = { desc_external }, YP_FMULTI, { check_external } }, { C_TPL, YP_TGRP, YP_VGRP = { desc_template }, YP_FMULTI, { check_template } }, { C_ZONE, YP_TGRP, YP_VGRP = { desc_zone }, YP_FMULTI | CONF_IO_FZONE, { check_zone } }, { C_INCL, YP_TSTR, YP_VNONE, CONF_IO_FDIFF_ZONES | CONF_IO_FRLD_ALL, { include_file } }, diff --git a/src/knot/conf/schema.h b/src/knot/conf/schema.h index 9ba63f542d..536b783586 100644 --- a/src/knot/conf/schema.h +++ b/src/knot/conf/schema.h @@ -58,6 +58,8 @@ #define C_ECS "\x12""edns-client-subnet" #define C_EXPIRE_MAX_INTERVAL "\x13""expire-max-interval" #define C_EXPIRE_MIN_INTERVAL "\x13""expire-min-interval" +#define C_EXTERNAL "\x08""external" +#define C_EXTERNAL_VLDT "\x13""external-validation" #define C_FILE "\x04""file" #define C_GLOBAL_MODULE "\x0D""global-module" #define C_ID "\x02""id" diff --git a/src/knot/conf/tools.c b/src/knot/conf/tools.c index 7e0b079403..bbffbc9a14 100644 --- a/src/knot/conf/tools.c +++ b/src/knot/conf/tools.c @@ -851,6 +851,12 @@ int check_policy( return KNOT_EOK; } +int check_external( + knotd_conf_check_args_t *args) +{ + return KNOT_EOK; +} + int check_key( knotd_conf_check_args_t *args) { diff --git a/src/knot/conf/tools.h b/src/knot/conf/tools.h index 81537d6ef9..10a5cb4fd5 100644 --- a/src/knot/conf/tools.h +++ b/src/knot/conf/tools.h @@ -119,6 +119,10 @@ int check_policy( knotd_conf_check_args_t *args ); +int check_external( + knotd_conf_check_args_t *args +); + int check_key( knotd_conf_check_args_t *args ); diff --git a/src/knot/ctl/commands.c b/src/knot/ctl/commands.c index 73505ab7f4..25311db967 100644 --- a/src/knot/ctl/commands.c +++ b/src/knot/ctl/commands.c @@ -933,6 +933,12 @@ static int zone_txn_commit_l(zone_t *zone, _unused_ ctl_args_t *args) return KNOT_TXN_ENOTEXISTS; } + if (zone->control_update->flags & UPDATE_WFEV) { + zone->control_update->flags |= UPDATE_EVOK; + knot_sem_post(&zone->control_update->external); + return KNOT_EOK; + } + int ret = zone_update_semcheck(conf(), zone->control_update); if (ret != KNOT_EOK) { return ret; // Recoverable error. @@ -1005,6 +1011,12 @@ static int zone_txn_abort(zone_t *zone, _unused_ ctl_args_t *args) return KNOT_TXN_ENOTEXISTS; } + if (zone->control_update->flags & UPDATE_WFEV) { + knot_sem_post(&zone->control_update->external); + pthread_mutex_unlock(&zone->cu_lock); + return KNOT_EOK; + } + zone_control_clear(zone); pthread_mutex_unlock(&zone->cu_lock); @@ -1953,7 +1965,7 @@ static int ctl_zone(ctl_args_t *args, ctl_cmd_t cmd) static void check_zone_txn(zone_t *zone, const knot_dname_t **exists) { - if (zone->control_update != NULL) { + if (zone->control_update != NULL && !(zone->control_update->flags & UPDATE_WFEV)) { *exists = zone->name; } } diff --git a/src/knot/events/handlers/expire.c b/src/knot/events/handlers/expire.c index bd74fb8ada..c49cd8f9c0 100644 --- a/src/knot/events/handlers/expire.c +++ b/src/knot/events/handlers/expire.c @@ -23,6 +23,7 @@ int event_expire(conf_t *conf, zone_t *zone) synchronize_rcu(); pthread_mutex_lock(&zone->cu_lock); + assert(zone->control_update == NULL || !(zone->control_update->flags & UPDATE_WFEV)); zone_control_clear(zone); pthread_mutex_unlock(&zone->cu_lock); diff --git a/src/knot/events/handlers/refresh.c b/src/knot/events/handlers/refresh.c index e633cffd47..44de0b70fa 100644 --- a/src/knot/events/handlers/refresh.c +++ b/src/knot/events/handlers/refresh.c @@ -667,6 +667,10 @@ static int ixfr_finalize(struct refresh_data *data) ret = zone_update_commit(data->conf, &up); if (ret != KNOT_EOK) { zone_update_clear(&up); + if (ret == KNOT_EEXTERNAL) { + data->fallback_axfr = false; + data->fallback->remote = false; + } IXFRIN_LOG(LOG_WARNING, data, "failed to store changes (%s)", knot_strerror(ret)); return ret; diff --git a/src/knot/server/server.c b/src/knot/server/server.c index b390cbdc2a..ceb203958f 100644 --- a/src/knot/server/server.c +++ b/src/knot/server/server.c @@ -1039,6 +1039,13 @@ int server_start(server_t *server, bool answering) return KNOT_EOK; } +static void zonedb_shutdown(server_t *server) +{ + if (server->zone_db != NULL) { + knot_zonedb_foreach(server->zone_db, zone_shutdown); + } +} + void server_wait(server_t *server) { if (server == NULL) { @@ -1404,6 +1411,8 @@ void server_stop(server_t *server) /* Stop scheduler. */ evsched_stop(&server->sched); + /* Shut down zones. */ + zonedb_shutdown(server); /* Interrupt background workers. */ worker_pool_stop(server->workers); @@ -1621,6 +1630,7 @@ void server_update_zones(conf_t *conf, server_t *server, reload_t mode) /* Suspend adding events to worker pool queue, wait for queued events. */ log_debug("suspending zone events"); evsched_pause(&server->sched); + zonedb_shutdown(server); worker_pool_wait(server->workers); log_debug("suspended zone events"); diff --git a/src/knot/updates/zone-update.c b/src/knot/updates/zone-update.c index b936643ee8..e020e43373 100644 --- a/src/knot/updates/zone-update.c +++ b/src/knot/updates/zone-update.c @@ -915,6 +915,42 @@ int zone_update_verify_digest(conf_t *conf, zone_update_t *update) return ret; } +int zone_update_external(conf_t *conf, zone_update_t *update) +{ + (void)conf; + pthread_mutex_lock(&update->zone->cu_lock); + + if (update->zone->control_update != NULL) { + assert(update->zone->control_update == update); + assert(!(update->flags & UPDATE_WFEV)); + pthread_mutex_unlock(&update->zone->cu_lock); + return KNOT_EOK; // real control update never waits for external validation + } + + if (zone_get_flag(update->zone, ZONE_SHUT_DOWN, false)) { + pthread_mutex_unlock(&update->zone->cu_lock); + return KNOT_EEXTERNAL; + } + + update->zone->control_update = update; + update->flags |= UPDATE_WFEV; + knot_sem_init(&update->external, 0); + pthread_mutex_unlock(&update->zone->cu_lock); + + log_zone_notice(update->zone->name, "waiting for external validation"); + + knot_sem_wait(&update->external); + + pthread_mutex_lock(&update->zone->cu_lock); + update->zone->control_update = NULL; + pthread_mutex_unlock(&update->zone->cu_lock); + + knot_sem_post(&update->external); + knot_sem_destroy(&update->external); + + return (update->flags & UPDATE_EVOK) ? KNOT_EOK : KNOT_EEXTERNAL; +} + int zone_update_commit(conf_t *conf, zone_update_t *update) { if (conf == NULL || update == NULL) { @@ -977,6 +1013,15 @@ int zone_update_commit(conf_t *conf, zone_update_t *update) } } + val = conf_zone_get(conf, C_EXTERNAL_VLDT, update->zone->name); + if (val.code == KNOT_EOK) { + ret = zone_update_external(conf, update, &val); + if (ret != KNOT_EOK) { + discard_adds_tree(update); + return ret; + } + } + ret = update_catalog(conf, update); if (ret != KNOT_EOK) { log_zone_error(update->zone->name, "failed to process catalog zone (%s)", knot_strerror(ret)); diff --git a/src/knot/updates/zone-update.h b/src/knot/updates/zone-update.h index f4f6ca94ef..3229eed625 100644 --- a/src/knot/updates/zone-update.h +++ b/src/knot/updates/zone-update.h @@ -29,6 +29,7 @@ typedef struct zone_update { 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. */ + knot_sem_t external; /*!< Lock for external validation. */ dnssec_validation_hint_t validation_hint; } zone_update_t; @@ -50,6 +51,8 @@ typedef enum { 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. */ UPDATE_SIGNED_FULL = 1 << 9, /*!< Full (non-incremental) zone sign took place during this update. */ + UPDATE_WFEV = 1 << 10, /*!< Update waiting for external validation. */ + UPDATE_EVOK = 1 << 11, /*!< External validation accepted the update. */ } zone_update_flags_t; /*! @@ -268,6 +271,18 @@ int zone_update_semcheck(conf_t *conf, zone_update_t *update); */ int zone_update_verify_digest(conf_t *conf, zone_update_t *update); +/*! + * \brief Wait for external validation. + * + * \param conf Configuration. + * \param update Zone update. + * + * \retval KNOT_EEXTERNAL External validation failed. + * \retval KNOT_EOK External validation succeeded. + * \return KNOT_E* + */ +int zone_update_external(conf_t *conf, zone_update_t *update); + /*! * \brief Commits all changes to the zone, signs it, saves changes to journal. * diff --git a/src/knot/zone/zone.c b/src/knot/zone/zone.c index f16ed6c546..2484f0352d 100644 --- a/src/knot/zone/zone.c +++ b/src/knot/zone/zone.c @@ -200,6 +200,16 @@ void zone_control_clear(zone_t *zone) zone->control_update = NULL; } +void zone_shutdown(zone_t *zone) +{ + pthread_mutex_lock(&zone->cu_lock); + if (zone->control_update != NULL && (zone->control_update->flags & UPDATE_WFEV)) { + knot_sem_post(&zone->control_update->external); + } + zone_set_flag(zone, ZONE_SHUT_DOWN); + pthread_mutex_unlock(&zone->cu_lock); +} + void zone_free(zone_t **zone_ptr) { if (zone_ptr == NULL || *zone_ptr == NULL) { @@ -226,6 +236,7 @@ void zone_free(zone_t **zone_ptr) knot_sem_destroy(&zone->cow_lock); /* Control update. */ + assert(zone->control_update == NULL || !(zone->control_update->flags & UPDATE_WFEV)); zone_control_clear(zone); free(zone->catalog_gen); diff --git a/src/knot/zone/zone.h b/src/knot/zone/zone.h index 5f7a156b32..22b6f5b04b 100644 --- a/src/knot/zone/zone.h +++ b/src/knot/zone/zone.h @@ -39,6 +39,7 @@ typedef enum { ZONE_XFR_FROZEN = 1 << 7, /*!< Outgoing AXFR/IXFR temporarily disabled. */ ZONE_USER_FLUSH = 1 << 8, /*!< User-triggered flush. */ ZONE_LAST_SIGN_OK = 1 << 9, /*!< Last full-sign event finished OK. */ + ZONE_SHUT_DOWN = 1 << 10, /*!< Zone events are shutting down. */ } zone_flag_t; /*! @@ -148,6 +149,13 @@ typedef struct zone */ zone_t* zone_new(const knot_dname_t *name); +/*! + * \brief Declare that zone is shutting down. + * + * \param zone Zone to be shut down. + */ +void zone_shutdown(zone_t *zone); + /*! * \brief Deallocates the zone structure. * diff --git a/src/knot/zone/zonedb-load.c b/src/knot/zone/zonedb-load.c index f9fa0b2188..8b5095aa5c 100644 --- a/src/knot/zone/zonedb-load.c +++ b/src/knot/zone/zonedb-load.c @@ -533,6 +533,8 @@ static knot_zonedb_t *create_zonedb(conf_t *conf, server_t *server, reload_t mod it = knot_zonedb_iter_begin(db_new); while (!knot_zonedb_iter_finished(it)) { zone_t *z = knot_zonedb_iter_val(it); + zone_unset_flag(z, ZONE_SHUT_DOWN); + conf_val_t val = conf_zone_get(conf, C_REVERSE_GEN, z->name); while (val.code == KNOT_EOK) { const knot_dname_t *forw_name = conf_dname(&val); diff --git a/src/libknot/errcode.h b/src/libknot/errcode.h index 6c789f3605..b05f703ac2 100644 --- a/src/libknot/errcode.h +++ b/src/libknot/errcode.h @@ -100,6 +100,7 @@ enum knot_error { KNOT_EBACKUPDATA, KNOT_ECPUCOMPAT, KNOT_EMODINVAL, + KNOT_EEXTERNAL, KNOT_GENERAL_ERROR = -900, diff --git a/src/libknot/error.c b/src/libknot/error.c index 21f34a246c..c82a5bbc37 100644 --- a/src/libknot/error.c +++ b/src/libknot/error.c @@ -99,6 +99,7 @@ static const struct error errors[] = { { KNOT_EBACKUPDATA, "requested data not in backup" }, { KNOT_ECPUCOMPAT, "incompatible CPU architecture" }, { KNOT_EMODINVAL, "invalid module" }, + { KNOT_EEXTERNAL, "external validation failed" }, { KNOT_GENERAL_ERROR, "unknown general error" }, diff --git a/tests-extra/tests/zone/external_vldt/test.py b/tests-extra/tests/zone/external_vldt/test.py new file mode 100644 index 0000000000..1a90f84ae0 --- /dev/null +++ b/tests-extra/tests/zone/external_vldt/test.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 + +""" +Test of external zone validation. +""" + +from dnstest.utils import * +from dnstest.test import Test +import random + +t = Test() + +master = t.server("knot") +slave = t.server("knot") +zone = t.zone_rnd(1, records=40) +t.link(zone, master, slave) + +def log_count_expect(server, pattern, expct): + fnd = server.log_search_count(pattern) + if fnd != expct: + detail_log("LOG SEARCH COUNT '%s' found %d expected %d" % (pattern, fnd, expct)) + set_err("LOG SEARCH COUNT %d != %d" % (fnd, expct)) + +ZONE = zone[0].name +LOG = "for external validation" + +slave.async_start = True +slave.zones[ZONE].external = True # TODO this will be a list or dict once 'external' secation has any fields + +master.dnssec(zone[0]).enable = random.choice([False, True]) + +t.start() +serial = master.zone_wait(zone) + +t.sleep(2) +log_count_expect(slave, LOG, 1) +resp = slave.dig(ZONE, "SOA") +resp.check(rcode="SERVFAIL") +resp.check_count(0, "SOA") + +slave.ctl("zone-commit " + ZONE) +t.sleep(2) +resp = slave.dig(ZONE, "SOA") +resp.check_soa_serial(serial) + +master.random_ddns(zone, allow_empty=False) +serial = master.zone_wait(zone, serial) + +t.sleep(2) +log_count_expect(slave, LOG, 2) +slave.ctl("zone-abort " + ZONE) +t.sleep(2) +resp = slave.dig(ZONE, "SOA") +resp.check_soa_serial(serial - 1) + +master.random_ddns(zone, allow_empty=False) +serial = master.zone_wait(zone, serial) + +t.sleep(2) +log_count_expect(slave, LOG, 3) +slave.ctl("zone-commit " + ZONE) +t.sleep(2) +resp = slave.dig(ZONE, "SOA") +resp.check_soa_serial(serial) + +slave.ctl("zone-freeze " + ZONE) +master.random_ddns(zone, allow_empty=False) +serial = master.zone_wait(zone, serial) + +slave.zonemd_generate = "zonemd-sha512" +slave.gen_confile() +slave.ctl("zone-thaw " + ZONE) +t.sleep(1) +slave.reload() + +master.random_ddns(zone, allow_empty=False) +serial = master.zone_wait(zone, serial) + +t.sleep(2) +log_count_expect(slave, LOG, 5) +slave.ctl("zone-commit " + ZONE) +t.sleep(2) +resp = slave.dig(ZONE, "SOA") +resp.check_soa_serial(serial) + +master.random_ddns(zone, allow_empty=False) +serial = master.zone_wait(zone, serial) + +t.sleep(2) +log_count_expect(slave, LOG, 6) +slave.stop() +t.sleep(2) +log_count_expect(slave, "shutting down", 1) + +t.end() diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py index 7f778e0acb..0c43f6ddda 100644 --- a/tests-extra/tools/dnstest/server.py +++ b/tests-extra/tools/dnstest/server.py @@ -102,6 +102,7 @@ class Zone(object): self.journal_content = journal_content # journal contents self.modules = [] self.reverse_from = None + self.external = None self.dnssec = ZoneDnssec() self.catalog_role = ZoneCatalogRole.NONE self.catalog_gen_name = None # Generated catalog name for this member @@ -216,6 +217,7 @@ class Server(object): self.zone_size_limit = None self.serial_policy = None self.auto_acl = None + self.async_start = None self.provide_ixfr = None self.master_pin_tol = None self.quic_log = None @@ -1543,6 +1545,7 @@ class Knot(Server): self._str(s, "remote-pool-limit", str(random.randint(0,6))) self._str(s, "remote-retry-delay", str(random.choice([0, 1, 5]))) self._bool(s, "automatic-acl", self.auto_acl) + self._bool(s, "async-start", self.async_start) if self.cert_key_file: s.item_str("key-file", self.cert_key_file[0]) s.item_str("cert-file", self.cert_key_file[1]) @@ -1787,6 +1790,18 @@ class Knot(Server): if have_dnskeysync: s.end() + have_external = False + for zone in sorted(self.zones): + z = self.zones[zone] + if not z.external: + continue + if not have_external: + s.begin("external") + have_external = True + s.id_item("id", z.name) + if have_external: + s.end() + have_keystore = False for zone in sorted(self.zones): z = self.zones[zone] @@ -1958,6 +1973,9 @@ class Knot(Server): self._str(s, "expire-min-interval", z.expire_min) self._str(s, "expire-max-interval", z.expire_max) + if z.external: + s.item("external-validation", z.name) + if self.zonefile_load is not None: s.item_str("zonefile-load", self.zonefile_load) elif z.ixfr: