From: Oliver Chen Date: Wed, 7 May 2025 07:35:18 +0000 (+0000) Subject: dnsdist: support skip hashing AR section for caching X-Git-Tag: dnsdist-2.0.0-beta1~36^2~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=07c8fab41538975d2a1b7bee5191c750f94a547c;p=thirdparty%2Fpdns.git dnsdist: support skip hashing AR section for caching 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. --- diff --git a/pdns/dnsdistdist/dnsdist-cache.cc b/pdns/dnsdistdist/dnsdist-cache.cc index 81b4748a4d..2332be704b 100644 --- a/pdns/dnsdistdist/dnsdist-cache.cc +++ b/pdns/dnsdistdist/dnsdist-cache.cc @@ -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(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(packet.data()), packet.size()), result, sizeof(dnsheader) + qnameWireLength, d_settings.d_optionsToSkip); diff --git a/pdns/dnsdistdist/dnsdist-cache.hh b/pdns/dnsdistdist/dnsdist-cache.hh index 5a068c1035..6d76d9c7e7 100644 --- a/pdns/dnsdistdist/dnsdist-cache.hh +++ b/pdns/dnsdistdist/dnsdist-cache.hh @@ -51,6 +51,7 @@ public: bool d_deferrableInsertLock{true}; bool d_parseECS{false}; bool d_keepStaleData{false}; + bool d_skipHashingAR{false}; }; DNSDistPacketCache(CacheSettings settings); diff --git a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc index d747f19349..766242067c 100644 --- a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc +++ b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc @@ -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(settings); registerType(packetCacheObj, cache.name); diff --git a/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc b/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc index b379e5080e..c46a09a2fd 100644 --- a/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc +++ b/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc @@ -38,6 +38,7 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client) .d_shardCount = 20, }; bool cookieHashing = false; + bool skipHashingAR = false; LuaArray skipOptions; size_t maximumEntrySize{4096}; @@ -54,6 +55,7 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client) getOptionalValue(vars, "truncatedTTL", settings.d_truncatedTTL); getOptionalValue(vars, "cookieHashing", cookieHashing); getOptionalValue(vars, "maximumEntrySize", maximumEntrySize); + getOptionalValue(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(settings); }); diff --git a/pdns/dnsdistdist/dnsdist-settings-definitions.yml b/pdns/dnsdistdist/dnsdist-settings-definitions.yml index 296cae61f3..96da7dcb4b 100644 --- a/pdns/dnsdistdist/dnsdist-settings-definitions.yml +++ b/pdns/dnsdistdist/dnsdist-settings-definitions.yml @@ -1936,6 +1936,10 @@ packet_cache: type: "Vec" 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" diff --git a/pdns/dnsdistdist/dnsdist.cc b/pdns/dnsdistdist/dnsdist.cc index 4c9d596724..e364a32619 100644 --- a/pdns/dnsdistdist/dnsdist.cc +++ b/pdns/dnsdistdist/dnsdist.cc @@ -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& 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(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& 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::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(query.data()), query.size(), &udpPayloadSize, &zValue); + if (udpPayloadSize < 512) { + udpPayloadSize = 512; + } + std::shared_ptr 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); diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 5d9bcaebe4..89f0767d84 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -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 diff --git a/regression-tests.dnsdist/test_Caching.py b/regression-tests.dnsdist/test_Caching.py index 80623b3012..26f3f95241 100644 --- a/regression-tests.dnsdist/test_Caching.py +++ b/regression-tests.dnsdist/test_Caching.py @@ -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)