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

Envoy 内部结构

用两张证书分用同一个端口

在 TT Lab 中继续学习

目标

在一个监听器中放置多条过滤器链,通过 SNI、传输协议、ALPN 来区分接收。并亲自确认条件重叠时谁会获胜。

为什么重要

在一台入口网关接收几十个域名的架构中,“哪条链接收这个连接”就等于“发出哪张证书”。如果把这条规则误解成顺序,就会把链上下移来移去,白白耗掉一整天。实际的判定是按八个固定顺序的步骤比较具体程度,而且没有匹配的链时,得到的不是错误响应,连接会直接断开——这两点亲手体验一次,“只有这个域名不行”的报告几分钟就能处理完。

步骤

  1. 在 /root/envd-listener/listener.yaml 中放置两个监听器——edge-a 在 127.0.0.1:10021 上返回 listener=a,edge-b 在 127.0.0.1:10022 上返回 listener=b,均使用 direct_response(管理端口为 9921)。启动后,把 /listeners?format=json 保存到 /root/envd-listener/01-listeners.json。
  2. 在 /root/envd-listener/tls/ 中创建两张自签名证书——a.crt/a.key 的 CN 和 SAN 为 a.envd.test,b.crt/b.key 的 CN 和 SAN 为 b.envd.test。把两张证书的 subject 和 SAN 写入 /root/envd-listener/02-certs.txt。
  3. 在 /root/envd-listener/listener.yaml 中添加监听器 mixed(127.0.0.1:10443)。启用 tls_inspector 监听器过滤器,并用 server_names 区分两条 TLS 链——a.envd.test 返回 chain=sni-a,b.envd.test 返回 chain=sni-b。把分别用这两个名称请求的结果,以 a= 和 b= 两行写入 /root/envd-listener/03-sni.txt。
  4. 用 openssl s_client 向 10443 连接两次,只改变 -servername,把各自返回的证书,以 a= 和 b= 两行写入 /root/envd-listener/04-cert.txt(值为证书的 CN)。
  5. 在 mixed 监听器中,最前面添加一条匹配 transport_protocol: "raw_buffer" 的链 plain,让它返回 chain=plain。向同一个端口 10443 分别发送明文 HTTP 请求和 TLS 请求,把结果以 plain= 和 tls= 两行写入 /root/envd-listener/05-raw.txt。
  6. 在 mixed 中添加第四条链 alpn-h2——只匹配 transport_protocol: "tls" 和 application_protocols: ["h2"],并以 HTTP/2 返回 chain=alpn-h2。然后分别确认:把 SNI 设为 a.envd.test 并使用 HTTP/2 请求时由哪条链接收,把 SNI 设为 z.envd.test 并使用 HTTP/2 请求时由哪条链接收,并把结果以 sni_and_h2= 和 h2_only= 两行写入 /root/envd-listener/06-alpn.txt。
  7. 用 z.envd.test 发送 HTTP/1.1 的 TLS 请求——它不匹配任何链。在 /root/envd-listener/07-nomatch.txt 中写入 nomatch_rc=(该请求的 curl 退出码)和 match_rc=(用 a.envd.test 发送的同样请求的退出码)两行。
  8. 在 /root/envd-listener/08-report.md 中写入 chains=(mixed 监听器的链数量)、same_port_plain_and_tls=(yes 或 no)、sni_beats_alpn=(第 6 步中 SNI 和 ALPN 同时匹配时获胜的链的名称)、nomatch_rc= 四行,并在下面至少写四行学到的内容。

参考

一个进程监听两个端口

在 /root/envd-listener/listener.yaml 中放置两个监听器——edge-a 在 127.0.0.1:10021 上返回 listener=a,edge-b 在 127.0.0.1:10022 上返回 listener=b,均使用 direct_response(管理端口为 9921)。启动后,把 /listeners?format=json 保存到 /root/envd-listener/01-listeners.json。

监听器的单位是“一个地址加一个端口”。一个进程可以有多个监听器,每个监听器都有各自的过滤器链列表。使用 direct_response 时,不需要上游就能返回响应,所以本实验不用启动后端。管理端口的 /listeners 默认是文本格式,需要加上 ?format=json 才会输出结构。

生成两张名称不同的证书

在 /root/envd-listener/tls/ 中创建两张自签名证书——a.crt/a.key 的 CN 和 SAN 为 a.envd.test,b.crt/b.key 的 CN 和 SAN 为 b.envd.test。把两张证书的 subject 和 SAN 写入 /root/envd-listener/02-certs.txt。

