]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add experimental support for TLS asynchronous engines
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 17 Sep 2021 15:31:22 +0000 (17:31 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 3 Dec 2021 15:31:03 +0000 (16:31 +0100)
12 files changed:
pdns/dnsdist-console.cc
pdns/dnsdist-lua.cc
pdns/dnsdist-tcp.cc
pdns/dnsdistdist/dnsdist-tcp-upstream.hh
pdns/dnsdistdist/docs/advanced/tuning.rst
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/test-dnsdistnghttp2_cc.cc
pdns/dnsdistdist/test-dnsdisttcp_cc.cc
pdns/libssl.cc
pdns/libssl.hh
pdns/tcpiohandler.cc
pdns/tcpiohandler.hh

index a052779e3801e228c432b786f0de35d9ffc1d853..12feb6bf4c3d07465030c667ea69ddbda27848ab 100644 (file)
@@ -490,6 +490,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "KeyValueStoreLookupRule", true, "kvs, lookupKey", "matches queries if the key is found in the specified Key Value store" },
   { "KeyValueStoreRangeLookupRule", true, "kvs, lookupKey", "matches queries if the key is found in the specified Key Value store" },
   { "leastOutstanding", false, "", "Send traffic to downstream server with least outstanding queries, with the lowest 'order', and within that the lowest recent latency"},
+  { "loadTLSEngine", true, "engineName [, defaultString]", "Load the OpenSSL engine named 'engineName', setting the engine default string to 'defaultString' if supplied"},
   { "LogAction", true, "[filename], [binary], [append], [buffered]", "Log a line for each query, to the specified file if any, to the console (require verbose) otherwise. When logging to a file, the `binary` optional parameter specifies whether we log in binary form (default) or in textual form, the `append` optional parameter specifies whether we open the file for appending or truncate each time (default), and the `buffered` optional parameter specifies whether writes to the file are buffered (default) or not." },
   { "LogResponseAction", true, "[filename], [append], [buffered]", "Log a line for each response, to the specified file if any, to the console (require verbose) otherwise. The `append` optional parameter specifies whether we open the file for appending or truncate each time (default), and the `buffered` optional parameter specifies whether writes to the file are buffered (default) or not." },
   { "LuaAction", true, "function", "Invoke a Lua function that accepts a DNSQuestion" },
index 782b5fed702291adce3f70a5f75dabc38bbc58af..03b8e826e257bbb2c3ffcdd95246452cb2cc4b12 100644 (file)
@@ -236,6 +236,10 @@ static void parseTLSConfig(TLSConfig& config, const std::string& context, boost:
   if (vars->count("enableRenegotiation")) {
     config.d_enableRenegotiation = boost::get<bool>((*vars)["enableRenegotiation"]);
   }
+
+  if (vars->count("tlsAsyncMode")) {
+    config.d_asyncMode = boost::get<bool>((*vars).at("tlsAsyncMode"));
+  }
 }
 
 #endif // defined(HAVE_DNS_OVER_TLS) || defined(HAVE_DNS_OVER_HTTPS)
@@ -2807,6 +2811,20 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
     g_socketUDPSendBuffer = snd;
     g_socketUDPRecvBuffer = recv;
   });
+
+#if defined(HAVE_LIBSSL)
+    luaCtx.writeFunction("loadTLSEngine", [client](const std::string& engineName, boost::optional<std::string> defaultString) {
+      if (client) {
+        return;
+      }
+
+      auto [success, error] = libssl_load_engine(engineName, defaultString ? std::optional<std::string>(*defaultString) : std::nullopt);
+      if (!success) {
+       g_outputBuffer = "Error while trying to load TLS engine '" + engineName + "': " + error + "\n";
+       errlog("Error while trying to load TLS engine '%s': %s", engineName, error);
+      }
+    });
+#endif /* HAVE_LIBSSL */
 }
 
 vector<std::function<void(void)>> setupLua(LuaContext& luaCtx, bool client, bool configCheck, const std::string& config)
index f181d751a657d5cc3df9e0041d43eaa274efb743..99180826fb450ace9fbdb51cdd3a9d3b8c10197b 100644 (file)
@@ -383,6 +383,17 @@ void IncomingTCPConnectionState::terminateClientConnection()
     }
   }
   d_ownedConnectionsToBackend.clear();
