Bug#1109845: unblock: pdns-recursor/5.2.4-2 [pre-approval]
Package: release.debian.org
Severity: normal
X-Debbugs-Cc: pdns-recursor@packages.debian.org
Control: affects -1 + src:pdns-recursor
User: release.debian.org@packages.debian.org
Usertags: unblock
Please unblock package pdns-recursor
[ Reason ]
Upstream security fix for CVE-2025-30192, Debian bug #1109808
[ Impact ]
Upstream classified the security issue as Severity: High under
non-default configuration
[ Tests ]
For the specific fix I don't know how to verify it, but upstream has
spent a lot of time on it (and skipped 5.2.3 because of the effort).
I've done a basic test of the general functionality. autopkgtests in
experimental have passed on amd64, arm64, riscv64 but were still in
progress on ppc64el, s390x.
[ Risks ]
While this is a new upstream version compared to testing, the only
change is the security fix. I've looked at the diff in upstreams git
and it is approximately the same size, minus some release noise.
[ Checklist ]
[x] all changes are documented in the d/changelog
[x] I reviewed all changes and I approve them
[x] attach debdiff against the package in testing
[ Other info ]
The debdiff is filtered to strip some noise:
- autoconf-generated ./configure
- effective_tld_names.dat and pubsuffix.cc are data, which the Debian
build ignores and instead uses from the publicsufffix package.
unblock pdns-recursor/5.2.4-2
diff -Nru pdns-recursor-5.2.2/configure.ac pdns-recursor-5.2.4/configure.ac
--- pdns-recursor-5.2.2/configure.ac 2025-04-08 12:41:43.000000000 +0200
+++ pdns-recursor-5.2.4/configure.ac 2025-07-17 14:21:38.000000000 +0200
@@ -1,6 +1,6 @@
AC_PREREQ([2.69])
-AC_INIT([pdns-recursor], [5.2.2])
+AC_INIT([pdns-recursor], [5.2.4])
AC_CONFIG_AUX_DIR([build-aux])
AM_INIT_AUTOMAKE([foreign dist-bzip2 no-dist-gzip tar-ustar -Wno-portability subdir-objects parallel-tests 1.11])
AM_SILENT_RULES([yes])
diff -Nru pdns-recursor-5.2.2/debian/changelog pdns-recursor-5.2.4/debian/changelog
--- pdns-recursor-5.2.2/debian/changelog 2025-07-20 12:57:46.000000000 +0200
+++ pdns-recursor-5.2.4/debian/changelog 2025-07-25 03:03:18.000000000 +0200
@@ -1,3 +1,17 @@
+pdns-recursor (5.2.4-2) unstable; urgency=medium
+
+ * Upload to unstable.
+
+ -- Chris Hofstaedtler <zeha@debian.org> Fri, 25 Jul 2025 03:03:18 +0200
+
+pdns-recursor (5.2.4-1) experimental; urgency=medium
+
+ * New upstream version 5.2.4, fixing CVE-2025-30192.
+ (Closes: #1109808)
+ * Upload to experimental.
+
+ -- Chris Hofstaedtler <zeha@debian.org> Thu, 24 Jul 2025 10:18:06 +0200
+
pdns-recursor (5.2.2-2) unstable; urgency=medium
* Really emit (X-Cargo-|Static-)Built-Using fields (Closes: #1109579)
diff -Nru pdns-recursor-5.2.2/dnsrecords.cc pdns-recursor-5.2.4/dnsrecords.cc
--- pdns-recursor-5.2.2/dnsrecords.cc 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/dnsrecords.cc 2025-07-17 14:20:08.000000000 +0200
@@ -1034,6 +1034,16 @@
}
}
+vector<pair<uint16_t, string>>::const_iterator EDNSOpts::getFirstOption(uint16_t optionCode) const
+{
+ for (auto iter = d_options.cbegin(); iter != d_options.cend(); ++iter) {
+ if (iter->first == optionCode) {
+ return iter;
+ }
+ }
+ return d_options.cend();
+}
+
#if 0
static struct Reporter
{
diff -Nru pdns-recursor-5.2.2/dnsrecords.hh pdns-recursor-5.2.4/dnsrecords.hh
--- pdns-recursor-5.2.2/dnsrecords.hh 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/dnsrecords.hh 2025-07-17 14:20:08.000000000 +0200
@@ -1057,6 +1057,8 @@
uint16_t d_packetsize{0};
uint16_t d_extFlags{0};
uint8_t d_extRCode, d_version;
+
+ [[nodiscard]] vector<pair<uint16_t, string>>::const_iterator getFirstOption(uint16_t optionCode) const;
};
//! Convenience function that fills out EDNS0 options, and returns true if there are any
diff -Nru pdns-recursor-5.2.2/ednsoptions.cc pdns-recursor-5.2.4/ednsoptions.cc
--- pdns-recursor-5.2.2/ednsoptions.cc 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/ednsoptions.cc 2025-07-17 14:20:08.000000000 +0200
@@ -22,6 +22,7 @@
#include "dns.hh"
#include "ednsoptions.hh"
#include "iputils.hh"
+#include "dnsparser.hh"
bool getNextEDNSOption(const char* data, size_t dataLen, uint16_t& optionCode, uint16_t& optionLen)
{
@@ -93,6 +94,61 @@
return ENOENT;
}
+bool slowParseEDNSOptions(const PacketBuffer& packet, EDNSOptionViewMap& options)
+{
+ if (packet.size() < sizeof(dnsheader)) {
+ return false;
+ }
+
+ const dnsheader_aligned dnsHeader(packet.data());
+
+ if (ntohs(dnsHeader->qdcount) == 0) {
+ return false;
+ }
+
+ if (ntohs(dnsHeader->arcount) == 0) {
+ return false;
+ }
+
+ try {
+ uint64_t numrecords = ntohs(dnsHeader->ancount) + ntohs(dnsHeader->nscount) + ntohs(dnsHeader->arcount);
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast,cppcoreguidelines-pro-type-const-cast)
+ DNSPacketMangler dpm(const_cast<char*>(reinterpret_cast<const char*>(packet.data())), packet.size());
+ uint64_t index{};
+ for (index = 0; index < ntohs(dnsHeader->qdcount); ++index) {
+ dpm.skipDomainName();
+ /* type and class */
+ dpm.skipBytes(4);
+ }
+
+ for (index = 0; index < numrecords; ++index) {
+ dpm.skipDomainName();
+
+ uint8_t section = index < ntohs(dnsHeader->ancount) ? 1 : (index < (ntohs(dnsHeader->ancount) + ntohs(dnsHeader->nscount)) ? 2 : 3);
+ uint16_t dnstype = dpm.get16BitInt();
+ dpm.get16BitInt();
+ dpm.skipBytes(4); /* TTL */
+
+ if (section == 3 && dnstype == QType::OPT) {
+ uint32_t offset = dpm.getOffset();
+ if (offset >= packet.size()) {
+ return false;
+ }
+ /* if we survive this call, we can parse it safely */
+ dpm.skipRData();
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+ return getEDNSOptions(reinterpret_cast<const char*>(&packet.at(offset)), packet.size() - offset, options) == 0;
+ }
+ dpm.skipRData();
+ }
+ }
+ catch (...) {
+ return false;
+ }
+
+ return true;
+}
+
/* extract all EDNS0 options from a pointer on the beginning rdLen of the OPT RR */
int getEDNSOptions(const char* optRR, const size_t len, EDNSOptionViewMap& options)
{
diff -Nru pdns-recursor-5.2.2/ednsoptions.hh pdns-recursor-5.2.4/ednsoptions.hh
--- pdns-recursor-5.2.2/ednsoptions.hh 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/ednsoptions.hh 2025-07-17 14:20:08.000000000 +0200
@@ -22,6 +22,8 @@
#pragma once
#include "namespaces.hh"
+#include "noinitvector.hh"
+
struct EDNSOptionCode
{
enum EDNSOptionCodeEnum {NSID=3, DAU=5, DHU=6, N3U=7, ECS=8, EXPIRE=9, COOKIE=10, TCPKEEPALIVE=11, PADDING=12, CHAIN=13, KEYTAG=14, EXTENDEDERROR=15};
@@ -54,3 +56,4 @@
bool getNextEDNSOption(const char* data, size_t dataLen, uint16_t& optionCode, uint16_t& optionLen);
void generateEDNSOption(uint16_t optionCode, const std::string& payload, std::string& res);
+bool slowParseEDNSOptions(const PacketBuffer& packet, EDNSOptionViewMap& options);
diff -Nru pdns-recursor-5.2.2/iputils.hh pdns-recursor-5.2.4/iputils.hh
--- pdns-recursor-5.2.2/iputils.hh 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/iputils.hh 2025-07-17 14:20:08.000000000 +0200
@@ -733,6 +733,11 @@
return std::tie(d_network, d_bits) == std::tie(rhs.d_network, rhs.d_bits);
}
+ bool operator!=(const Netmask& rhs) const
+ {
+ return !(*this == rhs);
+ }
+
[[nodiscard]] bool empty() const
{
return d_network.sin4.sin_family == 0;
diff -Nru pdns-recursor-5.2.2/lwres.cc pdns-recursor-5.2.4/lwres.cc
--- pdns-recursor-5.2.2/lwres.cc 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/lwres.cc 2025-07-17 14:20:08.000000000 +0200
@@ -58,6 +58,7 @@
thread_local TCPOutConnectionManager t_tcp_manager;
std::shared_ptr<Logr::Logger> g_slogout;
bool g_paddingOutgoing;
+bool g_ECSHardening;
void remoteLoggerQueueData(RemoteLoggerInterface& rli, const std::string& data)
{
@@ -422,18 +423,13 @@
pw.getHeader()->cd = (sendRDQuery && g_dnssecmode != DNSSECMode::Off);
string ping;
- bool weWantEDNSSubnet = false;
- uint8_t outgoingECSBits = 0;
- ComboAddress outgoingECSAddr;
+ std::optional<EDNSSubnetOpts> subnetOpts = std::nullopt;
if (EDNS0Level > 0) {
DNSPacketWriter::optvect_t opts;
if (srcmask) {
- EDNSSubnetOpts eo;
- eo.source = *srcmask;
- outgoingECSBits = srcmask->getBits();
- outgoingECSAddr = srcmask->getNetwork();
- opts.emplace_back(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(eo));
- weWantEDNSSubnet = true;
+ subnetOpts = EDNSSubnetOpts{};
+ subnetOpts->source = *srcmask;
+ opts.emplace_back(EDNSOptionCode::ECS, makeEDNSSubnetOptsString(*subnetOpts));
}
if (dnsOverTLS && g_paddingOutgoing) {
@@ -478,7 +474,7 @@
if (!doTCP) {
int queryfd;
- ret = asendto(vpacket.data(), vpacket.size(), 0, address, qid, domain, type, weWantEDNSSubnet, &queryfd, *now);
+ ret = asendto(vpacket.data(), vpacket.size(), 0, address, qid, domain, type, subnetOpts, &queryfd, *now);
if (ret != LWResult::Result::Success) {
return ret;
@@ -502,7 +498,7 @@
#endif /* HAVE_FSTRM */
// sleep until we see an answer to this, interface to mtasker
- ret = arecvfrom(buf, 0, address, len, qid, domain, type, queryfd, *now);
+ ret = arecvfrom(buf, 0, address, len, qid, domain, type, queryfd, subnetOpts, *now);
}
else {
bool isNew;
@@ -599,24 +595,37 @@
lwr->d_records.push_back(answer);
}
- EDNSOpts edo;
- if (EDNS0Level > 0 && getEDNSOpts(mdp, &edo)) {
+ if (EDNSOpts edo; EDNS0Level > 0 && getEDNSOpts(mdp, &edo)) {
lwr->d_haveEDNS = true;
- if (weWantEDNSSubnet) {
- for (const auto& opt : edo.d_options) {
- if (opt.first == EDNSOptionCode::ECS) {
- EDNSSubnetOpts reso;
- if (getEDNSSubnetOptsFromString(opt.second, &reso)) {
- /* rfc7871 states that 0 "indicate[s] that the answer is suitable for all addresses in FAMILY",
- so we might want to still pass the information along to be able to differentiate between
- IPv4 and IPv6. Still I'm pretty sure it doesn't matter in real life, so let's not duplicate
- entries in our cache. */
- if (reso.scope.getBits()) {
- uint8_t bits = std::min(reso.scope.getBits(), outgoingECSBits);
- outgoingECSAddr.truncate(bits);
- srcmask = Netmask(outgoingECSAddr, bits);
- }
+ // If we sent out ECS, we can also expect to see a return with or without ECS, the absent case
+ // is not handled explicitly. If we do see a ECS in the reply, the source part *must* match
+ // with what we sent out. See https://www.rfc-editor.org/rfc/rfc7871#section-7.3. and section
+ // 11.2.
+ // For ECS hardening mode, the case where we sent out an ECS but did not receive a matching
+ // one is handled in arecvfrom().
+ if (subnetOpts) {
+ // THE RFC is not clear about the case of having multiple ECS options. We only look at the first.
+ if (const auto opt = edo.getFirstOption(EDNSOptionCode::ECS); opt != edo.d_options.end()) {
+ EDNSSubnetOpts reso;
+ if (getEDNSSubnetOptsFromString(opt->second, &reso)) {
+ if (!doTCP && reso.source != subnetOpts->source) {
+ g_slogout->info(Logr::Notice, "Incoming ECS does not match outgoing",
+ "server", Logging::Loggable(address),
+ "qname", Logging::Loggable(domain),
+ "outgoing", Logging::Loggable(subnetOpts->source),
+ "incoming", Logging::Loggable(reso.source));
+ return LWResult::Result::Spoofed;
+ }
+ /* rfc7871 states that 0 "indicate[s] that the answer is suitable for all addresses in FAMILY",
+ so we might want to still pass the information along to be able to differentiate between
+ IPv4 and IPv6. Still I'm pretty sure it doesn't matter in real life, so let's not duplicate
+ entries in our cache. */
+ if (reso.scope.getBits() != 0) {
+ uint8_t bits = std::min(reso.scope.getBits(), subnetOpts->source.getBits());
+ auto outgoingECSAddr = subnetOpts->source.getNetwork();
+ outgoingECSAddr.truncate(bits);
+ srcmask = Netmask(outgoingECSAddr, bits);
}
}
}
diff -Nru pdns-recursor-5.2.2/lwres.hh pdns-recursor-5.2.4/lwres.hh
--- pdns-recursor-5.2.2/lwres.hh 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/lwres.hh 2025-07-17 14:20:08.000000000 +0200
@@ -51,6 +51,7 @@
extern std::shared_ptr<Logr::Logger> g_slogout;
extern bool g_paddingOutgoing;
+extern bool g_ECSHardening;
class LWResException : public PDNSException
{
@@ -71,6 +72,7 @@
OSLimitError = 3,
Spoofed = 4, /* Spoofing attempt (too many near-misses) */
ChainLimitError = 5,
+ ECSMissing = 6,
};
[[nodiscard]] static bool isLimitError(Result res)
@@ -86,9 +88,11 @@
bool d_haveEDNS{false};
};
+struct EDNSSubnetOpts;
+
LWResult::Result asendto(const void* data, size_t len, int flags, const ComboAddress& toAddress, uint16_t qid,
- const DNSName& domain, uint16_t qtype, bool ecs, int* fileDesc, timeval& now);
+ const DNSName& domain, uint16_t qtype, const std::optional<EDNSSubnetOpts>& ecs, int* fileDesc, timeval& now);
LWResult::Result arecvfrom(PacketBuffer& packet, int flags, const ComboAddress& fromAddr, size_t& len, uint16_t qid,
- const DNSName& domain, uint16_t qtype, int fileDesc, const struct timeval& now);
+ const DNSName& domain, uint16_t qtype, int fileDesc, const std::optional<EDNSSubnetOpts>& ecs, const struct timeval& now);
LWResult::Result asyncresolve(const ComboAddress& address, const DNSName& domain, int type, bool doTCP, bool sendRDQuery, int EDNS0Level, struct timeval* now, boost::optional<Netmask>& srcmask, const ResolveContext& context, const std::shared_ptr<std::vector<std::unique_ptr<RemoteLogger>>>& outgoingLoggers, const std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>>& fstrmLoggers, const std::set<uint16_t>& exportTypes, LWResult* lwr, bool* chained);
diff -Nru pdns-recursor-5.2.2/metrics_table.py pdns-recursor-5.2.4/metrics_table.py
--- pdns-recursor-5.2.2/metrics_table.py 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/metrics_table.py 2025-07-17 14:20:08.000000000 +0200
@@ -1401,4 +1401,10 @@
'pname': 'proxy-mapping-total-n-0', # For multicounters, state the first
# No SNMP
},
+ {
+ 'name': 'ecs-missing',
+ 'lambda': '[] { return g_Counters.sum(rec::Counter::ecsMissingCount); }',
+ 'desc': 'Number of answers where ECS info was missing',
+ 'snmp': 153,
+ },
]
diff -Nru pdns-recursor-5.2.2/pdns_recursor.1 pdns-recursor-5.2.4/pdns_recursor.1
--- pdns-recursor-5.2.2/pdns_recursor.1 2025-04-08 12:42:41.000000000 +0200
+++ pdns-recursor-5.2.4/pdns_recursor.1 2025-07-17 14:22:43.000000000 +0200
@@ -27,7 +27,7 @@
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "PDNS_RECURSOR" "1" "Apr 08, 2025" "" "PowerDNS Recursor"
+.TH "PDNS_RECURSOR" "1" "Jul 17, 2025" "" "PowerDNS Recursor"
.SH NAME
pdns_recursor \- The PowerDNS Recursor binary
.SH SYNOPSIS
diff -Nru pdns-recursor-5.2.2/pdns_recursor.cc pdns-recursor-5.2.4/pdns_recursor.cc
--- pdns-recursor-5.2.2/pdns_recursor.cc 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/pdns_recursor.cc 2025-07-17 14:20:08.000000000 +0200
@@ -30,8 +30,8 @@
#include "rec-taskqueue.hh"
#include "shuffle.hh"
#include "validate-recursor.hh"
-
#include "ratelimitedlog.hh"
+#include "ednsoptions.hh"
#ifdef HAVE_SYSTEMD
#include <systemd/sd-daemon.h>
@@ -224,7 +224,6 @@
else {
PacketBuffer empty;
g_multiTasker->sendEvent(pident, &empty);
- // cerr<<"Had some kind of error: "<<ret<<", "<<stringerror()<<endl;
}
}
@@ -277,45 +276,43 @@
/* these two functions are used by LWRes */
LWResult::Result asendto(const void* data, size_t len, int /* flags */,
- const ComboAddress& toAddress, uint16_t qid, const DNSName& domain, uint16_t qtype, bool ecs, int* fileDesc, timeval& now)
+ const ComboAddress& toAddress, uint16_t qid, const DNSName& domain, uint16_t qtype, const std::optional<EDNSSubnetOpts>& ecs, int* fileDesc, timeval& now)
{
auto pident = std::make_shared<PacketID>();
pident->domain = domain;
pident->remote = toAddress;
pident->type = qtype;
+ if (ecs) {
+ pident->ecsSubnet = ecs->source;
+ }
- // We cannot merge ECS-enabled queries based on the ECS source only, as the scope
- // of the response might be narrower, so instead we do not chain ECS-enabled queries
- // at all.
- if (!ecs) {
- // See if there is an existing outstanding request we can chain on to, using partial equivalence
- // function looking for the same query (qname and qtype) to the same host, but with a different
- // message ID.
- auto chain = g_multiTasker->getWaiters().equal_range(pident, PacketIDBirthdayCompare());
-
- for (; chain.first != chain.second; chain.first++) {
- // Line below detected an issue with the two ways of ordering PacketIDs (birthday and non-birthday)
- assert(chain.first->key->domain == pident->domain); // NOLINT
- // don't chain onto existing chained waiter or a chain already processed
- if (chain.first->key->fd > -1 && !chain.first->key->closed) {
- auto currentChainSize = chain.first->key->authReqChain.size();
- *fileDesc = -static_cast<int>(currentChainSize + 1); // value <= -1, gets used in waitEvent / sendEvent later on
- if (g_maxChainLength > 0 && currentChainSize >= g_maxChainLength) {
- return LWResult::Result::ChainLimitError;
- }
- assert(uSec(chain.first->key->creationTime) != 0); // NOLINT
- auto age = now - chain.first->key->creationTime;
- if (uSec(age) > static_cast<uint64_t>(1000) * authWaitTimeMSec(g_multiTasker) * 2 / 3) {
- return LWResult::Result::ChainLimitError;
- }
- chain.first->key->authReqChain.emplace(*fileDesc, qid); // we can chain
- auto maxLength = t_Counters.at(rec::Counter::maxChainLength);
- if (currentChainSize + 1 > maxLength) {
- t_Counters.at(rec::Counter::maxChainLength) = currentChainSize + 1;
- }
- return LWResult::Result::Success;
+ // See if there is an existing outstanding request we can chain on to, using partial equivalence
+ // function looking for the same query (qname, qtype and ecs if applicable) to the same host, but
+ // with a different message ID.
+ auto chain = g_multiTasker->getWaiters().equal_range(pident, PacketIDBirthdayCompare());
+
+ for (; chain.first != chain.second; chain.first++) {
+ // Line below detected an issue with the two ways of ordering PacketIDs (birthday and non-birthday)
+ assert(chain.first->key->domain == pident->domain); // NOLINT
+ // don't chain onto existing chained waiter or a chain already processed
+ if (chain.first->key->fd > -1 && !chain.first->key->closed && pident->ecsSubnet == chain.first->key->ecsSubnet) {
+ auto currentChainSize = chain.first->key->authReqChain.size();
+ *fileDesc = -static_cast<int>(currentChainSize + 1); // value <= -1, gets used in waitEvent / sendEvent later on
+ if (g_maxChainLength > 0 && currentChainSize >= g_maxChainLength) {
+ return LWResult::Result::ChainLimitError;
+ }
+ assert(uSec(chain.first->key->creationTime) != 0); // NOLINT
+ auto age = now - chain.first->key->creationTime;
+ if (uSec(age) > static_cast<uint64_t>(1000) * authWaitTimeMSec(g_multiTasker) * 2 / 3) {
+ return LWResult::Result::ChainLimitError;
+ }
+ chain.first->key->authReqChain.emplace(*fileDesc, qid); // we can chain
+ auto maxLength = t_Counters.at(rec::Counter::maxChainLength);
+ if (currentChainSize + 1 > maxLength) {
+ t_Counters.at(rec::Counter::maxChainLength) = currentChainSize + 1;
}
+ return LWResult::Result::Success;
}
}
@@ -341,8 +338,10 @@
return LWResult::Result::Success;
}
+static bool checkIncomingECSSource(const PacketBuffer& packet, const Netmask& subnet);
+
LWResult::Result arecvfrom(PacketBuffer& packet, int /* flags */, const ComboAddress& fromAddr, size_t& len,
- uint16_t qid, const DNSName& domain, uint16_t qtype, int fileDesc, const struct timeval& now)
+ uint16_t qid, const DNSName& domain, uint16_t qtype, int fileDesc, const std::optional<EDNSSubnetOpts>& ecs, const struct timeval& now)
{
static const unsigned int nearMissLimit = ::arg().asNum("spoof-nearmiss-max");
@@ -353,7 +352,13 @@
pident->type = qtype;
pident->remote = fromAddr;
pident->creationTime = now;
-
+ if (ecs) {
+ // We sent out the query using ecs
+ // We expect incoming source ECS to match, see https://www.rfc-editor.org/rfc/rfc7871#section-7.3
+ // But there's also section 11-2, which says we should treat absent incoming ecs as scope zero
+ // We fill in the search key with the ecs we sent out, so both cases are covered and accepted here.
+ pident->ecsSubnet = ecs->source;
+ }
int ret = g_multiTasker->waitEvent(pident, &packet, authWaitTimeMSec(g_multiTasker), &now);
len = 0;
@@ -366,6 +371,12 @@
len = packet.size();
+ // In ecs hardening mode, we consider a missing or a mismatched ECS in the reply as a case for
+ // retrying without ECS. The actual logic to do that is in Syncres::doResolveAtThisIP()
+ if (g_ECSHardening && pident->ecsSubnet && !checkIncomingECSSource(packet, *pident->ecsSubnet)) {
+ t_Counters.at(rec::Counter::ecsMissingCount)++;
+ return LWResult::Result::ECSMissing;
+ }
if (nearMissLimit > 0 && pident->nearMisses > nearMissLimit) {
/* we have received more than nearMissLimit answers on the right IP and port, from the right source (we are using connected sockets),
for the correct qname and qtype, but with an unexpected message ID. That looks like a spoofing attempt. */
@@ -2064,7 +2075,7 @@
/* we need to pass the record len */
int res = getEDNSOptions(reinterpret_cast<const char*>(&question.at(pos - sizeof(drh->d_clen))), questionLen - pos + (sizeof(drh->d_clen)), *options); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
if (res == 0) {
- const auto& iter = options->find(EDNSOptionCode::ECS);
+ const auto iter = options->find(EDNSOptionCode::ECS);
if (iter != options->end() && !iter->second.values.empty() && iter->second.values.at(0).content != nullptr && iter->second.values.at(0).size > 0) {
EDNSSubnetOpts eso;
if (getEDNSSubnetOptsFromString(iter->second.values.at(0).content, iter->second.values.at(0).size, &eso)) {
@@ -2671,7 +2682,6 @@
}
}
else {
- // cerr<<t_id<<" had error: "<<stringerror()<<endl;
if (firstQuery && errno == EAGAIN) {
t_Counters.at(rec::Counter::noPacketError)++;
}
@@ -2923,6 +2933,32 @@
assert(g_multiTasker->waitEvent(neverHappens, nullptr, jitterMsec) != -1); // NOLINT
}
+static bool checkIncomingECSSource(const PacketBuffer& packet, const Netmask& subnet)
+{
+ bool foundMatchingECS = false;
+
+ // We sent out ECS, check if the response has the expected ECS info
+ EDNSOptionViewMap ednsOptions;
+ if (slowParseEDNSOptions(packet, ednsOptions)) {
+ // check content
+ auto option = ednsOptions.find(EDNSOptionCode::ECS);
+ if (option != ednsOptions.end()) {
+ // found an ECS option
+ EDNSSubnetOpts ecs;
+ for (const auto& value : option->second.values) {
+ if (getEDNSSubnetOptsFromString(value.content, value.size, &ecs)) {
+ if (ecs.source == subnet) {
+ foundMatchingECS = true;
+ }
+ }
+ break; // The RFC isn't clear about multiple ECS options. We chose to handle it like cookies
+ // and only look at the first.
+ }
+ }
+ }
+ return foundMatchingECS;
+}
+
static void handleUDPServerResponse(int fileDesc, FDMultiplexer::funcparam_t& var)
{
auto pid = boost::any_cast<std::shared_ptr<PacketID>>(var);
@@ -3023,7 +3059,6 @@
// be a bit paranoid here since we're weakening our matching
if (pident->domain.empty() && !d_waiter.key->domain.empty() && pident->type == 0 && d_waiter.key->type != 0 && pident->id == d_waiter.key->id && d_waiter.key->remote == pident->remote) {
- // cerr<<"Empty response, rest matches though, sending to a waiter"<<endl;
pident->domain = d_waiter.key->domain;
pident->type = d_waiter.key->type;
goto retryWithName; // note that this only passes on an error, lwres will still reject the packet NOLINT(cppcoreguidelines-avoid-goto)
diff -Nru pdns-recursor-5.2.2/rec_control.1 pdns-recursor-5.2.4/rec_control.1
--- pdns-recursor-5.2.2/rec_control.1 2025-04-08 12:42:41.000000000 +0200
+++ pdns-recursor-5.2.4/rec_control.1 2025-07-17 14:22:43.000000000 +0200
@@ -27,7 +27,7 @@
.\" new: \\n[rst2man-indent\\n[rst2man-indent-level]]
.in \\n[rst2man-indent\\n[rst2man-indent-level]]u
..
-.TH "REC_CONTROL" "1" "Apr 08, 2025" "" "PowerDNS Recursor"
+.TH "REC_CONTROL" "1" "Jul 17, 2025" "" "PowerDNS Recursor"
.SH NAME
rec_control \- Command line tool to control a running Recursor
.SH SYNOPSIS
diff -Nru pdns-recursor-5.2.2/rec-main.cc pdns-recursor-5.2.4/rec-main.cc
--- pdns-recursor-5.2.2/rec-main.cc 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/rec-main.cc 2025-07-17 14:20:08.000000000 +0200
@@ -2254,6 +2254,7 @@
}
g_paddingTag = ::arg().asNum("edns-padding-tag");
g_paddingOutgoing = ::arg().mustDo("edns-padding-out");
+ g_ECSHardening = ::arg().mustDo("edns-subnet-harden");
RecThreadInfo::setNumDistributorThreads(::arg().asNum("distributor-threads"));
RecThreadInfo::setNumUDPWorkerThreads(::arg().asNum("threads"));
diff -Nru pdns-recursor-5.2.2/rec-metrics-gen.h pdns-recursor-5.2.4/rec-metrics-gen.h
--- pdns-recursor-5.2.2/rec-metrics-gen.h 2025-04-08 12:42:19.000000000 +0200
+++ pdns-recursor-5.2.4/rec-metrics-gen.h 2025-07-17 14:22:18.000000000 +0200
@@ -279,3 +279,4 @@
addGetStat("remote-logger-count", []() {
return toRemoteLoggerStatsMap("remote-logger-count");
});
+addGetStat("ecs-missing", [] { return g_Counters.sum(rec::Counter::ecsMissingCount); });
diff -Nru pdns-recursor-5.2.2/rec-oids-gen.h pdns-recursor-5.2.4/rec-oids-gen.h
--- pdns-recursor-5.2.2/rec-oids-gen.h 2025-04-08 12:42:19.000000000 +0200
+++ pdns-recursor-5.2.4/rec-oids-gen.h 2025-07-17 14:22:18.000000000 +0200
@@ -171,3 +171,4 @@
static const oid10 maxChainWeightOID = {RECURSOR_STATS_OID, 150};
static const oid10 chainLimitsOID = {RECURSOR_STATS_OID, 151};
static const oid10 tcpOverflowOID = {RECURSOR_STATS_OID, 152};
+static const oid10 ecsMissingOID = {RECURSOR_STATS_OID, 153};
diff -Nru pdns-recursor-5.2.2/rec-prometheus-gen.h pdns-recursor-5.2.4/rec-prometheus-gen.h
--- pdns-recursor-5.2.2/rec-prometheus-gen.h 2025-04-08 12:42:19.000000000 +0200
+++ pdns-recursor-5.2.4/rec-prometheus-gen.h 2025-07-17 14:22:18.000000000 +0200
@@ -197,3 +197,4 @@
{"cumul-authanswers-count4", MetricDefinition(PrometheusMetricType::histogram, "Cumulative counts of answer times to clients in buckets less than x microseconds.")},
{"policy-hits", MetricDefinition(PrometheusMetricType::multicounter, "Number of policy decisions based on Lua")},
{"proxy-mapping-total-n-0", MetricDefinition(PrometheusMetricType::multicounter, "Proxy mappings done")},
+{"ecs-missing", MetricDefinition(PrometheusMetricType::counter, "Number of answers where ECS info was missing")},
diff -Nru pdns-recursor-5.2.2/rec-snmp-gen.h pdns-recursor-5.2.4/rec-snmp-gen.h
--- pdns-recursor-5.2.2/rec-snmp-gen.h 2025-04-08 12:42:19.000000000 +0200
+++ pdns-recursor-5.2.4/rec-snmp-gen.h 2025-07-17 14:22:18.000000000 +0200
@@ -171,3 +171,4 @@
registerCounter64Stat("max-chain-weight", maxChainWeightOID);
registerCounter64Stat("chain-limits", chainLimitsOID);
registerCounter64Stat("tcp-overflow", tcpOverflowOID);
+registerCounter64Stat("ecs-missing", ecsMissingOID);
diff -Nru pdns-recursor-5.2.2/rec-tcounters.hh pdns-recursor-5.2.4/rec-tcounters.hh
--- pdns-recursor-5.2.2/rec-tcounters.hh 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/rec-tcounters.hh 2025-07-17 14:20:08.000000000 +0200
@@ -98,6 +98,7 @@
maxChainLength,
maxChainWeight,
chainLimits,
+ ecsMissingCount,
numberOfCounters
};
diff -Nru pdns-recursor-5.2.2/RECURSOR-MIB.in pdns-recursor-5.2.4/RECURSOR-MIB.in
--- pdns-recursor-5.2.2/RECURSOR-MIB.in 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/RECURSOR-MIB.in 2025-07-17 14:20:08.000000000 +0200
@@ -21,6 +21,9 @@
DESCRIPTION
"This MIB module describes information gathered through PowerDNS Recursor."
+ REVISION "202505270000Z"
+ DESCRIPTION "Added metric for missing ECS in reply"
+
REVISION "202408280000Z"
DESCRIPTION "Added metric for too many incoming TCP connections"
diff -Nru pdns-recursor-5.2.2/RECURSOR-MIB.txt pdns-recursor-5.2.4/RECURSOR-MIB.txt
--- pdns-recursor-5.2.2/RECURSOR-MIB.txt 2025-04-08 12:42:19.000000000 +0200
+++ pdns-recursor-5.2.4/RECURSOR-MIB.txt 2025-07-17 14:22:18.000000000 +0200
@@ -21,6 +21,9 @@
DESCRIPTION
"This MIB module describes information gathered through PowerDNS Recursor."
+ REVISION "202505270000Z"
+ DESCRIPTION "Added metric for missing ECS in reply"
+
REVISION "202408280000Z"
DESCRIPTION "Added metric for too many incoming TCP connections"
@@ -1291,6 +1294,14 @@
"Incoming TCP limits reached"
::= { stats 152 }
+ecsMissing OBJECT-TYPE
+ SYNTAX Counter64
+ MAX-ACCESS read-only
+ STATUS current
+ DESCRIPTION
+ "Number of answers where ECS info was missing"
+ ::= { stats 153 }
+
---
--- Traps / Notifications
---
@@ -1489,7 +1500,8 @@
maxChainLength,
maxChainWeight,
chainLimits,
- tcpOverflow
+ tcpOverflow,
+ ecsMissing
}
STATUS current
DESCRIPTION "Objects conformance group for PowerDNS Recursor"
diff -Nru pdns-recursor-5.2.2/settings/cxxsettings-generated.cc pdns-recursor-5.2.4/settings/cxxsettings-generated.cc
--- pdns-recursor-5.2.2/settings/cxxsettings-generated.cc 2025-04-08 12:42:42.000000000 +0200
+++ pdns-recursor-5.2.4/settings/cxxsettings-generated.cc 2025-07-17 14:22:44.000000000 +0200
@@ -64,6 +64,7 @@
::arg().set("edns-padding-tag", "Packetcache tag associated to responses sent with EDNS padding, to prevent sending these to clients for which padding is not enabled.") = "7830";
::arg().set("edns-subnet-whitelist", "List of netmasks and domains that we should enable EDNS subnet for (deprecated)") = "";
::arg().set("edns-subnet-allow-list", "List of netmasks and domains that we should enable EDNS subnet for") = "";
+ ::arg().setSwitch("edns-subnet-harden", "Do more strict checking or EDNS Client Subnet information returned by authoritative servers") = "no";
::arg().setSwitch("enable-old-settings", "Enable (deprecated) parsing of old-style settings") = "no";
::arg().set("entropy-source", "If set, read entropy from this file") = "/dev/urandom";
::arg().set("etc-hosts-file", "Path to 'hosts' file") = "/etc/hosts";
@@ -310,6 +311,7 @@
settings.outgoing.edns_padding = arg().mustDo("edns-padding-out");
settings.incoming.edns_padding_tag = static_cast<uint64_t>(arg().asNum("edns-padding-tag"));
settings.outgoing.edns_subnet_allow_list = getStrings("edns-subnet-allow-list");
+ settings.outgoing.edns_subnet_harden = arg().mustDo("edns-subnet-harden");
settings.recursor.etc_hosts_file = arg()["etc-hosts-file"];
settings.recursor.event_trace_enabled = static_cast<uint64_t>(arg().asNum("event-trace-enabled"));
settings.recursor.export_etc_hosts = arg().mustDo("export-etc-hosts");
@@ -864,6 +866,13 @@
to_yaml(rustvalue.vec_string_val, value);
return true;
}
+ if (key == "edns-subnet-harden") {
+ section = "outgoing";
+ fieldname = "edns_subnet_harden";
+ type_name = "bool";
+ to_yaml(rustvalue.bool_val, value);
+ return true;
+ }
if (key == "etc-hosts-file") {
section = "recursor";
fieldname = "etc_hosts_file";
@@ -2010,6 +2019,7 @@
::arg().set("edns-padding-out") = to_arg(settings.outgoing.edns_padding);
::arg().set("edns-padding-tag") = to_arg(settings.incoming.edns_padding_tag);
::arg().set("edns-subnet-allow-list") = to_arg(settings.outgoing.edns_subnet_allow_list);
+ ::arg().set("edns-subnet-harden") = to_arg(settings.outgoing.edns_subnet_harden);
::arg().set("etc-hosts-file") = to_arg(settings.recursor.etc_hosts_file);
::arg().set("event-trace-enabled") = to_arg(settings.recursor.event_trace_enabled);
::arg().set("export-etc-hosts") = to_arg(settings.recursor.export_etc_hosts);
diff -Nru pdns-recursor-5.2.2/settings/rust/src/lib.rs pdns-recursor-5.2.4/settings/rust/src/lib.rs
--- pdns-recursor-5.2.2/settings/rust/src/lib.rs 2025-04-08 12:42:42.000000000 +0200
+++ pdns-recursor-5.2.4/settings/rust/src/lib.rs 2025-07-17 14:22:44.000000000 +0200
@@ -925,6 +925,9 @@
edns_subnet_allow_list: Vec<String>,
#[serde(default, skip_serializing_if = "crate::is_default")]
+ edns_subnet_harden: bool,
+
+ #[serde(default, skip_serializing_if = "crate::is_default")]
lowercase: bool,
#[serde(default, skip_serializing_if = "crate::is_default")]
@@ -2048,6 +2051,9 @@
}
merge_vec(&mut self.edns_subnet_allow_list, &mut rhs.edns_subnet_allow_list);
}
+ if m.contains_key("edns_subnet_harden") {
+ rhs.edns_subnet_harden.clone_into(&mut self.edns_subnet_harden);
+ }
if m.contains_key("lowercase") {
rhs.lowercase.clone_into(&mut self.lowercase);
}
diff -Nru pdns-recursor-5.2.2/settings/table.py pdns-recursor-5.2.4/settings/table.py
--- pdns-recursor-5.2.2/settings/table.py 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/settings/table.py 2025-07-17 14:20:08.000000000 +0200
@@ -952,6 +952,18 @@
'versionadded': '4.5.0'
},
{
+ 'name' : 'edns_subnet_harden',
+ 'section' : 'outgoing',
+ 'type' : LType.Bool,
+ 'default' : 'false',
+ 'help' : 'Do more strict checking or EDNS Client Subnet information returned by authoritative servers',
+ 'doc' : '''
+Do more strict checking or EDNS Client Subnet information returned by authoritative servers.
+Answers missing ECS information will be ignored and followed up by an ECS-less query.
+ ''',
+ 'versionadded': ['5.2.x', '5.1.x', '5.0.x']
+ },
+ {
'name' : 'enable_old_settings',
'section' : 'recursor',
'type' : LType.Bool,
diff -Nru pdns-recursor-5.2.2/syncres.cc pdns-recursor-5.2.4/syncres.cc
--- pdns-recursor-5.2.2/syncres.cc 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/syncres.cc 2025-07-17 14:20:08.000000000 +0200
@@ -5490,19 +5490,24 @@
}
}
-bool SyncRes::doResolveAtThisIP(const std::string& prefix, const DNSName& qname, const QType qtype, LWResult& lwr, boost::optional<Netmask>& ednsmask, const DNSName& auth, bool const sendRDQuery, const bool wasForwarded, const DNSName& nsName, const ComboAddress& remoteIP, bool doTCP, bool doDoT, bool& truncated, bool& spoofed, boost::optional<EDNSExtendedError>& extendedError, bool dontThrottle)
+void SyncRes::checkTotalTime(const DNSName& qname, QType qtype, boost::optional<EDNSExtendedError>& extendedError) const
{
- bool chained = false;
- LWResult::Result resolveret = LWResult::Result::Success;
-
if (s_maxtotusec != 0 && d_totUsec > s_maxtotusec) {
if (s_addExtendedResolutionDNSErrors) {
extendedError = EDNSExtendedError{static_cast<uint16_t>(EDNSExtendedError::code::NoReachableAuthority), "Timeout waiting for answer(s)"};
}
throw ImmediateServFailException("Too much time waiting for " + qname.toLogString() + "|" + qtype.toString() + ", timeouts: " + std::to_string(d_timeouts) + ", throttles: " + std::to_string(d_throttledqueries) + ", queries: " + std::to_string(d_outqueries) + ", " + std::to_string(d_totUsec / 1000) + " ms");
}
+}
+
+bool SyncRes::doResolveAtThisIP(const std::string& prefix, const DNSName& qname, const QType qtype, LWResult& lwr, boost::optional<Netmask>& ednsmask, const DNSName& auth, bool const sendRDQuery, const bool wasForwarded, const DNSName& nsName, const ComboAddress& remoteIP, bool doTCP, bool doDoT, bool& truncated, bool& spoofed, boost::optional<EDNSExtendedError>& extendedError, bool dontThrottle)
+{
+ checkTotalTime(qname, qtype, extendedError);
+ bool chained = false;
+ LWResult::Result resolveret = LWResult::Result::Success;
int preOutQueryRet = RCode::NoError;
+
if (d_pdl && d_pdl->preoutquery(remoteIP, d_requestor, qname, qtype, doTCP, lwr.d_records, preOutQueryRet, d_eventTrace, timeval{0, 0})) {
LOG(prefix << qname << ": Query handled by Lua" << endl);
}
@@ -5516,6 +5521,13 @@
resolveret = asyncresolveWrapper(remoteIP, d_doDNSSEC, qname, auth, qtype.getCode(),
doTCP, sendRDQuery, &d_now, ednsmask, &lwr, &chained, nsName); // <- we go out on the wire!
ednsStats(ednsmask, qname, prefix);
+ if (resolveret == LWResult::Result::ECSMissing) {
+ ednsmask = boost::none;
+ LOG(prefix << qname << ": Answer has no ECS, trying again without EDNS Client Subnet Mask" << endl);
+ updateQueryCounts(prefix, qname, remoteIP, doTCP, doDoT);
+ resolveret = asyncresolveWrapper(remoteIP, d_doDNSSEC, qname, auth, qtype.getCode(),
+ doTCP, sendRDQuery, &d_now, ednsmask, &lwr, &chained, nsName);
+ }
}
/* preoutquery killed the query by setting dq.rcode to -3 */
diff -Nru pdns-recursor-5.2.2/syncres.hh pdns-recursor-5.2.4/syncres.hh
--- pdns-recursor-5.2.2/syncres.hh 2025-04-08 12:40:39.000000000 +0200
+++ pdns-recursor-5.2.4/syncres.hh 2025-07-17 14:20:08.000000000 +0200
@@ -634,6 +634,7 @@
std::map<DNSName, std::vector<ComboAddress>>* fallback);
void ednsStats(boost::optional<Netmask>& ednsmask, const DNSName& qname, const string& prefix);
void incTimeoutStats(const ComboAddress& remoteIP);
+ void checkTotalTime(const DNSName& qname, QType qtype, boost::optional<EDNSExtendedError>& extendedError) const;
bool doResolveAtThisIP(const std::string& prefix, const DNSName& qname, QType qtype, LWResult& lwr, boost::optional<Netmask>& ednsmask, const DNSName& auth, bool sendRDQuery, bool wasForwarded, const DNSName& nsName, const ComboAddress& remoteIP, bool doTCP, bool doDoT, bool& truncated, bool& spoofed, boost::optional<EDNSExtendedError>& extendedError, bool dontThrottle = false);
bool processAnswer(unsigned int depth, const string& prefix, LWResult& lwr, const DNSName& qname, QType qtype, DNSName& auth, bool wasForwarded, const boost::optional<Netmask>& ednsmask, bool sendRDQuery, NsSet& nameservers, std::vector<DNSRecord>& ret, const DNSFilterEngine& dfe, bool* gotNewServers, int* rcode, vState& state, const ComboAddress& remoteIP);
@@ -781,6 +782,7 @@
mutable chain_t authReqChain;
shared_ptr<TCPIOHandler> tcphandler{nullptr};
timeval creationTime{};
+ std::optional<Netmask> ecsSubnet;
string::size_type inPos{0}; // how far are we along in the inMSG
size_t inWanted{0}; // if this is set, we'll read until inWanted bytes are read
string::size_type outPos{0}; // how far we are along in the outMSG
@@ -803,7 +805,7 @@
inline ostream& operator<<(ostream& ostr, const PacketID& pid)
{
- return ostr << "PacketID(id=" << pid.id << ",remote=" << pid.remote.toString() << ",type=" << pid.type << ",tcpsock=" << pid.tcpsock << ",fd=" << pid.fd << ',' << pid.domain << ')';
+ return ostr << "PacketID(id=" << pid.id << ",remote=" << pid.remote.toString() << ",type=" << pid.type << ",tcpsock=" << pid.tcpsock << ",fd=" << pid.fd << ",name=" << pid.domain << ",ecs=" << (pid.ecsSubnet ? pid.ecsSubnet->toString() : "") << ')';
}
inline ostream& operator<<(ostream& ostr, const shared_ptr<PacketID>& pid)
Reply to: