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
.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
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
*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
}
}
+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)
{
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.
*
{ 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 } }, \
#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"
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;
}
}
+ 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,
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);
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)
--- /dev/null
+#!/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()
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
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)
-/* 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
{ 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 },