diff --git a/doc/09-object-types.md b/doc/09-object-types.md index 4ee242019..b1273516c 100644 --- a/doc/09-object-types.md +++ b/doc/09-object-types.md @@ -1418,6 +1418,7 @@ Configuration Attributes: cipher\_list | String | **Optional.** Cipher list that is allowed. For a list of available ciphers run `openssl ciphers`. Defaults to `ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:AES256-GCM-SHA384:AES128-GCM-SHA256`. tls\_protocolmin | String | **Optional.** Minimum TLS protocol version. Defaults to `TLSv1.2`. insecure\_noverify | Boolean | **Optional.** Whether not to verify the peer. + connect\_timeout | Number | **Optional.** Timeout for establishing new connections. Within this time, the TCP, TLS (if enabled) and Redis handshakes must complete. Defaults to `15s`. ### IdoMySqlConnection diff --git a/lib/base/tlsutility.hpp b/lib/base/tlsutility.hpp index 0bc1a8332..51bed97c1 100644 --- a/lib/base/tlsutility.hpp +++ b/lib/base/tlsutility.hpp @@ -28,6 +28,7 @@ namespace icinga const char * const DEFAULT_TLS_CIPHERS = "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:AES256-GCM-SHA384:AES128-GCM-SHA256"; const char * const DEFAULT_TLS_PROTOCOLMIN = "TLSv1.2"; +const unsigned int DEFAULT_CONNECT_TIMEOUT = 15; void InitializeOpenSSL(); diff --git a/lib/icingadb/icingadb.cpp b/lib/icingadb/icingadb.cpp index 6cfa81d72..a7eb3753e 100644 --- a/lib/icingadb/icingadb.cpp +++ b/lib/icingadb/icingadb.cpp @@ -66,7 +66,7 @@ void IcingaDB::Start(bool runtimeCreated) m_Rcon = new RedisConnection(GetHost(), GetPort(), GetPath(), GetPassword(), GetDbIndex(), GetEnableTls(), GetInsecureNoverify(), GetCertPath(), GetKeyPath(), GetCaPath(), GetCrlPath(), - GetTlsProtocolmin(), GetCipherList(), GetDebugInfo()); + GetTlsProtocolmin(), GetCipherList(), GetConnectTimeout(), GetDebugInfo()); auto connectedCallback ([this](boost::asio::yield_context& yc) { m_WorkQueue.Enqueue([this]() { OnConnectedHandler(); }); @@ -89,7 +89,7 @@ void IcingaDB::Start(bool runtimeCreated) m_Rcons[ctype] = new RedisConnection(GetHost(), GetPort(), GetPath(), GetPassword(), GetDbIndex(), GetEnableTls(), GetInsecureNoverify(), GetCertPath(), GetKeyPath(), GetCaPath(), GetCrlPath(), - GetTlsProtocolmin(), GetCipherList(), GetDebugInfo(), m_Rcon); + GetTlsProtocolmin(), GetCipherList(), GetConnectTimeout(), GetDebugInfo(), m_Rcon); } m_StatsTimer = new Timer(); @@ -175,6 +175,15 @@ void IcingaDB::ValidateTlsProtocolmin(const Lazy& lvalue, const Validati } } +void IcingaDB::ValidateConnectTimeout(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::ValidateConnectTimeout(lvalue, utils); + + if (lvalue() <= 0) { + BOOST_THROW_EXCEPTION(ValidationError(this, { "connect_timeout" }, "Value must be greater than 0.")); + } +} + void IcingaDB::AssertOnWorkQueue() { ASSERT(m_WorkQueue.IsWorkerThread()); diff --git a/lib/icingadb/icingadb.hpp b/lib/icingadb/icingadb.hpp index 07ed75904..2f7d0f98c 100644 --- a/lib/icingadb/icingadb.hpp +++ b/lib/icingadb/icingadb.hpp @@ -41,6 +41,7 @@ public: protected: void ValidateTlsProtocolmin(const Lazy& lvalue, const ValidationUtils& utils) override; + void ValidateConnectTimeout(const Lazy& lvalue, const ValidationUtils& utils) override; private: class DumpedGlobals diff --git a/lib/icingadb/icingadb.ti b/lib/icingadb/icingadb.ti index 35299573b..3e020282a 100644 --- a/lib/icingadb/icingadb.ti +++ b/lib/icingadb/icingadb.ti @@ -40,6 +40,10 @@ class IcingaDB : ConfigObject [config] String tls_protocolmin { default {{{ return DEFAULT_TLS_PROTOCOLMIN; }}} }; + + [config] double connect_timeout { + default {{{ return DEFAULT_CONNECT_TIMEOUT; }}} + }; }; } diff --git a/lib/icingadb/redisconnection.cpp b/lib/icingadb/redisconnection.cpp index 985aebe57..9c5796043 100644 --- a/lib/icingadb/redisconnection.cpp +++ b/lib/icingadb/redisconnection.cpp @@ -32,19 +32,19 @@ boost::regex RedisConnection::m_ErrAuth ("\\AERR AUTH "); RedisConnection::RedisConnection(const String& host, int port, const String& path, const String& password, int db, bool useTls, bool insecure, const String& certPath, const String& keyPath, const String& caPath, const String& crlPath, - const String& tlsProtocolmin, const String& cipherList, DebugInfo di, const RedisConnection::Ptr& parent) + const String& tlsProtocolmin, const String& cipherList, double connectTimeout, DebugInfo di, const RedisConnection::Ptr& parent) : RedisConnection(IoEngine::Get().GetIoContext(), host, port, path, password, db, - useTls, insecure, certPath, keyPath, caPath, crlPath, tlsProtocolmin, cipherList, std::move(di), parent) + useTls, insecure, certPath, keyPath, caPath, crlPath, tlsProtocolmin, cipherList, connectTimeout, std::move(di), parent) { } RedisConnection::RedisConnection(boost::asio::io_context& io, String host, int port, String path, String password, int db, bool useTls, bool insecure, String certPath, String keyPath, String caPath, String crlPath, - String tlsProtocolmin, String cipherList, DebugInfo di, const RedisConnection::Ptr& parent) + String tlsProtocolmin, String cipherList, double connectTimeout, DebugInfo di, const RedisConnection::Ptr& parent) : m_Host(std::move(host)), m_Port(port), m_Path(std::move(path)), m_Password(std::move(password)), m_DbIndex(db), m_CertPath(std::move(certPath)), m_KeyPath(std::move(keyPath)), m_Insecure(insecure), m_CaPath(std::move(caPath)), m_CrlPath(std::move(crlPath)), m_TlsProtocolmin(std::move(tlsProtocolmin)), - m_CipherList(std::move(cipherList)), m_DebugInfo(std::move(di)), m_Connecting(false), m_Connected(false), + m_CipherList(std::move(cipherList)), m_ConnectTimeout(connectTimeout), m_DebugInfo(std::move(di)), m_Connecting(false), m_Connected(false), m_Started(false), m_Strand(io), m_QueuedWrites(io), m_QueuedReads(io), m_LogStatsTimer(io), m_Parent(parent) { if (useTls && m_Path.IsEmpty()) { @@ -271,6 +271,8 @@ void RedisConnection::Connect(asio::yield_context& yc) auto conn (Shared::Make(m_Strand.context(), *m_TLSContext, m_Host)); auto& tlsConn (conn->next_layer()); + auto connectTimeout (MakeTimeout(conn)); + Defer cancelTimeout ([&connectTimeout]() { connectTimeout->Cancel(); }); if (!m_Insecure) { auto native (tlsConn.native_handle()); @@ -305,6 +307,9 @@ void RedisConnection::Connect(asio::yield_context& yc) << "Trying to connect to Redis server (async) on host '" << m_Host << ":" << m_Port << "'"; auto conn (Shared::Make(m_Strand.context())); + auto connectTimeout (MakeTimeout(conn)); + Defer cancelTimeout ([&connectTimeout]() { connectTimeout->Cancel(); }); + icinga::Connect(conn->next_layer(), m_Host, Convert::ToString(m_Port), yc); Handshake(conn, yc); m_TcpConn = std::move(conn); @@ -314,6 +319,9 @@ void RedisConnection::Connect(asio::yield_context& yc) << "Trying to connect to Redis server (async) on unix socket path '" << m_Path << "'"; auto conn (Shared::Make(m_Strand.context())); + auto connectTimeout (MakeTimeout(conn)); + Defer cancelTimeout ([&connectTimeout]() { connectTimeout->Cancel(); }); + conn->next_layer().async_connect(Unix::endpoint(m_Path.CStr()), yc); Handshake(conn, yc); m_UnixConn = std::move(conn); diff --git a/lib/icingadb/redisconnection.hpp b/lib/icingadb/redisconnection.hpp index a6408a930..0a9bfd61d 100644 --- a/lib/icingadb/redisconnection.hpp +++ b/lib/icingadb/redisconnection.hpp @@ -76,7 +76,7 @@ namespace icinga RedisConnection(const String& host, int port, const String& path, const String& password, int db, bool useTls, bool insecure, const String& certPath, const String& keyPath, const String& caPath, const String& crlPath, - const String& tlsProtocolmin, const String& cipherList, DebugInfo di, const Ptr& parent = nullptr); + const String& tlsProtocolmin, const String& cipherList, double connectTimeout, DebugInfo di, const Ptr& parent = nullptr); void UpdateTLSContext(); @@ -157,7 +157,7 @@ namespace icinga RedisConnection(boost::asio::io_context& io, String host, int port, String path, String password, int db, bool useTls, bool insecure, String certPath, String keyPath, String caPath, String crlPath, - String tlsProtocolmin, String cipherList, DebugInfo di, const Ptr& parent); + String tlsProtocolmin, String cipherList, double connectTimeout, DebugInfo di, const Ptr& parent); void Connect(boost::asio::yield_context& yc); void ReadLoop(boost::asio::yield_context& yc); @@ -179,6 +179,9 @@ namespace icinga template void Handshake(StreamPtr& stream, boost::asio::yield_context& yc); + template + Timeout::Ptr MakeTimeout(StreamPtr& stream); + String m_Path; String m_Host; int m_Port; @@ -192,6 +195,7 @@ namespace icinga String m_CrlPath; String m_TlsProtocolmin; String m_CipherList; + double m_ConnectTimeout; DebugInfo m_DebugInfo; boost::asio::io_context::strand m_Strand; @@ -454,6 +458,27 @@ void RedisConnection::Handshake(StreamPtr& strm, boost::asio::yield_context& yc) } } +/** + * Creates a Timeout which cancels stream's I/O after m_ConnectTimeout + * + * @param stream Redis server connection + */ +template +Timeout::Ptr RedisConnection::MakeTimeout(StreamPtr& stream) +{ + Ptr keepAlive (this); + + return new Timeout( + m_Strand.context(), + m_Strand, + boost::posix_time::microseconds(intmax_t(m_ConnectTimeout * 1000000)), + [keepAlive, stream](boost::asio::yield_context yc) { + boost::system::error_code ec; + stream->lowest_layer().cancel(ec); + } + ); +} + /** * Read a Redis protocol value from stream * diff --git a/lib/remote/apilistener.ti b/lib/remote/apilistener.ti index d62402b6e..8317abc4b 100644 --- a/lib/remote/apilistener.ti +++ b/lib/remote/apilistener.ti @@ -45,7 +45,7 @@ class ApiListener : ConfigObject }; [config] double connect_timeout { - default {{{ return 15.0; }}} + default {{{ return DEFAULT_CONNECT_TIMEOUT; }}} }; [config, no_user_view, no_user_modify] String ticket_salt;