一、参考RFC
https://www.ietf.org/rfc/rfc2018
https://www.ietf.org/rfc/rfc2883.txt
二、SACK选项(RFC2018)
SACK实现的需要发送方和接收方协作。为此,TCP首部实际上定义了两种选项:SACK允许选项、SACK选项。
- SACK允许选项,即Sack-Permitted选项,用于标识是否支持 SACK,是在 TCP 连接建立时发送。
- SACK选项,则包含了具体的 SACK 信息。
当前所有操作系统都已经默认打开SACK支持了,但也有些系统需要协商。
2.1 Sack-Permitted 选项
也就是SACK允许选项,格式如下:
TCP Sack-Permitted Option:Kind: 4+---------+---------+
| Kind=4 | Length=2|
+---------+---------+
SACK_Permitted 选项,该选项只允许在 TCP 连接建立时,有SYN标志的包中设置,也即TCP握手的前两个包中,分别表示通信的两方各自是否支持SACK。
在Linux中可以查看/proc/sys/net/ipv4/tcp_sack文件是否默认打开SACK-Permitted。如果为1,默认打开,SYN包不会再发送这个选项来确认了。
$ cat /proc/sys/net/ipv4/tcp_sack
1
wireshark抓包确认是在SYN包中存在
Linux 2.2之后默认打开Sack-Permitted,见
(default: enabled; since Linux 2.2) Enable RFC 2018 TCP Selective Acknowledgements
https://man7.org/linux/man-pages/man7/tcp.7.html
2.2 Sack选项
该选项参数告诉对方已经接收到并缓存的不连续的数据块,发送方可根据此信息检查究竟是哪些块丢失,从而发送相应的数据块。
由于整个TCP首部的选项部分不能超过40字节,所以一个ACK段中最多可以容纳4组SACK信息。
TCP SACK Option:Kind: 5Length: Variable+--------+--------+| Kind=5 | Length |+--------+--------+--------+--------+| Left Edge of 1st Block |+--------+--------+--------+--------+| Right Edge of 1st Block |+--------+--------+--------+--------+| |/ . . . /| |+--------+--------+--------+--------+| Left Edge of nth Block |+--------+--------+--------+--------+| Right Edge of nth Block |+--------+--------+--------+--------+
Left Edge表示已收到的不连续块的第一个序号,Right Edge表示已收到的不连续块的最后一个序号+1,即左闭右开区间。通过ACK和SACK信息,发送方就可以确定接收方具体没有收到的数据就是从ACK到最大SACK信息之间的那些空洞的序号。
注意两个关键词:已收到, 不连续,SACK信息指明的区间数据已经收到了,不用再重传了。
注意:SACK会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆SACK的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。
2.3 SACK使用示例
2.3.1 如果只有一个Block, 那么left edge应该比ack大
wireshark可以使用tcp.options.sack来过滤
2.3.2 如果有多个Block, 第一个block的范围不能被SACK的第二个block覆盖
wireshark可以使用tcp.options.sack.count > 1来过滤多个SACK Block的包
2.3.3 SACK在数据重传时候的应用
考虑有SACK包的一段TCP通信数据
要想接收5846这之前所有的数据,但是已经确认了4287之前的数据。还缺少[4287,5846) 这一段区间数据,但是这一段数据又已经有一部分发送成功了,还缺少[4287, 4330), [4910, 5185)这一部分数据。
如果双方都支持SACK选项,那么只需要发送[4287, 4330), [4910, 5185)这两段数据即可;如果不支持SACK选项,得必须重发[4287,5846)这一整段数据。
第一部分44字节,第二部分275字节。
44字节分了2个包发送。
又重传了[4910, 5185)这一部分数据。
对于区间重传的数据seq是可以直接填写的。
对于接收端来说,收到数据要有ACK
44字节接收完了,还缺275字节。
最终两部分缺失数据接收完毕。
结论:在重传数据的时候,可以根据SACK信息,只需要重传44+275字节,而不用重传5846-4287这一段数据了,节省流量。
三、D-SACK(RFC2883)
D-SACK,又叫duplicate-SACK。
RFC2883扩展了RFC2018对SACK选项的定义,来指示接收端收到了重复包(duplicate packet),并通知发送端。
RFC2883建议在收到重复报文的时候,SACK选项的第一个块(这个块也叫做D-SACK块)可以用来传递这个重复报文的信息。这样允许TCP发送端根据SACK选项来推测不必要的重传。进而利用这些信息在乱序传输的环境中执行更健壮的操作。
这个D-SACK扩展是与原有的SACK选项的实现相互兼容的。D-SACK的使用也不需要TCP连接的双方额外协商(只要之前协商了SACK选项即可)。当TCP的发送方不理解D-SACK扩展的时候会简单的丢弃D-SACK块并继续处理SACK选项中的其他块。
接收端每个重复包最多在一个D-SACK块中上报一次。如果接收端依次发送了两个带有相同D-SACK块信息的ACK报文,则表示接收端接收了两次重复包,因此带有D-SACK块信息的ACK确认包传输丢失的时候重复包信息也会丢失。
在linux中/proc/sys/net/ipv4/tcp_dsack控制发出的报文中是否携带DSACK信息,但是不管该参数设置为何值,对于接收的TCP报文,linux总是会执行D-SACK块的检测处理。当linux检测到D-SACK块信息的时候会尝试撤销拥塞控制对于拥塞窗口的作用。另外在TLP丢包探测中也可以用来做loss probe的丢包探测。
Linux 2.4开始默认支持D-SACK,见https://man7.org/linux/man-pages/man7/tcp.7.html
3.1 D-SACK的特点:
- 只能是第一个SACK block。即使SACK有多个block也只有第一个block能表示D-SACK,也就是说每个SACK选项中最多有一个D-SACK块。
- 如果SACK的第一个block的范围被ACK所覆盖,那么就是D-SACK
- 如果SACK的第一个block的范围被SACK的第二个block覆盖,那么就是D-SACK——相同也算是覆盖。
3.2 D-SACK的意义
可见,引入了D-SACK,有这么几个好处:
- 可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。
- 是不是自己的timeout太小了,导致重传。
- 网络上出现了先发的包后到的情况(又称reordering)
- 网络上是不是把我的数据包给复制了。
- 可以做拥塞控制。
3.3 抓包示例
接收端在接收到SACK报文的时候,应该把第一个SACK块与这个ACK报文的ack number比较(而不是和当前已经接收到的最大的ack number比较),如果小于等于ack number则说明是D-SACK块,如果大于ack number则应该与第二个SACK块比较。如果第二个SACK块包含第一个SACK块,则说明第一个SACK块为D-SACK块,如果上面两个条件都不满足说明第一个SACK块是普通的SACK块。
tcp.options.sack.dsack and tcp.options.sack.count > 1 过滤D-SACK组合情况
3.4 RFC示例
示例一:ACK丢包
下面的示例中,丢了两个ACK,所以,发送端重传了第一个数据包(3000-3499),于是接收端发现重复收到,于是回了一个SACK=3000-3500,因为ACK都到了4000意味着收到了4000之前的所有数据,所以这个SACK就是D-SACK——旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道,数据包没有丢,丢的是ACK包。
Transmitted Received ACK Sent
Segment Segment (Including SACK Blocks)3000-3499 3000-3499 3500 (ACK dropped)
3500-3999 3500-3999 4000 (ACK dropped)
3000-3499 3000-3499 4000, SACK=3000-3500---------
示例二,网络延误
下面的示例中,网络包(1000-1499)被网络给延误了,导致发送方没有收到ACK,而后面到达的三个包触发了“Fast Retransmit算法”,所以重传,但重传时,被延误的包又到了,所以,回了一个SACK=1000-1500,因为ACK已到了3000,所以,这个SACK是D-SACK——标识收到了重复的包。
这个案例下,发送端知道之前因为“Fast Retransmit算法”触发的重传不是因为发出去的包丢了,也不是因为回应的ACK包丢了,而是因为网络延时了。
Transmitted Received ACK Sent
Segment Segment (Including SACK Blocks)500-999 500-999 1000
1000-1499 (delayed)
1500-1999 1500-1999 1000, SACK=1500-2000
2000-2499 2000-2499 1000, SACK=1500-2500
2500-2999 2500-2999 1000, SACK=1500-3000
1000-1499 1000-1499 30001000-1499 3000, SACK=1000-1500---------
还有比较多的例子,详细的可以参考 RFC2883。