diff --git a/Knot.files b/Knot.files
index 757a4387466f91832898cef363fc97bc2d8bf55c..c779ff5c34c14def231686f5e166dee1deea8156 100644
--- a/Knot.files
+++ b/Knot.files
@@ -415,6 +415,9 @@ src/libknot/xdp/bpf-user.c
 src/libknot/xdp/bpf-user.h
 src/libknot/xdp/eth.c
 src/libknot/xdp/eth.h
+src/libknot/xdp/msg.h
+src/libknot/xdp/msg_init.h
+src/libknot/xdp/protocols.h
 src/libknot/xdp/xdp.c
 src/libknot/xdp/xdp.h
 src/libknot/yparser/yparser.c
diff --git a/src/knot/server/udp-handler.c b/src/knot/server/udp-handler.c
index b1cb5a871bd076fef88c158387aa6ac56273c3fb..58b6a97a9a1c886947446bd7f3a9ec5777da654c 100644
--- a/src/knot/server/udp-handler.c
+++ b/src/knot/server/udp-handler.c
@@ -391,7 +391,7 @@ static int xdp_recvmmsg_recv(int fd, void *d, void *xdp_sock)
 	UNUSED(fd);
 	struct xdp_recvmmsg *rq = d;
 
-	int ret = knot_xdp_recv(xdp_sock, rq->msgs_rx, XDP_BATCHLEN, &rq->rcvd);
+	int ret = knot_xdp_recv(xdp_sock, rq->msgs_rx, XDP_BATCHLEN, &rq->rcvd, NULL);
 
 	return ret == KNOT_EOK ? rq->rcvd : ret;
 }
@@ -407,8 +407,7 @@ static int xdp_recvmmsg_handle(udp_context_t *ctx, void *d, void *xdp_sock)
 		if (rq->msgs_rx[i].payload.iov_len == 0) {
 			continue; // Skip marked (zero length) messages.
 		}
-		int ret = knot_xdp_send_alloc(xdp_sock, rq->msgs_rx[i].ip_to.sin6_family == AF_INET6,
-		                              &rq->msgs_tx[i], &rq->msgs_rx[i]);
+		int ret = knot_xdp_reply_alloc(xdp_sock, &rq->msgs_rx[i], &rq->msgs_tx[i]);
 		if (ret != KNOT_EOK) {
 			break; // Still free all RX buffers.
 		}
diff --git a/src/libknot/Makefile.inc b/src/libknot/Makefile.inc
index 6fe35f495f1c773b872b2a8f19d4b1d25b3dfa19..64b54a748b035df7e7b1910f59b638aad8bf2326 100644
--- a/src/libknot/Makefile.inc
+++ b/src/libknot/Makefile.inc
@@ -83,6 +83,7 @@ libknot_la_LIBADD   += $(libbpf_LIBS)
 nobase_include_libknot_HEADERS += \
 	libknot/xdp/bpf-consts.h		\
 	libknot/xdp/eth.h			\
+	libknot/xdp/msg.h			\
 	libknot/xdp/xdp.h
 
 libknot_la_SOURCES  += \
@@ -91,6 +92,8 @@ libknot_la_SOURCES  += \
 	libknot/xdp/bpf-user.c			\
 	libknot/xdp/bpf-user.h			\
 	libknot/xdp/eth.c			\
+	libknot/xdp/msg_init.h			\
+	libknot/xdp/protocols.h			\
 	libknot/xdp/xdp.c
 endif ENABLE_XDP
 
