文章目录
TCP连接的建立与终止
一个TCP连接是由一个4元祖构成的,即两个IP地址,连个端口号。更准确地说,一个TCP连接是由一对端点或套接字构成,其中通信的每一端都由一对(IP地址,端口号)所唯一标识。
一个TCP连接通常分为3个阶段:建立连接、数据传输(连接已建立)、关闭连接。下面描述一次TCP连接的建立和关闭过程。
解释一下术语(本文描述会按照这些来说,但上图不是,上图应该写成Ack = K + 1):
SYN:标志位。
FIN:标志位。
ACK:标志位。
Ack:acknowledge sequence number,确认号。
Seq:Data sequence number,序号。
连接过程:
- 客户端发送一个SYN报文段,并指明自己想要连接的端口号和它的初始序列号,记为 I S N ( c ) ISN(c) ISN(c)。
- 服务器接收到客户端发送的SYN报文段后,也发送自己的SYN报文段作为响应,并包含了服务器的初始序列号 I S N ( s ) ISN(s) ISN(s),所以是个SYN。为了确认客户端的SYN,服务器将客户端SYN报文段中包含的 I S N ( c ) ISN(c) ISN(c)数值加1后作为返回的Ack号,所以是个ACK。加起来就是个SYN+ACK。另外,每发送一个SYN,序列号就会自动加1(其实就是,把SYN当成发送了data length=1的普通数据来看待,那自然对方的Ack号会加1了)。
- 客户端接收到服务器发送的SYN报文段后,为了确认服务器的SYN,客户端将 I S N ( s ) ISN(s) ISN(s)的数值加1后作为返回的Ack号。
- 至此,连接被建立,以上过程也被称为三次握手。TCP通信可进入数据传输阶段。
解释两个序号:
- 以这个分组的发出方作为自己,接收方作为对方。
- 自己发出的分组的Ack号:首先,它代表了自己已接收对方的
(... , Ack-1]
区间内的数据。另一层含义就是,自己期待下一次收到的对方的分组的Seq号就是这个Ack号,即自己期望对方的下一个分组包含了[Ack , ...)
的Data。 - 自己发出的分组的Seq号:自己发出的这个分组的Data的起始序列号,即这个分组包含了
[Seq , ...)
的Data。一般情况下,数据传输正常时,Seq号会和 上一次对方发来的分组的Ack号一样;这种情况下,可以说,自己刚好发送了对方期待的那个分组(或者对方期待的那个序号)。
三次握手的目的:
- 让通道双方了解到一个连接正在建立
- 利用TCP的选项来承载特殊信息,比如初始序列号(Initial Sequence Number,ISN)。
关闭过程:
- 连接的主动关闭者(此例中是客户端)发送一个FIN位字段置位的包指明希望断开连接,以及数据包所承载的Data的起始序列号(当然,实际上这个数据包并没有Data,但FIN标志位相当于一个字节的Data)。FIN报文段还包含了一个Ack号用于确认对方最近一次发来的数据L。
- 连接的被动关闭者(此例中是服务器)将K的数值加1作为响应的Ack值,以表明它已经成功接收到主动关闭者发送的FIN。
- 被动关闭者将身份转变为当前的主动关闭者,发送自己的FIN报文段,Seq号为L,Ack为K+1。
- 当前的被动关闭者(此时是客户端)接收到信息后,将L加1后作为ACK,将K作为Seq,回应给主动关闭者,以确认上一个FIN。
由于TCP的双工通信特性,TCP支持半关闭。TCP的半关闭操作是指仅关闭数据流的一个传输方向,而另一方仍在传输数据直到它被关闭为止。
TCP半关闭
伯克利套接字的API提供了半关闭操作:应用程序可以调用shutdown函数来代替close函数。
上图过程的不同点:被动关闭方会继续传一会数据。
同时打开和关闭
同时打开的过程需要4个报文段,但从下图可以看出,中间的那两个报文段是可以合并的。
同时关闭的过程依然需要4个报文段,只是顺序交叉了。
初始序列号
当一个连接打开时,任何拥有合适的IP地址、端口号、符合逻辑的序列号(即在滑动窗口内)以及正确校验和的报文段都将被对方接收。但一个延迟到达的报文段的序列号也有可能在滑动窗口内,这会造成一些问题。为了解决这个问题,我们需要谨慎选择序列号。
三次握手期间的前两次,双方会交换自己的初始序列号。初始序列号会随时间而改变,因此每一个连接都拥有不同的初始序列号。这样做的好处是:
- 对于同一方来说,一个连接的报文段的序列号,就不会和另一个连接的序列号重叠。
- 比如
(客户端a的ip:客户端a的端口:服务器:80)
和(客户端b的ip:客户端b的端口:服务器:80)
这两个四元组,它们的初始序列号不会重叠。
- 比如
- 对于同一个连接的两个不同实例来说,不同实例的序列号也不能出现重叠。
- 比如
(客户端a的ip:客户端a的端口:服务器:80)
打开了一会后关闭,然后客户端a又重新连接服务器,所以(客户端a的ip:客户端a的端口:服务器:80)
又被重新打开,这就是两个实例。
- 比如
连接建立超时
一种常见情况是服务器关闭。TCP客户端发送SYN包后,没有收到对应的ACK,那么它会频繁的继续发送SYN包。
如上图,时间间隔是3秒,然后6秒,然后12秒。这一行为称为指数回退。
发送初始SYN的次数,通常为5。
TCP选项
每个选项的头一个字节放置种类Kind。前两种选项只会占一个字节,即只有种类占的那个字节。其他的选项,会根据其种类来判断一共多少字节。
由于TCP头部是32bit的整数倍,所以使用TCP选项时,会使用NOP来填充;在末尾,会使用EOL来填充。
最大段大小选项
Maximum Segment Size,即MSS。MSS是指TCP协议所允许的从对方接收到的最大报文段。这个长度,实际上它是指 TCP协议的payload的最大长度,所以它不包括 TCP头部和IP头部的长度。
如上图为三次握手的前两次。第一次,本机ip告诉百度ip,我的笔记本能接收MSS为1460,百度知道这个信息后,每次往TCP协议塞数据的时候,最多也就会塞1460字节的数据。第二次,同理。
如果没有事先指明,那么MSS默认为536字节。
选择确认选项
TCP除了基本的累积确认以外,还会提供选择确认的功能。
接收方使用SACK来描述失序到达的数据,从而帮助发送方有效地重传数据。
窗口缩放选项
在TCP连接建立中,其中一端可以通过Window scale(即窗口缩放选项)设置其接收窗口的基数。
在三次握手的第一次中,本机给出了Window scale为8,那么基数为
2
8
2^8
28。
在三次握手的第一次中,本机给出了Window为512。那么最终本机的接收窗口的大小为512*
2
8
2^8
28=131072。这符合上图的Calculated window size。
时间戳选项与防回绕序列号
时间戳选项即TSOPT。
- 当使用时间戳选项时,发送方将一个32位的数值填充到时间戳字段作为时间戳的第一个部分(TSV或TSval);
- 接收方则将收到的时间戳数值原封不动的填充至第二部分的时间戳回显重试字段(TSER或TSecr)。使用时间戳选项的TCP头部会增加10字节(8字节用于保存2个时间戳数值,2字节用于指明选项数值和长度)。
时间戳只是一个单调增加的数值。在任意时刻,TCP的两端都会有各自的时间戳,但它们之间没有关系,也不要求两台主机之间进行时钟同步。
精确估算重传超时
由于有了时间戳选项,现在发送方可以通过发送数据的ACK来估算TCP连接的往返时间了。如上图,发送方通过a2与a1之间的差值,就能进行估算。
而估算一条TCP连接的往返时间主要是为了设置重传超时的时间。时间戳帮助我们精确估算重传超时。
防回绕
时间戳选项也可帮助TCP的接收方避免接收到旧的报文段。它被称作——防回绕序列号,即Protection Against Wrapped Sequence numbers,PAWS。
从TCP的结构可知,序列号总共只有32bit,而TCP作为一个基于字节流的协议,一直发数据下去,一定会超过32bit的范围,从而又从0开始作为其序列号。
我们可以把防回绕序列号,看作这32bit序列号的拓展。
- 从D到E,序列号已经不够用了,出现了回绕,又从0开始使用序列号了。
- 在B时刻,一个报文段丢失,然后马上重传。这个丢失的包在网络中绕了个大圈,之后才到达。
- 在F时刻的接收窗口,与B时刻的接收窗口,是同一个范围。既然窗口范围一样,完全有可能把旧的数据当作新的数据。
- 但现在有了时间戳选项,旧报文的时间戳2,小于现在的报文的时间戳5或6,所以接收方根据这个原因,就把旧的数据丢弃掉。
- 这里得考虑这个旧的数据的生存时间是小于MSL的。
用户超时选项
指明了TCP发送者在确认对方未能成功接收数据之前愿意等待该数据ACK确认的时间。
认证选项
目的在于增强与替换较早的TCP-MD5机制[RFC2385]。
TCP的路径最大传输单位发现
路径最大传输单元(Maximum Transmission Unit,MTU)——指经过两台主机之间路径的所有网络报文段中最大传输单元的最小值。就好比一个木桶,要取最短的那根木条。
知道路径最大传输单元后能够有助于 避免TCP分片。
TCP路径最大传输单位发现的过程:在一个连接建立时,TCP使用对外接口的最大传输单元的最小值,或者根据通信期间对方声明的最大段大小(SMSS)。
一条连接上的两个方向上的MTU是可能不同的。
一旦为发送方的MTU选定了初始值,TCP通过这条连接发送的所有IPv4数据报都会对DF位字段进行设置。即不允许分片了。
MTU是数据链路层的协议的payload大小的数值,而MSS是TCP协议的payload大小的数值。而MTU的payload是用来存放IP包的,所以:
- MTU = IP头部 + TCP头部 + TCP的payload
- MTU = IP头部 + TCP头部 + MSS
- MTU = 20 + 20 + MSS
- MTU = 40 + MSS
TCP状态转换
首先需要熟记上面这个流程图。
这张状态转换图的转移基本上可以从流程图得出。但这里需要重点讲一下一些特殊的转换:
- 从LISTEN转移到SYN_SENT。这个很少见,在伯克利套接字不被支持。
- 从SYN_SENT转移到SYN_RCVD。这个同时打开时才会出现,即双方几乎同时发送了SYN。
- 从SYN_RCVD转移到LISTEN。当服务器处于SYN_RCVD时收到了RST,就会这样。
上面的是被动关闭方的过程:
- 因为被动关闭方收到对方的FIN,说明对方已经关闭连接,只剩下自己这边要关闭连接了。即需要等待自己这边关闭,所以是CLOSE_WAIT。
- 被动关闭方发送了FIN后,另一半连接也就马上关闭了,但还需要等待最后这个ACK。所以是LAST_ACK(或者理解为LAST_ACK_WAIT)。
- 被动关闭方收到最后这个ACK后,整个流程完美结束,就变成CLOSED。
下面的是主动关闭方的过程(发送FIN后,主动关闭方进入FIN_WAIT_1):
- 主动关闭方接收到的是ACK,那么说明对方还有数据需要传输,所以进入FIN_WAIT_2。在FIN_WAIT_2时,接收到了FIN,那么发送ACK进入TIME_WAIT状态。
- 主动关闭方接收到的是FIN+ACK,那么说明对方没有数据需要传输了。那么直接发送ACK从FIN_WAIT_1状态进入TIME_WAIT状态(从流程图看,可能会漏掉这种可能,需要注意)。
- 主动关闭方接收到的是FIN,那么说明双方在同时关闭,这里先进入CLOSING这个中间状态。和上一行情况基本类似,只是拆分了一下,所以有个中间状态CLOSING,之后才收到ACK所以此时才从中间状态CLOSING转移到TIME_WAIT。
TIME_WAIT状态
主动关闭方在TIME_WAIT状态,会等待2个MSL的时间(最大段生存期,Maximum Segment Lifetime)。
TIME_WAIT状态存在的原因:
- 可靠的终止TCP连接。
- 保证让迟来的TCP报文有足够的时间被识别并丢弃。
TIME_WAIT状态需要等待两个MSL的原因:
- 主动关闭方最后发送的ACK可能会丢失。
- 对方(即被动关闭方)检测到ACK丢失需要的时间为1个MSL(此时对方会重发FIN),然后对方发的FIN到达自己这边需要1个MSL。加起来就是2个MSL。
TIME_WAIT状态会使得一个连接在2个MSL的时间内,不能重新使用。连接是指一个4元祖(源ip、源端口、目的ip、目的端口)。
服务器断开连接示例:
- 第一次执行
sock -v -s 6666
后,服务器对6666端口进行了listen监听。 - 然后服务器被中断,自然会断开连接。
- 第二次执行
sock -v -s 6666
时,提示地址已经被占用。其实是指4元祖(源ip(本机的ipv4地址,192.168.10.144
),6666(即命令行给的参数)、通配ip、通配端口)。 - 通过netstat可以看到,现在有一个4元祖符合上面说的4元祖,且处于TIME_WAIT状态,所以才会提示地址已经被占用。
- 等到过了2个MSL后,就可以执行
sock -v -s 6666
了。
客户端断开连接示例:
- 客户端上被键盘中断了。
- 客户端加了
-b 2091
参数,想重新以上一次使用的随机端口2091来重新开始这个连接。提示地址已经被占用。 - 等到过了2个MSL后,就可以用端口2091来重新开始这个连接了。
通过添加参数-A
,可以无视重新开始的连接是处于TIME_WAIT状态下的。(这条在linux上可以,在Windows上不行)
FIN_WAIT_2状态
理论上,4次挥手中,在关闭一半连接以后:
- 主动关闭方会处于FIN_WAIT_2状态。
- 被动关闭方会处于CLOSE_WAIT状态。
如果主动关闭方一直等不到对方的FIN,那么可能会一直处于FIN_WAIT_2状态。当执行的是完全关闭操作时(比如close函数就是完全关闭操作,而shutdown函数则提供半关闭操作),就会设置一个计时器,超时后就会转移到CLOSED状态。
RST重置报文段
当发现到达的报文段对于相关连接是不正确的时,TCP就会发送一个重置报文段RST。RST会导致TCP连接的快速拆卸。
针对不存在端口的连接请求
- 在UDP中,当一个数据报到达一个不能使用的目的端口时,会生成一个ICMP目的地不可达的消息。
- 在TCP中,则生成一个RST。
- 通过telnet连接了本地ip的9999端口,但这个端口不存在。
- (第一个数据)客户端执行三次握手的第一次,向9999端口发送SYN包。初始序列号为819。
- (第二个数据)对方回复了RST+ACK。
- RST代表这个端口不存在。
- ACK置位代表确认了对方的SYN,Ack序号为820(为819+1),代表确认了SYN收到。
终止一条连接
- 正常断开连接,发送FIN,称为有序释放。
- 也可以发送RST,称为终止释放。
终止释放有两大特性:
- 任何排队要发送的数据都将被抛弃,一个RST立即发出去。
- 接收到RST的那一方不会作出任何响应,它会终止连接然后通知应用程序连接被重置。
通过将“逗留于关闭”套接字选项(SO_LINGER)的数值设置为0来实现 终止释放。
其中一方崩溃后的半开连接
TCP建立连接后,一方(可以是客户端)突然主机崩溃,此时不会通知给另一方(可以是服务器)信息。这样,就产生了半开连接。而且,另一方只要不传输数据,就永远不会发现(使用keepalive选项,则可以发现)。
当发送崩溃后,没有发生崩溃的一方依然认为这条TCP连接有效,那么此时它传输一些数据。而数据到了 发生崩溃的一方,发生崩溃的一方就会一脸懵逼,然后发送RST来重置连接。
时间等待错误(TIME_WAIT状态时收到RST)
在TIME_WAIT状态中,需要等够2MSL的时间。但在这段时间内,主动关闭方如果收到一些报文段比如RST,就会提前结束掉TIME_WAIT而到达CLOSED。
上图,TIME_WAIT状态中,收到了一个旧数据包Seq = L -100, Ack = K -200
,然后客户端又返回了一个最新序号Ack = L
的包。但由于此时服务器处于CLOSED状态,所以会返回一个RST。这个RST会导致主动关闭方提前进入CLOSED状态,这导致了问题。
从上图可以看到,2MSL比RTT(即一个来回)时间长了太多,这是问题的根源。主动关闭方的时间轴显然更长。
许多系统会规定在TIME_WAIT状态中,忽略RST,这就避免了问题。
TCP服务器选项
大多TCP服务器是并发的,当一个新的连接请求到达服务器时,服务器接收该连接,并调用一个新的进程或线程来处理新的客户端。
TCP端口号
现在有一个拥有IPv4和IPv6双协议栈的主机,上面运行了sshd即安全外壳服务器。
netstat命令参数:
-a
报告所有的网络节点,包括处于LISTEN或未处于LISTEN的节点。-n
以点分十进制来打印ip地址,而不会用DNS把ip地址转换为域名。此外,该选项还会打印端口号。-t
只选择TCP节点。
节点解析:
:::22
代表服务器这边,使用的是通配符地址(如果服务器是多宿主主机,那么这里可能是不同的ip地址),和本地端口22。:::*
代表的是可以接受的客户端请求:通配符地址,通配符端口。
此时服务器有两个连接,一个监听的TCP套接字,一个已建立的TCP套接字。注意到,这两个套接字,服务器这边的端口号都是22,但ESTABLISHED的套接字的ip地址却是一个确定的ip地址。
如果服务器只有一个网络接口,那么就应该如上图所示。两个ESTABLISHED的套接字,服务器这边的二元组都是一样的。因为是同一台主机发来的客户端请求,所以ip地址一样;由于每次连接都是使用随机端口,所以端口不一样。
TCP使用4元祖的多路分解来获得报文段。处于LISTEN的套接字才能接收SYN报文,处于ESTABLISHED的套接字才能接收非SYN的正常数据。而上图有两个ESTABLISHED的套接字,那么通过数据的源ip和源端口来继续分解。
如上图,如果服务器是多宿主的,那么服务器这边的ip地址不止会有一个。
限制本地IP地址
sock命令使用-s
参数时,如果后面跟了ip地址,那么就指定了服务器的某个ip地址,而不能泛化到多宿主的不同ip地址。
限制外部节点
UDP服务器不仅能够指定本地ip地址和本地端口(这在上一章就试过了),也可以指定客户端的ip地址和端口。但普通的伯克利套接字API没有提供这样的方法。
进入连接队列
服务器之所以能够并行运行,是因为:服务器会为每个客户端分配一个新的进程或线程,这样负责监听的服务器总是在准备着处理下一个连接请求。
在服务器中,一个新的连接在被应用程序接受之前,有两种状态:
- 处于SYN_RCVD状态。(三次握手还未完成)
- 处于ESTABLISHED状态。(三次握手已经完成,但未被应用程序接受,可能是指没来得及分配给新进程)它们被称为backlog。
这两种状态的新连接对应到两个不同的队列里。
在linux中,适用一下规则:
- 当收到一个SYN时,会检查第一个队列的大小,超过大小则拒绝连接(但不会是发送RST给客户端,而是不做操作)。队列大小对应到
net.ipv4.tcp_max_syn_backlog
这个数值。 - 每一个LISTEN状态的节点都有一个队列来处理上面第二种状态的新连接。队列大小对应到
net.core.somaxconn
这个数值。 - 三次握手中,客户端发送ACK后,就进入了ESTABLISHED状态,但此时服务器还没有进入ESTABLISHED状态,又或者这个新连接处于ESTABLISHED状态但未被应用程序接受。但此时,客户端可能就会发送数据出来。所以,服务器的TCP模块会把这些数据暂时存储起来。
与TCP连接管理相关的攻击
SYN泛洪
SYN泛洪是一种TCP拒绝服务攻击,在这种攻击中,服务器会收到很多SYN(第一次握手),但不会收到客户端的ACK(第三次握手)。但服务器会为每个SYN_RCVD状态的连接分配资源,这种半打开连接太多会耗尽服务器资源。
在RFC 4987中,有这句话:
Per RFC 793, when a SYN is received for a local TCP port where connection is in the LISTEN state, then the state transitions to SYN-RECEIVED, and some of the TCB is initialized with information from the header fields of the received SYN segment.
上面说到,监听套接字会根据客户端的SYN包里的头部字段来分配资源。现在,服务器不再分配资源,转而把这些 SYN包里的头部字段 编码进入ISN(s)
,到三次握手完成时,客户端自然会把ISN(s)+1
发过来,然后服务器就可以根据ISN(s)
反推出 第一次握手的SYN包里的头部字段。这种抵御攻击的方法称为SYN cookies。
过小的MTU
攻击者伪造一个ICMP PTB消息(package too big),该消息包含一个非常小的MTU值。收到这个ICMP PTB消息的那一端,只能以非常小的数据来填充payload,从而大大降低性能。
可以禁用主机的MTU发现功能,当发现ICMP PTB消息的下一跳MTU小于576字节时。
TCP连接劫持
只要知道连接双方的序列号,就能劫持。
如上图,在攻击者攻击后,只要B给A发送一个包,A都认为这个包已经接受过了,会抛弃掉,这样,A那边的滑动窗口就永远不会往前移动了。除非,在流水线工作下,B发送了一个seq=b, len=100
和一个seq=b+100, len=z
,那么才能使得滑动窗口移动,但即使这种情况下,也会导致A接受到一段错误的字节。(这只是我个人理解的简单劫持原理,如有出入请告知)。但攻击者由于知道双方期望的序列号是多少,所以攻击者可以轻松使得双方继续接受错误的字节流。