]> granicus.if.org Git - pdns/commitdiff
rec: Allow access to EDNS options from the `gettag()` hook
authorRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 23 Mar 2017 16:27:21 +0000 (17:27 +0100)
committerPeter van Dijk <peter.van.dijk@powerdns.com>
Fri, 24 Mar 2017 10:15:16 +0000 (11:15 +0100)
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
docs/markdown/recursor/settings.md
pdns/ednsoptions.cc
pdns/ednsoptions.hh
pdns/lua-recursor4.cc
pdns/lua-recursor4.hh
pdns/pdns_recursor.cc
pdns/recursordist/Makefile.am
pdns/recursordist/ednscookies.cc [new symlink]
pdns/recursordist/ednscookies.hh [new symlink]
pdns/recursordist/test-ednsoptions_cc.cc [new file with mode: 0644]

index a82fdc42fc4b484ab6d8a5564f3ee947bf6fd584..dd3822c4aea3e00b4db69376b1e7aa96877a3ae5 100644 (file)
@@ -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
index f55efc6d6591552f0a1da7da2d12217ec1a4b816..c25162599876dd99abafcb1e0ae0c7db8575da1b 100644 (file)
@@ -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
 
index 1e5e56f2bf04d50d6a2a87debf920ce2c28a3982..eade5df5fffaf2d007b74fe143d686e781ba988c 100644 (file)
@@ -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<uint16_t, EDNSOptionView>& 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);
index f9cb17093bb30c75528be422c0a9f69f94f2ff6c..ea0279a3e069d6bee96018b4c0bf6852afddf053 100644 (file)
@@ -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<uint16_t, EDNSOptionView>& options);
+
 void generateEDNSOption(uint16_t optionCode, const std::string& payload, std::string& res);
 
 #endif
index e871f9e3197ebdae511ce2e78973c1020b96c03c..6445db8c6ecd52575803fdd1508153a7470c9d0e 100644 (file)
@@ -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<std::string(EDNSOptionView::*)()>("getContent", [](const EDNSOptionView& option) { return std::string(option.content, option.size); });
+
   d_lw->registerFunction<string(DNSRecord::*)()>("getContent", [](const DNSRecord& dr) { return dr.d_content->getZoneRepresentation(); });
   d_lw->registerFunction<boost::optional<ComboAddress>(DNSRecord::*)()>("getCA", [](const DNSRecord& dr) { 
       boost::optional<ComboAddress> 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<std::string>* 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<std::string>* policyTags, LuaContext::LuaObject& data, const std::map<uint16_t, EDNSOptionView>& 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);
index fcaabc5f2df223b4d11dad61da37700a278eb953..7305b559d5ee97cdd81dc425b05f6d86a1091d12 100644 (file)
 #include "namespaces.hh"
 #include "dnsrecords.hh"
 #include "filterpo.hh"
+#include "ednsoptions.hh"
+
 #include <unordered_map>
+
 #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<std::string>* policyTags, LuaContext::LuaObject& data);
+  unsigned int gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data, const std::map<uint16_t, EDNSOptionView>&);
 
   bool prerpz(DNSQuestion& dq, int& ret);
   bool preresolve(DNSQuestion& dq, int& ret);
@@ -118,8 +121,7 @@ public:
             d_postresolve);
   }
 
-  typedef std::function<std::tuple<unsigned int,boost::optional<std::unordered_map<int,string> >,boost::optional<LuaContext::LuaObject> >(ComboAddress, Netmask, ComboAddress, DNSName, 
-uint16_t)> gettag_t;
+  typedef std::function<std::tuple<unsigned int,boost::optional<std::unordered_map<int,string> >,boost::optional<LuaContext::LuaObject> >(ComboAddress, Netmask, ComboAddress, DNSName, uint16_t, const std::map<uint16_t, EDNSOptionView>&)> gettag_t;
   gettag_t d_gettag; // public so you can query if we have this hooked
 
 private:
index e87a6a4945f7ea44abc389f4c1d60da06b89cf86..3a54b734a2910f5732c8dd0513f6ca32675c3cdd 100644 (file)
@@ -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<DNSName> 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<uint16_t, EDNSOptionView>* 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<uint16_t, EDNSOptionView> 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<uint16_t, EDNSOptionView> 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";
index fec6d98efc792a19ff63bf1d4afff0f1e0f88133..2ab142b11af09e70414ab33bb2d325302b2a3abf 100644 (file)
@@ -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 (symlink)
index 0000000..e8c4721
--- /dev/null
@@ -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 (symlink)
index 0000000..f29d488
--- /dev/null
@@ -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 (file)
index 0000000..4f48e45
--- /dev/null
@@ -0,0 +1,115 @@
+#define BOOST_TEST_DYN_LINK
+#define BOOST_TEST_NO_MAIN
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include <boost/test/unit_test.hpp>
+#include <utility>
+
+#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<uint8_t>& 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<uint8_t> query;
+
+  getRawQueryWithECSAndCookie(name, ecs, "deadbeef", "deadbeef", query);
+
+  const struct dnsheader* dh = reinterpret_cast<struct dnsheader*>(query.data());
+  size_t questionLen = query.size();
+  unsigned int consumed = 0;
+  DNSName dnsname = DNSName(reinterpret_cast<const char*>(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<char*>(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<uint8_t> query;
+
+  getRawQueryWithECSAndCookie(name, ecs, "deadbeef", "deadbeef", query);
+
+  const struct dnsheader* dh = reinterpret_cast<struct dnsheader*>(query.data());
+  size_t questionLen = query.size();
+  unsigned int consumed = 0;
+  DNSName dnsname = DNSName(reinterpret_cast<const char*>(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<uint16_t, EDNSOptionView> options;
+  int res = getEDNSOptions(reinterpret_cast<char*>(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()