最近我们生产环境升级了 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 (envoy authority) 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 the httpproxy 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的彻底修复。


iyacontrol
1.4k 声望2.7k 粉丝

专注kubernetes,devops,aiops,service mesh。