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块信息。

协商过程如下:

  1. 客户端发送SYN段,携带SACK-Permitted选项
  2. 服务端收到SYN后,如果支持SACK,则在SYN-ACK中也携带SACK-Permitted选项
  3. 双方都看到对方的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时,它会:

  1. 解析SACK选项中的所有SACK块
  2. 对于每个SACK块,标记相应的序列号范围为"已SACK"
  3. 更新内部的接收状态跟踪

这种设计允许发送方准确知道哪些数据已经被接收方缓存,哪些数据仍在"飞行中"或已丢失。

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():决定下一个应该发送的数据段。优先级如下:

  1. 如果有已识别为丢失的数据段,优先重传
  2. 如果没有丢失的数据段,但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提供了一个宝贵的经验:当信息不对称导致系统决策困难时,最好的解决方案往往是让信息的流动更加透明。无论是在网络协议还是其他软件系统,这个原则都有其适用之处。


参考文献

  1. Postel, J. “Transmission Control Protocol.” RFC 793, IETF, September 1981.

  2. Mathis, M., Mahdavi, J., Floyd, S., Romanow, A. “TCP Selective Acknowledgment Options.” RFC 2018, IETF, October 1996.

  3. Floyd, S., Mahdavi, J., Mathis, M., Podolsky, M. “An Extension to the Selective Acknowledgement (SACK) Option for TCP.” RFC 2883, IETF, July 2000.

  4. Blanton, E., Allman, M. “Using TCP Duplicate Selective Acknowledgement (DSACKs) and SCTP Duplicate TSNs to Detect Spurious Retransmissions.” RFC 3708, IETF, February 2004.

  5. Blanton, E., Allman, M., Fall, K., Wang, L. “A Conservative Selective Acknowledgment (SACK)-based Loss Recovery Algorithm for TCP.” RFC 3517, IETF, April 2003.

  6. Fall, K., Floyd, S. “Simulation-based Comparisons of Tahoe, Reno, and SACK TCP.” Computer Communication Review, July 1996.

  7. Jacobson, V. “Congestion Avoidance and Control.” SIGCOMM ‘88, August 1988.

  8. Stevens, W. “TCP Slow Start, Congestion Avoidance, Fast Retransmit, and Fast Recovery Algorithms.” RFC 2001, IETF, January 1997.

  9. Hoe, J. “Improving the Start-up Behavior of a Congestion Control Scheme for TCP.” SIGCOMM ‘96, August 1996.

  10. Paxson, V., Allman, M., Chu, J., Sargent, M. “Computing TCP’s Retransmission Timer.” RFC 6298, IETF, June 2011.

  11. Allman, M., Paxson, V., Stevens, W. “TCP Congestion Control.” RFC 2581, IETF, April 1999.

  12. Allman, M., Paxson, V., Blanton, E. “TCP Congestion Control.” RFC 5681, IETF, September 2009.

  13. Henderson, T., Floyd, S., Gurtov, A., Nishida, Y. “The NewReno Modification to TCP’s Fast Recovery Algorithm.” RFC 6582, IETF, April 2012.

  14. Sarolahti, P., Kojo, M. “Forward RTO-Recovery (F-RTO): An Algorithm for Detecting Spurious Retransmission Timeouts with TCP.” RFC 5682, IETF, September 2009.

  15. Ludwig, R., Meyer, M. “The Eifel Detection Algorithm for TCP.” RFC 3522, IETF, April 2003.

  16. Mathis, M., Heffner, J. “Packetization Layer Path MTU Discovery.” RFC 4821, IETF, March 2007.

  17. Allman, M., Floyd, S., Partridge, C. “Increasing TCP’s Initial Window.” RFC 3390, IETF, October 2002.

  18. Chu, J., Dukkipati, N., Cheng, Y., Sutin, M. “Increasing TCP’s Initial Window.” RFC 6928, IETF, April 2013.

  19. Cheng, Y., Chu, J., Radhakrishnan, S., Jain, A. “TCP Fast Open.” RFC 7413, IETF, December 2014.

  20. Borman, D., Braden, R., Jacobson, V., Scheffenegger, R. “TCP Extensions for High Performance.” RFC 7323, IETF, September 2014.

  21. Ramakrishnan, K., Floyd, S., Black, D. “The Addition of Explicit Congestion Notification (ECN) to IP.” RFC 3168, IETF, September 2001.

  22. Ramaiah, A., Stewart, R., Dalal, M. “Improving TCP’s Robustness to Blind In-Window Attacks.” RFC 5961, IETF, August 2010.

  23. Touch, J. “Defending TCP Against Spoofing Attacks.” RFC 4953, IETF, July 2007.

  24. Gont, F. “TCP’s Reaction to Soft Errors.” RFC 5461, IETF, February 2009.

  25. Eddy, W. “TCP SYN Flooding Attacks and Common Mitigations.” RFC 4987, IETF, August 2007.

  26. Allman, M. “TCP Congestion Control with Appropriate Byte Counting (ABC).” RFC 3465, IETF, February 2003.

  27. Handley, M., Floyd, S., Pahdye, J., Widmer, J. “TCP Friendly Rate Control (TFRC): Protocol Specification.” RFC 3448, IETF, January 2003.

  28. Floyd, S., Handley, M., Padhye, J., Widmer, J. “TCP Friendly Rate Control (TFRC): Protocol Specification.” RFC 5348, IETF, September 2008.

  29. Kohler, E., Handley, M., Floyd, S. “Datagram Congestion Control Protocol (DCCP).” RFC 4340, IETF, March 2006.

  30. Stewart, R. “Stream Control Transmission Protocol.” RFC 4960, IETF, September 2007.

  31. Iyengar, J., Thomson, M. “QUIC: A UDP-Based Multiplexed and Secure Transport.” RFC 9000, IETF, May 2021.

  32. Dawson, T., Juszczak, C. “TCP Performance over Satellite Channels.” CSD-99-1083, UC Berkeley, 1999.

  33. Partridge, C., Shepard, T. “TCP/IP Performance over Satellite Links.” IEEE Network, June 1997.

  34. Allman, M., Glover, D., Sanchez, L. “Enhancing TCP Over Satellite Channels using Standard Mechanisms.” RFC 2488, IETF, January 1999.

  35. Border, J., Kojo, M., Griner, J., Montenegro, G., Shelby, Z. “Performance Enhancing Proxies Intended to Mitigate Link-Related Degradations.” RFC 3135, IETF, June 2001.

  36. Balakrishnan, H., Padmanabhan, V., Seshan, S., Katz, R. “A Comparison of Mechanisms for Improving TCP Performance over Wireless Links.” SIGCOMM ‘96, August 1996.

  37. Karn, P., Partridge, C. “Improving Round-Trip Time Estimates in Reliable Transport Protocols.” SIGCOMM ‘87, August 1987.

  38. Jacobson, V., Braden, R., Borman, D. “TCP Extensions for High-Delay Paths.” RFC 1323, IETF, May 1992.

  39. Mathis, M., Mahdavi, J. “Forward Acknowledgement: Refining TCP Congestion Control.” SIGCOMM ‘96, August 1996.

  40. Chappell, L. “Analyze Multiple SACK Blocks.” Chappell University, October 2024.