]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
external validation: implemented basic functionality
authorLibor Peltan <libor.peltan@nic.cz>
Tue, 10 Jun 2025 12:42:55 +0000 (14:42 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Thu, 31 Jul 2025 14:42:14 +0000 (16:42 +0200)
17 files changed:
src/knot/conf/schema.c
src/knot/conf/schema.h
src/knot/conf/tools.c
src/knot/conf/tools.h
src/knot/ctl/commands.c
src/knot/events/handlers/expire.c
src/knot/events/handlers/refresh.c
src/knot/server/server.c
src/knot/updates/zone-update.c
src/knot/updates/zone-update.h
src/knot/zone/zone.c
src/knot/zone/zone.h
src/knot/zone/zonedb-load.c
src/libknot/errcode.h
src/libknot/error.c
tests-extra/tests/zone/external_vldt/test.py [new file with mode: 0644]
tests-extra/tools/dnstest/server.py

index 0c4e8e954daa6718ac56373424a4368ac82d0057..72644dbfb05f4e186301f1f7d7c1c13bd17e72ae 100644 (file)
@@ -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 } },
index 9ba63f542d827032e4e75f990aabc34af8c211fe..536b7835868437906aec8b2b9eef11dafc284f7c 100644 (file)
@@ -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"
index 7e0b079403ab3f81fe210bdc617808771f52bdcb..bbffbc9a14bdf1fc65cb8da40687c192b586b29e 100644 (file)
@@ -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)
 {
index 81537d6ef9799b3847e534d810ee76c3d3dbe08f..10a5cb4fd587b6e3627efb005684d69bcddca659 100644 (file)
@@ -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
 );
index 73505ab7f4c2ea7c1dcd56519955e9f2c0e2c18b..25311db9674f8f244e9f8712e6077d6dc8f8e516 100644 (file)
@@ -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;
        }
 }
index bd74fb8adacba5be14e96d096b358c2f60cda3e7..c49cd8f9c09a33755a314ec875dc935640381825 100644 (file)
@@ -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);
 
index e633cffd476e88389eb94d0b6b9754c01f798f6b..44de0b70faec7021e8434d017fb4f7f81d4a7697 100644 (file)
@@ -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;
index b390cbdc2a79f9195ede523439e4617d3d6d57e4..ceb203958f8812926c1c1e2231418dd74666fb0f 100644 (file)
@@ -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");
 
index b936643ee8a3381fd37b38de53715e81c3bf00c9..e020e433738e1b33058dc0b377c691f392da760d 100644 (file)
@@ -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));
index f4f6ca94efae560f8d9eaaaa435e2ad276d2c57a..3229eed62519a3bbc6f28244c1e1e5db09aec212 100644 (file)
@@ -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.
  *
index f16ed6c5467fb0657bb559974a10bdbeabba449a..2484f0352d2e0386127e8a2ded94afac1db48c33 100644 (file)
@@ -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);
index 5f7a156b32dd24bbeeee2a964a5e05b53af4b245..22b6f5b04b8f46219277b246dc2e508c5a56ce17 100644 (file)
@@ -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.
  *
index f9fa0b2188cc90ada82c6ae438f1b5c93b653a1b..8b5095aa5c5996131c599dd8c9f53e559f506bdb 100644 (file)
@@ -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);
index 6c789f3605eace597fb52d604f7fa75a970e1e4f..b05f703ac25ce543345c2087f820746c7f33dbd3 100644 (file)
@@ -100,6 +100,7 @@ enum knot_error {
        KNOT_EBACKUPDATA,
        KNOT_ECPUCOMPAT,
        KNOT_EMODINVAL,
+       KNOT_EEXTERNAL,
 
        KNOT_GENERAL_ERROR = -900,
 
index 21f34a246cce2b643ff2e3125b216b5a0d7ed305..c82a5bbc37572a9eaa5fdd330c646570dd3f562a 100644 (file)
@@ -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 (file)
index 0000000..1a90f84
--- /dev/null
@@ -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()
index 7f778e0acbc4db94b3368c9fbb35867125b849a3..0c43f6ddda2046c4d8262189a28ee643c45349bf 100644 (file)
@@ -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: