tls: openssl: don't recycle a connection after peer close_notify#12079
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough
ChangesTLS close_notify handling
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Our commit linter complains as below: Commit subject and prefix are reasonable for us but we should avoid to use |
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 fluent#12076 Signed-off-by: ku524 <yeonjuyeong@gmail.com>
a6c3cf7 to
9c45844
Compare
|
Thanks for the quick review! Good catch — that was a line in the commit body that wrapped to start with I've reworded the body so no line starts with a |
cosmo0920
left a comment
There was a problem hiding this comment.
Looks good to me.
I'm also confusing why the TLS connections can be easily disconnected and reconnected.
This patch should resolve this phenomenon.
Summary
out_forward(and every other plugin using the TLS-enabled upstream pool) can end up reusing a TLS connection that the peer already half-closed, causing the same connection to fail identically on every reuse untilRetry_Limitis exhausted and records are dropped.Root cause:
tls_net_read()/tls_net_write()insrc/tls/openssl.conly setconnection->net_errorwhenSSL_get_error()returnsSSL_ERROR_SYSCALL. When the peer sends a TLSclose_notifywhile the TCP socket itself stays open — e.g. a TLS-terminating proxy (istio/envoy) in front of the real upstream doing a graceful TLS-only teardown —SSL_get_error()returnsSSL_ERROR_ZERO_RETURNinstead, which falls through to the generic error path without settingnet_error.flb_upstream_conn_release()only refuses to recycle a connection whennet_erroris set, so this half-closed connection goes right back into the keepalive pool. The next time it's handed out, the cached TLS shutdown state makes the read fail again immediately (no new network I/O), it gets recycled again, and the cycle repeats indefinitely.Fix: set
connection->net_error = ECONNRESETonSSL_ERROR_ZERO_RETURNin bothtls_net_read()andtls_net_write(), mirroring the existingSSL_ERROR_SYSCALLbranch. This is the same convention already used for non-syscall connection errors elsewhere in the codebase —flb_io.c'snet_io_propagate_critical_error()already treatsECONNRESETas 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 instead of repeating the same failure.Scope: only
SSL_ERROR_ZERO_RETURN(a cleanclose_notify) is handled here. The connection is torn down viaflb_tls_session_invalidate(), which callsSSL_shutdown()to complete the bidirectional close — which is valid afterZERO_RETURN. I deliberately leftSSL_ERROR_SSL(fatal protocol error) out:SSL_shutdown()must not be called after a fatal error, so making those sessions non-recyclable safely would need a separate change to the teardown path. Happy to follow up on that separately if maintainers want it.Addresses #12076
Relation to #12046
#12046 force-closes the connection on any non-
FLB_OKflush result inout_forward, which would incidentally also cover this case (a failed ack read returns non-OK). Its enumerated triggers are partial-write / handshake /WOULDBLOCKthough, not the TLS-readZERO_RETURNpath, and it compensates at the plugin level. This PR fixes the same class of problem one layer down, at the TLS transport (net_errorgating inflb_upstream_conn_release()), so it benefits every consumer of the upstream pool, not justout_forward. I see these as complementary rather than overlapping — happy to adjust scope if maintainers see it differently.Verification
There's no packet-level capture of the
close_notifyitself (TLS 1.3 encrypts alerts) —SSL_ERROR_ZERO_RETURNis, by definition, the evidence that one was received. To verify the mechanism end-to-end without relying on a real proxy, I built a minimal TLS receiver that injects a real OpenSSL-encryptedclose_notifyalert on an established connection viassl.MemoryBIOwhile leaving the underlying TCP socket open — the same condition a TLS-terminating proxy would produce.Config used (sender):
Before the fix — the poisoned connection is recycled and reused on every flush, failing identically each time:
After the fix — exactly one failure (the unavoidable in-flight loss), then the connection is destroyed and a fresh one takes over:
Valgrind (Debian/Docker, since Valgrind doesn't support Apple Silicon macOS): ran the same before/after repro under
valgrind --leak-check=fullagainst a debug build (-DFLB_VALGRIND=On):Also ran the existing
tests/internal/upstream_tls.cunit test against the change — passes unchanged.I did not add a dedicated C regression test:
tls_net_read()/tls_net_write()arestatic, and the existingupstream_tls.cexercises the release/destroy path through a mock TLS backend, so a deterministic test of this branch would need real-OpenSSLclose_notify-while-TCP-open injection infrastructure that isn't present in the internal test suite today. The Python receiver above is that reproduction. Glad to contribute a reusable test harness for this if maintainers would like one.Enter
[N/A]in the box, if an item is not applicable to your change.Testing
Before we can approve your change; please submit the following in a comment:
If this is a change to packaging of containers or native binaries then please confirm it works for all targets.
ok-package-testlabel to test for all targets (requires maintainer to do).Documentation
Backporting
The reported case reproduces on 3.2.3 as well as current master — happy to open a backport PR against the current stable branch if maintainers want one.
Fluent Bit is licensed under Apache 2.0, by submitting this pull request I understand that this code will be released under the terms of that license.
Summary by CodeRabbit