TCP连接管理浅析
文章目录
相对于SOCKET开发者,TCP创建过程和折除过程是由TCP/IP协议栈自动创建的.因此开发者并不需要控制这个过程.但是对于理解TCP底层运作机制,相当有帮助.
TCP报文格式
上图中有几个字段需要重点介绍下:
-
序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记。
-
确认号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1。
-
标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
URG:紧急指针(urgent pointer)有效。 ACK:确认序号有效。 PSH:接收方应该尽快将这个报文交给应用层。 RST:重置连接。 SYN:发起一个新连接。 FIN:释放一个连接。
需要注意的是:
-
不要将确认序号Ack与标志位中的ACK搞混了。
-
确认方Ack=发起方Req+1,两端配对。
TCP三次握手
过程详解
所谓三次握手(Three-way Handshake),是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。
三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换 TCP 窗口大小信息.在socket编程中,客户端执行connect()时。将触发三次握手。
第一次握手:
Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。
第二次握手:
Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。
第三次握手:
Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,
Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。
实际中还会出现同时发起主动打开的情况:通信的两端几乎在相同的时刻都会发送一个SYN报文段,然后它们进入SYN_SENT状态。当它们接收到对方发来的SYN报文段时会将状态迁移到SYN_RCVD,然后重新发送一个新的SYN并确认之前接收到的SYN,然后发送ACK。当通信两端都接收到了SYN和ACK,它们的状态都会迁移到ESTABLISHED,并且只建立一条连接。
为什么不能用两次握手进行连接
为了防止已失效的连接请求报文段突然又传送到了服务端,产生错误,产生的错误有两类:
client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。此时因为client没有发起建立连接请求,所以client处于CLOSED状态,接受到任何包都会丢弃。之后server一直等待接受数据,浪费大量资源。
如果服务器发送对这个延误的旧连接报文的确认的同时,客户端调用connect函数发起了连接,就会使客户端进入SYN_SEND状态,当服务器那个对延误旧连接报文的确认传到客户端时,因为客户端已经处于SYN_SEND状态,所以就会使客户端进入ESTABLISHED状态,此时服务器端反而丢弃了新的SYN包,而连接建立之后,发送包由于SEQ是以被丢弃的新的SYN包的序号为准,而服务器接收序号是以那个延误旧连接SYN报文序号为准,导致服务器丢弃后续发送的所有数据包。
采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,由于ACK不正确,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。
TCP 四次挥手
过程详解
TCP的连接的拆除需要发送四个包,因此称为四次挥手(four-way handshake)。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。
由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭,上图描述的即是如此。
第一次挥手:
Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。(发送了FIN只是表示这端不能继续发送数据(应用层不能再调用send发送),但是还可以接收数据。)
第二次挥手:
Server收到FIN后,发送一个ACK给Client,序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。Client接受到ACK序号后进入FIN_WAIT_2状态。
第三次挥手:
Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。
第四次挥手:
Client收到FIN后,Client进入TIME_WAIT状态,2MSL后发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。
上面是一方主动关闭,另一方被动关闭的情况,实际中还会出现同时发起主动关闭的情况,具体流程如下图:
CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
TIME_WAIT
从以上TCP连接关闭的状态转换图可以看出,主动关闭的一方在发送完对对方FIN报文的确认(ACK)报文后,会进入TIME_WAIT状态。TIME_WAIT状态也称为2MSL状态。
为什么需要TIME_WAIT
-
可靠地实现TCP全双工连接的终止
虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
-
允许老的重复segment在网络中消逝
假设在A端口和B端口之间有一个TCP连接。我们关闭这个连接,过一段时间后在相同的IP地址和端口建立另一个连接。后一个连接成为前一个的化身。因为它们的IP地址和端口号都相同。TCP必须防止来自某一个连接的老的重复分组在连接已经终止后再现,从而被误解成属于同一连接的某一个某一个新的化身。
为做到这一点,TCP将不给处于TIME_WAIT状态的连接发起新的化身。既然 TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活msl秒即被丢弃,另一个方向上的应答最多存活msl秒也被丢弃。 通过实施这个规则,我们就能保证每成功建立一个TCP连接时。来自该连接先前化身的重复分组都已经在网络中消逝了。
什么是2MSL
MSL就是maximum segment lifetime(最大分节生命期),这是一个IP数据包能在互联网上生存的最长时间,超过这个时间IP数据包将在网络中消失 。MSL在RFC 1122上建议是2分钟,而源自berkeley的TCP实现传统上使用30秒。
当主动关闭连接的A端在TIME_WAIT状态等待2MSL时间中, 如果对端B端未收到ACK则重发FIN报文,那么A端仍可以回复ACK。因此在2MSL时间中,如果A端没有收到B端的FIN报文,则表明B端正确接收ACK。所以将等待时间设为2MSL。
TIME_WAIT状态过多的处理方法
- net.ipv4.tcp_tw_reuse = 1
- 开启后,作为客户端时新连接可以使用仍然处于 TIME-WAIT 状态的端口
- 由于 timestamp 的存在,操作系统可以拒绝迟到的报文
- net.ipv4.tcp_timestamps = 1
- net.ipv4.tcp_tw_recycle = 0
- 开启后,同时作为客户端和服务器都可以使用 TIME-WAIT 状态的端口
- 不安全,无法避免报文延迟、重复等给新连接造成混乱
- net.ipv4.tcp_max_tw_buckets = 262144
- time_wait 状态连接的最大数量
- 超出后直接关闭连接
SO_REUSEADDR
当某个连接的一端处于TIME_WAIT状态时,该连接将不能再被使用。事实上,对于我们比较有现实意义的是,这个端口将不能再被使用。某个端口处于TIME_WAIT状态(其实应该是这个连接)时,这意味着这个TCP连接并没有断开(完全断开),那么,如果你bind这个端口,就会失败。对于服务器而言,如果服务器突然崩溃掉了,那么它将无法在2MSL内重新启动,因为bind会失败。解决这个问题的一个方法就是设置socket的SO_REUSEADDR选项。这个选项意味着你可以重用一个地址。
SO_LINGER
SO_LINGER选项决定调用close时TCP的行为。SO_LINGER涉及到linger结构体,如果设置结构体中l_onoff为非0,l_linger为0,那么调用close时TCP连接会立刻断开,TCP不会将发送缓冲中未发送的数据发送,而是立即发送一个RST报文给对方,这个时候TCP连接就不会进入TIME_WAIT状态。如你所见,这样做虽然解决了问题,但是并不安全。通过以上方式设置SO_LINGER状态,等同于设置SO_DONTLINGER状态。
CLOSE_WAIT大量出现原因与解决方案
close_wait大量出现的原因: 某一方在网络连接断开后,对等方没有检测到这个错误(对方断开)而没有调用 closesocket,导致了这个状态的出现.
那么,服务器上是怎么产生大量的失去控制的COLSE_WAIT状态的socket呢?
一个本质的原因是,服务器没有继续发FIN包给客户端。
服务器为什么不发FIN,可能是业务实现上的需要,现在不是发送FIN的时机,因为服务器还有数据要发往客户端,发送完了自然就要通过系统调用发FIN了,这个场景并不是上面我们提到的持续的COLSE_WAIT状态,这个在受控范围之内。
那么究竟是什么原因呢,咱们引入两个系统调用close(sockfd)和shutdown(sockfd,how)接着往下分析。
在这儿,需要明确的一个概念:一个进程打开一个socket,然后此进程再派生子进程的时候,此socket的sockfd会被继承。socket是系统级的对象,现在的结果是,此socket被两个进程打开,此socket的引用计数会变成2。
继续说上述两个系统调用对socket的关闭情况。
调用close(sockfd)时,内核检查此fd对应的socket上的引用计数。如果引用计数大于1,那么将这个引用计数减1,然后返回。如果引用计数等于1,那么内核会真正通过发FIN来关闭TCP连接。
调用shutdown(sockfd,SHUT_RDWR)时,内核不会检查此fd对应的socket上的引用计数,直接通过发FIN来关闭TCP连接。
现在应该真相大白了,可能是服务器的实现有点问题,父进程打开了socket,然后用派生子进程来处理业务,父进程继续对网络请求进行监听,永远不会终止。客户端发FIN过来的时候,处理业务的子进程的read返回0,子进程发现对端已经关闭了,直接调用close()对本端进行关闭。实际上,仅仅使socket的引用计数减1,socket并没关闭。从而导致系统中又多了一个CLOSE_WAIT的socket。
如何避免这样的情况发生?
有两种方法:
1. 父进程提前关闭套接字,让套接字在子进程中时引用计数为1,从而在子进程中顺利发送FIN
|
|
父进程显式关闭socket只是减少CLOSE_WAIT的socket,如果不加这个语句,在下次循环前,该socket也会自动消失,不影响正确性。
2. 子进程处理结束后利用shutdown强制发送FIN,这是最快速高效的手段
shutdown(sockfd, SHUT_RDWR);
close(sockfd);
这样处理,服务器的FIN会被发出,socket进入LAST_ACK状态,等待最后的ACK到来,就能进入初始状态CLOSED。
为什么建立连接协议是三次握手,而关闭连接却是四次握手呢
服务端的LISTEN状态下的SOCKET当收到SYN报文的连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。
但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可能未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
与TCP连接管理相关的攻击
SYN泛洪
在三次握手过程中,服务器发送SYN-ACK之后,收到客户端的ACK之前的TCP连接称为半连接(half-open connect).此时服务器处于Syn_RECV状态.当收到ACK后,服务器转入ESTABLISHED状态.
Syn泛洪就是攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直 至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。
Syn泛洪是一个典型的DDOS攻击。检测SYN攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击.在Linux下可以如下命令检测是否被Syn攻击
netstat -n -p TCP | grep SYN_RECV
因为区分合法的连接尝试与SYN泛洪并不是一件容易的事情,所以抵御上述进攻存在一定的难度。一种针对此问题的机制被称作SYN cookies 。 它的主要思想是,当一个SYN到达时,这条连接存储的大部分信息都会被编码并保存在SYN+ACK报文段的序列号字段。采用SYN cookies 的目标主机不需要为进入的连接请求分配任何存储资源——只有当SYN+ACK报文段本身被确认后(并且已返回初始序列号)才会分配真正的内存。在这种情况下,所有重要的连接参数都能够重新获得,同时连接也能够被设置为ESTABLISHED状态。
在执行SYN cookies 过程中需要服务器仔细地选择TCP初始序列号。本质上,服务器必须将任何必要的状态编码并存于SYN+ACK报文段的序列号字段。这样一个合法的客户端会将其值作为报文段的ACK号字段返回给服务器。很多方法都能够完成这项工作,下面介绍Linux系统所采用的技术。
服务器在接收到一个SYN后会采用下面的方法设置初始序列号(保存于SYN+ACK报文段,供于客户端)的数值:首5位是t模32的结果,其中t是一个32位的计数器,每隔64秒增1;接着3位是对服务器最大段大小的编码值,剩余的24位保存了4元组与t值得散列值。该数值是根据服务器选定的散列加密算法计算得到的。
在采用SYN cookies方法时,服务器总是以一个SYN+ACK报文段作为响应。在接收到ACK后,如果根据 其中的t值可以计算出与加密的散列值相同的结果,那么服务器才会为该SYN重新构建队列。
这种方法至少有两个缺陷。首先,由于需要对最大段大小进行编码,这种方法禁止使用任意大小的报文段。其次,由于计数器会回绕,连接建立过程会因周期非常长而无法正常工作。基于上述原因,这一功能并未作为默认设置。
序列号攻击
序列号攻击涉及破坏现有的TCP连接,甚至可能将其劫持。这一类攻击通常包含的第一步是使两个之前正在通信的TCP节点“失去同步”。这样它们将使用不正确的序列号。至少有两种方法能实现上述攻击:在连接建立过程中引发不正确的状态传输,在ESTABLISHED状态下产生额外的数据。一旦两段不能再进行通信(但却认为它们间拥有一个打开的连接),攻击者就能够在连接中注入新的流量,而且这些注入的流量会被TCP认为是正确的。
参考
文章作者 Forz
上次更新 2017-06-25