knot: implemented serial-modulo

This commit is contained in:
Libor Peltan 2023-07-20 12:43:43 +02:00 committed by Daniel Salzman
parent 36fff51b20
commit 0c475eae4a
11 changed files with 183 additions and 5 deletions

View 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

View 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

View 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)
{

View 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.
*

View 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 } }, \

View file

@ -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"

View 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,

View 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)

View file

@ -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()

View 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)

View 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 },