从一个fin 卡顿问题到 scapy 的使用
scapy 使用
scapy 可以绕过内核构造任意网络包
使用比较简单,git clone https://github.com/secdev/scapy 然后在有python3的环境直接可以跑(python2官方说也支持)
注意:
scapy会触发内核发送reset,所以先要在iptables条件一条规则把内核的reset干掉,要不影响scapy的测试
1 2 3
| iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 192.168.0.1 -j DROP
iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 192.168.0.1 -d 192.168.0.2 --sport 1234 --dport 12347 -j DROP
|
因为包是 scapy 绕过OS 构造的,导致 OS 不认识 scapy 模拟发出的包,内核里面没有你这个 Socket记录,只能 Reset你,所以还得用 iptables把这个OS 触发的 Reset 丢掉,否则 Reset 会让对端释放连接,这样测试才能顺利进行
三次握手
用scapy 模拟客户端来进行3次握手
代码:
1 2 3 4
| sport=random.randint(1024,65535) ip=IP(dst="127.0.0.1") SYN=TCP(sport=sport, dport=22345, flags='S', seq=123451000) c=sr1(ip/SYN)
|
完整案例:
1 2 3 4 5 6 7 8 9 10 11
| >>> sport=random.randint(1024,65535) //初始化一个本地随机端口 >>> SYN=TCP(sport=sport, dport=22345, flags='S', seq=123451000) //构造一个连 22345 目标端口的 SYN 包 >>> ip=IP(dst="192.168.0.2") //构造目标地址 >>> sr1(ip/SYN) 发送包 Begin emission: Finished sending 1 packets. .* Received 2 packets, got 1 answers, remaining 0 packets <IP version=4 ihl=5 tos=0x0 len=44 id=0 flags=DF frag=0 ttl=64 proto=tcp chksum=0xb978 src=192.168.0.2 dst=192.168.0.1 |<TCP sport=22345 dport=45814 seq=1599494827 ack=123451001 dataofs=6 reserved=0 flags=SA window=64240 chksum=0x99bb urgptr=0 options=[('MSS', 1460)] |<Padding load=b'\x00\x00' |>>> >>>
|
上面的代码是给对端22345 发了个syn包,然后收到了 syn+ack 包并展示在最后,这一来一回的两个握手包都可以用tcpdump 抓到
服务端的话可以起一个标准http server来验证:
1
| python3 -m http.server 22345
|
对应抓包:
1 2 3 4
| 15:58:43.301867 IP localhost.44633 > ky2.22345: Flags [S], seq 123451000, win 8192, length 0 15:58:43.301929 IP ky2.22345 > localhost.44633: Flags [S.], seq 106274946, ack 123451001, win 64240, options [mss 1460], length 0 15:58:44.361834 IP ky2.22345 > localhost.44633: Flags [S.], seq 106274946, ack 123451001, win 64240, options [mss 1460], length 0 15:58:46.441862 IP ky2.22345 > localhost.44633: Flags [S.], seq 106274946, ack 123451001, win 64240, options [mss 1460], length 0
|
fin 挥手端口卡顿案例
一个奇葩的tcp连接断开的卡顿问题(来自这里 https://mp.weixin.qq.com/s/BxU246Btm2FLt1pppBgYQg ),下面是我对这篇文章问题描述的总结:
1 两端几乎同时发fin, client收到fin回了一个ack
2 client发的ack先fin到达server,server收到ack直接进入time_wait
3 fin到达server被扔掉—-接下来就是要用scapy验证这个fin包被扔掉/忽略了,导致client不能立即断开要等200ms
4 client认为关闭失败,等了200ms重传fin然后关闭成功
这个问题的总结就是:TCP连接断开的四次挥手中,由于fin包和ack包乱序,导致等了一次timeout才关闭连接,但是上层业务设置了200ms超时,导致业务报错了,现在需要重现这个问题!
出现这种问题的场景:比如在 OVS/MOC 等网络场景下,SYN/FIN 等关键包需要更新路由(session flow),会走 slowpath(送到更高层的复杂逻辑处理,比如 SYN 就创建一个新的 session 记录,以后普通包直接命中这个 session 就快了;同样 FIN 需要走高层逻辑去释放这条 session 记录)
影响:因为 ack 比 FIN 先到,导致应用连接已被释放,但是后面的 FIN 被重传,从而可能使得应用记录的连接断开时间要晚甚至超时
原作者怎么分析定位,花了几周,这个过程大家可以去看上面的原因,本篇的目的是对这个问题用Scapy 来重现,目标掌握好 Scapy 这个工具,以后对各种其他问题大家自己都能快速定位
用scapy来模拟这个问题,server端用python实现,重点注意server端的断开方式有两个shutdown/close:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import socket
server_ip = "0.0.0.0" server_port = 22345
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.shutdown(socket.SHUT_RDWR) //比较shutdown和close的不同
time.sleep(3) server_socket.close()
|
对应的client 测试代码,引入了scapy,代码首先是和服务端3次握手,然后抓取(sniff)所有服务端的来包,看看是不是fin,是的话故意先回ack再回fin人为制造乱序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| from scapy.all import * import time import sys
target_ip = "127.0.0.1" target_port = 22345
src_port=54322
ip = IP(dst=target_ip) syn = TCP(sport=src_port, dport=target_port, flags="S", seq=4294967293) syn_ack = sr1(ip / syn) if syn_ack and TCP in syn_ack and syn_ack[TCP].flags == "SA": print("Received SYN-ACK") ack = TCP(sport=src_port, dport=target_port, flags="A", seq=4322, ack=syn_ack.seq+1) send(ip / ack) data="rrrrrrrrrrrrrrrrrrrr" payload=TCP(sport=src_port, dport=22345, flags='S', seq=4294967294) send(ip/payload()/Raw(load=data)) print("Send ACK") else: print("Failed to establish TCP connection")
def handle_packet(packet): print("handle fin packet") print(Ether(raw(packet))) if TCP in packet and packet[TCP].flags & 0x011 and packet[TCP].sport == 22345: print("Received FIN packet") ack = TCP(sport=src_port, dport=target_port, flags="A", seq=packet.ack+1, ack=packet.seq+1) send(ip / ack)
time.sleep(0.1) fin = TCP(sport=src_port, dport=target_port, flags="FA", seq=packet.ack, ack=packet.seq) send(ip / fin) sys.exit(0)
sniff(filter="tcp port 22345", iface="lo",prn=handle_packet)
|
下图是服务端 shutdown时模拟挥手断开时ack包和fin包乱序了,也就是先回ack,sleep一段时间后再回fin包,如图:

如果server端代码中将shutdown改成close 并做个对比,关键是上面绿框回复ack是4322(challenge_ack 表示seq=4322的fin包被忽略了),而下面close时的seq=4322的fin包会被正确接收并回复ack 4323 确认,那么这时client 可以断开了。而上图绿框表示fin 被忽略了,那么内核要继续等200ms 再次发 fin,等收到ack后client 才能断开

server上通过 netstat 观察连接的状态变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| //shutdown,可以看到server 发fin进入FIN_WAIT1,然后收到ack 进入 FIN_WAIT2,此时收到fin了,但是被扔掉了,无法断开进入TIME_WAIT # sh test.sh tcp 0 0 192.168.0.2:22345 192.168.0.1:54322 SYN_RECV tcp 0 1 192.168.0.2:22345 192.168.0.1:54322 FIN_WAIT1 tcp 0 0 192.168.0.2:22345 192.168.0.1:54322 FIN_WAIT2 tcp 0 0 192.168.0.2:22345 192.168.0.1:54322 FIN_WAIT2 tcp 0 0 192.168.0.2:22345 192.168.0.1:54322 FIN_WAIT2 tcp 0 0 192.168.0.2:22345 192.168.0.1:54322 FIN_WAIT2 tcp 0 0 192.168.0.2:22345 192.168.0.1:54322 FIN_WAIT2
//close 可以看到server 发fin进入FIN_WAIT1,然后收到ack 进入 FIN_WAIT2,此时收到fin了没有被扔掉,所以很快连接断开进入了TIME_WAIT tcp 0 0 192.168.0.2:22345 192.168.0.1:54322 SYN_RECV tcp 0 1 192.168.0.2:22345 192.168.0.1:54322 FIN_WAIT1 tcp 0 0 192.168.0.2:22345 192.168.0.1:54322 FIN_WAIT2 tcp 0 0 192.168.0.2:22345 192.168.0.1:54322 TIME_WAIT tcp 0 0 192.168.0.2:22345 192.168.0.1:54322 TIME_WAIT
|
seq 回绕到0 会导致丢包吗?
首先学习下 seq 是一个无符号32位的整数,最大值是4294967295
如图,有人发现探活连接不通,导致了一次非正常切换,所以需要分析连接为什么断开,抓包发现重传的时候正好seq 为0,于是他就奇怪了是不是这个seq溢出搞的鬼?怎么这么巧seq 刚好为0了?

要想正好巧合在 seq 回绕的时候刚好回绕到 0 还是非常不容易的,不过好在用 scapy 来模拟这事就简单了
重现代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| from scapy.all import * import time import sys
target_ip = "server_host_ip" target_port = 22345 src_port=random.randint(1024,65535)
#接近最大值 4294967295 start_seq=4294967292
ip = IP(dst=target_ip) syn = TCP(sport=src_port, dport=target_port, flags="S", seq=start_seq) syn_ack = sr1(ip / syn) if syn_ack and TCP in syn_ack and syn_ack[TCP].flags == "SA": print("Received SYN-ACK") ack = TCP(sport=src_port, dport=target_port, flags="A", seq=syn_ack.ack, ack=syn_ack.seq+1) print(syn_ack.seq) print(syn_ack.ack) print(ack) send(ip/ack) print("Send ACK") else: print("Failed to establish TCP connection")
print("send payload") #4294967293+3(3个r) 正好是无符号整数溢出(最大 4294967295),回绕到0 data="rrr" payload=TCP(sport=src_port, dport=22345,flags="AP", seq=syn_ack.ack, ack=syn_ack.seq+1) payload2=TCP(sport=src_port, dport=22345,flags="AP", seq=0, ack=syn_ack.seq+1) syn_ack=send(ip/payload/Raw(load=data)) syn_ack=send(ip/payload2/Raw(load=data))
|
对应的抓包:

从上面的实验来看内核能正确处理这种 seq 回绕到 0 的场景,所以问题不在内核处理上。进一步分析发现是交换机的 bug 导致的
最终确认原因:交换机bug 丢掉 seq=0的包
最后发现这个问题是中兴 9900系列交换机存在seq=0 push 发送模式下报文存在丢失缺陷, 丢包原因是中兴交换机从安全角度考量是支持antidos防攻击功能,该功能开启后会将 TCP seq 为 0 的报文作为非法报文并丢弃。中兴 9900交换机上该功能默认是关闭的但是未生效,需要重新触发关闭(现场看配置是关闭的,实际是开启的,现在执行将配置先打开再关闭)。
临时规避方案:(中兴内部验证测试针对此报文有效,现场环境可能有差异,需要现场验证确认)
1 2
| 先执行 (config)#anti-dos abnormal enable 再执行 (config)#anti-dos abnormal disable
|
现场实施完毕后,发包验证恢复正常,后续持续观察业务。
彻底解决方案:将该版本升级至V2.00.00R8P16
华为交换机针对默认连接丢包 bug
借着中兴这个交换机问题,说一下华为交换机的的 bug,华为 CE12800系列,V200R022C00SPC500之前的版本
当大规格路由反复震荡场景下会小概率
- 出现优先级更高的(子网掩码范围更小)路由表项残留,导致整个子网不通
- 某个接口下发的路由表和其他接口(芯片)不一致,导致某条连接一直丢包
可以重启单板修复表项异常问题,该问题在新版本上(V200R022C00SPC500)已经补丁修复(SPH220)
根因:残留表项或表项异常导致,老版本在大规格路由反复震荡场景下会小概率触发,是软件多线程处理路由下发或删除时,出现线程读取数据异常,导致芯片表项错误。
scapy 构造全连接队列溢出
server 端用python 起一个WEB 服务:
1
| nohup python3 -m http.server 22345 &
|
然后client端用如下scapy 代码不断去3次握手建立连接,试几次后就抓到如下现象:

抓包效果:

总结
我觉得scapy还是挺好用的,比packetdrill好用一万倍,直观明了,还有命令行可以交互测试
但是要注意 scapy 是绕过内核在模拟发包,收包靠 sniff,所以内核收到这些回包会认为连接不存在,直接reset,需要在iptables 里处理一下
这个问题是别人推荐给我看的,一般10分钟就看完了,但是我差不多花了2天时间,不断地想和去实验重现
参考资料
https://wizardforcel.gitbooks.io/scapy-docs/content/3.html
https://www.osgeo.cn/scapy/usage.html
https://zhuanlan.zhihu.com/p/51002301
如果你觉得看完对你很有帮助可以通过如下方式找到我
find me on twitter: @plantegg
知识星球:https://t.zsxq.com/0cSFEUh2J
开了一个星球,在里面讲解一些案例、知识、学习方法,肯定没法让大家称为顶尖程序员(我自己都不是),只是希望用我的方法、知识、经验、案例作为你的垫脚石,帮助你快速、早日成为一个基本合格的程序员。
争取在星球内:
- 养成基本动手能力
- 拥有起码的分析推理能力–按我接触的程序员,大多都是没有逻辑的
- 知识上教会你几个关键的知识点