From: Remi Gacogne Date: Tue, 30 Oct 2018 10:11:49 +0000 (+0100) Subject: dnsdist: Add security polling X-Git-Tag: dnsdist-1.3.3~13^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5d4e1ef88ee5d3cab86c6c281be2a576964a9343;p=thirdparty%2Fpdns.git dnsdist: Add security polling --- diff --git a/pdns/dnsdist-console.cc b/pdns/dnsdist-console.cc index d0195c9ecc..c9bdcb0c73 100644 --- a/pdns/dnsdist-console.cc +++ b/pdns/dnsdist-console.cc @@ -435,6 +435,8 @@ const std::vector 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" }, diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 40abb4bce8..cc169c3dc2 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -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>> certFiles, boost::variant>> keyFiles, boost::optional vars) { if (client) return; diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index f5e9f088ce..1f19cb56b1 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -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"); diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 8341b1bd49..f2d79be915 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -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 index 0000000000..49b9559567 --- /dev/null +++ b/pdns/dnsdistdist/dnsdist-secpoll.cc @@ -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 +#include + +#ifdef HAVE_LIBSODIUM +#include +#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(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 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 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 index 0000000000..c7d886917e --- /dev/null +++ b/pdns/dnsdistdist/dnsdist-secpoll.hh @@ -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); diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 345a255410..df9ef55624 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -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.'. diff --git a/pdns/misc.cc b/pdns/misc.cc index 9e671aab85..196a4bcf48 100644 --- a/pdns/misc.cc +++ b/pdns/misc.cc @@ -1413,3 +1413,39 @@ int mapThreadToCPUList(pthread_t tid, const std::set& cpus) return ENOSYS; #endif /* HAVE_PTHREAD_SETAFFINITY_NP */ } + +std::vector getResolvers(const std::string& resolvConfPath) +{ + std::vector 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 parts; + stringtok(parts, line, " \t,"); // be REALLY nice + for(vector::const_iterator iter = parts.begin() + 1; iter != parts.end(); ++iter) { + try { + results.emplace_back(*iter, 53); + } + catch(...) + { + } + } + } + } + + return results; +} diff --git a/pdns/misc.hh b/pdns/misc.hh index 0f17a3a609..fcf3c6a51d 100644 --- a/pdns/misc.hh +++ b/pdns/misc.hh @@ -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& cpus); + +std::vector getResolvers(const std::string& resolvConfPath); diff --git a/pdns/stubresolver.cc b/pdns/stubresolver.cc index 5d310043d3..3068842bfa 100644 --- a/pdns/stubresolver.cc +++ b/pdns/stubresolver.cc @@ -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 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 parts; - stringtok(parts, line, " \t,"); // be REALLY nice - for(vector::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); } } }