
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 positiveIS_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.protoproto 文档对其工作方式描述如下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_sourceconfig.core.v3.ConfigSource是validate 规则required: true定义 secret 的配置来源即 SDS 的 xDS 配置源certificate_mapperconfig.core.v3.TypedExtensionConfig是required: true扩展点指定「如何从握手消息计算 secret 名称」的函数下游场景在收到客户端CLIENT_HELLO后调用上游场景则基于 transport socket options 与SERVER_HELLO调用对应两类扩展分类envoy.tls.certificate_mappers下游与envoy.tls.upstream_certificate_mappers上游prefetch_secret_namesrepeated 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.ccQUIC 限制下游 on-demand 选择器明确不支持 QUIC 监听器配置时若for_quic为 true 同样返回InvalidArgumentError。四、异步选择的核心机制SecretManager 与 Handle4.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::notifyconfig.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.hcert_requestedCounter发起的证书拉取请求数cert_updatedCounter证书更新并刷新线程本地缓存次数cert_activeGaugeAccumulate当前活跃的证书订阅数。集成测试即通过这些指标断言行为例如 integration_test.cc 中断言首条连接触发cert_requested为 1随后第二条连接复用缓存仍为 1而sds.server.update_success与cert_updated各为 1。五、bug 根因连接拆除与挂起握手的竞态5.1 触发场景还原结合源码注释与修复内容误报IS_ENVOY_BUG的场景可以还原为客户端连接到达TLS 握手开始选择器判定证书未缓存返回PendingHandle被注册到SecretManager握手挂起在 SDS 响应尚未返回的窗口期内客户端主动断开连接或服务端因其他原因拆除该连接如 filter chain 被移除、空闲超时、连接被 resetSDS 响应随后到达updateCertificate遍历entry.callbacks_并调用handle-notify(cert_context)最终触发连接侧的回调onCertificateSelectionResult此时连接上下文已被销毁回调落在一个已拆除的连接上于是触发误报的IS_ENVOY_BUG断言。5.2 源码中的佐证修复相关的防御性设计在代码中留下了清晰痕迹a弱引用守卫 fetch 请求。SecretManager::fetchCertificateconfig.cc在把请求投递到主线程时同时持有weak_thisSecretManager 本身与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_ptrSecretManager(shared_from_this()), name std::string(secret_name), weak_handle std::weak_ptrHandle(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.ccvoid CertificateSelectionCallbackImpl::onSslHandshakeCancelled() { extended_socket_info_.reset(); } void CertificateSelectionCallbackImpl::onCertificateSelectionResult( OptRefconst 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默认配置可正常创建、BasicLoadTestQuicQUIC 场景、BasicLoadTestStatelessResumption/BasicLoadTestStatefulResumption开启会话恢复时创建失败、QuicCallQUIC 调用不被支持等集成级integration_test.cc 通过真实 SDSDELTA_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: V3gRPC 服务超时 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),仅供参考