记一个诡异的TCP挥手乱序问题

本文内容包括但不限于:tcp四次挥手(同时关闭),tcp包的seq/ack号规则,tcp状态机,内核tcp代码,tcp发送窗口等知识。

问题是什么?

内核版本linux 5.10.112

一句话:四次挥手中,由于fin包和ack包乱序,导致等了一次timeout才关闭连接。

过程细节:

  • 同时关闭的场景,server和client几乎同时向对方发送fin包。
  • client先收到了server的fin包,并回传ack包。
  • 然而server处发生乱序,先收到了client的ack包,后收到了fin包。
  • 结果表现为server未能正确处理client的fin包,未能返回正确的ack包。
  • client没收到(针对fin的)ack包,因此等待超时后重传fin包,之后才回归正常关闭连接的流程。

问题抓包具体分析

图中上半部分是client,下半部分是server。

重点关注id为14913,14914,20622,20623这四个包,后面为了方便分析,对seq和ack号取后四位:

  • 20622(seq=4416,ack=753),client发送的fin包:client主动关闭连接,向server发送fin包;
  • 14913(seq=753,ack=4416),server发送的fin包:server主动关闭连接,向client发送fin包;
  • 20623(seq=4417,ack=754),client响应的ack包:client收到server的fin,响应一个ack包;
  • 14914(seq=754,ack=4416),server发送的ack包;

问题发生在server处(红框位置),发送14913后:

  • 先收到20623(seq=4417),但此时期望收到的seq为4416,所以被标记为[previous segment not captured]
  • 然后收到20622,回传了一个ack包,id为14914,问题就出现在这里:这个数据包的ack=4416,这意味着server还在等待seq=4416的数据包,换言之,fin-20622没有被server真正接收到。
  • client发现20622没有被正确接收,因此在等到timeout后,重新发送了fin包(id=20624),此后连接正常关闭。

(这里再次强调一下ack-20623和fin-20622,后面会经常提到这两个包)

首先,这个现象在直觉上是很不合理的,tcp应当有恰当的机制保证乱序恢复。这里20622和20623都已经到达了server,虽然发生了乱序,也不应当影响server把两者都接收,这是主要的疑问点所在。

经过初步分析,我们推测最可能的原因是20622被server的内核忽略了(原因目前未知)。既然是内核的行为,就先尝试在本地环境复现这个问题。然而喜闻乐见的没有成功。

新问题:尝试复现未成功

为了模拟上述乱序的场景,我们使用两台ecs,在client上伪造tcp包,与server处的正常socket通信。

server处的抓包结果如下:

注意看No.为5,6,7,8的包:

  • 5: server向client发送fin(这里不知为何有一次重传,但是不影响后面的效果,没有深究)
  • 6: client先传回了seq=1002的ack包
  • 7: client后传回了seq=1001的fin包
  • 8: server传回了ack=1002的ack包,ack=1002意味着client的fin包被正常接收了!(如果在问题场景下,此时回传的ack包,ack应当为1001)

之后,为了保持内核版本一致,把相同的程序转移到本地虚拟机上运行,得到同样的结果。换言之,复现失败了。

附:模拟程序代码

工具:python + scapy

这里用scapy伪造client,发送乱序的ack和fin,是为了观察server回传的ack包。因为client并未真的走了tcp协议,所以无论复现成功与否,都不能观察到超时重传。

(1)server处正常socket监听:

import socket

server_ip = "0.0.0.0"
server_port = 12346

server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((server_ip, server_port))
server_socket.listen(5)

connection, client_address = server_socket.accept()

connection.close() #发送fin
server_socket.close()

(2)client模拟乱序:

from scapy.all import *
import time
import sys

target_ip = "略"
target_port = 12346
src_port = 1234

#伪造数据包,建立tcp连接
ip = IP(dst=target_ip)
syn = TCP(sport=src_port, dport=target_port, flags="S", seq=1000)
syn_ack = sr1(ip / syn)
if syn_ack and TCP in syn_ack and syn_ack[TCP].flags == "SA":
    print("Received SYN-A
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值