Skip to content

Commit c6c39e4

Browse files
authored
quic: add TLS session ticket resumption support (#42734)
Commit Message: quic: add session ticket resumption support using configured session ticket keys Additional Description: ## Summary TLS session resumption is essential for QUIC performance. Without it, every connection requires a full TLS handshake, and 0-RTT becomes meaningless since there's no session state to resume from. As noted in #42682, TLS-related data accounts for roughly 1/3 of bytes during connection establishment - session resumption eliminates most of this overhead. Currently, Envoy's QUIC implementation does not support session resumption across workers or processes. While users can configure `session_ticket_keys` or `session_ticket_keys_sds_secret_config` in downstream TLS context, these settings have no effect on QUIC connections. This limitation is documented in #25418, which explicitly states that session ticket key plumbing is missing from the QUIC implementation. This PR bridges that gap by enabling QUIC to use the same session ticket keys configured for TCP TLS, allowing session resumption to work across workers and processes. ## Implementation We subclass QUICHE's `TlsServerHandshaker` as `EnvoyTlsServerHandshaker` and install a session-ticket key callback on the shared QUICHE `SSL_CTX`. The callback reuses `ServerContextImpl::sessionTicketProcess()` so QUIC and TCP TLS share identical session-ticket handling (same keys, same format, same rotation semantics). **Key design decisions:** 1. **Per-connection pinning of `ServerContextImpl`**: Each `EnvoyTlsServerHandshaker` holds a `ServerContextSharedPtr` captured at connection creation, and stores `this` in SSL ex_data. The static ticket callback retrieves the handshaker from ex_data and delegates to the pinned context's `sessionTicketProcess()`. Because the shared pointer keeps the context alive, an SDS update that rotates the factory's active context does not invalidate in-flight connections — matching TCP TLS behavior where each connection is bound to the `ServerContextImpl` active at connection creation. 2. **`SSL_CTX_set_tlsext_ticket_key_cb` over `SSL_CTX_set_ticket_aead_method`**: We use the same callback mechanism as TCP TLS rather than QUICHE's `TicketCrypter` interface, so `ServerContextImpl::sessionTicketProcess()` can be reused unchanged. 3. **Graceful fallback**: If the runtime guard is toggled between `OnNewSslCtx` (which installs the callback on the shared `SSL_CTX`) and connection creation (which may fall back to the vanilla handshaker), the ticket callback finds a null handshaker in ex_data and returns 0 to skip ticket issuance for that connection rather than crashing. ## Flow ``` Server startup (once per SSL_CTX): EnvoyQuicProofSource::OnNewSslCtx() └─ if runtime flag on: SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx, EnvoyTlsServerHandshaker::ticketKeyCallback) Per connection: EnvoyQuicCryptoServerStreamFactoryImpl::createEnvoyQuicCryptoServerStream() └─ reads SessionTicketConfig from QuicServerTransportSocketFactory └─ constructs EnvoyTlsServerHandshaker(session, crypto_config, factory.sslCtx(), disable_resumption) └─ pins ServerContextSharedPtr └─ SSL_set_ex_data(ssl, handshakerExDataIndex(), this) └─ if disable_resumption || no ticket keys: DisableResumption() // SSL_OP_NO_TICKET During handshake (BoringSSL-driven): ticketKeyCallback(ssl, ...) └─ handshaker = SSL_get_ex_data(ssl, handshakerExDataIndex()) └─ if null: return 0 // guard toggled after OnNewSslCtx — skip ticket └─ return handshaker->pinnedServerContext()->sessionTicketProcess(ssl, ...) ``` Risk Level: Low (behind runtime guard, disabled by default) Testing: New unit tests for `EnvoyTlsServerHandshaker` and `EnvoyQuicProofSource`; new integration coverage in `sds_dynamic_integration_test` (`SessionTicketKeysViaSds`, `SessionTicketKeysRemovedViaSds`) and in `quic_http_integration_test` (`SessionTicketResumptionWithStaticKeys`, `NoSessionTicketResumptionWithoutKeys`). Docs Changes: N/A Release Notes: Added Platform Specific Features: N/A [Optional Runtime guard:] `envoy.reloadable_features.quic_session_ticket_support` (default: false) [Optional Fixes #Issue] Partially addresses #25418 --------- Signed-off-by: Doogie Min <doogie.min@sendbird.com>
1 parent 7ea91de commit c6c39e4

18 files changed

Lines changed: 504 additions & 11 deletions

changelogs/current.yaml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,13 @@ new_features:
9696
RSA public key exchange on behalf of the client. Added a new
9797
:ref:`downstream_ssl <envoy_v3_api_field_extensions.filters.network.mysql_proxy.v3.MySQLProxy.downstream_ssl>`
9898
config option with ``DISABLE``, ``REQUIRE``, and ``ALLOW`` modes.
99+
- area: quic
100+
change: |
101+
Added support for TLS session ticket resumption in QUIC using configured session ticket keys from
102+
:ref:`session_ticket_keys <envoy_v3_api_field_extensions.transport_sockets.tls.v3.DownstreamTlsContext.session_ticket_keys>`.
103+
This enables faster reconnection across server instances by allowing clients to resume TLS sessions
104+
without full handshakes. The feature is disabled by default and can be enabled by setting runtime guard
105+
``envoy.reloadable_features.quic_session_ticket_support`` to ``true``.
99106
- area: composite
100107
change: |
101108
Added support for the
@@ -104,5 +111,4 @@ new_features:
104111
instead of using the :ref:`ExtensionWithMatcher
105112
<envoy_v3_api_msg_extensions.common.matching.v3.ExtensionWithMatcher>` filter.
106113
107-
108114
deprecated:

source/common/quic/BUILD

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,19 @@ envoy_cc_library(
122122
]),
123123
)
124124

