TCP是一种面向连接的单播协议,在发送数据之前,通信双方必须在彼此建立一条连接;这与UDP的无连接不同,UDP无需通信双方发送数据之前建立连接。所有TCP需要处理多种TCP状态时需要面对的问题,比如连接的建立、传输、终止,以及无警告的情况下重新启动,这也是TCP与UDP之间的主要区别之一。
TCP连接的建立和终止过程
一个TCP连接由一个4元组构成,它们分别是两个IP地址和两个端口号。更准确地说,一个TCP连接是由一对端点或套接字构成,其中通信的每一端都由一对(IP地址,端口号)所唯一标识。
一个TCP连接通常分为3个阶段:建立连接、数据传输(连接已建立)、关闭连接。下面描述一次TCP连接的建立和关闭过程。
连接过程:
1. 客户端发送一个SYN报文段(即一个在TCP头部的SYN位字段置位的TCP/IP数据包),并指明自己想要连接的端口号和它的客户端初始序列号(记为ISN(c))。
2. 服务器接收到客户端发送的SYN报文段后,也发送自己的SYN报文段作为响应,并包含了服务器的初始序列号ISN(s)。为了确认客户端的SYN,服务器将客户端SYN报文段中包含的ISN(c)数值加1后作为返回的ACK数值。因此,每发送一个SYN,序列号就会自动加1。
3. 客户端接收到服务器发送的SYN报文段后,为了确认服务器的SYN,客户端将ISN(s)的数值加1后作为返回的ACK数值。
4. 至此,连接被建立,以上过程也被称为三次握手。TCP通信可进入数据传输阶段。
关闭过程:
1. 连接的主动关闭者(此例中是客户端)发送一个FIN位字段置位的TCP/IP数据包指明希望断开连接及接收者希望看到的自己当前序列号K。FIN报文段还包含了一个ACK段用于确认对方最近一次发来的数据L。
2. 连接的被动关闭者(此例中是服务器)将K的数值加1作为响应的ACK值,以表明它已经成功接收到主动关闭着发送的FIN。被动发送者将身份转变为主动关闭者,发送自己的FIN报文段,序列号为L,ACK为K+1。
3. 当前的主动关闭者(此时是客户端)发送一个FINA位字段置位的TCP/IP数据包指明希望断开连接,FIN报文段序列号为L,ACK为K+1。
4. 当前的被动关闭者(此时是服务器)接收到信息后,将L加1后作为ACK,将K作为Seq,回应给主动关闭者,以确认上一个FIN。
5. 至此,连接被关闭。
由于TCP的双工通信特性,TCP支持半关闭。TCP的半关闭操作是指仅关闭数据流的一个传输方向,而另一方扔在传输数据直到它被关闭为止。
文中还讨论过TCP通信双方同时打开与同时关闭的情况:假设主机A的应用程序通过本地7777端口向主机B的8888端口发送一个主动打开请求,同时,主机B的应用程序也通过本地8888端口向主机A的7777端口发送一个主动打开请求。
过程如下图:
通信的双方同时扮演客户端和服务器的角色。在同时打开时,与正常的连接建立过程相比,需要增加一个报文段;在同时关闭时,与正常关闭相似,只是报文段的顺序是交叉的。
初始序列号的存在由于以下2个原因:
1. 在连接打开时,任何拥有何时的IP地址、端口号、符合逻辑的序列号以及正确校验和的报文都将被对方接收。
2. TCP报文段在经过网络路由有可能会存在延迟抵达和排序混乱的情况。
在发送用于建立连接的SYN之前,通信双方会选择一个初始序列号。初始序列号会随时间而改变,因此每个连接都拥有不同的初始序列号。[RFC0793]指出初始序列号可被视为一个32位的计数器,计数器的值每4微秒加1,以防止出现于其他连接的序列号重叠的情况。
在连接建立超时的时候,连接的打开方会产生指数回退行为:客户端TCP为了建立连接而频繁发送SYN报文段,首个报文段发送后3s发送第二个,第二个报文段之后6s发送第三个,第三个报文段12s后发送第四个...
并且一些系统可配置发送初始SYN的次数,通常选择一个较小的数值5。比如在Linux中,系统配置变量net.ipv4.tcp_syn_retries表示了在一次主动打开申请中尝试重新发送SYN报文段的最大次数;net.ipv4.tcp_synack_retries则表示响应对方的一个主动打开请求时尝试重新发送SYN+ACK报文段的最大次数。
当TCP发现一个到达的报文段对于相关连接而言是不正确的时候,TCP就会发送一个重置报文段,该报文段将TCP头部的RST位字段置位。书本列出以下场景来证明重置报文段的用途:
1. 针对不存在端口的连接请求:回忆之前学习的UDP,当数据报遇到目的地不可达的情况会生成一个ICMP目的不可达的消息;而对于TCP则使用重置报文来代替相关工作。
2. 终止一条连接:发送FIN的终止连接方法有时被称为有序释放;通过发送一个RST报文段也可以终止一条连接,被称为终止释放。终止连接可以为应用程序提供两大特性:1.任何排队的数据都将被抛弃,一个重置报文段会被立即发送出去;2.重置报文段的接收方会说明通信另一端采用了终止的方式而不是正常关闭。
3. 半开连接:通信一方的主机(例如服务器)奔溃的情况下,只要不尝试通过半开连接传输数据,正常工作的一端(客户端)将不会检测出另一端已经奔溃。服务器重启后,客户端再次连接,由于服务器对收到的报文无法做出处理,会响应一个重置报文段,之后两端之间的连接将被关闭以重新建立。
4. 时间等待错误:
TCP状态
箭头表示因报文段传输、接收以及计时器超时而引发的状态转换;粗箭头表示典型的客户端的行为;虚线箭头表示典型的服务器行为。
针对上面的状态图,再看正常的TCP连接建立和关闭过程中客户端和服务器经历的状态变化:
值得注意的是状态TIME_WAIT(2MSL)也称为2MSL等待状态,在该状态中,TCP将会等待两倍于最大段生存期(MSL)的时间,也被称作加倍等待。每个实现都必须为最大段生存期选择一个数值,它代表任何报文段在被丢弃之前再网络中允许存在的最长时间。
TCP段文报以IP数据报的形式传输,IP数据报拥有TTL字段和跳数限制字段,也就限制了IP数据报的有效生存时间。[RFC0793]将最大段生存期设为2分钟,然而在大多数的实现中,该数值可修改。
假设已经设定了MSL的数值,按照规则:当TCP执行一个主动关闭并发送最终的ACK时,连接必须处于TIME_WAIT状态并持续两倍于最大生存期的时间,这样就能够让TCP重新发送最终的ACK以避免出现丢失的情况。另一个影响是,当TCP处于等待状态时,通信双方将该连接定义为不可重新使用。只有当2MSL等待结束时,才可以创建新的连接实例。
当连接处于2MSL等待状态时,任何延迟到达的报文段都将被丢弃。为了防止处于TIME_WAIT状态下的系统奔溃或者重启,[RFC0793]指出在崩溃或者重启后TCP协议应当在创建新的连接之前等待相当于一个MSL的时间(静默时间)。
TCP选项
TCP头部包含多个选项,整个选项列表是有互联网编号分配机构(IANA)维护的。每个选项的头一个字节为"种类",指明了该选项的类型。根据[RFC1122],不能被理解的选项会被简单的忽略掉。种类值为0或者1的选项仅占一个字节,其他选项会根据种类来确定自身的字节数len,选项的总长度包括了种类和len个字节。需要注意的是,TCP头部的长度应该是32比特的倍数,因为TCP头部长度字段是以此为单位的。
如一些TCP选项:
- 种类 | 长度(len) | 名称 | 描述
- 0 1 EOL 选项列表结束
- 1 1 NOP 无操作(用于填充)
- 2 4 MSS 最大段大小
- 3 3 WSOPT 窗口缩放因子(窗口左移量)
- 4 2 SACK-Permitted 发送者支持SACK选项
- 5 可变 SACK SACK阻塞(接收到乱序数据)
- 6 10 TSOPT 时间戳选项
- 28 4 UTO 用户超时(一段空闲时间后的终止)
- 29 可变 TCP-AO 认证选项(使用多种算法)
- 253 可变 Experimental 保留供实验所用
- 254 可变 Experimental 保留供实验所用
最大段大小(MSS)是指TCP协议所允许的从对方接收到的最大报文段,也是通信对方在发送数据时能够使用的最大报文段。根据[RFC0879],该选项只记录TCP数据的字节数而不包括其他相关的TCP与IP头部。
当建立一条TCP连接时,通信的每一方都要在SYN报文段的MSS选项中说明自已允许的最大段大小,在没有事先指明的情况下,最大段大小的默认数值为536字节。其计算:(576/IPv4数据报(最小)-20/IPv4基本头部(最小)-20/TCP基本头部(最小)=536)。根据以太网中最大传输单元与互联网路径最大传输单元的典型数值1500来算,最大段大小数值为1460,这也是IPv4协议族的典型值。
选择确认选项(SACK)用于发送方了解接收方的数据接收情况(1.由于采用累积ACK确认,TCP不能正确地确认之前已经接收的数据;2.由于数据的接收是无序的从而导致接收的数据序列号是不连续的),以在报文段丢失或被接收方遗漏时更换的进行重传工作。
SACK信息保存于ASCK选项中,包含了接收方已经成功接收的数据块的序列号范围,每个范围被称为一个SACK块,由一对32位的序列号表示,因此,一个SACK选项包含了n个SACK块,长度为(8n+2)个字节,增加的2个字节用于保存SACK选项的种类与长度。由于TCP头部选项的空间有限,因此一个报文段中发送的最大SACK块数目为3。虽然只有SYN报文段才能包含"允许选择确认"选项,但只要发送方已经发送了该选项,SACK块就能够通过任何报文段发送出去。
窗口缩放选项能够有效的将TCP窗口大小字段的范围从16位增加至30位[RFC1323],TCP头部不需要改变窗口大小字段的大小,仍维持16位的数值。同时,使用另一个选项作为这16位数值的比例因子,该比例因子能够使窗口字段值有效地左移,比如:将窗口数值扩大至原先的2的s次方倍,s为比例因子。窗口的移动数值是由TCP通信方根据接收缓存的大小自动选取的。一个字节的移动可以用0至14来计数,计数0表示没有任何比例,最大比例数值是14,它能够提供最大为1073725440字节(65535*2的14次方)的窗口,因此TCP使用一个32位的值来维护这个"真实"的窗口大小。该选项只能出现于一个SYN报文段中,因此当连接建立以后比例因子是与方向绑定的。为了保证窗口调整,通信双方都需要在SYN报文段中包含该选项,且每个方向的比例因子可各不相同。主动打开连接的一方利用自已的SYN中发送该选项,但被动打开连接的一方只能在接收到的SYN中指出该选项时才能发送。如果主动打开连接的一方发送了一个非0的比例因子但却没有接收到来自对方的窗口缩放选项,它会将自已发送与接收的比例因子数值都设为0。
时间戳选项要求发送方在每个报文段中添加2个4字节的时间戳数值。接收方在确认中反映这些数值,允许发送方针对每个收到的ACK估算TCP连接的往返时间。当时有时间戳选项时,发送方将一个32位的数值填充到时间戳字段作为时间戳的第一个部分;接收方则将收到的时间戳数值原封不动的填充至第二部分的时间戳回显重试字段(TSER或TSecr)。使用时间戳选项的TCP头部会增加10字节(8字节用于保存2个时间戳数值,2字节用于指明选项数值和长度)。使用时间戳选项可让接收者避免接收旧报文段和判断报文段正确性,时间戳选项使用扩展的这种方法被称为防回绕序列号(高速传输的环境中,如果重传时间差小于MSL)。
用户超时超时选项是数值指明了TCP发送者在确认对方未能成功接收数据之前愿意等待该数据ACK确认的时间。根据[RFCO793]描述,USER_TIMEOUT是TCP协议本地配置的一个参数,用户超时选项允许TCP通信方将自已的USER_TIMEOUT数值告知连接的对方,这样就方便了TCP接收方调整自已的行为。NAT设备也能够解释这些信息以帮助设置它们的连接活动计时器。
认证选项目的在于增强与替换较早的TCP-MD5机制[RFC2385],它使用一种加密散列算法以及TCP连接上分共同维护的一个秘密值来认证每一个报文段。TCP认证选项不仅提供各种加密算法,还使用“带内”信令来确认密钥是否改变,因此它与TCP-MD5相比有很大的提高。然而,该选项未提供一个全面密钥管理方案...也就是说,通信双方不得不采用一种方法在TCP认证选项运行之前建立出一套共享密钥。
TCP相关
TCP的路径最大传输单位发现
TCP常规的路径最大传输单元(MTU)发现过程如下:一个连接建立时,TCP使用对外接口的最大传输单元的最小值,或者根据通信对方声明的最大段大小来选择发送方的最大段大小(SMSS)。
TCP路径MTU发现遵循以下规则:
1. 路径最大传输单元发现不允许TCP发送方有超过另一方所声明的最大段大小的行为。
2. 如果对方没有指明最大段大小的数值,发送方将采用默认的536字节。
3. 如果收到PTB消息,TCP就会减少段的大小,然后用修改过的段大小进行重传(在网络传输过程中,如果中间链路的最大传输单元小于两端通信节点的,路径MTU发现机制能够找出合适的段大小以供传输使用)。
值得注意的是,一条连接的两个方向的路径最大传输单元是不同的。
重置报文段的使用例子
时间等待错误过程如下图:
如上图,当FIN报文被发送且客户端已经进入TIME_WAIT状态时,如果这时候服务器发送了一个如图上的"旧"的报文段(Seq=L-100,ACK=K-200),客户端会响应一个ACK(ACK=L,Seq=K)说明最新数据的信息,然而服务器接收到这个报文后它已经是关闭状态了,所以服务器响应一个RST报文段,告诉客户机(提前)关闭连接。
TCP服务器选项
对于服务器的设计而言,TCP也存在一些影响:
如果主机是多宿主主机,可以为本地IP地址指定一个单一的地址,并且只有被该接口接收到的连接才能够被接受。而哪一个进程该处理接收到的报文段则有4元组(目的IP、目的端口号、源IP、源端口号)多路分解后决定。
与UDP服务器相似,TCP服务也可以限制对本地IP地址的请求,如sock -s 10.0.0.1 8888,限制只能使用到达本地IPv4地址10.0.0.1的8888端口才能收到连接信息。
[RFC0793]描述,TCP的抽象接口函数允许一台服务器为一个指定的外部节点或者一个未被指定的外部节点执行被动打开。然而套接字API并未提供实现的函数,于是,服务器不必指定客户端的节点,而是等待连接的到来,然后检查客户端的IP地址和端口号。
一个并行服务器会为每个客户端分配一个新的进程或线程,这样负责侦听的服务器能够始终准备着处理下一个到来的连接请求,在侦听服务器正在创建一个新进程时,或操作系统忙于运行其他高优先级的进程时,或服务器可能正在被伪造的连接请求攻击时,多个连接请求可以回到达。针对以上情况,操作系统通常会维护连接列队来做请求数量的控制。如Linux中,连接列队遵循以下规则:
1. 当一个连接请求到达,检查系统范围的参数net.ipv4.tcp_max_syn_backlog(默认1000)。如果处于SYN_RCVD状态的连接数目超过这一阈值,进入的连接将被拒绝。
2. 每个处于侦听状态下的节点都拥有一个固定长度的连接列队。其中的连接已经被TCP完全接受(即已完成三次握手),但未被应用程序接受。应用程序会对这个列队做出限制,通常称为未完成连接(backlog),backlog的数目必须在0与系统指定的最大值之间,该最大值为net.core.somaxconn,默认值128(含)。
3. 如果侦听节点的列队中仍然有空间分配给新的连接,TCP模块会应答SYN并完成连接。知道接收到三次握手中的第三个报文段之后,与侦听节点相关的应用程序才会知道新的连接。这种情况下,客户端可能会认为服务器已经准备好接收数据了,然而服务器只是TCP模块构成了连接,应用程序并未收到相关连接,这时的TCP模块将会把接收的数据存入队列中。
4. 如果队列中没有足够的空间分配给新的连接,TCP会延迟对SYN做出响应,从而给应用程序处理的时间。如果系统变量nex.ipv4.tcp_abort_on_已经被设定,新进入的连接会被重置报文段重新置位。
在队列溢出的情况下,发送重置报文段通常是不可取的,而且默认情况4下该功能是关闭的。客户端在交换SYN期间如果收到一个重置报文段,它可能认为服务器是不存在的,而不是处于繁忙状态。所以,正常的操作是应当阻止应用程序再去服务那些进入的连接。
相关攻击
SYN泛洪,一种TCP拒绝服务攻击,在这种攻击中一个或多个恶意的客户端产生一系列TCP连接尝试(SYN报文段),并将它们发送给一台服务器,它们通常采用"伪造"的源IP地址。服务器会为每一条连接分配一定数量的连接资源,由于连接尚未完全建立,服务器为了维护大量的半打开连接会在耗尽自身内存后拒绝为后续的合法连接请求服务。
针对SYN泛洪,一种称为SYN cookies的机制做出以下处理:当一个SYN到达时,这条连接存储的大部分信息都会被编码并保存在SYN+ACK报文段的序列号字段。采用SYN cookies的目标主机不需要为进入的连接请求分配任何存储资源,只有当SYN+ACK报文段本身被确认后才会分配真正的内存。在这种情况下,所有重要的连接参数都能够重新获得,同时连接也能够被设置位ESTABLISHED状态。
有一种是攻击影响路径MTU发现过程,伪造ICMP PTB消息迫使通信双方采用非常小的数据报进行传输,从而降低通信的性能。
还有种破坏甚至劫持TCP连接的攻击,该攻击通常包含的第一步是使两个之前正在通信的TCP节点"失去同步"。这样他们就使用了不正确的序列号,称为序列号攻击。至少有两种方法实现上述攻击:在连接建立过程中引发不正确的状态传输;在ESTABLISHED状态下产生额外的数据。
欺骗攻击,这类攻击所涉及的TCP报文段由攻击者精心定制,目的在于破坏或改变现有TCP连接的行为。攻击者可能生成一个伪造的重置报文段并将其发送给一个TCP通信节点,假设与连接相关的4元组以及校验和都是正确的,序列号也处于正确的范围,就会造成连接的任意一端失败。相关的防御技术包括:使用TCP-AO选项;要求重置报文段拥有一个特殊的序列号以代替处于某一范围的序列号了要求时间戳选项具有特定的数值;使用其他形式的cookie文件,让非关键的数据依赖于更加准确的连接信息或一个秘密数值。
参考:
《TCP IP 详解卷1:协议》