TCP系列08—连接管理—7、TCP 常见选项(option)
一、TCP选项概述
在前面介绍TCP头的时候,我们说过tcp基本头下面可以带有tcp选项,其中有些选项只能在连接过程中随着SYN包发送,有些可以延后。下表汇总了一些tcp选项
其中我标记为红色的部分是常见的TCP选项,我们仅针对这些红色的TCP选项进行介绍(主要是非红色的我也不太了解~~~),另外RFC1323已经被RFC7323取代,这里给出的是TCP选项原始定义的RFC
按照RFC793规定,一个TCP选项只需要单字节对齐,但是在实现上一般是两字节对齐或者会通过NOP选项实现四字节对齐,例如3bytes长的WSOPT选项,linux在添加这个tcp选项的时候,就会在这个选项前面加一个1byte的NOP选项凑成4bytes。
TCP选项的格式有两种,一种是单字节长的TCP选项如EOL和NOP。另外一种是包含1byte的kind,1byte的length,在加上选项的数据。除了EOL和NOP选项外,其他的TCP选项都是后一种格式,且为了兼容,协议要求后续如果扩展其他tcp选项同样需要采用后一种格式。
RFC1122协议规定TCP接收端必须能够处理任意TCP包中的选项,对于不能识别的TCP选项则采取忽略该选项的办法。其中有一些选项如EOL、NOP、MSS等是协议规定必须支持的,同时协议要求将来新增的TCP选项都需要由length域,这样TCP实现不能识别这个选项的时候就可以跳过这个选项。
二、EOL和NOP
EOL格式如下
+--------+ |00000000| +--------+ Kind=0
这个选项用来指示TCP的选项列表结束,这个选项是用在所有TCP选项的后面,并不是每个TCP选项后面都需要这个选项来指示选项结束,只有在TCP选项列表结束后没有与TCP头中的Header Length字段指定的头长重合时候才需要使用EOL选项,另外这个选项并不一定放在TCP头(包括扩展头)的末尾。举个例子,假如Header Length指定的TCP头长为40bytes,其中第29-38bytes为TSOPT选项,则可以在第39byte处添加一个EOL选项指示选项列表结束,可以看到EOL并没有位于TCP头的结束位置的第40byte。对于最后一个byte RFC793协议规定需要以0来填充。注意这个EOL后面填充的0已经不属于TCP选项的一部分了。
NOP选项格式如下
+--------+ |00000001| +--------+ Kind=1
这个选项可以使用在选项之间或者结尾处,比如,为了使3bytes的WSOPT选项在四字节对齐的边界处结束,可以在WSOPT选项之前添加一个NOP选项,这样整个选项长度为4bytes,更容易对齐。但是按照RFC793协议规定,发送端并不保证会填充NOP选项来让其他选项达到对齐的目的,因此接收端也应该准备好接收非四字节对齐的WSOPT选项。也就是说同样的几个TCP选项可以有不同的选项排列顺序,即使是相同的排列顺序也可能因为NOP和EOL等等而有不同的排列布局。
最后从linux实现的角度来说,linux本身发送TCP数据包的时候并不会添加EOL选项,而是通过添加一个或者多个NOP选项来实现整个TCP头长的四字节对齐(还记得我们之前说过TCP头中的Header Length字段的单位是32-bit word,因此TCP的头长一定是4bytes的整数倍)。但是linux在接收数据包的时候支持解析EOL选项。另外协议虽然没有限制TCP选项的排列顺序,但是linux实现上会按照一定的顺序排列TCP选项。原因是虽然协议没有限定options的顺序,但是互联网上有些设备对这个顺序是比较敏感的,一些特定的options顺序可能会引起问题。
三、MSS
Maximum segment size(MSS)格式如下
+--------+--------+---------+--------+ |00000010|00000100| max seg size | +--------+--------+---------+--------+ Kind=2 Length=4
Maximum segment size(MSS)是TCP期望从对端接收的最大的报文长度,自然也是对端在发送报文的时候的最大报文长度,注意MSS值仅指示TCP数据长度,并不包含关联的TCP头和IP头的长度。当连接在建立的时候,每个endpoint通常会在对应的SYN包中通过MSS option通告对方自己的MSS,按照RFC1122规定如果没有MSS选项提供则会使用默认的536bytes作为MSS(注意原始的RFC793协议是说没有提供MSS选项的时候可以发送任意大小的包,RFC1122修正了该说法)。还有一点需要注意由于目前网卡普遍支持TSO、GSO功能,在开启这些功能的前提下,协议栈中的TCP层可能会按照MSS的整数倍发包,然后再由网卡硬件来对TCP分段,这样减轻了CPU的处理压力。后面为了方便讨论窗口管理等特性,我们还是按照TCP层最大包不超过MSS来讨论。
在IPV6的jumbogram中(RFC2675),如果接收端接收到的MSS值为65535时候,标识真实的MSS需要根据PMTU值来确定。即MSS=PMTU-60。(jumbogram是IPV6中一种发送超大IP报文的协议特性,PMTU是接收端和发送端链路之间所有设备的最小MTU。)
RFC6691重新澄清了MSS选项的相关说明,并修正了之前几个RFC的错误说法。RFC6691明确规定在MSS选项中传递的MSS值为MTU减去IP基本头(ipv4为20bytes,IPV6为40bytes)和TCP基本头(20bytes)的值,不考虑扩展头。发送端负责发送数据前在这个MSS值的基础上扣除扩展头长度得出真实传输数据的长度。
四、WSOPT
WSOPT格式如下
+---------+---------+---------+ | Kind=3 |Length=3 |shift.cnt| +---------+---------+---------+
RFC1323为长肥管道提供了两个高性能扩展,一个是WSOPT选项另外一个是TSOPT选项。长肥管道是指带宽时延积很大的网络。
我们在介绍TCP头结构的时候提到过Window Size字段,这个字段占16位,最大为2^16-1,在长肥管道中,当发送端TCP需要通告更大的接收窗口的时候,就需要通过WSOPT选项了。当使用WSOPT选项的时候,接收窗口的实际大小则为Window Size<<shift.cnt,其中shift.cnt按照协议最大只能为14,当接收端接收到的shift.cnt大于14的时候,则按照14来处理Window Size。
WSOPT选项只能在SYN包中发送,因此当TCP连接建立起来后,window scale就固定了。一般在TCP实现上会有一个最大接收缓存,进而决定了最大接收窗口和window scale。WSOPT选项将原有的16位Window Size扩展到近30位大小(大约1GB)可以有效提升TCP允许使用的接收缓存,进而提升长肥网络的性能。
如果要使能window scale,需要发送端在SYN包中发送WSOPT选项,接收端在SYN-ACK包中同样发送WSOPT。注意协商window scale过程中协议要求不能对SYN和SYN-ACK报文头中的window size应用WSopt选项。WSOPT中的shift.cnt可以为0,标识window scale factor为1(即2^0=1),即接收窗口的实际大小即为Window Size。如果发送端发送了WSOPT选项但是没有收到对端的WSOPT选项,则需要将自己的window scale factor设置为1。
发送端和接收端都各有一个接收窗口和一个发送窗口,因此总共四个窗口,共维护4个scale factor。假设发送端接收窗口的scale factor为R,发送窗口的scale factor为S,则对应的接收端的接收窗口scale factor为R,发送窗口scale factor为S。在协商好两端的scale factor后,之后接收到的数据包中的Window Size字段自动进行scale factor,发送出去的数据包中的这个字段则为实际接收窗口右移scale factor后的结果。
五、SACK-Permitted和SACK
SACK-Permitted格式
Kind: 4 +---------+---------+ | Kind=4 | Length=2| +---------+---------+
SACK格式
Kind: 5 Length: Variable +--------+--------+ | Kind=5 | Length | +--------+--------+--------+--------+ | Left Edge of 1st Block | +--------+--------+--------+--------+ | Right Edge of 1st Block | +--------+--------+--------+--------+ | | / . . . / | | +--------+--------+--------+--------+ | Left Edge of nth Block | +--------+--------+--------+--------+ | Right Edge of nth Block | +--------+--------+--------+--------+
之前我们介绍过TCP的滑窗和ACK机制,我们再来简单的举个例子,假设接收端依序接收到系列号为2100的byte,序列号2100之前的byte都已经按序接收到了,接着因为乱序传输或者丢包的原因,接收端并没有接收到系列号为2101的TCP数据包,而是收到了系列号为2201的TCP报文并且长度为100byte。也就是说接收端缺少了2101-2200byte的数据,我们称接收端这种情况在滑窗上面形成了一个洞(hole)。如下图红色部分表示接收端已经接收到的数据。
此时接收端给发送端返回ACK报文的时候,TCP头中的ack number字段只能填写2101,还记得我们之前说过ack number表示接收端期望接收到的下一个byte的系列号吧,它是已经收到的连续报文中的最大序列号加1。注意是连续报文,因为2100和2201之间有洞,因此此时ack number只能是2101,发送端在接收到2101这个ack number后,并不能知道接收端实际上已经接收到了2201-2300byte,因而可能会在重传2101-2200byte的同时也会重传2201-2300byte的数据。那么有了SACK后,接收端就可以通过SACK来告诉发送端已经接收到了2201-2300byte的数据,这个就是一个SACK块(SACK block),同时结合ack number,发送端就可以仅仅只是重传2101-2200byte的数据,而不需要重传2201-2300byte的数据了。
一个endpoint如果在SYN包或者SYN-ACK包中解析处SACK-Permitted选项,那么就说明对端支持SACK扩展。那么本端就可以把收到的不连续报文信息发送给对端来帮助对端高效重传了。通常SACK-Permitted选项一般是在SYN包中发送,一旦收到对端SACK-Permitted选项后,SACK选项则可以在任意包中传输。Linux中可以通过/proc/sys/net/ipv4/tcp_sack控制是否使能SACK功能,设置为1时候使能,设置为0时候关闭SACK功能。因为SACK选项和TCP重传以及拥塞控制等等由比较大的关系,后面我们讲到这些的时候再来详细介绍。
六、TSOPT
TSOPT格式如下
Kind: 8 Length: 10 bytes +-------+-------+---------------------+---------------------+ |Kind=8 | 10 | TS Value (TSval) |TS Echo Reply (TSecr)| +-------+-------+---------------------+---------------------+ 1 1 4 4
TSOPT选项也叫做timestamp选项,有时也会写为TSopt。如上面介绍WSOPT时候所说,TSOPT也是RFC1323为了改善长肥管道而提出的一个TCP扩展。当使用这个选项的时候,发送方在TSval处放置一个时间戳,接收方则会把这个时间通过TSecr返回来。因为接收端并不会处理这个TSval而只是直接从TSecr返回来,因此不需要双方时钟同步。这个时间戳一般是一个单调增的值,RFC1323建议这个时间戳每秒至少增加1。其中在初始SYN包中因为发送方没有对方时间戳的信息,因此TSecr会以0填充,TSval则填充自己的时间戳信息。
在RFC1323中,TSOPT主要有两个用途一个是RTTM(round-trip time measurement)即根据ACK报文中的这个选项测量往返时延,另外一个用途是PAWS(protect against wrapped sequence numbers),即防止同一个连接的系列号重叠。另外还有一些其他的用途,如SYN-cookie、 Eifel Detection Algorithm 等等。关于RTTM我们留到TCP重传部分进行介绍,此处我们简单介绍一下PAWS。
PAWS假设接收到的每个TCP包中的TSval都是随时间单调增的,基本思想就是如果接收到的一个TCP包中的TSval小于刚刚在这个连接上接收到的报文的TSval,则可以认为这个报文是一个旧的重复包而丢掉。实际上接收到的TCP报文的系列号如果落在接收窗口外面就可以丢弃,但是对于一些高速不稳定网络,可能会出现一种情况,就是系列号翻转后,之前某个无效的重传包系列号满足条件,落在了接收窗口内,这个时候仅仅依靠系列号就不足以鉴定这个TCP报文的有效性了,结合TSOPT则可以通过时间戳选项来进一步过滤旧的重复包。
类似PAWS,实际上时间戳作为了系列号的一个扩展,在同一个连接上单调增。RFC6191进一步利用这个特点,在同一个连接的不同实例间时间戳单调增的时候,可以利用这个时间戳区分同一个连接的不同实例的时候,即使在TIME-WAIT状态下也允许建立连接。
RFC7323明确在TCP头中的ACK标志位有效的时候TSecr字段才有效,如果ACK标志位没有置位的时候,发送端应该把TSecr置为0。当发送出去的数据包ACK标志位置位的时候,发送端必须在TSecr中回显一个最近接收到的TSval。当ACK标志位没有置位的时候,接收端必须忽视TSecr字段。
TCP可以在初始的SYN包中发送TSopt选项,但是接收端只有在接收到的初始SYN报文中解析到TSopt选项的时候才允许在SYN-ACK报文中发送TSopt选项。一旦TCP通信的两端通过SYN报文和SYN-ACK报文协商好TSopt选项后,在这个连接随后的非RST报文中,TSopt选项必须被发送。一旦接收到一个不带由TSopt选项的非RST报文的时候,TCP应该静默的丢弃这个报文(注意是应该should,不是必须must)。TCP不能(must not)因为缺少预期的TSopt选项而中止一个TCP连接。注意这里是协议的要求,实现上并不一定会静默的丢弃这个数据包,比如linux在协商好TSopt后,收到没有TSopt选项的数据也会正常接收,后面文章会有wireshark示例。
当在三次握手中没有协商TSopt选项而在随后的数据传输中接收到TSopt选项的时候,TCP必须忽视这个TSopt选项然后正常处理这个TCP报文。在TCP同开的时候如果一个SYN报文包含TSopt选项,另外一个SYN报文不包含TSopt选项,那么两端都可以在随后的SYN-ACK报文中发送TSopt选项。
另外TSopt选项还有两个重要作用,一个是RACK重传,另外一个是Eifel探测算法,后面的文章我们会专门进行实例介绍。Linux中/proc/sys/net/ipv4/tcp_timestamps可以设置是否启用TSopt选项,这个参数设置为0的时候TCP连接就不会使用TSopt选项。
七、FOC
FOC选项的格式如下
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Kind | Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
~ Cookie ~
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Kind 1 byte: value = 34
Length 1 byte: range 6 to 18 (bytes); limited by
remaining space in the options field.
The number MUST be even.
Cookie 0, or 4 to 16 bytes (Length - 2)
Fast Open选项用来请求或者发送一个FOC(Fast Open Cookie),当cookie域为空的时候,client使用这个选项来从服务器请求一个FOC。当cookie域非空的时候,服务器可使用这个选项来把cookie传递给client,或者client可以使用这个选项来执行TFO。
最小的cookie大小是4byte,虽然图示中cookie是32位对齐的,但不是强制要求的,当数据包中不带SYN标志、Length值无效或者TFO功能没有打开的时候,需要忽略这个选项。
八、wireshark抓包示例
我们看一下之前FastOpen第一次正常连接SYN包中请求FOC时候对应的tcp选项截图如下,限于篇幅不再逐步讲解,现在我们讲解了TCP选项,建议下载wireshark文件,对照本节对TCP选项的讲解在看一遍wireshark中的TCP选项。
补充说明
1、linux支持的tcp选项可以参考TCPOPT_NOP宏定义附近定义的选项,写入tcp选项可以参考代码tcp_options_write,本文中wireshark中TCP选项的特定顺序也是在tcp_options_write写入的。
2、linux对于MSS的处理可以参考tcp_current_mss
3、第二版TCPIP详解P609中对于TSOPT的第二个字段描述为Timestamp Echo Retry,实际应该是Timestamp Echo Reply