125+
envoy_cc_library(
126+
name = "envoy_tls_server_handshaker",
127+
srcs = envoy_select_enable_http3(["envoy_tls_server_handshaker.cc"]),
128+
hdrs = envoy_select_enable_http3(["envoy_tls_server_handshaker.h"]),
129+
external_deps = ["ssl"],
130+
deps = envoy_select_enable_http3([
131+
"//source/common/common:assert_lib",
132+
"//source/common/common:macros",
133+
"//source/common/tls:server_context_lib",
134+
"@quiche//:quic_server_session_lib",
135+
]),
136+
)
137+
125138
envoy_cc_library(
126139
name = "envoy_quic_proof_source_lib",
127140
srcs = envoy_select_enable_http3(["envoy_quic_proof_source.cc"]),
@@ -130,6 +143,7 @@ envoy_cc_library(
130143
deps = envoy_select_enable_http3([
131144
":envoy_quic_proof_source_base_lib",
132145
":envoy_quic_utils_lib",
146+
":envoy_tls_server_handshaker",
133147
":quic_io_handle_wrapper_lib",
134148
":quic_transport_socket_factory_lib",
135149
"//envoy/ssl:tls_certificate_config_interface",

source/common/quic/envoy_quic_proof_source.cc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
#include "envoy/ssl/tls_certificate_config.h"
66

7+
#include "source/common/common/assert.h"
78
#include "source/common/quic/envoy_quic_utils.h"
9+
#include "source/common/quic/envoy_tls_server_handshaker.h"
810
#include "source/common/quic/quic_io_handle_wrapper.h"
911
#include "source/common/stream_info/stream_info_impl.h"
1012

@@ -113,7 +115,12 @@ void EnvoyQuicProofSource::updateFilterChainManager(
113115
filter_chain_manager_ = &filter_chain_manager;
114116
}
115117

116-
void EnvoyQuicProofSource::OnNewSslCtx(SSL_CTX* ssl_ctx) { registerCertCompression(ssl_ctx); }
118+
void EnvoyQuicProofSource::OnNewSslCtx(SSL_CTX* ssl_ctx) {
119+
registerCertCompression(ssl_ctx);
120+
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.quic_session_ticket_support")) {
121+
SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx, EnvoyTlsServerHandshaker::ticketKeyCallback);
122+
}
123+
}
117124

118125
} // namespace Quic
119126
} // namespace Envoy

source/common/quic/envoy_quic_server_crypto_stream_factory.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
#include "quiche/quic/core/crypto/quic_crypto_server_config.h"
88
#include "quiche/quic/core/quic_crypto_server_stream_base.h"
99
#include "quiche/quic/core/quic_session.h"
10-
#include "quiche/quic/core/tls_server_handshaker.h"
1110

