]> granicus.if.org Git - pdns/commitdiff
dnsdist: Add security polling
authorRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 30 Oct 2018 10:11:49 +0000 (11:11 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 30 Oct 2018 10:11:49 +0000 (11:11 +0100)
pdns/dnsdist-console.cc
pdns/dnsdist-lua.cc
pdns/dnsdist.cc
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-secpoll.cc [new file with mode: 0644]
pdns/dnsdistdist/dnsdist-secpoll.hh [new file with mode: 0644]
pdns/dnsdistdist/docs/reference/config.rst
pdns/misc.cc
pdns/misc.hh
pdns/stubresolver.cc

index d0195c9ecc766f2bae176d3e636403d5afda70d8..c9bdcb0c73b72009ee1251453eea43214bd747ec 100644 (file)
@@ -435,6 +435,8 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "setRingBuffersLockRetries", true, "n", "set the number of attempts to get a non-blocking lock to a ringbuffer shard before blocking" },
   { "setRingBuffersSize", true, "n [, numberOfShards]", "set the capacity of the ringbuffers used for live traffic inspection to `n`, and optionally the number of shards to use to `numberOfShards`" },
   { "setRules", true, "list of rules", "replace the current rules with the supplied list of pairs of DNS Rules and DNS Actions (see `newRuleAction()`)" },
+  { "setSecurityPollInterval", true, "n", "set the security polling interval to `n` seconds" },
+  { "setSecurityPollSuffix", true, "suffix", "set the security polling suffix to the specified value" },
   { "setServerPolicy", true, "policy", "set server selection policy to that policy" },
   { "setServerPolicyLua", true, "name, function", "set server selection policy to one named 'name' and provided by 'function'" },
   { "setServFailWhenNoServer", true, "bool", "if set, return a ServFail when no servers are available, instead of the default behaviour of dropping the query" },
index 40abb4bce865c5b91f718a57f4042fb3fc55dd82..cc169c3dc26012622a2c54b8d12f8d4e5262c11f 100644 (file)
@@ -36,6 +36,7 @@
 #include "dnsdist-ecs.hh"
 #include "dnsdist-lua.hh"
 #include "dnsdist-rings.hh"
+#include "dnsdist-secpoll.hh"
 
 #include "base64.hh"
 #include "dnswriter.hh"
@@ -1506,6 +1507,24 @@ void setupLuaConfig(bool client)
       g_PayloadSizeSelfGenAnswers = payloadSize;
   });
 
