TCP窗口缩放、时间戳和SACK

2020-08-12 04:57:25

Linux TCP堆栈有无数允许更改其行为的sysctl旋钮。这包括可用于接收或发送操作的内存量、最大套接字数量以及可选功能和协议扩展。

由于各种“性能调优”或“安全”原因,建议禁用TCP扩展的文章很多,比如时间戳或选择性确认(SACK)。

本文提供了有关这些扩展的功能、为什么默认启用它们、它们如何相互关联以及为什么通常情况下关闭它们不是一个好主意的背景知识。

TCP能够支持的数据传输速率受到几个因素的限制。其中一些是:

往返时间(RTT)。这是数据包到达目的地和回复返回所需的时间。越低越好。

新数据可用于传输的速度例如,CPU需要能够足够快地将数据传递到网络适配器。如果CPU需要首先加密数据,适配器可能需要等待新数据。同样,如果磁盘存储不能足够快地读取数据,那么它可能会成为瓶颈。

TCP接收窗口的最大可能大小。接收窗口确定TCP在必须等待接收器报告接收到该数据之前可以传输多少数据(以字节为单位)。这是由接收者宣布的。接收器将在读取并确认接收到传入数据时不断更新此值。接收窗口当前值包含在TCP报头中,该报头是TCP发送的每个数据段的一部分。因此,只要发送方接收到来自对等方的确认,它就知道当前的接收窗口。这意味着往返时间越长,发件人获得接收窗口更新所需的时间就越长。

TCP限制为最多64千字节的未确认(传输中)数据。这甚至没有接近在大多数网络方案中维持良好数据速率所需的速率。让我们看看一些例子。

由于往返时间为100毫秒,TCP每秒最多可以传输640千字节。在延迟1秒的情况下,最大理论数据速率降至每秒64千字节。

这是因为接收窗口的原因。一旦发送了64K字节的数据,接收窗口就已经满了。发送者必须等待,直到对等体通知它应用程序已经读取了至少一部分数据。

发送的第一个数据段将TCP窗口减小该数据段的大小。在接收窗口值的更新变为可用之前,需要一次往返。如果更新到达时延迟1秒,即使链路有足够的可用带宽,也会导致64 KB的限制。

为了充分利用具有几毫秒延迟的快速网络,必须使用大于传统TCP支持的窗口大小。“64千字节限制”是协议规范的产物:TCP报头仅为接收窗口大小保留16位。这允许接收高达64K字节的窗口。最初设计TCP协议时,此大小不被视为限制。

遗憾的是,仅更改TCP报头以支持更大的最大窗口值是不可能的。这样做将意味着TCP的所有实现都必须同时更新,否则它们将无法再相互理解。为解决此问题,改为更改接收窗口值的解释。

“窗口缩放选项”允许这样做,同时保持与现有实现的兼容性。

TCP支持可选扩展。这允许使用新功能增强协议,而无需一次更新所有实现。当TCP发起方连接到对等方时,它还会发送支持的分机列表。所有扩展都遵循相同的格式:唯一的选项编号,后跟选项的长度和选项数据本身。

TCP响应器检查连接请求中包含的所有选项编号。如果它不理解选项编号,它会跳过数据的“长度”字节,并检查下一个选项编号。应答者在答复中省略了它不理解的内容。这使得发送方和接收方都可以了解受支持的公共选项集。

窗口比例选项(WSopt):种类:3,长度:3。+-+3|种类=3|长度=3|Shiftt.cnt|3|-+1:1:1:1。

窗口缩放选项告诉对等方,TCP报头中的接收窗口值应该按给定的数字进行缩放,以获得实际大小。

例如,宣布窗口缩放系数为7的tcp发起方试图指示响应方,任何携带接收窗口值512时的未来数据包实际上都会通告65536字节的窗口。这是增加了128倍。这将允许最大TCP窗口为8兆字节。

