From: Oliver Chen Date: Fri, 9 May 2025 14:29:34 +0000 (+0000) Subject: Use payload size ranking for cache sharing X-Git-Tag: dnsdist-2.0.0-beta1~36^2~3 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=98c8ea8cad478fb556a96569749ddb81e39d726c;p=thirdparty%2Fpdns.git Use payload size ranking for cache sharing --- diff --git a/pdns/dnsdistdist/dnsdist-cache.cc b/pdns/dnsdistdist/dnsdist-cache.cc index 2332be704b..44a53b4b1a 100644 --- a/pdns/dnsdistdist/dnsdist-cache.cc +++ b/pdns/dnsdistdist/dnsdist-cache.cc @@ -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(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(packet.data()), packet.size()), result, sizeof(dnsheader) + qnameWireLength, d_settings.d_optionsToSkip); + result = PacketCache::hashAfterQname(std::string_view(reinterpret_cast(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); diff --git a/pdns/dnsdistdist/dnsdist-cache.hh b/pdns/dnsdistdist/dnsdist-cache.hh index 6d76d9c7e7..d48397e8f9 100644 --- a/pdns/dnsdistdist/dnsdist-cache.hh +++ b/pdns/dnsdistdist/dnsdist-cache.hh @@ -38,6 +38,7 @@ public: struct CacheSettings { std::unordered_set d_optionsToSkip{EDNSOptionCode::COOKIE}; + std::vector 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); diff --git a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc index 766242067c..cb0b2d719a 100644 --- a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc +++ b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc @@ -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 ranks; for (const auto& option : cache.options_to_skip) { settings.d_optionsToSkip.insert(pdns::checked_stoi(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(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(settings); diff --git a/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc b/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc index c46a09a2fd..6441128f72 100644 --- a/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc +++ b/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc @@ -38,8 +38,9 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client) .d_shardCount = 20, }; bool cookieHashing = false; - bool skipHashingAR = false; LuaArray skipOptions; + LuaArray payloadRanks; + std::unordered_set ranks; size_t maximumEntrySize{4096}; getOptionalValue(vars, "deferrableInsertLock", settings.d_deferrableInsertLock); @@ -55,7 +56,6 @@ 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; @@ -67,6 +67,19 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client) } } + if (getOptionalValue(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(settings); }); diff --git a/pdns/dnsdistdist/dnsdist-settings-definitions.yml b/pdns/dnsdistdist/dnsdist-settings-definitions.yml index 96da7dcb4b..ca6d047b8d 100644 --- a/pdns/dnsdistdist/dnsdist-settings-definitions.yml +++ b/pdns/dnsdistdist/dnsdist-settings-definitions.yml @@ -1936,10 +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" + - name: "payload_ranks" + type: "Vec" + 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" diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 89f0767d84..c035266963 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -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 diff --git a/pdns/packetcache.hh b/pdns/packetcache.hh index 583ecfc8a9..c78d0d51d3 100644 --- a/pdns/packetcache.hh +++ b/pdns/packetcache.hh @@ -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& optionsToSkip = {EDNSOptionCode::COOKIE}) + static uint32_t hashAfterQname(const std::string_view& packet, uint32_t currentHash, size_t pos, const std::unordered_set& optionsToSkip = {EDNSOptionCode::COOKIE}, const std::vector& 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(&packet.at(pos)), packetSize - pos, currentHash); } return currentHash; } - currentHash = burtle(reinterpret_cast(&packet.at(pos)), 15, currentHash); + if (payloadRanks.empty()) { + currentHash = burtle(reinterpret_cast(&packet.at(pos)), 15, currentHash); + } + else { + std::vector 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(&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 */ diff --git a/regression-tests.dnsdist/test_Caching.py b/regression-tests.dnsdist/test_Caching.py index 26f3f95241..78aa81d231 100644 --- a/regression-tests.dnsdist/test_Caching.py +++ b/regression-tests.dnsdist/test_Caching.py @@ -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)