[转]如何干掉一条tcp 连接(活跃/非活跃)

原文来自:https://developer.aliyun.com/article/59308,之前遇到了同样的问题,在实现RTMP服务器时由于没有设置SO_RCVTIMEO,导致在RTMP握手消息交换阶段无限等待握手数据包,出现了非活跃状态的tcp连接占用情况,试过原版tcpkill,由于非活跃状态,无法强行断开连接,所幸找到了这个修改版,试了下成功踢掉了捣乱连接,特此分享,感谢原作者!

背景

最近在测试环境部署服务的时候老是会有端口被占用情况用netstat/ss 查看后发现端口一直被占用
同另外一个ip 建立了tcp 连接,类似于这样:

ESTAB      0      0      192.168.103.169:12345              192.168.103.12:10261 

当然这个问题也不是最近才遇到,之前也遇到过,不过之前都是很快这个连接就自动消失,我就可以欢快
的使用我自己喜欢的12345 端口,无奈这次一直连续好几天这个连接一直存在导致我一直无法使用这个端口。

解决过程

1. google 找答案

google 果然告诉我答案,有个叫tcpkill 的工具(可以自行搜索一下),看到之后立即将源码下下来
编译了一把,按照提示将几个确实的包安装后编译成功,然后按照tcpkill 的帮助文档进行操作

tcpkill -i eth0 src port 12345 and dst port 10261 and src host 192.168.103.169 dst host 192.168.103.12
# 按照libpcap 规则描述出一个tcp 连接

执行上述命令后发现tcpkill 感觉像是处于hang 住状态,运行了半天也没见把这个连接干掉,为什么这个连接没干掉? 于是用 nc 测试了下,发现如下情况

对于nc 已经建立的连接运行tcpkill 后无法立即干掉,只有在这个连接上有数据传输的时候才会把发送rst 包将连接reset 掉。

对于上述现象好奇的看了下tcpkill 的源码

pd = pcap_init(intf, filter, 64)
pcap_loop(pd, -1, tcp_kill_cb, (u_char *)l);

不难看出是每抓到一个符合条件的包回调一次tcp_kill_cb 函数,也就是说tcpkill 只有在这条
连接上还有数据传输才有可能被tcpkill 感知并干掉。
至此为什么我想干掉的那条tcp 连接没被干掉原因已经比较明朗了。不外乎以下两个原因:

1. 连接还活着,但是连接上没有数据传输
2. 这是一个半连接,由于网络原因或者对端服务器崩溃等原因导致,由于tcp keeplive
没打开而又没有数据向对端发送,导致一直无法感知次连接其实已经断开。
注:查看tcp keepalive 参数方法
# cat /proc/sys/net/ipv4/tcp_keepalive_time
  7200
  # cat /proc/sys/net/ipv4/tcp_keepalive_intvl
  75
  # cat /proc/sys/net/ipv4/tcp_keepalive_probes
  9 

按照以上分析tcpkill 是可以干掉一调活跃连接,那么对于一条非活跃连接或者半连接要如何干掉?

2. 如何干掉一条非活跃tcp 连接

2.1 直接RESET 掉

看了tcpkill 的代码,大致原理是捕捉到tcp 报文之后就知道了这条连接信息,然后
利用libnet 构造一条RST报文发回去直接RST 掉连接。于是在tcpkill 代码之上根据netstat/ss
看到的tcp 四元组信息构造了一条TST发送出去。

...
120     libnet_build_tcp(dport, sport,
121             seq, 0, TH_SYN, 0, 0, 0, LIBNET_TCP_H,
122             NULL, 0, l, 0);
123     libnet_build_ipv4(LIBNET_IPV4_H + LIBNET_TCP_H, 0,
124             libnet_get_prand(LIBNET_PRu16), 0, 64,
125             IPPROTO_TCP, 0, ip_dst.s_addr,
126             ip_src.s_addr, NULL, 0, l, 0);
127 
128     if (libnet_write(l) < 0) {
...            //error handler.
...        }
...

运行之后用tcpdump 抓包可以看到构造的RST 报文被顺利的发送出去,但是连接并没有
消失,也就是说TCP 可能根本就没响应此报文。对比tcpkill reset 部分代码后发现
还需要设置正确的系列号,突然明白为啥tcpkill 只能干掉活跃连接了,原来是需要
捕获流动的报文才能获取正确的系列号,利用此序列号构造出RST包才能reset掉TCP 连接。
那么如何才能获取一条非活跃连接的序列号呢?

2.2 获取一条非活跃连接(或者半连接)序列号

主动发送一条SYN包,获取应答后计算出正确的序列号。为啥要发送一个SYN 包呢,因为SYN包是一调
TCP 连接的开始,此时序列号还未产生,因此可以随机选取一个,当TCP 的一端收到这条SYN 包后
会以ACK报文进行应答,应答报文中会携带正确的序列号,因此我们只需要根据此序列号发送RST 包
就可以将此连接RESET 掉。
我们可以结合nc抓包看看:

前三个报文是正常建立TCP 连接的报文,至此nc 已经和服务器建立了一条连接,此时连接
上是没有任何数据的。
第四条报文是我们构造出来并主动发送的SYN报文
第五条报文是针对第四条报文给出的应答
第六条报文是tcpkill 获取第五条应答报文后计算出正确的序列号发送的RST报文。
至此问题解决。

注:按照上述原理修改了tcpkill 源码,使其能够主动kill 掉非活跃连接及半连接

4. 总结

4.1 干掉活跃连接

直接利用tcpkill干掉活跃连接即可

4.2 干掉非活跃连接

构造SYN 包发送给连接的一端,收到应答后根据应答中的序列号构造RST报文干掉连接

4.3 干掉半连接

干掉半连接的方法同干掉非活跃连接的方式一样,只是SYN 包一定要向半连接还存在的一端
发送。
4.4 注意问题

tcpkill 一定要运行在能接收到应答包的主机上在,最好运行在连接或半连接存在的一端主机上。

4.5 关于tcpkill 代码改动

1. 增加函数build_syn 根据相关参数构造syn 报文并发送到网络中,后台线程运行此函数不断发送syn 包
2. 运行pcap_loop捕获应答报文,并发送RST报文,reset 连接
从本质上说其实就是让非活跃连接或者半连接上有报文流动,被tcpkill 捕获到之后发RST报文
干掉连接。

4.6 改动过后新tcpkill 干掉一条连接的使用方法

sudo tcpkill -s 192.168.103.12:10261 -d 192.168.103.169:12345
4.7 代码

http://site-img-data.oss-cn-shanghai.aliyuncs.com/tcpkill/tcpkill_v2.tar.gz

博主友情提示:

如您在评论中需要提及如QQ号、电子邮件地址或其他隐私敏感信息,欢迎使用>>博主专用加密工具v3<<处理后发布,原文只有博主可以看到。