现代网络基础设施中的 TCP 握手之下
TCP 3 次握手
在最简单的形式中,TCP 三次握手很容易理解,并且有 大量在线材料都在讨论这个问题。(如果你能读懂 Chinease,你可以看看我之前的一篇文章。
然而,在实际中理解、练习和解决 TCP 问题 世界是另一回事。随着容器平台开始主宰世界,随着 以及 service-mesh 成为底层的下一个重大转变 网络基础设施,这些平台中的现代网络功能使 TCP 相关问题更加复杂。在传统观点中,这些问题 可能看起来相当奇怪。
本文将介绍其中两种情况。你看到时有什么想法 下面两张图片?
方案 1 的 TCP 流有问题
方案 2 的有问题的 TCP 流
1. 场景 1
1.1 现象:SYN -> SYN+ACK -> RST
客户端发起了与服务器的连接,服务器立即确认 (SYN+ACK),但客户端在收到此数据包时重置了它,并继续等待 用于来自服务器的下一个 SYN+ACK。经过多次 retransmit 和 reset, 连接终于超时了。
1.2 捕获
tcpdump 输出:
<span style="color:#333333"><span style="background-color:#f8f8f8"><span style="background-color:#f6f8fa"><code>1 18:56:40.353352 IP 10.4.26.45.35582 <span style="color:#000000"><strong>></strong></span> 10.4.26.234.80: Flags <span style="color:#000000"><strong>[</strong></span>S], <span style="color:#0086b3">seq </span>853654705, win 29200, length 0
2 18:56:40.353506 IP 10.4.26.11.80 <span style="color:#000000"><strong>></strong></span> 10.4.26.45.35582: Flags <span style="color:#000000"><strong>[</strong></span>S.], <span style="color:#0086b3">seq </span>914414059, ack 853654706, win 28960, length 0
3 18:56:40.353521 IP 10.4.26.45.35582 <span style="color:#000000"><strong>></strong></span> 10.4.26.11.80: Flags <span style="color:#000000"><strong>[</strong></span>R], <span style="color:#0086b3">seq </span>853654706, win 0, length 0
4 18:56:41.395322 IP 10.4.26.45.35582 <span style="color:#000000"><strong>></strong></span> 10.4.26.234.80: Flags <span style="color:#000000"><strong>[</strong></span>S], <span style="color:#0086b3">seq </span>853654705, win 29200, length 0
5 18:56:41.395441 IP 10.4.26.11.80 <span style="color:#000000"><strong>></strong></span> 10.4.26.45.35582: Flags <span style="color:#000000"><strong>[</strong></span>S.], <span style="color:#0086b3">seq </span>930694343, ack 853654706, win 28960, length 0
6 18:56:41.395457 IP 10.4.26.45.35582 <span style="color:#000000"><strong>></strong></span> 10.4.26.11.80: Flags <span style="color:#000000"><strong>[</strong></span>R], <span style="color:#0086b3">seq </span>853654706, win 0, length 0
</code></span></span></span>
哪里
- 客户:
10.4.26.45
- 服务器:,在端口提供 HTTP 服务
10.4.26.234
80
怎么了?在继续之前,请考虑一下这一点。
1.3 分析
让我们试着深入了解发生了什么:
#1
:客户端启动了与服务器的连接,使用src_port=35582,dst_port=80
#2
:服务器已确认 (SYN+ACK)#3
:客户端重置服务器的 SYN+ACK 数据包#4
:超时,客户端重传#1
#5
:服务器已确认(仍为 SYN+ACK)#4
#6
:客户端再次被拒绝 (, SYN+ACK)#5
此 TCP 流的时间序列在此处重新描述:
图 1.1 有问题的 TCP 流
乍一看,这似乎很奇怪,因为服务器确认了客户端的请求, 而 Client 端在收到后立即重置此数据包,然后一直等待 Next 来自服务器的 SYN+ACK(而不是关闭此连接尝试)。它 甚至在超时时重新传输第一个 SYN 数据包(注意到它使用与 do 相同的临时端口)。#4
#1
1.4 根本原因
注意这一点:客户端假设服务器在 ,为什么 SYN+ACK 数据包 ( 和 ) 来自 ?通过一些调查, 我们发现:该服务器部署为 K8S ExternalIP Service,作为 VIP(ExternalIP)和 PodIP。10.4.26.234
#2
#4
10.4.26.11
10.4.26.11
10.4.26.234
1.4.1 简短的回答
客户端连接到服务器,目标 IP 为 server,但 server (实例)回复了其真实 IP (PodIP)。IP 不匹配使客户相信 SYN+ACK 数据包无效,因此拒绝了它们。
1.4.2 长答案
首先,我们位于 Cilium 驱动的 K8S 集群中。 Cilium 将生成 BPF 规则,以将流量负载均衡到此 VIP 添加到其所有后端 Pod 中。正常流量路径如图 1.1 所示:
图 1.2 客户端和服务器实例之间的正常数据流
- @Client:客户端向服务器发送流量
VIP
- @ClientHost:Cilium 做 DNAT,把 VIP 改成它的 (backend 实例 IP)
PodIP
- @ServerHost:路由到 IP 为
PodIP
- @Server:服务器实例回复为自己的
PodIP
- @ServerHost:将回复数据包路由到客户端主机
- @ClientHost:Cilium 进行 SNAT,将服务器的 schange 为 ,然后转发 到客户端实例的流量
PodIP
VIP
- @Client:客户端接收流量。从它自己的角度来看, received packet 只是前一个发送的 packet (两者都是 ),因此它接受该数据包。3 次握手完成。
src_ip
dst_ip
VIP
当客户端和服务器位于同一主机上时,会出现此问题,其中 的情况下,步骤 6 不是由 Cilium 实现的,如图 1.2 所示:
图 1.3 客户端和服务器位于同一主机上时的数据流
我们已经报告了这个问题,并确认了一个错误,请参阅此 问题了解更多详情。
2. 场景 2
2.1 现象:握手正常,传输数据时连接重置
客户端成功启动了与服务器的 TCP 连接(3 个数据包),但是,在 发送第一个数据包(总共第 4 个数据包),连接得到 由 Server 立即重置。
2.2 捕获
<span style="color:#333333"><span style="background-color:#f8f8f8"><span style="background-color:#f6f8fa"><code>1 12:10:30.083284 IP 10.6.2.2.51136 <span style="color:#000000"><strong>></strong></span> 10.7.3.3.8080: Flags <span style="color:#000000"><strong>[</strong></span>S], <span style="color:#0086b3">seq </span>1658620893, win 29200, length 0
2 12:10:30.083513 IP 10.6.3.3.8080 <span style="color:#000000"><strong>></strong></span> 10.7.2.2.51136: Flags <span style="color:#000000"><strong>[</strong></span>S.], <span style="color:#0086b3">seq </span>2918345428, ack 1658620894, win 28960, length 0
3 12:10:30.083612 IP 10.6.2.2.51136 <span style="color:#000000"><strong>></strong></span> 10.7.3.3.8080: Flags <span style="color:#000000"><strong>[</strong></span>.], ack 1, win 229, length 0
4 12:10:30.083899 IP 10.6.2.2.51136 <span style="color:#000000"><strong>></strong></span> 10.7.3.3.8080: Flags <span style="color:#000000"><strong>[</strong></span>P.], <span style="color:#0086b3">seq </span>1:107, ack 1, win 229, length 106
5 12:10:30.084038 IP 10.6.3.3.8080 <span style="color:#000000"><strong>></strong></span> 10.7.2.2.51136: Flags <span style="color:#000000"><strong>[</strong></span>.], ack 107, win 227, length 0
6 12:10:30.084251 IP 10.6.3.3.8080 <span style="color:#000000"><strong>></strong></span> 10.7.2.2.51136: Flags <span style="color:#000000"><strong>[</strong></span>R.], <span style="color:#0086b3">seq </span>1, ack 107, win 227, length 0
</code></span></span></span>
同样,在继续之前考虑这一点是值得的。
2.3 分析
#1
:客户端启动了与服务器的连接,src_port=51136,dst_port=8080
#2
:服务器已确认 (SYN+ACK)#3
: 客户端已确认服务器,TCP 连接成功建立#4
:客户端发送了一个字节数据包106
#5
: 服务器已确认#4
#6
:服务器在之后立即重置此连接#5
此 TCP 流的时间序列在此处重新描述:
图 2.1 有问题的 TCP 流的时间序列
2.4 根本原因
客户端看到一个如图 2.1 所示的拓扑:
图 2.2 两侧的客户端视图
它发起了一个连接,该连接被服务器成功接受,即 3 次握手完成,没有任何错误。但是在传输数据时, 服务器立即拒绝了此连接。因此,问题必须存在于 服务器端。
深入研究服务器端,我们发现一个 sidecar(具体来说是 envoy) 已注入到服务器端容器。如果你不熟悉这个 word,请参考 Istio 的一些介绍性文档。 简而言之,sidecar 充当服务器容器和 外面的世界:
- 在 Ingress 方向上,它会拦截到 Server 的所有 Ingress 流量,做一些 处理,然后将允许的流量转发到 Server
- 在 egress 方向上,它会拦截来自服务器的所有 egress 流量,再次执行 some 处理,并将允许的流量转发到外部世界。
流量拦截是通过 Istio 中的 iptables 规则实现的。 详细实现的解释在本文的范围之外, 但如果您有兴趣,可以参考附录 A 中的图表。
这就是魔力所在:客户端和 server 直接访问,但拆分为 2 个单独的连接:
- 客户端和 sidecar 之间的连接
- Sidecar 和 Server 之间的连接
这两个连接是独立的握手,因此即使后者 失败,前者仍然可以成功。
图 2.3 双方的实际视图:一个中间人坐在客户端和服务器之间
这就是确切发生的情况:由于某些内部原因,服务器无法启动 错误,但 Client 和 sidecar 之间的连接已建立。什么时候 客户端开始发送数据包,sidecar 先 ack 接收,然后 将此转发到(失败的)服务器,但被拒绝。然后它意识到 后端服务不可用,因此关闭 (RST) 了 自身和 Client 端。
图 2.4 sidecar 和服务器之间的连接未建立
3. 结束语
在现代,底层网络基础设施越来越强大 且灵活,但代价是堆栈深度更深,并且构成更多 开发人员和维护人员的故障排除挑战。这不可避免 需要更深入地了解网络基础设施、虚拟化 技术、内核堆栈等。
4. 附录 A:Istio Sidecar 拦截
图 4.1 使用 iptables 规则的 Istio sidecar 拦截(入站)
对应的 iptables 规则:
<span style="color:#333333"><span style="background-color:#f8f8f8"><span style="background-color:#f6f8fa"><code><span style="color:#999988"><em># get the Pod netns</em></span>
<span style="color:#008080">$ </span>docker inspect <Container ID or Name> | <span style="color:#0086b3">grep</span> <span style="color:#dd1144">\"</span>Pid<span style="color:#dd1144">\"</span>
<span style="color:#dd1144">"Pid"</span>: 82881,
<span style="color:#999988"><em># show iptables rules in Pod netns</em></span>
<span style="color:#008080">$ </span>nsenter <span style="color:#000080">-t</span> 82881 <span style="color:#000080">-n</span> iptables <span style="color:#000080">-t</span> nat <span style="color:#000080">-nvL</span>
Chain PREROUTING <span style="color:#000000"><strong>(</strong></span>policy ACCEPT 1725 packets, 104K bytes<span style="color:#000000"><strong>)</strong></span>
pkts bytes target prot opt <span style="color:#000000"><strong>in </strong></span>out <span style="color:#0086b3">source </span>destination
2086 125K ISTIO_INBOUND tcp <span style="color:#000080">--</span> <span style="color:#000000"><strong>*</strong></span> <span style="color:#000000"><strong>*</strong></span> 0.0.0.0/0 0.0.0.0/0
Chain INPUT <span style="color:#000000"><strong>(</strong></span>policy ACCEPT 2087 packets, 125K bytes<span style="color:#000000"><strong>)</strong></span>
pkts bytes target prot opt <span style="color:#000000"><strong>in </strong></span>out <span style="color:#0086b3">source </span>destination
Chain OUTPUT <span style="color:#000000"><strong>(</strong></span>policy ACCEPT 465 packets, 29339 bytes<span style="color:#000000"><strong>)</strong></span>
pkts bytes target prot opt <span style="color:#000000"><strong>in </strong></span>out <span style="color:#0086b3">source </span>destination
464 27840 ISTIO_OUTPUT tcp <span style="color:#000080">--</span> <span style="color:#000000"><strong>*</strong></span> <span style="color:#000000"><strong>*</strong></span> 0.0.0.0/0 0.0.0.0/0
Chain POSTROUTING <span style="color:#000000"><strong>(</strong></span>policy ACCEPT 498 packets, 31319 bytes<span style="color:#000000"><strong>)</strong></span>
pkts bytes target prot opt <span style="color:#000000"><strong>in </strong></span>out <span style="color:#0086b3">source </span>destination
Chain ISTIO_INBOUND <span style="color:#000000"><strong>(</strong></span>1 references<span style="color:#000000"><strong>)</strong></span>
pkts bytes target prot opt <span style="color:#000000"><strong>in </strong></span>out <span style="color:#0086b3">source </span>destination
362 21720 ISTIO_IN_REDIRECT tcp <span style="color:#000080">--</span> <span style="color:#000000"><strong>*</strong></span> <span style="color:#000000"><strong>*</strong></span> 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080
Chain ISTIO_IN_REDIRECT <span style="color:#000000"><strong>(</strong></span>1 references<span style="color:#000000"><strong>)</strong></span>
pkts bytes target prot opt <span style="color:#000000"><strong>in </strong></span>out <span style="color:#0086b3">source </span>destination
362 21720 REDIRECT tcp <span style="color:#000080">--</span> <span style="color:#000000"><strong>*</strong></span> <span style="color:#000000"><strong>*</strong></span> 0.0.0.0/0 0.0.0.0/0 redir ports 15001
Chain ISTIO_OUTPUT <span style="color:#000000"><strong>(</strong></span>1 references<span style="color:#000000"><strong>)</strong></span>
pkts bytes target prot opt <span style="color:#000000"><strong>in </strong></span>out <span style="color:#0086b3">source </span>destination
0 0 ISTIO_REDIRECT all <span style="color:#000080">--</span> <span style="color:#000000"><strong>*</strong></span> lo 0.0.0.0/0 <span style="color:#000000"><strong>!</strong></span>127.0.0.1
420 25200 RETURN all <span style="color:#000080">--</span> <span style="color:#000000"><strong>*</strong></span> <span style="color:#000000"><strong>*</strong></span> 0.0.0.0/0 0.0.0.0/0 owner UID match 1337
0 0 RETURN all <span style="color:#000080">--</span> <span style="color:#000000"><strong>*</strong></span> <span style="color:#000000"><strong>*</strong></span> 0.0.0.0/0 0.0.0.0/0 owner GID match 1337
11 660 RETURN all <span style="color:#000080">--</span> <span style="color:#000000"><strong>*</strong></span> <span style="color:#000000"><strong>*</strong></span> 0.0.0.0/0 127.0.0.1
33 1980 ISTIO_REDIRECT all <span style="color:#000080">--</span> <span style="color:#000000"><strong>*</strong></span> <span style="color:#000000"><strong>*</strong></span> 0.0.0.0/0 0.0.0.0/0
Chain ISTIO_REDIRECT <span style="color:#000000"><strong>(</strong></span>2 references<span style="color:#000000"><strong>)</strong></span>
pkts bytes target prot opt <span style="color:#000000"><strong>in </strong></span>out <span style="color:#0086b3">source </span>destination
33 1980 REDIRECT tcp <span style="color:#000080">--</span> <span style="color:#000000"><strong>*</strong></span> <span style="color:#000000"><strong>*</strong></span> 0.0.0.0/0 0.0.0.0/0 redir ports 15001</code></span></span></span>