]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
rec: facility to resolve names via system resolver
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Fri, 23 Feb 2024 13:43:03 +0000 (14:43 +0100)
committerOtto Moerbeek <otto.moerbeek@open-xchange.com>
Mon, 25 Mar 2024 09:22:05 +0000 (10:22 +0100)
pdns/recursordist/Makefile.am
pdns/recursordist/rec-main.cc
pdns/recursordist/rec-system-resolve.cc [new file with mode: 0644]
pdns/recursordist/rec-system-resolve.hh [new file with mode: 0644]
pdns/recursordist/rec_channel_rec.cc
pdns/recursordist/reczones.cc
pdns/recursordist/settings/rust/Cargo.lock
pdns/recursordist/settings/rust/Cargo.toml
pdns/recursordist/settings/rust/src/bridge.rs
pdns/recursordist/test-rec-system-resolve.cc [new file with mode: 0644]
pdns/recursordist/test-settings.cc

index 9bb70569495e2411ad1b7274a6c5e12fda28abbe..762f7a37622665c45e9428b71f859a4cd6c8a435 100644 (file)
@@ -176,6 +176,7 @@ pdns_recursor_SOURCES = \
        rec-protozero.cc rec-protozero.hh \
        rec-responsestats.hh rec-responsestats.cc \
        rec-snmp.hh rec-snmp.cc \
+       rec-system-resolve.hh rec-system-resolve.cc \
        rec-taskqueue.cc rec-taskqueue.hh \
        rec-tcounters.cc rec-tcounters.hh \
        rec-tcp.cc \
@@ -308,6 +309,7 @@ testrunner_SOURCES = \
        rcpgenerator.cc \
        rec-eventtrace.cc rec-eventtrace.hh \
        rec-responsestats.hh rec-responsestats.cc \
+       rec-system-resolve.hh rec-system-resolve.cc \
        rec-taskqueue.cc rec-taskqueue.hh \
        rec-tcounters.cc rec-tcounters.hh \
        rec-zonetocache.cc rec-zonetocache.hh \
@@ -349,6 +351,7 @@ testrunner_SOURCES = \
        test-negcache_cc.cc \
        test-packetcache_hh.cc \
        test-rcpgenerator_cc.cc \
+       test-rec-system-resolve.cc \
        test-rec-taskqueue.cc \
        test-rec-tcounters_cc.cc \
        test-rec-zonetocache.cc \
index 69b59cec92ebb763b1f59c91223892c48d850271..d2c3abd5ddc85c3963057c5bc5c3f920160d611a 100644 (file)
@@ -41,7 +41,7 @@
 #include "dnsseckeeper.hh"
 #include "settings/cxxsettings.hh"
 #include "json.hh"
-
+#include "rec-system-resolve.hh"
 #ifdef NOD_ENABLED
 #include "nod.hh"
 #endif /* NOD_ENABLED */
