Envoy 上游 TLS 会话缓存按 SNI 隔离修复:实现原理与运行时回退指南
Envoy 上游 TLS 会话缓存按 SNI 隔离修复实现原理与运行时回退指南【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoyEnvoy 在本次变更中修复了上游 TLS 客户端会话缓存的一个跨 SNI 复用缺陷现在会话将按建立连接时实际生效的 SNIServer Name Indication进行隔离缓存从而避免把为某个上游 SNI 学到的会话错误地用于另一 SNI 的连接。本文将围绕该变更对应 changelogs/current/bug_fixes/tls__scope-upstream-session-cache-by-sni.rst展开结合 source/common/tls/client_context_impl.cc 等源码深入剖析 SNI 会话缓存的数据结构、生效 SNI 的确定逻辑、max_session_keys语义以及临时回退用的运行时开关帮助你理解该修复的原理并掌握配置与验证方法。问题背景跨 SNI 复用 TLS 会话的隐患TLS 会话恢复session resumption是降低上游 TLS 握手开销的关键机制客户端缓存服务端签发的会话票据Session TicketTLS 1.2 及更早版本、会话 IDSession ID或 TLS 1.3 的 Pre-Shared KeyPSK并在新连接上直接恢复会话从而省去完整握手。在 Envoy 的客户端 TLS 实现中一个ClientContextImpl上游 TLS 上下文可以被多个上游逻辑主机共享而这些主机可能携带不同的 SNI。修复前的会话缓存是**上下文级context-wide**的会话被缓存在一个不区分 SNI 的全局队列里。其风险在于——某个连接在为 SNI A 学习到会话后另一个使用 SNI B 的连接可能取出该会话进行恢复导致 TLS 身份错配甚至恢复出错误服务端的会话产生握手异常或安全语义偏差。从源码注释也可以印证这一点source/common/tls/client_context_impl.cc在未按 SNI 隔离缓存时newSSL()安装的是仅以 SNI 为键的缓存会话可能把为证书 A 建立的会话恢复给本应选择证书 B 的连接。这正是本次 bug fix 要根治的问题。修复内容速览changelog 原文解读本次变更的核心内容可概括为三点会话按“生效 SNI”隔离缓存上游 TLS 客户端会话缓存改为以建立连接时实际生效的 SNI 作为作用域为某个上游 SNI 学到的会话绝不会被提供给使用不同 SNI 的连接。max_session_keys语义保持不变该配置项仍然用于限制当前客户端上下文缓存的会话总数上限不会因为按 SNI 分桶而变成“每个 SNI 各 N 个”。提供临时回退开关可通过运行时 guardenvoy.reloadable_features.scope_upstream_tls_session_cache_by_sni设为false临时恢复旧的上下文级缓存行为仅用于过渡期不推荐长期使用。源码级剖析SNI 作用域会话缓存的实现1. 核心数据结构新缓存方案在 source/common/tls/client_context_impl.h 中定义了三层结构struct SniSessionCacheEntry { std::string sni; bssl::UniquePtrSSL_SESSION session; }; using SniSessionCacheList std::listSniSessionCacheEntry; struct SniSessionBucket { // Iterators into sni_session_keys_lru_, newest first for this SNI. std::dequeSniSessionCacheList::iterator sessions; };sni_session_keys_lru_SniSessionCacheList一个全局的 LRU 链表每个条目记录会话所属的 SNIsession_keys_by_sni_absl::flat_hash_mapstd::string, SniSessionBucket以 SNI 字符串为键的哈希表每个桶内用一个std::deque保存指向 LRU 链表节点的迭代器按最新在前排序两者均受session_keys_mu_absl::Mutex保护保证多线程并发下的安全性。这种“哈希分桶 全局 LRU 链表”的组合使得按 SNI 精确取用会话与跨 SNI 全局淘汰可以同时高效实现。2. “生效 SNI”的确定顺序所谓“生效 SNI”指实际写入 ClientHello 的 SNI它由 ClientContextImpl::effectiveSni() 按以下优先级确定TransportSocketOptions中的serverNameOverride传输层套接字选项级覆盖启用了auto_host_sni时上游主机的 hostnamehost-hostname()兜底使用配置中的sniUpstreamTlsContext.sni。在 ClientContextImpl::newSsl() 中生效 SNI 既会被设置进 BoringSSLSSL_set_tlsext_host_name还会被存入 SSL 对象的 ex-datasslEffectiveSniIndex以便后续的“新会话回调”能够拿到与 ClientHello 完全一致的缓存键。即使连接不发送 SNI也会用空字符串作为合法缓存键源码注释。3. 会话的存入newSessionKey当 BoringSSL 为客户端学到一个新会话时会触发 ClientContextImpl::newSessionKey()若max_session_keys_ 0直接释放会话、不缓存等价于禁用会话恢复若运行时 guardscopeUpstreamTlsSessionCacheBySni关闭则走旧逻辑把会话压入上下文级session_keys_双端队列超出上限时从队尾淘汰若 guard 开启默认则先从 SSL 对象的 ex-data 恢复生效 SNI把会话以{sni, session}形式推入 LRU 链表前端并在session_keys_by_sni_对应桶的前端登记该节点。淘汰策略当缓存总数超过max_session_keys_时从 LRU 链表末尾淘汰全局最久未使用的会话无论它属于哪个 SNI并同步清理对应桶若桶因此变空则删除该 SNI 的桶源码。这保证了max_session_keys_的原有语义——“整个客户端上下文缓存会话的总数上限”——不被破坏。4. 会话的取用setSessionForSni建立新连接时newSsl() 根据运行时 guard 决定走哪条路径guard 开启调用setSessionForSni(ssl, server_name_indication)在 该函数 中只从当前 SNI 对应的桶里取用会话且优先使用最新的那个guard 关闭调用setSessionFromContextCache(ssl)源码这是刻意忽略 SNI 的旧行为回退路径。一个值得注意的细节是 TLS 1.3 的会话票据通常是单次使用single-use的代码通过SSL_SESSION_should_be_single_use(session)判断若为单次使用票据则在安装到新 SSL 对象后立即从缓存中移除避免重复使用同一票据否则如可复用会话只将其提升到 LRU 前端以更新“最近使用”次序。5. 运行时机如何启用会话缓存会话缓存并非默认关闭其启用与max_session_keys的取值直接相关。在 ClientContextImpl 构造函数 中仅当max_session_keys_ 0时才设置SSL_CTX_set_session_cache_mode(..., SSL_SESS_CACHE_CLIENT)并注册新会话回调。配置实践max_session_keys 的语义与典型用法max_session_keys定义在 api/envoy/extensions/transport_sockets/tls/v3/tls.protoMaximum number of session keys (Pre-Shared Keys for TLSv1.3, Session IDs and Session Tickets for TLSv1.2 and older) to be stored for session resumption. Defaults to 1, setting this to 0 disables session resumption.即默认值为1设为0则完全禁用会话恢复。参数解析位于 source/common/tls/context_config_impl.ccPROTOBUF_GET_WRAPPED_OR_DEFAULT(config, max_session_keys, 1)。一个典型的上游 TLS 配置示例在UpstreamTlsContext中大致如下transport_socket: name: envoy.transport_sockets.tls typed_config: type: type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext common_tls_context: validation_context: trusted_ca: filename: /etc/ssl/certs/ca-certificates.crt sni: api.example.com auto_host_sni: true max_session_keys: 128要点解读sni与auto_host_sni共同决定生效 SNI启用auto_host_sni后会优先使用上游主机的 hostname 作为 SNI这正是“多个上游逻辑主机共享一个客户端上下文”时的典型场景也是本次修复重点保护的场景max_session_keys控制该客户端上下文可缓存的会话总数上限。按 SNI 隔离后缓存依然受此总量约束只是取用和淘汰都更为精确按 SNI 取、全局 LRU 淘汰。运行时回退scope_upstream_tls_session_cache_by_sni该运行时 guard 在 source/common/runtime/runtime_features.cc 中注册RUNTIME_GUARD(envoy_reloadable_features_scope_upstream_tls_session_cache_by_sni);guard 的读取点位于 client_context_impl.cc 的scopeUpstreamTlsSessionCacheBySni()通过Runtime::runtimeFeatureEnabled(...)判断并在两个关键路径上影响行为存入会话newSessionKey决定走上下文级队列还是 SNI 分桶缓存取用会话newSsl决定按 SNI 取用还是从上下文级缓存取用。如需临时恢复旧行为例如在升级窗口内做 A/B 对比或排查异常可按照 Envoy 常规的运行时层bootstrapruntime的 overlay/symlink 层将envoy.reloadable_features.scope_upstream_tls_session_cache_by_sni置为false也可通过 admin 接口的运行时管理能力动态调整。请务必注意该开关只是临时回退手段会重新引入跨 SNI 复用会话的风险应尽快切回默认的按 SNI 隔离行为。验证与排查建议确认版本包含该修复查看对应版本的 changelogs/current/bug_fixes/tls__scope-upstream-session-cache-by-sni.rst 是否存在于发布分支检查运行时 guard 生效状态通过 admin 接口读取运行时键值确认envoy.reloadable_features.scope_upstream_tls_session_cache_by_sni未被覆盖为false观察会话复用行为在启用会话恢复的上游集群上观察多 SNI 场景下的握手日志与 TLS 统计修复后同一客户端上下文内不同 SNI 的会话不会交叉复用max_session_keys仍作为缓存总量的硬上限生效。小结本次变更让 Envoy 的上游 TLS 会话缓存从“上下文级、不分 SNI”升级为“按生效 SNI 精确分桶、全局 LRU 淘汰”的机制彻底消除了跨 SNI 复用会话带来的 TLS 身份错配隐患。理解effectiveSni()的确定顺序、newSessionKey/setSessionForSni的存取路径以及运行时 guard 的回退位置将帮助你在多主机、多 SNI 的上游 TLS 场景下正确地配置和运维会话恢复并在需要时安全地完成行为回退与验证。/DSMLparameter /DSMLinvoke /DSMLtool_calls【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →