]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: implement simple packet shuffle in cache
authorKarel Bilek <kb@karelbilek.com>
Tue, 9 Sep 2025 14:03:33 +0000 (16:03 +0200)
committerKarel Bilek <kb@karelbilek.com>
Fri, 7 Nov 2025 13:20:38 +0000 (14:20 +0100)
The shuffle is implementing by directly swapping
pieces of RData memory in a single RRSet.

Signed-off-by: Karel Bilek <kb@karelbilek.com>
pdns/dnsdistdist/dnsdist-cache.cc
pdns/dnsdistdist/dnsdist-cache.hh
pdns/dnsdistdist/dnsdist-configuration-yaml.cc
pdns/dnsdistdist/dnsdist-console-completion.cc
pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc
pdns/dnsdistdist/dnsdist-settings-definitions.yml
pdns/dnsdistdist/docs/guides/cache.rst
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsparser.cc
pdns/dnsparser.hh

index afc1575bac1a88f015ba7a12d5ea5dc470c7fe4b..e0adb1cb03e8b736a3056cfe1a308443a2623638 100644 (file)
@@ -329,6 +329,12 @@ bool DNSDistPacketCache::get(DNSQuestion& dnsQuestion, uint16_t queryId, uint32_
     }
   }
 
+  if (d_settings.d_shuffle) {
+    dnsheader_aligned dh_aligned(response.data());
+    // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+    shuffleDNSPacket(reinterpret_cast<char*>(response.data()), response.size(), dh_aligned);
+  }
+
   ++d_hits;
   return true;
 }
index 8efc1701c56a2d47c18b65bffec6758eef0c11a3..71239a4d3ec0f9dcb053366952a483c0d4bb653f 100644 (file)
@@ -52,6 +52,7 @@ public:
     bool d_deferrableInsertLock{true};
     bool d_parseECS{false};
     bool d_keepStaleData{false};
+    bool d_shuffle{false};
   };
 
   DNSDistPacketCache(CacheSettings settings);
index d97f8898b228f7472e57c19976edaf60026fd2e1..e6064deca9e137839661167094b271481ad943d2 100644 (file)
@@ -999,6 +999,7 @@ static void handlePacketCacheConfiguration(const ::rust::Vec<dnsdist::rust::sett
       .d_deferrableInsertLock = cache.deferrable_insert_lock,
       .d_parseECS = cache.parse_ecs,
       .d_keepStaleData = cache.keep_stale_data,
+      .d_shuffle = cache.shuffle,
     };
     std::unordered_set<uint16_t> ranks;
     if (!cache.options_to_skip.empty()) {
index a5c7d931333cd7939f954a2f7c4a3345522d69f5..de9317de0139126d50a3ca4758bcd6a6e6f7488f 100644 (file)
@@ -186,7 +186,7 @@ static std::vector<dnsdist::console::completion::ConsoleKeyword> s_consoleKeywor
   {"newLMDBKVStore", true, "fname, dbName [, noLock]", "Return a new KeyValueStore object associated to the corresponding LMDB database"},
 #endif
   {"newNMG", true, "", "Returns a NetmaskGroup"},
-  {"newPacketCache", true, "maxEntries[, maxTTL=86400, minTTL=0, temporaryFailureTTL=60, staleTTL=60, dontAge=false, numberOfShards=1, deferrableInsertLock=true, options={}]", "return a new Packet Cache"},
+  {"newPacketCache", true, "maxEntries[, maxTTL=86400, minTTL=0, temporaryFailureTTL=60, staleTTL=60, dontAge=false, shuffle=false, numberOfShards=1, deferrableInsertLock=true, options={}]", "return a new Packet Cache"},
   {"newQPSLimiter", true, "rate, burst", "configure a QPS limiter with that rate and that burst capacity"},
   {"newRemoteLogger", true, "address:port [, timeout=2, maxQueuedEntries=100, reconnectWaitTime=1]", "create a Remote Logger object, to use with `RemoteLogAction()` and `RemoteLogResponseAction()`"},
   {"newRuleAction", true, R"(DNS rule, DNS action [, {uuid="UUID", name="name"}])", "return a pair of DNS Rule and DNS Action, to be used with `setRules()`"},
index 9e08e3794f8ac0f22ed74b6b7d0f51baef920fb4..6a3fb0059a54085ff2d97b6224eb9544a5fd6d95 100644 (file)
@@ -46,6 +46,7 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client)
     getOptionalValue<bool>(vars, "deferrableInsertLock", settings.d_deferrableInsertLock);
     getOptionalValue<bool>(vars, "dontAge", settings.d_dontAge);
     getOptionalValue<bool>(vars, "keepStaleData", settings.d_keepStaleData);
