]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Use payload size ranking for cache sharing
authorOliver Chen <oliver.chen@nokia-sbell.com>
Fri, 9 May 2025 14:29:34 +0000 (14:29 +0000)
committerOliver Chen <oliver.chen@nokia-sbell.com>
Wed, 11 Jun 2025 00:11:12 +0000 (00:11 +0000)
pdns/dnsdistdist/dnsdist-cache.cc
pdns/dnsdistdist/dnsdist-cache.hh
pdns/dnsdistdist/dnsdist-configuration-yaml.cc
pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc
pdns/dnsdistdist/dnsdist-settings-definitions.yml
pdns/dnsdistdist/docs/reference/config.rst
pdns/packetcache.hh
regression-tests.dnsdist/test_Caching.py

index 2332be704b7e843881f3f831def6c3ab60a6209a..44a53b4b1acd86aa635a8a79be31ba739062be9d 100644 (file)
@@ -459,27 +459,17 @@ uint32_t DNSDistPacketCache::getKey(const DNSName::string_t& qname, size_t qname
     throw std::range_error("Computing packet cache key for an invalid packet size (" + std::to_string(packet.size()) + ")");
   }
 
-  if (d_settings.d_skipHashingAR) {
-    /* skip Additional Resource Records */
-    result = burtle(&packet.at(2), sizeof(dnsheader) - 4, result);
-  }
-  else {
-    result = burtle(&packet.at(2), sizeof(dnsheader) - 2, result);
-  }
+  result = burtle(&packet.at(2), sizeof(dnsheader) - 2, result);
   // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
   result = burtleCI(reinterpret_cast<const unsigned char*>(qname.c_str()), qname.length(), result);
   if (packet.size() < sizeof(dnsheader) + qnameWireLength) {
     throw std::range_error("Computing packet cache key for an invalid packet (" + std::to_string(packet.size()) + " < " + std::to_string(sizeof(dnsheader) + qnameWireLength) + ")");
   }
   if (packet.size() > ((sizeof(dnsheader) + qnameWireLength))) {
-    if (d_settings.d_skipHashingAR) {
-      /* only need to include the 2+2 bytes for qtype and qclass */
-      result = burtle(&packet.at(sizeof(dnsheader) + qnameWireLength), 4, result);
-    }
-    else if (!d_settings.d_optionsToSkip.empty()) {
+    if (!d_settings.d_optionsToSkip.empty()) {
       /* skip EDNS options if any */
       // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
-      result = PacketCache::hashAfterQname(std::string_view(reinterpret_cast<const char*>(packet.data()), packet.size()), result, sizeof(dnsheader) + qnameWireLength, d_settings.d_optionsToSkip);
+      result = PacketCache::hashAfterQname(std::string_view(reinterpret_cast<const char*>(packet.data()), packet.size()), result, sizeof(dnsheader) + qnameWireLength, d_settings.d_optionsToSkip, d_settings.d_payloadRanks);
     }
     else {
       result = burtle(&packet.at(sizeof(dnsheader) + qnameWireLength), packet.size() - (sizeof(dnsheader) + qnameWireLength), result);
index 6d76d9c7e78890dff384f5b9ed3d8e0ba8738ca1..d48397e8f931bf779c0a35f81c82ab04ab820c42 100644 (file)
@@ -38,6 +38,7 @@ public:
   struct CacheSettings
   {
     std::unordered_set<uint16_t> d_optionsToSkip{EDNSOptionCode::COOKIE};
+    std::vector<uint16_t> d_payloadRanks{};
     size_t d_maxEntries{0};
     size_t d_maximumEntrySize{4096};
     uint32_t d_maxTTL{86400};
@@ -51,7 +52,6 @@ public:
     bool d_deferrableInsertLock{true};
     bool d_parseECS{false};
     bool d_keepStaleData{false};
-    bool d_skipHashingAR{false};
   };
 
   DNSDistPacketCache(CacheSettings settings);
index 766242067c35ddd5996ecfcd2afdb9d84dc7dfb4..cb0b2d719acdb3f1254039cc51ec72055975cec8 100644 (file)
@@ -1086,6 +1086,7 @@ bool loadConfigurationFromFile(const std::string& fileName, [[maybe_unused]] boo
         .d_parseECS = cache.parse_ecs,
         .d_keepStaleData = cache.keep_stale_data,
       };
+      std::unordered_set<uint16_t> ranks;
       for (const auto& option : cache.options_to_skip) {
         settings.d_optionsToSkip.insert(pdns::checked_stoi<uint16_t>(std::string(option)));
       }
@@ -1095,8 +1096,16 @@ bool loadConfigurationFromFile(const std::string& fileName, [[maybe_unused]] boo
       if (cache.maximum_entry_size >= sizeof(dnsheader)) {
         settings.d_maximumEntrySize = cache.maximum_entry_size;
       }
-      if (!cache.parse_ecs) {
-        settings.d_skipHashingAR = cache.skip_hashing_ar;
+      for (const auto& rankstr : cache.payload_ranks) {
+        auto rank = pdns::checked_stoi<uint16_t>(std::string(rankstr));
+        if (rank < 512 || rank > settings.d_maximumEntrySize) {
+          continue;
+        }
+        ranks.insert(rank);
+      }
+      if (!ranks.empty()) {
+        settings.d_payloadRanks.assign(ranks.begin(), ranks.end());
+        std::sort(settings.d_payloadRanks.begin(), settings.d_payloadRanks.end());
       }
       auto packetCacheObj = std::make_shared<DNSDistPacketCache>(settings);
 
index c46a09a2fd0ad328171f35eb736d5172b1ba027a..6441128f72aeeaa79aec3a0d548542ab9c60ab26 100644 (file)
@@ -38,8 +38,9 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client)
       .d_shardCount = 20,
     };
     bool cookieHashing = false;
-    bool skipHashingAR = false;
     LuaArray<uint16_t> skipOptions;
+    LuaArray<uint16_t> payloadRanks;
+    std::unordered_set<uint16_t> ranks;
     size_t maximumEntrySize{4096};
 
     getOptionalValue<bool>(vars, "deferrableInsertLock", settings.d_deferrableInsertLock);
@@ -55,7 +56,6 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client)
     getOptionalValue<size_t>(vars, "truncatedTTL", settings.d_truncatedTTL);
     getOptionalValue<bool>(vars, "cookieHashing", cookieHashing);
     getOptionalValue<size_t>(vars, "maximumEntrySize", maximumEntrySize);
-    getOptionalValue<bool>(vars, "skipHashingAR", skipHashingAR);
 
     if (maximumEntrySize >= sizeof(dnsheader)) {
       settings.d_maximumEntrySize = maximumEntrySize;
@@ -67,6 +67,19 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client)
       }
     }
 
