From d1b90b243564043c853b19ad6295203925900921 Mon Sep 17 00:00:00 2001
From: Jan Vcelak <jan.vcelak@nic.cz>
Date: Tue, 13 Sep 2016 18:30:48 +0200
Subject: [PATCH] huge events refactor

---
 Knot.files                          |   3 +-
 src/Makefile.am                     |   2 +
 src/knot/events/handlers/dnssec.c   |   2 +-
 src/knot/events/handlers/notify.c   | 130 ++++-
 src/knot/events/handlers/refresh.c  | 873 ++++++++++++++++++++++++----
 src/knot/modules/dnsproxy.c         |   2 +-
 src/knot/nameserver/axfr.c          | 260 +--------
 src/knot/nameserver/axfr.h          |  64 +-
 src/knot/nameserver/internet.c      |  71 ---
 src/knot/nameserver/internet.h      |  12 -
 src/knot/nameserver/ixfr.c          | 466 +--------------
 src/knot/nameserver/ixfr.h          |  45 +-
 src/knot/nameserver/log.h           |  69 ++-
 src/knot/nameserver/notify.c        |  41 +-
 src/knot/nameserver/notify.h        |   8 -
 src/knot/nameserver/process_query.c |   1 -
 src/knot/nameserver/update.c        |  17 +-
 src/knot/nameserver/xfr.c           |  87 +++
 src/knot/nameserver/xfr.h           |  69 +++
 src/knot/query/query.c              | 361 ++----------
 src/knot/query/query.h              |  69 ++-
 src/knot/query/requestor.c          |   9 +
 src/knot/zone/zone.c                |  24 -
 src/knot/zone/zone.h                |   5 +-
 src/libknot/packet/pkt.h            |   2 +
 tests/.gitignore                    |   1 -
 tests/Makefile.am                   |   2 -
 tests/process_answer.c              | 167 ------
 tests/requestor.c                   |   2 +-
 29 files changed, 1307 insertions(+), 1557 deletions(-)
 create mode 100644 src/knot/nameserver/xfr.c
 create mode 100644 src/knot/nameserver/xfr.h
 delete mode 100644 tests/process_answer.c

diff --git a/Knot.files b/Knot.files
index 56f55cb23..1e428bcf6 100644
--- a/Knot.files
+++ b/Knot.files
@@ -292,6 +292,8 @@ src/knot/nameserver/tsig_ctx.c
 src/knot/nameserver/tsig_ctx.h
 src/knot/nameserver/update.c
 src/knot/nameserver/update.h
+src/knot/nameserver/xfr.c
+src/knot/nameserver/xfr.h
 src/knot/query/capture.c
 src/knot/query/capture.h
 src/knot/query/layer.c
@@ -576,7 +578,6 @@ tests/libknot/test_ypscheme.c
 tests/libknot/test_yptrafo.c
 tests/modules/online_sign.c
 tests/node.c
-tests/process_answer.c
 tests/process_query.c
 tests/query_module.c
 tests/requestor.c
diff --git a/src/Makefile.am b/src/Makefile.am
index caea4d8bd..c482c9c6b 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -308,6 +308,8 @@ libknotd_la_SOURCES =				\
 	knot/nameserver/tsig_ctx.h		\
 	knot/nameserver/update.c		\
 	knot/nameserver/update.h		\
+	knot/nameserver/xfr.c			\
+	knot/nameserver/xfr.h			\
 	knot/query/capture.c			\
 	knot/query/capture.h			\
 	knot/query/layer.c			\
diff --git a/src/knot/events/handlers/dnssec.c b/src/knot/events/handlers/dnssec.c
index ce63d2e66..d44ad0d01 100644
--- a/src/knot/events/handlers/dnssec.c
+++ b/src/knot/events/handlers/dnssec.c
@@ -85,7 +85,7 @@ int event_dnssec(conf_t *conf, zone_t *zone)
 		apply_init_ctx(&a_ctx, NULL, APPLY_STRICT);
 
 		zone_contents_t *new_contents = NULL;
