From: Otto Moerbeek Date: Mon, 6 Oct 2025 10:14:58 +0000 (+0200) Subject: Provide TLS config by lookup on name or subnet. X-Git-Tag: rec-5.4.0-alpha1~190^2~7 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0ab91c10ded3fe161a42ce57c3e5b4618ba04a4c;p=thirdparty%2Fpdns.git Provide TLS config by lookup on name or subnet. With settable verification mode, provider, subject name and some more. Signed-off-by: Otto Moerbeek --- diff --git a/pdns/recursordist/lwres.cc b/pdns/recursordist/lwres.cc index 127c74bbe4..d409b677b3 100644 --- a/pdns/recursordist/lwres.cc +++ b/pdns/recursordist/lwres.cc @@ -378,7 +378,7 @@ class BindError { }; -static bool tcpconnect(const OptLog& log, const ComboAddress& remote, const std::optional localBind, TCPOutConnectionManager::Connection& connection, bool& dnsOverTLS, const std::string& nsName) +static bool tcpconnect(const OptLog& log, const ComboAddress& remote, const std::optional localBind, TCPOutConnectionManager::Connection& connection, bool& dnsOverTLS, const std::string& nsName, std::string& subjectName) { dnsOverTLS = SyncRes::s_dot_to_port_853 && remote.getPort() == 853; @@ -412,14 +412,21 @@ static bool tcpconnect(const OptLog& log, const ComboAddress& remote, const std: } std::shared_ptr tlsCtx{nullptr}; + bool subjectIsAddress = false; if (dnsOverTLS) { - tlsCtx = TCPOutConnectionManager::getTLSContext(nsName, remote); + subjectName = nsName; + std::string subjectAddress; + tlsCtx = TCPOutConnectionManager::getTLSContext(nsName, remote, connection.d_verboseLogging, subjectName, subjectAddress); if (tlsCtx == nullptr) { g_slogout->info(Logr::Error, "DoT requested but not available", "server", Logging::Loggable(remote)); dnsOverTLS = false; } + else if (subjectName.empty() && !subjectAddress.empty()) { + subjectName = subjectAddress; + subjectIsAddress = true; + } } - connection.d_handler = std::make_shared(nsName, false, sock.releaseHandle(), timeout, tlsCtx); + connection.d_handler = std::make_shared(subjectName, subjectIsAddress, sock.releaseHandle(), timeout, tlsCtx); connection.d_local = localBind; // Returned state ignored // This can throw an exception, retry will need to happen at higher level @@ -428,7 +435,8 @@ static bool tcpconnect(const OptLog& log, const ComboAddress& remote, const std: } static LWResult::Result tcpsendrecv(const ComboAddress& ip, TCPOutConnectionManager::Connection& connection, - ComboAddress& localip, const vector& vpacket, size_t& len, PacketBuffer& buf) + ComboAddress& localip, const vector& vpacket, size_t& len, PacketBuffer& buf, + const std::string& nsName, const std::string subjectName) { socklen_t slen = ip.getSocklen(); uint16_t tlen = htons(vpacket.size()); @@ -447,8 +455,15 @@ static LWResult::Result tcpsendrecv(const ComboAddress& ip, TCPOutConnectionMana LWResult::Result ret = asendtcp(packet, connection.d_handler); if (ret != LWResult::Result::Success) { - auto result = connection.d_handler->getVerifyResult(); - cerr << "ASENDTCP RETURNED FAIL " << ip.toString() << ' ' << result.first << ' ' << result.second << endl; + if (connection.d_handler->isTLS() && connection.d_verboseLogging) { + auto result = connection.d_handler->getVerifyResult(); + g_slogout->info(Logr::Error, "Failed to setup TLS connection", + "errorcode", Logging::Loggable(result.first), + "remote", Logging::Loggable(ip), + "nsname", Logging::Loggable(nsName), + "subjectName", Logging::Loggable(subjectName), + "tlsmessage", Logging::Loggable(result.second)); + } return ret; } @@ -720,8 +735,9 @@ static LWResult::Result asyncresolve(const OptLog& log, const ComboAddress& addr // peer has closed it on error, so we retry. At some point we // *will* get a new connection, so this loop is not endless. isNew = true; // tcpconnect() might throw for new connections. In that case, we want to break the loop, scanbuild complains here, which is a false positive afaik - isNew = tcpconnect(log, address, addressToBindTo, connection, dnsOverTLS, nsName); - ret = tcpsendrecv(address, connection, localip, vpacket, len, buf); + std::string subjectName; + isNew = tcpconnect(log, address, addressToBindTo, connection, dnsOverTLS, nsName, subjectName); + ret = tcpsendrecv(address, connection, localip, vpacket, len, buf, nsName, subjectName); #ifdef HAVE_FSTRM if (fstrmQEnabled) { logFstreamQuery(fstrmLoggers, queryTime, localip, address, !dnsOverTLS ? DnstapMessage::ProtocolType::DoTCP : DnstapMessage::ProtocolType::DoT, context.d_auth, vpacket); @@ -736,12 +752,15 @@ static LWResult::Result asyncresolve(const OptLog& log, const ComboAddress& addr // Cookie info already has been added to packet, so we must retry from a higher level auto lock = s_cookiestore.lock(); lock->erase(address); + VLOG(log, "BindError remote: " << address.toString() << " localAddress: " << (addressToBindTo ? addressToBindTo->toString() : "none") << endl); return LWResult::Result::BindError; } - catch (const NetworkError&) { + catch (const NetworkError& nwe) { + VLOG(log, "NetworkException: " << address.toString() << ": " << nwe.what() << endl); ret = LWResult::Result::OSLimitError; // OS limits error } - catch (const runtime_error&) { + catch (const runtime_error& rte) { + VLOG(log, "runtime_error: " << address.toString() << ": " << rte.what() << endl); ret = LWResult::Result::OSLimitError; // OS limits error (PermanentError is transport related) } } while (!isNew); diff --git a/pdns/recursordist/rec-main.cc b/pdns/recursordist/rec-main.cc index 55149fa15c..bdef68605b 100644 --- a/pdns/recursordist/rec-main.cc +++ b/pdns/recursordist/rec-main.cc @@ -2240,6 +2240,7 @@ static int serviceMain(Logr::log_t log) TCPOutConnectionManager::s_maxIdlePerAuth = ::arg().asNum("tcp-out-max-idle-per-auth"); TCPOutConnectionManager::s_maxQueries = ::arg().asNum("tcp-out-max-queries"); TCPOutConnectionManager::s_maxIdlePerThread = ::arg().asNum("tcp-out-max-idle-per-thread"); + TCPOutConnectionManager::setupOutgoingTLSTables(); g_gettagNeedsEDNSOptions = ::arg().mustDo("gettag-needs-edns-options"); diff --git a/pdns/recursordist/rec-rust-lib/generate.py b/pdns/recursordist/rec-rust-lib/generate.py index 910b7a20eb..902c344fd6 100644 --- a/pdns/recursordist/rec-rust-lib/generate.py +++ b/pdns/recursordist/rec-rust-lib/generate.py @@ -113,6 +113,7 @@ class LType(Enum): ListSubnets = auto() ListTrustAnchors = auto() ListZoneToCaches = auto() + ListOutgoingTLSConfigurations = auto() String = auto() Uint64 = auto() @@ -120,7 +121,8 @@ listOfStringTypes = (LType.ListSocketAddresses, LType.ListStrings, LType.ListSu listOfStructuredTypes = (LType.ListAuthZones, LType.ListForwardZones, LType.ListTrustAnchors, LType.ListNegativeTrustAnchors, LType.ListProtobufServers, LType.ListDNSTapFrameStreamServers, LType.ListDNSTapNODFrameStreamServers, LType.ListSortLists, LType.ListRPZs, LType.ListZoneToCaches, LType.ListAllowedAdditionalQTypes, - LType.ListProxyMappings, LType.ListForwardingCatalogZones, LType.ListIncomingWSConfigs) + LType.ListProxyMappings, LType.ListForwardingCatalogZones, LType.ListIncomingWSConfigs, + LType.ListOutgoingTLSConfigurations) def get_olddoc_typename(typ): """Given a type from table.py, return the old-style type name""" @@ -188,6 +190,8 @@ def get_newdoc_typename(typ): return 'Sequence of `ForwardingCatalogZone`_' if typ == LType.ListIncomingWSConfigs: return 'Sequence of `IncomingWSConfig`_' + if typ == LType.ListOutgoingTLSConfigurations: + return 'Sequence of `OutgoingTLSConfiguration`_' return 'Unknown2' + str(typ) def get_default_olddoc_value(typ, val): diff --git a/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs b/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs index 46b90831be..b1e8a2214b 100644 --- a/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs +++ b/pdns/recursordist/rec-rust-lib/rust-bridge-in.rs @@ -313,6 +313,7 @@ pub struct IncomingTLS { // #[serde(default, skip_serializing_if = "crate::is_default")] // password: String, Not currently supported, as rusttls does not support this out of the box } + #[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] #[serde(deny_unknown_fields)] pub struct IncomingWSConfig { @@ -322,6 +323,33 @@ pub struct IncomingWSConfig { tls: IncomingTLS, } +#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)] +#[serde(deny_unknown_fields)] +struct OutgoingTLSConfiguration { + #[serde(default, skip_serializing_if = "crate::is_default")] + name: String, + #[serde(default, skip_serializing_if = "crate::is_default")] + provider: String, + #[serde(default, skip_serializing_if = "crate::is_default")] + suffixes: Vec, + #[serde(default, skip_serializing_if = "crate::is_default")] + subnets: Vec, + #[serde(default, skip_serializing_if = "crate::is_default")] + validate_certificate: bool, + #[serde(default, skip_serializing_if = "crate::is_default")] + ca_store: String, + #[serde(default, skip_serializing_if = "crate::is_default")] + verbose_logging: bool, + #[serde(default, skip_serializing_if = "crate::is_default")] + subject_name: String, + #[serde(default, skip_serializing_if = "crate::is_default")] + subject_address: String, + #[serde(default, skip_serializing_if = "crate::is_default")] + ciphers: String, + #[serde(default, skip_serializing_if = "crate::is_default")] + ciphers_tls_13: String, +} + // Two structs used to generated YAML based on a vector of name to value mappings // Cannot use Enum as CXX has only very basic Enum support struct Value { diff --git a/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs b/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs index 4167b9758d..95b8b43a56 100644 --- a/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs +++ b/pdns/recursordist/rec-rust-lib/rust/src/bridge.rs @@ -773,6 +773,26 @@ impl IncomingWSConfig { } } +impl OutgoingTLSConfiguration { + pub fn validate(&self, field: &str) -> Result<(), ValidationError> { + if self.name.is_empty() { + let msg = format!("{}: value may not be empty", field); + return Err(ValidationError { msg }); + } + validate_vec( + &(field.to_string() + ".suffixes"), + &self.suffixes, + validate_name, + )?; + validate_vec( + &(field.to_string() + ".subnets"), + &self.subnets, + validate_subnet, + )?; + Ok(()) + } +} + #[allow(clippy::ptr_arg)] //# Avoids creating a rust::Slice object on the C++ side. pub fn validate_auth_zones(field: &str, vec: &Vec) -> Result<(), ValidationError> { validate_vec(field, vec, |field, element| element.validate(field)) diff --git a/pdns/recursordist/rec-rust-lib/table.py b/pdns/recursordist/rec-rust-lib/table.py index 02a54af3d3..d28db2e752 100644 --- a/pdns/recursordist/rec-rust-lib/table.py +++ b/pdns/recursordist/rec-rust-lib/table.py @@ -3642,4 +3642,17 @@ Addresses of servers that do not properly support DNS cookies (:rfc:`7873`, :rfc ''', 'versionadded': '5.3.0', }, + { + 'name' : 'tls_configurations', + 'section' : 'outgoing', + 'type' : LType.ListOutgoingTLSConfigurations, + 'default' : '', + 'help' : 'Sequence of OutgoingTLSConfiguration', + 'doc' : ''' +Sequence of OutgoingTLSConfiguration.` + ''', + 'skip-old' : 'No equivalent old style setting', + 'versionadded': '5.4.0', + 'runtime': ['reload-lua-config', 'reload-yaml'], # XXX + }, ] diff --git a/pdns/recursordist/rec-tcpout.cc b/pdns/recursordist/rec-tcpout.cc index 4ace08a172..5fb4076195 100644 --- a/pdns/recursordist/rec-tcpout.cc +++ b/pdns/recursordist/rec-tcpout.cc @@ -27,6 +27,10 @@ #undef CERT #include "syncres.hh" +#include "dnsname.hh" +#include "rec-main.hh" + +#include "cxxsettings.hh" timeval TCPOutConnectionManager::s_maxIdleTime; size_t TCPOutConnectionManager::s_maxQueries; @@ -82,12 +86,53 @@ TCPOutConnectionManager::Connection TCPOutConnectionManager::get(const endpoints return Connection{}; } -std::shared_ptr TCPOutConnectionManager::getTLSContext(const std::string& name, const ComboAddress& address) +static SuffixMatchTree s_suffixToConfig; +static NetmaskTree s_netmaskToConfig; + +void TCPOutConnectionManager::setupOutgoingTLSTables() { + auto settings = g_yamlStruct.lock(); + auto& vec = settings->outgoing.tls_configurations; + for (const auto& entry : vec) { + for (const auto& element : entry.suffixes) { + DNSName name = DNSName(std::string(element)); + auto copy = entry; + s_suffixToConfig.add(name, std::move(copy)); + } + for (const auto& element : entry.subnets) { + s_netmaskToConfig.insert(std::string(element)).second = entry; + } + } +} + +std::shared_ptr TCPOutConnectionManager::getTLSContext(const std::string& name, const ComboAddress& address, bool& verboseLogging, std::string& subjectName, std::string &subjectAddress) +{ + pdns::rust::settings::rec::OutgoingTLSConfiguration* config{nullptr}; + + if (auto* node = s_netmaskToConfig.lookup(address); node != nullptr) { + config = &node->second; + } + else if (auto* found = s_suffixToConfig.lookup(DNSName(name)); found != nullptr) { + config = found; + } + TLSContextParameters tlsParams; tlsParams.d_provider = "openssl"; - tlsParams.d_validateCertificates = true; - // tlsParams.d_caStore + tlsParams.d_validateCertificates = false; + if (config != nullptr) { + tlsParams.d_provider = std::string(config->provider); + tlsParams.d_validateCertificates = config->validate_certificate; + tlsParams.d_caStore = std::string(config->ca_store); + if (!config->subject_name.empty()) { + subjectName = std::string(config->subject_name); + }; + if (!config->subject_address.empty()) { + subjectAddress = std::string(config->subject_address); + }; + verboseLogging = config->verbose_logging = true; + tlsParams.d_ciphers = std::string(config->ciphers); + tlsParams.d_ciphers13 = std::string(config->ciphers_tls_13); + } return ::getTLSContext(tlsParams); } diff --git a/pdns/recursordist/rec-tcpout.hh b/pdns/recursordist/rec-tcpout.hh index 64fae67eda..4a954b2c77 100644 --- a/pdns/recursordist/rec-tcpout.hh +++ b/pdns/recursordist/rec-tcpout.hh @@ -51,6 +51,7 @@ public: std::optional d_local; timeval d_last_used{0, 0}; size_t d_numqueries{0}; + bool d_verboseLogging{false}; }; using endpoints_t = std::pair>; @@ -68,7 +69,8 @@ public: return new uint64_t(size()); // NOLINT(cppcoreguidelines-owning-memory): it's the API } - static std::shared_ptr getTLSContext(const std::string& name, const ComboAddress& address); + static void setupOutgoingTLSTables(); + static std::shared_ptr getTLSContext(const std::string& name, const ComboAddress& address, bool& verboseLogging, std::string& subjectName, std::string& subjectAddress); private: // This does not take into account that we can have multiple connections with different hosts (via SNI) to the same IP.