我与服务器 (owner-api.teslamotors.com) 建立了 SSL 连接,该连接通过 wget、curl 或 openssl s_client 挂起。我展示的是 curl 版本,因为它提供了最多的调试消息:
# curl -iv https://owner-api.teslamotors.com
* Trying 18.203.70.200:443...
* Connected to owner-api.teslamotors.com (18.203.70.200) port 443 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: /etc/ssl/certs
ClientHello 之后它就挂起了。TCP 连接已成功建立(也已通过 telnet/nc 确认)。其他网络连接(包括我尝试过的任何 SSL 连接)都可以正常工作。除了owner-api.teslamotors.com:443。
我发现这个帖子谈论的是 MTU,听起来有些牵强。但我减少了服务器 MTU,它就起作用了!它适用于任何 <= 1420 的 MTU。
服务器使用以太网 (MTU 1500) 连接到 Mikrotik 路由器,然后从那里通过 WireGuard 隧道 (MTU 1420)。我知道这可能不是最佳选择,因为来自服务器 >1420 的任何 IP 数据包都需要分段。但是,这与任何 L4 协议无关。TCP 上的 SSL 不应该关心分段和 MTU。然而,这个主机关心。
我在 Mikrotik 盒子上运行了数据包捕获,流量没有列出任何异常:
典型的 TCP 握手是第 1-3 次,然后是 ClientHello(第 4-5 次)和 ServerHello(第 6-7 次)。没有数据包大小接近 MTU,也没有其他 ICMP 消息表明存在碎片等问题。
根据评论,以下是 tracepath 输出:
# tracepath owner-api.teslamotors.com
1?: [LOCALHOST] pmtu 1500
1: XX.XX.56.210 0.410ms
1: XX.XX.56.210 0.198ms
2: XX.XX.56.210 0.138ms pmtu 1420
2: XX.XX.56.185 151.394ms
3: no reply
4: 100.100.200.1 157.220ms
5: 10.75.0.193 154.068ms
6: 10.75.2.53 161.950ms asymm 7
7: decix1.amazon.com 152.107ms asymm 8
8: decix2.amazon.com 153.068ms
9: no reply
[...]
我真的不知道这里到底发生了什么。
为什么这个 SSL 连接失败?
问题的核心是服务器不知道这样的数据包需要分片。
有一些大数据包(例如
Certificate
来自服务器4的 TLS 消息),但您的捕获看不到它们,因为它们大于您的 MTU,所以它们永远不会到达您的终端。这实际上是问题所在;如果这些数据包到达您的网络接口(这样它们在数据包捕获中可见),那么连接就不会挂起。捕获需要在隧道的“上游”端进行,具体来说,是在“低 MTU”接口前一步的入口接口上进行。因此,如果路径是“互联网 → 服务器 eth0 ⇒ 服务器 wg0 → 客户端 wg-foo ⇒ 客户端 ether1”,那么大数据包将在“服务器 eth0”上可见,但无法放入“服务器 wg0”中。因此,在 wg0 上捕获不会给您带来任何结果,但在 eth0 上捕获可能会显示一系列:
(请注意,硬件接收卸载可能会产生令人困惑的结果,因为您的以太网网卡可能会将各段合并为一个超级数据包,例如在终端主机本身上捕获时。如果您看到大小超过 2kB 的数据包,则可能需要
ethtool -K eth0 gso off gro off
在捕获期间这样做。)在 TCP 握手期间,客户端(实际上是两个对等体)声明其可以接收的 TCP MSS(最大 TCP 段大小)。由于客户端通常拥有无限内存1并且不限于小段,因此它实际上提供最大的 MSS,该 MSS 是它计算出的对于其已知的 MTU 而言最佳的 MSS,以避免需要进行 IP 级分段。
例如,如果您的以太网接口的 MTU 为 1500,那么您的操作系统可能会提供 1460 的 MSS,它恰好适合 IP 有效负载(假设 IPv4 开销为 20,TCP 开销再次为 20,这是最简单的情况)。
因此,降低客户端网络接口的 MTU 将导致它预先声明一个较小的可接受的 TCP 段大小,这会导致服务器始终发送较小的 IP 数据包(即保持在需要分段的限制以下),就像你减少了服务器的 MTU 一样。
同时,使用默认的 1500 MTU,服务器将以大型 IP 数据包的形式发送大型段,直到它从您的 ISP 网关(具有低 MTU 链接并且无法将这些数据包转发给您的网关)收到 ICMP“需要分段”;然后服务器将记录到您的新 PMTU 并开始在 IP 级别发送分段的段。5
但是,如果任何防火墙阻止3 ICMP 错误到达服务器,这种情况就不会发生,服务器将永远尝试在同一个大型 IP 数据包中发送该 TCP 段。(或者,如果服务器位于某种类型的防火墙后面,该防火墙会重新组装和重新分段通过它的所有 IP 数据包,那么它可能正确地分段了数据包,但防火墙可能会撤消其所有工作。)
网关(例如带有 nftables/iptables 的 Linux)通常具有修补通过它们的 TCP 握手的通告 MSS 的功能,以适合网关知道的 MTU ,例如当客户端位于 1500 字节 MTU 以太网上但网关即将通过 1420 字节 MTU PPPoE 隧道转发数据包时:
如果我的理解正确,TCP 必须关心 MTU,因为依赖 IP 碎片会降低 TCP 重传的效率——即使只有一个碎片“丢失”,那么整个 IP 数据包就会“丢失”,并且不会有任何碎片被传送到上层协议。
例如,如果 TCP 发送了一个 64k 的段,该段被分割成 45 个 IP 数据报,并且其中一个丢失,那么在 ICMP“重组时间已超出”之后,所有 IP 数据报都需要重新传输。(这是假设分段完全有效,但正如您所见,有时它不起作用。3)
而将相同的 64k 数据分成适合 IP MTU 的 TCP 段后,其他 44 个 IP 数据包仍将传送到接收方的 TCP 层并进行 SACK,只有丢失的数据包才需要重新传输(我认为这甚至可能在服务器收到指示漏洞的 SACK 后立即发生,而不是长时间的重组超时)。
4使用 TLSv1.2 时,“证书”消息可以清晰显示,但使用 TLSv1.3 时会加密,因此捕获时只会将其视为“应用程序数据”。
1大多数开发人员都是这么认为的。
2例如 Untangle 的默认“brouting”模式。
3也称为“PMTUD 黑洞”。请参阅 Cloudflare 博客文章 #1、#2和#3,了解一种情况,这种情况发生的原因并非系统管理员全面阻止 ICMP。
5我实际上不知道它是否在 IP 级别对相同的段进行分段,或者它是否也降低了该连接的 TCP MSS。实际上可能是后者。
正如另一个答案所解释的那样,它挂起的原因是因为服务器回复大于 MTU并且ICMP 错误没有到达服务器。
之所以只发生在这个服务器上,是因为它是目前唯一一个发送这么大回复的服务器。
如果我卷曲https://owner-api.teslamotors.com
18.203.70.200 是它将owner-api.teslamotors.com解析到的IP地址(经过三个CNAME之后,最终到达AWS主机,您可能会收到一个不同的主机),203.0.113.16作为客户端。
请注意,Server Hello 是一个 2958 字节的 TCP 数据包。这显然不符合 MTU 的规定。
wireshark 在其中识别出一个
TLSv1.3 Record Layer: Handshake Protocol: Server Hello
122 字节的文件,后面跟着一个TLSv1.3 Record Layer: Change Cipher Spec Protocol: Change Cipher Spec
6 字节的文件,但这并不能真正识别出发生了什么。设置后
--tls-max 1.2
我们得到更易读的数据:我们仍然得到 2958 字节的 Server hello,但现在它没有加密,所以我们可以“读取”它包含一个证书:
编号为 4478 的数据包实际上是由两个数据包构成的,从前一个数据包开始:
其内容如下:
后面跟着一个
TLSv1.2 Record Layer: Handshake Protocol: Server Hello Done
只有 9 个字节的 a。因此,它仅在这台服务器上失败的原因是它尝试发送一个接近 3000 字节的 TCP 数据包(嵌入 3010 字节的证书),而这在您尝试过的其他站点上不会发生(不过,还会有更多失败。不要丢弃这些 ICMP 错误,并使用匹配的 MTU(如果可能)。