+
+  /* let's make sure we don't have any remaining async descriptors laying around */
+  auto afds = d_handler.getAsyncFDs();
+  for (const auto afd : afds) {
+    try {
+      d_threadData.mplexer->removeReadFD(afd);
+    }
+    catch (...) {
+    }
+  }
+
   /* meaning we will no longer be 'active' when the backend
      response or timeout comes in */
   d_ioState.reset();
@@ -416,8 +427,43 @@ void IncomingTCPConnectionState::queueResponse(std::shared_ptr<IncomingTCPConnec
 
     // for the same reason we need to update the state right away, nobody will do that for us
     if (state->active()) {
-      state->d_ioState->update(iostate, handleIOCallback, state, iostate == IOState::NeedWrite ? state->getClientWriteTTD(now) : state->getClientReadTTD(now));
+      updateIO(state, iostate, now);
+    }
+  }
+}
+
+void IncomingTCPConnectionState::handleAsyncReady(int fd, FDMultiplexer::funcparam_t& param)
+{
+  auto state = boost::any_cast<std::shared_ptr<IncomingTCPConnectionState>>(param);
+
+  /* If we are here, the async jobs for this SSL* are finished
+     so we should be able to remove all FDs */
+  auto afds = state->d_handler.getAsyncFDs();
+  for (const auto afd : afds) {
+    try {
+      state->d_threadData.mplexer->removeReadFD(afd);
     }
+    catch (...) {
+    }
+  }
+
+  /* and now we restart our own I/O state machine */
+  struct timeval now;
+  gettimeofday(&now, nullptr);
+  handleIO(state, now);
+}
+
+void IncomingTCPConnectionState::updateIO(std::shared_ptr<IncomingTCPConnectionState>& state, IOState newState, const struct timeval& now)
+{
+  if (newState == IOState::Async) {
+    auto fds = state->d_handler.getAsyncFDs();
+    for (const auto fd : fds) {
+      state->d_threadData.mplexer->addReadFD(fd, handleAsyncReady, state);
+    }
+    state->d_ioState->update(IOState::Done, handleIOCallback, state);
+  }
+  else {
+    state->d_ioState->update(newState, handleIOCallback, state, newState == IOState::NeedWrite ? state->getClientWriteTTD(now) : state->getClientReadTTD(now));
   }
 }
 
@@ -1004,7 +1050,7 @@ void IncomingTCPConnectionState::handleIO(std::shared_ptr<IncomingTCPConnectionS
       state->d_ioState->update(iostate, handleIOCallback, state);
     }
     else {
-      state->d_ioState->update(iostate, handleIOCallback, state, iostate == IOState::NeedRead ? state->getClientReadTTD(now) : state->getClientWriteTTD(now));
+      updateIO(state, iostate, now);
     }
     ioGuard.release();
   }
