]> git.ipfire.org Git - thirdparty/knot-dns.git/commitdiff
knot: implemented serial-modulo
authorLibor Peltan <libor.peltan@nic.cz>
Thu, 20 Jul 2023 10:43:43 +0000 (12:43 +0200)
committerDaniel Salzman <daniel.salzman@nic.cz>
Tue, 25 Jul 2023 15:58:51 +0000 (17:58 +0200)
doc/man/knot.conf.5in
doc/reference.rst
src/knot/conf/conf.c
src/knot/conf/conf.h
src/knot/conf/schema.c
src/knot/conf/schema.h
src/knot/conf/tools.c
src/knot/zone/serial.c
tests-extra/tests/zone/serial_modulo/test.py [new file with mode: 0644]
tests-extra/tools/dnstest/server.py
tests/knot/test_confio.c

index ba2136951af8baf963c781e4eacc7a775d4d300f..35052ecbe03db1dfe5b8887617d9775005879f1a 100644 (file)
@@ -2106,6 +2106,7 @@ zone:
     zonemd\-verify: BOOL
     zonemd\-generate: none | zonemd\-sha384 | zonemd\-sha512 | remove
     serial\-policy: increment | unixtime | dateserial
+    serial\-modulo: INT/INT
     reverse\-generate: DNAME
     refresh\-min\-interval: TIME
     refresh\-max\-interval: TIME
@@ -2512,6 +2513,29 @@ Generated catalog zones use \fBunixtime\fP only.
 .UNINDENT
 .sp
 \fIDefault:\fP \fBincrement\fP (\fBunixtime\fP for generated catalog zones)
+.SS serial\-modulo
+.sp
+Specifies that the zone serials shall be congruent by specified modulo.
+The option value must be a string in the format \fBR/M\fP, where \fBR < M <= 256\fP are
+positive integers. Whenever the zone serial is incremented, it is ensured
+that \fBserial % M == R\fP\&. This can be useful in the case of multiple inconsistent
+primaries, where distinct zone serial sequences prevent cross\-master\-IXFR
+by any secondary.
+.sp
+\fBNOTE:\fP
+.INDENT 0.0
+.INDENT 3.5
+In order to ensure the congruent policy, this option is only allowed
+with \fI\%DNSSEC signing enabled\fP and
+\fI\%zonefile\-load\fP to be either \fBdifference\-no\-serial\fP or \fBnone\fP\&.
+.sp
+Because the zone serial effectively always increments by \fBM\fP instead of
+\fB1\fP, it is not recommended to use \fBdateserial\fP \fI\%serial\-policy\fP
+or even \fBunixtime\fP in case of rapidly updated zone.
+.UNINDENT
+.UNINDENT
+.sp
+\fIDefault:\fP \fB0/1\fP
 .SS reverse\-generate
 .sp
 This option triggers the automatic generation of reverse PTR records based on
index fa63362c8d5fc56effb5ef6ab1be565143f5e04a..d41693eee6259b92f20c9fdcd69837a3befb1ab8 100644 (file)
@@ -2298,6 +2298,7 @@ Definition of zones served by the server.
      zonemd-verify: BOOL
      zonemd-generate: none | zonemd-sha384 | zonemd-sha512 | remove
      serial-policy: increment | unixtime | dateserial
+     serial-modulo: INT/INT
      reverse-generate: DNAME
      refresh-min-interval: TIME
      refresh-max-interval: TIME
@@ -2727,6 +2728,29 @@ Possible values:
 
 *Default:* ``increment`` (``unixtime`` for generated catalog zones)
 
