summaryrefslogtreecommitdiffstats
path: root/debian
diff options
context:
space:
mode:
Diffstat (limited to 'debian')
-rw-r--r--debian/changelog8
-rw-r--r--debian/gbp.conf2
-rw-r--r--debian/patches/0001-validator-lower-the-NSEC3-iteration-limit-150-50.patch32
-rw-r--r--debian/patches/0002-validator-similarly-also-limit-excessive-NSEC3-salt-.patch143
-rw-r--r--debian/patches/0003-lib-cache-limit-the-amount-of-work-on-SHA1.patch60
-rw-r--r--debian/patches/0004-validator-limit-the-amount-of-work-on-SHA1-in-NSEC3-.patch31
-rw-r--r--debian/patches/0005-validator-refuse-to-validate-answers-with-more-than-.patch37
-rw-r--r--debian/patches/0006-validator-compatibility-with-older-libknot-versions.patch22
-rw-r--r--debian/patches/0007-lib-cache-bump-CACHE_VERSION.patch25
-rw-r--r--debian/patches/0008-lib-dnssec-kr_rrset_validate_with_key-deduplicate-cl.patch43
-rw-r--r--debian/patches/0009-lib-resolve-kr_request_set_extended_error-tweak-prio.patch25
-rw-r--r--debian/patches/0010-mitigate-KeyTrap-DoS-CVE-2023-50387.patch233
-rw-r--r--debian/patches/0011-mitigate-KeyTrap-DoS-CVE-2023-50387.patch80
-rw-r--r--debian/patches/0012-lib-dnssec-fix-imprecise-assertion.patch22
-rw-r--r--debian/patches/0013-daemon-more-avoidance-of-excessive-TCP-reconnections.patch153
-rw-r--r--debian/patches/series13
-rw-r--r--debian/salsa-ci.yml3
17 files changed, 931 insertions, 1 deletions
diff --git a/debian/changelog b/debian/changelog
index 40af4db..fb141be 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,11 @@
+knot-resolver (5.6.0-1+deb12u1) bookworm-security; urgency=medium
+
+ * Backport upstream fixes for CVE-2023-50868 "NSEC3"
+ * Backport upstream fixes for CVE-2023-50387 "KeyTrap"
+ * Backport upstream fix for CVE-2023-46317
+
+ -- Jakub Ružička <jakub.ruzicka@nic.cz> Fri, 23 Feb 2024 13:16:49 +0100
+
knot-resolver (5.6.0-1) unstable; urgency=medium
* New upstream release
diff --git a/debian/gbp.conf b/debian/gbp.conf
index ff17343..5b24517 100644
--- a/debian/gbp.conf
+++ b/debian/gbp.conf
@@ -1,5 +1,5 @@
[DEFAULT]
-debian-branch = debian/master
+debian-branch = debian/bookworm
debian-tag = debian/%(version)s
upstream-branch = upstream
upstream-tag = upstream/%(version)s
diff --git a/debian/patches/0001-validator-lower-the-NSEC3-iteration-limit-150-50.patch b/debian/patches/0001-validator-lower-the-NSEC3-iteration-limit-150-50.patch
new file mode 100644
index 0000000..90137eb
--- /dev/null
+++ b/debian/patches/0001-validator-lower-the-NSEC3-iteration-limit-150-50.patch
@@ -0,0 +1,32 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Tue, 2 Jan 2024 10:05:28 +0100
+Subject: validator: lower the NSEC3 iteration limit (150 -> 50)
+
+Also done by BIND9 >= 9.19.19:
+https://gitlab.isc.org/isc-projects/bind9/-/merge_requests/8515
+
+The latest real-life measurements show that values above 50 are rare:
+https://chat.dns-oarc.net/community/pl/aadp9wwrp7g7ux1b8chbzebmze
+---
+ lib/dnssec/nsec3.h | 7 ++-----
+ 1 file changed, 2 insertions(+), 5 deletions(-)
+
+diff --git a/lib/dnssec/nsec3.h b/lib/dnssec/nsec3.h
+index eb0bd39..723dc4a 100644
+--- a/lib/dnssec/nsec3.h
++++ b/lib/dnssec/nsec3.h
+@@ -11,12 +11,9 @@
+ * ...so we avoid doing all the work. The value is a current compromise;
+ * zones shooting over get downgraded to insecure status.
+ *
+- * Original restriction wasn't that strict:
+- https://datatracker.ietf.org/doc/html/rfc5155#section-10.3
+- * but there is discussion about officially lowering the limits:
+- https://tools.ietf.org/id/draft-hardaker-dnsop-nsec3-guidance-02.html#section-2.3
++ https://datatracker.ietf.org/doc/html/rfc9276#name-recommendation-for-validati
+ */
+-#define KR_NSEC3_MAX_ITERATIONS 150
++#define KR_NSEC3_MAX_ITERATIONS 50
+
+ /**
+ * Name error response check (RFC5155 7.2.2).
diff --git a/debian/patches/0002-validator-similarly-also-limit-excessive-NSEC3-salt-.patch b/debian/patches/0002-validator-similarly-also-limit-excessive-NSEC3-salt-.patch
new file mode 100644
index 0000000..2225c90
--- /dev/null
+++ b/debian/patches/0002-validator-similarly-also-limit-excessive-NSEC3-salt-.patch
@@ -0,0 +1,143 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Tue, 2 Jan 2024 11:18:31 +0100
+Subject: validator: similarly also limit excessive NSEC3 salt length
+
+Limit combination of iterations and salt length, based on estimated
+expense of the computation. Note that the result only differs for
+salt length > 44 which is rather nonsensical and very rare:
+https://chat.dns-oarc.net/community/pl/h58qx9sjkbgt9dajb7x988p78a
+---
+ lib/cache/api.c | 2 +-
+ lib/cache/nsec3.c | 2 +-
+ lib/dnssec/nsec3.c | 4 ++--
+ lib/dnssec/nsec3.h | 32 ++++++++++++++++++++++++++++----
+ lib/layer/validate.c | 7 ++++---
+ 5 files changed, 36 insertions(+), 11 deletions(-)
+
+diff --git a/lib/cache/api.c b/lib/cache/api.c
+index 116d775..bb627ea 100644
+--- a/lib/cache/api.c
++++ b/lib/cache/api.c
+@@ -500,7 +500,7 @@ static ssize_t stash_rrset(struct kr_cache *cache, const struct kr_query *qry,
+ return kr_ok();
+ }
+ if (rr->type == KNOT_RRTYPE_NSEC3 && rr->rrs.count
+- && knot_nsec3_iters(rr->rrs.rdata) > KR_NSEC3_MAX_ITERATIONS) {
++ && kr_nsec3_limited_rdata(rr->rrs.rdata)) {
+ /* This shouldn't happen often, thanks to downgrades during validation. */
+ VERBOSE_MSG(qry, "=> skipping NSEC3 with too many iterations\n");
+ return kr_ok();
+diff --git a/lib/cache/nsec3.c b/lib/cache/nsec3.c
+index 0b70775..9832630 100644
+--- a/lib/cache/nsec3.c
++++ b/lib/cache/nsec3.c
+@@ -84,7 +84,7 @@ static knot_db_val_t key_NSEC3_name(struct key *k, const knot_dname_t *name,
+ .data = (uint8_t *)/*const-cast*/name,
+ };
+
+- if (kr_fails_assert(nsec_p->libknot.iterations <= KR_NSEC3_MAX_ITERATIONS)) {
++ if (kr_fails_assert(!kr_nsec3_limited_params(&nsec_p->libknot))) {
+ /* This is mainly defensive; it shouldn't happen thanks to downgrades. */
+ return VAL_EMPTY;
+ }
+diff --git a/lib/dnssec/nsec3.c b/lib/dnssec/nsec3.c
+index 037d5bd..e4d314b 100644
+--- a/lib/dnssec/nsec3.c
++++ b/lib/dnssec/nsec3.c
+@@ -71,7 +71,7 @@ static int hash_name(dnssec_binary_t *hash, const dnssec_nsec3_params_t *params,
+ return kr_error(EINVAL);
+ if (!name)
+ return kr_error(EINVAL);
+- if (kr_fails_assert(params->iterations <= KR_NSEC3_MAX_ITERATIONS)) {
++ if (kr_fails_assert(!kr_nsec3_limited_params(params))) {
+ /* This if is mainly defensive; it shouldn't happen. */
+ return kr_error(EINVAL);
+ }
+@@ -565,7 +565,7 @@ int kr_nsec3_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_
+ const knot_rrset_t *rrset = knot_pkt_rr(sec, i);
+ if (rrset->type != KNOT_RRTYPE_NSEC3)
+ continue;
+- if (knot_nsec3_iters(rrset->rrs.rdata) > KR_NSEC3_MAX_ITERATIONS) {
++ if (kr_nsec3_limited_rdata(rrset->rrs.rdata)) {
+ /* Avoid hashing with too many iterations.
+ * If we get here, the `sname` wildcard probably ends up bogus,
+ * but it gets downgraded to KR_RANK_INSECURE when validator
+diff --git a/lib/dnssec/nsec3.h b/lib/dnssec/nsec3.h
+index 723dc4a..76ef2e9 100644
+--- a/lib/dnssec/nsec3.h
++++ b/lib/dnssec/nsec3.h
+@@ -5,15 +5,39 @@
+ #pragma once
+
+ #include <libknot/packet/pkt.h>
++#include <libknot/rrtype/nsec3.h>
++#include <libdnssec/nsec.h>
++
++
++static inline unsigned int kr_nsec3_price(unsigned int iterations, unsigned int salt_len)
++{
++ // SHA1 works on 64-byte chunks.
++ // On iterating we hash the salt + 20 bytes of the previous hash.
++ int chunks_per_iter = (20 + salt_len - 1) / 64 + 1;
++ return (iterations + 1) * chunks_per_iter;
++}
+
+ /** High numbers in NSEC3 iterations don't really help security
+ *
+- * ...so we avoid doing all the work. The value is a current compromise;
+- * zones shooting over get downgraded to insecure status.
++ * ...so we avoid doing all the work. The limit is a current compromise;
++ * answers using NSEC3 over kr_nsec3_limited* get downgraded to insecure status.
+ *
+ https://datatracker.ietf.org/doc/html/rfc9276#name-recommendation-for-validati
+ */
+-#define KR_NSEC3_MAX_ITERATIONS 50
++static inline bool kr_nsec3_limited(unsigned int iterations, unsigned int salt_len)
++{
++ const int MAX_ITERATIONS = 50; // limit with short salt length
++ return kr_nsec3_price(iterations, salt_len) > MAX_ITERATIONS + 1;
++}
++static inline bool kr_nsec3_limited_rdata(const knot_rdata_t *rd)
++{
++ return kr_nsec3_limited(knot_nsec3_iters(rd), knot_nsec3_salt_len(rd));
++}
++static inline bool kr_nsec3_limited_params(const dnssec_nsec3_params_t *params)
++{
++ return kr_nsec3_limited(params->iterations, params->salt.size);
++}
++
+
+ /**
+ * Name error response check (RFC5155 7.2.2).
+@@ -36,7 +60,7 @@ int kr_nsec3_name_error_response_check(const knot_pkt_t *pkt, knot_section_t sec
+ * KNOT_ERANGE - NSEC3 RR that covers a wildcard
+ * has been found, but has opt-out flag set;
+ * otherwise - error.
+- * Records over KR_NSEC3_MAX_ITERATIONS are skipped, so you probably get kr_error(ENOENT).
++ * Too expensive NSEC3 records are skipped, so you probably get kr_error(ENOENT).
+ */
+ int kr_nsec3_wildcard_answer_response_check(const knot_pkt_t *pkt, knot_section_t section_id,
+ const knot_dname_t *sname, int trim_to_next);
+diff --git a/lib/layer/validate.c b/lib/layer/validate.c
+index 93f1d4f..5fea99d 100644
+--- a/lib/layer/validate.c
++++ b/lib/layer/validate.c
+@@ -128,14 +128,15 @@ static bool maybe_downgrade_nsec3(const ranked_rr_array_entry_t *e, struct kr_qu
+ const knot_rdataset_t *rrs = &e->rr->rrs;
+ knot_rdata_t *rd = rrs->rdata;
+ for (int j = 0; j < rrs->count; ++j, rd = knot_rdataset_next(rd)) {
+- if (knot_nsec3_iters(rd) > KR_NSEC3_MAX_ITERATIONS)
++ if (kr_nsec3_limited_rdata(rd))
+ goto do_downgrade;
+ }
+ return false;
+
+ do_downgrade: // we do this deep inside calls because of having signer name available
+- VERBOSE_MSG(qry, "<= DNSSEC downgraded due to NSEC3 iterations %d > %d\n",
+- (int)knot_nsec3_iters(rd), (int)KR_NSEC3_MAX_ITERATIONS);
++ VERBOSE_MSG(qry,
++ "<= DNSSEC downgraded due to expensive NSEC3: %d iterations, %d salt length\n",
++ (int)knot_nsec3_iters(rd), (int)knot_nsec3_salt_len(rd));
+ qry->flags.DNSSEC_WANT = false;
+ qry->flags.DNSSEC_INSECURE = true;
+ rank_records(qry, true, KR_RANK_INSECURE, vctx->zone_name);
diff --git a/debian/patches/0003-lib-cache-limit-the-amount-of-work-on-SHA1.patch b/debian/patches/0003-lib-cache-limit-the-amount-of-work-on-SHA1.patch
new file mode 100644
index 0000000..2768b78
--- /dev/null
+++ b/debian/patches/0003-lib-cache-limit-the-amount-of-work-on-SHA1.patch
@@ -0,0 +1,60 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Sun, 11 Feb 2024 10:00:32 +0100
+Subject: lib/cache: limit the amount of work on SHA1
+
+That's when searching NSEC3 aggressive cache.
+---
+ lib/cache/nsec3.c | 14 ++++++++++++++
+ lib/dnssec/nsec3.h | 12 ++++++++++++
+ 2 files changed, 26 insertions(+)
+
+diff --git a/lib/cache/nsec3.c b/lib/cache/nsec3.c
+index 9832630..2716456 100644
+--- a/lib/cache/nsec3.c
++++ b/lib/cache/nsec3.c
+@@ -272,8 +272,22 @@ int nsec3_encloser(struct key *k, struct answer *ans,
+ const int zname_labels = knot_dname_labels(k->zname, NULL);
+ int last_nxproven_labels = -1;
+ const knot_dname_t *name = qry->sname;
++
++ /* Avoid doing too much work on SHA1; we might consider that a part of mitigating
++ * CVE-2023-50868: NSEC3 closest encloser proof can exhaust CPU
++ * As currently the code iterates from the longest name, we limit that.
++ * Note that we don't want to limit too much, as the alternative usually includes
++ * sending more queries upstream, which would come with nontrivial work, too.
++ */
++ const int max_labels = zname_labels + kr_nsec3_max_depth(&ans->nsec_p.libknot);
++ if (sname_labels > max_labels)
++ VERBOSE_MSG(qry, "=> NSEC3 hashing partly skipped due to too long SNAME (CVE-2023-50868)\n");
++
+ for (int name_labels = sname_labels; name_labels >= zname_labels;
+ --name_labels, name += 1 + name[0]) {
++ if (name_labels > max_labels)
++ continue; // avoid the hashing
++
+ /* Find a previous-or-equal NSEC3 in cache covering the name,
+ * checking TTL etc. */
+ const knot_db_val_t key = key_NSEC3_name(k, name, false, &ans->nsec_p);
+diff --git a/lib/dnssec/nsec3.h b/lib/dnssec/nsec3.h
+index 76ef2e9..a28d3c7 100644
+--- a/lib/dnssec/nsec3.h
++++ b/lib/dnssec/nsec3.h
+@@ -38,6 +38,18 @@ static inline bool kr_nsec3_limited_params(const dnssec_nsec3_params_t *params)
+ return kr_nsec3_limited(params->iterations, params->salt.size);
+ }
+
++/** Return limit on NSEC3 depth. The point is to avoid doing too much work on SHA1.
++ *
++ * CVE-2023-50868: NSEC3 closest encloser proof can exhaust CPU
++ *
++ * 128 is chosen so that zones with good NSEC3 parameters (giving _price() == 1)
++ * won't be limited in any way. Performance doesn't seem too bad with that either.
++ */
++static inline int kr_nsec3_max_depth(const dnssec_nsec3_params_t *params)
++{
++ return 128 / kr_nsec3_price(params->iterations, params->salt.size);
++}
++
+
+ /**
+ * Name error response check (RFC5155 7.2.2).
diff --git a/debian/patches/0004-validator-limit-the-amount-of-work-on-SHA1-in-NSEC3-.patch b/debian/patches/0004-validator-limit-the-amount-of-work-on-SHA1-in-NSEC3-.patch
new file mode 100644
index 0000000..a72b7e5
--- /dev/null
+++ b/debian/patches/0004-validator-limit-the-amount-of-work-on-SHA1-in-NSEC3-.patch
@@ -0,0 +1,31 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Mon, 12 Feb 2024 11:16:37 +0100
+Subject: validator: limit the amount of work on SHA1 in NSEC3 proofs
+
+---
+ lib/dnssec/nsec3.c | 12 ++++++++++++
+ 1 file changed, 12 insertions(+)
+
+diff --git a/lib/dnssec/nsec3.c b/lib/dnssec/nsec3.c
+index e4d314b..4199f25 100644
+--- a/lib/dnssec/nsec3.c
++++ b/lib/dnssec/nsec3.c
+@@ -146,6 +146,18 @@ static int closest_encloser_match(int *flags, const knot_rrset_t *nsec3,
+ const knot_dname_t *encloser = knot_wire_next_label(name, NULL);
+ *skipped = 1;
+
++ /* Avoid doing too much work on SHA1, mitigating:
++ * CVE-2023-50868: NSEC3 closest encloser proof can exhaust CPU
++ * We log nothing here; it wouldn't be easy from this place
++ * and huge SNAME should be suspicious on its own.
++ */
++ const int max_labels = knot_dname_labels(nsec3->owner, NULL) - 1
++ + kr_nsec3_max_depth(&params);
++ for (int l = knot_dname_labels(encloser, NULL); l > max_labels; --l) {
++ encloser = knot_wire_next_label(encloser, NULL);
++ ++(*skipped);
++ }
++
+ while(encloser) {
+ ret = hash_name(&name_hash, &params, encloser);
+ if (ret != 0)
diff --git a/debian/patches/0005-validator-refuse-to-validate-answers-with-more-than-.patch b/debian/patches/0005-validator-refuse-to-validate-answers-with-more-than-.patch
new file mode 100644
index 0000000..dd547a1
--- /dev/null
+++ b/debian/patches/0005-validator-refuse-to-validate-answers-with-more-than-.patch
@@ -0,0 +1,37 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Mon, 12 Feb 2024 11:16:47 +0100
+Subject: validator: refuse to validate answers with more than 8 NSEC3 records
+
+---
+ lib/layer/validate.c | 18 ++++++++++++++++++
+ 1 file changed, 18 insertions(+)
+
+diff --git a/lib/layer/validate.c b/lib/layer/validate.c
+index 5fea99d..95a7624 100644
+--- a/lib/layer/validate.c
++++ b/lib/layer/validate.c
+@@ -1120,6 +1120,24 @@ static int validate(kr_layer_t *ctx, knot_pkt_t *pkt)
+ }
+ }
+
++ /* Check for too many NSEC3 records. That's an issue, as some parts of validation
++ * are quadratic in their count, doing nontrivial computations inside.
++ * Also there seems to be no use in sending many NSEC3 records. */
++ if (!qry->flags.CACHED) {
++ const knot_pktsection_t *sec = knot_pkt_section(pkt, KNOT_AUTHORITY);
++ int count = 0;
++ for (int i = 0; i < sec->count; ++i)
++ count += (knot_pkt_rr(sec, i)->type == KNOT_RRTYPE_NSEC3);
++ if (count > 8) {
++ VERBOSE_MSG(qry, "<= too many NSEC3 records in AUTHORITY (%d)\n", count);
++ kr_request_set_extended_error(req, KNOT_EDNS_EDE_NSEC3_ITERS,
++ /* It's not about iteration values per se, but close enough. */
++ "DYRH: too many NSEC3 records");
++ qry->flags.DNSSEC_BOGUS = true;
++ return KR_STATE_FAIL;
++ }
++ }
++
+ if (knot_wire_get_aa(pkt->wire) && qtype == KNOT_RRTYPE_DNSKEY) {
+ const knot_rrset_t *ds = qry->zone_cut.trust_anchor;
+ if (ds && !kr_ds_algo_support(ds)) {
diff --git a/debian/patches/0006-validator-compatibility-with-older-libknot-versions.patch b/debian/patches/0006-validator-compatibility-with-older-libknot-versions.patch
new file mode 100644
index 0000000..0bb8fb1
--- /dev/null
+++ b/debian/patches/0006-validator-compatibility-with-older-libknot-versions.patch
@@ -0,0 +1,22 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Mon, 12 Feb 2024 11:30:50 +0100
+Subject: validator: compatibility with older libknot versions
+
+The value is in IANA registry, so it's very constant anyway.
+---
+ lib/layer/validate.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/lib/layer/validate.c b/lib/layer/validate.c
+index 95a7624..6c0d7ad 100644
+--- a/lib/layer/validate.c
++++ b/lib/layer/validate.c
+@@ -1130,7 +1130,7 @@ static int validate(kr_layer_t *ctx, knot_pkt_t *pkt)
+ count += (knot_pkt_rr(sec, i)->type == KNOT_RRTYPE_NSEC3);
+ if (count > 8) {
+ VERBOSE_MSG(qry, "<= too many NSEC3 records in AUTHORITY (%d)\n", count);
+- kr_request_set_extended_error(req, KNOT_EDNS_EDE_NSEC3_ITERS,
++ kr_request_set_extended_error(req, 27/*KNOT_EDNS_EDE_NSEC3_ITERS*/,
+ /* It's not about iteration values per se, but close enough. */
+ "DYRH: too many NSEC3 records");
+ qry->flags.DNSSEC_BOGUS = true;
diff --git a/debian/patches/0007-lib-cache-bump-CACHE_VERSION.patch b/debian/patches/0007-lib-cache-bump-CACHE_VERSION.patch
new file mode 100644
index 0000000..36c8aa4
--- /dev/null
+++ b/debian/patches/0007-lib-cache-bump-CACHE_VERSION.patch
@@ -0,0 +1,25 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Fri, 23 Feb 2024 10:07:35 +0100
+Subject: lib/cache: bump CACHE_VERSION
+
+Ideally we would've done that at once with increasing NSEC3 strictness,
+i.e. in 5.7.1 + 6.0.6, as otherwise we could run into some recoverable
+assertions until the records got removed or expired.
+We at least do the bump now.
+---
+ lib/cache/api.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/lib/cache/api.c b/lib/cache/api.c
+index bb627ea..f71a8d0 100644
+--- a/lib/cache/api.c
++++ b/lib/cache/api.c
+@@ -40,7 +40,7 @@
+
+
+ /** Cache version */
+-static const uint16_t CACHE_VERSION = 6;
++static const uint16_t CACHE_VERSION = 7;
+ /** Key size */
+ #define KEY_HSIZE (sizeof(uint8_t) + sizeof(uint16_t))
+ #define KEY_SIZE (KEY_HSIZE + KNOT_DNAME_MAXLEN)
diff --git a/debian/patches/0008-lib-dnssec-kr_rrset_validate_with_key-deduplicate-cl.patch b/debian/patches/0008-lib-dnssec-kr_rrset_validate_with_key-deduplicate-cl.patch
new file mode 100644
index 0000000..85fff28
--- /dev/null
+++ b/debian/patches/0008-lib-dnssec-kr_rrset_validate_with_key-deduplicate-cl.patch
@@ -0,0 +1,43 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Sat, 30 Dec 2023 09:20:56 +0100
+Subject: lib/dnssec kr_rrset_validate_with_key(): deduplicate cleanup
+
+---
+ lib/dnssec.c | 11 +++++------
+ 1 file changed, 5 insertions(+), 6 deletions(-)
+
+diff --git a/lib/dnssec.c b/lib/dnssec.c
+index d6ae3cc..c536357 100644
+--- a/lib/dnssec.c
++++ b/lib/dnssec.c
+@@ -356,9 +356,8 @@ static int kr_rrset_validate_with_key(kr_rrset_validation_ctx_t *vctx,
+ int retv = validate_rrsig_rr(&val_flgs, covered_labels, rdata_j,
+ key_alg, keytag, vctx);
+ if (retv == kr_error(EAGAIN)) {
+- kr_dnssec_key_free(&created_key);
+ vctx->result = retv;
+- return retv;
++ goto finish;
+ } else if (retv != 0) {
+ continue;
+ }
+@@ -392,15 +391,15 @@ static int kr_rrset_validate_with_key(kr_rrset_validation_ctx_t *vctx,
+
+ trim_ttl(covered, rdata_j, vctx);
+
+- kr_dnssec_key_free(&created_key);
+- vctx->result = kr_ok();
+ kr_rank_set(&vctx->rrs->at[i]->rank, KR_RANK_SECURE); /* upgrade from bogus */
+- return vctx->result;
++ vctx->result = kr_ok();
++ goto finish;
+ }
+ }
+ /* No applicable key found, cannot be validated. */
+- kr_dnssec_key_free(&created_key);
+ vctx->result = kr_error(ENOENT);
++finish:
++ kr_dnssec_key_free(&created_key);
+ return vctx->result;
+ }
+
diff --git a/debian/patches/0009-lib-resolve-kr_request_set_extended_error-tweak-prio.patch b/debian/patches/0009-lib-resolve-kr_request_set_extended_error-tweak-prio.patch
new file mode 100644
index 0000000..bca80ea
--- /dev/null
+++ b/debian/patches/0009-lib-resolve-kr_request_set_extended_error-tweak-prio.patch
@@ -0,0 +1,25 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Mon, 1 Jan 2024 16:05:46 +0100
+Subject: lib/resolve kr_request_set_extended_error(): tweak priorities
+
+Keep the first error in case priorities are equal.
+
+At least with the current KeyTrap topic that should work better,
+but blaming a single error is alchemy anyway, at least in some cases.
+---
+ lib/resolve.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/lib/resolve.c b/lib/resolve.c
+index aa3d521..9e82e6f 100644
+--- a/lib/resolve.c
++++ b/lib/resolve.c
+@@ -1684,7 +1684,7 @@ int kr_request_set_extended_error(struct kr_request *request, int info_code, con
+ return KNOT_EDNS_EDE_NONE;
+ }
+
+- if (ede_priority(info_code) >= ede_priority(ede->info_code)) {
++ if (ede_priority(info_code) > ede_priority(ede->info_code)) {
+ ede->info_code = info_code;
+ ede->extra_text = extra_text;
+ }
diff --git a/debian/patches/0010-mitigate-KeyTrap-DoS-CVE-2023-50387.patch b/debian/patches/0010-mitigate-KeyTrap-DoS-CVE-2023-50387.patch
new file mode 100644
index 0000000..d6109e0
--- /dev/null
+++ b/debian/patches/0010-mitigate-KeyTrap-DoS-CVE-2023-50387.patch
@@ -0,0 +1,233 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Mon, 1 Jan 2024 16:21:10 +0100
+Subject: mitigate KeyTrap DoS = CVE-2023-50387
+
+---
+ daemon/engine.c | 1 +
+ daemon/lua/kres-gen-30.lua | 3 +++
+ daemon/lua/kres-gen-31.lua | 3 +++
+ daemon/lua/kres-gen-32.lua | 3 +++
+ lib/defines.h | 2 ++
+ lib/dnssec.c | 28 ++++++++++++++++++++++++++++
+ lib/dnssec.h | 1 +
+ lib/layer/validate.c | 7 +++++++
+ lib/resolve.h | 3 +++
+ lib/rplan.h | 6 ++++++
+ 10 files changed, 57 insertions(+)
+
+diff --git a/daemon/engine.c b/daemon/engine.c
+index 26c225f..a3f439f 100644
+--- a/daemon/engine.c
++++ b/daemon/engine.c
+@@ -492,6 +492,7 @@ static int init_resolver(struct engine *engine)
+ /* Open resolution context */
+ ctx->trust_anchors = trie_create(NULL);
+ ctx->negative_anchors = trie_create(NULL);
++ ctx->vld_limit_crypto = KR_VLD_LIMIT_CRYPTO_DEFAULT;
+ ctx->pool = engine->pool;
+ ctx->modules = &engine->modules;
+ ctx->cache_rtt_tout_retry_interval = KR_NS_TIMEOUT_RETRY_INTERVAL;
+diff --git a/daemon/lua/kres-gen-30.lua b/daemon/lua/kres-gen-30.lua
+index 4353c5c..01ae6ac 100644
+--- a/daemon/lua/kres-gen-30.lua
++++ b/daemon/lua/kres-gen-30.lua
+@@ -336,6 +336,8 @@ struct kr_query {
+ struct kr_qflags forward_flags;
+ uint32_t secret;
+ uint32_t uid;
++ int32_t vld_limit_crypto_remains;
++ uint32_t vld_limit_uid;
+ uint64_t creation_time_mono;
+ uint64_t timestamp_mono;
+ struct timeval timestamp;
+@@ -353,6 +355,7 @@ struct kr_context {
+ knot_rrset_t *upstream_opt_rr;
+ trie_t *trust_anchors;
+ trie_t *negative_anchors;
++ int32_t vld_limit_crypto;
+ struct kr_zonecut root_hints;
+ struct kr_cache cache;
+ unsigned int cache_rtt_tout_retry_interval;
+diff --git a/daemon/lua/kres-gen-31.lua b/daemon/lua/kres-gen-31.lua
+index a68dd65..c3845ec 100644
+--- a/daemon/lua/kres-gen-31.lua
++++ b/daemon/lua/kres-gen-31.lua
+@@ -336,6 +336,8 @@ struct kr_query {
+ struct kr_qflags forward_flags;
+ uint32_t secret;
+ uint32_t uid;
++ int32_t vld_limit_crypto_remains;
++ uint32_t vld_limit_uid;
+ uint64_t creation_time_mono;
+ uint64_t timestamp_mono;
+ struct timeval timestamp;
+@@ -353,6 +355,7 @@ struct kr_context {
+ knot_rrset_t *upstream_opt_rr;
+ trie_t *trust_anchors;
+ trie_t *negative_anchors;
++ int32_t vld_limit_crypto;
+ struct kr_zonecut root_hints;
+ struct kr_cache cache;
+ unsigned int cache_rtt_tout_retry_interval;
+diff --git a/daemon/lua/kres-gen-32.lua b/daemon/lua/kres-gen-32.lua
+index 222891e..2c407d4 100644
+--- a/daemon/lua/kres-gen-32.lua
++++ b/daemon/lua/kres-gen-32.lua
+@@ -337,6 +337,8 @@ struct kr_query {
+ struct kr_qflags forward_flags;
+ uint32_t secret;
+ uint32_t uid;
++ int32_t vld_limit_crypto_remains;
++ uint32_t vld_limit_uid;
+ uint64_t creation_time_mono;
+ uint64_t timestamp_mono;
+ struct timeval timestamp;
+@@ -354,6 +356,7 @@ struct kr_context {
+ knot_rrset_t *upstream_opt_rr;
+ trie_t *trust_anchors;
+ trie_t *negative_anchors;
++ int32_t vld_limit_crypto;
+ struct kr_zonecut root_hints;
+ struct kr_cache cache;
+ unsigned int cache_rtt_tout_retry_interval;
+diff --git a/lib/defines.h b/lib/defines.h
+index 6b6dac5..e832892 100644
+--- a/lib/defines.h
++++ b/lib/defines.h
+@@ -57,6 +57,8 @@ static inline int KR_COLD kr_error(int x) {
+ #define KR_COUNT_NO_NSADDR_LIMIT 5
+ #define KR_CONSUME_FAIL_ROW_LIMIT 3 /* Maximum number of KR_STATE_FAIL in a row. */
+
++#define KR_VLD_LIMIT_CRYPTO_DEFAULT 32 /**< default for struct kr_query::vld_limit_crypto */
++
+ /*
+ * Defines.
+ */
+diff --git a/lib/dnssec.c b/lib/dnssec.c
+index c536357..1e6eb58 100644
+--- a/lib/dnssec.c
++++ b/lib/dnssec.c
+@@ -240,6 +240,29 @@ fail:
+ return NULL;
+ }
+
++/// Return if we want to afford yet another crypto-validation (and account it).
++static bool check_crypto_limit(const kr_rrset_validation_ctx_t *vctx)
++{
++ if (vctx->limit_crypto_remains == NULL)
++ return true; // no limiting
++ if (*vctx->limit_crypto_remains > 0) {
++ --*vctx->limit_crypto_remains;
++ return true;
++ }
++ // We got over limit. There are optional actions to do.
++ if (vctx->log_qry && kr_log_is_debug_qry(VALIDATOR, vctx->log_qry)) {
++ auto_free char *name_str = kr_dname_text(vctx->zone_name);
++ kr_log_q(vctx->log_qry, VALIDATOR,
++ "expensive crypto limited, mitigating CVE-2023-50387, current zone: %s\n",
++ name_str);
++ }
++ if (vctx->log_qry && vctx->log_qry->request) {
++ kr_request_set_extended_error(vctx->log_qry->request, KNOT_EDNS_EDE_BOGUS,
++ "EAIE: expensive crypto limited, mitigating CVE-2023-50387");
++ }
++ return false;
++}
++
+ static int kr_svldr_rrset_with_key(knot_rrset_t *rrs, const knot_rdataset_t *rrsigs,
+ kr_rrset_validation_ctx_t *vctx, const kr_svldr_key_t *key)
+ {
+@@ -258,6 +281,8 @@ static int kr_svldr_rrset_with_key(knot_rrset_t *rrs, const knot_rdataset_t *rrs
+ } else if (retv != 0) {
+ continue;
+ }
++ if (!check_crypto_limit(vctx))
++ return vctx->result = kr_error(E2BIG);
+ // We only expect non-expanded wildcard records in input;
+ // that also means we don't need to perform non-existence proofs.
+ const int trim_labels = (val_flgs & FLG_WILDCARD_EXPANSION) ? 1 : 0;
+@@ -367,6 +392,9 @@ static int kr_rrset_validate_with_key(kr_rrset_validation_ctx_t *vctx,
+ break;
+ }
+ }
++ if (!check_crypto_limit(vctx)) {
++ goto finish;
++ }
+ if (kr_check_signature(rdata_j, key, covered, trim_labels) != 0) {
+ vctx->rrs_counters.crypto_invalid++;
+ continue;
+diff --git a/lib/dnssec.h b/lib/dnssec.h
+index 0fbd47c..ca737cf 100644
+--- a/lib/dnssec.h
++++ b/lib/dnssec.h
+@@ -44,6 +44,7 @@ struct kr_rrset_validation_ctx {
+ uint32_t flags; /*!< Output - Flags. */
+ uint32_t err_cnt; /*!< Output - Number of validation failures. */
+ uint32_t cname_norrsig_cnt; /*!< Output - Number of CNAMEs missing RRSIGs. */
++ int32_t *limit_crypto_remains; /*!< Optional pointer to struct kr_query::vld_limit_crypto_remains */
+
+ /** Validation result: kr_error() code.
+ *
+diff --git a/lib/layer/validate.c b/lib/layer/validate.c
+index 6c0d7ad..4882da8 100644
+--- a/lib/layer/validate.c
++++ b/lib/layer/validate.c
+@@ -276,6 +276,7 @@ static int validate_records(struct kr_request *req, knot_pkt_t *answer, knot_mm_
+ .err_cnt = 0,
+ .cname_norrsig_cnt = 0,
+ .result = 0,
++ .limit_crypto_remains = &qry->vld_limit_crypto_remains,
+ .log_qry = qry,
+ };
+
+@@ -384,6 +385,7 @@ static int validate_keyset(struct kr_request *req, knot_pkt_t *answer, bool has_
+ .has_nsec3 = has_nsec3,
+ .flags = 0,
+ .result = 0,
++ .limit_crypto_remains = &qry->vld_limit_crypto_remains,
+ .log_qry = qry,
+ };
+ int ret = kr_dnskeys_trusted(&vctx, sig_rds, qry->zone_cut.trust_anchor);
+@@ -1030,6 +1032,11 @@ static int validate(kr_layer_t *ctx, knot_pkt_t *pkt)
+ struct kr_request *req = ctx->req;
+ struct kr_query *qry = req->current_query;
+
++ if (qry->vld_limit_uid != qry->uid) {
++ qry->vld_limit_uid = qry->uid;
++ qry->vld_limit_crypto_remains = req->ctx->vld_limit_crypto;
++ }
++
+ /* Ignore faulty or unprocessed responses. */
+ if (ctx->state & (KR_STATE_FAIL|KR_STATE_CONSUME)) {
+ return ctx->state;
+diff --git a/lib/resolve.h b/lib/resolve.h
+index 97ba07b..a2d5ec9 100644
+--- a/lib/resolve.h
++++ b/lib/resolve.h
+@@ -162,6 +162,9 @@ struct kr_context
+
+ trie_t *trust_anchors;
+ trie_t *negative_anchors;
++ /** Validator's limit on the number of cryptographic steps for a single upstream packet. */
++ int32_t vld_limit_crypto;
++
+ struct kr_zonecut root_hints;
+ struct kr_cache cache;
+ unsigned cache_rtt_tout_retry_interval;
+diff --git a/lib/rplan.h b/lib/rplan.h
+index 891781f..68174af 100644
+--- a/lib/rplan.h
++++ b/lib/rplan.h
+@@ -87,6 +87,12 @@ struct kr_query {
+ struct kr_qflags forward_flags;
+ uint32_t secret;
+ uint32_t uid; /**< Query iteration number, unique within the kr_rplan. */
++
++ /** Remaining limit; see kr_query::vld_limit_crypto docs */
++ int32_t vld_limit_crypto_remains;
++ /** ::uid value to which this remaining limit applies. */
++ uint32_t vld_limit_uid;
++
+ uint64_t creation_time_mono; /* The time of query's creation (milliseconds).
+ * Or time of creation of an oldest
+ * ancestor if it is a subquery. */
diff --git a/debian/patches/0011-mitigate-KeyTrap-DoS-CVE-2023-50387.patch b/debian/patches/0011-mitigate-KeyTrap-DoS-CVE-2023-50387.patch
new file mode 100644
index 0000000..e074a94
--- /dev/null
+++ b/debian/patches/0011-mitigate-KeyTrap-DoS-CVE-2023-50387.patch
@@ -0,0 +1,80 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Tue, 16 Jan 2024 07:35:20 +0100
+Subject: mitigate KeyTrap DoS = CVE-2023-50387
+
+Improve: don't retry in this case.
+---
+ lib/dnssec.c | 7 ++++---
+ lib/layer/validate.c | 4 ++++
+ lib/resolve.c | 4 +++-
+ 3 files changed, 11 insertions(+), 4 deletions(-)
+
+diff --git a/lib/dnssec.c b/lib/dnssec.c
+index 1e6eb58..262570c 100644
+--- a/lib/dnssec.c
++++ b/lib/dnssec.c
+@@ -134,7 +134,7 @@ int kr_rrset_validate(kr_rrset_validation_ctx_t *vctx, knot_rrset_t *covered)
+ memset(&vctx->rrs_counters, 0, sizeof(vctx->rrs_counters));
+ for (unsigned i = 0; i < vctx->keys->rrs.count; ++i) {
+ int ret = kr_rrset_validate_with_key(vctx, covered, i, NULL);
+- if (ret == 0) {
++ if (ret == 0 || ret == kr_error(E2BIG)) {
+ return ret;
+ }
+ }
+@@ -307,7 +307,7 @@ int kr_svldr_rrset(knot_rrset_t *rrs, const knot_rdataset_t *rrsigs,
+ }
+ for (ssize_t i = 0; i < ctx->keys.len; ++i) {
+ kr_svldr_rrset_with_key(rrs, rrsigs, &ctx->vctx, &ctx->keys.at[i]);
+- if (ctx->vctx.result == 0)
++ if (ctx->vctx.result == 0 || ctx->vctx.result == kr_error(E2BIG))
+ break;
+ }
+ return ctx->vctx.result;
+@@ -393,6 +393,7 @@ static int kr_rrset_validate_with_key(kr_rrset_validation_ctx_t *vctx,
+ }
+ }
+ if (!check_crypto_limit(vctx)) {
++ vctx->result = kr_error(E2BIG);
+ goto finish;
+ }
+ if (kr_check_signature(rdata_j, key, covered, trim_labels) != 0) {
+@@ -475,7 +476,7 @@ int kr_dnskeys_trusted(kr_rrset_validation_ctx_t *vctx, const knot_rdataset_t *s
+ if (ret == 0)
+ ret = kr_svldr_rrset_with_key(keys, sigs, vctx, &key);
+ svldr_key_del(&key);
+- if (ret == 0) {
++ if (ret == 0 || ret == kr_error(E2BIG)) {
+ kr_assert(vctx->result == 0);
+ return vctx->result;
+ }
+diff --git a/lib/layer/validate.c b/lib/layer/validate.c
+index 4882da8..1fdc57d 100644
+--- a/lib/layer/validate.c
++++ b/lib/layer/validate.c
+@@ -1177,6 +1177,10 @@ static int validate(kr_layer_t *ctx, knot_pkt_t *pkt)
+ ret = validate_records(req, pkt, req->rplan.pool, has_nsec3);
+ if (ret == KNOT_EDOWNGRADED) {
+ return KR_STATE_DONE;
++ } else if (ret == kr_error(E2BIG)) {
++ qry->flags.DNSSEC_BOGUS = true;
++ return KR_STATE_FAIL;
++
+ } else if (ret != 0) {
+ /* something exceptional - no DNS key, empty pointers etc
+ * normally it shouldn't happen */
+diff --git a/lib/resolve.c b/lib/resolve.c
+index 9e82e6f..456065b 100644
+--- a/lib/resolve.c
++++ b/lib/resolve.c
+@@ -880,7 +880,9 @@ int kr_resolve_consume(struct kr_request *request, struct kr_transport **transpo
+
+ /* Do not finish with bogus answer. */
+ if (qry->flags.DNSSEC_BOGUS) {
+- if (qry->flags.FORWARD || qry->flags.STUB) {
++ if (qry->flags.FORWARD || qry->flags.STUB
++ /* Probably CPU exhaustion attempt, so do not retry. */
++ || qry->vld_limit_crypto_remains <= 0) {
+ return KR_STATE_FAIL;
+ }
+ /* Other servers might not have broken DNSSEC. */
diff --git a/debian/patches/0012-lib-dnssec-fix-imprecise-assertion.patch b/debian/patches/0012-lib-dnssec-fix-imprecise-assertion.patch
new file mode 100644
index 0000000..8612afb
--- /dev/null
+++ b/debian/patches/0012-lib-dnssec-fix-imprecise-assertion.patch
@@ -0,0 +1,22 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Fri, 23 Feb 2024 09:33:21 +0100
+Subject: lib/dnssec: fix imprecise assertion
+
+It was no longer correct after commit cc5051b444130 (KeyTrap).
+---
+ lib/dnssec.c | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/lib/dnssec.c b/lib/dnssec.c
+index 262570c..fcae473 100644
+--- a/lib/dnssec.c
++++ b/lib/dnssec.c
+@@ -477,7 +477,7 @@ int kr_dnskeys_trusted(kr_rrset_validation_ctx_t *vctx, const knot_rdataset_t *s
+ ret = kr_svldr_rrset_with_key(keys, sigs, vctx, &key);
+ svldr_key_del(&key);
+ if (ret == 0 || ret == kr_error(E2BIG)) {
+- kr_assert(vctx->result == 0);
++ kr_assert(vctx->result == ret);
+ return vctx->result;
+ }
+ }
diff --git a/debian/patches/0013-daemon-more-avoidance-of-excessive-TCP-reconnections.patch b/debian/patches/0013-daemon-more-avoidance-of-excessive-TCP-reconnections.patch
new file mode 100644
index 0000000..fb1a3e4
--- /dev/null
+++ b/debian/patches/0013-daemon-more-avoidance-of-excessive-TCP-reconnections.patch
@@ -0,0 +1,153 @@
+From: =?utf-8?b?VmxhZGltw61yIMSMdW7DoXQ=?= <vladimir.cunat@nic.cz>
+Date: Sat, 29 Jul 2023 17:53:34 +0200
+Subject: daemon: more avoidance of excessive TCP reconnections
+
+Previously this penalization was only triggered if the remote server
+closed TCP. Now it's extended to us closing it when the server
+(only) sends back some nonsense. At least for the cases which I could
+see immediately.
+
+That's just three trivial one-line additions; the rest is refactoring.
+---
+ daemon/io.c | 38 ++++++++++----------------------------
+ daemon/session.c | 26 +++++++++++++++++++++-----
+ daemon/session.h | 5 +++--
+ 3 files changed, 34 insertions(+), 35 deletions(-)
+
+diff --git a/daemon/io.c b/daemon/io.c
+index 48bfed3..ac0d969 100644
+--- a/daemon/io.c
++++ b/daemon/io.c
+@@ -337,33 +337,6 @@ void tcp_timeout_trigger(uv_timer_t *timer)
+ }
+ }
+
+-static void tcp_disconnect(struct session *s, int errcode)
+-{
+- if (kr_log_is_debug(IO, NULL)) {
+- struct sockaddr *peer = session_get_peer(s);
+- char *peer_str = kr_straddr(peer);
+- kr_log_debug(IO, "=> connection to '%s' closed by peer (%s)\n",
+- peer_str ? peer_str : "",
+- uv_strerror(errcode));
+- }
+-
+- if (!session_was_useful(s) && session_flags(s)->outgoing) {
+- /* We want to penalize the IP address, if a task is asking a query.
+- * It might not be the right task, but that doesn't matter so much
+- * for attributing the useless session to the IP address. */
+- struct qr_task *t = session_tasklist_get_first(s);
+- struct kr_query *qry = NULL;
+- if (t) {
+- struct kr_request *req = worker_task_request(t);
+- qry = array_tail(req->rplan.pending);
+- }
+- if (qry) /* We reuse the error for connection, as it's quite similar. */
+- qry->server_selection.error(qry, worker_task_get_transport(t),
+- KR_SELECTION_TCP_CONNECT_FAILED);
+- }
+- worker_end_tcp(s);
+-}
+-
+ static void tcp_recv(uv_stream_t *handle, ssize_t nread, const uv_buf_t *buf)
+ {
+ struct session *s = handle->data;
+@@ -381,7 +354,16 @@ static void tcp_recv(uv_stream_t *handle, ssize_t nread, const uv_buf_t *buf)
+ }
+
+ if (nread < 0 || !buf->base) {
+- tcp_disconnect(s, nread);
++ if (kr_log_is_debug(IO, NULL)) {
++ struct sockaddr *peer = session_get_peer(s);
++ char *peer_str = kr_straddr(peer);
++ kr_log_debug(IO, "=> connection to '%s' closed by peer (%s)\n",
++ peer_str ? peer_str : "",
++ uv_strerror(nread));
++ }
++
++ session_tcp_penalize(s);
++ worker_end_tcp(s);
+ return;
+ }
+
+diff --git a/daemon/session.c b/daemon/session.c
+index a1f2207..ed0ff68 100644
+--- a/daemon/session.c
++++ b/daemon/session.c
+@@ -123,11 +123,6 @@ void session_close(struct session *session)
+ }
+ }
+
+-bool session_was_useful(const struct session *session)
+-{
+- return session->was_useful;
+-}
+-
+ int session_start_read(struct session *session)
+ {
+ return io_start_read(session->handle);
+@@ -582,6 +577,24 @@ ssize_t session_wirebuf_trim(struct session *session, ssize_t len)
+ return len;
+ }
+
++void session_tcp_penalize(struct session *s)
++{
++ if (s->was_useful || !s->sflags.outgoing)
++ return;
++ /* We want to penalize the IP address, if a task is asking a query.
++ * It might not be the right task, but that doesn't matter so much
++ * for attributing the useless session to the IP address. */
++ struct qr_task *t = session_tasklist_get_first(s);
++ struct kr_query *qry = NULL;
++ if (t) {
++ struct kr_request *req = worker_task_request(t);
++ qry = array_tail(req->rplan.pending);
++ }
++ if (qry) /* We reuse the error for connection, as it's quite similar. */
++ qry->server_selection.error(qry, worker_task_get_transport(t),
++ KR_SELECTION_TCP_CONNECT_FAILED);
++}
++
+ knot_pkt_t *session_produce_packet(struct session *session, knot_mm_t *mm)
+ {
+ session->sflags.wirebuf_error = false;
+@@ -617,6 +630,7 @@ knot_pkt_t *session_produce_packet(struct session *session, knot_mm_t *mm)
+ msg_size = knot_wire_read_u16(msg_start);
+ if (msg_size >= session->wire_buf_size) {
+ session->sflags.wirebuf_error = true;
++ session_tcp_penalize(session);
+ return NULL;
+ }
+ if (msg_size + 2 > wirebuf_msg_data_size) {
+@@ -624,6 +638,7 @@ knot_pkt_t *session_produce_packet(struct session *session, knot_mm_t *mm)
+ }
+ if (msg_size == 0) {
+ session->sflags.wirebuf_error = true;
++ session_tcp_penalize(session);
+ return NULL;
+ }
+ msg_start += 2;
+@@ -631,6 +646,7 @@ knot_pkt_t *session_produce_packet(struct session *session, knot_mm_t *mm)
+ msg_size = wirebuf_msg_data_size;
+ } else {
+ session->sflags.wirebuf_error = true;
++ session_tcp_penalize(session);
+ return NULL;
+ }
+
+diff --git a/daemon/session.h b/daemon/session.h
+index 603d7cb..1f95ac5 100644
+--- a/daemon/session.h
++++ b/daemon/session.h
+@@ -91,8 +91,9 @@ int session_tasklist_finalize_expired(struct session *session);
+ /** Both of task lists (associated & waiting). */
+ /** Check if empty. */
+ bool session_is_empty(const struct session *session);
+-/** Return whether session seems to have done something useful. */
+-bool session_was_useful(const struct session *session);
++/** Penalize this server if the session hasn't been useful (and is outgoing). */
++void session_tcp_penalize(struct session *session);
++
+ /** Get pointer to session flags */
+ struct session_flags *session_flags(struct session *session);
+ /** Get pointer to peer address. */
diff --git a/debian/patches/series b/debian/patches/series
new file mode 100644
index 0000000..beab14e
--- /dev/null
+++ b/debian/patches/series
@@ -0,0 +1,13 @@
+0001-validator-lower-the-NSEC3-iteration-limit-150-50.patch
+0002-validator-similarly-also-limit-excessive-NSEC3-salt-.patch
+0003-lib-cache-limit-the-amount-of-work-on-SHA1.patch
+0004-validator-limit-the-amount-of-work-on-SHA1-in-NSEC3-.patch
+0005-validator-refuse-to-validate-answers-with-more-than-.patch
+0006-validator-compatibility-with-older-libknot-versions.patch
+0007-lib-cache-bump-CACHE_VERSION.patch
+0008-lib-dnssec-kr_rrset_validate_with_key-deduplicate-cl.patch
+0009-lib-resolve-kr_request_set_extended_error-tweak-prio.patch
+0010-mitigate-KeyTrap-DoS-CVE-2023-50387.patch
+0011-mitigate-KeyTrap-DoS-CVE-2023-50387.patch
+0012-lib-dnssec-fix-imprecise-assertion.patch
+0013-daemon-more-avoidance-of-excessive-TCP-reconnections.patch
diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml
index 984a817..b19fd28 100644
--- a/debian/salsa-ci.yml
+++ b/debian/salsa-ci.yml
@@ -3,5 +3,8 @@ 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
+variables:
+ RELEASE: 'bookworm'
+
lintian:
allow_failure: true