由问题驱动,看一下TCP连接的本质吧。
1. TCP如何保证消息的有序和不丢包?
Sequence Number就是SYN——包的序号,用来解决网络包乱序(reordering)问题。
Acknowledgement Number就是ACK——用于确认收到,用来解决不丢包的问题。
2. 三次握手的目的?
为了建立可靠的数据传输,TCP通信双方相互告知初始化序列号(ISN),并确定对方已经收到ISN的(使用ACK机制)。
3. 三次握手的过程?
- 客户端发送一个
SYN
段,并指明客户端的初始序列号,即ISN(c)
. - 服务端发送自己的
SYN
段作为应答,同样指明自己的ISN(s)
。为了确认客户端的SYN
,将ISN(c)+1
作为ACK
数值。这样,每发送一个SYN
,序列号就会加1. 如果有丢失的情况,则会重传。 - 为了确认服务器端的
SYN
,客户端将ISN(s)+1
作为返回的ACK
数值。
4. 为什么不能用两次握手进行连接?
三次握手的目的就是为了建立可靠的连接,TCP通信双方都必须维护一个序列号,以标识发送的包哪些是被对方收到的。三次握手的过程中通信双方要相互告知初始化序列号,并确定对方已经收到。
如果只是两次握手,至多只有连接发起方的初始化序列号(ISN)能够被确认,另一方的序列化得不到确认。
5. 为什么建立连接是三次握手,而关闭连接却是四次挥手呢?
四次挥手过程:
主机A发送
FIN
后,进入终止等待状态,服务器B收到主机A连接的释放报文,就立即给主机A发送ACK
。然后服务器B就进入了close-wait
状态。并且服务器B再次发送
FIN
通知主机A关闭连接,服务器B进入最后确定状态。主机A收到服务器B
FIN
请求后,会发送一个ACK
告诉服务器B收到,于是客户端和服务器都关闭了。
FIN —— 该报文段的发送方已经结束向对方发送数据。
这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方是否现在关闭发送数据通道,需要上层应用来决定,因此,己方ACK和FIN一般都会分开发送。
一句话总结:是否关闭通道,是上层应用决定的的,TCP无权将FIN和ACK一同发送。
6. 三次握手建立连接时SYN超时?
server端接到了clien发的SYN后回了SYN-ACK后client掉线了,server端没有收到client回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server端如果在一定时间内没有收到的TCP会重发SYN-ACK
。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。
一句话总结:服务器未收到客户端的确定ACK
,便会一直重试。
7. 关于SYN攻击?
我们说过,在建立连接时,server未收到client端的ACK通知,便开始了长达63s的重试机制。
7.1 什么叫做SYN Flood:
一些恶意的人就为此制造了SYN Flood
攻击——给服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这样,攻击者就可以把服务器的syn连接的队列耗尽,让正常的连接请求不能处理。
7.2 妥协版的TCP协议:synccookies:
于是,Linux下给了一个叫tcp_syncookies
的参数来应对这个事——当SYN
队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number
发回去(又叫cookie
),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。请注意,请先千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为,synccookies
是妥协版的TCP协议,并不严谨。
7.3 对于正常的请求,你应该调整三个TCP参数可供你选择:
第一个是:tcp_synack_retries
可以用他来减少重试次数;
第二个是:tcp_max_syn_backlog
,可以增大SYN连接数;
第三个是:tcp_abort_on_overflow
处理不过来干脆就直接拒绝连接了。
8. 如何设置ISN的值?
三次握手的一个重要功能是客户端和服务端交换ISN(Initial Sequence Number)
, 以便让对方知道接下来接收数据的时候如何按序列号组装数据。
如果ISN是固定的,攻击者很容易猜出后续的确认号。
ISN = M + F(localhost, localport, remotehost, remoteport
)
M是一个计时器,每隔4微秒加1。 F是一个Hash算法,根据源IP、目的IP、源端口、目的端口生成一个随机数值。要保证hash算法不能被外部轻易推算得出。
一句话总结:ISN不能是固定不变的,一般是计时器(每4微妙+1)+随机hash值设置的,防止被攻击。
9、序列号回绕
因为ISN是随机的,所以序列号容易就会超过2^31-1. 而tcp对于丢包和乱序等问题的判断都是依赖于序列号大小比较的。此时就出现了所谓的tcp序列号回绕(sequence wraparound)问题。怎么解决?
内核代码:
/** The next routines deal with comparing 32 bit unsigned ints
* and worry about wraparound (automatic with unsigned arithmetic).*/
static inline int before(__u32 seq1, __u32 seq2){
return (__s32)(seq1-seq2) < 0;}
序列号发生回绕后,序列号变小,相减之后,把结果变成有符号数了,因此结果成了负数。
假设seq1=255, seq2=1(发生了回绕)。
seq1 = 1111 1111 seq2 = 0000 0001
我们希望比较结果是
seq1 - seq2=
1111 1111
-0000 0001
————————————
1111 1110
由于我们将结果转化成了有符号数,由于最高位是1,因此结果是一个负数,负数的绝对值为
0000 0001 + 1 = 0000 0010 = 2
因此seq1 - seq2 < 0
文章参考: