在TCP连接建立的过程中,客户端发送的SYN包会包含一个初始序号(ISN),这个ISN是随机生成的,而不是固定从0开始。
服务器端接收到SYN后,会回复一个SYN+ACK包,其中ACK部分会确认客户端的ISN加1,同时服务器也会设置自己的ISN,这个ISN同样是随机生成的。
这里ISN是随机的,那么随机有没有什么规则呢?
ISN(Initial Sequence Number,初始序列号)的随机化遵循一定的安全原则,以确保其难以被预测,从而提升TCP连接的安全性。
RFC 793最初建议了一个基于时间的计数器方法来生成ISN,但这种方法后来被认为是不够安全的,因为它可能允许攻击者预测未来的序列号。因此,现代实现倾向于使用更安全的策略。
RFC 1948介绍了一种推荐的方法来生成ISN,该方法结合了时间因素和随机数,确保每次系统启动时ISN的初始值都是不可预测的。具体算法包括以下几个关键点:
时间戳: 使用当前时间的一个表示作为基础,这有助于确保即使系统重启,序列号也不会重复。
PID或其它标识: 可能会加入进程ID或其他系统特定的标识符,以增加唯一性。
随机数: 添加一个真正的随机数组件,进一步增加不可预测性。
不同的操作系统和TCP/IP栈实现可能会有不同的具体策略来生成ISN,但核心思想都是确保其具有高度的随机性和不可预测性。
常用的ISN随机生成算法
ISN=M+F(localhost,localport,remotehost,remoteport)
其中:
M 是一个计时器的值,这个计时器每隔一定时间(通常是4毫秒)增加1。这样的设计让即使重启的系统,其ISN也会基于当前时间有一个不同的起始值,增加了序列号的不可预测性。
F 是一个散列函数(Hash Function),它根据源IP地址(localhost)、源端口号(localport)、目的IP地址(remotehost)和目的端口号(remoteport)这四个参数生成一个伪随机数。理想情况下,F应该是一个安全的哈希函数,如MD5或SHA系列,以确保生成的数值足够随机且难以被外部实体推算。通过结合这些连接特有的参数,可以确保即使对于相同的源和目标,每次连接尝试也会产生不同的ISN。
那么问题来了:为什么ISN要随机而不是采用固定的从0开始?
一是防止被预测乱用。
如果ISN初始化序列号是固定的,攻击者可以轻易预测后续的数据包序列号,这使得他们能够执行序号预测攻击(sequence number prediction attacks),比如重放攻击(replay attacks)。在这种攻击中,黑客可以捕获并重新发送以前的有效数据包,因为他们知道序列号的模式,可能导致接收方误以为是新的有效数据。
二是防止接收上一个相同四元组的历史报文。
我们都知道四次挥手的时候,会有一个TIME_WAIT状态持续2MSL,如果都是正常四次挥手关闭连接,那么新的连接肯定不会和上一个相同四元组之间有瓜葛。因为如果能正常四次挥手,由于 TIME_WAIT 状态会持续 2 MSL 时长,历史报文会在下一个连接之前就会自然消失。
问题是:我们不能保证每个连接都正常四次挥手关闭。
我们看一个例子:
假设A连接建立后第4步发送数据的时候网络出现了故障。
服务端这个时候发生了重启。服务端发送FIN到客户端,客户端收到后回复ACK,然后会立马收到一个RST报文,然后客户端断开连接。
客户端连接建立和A连接相同四元组的连接。重复上面的123步骤后,假设服务端的接收窗口是100,那么服务端此时接收的数据包序列号范围为:1到101,此时上一个A连接的第4步的数据发送过来了,那么服务端就接收了历史连接的数据。
再看一个已故的技术大神左耳朵耗子举的例子:
ISN是不能hard code的,不然会出问题的——比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是 client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client的Sequence Number 可能是3,而Server端认为client端的这个号是30了。全乱了。(我觉得这个举例情形还是很难出现。因为网络断了,client重连的话因为上一个连接的端口还没释放,其实这个新连接不太可能是相同的四元组,不过如果开启了 net.ipv4.tcp_tw_reuse 内核参数有一定的可能。关于这个例子大家有什么见解说一下,毕竟是技术大神的例子,我个人可能想不到。)
所以不能采用固定的初始化值。
那么初始化序列号随机了,万一两次随机的序列号一样呢?
这里我采用技术大神左耳朵耗子的解释:
RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(缩写为MSL – Wikipedia语条),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。
只要MSL的值小于4.55小时,那么我们就不会重用到ISN,不会发生两次随机的序列号一样的情况。
那么现在可以认为:两次不同的三次握手随机的序列号肯定会不一样,那么是否就可以完全避免接收到历史报文了?
答案可能让人失望了,不是的。
我们知道TCP序列号可能会发生回绕。
现在我们假设一种情形:
A连接建立后,初始化号随机是100,然后不断地发送数据,在发送seq = 20000的时候,网络出现了故障。
此时一个新的相同四元组连接建立,初始化序列号按照算法随机到了20000,B连接建立。这个时候A连接因为网络延迟seq = 20000的包到了,坏了啊,这么看怎么又收到了历史数据报文了?
这个时候就要用到TCP时间戳了。
防回绕序列号算法要求连接双方维护最近一次收到的数据包的时间戳(Recent TSval),每收到一个新数据包都会读取数据包中的时间戳值跟 Recent TSval 值做比较,如果发现收到的数据包中时间戳不是递增的,则表示该数据包是过期的,就会直接丢弃这个数据包。
关于TCP时间戳:拆解大厂面试题(校招):TCP报文中的时间戳有什么作用?这篇文章有更详细的说明。
总结 :
TCP通过初始化序列号随机+TCP时间戳两个机制来确保不会收到历史报文!
这里看似只有一道面试题,其实涉及的知识点很多:
TCP三次握手初始化序列号随机,为什么要随机?随机的算法是什么?
TCP时间戳
TCP如何避免收到历史连接报文?