+.. _zone_serial-modulo:
+
+serial-modulo
+-------------
+
+Specifies that the zone serials shall be congruent by specified modulo.
+The option value must be a string in the format ``R/M``, where ``R < M <= 256`` are
+positive integers. Whenever the zone serial is incremented, it is ensured
+that ``serial % M == R``. This can be useful in the case of multiple inconsistent
+primaries, where distinct zone serial sequences prevent cross-master-IXFR
+by any secondary.
+
+.. NOTE::
+   In order to ensure the congruent policy, this option is only allowed
+   with :ref:`DNSSEC signing enabled<zone_dnssec-signing>` and
+   :ref:`zone_zonefile-load` to be either ``difference-no-serial`` or ``none``.
+
+   Because the zone serial effectively always increments by ``M`` instead of
+   ``1``, it is not recommended to use ``dateserial`` :ref:`zone_serial-policy`
+   or even ``unixtime`` in case of rapidly updated zone.
+
+*Default:* ``0/1``
+
 .. _zone_reverse-generate:
 
 reverse-generate
index 7fd80832dcf7fd6228b2c77c09536f46aa84a1ed..ac359d80e618ce7be73bcae1fb8d3e39f05b49ce 100644 (file)
@@ -666,6 +666,16 @@ const char* conf_str(
        }
 }
 
