diff --git a/Knot.files b/Knot.files
index 38f94403f7ee94879fd47914589b5fd7a7ce2ff1..38fcd5a77724ebf1517ad57844614e4280db1df6 100644
--- a/Knot.files
+++ b/Knot.files
@@ -403,16 +403,16 @@ src/libknot/tsig-op.h
 src/libknot/tsig.c
 src/libknot/tsig.h
 src/libknot/wire.h
-src/libknot/xdp/af_xdp.c
-src/libknot/xdp/af_xdp.h
 src/libknot/xdp/bpf-consts.h
 src/libknot/xdp/bpf-kernel-obj.c
 src/libknot/xdp/bpf-kernel-obj.h
 src/libknot/xdp/bpf-kernel.c
 src/libknot/xdp/bpf-user.c
 src/libknot/xdp/bpf-user.h
-src/libknot/xdp/eth-tools.c
-src/libknot/xdp/eth-tools.h
+src/libknot/xdp/eth.c
+src/libknot/xdp/eth.h
+src/libknot/xdp/xdp.c
+src/libknot/xdp/xdp.h
 src/libknot/yparser/yparser.c
 src/libknot/yparser/yparser.h
 src/libknot/yparser/ypbody.c
diff --git a/src/knot/conf/base.c b/src/knot/conf/base.c
index d724e68d21e4a3192bbf5c59719a78649f1e0c35..98fdc454de2508c47e56c55cce912c75bfef2272 100644
--- a/src/knot/conf/base.c
+++ b/src/knot/conf/base.c
@@ -116,7 +116,8 @@ static void init_cache(
 	conf_t *conf,
 	bool reinit_cache)
 {
-	/* For UDP, TCP and background workers, cache the numbers of running
+	/*
+	 * For UDP, TCP, XDP, and background workers, cache the number of running
 	 * workers. Cache the setting of TCP reuseport too. These values
 	 * can't change in runtime, while config data can.
 	 */
@@ -125,12 +126,14 @@ static void init_cache(
 	static bool   running_tcp_reuseport;
 	static size_t running_udp_threads;
 	static size_t running_tcp_threads;
+	static size_t running_xdp_threads;
 	static size_t running_bg_threads;
 
 	if (first_init || reinit_cache) {
 		running_tcp_reuseport = conf_tcp_reuseport(conf);
 		running_udp_threads = conf_udp_threads(conf);
 		running_tcp_threads = conf_tcp_threads(conf);
+		running_xdp_threads = conf_xdp_threads(conf);
 		running_bg_threads = conf_bg_threads(conf);
 
 		first_init = false;
@@ -184,6 +187,8 @@ static void init_cache(
 
 	conf->cache.srv_tcp_threads = running_tcp_threads;
 
+	conf->cache.srv_xdp_threads = running_xdp_threads;
+
 	conf->cache.srv_bg_threads = running_bg_threads;
 
 	conf->cache.srv_tcp_max_clients = conf_tcp_max_clients(conf);
diff --git a/src/knot/conf/conf.c b/src/knot/conf/conf.c
index 1f289529cf90b5cb8db5aa6eb128ea49eacd92e7..3f0ce9a0ae2e6d067013a6f5cfa94eabcf7994e0 100644
--- a/src/knot/conf/conf.c
+++ b/src/knot/conf/conf.c
@@ -28,9 +28,11 @@
 #include "libknot/yparser/yptrafo.h"
 #include "contrib/macros.h"
 #include "contrib/sockaddr.h"
+#include "contrib/strtonum.h"
 #include "contrib/string.h"
 #include "contrib/wire_ctx.h"
 #include "contrib/openbsd/strlcat.h"
+#include "contrib/openbsd/strlcpy.h"
 
 #define DBG_LOG(err) CONF_LOG(LOG_DEBUG, "%s (%s)", __func__, knot_strerror((err)));
 
@@ -1127,6 +1129,26 @@ size_t conf_tcp_threads_txn(
 	return workers;
 }
 
+size_t conf_xdp_threads_txn(
+	conf_t *conf,
+	knot_db_txn_t *txn)
+{
+	size_t workers = 0;
+
+	conf_val_t val = conf_get_txn(conf, txn, C_SRV, C_LISTEN_XDP);
+	while (val.code == KNOT_EOK) {
+		struct sockaddr_storage addr = conf_addr(&val, NULL);
+		conf_xdp_iface_t iface;
+		int ret = conf_xdp_iface(&addr, &iface);
+		if (ret == KNOT_EOK) {
+			workers += iface.queues;
+		}
+		conf_val_next(&val);
+	}
+
+	return workers;
+}
+
 size_t conf_bg_threads_txn(
 	conf_t *conf,
 	knot_db_txn_t *txn)
@@ -1277,3 +1299,53 @@ conf_remote_t conf_remote_txn(
 
 	return out;
 }
+
+int conf_xdp_iface(
+	struct sockaddr_storage *addr,
+	conf_xdp_iface_t *iface)
+{
+#ifndef ENABLE_XDP
+	return KNOT_ENOTSUP;
+#else
+	if (addr == NULL || iface == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	if (addr->ss_family == AF_UNIX) {
+		const char *addr_str = ((struct sockaddr_un *)addr)->sun_path;
+		strlcpy(iface->name, addr_str, sizeof(iface->name));
+
+		const char *port = strchr(addr_str, '@');
+		if (port != NULL) {
+			iface->name[port - addr_str] = '\0';
+			int ret = str_to_u16(port + 1, &iface->port);
+			if (ret != KNOT_EOK) {
+				return ret;
+			} else if (iface->port == 0) {
+				return KNOT_EINVAL;
+			}
+		} else {
+			iface->port = 53;
+		}
+	} else {
+		int ret = knot_eth_name_from_addr(addr, iface->name, sizeof(iface->name));
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+		ret = sockaddr_port(addr);
+		if (ret < 1) {
+			return KNOT_EINVAL;
+		}
+		iface->port = ret;
+	}
+
+	int queues = knot_eth_queues(iface->name);
+	if (queues <= 0) {
+		assert(queues != 0);
+		return queues;
+	}
+	iface->queues = queues;
+
+	return KNOT_EOK;
+#endif
+}
diff --git a/src/knot/conf/conf.h b/src/knot/conf/conf.h
index a44e879684122521e883873004a0d3f24dd7f86c..c6521e1ab10beb9df1c5f60eef5b965ddd212b38 100644
--- a/src/knot/conf/conf.h
+++ b/src/knot/conf/conf.h
@@ -695,6 +695,24 @@ static inline size_t conf_tcp_threads(
 	return conf_tcp_threads_txn(conf, &conf->read_txn);
 }
 
+/*!
+ * Gets the number of used XDP threads.
+ *
+ * \param[in] conf  Configuration.
+ * \param[in] txn   Configuration DB transaction.
+ *
+ * \return Number of threads.
+ */
+size_t conf_xdp_threads_txn(
+	conf_t *conf,
+	knot_db_txn_t *txn
+);
+static inline size_t conf_xdp_threads(
+	conf_t *conf)
+{
+	return conf_xdp_threads_txn(conf, &conf->read_txn);
+}
+
 /*!
  * Gets the configured number of worker threads.
  *
@@ -777,5 +795,27 @@ static inline conf_remote_t conf_remote(
 	size_t index)
 {
 	return conf_remote_txn(conf, &conf->read_txn, id, index);
-
 }
+
+/*! XDP interface parameters. */
+typedef struct {
+	/*! Interface name. */
+	char name[32];
+	/*! UDP port to listen on. */
+	uint16_t port;
+	/*! Number of active IO queues. */
+	uint16_t queues;
+} conf_xdp_iface_t;
+
+/*!
+ * Gets the XDP interface parameters for a given configuration value.
+ *
+ * \param[in] addr    XDP interface name stored in the configuration.
+ * \param[out] iface  Interface parameters.
+ *
+ * \return Error code, KNOT_EOK if success.
+ */
+int conf_xdp_iface(
+	struct sockaddr_storage *addr,
+	conf_xdp_iface_t *iface
+);
diff --git a/src/knot/conf/schema.c b/src/knot/conf/schema.c
index 03bc54653a49db22bd283b0cf0dc96123134874f..98bb2627345a073470f04349703137a2bb160836 100644
--- a/src/knot/conf/schema.c
+++ b/src/knot/conf/schema.c
@@ -173,7 +173,7 @@ static const yp_item_t desc_server[] = {
 	                                                KNOT_EDNS_MAX_UDP_PAYLOAD,
 	                                                1232, YP_SSIZE } },
 	{ C_LISTEN,               YP_TADDR, YP_VADDR = { 53 }, YP_FMULTI },
-	{ C_LISTEN_XDP,           YP_TADDR, YP_VADDR = { 53 }, YP_FMULTI },
+	{ C_LISTEN_XDP,           YP_TADDR, YP_VADDR = { 53 }, YP_FMULTI, { check_xdp } },
 	{ C_ECS,                  YP_TBOOL, YP_VNONE },
 	{ C_ANS_ROTATION,         YP_TBOOL, YP_VNONE },
 	{ C_COMMENT,              YP_TSTR,  YP_VNONE },
diff --git a/src/knot/conf/tools.c b/src/knot/conf/tools.c
index 833ccc99596889ae7fcfaf32d54b11e66096e5ad..08d4eb85dce27856fb0156bc6c8d1ed827bc47ec 100644
--- a/src/knot/conf/tools.c
+++ b/src/knot/conf/tools.c
@@ -227,6 +227,43 @@ int check_ref_dflt(
 	return KNOT_EOK;
 }
 
+int check_xdp(
+	knotd_conf_check_args_t *args)
+{
+#ifndef ENABLE_XDP
+		args->err_str = "XDP is not available";
+		return KNOT_ENOTSUP;
+#else
+	bool no_port;
+	struct sockaddr_storage ss = yp_addr(args->data, &no_port);
+	conf_xdp_iface_t if_new;
+	int ret = conf_xdp_iface(&ss, &if_new);
+	if (ret != KNOT_EOK) {
+		args->err_str = "invalid XDP interface specification";
+		return ret;
+	}
+
+	conf_val_t xdp = conf_get_txn(args->extra->conf, args->extra->txn, C_SRV,
+	                              C_LISTEN_XDP);
+	size_t count = conf_val_count(&xdp);
+	while (xdp.code == KNOT_EOK && count-- > 1) {
+		struct sockaddr_storage addr = conf_addr(&xdp, NULL);
+		conf_xdp_iface_t if_prev;
+		ret = conf_xdp_iface(&addr, &if_prev);
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+		if (strcmp(if_new.name, if_prev.name) == 0) {
+			args->err_str = "duplicate XDP interface specification";
+			return KNOT_EINVAL;
+		}
+		conf_val_next(&xdp);
+	}
+
+	return KNOT_EOK;
+#endif
+}
+
 int check_modref(
 	knotd_conf_check_args_t *args)
 {
@@ -303,7 +340,7 @@ int check_server(
 	conf_val_t hshake = conf_get_txn(args->extra->conf, args->extra->txn, C_SRV,
 	                                 C_TCP_HSHAKE_TIMEOUT);
 	if (hshake.code == KNOT_EOK) {
-		CONF_LOG(LOG_NOTICE, "option 'tcp-handshake-timeout' is no longer supported");
+		CONF_LOG(LOG_NOTICE, "option 'server.tcp-handshake-timeout' is no longer supported");
 	}
 
 	CHECK_LEGACY_NAME(C_SRV, C_TCP_REPLY_TIMEOUT, C_TCP_RMT_IO_TIMEOUT);
diff --git a/src/knot/conf/tools.h b/src/knot/conf/tools.h
index 5d6900fe67445c421dfcfbd5ffc38fc2ae4fd31b..1f36261072e4b36231c87353d873d432f3d73deb 100644
--- a/src/knot/conf/tools.h
+++ b/src/knot/conf/tools.h
@@ -66,6 +66,10 @@ int check_ref_dflt(
 	knotd_conf_check_args_t *args
 );
 
+int check_xdp(
+	knotd_conf_check_args_t *args
+);
+
 int check_modref(
 	knotd_conf_check_args_t *args
 );
diff --git a/src/knot/ctl/commands.c b/src/knot/ctl/commands.c
index 0ffab8c4c76f38b625f01c3d1b803a4a30a96181..d65f9546d2d9ef3602e07f42492cb252340926a3 100644
--- a/src/knot/ctl/commands.c
+++ b/src/knot/ctl/commands.c
@@ -1384,10 +1384,11 @@ static int server_status(ctl_args_t *args)
 	} else if (strcasecmp(type, "workers") == 0) {
 		int running_bkg_wrk, wrk_queue;
 		worker_pool_status(args->server->workers, &running_bkg_wrk, &wrk_queue);
-		ret = snprintf(buff, sizeof(buff), "UDP workers: %zu, TCP workers %zu, "
-		               "background workers: %zu (running: %d, pending: %d)",
+		ret = snprintf(buff, sizeof(buff), "UDP workers: %zu, TCP workers: %zu, "
+		               "XDP workers: %zu, background workers: %zu (running: %d, pending: %d)",
 		               conf()->cache.srv_udp_threads, conf()->cache.srv_tcp_threads,
-		               conf()->cache.srv_bg_threads, running_bkg_wrk, wrk_queue);
+		               conf()->cache.srv_xdp_threads, conf()->cache.srv_bg_threads,
+		               running_bkg_wrk, wrk_queue);
 	} else if (strcasecmp(type, "configure") == 0) {
 		ret = snprintf(buff, sizeof(buff), "%s", CONFIGURE_SUMMARY);
 	} else {
diff --git a/src/knot/modules/dnstap/dnstap.c b/src/knot/modules/dnstap/dnstap.c
index 637e6cd5a39403a995b0513c4d03c0645f852146..630c5dd88663b643bec5845b184f53bc457ea707 100644
--- a/src/knot/modules/dnstap/dnstap.c
+++ b/src/knot/modules/dnstap/dnstap.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2020 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
@@ -275,7 +275,7 @@ int dnstap_load(knotd_mod_t *mod)
 	knotd_conf_t udp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_UDP);
 	knotd_conf_t xdp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_XDP);
 	knotd_conf_t tcp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_TCP);
-	size_t qcount = udp.single.integer + tcp.single.integer + xdp.single.integer;
+	size_t qcount = udp.single.integer + xdp.single.integer + tcp.single.integer;
 	fstrm_iothr_options_set_num_input_queues(opt, qcount);
 
 	/* Create the I/O thread. */
diff --git a/src/knot/modules/noudp/noudp.c b/src/knot/modules/noudp/noudp.c
index 91a4924cd93de87041ad2aac4033b6b5b2ad7dab..891c4c044fa0213071428581c71ac36aca947cd6 100644
--- a/src/knot/modules/noudp/noudp.c
+++ b/src/knot/modules/noudp/noudp.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2020 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
@@ -60,9 +60,9 @@ int noudp_load(knotd_mod_t *mod)
 	ctx->rate = conf.single.integer;
 	if (ctx->rate > 0) {
 		knotd_conf_t udp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_UDP);
-		// TODO xdp ?
-		size_t udp_workers = udp.single.integer;
-		ctx->counters = calloc(udp_workers, sizeof(uint32_t));
+		knotd_conf_t xdp = knotd_conf_env(mod, KNOTD_CONF_ENV_WORKERS_XDP);
+		size_t workers = udp.single.integer + xdp.single.integer;
+		ctx->counters = calloc(workers, sizeof(uint32_t));
 		if (ctx->counters == NULL) {
 			free(ctx);
 			return KNOT_ENOMEM;
diff --git a/src/knot/nameserver/query_module.c b/src/knot/nameserver/query_module.c
index 6768e498a072bec9d753c5d165446ffc1a0c080f..3954558a862e784326930a689fd933d5852fdafc 100644
--- a/src/knot/nameserver/query_module.c
+++ b/src/knot/nameserver/query_module.c
@@ -348,6 +348,9 @@ knotd_conf_t knotd_conf_env(knotd_mod_t *mod, knotd_conf_env_t env)
 	case KNOTD_CONF_ENV_WORKERS_TCP:
 		out.single.integer = config->cache.srv_tcp_threads;
 		break;
+	case KNOTD_CONF_ENV_WORKERS_XDP:
+		out.single.integer = config->cache.srv_xdp_threads;
+		break;
 	default:
 		return out;
 	}
diff --git a/src/knot/server/server.c b/src/knot/server/server.c
index a6455873f03ba73d0f19d95b353356785c687da1..67a61596f3107de238f2555a33d4e121c3c7c02d 100644
--- a/src/knot/server/server.c
+++ b/src/knot/server/server.c
@@ -16,17 +16,10 @@
 
 #define __APPLE_USE_RFC_3542
 
-#include <stdlib.h>
 #include <assert.h>
-#include <ifaddrs.h>
-#include <netinet/tcp.h>
 #include <sys/resource.h>
 
-#include "libknot/errcode.h"
-#ifdef ENABLE_XDP
-#include "libknot/xdp/af_xdp.h"
-#include "libknot/xdp/eth-tools.h"
-#endif
+#include "libknot/libknot.h"
 #include "libknot/yparser/ypschema.h"
 #include "knot/common/log.h"
 #include "knot/common/stats.h"
@@ -54,7 +47,7 @@ enum {
 };
 
 /*! \brief Unbind interface and clear the structure. */
-static void server_deinit_iface(iface_t *iface)
+static void server_deinit_iface(iface_t *iface, bool dealloc)
 {
 	assert(iface);
 
@@ -87,6 +80,10 @@ static void server_deinit_iface(iface_t *iface)
 		}
 		free(iface->fd_tcp);
 	}
+
+	if (dealloc) {
+		free(iface);
+	}
 }
 
 /*! \brief Deinit server interface list. */
@@ -94,7 +91,7 @@ static void server_deinit_iface_list(iface_t *ifaces, size_t n)
 {
 	if (ifaces != NULL) {
 		for (size_t i = 0; i < n; i++) {
-			server_deinit_iface(ifaces + i);
+			server_deinit_iface(ifaces + i, false);
 		}
 		free(ifaces);
 	}
@@ -201,148 +198,54 @@ static int enable_fastopen(int sock, int backlog)
 	return KNOT_EOK;
 }
 
-__attribute__((unused)) // ifndef ENABLE_REUSEPORT
-static int iface_addr2name(const struct sockaddr_storage *add, char **out)
-{
-	struct ifaddrs *ifas = NULL, *orig = NULL;
-	if (getifaddrs(&orig)) {
-		return -errno;
-	}
-
-	size_t matches = 0;
-	char *match_name = NULL;
-
-	for (ifas = orig; ifas != NULL; ifas = ifas->ifa_next) {
-		const struct sockaddr_storage *ifss = (struct sockaddr_storage *)ifas->ifa_addr; // ??
-		if (!ifss) { // allowed; observed on interfaces without any address
-			continue;
-		}
-
-		if ((ifss->ss_family == add->ss_family && sockaddr_is_any(add)) ||
-		    sockaddr_net_match(ifss, add, 64)) { // sockaddr_cmp() compares also port numbers
-			matches++;
-			match_name = ifas->ifa_name;
-		}
-	}
-
-	if (matches == 1) {
-		*out = strdup(match_name);
-		freeifaddrs(orig);
-		return *out == NULL ? KNOT_ENOMEM : KNOT_EOK;
-	}
-
-	freeifaddrs(orig);
-	*out = malloc(64);
-	if (*out == NULL) {
-		return KNOT_ENOMEM;
-	}
-	sockaddr_tostr(*out, 64, add);
-	return matches == 0 ? KNOT_EADDRNOTAVAIL : KNOT_ELIMIT;
-}
-
-// returns >= 0: the number of RX queues; < 0 : error
-static int get_xdp_iface(struct sockaddr_storage *addr, char **out_devname, uint16_t *out_port)
-{
-#ifndef ENABLE_XDP
-	return 0;
-#else
-
-	char *dev = NULL;
-	int port = 0;
-	if (addr->ss_family == AF_UNIX) {
-		dev = strdup(((struct sockaddr_un *)addr)->sun_path);
-		if (dev == NULL) {
-			log_warning("%s", knot_strerror(KNOT_ENOMEM));
-			return KNOT_ENOMEM;
-		}
-		char *sport = strchr(dev, '@');
-		if (sport == NULL || (port = atoi(sport + 1)) < 1 || port > 0xffff) {
-			log_warning("Wrong format of XDP listen, expected 'dev@port': '%s'", dev);
-			free(dev);
-			return KNOT_EINVAL;
-		}
-		*sport = '\0';
-	} else {
-		int ret = iface_addr2name(addr, &dev);
-		if (ret != KNOT_EOK) {
-			if (dev != NULL) {
-				log_warning("XDP failed: address %s corresponds to %s net devices", dev, ret == KNOT_EADDRNOTAVAIL ? "none" : "more");
-			} else {
-				log_warning("failed to find dev for address (%s)", knot_strerror(ret));
-			}
-			return ret;
-		}
-		port = sockaddr_port(addr);
-		if (port < 1 || port > 0xffff) {
-			log_warning("port out of range");
-			free(dev);
-			return KNOT_ELIMIT;
-		}
-	}
-
-	int rx_queues = knot_eth_get_rx_queues(dev);
-	if (rx_queues < 0) {
-		log_warning("failed to obtain RX queue count for iface %s (%s)\n", dev, knot_strerror(rx_queues));
-		free(dev);
-		return rx_queues;
-	}
-
-	if (out_devname != NULL) {
-		*out_devname = dev;
-	} else {
-		free(dev);
-	}
-	if (out_port != NULL) {
-		*out_port = port;
-	}
-	return rx_queues;
-#endif
-}
-
 static iface_t *server_init_xdp_iface(struct sockaddr_storage *addr, unsigned *thread_id_start)
 {
 #ifndef ENABLE_XDP
 	assert(0);
 	return NULL;
 #else
-	char *dev = NULL;
-	uint16_t port = 0;
-	int rx_queues = get_xdp_iface(addr, &dev, &port);
-	if (rx_queues < 0) {
+	conf_xdp_iface_t iface;
+	int ret = conf_xdp_iface(addr, &iface);
+	if (ret != KNOT_EOK) {
+		log_error("failed to initialize XDP interface (%s)",
+		          knot_strerror(ret));
 		return NULL;
 	}
 
 	iface_t *new_if = calloc(1, sizeof(*new_if));
 	if (new_if == NULL) {
-		log_error("failed to initialize interface");
-		free(dev);
+		log_error("failed to initialize XDP interface (%s)",
+		          knot_strerror(KNOT_ENOMEM));
 		return NULL;
 	}
 	memcpy(&new_if->addr, addr, sizeof(*addr));
 
-	new_if->fd_xdp = malloc(rx_queues * sizeof(int));
-	new_if->sock_xdp = calloc(rx_queues, sizeof(*new_if->sock_xdp));
+	new_if->fd_xdp = calloc(iface.queues, sizeof(int));
+	new_if->sock_xdp = calloc(iface.queues, sizeof(*new_if->sock_xdp));
 	if (new_if->fd_xdp == NULL || new_if->sock_xdp == NULL) {
-		log_warning("failed to init XDP: not enough memory\n");
+		log_error("failed to initialize XDP interface (%s)",
+		          knot_strerror(KNOT_ENOMEM));
 		free(new_if);
-		free(dev);
 		return NULL;
 	}
 	new_if->fd_thread_ids = *thread_id_start;
-	*thread_id_start += rx_queues;
-
-	for (int i = 0; i < rx_queues; i++) {
-		int ret = knot_xdp_init(new_if->sock_xdp + i, dev, i, port, i == 0);
+	*thread_id_start += iface.queues;
 
+	for (int i = 0; i < iface.queues; i++) {
+		ret = knot_xdp_init(new_if->sock_xdp + i, iface.name, i,
+		                    iface.port, i == 0);
 		if (ret != KNOT_EOK) {
-			log_warning("failed to init XDP in dev %s queue %d (%s)", dev, i, knot_strerror(ret));
+			log_warning("failed to initialize XDP interface %s@%u, queue %d (%s)",
+			            iface.name, iface.port, i, knot_strerror(ret));
 			new_if->fd_xdp[i] = -1;
 		} else {
 			new_if->fd_xdp[i] = knot_xdp_socket_fd(new_if->sock_xdp[i]);
-
 		}
 		new_if->fd_xdp_count++;
 	}
+
+	log_debug("initialized XDP interface %s@%u, queues %d",
+	          iface.name, iface.port, iface.queues);
 	return new_if;
 #endif
 }
@@ -392,10 +295,9 @@ static iface_t *server_init_iface(struct sockaddr_storage *addr,
 
 	new_if->fd_udp = malloc(udp_socket_count * sizeof(int));
 	new_if->fd_tcp = malloc(tcp_socket_count * sizeof(int));
-
 	if (new_if->fd_udp == NULL || new_if->fd_tcp == NULL) {
 		log_error("failed to initialize interface");
-		server_deinit_iface(new_if);
+		server_deinit_iface(new_if, true);
 		return NULL;
 	}
 
@@ -419,7 +321,7 @@ static iface_t *server_init_iface(struct sockaddr_storage *addr,
 		if (sock < 0) {
 			log_error("cannot bind address %s UDP (%s)", addr_str,
 			          knot_strerror(sock));
-			server_deinit_iface(new_if);
+			server_deinit_iface(new_if, true);
 			return NULL;
 		}
 
@@ -465,7 +367,7 @@ static iface_t *server_init_iface(struct sockaddr_storage *addr,
 		if (sock < 0) {
 			log_error("cannot bind address %s TCP (%s)", addr_str,
 			          knot_strerror(sock));
-			server_deinit_iface(new_if);
+			server_deinit_iface(new_if, true);
 			return NULL;
 		}
 
@@ -482,7 +384,7 @@ static iface_t *server_init_iface(struct sockaddr_storage *addr,
 		int ret = listen(sock, TCP_BACKLOG_SIZE);
 		if (ret < 0) {
 			log_error("failed to listen on TCP interface %s", addr_str);
-			server_deinit_iface(new_if);
+			server_deinit_iface(new_if, true);
 			return NULL;
 		}
 
@@ -518,38 +420,44 @@ static int configure_sockets(conf_t *conf, server_t *s)
 	if (lisxdp_val.code == KNOT_EOK) {
 		struct rlimit no_limit = { RLIM_INFINITY, RLIM_INFINITY };
 		int ret = setrlimit(RLIMIT_MEMLOCK, &no_limit);
-		if (ret) {
+		if (ret != 0) {
+			log_error("failed to set RLIMIT_MEMLOCK resource limit (%s)",
+			          knot_strerror(ret));
 			return -errno;
 		}
 	}
 
-	size_t nifs = conf_val_count(&listen_val) + conf_val_count(&lisxdp_val), real_n = 0;
+	size_t real_nifs = 0;
+	size_t nifs = conf_val_count(&listen_val) + conf_val_count(&lisxdp_val);
 	iface_t *newlist = calloc(nifs, sizeof(*newlist));
 	if (newlist == NULL) {
 		return KNOT_ENOMEM;
 	}
 
+	/* Normal UDP and TCP sockets. */
+	unsigned size_udp = s->handlers[IO_UDP].handler.unit->size;
+	unsigned size_tcp = s->handlers[IO_TCP].handler.unit->size;
+	bool tcp_reuseport = conf->cache.srv_tcp_reuseport;
 	char *rundir = conf_abs_path(&rundir_val, NULL);
 	while (listen_val.code == KNOT_EOK) {
-		/* Log interface binding. */
 		struct sockaddr_storage addr = conf_addr(&listen_val, rundir);
 		char addr_str[SOCKADDR_STRLEN] = { 0 };
 		sockaddr_tostr(addr_str, sizeof(addr_str), &addr);
 		log_info("binding to interface %s", addr_str);
 
-		/* Create new interface. */
-		unsigned size_udp = s->handlers[IO_UDP].handler.unit->size;
-		unsigned size_tcp = s->handlers[IO_TCP].handler.unit->size;
-		bool tcp_reuseport = conf->cache.srv_tcp_reuseport;
-		iface_t *new_if = server_init_iface(&addr, size_udp, size_tcp, tcp_reuseport);
+		iface_t *new_if = server_init_iface(&addr, size_udp, size_tcp,
+		                                    tcp_reuseport);
 		if (new_if != NULL) {
-			memcpy(&newlist[real_n++], new_if, sizeof(*newlist));
+			memcpy(&newlist[real_nifs++], new_if, sizeof(*newlist));
 			free(new_if);
 		}
-
 		conf_val_next(&listen_val);
 	}
-	unsigned thread_id = s->handlers[IO_UDP].handler.unit->size + s->handlers[IO_TCP].handler.unit->size;
+	free(rundir);
+
+	/* XDP sockets. */
+	unsigned thread_id = s->handlers[IO_UDP].handler.unit->size +
+	                     s->handlers[IO_TCP].handler.unit->size;
 	while (lisxdp_val.code == KNOT_EOK) {
 		struct sockaddr_storage addr = conf_addr(&lisxdp_val, NULL);
 		char addr_str[SOCKADDR_STRLEN] = { 0 };
@@ -558,20 +466,19 @@ static int configure_sockets(conf_t *conf, server_t *s)
 
 		iface_t *new_if = server_init_xdp_iface(&addr, &thread_id);
 		if (new_if != NULL) {
-			memcpy(&newlist[real_n++], new_if, sizeof(*newlist));
+			memcpy(&newlist[real_nifs++], new_if, sizeof(*newlist));
 			free(new_if);
 		}
 		conf_val_next(&lisxdp_val);
 	}
-	assert(real_n <= nifs);
-	nifs = real_n;
-	free(rundir);
+	assert(real_nifs <= nifs);
+	nifs = real_nifs;
 
 	/* Publish new list. */
 	s->ifaces = newlist;
 	s->n_ifaces = nifs;
 
-	/* Set the ID's (thread_id) of both the TCP and UDP threads. */
+	/* Set the thread IDs. */
 	unsigned thread_count = 0;
 	for (unsigned proto = IO_UDP; proto <= IO_XDP; ++proto) {
 		dt_unit_t *tu = s->handlers[proto].handler.unit;
@@ -583,22 +490,6 @@ static int configure_sockets(conf_t *conf, server_t *s)
 	return KNOT_EOK;
 }
 
-int server_count_xdp_threads(conf_t *conf)
-{
-	int res = 0, ret;
-	conf_val_t lisxdp_val = conf_get(conf, C_SRV, C_LISTEN_XDP);
-	while (lisxdp_val.code == KNOT_EOK) {
-		struct sockaddr_storage addr = conf_addr(&lisxdp_val, NULL);
-		ret = get_xdp_iface(&addr, NULL, NULL);
-		if (ret < 0) {
-			return ret;
-		}
-		res += ret;
-		conf_val_next(&lisxdp_val);
-	}
-	return res;
-}
-
 int server_init(server_t *server, int bg_workers)
 {
 	if (server == NULL) {
@@ -819,13 +710,14 @@ static int reload_conf(conf_t *new_conf)
 	return KNOT_EOK;
 }
 
-/*! \brief Check if parameter listen has been changed since knotd started. */
-static bool listen_changed(conf_t *conf, server_t *server, const yp_name_t *item)
+/*! \brief Check if parameter listen(-xdp) has been changed since knotd started. */
+static bool listen_changed(conf_t *conf, server_t *server)
 {
 	assert(server->ifaces);
 
-	conf_val_t listen_val = conf_get(conf, C_SRV, item);
-	size_t new_count = conf_val_count(&listen_val);
+	conf_val_t listen_val = conf_get(conf, C_SRV, C_LISTEN);
+	conf_val_t lisxdp_val = conf_get(conf, C_SRV, C_LISTEN_XDP);
+	size_t new_count = conf_val_count(&listen_val) + conf_val_count(&lisxdp_val);
 	size_t old_count = server->n_ifaces;
 	if (new_count != old_count) {
 		return true;
@@ -846,15 +738,29 @@ static bool listen_changed(conf_t *conf, server_t *server, const yp_name_t *item
 				break;
 			}
 		}
-
 		if (!found) {
 			break;
 		}
 		conf_val_next(&listen_val);
 	}
-
 	free(rundir);
 
+	while (lisxdp_val.code == KNOT_EOK) {
+		struct sockaddr_storage addr = conf_addr(&lisxdp_val, NULL);
+		bool found = false;
+		for (size_t i = 0; i < server->n_ifaces; i++) {
+			if (sockaddr_cmp(&addr, &server->ifaces[i].addr, false) == 0) {
+				matches++;
+				found = true;
+				break;
+			}
+		}
+		if (!found) {
+			break;
+		}
+		conf_val_next(&lisxdp_val);
+	}
+
 	return matches != old_count;
 }
 
@@ -868,7 +774,6 @@ static void warn_server_reconfigure(conf_t *conf, server_t *server)
 	static bool warn_tcp = true;
 	static bool warn_bg = true;
 	static bool warn_listen = true;
-	static bool warn_listen_xdp = true;
 
 	if (warn_tcp_reuseport && conf->cache.srv_tcp_reuseport != conf_tcp_reuseport(conf)) {
 		log_warning(msg, &C_TCP_REUSEPORT[1]);
@@ -890,15 +795,10 @@ static void warn_server_reconfigure(conf_t *conf, server_t *server)
 		warn_bg = false;
 	}
 
-	if (warn_listen && listen_changed(conf, server, C_LISTEN)) {
-		log_warning(msg, &C_LISTEN[1]);
+	if (warn_listen && listen_changed(conf, server)) {
+		log_warning(msg, "listen(-xdp)");
 		warn_listen = false;
 	}
-
-	if (warn_listen_xdp && listen_changed(conf, server, C_LISTEN_XDP)) {
-		log_warning(msg, &C_LISTEN_XDP[1]);
-		warn_listen_xdp = false;
-	}
 }
 
 int server_reload(server_t *server)
@@ -1003,16 +903,15 @@ static int set_handler(server_t *server, int index, unsigned size, runnable_t ru
 	return KNOT_EOK;
 }
 
-/*! \brief Reconfigure UDP and TCP query processing threads. */
-static int configure_threads(conf_t *conf, server_t *server, size_t xdp_threads)
+static int configure_threads(conf_t *conf, server_t *server)
 {
 	int ret = set_handler(server, IO_UDP, conf->cache.srv_udp_threads, udp_master);
 	if (ret != KNOT_EOK) {
 		return ret;
 	}
 
-	if (xdp_threads > 0) {
-		ret = set_handler(server, IO_XDP, xdp_threads, udp_master);
+	if (conf->cache.srv_xdp_threads > 0) {
+		ret = set_handler(server, IO_XDP, conf->cache.srv_xdp_threads, udp_master);
 		if (ret != KNOT_EOK) {
 			return ret;
 		}
@@ -1078,14 +977,8 @@ void server_reconfigure(conf_t *conf, server_t *server)
 			         knot_db_lmdb_get_path(conf->db));
 		}
 
-		if ((ret = server_count_xdp_threads(conf)) < 0) {
-			log_error("failed to configure XDP thread count (%s)",
-				  knot_strerror(ret));
-			ret = 0;
-		}
-
 		/* Configure server threads. */
-		if ((ret = configure_threads(conf, server, ret)) != KNOT_EOK) {
+		if ((ret = configure_threads(conf, server)) != KNOT_EOK) {
 			log_error("failed to configure server threads (%s)",
 			          knot_strerror(ret));
 		}
diff --git a/src/knot/server/udp-handler.c b/src/knot/server/udp-handler.c
index 4ca76886158f623ecabbffce0ce27c8c8295e507..0b8a843738bd2673887f39eae31e5d6fb83870ff 100644
--- a/src/knot/server/udp-handler.c
+++ b/src/knot/server/udp-handler.c
@@ -38,9 +38,6 @@
 #include "knot/query/layer.h"
 #include "knot/server/server.h"
 #include "knot/server/udp-handler.h"
-#ifdef ENABLE_XDP
-#include "libknot/xdp/af_xdp.h"
-#endif /* ENABLE_XDP */
 
 /* Buffer identifiers. */
 enum {
diff --git a/src/libknot/Makefile.inc b/src/libknot/Makefile.inc
index de2424f7bb63a1fd388ea79cc50f26716ce34ea3..6fe35f495f1c773b872b2a8f19d4b1d25b3dfa19 100644
--- a/src/libknot/Makefile.inc
+++ b/src/libknot/Makefile.inc
@@ -81,17 +81,17 @@ libknot_la_CPPFLAGS += $(libbpf_CFLAGS)
 libknot_la_LIBADD   += $(libbpf_LIBS)
 
 nobase_include_libknot_HEADERS += \
-	libknot/xdp/af_xdp.h			\
 	libknot/xdp/bpf-consts.h		\
-	libknot/xdp/eth-tools.h
+	libknot/xdp/eth.h			\
+	libknot/xdp/xdp.h
 
 libknot_la_SOURCES  += \
-	libknot/xdp/af_xdp.c                    \
 	libknot/xdp/bpf-kernel-obj.c		\
 	libknot/xdp/bpf-kernel-obj.h		\
 	libknot/xdp/bpf-user.c			\
 	libknot/xdp/bpf-user.h			\
-	libknot/xdp/eth-tools.c
+	libknot/xdp/eth.c			\
+	libknot/xdp/xdp.c
 endif ENABLE_XDP
 
 DIST_SUBDIRS = libknot/xdp
diff --git a/src/libknot/error.c b/src/libknot/error.c
index 7a58f8bc4f3e5a4b1a6705b86d698c2956699501..af412b4d683a562d1045f9f25482f872019728a3 100644
--- a/src/libknot/error.c
+++ b/src/libknot/error.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2019 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2020 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
@@ -72,7 +72,7 @@ static const struct error errors[] = {
 	{ KNOT_ETIMEOUT,     "connection timeout" },
 	{ KNOT_ENODIFF,      "cannot create zone diff" },
 	{ KNOT_ENOTSIG,      "expected a TSIG or SIG(0)" },
-	{ KNOT_ELIMIT,       "exceeded response rate limit" },
+	{ KNOT_ELIMIT,       "exceeded limit" },
 	{ KNOT_EZONESIZE,    "zone size exceeded" },
 	{ KNOT_EOF,          "end of file" },
 	{ KNOT_ESYSTEM,      "system error" },
diff --git a/src/libknot/libknot.h b/src/libknot/libknot.h
index e15e627e9911c45f424544e0b6a1417740aaae34..6d8a3e34eb9c249506c9995edcfaba5cd593fa0b 100644
--- a/src/libknot/libknot.h
+++ b/src/libknot/libknot.h
@@ -62,8 +62,10 @@
 #include "libknot/rrtype/soa.h"
 #include "libknot/rrtype/tsig.h"
 #include "libknot/wire.h"
-#include "libknot/xdp/af_xdp.h"
+#ifdef ENABLE_XDP
+#include "libknot/xdp/xdp.h"
 #include "libknot/xdp/bpf-consts.h"
-#include "libknot/xdp/eth-tools.h"
+#include "libknot/xdp/eth.h"
+#endif
 
 /*! @} */
diff --git a/src/libknot/xdp/bpf-user.h b/src/libknot/xdp/bpf-user.h
index ecdd2ed6b8b43191a213544dc595dcef0352a9d0..ce30582aaedc3e2ae657eb86de0484c40f42ddbb 100644
--- a/src/libknot/xdp/bpf-user.h
+++ b/src/libknot/xdp/bpf-user.h
@@ -18,7 +18,7 @@
 
 #include <bpf/xsk.h>
 
-#include "libknot/xdp/af_xdp.h"
+#include "libknot/xdp/xdp.h"
 
 struct kxsk_iface {
 	/*! Interface name. */
diff --git a/src/libknot/xdp/eth-tools.c b/src/libknot/xdp/eth.c
similarity index 55%
rename from src/libknot/xdp/eth-tools.c
rename to src/libknot/xdp/eth.c
index a996d6272f3d0f426f1d310ce84aa7fbc3957635..a2249e5b7c4f3202e89049babbf9961113a4a21f 100644
--- a/src/libknot/xdp/eth-tools.c
+++ b/src/libknot/xdp/eth.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2020 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
@@ -15,6 +15,7 @@
  */
 
 #include <errno.h>
+#include <ifaddrs.h>
 #include <linux/ethtool.h>
 #include <linux/if.h>
 #include <linux/sockios.h>
@@ -22,11 +23,13 @@
 #include <unistd.h>
 
 #include "contrib/openbsd/strlcpy.h"
+#include "contrib/sockaddr.h"
 #include "libknot/attribute.h"
 #include "libknot/errcode.h"
+#include "libknot/xdp/eth.h"
 
 _public_
-int knot_eth_get_rx_queues(const char *devname)
+int knot_eth_queues(const char *devname)
 {
 	if (devname == NULL) {
 		return KNOT_EINVAL;
@@ -63,3 +66,42 @@ int knot_eth_get_rx_queues(const char *devname)
 	close(fd);
 	return ret;
 }
+
+_public_
+int knot_eth_name_from_addr(const struct sockaddr_storage *addr, char *out,
+                            size_t out_len)
+{
+	if (addr == NULL || out == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	struct ifaddrs *ifaces = NULL;
+	if (getifaddrs(&ifaces) != 0) {
+		return -errno;
+	}
+
+	size_t matches = 0;
+	char *match_name = NULL;
+
+	for (struct ifaddrs *ifa = ifaces; ifa != NULL; ifa = ifa->ifa_next) {
+		const struct sockaddr_storage *ifss = (struct sockaddr_storage *)ifa->ifa_addr;
+		if (ifss == NULL) { // Observed on interfaces without any address.
+			continue;
+		}
+
+		if ((ifss->ss_family == addr->ss_family && sockaddr_is_any(addr)) ||
+		    sockaddr_cmp(ifss, addr, true) == 0) {
+			matches++;
+			match_name = ifa->ifa_name;
+		}
+	}
+
+	if (matches == 1) {
+		size_t len = strlcpy(out, match_name, out_len);
+		freeifaddrs(ifaces);
+		return (len >= out_len) ? KNOT_ESPACE : KNOT_EOK;
+	}
+
+	freeifaddrs(ifaces);
+	return matches == 0 ? KNOT_EADDRNOTAVAIL : KNOT_ELIMIT;
+}
diff --git a/src/libknot/xdp/eth-tools.h b/src/libknot/xdp/eth.h
similarity index 61%
rename from src/libknot/xdp/eth-tools.h
rename to src/libknot/xdp/eth.h
index 538d067b9ca968869c1a05b9091738fa2fc0c571..b5fc7ff259859ea4653b7a12593c8639cffcfadd 100644
--- a/src/libknot/xdp/eth-tools.h
+++ b/src/libknot/xdp/eth.h
@@ -16,13 +16,27 @@
 
 #pragma once
 
+#include <stddef.h>
+
 /*!
- * \brief Get number of RX queues of a network iface.
+ * \brief Get number of combined queues of a network interface.
  *
- * \param devname   Name of the ethdev (e.g. eth1).
+ * \param devname  Name of the ethdev (e.g. eth1).
  *
  * \retval < 0   KNOT_E* if error.
  * \retval 1     Default no of queues if the dev does not support.
  * \return > 0   Number of queues.
  */
-int knot_eth_get_rx_queues(const char *devname);
+int knot_eth_queues(const char *devname);
+
+/*!
+ * \brief Get the corresponding network interface name for the address.
+ *
+ * \param addr     Address of the inteface.
+ * \param out      Output buffer for the interface name.
+ * \param out_len  Size of the output buffer.
+ *
+ * \return KNOT_E*
+ */
+int knot_eth_name_from_addr(const struct sockaddr_storage *addr, char *out,
+                            size_t out_len);
diff --git a/src/libknot/xdp/af_xdp.c b/src/libknot/xdp/xdp.c
similarity index 99%
rename from src/libknot/xdp/af_xdp.c
rename to src/libknot/xdp/xdp.c
index 1e67b0d542e9b25b6713d5f2be2116796733a84f..9236998e627d9c1562b6ac5843c2cf8caf025ae0 100644
--- a/src/libknot/xdp/af_xdp.c
+++ b/src/libknot/xdp/xdp.c
@@ -28,8 +28,8 @@
 #include "libknot/attribute.h"
 #include "libknot/endian.h"
 #include "libknot/errcode.h"
-#include "libknot/xdp/af_xdp.h"
 #include "libknot/xdp/bpf-user.h"
+#include "libknot/xdp/xdp.h"
 #include "contrib/macros.h"
 
 /* Don't fragment flag. */
diff --git a/src/libknot/xdp/af_xdp.h b/src/libknot/xdp/xdp.h
similarity index 98%
rename from src/libknot/xdp/af_xdp.h
rename to src/libknot/xdp/xdp.h
index a00781582cd176ae857bfd403e512848e30eafe3..dc0538666cbafdf73d58bb1a58e3a5ebcb4397a1 100644
--- a/src/libknot/xdp/af_xdp.h
+++ b/src/libknot/xdp/xdp.h
@@ -23,6 +23,10 @@
 
 #include "libknot/xdp/bpf-consts.h"
 
+#ifdef ENABLE_XDP
+#define KNOT_XDP_AVAILABLE	1
+#endif
+
 /*! \brief A packet with src & dst MAC & IP addrs + UDP payload. */
 typedef struct {
 	struct sockaddr_in6 ip_from;
diff --git a/src/utils/xdp-gun/main.c b/src/utils/xdp-gun/main.c
index 6cc8dddbd151f90ff1b8740c6fe6a5b3c0ab110b..3ddee0e4819dd9599f024becbeea8ffaee423569 100644
--- a/src/utils/xdp-gun/main.c
+++ b/src/utils/xdp-gun/main.c
@@ -34,10 +34,7 @@
 #include <sys/socket.h>
 #include <sys/resource.h>
 
-#include "libknot/endian.h"
-#include "libknot/error.h"
-#include "libknot/xdp/af_xdp.h"
-#include "libknot/xdp/eth-tools.h"
+#include "libknot/libknot.h"
 #include "contrib/openbsd/strlcpy.h"
 
 #include "load_queries.h"
@@ -459,7 +456,7 @@ int main(int argc, char *argv[])
 			goto pusage;
 		}
 
-		arg = knot_eth_get_rx_queues(ctx.dev);
+		arg = knot_eth_queues(ctx.dev);
 		if (arg >= 0) {
 			ctx.n_threads = arg;
 			if (ctx.qps < ctx.n_threads) {