1981年,Jon Postel在RFC 793中定义了TCP的核心机制,其中累积确认(Cumulative Acknowledgment)的设计在当时看来简洁而优雅:接收方只需报告"我已经收到序列号N之前的所有数据"。这种设计在单包丢失场景下工作良好,但当网络中出现多个数据包丢失时,它暴露出了根本性的信息缺失问题——发送方根本不知道哪些数据已经成功到达,哪些需要重传。
这个问题困扰了互联网十五年,直到1996年RFC 2018正式定义了TCP选择性确认(Selective Acknowledgment,SACK)选项。SACK的引入标志着TCP从"盲猜"时代进入了"精确打击"时代,它让接收方能够精确报告已接收的非连续数据块,彻底改变了TCP在多包丢失场景下的行为模式。
累积确认的根本性缺陷
理解SACK的价值,必须先理解累积确认的局限性。考虑一个典型的多包丢失场景:发送方连续发送了序列号1000-1999、2000-2999、3000-3999、4000-4999四个数据段,其中第二个(2000-2999)和第四个(4000-4999)丢失。
在累积确认机制下,接收方只能发送ACK=2000,表示"我期望收到的下一个字节是2000"。这个确认信息只告诉发送方"1000-1999已经收到",但对于3000-3999的命运,发送方一无所知。发送方面临一个困境:3000-3999是丢了,还是只是在排队?
Kevin Fall和Sally Floyd在他们的经典论文"Simulation-based Comparisons of Tahoe, Reno, and SACK TCP"中深刻指出,在没有SACK的情况下,TCP实现必须在两种策略之间做出选择:
策略一:每个往返时间(RTT)最多只重传一个丢失的数据包。这是Reno和NewReno的做法。优点是避免不必要的重传,缺点是恢复速度极慢——如果丢失了四个包,就需要四个RTT才能完全恢复。
策略二:重传所有未确认的数据包,即使它们可能已经成功到达。这是Tahoe的做法。优点是恢复速度快,缺点是可能大量重复发送已经成功传输的数据,浪费带宽。
这两种策略都没有真正解决问题,只是在两个糟糕的选项中做出权衡。问题的根源在于信息不对称——发送方缺乏接收方已经拥有的关键信息。
SACK选项的工作原理
SACK选项的核心思想是让接收方报告"我已经收到的数据块",而不仅仅是"我期望收到的下一个字节"。这看似简单的扩展,却需要精心设计的协议格式。
SACK选项格式
RFC 2018定义的SACK选项格式如下:
+--------+--------+
| Kind=5 | Length |
+--------+--------+--------+--------+
| Left Edge of 1st Block |
+--------+--------+--------+--------+
| Right Edge of 1st Block |
+--------+--------+--------+--------+
| ... |
+--------+--------+--------+--------+
| Left Edge of nth Block |
+--------+--------+--------+--------+
| Right Edge of nth Block |
+--------+--------+--------+--------+
每个SACK块占用8字节,包含左边界(Left Edge)和右边界(Right Edge)。左边界是该数据块的起始序列号,右边界是该数据块最后一个字节的序列号加一。由于TCP选项字段的总长度限制(最多40字节),一个SACK选项最多包含4个SACK块(当同时使用时间戳选项时,这个数字会减少到3个)。
回到之前的例子,当接收方收到1000-1999、3000-3999两个段时,它可以发送:
- ACK字段:2000(累积确认边界)
- SACK块:3000-4000(表示已收到3000-3999)
发送方收到这个确认后,立即知道:
- 1000-1999已收到
- 2000-2999丢失(需要重传)
- 3000-3999已收到
- 4000-4999状态未知
这种精确的信息让发送方能够做出明智的重传决策。
SACK协商过程
SACK功能需要在TCP三次握手期间协商启用。RFC 2018定义了两个TCP选项:
SACK-Permitted选项(Kind=4):仅在SYN段中使用,表示发送方支持SACK功能。该选项长度为2字节,不携带额外数据。
SACK选项(Kind=5):在数据传输阶段使用,携带实际的SACK块信息。
协商过程如下:
- 客户端发送SYN段,携带SACK-Permitted选项
- 服务端收到SYN后,如果支持SACK,则在SYN-ACK中也携带SACK-Permitted选项
- 双方都看到对方的SACK-Permitted选项后,连接建立,后续数据传输可以使用SACK选项
值得注意的是,SACK-Permitted选项是单向声明。如果只有一方发送SACK-Permitted,那么只有发送方能够接收SACK信息。真正的双向SACK需要双方都在SYN和SYN-ACK中携带该选项。
发送方的核心算法:Scoreboard与Pipe
SACK选项让接收方能够报告精确的接收状态,但如何利用这些信息进行高效的重传,则是发送方的责任。RFC 3517定义了一套完整的SACK-based丢失恢复算法,其核心是两个关键数据结构:Scoreboard和Pipe。
Scoreboard:数据接收状态记分牌
Scoreboard是一个数据结构,用于跟踪每个数据段的接收状态。在概念上,它可以表示为一个位图或区间集合,记录哪些序列号范围已被SACK确认。
Linux内核中的实现使用了一个称为tcp_sacktag_state的结构,它会处理收到的每个SACK块,更新相应的接收状态。当发送方收到一个带有SACK选项的ACK时,它会:
- 解析SACK选项中的所有SACK块
- 对于每个SACK块,标记相应的序列号范围为"已SACK"
- 更新内部的接收状态跟踪
这种设计允许发送方准确知道哪些数据已经被接收方缓存,哪些数据仍在"飞行中"或已丢失。
Pipe算法:精确估算网络负载
在Fast Recovery期间,发送方需要估算网络中有多少数据正在传输,以决定何时可以发送新数据或重传数据。传统的Reno使用dupACK计数来估算,但这种方法在多包丢失场景下不准确。
SACK引入了pipe变量来精确估算:
pipe初始化为0
每发送一个新数据包:pipe++
每重传一个数据包:pipe++
每收到一个报告新数据的SACK:pipe--
每收到一个partial ACK:pipe -= 2
发送条件变为:pipe < cwnd时允许发送。
这个算法的关键洞察是:每个dupACK(带有SACK信息)代表一个数据包已经离开网络并被接收方缓存。通过跟踪pipe变量,发送方能够精确控制网络中的数据量,避免"管道干涸"或"管道溢出"。
IsLost和NextSeg算法
RFC 3517定义了两个关键函数:
IsLost(seq):判断序列号seq的数据段是否已经丢失。判断标准是:如果收到三个连续的SACK块,且这些SACK块的序列号都高于seq,则认为seq已丢失。这个"三个dupACK"的标准与Fast Retransmit的触发条件一致。
NextSeg():决定下一个应该发送的数据段。优先级如下:
- 如果有已识别为丢失的数据段,优先重传
- 如果没有丢失的数据段,但pipe < cwnd,发送新数据
这个算法确保了:
- 丢失的数据段被尽快重传
- 在等待确认期间,充分利用可用带宽
New-Reno vs SACK:每RTT一包的瓶颈
在SACK普及之前,New-Reno是处理多包丢失的最佳方案。它通过识别partial ACK来改进Reno的行为,但它仍然受限于累积确认的本质。
New-Reno的核心改进是:当收到partial ACK(确认了部分但非全部的未确认数据)时,立即重传下一个被认为丢失的数据段,而不退出Fast Recovery。这避免了Reno在多包丢失时需要等待超时的问题。
然而,New-Reno有一个根本性限制:每个RTT只能重传一个丢失的数据段。这是因为partial ACK只能告诉发送方"重传的第一个包已经到达",但对其他丢失包的状态一无所知。发送方只能保守地假设最坏情况,每收到一个partial ACK就重传下一个可能丢失的包。
Fall和Floyd的模拟数据清晰地展示了这个差异。在一个拥塞窗口为15个包、丢失4个包的场景中:
| TCP变种 | 恢复方式 | 恢复时间 | 问题 |
|---|---|---|---|
| Tahoe | Slow-Start | 快但不精准 | 重传已成功到达的数据 |
| Reno | 超时等待 | 非常慢 | 丢失"ACK时钟" |
| New-Reno | 4个RTT | 中等 | 每RTT只能重传一个包 |
| SACK | 1个RTT | 快且精准 | 无 |
SACK发送方在第一个RTT内就可以识别出所有四个丢失的数据段,并在条件允许时重传它们。这是因为SACK选项提供了完整的接收状态信息,发送方不需要"猜"。
D-SACK:检测虚假重传
RFC 2883扩展了SACK选项,引入了D-SACK(Duplicate-SACK)机制。D-SACK解决的问题是:发送方如何知道它重传了一个实际上已经成功到达的数据段?
这种情况可能发生在以下场景:
- ACK包丢失,导致发送方误认为数据丢失
- 网络延迟导致超时,但数据实际已到达
- 数据包重排序被误判为丢失
D-SACK的规则是:当接收方收到一个重复的数据段时,它应该在SACK选项的第一个块中报告这个重复段的序列号范围。这个第一个块就是D-SACK块。
考虑以下场景:
发送方发送:3000-3499
ACK丢失,发送方超时重传:3000-3499
接收方收到重复的3000-3499,发送:
ACK = 4000
SACK = [3000-3500](D-SACK块,表示收到重复数据)
发送方收到这个D-SACK后,知道自己的重传是不必要的,可以采取相应措施,比如恢复之前降低的拥塞窗口。
Laura Chappell在Wireshark分析教程中展示了如何识别D-SACK:当SACK块的左边界小于累积确认字段时,这个块就是D-SACK块。这提供了快速诊断虚假重传的工具。
Linux内核实现与CVE-2019-11477
Linux内核对SACK的支持始于2.2系列,经过多年的演进,已经非常成熟。核心实现分布在以下文件:
net/ipv4/tcp_input.c:处理收到的ACK和SACK选项net/ipv4/tcp_output.c:发送数据时的SACK处理include/linux/tcp.h:SACK相关的数据结构定义
然而,2019年Netflix安全团队发现的CVE-2019-11477漏洞暴露了实现中的问题。这个漏洞被称为"TCP SACK Panic"。
漏洞的本质是:当处理大量精心构造的SACK块时,内核会消耗过多的内存来跟踪SACK状态。攻击者可以发送大量TCP段,每个段携带精心设计的SACK选项,导致内核在管理tcp_sack_block结构时产生极高的内存碎片,最终触发内核panic。
影响范围包括Linux内核2.6.29及以后版本。修复方案包括:
- 限制SACK块的数量
- 改进SACK状态的内存管理
- 对SACK选项进行更严格的验证
这个漏洞提醒我们:即使是经过充分测试的核心协议实现,也可能存在安全风险。SACK选项的复杂性——需要处理大量边界情况——增加了实现难度。
Wireshark分析实战
在实际网络故障排查中,Wireshark是分析SACK行为的利器。以下是一些关键的分析技巧:
识别SACK协商
在TCP三次握手的SYN和SYN-ACK包中,检查TCP选项字段是否包含"SACK Permitted"。如果只有一方携带该选项,则只有该方向的接收方能够发送SACK。
解读SACK块
Wireshark会显示SACK选项为类似"SACK: 949330-958090"的格式。这里:
- ACK字段(如947870)表示"我期望收到的下一个序列号",也是第一个缺失段的起始序列号
- SACK块表示"我已经收到了949330到958089的字节"
多个SACK块的分析
当网络中发生多包丢失时,会出现多个SACK块。Wireshark专家Laura Chappell建议使用显示过滤器tcp.options.sack.count > 1来快速定位这些情况。
每个新出现的SACK块代表一个新确认的非连续数据段。通过分析SACK块的演变,可以重建接收方的接收缓冲区状态。
D-SACK的识别
当看到SACK块的序列号范围小于累积确认字段时,这就是D-SACK块,表示接收方收到了重复的数据。这通常意味着发送方进行了不必要的重传。
高延迟网络中的SACK价值
在卫星通信、跨国链路等高延迟环境中,SACK的价值更加凸显。原因很简单:每个RTT都很昂贵,等待超时的代价极大。
卫星网络的特征
卫星网络典型特征包括:
- RTT可达500-800ms(GEO卫星)
- 高带宽延迟积(BDP),需要大窗口
- 较高的误码率
在这些条件下,Reno/NewReno的表现极其糟糕。假设丢失了3个包,每个RTT 600ms:
- New-Reno:需要3个RTT = 1.8秒才能恢复
- SACK:在1个RTT内完成恢复
卫星TCP性能增强
研究文献中,多项研究表明SACK是卫星TCP性能增强的基础组件。Berkeley的技术报告CSD-99-1083详细分析了TCP在卫星信道上的性能问题,指出SACK是必需的优化之一。
更重要的是,SACK与其他增强技术的配合:
- SACK + Large Window:解决带宽延迟积问题
- SACK + Forward RTO-Recovery (F-RTO):检测虚假超时
- SACK + Eifel算法:更准确地恢复拥塞窗口
SACK的代价与权衡
SACK并非免费的午餐,它带来了一些代价:
协议开销
每个SACK块占用8字节。在最坏情况下(4个SACK块),一个TCP选项字段就被占用了34字节(2字节Kind/Length + 32字节数据)。这减少了有效载荷的传输效率。
实现复杂性
SACK的发送方逻辑比累积确认复杂得多。需要维护Scoreboard数据结构,实现Pipe算法,正确处理partial ACK等。这增加了内核代码的复杂性和潜在bug的风险。
兼容性问题
虽然SACK设计为向后兼容,但在某些特殊场景下可能产生问题。例如,一些老旧的防火墙或中间设备可能不正确地处理SACK选项,导致连接异常。
结语
从累积确认到选择性确认,TCP用了十五年时间完成了一次看似微小但影响深远的演进。SACK解决的问题——多包丢失场景下的信息不对称——看似简单,实则触及了可靠传输协议的核心。
SACK的设计哲学值得深思:它没有试图改变TCP的基本模型,而是通过一个选项字段,精确地补充了发送方缺失的信息。这种渐进式的改进,既保持了协议的兼容性,又显著提升了性能。
在今天的互联网中,SACK已经成为标配。几乎所有现代操作系统默认启用SACK,CDN和大型互联网公司依赖它来保证全球范围内的数据传输效率。当我们享受流畅的视频流、快速的文件下载时,很可能正是SACK在背后默默工作,确保丢包不会成为性能的瓶颈。
从工程实践的角度,SACK提供了一个宝贵的经验:当信息不对称导致系统决策困难时,最好的解决方案往往是让信息的流动更加透明。无论是在网络协议还是其他软件系统,这个原则都有其适用之处。
参考文献
-
Postel, J. “Transmission Control Protocol.” RFC 793, IETF, September 1981.
-
Mathis, M., Mahdavi, J., Floyd, S., Romanow, A. “TCP Selective Acknowledgment Options.” RFC 2018, IETF, October 1996.
-
Floyd, S., Mahdavi, J., Mathis, M., Podolsky, M. “An Extension to the Selective Acknowledgement (SACK) Option for TCP.” RFC 2883, IETF, July 2000.
-
Blanton, E., Allman, M. “Using TCP Duplicate Selective Acknowledgement (DSACKs) and SCTP Duplicate TSNs to Detect Spurious Retransmissions.” RFC 3708, IETF, February 2004.
-
Blanton, E., Allman, M., Fall, K., Wang, L. “A Conservative Selective Acknowledgment (SACK)-based Loss Recovery Algorithm for TCP.” RFC 3517, IETF, April 2003.
-
Fall, K., Floyd, S. “Simulation-based Comparisons of Tahoe, Reno, and SACK TCP.” Computer Communication Review, July 1996.
-
Jacobson, V. “Congestion Avoidance and Control.” SIGCOMM ‘88, August 1988.
-
Stevens, W. “TCP Slow Start, Congestion Avoidance, Fast Retransmit, and Fast Recovery Algorithms.” RFC 2001, IETF, January 1997.
-
Hoe, J. “Improving the Start-up Behavior of a Congestion Control Scheme for TCP.” SIGCOMM ‘96, August 1996.
-
Paxson, V., Allman, M., Chu, J., Sargent, M. “Computing TCP’s Retransmission Timer.” RFC 6298, IETF, June 2011.
-
Allman, M., Paxson, V., Stevens, W. “TCP Congestion Control.” RFC 2581, IETF, April 1999.
-
Allman, M., Paxson, V., Blanton, E. “TCP Congestion Control.” RFC 5681, IETF, September 2009.
-
Henderson, T., Floyd, S., Gurtov, A., Nishida, Y. “The NewReno Modification to TCP’s Fast Recovery Algorithm.” RFC 6582, IETF, April 2012.
-
Sarolahti, P., Kojo, M. “Forward RTO-Recovery (F-RTO): An Algorithm for Detecting Spurious Retransmission Timeouts with TCP.” RFC 5682, IETF, September 2009.
-
Ludwig, R., Meyer, M. “The Eifel Detection Algorithm for TCP.” RFC 3522, IETF, April 2003.
-
Mathis, M., Heffner, J. “Packetization Layer Path MTU Discovery.” RFC 4821, IETF, March 2007.
-
Allman, M., Floyd, S., Partridge, C. “Increasing TCP’s Initial Window.” RFC 3390, IETF, October 2002.
-
Chu, J., Dukkipati, N., Cheng, Y., Sutin, M. “Increasing TCP’s Initial Window.” RFC 6928, IETF, April 2013.
-
Cheng, Y., Chu, J., Radhakrishnan, S., Jain, A. “TCP Fast Open.” RFC 7413, IETF, December 2014.
-
Borman, D., Braden, R., Jacobson, V., Scheffenegger, R. “TCP Extensions for High Performance.” RFC 7323, IETF, September 2014.
-
Ramakrishnan, K., Floyd, S., Black, D. “The Addition of Explicit Congestion Notification (ECN) to IP.” RFC 3168, IETF, September 2001.
-
Ramaiah, A., Stewart, R., Dalal, M. “Improving TCP’s Robustness to Blind In-Window Attacks.” RFC 5961, IETF, August 2010.
-
Touch, J. “Defending TCP Against Spoofing Attacks.” RFC 4953, IETF, July 2007.
-
Gont, F. “TCP’s Reaction to Soft Errors.” RFC 5461, IETF, February 2009.
-
Eddy, W. “TCP SYN Flooding Attacks and Common Mitigations.” RFC 4987, IETF, August 2007.
-
Allman, M. “TCP Congestion Control with Appropriate Byte Counting (ABC).” RFC 3465, IETF, February 2003.
-
Handley, M., Floyd, S., Pahdye, J., Widmer, J. “TCP Friendly Rate Control (TFRC): Protocol Specification.” RFC 3448, IETF, January 2003.
-
Floyd, S., Handley, M., Padhye, J., Widmer, J. “TCP Friendly Rate Control (TFRC): Protocol Specification.” RFC 5348, IETF, September 2008.
-
Kohler, E., Handley, M., Floyd, S. “Datagram Congestion Control Protocol (DCCP).” RFC 4340, IETF, March 2006.
-
Stewart, R. “Stream Control Transmission Protocol.” RFC 4960, IETF, September 2007.
-
Iyengar, J., Thomson, M. “QUIC: A UDP-Based Multiplexed and Secure Transport.” RFC 9000, IETF, May 2021.
-
Dawson, T., Juszczak, C. “TCP Performance over Satellite Channels.” CSD-99-1083, UC Berkeley, 1999.
-
Partridge, C., Shepard, T. “TCP/IP Performance over Satellite Links.” IEEE Network, June 1997.
-
Allman, M., Glover, D., Sanchez, L. “Enhancing TCP Over Satellite Channels using Standard Mechanisms.” RFC 2488, IETF, January 1999.
-
Border, J., Kojo, M., Griner, J., Montenegro, G., Shelby, Z. “Performance Enhancing Proxies Intended to Mitigate Link-Related Degradations.” RFC 3135, IETF, June 2001.
-
Balakrishnan, H., Padmanabhan, V., Seshan, S., Katz, R. “A Comparison of Mechanisms for Improving TCP Performance over Wireless Links.” SIGCOMM ‘96, August 1996.
-
Karn, P., Partridge, C. “Improving Round-Trip Time Estimates in Reliable Transport Protocols.” SIGCOMM ‘87, August 1987.
-
Jacobson, V., Braden, R., Borman, D. “TCP Extensions for High-Delay Paths.” RFC 1323, IETF, May 1992.
-
Mathis, M., Mahdavi, J. “Forward Acknowledgement: Refining TCP Congestion Control.” SIGCOMM ‘96, August 1996.
-
Chappell, L. “Analyze Multiple SACK Blocks.” Chappell University, October 2024.