理解TCP三次握手,为什么是三次?兼谈初始化序号、发送和确认序号

上一篇关于TCP的文章,我们讲了TCP是如何一步步设计,来保证其消息发送的可靠性。参见:《TCP的滑动窗口机制,谈谈其设计演化过程(如何从无到有?从不可靠到可靠?)

这一节,我们就从TCP的三次握手开始,了解一下序号在TCP传输中的具体使用细节。

TCP三次握手

对于TCP通信的双方,在进行数据传输之前,需要建立一个连接。

这个连接其实是虚拟的,主要由一对套接字对,或者叫四元组来标识,即源IP、源端口号、目标IP和目标端口号。其实,准确来说,还要加上个协议类型TCP。

通过这个连接过程,可以完成几件事情:

  • 服务端了解客户端的信息,知道与谁建立连接;
  • 对于每一个新的连接,通信的双方都要互相告知自己的初始化序号ISN,以便用于接下来的第一次数据传输;
  • 双方互相交换一些控制信息,用于控制后续的交互操作应该如何进行;

TCP标志位

对于TCP的三次握手,会涉及到2个标志位,分别是SYN标志和ACK标志。

SYN标志(Synchronize Flag),字段长度为1位。如果设置为1,表示初始化一个新的连接,并同步自己的初始化序号(ISN,Initial Sequence Number)。

ACK(Acknowledgement Flag),字段长度为1位。如果设置为1,表示首部的确认序号有效。用于接收方在接收到报文段之后,对已接收的数据进行确认。

最基本的三次握手

为了建立连接,通信的双方都需要各发送一个SYN包,同时从对方接收一个ACK包。

从理论上讲,每一方都要一去一回,总共需要发送4个控制消息来完成连接的建立。

通常情况是,服务端在监听某个端口,客户端发送请求连接消息,然后服务端进行回应。由于服务端也需要向客户端发送SYN包,因此可以将SYN包和对客户端的ACK合并到同一个包,从而减少一次交互。

这就是TCP的三次握手。

握手过程

三次握手的一个非常重要的目的,就是通信的双方要相互通知对方自己的初始化序号,以便后续双方可以基于该序号发送和拼接数据,防止包乱序。

这个通知初始化序号的过程,称为SYN(Synchronize Sequence Numbers)。

三次握手的流程如下图所示:

(图片:https://coolshell.cn/articles/11564.html)

首先是服务端监听一个TCP端口,然后客户端发起连接,进行三次握手:

  1. 客户端向服务端发送SYN包,携带自己初始化序号为x;(TCP Flags中的SYN标识设置为1)
  2. 服务端向客户端发送SYN+ACK包,携带自己初始化序号为y,和对客户端的确认序号为x+1;(TCP Flags中的SYN标识、ACK标志设置为1)
  3. 客户端向服务端发送ACK包,携带对服务端的确认序号为y+1。(TCP Flags中的ACK标识设置为1)

如果以上三步都成功,则客户端和服务端就建立了一条虚拟的连接,双方的连接状态都为ESTABLISHED。

此时,客户端、服务端的当前序号分别为x+1和y+1,分别作为双方下一个包的序号。

另外,从图中可以发现,客户端和服务端发送的FIN包,在对方ACK之后,序号也会加1。

抓包示例

从192.168.31.163 telnet 192.168.31.131,通过Wireshark抓取TCP三次握手的过程如下:

由上往下:

  1. 第1个包[SYN] Seq=0,表示这是一个SYN包,192.168.31.163的初始化序号为0;
  2. 第2个包[SYN, ACK] Seq=0 Ack=1,表示这是一个SYN+ACK包,192.168.31.131的初始化序号为0,对192.168.31.163的确认序号为1;
  3. 第3个包[ACK],表示这是一个ACK包,192.168.31.163对192.168.31.131的确认序号为1。

如果我们对TCP的序号不太了解,每次看到三次握手的初始化序号为0,会误以为这就是其真实的初始化序号,而且每次都相同。

为了看到真实的序号,可以通过修改Wireshark的配置:Edit > Preferences > Protocols > TCP 的Relative sequence numbers选项,把打勾去掉,来显示绝对序号。效果如下:

关于序号

这里,引用一张精美的TCP首部结构图:

(图片:https://hauptj.com/2018/10/08/fatpipe-org-ip-packet-header-drawings/)

最开始的两部分,分别是源端口号(Source Port)和目标端口号(Destination Port),各占16位。

接下来的,是(发送)序号(Sequence Number)和确认序号(Acknowledgement Number),以及中间的TCP标志位(TCP Flags,也叫控制位)。

序号(Sequence Number)

指发送报文段的序号,也可以理解为发送数据的位置,这个位置表示的是报文段中数据部分的第一个字节。

序号字段的长度为32位无符号数,数值范围为0~2^32 – 1(即4,294,967,295),当序号达到最大值时,会重新从0开始递增。

每发送一次数据,序号就累加一次数据字节数的大小。比如本次发送的序号为1,发送的数据大小为4个字节,则下一次发送数据的序号为1+4=5。

序号的主要作用:用于解决网络包的乱序问题。确保接收端能够按照正确的顺序接收并处理包数据。

确认序号(Acknowledgement Number)

确认序号的长度为32位无符号数,用于接收端向发送端确认已经接收的数据位置。

发送端在收到这个确认序号之后,可以认为在这个序号之前(确认序号值减去1)的数据都已经被正常接收。

既然每个传输的字节都被计数,确认序号包含发送确认的一端所期望收到的下一个序号。因此,确认序号应当是上次已成功收到数据字节序号加1。

只有TCP首部Flags中的ACK标志为1时,确认序号字段才有效。

确认序号的主要作用:用来解决不丢包问题。通知接收端已经接收了哪些数据。

初始化序号(ISN,Initial Sequence Number)

关于序号,我们前文已经谈了很多。

对于通信的双方,发送数据最开始的序号,是从初始化序号开始的。

初始化序号是在双方进行三次握手时,互相交换给对方的。

对于每一方,实际发送数据的第一个字节,序号为其ISN+1。

那么,初始化序号又是怎么生成的呢?

传统上

传统上,使用时钟计数器来选择初始化序号ISN。该计数器在TCP启动时初始化,然后每隔4微秒增加1。

每次建立一个连接时,从计数器获取当前值,作为本次连接的ISN。

计数器从0增加到最大值4,294,967,295(2^32 – 1),大约需要4.77小时。因此,可以保证不同的连接初始化序号不会产生冲突。

现在

使用计数器这种方法,虽然可以保证不同的连接初始化序号不会产生冲突,但却存在一个问题,即它使ISN变得可以预测,因此它是与时间相关的。

为了解决ISN的可预测问题,现在的实现一般是使用随机数来生成ISN。

当建立一个新的连接时:

  1. 随机生成一个初始序号ISN;
  2. 并填充到首部的序号中;
  3. TCP首部Flags中的SYN标志设置为1;
  4. 发送SYN包开始握手建立连接。

以上是初始化序号生成,并通知对端主机的简化过程。

TCP劫持

如果攻击者可以获得初始化序号,那么将可以伪造包。

猜测ISN值

入侵者通过请求应答的方法,监听不同时刻的ISN值,得到一个离散的ISN值和时间的对照表,例如:

序号:ISN0 ISN1 ISN2 …

时间:t1 t2 t3 …

有了时间和ISN值的对照表,就可以很容易的通过某种数学方法,来推导出依赖于时间t的ISN生成函数。

而使用这个公式,攻击者就可以根据时间间隔预测下一个可能的ISN值。

例如下面的一个测试结果,ISN与时间形成一个线性关系:

(图片:https://www.techrepublic.com/article/tcp-hijacking/ )

问题

为什么握手是三次?

在这篇文章《TCP的滑动窗口机制,谈谈其设计演化过程(如何从无到有?从不可靠到可靠?)》中我们讲到,TCP为了保证其消息的可靠性,需要有ACK确认机制。

因此,对于每一个发送包,需要对应一个确认包。

如果减少为两次握手,那么服务端将收不到客户端对其SYN包的确认,无法确认客户端是否收到其SYN+ACK包。

如果增加握手次数,如四次,那么又增加了握手的通信次数,效率会降低。而且,TCP本来为了减少连接建立的通信次数,就已经将服务端的SYN和ACK进行了合并。

当然,你可能还会想到,如果服务端收不到客户端的最后一个ACK呢?那服务端是不是最好再回一个ACK,让客户端确认服务端确实收到了。其实是可以的,但是却没有必要。因为TCP一方面有重试机制,另一方面建立连接后通常会发送数据,是可以保证在实际传输数据之前正常建立连接的。

因此,三次握手对于TCP来说是比较合理的选择。

为什么初始化序号不从1开始?

或许你会觉得,TCP的初始化序号为什么搞得这么复杂,不能直接从1开始吗?

让每个连接都从序号1开始,可能会产生一个问题,就是让多个连接的数据报有可能产生互串。

假设主机S和主机D建立了连接,S向D发了一些数据包。如果因为某些原因,导致连接断开了。然后,S和D重新建立连接,初始化序号为1。这时,上一次发送的数据包,有可能由于网络拥堵等原因,导致包延迟到了本次新连接才到达,D收到这些包,会误认为是新连接S发过来的包。

因此,TCP在建立连接时,采用计数器或随机生成的方式,保证短时间内不会产生冲突,以避免此类问题。


http://www.tcpipguide.com/free/t_TCPConnectionEstablishmentSequenceNumberSynchroniz.htm

http://www.tcpipguide.com/free/t_TCPConnectionEstablishmentProcessTheThreeWayHandsh.htm

https://hauptj.com/2018/10/08/fatpipe-org-ip-packet-header-drawings/

https://www.techrepublic.com/article/tcp-hijacking/

TCP 的那些事儿(上)

《TCP/IP详解 卷1:协议》


---转载本站文章请注明作者和出处 二进制之路(binarylife.icu),请勿用于任何商业用途---

留下评论