+    if (getOptionalValue<decltype(payloadRanks)>(vars, "payloadRanks", payloadRanks) > 0) {
+      for (const auto& rank : payloadRanks) {
+        if (rank.second < 512 || rank.second > settings.d_maximumEntrySize) {
+          continue;
+        }
+        ranks.insert(rank.second);
+      }
+      if (!ranks.empty()) {
+        settings.d_payloadRanks.assign(ranks.begin(), ranks.end());
+        std::sort(settings.d_payloadRanks.begin(), settings.d_payloadRanks.end());
+      }
+    }
+
     if (cookieHashing) {
       settings.d_optionsToSkip.erase(EDNSOptionCode::COOKIE);
     }
@@ -83,9 +96,6 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client)
       settings.d_maxEntries = 1;
       settings.d_shardCount = 1;
     }
-    if (!settings.d_parseECS) {
-      settings.d_skipHashingAR = skipHashingAR;
-    }
 
     return std::make_shared<DNSDistPacketCache>(settings);
   });
index 96da7dcb4b2e6d7f9b7b1da9ab505ad71b760773..ca6d047b8d0ce9badc52696678a3b141ac8abd72 100644 (file)
@@ -1936,10 +1936,10 @@ packet_cache:
       type: "Vec<String>"
       default: ""
       description: "Extra list of EDNS option codes to skip when hashing the packet (if ``cookie_hashing`` above is false, EDNS cookie option number will be added to this list internally)"
