]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add `getAddressInfo()` for asynchronous DNS resolution
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 17 Nov 2023 09:42:29 +0000 (10:42 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 24 Nov 2023 15:13:47 +0000 (16:13 +0100)
pdns/dnsdist-lua-bindings.cc
pdns/dnsdist-lua.cc
pdns/dnsdist-lua.hh
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-lua-ffi.cc
pdns/dnsdistdist/dnsdist-resolver.cc [new file with mode: 0644]
pdns/dnsdistdist/dnsdist-resolver.hh [new file with mode: 0644]
pdns/dnsdistdist/docs/reference/config.rst
regression-tests.dnsdist/test_BackendDiscovery.py

index e6a821b31f09f392914672f782ddc70b0fad4e15..a81c8fe45b092ab5b094bdcaf84479f163a17a60 100644 (file)
 #include "bpf-filter.hh"
 #include "config.h"
 #include "dnsdist.hh"
+#include "dnsdist-async.hh"
 #include "dnsdist-lua.hh"
+#include "dnsdist-resolver.hh"
 #include "dnsdist-svc.hh"
 
 #include "dolog.hh"
 
 // NOLINTNEXTLINE(readability-function-cognitive-complexity): this function declares Lua bindings, even with a good refactoring it will likely blow up the threshold
-void setupLuaBindings(LuaContext& luaCtx, bool client)
+void setupLuaBindings(LuaContext& luaCtx, bool client, bool configCheck)
 {
   luaCtx.writeFunction("vinfolog", [](const string& arg) {
       vinfolog("%s", arg);
@@ -785,4 +787,23 @@ void setupLuaBindings(LuaContext& luaCtx, bool client)
     }
     return now;
   });
+
+  luaCtx.writeFunction("getAddressInfo", [client, configCheck](std::string hostname, std::function<void(const std::string& hostname, const LuaArray<ComboAddress>& ips)> callback) {
+    if (client || configCheck) {
+      return;
+    }
+    std::thread newThread(dnsdist::resolver::asynchronousResolver, std::move(hostname), [callback=std::move(callback)](const std::string& resolvedHostname, std::vector<ComboAddress>& ips) {
+      LuaArray<ComboAddress> result;
+      result.reserve(ips.size());
+      for (auto& entry : ips) {
+        result.emplace_back(result.size() + 1, std::move(entry));
+      }
+      {
+        auto lua = g_lua.lock();
+        callback(resolvedHostname, result);
+        dnsdist::handleQueuedAsynchronousEvents();
+      }
+    });
+    newThread.detach();
+  });
 }
