summaryrefslogtreecommitdiffstats
path: root/src/knot/events/handlers/refresh.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/knot/events/handlers/refresh.c')
-rw-r--r--src/knot/events/handlers/refresh.c1169
1 files changed, 1169 insertions, 0 deletions
diff --git a/src/knot/events/handlers/refresh.c b/src/knot/events/handlers/refresh.c
new file mode 100644
index 0000000..27aa2c4
--- /dev/null
+++ b/src/knot/events/handlers/refresh.c
@@ -0,0 +1,1169 @@
+/* Copyright (C) 2018 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 <assert.h>
+#include <stdint.h>
+
+#include "contrib/mempattern.h"
+#include "libdnssec/random.h"
+#include "knot/common/log.h"
+#include "knot/conf/conf.h"
+#include "knot/dnssec/zone-events.h"
+#include "knot/events/handlers.h"
+#include "knot/events/replan.h"
+#include "knot/nameserver/ixfr.h"
+#include "knot/query/layer.h"
+#include "knot/query/query.h"
+#include "knot/query/requestor.h"
+#include "knot/updates/changesets.h"
+#include "knot/zone/serial.h"
+#include "knot/zone/zone.h"
+#include "knot/zone/zonefile.h"
+#include "libknot/errcode.h"
+
+/*!
+ * \brief Refresh event processing.
+ *
+ * The following diagram represents refresh event processing.
+ *
+ * \verbatim
+ * O
+ * |
+ * +-----v-----+
+ * | BEGIN |
+ * +---+---+---+
+ * has SOA | | no SOA
+ * +-------------------+ +------------------------------+
+ * | |
+ * +------v------+ outdated +--------------+ error +-------v------+
+ * | SOA query +------------> IXFR query +-----------> AXFR query |
+ * +-----+---+---+ +------+-------+ +----+----+----+
+ * error | | current | success success | | error
+ * | +-----+ +---------------+ | |
+ * | | | +--------------------------------------+ |
+ * | | | | +----------+ +--------------+
+ * | | | | | | |
+ * | +--v-v-v--+ | +--v--v--+
+ * | | DONE | | | FAIL |
+ * | +---------+ | +--------+
+ * +----------------------------+
+ *
+ * \endverbatim
+ */
+
+#define REFRESH_LOG(priority, zone, remote, msg...) \
+ ns_log(priority, zone, LOG_OPERATION_REFRESH, LOG_DIRECTION_OUT, remote, msg)
+
+#define _XFRIN_LOG(priority, operation, zone, remote, msg...) \
+ ns_log(priority, zone, operation, LOG_DIRECTION_IN, remote, msg)
+
+#define AXFRIN_LOG(priority, zone, remote, msg...) \
+ _XFRIN_LOG(priority, LOG_OPERATION_AXFR, zone, remote, msg)
+
+#define IXFRIN_LOG(priority, zone, remote, msg...) \
+ _XFRIN_LOG(priority, LOG_OPERATION_IXFR, zone, remote, msg)
+
+#define BOOTSTRAP_MAXTIME (24*60*60)
+#define BOOTSTRAP_JITTER (30)
+
+enum state {
+ REFRESH_STATE_INVALID = 0,
+ STATE_SOA_QUERY,
+ STATE_TRANSFER,
+};
+
+enum xfr_type {
+ XFR_TYPE_ERROR = -1,
+ XFR_TYPE_UNDETERMINED = 0,
+ XFR_TYPE_UPTODATE,
+ XFR_TYPE_AXFR,
+ XFR_TYPE_IXFR,
+};
+
+struct refresh_data {
+ // transfer configuration, initialize appropriately:
+
+ zone_t *zone; //!< Zone to eventually updated.
+ conf_t *conf; //!< Server configuration.
+ const struct sockaddr *remote; //!< Remote endpoint.
+ const knot_rrset_t *soa; //!< Local SOA (NULL for AXFR).
+ const size_t max_zone_size; //!< Maximal zone size.
+ struct query_edns_data edns; //!< EDNS data to be used in queries.
+
+ // internal state, initialize with zeroes:
+
+ enum state state; //!< Event processing state.
+ enum xfr_type xfr_type; //!< Transer type (mostly IXFR versus AXFR).
+ knot_rrset_t *initial_soa_copy; //!< Copy of the received initial SOA.
+ struct xfr_stats stats; //!< Transfer statistics.
+ size_t change_size; //!< Size of added and removed RRs.
+
+ struct {
+ zone_contents_t *zone; //!< AXFR result, new zone.
+ } axfr;
+
+ struct {
+ struct ixfr_proc *proc; //!< IXFR processing context.
+ knot_rrset_t *final_soa; //!< SOA denoting end of transfer.
+ list_t changesets; //!< IXFR result, zone updates.
+ } ixfr;
+
+ bool updated; // TODO: Can we fid a better way to check if zone was updated?
+ knot_mm_t *mm; // TODO: This used to be used in IXFR. Remove or reuse.
+};
+
+static bool serial_is_current(uint32_t local_serial, uint32_t remote_serial)
+{
+ return (serial_compare(local_serial, remote_serial) & SERIAL_MASK_GEQ);
+}
+
+static time_t bootstrap_next(const zone_timers_t *timers)
+{
+ time_t expired_at = timers->last_refresh + timers->soa_expire;
+
+ // previous interval
+ time_t interval = timers->next_refresh - expired_at;
+ if (interval < 0) {
+ interval = 0;
+ }
+
+ // exponential backoff
+ interval *= 2;
+ if (interval > BOOTSTRAP_MAXTIME) {
+ interval = BOOTSTRAP_MAXTIME;
+ }
+
+ // prevent burst refresh
+ interval += dnssec_random_uint16_t() % BOOTSTRAP_JITTER;
+
+ return interval;
+}
+
+static int xfr_validate(zone_contents_t *zone, struct refresh_data *data)
+{
+ sem_handler_t handler = {
+ .cb = err_handler_logger
+ };
+
+ int ret = sem_checks_process(zone, false, &handler, time(NULL));
+ if (ret != KNOT_EOK) {
+ // error is logged by the error handler
+ return ret;
+ }
+
+ if (zone->size > data->max_zone_size) {
+ ns_log(LOG_WARNING, data->zone->name,
+ data->xfr_type == XFR_TYPE_IXFR ? LOG_OPERATION_IXFR : LOG_OPERATION_AXFR,
+ LOG_DIRECTION_IN, data->remote, "zone size exceeded");
+ return KNOT_EZONESIZE;
+ }
+
+ return KNOT_EOK;
+}
+
+static void xfr_log_publish(const knot_dname_t *zone_name,
+ const struct sockaddr *remote,
+ const uint32_t old_serial,
+ const uint32_t new_serial,
+ bool axfr_bootstrap)
+{
+ if (!axfr_bootstrap) {
+ REFRESH_LOG(LOG_INFO, zone_name, remote,
+ "zone updated, serial %u -> %u",
+ old_serial, new_serial);
+ } else {
+ REFRESH_LOG(LOG_INFO, zone_name, remote,
+ "zone updated, serial none -> %u",
+ new_serial);
+ }
+}
+
+static int axfr_init(struct refresh_data *data)
+{
+ zone_contents_t *new_zone = zone_contents_new(data->zone->name);
+ if (new_zone == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ data->axfr.zone = new_zone;
+ return KNOT_EOK;
+}
+
+static void axfr_cleanup(struct refresh_data *data)
+{
+ zone_contents_deep_free(data->axfr.zone);
+ data->axfr.zone = NULL;
+}
+
+static int axfr_finalize(struct refresh_data *data)
+{
+ zone_contents_t *new_zone = data->axfr.zone;
+
+ int ret = zone_contents_adjust_full(new_zone);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ ret = xfr_validate(new_zone, data);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ uint32_t master_serial = zone_contents_serial(new_zone);
+ uint32_t local_serial = zone_contents_serial(data->zone->contents);
+ bool have_lastsigned = zone_get_lastsigned_serial(data->zone, &local_serial);
+ uint32_t old_serial = local_serial;
+ bool bootstrap = (data->soa == NULL);
+ if ((!bootstrap || have_lastsigned) &&
+ (serial_compare(master_serial, local_serial) != SERIAL_GREATER)) {
+ conf_val_t val = conf_zone_get(data->conf, C_SERIAL_POLICY, data->zone->name);
+ local_serial = serial_next(local_serial, conf_opt(&val));
+ zone_contents_set_soa_serial(new_zone, local_serial);
+ }
+
+ zone_update_t up = { 0 };
+ ret = zone_update_from_contents(&up, data->zone, new_zone, UPDATE_FULL);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ // Seized by zone_update. Don't free the contents again in axfr_cleanup.
+ data->axfr.zone = NULL;
+
+ conf_val_t val = conf_zone_get(data->conf, C_DNSSEC_SIGNING, data->zone->name);
+ bool dnssec_enable = conf_bool(&val);
+ if (dnssec_enable) {
+ zone_sign_reschedule_t resch = { .allow_rollover = true };
+ ret = knot_dnssec_zone_sign(&up, ZONE_SIGN_KEEP_SERIAL, &resch);
+ if (ret != KNOT_EOK) {
+ zone_update_clear(&up);
+ return ret;
+ }
+ event_dnssec_reschedule(data->conf, data->zone, &resch, true);
+ }
+
+ ret = zone_update_commit(data->conf, &up);
+ zone_update_clear(&up);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ if (dnssec_enable) {
+ ret = zone_set_master_serial(data->zone, master_serial);
+ if (ret == KNOT_EOK) {
+ ret = zone_set_lastsigned_serial(data->zone, zone_contents_serial(new_zone));
+ }
+ if (ret != KNOT_EOK) {
+ log_zone_warning(data->zone->name,
+ "unable to save master serial, future transfers might be broken");
+ }
+ }
+
+ xfr_log_publish(data->zone->name, data->remote, old_serial,
+ zone_contents_serial(new_zone), bootstrap);
+
+ return KNOT_EOK;
+}
+
+static int axfr_consume_rr(const knot_rrset_t *rr, struct refresh_data *data)
+{
+ assert(rr);
+ assert(data);
+ assert(data->axfr.zone);
+
+ // zc is stateless structure which can be initialized for each rr
+ // the changes are stored only in data->axfr.zone (aka zc.z)
+ zcreator_t zc = {
+ .z = data->axfr.zone,
+ .master = false,
+ .ret = KNOT_EOK
+ };
+
+ if (rr->type == KNOT_RRTYPE_SOA &&
+ node_rrtype_exists(zc.z->apex, KNOT_RRTYPE_SOA)) {
+ return KNOT_STATE_DONE;
+ }
+
+ int ret = zcreator_step(&zc, rr);
+ if (ret != KNOT_EOK) {
+ return KNOT_STATE_FAIL;
+ }
+
+ data->change_size += knot_rrset_size(rr);
+ if (data->change_size > data->max_zone_size) {
+ AXFRIN_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "zone size exceeded");
+ return KNOT_STATE_FAIL;
+ }
+
+ return KNOT_STATE_CONSUME;
+}
+
+static int axfr_consume_packet(knot_pkt_t *pkt, struct refresh_data *data)
+{
+ assert(pkt);
+ assert(data);
+
+ const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+ int ret = KNOT_STATE_CONSUME;
+ for (uint16_t i = 0; i < answer->count && ret == KNOT_STATE_CONSUME; ++i) {
+ ret = axfr_consume_rr(knot_pkt_rr(answer, i), data);
+ }
+ return ret;
+}
+
+static int axfr_consume(knot_pkt_t *pkt, struct refresh_data *data)
+{
+ assert(pkt);
+ assert(data);
+
+ // Check RCODE
+ if (knot_pkt_ext_rcode(pkt) != KNOT_RCODE_NOERROR) {
+ AXFRIN_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "server responded with error '%s'",
+ knot_pkt_ext_rcode_name(pkt));
+ return KNOT_STATE_FAIL;
+ }
+
+ // Initialize with first packet
+ if (data->axfr.zone == NULL) {
+ int ret = axfr_init(data);
+ if (ret != KNOT_EOK) {
+ AXFRIN_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "failed to initialize (%s)",
+ knot_strerror(ret));
+ return KNOT_STATE_FAIL;
+ }
+
+ AXFRIN_LOG(LOG_INFO, data->zone->name, data->remote, "starting");
+ xfr_stats_begin(&data->stats);
+ data->change_size = 0;
+ }
+
+ int next;
+ // Process saved SOA if fallback from IXFR
+ if (data->initial_soa_copy != NULL) {
+ next = axfr_consume_rr(data->initial_soa_copy, data);
+ knot_rrset_free(data->initial_soa_copy, data->mm);
+ data->initial_soa_copy = NULL;
+ if (next != KNOT_STATE_CONSUME) {
+ return next;
+ }
+ }
+
+ // Process answer packet
+ xfr_stats_add(&data->stats, pkt->size);
+ next = axfr_consume_packet(pkt, data);
+
+ // Finalize
+ if (next == KNOT_STATE_DONE) {
+ xfr_stats_end(&data->stats);
+ }
+
+ return next;
+}
+
+/*! \brief Initialize IXFR-in processing context. */
+static int ixfr_init(struct refresh_data *data)
+{
+ struct ixfr_proc *proc = mm_alloc(data->mm, sizeof(*proc));
+ if (proc == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ memset(proc, 0, sizeof(struct ixfr_proc));
+ proc->state = IXFR_START;
+ proc->mm = data->mm;
+
+ data->ixfr.proc = proc;
+ data->ixfr.final_soa = NULL;
+
+ init_list(&data->ixfr.changesets);
+
+ return KNOT_EOK;
+}
+
+/*! \brief Clean up data allocated by IXFR-in processing. */
+static void ixfr_cleanup(struct refresh_data *data)
+{
+ if (data->ixfr.proc == NULL) {
+ return;
+ }
+
+ knot_rrset_free(data->ixfr.final_soa, data->mm);
+ data->ixfr.final_soa = NULL;
+ mm_free(data->mm, data->ixfr.proc);
+ data->ixfr.proc = NULL;
+
+ changesets_free(&data->ixfr.changesets);
+}
+
+static int ixfr_finalize(struct refresh_data *data)
+{
+ uint32_t master_serial;
+ int ret = zone_get_master_serial(data->zone, &master_serial);
+ if (ret != KNOT_EOK) {
+ log_zone_error(data->zone->name, "Failed reading master's serial"
+ "from KASP DB (%s)", knot_strerror(ret));
+ return ret;
+ }
+ uint32_t local_serial = zone_contents_serial(data->zone->contents);
+ uint32_t lastsigned_serial = local_serial;
+ bool have_lastsigned = zone_get_lastsigned_serial(data->zone, &lastsigned_serial);
+ uint32_t old_serial = local_serial;
+ changeset_t *chs = NULL;
+ WALK_LIST(chs, data->ixfr.changesets) {
+ master_serial = knot_soa_serial(chs->soa_to->rrs.rdata);
+ knot_soa_serial_set(chs->soa_from->rrs.rdata, local_serial);
+ if (have_lastsigned && (serial_compare(lastsigned_serial, local_serial) & SERIAL_MASK_GEQ)) {
+ local_serial = lastsigned_serial;
+ }
+ if (serial_compare(master_serial, local_serial) != SERIAL_GREATER) {
+ conf_val_t val = conf_zone_get(data->conf, C_SERIAL_POLICY, data->zone->name);
+ local_serial = serial_next(local_serial, conf_opt(&val));
+ knot_soa_serial_set(chs->soa_to->rrs.rdata, local_serial);
+ } else {
+ local_serial = master_serial;
+ }
+ }
+
+ zone_update_t up = { 0 };
+ ret = zone_update_init(&up, data->zone, UPDATE_INCREMENTAL | UPDATE_STRICT);
+ if (ret != KNOT_EOK) {
+ return ret;
+ }
+
+ changeset_t *set = NULL;
+ WALK_LIST(set, data->ixfr.changesets) {
+ ret = zone_update_apply_changeset(&up, set);
+ if (ret != KNOT_EOK) {
+ zone_update_clear(&up);
+ IXFRIN_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "failed to apply changes to zone (%s)",
+ knot_strerror(ret));
+ return ret;
+ }
+ }
+
+ ret = xfr_validate(up.new_cont, data);
+ if (ret != KNOT_EOK) {
+ zone_update_clear(&up);
+ return ret;
+ }
+
+ conf_val_t val = conf_zone_get(data->conf, C_DNSSEC_SIGNING, data->zone->name);
+ bool dnssec_enable = conf_bool(&val);
+ if (dnssec_enable) {
+ zone_sign_reschedule_t resch = { .allow_rollover = true };
+ ret = knot_dnssec_sign_update(&up, &resch);
+ if (ret != KNOT_EOK) {
+ zone_update_clear(&up);
+ return ret;
+ }
+ event_dnssec_reschedule(data->conf, data->zone, &resch, true);
+ }
+
+ ret = zone_update_commit(data->conf, &up);
+
+ if (ret == KNOT_EOK) {
+ if (dnssec_enable && !EMPTY_LIST(data->ixfr.changesets)) {
+ ret = zone_set_master_serial(data->zone, master_serial);
+ if (ret == KNOT_EOK) {
+ ret = zone_set_lastsigned_serial(data->zone,
+ zone_contents_serial(up.zone->contents));
+ }
+ if (ret != KNOT_EOK) {
+ log_zone_warning(data->zone->name,
+ "unable to save master serial, future transfers might be broken");
+ }
+ }
+ xfr_log_publish(data->zone->name, data->remote, old_serial,
+ zone_contents_serial(up.zone->contents), false);
+ } else {
+ IXFRIN_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "failed to store changes (%s)", knot_strerror(ret));
+ }
+
+ zone_update_clear(&up);
+ return ret;
+}
+
+/*! \brief Stores starting SOA into changesets structure. */
+static int ixfr_solve_start(const knot_rrset_t *rr, struct refresh_data *data)
+{
+ assert(data->ixfr.final_soa == NULL);
+ if (rr->type != KNOT_RRTYPE_SOA) {
+ return KNOT_EMALF;
+ }
+
+ // Store terminal SOA
+ data->ixfr.final_soa = knot_rrset_copy(rr, data->mm);
+ if (data->ixfr.final_soa == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Initialize list for changes
+ init_list(&data->ixfr.changesets);
+
+ return KNOT_EOK;
+}
+
+/*! \brief Decides what to do with a starting SOA (deletions). */
+static int ixfr_solve_soa_del(const knot_rrset_t *rr, struct refresh_data *data)
+{
+ if (rr->type != KNOT_RRTYPE_SOA) {
+ return KNOT_EMALF;
+ }
+
+ // Create new changeset.
+ changeset_t *change = changeset_new(data->zone->name);
+ if (change == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ // Store SOA into changeset.
+ change->soa_from = knot_rrset_copy(rr, NULL);
+ if (change->soa_from == NULL) {
+ changeset_free(change);
+ return KNOT_ENOMEM;
+ }
+
+ // Add changeset.
+ add_tail(&data->ixfr.changesets, &change->n);
+
+ return KNOT_EOK;
+}
+
+/*! \brief Stores ending SOA into changeset. */
+static int ixfr_solve_soa_add(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm)
+{
+ if (rr->type != KNOT_RRTYPE_SOA) {
+ return KNOT_EMALF;
+ }
+
+ change->soa_to = knot_rrset_copy(rr, NULL);
+ if (change->soa_to == NULL) {
+ return KNOT_ENOMEM;
+ }
+
+ return KNOT_EOK;
+}
+
+/*! \brief Adds single RR into remove section of changeset. */
+static int ixfr_solve_del(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm)
+{
+ return changeset_add_removal(change, rr, 0);
+}
+
+/*! \brief Adds single RR into add section of changeset. */
+static int ixfr_solve_add(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm)
+{
+ return changeset_add_addition(change, rr, 0);
+}
+
+/*! \brief Decides what the next IXFR-in state should be. */
+static int ixfr_next_state(struct refresh_data *data, const knot_rrset_t *rr)
+{
+ const bool soa = (rr->type == KNOT_RRTYPE_SOA);
+ enum ixfr_state state = data->ixfr.proc->state;
+
+ if ((state == IXFR_SOA_ADD || state == IXFR_ADD) &&
+ knot_rrset_equal(rr, data->ixfr.final_soa, KNOT_RRSET_COMPARE_WHOLE)) {
+ return IXFR_DONE;
+ }
+
+ switch (state) {
+ case IXFR_START:
+ // Final SOA already stored or transfer start.
+ return data->ixfr.final_soa ? IXFR_SOA_DEL : IXFR_START;
+ case IXFR_SOA_DEL:
+ // Empty delete section or start of delete section.
+ return soa ? IXFR_SOA_ADD : IXFR_DEL;
+ case IXFR_SOA_ADD:
+ // Empty add section or start of add section.
+ return soa ? IXFR_SOA_DEL : IXFR_ADD;
+ case IXFR_DEL:
+ // End of delete section or continue.
+ return soa ? IXFR_SOA_ADD : IXFR_DEL;
+ case IXFR_ADD:
+ // End of add section or continue.
+ return soa ? IXFR_SOA_DEL : IXFR_ADD;
+ default:
+ assert(0);
+ return IXFR_INVALID;
+ }
+}
+
+/*!
+ * \brief Processes single RR according to current IXFR-in state. The states
+ * correspond with IXFR-in message structure, in the order they are
+ * mentioned in the code.
+ *
+ * \param rr RR to process.
+ * \param proc Processing context.
+ *
+ * \return KNOT_E*
+ */
+static int ixfr_step(const knot_rrset_t *rr, struct refresh_data *data)
+{
+ data->ixfr.proc->state = ixfr_next_state(data, rr);
+ changeset_t *change = TAIL(data->ixfr.changesets);
+
+ switch (data->ixfr.proc->state) {
+ case IXFR_START:
+ return ixfr_solve_start(rr, data);
+ case IXFR_SOA_DEL:
+ return ixfr_solve_soa_del(rr, data);
+ case IXFR_DEL:
+ return ixfr_solve_del(rr, change, data->mm);
+ case IXFR_SOA_ADD:
+ return ixfr_solve_soa_add(rr, change, data->mm);
+ case IXFR_ADD:
+ return ixfr_solve_add(rr, change, data->mm);
+ case IXFR_DONE:
+ return KNOT_EOK;
+ default:
+ return KNOT_ERROR;
+ }
+}
+
+static int ixfr_consume_rr(const knot_rrset_t *rr, struct refresh_data *data)
+{
+ if (knot_dname_in_bailiwick(rr->owner, data->zone->name) < 0) {
+ return KNOT_STATE_CONSUME;
+ }
+
+ int ret = ixfr_step(rr, data);
+ if (ret != KNOT_EOK) {
+ IXFRIN_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "failed (%s)", knot_strerror(ret));
+ return KNOT_STATE_FAIL;
+ }
+
+ data->change_size += knot_rrset_size(rr);
+ if (data->change_size / 2 > data->max_zone_size) {
+ IXFRIN_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "transfer size exceeded");
+ return KNOT_STATE_FAIL;
+ }
+
+ if (data->ixfr.proc->state == IXFR_DONE) {
+ return KNOT_STATE_DONE;
+ }
+
+ return KNOT_STATE_CONSUME;
+}
+
+/*!
+ * \brief Processes IXFR reply packet and fills in the changesets structure.
+ *
+ * \param pkt Packet containing the IXFR reply in wire format.
+ * \param adata Answer data, including processing context.
+ *
+ * \return KNOT_STATE_CONSUME, KNOT_STATE_DONE, KNOT_STATE_FAIL
+ */
+static int ixfr_consume_packet(knot_pkt_t *pkt, struct refresh_data *data)
+{
+ // Process RRs in the message.
+ const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+ int ret = KNOT_STATE_CONSUME;
+ for (uint16_t i = 0; i < answer->count && ret == KNOT_STATE_CONSUME; ++i) {
+ ret = ixfr_consume_rr(knot_pkt_rr(answer, i), data);
+ }
+ return ret;
+}
+
+static enum xfr_type determine_xfr_type(const knot_pktsection_t *answer,
+ uint32_t zone_serial, const knot_rrset_t *initial_soa)
+{
+ if (answer->count < 1) {
+ return XFR_TYPE_ERROR;
+ }
+
+ const knot_rrset_t *rr_one = knot_pkt_rr(answer, 0);
+ if (initial_soa != NULL) {
+ if (rr_one->type == KNOT_RRTYPE_SOA) {
+ return knot_rrset_equal(initial_soa, rr_one, KNOT_RRSET_COMPARE_WHOLE) ?
+ XFR_TYPE_AXFR : XFR_TYPE_IXFR;
+ }
+ return XFR_TYPE_AXFR;
+ }
+
+ if (answer->count == 1) {
+ if (rr_one->type == KNOT_RRTYPE_SOA) {
+ return serial_is_current(zone_serial, knot_soa_serial(rr_one->rrs.rdata)) ?
+ XFR_TYPE_UPTODATE : XFR_TYPE_UNDETERMINED;
+ }
+ return XFR_TYPE_ERROR;
+ }
+
+ const knot_rrset_t *rr_two = knot_pkt_rr(answer, 1);
+ if (answer->count == 2 && rr_one->type == KNOT_RRTYPE_SOA &&
+ knot_rrset_equal(rr_one, rr_two, KNOT_RRSET_COMPARE_WHOLE)) {
+ return XFR_TYPE_AXFR;
+ }
+
+ return (rr_one->type == KNOT_RRTYPE_SOA && rr_two->type != KNOT_RRTYPE_SOA) ?
+ XFR_TYPE_AXFR : XFR_TYPE_IXFR;
+}
+
+static int ixfr_consume(knot_pkt_t *pkt, struct refresh_data *data)
+{
+ assert(pkt);
+ assert(data);
+
+ // Check RCODE
+ if (knot_pkt_ext_rcode(pkt) != KNOT_RCODE_NOERROR) {
+ IXFRIN_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "server responded with error '%s'",
+ knot_pkt_ext_rcode_name(pkt));
+ return KNOT_STATE_FAIL;
+ }
+
+ // Initialize with first packet
+ if (data->ixfr.proc == NULL) {
+ const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+
+ uint32_t master_serial;
+ int ret = zone_get_master_serial(data->zone, &master_serial);
+ if (ret != KNOT_EOK) {
+ log_zone_error(data->zone->name, "Failed reading master's serial"
+ "from KASP DB (%s)", knot_strerror(ret));
+ return KNOT_STATE_FAIL;
+ }
+ data->xfr_type = determine_xfr_type(answer, master_serial,
+ data->initial_soa_copy);
+ switch (data->xfr_type) {
+ case XFR_TYPE_ERROR:
+ IXFRIN_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "malformed response SOA");
+ return KNOT_STATE_FAIL;
+ case XFR_TYPE_UNDETERMINED:
+ // Store the SOA and check with next packet
+ data->initial_soa_copy = knot_rrset_copy(knot_pkt_rr(answer, 0), data->mm);
+ if (data->initial_soa_copy == NULL) {
+ return KNOT_STATE_FAIL;
+ }
+ xfr_stats_add(&data->stats, pkt->size);
+ return KNOT_STATE_CONSUME;
+ case XFR_TYPE_AXFR:
+ IXFRIN_LOG(LOG_INFO, data->zone->name, data->remote,
+ "receiving AXFR-style IXFR");
+ return axfr_consume(pkt, data);
+ case XFR_TYPE_UPTODATE:
+ IXFRIN_LOG(LOG_INFO, data->zone->name, data->remote,
+ "zone is up-to-date");
+ xfr_stats_begin(&data->stats);
+ xfr_stats_add(&data->stats, pkt->size);
+ xfr_stats_end(&data->stats);
+ return KNOT_STATE_DONE;
+ case XFR_TYPE_IXFR:
+ break;
+ default:
+ assert(0);
+ return KNOT_STATE_FAIL;
+ }
+
+ ret = ixfr_init(data);
+ if (ret != KNOT_EOK) {
+ IXFRIN_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "failed to initialize (%s)", knot_strerror(ret));
+ return KNOT_STATE_FAIL;
+ }
+
+ IXFRIN_LOG(LOG_INFO, data->zone->name, data->remote, "starting");
+ xfr_stats_begin(&data->stats);
+ data->change_size = 0;
+ }
+
+ int next;
+ // Process saved SOA if existing
+ if (data->initial_soa_copy != NULL) {
+ next = ixfr_consume_rr(data->initial_soa_copy, data);
+ knot_rrset_free(data->initial_soa_copy, data->mm);
+ data->initial_soa_copy = NULL;
+ if (next != KNOT_STATE_CONSUME) {
+ return next;
+ }
+ }
+
+ // Process answer packet
+ xfr_stats_add(&data->stats, pkt->size);
+ next = ixfr_consume_packet(pkt, data);
+
+ // Finalize
+ if (next == KNOT_STATE_DONE) {
+ xfr_stats_end(&data->stats);
+ }
+
+ return next;
+}
+
+static int soa_query_produce(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+ struct refresh_data *data = layer->data;
+
+ query_init_pkt(pkt);
+
+ int r = knot_pkt_put_question(pkt, data->zone->name, KNOT_CLASS_IN, KNOT_RRTYPE_SOA);
+ if (r != KNOT_EOK) {
+ return KNOT_STATE_FAIL;
+ }
+
+ r = query_put_edns(pkt, &data->edns);
+ if (r != KNOT_EOK) {
+ return KNOT_STATE_FAIL;
+ }
+
+ return KNOT_STATE_CONSUME;
+}
+
+static int soa_query_consume(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+ struct refresh_data *data = layer->data;
+
+ if (knot_pkt_ext_rcode(pkt) != KNOT_RCODE_NOERROR) {
+ REFRESH_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "server responded with error '%s'",
+ knot_pkt_ext_rcode_name(pkt));
+ return KNOT_STATE_FAIL;
+ }
+
+ const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+ const knot_rrset_t *rr = answer->count == 1 ? knot_pkt_rr(answer, 0) : NULL;
+ if (!rr || rr->type != KNOT_RRTYPE_SOA || rr->rrs.count != 1) {
+ REFRESH_LOG(LOG_WARNING, data->zone->name, data->remote,
+ "malformed message");
+ return KNOT_STATE_FAIL;
+ }
+
+ uint32_t local_serial;
+ int ret = zone_get_master_serial(data->zone, &local_serial);
+ if (ret != KNOT_EOK) {
+ log_zone_error(data->zone->name, "Failed reading master's serial"
+ "from KASP DB (%s)", knot_strerror(ret));
+ return KNOT_STATE_FAIL;
+ }
+ uint32_t remote_serial = knot_soa_serial(rr->rrs.rdata);
+ bool current = serial_is_current(local_serial, remote_serial);
+ bool master_uptodate = serial_is_current(remote_serial, local_serial);
+
+ REFRESH_LOG(LOG_INFO, data->zone->name, data->remote,
+ "remote serial %u, %s", remote_serial,
+ current ? (master_uptodate ? "zone is up-to-date" :
+ "master is outdated") : "zone is outdated");
+
+ if (current) {
+ return master_uptodate ? KNOT_STATE_DONE : KNOT_STATE_FAIL;
+ } else {
+ data->state = STATE_TRANSFER;
+ return KNOT_STATE_RESET;
+ }
+}
+
+static int transfer_produce(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+ struct refresh_data *data = layer->data;
+
+ bool ixfr = (data->xfr_type == XFR_TYPE_IXFR);
+
+ query_init_pkt(pkt);
+ knot_pkt_put_question(pkt, data->zone->name, KNOT_CLASS_IN,
+ ixfr ? KNOT_RRTYPE_IXFR : KNOT_RRTYPE_AXFR);
+
+ if (ixfr) {
+ assert(data->soa);
+ knot_rrset_t *sending_soa = knot_rrset_copy(data->soa, data->mm);
+ uint32_t master_serial;
+ int ret = zone_get_master_serial(data->zone, &master_serial);
+ if (ret != KNOT_EOK) {
+ log_zone_error(data->zone->name, "Failed reading master's serial"
+ "from KASP DB (%s)", knot_strerror(ret));
+ }
+ if (sending_soa == NULL || ret != KNOT_EOK) {
+ knot_rrset_free(sending_soa, data->mm);
+ return KNOT_STATE_FAIL;
+ }
+ knot_soa_serial_set(sending_soa->rrs.rdata, master_serial);
+ knot_pkt_begin(pkt, KNOT_AUTHORITY);
+ knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, sending_soa, 0);
+ knot_rrset_free(sending_soa, data->mm);
+ }
+
+ query_put_edns(pkt, &data->edns);
+
+ return KNOT_STATE_CONSUME;
+}
+
+static int transfer_consume(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+ struct refresh_data *data = layer->data;
+
+ int next = (data->xfr_type == XFR_TYPE_AXFR) ? axfr_consume(pkt, data) :
+ ixfr_consume(pkt, data);
+
+ // Transfer completed
+ if (next == KNOT_STATE_DONE) {
+ // Log transfer even if we still can fail
+ xfr_log_finished(data->zone->name,
+ data->xfr_type == XFR_TYPE_IXFR ||
+ data->xfr_type == XFR_TYPE_UPTODATE ?
+ LOG_OPERATION_IXFR : LOG_OPERATION_AXFR,
+ LOG_DIRECTION_IN, data->remote, &data->stats);
+
+ /*
+ * TODO: Move finialization into finish
+ * callback. And update requestor to allow reset from fallback
+ * as we need IXFR to AXFR failover.
+ */
+ if (tsig_unsigned_count(layer->tsig) != 0) {
+ return KNOT_STATE_FAIL;
+ }
+
+ // Finalize and publish the zone
+ int ret;
+ switch (data->xfr_type) {
+ case XFR_TYPE_IXFR:
+ ret = ixfr_finalize(data);
+ break;
+ case XFR_TYPE_AXFR:
+ ret = axfr_finalize(data);
+ break;
+ default:
+ return next;
+ }
+ if (ret == KNOT_EOK) {
+ data->updated = true;
+ } else {
+ next = KNOT_STATE_FAIL;
+ }
+ }
+
+ return next;
+}
+
+static int refresh_begin(knot_layer_t *layer, void *_data)
+{
+ layer->data = _data;
+ struct refresh_data *data = _data;
+
+ if (data->soa) {
+ data->state = STATE_SOA_QUERY;
+ data->xfr_type = XFR_TYPE_IXFR;
+ data->initial_soa_copy = NULL;
+ } else {
+ data->state = STATE_TRANSFER;
+ data->xfr_type = XFR_TYPE_AXFR;
+ data->initial_soa_copy = NULL;
+ }
+
+ return KNOT_STATE_PRODUCE;
+}
+
+static int refresh_produce(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+ struct refresh_data *data = layer->data;
+
+ switch (data->state) {
+ case STATE_SOA_QUERY: return soa_query_produce(layer, pkt);
+ case STATE_TRANSFER: return transfer_produce(layer, pkt);
+ default:
+ return KNOT_STATE_FAIL;
+ }
+}
+
+static int refresh_consume(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+ struct refresh_data *data = layer->data;
+
+ switch (data->state) {
+ case STATE_SOA_QUERY: return soa_query_consume(layer, pkt);
+ case STATE_TRANSFER: return transfer_consume(layer, pkt);
+ default:
+ return KNOT_STATE_FAIL;
+ }
+}
+
+static int refresh_reset(knot_layer_t *layer)
+{
+ return KNOT_STATE_PRODUCE;
+}
+
+static int refresh_finish(knot_layer_t *layer)
+{
+ struct refresh_data *data = layer->data;
+
+ // clean processing context
+ axfr_cleanup(data);
+ ixfr_cleanup(data);
+
+ return KNOT_STATE_NOOP;
+}
+
+static const knot_layer_api_t REFRESH_API = {
+ .begin = refresh_begin,
+ .produce = refresh_produce,
+ .consume = refresh_consume,
+ .reset = refresh_reset,
+ .finish = refresh_finish,
+};
+
+static size_t max_zone_size(conf_t *conf, const knot_dname_t *zone)
+{
+ conf_val_t val = conf_zone_get(conf, C_MAX_ZONE_SIZE, zone);
+ return conf_int(&val);
+}
+
+typedef struct {
+ bool force_axfr;
+ bool send_notify;
+} try_refresh_ctx_t;
+
+static int try_refresh(conf_t *conf, zone_t *zone, const conf_remote_t *master, void *ctx)
+{
+ // TODO: Abstract interface to issue DNS queries. This is almost copy-pasted.
+
+ assert(zone);
+ assert(master);
+ assert(ctx);
+
+ try_refresh_ctx_t *trctx = ctx;
+
+ knot_rrset_t soa = { 0 };
+ if (zone->contents) {
+ soa = node_rrset(zone->contents->apex, KNOT_RRTYPE_SOA);
+ }
+
+ struct refresh_data data = {
+ .zone = zone,
+ .conf = conf,
+ .remote = (struct sockaddr *)&master->addr,
+ .soa = zone->contents && !trctx->force_axfr ? &soa : NULL,
+ .max_zone_size = max_zone_size(conf, zone->name),
+ };
+
+ query_edns_data_init(&data.edns, conf, zone->name, master->addr.ss_family);
+
+ struct knot_requestor requestor;
+ knot_requestor_init(&requestor, &REFRESH_API, &data, NULL);
+
+ knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, NULL);
+ if (!pkt) {
+ knot_requestor_clear(&requestor);
+ return KNOT_ENOMEM;
+ }
+
+ const struct sockaddr *dst = (struct sockaddr *)&master->addr;
+ const struct sockaddr *src = (struct sockaddr *)&master->via;
+ struct knot_request *req = knot_request_make(NULL, dst, src, pkt, &master->key, 0);
+ if (!req) {
+ knot_request_free(req, NULL);
+ knot_requestor_clear(&requestor);
+ return KNOT_ENOMEM;
+ }
+
+ int timeout = conf->cache.srv_tcp_reply_timeout * 1000;
+
+ int ret;
+
+ // while loop runs 0x or 1x; IXFR to AXFR failover
+ while ((ret = knot_requestor_exec(&requestor, req, timeout)) != KNOT_EOK &&
+ data.xfr_type == XFR_TYPE_IXFR) {
+ REFRESH_LOG(LOG_WARNING, data.zone->name, data.remote,
+ "fallback to AXFR");
+ ixfr_cleanup(&data);
+ data.xfr_type = XFR_TYPE_AXFR;
+ requestor.layer.state = KNOT_STATE_RESET;
+ requestor.layer.flags |= KNOT_RQ_LAYER_CLOSE;
+ }
+ knot_request_free(req, NULL);
+ knot_requestor_clear(&requestor);
+
+ if (ret == KNOT_EOK) {
+ trctx->send_notify = data.updated;
+ }
+
+ return ret;
+}
+
+static int64_t min_refresh_interval(conf_t *conf, const knot_dname_t *zone)
+{
+ conf_val_t val = conf_zone_get(conf, C_MIN_REFRESH_INTERVAL, zone);
+ return conf_int(&val);
+}
+
+static int64_t max_refresh_interval(conf_t *conf, const knot_dname_t *zone)
+{
+ conf_val_t val = conf_zone_get(conf, C_MAX_REFRESH_INTERVAL, zone);
+ return conf_int(&val);
+}
+
+int event_refresh(conf_t *conf, zone_t *zone)
+{
+ assert(zone);
+
+ if (!zone_is_slave(conf, zone)) {
+ return KNOT_EOK;
+ }
+
+ try_refresh_ctx_t trctx = { 0 };
+
+ // TODO: Flag on zone is ugly. Event specific parameters would be nice.
+ if (zone->flags & ZONE_FORCE_AXFR) {
+ zone->flags &= ~ZONE_FORCE_AXFR;
+ trctx.force_axfr = true;
+ }
+
+ int ret = zone_master_try(conf, zone, try_refresh, &trctx, "refresh");
+ if (ret != KNOT_EOK) {
+ log_zone_error(zone->name, "refresh, failed (%s)", knot_strerror(ret));
+ }
+
+ time_t now = time(NULL);
+ const knot_rdataset_t *soa = zone_soa(zone);
+
+ if (ret == KNOT_EOK) {
+ zone->timers.soa_expire = knot_soa_expire(soa->rdata);
+ zone->timers.last_refresh = now;
+ zone->timers.next_refresh = now + knot_soa_refresh(soa->rdata);
+ } else {
+ time_t next = 0;
+ if (soa) {
+ next = knot_soa_retry(soa->rdata);
+ } else {
+ next = bootstrap_next(&zone->timers);
+ }
+ zone->timers.next_refresh = now + next;
+ }
+
+ /* Check for allowed refresh interval limits. */
+ int64_t min_refresh = min_refresh_interval(conf, zone->name);
+ if(zone->timers.next_refresh < now + min_refresh) {
+ zone->timers.next_refresh = now + min_refresh;
+ }
+ int64_t max_refresh = max_refresh_interval(conf, zone->name);
+ if(zone->timers.next_refresh > now + max_refresh) {
+ zone->timers.next_refresh = now + max_refresh;
+ }
+
+ /* Rechedule events. */
+ replan_from_timers(conf, zone);
+ if (trctx.send_notify) {
+ zone_events_schedule_at(zone, ZONE_EVENT_NOTIFY, time(NULL) + 1);
+ }
+
+ return KNOT_EOK;
+}