不能则学,不知则问
前言
之前提到了一个关于,当时的疑惑是在 PMTU 场景下,按照以前的理解,在收到 ICMP 需要分片的消息时(包含有下一跳 MTU),是直接会根据新的 MTU 进行重新分包重传的,详见下图所示。
但近期的一个实验现象,详见下图,却把这个结论推翻了,触发出来的重传数据包,竟然还是原先的 MTU,会是什么原因?
问题分析
现在我找到答案了,还是基础知识没掌握好,当时一些细节问题没想清楚,竟然没想到是 TSO/GSO 的问题。
对于 TSO/GSO ,网上资料介绍的也很多,就不再赘述了,以下直接引用一段说明:
TSO (TCP Segmentation Offload) 是一种利用网卡分割大数据包,减小 CPU 负荷的一种技术,也被叫做 LSO (Large segment offload) ,如果数据包的类型只能是 TCP,则被称之为 TSO,如果硬件支持 TSO 功能的话,也需要同时支持硬件的 TCP 校验计算和分散 - 聚集 (Scatter Gather) 功能。
TSO 是使得网络协议栈能够将大块数据推送至网卡,然后网卡执行分片工作,这样减轻了 CPU 的负荷,但 TSO 需要硬件来实现分片功能;而性能上的提高,主要是因为延缓分片而减轻了 CPU 的负载,因此,可以考虑将 TSO 技术一般化,因为其本质实际是延缓分片,这种技术,在 Linux 中被叫做 GSO(Generic Segmentation Offload),它比 TSO 更通用,原因在于它不需要硬件的支持分片就可使用,对于支持 TSO 功能的硬件,则先经过 GSO 功能,然后使用网卡的硬件分片能力执行分片;而对于不支持 TSO 功能的网卡,将分片的执行,放在了将数据推送的网卡的前一刻,也就是在调用驱动的 xmit 函数前。
问题测试
对于 TSO/GSO 开启的情况下,最直观的就是发送端通过 tcpdump 所捕获的数据包是可以看到大分段的,因为分片是在网卡执行,而 tcpdump 抓包是发生在网卡执行之前。
packetdrill 脚本测试如下,在 TSO/GSO 默认开启的情况下,MSS 为 1000,然后尝试发送大小为 2000 的数据包。
# cat tcp_troubleshooting_4_001.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 10000 <mss 1000,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
+0.01 write(4,...,2000) = 2000
+0 `sleep 1`
#
执行脚本,同时通过 tcpdump 抓取数据包,现象如下,可以看到第 4 个数据包的长度即为 2000 字节,尽管 MSS 为 1000 字节。
# packetdrill tcp_troubleshooting_4_001.pkt
#
# tcpdump -i any -nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
21:49:07.618996 tun0 In IP 192.0.2.1.34837 > 192.168.232.5.8080: Flags [S], seq 0, win 10000, options [mss 1000,nop,nop,sackOK], length 0
21:49:07.619034 tun0 Out IP 192.168.232.5.8080 > 192.0.2.1.34837: Flags [S.], seq 3518945590, ack 1, win 64240, options [mss 1460,nop,nop,sackOK], length 0
21:49:07.629130 tun0 In IP 192.0.2.1.34837 > 192.168.232.5.8080: Flags [.], ack 1, win 10000, length 0
21:49:07.639224 tun0 Out IP 192.168.232.5.8080 > 192.0.2.1.34837: Flags [P.], seq 1:2001, ack 1, win 64240, length 2000: HTTP
21:49:07.666685 tun0 Out IP 192.168.232.5.8080 > 192.0.2.1.34837: Flags [P.], seq 1001:2001, ack 1, win 64240, length 1000: HTTP
21:49:07.882701 tun0 Out IP 192.168.232.5.8080 > 192.0.2.1.34837: Flags [.], seq 1:1001, ack 1, win 64240, length 1000: HTTP
21:49:08.334717 tun0 Out IP 192.168.232.5.8080 > 192.0.2.1.34837: Flags [.], seq 1:1001, ack 1, win 64240, length 1000: HTTP
21:49:08.641935 ? Out IP 192.168.232.5.8080 > 192.0.2.1.34837: Flags [F.], seq 2001, ack 1, win 64240, length 0
21:49:08.641981 ? In IP 192.0.2.1.34837 > 192.168.232.5.8080: Flags [R.], seq 1, ack 1, win 10000, length 0
#
然后关闭 TSO/GSO 的情况下,再次测试。
# cat tcp_troubleshooting_4_002.pkt
`ethtool -K tun0 tso off
ethtool -K tun0 gso off`
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 10000 <mss 1000,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
+0.01 write(4,...,2000) = 2000
+0 `sleep 1`
#
执行脚本,同时通过 tcpdump 抓取数据包,现象如下,可以看到在尝试发送 2000 字节大小的数据段时,因为 MSS 为 1000 字节的缘故,会分成两个数据包 No.4 和 5 各为 1000 字节大小。
# packetdrill tcp_troubleshooting_4_002.pkt
#
# tcpdump -i any -nn port 8080
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
21:53:19.718982 tun0 In IP 192.0.2.1.51785 > 192.168.36.3.8080: Flags [S], seq 0, win 10000, options [mss 1000,nop,nop,sackOK], length 0
21:53:19.719011 tun0 Out IP 192.168.36.3.8080 > 192.0.2.1.51785: Flags [S.], seq 704148368, ack 1, win 64240, options [mss 1460,nop,nop,sackOK], length 0
21:53:19.729108 tun0 In IP 192.0.2.1.51785 > 192.168.36.3.8080: Flags [.], ack 1, win 10000, length 0
21:53:19.739395 tun0 Out IP 192.168.36.3.8080 > 192.0.2.1.51785: Flags [.], seq 1:1001, ack 1, win 64240, length 1000: HTTP
21:53:19.739416 tun0 Out IP 192.168.36.3.8080 > 192.0.2.1.51785: Flags [P.], seq 1001:2001, ack 1, win 64240, length 1000: HTTP
21:53:19.766664 tun0 Out IP 192.168.36.3.8080 > 192.0.2.1.51785: Flags [P.], seq 1001:2001, ack 1, win 64240, length 1000: HTTP
21:53:19.982696 tun0 Out IP 192.168.36.3.8080 > 192.0.2.1.51785: Flags [.], seq 1:1001, ack 1, win 64240, length 1000: HTTP
21:53:20.430688 tun0 Out IP 192.168.36.3.8080 > 192.0.2.1.51785: Flags [.], seq 1:1001, ack 1, win 64240, length 1000: HTTP
21:53:20.744724 ? Out IP 192.168.36.3.8080 > 192.0.2.1.51785: Flags [F.], seq 2001, ack 1, win 64240, length 0
21:53:20.744748 ? In IP 192.0.2.1.51785 > 192.168.36.3.8080: Flags [R.], seq 1, ack 1, win 10000, length 0
#
回到说 PMTU 场景下的问题,我也用两个脚本分别说明下过程。
packetdrill 脚本测试如下,首先仍然在 TSO/GSO 默认开启的情况下,MSS 为 1460,然后尝试发送大小为 1460 的数据包,之后在收到 ICMP 需要分片的消息(下一跳MTU 为 1000)。
# cat tcp_troubleshooting_4_003.pkt
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 10000 <mss 1460,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
+0.01 write(4, ..., 1460) = 1460
+0 write(4, ..., 200) = 200
+0.01 < icmp unreachable frag_needed mtu 1000 [1:1461(1460)]
+0 `sleep 3`
#
执行脚本,同时通过 tcpdump 抓取数据包,现象如下,可以看到首先发出了 No.4 1460 和 No.5 200 字节大小的数据包,之后在收到 ICMP 需要分片的消息(下一跳 MTU 为 1000),MTU/MSS 分别更新为 1000/960,并立马触发出一个 TCP 简单重传。
但此时由于 TSO/GSO 开启,分片的工作是在网卡上,而在网卡之前工作的 tcpdump 仍然是可以抓取到 1460 大小的数据包(尽管大于 MSS 960),而在实际出网卡后,才会真正变成两个 960+500 字节大小的数据包,也因为如此,在得不到 ACK 确认的情况下,会进行超时重传,而超时重传的数据包则为 960 字节大小,因为 TSO/GSO 实际是延迟分片发送,所以确切知道实际是分为两个数据包,超时重传时重传第一个。
# packetdrill tcp_troubleshooting_4_003.pkt
#
# tcpdump -i any -nn host 192.0.2.1
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
22:23:50.638965 tun0 In IP 192.0.2.1.45535 > 192.168.254.54.8080: Flags [S], seq 0, win 10000, options [mss 1460,nop,nop,sackOK], length 0
22:23:50.638995 tun0 Out IP 192.168.254.54.8080 > 192.0.2.1.45535: Flags [S.], seq 2307014636, ack 1, win 64240, options [mss 1460,nop,nop,sackOK], length 0
22:23:50.649095 tun0 In IP 192.0.2.1.45535 > 192.168.254.54.8080: Flags [.], ack 1, win 10000, length 0
22:23:50.659350 tun0 Out IP 192.168.254.54.8080 > 192.0.2.1.45535: Flags [P.], seq 1:1461, ack 1, win 64240, length 1460: HTTP
22:23:50.659403 tun0 Out IP 192.168.254.54.8080 > 192.0.2.1.45535: Flags [P.], seq 1461:1661, ack 1, win 64240, length 200: HTTP
22:23:50.669426 tun0 In IP 192.0.2.1 > 192.168.254.54: ICMP 192.0.2.1 unreachable - need to frag (mtu 1000), length 36
22:23:50.669458 tun0 Out IP 192.168.254.54.8080 > 192.0.2.1.45535: Flags [P.], seq 1:1461, ack 1, win 64240, length 1460: HTTP
22:23:50.882711 tun0 Out IP 192.168.254.54.8080 > 192.0.2.1.45535: Flags [.], seq 1:961, ack 1, win 64240, length 960: HTTP
22:23:51.310724 tun0 Out IP 192.168.254.54.8080 > 192.0.2.1.45535: Flags [.], seq 1:961, ack 1, win 64240, length 960: HTTP
22:23:52.174668 tun0 Out IP 192.168.254.54.8080 > 192.0.2.1.45535: Flags [.], seq 1:961, ack 1, win 64240, length 960: HTTP
22:23:53.671941 tun0 Out IP 192.168.254.54.8080 > 192.0.2.1.45535: Flags [F.], seq 1661, ack 1, win 64240, length 0
22:23:53.672004 tun0 In IP 192.0.2.1.45535 > 192.168.254.54.8080: Flags [R.], seq 1, ack 1, win 10000, length 0
#
继续在关闭 TSO/GSO 的情况下测试,仍然是 MSS 为 1460,然后尝试发送大小为 1460 的数据包,之后在收到 ICMP 需要分片的消息(下一跳MTU 为 1000)。
# cat tcp_troubleshooting_4_004.pkt
`ethtool -K tun0 tso off
ethtool -K tun0 gso off`
0 socket(..., SOCK_STREAM, IPPROTO_TCP) = 3
+0 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
+0 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
+0 bind(3, ..., ...) = 0
+0 listen(3, 1) = 0
+0 < S 0:0(0) win 10000 <mss 1460,nop,nop,sackOK>
+0 > S. 0:0(0) ack 1 <...>
+0.01 < . 1:1(0) ack 1 win 10000
+0 accept(3, ..., ...) = 4
+0.01 write(4, ..., 1460) = 1460
+0 write(4, ..., 200) = 200
+0.01 < icmp unreachable frag_needed mtu 1000 [1:1461(1460)]
+0 `sleep 3`
#
执行脚本,同时通过 tcpdump 抓取数据包,现象如下,可以看到首先发出了 No.4 1460 和 No.5 200 字节大小的数据包,之后在收到 ICMP 需要分片的消息(下一跳 MTU 为 1000),MTU/MSS 分别更新为 1000/960,并立马触发出一个 TCP 简单重传。
但此时由于 TSO/GSO 关闭,因此不会推迟到网卡上分段,而直接发送 MSS 大小的数据包,这样 1460 的数据段就会直接分为 960+500 字节大小的数据包,即 No.7 和 8。之后在得不到 ACK 确认的情况下,同样会进行超时重传,而超时重传的数据包为 960 字节大小。
# packetdrill tcp_troubleshooting_4_004.pkt
#
# tcpdump -i any -nn host 192.0.2.1
tcpdump: data link type LINUX_SLL2
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on any, link-type LINUX_SLL2 (Linux cooked v2), snapshot length 262144 bytes
22:24:56.679775 tun0 In IP 192.0.2.1.35701 > 192.168.213.154.8080: Flags [S], seq 0, win 10000, options [mss 1460,nop,nop,sackOK], length 0
22:24:56.679926 tun0 Out IP 192.168.213.154.8080 > 192.0.2.1.35701: Flags [S.], seq 2851577991, ack 1, win 64240, options [mss 1460,nop,nop,sackOK], length 0
22:24:56.690269 tun0 In IP 192.0.2.1.35701 > 192.168.213.154.8080: Flags [.], ack 1, win 10000, length 0
22:24:56.700465 tun0 Out IP 192.168.213.154.8080 > 192.0.2.1.35701: Flags [P.], seq 1:1461, ack 1, win 64240, length 1460: HTTP
22:24:56.700482 tun0 Out IP 192.168.213.154.8080 > 192.0.2.1.35701: Flags [P.], seq 1461:1661, ack 1, win 64240, length 200: HTTP
22:24:56.710507 tun0 In IP 192.0.2.1 > 192.168.213.154: ICMP 192.0.2.1 unreachable - need to frag (mtu 1000), length 36
22:24:56.710545 tun0 Out IP 192.168.213.154.8080 > 192.0.2.1.35701: Flags [.], seq 1:961, ack 1, win 64240, length 960: HTTP
22:24:56.710547 tun0 Out IP 192.168.213.154.8080 > 192.0.2.1.35701: Flags [P.], seq 961:1461, ack 1, win 64240, length 500: HTTP
22:24:56.922676 tun0 Out IP 192.168.213.154.8080 > 192.0.2.1.35701: Flags [.], seq 1:961, ack 1, win 64240, length 960: HTTP
22:24:57.358672 tun0 Out IP 192.168.213.154.8080 > 192.0.2.1.35701: Flags [.], seq 1:961, ack 1, win 64240, length 960: HTTP
22:24:58.222698 tun0 Out IP 192.168.213.154.8080 > 192.0.2.1.35701: Flags [.], seq 1:961, ack 1, win 64240, length 960: HTTP
22:24:59.713859 ? Out IP 192.168.213.154.8080 > 192.0.2.1.35701: Flags [F.], seq 1661, ack 1, win 64240, length 0
22:24:59.713884 ? In IP 192.0.2.1.35701 > 192.168.213.154.8080: Flags [R.], seq 1, ack 1, win 10000, length 0
#
实验总结
在细微处发现问题解决问题,总是一个有趣的过程。
另外在第一张图片中实际还有一个有意思的现象,在收到 ICMP 分片需要数据包后,不仅重传了之前的 No.11 数据包(超过 PMTU),而且还一并重传了 No.12 数据包(尽管没有超过 PMTU),分成了 No.14 和 No.15 数据包。
而最后一张图片中的现象并不是如此,在收到 ICMP 分片需要数据包后,仅仅重传了之前的 No.4 数据包(超过 PMTU),并没有一并重传 No.5 数据包。
我对此的猜测可能是 packetdrill 的缘故,可能因为 skb clone 并没有满足重组重传的条件,有待确认。🤔
往期推荐
推荐站内搜索:最好用的开发软件、免费开源系统、渗透测试工具云盘下载、最新渗透测试资料、最新黑客工具下载……
还没有评论,来说两句吧...