index e8d28dafec29f88d6db66477fc257bf01f1909c1..a943f2b9d34879d2ea03ab61f8067cc8f4c0cfc0 100644 (file)
@@ -3214,7 +3214,7 @@ vector<std::function<void(void)>> setupLua(LuaContext& luaCtx, bool client, bool
 
   setupLuaActions(luaCtx);
   setupLuaConfig(luaCtx, client, configCheck);
-  setupLuaBindings(luaCtx, client);
+  setupLuaBindings(luaCtx, client, configCheck);
   setupLuaBindingsDNSCrypt(luaCtx, client);
   setupLuaBindingsDNSParser(luaCtx);
   setupLuaBindingsDNSQuestion(luaCtx);
index 61a021fedf0a1062b2a5c98da7d24d821cf9a1cb..88439d91cf46a002b436ff836e0a424097a04de1 100644 (file)
@@ -166,7 +166,7 @@ void checkParameterBound(const std::string& parameter, uint64_t value, size_t ma
 
 vector<std::function<void(void)>> setupLua(LuaContext& luaCtx, bool client, bool configCheck, const std::string& config);
 void setupLuaActions(LuaContext& luaCtx);
-void setupLuaBindings(LuaContext& luaCtx, bool client);
+void setupLuaBindings(LuaContext& luaCtx, bool client, bool configCheck);
 void setupLuaBindingsDNSCrypt(LuaContext& luaCtx, bool client);
 void setupLuaBindingsDNSParser(LuaContext& luaCtx);
 void setupLuaBindingsDNSQuestion(LuaContext& luaCtx);
index 1124a4b35769407f73326bb1cbcf995ab708a26c..bd276cc03c48aa249904bea9685052e84813008f 100644 (file)
@@ -193,6 +193,7 @@ dnsdist_SOURCES = \
        dnsdist-protocols.cc dnsdist-protocols.hh \
        dnsdist-proxy-protocol.cc dnsdist-proxy-protocol.hh \
        dnsdist-random.cc dnsdist-random.hh \
+       dnsdist-resolver.cc dnsdist-resolver.hh \
        dnsdist-rings.cc dnsdist-rings.hh \
        dnsdist-rules.cc dnsdist-rules.hh \
        dnsdist-secpoll.cc dnsdist-secpoll.hh \
@@ -292,6 +293,7 @@ testrunner_SOURCES = \
        dnsdist-protocols.cc dnsdist-protocols.hh \
        dnsdist-proxy-protocol.cc dnsdist-proxy-protocol.hh \
        dnsdist-random.cc dnsdist-random.hh \
+       dnsdist-resolver.cc dnsdist-resolver.hh \
        dnsdist-rings.cc dnsdist-rings.hh \
        dnsdist-rules.cc dnsdist-rules.hh \
        dnsdist-session-cache.cc dnsdist-session-cache.hh \
index 9a4c41ed665eaa1ed46f86e0ba22f2d5426bc7f6..2633d64644bf6723b28b85db7ac6a2198a7ced87 100644 (file)
@@ -1000,7 +1000,7 @@ const char* getLuaFFIWrappers()
 
 void setupLuaLoadBalancingContext(LuaContext& luaCtx)
 {
-  setupLuaBindings(luaCtx, true);
+  setupLuaBindings(luaCtx, true, false);
   setupLuaBindingsDNSQuestion(luaCtx);
   setupLuaBindingsKVS(luaCtx, true);
   setupLuaVars(luaCtx);
diff --git a/pdns/dnsdistdist/dnsdist-resolver.cc b/pdns/dnsdistdist/dnsdist-resolver.cc
new file mode 100644 (file)
index 0000000..3a502aa
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * 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 <vector>
+
+#include "dnsdist-resolver.hh"
+#include "iputils.hh"
+
+namespace dnsdist::resolver
+{
+void asynchronousResolver(const std::string& hostname, const std::function<void(const std::string& hostname, std::vector<ComboAddress>& ips)>& callback)
+{
+  addrinfo hints{};
+  hints.ai_family = AF_UNSPEC;
+  hints.ai_flags = AI_V4MAPPED | AI_ADDRCONFIG;
+  hints.ai_socktype = SOCK_DGRAM;
+  addrinfo* infosRaw{nullptr};
+  std::vector<ComboAddress> addresses;
+  auto ret = getaddrinfo(hostname.c_str(), nullptr, &hints, &infosRaw);
+  if (ret != 0) {
+    callback(hostname, addresses);
+    return;
+  }
+  auto infos = std::unique_ptr<addrinfo, decltype(&freeaddrinfo)>(infosRaw, &freeaddrinfo);
+  for (const auto* addr = infos.get(); addr != nullptr; addr = addr->ai_next) {
+    try {
+      addresses.emplace_back(addr->ai_addr, addr->ai_addrlen);
+    }
+    catch (...) {
+    }
+  }
+  callback(hostname, addresses);
+}
+}
diff --git a/pdns/dnsdistdist/dnsdist-resolver.hh b/pdns/dnsdistdist/dnsdist-resolver.hh
new file mode 100644 (file)
index 0000000..44b1616
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * 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 "iputils.hh"
+
+namespace dnsdist::resolver
+{
+void asynchronousResolver(const std::string& hostname, const std::function<void(const std::string& hostname, std::vector<ComboAddress>& ips)>& callback);
+}
index 44e7d8dd937af754eedf5618ccb20b89c9d49dfc..fa84fc9da2a65c61d2045e1570c2749bf47a32d4 100644 (file)
@@ -1915,6 +1915,25 @@ These values can be set at configuration time via:
 Other functions
 ---------------
 
+.. function:: getAddressInfo(hostname, callback)
+
+  .. versionadded:: 1.9.0
+
+  Asynchronously resolve, via the system resolver (using ``getaddrinfo()``), the supplied ``hostname`` to IPv4 and IPv6 before invoking the supplied ``callback`` function with the ``hostname`` and a list of IPv4 and IPv6 addresses as :class:`ComboAddress`.
+  For example, to get the addresses of Quad9's and dynamically add them as backends:
+
+  .. code-block:: lua
+
+    function resolveCB(hostname, ips)
+      for _, ip in ipairs(ips) do
+        newServer(ip:toString())
+      end
+    end
+    getAddressInfo('dns.quad9.net.', resolveCB)
+
+  :param str hostname: The hostname to resolve.
+  :param function callback: The function to invoke when the name has been resolved.
+
 .. function:: getCurrentTime -> timespec
 
   .. versionadded:: 1.8.0
index c4178d94c415d23d7ac2d4afef4557a2fa674647..645bc59be8e047794cbb6a98da40a1fff5dade34 100644 (file)
@@ -409,3 +409,51 @@ class TestBackendDiscovery(DNSDistTest):
             # let's wait a bit longer
             time.sleep(5)
             self.assertTrue(self.checkBackendsUpgraded())
+
+class TestBackendDiscoveryByHostname(DNSDistTest):
+    _consoleKey = DNSDistTest.generateConsoleKey()
+    _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii')
+    _config_params = ['_consoleKeyB64', '_consolePort']
+    _config_template = """
+    setKey("%s")
+    controlSocket("127.0.0.1:%d")
+
+    function resolveCB(hostname, ips)
+      print('Got response for '..hostname)
+      for _, ip in ipairs(ips) do
+        print(ip)
+        newServer(ip:toString())
+      end
+    end
+
+    getAddressInfo('dns.quad9.net.', resolveCB)
+    """
+    def checkBackends(self):
+        output = self.sendConsoleCommand('showServers()')
+        print(output)
+        backends = {}
+        for line in output.splitlines(False):
+            if line.startswith('#') or line.startswith('All'):
+                continue
+            tokens = line.split()
+            self.assertTrue(len(tokens) == 13 or len(tokens) == 14)
+            backends[tokens[1]] = tokens[2]
+
+        if len(backends) != 4:
+            return False
+
+        for expected in ['9.9.9.9:53', '149.112.112.112:53', '[2620:fe::9]:53', '[2620:fe::fe]:53']:
+            self.assertIn(expected, backends)
+        for backend in backends:
+            self.assertTrue(backends[backend])
+        return True
+
+    def testBackendFromHostname(self):
+        """
+        Backend Discovery: From hostname
+        """
+        # enough time for resolution to happen
+        time.sleep(4)
+        if not self.checkBackends():
+            time.sleep(4)
+            self.assertTrue(self.checkBackends())