-		int ret = apply_changeset(&a_ctx, zone, &ch, &new_contents);
+		int ret = apply_changeset(&a_ctx, zone->contents, &ch, &new_contents);
 		if (ret != KNOT_EOK) {
 			log_zone_error(zone->name, "DNSSEC, failed to sign zone (%s)",
 				       knot_strerror(ret));
diff --git a/src/knot/events/handlers/notify.c b/src/knot/events/handlers/notify.c
index f0ede603c..bc3d4392b 100644
--- a/src/knot/events/handlers/notify.c
+++ b/src/knot/events/handlers/notify.c
@@ -19,35 +19,149 @@
 #include "knot/common/log.h"
 #include "knot/conf/conf.h"
 #include "knot/query/query.h"
+#include "knot/query/requestor.h"
 #include "knot/zone/zone.h"
 #include "libknot/errcode.h"
 
-#define NOTIFY_LOG(priority, zone, remote, msg...) \
-	ZONE_QUERY_LOG(priority, zone, remote, "NOTIFY, outgoing", msg);
+/*!
+ * \brief NOTIFY message processing data.
+ */
+struct notify_data {
+	const knot_dname_t *zone;
+	const knot_rrset_t *soa;
+	const struct sockaddr_storage *remote;
+	uint16_t response_rcode;
+};
+
+static int notify_begin(knot_layer_t *layer, void *params)
+{
+	layer->data = params;
+
+	return KNOT_STATE_PRODUCE;
+}
+
+static int notify_produce(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+	struct notify_data *data = layer->data;
+
+	// mandatory: NOTIFY opcode, AA flag, SOA qtype
+	query_init_pkt(pkt);
+	knot_wire_set_opcode(pkt->wire, KNOT_OPCODE_NOTIFY);
+	knot_wire_set_aa(pkt->wire);
+	knot_pkt_put_question(pkt, data->zone, KNOT_CLASS_IN, KNOT_RRTYPE_SOA);
+
+	// unsecure hint: new SOA
+	if (data->soa) {
+		knot_pkt_begin(pkt, KNOT_ANSWER);
+		knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, data->soa, 0);
+	}
+
+	return KNOT_STATE_CONSUME;
+}
+
+static int notify_consume(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+	struct notify_data *data = layer->data;
+
+	data->response_rcode = knot_pkt_get_ext_rcode(pkt);
+	if (data->response_rcode != KNOT_RCODE_NOERROR) {
+		return KNOT_STATE_FAIL;
+	}
+
+	return KNOT_STATE_DONE;
+}
+
+static const knot_layer_api_t NOTIFY_API = {
+	.begin = notify_begin,
+	.produce = notify_produce,
+	.consume = notify_consume,
+};
+
+static int send_notify(zone_t *zone, const knot_rrset_t *soa,
+                       const conf_remote_t *slave, int timeout, uint16_t *rcode)
+{
+	struct notify_data data = {
+		.zone = zone->name,
+		.soa = soa,
+		.remote = &slave->addr,
+	};
+
+	struct knot_requestor requestor = { 0 };
+	knot_requestor_init(&requestor, &NOTIFY_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 *)&slave->addr;
+	const struct sockaddr *src = (struct sockaddr *)&slave->via;
+	struct knot_request *req = knot_request_make(NULL, dst, src, pkt, &slave->key, 0);
+	if (!req) {
+		knot_request_free(req, NULL);
+		knot_requestor_clear(&requestor);
+		return KNOT_ENOMEM;
+	}
+
+	int ret = knot_requestor_exec(&requestor, req, timeout);
+	knot_request_free(req, NULL);
+	knot_requestor_clear(&requestor);
+
+	*rcode = data.response_rcode;
+
+	return ret;
+}
+
+#define NOTIFY_LOG(priority, zone, remote, fmt, ...) \
+	ns_log(priority, zone, LOG_OPERATION_NOTIFY, LOG_DIRECTION_OUT, remote, \
+	       fmt, ## __VA_ARGS__)
+
+static void log_notify_result(int ret, uint16_t rcode, const knot_dname_t *zone,
+                              const struct sockaddr_storage *_remote, uint32_t serial)
+{
+	const struct sockaddr *remote = (struct sockaddr *)_remote;
+
+	if (ret == KNOT_EOK) {
+		NOTIFY_LOG(LOG_INFO, zone, remote, "serial %u", serial);
+	} else if (rcode == 0) {
+		NOTIFY_LOG(LOG_WARNING, zone, remote, "failed (%s)", knot_strerror(ret));
+	} else {
+		const knot_lookup_t *lut = knot_lookup_by_id(knot_rcode_names, rcode);
+		if (lut) {
+			NOTIFY_LOG(LOG_WARNING, zone, remote, "server responded with %s", lut->name);
+		} else {
+			NOTIFY_LOG(LOG_WARNING, zone, remote, "server responded with RCODE %u", rcode);
+		}
+	}
+}
 
 int event_notify(conf_t *conf, zone_t *zone)
 {
 	assert(zone);
 
-	/* Check zone contents. */
 	if (zone_contents_is_empty(zone->contents)) {
 		return KNOT_EOK;
 	}
 
-	/* Walk through configured remotes and send messages. */
+	// NOTIFY content
+	int timeout = conf->cache.srv_tcp_reply_timeout * 1000;
+	knot_rrset_t soa = node_rrset(zone->contents->apex, KNOT_RRTYPE_SOA);
+	uint32_t serial = zone_contents_serial(zone->contents);
+
+	// send NOTIFY to each remote, use working address
 	conf_val_t notify = conf_zone_get(conf, C_NOTIFY, zone->name);
 	while (notify.code == KNOT_EOK) {
 		conf_val_t addr = conf_id_get(conf, C_RMT, C_ADDR, &notify);
 		size_t addr_count = conf_val_count(&addr);
 
 		for (int i = 0; i < addr_count; i++) {
+			uint16_t rcode = 0;
 			conf_remote_t slave = conf_remote(conf, &notify, i);
-			int ret = zone_query_execute(conf, zone, KNOT_QUERY_NOTIFY, &slave);
+			int ret = send_notify(zone, &soa, &slave, timeout, &rcode);
+			log_notify_result(ret, rcode, zone->name, &slave.addr, serial);
 			if (ret == KNOT_EOK) {
-				NOTIFY_LOG(LOG_INFO, zone, &slave, "serial %u", zone_contents_serial(zone->contents));
 				break;
-			} else {
-				NOTIFY_LOG(LOG_WARNING, zone, &slave, "failed (%s)", knot_strerror(ret));
 			}
 		}
 
diff --git a/src/knot/events/handlers/refresh.c b/src/knot/events/handlers/refresh.c
index c941d55bf..e95214237 100644
--- a/src/knot/events/handlers/refresh.c
+++ b/src/knot/events/handlers/refresh.c
@@ -16,177 +16,847 @@
 
 #include <assert.h>
 #include <stdint.h>
+#include <urcu.h>
 
 #include "contrib/trim.h"
 #include "dnssec/random.h"
 #include "knot/common/log.h"
 #include "knot/conf/conf.h"
+#include "knot/query/layer.h"
 #include "knot/query/query.h"
+#include "knot/updates/apply.h"
 #include "knot/zone/zone.h"
 #include "libknot/errcode.h"
 
-#define BOOTSTRAP_RETRY (30) /*!< Interval between AXFR bootstrap retries. */
-#define BOOTSTRAP_MAXTIME (24*60*60) /*!< Maximum AXFR retry cap of 24 hours. */
+/// TODO. Memory context.
+/// TODO. Adjusting.
+
+#include "contrib/mempattern.h" // mm_free()
+#include "knot/nameserver/ixfr.h" // struct ixfr_proc
+#include "knot/zone/zonefile.h" // err_handler_logger_t
+#include "knot/zone/serial.h" // serial_compare (move to libknot)
+
+/*!
+ * \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)
+
+enum refresh_state {
+	REFRESH_STATE_INVALID = 0,
+	REFRESH_STATE_SOA_QUERY,
+	REFRESH_STATE_TRANSFER,
+};
+
+struct refresh_result {
+	zone_contents_t *zone;  //!< AXFR, new zone
+	list_t changesets;      //!< IXFR, zone updates
+};
+
+struct refresh_data {
+	enum refresh_state state;         //!< Event processing state.
+	struct refresh_result result;     //!< Result of the refresh event.
+	bool is_ixfr;                     //!< Transfer is IXFR not AXFR.
+
+	const knot_dname_t *zone;         //!< Zone name.
+	const struct sockaddr *remote;    //!< Remote endpoint.
+	struct query_edns_data edns;      //!< EDNS data to be used in queries.
+	const knot_rrset_t *soa;          //!< Local SOA (NULL for AXFR).
+
+	struct xfr_stats stats;           //!< Transfer statistics.
+
+	struct {
+		struct ixfr_proc *proc;   //!< IXFR processing context.
+		knot_rrset_t *final_soa;  //!< SOA denoting end of transfer.
+	} ixfr;
+
+	knot_mm_t *mm; // TODO: check where this should be used
+};
+
+static const char *rcode_name(uint16_t rcode)
+{
+	const knot_lookup_t *lut = knot_lookup_by_id(knot_rcode_names, rcode);
+	return lut ? lut->name : "unknown RCODE";
+}
 
-#define LOG_TRANSFER(severity, pkt_type, msg, ...) \
-	if (pkt_type == KNOT_QUERY_AXFR) { \
-		ZONE_QUERY_LOG(severity, zone, master, "AXFR, incoming", msg, ##__VA_ARGS__); \
-	} else { \
-		ZONE_QUERY_LOG(severity, zone, master, "IXFR, incoming", msg, ##__VA_ARGS__); \
+static bool serial_is_current(uint32_t local_serial, uint32_t remote_serial)
+{
+	return serial_compare(local_serial, remote_serial) >= 0;
+}
+
+static void refresh_result_init(struct refresh_result *result)
+{
+	result->zone = NULL;
+	init_list(&result->changesets);
+}
+
+static void refresh_result_cleanup(struct refresh_result *result)
+{
+	zone_contents_deep_free(&result->zone);
+	changesets_free(&result->changesets);
+}
+
+static bool refresh_result_empty(const struct refresh_result *result)
+{
+	return result->zone == NULL && EMPTY_LIST(result->changesets);
+}
+
+static int axfr_consume_packet(knot_pkt_t *pkt, zone_contents_t *zone)
+{
+	assert(pkt);
+	assert(zone);
+
+	zcreator_t zc = { .z = zone, .master = false, .ret = KNOT_EOK };
+
+	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+	const knot_rrset_t *answer_rr = knot_pkt_rr(answer, 0);
+	for (uint16_t i = 0; i < answer->count; ++i) {
+		if (answer_rr[i].type == KNOT_RRTYPE_SOA &&
+		    node_rrtype_exists(zc.z->apex, KNOT_RRTYPE_SOA)) {
+			return KNOT_STATE_DONE;
+		}
+
+		int ret = zcreator_step(&zc, &answer_rr[i]);
+		if (ret != KNOT_EOK) {
+			return KNOT_STATE_FAIL;
+		}
 	}
 
-/*! \brief Get next bootstrap interval. */
-uint32_t bootstrap_next(uint32_t interval)
+	return KNOT_STATE_CONSUME;
+}
+
+static int axfr_consume(knot_pkt_t *pkt, struct refresh_data *data)
 {
-	interval *= 2;
-	interval += dnssec_random_uint32_t() % BOOTSTRAP_RETRY;
-	if (interval > BOOTSTRAP_MAXTIME) {
-		interval = BOOTSTRAP_MAXTIME;
+	assert(pkt);
+	assert(data);
+
+	// Check RCODE
+	uint16_t rcode = knot_pkt_get_ext_rcode(pkt);
+	if (rcode != KNOT_RCODE_NOERROR) {
+		AXFRIN_LOG(LOG_WARNING, data->zone, data->remote,
+		           "server responded with %s", rcode_name(rcode));
+		return KNOT_STATE_FAIL;
 	}
-	return interval;
+
+	// Initialize with first packet
+	if (data->result.zone == NULL) {
+		data->result.zone = zone_contents_new(data->zone);
+		if (!data->result.zone) {
+			AXFRIN_LOG(LOG_WARNING, data->zone, data->remote,
+			           "failed to initialize (%s)", knot_strerror(KNOT_ENOMEM));
+			return KNOT_STATE_FAIL;
+		}
+
+		AXFRIN_LOG(LOG_INFO, data->zone, data->remote, "starting");
+		xfr_stats_begin(&data->stats);
+	}
+
+	// Process answer packet
+	xfr_stats_add(&data->stats, pkt->size);
+	int next = axfr_consume_packet(pkt, data->result.zone);
+
+	// Finalize
+	if (next == KNOT_STATE_DONE) {
+		int ret = zone_contents_adjust_full(data->result.zone);
+		if (ret != KNOT_EOK) {
+			return KNOT_STATE_FAIL;
+		}
+
+		xfr_stats_end(&data->stats);
+	}
+
+	return next;
 }
 
-/*! \brief Get SOA from zone. */
-static const knot_rdataset_t *zone_soa(zone_t *zone)
+/*! \brief Initialize IXFR-in processing context. */
+static int ixfr_init(struct refresh_data *data)
 {
-	return node_rdataset(zone->contents->apex, KNOT_RRTYPE_SOA);
+	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;
+
+	return KNOT_EOK;
 }
 
-static int try_refresh(conf_t *conf, zone_t *zone, const conf_remote_t *master, void *ctx)
+/*! \brief Clean up data allocated by IXFR-in processing. */
+static void ixfr_cleanup(struct refresh_data *data)
 {
-	assert(zone);
-	assert(master);
+	knot_rrset_free(&data->ixfr.final_soa, data->mm);
+	mm_free(data->mm, data->ixfr.proc);
+	data->ixfr.proc = NULL;
+}
 
-	int ret = zone_query_execute(conf, zone, KNOT_QUERY_NORMAL, master);
-	if (ret != KNOT_EOK && ret != KNOT_LAYER_ERROR) {
-		ZONE_QUERY_LOG(LOG_WARNING, zone, master, "refresh, outgoing",
-		               "failed (%s)", knot_strerror(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;
 	}
 
-	return ret;
+	// 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->result.changesets);
+
+	return KNOT_EOK;
 }
 
-/*! \brief Schedule expire event, unless it is already scheduled. */
-static void start_expire_timer(conf_t *conf, zone_t *zone, const knot_rdataset_t *soa)
+/*! \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 (zone_events_is_scheduled(zone, ZONE_EVENT_EXPIRE)) {
-		return;
+	if (rr->type != KNOT_RRTYPE_SOA) {
+		return KNOT_EMALF;
 	}
 
-	zone_events_schedule(zone, ZONE_EVENT_EXPIRE, knot_soa_expire(soa));
+	// Create new changeset.
+	changeset_t *change = changeset_new(data->zone);
+	if (change == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	// Store SOA into changeset.
+	change->soa_from = knot_rrset_copy(rr, NULL);
+	if (change->soa_from == NULL) {
+		changeset_clear(change);
+		return KNOT_ENOMEM;
+	}
+
+	// Add changeset.
+	add_tail(&data->result.changesets, &change->n);
+
+	return KNOT_EOK;
 }
 
-int event_refresh(conf_t *conf, zone_t *zone)
+/*! \brief Stores ending SOA into changeset. */
+static int ixfr_solve_soa_add(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm)
 {
-	assert(zone);
+	if (rr->type != KNOT_RRTYPE_SOA) {
+		return KNOT_EMALF;
+	}
 
-	/* Ignore if not slave zone. */
-	if (!zone_is_slave(conf, zone)) {
-		return KNOT_EOK;
+	change->soa_to = knot_rrset_copy(rr, NULL);
+	if (change->soa_to == NULL) {
+		return KNOT_ENOMEM;
 	}
 
-	if (zone_contents_is_empty(zone->contents)) {
-		/* No contents, schedule retransfer now. */
-		zone_events_schedule(zone, ZONE_EVENT_XFER, ZONE_EVENT_NOW);
+	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->result.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;
 	}
+}
 
-	int ret = zone_master_try(conf, zone, try_refresh, NULL, "refresh");
-	const knot_rdataset_t *soa = zone_soa(zone);
-	if (ret != KNOT_EOK) {
-		log_zone_error(zone->name, "refresh, failed (%s)",
-		               knot_strerror(ret));
-		/* Schedule next retry. */
-		zone_events_schedule(zone, ZONE_EVENT_REFRESH, knot_soa_retry(soa));
-		start_expire_timer(conf, zone, soa);
+/*!
+ * \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);
+	for (uint16_t i = 0; i < answer->count; ++i) {
+		const knot_rrset_t *rr = knot_pkt_rr(answer, i);
+		if (!knot_dname_in(data->zone, rr->owner)) {
+			continue;
+		}
+
+		int ret = ixfr_step(rr, data);
+		if (ret != KNOT_EOK) {
+			IXFRIN_LOG(LOG_WARNING, data->zone, data->remote,
+			           "failed (%s)", knot_strerror(ret));
+			return KNOT_STATE_FAIL;
+		}
+
+		if (data->ixfr.proc->state == IXFR_DONE) {
+			return KNOT_STATE_DONE;
+		}
+	}
+
+	return KNOT_STATE_CONSUME;
+}
+
+static bool ixfr_check_header(const knot_pktsection_t *answer)
+{
+	return answer->count >= 1 &&
+	       knot_pkt_rr(answer, 0)->type == KNOT_RRTYPE_SOA;
+}
+
+static bool ixfr_is_axfr(const knot_pktsection_t *answer)
+{
+	return answer->count >= 2 &&
+	       knot_pkt_rr(answer, 0)->type == KNOT_RRTYPE_SOA &&
+	       knot_pkt_rr(answer, 1)->type != KNOT_RRTYPE_SOA;
+}
+
+static int ixfr_consume(knot_pkt_t *pkt, struct refresh_data *data)
+{
+	assert(pkt);
+	assert(data);
+
+	// Check RCODE
+	uint8_t rcode = knot_wire_get_rcode(pkt->wire);
+	if (rcode != KNOT_RCODE_NOERROR) {
+		const knot_lookup_t *lut = knot_lookup_by_id(knot_rcode_names, rcode);
+		if (lut != NULL) {
+			IXFRIN_LOG(LOG_WARNING, data->zone, data->remote,
+			           "server responded with %s", lut->name);
+		}
+		return KNOT_STATE_FAIL;
+	}
+
+	// Initialize with first packet
+	if (data->ixfr.proc == NULL) {
+		const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+
+		if (!ixfr_check_header(answer)) {
+			IXFRIN_LOG(LOG_WARNING, data->zone, data->remote, "malformed response");
+			return KNOT_STATE_FAIL;
+		}
+
+		if (ixfr_is_axfr(answer)) {
+			IXFRIN_LOG(LOG_NOTICE, data->zone, data->remote, "receiving AXFR-style IXFR");
+			data->is_ixfr = false;
+			return axfr_consume(pkt, data);
+		}
+
+		int ret = ixfr_init(data);
+		if (ret != KNOT_EOK) {
+			IXFRIN_LOG(LOG_WARNING, data->zone, data->remote,
+			           "failed to initialize (%s)", knot_strerror(ret));
+			return KNOT_STATE_FAIL;
+		}
+
+		IXFRIN_LOG(LOG_INFO, data->zone, data->remote, "starting");
+		xfr_stats_begin(&data->stats);
+	}
+
+	// Process answer packet
+	xfr_stats_add(&data->stats, pkt->size);
+	int 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, 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;
+
+	uint16_t rcode = knot_pkt_get_ext_rcode(pkt);
+	if (rcode != KNOT_RCODE_NOERROR) {
+		REFRESH_LOG(LOG_WARNING, data->zone, data->remote,
+		            "server responded with %s", rcode_name(rcode));
+		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.rr_count != 1) {
+		REFRESH_LOG(LOG_WARNING, data->zone, data->remote, "malformed message");
+		return KNOT_STATE_FAIL;
+	}
+
+	uint32_t local_serial = knot_soa_serial(&data->soa->rrs);
+	uint32_t remote_serial = knot_soa_serial(&rr->rrs);
+	bool current = serial_is_current(local_serial, remote_serial);
+
+	REFRESH_LOG(LOG_INFO, data->zone, data->remote, "remote serial %u, %s",
+	            remote_serial,
+	            current ? "zone is up-to-date" : "zone is outdated");
+
+	if (current) {
+		return KNOT_STATE_DONE;
 	} else {
-		/* SOA query answered, reschedule refresh timer. */
-		zone_events_schedule(zone, ZONE_EVENT_REFRESH, knot_soa_refresh(soa));
+		data->state = REFRESH_STATE_TRANSFER;
+		return KNOT_STATE_RESET;
 	}
+}
 
-	return KNOT_EOK;
+static int transfer_produce(knot_layer_t *layer, knot_pkt_t *pkt)
+{
+	struct refresh_data *data = layer->data;
+
+	bool ixfr = data->is_ixfr;
+
+	query_init_pkt(pkt);
+	knot_pkt_put_question(pkt, data->zone, KNOT_CLASS_IN,
+			      ixfr ? KNOT_RRTYPE_IXFR : KNOT_RRTYPE_AXFR);
+
+	if (ixfr) {
+		assert(data->soa);
+		knot_pkt_begin(pkt, KNOT_AUTHORITY);
+		knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, data->soa, 0);
+	}
+
+	query_put_edns(pkt, &data->edns);
+
+	return KNOT_STATE_CONSUME;
 }
 
-/*! \brief Execute zone transfer request. */
-static int zone_query_transfer(conf_t *conf, zone_t *zone, const conf_remote_t *master,
-                               uint16_t pkt_type)
+static int transfer_consume(knot_layer_t *layer, knot_pkt_t *pkt)
 {
-	assert(zone);
-	assert(master);
+	struct refresh_data *data = layer->data;
+
+	int next = data->is_ixfr ? ixfr_consume(pkt, data) : axfr_consume(pkt, data);
+
+	// IXFR to AXFR failover
+	if (data->is_ixfr && next == KNOT_STATE_FAIL) {
+		ixfr_cleanup(data);
+		data->is_ixfr = false;
+		return KNOT_STATE_RESET;
+	}
 
-	int ret = zone_query_execute(conf, zone, pkt_type, master);
+	// Log result, no failover after the transfer is complete
+	if (next == KNOT_STATE_DONE) {
+		xfr_log_finished(data->zone,
+		                 data->is_ixfr ? LOG_OPERATION_IXFR : LOG_OPERATION_AXFR,
+		                 LOG_DIRECTION_IN, data->remote, &data->stats);
+	}
+
+	// Cleanup processing context
+	if (next == KNOT_STATE_DONE || next == KNOT_STATE_FAIL) {
+		ixfr_cleanup(data);
+	}
+
+	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 = REFRESH_STATE_SOA_QUERY;
+		data->is_ixfr = true;
+	} else {
+		data->state = REFRESH_STATE_TRANSFER;
+		data->is_ixfr = false;
+	}
+
+	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 REFRESH_STATE_SOA_QUERY: return soa_query_produce(layer, pkt);
+	case REFRESH_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 REFRESH_STATE_SOA_QUERY: return soa_query_consume(layer, pkt);
+	case REFRESH_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 const knot_layer_api_t REFRESH_API = {
+	.begin = refresh_begin,
+	.produce = refresh_produce,
+	.consume = refresh_consume,
+	.reset = refresh_reset,
+};
+
+static int publish_zone(conf_t *conf, zone_t *zone, const struct sockaddr *remote,
+                        struct refresh_result *result)
+{
+	int ret = KNOT_ERROR;
+	bool axfr = result->zone != NULL;
+	apply_ctx_t apply_ctx = { 0 };
+
+	// Construct new zone
+
+	zone_contents_t *new_zone = NULL;
+
+	if (axfr) {
+		new_zone = result->zone;
+	} else {
+		apply_init_ctx(&apply_ctx, NULL, 0);
+		ret = apply_changesets(&apply_ctx, zone->contents,
+		                       &result->changesets, &new_zone);
+		if (ret != KNOT_EOK) {
+			goto fail;
+		}
+	}
+
+	assert(new_zone != NULL);
+
+	// Run semantic checks
+
+	err_handler_logger_t handler;
+	handler._cb.cb = err_handler_logger;
+	ret = zone_do_sem_checks(new_zone, false, &handler._cb);
 	if (ret != KNOT_EOK) {
-		/* IXFR failed, revert to AXFR. */
-		if (pkt_type == KNOT_QUERY_IXFR) {
-			LOG_TRANSFER(LOG_NOTICE, pkt_type, "fallback to AXFR");
-			return zone_query_transfer(conf, zone, master, KNOT_QUERY_AXFR);
+		goto fail;
+	}
+
+	// Write journal for IXFR
+
+	if (!axfr) {
+		ret = zone_changes_store(conf, zone, &result->changesets);
+		if (ret != KNOT_EOK) {
+			goto fail;
 		}
+	}
+
+	// Publish new zone
 
-		/* Log connection errors. */
-		LOG_TRANSFER(LOG_WARNING, pkt_type, "failed (%s)", knot_strerror(ret));
+	zone_contents_t *old_zone = zone_switch_contents(zone, new_zone);
+	zone->flags &= ~ZONE_EXPIRED;
+
+	if (old_zone) {
+		REFRESH_LOG(LOG_INFO, zone->name, remote,
+		            "zone updated, serial %u -> %u",
+		            zone_contents_serial(old_zone),
+		            zone_contents_serial(new_zone));
+	} else {
+		REFRESH_LOG(LOG_INFO, zone->name, remote,
+		            "zone updated, serial none -> %u",
+		            zone_contents_serial(new_zone));
+	}
+
+	// Clean up old resources
+
+	assert(ret == KNOT_EOK);
+	synchronize_rcu();
+
+fail:
+	if (axfr) {
+		if (ret == KNOT_EOK) {
+			zone_contents_deep_free(&old_zone);
+			result->zone = NULL; // seized
+		}
+	} else {
+		if (ret == KNOT_EOK) {
+			update_free_zone(&old_zone);
+			update_cleanup(&apply_ctx);
+		} else {
+			update_rollback(&apply_ctx);
+			update_free_zone(&new_zone);
+		}
 	}
 
 	return ret;
 }
 
-struct transfer_data {
-	uint16_t pkt_type;
-};
+#include "knot/query/requestor.h"
 
-static int try_xfer(conf_t *conf, zone_t *zone, const conf_remote_t *master, void *_data)
+static int try_refresh(conf_t *conf, zone_t *zone, const conf_remote_t *master, void *ctx)
 {
+	// XXX: COPY PASTED
+
 	assert(zone);
 	assert(master);
-	assert(_data);
 
-	struct transfer_data *data = _data;
+	knot_rrset_t soa = { 0 };
+	if (zone->contents) {
+		soa = node_rrset(zone->contents->apex, KNOT_RRTYPE_SOA);
+	}
+
+	struct refresh_data data = {
+		.remote = (struct sockaddr *)&master->addr,
+		.zone = zone->name,
+		.soa = zone->contents ? &soa : NULL,
+	};
+
+	refresh_result_init(&data.result);
+	query_edns_data_init(&data.edns, conf, zone->name, master->addr.ss_family);
+
+	// TODO: temporary until we can get event specific flags
+	if (zone->flags & ZONE_FORCE_AXFR) {
+		zone->flags &= ~ZONE_FORCE_AXFR;
+		data.soa = NULL;
+	}
+
+	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;
+	}
+
+	// TODO: hardcoded
+	int timeout = 2000;
+
+	int ret = knot_requestor_exec(&requestor, req, timeout);
+	knot_request_free(req, NULL);
+	knot_requestor_clear(&requestor);
+
+	if (ret == KNOT_EOK && !refresh_result_empty(&data.result)) {
+		ret = publish_zone(conf, zone, data.remote, &data.result);
+	}
+
+	refresh_result_cleanup(&data.result);
+
+	return ret;
+}
+
+#define BOOTSTRAP_RETRY (30) /*!< Interval between AXFR bootstrap retries. */
+#define BOOTSTRAP_MAXTIME (24*60*60) /*!< Maximum AXFR retry cap of 24 hours. */
+
+/*! \brief Get next bootstrap interval. */
+uint32_t bootstrap_next(uint32_t interval)
+{
+	interval *= 2;
+	interval += dnssec_random_uint16_t() % BOOTSTRAP_RETRY;
+	if (interval > BOOTSTRAP_MAXTIME) {
+		interval = BOOTSTRAP_MAXTIME;
+	}
+	return interval;
+}
+
+/*! \brief Get SOA from zone. */
+static const knot_rdataset_t *zone_soa(zone_t *zone)
+{
+	assert(zone);
+
+	if (zone->contents == NULL) {
+		return NULL;
+	}
 
-	return zone_query_transfer(conf, zone, master, data->pkt_type);
+	return node_rdataset(zone->contents->apex, KNOT_RRTYPE_SOA);
 }
 
-int event_xfer(conf_t *conf, zone_t *zone)
+/*! \brief Schedule expire event, unless it is already scheduled. */
+static void start_expire_timer(conf_t *conf, zone_t *zone, const knot_rdataset_t *soa)
+{
+	if (zone_events_is_scheduled(zone, ZONE_EVENT_EXPIRE)) {
+		return;
+	}
+
+	zone_events_schedule(zone, ZONE_EVENT_EXPIRE, knot_soa_expire(soa));
+}
+
+int event_refresh(conf_t *conf, zone_t *zone)
 {
 	assert(zone);
 
-	/* Ignore if not slave zone. */
+	// slave zones only
 	if (!zone_is_slave(conf, zone)) {
+		log_zone_debug(zone->name, "%s:%d possibly unreachable", __func__, __LINE__);
 		return KNOT_EOK;
 	}
 
-	struct transfer_data data = { 0 };
-	const char *err_str = "";
+	int ret = zone_master_try(conf, zone, try_refresh, NULL, "refresh");
 
-	/* Determine transfer type. */
-	bool is_bootstrap = zone_contents_is_empty(zone->contents);
-	if (is_bootstrap || zone->flags & ZONE_FORCE_AXFR) {
-		data.pkt_type = KNOT_QUERY_AXFR;
-		err_str = "AXFR, incoming";
-	} else {
-		data.pkt_type = KNOT_QUERY_IXFR;
-		err_str = "IXFR, incoming";
-	}
+	const knot_rdataset_t *soa = zone_soa(zone);
+	time_t next = 0;
 
-	/* Execute zone transfer. */
-	int ret = zone_master_try(conf, zone, try_xfer, &data, err_str);
-	zone_clear_preferred_master(zone);
-	if (ret != KNOT_EOK) {
-		log_zone_error(zone->name, "%s, failed (%s)", err_str,
-		               knot_strerror(ret));
-		if (is_bootstrap) {
-			zone->bootstrap_retry = bootstrap_next(zone->bootstrap_retry);
-			zone_events_schedule(zone, ZONE_EVENT_XFER, zone->bootstrap_retry);
+	if (ret == KNOT_EOK) {
+		assert(soa);
+		next = knot_soa_refresh(soa);
+	} else {
+		log_zone_error(zone->name, "refresh, failed (%s)", knot_strerror(ret));
+		if (soa) {
+			next = knot_soa_retry(soa);
 		} else {
-			const knot_rdataset_t *soa = zone_soa(zone);
-			zone_events_schedule(zone, ZONE_EVENT_XFER, knot_soa_retry(soa));
-			start_expire_timer(conf, zone, soa);
+			// TODO: boostrap period from timers
+			next = bootstrap_next(10);
 		}
+	}
 
-		return KNOT_EOK;
+	zone_events_schedule(zone, ZONE_EVENT_REFRESH, next);
+
+	// TODO: temporary until timers are refactored
+	if (ret != KNOT_EOK) {
+		start_expire_timer(conf, zone, soa);
 	}
 
-	assert(!zone_contents_is_empty(zone->contents));
-	const knot_rdataset_t *soa = zone_soa(zone);
 
+
+	// TODO: reschedule all events here
+	// REFRESH
+	// NOTIFY
+	// EXPIRE
+	// FLUSH
+
+	///* Transfer cleanup. */
+	//zone->bootstrap_retry = ZONE_EVENT_NOW;
+	//zone->flags &= ~ZONE_FORCE_AXFR;
+
+	// MEMORY TRIM?
+	/* Trim extra heap. */
+	//if (!is_bootstrap) {
+	//	mem_trim();
+	//}
+
+#if 0
 	/* Rechedule events. */
 	zone_events_schedule(zone, ZONE_EVENT_REFRESH, knot_soa_refresh(soa));
 	zone_events_schedule(zone, ZONE_EVENT_NOTIFY,  ZONE_EVENT_NOW);
@@ -199,15 +869,6 @@ int event_xfer(conf_t *conf, zone_t *zone)
 	           !zone_events_is_scheduled(zone, ZONE_EVENT_FLUSH)) {
 		zone_events_schedule(zone, ZONE_EVENT_FLUSH, sync_timeout);
 	}
-
-	/* Transfer cleanup. */
-	zone->bootstrap_retry = ZONE_EVENT_NOW;
-	zone->flags &= ~ZONE_FORCE_AXFR;
-
-	/* Trim extra heap. */
-	if (!is_bootstrap) {
-		mem_trim();
-	}
-
+#endif
 	return KNOT_EOK;
 }
diff --git a/src/knot/modules/dnsproxy.c b/src/knot/modules/dnsproxy.c
index 17c31753d..14929f4f8 100644
--- a/src/knot/modules/dnsproxy.c
+++ b/src/knot/modules/dnsproxy.c
@@ -80,7 +80,7 @@ static int dnsproxy_fwd(int state, knot_pkt_t *pkt, struct query_data *qdata, vo
 	bool is_tcp = net_is_stream(qdata->param->socket);
 	const struct sockaddr *dst = (const struct sockaddr *)&proxy->remote.addr;
 	const struct sockaddr *src = (const struct sockaddr *)&proxy->remote.via;
-	struct knot_request *req = knot_request_make(re.mm, dst, src, qdata->query,
+	struct knot_request *req = knot_request_make(re.mm, dst, src, qdata->query, NULL,
 	                                             is_tcp ? 0 : KNOT_RQ_UDP);
 	if (req == NULL) {
 		knot_requestor_clear(&re);
diff --git a/src/knot/nameserver/axfr.c b/src/knot/nameserver/axfr.c
index 29d986aff..f74b7d080 100644
--- a/src/knot/nameserver/axfr.c
+++ b/src/knot/nameserver/axfr.c
@@ -17,15 +17,23 @@
 #include <urcu.h>
 
 #include "contrib/mempattern.h"
-#include "contrib/print.h"
 #include "contrib/sockaddr.h"
 #include "knot/common/log.h"
 #include "knot/conf/conf.h"
 #include "knot/nameserver/axfr.h"
 #include "knot/nameserver/internet.h"
+#include "knot/nameserver/log.h"
+#include "knot/nameserver/xfr.h"
 #include "knot/zone/zonefile.h"
 #include "libknot/libknot.h"
 
+#define ZONE_NAME(qdata) knot_pkt_qname((qdata)->query)
+#define REMOTE(qdata) (struct sockaddr *)(qdata)->param->remote
+
+#define AXFROUT_LOG(priority, qdata, fmt...) \
+	ns_log(priority, ZONE_NAME(qdata), LOG_OPERATION_AXFR, \
+               LOG_DIRECTION_OUT, REMOTE(qdata), fmt)
+
 /* AXFR context. @note aliasing the generic xfr_proc */
 struct axfr_proc {
 	struct xfr_proc proc;
@@ -142,7 +150,7 @@ static int axfr_query_init(struct query_data *qdata)
 	init_list(&axfr->proc.nodes);
 
 	/* Put data to process. */
-	gettimeofday(&axfr->proc.tstamp, NULL);
+	xfr_stats_begin(&axfr->proc.stats);
 	ptrlist_add(&axfr->proc.nodes, zone->nodes, mm);
 	/* Put NSEC3 data if exists. */
 	if (!zone_tree_is_empty(zone->nsec3_nodes)) {
@@ -160,53 +168,6 @@ static int axfr_query_init(struct query_data *qdata)
 	return KNOT_EOK;
 }
 
-int xfr_process_list(knot_pkt_t *pkt, xfr_put_cb process_item,
-                     struct query_data *qdata)
-{
-	if (pkt == NULL || qdata == NULL || qdata->ext == NULL) {
-		return KNOT_EINVAL;
-	}
-
-	int ret = KNOT_EOK;
-	knot_mm_t *mm = qdata->mm;
-	struct xfr_proc *xfer = qdata->ext;
-
-	zone_contents_t *zone = qdata->zone->contents;
-	knot_rrset_t soa_rr = node_rrset(zone->apex, KNOT_RRTYPE_SOA);
-
-	/* Prepend SOA on first packet. */
-	if (xfer->npkts == 0) {
-		ret = knot_pkt_put(pkt, 0, &soa_rr, KNOT_PF_NOTRUNC);
-		if (ret != KNOT_EOK) {
-			return ret;
-		}
-	}
-
-	/* Process all items in the list. */
-	while (!EMPTY_LIST(xfer->nodes)) {
-		ptrnode_t *head = HEAD(xfer->nodes);
-		ret = process_item(pkt, head->d, xfer);
-		if (ret == KNOT_EOK) { /* Finished. */
-			/* Complete change set. */
-			rem_node((node_t *)head);
-			mm_free(mm, head);
-		} else { /* Packet full or other error. */
-			break;
-		}
-	}
-
-	/* Append SOA on last packet. */
-	if (ret == KNOT_EOK) {
-		ret = knot_pkt_put(pkt, 0, &soa_rr, KNOT_PF_NOTRUNC);
-	}
-
-	/* Update counters. */
-	xfer->npkts  += 1;
-	xfer->nbytes += pkt->size;
-
-	return ret;
-}
-
 int axfr_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 {
 	if (pkt == NULL || qdata == NULL) {
@@ -214,7 +175,6 @@ int axfr_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 	}
 
 	int ret = KNOT_EOK;
-	struct timeval now = {0};
 
 	/* If AXFR is disabled, respond with NOTIMPL. */
 	if (qdata->param->proc_flags & NS_QUERY_NO_AXFR) {
@@ -227,11 +187,11 @@ int axfr_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 
 		ret = axfr_query_init(qdata);
 		if (ret != KNOT_EOK) {
-			AXFROUT_LOG(LOG_ERR, "failed to start (%s)",
+			AXFROUT_LOG(LOG_ERR, qdata, "failed to start (%s)",
 			            knot_strerror(ret));
 			return KNOT_STATE_FAIL;
 		} else {
-			AXFROUT_LOG(LOG_INFO, "started, serial %u",
+			AXFROUT_LOG(LOG_INFO, qdata, "started, serial %u",
 			           zone_contents_serial(qdata->zone->contents));
 		}
 	}
@@ -246,201 +206,13 @@ int axfr_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 	case KNOT_ESPACE: /* Couldn't write more, send packet and continue. */
 		return KNOT_STATE_PRODUCE; /* Check for more. */
 	case KNOT_EOK:    /* Last response. */
-		gettimeofday(&now, NULL);
-		AXFROUT_LOG(LOG_INFO,
-		            "finished, %.02f seconds, %u messages, %u bytes",
-		            time_diff(&axfr->proc.tstamp, &now) / 1000.0,
-		            axfr->proc.npkts, axfr->proc.nbytes);
+		xfr_stats_end(&axfr->proc.stats);
+		xfr_log_finished(ZONE_NAME(qdata), LOG_OPERATION_AXFR, LOG_DIRECTION_OUT,
+		                 REMOTE(qdata), &axfr->proc.stats);
 		return KNOT_STATE_DONE;
 		break;
 	default:          /* Generic error. */
-		AXFROUT_LOG(LOG_ERR, "failed (%s)", knot_strerror(ret));
-		return KNOT_STATE_FAIL;
-	}
-}
-
-static void axfr_answer_cleanup(struct answer_data *data)
-{
-	assert(data != NULL);
-
-	struct xfr_proc *proc = data->ext;
-	if (proc) {
-		zone_contents_deep_free(&proc->contents);
-		mm_free(data->mm, proc);
-		data->ext = NULL;
-	}
-}
-
-static int axfr_answer_init(struct answer_data *data)
-{
-	assert(data);
-
-	/* Create new zone contents. */
-	zone_t *zone = data->param->zone;
-	zone_contents_t *new_contents = zone_contents_new(zone->name);
-	if (new_contents == NULL) {
-		return KNOT_ENOMEM;
-	}
-
-	/* Create new processing context. */
-	struct xfr_proc *proc = mm_alloc(data->mm, sizeof(struct xfr_proc));
-	if (proc == NULL) {
-		zone_contents_deep_free(&new_contents);
-		return KNOT_ENOMEM;
-	}
-
-	memset(proc, 0, sizeof(struct xfr_proc));
-	proc->contents = new_contents;
-	gettimeofday(&proc->tstamp, NULL);
-
-	/* Set up cleanup callback. */
-	data->ext = proc;
-	data->ext_cleanup = &axfr_answer_cleanup;
-
-	return KNOT_EOK;
-}
-
-static int axfr_answer_finalize(struct answer_data *adata)
-{
-	struct timeval now;
-	gettimeofday(&now, NULL);
-
-	/*
-	 * Adjust zone so that node count is set properly and nodes are
-	 * marked authoritative / delegation point.
-	 */
-	struct xfr_proc *proc = adata->ext;
-	int rc = zone_contents_adjust_full(proc->contents);
-	if (rc != KNOT_EOK) {
-		return rc;
-	}
-
-	err_handler_logger_t handler;
-	handler._cb.cb = err_handler_logger;
-	rc = zone_do_sem_checks(proc->contents, false, &handler._cb);
-
-	if (rc != KNOT_EOK) {
-		return rc;
-	}
-
-	conf_val_t val = conf_zone_get(adata->param->conf, C_MAX_ZONE_SIZE,
-	                               proc->contents->apex->owner);
-	int64_t size_limit = conf_int(&val);
-
-	if (proc->contents->size > size_limit) {
-		AXFRIN_LOG(LOG_WARNING, "zone size exceeded");
+		AXFROUT_LOG(LOG_ERR, qdata, "failed (%s)", knot_strerror(ret));
 		return KNOT_STATE_FAIL;
 	}
-
-	/* Switch contents. */
-	zone_t *zone = adata->param->zone;
-	zone_contents_t *old_contents =
-	                zone_switch_contents(zone, proc->contents);
-	zone->flags &= ~ZONE_EXPIRED;
-	synchronize_rcu();
-
-	if (old_contents != NULL) {
-		AXFRIN_LOG(LOG_INFO, "finished, "
-		           "serial %u -> %u, %.02f seconds, %u messages, %u bytes",
-		           zone_contents_serial(old_contents),
-		           zone_contents_serial(proc->contents),
-		           time_diff(&proc->tstamp, &now) / 1000.0,
-		           proc->npkts, proc->nbytes);
-	} else {
-		AXFRIN_LOG(LOG_INFO, "finished, "
-		           "serial %u, %.02f seconds, %u messages, %u bytes",
-		           zone_contents_serial(proc->contents),
-		           time_diff(&proc->tstamp, &now) / 1000.0,
-		           proc->npkts, proc->nbytes);
-	}
-
-	/* Do not free new contents with cleanup. */
-	zone_contents_deep_free(&old_contents);
-	proc->contents = NULL;
-
-	return KNOT_EOK;
-}
-
-static int axfr_answer_packet(knot_pkt_t *pkt, struct answer_data *adata)
-{
-	assert(adata != NULL);
-	struct xfr_proc *proc = adata->ext;
-	assert(pkt != NULL);
-	assert(proc != NULL);
-
-	/* Update counters. */
-	proc->npkts  += 1;
-	proc->nbytes += pkt->size;
-
-	conf_val_t val = conf_zone_get(adata->param->conf, C_MAX_ZONE_SIZE,
-	                               proc->contents->apex->owner);
-	int64_t size_limit = conf_int(&val);
-
-	/* Init zone creator. */
-	zcreator_t zc = {.z = proc->contents, .master = false, .ret = KNOT_EOK };
-
-	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
-	const knot_rrset_t *answer_rr = knot_pkt_rr(answer, 0);
-	for (uint16_t i = 0; i < answer->count; ++i) {
-		if (answer_rr[i].type == KNOT_RRTYPE_SOA &&
-		    node_rrtype_exists(zc.z->apex, KNOT_RRTYPE_SOA)) {
-			return KNOT_STATE_DONE;
-		} else {
-			int ret = zcreator_step(&zc, &answer_rr[i]);
-			if (ret != KNOT_EOK) {
-				return KNOT_STATE_FAIL;
-			}
-		}
-		proc->contents->size += knot_rrset_size(&answer_rr[i]);
-		if (proc->contents->size > size_limit) {
-			AXFRIN_LOG(LOG_WARNING, "zone size exceeded");
-			return KNOT_STATE_FAIL;
-		}
-	}
-
-	return KNOT_STATE_CONSUME;
-}
-
-int axfr_process_answer(knot_pkt_t *pkt, struct answer_data *adata)
-{
-	if (pkt == NULL || adata == NULL) {
-		return KNOT_STATE_FAIL;
-	}
-
-	/* Check RCODE. */
-	uint8_t rcode = knot_wire_get_rcode(pkt->wire);
-	if (rcode != KNOT_RCODE_NOERROR) {
-		const knot_lookup_t *lut = knot_lookup_by_id(knot_rcode_names, rcode);
-		if (lut != NULL) {
-			AXFRIN_LOG(LOG_WARNING, "server responded with %s", lut->name);
-		}
-		return KNOT_STATE_FAIL;
-	}
-
-	/* Initialize processing with first packet. */
-	if (adata->ext == NULL) {
-		NS_NEED_TSIG_SIGNED(&adata->param->tsig_ctx, 0);
-		AXFRIN_LOG(LOG_INFO, "starting");
-
-		int ret = axfr_answer_init(adata);
-		if (ret != KNOT_EOK) {
-			AXFRIN_LOG(LOG_WARNING, "failed (%s)", knot_strerror(ret));
-			return KNOT_STATE_FAIL;
-		}
-	} else {
-		NS_NEED_TSIG_SIGNED(&adata->param->tsig_ctx, 100);
-	}
-
-	/* Process answer packet. */
-	int ret = axfr_answer_packet(pkt, adata);
-	if (ret == KNOT_STATE_DONE) {
-		NS_NEED_TSIG_SIGNED(&adata->param->tsig_ctx, 0);
-		/* This was the last packet, finalize zone and publish it. */
-		int fret = axfr_answer_finalize(adata);
-		if (fret != KNOT_EOK) {
-			ret = KNOT_STATE_FAIL;
-		}
-	}
-
-	return ret;
 }
diff --git a/src/knot/nameserver/axfr.h b/src/knot/nameserver/axfr.h
index 4aa0c5802..c9c66aee5 100644
--- a/src/knot/nameserver/axfr.h
+++ b/src/knot/nameserver/axfr.h
@@ -13,64 +13,11 @@
     You should have received a copy of the GNU General Public License
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
-/*!
- * \file
- *
- * \brief AXFR processing.
- *
- * \addtogroup query_processing
- * @{
- */
 
 #pragma once
 
-#include "libknot/packet/pkt.h"
-#include "knot/nameserver/log.h"
-#include "knot/query/query.h"
 #include "knot/nameserver/process_query.h"
-#include "knot/zone/contents.h"
-#include "contrib/ucw/lists.h"
-
-/*!
- * \brief Transfer-specific logging (internal, expects 'qdata' variable set).
- *
- * Emits a message in the following format:
- * > [zone] type, outgoing, address: custom formatted message
- */
-#define TRANSFER_OUT_LOG(type, priority, msg, ...) \
-	NS_PROC_LOG(priority, knot_pkt_qname((qdata)->query), (qdata)->param->remote, \
-	            type ", outgoing", msg, ##__VA_ARGS__)
-#define AXFROUT_LOG(args...) TRANSFER_OUT_LOG("AXFR", args)
-#define IXFROUT_LOG(args...) TRANSFER_OUT_LOG("IXFR", args)
-
-/*!
- * \brief Transfer-specific logging (internal, expects 'adata' variable set).
- */
-#define TRANSFER_IN_LOG(type, priority, msg, ...) \
-	NS_PROC_LOG(priority, (adata)->param->zone->name, (adata)->param->remote, \
-	            type ", incoming", msg, ##__VA_ARGS__)
-#define AXFRIN_LOG(args...) TRANSFER_IN_LOG("AXFR", args)
-#define IXFRIN_LOG(args...) TRANSFER_IN_LOG("IXFR", args)
-
-
-/*! \brief Generic transfer processing state. */
-struct xfr_proc {
-	list_t nodes;    /* Items to process (ptrnode_t). */
-	unsigned npkts;  /* Packets processed. */
-	unsigned nbytes; /* Bytes processed. */
-	struct timeval tstamp; /* Start time. */
-	zone_contents_t *contents; /* Processed zone. */
-};
-
-/*! \brief Generic transfer processing (reused for IXFR).
- *  \return KNOT_EOK or an error
- */
-typedef int (*xfr_put_cb)(knot_pkt_t *pkt, const void *item, struct xfr_proc *xfer);
-
-/*! \brief Put all items from xfr_proc.nodes to packet using a callback function.
- *  \note qdata->ext points to struct xfr_proc* (this is xfer-specific context)
- */
-int xfr_process_list(knot_pkt_t *pkt, xfr_put_cb put, struct query_data *qdata);
+#include "libknot/packet/pkt.h"
 
 /*!
  * \brief Process an AXFR query message.
@@ -78,12 +25,3 @@ int xfr_process_list(knot_pkt_t *pkt, xfr_put_cb put, struct query_data *qdata);
  * \return KNOT_STATE_* processing states
  */
 int axfr_process_query(knot_pkt_t *pkt, struct query_data *qdata);
-
-/*!
- * \brief Processes an AXFR response message.
- *
- * \return KNOT_STATE_* processing states
- */
-int axfr_process_answer(knot_pkt_t *pkt, struct answer_data *adata);
-
-/*! @} */
diff --git a/src/knot/nameserver/internet.c b/src/knot/nameserver/internet.c
index 527d35e04..c91f71a6c 100644
--- a/src/knot/nameserver/internet.c
+++ b/src/knot/nameserver/internet.c
@@ -807,8 +807,6 @@ static int answer_query(struct query_plan *plan, knot_pkt_t *response, struct qu
 	return KNOT_STATE_DONE;
 }
 
-#undef SOLVE_STEP
-
 int internet_process_query(knot_pkt_t *response, struct query_data *qdata)
 {
 	if (response == NULL || qdata == NULL) {
@@ -834,72 +832,3 @@ int internet_process_query(knot_pkt_t *response, struct query_data *qdata)
 
 	return answer_query(qdata->zone->query_plan, response, qdata);
 }
-
-#include "knot/nameserver/log.h"
-#define REFRESH_LOG(priority, msg, ...) \
-	NS_PROC_LOG(priority, (data)->param->zone->name, (data)->param->remote, \
-	            "refresh, outgoing", msg, ##__VA_ARGS__)
-
-/*! \brief Process answer to SOA query. */
-static int process_soa_answer(knot_pkt_t *pkt, struct answer_data *data)
-{
-	zone_t *zone = data->param->zone;
-
-	/* Check RCODE. */
-	uint8_t rcode = knot_wire_get_rcode(pkt->wire);
-	if (rcode != KNOT_RCODE_NOERROR) {
-		const knot_lookup_t *lut = knot_lookup_by_id(knot_rcode_names, rcode);
-		if (lut != NULL) {
-			REFRESH_LOG(LOG_WARNING, "server responded with %s", lut->name);
-		}
-		return KNOT_STATE_FAIL;
-	}
-
-	/* Expect SOA in answer section. */
-	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
-	const knot_rrset_t *first_rr = knot_pkt_rr(answer, 0);
-	if (answer->count < 1 || first_rr->type != KNOT_RRTYPE_SOA) {
-		REFRESH_LOG(LOG_WARNING, "malformed message");
-		return KNOT_STATE_FAIL;
-	}
-
-	/* Our zone is expired, schedule transfer. */
-	if (zone_contents_is_empty(zone->contents)) {
-		zone_events_schedule(zone, ZONE_EVENT_XFER, ZONE_EVENT_NOW);
-		return KNOT_STATE_DONE;
-	}
-
-	/* Check if master has newer zone and schedule transfer. */
-	knot_rdataset_t *soa = node_rdataset(zone->contents->apex, KNOT_RRTYPE_SOA);
-	uint32_t our_serial = knot_soa_serial(soa);
-	uint32_t their_serial =	knot_soa_serial(&first_rr->rrs);
-	if (serial_compare(our_serial, their_serial) >= 0) {
-		REFRESH_LOG(LOG_INFO, "zone is up-to-date");
-		zone_events_cancel(zone, ZONE_EVENT_EXPIRE);
-		zone_clear_preferred_master(zone);
-		return KNOT_STATE_DONE; /* Our zone is up to date. */
-	}
-
-	/* Our zone is outdated, schedule zone transfer. */
-	REFRESH_LOG(LOG_INFO, "master has newer serial %u -> %u", our_serial, their_serial);
-	zone_set_preferred_master(zone, data->param->remote);
-	zone_events_schedule(zone, ZONE_EVENT_XFER, ZONE_EVENT_NOW);
-	return KNOT_STATE_DONE;
-}
-
-int internet_process_answer(knot_pkt_t *pkt, struct answer_data *data)
-{
-	if (pkt == NULL || data == NULL) {
-		return KNOT_STATE_FAIL;
-	}
-
-	NS_NEED_TSIG_SIGNED(&data->param->tsig_ctx, 0);
-
-	/* As of now, server can only issue SOA queries. */
-	switch(knot_pkt_qtype(pkt)) {
-	case KNOT_RRTYPE_SOA:
-		return process_soa_answer(pkt, data);
-	default:
-		return KNOT_STATE_NOOP;
-	}
-}
diff --git a/src/knot/nameserver/internet.h b/src/knot/nameserver/internet.h
index b5e035467..0d438386e 100644
--- a/src/knot/nameserver/internet.h
+++ b/src/knot/nameserver/internet.h
@@ -29,9 +29,6 @@
 
 /* Query data (from query processing). */
 struct query_data;
-struct query_plan;
-struct query_module;
-struct answer_data;
 
 /*! \brief Internet query processing states. */
 enum {
@@ -53,15 +50,6 @@ enum {
  */
 int internet_process_query(knot_pkt_t *resp, struct query_data *qdata);
 
-/*!
- * \brief Process answer in an IN class zone.
- *
- * \retval FAIL if it encountered an error.
- * \retval DONE if finished.
- * \retval NOOP if not supported.
- */
-int internet_process_answer(knot_pkt_t *pkt, struct answer_data *data);
-
 /*!
  * \brief Puts RRSet to packet, will store its RRSIG for later use.
  *
diff --git a/src/knot/nameserver/ixfr.c b/src/knot/nameserver/ixfr.c
index afc14aec2..d044b9ad5 100644
--- a/src/knot/nameserver/ixfr.c
+++ b/src/knot/nameserver/ixfr.c
@@ -17,46 +17,25 @@
 #include <urcu.h>
 
 #include "knot/common/log.h"
+#include "knot/nameserver/internet.h"
 #include "knot/nameserver/axfr.h"
 #include "knot/nameserver/ixfr.h"
-#include "knot/nameserver/internet.h"
+#include "knot/nameserver/log.h"
+#include "knot/nameserver/xfr.h"
 #include "knot/updates/apply.h"
 #include "knot/zone/serial.h"
 #include "knot/zone/semantic-check.h"
 #include "knot/zone/zonefile.h"
 #include "libknot/libknot.h"
 #include "contrib/mempattern.h"
-#include "contrib/print.h"
 #include "contrib/sockaddr.h"
 
-/* ------------------------ IXFR-out processing ----------------------------- */
-
-/*! \brief IXFR-in processing states. */
-enum ixfr_states {
-	IXFR_START = 0,  /* IXFR-in starting, expecting final SOA. */
-	IXFR_SOA_DEL,    /* Expecting starting SOA. */
-	IXFR_SOA_ADD,    /* Expecting ending SOA. */
-	IXFR_DEL,        /* Expecting RR to delete. */
-	IXFR_ADD,        /* Expecting RR to add. */
-	IXFR_DONE        /* Processing done, IXFR-in complete. */
-};
-
-/*! \brief Extended structure for IXFR-in/IXFR-out processing. */
-struct ixfr_proc {
-	struct xfr_proc proc;          /* Generic transfer processing context. */
-	changeset_iter_t cur;          /* Current changeset iteration state.*/
-	knot_rrset_t cur_rr;           /* Currently processed RRSet. */
-	int state;                     /* IXFR-in state. */
-	knot_rrset_t *final_soa;       /* First SOA received via IXFR. */
-	list_t changesets;             /* Processed changesets. */
-	size_t change_count;           /* Count of changesets received. */
-	size_t change_size;            /* Size of records to add and remove */
-	zone_t *zone;                  /* Modified zone - for journal access. */
-	knot_mm_t *mm;                 /* Memory context for RR allocations. */
-	struct query_data *qdata;
-	const knot_rrset_t *soa_from;
-	const knot_rrset_t *soa_to;
-};
+#define ZONE_NAME(qdata) knot_pkt_qname((qdata)->query)
+#define REMOTE(qdata) (struct sockaddr *)(qdata)->param->remote
+
+#define IXFROUT_LOG(priority, qdata, fmt...) \
+	ns_log(priority, ZONE_NAME(qdata), LOG_OPERATION_IXFR, \
+	       LOG_DIRECTION_OUT, REMOTE(qdata), fmt)
 
 /*! \brief Helper macro for putting RRs into packet. */
 #define IXFR_SAFE_PUT(pkt, rr) \
@@ -150,10 +129,9 @@ static int ixfr_process_changeset(knot_pkt_t *pkt, const void *item,
 	}
 
 	/* Finished change set. */
-	struct query_data *qdata = ixfr->qdata; /*< Required for IXFROUT_LOG() */
 	const uint32_t serial_from = knot_soa_serial(&chgset->soa_from->rrs);
 	const uint32_t serial_to = knot_soa_serial(&chgset->soa_to->rrs);
-	IXFROUT_LOG(LOG_DEBUG, "serial %u -> %u", serial_from, serial_to);
+	IXFROUT_LOG(LOG_DEBUG, ixfr->qdata, "serial %u -> %u", serial_from, serial_to);
 
 	return ret;
 }
@@ -259,7 +237,7 @@ static int ixfr_answer_init(struct query_data *qdata)
 		return KNOT_ENOMEM;
 	}
 	memset(xfer, 0, sizeof(struct ixfr_proc));
-	gettimeofday(&xfer->proc.tstamp, NULL);
+	xfr_stats_begin(&xfer->proc.stats);
 	xfer->state = IXFR_SOA_DEL;
 	init_list(&xfer->proc.nodes);
 	init_list(&xfer->changesets);
@@ -321,334 +299,6 @@ static int ixfr_answer_soa(knot_pkt_t *pkt, struct query_data *qdata)
 	return KNOT_STATE_DONE;
 }
 
-/* ------------------------- IXFR-in processing ----------------------------- */
-
-/*! \brief Checks whether server responded with AXFR-style IXFR. */
-static bool ixfr_is_axfr(const knot_pkt_t *pkt)
-{
-	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
-	return answer->count >= 2 &&
-	       knot_pkt_rr(answer, 0)->type == KNOT_RRTYPE_SOA &&
-	       knot_pkt_rr(answer, 1)->type != KNOT_RRTYPE_SOA;
-}
-
-/*! \brief Cleans up data allocated by IXFR-in processing. */
-static void ixfrin_cleanup(struct answer_data *data)
-{
-	struct ixfr_proc *proc = data->ext;
-	if (proc) {
-		changesets_free(&proc->changesets);
-		knot_rrset_free(&proc->final_soa, proc->mm);
-		mm_free(data->mm, proc);
-		data->ext = NULL;
-	}
-}
-
-/*! \brief Initializes IXFR-in processing context. */
-static int ixfrin_answer_init(struct answer_data *data)
-{
-	struct ixfr_proc *proc = mm_alloc(data->mm, sizeof(struct ixfr_proc));
-	if (proc == NULL) {
-		return KNOT_ENOMEM;
-	}
-	memset(proc, 0, sizeof(struct ixfr_proc));
-	gettimeofday(&proc->proc.tstamp, NULL);
-
-	init_list(&proc->changesets);
-
-	proc->state = IXFR_START;
-	proc->zone = data->param->zone;
-	proc->mm = data->mm;
-
-	data->ext = proc;
-	data->ext_cleanup = &ixfrin_cleanup;
-
-	return KNOT_EOK;
-}
-
-/*! \brief Finalizes IXFR-in processing. */
-static int ixfrin_finalize(struct answer_data *adata)
-{
-	struct ixfr_proc *ixfr = adata->ext;
-	assert(ixfr->state == IXFR_DONE);
-
-	apply_ctx_t a_ctx = { 0 };
-	apply_init_ctx(&a_ctx, NULL, APPLY_STRICT);
-
-	zone_contents_t *new_contents;
-	int ret = apply_changesets(&a_ctx, ixfr->zone, &ixfr->changesets, &new_contents);
-	if (ret != KNOT_EOK) {
-		IXFRIN_LOG(LOG_WARNING, "failed to apply changes to zone (%s)",
-		           knot_strerror(ret));
-		return ret;
-	}
-
-	err_handler_logger_t handler;
-	handler._cb.cb = err_handler_logger;
-	ret = zone_do_sem_checks(new_contents, false, &handler._cb);
-
-	if (ret != KNOT_EOK) {
-		IXFRIN_LOG(LOG_WARNING, "failed to apply changes to zone (%s)",
-		           knot_strerror(ret));
-		update_rollback(&a_ctx);
-		update_free_zone(&new_contents);
-		return ret;
-	}
-
-	conf_val_t val = conf_zone_get(adata->param->conf, C_MAX_ZONE_SIZE,
-	                               ixfr->zone->name);
-	const int64_t size_limit = conf_int(&val);
-
-	if (new_contents->size > size_limit) {
-		IXFRIN_LOG(LOG_WARNING, "zone size exceeded");
-		update_rollback(&a_ctx);
-		update_free_zone(&new_contents);
-		return KNOT_EZONESIZE;
-	}
-
-	/* Write changes to journal. */
-	ret = zone_changes_store(adata->param->conf, ixfr->zone, &ixfr->changesets);
-	if (ret != KNOT_EOK) {
-		IXFRIN_LOG(LOG_WARNING, "failed to write changes to journal (%s)",
-		           knot_strerror(ret));
-		update_rollback(&a_ctx);
-		update_free_zone(&new_contents);
-		return ret;
-	}
-
-	/* Switch zone contents. */
-	zone_contents_t *old_contents = zone_switch_contents(ixfr->zone, new_contents);
-	ixfr->zone->flags &= ~ZONE_EXPIRED;
-	synchronize_rcu();
-
-	struct timeval now = {0};
-	gettimeofday(&now, NULL);
-	IXFRIN_LOG(LOG_INFO, "finished, "
-	           "serial %u -> %u, %.02f seconds, %u messages, %u bytes",
-	           zone_contents_serial(old_contents),
-	           zone_contents_serial(new_contents),
-	           time_diff(&ixfr->proc.tstamp, &now) / 1000.0,
-	           ixfr->proc.npkts, ixfr->proc.nbytes);
-
-	update_free_zone(&old_contents);
-	update_cleanup(&a_ctx);
-
-	return KNOT_EOK;
-}
-
-/*! \brief Stores starting SOA into changesets structure. */
-static int solve_start(const knot_rrset_t *rr, struct ixfr_proc *proc)
-{
-	assert(proc->final_soa == NULL);
-	if (rr->type != KNOT_RRTYPE_SOA) {
-		return KNOT_EMALF;
-	}
-
-	// Store the first SOA for later use.
-	proc->final_soa = knot_rrset_copy(rr, proc->mm);
-	if (proc->final_soa == NULL) {
-		return KNOT_ENOMEM;
-	}
-
-	return KNOT_EOK;
-}
-
-/*! \brief Decides what to do with a starting SOA (deletions). */
-static int solve_soa_del(const knot_rrset_t *rr, struct ixfr_proc *proc)
-{
-	if (rr->type != KNOT_RRTYPE_SOA) {
-		return KNOT_EMALF;
-	}
-
-	// Create new changeset.
-	changeset_t *change = changeset_new(proc->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_clear(change);
-		return KNOT_ENOMEM;
-	}
-
-	// Add changeset.
-	add_tail(&proc->changesets, &change->n);
-	++proc->change_count;
-
-	return KNOT_EOK;
-}
-
-/*! \brief Stores ending SOA into changeset. */
-static int solve_soa_add(const knot_rrset_t *rr, changeset_t *change, knot_mm_t *mm)
-{
-	assert(rr->type == KNOT_RRTYPE_SOA);
-	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 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 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 ixfrin_next_state(struct ixfr_proc *proc, const knot_rrset_t *rr)
-{
-	const bool soa = (rr->type == KNOT_RRTYPE_SOA);
-	if (soa &&
-	    (proc->state == IXFR_SOA_ADD || proc->state == IXFR_ADD)) {
-		// Check end of transfer.
-		if (knot_rrset_equal(rr, proc->final_soa,
-		                     KNOT_RRSET_COMPARE_WHOLE)) {
-			// Final SOA encountered, transfer done.
-			return IXFR_DONE;
-		}
-	}
-
-	switch (proc->state) {
-	case IXFR_START:
-		// Final SOA already stored or transfer start.
-		return proc->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 0;
-	}
-}
-
-/*!
- * \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 ixfrin_step(const knot_rrset_t *rr, struct ixfr_proc *proc)
-{
-	proc->state = ixfrin_next_state(proc, rr);
-	changeset_t *change = TAIL(proc->changesets);
-
-	int ret;
-	switch (proc->state) {
-	case IXFR_START:
-		return solve_start(rr, proc);
-	case IXFR_SOA_DEL:
-		ret = solve_soa_del(rr, proc);
-		break;
-	case IXFR_DEL:
-		ret = solve_del(rr, change, proc->mm);
-		break;
-	case IXFR_SOA_ADD:
-		ret = solve_soa_add(rr, change, proc->mm);
-		break;
-	case IXFR_ADD:
-		ret = solve_add(rr, change, proc->mm);
-		break;
-	case IXFR_DONE:
-		return KNOT_EOK;
-	default:
-		return KNOT_ERROR;
-	}
-	if (ret == KNOT_EOK) {
-		proc->change_size += knot_rrset_size(rr);
-	}
-	return ret;
-}
-
-/*! \brief Checks whether journal node limit has not been exceeded. */
-static bool journal_limit_exceeded(struct ixfr_proc *proc)
-{
-	return proc->change_count > JOURNAL_NCOUNT;
-}
-
-/*! \brief Checks whether RR belongs into zone. */
-static bool out_of_zone(const knot_rrset_t *rr, struct ixfr_proc *proc)
-{
-	return !knot_dname_in(proc->zone->name, rr->owner);
-}
-
-/*!
- * \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 process_ixfrin_packet(knot_pkt_t *pkt, struct answer_data *adata)
-{
-	struct ixfr_proc *ixfr = (struct ixfr_proc *)adata->ext;
-
-	// Update counters.
-	ixfr->proc.npkts  += 1;
-	ixfr->proc.nbytes += pkt->size;
-
-	conf_val_t val = conf_zone_get(adata->param->conf, C_MAX_ZONE_SIZE,
-	                               ixfr->zone->name);
-	const int64_t size_limit = conf_int(&val);
-
-	// Process RRs in the message.
-	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
-	for (uint16_t i = 0; i < answer->count; ++i) {
-		if (journal_limit_exceeded(ixfr)) {
-			IXFRIN_LOG(LOG_WARNING, "journal is full");
-			return KNOT_STATE_FAIL;
-		}
-
-		const knot_rrset_t *rr = knot_pkt_rr(answer, i);
-		if (out_of_zone(rr, ixfr)) {
-			continue;
-		}
-
-		int ret = ixfrin_step(rr, ixfr);
-		if (ret != KNOT_EOK) {
-			IXFRIN_LOG(LOG_WARNING, "failed (%s)", knot_strerror(ret));
-			return KNOT_STATE_FAIL;
-		}
-
-		if (ixfr->state == IXFR_DONE) {
-			// Transfer done, do not consume more RRs.
-			return KNOT_STATE_DONE;
-		}
-
-		if (ixfr->change_size > 2 * size_limit) {
-			IXFRIN_LOG(LOG_WARNING, "transfer size exceeded");
-		}
-
-	}
-
-	return KNOT_STATE_CONSUME;
-}
-
-/* --------------------------------- API ------------------------------------ */
-
 int ixfr_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 {
 	if (pkt == NULL || qdata == NULL) {
@@ -656,7 +306,6 @@ int ixfr_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 	}
 
 	int ret = KNOT_EOK;
-	struct timeval now = {0};
 	struct ixfr_proc *ixfr = (struct ixfr_proc*)qdata->ext;
 
 	/* If IXFR is disabled, respond with SOA. */
@@ -670,20 +319,20 @@ int ixfr_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 		switch(ret) {
 		case KNOT_EOK:      /* OK */
 			ixfr = (struct ixfr_proc*)qdata->ext;
-			IXFROUT_LOG(LOG_INFO, "started, serial %u -> %u",
+			IXFROUT_LOG(LOG_INFO, qdata, "started, serial %u -> %u",
 			            knot_soa_serial(&ixfr->soa_from->rrs),
 			            knot_soa_serial(&ixfr->soa_to->rrs));
 			break;
 		case KNOT_EUPTODATE: /* Our zone is same age/older, send SOA. */
-			IXFROUT_LOG(LOG_INFO, "zone is up-to-date");
+			IXFROUT_LOG(LOG_INFO, qdata, "zone is up-to-date");
 			return ixfr_answer_soa(pkt, qdata);
 		case KNOT_ERANGE:   /* No history -> AXFR. */
 		case KNOT_ENOENT:
-			IXFROUT_LOG(LOG_INFO, "incomplete history, fallback to AXFR");
+			IXFROUT_LOG(LOG_INFO, qdata, "incomplete history, fallback to AXFR");
 			qdata->packet_type = KNOT_QUERY_AXFR; /* Solve as AXFR. */
 			return axfr_process_query(pkt, qdata);
 		default:            /* Server errors. */
-			IXFROUT_LOG(LOG_ERR, "failed to start (%s)", knot_strerror(ret));
+			IXFROUT_LOG(LOG_ERR, qdata, "failed to start (%s)", knot_strerror(ret));
 			return KNOT_STATE_FAIL;
 		}
 	}
@@ -697,93 +346,16 @@ int ixfr_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 	case KNOT_ESPACE: /* Couldn't write more, send packet and continue. */
 		return KNOT_STATE_PRODUCE; /* Check for more. */
 	case KNOT_EOK:    /* Last response. */
-		gettimeofday(&now, NULL);
-		IXFROUT_LOG(LOG_INFO,
-		            "finished, %.02f seconds, %u messages, %u bytes",
-		            time_diff(&ixfr->proc.tstamp, &now) / 1000.0,
-		            ixfr->proc.npkts, ixfr->proc.nbytes);
+		xfr_stats_end(&ixfr->proc.stats);
+		xfr_log_finished(ZONE_NAME(qdata), LOG_OPERATION_IXFR, LOG_DIRECTION_OUT,
+				 REMOTE(qdata), &ixfr->proc.stats);
 		ret = KNOT_STATE_DONE;
 		break;
 	default:          /* Generic error. */
-		IXFROUT_LOG(LOG_ERR, "failed (%s)", knot_strerror(ret));
+		IXFROUT_LOG(LOG_ERR, qdata, "failed (%s)", knot_strerror(ret));
 		ret = KNOT_STATE_FAIL;
 		break;
 	}
 
 	return ret;
 }
-
-static int check_format(knot_pkt_t *pkt)
-{
-	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
-
-	if (answer->count >= 1 && knot_pkt_rr(answer, 0)->type == KNOT_RRTYPE_SOA) {
-		return KNOT_EOK;
-	} else {
-		return KNOT_EMALF;
-	}
-}
-
-int ixfr_process_answer(knot_pkt_t *pkt, struct answer_data *adata)
-{
-	if (pkt == NULL || adata == NULL) {
-		return KNOT_STATE_FAIL;
-	}
-
-	/* Check RCODE. */
-	uint8_t rcode = knot_wire_get_rcode(pkt->wire);
-	if (rcode != KNOT_RCODE_NOERROR) {
-		const knot_lookup_t *lut = knot_lookup_by_id(knot_rcode_names, rcode);
-		if (lut != NULL) {
-			IXFRIN_LOG(LOG_WARNING, "server responded with %s", lut->name);
-		}
-		return KNOT_STATE_FAIL;
-	}
-
-	if (adata->ext == NULL) {
-		if (check_format(pkt) != KNOT_EOK) {
-			IXFRIN_LOG(LOG_WARNING, "malformed response");
-			return KNOT_STATE_FAIL;
-		}
-
-		/* Check for AXFR-style IXFR. */
-		if (ixfr_is_axfr(pkt)) {
-			IXFRIN_LOG(LOG_NOTICE, "receiving AXFR-style IXFR");
-			adata->response_type = KNOT_RESPONSE_AXFR;
-			return axfr_process_answer(pkt, adata);
-		}
-
-		/* Initialize processing with first packet. */
-		NS_NEED_TSIG_SIGNED(&adata->param->tsig_ctx, 0);
-		if (!zone_transfer_needed(adata->param->zone, pkt)) {
-			if (knot_pkt_section(pkt, KNOT_ANSWER)->count > 1) {
-				IXFRIN_LOG(LOG_WARNING, "old data, ignoring");
-			} else {
-				/* Single-SOA answer. */
-				IXFRIN_LOG(LOG_INFO, "zone is up-to-date");
-			}
-			return KNOT_STATE_DONE;
-		}
-
-		IXFRIN_LOG(LOG_INFO, "starting");
-		// First packet with IXFR, init context
-		int ret = ixfrin_answer_init(adata);
-		if (ret != KNOT_EOK) {
-			IXFRIN_LOG(LOG_WARNING, "failed (%s)", knot_strerror(ret));
-			return KNOT_STATE_FAIL;
-		}
-	} else {
-		NS_NEED_TSIG_SIGNED(&adata->param->tsig_ctx, 100);
-	}
-
-	int ret = process_ixfrin_packet(pkt, adata);
-	if (ret == KNOT_STATE_DONE) {
-		NS_NEED_TSIG_SIGNED(&adata->param->tsig_ctx, 0);
-		int fret = ixfrin_finalize(adata);
-		if (fret != KNOT_EOK) {
-			ret = KNOT_STATE_FAIL;
-		}
-	}
-
-	return ret;
-}
diff --git a/src/knot/nameserver/ixfr.h b/src/knot/nameserver/ixfr.h
index 3bd8dfd18..ec212eb13 100644
--- a/src/knot/nameserver/ixfr.h
+++ b/src/knot/nameserver/ixfr.h
@@ -24,9 +24,41 @@
 
 #pragma once
 
-#include "libknot/packet/pkt.h"
-#include "knot/query/query.h"
 #include "knot/nameserver/process_query.h"
+#include "knot/nameserver/xfr.h"
+#include "knot/query/query.h"
+#include "libknot/packet/pkt.h"
+
+/*! \brief IXFR-in processing states. */
+enum ixfr_state {
+	IXFR_INVALID = 0,
+	IXFR_START,      /* IXFR-in starting, expecting final SOA. */
+	IXFR_SOA_DEL,    /* Expecting starting SOA. */
+	IXFR_SOA_ADD,    /* Expecting ending SOA. */
+	IXFR_DEL,        /* Expecting RR to delete. */
+	IXFR_ADD,        /* Expecting RR to add. */
+	IXFR_DONE        /* Processing done, IXFR-in complete. */
+};
+
+/*! \brief Extended structure for IXFR-in/IXFR-out processing. */
+struct ixfr_proc {
+	/* Processing state. */
+	struct xfr_proc proc;
+	enum ixfr_state state;
+
+	/* Changes to be sent. */
+	list_t changesets;
+
+	/* Currenty processed changeset. */
+	knot_rrset_t cur_rr;
+	changeset_iter_t cur;
+	const knot_rrset_t *soa_from;
+	const knot_rrset_t *soa_to;
+
+	/* Processing context. */
+	struct query_data *qdata;
+	knot_mm_t *mm;
+};
 
 /*!
  * \brief IXFR query processing module.
@@ -37,13 +69,4 @@
  */
 int ixfr_process_query(knot_pkt_t *pkt, struct query_data *qdata);
 
-/*!
- * \brief IXFR response processing module.
- *
- * \retval CONSUME if more data are required.
- * \retval FAIL if it encountered an error, retry over AXFR will be done.
- * \retval DONE if finished.
- */
-int ixfr_process_answer(knot_pkt_t *pkt, struct answer_data *adata);
-
 /*! @} */
diff --git a/src/knot/nameserver/log.h b/src/knot/nameserver/log.h
index 3f6268a9f..73d3b0571 100644
--- a/src/knot/nameserver/log.h
+++ b/src/knot/nameserver/log.h
@@ -18,15 +18,70 @@
 
 #include "contrib/sockaddr.h"
 #include "knot/common/log.h"
+#include "libknot/dname.h"
+
+enum log_operation {
+	LOG_OPERATION_AXFR,
+	LOG_OPERATION_IXFR,
+	LOG_OPERATION_NOTIFY,
+	LOG_OPERATION_REFRESH,
+	LOG_OPERATION_UPDATE,
+};
+
+enum log_direction {
+	LOG_DIRECTION_IN,
+	LOG_DIRECTION_OUT,
+};
+
+static inline const char *log_operation_name(enum log_operation operation)
+{
+	switch (operation) {
+	case LOG_OPERATION_AXFR:
+		return "AXFR";
+	case LOG_OPERATION_IXFR:
+		return "IXFR";
+	case LOG_OPERATION_NOTIFY:
+		return "notify";
+	case LOG_OPERATION_REFRESH:
+		return "refresh";
+	case LOG_OPERATION_UPDATE:
+		return "DDNS";
+	default:
+		return "?";
+	}
+}
+
+static inline const char *log_direction_name(enum log_direction direction)
+{
+	switch (direction) {
+	case LOG_DIRECTION_IN:
+		return "incoming";
+	case LOG_DIRECTION_OUT:
+		return "outgoing";
+	default:
+		return "?";
+	}
+}
 
 /*!
- * \brief Base log message format for network communication.
+ * \brief Generate log message for server communication.
+ *
+ * If this macro was a function:
+ *
+ * void ns_log(int priority, const knot_dname_t *zone, enum log_operation op,
+ *             enum log_direction dir, const struct sockaddr *remote,
+ *             const char *fmt, ...);
+ *
+ * Example output:
+ *
+ * [example.com] NOTIFY, outgoing, 2001:db8::1@53: serial 123
  *
- * Emits a message in the following format:
- * > [zone] operation, address: custom formatted message
  */
-#define NS_PROC_LOG(priority, zone, remote, operation, msg, ...) do { \
-	char addr[SOCKADDR_STRLEN] = ""; \
-	sockaddr_tostr(addr, sizeof(addr), (struct sockaddr *)remote); \
-	log_msg_zone(priority, zone, "%s, %s: " msg, operation, addr, ##__VA_ARGS__); \
+#define ns_log(priority, zone, op, dir, remote, fmt, ...) \
+	do { \
+		char address[SOCKADDR_STRLEN] = ""; \
+		sockaddr_tostr(address, sizeof(address), remote); \
+		log_msg_zone(priority, zone, "%s, %s, %s: " fmt, \
+		             log_operation_name(op), log_direction_name(dir), address, \
+		             ## __VA_ARGS__); \
 	} while (0)
diff --git a/src/knot/nameserver/notify.c b/src/knot/nameserver/notify.c
index 53795c334..59dee7c4f 100644
--- a/src/knot/nameserver/notify.c
+++ b/src/knot/nameserver/notify.c
@@ -24,15 +24,9 @@
 #include "dnssec/random.h"
 #include "libknot/libknot.h"
 
-/* NOTIFY-specific logging (internal, expects 'qdata' variable set). */
-#define NOTIFY_IN_LOG(priority, msg, ...) \
-	NS_PROC_LOG(priority, knot_pkt_qname(qdata->query), qdata->param->remote, \
-	            "NOTIFY, incoming", msg, ##__VA_ARGS__)
-
-/* NOTIFY-specific logging (internal, expects 'adata' variable set). */
-#define NOTIFY_OUT_LOG(priority, msg, ...) \
-	NS_PROC_LOG(priority, adata->param->zone->name, adata->param->remote, \
-	            "NOTIFY, outgoing", msg, ##__VA_ARGS__)
+#define NOTIFY_IN_LOG(priority, qdata, fmt...) \
+	ns_log(priority, knot_pkt_qname(qdata->query), LOG_OPERATION_NOTIFY, \
+	       LOG_DIRECTION_IN, (struct sockaddr *)qdata->param->remote, fmt)
 
 static int notify_check_query(struct query_data *qdata)
 {
@@ -58,7 +52,7 @@ int notify_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 	if (state == KNOT_STATE_FAIL) {
 		switch (qdata->rcode) {
 		case KNOT_RCODE_NOTAUTH: /* Not authoritative or ACL check failed. */
-			NOTIFY_IN_LOG(LOG_NOTICE, "unauthorized request");
+			NOTIFY_IN_LOG(LOG_NOTICE, qdata, "unauthorized request");
 			break;
 		case KNOT_RCODE_FORMERR: /* Silently ignore bad queries. */
 		default:
@@ -76,12 +70,12 @@ int notify_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 		const knot_rrset_t *soa = knot_pkt_rr(answer, 0);
 		if (soa->type == KNOT_RRTYPE_SOA) {
 			uint32_t serial = knot_soa_serial(&soa->rrs);
-			NOTIFY_IN_LOG(LOG_INFO, "received serial %u", serial);
+			NOTIFY_IN_LOG(LOG_INFO, qdata, "received serial %u", serial);
 		} else { /* Complain, but accept N/A record. */
-			NOTIFY_IN_LOG(LOG_NOTICE, "received, bad record in answer section");
+			NOTIFY_IN_LOG(LOG_NOTICE, qdata, "received, bad record in answer section");
 		}
 	} else {
-		NOTIFY_IN_LOG(LOG_INFO, "received, doesn't have SOA");
+		NOTIFY_IN_LOG(LOG_INFO, qdata, "received, doesn't have SOA");
 	}
 
 	/* Incoming NOTIFY expires REFRESH timer and renews EXPIRE timer. */
@@ -91,24 +85,3 @@ int notify_process_query(knot_pkt_t *pkt, struct query_data *qdata)
 
 	return KNOT_STATE_DONE;
 }
-
-int notify_process_answer(knot_pkt_t *pkt, struct answer_data *adata)
-{
-	if (pkt == NULL || adata == NULL) {
-		return KNOT_STATE_FAIL;
-	}
-
-	/* Check RCODE. */
-	uint8_t rcode = knot_wire_get_rcode(pkt->wire);
-	if (rcode != KNOT_RCODE_NOERROR) {
-		const knot_lookup_t *lut = knot_lookup_by_id(knot_rcode_names, rcode);
-		if (lut != NULL) {
-			NOTIFY_OUT_LOG(LOG_WARNING, "server responded with %s", lut->name);
-		}
-		return KNOT_STATE_FAIL;
-	}
-
-	NS_NEED_TSIG_SIGNED(&adata->param->tsig_ctx, 0);
-
-	return KNOT_STATE_DONE; /* No processing. */
-}
diff --git a/src/knot/nameserver/notify.h b/src/knot/nameserver/notify.h
index 6d508ba37..24e0bcced 100644
--- a/src/knot/nameserver/notify.h
+++ b/src/knot/nameserver/notify.h
@@ -38,12 +38,4 @@
  */
 int notify_process_query(knot_pkt_t *pkt, struct query_data *qdata);
 
-/*!
- * \brief Process an answer to the NOTIFY query.
- *
- * \retval FAIL if it encountered an error.
- * \retval DONE if finished.
- */
-int notify_process_answer(knot_pkt_t *pkt, struct answer_data *data);
-
 /*! @} */
diff --git a/src/knot/nameserver/process_query.c b/src/knot/nameserver/process_query.c
index f8fb701de..af18d495e 100644
--- a/src/knot/nameserver/process_query.c
+++ b/src/knot/nameserver/process_query.c
@@ -689,7 +689,6 @@ int process_query_sign_response(knot_pkt_t *pkt, struct query_data *qdata)
 
 	/* KEY provided and verified TSIG or BADTIME allows signing. */
 	if (ctx->tsig_key.name != NULL && knot_tsig_can_sign(qdata->rcode_tsig)) {
-
 		/* Sign query response. */
 		size_t new_digest_len = dnssec_tsig_algorithm_size(ctx->tsig_key.algorithm);
 		if (ctx->pkt_count == 0) {
diff --git a/src/knot/nameserver/update.c b/src/knot/nameserver/update.c
index 9bd48a80c..89815bec7 100644
--- a/src/knot/nameserver/update.c
+++ b/src/knot/nameserver/update.c
@@ -31,10 +31,9 @@
 #include "contrib/net.h"
 #include "contrib/time.h"
 
-/* UPDATE-specific logging (internal, expects 'qdata' variable set). */
-#define UPDATE_LOG(severity, msg, ...) \
-	NS_PROC_LOG(severity, knot_pkt_qname(qdata->query), qdata->param->remote, \
-	            "DDNS", msg, ##__VA_ARGS__)
+#define UPDATE_LOG(priority, qdata, fmt...) \
+	ns_log(priority, knot_pkt_qname(qdata->query), LOG_OPERATION_UPDATE, \
+	       LOG_DIRECTION_IN, (struct sockaddr *)qdata->param->remote, fmt)
 
 static void init_qdata_from_request(struct query_data *qdata,
                                     const zone_t *zone,
@@ -55,7 +54,7 @@ static int check_prereqs(struct knot_request *request,
 	uint16_t rcode = KNOT_RCODE_NOERROR;
 	int ret = ddns_process_prereqs(request->query, update, &rcode);
 	if (ret != KNOT_EOK) {
-		UPDATE_LOG(LOG_WARNING, "prerequisites not met (%s)",
+		UPDATE_LOG(LOG_WARNING, qdata, "prerequisites not met (%s)",
 		           knot_strerror(ret));
 		assert(rcode != KNOT_RCODE_NOERROR);
 		knot_wire_set_rcode(request->resp->wire, rcode);
@@ -72,7 +71,7 @@ static int process_single_update(struct knot_request *request,
 	uint16_t rcode = KNOT_RCODE_NOERROR;
 	int ret = ddns_process_update(zone, request->query, update, &rcode);
 	if (ret != KNOT_EOK) {
-		UPDATE_LOG(LOG_WARNING, "failed to apply (%s)",
+		UPDATE_LOG(LOG_WARNING, qdata, "failed to apply (%s)",
 		           knot_strerror(ret));
 		assert(rcode != KNOT_RCODE_NOERROR);
 		knot_wire_set_rcode(request->resp->wire, rcode);
@@ -233,7 +232,7 @@ static int remote_forward(conf_t *conf, struct knot_request *request, conf_remot
 	/* Create a request. */
 	const struct sockaddr *dst = (const struct sockaddr *)&remote->addr;
 	const struct sockaddr *src = (const struct sockaddr *)&remote->via;
-	struct knot_request *req = knot_request_make(re.mm, dst, src, query, 0);
+	struct knot_request *req = knot_request_make(re.mm, dst, src, query, NULL, 0);
 	if (req == NULL) {
 		knot_requestor_clear(&re);
 		knot_pkt_free(&query);
@@ -305,14 +304,14 @@ static bool update_tsig_check(conf_t *conf, struct query_data *qdata, struct kno
 {
 	// Check that ACL is still valid.
 	if (!process_query_acl_check(conf, qdata->zone->name, ACL_ACTION_UPDATE, qdata)) {
-		UPDATE_LOG(LOG_WARNING, "ACL check failed");
+		UPDATE_LOG(LOG_WARNING, qdata, "ACL check failed");
 		knot_wire_set_rcode(req->resp->wire, qdata->rcode);
 		return false;
 	} else {
 		// Check TSIG validity.
 		int ret = process_query_verify(qdata);
 		if (ret != KNOT_EOK) {
-			UPDATE_LOG(LOG_WARNING, "failed (%s)",
+			UPDATE_LOG(LOG_WARNING, qdata, "failed (%s)",
 			           knot_strerror(ret));
 			knot_wire_set_rcode(req->resp->wire, qdata->rcode);
 			return false;
diff --git a/src/knot/nameserver/xfr.c b/src/knot/nameserver/xfr.c
new file mode 100644
index 000000000..b9419191e
--- /dev/null
+++ b/src/knot/nameserver/xfr.c
@@ -0,0 +1,87 @@
+/*  Copyright (C) 2016 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 "knot/nameserver/xfr.h"
+#include "contrib/mempattern.h"
+
+int xfr_process_list(knot_pkt_t *pkt, xfr_put_cb process_item,
+                     struct query_data *qdata)
+{
+	if (pkt == NULL || qdata == NULL || qdata->ext == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	int ret = KNOT_EOK;
+	knot_mm_t *mm = qdata->mm;
+	struct xfr_proc *xfer = qdata->ext;
+
+	zone_contents_t *zone = qdata->zone->contents;
+	knot_rrset_t soa_rr = node_rrset(zone->apex, KNOT_RRTYPE_SOA);
+
+	/* Prepend SOA on first packet. */
+	if (xfer->stats.messages == 0) {
+		ret = knot_pkt_put(pkt, 0, &soa_rr, KNOT_PF_NOTRUNC);
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+	}
+
+	/* Process all items in the list. */
+	while (!EMPTY_LIST(xfer->nodes)) {
+		ptrnode_t *head = HEAD(xfer->nodes);
+		ret = process_item(pkt, head->d, xfer);
+		if (ret == KNOT_EOK) { /* Finished. */
+			/* Complete change set. */
+			rem_node((node_t *)head);
+			mm_free(mm, head);
+		} else { /* Packet full or other error. */
+			break;
+		}
+	}
+
+	/* Append SOA on last packet. */
+	if (ret == KNOT_EOK) {
+		ret = knot_pkt_put(pkt, 0, &soa_rr, KNOT_PF_NOTRUNC);
+	}
+
+	/* Update counters. */
+	xfr_stats_add(&xfer->stats, pkt->size);
+
+	return ret;
+}
+
+void xfr_stats_begin(struct xfr_stats *stats)
+{
+	assert(stats);
+
+	memset(stats, 0, sizeof(*stats));
+	stats->begin = time_now();
+}
+
+void xfr_stats_add(struct xfr_stats *stats, unsigned bytes)
+{
+	assert(stats);
+
+	stats->messages += 1;
+	stats->bytes += bytes;
+}
+
+void xfr_stats_end(struct xfr_stats *stats)
+{
+	assert(stats);
+
+	stats->end = time_now();
+}
diff --git a/src/knot/nameserver/xfr.h b/src/knot/nameserver/xfr.h
new file mode 100644
index 000000000..605085ff6
--- /dev/null
+++ b/src/knot/nameserver/xfr.h
@@ -0,0 +1,69 @@
+/*  Copyright (C) 2016 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/>.
+ */
+
+#pragma once
+
+#include "contrib/time.h"
+#include "contrib/ucw/lists.h"
+#include "knot/nameserver/log.h"
+#include "knot/nameserver/process_query.h"
+#include "knot/zone/contents.h"
+#include "libknot/packet/pkt.h"
+
+struct xfr_stats {
+	unsigned messages;
+	unsigned bytes;
+	struct timespec begin;
+	struct timespec end;
+};
+
+void xfr_stats_begin(struct xfr_stats *stats);
+void xfr_stats_add(struct xfr_stats *stats, unsigned bytes);
+void xfr_stats_end(struct xfr_stats *stats);
+
+static inline
+void xfr_log_finished(const knot_dname_t *zone, enum log_operation op,
+                      enum log_direction dir, const struct sockaddr *remote,
+                      const struct xfr_stats *stats)
+{
+	ns_log(LOG_INFO, zone, op, dir, remote,
+	       "finished, %0.2f seconds, %u messages, %u bytes",
+	       time_diff_ms(&stats->begin, &stats->end) / 1000.0,
+	       stats->messages, stats->bytes);
+}
+
+/*!
+ * \brief Generic transfer processing state.
+ */
+struct xfr_proc {
+	list_t nodes;               //!< Items to process (ptrnode_t).
+	zone_contents_t *contents;  //!< Processed zone.
+	struct xfr_stats stats;     //!< Packet transfer statistics.
+};
+
+/*!
+ * \brief Generic transfer processing.
+ *
+ * \return KNOT_EOK or an error
+ */
+typedef int (*xfr_put_cb)(knot_pkt_t *pkt, const void *item, struct xfr_proc *xfer);
+
+/*!
+ * \brief Put all items from xfr_proc.nodes to packet using a callback function.
+ *
+ * \note qdata->ext points to struct xfr_proc* (this is xfer-specific context)
+ */
+int xfr_process_list(knot_pkt_t *pkt, xfr_put_cb put, struct query_data *qdata);
diff --git a/src/knot/query/query.c b/src/knot/query/query.c
index ae078ee19..f4d0826bd 100644
--- a/src/knot/query/query.c
+++ b/src/knot/query/query.c
@@ -14,256 +14,100 @@
     along with this program.  If not, see <http://www.gnu.org/licenses/>.
  */
 
+#include "knot/query/query.h"
+
 #include <stdint.h>
-#include <string.h>
 
-#include "contrib/mempattern.h"
-#include "contrib/ucw/mempool.h"
 #include "contrib/wire.h"
+#include "contrib/wire_ctx.h"
 #include "dnssec/random.h"
-#include "knot/query/query.h"
-#include "knot/query/requestor.h"
-#include "knot/zone/zone.h"
-#include "libknot/mm_ctx.h"
-#include "libknot/packet/pkt.h"
+#include "knot/conf/conf.h"
 #include "libknot/yparser/yptrafo.h"
+#include "libknot/rrset.h"
 
-// event handlers:
-#include "knot/nameserver/axfr.h"
-#include "knot/nameserver/internet.h"
-#include "knot/nameserver/ixfr.h"
-#include "knot/nameserver/notify.h"
-
-/*! \brief Accessor to query-specific data. */
-#define ANSWER_DATA(ctx) ((struct answer_data *)(ctx)->data)
-#define RESPONSE_TYPE_UNSET -1
-
-static void answer_data_init(knot_layer_t *ctx, void *module_param)
+int query_init_pkt(knot_pkt_t *pkt)
 {
-	/* Initialize persistent data. */
-	struct answer_data *data = ANSWER_DATA(ctx);
-	memset(data, 0, sizeof(struct answer_data));
-	data->response_type = RESPONSE_TYPE_UNSET;
-	data->mm = ctx->mm;
-	data->param = module_param;
-}
-
-/*! \brief Answer is paired to query if MsgId matches.
- *  @note Zone transfers are deliberate with QUESTION section and may not
- *        include it in multi-packet responses, therefore the response can be
- *        paired by MsgId only.
- */
-static bool is_answer_to_query(const knot_pkt_t *query, knot_pkt_t *answer)
-{
-	return knot_wire_get_id(query->wire) == knot_wire_get_id(answer->wire);
-}
-
-static int process_answer_begin(knot_layer_t *ctx, void *module_param)
-{
-	/* Initialize context. */
-	assert(ctx);
-	ctx->data = mm_alloc(ctx->mm, sizeof(struct answer_data));
-
-	/* Initialize persistent data. */
-	answer_data_init(ctx, module_param);
-
-	/* Issue the query. */
-	return KNOT_STATE_PRODUCE;
-}
-
-static int process_answer_reset(knot_layer_t *ctx)
-{
-	assert(ctx);
-	struct answer_data *data = ANSWER_DATA(ctx);
-
-	/* Remember persistent parameters. */
-	struct process_answer_param *module_param = data->param;
-
-	/* Free allocated data. */
-	if (data->ext_cleanup != NULL) {
-		data->ext_cleanup(data);
+	if (!pkt) {
+		return KNOT_EINVAL;
 	}
 
-	/* Initialize persistent data. */
-	answer_data_init(ctx, module_param);
+	knot_pkt_clear(pkt);
+	knot_wire_set_id(pkt->wire, dnssec_random_uint16_t());
 
-	/* Issue the query. */
-	return KNOT_STATE_PRODUCE;
+	return KNOT_EOK;
 }
 
-static int process_answer_finish(knot_layer_t *ctx)
+int query_edns_data_init(struct query_edns_data *edns_ptr, conf_t *conf,
+                         const knot_dname_t *zone, int remote_family)
 {
-	process_answer_reset(ctx);
-	mm_free(ctx->mm, ctx->data);
-	ctx->data = NULL;
-
-	return KNOT_STATE_NOOP;
-}
-
-/* \note Private helper for process_answer repetitive checks. */
-#define ANSWER_REQUIRES(condition, ret) \
-	if (!(condition)) { \
-		knot_pkt_free(&pkt); \
-		return ret; \
+	if (!edns_ptr || !conf || !zone) {
+		return KNOT_EINVAL;
 	}
 
-static int process_answer(knot_layer_t *ctx, knot_pkt_t *pkt)
-{
-	assert(pkt && ctx);
-	struct answer_data *data = ANSWER_DATA(ctx);
-
-	/* Check parse state. */
-	ANSWER_REQUIRES(pkt->parsed >= KNOT_WIRE_HEADER_SIZE, KNOT_STATE_FAIL);
-	ANSWER_REQUIRES(pkt->parsed == pkt->size, KNOT_STATE_FAIL);
-	/* Accept only responses. */
-	ANSWER_REQUIRES(knot_wire_get_qr(pkt->wire), KNOT_STATE_NOOP);
-	/* Check if we want answer paired to query. */
-	const knot_pkt_t *query = data->param->query;
-	if (!query) {
-		return KNOT_STATE_FAIL;
-	}
-	ANSWER_REQUIRES(is_answer_to_query(query, pkt), KNOT_STATE_NOOP);
+	struct query_edns_data edns = { 0 };
 
-	/* Verify incoming packet. */
-	int ret = tsig_verify_packet(&data->param->tsig_ctx, pkt);
-	if (ret != KNOT_EOK) {
-		NS_PROC_LOG(LOG_WARNING, data->param->zone->name, data->param->remote,
-		            "response", "denied (%s)", knot_strerror(ret));
-		return KNOT_STATE_FAIL;
-	}
+	// Determine max payload
 
-	/* Call appropriate processing handler. */
-	int next_state = KNOT_STATE_NOOP;
-	if (data->response_type == RESPONSE_TYPE_UNSET) {
-		/* @note We can't derive type from response, as it may not contain QUESTION at all. */
-		data->response_type = knot_pkt_type(query) | KNOT_RESPONSE;
-	}
-	switch(data->response_type) {
-	case KNOT_RESPONSE_NORMAL:
-		next_state = internet_process_answer(pkt, data);
-		break;
-	case KNOT_RESPONSE_AXFR:
-		next_state = axfr_process_answer(pkt, data);
-		break;
-	case KNOT_RESPONSE_IXFR:
-		next_state = ixfr_process_answer(pkt, data);
+	switch (remote_family) {
+	case AF_INET:
+		edns.max_payload = conf->cache.srv_max_ipv4_udp_payload;
 		break;
-	case KNOT_RESPONSE_NOTIFY:
-		next_state = notify_process_answer(pkt, data);
+	case AF_INET6:
+		edns.max_payload = conf->cache.srv_max_ipv6_udp_payload;
 		break;
 	default:
-		next_state = KNOT_STATE_NOOP;
-		break;
-	}
-
-	return next_state;
-}
-#undef ANSWER_REQUIRES
-
-static int prepare_query(knot_layer_t *ctx, knot_pkt_t *pkt)
-{
-	/* \note Don't touch the query, expect answer. */
-	return KNOT_STATE_CONSUME;
-}
-
-/*! \brief Module implementation. */
-const knot_layer_api_t *process_answer_layer(void)
-{
-	static const knot_layer_api_t api = {
-		.begin = &process_answer_begin,
-		.reset = &process_answer_reset,
-		.finish = &process_answer_finish,
-		.consume = &process_answer,
-		.produce = &prepare_query,
-	};
-	return &api;
-}
-
-
-
-/*! \brief Create zone query packet. */
-static knot_pkt_t *zone_query(const zone_t *zone, uint16_t pkt_type, knot_mm_t *mm)
-{
-	/* Determine query type and opcode. */
-	uint16_t query_type = KNOT_RRTYPE_SOA;
-	uint16_t opcode = KNOT_OPCODE_QUERY;
-	switch(pkt_type) {
-	case KNOT_QUERY_AXFR: query_type = KNOT_RRTYPE_AXFR; break;
-	case KNOT_QUERY_IXFR: query_type = KNOT_RRTYPE_IXFR; break;
-	case KNOT_QUERY_NOTIFY: opcode = KNOT_OPCODE_NOTIFY; break;
-	}
-
-	knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, mm);
-	if (pkt == NULL) {
-		return NULL;
+		return KNOT_EINVAL;
 	}
 
-	knot_wire_set_id(pkt->wire, dnssec_random_uint16_t());
-	knot_wire_set_opcode(pkt->wire, opcode);
-	if (pkt_type == KNOT_QUERY_NOTIFY) {
-		knot_wire_set_aa(pkt->wire);
-	}
+	// Determine custom option
 
-	knot_pkt_put_question(pkt, zone->name, KNOT_CLASS_IN, query_type);
-
-	/* Put current SOA (optional). */
-	zone_contents_t *contents = zone->contents;
-	if (pkt_type == KNOT_QUERY_IXFR) {  /* RFC1995, SOA in AUTHORITY. */
-		knot_pkt_begin(pkt, KNOT_AUTHORITY);
-		knot_rrset_t soa_rr = node_rrset(contents->apex, KNOT_RRTYPE_SOA);
-		knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, &soa_rr, 0);
-	} else if (pkt_type == KNOT_QUERY_NOTIFY) { /* RFC1996, SOA in ANSWER. */
-		knot_pkt_begin(pkt, KNOT_ANSWER);
-		knot_rrset_t soa_rr = node_rrset(contents->apex, KNOT_RRTYPE_SOA);
-		knot_pkt_put(pkt, KNOT_COMPR_HINT_QNAME, &soa_rr, 0);
-	}
-
-	return pkt;
+	conf_val_t val = conf_zone_get(conf, C_REQUEST_EDNS_OPTION, zone);
+	size_t opt_len = 0;
+	const uint8_t *opt_data = conf_data(&val, &opt_len);
+	if (opt_data != NULL) {
+		wire_ctx_t ctx = wire_ctx_init_const(opt_data, opt_len);
+		edns.custom_code = wire_ctx_read_u64(&ctx);
+		#warning No boundary check in the yparser API.
+		edns.custom_len  = yp_bin_len(ctx.position);
+		edns.custom_data = yp_bin(ctx.position);
+		if (ctx.error != KNOT_EOK) {
+			return KNOT_EINVAL;
+		}
+	}
+
+	*edns_ptr = edns;
+	return KNOT_EOK;
 }
 
-/*! \brief Set EDNS section. */
-static int prepare_edns(conf_t *conf, zone_t *zone, knot_pkt_t *pkt,
-                        const conf_remote_t *remote)
+int query_put_edns(knot_pkt_t *pkt, const struct query_edns_data *edns)
 {
-	conf_val_t val = conf_zone_get(conf, C_REQUEST_EDNS_OPTION, zone->name);
-
-	/* Check if an extra EDNS option is configured. */
-	size_t opt_len;
-	const uint8_t *opt_data = conf_data(&val, &opt_len);
-	if (opt_data == NULL) {
-		return KNOT_EOK;
+	if (!pkt || !edns) {
+		return KNOT_EINVAL;
 	}
 
-	int16_t max_payload;
-	switch (remote->addr.ss_family) {
-	case AF_INET:
-		max_payload = conf->cache.srv_max_ipv4_udp_payload;
-		break;
-	case AF_INET6:
-		max_payload = conf->cache.srv_max_ipv6_udp_payload;
-		break;
-	default:
-		return KNOT_ERROR;
-	}
-
-	knot_rrset_t opt_rr;
+	// Construct EDNS RR
 
-	int ret = knot_edns_init(&opt_rr, max_payload, 0, KNOT_EDNS_VERSION, &pkt->mm);
+	knot_rrset_t opt_rr = { 0 };
+	int ret = knot_edns_init(&opt_rr, edns->max_payload, 0, KNOT_EDNS_VERSION, &pkt->mm);
 	if (ret != KNOT_EOK) {
 		return ret;
 	}
 
-	ret = knot_edns_add_option(&opt_rr, wire_read_u64(opt_data),
-	                           yp_bin_len(opt_data + sizeof(uint64_t)),
-	                           yp_bin(opt_data + sizeof(uint64_t)), &pkt->mm);
-	if (ret != KNOT_EOK) {
-		knot_rrset_clear(&opt_rr, &pkt->mm);
-		return ret;
+	if (edns->custom_code != 0) {
+		ret = knot_edns_add_option(&opt_rr, edns->custom_code,
+		                           edns->custom_len, edns->custom_data,
+		                           &pkt->mm);
+		if (ret != KNOT_EOK) {
+			knot_rrset_clear(&opt_rr, &pkt->mm);
+			return ret;
+		}
 	}
 
+	// Add result into the packet
+
 	knot_pkt_begin(pkt, KNOT_ADDITIONAL);
 
-	ret = knot_pkt_put(pkt, KNOT_COMPR_HINT_NONE, &opt_rr, KNOT_PF_FREE);
+	ret = knot_pkt_put(pkt, KNOT_COMPR_HINT_NOCOMP, &opt_rr, KNOT_PF_FREE);
 	if (ret != KNOT_EOK) {
 		knot_rrset_clear(&opt_rr, &pkt->mm);
 		return ret;
@@ -271,94 +115,3 @@ static int prepare_edns(conf_t *conf, zone_t *zone, knot_pkt_t *pkt,
 
 	return KNOT_EOK;
 }
-
-/*! \brief Process query using requestor. */
-static int zone_query_request(knot_pkt_t *query, const conf_remote_t *remote,
-                              struct process_answer_param *param, knot_mm_t *mm)
-{
-	/* Create requestor instance. */
-	const knot_layer_api_t *api = process_answer_layer();
-	struct knot_requestor re;
-	int ret = knot_requestor_init(&re, api, param, mm);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	/* Create a request. */
-	const struct sockaddr *dst = (const struct sockaddr *)&remote->addr;
-	const struct sockaddr *src = (const struct sockaddr *)&remote->via;
-	struct knot_request *req = knot_request_make(re.mm, dst, src, query, 0);
-	if (req == NULL) {
-		knot_requestor_clear(&re);
-		return KNOT_ENOMEM;
-	}
-
-	/* Send the queries and process responses. */
-	int timeout = 1000 * param->conf->cache.srv_tcp_reply_timeout;
-	ret = knot_requestor_exec(&re, req, timeout);
-
-	/* Cleanup. */
-	knot_request_free(req, re.mm);
-	knot_requestor_clear(&re);
-
-	return ret;
-}
-
-/*!
- * \brief Create a zone event query, send it, wait for the response and process it.
- *
- * \note Everything in this function is executed synchronously, returns when
- *       the query processing is either complete or an error occurs.
- */
-int zone_query_execute(conf_t *conf, zone_t *zone, uint16_t pkt_type,
-                       const conf_remote_t *remote)
-{
-	/* Create a memory pool for this task. */
-	knot_mm_t mm;
-	mm_ctx_mempool(&mm, MM_DEFAULT_BLKSIZE);
-
-	/* Create a query message. */
-	knot_pkt_t *query = zone_query(zone, pkt_type, &mm);
-	if (query == NULL) {
-		mp_delete(mm.ctx);
-		return KNOT_ENOMEM;
-	}
-
-	/* Set EDNS section. */
-	int ret = prepare_edns(conf, zone, query, remote);
-	if (ret != KNOT_EOK) {
-		knot_pkt_free(&query);
-		mp_delete(mm.ctx);
-		return ret;
-	}
-
-	/* Answer processing parameters. */
-	struct process_answer_param param = {
-		.zone = zone,
-		.conf = conf,
-		.query = query,
-		.remote = &remote->addr
-	};
-
-	const knot_tsig_key_t *key = remote->key.name != NULL ?
-	                             &remote->key : NULL;
-	tsig_init(&param.tsig_ctx, key);
-
-	ret = tsig_sign_packet(&param.tsig_ctx, query);
-	if (ret != KNOT_EOK) {
-		tsig_cleanup(&param.tsig_ctx);
-		knot_pkt_free(&query);
-		mp_delete(mm.ctx);
-		return ret;
-	}
-
-	/* Process the query. */
-	ret = zone_query_request(query, remote, &param, &mm);
-
-	/* Cleanup. */
-	tsig_cleanup(&param.tsig_ctx);
-	knot_pkt_free(&query);
-	mp_delete(mm.ctx);
-
-	return ret;
-}
diff --git a/src/knot/query/query.h b/src/knot/query/query.h
index 4d3a9ca62..cd6029eb3 100644
--- a/src/knot/query/query.h
+++ b/src/knot/query/query.h
@@ -16,44 +16,51 @@
 
 #pragma once
 
-#include <stdint.h>
-
-#include "knot/conf/conf.h"
-#include "knot/nameserver/tsig_ctx.h"
 #include "knot/nameserver/log.h"
-#include "knot/query/layer.h"
-#include "knot/zone/zone.h"
-
-/* Answer processing module implementation. */
-const knot_layer_api_t *process_answer_layer(void);
+#include "libknot/packet/pkt.h"
 
 /*!
- * \brief Processing module parameters.
+ * \brief EDNS data.
  */
-struct process_answer_param {
-	zone_t *zone;                          /*!< Answer bailiwick. */
-	conf_t *conf;                          /*!< Configuration. */
-	const knot_pkt_t *query;               /*!< Query preceding the answer. */
-	const struct sockaddr_storage *remote; /*!< Answer origin. */
-	tsig_ctx_t tsig_ctx;                   /*!< Signing context. */
+struct query_edns_data {
+	uint16_t max_payload;
+
+	// Custom EDNS option:
+	uint16_t custom_code;
+	const uint8_t *custom_data;
+	uint16_t custom_len;
 };
 
 /*!
- * \brief Processing module context.
+ * \brief Initialize new packet.
+ *
+ * Clear the packet and generate random transaction ID.
+ *
+ * \param pkt  Packet to initialize.
+ *
+ * \return Always KNOT_EOK if valid parameters supplied.
  */
-struct answer_data {
-	/* Extensions. */
-	void *ext;
-	void (*ext_cleanup)(struct answer_data*); /*!< Extensions cleanup callback. */
-	knot_sign_context_t sign;            /*!< Signing context. */
-
-	/* Everything below should be kept on reset. */
-	int response_type; /*!< Type of incoming response. */
-	struct process_answer_param *param; /*!< Module parameters. */
-	knot_mm_t *mm;                      /*!< Memory context. */
-};
+int query_init_pkt(knot_pkt_t *pkt);
 
-int zone_query_execute(conf_t *conf, zone_t *zone, uint16_t pkt_type, const conf_remote_t *remote);
+/*!
+ * \brief Initialize EDNS parameters from server configuration.
+ *
+ * \param[out] edns           EDNS parameters to initialize.
+ * \param[in]  conf           Server configuration.
+ * \param[in]  zone           Zone name.
+ * \param[in]  remote_family  Address family for remote host.
+ *
+ * \return KNOT_E*
+ */
+int query_edns_data_init(struct query_edns_data *edns, conf_t *conf,
+                         const knot_dname_t *zone, int remote_family);
 
-#define ZONE_QUERY_LOG(priority, zone, remote, operation, msg, ...) \
-	NS_PROC_LOG(priority, zone->name, &(remote)->addr, operation, msg, ##__VA_ARGS__)
+/*!
+ * \brief Append EDNS into the packet.
+ *
+ * \param pkt   Packet to add EDNS into.
+ * \param edns  EDNS data.
+ *
+ * \return KNOT_E*
+ */
+int query_put_edns(knot_pkt_t *pkt, const struct query_edns_data *edns);
diff --git a/src/knot/query/requestor.c b/src/knot/query/requestor.c
index d589a3799..54b4e4151 100644
--- a/src/knot/query/requestor.c
+++ b/src/knot/query/requestor.c
@@ -28,6 +28,11 @@ static bool use_tcp(struct knot_request *request)
 	return (request->flags & KNOT_RQ_UDP) == 0;
 }
 
+static bool is_answer_to_query(const knot_pkt_t *query, const knot_pkt_t *answer)
+{
+	return knot_wire_get_id(query->wire) == knot_wire_get_id(answer->wire);
+}
+
 /*! \brief Ensure a socket is connected. */
 static int request_ensure_connected(struct knot_request *request)
 {
@@ -229,6 +234,10 @@ static int request_consume(struct knot_requestor *req,
 		return ret;
 	}
 
+	if (!is_answer_to_query(last->query, last->resp)) {
+		return KNOT_EMALF;
+	}
+
 	ret = tsig_verify_packet(&last->tsig, last->resp);
 	if (ret != KNOT_EOK) {
 		return ret;
diff --git a/src/knot/zone/zone.c b/src/knot/zone/zone.c
index b64223174..351517f35 100644
--- a/src/knot/zone/zone.c
+++ b/src/knot/zone/zone.c
@@ -444,27 +444,3 @@ size_t zone_update_dequeue(zone_t *zone, list_t *updates)
 
 	return update_count;
 }
-
-bool zone_transfer_needed(const zone_t *zone, const knot_pkt_t *pkt)
-{
-	if (zone == NULL || pkt == NULL) {
-		return false;
-	}
-
-	if (zone_contents_is_empty(zone->contents)) {
-		return true;
-	}
-
-	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
-	if (answer->count < 1) {
-		return false;
-	}
-
-	const knot_rrset_t *soa = knot_pkt_rr(answer, 0);
-	if (soa->type != KNOT_RRTYPE_SOA) {
-		return false;
-	}
-
-	return serial_compare(zone_contents_serial(zone->contents),
-	                           knot_soa_serial(&soa->rrs)) < 0;
-}
diff --git a/src/knot/zone/zone.h b/src/knot/zone/zone.h
index cde7f3c34..8ebef0caa 100644
--- a/src/knot/zone/zone.h
+++ b/src/knot/zone/zone.h
@@ -48,6 +48,8 @@ typedef enum zone_flag_t {
 	ZONE_EXPIRED      = 1 << 3, /* Zone is expired. */
 } zone_flag_t;
 
+/// XXX: ^ remove expired?
+
 /*!
  * \brief Structure for holding DNS zone.
  */
@@ -159,7 +161,4 @@ int zone_update_enqueue(zone_t *zone, knot_pkt_t *pkt, struct process_query_para
 /*! \brief Dequeue UPDATE request. Returns number of queued updates. */
 size_t zone_update_dequeue(zone_t *zone, list_t *updates);
 
-/*! \brief Returns true if final SOA in transfer has newer serial than zone */
-bool zone_transfer_needed(const zone_t *zone, const knot_pkt_t *pkt);
-
 /*! @} */
diff --git a/src/libknot/packet/pkt.h b/src/libknot/packet/pkt.h
index 2c6d7b79f..6fdf8f49e 100644
--- a/src/libknot/packet/pkt.h
+++ b/src/libknot/packet/pkt.h
@@ -41,6 +41,7 @@
 /* Forward decls */
 struct knot_pkt;
 
+/// XXX: server internal, wipe from the library
 /*!
  * \brief DNS query types (internal use only).
  *
@@ -181,6 +182,7 @@ int knot_pkt_reserve(knot_pkt_t *pkt, uint16_t size);
  */
 int knot_pkt_reclaim(knot_pkt_t *pkt, uint16_t size);
 
+// XXX: probably useless
 /*! \brief Classify packet according to the question.
  *  \return see enum knot_pkt_type_t
  */
diff --git a/tests/.gitignore b/tests/.gitignore
index 933a46a2d..6668bb4a6 100644
--- a/tests/.gitignore
+++ b/tests/.gitignore
@@ -48,7 +48,6 @@
 /journal
 /modules/online_sign
 /node
-/process_answer
 /process_query
 /query_module
 /requestor
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 97840d5f6..3a24add52 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -61,7 +61,6 @@ check_PROGRAMS += \
 	fdset				\
 	journal				\
 	node				\
-	process_answer			\
 	process_query			\
 	query_module			\
 	requestor			\
@@ -112,4 +111,3 @@ conf_SOURCES = conf.c test_conf.h
 confdb_SOURCES = confdb.c test_conf.h
 confio_SOURCES = confio.c test_conf.h
 process_query_SOURCES = process_query.c fake_server.h test_conf.h
-process_answer_SOURCES = process_answer.c fake_server.h test_conf.h
diff --git a/tests/process_answer.c b/tests/process_answer.c
deleted file mode 100644
index e8416c9cb..000000000
--- a/tests/process_answer.c
+++ /dev/null
@@ -1,167 +0,0 @@
-/*  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 <assert.h>
-#include <tap/basic.h>
-#include <string.h>
-#include <stdlib.h>
-
-#include "libknot/descriptor.h"
-#include "libknot/packet/wire.h"
-#include "knot/query/query.h"
-#include "fake_server.h"
-#include "contrib/ucw/mempool.h"
-
-/* @note Test helpers. */
-#define TEST_RESET() \
-	knot_layer_reset(proc); \
-	knot_layer_produce(proc, pkt); \
-	knot_pkt_clear(pkt)
-
-#define TEST_EXEC(expect, info) {\
-	knot_pkt_parse(pkt, 0); \
-	int state = knot_layer_consume(proc, pkt); \
-	is_int((expect), state, "proc_answer: " info); \
-	}
-
-#define INVALID_COUNT  2
-#define SPECIFIC_COUNT 1
-#define INCLASS_COUNT  2
-#define TEST_COUNT INVALID_COUNT + SPECIFIC_COUNT + INCLASS_COUNT
-
-static void test_invalid(knot_pkt_t *pkt, knot_layer_t *proc)
-{
-	/* Invalid packet - query. */
-	TEST_RESET();
-	knot_pkt_put_question(pkt, ROOT_DNAME, KNOT_CLASS_IN, KNOT_RRTYPE_A);
-	TEST_EXEC(KNOT_STATE_NOOP, "ignored query");
-
-	/* Invalid packet - mangled. */
-	TEST_RESET();
-	knot_pkt_put_question(pkt, ROOT_DNAME, KNOT_CLASS_IN, KNOT_RRTYPE_A);
-	pkt->size += 1; /* Mangle size. */
-	TEST_EXEC(KNOT_STATE_FAIL, "malformed query");
-}
-
-/* Test if context accepts only answer to specific query. */
-static void test_specific(knot_pkt_t *pkt, knot_layer_t *proc, struct process_answer_param *param)
-{
-	/* Set specific SOA query. */
-	uint16_t query_id = 0xBEEF;
-	knot_pkt_t *query = knot_pkt_new(NULL, KNOT_WIRE_MIN_PKTSIZE, proc->mm);
-	assert(query);
-	knot_pkt_put_question(query, ROOT_DNAME, KNOT_CLASS_IN, KNOT_RRTYPE_SOA);
-	knot_wire_set_id(query->wire, query_id);
-	param->query = query;
-
-	/* MSGID mismatch */
-	TEST_RESET();
-	knot_pkt_init_response(pkt, param->query);
-	knot_wire_set_id(pkt->wire, 0xDEAD);
-	TEST_EXEC(KNOT_STATE_NOOP, "ignored mismatching MSGID");
-
-	/* Clear the specific query. */
-	knot_pkt_free(&query);
-	param->query = NULL;
-}
-
-static void test_inclass(knot_pkt_t *pkt, knot_layer_t *proc, struct process_answer_param *param)
-{
-	/* Set specific SOA query. */
-	knot_pkt_t *query = knot_pkt_new(NULL, KNOT_WIRE_MIN_PKTSIZE, proc->mm);
-	assert(query);
-	knot_pkt_put_question(query, ROOT_DNAME, KNOT_CLASS_IN, KNOT_RRTYPE_SOA);
-	param->query = query;
-
-	/* SOA query answer. */
-	TEST_RESET();
-	zone_node_t *apex = param->zone->contents->apex;
-	knot_rrset_t soa = node_rrset(apex, KNOT_RRTYPE_SOA);
-	knot_pkt_put_question(pkt, ROOT_DNAME, KNOT_CLASS_IN, KNOT_RRTYPE_SOA);
-	knot_wire_set_qr(pkt->wire);
-	knot_pkt_begin(pkt, KNOT_ANSWER);
-	knot_pkt_put(pkt, KNOT_COMPR_HINT_OWNER, &soa, 0);
-	TEST_EXEC(KNOT_STATE_DONE, "IN/SOA answer");
-
-	/* Unsupported anwer. */
-	TEST_RESET();
-	knot_pkt_put_question(pkt, ROOT_DNAME, KNOT_CLASS_IN, KNOT_RRTYPE_TXT);
-	knot_wire_set_qr(pkt->wire);
-	TEST_EXEC(KNOT_STATE_NOOP, "IN/unsupported answer");
-
-	/* Clear the specific query. */
-	knot_pkt_free(&query);
-	param->query = NULL;
-}
-
-int main(int argc, char *argv[])
-{
-	plan(3 + TEST_COUNT);
-
-	/* Create processing context. */
-	knot_mm_t mm;
-	mm_ctx_mempool(&mm, MM_DEFAULT_BLKSIZE);
-
-	knot_layer_t proc;
-	memset(&proc, 0, sizeof(knot_layer_t));
-	knot_layer_init(&proc, &mm, process_answer_layer());
-
-	/* Create fake server environment. */
-	server_t server;
-	int ret = create_fake_server(&server, proc.mm);
-	ok(ret == KNOT_EOK, "proc_answer: fake server initialization");
-
-	/* Prepare. */
-	struct sockaddr_storage remote;
-	memset(&remote, 0, sizeof(struct sockaddr_storage));
-	sockaddr_set(&remote, AF_INET, "127.0.0.1", 53);
-	struct process_answer_param param = {0};
-	param.remote = &remote;
-	param.zone = knot_zonedb_find(server.zone_db, ROOT_DNAME);
-	knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, proc.mm);
-
-	/* Begin processing. */
-	int state = knot_layer_begin(&proc, &param);
-	ok(state == KNOT_STATE_PRODUCE, "proc_answer: expects query to be sent");
-
-	/* Invalid generic input tests. */
-	test_invalid(pkt, &proc);
-
-	/* Specific input tests (response to given query). */
-	test_specific(pkt, &proc, &param);
-
-	/* IN_CLASS input tests. */
-	test_inclass(pkt, &proc, &param);
-
-	/* IXFR input tests. */
-	/* AXFR input tests. */
-	/* NOTIFY input tests. */
-	/* TSIG check tests. */
-
-	/* Finish. */
-	state = knot_layer_finish(&proc);
-	ok(state == KNOT_STATE_NOOP, "proc_answer: processing end" );
-
-	/* Cleanup. */
-	mp_delete(mm.ctx);
-	server_deinit(&server);
-	conf_free(conf());
-
-	return 0;
-}
-
-#undef TEST_RESET
-#undef TEST_EXEC
diff --git a/tests/requestor.c b/tests/requestor.c
index 13c324385..96a49da7f 100644
--- a/tests/requestor.c
+++ b/tests/requestor.c
@@ -88,7 +88,7 @@ static struct knot_request *make_query(struct knot_requestor *requestor,
 	knot_pkt_put_question(pkt, root, KNOT_CLASS_IN, KNOT_RRTYPE_SOA);
 
 	return knot_request_make(requestor->mm, (struct sockaddr *)dst,
-	                         (struct sockaddr *)src, pkt, 0);
+	                         (struct sockaddr *)src, pkt, NULL, 0);
 }
 
 static void test_disconnected(struct knot_requestor *requestor,
-- 
GitLab