Envoy TLS 异步证书选择连接拆除误报断言修复解析:on_demand_secret 扩展的握手生命周期治理
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
本篇技术指南围绕 Envoy 主分支changelogs/current/bug_fixes中的一条 TLS bug 修复展开:当异步证书选择(asynchronous certificate selection)仍在进行时,如果连接被拆除(connection teardown),会产生误报的IS_ENVOY_BUG断言。文章将剖析该问题的触发场景、修复后的防御机制,并结合envoy.tls.certificate_selectors.on_demand_secret扩展的配置 proto、核心实现与集成测试,完整还原「握手挂起 → SDS 拉取证书 → 连接被重置 → 回调安全忽略」的全链路处理逻辑。读完本文,你将掌握 on-demand 证书选择的配置方法、异步握手回调的生命周期约束,以及如何从源码层面验证此类连接竞态问题的修复。
一、变更条目速览
本次修复记录位于 changelogs/current/bug_fixes/tls__allow-connection-teardown-during-async-cert-selection.rst,原文为:
Fixed a bug in TLS where a false positive
IS_ENVOY_BUGassertion was triggered when a connection was torn down while asynchronous certificate selection was still in progress.
用一句话概括:TLS 握手中,当异步证书选择尚未完成时,若连接被拆除,Envoy 不再误报IS_ENVOY_BUG断言。IS_ENVOY_BUG是 Envoy 断言体系中的一种,用于标记「本不该发生、但发生了也需兜底」的异常路径(区别于直接崩溃的ASSERT),其语义偏重「这里出现了一个 bug」,因此对于「连接在异步选择期间被拆除」这种正常的资源竞争场景,触发它是误报。
该修复的核心价值在于:在 on-demand 证书选择(证书并非在配置加载时预置,而是在 TLS 握手过程中按需从 SDS 拉取)这种异步模型下,连接拆除与证书回调完成之间天然存在竞态,修复后这条路径被显式标记为「预期行为」,而非「程序缺陷」。
二、背景:什么是 on-demand 异步证书选择
2.1 扩展定位
修复所涉及的扩展是envoy.tls.certificate_selectors.on_demand_secret,其 API 定义位于 api/envoy/extensions/transport_sockets/tls/cert_selectors/on_demand_secret/v3/config.proto,proto 文档对其工作方式描述如下:
Fetches the secret on-demand while allowing the parent cluster or listener to accept connections without warming. During the handshake, a secret name is derived from the peer hello message, an SDS resource request starts, and the handshake is paused. Once an SDS response is received with a resource, the handshake is resumed with the provided certificate. If the SDS server indicates the resource removal, the handshake is failed, and the SDS subscription to the resource is stopped.
即:父级监听器(listener)或集群在证书尚未就绪时即可接受连接而无需 warm up;握手期间从对端的 hello 消息推导出 secret 名称并发起 SDS 资源请求,同时挂起(pause)握手;收到 SDS 响应后,用返回的证书恢复握手;若 SDS 服务器指示资源被移除,则握手失败并停止对该资源的订阅。
实现代码位于 source/extensions/transport_sockets/tls/cert_selectors/on_demand/config.cc 与 source/extensions/transport_sockets/tls/cert_selectors/on_demand/config.h。
2.2 与常规 SDS 的区别
- 常规 SDS:证书在 listener/cluster 初始化(warm up)时同步或异步加载,未加载完成前父资源无法进入就绪状态;
- on-demand 证书:父资源不等证书即可就绪,证书延迟到「第一条连接的手握握手中」按需获取,从而支持超大证书池(如按 SNI 区分的海量域名证书)而不拖慢启动。
两者共用同一套外层 common TLS context 配置(例如对加载的证书施加 FIPS 合规策略),这一点在 config.proto 中亦有说明。
三、Config 配置项详解
on_demand_secret的Config消息共三个字段,均在 config.proto 中定义:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
config_source | config.core.v3.ConfigSource | 是(validate 规则required: true) | 定义 secret 的配置来源,即 SDS 的 xDS 配置源 |
certificate_mapper | config.core.v3.TypedExtensionConfig | 是(required: true) | 扩展点,指定「如何从握手消息计算 secret 名称」的函数;下游场景在收到客户端CLIENT_HELLO后调用,上游场景则基于 transport socket options 与SERVER_HELLO调用,对应两类扩展分类:envoy.tls.certificate_mappers(下游)与envoy.tls.upstream_certificate_mappers(上游) |
prefetch_secret_names | repeated string | 否 | 配置加载时(收到任何请求之前)即开始拉取的 secret 资源名列表;父资源初始化时不必等待这些拉取完成 |
3.1 完整配置示例
综合 integration_test.cc 中的用例,一个最小可用的下游配置如下:
common_tls_context: custom_tls_certificate_selector: name: envoy.tls.certificate_selectors.on_demand_secret typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.cert_selectors.on_demand_secret.v3.Config config_source: resource_api_version: V3 api_config_source: api_type: DELTA_GRPC transport_api_version: V3 grpc_services: - envoy_grpc: cluster_name: sds_cluster timeout: 300s certificate_mapper: name: static-name typed_config: "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.cert_mappers.static_name.v3.StaticName name: server prefetch_secret_names: - server要点说明:
- 证书映射器(certificate_mapper):测试中常使用
static-name(返回固定名称)或sni(按 SNI 推导名称,如default_value: "*"),对应的 mapper 实现位于 source/extensions/transport_sockets/tls/cert_mappers(含static_name、sni、filter_state_override三种); - SDS 配置源:测试通过 DELTA_GRPC 指向名为
sds_cluster的 gRPC 集群(见 integration_test.cc 的setConfigSource); - 会话恢复限制:配置 on-demand 证书时必须同时关闭无状态与有状态会话恢复(
disable_stateless_session_resumption与disable_stateful_session_resumption均置为 true),否则工厂创建阶段直接返回InvalidArgumentError——因为会话 ID 由父 TLS context 中的 server name 与证书生成,若允许用该 ID 恢复「父 context 中不存在的」按需证书是不安全的(见 config.cc); - QUIC 限制:下游 on-demand 选择器明确不支持 QUIC 监听器,配置时若
for_quic为 true 同样返回InvalidArgumentError。
四、异步选择的核心机制:SecretManager 与 Handle
4.1 整体架构
从 config.h 的类定义可以梳理出如下协作关系:
SecretManager:维护对 SDS secret 的动态订阅,将 xDS 形式的 secret 转换为 BoringSSL TLS context,并应用父级 TLS 配置。内部有两份状态:主线程可访问的cache_(记录订阅与待通知回调),以及每线程的 lock-free 缓存ThreadLocalCerts(记录名称 → 就绪 TLS context 的映射);AsyncSelector/UpstreamAsyncSelector:每个 worker 上每个 TLS socket 各持有一个的选择器实例。下游在收到SSL_CLIENT_HELLO时触发selectTlsContext,上游则在SERVER_HELLO基础上触发;Handle:代表一次「等待证书」的异步请求,持有证书选择回调(CertificateSelectionCallbackPtr)与客户端 OCSP 能力标记;一旦证书就绪,由notify()把结果投递给挂起的连接;AsyncContext/ServerAsyncContext/ClientAsyncContext:承载所选证书对应的底层 TLS context 及其 OCSP 策略。
4.2 选择流程(两阶段)
在 config.cc 的BaseAsyncSelector::doSelectTlsContext中可以看到典型的两阶段选择逻辑:
- 命中缓存(同步路径):
secret_manager_->getContext(name)返回已就绪的 TLS context,直接构造同步Handle并返回SelectionStatus::Success,无需等待; - 未命中(异步路径):返回
SelectionStatus::Pending,并调用fetchCertificate(name, cb, client_ocsp_capable)发起异步拉取,握手就此挂起。
4.3 回调投递与线程模型
Handle::notify(config.cc)是异步结果的落点:
void Handle::notify(AsyncContextConstSharedPtr cert_ctx) { ASSERT(cb_); bool staple = false; if (cert_ctx) { active_context_ = cert_ctx; staple = (ocspStapleAction(...) == Ssl::OcspStapleAction::Staple); } Event::Dispatcher& dispatcher = cb_->dispatcher(); dispatcher.post([cb = std::move(cb_), cert_ctx, staple] { cb->onCertificateSelectionResult( makeOptRefFromPtr(cert_ctx ? &cert_ctx->tlsContext() : nullptr), staple); }); cb_ = nullptr; }关键点:
SecretManager的所有状态变更(addCertificateConfig、updateCertificate、updateAll、doRemoveCertificateConfig)都强制要求在主线程(ASSERT_IS_MAIN_OR_TEST_THREAD());- 结果通过
dispatcher.post异步投递到连接所在 worker 线程,代码注释也提示未来可在 dispatcher 外层循环中对事件做批处理优化; - 证书被移除(SDS 指示资源删除)时以
nullptr通知,此时连接侧收到空上下文,握手将失败、订阅随之停止,这也正是 proto 文档所描述的「resource removal → handshake failed」路径。
4.4 指标统计
on_demand_secret扩展在主线程 scopeon_demand_secret.下维护三组指标(见 config.h):
cert_requested(Counter):发起的证书拉取请求数;cert_updated(Counter):证书更新并刷新线程本地缓存次数;cert_active(Gauge,Accumulate):当前活跃的证书订阅数。
集成测试即通过这些指标断言行为,例如 integration_test.cc 中断言首条连接触发cert_requested为 1,随后第二条连接复用缓存(仍为 1),而sds.server.update_success与cert_updated各为 1。
五、bug 根因:连接拆除与挂起握手的竞态
5.1 触发场景还原
结合源码注释与修复内容,误报IS_ENVOY_BUG的场景可以还原为:
- 客户端连接到达,TLS 握手开始;
- 选择器判定证书未缓存,返回
Pending,Handle被注册到SecretManager,握手挂起; - 在 SDS 响应尚未返回的窗口期内,客户端主动断开连接,或服务端因其他原因拆除该连接(如 filter chain 被移除、空闲超时、连接被 reset);
- SDS 响应随后到达,
updateCertificate遍历entry.callbacks_并调用handle->notify(cert_context),最终触发连接侧的回调onCertificateSelectionResult; - 此时连接上下文已被销毁,回调落在一个已拆除的连接上,于是触发误报的
IS_ENVOY_BUG断言。
5.2 源码中的佐证
修复相关的防御性设计在代码中留下了清晰痕迹:
(a)弱引用守卫 fetch 请求。SecretManager::fetchCertificate(config.cc)在把请求投递到主线程时同时持有weak_this(SecretManager 本身)与weak_handle(本次请求的 Handle):
// The manager might need to be destroyed after posting from a worker because // the filter chain is being removed. Therefore, use a weak_ptr and ignore // the request to fetch a secret. Handle can also be destroyed because the // underlying connection is reset, and handshake is cancelled. factory_context_.mainThreadDispatcher().post( [weak_this = std::weak_ptr<SecretManager>(shared_from_this()), name = std::string(secret_name), weak_handle = std::weak_ptr<Handle>(handle)]() mutable { auto that = weak_this.lock(); auto handle = weak_handle.lock(); if (that && handle) { that->addCertificateConfig(name, handle, {}); } });注释明确写到:filter chain 被移除时 manager 可能先于 worker 上的 socket 销毁,因此用弱引用并「忽略」过期的 fetch 请求;Handle 也可能因为底层连接被 reset、握手被取消而销毁。这正是本 bug 修复对应的语义:把「连接拆除导致 Handle 失效」视为预期路径,而不是断言失败。
(b)回调端的空值防御。连接侧的回调实现在 source/common/tls/ssl_handshaker.cc:
void CertificateSelectionCallbackImpl::onSslHandshakeCancelled() { extended_socket_info_.reset(); } void CertificateSelectionCallbackImpl::onCertificateSelectionResult( OptRef<const Ssl::TlsContext> selected_ctx, bool staple) { if (!extended_socket_info_.has_value()) { return; } extended_socket_info_->onCertificateSelectionCompleted(selected_ctx, staple, true); }当连接被拆除时,SslExtendedSocketInfoImpl析构(ssl_handshaker.cc)会调用onSslHandshakeCancelled()使extended_socket_info_置空;此后即使异步证书结果姗姗来迟,onCertificateSelectionResult也会在has_value()检查处直接返回,静默忽略过期结果,而不再触发IS_ENVOY_BUG断言。同一模式也适用于证书校验路径的ValidateResultCallbackImpl(握手取消时onSslHandshakeCancelled同样重置状态)。
从源码结构可以推断:该修复的核心正是「把异步回调与连接生命周期的解耦从『假定回调必达且连接必存』修正为『回调可能迟到、连接可能先亡,二者皆需防御』」。
六、修复的验证:集成测试覆盖
6.1 测试布局
该功能的测试分两层:
- 单元级:config_test.cc 覆盖工厂创建行为,包括
BasicLoadTest(默认配置可正常创建)、BasicLoadTestQuic(QUIC 场景)、BasicLoadTestStatelessResumption/BasicLoadTestStatefulResumption(开启会话恢复时创建失败)、QuicCall(QUIC 调用不被支持)等; - 集成级:integration_test.cc 通过真实 SDS(DELTA_GRPC)与 TLS 握手验证端到端行为。
6.2 与连接拆除直接相关的用例模式
集成测试大量使用conn.reset()/conn->close()(如 integration_test.cc 等数十处),其中尤具代表性的流程为:
- 建立连接并等待证书请求(
waitCertsRequested(1)); - 建立 xDS 连接并下发 SDS 响应(
waitSendSdsResponse("server")); - 连接完成握手、收发数据(
sendAndReceiveTlsData("hello", "world")); conn.reset()拆除连接;- 通过指标断言验证:
cert_requested为 1、cert_active为 1、sds.server.update_success为 1、sds.server.update_rejected为 0,且后续第二条连接直接复用缓存、不再触发 SDS 拉取。
这些用例与本次修复高度相关:它们反复在「证书就绪/未就绪」的不同时间点拆除连接,正是为了覆盖「连接先亡、回调后到」的竞态窗口,确保拆除路径不会触发IS_ENVOY_BUG断言。
6.3 测试中的关键配置断言
测试还对 on-demand 配置施加了严格的运行约束:
- 下游 TLS context 必须设置
disable_stateless_session_resumption(true)与disable_stateful_session_resumption(true)(integration_test.cc),与工厂层的校验逻辑相互印证; - SDS 使用 DELTA_GRPC 与
resource_api_version: V3,gRPC 服务超时 300s。
七、运维与排障建议
基于以上源码分析,给使用或排查 on-demand 证书选择的读者几点建议:
- 预期「握手可能长时间挂起」:证书拉取期间握手暂停,若 SDS 集群不可用,连接会一直等待。生产环境应配合握手/连接超时,并监控
cert_requested与cert_active指标判断是否出现证书拉取积压; - 不要误把连接重置当故障:连接在证书拉取完成前被客户端断开属于正常竞态,修复后 Envoy 不会再输出
IS_ENVOY_BUG;若仍看到该类断言,说明运行版本早于本次修复,应升级到包含该变更的版本; - 会话恢复与 QUIC 限制是硬约束:配置校验失败(
InvalidArgumentError)时,优先检查是否未关闭两种 session resumption,或是否试图用于 QUIC 监听器; - 善用 prefetch:对热点证书使用
prefetch_secret_names提前拉取,可将首条连接的「拉取等待」转化为「缓存命中」,显著降低首个请求的握手延迟(参见BasicSuccessWithPrefetch用例的指标表现)。
八、小结
本次修复针对的是一个容易在真实流量下高频触发的竞态误报:on-demand 异步证书选择天然存在「证书回调到达」与「连接拆除」的时间窗口竞争。修复通过双弱引用守卫(SecretManager与Handle)、握手取消状态重置、以及回调端has_value()防御三层机制,把这条路径从「程序 bug 断言」降级为「预期内的正常生命周期事件」。
该修复对应的功能主线——envoy.tls.certificate_selectors.on_demand_secret扩展——让 Envoy 得以在证书海量、按需分发(例如按 SNI 区分的多租户证书场景)时保持监听器快速就绪,其完整链路(proto 配置 → SecretManager → Handle 回调 → BoringSSL context)均可通过本文引用的源码文件与集成测试深入研读。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考