Parent epic: #279 (E26: TLS Hardening)
Follow-on from: #276 (S12.14 — fail-fast streams + eager Service drain)
Sibling: mbedTLS backend parity tracked separately under E26.
Background
SolidSyslogTlsStream
(Platform/OpenSsl/Source/SolidSyslogTlsStream.c) currently creates
SSL_CTX_new(TLS_client_method()) with default options. OpenSSL's
client-side session cache is off by default.
Combined with S12.14's fail-fast reconnect model, every transient peer
hiccup now produces a fresh socket and a fresh TLS handshake. Each full
handshake costs ~2 RTTs and the asymmetric crypto on both sides. On a
flaky link this is the dominant cost of the reconnect loop and CPU on
embedded targets is already constrained.
Session resumption (RFC 5077 tickets, or session IDs) cuts the next
handshake to ~1 RTT and skips the asymmetric crypto.
Scope
- Enable
SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_CLIENT).
- Hook the new-session callback (
SSL_CTX_sess_set_new_cb) so we hold
one SSL_SESSION* per TlsStream instance and feed it via
SSL_set_session(ssl, saved) on the next handshake.
- Free the saved session on
Close / Destroy (SSL_SESSION_free).
- One session per stream is enough — we don't multiplex destinations
inside a single Stream.
- Graceful fallback. When no saved session exists, or the peer
offers no ticket, or the saved session is expired/evicted, the stream
must fall back to a full handshake and still deliver. Resumption is
best-effort — never a delivery precondition.
Verification
Two tiers, matching the established TlsStream pattern:
- Unit (fake-libssl,
Tests/SolidSyslogTlsStreamTest.cpp): assert
SSL_CTX_set_session_cache_mode is called with the client-cache flag;
assert the new-session callback path stores a session pointer; assert
SSL_set_session is called on the second connect of the same stream
instance; assert the saved session is freed on Close / Destroy.
- Integration (real libssl,
Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp,
run as the integration-linux-openssl / integration-windows-openssl
CI checks): stand up a server that offers tickets, connect, force a
reconnect on the same stream (kill the peer / RST), and assert
SSL_session_reused(ssl) == 1 on the second handshake. Add a
no-ticket case asserting a clean full-handshake fallback that still
delivers.
Note: this is not a BDD concern. SSL_session_reused() is an
in-process libssl call that the syslog-ng / Behave layer cannot
observe — the resumption assertion only exists in the integration
harness.
Out of scope / note
- mbedTLS backend parity is a separate story under E26 — the two
backends' work is independent.
- Server-side session tickets are the peer's concern (we're a client).
- TLS 1.3 0-RTT (early data) is deliberately not in scope —
separate story under the same epic if/when there's demand.
Acceptance
- Integration test forces a same-stream reconnect and observes
SSL_session_reused(ssl) == 1 on the second handshake.
- Integration test with a non-resuming peer falls back to a full
handshake and still delivers the message.
- Coverage stays at 100% line/branch.
Parent epic: #279 (E26: TLS Hardening)
Follow-on from: #276 (S12.14 — fail-fast streams + eager Service drain)
Sibling: mbedTLS backend parity tracked separately under E26.
Background
SolidSyslogTlsStream(
Platform/OpenSsl/Source/SolidSyslogTlsStream.c) currently createsSSL_CTX_new(TLS_client_method())with default options. OpenSSL'sclient-side session cache is off by default.
Combined with S12.14's fail-fast reconnect model, every transient peer
hiccup now produces a fresh socket and a fresh TLS handshake. Each full
handshake costs ~2 RTTs and the asymmetric crypto on both sides. On a
flaky link this is the dominant cost of the reconnect loop and CPU on
embedded targets is already constrained.
Session resumption (RFC 5077 tickets, or session IDs) cuts the next
handshake to ~1 RTT and skips the asymmetric crypto.
Scope
SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_CLIENT).SSL_CTX_sess_set_new_cb) so we holdone
SSL_SESSION*perTlsStreaminstance and feed it viaSSL_set_session(ssl, saved)on the next handshake.Close/Destroy(SSL_SESSION_free).inside a single Stream.
offers no ticket, or the saved session is expired/evicted, the stream
must fall back to a full handshake and still deliver. Resumption is
best-effort — never a delivery precondition.
Verification
Two tiers, matching the established TlsStream pattern:
Tests/SolidSyslogTlsStreamTest.cpp): assertSSL_CTX_set_session_cache_modeis called with the client-cache flag;assert the new-session callback path stores a session pointer; assert
SSL_set_sessionis called on the second connect of the same streaminstance; assert the saved session is freed on
Close/Destroy.Tests/OpenSslIntegration/SolidSyslogTlsStreamIntegrationTest.cpp,run as the
integration-linux-openssl/integration-windows-opensslCI checks): stand up a server that offers tickets, connect, force a
reconnect on the same stream (kill the peer / RST), and assert
SSL_session_reused(ssl) == 1on the second handshake. Add ano-ticket case asserting a clean full-handshake fallback that still
delivers.
Out of scope / note
backends' work is independent.
separate story under the same epic if/when there's demand.
Acceptance
SSL_session_reused(ssl) == 1on the second handshake.handshake and still delivers the message.