diff --git a/src/libknot/xdp/msg.h b/src/libknot/xdp/msg.h
new file mode 100644
index 0000000000000000000000000000000000000000..179abf86299f82407e29afae65a08882d9c20d70
--- /dev/null
+++ b/src/libknot/xdp/msg.h
@@ -0,0 +1,45 @@
+/*  Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <linux/if_ether.h>
+#include <linux/ipv6.h>
+#include <sys/uio.h>
+
+/*! \brief Message flags. */
+typedef enum {
+	KNOT_XDP_MSG_IPV6  = (1 << 0), /*!< This packet is a IPv6 (IPv4 otherwise). */
+	KNOT_XDP_MSG_TCP   = (1 << 1), /*!< This packet is a TCP (UDP otherwise). */
+	KNOT_XDP_MSG_SYN   = (1 << 2), /*!< SYN flag set (TCP only). */
+	KNOT_XDP_MSG_ACK   = (1 << 3), /*!< ACK flag set (TCP only). */
+	KNOT_XDP_MSG_FIN   = (1 << 4), /*!< FIN flag set (TCP only). */
+	KNOT_XDP_MSG_RST   = (1 << 5), /*!< RST flag set (TCP only). */
+	KNOT_XDP_MSG_MSS   = (1 << 6), /*!< MSS option in TCP header (TCP only). */
+} knot_xdp_msg_flag_t;
+
+/*! \brief Packet description with src & dst MAC & IP addrs + DNS payload. */
+typedef struct knot_xdp_msg {
+	struct sockaddr_in6 ip_from;
+	struct sockaddr_in6 ip_to;
+	uint8_t eth_from[ETH_ALEN];
+	uint8_t eth_to[ETH_ALEN];
+	knot_xdp_msg_flag_t flags;
+	uint32_t seqno;
+	uint32_t ackno;
+	struct iovec payload;
+} knot_xdp_msg_t;
diff --git a/src/libknot/xdp/msg_init.h b/src/libknot/xdp/msg_init.h
new file mode 100644
index 0000000000000000000000000000000000000000..5aff3cb348477a14aaf83535882c76da9605c6a1
--- /dev/null
+++ b/src/libknot/xdp/msg_init.h
@@ -0,0 +1,79 @@
+/*  Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdbool.h>
+#include <string.h>
+
+#include "libknot/xdp/msg.h"
+
+inline static bool empty_msg(const knot_xdp_msg_t *msg)
+{
+	const unsigned tcp_flags = KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_ACK |
+	                           KNOT_XDP_MSG_FIN | KNOT_XDP_MSG_RST;
+
+	return (msg->payload.iov_len == 0 && !(msg->flags & tcp_flags));
+}
+
+// FIXME do we care for better random?
+inline static uint32_t rnd_uint32(void)
+{
+	uint32_t res = rand() & 0xffff;
+	res <<= 16;
+	res |= rand() & 0xffff;
+	return res;
+}
+
+inline static void msg_init_base(knot_xdp_msg_t *msg, knot_xdp_msg_flag_t flags)
+{
+	memset(msg, 0, sizeof(*msg));
+
+	msg->flags = flags;
+}
+
+inline static void msg_init(knot_xdp_msg_t *msg, knot_xdp_msg_flag_t flags)
+{
+	msg_init_base(msg, flags);
+
+	if (flags & KNOT_XDP_MSG_TCP) {
+		msg->ackno = 0;
+		msg->seqno = rnd_uint32();
+	}
+}
+
+inline static void msg_init_reply(knot_xdp_msg_t *msg, const knot_xdp_msg_t *query)
+{
+	msg_init_base(msg, query->flags & (KNOT_XDP_MSG_IPV6 | KNOT_XDP_MSG_TCP));
+
+	memcpy(msg->eth_from, query->eth_to,   ETH_ALEN);
+	memcpy(msg->eth_to,   query->eth_from, ETH_ALEN);
+
+	memcpy(&msg->ip_from, &query->ip_to,   sizeof(msg->ip_from));
+	memcpy(&msg->ip_to,   &query->ip_from, sizeof(msg->ip_to));
+
+	if (msg->flags & KNOT_XDP_MSG_TCP) {
+		msg->ackno = query->seqno;
+		msg->ackno += query->payload.iov_len;
+		if (query->flags & (KNOT_XDP_MSG_SYN | KNOT_XDP_MSG_FIN)) {
+			msg->ackno++;
+		}
+		msg->seqno = query->ackno;
+		if (msg->seqno == 0) {
+			msg->seqno = rnd_uint32();
+		}
+	}
+}
diff --git a/src/libknot/xdp/protocols.h b/src/libknot/xdp/protocols.h
new file mode 100644
index 0000000000000000000000000000000000000000..2914601ad14cba583016c06b4491b50e36337021
--- /dev/null
+++ b/src/libknot/xdp/protocols.h
@@ -0,0 +1,394 @@
+/*  Copyright (C) 2021 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 <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <assert.h>
+#include <linux/ip.h>
+#include <linux/ipv6.h>
+#include <linux/tcp.h>
+#include <linux/udp.h>
+#include <netinet/in.h>
+#include <string.h>
+
+#include "libknot/xdp/msg.h"
+
+/* Don't fragment flag. */
+#define	IP_DF 0x4000
+
+/*
+ * Following prot_read_*() functions do not check sanity of parsed packet.
+ * Broken packets have to be dropped by BPF filter prior getting here.
+ */
+
+inline static void *prot_read_udp(void *data, uint16_t *src_port, uint16_t *dst_port)
+{
+	const struct udphdr *udp = data;
+
+	*src_port = udp->source;
+	*dst_port = udp->dest;
+
+	return data + sizeof(*udp);
+}
+
+enum {
+	PROT_TCP_OPT_ENDOP = 0,
+	PROT_TCP_OPT_NOOP  = 1,
+	PROT_TCP_OPT_MSS   = 2,
+
+	PROT_TCP_OPT_LEN_MSS = 4,
+};
+
+inline static void *prot_read_tcp(void *data, knot_xdp_msg_t *msg, uint16_t *src_port, uint16_t *dst_port)
+{
+	const struct tcphdr *tcp = data;
+
+	msg->flags |= KNOT_XDP_MSG_TCP;
+
+	if (tcp->syn) {
+		msg->flags |= KNOT_XDP_MSG_SYN;
+	}
+	if (tcp->ack) {
+		msg->flags |= KNOT_XDP_MSG_ACK;
+	}
+	if (tcp->fin) {
+		msg->flags |= KNOT_XDP_MSG_FIN;
+	}
+	if (tcp->rst) {
+		msg->flags |= KNOT_XDP_MSG_RST;
+	}
+
+	msg->seqno = be32toh(tcp->seq);
+	msg->ackno = be32toh(tcp->ack_seq);
+
+	*src_port = tcp->source;
+	*dst_port = tcp->dest;
+
+	uint8_t *opts = data + sizeof(*tcp), *hdr_end = data + tcp->doff * 4;
+	while (opts < hdr_end) {
+		if (opts[0] == PROT_TCP_OPT_ENDOP || opts[0] == PROT_TCP_OPT_NOOP) {
+			opts++;
+			continue;
+		}
+
+		if (opts + 1 > hdr_end || opts + opts[1] > hdr_end) {
+			// Malformed option.
+			break;
+		}
+
+		if (opts[0] == PROT_TCP_OPT_MSS && opts[1] == PROT_TCP_OPT_LEN_MSS) {
+			msg->flags |= KNOT_XDP_MSG_MSS;
+			// TODO read MSS value
+		}
+
+		opts += opts[1];
+	}
+
+	return hdr_end;
+}
+
+inline static void *prot_read_ipv4(void *data, knot_xdp_msg_t *msg, void **data_end)
+{
+	const struct iphdr *ip4 = data;
+
+	// Conditions ensured by the BPF filter.
+	assert(ip4->version == 4);
+	assert(ip4->frag_off == 0 || ip4->frag_off == __constant_htons(IP_DF));
+	// IPv4 header checksum is not verified!
+
+	struct sockaddr_in *src = (struct sockaddr_in *)&msg->ip_from;
+	struct sockaddr_in *dst = (struct sockaddr_in *)&msg->ip_to;
+	memcpy(&src->sin_addr, &ip4->saddr, sizeof(src->sin_addr));
+	memcpy(&dst->sin_addr, &ip4->daddr, sizeof(dst->sin_addr));
+	src->sin_family = AF_INET;
+	dst->sin_family = AF_INET;
+
+	*data_end = data + be16toh(ip4->tot_len);
+	data += ip4->ihl * 4;
+
+	if (ip4->protocol == IPPROTO_TCP) {
+		return prot_read_tcp(data, msg, &src->sin_port, &dst->sin_port);
+	} else {
+		assert(ip4->protocol == IPPROTO_UDP);
+		return prot_read_udp(data, &src->sin_port, &dst->sin_port);
+	}
+}
+
+inline static void *prot_read_ipv6(void *data, knot_xdp_msg_t *msg, void **data_end)
+{
+	const struct ipv6hdr *ip6 = data;
+
+	msg->flags |= KNOT_XDP_MSG_IPV6;
+
+	// Conditions ensured by the BPF filter.
+	assert(ip6->version == 6);
+
+	struct sockaddr_in6 *src = (struct sockaddr_in6 *)&msg->ip_from;
+	struct sockaddr_in6 *dst = (struct sockaddr_in6 *)&msg->ip_to;
+	memcpy(&src->sin6_addr, &ip6->saddr, sizeof(src->sin6_addr));
+	memcpy(&dst->sin6_addr, &ip6->daddr, sizeof(dst->sin6_addr));
+	src->sin6_family = AF_INET6;
+	dst->sin6_family = AF_INET6;
+	// Flow label is ignored.
+
+	data += sizeof(*ip6);
+	*data_end = data + be16toh(ip6->payload_len);
+
+	if (ip6->nexthdr == IPPROTO_TCP) {
+		return prot_read_tcp(data, msg, &src->sin6_port, &dst->sin6_port);
+	} else {
+		assert(ip6->nexthdr == IPPROTO_UDP);
+		return prot_read_udp(data, &src->sin6_port, &dst->sin6_port);
+	}
+}
+
+inline static void *prot_read_eth(void *data, knot_xdp_msg_t *msg, void **data_end)
+{
+	const struct ethhdr *eth = data;
+
+	memcpy(msg->eth_from, eth->h_source, ETH_ALEN);
+	memcpy(msg->eth_to,   eth->h_dest,   ETH_ALEN);
+	msg->flags = 0;
+
+	data += sizeof(*eth);
+
+	if (eth->h_proto == __constant_htons(ETH_P_IPV6)) {
+		return prot_read_ipv6(data, msg, data_end);
+	} else {
+		assert(eth->h_proto == __constant_htons(ETH_P_IP));
+		return prot_read_ipv4(data, msg, data_end);
+	}
+}
+
+inline static size_t prot_write_hdrs_len(const knot_xdp_msg_t *msg)
+{
+	size_t res = sizeof(struct ethhdr) + sizeof(struct iphdr) + sizeof(struct udphdr);
+
+	if (msg->flags & KNOT_XDP_MSG_IPV6) {
+		res += sizeof(struct ipv6hdr) - sizeof(struct iphdr);
+	}
+
+	if (msg->flags & KNOT_XDP_MSG_TCP) {
+		res += sizeof(struct tcphdr) - sizeof(struct udphdr);
+
+		if (msg->flags & KNOT_XDP_MSG_MSS) {
+			res += PROT_TCP_OPT_LEN_MSS;
+		}
+	}
+
+	return res;
+}
+
+/* Checksum endianness implementation notes for ipv4_checksum() and checksum().
+ *
+ * The basis for checksum is addition on big-endian 16-bit words, with bit 16 carrying
+ * over to bit 0.  That can be viewed as first byte carrying to the second and the
+ * second one carrying back to the first one, i.e. a symmetrical situation.
+ * Therefore the result is the same even when arithmetics is done on litte-endian (!)
+ */
+
+inline static void checksum(uint32_t *result, const void *_data, uint32_t _data_len)
+{
+	assert(!(_data_len & 1));
+	const uint16_t *data = _data;
+	uint32_t len = _data_len / 2;
+	while (len-- > 0) {
+		*result += *data++;
+	}
+}
+
+inline static void checksum_uint16(uint32_t *result, uint16_t x)
+{
+	checksum(result, &x, sizeof(x));
+}
+
+inline static void checksum_payload(uint32_t *result, void *payload, size_t pay_len)
+{
+	if (pay_len & 1) {
+		((uint8_t *)payload)[pay_len++] = 0;
+	}
+	checksum(result, payload, pay_len);
+}
+
+inline static uint16_t checksum_finish(uint32_t result, bool nonzero)
+{
+	while (result > 0xffff) {
+		result = (result & 0xffff) + (result >> 16);
+	}
+	if (!nonzero || result != 0xffff) {
+		result = ~result;
+	}
+	return result;
+}
+
+inline static void prot_write_udp(void *data, const knot_xdp_msg_t *msg, void *data_end,
+                                  uint16_t src_port, uint16_t dst_port, uint32_t chksum)
+{
+	struct udphdr *udp = data;
+
+	udp->len    = htobe16(data_end - data);
+	udp->source = src_port;
+	udp->dest   = dst_port;
+
+	if (msg->flags & KNOT_XDP_MSG_IPV6) {
+		udp->check = 0;
+		checksum(&chksum, &udp->len, sizeof(udp->len));
+		checksum_uint16(&chksum, htobe16(IPPROTO_UDP));
+		checksum_payload(&chksum, data, data_end - data);
+		udp->check = checksum_finish(chksum, true);
+	} else {
+		udp->check = 0; // UDP over IPv4 doesn't require checksum.
+	}
+
+	assert(data + sizeof(*udp) == msg->payload.iov_base);
+}
+
+inline static void prot_write_tcp(void *data, const knot_xdp_msg_t *msg, void *data_end,
+                                  uint16_t src_port, uint16_t dst_port, uint32_t chksum)
+{
+	struct tcphdr *tcp = data;
+
+	tcp->source  = src_port;
+	tcp->dest    = dst_port;
+	tcp->seq     = htobe32(msg->seqno);
+	tcp->ack_seq = htobe32(msg->ackno);
+	tcp->window  = htobe16(0x8000); // TODO proper window size handling in TCP streams
+	tcp->check   = 0; // Temporarily initialize before checksum calculation.
+
+	tcp->syn = ((msg->flags & KNOT_XDP_MSG_SYN) ? 1 : 0);
+	tcp->ack = ((msg->flags & KNOT_XDP_MSG_ACK) ? 1 : 0);
+	tcp->fin = ((msg->flags & KNOT_XDP_MSG_FIN) ? 1 : 0);
+	tcp->rst = ((msg->flags & KNOT_XDP_MSG_RST) ? 1 : 0);
+
+	uint8_t *hdr_end = data + sizeof(*tcp);
+	if (msg->flags & KNOT_XDP_MSG_MSS) {
+		uint16_t mss = htobe16(1460); // TODO: set proper MSS value
+		hdr_end[0] = PROT_TCP_OPT_MSS;
+		hdr_end[1] = PROT_TCP_OPT_LEN_MSS;
+		memcpy(&hdr_end[2], &mss, sizeof(mss));
+		hdr_end += PROT_TCP_OPT_LEN_MSS;
+	}
+
+	tcp->psh = ((data_end - (void *)hdr_end > 0) ? 1 : 0);
+	tcp->doff = (hdr_end - (uint8_t *)tcp) / 4;
+	assert((hdr_end - (uint8_t *)tcp) % 4 == 0);
+
+	checksum_uint16(&chksum, htobe16(IPPROTO_TCP));
+	checksum_uint16(&chksum, htobe16(data_end - data));
+	checksum_payload(&chksum, data, data_end - data);
+	tcp->check = checksum_finish(chksum, false);
+
+	assert(hdr_end == msg->payload.iov_base);
+}
+
+inline static uint16_t from32to16(uint32_t sum)
+{
+	sum = (sum & 0xffff) + (sum >> 16);
+	sum = (sum & 0xffff) + (sum >> 16);
+	return sum;
+}
+
+inline static uint16_t ipv4_checksum(const uint16_t *ipv4_hdr)
+{
+	uint32_t sum32 = 0;
+	for (int i = 0; i < 10; ++i) {
+		if (i != 5) {
+			sum32 += ipv4_hdr[i];
+		}
+	}
+	return ~from32to16(sum32);
+}
+
+inline static void prot_write_ipv4(void *data, const knot_xdp_msg_t *msg, void *data_end)
+{
+	struct iphdr *ip4 = data;
+
+	ip4->version  = 4;
+	ip4->ihl      = sizeof(*ip4) / 4;
+	ip4->tos      = 0;
+	ip4->tot_len  = htobe16(data_end - data);
+	ip4->id       = 0;
+	ip4->frag_off = 0;
+	ip4->ttl      = IPDEFTTL;
+	ip4->protocol = ((msg->flags & KNOT_XDP_MSG_TCP) ? IPPROTO_TCP : IPPROTO_UDP);
+
+	const struct sockaddr_in *src = (const struct sockaddr_in *)&msg->ip_from;
+	const struct sockaddr_in *dst = (const struct sockaddr_in *)&msg->ip_to;
+	memcpy(&ip4->saddr, &src->sin_addr, sizeof(src->sin_addr));
+	memcpy(&ip4->daddr, &dst->sin_addr, sizeof(dst->sin_addr));
+
+	ip4->check = ipv4_checksum(data);
+
+	data += sizeof(*ip4);
+
+	if (msg->flags & KNOT_XDP_MSG_TCP) {
+		uint32_t chk = 0;
+		checksum(&chk, &src->sin_addr, sizeof(src->sin_addr));
+		checksum(&chk, &dst->sin_addr, sizeof(dst->sin_addr));
+
+		prot_write_tcp(data, msg, data_end, src->sin_port, dst->sin_port, chk);
+	} else {
+		prot_write_udp(data, msg, data_end, src->sin_port, dst->sin_port, 0); // IPv4/UDP requires no checksum
+	}
+}
+
+inline static void prot_write_ipv6(void *data, const knot_xdp_msg_t *msg, void *data_end)
+{
+	struct ipv6hdr *ip6 = data;
+
+	ip6->version     = 6;
+	ip6->priority    = 0;
+	ip6->payload_len = htobe16(data_end - data - sizeof(*ip6));
+	ip6->nexthdr     = ((msg->flags & KNOT_XDP_MSG_TCP) ? IPPROTO_TCP : IPPROTO_UDP);
+	ip6->hop_limit   = IPDEFTTL;
+
+	memset(ip6->flow_lbl, 0, sizeof(ip6->flow_lbl));
+
+	const struct sockaddr_in6 *src = (const struct sockaddr_in6 *)&msg->ip_from;
+	const struct sockaddr_in6 *dst = (const struct sockaddr_in6 *)&msg->ip_to;
+	memcpy(&ip6->saddr, &src->sin6_addr, sizeof(src->sin6_addr));
+	memcpy(&ip6->daddr, &dst->sin6_addr, sizeof(dst->sin6_addr));
+
+	data += sizeof(*ip6);
+
+	uint32_t chk = 0;
+	checksum(&chk, &src->sin6_addr, sizeof(src->sin6_addr));
+	checksum(&chk, &dst->sin6_addr, sizeof(dst->sin6_addr));
+
+	if (msg->flags & KNOT_XDP_MSG_TCP) {
+		prot_write_tcp(data, msg, data_end, src->sin6_port, dst->sin6_port, chk);
+	} else {
+		prot_write_udp(data, msg, data_end, src->sin6_port, dst->sin6_port, chk);
+	}
+}
+
+inline static void prot_write_eth(void *data, const knot_xdp_msg_t *msg, void *data_end)
+{
+	struct ethhdr *eth = data;
+
+	memcpy(eth->h_source, msg->eth_from, ETH_ALEN);
+	memcpy(eth->h_dest,   msg->eth_to,   ETH_ALEN);
+
+	data += sizeof(*eth);
+
+	if (msg->flags & KNOT_XDP_MSG_IPV6) {
+		eth->h_proto = __constant_htons(ETH_P_IPV6);
+		prot_write_ipv6(data, msg, data_end);
+	} else {
+		eth->h_proto = __constant_htons(ETH_P_IP);
+		prot_write_ipv4(data, msg, data_end);
+	}
+}
diff --git a/src/libknot/xdp/xdp.c b/src/libknot/xdp/xdp.c
index 29dfe619f1b158cdd0784c332f7a9bb9793f95e4..a81e718cf1e9aa5fe61b0dfbb772c17fb4689985 100644
--- a/src/libknot/xdp/xdp.c
+++ b/src/libknot/xdp/xdp.c
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2021 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
@@ -29,12 +29,11 @@
 #include "libknot/endian.h"
 #include "libknot/errcode.h"
 #include "libknot/xdp/bpf-user.h"
