From: Otto Moerbeek Date: Fri, 23 Feb 2024 13:43:03 +0000 (+0100) Subject: rec: facility to resolve names via system resolver X-Git-Tag: rec-5.1.0-alpha1~82^2~13 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d4a617401ea848686f22d9ceb1caa42176d7fb3a;p=thirdparty%2Fpdns.git rec: facility to resolve names via system resolver --- diff --git a/pdns/recursordist/Makefile.am b/pdns/recursordist/Makefile.am index 9bb7056949..762f7a3762 100644 --- a/pdns/recursordist/Makefile.am +++ b/pdns/recursordist/Makefile.am @@ -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 \ diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index 69b59cec92..d2c3abd5dd 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -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(::arg().asNum("record-cache-shards")); g_negCache = std::make_unique(::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 index 0000000000..ced2c3eb46 --- /dev/null +++ b/pdns/recursordist/rec-system-resolve.cc @@ -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 +#include +#include + +#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 pdns::RecResolve::s_callback; +time_t pdns::RecResolve::s_ttl; + +void pdns::RecResolve::setInstanceParameters(time_t ttl, const std::function& callback) +{ + pdns::RecResolve::s_ttl = ttl; + pdns::RecResolve::s_callback = callback; +} + +pdns::RecResolve& pdns::RecResolve::getInstance() +{ + static unique_ptr res = make_unique(s_ttl, s_callback); + return *res; +} + +pdns::RecResolve::RecResolve(time_t ttl, const std::function& 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 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& callback, pdns::RecResolve& res) : + d_resolver(res), d_callback(callback), d_interval(std::max(static_cast(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 index 0000000000..3931f790db --- /dev/null +++ b/pdns/recursordist/rec-system-resolve.hh @@ -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 +#include + +#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& callback); + static RecResolve& getInstance(); + + RecResolve(time_t ttl = 60, const std::function& 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 d_map; + }; + LockGuarded d_data; + const time_t d_ttl; + + class Refresher + { + public: + Refresher(time_t interval, const std::function& 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 changes{false}; + private: + void refreshLoop(); + + pdns::RecResolve& d_resolver; + std::function d_callback; + time_t d_interval; + std::thread d_thread; + std::mutex mutex; + std::condition_variable condVar; + std::atomic wakeup{false}; + std::atomic stop{false}; + }; + + Refresher d_refresher; + + static std::function s_callback; + static time_t s_ttl; +}; + +} diff --git a/pdns/recursordist/rec_channel_rec.cc b/pdns/recursordist/rec_channel_rec.cc index 3ad2f9f5f6..8a3af52076 100644 --- a/pdns/recursordist/rec_channel_rec.cc +++ b/pdns/recursordist/rec_channel_rec.cc @@ -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)}; diff --git a/pdns/recursordist/reczones.cc b/pdns/recursordist/reczones.cc index ced34549a8..c07cb600c1 100644 --- a/pdns/recursordist/reczones.cc +++ b/pdns/recursordist/reczones.cc @@ -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(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 servers; @@ -70,7 +95,7 @@ static void convertServersForAD(const std::string& zone, const std::string& inpu vector 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()); diff --git a/pdns/recursordist/settings/rust/Cargo.lock b/pdns/recursordist/settings/rust/Cargo.lock index d1d48a1465..6c09825964 100644 --- a/pdns/recursordist/settings/rust/Cargo.lock +++ b/pdns/recursordist/settings/rust/Cargo.lock @@ -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", diff --git a/pdns/recursordist/settings/rust/Cargo.toml b/pdns/recursordist/settings/rust/Cargo.toml index 89cef97885..80ab080e98 100644 --- a/pdns/recursordist/settings/rust/Cargo.toml +++ b/pdns/recursordist/settings/rust/Cargo.toml @@ -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" diff --git a/pdns/recursordist/settings/rust/src/bridge.rs b/pdns/recursordist/settings/rust/src/bridge.rs index 181e234242..d8a004febe 100644 --- a/pdns/recursordist/settings/rust/src/bridge.rs +++ b/pdns/recursordist/settings/rust/src/bridge.rs @@ -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::().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 index 0000000000..79e790cbed --- /dev/null +++ b/pdns/recursordist/test-rec-system-resolve.cc @@ -0,0 +1,23 @@ +#ifndef BOOST_TEST_DYN_LINK +#define BOOST_TEST_DYN_LINK +#endif + +#include + +#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() diff --git a/pdns/recursordist/test-settings.cc b/pdns/recursordist/test-settings.cc index 91cc367853..0e1eb5b002 100644 --- a/pdns/recursordist/test-settings.cc +++ b/pdns/recursordist/test-settings.cc @@ -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);