不理解此选项的TCP响应方会忽略它。则回复连接请求而发送的TCP分组(SYN-ACK)不包含窗口比例选项。在这种情况下,双方只能使用64k大小的窗口。幸运的是,几乎每个TCP堆栈都默认支持并启用此选项,包括Linux。

应答器包括其自己所需的比例因子。两个对等点可以使用不同的号码。宣布比例因子为0也是合法的。这意味着对等点应该逐字处理它接收到的接收窗口值,但它允许回复方向上的缩放值-然后接收方可以使用更大的接收窗口。

与SACK或TCP时间戳不同,窗口缩放选项仅出现在TCP连接的前两个数据包中,之后无法更改。也不可能通过查看不包含初始连接三次握手的连接的数据包捕获来确定比例因子。

支持的最大比例因子为14。这允许TCP窗口大小最大为1 GB。

在非常特殊的情况下,它可能会导致数据损坏。在禁用该选项之前-这在正常情况下是不可能的。也有一个解决方案可以防止这种情况发生。不幸的是,有些人在没有意识到与窗口缩放的关系的情况下禁用了该解决方案。首先,让我们来看看需要解决的实际问题。想象一下以下一系列事件:

发送者认为s_2丢失,并再次发送它。它还发送包含在段s_n+1中的新数据。

例如,当发送方过早触发重传时,可能会发生这种情况。即使在窗口缩放的情况下,这种错误的重传在正常情况下也不是问题。接收器将直接丢弃副本。

TCP序列号最多可以是4千兆字节。如果它变得大于这个值,则序列回绕回0,然后再次递增。这本身不是问题,但是如果这发生得足够快,那么上面的场景可能会造成歧义。

如果在正确的时刻发生回绕,则序列号s_2(重发的分组)可能已经大于s_n+1。因此,在最后的步骤(4)中,接收器可以将其解释为:s_2,s_n+1,s_n+m,即,它可以将‘旧’分组s_2视为包含新数据。

通常情况下,这种情况不会发生,因为即使在高带宽链路上,“回绕”也只会每隔几秒或几分钟发生一次。原始和不需要的重传之间的间隔将小得多。

例如,在传输速度为每秒50兆字节的情况下,副本需要延迟超过一分钟才会出现问题。序列号的换行速度不够快,导致较小的延迟导致此问题。

一旦TCP接近“每秒千兆字节”的吞吐速率,序列号就会包装得如此之快,以至于即使只有几毫秒的延迟也会产生TCP无法再检测到的重复项。通过解决接收窗口太小的问题,TCP现在可以用于以前不可能达到的网络速度-这就产生了一个新的问题,尽管这种问题很少见。为了在RTT非常低的环境中安全地使用千兆字节/秒的速度,接收器必须能够检测到这样的旧副本,而不是仅依赖序列号。

最简单地说,TCP时间戳只是向数据包添加一个时间戳,以解决由于非常快的序列号回绕而引起的歧义。如果数据段似乎包含新数据,但其时间戳比窗口内的最后一个数据包旧,则序列号已包装,而“新”数据包实际上是较旧的副本。这解决了重传的模糊性,即使对于极端的转角情况也是如此。

但是该扩展允许的不仅仅是检测旧分组。TCP时间戳实现的另一个主要功能是更精确的往返时间测量(RTTm)。

当两个对等点都支持时间戳时,每个TCP数据段都会额外携带两个数字:时间戳值和时间戳回送。

TCP时间戳选项(TSopt):种类:8,长度:10+-+----+|KIND=8|10|TS值(TSval)|EchoReply(TSecr)|+-+----+:1%。1:00-4:00,11:00-4:00。

准确的RTT估计对于TCP性能至关重要。TCP自动重新发送未确认的数据。重新传输由计时器触发:如果超时,TCP认为尚未收到确认的一个或多个数据包丢失。然后,它们被再次发送。

但“尚未确认”并不意味着数据段丢失。也有可能是接收方到目前为止没有发送确认,或者确认仍在传输中。这造成了一个两难境地:TCP必须等待足够长的时间,才能让这种轻微的延迟无关紧要,但它也不能等待太长时间。

