Skip to content

Bound the TLS handshake by the connect timeout - #583

Open
iliaal wants to merge 1 commit into
ClickHouse:masterfrom
iliaal:fix/tls-handshake-timeout
Open

iliaal wants to merge 1 commit into
ClickHouse:masterfrom
iliaal:fix/tls-handshake-timeout

Conversation

@iliaal

@iliaal iliaal commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

connection_connect_timeout only bounds the TCP connect poll in SocketConnect(). SSLSocket then runs a blocking SSL_connect(), whose reads are bounded only by SO_RCVTIMEO, which comes from the receive timeout and defaults to infinite. So a peer that accepts TCP but never answers the ClientHello blocks the client forever, and failover to the next endpoint never runs. With a receive timeout set, expiry surfaces as OpenSSLError from SSL_ERROR_WANT_READ, which ResetConnectionEndpoint() doesn't rotate on either.

Fix: SSLSocket::Handshake() runs SSL_connect() non-blocking, using the new Socket::SetNonBlocking/Socket::WaitFor, and polls against the connect timeout. On expiry it throws std::system_error(ETIMEDOUT) (WSAETIMEDOUT on Windows), which every rotation site already treats as a connection failure. The socket goes back to blocking mode after the handshake. A negative timeout still waits forever, like the TCP connect. A TLS endpoint can now take up to twice the connect timeout, and the option comment says so.

Related to #486, which covers the plaintext Hello phase after the handshake; this PR doesn't change that phase.

Reproducer: point a TLS client with a short connect timeout at a listener that accepts and never writes.

Test: OpenSSLConnection.HandshakeIsBoundedByConnectTimeout uses two such tarpits, a 500 ms connect timeout and a 10 s receive timeout as a hang guard. On master it fails after about 10 s with an uncaught OpenSSLError. Here it fails over both endpoints with ETIMEDOUT in about 1 s. Only the Linux build was compiled and run; the Windows and macOS paths use the existing FIONBIO/fcntl and WSAPoll/poll wrappers.

connection_connect_timeout only covers the TCP connect poll in
SocketConnect(). SSLSocket then runs a blocking SSL_connect() whose
reads are bounded by SO_RCVTIMEO, i.e. the receive timeout, which
defaults to infinite, so a peer that accepts the connection but never
answers the ClientHello blocks the client forever and failover to the
next endpoint never happens.

I run the handshake non-blocking in SSLSocket::Handshake() and poll
against the connect timeout, throwing std::system_error(ETIMEDOUT) on
expiry so ResetConnectionEndpoint() moves on to the next endpoint.
@iliaal
iliaal requested review from mzitnik and slabko as code owners October 18, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant