From: Karel Bilek Date: Tue, 9 Sep 2025 14:03:33 +0000 (+0200) Subject: dnsdist: implement simple packet shuffle in cache X-Git-Tag: rec-5.4.0-alpha1~90^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=a6f6d7f16ed0f053581b83dc696bea9a4410c327;p=thirdparty%2Fpdns.git dnsdist: implement simple packet shuffle in cache The shuffle is implementing by directly swapping pieces of RData memory in a single RRSet. Signed-off-by: Karel Bilek --- diff --git a/pdns/dnsdistdist/dnsdist-cache.cc b/pdns/dnsdistdist/dnsdist-cache.cc index afc1575bac..e0adb1cb03 100644 --- a/pdns/dnsdistdist/dnsdist-cache.cc +++ b/pdns/dnsdistdist/dnsdist-cache.cc @@ -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(response.data()), response.size(), dh_aligned); + } + ++d_hits; return true; } diff --git a/pdns/dnsdistdist/dnsdist-cache.hh b/pdns/dnsdistdist/dnsdist-cache.hh index 8efc1701c5..71239a4d3e 100644 --- a/pdns/dnsdistdist/dnsdist-cache.hh +++ b/pdns/dnsdistdist/dnsdist-cache.hh @@ -52,6 +52,7 @@ public: bool d_deferrableInsertLock{true}; bool d_parseECS{false}; bool d_keepStaleData{false}; + bool d_shuffle{false}; }; DNSDistPacketCache(CacheSettings settings); diff --git a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc index d97f8898b2..e6064deca9 100644 --- a/pdns/dnsdistdist/dnsdist-configuration-yaml.cc +++ b/pdns/dnsdistdist/dnsdist-configuration-yaml.cc @@ -999,6 +999,7 @@ static void handlePacketCacheConfiguration(const ::rust::Vec ranks; if (!cache.options_to_skip.empty()) { diff --git a/pdns/dnsdistdist/dnsdist-console-completion.cc b/pdns/dnsdistdist/dnsdist-console-completion.cc index a5c7d93133..de9317de01 100644 --- a/pdns/dnsdistdist/dnsdist-console-completion.cc +++ b/pdns/dnsdistdist/dnsdist-console-completion.cc @@ -186,7 +186,7 @@ static std::vector 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()`"}, diff --git a/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc b/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc index 9e08e3794f..6a3fb0059a 100644 --- a/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc +++ b/pdns/dnsdistdist/dnsdist-lua-bindings-packetcache.cc @@ -46,6 +46,7 @@ void setupLuaBindingsPacketCache(LuaContext& luaCtx, bool client) getOptionalValue(vars, "deferrableInsertLock", settings.d_deferrableInsertLock); getOptionalValue(vars, "dontAge", settings.d_dontAge); getOptionalValue(vars, "keepStaleData", settings.d_keepStaleData); + getOptionalValue(vars, "shuffle", settings.d_shuffle); getOptionalValue(vars, "maxNegativeTTL", settings.d_maxNegativeTTL); getOptionalValue(vars, "maxTTL", settings.d_maxTTL); getOptionalValue(vars, "minTTL", settings.d_minTTL); diff --git a/pdns/dnsdistdist/dnsdist-settings-definitions.yml b/pdns/dnsdistdist/dnsdist-settings-definitions.yml index b0f7a70b3c..54d59d59c6 100644 --- a/pdns/dnsdistdist/dnsdist-settings-definitions.yml +++ b/pdns/dnsdistdist/dnsdist-settings-definitions.yml @@ -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" diff --git a/pdns/dnsdistdist/docs/guides/cache.rst b/pdns/dnsdistdist/docs/guides/cache.rst index 24b1a3f00d..536e2b3716 100644 --- a/pdns/dnsdistdist/docs/guides/cache.rst +++ b/pdns/dnsdistdist/docs/guides/cache.rst @@ -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" diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 1c96a7da6a..402927f0b1 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -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 diff --git a/pdns/dnsparser.cc b/pdns/dnsparser.cc index 4c70d8052d..b0eaa690ee 100644 --- a/pdns/dnsparser.cc +++ b/pdns/dnsparser.cc @@ -24,6 +24,7 @@ #include #include +#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> 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>::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::max(); diff --git a/pdns/dnsparser.hh b/pdns/dnsparser.hh index 4deb14f35b..7e8142a9cc 100644 --- a/pdns/dnsparser.hh +++ b/pdns/dnsparser.hh @@ -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& visitor); @@ -617,6 +618,15 @@ public: moveOffset(toskip); } + std::pair skipRDataAndReturnOffsets() + { + auto toskip = get16BitInt(); + uint32_t start = d_offset; + moveOffset(toskip); + uint32_t end = d_offset; + return std::pair(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 a, std::pair 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) {