+    getOptionalValue<bool>(vars, "shuffle", settings.d_shuffle);
     getOptionalValue<size_t>(vars, "maxNegativeTTL", settings.d_maxNegativeTTL);
     getOptionalValue<size_t>(vars, "maxTTL", settings.d_maxTTL);
     getOptionalValue<size_t>(vars, "minTTL", settings.d_minTTL);
index b0f7a70b3cf2b34acbe9dd4768eb2e68ece92dc1..54d59d59c6683d68ac9c99b27ae80692ed5bfa9c 100644 (file)
@@ -1917,6 +1917,10 @@ packet_cache:
       type: "bool"
       default: "false"
       description: "Whether to suspend the removal of expired entries from the cache when there is no backend available in at least one of the pools using this cache"
+    - name: "shuffle"
+      type: "bool"
+      default: "false"
+      description: "Whether A and AAAA records should be shuffled when serving from cache, for load-balancing. The cache might not be shuffled if the cached packet is too complex for the simple parser used for this feature."
     - name: "max_negative_ttl"
       type: "u32"
       default: "3600"
index 24b1a3f00da386e4d4ad1eddba7f6cdb8cc350cf..536e2b37166dd474052ce54b5891016de3801db9 100644 (file)
@@ -5,7 +5,7 @@ Caching Responses
 It is enabled per-pool, but the same cache can be shared between several pools.
 The first step is to define a cache with :func:`newPacketCache`, then to assign that cache to the chosen pool, the default one being represented by the empty string::
 
-  pc = newPacketCache(10000, {maxTTL=86400, minTTL=0, temporaryFailureTTL=60, staleTTL=60, dontAge=false})
+  pc = newPacketCache(10000, {maxTTL=86400, minTTL=0, temporaryFailureTTL=60, staleTTL=60, dontAge=false, shuffle=false})
   getPool(""):setCache(pc)
 
 + The first parameter (10000) is the maximum number of entries stored in the cache, and is the only one required. All the other parameters are optional and in seconds, except the last one which is a boolean.
@@ -37,6 +37,7 @@ The equivalent ``yaml`` configuration would be:
       temporary_failure_ttl: 60
       state_ttl: 60
       dont_age: false
+      shuffle: false
   pools:
     - name: ""
       packet_cache: "pc"
index 1c96a7da6a43c4a025d9c6e19d259c05ed04e947..402927f0b1b8930c9557dc1f961bea6bb62f7dff 100644 (file)
@@ -1093,6 +1093,7 @@ See :doc:`../guides/cache` for a how to.
   * ``skipOptions={10, 12}``: Extra list of EDNS option codes to skip when hashing the packet (if ``cookieHashing`` above is true, EDNS cookie option number will be removed from this list internally).
   * ``maximumEntrySize=4096``: int - The maximum size, in bytes, of a DNS packet that can be inserted into the packet cache. Default is 4096 bytes, which was the fixed size before 1.9.0, and is also a hard limit for UDP responses.
   * ``payloadRanks={}``: List of payload size used when hashing the packet. The list will be sorted in ascending order and searched to find a lower bound value for the payload size in the packet. If found then it will be used for packet hashing. Values less than 512 or greater than ``maximumEntrySize`` above will be discarded. This option is to enable cache entry sharing between clients using different payload sizes when needed.
+  * ``shuffle=false``: bool - Whether A and AAAA records should be shuffled when serving from cache, for load-balancing. The cache might not be shuffled if the cached packet is too complex for the simple parser used for this feature.
 
 .. class:: PacketCache
 
index 4c70d8052d25232165227898fdb959321b35eafe..b0eaa690ee1a18051a31da18b1746efdc2e03d8b 100644 (file)
@@ -24,6 +24,7 @@
 #include <boost/algorithm/string.hpp>
 #include <boost/format.hpp>
 
+#include "dns_random.hh"
 #include "namespaces.hh"
 #include "noinitvector.hh"
 
@@ -1003,6 +1004,87 @@ void ageDNSPacket(std::string& packet, uint32_t seconds, const dnsheader_aligned
   ageDNSPacket(packet.data(), packet.length(), seconds, aligned_dh);
 }
 