1211
namespace Envoy {
1312
namespace Quic {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#include "source/common/quic/envoy_tls_server_handshaker.h"
2+
3+
#include "source/common/common/macros.h"
4+
5+
namespace Envoy {
6+
namespace Quic {
7+
8+
EnvoyTlsServerHandshaker::EnvoyTlsServerHandshaker(
9+
quic::QuicSession* session, const quic::QuicCryptoServerConfig* crypto_config,
10+
Ssl::ServerContextSharedPtr pinned_ssl_ctx, bool disable_resumption)
11+
: TlsServerHandshaker(session, crypto_config), pinned_ssl_ctx_(std::move(pinned_ssl_ctx)) {
12+
SSL_set_ex_data(ssl(), handshakerExDataIndex(), this);
13+
// Also check the pinned context for keys: the factory is shared across workers and
14+
// config_ may reflect an SDS update before ssl_ctx_ is swapped on the main thread.
15+
if (disable_resumption || !pinnedServerContext()->hasSessionTicketKeys()) {
16+
DisableResumption();
17+
}
18+
}
19+
20+
int EnvoyTlsServerHandshaker::handshakerExDataIndex() {
21+
CONSTRUCT_ON_FIRST_USE(int, []() -> int {
22+
int index = SSL_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr);
23+
RELEASE_ASSERT(index >= 0, "Failed to allocate SSL ex_data index for handshaker");
24+
return index;
25+
}());
26+
}
27+
28+
int EnvoyTlsServerHandshaker::ticketKeyCallback(SSL* ssl, uint8_t* key_name, uint8_t* iv,
29+
EVP_CIPHER_CTX* ctx, HMAC_CTX* hmac_ctx,
30+
int encrypt) {
31+
auto* handshaker =
32+
static_cast<EnvoyTlsServerHandshaker*>(SSL_get_ex_data(ssl, handshakerExDataIndex()));
33+
if (handshaker == nullptr || handshaker->pinnedServerContext() == nullptr) {
34+
// Null handshaker can occur if the runtime guard was toggled between
35+
// OnNewSslCtx (which installed this callback on the SSL_CTX) and
36+
// connection creation (which fell back to the vanilla TlsServerHandshaker).
37+
// Return 0 to disable ticket for this connection — graceful fallback.
38+
return 0;
39+
}
40+
return handshaker->pinnedServerContext()->sessionTicketProcess(ssl, key_name, iv, ctx, hmac_ctx,
41+
encrypt);
42+
}
43+
44+
} // namespace Quic
45+
} // namespace Envoy
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#pragma once
2+
3+
#include <openssl/ssl.h>
4+
5+
#include "source/common/common/assert.h"
6+
#include "source/common/tls/server_context_impl.h"
7+
8+
#include "quiche/quic/core/tls_server_handshaker.h"
9+
10+
namespace Envoy {
11+
namespace Quic {
12+
13+
// TlsServerHandshaker subclass for QUIC session ticket handling.
14+
//
15+
// The session ticket key callback is installed on the shared QUICHE ssl
16+
// context, so every connection reaches the same callback regardless of which
17+
// filter chain served it. To find the right session ticket keys at callback
18+
// time, each connection pins a shared pointer to its ServerContextImpl in
19+
// ssl ex data at creation time. The pinned pointer keeps the context alive
20+
// for the connection even after an SDS update rotates the factory's active
21+
// context, and it matches TCP TLS behavior where each connection is bound
22+
// to the ServerContextImpl that was current at connection creation.
23+
class EnvoyTlsServerHandshaker : public quic::TlsServerHandshaker {
24+
public:
25+
EnvoyTlsServerHandshaker(quic::QuicSession* session,
26+
const quic::QuicCryptoServerConfig* crypto_config,
27+
Ssl::ServerContextSharedPtr pinned_ssl_ctx, bool disable_resumption);
28+
29+
// Session ticket key callback installed on the QUICHE ssl context.
30+
// Retrieves the handshaker from ssl ex_data and delegates to the pinned
31+
// ServerContextImpl::sessionTicketProcess().
32+
static int ticketKeyCallback(SSL* ssl, uint8_t* key_name, uint8_t* iv, EVP_CIPHER_CTX* ctx,
33+
HMAC_CTX* hmac_ctx, int encrypt);
34+
35+
// SSL ex_data index for storing the handshaker pointer per-connection.
36+
static int handshakerExDataIndex();
37+
38+
private:
39+
// QuicServerTransportSocketFactory always creates ServerContextImpl,
40+
// so this downcast is safe for all QUIC connections.
41+
Extensions::TransportSockets::Tls::ServerContextImpl* pinnedServerContext() const {
42+
return static_cast<Extensions::TransportSockets::Tls::ServerContextImpl*>(
43+
pinned_ssl_ctx_.get());
44+
}
45+
46+
Ssl::ServerContextSharedPtr pinned_ssl_ctx_;
47+
};
48+
49+
} // namespace Quic
50+
} // namespace Envoy

source/common/quic/quic_server_transport_socket_factory.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,34 @@ class QuicServerTransportSocketFactory : public Network::DownstreamTransportSock
3838

3939
bool earlyDataEnabled() const { return enable_early_data_; }
4040

41+
struct SessionTicketConfig {
42+
// True when session ticket encryption keys are explicitly configured via
43+
// session_ticket_keys or session_ticket_keys_sds_secret_config. Without
44+
// keys, the server cannot encrypt or decrypt session tickets.
45+
bool has_keys;
46+
// True when disable_stateless_session_resumption is set in
47+
// DownstreamTlsContext. When enabled, the server will not issue session
48+
// tickets and clients must perform full handshakes on every connection.
49+
bool disable_stateless_resumption;
50+
// True when an external mechanism (e.g., SDS provider) manages session
51+
// resumption including ticket encryption/decryption. When set, Envoy
52+
// should not install its own session ticket key processing callback.
53+
bool handles_session_resumption;
54+
};
55+
56+
SessionTicketConfig getSessionTicketConfig() const {
57+
return {!config_->sessionTicketKeys().empty(), config_->disableStatelessSessionResumption(),
58+
config_->capabilities().handles_session_resumption};
59+
}
60+
61+
// Returns the current ServerContextImpl, pinning a shared_ptr so it
62+
// remains valid for the caller's lifetime. May return null before
63+
// initialize() completes or if context creation failed.
64+
Ssl::ServerContextSharedPtr sslCtx() const {
65+
absl::ReaderMutexLock l(ssl_ctx_mu_);
66+
return ssl_ctx_;
67+
}
68+
4169
protected:
4270
QuicServerTransportSocketFactory(bool enable_early_data, Stats::Scope& store,
4371
Ssl::ServerContextConfigPtr config,

source/common/runtime/runtime_features.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ FALSE_RUNTIME_GUARD(envoy_reloadable_features_always_use_v6);
145145
FALSE_RUNTIME_GUARD(envoy_restart_features_upstream_http_filters_with_tcp_proxy);
146146
// TODO(danzh) false deprecate it once QUICHE has its own enable/disable flag.
147147
FALSE_RUNTIME_GUARD(envoy_reloadable_features_quic_reject_all);
148+
// TODO(doogie): Flip to true once QUIC session ticket support is stable.
149+
FALSE_RUNTIME_GUARD(envoy_reloadable_features_quic_session_ticket_support);
148150
// TODO(#10646) change to true when UHV is sufficiently tested
149151
// For more information about Universal Header Validation, please see
150152
// https://github.com/envoyproxy/envoy/issues/10646

source/common/tls/server_context_impl.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ class ServerContextImpl : public ContextImpl,
7070

7171
Ssl::CurveNIDVector getClientEcdsaCapabilities(const SSL_CLIENT_HELLO& ssl_client_hello) const;
7272

73+
int sessionTicketProcess(SSL* ssl, uint8_t* key_name, uint8_t* iv, EVP_CIPHER_CTX* ctx,
74+
HMAC_CTX* hmac_ctx, int encrypt);
75+
bool hasSessionTicketKeys() const { return !session_ticket_keys_.empty(); }
76+
7377
protected:
7478
ServerContextImpl(
7579
Stats::Scope& scope, const Envoy::Ssl::ServerContextConfig& config,
@@ -82,8 +86,6 @@ class ServerContextImpl : public ContextImpl,
8286

8387
int alpnSelectCallback(const unsigned char** out, unsigned char* outlen, const unsigned char* in,
8488
unsigned int inlen);
85-
int sessionTicketProcess(SSL* ssl, uint8_t* key_name, uint8_t* iv, EVP_CIPHER_CTX* ctx,
86-
HMAC_CTX* hmac_ctx, int encrypt);
8789

8890
absl::StatusOr<SessionContextID>
8991
generateHashForSessionContextId(const std::vector<std::string>& server_names);

source/extensions/quic/crypto_stream/BUILD

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ envoy_cc_library(
2727
deps = envoy_select_enable_http3([
2828
"//envoy/registry",
2929
"//source/common/quic:envoy_quic_server_crypto_stream_factory_lib",
30+
"//source/common/quic:envoy_tls_server_handshaker",
31+
"//source/common/quic:quic_server_transport_socket_factory_lib",
3032
"@envoy_api//envoy/extensions/quic/crypto_stream/v3:pkg_cc_proto",
33+
"@quiche//:quic_server_session_lib",
3134
]),
3235
alwayslink = LEGACY_ALWAYSLINK,
3336
)

0 commit comments

Comments
 (0)