external validation: implemented basic functionality

This commit is contained in:
Libor Peltan 2025-06-10 14:42:55 +02:00 committed by Daniel Salzman
parent 6264449184
commit 8ccc39d8dc
17 changed files with 243 additions and 1 deletions

View file

@ -454,6 +454,11 @@ static const yp_item_t desc_policy[] = {
{ NULL }
};
static const yp_item_t desc_external[] = {
{ C_ID, YP_TSTR, YP_VNONE, CONF_IO_FREF },
{ NULL }
};
#define ZONE_ITEMS(FLAGS) \
{ C_STORAGE, YP_TSTR, YP_VSTR = { STORAGE_DIR }, FLAGS }, \
{ C_FILE, YP_TSTR, YP_VNONE, FLAGS }, \
@ -479,6 +484,7 @@ static const yp_item_t desc_policy[] = {
{ C_IXFR_FROM_AXFR, YP_TBOOL, YP_VNONE }, \
{ C_ZONE_MAX_SIZE, YP_TINT, YP_VINT = { 0, SSIZE_MAX, SSIZE_MAX, YP_SSIZE }, FLAGS }, \
{ C_ADJUST_THR, YP_TINT, YP_VINT = { 1, UINT16_MAX, 1 } }, \
{ C_EXTERNAL_VLDT, YP_TREF, YP_VREF = { C_EXTERNAL }, FLAGS, { check_ref_dflt } }, \
{ C_DNSSEC_SIGNING, YP_TBOOL, YP_VNONE, FLAGS }, \
{ C_DNSSEC_VALIDATION, YP_TBOOL, YP_VNONE, FLAGS }, \
{ C_DNSSEC_POLICY, YP_TREF, YP_VREF = { C_POLICY }, FLAGS, { check_ref_dflt } }, \
@ -535,6 +541,7 @@ const yp_item_t conf_schema[] = {
{ C_SBM, YP_TGRP, YP_VGRP = { desc_submission }, YP_FMULTI },
{ C_DNSKEY_SYNC, YP_TGRP, YP_VGRP = { desc_dnskey_sync }, YP_FMULTI, { check_dnskey_sync } },
{ C_POLICY, YP_TGRP, YP_VGRP = { desc_policy }, YP_FMULTI, { check_policy } },
{ C_EXTERNAL, YP_TGRP, YP_VGRP = { desc_external }, YP_FMULTI, { check_external } },
{ C_TPL, YP_TGRP, YP_VGRP = { desc_template }, YP_FMULTI, { check_template } },
{ C_ZONE, YP_TGRP, YP_VGRP = { desc_zone }, YP_FMULTI | CONF_IO_FZONE, { check_zone } },
{ C_INCL, YP_TSTR, YP_VNONE, CONF_IO_FDIFF_ZONES | CONF_IO_FRLD_ALL, { include_file } },

View file

@ -58,6 +58,8 @@
#define C_ECS "\x12""edns-client-subnet"
#define C_EXPIRE_MAX_INTERVAL "\x13""expire-max-interval"
#define C_EXPIRE_MIN_INTERVAL "\x13""expire-min-interval"
#define C_EXTERNAL "\x08""external"
#define C_EXTERNAL_VLDT "\x13""external-validation"
#define C_FILE "\x04""file"
#define C_GLOBAL_MODULE "\x0D""global-module"
#define C_ID "\x02""id"

View file

@ -851,6 +851,12 @@ int check_policy(
return KNOT_EOK;
}
int check_external(
knotd_conf_check_args_t *args)
{
return KNOT_EOK;
}
int check_key(
knotd_conf_check_args_t *args)
{

View file

@ -119,6 +119,10 @@ int check_policy(
knotd_conf_check_args_t *args
);
int check_external(
knotd_conf_check_args_t *args
);
int check_key(
knotd_conf_check_args_t *args
);

View file

@ -933,6 +933,12 @@ static int zone_txn_commit_l(zone_t *zone, _unused_ ctl_args_t *args)
return KNOT_TXN_ENOTEXISTS;
}
if (zone->control_update->flags & UPDATE_WFEV) {
zone->control_update->flags |= UPDATE_EVOK;
knot_sem_post(&zone->control_update->external);
return KNOT_EOK;
}
int ret = zone_update_semcheck(conf(), zone->control_update);
if (ret != KNOT_EOK) {
return ret; // Recoverable error.
@ -1005,6 +1011,12 @@ static int zone_txn_abort(zone_t *zone, _unused_ ctl_args_t *args)
return KNOT_TXN_ENOTEXISTS;
}
if (zone->control_update->flags & UPDATE_WFEV) {
knot_sem_post(&zone->control_update->external);
pthread_mutex_unlock(&zone->cu_lock);
return KNOT_EOK;
}
zone_control_clear(zone);
pthread_mutex_unlock(&zone->cu_lock);
@ -1953,7 +1965,7 @@ static int ctl_zone(ctl_args_t *args, ctl_cmd_t cmd)
static void check_zone_txn(zone_t *zone, const knot_dname_t **exists)
{
if (zone->control_update != NULL) {
if (zone->control_update != NULL && !(zone->control_update->flags & UPDATE_WFEV)) {
*exists = zone->name;
}
}

View file

@ -23,6 +23,7 @@ int event_expire(conf_t *conf, zone_t *zone)
synchronize_rcu();
pthread_mutex_lock(&zone->cu_lock);
assert(zone->control_update == NULL || !(zone->control_update->flags & UPDATE_WFEV));
zone_control_clear(zone);
pthread_mutex_unlock(&zone->cu_lock);

View file

@ -667,6 +667,10 @@ static int ixfr_finalize(struct refresh_data *data)
ret = zone_update_commit(data->conf, &up);
if (ret != KNOT_EOK) {
zone_update_clear(&up);
if (ret == KNOT_EEXTERNAL) {
data->fallback_axfr = false;
data->fallback->remote = false;
}
IXFRIN_LOG(LOG_WARNING, data,
"failed to store changes (%s)", knot_strerror(ret));
return ret;

View file

@ -1039,6 +1039,13 @@ int server_start(server_t *server, bool answering)
return KNOT_EOK;
}
static void zonedb_shutdown(server_t *server)
{
if (server->zone_db != NULL) {
knot_zonedb_foreach(server->zone_db, zone_shutdown);
}
}
void server_wait(server_t *server)
{
if (server == NULL) {
@ -1404,6 +1411,8 @@ void server_stop(server_t *server)
/* Stop scheduler. */
evsched_stop(&server->sched);
/* Shut down zones. */
zonedb_shutdown(server);
/* Interrupt background workers. */
worker_pool_stop(server->workers);
@ -1621,6 +1630,7 @@ void server_update_zones(conf_t *conf, server_t *server, reload_t mode)
/* Suspend adding events to worker pool queue, wait for queued events. */
log_debug("suspending zone events");
evsched_pause(&server->sched);
zonedb_shutdown(server);
worker_pool_wait(server->workers);
log_debug("suspended zone events");

View file

@ -915,6 +915,42 @@ int zone_update_verify_digest(conf_t *conf, zone_update_t *update)
return ret;
}
int zone_update_external(conf_t *conf, zone_update_t *update)
{
(void)conf;
pthread_mutex_lock(&update->zone->cu_lock);
if (update->zone->control_update != NULL) {
assert(update->zone->control_update == update);
assert(!(update->flags & UPDATE_WFEV));
pthread_mutex_unlock(&update->zone->cu_lock);
return KNOT_EOK; // real control update never waits for external validation
}
if (zone_get_flag(update->zone, ZONE_SHUT_DOWN, false)) {
pthread_mutex_unlock(&update->zone->cu_lock);
return KNOT_EEXTERNAL;
}
update->zone->control_update = update;
update->flags |= UPDATE_WFEV;
knot_sem_init(&update->external, 0);
pthread_mutex_unlock(&update->zone->cu_lock);
log_zone_notice(update->zone->name, "waiting for external validation");
knot_sem_wait(&update->external);
pthread_mutex_lock(&update->zone->cu_lock);
update->zone->control_update = NULL;
pthread_mutex_unlock(&update->zone->cu_lock);
knot_sem_post(&update->external);
knot_sem_destroy(&update->external);
return (update->flags & UPDATE_EVOK) ? KNOT_EOK : KNOT_EEXTERNAL;
}
int zone_update_commit(conf_t *conf, zone_update_t *update)
{
if (conf == NULL || update == NULL) {
@ -977,6 +1013,15 @@ int zone_update_commit(conf_t *conf, zone_update_t *update)
}
}
val = conf_zone_get(conf, C_EXTERNAL_VLDT, update->zone->name);
if (val.code == KNOT_EOK) {
ret = zone_update_external(conf, update, &val);
if (ret != KNOT_EOK) {
discard_adds_tree(update);
return ret;
}
}
ret = update_catalog(conf, update);
if (ret != KNOT_EOK) {
log_zone_error(update->zone->name, "failed to process catalog zone (%s)", knot_strerror(ret));

View file

@ -29,6 +29,7 @@ typedef struct zone_update {
changeset_t extra_ch; /*!< Extra changeset to store just diff btwn zonefile and result. */
apply_ctx_t *a_ctx; /*!< Context for applying changesets. */
uint32_t flags; /*!< Zone update flags. */
knot_sem_t external; /*!< Lock for external validation. */
dnssec_validation_hint_t validation_hint;
} zone_update_t;
@ -50,6 +51,8 @@ typedef enum {
UPDATE_CHANGED_NSEC = 1 << 7, /*!< This incremental update affects NSEC or NSEC3 nodes in zone. */
UPDATE_NO_CHSET = 1 << 8, /*!< Avoid using changeset and serialize to journal from diff of bi-nodes. */
UPDATE_SIGNED_FULL = 1 << 9, /*!< Full (non-incremental) zone sign took place during this update. */
UPDATE_WFEV = 1 << 10, /*!< Update waiting for external validation. */
UPDATE_EVOK = 1 << 11, /*!< External validation accepted the update. */
} zone_update_flags_t;
/*!
@ -268,6 +271,18 @@ int zone_update_semcheck(conf_t *conf, zone_update_t *update);
*/
int zone_update_verify_digest(conf_t *conf, zone_update_t *update);
/*!
* \brief Wait for external validation.
*
* \param conf Configuration.
* \param update Zone update.
*
* \retval KNOT_EEXTERNAL External validation failed.
* \retval KNOT_EOK External validation succeeded.
* \return KNOT_E*
*/
int zone_update_external(conf_t *conf, zone_update_t *update);
/*!
* \brief Commits all changes to the zone, signs it, saves changes to journal.
*

View file

@ -200,6 +200,16 @@ void zone_control_clear(zone_t *zone)
zone->control_update = NULL;
}
void zone_shutdown(zone_t *zone)
{
pthread_mutex_lock(&zone->cu_lock);
if (zone->control_update != NULL && (zone->control_update->flags & UPDATE_WFEV)) {
knot_sem_post(&zone->control_update->external);
}
zone_set_flag(zone, ZONE_SHUT_DOWN);
pthread_mutex_unlock(&zone->cu_lock);
}
void zone_free(zone_t **zone_ptr)
{
if (zone_ptr == NULL || *zone_ptr == NULL) {
@ -226,6 +236,7 @@ void zone_free(zone_t **zone_ptr)
knot_sem_destroy(&zone->cow_lock);
/* Control update. */
assert(zone->control_update == NULL || !(zone->control_update->flags & UPDATE_WFEV));
zone_control_clear(zone);
free(zone->catalog_gen);

View file

@ -39,6 +39,7 @@ typedef enum {
ZONE_XFR_FROZEN = 1 << 7, /*!< Outgoing AXFR/IXFR temporarily disabled. */
ZONE_USER_FLUSH = 1 << 8, /*!< User-triggered flush. */
ZONE_LAST_SIGN_OK = 1 << 9, /*!< Last full-sign event finished OK. */
ZONE_SHUT_DOWN = 1 << 10, /*!< Zone events are shutting down. */
} zone_flag_t;
/*!
@ -148,6 +149,13 @@ typedef struct zone
*/
zone_t* zone_new(const knot_dname_t *name);
/*!
* \brief Declare that zone is shutting down.
*
* \param zone Zone to be shut down.
*/
void zone_shutdown(zone_t *zone);
/*!
* \brief Deallocates the zone structure.
*

View file

@ -533,6 +533,8 @@ static knot_zonedb_t *create_zonedb(conf_t *conf, server_t *server, reload_t mod
it = knot_zonedb_iter_begin(db_new);
while (!knot_zonedb_iter_finished(it)) {
zone_t *z = knot_zonedb_iter_val(it);
zone_unset_flag(z, ZONE_SHUT_DOWN);
conf_val_t val = conf_zone_get(conf, C_REVERSE_GEN, z->name);
while (val.code == KNOT_EOK) {
const knot_dname_t *forw_name = conf_dname(&val);

View file

@ -100,6 +100,7 @@ enum knot_error {
KNOT_EBACKUPDATA,
KNOT_ECPUCOMPAT,
KNOT_EMODINVAL,
KNOT_EEXTERNAL,
KNOT_GENERAL_ERROR = -900,

View file

@ -99,6 +99,7 @@ static const struct error errors[] = {
{ KNOT_EBACKUPDATA, "requested data not in backup" },
{ KNOT_ECPUCOMPAT, "incompatible CPU architecture" },
{ KNOT_EMODINVAL, "invalid module" },
{ KNOT_EEXTERNAL, "external validation failed" },
{ KNOT_GENERAL_ERROR, "unknown general error" },

View file

@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
Test of external zone validation.
"""
from dnstest.utils import *
from dnstest.test import Test
import random
t = Test()
master = t.server("knot")
slave = t.server("knot")
zone = t.zone_rnd(1, records=40)
t.link(zone, master, slave)
def log_count_expect(server, pattern, expct):
fnd = server.log_search_count(pattern)
if fnd != expct:
detail_log("LOG SEARCH COUNT '%s' found %d expected %d" % (pattern, fnd, expct))
set_err("LOG SEARCH COUNT %d != %d" % (fnd, expct))
ZONE = zone[0].name
LOG = "for external validation"
slave.async_start = True
slave.zones[ZONE].external = True # TODO this will be a list or dict once 'external' secation has any fields
master.dnssec(zone[0]).enable = random.choice([False, True])
t.start()
serial = master.zone_wait(zone)
t.sleep(2)
log_count_expect(slave, LOG, 1)
resp = slave.dig(ZONE, "SOA")
resp.check(rcode="SERVFAIL")
resp.check_count(0, "SOA")
slave.ctl("zone-commit " + ZONE)
t.sleep(2)
resp = slave.dig(ZONE, "SOA")
resp.check_soa_serial(serial)
master.random_ddns(zone, allow_empty=False)
serial = master.zone_wait(zone, serial)
t.sleep(2)
log_count_expect(slave, LOG, 2)
slave.ctl("zone-abort " + ZONE)
t.sleep(2)
resp = slave.dig(ZONE, "SOA")
resp.check_soa_serial(serial - 1)
master.random_ddns(zone, allow_empty=False)
serial = master.zone_wait(zone, serial)
t.sleep(2)
log_count_expect(slave, LOG, 3)
slave.ctl("zone-commit " + ZONE)
t.sleep(2)
resp = slave.dig(ZONE, "SOA")
resp.check_soa_serial(serial)
slave.ctl("zone-freeze " + ZONE)
master.random_ddns(zone, allow_empty=False)
serial = master.zone_wait(zone, serial)
slave.zonemd_generate = "zonemd-sha512"
slave.gen_confile()
slave.ctl("zone-thaw " + ZONE)
t.sleep(1)
slave.reload()
master.random_ddns(zone, allow_empty=False)
serial = master.zone_wait(zone, serial)
t.sleep(2)
log_count_expect(slave, LOG, 5)
slave.ctl("zone-commit " + ZONE)
t.sleep(2)
resp = slave.dig(ZONE, "SOA")
resp.check_soa_serial(serial)
master.random_ddns(zone, allow_empty=False)
serial = master.zone_wait(zone, serial)
t.sleep(2)
log_count_expect(slave, LOG, 6)
slave.stop()
t.sleep(2)
log_count_expect(slave, "shutting down", 1)
t.end()

View file

@ -102,6 +102,7 @@ class Zone(object):
self.journal_content = journal_content # journal contents
self.modules = []
self.reverse_from = None
self.external = None
self.dnssec = ZoneDnssec()
self.catalog_role = ZoneCatalogRole.NONE
self.catalog_gen_name = None # Generated catalog name for this member
@ -216,6 +217,7 @@ class Server(object):
self.zone_size_limit = None
self.serial_policy = None
self.auto_acl = None
self.async_start = None
self.provide_ixfr = None
self.master_pin_tol = None
self.quic_log = None
@ -1543,6 +1545,7 @@ class Knot(Server):
self._str(s, "remote-pool-limit", str(random.randint(0,6)))
self._str(s, "remote-retry-delay", str(random.choice([0, 1, 5])))
self._bool(s, "automatic-acl", self.auto_acl)
self._bool(s, "async-start", self.async_start)
if self.cert_key_file:
s.item_str("key-file", self.cert_key_file[0])
s.item_str("cert-file", self.cert_key_file[1])
@ -1787,6 +1790,18 @@ class Knot(Server):
if have_dnskeysync:
s.end()
have_external = False
for zone in sorted(self.zones):
z = self.zones[zone]
if not z.external:
continue
if not have_external:
s.begin("external")
have_external = True
s.id_item("id", z.name)
if have_external:
s.end()
have_keystore = False
for zone in sorted(self.zones):
z = self.zones[zone]
@ -1958,6 +1973,9 @@ class Knot(Server):
self._str(s, "expire-min-interval", z.expire_min)
self._str(s, "expire-max-interval", z.expire_max)
if z.external:
s.item("external-validation", z.name)
if self.zonefile_load is not None:
s.item_str("zonefile-load", self.zonefile_load)
elif z.ixfr: