背景
我个人有一台 Hetzner 的独立服务器。Hetner 对于独立服务器,提供的有 Stateless 的防火强服务。
关于无状态(Stateless)防火墙
简而言之,就是防火墙不会去追踪每个链接的信息,仅仅按照条件过滤包的内容。 Hetzner 的防火墙更是仅仅只有四元组(源 IP、目标 IP、源端口、目标端口)这一种过滤条件。
由于懒得处理 Docker 的端口问题,所以很自然而然的打开了这个防火墙服务,并使用了其默认配置的 WebServer 模版。
默认配置如下图:
与此同时,我使用了 Cloudflare 的 Zero Trust 服务,来访问一些内部服务。
问题
在配置好防火墙后的某一天,突然发现,本应经由 Cloudflare ZeroTrust 访问的服务,突然变得无法访问了。
Cloudflare 官方给出的报错非常简单明了,就是 Argo Tunnel 的连接失败。 在查询了 Cloudflare 的后台后,发现这台服务器已经标记为离线状态。
在连接到服务并查询Cloudfalred 的日志,会发现有如下报错信息:
那么,我们可以仅因此判断,时与 Cloudflare 的连接出现了问题。
那么问题来了,是什么东西导致的问题的。
排查与分析
首先的排查对象肯定是对应 IP 的可联通性,我曾经尝试过使用 Curl
来访问服务,发现可以正常获取到服务器的的错误页面。 那么也就是说,单纯的访问这个页面其实是没问题的。
但是,这里的小字,quic
引起了我的注意。 尽管之前曾听说过 QUIC 的大名,但其实除了仅仅在各大防火墙的配置上,以及知道是使用 UDP 来进行通信外,并没有更多的了解。
知道 QUIC 的原因,也仅仅是因为各大防火墙服务,提供了阻止 QUIC 链接的选项。(此时的我并没有意识到为什么要阻止 QUIC)。
Cloudflare 官方是提供的关于 ZeroTrust 和防火墙的文档。
根据这份文档,可以得到的结论是,Cloudflared 使用 QUIC 来进行通讯,因此,需要开放 7844 这个端口的出站。
但是根据 Hetzner 的模版,出站是完全开放的,也就是说不存在任何阻止。
(这期间,我曾经怀疑过是否是 Hetzner 防火墙的问题,使用诸如允许所有链接这样的方法来解决,但是现在看来并没有任何意义。)
即便使用防火墙单独开放 QUIC 的 7844 端口,也依然无法解决问题。
直到最后,我放行了全部的 UDP 访问,QUIC 突然就通了。
最后,所有的定位都定位到了 QUIC 这个协议的问题。
来自 QUIC 的问题
QUIC 的具体工作细节,有很多人都比我更加了解。仅就目前我们可以知道的是,QUIC 使用了 UDP 进行通讯。 由于减少了 TCP 一样的握手过程,整个通讯速度非常快(至少官方是这么说的), 可以做到 0-RTT。
而与我,以及很多人想象的不同,QUIC 并不是要取代 HTTP 应用层传输协议,QUIC 实际上是与 TCP 和 UDP 同属于传输层的协议。
这也就是说,QUIC 实际上能够承载的,不仅仅只有 HTTP 一种。
而 UDP 本身是一种只管发不管理的协议,本身也并不具备任何的可靠性可言。但很明显,QUIC 既然要试图达到 TCP 的效果,自然需要实现某种特定的机制,来解决链接的可靠性问题。
TCP 与防火墙
TCP 对于链接的识别,实际上是基于四元组(源 IP、目标 IP、源端口、目标端口)这样的方式来识别的。 而用户或服务端的网络 IP 发生变化 (例如,用户位于移动网络中),那么,用户与服务器的链接实际上是完全不同的链接。
对于大多数防火墙而言,由于接收方均为固定端口,所以只要设置好目标的端口,那么出站策略为目标 IP 和端口就完全 OK 了.
QUIC 的可靠性
由于 UDP 并没有类似的操作,本质上是只要发出去就算成功,所以 QUIC 自己实现了一套类似的机制。这个机制的名字叫做 Connection ID
。
根据这篇文档,
可以看到 QUIC 的包其实有这个如下结构:
也就是说,整个通讯实际都是依靠 Connection ID
来维持整个通讯的唯一性。
于此同时,QUIC 使用了 TLS 来进行加密,这个 Connection ID
并不能被任何中间人所识别和定位。
根本原因
这个根本原因,其实比任何人想象的都简单,由于 QUIC 使用了 UDP 进行通讯,他也需要一个端口来进行通讯的接收。
问题就出现在这个问题端口上,QUIC 中,客户端的端口,是完全随机决定的!
我们简单梳理下,这个通讯是这样一个逻辑:
- 服务端的端口是确定的,在发送一个 QUIC 请求的时候,客户端会先随机选择一个端口发送 UDP的包。
- 服务端接收到这个包,直接向用户发送 UDP 请求的端口返回。
但是,由于与 TCP 不同,UDP 的包本身不包含任何确认信息(如,HETZNER 的防火墙,就针对 TCP 的 ACK包,做了特殊的处理), 防火墙均无法分辨这个请求是否是用来建立链接的,只能将 UDP 完全 DROP。
解决方案
这个问题到此就很晴朗了,理论上将,只需要开放所有的 UDP 端口,就可以解决这个问题。
但是,开放全部的 UDP 端口,并不是一个很好的策略。
由于 QUIC 并不是百分百普及,仅对于Cloudflare ZeroTrust 而言,我们完全可以选择其基于 HTTP/2 的通讯方式。
使用方法也仅仅是使用 --protocol http2
这个参数即可。
当然,对于其他的有防火墙的情况,关闭 QUIC 在目前来看,并不失为一种选择。