diff --git a/pdns/dnswriter.cc b/pdns/dnswriter.cc index 09f195db853e..c106ee6f9035 100644 --- a/pdns/dnswriter.cc +++ b/pdns/dnswriter.cc @@ -451,7 +451,7 @@ template void GenericDNSPacketWriter::getRecordP records.assign(d_content.begin() + d_sor, d_content.end()); } -template uint32_t GenericDNSPacketWriter::size() +template uint32_t GenericDNSPacketWriter::size() const { return d_content.size(); } @@ -495,8 +495,18 @@ template void GenericDNSPacketWriter::commit() } +template size_t GenericDNSPacketWriter::getSizeWithOpts(const optvect_t& options) const +{ + size_t result = size() + /* root */ 1 + DNS_TYPE_SIZE + DNS_CLASS_SIZE + DNS_TTL_SIZE + DNS_RDLENGTH_SIZE; + + for(auto const &option : options) { + result += 4; + result += option.second.size(); + } + + return result; +} + template class GenericDNSPacketWriter>; #include "noinitvector.hh" template class GenericDNSPacketWriter; - - diff --git a/pdns/dnswriter.hh b/pdns/dnswriter.hh index aef99ae49d7d..a8e13cbe1bf7 100644 --- a/pdns/dnswriter.hh +++ b/pdns/dnswriter.hh @@ -77,7 +77,7 @@ public: */ void commit(); - uint32_t size(); // needs to be 32 bit because otherwise we don't see the wrap coming when it happened! + uint32_t size() const; // needs to be 32 bit because otherwise we don't see the wrap coming when it happened! /** Should the packet have grown too big for the writer's liking, rollback removes the record currently being written */ void rollback(); @@ -155,6 +155,9 @@ public: const string getRemaining() const { return ""; } + + size_t getSizeWithOpts(const optvect_t& options) const; + private: uint16_t lookupName(const DNSName& name, uint16_t* matchlen); vector d_namepositions; diff --git a/pdns/ednspadding.cc b/pdns/ednspadding.cc new file mode 100644 index 000000000000..d5ab8ee18738 --- /dev/null +++ b/pdns/ednspadding.cc @@ -0,0 +1,31 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "ednspadding.hh" + +std::string makeEDNSPaddingOptString(size_t bytes) +{ + std::string ret; + ret.resize(bytes, '0'); + return ret; +} + diff --git a/pdns/ednspadding.hh b/pdns/ednspadding.hh new file mode 100644 index 000000000000..9db16c6abcaa --- /dev/null +++ b/pdns/ednspadding.hh @@ -0,0 +1,26 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#pragma once + +#include + +std::string makeEDNSPaddingOptString(size_t bytes); diff --git a/pdns/pdns_recursor.cc b/pdns/pdns_recursor.cc index b9e79acb5b43..f219de1672fe 100644 --- a/pdns/pdns_recursor.cc +++ b/pdns/pdns_recursor.cc @@ -90,6 +90,7 @@ #include "rec-lua-conf.hh" #include "ednsoptions.hh" #include "ednsextendederror.hh" +#include "ednspadding.hh" #include "gettime.hh" #include "proxy-protocol.hh" #include "pubsuffix.hh" @@ -192,6 +193,7 @@ static deferredAdd_t g_deferredAdds; typedef vector tcpListenSockets_t; typedef map listenSocketsAddresses_t; // is shared across all threads right now +enum class PaddingMode { Always, PaddedQueries }; static listenSocketsAddresses_t g_listenSocketsAddresses; // is shared across all threads right now static set g_fromtosockets; // listen sockets that use 'sendfromto()' mechanism (without actually using sendfromto()) @@ -200,6 +202,7 @@ static std::shared_ptr g_initialDomainMap; // new threads static std::shared_ptr g_initialAllowFrom; // new thread needs to be setup with this static NetmaskGroup g_XPFAcl; static NetmaskGroup g_proxyProtocolACL; +static NetmaskGroup g_paddingFrom; static boost::optional g_dns64Prefix{boost::none}; static DNSName g_dns64PrefixReverse; static size_t g_proxyProtocolMaximumSize; @@ -211,9 +214,11 @@ static unsigned int g_maxTCPPerClient; static unsigned int g_maxMThreads; static unsigned int g_numDistributorThreads; static unsigned int g_numWorkerThreads; +static unsigned int g_paddingTag; static int g_tcpTimeout; static uint16_t g_udpTruncationThreshold; static uint16_t g_xpfRRCode{0}; +static PaddingMode g_paddingMode; static std::atomic statsWanted; static std::atomic g_quiet; static bool g_logCommonErrors; @@ -1484,6 +1489,8 @@ static void startDoResolve(void *p) std::vector > ednsOpts; bool variableAnswer = dc->d_variable; bool haveEDNS=false; + bool paddingAllowed = g_paddingFrom.match(dc->d_remote); + bool padResponse = paddingAllowed ? (g_paddingMode == PaddingMode::Always) : false; #ifdef NOD_ENABLED bool hasUDR = false; #endif /* NOD_ENABLED */ @@ -1515,9 +1522,24 @@ static void startDoResolve(void *p) variableAnswer = true; // Can't packetcache an answer with NSID maxanswersize -= EDNSOptionCodeSize + EDNSOptionLengthSize + mode_server_id.size(); } + } else if (paddingAllowed && padResponse == false && g_paddingMode == PaddingMode::PaddedQueries && o.first == EDNSOptionCode::PADDING) { + /* we should only pad on 'secure' cases to limit amplification: + - over TCP ; + - from 'known-good' sources (dnsdist for example). + We should also support a strict mode (only pad if the query has a padding option) + and a relaxed mode (pad if the client supports EDNS). + The block-size should be configurable as well. + */ + padResponse = true; + maxanswersize -= 4; } } } + + if (padResponse && dc->d_tag == 0) { + dc->d_tag = g_paddingTag; + } + /* perhaps there was no EDNS or no ECS but by now we looked */ dc->d_ecsParsed = true; vector ret; @@ -1968,6 +1990,26 @@ static void startDoResolve(void *p) } } + if (padResponse) { + size_t currentSize = pw.getSizeWithOpts(returnedEdnsOptions); + if (currentSize < (static_cast(maxanswersize) - 4)) { + size_t remaining = static_cast(maxanswersize) - currentSize; + /* from rfc8647, "4.1. Recommended Strategy: Block-Length Padding": + If a server receives a query that includes the EDNS(0) "Padding" + option, it MUST pad the corresponding response (see Section 4 of + RFC 7830) and SHOULD pad the corresponding response to a + multiple of 468 octets (see below). + */ + static const size_t blockSize = 468; + size_t modulo = (currentSize + 4) % blockSize; + size_t padSize = 0; + if (modulo > 0) { + padSize = std::min(blockSize - modulo, remaining); + } + returnedEdnsOptions.push_back(make_pair(EDNSOptionCode::PADDING, makeEDNSPaddingOptString(padSize))); + } + } + if (haveEDNS) { auto state = sr.getValidationState(); if (dc->d_extendedErrorCode || (s_addExtendedResolutionDNSErrors && vStateIsBogus(state))) { @@ -2599,6 +2641,10 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) } } + if (dc->d_tag == 0 && g_paddingFrom.match(dc->d_remote)) { + dc->d_tag = g_paddingTag; + } + const struct dnsheader* dh = reinterpret_cast(&conn->data[0]); if (t_protobufServers || t_outgoingProtobufServers) { @@ -2889,13 +2935,15 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr } RecursorPacketCache::OptPBData pbData{boost::none}; - if (t_protobufServers) { if (logQuery && !(luaconfsLocal->protobufExportConfig.taggedOnly && policyTags.empty())) { protobufLogQuery(luaconfsLocal, uniqueId, source, destination, ednssubnet.source, false, dh->id, question.size(), qname, qtype, qclass, policyTags, requestorId, deviceId, deviceName); } } + if (ctag == 0 && g_paddingFrom.match(fromaddr)) { + ctag = g_paddingTag; + } /* It might seem like a good idea to skip the packet cache lookup if we know that the answer is not cacheable, but it means that the hash would not be computed. If some script decides at a later time to mark back the answer as cacheable we would cache it with a wrong tag, so better safe than sorry. */ @@ -4716,6 +4764,19 @@ static int serviceMain(int argc, char*argv[]) g_lowercaseOutgoing = ::arg().mustDo("lowercase-outgoing"); + g_paddingFrom.toMasks(::arg()["edns-padding-from"]); + if (::arg()["edns-padding-mode"] == "always") { + g_paddingMode = PaddingMode::Always; + } + else if (::arg()["edns-padding-mode"] == "padded-queries-only") { + g_paddingMode = PaddingMode::PaddedQueries; + } + else { + g_log << Logger::Error << "Unknown edns-padding-mode: " << ::arg()["edns-padding-mode"] << endl; + exit(1); + } + g_paddingTag = ::arg().asNum("edns-padding-tag"); + g_numDistributorThreads = ::arg().asNum("distributor-threads"); g_numWorkerThreads = ::arg().asNum("threads"); if (g_numWorkerThreads < 1) { @@ -5530,6 +5591,10 @@ int main(int argc, char **argv) ::arg().setSwitch("aggressive-nsec-cache-size", "The number of records to cache in the aggressive cache. If set to a value greater than 0, and DNSSEC validation is enabled, the recursor will cache NSEC and NSEC3 records to generate negative answers, as defined in rfc8198")="100000"; + ::arg().set("edns-padding-from", "Sources (proxy IP in case of XPF or proxy-protocol presence, client IP otherwise) for which EDNS padding will be enabled in responses")=""; + ::arg().set("edns-padding-mode", "Whether to add EDNS padding to all responses ('always') or only to the ones to padded queries ('padded-queries-only')")="padded-queries-only"; + ::arg().set("edns-padding-tag", "Packetcache tag associated to responses sent with EDNS padding, to prevent sending these to non-whitelisted clients.")="7830"; + ::arg().setCmd("help","Provide a helpful message"); ::arg().setCmd("version","Print version string"); ::arg().setCmd("config","Output blank configuration"); diff --git a/pdns/recursordist/Makefile.am b/pdns/recursordist/Makefile.am index fe925a4b59b8..64e3c8f9d432 100644 --- a/pdns/recursordist/Makefile.am +++ b/pdns/recursordist/Makefile.am @@ -117,6 +117,7 @@ pdns_recursor_SOURCES = \ dnswriter.cc dnswriter.hh \ ednsextendederror.cc ednsextendederror.hh \ ednsoptions.cc ednsoptions.hh \ + ednspadding.cc ednspadding.hh \ ednssubnet.cc ednssubnet.hh \ filterpo.cc filterpo.hh \ fstrm_logger.cc fstrm_logger.hh \ @@ -246,6 +247,7 @@ testrunner_SOURCES = \ ednscookies.cc ednscookies.hh \ ednsextendederror.cc ednsextendederror.hh \ ednsoptions.cc ednsoptions.hh \ + ednspadding.cc ednspadding.hh \ ednssubnet.cc ednssubnet.hh \ filterpo.cc filterpo.hh \ gettime.cc gettime.hh \ diff --git a/pdns/recursordist/docs/settings.rst b/pdns/recursordist/docs/settings.rst index 4a76db041b91..82f8ac64f6e2 100644 --- a/pdns/recursordist/docs/settings.rst +++ b/pdns/recursordist/docs/settings.rst @@ -610,6 +610,40 @@ found, the recursor fallbacks to sending 127.0.0.1. This is the value set for the EDNS0 buffer size in outgoing packets. Lower this if you experience timeouts. +.. _setting-edns-padding-from: + +``edns-padding-from`` +--------------------- +.. versionadded:: 4.4.0 + +- Comma separated list of netmasks +- Default: (none) + +List of netmasks (proxy IP in case of XPF or proxy-protocol presence, client IP otherwise) for which EDNS padding will be enabled in responses. See also `edns-padding-mode`_. + +.. _setting-edns-padding-mode: + +``edns-padding-mode`` +--------------------- +.. versionadded:: 4.4.0 + +- One of ``always``, ``padded-queries-only``, String +- Default: ``padded-queries-only`` + +Whether to add EDNS padding to all responses (``always``) or only to the ones for padded queries (``padded-queries-only``, the default) coming from `edns-padding-from`_ sources. + +.. _setting-edns-padding-mode: + +``edns-padding-tag`` +-------------------- +.. versionadded:: 4.4.0 + +- Integer +- Default: 7830 + +The packetcache tag to use for padded responses, to prevent a client not allowed by the `edns-padding-from`_ list to be served a cached answer generated for an allowed one. This +effectively divides the packet cache in two when `edns-padding-from`_ is used. Note that this will not override a tag set from one of the ``Lua`` hooks. + .. _setting-edns-subnet-whitelist: ``edns-subnet-whitelist`` diff --git a/pdns/recursordist/ednspadding.cc b/pdns/recursordist/ednspadding.cc new file mode 120000 index 000000000000..fa7a85231b2a --- /dev/null +++ b/pdns/recursordist/ednspadding.cc @@ -0,0 +1 @@ +../ednspadding.cc \ No newline at end of file diff --git a/pdns/recursordist/ednspadding.hh b/pdns/recursordist/ednspadding.hh new file mode 120000 index 000000000000..8701869f0937 --- /dev/null +++ b/pdns/recursordist/ednspadding.hh @@ -0,0 +1 @@ +../ednspadding.hh \ No newline at end of file