]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: support skip hashing AR section for caching
authorOliver Chen <oliver.chen@nokia-sbell.com>
Wed, 7 May 2025 07:35:18 +0000 (07:35 +0000)
committerOliver Chen <oliver.chen@nokia-sbell.com>
Wed, 11 Jun 2025 00:10:42 +0000 (00:10 +0000)
Use case arise that two clients with different udp payload size,
a customized client uses 4096 while glibc resolver uses 1232.
User would like to share cache result for the same query name,
type and class in this case. The downstream servers does not use
ECS and would not return different answers upon other EDNS
options so this is to add an option to support such use case.

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/dnsdist.cc
pdns/dnsdistdist/docs/reference/config.rst
regression-tests.dnsdist/test_Caching.py

index 81b4748a4df866c82bab766496c8bb8aeca42e67..2332be704b7e843881f3f831def6c3ab60a6209a 100644 (file)
@@ -459,14 +459,24 @@ 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()) + ")");
   }
 
-  result = burtle(&packet.at(2), sizeof(dnsheader) - 2, result);
+  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);
+  }
   // 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_optionsToSkip.empty()) {
+    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()) {
       /* 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);
index 5a068c1035e65ec5caed3dca151496f4c5c893f9..6d76d9c7e78890dff384f5b9ed3d8e0ba8738ca1 100644 (file)
@@ -51,6 +51,7 @@ public:
     bool d_deferrableInsertLock{true};
     bool d_parseECS{false};
     bool d_keepStaleData{false};
+    bool d_skipHashingAR{false};
   };
 
   DNSDistPacketCache(CacheSettings settings);
index d747f193494fd775880081c39c311199b91c82b1..766242067c35ddd5996ecfcd2afdb9d84dc7dfb4 100644 (file)
@@ -1095,6 +1095,9 @@ 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;
+      }
       auto packetCacheObj = std::make_shared<DNSDistPacketCache>(settings);
 
       registerType<DNSDistPacketCache>(packetCacheObj, cache.name);
index b379e5080e3cf1f24e1f38aafa46bf0b49c97ddd..c46a09a2fd0ad328171f35eb736d5172b1ba027a 100644 (file)
@@ -38,6 +38,7 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client)
       .d_shardCount = 20,
     };
     bool cookieHashing = false;
+    bool skipHashingAR = false;
     LuaArray<uint16_t> skipOptions;
     size_t maximumEntrySize{4096};
 
@@ -54,6 +55,7 @@ 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;
@@ -81,6 +83,9 @@ 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 296cae61f39a9217c6431a363e5912da49b0bad5..96da7dcb4b2e6d7f9b7b1da9ab505ad71b760773 100644 (file)
@@ -1936,6 +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"
 
 proxy_protocol:
   description: "Proxy Protocol-related settings"
index 4c9d5967244bf8b8c118db49648d938056c2a984..e364a326193fc8ebb8a3eca275a5ed93142781c2 100644 (file)
@@ -637,12 +637,18 @@ void handleResponseSent(const DNSName& qname, const QType& qtype, double udiff,
   doLatencyStats(incomingProtocol, udiff);
 }
 
-static void handleResponseForUDPClient(InternalQueryState& ids, PacketBuffer& response, const std::shared_ptr<DownstreamState>& backend, bool isAsync, bool selfGenerated)
+static void handleResponseTC4UDPClient(uint16_t udpPayloadSize, PacketBuffer& response, DNSResponse& dnsResponse)
 {
-  DNSResponse dnsResponse(ids, response, backend);
-
-  if (ids.udpPayloadSize > 0 && response.size() > ids.udpPayloadSize) {
-    vinfolog("Got a response of size %d while the initial UDP payload size was %d, truncating", response.size(), ids.udpPayloadSize);
+  if (udpPayloadSize == 0) {
+    uint16_t zValue = 0;
+    // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+    getEDNSUDPPayloadSizeAndZ(reinterpret_cast<const char*>(response.data()), response.size(), &udpPayloadSize, &zValue);
+    if (udpPayloadSize < 512) {
+      udpPayloadSize = 512;
+    }
+  }
+  if (response.size() > udpPayloadSize) {
+    vinfolog("Got a response of size %d while the initial UDP payload size was %d, truncating", response.size(), udpPayloadSize);
     truncateTC(dnsResponse.getMutableData(), dnsResponse.getMaximumSize(), dnsResponse.ids.qname.wirelength(), dnsdist::configuration::getCurrentRuntimeConfiguration().d_addEDNSToSelfGeneratedResponses);
     dnsdist::PacketMangling::editDNSHeaderFromPacket(dnsResponse.getMutableData(), [](dnsheader& header) {
       header.tc = true;
@@ -652,6 +658,13 @@ static void handleResponseForUDPClient(InternalQueryState& ids, PacketBuffer& re
   else if (dnsResponse.getHeader()->tc && dnsdist::configuration::getCurrentRuntimeConfiguration().d_truncateTC) {
     truncateTC(response, dnsResponse.getMaximumSize(), dnsResponse.ids.qname.wirelength(), dnsdist::configuration::getCurrentRuntimeConfiguration().d_addEDNSToSelfGeneratedResponses);
   }
+}
+
+static void handleResponseForUDPClient(InternalQueryState& ids, PacketBuffer& response, const std::shared_ptr<DownstreamState>& backend, bool isAsync, bool selfGenerated)
+{
+  DNSResponse dnsResponse(ids, response, backend);
+
+  handleResponseTC4UDPClient(ids.udpPayloadSize, response, dnsResponse);
 
   /* when the answer is encrypted in place, we need to get a copy
      of the original header before encryption to fill the ring buffer */
@@ -1862,6 +1875,15 @@ static void processUDPQuery(ClientState& clientState, const struct msghdr* msgh,
       dnsQuestion.proxyProtocolValues = make_unique<std::vector<ProxyProtocolValue>>(std::move(proxyProtocolValues));
     }
 
+    // save UDP payload size from origin query
+    uint16_t udpPayloadSize = 0;
+    uint16_t zValue = 0;
+    // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
+    getEDNSUDPPayloadSizeAndZ(reinterpret_cast<const char*>(query.data()), query.size(), &udpPayloadSize, &zValue);
+    if (udpPayloadSize < 512) {
+      udpPayloadSize = 512;
+    }
+
     std::shared_ptr<DownstreamState> backend{nullptr};
     auto result = processQuery(dnsQuestion, backend);
 
@@ -1882,6 +1904,9 @@ static void processUDPQuery(ClientState& clientState, const struct msghdr* msgh,
       }
 #endif /* defined(HAVE_RECVMMSG) && defined(HAVE_SENDMMSG) && defined(MSG_WAITFORONE) */
 #endif /* DISABLE_RECVMMSG */
+      /* ensure payload size is not exceeded */
+      DNSResponse dnsResponse(ids, query, nullptr);
+      handleResponseTC4UDPClient(udpPayloadSize, query, dnsResponse);
       /* we use dest, always, because we don't want to use the listening address to send a response since it could be 0.0.0.0 */
       sendUDPResponse(clientState.udpFD, query, dnsQuestion.ids.delayMsec, dest, remote);
 
index 5d9bcaebe45fde5d0df1f891eef292677b831e35..89f0767d8455e917b1dfb1b62e77056b527e9844 100644 (file)
@@ -1042,6 +1042,9 @@ See :doc:`../guides/cache` for a how to.
   .. versionchanged:: 2.0.0
     ``truncatedTTL`` parameter added.
 
+  .. versionchanged:: 2.0.0
+    ``skipHashingAR`` parameter added.
+
   Creates a new :class:`PacketCache` with the settings specified.
 
   :param int maxEntries: The maximum number of entries in this cache
@@ -1062,6 +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.
 
 .. class:: PacketCache
 
index 80623b30121466c1e45159ad5fc745225e549ac5..26f3f95241e74e6c4e67d4dc78dd25de14272b3c 100644 (file)
@@ -5,6 +5,8 @@ import dns
 import clientsubnetoption
 import cookiesoption
 import requests
+import random
+import string
 from dnsdisttests import DNSDistTest, pickAvailablePort
 
 class TestCaching(DNSDistTest):
@@ -3123,3 +3125,132 @@ class TestCacheEmptyTC(DNSDistTest):
             sender = getattr(self, method)
             (_, receivedResponse) = sender(query, response=None, useQueue=False)
             self.assertEqual(receivedResponse, response)
+
+class TestCachingSkipAR(DNSDistTest):
+
+    _verboseMode = True
+    _testServerPort = pickAvailablePort()
+    _webTimeout = 2.0
+    _webServerPort = pickAvailablePort()
+    _webServerAPIKey = 'apisecret'
+    _webServerAPIKeyHashed = '$scrypt$ln=10,p=1,r=8$9v8JxDfzQVyTpBkTbkUqYg==$bDQzAOHeK1G9UvTPypNhrX48w974ZXbFPtRKS34+aso='
+    _config_params = ['_webServerPort', '_webServerAPIKeyHashed', '_testServerPort']
+    _config_template = """
+    webserver("127.0.0.1:%s")
+    setWebserverConfig({apiKey="%s"})
+    pc = newPacketCache(100, {maxTTL=86400, minTTL=1, skipHashingAR=true})
+    getPool(""):setCache(pc)
+    newServer{address="127.0.0.1:%d"}
+    """
+
+    def getPoolMetric(self, poolID, metricName):
+        headers = {'x-api-key': self._webServerAPIKey}
+        url = 'http://127.0.0.1:' + str(self._webServerPort) + '/api/v1/servers/localhost'
+        r = requests.get(url, headers=headers, timeout=self._webTimeout)
+        self.assertTrue(r)
+        self.assertEqual(r.status_code, 200)
+        self.assertTrue(r.json())
+        content = r.json()
+        self.assertIn('pools', content)
+        pools = content['pools']
+        self.assertGreater(len(pools), poolID)
+        pool = pools[poolID]
+        return int(pool[metricName])
+
+    def testCacheSkipAR(self):
+        """
+        Cache: Testing ``skipHashingAR`` 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_1 = dns.message.make_query(name1, 'AAAA', 'IN', payload=600)
+        response1 = dns.message.make_response(query1)
+        rrset1 = dns.rrset.from_text(name1,
+                                    3600,
+                                    dns.rdataclass.IN,
+                                    dns.rdatatype.AAAA,
+                                    '::1')
+        response1.answer.append(rrset1)
+
+        # first query to fill the cache
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query1, response1)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        self.assertEqual(self.getPoolMetric(0, 'cacheHits'), 0)
+        receivedQuery.id = query1.id
+        self.assertEqual(query1, receivedQuery)
+        self.assertEqual(receivedResponse, response1)
+
+        # same query shall hit cache
+        (_, receivedResponse) = self.sendUDPQuery(query1, response=None, useQueue=False)
+        self.assertTrue(receivedResponse)
+        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
+        (_, receivedResponse) = self.sendUDPQuery(query1_1, response=None, useQueue=False)
+        self.assertTrue(receivedResponse)
+        self.assertEqual(self.getPoolMetric(0, 'cacheHits'), 2)
+        self.assertEqual(len(receivedResponse.answer), 1)
+        self.assertEqual(receivedResponse.answer[0], rrset1)
+
+        # 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)
+
+        response2 = dns.message.make_response(query2)
+        v6addr_list = []
+        for i in range(1,41):
+            v6addr_list.append(f'fe80:fe80:fe80:fe80::{i}')
+        rrset2 = dns.rrset.from_text_list(name2,
+                                          3600,
+                                          dns.rdataclass.IN,
+                                          dns.rdatatype.AAAA,
+                                          v6addr_list)
+        response2.answer.append(rrset2) # reponse > 40x(16+10)=1040 bytes
+
+        # first query to fill the cache
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query2, response2)
+        self.assertTrue(receivedQuery)
+        self.assertEqual(self.getPoolMetric(0, 'cacheHits'), 2)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = query2.id
+        self.assertEqual(query2, receivedQuery)
+        self.assertEqual(receivedResponse, response2)
+
+        # same query shall hit cache
+        (_, receivedResponse) = self.sendUDPQuery(query2, response=None, useQueue=False)
+        self.assertTrue(receivedResponse)
+        self.assertEqual(self.getPoolMetric(0, 'cacheHits'), 3)
+        self.assertEqual(receivedResponse, response2)
+
+        # query2_1 shall also hit cache even it has a different payload size
+        (_, 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
+        (_, 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)