Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4a0cf75
quic: add TLS session ticket resumption support
bellatoris Dec 18, 2025
d368288
fix: format issues
bellatoris Dec 30, 2025
c19d430
Resolve merge conflict in changelogs/current.yaml
bellatoris Jan 13, 2026
52cd01c
merge main
bellatoris Jan 16, 2026
a290a7e
merge main
bellatoris Jan 30, 2026
9295202
merge upstream/main
bellatoris Mar 11, 2026
2631e06
Address review comments and fix QUICHE API compatibility
bellatoris Mar 17, 2026
8060975
Merge remote-tracking branch 'upstream/main' into doogie/quic-proof-s…
bellatoris Mar 17, 2026
81ea972
fix: add 'codepoint' to spelling dictionary
bellatoris Mar 17, 2026
2123532
test: add coverage tests for QUIC session ticket resumption
bellatoris Mar 17, 2026
6231e2c
test: add coverage tests for QUIC session ticket resumption
bellatoris Mar 20, 2026
5c6f897
test: extract ticketKeyCallback for testability and add callback cove…
bellatoris Mar 20, 2026
fccf79f
test: add old GetCertChain API path coverage for handshaker SelectCer…
bellatoris Mar 20, 2026
e660c96
refactor: eliminate custom EnvoyTlsServerHandshaker, use QUICHE default
bellatoris Mar 24, 2026
4a17b26
Merge upstream main
bellatoris Mar 24, 2026
c1b2de5
fix: spelling check error in dispatcher test comment
bellatoris Mar 24, 2026
ce520ef
address review: use transport_socket_factory directly, remove filter_…
bellatoris Mar 25, 2026
8ff74e4
Merge upstream main
bellatoris Mar 25, 2026
f1e4cc3
address review: ENVOY_BUG for null factory, rename test, replace semi…
bellatoris Apr 2, 2026
e852b81
fix: guard against empty session ticket keys in processSessionTicket
bellatoris Apr 2, 2026
54c118d
fix: use inline_bytes for session ticket key in integration test
bellatoris Apr 2, 2026
538e8dc
test: add SDS session ticket integration tests
bellatoris Apr 3, 2026
b32b0ea
Merge branch 'main' into doogie/quic-proof-source-update
bellatoris Apr 7, 2026
e3203f7
Merge branch 'main' into doogie/quic-proof-source-update
bellatoris Apr 13, 2026
0768977
Merge branch 'main' into doogie/quic-proof-source-update
bellatoris Apr 14, 2026
f3150bc
refactor: consolidate session ticket logic into EnvoyTlsServerHandshaker
bellatoris Apr 14, 2026
f36f4ba
fix: spelling check - remove tlsext from comment
bellatoris Apr 15, 2026
f692b02
fix: spelling check - avoid BoringSSL type names in comments
bellatoris Apr 15, 2026
3455bd0
Merge branch 'main' into doogie/quic-proof-source-update
bellatoris Apr 17, 2026
200d459
Merge branch 'main' into doogie/quic-proof-source-update
bellatoris Apr 21, 2026
36bb344
fix: address review comments (comment clarifications + test cleanup)
bellatoris Apr 22, 2026
14eff46
fix: spelling check - avoid QUICHE's possessive in comment
bellatoris Apr 22, 2026
8349fe1
Merge branch 'main' into doogie/quic-proof-source-update
bellatoris Apr 24, 2026
bba7a8f
Merge branch 'main' into doogie/quic-proof-source-update
bellatoris Apr 25, 2026
3e26bc9
Merge branch 'main' into doogie/quic-proof-source-update
bellatoris Apr 25, 2026
d7a87e4
Merge branch 'main' into doogie/quic-proof-source-update
bellatoris Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,13 @@ removed_config_or_runtime:
and legacy code path.

new_features:
- area: quic
change: |
Added support for TLS session ticket resumption in QUIC using configured session ticket keys from
:ref:`session_ticket_keys <envoy_v3_api_field_extensions.transport_sockets.tls.v3.DownstreamTlsContext.session_ticket_keys>`.
This enables faster reconnection across server instances by allowing clients to resume TLS sessions
without full handshakes. The feature is disabled by default and can be enabled by setting runtime guard
``envoy.reloadable_features.quic_session_ticket_support`` to ``true``.
- area: lua
change: |
Added stats API support to the :ref:`Lua filter <config_http_filters_lua>`, allowing Lua
Expand Down
14 changes: 14 additions & 0 deletions source/common/quic/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,19 @@ envoy_cc_library(
]),
)

envoy_cc_library(
name = "envoy_tls_server_handshaker",
srcs = envoy_select_enable_http3(["envoy_tls_server_handshaker.cc"]),
hdrs = envoy_select_enable_http3(["envoy_tls_server_handshaker.h"]),
external_deps = ["ssl"],
deps = envoy_select_enable_http3([
"//source/common/common:assert_lib",
"//source/common/common:macros",
"//source/common/tls:server_context_lib",
"@quiche//:quic_server_session_lib",
]),
)