+  g_lua.writeFunction("setSecurityPollSuffix", [](const std::string& suffix) {
+      if (g_configurationDone) {
+        g_outputBuffer="setSecurityPollSuffix() cannot be used at runtime!\n";
+        return;
+      }
+
+      g_secPollSuffix = suffix;
+  });
+
+  g_lua.writeFunction("setSecurityPollInterval", [](time_t newInterval) {
+      if (newInterval <= 0) {
+        warnlog("setSecurityPollInterval() should be > 0, skipping");
+        g_outputBuffer="setSecurityPollInterval() should be > 0, skipping";
+      }
+
+      g_secPollInterval = newInterval;
+  });
+
   g_lua.writeFunction("addTLSLocal", [client](const std::string& addr, boost::variant<std::string, std::vector<std::pair<int,std::string>>> certFiles, boost::variant<std::string, std::vector<std::pair<int,std::string>>> keyFiles, boost::optional<localbind_t> vars) {
         if (client)
           return;
index f5e9f088ce772f1b24a7d228d0b7f695a2ac53ce..1f19cb56b1363de02806619d39d898ed0dae6cb7 100644 (file)
@@ -47,6 +47,7 @@
 #include "dnsdist-ecs.hh"
 #include "dnsdist-lua.hh"
 #include "dnsdist-rings.hh"
+#include "dnsdist-secpoll.hh"
 
 #include "base64.hh"
 #include "delaypipe.hh"
@@ -1914,7 +1915,21 @@ void* maintThread()
   return 0;
 }
 
-void* healthChecksThread()
+static void* secPollThread()
+{
+  setThreadName("dnsdist/secpoll");
+
+  for (;;) {
+    try {
+      doSecPoll(g_secPollSuffix);
+    }
+    catch(...) {
+    }
+    sleep(g_secPollInterval);
+  }
+}
+
+static void* healthChecksThread()
 {
   setThreadName("dnsdist/healthC");
 
@@ -2775,6 +2790,11 @@ try
   
   thread healththread(healthChecksThread);
 
+  if (!g_secPollSuffix.empty()) {
+    thread secpollthread(secPollThread);
+    secpollthread.detach();
+  }
+
   if(g_cmdLine.beSupervised) {
 #ifdef HAVE_SYSTEMD
     sd_notify(0, "READY=1");
index 8341b1bd493f14ec9ee666bd272f7d684885a237..f2d79be915783a01f9258f0cb9080b75322b5132 100644 (file)
@@ -107,6 +107,7 @@ dnsdist_SOURCES = \
        dnsdist-protobuf.cc dnsdist-protobuf.hh \
        dnsdist-rings.cc dnsdist-rings.hh \
        dnsdist-rules.hh \
+       dnsdist-secpoll.cc dnsdist-secpoll.hh \
        dnsdist-snmp.cc dnsdist-snmp.hh \
        dnsdist-tcp.cc \
        dnsdist-web.cc \
diff --git a/pdns/dnsdistdist/dnsdist-secpoll.cc b/pdns/dnsdistdist/dnsdist-secpoll.cc
new file mode 100644 (file)
index 0000000..49b9559
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+ * 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 "config.h"
+
+#include <string>
+#include <vector>
+
+#ifdef HAVE_LIBSODIUM
+#include <sodium.h>
+#endif /* HAVE_LIBSODIUM */
+
+#include "dnsparser.hh"
+#include "dolog.hh"
+#include "iputils.hh"
+#include "misc.hh"
+#include "sstuff.hh"
+
+#include "dnsdist-secpoll.hh"
+
+static std::string getFirstTXTAnswer(const std::string& answer)
+{
+  if (answer.size() <= sizeof(struct dnsheader)) {
+    throw std::runtime_error("Looking for a TXT record in an answer smaller than the DNS header");
+  }
+
+  const struct dnsheader* dh = reinterpret_cast<const struct dnsheader*>(answer.data());
+  PacketReader pr(answer);
+  uint16_t qdcount = ntohs(dh->qdcount);
+  uint16_t ancount = ntohs(dh->ancount);
+
+  DNSName rrname;
+  uint16_t rrtype;
+  uint16_t rrclass;
+
+  size_t idx = 0;
+  /* consume qd */
+  for(; idx < qdcount; idx++) {
+    rrname = pr.getName();
+    rrtype = pr.get16BitInt();
+    rrclass = pr.get16BitInt();
+    (void) rrtype;
+    (void) rrclass;
+  }
+
+  /* parse AN */
+  for (idx = 0; idx < ancount; idx++) {
+    string blob;
+    struct dnsrecordheader ah;
+    rrname = pr.getName();
+    pr.getDnsrecordheader(ah);
+
+    if (ah.d_type == QType::TXT) {
+      string txt;
+      pr.xfrText(txt);
+
+      return txt;
+    }
+    else {
+      pr.xfrBlob(blob);
+    }
+  }
+
+  throw std::runtime_error("No TXT record in answer");
+}
+
+static std::string getSecPollStatus(const std::string& queriedName, int timeout=2)
+{
+  vector<uint8_t> packet;
+  DNSPacketWriter pw(packet, DNSName(queriedName), QType::TXT);
+#ifdef HAVE_LIBSODIUM
+  pw.getHeader()->id = randombytes_random() % 65536;
+#else
+  pw.getHeader()->id = random() % 65536;
+#endif
+  pw.getHeader()->rd = 1;
+
+  const auto& resolversForStub = getResolvers("/etc/resolv.conf");
+
+  for(const auto& dest : resolversForStub) {
+    Socket sock(dest.sin4.sin_family, SOCK_DGRAM);
+    sock.setNonBlocking();
+    sock.connect(dest);
+    sock.send(string(packet.begin(), packet.end()));
+
+    string reply;
+    int ret = waitForData(sock.getHandle(), timeout, 0);
+    if (ret < 0) {
+      if (g_verbose) {
+        warnlog("Error while waiting for the secpoll response from stub resolver %s: %d", dest.toString(), ret);
+      }
+      continue;
+    }
+    else if (ret == 0) {
+      if (g_verbose) {
+        warnlog("Timeout while waiting for the secpoll response from stub resolver %s", dest.toString());
+      }
+      continue;
+    }
+
+    try {
+      sock.read(reply);
+    }
+    catch(const std::exception& e) {
+      if (g_verbose) {
+        warnlog("Error while reading for the secpoll response from stub resolver %s: %s", dest.toString(), e.what());
+      }
+      continue;
+    }
+
+    if (reply.size() <= sizeof(struct dnsheader)) {
+      if (g_verbose) {
+        warnlog("Too short answer of size %d received from the secpoll stub resolver %s", reply.size(), dest.toString());
+      }
+      continue;
+    }
+
+    struct dnsheader d;
+    memcpy(&d, reply.c_str(), sizeof(d));
+    if (d.id != pw.getHeader()->id) {
+      if (g_verbose) {
+        warnlog("Invalid ID (%d / %d) received from the secpoll stub resolver %s", d.id, pw.getHeader()->id, dest.toString());
+      }
+      continue;
+    }
+
+    if (d.rcode != RCode::NoError) {
+      if (g_verbose) {
+        warnlog("Response code '%s' received from the secpoll stub resolver %s for '%s'", RCode::to_s(d.rcode), dest.toString(), queriedName);
+      }
+
+      /* no need to try another resolver if the domain does not exist */
+      if (d.rcode == RCode::NXDomain) {
+        throw std::runtime_error("Unable to get a valid Security Status update");
+      }
+      continue;
+    }
+
+    if (ntohs(d.qdcount) != 1 || ntohs(d.ancount) != 1) {
+      if (g_verbose) {
+        warnlog("Invalid answer (qdcount %d / ancount %d) received from the secpoll stub resolver %s", dest.toString());
+      }
+      continue;
+    }
+
+    return getFirstTXTAnswer(reply);
+  }
+
+  throw std::runtime_error("Unable to get a valid Security Status update");
+}
+
+static bool g_secPollDone{false};
+std::string g_secPollSuffix{"secpoll.powerdns.com."};
+time_t g_secPollInterval{3600};
+
+void doSecPoll(const std::string& suffix)
+{
+  if (suffix.empty()) {
+    return;
+  }
+
+  const std::string pkgv(PACKAGE_VERSION);
+  bool releaseVersion = pkgv.find("0.0.") != 0;
+
+  struct timeval now;
+  gettimeofday(&now, 0);
+
+  const std::string version = "dnsdist-" + std::string(VERSION);
+  std::string queriedName = version.substr(0, 63) + ".security-status." + suffix;
+
+  if (*queriedName.rbegin() != '.') {
+    queriedName += '.';
+  }
+
+  boost::replace_all(queriedName, "+", "_");
+  boost::replace_all(queriedName, "~", "_");
+
+  try {
+    std::string status = getSecPollStatus(queriedName);
+    pair<string, string> split = splitField(unquotify(status), ' ');
+
+    int securityStatus = std::stoi(split.first);
+    std::string securityMessage = split.second;
+
+    if(securityStatus == 1 && !g_secPollDone) {
+      warnlog("Polled security status of version %s at startup, no known issues reported: %s", std::string(VERSION), securityMessage);
+    }
+    if(securityStatus == 2) {
+      errlog("PowerDNS DNSDist Security Update Recommended: %s", securityMessage);
+    }
+    else if(securityStatus == 3) {
+      errlog("PowerDNS DNSDist Security Update Mandatory: %s", securityMessage);
+    }
+
+    g_secPollDone = true;
+    return;
+  }
+  catch(const std::exception& e) {
+    if (releaseVersion) {
+      warnlog("Error while retrieving the security update for version %s: %s", version, e.what());
+    }
+    else if (!g_secPollDone) {
+      infolog("Error while retrieving the security update for version %s: %s", version, e.what());
+    }
+  }
+
+  if (releaseVersion) {
+    warnlog("Could not retrieve security status update for '%s' on %s", pkgv, queriedName);
+  }
+  else if (!g_secPollDone) {
+    infolog("Not validating response for security status update, this is a non-release version.");
+
+    /* for non-released versions, there is no use sending the same message several times,
+       let's just accept that there will be no security polling for this exact version */
+    g_secPollDone = true;
+  }
+}
diff --git a/pdns/dnsdistdist/dnsdist-secpoll.hh b/pdns/dnsdistdist/dnsdist-secpoll.hh
new file mode 100644 (file)
index 0000000..c7d8869
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * 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
+
+extern std::string g_secPollSuffix;
+extern time_t g_secPollInterval;
+
+void doSecPoll(const std::string& suffix);
index 345a255410b22998045a5af7255627e7c8ee3cdf..df9ef5562437c5eb9104744150db1164c967eb76 100644 (file)
@@ -1006,3 +1006,35 @@ overriden using :func:`setPayloadSizeOnSelfGeneratedAnswers`.
   :rfc:`RFC 6891 <6891#section-6.2.5>`, values lower than 512 will be treated as equal to 512.
 
   :param int payloadSize: The responder's maximum UDP payload size, in bytes. Default is 1500.
+
+Security Polling
+~~~~~~~~~~~~~~~~
+
+PowerDNS products can poll the security status of their respective versions. This polling, naturally,
+happens over DNS. If the result is that a given version has a security problem, the software will
+report this at level ‘Error’ during startup, and repeatedly during operations, every
+:func:`setSecurityPollInterval` seconds.
+
+By default, security polling happens on the domain ‘secpoll.powerdns.com’, but this can be changed with
+the :func:`setSecurityPollSuffix` function. If this setting is made empty, no polling will take place.
+Organizations wanting to host their own security zones can do so by changing this setting to a domain name
+under their control.
+
+To enable distributors of PowerDNS to signal that they have backported versions, the PACKAGEVERSION
+compilation-time macro can be used to set a distributor suffix.
+
+.. function:: setSecurityPollInterval(interval)
+
+  .. versionadded:: 1.3.3
+
+  Set the interval, in seconds, between two security pollings.
+
+  :param int interval: The interval, in seconds, between two pollings. Default is 3600.
+
+.. function:: setSecurityPollSuffix(suffix)
+
+  .. versionadded:: 1.3.3
+
+  Domain name from which to query security update notifications. Setting this to an empty string disables secpoll.
+
+  :param string suffix: The suffix to use, default is 'secpoll.powerdns.com.'.
index 9e671aab85c0703b9fa5cbdc6398f997c765736f..196a4bcf4822bd309f069cc0ccb5c49108647503 100644 (file)
@@ -1413,3 +1413,39 @@ int mapThreadToCPUList(pthread_t tid, const std::set<int>& cpus)
   return ENOSYS;
 #endif /* HAVE_PTHREAD_SETAFFINITY_NP */
 }
+
+std::vector<ComboAddress> getResolvers(const std::string& resolvConfPath)
+{
+  std::vector<ComboAddress> results;
+
+  ifstream ifs(resolvConfPath);
+  if (!ifs) {
+    return results;
+  }
+
+  string line;
+  while(std::getline(ifs, line)) {
+    boost::trim_right_if(line, is_any_of(" \r\n\x1a"));
+    boost::trim_left(line); // leading spaces, let's be nice
+
+    string::size_type tpos = line.find_first_of(";#");
+    if (tpos != string::npos) {
+      line.resize(tpos);
+    }
+
+    if (boost::starts_with(line, "nameserver ") || boost::starts_with(line, "nameserver\t")) {
+      vector<string> parts;
+      stringtok(parts, line, " \t,"); // be REALLY nice
+      for(vector<string>::const_iterator iter = parts.begin() + 1; iter != parts.end(); ++iter) {
+        try {
+          results.emplace_back(*iter, 53);
+        }
+        catch(...)
+        {
+        }
+      }
+    }
+  }
+
+  return results;
+}
index 0f17a3a60987c07211632d2af45cbd95a03f8ab2..fcf3c6a51dabe77557ee3b6a8fce01febac1f6e2 100644 (file)
@@ -598,3 +598,5 @@ unsigned int pdns_stou(const std::string& str, size_t * idx = 0, int base = 10);
 
 bool isSettingThreadCPUAffinitySupported();
 int mapThreadToCPUList(pthread_t tid, const std::set<int>& cpus);
+
+std::vector<ComboAddress> getResolvers(const std::string& resolvConfPath);
index 5d310043d330b4973560962ae03fc695a41b6ef9..3068842bfa84b8a61d87ac92facbed27e799869b 100644 (file)
@@ -53,35 +53,15 @@ static void parseLocalResolvConf_locked(const time_t& now)
 
   if (stat(LOCAL_RESOLV_CONF_PATH, &st) != -1) {
     if (st.st_mtime != s_localResolvConfMtime) {
-      ifstream ifs(LOCAL_RESOLV_CONF_PATH);
-      string line;
+      std::vector<ComboAddress> resolvers = getResolvers(LOCAL_RESOLV_CONF_PATH);
 
       s_localResolvConfMtime = st.st_mtime;
-      if(!ifs)
-        return;
 
-      s_resolversForStub.clear();
-      while(std::getline(ifs, line)) {
-        boost::trim_right_if(line, is_any_of(" \r\n\x1a"));
-        boost::trim_left(line); // leading spaces, let's be nice
-
-        string::size_type tpos = line.find_first_of(";#");
-        if(tpos != string::npos)
-          line.resize(tpos);
-
-        if(boost::starts_with(line, "nameserver ") || boost::starts_with(line, "nameserver\t")) {
-          vector<string> parts;
-          stringtok(parts, line, " \t,"); // be REALLY nice
-          for(vector<string>::const_iterator iter = parts.begin()+1; iter != parts.end(); ++iter) {
-            try {
-              s_resolversForStub.push_back(ComboAddress(*iter, 53));
-            }
-            catch(...)
-            {
-            }
-          }
-        }
+      if (resolvers.empty()) {
+        return;
       }
+
+      s_resolversForStub = std::move(resolvers);
     }
   }
 }