+#include "libknot/xdp/msg_init.h"
+#include "libknot/xdp/protocols.h"
 #include "libknot/xdp/xdp.h"
 #include "contrib/macros.h"
 
-/* Don't fragment flag. */
-#define	IP_DF 0x4000
-
 #define FRAME_SIZE 2048
 #define UMEM_FRAME_COUNT_RX 4096
 #define UMEM_FRAME_COUNT_TX UMEM_FRAME_COUNT_RX // No reason to differ so far.
@@ -56,48 +55,10 @@ _Static_assert((FRAME_SIZE == 4096 || FRAME_SIZE == 2048)
 	, "Incorrect #define combination for AF_XDP.");
 #endif
 
-/*! \brief The memory layout of IPv4 umem frame. */
-struct udpv4 {
-	union {
-		uint8_t bytes[1];
-		struct {
-			struct ethhdr eth; // No VLAN support; CRC at the "end" of .data!
-			struct iphdr ipv4;
-			struct udphdr udp;
-			uint8_t data[];
-		} __attribute__((packed));
-	};
-};
-
-/*! \brief The memory layout of IPv6 umem frame. */
-struct udpv6 {
-	union {
-		uint8_t bytes[1];
-		struct {
-			struct ethhdr eth; // No VLAN support; CRC at the "end" of .data!
-			struct ipv6hdr ipv6;
-			struct udphdr udp;
-			uint8_t data[];
-		} __attribute__((packed));
-	};
-};
-
-/*! \brief The memory layout of each umem frame. */
 struct umem_frame {
-	union {
-		uint8_t bytes[FRAME_SIZE];
-		union {
-			struct udpv4 udpv4;
-			struct udpv6 udpv6;
-		};
-	};
+	uint8_t bytes[FRAME_SIZE];
 };
 