envoy_cc_library(
name = "envoy_quic_proof_source_lib",
srcs = envoy_select_enable_http3(["envoy_quic_proof_source.cc"]),
Expand All @@ -119,6 +132,7 @@ envoy_cc_library(
deps = envoy_select_enable_http3([
":envoy_quic_proof_source_base_lib",
":envoy_quic_utils_lib",
":envoy_tls_server_handshaker",
":quic_io_handle_wrapper_lib",
":quic_transport_socket_factory_lib",
"//envoy/ssl:tls_certificate_config_interface",
Expand Down
9 changes: 8 additions & 1 deletion source/common/quic/envoy_quic_proof_source.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

#include "envoy/ssl/tls_certificate_config.h"

#include "source/common/common/assert.h"
#include "source/common/quic/envoy_quic_utils.h"
#include "source/common/quic/envoy_tls_server_handshaker.h"
#include "source/common/quic/quic_io_handle_wrapper.h"
#include "source/common/stream_info/stream_info_impl.h"

Expand Down Expand Up @@ -113,7 +115,12 @@ void EnvoyQuicProofSource::updateFilterChainManager(
filter_chain_manager_ = &filter_chain_manager;
}

void EnvoyQuicProofSource::OnNewSslCtx(SSL_CTX* ssl_ctx) { registerCertCompression(ssl_ctx); }
void EnvoyQuicProofSource::OnNewSslCtx(SSL_CTX* ssl_ctx) {
registerCertCompression(ssl_ctx);
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.quic_session_ticket_support")) {
SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx, EnvoyTlsServerHandshaker::ticketKeyCallback);
}
}

} // namespace Quic
} // namespace Envoy
10 changes: 5 additions & 5 deletions source/common/quic/envoy_quic_proof_source.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ class EnvoyQuicProofSource : public EnvoyQuicProofSourceBase {
const Network::FilterChain& filter_chain_;
};

absl::optional<TransportSocketFactoryWithFilterChain>
getTransportSocketAndFilterChain(const quic::QuicSocketAddress& server_address,
const quic::QuicSocketAddress& client_address,
const std::string& hostname);

struct CertWithFilterChain {
quiche::QuicheReferenceCountedPointer<quic::ProofSource::Chain> cert_;
std::shared_ptr<quic::CertificatePrivateKey> private_key_;
Expand All @@ -49,11 +54,6 @@ class EnvoyQuicProofSource : public EnvoyQuicProofSourceBase {
CertWithFilterChain getTlsCertAndFilterChain(const TransportSocketFactoryWithFilterChain& data,
const std::string& hostname, bool* cert_matched_sni);

absl::optional<TransportSocketFactoryWithFilterChain>
getTransportSocketAndFilterChain(const quic::QuicSocketAddress& server_address,
const quic::QuicSocketAddress& client_address,
const std::string& hostname);

Network::Socket& listen_socket_;
Network::FilterChainManager* filter_chain_manager_{nullptr};
Server::ListenerStats& listener_stats_;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
#include "quiche/quic/core/crypto/quic_crypto_server_config.h"
#include "quiche/quic/core/quic_crypto_server_stream_base.h"
#include "quiche/quic/core/quic_session.h"
#include "quiche/quic/core/tls_server_handshaker.h"

namespace Envoy {
namespace Quic {
Expand Down
45 changes: 45 additions & 0 deletions source/common/quic/envoy_tls_server_handshaker.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#include "source/common/quic/envoy_tls_server_handshaker.h"

#include "source/common/common/macros.h"

namespace Envoy {
namespace Quic {

EnvoyTlsServerHandshaker::EnvoyTlsServerHandshaker(
quic::QuicSession* session, const quic::QuicCryptoServerConfig* crypto_config,
Ssl::ServerContextSharedPtr pinned_ssl_ctx, bool disable_resumption)
: TlsServerHandshaker(session, crypto_config), pinned_ssl_ctx_(std::move(pinned_ssl_ctx)) {
SSL_set_ex_data(ssl(), handshakerExDataIndex(), this);
// Also check the pinned context for keys: the factory is shared across workers and
// config_ may reflect an SDS update before ssl_ctx_ is swapped on the main thread.
if (disable_resumption || !pinnedServerContext()->hasSessionTicketKeys()) {
DisableResumption();
}
}

int EnvoyTlsServerHandshaker::handshakerExDataIndex() {
CONSTRUCT_ON_FIRST_USE(int, []() -> int {
int index = SSL_get_ex_new_index(0, nullptr, nullptr, nullptr, nullptr);
RELEASE_ASSERT(index >= 0, "Failed to allocate SSL ex_data index for handshaker");
return index;
}());
}

int EnvoyTlsServerHandshaker::ticketKeyCallback(SSL* ssl, uint8_t* key_name, uint8_t* iv,
EVP_CIPHER_CTX* ctx, HMAC_CTX* hmac_ctx,
int encrypt) {
auto* handshaker =
static_cast<EnvoyTlsServerHandshaker*>(SSL_get_ex_data(ssl, handshakerExDataIndex()));
if (handshaker == nullptr || handshaker->pinnedServerContext() == nullptr) {
// Null handshaker can occur if the runtime guard was toggled between
// OnNewSslCtx (which installed this callback on the SSL_CTX) and
// connection creation (which fell back to the vanilla TlsServerHandshaker).
// Return 0 to disable ticket for this connection — graceful fallback.
return 0;
}
return handshaker->pinnedServerContext()->sessionTicketProcess(ssl, key_name, iv, ctx, hmac_ctx,
encrypt);
}

} // namespace Quic
} // namespace Envoy
47 changes: 47 additions & 0 deletions source/common/quic/envoy_tls_server_handshaker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#pragma once

#include <openssl/ssl.h>

#include "source/common/common/assert.h"
#include "source/common/tls/server_context_impl.h"

#include "quiche/quic/core/tls_server_handshaker.h"

namespace Envoy {
namespace Quic {

// Minimal TlsServerHandshaker subclass that pins a ServerContextImpl at
Comment thread
bellatoris marked this conversation as resolved.
Outdated
// connection creation time. Ensures session ticket keys remain valid for
// the connection's lifetime, matching TCP TLS behavior.
//
// Also owns the per-connection session ticket callback plumbing:
// self-registers in SSL ex_data so ticketKeyCallback() can retrieve
// the pinned context from an SSL* in a C callback.
class EnvoyTlsServerHandshaker : public quic::TlsServerHandshaker {
public:
EnvoyTlsServerHandshaker(quic::QuicSession* session,
const quic::QuicCryptoServerConfig* crypto_config,
Ssl::ServerContextSharedPtr pinned_ssl_ctx, bool disable_resumption);

// Session ticket key callback installed on the QUICHE ssl context.
// Retrieves the handshaker from ssl ex_data and delegates to the pinned
// ServerContextImpl::sessionTicketProcess().
static int ticketKeyCallback(SSL* ssl, uint8_t* key_name, uint8_t* iv, EVP_CIPHER_CTX* ctx,
HMAC_CTX* hmac_ctx, int encrypt);

// SSL ex_data index for storing the handshaker pointer per-connection.
static int handshakerExDataIndex();

private:
// QuicServerTransportSocketFactory always creates ServerContextImpl,
// so this downcast is safe for all QUIC connections.
Extensions::TransportSockets::Tls::ServerContextImpl* pinnedServerContext() const {
return static_cast<Extensions::TransportSockets::Tls::ServerContextImpl*>(
pinned_ssl_ctx_.get());
}

Ssl::ServerContextSharedPtr pinned_ssl_ctx_;
};

} // namespace Quic
} // namespace Envoy
28 changes: 28 additions & 0 deletions source/common/quic/quic_server_transport_socket_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,34 @@ class QuicServerTransportSocketFactory : public Network::DownstreamTransportSock

bool earlyDataEnabled() const { return enable_early_data_; }

struct SessionTicketConfig {
// True when session ticket encryption keys are explicitly configured via
// session_ticket_keys or session_ticket_keys_sds_secret_config. Without
// keys, the server cannot encrypt or decrypt session tickets.
bool has_keys;
// True when disable_stateless_session_resumption is set in
// DownstreamTlsContext. When enabled, the server will not issue session
// tickets and clients must perform full handshakes on every connection.
bool disable_stateless_resumption;
// True when an external mechanism (e.g., SDS provider) manages session
// resumption including ticket encryption/decryption. When set, Envoy
// should not install its own session ticket key processing callback.
bool handles_session_resumption;
Comment thread
bellatoris marked this conversation as resolved.
};

SessionTicketConfig getSessionTicketConfig() const {
return {!config_->sessionTicketKeys().empty(), config_->disableStatelessSessionResumption(),
config_->capabilities().handles_session_resumption};
}

// Returns the current ServerContextImpl, pinning a shared_ptr so it
// remains valid for the caller's lifetime. May return null before
// initialize() completes or if context creation failed.
Ssl::ServerContextSharedPtr sslCtx() const {
absl::ReaderMutexLock l(ssl_ctx_mu_);
return ssl_ctx_;
}

protected:
QuicServerTransportSocketFactory(bool enable_early_data, Stats::Scope& store,
Ssl::ServerContextConfigPtr config,
Expand Down
2 changes: 2 additions & 0 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ FALSE_RUNTIME_GUARD(envoy_reloadable_features_always_use_v6);
FALSE_RUNTIME_GUARD(envoy_restart_features_upstream_http_filters_with_tcp_proxy);
// TODO(danzh) false deprecate it once QUICHE has its own enable/disable flag.
FALSE_RUNTIME_GUARD(envoy_reloadable_features_quic_reject_all);
// TODO(doogie): Flip to true once QUIC session ticket support is stable.
FALSE_RUNTIME_GUARD(envoy_reloadable_features_quic_session_ticket_support);
// TODO(#10646) change to true when UHV is sufficiently tested
// For more information about Universal Header Validation, please see
// https://github.com/envoyproxy/envoy/issues/10646
Expand Down
6 changes: 4 additions & 2 deletions source/common/tls/server_context_impl.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ class ServerContextImpl : public ContextImpl,

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

int sessionTicketProcess(SSL* ssl, uint8_t* key_name, uint8_t* iv, EVP_CIPHER_CTX* ctx,
HMAC_CTX* hmac_ctx, int encrypt);
bool hasSessionTicketKeys() const { return !session_ticket_keys_.empty(); }

protected:
ServerContextImpl(
Stats::Scope& scope, const Envoy::Ssl::ServerContextConfig& config,
Expand All @@ -82,8 +86,6 @@ class ServerContextImpl : public ContextImpl,

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

absl::StatusOr<SessionContextID>
generateHashForSessionContextId(const std::vector<std::string>& server_names);
Expand Down
3 changes: 3 additions & 0 deletions source/extensions/quic/crypto_stream/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ envoy_cc_library(
deps = envoy_select_enable_http3([
"//envoy/registry",
"//source/common/quic:envoy_quic_server_crypto_stream_factory_lib",
"//source/common/quic:envoy_tls_server_handshaker",
"//source/common/quic:quic_server_transport_socket_factory_lib",
"@envoy_api//envoy/extensions/quic/crypto_stream/v3:pkg_cc_proto",
"@quiche//:quic_server_session_lib",
]),
alwayslink = LEGACY_ALWAYSLINK,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
#include "source/extensions/quic/crypto_stream/envoy_quic_crypto_server_stream.h"

#include "source/common/quic/envoy_tls_server_handshaker.h"
#include "source/common/quic/quic_server_transport_socket_factory.h"
#include "source/common/runtime/runtime_features.h"

namespace Envoy {
namespace Quic {

Expand All @@ -8,11 +12,26 @@ EnvoyQuicCryptoServerStreamFactoryImpl::createEnvoyQuicCryptoServerStream(
const quic::QuicCryptoServerConfig* crypto_config,
quic::QuicCompressedCertsCache* compressed_certs_cache, quic::QuicSession* session,
quic::QuicCryptoServerStreamBase::Helper* helper,
// Though this extension doesn't use the two parameters below, they might be used by
// downstreams. Do not remove them.
OptRef<const Network::DownstreamTransportSocketFactory> /*transport_socket_factory*/,
OptRef<const Network::DownstreamTransportSocketFactory> transport_socket_factory,
// Though this extension doesn't use the dispatcher parameter, it might be used by
// downstreams. Do not remove it.
Envoy::Event::Dispatcher& /*dispatcher*/) {
return quic::CreateCryptoServerStream(crypto_config, compressed_certs_cache, session, helper);

if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.quic_session_ticket_support") ||
!transport_socket_factory.has_value()) {
return quic::CreateCryptoServerStream(crypto_config, compressed_certs_cache, session, helper);
Comment thread
bellatoris marked this conversation as resolved.
}

// QUIC listeners always use QuicServerTransportSocketFactory. The factory's
// ssl_ctx_ is set in the constructor and swapped (never nulled) on SDS updates.
auto& factory = static_cast<const QuicServerTransportSocketFactory&>(*transport_socket_factory);

auto ticket_config = factory.getSessionTicketConfig();
bool disable_resumption = ticket_config.disable_stateless_resumption || !ticket_config.has_keys ||
ticket_config.handles_session_resumption;

return std::make_unique<EnvoyTlsServerHandshaker>(session, crypto_config, factory.sslCtx(),
disable_resumption);
}

REGISTER_FACTORY(EnvoyQuicCryptoServerStreamFactoryImpl,
Expand Down
8 changes: 8 additions & 0 deletions test/common/quic/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ envoy_cc_test(
]),
)

envoy_cc_test(
name = "envoy_tls_server_handshaker_test",
srcs = envoy_select_enable_http3(["envoy_tls_server_handshaker_test.cc"]),
deps = envoy_select_enable_http3([
"//source/common/quic:envoy_tls_server_handshaker",
]),
)

envoy_cc_test(
name = "envoy_quic_proof_source_test",
srcs = envoy_select_enable_http3(["envoy_quic_proof_source_test.cc"]),
Expand Down
Loading
Loading