]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Parse ECS info if relevant and act on it if it mismatches
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Wed, 23 Apr 2025 11:50:51 +0000 (13:50 +0200)
committerOtto Moerbeek <otto.moerbeek@open-xchange.com>
Wed, 28 May 2025 14:10:56 +0000 (16:10 +0200)
Moved slowParseEDNSOptions() from dnsdist specific code to common code

pdns/ednsoptions.cc
pdns/ednsoptions.hh
pdns/recursordist/lwres.cc
pdns/recursordist/lwres.hh
pdns/recursordist/pdns_recursor.cc
pdns/recursordist/rec-tcounters.hh
pdns/recursordist/syncres.cc
pdns/recursordist/syncres.hh

index 8221f7201907e3c2f2ab24410d50d798c981b802..78fda3ae06dc47863641ff37815279665cea76eb 100644 (file)
@@ -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)
 {
@@ -91,6 +92,61 @@ int getEDNSOption(const char* optRR, const size_t len, uint16_t wantedOption, si
   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) {
+    throw std::runtime_error("slowParseEDNSOptions() should not be called for queries that have no EDNS");
+  }
+
+  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)
 {
index fe6e4bf1dcd76eff1254d27babb48521e15797bd..a6bca828e63b3eda5da1dc452471782f4db18414 100644 (file)
@@ -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 getEDNSOptionsFromContent(const std::string& content, std::vector<std::pair
 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);
index a1d6a548508a05ae2ff5b5b472932d7f974facf4..f9ce5444f2329e743c296481ef96a1dd38cb2e37 100644 (file)
@@ -58,6 +58,7 @@
 thread_local TCPOutConnectionManager t_tcp_manager;
 std::shared_ptr<Logr::Logger> g_slogout;
 bool g_paddingOutgoing;
+bool g_ECSHardening{false};
 
 void remoteLoggerQueueData(RemoteLoggerInterface& r, const std::string& data)
 {
@@ -462,7 +463,7 @@ static LWResult::Result asyncresolve(const ComboAddress& address, const DNSName&
   if (!doTCP) {
     int queryfd;
 
-    ret = asendto(vpacket.data(), vpacket.size(), 0, address, qid, domain, type, subnetOpts, &queryfd, *now);
+    ret = asendto(vpacket.data(), vpacket.size(), 0, address, qid, domain, type, subnetOpts, &queryfd);
 
     if (ret != LWResult::Result::Success) {
       return ret;
@@ -580,15 +581,16 @@ static LWResult::Result asyncresolve(const ComboAddress& address, const DNSName&
     if (EDNS0Level > 0 && getEDNSOpts(mdp, &edo)) {
       lwr->d_haveEDNS = true;
 
-      // 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
+      // If we sent out ECS, we can also expect to see a return with or without ECS, the absent case
+      // is not handled explicitly. In hardening mode, 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
       if (subnetOpts) {
         for (const auto& opt : edo.d_options) {
           if (opt.first == EDNSOptionCode::ECS) {
             EDNSSubnetOpts reso;
             if (getEDNSSubnetOptsFromString(opt.second, &reso)) {
-              if (!(reso.source == subnetOpts->source)) {
+              if (g_ECSHardening && !(reso.source == subnetOpts->source)) {
                 g_slogout->info(Logr::Notice, "Incoming ECS does not match outgoing",
                                 "server", Logging::Loggable(address),
                                 "qname", Logging::Loggable(domain),
index c136b5952f08215a6ed4d14da4634eaadd5bc945..60e09df5ee8ccede86950c47da05bf7ed170a3ec 100644 (file)
@@ -51,6 +51,7 @@ void remoteLoggerQueueData(RemoteLoggerInterface&, const std::string&);
 
 extern std::shared_ptr<Logr::Logger> g_slogout;
 extern bool g_paddingOutgoing;
+extern bool g_ECSHardening;
 
 class LWResException : public PDNSException
 {
@@ -72,7 +73,8 @@ public:
     Success = 1,
     PermanentError = 2 /* not transport related */,
     OSLimitError = 3,
-    Spoofed = 4 /* Spoofing attempt (too many near-misses) */
+    Spoofed = 4, /* Spoofing attempt (too many near-misses) */
+    ECSMissing = 5,
   };
 
   vector<DNSRecord> d_records;
@@ -86,7 +88,7 @@ public:
 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, const std::optional<EDNSSubnetOpts>& ecs, int* fileDesc, timeval& now);
+                         const DNSName& domain, uint16_t qtype, const std::optional<EDNSSubnetOpts>& ecs, int* fileDesc);
 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 std::optional<EDNSSubnetOpts>& ecs, const struct timeval& now);
 
index 08a5edaf6150f05bcda3635cdfb3b801faddf0fa..e116284b6db541009c2607d2b65617c5d05d1bae 100644 (file)
@@ -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>
@@ -230,7 +230,6 @@ static void handleGenUDPQueryResponse(int fileDesc, FDMultiplexer::funcparam_t&
   else {
     PacketBuffer empty;
     g_multiTasker->sendEvent(pident, &empty);
-    //    cerr<<"Had some kind of error: "<<ret<<", "<<stringerror()<<endl;
   }
 }
 
@@ -269,7 +268,7 @@ thread_local std::unique_ptr<UDPClientSocks> t_udpclientsocks;
 
 /* 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, const std::optional<EDNSSubnetOpts>& ecs, int* fileDesc, timeval& now)
+                         const ComboAddress& toAddress, uint16_t qid, const DNSName& domain, uint16_t qtype, const std::optional<EDNSSubnetOpts>& ecs, int* fileDesc)
 {
 
   auto pident = std::make_shared<PacketID>();
@@ -279,24 +278,19 @@ LWResult::Result asendto(const void* data, size_t len, int /* flags */,
   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 (true || !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->d_waiters.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) {
-        chain.first->key->chain.insert(qid); // we can chain
-        *fileDesc = -1; // gets used in waitEvent / sendEvent later on
-        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->d_waiters.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) {
+      *fileDesc = -1;
+      return LWResult::Result::Success;
     }
   }
 
@@ -353,6 +347,10 @@ LWResult::Result arecvfrom(PacketBuffer& packet, int /* flags */, const ComboAdd
 
     len = packet.size();
 
+    if (g_ECSHardening && pident->ecsSubnet && !*pident->ecsReceived) {
+      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. */
@@ -2649,7 +2647,6 @@ static void handleNewUDPQuestion(int fileDesc, FDMultiplexer::funcparam_t& /* va
       }
     }
     else {
-      // cerr<<t_id<<" had error: "<<stringerror()<<endl;
       if (firstQuery && errno == EAGAIN) {
         t_Counters.at(rec::Counter::noPacketError)++;
       }
@@ -2861,7 +2858,7 @@ void distributeAsyncFunction(const string& packet, const pipefunc_t& func)
 }
 
 // resend event to everybody chained onto it
-static void doResends(MT_t::waiters_t::iterator& iter, const std::shared_ptr<PacketID>& resend, const PacketBuffer& content)
+static void doResends(MT_t::waiters_t::iterator& iter, const std::shared_ptr<PacketID>& resend, const PacketBuffer& content, const std::optional<bool>& ecsReceived)
 {
   // We close the chain for new entries, since they won't be processed anyway
   iter->key->closed = true;
@@ -2869,6 +2866,10 @@ static void doResends(MT_t::waiters_t::iterator& iter, const std::shared_ptr<Pac
   if (iter->key->chain.empty()) {
     return;
   }
+
+  if (ecsReceived) {
+    iter->key->ecsReceived = ecsReceived;
+  }
   for (auto i = iter->key->chain.begin(); i != iter->key->chain.end(); ++i) {
     auto packetID = std::make_shared<PacketID>(*resend);
     packetID->fd = -1;
@@ -2878,6 +2879,42 @@ static void doResends(MT_t::waiters_t::iterator& iter, const std::shared_ptr<Pac
   }
 }
 
+void mthreadSleep(unsigned int jitterMsec)
+{
+  auto neverHappens = std::make_shared<PacketID>();
+  neverHappens->id = dns_random_uint16();
+  neverHappens->type = dns_random_uint16();
+  neverHappens->remote = ComboAddress("100::"); // discard-only
+  neverHappens->remote.setPort(dns_random_uint16());
+  neverHappens->fd = -1;
+  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; // only look at first
+      }
+    }
+  }
+  return foundMatchingECS;
+}
+
 static void handleUDPServerResponse(int fileDesc, FDMultiplexer::funcparam_t& var)
 {
   auto pid = boost::any_cast<std::shared_ptr<PacketID>>(var);
@@ -2895,9 +2932,9 @@ static void handleUDPServerResponse(int fileDesc, FDMultiplexer::funcparam_t& va
     t_udpclientsocks->returnSocket(fileDesc);
 
     PacketBuffer empty;
-    MT_t::waiters_t::iterator iter = g_multiTasker->d_waiters.find(pid);
+    auto iter = g_multiTasker->d_waiters.find(pid);
     if (iter != g_multiTasker->d_waiters.end()) {
-      doResends(iter, pid, empty);
+      doResends(iter, pid, empty, false);
     }
     g_multiTasker->sendEvent(pid, &empty); // this denotes error (does retry lookup using other NS)
     return;
@@ -2956,9 +2993,12 @@ static void handleUDPServerResponse(int fileDesc, FDMultiplexer::funcparam_t& va
   }
 
   if (!pident->domain.empty()) {
-    MT_t::waiters_t::iterator iter = g_multiTasker->d_waiters.find(pident);
+    auto iter = g_multiTasker->d_waiters.find(pident);
     if (iter != g_multiTasker->d_waiters.end()) {
-      doResends(iter, pident, packet);
+      if (g_ECSHardening) {
+        iter->key->ecsReceived = iter->key->ecsSubnet && checkIncomingECSSource(packet, *iter->key->ecsSubnet);
+      }
+      doResends(iter, pident, packet, iter->key->ecsReceived);
     }
   }
 
@@ -2978,7 +3018,6 @@ retryWithName:
 
       // 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)
index 23e17efe5667857c34025ee14c8f789544168e03..ae9469720349902a9a4cd17ec788f6ccd0530b77 100644 (file)
@@ -96,6 +96,7 @@ enum class Counter : uint8_t
   maintenanceCalls,
   nodCount,
   udrCount,
+  ecsMissingCount,
 
   numberOfCounters
 };
index 140ef7a45ac9e9e8d4bb0a5dacc4c42ab3cfee0a..3c97eaff2da638ff1cf24ac64253a47499b76704 100644 (file)
@@ -5350,15 +5350,27 @@ bool SyncRes::doResolveAtThisIP(const std::string& prefix, const DNSName& qname,
     LOG(prefix << qname << ": Query handled by Lua" << endl);
   }
   else {
-    ednsmask = getEDNSSubnetMask(qname, remoteIP);
-    if (ednsmask) {
-      LOG(prefix << qname << ": Adding EDNS Client Subnet Mask " << ednsmask->toString() << " to query" << endl);
-      s_ecsqueries++;
-    }
-    updateQueryCounts(prefix, qname, remoteIP, doTCP, doDoT);
-    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);
+    bool sendECSIfRelevant = true;
+    for (int count = 0; count < 2; ++count) {
+      ednsmask = sendECSIfRelevant ? getEDNSSubnetMask(qname, remoteIP) : boost::none;
+      if (ednsmask) {
+        LOG(prefix << qname << ": Adding EDNS Client Subnet Mask " << ednsmask->toString() << " to query" << endl);
+        s_ecsqueries++;
+      }
+      updateQueryCounts(prefix, qname, remoteIP, doTCP, doDoT);
+      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) {
+        if (sendECSIfRelevant) {
+          sendECSIfRelevant = false;
+          LOG(prefix << qname << ": Answer has no ECS, trying again without EDNS Client Subnet Mask" << endl);
+          continue;
+        }
+        assert(0); // should not happen
+      }
+      break; // when no ECS shenanigans happened, only one pass through the loop
+    }
   }
 
   /* preoutquery killed the query by setting dq.rcode to -3 */
index ce6a490dbd5aa8c22f09e3fc814ce28146ef7333..9e303160876dc3ddf1bb76b8849b3834dd2ccfb3 100644 (file)
@@ -774,6 +774,7 @@ struct PacketID
   bool inIncompleteOkay{false};
   uint16_t id{0}; // wait for a specific id/remote pair
   uint16_t type{0}; // and this is its type
+  std::optional<bool> ecsReceived; // only set in ecsHardened mode
   TCPAction highState{TCPAction::DoingRead};
   IOState lowState{IOState::NeedRead};