-    - name: "skip_hashing_ar"
-      type: "bool"
-      default: "false"
-      description: "If true, the whole Additional Resource Record section (including all EDNS options) will be skipped when hashing the packet. This will allow cache entry sharing between multiple clients who will use different EDNS0 payload size in its request for the same query name/type/class. However, if ``parse_ecs`` abvoe is true, this parameter is ignored since the answer to the same query name/type/class might be different if ECS option is used"
+    - name: "payload_ranks"
+      type: "Vec<String>"
+      default: ""
+      description: "List of payload size used when hashing the packet. The list will be sorted in ascend 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 ``maximum_entry_size`` above will be discarded. This option is to enable cache entry sharing between clients using different payload sizes when needed"
 
 proxy_protocol:
   description: "Proxy Protocol-related settings"
index 89f0767d8455e917b1dfb1b62e77056b527e9844..c0352669631a6af5a52316da58abed952a935460 100644 (file)
@@ -1043,7 +1043,7 @@ See :doc:`../guides/cache` for a how to.
     ``truncatedTTL`` parameter added.
 
   .. versionchanged:: 2.0.0
-    ``skipHashingAR`` parameter added.
+    ``payloadRanks`` parameter added.
 
   Creates a new :class:`PacketCache` with the settings specified.
 
@@ -1065,7 +1065,7 @@ See :doc:`../guides/cache` for a how to.
   * ``cookieHashing=false``: bool - If true, EDNS Cookie values will be hashed, resulting in separate entries for different cookies in the packet cache. This is required if the backend is sending answers with EDNS Cookies, otherwise a client might receive an answer with the wrong cookie.
   * ``skipOptions={}``: Extra list of EDNS option codes to skip when hashing the packet (if ``cookieHashing`` above is false, EDNS cookie option number will be added to 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.
-  * ``skipHashingAR=false``: bool - If true, the whole Additional Resource Record section (including all EDNS options) will be skipped when hashing the packet. This will allow cache entry sharing between multiple clients who will use different EDNS0 payload size in its request for the same query name/type/class. However, if ``parseECS`` abvoe is true, this parameter is ignored since the answer to the same query name/type/class might be different if ECS option is used.
+  * ``payloadRanks={}``: List of payload size used when hashing the packet. The list will be sorted in ascend 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.
 
 .. class:: PacketCache
 
index 583ecfc8a9f9d04227e85d4f4c00f686906d2712..c78d0d51d312c33e2dd07be0e08b3e3debfe1d6b 100644 (file)
@@ -33,7 +33,7 @@ public:
      - EDNS Cookie options, if any ;
      - Any given option code present in optionsToSkip
   */
-  static uint32_t hashAfterQname(const std::string_view& packet, uint32_t currentHash, size_t pos, const std::unordered_set<uint16_t>& optionsToSkip = {EDNSOptionCode::COOKIE})
+  static uint32_t hashAfterQname(const std::string_view& packet, uint32_t currentHash, size_t pos, const std::unordered_set<uint16_t>& optionsToSkip = {EDNSOptionCode::COOKIE}, const std::vector<uint16_t>& payloadRanks = {})
   {
     const size_t packetSize = packet.size();
     assert(packetSize >= sizeof(dnsheader));
@@ -46,14 +46,31 @@ public:
     */
     const dnsheader_aligned dnsheaderdata(packet.data());
     const struct dnsheader *dh = dnsheaderdata.get();
-    if (ntohs(dh->qdcount) != 1 || ntohs(dh->ancount) != 0 || ntohs(dh->nscount) != 0 || ntohs(dh->arcount) != 1 || (pos + 15) >= packetSize) {
+    if (ntohs(dh->qdcount) != 1 || ntohs(dh->ancount) != 0 || ntohs(dh->nscount) != 0 || ntohs(dh->arcount) != 1 || (pos + 15) > packetSize) {
       if (packetSize > pos) {
         currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(pos)), packetSize - pos, currentHash);
       }
       return currentHash;
     }
 
-    currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(pos)), 15, currentHash);
+    if (payloadRanks.empty()) {
+      currentHash = burtle(reinterpret_cast<const unsigned char*>(&packet.at(pos)), 15, currentHash);
+    }
+    else {
+      std::vector<unsigned char> optrr(packet.begin() + pos, packet.begin() + pos + 15);
+      uint16_t bufSize = optrr.at(7) * 256 + optrr.at(8);
+      auto it = std::upper_bound(payloadRanks.begin(), payloadRanks.end(), bufSize);
+      if (it != payloadRanks.begin()) {
+        it--;
+        optrr[7] = (*it) >> 8;
+        optrr[8] = (*it) & 0xff;
+      }
+      currentHash = burtle(reinterpret_cast<const unsigned char*>(&optrr.at(0)), 15, currentHash);
+    }
+    if ( (pos + 15) == packetSize ) {
+      return currentHash;
+    }
+
     /* skip the qtype (2), qclass (2) */
     /* root label (1), type (2), class (2) and ttl (4) */
     /* already hashed above */
