]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
zonemd: ignore ZONEMD removal within IXFR if zonemd-generate configured...
authorLibor Peltan <libor.peltan@nic.cz>
Fri, 24 Oct 2025 09:40:04 +0000 (11:40 +0200)
committerLibor Peltan <libor.peltan@nic.cz>
Wed, 29 Oct 2025 13:01:13 +0000 (14:01 +0100)
...because it is overwritten anyway, causing semantic errors when applying incoming changeset

src/knot/events/handlers/refresh.c
tests-extra/tests/zone/zonemd_chain/test.py [new file with mode: 0644]

index 04e2c4634b29e9b62c889de9213e185b31a5d225..583819f7075378ea03bb4e9c6afc1de7f84717ce 100644 (file)
@@ -108,6 +108,7 @@ struct refresh_data {
        bool ixfr_by_one;                 //!< Allow only single changeset within IXFR.
        bool ixfr_from_axfr;              //!< Diff computation of incremental update from AXFR allowed.
        bool reverse_or_include;          //!< Auto reverse generation or subzone inclusion configured.
+       bool ignore_zonemd;               //!< Ignore apex ZONEMD in incomming IXFR as it is overwritten anyway.
        uint32_t expire_timer;            //!< Result: expire timer from answer EDNS.
 
        // internal state, initialize with zeroes:
@@ -841,6 +842,10 @@ static int ixfr_step(const knot_rrset_t *rr, struct refresh_data *data)
        case IXFR_SOA_DEL:
                return ixfr_solve_soa_del(rr, data);
        case IXFR_DEL:
+               if (data->ignore_zonemd && rr->type == KNOT_RRTYPE_ZONEMD &&
+                   knot_dname_is_equal(rr->owner, data->zone->name)) {
+                       return KNOT_EOK;
+               }
                return ixfr_solve_del(rr, change, data->mm);
        case IXFR_SOA_ADD:
                return ixfr_solve_soa_add(rr, change, data->mm);
@@ -1355,6 +1360,7 @@ typedef struct {
        bool ixfr_from_axfr;
        bool reverse_or_include;
        bool more_xfr;
+       bool ignore_zonemd;
 } try_refresh_ctx_t;
 
 static int try_refresh(conf_t *conf, zone_t *zone, const conf_remote_t *master,
@@ -1393,6 +1399,8 @@ static int try_refresh(conf_t *conf, zone_t *zone, const conf_remote_t *master,
                .ixfr_by_one = trctx->ixfr_by_one,
                .ixfr_from_axfr = trctx->ixfr_from_axfr,
                .reverse_or_include = trctx->reverse_or_include,
+               .ignore_zonemd = trctx->ignore_zonemd,
+               // TODO refactor refresh_data and try_refresh_ctx_t so that this cross-assignment is not necessary for simple fields
        };
 
        knot_requestor_t requestor;
@@ -1466,6 +1474,8 @@ int event_refresh(conf_t *conf, zone_t *zone)
 
        conf_val_t val = conf_zone_get(conf, C_IXFR_BY_ONE, zone->name);
        trctx.ixfr_by_one = conf_bool(&val);
+       val = conf_zone_get(conf, C_ZONEMD_GENERATE, zone->name);
+       trctx.ignore_zonemd = (conf_opt(&val) != ZONE_DIGEST_NONE);
        val = conf_zone_get(conf, C_IXFR_FROM_AXFR, zone->name);
        trctx.ixfr_from_axfr = conf_bool(&val);
 
diff --git a/tests-extra/tests/zone/zonemd_chain/test.py b/tests-extra/tests/zone/zonemd_chain/test.py
new file mode 100644 (file)
index 0000000..981a452
--- /dev/null
@@ -0,0 +1,51 @@
+#!/usr/bin/env python3
+
+"""
+Test of bump-in-the-wire signer receiving ZONEMD in unsigned version of the zone.
+"""
+
+from dnstest.utils import *
+from dnstest.test import Test
+import random
+import threading
+import time
+
+t = Test()
+
+master = t.server("knot")
+signer = t.server("knot")
+slave = t.server("knot")
+ZONE = "example."
+zones = t.zone(ZONE)
+
+t.link(zones, master, signer)
+t.link(zones, signer, slave)
+
+master.conf_zone(zones).zonemd_generate = "zonemd-sha384"
+signer.conf_zone(zones).zonemd_verify = True
+
+signer.dnssec(zones).enable = True
+signer.conf_zone(zones).zonemd_generate = "zonemd-sha384"
+
+slave.conf_zone(zones).dnssec_validation = True
+slave.conf_zone(zones).zonemd_verify = True
+
+t.start()
+serials = slave.zones_wait(zones)
+
+master.random_ddns(zones, allow_empty=False)
+t.sleep(4)
+slave.zones_wait(zones, serials, equal=True, greater=False)
+signer.ctl("zone-retransfer")
+serials = slave.zones_wait(zones, serials)
+
+signer.conf_zone(zones).zonemd_verify = False
+signer.gen_confile()
+signer.reload()
+
+master.random_ddns(zones, allow_empty=False)
+serials = slave.zones_wait(zones, serials)
+if signer.log_search_count("fallback to AXFR ") > 0: # NOTE without the trailing space the message can appear for outgoing IXFR as well, which it actually should
+    set_err("AXFR fallback")
+
+t.end()