要用 SNI 区分过滤器链,至少需要两张名称不同的证书。一行 openssl req -x509 -newkey rsa:2048 -nodes 就能同时生成密钥和证书。请不要漏掉 -addext "subjectAltName=DNS:..."——如今的客户端不看 CN,只看 SAN。用 openssl x509 -noout -subject -ext subjectAltName 确认。

同一个端口,按名称由不同的链接收

在 /root/envd-listener/listener.yaml 中添加监听器 mixed(127.0.0.1:10443)。启用 tls_inspector 监听器过滤器,并用 server_names 区分两条 TLS 链——a.envd.test 返回 chain=sni-a,b.envd.test 返回 chain=sni-b。把分别用这两个名称请求的结果,以 a= 和 b= 两行写入 /root/envd-listener/03-sni.txt。

SNI 以明文形式包含在 TLS 握手的第一条消息中。所以在解密之前就能读取,负责这件事的就是 tls_inspector 监听器过滤器。不启用这个过滤器,server_names 条件就不会匹配任何连接。请求用 curl -k --resolve a.envd.test:포트:127.0.0.1 https://...(占位符为端口)发送。

链不同,发出的证书也不同

用 openssl s_client 向 10443 连接两次,只改变 -servername,把各自返回的证书,以 a= 和 b= 两行写入 /root/envd-listener/04-cert.txt(值为证书的 CN)。

每条链都有各自的 transport_socket,所以选中哪条链,就等于发出哪张证书。这就是一台入口网关能够持有几十个域名证书的原因。用 echo | openssl s_client -connect 127.0.0.1:포트 -servername 이름 2>/dev/null | openssl x509 -noout -subject(占位符依次为端口与域名)确认。

在一个端口上同时接收明文和 TLS

在 mixed 监听器中,最前面添加一条匹配 transport_protocol: "raw_buffer" 的链 plain,让它返回 chain=plain。向同一个端口 10443 分别发送明文 HTTP 请求和 TLS 请求,把结果以 plain= 和 tls= 两行写入 /root/envd-listener/05-raw.txt。

tls_inspector 会查看第一个字节,判定这个连接是不是 TLS,并把 transport_protocol 设为 tls 或 raw_buffer。所以同一个端口可以同时接收两种连接。明文请求用 curl http://127.0.0.1:포트/(占位符为端口),TLS 请求的方式与前一步相同。链的顺序不影响匹配——由条件的具体程度决定。

两个条件都匹配时,谁会获胜

在 mixed 中添加第四条链 alpn-h2——只匹配 transport_protocol: "tls" 和 application_protocols: ["h2"],并以 HTTP/2 返回 chain=alpn-h2。然后分别确认:把 SNI 设为 a.envd.test 并使用 HTTP/2 请求时由哪条链接收,把 SNI 设为 z.envd.test 并使用 HTTP/2 请求时由哪条链接收,并把结果以 sni_and_h2= 和 h2_only= 两行写入 /root/envd-listener/06-alpn.txt。

过滤器链匹配会按固定顺序的八个步骤逐步缩小范围——目标端口、目标 IP、服务器名称(SNI)、传输协议、应用协议(ALPN),然后是各种来源条件。SNI 排在 ALPN 之前,这是本步骤的关键。请亲自观察两个条件同时匹配时留下的是什么。用 curl --http2 -k --resolve ... 让 ALPN 变为 h2。

不匹配任何链时,得到的不是响应,而是沉默

用 z.envd.test 发送 HTTP/1.1 的 TLS 请求——它不匹配任何链。在 /root/envd-listener/07-nomatch.txt 中写入 nomatch_rc=(该请求的 curl 退出码)和 match_rc=(用 a.envd.test 发送的同样请求的退出码)两行。

没有匹配的链时,Envoy 不会返回 404——因为请求根本没有到达 HTTP 层,它直接关闭连接。从客户端看,这表现为 TLS 握手失败,curl 会给出非 0 的退出码。在生产环境中,“证书加了,连接却直接断开”的报告,很大一部分就是这个原因。curl 的退出码用 $? 获取。

留下过滤器链设计备忘

在 /root/envd-listener/08-report.md 中写入 chains=(mixed 监听器的链数量)、same_port_plain_and_tls=(yes 或 no)、sni_beats_alpn=(第 6 步中 SNI 和 ALPN 同时匹配时获胜的链的名称)、nomatch_rc= 四行,并在下面至少写四行学到的内容。

这份备忘是下次设计网关时你自己要读的文字。不要只写值,要写“所以今后要注意什么”。链的数量不要用眼睛数,用 yq 提取 filter_chains 的长度更准确。