-_public_
-const size_t KNOT_XDP_PAYLOAD_OFFSET4 = offsetof(struct udpv4, data) + offsetof(struct umem_frame, udpv4);
-_public_
-const size_t KNOT_XDP_PAYLOAD_OFFSET6 = offsetof(struct udpv6, data) + offsetof(struct umem_frame, udpv6);
-
 static int configure_xsk_umem(struct kxsk_umem **out_umem)
 {
 	/* Allocate memory and call driver to create the UMEM. */
@@ -299,191 +260,48 @@ static struct umem_frame *alloc_tx_frame(struct kxsk_umem *umem)
 	return umem->frames + index;
 }
 
+static void prepare_payload(knot_xdp_msg_t *msg, void *uframe)
+{
+	size_t hdr_len = prot_write_hdrs_len(msg);
+	msg->payload.iov_base = uframe + hdr_len;
+	msg->payload.iov_len = FRAME_SIZE - hdr_len;
+}
+
 _public_
-int knot_xdp_send_alloc(knot_xdp_socket_t *socket, bool ipv6, knot_xdp_msg_t *out,
-                        const knot_xdp_msg_t *in_reply_to)
+int knot_xdp_send_alloc(knot_xdp_socket_t *socket, knot_xdp_msg_flag_t flags,
+                        knot_xdp_msg_t *out)
 {
 	if (socket == NULL || out == NULL) {
 		return KNOT_EINVAL;
 	}
 
-	size_t ofs = ipv6 ? KNOT_XDP_PAYLOAD_OFFSET6 : KNOT_XDP_PAYLOAD_OFFSET4;
-
 	struct umem_frame *uframe = alloc_tx_frame(socket->umem);
 	if (uframe == NULL) {
 		return KNOT_ENOMEM;
 	}
 
-	memset(out, 0, sizeof(*out));
-
-	out->payload.iov_base = ipv6 ? uframe->udpv6.data : uframe->udpv4.data;
-	out->payload.iov_len = MIN(UINT16_MAX, FRAME_SIZE - ofs);
-
-	const struct ethhdr *eth = (struct ethhdr *)uframe;
-	out->eth_from = (void *)&eth->h_source;
-	out->eth_to = (void *)&eth->h_dest;
-
-	if (in_reply_to != NULL) {
-		memcpy(out->eth_from, in_reply_to->eth_to, ETH_ALEN);
-		memcpy(out->eth_to, in_reply_to->eth_from, ETH_ALEN);
-
-		memcpy(&out->ip_from, &in_reply_to->ip_to, sizeof(out->ip_from));
-		memcpy(&out->ip_to, &in_reply_to->ip_from, sizeof(out->ip_to));
-	}
+	msg_init(out, flags);
+	prepare_payload(out, uframe);
 
 	return KNOT_EOK;
 }
 
