使用 tcpdump 抓包分析 TCP 三次握手、四次挥手与 TCP 状态转移
2019-01-24 18:22 云物互联 阅读(1806) 评论(0) 编辑 收藏 举报目录
前文列表
TCP 协议
TCP(Transmission Control Protocol,传输控制协议),是一种面向连接的可靠传输协议,提供可靠(无差错、不丢失、不重复、按顺序)的字节流数据传输服务。在传输效率和可靠性之间选择了后者,所以也具有开销大、传输速度慢的缺点。
TCP 的可靠性传输具有非常复杂的实现细节,包括但不限于:
ACK 确定机制:当接收方接收到数据段后,会返回 ACK 确认。
定时重发:方发送方发送数据段后,会启动定时器,超时未接收到 ACK 确认,会重发该数据段。
数据校验:接收方会对数据段进行数据校验,如果发现数据段有差错,会将该数据段丢弃,等待超时重传。
顺序传输:TCP 字节流会为每个字节排序,确保数据传输顺序的正确性。
滑动窗口:TCP 数据段长度可根据收发双方的缓存、网络等状态而调整。接收方只允许发送方发送接收缓冲区所能接纳的数据,防止缓冲区溢出。
TCP 数据段首部:
- Sequence Number 序列号:字节流中的每个字节都要按序编号,该字段值为本数据段数据部分的第一个字节的序号
- Acknowledgment Number 确认号:确认序列号
- Offset 偏移量:数据段首部的长度,字段值为首部长度除以 4
- Reserved 预留:保留位,供今后使用
- TCP Flags 标签:标识数据段性质。
- Window 窗口:标识发送者接收窗口的大小
- CheckSum 校验值:用于检查数据段在传输过程中是否出现差错
- Urgent Pointer 紧急指针:当字段值为 1 时生效,标识本数据段具有紧急数据
其中的 TCP Flags 字段,是非常重要的功能标识,占 8 位,分别为:
- C(CWR)、E(ECE):用于支持 ECN(显示阻塞通告)。
- U(URGENT):当值为 1 时,标识此数据段有紧急数据(比如紧急关闭),应优先传送,要与紧急指针字段配合使用。
- A(ACK):仅当字段值为 1 时才有效,建立 TCP 连接后,所有数据段都必须把 ACK 字段值置为 1。
- P(PUSH):若 TCP 连接的一端希望另一端立即响应,PSH 字段便可以 “催促” 对方,不再等到缓存区填满才发送返回。
- R(RESET):若 TCP 连接出现严重差错,该字段的值置为 1,表示先断开 TCP 连接,再重连。
- S(SYN,Synchronize Sequence Numbers):用于建立和释放连接,当字段值为 1 时,表示建立连接。
- F(FIN):用于释放连接,当字段值为 1 时,表明发送方已经发送完毕,要求释放 TCP 连接。
NOTE:TCP Flags 在整个 TCP 建立/释放连接的过程中都起到了非常重要的标志作用。
图示三次握手与四次挥手
抓包结果
Client IP:172.18.128.204
Server TCP Socket:(10.0.0.128, 80)
抓包分析
TCP 三次握手
- Step1.
172.18.128.204.62534 > 10.0.0.128.80: Flags [S], cksum 0x8523 (correct), seq 3401804541, win 65535, options [mss 1460,nop,wscale 5,nop,nop,TS val 960632545 ecr 0,sackOK,eol], length 0
客户端执行系统调用 connect()
发出 SYN 请求建立 TCP 连接,此时客户端的 TCP 端口状态为 SYN_SENT(表示请求连接)。
- Step 2.
10.0.0.128.80 > 172.18.128.204.62534: Flags [S.], cksum 0x378d (incorrect -> 0xf136), seq 2666924726, ack 3401804542, win 27960, options [mss 1410,sackOK,TS val 19959035 ecr 960632545,nop,wscale 7], length 0
服务端执行系统调用 listen()
监听到 SYN 请求,TCP 端口状态从 LISTEN 转为 SYN_RCVD 并第一次响应 ACK。
- Step 3.
172.18.128.204.62534 > 10.0.0.128.80: Flags [.], cksum 0x7cfb (correct), seq 3401804542, ack 2666924727, win 4106, options [nop,nop,TS val 960632549 ecr 19959035], length 0
客户端接收到 ACK 响应之后,TCP 端口状态变成 ESTABLISHED(已建立连接,表示通信双方正在通信),再给服务端发送 ACK。服务端接收到 ACK 之后,服务端的 TCP 端口状态为 ESTABLISHED。
NOTE:Flags are some combination of S (SYN), F (FIN), P (PUSH), R (RST), W (ECN CWR) or E (ECN-Echo), or a single ‘.’ (no flags)
- options 表示选项
- mss 表示是发送端通告的最大报文长度
- sackOK 表示发送端支持 SACK 选项,SACK 选项是为了更好的确定数据的准确接收的
- TS val 发送端的时间戳 ecr 接收端的时间戳
- wscale 表示窗口因子大小
为什么需要三次握手来保证数据传输的可靠性?
“握手” 的行为实际上为了告知收发双方自己的 ISN(Initial Sequence Number,初始化序号),如上文 Client:seq=996980318
或 Server:seq=2180032179
,这个 ISN 会被作为建立连接后进行「顺序」数据传输的依据。我们可以从数据段首部的 Sequence Number 和 Acknowledgment Number 都占 32 位得知,seq 和 ack 的取值范围均是 [0, 2^32-1],所以 ISN 实际上会被顺序循环使用。而且 seq 并非每次都是从 0 开始的,TCP 协议会以 4μs 一次的频率进行 ISN+=1 操作,以此来避免 TCP 重连时出现在同一条连接中存在两个及以上 seq number 相同的数据包,而最终导致顺序错乱。所以,三次握手实际上就是初始化通信双方的 seq ISN,保证数据包的有序传输。
数据传输
双方建立通信之后,Client 向 Server 正式发出 HTTP 请求:
IP (tos 0x0, ttl 60, id 0, offset 0, flags [DF], proto TCP (6), length 129)
172.18.128.204.62534 > 10.0.0.128.80: Flags [P.], cksum 0xe98f (correct), seq 3401804542:3401804619, ack 2666924727, win 4106, options [nop,nop,TS val 960632549 ecr 19959035], length 77: HTTP, length: 77
GET / HTTP/1.1
Host: 172.18.22.208
User-Agent: curl/7.54.0
Accept: */*
IP (tos 0x0, ttl 64, id 34743, offset 0, flags [DF], proto TCP (6), length 52)
10.0.0.128.80 > 172.18.128.204.62534: Flags [.], cksum 0x3785 (incorrect -> 0x8bd8), seq 2666924727, ack 3401804619, win 219, options [nop,nop,TS val 19959040 ecr 960632549], length 0
- Client => Server:seq = x+1, ack = y+1 继承了第三次连接的 seq 和 ack number
- 请求长度 length: 77,
seq 3401804542:3401804619
==seq 3401804542:[3401804542+length]
Server 处理请求并响应:
IP (tos 0x0, ttl 64, id 34744, offset 0, flags [DF], proto TCP (6), length 295)
10.0.0.128.80 > 172.18.128.204.62534: Flags [P.], cksum 0x3878 (incorrect -> 0x528d), seq 2666924727:2666924970, ack 3401804619, win 219, options [nop,nop,TS val 19959044 ecr 960632549], length 243: HTTP, length: 243
HTTP/1.1 200 OK
Date: Thu, 24 Jan 2019 10:08:31 GMT
Server: Apache/2.4.6 (CentOS)
Last-Modified: Thu, 24 Jan 2019 08:26:02 GMT
ETag: "4-5802ff5f8b6b4"
Accept-Ranges: bytes
Content-Length: 4
Content-Type: text/html; charset=UTF-8
123
IP (tos 0x0, ttl 60, id 0, offset 0, flags [DF], proto TCP (6), length 52)
172.18.128.204.62534 > 10.0.0.128.80: Flags [.], cksum 0x7bae (correct), seq 3401804619, ack 2666924970, win 4099, options [nop,nop,TS val 960632560 ecr 19959044], length 0
- Server => Client:seq S_ISN:[S_ISN+length], ack C_ISN
NOTE 1:在经过了三次握手之后(Client 和 Server 都确定了对方的 seq ISN),正式的 HTTP 数据传输是在有序进行的。
NOTE 2:可以看见每一个数据包的发出都有相应的 ACK 响应,确保接收方有确切的接收到数据包,否则发送方会启用超时重发。
四次挥手
TCP 协议规定,对于已经建立的连接,收发双方要进行四次挥手才能成功断开连接,如果缺少了其中某个步骤,都会使连接处于假死状态,连接本身所占用的资源不会被释放。
- Step 1.
172.18.128.204.62534 > 10.0.0.128.80: Flags [F.], cksum 0x7bad (correct), seq 3401804619, ack 2666924970, win 4099, options [nop,nop,TS val 960632560 ecr 19959044], length 0
由 Client 提出断开连接请求 FIN(Flags [F.],
),Client 的 TCP 端口进入 FIN-WAIT-1 状态。
- Step 2、3.
10.0.0.128.80 > 172.18.128.204.62534: Flags [F.], cksum 0x3785 (incorrect -> 0x8acb), seq 2666924970, ack 3401804620, win 219, options [nop,nop,TS val 19959053 ecr 960632560], length 0
Server 发送 ACK 应答,Server 的 TCP 端口进入 CLOSE_WAIT 状态,Client 的 TCP 端口进入 FIN-WAIT-2 状态。几乎同时 Server 还发送了一个 FIN 端口连接请求,进入 LAST-ACK 状态。
- Step 4.
172.18.128.204.62534 > 10.0.0.128.80: Flags [.], cksum 0x7b9a (correct), seq 3401804620, ack 2666924971, win 4099, options [nop,nop,TS val 960632569 ecr 19959053], length 0
Client 进行 ACK 应答(LAST ACK),进入 TIME-WAIT(两倍的分段最大生存期),并最终变成 CLOSED 状态。
TCP 端口状态转移
TCP 协议规定,对于已经建立的连接,收发双方要进行四次挥手才能成功断开连接,如果缺少了其中某个步骤,都会使连接处于假死状态,连接本身所占用的资源不会被释放。实际上,一个网络服务器经常要同时管理大量的并发连接,所以需要保证无用的连接被完全断开,否则大量假死的连接会占用许多服务器资源。
对于这个问题,我们要关注 TCP 端口的:CLOSE_WAIT 和 TIME_WAIT 状态,与 TCP 四次挥手过程密切相关。
可以通过下面方法来查看 TCP 端口状态数量:
╭─mickeyfan@localhost ~
╰─$ netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}' 127 ↵
CLOSE_WAIT 1
TIME_WAIT 1
ESTABLISHED 17
状态转移
-
LISTENING:网络服务启动后首先会处于监听的状态
-
SYN_SENT:表示收发方请求建立连接
-
ESTABLISHED:表示成功建立连接,当你要访问其它的计算机的网络服务时首先要发个 SYN 信号到特定端口,此时当前服务器的 TCP 端口状态为 SYN_SENT,如果连接成功了则会变为 ESTABLISHED。
NOTE:SYN_SENT 状态一般非常短暂,但如果发现 SYN_SENT 的数量非常多并且在向不同的网络服务器发出,那当前机器可能中了冲击波或震荡波之类的病毒。这类病毒为了感染别的计算机,它就要扫描别的计算机,在扫描的过程中对每个要扫描的计算机都要发出了 SYN 请求,这也是出现许多 SYN_SENT 的原因。
- TIME_WAIT:当我方主动执行系统调用
close()
断开连接,并且收到对方的 ACK 确认后,我方的 TCP 端口状态就会变为 TIME_WAIT。
NOTE:TCP 协议规定 TIME_WAIT 状态会一直持续 2MSL(两倍的分段最大生存期,240s),以此来保证重新分配的 Socket 不会受到之前残留的延迟重发报文的影响(保证旧的连接状态不会对新连接产生影响)。处于 TIME_WAIT 状态的连接(Socket)占用的资源不会被内核释放,所以作为网络服务器,在可能的情况下,尽量不要主动断开连接。尤其对于要处理大量短连接的服务器,应该由客户端来主动提出断开,以减少 TIME_WAIT 状态造成的资源浪费。如果发现服务器存在大量的 TIME_WAIT,那么你应该检查是否有大量的自动断开连接动作存在服务器上。还有这样的情况,又我方提出断开连接,但对方一直不给 ACK 应答,我方就会卡在 FIN_WAIT_2 状态,此时我方默认等到 60 秒(可修改,参考 tcp_max_orphans)。所以这种情况下我方内存也会被大量无效数据报填满。
- CLOSE_WAIT:对方主动关闭连接(或连接异常中断),我方的状态就会变成 CLOSE_WAIT。此时我方会主动调用
close()
来使得连接被正确关闭。
NOTE:CLOSE_WAIT 表示我方被动断开连接,如果存在大量的 CLOSE_WAIT,表示我方只在第二次挥手时向对方应答 ACK,并没有完成第三次挥手,向对方发送 FIN 请求,这时就可能是因为在关闭连接之前网络服务器还有大量的数据要发送或者其他事要做,导致没有发送这个 FIN packet。一般是由网络服务器负载过高,或出现了不可预料的问题导致的。一个 CLOSE_WAIT 会维持至少 2 个小时,如果存在由于负载一直居高不下,生产了大量的 CLOSE_WAIT,就会造成资源极大的损耗,那么通常是等不到释放的那一刻,系统就已经解决崩溃了。
相关的内核参数:
vi /etc/sysctl.conf
net.ipv4.tcp_syncookies = 1
:表示开启 SYN Cookies。当出现 SYN 等待队列溢出时,启用 Cookies 来处理,可防范少量的 SYN 攻击,默认为 0,表示关闭.net.ipv4.tcp_tw_reuse = 1
:表示开启重用。允许将 TIME-WAIT Sockets 重新用于新的 TCP 连接,默认为 0,表示关闭。net.ipv4.tcp_tw_recycle = 1
:表示开启 TCP 连接中 TIME-WAIT Sockets 的快速回收,默认为 0,表示关闭。net.ipv4.tcp_fin_timeout
:系統等待 FIN_WAIT 超时时间。