From 9c45844c2ba94e0ce616a9db70050839b58cda10 Mon Sep 17 00:00:00 2001 From: ku524 Date: Thu, 9 Jul 2026 22:12:47 +0900 Subject: [PATCH] tls: openssl: don't recycle a connection after peer close_notify tls_net_read() and tls_net_write() only set connection->net_error on SSL_ERROR_SYSCALL. When the peer sends a TLS close_notify while the underlying TCP socket stays open (for example a proxy in front of the real upstream doing a graceful TLS-only teardown, or the upstream itself), SSL_get_error() returns SSL_ERROR_ZERO_RETURN instead, which falls through to the generic error path and leaves net_error unset. flb_upstream_conn_release() only refuses to recycle a connection when net_error is set, so this half-closed connection is returned to the keepalive pool. The next reuse hits the same cached TLS shutdown state immediately, with no new network read, fails the same way, and gets recycled again -- repeating indefinitely until Retry_Limit is exhausted and the records are dropped. Set net_error to ECONNRESET on SSL_ERROR_ZERO_RETURN in both tls_net_read() and tls_net_write(), mirroring the existing SSL_ERROR_SYSCALL branch. This follows the same convention already used for non-syscall connection errors elsewhere in the codebase: net_io_propagate_critical_error() in flb_io.c already treats ECONNRESET as a critical, non-recyclable error on the plain-socket path. Once set, flb_upstream_conn_release() destroys the connection instead of recycling it, so the next flush opens a fresh connection rather than repeating the same failure. Only SSL_ERROR_ZERO_RETURN (a clean close_notify) is handled here. The connection is torn down via flb_tls_session_invalidate(), which calls SSL_shutdown() to complete the bidirectional close, valid after ZERO_RETURN. Fatal SSL_ERROR_SSL sessions are intentionally left out of scope: SSL_shutdown() must not be called after a fatal error, so recycling them safely needs a separate change to the teardown path. Verified with a local repro using a TLS receiver that injects a close_notify alert on an idle connection while keeping the TCP socket open. Before this change the same poisoned connection is recycled and reused on every subsequent flush (repeated "cannot get ack"); after this change the connection is destroyed after the single in-flight failure and the following flush establishes a fresh connection and succeeds. Addresses #12076 Signed-off-by: ku524 --- src/tls/openssl.c | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/tls/openssl.c b/src/tls/openssl.c index 53e3574ea8a..989bbc79bae 100644 --- a/src/tls/openssl.c +++ b/src/tls/openssl.c @@ -1498,6 +1498,23 @@ static int tls_net_read(struct flb_tls_session *session, ret = -1; } + else if (ret == SSL_ERROR_ZERO_RETURN) { + flb_debug("[tls] connection closed by the remote peer " + "(close_notify)"); + + /* + * The peer performed a clean TLS shutdown, so this session + * is finished. Flag the connection as errored so it is not + * reused: a reused session would keep hitting the cached + * shutdown state and fail every subsequent read identically. + * For a pooled upstream connection this makes + * flb_upstream_conn_release() destroy it instead of + * returning it to the keepalive pool. + */ + session->connection->net_error = ECONNRESET; + + ret = -1; + } else if (ret < 0) { err_code = ERR_get_error(); @@ -1580,6 +1597,23 @@ static int tls_net_write(struct flb_tls_session *session, ret = -1; } + else if (ssl_ret == SSL_ERROR_ZERO_RETURN) { + flb_debug("[tls] connection closed by the remote peer " + "(close_notify)"); + + /* + * The peer performed a clean TLS shutdown, so this session + * is finished. Flag the connection as errored so it is not + * reused: a reused session would keep hitting the cached + * shutdown state and fail every subsequent operation + * identically. For a pooled upstream connection this makes + * flb_upstream_conn_release() destroy it instead of + * returning it to the keepalive pool. + */ + session->connection->net_error = ECONNRESET; + + ret = -1; + } else { err_code = ERR_get_error(); if (err_code == 0) {