@@ -2453,6 +2453,13 @@ static void houseKeepingWork(Logr::log_t log)
     });
   }
   else if (info.isHandler()) {
+    // static PeriodicTask systemResolveTask{"SysResolveCheckTask", 10};
+    // systemResolveTask.runIfDue(now, [] () {
+    //   auto& sysResolver = pdns::RecResolve::getInstance();
+    //   if (sysResolver.changeDetected()) {
+    //     reloadZoneConfiguration(g_yamlSettings);
+    //   }
+    // });
     if (g_packetCache) {
       static PeriodicTask packetCacheTask{"packetCacheTask", 5};
       packetCacheTask.runIfDue(now, [now]() {
@@ -3201,6 +3208,8 @@ int main(int argc, char** argv)
 
     handleRuntimeDefaults(startupLog);
 
+    pdns::RecResolve::setInstanceParameters(60, []() { reloadZoneConfiguration(g_yamlSettings); });
+
     g_recCache = std::make_unique<MemRecursorCache>(::arg().asNum("record-cache-shards"));
     g_negCache = std::make_unique<NegCache>(::arg().asNum("record-cache-shards") / 8);
     if (!::arg().mustDo("disable-packetcache")) {
diff --git a/pdns/recursordist/rec-system-resolve.cc b/pdns/recursordist/rec-system-resolve.cc
new file mode 100644 (file)
index 0000000..ced2c3e
--- /dev/null
@@ -0,0 +1,233 @@
+/*
+ * 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 <sys/types.h>
+#include <sys/socket.h>
+#include <netdb.h>
+
+#include "rec-system-resolve.hh"
+#include "logging.hh"
+#include "threadname.hh"
+
+namespace
+{
+ComboAddress resolve(const std::string& name)
+{
+  struct addrinfo hints = {};
+  hints.ai_flags = AI_ADDRCONFIG;
+  hints.ai_family = 0;
+
+  struct addrinfo* res = nullptr;
+  auto ret = getaddrinfo(name.c_str(), nullptr, &hints, &res);
+  // We pick the first address returned for now
+  if (ret == 0) {
+    auto address = ComboAddress{res->ai_addr, res->ai_addrlen};
+    freeaddrinfo(res);
+    return address;
+  }
+  return {};
+}
+} // anonymous namespace
+
+std::function<void()> pdns::RecResolve::s_callback;
+time_t pdns::RecResolve::s_ttl;
+
+void pdns::RecResolve::setInstanceParameters(time_t ttl, const std::function<void()>& callback)
+{
+  pdns::RecResolve::s_ttl = ttl;
+  pdns::RecResolve::s_callback = callback;
+}
+
+pdns::RecResolve& pdns::RecResolve::getInstance()
+{
+  static unique_ptr<RecResolve> res = make_unique<pdns::RecResolve>(s_ttl, s_callback);
+  return *res;
+}
+
+pdns::RecResolve::RecResolve(time_t ttl, const std::function<void()>& callback) :
+  d_ttl(ttl), d_refresher(ttl / 6, callback, *this)
+{
+}
+
+pdns::RecResolve::~RecResolve() = default;
+
+void pdns::RecResolve::stopRefresher()
+{
+  d_refresher.finish();
+}
+
+void pdns::RecResolve::startRefresher()
+{
+  d_refresher.start();
+}
+
+ComboAddress pdns::RecResolve::lookupAndRegister(const std::string& name, time_t now)
+{
+  auto data = d_data.lock();
+  if (auto iter = data->d_map.find(name); iter != data->d_map.end()) {
+      if (iter->second.d_ttd < now) {
+        return iter->second.d_address;
+      }
+      // If it's stale, re-resolve below
+  }
+  // We keep the lock while resolving, even though this might take a while...
+  auto address = resolve(name);
+
+  time_t ttd = now + d_ttl;
+  auto iter = data->d_map.emplace(name, AddressData{address, ttd}).first;
+  return iter->second.d_address;
+}
+
+ComboAddress pdns::RecResolve::lookup(const std::string& name)
+{
+  auto data = d_data.lock();
+  if (auto iter = data->d_map.find(name); iter != data->d_map.end()) {
+    // always return it, even if it's stale
+    return iter->second.d_address;
+  }
+  throw PDNSException("system resolve of unregistered name: " + name);
+}
+
+void pdns::RecResolve::wipe(const string& name)
+{
+  auto data = d_data.lock();
+  if (name.empty()) {
+    data->d_map.clear();
+  }
+  else {
+    data->d_map.erase(name);
+  }
+}
+
+bool pdns::RecResolve::refresh(time_t now)
+{
+  // The refrsh taks shol dnot take the lock for a long time, so we're working on a copy
+  ResolveData copy;
+  {
+    auto data = d_data.lock();
+    copy = *data;
+  }
+  std::map<std::string, AddressData> newData;
+
+  auto log = g_slog->withName("system-resolver");
+
+  bool updated = false;
+  for (const auto& entry : copy.d_map) {
+    if (entry.second.d_ttd <= now) {
+      auto newAddress = resolve(entry.first);
+      time_t ttd = now;
+      if (newAddress != ComboAddress()) {
+        // positive resolve, good for ttl
+        ttd += d_ttl;
+      }
+      else {
+        log->error(Logr::Error, "Name did not resolve", "name", Logging::Loggable(entry.first));
+      }
+      if (newAddress != entry.second.d_address) {
+        log->info(Logr::Debug, "Name resolved to new address", "name", Logging::Loggable(entry.first),
+                  "address", Logging::Loggable(newAddress.toString()));
+        // An address changed
+        updated = true;
+      }
+      newData.emplace(entry.first, AddressData{newAddress, ttd});
+    }
+  }
+
+  if (!newData.empty()) {
+    auto data = d_data.lock();
+    for (const auto& entry : newData) {
+      data->d_map.insert_or_assign(entry.first, entry.second);
+    }
+  }
+  if (updated) {
+    log->info(Logr::Info, "Changes in names detected");
+  }
+  return updated;
+}
+
+bool pdns::RecResolve::changeDetected()
+{
+  bool change = d_refresher.changes.exchange(false);
+  return change;
+}
+
+pdns::RecResolve::Refresher::Refresher(time_t interval, const std::function<void()>& callback, pdns::RecResolve& res) :
+  d_resolver(res), d_callback(callback), d_interval(std::max(static_cast<time_t>(1), interval))
+{
+  start();
+}
+
+pdns::RecResolve::Refresher::~Refresher()
+{
+  finish();
+}
+
+void pdns::RecResolve::Refresher::refreshLoop()
+{
+  setThreadName("rec/sysres");
+
+  while (!stop) {
+    const time_t startTime = time(nullptr);
+    time_t wakeTime = startTime;
+    while (wakeTime - startTime < d_interval) {
+      std::unique_lock lock(mutex);
+      time_t remaining = d_interval - (wakeTime - startTime);
+      if (remaining <= 0) {
+        break;
+      }
+      condVar.wait_for(lock, std::chrono::seconds(remaining),
+                       [&wakeup = wakeup] { return wakeup.load(); });
+      wakeup = false;
+      if (stop) {
+        break;
+      }
+      changes = d_resolver.refresh(time(nullptr));
+      wakeTime = time(nullptr);
+      if (changes) {
+        d_callback();
+        changes = false;
+      }
+    }
+  }
+}
+
+void pdns::RecResolve::Refresher::finish()
+{
+  stop = true;
+  wakeup = true;
+  condVar.notify_one();
+  d_thread.join();
+}
+
+void pdns::RecResolve::Refresher::start()
+{
+  stop = false;
+  wakeup = false;
+  d_thread = std::thread([this]() { refreshLoop(); });
+}
+
+void pdns::RecResolve::Refresher::trigger()
+{
+  stop = true;
+  wakeup = true;
+  condVar.notify_one();
+}
diff --git a/pdns/recursordist/rec-system-resolve.hh b/pdns/recursordist/rec-system-resolve.hh
new file mode 100644 (file)
index 0000000..3931f79
--- /dev/null
@@ -0,0 +1,100 @@
+/*
+ * 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 "config.h"
+
+#include <functional>
+#include <thread>
+
+#include "namespaces.hh"
+#include "iputils.hh"
+#include "lock.hh"
+
+namespace pdns
+{
+class RecResolve
+{
+public:
+  // Should be called before any getInstance() call is done
+  static void setInstanceParameters(time_t ttl, const std::function<void()>& callback);
+  static RecResolve& getInstance();
+
+  RecResolve(time_t ttl = 60, const std::function<void()>& callback = nullptr);
+  ~RecResolve();
+  ComboAddress lookupAndRegister(const std::string& name, time_t now);
+  ComboAddress lookup(const std::string& name);
+  void stopRefresher();
+  void startRefresher();
+  void wipe(const std::string& name = "");
+  bool refresh(time_t now);
+  bool changeDetected();
+
+private:
+  struct AddressData
+  {
+    ComboAddress d_address;
+    time_t d_ttd{0};
+  };
+  struct ResolveData
+  {
+    std::map<std::string, AddressData> d_map;
+  };
+  LockGuarded<ResolveData> d_data;
+  const time_t d_ttl;
+
+  class Refresher
+  {
+  public:
+    Refresher(time_t interval, const std::function<void()>& callback, pdns::RecResolve& res);
+    Refresher(const Refresher&) = delete;
+    Refresher(Refresher&&) = delete;
+    Refresher& operator=(const Refresher&) = delete;
+    Refresher& operator=(Refresher&&) = delete;
+    ~Refresher();
+
+    void start();
+    void finish();
+    void trigger();
+
+    std::atomic<bool> changes{false};
+  private:
+    void refreshLoop();
+
+    pdns::RecResolve& d_resolver;
+    std::function<void()> d_callback;
+    time_t d_interval;
+    std::thread d_thread;
+    std::mutex mutex;
+    std::condition_variable condVar;
+    std::atomic<bool> wakeup{false};
+    std::atomic<bool> stop{false};
+  };
+
+  Refresher d_refresher;
+
+  static std::function<void()> s_callback;
+  static time_t s_ttl;
+};
+
+}
index 3ad2f9f5f6493ec8843b8e995124b58afe4bc960..8a3af52076cfa16fc7554af273bbc511bdfa1bb1 100644 (file)
@@ -38,6 +38,7 @@
 #include "rec-taskqueue.hh"
 #include "rec-tcpout.hh"
 #include "rec-main.hh"
+#include "rec-system-resolve.hh"
 
 #include "settings/cxxsettings.hh"
 
@@ -2174,6 +2175,16 @@ static RecursorControlChannel::Answer reloadACLs()
   return {0, "ok\n"};
 }
 
+static std::string reloadZoneConfigurationWithSysResolveReset()
+{
+  auto& sysResolver = pdns::RecResolve::getInstance();
+  sysResolver.stopRefresher();
+  sysResolver.wipe();
+  auto ret = reloadZoneConfiguration(g_yamlSettings);
+  sysResolver.startRefresher();
+  return ret;
+}
+
 RecursorControlChannel::Answer RecursorControlParser::getAnswer(int socket, const string& question, RecursorControlParser::func_t** command)
 {
   *command = nop;
@@ -2317,7 +2328,7 @@ RecursorControlChannel::Answer RecursorControlParser::getAnswer(int socket, cons
       g_log << Logger::Error << "Unable to reload zones and forwards when chroot()'ed, requested via control channel" << endl;
       return {1, "Unable to reload zones and forwards when chroot()'ed, please restart\n"};
     }
-    return {0, reloadZoneConfiguration(g_yamlSettings)};
+    return {0, reloadZoneConfigurationWithSysResolveReset()};
   }
   if (cmd == "set-ecs-minimum-ttl") {
     return {0, setMinimumECSTTL(begin, end)};
index ced34549a8e68b69a7b321efd6b35f3e25ed2009..c07cb600c13ba6ed5ecd0486df9ca27e2584c34b 100644 (file)
@@ -33,6 +33,7 @@
 #include "syncres.hh"
 #include "zoneparser-tng.hh"
 #include "settings/cxxsettings.hh"
+#include "rec-system-resolve.hh"
 
 extern int g_argc;
 extern char** g_argv;
@@ -62,6 +63,30 @@ bool primeHints(time_t now)
   return ret;
 }
 
+static ComboAddress fromNameOrIP(const string& str, uint16_t defPort, Logr::log_t log)
+{
+  try {
+    ComboAddress addr = parseIPAndPort(str, defPort);
+    return addr;
+  }
+  catch (const PDNSException&) {
+    uint16_t port = defPort;
+    string::size_type pos = str.rfind(':');
+    if (pos != string::npos) {
+      cerr << str.substr(pos) << endl;
+      port = pdns::checked_stoi<uint16_t>(str.substr(pos + 1));
+    }
+    auto& res = pdns::RecResolve::getInstance();
+    ComboAddress address = res.lookupAndRegister(str.substr(0, pos), time(nullptr));
+    if (address != ComboAddress()) {
+      address.setPort(port);
+      return address;
+    }
+    log->error(Logr::Error, "Could not resolve name", "name", Logging::Loggable(str));
+    throw PDNSException("Could not resolve " + str);
+  }
+}
+
 static void convertServersForAD(const std::string& zone, const std::string& input, SyncRes::AuthDomain& authDomain, const char* sepa, Logr::log_t log, bool verbose = true)
 {
   vector<string> servers;
@@ -70,7 +95,7 @@ static void convertServersForAD(const std::string& zone, const std::string& inpu
 
   vector<string> addresses;
   for (auto& server : servers) {
-    ComboAddress addr = parseIPAndPort(server, 53);
+    ComboAddress addr = fromNameOrIP(server, 53, log);
     authDomain.d_servers.push_back(addr);
     if (verbose) {
       addresses.push_back(addr.toStringWithPort());
index d1d48a14650c20b5839c90f3ed82beb51eb551c4..6c098259647e280e1407615560b9f023c21739f8 100644 (file)
@@ -77,6 +77,12 @@ version = "0.14.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
 
+[[package]]
+name = "hostname-validator"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f558a64ac9af88b5ba400d99b579451af0d39c6d360980045b91aac966d705e2"
+
 [[package]]
 name = "indexmap"
 version = "2.1.0"
@@ -189,6 +195,7 @@ version = "0.1.0"
 dependencies = [
  "cxx",
  "cxx-build",
+ "hostname-validator",
  "ipnet",
  "once_cell",
  "serde",
index 89cef97885b99a19b9044f310fe68fa681d7c5da..80ab080e98f5393de20f0080c263896d8749efa6 100644 (file)
@@ -13,6 +13,7 @@ serde = { version = "1.0", features = ["derive"] }
 serde_yaml = "0.9"
 ipnet = "2.8"
 once_cell = "1.18.0"
+hostname-validator = "1.1.1"
 
 [build-dependencies]
 cxx-build = "1.0"
index 181e23424247d1112d6d6106fdde6a11dc84a551..d8a004febe1e66ed5b62a1b6a1bc28ef13da6643 100644 (file)
@@ -67,6 +67,30 @@ pub fn validate_socket_address(field: &str, val: &String) -> Result<(), Validati
     Ok(())
 }
 
+fn is_port_number(str: &str) -> bool {
+    str.parse::<u16>().is_ok()
+}
+
+pub fn validate_socket_address_or_name(field: &str, val: &String) -> Result<(), ValidationError> {
+    let sa = SocketAddr::from_str(val);
+    if sa.is_err() {
+        let ip = IpAddr::from_str(val);
+        if ip.is_err() {
+            if !hostname_validator::is_valid(val) {
+                let parts:Vec<&str> = val.split(':').collect();
+                if parts.len () != 2 || !hostname_validator::is_valid(parts[0]) || !is_port_number(parts[1]) {
+                    let msg = format!(
+                        "{}: value `{}' is not an IP, IP:port, name or name:port combination",
+                        field, val
+                    );
+                    return Err(ValidationError { msg });
+                }
+            }
+        }
+    }
+    Ok(())
+}
+
 fn validate_name(field: &str, val: &String) -> Result<(), ValidationError> {
     if val.is_empty() {
         let msg = format!("{}: value may not be empty", field);
@@ -159,7 +183,7 @@ impl ForwardZone {
         validate_vec(
             &(field.to_owned() + ".forwarders"),
             &self.forwarders,
-            validate_socket_address,
+            validate_socket_address_or_name,
         )
     }
 
diff --git a/pdns/recursordist/test-rec-system-resolve.cc b/pdns/recursordist/test-rec-system-resolve.cc
new file mode 100644 (file)
index 0000000..79e790c
--- /dev/null
@@ -0,0 +1,23 @@
+#ifndef BOOST_TEST_DYN_LINK
+#define BOOST_TEST_DYN_LINK
+#endif
+
+#include <boost/test/unit_test.hpp>
+
+#include "rec-system-resolve.hh"
+
+BOOST_AUTO_TEST_SUITE(rec_system_resolve)
+
+BOOST_AUTO_TEST_CASE(test_basic_resolee)
+{
+  auto sysResolve = pdns::RecResolve();
+
+  auto address = sysResolve.lookupAndRegister("localhost", time(nullptr));
+  BOOST_CHECK(address.toString() == "127.0.0.1" || address.toString() == "::1");
+  address = sysResolve.lookup("localhost");
+  BOOST_CHECK(address.toString() == "127.0.0.1" || address.toString() == "::1");
+  sysResolve.wipe("localhost");
+  BOOST_CHECK_THROW(sysResolve.lookup("localhost"), PDNSException);
+}
+
+BOOST_AUTO_TEST_SUITE_END()
index 91cc367853363a178de42ff0270fc2db42dda71a..0e1eb5b002df47dfac27b79154998b63d8ec1869 100644 (file)
@@ -112,7 +112,7 @@ recursor:
     - zone: "example.com"
       forwarders:
         - 1.2.3.4
-        - a.b
+        - '-a.b'
 )EOT";
 
   auto settings = pdns::rust::settings::rec::parse_yaml_string(yaml);