在延迟较高的网络中,如果计时器触发过快,TCP经常会将时间和带宽浪费在不必要的重发上。

然而,在延迟较低的网络中,等待太长时间会在发生真正的数据包丢失时导致吞吐量降低。因此,低延迟网络中的计时器应该比高延迟网络中的计时器更早到期。因此,TCP重传超时不能使用固定常量值作为超时。它需要根据其在网络中经历的延迟来调整该值。

TCP根据预期往返时间(RTT)选择重传超时。RTT事先是未知的。RTT是通过测量发送数据段的时间和TCP接收该数据段所携带数据的确认之间的增量来估计的。

出于性能原因,TCP不会为其收到的每个数据包生成新确认。它等待的时间非常短:如果更多的数据段到达,则可以用单个ACK数据包确认它们的接收。这称为“累积确认”。

往返时间不是恒定的。这是由多种因素造成的。例如,客户端可能是移动时切换到不同基站的移动电话。当链路或CPU利用率增加时,分组交换也可能需要更长的时间。

必须在计算期间忽略必须重新发送的分组。这是因为发送器无法知道重传数据段的ACK是确认原始传输(最终到达)还是重传。

最后一点很重要:当TCP忙于从丢失中恢复时,它可能只接收重新传输的数据段的ACK。因此,它不能在此恢复阶段测量(更新)RTT。因此,它不能调整重传超时,然后重传超时将以指数级增长。这是一个非常具体的情况(它假设快速重传或SACK等其他机制没有帮助)。然而,使用TCP时间戳,即使在这种情况下也会进行RTT评估。

如果使用扩展,对等设备将从TCP段扩展空间读取时间戳值并将其存储在本地。然后,它将该值放入它作为“时间戳回送”发回的所有数据段中。

因此,该选项带有两个时间戳:它的发送者拥有自己的时间戳,以及它从对等点接收到的最新时间戳。原始发送方使用“回应时间戳”来计算RTT。它是它当前的时间戳时钟和“时间戳回声”中所反映的之间的增量。

除了PAWS和RTT测量之外,TCP时间戳甚至还有其他用途。例如,可以检测是否不需要重发。如果确认携带较旧的时间戳回声,则确认针对的是初始数据包,而不是重新传输的数据包。

TCP时间戳的另一个更模糊的用例与TCP syn cookie功能相关。

当连接请求到达的速度超过服务器应用程序接受新传入连接的速度时,连接积压最终将达到其极限。这可能是由于系统配置错误或应用程序中的错误造成的。当一个或多个客户端发送连接请求而不响应‘syn ack’响应时,也会发生这种情况。这会用不完整的连接填充连接队列。这些条目需要几秒钟才会超时。这被称为“SYN洪水攻击”。

即使队列已满,一些TCP堆栈也允许接受新连接。发生这种情况时,Linux内核将在系统日志中打印一条突出的消息:

此机制完全绕过连接队列。通常存储在连接队列中的信息被编码到SYN/ACK响应TCP序列号中。当ACK返回时,可以从序列号重建队列条目。

序列号只有有限的空间来存储信息。因此,使用‘TCP syn cookie’机制建立的连接不支持TCP选项。

但是,两个对等体共有的TCP选项可以存储在时间戳中。ACK数据包将该值反映回时间戳回送字段中,该字段还允许恢复商定的TCP选项。否则,Cookie连接受标准64kbyte接收窗口的限制。

不幸的是,一些指南建议禁用TCP时间戳,以减少内核访问时间戳时钟以获取当前时间所需的次数。这是不对的。如前所述,RTT估计是TCP的必要部分。因此,内核在接收/发送数据包时总是采用微秒级的时间戳。

Linux在数据包处理步骤的其余部分重用RTT估计所用的时钟时间戳。这还避免了将时间戳添加到传出TCP数据包的额外时钟访问。

整个时间戳选项在每个数据包中只需要10字节的TCP选项空间,这并没有显著减少可用于数据包有效负载的空间。

