Envoy 开启 HTTP2 后偶现 404 如何解决?
问题背景
在大部分基于 Envoy 实现的网关里,都存在这样一个问题,当开启 http2 时,客户端访问会出现偶发的 404,并且可以从日志注意到这些 404 的请求,:authority 头里的域名和 SNI 里的域名不一致。
且在使用泛域名证书,且配置了多个域名的路由的情况下,这个问题特别容易出现。问题相关的社区 issue:
- https://github.com/envoyproxy/envoy/issues/6767
- https://github.com/istio/istio/issues/13589
- https://github.com/projectcontour/contour/issues/1493
问题成因
为什么:authority 头和 SNI 不一致
这个问题涉及到客户端的连接复用机制,对于 http2 来说,连接多路复用的能力是对比 http1 的一个核心差异。特别是对于浏览器场景,尽可能的连接复用,可以在开启 TLS 的场景下,显著优化页面加载时间(不考虑队头阻塞的情况下)。在 http2 的 RFC 规约里对于连接复用也有以下描述:
Connections that are made to an origin server, either directly or through a tunnel created using the CONNECT method (Section 8.3),MAY be reused for requests with multiple different URI authority components. A connection can be reused as long as the origin server is authoritative(Section 10.1). For TCP connections without TLS,this depends on the host having resolved to the same IP address.
所以如 Chrome 浏览器就会在达成以下条件时,对一个域名 B 的请求,复用和域名 A 建立的 http2 连接:
- 域名 B 和域名 A 是解析到同一个 IP。
- 跟域名 A 建立通信时获取的证书的 Common Name 是 wildcard,且可以匹配到域名 B;或者 SAN 中存在域名 B。
一旦和域名 A 建立的连接上发送了域名 B 的请求,就出现了上面说的,在网关侧的日志中看到 SNI 和 :authority 头不匹配的问题了。
为什么产生 404
在 Envoy 的网关里,常见的 SNI 和域名路由的映射方式是一对一的,这样匹配到 SNI A 时,只会出现 A 域名的路由配置,没有 B 域名的路由,所以产生了 404。
具体来说,常见的 Envoy 配置组织方式是每个 SNI 有自己独立的 filter chain,而且这个 filter chain 中的 HCM 配置里的 RDS 配置也是独立的。
解决方案
方案一:使用相同证书的域名复用同一个 filter chain
这个问题严格来说不是 envoy 的 bug,而是配置组织不当的问题,通过相同证书的域名复用 filter chain 就可以解决。
这个方案有两处不足:
- 如果当域名的证书发生了修改,需要重建 filter chain,就会影响 downstream 连接断开。
- 增加了控制面的复杂度,例如使用 Gateway API 时,不能基于 filter chain 和 Gateway 中 Listener 一对一映射。
方案二:基于 HTTP 421 状态码
常见的是通过 lua filter 来返回 421,例如:
"@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua" inlineCode: | function envoy_on_request(request_handle) local streamInfo = request_handle:streamInfo() if streamInfo:requestedServerName() ~= "" then if (string.sub(streamInfo:requestedServerName(), 1, 2) == "*." and not string.find(request_handle:headers():get(":authority"), string.sub(streamInfo:requestedServerName(), 2))) then request_handle:respond({[":status"] = "421"}, "Misdirected Request") end if (string.sub(streamInfo:requestedServerName(), 1, 2) ~= "*." and streamInfo:requestedServerName() ~= request_handle:headers():get(":authority")) then request_handle:respond({[":status"] = "421"}, "Misdirected Request") end end end
这也是基于 HTTP2 RFC 的建议:
In some deployments, reusing a connection for multiple origins can result in requests being directed to the wrong origin server. For example, TLS termination might be performed by a middlebox that uses the TLS Server Name Indication (SNI) [TLS-EXT] extension to select an origin server. This means that it is possible for clients to send confidential information to servers that might not be the intended target for the request, even though the server is otherwise authoritative.A server that does not wish clients to reuse connections can indicate that it is not authoritative for a request by sending a 421 (Misdirected Request) status code in response to the request (see Section 9.1.2).
这个方案也有两处不足:
- 失去了连接复用的特性,对于一些依赖 HTTP2 连接复用实现页面加载优化的场景,可能是不接受的。
- HTTP 421 存在版本兼容问题,特别是在中国有很多基于低版本 chromium 构建的 Hybrid Android APP,存在对不同域名复用连接,但是又不支持基于 421 重新建连的问题,返回 421 会直接造成业务报错。
方案三:filter chains 共用路由配置
1. 基于 RDS
所有 https 的 filter chain 如果都共用同一份 RDS,可以解决这个问题,但是会造成 RDS 资源过大的问题,资源过大,就无法通过 delta xDS 等方案去做增量推送优化。而且 RDS 里任何资源变化,导致资源 checksum 修改,就会使得 Envoy 加载配置时需要对整个 RDS 重新做一遍配置解析和数据结构生成,造成主线程 CPU 过高。
2. 基于 VHDS
目前的 VHDS 方案是基于 onDemand 的,只有当前请求的路由找不到域名配置时,会触发向 xDS 服务器拉取配置。这样会将数据面的流量穿透给控制面,如果有大量404的请求会对控制面造成压力,而且控制面的可用性直接影响到数据面的可用性。
3. 基于 SRDS
目前 Envoy 支持基于指定 Header 来做路由配置切片(Scoped RDS)。看之前的设计,是为了能对不同 cookie 走不同的路由配置。可以对 SRDS 做扩展来支持按域名切片进行匹配路由。扩展的关键点是:
- 要支持 wildcard domain,支持前缀匹配。
- 不同的 port 下相同域名逻辑可能不同,例如 80 下可能是强制跳转。
下面是 Higress 对 ScopedRoutes 做的扩展在配置上的体现:
// [#next-free-field: 6] message ScopedRoutes { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.ScopedRoutes"; ... ... message HostValueExtractor { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.ScopedRoutes.ScopeKeyBuilder." "FragmentBuilder.HostValueExtractor"; // The maximum number of host superset recomputes. If not specified, defaults to 100. google.protobuf.UInt32Value max_recompute_num = 1; } message LocalPortValueExtractor { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.network.http_connection_manager.v2.ScopedRoutes.ScopeKeyBuilder." "FragmentBuilder.LocalPortValueExtractor"; } oneof type { option (validate.required) = true; // Specifies how a header field's value should be extracted. HeaderValueExtractor header_value_extractor = 1; // Extract the fragemnt value from the :authority header, and support recompute with the wildcard domains, // i.e. ``www.example.com`` can be recomputed with ``*.example.com``, then ``*.com``, then ``*``. HostValueExtractor host_value_extractor = 101; // Extract the fragment value from local port of the connection. LocalPortValueExtractor local_port_value_extractor = 102; } } // The final(built) scope key consists of the ordered union of these fragments, which are compared in order with the // fragments of a :ref:`ScopedRouteConfiguration<envoy_v3_api_msg_config.route.v3.scopedrouteconfiguration>`. // A missing fragment during comparison will make the key invalid, i.e., the computed key doesn't match any key. repeated FragmentBuilder fragments = 1 [(validate.rules).repeated = {min_items: 1}]; }
共用路由配置的安全性考虑
在所有 filter chains 共用路由配置的情况下,因为不同的 filter chain 可能有不同的认证策略,比如常见的需要认证客户端证书(mTLS)的情况,或者基于 IP 的 RBAC 等。
贸然将所有路由都暴露到任意的 filter chain 上是不安全的。
可以考虑的方案,是由控制面来识别这一安全隐患,在发现某个域名必须通过特定 filter chain 的认证后才能访问时,进行对应的保护。
Higress 是给 VirtualHost 增加配置项 allow_server_names,例如在开启 mTLS 时,会配置只允许请求带特定 SNI 的情况下才能访问:
// [#protodoc-title: HTTP route components] // * Routing :ref:`architecture overview <arch_overview_http_routing>` // * HTTP :ref:`router filter <config_http_filters_router>` // The top level element in the routing configuration is a virtual host. Each virtual host has // a logical name as well as a set of domains that get routed to it based on the incoming request's // host header. This allows a single listener to service multiple top level domain path trees. Once // a virtual host is selected based on the domain, the routes are processed in order to see which // upstream cluster to route to or whether to perform a redirect. // [#next-free-field: 24] message VirtualHost { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.VirtualHost"; ... ... // If non-empty, a list of server names (such as SNI for the TLS protocol) is used to determine // whether this request is allowed to access this VirutalHost. If not allowed, 421 Misdirected Request will be returned. // // The server name can be matched whith wildcard domains, i.e. ``www.example.com`` can be matched with // ``www.example.com``, ``*.example.com`` and ``*.com``. // // Note that partial wildcards are not supported, and values like ``*w.example.com`` are invalid. // // This is useful when expose all virtual hosts to arbitrary HCM filters (such as using SRDS), and you want to make // mTLS-protected routes invisible to requests with different SNIs. // // .. attention:: // // See the :ref:`FAQ entry <faq_how_to_setup_sni>` on how to configure SNI for more // information. repeated string allow_server_names = 101; }
是否有安全隐患?
以 httpd 为例的一些传统 HTTP 代理软件并不支持当 :authority 和 SNI 不一致时进行路由。而 Nginx 应该是比较早实现这一特性的网关,也有人给 Nginx 社区提了 issue,认为有安全隐患:https://trac.nginx.org/nginx/ticket/1694
这是 Nginx 当时维护者的答复,说的很明白,并没有安全隐患:
In theory, you are right. SNI was designed to be used with the only one name, and requesting different names over a connection which uses SNI is not correct. QuotingRFC 6066:If the server_name is established in the TLS session handshake, the client SHOULD NOT attempt to request a different server name at the application layer.But in practice, SPDY introduced so-called "connection reuse", which effectively uses a connection with an established SNI for request to different application-level names. And it is followed byHTTP/2 connection reuse, which does the same: a HTTP/2 client can request a different host over an already established connection.The 421 (Misdirected Request) status code, also introduced by HTTP/2 RFC, is expected to be used only when such a connection reuse is not possible due to server limitations. In nginx, 421 is returned when a client tries to request a server protected with client SSL certificates over a connection established to a different server.
技术是在不断发展的,RFC 6066 制订时,还没有出现 HTTP/2 多路复用这样的技术,所以认为客户端在一条使用 SNI 的连接上发送不同域名的请求是不正确的。
但随着 Web 技术的发展,前端页面承载的内容愈加丰富,页面并发请求愈加增多,不论是对客户端还是服务端都提出了更高性能,更快响应的要求,SPDY 以及 HTTP/2 顺应而生,面对 TLS 下昂贵的连接开销,连接复用应当“应用尽用”。既然对于授信的服务端连接,通过 HTTPS 证书认证出了服务端能合法地处理其他域名的请求,那么,在连接上发送和 SNI 不同域名的请求也完全是安全的。
对于这样合理的客户端需求,网关自然应该承接。从用户体验的角度看,可以通过连接复用提升页面渲染或接口访问速度;从更大的视角看,也是让信息的传递,在保障安全的前提下,删繁就简,更加节能低碳。</faq_how_to_setup_sni></config_http_filters_router></arch_overview_http_routing></envoy_v3_api_msg_config.route.v3.scopedrouteconfiguration>

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
OpenAI 宕机思考丨Kubernetes 复杂度带来的服务发现系统的风险和应对措施
作者:王建伟(正己) 12 月 11 日,OpenAI 旗下 AI 聊天机器人平台 ChatGPT、视频生成工具 Sora 及其面向开发人员的 API 自太平洋时间下午 3 点左右起发生严重中断,耗费约三个小时才顺利恢复所有服务。 OpenAI 在事后报告中写道,"该问题源自新部署的遥测服务,此项服务无意间压垮了 Kubernetes 控制平面,导致关键系统发生连锁故障。引发事故的根本原因就是新的遥测服务配置意外在大规模集群中产生了大量 Kubernetes API 负载,导致控制平面不堪重负并破坏了基于 DNS 的服务发现能力。" 可见,即使如实力强大的 OpenAI,面对复杂 Kubernetes 架构,也不能很好处理 Kubernetes 服务发现和控制面解耦的问题。造成这个问题的关键原因在于容器调度和业务关键服务发现链路耦合在一起,互相干扰,Kubernetes 控制面故障影响了业务服务发现链路。那么,Kubernetes 体系下应如何选择服务发现系统,进一步提升业务稳定性呢? 笔者认为,大型业务的服务发现系统应该具备高可靠性,高可伸缩性,高性能及高可维护性等特点,采用独立服务...
- 下一篇
海外玩家开山立派,中国软件出海怎么赢得用户青睐?
12 月 26 日,开源中国邀请观测云 CEO 蒋烁淼将、亚马逊云科技 SA Manager 梁风飚、OceanBase CEO杨冰、AutoMQ 联合创始人兼 CEO 王小瑞、Bytebase 联合创始⼈兼 CEO 陈天舟等企业家做客【开源漫谈】直播间,探讨我国基础软件如何出海。 期间,大家聊到了中国软件要怎么在全球市场赢得客户青睐。 扫码查看直播回放: 以下内容根据直播整理: 蒋烁淼:小瑞,你们的产品在GitHub上的表现显然已经远超过国外的竞争对手。从技术上来看,是这样的吗?在市场这块,你们有什么策略来赢得全球用户的青睐? 王小瑞:先说说我们为什么要开发 AutoMQ这个软件?因为 Apache Kafka,已经是一个十年前设计的产物了。如今,当我们从云的角度重新审视这个产品时,发现它并不是那么适应云基础设施的需求。所以说,在云环境下,我们绝对是领先的。 然而,在开展海外业务的过程中,我们也遇到了不少挑战。海外玩家在数仓、可观测性等很多领域已经实现开山立派,成为了足以定义该技术的角色。 云给了我们在全球市场跟海外产品公平竞争的机会。当然了,在云上,用户考量的维度就很多了,而不仅仅...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- 2048小游戏-低调大师作品
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- MySQL8.0.19开启GTID主从同步CentOS8
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS7,CentOS8安装Elasticsearch6.8.6
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2全家桶,快速入门学习开发网站教程