]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Provide TLS config by lookup on name or subnet.
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Mon, 6 Oct 2025 10:14:58 +0000 (12:14 +0200)
committerOtto Moerbeek <otto.moerbeek@open-xchange.com>
Thu, 9 Oct 2025 13:43:50 +0000 (15:43 +0200)
With settable verification mode, provider, subject name and some more.

Signed-off-by: Otto Moerbeek <otto.moerbeek@open-xchange.com>
pdns/recursordist/lwres.cc
pdns/recursordist/rec-main.cc
pdns/recursordist/rec-rust-lib/generate.py
pdns/recursordist/rec-rust-lib/rust-bridge-in.rs
pdns/recursordist/rec-rust-lib/rust/src/bridge.rs
pdns/recursordist/rec-rust-lib/table.py
pdns/recursordist/rec-tcpout.cc
pdns/recursordist/rec-tcpout.hh

index 127c74bbe4981090dceb1f0ff2cc7fadd39fcd13..d409b677b34743f28de387ea97c79bc2f5c2f2f4 100644 (file)
@@ -378,7 +378,7 @@ class BindError
 {
 };
 
-static bool tcpconnect(const OptLog& log, const ComboAddress& remote, const std::optional<ComboAddress> localBind, TCPOutConnectionManager::Connection& connection, bool& dnsOverTLS, const std::string& nsName)
+static bool tcpconnect(const OptLog& log, const ComboAddress& remote, const std::optional<ComboAddress> 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> 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<TCPIOHandler>(nsName, false, sock.releaseHandle(), timeout, tlsCtx);
+  connection.d_handler = std::make_shared<TCPIOHandler>(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<uint8_t>& vpacket, size_t& len, PacketBuffer& buf)
+                                    ComboAddress& localip, const vector<uint8_t>& 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);
index 55149fa15c072e6516bea3022bf9c2c4d83267bf..bdef68605b2d14a73bcce7ba574c959733eda5a4 100644 (file)
@@ -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");
 
index 910b7a20eb86f11e3592a331f33af6dceb23005c..902c344fd67e07e4d157f6aff221b0ef2226b1e3 100644 (file)
@@ -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):
index 46b90831be3394390ca987db3a3274bb13720e42..b1e8a2214b7767f5a0d170ea22ecbac96fa44001 100644 (file)
@@ -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<String>,
+    #[serde(default, skip_serializing_if = "crate::is_default")]
+    subnets: Vec<String>,
+    #[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 {
index 4167b9758db141c54ce48ffeb15d1de659454051..95b8b43a5604ed535ea51d62fbc210c02c84c0da 100644 (file)
@@ -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<AuthZone>) -> Result<(), ValidationError> {
     validate_vec(field, vec, |field, element| element.validate(field))
index 02a54af3d3f4e3d23ba10abd551e737a81a61e6e..d28db2e752bbe33c5c51d38569fb3c7d718b3eb4 100644 (file)
@@ -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
+    },
 ]
index 4ace08a1724c60621665552ffe48f259847d879f..5fb4076195ab622d18f908ac03e3af0fb83705cc 100644 (file)
 #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<TLSCtx> TCPOutConnectionManager::getTLSContext(const std::string& name, const ComboAddress& address)
+static SuffixMatchTree<pdns::rust::settings::rec::OutgoingTLSConfiguration> s_suffixToConfig;
+static NetmaskTree<pdns::rust::settings::rec::OutgoingTLSConfiguration> 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<TLSCtx> 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);
 }
 
index 64fae67eda1e8fb053739bf54e21d1bf87200510..4a954b2c77fdca4eab428fbfb58c6b28093cdf23 100644 (file)
@@ -51,6 +51,7 @@ public:
     std::optional<ComboAddress> d_local;
     timeval d_last_used{0, 0};
     size_t d_numqueries{0};
+    bool d_verboseLogging{false};
   };
 
   using endpoints_t = std::pair<ComboAddress, std::optional<ComboAddress>>;
@@ -68,7 +69,8 @@ public:
     return new uint64_t(size()); // NOLINT(cppcoreguidelines-owning-memory): it's the API
   }
 
-  static std::shared_ptr<TLSCtx> getTLSContext(const std::string& name, const ComboAddress& address);
+  static void setupOutgoingTLSTables();
+  static std::shared_ptr<TLSCtx> 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.