测试工具
本片文章会用到以下工具来学习tcp三次握手:
- tcpdump,一个运行在用户态的应用程序,它本质上是通过调用 libpcap 库的各种 api 来实现数据包的抓取功能。数据包到达网卡后,经过数据包过滤器(BPF)筛选后,拷贝至用户态的 tcpdump 程序,以供 tcpdump 工具进行后续的处理工作,输出或保存到 pcap 文件。我们用tcpdump来抓取三次握手的报文
- iptables,也是一个运行在用户态的应用程序,底层用的内核模块netfilter,是一个linux防火墙,我们用它来模拟三次握手的丢包。
- curl,用来作为测试的客户端。
- nginx,用来作为测试的服务端。
接下来我们就进入正题–
三次握手丢包后会发生什么?
接下来给大家来演示一下tcp三次握手如果出现丢包会发生什么。
我们使用的客户端ip:xxx.xxx.xxx.92
我们使用的服务端ip:xxx.xxx.xxx.95
正常的三次握手
我们首先来抓包看一下正常的三次握手。下图是一个tcp通信的全过程。
客户端
在客户端使用curl来发送一个http请求,并用tcpdump来进行抓包,过滤服务端ip。
tcpdump -i any host xxx.xxx.xxx.95 -n
抓包结果如上图所示,前三个包是我们熟知的三次握手(’.'是ACK),后面四个包是http数据传输,最后的三个包是tcp四次挥手。四次挥手为啥只有三个包,因为服务端收到客户端发送过来的fin包之后,没有数据需要传输,所以将第二次、第三次挥手合在一块了。
服务端
服务端抓包过滤客户端ip:
tcpdump -i any host xxx.xxx.xxx.92 -n
由于没有丢包,抓包结果和客户端完全一致。
第一次握手丢包
我们首先进行理论分析。如果第一次握手的syn包丢了,那首先服务端不会有任何反应,客户端由于收不到第二次握手的synack包,所以会进行重传,重传一定次数 (由net.ipv4.tcp_syn_retries内核参数控制)之后,客户端会自动销毁这个连接。
我们用iptables来模拟丢包,值得一提的是,tcpdump和iptables有一定的先后顺序。入方向的话,tcpdump运行在iptables的前面,出方向是相反的。我们可以非常简单的把我们的测试环境拓扑看成 客户端应用程序(curl)->客户端的iptables->客户端的tcpdump->服务端的tcpdump->服务端的iptables->服务的的应用程序(nginx),所以如果在入方向用iptables丢包,tcpdump也可以抓到。如果你在实践过程中发现用iptables丢的包,tcpdump依然可以抓到,那就可以想一下这个拓扑。
用iptables来丢弃92发送过来的syn包(两个命令都是一样的效果,区别是用tcpdump抓包看到的结果不同,原因上面说了):
client:
# iptables -A OUTPUT -p tcp -d xxx.xxx.xxx.95 --tcp-flags ALL SYN -j DROP
server:
# iptables -A INPUT -p tcp -s xxx.xxx.xxx.92 --tcp-flags ALL SYN -j DROP
客户端
可以看到客户端重传三次之后释放了连接,第一次重传时间是1s,之后每次重传时间是之前的2倍。
服务端
服务端抓不到包,和我们的预期相符。
第二次握手丢包
第二次握手丢包也比较简单,客户端由于收不到第二次握手的synack包,会进行重传,而服务端收不到第三次握手的ack包,也会进行重传(由net.ipv4.tcp_synack_retries内核参数控制)。
我们同样用iptalbes来模拟丢包。
client:
# iptables -A INPUT -p tcp -s xxx.xxx.xxx.95 --tcp-flags ALL SYN,ACK -j DROP
server:
# iptables -A OUTPUT -p tcp -d xxx.xxx.xxx.92 --tcp-flags ALL SYN,ACK -j DROP
客户端
可以看到客户端现象和之前第一次握手丢包一样,重传失败三次之后释放了连接。
服务端
服务端由于在重传第二次握手的过程中,又收到了客户端重传过来的syn包,所以看起来比较乱一点。我们简单数一下,服务端一共收到了4个syn包(同序列号),发出去了6个synack包(同序列号),相当于重传了两次,与内核参数net.ipv4.tcp_synack_retries = 2一致。
测试中还遇到一个场景,服务端在重传的过程中有概率换一个序列号,如果换了一个序列号,那这个序列号的第二次握手也要重传两次。
第三次握手丢包
第三次握手情况比前两种复杂一些,我们还是先从理论上分析一下。假如第三次握手客户端发送的ack包丢了,客户端是不知道的,因为只要第三次握手发出之后,客户端就认为连接已经建立了,状态变成ESTABLISHED;而服务端由于收不到第三次握手的ack包,状态依然为SYN_REVD,理论上服务端会进行重传,重传一定次数之后会释放连接。而客户端如果发送数据,服务端收到请求之后,由于不处于ESTABLISHED状态,可能认为这不是一个正常的连接,然后回个rst;如果客户端不发送数据,那可能就会hang住,只能等待keepalive超时。事实真的是这样吗,我们来实践一下。
丢包后客户端发送数据
我们依然用iptables来模拟丢包
client:
# iptables -A OUTPUT -p tcp -d xxx.xxx.xxx.95 --tcp-flags ALL ACK -j DROP
server:
# iptables -A INPUT -p tcp -s xxx.xxx.xxx.xxx.92 --tcp-flags ALL ACK -j DROP
客户端
服务端
root@xxx-xxx-xxx-92:~# curl xxx.xxx.xxx.95:8004
{“host”: “xxx.xxx.xxx.95”, “port”: “80”}
我们先看结论,可以看到这个请求竟然通了,但是在服务端抓包看到只抓到了前两次握手,第三握手的ack包确实丢了。上网查了下资料发现原因就是服务端收到的第三个包:psh+ack包。就像上面我们分析的,客户端发送完第三次握手之后,就认为连接已经建立,随后就开始发送数据了,这个间隔是很小的(可以看到只有0.00001s);而服务端的第二次握手重传需要等待1s,所以还没来得及重传,服务端就已经收到了客户端发送过来的第一个数据包。又因为客户端发送过来的第一个数据包里面带着ack的flag,所以服务端会正常进入ESTABLISHED状态。
丢包后客户端不发送数据
如果客户端不发送数据会出现什么现象呢,我们用iptables来模拟丢弃第三次握手和客户端发送的第一个数据包。
client:
# iptables -A OUTPUT -p tcp -d xxxx.95 --tcp-flags ALL ACK -j DROP
# iptables -A OUTPUT -p tcp -d xxxx.95 --tcp-flags ALL PSH,ACK -j DROP
server:
# iptables -A INPUT -p tcp -s xxxx.92 --tcp-flags ALL ACK -j DROP
# iptables -A INPUT -p tcp -s xxxx.92 --tcp-flags ALL PSH,ACK -j DROP
客户端
root@xxx92:~# netstat -anp | grep .95
tcp 0 81 xxx.xxx.xxx.92:27390 xxx.xxx.xxx.95:8004 ESTABLISHED 1283113/curl
客户端抓包可以看到由于客户端认为连接已经建立成功,会一直重传三次握手后的第一个psh+ack数据包,curl客户端会hang住,而这个连接一直处在ESTABLISHED状态。等待重传一定次数之后,连接会被释放。
如果客户端三次握手后一直不发送数据,显然,这个连接就成为了一个死连接,会一直处于ESTABLISHED状态。只能等待http keepalive超时或者tcp keepalive超时,前者由应用程序如nginx的keepalive_timeout控制,后者可以通过修改这三个内核参数来进行设置:
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
net.ipv4.tcp_keepalive_time = 7200
可见,如果应用程序没有配置keepalive超时的话,需要等待2个小时内核才会释放连接。
服务端
服务端的现象和第二次握手丢包一样,由于收不到第三次握手,会进行重传。
值得一提的是synflood其实利用的就是这个原理,发送大量的syn包,然后故意不发送第三次握手的ack包,导致服务端产生大量半连接,从而占用服务端大量资源。