+int conf_tuple(
+       conf_val_t *val,
+       uint32_t *a,
+       uint32_t *b)
+{
+       const char *str = conf_str(val);
+       int res = sscanf(str, "%"SCNu32"/%"SCNu32, a, b);
+       return res == 2 ? KNOT_EOK : KNOT_EMALF;
+}
+
 const knot_dname_t* conf_dname(
        conf_val_t *val)
 {
index 99af9bd303b9223cde0a4521c977fa736c42210c..707d8b564099839fca6bd9c575b0837f416f9350 100644 (file)
@@ -495,6 +495,21 @@ const char* conf_str(
        conf_val_t *val
 );
 
+/*!
+ * Gets the tuple value of a string item in format "##/##".
+ *
+ * \param[in]    val  Item value.
+ * \param[out]   a    First numeric value.
+ * \param[out]   b    Second numeric value.
+ *
+ * \return KNOT_EOK if OK, KNOT_EMALF if the tuple format not recognized.
+ */
+int conf_tuple(
+       conf_val_t *val,
+       uint32_t *a,
+       uint32_t *b
+);
+
 /*!
  * Gets the dname value of the item.
  *
index 05f8e135d1de0657a45845597b1d9145eee8ffac..4053e58029db92126dbc580300842eab6e549fca 100644 (file)
@@ -469,6 +469,7 @@ static const yp_item_t desc_policy[] = {
                                           { check_ref } }, \
        { C_REVERSE_GEN,         YP_TDNAME,YP_VNONE, FLAGS | CONF_IO_FRLD_ZONES }, \
        { C_SERIAL_POLICY,       YP_TOPT,  YP_VOPT = { serial_policies, SERIAL_POLICY_INCREMENT } }, \
+       { C_SERIAL_MODULO,       YP_TSTR,  YP_VSTR = { "0/1" } }, \
        { C_ZONEMD_GENERATE,     YP_TOPT,  YP_VOPT = { zone_digest, ZONE_DIGEST_NONE }, FLAGS }, \
        { C_ZONEMD_VERIFY,       YP_TBOOL, YP_VNONE, FLAGS }, \
        { C_REFRESH_MIN_INTERVAL,YP_TINT,  YP_VINT = { 2, UINT32_MAX, 2, YP_STIME } }, \
index 273c9b6ea20de4cbe1bec9930ab56718033e4652..e7b057df6298f6cc0cb16d5090ba94f905bc46a8 100644 (file)
 #define C_SBM                  "\x0A""submission"
 #define C_SECRET               "\x06""secret"
 #define C_SEM_CHECKS           "\x0F""semantic-checks"
+#define C_SERIAL_MODULO                "\x0D""serial-modulo"
 #define C_SERIAL_POLICY                "\x0D""serial-policy"
 #define C_SERVER               "\x06""server"
 #define C_SIGNING_THREADS      "\x0F""signing-threads"
index 691475f74912753120f017daf7bbe003d9c70387..50e0e6d5de7ac6fc884fef0bfcfc2504b21db72f 100644 (file)
@@ -940,7 +940,8 @@ int check_zone(
                                               C_ZONEFILE_LOAD, yp_dname(args->id));
        conf_val_t journal = conf_zone_get_txn(args->extra->conf, args->extra->txn,
                                               C_JOURNAL_CONTENT, yp_dname(args->id));
-       if (conf_opt(&zf_load) == ZONEFILE_LOAD_DIFSE) {
+       int zf_load_val = conf_opt(&zf_load);
+       if (zf_load_val == ZONEFILE_LOAD_DIFSE) {
                if (conf_opt(&journal) != JOURNAL_CONTENT_ALL) {
                        args->err_str = "'zonefile-load: difference-no-serial' requires 'journal-content: all'";
                        return KNOT_EINVAL;
@@ -965,6 +966,24 @@ int check_zone(
                }
        }
 
+       conf_val_t serial_modulo = conf_zone_get_txn(args->extra->conf, args->extra->txn,
+                                                    C_SERIAL_MODULO, yp_dname(args->id));
+       uint32_t a, b;
+       int ret = conf_tuple(&serial_modulo, &a, &b);
+       if (ret != KNOT_EOK || a >= b) {
+               args->err_str = "invalid format of 'serial-modulo'";
+               return KNOT_EINVAL;
+       } else if (b > 1) {
+               if (!conf_bool(&signing)) {
+                       args->err_str = "'serial-modulo' is only possible with `dnssec-signing`";
+                       return KNOT_EINVAL;
+               } else if (zf_load_val != ZONEFILE_LOAD_DIFSE && zf_load_val != ZONEFILE_LOAD_NONE) {
+                       args->err_str = "'serial-modulo' requires 'zonefile-load' either 'none'"
+                                       " or 'difference-no-serial'";
+                       return KNOT_EINVAL;
+               }
+       }
+
        conf_val_t catalog_role = conf_zone_get_txn(args->extra->conf, args->extra->txn,
                                                    C_CATALOG_ROLE, yp_dname(args->id));
        conf_val_t catalog_tpl = conf_zone_get_txn(args->extra->conf, args->extra->txn,
index 97bc93bf9d5bd6c2e3a6c1cbfa640967b8567b19..a58cbc53e373e95880b9d877857ec6a32ec4c003 100644 (file)
@@ -50,7 +50,7 @@ static uint32_t serial_dateserial(uint32_t current)
 uint32_t serial_next(uint32_t current, conf_t *conf, const knot_dname_t *zone,
                      unsigned policy, uint32_t must_increment)
 {
-       uint32_t minimum;
+       uint32_t minimum, result;
 
        if (policy == SERIAL_POLICY_AUTO) {
                assert(conf);
@@ -74,10 +74,25 @@ uint32_t serial_next(uint32_t current, conf_t *conf, const knot_dname_t *zone,
                return 0;
        }
        if (serial_compare(minimum, current) != SERIAL_GREATER) {
-               return current + must_increment;
+               result = current + must_increment;
        } else {
-               return minimum;
+               result = minimum;
        }
+
+       if (conf != NULL) { // NULL conf SHOULD be only allowed in tests
+               conf_val_t val = conf_zone_get(conf, C_SERIAL_MODULO, zone);
+               uint32_t a, b;
+               int ret = conf_tuple(&val, &a, &b);
+               assert(ret == KNOT_EOK && a < b); // ensured by conf/tools.c
+               if (b > 1) {
+                       uint32_t incr = ((a + b) - (result % b)) % b;
+                       assert(incr == 0 || result % b != a);
+                       result += incr;
+                       assert(result % b == a);
+               }
+       }
+
+       return result;
 }
 
 serial_cmp_result_t kserial_cmp(kserial_t a, kserial_t b)
diff --git a/tests-extra/tests/zone/serial_modulo/test.py b/tests-extra/tests/zone/serial_modulo/test.py
new file mode 100644 (file)
index 0000000..299bdef
--- /dev/null
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+
+"""
+Test of serial modulo.
+"""
+
+import random
+from dnstest.utils import *
+from dnstest.test import Test
+
+SCENARIO = random.choice(["xfr-ddns", "ddns", "reload", "restart", "ddns-restart"])
+MODULO_A = 3
+MODULO_B = 7
+
+def check_serial(serial, modulo_a, msg):
+    if serial % MODULO_B != modulo_a:
+        set_err("WRONG MODULO " + msg)
+        detail_log("%s: serial %d modulo %d expected %d found %d" % \
+                   (msg, serial, MODULO_B, modulo_a, serial % MODULO_B))
+
+def check_serials(serials, modulo_a, msg):
+    for z in serials:
+        check_serial(serials[z], modulo_a, msg)
+
+t = Test()
+
+master = t.server("knot")
+knot = t.server("knot")
+zones = t.zone("example.com.")
+if "xfr" in SCENARIO:
+    source = master
+    t.link(zones, master, knot)
+else:
+    source = knot
+    t.link(zones, knot)
+
+for z in zones:
+    knot.dnssec(z).enable = True
+    knot.zones[z.name].serial_modulo = "%d/%d" % (MODULO_A, MODULO_B)
+
+    knot.zonefile_load = "difference-no-serial"
+    knot.zones[z.name].journal_content = "all"
+
+detail_log("SCENARIO " + SCENARIO)
+t.start()
+
+serials = knot.zones_wait(zones)
+check_serials(serials, MODULO_A, "INIT")
+
+if "ddns" in SCENARIO:
+    for z in zones:
+        source.random_ddns(z, allow_empty=False)
+else:
+    for z in zones:
+        source.zones[z.name].zfile.update_rnd()
+
+if "start" in SCENARIO:
+    source.stop()
+    source.start()
+elif "reload" in SCENARIO:
+    source.ctl("zone-reload")
+
+serials = knot.zones_wait(zones, serials)
+check_serials(serials, MODULO_A, "DDNS")
+
+t.end()
index 5cce5cbbede63b5fcaedcfc1f9f8736d240af6b9..16b5698e90fcd25b405815cc7c09514dcefb6060 100644 (file)
@@ -92,6 +92,7 @@ class Zone(object):
         self.zfile = zone_file
         self.masters = set()
         self.slaves = set()
+        self.serial_modulo = None
         self.ddns = ddns
         self.ixfr = ixfr
         self.journal_content = journal_content # journal contents
@@ -1616,6 +1617,7 @@ class Knot(Server):
             self.config_xfr(z, s)
 
             self._str(s, "serial-policy", self.serial_policy)
+            self._str(s, "serial-modulo", z.serial_modulo)
             self._str(s, "ddns-master", self.ddns_master)
 
             s.item_str("journal-content", z.journal_content)
index f5d3c949fa66f173b62321ee7eedcafda6e1e895..44bc7c597c00e0f8c706cd16d6c890dc632af994 100644 (file)
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  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
@@ -1034,6 +1034,7 @@ static const knot_lookup_t opts[] = {
        { C_JOURNAL_CONTENT,   YP_TOPT,  YP_VOPT = { opts, 0 } }, \
        { C_DNSSEC_SIGNING,    YP_TBOOL, YP_VNONE }, \
        { C_DNSSEC_VALIDATION, YP_TBOOL, YP_VNONE }, \
+       { C_SERIAL_MODULO,     YP_TSTR,  YP_VSTR = { "0/1" } }, \
        { C_CATALOG_ROLE,      YP_TOPT,  YP_VOPT = { opts, 0 } }, \
        { C_CATALOG_TPL,       YP_TREF,  YP_VREF = { C_RMT } }, \
        { C_COMMENT,           YP_TSTR,  YP_VNONE },