@@ -1028,7 +1074,7 @@ void IncomingTCPConnectionState::notifyIOError(IDState&& query, const struct tim
 
       if (state->active() && iostate != IOState::Done) {
         // we need to update the state right away, nobody will do that for us
-        state->d_ioState->update(iostate, handleIOCallback, state, iostate == IOState::NeedWrite ? state->getClientWriteTTD(now) : state->getClientReadTTD(now));
+       updateIO(state, iostate, now);
       }
     }
     catch (const std::exception& e) {
index 9ed8b6b3fa4dbb697bd0bbe0714318020f1d857f..6fa76ab6a1a2415f49a9c6260ffaa5c280146932 100644 (file)
@@ -113,6 +113,8 @@ public:
 
   static void handleIO(std::shared_ptr<IncomingTCPConnectionState>& conn, const struct timeval& now);
   static void handleIOCallback(int fd, FDMultiplexer::funcparam_t& param);
+  static void handleAsyncReady(int fd, FDMultiplexer::funcparam_t& param);
+  static void updateIO(std::shared_ptr<IncomingTCPConnectionState>& state, IOState newState, const struct timeval& now);
 
   static IOState sendResponse(std::shared_ptr<IncomingTCPConnectionState>& state, const struct timeval& now, TCPResponse&& response);
   static void queueResponse(std::shared_ptr<IncomingTCPConnectionState>& state, const struct timeval& now, TCPResponse&& response);
index 6d85961bf4a97abf9f6bd517fb2e1e09e7853aa7..20aebc7bd998d27c0222e8b7ddbd32d3e2f01873 100644 (file)
@@ -107,6 +107,10 @@ If the number of queued connections ("Queued" in :func:`showTCPStats`) reaches t
 
 A different possibility is that there is not enough threads accepting new connections and distributing them to worker threads. Looking at whether the ``listenOverflows`` metric in :func:`dumpStats` increase over time will tell if we are losing TCP connections because the queue is full. In that case, since a single :func:`addLocal` or :func:`addTLSLocal` directive results in only one acceptor thread, it might useful to add more of these.
 
+For incoming and outgoing DNS over TLS support, the choice of the TLS provider (OpenSSL and GnuTLS are both supported) might yield very different results depending on the exact architecture.
+
+Since 1.7.0, incoming DNS over TLS might also benefits from experimental support for TLS acceleration engines, like Intel QAT. See :func:`loadTLSEngine`, and the `tlsAsyncMode` parameter of :func:`addTLSLocal` for more information.
+
 Rules and Lua
 -------------
 
index 5b5cc0cf83ae1033a874865b23a99deb926c8fb0..c597e803023cf49b2916e4a06fa355b290ae5bcb 100644 (file)
@@ -166,6 +166,8 @@ Listen Sockets
     ``sessionTimeout`` and ``tcpListenQueueSize`` options added.
   .. versionchanged:: 1.6.0
     ``enableRenegotiation``, ``maxConcurrentTCPConnections``, ``maxInFlight`` and ``releaseBuffers`` options added.
+  .. versionchanged:: 1.7.0
+    ``tlsAsyncMode`` option added.
 
   Listen on the specified address and TCP port for incoming DNS over TLS connections, presenting the specified X.509 certificate.
 
@@ -199,6 +201,7 @@ Listen Sockets
   * ``maxConcurrentTCPConnections=0``: int - Maximum number of concurrent incoming TCP connections. The default is 0 which means unlimited.
   * ``releaseBuffers=true``: bool - Whether OpenSSL should release its I/O buffers when a connection goes idle, saving roughly 35 kB of memory per connection.
   * ``enableRenegotiation=false``: bool - Whether secure TLS renegotiation should be enabled (OpenSSL only, the GnuTLS provider does not support it). Disabled by default since it increases the attack surface and is seldom used for DNS.
+  * ``tlsAsyncMode=false``: bool - Whether to enable experimental asynchronous TLS I/O operations if OpenSSL is used as the TLS provider and an asynchronous capable SSL engine is loaded. See also :func:`loadTLSEngine` to load the engine.
 
 .. function:: setLocal(address[, options])
 
@@ -1611,6 +1614,17 @@ Other functions
   :param int numberOfDaysOfValidity: Number of days this OCSP response should be valid.
   :param int numberOfMinutesOfValidity: Number of minutes this OCSP response should be valid, in addition to the number of days.
 
+.. function:: loadTLSEngine(engineName [, defaultString])
+
+  .. versionadded:: 1.7.0
+
+  Load the OpenSSL engine named ``engineName``, setting the engine default string to ``defaultString`` if supplied. Engines can be used to accelerate cryptographic operations, like for example Intel QAT.
+  At the moment up to a maximum of 32 loaded engines are supported, and that support is experimental.
+  Some engines might not actually decrease, and sometimes increase, the CPU usage when the TLS asynchronous mode of OpenSSL is not enabled. To enable it see the ``tlsAsyncMode`` parameter on :func:`addTLSLocal`.
+
+  :param string engineName: The name of the engine to load.
+  :param string defaultString: The default string to pass to the engine. The exact value depends on the engine but represents the algorithms to register to this engine, as a list of  comma-separated keywords. For example "RSA,EC,DSA,DH,PKEY,PKEY_CRYPTO,PKEY_ASN1".
+
 DOHFrontend
 ~~~~~~~~~~~
 
index edea2c88df6603cd5e0154f5ef3de9879882937d..87f79286b9f99381a27a798ca2b1c63a16d86fd4 100644 (file)
@@ -444,6 +444,11 @@ public:
   {
   }
 
+  std::vector<int> getAsyncFDs() override
+  {
+    return {};
+  }
+
   /* unused in that context, don't bother */
   void doHandshake() override
   {
index 1fbb00e9ceb71be4097442aa3528d448462abedb..9b81805ca35f9f7d05a0317e29da7d2abba3c673 100644 (file)
@@ -254,6 +254,11 @@ public:
     return {};
   }
 
+  std::vector<int> getAsyncFDs() override
+  {
+    return {};
+  }
+
   void setSession(std::unique_ptr<TLSSession>& session) override
   {
   }
index 2c0e8be68251e9a9a142f6dd6589f33864cfca16..776580803b6f50c10746316a17fc86ef97db2541 100644 (file)
@@ -8,9 +8,11 @@
 #include <fstream>
 #include <cstring>
 #include <mutex>
+#include <unordered_map>
 #include <pthread.h>
 
 #include <openssl/conf.h>
+#include <openssl/engine.h>
 #include <openssl/err.h>
 #include <openssl/ocsp.h>
 #include <openssl/rand.h>
@@ -63,6 +65,7 @@ static void openssl_thread_cleanup()
 #endif /* (OPENSSL_VERSION_NUMBER < 0x1010000fL || (defined LIBRESSL_VERSION_NUMBER) && LIBRESSL_VERSION_NUMBER < 0x2090100fL) */
 
 static std::atomic<uint64_t> s_users;
+static LockGuarded<std::unordered_map<std::string, std::unique_ptr<ENGINE, int(*)(ENGINE*)>>> s_engines;
 static int s_ticketsKeyIndex{-1};
 static int s_countersIndex{-1};
 static int s_keyLogIndex{-1};
@@ -104,6 +107,11 @@ void registerOpenSSLUser()
 void unregisterOpenSSLUser()
 {
   if (s_users.fetch_sub(1) == 1) {
+    for (auto& [name, engine] : *s_engines.lock()) {
+      ENGINE_finish(engine.get());
+      engine.reset();
+    }
+    s_engines.lock()->clear();
 #if (OPENSSL_VERSION_NUMBER < 0x1010000fL || (defined LIBRESSL_VERSION_NUMBER && LIBRESSL_VERSION_NUMBER < 0x2090100fL))
     ERR_free_strings();
 
@@ -119,6 +127,42 @@ void unregisterOpenSSLUser()
   }
 }
 
+std::pair<bool, std::string> libssl_load_engine(const std::string& engineName, const std::optional<std::string>& defaultString)
+{
+  if (s_users.load() == 0) {
+    /* We need to make sure that OpenSSL has been properly initialized before loading an engine.
+       This messes up our accounting a bit, so some memory might not be properly released when
+       the program exits when engines are in use. */
+    registerOpenSSLUser();
+  }
+
+  auto engines = s_engines.lock();
+  if (engines->count(engineName) > 0) {
+    return { false, "TLS engine already loaded" };
+  }
+
+  ENGINE* enginePtr = ENGINE_by_id(engineName.c_str());
+  if (enginePtr == nullptr) {
+    return { false, "unable to load TLS engine" };
+  }
+
+  auto engine = std::unique_ptr<ENGINE, int(*)(ENGINE*)>(enginePtr, ENGINE_free);
+  enginePtr = nullptr;
+
+  if (!ENGINE_init(engine.get())) {
+    return { false, "Unable to init TLS engine" };
+  }
+
+  if (defaultString) {
+    if (ENGINE_set_default_string(engine.get(), defaultString->c_str()) == 0) {
+      return { false, "error while setting the TLS engine default string" };
+    }
+  }
+
+  engines->insert({engineName, std::move(engine)});
+  return { true, "" };
+}
+
 void* libssl_get_ticket_key_callback_data(SSL* s)
 {
   SSL_CTX* sslCtx = SSL_get_SSL_CTX(s);
@@ -706,12 +750,21 @@ std::unique_ptr<SSL_CTX, void(*)(SSL_CTX*)> libssl_init_server_context(const TLS
     SSL_CTX_sess_set_cache_size(ctx.get(), config.d_maxStoredSessions);
   }
 
+  long mode = 0;
 #ifdef SSL_MODE_RELEASE_BUFFERS
   if (config.d_releaseBuffers) {
-    SSL_CTX_set_mode(ctx.get(), SSL_MODE_RELEASE_BUFFERS);
+    mode |= SSL_MODE_RELEASE_BUFFERS;
   }
 #endif
 
+#ifdef SSL_MODE_ASYNC
+  if (config.d_asyncMode) {
+    mode |= SSL_MODE_ASYNC;
+  }
+#endif
+
+  SSL_CTX_set_mode(ctx.get(), mode);
+
   /* we need to set this callback to acknowledge the server name sent by the client,
      otherwise it will not stored in the session and will not be accessible when the
      session is resumed, causing SSL_get_servername to return nullptr */
index 4561b4a496f10b7671f42a00d564f421e03104aa..db127512217a82598cee4101bc9daa5bff0bd999 100644 (file)
@@ -4,6 +4,7 @@
 #include <fstream>
 #include <map>
 #include <memory>
+#include <optional>
 #include <string>
 #include <vector>
 
@@ -37,6 +38,8 @@ public:
   bool d_releaseBuffers{true};
   /* whether so-called secure renegotiation should be allowed for TLS < 1.3 */
   bool d_enableRenegotiation{false};
+  /* enable TLS async mode, if supported by any engine */
+  bool d_asyncMode{false};
 };
 
 struct TLSErrorCounters
@@ -135,4 +138,6 @@ bool libssl_set_alpn_protos(SSL_CTX* ctx, const std::vector<std::vector<uint8_t>
 
 std::string libssl_get_error_string();
 
+std::pair<bool, std::string> libssl_load_engine(const std::string& engineName, const std::optional<std::string>& defaultString);
+
 #endif /* HAVE_LIBSSL */
index 143d93d8782adfbceff174559d88112438058a79..a716932f5f833f767f9d4e10b9da27b336dc8e26 100644 (file)
@@ -144,6 +144,28 @@ public:
     SSL_set_ex_data(d_conn.get(), s_tlsConnIndex, this);
   }
 
+  std::vector<int> getAsyncFDs() override
+  {
+    if (SSL_waiting_for_async(d_conn.get()) != 1) {
+      return {};
+    }
+
+    OSSL_ASYNC_FD fds[32];
+    size_t numfds = sizeof(fds)/sizeof(*fds);
+    SSL_get_all_async_fds(d_conn.get(), nullptr, &numfds);
+    if (numfds == 0) {
+      return {};
+    }
+
+    SSL_get_all_async_fds(d_conn.get(), fds, &numfds);
+    std::vector<int> results;
+    results.reserve(numfds);
+    for (size_t idx = 0; idx < numfds; idx++) {
+      results.push_back(fds[idx]);
+    }
+    return results;
+  }
+
   IOState convertIORequestToIOState(int res) const
   {
     int error = SSL_get_error(d_conn.get(), res);
@@ -164,6 +186,9 @@ public:
     else if (error == SSL_ERROR_ZERO_RETURN) {
       throw std::runtime_error("TLS connection closed by remote end");
     }
+    else if (error == SSL_ERROR_WANT_ASYNC) {
+      return IOState::Async;
+    }
     else {
       if (g_verbose) {
         throw std::runtime_error("Error while processing TLS connection: (" + std::to_string(error) + ") " + libssl_get_error_string());
@@ -1450,6 +1475,11 @@ public:
     return gnutls_alpn_set_protocols(d_conn.get(), values.data(), values.size(), flags);
   }
 
+  std::vector<int> getAsyncFDs() override
+  {
+    return {};
+  }
+
 private:
   std::shared_ptr<gnutls_certificate_credentials_st> d_creds;
   std::shared_ptr<GnuTLSTicketsKey> d_ticketsKey;
index 5b23e00dc3b77589d3e865a7578686080f47a9b8..87822e091b0598215f961c951a4aa52bc1cf834d 100644 (file)
@@ -9,7 +9,7 @@
 #include "misc.hh"
 #include "noinitvector.hh"
 
-enum class IOState : uint8_t { Done, NeedRead, NeedWrite };
+enum class IOState : uint8_t { Done, NeedRead, NeedWrite, Async };
 
 class TLSSession
 {
@@ -39,6 +39,7 @@ public:
   virtual std::vector<std::unique_ptr<TLSSession>> getSessions() = 0;
   virtual void setSession(std::unique_ptr<TLSSession>& session) = 0;
   virtual bool isUsable() const = 0;
+  virtual std::vector<int> getAsyncFDs() = 0;
   virtual void close() = 0;
 
   void setUnknownTicketKey()
@@ -547,6 +548,14 @@ public:
     return d_conn->isUsable();
   }
 
+  std::vector<int> getAsyncFDs()
+  {
+    if (!d_conn) {
+      return {};
+    }
+    return d_conn->getAsyncFDs();
+  }
+
   const static bool s_disableConnectForUnitTests;
 
 private: