上一篇关于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包。
最基本的三次握手
为了建立连接,通信的双方都需要各发送一个SYN包,同时从对方接收一个ACK包。
从理论上讲,每一方都要一去一回,总共需要发送4个控制消息来完成连接的建立。
通常情况是,服务端在监听某个端口,客户端发送请求连接消息,然后服务端进行回应。由于服务端也需要向客户端发送SYN包,因此可以将SYN包和对客户端的ACK合并到同一个包,从而减少一次交互。
这就是TCP的三次握手。
握手过程
三次握手的一个非常重要的目的,就是通信的双方要相互通知对方自己的初始化序号,以便后续双方可以基于该序号发送和拼接数据,防止包乱序。
这个通知初始化序号的过程,称为SYN(Synchronize Sequence Numbers)。
三次握手的流程如下图所示:
首先是服务端监听一个TCP端口,然后客户端发起连接,进行三次握手:
- 客户端向服务端发送SYN包,携带自己初始化序号为x;(TCP Flags中的SYN标识设置为1)
- 服务端向客户端发送SYN+ACK包,携带自己初始化序号为y,和对客户端的确认序号为x+1;(TCP Flags中的SYN标识、ACK标志设置为1)
- 客户端向服务端发送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个包[SYN] Seq=0,表示这是一个SYN包,192.168.31.163的初始化序号为0;
- 第2个包[SYN, ACK] Seq=0 Ack=1,表示这是一个SYN+ACK包,192.168.31.131的初始化序号为0,对192.168.31.163的确认序号为1;
- 第3个包[ACK],表示这是一个ACK包,192.168.31.163对192.168.31.131的确认序号为1。
如果我们对TCP的序号不太了解,每次看到三次握手的初始化序号为0,会误以为这就是其真实的初始化序号,而且每次都相同。
为了看到真实的序号,可以通过修改Wireshark的配置:Edit > Preferences > Protocols > TCP 的Relative sequence numbers选项,把打勾去掉,来显示绝对序号。效果如下:
关于序号
这里,引用一张精美的TCP首部结构图:
最开始的两部分,分别是源端口号(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。
当建立一个新的连接时:
- 随机生成一个初始序号ISN;
- 并填充到首部的序号中;
- TCP首部Flags中的SYN标志设置为1;
- 发送SYN包开始握手建立连接。
以上是初始化序号生成,并通知对端主机的简化过程。
TCP劫持
如果攻击者可以获得初始化序号,那么将可以伪造包。
猜测ISN值
入侵者通过请求应答的方法,监听不同时刻的ISN值,得到一个离散的ISN值和时间的对照表,例如:
序号:ISN0 ISN1 ISN2 ...
时间:t1 t2 t3 ...
有了时间和ISN值的对照表,就可以很容易的通过某种数学方法,来推导出依赖于时间t的ISN生成函数。
而使用这个公式,攻击者就可以根据时间间隔预测下一个可能的ISN值。
例如下面的一个测试结果,ISN与时间形成一个线性关系:
(图片:www.techrepublic.com/article/tcp… )
问题
为什么握手是三次?
在这篇文章《TCP的滑动窗口机制,谈谈其设计演化过程(如何从无到有?从不可靠到可靠?)》中我们讲到,TCP为了保证其消息的可靠性,需要有ACK确认机制。
因此,对于每一个SYN包,至少要对应一次确认包。
如果减少为两次握手,那么服务端将收不到客户端对其SYN包的确认,无法确认客户端是否收到其SYN+ACK包。
如果增加握手次数,如四次,那么又增加了握手的通信次数,效率会降低。而且,TCP本来为了减少连接建立的通信次数,就已经将服务端的SYN和ACK进行了合并。
当然,你可能还会想到,如果服务端收不到客户端的最后一个ACK呢?那服务端是不是最好再回一个ACK,让客户端确认服务端确实收到了。其实是可以的,但是却没有必要。因为TCP一方面有重试机制,另一方面建立连接后通常会发送数据,是可以保证在实际传输数据之前正常建立连接的。
因此,三次握手对于TCP来说是比较合理的选择。
为什么初始化序号不从1开始?
或许你会觉得,TCP的初始化序号为什么搞得这么复杂,不能直接从1开始吗?
让每个连接都从序号1开始,可能会产生一个问题,就是让多个连接的数据报有可能产生互串。
假设主机S和主机D建立了连接,S向D发了一些数据包。如果因为某些原因,导致连接断开了。然后,S和D重新建立连接,初始化序号为1。这时,上一次发送的数据包,有可能由于网络拥堵等原因,导致包延迟到了本次新连接才到达,D收到这些包,会误认为是新连接S发过来的包。
因此,TCP在建立连接时,采用计数器或随机生成的方式,保证短时间内不会产生冲突,以避免此类问题。
www.tcpipguide.com/free/t_TCPC…
www.tcpipguide.com/free/t_TCPC…
www.techrepublic.com/article/tcp…
《TCP/IP详解 卷1:协议》
个人公众号
更多文章,请关注公众号:二进制之路