/* * 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 "dnsdist-secpoll.hh" #ifndef DISABLE_SECPOLL #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.hh" #include "dnsdist-metrics.hh" #include "dnsdist-random.hh" #ifndef PACKAGEVERSION #define PACKAGEVERSION PACKAGE_VERSION #endif 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 dnsheader_aligned dh(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) { const DNSName sentName(queriedName); std::vector packet; DNSPacketWriter pw(packet, sentName, QType::TXT); pw.getHeader()->id = dnsdist::getRandomDNSID(); 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", ntohs(d.qdcount), ntohs(d.ancount), dest.toString()); } continue; } uint16_t receivedType; uint16_t receivedClass; DNSName receivedName(reply.c_str(), reply.size(), sizeof(dnsheader), false, &receivedType, &receivedClass); if (receivedName != sentName || receivedType != QType::TXT || receivedClass != QClass::IN) { if (g_verbose) { warnlog("Invalid answer, either the qname (%s / %s), qtype (%s / %s) or qclass (%s / %s) does not match, received from the secpoll stub resolver %s", receivedName, sentName, QType(receivedType).toString(), QType(QType::TXT).toString(), QClass(receivedClass).toString(), QClass::IN.toString(), 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(PACKAGEVERSION); bool releaseVersion = std::count(pkgv.begin(), pkgv.end(), '.') == 2; const std::string version = "dnsdist-" + pkgv; 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) { infolog("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); } dnsdist::metrics::g_stats.securityStatus = securityStatus; 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("Failed to 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; } } #endif /* DISABLE_SECPOLL */