]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Dynamic discovery and upgrade of backends
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 7 Jan 2022 16:40:50 +0000 (17:40 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Tue, 22 Feb 2022 09:00:56 +0000 (10:00 +0100)
pdns/dnsdist-lua.cc
pdns/dnsdist-protocols.cc
pdns/dnsdist-protocols.hh
pdns/dnsdist.cc
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-discovery.cc [new file with mode: 0644]
pdns/dnsdistdist/dnsdist-discovery.hh [new file with mode: 0644]
pdns/dnsdistdist/dnsdist-secpoll.cc
pdns/dnsdistdist/docs/reference/config.rst

index 755e146e66bffdcf4480ebf2d67a65f4a4b56e84..6ecdea0ec5287d7c8b71a7775a1ce9979f8501dd 100644 (file)
@@ -35,6 +35,7 @@
 #include "dnsdist-carbon.hh"
 #include "dnsdist-console.hh"
 #include "dnsdist-dynblocks.hh"
+#include "dnsdist-discovery.hh"
 #include "dnsdist-ecs.hh"
 #include "dnsdist-healthchecks.hh"
 #include "dnsdist-lua.hh"
@@ -312,8 +313,8 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
                          setLuaSideEffect();
 
                          newserver_t vars;
+                         DownstreamState::Config config;
 
-                         ComboAddress serverAddr;
                          std::string serverAddressStr;
                          if (auto addrStr = boost::get<string>(&pvars)) {
                            serverAddressStr = *addrStr;
@@ -327,7 +328,7 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
                          }
 
                          try {
-                           serverAddr = ComboAddress(serverAddressStr, 53);
+                           config.remote = ComboAddress(serverAddressStr, 53);
                          }
                          catch (const PDNSException& e) {
                            g_outputBuffer = "Error creating new server: " + string(e.reason);
@@ -340,14 +341,12 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
                            return std::shared_ptr<DownstreamState>();
                          }
 
-                         if (IsAnyAddress(serverAddr)) {
+                         if (IsAnyAddress(config.remote)) {
                            g_outputBuffer = "Error creating new server: invalid address for a downstream server.";
                            errlog("Error creating new server: %s is not a valid address for a downstream server", serverAddressStr);
                            return std::shared_ptr<DownstreamState>();
                          }
 
-                         DownstreamState::Config config;
-
                          if (vars.count("source")) {
                            /* handle source in the following forms:
                               - v4 address ("192.0.2.1")
@@ -601,10 +600,47 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
                            }
                          }
 
+                         bool autoUpgrade = false;
+                         bool keepAfterUpgrade = false;
+                         uint32_t upgradeInterval = 3600;
+                         uint16_t upgradeDoHKey = 7;
+                         std::string upgradePool;
+
+                         if (vars.count("autoUpgrade") && boost::get<bool>(vars.at("autoUpgrade"))) {
+                           autoUpgrade = true;
+
+                           if (vars.count("autoUpgradeInterval")) {
+                             try {
+                               upgradeInterval = static_cast<uint32_t>(std::stoul(boost::get<string>(vars.at("autoUpgradeInterval"))));
+                             }
+                             catch (const std::exception& e) {
+                               warnlog("Error parsing 'autoUpgradeInterval' value: %s", e.what());
+                             }
+                           }
+                           if (vars.count("autoUpgradeKeep")) {
+                             keepAfterUpgrade = boost::get<bool>(vars.at("autoUpgradeKeep"));
+                           }
+                           if (vars.count("autoUpgradePool")) {
+                             upgradePool = boost::get<string>(vars.at("autoUpgradePool"));
+                           }
+                           if (vars.count("autoUpgradeDoHKey")) {
+                             try {
+                               upgradeDoHKey = static_cast<uint16_t>(std::stoul(boost::get<string>(vars.at("autoUpgradeDoHKey"))));
+                             }
+                             catch (const std::exception& e) {
+                               warnlog("Error parsing 'autoUpgradeDoHKey' value: %s", e.what());
+                             }
+                           }
+                         }
+
                          // create but don't connect the socket in client or check-config modes
                          auto ret = std::make_shared<DownstreamState>(std::move(config), std::move(tlsCtx), !(client || configCheck));
                          if (!(client || configCheck)) {
-                           infolog("Added downstream server %s", serverAddr.toStringWithPort());
+                           infolog("Added downstream server %s", config.remote.toStringWithPort());
+                         }
+
+                         if (autoUpgrade) {
+                           dnsdist::ServiceDiscovery::addUpgradeableServer(ret, upgradeInterval, upgradePool, upgradeDoHKey, keepAfterUpgrade);
                          }
 
                          /* this needs to be done _AFTER_ the order has been set,
@@ -622,7 +658,7 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
 
                          if (ret->connected) {
                            if (g_launchWork) {
-                             g_launchWork->push_back([&ret]() {
+                             g_launchWork->push_back([ret]() {
                                ret->start();
                              });
                            }
index 89b65ecac53c223ccb6e2464e6dcc914bc5740be..8af8fe836891a5a58af65e61cca04f744a1f0a5f 100644 (file)
@@ -67,6 +67,11 @@ bool Protocol::operator==(Protocol::typeenum type) const
   return d_protocol == type;
 }
 
+bool Protocol::operator!=(Protocol::typeenum type) const
+{
+  return d_protocol != type;
+}
+
 const std::string& Protocol::toString() const
 {
   return names.at(static_cast<uint8_t>(d_protocol));
index a90e6e0c5602a18fe9c30b7d2227e6bdf22c2215..ddd74e3688e5856282386ca08a188ca424671c03 100644 (file)
@@ -43,6 +43,7 @@ public:
   explicit Protocol(const std::string& protocol);
 
   bool operator==(typeenum) const;
+  bool operator!=(typeenum) const;
 
   const std::string& toString() const;
   const std::string& toPrettyString() const;
index 5aa8bc81c7427ef5eadced832cedec4e6bdb43ef..1e713fd297d222dc4f6796818ab00b88e63f2159 100644 (file)
@@ -48,6 +48,7 @@
 #include "dnsdist-cache.hh"
 #include "dnsdist-carbon.hh"
 #include "dnsdist-console.hh"
+#include "dnsdist-discovery.hh"
 #include "dnsdist-dynblocks.hh"
 #include "dnsdist-ecs.hh"
 #include "dnsdist-healthchecks.hh"
@@ -2621,6 +2622,8 @@ int main(int argc, char** argv)
       }
     }
 
+    dnsdist::ServiceDiscovery::run();
+
 #ifndef DISABLE_CARBON
     thread carbonthread(carbonDumpThread);
     carbonthread.detach();
index fa5d8aa5b19f56f7e1579d32c352ce7ace8d2a72..2a15da9086e34b04480d55d577bb89969e37ce5e 100644 (file)
@@ -138,6 +138,7 @@ dnsdist_SOURCES = \
        dnsdist-cache.cc dnsdist-cache.hh \
        dnsdist-carbon.cc dnsdist-carbon.hh \
        dnsdist-console.cc dnsdist-console.hh \
+       dnsdist-discovery.cc dnsdist-discovery.hh \
        dnsdist-dnscrypt.cc \
        dnsdist-dynblocks.cc dnsdist-dynblocks.hh \
        dnsdist-dynbpf.cc dnsdist-dynbpf.hh \
diff --git a/pdns/dnsdistdist/dnsdist-discovery.cc b/pdns/dnsdistdist/dnsdist-discovery.cc
new file mode 100644 (file)
index 0000000..d1e0618
--- /dev/null
@@ -0,0 +1,504 @@
+/*
+ * 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 "dnsdist-discovery.hh"
+#include "dnsdist.hh"
+#include "dnsdist-random.hh"
+#include "dnsparser.hh"
+#include "dolog.hh"
+#include "sstuff.hh"
+
+namespace dnsdist {
+
+const DNSName ServiceDiscovery::s_discoveryDomain{"_dns.resolver.arpa."};
+const QType ServiceDiscovery::s_discoveryType{QType::SVCB};
+const uint16_t ServiceDiscovery::s_defaultDoHSVCKey{7};
+
+bool ServiceDiscovery::addUpgradeableServer(std::shared_ptr<DownstreamState>& server, uint32_t interval, std::string poolAfterUpgrade, uint16_t dohSVCKey, bool keepAfterUpgrade)
+{
+  s_upgradeableBackends.lock()->emplace_back(UpgradeableBackend{server, poolAfterUpgrade, 0, interval, dohSVCKey, keepAfterUpgrade});
+  return true;
+}
+
+struct DesignatedResolvers
+{
+  DNSName target;
+  std::set<SvcParam> params;
+  std::vector<ComboAddress> hints;
+};
+
+static bool parseSVCParams(const PacketBuffer& answer, std::map<uint16_t, DesignatedResolvers>& resolvers)
+{
+  if (answer.size() <= sizeof(struct dnsheader)) {
+    throw std::runtime_error("Looking for SVC records in a packet smaller than a DNS header");
+  }
+
+  std::map<DNSName, std::vector<ComboAddress>> hints;
+  const struct dnsheader* dh = reinterpret_cast<const struct dnsheader*>(answer.data());
+  PacketReader pr(pdns_string_view(reinterpret_cast<const char*>(answer.data()), answer.size()));
+  uint16_t qdcount = ntohs(dh->qdcount);
+  uint16_t ancount = ntohs(dh->ancount);
+  uint16_t nscount = ntohs(dh->nscount);
+  uint16_t arcount = ntohs(dh->arcount);
+
+  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::SVCB) {
+      auto prio = pr.get16BitInt();
+      auto target = pr.getName();
+      std::set<SvcParam> params;
+
+      if (prio != 0) {
+        pr.xfrSvcParamKeyVals(params);
+      }
+
+      resolvers[prio] = { std::move(target), std::move(params), {} };
+    }
+    else {
+      pr.xfrBlob(blob);
+    }
+  }
+
+  /* parse NS */
+  for (idx = 0; idx < nscount; idx++) {
+    string blob;
+    struct dnsrecordheader ah;
+    rrname = pr.getName();
+    pr.getDnsrecordheader(ah);
+
+    pr.xfrBlob(blob);
+  }
+
+  /* parse additional for hints */
+  for (idx = 0; idx < arcount; idx++) {
+    string blob;
+    struct dnsrecordheader ah;
+    rrname = pr.getName();
+    pr.getDnsrecordheader(ah);
+
+    if (ah.d_type == QType::A) {
+      ComboAddress addr;
+      pr.xfrCAWithoutPort(4, addr);
+      hints[rrname].push_back(addr);
+    }
+    else if (ah.d_type == QType::AAAA) {
+      ComboAddress addr;
+      pr.xfrCAWithoutPort(6, addr);
+      hints[rrname].push_back(addr);
+    }
+    else {
+      pr.xfrBlob(blob);
+    }
+  }
+
+  for (auto& resolver : resolvers) {
+    auto hint = hints.find(resolver.second.target);
+    if (hint != hints.end()) {
+      resolver.second.hints = hint->second;
+    }
+  }
+
+  return !resolvers.empty();
+}
+
+static bool handleSVCResult(const PacketBuffer& answer, const ComboAddress& existingAddr, uint16_t dohSVCKey, ServiceDiscovery::DiscoveredResolverConfig& config)
+{
+  std::map<uint16_t, DesignatedResolvers> resolvers;
+  if (!parseSVCParams(answer, resolvers)) {
+    return false;
+  }
+
+  for (const auto& [priority, resolver] : resolvers) {
+    /* do not compare the ports */
+    std::set<ComboAddress, ComboAddress::addressOnlyLessThan> tentativeAddresses;
+    ServiceDiscovery::DiscoveredResolverConfig tempConfig;
+    tempConfig.d_addr.sin4.sin_family = 0;
+
+    for (const auto& param : resolver.params) {
+      if (param.getKey() == SvcParam::alpn) {
+        auto alpns = param.getALPN();
+        for (const auto& alpn : alpns) {
+          if (alpn == "dot") {
+            tempConfig.d_protocol = dnsdist::Protocol::DoT;
+            if (tempConfig.d_port == 0) {
+              tempConfig.d_port = 853;
+            }
+          }
+          else if (alpn == "h2") {
+            tempConfig.d_protocol = dnsdist::Protocol::DoH;
+            if (tempConfig.d_port == 0) {
+              tempConfig.d_port = 443;
+            }
+          }
+        }
+      }
+      else if (param.getKey() == SvcParam::port) {
+        tempConfig.d_port = param.getPort();
+      }
+      else if (param.getKey() == SvcParam::ipv4hint || param.getKey() == SvcParam::ipv6hint) {
+        if (tempConfig.d_addr.sin4.sin_family == 0) {
+          auto hints = param.getIPHints();
+          for (const auto& hint : hints) {
+            tentativeAddresses.insert(hint);
+          }
+        }
+      }
+      else if (dohSVCKey != 0 && param.getKey() == dohSVCKey) {
+        tempConfig.d_dohPath = param.getValue();
+        auto expression = tempConfig.d_dohPath.find('{');
+        if (expression != std::string::npos) {
+          /* nuke the {?dns} expression, if any, as we only support POST anyway */
+          tempConfig.d_dohPath.resize(expression);
+        }
+      }
+    }
+
+    if (tempConfig.d_protocol == dnsdist::Protocol::DoH){
+#ifndef HAVE_DNS_OVER_HTTPS
+      continue;
+#endif
+      if (tempConfig.d_dohPath.empty()) {
+        continue;
+      }
+    }
+    else if (tempConfig.d_protocol == dnsdist::Protocol::DoT) {
+#ifndef HAVE_DNS_OVER_TLS
+      continue;
+#endif
+    }
+    else {
+      continue;
+    }
+
+    /* we have a config that we can use! */
+
+    for (const auto& hint : resolver.hints) {
+      tentativeAddresses.insert(hint);
+    }
+
+    /* we prefer the address we already know, whenever possible */
+    if (tentativeAddresses.count(existingAddr) != 0) {
+      tempConfig.d_addr = existingAddr;
+    }
+    else {
+      tempConfig.d_addr = *tentativeAddresses.begin();
+    }
+
+    tempConfig.d_subjectName = resolver.target.toStringNoDot();
+    tempConfig.d_addr.sin4.sin_port = tempConfig.d_port;
+
+    config = tempConfig;
+    return true;
+  }
+
+  return false;
+}
+
+bool ServiceDiscovery::getDiscoveredConfig(const UpgradeableBackend& upgradeableBackend, ServiceDiscovery::DiscoveredResolverConfig& config)
+{
+  const auto& backend = upgradeableBackend.d_ds;
+  const auto& addr = backend->d_config.remote;
+  try {
+    auto id = dnsdist::getRandomDNSID();
+    PacketBuffer packet;
+    GenericDNSPacketWriter pw(packet, s_discoveryDomain, s_discoveryType);
+    pw.getHeader()->id = id;
+    pw.getHeader()->rd = 1;
+    pw.addOpt(4096, 0, 0);
+
+    uint16_t querySize = static_cast<uint16_t>(packet.size());
+    const uint8_t sizeBytes[] = { static_cast<uint8_t>(querySize / 256), static_cast<uint8_t>(querySize % 256) };
+    packet.insert(packet.begin(), sizeBytes, sizeBytes + 2);
+
+    Socket sock(addr.sin4.sin_family, SOCK_STREAM);
+    sock.setNonBlocking();
+    if (!IsAnyAddress(backend->d_config.sourceAddr)) {
+      sock.setReuseAddr();
+#ifdef IP_BIND_ADDRESS_NO_PORT
+      if (backend->d_config.ipBindAddrNoPort) {
+        SSetsockopt(sock.getHandle(), SOL_IP, IP_BIND_ADDRESS_NO_PORT, 1);
+      }
+#endif
+
+      if (!backend->d_config.sourceItfName.empty()) {
+#ifdef SO_BINDTODEVICE
+        setsockopt(sock.getHandle(), SOL_SOCKET, SO_BINDTODEVICE, backend->d_config.sourceItfName.c_str(), backend->d_config.sourceItfName.length());
+#endif
+      }
+      sock.bind(backend->d_config.sourceAddr);
+    }
+    sock.connect(addr, backend->d_config.tcpConnectTimeout);
+
+    sock.writenWithTimeout(reinterpret_cast<const char*>(packet.data()), packet.size(), backend->d_config.tcpSendTimeout);
+
+    uint16_t responseSize = 0;
+    auto got = sock.readWithTimeout(reinterpret_cast<char*>(&responseSize), sizeof(responseSize), backend->d_config.tcpRecvTimeout);
+    if (got < 0 || static_cast<size_t>(got) != sizeof(responseSize)) {
+      if (g_verbose) {
+        warnlog("Error while waiting for the ADD upgrade response from backend %s: %d", addr.toString(), got);
+      }
+      return false;
+    }
+
+    packet.resize(ntohs(responseSize));
+
+    got = sock.readWithTimeout(reinterpret_cast<char *>(packet.data()), packet.size(), backend->d_config.tcpRecvTimeout);
+    if (got < 0 || static_cast<size_t>(got) != packet.size()) {
+      if (g_verbose) {
+        warnlog("Error while waiting for the ADD upgrade response from backend %s: %d", addr.toString(), got);
+      }
+      return false;
+    }
+
+    if (packet.size() <= sizeof(struct dnsheader)) {
+      if (g_verbose) {
+        warnlog("Too short answer of size %d received from the backend %s", packet.size(), addr.toString());
+      }
+      return false;
+    }
+
+    struct dnsheader d;
+    memcpy(&d, packet.data(), sizeof(d));
+    if (d.id != id) {
+      if (g_verbose) {
+        warnlog("Invalid ID (%d / %d) received from the backend %s", d.id, id, addr.toString());
+      }
+      return false;
+    }
+
+    if (d.rcode != RCode::NoError) {
+      if (g_verbose) {
+        warnlog("Response code '%s' received from the backend %s for '%s'", RCode::to_s(d.rcode), addr.toString(), s_discoveryDomain);
+      }
+
+      return false;
+    }
+
+    if (ntohs(d.qdcount) != 1) {
+      if (g_verbose) {
+        warnlog("Invalid answer (qdcount %d) received from the backend %s", ntohs(d.qdcount), addr.toString());
+      }
+      return false;
+    }
+
+    uint16_t receivedType;
+    uint16_t receivedClass;
+    DNSName receivedName(reinterpret_cast<const char*>(packet.data()), packet.size(), sizeof(dnsheader), false, &receivedType, &receivedClass);
+
+    if (receivedName != s_discoveryDomain || receivedType != s_discoveryType || 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 backend %s", receivedName, s_discoveryDomain, QType(receivedType).toString(), s_discoveryType.toString(), QClass(receivedClass).toString(), QClass::IN.toString(), addr.toString());
+      }
+      return false;
+    }
+
+    return handleSVCResult(packet, addr, upgradeableBackend.d_dohKey, config);
+  }
+  catch (const std::exception& e) {
+    errlog("Error while trying to discover backend upgrade for %s: %s", addr.toStringWithPort(), e.what());
+  }
+  catch (...) {
+    errlog("Error while trying to discover backend upgrade for %s", addr.toStringWithPort());
+  }
+
+  return false;
+}
+
+bool ServiceDiscovery::tryToUpgradeBackend(const UpgradeableBackend& backend)
+{
+  ServiceDiscovery::DiscoveredResolverConfig discoveredConfig;
+
+  if (!ServiceDiscovery::getDiscoveredConfig(backend, discoveredConfig)) {
+    return false;
+  }
+
+  if (discoveredConfig.d_protocol != dnsdist::Protocol::DoT && discoveredConfig.d_protocol != dnsdist::Protocol::DoH) {
+    return false;
+  }
+
+  DownstreamState::Config config(backend.d_ds->d_config);
+  config.remote = discoveredConfig.d_addr;
+  if (discoveredConfig.d_port != 0) {
+    config.remote.setPort(discoveredConfig.d_port);
+  }
+  else {
+    if (discoveredConfig.d_protocol == dnsdist::Protocol::DoT) {
+      config.remote.setPort(853);
+    }
+    else if (discoveredConfig.d_protocol == dnsdist::Protocol::DoH) {
+      config.remote.setPort(443);
+    }
+  }
+
+  ComboAddress::addressOnlyEqual comparator;
+  config.d_dohPath = discoveredConfig.d_dohPath;
+  if (comparator(config.remote, backend.d_ds->d_config.remote)) {
+    /* same address, we can used the supplied name for validation */
+    config.d_tlsSubjectName = discoveredConfig.d_subjectName;
+  }
+  else {
+    /* different name, and draft-ietf-add-ddr-04 states that:
+       "In order to be considered a verified Designated Resolver, the TLS
+       certificate presented by the Designated Resolver MUST contain the IP
+       address of the designating Unencrypted Resolver in a subjectAltName
+       extension."
+    */
+    config.d_tlsSubjectName = backend.d_ds->d_config.remote.toString();
+  }
+
+  if (!backend.d_poolAfterUpgrade.empty()) {
+    config.pools.clear();
+    config.pools.insert(backend.d_poolAfterUpgrade);
+  }
+
+  try {
+    /* create new backend, put it into the right pool(s) */
+    TLSContextParameters tlsParams;
+    auto tlsCtx = getTLSContext(tlsParams);
+    auto newServer = std::make_shared<DownstreamState>(std::move(config), std::move(tlsCtx), true);
+
+    infolog("Added automatically upgraded server %s", newServer->getNameWithAddr());
+
+    auto localPools = g_pools.getCopy();
+    if (!newServer->d_config.pools.empty()) {
+      for (const auto& poolName : newServer->d_config.pools) {
+        addServerToPool(localPools, poolName, newServer);
+      }
+    }
+    else {
+      addServerToPool(localPools, "", newServer);
+    }
+    g_pools.setState(localPools);
+
+    newServer->start();
+
+    auto states = g_dstates.getCopy();
+    states.push_back(newServer);
+    /* remove the existing backend if needed */
+    if (!backend.keepAfterUpgrade) {
+      for (auto it = states.begin(); it != states.end(); ++it) {
+        if (*it == backend.d_ds) {
+          states.erase(it);
+          break;
+        }
+      }
+    }
+
+    std::stable_sort(states.begin(), states.end(), [](const decltype(newServer)& a, const decltype(newServer)& b) {
+      return a->d_config.order < b->d_config.order;
+    });
+    g_dstates.setState(states);
+
+    return true;
+  }
+  catch (const std::exception& e) {
+    warnlog("Error when trying to upgrade a discovered backend: %s", e.what());
+  }
+
+  return false;
+}
+
+void ServiceDiscovery::worker()
+{
+  while (true) {
+    time_t now = time(nullptr);
+
+    auto upgradeables = *(s_upgradeableBackends.lock());
+    std::set<std::shared_ptr<DownstreamState>> upgradedBackends;
+
+    for (auto backendIt = upgradeables.begin(); backendIt != upgradeables.end(); ) {
+      try {
+        auto& backend = *backendIt;
+        if (backend.d_nextCheck > now) {
+          ++backendIt;
+          continue;
+        }
+
+        auto upgraded = tryToUpgradeBackend(backend);
+        if (upgraded) {
+          upgradedBackends.insert(backend.d_ds);
+          backendIt = upgradeables.erase(backendIt);
+        }
+        else {
+          backend.d_nextCheck = now + backend.d_interval;
+          ++backendIt;
+        }
+      }
+      catch (const std::exception& e) {
+        vinfolog("Exception in the Service Discovery thread: %s", e.what());
+      }
+      catch (...) {
+        vinfolog("Exception in the Service Discovery thread");
+      }
+    }
+
+
+    {
+      auto backends = s_upgradeableBackends.lock();
+      for (auto it = backends->begin(); it != backends->end(); ) {
+        if (upgradedBackends.count(it->d_ds) != 0) {
+          it = backends->erase(it);
+        }
+        else {
+          ++it;
+        }
+      }
+    }
+
+    /* we could sleep until the next check but a new backend
+       could be added in the meantime, so let's just check every
+       minute if we have something to do */
+    sleep(60);
+  }
+}
+
+bool ServiceDiscovery::run()
+{
+  s_thread = std::thread(&ServiceDiscovery::worker);
+  s_thread.detach();
+
+  return true;
+}
+
+LockGuarded<std::vector<ServiceDiscovery::UpgradeableBackend>> ServiceDiscovery::s_upgradeableBackends;
+std::thread ServiceDiscovery::s_thread;
+}
diff --git a/pdns/dnsdistdist/dnsdist-discovery.hh b/pdns/dnsdistdist/dnsdist-discovery.hh
new file mode 100644 (file)
index 0000000..a2591c3
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * 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
+#include <vector>
+#include <thread>
+
+#include "dnsname.hh"
+#include "dnsdist-protocols.hh"
+#include "iputils.hh"
+#include "lock.hh"
+
+struct DownstreamState;
+
+namespace dnsdist {
+
+class ServiceDiscovery
+{
+public:
+
+  static bool addUpgradeableServer(std::shared_ptr<DownstreamState>& server, uint32_t interval, std::string poolAfterUpgrade, uint16_t dohSVCKey, bool keepAfterUpgrade);
+
+  /* starts a background thread if needed */
+  static bool run();
+
+  struct DiscoveredResolverConfig
+  {
+    ComboAddress d_addr;
+    std::string d_subjectName;
+    std::string d_dohPath;
+    uint16_t d_port{0};
+    dnsdist::Protocol d_protocol;
+  };
+
+private:
+  static const DNSName s_discoveryDomain;
+  static const QType s_discoveryType;
+  static const uint16_t s_defaultDoHSVCKey;
+
+  struct UpgradeableBackend
+  {
+    std::shared_ptr<DownstreamState> d_ds;
+    std::string d_poolAfterUpgrade;
+    time_t d_nextCheck;
+    uint32_t d_interval;
+    uint16_t d_dohKey;
+    bool keepAfterUpgrade;
+  };
+
+  static bool getDiscoveredConfig(const UpgradeableBackend& backend, DiscoveredResolverConfig& config);
+  static bool tryToUpgradeBackend(const UpgradeableBackend& backend);
+
+  static void worker();
+
+  static LockGuarded<std::vector<UpgradeableBackend>> s_upgradeableBackends;
+  static std::thread s_thread;
+};
+
+}
index 4f5a9994bc39d247c67c4a5e1891da5c3ef4f8d7..eb38d356c65ec359c6f123711d3597dd115bae13 100644 (file)
@@ -90,7 +90,7 @@ static std::string getFirstTXTAnswer(const std::string& answer)
 
 static std::string getSecPollStatus(const std::string& queriedName, int timeout=2)
 {
-  const DNSName& sentName = DNSName(queriedName);
+  const DNSName sentName(queriedName);
   std::vector<uint8_t> packet;
   DNSPacketWriter pw(packet, sentName, QType::TXT);
   pw.getHeader()->id = dnsdist::getRandomDNSID();
index b7c4b8974937fa2b35e430d807011e55d29124f7..62b7b3cf85b42de97158561baae55b1fbbb82141 100644 (file)
@@ -532,6 +532,9 @@ Servers
   .. versionchanged:: 1.7.0
     Added ``addXForwardedHeaders``, ``caStore``, ``checkTCP``, ``ciphers``, ``ciphers13``, ``dohPath``, ``enableRenegotiation``, ``releaseBuffers``, ``subjectName``, ``tcpOnly``, ``tls`` and ``validateCertificates`` to server_table.
 
+  .. versionchanged:: 1.8.0
+    Added ``autoUpgrade``, ``autoUpgradeDoHKey``, ``autoUpgradeInterval``, ``autoUpgradeKeep`` and ``autoUpgradePool`` to server_table.
+
   Add a new backend server. Call this function with either a string::
 
     newServer(
@@ -589,7 +592,12 @@ Servers
       dohPath=STRING,           -- Enable DNS over HTTPS communication for this backend, using POST queries to the HTTP host supplied as ``subjectName`` and the HTTP path supplied in this parameter.
       addXForwardedHeaders=BOOL,-- Whether to add X-Forwarded-For, X-Forwarded-Port and X-Forwarded-Proto headers to a DNS over HTTPS backend.
       releaseBuffers=BOOL,      -- Whether OpenSSL should release its I/O buffers when a connection goes idle, saving roughly 35 kB of memory per connection. Default to true.
-      enableRenegotiation=BOOL  -- Whether secure TLS renegotiation should be enabled. Disabled by default since it increases the attack surface and is seldom used for DNS.
+      enableRenegotiation=BOOL, -- Whether secure TLS renegotiation should be enabled. Disabled by default since it increases the attack surface and is seldom used for DNS.
+      autoUpgrade=BOOL,         -- Whether to use the 'Discovery of Designated Resolvers' mechanism to automatically upgrade an Do53 backend to DoT or DoH. Default to false.
+      autoUpgradeInterval=NUM,  -- If ``autoUpgrade`` is set, how often to check if an upgrade is available, in seconds. Default is 3600 seconds.
+      autoUpgradeKeep=BOOL,     -- If ``autoUpgrade`` is set, whether to keep the existing Do53 backend around after an upgrade. Default is false which means the Do53 backend will be replaced by the upgraded one.
+      autoUpgradePool=STRING,   -- If ``autoUpgrade`` is set, in which pool to place the newly upgraded backend. Default is empty which means the backend is placed in the default pool.
+      autoUpgradeDoHKey=NUM     -- If ``autoUpgrade`` is set, the value to use for the SVC key corresponding to the DoH path. Default is 7.
     })
 
   :param str server_string: A simple IP:PORT string.