From 0c475eae4a9527673f7bef43bb65cd1177f2f082 Mon Sep 17 00:00:00 2001 From: Libor Peltan Date: Thu, 20 Jul 2023 12:43:43 +0200 Subject: [PATCH] knot: implemented serial-modulo --- doc/man/knot.conf.5in | 24 +++++++ doc/reference.rst | 24 +++++++ src/knot/conf/conf.c | 10 +++ src/knot/conf/conf.h | 15 +++++ src/knot/conf/schema.c | 1 + src/knot/conf/schema.h | 1 + src/knot/conf/tools.c | 21 ++++++- src/knot/zone/serial.c | 21 ++++++- tests-extra/tests/zone/serial_modulo/test.py | 66 ++++++++++++++++++++ tests-extra/tools/dnstest/server.py | 2 + tests/knot/test_confio.c | 3 +- 11 files changed, 183 insertions(+), 5 deletions(-) create mode 100644 tests-extra/tests/zone/serial_modulo/test.py diff --git a/doc/man/knot.conf.5in b/doc/man/knot.conf.5in index ba2136951..35052ecbe 100644 --- a/doc/man/knot.conf.5in +++ b/doc/man/knot.conf.5in @@ -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 diff --git a/doc/reference.rst b/doc/reference.rst index fa63362c8..d41693eee 100644 --- a/doc/reference.rst +++ b/doc/reference.rst @@ -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` 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 diff --git a/src/knot/conf/conf.c b/src/knot/conf/conf.c index 7fd80832d..ac359d80e 100644 --- a/src/knot/conf/conf.c +++ b/src/knot/conf/conf.c @@ -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) { diff --git a/src/knot/conf/conf.h b/src/knot/conf/conf.h index 99af9bd30..707d8b564 100644 --- a/src/knot/conf/conf.h +++ b/src/knot/conf/conf.h @@ -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. * diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c index 05f8e135d..4053e5802 100644 --- a/src/knot/conf/schema.c +++ b/src/knot/conf/schema.c @@ -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 } }, \ diff --git a/src/knot/conf/schema.h b/src/knot/conf/schema.h index 273c9b6ea..e7b057df6 100644 --- a/src/knot/conf/schema.h +++ b/src/knot/conf/schema.h @@ -132,6 +132,7 @@ #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" diff --git a/src/knot/conf/tools.c b/src/knot/conf/tools.c index 691475f74..50e0e6d5d 100644 --- a/src/knot/conf/tools.c +++ b/src/knot/conf/tools.c @@ -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, diff --git a/src/knot/zone/serial.c b/src/knot/zone/serial.c index 97bc93bf9..a58cbc53e 100644 --- a/src/knot/zone/serial.c +++ b/src/knot/zone/serial.c @@ -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 index 000000000..299bdef25 --- /dev/null +++ b/tests-extra/tests/zone/serial_modulo/test.py @@ -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() diff --git a/tests-extra/tools/dnstest/server.py b/tests-extra/tools/dnstest/server.py index 5cce5cbbe..16b5698e9 100644 --- a/tests-extra/tools/dnstest/server.py +++ b/tests-extra/tools/dnstest/server.py @@ -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) diff --git a/tests/knot/test_confio.c b/tests/knot/test_confio.c index f5d3c949f..44bc7c597 100644 --- a/tests/knot/test_confio.c +++ b/tests/knot/test_confio.c @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 CZ.NIC, z.s.p.o. +/* Copyright (C) 2023 CZ.NIC, z.s.p.o. 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 },