-static uint16_t from32to16(uint32_t sum)
-{
-	sum = (sum & 0xffff) + (sum >> 16);
-	sum = (sum & 0xffff) + (sum >> 16);
-	return sum;
-}
-
-static uint16_t ipv4_checksum(const uint8_t *ipv4_hdr)
-{
-	const uint16_t *h = (const uint16_t *)ipv4_hdr;
-	uint32_t sum32 = 0;
-	for (int i = 0; i < 10; ++i) {
-		if (i != 5) {
-			sum32 += h[i];
-		}
-	}
-	return ~from32to16(sum32);
-}
-
-/* Checksum endianness implementation notes for ipv4_checksum() and udp_checksum_step().
- *
- * The basis for checksum is addition on big-endian 16-bit words, with bit 16 carrying
- * over to bit 0.  That can be viewed as first byte carrying to the second and the
- * second one carrying back to the first one, i.e. a symmetrical situation.
- * Therefore the result is the same even when arithmetics is done on litte-endian (!)
- */
-
-static void udp_checksum_step(size_t *result, const void *_data, size_t _data_len)
+_public_
+int knot_xdp_reply_alloc(knot_xdp_socket_t *socket, const knot_xdp_msg_t *query,
+                         knot_xdp_msg_t *out)
 {
-	assert(!(_data_len & 1));
-	const uint16_t *data = _data;
-	size_t len = _data_len / 2;
-	while (len-- > 0) {
-		*result += *data++;
+	if (socket == NULL || query == NULL || out == NULL) {
+		return KNOT_EINVAL;
 	}
-}
 
-static void udp_checksum_finish(size_t *result)
-{
-	while (*result > 0xffff) {
-		*result = (*result & 0xffff) + (*result >> 16);
-	}
-	if (*result != 0xffff) {
-		*result = ~*result;
+	struct umem_frame *uframe = alloc_tx_frame(socket->umem);
+	if (uframe == NULL) {
+		return KNOT_ENOMEM;
 	}
-}
-
-static uint8_t *msg_uframe_ptr(knot_xdp_socket_t *socket, const knot_xdp_msg_t *msg,
-                               /* Next parameters are just for debugging. */
-                               bool ipv6)
-{
-	uint8_t *uNULL = NULL;
-	uint8_t *uframe_p = uNULL + ((msg->payload.iov_base - NULL) & ~(FRAME_SIZE - 1));
-
-#ifndef NDEBUG
-	intptr_t pd = (uint8_t *)msg->payload.iov_base - uframe_p
-	              - (ipv6 ? KNOT_XDP_PAYLOAD_OFFSET6 : KNOT_XDP_PAYLOAD_OFFSET4);
-	/* This assertion might fire in some OK cases.  For example, the second branch
-	 * had to be added for cases with "emulated" AF_XDP support. */
-	assert(pd == XDP_PACKET_HEADROOM || pd == 0);
-
-	const uint8_t *umem_mem_start = socket->umem->frames->bytes;
-	const uint8_t *umem_mem_end = umem_mem_start + FRAME_SIZE * UMEM_FRAME_COUNT;
-	assert(umem_mem_start <= uframe_p && uframe_p < umem_mem_end);
-#endif
-	return uframe_p;
-}
-
-static void xsk_sendmsg_ipv4(knot_xdp_socket_t *socket, const knot_xdp_msg_t *msg,
-                             uint32_t index)
-{
-	uint8_t *uframe_p = msg_uframe_ptr(socket, msg, false);
-	struct umem_frame *uframe = (struct umem_frame *)uframe_p;
-	struct udpv4 *h = &uframe->udpv4;
-
-	const struct sockaddr_in *src_v4 = (const struct sockaddr_in *)&msg->ip_from;
-	const struct sockaddr_in *dst_v4 = (const struct sockaddr_in *)&msg->ip_to;
-	const uint16_t udp_len = sizeof(h->udp) + msg->payload.iov_len;
-
-	h->eth.h_proto = __constant_htons(ETH_P_IP);
-
-	h->ipv4.version  = IPVERSION;
-	h->ipv4.ihl      = 5;
-	h->ipv4.tos      = 0;
-	h->ipv4.tot_len  = htobe16(5 * 4 + udp_len);
-	h->ipv4.id       = 0;
-	h->ipv4.frag_off = 0;
-	h->ipv4.ttl      = IPDEFTTL;
-	h->ipv4.protocol = IPPROTO_UDP;
-	memcpy(&h->ipv4.saddr, &src_v4->sin_addr, sizeof(src_v4->sin_addr));
-	memcpy(&h->ipv4.daddr, &dst_v4->sin_addr, sizeof(dst_v4->sin_addr));
-	h->ipv4.check    = ipv4_checksum(h->bytes + sizeof(struct ethhdr));
-
-	h->udp.len    = htobe16(udp_len);
-	h->udp.source = src_v4->sin_port;
-	h->udp.dest   = dst_v4->sin_port;
-	h->udp.check  = 0; // Optional for IPv4 - not computed.
-
-	*xsk_ring_prod__tx_desc(&socket->tx, index) = (struct xdp_desc){
-		.addr = h->bytes - socket->umem->frames->bytes,
-		.len = KNOT_XDP_PAYLOAD_OFFSET4 + msg->payload.iov_len
-	};
-}
 
-static void xsk_sendmsg_ipv6(knot_xdp_socket_t *socket, const knot_xdp_msg_t *msg,
-                             uint32_t index)
-{
-	uint8_t *uframe_p = msg_uframe_ptr(socket, msg, true);
-	struct umem_frame *uframe = (struct umem_frame *)uframe_p;
-	struct udpv6 *h = &uframe->udpv6;
-
-	const struct sockaddr_in6 *src_v6 = (const struct sockaddr_in6 *)&msg->ip_from;
-	const struct sockaddr_in6 *dst_v6 = (const struct sockaddr_in6 *)&msg->ip_to;
-	const uint16_t udp_len = sizeof(h->udp) + msg->payload.iov_len;
-
-	h->eth.h_proto = __constant_htons(ETH_P_IPV6);
-
-	h->ipv6.version     = 6;
-	h->ipv6.priority    = 0;
-	memset(h->ipv6.flow_lbl, 0, sizeof(h->ipv6.flow_lbl));
-	h->ipv6.payload_len = htobe16(udp_len);
-	h->ipv6.nexthdr     = IPPROTO_UDP;
-	h->ipv6.hop_limit   = IPDEFTTL;
-	memcpy(&h->ipv6.saddr, &src_v6->sin6_addr, sizeof(src_v6->sin6_addr));
-	memcpy(&h->ipv6.daddr, &dst_v6->sin6_addr, sizeof(dst_v6->sin6_addr));
-
-	h->udp.len    = htobe16(udp_len);
-	h->udp.source = src_v6->sin6_port;
-	h->udp.dest   = dst_v6->sin6_port;
-	h->udp.check  = 0; // Mandatory for IPv6 - computed afterwards.
-
-	size_t chk = 0;
-	udp_checksum_step(&chk, &h->ipv6.saddr, sizeof(h->ipv6.saddr));
-	udp_checksum_step(&chk, &h->ipv6.daddr, sizeof(h->ipv6.daddr));
-	udp_checksum_step(&chk, &h->udp.len, sizeof(h->udp.len));
-	__be16 version = htobe16(h->ipv6.nexthdr);
-	udp_checksum_step(&chk, &version, sizeof(version));
-	udp_checksum_step(&chk, &h->udp, sizeof(h->udp));
-	size_t padded_len = msg->payload.iov_len;
-	if (padded_len & 1) {
-		((uint8_t *)msg->payload.iov_base)[padded_len++] = 0;
-	}
-	udp_checksum_step(&chk, msg->payload.iov_base, padded_len);
-	udp_checksum_finish(&chk);
-	h->udp.check = chk;
-
-	*xsk_ring_prod__tx_desc(&socket->tx, index) = (struct xdp_desc){
-		.addr = h->bytes - socket->umem->frames->bytes,
-		.len = KNOT_XDP_PAYLOAD_OFFSET6 + msg->payload.iov_len
-	};
+	msg_init_reply(out, query);
+	prepare_payload(out, uframe);
+	return KNOT_EOK;
 }
 
 _public_
@@ -507,15 +325,20 @@ int knot_xdp_send(knot_xdp_socket_t *socket, const knot_xdp_msg_t msgs[],
 	for (uint32_t i = 0; i < count; ++i) {
 		const knot_xdp_msg_t *msg = &msgs[i];
 
-		if (msg->payload.iov_len && msg->ip_from.sin6_family == AF_INET) {
-			xsk_sendmsg_ipv4(socket, msg, idx++);
-		} else if (msg->payload.iov_len && msg->ip_from.sin6_family == AF_INET6) {
-			xsk_sendmsg_ipv6(socket, msg, idx++);
-		} else {
-			/* Some problem; we just ignore this message. */
+		if (empty_msg(msg)) {
 			uint64_t addr_relative = (uint8_t *)msg->payload.iov_base
 			                         - socket->umem->frames->bytes;
 			tx_free_relative(socket->umem, addr_relative);
+		} else {
+			size_t hdr_len = prot_write_hdrs_len(msg);
+			size_t tot_len = hdr_len + msg->payload.iov_len;
+			uint8_t *msg_beg = msg->payload.iov_base - hdr_len;
+			prot_write_eth(msg_beg, msg, msg_beg + tot_len);
+
+			*xsk_ring_prod__tx_desc(&socket->tx, idx++) = (struct xdp_desc) {
+				.addr = msg_beg - socket->umem->frames->bytes,
+				.len = tot_len,
+			};
 		}
 	}
 
@@ -565,78 +388,9 @@ int knot_xdp_send_finish(knot_xdp_socket_t *socket)
 	 */
 }
 
-static void rx_desc(knot_xdp_socket_t *socket, const struct xdp_desc *desc,
-                    knot_xdp_msg_t *msg)
-{
-	uint8_t *uframe_p = socket->umem->frames->bytes + desc->addr;
-	const struct ethhdr *eth = (struct ethhdr *)uframe_p;
-	const struct iphdr *ip4 = NULL;
-	const struct ipv6hdr *ip6 = NULL;
-	const struct udphdr *udp = NULL;
-
-	switch (eth->h_proto) {
-	case __constant_htons(ETH_P_IP):
-		ip4 = (struct iphdr *)(uframe_p + sizeof(struct ethhdr));
-		// Next conditions are ensured by the BPF filter.
-		assert(ip4->version == 4);
-		assert(ip4->frag_off == 0 ||
-		       ip4->frag_off == __constant_htons(IP_DF));
-		assert(ip4->protocol == IPPROTO_UDP);
-		// IPv4 header checksum is not verified!
-		udp = (struct udphdr *)(uframe_p + sizeof(struct ethhdr) +
-		                        ip4->ihl * 4);
-		break;
-	case __constant_htons(ETH_P_IPV6):
-		ip6 = (struct ipv6hdr *)(uframe_p + sizeof(struct ethhdr));
-		// Next conditions are ensured by the BPF filter.
-		assert(ip6->version == 6);
-		assert(ip6->nexthdr == IPPROTO_UDP);
-		udp = (struct udphdr *)(uframe_p + sizeof(struct ethhdr) +
-		                        sizeof(struct ipv6hdr));
-		break;
-	default:
-		assert(0);
-		msg->payload.iov_len = 0;
-		return;
-	}
-	// UDP checksum is not verified!
-
-	assert(eth && (!!ip4 != !!ip6) && udp);
-
-	// Process the packet; ownership is passed on, beware of holding frames.
-
-	msg->payload.iov_base = (uint8_t *)udp + sizeof(struct udphdr);
-	msg->payload.iov_len = be16toh(udp->len) - sizeof(struct udphdr);
-
-	msg->eth_from = (void *)&eth->h_source;
-	msg->eth_to = (void *)&eth->h_dest;
-
-	if (ip4 != NULL) {
-		struct sockaddr_in *src_v4 = (struct sockaddr_in *)&msg->ip_from;
-		struct sockaddr_in *dst_v4 = (struct sockaddr_in *)&msg->ip_to;
-		memcpy(&src_v4->sin_addr, &ip4->saddr, sizeof(src_v4->sin_addr));
-		memcpy(&dst_v4->sin_addr, &ip4->daddr, sizeof(dst_v4->sin_addr));
-		src_v4->sin_port = udp->source;
-		dst_v4->sin_port = udp->dest;
-		src_v4->sin_family = AF_INET;
-		dst_v4->sin_family = AF_INET;
-	} else {
-		assert(ip6);
-		struct sockaddr_in6 *src_v6 = (struct sockaddr_in6 *)&msg->ip_from;
-		struct sockaddr_in6 *dst_v6 = (struct sockaddr_in6 *)&msg->ip_to;
-		memcpy(&src_v6->sin6_addr, &ip6->saddr, sizeof(src_v6->sin6_addr));
-		memcpy(&dst_v6->sin6_addr, &ip6->daddr, sizeof(dst_v6->sin6_addr));
-		src_v6->sin6_port = udp->source;
-		dst_v6->sin6_port = udp->dest;
-		src_v6->sin6_family = AF_INET6;
-		dst_v6->sin6_family = AF_INET6;
-		// Flow label is ignored.
-	}
-}
-
 _public_
 int knot_xdp_recv(knot_xdp_socket_t *socket, knot_xdp_msg_t msgs[],
-                  uint32_t max_count, uint32_t *count)
+                  uint32_t max_count, uint32_t *count, size_t *wire_size)
 {
 	if (socket == NULL || msgs == NULL || count == NULL) {
 		return KNOT_EINVAL;
@@ -651,7 +405,18 @@ int knot_xdp_recv(knot_xdp_socket_t *socket, knot_xdp_msg_t msgs[],
 	assert(available <= max_count);
 
 	for (uint32_t i = 0; i < available; ++i) {
-		rx_desc(socket, xsk_ring_cons__rx_desc(&socket->rx, idx++), &msgs[i]);
+		knot_xdp_msg_t *msg = &msgs[i];
+		const struct xdp_desc *desc = xsk_ring_cons__rx_desc(&socket->rx, idx++);
+		uint8_t *uframe_p = socket->umem->frames->bytes + desc->addr;
+
+		void *payl_end, *payl_start = prot_read_eth(uframe_p, msg, &payl_end);
+
+		msg->payload.iov_base = payl_start;
+		msg->payload.iov_len = payl_end - payl_start;
+
+		if (wire_size != NULL) {
+			(*wire_size) += desc->len;
+		}
 	}
 
 	xsk_ring_cons__release(&socket->rx, available);
@@ -660,6 +425,11 @@ int knot_xdp_recv(knot_xdp_socket_t *socket, knot_xdp_msg_t msgs[],
 	return KNOT_EOK;
 }
 
+static uint8_t *msg_uframe_ptr(const knot_xdp_msg_t *msg)
+{
+	return NULL + ((msg->payload.iov_base - NULL) & ~(FRAME_SIZE - 1));
+}
+
 _public_
 void knot_xdp_recv_finish(knot_xdp_socket_t *socket, const knot_xdp_msg_t msgs[],
                           uint32_t count)
@@ -676,8 +446,7 @@ void knot_xdp_recv_finish(knot_xdp_socket_t *socket, const knot_xdp_msg_t msgs[]
 	assert(reserved == count);
 
 	for (uint32_t i = 0; i < reserved; ++i) {
-		uint8_t *uframe_p = msg_uframe_ptr(socket, &msgs[i],
-		                                   msgs[i].ip_from.sin6_family == AF_INET6);
+		uint8_t *uframe_p = msg_uframe_ptr(&msgs[i]);
 		uint64_t offset = uframe_p - umem->frames->bytes;
 		*xsk_ring_prod__fill_addr(fq, idx++) = offset;
 	}
diff --git a/src/libknot/xdp/xdp.h b/src/libknot/xdp/xdp.h
index 74a845a1ae71f4f89313a054226acd30197038e7..8e8f02fac9a18808c52ede47d82057a5c2e68583 100644
--- a/src/libknot/xdp/xdp.h
+++ b/src/libknot/xdp/xdp.h
@@ -1,4 +1,4 @@
-/*  Copyright (C) 2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+/*  Copyright (C) 2021 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
@@ -22,21 +22,12 @@
 #include <netinet/in.h>
 
 #include "libknot/xdp/bpf-consts.h"
+#include "libknot/xdp/msg.h"
 
 #ifdef ENABLE_XDP
 #define KNOT_XDP_AVAILABLE	1
 #endif
 
-/*! \brief A packet with src & dst MAC & IP addrs + UDP payload. */
-typedef struct knot_xdp_msg knot_xdp_msg_t;
-struct knot_xdp_msg {
-	struct sockaddr_in6 ip_from;
-	struct sockaddr_in6 ip_to;
-	uint8_t *eth_from;
-	uint8_t *eth_to;
-	struct iovec payload;
-};
-
 /*!
  * \brief Styles of loading BPF program.
  *
@@ -55,10 +46,6 @@ typedef enum {
 /*! \brief Context structure for one XDP socket. */
 typedef struct knot_xdp_socket knot_xdp_socket_t;
 
-/*! \brief Offset of DNS payload inside ethernet frame (IPv4 and v6 variants). */
-extern const size_t KNOT_XDP_PAYLOAD_OFFSET4;
-extern const size_t KNOT_XDP_PAYLOAD_OFFSET6;
-
 /*!
  * \brief Initialize XDP socket.
  *
@@ -100,20 +87,31 @@ void knot_xdp_send_prepare(knot_xdp_socket_t *socket);
  * \brief Allocate one buffer for an outgoing packet.
  *
  * \param socket       XDP socket.
- * \param ipv6         The packet will use IPv6 (IPv4 otherwise).
+ * \param flags        Flags for new message.
+ * \param out          Out: the allocated packet buffer.
+ *
+ * \return KNOT_E*
+ */
+int knot_xdp_send_alloc(knot_xdp_socket_t *socket, knot_xdp_msg_flag_t flags,
+                        knot_xdp_msg_t *out);
+
+/*!
+ * \brief Allocate one buffer for a reply packet.
+ *
+ * \param socket       XDP socket.
+ * \param query        The packet to be replied to.
  * \param out          Out: the allocated packet buffer.
- * \param in_reply_to  Optional: fill in addresses from this query.
  *
  * \return KNOT_E*
  */
-int knot_xdp_send_alloc(knot_xdp_socket_t *socket, bool ipv6, knot_xdp_msg_t *out,
-                        const knot_xdp_msg_t *in_reply_to);
+int knot_xdp_reply_alloc(knot_xdp_socket_t *socket, const knot_xdp_msg_t *query,
+                         knot_xdp_msg_t *out);
 
 /*!
  * \brief Send multiple packets thru XDP.
  *
  * \note The packets all must have been allocated by knot_xdp_send_alloc()!
- * \note Do not free the packets payloads afterwards.
+ * \note Do not free the packet payloads afterwards.
  * \note Packets with zero length will be skipped.
  *
  * \param socket  XDP socket.
@@ -142,11 +140,12 @@ int knot_xdp_send_finish(knot_xdp_socket_t *socket);
  * \param msgs       Out: buffers to be filled in with incomming packets.
  * \param max_count  Limit for number of packets received at once.
  * \param count      Out: real number of received packets.
+ * \param wire_size  Out: (optional) total wire size of received packets.
  *
  * \return KNOT_E*
  */
 int knot_xdp_recv(knot_xdp_socket_t *socket, knot_xdp_msg_t msgs[],
-                  uint32_t max_count, uint32_t *count);
+                  uint32_t max_count, uint32_t *count, size_t *wire_size);
 
 /*!
  * \brief Free buffers with received packets.
diff --git a/src/utils/kxdpgun/main.c b/src/utils/kxdpgun/main.c
index f3c06dcaa7109e4c8148fb5a8f4318cf85f8dea3..05417c02d8059bd169dc7e6ada94728ee4665717 100644
--- a/src/utils/kxdpgun/main.c
+++ b/src/utils/kxdpgun/main.c
@@ -51,6 +51,7 @@ pthread_mutex_t global_mutex;
 uint64_t global_pkts_sent = 0;
 uint64_t global_pkts_recv = 0;
 uint64_t global_size_recv = 0;
+uint64_t global_wire_recv = 0;
 unsigned global_cpu_aff_start = 0;
 unsigned global_cpu_aff_step = 1;
 
@@ -142,7 +143,7 @@ static int alloc_pkts(knot_xdp_msg_t *pkts, int npkts, struct knot_xdp_socket *x
 	uint64_t unique = (tick * ctx->n_threads + ctx->thread_id) * ctx->at_once;
 
 	for (int i = 0; i < npkts; i++) {
-		int ret = knot_xdp_send_alloc(xsk, ctx->ipv6, &pkts[i], NULL);
+		int ret = knot_xdp_send_alloc(xsk, ctx->ipv6, &pkts[i]);
 		if (ret != KNOT_EOK) {
 			return ret;
 		}
@@ -178,7 +179,7 @@ void *xdp_gun_thread(void *_ctx)
 	struct knot_xdp_socket *xsk;
 	struct timespec timer;
 	knot_xdp_msg_t pkts[ctx->at_once];
-	uint64_t tot_sent = 0, tot_recv = 0, tot_size = 0, errors = 0;
+	uint64_t tot_sent = 0, tot_recv = 0, tot_size = 0, tot_wire = 0, errors = 0;
 	uint64_t duration = 0;
 
 	knot_xdp_load_bpf_t mode = (ctx->thread_id == 0 ?
@@ -248,7 +249,8 @@ void *xdp_gun_thread(void *_ctx)
 				}
 
 				uint32_t recvd = 0;
-				ret = knot_xdp_recv(xsk, pkts, ctx->at_once, &recvd);
+				size_t wire = 0;
+				ret = knot_xdp_recv(xsk, pkts, ctx->at_once, &recvd, &wire);
 				if (ret != KNOT_EOK) {
 					errors++;
 					break;
@@ -262,6 +264,7 @@ void *xdp_gun_thread(void *_ctx)
 					tot_size += pkts[i].payload.iov_len;
 					tot_recv++;
 				}
+				tot_wire += wire;
 				knot_xdp_recv_finish(xsk, pkts, recvd);
 				pfd.revents = 0;
 			}
@@ -287,6 +290,7 @@ void *xdp_gun_thread(void *_ctx)
 	global_pkts_sent += tot_sent;
 	global_pkts_recv += tot_recv;
 	global_size_recv += tot_size;
+	global_wire_recv += tot_wire;
 	pthread_mutex_unlock(&global_mutex);
 
 	return NULL;
@@ -724,8 +728,7 @@ int main(int argc, char *argv[])
 		printf("total replies: %lu (%lu pps) (%lu%%)\n", global_pkts_recv,
 		       global_pkts_recv * 1000 / (ctx.duration / 1000), global_pkts_recv * 100 / global_pkts_sent);
 		printf("average DNS reply size: %lu B\n", global_pkts_recv > 0 ? global_size_recv / global_pkts_recv : 0);
-		size_t bytes_recv = global_size_recv + (ctx.ipv6 ? KNOT_XDP_PAYLOAD_OFFSET6 : KNOT_XDP_PAYLOAD_OFFSET4) * global_pkts_recv;
-		printf("average Ethernet reply rate: %lu bps\n", bytes_recv * 8 * 1000 / (ctx.duration / 1000));
+		printf("average Ethernet reply rate: %lu bps\n", global_wire_recv * 8 * 1000 / (ctx.duration / 1000));
 		for (int i = 0; i < KNOWN_RCODE_MAX; i++) {
 			uint64_t rcode_count = 0;
 			for (size_t j = 0; j < ctx.n_threads; j++) {