Control: tags -1 moreinfo confirmed On 2025-07-25 03:08:11 +0200, Chris Hofstädtler wrote: > Package: release.debian.org > Severity: normal > X-Debbugs-Cc: [email protected] > Control: affects -1 + src:pdns-recursor > User: [email protected] > Usertags: unblock > > Please unblock package pdns-recursor > > [ Reason ] > Upstream security fix for CVE-2025-30192, Debian bug #1109808
Please go ahead but keep in mind that the upload needs to happen very soon. Remove the moreinfo tag once the upload is available in unstable. Cheers > > [ 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 <[email protected]> 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 <[email protected]> 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) -- Sebastian Ramacher