index 26f3f95241e74e6c4e67d4dc78dd25de14272b3c..78aa81d2311b817669ccba2503c6b1b6887718cb 100644 (file)
@@ -5,8 +5,6 @@ import dns
 import clientsubnetoption
 import cookiesoption
 import requests
-import random
-import string
 from dnsdisttests import DNSDistTest, pickAvailablePort
 
 class TestCaching(DNSDistTest):
@@ -538,7 +536,7 @@ class TestCaching(DNSDistTest):
         """
         numberOfQueries = 10
         name = 'large-answer.cache.tests.powerdns.com.'
-        query = dns.message.make_query(name, 'TXT', 'IN')
+        query = dns.message.make_query(name, 'TXT', 'IN', payload=4096)
         response = dns.message.make_response(query)
         # we prepare a large answer
         content = ""
@@ -547,7 +545,7 @@ class TestCaching(DNSDistTest):
                 content = content + ', '
             content = content + (str(i)*50)
         # pad up to 4096
-        content = content + 'A'*42
+        content = content + 'A'*31
 
         rrset = dns.rrset.from_text(name,
                                     3600,
@@ -3126,7 +3124,7 @@ class TestCacheEmptyTC(DNSDistTest):
             (_, receivedResponse) = sender(query, response=None, useQueue=False)
             self.assertEqual(receivedResponse, response)
 
-class TestCachingSkipAR(DNSDistTest):
+class TestCachingPayloadRanks(DNSDistTest):
 
     _verboseMode = True
     _testServerPort = pickAvailablePort()
@@ -3138,7 +3136,7 @@ class TestCachingSkipAR(DNSDistTest):
     _config_template = """
     webserver("127.0.0.1:%s")
     setWebserverConfig({apiKey="%s"})
-    pc = newPacketCache(100, {maxTTL=86400, minTTL=1, skipHashingAR=true})
+    pc = newPacketCache(100, {maxTTL=86400, minTTL=1, payloadRanks={768, 512, 4096, 1280, 1024, 2048}})
     getPool(""):setCache(pc)
     newServer{address="127.0.0.1:%d"}
     """
@@ -3157,17 +3155,14 @@ class TestCachingSkipAR(DNSDistTest):
         pool = pools[poolID]
         return int(pool[metricName])
 
-    def testCacheSkipAR(self):
+    def testCachePayloadRanks(self):
         """
-        Cache: Testing ``skipHashingAR`` parameter for caching
+        Cache: Testing ``payloadRanks`` parameter for caching
 
