Forked from
Knot projects / Knot DNS
11357 commits behind the upstream repository.
-
Marek Vavrusa authoredMarek Vavrusa authored
zone-load.c 14.64 KiB
/* Copyright (C) 2013 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
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <config.h>
#include <assert.h>
#include <sys/stat.h>
#include <inttypes.h>
#include "knot/conf/conf.h"
#include "knot/other/debug.h"
#include "knot/server/zone-load.h"
#include "knot/server/zones.h"
#include "knot/zone/zone-load.h"
#include "libknot/dname.h"
#include "libknot/dnssec/crypto.h"
#include "libknot/dnssec/random.h"
#include "libknot/rdata.h"
#include "knot/zone/zone.h"
#include "knot/zone/zone.h"
#include "knot/zone/zonedb.h"
#include "common/descriptor.h"
/* Constants */
#define XFRIN_BOOTSTRAP_DELAY 2000 /*!< AXFR bootstrap avg. delay */
/*- zone file status --------------------------------------------------------*/
/*!
* \brief Zone file status.
*/
typedef enum {
ZONE_STATUS_NOT_FOUND = 0, //!< Zone file does not exist.
ZONE_STATUS_FOUND_NEW, //!< Zone file exists, not loaded yet.
ZONE_STATUS_FOUND_CURRENT, //!< Zone file exists, same as loaded.
ZONE_STATUS_FOUND_UPDATED, //!< Zone file exists, newer than loaded.
} zone_status_t;
/*!
* \brief Check zone file status.
*
* \param old_zone Previous instance of the zone (can be NULL).
* \param filename File name of zone file.
*
* \return Zone status.
*/
static zone_status_t zone_file_status(const zone_t *old_zone,
const char *filename)
{
struct stat zf_stat = { 0 };
int result = stat(filename, &zf_stat);
if (result == -1) {
return ZONE_STATUS_NOT_FOUND;
} else if (old_zone == NULL) {
return ZONE_STATUS_FOUND_NEW;
} else if (old_zone->zonefile_mtime == zf_stat.st_mtime) {
return ZONE_STATUS_FOUND_CURRENT;
} else {
return ZONE_STATUS_FOUND_UPDATED;
}
}
/*- zone loading/updating ---------------------------------------------------*/
/*!
* \brief Handle retrieval of zone if zone file does not exist.
*
* \param conf New configuration for given zone.
*
* \return New zone, NULL if bootstrap not possible.
*/
static zone_t *bootstrap_zone(conf_zone_t *conf)
{
assert(conf);
bool bootstrap = !EMPTY_LIST(conf->acl.xfr_in);
if (!bootstrap) {
return load_zone_file(conf); /* No master for this zone, fallback. */
}
zone_t *new_zone = zone_new(conf);
if (!new_zone) {
log_zone_error("Bootstrap of zone '%s' failed: %s\n",
conf->name, knot_strerror(KNOT_ENOMEM));
return NULL;
}
/* Initialize bootstrap timer. */
new_zone->xfr_in.bootstrap_retry = knot_random_uint32_t() % XFRIN_BOOTSTRAP_DELAY;
return new_zone;
}
zone_t *load_zone_file(conf_zone_t *conf)
{
assert(conf);
/* Open zone file for parsing. */
zloader_t zl;
int ret = zonefile_open(&zl, conf);
if (ret != KNOT_EOK) {
log_zone_error("Failed to open zone file '%s': %s\n",
conf->file, knot_strerror(ret));
return NULL;
}
struct stat st;
if (stat(conf->file, &st) < 0) {
/* Go silently and reset mtime to 0. */
memset(&st, 0, sizeof(struct stat));
}
/* Load the zone contents. */
knot_zone_contents_t *zone_contents = zonefile_load(&zl);
zonefile_close(&zl);
/* Check the loader result. */
if (zone_contents == NULL) {
log_zone_error("Failed to load zone file '%s'.\n", conf->file);
return NULL;
}
/* Create the new zone. */
zone_t *zone = zone_new((conf_zone_t *)conf);
if (zone == NULL) {
log_zone_error("Failed to create zone '%s': %s\n",
conf->name, knot_strerror(KNOT_ENOMEM));
knot_zone_contents_deep_free(&zone_contents);
return NULL;
}
/* Link zone contents to zone. */
zone->contents = zone_contents;
/* Save the timestamp from the zone db file. */
zone->zonefile_mtime = st.st_mtime;
zone->zonefile_serial = knot_zone_serial(zone->contents);
return zone;
}
/*!
* \brief Create a new zone structure according to documentation, but reuse
* existing zone content.
*/
static zone_t *preserve_zone(conf_zone_t *conf, const zone_t *old_zone)
{
assert(old_zone);
zone_t *new_zone = zone_new(conf);
if (!new_zone) {
log_zone_error("Preserving current zone '%s' failed: %s\n",
conf->name, knot_strerror(KNOT_ENOMEM));
return NULL;
}
new_zone->contents = old_zone->contents;
return new_zone;
}
/*!
* \brief Log message about loaded zone (name, status, serial).
*
* \param zone Zone structure.
* \param zone_name Printable name of the zone.
* \param status Zone file status.
*/
static void log_zone_load_info(const zone_t *zone, const char *zone_name,
zone_status_t status)
{
assert(zone);
assert(zone_name);
const char *action = NULL;
switch (status) {
case ZONE_STATUS_NOT_FOUND: action = "bootstrapped"; break;
case ZONE_STATUS_FOUND_NEW: action = "loaded"; break;
case ZONE_STATUS_FOUND_CURRENT: action = "is up-to-date"; break;
case ZONE_STATUS_FOUND_UPDATED: action = "reloaded"; break;
}
assert(action);
int64_t serial = 0;
if (zone->contents && zone->contents->apex) {
const knot_rrset_t *soa;
soa = knot_node_rrset(zone->contents->apex, KNOT_RRTYPE_SOA);
serial = knot_rdata_soa_serial(soa);
}
log_zone_info("Zone '%s' %s (serial %" PRId64 ")\n", zone_name, action, serial);
}
/*!
* \brief Load or reload the zone.
*
* \param old_zone Already loaded zone (can be NULL).
* \param conf Zone configuration.
* \param server Name server.
*
* \return Error code, KNOT_EOK if successful.
*/
static zone_t *create_zone(zone_t *old_zone, conf_zone_t *conf, server_t *server)
{
assert(conf);
zone_status_t zstatus = zone_file_status(old_zone, conf->file);
zone_t *new_zone = NULL;
switch (zstatus) {
case ZONE_STATUS_NOT_FOUND:
new_zone = bootstrap_zone(conf);
break;
case ZONE_STATUS_FOUND_NEW:
case ZONE_STATUS_FOUND_UPDATED:
new_zone = load_zone_file(conf);
break;
case ZONE_STATUS_FOUND_CURRENT:
new_zone = preserve_zone(conf, old_zone);
break;
default:
assert(0);
}
if (!new_zone) {
log_server_error("Failed to load zone '%s'.\n", conf->name);
return NULL;
}
/* Initialize zone timers. */
zone_timers_create(new_zone, &server->sched);
log_zone_load_info(new_zone, conf->name, zstatus);
return new_zone;
}
/*!
* \brief Load/reload the zone, apply journal, sign it and schedule XFR sync.
*
* \param[in] old_zone Old zone (if loaded).
* \param[in] conf Zone configuration.
* \param[in] server Name server structure.
*
* \return Updated zone on success, NULL otherwise.
*/
static zone_t* update_zone(zone_t *old_zone, conf_zone_t *conf, server_t *server)
{
assert(conf);
int result = KNOT_ERROR;
// Load zone.
zone_t *new_zone = create_zone(old_zone, conf, server);
if (!new_zone) {
return NULL;
}
bool new_content = (old_zone == NULL || old_zone->contents != new_zone->contents);
result = zones_journal_apply(new_zone);
if (result != KNOT_EOK && result != KNOT_ERANGE && result != KNOT_ENOENT) {
log_zone_error("Zone '%s', failed to apply changes from journal: %s\n",
conf->name, knot_strerror(result));
goto fail;
}
result = zones_do_diff_and_sign(conf, new_zone, old_zone, new_content);
if (result != KNOT_EOK) {
log_zone_error("Zone '%s', failed to sign the zone: %s\n",
conf->name, knot_strerror(result));
goto fail;
}
fail:
assert(new_zone);
if (result == KNOT_EOK) {
return new_zone;
} else {
/* Preserved zone, don't free the shared contents. */
if (!new_content) {
new_zone->contents = NULL;
}
/* Disconnect config, caller is responsible for it. */
new_zone->conf = NULL;
zone_free(&new_zone);
}
return NULL;
}
/*!
* \brief Check zone configuration constraints.
*/
static int update_zone_postcond(zone_t *new_zone, const conf_t *config)
{
/* Bootstrapped zone, no checks apply. */
if (new_zone->contents == NULL) {
return KNOT_EOK;
}
/* Check minimum EDNS0 payload if signed. (RFC4035/sec. 3) */
if (knot_zone_contents_is_signed(new_zone->contents)) {
unsigned edns_dnssec_min = EDNS_MIN_DNSSEC_PAYLOAD;
if (config->max_udp_payload < edns_dnssec_min) {
log_zone_warning("EDNS payload lower than %uB for "
"DNSSEC-enabled zone '%s'.\n",
edns_dnssec_min, new_zone->conf->name);
}
}
/* Check NSEC3PARAM state if present. */
int result = knot_zone_contents_load_nsec3param(new_zone->contents);
if (result != KNOT_EOK) {
log_zone_error("NSEC3 signed zone has invalid or no "
"NSEC3PARAM record.\n");
return result;
}
return KNOT_EOK;
}
/*! \brief Context for threaded zone loader. */
typedef struct {
const struct conf_t *config;
server_t *server;
knot_zonedb_t *db_old;
knot_zonedb_t *db_new;
pthread_mutex_t lock;
} zone_loader_ctx_t;
/*! Thread entrypoint for loading zones. */
static int zone_loader_thread(dthread_t *thread)
{
if (thread == NULL || thread->data == NULL) {
return KNOT_EINVAL;
}
zone_t *zone = NULL;
conf_zone_t *zone_config = NULL;
zone_loader_ctx_t *ctx = (zone_loader_ctx_t *)thread->data;
for(;;) {
/* Fetch zone configuration from the list. */
pthread_mutex_lock(&ctx->lock);
if (EMPTY_LIST(ctx->config->zones)) {
pthread_mutex_unlock(&ctx->lock);
break;
}
/* Disconnect from the list and start processing. */
zone_config = HEAD(ctx->config->zones);
rem_node(&zone_config->n);
pthread_mutex_unlock(&ctx->lock);
/* Retrive old zone (if exists). */
knot_dname_t *apex = knot_dname_from_str(zone_config->name);
if (!apex) {
return KNOT_ENOMEM;
}
zone_t *old_zone = knot_zonedb_find(ctx->db_old, apex);
knot_dname_free(&apex);
/* Update the zone. */
zone = update_zone(old_zone, zone_config, ctx->server);
if (zone == NULL) {
conf_free_zone(zone_config);
continue;
}
/* Check updated zone post-conditions. */
int ret = update_zone_postcond(zone, ctx->config);
/* Insert into database if properly loaded. */
if (ret == KNOT_EOK) {
pthread_mutex_lock(&ctx->lock);
if (zone != NULL) {
ret = knot_zonedb_insert(ctx->db_new, zone);
}
pthread_mutex_unlock(&ctx->lock);
}
/* Check for any failure. */
if (ret != KNOT_EOK && zone) {
/* Preserved zone, don't free the shared contents. */
if (old_zone && old_zone->contents == zone->contents) {
zone->contents = NULL;
}
zone_free(&zone);
}
}
return KNOT_EOK;
}
static int zone_loader_destruct(dthread_t *thread)
{
knot_crypto_cleanup_thread();
return KNOT_EOK;
}
/*!
* \brief Fill the new database with zones.
*
* Zones that should be retained are just added from the old database to the
* new. New zones are loaded.
*
* \param server Name server instance.
* \param conf Server configuration.
*
* \return Number of inserted zones.
*/
static knot_zonedb_t *load_zonedb(server_t *server, const conf_t *conf)
{
/* Initialize threaded loader. */
zone_loader_ctx_t ctx = {0};
ctx.config = conf;
ctx.server = server;
ctx.db_old = server->zone_db;
ctx.db_new = knot_zonedb_new(conf->zones_count);
if (ctx.db_new == NULL) {
return NULL;
}
if (conf->zones_count == 0) {
return ctx.db_new;
}
if (pthread_mutex_init(&ctx.lock, NULL) < 0) {
knot_zonedb_free(&ctx.db_new);
return NULL;
}
/* Initialize threads. */
size_t thread_count = MIN(conf->zones_count, dt_optimal_size());
dt_unit_t *unit = NULL;
unit = dt_create(thread_count, &zone_loader_thread,
&zone_loader_destruct, &ctx);
if (unit != NULL) {
/* Start loading. */
dt_start(unit);
dt_join(unit);
dt_delete(&unit);
} else {
knot_zonedb_free(&ctx.db_new);
ctx.db_new = NULL;
}
pthread_mutex_destroy(&ctx.lock);
return ctx.db_new;
}
/*!
* \brief Remove old zones and zone database.
*
* \note Zone may be preserved in the new zone database, in this case
* new and old zone share the contents. Shared content is not freed.
*
* \param db_new New zone database.
* \param db_old Old zone database.
*/
static int remove_old_zonedb(const knot_zonedb_t *db_new, knot_zonedb_t *db_old)
{
if (db_old == NULL) {
return KNOT_EOK; /* Nothing to free. */
}
knot_zonedb_iter_t it;
knot_zonedb_iter_begin(db_new, &it);
while(!knot_zonedb_iter_finished(&it)) {
zone_t *new_zone = knot_zonedb_iter_val(&it);
zone_t *old_zone = knot_zonedb_find(db_old, new_zone->name);
/* If the zone exists in both new and old database and the contents
* didn't change. We must invalidate the pointer in the old zone
* to preserve the contents.
*/
if (old_zone && old_zone->contents == new_zone->contents) {
old_zone->contents = NULL;
}
knot_zonedb_iter_next(&it);
}
synchronize_rcu();
/* Delete all deprecated zones and delete the old database. */
knot_zonedb_deep_free(&db_old);
return KNOT_EOK;
}
/*- public API functions ----------------------------------------------------*/
/*!
* \brief Update zone database according to configuration.
*/
int load_zones_from_config(const conf_t *conf, struct server_t *server)
{
/* Check parameters */
if (conf == NULL || server == NULL) {
return KNOT_EINVAL;
}
/* Freeze zone timers. */
if (server->zone_db) {
knot_zonedb_foreach(server->zone_db, zone_timers_freeze);
}
/* Insert all required zones to the new zone DB. */
/*! \warning RCU must not be locked as some contents switching will
be required. */
knot_zonedb_t *db_new = load_zonedb(server, conf);
if (db_new == NULL) {
log_server_warning("Failed to load zones.\n");
return KNOT_ENOMEM;
} else {
size_t loaded = knot_zonedb_size(db_new);
log_server_info("Loaded %zu out of %d zones.\n",
loaded, conf->zones_count);
if (loaded != conf->zones_count) {
log_server_warning("Not all the zones were loaded.\n");
}
}
/* Rebuild zone database search stack. */
knot_zonedb_build_index(db_new);
/* Switch the databases. */
knot_zonedb_t *db_old = rcu_xchg_pointer(&server->zone_db, db_new);
/* Wait for readers to finish reading old zone database. */
synchronize_rcu();
/* Thaw zone events now that the database is published. */
if (server->zone_db) {
knot_zonedb_foreach(server->zone_db, zone_timers_thaw);
knot_zonedb_foreach(server->zone_db, zones_schedule_notify, server);
}
/*
* Remove all zones present in the new DB from the old DB.
* No new thread can access these zones in the old DB, as the
* databases are already switched.
*/
return remove_old_zonedb(db_new, db_old);
}