一些安全审计工具和(较旧的)博客文章建议禁用TCP时间戳,因为据称它们泄露了系统正常运行时间:这将允许估计系统/内核的补丁级别。这在过去是正确的:时间戳时钟基于在每次系统引导时从固定值开始的不断增加的值。时间戳值将估计机器已经运行了多长时间(正常运行时间)。

从Linux4.12开始,TCP时间戳不再显示正常运行时间。发送的所有时间戳值都使用对等设备特定的偏移量。时间戳值也每49天换行一次。

换句话说,从地址“A”到地址“A”的连接看到的时间戳与到远程地址“B”的连接看到的时间戳不同。

运行sysctl net.ipv4.tcp_timeStamp=2以禁用随机化偏移。这使得分析由Wireshark或tcpdump等工具记录的数据包跟踪更容易-从主机发送的数据包在其TCP选项时间戳中都具有相同的时钟基数。对于正常操作,默认设置应保持不变。

如果同一数据窗口中的多个数据包丢失,则TCP会出现问题。这是因为TCP确认是累积的,但仅针对按顺序到达的数据包。示例:

这意味着S_1和S_2都被接收,并且发送方不再需要保留这些数据段。

发送器等待s_2到达的“重传超时”或“重复ACK”。如果发生重传超时或s_2的几个重复ACK到达,则发送器再次发送s_3。

如果发送方接收到对s_n的确认,则s_3是唯一丢失的分组。这是最理想的情况。只有一个丢失的分组被重发。

如果发送器接收到小于s_n的确认数据段,例如s_4,则意味着丢失了多个数据包。发送方还需要重新传输下一个数据段。

可以只重复相同的序列:重新发送下一个分组,直到接收方指示它已经处理了直到S_n的所有分组。这种方法的问题在于,它需要一个RTT,直到发送方知道它下一次必须重新发送哪个分组。虽然这样的策略避免了不必要的重新传输,但在TCP重新发送整个数据窗口之前,可能需要几秒钟甚至更长时间。

另一种方法是一次重新发送几个数据包。此方法允许TCP在丢失多个数据包时更快地恢复。在上面的示例中,TCP重新发送s_3、s_4、s_5、..。而只能确定S3已经丢失。

从延迟的角度来看,这两种策略都不是最优的。第一种策略在只需要重发单个数据包的情况下速度很快,但在丢失多个数据包时耗时太长。

第二种算法即使需要重发多个数据包也很快,但代价是浪费带宽。此外,这样的TCP发送器在进行不需要的重传时可能已经传输了新数据。

有了可用的信息,TCP就不能知道哪些分组丢失了。这就是TCP选择性确认(SACK)的用武之地。就像窗口缩放和时间戳一样,它是另一个可选的、但非常有用的TCP功能。

支持此扩展的发送方在连接请求中包括“允许SACK”选项。如果两个端点都支持扩展,则检测到数据流中缺少数据包的对等点可以通知发送方这一点。

TCP SACK选项:种类:5,长度:可变的第一块左边缘|+-+-+。-。。。*-+-+。-+|第n个区块的右边缘|+-+

遇到…_s2后跟s_5段的接收器。S_n,则它在发送针对s_2的确认时将包括SACK块:

这告诉发送方直到s_2的段是按顺序到达的,但它也让发送方知道段s_5到s_n也被接收到。然后,发送方可以重新发送这两个分组,并继续发送新数据。

理论上,如果连接不会丢失数据包,SACK不会提供任何优势。或者连接具有如此低的延迟,以至于即使等待一个完整的RTT也无关紧要。

在实践中,无损行为几乎是不可能保证的。即使网络及其所有交换机和路由器都有充足的带宽和缓冲区空间,数据包仍可能丢失:

主机操作系统可能在内存压力下丢弃数据包。请记住,一台主机可能同时处理数万个数据包流。

CPU可能无法足够快地从网络接口排出传入的数据包。

.