-        dnsdist is configured to cache entries with ``skipHashingAR`` turned on,
-        cache entry will be used even with/without AR section or different payload
-        size is used.
         """
         # testing with and without EDNS0 payload size
         name1 = 'cached.cache.tests.powerdns.com.'
-        query1 = dns.message.make_query(name1, 'AAAA', 'IN')
+        query1 = dns.message.make_query(name1, 'AAAA', 'IN', payload=512)
         query1_1 = dns.message.make_query(name1, 'AAAA', 'IN', payload=600)
         response1 = dns.message.make_response(query1)
         rrset1 = dns.rrset.from_text(name1,
@@ -3192,7 +3187,7 @@ class TestCachingSkipAR(DNSDistTest):
         self.assertEqual(self.getPoolMetric(0, 'cacheHits'), 1)
         self.assertEqual(receivedResponse, response1)
 
-        # query1_1 shall also hit cache even it inlcudes Opt RR for payload size
+        # query1_1 shall also hit cache since 600 round down to 512
         (_, receivedResponse) = self.sendUDPQuery(query1_1, response=None, useQueue=False)
         self.assertTrue(receivedResponse)
         self.assertEqual(self.getPoolMetric(0, 'cacheHits'), 2)
@@ -3201,12 +3196,9 @@ class TestCachingSkipAR(DNSDistTest):
 
         # testing for large sized cache entry
         name2 = 'bigcached.cache.tests.powerdns.com.'
-        cookieBytes = ''.join(random.choices(string.hexdigits, k=8)).lower().encode()
-        myCookie = dns.edns.CookieOption(client=cookieBytes, server=b'')
-        query2 = dns.message.make_query(name2, 'AAAA', 'IN', payload=4096)
-        query2_1 = dns.message.make_query(name2, 'AAAA', 'IN', payload=2000)
-        query2_2 = dns.message.make_query(name2, 'AAAA', 'IN', payload=1500, options=[myCookie])
-        query2_3 = dns.message.make_query(name2, 'AAAA', 'IN', payload=800)
+        query2 = dns.message.make_query(name2, 'AAAA', 'IN', payload=1279)
+        query2_1 = dns.message.make_query(name2, 'AAAA', 'IN', payload=1200)
+        query2_2 = dns.message.make_query(name2, 'AAAA', 'IN', payload=1024)
 
         response2 = dns.message.make_response(query2)
         v6addr_list = []
@@ -3234,23 +3226,16 @@ class TestCachingSkipAR(DNSDistTest):
         self.assertEqual(self.getPoolMetric(0, 'cacheHits'), 3)
         self.assertEqual(receivedResponse, response2)
 
-        # query2_1 shall also hit cache even it has a different payload size
+        # query2_1 shall hit cache
         (_, receivedResponse) = self.sendUDPQuery(query2_1, response=None, useQueue=False)
         self.assertTrue(receivedResponse)
         self.assertEqual(self.getPoolMetric(0, 'cacheHits'), 4)
         self.assertEqual(len(receivedResponse.answer), 1)
         self.assertEqual(receivedResponse.answer[0], rrset2)
 
-        # query2_2 shall also hit cache even it has a different payload size and EDNS COOKIE
+        # query2_2 shall hit cache but truncated since payload size is not enough
         (_, receivedResponse) = self.sendUDPQuery(query2_2, response=None, useQueue=False)
         self.assertTrue(receivedResponse)
         self.assertEqual(self.getPoolMetric(0, 'cacheHits'), 5)
-        self.assertEqual(len(receivedResponse.answer), 1)
-        self.assertEqual(receivedResponse.answer[0], rrset2)
-
-        # query2_3 shall also hit cache but truncated since payload size is not enough
-        (_, receivedResponse) = self.sendUDPQuery(query2_3, response=None, useQueue=False)
-        self.assertTrue(receivedResponse)
-        self.assertEqual(self.getPoolMetric(0, 'cacheHits'), 6)
         self.assertEqual(len(receivedResponse.answer), 0)
         self.assertEqual(receivedResponse.flags & dns.flags.TC, dns.flags.TC)