[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#1109845: unblock: pdns-recursor/5.2.4-2 [pre-approval]



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: 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

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 <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)


-- 
Sebastian Ramacher


Reply to: