Skip to content
Open
Show file tree
Hide file tree
Changes from 24 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 @@ -344,6 +344,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: ratelimit
change: |
Added ``is_negative_hits`` boolean to the ``hits_addend``
Expand Down
1 change: 1 addition & 0 deletions source/common/quic/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ envoy_cc_library(
":quic_io_handle_wrapper_lib",
":quic_transport_socket_factory_lib",
"//envoy/ssl:tls_certificate_config_interface",
"//source/common/common:macros",
"//source/common/quic:quic_server_transport_socket_factory_lib",
"//source/common/stream_info:stream_info_lib",
"//source/server:listener_stats",
Expand Down
28 changes: 27 additions & 1 deletion source/common/quic/envoy_quic_proof_source.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "envoy/ssl/tls_certificate_config.h"

#include "source/common/common/assert.h"
#include "source/common/common/macros.h"
#include "source/common/quic/envoy_quic_utils.h"
#include "source/common/quic/quic_io_handle_wrapper.h"
#include "source/common/stream_info/stream_info_impl.h"
Expand All @@ -14,6 +16,14 @@
namespace Envoy {
namespace Quic {

int EnvoyQuicProofSource::transportSocketFactoryExDataIndex() {
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 transport socket factory");
return index;
}());
}

quiche::QuicheReferenceCountedPointer<quic::ProofSource::Chain>
EnvoyQuicProofSource::GetCertChain(const quic::QuicSocketAddress& server_address,
const quic::QuicSocketAddress& client_address,
Expand Down Expand Up @@ -113,7 +123,23 @@ void EnvoyQuicProofSource::updateFilterChainManager(
filter_chain_manager_ = &filter_chain_manager;
}

void EnvoyQuicProofSource::OnNewSslCtx(SSL_CTX* ssl_ctx) { registerCertCompression(ssl_ctx); }
int EnvoyQuicProofSource::ticketKeyCallback(SSL* ssl, uint8_t* key_name, uint8_t* iv,
EVP_CIPHER_CTX* ctx, HMAC_CTX* hmac_ctx, int encrypt) {
auto* factory = static_cast<const QuicServerTransportSocketFactory*>(
SSL_get_ex_data(ssl, transportSocketFactoryExDataIndex()));
if (factory == nullptr) {
IS_ENVOY_BUG("QUIC session ticket callback invoked without transport socket factory");
return 0;
Comment thread
bellatoris marked this conversation as resolved.
Outdated
}
return factory->processSessionTicket(ssl, key_name, iv, ctx, hmac_ctx, encrypt);
}

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, ticketKeyCallback);
}
}

} // namespace Quic
} // namespace Envoy
30 changes: 20 additions & 10 deletions source/common/quic/envoy_quic_proof_source.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ class EnvoyQuicProofSource : public EnvoyQuicProofSourceBase {

void updateFilterChainManager(Network::FilterChainManager& filter_chain_manager);

// Returns the SSL ex_data index used to store transport socket factory pointer during QUIC
// handshakes.
static int transportSocketFactoryExDataIndex();

// Session ticket key callback installed on SSL_CTX by OnNewSslCtx.
// Retrieves the QuicServerTransportSocketFactory from SSL ex_data and
// delegates to processSessionTicket.
static int ticketKeyCallback(SSL* ssl, uint8_t* key_name, uint8_t* iv, EVP_CIPHER_CTX* ctx,
HMAC_CTX* hmac_ctx, int encrypt);

struct TransportSocketFactoryWithFilterChain {
const QuicServerTransportSocketFactory& transport_socket_factory_;
const Network::FilterChain& filter_chain_;
};

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

Comment thread
bellatoris marked this conversation as resolved.
Outdated
protected:
// quic::ProofSource
void signPayload(const quic::QuicSocketAddress& server_address,
Expand All @@ -35,11 +55,6 @@ class EnvoyQuicProofSource : public EnvoyQuicProofSourceBase {
std::unique_ptr<quic::ProofSource::SignatureCallback> callback) override;

private:
struct TransportSocketFactoryWithFilterChain {
const QuicServerTransportSocketFactory& transport_socket_factory_;
const Network::FilterChain& filter_chain_;
};

struct CertWithFilterChain {
quiche::QuicheReferenceCountedPointer<quic::ProofSource::Chain> cert_;
std::shared_ptr<quic::CertificatePrivateKey> private_key_;
Expand All @@ -49,11 +64,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
24 changes: 24 additions & 0 deletions source/common/quic/quic_server_transport_socket_factory.cc
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,30 @@ QuicServerTransportSocketFactory::getTlsCertificateAndKey(absl::string_view sni,
return {tls_context.quic_cert_, tls_context.quic_private_key_};
}

int QuicServerTransportSocketFactory::processSessionTicket(SSL* ssl, uint8_t* key_name, uint8_t* iv,
EVP_CIPHER_CTX* ctx, HMAC_CTX* hmac_ctx,
int encrypt) const {
Envoy::Ssl::ServerContextSharedPtr ssl_ctx;
{
absl::ReaderMutexLock l(ssl_ctx_mu_);
ssl_ctx = ssl_ctx_;
}
if (!ssl_ctx) {
return 0;
}

// QuicServerTransportSocketFactory always creates ServerContextImpl.
auto server_ctx =
std::static_pointer_cast<Extensions::TransportSockets::Tls::ServerContextImpl>(ssl_ctx);
// Session ticket keys may have been removed by an SDS update after this
// connection was created. Unlike TCP TLS where each connection pins its own
// ServerContextImpl, QUIC reads the current ssl_ctx_ which may have empty keys.
if (!server_ctx->hasSessionTicketKeys()) {
return 0;
}
return server_ctx->sessionTicketProcess(ssl, key_name, iv, ctx, hmac_ctx, encrypt);
Comment thread
bellatoris marked this conversation as resolved.
Outdated
}

absl::Status QuicServerTransportSocketFactory::onSecretUpdated() {
ENVOY_LOG(debug, "Secret is updated.");

Expand Down
26 changes: 26 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,32 @@ 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};
}

// Processes a session ticket encrypt or decrypt operation by delegating to the
// underlying ServerContextImpl. Returns 0 on failure, 1 on success, 2 on
// success with key renewal (decrypt only).
int processSessionTicket(SSL* ssl, uint8_t* key_name, uint8_t* iv, EVP_CIPHER_CTX* ctx,
HMAC_CTX* hmac_ctx, int encrypt) const;

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 @@ -141,6 +141,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 @@ -26,8 +26,11 @@ envoy_cc_library(
],
deps = envoy_select_enable_http3([
"//envoy/registry",
"//source/common/quic:envoy_quic_proof_source_lib",
"//source/common/quic:envoy_quic_server_crypto_stream_factory_lib",
"//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,13 @@
#include "source/extensions/quic/crypto_stream/envoy_quic_crypto_server_stream.h"

#include <openssl/ssl.h>

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

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

namespace Envoy {
namespace Quic {

Expand All @@ -8,11 +16,35 @@ 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);
auto stream =
quic::CreateCryptoServerStream(crypto_config, compressed_certs_cache, session, helper);

if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.quic_session_ticket_support") &&
transport_socket_factory.has_value() && stream->GetSsl() != nullptr) {
// QUIC listeners always use QuicServerTransportSocketFactory.
auto& factory = static_cast<const QuicServerTransportSocketFactory&>(*transport_socket_factory);

// Store factory in SSL ex_data for the per-connection ticket key callback
// installed by EnvoyQuicProofSource::OnNewSslCtx.
SSL_set_ex_data(stream->GetSsl(), EnvoyQuicProofSource::transportSocketFactoryExDataIndex(),
const_cast<QuicServerTransportSocketFactory*>(&factory));

auto ticket_config = factory.getSessionTicketConfig();
if (ticket_config.disable_stateless_resumption || !ticket_config.has_keys ||
ticket_config.handles_session_resumption) {
// GetSsl() returning non-null guarantees this is a TlsServerHandshaker (not the
// legacy QuicCryptoServerStream which returns nullptr from GetSsl()).
// DisableResumption() works here: can_disable_resumption_ is true at construction,
// only set false in EarlySelectCertCallback which hasn't fired yet.
static_cast<quic::TlsServerHandshaker*>(stream.get())->DisableResumption();
}
}

return stream;
}

REGISTER_FACTORY(EnvoyQuicCryptoServerStreamFactoryImpl,
Expand Down
Loading
Loading