From 00b8cadc9fa22f0ac3d1e2cb5f68964471693130 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Thu, 23 Mar 2017 17:27:21 +0100 Subject: [PATCH] rec: Allow access to EDNS options from the `gettag()` hook If `gettag-needs-edns-options` is set, the EDNS options are extracted and passed to the `gettag()` hook as a table whose keys are the EDNS option code and the values are `EDNSOptionView` object. `EDNSOptionView` has two members, `content` and `size`, with `content` holding the raw, undecoded option value. --- docs/markdown/recursor/scripting.md | 7 +- docs/markdown/recursor/settings.md | 8 ++ pdns/ednsoptions.cc | 39 ++++++++ pdns/ednsoptions.hh | 10 ++ pdns/lua-recursor4.cc | 9 +- pdns/lua-recursor4.hh | 8 +- pdns/pdns_recursor.cc | 47 ++++++--- pdns/recursordist/Makefile.am | 2 + pdns/recursordist/ednscookies.cc | 1 + pdns/recursordist/ednscookies.hh | 1 + pdns/recursordist/test-ednsoptions_cc.cc | 115 +++++++++++++++++++++++ 11 files changed, 227 insertions(+), 20 deletions(-) create mode 120000 pdns/recursordist/ednscookies.cc create mode 120000 pdns/recursordist/ednscookies.hh create mode 100644 pdns/recursordist/test-ednsoptions_cc.cc diff --git a/docs/markdown/recursor/scripting.md b/docs/markdown/recursor/scripting.md index a82fdc42f..dd3822c4a 100644 --- a/docs/markdown/recursor/scripting.md +++ b/docs/markdown/recursor/scripting.md @@ -151,7 +151,7 @@ end This hook does not get the full DNSQuestion object, since filling out the fields would require packet parsing, which is what we are trying to prevent with `ipfilter`. -### `function gettag(remote, ednssubnet, local, qname, qtype)` +### `function gettag(remote, ednssubnet, local, qname, qtype, ednsoptions)` The `gettag` function is invoked when the Recursor attempts to discover in which packetcache an answer is available. @@ -168,6 +168,11 @@ e.g. been filtered for certain IPs (this logic should be implemented in the setting dq.variable to `true`. In the latter case, repeated queries will pass through the entire Lua script. +`ednsoptions` is a table whose keys are EDNS option codes and values are +`EDNSOptionView` objects, with the EDNS option content size in the `size` member +and the content accessible as a NULL-safe string object via `getContent()`. +This table is empty unless the `gettag-needs-edns-options` parameter is set. + ### `function prerpz(dq)` This hook is called before any filtering policy have been applied, making it diff --git a/docs/markdown/recursor/settings.md b/docs/markdown/recursor/settings.md index f55efc6d6..c25162599 100644 --- a/docs/markdown/recursor/settings.md +++ b/docs/markdown/recursor/settings.md @@ -360,6 +360,14 @@ forward queries to other recursive servers. The DNSSEC notes from [`forward-zones`](#forward-zones) apply here as well. +## `gettag-needs-edns-options` +* Boolean +* Default: no +* Available since: 4.1.0 + +If set, EDNS options in incoming queries are extracted and passed to the `gettag()` +hook in the `ednsoptions` table. + ## `hint-file` * Path diff --git a/pdns/ednsoptions.cc b/pdns/ednsoptions.cc index 1e5e56f2b..eade5df5f 100644 --- a/pdns/ednsoptions.cc +++ b/pdns/ednsoptions.cc @@ -66,6 +66,45 @@ int getEDNSOption(char* optRR, const size_t len, uint16_t wantedOption, char ** return ENOENT; } +/* extract all EDNS0 options from a pointer on the beginning rdLen of the OPT RR */ +int getEDNSOptions(const char* optRR, const size_t len, std::map& options) +{ + assert(optRR != NULL); + size_t pos = 0; + if (len < DNS_RDLENGTH_SIZE) + return EINVAL; + + const uint16_t rdLen = (((unsigned char) optRR[pos]) * 256) + ((unsigned char) optRR[pos+1]); + size_t rdPos = 0; + pos += DNS_RDLENGTH_SIZE; + if ((pos + rdLen) > len) { + return EINVAL; + } + + while(len >= (pos + EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE) && + rdLen >= (rdPos + EDNS_OPTION_CODE_SIZE + EDNS_OPTION_LENGTH_SIZE)) { + const uint16_t optionCode = (((unsigned char) optRR[pos]) * 256) + ((unsigned char) optRR[pos+1]); + pos += EDNS_OPTION_CODE_SIZE; + rdPos += EDNS_OPTION_CODE_SIZE; + const uint16_t optionLen = (((unsigned char) optRR[pos]) * 256) + ((unsigned char) optRR[pos+1]); + pos += EDNS_OPTION_LENGTH_SIZE; + rdPos += EDNS_OPTION_LENGTH_SIZE; + if (optionLen > (rdLen - rdPos) || optionLen > (len - pos)) + return EINVAL; + + EDNSOptionView view; + view.content = optRR + pos; + view.size = optionLen; + options[optionCode] = view; + + /* skip this option */ + pos += optionLen; + rdPos += optionLen; + } + + return 0; +} + void generateEDNSOption(uint16_t optionCode, const std::string& payload, std::string& res) { const uint16_t ednsOptionCode = htons(optionCode); diff --git a/pdns/ednsoptions.hh b/pdns/ednsoptions.hh index f9cb17093..ea0279a3e 100644 --- a/pdns/ednsoptions.hh +++ b/pdns/ednsoptions.hh @@ -31,6 +31,16 @@ struct EDNSOptionCode /* extract a specific EDNS0 option from a pointer on the beginning rdLen of the OPT RR */ int getEDNSOption(char* optRR, size_t len, uint16_t wantedOption, char ** optionValue, size_t * optionValueSize); + +struct EDNSOptionView +{ + const char* content{nullptr}; + uint16_t size{0}; +}; + +/* extract all EDNS0 options from a pointer on the beginning rdLen of the OPT RR */ +int getEDNSOptions(const char* optRR, size_t len, std::map& options); + void generateEDNSOption(uint16_t optionCode, const std::string& payload, std::string& res); #endif diff --git a/pdns/lua-recursor4.cc b/pdns/lua-recursor4.cc index e871f9e31..6445db8c6 100644 --- a/pdns/lua-recursor4.cc +++ b/pdns/lua-recursor4.cc @@ -384,7 +384,10 @@ RecursorLua4::RecursorLua4(const std::string& fname) d_lw->registerMember("type", &DNSRecord::d_type); d_lw->registerMember("ttl", &DNSRecord::d_ttl); d_lw->registerMember("place", &DNSRecord::d_place); - + + d_lw->registerMember("size", &EDNSOptionView::size); + d_lw->registerFunction("getContent", [](const EDNSOptionView& option) { return std::string(option.content, option.size); }); + d_lw->registerFunction("getContent", [](const DNSRecord& dr) { return dr.d_content->getZoneRepresentation(); }); d_lw->registerFunction(DNSRecord::*)()>("getCA", [](const DNSRecord& dr) { boost::optional ret; @@ -584,10 +587,10 @@ bool RecursorLua4::ipfilter(const ComboAddress& remote, const ComboAddress& loca return false; // don't block } -unsigned int RecursorLua4::gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector* policyTags, LuaContext::LuaObject& data) +unsigned int RecursorLua4::gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector* policyTags, LuaContext::LuaObject& data, const std::map& ednsOptions) { if(d_gettag) { - auto ret = d_gettag(remote, ednssubnet, local, qname, qtype); + auto ret = d_gettag(remote, ednssubnet, local, qname, qtype, ednsOptions); if (policyTags) { const auto& tags = std::get<1>(ret); diff --git a/pdns/lua-recursor4.hh b/pdns/lua-recursor4.hh index fcaabc5f2..7305b559d 100644 --- a/pdns/lua-recursor4.hh +++ b/pdns/lua-recursor4.hh @@ -25,7 +25,10 @@ #include "namespaces.hh" #include "dnsrecords.hh" #include "filterpo.hh" +#include "ednsoptions.hh" + #include + #ifdef HAVE_CONFIG_H #include "config.h" #endif @@ -98,7 +101,7 @@ public: DNSName followupName; }; - unsigned int gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector* policyTags, LuaContext::LuaObject& data); + unsigned int gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector* policyTags, LuaContext::LuaObject& data, const std::map&); bool prerpz(DNSQuestion& dq, int& ret); bool preresolve(DNSQuestion& dq, int& ret); @@ -118,8 +121,7 @@ public: d_postresolve); } - typedef std::function >,boost::optional >(ComboAddress, Netmask, ComboAddress, DNSName, -uint16_t)> gettag_t; + typedef std::function >,boost::optional >(ComboAddress, Netmask, ComboAddress, DNSName, uint16_t, const std::map&)> gettag_t; gettag_t d_gettag; // public so you can query if we have this hooked private: diff --git a/pdns/pdns_recursor.cc b/pdns/pdns_recursor.cc index e87a6a494..3a54b734a 100644 --- a/pdns/pdns_recursor.cc +++ b/pdns/pdns_recursor.cc @@ -154,6 +154,7 @@ static bool g_lowercaseOutgoing; static bool g_weDistributeQueries; // if true, only 1 thread listens on the incoming query sockets static bool g_reusePort{false}; static bool g_useOneSocketPerThread; +static bool g_gettagNeedsEDNSOptions{false}; std::unordered_set g_delegationOnly; RecursorControlChannel s_rcc; // only active in thread 0 @@ -1299,7 +1300,7 @@ static void makeControlChannelSocket(int processNum=-1) } } -static bool getQNameAndSubnet(const std::string& question, DNSName* dnsname, uint16_t* qtype, uint16_t* qclass, EDNSSubnetOpts* ednssubnet) +static bool getQNameAndSubnet(const std::string& question, DNSName* dnsname, uint16_t* qtype, uint16_t* qclass, EDNSSubnetOpts* ednssubnet, std::map* options) { bool found = false; const struct dnsheader* dh = (struct dnsheader*)question.c_str(); @@ -1313,14 +1314,29 @@ static bool getQNameAndSubnet(const std::string& question, DNSName* dnsname, uin if(ntohs(dh->arcount) == 1 && questionLen > pos + 11) { // this code can extract one (1) EDNS Subnet option /* OPT root label (1) followed by type (2) */ if(question.at(pos)==0 && question.at(pos+1)==0 && question.at(pos+2)==QType::OPT) { - char* ecsStart = nullptr; - size_t ecsLen = 0; - int res = getEDNSOption((char*)question.c_str()+pos+9, questionLen - pos - 9, EDNSOptionCode::ECS, &ecsStart, &ecsLen); - if (res == 0 && ecsLen > 4) { - EDNSSubnetOpts eso; - if(getEDNSSubnetOptsFromString(ecsStart + 4, ecsLen - 4, &eso)) { - *ednssubnet=eso; - found = true; + if (!options) { + char* ecsStart = nullptr; + size_t ecsLen = 0; + int res = getEDNSOption((char*)question.c_str()+pos+9, questionLen - pos - 9, EDNSOptionCode::ECS, &ecsStart, &ecsLen); + if (res == 0 && ecsLen > 4) { + EDNSSubnetOpts eso; + if(getEDNSSubnetOptsFromString(ecsStart + 4, ecsLen - 4, &eso)) { + *ednssubnet=eso; + found = true; + } + } + } + else { + int res = getEDNSOptions((char*)question.c_str()+pos+9, questionLen - pos - 9, *options); + if (res == 0) { + const auto& it = options->find(EDNSOptionCode::ECS); + if (it != options->end() && it->second.content != nullptr && it->second.size > 0) { + EDNSSubnetOpts eso; + if(getEDNSSubnetOptsFromString(it->second.content, it->second.size, &eso)) { + *ednssubnet=eso; + found = true; + } + } } } } @@ -1405,12 +1421,13 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var) if(needECS || (t_pdl->get() && (*t_pdl)->d_gettag)) { try { + std::map ednsOptions; dc->d_ecsParsed = true; - dc->d_ecsFound = getQNameAndSubnet(std::string(conn->data, conn->qlen), &qname, &qtype, &qclass, &dc->d_ednssubnet); + dc->d_ecsFound = getQNameAndSubnet(std::string(conn->data, conn->qlen), &qname, &qtype, &qclass, &dc->d_ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr); if(t_pdl->get() && (*t_pdl)->d_gettag) { try { - dc->d_tag = (*t_pdl)->gettag(conn->d_remote, dc->d_ednssubnet.source, dest, qname, qtype, &dc->d_policyTags, dc->d_data); + dc->d_tag = (*t_pdl)->gettag(conn->d_remote, dc->d_ednssubnet.source, dest, qname, qtype, &dc->d_policyTags, dc->d_data, ednsOptions); } catch(std::exception& e) { if(g_logCommonErrors) @@ -1576,13 +1593,14 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr if(needECS || (t_pdl->get() && (*t_pdl)->d_gettag)) { try { - ecsFound = getQNameAndSubnet(question, &qname, &qtype, &qclass, &ednssubnet); + std::map ednsOptions; + ecsFound = getQNameAndSubnet(question, &qname, &qtype, &qclass, &ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr); qnameParsed = true; ecsParsed = true; if(t_pdl->get() && (*t_pdl)->d_gettag) { try { - ctag=(*t_pdl)->gettag(fromaddr, ednssubnet.source, destaddr, qname, qtype, &policyTags, data); + ctag=(*t_pdl)->gettag(fromaddr, ednssubnet.source, destaddr, qname, qtype, &policyTags, data, ednsOptions); } catch(std::exception& e) { if(g_logCommonErrors) @@ -2804,6 +2822,8 @@ static int serviceMain(int argc, char*argv[]) g_numThreads = g_numWorkerThreads + g_weDistributeQueries; g_maxMThreads = ::arg().asNum("max-mthreads"); + g_gettagNeedsEDNSOptions = ::arg().mustDo("gettag-needs-edns-options"); + #ifdef SO_REUSEPORT g_reusePort = ::arg().mustDo("reuseport"); #endif @@ -3184,6 +3204,7 @@ int main(int argc, char **argv) ::arg().setSwitch( "root-nx-trust", "If set, believe that an NXDOMAIN from the root means the TLD does not exist")="yes"; ::arg().setSwitch( "any-to-tcp","Answer ANY queries with tc=1, shunting to TCP" )="no"; ::arg().setSwitch( "lowercase-outgoing","Force outgoing questions to lowercase")="no"; + ::arg().setSwitch("gettag-needs-edns-options", "If EDNS Options should be extracted before calling the gettag() hook")="no"; ::arg().set("udp-truncation-threshold", "Maximum UDP response size before we truncate")="1680"; ::arg().set("edns-outgoing-bufsize", "Outgoing EDNS buffer size")="1680"; ::arg().set("minimum-ttl-override", "Set under adverse conditions, a minimum TTL")="0"; diff --git a/pdns/recursordist/Makefile.am b/pdns/recursordist/Makefile.am index fec6d98ef..2ab142b11 100644 --- a/pdns/recursordist/Makefile.am +++ b/pdns/recursordist/Makefile.am @@ -185,6 +185,7 @@ testrunner_SOURCES = \ dnsrecords.cc \ dnssecinfra.cc \ dnswriter.cc dnswriter.hh \ + ednscookies.cc ednscookies.hh \ ednsoptions.cc ednsoptions.hh \ ednssubnet.cc ednssubnet.hh \ gettime.cc gettime.hh \ @@ -212,6 +213,7 @@ testrunner_SOURCES = \ test-dnsname_cc.cc \ test-dnsparser_hh.cc \ test-dnsrecords_cc.cc \ + test-ednsoptions_cc.cc \ test-iputils_hh.cc \ test-misc_hh.cc \ test-nmtree.cc \ diff --git a/pdns/recursordist/ednscookies.cc b/pdns/recursordist/ednscookies.cc new file mode 120000 index 000000000..e8c4721bb --- /dev/null +++ b/pdns/recursordist/ednscookies.cc @@ -0,0 +1 @@ +../ednscookies.cc \ No newline at end of file diff --git a/pdns/recursordist/ednscookies.hh b/pdns/recursordist/ednscookies.hh new file mode 120000 index 000000000..f29d488b6 --- /dev/null +++ b/pdns/recursordist/ednscookies.hh @@ -0,0 +1 @@ +../ednscookies.hh \ No newline at end of file diff --git a/pdns/recursordist/test-ednsoptions_cc.cc b/pdns/recursordist/test-ednsoptions_cc.cc new file mode 100644 index 000000000..4f48e4561 --- /dev/null +++ b/pdns/recursordist/test-ednsoptions_cc.cc @@ -0,0 +1,115 @@ +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_NO_MAIN + +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif +#include +#include + +#include "dnsname.hh" +#include "dnswriter.hh" +#include "ednscookies.hh" +#include "ednsoptions.hh" +#include "ednssubnet.hh" +#include "iputils.hh" + +/* extract a specific EDNS0 option from a pointer on the beginning rdLen of the OPT RR */ +int getEDNSOption(char* optRR, size_t len, uint16_t wantedOption, char ** optionValue, size_t * optionValueSize); + +BOOST_AUTO_TEST_SUITE(ednsoptions_cc) + +static void getRawQueryWithECSAndCookie(const DNSName& name, const Netmask& ecs, const std::string& clientCookie, const std::string& serverCookie, std::vector& query) +{ + DNSPacketWriter pw(query, name, QType::A, QClass::IN, 0); + pw.commit(); + + EDNSCookiesOpt cookiesOpt; + cookiesOpt.client = clientCookie; + cookiesOpt.server = serverCookie; + string cookiesOptionStr = makeEDNSCookiesOptString(cookiesOpt); + EDNSSubnetOpts ecsOpts; + ecsOpts.source = ecs; + string origECSOptionStr = makeEDNSSubnetOptsString(ecsOpts); + DNSPacketWriter::optvect_t opts; + opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr)); + opts.push_back(make_pair(EDNSOptionCode::ECS, origECSOptionStr)); + opts.push_back(make_pair(EDNSOptionCode::COOKIE, cookiesOptionStr)); + pw.addOpt(512, 0, 0, opts); + pw.commit(); +} + +BOOST_AUTO_TEST_CASE(test_getEDNSOption) { + DNSName name("www.powerdns.com."); + Netmask ecs("127.0.0.1/32"); + vector query; + + getRawQueryWithECSAndCookie(name, ecs, "deadbeef", "deadbeef", query); + + const struct dnsheader* dh = reinterpret_cast(query.data()); + size_t questionLen = query.size(); + unsigned int consumed = 0; + DNSName dnsname = DNSName(reinterpret_cast(query.data()), questionLen, sizeof(dnsheader), false, nullptr, nullptr, &consumed); + + size_t pos = sizeof(dnsheader) + consumed + 4; + /* at least OPT root label (1), type (2), class (2) and ttl (4) + OPT RR rdlen (2) = 11 */ + BOOST_REQUIRE_EQUAL(ntohs(dh->arcount), 1); + BOOST_REQUIRE(questionLen > pos + 11); + /* OPT root label (1) followed by type (2) */ + BOOST_REQUIRE_EQUAL(query.at(pos), 0); + BOOST_REQUIRE(query.at(pos+2) == QType::OPT); + + char* ecsStart = nullptr; + size_t ecsLen = 0; + int res = getEDNSOption(reinterpret_cast(query.data())+pos+9, questionLen - pos - 9, EDNSOptionCode::ECS, &ecsStart, &ecsLen); + BOOST_CHECK_EQUAL(res, 0); + + EDNSSubnetOpts eso; + BOOST_REQUIRE(getEDNSSubnetOptsFromString(ecsStart + 4, ecsLen - 4, &eso)); + + BOOST_CHECK(eso.source == ecs); +} + +BOOST_AUTO_TEST_CASE(test_getEDNSOptions) { + DNSName name("www.powerdns.com."); + Netmask ecs("127.0.0.1/32"); + vector query; + + getRawQueryWithECSAndCookie(name, ecs, "deadbeef", "deadbeef", query); + + const struct dnsheader* dh = reinterpret_cast(query.data()); + size_t questionLen = query.size(); + unsigned int consumed = 0; + DNSName dnsname = DNSName(reinterpret_cast(query.data()), questionLen, sizeof(dnsheader), false, nullptr, nullptr, &consumed); + + size_t pos = sizeof(dnsheader) + consumed + 4; + /* at least OPT root label (1), type (2), class (2) and ttl (4) + OPT RR rdlen (2) = 11 */ + BOOST_REQUIRE_EQUAL(ntohs(dh->arcount), 1); + BOOST_REQUIRE(questionLen > pos + 11); + /* OPT root label (1) followed by type (2) */ + BOOST_REQUIRE_EQUAL(query.at(pos), 0); + BOOST_REQUIRE(query.at(pos+2) == QType::OPT); + + std::map options; + int res = getEDNSOptions(reinterpret_cast(query.data())+pos+9, questionLen - pos - 9, options); + BOOST_REQUIRE_EQUAL(res, 0); + + /* 3 EDNS options but two of them are EDNS Cookie, so we only keep one */ + BOOST_CHECK_EQUAL(options.size(), 2); + + auto it = options.find(EDNSOptionCode::ECS); + BOOST_REQUIRE(it != options.end()); + BOOST_REQUIRE(it->second.content != nullptr); + BOOST_REQUIRE_GT(it->second.size, 0); + + EDNSSubnetOpts eso; + BOOST_REQUIRE(getEDNSSubnetOptsFromString(it->second.content, it->second.size, &eso)); + BOOST_CHECK(eso.source == ecs); + + it = options.find(EDNSOptionCode::COOKIE); + BOOST_REQUIRE(it != options.end()); + BOOST_REQUIRE(it->second.content != nullptr); + BOOST_REQUIRE_GT(it->second.size, 0); +} + +BOOST_AUTO_TEST_SUITE_END() -- 2.40.0