一、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