diff -pruN 5.4.4-1/AUTHORS 5.5.1-5/AUTHORS
--- 5.4.4-1/AUTHORS	2022-01-05 13:19:14.403727000 +0000
+++ 5.5.1-5/AUTHORS	2022-06-14 07:17:30.359490200 +0000
@@ -14,6 +14,7 @@ Ali Asad Lotia <ali.asad.lotia@gmail.com
 Anbang Wen <anbang@cloudflare.com>
 Andreas Rammhold <andreas@rammhold.de>
 Christophe Nowicki <cscm@csquad.org>
+cronfy <cronfy@gmail.com>
 Daniel Kahn Gillmor <dkg@fifthhorseman.net>
 Daniel Salzman <daniel.salzman@nic.cz>
 daurnimator <quae@daurnimator.com>
@@ -22,6 +23,7 @@ Grigorii Demidov <grigorii.demidov@nic.c
 Hasnat <hasnat.ullah@gmail.com>
 Héctor Molinero Fernández <hector@molinero.dev>
 Ivana Krumlová <ivana.krumlova@nic.cz>
+Jakub Jirutka <jakub@jirutka.cz>
 Jakub Ružička <jakub.ruzicka@nic.cz>
 Jan Hák <jan.hak@nic.cz>
 Jan Holuša <jan.holusa@nic.cz>
@@ -32,6 +34,7 @@ Jiří Helebrant <jiri.helebrant@nic.cz>
 Jonathan Coetzee <jon@thancoetzee.com>
 Josh Soref <jsoref@users.noreply.github.com>
 Karel Slaný <karel.slany@nic.cz>
+Konstantin Amelichev <kostya.amelichev@gmail.com>
 Leo Vandewoestijne <github@unicycle.net>
 Libor Peltan <libor.peltan@nic.cz>
 Lukáš Ježek <lukas.jezek@nic.cz>
@@ -66,7 +69,6 @@ Vladimír Čunát <vladimir.cunat@nic.cz
 Knot Resolver source tree also bundles code and content published by:
 Austin Appleby <aappleby@gmail.com>
 Dan Vanderkam <danvdk@gmail.com>
-Jonas Gehring <jonas@jgehring.net>
 Jonathan Allard <jonathan@allard.io>
 Joseph A. Adams <joeyadams3.14159@gmail.com>
 Mark DiMarco <mark.dimarco@gmail.com>
diff -pruN 5.4.4-1/ci/debian-11/Dockerfile 5.5.1-5/ci/debian-11/Dockerfile
--- 5.4.4-1/ci/debian-11/Dockerfile	2022-01-05 13:19:14.415727100 +0000
+++ 5.5.1-5/ci/debian-11/Dockerfile	1970-01-01 00:00:00.000000000 +0000
@@ -1,139 +0,0 @@
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-FROM debian:bullseye
-MAINTAINER Knot Resolver <knot-resolver@labs.nic.cz>
-# >= 3.0 needed because of --enable-xdp=yes
-ARG KNOT_BRANCH=3.1
-ENV DEBIAN_FRONTEND=noninteractive
-
-WORKDIR /root
-CMD ["/bin/bash"]
-
-# generic cleanup
-RUN apt-get update -qq
-
-# Knot and Knot Resolver dependencies
-RUN apt-get install -y -qqq git make cmake pkg-config meson \
-	build-essential bsdmainutils libtool autoconf libcmocka-dev \
-	liburcu-dev libgnutls28-dev libedit-dev liblmdb-dev libcap-ng-dev libsystemd-dev \
-	libelf-dev libmnl-dev libidn11-dev libuv1-dev \
-	libluajit-5.1-dev lua-http libssl-dev libnghttp2-dev
-
-# Build and testing deps for Resolver's dnstap module (go stuff is just for testing)
-RUN apt-get install -y -qqq \
-	protobuf-c-compiler libprotobuf-c-dev libfstrm-dev \
-	golang-any
-RUN bash -c "go get github.com/{FiloSottile/gvt,cloudflare/dns,dnstap/golang-dnstap,golang/protobuf/proto}"
-
-# documentation dependencies
-RUN apt-get install -y -qqq doxygen python3-sphinx python3-breathe python3-sphinx-rtd-theme
-
-# Python packages required for Deckard CI
-# Python: grab latest versions from PyPi
-# (Augeas binding in Debian packages are slow and buggy)
-RUN apt-get install -y -qqq python3-pip wget augeas-tools
-RUN pip3 install --upgrade pip
-RUN pip3 install pylint
-RUN pip3 install pep8
-RUN pip3 install pytest-xdist
-# tests/pytest dependencies: skip over broken versions
-RUN pip3 install 'dnspython != 2.0.0' jinja2 'pytest != 6.0.0' pytest-html pytest-xdist
-# apkg for packaging
-RUN pip3 install apkg
-
-# packet capture tools for Deckard
-RUN apt-get install --no-install-suggests --no-install-recommends -y -qqq tcpdump wireshark-common
-
-# Faketime for Deckard
-RUN apt-get install -y -qqq faketime
-
-# C dependencies for python-augeas
-RUN apt-get install -y -qqq libaugeas-dev libffi-dev
-# Python dependencies for Deckard
-RUN wget https://gitlab.nic.cz/knot/deckard/raw/master/requirements.txt -O /tmp/deckard-req.txt
-RUN pip3 install -r /tmp/deckard-req.txt
-
-# build and install latest version of Knot DNS
-RUN git clone --depth=1 --branch=$KNOT_BRANCH https://gitlab.nic.cz/knot/knot-dns.git /tmp/knot
-WORKDIR /tmp/knot
-RUN pwd
-RUN autoreconf -if
-RUN ./configure --prefix=/usr --enable-xdp=yes
-RUN CFLAGS="-g" make
-RUN make install
-RUN ldconfig
-
-# Valgrind for kresd CI
-RUN apt-get install valgrind -y -qqq
-RUN wget https://github.com/LuaJIT/LuaJIT/raw/v2.1.0-beta3/src/lj.supp -O /lj.supp
-# TODO: rebuild LuaJIT with Valgrind support
-
-# Lua lint for kresd CI
-RUN apt-get install luarocks -y -qqq
-RUN luarocks --lua-version 5.1 install luacheck
-
-# respdiff for kresd CI
-RUN apt-get install lmdb-utils -y -qqq
-RUN git clone --depth=1 https://gitlab.nic.cz/knot/respdiff /var/opt/respdiff
-RUN pip3 install -r /var/opt/respdiff/requirements.txt
-
-# Python static analysis for respdiff
-RUN pip3 install mypy
-RUN pip3 install flake8
-
-# Python requests for CI scripts
-RUN pip3 install requests
-
-# docker-py for packaging tests
-RUN pip3 install docker
-
-# Unbound for respdiff
-RUN apt-get install unbound unbound-anchor -y -qqq
-RUN printf "server:\n interface: 127.0.0.1@53535\n use-syslog: yes\n do-ip6: no\nremote-control:\n control-enable: no\n" >> /etc/unbound/unbound.conf
-
-# BIND for respdiff
-RUN apt-get install bind9 -y -qqq
-RUN printf '\nOPTIONS="-4 $OPTIONS"' >> /etc/default/bind9
-RUN printf 'options {\n directory "/var/cache/bind";\n listen-on port 53533 { 127.0.0.1; };\n listen-on-v6 port 53533 { ::1; };\n};\n' > /etc/bind/named.conf.options
-
-# PowerDNS Recursor for Deckard CI
-RUN apt-get install pdns-recursor -y -qqq
-
-# code coverage
-RUN apt-get install -y -qqq lcov
-RUN luarocks --lua-version 5.1 install luacov
-
-# LuaJIT binary for stand-alone scripting
-RUN apt-get install -y -qqq luajit
-
-# clang for kresd CI, version updated as debian updates it
-RUN apt-get install -y -qqq clang clang-tools clang-tidy
-
-# OpenBuildService CLI tool
-RUN apt-get install -y osc
-
-# curl (API)
-RUN apt-get install -y curl
-
-# configure knot-resolver-testing OBS repo for dependencies missing in Debian
-RUN echo 'deb http://download.opensuse.org/repositories/home:/CZ-NIC:/knot-resolver-testing/Debian_11/ /' > /etc/apt/sources.list.d/knot-resolver-testing.list
-RUN wget -nv https://download.opensuse.org/repositories/home:CZ-NIC:knot-resolver-testing/Debian_11/Release.key -O Release.key
-RUN APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 apt-key add Release.key
-RUN rm Release.key
-RUN apt-get update -qq
-
-# packages from our knot-resolver-testing repo
-RUN apt-get update
-RUN apt-get install -y -qqq lua-psl
-
-# en_US.UTF-8 locale for scripts.update-authors.sh
-RUN apt-get install -y -qqq locales
-RUN sed -i "/en_US.UTF-8/ s/^#\(.*\)/\1/" /etc/locale.gen
-RUN locale-gen
-
-# SonarCloud scanner
-RUN wget -O /var/opt/wrapper.zip https://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip
-RUN wget -O /var/opt/scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.4.0.2170-linux.zip
-RUN unzip -d /var/opt /var/opt/wrapper.zip
-RUN unzip -d /var/opt /var/opt/scanner.zip
-ENV PATH "$PATH:/var/opt/build-wrapper-linux-x86:/var/opt/sonar-scanner-4.4.0.2170-linux/bin"
diff -pruN 5.4.4-1/ci/debian-buster/Dockerfile 5.5.1-5/ci/debian-buster/Dockerfile
--- 5.4.4-1/ci/debian-buster/Dockerfile	2022-01-05 13:19:14.415727100 +0000
+++ 5.5.1-5/ci/debian-buster/Dockerfile	1970-01-01 00:00:00.000000000 +0000
@@ -1,145 +0,0 @@
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-FROM debian:buster
-MAINTAINER Knot Resolver <knot-resolver@labs.nic.cz>
-# >= 3.0 needed because of --enable-xdp=yes
-ARG KNOT_BRANCH=3.0
-ENV DEBIAN_FRONTEND=noninteractive
-
-WORKDIR /root
-CMD ["/bin/bash"]
-
-# generic cleanup
-RUN apt-get update -qq
-# TODO: run upgrade once buster reaches a stable release
-# RUN apt-get upgrade -y -qqq
-
-# Knot and Knot Resolver dependencies
-RUN apt-get install -y -qqq git make cmake pkg-config meson \
-	build-essential bsdmainutils libtool autoconf libcmocka-dev \
-	liburcu-dev libgnutls28-dev libedit-dev liblmdb-dev libcap-ng-dev libsystemd-dev \
-	libelf-dev libmnl-dev libidn11-dev libuv1-dev \
-	libluajit-5.1-dev lua-http libssl-dev libnghttp2-dev
-
-# Build and testing deps for Resolver's dnstap module (go stuff is just for testing)
-RUN apt-get install -y -qqq \
-	protobuf-c-compiler libprotobuf-c-dev libfstrm-dev \
-	golang-any
-# Some stuff won't work on buster:
-# package crypto/ed25519: unrecognized import path "crypto/ed25519"
-#RUN bash -c "go get github.com/{FiloSottile/gvt,cloudflare/dns,dnstap/golang-dnstap}"
-
-# documentation dependencies
-RUN apt-get install -y -qqq doxygen python3-sphinx python3-breathe python3-sphinx-rtd-theme
-
-# Python packages required for Deckard CI
-# Python: grab latest versions from PyPi
-# (Augeas binding in Debian packages are slow and buggy)
-RUN apt-get install -y -qqq python3-pip wget augeas-tools
-RUN pip3 install --upgrade pip
-RUN pip3 install pylint
-RUN pip3 install pep8
-RUN pip3 install pytest-xdist
-# tests/pytest dependencies: skip over broken versions
-RUN pip3 install 'dnspython != 2.0.0' jinja2 'pytest != 6.0.0' pytest-html pytest-xdist
-
-# packet capture tools for Deckard
-RUN apt-get install --no-install-suggests --no-install-recommends -y -qqq tcpdump wireshark-common
-
-# Faketime for Deckard
-RUN apt-get install -y -qqq faketime
-
-# C dependencies for python-augeas
-RUN apt-get install -y -qqq libaugeas-dev libffi-dev
-# Python dependencies for Deckard
-RUN wget https://gitlab.nic.cz/knot/deckard/raw/master/requirements.txt -O /tmp/deckard-req.txt
-RUN pip3 install -r /tmp/deckard-req.txt
-
-# build and install latest version of Knot DNS
-RUN git clone --depth=1 --branch=$KNOT_BRANCH https://gitlab.nic.cz/knot/knot-dns.git /tmp/knot
-WORKDIR /tmp/knot
-RUN pwd
-RUN autoreconf -if
-RUN ./configure --prefix=/usr --enable-xdp=yes
-RUN CFLAGS="-g" make
-RUN make install
-RUN ldconfig
-
-# Valgrind for kresd CI
-RUN apt-get install valgrind -y -qqq
-RUN wget https://github.com/LuaJIT/LuaJIT/raw/v2.1.0-beta3/src/lj.supp -O /lj.supp
-# TODO: rebuild LuaJIT with Valgrind support
-
-# Lua lint for kresd CI
-RUN apt-get install luarocks -y -qqq
-RUN luarocks --lua-version 5.1 install luacheck
-
-# respdiff for kresd CI
-RUN apt-get install lmdb-utils -y -qqq
-RUN git clone --depth=1 https://gitlab.nic.cz/knot/respdiff /var/opt/respdiff
-RUN pip3 install -r /var/opt/respdiff/requirements.txt
-
-# Python static analysis for respdiff
-RUN pip3 install mypy
-RUN pip3 install flake8
-
-# Python requests for CI scripts
-RUN pip3 install requests
-
-# docker-py for packaging tests
-RUN pip3 install docker
-
-# Unbound for respdiff
-RUN apt-get install unbound unbound-anchor -y -qqq
-RUN printf "server:\n interface: 127.0.0.1@53535\n use-syslog: yes\n do-ip6: no\nremote-control:\n control-enable: no\n" >> /etc/unbound/unbound.conf
-
-# BIND for respdiff
-RUN apt-get install bind9 -y -qqq
-RUN printf '\nOPTIONS="-4 $OPTIONS"' >> /etc/default/bind9
-RUN printf 'options {\n directory "/var/cache/bind";\n listen-on port 53533 { 127.0.0.1; };\n listen-on-v6 port 53533 { ::1; };\n};\n' > /etc/bind/named.conf.options
-
-# PowerDNS Recursor for Deckard CI
-RUN apt-get install pdns-recursor -y -qqq
-
-# code coverage
-RUN apt-get install -y -qqq lcov
-RUN luarocks --lua-version 5.1 install luacov
-
-# LuaJIT binary for stand-alone scripting
-RUN apt-get install -y -qqq luajit
-
-# clang for kresd CI, version updated as debian updates it
-RUN apt-get install -y -qqq clang clang-tools clang-tidy
-
-# OpenBuildService CLI tool
-RUN apt-get install -y osc
-
-# curl (API)
-RUN apt-get install -y curl
-
-# configure knot-resolver-testing OBS repo for dependencies missing in Debian
-RUN echo 'deb http://download.opensuse.org/repositories/home:/CZ-NIC:/knot-resolver-testing/Debian_10/ /' > /etc/apt/sources.list.d/knot-resolver-testing.list
-RUN wget -nv https://download.opensuse.org/repositories/home:CZ-NIC:knot-resolver-testing/Debian_10/Release.key -O Release.key
-RUN APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 apt-key add Release.key
-RUN rm Release.key
-RUN apt-get update -qq
-
-# packages from our knot-resolver-testing repo
-RUN apt-get install -y -qqq lua-http lua-psl
-
-# en_US.UTF-8 locale for scripts.update-authors.sh
-RUN apt-get install -y -qqq locales
-RUN sed -i "/en_US.UTF-8/ s/^#\(.*\)/\1/" /etc/locale.gen
-RUN locale-gen
-
-# SonarCloud scanner
-RUN wget -O /var/opt/wrapper.zip https://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip
-RUN wget -O /var/opt/scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.4.0.2170-linux.zip
-RUN unzip -d /var/opt /var/opt/wrapper.zip
-RUN unzip -d /var/opt /var/opt/scanner.zip
-ENV PATH "$PATH:/var/opt/build-wrapper-linux-x86:/var/opt/sonar-scanner-4.4.0.2170-linux/bin"
-
-# let's get newer meson from backports
-RUN echo 'deb http://deb.debian.org/debian buster-backports main' > /etc/apt/sources.list.d/backports.list
-RUN apt-get update -qq
-RUN apt-get -t buster-backports install -y -qqq meson
diff -pruN 5.4.4-1/ci/images/build.sh 5.5.1-5/ci/images/build.sh
--- 5.4.4-1/ci/images/build.sh	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/ci/images/build.sh	2022-06-14 07:17:30.371490200 +0000
@@ -0,0 +1,13 @@
+#!/bin/bash
+# build specified docker image
+
+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+source "${CURRENT_DIR}"/vars.sh "$@"
+set -ex
+
+if [ -n "$COVERITY_SCAN_TOKEN" ]; then
+	SECRETS="$SECRETS --secret id=coverity-token,env=COVERITY_SCAN_TOKEN"
+fi
+
+export DOCKER_BUILDKIT=1 # Enables using secrets in docker-build
+docker build --pull --no-cache -t "${FULL_NAME}" "${IMAGE}" --build-arg KNOT_BRANCH=${KNOT_BRANCH} $SECRETS
diff -pruN 5.4.4-1/ci/images/debian-11/Dockerfile 5.5.1-5/ci/images/debian-11/Dockerfile
--- 5.4.4-1/ci/images/debian-11/Dockerfile	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/ci/images/debian-11/Dockerfile	2022-06-14 07:17:30.371490200 +0000
@@ -0,0 +1,144 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+FROM debian:bullseye
+MAINTAINER Knot Resolver <knot-resolver@labs.nic.cz>
+# >= 3.0 needed because of --enable-xdp=yes
+ARG KNOT_BRANCH=3.1
+ENV DEBIAN_FRONTEND=noninteractive
+
+WORKDIR /root
+CMD ["/bin/bash"]
+
+# generic cleanup
+RUN apt-get update -qq
+
+# Knot and Knot Resolver dependencies
+RUN apt-get install -y -qqq git make cmake pkg-config meson \
+	build-essential bsdmainutils libtool autoconf libcmocka-dev \
+	liburcu-dev libgnutls28-dev libedit-dev liblmdb-dev libcap-ng-dev libsystemd-dev \
+	libelf-dev libmnl-dev libidn11-dev libuv1-dev \
+	libluajit-5.1-dev lua-http libssl-dev libnghttp2-dev
+
+# Build and testing deps for Resolver's dnstap module (go stuff is just for testing)
+RUN apt-get install -y -qqq \
+	protobuf-c-compiler libprotobuf-c-dev libfstrm-dev \
+	golang-any
+RUN bash -c "go get github.com/{FiloSottile/gvt,cloudflare/dns,dnstap/golang-dnstap,golang/protobuf/proto}"
+
+# documentation dependencies
+RUN apt-get install -y -qqq doxygen python3-sphinx python3-breathe python3-sphinx-rtd-theme
+
+# Python packages required for Deckard CI
+# Python: grab latest versions from PyPi
+# (Augeas binding in Debian packages are slow and buggy)
+RUN apt-get install -y -qqq python3-pip wget augeas-tools
+RUN pip3 install --upgrade pip
+RUN pip3 install pylint
+RUN pip3 install pep8
+RUN pip3 install pytest-xdist
+# FIXME replace with dnspython >= 2.2.0 once released
+RUN pip3 install git+https://github.com/bwelling/dnspython.git@72348d4698a8f8b209fbdf9e72738904ad31b930
+# tests/pytest dependencies: skip over broken versions
+RUN pip3 install jinja2 'pytest != 6.0.0' pytest-html pytest-xdist
+# apkg for packaging
+RUN pip3 install apkg
+
+# packet capture tools for Deckard
+RUN apt-get install --no-install-suggests --no-install-recommends -y -qqq tcpdump wireshark-common
+
+# Faketime for Deckard
+RUN apt-get install -y -qqq faketime
+
+# C dependencies for python-augeas
+RUN apt-get install -y -qqq libaugeas-dev libffi-dev
+# Python dependencies for Deckard
+RUN wget https://gitlab.nic.cz/knot/deckard/raw/master/requirements.txt -O /tmp/deckard-req.txt
+RUN pip3 install -r /tmp/deckard-req.txt
+
+# build and install latest version of Knot DNS
+RUN git clone --depth=1 --branch=$KNOT_BRANCH https://gitlab.nic.cz/knot/knot-dns.git /tmp/knot
+WORKDIR /tmp/knot
+RUN pwd
+RUN autoreconf -if
+RUN ./configure --prefix=/usr --enable-xdp=yes
+RUN CFLAGS="-g" make
+RUN make install
+RUN ldconfig
+
+# Valgrind for kresd CI
+RUN apt-get install valgrind -y -qqq
+RUN wget https://github.com/LuaJIT/LuaJIT/raw/v2.1.0-beta3/src/lj.supp -O /lj.supp
+# TODO: rebuild LuaJIT with Valgrind support
+
+# Lua lint for kresd CI
+RUN apt-get install luarocks -y -qqq
+RUN luarocks --lua-version 5.1 install luacheck
+
+# respdiff for kresd CI
+RUN apt-get install lmdb-utils -y -qqq
+RUN git clone --depth=1 https://gitlab.nic.cz/knot/respdiff /var/opt/respdiff
+RUN pip3 install -r /var/opt/respdiff/requirements.txt
+
+# Python static analysis for respdiff
+RUN pip3 install mypy
+RUN pip3 install flake8
+
+# Python requests for CI scripts
+RUN pip3 install requests
+
+# docker-py for packaging tests
+RUN pip3 install docker
+
+# Unbound for respdiff
+RUN apt-get install unbound unbound-anchor -y -qqq
+RUN printf "server:\n interface: 127.0.0.1@53535\n use-syslog: yes\n do-ip6: no\nremote-control:\n control-enable: no\n" >> /etc/unbound/unbound.conf
+
+# BIND for respdiff
+RUN apt-get install bind9 -y -qqq
+RUN printf '\nOPTIONS="-4 $OPTIONS"' >> /etc/default/bind9
+RUN printf 'options {\n directory "/var/cache/bind";\n listen-on port 53533 { 127.0.0.1; };\n listen-on-v6 port 53533 { ::1; };\n};\n' > /etc/bind/named.conf.options
+
+# PowerDNS Recursor for Deckard CI
+RUN apt-get install pdns-recursor -y -qqq
+
+# dnsdist for Deckard CI
+RUN apt-get install dnsdist -y -qqq
+
+# code coverage
+RUN apt-get install -y -qqq lcov
+RUN luarocks --lua-version 5.1 install luacov
+
+# LuaJIT binary for stand-alone scripting
+RUN apt-get install -y -qqq luajit
+
+# clang for kresd CI, version updated as debian updates it
+RUN apt-get install -y -qqq clang clang-tools clang-tidy
+
+# OpenBuildService CLI tool
+RUN apt-get install -y osc
+
+# curl (API)
+RUN apt-get install -y curl
+
+# configure knot-resolver-testing OBS repo for dependencies missing in Debian
+RUN echo 'deb http://download.opensuse.org/repositories/home:/CZ-NIC:/knot-resolver-testing/Debian_11/ /' > /etc/apt/sources.list.d/knot-resolver-testing.list
+RUN wget -nv https://download.opensuse.org/repositories/home:CZ-NIC:knot-resolver-testing/Debian_11/Release.key -O Release.key
+RUN APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 apt-key add Release.key
+RUN rm Release.key
+RUN apt-get update -qq
+
+# packages from our knot-resolver-testing repo
+RUN apt-get update
+RUN apt-get install -y -qqq lua-psl
+
+# en_US.UTF-8 locale for scripts.update-authors.sh
+RUN apt-get install -y -qqq locales
+RUN sed -i "/en_US.UTF-8/ s/^#\(.*\)/\1/" /etc/locale.gen
+RUN locale-gen
+
+# SonarCloud scanner
+RUN wget -O /var/opt/wrapper.zip https://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip
+RUN wget -O /var/opt/scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.4.0.2170-linux.zip
+RUN unzip -d /var/opt /var/opt/wrapper.zip
+RUN unzip -d /var/opt /var/opt/scanner.zip
+ENV PATH "$PATH:/var/opt/build-wrapper-linux-x86:/var/opt/sonar-scanner-4.4.0.2170-linux/bin"
diff -pruN 5.4.4-1/ci/images/debian-11-coverity/Dockerfile 5.5.1-5/ci/images/debian-11-coverity/Dockerfile
--- 5.4.4-1/ci/images/debian-11-coverity/Dockerfile	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/ci/images/debian-11-coverity/Dockerfile	2022-06-14 07:17:30.371490200 +0000
@@ -0,0 +1,43 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+FROM debian:bullseye
+MAINTAINER Knot Resolver <knot-resolver@labs.nic.cz>
+# >= 3.0 needed because of --enable-xdp=yes
+ARG KNOT_BRANCH=3.1
+ARG COVERITY_SCAN_PROJECT_NAME=CZ-NIC/knot-resolver
+ENV DEBIAN_FRONTEND=noninteractive
+
+WORKDIR /root
+CMD ["/bin/bash"]
+
+# generic cleanup
+RUN apt-get update -qq
+
+# Knot and Knot Resolver dependencies
+RUN apt-get install -y -qqq git make cmake pkg-config meson \
+	build-essential bsdmainutils libtool autoconf libcmocka-dev \
+	liburcu-dev libgnutls28-dev libedit-dev liblmdb-dev libcap-ng-dev libsystemd-dev \
+	libelf-dev libmnl-dev libidn11-dev libuv1-dev \
+	libluajit-5.1-dev lua-http libssl-dev libnghttp2-dev
+
+# LuaJIT binary for stand-alone scripting
+RUN apt-get install -y -qqq luajit
+
+# build and install latest version of Knot DNS
+RUN git clone --depth=1 --branch=$KNOT_BRANCH https://gitlab.nic.cz/knot/knot-dns.git /tmp/knot
+WORKDIR /tmp/knot
+RUN pwd
+RUN autoreconf -if
+RUN ./configure --prefix=/usr --enable-xdp=yes
+RUN CFLAGS="-g" make
+RUN make install
+RUN ldconfig
+
+# curl and tar (for downloading Coverity tools and uploading logs)
+RUN apt-get install -y curl tar
+
+RUN --mount=type=secret,id=coverity-token \
+	curl -o /tmp/cov-analysis-linux64.tar.gz https://scan.coverity.com/download/cxx/linux64 \
+	--form project=$COVERITY_SCAN_PROJECT_NAME --form token=$(cat /run/secrets/coverity-token)
+RUN tar xfz /tmp/cov-analysis-linux64.tar.gz
+RUN mv cov-analysis-linux64-* /opt/cov-analysis
diff -pruN 5.4.4-1/ci/images/debian-buster/Dockerfile 5.5.1-5/ci/images/debian-buster/Dockerfile
--- 5.4.4-1/ci/images/debian-buster/Dockerfile	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/ci/images/debian-buster/Dockerfile	2022-06-14 07:17:30.371490200 +0000
@@ -0,0 +1,145 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+FROM debian:buster
+MAINTAINER Knot Resolver <knot-resolver@labs.nic.cz>
+# >= 3.0 needed because of --enable-xdp=yes
+ARG KNOT_BRANCH=3.0
+ENV DEBIAN_FRONTEND=noninteractive
+
+WORKDIR /root
+CMD ["/bin/bash"]
+
+# generic cleanup
+RUN apt-get update -qq
+# TODO: run upgrade once buster reaches a stable release
+# RUN apt-get upgrade -y -qqq
+
+# Knot and Knot Resolver dependencies
+RUN apt-get install -y -qqq git make cmake pkg-config meson \
+	build-essential bsdmainutils libtool autoconf libcmocka-dev \
+	liburcu-dev libgnutls28-dev libedit-dev liblmdb-dev libcap-ng-dev libsystemd-dev \
+	libelf-dev libmnl-dev libidn11-dev libuv1-dev \
+	libluajit-5.1-dev lua-http libssl-dev libnghttp2-dev
+
+# Build and testing deps for Resolver's dnstap module (go stuff is just for testing)
+RUN apt-get install -y -qqq \
+	protobuf-c-compiler libprotobuf-c-dev libfstrm-dev \
+	golang-any
+# Some stuff won't work on buster:
+# package crypto/ed25519: unrecognized import path "crypto/ed25519"
+#RUN bash -c "go get github.com/{FiloSottile/gvt,cloudflare/dns,dnstap/golang-dnstap}"
+
+# documentation dependencies
+RUN apt-get install -y -qqq doxygen python3-sphinx python3-breathe python3-sphinx-rtd-theme
+
+# Python packages required for Deckard CI
+# Python: grab latest versions from PyPi
+# (Augeas binding in Debian packages are slow and buggy)
+RUN apt-get install -y -qqq python3-pip wget augeas-tools
+RUN pip3 install --upgrade pip
+RUN pip3 install pylint
+RUN pip3 install pep8
+RUN pip3 install pytest-xdist
+# tests/pytest dependencies: skip over broken versions
+RUN pip3 install 'dnspython != 2.0.0' jinja2 'pytest != 6.0.0' pytest-html pytest-xdist
+
+# packet capture tools for Deckard
+RUN apt-get install --no-install-suggests --no-install-recommends -y -qqq tcpdump wireshark-common
+
+# Faketime for Deckard
+RUN apt-get install -y -qqq faketime
+
+# C dependencies for python-augeas
+RUN apt-get install -y -qqq libaugeas-dev libffi-dev
+# Python dependencies for Deckard
+RUN wget https://gitlab.nic.cz/knot/deckard/raw/master/requirements.txt -O /tmp/deckard-req.txt
+RUN pip3 install -r /tmp/deckard-req.txt
+
+# build and install latest version of Knot DNS
+RUN git clone --depth=1 --branch=$KNOT_BRANCH https://gitlab.nic.cz/knot/knot-dns.git /tmp/knot
+WORKDIR /tmp/knot
+RUN pwd
+RUN autoreconf -if
+RUN ./configure --prefix=/usr --enable-xdp=yes
+RUN CFLAGS="-g" make
+RUN make install
+RUN ldconfig
+
+# Valgrind for kresd CI
+RUN apt-get install valgrind -y -qqq
+RUN wget https://github.com/LuaJIT/LuaJIT/raw/v2.1.0-beta3/src/lj.supp -O /lj.supp
+# TODO: rebuild LuaJIT with Valgrind support
+
+# Lua lint for kresd CI
+RUN apt-get install luarocks -y -qqq
+RUN luarocks --lua-version 5.1 install luacheck
+
+# respdiff for kresd CI
+RUN apt-get install lmdb-utils -y -qqq
+RUN git clone --depth=1 https://gitlab.nic.cz/knot/respdiff /var/opt/respdiff
+RUN pip3 install -r /var/opt/respdiff/requirements.txt
+
+# Python static analysis for respdiff
+RUN pip3 install mypy
+RUN pip3 install flake8
+
+# Python requests for CI scripts
+RUN pip3 install requests
+
+# docker-py for packaging tests
+RUN pip3 install docker
+
+# Unbound for respdiff
+RUN apt-get install unbound unbound-anchor -y -qqq
+RUN printf "server:\n interface: 127.0.0.1@53535\n use-syslog: yes\n do-ip6: no\nremote-control:\n control-enable: no\n" >> /etc/unbound/unbound.conf
+
+# BIND for respdiff
+RUN apt-get install bind9 -y -qqq
+RUN printf '\nOPTIONS="-4 $OPTIONS"' >> /etc/default/bind9
+RUN printf 'options {\n directory "/var/cache/bind";\n listen-on port 53533 { 127.0.0.1; };\n listen-on-v6 port 53533 { ::1; };\n};\n' > /etc/bind/named.conf.options
+
+# PowerDNS Recursor for Deckard CI
+RUN apt-get install pdns-recursor -y -qqq
+
+# code coverage
+RUN apt-get install -y -qqq lcov
+RUN luarocks --lua-version 5.1 install luacov
+
+# LuaJIT binary for stand-alone scripting
+RUN apt-get install -y -qqq luajit
+
+# clang for kresd CI, version updated as debian updates it
+RUN apt-get install -y -qqq clang clang-tools clang-tidy
+
+# OpenBuildService CLI tool
+RUN apt-get install -y osc
+
+# curl (API)
+RUN apt-get install -y curl
+
+# configure knot-resolver-testing OBS repo for dependencies missing in Debian
+RUN echo 'deb http://download.opensuse.org/repositories/home:/CZ-NIC:/knot-resolver-testing/Debian_10/ /' > /etc/apt/sources.list.d/knot-resolver-testing.list
+RUN wget -nv https://download.opensuse.org/repositories/home:CZ-NIC:knot-resolver-testing/Debian_10/Release.key -O Release.key
+RUN APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 apt-key add Release.key
+RUN rm Release.key
+RUN apt-get update -qq
+
+# packages from our knot-resolver-testing repo
+RUN apt-get install -y -qqq lua-http lua-psl
+
+# en_US.UTF-8 locale for scripts.update-authors.sh
+RUN apt-get install -y -qqq locales
+RUN sed -i "/en_US.UTF-8/ s/^#\(.*\)/\1/" /etc/locale.gen
+RUN locale-gen
+
+# SonarCloud scanner
+RUN wget -O /var/opt/wrapper.zip https://sonarcloud.io/static/cpp/build-wrapper-linux-x86.zip
+RUN wget -O /var/opt/scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.4.0.2170-linux.zip
+RUN unzip -d /var/opt /var/opt/wrapper.zip
+RUN unzip -d /var/opt /var/opt/scanner.zip
+ENV PATH "$PATH:/var/opt/build-wrapper-linux-x86:/var/opt/sonar-scanner-4.4.0.2170-linux/bin"
+
+# let's get newer meson from backports
+RUN echo 'deb http://deb.debian.org/debian buster-backports main' > /etc/apt/sources.list.d/backports.list
+RUN apt-get update -qq
+RUN apt-get -t buster-backports install -y -qqq meson
diff -pruN 5.4.4-1/ci/images/lxc-debian-11/Dockerfile 5.5.1-5/ci/images/lxc-debian-11/Dockerfile
--- 5.4.4-1/ci/images/lxc-debian-11/Dockerfile	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/ci/images/lxc-debian-11/Dockerfile	2022-06-14 07:17:30.371490200 +0000
@@ -0,0 +1,131 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+FROM registry.nic.cz/labs/lxc-gitlab-runner/debian-11:latest
+MAINTAINER Knot Resolver <knot-resolver@labs.nic.cz>
+# >= 3.0 needed because of --enable-xdp=yes
+ARG KNOT_BRANCH=3.1
+ENV DEBIAN_FRONTEND=noninteractive
+
+# generic cleanup
+RUN apt-get update -qq
+
+# Knot and Knot Resolver dependencies
+RUN apt-get install -y -qqq git make cmake pkg-config meson \
+	build-essential bsdmainutils libtool autoconf libcmocka-dev \
+	liburcu-dev libgnutls28-dev libedit-dev liblmdb-dev libcap-ng-dev libsystemd-dev \
+	libelf-dev libmnl-dev libidn11-dev libuv1-dev \
+	libluajit-5.1-dev lua-http libssl-dev libnghttp2-dev
+
+# Build and testing deps for Resolver's dnstap module (go stuff is just for testing)
+RUN apt-get install -y -qqq \
+	protobuf-c-compiler libprotobuf-c-dev libfstrm-dev \
+	golang-any
+RUN bash -c "go get github.com/{FiloSottile/gvt,cloudflare/dns,dnstap/golang-dnstap,golang/protobuf/proto}"
+
+# documentation dependencies
+RUN apt-get install -y -qqq doxygen python3-sphinx python3-breathe python3-sphinx-rtd-theme
+
+# Python packages required for Deckard CI
+# Python: grab latest versions from PyPi
+# (Augeas binding in Debian packages are slow and buggy)
+RUN apt-get install -y -qqq python3-pip wget augeas-tools
+RUN pip3 install --upgrade pip
+RUN pip3 install pylint
+RUN pip3 install pep8
+RUN pip3 install pytest-xdist
+# FIXME replace with dnspython >= 2.2.0 once released
+RUN pip3 install git+https://github.com/bwelling/dnspython.git@72348d4698a8f8b209fbdf9e72738904ad31b930
+# tests/pytest dependencies: skip over broken versions
+RUN pip3 install jinja2 'pytest != 6.0.0' pytest-html pytest-xdist
+# apkg for packaging
+RUN pip3 install apkg
+
+# packet capture tools for Deckard
+RUN apt-get install --no-install-suggests --no-install-recommends -y -qqq tcpdump wireshark-common
+
+# Faketime for Deckard
+RUN apt-get install -y -qqq faketime
+
+# C dependencies for python-augeas
+RUN apt-get install -y -qqq libaugeas-dev libffi-dev
+# Python dependencies for Deckard
+RUN wget https://gitlab.nic.cz/knot/deckard/raw/master/requirements.txt -O /tmp/deckard-req.txt
+RUN pip3 install -r /tmp/deckard-req.txt
+
+# build and install latest version of Knot DNS
+RUN git clone --depth=1 --branch=$KNOT_BRANCH https://gitlab.nic.cz/knot/knot-dns.git /tmp/knot
+WORKDIR /tmp/knot
+RUN pwd
+RUN autoreconf -if
+RUN ./configure --prefix=/usr --enable-xdp=yes
+RUN CFLAGS="-g" make
+RUN make install
+RUN ldconfig
+
+# Valgrind for kresd CI
+RUN apt-get install valgrind -y -qqq
+RUN wget https://github.com/LuaJIT/LuaJIT/raw/v2.1.0-beta3/src/lj.supp -O /lj.supp
+# TODO: rebuild LuaJIT with Valgrind support
+
+# Lua lint for kresd CI
+RUN apt-get install luarocks -y -qqq
+RUN luarocks --lua-version 5.1 install luacheck
+
+# respdiff for kresd CI
+RUN apt-get install lmdb-utils -y -qqq
+RUN git clone --depth=1 https://gitlab.nic.cz/knot/respdiff /var/opt/respdiff
+RUN pip3 install -r /var/opt/respdiff/requirements.txt
+
+# Python static analysis for respdiff
+RUN pip3 install mypy
+RUN pip3 install flake8
+
+# Python requests for CI scripts
+RUN pip3 install requests
+
+# docker-py for packaging tests
+RUN pip3 install docker
+
+# Unbound for respdiff
+RUN apt-get install unbound unbound-anchor -y -qqq
+RUN printf "server:\n interface: 127.0.0.1@53535\n use-syslog: yes\n do-ip6: no\nremote-control:\n control-enable: no\n" >> /etc/unbound/unbound.conf
+
+# BIND for respdiff
+RUN apt-get install bind9 -y -qqq
+RUN printf '\nOPTIONS="-4 $OPTIONS"' >> /etc/default/bind9
+RUN printf 'options {\n directory "/var/cache/bind";\n listen-on port 53533 { 127.0.0.1; };\n listen-on-v6 port 53533 { ::1; };\n};\n' > /etc/bind/named.conf.options
+
+# PowerDNS Recursor for Deckard CI
+RUN apt-get install pdns-recursor -y -qqq
+
+# code coverage
+RUN apt-get install -y -qqq lcov
+RUN luarocks --lua-version 5.1 install luacov
+
+# LuaJIT binary for stand-alone scripting
+RUN apt-get install -y -qqq luajit
+
+# clang for kresd CI, version updated as debian updates it
+RUN apt-get install -y -qqq clang clang-tools clang-tidy
+
+# OpenBuildService CLI tool
+RUN apt-get install -y osc
+
+# curl (API)
+RUN apt-get install -y curl
+
+# configure knot-resolver-testing OBS repo for dependencies missing in Debian
+RUN echo 'deb http://download.opensuse.org/repositories/home:/CZ-NIC:/knot-resolver-testing/Debian_11/ /' > /etc/apt/sources.list.d/knot-resolver-testing.list
+RUN wget -nv https://download.opensuse.org/repositories/home:CZ-NIC:knot-resolver-testing/Debian_11/Release.key -O Release.key
+RUN APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 apt-key add Release.key
+RUN rm Release.key
+RUN apt-get update -qq
+
+# packages from our knot-resolver-testing repo
+RUN apt-get update
+RUN apt-get install -y -qqq lua-psl
+
+# en_US.UTF-8 locale for scripts.update-authors.sh
+RUN apt-get install -y -qqq locales
+RUN sed -i "/en_US.UTF-8/ s/^#\(.*\)/\1/" /etc/locale.gen
+RUN locale-gen
diff -pruN 5.4.4-1/ci/images/push.sh 5.5.1-5/ci/images/push.sh
--- 5.4.4-1/ci/images/push.sh	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/ci/images/push.sh	2022-06-14 07:17:30.371490200 +0000
@@ -0,0 +1,8 @@
+#!/bin/bash
+# upload docker image into registry
+
+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+source "${CURRENT_DIR}"/vars.sh "$@"
+set -ex
+
+docker push "${FULL_NAME}"
diff -pruN 5.4.4-1/ci/images/README.md 5.5.1-5/ci/images/README.md
--- 5.4.4-1/ci/images/README.md	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/ci/images/README.md	2022-06-14 07:17:30.371490200 +0000
@@ -0,0 +1,42 @@
+# Container images for CI
+
+## Image purpose
+
+### debian-11
+
+The main image used by shared runners to execute most CI builds and tests.
+
+### debian-11-coverity
+
+A stripped down version of `debian-11`. It only contains build (not test)
+dependencies of `kresd`. It also contains the `cov-build` tool for generating
+inputs for [Coverity Scan](https://scan.coverity.com/).
+
+It is used by the `coverity` CI job to generate and send data to Coverity Scan
+for analysis.
+
+### debian-bullseye
+
+Used to serve the same purpose as `debian-11`. As of 2022-03-09, it is still
+used by some jobs (linters).
+
+### lxc-debian-11
+
+Very similar to the main image. The main difference is a custom base image
+which can be used for LXC runners and boots into systemd. It is useful to
+update it when `debian-11` gets updated, as it will allow some of the tests to
+be migrated to the LXC runners in the future (especially the
+unstable/problematic ones - pytests already migrated, deckard might be a good
+candidate).
+
+## Maintenance
+
+The `ci/images/` directory contains utility scripts to build, push or update
+the container images.
+
+```
+$ ./build.sh debian-11    # builds a debian-11 image locally
+$ ./push.sh debian-11     # pushes the local image into target registry
+$ ./update.sh debian-11   # utility wrapper that both builds and pushes the image
+$ ./update.sh */          # use shell expansion of dirnames to update all images
+```
diff -pruN 5.4.4-1/ci/images/update.sh 5.5.1-5/ci/images/update.sh
--- 5.4.4-1/ci/images/update.sh	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/ci/images/update.sh	2022-06-14 07:17:30.371490200 +0000
@@ -0,0 +1,22 @@
+#!/bin/bash
+# build and upload docker image(s) into registry
+#
+# this is a simple wrapper around build.sh and update.sh
+#
+# to build & upload all images: ./update.sh */
+
+if [[ $# -le 0 ]]; then
+    echo "usage: $0 IMAGE..."
+    exit 1
+fi
+set -e
+
+for ARG in "$@"
+do
+    IMAGE=${ARG%/}
+    echo "Building $IMAGE..."
+    ./build.sh $IMAGE
+    echo "Pushing $IMAGE..."
+    ./push.sh $IMAGE
+done
+
diff -pruN 5.4.4-1/ci/images/vars.sh 5.5.1-5/ci/images/vars.sh
--- 5.4.4-1/ci/images/vars.sh	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/ci/images/vars.sh	2022-06-14 07:17:30.371490200 +0000
@@ -0,0 +1,13 @@
+#!/bin/bash
+# define common variables for image build scripts
+
+KNOT_BRANCH="${KNOT_BRANCH:-3.1}"
+
+REGISTRY="registry.nic.cz/knot/knot-resolver/ci"
+IMAGE=$1
+if [ -z "${IMAGE}" ]; then
+    echo "image name not provided"
+    exit 1
+fi
+TAG="knot-${KNOT_BRANCH}"
+FULL_NAME="${REGISTRY}/${IMAGE}:${TAG}"
diff -pruN 5.4.4-1/ci/pkgtest.yaml 5.5.1-5/ci/pkgtest.yaml
--- 5.4.4-1/ci/pkgtest.yaml	2022-01-05 13:19:14.415727100 +0000
+++ 5.5.1-5/ci/pkgtest.yaml	2022-06-14 07:17:30.371490200 +0000
@@ -20,8 +20,8 @@ stages:
     paths:
       - pkg/
 
-.apkgbuild: &apkgbuild
-  - pip3 install -U apkg
+.apkgbuild: &apkgbuild # new jinja2 breaks docs (sphinx/breathe)
+  - pip3 install -U apkg 'jinja2<3.1'
   - apkg build-dep -y
   - apkg build
 
@@ -143,10 +143,13 @@ ubuntu-21.10:pkgbuild:
 nixos-unstable:pkgbuild:
   <<: *pkgbuild
   # We do NOT use LXC, for now at least.
+  parallel:
+    matrix:
+      - PLATFORM: [ amd64, arm64 ]
   tags:
     - docker
     - linux
-    - amd64
+    - ${PLATFORM}
   image: nixos/nix
 
   variables:
@@ -154,7 +157,7 @@ nixos-unstable:pkgbuild:
     NIX_PATH: nixpkgs=https://github.com/vcunat/nixpkgs/archive/p/apkg.tar.gz
   before_script:
   script:
-    - nix build nixpkgs.apkg
+    - nix-build '<nixpkgs>' -QA apkg
     # the image auto-detects as alpine distro
     - ./result/bin/apkg install -d nix
     - kresd --version
diff -pruN 5.4.4-1/ci/README.md 5.5.1-5/ci/README.md
--- 5.4.4-1/ci/README.md	2022-01-05 13:19:14.415727100 +0000
+++ 5.5.1-5/ci/README.md	1970-01-01 00:00:00.000000000 +0000
@@ -1,11 +0,0 @@
-Docker Build
-------------
-
-```
-$ export DISTRO=debian-buster # also debian-11
-$ export KNOT_BRANCH=3.0 # also master
-$ docker build --no-cache -t registry.nic.cz/knot/knot-resolver/ci/$DISTRO:knot-$KNOT_BRANCH --build-arg KNOT_BRANCH=$KNOT_BRANCH $DISTRO
-
-$ docker login registry.nic.cz
-$ docker push registry.nic.cz/knot/knot-resolver/ci/$DISTRO:knot-$KNOT_BRANCH
-```
diff -pruN 5.4.4-1/daemon/bindings/cache.c 5.5.1-5/daemon/bindings/cache.c
--- 5.4.4-1/daemon/bindings/cache.c	2022-01-05 13:19:14.423727300 +0000
+++ 5.5.1-5/daemon/bindings/cache.c	2022-06-14 07:17:30.375490400 +0000
@@ -4,8 +4,6 @@
 
 #include "daemon/bindings/impl.h"
 
-#include "daemon/zimport.h"
-
 /** @internal return cache, or throw lua error if not open */
 static struct kr_cache * cache_assert_open(lua_State *L)
 {
@@ -130,10 +128,10 @@ static int cache_max_ttl(lua_State *L)
 			lua_error_p(L, "expected 'max_ttl(number ttl)'");
 		uint32_t min = cache->ttl_min;
 		int64_t ttl = lua_tointeger(L, 1);
-		if (ttl < 1 || ttl < min || ttl > UINT32_MAX) {
+		if (ttl < 1 || ttl < min || ttl > TTL_MAX_MAX) {
 			lua_error_p(L,
 				"max_ttl must be larger than minimum TTL, and in range <1, "
-				STR(UINT32_MAX) ">'");
+				STR(TTL_MAX_MAX) ">'");
 		}
 		cache->ttl_max = ttl;
 	}
@@ -152,10 +150,10 @@ static int cache_min_ttl(lua_State *L)
 			lua_error_p(L, "expected 'min_ttl(number ttl)'");
 		uint32_t max = cache->ttl_max;
 		int64_t ttl = lua_tointeger(L, 1);
-		if (ttl < 0 || ttl > max || ttl > UINT32_MAX) {
+		if (ttl < 0 || ttl > max || ttl > TTL_MAX_MAX) {
 			lua_error_p(L,
 				"min_ttl must be smaller than maximum TTL, and in range <0, "
-				STR(UINT32_MAX) ">'");
+				STR(TTL_MAX_MAX) ">'");
 		}
 		cache->ttl_min = ttl;
 	}
@@ -361,81 +359,6 @@ static int cache_ns_tout(lua_State *L)
 	return 1;
 }
 
-/** Zone import completion callback.
- * Deallocates zone import context. */
-static void cache_zone_import_cb(int state, void *param)
-{
-	(void)state;
-	struct worker_ctx *worker = param;
-	if (kr_fails_assert(worker && worker->z_import)) return;
-	zi_free(worker->z_import);
-	worker->z_import = NULL;
-}
-
-/** Import zone from file. */
-static int cache_zone_import(lua_State *L)
-{
-	int ret = -1;
-	char msg[128];
-
-	struct worker_ctx *worker = the_worker;
-	if (!worker) {
-		strncpy(msg, "internal error, empty worker pointer", sizeof(msg));
-		goto finish;
-	}
-
-	if (worker->z_import && zi_import_started(worker->z_import)) {
-		strncpy(msg, "import already started", sizeof(msg));
-		goto finish;
-	}
-
-	(void)cache_assert_open(L); /* just check it in advance */
-
-	/* Check parameters */
-	int n = lua_gettop(L);
-	if (n < 1 || !lua_isstring(L, 1)) {
-		strncpy(msg, "expected 'cache.zone_import(path to zone file)'", sizeof(msg));
-		goto finish;
-	}
-
-	/* Parse zone file */
-	const char *zone_file = lua_tostring(L, 1);
-
-	const char *default_origin = NULL; /* TODO */
-	uint16_t default_rclass = 1;
-	uint32_t default_ttl = 0;
-
-	if (worker->z_import == NULL) {
-		worker->z_import = zi_allocate(worker, cache_zone_import_cb, worker);
-		if (worker->z_import == NULL) {
-			strncpy(msg, "can't allocate zone import context", sizeof(msg));
-			goto finish;
-		}
-	}
-
-	ret = zi_zone_import(worker->z_import, zone_file, default_origin,
-			     default_rclass, default_ttl);
-
-	lua_newtable(L);
-	if (ret == 0) {
-		strncpy(msg, "zone file successfully parsed, import started", sizeof(msg));
-	} else if (ret == 1) {
-		strncpy(msg, "TA not found", sizeof(msg));
-	} else {
-		strncpy(msg, "error parsing zone file", sizeof(msg));
-	}
-
-finish:
-	msg[sizeof(msg) - 1] = 0;
-	lua_newtable(L);
-	lua_pushstring(L, msg);
-	lua_setfield(L, -2, "msg");
-	lua_pushnumber(L, ret);
-	lua_setfield(L, -2, "code");
-
-	return 1;
-}
-
 int kr_bindings_cache(lua_State *L)
 {
 	static const luaL_Reg lib[] = {
@@ -450,7 +373,6 @@ int kr_bindings_cache(lua_State *L)
 		{ "max_ttl", cache_max_ttl },
 		{ "min_ttl", cache_min_ttl },
 		{ "ns_tout", cache_ns_tout },
-		{ "zone_import", cache_zone_import },
 		{ NULL, NULL }
 	};
 
diff -pruN 5.4.4-1/daemon/bindings/impl.h 5.5.1-5/daemon/bindings/impl.h
--- 5.4.4-1/daemon/bindings/impl.h	2022-01-05 13:19:14.423727300 +0000
+++ 5.5.1-5/daemon/bindings/impl.h	2022-06-14 07:17:30.375490400 +0000
@@ -83,7 +83,7 @@ static inline int execute_callback(lua_S
  */
 static inline void lua_pushpointer(lua_State *L, void *p)
 {
-       void *addr = lua_newuserdata(L, sizeof(void *));
+       void **addr = lua_newuserdata(L, sizeof(void *));
        kr_require(addr);
        memcpy(addr, &p, sizeof(void *));
 }
diff -pruN 5.4.4-1/daemon/bindings/net.c 5.5.1-5/daemon/bindings/net.c
--- 5.4.4-1/daemon/bindings/net.c	2022-01-05 13:19:14.427727200 +0000
+++ 5.5.1-5/daemon/bindings/net.c	2022-06-14 07:17:30.375490400 +0000
@@ -5,17 +5,21 @@
 #include "daemon/bindings/impl.h"
 
 #include "contrib/base64.h"
+#include "contrib/cleanup.h"
 #include "daemon/network.h"
 #include "daemon/tls.h"
+#include "lib/utils.h"
 
 #include <stdlib.h>
 
+#define PROXY_DATA_STRLEN (INET6_ADDRSTRLEN + 1 + 3 + 1)
+
 /** Table and next index on top of stack -> append entries for given endpoint_array_t. */
-static int net_list_add(const char *key, void *val, void *ext)
+static int net_list_add(const char *b_key, uint32_t key_len, trie_val_t *val, void *ext)
 {
+	endpoint_array_t *ep_array = *val;
 	lua_State *L = (lua_State *)ext;
 	lua_Integer i = lua_tointeger(L, -1);
-	endpoint_array_t *ep_array = val;
 	for (int j = 0; j < ep_array->len; ++j) {
 		struct endpoint *ep = &ep_array->at[j];
 		lua_newtable(L);  // connection tuple
@@ -54,7 +58,15 @@ static int net_list_add(const char *key,
 		}
 		lua_setfield(L, -2, "family");
 
-		lua_pushstring(L, key);
+		const char *ip_str_const = network_endpoint_key_str((struct endpoint_key *) b_key);
+		kr_require(ip_str_const);
+		auto_free char *ip_str = strdup(ip_str_const);
+		kr_require(ip_str);
+		char *hm = strchr(ip_str, '#');
+		if (hm) /* Omit port */
+			*hm = '\0';
+		lua_pushstring(L, ip_str);
+
 		if (ep->family == AF_INET || ep->family == AF_INET6) {
 			lua_setfield(L, -2, "ip");
 			lua_pushboolean(L, ep->flags.freebind);
@@ -98,7 +110,7 @@ static int net_list(lua_State *L)
 {
 	lua_newtable(L);
 	lua_pushinteger(L, 1);
-	map_walk(&the_worker->engine->net.endpoints, net_list_add, L);
+	trie_apply_with_key(the_worker->engine->net.endpoints, net_list_add, L);
 	lua_pop(L, 1);
 	return 1;
 }
@@ -280,6 +292,114 @@ static int net_listen(lua_State *L)
 	return 1;
 }
 
+/** Prints the specified `data` into the specified `dst` buffer. */
+static char *proxy_data_to_string(int af, const struct net_proxy_data *data,
+		char *dst, size_t size)
+{
+	kr_assert(size >= PROXY_DATA_STRLEN);
+	const void *in_addr = (af == AF_INET)
+		? (void *) &data->addr.ip4
+		: (void *) &data->addr.ip6;
+	char *cur = dst;
+
+	const char *ret = inet_ntop(af, in_addr, cur, size);
+	if (!ret)
+		return NULL;
+
+	cur += strlen(cur); /*< advance cursor to after the address */
+	*(cur++) = '/';
+	int masklen = snprintf(cur, 3 + 1, "%u", data->netmask);
+	cur[masklen] = '\0';
+	return dst;
+}
+
+/** Put all IP addresses from `trie` into the table at the top of the Lua stack.
+ * For each address, increment the integer at `i`. All addresses in `trie` must
+ * be from the specified `family`. */
+static void net_proxy_addr_put(lua_State *L, int family, trie_t *trie, int *i)
+{
+	char addrbuf[PROXY_DATA_STRLEN];
+	const char *addr;
+	trie_it_t *it;
+	for (it = trie_it_begin(trie); !trie_it_finished(it); trie_it_next(it)) {
+		lua_pushinteger(L, *i);
+		struct net_proxy_data *data = *trie_it_val(it);
+		addr = proxy_data_to_string(family, data,
+				addrbuf, sizeof(addrbuf));
+		lua_pushstring(L, addr);
+		lua_settable(L, -3);
+		*i += 1;
+	}
+	trie_it_free(it);
+}
+
+/** Allow PROXYv2 headers for IP address. */
+static int net_proxy_allowed(lua_State *L)
+{
+	struct network *net = &the_worker->engine->net;
+	int n = lua_gettop(L);
+	int i = 1;
+	const char *addr;
+
+	/* Return current state */
+	if (n == 0) {
+		lua_newtable(L);
+		i = 1;
+
+		if (net->proxy_all4) {
+			lua_pushinteger(L, i);
+			lua_pushstring(L, "0.0.0.0/0");
+			lua_settable(L, -3);
+			i += 1;
+		} else {
+			net_proxy_addr_put(L, AF_INET, net->proxy_addrs4, &i);
+		}
+
+		if (net->proxy_all6) {
+			lua_pushinteger(L, i);
+			lua_pushstring(L, "::/0");
+			lua_settable(L, -3);
+			i += 1;
+		} else {
+			net_proxy_addr_put(L, AF_INET6, net->proxy_addrs6, &i);
+		}
+
+		return 1;
+	}
+
+	if (n != 1)
+		lua_error_p(L, "net.proxy_allowed() takes one parameter (string or table)");
+
+	if (!lua_istable(L, 1) && !lua_isstring(L, 1))
+		lua_error_p(L, "net.proxy_allowed() argument must be string or table");
+
+	/* Reset allowed proxy addresses */
+	network_proxy_reset(net);
+
+	/* Add new proxy addresses */
+	if (lua_istable(L, 1)) {
+		for (i = 1; !lua_isnil(L, -1); i++) {
+			lua_pushinteger(L, i);
+			lua_gettable(L, 1);
+			if (lua_isnil(L, -1)) /* missing value - end iteration */
+				break;
+			if (!lua_isstring(L, -1))
+				lua_error_p(L, "net.proxy_allowed() argument may only contain strings");
+			addr = lua_tostring(L, -1);
+			int ret = network_proxy_allow(net, addr);
+			if (ret)
+				lua_error_p(L, "invalid argument");
+		}
+	} else if (lua_isstring(L, 1)) {
+		addr = lua_tostring(L, 1);
+		int ret = network_proxy_allow(net, addr);
+		if (ret)
+			lua_error_p(L, "invalid argument");
+	}
+
+	return 0;
+}
+
 /** Close endpoint. */
 static int net_close(lua_State *L)
 {
@@ -333,7 +453,17 @@ static int net_interfaces(lua_State *L)
 		} else {
 			buf[0] = '\0';
 		}
-		lua_pushstring(L, buf);
+
+		if (kr_sockaddr_link_local((struct sockaddr *) &iface.address)) {
+			/* Link-local IPv6: add %interface prefix */
+			auto_free char *str = NULL;
+			int ret = asprintf(&str, "%s%%%s", buf, iface.name);
+			kr_assert(ret > 0);
+			lua_pushstring(L, str);
+		} else {
+			lua_pushstring(L, buf);
+		}
+
 		lua_rawseti(L, -2, lua_objlen(L, -2) + 1);
 		lua_setfield(L, -2, "addr");
 
@@ -929,11 +1059,11 @@ static int net_tls_sticket_secret_file(l
 
 static int net_outgoing(lua_State *L, int family)
 {
-	union inaddr *addr;
+	union kr_sockaddr *addr;
 	if (family == AF_INET)
-		addr = (union inaddr*)&the_worker->out_addr4;
+		addr = (union kr_sockaddr*)&the_worker->out_addr4;
 	else
-		addr = (union inaddr*)&the_worker->out_addr6;
+		addr = (union kr_sockaddr*)&the_worker->out_addr6;
 
 	if (lua_gettop(L) == 0) { /* Return the current value. */
 		if (addr->ip.sa_family == AF_UNSPEC) {
@@ -1102,6 +1232,7 @@ int kr_bindings_net(lua_State *L)
 	static const luaL_Reg lib[] = {
 		{ "list",         net_list },
 		{ "listen",       net_listen },
+		{ "proxy_allowed", net_proxy_allowed },
 		{ "close",        net_close },
 		{ "interfaces",   net_interfaces },
 		{ "bufsize",      net_bufsize },
diff -pruN 5.4.4-1/daemon/bindings/net_server.rst 5.5.1-5/daemon/bindings/net_server.rst
--- 5.4.4-1/daemon/bindings/net_server.rst	2022-01-05 13:19:14.427727200 +0000
+++ 5.5.1-5/daemon/bindings/net_server.rst	2022-06-14 07:17:30.375490400 +0000
@@ -66,6 +66,65 @@ Examples:
         addresses if the network address ranges overlap,
         and clients would probably refuse such a response.
 
+.. _proxyv2:
+
+PROXYv2 protocol
+^^^^^^^^^^^^^^^^
+
+Knot Resolver supports proxies that utilize the `PROXYv2 protocol <https://www.haproxy.org/download/2.5/doc/proxy-protocol.txt>`_
+to identify clients.
+
+A PROXY header contains the IP address of the original client who sent a query.
+This allows the resolver to treat queries as if they actually came from
+the client's IP address rather than the address of the proxy they came through.
+For example, :ref:`Views and ACLs <mod-view>` are able to work properly when
+PROXYv2 is in use.
+
+Since allowing usage of the PROXYv2 protocol for all clients would be a security
+vulnerability, because clients would then be able to spoof their IP addresses via
+the PROXYv2 header, the resolver requires you to specify explicitly which clients
+are allowed to send PROXYv2 headers via the :func:`net.proxy_allowed` function.
+
+PROXYv2 queries from clients who are not explicitly allowed to use this protocol
+will be discarded.
+
+.. function:: net.proxy_allowed([addresses])
+
+   Allow usage of the PROXYv2 protocol headers by clients on the specified
+   ``addresses``. It is possible to permit whole networks to send PROXYv2 headers
+   by specifying the network mask using the CIDR notation
+   (e.g. ``172.22.0.0/16``). IPv4 as well as IPv6 addresses are supported.
+
+   If you wish to allow all clients to use PROXYv2 (e.g. because you have this
+   kind of security handled on another layer of your network infrastructure),
+   you can specify a netmask of ``/0``. Please note that this setting is
+   address-family-specific, so this needs to be applied to both IPv4 and IPv6
+   separately.
+
+   Subsequent calls to the function overwrite the effects of all previous calls.
+   Providing a table of strings as the function parameter allows multiple
+   distinct addresses to use the PROXYv2 protocol.
+
+   When called without arguments, ``net.proxy_allowed`` returns a table of all
+   addresses currently allowed to use the PROXYv2 protocol and does not change
+   the configuration.
+
+Examples:
+
+   .. code-block:: lua
+
+	net.proxy_allowed('172.22.0.1')    -- allows '172.22.0.1' specifically
+	net.proxy_allowed('172.18.1.0/24') -- allows everyone at '172.18.1.*'
+	net.proxy_allowed({
+		'172.22.0.1', '172.18.1.0/24'
+	})                                 -- allows both of the above at once
+	net.proxy_allowed({ 'fe80::/10' }  -- allows everyone at IPv6 link-local
+	net.proxy_allowed({
+		'::/0', '0.0.0.0/0'
+	})                                 -- allows everyone
+	net.proxy_allowed('::/0')          -- allows all IPv6 (but no IPv4)
+	net.proxy_allowed({})              -- prevents everyone from using PROXYv2
+	net.proxy_allowed()                -- returns a list of all currently allowed addresses
 
 Features for scripting
 ^^^^^^^^^^^^^^^^^^^^^^
diff -pruN 5.4.4-1/daemon/bindings/net_tlssrv.rst 5.5.1-5/daemon/bindings/net_tlssrv.rst
--- 5.4.4-1/daemon/bindings/net_tlssrv.rst	2022-01-05 13:19:14.427727200 +0000
+++ 5.5.1-5/daemon/bindings/net_tlssrv.rst	2022-06-14 07:17:30.375490400 +0000
@@ -68,6 +68,28 @@ additional considerations for TLS 1.2 re
 
 .. _dot-doh-config-options:
 
+HTTP status codes
+"""""""""""""""""
+
+As specified by :rfc:`8484`, the resolver responds with status **200 OK** whenever
+it can produce a valid DNS reply for a given query, even in cases where the DNS
+``rcode`` indicates an error (like ``NXDOMAIN``, ``SERVFAIL``, etc.).
+
+For DoH queries malformed at the HTTP level, the resolver may respond with
+the following status codes:
+
+ * **400 Bad Request** for a generally malformed query, like one not containing
+   a valid DNS packet
+ * **404 Not Found** when an incorrect HTTP endpoint is queried - the only
+   supported ones are ``/dns-query`` and ``/doh``
+ * **413 Payload Too Large** when the DNS query exceeds its maximum size
+ * **415 Unsupported Media Type** when the query's ``Content-Type`` header
+   is not ``application/dns-message``
+ * **431 Request Header Fields Too Large** when a header in the query is too
+   large to process
+ * **501 Not Implemented** when the query uses a method other than
+   ``GET``, ``POST``, or ``HEAD``
+
 Configuration options for DoT and DoH
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -113,15 +135,18 @@ by a trusted CA. This is done using func
    This synchronization works only among instances having the same endianness
    and time_t structure and size (`sizeof(time_t)`).
 
+.. _pfs: https://en.wikipedia.org/wiki/Forward_secrecy
+
    **For good security** the secret must have enough entropy to be hard to guess,
    and it should still be occasionally rotated manually and securely forgotten,
    to reduce the scope of privacy leak in case the
-   `secret leaks eventually <https://en.wikipedia.org/wiki/Forward_secrecy>`_.
+   `secret leaks eventually <pfs_>`_.
 
-   .. warning:: **Setting the secret is probably too risky with TLS <= 1.2**.
-      GnuTLS stable release supports TLS 1.3 since 3.6.3 (summer 2018).
-      Therefore setting the secrets should be considered experimental for now
-      and might not be available on your system.
+   .. warning:: **Setting the secret is probably too risky with TLS <= 1.2 and
+      GnuTLS < 3.7.5**. GnuTLS 3.7.5 adds an option to disable resumption via
+      tickets for TLS <= 1.2, enabling them only for protocols that do guarantee
+      `PFS <pfs_>`_. Knot Resolver makes use of this new option when linked
+      against GnuTLS >= 3.7.5.
 
 .. function:: net.tls_sticket_secret_file([string with path to a file containing pre-shared secret])
 
diff -pruN 5.4.4-1/daemon/cache.test/clear.test.lua 5.5.1-5/daemon/cache.test/clear.test.lua
--- 5.4.4-1/daemon/cache.test/clear.test.lua	2022-01-05 13:19:14.427727200 +0000
+++ 5.5.1-5/daemon/cache.test/clear.test.lua	2022-06-14 07:17:30.379490400 +0000
@@ -47,8 +47,8 @@ env.KRESD_NO_LISTEN = true
 
 
 local function import_zone()
-	local import_res = cache.zone_import('testroot.zone')
-	assert(import_res.code == 0)
+	local import_res = require('ffi').C.zi_zone_import({ zone_file = 'testroot.zone' })
+	assert(import_res == 0)
 	-- beware that import takes at least 100 ms
 	worker.sleep(0.2)  -- zimport is delayed by 100 ms from function call
 	-- sanity checks - cache must be filled in
@@ -189,22 +189,23 @@ end
 local function test_cache_used(lower, upper)
 	return function()
 		local usage = cache.stats().usage_percent
-		ok(usage >= lower and usage <= upper, string.format('cache percentage usage is between <%d, %d>', lower, upper))
+		ok(usage >= lower and usage <= upper,
+		   string.format('cache percentage usage %.1f is between <%d, %d>', usage, lower, upper))
 	end
 end
 
 return {
 	test_cache_used(0, 1),
 	import_zone,
-	test_cache_used(11, 12),
+	test_cache_used(9, 10),
 	test_exact_match_qtype,
 	test_exact_match_qname,
 	test_callback,
 	import_zone,
 	test_subtree,
-	test_cache_used(10, 11),
+	test_cache_used(9, 10),
 	test_subtree_limit,
-	test_cache_used(5, 6),
+	test_cache_used(5, 7),
 	test_apex,
 	import_zone,
 	test_root,
diff -pruN 5.4.4-1/daemon/engine.c 5.5.1-5/daemon/engine.c
--- 5.4.4-1/daemon/engine.c	2022-01-05 13:19:14.427727200 +0000
+++ 5.5.1-5/daemon/engine.c	2022-06-14 07:17:30.379490400 +0000
@@ -490,8 +490,8 @@ static int init_resolver(struct engine *
 	ctx->options.REORDER_RR = true;
 
 	/* Open resolution context */
-	ctx->trust_anchors = map_make(NULL);
-	ctx->negative_anchors = map_make(NULL);
+	ctx->trust_anchors = trie_create(NULL);
+	ctx->negative_anchors = trie_create(NULL);
 	ctx->pool = engine->pool;
 	ctx->modules = &engine->modules;
 	ctx->cache_rtt_tout_retry_interval = KR_NS_TIMEOUT_RETRY_INTERVAL;
@@ -699,8 +699,10 @@ void engine_deinit(struct engine *engine
 	/* Free data structures */
 	array_clear(engine->modules);
 	array_clear(engine->backends);
-	kr_ta_clear(&engine->resolver.trust_anchors);
-	kr_ta_clear(&engine->resolver.negative_anchors);
+	kr_ta_clear(engine->resolver.trust_anchors);
+	trie_free(engine->resolver.trust_anchors);
+	kr_ta_clear(engine->resolver.negative_anchors);
+	trie_free(engine->resolver.negative_anchors);
 	free(engine->hostname);
 }
 
diff -pruN 5.4.4-1/daemon/http.c 5.5.1-5/daemon/http.c
--- 5.4.4-1/daemon/http.c	2022-01-05 13:19:14.427727200 +0000
+++ 5.5.1-5/daemon/http.c	2022-06-14 07:17:30.379490400 +0000
@@ -8,6 +8,7 @@
  */
 
 #include <errno.h>
+#include <inttypes.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -18,16 +19,26 @@
 #include "daemon/session.h"
 #include "lib/layer/iterate.h" /* kr_response_classify */
 #include "lib/cache/util.h"
+#include "lib/generic/array.h"
 
 #include "contrib/cleanup.h"
 #include "contrib/base64url.h"
 
+/** Makes a `nghttp2_nv`. `K` is the key, `KS` is the key length,
+ * `V` is the value, `VS` is the value length. */
 #define MAKE_NV(K, KS, V, VS) \
-	{ (uint8_t *)(K), (uint8_t *)(V), (KS), (VS), NGHTTP2_NV_FLAG_NONE }
+	(nghttp2_nv) { (uint8_t *)(K), (uint8_t *)(V), (KS), (VS), NGHTTP2_NV_FLAG_NONE }
 
+/** Makes a `nghttp2_nv` with static data. `K` is the key,
+ * `V` is the value. Both `K` and `V` MUST be string literals. */
 #define MAKE_STATIC_NV(K, V) \
 	MAKE_NV((K), sizeof(K) - 1, (V), sizeof(V) - 1)
 
+/** Makes a `nghttp2_nv` with a static key. `K` is the key,
+ * `V` is the value, `VS` is the value length. `K` MUST be a string literal. */
+#define MAKE_STATIC_KEY_NV(K, V, VS) \
+	MAKE_NV((K), sizeof(K) - 1, (V), (VS))
+
 /* Use same maximum as for tcp_pipeline_max. */
 #define HTTP_MAX_CONCURRENT_STREAMS UINT16_MAX
 
@@ -47,6 +58,20 @@ struct http_data {
 	uv_write_t *req;
 };
 
+typedef array_t(nghttp2_nv) nghttp2_array_t;
+
+static int http_send_response(struct http_ctx *ctx, int32_t stream_id,
+			      nghttp2_data_provider *prov, enum http_status status);
+static int http_send_response_rst_stream(struct http_ctx *ctx, int32_t stream_id,
+			      nghttp2_data_provider *prov, enum http_status status);
+
+/** Checks if `status` has the correct `category`.
+ * E.g. status 200 has category 2, status 404 has category 4, 501 has category 5 etc. */
+static inline bool http_status_has_category(enum http_status status, int category)
+{
+	return status / 100 == category;
+}
+
 /*
  * Write HTTP/2 protocol data to underlying transport layer.
  */
@@ -58,6 +83,16 @@ static ssize_t send_callback(nghttp2_ses
 }
 
 /*
+ * Sets the HTTP status of the specified `context`, but only if its status has
+ * not already been changed to an unsuccessful one.
+ */
+static inline void set_status(struct http_ctx *ctx, enum http_status status)
+{
+	if (http_status_has_category(ctx->status, 2))
+		ctx->status = status;
+}
+
+/*
  * Send padding length (if greater than zero).
  */
 static int send_padlen(struct http_ctx *ctx, size_t padlen)
@@ -174,7 +209,7 @@ static int check_uri(const char* uri_pat
 	}
 
 	if (ret) /* no endpoint found */
-		return -1;
+		return kr_error(ENOENT);
 
 	/* FIXME This also passes for GET when no variables are provided.
 	 * Fixing it doesn't seem straightforward, since :method may not be
@@ -191,7 +226,7 @@ static int check_uri(const char* uri_pat
 			}
 			end_prev = beg + strlen(beg);
 			beg = strtok(NULL, delim);
-			if (beg-1 != end_prev) { /* detect && */
+			if (!beg || beg-1 != end_prev) { /* detect && */
 				return -1;
 			}
 		}
@@ -204,6 +239,23 @@ static int check_uri(const char* uri_pat
 	return 0;
 }
 
+static kr_http_header_array_t *headers_dup(kr_http_header_array_t *src)
+{
+	kr_http_header_array_t *dst = malloc(sizeof(kr_http_header_array_t));
+	kr_require(dst);
+	array_init(*dst);
+	for (size_t i = 0; i < src->len; i++) {
+		struct kr_http_header_array_entry *src_entry = &src->at[i];
+		struct kr_http_header_array_entry dst_entry = {
+			.name = strdup(src_entry->name),
+			.value = strdup(src_entry->value)
+		};
+		array_push(*dst, dst_entry);
+	}
+
+	return dst;
+}
+
 /*
  * Process a query from URI path if there's base64url encoded dns variable.
  */
@@ -216,7 +268,6 @@ static int process_uri_path(struct http_
 	char *beg = strstr(path, key);
 	char *end;
 	size_t remaining;
-	ssize_t ret;
 	uint8_t *dest;
 
 	if (!beg)  /* No dns variable in path. */
@@ -231,10 +282,10 @@ static int process_uri_path(struct http_
 	remaining = ctx->buf_size - ctx->submitted - ctx->buf_pos;
 	dest = ctx->buf + ctx->buf_pos;
 
-	ret = kr_base64url_decode((uint8_t*)beg, end - beg, dest, remaining);
+	int ret = kr_base64url_decode((uint8_t*)beg, end - beg, dest, remaining);
 	if (ret < 0) {
 		ctx->buf_pos = 0;
-		kr_log_debug(DOH, "[%p] base64url decode failed %s\n", (void *)ctx->h2, strerror(ret));
+		kr_log_debug(DOH, "[%p] base64url decode failed %s\n", (void *)ctx->h2, kr_strerror(ret));
 		return ret;
 	}
 
@@ -242,7 +293,7 @@ static int process_uri_path(struct http_
 
 	struct http_stream stream = {
 		.id = stream_id,
-		.headers = ctx->headers
+		.headers = headers_dup(ctx->headers)
 	};
 	queue_push(ctx->streams, stream);
 	return 0;
@@ -302,6 +353,7 @@ static int begin_headers_callback(nghttp
 	} else {
 		http_cleanup_stream(ctx);  // Free any leftover data and ensure pristine state
 		ctx->incomplete_stream = stream_id;
+		ctx->last_stream = stream_id;
 		ctx->headers = malloc(sizeof(kr_http_header_array_t));
 		array_init(*ctx->headers);
 	}
@@ -341,7 +393,7 @@ static int header_callback(nghttp2_sessi
 				kr_log_debug(DOH,
 					"[%p] stream %d: header too large (%zu B), refused\n",
 					(void *)h2, stream_id, valuelen);
-				refuse_stream(h2, stream_id);
+				set_status(ctx, HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE);
 				return 0;
 			}
 
@@ -360,8 +412,12 @@ static int header_callback(nghttp2_sessi
 	}
 
 	if (!strcasecmp(":path", (const char *)name)) {
-		if (check_uri((const char *)value) < 0) {
-			refuse_stream(h2, stream_id);
+		int uri_result = check_uri((const char *)value);
+		if (uri_result == kr_error(ENOENT)) {
+			set_status(ctx, HTTP_STATUS_NOT_FOUND);
+			return 0;
+		} else if (uri_result < 0) {
+			set_status(ctx, HTTP_STATUS_BAD_REQUEST);
 			return 0;
 		}
 
@@ -378,8 +434,19 @@ static int header_callback(nghttp2_sessi
 			ctx->current_method = HTTP_METHOD_GET;
 		} else if (!strcasecmp("post", (const char *)value)) {
 			ctx->current_method = HTTP_METHOD_POST;
+		} else if (!strcasecmp("head", (const char *)value)) {
+			ctx->current_method = HTTP_METHOD_HEAD;
 		} else {
 			ctx->current_method = HTTP_METHOD_NONE;
+			set_status(ctx, HTTP_STATUS_NOT_IMPLEMENTED);
+			return 0;
+		}
+	}
+
+	if (!strcasecmp("content-type", (const char *)name)) {
+		if (strcasecmp("application/dns-message", (const char *)value)) {
+			set_status(ctx, HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE);
+			return 0;
 		}
 	}
 
@@ -433,7 +500,7 @@ static int data_chunk_recv_callback(nght
 		ctx->buf_pos = sizeof(uint16_t);  /* Reserve 2B for dnsmsg len. */
 		struct http_stream stream = {
 			.id = stream_id,
-			.headers = ctx->headers
+			.headers = headers_dup(ctx->headers)
 		};
 		queue_push(ctx->streams, stream);
 	}
@@ -448,7 +515,8 @@ static int submit_to_wirebuffer(struct h
 	int ret = -1;
 	ssize_t len;
 
-	/* Transfer ownership to stream (waiting in wirebuffer) */
+	/* Free http_ctx's headers - by now the stream has obtained its own
+	 * copy of the headers which it can operate on. */
 	/* FIXME: technically, transferring memory ownership should happen
 	 * along with queue_push(ctx->streams) to avoid confusion of who owns
 	 * what and when. Pushing to queue should be done AFTER we successfully
@@ -458,13 +526,25 @@ static int submit_to_wirebuffer(struct h
 	 *
 	 * For now, we assume memory is transferred even on error and the
 	 * headers themselves get cleaned up during http_free() which is
-	 * triggered after the error when session is closed.  */
+	 * triggered after the error when session is closed.
+	 *
+	 * EDIT(2022-05-19): The original logic was causing occasional
+	 * double-free conditions once status code support was extended.
+	 *
+	 * Currently, we are copying the headers from ctx instead of transferring
+	 * ownership, which is still a dirty workaround and, ideally, the whole
+	 * logic around header (de)allocation should be reworked to make
+	 * the ownership situation clear. */
+	http_free_headers(ctx->headers);
 	ctx->headers = NULL;
 
 	len = ctx->buf_pos - sizeof(uint16_t);
 	if (len <= 0 || len > KNOT_WIRE_MAX_PKTSIZE) {
 		kr_log_debug(DOH, "[%p] invalid dnsmsg size: %zd B\n", (void *)ctx->h2, len);
-		ret = -1;
+		set_status(ctx, (len <= 0)
+				? HTTP_STATUS_BAD_REQUEST
+				: HTTP_STATUS_PAYLOAD_TOO_LARGE);
+		ret = 0;
 		goto cleanup;
 	}
 
@@ -495,10 +575,13 @@ static int on_frame_recv_callback(nghttp
 		return NGHTTP2_ERR_CALLBACK_FAILURE;
 
 	if ((frame->hd.flags & NGHTTP2_FLAG_END_STREAM) && ctx->incomplete_stream == stream_id) {
-		if (ctx->current_method == HTTP_METHOD_GET) {
+		ctx->streaming = false;
+
+		if (ctx->current_method == HTTP_METHOD_GET || ctx->current_method == HTTP_METHOD_HEAD) {
 			if (process_uri_path(ctx, ctx->uri_path, stream_id) < 0) {
-				refuse_stream(h2, stream_id);
-				return 0;  /* End processing - don't submit to wirebuffer. */
+				/* End processing - don't submit to wirebuffer. */
+				set_status(ctx, HTTP_STATUS_BAD_REQUEST);
+				return 0;
 			}
 		}
 
@@ -585,9 +668,12 @@ struct http_ctx* http_new(struct session
 	queue_init(ctx->streams);
 	ctx->stream_write_data = trie_create(NULL);
 	ctx->incomplete_stream = -1;
+	ctx->last_stream = -1;
 	ctx->submitted = 0;
+	ctx->streaming = true;
 	ctx->current_method = HTTP_METHOD_NONE;
 	ctx->uri_path = NULL;
+	ctx->status = HTTP_STATUS_OK;
 
 	nghttp2_session_server_new(&ctx->h2, callbacks, ctx);
 	nghttp2_submit_settings(ctx->h2, NGHTTP2_FLAG_NONE,
@@ -604,9 +690,12 @@ finish:
  * Process inbound HTTP/2 data and return number of bytes read into session wire buffer.
  *
  * This function may trigger outgoing HTTP/2 data, such as stream resets, window updates etc.
+ *
+ * Returns 1 if stream has not ended yet, 0 if the stream has ended, or
+ * a negative value on error.
  */
-ssize_t http_process_input_data(struct session *session, const uint8_t *buf,
-				ssize_t nread)
+int http_process_input_data(struct session *session, const uint8_t *buf, ssize_t nread,
+			    ssize_t *out_submitted)
 {
 	struct http_ctx *ctx = session_http_get_server_ctx(session);
 	ssize_t ret = 0;
@@ -623,6 +712,7 @@ ssize_t http_process_input_data(struct s
 	 * query will be ignored).  This may also be problematic in other
 	 * cases.  */
 	ctx->submitted = 0;
+	ctx->streaming = true;
 	ctx->buf = session_wirebuf_get_free_start(session);
 	ctx->buf_pos = 0;
 	ctx->buf_size = session_wirebuf_get_free_size(session);
@@ -641,7 +731,25 @@ ssize_t http_process_input_data(struct s
 		return kr_error(EIO);
 	}
 
-	return ctx->submitted;
+	if (!http_status_has_category(ctx->status, 2)) {
+		*out_submitted = 0;
+		http_send_status(session, ctx->status);
+		http_cleanup_stream(ctx);
+		return 0;
+	}
+
+	*out_submitted = ctx->submitted;
+	return ctx->streaming;
+}
+
+int http_send_status(struct session *session, enum http_status status)
+{
+	struct http_ctx *ctx = session_http_get_server_ctx(session);
+	if (ctx->last_stream >= 0)
+		return http_send_response_rst_stream(
+				ctx, ctx->last_stream, NULL, status);
+
+	return 0;
 }
 
 /*
@@ -671,35 +779,67 @@ static ssize_t read_callback(nghttp2_ses
 	return send;
 }
 
+/** Convenience function for pushing `nghttp2_nv` made with MAKE_*_NV into
+ * arrays. */
+static inline void push_nv(nghttp2_array_t *arr, nghttp2_nv nv)
+{
+	array_push(*arr, nv);
+}
+
 /*
  * Send dns response provided by the HTTP/2 data provider.
  *
  * Data isn't guaranteed to be sent immediately due to underlying HTTP/2 flow control.
  */
 static int http_send_response(struct http_ctx *ctx, int32_t stream_id,
-			      nghttp2_data_provider *prov)
+			      nghttp2_data_provider *prov, enum http_status status)
 {
 	nghttp2_session *h2 = ctx->h2;
-	struct http_data *data = (struct http_data*)prov->source.ptr;
 	int ret;
-	const char *directive_max_age = "max-age=";
-	char size[MAX_DECIMAL_LENGTH(data->len)] = { 0 };
-	int max_age_len = MAX_DECIMAL_LENGTH(data->ttl) + strlen(directive_max_age);
-	char max_age[max_age_len];
-	int size_len;
-
-	memset(max_age, 0, max_age_len * sizeof(*max_age));
-	size_len = snprintf(size, MAX_DECIMAL_LENGTH(data->len), "%zu", data->len);
-	max_age_len = snprintf(max_age, max_age_len, "%s%u", directive_max_age, data->ttl);
-
-	nghttp2_nv hdrs[] = {
-		MAKE_STATIC_NV(":status", "200"),
-		MAKE_STATIC_NV("content-type", "application/dns-message"),
-		MAKE_NV("content-length", 14, size, size_len),
-		MAKE_NV("cache-control", 13, max_age, max_age_len),
-	};
 
-	ret = nghttp2_submit_response(h2, stream_id, hdrs, sizeof(hdrs)/sizeof(*hdrs), prov);
+	nghttp2_array_t hdrs;
+	array_init(hdrs);
+	array_reserve(hdrs, 5);
+
+	auto_free char *status_str = NULL;
+	if (likely(status == HTTP_STATUS_OK)) {
+		push_nv(&hdrs, MAKE_STATIC_NV(":status", "200"));
+	} else {
+		int status_len = asprintf(&status_str, "%" PRIu16, status);
+		kr_require(status_len >= 0);
+		push_nv(&hdrs, MAKE_STATIC_KEY_NV(":status", status_str, status_len));
+	}
+	push_nv(&hdrs, MAKE_STATIC_NV("access-control-allow-origin", "*"));
+
+	struct http_data *data = NULL;
+	auto_free char *size = NULL;
+	auto_free char *max_age = NULL;
+
+	if (ctx->current_method == HTTP_METHOD_HEAD && prov) {
+		/* HEAD method is the same as GET but only returns headers,
+		 * so let's clean up the data here as we don't need it. */
+		free(prov->source.ptr);
+		prov = NULL;
+	}
+
+	if (prov) {
+		data = (struct http_data*)prov->source.ptr;
+		const char *directive_max_age = "max-age=";
+		int max_age_len;
+		int size_len;
+
+		size_len = asprintf(&size, "%zu", data->len);
+		kr_require(size_len >= 0);
+		max_age_len = asprintf(&max_age, "%s%" PRIu32, directive_max_age, data->ttl);
+		kr_require(max_age_len >= 0);
+
+		push_nv(&hdrs, MAKE_STATIC_NV("content-type", "application/dns-message"));
+		push_nv(&hdrs, MAKE_STATIC_KEY_NV("content-length", size, size_len));
+		push_nv(&hdrs, MAKE_STATIC_KEY_NV("cache-control", max_age, max_age_len));
+	}
+
+	ret = nghttp2_submit_response(h2, stream_id, hdrs.at, hdrs.len, prov);
+	array_clear(hdrs);
 	if (ret != 0) {
 		kr_log_debug(DOH, "[%p] nghttp2_submit_response failed: %s\n", (void *)h2, nghttp2_strerror(ret));
 		free(data);
@@ -734,6 +874,24 @@ static int http_send_response(struct htt
 }
 
 /*
+ * Same as `http_send_response`, but resets the HTTP stream afterwards. Used
+ * for sending negative status messages.
+ */
+static int http_send_response_rst_stream(struct http_ctx *ctx, int32_t stream_id,
+			      nghttp2_data_provider *prov, enum http_status status)
+{
+	int ret = http_send_response(ctx, stream_id, prov, status);
+	if (ret)
+		return ret;
+
+	ctx->last_stream = -1;
+	nghttp2_submit_rst_stream(ctx->h2, NGHTTP2_FLAG_NONE, stream_id, NGHTTP2_NO_ERROR);
+	ret = nghttp2_session_send(ctx->h2);
+	return ret;
+}
+
+
+/*
  * Send HTTP/2 stream data created from packet's wire buffer.
  *
  * If this function returns an error, the on_write() callback isn't (and
@@ -761,7 +919,7 @@ static int http_write_pkt(struct http_ct
 	prov.source.ptr = data;
 	prov.read_callback = read_callback;
 
-	return http_send_response(ctx, stream_id, &prov);
+	return http_send_response(ctx, stream_id, &prov, HTTP_STATUS_OK);
 }
 
 /*
@@ -811,8 +969,6 @@ void http_free(struct http_ctx *ctx)
 	while (queue_len(ctx->streams) > 0) {
 		struct http_stream stream = queue_head(ctx->streams);
 		http_free_headers(stream.headers);
-		if (stream.headers == ctx->headers)
-			ctx->headers = NULL;  // to prevent double-free
 		queue_pop(ctx->streams);
 	}
 
diff -pruN 5.4.4-1/daemon/http.h 5.5.1-5/daemon/http.h
--- 5.4.4-1/daemon/http.h	2022-01-05 13:19:14.427727200 +0000
+++ 5.5.1-5/daemon/http.h	2022-06-14 07:17:30.379490400 +0000
@@ -16,6 +16,7 @@
 #endif
 
 #include "lib/generic/queue.h"
+#include "lib/generic/trie.h"
 
 /** Transport session (opaque). */
 struct session;
@@ -35,8 +36,23 @@ typedef enum {
 	HTTP_METHOD_NONE = 0,
 	HTTP_METHOD_GET = 1,
 	HTTP_METHOD_POST = 2,
+	HTTP_METHOD_HEAD = 3, /**< Same as GET, except it does not return payload.
+			       * Required to be implemented by RFC 7231. */
 } http_method_t;
 
+/** HTTP status codes returned by kresd.
+ * This is obviously non-exhaustive of all HTTP status codes, feel free to add
+ * more if needed. */
+enum http_status {
+	HTTP_STATUS_OK                              = 200,
+	HTTP_STATUS_BAD_REQUEST                     = 400,
+	HTTP_STATUS_NOT_FOUND                       = 404,
+	HTTP_STATUS_PAYLOAD_TOO_LARGE               = 413,
+	HTTP_STATUS_UNSUPPORTED_MEDIA_TYPE          = 415,
+	HTTP_STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE = 431,
+	HTTP_STATUS_NOT_IMPLEMENTED                 = 501,
+};
+
 struct http_ctx {
 	struct nghttp2_session *h2;
 	http_send_callback send_cb;
@@ -44,6 +60,8 @@ struct http_ctx {
 	queue_http_stream streams;  /* Streams present in the wire buffer. */
 	trie_t *stream_write_data;  /* Dictionary of stream data that needs to be freed after write. */
 	int32_t incomplete_stream;
+	int32_t last_stream;   /* The last used stream - mostly the same as incomplete_stream, but can be used after
+				  completion for sending HTTP status codes. */
 	ssize_t submitted;
 	http_method_t current_method;
 	char *uri_path;
@@ -51,11 +69,15 @@ struct http_ctx {
 	uint8_t *buf;  /* Part of the wire_buf that belongs to current HTTP/2 stream. */
 	ssize_t buf_pos;
 	ssize_t buf_size;
+	enum http_status status;
+	bool streaming;             /* True: not all data in the stream has been received yet. */
 };
 
 #if ENABLE_DOH2
 struct http_ctx* http_new(struct session *session, http_send_callback send_cb);
-ssize_t http_process_input_data(struct session *session, const uint8_t *buf, ssize_t nread);
+int http_process_input_data(struct session *session, const uint8_t *buf, ssize_t nread,
+			    ssize_t *out_submitted);
+int http_send_status(struct session *session, enum http_status status);
 int http_write(uv_write_t *req, uv_handle_t *handle, knot_pkt_t* pkt, int32_t stream_id,
 	       uv_write_cb on_write);
 void http_free(struct http_ctx *ctx);
diff -pruN 5.4.4-1/daemon/io.c 5.5.1-5/daemon/io.c
--- 5.4.4-1/daemon/io.c	2022-01-05 13:19:14.431727400 +0000
+++ 5.5.1-5/daemon/io.c	2022-06-14 07:17:30.379490400 +0000
@@ -17,6 +17,7 @@
 #endif
 
 #include "daemon/network.h"
+#include "daemon/proxyv2.h"
 #include "daemon/worker.h"
 #include "daemon/tls.h"
 #include "daemon/http.h"
@@ -68,26 +69,79 @@ static void handle_getbuf(uv_handle_t* h
 }
 
 void udp_recv(uv_udp_t *handle, ssize_t nread, const uv_buf_t *buf,
-	const struct sockaddr *addr, unsigned flags)
+	const struct sockaddr *comm_addr, unsigned flags)
 {
 	struct session *s = handle->data;
-	if (session_flags(s)->closing || nread <= 0 || addr->sa_family == AF_UNSPEC)
+	if (session_flags(s)->closing || nread <= 0 || comm_addr->sa_family == AF_UNSPEC)
 		return;
 
 	if (session_flags(s)->outgoing) {
 		const struct sockaddr *peer = session_get_peer(s);
 		if (kr_fails_assert(peer->sa_family != AF_UNSPEC))
 			return;
-		if (kr_sockaddr_cmp(peer, addr) != 0) {
+		if (kr_sockaddr_cmp(peer, comm_addr) != 0) {
 			kr_log_debug(IO, "<= ignoring UDP from unexpected address '%s'\n",
-					kr_straddr(addr));
+					kr_straddr(comm_addr));
 			return;
 		}
 	}
-	ssize_t consumed = session_wirebuf_consume(s, (const uint8_t *)buf->base,
-						   nread);
-	kr_assert(consumed == nread);
-	session_wirebuf_process(s, addr);
+
+	const uint8_t *data = (const uint8_t *)buf->base;
+	ssize_t data_len = nread;
+	const struct sockaddr *src_addr = comm_addr;
+	const struct sockaddr *dst_addr = NULL;
+	struct proxy_result proxy;
+	bool has_proxy = false;
+	if (!session_flags(s)->outgoing && proxy_header_present(data, data_len)) {
+		if (!proxy_allowed(&the_worker->engine->net, comm_addr)) {
+			kr_log_debug(IO, "<= ignoring PROXYv2 UDP from disallowed address '%s'\n",
+					kr_straddr(comm_addr));
+			return;
+		}
+
+		ssize_t trimmed = proxy_process_header(&proxy, s, data, data_len);
+		if (trimmed == KNOT_EMALF) {
+			if (kr_log_is_debug(IO, NULL)) {
+				kr_log_debug(IO, "<= ignoring malformed PROXYv2 UDP "
+						"from address '%s'\n",
+						kr_straddr(comm_addr));
+			}
+			return;
+		} else if (trimmed < 0) {
+			if (kr_log_is_debug(IO, NULL)) {
+				kr_log_debug(IO, "<= error processing PROXYv2 UDP "
+						"from address '%s', ignoring\n",
+						kr_straddr(comm_addr));
+			}
+			return;
+		}
+
+		if (proxy.command == PROXY2_CMD_PROXY && proxy.family != AF_UNSPEC) {
+			has_proxy = true;
+			src_addr = &proxy.src_addr.ip;
+			dst_addr = &proxy.dst_addr.ip;
+
+			if (kr_log_is_debug(IO, NULL)) {
+				kr_log_debug(IO, "<= UDP query from '%s'\n",
+						kr_straddr(src_addr));
+				kr_log_debug(IO, "<= proxied through '%s'\n",
+						kr_straddr(comm_addr));
+			}
+		}
+		data = session_wirebuf_get_free_start(s);
+		data_len = nread - trimmed;
+	}
+
+	ssize_t consumed = session_wirebuf_consume(s, data, data_len);
+	kr_assert(consumed == data_len);
+
+	struct io_comm_data comm = {
+		.src_addr = src_addr,
+		.comm_addr = comm_addr,
+		.dst_addr = dst_addr,
+		.proxy = (has_proxy) ? &proxy : NULL
+	};
+	session_wirebuf_process(s, &comm);
 	session_wirebuf_discard(s);
 	mp_flush(the_worker->pkt_pool.ctx);
 }
@@ -129,29 +183,42 @@ int io_bind(const struct sockaddr *addr,
 
 	int yes = 1;
 	if (addr->sa_family == AF_INET || addr->sa_family == AF_INET6) {
-		if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)))
+		if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes))) {
+			close(fd);
 			return kr_error(errno);
+		}
 
 #ifdef SO_REUSEPORT_LB
-		if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT_LB, &yes, sizeof(yes)))
+		if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT_LB, &yes, sizeof(yes))) {
+			close(fd);
 			return kr_error(errno);
+		}
 #elif defined(SO_REUSEPORT) && defined(__linux__) /* different meaning on (Free)BSD */
-		if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes)))
+		if (setsockopt(fd, SOL_SOCKET, SO_REUSEPORT, &yes, sizeof(yes))) {
+			close(fd);
 			return kr_error(errno);
+		}
 #endif
 
 #ifdef IPV6_V6ONLY
 		if (addr->sa_family == AF_INET6
-		    && setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes)))
+		    && setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &yes, sizeof(yes))) {
+			close(fd);
 			return kr_error(errno);
+		}
 #endif
 		if (flags != NULL && flags->freebind) {
 			int optlevel;
 			int optname;
 			int ret = family_to_freebind_option(addr->sa_family, &optlevel, &optname);
-			if (ret) return kr_error(ret);
-			if (setsockopt(fd, optlevel, optname, &yes, sizeof(yes)))
+			if (ret) {
+				close(fd);
+				return kr_error(ret);
+			}
+			if (setsockopt(fd, optlevel, optname, &yes, sizeof(yes))) {
+				close(fd);
 				return kr_error(errno);
+			}
 		}
 
 		/* Linux 3.15 has IP_PMTUDISC_OMIT which makes sockets
@@ -172,8 +239,10 @@ int io_bind(const struct sockaddr *addr,
 #endif
 	}
 
-	if (bind(fd, addr, kr_sockaddr_len(addr)))
+	if (bind(fd, addr, kr_sockaddr_len(addr))) {
+		close(fd);
 		return kr_error(errno);
+	}
 
 	return fd;
 }
@@ -196,7 +265,7 @@ int io_listen_udp(uv_loop_t *loop, uv_ud
 	kr_require(s);
 	session_flags(s)->outgoing = false;
 
-	int socklen = sizeof(union inaddr);
+	int socklen = sizeof(union kr_sockaddr);
 	ret = uv_udp_getsockname(handle, session_get_sockname(s), &socklen);
 	if (ret) {
 		kr_log_error(IO, "ERROR: getsockname failed: %s\n", uv_strerror(ret));
@@ -292,17 +361,68 @@ static void tcp_recv(uv_stream_t *handle
 		return;
 	}
 
-	ssize_t consumed = 0;
 	const uint8_t *data = (const uint8_t *)buf->base;
 	ssize_t data_len = nread;
+	const struct sockaddr *src_addr = session_get_peer(s);
+	const struct sockaddr *dst_addr = NULL;
+	if (!session_flags(s)->outgoing && !session_flags(s)->no_proxy &&
+			proxy_header_present(data, data_len)) {
+		if (!proxy_allowed(&the_worker->engine->net, src_addr)) {
+			if (kr_log_is_debug(IO, NULL)) {
+				kr_log_debug(IO, "<= connection to '%s': PROXYv2 not allowed "
+						"for this peer, close\n",
+						kr_straddr(src_addr));
+			}
+			worker_end_tcp(s);
+			return;
+		}
+
+		struct proxy_result *proxy = session_proxy_create(s);
+		ssize_t trimmed = proxy_process_header(proxy, s, data, data_len);
+		if (trimmed < 0) {
+			if (kr_log_is_debug(IO, NULL)) {
+				if (trimmed == KNOT_EMALF) {
+					kr_log_debug(IO, "<= connection to '%s': "
+							"malformed PROXYv2 header, close\n",
+							kr_straddr(src_addr));
+				} else {
+					kr_log_debug(IO, "<= connection to '%s': "
+							"error processing PROXYv2 header, close\n",
+							kr_straddr(src_addr));
+				}
+			}
+			worker_end_tcp(s);
+			return;
+		} else if (trimmed == 0) {
+			return;
+		}
+
+		if (proxy->command != PROXY2_CMD_LOCAL && proxy->family != AF_UNSPEC) {
+			src_addr = &proxy->src_addr.ip;
+			dst_addr = &proxy->dst_addr.ip;
+
+			if (kr_log_is_debug(IO, NULL)) {
+				kr_log_debug(IO, "<= TCP stream from '%s'\n",
+						kr_straddr(src_addr));
+				kr_log_debug(IO, "<= proxied through '%s'\n",
+						kr_straddr(session_get_peer(s)));
+			}
+		}
+
+		data = session_wirebuf_get_free_start(s);
+		data_len = nread - trimmed;
+	}
+
+	session_flags(s)->no_proxy = true;
+
+	ssize_t consumed = 0;
 	if (session_flags(s)->has_tls) {
 		/* buf->base points to start of the tls receive buffer.
 		   Decode data free space in session wire buffer. */
-		consumed = tls_process_input_data(s, (const uint8_t *)buf->base, nread);
+		consumed = tls_process_input_data(s, data, data_len);
 		if (consumed < 0) {
 			if (kr_log_is_debug(IO, NULL)) {
-				struct sockaddr *peer = session_get_peer(s);
-				char *peer_str = kr_straddr(peer);
+				char *peer_str = kr_straddr(src_addr);
 				kr_log_debug(IO, "=> connection to '%s': "
 					       "error processing TLS data, close\n",
 					       peer_str ? peer_str : "");
@@ -316,19 +436,21 @@ static void tcp_recv(uv_stream_t *handle
 		data_len = consumed;
 	}
 #if ENABLE_DOH2
+	int streaming = 1;
 	if (session_flags(s)->has_http) {
-		consumed = http_process_input_data(s, data, data_len);
-		if (consumed < 0) {
+		streaming = http_process_input_data(s, data, data_len,
+				&consumed);
+		if (streaming < 0) {
 			if (kr_log_is_debug(IO, NULL)) {
-				struct sockaddr *peer = session_get_peer(s);
-				char *peer_str = kr_straddr(peer);
+				char *peer_str = kr_straddr(src_addr);
 				kr_log_debug(IO, "=> connection to '%s': "
 				       "error processing HTTP data, close\n",
 				       peer_str ? peer_str : "");
 			}
 			worker_end_tcp(s);
 			return;
-		} else if (consumed == 0) {
+		}
+		if (consumed == 0) {
 			return;
 		}
 		data = session_wirebuf_get_free_start(s);
@@ -341,13 +463,28 @@ static void tcp_recv(uv_stream_t *handle
 	consumed = session_wirebuf_consume(s, data, data_len);
 	kr_assert(consumed == data_len);
 
-	int ret = session_wirebuf_process(s, session_get_peer(s));
+	struct io_comm_data comm = {
+		.src_addr = src_addr,
+		.comm_addr = session_get_peer(s),
+		.dst_addr = dst_addr,
+		.proxy = session_proxy_get(s)
+	};
+	int ret = session_wirebuf_process(s, &comm);
 	if (ret < 0) {
 		/* An error has occurred, close the session. */
 		worker_end_tcp(s);
 	}
 	session_wirebuf_compress(s);
 	mp_flush(the_worker->pkt_pool.ctx);
+#if ENABLE_DOH2
+	if (session_flags(s)->has_http && streaming == 0 && ret == 0) {
+		ret = http_send_status(s, HTTP_STATUS_BAD_REQUEST);
+		if (ret) {
+			/* An error has occurred, close the session. */
+			worker_end_tcp(s);
+		}
+	}
+#endif
 }
 
 #if ENABLE_DOH2
@@ -611,11 +748,11 @@ void io_tty_process_input(uv_stream_t *s
 	char *cmd, *cmd_next = NULL;
 	bool incomplete_cmd = false;
 
-	if (!(stream && commands && nread > 0)) {
+	if (!commands || nread <= 0) {
 		goto finish;
 	}
-	/* Execute */
 
+	/* Execute */
 	if (commands[nread - 1] != '\n') {
 		incomplete_cmd = true;
 	}
@@ -749,24 +886,30 @@ struct io_stream_data *io_tty_alloc_data
 
 void io_tty_accept(uv_stream_t *master, int status)
 {
-	struct io_stream_data *data = io_tty_alloc_data();
 	/* We can't use any allocations after mp_start() and it's easier anyway. */
 	uv_pipe_t *client = malloc(sizeof(*client));
+	if (!client)
+		return;
+
+	struct io_stream_data *data = io_tty_alloc_data();
+	if (!data) {
+		free(client);
+		return;
+	}
 	client->data = data;
 
 	struct args *args = the_args;
-	if (client && client->data) {
-		 uv_pipe_init(master->loop, client, 0);
-		 if (uv_accept(master, (uv_stream_t *)client) != 0) {
-			mp_delete(data->pool->ctx);
-			return;
-		 }
-		 uv_read_start((uv_stream_t *)client, io_tty_alloc, io_tty_process_input);
-		 /* Write command line */
-		 if (!args->quiet) {
-			uv_buf_t buf = { "> ", 2 };
-			uv_try_write((uv_stream_t *)client, &buf, 1);
-		 }
+	uv_pipe_init(master->loop, client, 0);
+	if (uv_accept(master, (uv_stream_t *)client) != 0) {
+		mp_delete(data->pool->ctx);
+		return;
+	}
+	uv_read_start((uv_stream_t *)client, io_tty_alloc, io_tty_process_input);
+
+	/* Write command line */
+	if (!args->quiet) {
+		uv_buf_t buf = { "> ", 2 };
+		uv_try_write((uv_stream_t *)client, &buf, 1);
 	}
 }
 
@@ -827,9 +970,12 @@ static void xdp_rx(uv_poll_t* handle, in
 		if (kpkt == NULL) {
 			ret = kr_error(ENOMEM);
 		} else {
-			ret = worker_submit(xhd->session,
-					(const struct sockaddr *)&msg->ip_from,
-					(const struct sockaddr *)&msg->ip_to,
+			struct io_comm_data comm = {
+				.src_addr = (const struct sockaddr *)&msg->ip_from,
+				.comm_addr = (const struct sockaddr *)&msg->ip_from,
+				.dst_addr = (const struct sockaddr *)&msg->ip_to
+			};
+			ret = worker_submit(xhd->session, &comm,
 					msg->eth_from, msg->eth_to, kpkt);
 		}
 		if (ret)
diff -pruN 5.4.4-1/daemon/io.h 5.5.1-5/daemon/io.h
--- 5.4.4-1/daemon/io.h	2022-01-05 13:19:14.431727400 +0000
+++ 5.5.1-5/daemon/io.h	2022-06-14 07:17:30.379490400 +0000
@@ -16,6 +16,29 @@ struct tls_ctx;
 struct tls_client_ctx;
 struct io_stream_data;
 
+/** Communication data. */
+struct io_comm_data {
+	/** The original address the data came from. May be that of a proxied
+	 * client, if they came through a proxy. May be `NULL` if
+	 * the communication did not come from network. */
+	const struct sockaddr *src_addr;
+
+	/** The actual address the resolver is communicating with. May be
+	 * the address of a proxy if the communication came through one,
+	 * otherwise it will be the same as `src_addr`. May be `NULL` if
+	 * the communication did not come from network. */
+	const struct sockaddr *comm_addr;
+
+	/** The original destination address. May be the resolver's address, or
+	 * the address of a proxy if the communication came through one. May be
+	 * `NULL` if the communication did not come from network. */
+	const struct sockaddr *dst_addr;
+
+	/** Data parsed from a PROXY header. May be `NULL` if the communication
+	 * did not come through a proxy, or if the PROXYv2 protocol was not used. */
+	const struct proxy_result *proxy;
+};
+
 /** Bind address into a file-descriptor (only, no libuv).  type is e.g. SOCK_DGRAM */
 int io_bind(const struct sockaddr *addr, int type, const endpoint_flags_t *flags);
 /** Initialize a UDP handle and start listening. */
diff -pruN 5.4.4-1/daemon/lua/kluautil.lua 5.5.1-5/daemon/lua/kluautil.lua
--- 5.4.4-1/daemon/lua/kluautil.lua	2022-01-05 13:19:14.431727400 +0000
+++ 5.5.1-5/daemon/lua/kluautil.lua	2022-06-14 07:17:30.379490400 +0000
@@ -1,5 +1,6 @@
 -- SPDX-License-Identifier: GPL-3.0-or-later
 
+local ffi = require('ffi')
 local kluautil = {}
 
 -- Get length of table
@@ -79,6 +80,15 @@ function kluautil.kr_https_fetch(url, ou
 	return true
 end
 
+-- Copy a lua string to C (to knot_mm_t or nil=malloc, zero-terminated).
+function kluautil.kr_string2c(str, mempool)
+	if str == nil then return nil end
+	local result = ffi.C.mm_realloc(mempool, nil, #str + 1, 0)
+	if result == nil then panic("not enough memory") end
+	ffi.copy(result, str)
+	return ffi.cast('const char *', result)
+end
+
 kluautil.list_dir = kluautil_list_dir
 
 return kluautil
diff -pruN 5.4.4-1/daemon/lua/kres-gen-29.lua 5.5.1-5/daemon/lua/kres-gen-29.lua
--- 5.4.4-1/daemon/lua/kres-gen-29.lua	2022-01-05 13:19:14.431727400 +0000
+++ 5.5.1-5/daemon/lua/kres-gen-29.lua	1970-01-01 00:00:00.000000000 +0000
@@ -1,617 +0,0 @@
--- SPDX-License-Identifier: GPL-3.0-or-later
-
-local ffi = require('ffi')
---[[ This file is generated by ./kres-gen.sh ]] ffi.cdef[[
-typedef long time_t;
-typedef long __time_t;
-typedef long __suseconds_t;
-struct timeval {
-	__time_t tv_sec;
-	__suseconds_t tv_usec;
-};
-
-typedef struct knot_dump_style knot_dump_style_t;
-extern const knot_dump_style_t KR_DUMP_STYLE_DEFAULT;
-struct kr_cdb_api {};
-struct lru {};
-typedef enum {KNOT_ANSWER, KNOT_AUTHORITY, KNOT_ADDITIONAL} knot_section_t;
-typedef struct {
-	uint16_t pos;
-	uint16_t flags;
-	uint16_t compress_ptr[16];
-} knot_rrinfo_t;
-typedef unsigned char knot_dname_t;
-typedef struct {
-	uint16_t len;
-	uint8_t data[];
-} knot_rdata_t;
-typedef struct {
-	uint16_t count;
-	uint32_t size;
-	knot_rdata_t *rdata;
-} knot_rdataset_t;
-
-typedef struct knot_mm {
-	void *ctx, *alloc, *free;
-} knot_mm_t;
-
-typedef void *(*map_alloc_f)(void *, size_t);
-typedef void (*map_free_f)(void *baton, void *ptr);
-typedef void (*trace_log_f) (const struct kr_request *, const char *);
-typedef void (*trace_callback_f)(struct kr_request *);
-typedef uint8_t * (*alloc_wire_f)(struct kr_request *req, uint16_t *maxlen);
-typedef bool (*addr_info_f)(struct sockaddr*);
-typedef struct {
-	knot_dname_t *_owner;
-	uint32_t _ttl;
-	uint16_t type;
-	uint16_t rclass;
-	knot_rdataset_t rrs;
-	void *additional;
-} knot_rrset_t;
-
-struct kr_module;
-typedef char *(kr_prop_cb)(void *, struct kr_module *, const char *);
-typedef struct knot_pkt knot_pkt_t;
-typedef struct {
-	uint8_t *ptr[15];
-} knot_edns_options_t;
-typedef struct {
-	knot_pkt_t *pkt;
-	uint16_t pos;
-	uint16_t count;
-} knot_pktsection_t;
-typedef struct knot_compr {
-	uint8_t *wire;
-	knot_rrinfo_t *rrinfo;
-	struct {
-		uint16_t pos;
-		uint8_t labels;
-	} suffix;
-} knot_compr_t;
-struct knot_pkt {
-	uint8_t *wire;
-	size_t size;
-	size_t max_size;
-	size_t parsed;
-	uint16_t reserved;
-	uint16_t qname_size;
-	uint16_t rrset_count;
-	uint16_t flags;
-	knot_rrset_t *opt_rr;
-	knot_rrset_t *tsig_rr;
-	knot_edns_options_t *edns_opts;
-	struct {
-		uint8_t *pos;
-		size_t len;
-	} tsig_wire;
-	knot_section_t current;
-	knot_pktsection_t sections[3];
-	size_t rrset_allocd;
-	knot_rrinfo_t *rr_info;
-	knot_rrset_t *rr;
-	knot_mm_t mm;
-	knot_compr_t compr;
-};
-typedef struct {
-	void *root;
-	struct knot_mm *pool;
-} map_t;
-typedef struct trie trie_t;
-struct kr_qflags {
-	_Bool NO_MINIMIZE : 1;
-	_Bool NO_IPV6 : 1;
-	_Bool NO_IPV4 : 1;
-	_Bool TCP : 1;
-	_Bool RESOLVED : 1;
-	_Bool AWAIT_IPV4 : 1;
-	_Bool AWAIT_IPV6 : 1;
-	_Bool AWAIT_CUT : 1;
-	_Bool NO_EDNS : 1;
-	_Bool CACHED : 1;
-	_Bool NO_CACHE : 1;
-	_Bool EXPIRING : 1;
-	_Bool ALLOW_LOCAL : 1;
-	_Bool DNSSEC_WANT : 1;
-	_Bool DNSSEC_BOGUS : 1;
-	_Bool DNSSEC_INSECURE : 1;
-	_Bool DNSSEC_CD : 1;
-	_Bool STUB : 1;
-	_Bool ALWAYS_CUT : 1;
-	_Bool DNSSEC_WEXPAND : 1;
-	_Bool PERMISSIVE : 1;
-	_Bool STRICT : 1;
-	_Bool BADCOOKIE_AGAIN : 1;
-	_Bool CNAME : 1;
-	_Bool REORDER_RR : 1;
-	_Bool TRACE : 1;
-	_Bool NO_0X20 : 1;
-	_Bool DNSSEC_NODS : 1;
-	_Bool DNSSEC_OPTOUT : 1;
-	_Bool NONAUTH : 1;
-	_Bool FORWARD : 1;
-	_Bool DNS64_MARK : 1;
-	_Bool CACHE_TRIED : 1;
-	_Bool NO_NS_FOUND : 1;
-	_Bool PKT_IS_SANE : 1;
-	_Bool DNS64_DISABLE : 1;
-};
-typedef struct ranked_rr_array_entry {
-	uint32_t qry_uid;
-	uint8_t rank;
-	uint8_t revalidation_cnt;
-	_Bool cached : 1;
-	_Bool yielded : 1;
-	_Bool to_wire : 1;
-	_Bool expiring : 1;
-	_Bool in_progress : 1;
-	_Bool dont_cache : 1;
-	knot_rrset_t *rr;
-} ranked_rr_array_entry_t;
-typedef struct {
-	ranked_rr_array_entry_t **at;
-	size_t len;
-	size_t cap;
-} ranked_rr_array_t;
-typedef struct kr_http_header_array_entry {
-	char *name;
-	char *value;
-} kr_http_header_array_entry_t;
-typedef struct {
-	kr_http_header_array_entry_t *at;
-	size_t len;
-	size_t cap;
-} kr_http_header_array_t;
-typedef struct {
-	union inaddr *at;
-	size_t len;
-	size_t cap;
-} inaddr_array_t;
-struct kr_zonecut {
-	knot_dname_t *name;
-	knot_rrset_t *key;
-	knot_rrset_t *trust_anchor;
-	struct kr_zonecut *parent;
-	trie_t *nsset;
-	knot_mm_t *pool;
-};
-typedef struct {
-	struct kr_query **at;
-	size_t len;
-	size_t cap;
-} kr_qarray_t;
-struct kr_rplan {
-	kr_qarray_t pending;
-	kr_qarray_t resolved;
-	struct kr_query *initial;
-	struct kr_request *request;
-	knot_mm_t *pool;
-	uint32_t next_uid;
-};
-struct kr_request_qsource_flags {
-	_Bool tcp : 1;
-	_Bool tls : 1;
-	_Bool http : 1;
-	_Bool xdp : 1;
-};
-struct kr_request {
-	struct kr_context *ctx;
-	knot_pkt_t *answer;
-	struct kr_query *current_query;
-	struct {
-		const struct sockaddr *addr;
-		const struct sockaddr *dst_addr;
-		const knot_pkt_t *packet;
-		struct kr_request_qsource_flags flags;
-		size_t size;
-		int32_t stream_id;
-		kr_http_header_array_t headers;
-	} qsource;
-	struct {
-		unsigned int rtt;
-		const struct kr_transport *transport;
-	} upstream;
-	struct kr_qflags options;
-	int state;
-	ranked_rr_array_t answ_selected;
-	ranked_rr_array_t auth_selected;
-	ranked_rr_array_t add_selected;
-	_Bool answ_validated;
-	_Bool auth_validated;
-	uint8_t rank;
-	struct kr_rplan rplan;
-	trace_log_f trace_log;
-	trace_callback_f trace_finish;
-	int vars_ref;
-	knot_mm_t pool;
-	unsigned int uid;
-	struct {
-		addr_info_f is_tls_capable;
-		addr_info_f is_tcp_connected;
-		addr_info_f is_tcp_waiting;
-		inaddr_array_t forwarding_targets;
-	} selection_context;
-	unsigned int count_no_nsaddr;
-	unsigned int count_fail_row;
-	alloc_wire_f alloc_wire_cb;
-};
-enum kr_rank {KR_RANK_INITIAL, KR_RANK_OMIT, KR_RANK_TRY, KR_RANK_INDET = 4, KR_RANK_BOGUS, KR_RANK_MISMATCH, KR_RANK_MISSING, KR_RANK_INSECURE, KR_RANK_AUTH = 16, KR_RANK_SECURE = 32};
-typedef struct kr_cdb * kr_cdb_pt;
-struct kr_cdb_stats {
-	uint64_t open;
-	uint64_t close;
-	uint64_t count;
-	uint64_t count_entries;
-	uint64_t clear;
-	uint64_t commit;
-	uint64_t read;
-	uint64_t read_miss;
-	uint64_t write;
-	uint64_t remove;
-	uint64_t remove_miss;
-	uint64_t match;
-	uint64_t match_miss;
-	uint64_t read_leq;
-	uint64_t read_leq_miss;
-	double usage_percent;
-};
-typedef struct uv_timer_s uv_timer_t;
-struct kr_cache {
-	kr_cdb_pt db;
-	const struct kr_cdb_api *api;
-	struct kr_cdb_stats stats;
-	uint32_t ttl_min;
-	uint32_t ttl_max;
-	struct timeval checkpoint_walltime;
-	uint64_t checkpoint_monotime;
-	uv_timer_t *health_timer;
-};
-typedef struct kr_layer {
-	int state;
-	struct kr_request *req;
-	const struct kr_layer_api *api;
-	knot_pkt_t *pkt;
-	struct sockaddr *dst;
-	_Bool is_stream;
-} kr_layer_t;
-typedef struct kr_layer_api {
-	int (*begin)(kr_layer_t *);
-	int (*reset)(kr_layer_t *);
-	int (*finish)(kr_layer_t *);
-	int (*consume)(kr_layer_t *, knot_pkt_t *);
-	int (*produce)(kr_layer_t *, knot_pkt_t *);
-	int (*checkout)(kr_layer_t *, knot_pkt_t *, struct sockaddr *, int);
-	int (*answer_finalize)(kr_layer_t *);
-	void *data;
-	int cb_slots[];
-} kr_layer_api_t;
-struct kr_prop {
-	kr_prop_cb *cb;
-	const char *name;
-	const char *info;
-};
-struct kr_module {
-	char *name;
-	int (*init)(struct kr_module *);
-	int (*deinit)(struct kr_module *);
-	int (*config)(struct kr_module *, const char *);
-	const kr_layer_api_t *layer;
-	const struct kr_prop *props;
-	void *lib;
-	void *data;
-};
-struct kr_server_selection {
-	_Bool initialized;
-	void (*choose_transport)(struct kr_query *, struct kr_transport **);
-	void (*update_rtt)(struct kr_query *, const struct kr_transport *, unsigned int);
-	void (*error)(struct kr_query *, const struct kr_transport *, enum kr_selection_error);
-	struct local_state *local_state;
-};
-typedef int kr_log_level_t;
-enum kr_log_group {LOG_GRP_UNKNOWN = -1, LOG_GRP_SYSTEM = 1, LOG_GRP_CACHE, LOG_GRP_IO, LOG_GRP_NETWORK, LOG_GRP_TA, LOG_GRP_TLS, LOG_GRP_GNUTLS, LOG_GRP_TLSCLIENT, LOG_GRP_XDP, LOG_GRP_ZIMPORT, LOG_GRP_ZSCANNER, LOG_GRP_DOH, LOG_GRP_DNSSEC, LOG_GRP_HINT, LOG_GRP_PLAN, LOG_GRP_ITERATOR, LOG_GRP_VALIDATOR, LOG_GRP_RESOLVER, LOG_GRP_SELECTION, LOG_GRP_ZCUT, LOG_GRP_COOKIES, LOG_GRP_STATISTICS, LOG_GRP_REBIND, LOG_GRP_WORKER, LOG_GRP_POLICY, LOG_GRP_TASENTINEL, LOG_GRP_TASIGNALING, LOG_GRP_TAUPDATE, LOG_GRP_DAF, LOG_GRP_DETECTTIMEJUMP, LOG_GRP_DETECTTIMESKEW, LOG_GRP_GRAPHITE, LOG_GRP_PREFILL, LOG_GRP_PRIMING, LOG_GRP_SRVSTALE, LOG_GRP_WATCHDOG, LOG_GRP_NSID, LOG_GRP_DNSTAP, LOG_GRP_TESTS, LOG_GRP_DOTAUTH, LOG_GRP_HTTP, LOG_GRP_CONTROL, LOG_GRP_MODULE, LOG_GRP_DEVEL, LOG_GRP_RENUMBER, LOG_GRP_REQDBG};
-
-kr_layer_t kr_layer_t_static;
-_Bool kr_dbg_assertion_abort;
-int kr_dbg_assertion_fork;
-
-typedef int32_t (*kr_stale_cb)(int32_t ttl, const knot_dname_t *owner, uint16_t type,
-				const struct kr_query *qry);
-
-void kr_rrset_init(knot_rrset_t *rrset, knot_dname_t *owner,
-			uint16_t type, uint16_t rclass, uint32_t ttl);
-struct kr_query {
-	struct kr_query *parent;
-	knot_dname_t *sname;
-	uint16_t stype;
-	uint16_t sclass;
-	uint16_t id;
-	uint16_t reorder;
-	struct kr_qflags flags;
-	struct kr_qflags forward_flags;
-	uint32_t secret;
-	uint32_t uid;
-	uint64_t creation_time_mono;
-	uint64_t timestamp_mono;
-	struct timeval timestamp;
-	struct kr_zonecut zone_cut;
-	struct kr_layer_pickle *deferred;
-	int8_t cname_depth;
-	struct kr_query *cname_parent;
-	struct kr_request *request;
-	kr_stale_cb stale_cb;
-	struct kr_server_selection server_selection;
-};
-struct kr_context {
-	struct kr_qflags options;
-	knot_rrset_t *downstream_opt_rr;
-	knot_rrset_t *upstream_opt_rr;
-	map_t trust_anchors;
-	map_t negative_anchors;
-	struct kr_zonecut root_hints;
-	struct kr_cache cache;
-	unsigned int cache_rtt_tout_retry_interval;
-	char _stub[];
-};
-struct kr_transport {
-	knot_dname_t *ns_name;
-	/* beware: hidden stub, to avoid hardcoding sockaddr lengths */
-};
-const char *knot_strerror(int);
-knot_dname_t *knot_dname_copy(const knot_dname_t *, knot_mm_t *);
-knot_dname_t *knot_dname_from_str(uint8_t *, const char *, size_t);
-int knot_dname_in_bailiwick(const knot_dname_t *, const knot_dname_t *);
-_Bool knot_dname_is_equal(const knot_dname_t *, const knot_dname_t *);
-size_t knot_dname_labels(const uint8_t *, const uint8_t *);
-size_t knot_dname_size(const knot_dname_t *);
-void knot_dname_to_lower(knot_dname_t *);
-char *knot_dname_to_str(char *, const knot_dname_t *, size_t);
-knot_rdata_t *knot_rdataset_at(const knot_rdataset_t *, uint16_t);
-int knot_rdataset_merge(knot_rdataset_t *, const knot_rdataset_t *, knot_mm_t *);
-int knot_rrset_add_rdata(knot_rrset_t *, const uint8_t *, uint16_t, knot_mm_t *);
-void knot_rrset_free(knot_rrset_t *, knot_mm_t *);
-int knot_rrset_txt_dump(const knot_rrset_t *, char **, size_t *, const knot_dump_style_t *);
-int knot_rrset_txt_dump_data(const knot_rrset_t *, const size_t, char *, const size_t, const knot_dump_style_t *);
-size_t knot_rrset_size(const knot_rrset_t *);
-int knot_pkt_begin(knot_pkt_t *, knot_section_t);
-int knot_pkt_put_question(knot_pkt_t *, const knot_dname_t *, uint16_t, uint16_t);
-int knot_pkt_put_rotate(knot_pkt_t *, uint16_t, const knot_rrset_t *, uint16_t, uint16_t);
-knot_pkt_t *knot_pkt_new(void *, uint16_t, knot_mm_t *);
-void knot_pkt_free(knot_pkt_t *);
-int knot_pkt_parse(knot_pkt_t *, unsigned int);
-knot_rrset_t *kr_request_ensure_edns(struct kr_request *);
-knot_pkt_t *kr_request_ensure_answer(struct kr_request *);
-struct kr_rplan *kr_resolve_plan(struct kr_request *);
-knot_mm_t *kr_resolve_pool(struct kr_request *);
-struct kr_query *kr_rplan_push(struct kr_rplan *, struct kr_query *, const knot_dname_t *, uint16_t, uint16_t);
-int kr_rplan_pop(struct kr_rplan *, struct kr_query *);
-struct kr_query *kr_rplan_resolved(struct kr_rplan *);
-struct kr_query *kr_rplan_last(struct kr_rplan *);
-int kr_forward_add_target(struct kr_request *, const struct sockaddr *);
-void kr_log_req1(const struct kr_request * const, uint32_t, const unsigned int, enum kr_log_group, const char *, const char *, ...);
-void kr_log_q1(const struct kr_query * const, enum kr_log_group, const char *, const char *, ...);
-const char *kr_log_grp2name(enum kr_log_group);
-void kr_log_fmt(enum kr_log_group, kr_log_level_t, const char *, const char *, const char *, const char *, ...);
-int kr_make_query(struct kr_query *, knot_pkt_t *);
-void kr_pkt_make_auth_header(knot_pkt_t *);
-int kr_pkt_put(knot_pkt_t *, const knot_dname_t *, uint32_t, uint16_t, uint16_t, const uint8_t *, uint16_t);
-int kr_pkt_recycle(knot_pkt_t *);
-int kr_pkt_clear_payload(knot_pkt_t *);
-uint16_t kr_pkt_has_dnssec(const knot_pkt_t *);
-uint16_t kr_pkt_qclass(const knot_pkt_t *);
-uint16_t kr_pkt_qtype(const knot_pkt_t *);
-char *kr_pkt_text(const knot_pkt_t *);
-void kr_rnd_buffered(void *, unsigned int);
-uint32_t kr_rrsig_sig_inception(const knot_rdata_t *);
-uint32_t kr_rrsig_sig_expiration(const knot_rdata_t *);
-uint16_t kr_rrsig_type_covered(const knot_rdata_t *);
-const char *kr_inaddr(const struct sockaddr *);
-int kr_inaddr_family(const struct sockaddr *);
-int kr_inaddr_len(const struct sockaddr *);
-int kr_inaddr_str(const struct sockaddr *, char *, size_t *);
-int kr_sockaddr_cmp(const struct sockaddr *, const struct sockaddr *);
-int kr_sockaddr_len(const struct sockaddr *);
-uint16_t kr_inaddr_port(const struct sockaddr *);
-int kr_straddr_family(const char *);
-int kr_straddr_subnet(void *, const char *);
-int kr_bitcmp(const char *, const char *, int);
-int kr_family_len(int);
-struct sockaddr *kr_straddr_socket(const char *, int, knot_mm_t *);
-int kr_straddr_split(const char *, char * restrict, uint16_t *);
-_Bool kr_rank_test(uint8_t, uint8_t);
-int kr_ranked_rrarray_add(ranked_rr_array_t *, const knot_rrset_t *, uint8_t, _Bool, uint32_t, knot_mm_t *);
-int kr_ranked_rrarray_finalize(ranked_rr_array_t *, uint32_t, knot_mm_t *);
-void kr_qflags_set(struct kr_qflags *, struct kr_qflags);
-void kr_qflags_clear(struct kr_qflags *, struct kr_qflags);
-int kr_zonecut_add(struct kr_zonecut *, const knot_dname_t *, const void *, int);
-_Bool kr_zonecut_is_empty(struct kr_zonecut *);
-void kr_zonecut_set(struct kr_zonecut *, const knot_dname_t *);
-uint64_t kr_now();
-const char *kr_strptime_diff(const char *, const char *, const char *, double *);
-time_t kr_file_mtime(const char *);
-long long kr_fssize(const char *);
-const char *kr_dirent_name(const struct dirent *);
-void lru_free_items_impl(struct lru *);
-struct lru *lru_create_impl(unsigned int, unsigned int, knot_mm_t *, knot_mm_t *);
-void *lru_get_impl(struct lru *, const char *, unsigned int, unsigned int, _Bool, _Bool *);
-void *mm_realloc(knot_mm_t *, void *, size_t, size_t);
-knot_rrset_t *kr_ta_get(map_t *, const knot_dname_t *);
-int kr_ta_add(map_t *, const knot_dname_t *, uint16_t, uint32_t, const uint8_t *, uint16_t);
-int kr_ta_del(map_t *, const knot_dname_t *);
-void kr_ta_clear(map_t *);
-_Bool kr_dnssec_key_ksk(const uint8_t *);
-_Bool kr_dnssec_key_revoked(const uint8_t *);
-int kr_dnssec_key_tag(uint16_t, const uint8_t *, size_t);
-int kr_dnssec_key_match(const uint8_t *, size_t, const uint8_t *, size_t);
-int kr_cache_closest_apex(struct kr_cache *, const knot_dname_t *, _Bool, knot_dname_t **);
-int kr_cache_insert_rr(struct kr_cache *, const knot_rrset_t *, const knot_rrset_t *, uint8_t, uint32_t, _Bool);
-int kr_cache_remove(struct kr_cache *, const knot_dname_t *, uint16_t);
-int kr_cache_remove_subtree(struct kr_cache *, const knot_dname_t *, _Bool, int);
-int kr_cache_commit(struct kr_cache *);
-uint32_t packet_ttl(const knot_pkt_t *, _Bool);
-typedef struct {
-	int sock_type;
-	_Bool tls;
-	_Bool http;
-	_Bool xdp;
-	_Bool freebind;
-	const char *kind;
-} endpoint_flags_t;
-typedef struct {
-	char **at;
-	size_t len;
-	size_t cap;
-} addr_array_t;
-typedef struct {
-	int fd;
-	endpoint_flags_t flags;
-} flagged_fd_t;
-typedef struct {
-	flagged_fd_t *at;
-	size_t len;
-	size_t cap;
-} flagged_fd_array_t;
-typedef struct {
-	const char **at;
-	size_t len;
-	size_t cap;
-} config_array_t;
-struct args {
-	addr_array_t addrs;
-	addr_array_t addrs_tls;
-	flagged_fd_array_t fds;
-	int control_fd;
-	int forks;
-	config_array_t config;
-	const char *rundir;
-	_Bool interactive;
-	_Bool quiet;
-	_Bool tty_binary_output;
-};
-struct args *the_args;
-struct endpoint {
-	void *handle;
-	int fd;
-	int family;
-	uint16_t port;
-	int16_t nic_queue;
-	_Bool engaged;
-	endpoint_flags_t flags;
-};
-struct request_ctx {
-	struct kr_request req;
-	struct worker_ctx *worker;
-	struct qr_task *task;
-	/* beware: hidden stub, to avoid hardcoding sockaddr lengths */
-};
-struct qr_task {
-	struct request_ctx *ctx;
-	/* beware: hidden stub, to avoid qr_tasklist_t */
-};
-int worker_resolve_exec(struct qr_task *, knot_pkt_t *);
-knot_pkt_t *worker_resolve_mk_pkt(const char *, uint16_t, uint16_t, const struct kr_qflags *);
-struct qr_task *worker_resolve_start(knot_pkt_t *, struct kr_qflags);
-struct engine {
-	struct kr_context resolver;
-	char _stub[];
-};
-struct worker_ctx {
-	struct engine *engine;
-	char _stub[];
-};
-struct worker_ctx *the_worker;
-typedef struct {
-	uint8_t bitmap[32];
-	uint8_t length;
-} zs_win_t;
-typedef struct {
-	uint8_t excl_flag;
-	uint16_t addr_family;
-	uint8_t prefix_length;
-} zs_apl_t;
-typedef struct {
-	uint32_t d1;
-	uint32_t d2;
-	uint32_t m1;
-	uint32_t m2;
-	uint32_t s1;
-	uint32_t s2;
-	uint32_t alt;
-	uint64_t siz;
-	uint64_t hp;
-	uint64_t vp;
-	int8_t lat_sign;
-	int8_t long_sign;
-	int8_t alt_sign;
-} zs_loc_t;
-typedef enum {ZS_STATE_NONE, ZS_STATE_DATA, ZS_STATE_ERROR, ZS_STATE_INCLUDE, ZS_STATE_EOF, ZS_STATE_STOP} zs_state_t;
-typedef struct zs_scanner zs_scanner_t;
-typedef struct zs_scanner {
-	int cs;
-	int top;
-	int stack[16];
-	_Bool multiline;
-	uint64_t number64;
-	uint64_t number64_tmp;
-	uint32_t decimals;
-	uint32_t decimal_counter;
-	uint32_t item_length;
-	uint32_t item_length_position;
-	uint8_t *item_length_location;
-	uint32_t buffer_length;
-	uint8_t buffer[65535];
-	char include_filename[65535];
-	char *path;
-	zs_win_t windows[256];
-	int16_t last_window;
-	zs_apl_t apl;
-	zs_loc_t loc;
-	uint8_t addr[16];
-	_Bool long_string;
-	uint8_t *dname;
-	uint32_t *dname_length;
-	uint32_t dname_tmp_length;
-	uint32_t r_data_tail;
-	uint32_t zone_origin_length;
-	uint8_t zone_origin[318];
-	uint16_t default_class;
-	uint32_t default_ttl;
-	zs_state_t state;
-	struct {
-		_Bool automatic;
-		void (*record)(zs_scanner_t *);
-		void (*error)(zs_scanner_t *);
-		void (*comment)(zs_scanner_t *);
-		void *data;
-	} process;
-	struct {
-		const char *start;
-		const char *current;
-		const char *end;
-		_Bool eof;
-		_Bool mmaped;
-	} input;
-	struct {
-		char *name;
-		int descriptor;
-	} file;
-	struct {
-		int code;
-		uint64_t counter;
-		_Bool fatal;
-	} error;
-	uint64_t line_counter;
-	uint32_t r_owner_length;
-	uint8_t r_owner[318];
-	uint16_t r_class;
-	uint32_t r_ttl;
-	uint16_t r_type;
-	uint32_t r_data_length;
-	uint8_t r_data[65535];
-} zs_scanner_t;
-void zs_deinit(zs_scanner_t *);
-int zs_init(zs_scanner_t *, const char *, const uint16_t, const uint32_t);
-int zs_parse_record(zs_scanner_t *);
-int zs_set_input_file(zs_scanner_t *, const char *);
-int zs_set_input_string(zs_scanner_t *, const char *, size_t);
-const char *zs_strerror(const int);
-]]
diff -pruN 5.4.4-1/daemon/lua/kres-gen-30.lua 5.5.1-5/daemon/lua/kres-gen-30.lua
--- 5.4.4-1/daemon/lua/kres-gen-30.lua	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/lua/kres-gen-30.lua	2022-06-14 07:17:30.379490400 +0000
@@ -0,0 +1,637 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+local ffi = require('ffi')
+--[[ This file is generated by ./kres-gen.sh ]] ffi.cdef[[
+typedef long time_t;
+typedef long __time_t;
+typedef long __suseconds_t;
+struct timeval {
+	__time_t tv_sec;
+	__suseconds_t tv_usec;
+};
+
+typedef struct knot_dump_style knot_dump_style_t;
+extern const knot_dump_style_t KR_DUMP_STYLE_DEFAULT;
+struct kr_cdb_api {};
+struct lru {};
+typedef enum {KNOT_ANSWER, KNOT_AUTHORITY, KNOT_ADDITIONAL} knot_section_t;
+typedef struct {
+	uint16_t pos;
+	uint16_t flags;
+	uint16_t compress_ptr[16];
+} knot_rrinfo_t;
+typedef unsigned char knot_dname_t;
+typedef struct {
+	uint16_t len;
+	uint8_t data[];
+} knot_rdata_t;
+typedef struct {
+	uint16_t count;
+	uint32_t size;
+	knot_rdata_t *rdata;
+} knot_rdataset_t;
+
+typedef struct knot_mm {
+	void *ctx, *alloc, *free;
+} knot_mm_t;
+
+typedef void *(*map_alloc_f)(void *, size_t);
+typedef void (*map_free_f)(void *baton, void *ptr);
+typedef void (*trace_log_f) (const struct kr_request *, const char *);
+typedef void (*trace_callback_f)(struct kr_request *);
+typedef uint8_t * (*alloc_wire_f)(struct kr_request *req, uint16_t *maxlen);
+typedef bool (*addr_info_f)(struct sockaddr*);
+typedef void (*zi_callback)(int state, void *param);
+typedef struct {
+	knot_dname_t *_owner;
+	uint32_t _ttl;
+	uint16_t type;
+	uint16_t rclass;
+	knot_rdataset_t rrs;
+	void *additional;
+} knot_rrset_t;
+
+struct kr_module;
+typedef char *(kr_prop_cb)(void *, struct kr_module *, const char *);
+typedef struct knot_pkt knot_pkt_t;
+typedef struct {
+	uint8_t *ptr[15];
+} knot_edns_options_t;
+typedef struct {
+	knot_pkt_t *pkt;
+	uint16_t pos;
+	uint16_t count;
+} knot_pktsection_t;
+typedef struct knot_compr {
+	uint8_t *wire;
+	knot_rrinfo_t *rrinfo;
+	struct {
+		uint16_t pos;
+		uint8_t labels;
+	} suffix;
+} knot_compr_t;
+struct knot_pkt {
+	uint8_t *wire;
+	size_t size;
+	size_t max_size;
+	size_t parsed;
+	uint16_t reserved;
+	uint16_t qname_size;
+	uint16_t rrset_count;
+	uint16_t flags;
+	knot_rrset_t *opt_rr;
+	knot_rrset_t *tsig_rr;
+	knot_edns_options_t *edns_opts;
+	struct {
+		uint8_t *pos;
+		size_t len;
+	} tsig_wire;
+	knot_section_t current;
+	knot_pktsection_t sections[3];
+	size_t rrset_allocd;
+	knot_rrinfo_t *rr_info;
+	knot_rrset_t *rr;
+	knot_mm_t mm;
+	knot_compr_t compr;
+};
+typedef struct trie trie_t;
+struct kr_qflags {
+	_Bool NO_MINIMIZE : 1;
+	_Bool NO_IPV6 : 1;
+	_Bool NO_IPV4 : 1;
+	_Bool TCP : 1;
+	_Bool NO_ANSWER : 1;
+	_Bool RESOLVED : 1;
+	_Bool AWAIT_IPV4 : 1;
+	_Bool AWAIT_IPV6 : 1;
+	_Bool AWAIT_CUT : 1;
+	_Bool NO_EDNS : 1;
+	_Bool CACHED : 1;
+	_Bool NO_CACHE : 1;
+	_Bool EXPIRING : 1;
+	_Bool ALLOW_LOCAL : 1;
+	_Bool DNSSEC_WANT : 1;
+	_Bool DNSSEC_BOGUS : 1;
+	_Bool DNSSEC_INSECURE : 1;
+	_Bool DNSSEC_CD : 1;
+	_Bool STUB : 1;
+	_Bool ALWAYS_CUT : 1;
+	_Bool DNSSEC_WEXPAND : 1;
+	_Bool PERMISSIVE : 1;
+	_Bool STRICT : 1;
+	_Bool BADCOOKIE_AGAIN : 1;
+	_Bool CNAME : 1;
+	_Bool REORDER_RR : 1;
+	_Bool TRACE : 1;
+	_Bool NO_0X20 : 1;
+	_Bool DNSSEC_NODS : 1;
+	_Bool DNSSEC_OPTOUT : 1;
+	_Bool NONAUTH : 1;
+	_Bool FORWARD : 1;
+	_Bool DNS64_MARK : 1;
+	_Bool CACHE_TRIED : 1;
+	_Bool NO_NS_FOUND : 1;
+	_Bool PKT_IS_SANE : 1;
+	_Bool DNS64_DISABLE : 1;
+};
+typedef struct ranked_rr_array_entry {
+	uint32_t qry_uid;
+	uint8_t rank;
+	uint8_t revalidation_cnt;
+	_Bool cached : 1;
+	_Bool yielded : 1;
+	_Bool to_wire : 1;
+	_Bool expiring : 1;
+	_Bool in_progress : 1;
+	_Bool dont_cache : 1;
+	knot_rrset_t *rr;
+} ranked_rr_array_entry_t;
+typedef struct {
+	ranked_rr_array_entry_t **at;
+	size_t len;
+	size_t cap;
+} ranked_rr_array_t;
+typedef struct kr_http_header_array_entry {
+	char *name;
+	char *value;
+} kr_http_header_array_entry_t;
+typedef struct {
+	kr_http_header_array_entry_t *at;
+	size_t len;
+	size_t cap;
+} kr_http_header_array_t;
+typedef struct {
+	union kr_sockaddr *at;
+	size_t len;
+	size_t cap;
+} kr_sockaddr_array_t;
+struct kr_zonecut {
+	knot_dname_t *name;
+	knot_rrset_t *key;
+	knot_rrset_t *trust_anchor;
+	struct kr_zonecut *parent;
+	trie_t *nsset;
+	knot_mm_t *pool;
+};
+typedef struct {
+	struct kr_query **at;
+	size_t len;
+	size_t cap;
+} kr_qarray_t;
+struct kr_rplan {
+	kr_qarray_t pending;
+	kr_qarray_t resolved;
+	struct kr_query *initial;
+	struct kr_request *request;
+	knot_mm_t *pool;
+	uint32_t next_uid;
+};
+struct kr_request_qsource_flags {
+	_Bool tcp : 1;
+	_Bool tls : 1;
+	_Bool http : 1;
+	_Bool xdp : 1;
+};
+struct kr_extended_error {
+	int32_t info_code;
+	const char *extra_text;
+};
+struct kr_request {
+	struct kr_context *ctx;
+	knot_pkt_t *answer;
+	struct kr_query *current_query;
+	struct {
+		const struct sockaddr *addr;
+		const struct sockaddr *comm_addr;
+		const struct sockaddr *dst_addr;
+		const knot_pkt_t *packet;
+		struct kr_request_qsource_flags flags;
+		struct kr_request_qsource_flags comm_flags;
+		size_t size;
+		int32_t stream_id;
+		kr_http_header_array_t headers;
+	} qsource;
+	struct {
+		unsigned int rtt;
+		const struct kr_transport *transport;
+	} upstream;
+	struct kr_qflags options;
+	int state;
+	ranked_rr_array_t answ_selected;
+	ranked_rr_array_t auth_selected;
+	ranked_rr_array_t add_selected;
+	_Bool answ_validated;
+	_Bool auth_validated;
+	uint8_t rank;
+	struct kr_rplan rplan;
+	trace_log_f trace_log;
+	trace_callback_f trace_finish;
+	int vars_ref;
+	knot_mm_t pool;
+	unsigned int uid;
+	struct {
+		addr_info_f is_tls_capable;
+		addr_info_f is_tcp_connected;
+		addr_info_f is_tcp_waiting;
+		kr_sockaddr_array_t forwarding_targets;
+	} selection_context;
+	unsigned int count_no_nsaddr;
+	unsigned int count_fail_row;
+	alloc_wire_f alloc_wire_cb;
+	struct kr_extended_error extended_error;
+};
+enum kr_rank {KR_RANK_INITIAL, KR_RANK_OMIT, KR_RANK_TRY, KR_RANK_INDET = 4, KR_RANK_BOGUS, KR_RANK_MISMATCH, KR_RANK_MISSING, KR_RANK_INSECURE, KR_RANK_AUTH = 16, KR_RANK_SECURE = 32};
+typedef struct kr_cdb * kr_cdb_pt;
+struct kr_cdb_stats {
+	uint64_t open;
+	uint64_t close;
+	uint64_t count;
+	uint64_t count_entries;
+	uint64_t clear;
+	uint64_t commit;
+	uint64_t read;
+	uint64_t read_miss;
+	uint64_t write;
+	uint64_t remove;
+	uint64_t remove_miss;
+	uint64_t match;
+	uint64_t match_miss;
+	uint64_t read_leq;
+	uint64_t read_leq_miss;
+	double usage_percent;
+};
+typedef struct uv_timer_s uv_timer_t;
+struct kr_cache {
+	kr_cdb_pt db;
+	const struct kr_cdb_api *api;
+	struct kr_cdb_stats stats;
+	uint32_t ttl_min;
+	uint32_t ttl_max;
+	struct timeval checkpoint_walltime;
+	uint64_t checkpoint_monotime;
+	uv_timer_t *health_timer;
+};
+typedef struct kr_layer {
+	int state;
+	struct kr_request *req;
+	const struct kr_layer_api *api;
+	knot_pkt_t *pkt;
+	struct sockaddr *dst;
+	_Bool is_stream;
+} kr_layer_t;
+typedef struct kr_layer_api {
+	int (*begin)(kr_layer_t *);
+	int (*reset)(kr_layer_t *);
+	int (*finish)(kr_layer_t *);
+	int (*consume)(kr_layer_t *, knot_pkt_t *);
+	int (*produce)(kr_layer_t *, knot_pkt_t *);
+	int (*checkout)(kr_layer_t *, knot_pkt_t *, struct sockaddr *, int);
+	int (*answer_finalize)(kr_layer_t *);
+	void *data;
+	int cb_slots[];
+} kr_layer_api_t;
+struct kr_prop {
+	kr_prop_cb *cb;
+	const char *name;
+	const char *info;
+};
+struct kr_module {
+	char *name;
+	int (*init)(struct kr_module *);
+	int (*deinit)(struct kr_module *);
+	int (*config)(struct kr_module *, const char *);
+	const kr_layer_api_t *layer;
+	const struct kr_prop *props;
+	void *lib;
+	void *data;
+};
+struct kr_server_selection {
+	_Bool initialized;
+	void (*choose_transport)(struct kr_query *, struct kr_transport **);
+	void (*update_rtt)(struct kr_query *, const struct kr_transport *, unsigned int);
+	void (*error)(struct kr_query *, const struct kr_transport *, enum kr_selection_error);
+	struct local_state *local_state;
+};
+typedef int kr_log_level_t;
+enum kr_log_group {LOG_GRP_UNKNOWN = -1, LOG_GRP_SYSTEM = 1, LOG_GRP_CACHE, LOG_GRP_IO, LOG_GRP_NETWORK, LOG_GRP_TA, LOG_GRP_TLS, LOG_GRP_GNUTLS, LOG_GRP_TLSCLIENT, LOG_GRP_XDP, LOG_GRP_DOH, LOG_GRP_DNSSEC, LOG_GRP_HINT, LOG_GRP_PLAN, LOG_GRP_ITERATOR, LOG_GRP_VALIDATOR, LOG_GRP_RESOLVER, LOG_GRP_SELECTION, LOG_GRP_ZCUT, LOG_GRP_COOKIES, LOG_GRP_STATISTICS, LOG_GRP_REBIND, LOG_GRP_WORKER, LOG_GRP_POLICY, LOG_GRP_TASENTINEL, LOG_GRP_TASIGNALING, LOG_GRP_TAUPDATE, LOG_GRP_DAF, LOG_GRP_DETECTTIMEJUMP, LOG_GRP_DETECTTIMESKEW, LOG_GRP_GRAPHITE, LOG_GRP_PREFILL, LOG_GRP_PRIMING, LOG_GRP_SRVSTALE, LOG_GRP_WATCHDOG, LOG_GRP_NSID, LOG_GRP_DNSTAP, LOG_GRP_TESTS, LOG_GRP_DOTAUTH, LOG_GRP_HTTP, LOG_GRP_CONTROL, LOG_GRP_MODULE, LOG_GRP_DEVEL, LOG_GRP_RENUMBER, LOG_GRP_EDE, LOG_GRP_REQDBG};
+
+kr_layer_t kr_layer_t_static;
+_Bool kr_dbg_assertion_abort;
+int kr_dbg_assertion_fork;
+
+typedef int32_t (*kr_stale_cb)(int32_t ttl, const knot_dname_t *owner, uint16_t type,
+				const struct kr_query *qry);
+
+void kr_rrset_init(knot_rrset_t *rrset, knot_dname_t *owner,
+			uint16_t type, uint16_t rclass, uint32_t ttl);
+struct kr_query {
+	struct kr_query *parent;
+	knot_dname_t *sname;
+	uint16_t stype;
+	uint16_t sclass;
+	uint16_t id;
+	uint16_t reorder;
+	struct kr_qflags flags;
+	struct kr_qflags forward_flags;
+	uint32_t secret;
+	uint32_t uid;
+	uint64_t creation_time_mono;
+	uint64_t timestamp_mono;
+	struct timeval timestamp;
+	struct kr_zonecut zone_cut;
+	struct kr_layer_pickle *deferred;
+	int8_t cname_depth;
+	struct kr_query *cname_parent;
+	struct kr_request *request;
+	kr_stale_cb stale_cb;
+	struct kr_server_selection server_selection;
+};
+struct kr_context {
+	struct kr_qflags options;
+	knot_rrset_t *downstream_opt_rr;
+	knot_rrset_t *upstream_opt_rr;
+	trie_t *trust_anchors;
+	trie_t *negative_anchors;
+	struct kr_zonecut root_hints;
+	struct kr_cache cache;
+	unsigned int cache_rtt_tout_retry_interval;
+	char _stub[];
+};
+struct kr_transport {
+	knot_dname_t *ns_name;
+	/* beware: hidden stub, to avoid hardcoding sockaddr lengths */
+};
+const char *knot_strerror(int);
+knot_dname_t *knot_dname_copy(const knot_dname_t *, knot_mm_t *);
+knot_dname_t *knot_dname_from_str(uint8_t *, const char *, size_t);
+int knot_dname_in_bailiwick(const knot_dname_t *, const knot_dname_t *);
+_Bool knot_dname_is_equal(const knot_dname_t *, const knot_dname_t *);
+size_t knot_dname_labels(const uint8_t *, const uint8_t *);
+size_t knot_dname_size(const knot_dname_t *);
+void knot_dname_to_lower(knot_dname_t *);
+char *knot_dname_to_str(char *, const knot_dname_t *, size_t);
+knot_rdata_t *knot_rdataset_at(const knot_rdataset_t *, uint16_t);
+int knot_rdataset_merge(knot_rdataset_t *, const knot_rdataset_t *, knot_mm_t *);
+int knot_rrset_add_rdata(knot_rrset_t *, const uint8_t *, uint16_t, knot_mm_t *);
+void knot_rrset_free(knot_rrset_t *, knot_mm_t *);
+int knot_rrset_txt_dump(const knot_rrset_t *, char **, size_t *, const knot_dump_style_t *);
+int knot_rrset_txt_dump_data(const knot_rrset_t *, const size_t, char *, const size_t, const knot_dump_style_t *);
+size_t knot_rrset_size(const knot_rrset_t *);
+int knot_pkt_begin(knot_pkt_t *, knot_section_t);
+int knot_pkt_put_question(knot_pkt_t *, const knot_dname_t *, uint16_t, uint16_t);
+int knot_pkt_put_rotate(knot_pkt_t *, uint16_t, const knot_rrset_t *, uint16_t, uint16_t);
+knot_pkt_t *knot_pkt_new(void *, uint16_t, knot_mm_t *);
+void knot_pkt_free(knot_pkt_t *);
+int knot_pkt_parse(knot_pkt_t *, unsigned int);
+knot_rrset_t *kr_request_ensure_edns(struct kr_request *);
+knot_pkt_t *kr_request_ensure_answer(struct kr_request *);
+int kr_request_set_extended_error(struct kr_request *, int, const char *);
+struct kr_rplan *kr_resolve_plan(struct kr_request *);
+knot_mm_t *kr_resolve_pool(struct kr_request *);
+struct kr_query *kr_rplan_push(struct kr_rplan *, struct kr_query *, const knot_dname_t *, uint16_t, uint16_t);
+int kr_rplan_pop(struct kr_rplan *, struct kr_query *);
+struct kr_query *kr_rplan_resolved(struct kr_rplan *);
+struct kr_query *kr_rplan_last(struct kr_rplan *);
+int kr_forward_add_target(struct kr_request *, const struct sockaddr *);
+_Bool kr_log_is_debug_fun(enum kr_log_group, const struct kr_request *);
+void kr_log_req1(const struct kr_request * const, uint32_t, const unsigned int, enum kr_log_group, const char *, const char *, ...);
+void kr_log_q1(const struct kr_query * const, enum kr_log_group, const char *, const char *, ...);
+const char *kr_log_grp2name(enum kr_log_group);
+void kr_log_fmt(enum kr_log_group, kr_log_level_t, const char *, const char *, const char *, const char *, ...);
+int kr_make_query(struct kr_query *, knot_pkt_t *);
+void kr_pkt_make_auth_header(knot_pkt_t *);
+int kr_pkt_put(knot_pkt_t *, const knot_dname_t *, uint32_t, uint16_t, uint16_t, const uint8_t *, uint16_t);
+int kr_pkt_recycle(knot_pkt_t *);
+int kr_pkt_clear_payload(knot_pkt_t *);
+_Bool kr_pkt_has_wire(const knot_pkt_t *);
+_Bool kr_pkt_has_dnssec(const knot_pkt_t *);
+uint16_t kr_pkt_qclass(const knot_pkt_t *);
+uint16_t kr_pkt_qtype(const knot_pkt_t *);
+char *kr_pkt_text(const knot_pkt_t *);
+void kr_rnd_buffered(void *, unsigned int);
+uint32_t kr_rrsig_sig_inception(const knot_rdata_t *);
+uint32_t kr_rrsig_sig_expiration(const knot_rdata_t *);
+uint16_t kr_rrsig_type_covered(const knot_rdata_t *);
+const char *kr_inaddr(const struct sockaddr *);
+int kr_inaddr_family(const struct sockaddr *);
+int kr_inaddr_len(const struct sockaddr *);
+int kr_inaddr_str(const struct sockaddr *, char *, size_t *);
+int kr_sockaddr_cmp(const struct sockaddr *, const struct sockaddr *);
+int kr_sockaddr_len(const struct sockaddr *);
+uint16_t kr_inaddr_port(const struct sockaddr *);
+int kr_straddr_family(const char *);
+int kr_straddr_subnet(void *, const char *);
+int kr_bitcmp(const char *, const char *, int);
+int kr_family_len(int);
+struct sockaddr *kr_straddr_socket(const char *, int, knot_mm_t *);
+int kr_straddr_split(const char *, char * restrict, uint16_t *);
+_Bool kr_rank_test(uint8_t, uint8_t);
+int kr_ranked_rrarray_add(ranked_rr_array_t *, const knot_rrset_t *, uint8_t, _Bool, uint32_t, knot_mm_t *);
+int kr_ranked_rrarray_finalize(ranked_rr_array_t *, uint32_t, knot_mm_t *);
+void kr_qflags_set(struct kr_qflags *, struct kr_qflags);
+void kr_qflags_clear(struct kr_qflags *, struct kr_qflags);
+int kr_zonecut_add(struct kr_zonecut *, const knot_dname_t *, const void *, int);
+_Bool kr_zonecut_is_empty(struct kr_zonecut *);
+void kr_zonecut_set(struct kr_zonecut *, const knot_dname_t *);
+uint64_t kr_now();
+const char *kr_strptime_diff(const char *, const char *, const char *, double *);
+time_t kr_file_mtime(const char *);
+long long kr_fssize(const char *);
+const char *kr_dirent_name(const struct dirent *);
+void lru_free_items_impl(struct lru *);
+struct lru *lru_create_impl(unsigned int, unsigned int, knot_mm_t *, knot_mm_t *);
+void *lru_get_impl(struct lru *, const char *, unsigned int, unsigned int, _Bool, _Bool *);
+void *mm_realloc(knot_mm_t *, void *, size_t, size_t);
+knot_rrset_t *kr_ta_get(trie_t *, const knot_dname_t *);
+int kr_ta_add(trie_t *, const knot_dname_t *, uint16_t, uint32_t, const uint8_t *, uint16_t);
+int kr_ta_del(trie_t *, const knot_dname_t *);
+void kr_ta_clear(trie_t *);
+_Bool kr_dnssec_key_ksk(const uint8_t *);
+_Bool kr_dnssec_key_revoked(const uint8_t *);
+int kr_dnssec_key_tag(uint16_t, const uint8_t *, size_t);
+int kr_dnssec_key_match(const uint8_t *, size_t, const uint8_t *, size_t);
+int kr_cache_closest_apex(struct kr_cache *, const knot_dname_t *, _Bool, knot_dname_t **);
+int kr_cache_insert_rr(struct kr_cache *, const knot_rrset_t *, const knot_rrset_t *, uint8_t, uint32_t, _Bool);
+int kr_cache_remove(struct kr_cache *, const knot_dname_t *, uint16_t);
+int kr_cache_remove_subtree(struct kr_cache *, const knot_dname_t *, _Bool, int);
+int kr_cache_commit(struct kr_cache *);
+uint32_t packet_ttl(const knot_pkt_t *, _Bool);
+typedef struct {
+	int sock_type;
+	_Bool tls;
+	_Bool http;
+	_Bool xdp;
+	_Bool freebind;
+	const char *kind;
+} endpoint_flags_t;
+typedef struct {
+	char **at;
+	size_t len;
+	size_t cap;
+} addr_array_t;
+typedef struct {
+	int fd;
+	endpoint_flags_t flags;
+} flagged_fd_t;
+typedef struct {
+	flagged_fd_t *at;
+	size_t len;
+	size_t cap;
+} flagged_fd_array_t;
+typedef struct {
+	const char **at;
+	size_t len;
+	size_t cap;
+} config_array_t;
+struct args {
+	addr_array_t addrs;
+	addr_array_t addrs_tls;
+	flagged_fd_array_t fds;
+	int control_fd;
+	int forks;
+	config_array_t config;
+	const char *rundir;
+	_Bool interactive;
+	_Bool quiet;
+	_Bool tty_binary_output;
+};
+typedef struct {
+	const char *zone_file;
+	const char *origin;
+	uint32_t ttl;
+	enum {ZI_STAMP_NOW, ZI_STAMP_MTIM} time_src;
+	_Bool downgrade;
+	_Bool zonemd;
+	const knot_rrset_t *ds;
+	zi_callback cb;
+	void *cb_param;
+} zi_config_t;
+struct args *the_args;
+struct endpoint {
+	void *handle;
+	int fd;
+	int family;
+	uint16_t port;
+	int16_t nic_queue;
+	_Bool engaged;
+	endpoint_flags_t flags;
+};
+struct request_ctx {
+	struct kr_request req;
+	struct worker_ctx *worker;
+	struct qr_task *task;
+	/* beware: hidden stub, to avoid hardcoding sockaddr lengths */
+};
+struct qr_task {
+	struct request_ctx *ctx;
+	/* beware: hidden stub, to avoid qr_tasklist_t */
+};
+int worker_resolve_exec(struct qr_task *, knot_pkt_t *);
+knot_pkt_t *worker_resolve_mk_pkt(const char *, uint16_t, uint16_t, const struct kr_qflags *);
+struct qr_task *worker_resolve_start(knot_pkt_t *, struct kr_qflags);
+int zi_zone_import(const zi_config_t);
+struct engine {
+	struct kr_context resolver;
+	char _stub[];
+};
+struct worker_ctx {
+	struct engine *engine;
+	char _stub[];
+};
+struct worker_ctx *the_worker;
+typedef struct {
+	uint8_t bitmap[32];
+	uint8_t length;
+} zs_win_t;
+typedef struct {
+	uint8_t excl_flag;
+	uint16_t addr_family;
+	uint8_t prefix_length;
+} zs_apl_t;
+typedef struct {
+	uint32_t d1;
+	uint32_t d2;
+	uint32_t m1;
+	uint32_t m2;
+	uint32_t s1;
+	uint32_t s2;
+	uint32_t alt;
+	uint64_t siz;
+	uint64_t hp;
+	uint64_t vp;
+	int8_t lat_sign;
+	int8_t long_sign;
+	int8_t alt_sign;
+} zs_loc_t;
+typedef enum {ZS_STATE_NONE, ZS_STATE_DATA, ZS_STATE_ERROR, ZS_STATE_INCLUDE, ZS_STATE_EOF, ZS_STATE_STOP} zs_state_t;
+typedef struct zs_scanner zs_scanner_t;
+typedef struct zs_scanner {
+	int cs;
+	int top;
+	int stack[16];
+	_Bool multiline;
+	uint64_t number64;
+	uint64_t number64_tmp;
+	uint32_t decimals;
+	uint32_t decimal_counter;
+	uint32_t item_length;
+	uint32_t item_length_position;
+	uint8_t *item_length_location;
+	uint32_t buffer_length;
+	uint8_t buffer[65535];
+	char include_filename[65535];
+	char *path;
+	zs_win_t windows[256];
+	int16_t last_window;
+	zs_apl_t apl;
+	zs_loc_t loc;
+	uint8_t addr[16];
+	_Bool long_string;
+	uint8_t *dname;
+	uint32_t *dname_length;
+	uint32_t dname_tmp_length;
+	uint32_t r_data_tail;
+	uint32_t zone_origin_length;
+	uint8_t zone_origin[318];
+	uint16_t default_class;
+	uint32_t default_ttl;
+	zs_state_t state;
+	struct {
+		_Bool automatic;
+		void (*record)(zs_scanner_t *);
+		void (*error)(zs_scanner_t *);
+		void (*comment)(zs_scanner_t *);
+		void *data;
+	} process;
+	struct {
+		const char *start;
+		const char *current;
+		const char *end;
+		_Bool eof;
+		_Bool mmaped;
+	} input;
+	struct {
+		char *name;
+		int descriptor;
+	} file;
+	struct {
+		int code;
+		uint64_t counter;
+		_Bool fatal;
+	} error;
+	uint64_t line_counter;
+	uint32_t r_owner_length;
+	uint8_t r_owner[318];
+	uint16_t r_class;
+	uint32_t r_ttl;
+	uint16_t r_type;
+	uint32_t r_data_length;
+	uint8_t r_data[65535];
+} zs_scanner_t;
+void zs_deinit(zs_scanner_t *);
+int zs_init(zs_scanner_t *, const char *, const uint16_t, const uint32_t);
+int zs_parse_record(zs_scanner_t *);
+int zs_set_input_file(zs_scanner_t *, const char *);
+int zs_set_input_string(zs_scanner_t *, const char *, size_t);
+const char *zs_strerror(const int);
+]]
diff -pruN 5.4.4-1/daemon/lua/kres-gen-31.lua 5.5.1-5/daemon/lua/kres-gen-31.lua
--- 5.4.4-1/daemon/lua/kres-gen-31.lua	2022-01-05 13:19:14.431727400 +0000
+++ 5.5.1-5/daemon/lua/kres-gen-31.lua	2022-06-14 07:17:30.379490400 +0000
@@ -41,6 +41,7 @@ typedef void (*trace_log_f) (const struc
 typedef void (*trace_callback_f)(struct kr_request *);
 typedef uint8_t * (*alloc_wire_f)(struct kr_request *req, uint16_t *maxlen);
 typedef bool (*addr_info_f)(struct sockaddr*);
+typedef void (*zi_callback)(int state, void *param);
 typedef struct {
 	knot_dname_t *_owner;
 	uint32_t _ttl;
@@ -93,16 +94,13 @@ struct knot_pkt {
 	knot_mm_t mm;
 	knot_compr_t compr;
 };
-typedef struct {
-	void *root;
-	struct knot_mm *pool;
-} map_t;
 typedef struct trie trie_t;
 struct kr_qflags {
 	_Bool NO_MINIMIZE : 1;
 	_Bool NO_IPV6 : 1;
 	_Bool NO_IPV4 : 1;
 	_Bool TCP : 1;
+	_Bool NO_ANSWER : 1;
 	_Bool RESOLVED : 1;
 	_Bool AWAIT_IPV4 : 1;
 	_Bool AWAIT_IPV6 : 1;
@@ -163,10 +161,10 @@ typedef struct {
 	size_t cap;
 } kr_http_header_array_t;
 typedef struct {
-	union inaddr *at;
+	union kr_sockaddr *at;
 	size_t len;
 	size_t cap;
-} inaddr_array_t;
+} kr_sockaddr_array_t;
 struct kr_zonecut {
 	knot_dname_t *name;
 	knot_rrset_t *key;
@@ -194,15 +192,21 @@ struct kr_request_qsource_flags {
 	_Bool http : 1;
 	_Bool xdp : 1;
 };
+struct kr_extended_error {
+	int32_t info_code;
+	const char *extra_text;
+};
 struct kr_request {
 	struct kr_context *ctx;
 	knot_pkt_t *answer;
 	struct kr_query *current_query;
 	struct {
 		const struct sockaddr *addr;
+		const struct sockaddr *comm_addr;
 		const struct sockaddr *dst_addr;
 		const knot_pkt_t *packet;
 		struct kr_request_qsource_flags flags;
+		struct kr_request_qsource_flags comm_flags;
 		size_t size;
 		int32_t stream_id;
 		kr_http_header_array_t headers;
@@ -229,11 +233,12 @@ struct kr_request {
 		addr_info_f is_tls_capable;
 		addr_info_f is_tcp_connected;
 		addr_info_f is_tcp_waiting;
-		inaddr_array_t forwarding_targets;
+		kr_sockaddr_array_t forwarding_targets;
 	} selection_context;
 	unsigned int count_no_nsaddr;
 	unsigned int count_fail_row;
 	alloc_wire_f alloc_wire_cb;
+	struct kr_extended_error extended_error;
 };
 enum kr_rank {KR_RANK_INITIAL, KR_RANK_OMIT, KR_RANK_TRY, KR_RANK_INDET = 4, KR_RANK_BOGUS, KR_RANK_MISMATCH, KR_RANK_MISSING, KR_RANK_INSECURE, KR_RANK_AUTH = 16, KR_RANK_SECURE = 32};
 typedef struct kr_cdb * kr_cdb_pt;
@@ -308,7 +313,7 @@ struct kr_server_selection {
 	struct local_state *local_state;
 };
 typedef int kr_log_level_t;
-enum kr_log_group {LOG_GRP_UNKNOWN = -1, LOG_GRP_SYSTEM = 1, LOG_GRP_CACHE, LOG_GRP_IO, LOG_GRP_NETWORK, LOG_GRP_TA, LOG_GRP_TLS, LOG_GRP_GNUTLS, LOG_GRP_TLSCLIENT, LOG_GRP_XDP, LOG_GRP_ZIMPORT, LOG_GRP_ZSCANNER, LOG_GRP_DOH, LOG_GRP_DNSSEC, LOG_GRP_HINT, LOG_GRP_PLAN, LOG_GRP_ITERATOR, LOG_GRP_VALIDATOR, LOG_GRP_RESOLVER, LOG_GRP_SELECTION, LOG_GRP_ZCUT, LOG_GRP_COOKIES, LOG_GRP_STATISTICS, LOG_GRP_REBIND, LOG_GRP_WORKER, LOG_GRP_POLICY, LOG_GRP_TASENTINEL, LOG_GRP_TASIGNALING, LOG_GRP_TAUPDATE, LOG_GRP_DAF, LOG_GRP_DETECTTIMEJUMP, LOG_GRP_DETECTTIMESKEW, LOG_GRP_GRAPHITE, LOG_GRP_PREFILL, LOG_GRP_PRIMING, LOG_GRP_SRVSTALE, LOG_GRP_WATCHDOG, LOG_GRP_NSID, LOG_GRP_DNSTAP, LOG_GRP_TESTS, LOG_GRP_DOTAUTH, LOG_GRP_HTTP, LOG_GRP_CONTROL, LOG_GRP_MODULE, LOG_GRP_DEVEL, LOG_GRP_RENUMBER, LOG_GRP_REQDBG};
+enum kr_log_group {LOG_GRP_UNKNOWN = -1, LOG_GRP_SYSTEM = 1, LOG_GRP_CACHE, LOG_GRP_IO, LOG_GRP_NETWORK, LOG_GRP_TA, LOG_GRP_TLS, LOG_GRP_GNUTLS, LOG_GRP_TLSCLIENT, LOG_GRP_XDP, LOG_GRP_DOH, LOG_GRP_DNSSEC, LOG_GRP_HINT, LOG_GRP_PLAN, LOG_GRP_ITERATOR, LOG_GRP_VALIDATOR, LOG_GRP_RESOLVER, LOG_GRP_SELECTION, LOG_GRP_ZCUT, LOG_GRP_COOKIES, LOG_GRP_STATISTICS, LOG_GRP_REBIND, LOG_GRP_WORKER, LOG_GRP_POLICY, LOG_GRP_TASENTINEL, LOG_GRP_TASIGNALING, LOG_GRP_TAUPDATE, LOG_GRP_DAF, LOG_GRP_DETECTTIMEJUMP, LOG_GRP_DETECTTIMESKEW, LOG_GRP_GRAPHITE, LOG_GRP_PREFILL, LOG_GRP_PRIMING, LOG_GRP_SRVSTALE, LOG_GRP_WATCHDOG, LOG_GRP_NSID, LOG_GRP_DNSTAP, LOG_GRP_TESTS, LOG_GRP_DOTAUTH, LOG_GRP_HTTP, LOG_GRP_CONTROL, LOG_GRP_MODULE, LOG_GRP_DEVEL, LOG_GRP_RENUMBER, LOG_GRP_EDE, LOG_GRP_REQDBG};
 
 kr_layer_t kr_layer_t_static;
 _Bool kr_dbg_assertion_abort;
@@ -345,8 +350,8 @@ struct kr_context {
 	struct kr_qflags options;
 	knot_rrset_t *downstream_opt_rr;
 	knot_rrset_t *upstream_opt_rr;
-	map_t trust_anchors;
-	map_t negative_anchors;
+	trie_t *trust_anchors;
+	trie_t *negative_anchors;
 	struct kr_zonecut root_hints;
 	struct kr_cache cache;
 	unsigned int cache_rtt_tout_retry_interval;
@@ -380,6 +385,7 @@ void knot_pkt_free(knot_pkt_t *);
 int knot_pkt_parse(knot_pkt_t *, unsigned int);
 knot_rrset_t *kr_request_ensure_edns(struct kr_request *);
 knot_pkt_t *kr_request_ensure_answer(struct kr_request *);
+int kr_request_set_extended_error(struct kr_request *, int, const char *);
 struct kr_rplan *kr_resolve_plan(struct kr_request *);
 knot_mm_t *kr_resolve_pool(struct kr_request *);
 struct kr_query *kr_rplan_push(struct kr_rplan *, struct kr_query *, const knot_dname_t *, uint16_t, uint16_t);
@@ -387,6 +393,7 @@ int kr_rplan_pop(struct kr_rplan *, stru
 struct kr_query *kr_rplan_resolved(struct kr_rplan *);
 struct kr_query *kr_rplan_last(struct kr_rplan *);
 int kr_forward_add_target(struct kr_request *, const struct sockaddr *);
+_Bool kr_log_is_debug_fun(enum kr_log_group, const struct kr_request *);
 void kr_log_req1(const struct kr_request * const, uint32_t, const unsigned int, enum kr_log_group, const char *, const char *, ...);
 void kr_log_q1(const struct kr_query * const, enum kr_log_group, const char *, const char *, ...);
 const char *kr_log_grp2name(enum kr_log_group);
@@ -396,7 +403,8 @@ void kr_pkt_make_auth_header(knot_pkt_t
 int kr_pkt_put(knot_pkt_t *, const knot_dname_t *, uint32_t, uint16_t, uint16_t, const uint8_t *, uint16_t);
 int kr_pkt_recycle(knot_pkt_t *);
 int kr_pkt_clear_payload(knot_pkt_t *);
-uint16_t kr_pkt_has_dnssec(const knot_pkt_t *);
+_Bool kr_pkt_has_wire(const knot_pkt_t *);
+_Bool kr_pkt_has_dnssec(const knot_pkt_t *);
 uint16_t kr_pkt_qclass(const knot_pkt_t *);
 uint16_t kr_pkt_qtype(const knot_pkt_t *);
 char *kr_pkt_text(const knot_pkt_t *);
@@ -434,10 +442,10 @@ void lru_free_items_impl(struct lru *);
 struct lru *lru_create_impl(unsigned int, unsigned int, knot_mm_t *, knot_mm_t *);
 void *lru_get_impl(struct lru *, const char *, unsigned int, unsigned int, _Bool, _Bool *);
 void *mm_realloc(knot_mm_t *, void *, size_t, size_t);
-knot_rrset_t *kr_ta_get(map_t *, const knot_dname_t *);
-int kr_ta_add(map_t *, const knot_dname_t *, uint16_t, uint32_t, const uint8_t *, uint16_t);
-int kr_ta_del(map_t *, const knot_dname_t *);
-void kr_ta_clear(map_t *);
+knot_rrset_t *kr_ta_get(trie_t *, const knot_dname_t *);
+int kr_ta_add(trie_t *, const knot_dname_t *, uint16_t, uint32_t, const uint8_t *, uint16_t);
+int kr_ta_del(trie_t *, const knot_dname_t *);
+void kr_ta_clear(trie_t *);
 _Bool kr_dnssec_key_ksk(const uint8_t *);
 _Bool kr_dnssec_key_revoked(const uint8_t *);
 int kr_dnssec_key_tag(uint16_t, const uint8_t *, size_t);
@@ -487,6 +495,17 @@ struct args {
 	_Bool quiet;
 	_Bool tty_binary_output;
 };
+typedef struct {
+	const char *zone_file;
+	const char *origin;
+	uint32_t ttl;
+	enum {ZI_STAMP_NOW, ZI_STAMP_MTIM} time_src;
+	_Bool downgrade;
+	_Bool zonemd;
+	const knot_rrset_t *ds;
+	zi_callback cb;
+	void *cb_param;
+} zi_config_t;
 struct args *the_args;
 struct endpoint {
 	void *handle;
@@ -510,6 +529,7 @@ struct qr_task {
 int worker_resolve_exec(struct qr_task *, knot_pkt_t *);
 knot_pkt_t *worker_resolve_mk_pkt(const char *, uint16_t, uint16_t, const struct kr_qflags *);
 struct qr_task *worker_resolve_start(knot_pkt_t *, struct kr_qflags);
+int zi_zone_import(const zi_config_t);
 struct engine {
 	struct kr_context resolver;
 	char _stub[];
diff -pruN 5.4.4-1/daemon/lua/kres-gen.sh 5.5.1-5/daemon/lua/kres-gen.sh
--- 5.4.4-1/daemon/lua/kres-gen.sh	2022-01-05 13:19:14.431727400 +0000
+++ 5.5.1-5/daemon/lua/kres-gen.sh	2022-06-14 07:17:30.379490400 +0000
@@ -89,6 +89,7 @@ typedef void (*trace_log_f) (const struc
 typedef void (*trace_callback_f)(struct kr_request *);
 typedef uint8_t * (*alloc_wire_f)(struct kr_request *req, uint16_t *maxlen);
 typedef bool (*addr_info_f)(struct sockaddr*);
+typedef void (*zi_callback)(int state, void *param);
 "
 
 genResType() {
@@ -110,8 +111,6 @@ ${CDEFS} ${LIBKRES} types <<-EOF
 	knot_pktsection_t
 	knot_compr_t
 	struct knot_pkt
-	# lib/generic/
-	map_t
 	#trie_t inside is private to libknot
 	typedef trie_t
 	# libkres
@@ -120,11 +119,12 @@ ${CDEFS} ${LIBKRES} types <<-EOF
 	ranked_rr_array_t
 	kr_http_header_array_entry_t
 	kr_http_header_array_t
-	inaddr_array_t
+	kr_sockaddr_array_t
 	struct kr_zonecut
 	kr_qarray_t
 	struct kr_rplan
 	struct kr_request_qsource_flags
+	struct kr_extended_error
 	struct kr_request
 	enum kr_rank
 	typedef kr_cdb_pt
@@ -203,6 +203,7 @@ ${CDEFS} ${LIBKRES} functions <<-EOF
 # Resolution request
 	kr_request_ensure_edns
 	kr_request_ensure_answer
+	kr_request_set_extended_error
 	kr_resolve_plan
 	kr_resolve_pool
 # Resolution plan
@@ -213,6 +214,7 @@ ${CDEFS} ${LIBKRES} functions <<-EOF
 # Forwarding
 	kr_forward_add_target
 # Utils
+	kr_log_is_debug_fun
 	kr_log_req1
 	kr_log_q1
 	kr_log_grp2name
@@ -222,6 +224,7 @@ ${CDEFS} ${LIBKRES} functions <<-EOF
 	kr_pkt_put
 	kr_pkt_recycle
 	kr_pkt_clear_payload
+	kr_pkt_has_wire
 	kr_pkt_has_dnssec
 	kr_pkt_qclass
 	kr_pkt_qtype
@@ -291,6 +294,7 @@ ${CDEFS} ${KRESD} types <<-EOF
 	flagged_fd_array_t
 	config_array_t
 	struct args
+	zi_config_t
 EOF
 echo "struct args *the_args;"
 
@@ -306,6 +310,7 @@ ${CDEFS} ${KRESD} functions <<-EOF
 	worker_resolve_exec
 	worker_resolve_mk_pkt
 	worker_resolve_start
+	zi_zone_import
 EOF
 
 echo "struct engine" | ${CDEFS} ${KRESD} types | sed '/struct network/,$ d'
diff -pruN 5.4.4-1/daemon/lua/kres.lua 5.5.1-5/daemon/lua/kres.lua
--- 5.4.4-1/daemon/lua/kres.lua	2022-01-05 13:19:14.431727400 +0000
+++ 5.5.1-5/daemon/lua/kres.lua	2022-06-14 07:17:30.379490400 +0000
@@ -5,6 +5,7 @@
 
 local kres -- the module
 
+local kluautil = require('kluautil')
 local ffi = require('ffi')
 local bit = require('bit')
 local bor = bit.bor
@@ -203,6 +204,34 @@ local const_rank = {
 	AUTH = 16,
 	SECURE = 32
 }
+local const_extended_error = {
+	NONE = -1,
+	OTHER = 0,
+	DNSKEY_ALG = 1,
+	DS_DIGEST = 2,
+	STALE = 3,
+	FORGED = 4,
+	INDETERMINATE = 5,
+	BOGUS = 6,
+	SIG_EXPIRED = 7,
+	SIG_NOTYET = 8,
+	DNSKEY_MISS = 9,
+	RRSIG_MISS = 10,
+	DNSKEY_BIT = 11,
+	NSEC_MISS = 12,
+	CACHED_ERR = 13,
+	NOT_READY = 14,
+	BLOCKED = 15,
+	CENSORED = 16,
+	FILTERED = 17,
+	PROHIBITED = 18,
+	STALE_NXD = 19,
+	NOTAUTH = 20,
+	NOTSUP = 21,
+	NREACH_AUTH = 22,
+	NETWORK = 23,
+	INV_DATA = 24,
+}
 
 -- Constant tables
 local const_class_str = itable(const_class)
@@ -211,6 +240,7 @@ local const_rcode_str = itable(const_rco
 local const_opcode_str = itable(const_opcode)
 local const_section_str = itable(const_section)
 local const_rank_str = itable(const_rank)
+local const_extended_error_str = itable(const_extended_error)
 
 -- Metatype for RR types to allow anonymous types
 setmetatable(const_type, {
@@ -658,6 +688,12 @@ ffi.metatype( knot_pkt_t, {
 			if ret ~= 0 then return nil, knot_error_t(ret) end
 			return true
 		end,
+		-- Checks whether the packet has a wire, i.e. the .size is not
+		-- equal to KR_PKT_SIZE_NOWIRE
+		has_wire = function (pkt)
+			assert(ffi.istype(knot_pkt_t, pkt))
+			return C.kr_pkt_has_wire(pkt)
+		end,
 		recycle = function (pkt)
 			assert(ffi.istype(knot_pkt_t, pkt))
 			local ret = C.kr_pkt_recycle(pkt)
@@ -832,6 +868,11 @@ ffi.metatype( kr_request_t, {
 			end
 			return table.concat(buf, '')
 		end,
+		set_extended_error = function(req, code, msg)
+			assert(ffi.istype(kr_request_t, req))
+			msg = kluautil.kr_string2c(msg, req.pool)
+			ffi.C.kr_request_set_extended_error(req, code, msg)
+		end,
 
 		-- chain new callbacks after the old ones
 		-- creates new wrapper functions as necessary
@@ -929,6 +970,7 @@ local function rank_tostring(rank)
 			table.insert(names, string.lower(name))
 		end
 	end
+	table.sort(names) -- pairs() above doesn't give a stable ordering
 	return string.format('0%.2o (%s)', rank, table.concat(names, ' '))
 end
 
@@ -1026,6 +1068,7 @@ kres = {
 	rcode = const_rcode,
 	opcode = const_opcode,
 	rank = const_rank,
+	extended_error = const_extended_error,
 
 	-- Constants to strings
 	tostring = {
@@ -1035,6 +1078,7 @@ kres = {
 		rcode = const_rcode_str,
 		opcode = const_opcode_str,
 		rank = const_rank_str,
+		extended_eror = const_extended_error_str,
 	},
 
 	-- Create a struct kr_qflags from a single flag name or a list of names.
diff -pruN 5.4.4-1/daemon/lua/meson.build 5.5.1-5/daemon/lua/meson.build
--- 5.4.4-1/daemon/lua/meson.build	2022-01-05 13:19:14.431727400 +0000
+++ 5.5.1-5/daemon/lua/meson.build	2022-06-14 07:17:30.379490400 +0000
@@ -40,7 +40,7 @@ distro_preconfig = configure_file(
 
 # Unfortunately the different ABI implies different contents of 'kres-gen.lua'.
 kres_gen_fname = (libknot.version().version_compare('>= 3.1')
-                 ? 'kres-gen-31.lua' : 'kres-gen-29.lua')
+                 ? 'kres-gen-31.lua' : 'kres-gen-30.lua')
 
 kres_gen_lua = configure_file(
   input: kres_gen_fname,
@@ -87,7 +87,7 @@ if get_option('kres_gen_test') and not m
     '''.format(ttc.get('tname'), tsize)
   endforeach
   # Now feed it directly into luajit.
-  kres_gen_test = run_command(find_program('luajit'), '-e', kres_gen_test_luastr)
+  kres_gen_test = run_command(find_program('luajit'), '-e', kres_gen_test_luastr, check: false)
   if kres_gen_test.returncode() != 0
     error('if you use released Knot* versions, please contact us: https://www.knot-resolver.cz/contact/\n'
         + kres_gen_test.stderr().strip())
diff -pruN 5.4.4-1/daemon/lua/sandbox.lua.in 5.5.1-5/daemon/lua/sandbox.lua.in
--- 5.4.4-1/daemon/lua/sandbox.lua.in	2022-01-05 13:19:14.431727400 +0000
+++ 5.5.1-5/daemon/lua/sandbox.lua.in	2022-06-14 07:17:30.383490600 +0000
@@ -509,6 +509,7 @@ modules.load('ta_sentinel')
 modules.load('edns_keepalive')
 modules.load('refuse_nord')
 modules.load('watchdog')
+modules.load('extended_error')
 
 -- Load keyfile_default
 trust_anchors.add_file('@keyfile_default@', @unmanaged@)
diff -pruN 5.4.4-1/daemon/main.c 5.5.1-5/daemon/main.c
--- 5.4.4-1/daemon/main.c	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/main.c	2022-06-14 07:17:30.383490600 +0000
@@ -4,19 +4,14 @@
 
 #include "kresconfig.h"
 
-#include "contrib/ccan/asprintf/asprintf.h"
-#include "contrib/cleanup.h"
 #include "contrib/ucw/mempool.h"
 #include "daemon/engine.h"
 #include "daemon/io.h"
 #include "daemon/network.h"
-#include "daemon/tls.h"
 #include "daemon/udp_queue.h"
 #include "daemon/worker.h"
 #include "lib/defines.h"
 #include "lib/dnssec.h"
-#include "lib/dnssec/ta.h"
-#include "lib/resolve.h"
 #include "lib/log.h"
 
 #include <arpa/inet.h>
@@ -159,6 +154,8 @@ static int run_worker(uv_loop_t *loop, s
 
 	/* Control sockets or TTY */
 	uv_pipe_t *pipe = malloc(sizeof(*pipe));
+	if (!pipe)
+		return EXIT_FAILURE;
 	uv_pipe_init(loop, pipe, 0);
 	if (args->interactive) {
 		if (!args->quiet)
diff -pruN 5.4.4-1/daemon/meson.build 5.5.1-5/daemon/meson.build
--- 5.4.4-1/daemon/meson.build	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/meson.build	2022-06-14 07:17:30.383490600 +0000
@@ -13,6 +13,7 @@ kresd_src = files([
   'io.c',
   'main.c',
   'network.c',
+  'proxyv2.c',
   'session.c',
   'tls.c',
   'tls_ephemeral_credentials.c',
@@ -29,10 +30,12 @@ c_src_lint += kresd_src
 
 config_tests += [
   ['cache.clear', files('cache.test/clear.test.lua')],
+  ['zimport', files('zimport.test/zimport.test.lua')],
 ]
 
 integr_tests += [
-  ['cache_insert_ns', meson.current_source_dir() / 'cache.test' / 'insert_ns.test.integr']
+  ['cache_insert_ns', meson.current_source_dir() / 'cache.test' / 'insert_ns.test.integr'],
+  ['proxyv2', meson.current_source_dir() / 'proxyv2.test']
 ]
 
 kresd_deps = [
diff -pruN 5.4.4-1/daemon/network.c 5.5.1-5/daemon/network.c
--- 5.4.4-1/daemon/network.c	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/network.c	2022-06-14 07:17:30.383490600 +0000
@@ -4,10 +4,12 @@
 
 #include "daemon/network.h"
 
+#include "contrib/cleanup.h"
 #include "daemon/bindings/impl.h"
 #include "daemon/io.h"
 #include "daemon/tls.h"
 #include "daemon/worker.h"
+#include "lib/utils.h"
 
 #if ENABLE_XDP
 	#include <libknot/xdp/eth.h>
@@ -18,12 +20,53 @@
 #include <sys/un.h>
 #include <unistd.h>
 
+/** Determines the type of `struct endpoint_key`. */
+enum endpoint_key_type
+{
+	ENDPOINT_KEY_SOCKADDR = 1,
+	ENDPOINT_KEY_IFNAME   = 2,
+};
+
+/** Used as a key in the `struct network::endpoints` trie. */
+struct endpoint_key {
+	enum endpoint_key_type type;
+	char data[];
+};
+
+struct __attribute__((packed)) endpoint_key_sockaddr {
+	enum endpoint_key_type type;
+	struct kr_sockaddr_key_storage sa_key;
+};
+
+struct __attribute__((packed)) endpoint_key_ifname {
+	enum endpoint_key_type type;
+	char ifname[128];
+};
+
+/** Used for reserving enough storage for `endpoint_key`. */
+struct endpoint_key_storage {
+	union {
+		enum endpoint_key_type type;
+		struct endpoint_key_sockaddr sa;
+		struct endpoint_key_ifname ifname;
+		char bytes[1]; /* for easier casting */
+	};
+};
+
+static_assert(_Alignof(struct endpoint_key) <= 4, "endpoint_key must be aligned to <=4");
+static_assert(_Alignof(struct endpoint_key_sockaddr) <= 4, "endpoint_key must be aligned to <=4");
+static_assert(_Alignof(struct endpoint_key_ifname) <= 4, "endpoint_key must be aligned to <=4");
+
 void network_init(struct network *net, uv_loop_t *loop, int tcp_backlog)
 {
 	if (net != NULL) {
 		net->loop = loop;
-		net->endpoints = map_make(NULL);
+		net->endpoints = trie_create(NULL);
 		net->endpoint_kinds = trie_create(NULL);
+		net->proxy_all4 = false;
+		net->proxy_all6 = false;
+		net->proxy_addrs4 = trie_create(NULL);
+		net->proxy_addrs6 = trie_create(NULL);
 		net->tls_client_params = NULL;
 		net->tls_session_ticket_ctx = /* unsync. random, by default */
 		tls_session_ticket_ctx_create(loop, NULL, 0);
@@ -73,24 +116,29 @@ static int endpoint_open_lua_cb(struct n
 	return kr_ok();
 }
 
-static int engage_endpoint_array(const char *key, void *endpoints, void *net)
+static int engage_endpoint_array(const char *b_key, uint32_t key_len, trie_val_t *val, void *net)
 {
-	endpoint_array_t *eps = (endpoint_array_t *)endpoints;
+	const char *log_addr = network_endpoint_key_str((struct endpoint_key *) b_key);
+	if (!log_addr)
+		log_addr = "[unknown]";
+
+	endpoint_array_t *eps = *val;
 	for (int i = 0; i < eps->len; ++i) {
 		struct endpoint *ep = &eps->at[i];
 		const bool match = !ep->engaged && ep->flags.kind;
 		if (!match) continue;
-		int ret = endpoint_open_lua_cb(net, ep, key);
+		int ret = endpoint_open_lua_cb(net, ep, log_addr);
 		if (ret) return ret;
 	}
 	return 0;
 }
+
 int network_engage_endpoints(struct network *net)
 {
 	if (net->missing_kind_is_error)
 		return kr_ok(); /* maybe weird, but let's make it idempotent */
 	net->missing_kind_is_error = true;
-	int ret = map_walk(&net->endpoints, engage_endpoint_array, net);
+	int ret = trie_apply_with_key(net->endpoints, engage_endpoint_array, net);
 	if (ret) {
 		net->missing_kind_is_error = false; /* avoid the same errors when closing */
 		return ret;
@@ -98,6 +146,25 @@ int network_engage_endpoints(struct netw
 	return kr_ok();
 }
 
+const char *network_endpoint_key_str(const struct endpoint_key *key)
+{
+	switch (key->type)
+	{
+	case ENDPOINT_KEY_SOCKADDR:;
+		const struct endpoint_key_sockaddr *sa_key =
+			(struct endpoint_key_sockaddr *) key;
+		struct sockaddr_storage sa_storage;
+		struct sockaddr *sa = kr_sockaddr_from_key(&sa_storage, (const char *) &sa_key->sa_key);
+		return kr_straddr(sa);
+	case ENDPOINT_KEY_IFNAME:;
+		const struct endpoint_key_ifname *if_key =
+			(struct endpoint_key_ifname *) key;
+		return if_key->ifname;
+	default:
+		kr_assert(false);
+		return NULL;
+	}
+}
 
 /** Notify the registered function about endpoint about to be closed. */
 static void endpoint_close_lua_cb(struct network *net, struct endpoint *ep)
@@ -169,19 +236,19 @@ static void endpoint_close(struct networ
 	}
 }
 
-/** Endpoint visitor (see @file map.h) */
-static int close_key(const char *key, void *val, void *net)
+/** Endpoint visitor (see @file trie.h) */
+static int close_key(trie_val_t *val, void* net)
 {
-	endpoint_array_t *ep_array = val;
+	endpoint_array_t *ep_array = *val;
 	for (int i = 0; i < ep_array->len; ++i) {
 		endpoint_close(net, &ep_array->at[i], true);
 	}
 	return 0;
 }
 
-static int free_key(const char *key, void *val, void *ext)
+static int free_key(trie_val_t *val, void* ext)
 {
-	endpoint_array_t *ep_array = val;
+	endpoint_array_t *ep_array = *val;
 	array_clear(*ep_array);
 	free(ep_array);
 	return kr_ok();
@@ -197,18 +264,34 @@ int kind_unregister(trie_val_t *tv, void
 void network_close_force(struct network *net)
 {
 	if (net != NULL) {
-		map_walk(&net->endpoints, close_key, net);
-		map_walk(&net->endpoints, free_key, 0);
-		map_clear(&net->endpoints);
+		trie_apply(net->endpoints, close_key, net);
+		trie_apply(net->endpoints, free_key, NULL);
+		trie_clear(net->endpoints);
 	}
 }
 
+/** Frees all the `struct net_proxy_data` in the specified trie. */
+void network_proxy_free_addr_data(trie_t* trie)
+{
+	trie_it_t *it;
+	for (it = trie_it_begin(trie); !trie_it_finished(it); trie_it_next(it)) {
+		struct net_proxy_data *data = *trie_it_val(it);
+		free(data);
+	}
+	trie_it_free(it);
+}
+
 void network_deinit(struct network *net)
 {
 	if (net != NULL) {
 		network_close_force(net);
 		trie_apply(net->endpoint_kinds, kind_unregister, the_worker->engine->L);
 		trie_free(net->endpoint_kinds);
+		trie_free(net->endpoints);
+		network_proxy_free_addr_data(net->proxy_addrs4);
+		trie_free(net->proxy_addrs4);
+		network_proxy_free_addr_data(net->proxy_addrs6);
+		trie_free(net->proxy_addrs6);
 
 		tls_credentials_free(net->tls_credentials);
 		tls_client_params_free(net->tls_client_params);
@@ -219,21 +302,44 @@ void network_deinit(struct network *net)
 	}
 }
 
+static ssize_t endpoint_key_create(struct endpoint_key_storage *dst,
+                                   const char *addr_str,
+                                   const struct sockaddr *sa)
+{
+	memset(dst, 0, sizeof(*dst));
+	if (sa) {
+		struct endpoint_key_sockaddr *key = &dst->sa;
+		key->type = ENDPOINT_KEY_SOCKADDR;
+		ssize_t keylen = kr_sockaddr_key(&key->sa_key, sa);
+		if (keylen < 0)
+			return keylen;
+		return sizeof(struct endpoint_key) + keylen;
+	} else {
+		struct endpoint_key_ifname *key = &dst->ifname;
+		key->type = ENDPOINT_KEY_IFNAME;
+		strncpy(key->ifname, addr_str, sizeof(key->ifname) - 1);
+		return sizeof(struct endpoint_key) + strnlen(key->ifname, sizeof(key->ifname));
+	}
+}
+
 /** Fetch or create endpoint array and insert endpoint (shallow memcpy). */
-static int insert_endpoint(struct network *net, const char *addr, struct endpoint *ep)
+static int insert_endpoint(struct network *net, const char *addr_str,
+                           const struct sockaddr *addr, struct endpoint *ep)
 {
 	/* Fetch or insert address into map */
-	endpoint_array_t *ep_array = map_get(&net->endpoints, addr);
-	if (ep_array == NULL) {
+	struct endpoint_key_storage key;
+	ssize_t keylen = endpoint_key_create(&key, addr_str, addr);
+	if (keylen < 0)
+		return keylen;
+	trie_val_t *val = trie_get_ins(net->endpoints, key.bytes, keylen);
+	endpoint_array_t *ep_array;
+	if (*val) {
+		ep_array = *val;
+	} else {
 		ep_array = malloc(sizeof(*ep_array));
-		if (ep_array == NULL) {
-			return kr_error(ENOMEM);
-		}
-		if (map_set(&net->endpoints, addr, ep_array) != 0) {
-			free(ep_array);
-			return kr_error(ENOMEM);
-		}
+		kr_require(ep_array);
 		array_init(*ep_array);
+		*val = ep_array;
 	}
 
 	if (array_reserve(*ep_array, ep_array->len + 1)) {
@@ -250,17 +356,16 @@ static int open_endpoint(struct network
 {
 	const bool is_control = ep->flags.kind && strcmp(ep->flags.kind, "control") == 0;
 	const bool is_xdp     = ep->family == AF_XDP;
-	bool ok = is_xdp
-		? sa == NULL && ep->fd == -1 && ep->nic_queue >= 0
-			&& ep->flags.sock_type == SOCK_DGRAM && !ep->flags.tls
-		: (sa != NULL) != (ep->fd != -1);
+	bool ok = (!is_xdp)
+		|| (sa == NULL && ep->fd == -1 && ep->nic_queue >= 0
+			&& ep->flags.sock_type == SOCK_DGRAM && !ep->flags.tls);
 	if (kr_fails_assert(ok))
 		return kr_error(EINVAL);
 	if (ep->handle) {
 		return kr_error(EEXIST);
 	}
 
-	if (sa) {
+	if (sa && ep->fd == -1) {
 		if (sa->sa_family == AF_UNIX) {
 			struct sockaddr_un *sun = (struct sockaddr_un*)sa;
 			char *dirc = strdup(sun->sun_path);
@@ -344,16 +449,24 @@ finish_ret:
  * Beware that there might be multiple matches, though that's not common.
  * The matching isn't really precise in the sense that it might not find
  * and endpoint that would *collide* the passed one. */
-static struct endpoint * endpoint_get(struct network *net, const char *addr,
-					uint16_t port, endpoint_flags_t flags)
-{
-	endpoint_array_t *ep_array = map_get(&net->endpoints, addr);
-	if (!ep_array) {
+static struct endpoint * endpoint_get(struct network *net,
+                                      const char *addr_str,
+                                      const struct sockaddr *sa,
+                                      endpoint_flags_t flags)
+{
+	struct endpoint_key_storage key;
+	ssize_t keylen = endpoint_key_create(&key, addr_str, sa);
+	if (keylen < 0)
 		return NULL;
-	}
+	trie_val_t *val = trie_get_try(net->endpoints, key.bytes, keylen);
+	if (!val)
+		return NULL;
+	endpoint_array_t *ep_array = *val;
+
+	uint16_t port = kr_inaddr_port(sa);
 	for (int i = 0; i < ep_array->len; ++i) {
 		struct endpoint *ep = &ep_array->at[i];
-		if (ep->port == port && endpoint_flags_eq(ep->flags, flags)) {
+		if ((flags.xdp || ep->port == port) && endpoint_flags_eq(ep->flags, flags)) {
 			return ep;
 		}
 	}
@@ -364,11 +477,11 @@ static struct endpoint * endpoint_get(st
  *  \note in XDP case addr_str is interface name
  *  \note ownership of ep.flags.* is taken on success. */
 static int create_endpoint(struct network *net, const char *addr_str,
-				struct endpoint *ep, const struct sockaddr *sa)
+                           struct endpoint *ep, const struct sockaddr *sa)
 {
 	int ret = open_endpoint(net, addr_str, ep, sa);
 	if (ret == 0) {
-		ret = insert_endpoint(net, addr_str, ep);
+		ret = insert_endpoint(net, addr_str, sa, ep);
 	}
 	if (ret != 0 && ep->handle) {
 		endpoint_close(net, ep, false);
@@ -429,7 +542,7 @@ int network_listen_fd(struct network *ne
 
 	/* always create endpoint for supervisor supplied fd
 	 * even if addr+port is not unique */
-	return create_endpoint(net, addr_str, &ep, NULL);
+	return create_endpoint(net, addr_str, &ep, (struct sockaddr *) &ss);
 }
 
 /** Try selecting XDP queue automatically. */
@@ -482,7 +595,7 @@ int network_listen(struct network *net,
 	}
 	// XDP: if addr failed to parse as address, we assume it's an interface name.
 
-	if (endpoint_get(net, addr, port, flags)) {
+	if (endpoint_get(net, addr, sa, flags)) {
 		return kr_error(EADDRINUSE); // Already listening
 	}
 
@@ -506,13 +619,98 @@ int network_listen(struct network *net,
 	return ret;
 }
 
-int network_close(struct network *net, const char *addr, int port)
+int network_proxy_allow(struct network *net, const char* addr)
 {
-	endpoint_array_t *ep_array = map_get(&net->endpoints, addr);
-	if (!ep_array) {
-		return kr_error(ENOENT);
+	if (kr_fails_assert(net != NULL && addr != NULL))
+		return kr_error(EINVAL);
+
+	int family = kr_straddr_family(addr);
+	if (family < 0) {
+		kr_log_error(NETWORK, "Wrong address format for proxy_allowed: %s\n",
+				addr);
+		return kr_error(EINVAL);
+	} else if (family == AF_UNIX) {
+		kr_log_error(NETWORK, "Unix sockets not supported for proxy_allowed: %s\n",
+				addr);
+		return kr_error(EINVAL);
+	}
+
+	union kr_in_addr ia;
+	int netmask = kr_straddr_subnet(&ia, addr);
+	if (netmask < 0) {
+		kr_log_error(NETWORK, "Wrong netmask format for proxy_allowed: %s\n", addr);
+		return kr_error(EINVAL);
+	} else if (netmask == 0) {
+		/* Netmask is zero: allow all addresses to use PROXYv2 */
+		switch (family) {
+		case AF_INET:
+			net->proxy_all4 = true;
+			break;
+		case AF_INET6:
+			net->proxy_all6 = true;
+			break;
+		default:
+			kr_assert(false);
+			return kr_error(EINVAL);
+		}
+
+		return kr_ok();
+	}
+
+	size_t addr_length;
+	trie_t *trie;
+	switch (family) {
+	case AF_INET:
+		addr_length = sizeof(ia.ip4);
+		trie = net->proxy_addrs4;
+		break;
+	case AF_INET6:
+		addr_length = sizeof(ia.ip6);
+		trie = net->proxy_addrs6;
+		break;
+	default:
+		kr_assert(false);
+		return kr_error(EINVAL);
+	}
+
+	kr_bitmask((unsigned char *) &ia, addr_length, netmask);
+	trie_val_t *val = trie_get_ins(trie, (char *) &ia, addr_length);
+	if (!val)
+		return kr_error(ENOMEM);
+
+	struct net_proxy_data *data = *val;
+	if (!data) {
+		/* Allocate data if the entry is new in the trie */
+		*val = malloc(sizeof(struct net_proxy_data));
+		data = *val;
+		data->netmask = 0;
 	}
 
+	if (data->netmask == 0) {
+		memcpy(&data->addr, &ia, addr_length);
+		data->netmask = netmask;
+	} else if (data->netmask > netmask) {
+		/* A more relaxed netmask configured - replace it */
+		data->netmask = netmask;
+	}
+
+	return kr_ok();
+}
+
+void network_proxy_reset(struct network *net)
+{
+	net->proxy_all4 = false;
+	network_proxy_free_addr_data(net->proxy_addrs4);
+	trie_clear(net->proxy_addrs4);
+	net->proxy_all6 = false;
+	network_proxy_free_addr_data(net->proxy_addrs6);
+	trie_clear(net->proxy_addrs6);
+}
+
+static int endpoints_close(struct network *net,
+                           struct endpoint_key_storage *key, ssize_t keylen,
+                           endpoint_array_t *ep_array, int port)
+{
 	size_t i = 0;
 	bool matched = false; /*< at least one match */
 	while (i < ep_array->len) {
@@ -530,14 +728,108 @@ int network_close(struct network *net, c
 		return kr_error(ENOENT);
 	}
 
+	return kr_ok();
+}
+
+static bool endpoint_key_addr_matches(struct endpoint_key_storage *key_a,
+                                      struct endpoint_key_storage *key_b)
+{
+	if (key_a->type != key_b->type)
+		return false;
+
+	if (key_a->type == ENDPOINT_KEY_IFNAME)
+		return strncmp(key_a->ifname.ifname,
+		               key_b->ifname.ifname,
+		               sizeof(key_a->ifname.ifname)) == 0;
+
+	if (key_a->type == ENDPOINT_KEY_SOCKADDR) {
+		return kr_sockaddr_key_same_addr(
+				key_a->sa.sa_key.bytes, key_b->sa.sa_key.bytes);
+	}
+
+	kr_assert(false);
+	return kr_error(EINVAL);
+}
+
+struct endpoint_key_with_len {
+	struct endpoint_key_storage key;
+	size_t keylen;
+};
+typedef array_t(struct endpoint_key_with_len) endpoint_key_array_t;
+
+struct endpoint_close_wildcard_context {
+	struct network *net;
+	struct endpoint_key_storage *match_key;
+	endpoint_key_array_t del;
+	int ret;
+};
+
+static int endpoints_close_wildcard(const char *s_key, uint32_t keylen, trie_val_t *val, void *baton)
+{
+	struct endpoint_close_wildcard_context *ctx = baton;
+	struct endpoint_key_storage *key = (struct endpoint_key_storage *)s_key;
+
+	if (!endpoint_key_addr_matches(key, ctx->match_key))
+		return kr_ok();
+
+	endpoint_array_t *ep_array = *val;
+	int ret = endpoints_close(ctx->net, key, keylen, ep_array, -1);
+	if (ret)
+		ctx->ret = ret;
+
+	if (ep_array->len == 0) {
+		struct endpoint_key_with_len to_del = {
+			.key = *key,
+			.keylen = keylen
+		};
+		array_push(ctx->del, to_del);
+	}
+
+	return kr_ok();
+}
+
+int network_close(struct network *net, const char *addr_str, int port)
+{
+	auto_free struct sockaddr *addr = kr_straddr_socket(addr_str, port, NULL);
+	struct endpoint_key_storage key;
+	ssize_t keylen = endpoint_key_create(&key, addr_str, addr);
+	if (keylen < 0)
+		return keylen;
+
+	if (port < 0) {
+		struct endpoint_close_wildcard_context ctx = {
+			.net = net,
+			.match_key = &key
+		};
+		array_init(ctx.del);
+		trie_apply_with_key(net->endpoints, endpoints_close_wildcard, &ctx);
+		for (size_t i = 0; i < ctx.del.len; i++) {
+			trie_val_t val;
+			trie_del(net->endpoints,
+			         ctx.del.at[i].key.bytes, ctx.del.at[i].keylen,
+			         &val);
+			if (val) {
+				array_clear(*(endpoint_array_t *) val);
+				free(val);
+			}
+		}
+		return ctx.ret;
+	}
+
+	trie_val_t *val = trie_get_try(net->endpoints, key.bytes, keylen);
+	if (!val)
+		return kr_error(ENOENT);
+	endpoint_array_t *ep_array = *val;
+	int ret = endpoints_close(net, &key, keylen, ep_array, port);
+
 	/* Collapse key if it has no endpoint. */
 	if (ep_array->len == 0) {
 		array_clear(*ep_array);
 		free(ep_array);
-		map_del(&net->endpoints, addr);
+		trie_del(net->endpoints, key.bytes, keylen, NULL);
 	}
 
-	return kr_ok();
+	return ret;
 }
 
 void network_new_hostname(struct network *net, struct engine *engine)
@@ -557,10 +849,10 @@ void network_new_hostname(struct network
 }
 
 #ifdef SO_ATTACH_BPF
-static int set_bpf_cb(const char *key, void *val, void *ext)
+static int set_bpf_cb(trie_val_t *val, void *ctx)
 {
-	endpoint_array_t *endpoints = (endpoint_array_t *)val;
-	int *bpffd = (int *)ext;
+	endpoint_array_t *endpoints = *val;
+	int *bpffd = (int *)ctx;
 	if (kr_fails_assert(endpoints && bpffd))
 		return kr_error(EINVAL);
 
@@ -582,7 +874,7 @@ static int set_bpf_cb(const char *key, v
 int network_set_bpf(struct network *net, int bpf_fd)
 {
 #ifdef SO_ATTACH_BPF
-	if (map_walk(&net->endpoints, set_bpf_cb, &bpf_fd) != 0) {
+	if (trie_apply(net->endpoints, set_bpf_cb, &bpf_fd) != 0) {
 		/* set_bpf_cb() has returned error. */
 		network_clear_bpf(net);
 		return 0;
@@ -597,9 +889,9 @@ int network_set_bpf(struct network *net,
 }
 
 #ifdef SO_DETACH_BPF
-static int clear_bpf_cb(const char *key, void *val, void *ext)
+static int clear_bpf_cb(trie_val_t *val, void *ctx)
 {
-	endpoint_array_t *endpoints = (endpoint_array_t *)val;
+	endpoint_array_t *endpoints = *val;
 	if (kr_fails_assert(endpoints))
 		return kr_error(EINVAL);
 
@@ -623,7 +915,7 @@ static int clear_bpf_cb(const char *key,
 void network_clear_bpf(struct network *net)
 {
 #ifdef SO_DETACH_BPF
-	map_walk(&net->endpoints, clear_bpf_cb, NULL);
+	trie_apply(net->endpoints, clear_bpf_cb, NULL);
 #else
 	kr_log_error(NETWORK, "SO_DETACH_BPF socket option doesn't supported\n");
 	(void)net;
diff -pruN 5.4.4-1/daemon/network.h 5.5.1-5/daemon/network.h
--- 5.4.4-1/daemon/network.h	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/network.h	2022-06-14 07:17:30.383490600 +0000
@@ -7,7 +7,6 @@
 #include "daemon/tls.h"
 
 #include "lib/generic/array.h"
-#include "lib/generic/map.h"
 #include "lib/generic/trie.h"
 
 #include <uv.h>
@@ -31,6 +30,8 @@ typedef struct {
 	const char *kind; /**< tag for other types: "control" or module-handled kinds */
 } endpoint_flags_t;
 
+struct endpoint_key;
+
 static inline bool endpoint_flags_eq(endpoint_flags_t f1, endpoint_flags_t f2)
 {
 	if (f1.sock_type != f2.sock_type)
@@ -68,20 +69,35 @@ struct net_tcp_param {
 	uint64_t tls_handshake_timeout;
 };
 
+/** Information about an address that is allowed to use PROXYv2. */
+struct net_proxy_data {
+	union kr_in_addr addr;
+	uint8_t netmask;   /**< Number of bits to be matched */
+};
+
 struct network {
 	uv_loop_t *loop;
 
 	/** Map: address string -> endpoint_array_t.
-	 * \note even same address-port-flags tuples may appear.
-	 * TODO: trie_t, keyed on *binary* address-port pair. */
-	map_t endpoints;
+	 * \note even same address-port-flags tuples may appear. */
+	trie_t *endpoints;
 
 	/** Registry of callbacks for special endpoint kinds (for opening/closing).
 	 * Map: kind (lowercased) -> lua function ID converted to void *
 	 * The ID is the usual: raw int index in the LUA_REGISTRYINDEX table. */
 	trie_t *endpoint_kinds;
 	/** See network_engage_endpoints() */
-	bool missing_kind_is_error;
+	bool missing_kind_is_error : 1;
+
+	/** True: All IPv4 addresses are allowed to use the PROXYv2 protocol */
+	bool proxy_all4 : 1;
+	/** True: All IPv6 addresses are allowed to use the PROXYv2 protocol */
+	bool proxy_all6 : 1;
+
+	/** IPv4 addresses and networks allowed to use the PROXYv2 protocol */
+	trie_t *proxy_addrs4;
+	/** IPv6 addresses and networks allowed to use the PROXYv2 protocol */
+	trie_t *proxy_addrs6;
 
 	struct tls_credentials *tls_credentials;
 	tls_client_params_t *tls_client_params; /**< Use tls_client_params_*() functions. */
@@ -105,6 +121,17 @@ void network_deinit(struct network *net)
 int network_listen(struct network *net, const char *addr, uint16_t port,
 		   int16_t nic_queue, endpoint_flags_t flags);
 
+/** Allow the specified address to send the PROXYv2 header.
+ * \note the address may be specified with a netmask
+ */
+int network_proxy_allow(struct network *net, const char* addr);
+
+/** Reset all addresses allowed to send the PROXYv2 header. No addresses will
+ * be allowed to send PROXYv2 headers from the point of calling this function
+ * until re-allowed via network_proxy_allow again.
+ */
+void network_proxy_reset(struct network *net);
+
 /** Start listening on an open file-descriptor.
  * \note flags.sock_type isn't meaningful here.
  * \note ownership of flags.* is taken on success.  TODO: non-success?
@@ -123,6 +150,11 @@ void network_close_force(struct network
  * This only does anything with struct endpoint::flags.kind != NULL. */
 int network_engage_endpoints(struct network *net);
 
+/** Returns a string representation of the specified endpoint key.
+ *
+ * The result points into key or is on static storage like for kr_straddr() */
+const char *network_endpoint_key_str(const struct endpoint_key *key);
+
 int network_set_tls_cert(struct network *net, const char *cert);
 int network_set_tls_key(struct network *net, const char *key);
 void network_new_hostname(struct network *net, struct engine *engine);
diff -pruN 5.4.4-1/daemon/proxyv2.c 5.5.1-5/daemon/proxyv2.c
--- 5.4.4-1/daemon/proxyv2.c	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/proxyv2.c	2022-06-14 07:17:30.383490600 +0000
@@ -0,0 +1,290 @@
+/*  Copyright (C) 2014-2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+ *  SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#include "daemon/proxyv2.h"
+
+#include "lib/generic/trie.h"
+
+const char PROXY2_SIGNATURE[12] = {
+	0x0D, 0x0A, 0x0D, 0x0A, 0x00, 0x0D, 0x0A, 0x51, 0x55, 0x49, 0x54, 0x0A
+};
+
+#define PROXY2_IP6_ADDR_SIZE 16
+#define PROXY2_UNIX_ADDR_SIZE 108
+
+#define TLV_TYPE_SSL 0x20
+
+enum proxy2_family {
+	PROXY2_AF_UNSPEC = 0x0,
+	PROXY2_AF_INET   = 0x1,
+	PROXY2_AF_INET6  = 0x2,
+	PROXY2_AF_UNIX   = 0x3
+};
+
+enum proxy2_protocol {
+	PROXY2_PROTOCOL_UNSPEC = 0x0,
+	PROXY2_PROTOCOL_STREAM = 0x1,
+	PROXY2_PROTOCOL_DGRAM  = 0x2
+};
+
+/** PROXYv2 protocol header section */
+struct proxy2_header {
+	uint8_t signature[sizeof(PROXY2_SIGNATURE)];
+	uint8_t version_command;
+	uint8_t family_protocol;
+	uint16_t length; /**< Length of the address section */
+};
+
+/** PROXYv2 additional information in Type-Length-Value (TLV) format. */
+struct proxy2_tlv {
+	uint8_t type;
+	uint8_t length_hi;
+	uint8_t length_lo;
+	uint8_t value[];
+};
+
+/** PROXYv2 protocol address section */
+union proxy2_address {
+	struct {
+		uint32_t src_addr;
+		uint32_t dst_addr;
+		uint16_t src_port;
+		uint16_t dst_port;
+	} ipv4_addr;
+	struct {
+		uint8_t src_addr[PROXY2_IP6_ADDR_SIZE];
+		uint8_t dst_addr[PROXY2_IP6_ADDR_SIZE];
+		uint16_t src_port;
+		uint16_t dst_port;
+	} ipv6_addr;
+	struct {
+		uint8_t src_addr[PROXY2_UNIX_ADDR_SIZE];
+		uint8_t dst_addr[PROXY2_UNIX_ADDR_SIZE];
+	} unix_addr;
+};
+
+
+/** Gets protocol version from the specified PROXYv2 header. */
+static inline unsigned char proxy2_header_version(const struct proxy2_header* h)
+{
+	return (h->version_command & 0xF0) >> 4;
+}
+
+/** Gets command from the specified PROXYv2 header. */
+static inline enum proxy2_command proxy2_header_command(const struct proxy2_header *h)
+{
+	return h->version_command & 0x0F;
+}
+
+/** Gets address family from the specified PROXYv2 header. */
+static inline enum proxy2_family proxy2_header_family(const struct proxy2_header *h)
+{
+	return (h->family_protocol & 0xF0) >> 4;
+}
+
+/** Gets transport protocol from the specified PROXYv2 header. */
+static inline enum proxy2_family proxy2_header_protocol(const struct proxy2_header *h)
+{
+	return h->family_protocol & 0x0F;
+}
+
+static inline union proxy2_address *proxy2_get_address(const struct proxy2_header *h)
+{
+	return (union proxy2_address *) ((uint8_t *) h + sizeof(struct proxy2_header));
+}
+
+static inline struct proxy2_tlv *get_tlvs(const struct proxy2_header *h, size_t addr_len)
+{
+	return (struct proxy2_tlv *) ((uint8_t *) proxy2_get_address(h) + addr_len);
+}
+
+/** Gets the length of the TLV's `value` attribute. */
+static inline uint16_t proxy2_tlv_length(const struct proxy2_tlv *tlv)
+{
+	return ((uint16_t) tlv->length_hi << 16) | tlv->length_lo;
+}
+
+static inline bool has_tlv(const struct proxy2_header *h,
+                           const struct proxy2_tlv *tlv)
+{
+	uint64_t addr_length = ntohs(h->length);
+	ptrdiff_t hdr_len = sizeof(struct proxy2_header) + addr_length;
+
+	uint8_t *tlv_hdr_end = (uint8_t *) tlv + sizeof(struct proxy2_tlv);
+	ptrdiff_t distance = tlv_hdr_end - (uint8_t *) h;
+	if (hdr_len < distance)
+		return false;
+
+	uint8_t *tlv_end = tlv_hdr_end + proxy2_tlv_length(tlv);
+	distance = tlv_end - (uint8_t *) h;
+	return hdr_len >= distance;
+}
+
+static inline void next_tlv(struct proxy2_tlv **tlv)
+{
+	uint8_t *next = ((uint8_t *) *tlv + sizeof(struct proxy2_tlv) + proxy2_tlv_length(*tlv));
+	*tlv = (struct proxy2_tlv *) next;
+}
+
+
+bool proxy_allowed(const struct network *net, const struct sockaddr *saddr)
+{
+	union kr_in_addr addr;
+	trie_t *trie;
+	size_t addr_size;
+	switch (saddr->sa_family) {
+	case AF_INET:
+		if (net->proxy_all4)
+			return true;
+
+		trie = net->proxy_addrs4;
+		addr_size = sizeof(addr.ip4);
+		addr.ip4 = ((struct sockaddr_in *) saddr)->sin_addr;
+		break;
+	case AF_INET6:
+		if (net->proxy_all6)
+			return true;
+
+		trie = net->proxy_addrs6;
+		addr_size = sizeof(addr.ip6);
+		addr.ip6 = ((struct sockaddr_in6 *) saddr)->sin6_addr;
+		break;
+	default:
+		kr_assert(false); // Only IPv4 and IPv6 proxy addresses supported
+		return false;
+	}
+
+	trie_val_t *val;
+	int ret = trie_get_leq(trie, (char *) &addr, addr_size, &val);
+	if (ret != kr_ok() && ret != 1)
+		return false;
+
+	kr_assert(val);
+	const struct net_proxy_data *found = *val;
+	kr_assert(found);
+	return kr_bitcmp((char *) &addr, (char *) &found->addr, found->netmask) == 0;
+}
+
+ssize_t proxy_process_header(struct proxy_result *out, struct session *s,
+		const void *buf, const ssize_t nread)
+{
+	if (!buf)
+		return kr_error(EINVAL);
+
+	const struct proxy2_header *hdr = (struct proxy2_header *) buf;
+
+	uint64_t content_length = ntohs(hdr->length);
+	ssize_t hdr_len = sizeof(struct proxy2_header) + content_length;
+
+	/* PROXYv2 requires the header to be received all at once */
+	if (nread < hdr_len) {
+		return kr_error(KNOT_EMALF);
+	}
+
+	unsigned char version = proxy2_header_version(hdr);
+	if (version != 2) {
+		/* Version MUST be 2 for PROXYv2 protocol */
+		return kr_error(KNOT_EMALF);
+	}
+
+	enum proxy2_command command = proxy2_header_command(hdr);
+	if (command == PROXY2_CMD_LOCAL) {
+		/* Addresses for LOCAL are to be discarded */
+		*out = (struct proxy_result) { .command = PROXY2_CMD_LOCAL };
+		goto fill_wirebuf;
+	}
+
+	if (command != PROXY2_CMD_PROXY) {
+		/* PROXYv2 prohibits values other than LOCAL and PROXY */
+		return kr_error(KNOT_EMALF);
+	}
+
+	*out = (struct proxy_result) { .command = PROXY2_CMD_PROXY };
+
+	/* Parse flags */
+	enum proxy2_family family = proxy2_header_family(hdr);
+	switch(family) {
+	case PROXY2_AF_UNSPEC:
+	case PROXY2_AF_UNIX: /* UNIX is unsupported, fall back to UNSPEC */
+		out->family = AF_UNSPEC;
+		break;
+	case PROXY2_AF_INET:
+		out->family = AF_INET;
+		break;
+	case PROXY2_AF_INET6:
+		out->family = AF_INET6;
+		break;
+	default: /* PROXYv2 prohibits other values */
+		return kr_error(KNOT_EMALF);
+	}
+
+	enum proxy2_family protocol = proxy2_header_protocol(hdr);
+	switch (protocol) {
+	case PROXY2_PROTOCOL_DGRAM:
+		out->protocol = SOCK_DGRAM;
+		break;
+	case PROXY2_PROTOCOL_STREAM:
+		out->protocol = SOCK_STREAM;
+		break;
+	default: /* PROXYv2 prohibits other values */
+		return kr_error(KNOT_EMALF);
+	}
+
+	/* Parse addresses */
+	union proxy2_address* addr = proxy2_get_address(hdr);
+	size_t addr_length = 0;
+	switch(out->family) {
+	case AF_INET:
+		addr_length = sizeof(addr->ipv4_addr);
+		if (content_length < addr_length)
+			return kr_error(KNOT_EMALF);
+
+		out->src_addr.ip4 = (struct sockaddr_in) {
+			.sin_family = AF_INET,
+			.sin_addr = { .s_addr = addr->ipv4_addr.src_addr },
+			.sin_port = addr->ipv4_addr.src_port,
+		};
+		out->dst_addr.ip4 = (struct sockaddr_in) {
+			.sin_family = AF_INET,
+			.sin_addr = { .s_addr = addr->ipv4_addr.dst_addr },
+			.sin_port = addr->ipv4_addr.dst_port,
+		};
+		break;
+	case AF_INET6:
+		addr_length = sizeof(addr->ipv6_addr);
+		if (content_length < addr_length)
+			return kr_error(KNOT_EMALF);
+
+		out->src_addr.ip6 = (struct sockaddr_in6) {
+			.sin6_family = AF_INET6,
+			.sin6_port = addr->ipv6_addr.src_port
+		};
+		memcpy(
+				&out->src_addr.ip6.sin6_addr.s6_addr,
+				&addr->ipv6_addr.src_addr,
+				sizeof(out->src_addr.ip6.sin6_addr.s6_addr));
+		out->dst_addr.ip6 = (struct sockaddr_in6) {
+			.sin6_family = AF_INET6,
+			.sin6_port = addr->ipv6_addr.dst_port
+		};
+		memcpy(
+				&out->dst_addr.ip6.sin6_addr.s6_addr,
+				&addr->ipv6_addr.dst_addr,
+				sizeof(out->dst_addr.ip6.sin6_addr.s6_addr));
+		break;
+	}
+
+	/* Process additional information */
+	for (struct proxy2_tlv *tlv = get_tlvs(hdr, addr_length); has_tlv(hdr, tlv); next_tlv(&tlv)) {
+		switch (tlv->type) {
+		case TLV_TYPE_SSL:
+			out->has_tls = true;
+			break;
+		/* TODO: add more TLV types if needed */
+		}
+	}
+
+fill_wirebuf:
+	return session_wirebuf_trim(s, hdr_len);
+}
diff -pruN 5.4.4-1/daemon/proxyv2.h 5.5.1-5/daemon/proxyv2.h
--- 5.4.4-1/daemon/proxyv2.h	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/proxyv2.h	2022-06-14 07:17:30.383490600 +0000
@@ -0,0 +1,50 @@
+/*  Copyright (C) 2014-2020 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+ *  SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include "daemon/session.h"
+#include "daemon/network.h"
+#include "lib/utils.h"
+
+extern const char PROXY2_SIGNATURE[12];
+
+#define PROXY2_MIN_SIZE 16
+
+enum proxy2_command {
+	PROXY2_CMD_LOCAL = 0x0,
+	PROXY2_CMD_PROXY = 0x1
+};
+
+/** Parsed result of the PROXY protocol */
+struct proxy_result {
+	enum proxy2_command command;  /**< Proxy command - PROXY or LOCAL. */
+	int family;                   /**< Address family from netinet library (e.g. AF_INET6). */
+	int protocol;                 /**< Protocol type from socket library (e.g. SOCK_STREAM). */
+	union kr_sockaddr src_addr;   /**< Parsed source address and port. */
+	union kr_sockaddr dst_addr;   /**< Parsed destination address and port. */
+	bool has_tls : 1;             /**< `true` = client has used TLS with the proxy.
+	                                   If TLS padding is enabled, it will be used even if
+	                                   the proxy did not use TLS with kresd. */
+};
+
+/** Checks for a PROXY protocol version 2 signature in the specified buffer. */
+static inline bool proxy_header_present(const void* buf, const ssize_t nread)
+{
+	return nread >= PROXY2_MIN_SIZE &&
+		memcmp(buf, PROXY2_SIGNATURE, sizeof(PROXY2_SIGNATURE)) == 0;
+}
+
+/** Checks whether the use of PROXYv2 protocol is allowed for the specified
+ * address. */
+bool proxy_allowed(const struct network *net, const struct sockaddr *saddr);
+
+/** Parses the PROXYv2 header from buf of size nread and writes the result into
+ * out. The rest of the buffer is moved to free bytes of the specified session's
+ * wire buffer. The function assumes that the PROXYv2 signature is present
+ * and has been already checked by the caller (like `udp_recv` or `tcp_recv`). */
+ssize_t proxy_process_header(struct proxy_result *out, struct session *s,
+                             const void *buf, ssize_t nread);
diff -pruN 5.4.4-1/daemon/proxyv2.test/deckard.yaml 5.5.1-5/daemon/proxyv2.test/deckard.yaml
--- 5.4.4-1/daemon/proxyv2.test/deckard.yaml	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/proxyv2.test/deckard.yaml	2022-06-14 07:17:30.383490600 +0000
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+programs:
+  - name: dnsdist
+    binary: dnsdist
+    additional:
+      - --verbose
+      - --supervised
+      - --config
+      - dnsdist.conf
+    ignore_exit_code: True
+    templates:
+      - daemon/proxyv2.test/dnsdist_config.j2
+    configs:
+      - dnsdist.conf
+  - name: kresd
+    binary: kresd
+    additional:
+      - --noninteractive
+    templates:
+      - daemon/proxyv2.test/kresd_config.j2
+      - tests/integration/hints_zone.j2
+    configs:
+      - config
+      - hints
diff -pruN 5.4.4-1/daemon/proxyv2.test/dnsdist_config.j2 5.5.1-5/daemon/proxyv2.test/dnsdist_config.j2
--- 5.4.4-1/daemon/proxyv2.test/dnsdist_config.j2	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/proxyv2.test/dnsdist_config.j2	2022-06-14 07:17:30.383490600 +0000
@@ -0,0 +1,11 @@
+-- vim:syntax=lua
+setLocal('{{SELF_ADDR}}')
+setVerboseHealthChecks(true)
+setServerPolicy(firstAvailable)
+
+local server = newServer({
+	address="{{PROGRAMS['kresd']['address']}}",
+	useProxyProtocol=true,
+	checkName="example.cz."
+})
+server:setUp()
diff -pruN 5.4.4-1/daemon/proxyv2.test/kresd_config.j2 5.5.1-5/daemon/proxyv2.test/kresd_config.j2
--- 5.4.4-1/daemon/proxyv2.test/kresd_config.j2	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/proxyv2.test/kresd_config.j2	2022-06-14 07:17:30.383490600 +0000
@@ -0,0 +1,63 @@
+-- SPDX-License-Identifier: GPL-3.0-or-later
+{% raw %}
+modules.load('view < policy')
+view:addr("127.127.0.0", policy.suffix(policy.DENY_MSG("addr 127.127.0.0 matched com"),{"\3com\0"}))
+-- policy.add(policy.all(policy.FORWARD('1.2.3.4')))
+
+-- make sure DNSSEC is turned off for tests
+trust_anchors.remove('.')
+
+-- Disable RFC5011 TA update
+if ta_update then
+        modules.unload('ta_update')
+end
+
+-- Disable RFC8145 signaling, scenario doesn't provide expected answers
+if ta_signal_query then
+        modules.unload('ta_signal_query')
+end
+
+-- Disable RFC8109 priming, scenario doesn't provide expected answers
+if priming then
+        modules.unload('priming')
+end
+
+-- Disable this module because it make one priming query
+if detect_time_skew then
+        modules.unload('detect_time_skew')
+end
+
+_hint_root_file('hints')
+cache.size = 2*MB
+log_level('debug')
+{% endraw %}
+
+-- Allow PROXYv2 from dnsdist's address
+--net.proxy_allowed("{{PROGRAMS['dnsdist']['address']}}")
+net.proxy_allowed("127.127.0.0/16")
+
+net = { '{{SELF_ADDR}}' }
+
+{% if QMIN == "false" %}
+option('NO_MINIMIZE', true)
+{% else %}
+option('NO_MINIMIZE', false)
+{% endif %}
+
+
+-- Self-checks on globals
+assert(help() ~= nil)
+assert(worker.id ~= nil)
+-- Self-checks on facilities
+assert(cache.count() == 0)
+assert(cache.stats() ~= nil)
+assert(cache.backends() ~= nil)
+assert(worker.stats() ~= nil)
+assert(net.interfaces() ~= nil)
+-- Self-checks on loaded stuff
+assert(net.list()[1].transport.ip == '{{SELF_ADDR}}')
+assert(#modules.list() > 0)
+-- Self-check timers
+ev = event.recurrent(1 * sec, function (ev) return 1 end)
+event.cancel(ev)
+ev = event.after(0, function (ev) return 1 end)
diff -pruN 5.4.4-1/daemon/proxyv2.test/proxyv2_valid.rpl 5.5.1-5/daemon/proxyv2.test/proxyv2_valid.rpl
--- 5.4.4-1/daemon/proxyv2.test/proxyv2_valid.rpl	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/proxyv2.test/proxyv2_valid.rpl	2022-06-14 07:17:30.383490600 +0000
@@ -0,0 +1,72 @@
+; SPDX-License-Identifier: GPL-3.0-or-later
+; config options
+	stub-addr: 1.2.3.4
+	query-minimization: off
+CONFIG_END
+
+SCENARIO_BEGIN proxyv2:valid test
+
+RANGE_BEGIN 0 110
+	ADDRESS 1.2.3.4
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+example.cz. IN A
+SECTION ANSWER
+example.cz. IN A 5.6.7.8
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+k.root-servers.net. IN AAAA
+SECTION ANSWER
+k.root-servers.net. IN AAAA ::1
+ENTRY_END
+
+RANGE_END
+
+; query with PROXYv2 header - not blocked
+STEP 10 QUERY
+ENTRY_BEGIN
+ADJUST raw_id
+REPLY RD
+SECTION QUESTION
+example.cz. IN A
+ENTRY_END
+
+STEP 20 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH flags rcode question answer
+REPLY QR RD RA NOERROR
+SECTION QUESTION
+example.cz. IN A
+SECTION ANSWER
+example.cz. IN A 5.6.7.8
+ENTRY_END
+
+; query with PROXYv2 header - blocked by view:addr
+; NXDOMAIN expected
+STEP 30 QUERY
+ENTRY_BEGIN
+REPLY RD
+SECTION QUESTION
+example.com. IN A
+ENTRY_END
+
+STEP 31 CHECK_ANSWER
+ENTRY_BEGIN
+MATCH opcode question rcode additional
+REPLY QR RD RA AA NXDOMAIN
+SECTION QUESTION
+example.com. IN A
+SECTION ADDITIONAL
+explanation.invalid. 10800 IN TXT "addr 127.127.0.0 matched com"
+ENTRY_END
+
+SCENARIO_END
diff -pruN 5.4.4-1/daemon/session.c 5.5.1-5/daemon/session.c
--- 5.4.4-1/daemon/session.c	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/session.c	2022-06-14 07:17:30.383490600 +0000
@@ -6,11 +6,11 @@
 
 #include "lib/defines.h"
 #include "daemon/session.h"
-#include "daemon/engine.h"
 #include "daemon/tls.h"
 #include "daemon/http.h"
 #include "daemon/worker.h"
 #include "daemon/io.h"
+#include "daemon/proxyv2.h"
 #include "lib/generic/queue.h"
 
 #define TLS_CHUNK_SIZE (16 * 1024)
@@ -28,14 +28,16 @@
  */
 struct session {
 	struct session_flags sflags;  /**< miscellaneous flags. */
-	union inaddr peer;            /**< address of peer; not for UDP clients (downstream) */
-	union inaddr sockname;        /**< our local address; for UDP it may be a wildcard */
+	union kr_sockaddr peer;       /**< address of peer; not for UDP clients (downstream) */
+	union kr_sockaddr sockname;   /**< our local address; for UDP it may be a wildcard */
 	uv_handle_t *handle;          /**< libuv handle for IO operations. */
 	uv_timer_t timeout;           /**< libuv handle for timer. */
 
 	struct tls_ctx *tls_ctx;      /**< server side tls-related data. */
 	struct tls_client_ctx *tls_client_ctx;  /**< client side tls-related data. */
 
+	struct proxy_result *proxy;   /**< PROXYv2 data for TCP. May be `NULL` if not proxied. */
+
 #if ENABLE_DOH2
 	struct http_ctx *http_ctx;  /**< server side http-related data. */
 #endif
@@ -83,6 +85,7 @@ void session_clear(struct session *sessi
 	if (session->handle && session->handle->type == UV_TCP) {
 		free(session->wire_buf);
 	}
+	free(session->proxy);
 #if ENABLE_DOH2
 	http_free(session->http_ctx);
 #endif
@@ -108,7 +111,7 @@ void session_close(struct session *sessi
 	if (!uv_is_closing((uv_handle_t *)&session->timeout)) {
 		uv_timer_stop(&session->timeout);
 		if (session->tls_client_ctx) {
-			tls_close(&session->tls_client_ctx->c);
+			tls_client_close(session->tls_client_ctx);
 		}
 		if (session->tls_ctx) {
 			tls_close(&session->tls_ctx->c);
@@ -436,6 +439,21 @@ void session_waitinglist_finalize(struct
 	}
 }
 
+struct proxy_result *session_proxy_create(struct session *session)
+{
+	if (!kr_fails_assert(!session->proxy)) {
+		session->proxy = calloc(1, sizeof(struct proxy_result));
+		kr_require(session->proxy);
+	}
+
+	return session->proxy;
+}
+
+struct proxy_result *session_proxy_get(struct session *session)
+{
+	return session->proxy;
+}
+
 void session_tasklist_finalize(struct session *session, int status)
 {
 	while (session_tasklist_get_len(session) > 0) {
@@ -458,6 +476,9 @@ int session_tasklist_finalize_expired(st
 		trie_val_t *v = trie_it_val(it);
 		struct qr_task *task = (struct qr_task *)*v;
 		if ((now - worker_task_creation_time(task)) >= KR_RESOLVE_TIME_LIMIT) {
+			struct kr_request *req = worker_task_request(task);
+			if (!kr_fails_assert(req))
+				kr_query_inform_timeout(req, req->current_query);
 			queue_push(q, task);
 			worker_task_ref(task);
 		}
@@ -531,25 +552,30 @@ int session_timer_stop(struct session *s
 
 ssize_t session_wirebuf_consume(struct session *session, const uint8_t *data, ssize_t len)
 {
-	if (data != &session->wire_buf[session->wire_buf_end_idx]) {
-		/* shouldn't happen */
+	if (kr_fails_assert(data == &session->wire_buf[session->wire_buf_end_idx]))
 		return kr_error(EINVAL);
-	}
-
-	if (len < 0) {
-		/* shouldn't happen */
+	if (kr_fails_assert(len >= 0))
 		return kr_error(EINVAL);
-	}
-
-	if (session->wire_buf_end_idx + len > session->wire_buf_size) {
-		/* shouldn't happen */
+	if (kr_fails_assert(session->wire_buf_end_idx + len <= session->wire_buf_size))
 		return kr_error(EINVAL);
-	}
 
 	session->wire_buf_end_idx += len;
 	return len;
 }
 
+ssize_t session_wirebuf_trim(struct session *session, ssize_t len)
+{
+	if (kr_fails_assert(len >= 0))
+		return kr_error(EINVAL);
+	if (kr_fails_assert(session->wire_buf_start_idx + len <= session->wire_buf_size))
+		return kr_error(EINVAL);
+
+	session->wire_buf_start_idx += len;
+	if (session->wire_buf_start_idx > session->wire_buf_end_idx)
+		session->wire_buf_end_idx = session->wire_buf_start_idx;
+	return len;
+}
+
 knot_pkt_t *session_produce_packet(struct session *session, knot_mm_t *mm)
 {
 	session->sflags.wirebuf_error = false;
@@ -741,7 +767,7 @@ void session_unpoison(struct session *se
 	kr_asan_unpoison(session, sizeof(*session));
 }
 
-int session_wirebuf_process(struct session *session, const struct sockaddr *peer)
+int session_wirebuf_process(struct session *session, struct io_comm_data *comm)
 {
 	int ret = 0;
 	if (session->wire_buf_start_idx == session->wire_buf_end_idx)
@@ -756,7 +782,7 @@ int session_wirebuf_process(struct sessi
 	       (ret < max_iterations)) {
 		if (kr_fails_assert(!session_wirebuf_error(session)))
 			return -1;
-		int res = worker_submit(session, peer, NULL, NULL, NULL, pkt);
+		int res = worker_submit(session, comm, NULL, NULL, pkt);
 		/* Errors from worker_submit() are intentionally *not* handled in order to
 		 * ensure the entire wire buffer is processed. */
 		if (res == kr_ok())
diff -pruN 5.4.4-1/daemon/session.h 5.5.1-5/daemon/session.h
--- 5.4.4-1/daemon/session.h	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/session.h	2022-06-14 07:17:30.383490600 +0000
@@ -13,6 +13,8 @@
 struct qr_task;
 struct worker_ctx;
 struct session;
+struct io_comm_data;
+struct proxy_result;
 
 struct session_flags {
 	bool outgoing : 1;      /**< True: to upstream; false: from a client. */
@@ -20,6 +22,9 @@ struct session_flags {
 	bool has_tls : 1;       /**< True: given session uses TLS. */
 	bool has_http : 1;      /**< True: given session uses HTTP. */
 	bool connected : 1;     /**< True: TCP connection is established. */
+	bool no_proxy : 1;      /**< True: TCP has gotten some data - PROXYv2 header
+	                         * disallowed. Proxy headers are only expected at
+	                         * the very start of a stream. */
 	bool closing : 1;       /**< True: session close sequence is in progress. */
 	bool wirebuf_error : 1; /**< True: last operation with wirebuf ended up with an error. */
 };
@@ -54,6 +59,13 @@ void session_waitinglist_retry(struct se
 /** Finalize all tasks in the list. */
 void session_waitinglist_finalize(struct session *session, int status);
 
+/** PROXYv2 data. */
+/** Creates zero-initialized PROXYv2 data for the session. Should only be called
+ * once per session. */
+struct proxy_result *session_proxy_create(struct session *session);
+/** Gets the session's PROXYv2 data, if it exists. If it does not, returns `NULL`. */
+struct proxy_result *session_proxy_get(struct session *session);
+
 /** List of tasks associated with session. */
 /** Check if list is empty. */
 bool session_tasklist_is_empty(const struct session *session);
@@ -128,10 +140,13 @@ size_t session_wirebuf_get_free_size(str
 void session_wirebuf_discard(struct session *session);
 /** Move all data to the beginning of the buffer. */
 void session_wirebuf_compress(struct session *session);
-int session_wirebuf_process(struct session *session, const struct sockaddr *peer);
+int session_wirebuf_process(struct session *session, struct io_comm_data *comm);
 ssize_t session_wirebuf_consume(struct session *session,
 				const uint8_t *data, ssize_t len);
-
+/** Trims `len` bytes from the start of the session's wire buffer.
+ * If this operation makes the buffer's end appear before the start, it gets
+ * nudged to the same position as the start. */
+ssize_t session_wirebuf_trim(struct session *session, ssize_t len);
 /** poison session structure with ASAN. */
 void session_poison(struct session *session);
 /** unpoison session structure with ASAN. */
diff -pruN 5.4.4-1/daemon/tls.c 5.5.1-5/daemon/tls.c
--- 5.4.4-1/daemon/tls.c	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/tls.c	2022-06-14 07:17:30.387490500 +0000
@@ -19,7 +19,6 @@
 
 #include "contrib/ucw/lib.h"
 #include "contrib/base64.h"
-#include "daemon/io.h"
 #include "daemon/tls.h"
 #include "daemon/worker.h"
 #include "daemon/session.h"
@@ -332,7 +331,12 @@ struct tls_ctx *tls_new(struct worker_ct
 		return NULL;
 	}
 
-	int err = gnutls_init(&tls->c.tls_session, GNUTLS_SERVER | GNUTLS_NONBLOCK);
+	int flags = GNUTLS_SERVER | GNUTLS_NONBLOCK;
+#if GNUTLS_VERSION_NUMBER >= 0x030705
+	if (gnutls_check_version("3.7.5"))
+		flags |= GNUTLS_NO_TICKETS_TLS12;
+#endif
+	int err = gnutls_init(&tls->c.tls_session, flags);
 	if (err != GNUTLS_E_SUCCESS) {
 		kr_log_error(TLS, "gnutls_init(): %s (%d)\n", gnutls_strerror_name(err), err);
 		tls_free(tls);
@@ -380,6 +384,17 @@ void tls_close(struct tls_common_ctx *ct
 	}
 }
 
+void tls_client_close(struct tls_client_ctx *ctx)
+{
+	/* Store the current session data for potential resumption of this session */
+	if (ctx->params) {
+		gnutls_free(ctx->params->session_data.data);
+		gnutls_session_get_data2(ctx->c.tls_session, &ctx->params->session_data);
+	}
+
+	tls_close(&ctx->c);
+}
+
 void tls_free(struct tls_ctx *tls)
 {
 	if (!tls) {
@@ -842,7 +857,7 @@ tls_client_param_t * tls_client_param_ne
  * \param len[out] output length
  * \param key[out] output buffer
  */
-static bool construct_key(const union inaddr *addr, uint32_t *len, char *key)
+static bool construct_key(const union kr_sockaddr *addr, uint32_t *len, char *key)
 {
 	switch (addr->ip.sa_family) {
 	case AF_INET:
@@ -875,7 +890,7 @@ tls_client_param_t ** tls_client_param_g
 			return NULL;
 	}
 	/* Construct the key. */
-	const union inaddr *ia = (const union inaddr *)addr;
+	const union kr_sockaddr *ia = (const union kr_sockaddr *)addr;
 	char key[sizeof(ia->ip6.sin6_port) + sizeof(ia->ip6.sin6_addr)];
 	uint32_t len;
 	if (!construct_key(ia, &len, key))
@@ -887,7 +902,7 @@ tls_client_param_t ** tls_client_param_g
 
 int tls_client_param_remove(tls_client_params_t *params, const struct sockaddr *addr)
 {
-	const union inaddr *ia = (const union inaddr *)addr;
+	const union kr_sockaddr *ia = (const union kr_sockaddr *)addr;
 	char key[sizeof(ia->ip6.sin6_port) + sizeof(ia->ip6.sin6_addr)];
 	uint32_t len;
 	if (!construct_key(ia, &len, key))
@@ -1056,6 +1071,10 @@ struct tls_client_ctx *tls_client_ctx_ne
 			     | GNUTLS_ENABLE_FALSE_START
 #endif
 	;
+#if GNUTLS_VERSION_NUMBER >= 0x030705
+	if (gnutls_check_version("3.7.5"))
+		flags |= GNUTLS_NO_TICKETS_TLS12;
+#endif
 	int ret = gnutls_init(&ctx->c.tls_session,  flags);
 	if (ret != GNUTLS_E_SUCCESS) {
 		tls_client_ctx_free(ctx);
diff -pruN 5.4.4-1/daemon/tls_ephemeral_credentials.c 5.5.1-5/daemon/tls_ephemeral_credentials.c
--- 5.4.4-1/daemon/tls_ephemeral_credentials.c	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/tls_ephemeral_credentials.c	2022-06-14 07:17:30.387490500 +0000
@@ -12,7 +12,7 @@
 #include <gnutls/x509.h>
 #include <gnutls/crypto.h>
 
-#include "daemon/worker.h"
+#include "daemon/engine.h"
 #include "daemon/tls.h"
 
 #define EPHEMERAL_PRIVKEY_FILENAME "ephemeral_key.pem"
diff -pruN 5.4.4-1/daemon/tls.h 5.5.1-5/daemon/tls.h
--- 5.4.4-1/daemon/tls.h	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/tls.h	2022-06-14 07:17:30.387490500 +0000
@@ -151,6 +151,10 @@ struct tls_ctx* tls_new(struct worker_ct
 /*! Close a TLS context (call gnutls_bye()) */
 void tls_close(struct tls_common_ctx *ctx);
 
+/*! Close a TLS client context (call gnutls_bye()), storing its session data
+ * for potential resumption. */
+void tls_client_close(struct tls_client_ctx *ctx);
+
 /*! Release a TLS context */
 void tls_free(struct tls_ctx* tls);
 
diff -pruN 5.4.4-1/daemon/udp_queue.c 5.5.1-5/daemon/udp_queue.c
--- 5.4.4-1/daemon/udp_queue.c	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/udp_queue.c	2022-06-14 07:17:30.387490500 +0000
@@ -5,7 +5,6 @@
 #include "kresconfig.h"
 #include "daemon/udp_queue.h"
 
-#include "daemon/session.h"
 #include "daemon/worker.h"
 #include "lib/generic/array.h"
 #include "lib/utils.h"
@@ -44,6 +43,8 @@ typedef struct {
 static udp_queue_t * udp_queue_create()
 {
 	udp_queue_t *q = calloc(1, sizeof(*q));
+	kr_require(q != NULL);
+
 	for (int i = 0; i < UDP_QUEUE_LEN; ++i) {
 		struct msghdr *mhi = &q->msgvec[i].msg_hdr;
 		/* These shall remain always the same. */
@@ -120,7 +121,7 @@ void udp_queue_push(int fd, struct kr_re
 	udp_queue_t *const q = state.udp_queues[fd];
 
 	/* Append to the queue */
-	struct sockaddr *sa = (struct sockaddr *)/*const-cast*/req->qsource.addr;
+	struct sockaddr *sa = (struct sockaddr *)/*const-cast*/req->qsource.comm_addr;
 	q->msgvec[q->len].msg_hdr.msg_name = sa;
 	q->msgvec[q->len].msg_hdr.msg_namelen = kr_sockaddr_len(sa);
 	q->items[q->len].task = task;
diff -pruN 5.4.4-1/daemon/worker.c 5.5.1-5/daemon/worker.c
--- 5.4.4-1/daemon/worker.c	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/worker.c	2022-06-14 07:17:30.387490500 +0000
@@ -27,11 +27,11 @@
 #include "daemon/bindings/api.h"
 #include "daemon/engine.h"
 #include "daemon/io.h"
+#include "daemon/proxyv2.h"
 #include "daemon/session.h"
 #include "daemon/tls.h"
 #include "daemon/http.h"
 #include "daemon/udp_queue.h"
-#include "daemon/zimport.h"
 #include "lib/layer.h"
 #include "lib/utils.h"
 
@@ -51,7 +51,7 @@
 #define MAX_PIPELINED 100
 #endif
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE(qry, WORKER, __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q(qry, WORKER, __VA_ARGS__)
 
 /** Client request state. */
 struct request_ctx
@@ -64,10 +64,12 @@ struct request_ctx
 		/** NULL if the request didn't come over network. */
 		struct session *session;
 		/** Requestor's address; separate because of UDP session "sharing". */
-		union inaddr addr;
+		union kr_sockaddr addr;
+		/** Request communication address; if not from a proxy, same as addr. */
+		union kr_sockaddr comm_addr;
 		/** Local address.  For AF_XDP we couldn't use session's,
 		 * as the address might be different every time. */
-		union inaddr dst_addr;
+		union kr_sockaddr dst_addr;
 		/** MAC addresses - ours [0] and router's [1], in case of AF_XDP socket. */
 		uint8_t eth_addrs[2][6];
 	} source;
@@ -104,11 +106,6 @@ struct qr_task
 			qr_task_free((task)); \
 	} while (0)
 
-/** @internal get key for tcp session
- *  @note kr_straddr() return pointer to static string
- */
-#define tcpsess_key(addr) kr_straddr(addr)
-
 /* Forward decls */
 static void qr_task_free(struct qr_task *task);
 static int qr_task_step(struct qr_task *task,
@@ -163,11 +160,11 @@ static uv_handle_t *ioreq_spawn(struct w
 	}
 
 	/* Bind to outgoing address, according to IP v4/v6. */
-	union inaddr *addr;
+	union kr_sockaddr *addr;
 	if (family == AF_INET) {
-		addr = (union inaddr *)&worker->out_addr4;
+		addr = (union kr_sockaddr *)&worker->out_addr4;
 	} else {
-		addr = (union inaddr *)&worker->out_addr6;
+		addr = (union kr_sockaddr *)&worker->out_addr6;
 	}
 	if (addr->ip.sa_family != AF_UNSPEC) {
 		if (kr_fails_assert(addr->ip.sa_family == family)) {
@@ -277,7 +274,7 @@ static uint8_t *alloc_wire_cb(struct kr_
 		return NULL;
 	xdp_handle_data_t *xhd = handle->data;
 	knot_xdp_msg_t out;
-	bool ipv6 = ctx->source.addr.ip.sa_family == AF_INET6;
+	bool ipv6 = ctx->source.comm_addr.ip.sa_family == AF_INET6;
 	int ret = knot_xdp_send_alloc(xhd->socket,
 			#if KNOT_VERSION_HEX >= 0x030100
 					ipv6 ? KNOT_XDP_MSG_IPV6 : 0, &out);
@@ -290,9 +287,11 @@ static uint8_t *alloc_wire_cb(struct kr_
 		return NULL;
 	}
 	*maxlen = MIN(*maxlen, out.payload.iov_len);
+#if KNOT_VERSION_HEX < 0x030100
 	/* It's most convenient to fill the MAC addresses at this point. */
 	memcpy(out.eth_from, &ctx->source.eth_addrs[0], 6);
 	memcpy(out.eth_to,   &ctx->source.eth_addrs[1], 6);
+#endif
 	return out.payload.iov_base;
 }
 static void free_wire(const struct request_ctx *ctx)
@@ -313,8 +312,13 @@ static void free_wire(const struct reque
 	knot_xdp_msg_t out;
 	out.payload.iov_base = ans->wire;
 	out.payload.iov_len = 0;
-	uint32_t sent;
+	uint32_t sent = 0;
+#if KNOT_VERSION_HEX >= 0x030100
+	int ret = 0;
+	knot_xdp_send_free(xhd->socket, &out, 1);
+#else
 	int ret = knot_xdp_send(xhd->socket, &out, 1, &sent);
+#endif
 	kr_assert(ret == KNOT_EOK && sent == 0);
 	kr_log_debug(XDP, "freed unsent buffer, ret = %d\n", ret);
 }
@@ -339,12 +343,11 @@ static inline bool is_tcp_waiting(struct
  * in case the request didn't come from network.
  */
 static struct request_ctx *request_create(struct worker_ctx *worker,
-					  struct session *session,
-					  const struct sockaddr *addr,
-					  const struct sockaddr *dst_addr,
-					  const uint8_t *eth_from,
-					  const uint8_t *eth_to,
-					  uint32_t uid)
+                                          struct session *session,
+                                          struct io_comm_data *comm,
+                                          const uint8_t *eth_from,
+                                          const uint8_t *eth_to,
+                                          uint32_t uid)
 {
 	knot_mm_t pool = {
 		.ctx = pool_borrow(worker),
@@ -390,15 +393,30 @@ static struct request_ctx *request_creat
 	req->pool = pool;
 	req->vars_ref = LUA_NOREF;
 	req->uid = uid;
-	req->qsource.flags.xdp = is_xdp;
+	req->qsource.comm_flags.xdp = is_xdp;
+	kr_request_set_extended_error(req, KNOT_EDNS_EDE_NONE, NULL);
 	array_init(req->qsource.headers);
 	if (session) {
-		req->qsource.flags.tcp = session_get_handle(session)->type == UV_TCP;
-		req->qsource.flags.tls = session_flags(session)->has_tls;
-		req->qsource.flags.http = session_flags(session)->has_http;
+		kr_require(comm);
+
+		const struct sockaddr *src_addr = comm->src_addr;
+		const struct sockaddr *comm_addr = comm->comm_addr;
+		const struct sockaddr *dst_addr = comm->dst_addr;
+		const struct proxy_result *proxy = comm->proxy;
+
+		req->qsource.comm_flags.tcp = session_get_handle(session)->type == UV_TCP;
+		req->qsource.comm_flags.tls = session_flags(session)->has_tls;
+		req->qsource.comm_flags.http = session_flags(session)->has_http;
+
+		req->qsource.flags = req->qsource.comm_flags;
+		if (proxy) {
+			req->qsource.flags.tcp = proxy->protocol == SOCK_STREAM;
+			req->qsource.flags.tls = proxy->has_tls;
+		}
+
 		req->qsource.stream_id = -1;
 #if ENABLE_DOH2
-		if (req->qsource.flags.http) {
+		if (req->qsource.comm_flags.http) {
 			struct http_ctx *http_ctx = session_http_get_server_ctx(session);
 			struct http_stream stream = queue_head(http_ctx->streams);
 			req->qsource.stream_id = stream.id;
@@ -410,8 +428,14 @@ static struct request_ctx *request_creat
 		}
 #endif
 		/* We need to store a copy of peer address. */
-		memcpy(&ctx->source.addr.ip, addr, kr_sockaddr_len(addr));
+		memcpy(&ctx->source.addr.ip, src_addr, kr_sockaddr_len(src_addr));
 		req->qsource.addr = &ctx->source.addr.ip;
+
+		if (!comm_addr)
+			comm_addr = src_addr;
+		memcpy(&ctx->source.comm_addr.ip, comm_addr, kr_sockaddr_len(comm_addr));
+		req->qsource.comm_addr = &ctx->source.comm_addr.ip;
+
 		if (!dst_addr) /* We wouldn't have to copy in this case, but for consistency. */
 			dst_addr = session_get_sockname(session);
 		memcpy(&ctx->source.dst_addr.ip, dst_addr, kr_sockaddr_len(dst_addr));
@@ -699,7 +723,7 @@ static int qr_task_send(struct qr_task *
 	if (kr_fails_assert(handle && handle->data == session))
 		return qr_task_on_send(task, NULL, kr_error(EINVAL));
 	const bool is_stream = handle->type == UV_TCP;
-	if (!is_stream && handle->type != UV_UDP) abort();
+	kr_require(is_stream || handle->type == UV_UDP);
 
 	if (addr == NULL)
 		addr = session_get_peer(session);
@@ -721,13 +745,6 @@ static int qr_task_send(struct qr_task *
 		worker_task_pkt_set_msgid(task, msg_id);
 	}
 
-	uv_handle_t *ioreq = malloc(is_stream ? sizeof(uv_write_t) : sizeof(uv_udp_send_t));
-	if (!ioreq)
-		return qr_task_on_send(task, handle, kr_error(ENOMEM));
-
-	/* Pending ioreq on current task */
-	qr_task_ref(task);
-
 	struct worker_ctx *worker = ctx->worker;
 	/* Note time for upstream RTT */
 	task->send_time = kr_now();
@@ -735,6 +752,14 @@ static int qr_task_send(struct qr_task *
 	/* Send using given protocol */
 	if (kr_fails_assert(!session_flags(session)->closing))
 		return qr_task_on_send(task, NULL, kr_error(EIO));
+
+	uv_handle_t *ioreq = malloc(is_stream ? sizeof(uv_write_t) : sizeof(uv_udp_send_t));
+	if (!ioreq)
+		return qr_task_on_send(task, handle, kr_error(ENOMEM));
+
+	/* Pending ioreq on current task */
+	qr_task_ref(task);
+
 	if (session_flags(session)->has_http) {
 #if ENABLE_DOH2
 		uv_write_t *write_req = (uv_write_t *)ioreq;
@@ -857,9 +882,12 @@ static int session_tls_hs_cb(struct sess
 			 * So that it MUST be unsuccessful rehandshake.
 			 * Check it. */
 			kr_require(deletion_res != 0);
-			const char *key = tcpsess_key(peer);
-			kr_require(key);
-			kr_require(map_contains(&the_worker->tcp_connected, key) != 0);
+			struct kr_sockaddr_key_storage key;
+			ssize_t keylen = kr_sockaddr_key(&key, peer);
+			if (keylen < 0)
+				return keylen;
+			trie_val_t *val;
+			kr_require((val = trie_get_try(the_worker->tcp_connected, key.bytes, keylen)) && *val);
 		}
 #endif
 		return ret;
@@ -1180,6 +1208,7 @@ static uv_handle_t *transmit(struct qr_t
 		struct session *session = ret->data;
 		struct sockaddr *peer = session_get_peer(session);
 		kr_assert(peer->sa_family == AF_UNSPEC && session_flags(session)->outgoing);
+		kr_require(addr->sa_family == AF_INET || addr->sa_family == AF_INET6);
 		memcpy(peer, addr, kr_sockaddr_len(addr));
 		if (qr_task_send(task, session, (struct sockaddr *)choice,
 				 task->pktbuf) != 0) {
@@ -1300,8 +1329,17 @@ static int xdp_push(struct qr_task *task
 		return qr_task_on_send(task, src_handle, kr_error(EINVAL));
 
 	knot_xdp_msg_t msg;
+#if KNOT_VERSION_HEX >= 0x030100
+	/* We don't have a nice way of preserving the _msg_t from frame allocation,
+	 * so we manually redo all other parts of knot_xdp_send_alloc() */
+	memset(&msg, 0, sizeof(msg));
+	bool ipv6 = ctx->source.addr.ip.sa_family == AF_INET6;
+	msg.flags = ipv6 ? KNOT_XDP_MSG_IPV6 : 0;
+	memcpy(msg.eth_from, &ctx->source.eth_addrs[0], 6);
+	memcpy(msg.eth_to,   &ctx->source.eth_addrs[1], 6);
+#endif
 	const struct sockaddr *ip_from = &ctx->source.dst_addr.ip;
-	const struct sockaddr *ip_to   = &ctx->source.addr.ip;
+	const struct sockaddr *ip_to   = &ctx->source.comm_addr.ip;
 	memcpy(&msg.ip_from, ip_from, kr_sockaddr_len(ip_from));
 	memcpy(&msg.ip_to,   ip_to,   kr_sockaddr_len(ip_to));
 	msg.payload.iov_base = ctx->req.answer->wire;
@@ -1337,7 +1375,11 @@ static int qr_task_finalize(struct qr_ta
 		return state == KR_STATE_DONE ? kr_ok() : kr_error(EIO);
 	}
 
-	if (unlikely(ctx->req.answer == NULL)) { /* meant to be dropped */
+	/* meant to be dropped */
+	if (unlikely(ctx->req.answer == NULL || ctx->req.options.NO_ANSWER)) {
+		/* For NO_ANSWER, a well-behaved layer should set the state to FAIL */
+		kr_assert(!ctx->req.options.NO_ANSWER || (ctx->req.state & KR_STATE_FAIL));
+
 		(void) qr_task_on_send(task, NULL, kr_ok());
 		return kr_ok();
 	}
@@ -1365,7 +1407,7 @@ static int qr_task_finalize(struct qr_ta
 		else
 			kr_assert(false);
 	} else {
-		ret = qr_task_send(task, source_session, &ctx->source.addr.ip, ctx->req.answer);
+		ret = qr_task_send(task, source_session, &ctx->source.comm_addr.ip, ctx->req.answer);
 	}
 
 	if (ret != kr_ok()) {
@@ -1616,6 +1658,9 @@ static int qr_task_step(struct qr_task *
 	/* Close pending I/O requests */
 	subreq_finalize(task, packet_source, packet);
 	if ((kr_now() - worker_task_creation_time(task)) >= KR_RESOLVE_TIME_LIMIT) {
+		struct kr_request *req = worker_task_request(task);
+		if (!kr_fails_assert(req))
+			kr_query_inform_timeout(req, req->current_query);
 		return qr_task_finalize(task, KR_STATE_FAIL);
 	}
 
@@ -1658,10 +1703,15 @@ static int qr_task_step(struct qr_task *
 			struct kr_rplan *rplan = &req->rplan;
 			struct kr_query *last = kr_rplan_last(rplan);
 			if (task->iter_count > KR_ITER_LIMIT) {
-				VERBOSE_MSG(last, "canceling query due to exceeded iteration count limit of %d\n", KR_ITER_LIMIT);
+				char *msg = "cancelling query due to exceeded iteration count limit";
+				VERBOSE_MSG(last, "%s of %d\n", msg, KR_ITER_LIMIT);
+				kr_request_set_extended_error(req, KNOT_EDNS_EDE_OTHER,
+					"OGHD: exceeded iteration count limit");
 			}
 			if (task->timeouts >= KR_TIMEOUT_LIMIT) {
-				VERBOSE_MSG(last, "canceling query due to exceeded timeout retries limit of %d\n", KR_TIMEOUT_LIMIT);
+				char *msg = "cancelling query due to exceeded timeout retries limit";
+				VERBOSE_MSG(last, "%s of %d\n", msg, KR_TIMEOUT_LIMIT);
+				kr_request_set_extended_error(req, KNOT_EDNS_EDE_NREACH_AUTH, "QLPL");
 			}
 
 			return qr_task_finalize(task, KR_STATE_FAIL);
@@ -1709,9 +1759,8 @@ static int parse_packet(knot_pkt_t *quer
 	return ret;
 }
 
-int worker_submit(struct session *session,
-		  const struct sockaddr *peer, const struct sockaddr *dst_addr,
-		  const uint8_t *eth_from, const uint8_t *eth_to, knot_pkt_t *pkt)
+int worker_submit(struct session *session, struct io_comm_data *comm,
+                  const uint8_t *eth_from, const uint8_t *eth_to, knot_pkt_t *pkt)
 {
 	if (!session || !pkt)
 		return kr_error(EINVAL);
@@ -1728,6 +1777,12 @@ int worker_submit(struct session *sessio
 	struct http_ctx *http_ctx = NULL;
 #if ENABLE_DOH2
 	http_ctx = session_http_get_server_ctx(session);
+
+	/* Badly formed query when using DoH leads to a Bad Request */
+	if (http_ctx && !is_outgoing && ret) {
+		http_send_status(session, HTTP_STATUS_BAD_REQUEST);
+		return ret;
+	}
 #endif
 
 	if (!is_outgoing && http_ctx && queue_len(http_ctx->streams) <= 0)
@@ -1738,13 +1793,6 @@ int worker_submit(struct session *sessio
 	    (is_query == is_outgoing)) {
 		if (!is_outgoing) {
 			the_worker->stats.dropped += 1;
-		#if ENABLE_DOH2
-			if (http_ctx) {
-				struct http_stream stream = queue_head(http_ctx->streams);
-				http_free_headers(stream.headers);
-				queue_pop(http_ctx->streams);
-			}
-		#endif
 		}
 		return kr_error(EILSEQ);
 	}
@@ -1755,8 +1803,8 @@ int worker_submit(struct session *sessio
 	const struct sockaddr *addr = NULL;
 	if (!is_outgoing) { /* request from a client */
 		struct request_ctx *ctx =
-			request_create(the_worker, session, peer, dst_addr,
-					eth_from, eth_to, knot_wire_get_id(pkt->wire));
+			request_create(the_worker, session, comm, eth_from,
+			               eth_to, knot_wire_get_id(pkt->wire));
 		if (http_ctx)
 			queue_pop(http_ctx->streams);
 		if (!ctx)
@@ -1787,7 +1835,7 @@ int worker_submit(struct session *sessio
 		}
 		if (kr_fails_assert(!session_flags(session)->closing))
 			return kr_error(EINVAL);
-		addr = peer;
+		addr = (comm) ? comm->src_addr : NULL;
 		/* Note receive time for RTT calculation */
 		task->recv_time = kr_now();
 	}
@@ -1802,77 +1850,83 @@ int worker_submit(struct session *sessio
 	return qr_task_step(task, addr, pkt);
 }
 
-static int map_add_tcp_session(map_t *map, const struct sockaddr* addr,
-			       struct session *session)
+static int trie_add_tcp_session(trie_t *trie, const struct sockaddr *addr,
+                                struct session *session)
 {
-	if (kr_fails_assert(map && addr))
+	if (kr_fails_assert(trie && addr))
 		return kr_error(EINVAL);
-	const char *key = tcpsess_key(addr);
-	if (kr_fails_assert(key && map_contains(map, key) == 0))
+	struct kr_sockaddr_key_storage key;
+	ssize_t keylen = kr_sockaddr_key(&key, addr);
+	if (keylen < 0)
+		return keylen;
+	trie_val_t *val = trie_get_ins(trie, key.bytes, keylen);
+	if (kr_fails_assert(*val == NULL))
 		return kr_error(EINVAL);
-	int ret = map_set(map, key, session);
-	return ret ? kr_error(EINVAL) : kr_ok();
+	*val = session;
+	return kr_ok();
 }
 
-static int map_del_tcp_session(map_t *map, const struct sockaddr* addr)
+static int trie_del_tcp_session(trie_t *trie, const struct sockaddr *addr)
 {
-	if (kr_fails_assert(map && addr))
-		return kr_error(EINVAL);
-	const char *key = tcpsess_key(addr);
-	if (kr_fails_assert(key))
+	if (kr_fails_assert(trie && addr))
 		return kr_error(EINVAL);
-	int ret = map_del(map, key);
+	struct kr_sockaddr_key_storage key;
+	ssize_t keylen = kr_sockaddr_key(&key, addr);
+	if (keylen < 0)
+		return keylen;
+	int ret = trie_del(trie, key.bytes, keylen, NULL);
 	return ret ? kr_error(ENOENT) : kr_ok();
 }
 
-static struct session* map_find_tcp_session(map_t *map,
-					    const struct sockaddr *addr)
+static struct session *trie_find_tcp_session(trie_t *trie,
+                                             const struct sockaddr *addr)
 {
-	if (kr_fails_assert(map && addr))
+	if (kr_fails_assert(trie && addr))
 		return NULL;
-	const char *key = tcpsess_key(addr);
-	if (kr_fails_assert(key))
+	struct kr_sockaddr_key_storage key;
+	ssize_t keylen = kr_sockaddr_key(&key, addr);
+	if (keylen < 0)
 		return NULL;
-	struct session* ret = map_get(map, key);
-	return ret;
+	trie_val_t *val = trie_get_try(trie, key.bytes, keylen);
+	return val ? *val : NULL;
 }
 
 int worker_add_tcp_connected(struct worker_ctx *worker,
 				    const struct sockaddr* addr,
 				    struct session *session)
 {
-	return map_add_tcp_session(&worker->tcp_connected, addr, session);
+	return trie_add_tcp_session(worker->tcp_connected, addr, session);
 }
 
 int worker_del_tcp_connected(struct worker_ctx *worker,
 				    const struct sockaddr* addr)
 {
-	return map_del_tcp_session(&worker->tcp_connected, addr);
+	return trie_del_tcp_session(worker->tcp_connected, addr);
 }
 
 struct session* worker_find_tcp_connected(struct worker_ctx *worker,
 						 const struct sockaddr* addr)
 {
-	return map_find_tcp_session(&worker->tcp_connected, addr);
+	return trie_find_tcp_session(worker->tcp_connected, addr);
 }
 
 static int worker_add_tcp_waiting(struct worker_ctx *worker,
 				  const struct sockaddr* addr,
 				  struct session *session)
 {
-	return map_add_tcp_session(&worker->tcp_waiting, addr, session);
+	return trie_add_tcp_session(worker->tcp_waiting, addr, session);
 }
 
 int worker_del_tcp_waiting(struct worker_ctx *worker,
 			   const struct sockaddr* addr)
 {
-	return map_del_tcp_session(&worker->tcp_waiting, addr);
+	return trie_del_tcp_session(worker->tcp_waiting, addr);
 }
 
 struct session* worker_find_tcp_waiting(struct worker_ctx *worker,
 					       const struct sockaddr* addr)
 {
-	return map_find_tcp_session(&worker->tcp_waiting, addr);
+	return trie_find_tcp_session(worker->tcp_waiting, addr);
 }
 
 int worker_end_tcp(struct session *session)
@@ -1995,8 +2049,8 @@ struct qr_task *worker_resolve_start(kno
 		return NULL;
 
 
-	struct request_ctx *ctx = request_create(worker, NULL, NULL, NULL, NULL, NULL,
-						 worker->next_request_uid);
+	struct request_ctx *ctx = request_create(worker, NULL, NULL, NULL, NULL,
+	                                         worker->next_request_uid);
 	if (!ctx)
 		return NULL;
 
@@ -2128,8 +2182,8 @@ bool worker_task_finished(struct qr_task
 /** Reserve worker buffers.  We assume worker's been zeroed. */
 static int worker_reserve(struct worker_ctx *worker, size_t ring_maxlen)
 {
-	worker->tcp_connected = map_make(NULL);
-	worker->tcp_waiting = map_make(NULL);
+	worker->tcp_connected = trie_create(NULL);
+	worker->tcp_waiting = trie_create(NULL);
 	worker->subreq_out = trie_create(NULL);
 
 	array_init(worker->pool_mp);
@@ -2157,12 +2211,8 @@ void worker_deinit(void)
 	struct worker_ctx *worker = the_worker;
 	if (kr_fails_assert(worker))
 		return;
-	if (worker->z_import != NULL) {
-		zi_free(worker->z_import);
-		worker->z_import = NULL;
-	}
-	map_clear(&worker->tcp_connected);
-	map_clear(&worker->tcp_waiting);
+	trie_free(worker->tcp_connected);
+	trie_free(worker->tcp_waiting);
 	trie_free(worker->subreq_out);
 	worker->subreq_out = NULL;
 
diff -pruN 5.4.4-1/daemon/worker.h 5.5.1-5/daemon/worker.h
--- 5.4.4-1/daemon/worker.h	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/worker.h	2022-06-14 07:17:30.387490500 +0000
@@ -6,7 +6,7 @@
 
 #include "daemon/engine.h"
 #include "lib/generic/array.h"
-#include "lib/generic/map.h"
+#include "lib/generic/trie.h"
 
 
 /** Query resolution task (opaque). */
@@ -17,6 +17,8 @@ struct worker_ctx;
 struct session;
 /** Zone import context (opaque). */
 struct zone_import_ctx;
+/** Data about the communication (defined in io.h). */
+struct io_comm_data;
 
 /** Pointer to the singleton worker.  NULL if not initialized. */
 KR_EXPORT extern struct worker_ctx *the_worker;
@@ -31,15 +33,14 @@ void worker_deinit(void);
 /**
  * Process an incoming packet (query from a client or answer from upstream).
  *
- * @param session  session the packet came from, or NULL (not from network)
- * @param peer     address the packet came from, or NULL (not from network)
- * @param eth_*    MAC addresses or NULL (they're useful for XDP)
- * @param pkt      the packet, or NULL (an error from the transport layer)
+ * @param session     session the packet came from, or NULL (not from network)
+ * @param comm        IO communication data (see `struct io_comm_data` docs)
+ * @param eth_*       MAC addresses or NULL (they're useful for XDP)
+ * @param pkt         the packet, or NULL (an error from the transport layer)
  * @return 0 or an error code
  */
-int worker_submit(struct session *session,
-		  const struct sockaddr *peer, const struct sockaddr *dst_addr,
-		  const uint8_t *eth_from, const uint8_t *eth_to, knot_pkt_t *pkt);
+int worker_submit(struct session *session, struct io_comm_data *comm,
+                  const uint8_t *eth_from, const uint8_t *eth_to, knot_pkt_t *pkt);
 
 /**
  * End current DNS/TCP session, this disassociates pending tasks from this session
@@ -176,13 +177,12 @@ struct worker_ctx {
 
 	struct worker_stats stats;
 
-	struct zone_import_ctx* z_import;
 	bool too_many_open;
 	size_t rconcurrent_highwatermark;
 	/** List of active outbound TCP sessions */
-	map_t tcp_connected;
+	trie_t *tcp_connected;
 	/** List of outbound TCP sessions waiting to be accepted */
-	map_t tcp_waiting;
+	trie_t *tcp_waiting;
 	/** Subrequest leaders (struct qr_task*), indexed by qname+qtype+qclass. */
 	trie_t *subreq_out;
 	mp_freelist_t pool_mp;
diff -pruN 5.4.4-1/daemon/zimport.c 5.5.1-5/daemon/zimport.c
--- 5.4.4-1/daemon/zimport.c	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/zimport.c	2022-06-14 07:17:30.387490500 +0000
@@ -8,605 +8,430 @@
  * For now only root zone import is supported.
  *
  * Import process consists of two stages.
- * 1) Zone file parsing.
- * 2) Import of parsed entries into the cache.
+ * 1) Zone file parsing and (optionally) ZONEMD verification.
+ * 2) DNSSEC validation and storage in cache.
  *
  * These stages are implemented as two separate functions
- * (zi_zone_import and zi_zone_process) which runs sequentially with the
+ * (zi_zone_import and zi_zone_process) which run sequentially with a
  * pause between them. This is done because resolver is a single-threaded
  * application, so it can't process user's requests during the whole import
  * process. Separation into two stages allows to reduce the
  * continuous time interval when resolver can't serve user requests.
- * Since root zone isn't large it is imported as single
- * chunk. If it would be considered as necessary, import stage can be
- * split into shorter stages.
- *
- * zi_zone_import() uses libzscanner to parse zone file.
- * Parsed records are stored to internal storage from where they are imported to
- * cache during the second stage.
- *
- * zi_zone_process() imports parsed resource records to cache.
- * It imports rrset by creating request that will never be sent to upstream.
- * After request creation resolver creates pseudo-answer which must contain
- * all necessary data for validation. Then resolver process answer as if he had
- * been received from network.
+ * Since root zone isn't large, it is imported as single chunk.
  */
 
+#include "daemon/zimport.h"
+
 #include <inttypes.h> /* PRIu64 */
 #include <limits.h>
+#include <math.h>
 #include <stdlib.h>
+#include <sys/stat.h>
 #include <uv.h>
-#include <ucw/mempool.h>
+
 #include <libknot/rrset.h>
 #include <libzscanner/scanner.h>
 
-#include "lib/utils.h"
-#include "lib/dnssec/ta.h"
-#include "daemon/worker.h"
-#include "daemon/zimport.h"
-#include "lib/generic/map.h"
-#include "lib/generic/array.h"
+#include <libknot/version.h>
+#define ENABLE_ZONEMD (KNOT_VERSION_HEX >= 0x030100)
+#if ENABLE_ZONEMD
+	#include <libdnssec/digest.h>
+
+	#if KNOT_VERSION_HEX < 0x030200
+		#define KNOT_ZONEMD_ALGORITHM_SHA384 KNOT_ZONEMD_ALORITHM_SHA384
+		#define KNOT_ZONEMD_ALGORITHM_SHA512 KNOT_ZONEMD_ALORITHM_SHA512
+	#endif
+#endif
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE(qry, ZIMPORT, __VA_ARGS__)
+#include "daemon/worker.h"
+#include "lib/dnssec/ta.h"
+#include "lib/dnssec.h"
+#include "lib/generic/trie.h"
+#include "lib/utils.h"
 
-/* Pause between parse and import stages, milliseconds.
- * See comment in zi_zone_import() */
+/* Pause between parse and import stages, milliseconds. */
 #define ZONE_IMPORT_PAUSE 100
 
-typedef array_t(knot_rrset_t *) qr_rrsetlist_t;
+// NAN normally comes from <math.h> but it's not guaranteed.
+#ifndef NAN
+	#define NAN nan("")
+#endif
 
 struct zone_import_ctx {
-	struct worker_ctx *worker;
-	bool started;
+	knot_mm_t *pool; /// memory pool for all allocations (including struct itself)
 	knot_dname_t *origin;
-	knot_rrset_t *ta;
-	knot_rrset_t *key;
-	uint64_t start_timestamp;
-	size_t rrset_idx;
 	uv_timer_t timer;
-	map_t rrset_indexed;
-	qr_rrsetlist_t rrset_sorted;
-	knot_mm_t pool;
+
+	// from zi_config_t
 	zi_callback cb;
 	void *cb_param;
-};
-
-typedef struct zone_import_ctx zone_import_ctx_t;
-
-static int RRSET_IS_ALREADY_IMPORTED = 1;
-
-/** @internal Allocate zone import context.
- * @return pointer to zone import context or NULL. */
-static zone_import_ctx_t *zi_ctx_alloc()
-{
-	return calloc(1, sizeof(zone_import_ctx_t));
-}
-
-/** @internal Free zone import context. */
-static void zi_ctx_free(zone_import_ctx_t *z_import)
-{
-	if (z_import != NULL) {
-		free(z_import);
-	}
-}
-
-/** @internal Reset all fields in the zone import context to their default values.
- * Flushes memory pool, but doesn't reallocate memory pool buffer.
- * Doesn't affect timer handle, pointers to callback and callback parameter.
- * @return 0 if success; -1 if failed. */
-static int zi_reset(struct zone_import_ctx *z_import, size_t rrset_sorted_list_size)
-{
-	mp_flush(z_import->pool.ctx);
 
-	z_import->started = false;
-	z_import->start_timestamp = 0;
-	z_import->rrset_idx = 0;
-	z_import->pool.alloc = (knot_mm_alloc_t) mp_alloc;
-	z_import->rrset_indexed = map_make(&z_import->pool);
+	trie_t *rrsets; /// map: key_get() -> knot_rrset_t*, in ZONEMD order
+	uint32_t timestamp_rr; /// stamp of when RR data arrived (seconds since epoch)
 
-	array_init(z_import->rrset_sorted);
+	struct kr_svldr_ctx *svldr; /// DNSSEC validator; NULL iff we don't validate
+	const knot_dname_t *last_cut; /// internal to zi_rrset_import()
 
-	int ret = 0;
-	if (rrset_sorted_list_size) {
-		ret = array_reserve_mm(z_import->rrset_sorted, rrset_sorted_list_size,
-				       kr_memreserve, &z_import->pool);
-	}
+#if ENABLE_ZONEMD
+	uint8_t *digest_buf; /// temporary buffer for digest computation (on pool)
+	#define DIGEST_BUF_SIZE (64*1024 - 1)
+	#define DIGEST_ALG_COUNT 2
+	struct {
+		bool active; /// whether we want it computed
+		dnssec_digest_ctx_t *ctx;
+		const uint8_t *expected; /// expected digest (inside zonemd on pool)
+	} digests[DIGEST_ALG_COUNT]; /// we use indices 0 and 1 for SHA 384 and 512
+#endif
+};
 
-	return ret;
-}
+typedef struct zone_import_ctx zone_import_ctx_t;
 
-/** @internal Close callback for timer handle.
- * @note Actually frees zone import context. */
-static void on_timer_close(uv_handle_t *handle)
-{
-	zone_import_ctx_t *z_import = (zone_import_ctx_t *)handle->data;
-	if (z_import != NULL) {
-		zi_ctx_free(z_import);
-	}
-}
 
-zone_import_ctx_t *zi_allocate(struct worker_ctx *worker,
-			       zi_callback cb, void *param)
+#define KEY_LEN (KNOT_DNAME_MAXLEN + 1 + 2 + 2)
+/** Construct key for name, type and signed type (if type == RRSIG).
+ *
+ * Return negative error code in asserted cases.
+ */
+static int key_get(char buf[KEY_LEN], const knot_dname_t *name,
+		uint16_t type, uint16_t type_maysig, char **key_p)
 {
-	if (worker->loop == NULL) {
+	char *lf = (char *)knot_dname_lf(name, (uint8_t *)buf);
+	if (kr_fails_assert(lf && key_p))
+		return kr_error(EINVAL);
+	int len = lf[0];
+	lf++;  // point to start of data
+	*key_p = lf;
+	// Check that LF is right-aligned to KNOT_DNAME_MAXLEN in buf.
+	if (kr_fails_assert(lf + len == buf + KNOT_DNAME_MAXLEN))
+		return kr_error(EINVAL);
+	buf[KNOT_DNAME_MAXLEN] = 0;  // this ensures correct ZONEMD order
+	memcpy(buf + KNOT_DNAME_MAXLEN + 1, &type, sizeof(type));
+	len += 1 + sizeof(type);
+	if (type == KNOT_RRTYPE_RRSIG) {
+		memcpy(buf + KNOT_DNAME_MAXLEN + 1 + sizeof(type),
+			&type_maysig, sizeof(type_maysig));
+		len += sizeof(type_maysig);
+	}
+	return len;
+}
+
+/** Simple helper to retreive from zone_import_ctx_t::rrsets */
+static knot_rrset_t * rrset_get(trie_t *rrsets, const knot_dname_t *name,
+				uint16_t type, uint16_t type_maysig)
+{
+	char key_buf[KEY_LEN], *key;
+	const int len = key_get(key_buf, name, type, type_maysig, &key);
+	if (len < 0)
 		return NULL;
-	}
-	zone_import_ctx_t *z_import = zi_ctx_alloc();
-	if (!z_import) {
-		return NULL;
-	}
-	void *mp = mp_new (8192);
-	if (!mp) {
-		zi_ctx_free(z_import);
+	const trie_val_t *rrsig_p = trie_get_try(rrsets, key, len);
+	if (!rrsig_p)
 		return NULL;
-	}
-	z_import->pool.ctx = mp;
-	z_import->worker = worker;
-	int ret = zi_reset(z_import, 0);
-	if (ret < 0) {
-		mp_delete(mp);
-		zi_ctx_free(z_import);
-		return NULL;
-	}
-	uv_timer_init(z_import->worker->loop, &z_import->timer);
-	z_import->timer.data = z_import;
-	z_import->cb = cb;
-	z_import->cb_param = param;
-	return z_import;
-}
-
-void zi_free(zone_import_ctx_t *z_import)
-{
-	z_import->started = false;
-	z_import->start_timestamp = 0;
-	z_import->rrset_idx = 0;
-	mp_delete(z_import->pool.ctx);
-	z_import->pool.ctx = NULL;
-	z_import->pool.alloc = NULL;
-	z_import->worker = NULL;
-	z_import->cb = NULL;
-	z_import->cb_param = NULL;
-	uv_close((uv_handle_t *)&z_import->timer, on_timer_close);
-}
-
-/** @internal Mark rrset that has been already imported
- *  to avoid repeated import. */
-static inline void zi_rrset_mark_as_imported(knot_rrset_t *rr)
-{
-	rr->additional = (void *)&RRSET_IS_ALREADY_IMPORTED;
+	kr_assert(*rrsig_p);
+	return *rrsig_p;
 }
 
-/** @internal Check if rrset is marked as "already imported".
- * @return true if marked, false if isn't */
-static inline bool zi_rrset_is_marked_as_imported(knot_rrset_t *rr)
+#if ENABLE_ZONEMD
+static int digest_rrset(trie_val_t *rr_p, void *z_import_v)
 {
-	return (rr->additional == &RRSET_IS_ALREADY_IMPORTED);
-}
-
-/** @internal Try to find rrset with given requisites amongst parsed rrsets
- * and put it to given packet. If there is RRSIG which covers that rrset, it
- * will be added as well. If rrset found and successfully put, it marked as
- * "already imported" to avoid repeated import. The same is true for RRSIG.
- * @return -1 if failed
- *          0 if required record been actually put into the packet
- *          1 if required record could not be found */
-static int zi_rrset_find_put(struct zone_import_ctx *z_import,
-			     knot_pkt_t *pkt, const knot_dname_t *owner,
-			     uint16_t class, uint16_t type, uint16_t additional)
-{
-	if (type != KNOT_RRTYPE_RRSIG) {
-		/* If required rrset isn't rrsig, these must be the same values */
-		additional = type;
-	}
-
-	char key[KR_RRKEY_LEN];
-	int err = kr_rrkey(key, class, owner, type, additional);
-	if (err <= 0) {
-		return -1;
-	}
-	knot_rrset_t *rr = map_get(&z_import->rrset_indexed, key);
-	if (!rr) {
-		return 1;
-	}
-	err = knot_pkt_put(pkt, 0, rr, 0);
-	if (err != KNOT_EOK) {
-		return -1;
-	}
-	zi_rrset_mark_as_imported(rr);
-
-	if (type != KNOT_RRTYPE_RRSIG) {
-		/* Try to find corresponding rrsig */
-		err = zi_rrset_find_put(z_import, pkt, owner,
-					class, KNOT_RRTYPE_RRSIG, type);
-		if (err < 0) {
-			return err;
-		}
+	zone_import_ctx_t *z_import = z_import_v;
+	const knot_rrset_t *rr = *rr_p;
+
+	// ignore apex ZONEMD or its RRSIG, and also out of bailiwick records
+	const int origin_bailiwick = knot_dname_in_bailiwick(rr->owner, z_import->origin);
+	const bool is_apex = origin_bailiwick == 0;
+	if (is_apex && kr_rrset_type_maysig(rr) == KNOT_RRTYPE_ZONEMD)
+		return KNOT_EOK;
+	if (unlikely(origin_bailiwick < 0))
+		return KNOT_EOK;
+
+	const int len = knot_rrset_to_wire_extra(rr, z_import->digest_buf, DIGEST_BUF_SIZE,
+						 0, NULL, KNOT_PF_ORIGTTL);
+	if (len < 0)
+		return kr_error(len);
+
+	// digest serialized RRSet
+	for (int i = 0; i < DIGEST_ALG_COUNT; ++i) {
+		if (!z_import->digests[i].active)
+			continue;
+		dnssec_binary_t bufbin = { len, z_import->digest_buf };
+		int ret = dnssec_digest(z_import->digests[i].ctx, &bufbin);
+		if (ret != KNOT_EOK)
+			return kr_error(ret);
 	}
-
-	return 0;
+	return KNOT_EOK;
 }
 
-/** @internal Try to put given rrset to the given packet.
- * If there is RRSIG which covers that rrset, it will be added as well.
- * If rrset successfully put in the packet, it marked as
- * "already imported" to avoid repeated import.
- * The same is true for RRSIG.
- * @return -1 if failed
- *          0 if required record been actually put into the packet */
-static int zi_rrset_put(struct zone_import_ctx *z_import, knot_pkt_t *pkt,
-			knot_rrset_t *rr)
+/** Verify ZONEMD in the stored zone, and return error code.
+ *
+ * ZONEMD signature is verified iff z_import->svldr != NULL
+   https://www.rfc-editor.org/rfc/rfc8976.html#name-verifying-zone-digest
+ */
+static int zonemd_verify(zone_import_ctx_t *z_import)
 {
-	if (kr_fails_assert(rr && rr->type != KNOT_RRTYPE_RRSIG))
-		return -1;
-	int err = knot_pkt_put(pkt, 0, rr, 0);
-	if (err != KNOT_EOK) {
-		return -1;
-	}
-	zi_rrset_mark_as_imported(rr);
-	/* Try to find corresponding RRSIG */
-	err = zi_rrset_find_put(z_import, pkt, rr->owner, rr->rclass,
-				KNOT_RRTYPE_RRSIG, rr->type);
-	return (err < 0) ? err : 0;
-}
-
-/** @internal Try to put DS & NSEC* for rset->owner to given packet.
- * @return -1 if failed;
- *          0 if no errors occurred (it doesn't mean
- *            that records were actually added). */
-static int zi_put_delegation(zone_import_ctx_t *z_import, knot_pkt_t *pkt,
-			     knot_rrset_t *rr)
-{
-	int err = zi_rrset_find_put(z_import, pkt, rr->owner,
-				    rr->rclass, KNOT_RRTYPE_DS, 0);
-	if (err == 1) {
-		/* DS not found, maybe there are NSEC* */
-		err = zi_rrset_find_put(z_import, pkt, rr->owner,
-					rr->rclass, KNOT_RRTYPE_NSEC, 0);
-		if (err >= 0) {
-			err = zi_rrset_find_put(z_import, pkt, rr->owner,
-						rr->rclass, KNOT_RRTYPE_NSEC3, 0);
+	bool zonemd_is_valid = false;
+	// Find ZONEMD RR + RRSIG
+	knot_rrset_t * const rr_zonemd
+		= rrset_get(z_import->rrsets, z_import->origin, KNOT_RRTYPE_ZONEMD, 0);
+	if (!rr_zonemd) {
+		// no zonemd; let's compute the shorter digest and print info later
+		z_import->digests[KNOT_ZONEMD_ALGORITHM_SHA384 - 1].active = true;
+		goto do_digest;
+	}
+	// Validate ZONEMD RRSIG, if desired
+	if (z_import->svldr) {
+		const knot_rrset_t *rrsig_zonemd
+			= rrset_get(z_import->rrsets, z_import->origin,
+					KNOT_RRTYPE_RRSIG, KNOT_RRTYPE_ZONEMD);
+		int ret = rrsig_zonemd
+			? kr_svldr_rrset(rr_zonemd, &rrsig_zonemd->rrs, z_import->svldr)
+			: kr_error(ENOENT);
+		zonemd_is_valid = (ret == kr_ok());
+
+		if (!rrsig_zonemd) {
+			kr_log_error(PREFILL, "ZONEMD signature missing\n");
+		} else if (!zonemd_is_valid) {
+			kr_log_error(PREFILL, "ZONEMD signature failed to validate\n");
 		}
 	}
-	return err < 0 ? err : 0;
-}
 
-/** @internal Try to put A & AAAA records for rset->owner to given packet.
- * @return -1 if failed;
- *          0 if no errors occurred (it doesn't mean
- *            that records were actually added). */
-static int zi_put_glue(zone_import_ctx_t *z_import, knot_pkt_t *pkt,
-			     knot_rrset_t *rr)
-{
-	int err = 0;
-	knot_rdata_t *rdata_i = rr->rrs.rdata;
-	for (uint16_t i = 0; i < rr->rrs.count;
-			++i, rdata_i = knot_rdataset_next(rdata_i)) {
-		const knot_dname_t *ns_name = knot_ns_name(rdata_i);
-		err = zi_rrset_find_put(z_import, pkt, ns_name,
-					rr->rclass, KNOT_RRTYPE_A, 0);
-		if (err < 0) {
-			break;
-		}
-
-		err = zi_rrset_find_put(z_import, pkt, ns_name,
-					rr->rclass, KNOT_RRTYPE_AAAA, 0);
-		if (err < 0) {
-			break;
+	// Get SOA serial
+	const knot_rrset_t *soa = rrset_get(z_import->rrsets, z_import->origin,
+						KNOT_RRTYPE_SOA, 0);
+	if (!soa) {
+		kr_log_error(PREFILL, "SOA record not found\n");
+		return kr_error(ENOENT);
+	}
+	if (soa->rrs.count != 1) {
+		kr_log_error(PREFILL, "the SOA RR set is weird\n");
+		return kr_error(EINVAL);
+	} // length is checked by parser already
+	const uint32_t soa_serial = knot_soa_serial(soa->rrs.rdata);
+
+	// Figure out SOA+ZONEMD RR contents.
+	bool some_active = false;
+	knot_rdata_t *rd = rr_zonemd->rrs.rdata;
+	for (int i = 0; i < rr_zonemd->rrs.count; ++i, rd = knot_rdataset_next(rd)) {
+		if (rd->len < 6 || knot_zonemd_scheme(rd) != KNOT_ZONEMD_SCHEME_SIMPLE
+		    || knot_zonemd_soa_serial(rd) != soa_serial)
+			continue;
+		const int algo = knot_zonemd_algorithm(rd);
+		if (algo != KNOT_ZONEMD_ALGORITHM_SHA384 && algo != KNOT_ZONEMD_ALGORITHM_SHA512)
+			continue;
+		if (rd->len != 6 + knot_zonemd_digest_size(rd)) {
+			kr_log_error(PREFILL, "ZONEMD record has incorrect digest length\n");
+			return kr_error(EINVAL);
+		}
+		if (z_import->digests[algo - 1].active) {
+			kr_log_error(PREFILL, "multiple clashing ZONEMD records found\n");
+			return kr_error(EINVAL);
+		}
+		some_active = true;
+		z_import->digests[algo - 1].active = true;
+		z_import->digests[algo - 1].expected = knot_zonemd_digest(rd);
+	}
+	if (!some_active) {
+		kr_log_error(PREFILL, "ZONEMD record(s) found but none were usable\n");
+		return kr_error(ENOENT);
+	}
+do_digest:
+	// Init memory, etc.
+	if (!z_import->digest_buf) {
+		z_import->digest_buf = mm_alloc(z_import->pool, DIGEST_BUF_SIZE);
+		if (!z_import->digest_buf)
+			return kr_error(ENOMEM);
+	}
+	for (int i = 0; i < DIGEST_ALG_COUNT; ++i) {
+		const int algo = i + 1;
+		if (!z_import->digests[i].active)
+			continue;
+		int ret = dnssec_digest_init(algo, &z_import->digests[i].ctx);
+		if (ret != KNOT_EOK) {
+			// free previous successful _ctx, if applicable
+			dnssec_binary_t digest = { 0 };
+			while (--i >= 0) {
+				if (z_import->digests[i].active)
+					dnssec_digest_finish(z_import->digests[i].ctx,
+								&digest);
+			}
+			return kr_error(ENOMEM);
 		}
 	}
-	return err < 0 ? err : 0;
-}
-
-/** @internal Create query. */
-static knot_pkt_t *zi_query_create(zone_import_ctx_t *z_import, knot_rrset_t *rr)
-{
-	knot_mm_t *pool = &z_import->pool;
-
-	uint32_t msgid = kr_rand_bytes(2);
-
-	knot_pkt_t *query = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, pool);
-	if (!query) {
-		return NULL;
-	}
-
-	knot_pkt_put_question(query, rr->owner, rr->rclass, rr->type);
-	knot_pkt_begin(query, KNOT_ANSWER);
-	knot_wire_set_rd(query->wire);
-	knot_wire_set_id(query->wire, msgid);
-	int err = knot_pkt_parse(query, 0);
-	if (err != KNOT_EOK) {
-		knot_pkt_free(query);
-		return NULL;
+	// Actually compute the digest(s).
+	int ret = trie_apply(z_import->rrsets, digest_rrset, z_import);
+	dnssec_binary_t digs[DIGEST_ALG_COUNT] = { { 0 } };
+	for (int i = 0; i < DIGEST_ALG_COUNT; ++i) {
+		if (!z_import->digests[i].active)
+			continue;
+		int ret2 = dnssec_digest_finish(z_import->digests[i].ctx, &digs[i]);
+		if (ret == DNSSEC_EOK)
+			ret = ret2;
+		// we need to keep going to free all digests[*].ctx
+	}
+	if (ret != DNSSEC_EOK) {
+		for (int i = 0; i < DIGEST_ALG_COUNT; ++i)
+			free(digs[i].data);
+		kr_log_error(PREFILL, "error when computing digest: %s\n",
+				kr_strerror(ret));
+		return kr_error(ret);
+	}
+	// Now only check that one of the hashes match.
+	bool has_match = false;
+	for (int i = 0; i < DIGEST_ALG_COUNT; ++i) {
+		if (!z_import->digests[i].active)
+			continue;
+		// hexdump the hash for logging
+		char hash_str[digs[i].size * 2 + 1];
+		for (ssize_t j = 0; j < digs[i].size; ++j)
+			sprintf(hash_str + 2*j, "%02x", digs[i].data[j]);
+
+		if (!z_import->digests[i].expected) {
+			kr_log_error(PREFILL, "no ZONEMD found; computed hash: %s\n",
+					hash_str);
+		} else if (memcmp(z_import->digests[i].expected, digs[i].data,
+					digs[i].size) != 0) {
+			kr_log_error(PREFILL, "ZONEMD hash mismatch; computed hash: %s\n",
+					hash_str);
+		} else {
+			kr_log_debug(PREFILL, "ZONEMD hash matches\n");
+			has_match = true;
+			continue;
+		}
 	}
 
-	return query;
+	for (int i = 0; i < DIGEST_ALG_COUNT; ++i)
+		free(digs[i].data);
+	bool ok = has_match && (zonemd_is_valid || !z_import->svldr);
+	return ok ? kr_ok() : kr_error(ENOENT);
 }
+#endif
 
-/** @internal Import given rrset to cache.
- * @return -1 if failed; 0 if success */
-static int zi_rrset_import(zone_import_ctx_t *z_import, knot_rrset_t *rr)
-{
-	/* Create "pseudo query" which asks for given rrset. */
-	knot_pkt_t *query = zi_query_create(z_import, rr);
-	if (!query) {
-		return -1;
-	}
-
-	knot_mm_t *pool = &z_import->pool;
-	uint8_t *dname = rr->owner;
-	uint16_t rrtype = rr->type;
-	uint16_t rrclass = rr->rclass;
-
-	/* Create "pseudo answer". */
-	knot_pkt_t *answer = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, pool);
-	if (!answer) {
-		knot_pkt_free(query);
-		return -1;
-	}
-	knot_pkt_put_question(answer, dname, rrclass, rrtype);
-	knot_pkt_begin(answer, KNOT_ANSWER);
 
-	struct kr_qflags options = { 0 };
-	options.DNSSEC_WANT = true;
-	options.NO_MINIMIZE = true;
-
-	/* This call creates internal structures which necessary for
-	 * resolving - qr_task & request_ctx. */
-	struct qr_task *task = worker_resolve_start(query, options);
-	if (!task) {
-		knot_pkt_free(query);
-		knot_pkt_free(answer);
-		return -1;
-	}
+/**
+ * @internal Import given rrset to cache.
+ *
+ * @return error code; we could've chosen to keep importing even if some RRset fails,
+ *   but it would be harder to ensure that we don't generate too many logs
+ *   and that we pass an error to the finishing callback.
+ */
+static int zi_rrset_import(trie_val_t *rr_p, void *z_import_v)
+{
+	zone_import_ctx_t *z_import = z_import_v;
+	knot_rrset_t *rr = *rr_p;
 
-	/* Push query to the request resolve plan.
-	 * Actually query will never been sent to upstream. */
-	struct kr_request *request = worker_task_request(task);
-	struct kr_rplan *rplan = &request->rplan;
-	struct kr_query *qry = kr_rplan_push(rplan, NULL, dname, rrclass, rrtype);
-	int state = KR_STATE_FAIL;
-	bool origin_is_owner = knot_dname_is_equal(rr->owner, z_import->origin);
-	bool is_referral = (rrtype == KNOT_RRTYPE_NS && !origin_is_owner);
-	uint32_t msgid = knot_wire_get_id(query->wire);
-
-	qry->id = msgid;
-
-	/* Prepare zonecut. It must have all the necessary requisites for
-	 * successful validation - matched zone name & keys & trust-anchors. */
-	kr_zonecut_init(&qry->zone_cut, z_import->origin, pool);
-	qry->zone_cut.key = z_import->key;
-	qry->zone_cut.trust_anchor = z_import->ta;
-
-	if (knot_pkt_init_response(answer, query) != 0) {
-		goto cleanup;
-	}
-
-	/* Since "pseudo" query asks for NS for subzone,
-	 * "pseudo" answer must simulate referral. */
-	if (is_referral) {
-		knot_pkt_begin(answer, KNOT_AUTHORITY);
-	}
-
-	/* Put target rrset to ANSWER\AUTHORITY as well as corresponding RRSIG */
-	int err = zi_rrset_put(z_import, answer, rr);
-	if (err != 0) {
-		goto cleanup;
-	}
+	if (rr->type == KNOT_RRTYPE_RRSIG)
+		return 0; // we do RRSIGs at once with their types
 
-	if (!is_referral) {
-		knot_wire_set_aa(answer->wire);
+	const int origin_bailiwick = knot_dname_in_bailiwick(rr->owner, z_import->origin);
+	if (unlikely(origin_bailiwick < 0)) {
+		KR_DNAME_GET_STR(owner_str, rr->owner);
+		kr_log_warning(PREFILL, "ignoring out of bailiwick record(s) on %s\n",
+				owner_str);
+		return 0; // well, let's continue without error
+	}
+
+	// Determine if this RRset is authoritative.
+	// We utilize that iteration happens in canonical order.
+	bool is_auth;
+	const int kdib = knot_dname_in_bailiwick(rr->owner, z_import->last_cut);
+	if (kdib == 0 && (rr->type == KNOT_RRTYPE_DS || rr->type == KNOT_RRTYPE_NSEC
+				|| rr->type == KNOT_RRTYPE_NSEC3)) {
+		// parent side of the zone cut (well, presumably in case of NSEC*)
+		is_auth = true;
+	} else if (kdib >= 0) {
+		// inside non-auth subtree
+		is_auth = false;
+	} else if (rr->type == KNOT_RRTYPE_NS && origin_bailiwick > 0) {
+		// entering non-auth subtree
+		z_import->last_cut = rr->owner;
+		is_auth = false;
 	} else {
-		/* Type is KNOT_RRTYPE_NS and owner is not equal to origin.
-		 * It will be "referral" answer and must contain delegation. */
-		err = zi_put_delegation(z_import, answer, rr);
-		if (err < 0) {
-			goto cleanup;
+		// outside non-auth subtree
+		is_auth = true;
+		z_import->last_cut = NULL; // so that the next _in_bailiwick() is faster
+	}
+	// Rare case: `A` exactly on zone cut would be misdetected and fail validation;
+	// it's the only type ordered before NS.
+	if (unlikely(is_auth && rr->type < KNOT_RRTYPE_NS)) {
+		if (rrset_get(z_import->rrsets, rr->owner, KNOT_RRTYPE_NS, 0))
+			is_auth = false;
+	}
+
+	// Get and validate the corresponding RRSIGs, if authoritative.
+	const knot_rrset_t *rrsig = NULL;
+	if (is_auth) {
+		rrsig = rrset_get(z_import->rrsets, rr->owner, KNOT_RRTYPE_RRSIG, rr->type);
+		if (unlikely(!rrsig && z_import->svldr)) {
+			KR_DNAME_GET_STR(owner_str, rr->owner);
+			KR_RRTYPE_GET_STR(type_str, rr->type);
+			kr_log_error(PREFILL, "no records found for %s RRSIG %s\n",
+					owner_str, type_str);
+			return kr_error(ENOENT);
+		}
+	}
+	if (is_auth && z_import->svldr) {
+		int ret = kr_svldr_rrset(rr, &rrsig->rrs, z_import->svldr);
+		if (unlikely(ret)) {
+			KR_DNAME_GET_STR(owner_str, rr->owner);
+			KR_RRTYPE_GET_STR(type_str, rr->type);
+			kr_log_error(PREFILL, "validation failed for %s %s: %s\n",
+					owner_str, type_str, kr_strerror(ret));
+			return kr_error(ret);
 		}
 	}
 
-	knot_pkt_begin(answer, KNOT_ADDITIONAL);
-
-	if (rrtype == KNOT_RRTYPE_NS) {
-		/* Try to find glue addresses. */
-		err = zi_put_glue(z_import, answer, rr);
-		if (err < 0) {
-			goto cleanup;
-		}
+	uint8_t rank;
+	if (!is_auth) {
+		rank = KR_RANK_OMIT;
+	} else if (z_import->svldr) {
+		rank = KR_RANK_AUTH|KR_RANK_SECURE;
+	} else {
+		rank = KR_RANK_AUTH|KR_RANK_INSECURE;
 	}
 
-	knot_wire_set_id(answer->wire, msgid);
-	answer->parsed = answer->size;
-	err = knot_pkt_parse(answer, 0);
-	if (err != KNOT_EOK) {
-		goto cleanup;
+	int ret = kr_cache_insert_rr(&the_worker->engine->resolver.cache, rr, rrsig,
+					rank, z_import->timestamp_rr,
+					// Optim.: only stash NSEC* params at the apex.
+					origin_bailiwick == 0);
+	if (ret) {
+		kr_log_error(PREFILL, "caching an RRset failed: %s\n",
+				kr_strerror(ret));
+		return kr_error(ret);
 	}
-
-	/* Importing doesn't imply communication with upstream at all.
-	 * "answer" contains pseudo-answer from upstream and must be successfully
-	 * validated in CONSUME stage. If not, something gone wrong. */
-	state = kr_resolve_consume(request, NULL, answer);
-
-cleanup:
-
-	knot_pkt_free(query);
-	knot_pkt_free(answer);
-	worker_task_finalize(task, state);
-	return state == (is_referral ? KR_STATE_PRODUCE : KR_STATE_DONE) ? 0 : -1;
+	return 0; // success
 }
 
-/** @internal Create element in qr_rrsetlist_t rrset_list for
- * given node of map_t rrset_sorted.  */
-static int zi_mapwalk_preprocess(const char *k, void *v, void *baton)
+static void ctx_delete(zone_import_ctx_t *z_import)
 {
-	zone_import_ctx_t *z_import = (zone_import_ctx_t *)baton;
+	if (kr_fails_assert(z_import)) return;
+	kr_svldr_free_ctx(z_import->svldr);
 
-	int ret = array_push_mm(z_import->rrset_sorted, v, kr_memreserve, &z_import->pool);
-
-	return (ret < 0);
+	/* Free `z_import`'s pool, including `z_import` itself, because it is
+	 * allocated inside said pool. */
+	mm_ctx_delete(z_import->pool);
+}
+static void timer_close(uv_handle_t *handle)
+{
+	ctx_delete(handle->data);
 }
 
 /** @internal Iterate over parsed rrsets and try to import each of them. */
-static void zi_zone_process(uv_timer_t* handle)
+static void zi_zone_process(uv_timer_t *timer)
 {
-	zone_import_ctx_t *z_import = (zone_import_ctx_t *)handle->data;
-
-	size_t failed = 0;
-	size_t ns_imported = 0;
-	size_t other_imported = 0;
-
-	if (kr_fails_assert(z_import->worker)) {
-		failed = 1;
-		goto finish;
-	}
-
-	/* At the moment import of root zone only is supported.
-	 * Check the name of the parsed zone.
-	 * TODO - implement importing of arbitrary zone. */
-	KR_DNAME_GET_STR(zone_name_str, z_import->origin);
-
-	if (strcmp(".", zone_name_str) != 0) {
-		kr_log_error(ZIMPORT, "unexpected zone name `%s` (root zone expected), fail\n",
-			     zone_name_str);
-		failed = 1;
-		goto finish;
-	}
-
-	if (z_import->rrset_sorted.len <= 0) {
-		kr_log_error(ZIMPORT, "zone `%s` is empty\n", zone_name_str);
-		goto finish;
-	}
-
-	/* TA have been found, zone is secured.
-	 * DNSKEY must be somewhere amongst the imported records. Find it.
-	 * TODO - For those zones that provenly do not have TA this step must be skipped. */
-	char key[KR_RRKEY_LEN];
-	int err = kr_rrkey(key, KNOT_CLASS_IN, z_import->origin,
-			   KNOT_RRTYPE_DNSKEY, KNOT_RRTYPE_DNSKEY);
-	if (err <= 0) {
-		failed = 1;
-		goto finish;
-	}
-
-	knot_rrset_t *rr_key = map_get(&z_import->rrset_indexed, key);
-	if (!rr_key) {
-		/* DNSKEY MUST be here. If not found - fail. */
-		kr_log_error(ZIMPORT, "DNSKEY not found for `%s`, fail\n", zone_name_str);
-		failed = 1;
-		goto finish;
-	}
-	z_import->key = rr_key;
-
-	map_t *trust_anchors = &z_import->worker->engine->resolver.trust_anchors;
-	knot_rrset_t *rr_ta = kr_ta_get(trust_anchors, z_import->origin);
-	if (!rr_ta) {
-		kr_log_error(ZIMPORT, "error: TA for zone `%s` vanished, fail", zone_name_str);
-		failed = 1;
-		goto finish;
-	}
-	z_import->ta = rr_ta;
-
-	VERBOSE_MSG(NULL, "started: zone: '%s'\n", zone_name_str);
-
-	z_import->start_timestamp = kr_now();
-
-	/* Import DNSKEY at first step. If any validation problems will appear,
-	 * cancel import of whole zone. */
-	KR_DNAME_GET_STR(kname_str, rr_key->owner);
-	KR_RRTYPE_GET_STR(ktype_str, rr_key->type);
-
-	VERBOSE_MSG(NULL, "importing: name: '%s' type: '%s'\n",
-		    kname_str, ktype_str);
-
-	int res = zi_rrset_import(z_import, rr_key);
-	if (res != 0) {
-		kr_log_error(ZIMPORT, "import failed: qname: '%s' type: '%s'\n",
-			    kname_str, ktype_str);
-		failed = 1;
-		goto finish;
-	}
-
-	/* Import all NS records */
-	for (size_t i = 0; i < z_import->rrset_sorted.len; ++i) {
-		knot_rrset_t *rr = z_import->rrset_sorted.at[i];
-
-		if (rr->type != KNOT_RRTYPE_NS) {
-			continue;
-		}
-
-		KR_DNAME_GET_STR(name_str, rr->owner);
-		KR_RRTYPE_GET_STR(type_str, rr->type);
-		VERBOSE_MSG(NULL, "importing: name: '%s' type: '%s'\n",
-			    name_str, type_str);
-		int ret = zi_rrset_import(z_import, rr);
-		if (ret == 0) {
-			++ns_imported;
-		} else {
-			VERBOSE_MSG(NULL, "import failed: name: '%s' type: '%s'\n",
-				    name_str, type_str);
-			++failed;
-		}
-		z_import->rrset_sorted.at[i] = NULL;
-	}
-
-	/* NS records have been imported as well as relative DS, NSEC* and glue.
-	 * Now import what's left. */
-	for (size_t i = 0; i < z_import->rrset_sorted.len; ++i) {
-
-		knot_rrset_t *rr = z_import->rrset_sorted.at[i];
-		if (rr == NULL) {
-			continue;
-		}
-
-		if (zi_rrset_is_marked_as_imported(rr)) {
-			continue;
-		}
-
-		if (rr->type == KNOT_RRTYPE_DNSKEY || rr->type == KNOT_RRTYPE_RRSIG) {
-			continue;
-		}
+	zone_import_ctx_t *z_import = timer->data;
 
-		KR_DNAME_GET_STR(name_str, rr->owner);
-		KR_RRTYPE_GET_STR(type_str, rr->type);
-		VERBOSE_MSG(NULL, "importing: name: '%s' type: '%s'\n",
-			    name_str, type_str);
-		res = zi_rrset_import(z_import, rr);
-		if (res == 0) {
-			++other_imported;
-		} else {
-			VERBOSE_MSG(NULL, "import failed: name: '%s' type: '%s'\n",
-				    name_str, type_str);
-			++failed;
-		}
-	}
+	kr_timer_t stopwatch;
+	kr_timer_start(&stopwatch);
 
-	uint64_t elapsed = kr_now() - z_import->start_timestamp;
-	elapsed = elapsed > UINT_MAX ? UINT_MAX : elapsed;
-
-	VERBOSE_MSG(NULL, "finished in %"PRIu64" ms; zone: `%s`; ns: %zd"
-		    "; other: %zd; failed: %zd\n",
-		    elapsed, zone_name_str, ns_imported, other_imported, failed);
-
-finish:
-
-	uv_timer_stop(&z_import->timer);
-	z_import->started = false;
-
-	int import_state = 0;
-
-	if (failed != 0) {
-		if (ns_imported == 0 && other_imported == 0) {
-			import_state = -1;
-			kr_log_error(ZIMPORT, "import failed; zone `%s` \n", zone_name_str);
-		} else {
-			import_state = 1;
-		}
-	} else {
-		import_state = 0;
+	int ret = trie_apply(z_import->rrsets, zi_rrset_import, z_import);
+	(void)kr_cache_commit(&the_worker->engine->resolver.cache); // RW transaction open
+	if (ret == 0) {
+		kr_log_info(PREFILL, "performance: validating and caching took %.3lf s\n",
+			kr_timer_elapsed(&stopwatch));
 	}
 
-	if (z_import->cb != NULL) {
-		z_import->cb(import_state, z_import->cb_param);
-	}
+	if (z_import->cb)
+		z_import->cb(kr_error(ret), z_import->cb_param);
+	uv_close((uv_handle_t *)timer, timer_close);
 }
 
 /** @internal Store rrset that has been imported to zone import context memory pool.
@@ -615,13 +440,13 @@ static int zi_record_store(zs_scanner_t
 {
 	if (s->r_data_length > UINT16_MAX) {
 		/* Due to knot_rrset_add_rdata(..., const uint16_t size, ...); */
-		kr_log_error(ZSCANNER, "line %"PRIu64": rdata is too long\n",
+		kr_log_error(PREFILL, "line %"PRIu64": rdata is too long\n",
 				s->line_counter);
 		return -1;
 	}
 
 	if (knot_dname_size(s->r_owner) != strlen((const char *)(s->r_owner)) + 1) {
-		kr_log_error(ZSCANNER, "line %"PRIu64
+		kr_log_error(PREFILL, "line %"PRIu64
 				": owner name contains zero byte, skip\n",
 				s->line_counter);
 		return 0;
@@ -630,43 +455,49 @@ static int zi_record_store(zs_scanner_t
 	zone_import_ctx_t *z_import = (zone_import_ctx_t *)s->process.data;
 
 	knot_rrset_t *new_rr = knot_rrset_new(s->r_owner, s->r_type, s->r_class,
-					      s->r_ttl, &z_import->pool);
+					      s->r_ttl, z_import->pool);
 	if (!new_rr) {
-		kr_log_error(ZSCANNER, "line %"PRIu64": error creating rrset\n",
+		kr_log_error(PREFILL, "line %"PRIu64": error creating rrset\n",
 				s->line_counter);
 		return -1;
 	}
 	int res = knot_rrset_add_rdata(new_rr, s->r_data, s->r_data_length,
-				       &z_import->pool);
+				       z_import->pool);
 	if (res != KNOT_EOK) {
-		kr_log_error(ZSCANNER, "line %"PRIu64": error adding rdata to rrset\n",
+		kr_log_error(PREFILL, "line %"PRIu64": error adding rdata to rrset\n",
 				s->line_counter);
 		return -1;
 	}
+	/* zscanner itself does not canonize - neither owner nor insides */
+	res = knot_rrset_rr_to_canonical(new_rr);
+	if (res != KNOT_EOK) {
+		kr_log_error(PREFILL, "line %"PRIu64": error when canonizing: %s\n",
+				s->line_counter, knot_strerror(res));
+		return -1;
+	}
 
 	/* Records in zone file may not be grouped by name and RR type.
 	 * Use map to create search key and
 	 * avoid ineffective searches across all the imported records. */
-	char key[KR_RRKEY_LEN];
-	uint16_t additional_key_field = kr_rrset_type_maysig(new_rr);
-
-	res = kr_rrkey(key, new_rr->rclass, new_rr->owner, new_rr->type,
-		       additional_key_field);
-	if (res <= 0) {
-		kr_log_error(ZSCANNER, "line %"PRIu64": error constructing rrkey\n",
+	char key_buf[KEY_LEN], *key;
+	const int len = key_get(key_buf, new_rr->owner, new_rr->type,
+				kr_rrset_type_maysig(new_rr), &key);
+	if (len < 0) {
+		kr_log_error(PREFILL, "line %"PRIu64": error constructing rrkey\n",
 				s->line_counter);
 		return -1;
 	}
-
-	knot_rrset_t *saved_rr = map_get(&z_import->rrset_indexed, key);
-	if (saved_rr) {
-		res = knot_rdataset_merge(&saved_rr->rrs, &new_rr->rrs,
-					  &z_import->pool);
+	trie_val_t *rr_p = trie_get_ins(z_import->rrsets, key, len);
+	if (!rr_p)
+		return -1; // ENOMEM
+	if (*rr_p) {
+		knot_rrset_t *rr = *rr_p;
+		res = knot_rdataset_merge(&rr->rrs, &new_rr->rrs, z_import->pool);
 	} else {
-		res = map_set(&z_import->rrset_indexed, key, new_rr);
+		*rr_p = new_rr;
 	}
 	if (res != 0) {
-		kr_log_error(ZSCANNER, "line %"PRIu64": error saving parsed rrset\n",
+		kr_log_error(PREFILL, "line %"PRIu64": error saving parsed rrset\n",
 				s->line_counter);
 		return -1;
 	}
@@ -674,7 +505,6 @@ static int zi_record_store(zs_scanner_t
 	return 0;
 }
 
-/** @internal zscanner callback. */
 static int zi_state_parsing(zs_scanner_t *s)
 {
 	bool empty = true;
@@ -686,35 +516,35 @@ static int zi_state_parsing(zs_scanner_t
 			}
 			zone_import_ctx_t *z_import = (zone_import_ctx_t *) s->process.data;
 			empty = false;
-			if (s->r_type == 6) {
+			if (s->r_type == KNOT_RRTYPE_SOA) {
 				z_import->origin = knot_dname_copy(s->r_owner,
-                                                                 &z_import->pool);
+                                                                   z_import->pool);
 			}
 			break;
 		case ZS_STATE_ERROR:
-			kr_log_error(ZSCANNER, "line: %"PRIu64
+			kr_log_error(PREFILL, "line: %"PRIu64
 				     ": parse error; code: %i ('%s')\n",
 				     s->line_counter, s->error.code,
 				     zs_strerror(s->error.code));
 			return -1;
 		case ZS_STATE_INCLUDE:
-			kr_log_error(ZSCANNER, "line: %"PRIu64
+			kr_log_error(PREFILL, "line: %"PRIu64
 				     ": INCLUDE is not supported\n",
 				     s->line_counter);
 			return -1;
 		case ZS_STATE_EOF:
 		case ZS_STATE_STOP:
 			if (empty) {
-				kr_log_error(ZIMPORT, "empty zone file\n");
+				kr_log_error(PREFILL, "empty zone file\n");
 				return -1;
 			}
 			if (!((zone_import_ctx_t *) s->process.data)->origin) {
-				kr_log_error(ZIMPORT, "zone file doesn't contain SOA record\n");
+				kr_log_error(PREFILL, "zone file doesn't contain SOA record\n");
 				return -1;
 			}
 			return (s->error.counter == 0) ? 0 : -1;
 		default:
-			kr_log_error(ZSCANNER, "line: %"PRIu64
+			kr_log_error(PREFILL, "line: %"PRIu64
 				     ": unexpected parse state: %i\n",
 				     s->line_counter, s->state);
 			return -1;
@@ -724,94 +554,188 @@ static int zi_state_parsing(zs_scanner_t
 	return -1;
 }
 
-int zi_zone_import(struct zone_import_ctx *z_import,
-		   const char *zone_file, const char *origin,
-		   uint16_t rclass, uint32_t ttl)
+int zi_zone_import(const zi_config_t config)
 {
-	if (kr_fails_assert(z_import && z_import->worker && zone_file))
-		return -1;
+	const zi_config_t *c = &config;
+	if (kr_fails_assert(c && c->zone_file))
+		return kr_error(EINVAL);
+
+	knot_mm_t *pool = mm_ctx_mempool2(1024 * 1024);
+	zone_import_ctx_t *z_import = mm_calloc(pool, 1, sizeof(*z_import));
+	if (!z_import) return kr_error(ENOMEM);
+	z_import->pool = pool;
+
+	z_import->cb = c->cb;
+	z_import->cb_param = c->cb_param;
+	z_import->rrsets = trie_create(z_import->pool);
 
-	zs_scanner_t *s = malloc(sizeof(zs_scanner_t));
-	if (s == NULL) {
-		kr_log_error(ZSCANNER, "error creating instance of zone scanner (malloc() fails)\n");
-		return -1;
-	}
+	kr_timer_t stopwatch;
+	kr_timer_start(&stopwatch);
 
+   //// Parse the whole zone file into z_import->rrsets.
+	zs_scanner_t s_storage, *s = &s_storage;
 	/* zs_init(), zs_set_input_file(), zs_set_processing() returns -1 in case of error,
 	 * so don't print error code as it meaningless. */
-	int res = zs_init(s, origin, rclass, ttl);
-	if (res != 0) {
-		kr_log_error(ZSCANNER, "error initializing zone scanner instance, error: %i (%s)\n",
+	int ret = zs_init(s, c->origin, KNOT_CLASS_IN, c->ttl);
+	if (ret != 0) {
+		kr_log_error(PREFILL, "error initializing zone scanner instance, error: %i (%s)\n",
 			     s->error.code, zs_strerror(s->error.code));
-		free(s);
-		return -1;
+		goto fail;
 	}
 
-	res = zs_set_input_file(s, zone_file);
-	if (res != 0) {
-		kr_log_error(ZSCANNER, "error opening zone file `%s`, error: %i (%s)\n",
-			     zone_file, s->error.code, zs_strerror(s->error.code));
+	ret = zs_set_input_file(s, c->zone_file);
+	if (ret != 0) {
+		kr_log_error(PREFILL, "error opening zone file `%s`, error: %i (%s)\n",
+			     c->zone_file, s->error.code, zs_strerror(s->error.code));
 		zs_deinit(s);
-		free(s);
-		return -1;
+		goto fail;
 	}
 
 	/* Don't set processing and error callbacks as we don't use automatic parsing.
 	 * Parsing as well error processing will be performed in zi_state_parsing().
 	 * Store pointer to zone import context for further use. */
-	if (zs_set_processing(s, NULL, NULL, (void *)z_import) != 0) {
-		kr_log_error(ZSCANNER, "zs_set_processing() failed for zone file `%s`, "
+	ret = zs_set_processing(s, NULL, NULL, (void *)z_import);
+	if (ret != 0) {
+		kr_log_error(PREFILL, "zs_set_processing() failed for zone file `%s`, "
 				"error: %i (%s)\n",
-				zone_file, s->error.code, zs_strerror(s->error.code));
+				c->zone_file, s->error.code, zs_strerror(s->error.code));
 		zs_deinit(s);
-		free(s);
-		return -1;
+		goto fail;
 	}
 
-	uint64_t elapsed = 0;
-	int ret = zi_reset(z_import, 4096);
-	if (ret == 0) {
-		z_import->started = true;
-		z_import->start_timestamp = kr_now();
-		VERBOSE_MSG(NULL, "[zscanner] started; zone file `%s`\n",
-			    zone_file);
-		ret = zi_state_parsing(s);
-		if (ret == 0) {
-			/* Try to find TA for worker->z_import.origin. */
-			map_t *trust_anchors = &z_import->worker->engine->resolver.trust_anchors;
-			knot_rrset_t *rr = kr_ta_get(trust_anchors, z_import->origin);
-			if (!rr) {
-				/* For now - fail.
-				 * TODO - query DS and continue after answer had been obtained. */
-				KR_DNAME_GET_STR(zone_name_str, z_import->origin);
-				kr_log_error(ZIMPORT, "no TA found for `%s`, fail\n", zone_name_str);
-				ret = 1;
-			}
-			elapsed = kr_now() - z_import->start_timestamp;
-			elapsed = elapsed > UINT_MAX ? UINT_MAX : elapsed;
-		}
-	}
+	ret = zi_state_parsing(s);
 	zs_deinit(s);
-	free(s);
-
+	const double time_parse = kr_timer_elapsed(&stopwatch);
 	if (ret != 0) {
-		kr_log_error(ZSCANNER, "error parsing zone file `%s`\n", zone_file);
-		z_import->started = false;
-		return ret;
+		kr_log_error(PREFILL, "error parsing zone file `%s`\n", c->zone_file);
+		goto fail;
 	}
+	kr_log_debug(PREFILL, "import started for zone file `%s`\n", c->zone_file);
 
-	VERBOSE_MSG(NULL, "[zscanner] finished in %"PRIu64" ms; zone file `%s`\n",
-			    elapsed, zone_file);
-	map_walk(&z_import->rrset_indexed, zi_mapwalk_preprocess, z_import);
-
-	/* Zone have been parsed already, so start the import. */
-	uv_timer_start(&z_import->timer, zi_zone_process,
-		       ZONE_IMPORT_PAUSE, ZONE_IMPORT_PAUSE);
+	KR_DNAME_GET_STR(zone_name_str, z_import->origin);
 
-	return 0;
-}
+   //// Choose timestamp_rr, according to config.
+	struct timespec now;
+	if (clock_gettime(CLOCK_REALTIME, &now)) {
+		ret = kr_error(errno);
+		kr_log_error(PREFILL, "failed to get current time: %s\n", kr_strerror(ret));
+		goto fail;
+	}
+	if (config.time_src == ZI_STAMP_NOW) {
+		z_import->timestamp_rr = now.tv_sec;
+	} else if (config.time_src == ZI_STAMP_MTIM) {
+		struct stat st;
+		if (stat(c->zone_file, &st) != 0) {
+			kr_log_debug(PREFILL, "failed to stat file `%s`: %s\n",
+					c->zone_file, strerror(errno));
+			goto fail;
+		}
+		z_import->timestamp_rr = st.st_mtime;
+	} else {
+		ret = kr_error(EINVAL);
+		goto fail;
+	}
+   //// Some sanity checks
+	const knot_rrset_t *soa = rrset_get(z_import->rrsets, z_import->origin,
+						KNOT_RRTYPE_SOA, 0);
+	if (z_import->timestamp_rr > now.tv_sec) {
+		kr_log_warning(PREFILL, "zone file `%s` comes from future\n", c->zone_file);
+	} else if (!soa) {
+		kr_log_warning(PREFILL, "missing %s SOA\n", zone_name_str);
+	} else if ((int64_t)z_import->timestamp_rr + soa->ttl < now.tv_sec) {
+		kr_log_warning(PREFILL, "%s SOA already expired\n", zone_name_str);
+	}
+
+   //// Initialize validator context with the DNSKEY.
+	if (c->downgrade)
+		goto zonemd;
+	struct kr_context *resolver = &the_worker->engine->resolver;
+	const knot_rrset_t * const ds = c->ds ? c->ds :
+		kr_ta_get(resolver->trust_anchors, z_import->origin);
+	if (!ds) {
+		if (!kr_ta_closest(resolver, z_import->origin, KNOT_RRTYPE_DNSKEY))
+			goto zonemd; // our TAs say we're insecure
+		kr_log_error(PREFILL, "no DS found for `%s`, fail\n", zone_name_str);
+		ret = kr_error(ENOENT);
+		goto fail;
+	}
+	if (!knot_dname_is_equal(ds->owner, z_import->origin)) {
+		kr_log_error(PREFILL, "mismatching DS owner, fail\n");
+		ret = kr_error(EINVAL);
+		goto fail;
+	}
+
+	knot_rrset_t * const dnskey = rrset_get(z_import->rrsets, z_import->origin,
+						KNOT_RRTYPE_DNSKEY, 0);
+	if (!dnskey) {
+		kr_log_error(PREFILL, "no DNSKEY found for `%s`, fail\n", zone_name_str);
+		ret = kr_error(ENOENT);
+		goto fail;
+	}
+	knot_rrset_t * const dnskey_sigs = rrset_get(z_import->rrsets, z_import->origin,
+						KNOT_RRTYPE_RRSIG, KNOT_RRTYPE_DNSKEY);
+	if (!dnskey_sigs) {
+		kr_log_error(PREFILL, "no RRSIGs for DNSKEY found for `%s`, fail\n",
+				zone_name_str);
+		ret = kr_error(ENOENT);
+		goto fail;
+	}
+
+	kr_rrset_validation_ctx_t err_ctx;
+	z_import->svldr = kr_svldr_new_ctx(ds, dnskey, &dnskey_sigs->rrs,
+						z_import->timestamp_rr, &err_ctx);
+	if (!z_import->svldr) {
+		// log RRSIG stats; very similar to log_bogus_rrsig()
+		kr_log_error(PREFILL, "failed to validate DNSKEY for `%s` "
+			"(%u matching RRSIGs, %u expired, %u not yet valid, "
+			"%u invalid signer, %u invalid label count, %u invalid key, "
+			"%u invalid crypto, %u invalid NSEC)\n",
+			zone_name_str,
+			err_ctx.rrs_counters.matching_name_type,
+			err_ctx.rrs_counters.expired, err_ctx.rrs_counters.notyet,
+			err_ctx.rrs_counters.signer_invalid,
+			err_ctx.rrs_counters.labels_invalid,
+			err_ctx.rrs_counters.key_invalid,
+			err_ctx.rrs_counters.crypto_invalid,
+			err_ctx.rrs_counters.nsec_invalid);
+		ret = kr_error(ENOENT);
+		goto fail;
+	}
+
+   //// Do all ZONEMD processing, if desired.
+zonemd: (void)0; // C can't have a variable definition following a label
+	double time_zonemd = NAN;
+	if (c->zonemd) {
+		#if ENABLE_ZONEMD
+			kr_timer_start(&stopwatch);
+			ret = zonemd_verify(z_import);
+			time_zonemd = kr_timer_elapsed(&stopwatch);
+		#else
+			kr_log_error(PREFILL,
+				"ZONEMD check requested but not supported, fail\n");
+			ret = kr_error(ENOSYS);
+		#endif
+	} else {
+		ret = kr_ok();
+	}
+	kr_log_info(PREFILL, "performance: parsing took %.3lf s, hashing took %.3lf s\n",
+			time_parse, time_zonemd);
+	if (ret) goto fail;
+
+   //// Phase two, after a pause.  Validate and import all the remaining records.
+	ret = uv_timer_init(the_worker->loop, &z_import->timer);
+	if (ret) goto fail;
+	z_import->timer.data = z_import;
+	ret = uv_timer_start(&z_import->timer, zi_zone_process, ZONE_IMPORT_PAUSE, 0);
+	if (ret) goto fail;
 
-bool zi_import_started(struct zone_import_ctx *z_import)
-{
-	return z_import ? z_import->started : false;
+	return kr_ok();
+fail:
+	if (z_import->cb)
+		z_import->cb(kr_error(ret), z_import->cb_param);
+	if (kr_fails_assert(ret))
+		ret = ENOENT;
+	ctx_delete(z_import);
+	return kr_error(ret);
 }
+
diff -pruN 5.4.4-1/daemon/zimport.h 5.5.1-5/daemon/zimport.h
--- 5.4.4-1/daemon/zimport.h	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/daemon/zimport.h	2022-06-14 07:17:30.387490500 +0000
@@ -5,52 +5,44 @@
 #pragma once
 
 #include <stdbool.h>
-
-struct worker_ctx;
-/** Zone import context (opaque).  */
-struct zone_import_ctx;
+#include <libknot/rrset.h>
+#include "lib/defines.h"
 
 /**
  * Completion callback
  *
- * @param state -1 - fail
- *               0 - success
- *               1 - success, but there are non-critical errors
- * @param pointer to user data
+ * @param state  0 for OK completion, < 0 for errors (unfinished)
+ * @param param  pointer to user data
  */
 typedef void (*zi_callback)(int state, void *param);
+typedef struct {
+	/* Parser, see zs_init() */
+	const char *zone_file;
+	const char *origin;
+	uint32_t ttl;
+
+	/// Source of time: current real time, or file modification time.
+	enum { ZI_STAMP_NOW = 0, ZI_STAMP_MTIM } time_src;
+
+	/* Validator */
+	bool downgrade; /// true -> disable validation
+	bool zonemd; /// true -> verify zonemd
+	const knot_rrset_t *ds; /// NULL -> use trust anchors
+
+	zi_callback cb;
+	void *cb_param;
+} zi_config_t;
 
-/**
- * Allocate and initialize zone import context.
+/** Import zone from a file.
  *
- * @param worker pointer to worker state
- * @return NULL or pointer to zone import context.
- */
-struct zone_import_ctx *zi_allocate(struct worker_ctx *worker,
-				    zi_callback cb, void *param);
-
-/** Free zone import context. */
-void zi_free(struct zone_import_ctx *z_import);
-
-/**
- * Import zone from file.
+ * Error can be directly returned in the first phase (parsing + ZONEMD);
+ * otherwise it will be kr_ok() and config->cb gets (optionally) called finally.
  *
- * @note only root zone import is supported; origin must be NULL or "."
- * @param z_import pointer to zone import context
- * @param zone_file zone file name
- * @param origin default origin
- * @param rclass default class
- * @param ttl    default ttl
- * @return 0 or an error code
- */
-int zi_zone_import(struct zone_import_ctx *z_import,
-		   const char *zone_file, const char *origin,
-		   uint16_t rclass, uint32_t ttl);
-
-/**
- * Check if import already in process.
+ * Large zone would pause other processing for longer time;
+ * that's generally not advisable.
  *
- * @param z_import pointer to zone import context.
- * @return true if import already in process; false otherwise.
+ * Zone origin is detected from SOA, but it's certainly not perfect now.
  */
-bool zi_import_started(struct zone_import_ctx *z_import);
+KR_EXPORT
+int zi_zone_import(const zi_config_t config);
+
diff -pruN 5.4.4-1/daemon/zimport.test/tz-rfc-a1-bad.zone 5.5.1-5/daemon/zimport.test/tz-rfc-a1-bad.zone
--- 5.4.4-1/daemon/zimport.test/tz-rfc-a1-bad.zone	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/zimport.test/tz-rfc-a1-bad.zone	2022-06-14 07:17:30.387490500 +0000
@@ -0,0 +1,14 @@
+$ORIGIN example.
+example.      86400  IN  SOA     ns1 admin 2018031900 (
+                                 1800 900 604800 86400 )
+              86400  IN  NS      ns1
+              86400  IN  NS      ns2
+              86400  IN  ZONEMD  2018031900 1 1 (
+                                 BAAAAAAADa7aed71
+                                 6bc459f9340e3d7c
+                                 1370d4d24b7e2fc3
+                                 a1ddc0b9a87153b9
+                                 a9713b3c9ae5cc27
+                                 777f98b8e730044c )
+ns1           3600   IN  A       203.0.113.63
+ns2           3600   IN  AAAA    2001:db8::63
diff -pruN 5.4.4-1/daemon/zimport.test/tz-rfc-a1.zone 5.5.1-5/daemon/zimport.test/tz-rfc-a1.zone
--- 5.4.4-1/daemon/zimport.test/tz-rfc-a1.zone	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/zimport.test/tz-rfc-a1.zone	2022-06-14 07:17:30.387490500 +0000
@@ -0,0 +1,14 @@
+$ORIGIN example.
+example.      86400  IN  SOA     ns1 admin 2018031900 (
+                                 1800 900 604800 86400 )
+              86400  IN  NS      ns1
+              86400  IN  NS      ns2
+              86400  IN  ZONEMD  2018031900 1 1 (
+                                 c68090d90a7aed71
+                                 6bc459f9340e3d7c
+                                 1370d4d24b7e2fc3
+                                 a1ddc0b9a87153b9
+                                 a9713b3c9ae5cc27
+                                 777f98b8e730044c )
+ns1           3600   IN  A       203.0.113.63
+ns2           3600   IN  AAAA    2001:db8::63
diff -pruN 5.4.4-1/daemon/zimport.test/tz-rfc-a2.zone 5.5.1-5/daemon/zimport.test/tz-rfc-a2.zone
--- 5.4.4-1/daemon/zimport.test/tz-rfc-a2.zone	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/zimport.test/tz-rfc-a2.zone	2022-06-14 07:17:30.387490500 +0000
@@ -0,0 +1,35 @@
+$ORIGIN example.
+example.      86400  IN  SOA     ns1 admin 2018031900 (
+                                 1800 900 604800 86400 )
+              86400  IN  NS      ns1
+              86400  IN  NS      ns2
+              86400  IN  ZONEMD  2018031900 1 1 (
+                                 a3b69bad980a3504
+                                 e1cffcb0fd6397f9
+                                 3848071c93151f55
+                                 2ae2f6b1711d4bd2
+                                 d8b39808226d7b9d
+                                 b71e34b72077f8fe )
+ns1           3600   IN  A       203.0.113.63
+NS2           3600   IN  AAAA    2001:db8::63
+occluded.sub  7200   IN  TXT     "I'm occluded but must be digested"
+sub           7200   IN  NS      ns1
+duplicate     300    IN  TXT     "I must be digested just once"
+duplicate     300    IN  TXT     "I must be digested just once"
+foo.test.     555    IN  TXT     "out-of-zone data must be excluded"
+UPPERCASE     3600   IN  TXT     "canonicalize uppercase owner names"
+*             777    IN  PTR     dont-forget-about-wildcards
+mail          3600   IN  MX      20 MAIL1
+mail          3600   IN  MX      10 Mail2.Example.
+sortme        3600   IN  AAAA    2001:db8::5:61
+sortme        3600   IN  AAAA    2001:db8::3:62
+sortme        3600   IN  AAAA    2001:db8::4:63
+sortme        3600   IN  AAAA    2001:db8::1:65
+sortme        3600   IN  AAAA    2001:db8::2:64
+non-apex      900    IN  ZONEMD  2018031900 1 1 (
+                                 616c6c6f77656420
+                                 6275742069676e6f
+                                 7265642e20616c6c
+                                 6f77656420627574
+                                 2069676e6f726564
+                                 2e20616c6c6f7765 )
diff -pruN 5.4.4-1/daemon/zimport.test/tz-rfc-a3.zone 5.5.1-5/daemon/zimport.test/tz-rfc-a3.zone
--- 5.4.4-1/daemon/zimport.test/tz-rfc-a3.zone	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/zimport.test/tz-rfc-a3.zone	2022-06-14 07:17:30.387490500 +0000
@@ -0,0 +1,31 @@
+$ORIGIN example.
+example.      86400  IN  SOA     ns1 admin 2018031900 (
+                                 1800 900 604800 86400 )
+example.      86400  IN  NS      ns1.example.
+example.      86400  IN  NS      ns2.example.
+example.      86400  IN  ZONEMD  2018031900 1 1 (
+                                 62e6cf51b02e54b9
+                                 b5f967d547ce4313
+                                 6792901f9f88e637
+                                 493daaf401c92c27
+                                 9dd10f0edb1c56f8
+                                 080211f8480ee306 )
+example.      86400  IN  ZONEMD  2018031900 1 2 (
+                                 08cfa1115c7b948c
+                                 4163a901270395ea
+                                 226a930cd2cbcf2f
+                                 a9a5e6eb85f37c8a
+                                 4e114d884e66f176
+                                 eab121cb02db7d65
+                                 2e0cc4827e7a3204
+                                 f166b47e5613fd27 )
+example.      86400  IN  ZONEMD  2018031900 1 240 (
+                                 e2d523f654b9422a
+                                 96c5a8f44607bbee )
+example.      86400  IN  ZONEMD  2018031900 241 1 (
+                                 e1846540e33a9e41
+                                 89792d18d5d131f6
+                                 05fc283e )
+ns1.example.  3600   IN  A       203.0.113.63
+ns2.example.  86400  IN  TXT     "This example has multiple digests"
+NS2.EXAMPLE.  3600   IN  AAAA    2001:db8::63
diff -pruN 5.4.4-1/daemon/zimport.test/tz-rfc-a4.zone 5.5.1-5/daemon/zimport.test/tz-rfc-a4.zone
--- 5.4.4-1/daemon/zimport.test/tz-rfc-a4.zone	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/zimport.test/tz-rfc-a4.zone	2022-06-14 07:17:30.387490500 +0000
@@ -0,0 +1,37 @@
+$ORIGIN example.
+;; White-space had to be changed from the RFC, as libzscanner only allows spaces in base64 on some places.
+uri.arpa.	3600 IN SOA	sns.dns.icann.org. noc.dns.icann.org. 2018100702 10800 3600 1209600 3600
+uri.arpa.	3600 IN RRSIG	SOA 8 2 3600 20210217232440 20210120232440 37444 uri.arpa. GzQw+QzwLDJr13REPGVmpEChjD1D2XlX0ie1DnWHpgaEw1E/dhs3lCN3 +BmHd4Kx3tffTRgiyq65HxR6feQ5v7VmAifjyXUYB1DZur1eP5q0Ms2y gCB3byoeMgCNsFS1oKZ2LdzNBRpy3oace8xQn1SpmHGfyrsgg+WbHKCT 1dY=
+uri.arpa.	86400 IN NS	a.iana-servers.net.
+uri.arpa.	86400 IN NS	b.iana-servers.net.
+uri.arpa.	86400 IN NS	c.iana-servers.net.
+uri.arpa.	86400 IN NS	ns2.lacnic.net.
+uri.arpa.	86400 IN NS	sec3.apnic.net.
+uri.arpa.	86400 IN RRSIG	NS 8 2 86400 20210217232440 20210120232440 37444 uri.arpa. M+Iei2lcewWGaMtkPlrhM9FpUAHXFkCHTVpeyrjxjEONeNgKtHZor5e4 V4qJBOzNqo8go/qJpWlFBm+T5Hn3asaBZVstFIYky38/C8UeRLPKq1hT THARYUlFrexr5fMtSUAVOgOQPSBfH3xBq/BgSccTdRb9clD+HE7djpqr LS4=
+uri.arpa.	600 IN MX	10 pechora.icann.org.
+uri.arpa.	600 IN RRSIG	MX 8 2 600 20210217232440 20210120232440 37444 uri.arpa. kQAJQivmv6A5hqYBK8h6Z13ESY69gmosXwKI6WE09I8RFetfrxr24ecd nYd0lpnDtgNNSoHkYRSOoB+C4+zuJsoyAAzGo9uoWMWj97/2xeGhf3PT C9meQ9Ohi6hul9By7OR76XYmGhdWX8PBi60RUmZ1guslFBfQ8izwPqzu phs=
+uri.arpa.	3600 IN NSEC	ftp.uri.arpa. NS SOA MX RRSIG NSEC DNSKEY ZONEMD
+uri.arpa.	3600 IN RRSIG	NSEC 8 2 3600 20210217232440 20210120232440 37444 uri.arpa. dU/rXLM/naWd1+1PiWiYVaNJyCkiuyZJSccr91pJI673T8r3685B4ODM YFafZRboVgwnl3ZrXddY6xOhZL3n9V9nxXZwjLJ2HJUojFoKcXTlpnUy YUYvVQ2kj4GHAo6fcGCEp5QFJ2KbCpeJoS+PhKGRRx28icCiNT4/uXQv O2E=
+uri.arpa.	3600 IN DNSKEY	256 3 8 AwEAAbMxuFuLeVDuOwIMzYOTD/bTREjLflo7wOi6ieIJhqltEzgjNzmW Jf9kGwwDmzxU7kbthMEhBNBZNn84zmcyRSCMzuStWveL7xmqqUlE3swL 8kLOvdZvc75XnmpHrk3ndTyEb6eZM7slh2C63Oh6K8VR5VkiZAkEGg0u ZIT3NjsF
+uri.arpa.	3600 IN DNSKEY	257 3 8 AwEAAdkTaWkZtZuRh7/OobBUFxM+ytTst+bCu0r9w+rEwXD7GbDs0pIM hMenrZzoAvmv1fQxw2MGs6Ri6yPKfNULcFOSt9l8i6BVBLI+SKTY6XXe DUQpSEmSaxohHeRPMQFzpysfjxINp/L2rGtZ7yPmxY/XRiFPSO0myqwG Ja9r06Zw9CHM5UDHKWV/E+zxPFq/I7CfPbrrzbUotBX7Z6Vh3Sarllbe 8cGUB2UFNaTRgwB0TwDBPRD5ER3w2Dzbry9NhbElTr7vVfhaGWeOGuqA UXwlXEg6CrNkmJXJ2F1Rzr9WHUzhp7uWxhAbmJREGfi2dEyPAbUAyCjB qhFaqglknvc=
+uri.arpa.	3600 IN DNSKEY	257 3 8 AwEAAenQaBoFmDmvRT+/H5oNbm0Tr5FmNRNDEun0Jpj/ELkzeUrTWhNp QmZeIMC8I0kZ185tEvOnRvn8OvV39B17QIdrvvKGIh2HlgeDRCLolhao jfn2QM0DStjF/WWHpxJOmE6CIuvhqYEU37yoJscGAPpPVPzNvnL1HhYT aao1VRYWQ/maMrJ+bfHg+YX1N6M/8MnRjIKBif1FWjbCKvsn6dnuGGL9 oCWYUFJ3DwofXuhgPyZMkzPc88YkJj5EMvbMH4wtelbCwC+ivx732l0w /rXJn0ciQSOgoeVvDio8dIJmWQITWQAuP+q/ZHFEFHPlrP3gvQh5mcVS 48eLX71Bq7c=
+uri.arpa.	3600 IN RRSIG	DNSKEY 8 2 3600 20210217232440 20210120232440 12670 uri.arpa. DBE2gkKAoxJCfz47KKxzoImN/0AKArhIVHE7TyTwy0DdRPo44V5R+vL6 thUxlQ1CJi2Rw0jwAXymx5Y3Q873pOEllH+4bJoIT4dmoBmPXfYWW7Cl vw9UPKHRP0igKHmCVwIeBYDTU3gfLcMTbR4nEWPDN0GxlL1Mf7ITaC2I oabo79Ip3M/MR8I3Vx/xZ4ZKKPHtLn3xUuJluPNanqJrED2gTslL2xWZ 1tqjsAjJv7JnJo2HJ8XVRB5zBto0IaJ2oBlqcjdcQ/0VlyoM8uOy1pDw HQ2BJl7322gNMHBP9HSiUPIOaIDNUCwW8eUcW6DIUk+s9u3GN1uTqwWz sYB/rA==
+uri.arpa.	3600 IN RRSIG	DNSKEY 8 2 3600 20210217232440 20210120232440 30577 uri.arpa. Kx6HwP4UlkGc1UZ7SERXtQjPajOF4iUvkwDj7MEG1xbQFB1KoJiEb/ei W0qmSWdIhMDv8myhgauejRLyJxwxz8HDRV4xOeHWnRGfWBk4XGYwkejV zOHzoIArVdUVRbr2JKigcTOoyFN+uu52cNB7hRYu7dH5y1hlc6UbOnzR pMtGxcgVyKQ+/ARbIqGG3pegdEOvV49wTPWEiyY65P2urqhvnRg5ok/j zwAdMx4XGshiib7Ojq0sRVl2ZIzj4rFgY/qsSO8SEXEhMo2VuSkoJNio fVzYoqpxEeGnANkIT7Tx2xJL1BWyJxyc7E8Wr2QSgCcc+rYL6IkHDtJG Hy7TaQ==
+uri.arpa.	3600 IN ZONEMD	2018100702 1 1 0DBC3C4DBFD75777C12CA19C337854B1577799901307C482E9D91D5D 15CD934D16319D98E30C4201CF25A1D5A0254960
+uri.arpa.	3600 IN RRSIG	ZONEMD 8 2 3600 20210217232440 20210120232440 37444 uri.arpa. QDo4XZcL3HMyn8aAHyCUsu/Tqj4Gkth8xY1EqByOb8XOTwVtA4ZNQORE 1siqNqjtJUbeJPtJSbLNqCL7rCq0CzNNnBscv6IIf4gnqJZjlGtHO30o hXtKvEc4z7SU3IASsi6bB3nLmEAyERdYSeU6UBfx8vatQDIRhkgEnnWU Th4=
+ftp.uri.arpa.	604800 IN	NAPTR	0 0 "" "" "!^ftp://([^:/?#]*).*$!\\1!i" .
+ftp.uri.arpa.	604800 IN	RRSIG	NAPTR 8 3 604800 20210217232440 20210120232440 37444 uri.arpa. EygekDgl+Lyyq4NMSEpPyOrOywYf9Y3FAB4v1DT44J3R5QGidaH8l7ZF jHoYFI8sY64iYOCV4sBnX/dh6C1L5NgpY+8l5065Xu3vvjyzbtuJ2k6Y YwJrrCbvl5DDn53zAhhO2hL9uLgyLraZGi9i7TFGd0sm3zNyUF/EVL0C cxU=
+ftp.uri.arpa.	3600 IN NSEC	http.uri.arpa. NAPTR RRSIG NSEC
+ftp.uri.arpa.	3600 IN RRSIG	NSEC 8 3 3600 20210217232440 20210120232440 37444 uri.arpa. pbP4KxevPXCu/bDqcvXiuBppXyFEmtHyiy0eAN5gS7mi6mp9Z9bWFjx/ LdH9+6oFGYa5vGmJ5itu/4EDMe8iQeZbI8yrpM4TquB7RR/MGfBnTd8S +sjyQtlRYG7yqEu77Vd78Fme22BKPJ+MVqjS0JHMUE/YUGomPkAjLJJw wGw=
+http.uri.arpa.	604800 IN	NAPTR	0 0 "" "" "!^http://([^:/?#]*).*$!\\1!i" .
+http.uri.arpa.	604800 IN	RRSIG	NAPTR 8 3 604800 20210217232440 20210120232440 37444 uri.arpa. eTqbWvt1GvTeXozuvm4ebaAfkXFQKrtdu0cEiExto80sHIiCbO0WL8UD a/J3cDivtQca7LgUbOb6c17NESsrsVkc6zNPx5RK2tG7ZQYmhYmtqtfg 1oU5BRdHZ5TyqIXcHlw9Blo2pir1Y9IQgshhD7UOGkbkEmvB1Lrd0aHh AAg=
+http.uri.arpa.	3600 IN NSEC	mailto.uri.arpa. NAPTR RRSIG NSEC
+http.uri.arpa.	3600 IN RRSIG	NSEC 8 3 3600 20210217232440 20210120232440 37444 uri.arpa. R9rlNzw1CVz2N08q6DhULzcsuUm0UKcPaGAWEU40tr81jEDHsFHNM+kh CdOI8nDstzA42aee4rwCEgijxJpRCcY9hrO1Ysrrr2fdqNz60JikMdar vU5O0p0VXeaaJDfJQT44+o+YXaBwI7Qod3FTMx7aRib8i7istvPm1Rr7 ixA=
+mailto.uri.arpa. 604800 IN	NAPTR	0 0 "" "" "!^mailto:(.*)@(.*)$!\\2!i" .
+mailto.uri.arpa. 604800 IN	RRSIG	NAPTR 8 3 604800 20210217232440 20210120232440 37444 uri.arpa. Ch2zTG2F1plEvQPyIH4Yd80XXLjXOPvMbiqDjpJBcnCJsV8QF7kr0wTL nUT3dB+asQudOjPyzaHGwFlMzmrrAsszN4XAMJ6htDtFJdsgTMP/NkHh YRSmVv6rLeAhd+mVfObY12M//b/GGVTjeUI/gJaLW0fLVZxr1Fp5U5CR jyw=
+mailto.uri.arpa. 3600 IN NSEC	urn.uri.arpa. NAPTR RRSIG NSEC
+mailto.uri.arpa. 3600 IN RRSIG	NSEC 8 3 3600 20210217232440 20210120232440 37444 uri.arpa. fQUbSIE6E7JDi2rosah4SpCOTrKufeszFyj5YEavbQuYlQ5cNFvtm8Ku E2xXMRgRI4RGvM2leVqcoDw5hS3m2pOJLxH8l2WE72YjYvWhvnwc5Rof e/8yB/vaSK9WCnqN8y2q6Vmy73AGP0fuiwmuBra7LlkOiqmyx3amSFiz wms=
+urn.uri.arpa.	604800 IN	NAPTR	0 0 "" "" "/urn:([^:]+)/\\1/i" .
+urn.uri.arpa.	604800 IN	RRSIG	NAPTR 8 3 604800 20210217232440 20210120232440 37444 uri.arpa. CVt2Tgz0e5ZmaSXqRfNys/8OtVCk9nfP0zhezhN8Bo6MDt6yyKZ2kEEW JPjkN7PCYHjO8fGjnUn0AHZI2qBNv7PKHcpR42VY03q927q85a65weOO 1YE0vPYMzACpua9TOtfNnynM2Ws0uN9URxUyvYkXBdqOC81N3sx1dVEL cwc=
+urn.uri.arpa.	3600 IN NSEC	uri.arpa. NAPTR RRSIG NSEC
+urn.uri.arpa.	3600 IN RRSIG	NSEC 8 3 3600 20210217232440 20210120232440 37444 uri.arpa. JuKkMiC3/j9iM3V8/izcouXWAVGnSZjkOgEgFPhutMqoylQNRcSkbEZQ zFK8B/PIVdzZF0Y5xkO6zaKQjOzz6OkSaNPIo1a7Vyyl3wDY/uLCRRAH RJfpknuY7O+AUNXvVVIEYJqZggd4kl/Rjh1GTzPYZTRrVi5eQidI1LqC Oeg=
diff -pruN 5.4.4-1/daemon/zimport.test/tz-rfc-a5.zone 5.5.1-5/daemon/zimport.test/tz-rfc-a5.zone
--- 5.4.4-1/daemon/zimport.test/tz-rfc-a5.zone	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/zimport.test/tz-rfc-a5.zone	2022-06-14 07:17:30.387490500 +0000
@@ -0,0 +1,48 @@
+root-servers.net.     3600000 IN  SOA     a.root-servers.net. (
+    nstld.verisign-grs.com. 2018091100 14400 7200 1209600 3600000 )
+root-servers.net.     3600000 IN  NS      a.root-servers.net.
+root-servers.net.     3600000 IN  NS      b.root-servers.net.
+root-servers.net.     3600000 IN  NS      c.root-servers.net.
+root-servers.net.     3600000 IN  NS      d.root-servers.net.
+root-servers.net.     3600000 IN  NS      e.root-servers.net.
+root-servers.net.     3600000 IN  NS      f.root-servers.net.
+root-servers.net.     3600000 IN  NS      g.root-servers.net.
+root-servers.net.     3600000 IN  NS      h.root-servers.net.
+root-servers.net.     3600000 IN  NS      i.root-servers.net.
+root-servers.net.     3600000 IN  NS      j.root-servers.net.
+root-servers.net.     3600000 IN  NS      k.root-servers.net.
+root-servers.net.     3600000 IN  NS      l.root-servers.net.
+root-servers.net.     3600000 IN  NS      m.root-servers.net.
+a.root-servers.net.   3600000 IN  AAAA    2001:503:ba3e::2:30
+a.root-servers.net.   3600000 IN  A       198.41.0.4
+b.root-servers.net.   3600000 IN  MX      20 mail.isi.edu.
+b.root-servers.net.   3600000 IN  AAAA    2001:500:200::b
+b.root-servers.net.   3600000 IN  A       199.9.14.201
+c.root-servers.net.   3600000 IN  AAAA    2001:500:2::c
+c.root-servers.net.   3600000 IN  A       192.33.4.12
+d.root-servers.net.   3600000 IN  AAAA    2001:500:2d::d
+d.root-servers.net.   3600000 IN  A       199.7.91.13
+e.root-servers.net.   3600000 IN  AAAA    2001:500:a8::e
+e.root-servers.net.   3600000 IN  A       192.203.230.10
+f.root-servers.net.   3600000 IN  AAAA    2001:500:2f::f
+f.root-servers.net.   3600000 IN  A       192.5.5.241
+g.root-servers.net.   3600000 IN  AAAA    2001:500:12::d0d
+g.root-servers.net.   3600000 IN  A       192.112.36.4
+h.root-servers.net.   3600000 IN  AAAA    2001:500:1::53
+h.root-servers.net.   3600000 IN  A       198.97.190.53
+i.root-servers.net.   3600000 IN  MX      10 mx.i.root-servers.org.
+i.root-servers.net.   3600000 IN  AAAA    2001:7fe::53
+i.root-servers.net.   3600000 IN  A       192.36.148.17
+j.root-servers.net.   3600000 IN  AAAA    2001:503:c27::2:30
+j.root-servers.net.   3600000 IN  A       192.58.128.30
+k.root-servers.net.   3600000 IN  AAAA    2001:7fd::1
+k.root-servers.net.   3600000 IN  A       193.0.14.129
+l.root-servers.net.   3600000 IN  AAAA    2001:500:9f::42
+l.root-servers.net.   3600000 IN  A       199.7.83.42
+m.root-servers.net.   3600000 IN  AAAA    2001:dc3::35
+m.root-servers.net.   3600000 IN  A       202.12.27.33
+root-servers.net.     3600000 IN  SOA     a.root-servers.net. (
+    nstld.verisign-grs.com. 2018091100 14400 7200 1209600 3600000 )
+root-servers.net.     3600000 IN  ZONEMD  2018091100 1 1 (
+    f1ca0ccd91bd5573d9f431c00ee0101b2545c97602be0a97
+    8a3b11dbfc1c776d5b3e86ae3d973d6b5349ba7f04340f79 )
diff -pruN 5.4.4-1/daemon/zimport.test/zimport.test.lua 5.5.1-5/daemon/zimport.test/zimport.test.lua
--- 5.4.4-1/daemon/zimport.test/zimport.test.lua	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/daemon/zimport.test/zimport.test.lua	2022-06-14 07:17:30.387490500 +0000
@@ -0,0 +1,47 @@
+-- unload modules which are not related to this test
+-- SPDX-License-Identifier: GPL-3.0-or-later
+
+if ta_signal_query then
+        modules.unload('ta_signal_query')
+end
+if priming then
+        modules.unload('priming')
+end
+if detect_time_skew then
+        modules.unload('detect_time_skew')
+end
+
+-- do not listen, test is driven by config code
+env.KRESD_NO_LISTEN = true
+
+
+cache.size = 5*MB
+log_groups({'prefil'})
+
+--[[ This test checks ZONEMD computation on some model cases. (no DNSSEC validation)
+	https://www.rfc-editor.org/rfc/rfc8976.html#name-example-zones-with-digests
+--]]
+
+
+local function test_zone(file_name, success) return function()
+	local import_res = require('ffi').C.zi_zone_import({
+		zone_file = file_name,
+		zonemd = true,
+		downgrade = true,
+	})
+	if success == nil or success then
+		is(import_res, 0, 'zone import should start OK for file ' .. file_name)
+	else
+		isnt(import_res, 0, 'zone import should fail for file ' .. file_name)
+	end
+	worker.sleep(0.2)  -- zimport is delayed by 100 ms from function call
+end end
+
+return {
+	test_zone('tz-rfc-a1.zone'),
+	test_zone('tz-rfc-a1-bad.zone', false),
+	test_zone('tz-rfc-a2.zone'),
+	test_zone('tz-rfc-a3.zone'),
+	test_zone('tz-rfc-a4.zone'),
+	test_zone('tz-rfc-a5.zone'),
+}
diff -pruN 5.4.4-1/debian/changelog 5.5.1-5/debian/changelog
--- 5.4.4-1/debian/changelog	2022-01-05 17:35:35.000000000 +0000
+++ 5.5.1-5/debian/changelog	2022-08-05 16:59:22.000000000 +0000
@@ -1,3 +1,42 @@
+knot-resolver (5.5.1-5) unstable; urgency=medium
+
+  * d/control: remove ppc64el from module-http
+
+ -- Jakub Ružička <jakub.ruzicka@nic.cz>  Fri, 05 Aug 2022 16:59:22 +0000
+
+knot-resolver (5.5.1-4) unstable; urgency=medium
+
+  * d/control: remove broken ppc64el and s390x arches
+
+ -- Jakub Ružička <jakub.ruzicka@nic.cz>  Tue, 02 Aug 2022 10:09:52 +0000
+
+knot-resolver (5.5.1-3) unstable; urgency=medium
+
+  * Upload to unstable
+
+ -- Jakub Ružička <jakub.ruzicka@nic.cz>  Wed, 27 Jul 2022 08:14:55 +0000
+
+knot-resolver (5.5.1-2) experimental; urgency=medium
+
+  * Use experimental to test luajit2 (Closes: #1013810)
+  * d/control: enable s390x due to luajit2 support
+
+ -- Jakub Ružička <jakub.ruzicka@nic.cz>  Sun, 03 Jul 2022 18:19:14 +0000
+
+knot-resolver (5.5.1-1) unstable; urgency=medium
+
+  * New upstream release
+
+ -- Jakub Ružička <jakub.ruzicka@nic.cz>  Tue, 14 Jun 2022 13:20:04 +0000
+
+knot-resolver (5.5.0-1) unstable; urgency=medium
+
+  * New upstream release
+  * d/upstream/signing-key.asc: update upstream key
+  * d/control: require Knot DNS 3.0.2
+
+ -- Jakub Ružička <jakub.ruzicka@nic.cz>  Thu, 17 Mar 2022 17:34:34 +0000
+
 knot-resolver (5.4.4-1) unstable; urgency=medium
 
   * New upstream release
diff -pruN 5.4.4-1/debian/control 5.5.1-5/debian/control
--- 5.4.4-1/debian/control	2022-01-05 17:35:35.000000000 +0000
+++ 5.5.1-5/debian/control	2022-08-05 16:57:39.000000000 +0000
@@ -15,7 +15,7 @@ Build-Depends: debhelper-compat (= 13),
                libedit-dev,
                libfstrm-dev,
                libgnutls28-dev,
-               libknot-dev (>= 3.0.1),
+               libknot-dev (>= 3.0.2),
                liblmdb-dev,
                libluajit-5.1-dev,
                libnghttp2-dev,
@@ -41,9 +41,7 @@ Vcs-Git: https://salsa.debian.org/dns-te
 Rules-Requires-Root: no
 
 Package: knot-resolver
-# Doesn't build on s390x due to missing luajit
-#   https://github.com/LuaJIT/LuaJIT/issues/397
-Architecture: amd64 arm64 armel armhf i386 mips mips64el mipsel ppc64 ppc64el
+Architecture: amd64 arm64 armel armhf i386 mips mips64el mipsel ppc64
 Depends: adduser,
          debconf,
          dns-root-data,
@@ -74,7 +72,7 @@ Description: caching, DNSSEC-validating
  nodes depending on the contention without downtime.
 
 Package: knot-resolver-module-http
-Architecture: amd64 arm64 armel armhf i386 mips mips64el mipsel ppc64 ppc64el
+Architecture: amd64 arm64 armel armhf i386 mips mips64el mipsel ppc64
 Multi-Arch: same
 Depends: fonts-glyphicons-halflings,
          libjs-bootstrap,
diff -pruN 5.4.4-1/debian/salsa-ci.yml 5.5.1-5/debian/salsa-ci.yml
--- 5.4.4-1/debian/salsa-ci.yml	2022-01-05 17:35:35.000000000 +0000
+++ 5.5.1-5/debian/salsa-ci.yml	2022-07-27 08:12:39.000000000 +0000
@@ -2,3 +2,6 @@
 include:
   - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
   - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml
+
+lintian:
+  allow_failure: true
diff -pruN 5.4.4-1/debian/upstream/signing-key.asc 5.5.1-5/debian/upstream/signing-key.asc
--- 5.4.4-1/debian/upstream/signing-key.asc	2022-01-05 17:35:35.000000000 +0000
+++ 5.5.1-5/debian/upstream/signing-key.asc	2022-07-03 18:08:26.000000000 +0000
@@ -1,311 +1,126 @@
 -----BEGIN PGP PUBLIC KEY BLOCK-----
 
-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
-X368eUGLsR8CANIB8yYgaKRI6zk=
-=mbmb
+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
+=dRuR
 -----END PGP PUBLIC KEY BLOCK-----
diff -pruN 5.4.4-1/distro/config/apkg.toml 5.5.1-5/distro/config/apkg.toml
--- 5.4.4-1/distro/config/apkg.toml	2022-01-05 13:19:14.435727400 +0000
+++ 5.5.1-5/distro/config/apkg.toml	2022-06-14 07:17:30.387490500 +0000
@@ -9,4 +9,4 @@ archive_url = "https://secure.nic.cz/fil
 signature_url = "https://secure.nic.cz/files/knot-resolver/knot-resolver-{{ version }}.tar.xz.asc"
 
 [apkg]
-compat = 1
+compat = 2
diff -pruN 5.4.4-1/distro/pkg/deb/changelog 5.5.1-5/distro/pkg/deb/changelog
--- 5.4.4-1/distro/pkg/deb/changelog	2022-01-05 13:19:14.439727500 +0000
+++ 5.5.1-5/distro/pkg/deb/changelog	2022-06-14 07:17:30.387490500 +0000
@@ -1,6 +1,6 @@
 knot-resolver ({{ version }}-cznic.{{ release }}) unstable; urgency=medium
 
-  * move changelog to OBS
+  * upstream package
   * see NEWS or https://knot-resolver.cz
 
- -- Tomas Krizek <tomas.krizek@nic.cz>  Tue, 20 Feb 2018 19:36:45 +0100
+ -- Jakub Ružička <jakub.ruzicka@nic.cz>  {{ now }}
diff -pruN 5.4.4-1/distro/pkg/deb/control 5.5.1-5/distro/pkg/deb/control
--- 5.4.4-1/distro/pkg/deb/control	2022-01-05 13:19:14.439727500 +0000
+++ 5.5.1-5/distro/pkg/deb/control	2022-06-14 07:17:30.387490500 +0000
@@ -10,7 +10,7 @@ Build-Depends:
  libedit-dev,
  libfstrm-dev,
  libgnutls28-dev,
- libknot-dev (>= 2.9),
+ libknot-dev (>= 3.0.2),
  liblmdb-dev,
  libluajit-5.1-dev,
  libnghttp2-dev,
diff -pruN 5.4.4-1/distro/pkg/deb/copyright 5.5.1-5/distro/pkg/deb/copyright
--- 5.4.4-1/distro/pkg/deb/copyright	2022-01-05 13:19:14.439727500 +0000
+++ 5.5.1-5/distro/pkg/deb/copyright	2022-06-14 07:17:30.387490500 +0000
@@ -55,13 +55,6 @@ Files: contrib/ccan/json/*
 Copyright: 2011 Joey Adams
 License: Expat
 
-Files: lib/generic/map.c lib/generic/map.h
-Copyright: Dan Bernstein
-	   Jonas Gehring
-	   Adam Langley
-	   Marek Vavrusa
-License: public-domain
-
 Files: modules/policy/lua-aho-corasick/*
 Copyright: 2013 CloudFlare, Inc.
 License: BSD-3-CloudFlare
diff -pruN 5.4.4-1/distro/pkg/deb/knot-resolver.install 5.5.1-5/distro/pkg/deb/knot-resolver.install
--- 5.4.4-1/distro/pkg/deb/knot-resolver.install	2022-01-05 13:19:14.439727500 +0000
+++ 5.5.1-5/distro/pkg/deb/knot-resolver.install	2022-06-14 07:17:30.387490500 +0000
@@ -8,6 +8,7 @@ usr/lib/knot-resolver/*.so
 usr/lib/knot-resolver/*.lua
 usr/lib/knot-resolver/kres_modules/bogus_log.so
 usr/lib/knot-resolver/kres_modules/edns_keepalive.so
+usr/lib/knot-resolver/kres_modules/extended_error.so
 usr/lib/knot-resolver/kres_modules/hints.so
 usr/lib/knot-resolver/kres_modules/nsid.so
 usr/lib/knot-resolver/kres_modules/refuse_nord.so
diff -pruN 5.4.4-1/distro/pkg/rpm/knot-resolver.spec 5.5.1-5/distro/pkg/rpm/knot-resolver.spec
--- 5.4.4-1/distro/pkg/rpm/knot-resolver.spec	2022-01-05 13:19:14.439727500 +0000
+++ 5.5.1-5/distro/pkg/rpm/knot-resolver.spec	2022-06-14 07:17:30.387490500 +0000
@@ -29,9 +29,8 @@ Source1:        knot-resolver-%{version}
 # PGP keys used to sign upstream releases
 # Export with --armor using command from https://fedoraproject.org/wiki/PackagingDrafts:GPGSignatures
 # Don't forget to update %%prep section when adding/removing keys
-Source100:	gpgkey-B6006460B60A80E782062449E747DF1F9575A3AA.gpg.asc
-Source101:	gpgkey-BE26EBB9CBE059B3910CA35BCE8DD6A1A50A21E4.gpg.asc
-Source102:	gpgkey-4A8BA48C2AED933BD495C509A1FBA5F7EF8C4869.gpg.asc
+# This key is from: https://secure.nic.cz/files/knot-resolver/kresd-keyblock.asc
+Source100:      kresd-keyblock.asc
 BuildRequires:  gnupg2
 %endif
 
@@ -41,9 +40,9 @@ BuildRequires:  meson
 BuildRequires:  pkgconfig(cmocka)
 BuildRequires:  pkgconfig(gnutls)
 BuildRequires:  pkgconfig(libedit)
-BuildRequires:  pkgconfig(libknot) >= 2.9
-BuildRequires:  pkgconfig(libzscanner) >= 2.9
-BuildRequires:  pkgconfig(libdnssec) >= 2.9
+BuildRequires:  pkgconfig(libknot) >= 3.0.2
+BuildRequires:  pkgconfig(libzscanner) >= 3.0.2
+BuildRequires:  pkgconfig(libdnssec) >= 3.0.2
 BuildRequires:  pkgconfig(libnghttp2)
 BuildRequires:  pkgconfig(libsystemd)
 BuildRequires:  pkgconfig(libcap-ng)
@@ -162,8 +161,8 @@ native C implementation, which doesn't r
 %prep
 %if 0%{GPG_CHECK}
 export GNUPGHOME=./gpg-keyring
-mkdir ${GNUPGHOME}
-gpg2 --import %{SOURCE100} %{SOURCE101} %{SOURCE102}
+mkdir -m 700 ${GNUPGHOME}
+gpg2 --import %{SOURCE100}
 gpg2 --verify %{SOURCE1} %{SOURCE0}
 %endif
 %setup -q -n %{name}-%{version}
@@ -321,6 +320,7 @@ fi
 %dir %{_libdir}/knot-resolver/kres_modules
 %{_libdir}/knot-resolver/kres_modules/bogus_log.so
 %{_libdir}/knot-resolver/kres_modules/edns_keepalive.so
+%{_libdir}/knot-resolver/kres_modules/extended_error.so
 %{_libdir}/knot-resolver/kres_modules/hints.so
 %{_libdir}/knot-resolver/kres_modules/nsid.so
 %{_libdir}/knot-resolver/kres_modules/refuse_nord.so
@@ -377,6 +377,6 @@ fi
 %endif
 
 %changelog
-* Fri Feb 16 2018 Tomas Krizek <tomas.krizek@nic.cz> - 2.1.0-1
+* {{ now }} Jakub Ružička <jakub.ruzicka@nic.cz> - {{ version }}-{{ release }}
+- upstream package
 - see NEWS or https://www.knot-resolver.cz/
-- move spec file to upstream
diff -pruN 5.4.4-1/distro/tests/ansible-roles/knot_resolver/tasks/main.yaml 5.5.1-5/distro/tests/ansible-roles/knot_resolver/tasks/main.yaml
--- 5.4.4-1/distro/tests/ansible-roles/knot_resolver/tasks/main.yaml	2022-01-05 13:19:14.439727500 +0000
+++ 5.5.1-5/distro/tests/ansible-roles/knot_resolver/tasks/main.yaml	2022-06-14 07:17:30.391490700 +0000
@@ -42,11 +42,11 @@
             state: latest
 
         - include: configure_doh.yaml
-          when: ansible_distribution in ["CentOS", "Fedora", "Debian", "Ubuntu"]
+          when: ansible_distribution in ["CentOS", "Rocky", "Fedora", "Debian", "Ubuntu"]
 
         - include: restart_kresd.yaml
         - include: test_doh.yaml
-      when: distro in ["Fedora", "Debian", "CentOS"] or (distro == "Ubuntu" and ansible_distribution_major_version|int >= 18)
+      when: distro in ["Fedora", "Debian", "CentOS", "Rocky"] or (distro == "Ubuntu" and ansible_distribution_major_version|int >= 18)
 
     - name: Test dnstap module
       block:
@@ -56,7 +56,7 @@
             state: latest
         - include: configure_dnstap.yaml
         - include: restart_kresd.yaml
-      when: distro in ["Fedora", "Debian", "CentOS", "Ubuntu"]
+      when: distro in ["Fedora", "Debian", "CentOS", "Rocky", "Ubuntu"]
 
   always:
 
diff -pruN 5.4.4-1/distro/tests/ansible-roles/knot_resolver/vars/Rocky.yaml 5.5.1-5/distro/tests/ansible-roles/knot_resolver/vars/Rocky.yaml
--- 5.4.4-1/distro/tests/ansible-roles/knot_resolver/vars/Rocky.yaml	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/distro/tests/ansible-roles/knot_resolver/vars/Rocky.yaml	2022-06-14 07:17:30.391490700 +0000
@@ -0,0 +1,6 @@
+---
+# SPDX-License-Identifier: GPL-3.0-or-later
+show_package_version: rpm -qi knot-resolver | grep '^Version'
+packages:
+  - knot-resolver
+  - knot-utils
diff -pruN 5.4.4-1/distro/tests/ansible-roles/obs_repos/tasks/Rocky.yaml 5.5.1-5/distro/tests/ansible-roles/obs_repos/tasks/Rocky.yaml
--- 5.4.4-1/distro/tests/ansible-roles/obs_repos/tasks/Rocky.yaml	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/distro/tests/ansible-roles/obs_repos/tasks/Rocky.yaml	2022-06-14 07:17:30.391490700 +0000
@@ -0,0 +1,13 @@
+---
+# SPDX-License-Identifier: GPL-3.0-or-later
+- name: Install EPEL
+  yum:
+    name: epel-release
+    state: present
+
+- name: Download repo file(s)
+  get_url:
+    url: "{{ obs_repofile_url }}"
+    dest: /etc/yum.repos.d/home:CZ-NIC:{{ item }}.repo
+    mode: 0644
+  with_items: "{{ repos }}"
diff -pruN 5.4.4-1/distro/tests/ansible-roles/obs_repos/vars/Rocky.yaml 5.5.1-5/distro/tests/ansible-roles/obs_repos/vars/Rocky.yaml
--- 5.4.4-1/distro/tests/ansible-roles/obs_repos/vars/Rocky.yaml	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/distro/tests/ansible-roles/obs_repos/vars/Rocky.yaml	2022-06-14 07:17:30.391490700 +0000
@@ -0,0 +1,3 @@
+---
+# SPDX-License-Identifier: GPL-3.0-or-later
+obs_repo_version: "CentOS_{{ ansible_distribution_major_version }}_EPEL"
diff -pruN 5.4.4-1/distro/tests/centos8/ansible.cfg 5.5.1-5/distro/tests/centos8/ansible.cfg
--- 5.4.4-1/distro/tests/centos8/ansible.cfg	2022-01-05 13:19:14.439727500 +0000
+++ 5.5.1-5/distro/tests/centos8/ansible.cfg	1970-01-01 00:00:00.000000000 +0000
@@ -1,8 +0,0 @@
-# SPDX-License-Identifier: GPL-3.0-or-later
-
-[defaults]
-
-# additional paths to search for roles in, colon separated
-roles_path = ../ansible-roles
-interpreter_python = auto
-stdout_callback=debug
diff -pruN 5.4.4-1/distro/tests/centos8/Vagrantfile 5.5.1-5/distro/tests/centos8/Vagrantfile
--- 5.4.4-1/distro/tests/centos8/Vagrantfile	2022-01-05 13:19:14.443727500 +0000
+++ 5.5.1-5/distro/tests/centos8/Vagrantfile	1970-01-01 00:00:00.000000000 +0000
@@ -1,30 +0,0 @@
-# SPDX-License-Identifier: GPL-3.0-or-later
-# -*- mode: ruby -*-
-# vi: set ft=ruby :
-#
-
-Vagrant.configure(2) do |config|
-
-    config.vm.box = "centos/8"
-    config.vm.synced_folder ".", "/vagrant", disabled: true
-
-    config.vm.define "centos8_knot-resolver"  do |machine|
-        machine.vm.provision "ansible" do |ansible|
-            ansible.playbook = "../knot-resolver-pkgtest.yaml"
-            ansible.extra_vars = {
-                ansible_python_interpreter: "/usr/libexec/platform-python"
-            }
-        end
-    end
-
-    config.vm.provider :libvirt do |libvirt|
-      libvirt.cpus = 1
-      libvirt.memory = 1024
-    end
-
-    config.vm.provider :virtualbox do |vbox|
-      vbox.cpus = 1
-      vbox.memory = 1024
-    end
-
-end
diff -pruN 5.4.4-1/distro/tests/rocky8/ansible.cfg 5.5.1-5/distro/tests/rocky8/ansible.cfg
--- 5.4.4-1/distro/tests/rocky8/ansible.cfg	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/distro/tests/rocky8/ansible.cfg	2022-06-14 07:17:30.387490500 +0000
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+[defaults]
+
+# additional paths to search for roles in, colon separated
+roles_path = ../ansible-roles
+interpreter_python = auto
+stdout_callback=debug
diff -pruN 5.4.4-1/distro/tests/rocky8/Vagrantfile 5.5.1-5/distro/tests/rocky8/Vagrantfile
--- 5.4.4-1/distro/tests/rocky8/Vagrantfile	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/distro/tests/rocky8/Vagrantfile	2022-06-14 07:17:30.391490700 +0000
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+#
+
+Vagrant.configure(2) do |config|
+
+    config.vm.box = "generic/rocky8"
+    config.vm.synced_folder ".", "/vagrant", disabled: true
+
+    config.vm.define "rocky8_knot-resolver"  do |machine|
+        machine.vm.provision "ansible" do |ansible|
+            ansible.playbook = "../knot-resolver-pkgtest.yaml"
+            ansible.extra_vars = {
+                ansible_python_interpreter: "/usr/libexec/platform-python"
+            }
+        end
+    end
+
+    config.vm.provider :libvirt do |libvirt|
+      libvirt.cpus = 1
+      libvirt.memory = 1024
+    end
+
+    config.vm.provider :virtualbox do |vbox|
+      vbox.cpus = 1
+      vbox.memory = 1024
+    end
+
+end
diff -pruN 5.4.4-1/doc/build.rst 5.5.1-5/doc/build.rst
--- 5.4.4-1/doc/build.rst	2022-01-05 13:19:14.447727700 +0000
+++ 5.5.1-5/doc/build.rst	2022-06-14 07:17:30.395490600 +0000
@@ -33,7 +33,7 @@ The following dependencies are needed to
    "meson >= 0.49", "*build only* [#]_"
    "C and C++ compiler", "*build only* [#]_"
    "`pkg-config`_", "*build only* [#]_"
-   "libknot_ 2.9+", "Knot DNS libraries"
+   "libknot_ 3.0.2+", "Knot DNS libraries"
    "LuaJIT_ 2.0+", "Embedded scripting language"
    "libuv_ 1.7+", "Multiplatform I/O and services"
    "lmdb", "Memory-mapped database for cache"
@@ -166,36 +166,7 @@ For complete control over the build flag
 ``CFLAGS``, ``LDFLAGS`` when creating the build directory with ``meson``
 command.
 
-Tests
------
-
-The following command runs all enabled tests. By default, only unit tests are
-enabled (when ``cmocka`` is installed).
-
-.. code-block:: bash
-
-   $ ninja -C build_dir
-   $ meson test -C build_dir
-
-More comprehensive tests require you to install ``kresd`` into the configured
-prefix before running the test suite. They also have to be explicitly enabled
-by using either ``-Dconfig_tests=enabled`` for postinstall config tests, or
-``-Dextra_tests=enabled`` for all tests, including deckard tests.
-
-.. code-block:: bash
-
-   $ meson configure build_dir -Dconfig_tests=enabled
-   $ ninja install -C build_dir
-   $ meson test -C build_dir
-
-It's also possible to run only specific test suite or a test.
-
-.. code-block:: bash
-
-   $ meson test -C build_dir --help
-   $ meson test -C build_dir --list
-   $ meson test -C build_dir --no-suite postinstall
-   $ meson test -C build_dir integration.serve_stale
+.. include:: ../tests/README.rst
 
 .. _build-html-doc:
 
diff -pruN 5.4.4-1/doc/config-debugging.rst 5.5.1-5/doc/config-debugging.rst
--- 5.4.4-1/doc/config-debugging.rst	2022-01-05 13:19:14.447727700 +0000
+++ 5.5.1-5/doc/config-debugging.rst	2022-06-14 07:17:30.395490600 +0000
@@ -6,7 +6,7 @@ Debugging options
 In case the resolver crashes, it is often helpful to collect a coredump from
 the crashed process. Configuring the system to collect coredump from crashed
 process is out of the scope of this documentation, but some tips can be found
-`here <https://lists.nic.cz/pipermail/knot-resolver-users/2019/000239.html>`_.
+`here <https://lists.nic.cz/hyperkitty/list/knot-resolver-users@lists.nic.cz/message/GUHW4JSDXZ6SZUAYYQ3U2WWOZEIVVF2S/>`_.
 
 Kresd uses its own mechanism for assertions. They are checks that should always
 pass and indicate some weird or unexpected state if they don't. In such cases,
diff -pruN 5.4.4-1/doc/config-logging-monitoring.rst 5.5.1-5/doc/config-logging-monitoring.rst
--- 5.4.4-1/doc/config-logging-monitoring.rst	2022-01-05 13:19:14.447727700 +0000
+++ 5.5.1-5/doc/config-logging-monitoring.rst	2022-06-14 07:17:30.395490600 +0000
@@ -64,7 +64,7 @@ Less verbose logging for DNSSEC validati
 
 .. py:function:: log_groups([table])
 
-  :param: table of string(s) representing ref:`log groups <config_log_groups>`
+  :param: table of string(s) representing :ref:`log groups <config_log_groups>`
   :return: table of string with currently set log groups
 
   Use to turn-on debug logging for the selected groups regardless of the global
diff -pruN 5.4.4-1/doc/daemon-bindings-net_server.rst 5.5.1-5/doc/daemon-bindings-net_server.rst
--- 5.4.4-1/doc/daemon-bindings-net_server.rst	2022-01-05 13:19:14.427727200 +0000
+++ 5.5.1-5/doc/daemon-bindings-net_server.rst	2022-06-14 07:17:30.375490400 +0000
@@ -66,6 +66,65 @@ Examples:
         addresses if the network address ranges overlap,
         and clients would probably refuse such a response.
 
+.. _proxyv2:
+
+PROXYv2 protocol
+^^^^^^^^^^^^^^^^
+
+Knot Resolver supports proxies that utilize the `PROXYv2 protocol <https://www.haproxy.org/download/2.5/doc/proxy-protocol.txt>`_
+to identify clients.
+
+A PROXY header contains the IP address of the original client who sent a query.
+This allows the resolver to treat queries as if they actually came from
+the client's IP address rather than the address of the proxy they came through.
+For example, :ref:`Views and ACLs <mod-view>` are able to work properly when
+PROXYv2 is in use.
+
+Since allowing usage of the PROXYv2 protocol for all clients would be a security
+vulnerability, because clients would then be able to spoof their IP addresses via
+the PROXYv2 header, the resolver requires you to specify explicitly which clients
+are allowed to send PROXYv2 headers via the :func:`net.proxy_allowed` function.
+
+PROXYv2 queries from clients who are not explicitly allowed to use this protocol
+will be discarded.
+
+.. function:: net.proxy_allowed([addresses])
+
+   Allow usage of the PROXYv2 protocol headers by clients on the specified
+   ``addresses``. It is possible to permit whole networks to send PROXYv2 headers
+   by specifying the network mask using the CIDR notation
+   (e.g. ``172.22.0.0/16``). IPv4 as well as IPv6 addresses are supported.
+
+   If you wish to allow all clients to use PROXYv2 (e.g. because you have this
+   kind of security handled on another layer of your network infrastructure),
+   you can specify a netmask of ``/0``. Please note that this setting is
+   address-family-specific, so this needs to be applied to both IPv4 and IPv6
+   separately.
+
+   Subsequent calls to the function overwrite the effects of all previous calls.
+   Providing a table of strings as the function parameter allows multiple
+   distinct addresses to use the PROXYv2 protocol.
+
+   When called without arguments, ``net.proxy_allowed`` returns a table of all
+   addresses currently allowed to use the PROXYv2 protocol and does not change
+   the configuration.
+
+Examples:
+
+   .. code-block:: lua
+
+	net.proxy_allowed('172.22.0.1')    -- allows '172.22.0.1' specifically
+	net.proxy_allowed('172.18.1.0/24') -- allows everyone at '172.18.1.*'
+	net.proxy_allowed({
+		'172.22.0.1', '172.18.1.0/24'
+	})                                 -- allows both of the above at once
+	net.proxy_allowed({ 'fe80::/10' }  -- allows everyone at IPv6 link-local
+	net.proxy_allowed({
+		'::/0', '0.0.0.0/0'
+	})                                 -- allows everyone
+	net.proxy_allowed('::/0')          -- allows all IPv6 (but no IPv4)
+	net.proxy_allowed({})              -- prevents everyone from using PROXYv2
+	net.proxy_allowed()                -- returns a list of all currently allowed addresses
 
 Features for scripting
 ^^^^^^^^^^^^^^^^^^^^^^
diff -pruN 5.4.4-1/doc/daemon-bindings-net_tlssrv.rst 5.5.1-5/doc/daemon-bindings-net_tlssrv.rst
--- 5.4.4-1/doc/daemon-bindings-net_tlssrv.rst	2022-01-05 13:19:14.427727200 +0000
+++ 5.5.1-5/doc/daemon-bindings-net_tlssrv.rst	2022-06-14 07:17:30.375490400 +0000
@@ -68,6 +68,28 @@ additional considerations for TLS 1.2 re
 
 .. _dot-doh-config-options:
 
+HTTP status codes
+"""""""""""""""""
+
+As specified by :rfc:`8484`, the resolver responds with status **200 OK** whenever
+it can produce a valid DNS reply for a given query, even in cases where the DNS
+``rcode`` indicates an error (like ``NXDOMAIN``, ``SERVFAIL``, etc.).
+
+For DoH queries malformed at the HTTP level, the resolver may respond with
+the following status codes:
+
+ * **400 Bad Request** for a generally malformed query, like one not containing
+   a valid DNS packet
+ * **404 Not Found** when an incorrect HTTP endpoint is queried - the only
+   supported ones are ``/dns-query`` and ``/doh``
+ * **413 Payload Too Large** when the DNS query exceeds its maximum size
+ * **415 Unsupported Media Type** when the query's ``Content-Type`` header
+   is not ``application/dns-message``
+ * **431 Request Header Fields Too Large** when a header in the query is too
+   large to process
+ * **501 Not Implemented** when the query uses a method other than
+   ``GET``, ``POST``, or ``HEAD``
+
 Configuration options for DoT and DoH
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
@@ -113,15 +135,18 @@ by a trusted CA. This is done using func
    This synchronization works only among instances having the same endianness
    and time_t structure and size (`sizeof(time_t)`).
 
+.. _pfs: https://en.wikipedia.org/wiki/Forward_secrecy
+
    **For good security** the secret must have enough entropy to be hard to guess,
    and it should still be occasionally rotated manually and securely forgotten,
    to reduce the scope of privacy leak in case the
-   `secret leaks eventually <https://en.wikipedia.org/wiki/Forward_secrecy>`_.
+   `secret leaks eventually <pfs_>`_.
 
-   .. warning:: **Setting the secret is probably too risky with TLS <= 1.2**.
-      GnuTLS stable release supports TLS 1.3 since 3.6.3 (summer 2018).
-      Therefore setting the secrets should be considered experimental for now
-      and might not be available on your system.
+   .. warning:: **Setting the secret is probably too risky with TLS <= 1.2 and
+      GnuTLS < 3.7.5**. GnuTLS 3.7.5 adds an option to disable resumption via
+      tickets for TLS <= 1.2, enabling them only for protocols that do guarantee
+      `PFS <pfs_>`_. Knot Resolver makes use of this new option when linked
+      against GnuTLS >= 3.7.5.
 
 .. function:: net.tls_sticket_secret_file([string with path to a file containing pre-shared secret])
 
diff -pruN 5.4.4-1/doc/meson.build 5.5.1-5/doc/meson.build
--- 5.4.4-1/doc/meson.build	2022-01-05 13:19:14.447727700 +0000
+++ 5.5.1-5/doc/meson.build	2022-06-14 07:17:30.395490600 +0000
@@ -4,7 +4,7 @@
 # man page
 man_config = configuration_data()
 man_config.set('version', meson.project_version())
-man_config.set('date', run_command('../scripts/get-date.sh').stdout())
+man_config.set('date', run_command('../scripts/get-date.sh', check: true).stdout())
 
 man_config.set('man_seealso_systemd', '')
 if systemd_files == 'enabled'
@@ -30,20 +30,20 @@ if get_option('doc') == 'enabled'
   makeinfo = find_program('makeinfo', required: false)
 
   # python dependencies: breathe, sphinx_rtd_theme
-  python_breathe = run_command('python3', '-c', 'import breathe')
+  python_breathe = run_command('python3', '-c', 'import breathe', check: false)
   if python_breathe.returncode() != 0
     # some distros might use python2 sphinx
-    python_breathe = run_command('python2', '-c', 'import breathe')
+    python_breathe = run_command('python2', '-c', 'import breathe', check: false)
     if python_breathe.returncode() != 0
       error('missing doc dependency: python breathe')
     else
-      python_sphinx_rtd_theme = run_command('python2', '-c', 'import sphinx_rtd_theme')
+      python_sphinx_rtd_theme = run_command('python2', '-c', 'import sphinx_rtd_theme', check: false)
       if python_sphinx_rtd_theme.returncode() != 0
 	error('missing doc dependency: python sphinx_rtd_theme')
       endif
     endif
   else
-    python_sphinx_rtd_theme = run_command('python3', '-c', 'import sphinx_rtd_theme')
+    python_sphinx_rtd_theme = run_command('python3', '-c', 'import sphinx_rtd_theme', check: false)
     if python_sphinx_rtd_theme.returncode() != 0
       error('missing doc dependency: python sphinx_rtd_theme')
     endif
diff -pruN 5.4.4-1/doc/modules-dnstap.rst 5.5.1-5/doc/modules-dnstap.rst
--- 5.4.4-1/doc/modules-dnstap.rst	2022-01-05 13:19:14.471727800 +0000
+++ 5.5.1-5/doc/modules-dnstap.rst	2022-06-14 07:17:30.411491000 +0000
@@ -10,6 +10,8 @@ socket in `dnstap format <https://dnstap
 This logging is useful if you need effectively log all DNS traffic.
 
 The unix socket and the socket reader must be present before starting resolver instances.
+Also it needs appropriate filesystem permissions;
+the typical user and group of the daemon are called ``knot-resolver``.
 
 Tunables:
 
diff -pruN 5.4.4-1/doc/modules-hints.rst 5.5.1-5/doc/modules-hints.rst
--- 5.4.4-1/doc/modules-hints.rst	2022-01-05 13:19:14.479728000 +0000
+++ 5.5.1-5/doc/modules-hints.rst	2022-06-14 07:17:30.415490900 +0000
@@ -32,8 +32,26 @@ Examples
     -- Add a custom hint
     hints['foo.bar'] = '127.0.0.1'
 
-.. note:: The :ref:`policy <mod-policy>` module applies before hints, meaning e.g. that hints for special names (:rfc:`6761#section-6`) like ``localhost`` or ``test`` will get shadowed by policy rules by default.
-    That can be worked around e.g. by explicit :any:`policy.PASS` action.
+.. note::
+   The :ref:`policy <mod-policy>` module applies before hints,
+   so your hints might get surprisingly shadowed by even default policies.
+
+   That most often happens for :rfc:`6761#section-6` names, e.g.
+   ``localhost`` and ``test`` or with ``PTR`` records in private address ranges.
+   To unblock the required names, you may use an explicit :any:`policy.PASS` action.
+
+   .. code-block:: lua
+
+      policy.add(policy.suffix(policy.PASS, {todname('1.168.192.in-addr.arpa')}))
+
+   This ``.PASS`` workaround isn't ideal.  To improve some cases,
+   we recommend to move these ``.PASS`` lines to the end of your rule list.
+   The point is that applying any :ref:`non-chain action <mod-policy-actions>`
+   (e.g. :ref:`forwarding actions <forwarding>` or ``.PASS`` itself)
+   stops processing *any* later policy rules for that request (including the default block-rules).
+   You probably don't want this ``.PASS`` to shadow any other rules you might have;
+   and on the other hand, if any other non-chain rule triggers,
+   additional ``.PASS`` would not change anything even if it were somehow force-executed.
 
 Properties
 ----------
diff -pruN 5.4.4-1/doc/modules-policy.rst 5.5.1-5/doc/modules-policy.rst
--- 5.4.4-1/doc/modules-policy.rst	2022-01-05 13:19:14.491728300 +0000
+++ 5.5.1-5/doc/modules-policy.rst	2022-06-14 07:17:30.427491200 +0000
@@ -36,12 +36,21 @@ A *filter* selects which queries will be
 
    Applies the action if query name suffix matches one of suffixes in the table (useful for "is domain in zone" rules).
 
-.. note:: For speed this filter requires domain names in DNS wire format, not textual representation, so each label in the name must be prefixed with its length. Always use convenience function :func:`policy.todnames` for automatic conversion from strings! For example:
-
    .. code-block:: lua
 
       policy.add(policy.suffix(policy.DENY, policy.todnames({'example.com', 'example.net'})))
 
+.. note:: For speed this filter requires domain names in DNS wire format, not textual representation, so each label in the name must be prefixed with its length. Always use convenience function :func:`policy.todnames` for automatic conversion from strings! For example:
+
+.. _IDN:
+
+.. note:: Non-ASCII is not supported.
+
+   Knot Resolver does not provide any convenience support for IDN.
+   Therefore everywhere (all configuration, logs, RPZ files) you need to deal with the
+   `xn\-\- forms <https://en.wikipedia.org/wiki/Internationalized_domain_name#Example_of_IDNA_encoding>`_
+   of domain name labels, instead of directly using unicode characters.
+
 .. function:: domains(action, domain_table)
 
    Like :func:`policy.suffix` match, but the queried name must match exactly, not just its suffix.
@@ -123,9 +132,18 @@ Following actions stop the policy matchi
 
    Deny existence of names matching filter, i.e. reply NXDOMAIN authoritatively.
 
-.. function:: DENY_MSG(message)
+.. function:: DENY_MSG(message, [extended_error=kres.extended_error.BLOCKED])
 
-   Deny existence of a given domain and add explanatory message. NXDOMAIN reply contains an additional explanatory message as TXT record in the additional section.
+   Deny existence of a given domain and add explanatory message. NXDOMAIN reply
+   contains an additional explanatory message as TXT record in the additional
+   section.
+
+   You may override the extended DNS error to provide the user with more
+   information. By default, ``BLOCKED`` is returned to indicate the domain is
+   blocked due to the internal policy of the operator. Other suitable error
+   codes are ``CENSORED`` (for externally imposed policy reasons) or
+   ``FILTERED`` (for blocking requested by the client). For more information,
+   please refer to :rfc:`8914`.
 
 .. py:attribute:: DROP
 
@@ -135,6 +153,17 @@ Following actions stop the policy matchi
 
    Terminate query resolution and return REFUSED to the requestor.
 
+.. py:attribute:: NO_ANSWER
+
+   Terminate query resolution and do not return any answer to the requestor.
+
+   .. warning:: During normal operation, an answer should always be returned.
+      Deliberate query drops are indistinguishable from packet loss and may
+      cause problems as described in :rfc:`8906`. Only use :any:`NO_ANSWER`
+      on very specific occasions, e.g. as a defense mechanism during an attack,
+      and prefer other actions (e.g. :any:`DROP` or :any:`REFUSE`) for normal
+      operation.
+
 .. py:attribute:: TC
 
    Force requestor to use TCP. It sets truncated bit (*TC*) in response to true if the request came through UDP, which will force standard-compliant clients to retry the request over TCP.
@@ -292,6 +321,24 @@ They are marked as ``debug`` level, so e
    It makes most sense together with :ref:`mod-view` (enabling per-client)
    and probably with verbose logging those request (e.g. use :any:`DEBUG_ALWAYS` instead).
 
+.. py:attribute:: IPTRACE
+
+   Log how the request arrived.
+   Most notably, this includes the client's IP address, so beware of privacy implications.
+
+   .. code-block:: lua
+
+        -- example usage in configuration
+        policy.add(policy.all(policy.IPTRACE))
+        -- you might want to combine it with some other logs, e.g.
+        policy.add(policy.all(policy.DEBUG_ALWAYS))
+
+   .. code-block:: text
+
+        -- example log lines from IPTRACE:
+        [reqdbg][policy][57517.00] request packet arrived from ::1#37931 to ::1#00853 (TCP + TLS)
+        [reqdbg][policy][65538.00] request packet arrived internally
+
 
 Custom actions
 ^^^^^^^^^^^^^^
@@ -380,6 +427,20 @@ Actions :func:`policy.FORWARD`, :func:`p
    `0x20 randomization <https://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00>`_.
    See example in `Replacing part of the DNS tree`_.
 
+.. warning::
+   Limiting forwarding actions by filters (e.g. :func:`policy.suffix`) may have unexpected consequences.
+   Notably, forwarders can inject *any* records into your cache
+   even if you "restrict" them to an insignificant DNS subtree --
+   except in cases where DNSSEC validation applies, of course.
+
+   The behavior is probably best understood through the fact
+   that filters and actions are completely decoupled.
+   The forwarding actions have no clue about why they were executed,
+   e.g. that the user wanted to restrict the forwarder only to some subtree.
+   The action just selects some set of forwarders to process this whole request from the client,
+   and during that processing it might need some other "sub-queries" (e.g. for validation).
+   Some of those might not've passed the intended filter,
+   but policy rule-set only applies once per client's request.
 
 .. _tls-forwarding:
 
@@ -631,6 +692,17 @@ Response policy zones
   .. [#] Our :any:`policy.DROP` returns *SERVFAIL* answer (for historical reasons).
 
 
+  .. note::
+
+     To debug which domains are affected by RPZ (or other policy actions), you can enable the ``policy`` log group:
+
+     .. code-block:: lua
+
+        log_groups({'policy'})
+
+     See also :ref:`non-ASCII support note <IDN>`.
+
+
 .. function:: rpz(action, path, [watch = true])
 
   :param action: the default action for match in the zone; typically you want :any:`policy.DENY`
diff -pruN 5.4.4-1/doc/modules-predict.rst 5.5.1-5/doc/modules-predict.rst
--- 5.4.4-1/doc/modules-predict.rst	2022-01-05 13:19:14.491728300 +0000
+++ 5.5.1-5/doc/modules-predict.rst	2022-06-14 07:17:30.427491200 +0000
@@ -22,7 +22,7 @@ Prediction
 ----------
 
 The predict module can also learn usage patterns and repetitive queries,
-though this mechanism is basically a prototype.
+though this mechanism is a prototype and **not recommended** for use in production or with high traffic.
 
 For example, if it makes a query every day at 18:00,
 the resolver expects that it is needed by that time and prefetches it ahead of time.
@@ -41,13 +41,12 @@ Example configuration
 
 	modules = {
 		predict = {
+                        -- this mode is NOT RECOMMENDED for use in production
 			window = 15, -- 15 minutes sampling window
 			period = 6*(60/15) -- track last 6 hours
 		}
 	}
 
-Defaults are as above: 15 minutes window, 6 hours period.
-
 Exported metrics
 ----------------
 
diff -pruN 5.4.4-1/doc/modules-renumber.rst 5.5.1-5/doc/modules-renumber.rst
--- 5.4.4-1/doc/modules-renumber.rst	2022-01-05 13:19:14.495728300 +0000
+++ 5.5.1-5/doc/modules-renumber.rst	2022-06-14 07:17:30.427491200 +0000
@@ -15,9 +15,6 @@ in local zones, that will be remapped to
    breaks signatures. You can see whether an answer was valid or not based on
    the AD flag.
 
-.. warning:: The module is currently limited to rewriting complete octets of
-   the IP addresses, i.e. only /8, /16, /24 etc. network masks are supported.
-
 Example configuration
 ---------------------
 
@@ -28,6 +25,12 @@ Example configuration
 			-- Source subnet, destination subnet
 			{'10.10.10.0/24', '192.168.1.0'},
 			-- Remap /16 block to localhost address range
-			{'166.66.0.0/16', '127.0.0.0'}
+			{'166.66.0.0/16', '127.0.0.0'},
+			-- Remap /26 subnet (64 ip addresses)
+			{'166.55.77.128/26', '127.0.0.192'},
+			-- Remap a /32 block to a single address
+			{'2001:db8::/32', '::1!'},
 		}
 	}
+
+.. TODO: renumber.name() hangs in vacuum, kind of.  No occurrences in code or docs, and probably bad UX.
diff -pruN 5.4.4-1/doc/upgrading.rst 5.5.1-5/doc/upgrading.rst
--- 5.4.4-1/doc/upgrading.rst	2022-01-05 13:19:14.451727600 +0000
+++ 5.5.1-5/doc/upgrading.rst	2022-06-14 07:17:30.395490600 +0000
@@ -28,6 +28,25 @@ newer versions when they are released.
 .. _`systemd`: https://systemd.io/
 .. _`supervisord`: http://supervisord.org/
 
+
+5.4 to 5.5
+==========
+
+Packagers & Developers
+----------------------
+
+* Knot DNS >= 3.0.2 is required.
+
+Module API changes
+------------------
+* Function `cache.zone_import` was removed;
+  you can use `ffi.C.zi_zone_import` instead (different API).
+* When using :ref:`proxyv2`, the meaning of ``qsource.flags`` and ``qsource.comm_flags``
+  in :c:member:`kr_request` changes so that ``flags`` describes the original client
+  communicating with the proxy, while ``comm_flags`` describes the proxy communicating
+  with the resolver. When there is no proxy, ``flags`` and ``comm_flags`` are the same.
+
+
 5.3 to 5.4
 ==========
 
diff -pruN 5.4.4-1/etc/config/meson.build 5.5.1-5/etc/config/meson.build
--- 5.4.4-1/etc/config/meson.build	2022-01-05 13:19:14.451727600 +0000
+++ 5.5.1-5/etc/config/meson.build	2022-06-14 07:17:30.399490600 +0000
@@ -21,7 +21,7 @@ install_data(
 # kresd.conf
 install_kresd_conf = get_option('install_kresd_conf') == 'enabled'
 if get_option('install_kresd_conf') == 'auto'
-  if run_command(['test', '-r', etc_dir / 'kresd.conf']).returncode() == 1
+  if run_command(['test', '-r', etc_dir / 'kresd.conf'], check: false).returncode() == 1
     install_kresd_conf = true
   endif
 endif
diff -pruN 5.4.4-1/lib/cache/api.c 5.5.1-5/lib/cache/api.c
--- 5.4.4-1/lib/cache/api.c	2022-01-05 13:19:14.455727800 +0000
+++ 5.5.1-5/lib/cache/api.c	2022-06-14 07:17:30.399490600 +0000
@@ -237,8 +237,13 @@ int32_t get_new_ttl(const struct entry_h
 	if (res < 0 && owner && qry && qry->stale_cb) {
 		/* Stale-serving decision, delegated to a callback. */
 		int res_stale = qry->stale_cb(res, owner, type, qry);
-		if (res_stale >= 0)
+		if (res_stale >= 0) {
+			VERBOSE_MSG(qry, "responding with stale answer\n");
+			/* LATER: Perhaps we could use a more specific Stale
+			 * NXDOMAIN Answer code for applicable responses. */
+			kr_request_set_extended_error(qry->request, KNOT_EDNS_EDE_STALE, "6Q6X");
 			return res_stale;
+		}
 	}
 	return res;
 }
diff -pruN 5.4.4-1/lib/cache/api.h 5.5.1-5/lib/cache/api.h
--- 5.4.4-1/lib/cache/api.h	2022-01-05 13:19:14.455727800 +0000
+++ 5.5.1-5/lib/cache/api.h	2022-06-14 07:17:30.399490600 +0000
@@ -11,10 +11,6 @@
 #include "lib/defines.h"
 #include "contrib/ucw/config.h" /*uint*/
 
-/** When knot_pkt is passed from cache without ->wire, this is the ->size. */
-static const size_t PKT_SIZE_NOWIRE = -1;
-
-
 #include "lib/module.h"
 /* Prototypes for the 'cache' module implementation. */
 int cache_peek(kr_layer_t *ctx, knot_pkt_t *pkt);
@@ -37,6 +33,8 @@ struct kr_cache
 
 	uv_timer_t *health_timer; /**< Timer used for kr_cache_check_health() */
 };
+// https://datatracker.ietf.org/doc/html/rfc2181#section-8
+#define TTL_MAX_MAX ((1u << 31) - 1)
 
 /**
  * Open/create cache with provided storage options.
diff -pruN 5.4.4-1/lib/cache/cdb_lmdb.c 5.5.1-5/lib/cache/cdb_lmdb.c
--- 5.4.4-1/lib/cache/cdb_lmdb.c	2022-01-05 13:19:14.455727800 +0000
+++ 5.5.1-5/lib/cache/cdb_lmdb.c	2022-06-14 07:17:30.399490600 +0000
@@ -15,8 +15,6 @@
 #include "contrib/ucw/lib.h"
 #include "lib/cache/cdb_lmdb.h"
 #include "lib/cache/cdb_api.h"
-#include "lib/cache/api.h"
-#include "lib/cache/impl.h"
 #include "lib/utils.h"
 
 
@@ -381,9 +379,10 @@ static int cdb_open_env(struct lmdb_env
 	} else {
 		ret = 0;
 	}
-	if (ret == EINVAL) {
+	if (ret == EINVAL || ret == EOPNOTSUPP) {
 		/* POSIX says this can happen when the feature isn't supported by the FS.
-		 * We haven't seen this happen on Linux+glibc but it was reported on FreeBSD.*/
+		 * We haven't seen this happen on Linux+glibc but it was reported on
+		 * Linux+musl and FreeBSD. */
 		kr_log_info(CACHE, "space pre-allocation failed and ignored; "
 				"your (file)system probably doesn't support it.\n");
 	} else if (ret != 0) {
diff -pruN 5.4.4-1/lib/cache/entry_pkt.c 5.5.1-5/lib/cache/entry_pkt.c
--- 5.4.4-1/lib/cache/entry_pkt.c	2022-01-05 13:19:14.455727800 +0000
+++ 5.5.1-5/lib/cache/entry_pkt.c	2022-06-14 07:17:30.399490600 +0000
@@ -18,7 +18,7 @@ KR_EXPORT
 uint32_t packet_ttl(const knot_pkt_t *pkt, bool is_negative)
 {
 	bool has_ttl = false;
-	uint32_t ttl = UINT32_MAX;
+	uint32_t ttl = TTL_MAX_MAX;
 	/* Find minimum entry TTL in the packet or SOA minimum TTL. */
 	for (knot_section_t i = KNOT_ANSWER; i <= KNOT_ADDITIONAL; ++i) {
 		const knot_pktsection_t *sec = knot_pkt_section(pkt, i);
diff -pruN 5.4.4-1/lib/cache/impl.h 5.5.1-5/lib/cache/impl.h
--- 5.4.4-1/lib/cache/impl.h	2022-01-05 13:19:14.455727800 +0000
+++ 5.5.1-5/lib/cache/impl.h	2022-06-14 07:17:30.399490600 +0000
@@ -417,7 +417,7 @@ int nsec3_src_synth(struct key *k, struc
 
 
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE((qry), CACHE,  ## __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q((qry), CACHE,  ## __VA_ARGS__)
 #define WITH_VERBOSE(qry) if (kr_log_is_debug_qry(CACHE, (qry)))
 
 /** Shorthand for operations on cache backend */
diff -pruN 5.4.4-1/lib/cache/knot_pkt.c 5.5.1-5/lib/cache/knot_pkt.c
--- 5.4.4-1/lib/cache/knot_pkt.c	2022-01-05 13:19:14.455727800 +0000
+++ 5.5.1-5/lib/cache/knot_pkt.c	2022-06-14 07:17:30.399490600 +0000
@@ -20,7 +20,7 @@ int pkt_renew(knot_pkt_t *pkt, const kno
 		if (ret) return kr_error(ret);
 	}
 
-	pkt->parsed = pkt->size = PKT_SIZE_NOWIRE;
+	pkt->parsed = pkt->size = KR_PKT_SIZE_NOWIRE;
 	knot_wire_set_qr(pkt->wire);
 	knot_wire_set_aa(pkt->wire);
 	return kr_ok();
diff -pruN 5.4.4-1/lib/cache/peek.c 5.5.1-5/lib/cache/peek.c
--- 5.4.4-1/lib/cache/peek.c	2022-01-05 13:19:14.455727800 +0000
+++ 5.5.1-5/lib/cache/peek.c	2022-06-14 07:17:30.399490600 +0000
@@ -75,7 +75,7 @@ static int nsec_p_ttl(knot_db_val_t entr
 static uint8_t get_lowest_rank(const struct kr_query *qry, const knot_dname_t *name, const uint16_t type)
 {
 	/* Shut up linters. */
-	if (unlikely(!qry || !qry->request)) abort();
+	kr_require(qry && qry->request);
 	/* TODO: move rank handling into the iterator (DNSSEC_* flags)? */
 	const bool allow_unverified =
 		knot_wire_get_cd(qry->request->qsource.packet->wire) || qry->flags.STUB;
diff -pruN 5.4.4-1/lib/defines.h 5.5.1-5/lib/defines.h
--- 5.4.4-1/lib/defines.h	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/defines.h	2022-06-14 07:17:30.403490800 +0000
@@ -8,6 +8,7 @@
 #include <libknot/errcode.h>
 #include <libknot/dname.h>
 #include <libknot/rrset.h>
+#include <libknot/version.h>
 
 /* Function attributes */
 #if __GNUC__ >= 4
@@ -70,6 +71,11 @@ static inline int KR_COLD kr_error(int x
 #define KR_DNAME_STR_MAXLEN (KNOT_DNAME_TXT_MAXLEN + 1)
 #define KR_RRTYPE_STR_MAXLEN (16 + 1)
 
+/* Compatibility with libknot<3.1.0 */
+#if KNOT_VERSION_HEX < 0x030100
+#define KNOT_EDNS_EDE_NONE (-1)
+#endif
+
 /*
  * Address sanitizer hints.
  */
diff -pruN 5.4.4-1/lib/dnssec/nsec3.c 5.5.1-5/lib/dnssec/nsec3.c
--- 5.4.4-1/lib/dnssec/nsec3.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/dnssec/nsec3.c	2022-06-14 07:17:30.403490800 +0000
@@ -650,7 +650,6 @@ int kr_nsec3_ref_to_unsigned(const knot_
 		if (ns->type != KNOT_RRTYPE_NS)
 			continue;
 
-		int flags = 0;
 		bool nsec3_found = false;
 		for (unsigned j = 0; j < sec->count; ++j) {
 			const knot_rrset_t *nsec3 = knot_pkt_rr(sec, j);
@@ -680,13 +679,6 @@ int kr_nsec3_ref_to_unsigned(const knot_
 		}
 		if (!nsec3_found)
 			return kr_error(DNSSEC_NOT_FOUND);
-		if (flags & FLG_NAME_MATCHED) {
-			/* nsec3 which owner matches
-			 * the delegation name was found,
-			 * but nsec3 type bitmap contains wrong types
-			 */
-			return kr_error(EINVAL);
-		}
 		/* nsec3 that matches the delegation was not found.
 		 * Check rfc5155, 8.9. paragraph 4.
 		 * Find closest provable encloser.
diff -pruN 5.4.4-1/lib/dnssec/nsec.c 5.5.1-5/lib/dnssec/nsec.c
--- 5.4.4-1/lib/dnssec/nsec.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/dnssec/nsec.c	2022-06-14 07:17:30.403490800 +0000
@@ -16,11 +16,11 @@
 #include "lib/defines.h"
 #include "lib/dnssec/nsec.h"
 #include "lib/utils.h"
-
+#include "resolve.h"
 
 int kr_nsec_children_in_zone_check(const uint8_t *bm, uint16_t bm_size)
 {
-	if (!bm)
+	if (kr_fails_assert(bm))
 		return kr_error(EINVAL);
 	const bool parent_side =
 		dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_DNAME)
@@ -94,26 +94,28 @@ static int dname_cmp(const knot_dname_t
 
 
 /**
- * Check whether the NSEC RR proves that there is no closer match for <SNAME, SCLASS>.
+ * Check whether this nsec proves that there is no closer match for sname.
+ *
  * @param nsec  NSEC RRSet.
  * @param sname Searched name.
- * @return      0 if proves, >0 if not (abs(ENOENT)), or error code (<0).
+ * @return      0 if proves, >0 if not (abs(ENOENT) or abs(EEXIST)), or error code (<0).
  */
 static int nsec_covers(const knot_rrset_t *nsec, const knot_dname_t *sname)
 {
 	if (kr_fails_assert(nsec && sname))
 		return kr_error(EINVAL);
-	if (dname_cmp(sname, nsec->owner) <= 0)
-		return abs(ENOENT); /* 'sname' before 'owner', so can't be covered */
+	const int cmp = dname_cmp(sname, nsec->owner);
+	if (cmp  < 0) return abs(ENOENT); /* 'sname' before 'owner', so can't be covered */
+	if (cmp == 0) return abs(EEXIST); /* matched, not covered */
 
-	/* If NSEC 'owner' >= 'next', it means that there is nothing after 'owner' */
-	/* We have to lower-case it with libknot >= 2.7; see also RFC 6840 5.1. */
+	/* We have to lower-case 'next' with libknot >= 2.7; see also RFC 6840 5.1. */
 	knot_dname_t next[KNOT_DNAME_MAXLEN];
 	int ret = knot_dname_to_wire(next, knot_nsec_next(nsec->rrs.rdata), sizeof(next));
 	if (kr_fails_assert(ret >= 0))
 		return kr_error(ret);
 	knot_dname_to_lower(next);
 
+	/* If NSEC 'owner' >= 'next', it means that there is nothing after 'owner' */
 	const bool is_last_nsec = dname_cmp(nsec->owner, next) >= 0;
 	const bool in_range = is_last_nsec || dname_cmp(sname, next) < 0;
 	if (!in_range)
@@ -130,140 +132,6 @@ static int nsec_covers(const knot_rrset_
 	return kr_nsec_children_in_zone_check(bm, bm_size);
 }
 
-#define FLG_NOEXIST_RRTYPE (1 << 0) /**< <SNAME, SCLASS> exists, <SNAME, SCLASS, STYPE> does not exist. */
-#define FLG_NOEXIST_RRSET  (1 << 1) /**< <SNAME, SCLASS> does not exist. */
-#define FLG_NOEXIST_WILDCARD (1 << 2) /**< No wildcard covering <SNAME, SCLASS> exists. */
-#define FLG_NOEXIST_CLOSER (1 << 3) /**< Wildcard covering <SNAME, SCLASS> exists, but doesn't match STYPE. */
-
-
-/**
- * According to set flags determine whether NSEC proving
- * RRset or RRType non-existence has been found.
- * @param f Flags to inspect.
- * @return  True if required NSEC exists.
- */
-#define kr_nsec_rrset_noexist(f) \
-        ((f) & (FLG_NOEXIST_RRTYPE | FLG_NOEXIST_RRSET))
-/**
- * According to set flags determine whether wildcard non-existence
- * has been proven.
- * @param f Flags to inspect.
- * @return  True if wildcard not exists.
- */
-#define kr_nsec_wcard_noexist(f) ((f) & FLG_NOEXIST_WILDCARD)
-
-/**
- * According to set flags determine whether authenticated denial of existence has been proven.
- * @param f Flags to inspect.
- * @return  True if denial of existence proven.
- */
-#define kr_nsec_existence_denied(f) \
-	((kr_nsec_rrset_noexist(f)) && (kr_nsec_wcard_noexist(f)))
-
-/**
- * Name error response check (RFC4035 3.1.3.2; RFC4035 5.4, bullet 2).
- * @note Returned flags must be checked in order to prove denial.
- * @param flags Flags to be set according to check outcome.
- * @param nsec  NSEC RR.
- * @param name  Name to be checked.
- * @param pool
- * @return      0 or error code.
- */
-static int name_error_response_check_rr(int *flags, const knot_rrset_t *nsec,
-                                        const knot_dname_t *name)
-{
-	if (kr_fails_assert(flags && nsec && name))
-		return kr_error(EINVAL);
-
-	if (nsec_covers(nsec, name) == 0)
-		*flags |= FLG_NOEXIST_RRSET;
-
-	/* Try to find parent wildcard that is proved by this NSEC. */
-	uint8_t namebuf[KNOT_DNAME_MAXLEN];
-	int ret = knot_dname_to_wire(namebuf, name, sizeof(namebuf));
-	if (ret < 0)
-		return ret;
-	knot_dname_t *ptr = namebuf;
-	while (ptr[0]) {
-		/* Remove leftmost label and replace it with '\1*'. */
-		ptr = (uint8_t *) knot_wire_next_label(ptr, NULL);
-		if (!ptr)
-			return kr_error(EINVAL);
-		*(--ptr) = '*';
-		*(--ptr) = 1;
-		/* True if this wildcard provably doesn't exist. */
-		if (nsec_covers(nsec, ptr) == 0) {
-			*flags |= FLG_NOEXIST_WILDCARD;
-			break;
-		}
-		/* Remove added leftmost asterisk. */
-		ptr += 2;
-	}
-
-	return kr_ok();
-}
-
-int kr_nsec_name_error_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
-                                      const knot_dname_t *sname)
-{
-	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
-	if (!sec || !sname)
-		return kr_error(EINVAL);
-
-	int flags = 0;
-	for (unsigned i = 0; i < sec->count; ++i) {
-		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
-		if (rrset->type != KNOT_RRTYPE_NSEC)
-			continue;
-		int ret = name_error_response_check_rr(&flags, rrset, sname);
-		if (ret != 0)
-			return ret;
-	}
-
-	return kr_nsec_existence_denied(flags) ? kr_ok() : kr_error(ENOENT);
-}
-
-/**
- * Returns the labels from the covering RRSIG RRs.
- * @note The number must be the same in all covering RRSIGs.
- * @param nsec NSEC RR.
- * @param sec  Packet section.
- * @param      Number of labels or (negative) error code.
- */
-static int covering_rrsig_labels(const knot_rrset_t *nsec, const knot_pktsection_t *sec)
-{
-	if (kr_fails_assert(nsec && sec))
-		return kr_error(EINVAL);
-
-	int ret = kr_error(ENOENT);
-
-	for (unsigned i = 0; i < sec->count; ++i) {
-		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
-		if ((rrset->type != KNOT_RRTYPE_RRSIG) ||
-		    (!knot_dname_is_equal(rrset->owner, nsec->owner))) {
-			continue;
-		}
-
-		knot_rdata_t *rdata_j = rrset->rrs.rdata;
-		for (uint16_t j = 0; j < rrset->rrs.count;
-				++j, rdata_j = knot_rdataset_next(rdata_j)) {
-			if (knot_rrsig_type_covered(rdata_j) != KNOT_RRTYPE_NSEC)
-				continue;
-
-			if (ret < 0) {
-				ret = knot_rrsig_labels(rdata_j);
-			} else {
-				if (ret != knot_rrsig_labels(rdata_j)) {
-					return kr_error(EINVAL);
-				}
-			}
-		}
-	}
-
-	return ret;
-}
-
-
 int kr_nsec_bitmap_nodata_check(const uint8_t *bm, uint16_t bm_size, uint16_t type, const knot_dname_t *owner)
 {
 	const int NO_PROOF = abs(ENOENT);
@@ -306,114 +174,14 @@ int kr_nsec_bitmap_nodata_check(const ui
 	return kr_ok();
 }
 
-/**
- * Attempt to prove NODATA given a matching NSEC.
- * @param flags Flags to be set according to check outcome.
- * @param nsec  NSEC RR.
- * @param type  Type to be checked.
- * @return      0 on success, abs(ENOENT) for no proof, or error code (<0).
- * @note        It's not a *full* proof, of course (wildcards, etc.)
- * @TODO returning result via `flags` is just ugly.
- */
-static int no_data_response_check_rrtype(int *flags, const knot_rrset_t *nsec,
-                                         uint16_t type)
+/// Convenience wrapper for kr_nsec_bitmap_nodata_check()
+static int no_data_response_check_rrtype(const knot_rrset_t *nsec, uint16_t type)
 {
-	if (kr_fails_assert(flags && nsec))
+	if (kr_fails_assert(nsec && nsec->type == KNOT_RRTYPE_NSEC))
 		return kr_error(EINVAL);
-
 	const uint8_t *bm = knot_nsec_bitmap(nsec->rrs.rdata);
 	uint16_t bm_size = knot_nsec_bitmap_len(nsec->rrs.rdata);
-	int ret = kr_nsec_bitmap_nodata_check(bm, bm_size, type, nsec->owner);
-	if (ret == kr_ok())
-		*flags |= FLG_NOEXIST_RRTYPE;
-	return ret <= 0 ? ret : kr_ok();
-}
-
-/**
- * Perform check for RR type wildcard existence denial according to RFC4035 5.4, bullet 1.
- * @param flags Flags to be set according to check outcome.
- * @param nsec  NSEC RR.
- * @param sec   Packet section to work with.
- * @return      0 or error code.
- */
-static int no_data_wildcard_existence_check(int *flags, const knot_rrset_t *nsec,
-                                            const knot_pktsection_t *sec)
-{
-	if (kr_fails_assert(flags && nsec && sec))
-		return kr_error(EINVAL);
-
-	int rrsig_labels = covering_rrsig_labels(nsec, sec);
-	if (rrsig_labels < 0)
-		return rrsig_labels;
-	int nsec_labels = knot_dname_labels(nsec->owner, NULL);
-	if (nsec_labels < 0)
-		return nsec_labels;
-
-	if (rrsig_labels == nsec_labels)
-		*flags |= FLG_NOEXIST_WILDCARD;
-
-	return kr_ok();
-}
-
-/**
- * Perform check for NSEC wildcard existence that covers sname and
- * have no stype bit set.
- * @param pkt   Packet structure to be processed.
- * @param sec   Packet section to work with.
- * @param sname Queried domain name.
- * @param stype Queried type.
- * @return      0 or error code.
- */
-static int wildcard_match_check(const knot_pkt_t *pkt, const knot_pktsection_t *sec,
-				const knot_dname_t *sname, uint16_t stype)
-{
-	if (!sec || !sname)
-		return kr_error(EINVAL);
-
-	int flags = 0;
-	for (unsigned i = 0; i < sec->count; ++i) {
-		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
-		if (rrset->type != KNOT_RRTYPE_NSEC)
-			continue;
-		if (!knot_dname_is_wildcard(rrset->owner))
-			continue;
-		if (!knot_dname_is_equal(rrset->owner, sname)) {
-			int wcard_labels = knot_dname_labels(rrset->owner, NULL);
-			int common_labels = knot_dname_matched_labels(rrset->owner, sname);
-			int rrsig_labels = covering_rrsig_labels(rrset, sec);
-			if (wcard_labels < 1 ||
-			    common_labels != wcard_labels - 1 ||
-			    common_labels != rrsig_labels) {
-				continue;
-			}
-		}
-		int ret = no_data_response_check_rrtype(&flags, rrset, stype);
-		if (ret != 0)
-			return ret;
-	}
-	return (flags & FLG_NOEXIST_RRTYPE) ? kr_ok() : kr_error(ENOENT);
-}
-
-int kr_nsec_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
-                                   const knot_dname_t *sname, uint16_t stype)
-{
-	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
-	if (!sec || !sname)
-		return kr_error(EINVAL);
-
-	int flags = 0;
-	for (unsigned i = 0; i < sec->count; ++i) {
-		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
-		if (rrset->type != KNOT_RRTYPE_NSEC)
-			continue;
-		if (knot_dname_is_equal(rrset->owner, sname)) {
-			int ret = no_data_response_check_rrtype(&flags, rrset, stype);
-			if (ret != 0)
-				return ret;
-		}
-	}
-
-	return (flags & FLG_NOEXIST_RRTYPE) ? kr_ok() : kr_error(ENOENT);
+	return kr_nsec_bitmap_nodata_check(bm, bm_size, type, nsec->owner);
 }
 
 int kr_nsec_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
@@ -434,116 +202,111 @@ int kr_nsec_wildcard_answer_response_che
 	return kr_error(ENOENT);
 }
 
-int kr_nsec_existence_denial(const knot_pkt_t *pkt, knot_section_t section_id,
-                             const knot_dname_t *sname, uint16_t stype)
+int kr_nsec_negative(const ranked_rr_array_t *rrrs, uint32_t qry_uid,
+			const knot_dname_t *sname, uint16_t stype)
 {
-	const knot_pktsection_t *sec = knot_pkt_section(pkt, section_id);
-	if (!sec || !sname)
-		return kr_error(EINVAL);
-
-	int flags = 0;
-	for (unsigned i = 0; i < sec->count; ++i) {
-		const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
-		if (rrset->type != KNOT_RRTYPE_NSEC)
-			continue;
-		/* NSEC proves that name exists, but has no data (RFC4035 4.9, 1) */
-		if (knot_dname_is_equal(rrset->owner, sname)) {
-			no_data_response_check_rrtype(&flags, rrset, stype);
-		} else {
-			/* NSEC proves that name doesn't exist (RFC4035, 4.9, 2) */
-			name_error_response_check_rr(&flags, rrset, sname);
+	// We really only consider the (canonically) first NSEC in each RRset.
+	// Using same owner with differing content probably isn't useful for NSECs anyway.
+	// Many other parts of code do the same, too.
+	if (kr_fails_assert(rrrs && sname))
+		return kr_error(EINVAL);
+
+	// Terminology: https://datatracker.ietf.org/doc/html/rfc4592#section-3.3.1
+	int clencl_labels = -1; // the label count of the closest encloser of sname
+	for (int i = rrrs->len - 1; i >= 0; --i) { // NSECs near the end typically
+		const knot_rrset_t *nsec = rrrs->at[i]->rr;
+		bool ok = rrrs->at[i]->qry_uid == qry_uid
+			&& nsec->type == KNOT_RRTYPE_NSEC
+			&& kr_rank_test(rrrs->at[i]->rank, KR_RANK_SECURE);
+		if (!ok) continue;
+		const int covers = nsec_covers(nsec, sname);
+		if (covers == abs(EEXIST)
+		    && no_data_response_check_rrtype(nsec, stype) == 0) {
+			return PKT_NODATA; // proven NODATA by matching NSEC
 		}
-		no_data_wildcard_existence_check(&flags, rrset, sec);
+		if (covers != 0) continue;
+
+		// We have to lower-case 'next' with libknot >= 2.7; see also RFC 6840 5.1.
+		// LATER(optim.): it's duplicate work with the nsec_covers() call.
+		knot_dname_t next[KNOT_DNAME_MAXLEN];
+		int ret = knot_dname_to_wire(next, knot_nsec_next(nsec->rrs.rdata), sizeof(next));
+		if (kr_fails_assert(ret >= 0))
+			return kr_error(ret);
+		knot_dname_to_lower(next);
+
+		clencl_labels = MAX(knot_dname_matched_labels(nsec->owner, sname),
+				    knot_dname_matched_labels(sname, next));
+		break; // reduce indentation again
 	}
-	if (kr_nsec_existence_denied(flags)) {
-		/* denial of existence proved accordingly to 4035 5.4 -
-		 * NSEC proving either rrset non-existence or
-		 * qtype non-existence has been found,
-		 * and no wildcard expansion occurred.
-		 */
-		return kr_ok();
-	} else if (kr_nsec_rrset_noexist(flags)) {
-		/* NSEC proving either rrset non-existence or
-		 * qtype non-existence has been found,
-		 * but wildcard expansion occurs.
-		 * Try to find matching wildcard and check
-		 * corresponding types.
-		 */
-		return wildcard_match_check(pkt, sec, sname, stype);
+
+	if (clencl_labels < 0)
+		return kr_error(ENOENT);
+	const int sname_labels = knot_dname_labels(sname, NULL);
+	if (sname_labels == clencl_labels)
+		return PKT_NODATA; // proven NODATA; sname is an empty non-terminal
+
+	// Explicitly construct name for the corresponding source of synthesis.
+	knot_dname_t ssynth[KNOT_DNAME_MAXLEN + 2];
+	ssynth[0] = 1;
+	ssynth[1] = '*';
+	const knot_dname_t *clencl = sname;
+	for (int l = sname_labels; l > clencl_labels; --l)
+		clencl = knot_wire_next_label(clencl, NULL);
+	(void)!!knot_dname_store(&ssynth[2], clencl);
+
+	// Try to (dis)prove the source of synthesis by a covering or matching NSEC.
+	for (int i = rrrs->len - 1; i >= 0; --i) { // NSECs near the end typically
+		const knot_rrset_t *nsec = rrrs->at[i]->rr;
+		bool ok = rrrs->at[i]->qry_uid == qry_uid
+			&& nsec->type == KNOT_RRTYPE_NSEC
+			&& kr_rank_test(rrrs->at[i]->rank, KR_RANK_SECURE);
+		if (!ok) continue;
+		const int covers = nsec_covers(nsec, ssynth);
+		if (covers == abs(EEXIST)) {
+			int ret = no_data_response_check_rrtype(nsec, stype);
+			if (ret == 0) return PKT_NODATA; // proven NODATA by wildcard NSEC
+			// TODO: also try expansion?  Or at least a different return code?
+		} else if (covers == 0) {
+			return PKT_NXDOMAIN | PKT_NODATA;
+		}
 	}
 	return kr_error(ENOENT);
 }
 
-int kr_nsec_ref_to_unsigned(const knot_pkt_t *pkt)
+int kr_nsec_ref_to_unsigned(const ranked_rr_array_t *rrrs, uint32_t qry_uid,
+				const knot_dname_t *sname)
 {
-	int nsec_found = 0;
-	const knot_pktsection_t *sec = knot_pkt_section(pkt, KNOT_AUTHORITY);
-	if (!sec)
-		return kr_error(EINVAL);
-	for (unsigned i = 0; i < sec->count; ++i) {
-		const knot_rrset_t *ns = knot_pkt_rr(sec, i);
-		if (ns->type == KNOT_RRTYPE_DS)
-			return kr_error(EEXIST);
-		if (ns->type != KNOT_RRTYPE_NS)
-			continue;
-		nsec_found = 0;
-		for (unsigned j = 0; j < sec->count; ++j) {
-			const knot_rrset_t *nsec = knot_pkt_rr(sec, j);
-			if (nsec->type == KNOT_RRTYPE_DS)
-				return kr_error(EEXIST);
-			if (nsec->type != KNOT_RRTYPE_NSEC)
-				continue;
-			/* nsec found
-			 * check if owner name matches the delegation name
-			 */
-			if (!knot_dname_is_equal(nsec->owner, ns->owner)) {
-				/* nsec does not match the delegation */
-				continue;
-			}
-			nsec_found = 1;
-			const uint8_t *bm = knot_nsec_bitmap(nsec->rrs.rdata);
-			uint16_t bm_size = knot_nsec_bitmap_len(nsec->rrs.rdata);
-			if (!bm)
-				return kr_error(EINVAL);
-			if (dnssec_nsec_bitmap_contains(bm, bm_size,
-							  KNOT_RRTYPE_NS) &&
-			    !dnssec_nsec_bitmap_contains(bm, bm_size,
-							  KNOT_RRTYPE_DS) &&
-			    !dnssec_nsec_bitmap_contains(bm, bm_size,
-							  KNOT_RRTYPE_SOA)) {
-				/* rfc4035, 5.2 */
-				return kr_ok();
-			}
-		}
-		if (nsec_found) {
-			/* nsec which owner matches
-			 * the delegation name was found,
-			 * but nsec type bitmap contains wrong types
-			 */
-			return kr_error(EINVAL);
-		} else {
-			/* nsec that matches delegation was not found */
-			return kr_error(DNSSEC_NOT_FOUND);
-		}
+	for (int i = rrrs->len - 1; i >= 0; --i) { // NSECs near the end typically
+		const knot_rrset_t *nsec = rrrs->at[i]->rr;
+		bool ok = rrrs->at[i]->qry_uid == qry_uid
+			&& nsec->type == KNOT_RRTYPE_NSEC
+			&& kr_rank_test(rrrs->at[i]->rank, KR_RANK_SECURE)
+			// avoid any possibility of getting tricked in deeper zones
+			&& knot_dname_in_bailiwick(sname, nsec->owner) >= 0;
+		if (!ok) continue;
+
+		kr_assert(nsec->rrs.rdata);
+		const uint8_t *bm = knot_nsec_bitmap(nsec->rrs.rdata);
+		uint16_t bm_size = knot_nsec_bitmap_len(nsec->rrs.rdata);
+		ok = ok &&  dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_NS)
+			&& !dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_DS)
+			&& !dnssec_nsec_bitmap_contains(bm, bm_size, KNOT_RRTYPE_SOA);
+		if (ok) return kr_ok();
 	}
-
-	return kr_error(EINVAL);
+	return kr_error(DNSSEC_NOT_FOUND);
 }
 
 int kr_nsec_matches_name_and_type(const knot_rrset_t *nsec,
 				   const knot_dname_t *name, uint16_t type)
 {
 	/* It's not secure enough to just check a single bit for (some) other types,
-	 * but we don't (currently) only use this API for NS.  See RFC 6840 sec. 4.
-	 */
-	if (kr_fails_assert(type == KNOT_RRTYPE_NS && nsec && name))
+	 * but we (currently) only use this API for NS.  See RFC 6840 sec. 4.  */
+	if (kr_fails_assert(type == KNOT_RRTYPE_NS && nsec && nsec->rrs.rdata && name))
 		return kr_error(EINVAL);
 	if (!knot_dname_is_equal(nsec->owner, name))
 		return kr_error(ENOENT);
 	const uint8_t *bm = knot_nsec_bitmap(nsec->rrs.rdata);
 	uint16_t bm_size = knot_nsec_bitmap_len(nsec->rrs.rdata);
-	if (!bm)
-		return kr_error(EINVAL);
 	if (dnssec_nsec_bitmap_contains(bm, bm_size, type)) {
 		return kr_ok();
 	} else {
diff -pruN 5.4.4-1/lib/dnssec/nsec.h 5.5.1-5/lib/dnssec/nsec.h
--- 5.4.4-1/lib/dnssec/nsec.h	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/dnssec/nsec.h	2022-06-14 07:17:30.403490800 +0000
@@ -6,6 +6,8 @@
 
 #include <libknot/packet/pkt.h>
 
+#include "lib/layer/iterate.h"
+
 /**
  * Check bitmap that child names are contained in the same zone.
  * @note see RFC6840 4.1.
@@ -27,28 +29,6 @@ int kr_nsec_children_in_zone_check(const
 int kr_nsec_bitmap_nodata_check(const uint8_t *bm, uint16_t bm_size, uint16_t type, const knot_dname_t *owner);
 
 /**
- * Name error response check (RFC4035 3.1.3.2; RFC4035 5.4, bullet 2).
- * @note No RRSIGs are validated.
- * @param pkt        Packet structure to be processed.
- * @param section_id Packet section to be processed.
- * @param sname      Name to be checked.
- * @return           0 or error code.
- */
-int kr_nsec_name_error_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
-                                      const knot_dname_t *sname);
-
-/**
- * No data response check (RFC4035 3.1.3.1; RFC4035 5.4, bullet 1).
- * @param pkt        Packet structure to be processed.
- * @param section_id Packet section to be processed.
- * @param sname      Name to be checked.
- * @param stype      Type to be checked.
- * @return           0 or error code.
- */
-int kr_nsec_no_data_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
-                                   const knot_dname_t *sname, uint16_t stype);
-
-/**
  * Wildcard answer response check (RFC4035 3.1.3.3).
  * @param pkt        Packet structure to be processed.
  * @param section_id Packet section to be processed.
@@ -59,28 +39,24 @@ int kr_nsec_wildcard_answer_response_che
                                            const knot_dname_t *sname);
 
 /**
- * Authenticated denial of existence according to RFC4035 5.4.
- * @note No RRSIGs are validated.
- * @param pkt        Packet structure to be processed.
- * @param section_id Packet section to be processed.
- * @param sname      Queried domain name.
- * @param stype      Queried type.
- * @return           0 or error code.
+ * Search for a negative proof for sname+stype among validated NSECs.
+ *
+ * @param rrrs       list of RRs to search; typically kr_request::auth_selected
+ * @param qry_uid    only consider NSECs from this packet, for better efficiency
+ * @return           negative error code, or PKT_NXDOMAIN | PKT_NODATA (both for NXDOMAIN)
  */
-int kr_nsec_existence_denial(const knot_pkt_t *pkt, knot_section_t section_id,
-                             const knot_dname_t *sname, uint16_t stype);
+int kr_nsec_negative(const ranked_rr_array_t *rrrs, uint32_t qry_uid,
+			const knot_dname_t *sname, uint16_t stype);
 
 /**
  * Referral to unsigned subzone check (RFC4035 5.2).
- * @note 	     No RRSIGs are validated.
- * @param pkt        Packet structure to be processed.
- * @return           0 or error code:
- * 		     DNSSEC_NOT_FOUND - neither ds nor nsec records
- *		     were not found.
- *		     EEXIST - ds record was found.
- *		     EINVAL - bogus.
+ *
+ * @param rrrs       list of RRs to search; typically kr_request::auth_selected
+ * @param qry_uid    only consider NSECs from this packet, for better efficiency
+ * @return           0 or negative error code, in particular DNSSEC_NOT_FOUND
  */
-int kr_nsec_ref_to_unsigned(const knot_pkt_t *pkt);
+int kr_nsec_ref_to_unsigned(const ranked_rr_array_t *rrrs, uint32_t qry_uid,
+				const knot_dname_t *sname);
 
 /**
  * Checks whether supplied NSEC RR matches the supplied name and type.
diff -pruN 5.4.4-1/lib/dnssec/signature.c 5.5.1-5/lib/dnssec/signature.c
--- 5.4.4-1/lib/dnssec/signature.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/dnssec/signature.c	2022-06-14 07:17:30.403490800 +0000
@@ -47,18 +47,34 @@ int kr_authenticate_referral(const knot_
 	if (ref->type != KNOT_RRTYPE_DS)
 		return kr_error(EINVAL);
 
-	/* Try all possible DS records */
-	int ret = 0;
+	/* Determine whether to ignore SHA1 digests, because:
+	    https://datatracker.ietf.org/doc/html/rfc4509#section-3
+	 * Now, the RFCs seem to only mention SHA1 and SHA256 (e.g. no SHA384),
+	 * but the most natural extension is to make any other algorithm trump SHA1.
+	 * (Note that the old GOST version is already unsupported by libdnssec.) */
+	bool skip_sha1 = false;
 	knot_rdata_t *rd = ref->rrs.rdata;
-	for (uint16_t i = 0; i < ref->rrs.count; ++i) {
+	for (int i = 0; i < ref->rrs.count; ++i, rd = knot_rdataset_next(rd)) {
+		const uint8_t algo = knot_ds_digest_type(rd);
+		if (algo != DNSSEC_KEY_DIGEST_SHA1 && dnssec_algorithm_digest_support(algo)) {
+			skip_sha1 = true;
+			break;
+		}
+	}
+	/* But otherwise try all possible DS records. */
+	int ret = 0;
+	rd = ref->rrs.rdata;
+	for (int i = 0; i < ref->rrs.count; ++i, rd = knot_rdataset_next(rd)) {
+		const uint8_t algo = knot_ds_digest_type(rd);
+		if (skip_sha1 && algo == DNSSEC_KEY_DIGEST_SHA1)
+			continue;
 		dnssec_binary_t ds_rdata = {
 			.size = rd->len,
 			.data = rd->data
 		};
-		ret = authenticate_ds(key, &ds_rdata, knot_ds_digest_type(rd));
+		ret = authenticate_ds(key, &ds_rdata, algo);
 		if (ret == 0) /* Found a good DS */
 			return kr_ok();
-		rd = knot_rdataset_next(rd);
 	}
 
 	return kr_error(ret);
@@ -274,11 +290,7 @@ int kr_check_signature(const knot_rdata_
 		goto fail;
 	}
 
-	ret = dnssec_sign_verify(sign_ctx,
-		#if KNOT_VERSION_MAJOR >= 3
-			false,
-		#endif
-			&signature);
+	ret = dnssec_sign_verify(sign_ctx, false, &signature);
 	if (ret != 0) {
 		ret = kr_error(EBADMSG);
 		goto fail;
diff -pruN 5.4.4-1/lib/dnssec/ta.c 5.5.1-5/lib/dnssec/ta.c
--- 5.4.4-1/lib/dnssec/ta.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/dnssec/ta.c	2022-06-14 07:17:30.403490800 +0000
@@ -16,9 +16,10 @@
 #include "lib/resolve.h"
 #include "lib/utils.h"
 
-knot_rrset_t *kr_ta_get(map_t *trust_anchors, const knot_dname_t *name)
+knot_rrset_t *kr_ta_get(trie_t *trust_anchors, const knot_dname_t *name)
 {
-	return map_get(trust_anchors, (const char *)name);
+	trie_val_t *val = trie_get_try(trust_anchors, (const char *)name, strlen((const char *)name));
+	return (val) ? *val : NULL;
 }
 
 const knot_dname_t * kr_ta_closest(const struct kr_context *ctx, const knot_dname_t *name,
@@ -31,10 +32,10 @@ const knot_dname_t * kr_ta_closest(const
 	}
 	while (name) {
 		struct kr_context *ctx_nc = (struct kr_context *)/*const-cast*/ctx;
-		if (kr_ta_get(&ctx_nc->trust_anchors, name)) {
+		if (kr_ta_get(ctx_nc->trust_anchors, name)) {
 			return name;
 		}
-		if (kr_ta_get(&ctx_nc->negative_anchors, name)) {
+		if (kr_ta_get(ctx_nc->negative_anchors, name)) {
 			return NULL;
 		}
 		name = knot_wire_next_label(name, NULL);
@@ -78,7 +79,7 @@ cleanup:
 }
 
 /* @internal Insert new TA to trust anchor set, rdata MUST be of DS type. */
-static int insert_ta(map_t *trust_anchors, const knot_dname_t *name,
+static int insert_ta(trie_t *trust_anchors, const knot_dname_t *name,
                      uint32_t ttl, const uint8_t *rdata, uint16_t rdlen)
 {
 	bool is_new_key = false;
@@ -93,12 +94,15 @@ static int insert_ta(map_t *trust_anchor
 		return kr_error(ENOMEM);
 	}
 	if (is_new_key) {
-		return map_set(trust_anchors, (const char *)name, ta_rr);
+		trie_val_t *val = trie_get_ins(trust_anchors, (const char *)name, strlen((const char *)name));
+		if (kr_fails_assert(val))
+			return kr_error(EINVAL);
+		*val = ta_rr;
 	}
 	return kr_ok();
 }
 
-int kr_ta_add(map_t *trust_anchors, const knot_dname_t *name, uint16_t type,
+int kr_ta_add(trie_t *trust_anchors, const knot_dname_t *name, uint16_t type,
               uint32_t ttl, const uint8_t *rdata, uint16_t rdlen)
 {
 	if (!trust_anchors || !name) {
@@ -124,27 +128,27 @@ int kr_ta_add(map_t *trust_anchors, cons
 }
 
 /* Delete record data */
-static int del_record(const char *k, void *v, void *ext)
+static int del_record(trie_val_t *v, void *ext)
 {
-	knot_rrset_t *ta_rr = v;
+	knot_rrset_t *ta_rr = *v;
 	if (ta_rr) {
 		knot_rrset_free(ta_rr, NULL);
 	}
 	return 0;
 }
 
-int kr_ta_del(map_t *trust_anchors, const knot_dname_t *name)
+int kr_ta_del(trie_t *trust_anchors, const knot_dname_t *name)
 {
-	knot_rrset_t *ta_rr = kr_ta_get(trust_anchors, name);
-	if (ta_rr) {
-		del_record(NULL, ta_rr, NULL);
-		map_del(trust_anchors, (const char *)name);
-	}
+	knot_rrset_t *ta_rr;
+	int ret = trie_del(trust_anchors, (const char *)name, strlen((const char *)name),
+			(trie_val_t *) &ta_rr);
+	if (ret == KNOT_EOK && ta_rr)
+		knot_rrset_free(ta_rr, NULL);
 	return kr_ok();
 }
 
-void kr_ta_clear(map_t *trust_anchors)
+void kr_ta_clear(trie_t *trust_anchors)
 {
-	map_walk(trust_anchors, del_record, NULL);
-	map_clear(trust_anchors);
+	trie_apply(trust_anchors, del_record, NULL);
+	trie_clear(trust_anchors);
 }
diff -pruN 5.4.4-1/lib/dnssec/ta.h 5.5.1-5/lib/dnssec/ta.h
--- 5.4.4-1/lib/dnssec/ta.h	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/dnssec/ta.h	2022-06-14 07:17:30.403490800 +0000
@@ -5,7 +5,7 @@
 #pragma once
 
 #include "lib/defines.h"
-#include "lib/generic/map.h"
+#include "lib/generic/trie.h"
 #include <libknot/rrset.h>
 
 /**
@@ -15,20 +15,20 @@
  * @return non-empty RRSet or NULL
  */
 KR_EXPORT
-knot_rrset_t *kr_ta_get(map_t *trust_anchors, const knot_dname_t *name);
+knot_rrset_t *kr_ta_get(trie_t *trust_anchors, const knot_dname_t *name);
 
 /**
  * Add TA to trust store. DS or DNSKEY types are supported.
  * @param  trust_anchors trust store
  * @param  name          name of the TA
  * @param  type          RR type of the TA (DS or DNSKEY)
- * @param  ttl           
- * @param  rdata         
- * @param  rdlen         
+ * @param  ttl
+ * @param  rdata
+ * @param  rdlen
  * @return 0 or an error
  */
 KR_EXPORT
-int kr_ta_add(map_t *trust_anchors, const knot_dname_t *name, uint16_t type,
+int kr_ta_add(trie_t *trust_anchors, const knot_dname_t *name, uint16_t type,
                uint32_t ttl, const uint8_t *rdata, uint16_t rdlen);
 
 struct kr_context;
@@ -39,7 +39,7 @@ struct kr_context;
  * "Closest" means on path towards root.  Closer negative anchor results into NULL.
  * @param type serves as a shorthand because DS needs to start one level higher.
  */
-KR_PURE
+KR_EXPORT KR_PURE
 const knot_dname_t * kr_ta_closest(const struct kr_context *ctx, const knot_dname_t *name,
 				   const uint16_t type);
 
@@ -50,12 +50,12 @@ const knot_dname_t * kr_ta_closest(const
  * @return 0 or an error
  */
 KR_EXPORT
-int kr_ta_del(map_t *trust_anchors, const knot_dname_t *name);
+int kr_ta_del(trie_t *trust_anchors, const knot_dname_t *name);
 
 /**
  * Clear trust store.
  * @param trust_anchors trust store
  */
 KR_EXPORT
-void kr_ta_clear(map_t *trust_anchors);
+void kr_ta_clear(trie_t *trust_anchors);
 
diff -pruN 5.4.4-1/lib/dnssec.c 5.5.1-5/lib/dnssec.c
--- 5.4.4-1/lib/dnssec.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/dnssec.c	2022-06-14 07:17:30.403490800 +0000
@@ -153,7 +153,7 @@ static bool trim_ttl(knot_rrset_t *rrs,
 	if (kr_log_is_debug_qry(VALIDATOR, log_qry)) {
 		auto_free char *name_str = kr_dname_text(rrs->owner),
 				*type_str = kr_rrtype_text(rrs->type);
-		QRVERBOSE(log_qry, VALIDATOR, "trimming TTL of %s %s: %d -> %d\n",
+		kr_log_q(log_qry, VALIDATOR, "trimming TTL of %s %s: %d -> %d\n",
 			name_str, type_str, (int)rrs->ttl, (int)ttl_max);
 	}
 	rrs->ttl = ttl_max;
@@ -197,7 +197,8 @@ void kr_svldr_free_ctx(struct kr_svldr_c
 	free(ctx);
 }
 struct kr_svldr_ctx * kr_svldr_new_ctx(const knot_rrset_t *ds, knot_rrset_t *dnskey,
-		const knot_rdataset_t *dnskey_sigs, uint32_t timestamp)
+		const knot_rdataset_t *dnskey_sigs, uint32_t timestamp,
+		kr_rrset_validation_ctx_t *err_ctx)
 {
 	// Basic init.
 	struct kr_svldr_ctx *ctx = calloc(1, sizeof(*ctx));
@@ -225,6 +226,8 @@ struct kr_svldr_ctx * kr_svldr_new_ctx(c
 	}
 	return ctx;
 fail:
+	if (err_ctx)
+		memcpy(err_ctx, &ctx->vctx, sizeof(*err_ctx));
 	kr_svldr_free_ctx(ctx);
 	return NULL;
 }
diff -pruN 5.4.4-1/lib/dnssec.h 5.5.1-5/lib/dnssec.h
--- 5.4.4-1/lib/dnssec.h	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/dnssec.h	2022-06-14 07:17:30.403490800 +0000
@@ -166,10 +166,12 @@ struct kr_svldr_ctx;
  * - `ds` is assumed to be trusted, and it's used to validate `dnskey+dnskey_sigs`.
  * - The TTL of `dnskey` may get trimmed.
  * - The insides are placed on malloc heap (use _free_ctx).
+ * - `err_ctx` is optional, for use when error happens (but avoid the inside pointers)
  */
 KR_EXPORT
 struct kr_svldr_ctx * kr_svldr_new_ctx(const knot_rrset_t *ds, knot_rrset_t *dnskey,
-		const knot_rdataset_t *dnskey_sigs, uint32_t timestamp);
+		const knot_rdataset_t *dnskey_sigs, uint32_t timestamp,
+		kr_rrset_validation_ctx_t *err_ctx);
 /** Free the context.  Passing NULL is OK. */
 KR_EXPORT
 void kr_svldr_free_ctx(struct kr_svldr_ctx *ctx);
diff -pruN 5.4.4-1/lib/generic/map.c 5.5.1-5/lib/generic/map.c
--- 5.4.4-1/lib/generic/map.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/map.c	1970-01-01 00:00:00.000000000 +0000
@@ -1,355 +0,0 @@
-/*
- * critbit89 - A crit-bit tree implementation for strings in C89
- * Written by Jonas Gehring <jonas@jgehring.net>
- * Implemented key-value storing by Marek Vavrusa <marek.vavrusa@nic.cz>
- * SPDX-License-Identifier: GPL-3.0-or-later
- *
- * The code makes the assumption that malloc returns pointers aligned at at
- * least a two-byte boundary. Since the C standard requires that malloc return
- * pointers that can store any type, there are no commonly-used toolchains for
- * which this assumption is false.
- *
- * See https://github.com/agl/critbit/blob/master/critbit.pdf for reference.
- */
-
-#include <errno.h>
-#include <string.h>
-#include <stdlib.h>
-
-#include "map.h"
-#include "lib/utils.h"
-
- /* Exports */
-#if defined _WIN32 || defined __CYGWIN__
-  #define EXPORT __attribute__ ((dllexport))
-#else
-  #define EXPORT __attribute__ ((visibility ("default")))
-#endif
-
-#ifdef _MSC_VER /* MSVC */
- typedef unsigned __int8 uint8_t;
- typedef unsigned __int32 uint32_t;
- #ifdef _WIN64
-  typedef signed __int64 intptr_t;
- #else
-  typedef _W64 signed int intptr_t;
- #endif
-#else /* Not MSVC */
- #include <stdint.h>
-#endif
-
-typedef struct {
-	void* value;
-	uint8_t key[];
-} cb_data_t;
-
-typedef struct {
-	void *child[2];
-	uint32_t byte;
-	uint8_t otherbits;
-} cb_node_t;
-
-/* Return true if ptr is internal node. */
-static inline int ref_is_internal(const uint8_t *p)
-{
-	return 1 & (intptr_t)p;
-}
-
-/* Get internal node. */
-static inline cb_node_t *ref_get_internal(uint8_t *p)
-{
-	return (cb_node_t *)(p - 1);
-}
-
-/* Static helper functions */
-static void cbt_traverse_delete(map_t *map, void *top)
-{
-	uint8_t *p = top;
-	if (ref_is_internal(p)) {
-		cb_node_t *q = ref_get_internal(p);
-		cbt_traverse_delete(map, q->child[0]);
-		cbt_traverse_delete(map, q->child[1]);
-		mm_free(map->pool, q);
-	} else {
-		mm_free(map->pool, p);
-	}
-}
-
-static int cbt_traverse_prefixed(void *top,
-	int (*callback)(const char *, void *, void *), void *baton)
-{
-	uint8_t *p = top;
-	cb_data_t *x = (cb_data_t *)top;
-
-	if (ref_is_internal(p)) {
-		cb_node_t *q = ref_get_internal(p);
-		int ret = 0;
-
-		ret = cbt_traverse_prefixed(q->child[0], callback, baton);
-		if (ret != 0) {
-			return ret;
-		}
-		ret = cbt_traverse_prefixed(q->child[1], callback, baton);
-		if (ret != 0) {
-			return ret;
-		}
-		return 0;
-	}
-
-	return (callback)((const char *)x->key, x->value, baton);
-}
-
-static cb_data_t *cbt_make_data(map_t *map, const uint8_t *str, size_t len, void *value)
-{
-	cb_data_t *x = mm_alloc(map->pool, sizeof(cb_data_t) + len);
-	if (x != NULL) {
-		x->value = value;
-		memcpy(x->key, str, len);
-	}
-	return x;
-}
-
-/*! Like map_contains, but also set the value, if passed and found. */
-static int cbt_get(map_t *map, const char *str, void **value)
-{
-	const uint8_t *ubytes = (void *)str;
-	const size_t ulen = strlen(str);
-	uint8_t *p = map->root;
-	cb_data_t *x = NULL;
-
-	if (p == NULL) {
-		return 0;
-	}
-
-	while (ref_is_internal(p)) {
-		cb_node_t *q = ref_get_internal(p);
-		uint8_t c = 0;
-		int direction;
-
-		if (q->byte < ulen) {
-			c = ubytes[q->byte];
-		}
-		direction = (1 + (q->otherbits | c)) >> 8;
-
-		p = q->child[direction];
-	}
-
-	x = (cb_data_t *)p;
-	if (strcmp(str, (const char *)x->key) == 0) {
-		if (value != NULL) {
-			*value = x->value;
-		}
-		return 1;
-	}
-
-	return 0;
-}
-
-/*! Returns non-zero if map contains str */
-EXPORT int map_contains(map_t *map, const char *str)
-{
-	return cbt_get(map, str, NULL);
-}
-
-EXPORT void *map_get(map_t *map, const char *str)
-{
-	void *v = NULL;
-	cbt_get(map, str, &v);
-	return v;
-}
-
-EXPORT int map_set(map_t *map, const char *str, void *val)
-{
-	const uint8_t *const ubytes = (void *)str;
-	const size_t ulen = strlen(str);
-	uint8_t *p = map->root;
-	uint8_t c = 0, *x = NULL;
-	uint32_t newbyte = 0;
-	uint32_t newotherbits = 0;
-	int direction = 0, newdirection = 0;
-	cb_node_t *newnode = NULL;
-	cb_data_t *data = NULL;
-	void **wherep = NULL;
-
-	if (p == NULL) {
-		map->root = cbt_make_data(map, (const uint8_t *)str, ulen + 1, val);
-		if (map->root == NULL) {
-			return ENOMEM;
-		}
-		return 0;
-	}
-
-	while (ref_is_internal(p)) {
-		cb_node_t *q = ref_get_internal(p);
-		c = 0;
-		if (q->byte < ulen) {
-			c = ubytes[q->byte];
-		}
-		direction = (1 + (q->otherbits | c)) >> 8;
-
-		p = q->child[direction];
-	}
-
-	data = (cb_data_t *)p;
-	for (newbyte = 0; newbyte < ulen; ++newbyte) {
-		if (data->key[newbyte] != ubytes[newbyte]) {
-			newotherbits = data->key[newbyte] ^ ubytes[newbyte];
-			goto different_byte_found;
-		}
-	}
-
-	if (data->key[newbyte] != 0) {
-		newotherbits = data->key[newbyte];
-		goto different_byte_found;
-	}
-	data->value = val;
-	return 1;
-
-different_byte_found:
-	newotherbits |= newotherbits >> 1;
-	newotherbits |= newotherbits >> 2;
-	newotherbits |= newotherbits >> 4;
-	newotherbits = (newotherbits & ~(newotherbits >> 1)) ^ 255;
-	c = data->key[newbyte];
-	newdirection = (1 + (newotherbits | c)) >> 8;
-
-	newnode = mm_alloc(map->pool, sizeof(cb_node_t));
-	if (newnode == NULL) {
-		return ENOMEM;
-	}
-
-	x = (uint8_t *)cbt_make_data(map, ubytes, ulen + 1, val);
-	if (x == NULL) {
-		mm_free(map->pool, newnode);
-		return ENOMEM;
-	}
-
-	newnode->byte = newbyte;
-	newnode->otherbits = newotherbits;
-	newnode->child[1 - newdirection] = x;
-
-	/* Insert into map */
-	wherep = &map->root;
-	for (;;) {
-		cb_node_t *q;
-		p = *wherep;
-		if (!ref_is_internal(p)) {
-			break;
-		}
-
-		q = ref_get_internal(p);
-		if (q->byte > newbyte) {
-			break;
-		}
-		if (q->byte == newbyte && q->otherbits > newotherbits) {
-			break;
-		}
-
-		c = 0;
-		if (q->byte < ulen) {
-			c = ubytes[q->byte];
-		}
-		direction = (1 + (q->otherbits | c)) >> 8;
-		wherep = q->child + direction;
-	}
-
-	newnode->child[newdirection] = *wherep;
-	*wherep = (void *)(1 + (char *)newnode);
-	return 0;
-}
-
-/*! Deletes str from the map, returns 0 on success */
-EXPORT int map_del(map_t *map, const char *str)
-{
-	const uint8_t *ubytes = (void *)str;
-	const size_t ulen = strlen(str);
-	uint8_t *p = map->root;
-	void **wherep = NULL, **whereq = NULL;
-	cb_node_t *q = NULL;
-	cb_data_t *data = NULL;
-	int direction = 0;
-
-	if (map->root == NULL) {
-		return 1;
-	}
-	wherep = &map->root;
-
-	while (ref_is_internal(p)) {
-		uint8_t c = 0;
-		whereq = wherep;
-		q = ref_get_internal(p);
-
-		if (q->byte < ulen) {
-			c = ubytes[q->byte];
-		}
-		direction = (1 + (q->otherbits | c)) >> 8;
-		wherep = q->child + direction;
-		p = *wherep;
-	}
-
-	data = (cb_data_t *)p;
-	if (strcmp(str, (const char *)data->key) != 0) {
-		return 1;
-	}
-	mm_free(map->pool, p);
-
-	if (!whereq) {
-		map->root = NULL;
-		return 0;
-	}
-
-	*whereq = q->child[1 - direction];
-	mm_free(map->pool, q);
-	return 0;
-}
-
-/*! Clears the given map */
-EXPORT void map_clear(map_t *map)
-{
-	if (map->root) {
-		cbt_traverse_delete(map, map->root);
-	}
-	map->root = NULL;
-}
-
-/*! Calls callback for all strings in map with the given prefix */
-EXPORT int map_walk_prefixed(map_t *map, const char *prefix,
-	int (*callback)(const char *, void *, void *), void *baton)
-{
-	if (!map) {
-		return 0;
-	}
-
-	const uint8_t *ubytes = (void *)prefix;
-	const size_t ulen = strlen(prefix);
-	uint8_t *p = map->root;
-	uint8_t *top = p;
-	cb_data_t *data = NULL;
-
-	if (p == NULL) {
-		return 0;
-	}
-
-	while (ref_is_internal(p)) {
-		cb_node_t *q = ref_get_internal(p);
-		uint8_t c = 0;
-		int direction;
-
-		if (q->byte < ulen) {
-			c = ubytes[q->byte];
-		}
-		direction = (1 + (q->otherbits | c)) >> 8;
-
-		p = q->child[direction];
-		if (q->byte < ulen) {
-			top = p;
-		}
-	}
-
-	data = (cb_data_t *)p;
-	if (strlen((const char *)data->key) < ulen || memcmp(data->key, prefix, ulen) != 0) {
-		return 0; /* No strings match */
-	}
-
-	return cbt_traverse_prefixed(top, callback, baton);
-}
diff -pruN 5.4.4-1/lib/generic/map.h 5.5.1-5/lib/generic/map.h
--- 5.4.4-1/lib/generic/map.h	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/map.h	1970-01-01 00:00:00.000000000 +0000
@@ -1,116 +0,0 @@
-/*
- * critbit89 - A crit-bit map implementation for strings in C89
- * Written by Jonas Gehring <jonas@jgehring.net>
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-/**
- * @file map.h
- * @brief A Crit-bit tree key-value map implementation.
- *
- * @warning If the user provides a custom allocator, it must return addresses aligned to 2B boundary.
- *
- * # Example usage:
- *
- * @code{.c}
- *      map_t map = map_make(NULL);
- *
- *      // Custom allocator (optional)
- *      map.malloc = &mymalloc;
- *      map.baton  = &mymalloc_context;
- *
- *      // Insert k-v pairs
- *      int values = { 42, 53, 64 };
- *      if (map_set(&map, "princess", &values[0]) != 0 ||
- *          map_set(&map, "prince", &values[1])   != 0 ||
- *          map_set(&map, "leia", &values[2])     != 0) {
- *          fail();
- *      }
- *
- *      // Test membership
- *      if (map_contains(&map, "leia")) {
- *          success();
- *      }
- *
- *      // Prefix search
- *      int i = 0;
- *      int count(const char *k, void *v, void *ext) { (*(int *)ext)++; return 0; }
- *      if (map_walk_prefixed(map, "princ", count, &i) == 0) {
- *          printf("%d matches\n", i);
- *      }
- *
- *      // Delete
- *      if (map_del(&map, "badkey") != 0) {
- *          fail(); // No such key
- *      }
- *
- *      // Clear the map
- *      map_clear(&map);
- * @endcode
- *
- * \addtogroup generics
- * @{
- */
-
-#pragma once
-
-#include <stddef.h>
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-struct knot_mm; /* avoid the unnecessary include */
-
-/** Main data structure */
-typedef struct {
-	void *root;
-	struct knot_mm *pool;
-} map_t;
-
-/** Creates an new empty critbit map.  Pass NULL for malloc+free. */
-static inline map_t map_make(struct knot_mm *pool)
-{
-	return (map_t){ .root = NULL, .pool = pool };
-}
-
-/** Returns non-zero if map contains str */
-int map_contains(map_t *map, const char *str);
-
-/** Returns value if map contains str.  Note: NULL may mean two different things. */
-void *map_get(map_t *map, const char *str);
-
-/** Inserts str into map.  Returns 0 if new, 1 if replaced, or ENOMEM. */
-int map_set(map_t *map, const char *str, void *val);
-
-/** Deletes str from the map, returns 0 on success */
-int map_del(map_t *map, const char *str);
-
-/** Clears the given map */
-void map_clear(map_t *map);
-
-/**
- * Calls callback for all strings in map
- * See @fn map_walk_prefixed() for documentation on parameters.
- */
-#define map_walk(map, callback, baton) \
-	map_walk_prefixed((map), "", (callback), (baton))
-
-/**
- * Calls callback for all strings in map with the given prefix.
- * Returns value immediately if a callback returns nonzero.
- *
- * @param map
- * @param prefix   required string prefix (empty => all strings)
- * @param callback callback parameters are (key, value, baton)
- * @param baton    passed uservalue
- */
-int map_walk_prefixed(map_t *map, const char *prefix,
-	int (*callback)(const char *, void *, void *), void *baton);
-
-
-#ifdef __cplusplus
-}
-#endif
-
-/** @} */
diff -pruN 5.4.4-1/lib/generic/map.spdx 5.5.1-5/lib/generic/map.spdx
--- 5.4.4-1/lib/generic/map.spdx	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/map.spdx	1970-01-01 00:00:00.000000000 +0000
@@ -1,10 +0,0 @@
-SPDXVersion: SPDX-2.1
-DataLicense: CC0-1.0
-SPDXID: SPDXRef-DOCUMENT
-DocumentName: map
-DocumentNamespace: http://spdx.org/spdxdocs/spdx-v2.1-d9b4db4c-062f-4add-89b6-f603224f5a2c
-
-PackageName: critbit89
-PackageDownloadLocation: git+https://github.com/jgehring/critbit89.git@4f7e1d2a5f4794e0d08cb408346973fb1e39489c#critbit.c
-PackageOriginator: Person: Jonas Gehring (jonas@jgehring.net)
-PackageLicenseDeclared: CC0-1.0
diff -pruN 5.4.4-1/lib/generic/queue.c 5.5.1-5/lib/generic/queue.c
--- 5.4.4-1/lib/generic/queue.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/queue.c	2022-06-14 07:17:30.403490800 +0000
@@ -5,6 +5,8 @@
 #include "lib/generic/queue.h"
 #include <string.h>
 
+extern inline void * queue_head_impl(const struct queue *q);
+
 void queue_init_impl(struct queue *q, size_t item_size)
 {
 	q->len = 0;
@@ -36,8 +38,9 @@ void queue_deinit_impl(struct queue *q)
 
 static struct queue_chunk * queue_chunk_new(const struct queue *q)
 {
+	/* size_t cast is to avoid unintended sign-extension */
 	struct queue_chunk *c = malloc(offsetof(struct queue_chunk, data)
-					+ q->chunk_cap * q->item_size);
+					+ (size_t) q->chunk_cap * (size_t) q->item_size);
 	if (unlikely(!c)) abort(); // simplify stuff
 	memset(c, 0, offsetof(struct queue_chunk, data));
 	c->cap = q->chunk_cap;
@@ -57,9 +60,10 @@ void * queue_push_impl(struct queue *q)
 	} else
 	if (t->end == t->cap) {
 		if (t->begin * 2 >= t->cap) {
-			/* Utilization is below 50%, so let's shift (no overlap). */
+			/* Utilization is below 50%, so let's shift (no overlap).
+			 * (size_t cast is to avoid unintended sign-extension) */
 			memcpy(t->data, t->data + t->begin * q->item_size,
-				(t->end - t->begin) * q->item_size);
+				(size_t) (t->end - t->begin) * (size_t) q->item_size);
 			t->end -= t->begin;
 			t->begin = 0;
 		} else {
@@ -91,10 +95,11 @@ void * queue_push_head_impl(struct queue
 	if (h->begin == 0) {
 		if (h->end * 2 <= h->cap) {
 			/* Utilization is below 50%, so let's shift (no overlap).
-			 * Computations here are simplified due to h->begin == 0. */
+			 * Computations here are simplified due to h->begin == 0.
+			 * (size_t cast is to avoid unintended sign-extension) */
 			const int cnt = h->end;
 			memcpy(h->data + (h->cap - cnt) * q->item_size, h->data,
-				cnt * q->item_size);
+				(size_t) cnt * (size_t) q->item_size);
 			h->begin = h->cap - cnt;
 			h->end = h->cap;
 		} else {
@@ -111,3 +116,25 @@ void * queue_push_head_impl(struct queue
 	return h->data + q->item_size * h->begin;
 }
 
+void queue_pop_impl(struct queue *q)
+{
+	kr_require(q);
+	struct queue_chunk *h = q->head;
+	kr_require(h && h->end > h->begin);
+	if (h->end - h->begin == 1) {
+		/* removing the last element in the chunk */
+		kr_require((q->len == 1) == (q->head == q->tail));
+		if (q->len == 1) {
+			q->tail = NULL;
+			kr_require(!h->next);
+		} else {
+			kr_require(h->next);
+		}
+		q->head = h->next;
+		free(h);
+	} else {
+		++(h->begin);
+	}
+	--(q->len);
+}
+
diff -pruN 5.4.4-1/lib/generic/queue.h 5.5.1-5/lib/generic/queue.h
--- 5.4.4-1/lib/generic/queue.h	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/queue.h	2022-06-14 07:17:30.403490800 +0000
@@ -146,6 +146,7 @@ KR_EXPORT void queue_init_impl(struct qu
 KR_EXPORT void queue_deinit_impl(struct queue *q);
 KR_EXPORT void * queue_push_impl(struct queue *q);
 KR_EXPORT void * queue_push_head_impl(struct queue *q);
+KR_EXPORT void queue_pop_impl(struct queue *q);
 
 struct queue_chunk;
 struct queue {
@@ -172,7 +173,7 @@ struct queue_chunk {
 	 */
 };
 
-static inline void * queue_head_impl(const struct queue *q)
+KR_EXPORT inline void * queue_head_impl(const struct queue *q)
 {
 	kr_require(q);
 	struct queue_chunk *h = q->head;
@@ -188,29 +189,6 @@ static inline void * queue_tail_impl(con
 	return t->data + (t->end - 1) * q->item_size;
 }
 
-static inline void queue_pop_impl(struct queue *q)
-{
-	kr_require(q);
-	struct queue_chunk *h = q->head;
-	kr_require(h && h->end > h->begin);
-	if (h->end - h->begin == 1) {
-		/* removing the last element in the chunk */
-		kr_require((q->len == 1) == (q->head == q->tail));
-		if (q->len == 1) {
-			q->tail = NULL;
-			kr_require(!h->next);
-		} else {
-			kr_require(h->next);
-		}
-		q->head = h->next;
-		free(h);
-	} else {
-		++(h->begin);
-	}
-	--(q->len);
-}
-
-
 struct queue_it {
 	struct queue_chunk *chunk;
 	int16_t pos, item_size;
diff -pruN 5.4.4-1/lib/generic/README.rst 5.5.1-5/lib/generic/README.rst
--- 5.4.4-1/lib/generic/README.rst	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/README.rst	2022-06-14 07:17:30.403490800 +0000
@@ -10,8 +10,6 @@ as long as it comes with a test case in
 
 * array_ - a set of simple macros to make working with dynamic arrays easier.
 * queue_ - a FIFO + LIFO queue.
-* map_ - a `Crit-bit tree`_ key-value map implementation (public domain) that comes with tests.
-* set_ - set abstraction implemented on top of ``map`` (unused now).
 * pack_ - length-prefixed list of objects (i.e. array-list).
 * lru_ - LRU-like hash table
 * trie_ - a trie-based key-value map, taken from knot-dns
@@ -28,18 +26,6 @@ queue
 .. doxygenfile:: queue.h
    :project: libkres
 
-map
-~~~
-
-.. doxygenfile:: map.h
-   :project: libkres
-
-set
-~~~
-
-.. doxygenfile:: set.h
-   :project: libkres
-
 pack
 ~~~~
 
diff -pruN 5.4.4-1/lib/generic/set.h 5.5.1-5/lib/generic/set.h
--- 5.4.4-1/lib/generic/set.h	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/set.h	1970-01-01 00:00:00.000000000 +0000
@@ -1,93 +0,0 @@
-/*  Copyright (C) 2015-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
- *  SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-/**
- * @file set.h
- * @brief A set abstraction implemented on top of map.
- *
- * @note The API is based on map.h, see it for more examples.
- *
- * # Example usage:
- *
- * @code{.c}
- *      set_t set = set_make(NULL);
- *
- *      // Insert keys
- *      if (set_add(&set, "princess") != 0 ||
- *          set_add(&set, "prince")   != 0 ||
- *          set_add(&set, "leia")     != 0) {
- *          fail();
- *      }
- *
- *      // Test membership
- *      if (set_contains(&set, "leia")) {
- *          success();
- *      }
- *
- *      // Prefix search
- *      int i = 0;
- *      int count(const char *s, void *n) { (*(int *)n)++; return 0; }
- *      if (set_walk_prefixed(set, "princ", count, &i) == 0) {
- *          printf("%d matches\n", i);
- *      }
- *
- *      // Delete
- *      if (set_del(&set, "badkey") != 0) {
- *          fail(); // No such key
- *      }
- *
- *      // Clear the set
- *      set_clear(&set);
- * @endcode
- *
- * \addtogroup generics
- * @{
- */
-
-#pragma once
-
-#include <stddef.h>
-#include "map.h"
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-typedef map_t set_t;
-typedef int (set_walk_cb)(const char *, void *);
-
-/*! Creates an new, empty critbit set */
-#define set_make \
-	map_make
-
-/*! Returns non-zero if set contains str */
-#define set_contains(set, str) \
-	map_contains((set), (str))
-
-/*! Inserts str into set.  Returns 0 if new, 1 if already present, or ENOMEM. */
-#define set_add(set, str) \
-	map_set((set), (str), (void *)1)
-
-/*! Deletes str from the set, returns 0 on success */
-#define set_del(set, str) \
-	map_del((set), (str))
-
-/*! Clears the given set */
-#define set_clear(set) \
-	map_clear(set)
-
-/*! Calls callback for all strings in map  */
-#define set_walk(set, callback, baton) \
-	map_walk_prefixed((set), "", (callback), (baton))
-
-/*! Calls callback for all strings in set with the given prefix  */
-#define set_walk_prefixed(set, prefix, callback, baton) \
-	map_walk_prefixed((set), (prefix), (callback), (baton))
-
-
-#ifdef __cplusplus
-}
-#endif
-
-/** @} */
diff -pruN 5.4.4-1/lib/generic/test_map.c 5.5.1-5/lib/generic/test_map.c
--- 5.4.4-1/lib/generic/test_map.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/test_map.c	1970-01-01 00:00:00.000000000 +0000
@@ -1,119 +0,0 @@
-/*  Copyright (C) 2014-2017 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
- *  SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-#include <errno.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include "tests/unit/test.h"
-#include "lib/generic/map.h"
-
-/*
- * Sample dictionary
- */
-static const char *dict[] = {
-	"catagmatic", "prevaricator", "statoscope", "workhand", "benzamide",
-	"alluvia", "fanciful", "bladish", "Tarsius", "unfast", "appropriative",
-	"seraphically", "monkeypod", "deflectometer", "tanglesome", "zodiacal",
-	"physiologically", "economizer", "forcepslike", "betrumpet",
-	"Danization", "broadthroat", "randir", "usherette", "nephropyosis",
-	"hematocyanin", "chrysohermidin", "uncave", "mirksome", "podophyllum",
-	"siphonognathous", "indoor", "featheriness", "forwardation",
-	"archruler", "soricoid", "Dailamite", "carmoisin", "controllability",
-	"unpragmatical", "childless", "transumpt", "productive",
-	"thyreotoxicosis", "oversorrow", "disshadow", "osse", "roar",
-	"pantomnesia", "talcer", "hydrorrhoea", "Satyridae", "undetesting",
-	"smoothbored", "widower", "sivathere", "pendle", "saltation",
-	"autopelagic", "campfight", "unexplained", "Macrorhamphosus",
-	"absconsa", "counterflory", "interdependent", "triact", "reconcentration",
-	"oversharpness", "sarcoenchondroma", "superstimulate", "assessory",
-	"pseudepiscopacy", "telescopically", "ventriloque", "politicaster",
-	"Caesalpiniaceae", "inopportunity", "Helion", "uncompatible",
-	"cephaloclasia", "oversearch", "Mahayanistic", "quarterspace",
-	"bacillogenic", "hamartite", "polytheistical", "unescapableness",
-	"Pterophorus", "cradlemaking", "Hippoboscidae", "overindustrialize",
-	"perishless", "cupidity", "semilichen", "gadge", "detrimental",
-	"misencourage", "toparchia", "lurchingly", "apocatastasis"
-};
-
-/* Insertions */
-static void test_insert(void **state)
-{
-	map_t *tree = *state;
-	int dict_size = sizeof(dict) / sizeof(const char *);
-	int i;
-
-	for (i = 0; i < dict_size; i++) {
-		assert_int_equal(map_set(tree, dict[i], (void *)dict[i]), 0);
-	}
-}
-
-/* Searching */
-static void test_get(void **state)
-{
-	map_t *tree = *state;
-	char *in;
-	const char *notin = "not in tree";
-
-	in = malloc(strlen(dict[23])+1);
-	strcpy(in, dict[23]);
-
-	assert_true(map_get(tree, in) == dict[23]);
-	assert_true(map_get(tree, notin) == NULL);
-	assert_true(map_get(tree, "") == NULL);
-	in[strlen(in)/2] = '\0';
-	assert_true(map_get(tree, in) == NULL);
-
-	free(in);
-}
-
-/* Deletion */
-static void test_delete(void **state)
-{
-	map_t *tree = *state;
-	assert_int_equal(map_del(tree, dict[91]), 0);
-	assert_false(map_contains(tree, dict[91]));
-	assert_int_equal(map_del(tree, "most likely not in tree"), 1);
-}
-
-/* Test null value existence */
-static void test_null_value(void **state)
-{
-	map_t *tree = *state;
-	char *key = "foo";
-
-	assert_int_equal(map_set(tree, key, (void *)0), 0);
-	assert_true(map_contains(tree, key));
-	assert_int_equal(map_del(tree, key), 0);
-}
-
-static void test_init(void **state)
-{
-	static map_t tree;
-	tree = map_make(NULL);
-	*state = &tree;
-	assert_non_null(*state);
-}
-
-static void test_deinit(void **state)
-{
-	map_t *tree = *state;
-	map_clear(tree);
-}
-
-/* Program entry point */
-int main(int argc, char **argv)
-{
-	const UnitTest tests[] = {
-	        group_test_setup(test_init),
-	        unit_test(test_insert),
-		unit_test(test_get),
-		unit_test(test_delete),
-		unit_test(test_null_value),
-	        group_test_teardown(test_deinit)
-	};
-
-	return run_group_tests(tests);
-}
diff -pruN 5.4.4-1/lib/generic/test_pack.c 5.5.1-5/lib/generic/test_pack.c
--- 5.4.4-1/lib/generic/test_pack.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/test_pack.c	2022-06-14 07:17:30.403490800 +0000
@@ -36,12 +36,10 @@ static void test_pack_std(void **state)
 	/* Iterate */
 	uint8_t *it = pack_head(pack);
 	assert_non_null(it);
-	unsigned count = 0;
 	while (it != pack_tail(pack)) {
 		assert_int_equal(pack_obj_len(it), 2);
 		assert_true(memcmp(pack_obj_val(it), "de", 2) == 0);
 		it = pack_obj_next(it);
-		count += 1;
 	}
 
 	/* Find */
diff -pruN 5.4.4-1/lib/generic/test_set.c 5.5.1-5/lib/generic/test_set.c
--- 5.4.4-1/lib/generic/test_set.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/test_set.c	1970-01-01 00:00:00.000000000 +0000
@@ -1,221 +0,0 @@
-/*
- * critbit89 - A crit-bit tree implementation for strings in C89
- * Written by Jonas Gehring <jonas@jgehring.net>
- * SPDX-License-Identifier: GPL-3.0-or-later
- */
-
-
-#include <errno.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include "tests/unit/test.h"
-#include "lib/generic/set.h"
-#include "lib/utils.h"
-
-
-/*
- * Sample dictionary: 100 random words from /usr/share/dict/words
- * Generated using random.org:
- * MAX=`wc -l < /usr/share/dict/words | tr -d " "`
- * for i in `curl "http://www.random.org/integers/?num=100&min=1&max=$MAX&col=1&base=10&format=plain&rnd=new"`; do
- *   nl /usr/share/dict/words | grep -w $i | tr -d "0-9\t "
- * done
- */
-static const char *dict[] = {
-	"catagmatic", "prevaricator", "statoscope", "workhand", "benzamide",
-	"alluvia", "fanciful", "bladish", "Tarsius", "unfast", "appropriative",
-	"seraphically", "monkeypod", "deflectometer", "tanglesome", "zodiacal",
-	"physiologically", "economizer", "forcepslike", "betrumpet",
-	"Danization", "broadthroat", "randir", "usherette", "nephropyosis",
-	"hematocyanin", "chrysohermidin", "uncave", "mirksome", "podophyllum",
-	"siphonognathous", "indoor", "featheriness", "forwardation",
-	"archruler", "soricoid", "Dailamite", "carmoisin", "controllability",
-	"unpragmatical", "childless", "transumpt", "productive",
-	"thyreotoxicosis", "oversorrow", "disshadow", "osse", "roar",
-	"pantomnesia", "talcer", "hydrorrhoea", "Satyridae", "undetesting",
-	"smoothbored", "widower", "sivathere", "pendle", "saltation",
-	"autopelagic", "campfight", "unexplained", "Macrorhamphosus",
-	"absconsa", "counterflory", "interdependent", "triact", "reconcentration",
-	"oversharpness", "sarcoenchondroma", "superstimulate", "assessory",
-	"pseudepiscopacy", "telescopically", "ventriloque", "politicaster",
-	"Caesalpiniaceae", "inopportunity", "Helion", "uncompatible",
-	"cephaloclasia", "oversearch", "Mahayanistic", "quarterspace",
-	"bacillogenic", "hamartite", "polytheistical", "unescapableness",
-	"Pterophorus", "cradlemaking", "Hippoboscidae", "overindustrialize",
-	"perishless", "cupidity", "semilichen", "gadge", "detrimental",
-	"misencourage", "toparchia", "lurchingly", "apocatastasis"
-};
-
-/* Insertions */
-static void test_insert(void **state)
-{
-	set_t *set = *state;
-	int dict_size = sizeof(dict) / sizeof(const char *);
-	int i;
-
-	for (i = 0; i < dict_size; i++) {
-		assert_int_equal(set_add(set, dict[i]), 0);
-	}
-}
-
-/* Insertion of duplicate element */
-static void test_insert_dup(void **state)
-{
-	set_t *set = *state;
-	int dict_size = sizeof(dict) / sizeof(const char *);
-	int i;
-
-	for (i = 0; i < dict_size; i++) {
-		if (!set_contains(set, dict[i])) {
-			continue;
-		}
-		assert_int_equal(set_add(set, dict[i]), 1);
-	}
-}
-
-/* Searching */
-static void test_contains(void **state)
-{
-	set_t *set = *state;
-	char *in;
-	const char *notin = "not in set";
-
-	in = malloc(strlen(dict[23])+1);
-	strcpy(in, dict[23]);
-
-	assert_true(set_contains(set, in));
-	assert_false(set_contains(set, notin));
-	assert_false(set_contains(set, ""));
-	in[strlen(in)/2] = '\0';
-	assert_false(set_contains(set, in));
-
-	free(in);
-}
-
-/* Count number of items */
-static int count_cb(const char *s, void *_, void *n) { (*(int *)n)++; return 0; }
-static void test_complete(set_t *set, int n)
-{
-	int i = 0;
-	if (set_walk(set, count_cb, &i) != 0) {
-		abort();
-	}
-	if (i != n) {
-		abort();
-	}
-}
-static void test_complete_full(void **state) { test_complete(*state, sizeof(dict) / sizeof(const char *)); }
-static void test_complete_zero(void **state) { test_complete(*state, 0); }
-
-/* Deletion */
-static void test_delete(void **state)
-{
-	set_t *set = *state;
-	assert_int_equal(set_del(set, dict[91]), 0);
-	assert_int_equal(set_del(set, "most likely not in set"), 1);
-}
-
-/* Complete deletion */
-static void test_delete_all(void **state)
-{
-	set_t *set = *state;
-	int dict_size = sizeof(dict) / sizeof(const char *);
-	int i;
-
-	for (i = 0; i < dict_size; i++) {
-		if (!set_contains(set, dict[i])) {
-			continue;
-		}
-		assert_int_equal(set_del(set, dict[i]), 0);
-	}
-}
-
-/* Fake allocator */
-static void *fake_malloc(void *b, size_t s) { return NULL; }
-static void test_allocator(void **state)
-{
-	knot_mm_t fake_pool = { .ctx = NULL, .alloc = fake_malloc, .free = NULL };
-	set_t set = set_make(&fake_pool);
-	assert_int_equal(set_add(&set, dict[0]), ENOMEM);
-}
-
-/* Empty set */
-static void test_empty(void **state)
-{
-	set_t *set = *state;
-	assert_int_equal(set_contains(set, dict[1]), 0);
-	assert_int_not_equal(set_del(set, dict[1]), 0);
-}
-
-/* Prefix walking */
-static void test_prefixes(void **state)
-{
-	set_t *set = *state;
-	int i = 0;
-	if ((set_add(set, "1str") != 0) ||
-			(set_add(set, "11str2") != 0) ||
-			(set_add(set, "12str") != 0) ||
-			(set_add(set, "11str") != 0)) {
-		assert_int_equal(1, 0);
-	}
-
-	assert_int_equal(set_walk_prefixed(set, "11", count_cb, &i), 0);
-	assert_int_equal(i, 2);
-	i = 0;
-	assert_int_equal(set_walk_prefixed(set, "13", count_cb, &i), 0);
-	assert_int_equal(i, 0);
-	i = 0;
-	assert_int_equal(set_walk_prefixed(set, "12345678", count_cb, &i), 0);
-	assert_int_equal(i, 0);
-	i = 0;
-	assert_int_equal(set_walk_prefixed(set, "11str", count_cb, &i), 0);
-	assert_int_equal(i, 2);
-}
-
-static void test_clear(void **state)
-{
-	set_t *set = *state;
-	set_clear(set);
-}
-
-static void test_init(void **state)
-{
-	static set_t set;
-	set = set_make(NULL);
-	*state = &set;
-	assert_non_null(*state);
-}
-
-static void test_deinit(void **state)
-{
-	set_t *set = *state;
-	set_clear(set);
-}
-
-/* Program entry point */
-int main(int argc, char **argv)
-{
-	const UnitTest tests[] = {
-	        group_test_setup(test_init),
-	        unit_test(test_insert),
-	        unit_test(test_complete_full),
-	        unit_test(test_insert_dup),
-	        unit_test(test_contains),
-		unit_test(test_delete),
-		unit_test(test_clear),
-		unit_test(test_insert),
-		unit_test(test_complete_full),
-		unit_test(test_delete_all),
-		unit_test(test_complete_zero),
-		unit_test(test_allocator),
-		unit_test(test_clear),
-		unit_test(test_empty),
-		unit_test(test_insert),
-		unit_test(test_prefixes),
-	        group_test_teardown(test_deinit)
-	};
-
-	return run_group_tests(tests);
-}
diff -pruN 5.4.4-1/lib/generic/trie.c 5.5.1-5/lib/generic/trie.c
--- 5.4.4-1/lib/generic/trie.c	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/trie.c	2022-06-14 07:17:30.403490800 +0000
@@ -843,6 +843,26 @@ int trie_apply(trie_t *tbl, int (*f)(tri
 	return apply_trie(&tbl->root, f, d);
 }
 
+/*! \brief Apply a function to every key + trie_val_t*, in order; a recursive solution. */
+static int apply_trie_with_key(node_t *t, int (*f)(const char *, uint32_t, trie_val_t *, void *), void *d)
+{
+	kr_require(t);
+	if (!isbranch(t))
+		return f(t->leaf.key->chars, t->leaf.key->len, &t->leaf.val, d);
+	int child_count = bitmap_weight(t->branch.bitmap);
+	for (int i = 0; i < child_count; ++i)
+		ERR_RETURN(apply_trie_with_key(twig(t, i), f, d));
+	return KNOT_EOK;
+}
+
+int trie_apply_with_key(trie_t *tbl, int (*f)(const char *, uint32_t, trie_val_t *, void *), void *d)
+{
+	kr_require(tbl && f);
+	if (!tbl->weight)
+		return KNOT_EOK;
+	return apply_trie_with_key(&tbl->root, f, d);
+}
+
 /* These are all thin wrappers around static Tns* functions. */
 trie_it_t* trie_it_begin(trie_t *tbl)
 {
diff -pruN 5.4.4-1/lib/generic/trie.h 5.5.1-5/lib/generic/trie.h
--- 5.4.4-1/lib/generic/trie.h	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/generic/trie.h	2022-06-14 07:17:30.403490800 +0000
@@ -84,6 +84,17 @@ KR_EXPORT
 int trie_apply(trie_t *tbl, int (*f)(trie_val_t *, void *), void *d);
 
 /*!
+ * \brief Apply a function to every trie_val_t, in order.
+ *
+ * It's like trie_apply() but additionally passes keys and their lengths.
+ *
+ * \param d Parameter passed as the second argument to f().
+ * \return First nonzero from f() or zero (i.e. KNOT_EOK).
+ */
+KR_EXPORT
+int trie_apply_with_key(trie_t *tbl, int (*f)(const char *, uint32_t, trie_val_t *, void *), void *d);
+
+/*!
  * \brief Remove an item, returning KNOT_EOK if succeeded or KNOT_ENOENT if not found.
  *
  * If val!=NULL and deletion succeeded, the deleted value is set.
diff -pruN 5.4.4-1/lib/layer/iterate.c 5.5.1-5/lib/layer/iterate.c
--- 5.4.4-1/lib/layer/iterate.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/layer/iterate.c	2022-06-14 07:17:30.403490800 +0000
@@ -23,7 +23,6 @@
 #include <libknot/rrtype/rdname.h>
 #include <libknot/rrtype/rrsig.h>
 
-#include "kresconfig.h"
 #include "lib/layer/iterate.h"
 #include "lib/resolve.h"
 #include "lib/rplan.h"
@@ -32,8 +31,8 @@
 #include "lib/module.h"
 #include "lib/dnssec/ta.h"
 
-#define VERBOSE_MSG(...) QRVERBOSE(req->current_query, ITERATOR, __VA_ARGS__)
-#define QVERBOSE_MSG(qry, ...) QRVERBOSE(qry, ITERATOR, __VA_ARGS__)
+#define VERBOSE_MSG(...) kr_log_q(req->current_query, ITERATOR, __VA_ARGS__)
+#define QVERBOSE_MSG(qry, ...) kr_log_q(qry, ITERATOR, __VA_ARGS__)
 #define WITH_VERBOSE(qry) if (kr_log_is_debug_qry(ITERATOR, (qry)))
 
 /* Iterator often walks through packet section, this is an abstraction. */
diff -pruN 5.4.4-1/lib/layer/validate.c 5.5.1-5/lib/layer/validate.c
--- 5.4.4-1/lib/layer/validate.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/layer/validate.c	2022-06-14 07:17:30.407490700 +0000
@@ -25,7 +25,7 @@
 #include "lib/module.h"
 #include "lib/selection.h"
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE(qry, VALIDATOR, __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q(qry, VALIDATOR, __VA_ARGS__)
 
 #define MAX_REVALIDATION_CNT 2
 
@@ -148,6 +148,7 @@ do_downgrade: // we do this deep inside
 static int validate_section(kr_rrset_validation_ctx_t *vctx, struct kr_query *qry,
 			    knot_mm_t *pool)
 {
+	struct kr_request *req = qry->request;
 	if (!vctx) {
 		return kr_error(EINVAL);
 	}
@@ -235,10 +236,17 @@ static int validate_section(kr_rrset_val
 			/* no RRSIGs found */
 			kr_rank_set(&entry->rank, KR_RANK_MISSING);
 			vctx->err_cnt += 1;
+			kr_request_set_extended_error(req, KNOT_EDNS_EDE_RRSIG_MISS, "JZAJ");
 			log_bogus_rrsig(vctx, rr, "no valid RRSIGs found");
 		} else {
 			kr_rank_set(&entry->rank, KR_RANK_BOGUS);
 			vctx->err_cnt += 1;
+			if (vctx->rrs_counters.expired > 0)
+				kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_EXPIRED, "YFJ2");
+			else if (vctx->rrs_counters.notyet > 0)
+				kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_NOTYET, "UBBS");
+			else
+				kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "I74V");
 			log_bogus_rrsig(vctx, rr, "bogus signatures");
 		}
 	}
@@ -357,6 +365,7 @@ static int validate_keyset(struct kr_req
 			}
 		}
 		if (sig_index < 0) {
+			kr_request_set_extended_error(req, KNOT_EDNS_EDE_RRSIG_MISS, "EZDC");
 			return kr_error(ENOENT);
 		}
 		const knot_rdataset_t *sig_rds = &req->answ_selected.at[sig_index]->rr->rrs;
@@ -381,12 +390,15 @@ static int validate_keyset(struct kr_req
 				ret == 0 ? KR_RANK_SECURE : KR_RANK_BOGUS);
 
 		if (ret != 0) {
-			if (ret != kr_error(DNSSEC_INVALID_DS_ALGORITHM) &&
-			    ret != kr_error(EAGAIN)) {
-				log_bogus_rrsig(&vctx, qry->zone_cut.key, "bogus key");
-			}
+			log_bogus_rrsig(&vctx, qry->zone_cut.key, "bogus key");
 			knot_rrset_free(qry->zone_cut.key, qry->zone_cut.pool);
 			qry->zone_cut.key = NULL;
+			if (vctx.rrs_counters.expired > 0)
+				kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_EXPIRED, "6GJV");
+			else if (vctx.rrs_counters.notyet > 0)
+				kr_request_set_extended_error(req, KNOT_EDNS_EDE_SIG_NOTYET, "4DJQ");
+			else
+				kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "EXRU");
 			return ret;
 		}
 
@@ -524,10 +536,19 @@ static int update_delegation(struct kr_r
 		if (!has_nsec3) {
 			if (referral) {
 				/* Check if it is referral to unsigned, rfc4035 5.2 */
-				ret = kr_nsec_ref_to_unsigned(answer);
+				ret = kr_nsec_ref_to_unsigned(&req->auth_selected,
+								qry->uid, proved_name);
 			} else {
 				/* No-data answer */
-				ret = kr_nsec_existence_denial(answer, KNOT_AUTHORITY, proved_name, KNOT_RRTYPE_DS);
+				ret = kr_nsec_negative(&req->auth_selected, qry->uid,
+							proved_name, KNOT_RRTYPE_DS);
+				if (ret >= 0) {
+					if (ret == PKT_NODATA) {
+						ret = kr_ok();
+					} else {
+						ret = kr_error(ENOENT); // suspicious
+					}
+				}
 			}
 		} else {
 			if (referral) {
@@ -555,6 +576,7 @@ static int update_delegation(struct kr_r
 			}
 		} else if (ret != 0) {
 			VERBOSE_MSG(qry, "<= bogus proof of DS non-existence\n");
+			kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "Z4I6");
 			qry->flags.DNSSEC_BOGUS = true;
 		} else if (proved_name[0] != '\0') { /* don't go to insecure for . DS */
 			qry->flags.DNSSEC_NODS = true;
@@ -699,6 +721,8 @@ static int check_validation_result(kr_la
 	if (!kr_rank_test(invalid_entry->rank, KR_RANK_SECURE) &&
 	    (++(invalid_entry->revalidation_cnt) > MAX_REVALIDATION_CNT)) {
 		VERBOSE_MSG(qry, "<= continuous revalidation, fails\n");
+		kr_request_set_extended_error(req, KNOT_EDNS_EDE_OTHER,
+			"4T4L: continuous revalidation");
 		qry->flags.DNSSEC_BOGUS = true;
 		return KR_STATE_FAIL;
 	}
@@ -722,6 +746,7 @@ static int check_validation_result(kr_la
 	} else if (kr_rank_test(invalid_entry->rank, KR_RANK_MISSING)) {
 		ret = rrsig_not_found(ctx, pkt, rr);
 	} else if (!kr_rank_test(invalid_entry->rank, KR_RANK_SECURE)) {
+		kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "NXJA");
 		qry->flags.DNSSEC_BOGUS = true;
 		ret = KR_STATE_FAIL;
 	}
@@ -1059,6 +1084,7 @@ static int validate(kr_layer_t *ctx, kno
 	bool use_signatures = (knot_pkt_qtype(pkt) != KNOT_RRTYPE_RRSIG);
 	if (!(qry->flags.CACHED) && !knot_pkt_has_dnssec(pkt) && !use_signatures) {
 		VERBOSE_MSG(qry, "<= got insecure response\n");
+		kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "MISQ");
 		qry->flags.DNSSEC_BOGUS = true;
 		return KR_STATE_FAIL;
 	}
@@ -1074,6 +1100,7 @@ static int validate(kr_layer_t *ctx, kno
 		 * but iterator has not selected any records. */
 		if (!check_empty_answer(ctx, pkt)) {
 			VERBOSE_MSG(qry, "<= no useful RR in authoritative answer\n");
+			kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "MJX6");
 			qry->flags.DNSSEC_BOGUS = true;
 			return KR_STATE_FAIL;
 		}
@@ -1095,6 +1122,7 @@ static int validate(kr_layer_t *ctx, kno
 		if (ds && !kr_ds_algo_support(ds)) {
 			VERBOSE_MSG(qry, ">< all DS entries use unsupported algorithm pairs, going insecure\n");
 			/* ^ the message is a bit imprecise to avoid being too verbose */
+			kr_request_set_extended_error(req, KNOT_EDNS_EDE_OTHER, "LSLC: unsupported digest/key");
 			qry->flags.DNSSEC_WANT = false;
 			qry->flags.DNSSEC_INSECURE = true;
 			rank_records(qry, true, KR_RANK_INSECURE, qry->zone_cut.name);
@@ -1108,6 +1136,7 @@ static int validate(kr_layer_t *ctx, kno
 			return KR_STATE_YIELD;
 		} else if (ret != 0) {
 			VERBOSE_MSG(qry, "<= bad keys, broken trust chain\n");
+			/* EDE code already set in validate_keyset() */
 			qry->flags.DNSSEC_BOGUS = true;
 			return KR_STATE_FAIL;
 		}
@@ -1124,6 +1153,8 @@ static int validate(kr_layer_t *ctx, kno
 			/* something exceptional - no DNS key, empty pointers etc
 			 * normally it shouldn't happen */
 			VERBOSE_MSG(qry, "<= couldn't validate RRSIGs\n");
+			kr_request_set_extended_error(req, KNOT_EDNS_EDE_OTHER,
+				"O4TP: couldn't validate RRSIGs");
 			qry->flags.DNSSEC_BOGUS = true;
 			return KR_STATE_FAIL;
 		}
@@ -1149,7 +1180,15 @@ static int validate(kr_layer_t *ctx, kno
 	if (!qry->flags.CACHED && pkt_rcode == KNOT_RCODE_NXDOMAIN && !qry->flags.CNAME) {
 		/* @todo If knot_pkt_qname(pkt) is used instead of qry->sname then the tests crash. */
 		if (!has_nsec3) {
-			ret = kr_nsec_name_error_response_check(pkt, KNOT_AUTHORITY, qry->sname);
+			ret = kr_nsec_negative(&req->auth_selected, qry->uid,
+						qry->sname, KNOT_RRTYPE_NULL);
+			if (ret >= 0) {
+				if (ret & PKT_NXDOMAIN) {
+					ret = kr_ok();
+				} else {
+					ret = kr_error(ENOENT); // probably proved NODATA
+				}
+			}
 		} else {
 			ret = kr_nsec3_name_error_response_check(pkt, KNOT_AUTHORITY, qry->sname);
 		}
@@ -1162,6 +1201,7 @@ static int validate(kr_layer_t *ctx, kno
 			VERBOSE_MSG(qry, "<= can't prove NXDOMAIN due to optout, going insecure\n");
 		} else if (ret != 0) {
 			VERBOSE_MSG(qry, "<= bad NXDOMAIN proof\n");
+			kr_request_set_extended_error(req, KNOT_EDNS_EDE_NSEC_MISS, "3WKM");
 			qry->flags.DNSSEC_BOGUS = true;
 			return KR_STATE_FAIL;
 		}
@@ -1179,7 +1219,15 @@ static int validate(kr_layer_t *ctx, kno
 			 * ? merge the functionality together to share code/resources
 			 */
 			if (!has_nsec3) {
-				ret = kr_nsec_existence_denial(pkt, KNOT_AUTHORITY, knot_pkt_qname(pkt), knot_pkt_qtype(pkt));
+				ret = kr_nsec_negative(&req->auth_selected, qry->uid,
+							knot_pkt_qname(pkt), knot_pkt_qtype(pkt));
+				if (ret >= 0) {
+					if (ret == PKT_NODATA) {
+						ret = kr_ok();
+					} else {
+						ret = kr_error(ENOENT); // suspicious
+					}
+				}
 			} else {
 				ret = kr_nsec3_no_data(pkt, KNOT_AUTHORITY, knot_pkt_qname(pkt), knot_pkt_qtype(pkt));
 			}
@@ -1193,6 +1241,7 @@ static int validate(kr_layer_t *ctx, kno
 					 * parent queries as insecure */
 				} else {
 					VERBOSE_MSG(qry, "<= bad NODATA proof\n");
+					kr_request_set_extended_error(req, KNOT_EDNS_EDE_NSEC_MISS, "AHXI");
 					qry->flags.DNSSEC_BOGUS = true;
 					return KR_STATE_FAIL;
 				}
@@ -1207,6 +1256,7 @@ static int validate(kr_layer_t *ctx, kno
 	if (ret == DNSSEC_NOT_FOUND && qry->stype != KNOT_RRTYPE_DS) {
 		if (ctx->state == KR_STATE_YIELD) {
 			VERBOSE_MSG(qry, "<= can't validate referral\n");
+			kr_request_set_extended_error(req, KNOT_EDNS_EDE_BOGUS, "XLE4");
 			qry->flags.DNSSEC_BOGUS = true;
 			return KR_STATE_FAIL;
 		} else {
@@ -1276,6 +1326,24 @@ static int validate_wrapper(kr_layer_t *
 	struct kr_query *qry = req->current_query;
 	if (ret & KR_STATE_FAIL && qry->flags.DNSSEC_BOGUS)
 		qry->server_selection.error(qry, req->upstream.transport, KR_SELECTION_DNSSEC_ERROR);
+	if (ret & KR_STATE_DONE && !qry->flags.DNSSEC_BOGUS) {
+		/* Don't report extended DNS errors related to validation
+		 * when it managed to succeed (e.g. by trying different auth). */
+		switch (req->extended_error.info_code) {
+		case KNOT_EDNS_EDE_BOGUS:
+		case KNOT_EDNS_EDE_NSEC_MISS:
+		case KNOT_EDNS_EDE_RRSIG_MISS:
+		case KNOT_EDNS_EDE_SIG_EXPIRED:
+		case KNOT_EDNS_EDE_SIG_NOTYET:
+			kr_request_set_extended_error(req, KNOT_EDNS_EDE_NONE, NULL);
+			break;
+		case KNOT_EDNS_EDE_DNSKEY_MISS:
+		case KNOT_EDNS_EDE_DNSKEY_BIT:
+			kr_assert(false);  /* These EDE codes aren't used. */
+			break;
+		default: break;  /* Remaining codes don't indicate hard DNSSEC failure. */
+		}
+	}
 	return ret;
 }
 
diff -pruN 5.4.4-1/lib/layer.h 5.5.1-5/lib/layer.h
--- 5.4.4-1/lib/layer.h	2022-01-05 13:19:14.459727800 +0000
+++ 5.5.1-5/lib/layer.h	2022-06-14 07:17:30.403490800 +0000
@@ -8,8 +8,6 @@
 #include "lib/defines.h"
 #include "lib/utils.h"
 
-#define QRVERBOSE kr_log_q // TODO: perhaps eliminate
-
 /** Layer processing states.  Only one value at a time (but see TODO).
  *
  *  Each state represents the state machine transition,
diff -pruN 5.4.4-1/lib/log.c 5.5.1-5/lib/log.c
--- 5.4.4-1/lib/log.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/log.c	2022-06-14 07:17:30.407490700 +0000
@@ -43,8 +43,6 @@ const log_group_names_t log_group_names[
 	GRP_NAME_ITEM(LOG_GRP_GNUTLS),
 	GRP_NAME_ITEM(LOG_GRP_TLSCLIENT),
 	GRP_NAME_ITEM(LOG_GRP_XDP),
-	GRP_NAME_ITEM(LOG_GRP_ZIMPORT),
-	GRP_NAME_ITEM(LOG_GRP_ZSCANNER),
 	GRP_NAME_ITEM(LOG_GRP_DOH),
 	GRP_NAME_ITEM(LOG_GRP_DNSSEC),
 	GRP_NAME_ITEM(LOG_GRP_HINT),
@@ -79,6 +77,7 @@ const log_group_names_t log_group_names[
 	GRP_NAME_ITEM(LOG_GRP_MODULE),
 	GRP_NAME_ITEM(LOG_GRP_DEVEL),
 	GRP_NAME_ITEM(LOG_GRP_RENUMBER),
+	GRP_NAME_ITEM(LOG_GRP_EDE),
 	GRP_NAME_ITEM(LOG_GRP_REQDBG),
 	{ NULL, LOG_GRP_UNKNOWN },
 };
diff -pruN 5.4.4-1/lib/log.h 5.5.1-5/lib/log.h
--- 5.4.4-1/lib/log.h	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/log.h	2022-06-14 07:17:30.407490700 +0000
@@ -43,8 +43,6 @@ enum kr_log_group {
 	LOG_GRP_GNUTLS,
 	LOG_GRP_TLSCLIENT,
 	LOG_GRP_XDP,
-	LOG_GRP_ZIMPORT,
-	LOG_GRP_ZSCANNER,
 	LOG_GRP_DOH,
 	LOG_GRP_DNSSEC,
 	LOG_GRP_HINT,
@@ -79,6 +77,7 @@ enum kr_log_group {
 	LOG_GRP_MODULE,
 	LOG_GRP_DEVEL,
 	LOG_GRP_RENUMBER,
+	LOG_GRP_EDE,
 	/* ^^ Add new log groups above ^^. */
 	LOG_GRP_REQDBG, /* Must be first non-displayed entry in enum! */
 };
@@ -99,8 +98,6 @@ enum kr_log_group {
 #define LOG_GRP_GNUTLS_TAG		"gnutls"	/**< ``gnutls``: low-level logs from GnuTLS */
 #define LOG_GRP_TLSCLIENT_TAG		"tls_cl"	/**< ``tls_cl``: TLS client messages (used for TLS forwarding) */
 #define LOG_GRP_XDP_TAG			"xdp"		/**< ``xdp``: operations related to XDP */
-#define LOG_GRP_ZIMPORT_TAG		"zimprt"	/**< ``zimprt``: operations related to zimport */
-#define LOG_GRP_ZSCANNER_TAG		"zscann"	/**< ``zscann``: operations related to zscanner */
 #define LOG_GRP_DOH_TAG			"doh"		/**< ``doh``: DNS-over-HTTPS logger (doh2 implementation) */
 #define LOG_GRP_DNSSEC_TAG		"dnssec"	/**< ``dnssec``: operations related to DNSSEC */
 #define LOG_GRP_HINT_TAG		"hint"		/**< ``hint``: operations related to static hints */
@@ -132,6 +129,7 @@ enum kr_log_group {
 #define LOG_GRP_MODULE_TAG		"module"	/**< ``module``: suitable for user-defined modules */
 #define LOG_GRP_DEVEL_TAG		"devel"		/**< ``devel``: for development purposes */
 #define LOG_GRP_RENUMBER_TAG		"renum"		/**< ``renum``: operation related to renumber */
+#define LOG_GRP_EDE_TAG			"exterr"	/**< ``exterr``: extended error module */
 #define LOG_GRP_REQDBG_TAG		"reqdbg"	/**< ``reqdbg``: debug logs enabled by policy actions */
 ///@}
 
diff -pruN 5.4.4-1/lib/meson.build 5.5.1-5/lib/meson.build
--- 5.4.4-1/lib/meson.build	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/meson.build	2022-06-14 07:17:30.407490700 +0000
@@ -17,7 +17,6 @@ libkres_src = files([
   'dnssec/signature.c',
   'dnssec/ta.c',
   'generic/lru.c',
-  'generic/map.c',
   'generic/queue.c',
   'generic/trie.c',
   'layer/cache.c',
@@ -48,7 +47,6 @@ libkres_headers = files([
   'dnssec/ta.h',
   'generic/array.h',
   'generic/lru.h',
-  'generic/map.h',
   'generic/pack.h',
   'generic/queue.h',
   'generic/trie.h',
@@ -68,10 +66,8 @@ libkres_headers = files([
 unit_tests += [
   ['array', files('generic/test_array.c')],
   ['lru', files('generic/test_lru.c')],
-  ['map', files('generic/test_map.c')],
   ['pack', files('generic/test_pack.c')],
   ['queue', files('generic/test_queue.c')],
-  ['set', files('generic/test_set.c')],
   ['trie', files('generic/test_trie.c')],
   ['module', files('test_module.c')],
   ['rplan', files('test_rplan.c')],
diff -pruN 5.4.4-1/lib/README.rst 5.5.1-5/lib/README.rst
--- 5.4.4-1/lib/README.rst	2022-01-05 13:19:14.451727600 +0000
+++ 5.5.1-5/lib/README.rst	2022-06-14 07:17:30.399490600 +0000
@@ -152,6 +152,7 @@ Elementary types and constants
 * DNS classes are in ``kres.class`` table, e.g. ``kres.class.IN`` for Internet class.
 * DNS types are in  ``kres.type`` table, e.g. ``kres.type.AAAA`` for AAAA type.
 * DNS rcodes types are in ``kres.rcode`` table, e.g. ``kres.rcode.NOERROR``.
+* Extended DNS error codes are in  ``kres.extended_error`` table, e.g. ``kres.extended_error.BLOCKED``.
 * Packet sections (QUESTION, ANSWER, AUTHORITY, ADDITIONAL) are in the ``kres.section`` table.
 
 Working with domain names
diff -pruN 5.4.4-1/lib/resolve.c 5.5.1-5/lib/resolve.c
--- 5.4.4-1/lib/resolve.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/resolve.c	2022-06-14 07:17:30.407490700 +0000
@@ -11,7 +11,6 @@
 #include <libknot/descriptor.h>
 #include <ucw/mempool.h>
 #include <sys/socket.h>
-#include "kresconfig.h"
 #include "lib/resolve.h"
 #include "lib/layer.h"
 #include "lib/rplan.h"
@@ -26,7 +25,7 @@
 #define KNOT_EDNS_OPTION_COOKIE 10
 #endif /* ENABLE_COOKIES */
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE((qry), RESOLVER,  __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q((qry), RESOLVER,  __VA_ARGS__)
 
 bool kr_rank_check(uint8_t rank)
 {
@@ -346,6 +345,19 @@ static int edns_erase_and_reserve(knot_p
 	return knot_pkt_reserve(pkt, len);
 }
 
+static inline size_t edns_padding_option_size(int32_t tls_padding)
+{
+	if (tls_padding == -1)
+		/* FIXME: we do not know how to reserve space for the
+		 * default padding policy, since we can't predict what
+		 * it will select. So i'm just guessing :/ */
+		return KNOT_EDNS_OPTION_HDRLEN + 512;
+	if (tls_padding >= 2)
+		return KNOT_EDNS_OPTION_HDRLEN + tls_padding;
+
+	return 0;
+}
+
 static int edns_create(knot_pkt_t *pkt, const struct kr_request *req)
 {
 	pkt->opt_rr = knot_rrset_copy(req->ctx->upstream_opt_rr, &pkt->mm);
@@ -356,14 +368,8 @@ static int edns_create(knot_pkt_t *pkt,
 		wire_size += KR_COOKIE_OPT_MAX_LEN;
 	}
 #endif /* ENABLE_COOKIES */
-	if (req->qsource.flags.tls) {
-		if (req->ctx->tls_padding == -1)
-			/* FIXME: we do not know how to reserve space for the
-			 * default padding policy, since we can't predict what
-			 * it will select. So i'm just guessing :/ */
-			wire_size += KNOT_EDNS_OPTION_HDRLEN + 512;
-		if (req->ctx->tls_padding >= 2)
-			wire_size += KNOT_EDNS_OPTION_HDRLEN + req->ctx->tls_padding;
+	if (req->qsource.flags.tls || req->qsource.comm_flags.tls) {
+		wire_size += edns_padding_option_size(req->ctx->tls_padding);
 	}
 	return knot_pkt_reserve(pkt, wire_size);
 }
@@ -417,26 +423,17 @@ static int write_extra_ranked_records(co
 	return err;
 }
 
-/** @internal Add an EDNS padding RR into the answer if requested and required. */
-static int answer_padding(struct kr_request *request)
+static int pkt_padding(knot_pkt_t *packet, int32_t padding)
 {
-	if (kr_fails_assert(request && request->answer && request->ctx))
-		return kr_error(EINVAL);
-	if (!request->qsource.flags.tls) {
-		/* Not meaningful to pad without encryption. */
-		return kr_ok();
-	}
-	int32_t padding = request->ctx->tls_padding;
-	knot_pkt_t *answer = request->answer;
-	knot_rrset_t *opt_rr = answer->opt_rr;
+	knot_rrset_t *opt_rr = packet->opt_rr;
 	int32_t pad_bytes = -1;
 
 	if (padding == -1) { /* use the default padding policy from libknot */
-		pad_bytes =  knot_pkt_default_padding_size(answer, opt_rr);
+		pad_bytes =  knot_pkt_default_padding_size(packet, opt_rr);
 	}
 	if (padding >= 2) {
-		int32_t max_pad_bytes = knot_edns_get_payload(opt_rr) - (answer->size + knot_rrset_size(opt_rr));
-		pad_bytes = MIN(knot_edns_alignment_size(answer->size, knot_rrset_size(opt_rr), padding),
+		int32_t max_pad_bytes = knot_edns_get_payload(opt_rr) - (packet->size + knot_rrset_size(opt_rr));
+		pad_bytes = MIN(knot_edns_alignment_size(packet->size, knot_rrset_size(opt_rr), padding),
 				max_pad_bytes);
 	}
 
@@ -444,15 +441,27 @@ static int answer_padding(struct kr_requ
 		uint8_t zeros[MAX(1, pad_bytes)];
 		memset(zeros, 0, sizeof(zeros));
 		int r = knot_edns_add_option(opt_rr, KNOT_EDNS_OPTION_PADDING,
-					     pad_bytes, zeros, &answer->mm);
+					     pad_bytes, zeros, &packet->mm);
 		if (r != KNOT_EOK) {
-			knot_rrset_clear(opt_rr, &answer->mm);
+			knot_rrset_clear(opt_rr, &packet->mm);
 			return kr_error(r);
 		}
 	}
 	return kr_ok();
 }
 
+/** @internal Add an EDNS padding RR into the answer if requested and required. */
+static int answer_padding(struct kr_request *request)
+{
+	if (kr_fails_assert(request && request->answer && request->ctx))
+		return kr_error(EINVAL);
+	if (!request->qsource.flags.tls && !request->qsource.comm_flags.tls) {
+		/* Not meaningful to pad without encryption. */
+		return kr_ok();
+	}
+	return pkt_padding(request->answer, request->ctx->tls_padding);
+}
+
 /* Make a clean SERVFAIL answer. */
 static void answer_fail(struct kr_request *request)
 {
@@ -538,27 +547,26 @@ static void answer_finalize(struct kr_re
 	/* AD flag.  We can only change `secure` from true to false.
 	 * Be conservative.  Primary approach: check ranks of all RRs in wire.
 	 * Only "negative answers" need special handling. */
-	bool secure = last != NULL && request->state == KR_STATE_DONE /*< suspicious otherwise */
+	bool secure = request->state == KR_STATE_DONE /*< suspicious otherwise */
 		&& knot_pkt_qtype(answer) != KNOT_RRTYPE_RRSIG;
-	if (last && (last->flags.STUB)) {
+	if (last->flags.STUB) {
 		secure = false; /* don't trust forwarding for now */
 	}
-	if (last && (last->flags.DNSSEC_OPTOUT)) {
+	if (last->flags.DNSSEC_OPTOUT) {
 		VERBOSE_MSG(last, "insecure because of opt-out\n");
 		secure = false; /* the last answer is insecure due to opt-out */
 	}
 
 	/* Write all RRsets meant for the answer. */
-	const uint16_t reorder = last ? last->reorder : 0;
 	bool answ_all_cnames = false/*arbitrary*/;
 	if (knot_pkt_begin(answer, KNOT_ANSWER)
-	    || write_extra_ranked_records(&request->answ_selected, reorder,
+	    || write_extra_ranked_records(&request->answ_selected, last->reorder,
 					answer, &secure, &answ_all_cnames)
 	    || knot_pkt_begin(answer, KNOT_AUTHORITY)
-	    || write_extra_ranked_records(&request->auth_selected, reorder,
+	    || write_extra_ranked_records(&request->auth_selected, last->reorder,
 					answer, &secure, NULL)
 	    || knot_pkt_begin(answer, KNOT_ADDITIONAL)
-	    || write_extra_ranked_records(&request->add_selected, reorder,
+	    || write_extra_ranked_records(&request->add_selected, last->reorder,
 					answer, NULL/*not relevant to AD*/, NULL)
 	    || answer_append_edns(request)
 	   )
@@ -567,7 +575,6 @@ static void answer_finalize(struct kr_re
 		return;
 	}
 
-	if (!last) secure = false; /*< should be no-op, mostly documentation */
 	/* AD: "negative answers" need more handling. */
 	if (kr_response_classify(answer) != PKT_NOERROR
 	    /* Additionally check for CNAME chains that "end in NODATA",
@@ -722,6 +729,10 @@ knot_rrset_t* kr_request_ensure_edns(str
 
 knot_pkt_t *kr_request_ensure_answer(struct kr_request *request)
 {
+	if (request->options.NO_ANSWER) {
+		kr_assert(request->state & KR_STATE_FAIL);
+		return NULL;
+	}
 	if (request->answer)
 		return request->answer;
 
@@ -731,9 +742,10 @@ knot_pkt_t *kr_request_ensure_answer(str
 	// Find answer_max: limit on DNS wire length.
 	uint16_t answer_max;
 	const struct kr_request_qsource_flags *qs_flags = &request->qsource.flags;
-	if (kr_fails_assert((qs_flags->tls || qs_flags->http) ? qs_flags->tcp : true))
+	const struct kr_request_qsource_flags *qs_cflags = &request->qsource.comm_flags;
+	if (kr_fails_assert(!(qs_flags->tls || qs_cflags->tls || qs_cflags->http) || qs_flags->tcp))
 		goto fail;
-	if (!request->qsource.addr || qs_flags->tcp) {
+	if (!request->qsource.addr || qs_flags->tcp || qs_cflags->tcp) {
 		// not on UDP
 		answer_max = KNOT_WIRE_MAX_PKTSIZE;
 	} else if (knot_pkt_has_edns(qs_pkt)) {
@@ -780,16 +792,6 @@ fail:
 	return request->answer = NULL;
 }
 
-static bool resolution_time_exceeded(struct kr_query *qry, uint64_t now)
-{
-	uint64_t resolving_time = now - qry->creation_time_mono;
-	if (resolving_time > KR_RESOLVE_TIME_LIMIT) {
-		VERBOSE_MSG(qry, "query resolution time limit exceeded\n");
-		return true;
-	}
-	return false;
-}
-
 int kr_resolve_consume(struct kr_request *request, struct kr_transport **transport, knot_pkt_t *packet)
 {
 	struct kr_rplan *rplan = &request->rplan;
@@ -802,36 +804,36 @@ int kr_resolve_consume(struct kr_request
 	/* Different processing for network error */
 	struct kr_query *qry = array_tail(rplan->pending);
 	/* Check overall resolution time */
-	if (resolution_time_exceeded(qry, kr_now())) {
+	if (kr_now() - qry->creation_time_mono >= KR_RESOLVE_TIME_LIMIT) {
+		kr_query_inform_timeout(request, qry);
 		return KR_STATE_FAIL;
 	}
 	bool tried_tcp = (qry->flags.TCP);
-	if (!packet || packet->size == 0) {
+	if (!packet || packet->size == 0)
 		return KR_STATE_PRODUCE;
+
+	/* Packet cleared, derandomize QNAME. */
+	knot_dname_t *qname_raw = knot_pkt_qname(packet);
+	if (qname_raw && qry->secret != 0) {
+		randomized_qname_case(qname_raw, qry->secret);
+	}
+	request->state = KR_STATE_CONSUME;
+	if (qry->flags.CACHED) {
+		ITERATE_LAYERS(request, qry, consume, packet);
 	} else {
-		/* Packet cleared, derandomize QNAME. */
-		knot_dname_t *qname_raw = knot_pkt_qname(packet);
-		if (qname_raw && qry->secret != 0) {
-			randomized_qname_case(qname_raw, qry->secret);
-		}
-		request->state = KR_STATE_CONSUME;
-		if (qry->flags.CACHED) {
-			ITERATE_LAYERS(request, qry, consume, packet);
-		} else {
-			/* Fill in source and latency information. */
-			request->upstream.rtt = kr_now() - qry->timestamp_mono;
-			request->upstream.transport = transport ? *transport : NULL;
-			ITERATE_LAYERS(request, qry, consume, packet);
-			/* Clear temporary information */
-			request->upstream.transport = NULL;
-			request->upstream.rtt = 0;
-		}
+		/* Fill in source and latency information. */
+		request->upstream.rtt = kr_now() - qry->timestamp_mono;
+		request->upstream.transport = transport ? *transport : NULL;
+		ITERATE_LAYERS(request, qry, consume, packet);
+		/* Clear temporary information */
+		request->upstream.transport = NULL;
+		request->upstream.rtt = 0;
 	}
 
 	if (transport && !qry->flags.CACHED) {
 		if (!(request->state & KR_STATE_FAIL)) {
 			/* Do not complete NS address resolution on soft-fail. */
-			const int rcode = packet ? knot_wire_get_rcode(packet->wire) : 0;
+			const int rcode = knot_wire_get_rcode(packet->wire);
 			if (rcode != KNOT_RCODE_SERVFAIL && rcode != KNOT_RCODE_REFUSED) {
 				qry->flags.AWAIT_IPV6 = false;
 				qry->flags.AWAIT_IPV4 = false;
@@ -915,8 +917,8 @@ static struct kr_query *zone_cut_subreq(
 static int forward_trust_chain_check(struct kr_request *request, struct kr_query *qry, bool resume)
 {
 	struct kr_rplan *rplan = &request->rplan;
-	map_t *trust_anchors = &request->ctx->trust_anchors;
-	map_t *negative_anchors = &request->ctx->negative_anchors;
+	trie_t *trust_anchors = request->ctx->trust_anchors;
+	trie_t *negative_anchors = request->ctx->negative_anchors;
 
 	if (qry->parent != NULL &&
 	    !(qry->forward_flags.CNAME) &&
@@ -1101,8 +1103,8 @@ static int forward_trust_chain_check(str
 static int trust_chain_check(struct kr_request *request, struct kr_query *qry)
 {
 	struct kr_rplan *rplan = &request->rplan;
-	map_t *trust_anchors = &request->ctx->trust_anchors;
-	map_t *negative_anchors = &request->ctx->negative_anchors;
+	trie_t *trust_anchors = request->ctx->trust_anchors;
+	trie_t *negative_anchors = request->ctx->negative_anchors;
 
 	/* Disable DNSSEC if it enters NTA. */
 	if (kr_ta_get(negative_anchors, qry->zone_cut.name)){
@@ -1534,6 +1536,17 @@ int kr_resolve_checkout(struct kr_reques
 
 	/* Write down OPT unless in safemode */
 	if (!(qry->flags.NO_EDNS)) {
+		/* TLS padding */
+		if (transport->protocol == KR_TRANSPORT_TLS) {
+			size_t padding_size = edns_padding_option_size(request->ctx->tls_padding);
+			ret = knot_pkt_reserve(packet, padding_size);
+			if (ret)
+				return kr_error(EINVAL);
+			ret = pkt_padding(packet, request->ctx->tls_padding);
+			if (ret)
+				return kr_error(EINVAL);
+		}
+
 		ret = edns_put(packet, true);
 		if (ret != 0) {
 			return kr_error(EINVAL);
@@ -1615,4 +1628,71 @@ knot_mm_t *kr_resolve_pool(struct kr_req
 	return NULL;
 }
 
+static int ede_priority(int info_code)
+{
+	switch(info_code) {
+	case KNOT_EDNS_EDE_DNSKEY_BIT:
+	case KNOT_EDNS_EDE_DNSKEY_MISS:
+	case KNOT_EDNS_EDE_SIG_EXPIRED:
+	case KNOT_EDNS_EDE_SIG_NOTYET:
+	case KNOT_EDNS_EDE_RRSIG_MISS:
+	case KNOT_EDNS_EDE_NSEC_MISS:
+		return 900;  /* Specific DNSSEC failures */
+	case KNOT_EDNS_EDE_BOGUS:
+		return 800;  /* Generic DNSSEC failure */
+	case KNOT_EDNS_EDE_FORGED:
+	case KNOT_EDNS_EDE_FILTERED:
+		return 700;  /* Considered hard fail by firefox */
+	case KNOT_EDNS_EDE_PROHIBITED:
+	case KNOT_EDNS_EDE_BLOCKED:
+	case KNOT_EDNS_EDE_CENSORED:
+		return 600;  /* Policy related */
+	case KNOT_EDNS_EDE_DNSKEY_ALG:
+	case KNOT_EDNS_EDE_DS_DIGEST:
+		return 500;  /* Non-critical DNSSEC issues */
+	case KNOT_EDNS_EDE_STALE:
+	case KNOT_EDNS_EDE_STALE_NXD:
+		return 300;  /* Serve-stale answers. */
+	case KNOT_EDNS_EDE_INDETERMINATE:
+	case KNOT_EDNS_EDE_CACHED_ERR:
+	case KNOT_EDNS_EDE_NOT_READY:
+	case KNOT_EDNS_EDE_NOTAUTH:
+	case KNOT_EDNS_EDE_NOTSUP:
+	case KNOT_EDNS_EDE_NREACH_AUTH:
+	case KNOT_EDNS_EDE_NETWORK:
+	case KNOT_EDNS_EDE_INV_DATA:
+		return 200;  /* Assorted codes */
+	case KNOT_EDNS_EDE_OTHER:
+		return 100;  /* Most generic catch-all error */
+	case KNOT_EDNS_EDE_NONE:
+		return 0;  /* No error - allow overriding */
+	default:
+		kr_assert(false);  /* Unknown info_code */
+		return 50;
+	}
+}
+
+int kr_request_set_extended_error(struct kr_request *request, int info_code, const char *extra_text)
+{
+	if (kr_fails_assert(request))
+		return KNOT_EDNS_EDE_NONE;
+
+	struct kr_extended_error *ede = &request->extended_error;
+
+	/* Clear any previously set error. */
+	if (info_code == KNOT_EDNS_EDE_NONE) {
+		kr_assert(extra_text == NULL);
+		ede->info_code = KNOT_EDNS_EDE_NONE;
+		ede->extra_text = NULL;
+		return KNOT_EDNS_EDE_NONE;
+	}
+
+	if (ede_priority(info_code) >= ede_priority(ede->info_code)) {
+		ede->info_code = info_code;
+		ede->extra_text = extra_text;
+	}
+
+	return ede->info_code;
+}
+
 #undef VERBOSE_MSG
diff -pruN 5.4.4-1/lib/resolve.h 5.5.1-5/lib/resolve.h
--- 5.4.4-1/lib/resolve.h	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/resolve.h	2022-06-14 07:17:30.407490700 +0000
@@ -11,7 +11,6 @@
 #include "lib/cookies/control.h"
 #include "lib/cookies/lru_cache.h"
 #include "lib/layer.h"
-#include "lib/generic/map.h"
 #include "lib/generic/array.h"
 #include "lib/selection.h"
 #include "lib/rplan.h"
@@ -161,8 +160,8 @@ struct kr_context
 	knot_rrset_t *downstream_opt_rr;
 	knot_rrset_t *upstream_opt_rr;
 
-	map_t trust_anchors;
-	map_t negative_anchors;
+	trie_t *trust_anchors;
+	trie_t *negative_anchors;
 	struct kr_zonecut root_hints;
 	struct kr_cache cache;
 	unsigned cache_rtt_tout_retry_interval;
@@ -184,9 +183,16 @@ struct kr_request_qsource_flags {
 	bool xdp:1; /**< true if the request is on AF_XDP; only meaningful if (dst_addr). */
 };
 
+/* Extended DNS Errors, RFC 8914 */
+struct kr_extended_error {
+	int32_t info_code;  /**< May contain -1 (KNOT_EDNS_EDE_NONE); filter before converting to uint16_t. */
+	const char *extra_text; /**< Can be NULL.  Allocated on the kr_request::pool or static. */
+};
+
+
 typedef bool (*addr_info_f)(struct sockaddr*);
 typedef void (*async_resolution_f)(knot_dname_t*, enum knot_rr_type);
-typedef array_t(union inaddr) inaddr_array_t;
+typedef array_t(union kr_sockaddr) kr_sockaddr_array_t;
 
 /**
  * Name resolution request.
@@ -203,14 +209,27 @@ struct kr_request {
 	knot_pkt_t *answer; /**< See kr_request_ensure_answer() */
 	struct kr_query *current_query;    /**< Current evaluated query. */
 	struct {
-		/** Address that originated the request. NULL for internal origin. */
+		/** Address that originated the request. May be that of a client
+		 * behind a proxy, if PROXYv2 is used. Otherwise, it will be
+		 * the same as `comm_addr`. `NULL` for internal origin. */
 		const struct sockaddr *addr;
-		/** Address that accepted the request.  NULL for internal origin.
+		/** Address that communicated the request. This may be the address
+		 * of a proxy. It is the same as `addr` if no proxy is used.
+		 * `NULL` for internal origin. */
+		const struct sockaddr *comm_addr;
+		/** Address that accepted the request. `NULL` for internal origin.
 		 * Beware: in case of UDP on wildcard address it will be wildcard;
 		 * closely related: issue #173. */
 		const struct sockaddr *dst_addr;
 		const knot_pkt_t *packet;
-		struct kr_request_qsource_flags flags; /**< See definition above. */
+		/** Request flags from the point of view of the original client.
+		 * This client may be behind a proxy. */
+		struct kr_request_qsource_flags flags;
+		/** Request flags from the point of view of the client actually
+		 * communicating with the resolver. When PROXYv2 protocol is used,
+		 * this describes the request from the proxy. When there is no
+		 * proxy, this will have exactly the same value as `flags`. */
+		struct kr_request_qsource_flags comm_flags;
 		size_t size; /**< query packet size */
 		int32_t stream_id; /**< HTTP/2 stream ID for DoH requests */
 		kr_http_header_array_t headers;  /**< HTTP/2 headers for DoH requests */
@@ -246,11 +265,12 @@ struct kr_request {
 		addr_info_f is_tls_capable;
 		addr_info_f is_tcp_connected;
 		addr_info_f is_tcp_waiting;
-		inaddr_array_t forwarding_targets; /**< When forwarding, possible targets are put here */
+		kr_sockaddr_array_t forwarding_targets; /**< When forwarding, possible targets are put here */
 	} selection_context;
 	unsigned int count_no_nsaddr;
 	unsigned int count_fail_row;
 	alloc_wire_f alloc_wire_cb; /**< CB to allocate answer wire (can be NULL). */
+	struct kr_extended_error extended_error;  /**< EDE info; don't modify directly, use kr_request_set_extended_error() */
 };
 
 /** Initializer for an array of *_selected. */
@@ -365,3 +385,36 @@ struct kr_rplan *kr_resolve_plan(struct
  */
 KR_EXPORT KR_PURE
 knot_mm_t *kr_resolve_pool(struct kr_request *request);
+
+/**
+ * Set the extended DNS error for request.
+ *
+ * The error is set only if it has a higher or the same priority as the one
+ * already assigned.  The provided extra_text may be NULL, or a string that is
+ * allocated either statically, or on the request's mempool. To clear any
+ * error, call it with KNOT_EDNS_EDE_NONE and NULL as extra_text.
+ *
+ * To facilitate debugging, we include a unique base32 identifier at the start
+ * of the extra_text field for every call of this function. To generate such an
+ * identifier, you can use the command:
+ * $ base32 /dev/random | head -c 4
+ *
+ * @param  request     request state
+ * @param  info_code   extended DNS error code
+ * @param  extra_text  optional string with additional information
+ * @return             info_code that is set after the call
+ */
+KR_EXPORT
+int kr_request_set_extended_error(struct kr_request *request, int info_code, const char *extra_text);
+
+static inline void kr_query_inform_timeout(struct kr_request *req, const struct kr_query *qry)
+{
+	kr_request_set_extended_error(req, KNOT_EDNS_EDE_NREACH_AUTH, "RRPF");
+
+	unsigned ind = 0;
+	for (const struct kr_query *q = qry; q; q = q->parent)
+		ind += 2;
+	const uint32_t qid = qry ? qry->uid : 0;
+
+	kr_log_req(req, qid, ind, WORKER, "internal timeout for resolving the request has expired\n");
+}
diff -pruN 5.4.4-1/lib/rplan.c 5.5.1-5/lib/rplan.c
--- 5.4.4-1/lib/rplan.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/rplan.c	2022-06-14 07:17:30.407490700 +0000
@@ -7,11 +7,8 @@
 
 #include "lib/rplan.h"
 #include "lib/resolve.h"
-#include "lib/cache/api.h"
-#include "lib/defines.h"
-#include "lib/layer.h"
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE(qry, PLAN,  __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q(qry, PLAN,  __VA_ARGS__)
 
 inline static unsigned char chars_or(const unsigned char a, const unsigned char b)
 {
@@ -28,7 +25,7 @@ inline static unsigned char chars_mask(c
 inline static void kr_qflags_mod(struct kr_qflags *fl1, struct kr_qflags fl2,
 			unsigned char mod(const unsigned char a, const unsigned char b))
 {
-	if (!fl1) abort();
+	kr_require(fl1);
 	union {
 		struct kr_qflags flags;
 		/* C99 section 6.5.3.4: sizeof(char) == 1 */
diff -pruN 5.4.4-1/lib/rplan.h 5.5.1-5/lib/rplan.h
--- 5.4.4-1/lib/rplan.h	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/rplan.h	2022-06-14 07:17:30.407490700 +0000
@@ -18,6 +18,9 @@ struct kr_qflags {
 	bool NO_IPV6 : 1;        /**< Disable IPv6 */
 	bool NO_IPV4 : 1;        /**< Disable IPv4 */
 	bool TCP : 1;            /**< Use TCP (or TLS) for this query. */
+	bool NO_ANSWER : 1;      /**< Do not send any answer to the client.
+				  *   Request state should be set to `KR_STATE_FAIL`
+				  *   when this flag is set. */
 	bool RESOLVED : 1;       /**< Query is resolved.  Note that kr_query gets
 				  *   RESOLVED before following a CNAME chain; see .CNAME. */
 	bool AWAIT_IPV4 : 1;     /**< Query is waiting for A address. */
diff -pruN 5.4.4-1/lib/selection.c 5.5.1-5/lib/selection.c
--- 5.4.4-1/lib/selection.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/selection.c	2022-06-14 07:17:30.407490700 +0000
@@ -3,23 +3,18 @@
 #include "lib/selection.h"
 #include "lib/selection_forward.h"
 #include "lib/selection_iter.h"
-#include "lib/generic/pack.h"
-#include "lib/generic/trie.h"
 #include "lib/rplan.h"
 #include "lib/cache/api.h"
 #include "lib/resolve.h"
 
-#include "daemon/worker.h"
-#include "daemon/tls.h"
-
 #include "lib/utils.h"
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE((qry), SELECTION, __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q((qry), SELECTION, __VA_ARGS__)
 
 #define DEFAULT_TIMEOUT 400
 #define MAX_TIMEOUT 10000
 #define EXPLORE_TIMEOUT_COEFFICIENT 2
-#define MAX_BACKOFF 5
+#define MAX_BACKOFF 8
 #define MINIMAL_TIMEOUT_ADDITION 20
 
 /* After TCP_TIMEOUT_THRESHOLD timeouts one transport, we'll switch to TCP. */
@@ -81,7 +76,7 @@ static struct {
 	uint8_t addr_prefixes[NO6_PREFIX_COUNT][NO6_PREFIX_BYTES];
 } no6_est = { .len_used = 0 };
 
-static inline bool no6_is_bad(void)
+bool no6_is_bad(void)
 {
 	return no6_est.len_used == NO6_PREFIX_COUNT;
 }
@@ -180,7 +175,7 @@ int put_rtt_state(const uint8_t *ip, siz
 	return ret;
 }
 
-void bytes_to_ip(uint8_t *bytes, size_t len, uint16_t port, union inaddr *dst)
+void bytes_to_ip(uint8_t *bytes, size_t len, uint16_t port, union kr_sockaddr *dst)
 {
 	switch (len) {
 	case sizeof(struct in_addr):
@@ -199,7 +194,7 @@ void bytes_to_ip(uint8_t *bytes, size_t
 	}
 }
 
-uint8_t *ip_to_bytes(const union inaddr *src, size_t len)
+uint8_t *ip_to_bytes(const union kr_sockaddr *src, size_t len)
 {
 	switch (len) {
 	case sizeof(struct in_addr):
@@ -254,7 +249,7 @@ static void invalidate_dead_upstream(str
 				     unsigned int retry_timeout)
 {
 	struct rtt_state *rs = &state->rtt_state;
-	if (rs->consecutive_timeouts >= KR_NS_TIMEOUT_ROW_DEAD) {
+	if (rs->dead_since) {
 		uint64_t now = kr_now();
 		if (now < rs->dead_since) {
 			// broken continuity of timestamp (reboot, different machine, etc.)
@@ -311,7 +306,7 @@ static void check_network_settings(struc
 	}
 }
 
-void update_address_state(struct address_state *state, union inaddr *address,
+void update_address_state(struct address_state *state, union kr_sockaddr *address,
 			  size_t address_len, struct kr_query *qry)
 {
 	check_tls_capable(state, qry->request, &address->ip);
@@ -335,11 +330,8 @@ void update_address_state(struct address
 #endif
 }
 
-static int cmp_choices(const void *a, const void *b)
+static int cmp_choices(const struct choice *a_, const struct choice *b_)
 {
-	const struct choice *a_ = a;
-	const struct choice *b_ = b;
-
 	int diff;
 	/* Prefer IPv4 if IPv6 appears to be generally broken. */
 	diff = (int)a_->address_len - (int)b_->address_len;
@@ -364,21 +356,40 @@ static int cmp_choices(const void *a, co
 	}
 	return 0;
 }
-
-/* Fisher-Yates shuffle of the choices */
-static void shuffle_choices(struct choice choices[], int choices_len)
+/** Select the best entry from choices[] according to cmp_choices() comparator.
+ *
+ * Ties are decided in an (almost) uniformly random fashion.
+ */
+static const struct choice * select_best(const struct choice choices[], int choices_len)
 {
-	struct choice tmp;
-	for (int i = choices_len - 1; i > 0; i--) {
-		int j = kr_rand_bytes(1) % (i + 1);
-		tmp = choices[i];
-		choices[i] = choices[j];
-		choices[j] = tmp;
+	/* Deciding ties: it's as-if each index carries one byte of randomness.
+	 * Ties get decided by comparing that byte, and the byte itself
+	 * is computed lazily (negative until computed).
+	 */
+	int best_i = 0;
+	int best_rnd = -1;
+	for (int i = 1; i < choices_len; ++i) {
+		int diff = cmp_choices(&choices[i], &choices[best_i]);
+		if (diff > 0)
+			continue;
+		if (diff < 0) {
+			best_i = i;
+			best_rnd = -1;
+			continue;
+		}
+		if (best_rnd < 0)
+			best_rnd = kr_rand_bytes(1);
+		int new_rnd = kr_rand_bytes(1);
+		if (new_rnd < best_rnd) {
+			best_i = i;
+			best_rnd = new_rnd;
+		}
 	}
+	return &choices[best_i];
 }
 
 /* Adjust choice from `unresolved` in case of NO6 (broken IPv6). */
-static struct kr_transport unresolved_adjust(struct to_resolve unresolved[],
+static struct kr_transport unresolved_adjust(const struct to_resolve unresolved[],
 					     int unresolved_len, int index)
 {
 	if (unresolved[index].type != KR_TRANSPORT_RESOLVE_AAAA || !no6_is_bad())
@@ -409,8 +420,8 @@ finish:
 }
 
 /* Performs the actual selection (currently variation on epsilon-greedy). */
-struct kr_transport *select_transport(struct choice choices[], int choices_len,
-				      struct to_resolve unresolved[],
+struct kr_transport *select_transport(const struct choice choices[], int choices_len,
+				      const struct to_resolve unresolved[],
 				      int unresolved_len, int timeouts,
 				      struct knot_mm *mempool, bool tcp,
 				      size_t *choice_index)
@@ -422,17 +433,16 @@ struct kr_transport *select_transport(st
 
 	struct kr_transport *transport = mm_calloc(mempool, 1, sizeof(*transport));
 
-	/* Shuffle, so we choose fairly between choices with same attributes. */
-	shuffle_choices(choices, choices_len);
 	/* If there are some addresses with no rtt_info we try them
 	 * first (see cmp_choices). So unknown servers are chosen
 	 * *before* the best know server. This ensures that every option
 	 * is tried before going back to some that was tried before. */
-	qsort(choices, choices_len, sizeof(struct choice), cmp_choices);
-	struct choice *best = &choices[0];
-	struct choice *chosen;
+	const struct choice *best = select_best(choices, choices_len);
+	const struct choice *chosen;
 
-	const bool explore = choices_len == 0 || kr_rand_coin(EPSILON_NOMIN, EPSILON_DENOM);
+	const bool explore = choices_len == 0 || kr_rand_coin(EPSILON_NOMIN, EPSILON_DENOM)
+		/* We may need to explore to get at least one A record. */
+		|| (no6_is_bad() && best->address.ip.sa_family == AF_INET6);
 	if (explore) {
 		/* "EXPLORE":
 		 * randomly choose some option
@@ -449,8 +459,6 @@ struct kr_transport *select_transport(st
 		/* "EXPLOIT":
 		 * choose a resolved address which seems best right now. */
 		chosen = best;
-		if (no6_is_bad())
-			VERBOSE_MSG(NULL, "NO6: is KO [exploit]\n");
 	}
 
 	/* Don't try the same server again when there are other choices to be explored */
@@ -576,36 +584,50 @@ void update_rtt(struct kr_query *qry, st
 	}
 }
 
-static void cache_timeout(const struct kr_query *qry, const struct kr_transport *transport,
+/// Update rtt_state (including caching) after a server timed out.
+static void server_timeout(const struct kr_query *qry, const struct kr_transport *transport,
 			  struct address_state *addr_state, struct kr_cache *cache)
 {
-	if (transport->deduplicated) {
-		/* Transport was chosen by a different query, that one will
-		 * cache the result. */
+	// Make sure that the timeout wasn't capped; see kr_transport::timeout_capped
+	if (transport->timeout_capped)
 		return;
-	}
 
-	uint8_t *address = ip_to_bytes(&transport->address, transport->address_len);
+	const uint8_t *address = ip_to_bytes(&transport->address, transport->address_len);
 	if (transport->address_len == sizeof(struct in6_addr))
 		no6_timed_out(qry, address);
 
-	struct rtt_state old_state = addr_state->rtt_state;
-	struct rtt_state cur_state =
-		get_rtt_state(address, transport->address_len, cache);
-
-	/* We could lose some update from some other process by doing this,
-	 * but at least timeout count can't blow up. */
-	if (cur_state.consecutive_timeouts == old_state.consecutive_timeouts) {
-		if (++cur_state.consecutive_timeouts >=
-		    KR_NS_TIMEOUT_ROW_DEAD) {
-			cur_state.dead_since = kr_now();
-		}
-		put_rtt_state(address, transport->address_len, cur_state, cache);
+	struct rtt_state *state = &addr_state->rtt_state;
+	// While we were waiting for timeout, the stats might have changed considerably,
+	// so let's overwrite what we had by fresh cache contents.
+	// This is useful when the address is busy (we query it concurrently).
+	*state = get_rtt_state(address, transport->address_len, cache);
+
+	++state->consecutive_timeouts;
+	// Avoid overflow; we don't utilize very high values anyway (arbitrary limit).
+	state->consecutive_timeouts = MIN(64, state->consecutive_timeouts);
+	if (state->consecutive_timeouts >= KR_NS_TIMEOUT_ROW_DEAD) {
+		// Only mark as dead if we waited long enough,
+		// so that many (concurrent) short attempts can't cause the dead state.
+		if (transport->timeout >= KR_NS_TIMEOUT_MIN_DEAD_TIMEOUT)
+			state->dead_since = kr_now();
+	}
+
+	// If transport was chosen by a different query, that one will cache it.
+	if (!transport->deduplicated) {
+		put_rtt_state(address, transport->address_len, *state, cache);
 	} else {
-		/* `get_rtt_state` opens a cache transaction, we have to end it. */
-		kr_cache_commit(cache);
+		kr_cache_commit(cache); // Avoid any risk of long transaction.
 	}
 }
+// Not everything can be checked in nice ways like static_assert()
+static __attribute__((constructor)) void test_RTT_consts(void)
+{
+	// See KR_NS_TIMEOUT_MIN_DEAD_TIMEOUT above.
+	kr_require(
+		calc_timeout((struct rtt_state){ .consecutive_timeouts = MAX_BACKOFF, })
+		 >= KR_NS_TIMEOUT_MIN_DEAD_TIMEOUT
+	);
+}
 
 void error(struct kr_query *qry, struct address_state *addr_state,
 	   const struct kr_transport *transport,
@@ -629,12 +651,7 @@ void error(struct kr_query *qry, struct
 	case KR_SELECTION_TLS_HANDSHAKE_FAILED:
 	case KR_SELECTION_QUERY_TIMEOUT:
 		qry->server_selection.local_state->timeouts++;
-		/* Make sure that the query was chosen by this query and timeout wasn't capped
-		 * (see kr_transport::timeout_capped for details). */
-		if (!transport->deduplicated && !transport->timeout_capped) {
-			cache_timeout(qry, transport, addr_state,
-				      &qry->request->ctx->cache);
-		}
+		server_timeout(qry, transport, addr_state, &qry->request->ctx->cache);
 		break;
 	case KR_SELECTION_FORMERR:
 		if (qry->flags.NO_EDNS) {
@@ -751,7 +768,7 @@ int kr_forward_add_target(struct kr_requ
 		return kr_error(EINVAL);
 	}
 
-	union inaddr address;
+	union kr_sockaddr address;
 
 	switch (sock->sa_family) {
 	case AF_INET:
diff -pruN 5.4.4-1/lib/selection_forward.c 5.5.1-5/lib/selection_forward.c
--- 5.4.4-1/lib/selection_forward.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/selection_forward.c	2022-06-14 07:17:30.407490700 +0000
@@ -5,12 +5,17 @@
 #include "lib/selection_forward.h"
 #include "lib/resolve.h"
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE((qry), SELECTION, __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q((qry), SELECTION, __VA_ARGS__)
 
 #define FORWARDING_TIMEOUT 2000
+/* TODO: well, this is a bit hard; maybe we'd better have a different approach
+ * for estimating DEAD-ness for forwarding.
+ * Even ACKs on connections might be useful here. */
+static_assert(FORWARDING_TIMEOUT >= KR_NS_TIMEOUT_MIN_DEAD_TIMEOUT,
+		"Bad combination of NS selection limits.");
 
 struct forward_local_state {
-	inaddr_array_t *targets;
+	kr_sockaddr_array_t *targets;
 	struct address_state *addr_states;
 	/** Index of last choice in the targets array, used for error reporting. */
 	size_t last_choice_index;
@@ -38,7 +43,7 @@ void forward_choose_transport(struct kr_
 	int valid = 0;
 
 	for (int i = 0; i < local_state->targets->len; i++) {
-		union inaddr *address = &local_state->targets->at[i];
+		union kr_sockaddr *address = &local_state->targets->at[i];
 		size_t addr_len;
 		uint16_t port;
 		switch (address->ip.sa_family) {
diff -pruN 5.4.4-1/lib/selection.h 5.5.1-5/lib/selection.h
--- 5.4.4-1/lib/selection.h	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/selection.h	2022-06-14 07:17:30.407490700 +0000
@@ -12,8 +12,11 @@
 
 #include "lib/cache/api.h"
 
-/* After KR_NS_TIMEOUT_ROW_DEAD consecutive timeouts, we consider the upstream IP dead for KR_NS_TIMEOUT_RETRY_INTERVAL ms */
+/* After KR_NS_TIMEOUT_ROW_DEAD consecutive timeouts
+ * where at least one was over KR_NS_TIMEOUT_MIN_DEAD_TIMEOUT ms,
+ * we consider the upstream IP dead for KR_NS_TIMEOUT_RETRY_INTERVAL ms */
 #define KR_NS_TIMEOUT_ROW_DEAD 4
+#define KR_NS_TIMEOUT_MIN_DEAD_TIMEOUT 800 /* == DEFAULT_TIMEOUT * 2 */
 #define KR_NS_TIMEOUT_RETRY_INTERVAL 1000
 
 /**
@@ -66,7 +69,7 @@ enum kr_transport_protocol {
  */
 struct kr_transport {
 	knot_dname_t *ns_name; /**< Set to "." for forwarding targets.*/
-	union inaddr address;
+	union kr_sockaddr address;
 	size_t address_len;
 	enum kr_transport_protocol protocol;
 	unsigned timeout; /**< Timeout in ms to be set for UDP transmission. */
@@ -179,7 +182,7 @@ struct address_state {
  * @brief Array of these is one of inputs for the actual selection algorithm (`select_transport`)
  */
 struct choice {
-	union inaddr address;
+	union kr_sockaddr address;
 	size_t address_len;
 	struct address_state *address_state;
 	/** used to overwrite the port number;
@@ -210,8 +213,8 @@ struct to_resolve {
  * @param[out] choice_index Optionally index of the chosen transport in the @p choices array.
  * @return Chosen transport (on mempool) or NULL when no choice is viable
  */
-struct kr_transport *select_transport(struct choice choices[], int choices_len,
-				      struct to_resolve unresolved[],
+struct kr_transport *select_transport(const struct choice choices[], int choices_len,
+				      const struct to_resolve unresolved[],
 				      int unresolved_len, int timeouts,
 				      struct knot_mm *mempool, bool tcp,
 				      size_t *choice_index);
@@ -245,12 +248,12 @@ int put_rtt_state(const uint8_t *ip, siz
 /**
  * @internal Helper function for conversion between different IP representations.
  */
-void bytes_to_ip(uint8_t *bytes, size_t len, uint16_t port, union inaddr *dst);
+void bytes_to_ip(uint8_t *bytes, size_t len, uint16_t port, union kr_sockaddr *dst);
 
 /**
  * @internal Helper function for conversion between different IP representations.
  */
-uint8_t *ip_to_bytes(const union inaddr *src, size_t len);
+uint8_t *ip_to_bytes(const union kr_sockaddr *src, size_t len);
 
 /**
  * @internal Fetch per-address information from various sources.
@@ -258,5 +261,9 @@ uint8_t *ip_to_bytes(const union inaddr
  * Note that this opens a RO cache transaction; the callee is responsible
  * for its closing not too long afterwards (e.g. calling kr_cache_commit).
  */
-void update_address_state(struct address_state *state, union inaddr *address,
+void update_address_state(struct address_state *state, union kr_sockaddr *address,
 			  size_t address_len, struct kr_query *qry);
+
+/** @internal Return whether IPv6 is considered to be broken. */
+bool no6_is_bad(void);
+
diff -pruN 5.4.4-1/lib/selection_iter.c 5.5.1-5/lib/selection_iter.c
--- 5.4.4-1/lib/selection_iter.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/selection_iter.c	2022-06-14 07:17:30.407490700 +0000
@@ -10,7 +10,7 @@
 #include "lib/zonecut.h"
 #include "lib/resolve.h"
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE((qry), SELECTION, __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q((qry), SELECTION, __VA_ARGS__)
 
 /// To be held per query and locally.  Allocations are in the kr_request's mempool.
 struct iter_local_state {
@@ -121,7 +121,7 @@ static void unpack_state_from_zonecut(st
 			} else if (address_len == sizeof(struct in6_addr)) {
 				name_state->aaaa_state = RECORD_RESOLVED;
 			}
-			union inaddr tmp_address;
+			union kr_sockaddr tmp_address;
 			bytes_to_ip(address, address_len, 0, &tmp_address);
 			update_address_state(address_state, &tmp_address, address_len, qry);
 		}
@@ -249,10 +249,29 @@ void iter_choose_transport(struct kr_que
 	// Filter valid addresses and names from the tries
 	int choices_len = get_valid_addresses(local_state, choices);
 	int resolvable_len = get_resolvable_names(local_state, resolvable, qry);
+	bool * const force_resolve_p = &qry->server_selection.local_state->force_resolve;
 
-	if (qry->server_selection.local_state->force_resolve && resolvable_len) {
+	// Print some stats into debug logs.
+	if (kr_log_is_debug_qry(SELECTION, qry)) {
+		int v4_choices = 0;
+		for (int i = 0; i < choices_len; ++i)
+			if (choices[i].address.ip.sa_family == AF_INET)
+				++v4_choices;
+		int v4_resolvable = 0;
+		for (int i = 0; i < resolvable_len; ++i)
+			if (resolvable[i].type == KR_TRANSPORT_RESOLVE_A)
+				++v4_resolvable;
+		VERBOSE_MSG(qry, "=> id: '%05u' choosing from addresses: %d v4 + %d v6; "
+			"names to resolve: %d v4 + %d v6; "
+			"force_resolve: %d; NO6: IPv6 is %s\n",
+			qry->id, v4_choices, choices_len - v4_choices,
+			v4_resolvable, resolvable_len - v4_resolvable,
+			(int)*force_resolve_p, no6_is_bad() ? "KO" : "OK");
+	}
+
+	if (*force_resolve_p && resolvable_len) {
 		choices_len = 0;
-		qry->server_selection.local_state->force_resolve = false;
+		*force_resolve_p = false;
 	}
 
 	bool tcp = qry->flags.TCP || qry->server_selection.local_state->truncated;
diff -pruN 5.4.4-1/lib/test_module.c 5.5.1-5/lib/test_module.c
--- 5.4.4-1/lib/test_module.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/test_module.c	2022-06-14 07:17:30.407490700 +0000
@@ -7,7 +7,7 @@
 
 static void test_module_params(void **state)
 {
-	struct kr_module module;
+	struct kr_module module = { 0 };
 	assert_int_equal(kr_module_load(NULL, NULL, NULL), kr_error(EINVAL));
 	assert_int_equal(kr_module_load(&module, NULL, NULL), kr_error(EINVAL));
 	kr_module_unload(NULL);
@@ -15,14 +15,14 @@ static void test_module_params(void **st
 
 static void test_module_builtin(void **state)
 {
-	struct kr_module module;
+	struct kr_module module = { 0 };
 	assert_int_equal(kr_module_load(&module, "iterate", NULL), 0);
 	kr_module_unload(&module);
 }
 
 static void test_module_c(void **state)
 {
-	struct kr_module module;
+	struct kr_module module = { 0 };
 	assert_int_equal(kr_module_load(&module, "mock_cmodule", "tests/unit"), 0);
 	kr_module_unload(&module);
 }
diff -pruN 5.4.4-1/lib/test_rplan.c 5.5.1-5/lib/test_rplan.c
--- 5.4.4-1/lib/test_rplan.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/test_rplan.c	2022-06-14 07:17:30.407490700 +0000
@@ -28,7 +28,7 @@ static void test_rplan_params(void **sta
 
 static void test_rplan_push(void **state)
 {
-	knot_mm_t mm;
+	knot_mm_t mm = { 0 };
 	test_mm_ctx_init(&mm);
 	struct kr_request request = {
 		.pool = mm,
diff -pruN 5.4.4-1/lib/test_utils.c 5.5.1-5/lib/test_utils.c
--- 5.4.4-1/lib/test_utils.c	2022-01-05 13:19:14.463727700 +0000
+++ 5.5.1-5/lib/test_utils.c	2022-06-14 07:17:30.407490700 +0000
@@ -67,6 +67,41 @@ static void test_straddr(void **state)
 	assert_int_not_equal(test_bitcmp(ip6_sub, ip6_out, 4), 0);
 }
 
+static inline int assert_bitmask(const char *addr, const char *exp_masked)
+{
+	unsigned char addr_buf[16];
+	unsigned char exp_masked_buf[16];
+
+	int bits = kr_straddr_subnet(addr_buf, addr);
+	size_t addr_len = (kr_straddr_family(addr) == AF_INET6) ? 16 : 4;
+	int exp_masked_bits = kr_straddr_subnet(exp_masked_buf, exp_masked);
+	size_t exp_masked_len = (kr_straddr_family(exp_masked) == AF_INET6) ? 16 : 4;
+
+	/* sanity checks */
+	assert_true(bits >= 0);
+	assert_int_equal(addr_len, exp_masked_len);
+	assert_int_equal(exp_masked_bits, exp_masked_len * 8);
+
+	kr_bitmask(addr_buf, addr_len, bits);
+	return memcmp(addr_buf, exp_masked_buf, addr_len);
+}
+
+static void test_bitmask(void **state)
+{
+	assert_int_equal(assert_bitmask("10.0.1.5/32", "10.0.1.5"), 0);
+	assert_int_equal(assert_bitmask("10.0.1.5", "10.0.1.5"), 0);
+	assert_int_equal(assert_bitmask("10.0.1.5/24", "10.0.1.0"), 0);
+	assert_int_equal(assert_bitmask("128.30.1.16/16", "128.30.0.0"), 0);
+	assert_int_equal(assert_bitmask("255.255.255.255/20", "255.255.240.0"), 0);
+	assert_int_equal(assert_bitmask("255.255.255.255/22", "255.255.252.0"), 0);
+	assert_int_equal(assert_bitmask("192.168.0.1/0", "0.0.0.0"), 0);
+	assert_int_equal(assert_bitmask("7caa::/4", "7000::"), 0);
+	assert_int_equal(assert_bitmask("dead:beef::/16", "dead::"), 0);
+	assert_int_equal(assert_bitmask("dead:beef::/20", "dead:b000::"), 0);
+	assert_int_equal(assert_bitmask("dead:beef::/0", "::"), 0);
+	assert_int_equal(assert_bitmask("64aa:22fa:1378:aaaa:bbbb::/36", "64aa:22fa:1000::"), 0);
+}
+
 static void test_strptime_diff(void **state)
 {
 	char *format = "%Y-%m-%dT%H:%M:%S";
@@ -104,7 +139,8 @@ int main(void)
 	const UnitTest tests[] = {
 		unit_test(test_strcatdup),
 		unit_test(test_straddr),
-		unit_test(test_strptime_diff),
+		unit_test(test_bitmask),
+		unit_test(test_strptime_diff)
 	};
 
 	return run_tests(tests);
diff -pruN 5.4.4-1/lib/utils.c 5.5.1-5/lib/utils.c
--- 5.4.4-1/lib/utils.c	2022-01-05 13:19:14.467728000 +0000
+++ 5.5.1-5/lib/utils.c	2022-06-14 07:17:30.407490700 +0000
@@ -4,14 +4,12 @@
 
 #include "lib/utils.h"
 
-#include "contrib/ccan/asprintf/asprintf.h"
 #include "contrib/cleanup.h"
 #include "contrib/ucw/mempool.h"
 #include "kresconfig.h"
 #include "lib/defines.h"
 #include "lib/generic/array.h"
 #include "lib/module.h"
-#include "lib/selection.h"
 #include "lib/resolve.h"
 
 #include <libknot/descriptor.h>
@@ -31,6 +29,30 @@
 #include <sys/statvfs.h>
 #include <sys/un.h>
 
+struct __attribute__((packed)) kr_sockaddr_key {
+	int family;
+};
+
+struct __attribute__((packed)) kr_sockaddr_in_key {
+	int family;
+	char address[sizeof(((struct sockaddr_in *) NULL)->sin_addr)];
+	uint16_t port;
+};
+
+struct __attribute__((packed)) kr_sockaddr_in6_key {
+	int family;
+	char address[sizeof(((struct sockaddr_in6 *) NULL)->sin6_addr)];
+	uint32_t scope;
+	uint16_t port;
+};
+
+struct __attribute((packed)) kr_sockaddr_un_key {
+	int family;
+	char path[sizeof(((struct sockaddr_un *) NULL)->sun_path)];
+};
+
+extern inline uint64_t kr_rand_bytes(unsigned int size);
+
 /* Logging & debugging */
 bool kr_dbg_assertion_abort = DBG_ASSERTION_ABORT;
 int kr_dbg_assertion_fork = DBG_ASSERTION_FORK;
@@ -109,7 +131,7 @@ char* kr_strcatdup(unsigned n, ...)
 	/* Allocate result and fill */
 	char *result = NULL;
 	if (total_len > 0) {
-		if (unlikely(total_len + 1 == 0)) return NULL;
+		if (unlikely(total_len == SIZE_MAX)) return NULL;
 		result = malloc(total_len + 1);
 	}
 	if (result) {
@@ -284,6 +306,130 @@ int kr_sockaddr_len(const struct sockadd
 	}
 }
 
+ssize_t kr_sockaddr_key(struct kr_sockaddr_key_storage *dst,
+                        const struct sockaddr *addr)
+{
+	kr_require(addr);
+
+	switch (addr->sa_family) {
+	case AF_INET:;
+		const struct sockaddr_in *addr_in = (const struct sockaddr_in *) addr;
+		struct kr_sockaddr_in_key *inkey = (struct kr_sockaddr_in_key *) dst;
+		inkey->family = AF_INET;
+		memcpy(&inkey->address, &addr_in->sin_addr, sizeof(inkey->address));
+		memcpy(&inkey->port, &addr_in->sin_port, sizeof(inkey->port));
+		return sizeof(*inkey);
+
+	case AF_INET6:;
+		const struct sockaddr_in6 *addr_in6 = (const struct sockaddr_in6 *) addr;
+		struct kr_sockaddr_in6_key *in6key = (struct kr_sockaddr_in6_key *) dst;
+		in6key->family = AF_INET6;
+		memcpy(&in6key->address, &addr_in6->sin6_addr, sizeof(in6key->address));
+		memcpy(&in6key->port, &addr_in6->sin6_port, sizeof(in6key->port));
+		if (kr_sockaddr_link_local(addr))
+			memcpy(&in6key->scope, &addr_in6->sin6_scope_id, sizeof(in6key->scope));
+		else
+			in6key->scope = 0;
+		return sizeof(*in6key);
+
+	case AF_UNIX:;
+		const struct sockaddr_un *addr_un = (const struct sockaddr_un *) addr;
+		struct kr_sockaddr_un_key *unkey = (struct kr_sockaddr_un_key *) dst;
+		unkey->family = AF_UNIX;
+		size_t pathlen = strnlen(addr_un->sun_path, sizeof(unkey->path));
+		if (pathlen == 0 || pathlen >= sizeof(unkey->path)) {
+			/* Abstract sockets are not supported - we would need
+			 * to also supply a length value for the abstract
+			 * pathname.
+			 *
+			 * UNIX socket path should be null-terminated.
+			 *
+			 * See unix(7). */
+			return kr_error(EINVAL);
+		}
+
+		pathlen += 1; /* Include null-terminator */
+		strncpy(unkey->path, addr_un->sun_path, pathlen);
+		return offsetof(struct kr_sockaddr_un_key, path) + pathlen;
+
+	default:
+		return kr_error(EAFNOSUPPORT);
+	}
+}
+
+struct sockaddr *kr_sockaddr_from_key(struct sockaddr_storage *dst,
+                                      const char *key)
+{
+	kr_require(key);
+
+	switch (((struct kr_sockaddr_key *) key)->family) {
+	case AF_INET:;
+		const struct kr_sockaddr_in_key *inkey = (struct kr_sockaddr_in_key *) key;
+		struct sockaddr_in *addr_in = (struct sockaddr_in *) dst;
+		addr_in->sin_family = AF_INET;
+		memcpy(&addr_in->sin_addr, &inkey->address, sizeof(inkey->address));
+		memcpy(&addr_in->sin_port, &inkey->port, sizeof(inkey->port));
+		return (struct sockaddr *) addr_in;
+
+	case AF_INET6:;
+		const struct kr_sockaddr_in6_key *in6key = (struct kr_sockaddr_in6_key *) key;
+		struct sockaddr_in6 *addr_in6 = (struct sockaddr_in6 *) dst;
+		addr_in6->sin6_family = AF_INET6;
+		memcpy(&addr_in6->sin6_addr, &in6key->address, sizeof(in6key->address));
+		memcpy(&addr_in6->sin6_port, &in6key->port, sizeof(in6key->port));
+		memcpy(&addr_in6->sin6_scope_id, &in6key->scope, sizeof(in6key->scope));
+		return (struct sockaddr *) addr_in6;
+
+	case AF_UNIX:;
+		const struct kr_sockaddr_un_key *unkey = (struct kr_sockaddr_un_key *) key;
+		struct sockaddr_un *addr_un = (struct sockaddr_un *) dst;
+		addr_un->sun_family = AF_UNIX;
+		strncpy(addr_un->sun_path, unkey->path, sizeof(unkey->path));
+		return (struct sockaddr *) addr_un;
+
+	default:
+		kr_assert(false);
+		return NULL;
+	}
+}
+
+bool kr_sockaddr_key_same_addr(const char *key_a, const char *key_b)
+{
+	const struct kr_sockaddr_in6_key *kkey_a = (struct kr_sockaddr_in6_key *) key_a;
+	const struct kr_sockaddr_in6_key *kkey_b = (struct kr_sockaddr_in6_key *) key_b;
+
+	if (kkey_a->family != kkey_b->family)
+		return false;
+
+	ptrdiff_t offset;
+	switch (kkey_a->family) {
+		case AF_INET:
+			offset = offsetof(struct kr_sockaddr_in_key, address);
+			break;
+		case AF_INET6:
+			if (unlikely(kkey_a->scope != kkey_b->scope))
+				return false;
+			offset = offsetof(struct kr_sockaddr_in6_key, address);
+			break;
+
+		case AF_UNIX:;
+			const struct kr_sockaddr_un_key *unkey_a =
+				(struct kr_sockaddr_un_key *) key_a;
+			const struct kr_sockaddr_un_key *unkey_b =
+				(struct kr_sockaddr_un_key *) key_b;
+
+			return strncmp(unkey_a->path, unkey_b->path,
+			               sizeof(unkey_a->path)) == 0;
+
+		default:
+			kr_assert(false);
+			return false;
+	}
+
+	size_t len = kr_family_len(kkey_a->family);
+	return memcmp(key_a + offset, key_b + offset, len) == 0;
+}
+
 int kr_sockaddr_cmp(const struct sockaddr *left, const struct sockaddr *right)
 {
 	if (!left || !right) {
@@ -335,9 +481,14 @@ void kr_inaddr_set_port(struct sockaddr
 		return;
 	}
 	switch (addr->sa_family) {
-	case AF_INET:  ((struct sockaddr_in *)addr)->sin_port = htons(port);
-	case AF_INET6: ((struct sockaddr_in6 *)addr)->sin6_port = htons(port);
-	default: break;
+	case AF_INET:
+		((struct sockaddr_in *)addr)->sin_port = htons(port);
+		break;
+	case AF_INET6:
+		((struct sockaddr_in6 *)addr)->sin6_port = htons(port);
+		break;
+	default:
+		break;
 	}
 }
 
@@ -372,6 +523,19 @@ int kr_ntop_str(int family, const void *
 	return kr_ok();
 }
 
+char *kr_straddr(const struct sockaddr *addr)
+{
+	if (kr_fails_assert(addr)) return NULL;
+	static char str[KR_STRADDR_MAXLEN + 1] = {0};
+	if (addr->sa_family == AF_UNIX) {
+		strncpy(str, ((struct sockaddr_un *)addr)->sun_path, sizeof(str) - 1);
+		return str;
+	}
+	size_t len = KR_STRADDR_MAXLEN;
+	int ret = kr_inaddr_str(addr, str, &len);
+	return ret != kr_ok() || len == 0 ? NULL : str;
+}
+
 int kr_straddr_family(const char *addr)
 {
 	if (!addr) {
@@ -383,7 +547,10 @@ int kr_straddr_family(const char *addr)
 	if (strchr(addr, ':')) {
 		return AF_INET6;
 	}
-	return AF_INET;
+	if (strchr(addr, '.')) {
+		return AF_INET;
+	}
+	return kr_error(EINVAL);
 }
 
 int kr_family_len(int family)
@@ -428,7 +595,6 @@ struct sockaddr * kr_straddr_socket(cons
 		return (struct sockaddr *)res;
 	}
 	default:
-		kr_assert(false);
 		return NULL;
 	}
 }
@@ -443,6 +609,7 @@ int kr_straddr_subnet(void *dst, const c
 	int family = kr_straddr_family(addr);
 	if (family != AF_INET && family != AF_INET6)
 		return kr_error(EINVAL);
+	const int max_len = (family == AF_INET6) ? 128 : 32;
 	auto_free char *addr_str = strdup(addr);
 	char *subnet = strchr(addr_str, '/');
 	if (subnet) {
@@ -450,13 +617,12 @@ int kr_straddr_subnet(void *dst, const c
 		subnet += 1;
 		bit_len = strtol(subnet, NULL, 10);
 		/* Check client subnet length */
-		const int max_len = (family == AF_INET6) ? 128 : 32;
 		if (bit_len < 0 || bit_len > max_len) {
 			return kr_error(ERANGE);
 		}
 	} else {
 		/* No subnet, use maximal subnet length. */
-		bit_len = (family == AF_INET6) ? 128 : 32;
+		bit_len = max_len;
 	}
 	/* Parse address */
 	int ret = inet_pton(family, addr_str, dst);
@@ -552,6 +718,22 @@ int kr_bitcmp(const char *a, const char
 	return ret;
 }
 
+void kr_bitmask(unsigned char *a, size_t a_len, int bits)
+{
+	if (bits < 0 || !a || !a_len) {
+		return;
+	}
+
+	size_t i = bits / 8;
+	const size_t mid_bits = 8 - (bits % 8);
+	const unsigned char mask = 0xFF << mid_bits;
+	if (i < a_len)
+		a[i] &= mask;
+
+	for (++i; i < a_len; ++i)
+		a[i] = 0;
+}
+
 int kr_rrkey(char *key, uint16_t class, const knot_dname_t *owner,
 	     uint16_t type, uint16_t additional)
 {
@@ -713,7 +895,10 @@ int kr_ranked_rrarray_add(ranked_rr_arra
 		return kr_error(ENOMEM);
 	}
 	rr_new->rrs = rr->rrs;
-	if (kr_fails_assert(rr_new->additional == NULL)) return kr_error(EINVAL);
+	if (kr_fails_assert(rr_new->additional == NULL)) {
+		mm_free(pool, entry);
+		return kr_error(EINVAL);
+	}
 
 	entry->qry_uid = qry_uid;
 	entry->rr = rr_new;
@@ -770,7 +955,7 @@ int kr_ranked_rrarray_finalize(ranked_rr
 				if (knot_rdata_cmp(ra->at[i], ra->at[i + 1]) == 0) {
 					ra->at[i] = NULL;
 					++dup_count;
-					QRVERBOSE(NULL, ITERATOR, "deleted duplicate RR\n");
+					kr_log_q(NULL, ITERATOR, "deleted duplicate RR\n");
 				}
 			}
 			/* Prepare rdataset, except rdata contents. */
@@ -1150,7 +1335,11 @@ void kr_rrset_init(knot_rrset_t *rrset,
 	if (kr_fails_assert(rrset)) return;
 	knot_rrset_init(rrset, owner, type, rclass, ttl);
 }
-uint16_t kr_pkt_has_dnssec(const knot_pkt_t *pkt)
+bool kr_pkt_has_wire(const knot_pkt_t *pkt)
+{
+	return pkt->size != KR_PKT_SIZE_NOWIRE;
+}
+bool kr_pkt_has_dnssec(const knot_pkt_t *pkt)
 {
 	return knot_pkt_has_dnssec(pkt);
 }
diff -pruN 5.4.4-1/lib/utils.h 5.5.1-5/lib/utils.h
--- 5.4.4-1/lib/utils.h	2022-01-05 13:19:14.467728000 +0000
+++ 5.5.1-5/lib/utils.h	2022-06-14 07:17:30.407490700 +0000
@@ -10,6 +10,7 @@
 #include <stdbool.h>
 #include <sys/socket.h>
 #include <sys/time.h>
+#include <sys/un.h>
 #include <netinet/in.h>
 #include <unistd.h>
 
@@ -28,6 +29,19 @@
 #include "lib/generic/array.h"
 #include "lib/log.h"
 
+/** When knot_pkt is passed from cache without ->wire, this is the ->size. */
+static const size_t KR_PKT_SIZE_NOWIRE = -1;
+
+/** Maximum length (excluding null-terminator) of a presentation-form address
+ * returned by `kr_straddr`. */
+#define KR_STRADDR_MAXLEN 109
+
+/** Used for reserving enough space for the `kr_sockaddr_key` function
+ * output. */
+struct kr_sockaddr_key_storage {
+	char bytes[sizeof(struct sockaddr_storage)];
+};
+
 
 /*
  * Logging and debugging.
@@ -171,7 +185,8 @@ KR_EXPORT
 void kr_rnd_buffered(void *data, unsigned int size);
 
 /** Return a few random bytes. */
-static inline uint64_t kr_rand_bytes(unsigned int size)
+KR_EXPORT inline
+uint64_t kr_rand_bytes(unsigned int size)
 {
 	uint64_t result;
 	if (size <= 0 || size > sizeof(result)) {
@@ -233,13 +248,20 @@ int kr_pkt_put(knot_pkt_t *pkt, const kn
 KR_EXPORT
 void kr_pkt_make_auth_header(knot_pkt_t *pkt);
 
-/** Simple storage for IPx address or AF_UNSPEC. */
-union inaddr {
+/** Simple storage for IPx address and their ports or AF_UNSPEC. */
+union kr_sockaddr {
 	struct sockaddr ip;
 	struct sockaddr_in ip4;
 	struct sockaddr_in6 ip6;
 };
 
+/** Simple storage for IPx addresses. */
+union kr_in_addr {
+	struct in_addr ip4;
+	struct in6_addr ip6;
+};
+
+/* TODO: rename kr_inaddr functions to kr_sockaddr */
 /** Address bytes for given family. */
 KR_EXPORT KR_PURE
 const char *kr_inaddr(const struct sockaddr *addr);
@@ -252,6 +274,27 @@ int kr_inaddr_len(const struct sockaddr
 /** Sockaddr length for given family, i.e. sizeof(struct sockaddr_in*). */
 KR_EXPORT KR_PURE
 int kr_sockaddr_len(const struct sockaddr *addr);
+
+/** Creates a packed structure from the specified `addr`, safe for use as a key
+ * in containers like `trie_t`, and writes it into `dst`. On success, returns
+ * the actual length of the key.
+ *
+ * Returns `kr_error(EAFNOSUPPORT)` if the family of `addr` is unsupported. */
+KR_EXPORT
+ssize_t kr_sockaddr_key(struct kr_sockaddr_key_storage *dst,
+                        const struct sockaddr *addr);
+
+/** Creates a `struct sockaddr` from the specified `key` created using the
+ * `kr_sockaddr_key()` function. */
+KR_EXPORT
+struct sockaddr *kr_sockaddr_from_key(struct sockaddr_storage *dst,
+                                      const char *key);
+
+/** Checks whether the two keys represent the same address;
+ * does NOT compare the ports. */
+KR_EXPORT
+bool kr_sockaddr_key_same_addr(const char *key_a, const char *key_b);
+
 /** Compare two given sockaddr.
  * return 0 - addresses are equal, error code otherwise.
  */
@@ -278,18 +321,10 @@ KR_EXPORT
 int kr_ntop_str(int family, const void *src, uint16_t port, char *buf, size_t *buflen);
 
 /** @internal Create string representation addr#port.
- *  @return pointer to static string
+ *  @return pointer to a *static* string, i.e. each call will overwrite it
  */
-static inline char *kr_straddr(const struct sockaddr *addr)
-{
-	if (kr_fails_assert(addr)) return NULL;
-	/* We are the single-threaded application */
-	static char str[INET6_ADDRSTRLEN + 1 + 5 + 1];
-	size_t len = sizeof(str);
-	int ret = kr_inaddr_str(addr, str, &len);
-	return ret != kr_ok() || len == 0 ? NULL : str;
-}
-
+KR_EXPORT
+char *kr_straddr(const struct sockaddr *addr);
 
 /** Return address type for string. */
 KR_EXPORT KR_PURE
@@ -337,6 +372,25 @@ int kr_straddr_join(const char *addr, ui
 KR_EXPORT KR_PURE
 int kr_bitcmp(const char *a, const char *b, int bits);
 
+/** Masks bits. The specified number of bits in `a` from the left (network order)
+ * will remain their original value, while the rest will be set to zero.
+ * This is useful for storing network addresses in a trie. */
+KR_EXPORT
+void kr_bitmask(unsigned char *a, size_t a_len, int bits);
+
+/** Check whether `addr` points to an `AF_INET6` address and whether the address
+ * is link-local. */
+static inline bool kr_sockaddr_link_local(const struct sockaddr *addr)
+{
+	if (addr->sa_family != AF_INET6)
+		return false;
+
+	/* Link-local: https://tools.ietf.org/html/rfc4291#section-2.4 */
+	const uint8_t prefix[] = { 0xFE, 0x80 };
+	const struct sockaddr_in6 *ip6 = (const struct sockaddr_in6 *) addr;
+	return kr_bitcmp((char *) ip6->sin6_addr.s6_addr, (char *) prefix, 10) == 0;
+}
+
 /** @internal RR map flags. */
 static const uint8_t KEY_FLAG_RRSIG = 0x02;
 static inline uint8_t KEY_FLAG_RANK(const char *key)
@@ -531,7 +585,8 @@ const char *kr_strptime_diff(const char
 /* Trivial non-inline wrappers, to be used in lua. */
 KR_EXPORT void kr_rrset_init(knot_rrset_t *rrset, knot_dname_t *owner,
 				uint16_t type, uint16_t rclass, uint32_t ttl);
-KR_EXPORT uint16_t kr_pkt_has_dnssec(const knot_pkt_t *pkt);
+KR_EXPORT bool kr_pkt_has_wire(const knot_pkt_t *pkt);
+KR_EXPORT bool kr_pkt_has_dnssec(const knot_pkt_t *pkt);
 KR_EXPORT uint16_t kr_pkt_qclass(const knot_pkt_t *pkt);
 KR_EXPORT uint16_t kr_pkt_qtype(const knot_pkt_t *pkt);
 KR_EXPORT uint32_t kr_rrsig_sig_inception(const knot_rdata_t *rdata);
diff -pruN 5.4.4-1/lib/zonecut.c 5.5.1-5/lib/zonecut.c
--- 5.4.4-1/lib/zonecut.c	2022-01-05 13:19:14.467728000 +0000
+++ 5.5.1-5/lib/zonecut.c	2022-06-14 07:17:30.407490700 +0000
@@ -7,7 +7,6 @@
 #include "contrib/cleanup.h"
 #include "lib/defines.h"
 #include "lib/generic/pack.h"
-#include "lib/layer.h"
 #include "lib/resolve.h"
 #include "lib/rplan.h"
 
@@ -15,7 +14,7 @@
 #include <libknot/packet/wire.h>
 #include <libknot/rrtype/rdname.h>
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE(qry, ZCUT, __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q(qry, ZCUT, __VA_ARGS__)
 
 /** Information for one NS name + address type. */
 typedef enum {
@@ -87,7 +86,7 @@ void kr_zonecut_deinit(struct kr_zonecut
 
 void kr_zonecut_move(struct kr_zonecut *to, const struct kr_zonecut *from)
 {
-	if (!to || !from) abort();
+	kr_require(to && from);
 	kr_zonecut_deinit(to);
 	memcpy(to, from, sizeof(*to));
 }
@@ -322,7 +321,7 @@ static addrset_info_t fetch_addr(pack_t
 		- cached_rr.rrs.count * offsetof(knot_rdata_t, len);
 	int ret = pack_reserve_mm(*addrs, cached_rr.rrs.count, pack_extra_size,
 				  kr_memreserve, mm_pool);
-	if (ret) abort(); /* ENOMEM "probably" */
+	kr_require(ret == 0); /* ENOMEM "probably" */
 
 	int usable_cnt = 0;
 	addrset_info_t result = AI_EMPTY;
diff -pruN 5.4.4-1/.mailmap 5.5.1-5/.mailmap
--- 5.4.4-1/.mailmap	2022-01-05 13:19:14.403727000 +0000
+++ 5.5.1-5/.mailmap	2022-06-14 07:17:30.359490200 +0000
@@ -1,4 +1,4 @@
-Aleš Mrázek <ales.mrazek@nic.cz> Ales Mrazek <ales.mrazek@nic.cz>
+Aleš Mrázek <ales.mrazek@nic.cz>
 Alex Forster <aforster@cloudflare.com>
 Ali Asad Lotia <ali.asad.lotia@gmail.com>
 Anbang Wen <anbang@cloudflare.com> <xofyarg@gmail.com>
diff -pruN 5.4.4-1/meson.build 5.5.1-5/meson.build
--- 5.4.4-1/meson.build	2022-01-05 13:19:14.467728000 +0000
+++ 5.5.1-5/meson.build	2022-06-14 07:17:30.407490700 +0000
@@ -4,7 +4,7 @@ project(
   'knot-resolver',
   ['c', 'cpp'],
   license: 'GPLv3+',
-  version: '5.4.4',
+  version: '5.5.1',
   default_options: ['c_std=gnu11', 'b_ndebug=true'],
   meson_version: '>=0.49',
 )
@@ -18,7 +18,7 @@ endif
 
 
 message('--- required dependencies ---')
-knot_version = '>=2.9'
+knot_version = '>=3.0.2'
 libknot = dependency('libknot', version: knot_version)
 libdnssec = dependency('libdnssec', version: knot_version)
 libzscanner = dependency('libzscanner', version: knot_version)
@@ -110,8 +110,7 @@ else
 endif
 
 ### XDP: not configurable - we just check if libknot supports it
-xdp = meson.get_compiler('c').has_header('libknot/xdp/xdp.h'
-    ) and libknot.version().version_compare('>= 3.0.2')
+xdp = meson.get_compiler('c').has_header('libknot/xdp/xdp.h')
 
 ### Systemd
 systemd_files = get_option('systemd_files')
diff -pruN 5.4.4-1/modules/cookies/cookiemonster.c 5.5.1-5/modules/cookies/cookiemonster.c
--- 5.4.4-1/modules/cookies/cookiemonster.c	2022-01-05 13:19:14.467728000 +0000
+++ 5.5.1-5/modules/cookies/cookiemonster.c	2022-06-14 07:17:30.411491000 +0000
@@ -20,7 +20,7 @@
 #include "lib/rplan.h"
 #include "modules/cookies/cookiemonster.h"
 
-#define VERBOSE_MSG(qry, ...) QRVERBOSE(qry, COOKIES, __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q(qry, COOKIES, __VA_ARGS__)
 
 /**
  * Obtain address from query/response context if if can be obtained.
diff -pruN 5.4.4-1/modules/dns64/dns64.lua 5.5.1-5/modules/dns64/dns64.lua
--- 5.4.4-1/modules/dns64/dns64.lua	2022-01-05 13:19:14.471727800 +0000
+++ 5.5.1-5/modules/dns64/dns64.lua	2022-06-14 07:17:30.411491000 +0000
@@ -1,5 +1,6 @@
 -- SPDX-License-Identifier: GPL-3.0-or-later
 -- Module interface
+local kres = require('kres')
 local ffi = require('ffi')
 local C = ffi.C
 local M = { layer = { } }
@@ -150,7 +151,8 @@ function M.layer.consume(state, req, pkt
 				req.pool)
 		end
 	end
-	ffi.C.kr_ranked_rrarray_finalize(req.answ_selected, qry.uid, req.pool);
+	ffi.C.kr_ranked_rrarray_finalize(req.answ_selected, qry.uid, req.pool)
+	req:set_extended_error(kres.extended_error.FORGED, "BHD4: DNS64 synthesis")
 end
 
 local function hexchar2int(char)
@@ -175,7 +177,7 @@ function M.layer.produce(_, req, pkt)
 		then return end
 	-- Update packet question if it was minimized.
 	qry.flags.NO_MINIMIZE = true
-	if not ffi.C.knot_dname_is_equal(pkt.wire + 12, sname) then
+	if not ffi.C.knot_dname_is_equal(pkt.wire + 12, sname) or not pkt:has_wire() then
 		if not pkt:recycle() or not pkt:question(sname, qry.sclass, qry.stype)
 			then return end
 	end
diff -pruN 5.4.4-1/modules/dnstap/dnstap.c 5.5.1-5/modules/dnstap/dnstap.c
--- 5.4.4-1/modules/dnstap/dnstap.c	2022-01-05 13:19:14.471727800 +0000
+++ 5.5.1-5/modules/dnstap/dnstap.c	2022-06-14 07:17:30.411491000 +0000
@@ -22,6 +22,7 @@
 #include <uv.h>
 
 #define DEBUG_MSG(fmt, ...) kr_log_debug(DNSTAP, fmt, ##__VA_ARGS__);
+#define ERROR_MSG(fmt, ...) kr_log_error(DNSTAP, fmt, ##__VA_ARGS__);
 #define CFG_SOCK_PATH "socket_path"
 #define CFG_IDENTITY_STRING "identity"
 #define CFG_VERSION_STRING "version"
@@ -404,17 +405,18 @@ static bool find_bool(const JsonNode *no
 KR_EXPORT
 int dnstap_config(struct kr_module *module, const char *conf) {
 	dnstap_clear(module);
+	if (!conf) return kr_ok(); /* loaded module without configuring */
 	struct dnstap_data *data = module->data;
 	auto_free char *sock_path = NULL;
 
 	/* Empty conf passed, set default */
-	if (!conf || strlen(conf) < 1) {
+	if (strlen(conf) < 1) {
 		sock_path = strdup(DEFAULT_SOCK_PATH);
 	} else {
 
 		JsonNode *root_node = json_decode(conf);
 		if (!root_node) {
-			DEBUG_MSG("error parsing json\n");
+			ERROR_MSG("error parsing json\n");
 			return kr_error(EINVAL);
 		}
 
@@ -483,13 +485,15 @@ int dnstap_config(struct kr_module *modu
 	DEBUG_MSG("opening sock file %s\n",sock_path);
 	struct fstrm_writer *writer = dnstap_unix_writer(sock_path);
 	if (!writer) {
-		DEBUG_MSG("can't create unix writer\n");
+		ERROR_MSG("failed to open socket %s\n"
+			"Please ensure that it exists beforehand and has appropriate access permissions.\n",
+			sock_path);
 		return kr_error(EINVAL);
 	}
 
 	struct fstrm_iothr_options *opt = fstrm_iothr_options_init();
 	if (!opt) {
-		DEBUG_MSG("can't init fstrm options\n");
+		ERROR_MSG("can't init fstrm options\n");
 		fstrm_writer_destroy(&writer);
 		return kr_error(EINVAL);
 	}
@@ -498,7 +502,7 @@ int dnstap_config(struct kr_module *modu
 	data->iothread = fstrm_iothr_init(opt, &writer);
 	fstrm_iothr_options_destroy(&opt);
 	if (!data->iothread) {
-		DEBUG_MSG("can't init fstrm_iothr\n");
+		ERROR_MSG("can't init fstrm_iothr\n");
 		fstrm_writer_destroy(&writer);
 		return kr_error(ENOMEM);
 	}
@@ -509,7 +513,7 @@ int dnstap_config(struct kr_module *modu
 	data->ioq = fstrm_iothr_get_input_queue_idx(data->iothread, 0);
 	if (!data->ioq) {
 		fstrm_iothr_destroy(&data->iothread);
-		DEBUG_MSG("can't get fstrm queue\n");
+		ERROR_MSG("can't get fstrm queue\n");
 		return kr_error(EBUSY);
 	}
 
diff -pruN 5.4.4-1/modules/dnstap/README.rst 5.5.1-5/modules/dnstap/README.rst
--- 5.4.4-1/modules/dnstap/README.rst	2022-01-05 13:19:14.471727800 +0000
+++ 5.5.1-5/modules/dnstap/README.rst	2022-06-14 07:17:30.411491000 +0000
@@ -10,6 +10,8 @@ socket in `dnstap format <https://dnstap
 This logging is useful if you need effectively log all DNS traffic.
 
 The unix socket and the socket reader must be present before starting resolver instances.
+Also it needs appropriate filesystem permissions;
+the typical user and group of the daemon are called ``knot-resolver``.
 
 Tunables:
 
diff -pruN 5.4.4-1/modules/extended_error/extended_error.c 5.5.1-5/modules/extended_error/extended_error.c
--- 5.4.4-1/modules/extended_error/extended_error.c	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/modules/extended_error/extended_error.c	2022-06-14 07:17:30.415490900 +0000
@@ -0,0 +1,47 @@
+#include <libknot/rrtype/opt.h>
+
+#include "lib/module.h"
+#include "daemon/engine.h"
+
+static int extended_error_finalize(kr_layer_t *ctx) {
+	struct kr_request *req = ctx->req;
+	const knot_rrset_t *src_opt = req->qsource.packet->opt_rr;
+	const struct kr_extended_error *ede = &req->extended_error;
+
+	if (ede->info_code == KNOT_EDNS_EDE_NONE  /* no extended error */
+	    || src_opt == NULL  /* no EDNS in query */
+	    || kr_fails_assert(ede->info_code >= 0 && ede->info_code < UINT16_MAX)  /* info code out of range */
+	    || kr_fails_assert(req->answer->opt_rr)  /* sanity check - answer should have EDNS */
+	    ) {
+		return ctx->state;
+	}
+
+	const uint16_t info_code = (uint16_t)ede->info_code;
+	const size_t extra_len = ede->extra_text ? strlen(ede->extra_text) : 0;
+	uint8_t buf[sizeof(info_code) + extra_len];
+	knot_wire_write_u16(buf, info_code);
+	if (extra_len)
+		memcpy(buf + sizeof(info_code), ede->extra_text, extra_len);
+
+	if (knot_edns_add_option(req->answer->opt_rr, KNOT_EDNS_OPTION_EDE,
+				 sizeof(buf), buf, &req->pool) != KNOT_EOK) {
+		/* something went wrong and there is no way to salvage content of OPT RRset */
+		kr_log_req(req, 0, 0, EDE, "unable to add Extended Error option\n");
+		knot_rrset_clear(req->answer->opt_rr, &req->pool);
+	}
+
+	return ctx->state;
+}
+
+KR_EXPORT
+int extended_error_init(struct kr_module *module) {
+	static kr_layer_api_t layer = {
+		.answer_finalize = &extended_error_finalize,
+	};
+	layer.data = module;
+	module->layer = &layer;
+
+	return kr_ok();
+}
+
+KR_MODULE_EXPORT(extended_error)
diff -pruN 5.4.4-1/modules/extended_error/meson.build 5.5.1-5/modules/extended_error/meson.build
--- 5.4.4-1/modules/extended_error/meson.build	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/modules/extended_error/meson.build	2022-06-14 07:17:30.415490900 +0000
@@ -0,0 +1,20 @@
+# C module: extended_error
+# SPDX-License-Identifier: GPL-3.0-or-later
+
+extended_error_src = files([
+  'extended_error.c',
+])
+c_src_lint += extended_error_src
+
+extended_error_mod = shared_module(
+  'extended_error',
+  extended_error_src,
+  dependencies: [
+    libknot,
+    luajit_inc,
+  ],
+  include_directories: mod_inc_dir,
+  name_prefix: '',
+  install: true,
+  install_dir: modules_dir,
+)
diff -pruN 5.4.4-1/modules/hints/hints.c 5.5.1-5/modules/hints/hints.c
--- 5.4.4-1/modules/hints/hints.c	2022-01-05 13:19:14.479728000 +0000
+++ 5.5.1-5/modules/hints/hints.c	2022-06-14 07:17:30.415490900 +0000
@@ -25,7 +25,7 @@
 #include <math.h>
 
 /* Defaults */
-#define VERBOSE_MSG(qry, ...) QRVERBOSE(qry, HINT,  __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q(qry, HINT,  __VA_ARGS__)
 #define ERR_MSG(...) kr_log_error(HINT, "[     ]" __VA_ARGS__)
 
 struct hints_data {
@@ -169,7 +169,7 @@ static int query(kr_layer_t *ctx, knot_p
 	return KR_STATE_DONE;
 }
 
-static int parse_addr_str(union inaddr *sa, const char *addr)
+static int parse_addr_str(union kr_sockaddr *sa, const char *addr)
 {
 	int family = strchr(addr, ':') ? AF_INET6 : AF_INET;
 	memset(sa, 0, sizeof(*sa));
@@ -220,7 +220,7 @@ static const knot_dname_t * raw_addr2rev
 static const knot_dname_t * addr2reverse(const char *addr)
 {
 	/* Parse address string */
-	union inaddr ia;
+	union kr_sockaddr ia;
 	if (parse_addr_str(&ia, addr) != 0) {
 		return NULL;
 	}
@@ -237,7 +237,7 @@ static int add_pair(struct kr_zonecut *h
 	}
 	knot_dname_to_lower(key);
 
-	union inaddr ia;
+	union kr_sockaddr ia;
 	if (parse_addr_str(&ia, addr) != 0) {
 		return kr_error(EINVAL);
 	}
@@ -276,7 +276,7 @@ static int del_pair(struct hints_data *d
 
         if (addr) {
 		/* Remove the pair. */
-		union inaddr ia;
+		union kr_sockaddr ia;
 		if (parse_addr_str(&ia, addr) != 0) {
 			return kr_error(EINVAL);
 		}
diff -pruN 5.4.4-1/modules/hints/README.rst 5.5.1-5/modules/hints/README.rst
--- 5.4.4-1/modules/hints/README.rst	2022-01-05 13:19:14.479728000 +0000
+++ 5.5.1-5/modules/hints/README.rst	2022-06-14 07:17:30.415490900 +0000
@@ -32,8 +32,26 @@ Examples
     -- Add a custom hint
     hints['foo.bar'] = '127.0.0.1'
 
-.. note:: The :ref:`policy <mod-policy>` module applies before hints, meaning e.g. that hints for special names (:rfc:`6761#section-6`) like ``localhost`` or ``test`` will get shadowed by policy rules by default.
-    That can be worked around e.g. by explicit :any:`policy.PASS` action.
+.. note::
+   The :ref:`policy <mod-policy>` module applies before hints,
+   so your hints might get surprisingly shadowed by even default policies.
+
+   That most often happens for :rfc:`6761#section-6` names, e.g.
+   ``localhost`` and ``test`` or with ``PTR`` records in private address ranges.
+   To unblock the required names, you may use an explicit :any:`policy.PASS` action.
+
+   .. code-block:: lua
+
+      policy.add(policy.suffix(policy.PASS, {todname('1.168.192.in-addr.arpa')}))
+
+   This ``.PASS`` workaround isn't ideal.  To improve some cases,
+   we recommend to move these ``.PASS`` lines to the end of your rule list.
+   The point is that applying any :ref:`non-chain action <mod-policy-actions>`
+   (e.g. :ref:`forwarding actions <forwarding>` or ``.PASS`` itself)
+   stops processing *any* later policy rules for that request (including the default block-rules).
+   You probably don't want this ``.PASS`` to shadow any other rules you might have;
+   and on the other hand, if any other non-chain rule triggers,
+   additional ``.PASS`` would not change anything even if it were somehow force-executed.
 
 Properties
 ----------
diff -pruN 5.4.4-1/modules/meson.build 5.5.1-5/modules/meson.build
--- 5.4.4-1/modules/meson.build	2022-01-05 13:19:14.491728300 +0000
+++ 5.5.1-5/modules/meson.build	2022-06-14 07:17:30.427491200 +0000
@@ -30,8 +30,6 @@ config_tests += [
 integr_tests += [
   ['rebinding', meson.current_source_dir() / 'rebinding' / 'test.integr'],
   ['serve_stale', meson.current_source_dir() / 'serve_stale' / 'test.integr'],
-  # NOTE: ta_update may pass in cases when it should fail due to race conditions
-  # To ensure reliability, deckard should introduce a time wait
 ]
 
 
@@ -42,6 +40,7 @@ subdir('daf')
 subdir('dnstap')
 subdir('edns_keepalive')
 subdir('experimental_dot_auth')
+subdir('extended_error')
 subdir('hints')
 subdir('http')
 subdir('nsid')
diff -pruN 5.4.4-1/modules/policy/meson.build 5.5.1-5/modules/policy/meson.build
--- 5.4.4-1/modules/policy/meson.build	2022-01-05 13:19:14.491728300 +0000
+++ 5.5.1-5/modules/policy/meson.build	2022-06-14 07:17:30.427491200 +0000
@@ -19,7 +19,8 @@ integr_tests += [
 
 # check git submodules were initialized
 lua_ac_submodule = run_command(['test', '-r',
-  '@0@/lua-aho-corasick/ac_fast.cxx'.format(meson.current_source_dir())])
+  '@0@/lua-aho-corasick/ac_fast.cxx'.format(meson.current_source_dir())],
+  check: false)
 if lua_ac_submodule.returncode() != 0
   error('run "git submodule update --init --recursive" to initialize git submodules')
 endif
diff -pruN 5.4.4-1/modules/policy/policy.lua 5.5.1-5/modules/policy/policy.lua
--- 5.4.4-1/modules/policy/policy.lua	2022-01-05 13:19:14.491728300 +0000
+++ 5.5.1-5/modules/policy/policy.lua	2022-06-14 07:17:30.427491200 +0000
@@ -57,6 +57,17 @@ local function addr2sock(target, default
 	return sock
 end
 
+-- Debug logging for taken policy actions
+local function log_policy_action(req, name)
+	if ffi.C.kr_log_is_debug_fun(ffi.C.LOG_GRP_POLICY, req) then
+		local qry = req:current()
+		ffi.C.kr_log_req1(
+			req, qry.uid, 2, ffi.C.LOG_GRP_POLICY, LOG_GRP_POLICY_TAG,
+			"%s applied for %s %s\n",
+			name, kres.dname2str(qry.sname), kres.tostring.type[qry.stype])
+	end
+end
+
 -- policy functions are defined below
 local policy = {}
 
@@ -236,6 +247,7 @@ function policy.ANSWER(rtable, nodata)
 		ffi.C.kr_pkt_make_auth_header(answer)
 		local ttl = (data or {}).ttl or 1
 		answer:rcode(kres.rcode.NOERROR)
+		req:set_extended_error(kres.extended_error.FORGED, "5DO5")
 
 		if data == nil then -- want NODATA, i.e. just a SOA
 			answer:begin(kres.section.AUTHORITY)
@@ -246,6 +258,7 @@ function policy.ANSWER(rtable, nodata)
 			else
 				mkauth_soa(answer, kres.dname2wire(qry.sname), nil, ttl)
 			end
+			log_policy_action(req, 'ANSWER (nodata)')
 		else
 			answer:begin(kres.section.ANSWER)
 			if type(data.rdata) == 'table' then
@@ -255,6 +268,7 @@ function policy.ANSWER(rtable, nodata)
 			else
 				answer:put(qry.sname, ttl, qry.sclass, qry.stype, data.rdata)
 			end
+			log_policy_action(req, 'ANSWER (forged)')
 		end
 		return kres.DONE
 	end
@@ -664,10 +678,14 @@ local function answer_clear(req)
 	return pkt
 end
 
-function policy.DENY_MSG(msg)
+function policy.DENY_MSG(msg, extended_error)
 	if msg and (type(msg) ~= 'string' or #msg >= 255) then
 		error('DENY_MSG: optional msg must be string shorter than 256 characters')
         end
+	if extended_error == nil then
+		extended_error = kres.extended_error.BLOCKED
+	end
+	local action_name = msg and 'DENY_MSG' or 'DENY'
 
 	return function (_, req)
 		-- Write authority information
@@ -683,6 +701,8 @@ function policy.DENY_MSG(msg)
 				   string.char(#msg) .. msg)
 
 		end
+		req:set_extended_error(extended_error, "CR36")
+		log_policy_action(req, action_name)
 		return kres.DONE
 	end
 end
@@ -728,6 +748,24 @@ function policy.REQTRACE(_, req)
 	log_notrace(req, 'request packet:\n%s', req.qsource.packet)
 end
 
+-- log how the request arrived, notably the client's IP
+function policy.IPTRACE(_, req)
+	if req.qsource.addr == nil then
+		log_notrace(req, 'request packet arrived internally\n')
+	else
+		-- stringify transport flags: struct kr_request_qsource_flags
+		local qf = req.qsource.flags
+		local qf_str = qf.tcp and 'TCP' or 'UDP'
+		if qf.tls  then qf_str = qf_str .. ' + TLS'  end
+		if qf.http then qf_str = qf_str .. ' + HTTP' end
+		if qf.xdp  then qf_str = qf_str .. ' + XDP'  end
+
+		log_notrace(req, 'request packet arrived from %s to %s (%s)\n',
+			req.qsource.addr, req.qsource.dst_addr, qf_str)
+	end
+	return nil -- chain rule
+end
+
 function policy.DEBUG_ALWAYS(state, req)
 	policy.QTRACE(state, req)
 	req:trace_chain_callbacks(debug_logline_cb, debug_logfinish_cb)
@@ -780,6 +818,14 @@ policy.DENY = policy.DENY_MSG() -- compa
 function policy.DROP(_, req)
 	local answer = answer_clear(req)
 	if answer == nil then return nil end
+	req:set_extended_error(kres.extended_error.PROHIBITED, "U5KL")
+	log_policy_action(req, 'DROP')
+	return kres.FAIL
+end
+
+function policy.NO_ANSWER(_, req)
+	req.options.NO_ANSWER = true
+	log_policy_action(req, 'NO_ANSWER')
 	return kres.FAIL
 end
 
@@ -788,6 +834,8 @@ function policy.REFUSE(_, req)
 	if answer == nil then return nil end
 	answer:rcode(kres.rcode.REFUSED)
 	answer:ad(false)
+	req:set_extended_error(kres.extended_error.PROHIBITED, "EIM4")
+	log_policy_action(req, 'REFUSE')
 	return kres.DONE
 end
 
@@ -801,6 +849,7 @@ function policy.TC(state, req)
 	if answer == nil then return nil end
 	answer:tc(1)
 	answer:ad(false)
+	log_policy_action(req, 'TC')
 	return kres.DONE
 end
 
@@ -990,7 +1039,8 @@ policy.special_names = {
 		cb=policy.suffix_common(policy.DENY_MSG(
 			'Blocking is mandated by standards, see references on '
 			.. 'https://www.iana.org/assignments/'
-			.. 'locally-served-dns-zones/locally-served-dns-zones.xhtml'),
+			.. 'locally-served-dns-zones/locally-served-dns-zones.xhtml',
+			kres.extended_error.NOTSUP),
 			private_zones, todname('arpa.')),
 		count=0
 	},
@@ -998,7 +1048,8 @@ policy.special_names = {
 		cb=policy.suffix(policy.DENY_MSG(
 			'Blocking is mandated by standards, see references on '
 			.. 'https://www.iana.org/assignments/'
-			.. 'special-use-domain-names/special-use-domain-names.xhtml'),
+			.. 'special-use-domain-names/special-use-domain-names.xhtml',
+			kres.extended_error.NOTSUP),
 			{
 				todname('test.'),
 				todname('onion.'),
diff -pruN 5.4.4-1/modules/policy/README.rst 5.5.1-5/modules/policy/README.rst
--- 5.4.4-1/modules/policy/README.rst	2022-01-05 13:19:14.491728300 +0000
+++ 5.5.1-5/modules/policy/README.rst	2022-06-14 07:17:30.427491200 +0000
@@ -36,12 +36,21 @@ A *filter* selects which queries will be
 
    Applies the action if query name suffix matches one of suffixes in the table (useful for "is domain in zone" rules).
 
-.. note:: For speed this filter requires domain names in DNS wire format, not textual representation, so each label in the name must be prefixed with its length. Always use convenience function :func:`policy.todnames` for automatic conversion from strings! For example:
-
    .. code-block:: lua
 
       policy.add(policy.suffix(policy.DENY, policy.todnames({'example.com', 'example.net'})))
 
+.. note:: For speed this filter requires domain names in DNS wire format, not textual representation, so each label in the name must be prefixed with its length. Always use convenience function :func:`policy.todnames` for automatic conversion from strings! For example:
+
+.. _IDN:
+
+.. note:: Non-ASCII is not supported.
+
+   Knot Resolver does not provide any convenience support for IDN.
+   Therefore everywhere (all configuration, logs, RPZ files) you need to deal with the
+   `xn\-\- forms <https://en.wikipedia.org/wiki/Internationalized_domain_name#Example_of_IDNA_encoding>`_
+   of domain name labels, instead of directly using unicode characters.
+
 .. function:: domains(action, domain_table)
 
    Like :func:`policy.suffix` match, but the queried name must match exactly, not just its suffix.
@@ -123,9 +132,18 @@ Following actions stop the policy matchi
 
    Deny existence of names matching filter, i.e. reply NXDOMAIN authoritatively.
 
-.. function:: DENY_MSG(message)
+.. function:: DENY_MSG(message, [extended_error=kres.extended_error.BLOCKED])
 
-   Deny existence of a given domain and add explanatory message. NXDOMAIN reply contains an additional explanatory message as TXT record in the additional section.
+   Deny existence of a given domain and add explanatory message. NXDOMAIN reply
+   contains an additional explanatory message as TXT record in the additional
+   section.
+
+   You may override the extended DNS error to provide the user with more
+   information. By default, ``BLOCKED`` is returned to indicate the domain is
+   blocked due to the internal policy of the operator. Other suitable error
+   codes are ``CENSORED`` (for externally imposed policy reasons) or
+   ``FILTERED`` (for blocking requested by the client). For more information,
+   please refer to :rfc:`8914`.
 
 .. py:attribute:: DROP
 
@@ -135,6 +153,17 @@ Following actions stop the policy matchi
 
    Terminate query resolution and return REFUSED to the requestor.
 
+.. py:attribute:: NO_ANSWER
+
+   Terminate query resolution and do not return any answer to the requestor.
+
+   .. warning:: During normal operation, an answer should always be returned.
+      Deliberate query drops are indistinguishable from packet loss and may
+      cause problems as described in :rfc:`8906`. Only use :any:`NO_ANSWER`
+      on very specific occasions, e.g. as a defense mechanism during an attack,
+      and prefer other actions (e.g. :any:`DROP` or :any:`REFUSE`) for normal
+      operation.
+
 .. py:attribute:: TC
 
    Force requestor to use TCP. It sets truncated bit (*TC*) in response to true if the request came through UDP, which will force standard-compliant clients to retry the request over TCP.
@@ -292,6 +321,24 @@ They are marked as ``debug`` level, so e
    It makes most sense together with :ref:`mod-view` (enabling per-client)
    and probably with verbose logging those request (e.g. use :any:`DEBUG_ALWAYS` instead).
 
+.. py:attribute:: IPTRACE
+
+   Log how the request arrived.
+   Most notably, this includes the client's IP address, so beware of privacy implications.
+
+   .. code-block:: lua
+
+        -- example usage in configuration
+        policy.add(policy.all(policy.IPTRACE))
+        -- you might want to combine it with some other logs, e.g.
+        policy.add(policy.all(policy.DEBUG_ALWAYS))
+
+   .. code-block:: text
+
+        -- example log lines from IPTRACE:
+        [reqdbg][policy][57517.00] request packet arrived from ::1#37931 to ::1#00853 (TCP + TLS)
+        [reqdbg][policy][65538.00] request packet arrived internally
+
 
 Custom actions
 ^^^^^^^^^^^^^^
@@ -380,6 +427,20 @@ Actions :func:`policy.FORWARD`, :func:`p
    `0x20 randomization <https://tools.ietf.org/html/draft-vixie-dnsext-dns0x20-00>`_.
    See example in `Replacing part of the DNS tree`_.
 
+.. warning::
+   Limiting forwarding actions by filters (e.g. :func:`policy.suffix`) may have unexpected consequences.
+   Notably, forwarders can inject *any* records into your cache
+   even if you "restrict" them to an insignificant DNS subtree --
+   except in cases where DNSSEC validation applies, of course.
+
+   The behavior is probably best understood through the fact
+   that filters and actions are completely decoupled.
+   The forwarding actions have no clue about why they were executed,
+   e.g. that the user wanted to restrict the forwarder only to some subtree.
+   The action just selects some set of forwarders to process this whole request from the client,
+   and during that processing it might need some other "sub-queries" (e.g. for validation).
+   Some of those might not've passed the intended filter,
+   but policy rule-set only applies once per client's request.
 
 .. _tls-forwarding:
 
@@ -631,6 +692,17 @@ Response policy zones
   .. [#] Our :any:`policy.DROP` returns *SERVFAIL* answer (for historical reasons).
 
 
+  .. note::
+
+     To debug which domains are affected by RPZ (or other policy actions), you can enable the ``policy`` log group:
+
+     .. code-block:: lua
+
+        log_groups({'policy'})
+
+     See also :ref:`non-ASCII support note <IDN>`.
+
+
 .. function:: rpz(action, path, [watch = true])
 
   :param action: the default action for match in the zone; typically you want :any:`policy.DENY`
diff -pruN 5.4.4-1/modules/predict/README.rst 5.5.1-5/modules/predict/README.rst
--- 5.4.4-1/modules/predict/README.rst	2022-01-05 13:19:14.491728300 +0000
+++ 5.5.1-5/modules/predict/README.rst	2022-06-14 07:17:30.427491200 +0000
@@ -22,7 +22,7 @@ Prediction
 ----------
 
 The predict module can also learn usage patterns and repetitive queries,
-though this mechanism is basically a prototype.
+though this mechanism is a prototype and **not recommended** for use in production or with high traffic.
 
 For example, if it makes a query every day at 18:00,
 the resolver expects that it is needed by that time and prefetches it ahead of time.
@@ -41,13 +41,12 @@ Example configuration
 
 	modules = {
 		predict = {
+                        -- this mode is NOT RECOMMENDED for use in production
 			window = 15, -- 15 minutes sampling window
 			period = 6*(60/15) -- track last 6 hours
 		}
 	}
 
-Defaults are as above: 15 minutes window, 6 hours period.
-
 Exported metrics
 ----------------
 
diff -pruN 5.4.4-1/modules/prefill/prefill.lua 5.5.1-5/modules/prefill/prefill.lua
--- 5.4.4-1/modules/prefill/prefill.lua	2022-01-05 13:19:14.491728300 +0000
+++ 5.5.1-5/modules/prefill/prefill.lua	2022-06-14 07:17:30.427491200 +0000
@@ -56,9 +56,7 @@ end
 -- returns: number of seconds the file is valid for
 -- 0 indicates immediate download
 local function get_file_ttl(fname)
-	local c_str = ffi.new("char[?]", #fname)
-	ffi.copy(c_str, fname)
-	local mtime = tonumber(ffi.C.kr_file_mtime(c_str))
+	local mtime = tonumber(ffi.C.kr_file_mtime(fname))
 
 	if mtime > 0 then
 		local age = os.time() - mtime
@@ -75,27 +73,30 @@ local function download(url, fname)
 	local file, rcode, errmsg
 	file, errmsg = io.open(fname, 'w')
 	if not file then
-		error(string.format("[prefill] unable to open file %s (%s)",
+		error(string.format("[prefil] unable to open file %s (%s)",
 			fname, errmsg))
 	end
 
 	log_info(ffi.C.LOG_GRP_PREFILL, "downloading root zone to file %s ...", fname)
 	rcode, errmsg = kluautil.kr_https_fetch(url, file, rz_ca_file)
 	if rcode == nil then
-		error(string.format("[prefill] fetch of `%s` failed: %s", url, errmsg))
+		error(string.format("[prefil] fetch of `%s` failed: %s", url, errmsg))
 	end
 
 	file:close()
 end
 
 local function import(fname)
-	local res = cache.zone_import(fname)
-	if res.code == 1 then -- no TA found, wait
-		error("[prefill] no trust anchor found for root zone, import aborted")
-	elseif res.code == 0 then
-		log_info(ffi.C.LOG_GRP_PREFILL, "root zone successfully parsed, import started")
+	local ret = ffi.C.zi_zone_import({
+		zone_file = fname,
+		time_src = ffi.C.ZI_STAMP_MTIM, -- the file might be slightly older
+	})
+	if ret == 0 then
+		log_info(ffi.C.LOG_GRP_PREFILL, "zone successfully parsed, import started")
 	else
-		error(string.format("[prefill] root zone import failed (%s)", res.msg))
+		error(string.format(
+			"[prefil] zone import failed: %s", ffi.C.knot_strerror(ret)
+		))
 	end
 end
 
@@ -153,7 +154,7 @@ local function config_zone(zone_cfg)
 	if zone_cfg.interval then
 		zone_cfg.interval = tonumber(zone_cfg.interval)
 		if zone_cfg.interval < rz_interval_min then
-			error(string.format('[prefill] refresh interval %d s is too short, '
+			error(string.format('[prefil] refresh interval %d s is too short, '
 				.. 'minimal interval is %d s',
 				zone_cfg.interval, rz_interval_min))
 		end
@@ -164,7 +165,7 @@ local function config_zone(zone_cfg)
 	rz_ca_file = zone_cfg.ca_file
 
 	if not zone_cfg.url or not string.match(zone_cfg.url, '^https://') then
-		error('[prefill] option url must contain a '
+		error('[prefil] option url must contain a '
 			.. 'https:// URL of a zone file')
 	else
 		rz_url = zone_cfg.url
@@ -175,12 +176,12 @@ function prefill.config(config)
 	if config == nil then return end -- e.g. just modules = { 'prefill' }
 	local root_configured = false
 	if type(config) ~= 'table' then
-		error('[prefill] configuration must be in table '
+		error('[prefil] configuration must be in table '
 			.. '{owner name = {per-zone config}}')
 	end
 	for owner, zone_cfg in pairs(config) do
 		if owner ~= '.' then
-			error('[prefill] only root zone can be imported '
+			error('[prefil] only root zone can be imported '
 				.. 'at the moment')
 		else
 			config_zone(zone_cfg)
@@ -188,7 +189,7 @@ function prefill.config(config)
 		end
 	end
 	if not root_configured then
-		error('[prefill] this module version requires configuration '
+		error('[prefil] this module version requires configuration '
 			.. 'for root zone')
 	end
 
diff -pruN 5.4.4-1/modules/prefill/prefill.test/prefill.test.lua 5.5.1-5/modules/prefill/prefill.test/prefill.test.lua
--- 5.4.4-1/modules/prefill/prefill.test/prefill.test.lua	2022-01-05 13:19:14.491728300 +0000
+++ 5.5.1-5/modules/prefill/prefill.test/prefill.test.lua	2022-06-14 07:17:30.427491200 +0000
@@ -44,10 +44,17 @@ env.KRESD_NO_LISTEN = true
 
 local check_answer = require('test_utils').check_answer
 
+local function zone_import(fname, downgrade)
+	return require('ffi').C.zi_zone_import({
+			zone_file = fname,
+			downgrade = downgrade,
+	})
+end
+
 local function import_valid_root_zone()
 	cache.clear()
-	local import_res = cache.zone_import('testroot.zone')
-	assert(import_res.code == 0)
+	local import_res = zone_import('testroot.zone')
+	assert(import_res == 0)
 	-- beware that import takes at least 100 ms
 	worker.sleep(0.2)  -- zimport is delayed by 100 ms from function call
 	-- sanity checks - cache must be filled in
@@ -60,8 +67,8 @@ end
 
 local function import_root_no_soa()
 	cache.clear()
-	local import_res = cache.zone_import('testroot_no_soa.zone')
-	assert(import_res.code == -1)
+	local import_res = zone_import('testroot_no_soa.zone')
+	assert(import_res == -1)
 	-- beware that import takes at least 100 ms
 	worker.sleep(0.2)  -- zimport is delayed by 100 ms from function call
 	-- sanity checks - cache must be filled in
@@ -70,28 +77,26 @@ end
 
 local function import_unsigned_root_zone()
 	cache.clear()
-	local import_res = cache.zone_import('testroot.zone.unsigned')
-	assert(import_res.code == 0)
+	zone_import('testroot.zone.unsigned')
 	-- beware that import takes at least 100 ms
 	worker.sleep(0.2)  -- zimport is delayed by 100 ms from function call
-	-- sanity checks - cache must be filled in
+	-- we wanted it to fail
 	ok(cache.count() == 0, 'cache is still empty after import of unsigned zone')
 end
 
 local function import_not_root_zone()
 	cache.clear()
-        local import_res = cache.zone_import('example.com.zone')
-	assert(import_res.code == 1)
+	zone_import('example.com.zone')
 	-- beware that import takes at least 100 ms
 	worker.sleep(0.2)  -- zimport is delayed by 100 ms from function call
-	-- sanity checks - cache must be filled in
+	-- we wanted it to fail
 	ok(cache.count() == 0, 'cache is still empty after import of other zone than root')
 end
 
 local function import_empty_zone()
 	cache.clear()
-	local import_res = cache.zone_import('empty.zone')
-	assert(import_res.code == -1)
+	local import_res = zone_import('empty.zone')
+	assert(import_res < 0)
 	-- beware that import takes at least 100 ms
 	worker.sleep(0.2)  -- zimport is delayed by 100 ms from function call
 	-- sanity checks - cache must be filled in
@@ -100,8 +105,8 @@ end
 
 local function import_random_trash()
 	cache.clear()
-	local import_res = cache.zone_import('random.zone')
-	assert(import_res.code == -1)
+	local import_res = zone_import('random.zone')
+	assert(import_res < 0)
 	-- beware that import takes at least 100 ms
 	worker.sleep(0.2)  -- zimport is delayed by 100 ms from function call
 	-- sanity checks - cache must be filled in
diff -pruN 5.4.4-1/modules/refuse_nord/refuse_nord.c 5.5.1-5/modules/refuse_nord/refuse_nord.c
--- 5.4.4-1/modules/refuse_nord/refuse_nord.c	2022-01-05 13:19:14.495728300 +0000
+++ 5.5.1-5/modules/refuse_nord/refuse_nord.c	2022-06-14 07:17:30.427491200 +0000
@@ -21,6 +21,7 @@ static int refuse_nord_query(kr_layer_t
 		return ctx->state;
 	knot_wire_set_rcode(answer->wire, KNOT_RCODE_REFUSED);
 	knot_wire_clear_ad(answer->wire);
+	kr_request_set_extended_error(req, KNOT_EDNS_EDE_NOTAUTH, "ABC4");
 	ctx->state = KR_STATE_DONE;
 	return ctx->state;
 }
diff -pruN 5.4.4-1/modules/renumber/README.rst 5.5.1-5/modules/renumber/README.rst
--- 5.4.4-1/modules/renumber/README.rst	2022-01-05 13:19:14.495728300 +0000
+++ 5.5.1-5/modules/renumber/README.rst	2022-06-14 07:17:30.427491200 +0000
@@ -15,9 +15,6 @@ in local zones, that will be remapped to
    breaks signatures. You can see whether an answer was valid or not based on
    the AD flag.
 
-.. warning:: The module is currently limited to rewriting complete octets of
-   the IP addresses, i.e. only /8, /16, /24 etc. network masks are supported.
-
 Example configuration
 ---------------------
 
@@ -28,6 +25,12 @@ Example configuration
 			-- Source subnet, destination subnet
 			{'10.10.10.0/24', '192.168.1.0'},
 			-- Remap /16 block to localhost address range
-			{'166.66.0.0/16', '127.0.0.0'}
+			{'166.66.0.0/16', '127.0.0.0'},
+			-- Remap /26 subnet (64 ip addresses)
+			{'166.55.77.128/26', '127.0.0.192'},
+			-- Remap a /32 block to a single address
+			{'2001:db8::/32', '::1!'},
 		}
 	}
+
+.. TODO: renumber.name() hangs in vacuum, kind of.  No occurrences in code or docs, and probably bad UX.
diff -pruN 5.4.4-1/modules/renumber/renumber.lua 5.5.1-5/modules/renumber/renumber.lua
--- 5.4.4-1/modules/renumber/renumber.lua	2022-01-05 13:19:14.495728300 +0000
+++ 5.5.1-5/modules/renumber/renumber.lua	2022-06-14 07:17:30.427491200 +0000
@@ -3,34 +3,78 @@
 local ffi = require('ffi')
 local prefixes_global = {}
 
+-- get address from config: either subnet prefix or fixed endpoint
+local function extract_address(target)
+	local idx = string.find(target, "!", 1, true)
+	if idx == nil then
+		return target, false
+	end
+	if idx ~= #target then
+		error("[renumber] \"!\" symbol in target is only accepted at the end of address")
+	end
+	return string.sub(target, 1, idx - 1), true
+end
+
+-- Create bitmask from integer mask for single octet: 2 -> 11000000
+local function getOctetBitmask(intMask)
+	return bit.lshift(bit.rshift(255, 8 - intMask), 8 - intMask)
+end
+
+-- Merge ipNet with ipHost, using intMask
+local function mergeIps(ipNet, ipHost, intMask)
+	local octetMask
+	local result = ""
+
+	if (#ipNet ~= #ipHost) then
+		return nil
+	end
+
+	for currentOctetNo = 1, #ipNet do
+		if intMask >= 8 then
+			result = result .. ipNet:sub(currentOctetNo,currentOctetNo)
+		elseif (intMask <= 0) then
+			result = result .. ipHost:sub(currentOctetNo,currentOctetNo)
+		else
+			octetMask = getOctetBitmask(intMask)
+			result = result .. string.char(bit.bor(
+					bit.band(string.byte(ipNet:sub(currentOctetNo,currentOctetNo)), octetMask),
+					bit.band(string.byte(ipHost:sub(currentOctetNo,currentOctetNo)), bit.bnot(octetMask))
+			))
+		end
+		intMask = intMask - 8
+	end
+
+	return result
+end
+
 -- Create subnet prefix rule
 local function matchprefix(subnet, addr)
+	local is_exact
+	addr, is_exact = extract_address(addr)
 	local target = kres.str2ip(addr)
 	if target == nil then error('[renumber] invalid address: '..addr) end
 	local addrtype = string.find(addr, ':', 1, true) and kres.type.AAAA or kres.type.A
 	local subnet_cd = ffi.new('char[16]')
 	local bitlen = ffi.C.kr_straddr_subnet(subnet_cd, subnet)
 	if bitlen < 0 then error('[renumber] invalid subnet: '..subnet) end
-	return {subnet_cd, bitlen, target, addrtype}
+	return {subnet_cd, bitlen, target, addrtype, is_exact}
 end
 
 -- Create name match rule
 local function matchname(name, addr)
+	local is_exact
+	addr, is_exact = extract_address(addr) -- though matchname() always leads to replacing whole address
 	local target = kres.str2ip(addr)
 	if target == nil then error('[renumber] invalid address: '..addr) end
 	local owner = todname(name)
 	if not name then error('[renumber] invalid name: '..name) end
 	local addrtype = string.find(addr, ':', 1, true) and kres.type.AAAA or kres.type.A
-	return {owner, nil, target, addrtype}
+	return {owner, nil, target, addrtype, is_exact}
 end
 
 -- Add subnet prefix rewrite rule
 local function add_prefix(subnet, addr)
 	local prefix = matchprefix(subnet, addr)
-	local bitlen = prefix[2]
-	if bitlen ~= nil and bitlen % 8 ~= 0 then
-		log_warn(ffi.C.LOG_GRP_RENUMBER, 'network mask: only /8, /16, /24 etc. are supported (entire octets are rewritten)')
-	end
 	table.insert(prefixes_global, prefix)
 end
 
@@ -42,21 +86,25 @@ local function match_subnet(subnet, bitl
 end
 
 -- Renumber address record
-local addr_buf = ffi.new('char[16]')
 local function renumber_record(tbl, rr)
 	for i = 1, #tbl do
 		local prefix = tbl[i]
+		local subnet = prefix[1]
+		local bitlen = prefix[2]
+		local target = prefix[3]
+		local addrtype = prefix[4]
+		local is_exact = prefix[5]
+
 		-- Match record type to address family and record address to given subnet
 		-- If provided, compare record owner to prefix name
-		if match_subnet(prefix[1], prefix[2], prefix[4], rr) then
-			-- Replace part or whole address
-			local to_copy = prefix[2] or (#prefix[3] * 8)
-			local chunks = to_copy / 8
-			local rdlen = #rr.rdata
-			if rdlen < chunks then return rr end -- Address length mismatch
-			ffi.copy(addr_buf, rr.rdata, rdlen)
-			ffi.copy(addr_buf, prefix[3], chunks) -- Rewrite prefix
-			rr.rdata = ffi.string(addr_buf, rdlen)
+		if match_subnet(subnet, bitlen, addrtype, rr) then
+			if is_exact then
+				rr.rdata = target
+			else
+				local mergedHost = mergeIps(target, rr.rdata, bitlen)
+				if mergedHost ~= nil then rr.rdata = mergedHost end
+			end
+
 			return rr
 		end
 	end
@@ -99,6 +147,7 @@ local function rule(prefixes)
 				pkt:put(rr.owner, rr.ttl, rr.class, rr.type, rr.rdata)
 			end
 		end
+		req:set_extended_error(kres.extended_error.FORGED, "DUQR")
 		return state
 	end
 end
diff -pruN 5.4.4-1/modules/renumber/renumber.test.lua 5.5.1-5/modules/renumber/renumber.test.lua
--- 5.4.4-1/modules/renumber/renumber.test.lua	2022-01-05 13:19:14.495728300 +0000
+++ 5.5.1-5/modules/renumber/renumber.test.lua	2022-06-14 07:17:30.427491200 +0000
@@ -33,10 +33,21 @@ local function prepare_cache()
 			}),
 		nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH))
 	assert(c:insert(
+		gen_rrset('a10-3plus4.test.',
+			kres.type.A, {
+				kres.str2ip('10.3.0.1'),
+				kres.str2ip('10.4.0.1')
+			}),
+		nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH))
+	assert(c:insert(
 		gen_rrset('a166-66.test.',
 			kres.type.A, kres.str2ip('166.66.42.123')),
 		nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH))
 	assert(c:insert(
+		gen_rrset('a167-81.test.',
+			kres.type.A, kres.str2ip('167.81.254.221')),
+		nil, ffi.C.KR_RANK_SECURE + ffi.C.KR_RANK_AUTH))
+	assert(c:insert(
 		gen_rrset('aaaa-db8-1.test.',
 			kres.type.AAAA, {
 				kres.str2ip('2001:db8:1::1'),
@@ -56,9 +67,12 @@ local function test_renumber()
 		'a10-2.test.', kres.type.A, kres.rcode.NOERROR, '192.168.2.1')
 	check_answer('mix of known and unknown IPv4 ranges is remapped correctly',
 		'a10-0plus2.test.', kres.type.A, kres.rcode.NOERROR, {'192.168.2.1', '10.0.0.1'})
+	check_answer('mix of known and unknown IPv4 ranges is remapped correctly to exact address',
+		'a10-3plus4.test.', kres.type.A, kres.rcode.NOERROR, {'10.3.0.1', '192.168.3.10'})
 	check_answer('known IPv4 range is remapped when matching second-defined rule',
 		'a166-66.test.', kres.type.A, kres.rcode.NOERROR, '127.0.42.123')
-
+	check_answer('known IPv4 range is remapped when matching a rule with netmask not on a byte boundary',
+		'a167-81.test.', kres.type.A, kres.rcode.NOERROR, {'127.0.30.221'})
 
 	check_answer('two AAAA records',
 		'aaaa-db8-1.test.', kres.type.AAAA, kres.rcode.NOERROR,
@@ -78,7 +92,9 @@ modules.load('renumber < cache')
 renumber.config({
 	-- Source subnet, destination subnet
 	{'10.2.0.0/24', '192.168.2.0'},
+	{'10.4.0.0/24', '192.168.3.10!'},
 	{'166.66.0.0/16', '127.0.0.0'},
+	{'167.81.255.0/19', '127.0.0.0'},
 	{'2001:db8:1::/48', '2001:db8:2::'},
 })
 
diff -pruN 5.4.4-1/modules/stats/stats.c 5.5.1-5/modules/stats/stats.c
--- 5.4.4-1/modules/stats/stats.c	2022-01-05 13:19:14.495728300 +0000
+++ 5.5.1-5/modules/stats/stats.c	2022-06-14 07:17:30.431491100 +0000
@@ -18,6 +18,7 @@
 #include <arpa/inet.h>
 #include <lua.h>
 
+#include "lib/generic/trie.h"
 #include "lib/layer/iterate.h"
 #include "lib/rplan.h"
 #include "lib/module.h"
@@ -25,7 +26,7 @@
 #include "lib/resolve.h"
 
 /* Defaults */
-#define VERBOSE_MSG(qry, ...) QRVERBOSE(qry, STATISTICS,  __VA_ARGS__)
+#define VERBOSE_MSG(qry, ...) kr_log_q(qry, STATISTICS,  __VA_ARGS__)
 #define FREQUENT_PSAMPLE  10 /* Sampling rate, 1 in N */
 #ifdef LRU_REP_SIZE
  #define FREQUENT_COUNT LRU_REP_SIZE /* Size of frequent tables */
@@ -70,7 +71,7 @@ typedef array_t(struct sockaddr_in6) add
 
 /** @internal Stats data structure. */
 struct stat_data {
-	map_t map;
+	trie_t *trie;
 	struct {
 		namehash_t *frequent;
 	} queries;
@@ -156,7 +157,7 @@ static int collect_rtt(kr_layer_t *ctx,
 	/* Socket address is encoded into sockaddr_in6 struct that
 	 * unions with sockaddr_in and differ in sa_family */
 	struct sockaddr_in6 *e = &data->upstreams.q.at[data->upstreams.head];
-	const union inaddr *src = &req->upstream.transport->address;
+	const union kr_sockaddr *src = &req->upstream.transport->address;
 	switch (src->ip.sa_family) {
 		case AF_INET:  memcpy(e, &src->ip4, sizeof(src->ip4)); break;
 		case AF_INET6: memcpy(e, &src->ip6, sizeof(src->ip6)); break;
@@ -291,7 +292,8 @@ static char* stats_set(void *env, struct
 				return NULL;
 			}
 		}
-		map_set(&data->map, pair, (void *)number);
+		trie_val_t *trie_val = trie_get_ins(data->trie, pair, strlen(pair));
+		*trie_val = (void *)number;
 	}
 
 	return NULL;
@@ -324,20 +326,39 @@ static char* stats_get(void *env, struct
 		}
 	}
 	/* Check in variable map */
-	if (!map_contains(&data->map, args)) {
+	trie_val_t *val = trie_get_try(data->trie, args, strlen(args));
+	if (!val) {
 		free(ret);
 		return NULL;
 	}
-	void *val = map_get(&data->map, args);
-	sprintf(ret, "%zu", (size_t) val);
+	sprintf(ret, "%zu", (size_t) *val);
 	return ret;
 }
 
-static int list_entry(const char *key, void *val, void *baton)
+/** Checks whether:
+ *   - `key` starts with `prefix`; OR
+ *   - The prefix is a wildcard, which is indicated by `prefix_len` being zero. */
+static inline bool key_matches_prefix(const char *key, size_t key_len,
+                                      const char *prefix, size_t prefix_len)
+{
+	return prefix_len == 0 || (prefix_len <= key_len && memcmp(key, prefix, prefix_len) == 0);
+}
+
+struct list_entry_context {
+	JsonNode *root;         /**< JSON object into which matching entries will be inserted. */
+	const char *key_prefix; /**< The prefix against which entries will be matched. */
+	size_t key_prefix_len;  /**< Prefix length. Prefix is a wildcard if zero. */
+};
+
+/** Inserts the entry with a matching key into the JSON object. */
+static int list_entry(const char *key, uint32_t key_len, trie_val_t *val, void *baton)
 {
-	JsonNode *root = baton;
-	size_t number = (size_t) val;
-	json_append_member(root, key, json_mknumber(number));
+	struct list_entry_context *ctx = baton;
+	if (!key_matches_prefix(key, key_len, ctx->key_prefix, ctx->key_prefix_len))
+		return 0;
+	size_t number = (size_t) *val;
+	auto_free char *key_nt = strndup(key, key_len);
+	json_append_member(ctx->root, key_nt, json_mknumber(number));
 	return 0;
 }
 
@@ -348,7 +369,6 @@ static int list_entry(const char *key, v
  */
 static char* stats_list(void *env, struct kr_module *module, const char *args)
 {
-	struct stat_data *data = module->data;
 	JsonNode *root = json_mkobject();
 	/* Walk const metrics map */
 	size_t args_len = args ? strlen(args) : 0;
@@ -358,7 +378,13 @@ static char* stats_list(void *env, struc
 			json_append_member(root, elm->key, json_mknumber(elm->val));
 		}
 	}
-	map_walk_prefixed(&data->map, (args_len > 0) ? args : "", list_entry, root);
+	struct list_entry_context ctx = {
+		.root = root,
+		.key_prefix = args,
+		.key_prefix_len = args_len
+	};
+	struct stat_data *data = module->data;
+	trie_apply_with_key(data->trie, list_entry, &ctx);
 	char *ret = json_encode(root);
 	json_delete(root);
 	return ret;
@@ -475,7 +501,7 @@ int stats_init(struct kr_module *module)
 	if (!data) {
 		return kr_error(ENOMEM);
 	}
-	data->map = map_make(NULL);
+	data->trie = trie_create(NULL);
 	module->data = data;
 	lru_create(&data->queries.frequent, FREQUENT_COUNT, NULL, NULL);
 	/* Initialize ring buffer of recently visited upstreams */
@@ -495,7 +521,7 @@ int stats_deinit(struct kr_module *modul
 {
 	struct stat_data *data = module->data;
 	if (data) {
-		map_clear(&data->map);
+		trie_free(data->trie);
 		lru_free(data->queries.frequent);
 		array_clear(data->upstreams.q);
 		free(data);
diff -pruN 5.4.4-1/modules/ta_update/meson.build 5.5.1-5/modules/ta_update/meson.build
--- 5.4.4-1/modules/ta_update/meson.build	2022-01-05 13:19:14.495728300 +0000
+++ 5.5.1-5/modules/ta_update/meson.build	2022-06-14 07:17:30.431491100 +0000
@@ -6,6 +6,8 @@ config_tests += [
 ]
 
 integr_tests += [
+  # NOTE: ta_update may pass in cases when it should fail due to race conditions
+  # To ensure reliability, deckard should introduce a time wait
   ['ta_update', meson.current_source_dir() / 'ta_update.test.integr'],
   ['ta_update.unmanagedkey', meson.current_source_dir() / 'ta_update.unmanagedkey.test.integr'],
 ]
diff -pruN 5.4.4-1/NEWS 5.5.1-5/NEWS
--- 5.4.4-1/NEWS	2022-01-05 13:19:14.407727000 +0000
+++ 5.5.1-5/NEWS	2022-06-14 07:17:30.359490200 +0000
@@ -1,3 +1,48 @@
+Knot Resolver 5.5.1 (2022-06-14)
+================================
+
+Improvements
+------------
+- daemon/tls: disable TLS resumption via tickets for TLS <= 1.2 (#742, !1295)
+- daemon/http: DoH now responds with proper HTTP codes (#728, !1279)
+- renumber module: allow rewriting subnet to a single IP (!1302)
+- renumber module: allow arbitrary netmask (!1306)
+- nameserver selection algorithm: improve IPv6 avoidance if broken (!1298)
+
+Bugfixes
+--------
+- modules/dns64: fix incorrect packet writes for cached packets (#727, !1275)
+- xdp: make it work also with libknot 3.1 (#735, !1276)
+- prefill module: fix lockup when starting multiple idle instances (!1285)
+- validator: fix some failing negative NSEC proofs (!1294, #738, #443)
+
+
+Knot Resolver 5.5.0 (2022-03-15)
+================================
+
+Improvements
+------------
+- extended_errors: module for extended DNS error support, RFC8914 (!1234)
+- policy: log policy actions; useful for RPZ debugging (!1239)
+- policy: new action policy.IPTRACE for logging request origin (!1239)
+- prefill module: prepare for ZONEMD, improve performance (!1225)
+- validator: conditionally ignore SHA1 DS, as SHOULD by RFC4509 (!1251)
+- lib/resolve: use EDNS padding for outgoing TLS queries (!1254)
+- support for PROXYv2 protocol (!1238)
+- lib/resolve, policy: new NO_ANSWER flag for not responding to clients (!1257)
+
+Incompatible changes
+--------------------
+- libknot >= 3.0.2 is required
+
+Bugfixes
+--------
+- doh2: fix CORS by adding `access-control-allow-origin: *` (!1246)
+- net: fix listen by interface - add interface suffix to link-local IPv6 (!1253)
+- daemon/tls: fix resumption for outgoing TLS (e.g. TLS_FORWARD) (!1261)
+- nameserver selection: fix interaction of timeouts with reboots (#722, !1269)
+
+
 Knot Resolver 5.4.4 (2022-01-05)
 ================================
 
@@ -605,7 +650,7 @@ Incompatible changes
   it will be later reworked to reflect development in IEFT dnsop working group
 - version module was permanently removed because it was not really used by users;
   if you want to receive notifications about new releases please subscribe to
-  https://lists.nic.cz/cgi-bin/mailman/listinfo/knot-resolver-announce
+  https://lists.nic.cz/postorius/lists/knot-resolver-announce.lists.nic.cz/
 
 Bugfixes
 --------
diff -pruN 5.4.4-1/README.md 5.5.1-5/README.md
--- 5.4.4-1/README.md	2022-01-05 13:19:14.407727000 +0000
+++ 5.5.1-5/README.md	2022-06-14 07:17:30.359490200 +0000
@@ -68,6 +68,6 @@ See the documentation at [knot-resolver.
 ### Contacting us
 
 - [GitLab issues](https://gitlab.nic.cz/knot/knot-resolver/issues) (you may authenticate via GitHub)
-- [mailing list](https://lists.nic.cz/cgi-bin/mailman/listinfo/knot-resolver-users)
+- [mailing list](https://lists.nic.cz/postorius/lists/knot-resolver-announce.lists.nic.cz/)
 - [![Join the chat at https://gitter.im/CZ-NIC/knot-resolver](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/CZ-NIC/knot-resolver?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
 
diff -pruN 5.4.4-1/scripts/gen-pgp-keyblock.sh 5.5.1-5/scripts/gen-pgp-keyblock.sh
--- 5.4.4-1/scripts/gen-pgp-keyblock.sh	2022-01-05 13:19:14.503728400 +0000
+++ 5.5.1-5/scripts/gen-pgp-keyblock.sh	2022-06-14 07:17:30.435491300 +0000
@@ -4,7 +4,8 @@ set -o errexit -o nounset
 
 keys=(
     'B6006460B60A80E782062449E747DF1F9575A3AA'  # vladimir.cunat@nic.cz
-    '4A8BA48C2AED933BD495C509A1FBA5F7EF8C4869'  # tomas.krizek@nic.cz
+    '3057EE9A448F362D74205A779AB120DA0A76F6DE'  # ales.mrazek@nic.cz
+    # '4A8BA48C2AED933BD495C509A1FBA5F7EF8C4869'  # tomas.krizek@nic.cz  expired 2022-03-31
 )
 outfile="kresd-keyblock.asc"
 url="https://secure.nic.cz/files/knot-resolver/kresd-keyblock.asc"
diff -pruN 5.4.4-1/security.txt 5.5.1-5/security.txt
--- 5.4.4-1/security.txt	2022-01-05 13:19:14.503728400 +0000
+++ 5.5.1-5/security.txt	2022-06-14 07:17:30.435491300 +0000
@@ -1,8 +1,7 @@
-Please report security issues that require encryption to both of the following
-e-mail addresses.
+Please report security issues that require encryption to the following e-mail
+address.
 
 vladimir.cunat@nic.cz
-tomas.krizek@nic.cz
 
 You can obtain our PGP keys from:
 https://secure.nic.cz/files/knot-resolver/kresd-keyblock.asc
diff -pruN 5.4.4-1/tests/config/doh2.test.lua 5.5.1-5/tests/config/doh2.test.lua
--- 5.4.4-1/tests/config/doh2.test.lua	2022-01-05 13:19:14.503728400 +0000
+++ 5.5.1-5/tests/config/doh2.test.lua	2022-06-14 07:17:30.435491300 +0000
@@ -63,15 +63,15 @@ local function check_ok(req, desc)
 	return headers, pkt
 end
 
---local function check_err(req, exp_status, desc)
---	local headers, errmsg, errno = req:go(16)
---	if errno then
---		nok(errmsg, desc .. ': ' .. errmsg)
---		return
---	end
---	local got_status = headers:get(':status')
---	same(got_status, exp_status, desc)
---end
+local function check_err(req, exp_status, desc)
+	local headers, errmsg, errno = req:go(16)
+	if errno then
+		nok(errmsg, desc .. ': ' .. errmsg)
+		return
+	end
+	local got_status = headers:get(':status')
+	same(got_status, exp_status, desc)
+end
 
 -- check prerequisites
 local bound, port
@@ -117,6 +117,7 @@ else
 		end
 		-- uncacheable
 		same(headers:get('cache-control'), 'max-age=0', desc .. ': TTL 0')
+		same(headers:get('access-control-allow-origin'), '*', desc .. ': CORS headers')
 		same(pkt:rcode(), kres.rcode.SERVFAIL, desc .. ': rcode matches')
 	end
 
@@ -168,35 +169,39 @@ else
 	end
 
 	-- test an invalid DNS query using POST
---	local function test_post_short_input()
---		local req = assert(req_templ:clone())
---		req.headers:upsert(':method', 'POST')
---		req:set_body(string.rep('0', 11))  -- 11 bytes < DNS msg header
---		check_err(req, '400', 'too short POST finishes with 400')
---	end
---
+	local function test_post_short_input()
+		local req = assert(req_templ:clone())
+		req.headers:upsert(':method', 'POST')
+		req:set_body(string.rep('0', 11))  -- 11 bytes < DNS msg header
+		check_err(req, '400', 'too short POST finishes with 400')
+	end
+
 --	local function test_post_long_input()
---		local req = assert(req_templ:clone())
---		req.headers:upsert(':method', 'POST')
---		req:set_body(string.rep('s', 1025))  -- > DNS msg over UDP
---		check_err(req, '413', 'too long POST finishes with 413')
---	end
---
---	local function test_post_unparseable_input()
---		local req = assert(req_templ:clone())
---		req.headers:upsert(':method', 'POST')
---		req:set_body(string.rep('\0', 1024))  -- garbage
---		check_err(req, '400', 'unparseable DNS message finishes with 400')
---	end
+--		-- FIXME: This test is broken in Lua. The connection times out
+--		-- for some reason, but sending a request like this with `curl`
+--		-- or PowerShell's `Invoke-RestMethod` provides correct results.
 --
---	local function test_post_unsupp_type()
 --		local req = assert(req_templ:clone())
 --		req.headers:upsert(':method', 'POST')
---		req.headers:upsert('content-type', 'application/dns+json')
---		req:set_body(string.rep('\0', 12))  -- valid message
---		check_err(req, '415', 'unsupported request content type finishes with 415')
+--		req:set_body(string.rep('s', 1025))  -- > DNS msg over UDP
+--		check_err(req, '400', 'too long POST finishes with 400')
 --	end
 
+	local function test_post_unparseable_input()
+		local req = assert(req_templ:clone())
+		req.headers:upsert(':method', 'POST')
+		req:set_body(string.rep('\0', 1024))  -- garbage
+		check_err(req, '400', 'unparseable DNS message finishes with 400')
+	end
+
+	local function test_post_unsupp_type()
+		local req = assert(req_templ:clone())
+		req.headers:upsert(':method', 'POST')
+		req.headers:upsert('content-type', 'application/dns+json')
+		req:set_body(string.rep('\0', 12))  -- valid message
+		check_err(req, '415', 'unsupported request content type finishes with 415')
+	end
+
 	-- test a valid DNS query using GET
 	local function test_get_servfail()
 		local desc = 'valid GET query which ends with SERVFAIL'
@@ -225,6 +230,7 @@ else
 		end
 		-- HTTP TTL is minimum from all RRs in the answer
 		same(headers:get('cache-control'), 'max-age=300', desc .. ': TTL 900')
+		same(headers:get('access-control-allow-origin'), '*', desc .. ': CORS headers')
 		same(pkt:rcode(), kres.rcode.NOERROR, desc .. ': rcode matches')
 		same(pkt:ancount(), 3, desc .. ': ANSWER is present')
 		same(pkt:nscount(), 1, desc .. ': AUTHORITY is present')
@@ -273,47 +279,47 @@ else
 		check_ok(req, desc)
 	end
 
---	-- test an invalid DNS query using GET
---		local function test_get_long_input()
---		local req = assert(req_templ:clone())
---		req.headers:upsert(':method', 'GET')
---		req.headers:upsert(':path', '/doh?dns=' .. basexx.to_url64(string.rep('\0', 1030)))
---		check_err(req, '414', 'too long GET finishes with 414')
---	end
---
---	local function test_get_no_dns_param()
---		local req = assert(req_templ:clone())
---		req.headers:upsert(':method', 'GET')
---		req.headers:upsert(':path', '/doh?notdns=' .. basexx.to_url64(string.rep('\0', 1024)))
---		check_err(req, '400', 'GET without dns parameter finishes with 400')
---	end
---
---	local function test_get_unparseable()
---		local req = assert(req_templ:clone())
---		req.headers:upsert(':method', 'GET')
---		req.headers:upsert(':path', '/doh??dns=' .. basexx.to_url64(string.rep('\0', 1024)))
---		check_err(req, '400', 'unparseable GET finishes with 400')
---	end
---
---	local function test_get_invalid_b64()
---		local req = assert(req_templ:clone())
---		req.headers:upsert(':method', 'GET')
---		req.headers:upsert(':path', '/doh?dns=thisisnotb64')
---		check_err(req, '400', 'GET with invalid base64 finishes with 400')
---	end
---
---	local function test_get_invalid_chars()
---		local req = assert(req_templ:clone())
---		req.headers:upsert(':method', 'GET')
---		req.headers:upsert(':path', '/doh?dns=' .. basexx.to_url64(string.rep('\0', 200)) .. '@#$%?!')
---		check_err(req, '400', 'GET with invalid characters in b64 finishes with 400')
---	end
---
---	local function test_unsupp_method()
---		local req = assert(req_templ:clone())
---		req.headers:upsert(':method', 'PUT')
---		check_err(req, '405', 'unsupported method finishes with 405')
---	end
+	-- test an invalid DNS query using GET
+	local function test_get_long_input()
+		local req = assert(req_templ:clone())
+		req.headers:upsert(':method', 'GET')
+		req.headers:upsert(':path', '/doh?dns=' .. basexx.to_url64(string.rep('\0', 1030)))
+		check_err(req, '400', 'too long GET finishes with 400')
+	end
+
+	local function test_get_no_dns_param()
+		local req = assert(req_templ:clone())
+		req.headers:upsert(':method', 'GET')
+		req.headers:upsert(':path', '/doh?notdns=' .. basexx.to_url64(string.rep('\0', 1024)))
+		check_err(req, '400', 'GET without dns parameter finishes with 400')
+	end
+
+	local function test_get_unparseable()
+		local req = assert(req_templ:clone())
+		req.headers:upsert(':method', 'GET')
+		req.headers:upsert(':path', '/doh??dns=' .. basexx.to_url64(string.rep('\0', 1024)))
+		check_err(req, '400', 'unparseable GET finishes with 400')
+	end
+
+	local function test_get_invalid_b64()
+		local req = assert(req_templ:clone())
+		req.headers:upsert(':method', 'GET')
+		req.headers:upsert(':path', '/doh?dns=thisisnotb64')
+		check_err(req, '400', 'GET with invalid base64 finishes with 400')
+	end
+
+	local function test_get_invalid_chars()
+		local req = assert(req_templ:clone())
+		req.headers:upsert(':method', 'GET')
+		req.headers:upsert(':path', '/doh?dns=' .. basexx.to_url64(string.rep('\0', 200)) .. '@#$%?!')
+		check_err(req, '400', 'GET with invalid characters in b64 finishes with 400')
+	end
+
+	local function test_unsupp_method()
+		local req = assert(req_templ:clone())
+		req.headers:upsert(':method', 'PUT')
+		check_err(req, '501', 'unsupported method finishes with 501')
+	end
 
 	local function test_dstaddr()
 		local triggered = false
@@ -436,29 +442,28 @@ else
 --	end
 
 	-- plan tests
-	-- TODO: implement (some) of the error status codes
 	local tests = {
 		start_server,
 		test_post_servfail,
 		test_post_noerror,
 		test_post_nxdomain,
 		test_huge_answer,
-		--test_post_short_input,
-		--test_post_long_input,
-		--test_post_unparseable_input,
-		--test_post_unsupp_type,
+		test_post_short_input,
+--		test_post_long_input, -- FIXME see the test function
+		test_post_unparseable_input,
+		test_post_unsupp_type,
 		test_get_servfail,
 		test_get_noerror,
 		test_get_nxdomain,
 		test_get_other_params_before_dns,
 		test_get_other_params_after_dns,
 		test_get_other_params,
-		--test_get_long_input,
-		--test_get_no_dns_param,
-		--test_get_unparseable,
-		--test_get_invalid_b64,
-		--test_get_invalid_chars,
-		--test_unsupp_method,
+		test_get_long_input,
+		test_get_no_dns_param,
+		test_get_unparseable,
+		test_get_invalid_b64,
+		test_get_invalid_chars,
+		test_unsupp_method,
 		test_dstaddr,
 		test_srcaddr,
 		test_headers
diff -pruN 5.4.4-1/tests/config/meson.build 5.5.1-5/tests/config/meson.build
--- 5.4.4-1/tests/config/meson.build	2022-01-05 13:19:14.503728400 +0000
+++ 5.5.1-5/tests/config/meson.build	2022-06-14 07:17:30.435491300 +0000
@@ -29,8 +29,8 @@ foreach config_test : config_tests
     'config.' + config_test[0],
     run_configtest,
     args: [
-	'-c', files('test.cfg'),
-	'-n'
+      '-c', files('test.cfg'),
+      '-n'
     ],
     env: conftest_env,
     suite: [
diff -pruN 5.4.4-1/tests/config/net.test.lua 5.5.1-5/tests/config/net.test.lua
--- 5.4.4-1/tests/config/net.test.lua	2022-01-05 13:19:14.503728400 +0000
+++ 5.5.1-5/tests/config/net.test.lua	2022-06-14 07:17:30.435491300 +0000
@@ -25,10 +25,40 @@ local function test_freebind()
 		'net.listen({freebind = true}) enables FREEBIND for UDP listener')
 	same(net_list[2].transport.freebind, true,
 		'net.listen({freebind = true}) enables FREEBIND for TCP listener')
+end
 
+local function test_proxy_allowed()
+	same(net.proxy_allowed(), {}, 'net.proxy_allowed() empty by default')
+	net.proxy_allowed('172.22.0.1')
+	same(net.proxy_allowed(), {'172.22.0.1/32'}, 'net.proxy_allowed() single IPv4 host')
+	net.proxy_allowed({'172.22.0.1'})
+	same(net.proxy_allowed(), {'172.22.0.1/32'}, 'net.proxy_allowed() single IPv4 host (as table)')
+	net.proxy_allowed('172.18.1.0/24')
+	same(net.proxy_allowed(), {'172.18.1.0/24'}, 'net.proxy_allowed() IPv4 net')
+	net.proxy_allowed({'172.22.0.1', '172.18.1.0/24'})
+	same(net.proxy_allowed(), {'172.18.1.0/24', '172.22.0.1/32'}, 'net.proxy_allowed() multiple IPv4 args as table')
+	net.proxy_allowed({})
+	same(net.proxy_allowed(), {}, 'net.proxy_allowed() clear table')
+	net.proxy_allowed({'::1'})
+	same(net.proxy_allowed(), {'::1/128'}, 'net.proxy_allowed() single IPv6 host')
+	net.proxy_allowed({'2001:db8:cafe:beef::/64'})
+	same(net.proxy_allowed(), {'2001:db8:cafe:beef::/64'}, 'net.proxy_allowed() IPv6 net')
+	net.proxy_allowed({'0.0.0.0/0', '::/0'})
+	same(net.proxy_allowed(), {'0.0.0.0/0', '::/0'}, 'net.proxy_allowed() allow all IPv4 and IPv6')
+	net.proxy_allowed({'::1'})
+	same(net.proxy_allowed(), {'::1/128'}, 'net.proxy_allowed() single IPv6 host after all (proper reset)')
+	boom(net.proxy_allowed, {'a'}, 'net.proxy_allowed() invalid string arg')
+	boom(net.proxy_allowed, {'127.0.0.'}, 'net.proxy_allowed() incomplete IPv4')
+	boom(net.proxy_allowed, {'256.0.0.0'}, 'net.proxy_allowed() invalid IPv4')
+	boom(net.proxy_allowed, {'xx::'}, 'net.proxy_allowed() invalid IPv6')
+	boom(net.proxy_allowed, {'127.0.0.1/33'}, 'net.proxy_allowed() IPv4 invalid netmask')
+	boom(net.proxy_allowed, {'127.0.0.1/-1'}, 'net.proxy_allowed() IPv4 negative netmask')
+	boom(net.proxy_allowed, {'fd::/132'}, 'net.proxy_allowed() IPv6 invalid netmask')
+	boom(net.proxy_allowed, {{'127.0.0.0/8', '::1/129'}}, 'net.proxy_allowed() single param invalid')
 end
 
 return {
 	test_env_no_listen,
 	test_freebind,
+	test_proxy_allowed,
 }
diff -pruN 5.4.4-1/tests/config/tapered/src/tapered.lua 5.5.1-5/tests/config/tapered/src/tapered.lua
--- 5.4.4-1/tests/config/tapered/src/tapered.lua	2022-01-05 13:19:16.239752300 +0000
+++ 5.5.1-5/tests/config/tapered/src/tapered.lua	2022-06-14 07:17:32.003513600 +0000
@@ -1,3 +1,4 @@
+-- vim: et:ts=2:sw=2
 -- Helper variables and functions
 local get_info = debug.getinfo
 local pcall = pcall
@@ -11,6 +12,7 @@ local write = io.write
 local rawget = rawget
 local getmetatable = getmetatable
 local exit = os.exit
+local krprint = require("krprint")
 
 ---- Helper methods
 
@@ -19,6 +21,12 @@ local printf = function(fmt, ...)
   write(sprintf(fmt, ...))
 end
 
+local printdiff = function(func_name, actual, expected)
+  printf("Assertion %s() failed for test below (marked 'not ok'):\n", func_name)
+  printf("Expected: %s\n", krprint.pprint(expected))
+  printf("Got: %s\n", krprint.pprint(actual))
+end
+
 --- Compare potentially complex tables or objects
 --
 -- Ideas here are taken from [Penlight][p], [Underscore][u], [cwtest][cw], and
@@ -104,7 +112,11 @@ local nok = function (expression, msg)
 end
 
 local is = function (actual, expected, msg)
-  _test(actual == expected, msg)
+  local result = actual == expected;
+  if not result then
+    printdiff("is", actual, expected)
+  end
+  _test(result, msg)
 end
 
 local isnt = function (actual, expected, msg)
@@ -112,7 +124,11 @@ local isnt = function (actual, expected,
 end
 
 local same = function (actual, expected, msg)
-  _test(deepsame(actual, expected), msg)
+  local result = deepsame(actual, expected)
+  if not result then
+    printdiff("same", actual, expected)
+  end
+  _test(result, msg)
 end
 
 local like = function (str, pattern, msg)
diff -pruN 5.4.4-1/tests/dnstap/src/dnstap-test/run.sh 5.5.1-5/tests/dnstap/src/dnstap-test/run.sh
--- 5.4.4-1/tests/dnstap/src/dnstap-test/run.sh	2022-01-05 13:19:14.503728400 +0000
+++ 5.5.1-5/tests/dnstap/src/dnstap-test/run.sh	2022-06-14 07:17:30.435491300 +0000
@@ -11,7 +11,7 @@ if [ -z "$GITLAB_CI" ]; then
 	type -P go >/dev/null || exit 77
 	echo "Building the dnstap test and its dependencies..."
 	# some packages may be missing on the system right now
-	go get github.com/{FiloSottile/gvt,cloudflare/dns,dnstap/golang-dnstap}
+	go get github.com/{FiloSottile/gvt,cloudflare/dns,dnstap/golang-dnstap,golang/protobuf/proto}
 else
 	# In CI we've prebuilt dependencies into the default GOPATH.
 	# We're in a scratch container, so we just add the dnstap test inside.
diff -pruN 5.4.4-1/tests/integration/deckard/ci/junit-compare.py 5.5.1-5/tests/integration/deckard/ci/junit-compare.py
--- 5.4.4-1/tests/integration/deckard/ci/junit-compare.py	2022-01-05 13:19:16.291753000 +0000
+++ 5.5.1-5/tests/integration/deckard/ci/junit-compare.py	2022-06-14 07:17:32.043514300 +0000
@@ -25,7 +25,8 @@ def parse_junit_xml(filename):
 
 new = sys.argv[1]
 old = sys.argv[2]
-modified_tests = [line.strip() for line in open(sys.argv[3]).readlines()]
+with open(sys.argv[3]) as f:
+    modified_tests = [line.strip() for line in f.readlines()]
 
 test_diffs = parse_junit_xml(old) ^ parse_junit_xml(new)
 errorneous_rpls = [diff[1] for diff in test_diffs
diff -pruN 5.4.4-1/tests/integration/deckard/configs/named.yaml 5.5.1-5/tests/integration/deckard/configs/named.yaml
--- 5.4.4-1/tests/integration/deckard/configs/named.yaml	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/tests/integration/deckard/configs/named.yaml	2022-06-14 07:17:32.043514300 +0000
@@ -0,0 +1,15 @@
+programs:
+- name: named
+  binary: named
+  additional:
+    - -g
+    - -d
+    - "99"
+    - -c
+    - named.conf
+  templates:
+    - template/named.j2
+    - template/hints_zone.j2
+  configs:
+    - named.conf
+    - hints.zone
diff -pruN 5.4.4-1/tests/integration/deckard/deckard.py 5.5.1-5/tests/integration/deckard/deckard.py
--- 5.4.4-1/tests/integration/deckard/deckard.py	2022-01-05 13:19:16.295753000 +0000
+++ 5.5.1-5/tests/integration/deckard/deckard.py	2022-06-14 07:17:32.043514300 +0000
@@ -33,10 +33,8 @@ def setup_internal_addresses(context):
 
 
 def write_timestamp_file(path, tst):
-    time_file = open(path, 'w')
-    time_file.write(datetime.fromtimestamp(tst).strftime('@%Y-%m-%d %H:%M:%S'))
-    time_file.flush()
-    time_file.close()
+    with open(path, 'w') as time_file:
+        time_file.write(datetime.fromtimestamp(tst).strftime('@%Y-%m-%d %H:%M:%S'))
 
 
 def setup_faketime(context):
@@ -115,20 +113,21 @@ def run_daemon(program_config):
     name = program_config['DAEMON_NAME']
     proc = None
     program_config['log'] = os.path.join(program_config["WORKING_DIR"], 'server.log')
-    daemon_log_file = open(program_config['log'], 'w')
     program_config['args'] = (
         shlex.split(os.environ.get('DECKARD_WRAPPER', ''))
         + [program_config['binary']]
         + program_config['additional']
     )
     logging.getLogger('deckard.daemon.%s.argv' % name).debug('%s', program_config['args'])
-    try:
-        proc = subprocess.Popen(program_config['args'], stdout=daemon_log_file,
-                                stderr=subprocess.STDOUT, cwd=program_config['WORKING_DIR'])
-    except subprocess.CalledProcessError:
-        logger = logging.getLogger('deckard.daemon_log.%s' % name)
-        logger.exception("Can't start '%s'", program_config['args'])
-        raise
+    with open(program_config['log'], 'w') as daemon_log_file:
+        try:
+            # pylint: disable=consider-using-with
+            proc = subprocess.Popen(program_config['args'], stdout=daemon_log_file,
+                                    stderr=subprocess.STDOUT, cwd=program_config['WORKING_DIR'])
+        except subprocess.CalledProcessError:
+            logger = logging.getLogger('deckard.daemon_log.%s' % name)
+            logger.exception("Can't start '%s'", program_config['args'])
+            raise
     return proc
 
 
diff -pruN 5.4.4-1/tests/integration/deckard/deckard_pytest.py 5.5.1-5/tests/integration/deckard/deckard_pytest.py
--- 5.4.4-1/tests/integration/deckard/deckard_pytest.py	2022-01-05 13:19:16.295753000 +0000
+++ 5.5.1-5/tests/integration/deckard/deckard_pytest.py	2022-06-14 07:17:32.043514300 +0000
@@ -59,6 +59,7 @@ class TCPDump:
     def __enter__(self):
         cmd = self.DUMPCAP_CMD.copy()
         cmd.append(self.config["pcap"])
+        # pylint: disable=consider-using-with
         self.tcpdump = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
 
     def __exit__(self, _, exc_value, __):
diff -pruN 5.4.4-1/tests/integration/deckard/doc/scenario_guide.rst 5.5.1-5/tests/integration/deckard/doc/scenario_guide.rst
--- 5.4.4-1/tests/integration/deckard/doc/scenario_guide.rst	2022-01-05 13:19:16.295753000 +0000
+++ 5.5.1-5/tests/integration/deckard/doc/scenario_guide.rst	2022-06-14 07:17:32.043514300 +0000
@@ -258,7 +258,7 @@ raw_id        query id will be copied in
 do_not_answer no response will be sent at all
 ============= ===========================================================================================
 
-.. [copy_id_bug] https://gitlab.labs.nic.cz/knot/deckard/issues/9
+.. [copy_id_bug] https://gitlab.nic.cz/knot/deckard/issues/9
 
 
 .. _`entry flags`:
diff -pruN 5.4.4-1/tests/integration/deckard/doc/user_guide.rst 5.5.1-5/tests/integration/deckard/doc/user_guide.rst
--- 5.4.4-1/tests/integration/deckard/doc/user_guide.rst	2022-01-05 13:19:16.295753000 +0000
+++ 5.5.1-5/tests/integration/deckard/doc/user_guide.rst	2022-06-14 07:17:32.043514300 +0000
@@ -19,7 +19,7 @@ Let's start with the easiest case:
 
 First run
 ---------
-Easiest way to run Deckard is using one of the prepared Shell scripts in Deckard repository (``{kresd,unbound,pdns}_run.sh`` for Knot Resolver, Unbound and PowerDNS Recursor respectively).
+Easiest way to run Deckard is using one of the prepared Shell scripts in Deckard repository (``{kresd,named,pdns,unbound}_run.sh`` for Knot Resolver, BIND, PowerDNS, and Unbound Recursor respectively).
 
 Deckard uses `pytest` to generate and run the tests as well as collect the results.
 Output is therefore generated by `pytest` as well (``.`` for passed test, ``F`` for failed test and ``s`` for skipped test) and will look something like this:
diff -pruN 5.4.4-1/tests/integration/deckard/mypy.ini 5.5.1-5/tests/integration/deckard/mypy.ini
--- 5.4.4-1/tests/integration/deckard/mypy.ini	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/tests/integration/deckard/mypy.ini	2022-06-14 07:17:32.043514300 +0000
@@ -0,0 +1,3 @@
+[mypy]
+[mypy-yaml.*]
+ignore_missing_imports = True
diff -pruN 5.4.4-1/tests/integration/deckard/named_run.sh 5.5.1-5/tests/integration/deckard/named_run.sh
--- 5.4.4-1/tests/integration/deckard/named_run.sh	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/tests/integration/deckard/named_run.sh	2022-06-14 07:17:32.043514300 +0000
@@ -0,0 +1,11 @@
+#!/bin/bash
+set -o errexit -o nounset
+named -V | grep --quiet -- '--without-jemalloc' || echo 'WARNING: Make sure BIND is compiled without jemalloc library; for 9.17+ use ./configure --without-jemalloc'
+MINOR="$(named -v | cut -d . -f 2)"
+if [[ "$MINOR" -le "13" ]]
+then
+	echo 'WARNING: For BIND <= 9.13.2 manually remove qname-minimization option from named.conf template referenced in configs/named.yaml (usually template/named.j2)'
+fi
+
+RUNDIR="$(dirname "$0")"
+cd "$RUNDIR" && ./run.sh --config configs/named.yaml "$@"
diff -pruN 5.4.4-1/tests/integration/deckard/networking.py 5.5.1-5/tests/integration/deckard/networking.py
--- 5.4.4-1/tests/integration/deckard/networking.py	2022-01-05 13:19:16.295753000 +0000
+++ 5.5.1-5/tests/integration/deckard/networking.py	2022-06-14 07:17:32.043514300 +0000
@@ -2,9 +2,11 @@ import errno
 from ipaddress import IPv4Network, IPv6Network, ip_address
 from socket import AF_INET, AF_INET6
 
+# pylint: disable=no-name-in-module,import-error
 from pyroute2 import IPRoute
 from pyroute2.netlink.rtnl import ndmsg
 from pyroute2.netlink.exceptions import NetlinkError
+# pylint: enable=no-name-in-module,import-error
 
 
 class InterfaceManager:
diff -pruN 5.4.4-1/tests/integration/deckard/pylintrc 5.5.1-5/tests/integration/deckard/pylintrc
--- 5.4.4-1/tests/integration/deckard/pylintrc	2022-01-05 13:19:16.295753000 +0000
+++ 5.5.1-5/tests/integration/deckard/pylintrc	2022-06-14 07:17:32.047514200 +0000
@@ -14,6 +14,7 @@ disable=
     global-statement,
     no-else-return,
     bad-continuation,
+    duplicate-code,
 
 
 [SIMILARITIES]
diff -pruN 5.4.4-1/tests/integration/deckard/README.rst 5.5.1-5/tests/integration/deckard/README.rst
--- 5.4.4-1/tests/integration/deckard/README.rst	2022-01-05 13:19:16.291753000 +0000
+++ 5.5.1-5/tests/integration/deckard/README.rst	2022-06-14 07:17:32.043514300 +0000
@@ -69,9 +69,9 @@ The original test case format is describ
 Contacting us
 -------------
 
-Please report problems to our GitLab: https://gitlab.labs.nic.cz/knot/deckard/issues
+Please report problems to our GitLab: https://gitlab.nic.cz/knot/deckard/issues
 
-If you have any comments feel free to send e-mail to knot-dns@labs.nic.cz! Do not get confused by the name, we are happy if you want to use Deckard with any software.
+If you have any comments feel free to send e-mail to knot-resolver@labs.nic.cz! Do not get confused by the name, we are happy if you want to use Deckard with any software.
 
 Happy testing.
 
@@ -79,7 +79,7 @@ Happy testing.
 .. _`augeas`: http://augeas.net/
 .. _`CSR`: http://apple.stackexchange.com/questions/193368/what-is-the-rootless-feature-in-el-capitan-really
 .. _`Jinja2`: http://jinja.pocoo.org/
-.. _`Knot DNS Resolver`: https://gitlab.labs.nic.cz/knot/resolver/blob/master/README.md
+.. _`Knot Resolver`: https://gitlab.nic.cz/knot/resolver/blob/master/README.md
 .. _`NLnet Labs`: https://www.nlnetlabs.nl/
 .. _`PowerDNS Recursor`: https://doc.powerdns.com/md/recursor/
 .. _`PyYAML`: http://pyyaml.org/
diff -pruN 5.4.4-1/tests/integration/deckard/rplint.py 5.5.1-5/tests/integration/deckard/rplint.py
--- 5.4.4-1/tests/integration/deckard/rplint.py	2022-01-05 13:19:16.295753000 +0000
+++ 5.5.1-5/tests/integration/deckard/rplint.py	2022-06-14 07:17:32.047514200 +0000
@@ -28,10 +28,11 @@ class RplintError(ValueError):
 
 def get_line_number(file: str, char_number: int) -> int:
     pos = 0
-    for number, line in enumerate(open(file)):
-        pos += len(line)
-        if pos >= char_number:
-            return number + 2
+    with open(file) as f:
+        for number, line in enumerate(f):
+            pos += len(line)
+            if pos >= char_number:
+                return number + 2
     return 0
 
 
diff -pruN 5.4.4-1/tests/integration/deckard/sets/resolver/iter_cname_qnamecopy.rpl 5.5.1-5/tests/integration/deckard/sets/resolver/iter_cname_qnamecopy.rpl
--- 5.4.4-1/tests/integration/deckard/sets/resolver/iter_cname_qnamecopy.rpl	2022-01-05 13:19:16.299753200 +0000
+++ 5.5.1-5/tests/integration/deckard/sets/resolver/iter_cname_qnamecopy.rpl	2022-06-14 07:17:32.047514200 +0000
@@ -14,7 +14,7 @@ RANGE_BEGIN 0 100
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 . IN NS
 SECTION ANSWER
@@ -55,7 +55,7 @@ RANGE_BEGIN 0 100
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 com. IN NS
 SECTION ANSWER
@@ -67,7 +67,7 @@ ENTRY_END
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 net. IN NS
 SECTION ANSWER
@@ -81,7 +81,7 @@ ENTRY_END
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 gtld-servers.net. IN NS
 SECTION ANSWER
@@ -102,7 +102,7 @@ ENTRY_END
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 a.gtld-servers.net.	IN 	A
 SECTION ANSWER
@@ -113,7 +113,7 @@ ENTRY_END
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 a.gtld-servers.net.	IN 	AAAA
 SECTION ANSWER
@@ -124,7 +124,7 @@ ENTRY_END
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 root-servers.net. IN NS
 SECTION ANSWER
@@ -146,7 +146,7 @@ ENTRY_END
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 k.root-servers.net.	IN 	A
 SECTION ANSWER
@@ -157,7 +157,7 @@ ENTRY_END
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 k.root-servers.net.	IN 	AAAA
 SECTION AUTHORITY
@@ -197,7 +197,7 @@ RANGE_BEGIN 0 100
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 example.com. IN NS
 SECTION ANSWER
@@ -209,17 +209,17 @@ ENTRY_END
 ENTRY_BEGIN
 MATCH opcode qname qtype
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 ns.example.com.		IN 	A
-SECTION ADDITIONAL
+SECTION ANSWER
 ns.example.com.		IN 	A	1.2.3.4
 ENTRY_END
 
 ENTRY_BEGIN
 MATCH opcode qname qtype
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 ns.example.com.		IN 	AAAA
 SECTION ADDITIONAL
@@ -258,7 +258,7 @@ ENTRY_END
 ENTRY_BEGIN
 MATCH opcode qtype qname
 ADJUST copy_id
-REPLY QR NOERROR
+REPLY QR AA NOERROR
 SECTION QUESTION
 next.com. IN NS
 SECTION ANSWER
@@ -266,6 +266,29 @@ next.com.	IN NS	ns.next.com.
 SECTION ADDITIONAL
 ns.next.com.		IN 	A	1.2.3.5
 ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+ns.next.com. IN A
+SECTION ANSWER
+ns.next.com.		IN 	A	1.2.3.5
+ENTRY_END
+
+ENTRY_BEGIN
+MATCH opcode qtype qname
+ADJUST copy_id
+REPLY QR AA NOERROR
+SECTION QUESTION
+ns.next.com. IN AAAA
+SECTION ANSWER
+SECTION AUTHORITY
+next.com. 	IN SOA next.com. next.com. 2007090400 28800 7200 604800 18000
+SECTION ADDITIONAL
+ENTRY_END
+
 RANGE_END
 
 STEP 1 QUERY
diff -pruN 5.4.4-1/tests/integration/deckard/sets/resolver/val_dname_bogus.rpl 5.5.1-5/tests/integration/deckard/sets/resolver/val_dname_bogus.rpl
--- 5.4.4-1/tests/integration/deckard/sets/resolver/val_dname_bogus.rpl	2022-01-05 13:19:16.311753300 +0000
+++ 5.5.1-5/tests/integration/deckard/sets/resolver/val_dname_bogus.rpl	2022-06-14 07:17:32.055514300 +0000
@@ -4,7 +4,7 @@ do-ip6: no
 trust-anchor: ".	IN	DS	37471 5 1 da74e4e0fe4067c2afd1d4a3cceb852a3c0d4401"
 stub-addr: 193.0.14.129         # K.ROOT-SERVERS.NET.
 val-override-date: "20170301000000"
-domain-insecure: net.
+query-minimization: off # missing net. NS proof for NODATA, so we'd need to resign everything
 CONFIG_END
 
 SCENARIO_BEGIN Test DNAME validation
@@ -12,28 +12,6 @@ SCENARIO_BEGIN Test DNAME validation
 ; all the data are on the "root servers"
 RANGE_BEGIN 0 10000000
 	ADDRESS 193.0.14.129
-
-ENTRY_BEGIN
-MATCH qname qtype
-ADJUST copy_id copy_query
-REPLY QR AA NOERROR
-SECTION QUESTION
-net. IN NS
-SECTION ANSWER
-net. 3600 IN NS K.ROOT-SERVERS.NET.
-ENTRY_END
-
-ENTRY_BEGIN
-MATCH qname qtype
-ADJUST copy_id copy_query
-REPLY QR AA NOERROR
-SECTION QUESTION
-root-servers.net. IN NS
-SECTION AUTHORITY
-.       86400   IN      SOA     . . 2017021500 1800 900 604800 86400
-.       86400   IN      RRSIG   SOA 5 0 86400 20170315140518 20170215140518 37471 . drrv7SjrOkuNwlILiziPxHTuIKs/tO2WcVEdipA/LNkt0h09zuWbr3Rk5gtEDTSECbZEXYTa4YaeJs3ODmikzVaJd5EVLsDdGnV3mZ/w7WYHA0Uc1GH5HZm1uQwA4DlwY5e5Ry80pIhInZ1Lqiz1ut9yWbHzODdcUOdpE+XiPzYCKR1hRWi099dIQtDhZYottvQNXXmsJDY41PwvWaxqbXGYgiQCX3cN/W5PM0hs7xMxAjanKh32PXKcHSfTeko87BvERMZnibc2O8efl7S62Zp68Q4guMfe4P++ue22PctjwfeR5nDi31c3+USi63ujrKSDGujaIsIMyIHNFm1/zQ==
-ENTRY_END
-
 ENTRY_BEGIN
 MATCH qname qtype opcode
 ADJUST copy_id
diff -pruN 5.4.4-1/tests/integration/deckard/sets/resolver/zone.rpz 5.5.1-5/tests/integration/deckard/sets/resolver/zone.rpz
--- 5.4.4-1/tests/integration/deckard/sets/resolver/zone.rpz	2022-01-05 13:19:16.319753400 +0000
+++ 5.5.1-5/tests/integration/deckard/sets/resolver/zone.rpz	1970-01-01 00:00:00.000000000 +0000
@@ -1,10 +0,0 @@
-$TTL 30
-@        SOA nonexistent.nodomain.none. dummy.nodomain.none. 1 12h 15m 3w 2h
-         NS  nonexistant.nodomain.none.
-
-example.cz CNAME .
-*.example.cz CNAME *.
-nic.cz CNAME rpz-drop.
-*.nic.cz CNAME rpz-tcp-only.
-example.com CNAME rpz-passthru.
-	
diff -pruN 5.4.4-1/tests/integration/deckard/setup.py 5.5.1-5/tests/integration/deckard/setup.py
--- 5.4.4-1/tests/integration/deckard/setup.py	2022-01-05 13:19:16.319753400 +0000
+++ 5.5.1-5/tests/integration/deckard/setup.py	2022-06-14 07:17:32.063514500 +0000
@@ -14,7 +14,7 @@ setup(
     author='CZ.NIC',
     author_email='knot-dns-users@lists.nic.cz',
     license='BSD',
-    url='https://gitlab.labs.nic.cz/knot/deckard',
+    url='https://gitlab.nic.cz/knot/deckard',
     packages=['pydnstest'],
     python_requires='>=3.5',
     install_requires=[
diff -pruN 5.4.4-1/tests/integration/deckard/template/kresd.j2 5.5.1-5/tests/integration/deckard/template/kresd.j2
--- 5.4.4-1/tests/integration/deckard/template/kresd.j2	2022-01-05 13:19:16.319753400 +0000
+++ 5.5.1-5/tests/integration/deckard/template/kresd.j2	2022-06-14 07:17:32.063514500 +0000
@@ -9,8 +9,7 @@ net.bufsize(4096)
 
 modules = {'stats', 'policy', 'hints'}
 
--- extra verbose logging for all operations
-verbose(true)
+-- trace logging for all requests
 policy.add(policy.all(policy.DEBUG_ALWAYS))
 
 -- test. domain is used by some tests, allow it
diff -pruN 5.4.4-1/tests/integration/deckard/template/named.j2 5.5.1-5/tests/integration/deckard/template/named.j2
--- 5.4.4-1/tests/integration/deckard/template/named.j2	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/tests/integration/deckard/template/named.j2	2022-06-14 07:17:32.063514500 +0000
@@ -0,0 +1,106 @@
+options {
+	querylog yes;
+
+{% if ':' in SELF_ADDR %}
+	listen-on-v6 { {{SELF_ADDR}}; };
+	query-source-v6 address {{SELF_ADDR}};
+{% else %}
+	listen-on { {{SELF_ADDR}}; };
+	query-source address {{SELF_ADDR}};
+{% endif %}
+
+	edns-udp-size 4096;
+	max-cache-size 2097152;
+
+{% if QMIN == "false" %}
+qname-minimization off;
+{% else %}
+qname-minimization strict;
+{% endif %}
+
+	// Disable RFC8145 signaling, scenario doesn't provide expected ansers
+	trust-anchor-telemetry no;
+
+{% if not TRUST_ANCHOR_FILES %}
+	dnssec-validation no;
+{% else %}
+unsupported as of yet
+-- make sure that value specified at compile-time does not break tests
+{% for TAF in TRUST_ANCHOR_FILES %}
+trust_anchors.add_file('{{TAF}}')
+{% endfor %}
+{% endif %}
+
+{% if NEGATIVE_TRUST_ANCHORS %}
+unsupported as of yet
+validate-except {
+{% for DI in NEGATIVE_TRUST_ANCHORS %}
+{{DI}}
+{% endfor %}
+};
+{% endif %}
+
+};
+
+
+{% if FORWARD_ADDR %}
+zone "." {
+	type forward;
+	forward only;
+	forwarders { {{FORWARD_ADDR}}; };
+};
+{% endif %}
+
+zone "." {
+	type hint;
+	file "hints.zone";
+};
+
+
+{% if DO_NOT_QUERY_LOCALHOST == "false" %}
+{% endif %}
+
+{% if HARDEN_GLUE == "true" %}
+{% endif %}
+
+
+{% if DO_IP6 == "true" %}
+{% else %}
+server ::/0 {
+	bogus true;
+};
+{% endif %}
+
+{% if DO_IP4 == "true" %}
+{% else %}
+server 0.0.0.0/0 {
+	bogus true;
+};
+{% endif %}
+
+{% if FEATURES.min_ttl is defined %}
+min-cache-ttl {FEATURES.min_ttl}};
+min-ncache-ttl {FEATURES.min_ttl}};
+{% endif %}
+
+{% if FEATURES.max_ttl is defined %}
+max-cache-ttl {{FEATURES.max_ttl}};
+{% endif %}
+
+{% if FEATURES.dns64_prefix is defined %}
+// dns64.config('{{FEATURES.dns64_prefix}}')
+{% endif %}
+
+{% if FEATURES.static_hint_name is defined %}
+static hint unsupported
+{% endif %}
+
+logging {
+	category resolver {
+		stderr;
+	};
+	channel stderr {
+		stderr;
+		severity debug 10;
+	};
+};
diff -pruN 5.4.4-1/tests/integration/meson.build 5.5.1-5/tests/integration/meson.build
--- 5.4.4-1/tests/integration/meson.build	2022-01-05 13:19:14.507728300 +0000
+++ 5.5.1-5/tests/integration/meson.build	2022-06-14 07:17:30.435491300 +0000
@@ -2,7 +2,7 @@
 # SPDX-License-Identifier: GPL-3.0-or-later
 
 # deckard dependencies
-cmake = find_program('cmake')  # for libswrapper
+faketime = find_program('faketime')
 git = find_program('git')
 make = find_program('make')
 augeas = dependency('augeas')
@@ -11,7 +11,9 @@ augeas = dependency('augeas')
 py3_deps += [
   ['augeas', 'augeas (for deckard)'],
   ['dns', 'dnspython (for deckard)'],
+  ['dpkt', 'dpkt (for deckard)'],
   ['jinja2', 'jinja2 (for deckard)'],
+  ['pyroute2', 'pyroute2 (for deckard)'],
   ['pytest', 'pytest (for deckard)'],
   ['xdist', 'pytest-xdist (for deckard)'],
   ['yaml', 'PyYAML (for deckard)'],
diff -pruN 5.4.4-1/tests/meson.build 5.5.1-5/tests/meson.build
--- 5.4.4-1/tests/meson.build	2022-01-05 13:19:14.507728300 +0000
+++ 5.5.1-5/tests/meson.build	2022-06-14 07:17:30.439491300 +0000
@@ -22,15 +22,15 @@ endif
 ## config tests
 if build_config_tests
   message('--- config_tests dependencies ---')
-  cqueues = run_command('luajit', '-l', 'cqueues', '-e', 'os.exit(0)')  # luajit -l $(1) -e "os.exit(0)"
+  cqueues = run_command('luajit', '-l', 'cqueues', '-e', 'os.exit(0)', check: false)  # luajit -l $(1) -e "os.exit(0)"
   if cqueues.returncode() != 0
     error('missing luajit package: cqueues')
   endif
-  basexx = run_command('luajit', '-l', 'basexx', '-e', 'os.exit(0)')  # luajit -l $(1) -e "os.exit(0)"
+  basexx = run_command('luajit', '-l', 'basexx', '-e', 'os.exit(0)', check: false)  # luajit -l $(1) -e "os.exit(0)"
   if basexx.returncode() != 0
     error('missing luajit package: basexx')
   endif
-  ffi = run_command('luajit', '-l', 'ffi', '-e', 'os.exit(0)')  # luajit -l $(1) -e "os.exit(0)"
+  ffi = run_command('luajit', '-l', 'ffi', '-e', 'os.exit(0)', check: false)  # luajit -l $(1) -e "os.exit(0)"
   if ffi.returncode() != 0
     error('missing luajit package: ffi')
   endif
@@ -52,7 +52,7 @@ if build_extra_tests
   endif
 
   foreach py3_dep : py3_deps
-    py3_import = run_command(python3, '-c', 'import @0@'.format(py3_dep[0]))
+    py3_import = run_command(python3, '-c', 'import @0@'.format(py3_dep[0]), check: false)
     if py3_import.returncode() != 0
       error('missing python3 dependency: @0@'.format(py3_dep[1]))
     endif
diff -pruN 5.4.4-1/tests/packaging/test_packaging.py 5.5.1-5/tests/packaging/test_packaging.py
--- 5.4.4-1/tests/packaging/test_packaging.py	2022-01-05 13:19:14.507728300 +0000
+++ 5.5.1-5/tests/packaging/test_packaging.py	2022-06-14 07:17:30.439491300 +0000
@@ -150,15 +150,33 @@ class DockerImages(ABC):
 
         dockerf.close()
 
+    def build_printing_errors(self, path, dockerfile, network_mode, tag, rm):
+        try:
+            return client.images.build(path=path, dockerfile=dockerfile,
+                                       network_mode=network_mode, tag=tag, rm=rm)
+        except docker.errors.BuildError as e:
+            iterable = iter(e.build_log)
+            while True:
+                try:
+                    item = next(iterable)
+                    if item['stream']:
+                        for l in item['stream'].splitlines():
+                            stripped = l.strip()
+                            if stripped:
+                                logging.error(stripped)
+                except StopIteration:
+                    break
+            raise e
+
     def build(self, tmpdir, tag="", from_image=None):
         self.__genDockerFile(tmpdir, from_image=from_image)
 
         logger.debug('tmpdir={}'.format(tmpdir))
         logger.debug('datadir={}'.format(pytest.KR_ROOT_DIR))
         logger.debug('tag={}'.format(tag))
-        image = client.images.build(path=str(pytest.KR_ROOT_DIR),
-                                    dockerfile=os.path.join(tmpdir, 'Dockerfile-build'),
-                                    network_mode='host', tag=tag, rm=True)
+        image = self.build_printing_errors(path=str(pytest.KR_ROOT_DIR),
+                                           dockerfile=os.path.join(tmpdir, 'Dockerfile-build'),
+                                           network_mode='host', tag=tag, rm=True)
         logger.info('"Build image" ID={} created'.format(image[0].short_id))
         self.build_id = image[0].short_id
         return self.build_id
@@ -169,9 +187,9 @@ class DockerImages(ABC):
         logger.debug('tmpdir={}'.format(tmpdir))
         logger.debug('datadir={}'.format(tmpdir))
         logger.debug('tag={}'.format(tag))
-        image = client.images.build(path=str(tmpdir),
-                                    dockerfile=os.path.join(tmpdir, 'Dockerfile-run'),
-                                    network_mode='host', tag=tag, rm=True)
+        image = self.build_printing_errors(path=str(tmpdir),
+                                           dockerfile=os.path.join(tmpdir, 'Dockerfile-run'),
+                                           network_mode='host', tag=tag, rm=True)
         logger.info('"Run image" ID={} created'.format(image[0].short_id))
         self.run_id = image[0].short_id
         return self.run_id
diff -pruN 5.4.4-1/tests/pytests/templates/kresd.conf.j2 5.5.1-5/tests/pytests/templates/kresd.conf.j2
--- 5.4.4-1/tests/pytests/templates/kresd.conf.j2	2022-01-05 13:19:14.511728500 +0000
+++ 5.5.1-5/tests/pytests/templates/kresd.conf.j2	2022-06-14 07:17:30.439491300 +0000
@@ -46,6 +46,11 @@ policy.add(policy.all(
 policy.add(policy.suffix(policy.PASS, {todname('test.')}))
 {% endif %}
 
+-- EDNS EDE tests
+policy.add(policy.suffix(policy.DENY, {todname('deny.test.')}))
+policy.add(policy.suffix(policy.REFUSE, {todname('refuse.test.')}))
+policy.add(policy.suffix(policy.ANSWER({ [kres.type.A] = { rdata=kres.str2ip('192.0.2.7'), ttl=300 } }), {todname('forge.test.')}))
+
 -- make sure DNSSEC is turned off for tests
 trust_anchors.remove('.')
 modules.unload("ta_update")
diff -pruN 5.4.4-1/tests/pytests/test_edns.py 5.5.1-5/tests/pytests/test_edns.py
--- 5.4.4-1/tests/pytests/test_edns.py	1970-01-01 00:00:00.000000000 +0000
+++ 5.5.1-5/tests/pytests/test_edns.py	2022-06-14 07:17:30.439491300 +0000
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-3.0-or-later
+"""EDNS tests"""
+
+import dns
+import pytest
+
+import utils
+
+
+@pytest.mark.parametrize('dname, code, text', [
+    ('deny.test.', dns.edns.EDECode.BLOCKED, 'CR36'),
+    ('refuse.test.', dns.edns.EDECode.PROHIBITED, 'EIM4'),
+    ('forge.test.', dns.edns.EDECode.FORGED_ANSWER, '5DO5'),
+])
+def test_edns_ede(kresd_sock, dname, code, text):
+    """Check that kresd responds with EDNS EDE codes in selected cases."""
+    buff, msgid = utils.get_msgbuff(dname)
+    kresd_sock.sendall(buff)
+    answer = utils.receive_parse_answer(kresd_sock)
+    assert answer.id == msgid
+    assert answer.options[0].code == code
+    assert answer.options[0].text == text
diff -pruN 5.4.4-1/tests/pytests/utils.py 5.5.1-5/tests/pytests/utils.py
--- 5.4.4-1/tests/pytests/utils.py	2022-01-05 13:19:14.511728500 +0000
+++ 5.5.1-5/tests/pytests/utils.py	2022-06-14 07:17:30.439491300 +0000
@@ -50,7 +50,7 @@ def prepare_wire(
         qclass=dns.rdataclass.IN,
         msgid=None):
     """Utility function to generate DNS wire format message"""
-    msg = dns.message.make_query(qname, qtype, qclass)
+    msg = dns.message.make_query(qname, qtype, qclass, use_edns=True)
     if msgid is not None:
         msg.id = msgid
     return msg.to_wire(), msg.id
diff -pruN 5.4.4-1/tests/README.rst 5.5.1-5/tests/README.rst
--- 5.4.4-1/tests/README.rst	2022-01-05 13:19:14.503728400 +0000
+++ 5.5.1-5/tests/README.rst	2022-06-14 07:17:30.435491300 +0000
@@ -1,31 +1,94 @@
 .. SPDX-License-Identifier: GPL-3.0-or-later
 
+Tests
+-----
+
+The following is a non-comprehensitve lists of various tests that can be found
+in this repo. These can be enabled by the build system.
+
 Unit tests
-==========
+~~~~~~~~~~
 
-The unit tests depend on cmocka_.
+The unit tests depend on cmocka_ and can easily be executed after compilation.
+They are enabled by default (if ``cmocka`` is found).
 
 .. code-block:: bash
 
-	$ make check
+        $ ninja -C build_dir
+        $ meson test -C build_dir --suite unit
+
+Postinstall tests
+~~~~~~~~~~~~~~~~~
+
+There following tests require a working installation of kresd.  The
+binary ``kresd`` found in ``$PATH`` will be tested. When testing through meson,
+``$PATH`` is modified automatically and you just need to make sure to install
+kresd first.
 
-.. todo:: Writing tests.
+.. code-block:: bash
+
+        $ ninja install -C build_dir
 
-Integration tests
-=================
+Config tests
+~~~~~~~~~~~~
 
-The integration tests are using Deckard, the `DNS test harness <deckard>`_.
-It requires Jinja2_ and Python, `socket_wrapper`_, libfaketime_ are embedded in the build (cmake is required for `socket_wrapper`_).
+Config tests utilize the kresd's lua config file to execute arbitrary tests,
+typically testing various modules, their API etc.
 
-Execute the tests by:
+To enable these tests, specify ``-Dconfig_tests=enabled`` option for meson.
+Multiple dependencies are required (refer to meson's output when configuring
+the build dir).
 
 .. code-block:: bash
 
-	$ make check-integration
+        $ meson configure build_dir -Dconfig_tests=enabled
+        $ ninja install -C build_dir
+        $ meson test -C build_dir --suite config
+
+Extra tests
+~~~~~~~~~~~
+
+The extra tests require a large set of additional dependencies and executing
+them outside of upstream development is probably redundant.
+
+To enable these tests, specify ``-Dextra_tests=enabled`` option for meson.
+Multiple dependencies are required (refer to meson's output when configuring
+the build dir). Enabling ``extra_tests`` automatically enables config tests as
+well.
+
+**Integration tests**
+
+The integration tests are using Deckard, the `DNS test harness
+<https://gitlab.nic.cz/knot/deckard>`_. The tests simulate specific DNS
+scenarios, including authoritative server and their responses. These tests rely
+on linux namespaces, refer to Deckard documentation for more info.
+
+.. code-block:: bash
 
-See deckard_ documentation on how to write additional tests.
+        $ meson configure build_dir -Dextra_tests=enabled
+        $ ninja install -C build_dir
+        $ meson test -C build_dir --suite integration
+
+**Pytests**
+
+The pytest suite is designed to spin up a kresd instance, acquire a connected
+socket, and then performs any tests on it. These tests are used to test for
+example TCP, TLS and its connection management.
+
+.. code-block:: bash
+
+        $ meson configure build_dir -Dextra_tests=enabled
+        $ ninja install -C build_dir
+        $ meson test -C build_dir --suite pytests
+
+Useful meson commands
+~~~~~~~~~~~~~~~~~~~~~
+
+It's possible to run only specific test suite or a test.
+
+.. code-block:: bash
 
-.. _cmocka: https://cmocka.org/
-.. _`socket_wrapper`: https://cwrap.org/socket_wrapper.html
-.. _`libfaketime`: https://github.com/wolfcw/libfaketime
-.. _deckard: https://gitlab.nic.cz/knot/deckard
+   $ meson test -C build_dir --help
+   $ meson test -C build_dir --list
+   $ meson test -C build_dir --no-suite postinstall
+   $ meson test -C build_dir integration.serve_stale
diff -pruN 5.4.4-1/utils/cache_gc/db.c 5.5.1-5/utils/cache_gc/db.c
--- 5.4.4-1/utils/cache_gc/db.c	2022-01-05 13:19:14.511728500 +0000
+++ 5.5.1-5/utils/cache_gc/db.c	2022-06-14 07:17:30.443491200 +0000
@@ -200,7 +200,7 @@ int kr_gc_cache_iter(knot_db_t * knot_db
 
 		info.entry_size = key.len + val.len;
 		info.valid = false;
-		const int entry_type = ret == KNOT_EOK ? kr_gc_key_consistent(key) : -1;
+		const int entry_type = kr_gc_key_consistent(key);
 		const struct entry_h *entry = NULL;
 		if (entry_type >= 0) {
 			counter_gc_consistent++;
diff -pruN 5.4.4-1/utils/cache_gc/kr_cache_gc.c 5.5.1-5/utils/cache_gc/kr_cache_gc.c
--- 5.4.4-1/utils/cache_gc/kr_cache_gc.c	2022-01-05 13:19:14.511728500 +0000
+++ 5.5.1-5/utils/cache_gc/kr_cache_gc.c	2022-06-14 07:17:30.443491200 +0000
@@ -115,6 +115,7 @@ int cb_delete_categories(const knot_db_v
 		if ((ctx->cfg_temp_keys_space > 0 &&
 		     used > ctx->cfg_temp_keys_space) || todelete == NULL) {
 			ctx->oversize_records++;
+			free(todelete);
 		} else {
 			entry_dynarray_add(&ctx->to_delete, &todelete);
 			ctx->used_space = used;
diff -pruN 5.4.4-1/utils/client/kresc.c 5.5.1-5/utils/client/kresc.c
--- 5.4.4-1/utils/client/kresc.c	2022-01-05 13:19:14.511728500 +0000
+++ 5.5.1-5/utils/client/kresc.c	2022-06-14 07:17:30.443491200 +0000
@@ -37,7 +37,7 @@ bool starts_with(const char *a, const ch
 }
 
 //! Returns Lua name of type of value, NULL on error. Puts length of type in name_len;
-const char *get_type_name(const char *value)
+char *get_type_name(const char *value)
 {
 	if (value == NULL) {
 		return NULL;
@@ -51,7 +51,6 @@ const char *get_type_name(const char *va
 	}
 
 	char *cmd = afmt("type(%s)", value);
-
 	if (!cmd) {
 		perror("While tab-completing.");
 		return NULL;
@@ -61,16 +60,15 @@ const char *get_type_name(const char *va
 	char *type = run_cmd(cmd, &name_len);
 	if (!type) {
 		return NULL;
-	} else {
-		free(cmd);
 	}
+	free(cmd);
 
 	if (starts_with(type, "[")) {
 		//Return "nil" on non-valid name.
 		free(type);
-		return "nil";
+		return strdup("nil");
 	} else {
-		type[(strlen(type)) - 1] = '\0';
+		type[strlen(type) - 1] = '\0';
 		return type;
 	}
 }
@@ -145,7 +143,7 @@ static void complete_members(EditLine *
 		} else {
 			//Print members matching the current line.
 			while (token) {
-				if (str && starts_with(token, dot + 1)) {
+				if (starts_with(token, dot + 1)) {
 					const char *member_type =
 					    get_type_name(afmt
 							  ("%s.%s", table,
@@ -204,7 +202,9 @@ static void complete_globals(EditLine *
 	char *lastmatch = NULL;
 	while (token) {
 		if (str && starts_with(token, str)) {
-			printf("\n%s (%s)", token, get_type_name(token));
+			char *name = get_type_name(token);
+			printf("\n%s (%s)", token, name);
+			free(name);
 			lastmatch = token;
 			matches++;
 		}
@@ -332,6 +332,8 @@ static char *run_cmd(const char *cmd, si
 	if (!fread(&len, sizeof(len), 1, g_tty))
 		return NULL;
 	len = ntohl(len);
+	if (!len)
+		return NULL;
 	char *msg = malloc(1 + (size_t) len);
 	if (!msg)
 		return NULL;
