最近我们生产环境升级了 contour 到1.4.0版本,用户反映偶发404问题。
经过简单测试,只在通过浏览器访问启用了https的网站上会偶发404。
我们的一个项目httpproxy如下:
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata:
name: hawkeye-grafana
namespace: sgt
spec:
virtualhost:
fqdn: hawkeye.xx.me
tls:
secretName: https-xx-me-new
routes:
- conditions:
- prefix: /
services:
- name: hawkeye-grafana
port: 80
查看envoy的accesslog 可以看到:
[2020-04-26T22:28:27.120Z] "GET / HTTP/2" 404 NR 0 0 0 - "10.107.8.251" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.122 Safari/537.36" "45fc064a-3124-47f2-b49e-cffb6de031e9" "hawkeye.xx.me" "-"
考虑是和SNI相关。
此时万能的github 搜索一下。果然已经有其他人踩到坑了。
I've found a similar behavior after upgrading as well. It appears to be related to http2 connection coalescing. The SNI (envoyauthority
) will not match the requested host name and Envoy will 404 the connection similar to how it behaves in #1493 . So far I've only seen it impact users on Mozilla/Firefox which fits since it appears to have the most aggressive connection coalescing from what I've read. Pretty certain this is due to #2381 , but given the Envoy CVE it probably shouldn't be reverted until Envoy comes up with a fix.
I was able to work around it by issuing separate certs for each virtualhost and updating thehttpproxy
to use the cert for that virtualhost instead of using a wildcard that covered them all.
本质上是envoy的一个bug(Envoy does not adhere to HTTP/2 RFC 7540)导致的。
大致原理是:浏览器会非常积极地复用HTTP/2连接:当浏览器打开与www.example.com
的连接并在TLS握手期间显示*.example.com
的证书时,它将在所有情况下重新使用此连接,只要主机名解析为相同的IP(某些浏览器甚至不关心它),所有的请求都被路由到*.example.com
。
只要所有*.example.com
主机名都由同一侦听器/过滤器链提供服务,这在Envoy中就不成问题,因为路由是基于每个请求而不是每个连接进行的。
但是,如果www.example.com
(带有*.example.com
证书)由一个侦听器/过滤器链提供服务,而app.example.com
由另一个侦听器/过滤器链提供服务,则存在问题,因为连接是在连接的整个生命周期内都锁在单个侦听器/过滤器链上,如果首先建立了与www.example.com
的连接,则对app.example.com
的请求将使用www.example.com
的配置在同一连接上合并,然后转发到错误的后端。
很可惜,目前envoy并没有修复。不过社区给出了两种解决方案。
- 一种解决方案是将421错误定向的请求响应发送给未在给定侦听器/过滤器链上配置的主机名请求(但是如果配置了
*.example.com
,则将不起作用),或者将421错误定向的请求响应发送给请求用于在其他侦听器/过滤器链上配置的主机名(但这需要所有已配置主机名的全局列表)。 - 另一种解决方案是使用HTTP/2 ORIGIN框架(RFC8336)在给定的侦听器/过滤器链上广播允许的主机名(但这也需要全局列表,并且只有少数客户端支持此扩展名)。
对于第一种方案,大致三种思路:
- 如果您可以为RBAC过滤器DENY指定HTTP响应代码,该怎么办?然后,配置了HCM的管理服务器可以为其在该HCM上允许的服务器名称添加RBAC策略,并在DENY上生成421。
- 管理服务器可以在Lua过滤器中对SNI服务器名称检查进行编程,如果不匹配则生成421。
- 添加可以使用可接受的SNI服务器名称配置的专用过滤器。
contour 选择envoy作为数据层,避不开该问题,团队最终选择了第二种思路来解决。
利用lua 实现了一个TLS错误请求的过滤器。
具体代码如下:
func FilterMisdirectedRequests(fqdn string) *http.HttpFilter {
code := `
function envoy_on_request(request_handle)
local headers = request_handle:headers()
local host = headers:get(":authority")
if host ~= "%s" then
request_handle:respond({
[":status"] = "421",
},
""
)
end
end
`
return &http.HttpFilter{
Name: "envoy.filters.http.lua",
ConfigType: &http.HttpFilter_TypedConfig{
TypedConfig: protobuf.MustMarshalAny(&lua.Lua{
InlineCode: fmt.Sprintf(code, fqdn),
}),
},
}
}
TLS路由专用于唯一的虚拟主机名。但是,如果使用通配符证书,即使完整的原始主机名并不匹配,浏览器也会积极合并并重用服务器连接。这就会发生404的错误响应,因为每个TLS虚拟主机只有一个路由到一个主机上。
如果不匹配虚拟主机的FQDN,通过生成421来避免这种行为泄露给用户。在这种情况下,应该使用浏览器了解该请求未得到处理,然后将其重新发送到新的连接。
当然这只是一个临时方案,真正的解决还需要envoy的彻底修复。