一次网络连接残留的分析
本来放在知识星球,作删减和调整后发博客吧
问题描述
LVS TCP 探活一般是 3 次握手(验证服务节点还在)后立即发送一个 RST packet 来断开连接(效率高,不需要走四次挥手),但是在我们的LVS 后面的 RS 上发现有大量的探活连接残留,需要分析为什么?
一通分析下来发现是 RST 包 和第三次握手的 ack 到达对端乱序了,导致 RST 被drop 掉了。但是还需要进一步分析 drop 的时候和 RST 包里面带的 timestamp 有没有关系?
可以用 Scapy 来实验验证如下 4 个场景:
- 正常三次握手,然后发送 RST 看看是否被 drop —— 期望 RST 不被 drop,连接正常释放,作为对比项
- 正常 2 次握手,然后立即发送 RST(正常带 timestamp),再发送 ack(制造乱序),看看 RST 会不会被 drop,如果 RST drop 后连接还能正常握手成功并残留吗?
- 正常 2 次握手,然后立即发送 RST(不带 timestamp),再发送 ack(制造乱序),看看 RST 会不会被 drop
- 正常 2 次握手,然后立即发送 RST(带 timestamp,但是 timestamp 为 0),再发送 ack(制造乱序),看看 RST 会不会被 drop
重现场景构造如下:通过客户端+服务端来尝试重现,客户端用 scapy 来构造任意网络包,服务端通过 python 起一个 WEB 服务
客户端
因为最新的 scapy 需要 python3.7 ,可以搞一个内核版本较高的 Linux 来测试(星球统一 99 块的实验 ECS 就符合要求),安装命令大概是这样:yum install python3-scapy
用 scapy 脚本构造如上 3 个场景的网络包,代码和使用帮助我放到这里了:https://github.com/plantegg/programmer_case/commit/e71ade38050c48170c7d6fb5922f78188a96435b#diff-3d18b8aa76586e6c59227e020ba22ef1ef8c5416764d0a923b198ad824996eda
如果需要构造带 timestamp 的RST 用如下代码段,乱序通过调整 ack和 RST 的顺序来实现
1 | # 构造 ACK 包 |
在scapy 机器上drop 掉OS 自动发送的 RST(因为连接是 scapy 伪造的,OS 收到 syn+ack 后会 OS系统会发 RST(这个 RST不带 timestamp))
1 | iptables -A OUTPUT -p tcp --dport 8000 --tcp-flags RST RST ! --tcp-option 8 -j DROP |
scapy 构造的包流程,可以看到不走内核 tcp 协议栈,也不走 nf_hook(防火墙),不受上面的 iptables 规则限制,所以能发送到服务端:
1 | ***************** c7d8ea00 *************** |
Server 端
先记住一个知识点,后面看内核调用堆栈会用得上确认是否被丢包
一个网络包正常处理流程最后调 consume_skb 来释放,如果网络包需要 Drop 就调
kfree_skb
来丢包
server端 安装 netstrace来监控包是否被drop,并通过 python 拉起一个端口:
1 | python -m http.server 8000 |
tcpdump 确认 8000 端口收到的包
在 8000端口机器上执行抓包验证收到的包顺序和所携带的 timestamp,包含 3 个场景的包:
1 | #tcpdump -i eth0 port 8000 -nn |
场景 1:正常三次握手后再 RST,作为对比
netstrace 命令和结果
1 | #netstat -P 8000 |
场景 2:正常 2 次握手,然后立即发送 RST(带 timestamp)
1 | #netstat -P 8000 |
场景 3:正常 2 次握手,然后立即发送 RST(不带 timestamp)
可以看到 RST 被 drop 然后 握手失败
1 | ***************** c22c8900,c22c8f00 *************** |
上面三个场景都没能重现问题,所以继续构造场景 4
场景 4 timestamp 不递增
保证 tcp options 里面有 timestamp,且不递增,这时终于重现了连接残留:
1 | //这表示有 tcp 连接残留在 8000 端口上,而实际上期望连接要因为有 RST 而被释放 |
不会导致连接残留的情况:
1 | //连接不残留, ts 递增 |
结论
最终重现的必要条件:内核在三次握手阶段(TCP_NEW_SYN_RECV),收到的RST 包里有 timestamp 且不递增 就会丢弃 RST
注意:
- 如果 RST 的 seq 不递增也会导致连接残留,这属于 seq 回绕了 // /proc/net/netstat 中没找到 有哪个指标对应的监控
- 要区分 timestamp 没有和 timestamp 为 0 的情况,为 0 表示有,大概率回绕了//场景 1-3 都忽略了这个问题
- options=[(‘NOP’, None), (‘NOP’, None)]) 表示没有 timestamp,也不能重现问题
- 以上案例 2/3/4 场景下 nettrace 看到的 RST 都被 drop 了,但是不妨碍连接的释放 //这个还需要分析为什么连接 RST 起作用了但是还是会 drop RST 包
- 如果出现连接残留,也会导致全连接队列增大直到溢出
- 三次握手成功后的通信阶段(established),此时只校验 RST 的 seq 有没有回绕,不校验 timestamp,这样连接能正确释放
1 | //三次握手成功,如果 RST 带的 timestamp 不递增也会正确触发释放连接,也就是 ESTABLISHED 时只校验 RST 的 seq 有没有回绕,不校验 timestamp |
对应的内核 commit
Server 在握手的第三阶段(TCP_NEW_SYN_RECV),等待对端进行握手的第三步回 ACK时候,如果收到RST 内核会对报文进行PAWS校验,如果 RST 带的 timestamp(TVal) 不递增就会因为通不过 PAWS 校验而被扔掉
问题引入:https://github.com/torvalds/linux/commit/7faee5c0d514162853a343d93e4a0b6bb8bfec21 这个 commit 去掉了TCP_SKB_CB(skb)->when = tcp_time_stamp,导致 3.18 的内核版本linger close主动发送的 RST 中 ts_val为0
问题修复:修复的commit在 675ee231d960af2af3606b4480324e26797eb010,直到 4.10 才合并进内核
监控
对应这种握手阶段连接建立如何监控呢?
从内核代码 net/ipv4/tcp_minisocks.c/tcp_check_req 函数会对报文调用 tcp_paws_reject 函数进行 paws_reject 检测,tcp_paws_reject 如果返回值为true,则 tcp_check_req 返回NULL,并且记录 LINUX_MIB_PAWSESTABREJECTED 计数
可以观察 /proc/net/netstat 中的监控指标:PAWSEstab
1 | //内核中的指标 |
虽然三次握手没有完成,但是在服务端连接已经是 ESTABLISHED,所以这里的统计指标还是 PAWSEstab,可以通过 netstat -s 来查看:
1 | #netstat -s |grep -E -i "timestamp|paws" |
这个指标对应在 netstat 源码(net-tools) 中的解释:
1 | {"PAWSEstab", N_("%llu packets rejected in established connections because of timestamp"), opt_number}, |
总结
星球里之前也写过 scapy 的入门以及使用案例: scapy 重现网络问题真香
就像学英语的时候要精读,分析 case 也需要深挖,可以挖上一到两周,不要每天假学习(似乎啥都看了,当时啥都懂,过几个月啥都不懂)
掌握技能比掌握知识点和问题的原因更重要
nettrace 也真的很好用/很好玩,可以帮你学到很多内核知识
参考资料
https://cloud.tencent.com/developer/article/2210423