TT Lab
开始
学习 学习路径 课程

Envoy 内部结构

过滤器链靠具体性取胜,而非顺序

在 TT Lab 中继续学习

一句话总结

监听器是“一个地址加一个端口”,真正接收连接的是其中的过滤器链。由哪条链接收,取决于连接的特征(SNI、传输协议、ALPN、来源),这个判定按八个固定顺序的步骤逐步缩小范围。配置中书写的顺序则毫无关系。

为什么需要它

一台入口网关要接收几十个域名。每个域名的证书不同,有的域名必须以 HTTP/2 接收,内部的健康检查则以明文进入。可以为每个端口单独启动一个进程来解决,但那样地址要分出去那么多,配置也会随之分散到那么多地方。

Envoy 选择了另一条路:端口只保留一个,根据进入该端口的连接的特征,把它分到不同的链。因此一个 443 就能为不同域名提供不同的证书,同一个端口也能接收明文请求。

不了解这个结构,就会在两种症状面前卡住:一种是“证书明明加了,只有这个域名的连接直接断开”,另一种是“把链往上移了,接收请求的却仍然是下面那一条”。二者的原因相同——把链的选择规则误解成了顺序。

工作原理

连接进入后,监听器过滤器先运行。这些过滤器此时只读取字节,不改变任何东西。其中 tls_inspector 做的事情是关键。TLS 握手的第一条消息(ClientHello)是明文,无需解密就能读取,其中包含 SNI(要访问的名称)和 ALPN(想使用的协议列表)。检查器把这两项取出并附加到连接上,顺带还会判定“这个连接是不是 TLS”,把 transport_protocol 设为 tls 或 raw_buffer。

不启用这个过滤器,server_names 和 application_protocols 条件就不会匹配任何连接。配置能通过,只是不起作用,属于最难发现的一类错误。

接下来进行匹配。官方文档记载的顺序如下。

1. 목적지 포트        2. 목적지 IP
3. 서버 이름(SNI)     4. 전송 프로토콜
5. 애플리케이션 프로토콜(ALPN)
6. 직접 연결된 출발지 IP   7. 출발지 유형
8. 출발지 IP          9. 출발지 포트

在每个步骤中,只有匹配得最具体的链才能进入下一个步骤。没有写条件的链意味着“什么都接收”,所以能够留下;写了条件但不匹配的链则当场被淘汰。经过所有步骤后,保证最多只剩下一条链。

由此得出一个在实际工作中经常绊倒人的结论:SNI 条件排在 ALPN 条件之前。所以,当有一条链只写了 server_names: ["a.example.com"],另一条链只写了 application_protocols: ["h2"] 时,如果 a.example.com 收到了 HTTP/2 请求,接收它的是 SNI 那一条。“本来想把 h2 送到那边的链”这句话,就是在这里冒出来的。

通配符也按具体程度比较。当 SNI 为 www.example.com 时,优先级依次为 www.example.com → *.example.com → *.com → 无条件。

在现场相遇的样子

“只有这个域名的连接断开。”没有响应状态码就是线索。404 或 503 说明请求已经到达 HTTP 层,而没有匹配的链时,连接在此之前就被关闭了,所以客户端看到的只是 TLS 握手失败。添加新域名的证书时,忘了把名称加入 server_names,正是这个样子。

“调换链的顺序,结果也一样。”理所当然。顺序不参与匹配。需要调整的是条件的具体程度。

用一个端口同时接收明文和 TLS 的架构。在 Kubernetes 内部,希望健康检查使用明文、来自外部的流量使用 TLS 时,再增加一条 transport_protocol: raw_buffer 链就能解决。这比多开一个端口、多建一条防火墙规则要简单得多。

官方文档:Listeners · FilterChainMatch · TLS Inspector

下一项实验要做什么

亲手生成两张证书,用 SNI 区分过滤器链,在同一个端口上同时接收明文和 TLS,并亲自确认 SNI 与 ALPN 同时匹配时哪一方获胜。最后发送一个不匹配任何链的请求,亲眼看看“不是响应,而是沉默”。