介绍如何利用tcpkill和hping3关闭一个TCP连接,以及基本原理。
1. 背景1、梳理Redis中的epoll流程时(梳理Redis和Nginx中的epoll机制),看到accept回调中客户端关于keepalive相关的初始配置,想起来之前关于连接建立的实验和梳理:网络实验 – TIME_WAIT状态的连接收到SYN是什么表现,回顾了一遍。
实验中发生Seq回绕时,会向已经Established的服务端发送一个非法的Seq,而服务端会发送正确的ACK,这个ACK叫 Challenge ACK。客户端发现ACK并不是自己期望的,于是回复RST报文,服务端收到后就会关闭连接。
具体过程见参考链接:4.9 已建立连接的TCP,收到SYN会发生什么?。
2、另外上周有人问有什么工具可以关闭一个TCP连接,查了下试试tcpkill(位于dsniff包),但环境网络限制和操作系统限制下依赖没解决,GitHub找了个单独实现:tcpkill,编译简单用了一下就推过去了。
本篇介绍下关闭连接的基本原理 和 tcpkill、killcx、hping3工具。
2. Challenge AckChallenge ACK流程示例,客户端宕机场景:
出处
此时服务端是Established状态,客户端上线进行三次握手,发送第一次SYN请求,而服务端还是会发送之前正确的ACK(携带了正确序列号和确认号),这个ACK叫 Challenge ACK。客户端发现ACK并不是自己期望的,于是回复RST报文,服务端收到后就会关闭连接。
3. 几种异常时的TCP状态表现上述要关闭一个TCP连接,直接粗暴的方式:
1、在客户端杀掉进程的话,就会发送 FIN 报文,这个客户端进程与服务端建立的所有 TCP 连接都会被关闭
2、在服务端杀掉进程,此时所有的 TCP 连接都会被关闭
下面说明下几种异常情况下TCP连接的状态。
3.1. 拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?拔掉网线后,需要分场景来讨论:
1、拔掉网线后,若有数据传输:
1)若服务端重传报文的过程中,客户端刚好把网线插回去了
拔掉网线并不会改变客户端的TCP连接状态,连接还是处于 ESTABLISHED 状态,所以这时客户端是可以正常接收服务端发来的数据报文,然后客户端回复ACK响应报文。2)若重传报文的过程中,客户端一直没有将网线插回去
服务端向客户端发送数据得不到响应,触发超时重传机制。达到一定阈值后,内核就会判定出该TCP有问题,并通过socket接口告诉应用程序net.ipv4.tcp_retries1:默认值一般为3,TCP重传次数达到tcp_retries1时,内核会认为网络出现了比较严重的问题,可能会尝试重置TCP连接或者进行其他错误处理,但通常不会直接关闭连接net.ipv4.tcp_retries2:默认值一般为15,TCP重传次数达到tcp_retries2时,内核会认为连接已经失效,从而关闭该TCP连接。具体的重传时间间隔会根据 网络状况 和 TCP拥塞控制算法 动态调整,内核会根据tcp_retries2设置的值,计算出一个timeout,超过则断开连接体感:如果 tcp_retries2 =15,那么计算得到的 timeout = 924600 ms,大约为 924.6 秒,约 15 分钟本地CentOS8环境默认参数:
1
2
3
[CentOS-root@xdlinux ➜ ~ ]$ sysctl -a|grep tcp_retries
net.ipv4.tcp_retries1 = 3
net.ipv4.tcp_retries2 = 15
2、拔掉网线后,若没有数据传输:
1)如果没有开启 TCP keepalive 机制,在客户端拔掉网线后,并且双方都没有进行数据传输,那么客户端和服务端的 TCP 连接将会一直保持存在。
2)如果开启了 TCP keepalive 机制
若服务端正常,TCP保活时间重置,等待下一次保活探测若服务端宕机,那么在客户端拔掉网线后,客户端和服务端的TCP连接将会在探测超时后关闭(2 小时 11 分 15 秒)。TCP keepalive机制(TCP保活机制),开启需要手动设置SO_KEEPALIVE,比如Redis里:
1
2
3
4
5
6
7
8
9
10
11
// redis-5.0.3/src/anet.c
int anetKeepAlive(char *err, int fd, int interval)
{
int val = 1;
if (setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &val, sizeof(val)) == -1)
{
anetSetError(err, "setsockopt SO_KEEPALIVE: %s", strerror(errno));
return ANET_ERR;
}
...
}
tcp keepalive相关参数:
1
2
3
4
5
6
7
[CentOS-root@xdlinux ➜ ~ ]$ sysctl -a|grep keepalive
# 每次检测间隔 75 秒
net.ipv4.tcp_keepalive_intvl = 75
# 检测 9 次无响应,认为对方不可达
net.ipv4.tcp_keepalive_probes = 9
# 保活时间是 7200 秒(2小时),即 2 小时内如果没有任何连接相关的活动,则会启动保活机制
net.ipv4.tcp_keepalive_time = 7200
即TCP内核态保活机制 7200+75*9 = 2小时11分15秒 才中断连接。此外上层应用可以另行实现探测机制进行保活判断。
具体分析请参考:4.13 拔掉网线后, 原本的 TCP 连接还存在吗?
3.2. TCP 连接,一端断电和进程崩溃有什么区别?1、一端断电宕机:
跟拔掉网线场景是一样的,无法被对端感知。对于有无数据传输、是否开启TCP keepalive保活机制,表现和上述拔网线一样。所以如果在没有数据传输,并且没有开启 TCP keepalive 机制时,对端的 TCP 连接将会一直处于 ESTABLISHED 连接状态。2、进程崩溃:
进程崩溃时内核可以感知到,即使没有数据传输、没开启TCP keepalive机制内核会向对端发送FIN报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,会正常进行四次挥手。对于进程崩溃后立即重启,收到对端之前TCP连接的报文时,进程都会回复RST报文以断开连接具体分析请参考:4.12 TCP 连接,一端断电和进程崩溃有什么区别?
4. 如何关闭一个TCP连接(实验)利用TCP交互过程中的RST机制开发工具,都可以实现连接关闭,比如tcpkill/killcx/ngrep/scapy/hping3,这里介绍参考链接中的tcpkill和killcx。
tcpkill 和 killcx 两个工具都是通过 伪造RST报文 来关闭指定的 TCP连接,但是它们拿到正确序列号的实现方式是不同的。
tcpkill 是在双方进行 TCP 通信时,拿到对方下一次期望收到的序列号,然后将序列号填充到伪造的 RST 报文,并将其发送给对方,达到关闭 TCP 连接的效果。tcpkill 工具属于被动获取,双方通信时才能获取正确序列号,这种方式无法关闭非活跃的 TCP 连接killcx 是主动发送一个 SYN 报文,对方收到后会回复一个携带了正确序列号和确认号的 ACK 报文,这个 ACK 即 Challenge ACK,这时就可以拿到对方下一次期望收到的序列号,然后将序列号填充到伪造的 RST 报文,并将其发送给对方,达到关闭 TCP 连接的效果。killcx 工具属于主动获取,无论 TCP 连接是否活跃,都可以关闭利用 SEQ/ACK 号伪造两个RST报文分别发给客户端和服务端,从而关闭双方的连接killcx通过Challenge ACK关闭连接 流程示意图:
出处
4.1. tcpkill实验tcpkill来自dsniff工具集。原理跟tcpdump差不多,都会通过libpcap库抓取符合条件的包,选项参数也差不多。
官网(这是个人项目):dsniffgithub上也有其他人的fork:dsniffCentOS安装:
1
2
yum install epel-release
yum install dsniff
实验步骤:
1)python -m http.server启动服务端(貌似nc -l更合适),默认80002)nc 127.0.0.1 8000连接服务端,此时是终端阻塞的3)并且tcpdump -i any port 8000 -nn开启监听4)tcpkill -i any port 8000指定关闭连接,由于没有数据传输5)nc客户端手动输入 “abc” 并回车6)查看tcpdump和tcpkill能看到RST报文tcpkill演示–有数据传输前:
tcpkill演示–有数据传输后:
由于此处tcpkill未特意限制 源端 和 目的端,都进行了RST
4.2. killcx实验(失败)工具网站:Killcx
CentOS下安装(参考 安装 killcx):
1
2
3
4
5
6
7
在上述网站下载工具,killcx实际是一个perl脚本
yum install cpan
cpan install Net::RawIP
yum install perl-Net-Pcap
yum install epel-release
yum install libpcap
cpan install NetPacket::Ethernet
实验:nc -l 8000监听,nc 127.0.0.1 8000连接,netstat查看连接,使用killcx关闭连接
结果:失败,没有成功关闭连接,抓包只发了SYN没有应答(尝试python -m http.server也失败)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[CentOS-root@xdlinux ➜ ~ ]$ netstat -anp|grep -w nc
tcp 0 0 192.168.1.150:8000 192.168.1.150:56430 ESTABLISHED 128082/nc
tcp 0 0 192.168.1.150:56430 192.168.1.150:8000 ESTABLISHED 128092/nc
[CentOS-root@xdlinux ➜ killcx-1.0.3 ]$ ./killcx 192.168.1.150:56430
killcx v1.0.3 - (c)2009-2011 Jerome Bruandet - http://killcx.sourceforge.net/
[PARENT] checking connection with [192.168.1.150:56430]
[PARENT] found connection with [192.168.1.150:8000] (ESTABLISHED)
[PARENT] forking child
[CHILD] interface not defined, will use [enp4s0]
[CHILD] setting up filter to sniff ACK on [enp4s0] for 5 seconds
[PARENT] sending spoofed SYN to [192.168.1.150:8000] with bogus SeqNum
[PARENT] no response from child, operation may have failed
[PARENT] => you may try using 'lo' as interface parameter
[PARENT] killing child [128840] and exiting program
4.3. hping3实验(成功)上述killcx工具使用失败了,不去深究原因了。工具需要安装很多依赖,在平时环境使用也很麻烦,弃用。
其实只要基于Challenge ACK机制就能实现RST连接的效果。调整为使用hping3发送TCP报文,该工具用于生成和发送自定义的网络数据包。
CentOS安装:yum install hping3
实验步骤:
1)nc -l 192.168.1.150 80002)nc 192.168.1.150 8000连接服务端,此时客户端发送信息都会在服务端接收并显示3)并且tcpdump -i any port 8000 -nn开启监听4)netstat -anp|grep 8000查看连接5)通过hping3向关闭非监听端口发起的那条连接,即最终向临时端口发送RST先发送SYN报文:hping3 192.168.1.150 -a 192.168.1.150 -s 8000 -p 56458 --syn -V -c 1选项解释: hping3 目的ip -a 源ip -s 源端口号 -p 目的端口号 发syn报文再根据抓包获取应答的ACK(此处为13790691),组装RST报文并发送hping3 192.168.1.150 -a 192.168.1.150 -s 8000 -p 56458 --rst --win 0 --setseq 13790691 -c 16)查看nc和netstat结果hping3操作之前:
hping3操作之后:
可看到客户端发起的那条TCP连接已经关闭了。
5. 小结梳理异常情况下的TCP连接状态;并介绍和使用tcpkill、killcx和hping3,如何关闭一个TCP连接,以及其中的基本原理。
TODO:后续阅读工具的代码,考虑用Cpp/Go/Rust实现小工具。
6. 参考网络实验 – TIME_WAIT状态的连接收到SYN是什么表现4.9 已建立连接的TCP,收到SYN会发生什么?4.13 拔掉网线后, 原本的 TCP 连接还存在吗?4.12 TCP 连接,一端断电和进程崩溃有什么区别?