+void shuffleDNSPacket(char* packet, size_t length, const dnsheader_aligned& aligned_dh)
+{
+  if (length < sizeof(dnsheader)) {
+    return;
+  }
+  try {
+    const dnsheader* dhp = aligned_dh.get();
+    const uint16_t ancount = ntohs(dhp->ancount);
+    if (ancount == 1) {
+      // quick exit, nothing to shuffle
+      return;
+    }
+
+    DNSPacketMangler dpm(packet, length);
+
+    const uint16_t qdcount = ntohs(dhp->qdcount);
+
+    for(size_t iter = 0; iter < qdcount; ++iter) {
+      dpm.skipDomainName();
+      /* type and class */
+      dpm.skipBytes(4);
+    }
+
+    // for now shuffle only first rrset, only As and AAAAs
+    uint16_t rrset_type = 0;
+    DNSName rrset_dnsname{};
+    std::vector<std::pair<uint32_t, uint32_t>> rrdata_indexes;
+    rrdata_indexes.reserve(ancount);
+
+    for(size_t iter = 0; iter < ancount; ++iter) {
+      auto domain_start = dpm.getOffset();
+      dpm.skipDomainName();
+      const uint16_t dnstype = dpm.get16BitInt();
+      if (dnstype == QType::A || dnstype == QType::AAAA) {
+        if (rrdata_indexes.empty()) {
+          rrset_type = dnstype;
+          rrset_dnsname = DNSName(packet, length, domain_start, true);
+        } else {
+          if (dnstype != rrset_type) {
+            break;
+          }
+          if (DNSName(packet, length, domain_start, true) != rrset_dnsname) {
+            break;
+          }
+        }
+        /* class */
+        dpm.skipBytes(2);
+
+        /* ttl */
+        dpm.skipBytes(4);
+        rrdata_indexes.push_back(dpm.skipRDataAndReturnOffsets());
+      } else {
+        if (!rrdata_indexes.empty()) {
+          break;
+        }
+        /* class */
+        dpm.skipBytes(2);
+
+        /* ttl */
+        dpm.skipBytes(4);
+        dpm.skipRData();
+      }
+    }
+
+    if (rrdata_indexes.size() >= 2) {
+      using uid = std::uniform_int_distribution<std::vector<std::pair<uint32_t, uint32_t>>::size_type>;
+      uid dist;
+
+      pdns::dns_random_engine randomEngine;
+      for (auto swapped = rrdata_indexes.size() - 1; swapped > 0; --swapped) {
+        auto swapped_with = dist(randomEngine, uid::param_type(0, swapped));
+        if (swapped != swapped_with) {
+          dpm.swapInPlace(rrdata_indexes.at(swapped), rrdata_indexes.at(swapped_with));
+        }
+      }
+    }
+  }
+  catch(...) {
+  }
+}
+
 uint32_t getDNSPacketMinTTL(const char* packet, size_t length, bool* seenAuthSOA)
 {
   uint32_t result = std::numeric_limits<uint32_t>::max();
index 4deb14f35b4e5aa18b1a04779f655df80c6da2ba..7e8142a9ccb712c82b51aad1eec5e6b2d3034521 100644 (file)
@@ -531,6 +531,7 @@ private:
 };
 
 string simpleCompress(const string& label, const string& root="");
+void shuffleDNSPacket(char* packet, size_t length, const dnsheader_aligned& aligned_dh);
 void ageDNSPacket(char* packet, size_t length, uint32_t seconds, const dnsheader_aligned&);
 void ageDNSPacket(std::string& packet, uint32_t seconds, const dnsheader_aligned&);
 void editDNSPacketTTL(char* packet, size_t length, const std::function<uint32_t(uint8_t, uint16_t, uint16_t, uint32_t)>& visitor);
@@ -617,6 +618,15 @@ public:
     moveOffset(toskip);
   }
 
+  std::pair<uint32_t, uint32_t> skipRDataAndReturnOffsets()
+  {
+    auto toskip = get16BitInt();
+    uint32_t start = d_offset;
+    moveOffset(toskip);
+    uint32_t end = d_offset;
+    return std::pair<uint32_t,uint32_t>(start, end);
+  }
+
   void decreaseAndSkip32BitInt(uint32_t decrease)
   {
     const char *p = d_packet + d_offset;
@@ -647,6 +657,30 @@ public:
     return d_offset;
   }
 
+  void swapInPlace(std::pair<uint32_t, uint32_t> a, std::pair<uint32_t, uint32_t> b) {
+    // some basic range checks
+    if (b.first < a.first) {
+      std::swap(a, b);
+    }
+    if (a.second-a.first != b.second-b.first) {
+      throw std::out_of_range("swap: segments have different lengths");
+    }
+    if (a.second <= a.first) {
+      throw std::out_of_range("swap: ending of segment before start of segment");
+    }
+    if (a.second > b.first) {
+      throw std::out_of_range("swap: overlapping segments");
+    }
+    if (b.second > d_length) {
+      throw std::out_of_range("swap: ending of segment after end of array");
+    }
+    // don't allow to swap what we haven't read yet
+    if (b.second > d_offset) {
+      throw std::out_of_range("swap: ending of segment after current offset");
+    }
+    std::swap_ranges(d_packet+a.first, d_packet+a.second, d_packet+b.first);
+  }
+
 private:
   void moveOffset(uint16_t by)
   {