0x01 簡介
TCP頭部和IPV4頭部除了固定的20字節外,都設置了 OPTION 字段用於存儲自定義的數據,因為TCP頭部和IPV4的報文長度字段均為4字節,所表示的最大值為15, 乘4,報文頭部最大長度為60字節,因此Option字段最大長度為40字節,足夠存儲大量的報文控制信息。TCP和IPV4 OPTION的格式均為(標識字段 - 長度 - 數據)格式,一般采取4字節對齊存儲。
目前 IP Option應用場景較少,且公網路由器對 IP Option的檢查較為嚴格,一般都會直接丟棄帶有 IP Option 的報文。TCP Option 的應用場景則較為廣泛,常見的包括 TimeStamp(應用於時延測量), TCP_Window_Scalling(長肥網絡下,TCP接收窗口需要足夠大才能達到瓶頸帶寬,此時需要 Window_Scaling 來表示一個更大的接收窗口),TCP_SACK(選擇性確認,可以大幅提高TCP在丟包時的性能)等等,這些選項一般都會默認開啟,路由器、端主機對這些選項的支持度也較高。本文主要介紹 TCP & IPV4 Option的處理邏輯,然后介紹一種通過 IP Option 字段在內網傳輸報文控制數據的方法。
0x02 TCP OPTION Linux實現
步驟1: 構造TCP Option,計算存儲空間
Linux把TCP選項的處理邏輯分為了SYN報文的選項和普通報文的選項兩個部分,在TCP報文構造函數 static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask) 中有下面這段邏輯
- 如果報文為SYN報文,則調用 tcp_syn_options 函數處理握手期間的一些TCP Option,例如MSS協商,Window_Scaling等。
- 如果報文為非SYN報文,則調用 TCP_established_options 函數處理包括TimeStamp,SACK等選項的構造。
- 構造好的TCP選項並不是直接寫入TCP頭部,而是存儲在 struct tcp_out_options 類型的結構體變量 opts 中。
步驟2:分配 TCP 頭部需要的存儲空間,包括20個字節的標准頭部加上TCP Option的部分。
tcp_options_size 為TCP選項部分的總長度,tcp_transmit_skb 接下來在 skb_push 中為TCP頭部分配存儲空間。
步驟3 : 向TCP頭部寫入構造好的TCP Option。
tcp_transmit_skb 接下來通過 tcp_options_write() 函數把構造好的TCP Option 從 opts 中讀出並寫入TCP報文首部
步驟4 : TCP Option 的構造邏輯完成,報文進入IP層的處理邏輯。
步驟5:TCP Option 的解析在 tcp_parse_options() 函數 中完成,協議棧在接收到TCP報文后,會調用該函數完成報文頭部的OPTION字段的解析。

0x03 IP OPTION Linux實現
IP Option的構造與TCP Option類似。
步驟1:分配存儲空間,在IP報文構造函數 ip_queue_xmit 中,有下面這一段邏輯,分配IP報文頭部空間和OPTION字段的存儲空間
步驟2 : 構造OPTION字段,具體邏輯在 ip_options_build 中,與 TCP Option的邏輯類似。
步驟3: 解析, IP Option的解析在 ip_options_compile 中完成
0x04 應用
在我們的應用場景下,例如向路由器通告一些信息等,報文除了傳輸數據外,還要傳輸一些控制信息,我們就是通過IP報頭的OPTION字段來攜帶這些控制信息。一般情況下,帶有TCP選項的報文,即使TCP選項是自定義的,公網路由器也不會輕易丟包,但公網路由器對IP選項的審查要嚴格的多,帶有IP選項的報文。當報文從局域網轉發到公網的時候,Linux網關可以在Qdisc中刪除該選項,並轉發到公網,Linux上發送一個帶有自定義IP選項的報文也非常容易,不需要修改內核,只需要在用戶態用 setsockopt() 調用便可以完成,下面是一個設置自定義IP 選項的Linux套接字客戶端程序示例。
1 #include <stdio.h> 2 #include <string.h> 3 #include <stdlib.h> 4 #include <unistd.h> 5 #include <arpa/inet.h> 6 #include <sys/socket.h> 7 #include <netinet/ip.h> 8 9 #define SERV_PORT 1234 10 #define SERV_IP "127.0.0.1" 11 #define MAXLINE 4096 12 #define MAXSIZE 40 13 14 #define IPOPT_TAG 0x21 //IP選項標志字段 15 #define IPOPT_LEN 8 //IP選項長度字段 16 17 int main(int argc,char *argv[]) 18 { 19 int sockfd; 20 struct sockaddr_in servaddr; 21 22 memset(&servaddr,0,sizeof(servaddr)); 23 servaddr.sin_family = AF_INET; 24 servaddr.sin_addr.s_addr = inet_addr(SERV_IP); 25 servaddr.sin_port = htons(SERV_PORT); 26 27 //構造自定義的TCP選項 28 unsigned char opt[MAXSIZE]; 29 opt[0] = IPOPT_TAG; 30 opt[1] = IPOPT_LEN; 31 //寫入選項數據 32 *(int *)(opt + 4) = htonl(50000); 33 34 if((sockfd = socket(AF_INET,SOCK_STREAM,0)) <= 0){ 35 perror("socket error : "); 36 exit(1); 37 } 38 39 if(connect(sockfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0){ 40 perror("connect error "); 41 exit(1); 42 } 43 44 //設置套接字發送該選項 45 if(setsockopt(sockfd,IPPROTO_IP,IP_OPTIONS,(void *)opt,IPOPT_LEN) < 0){ 46 perror("setsockopt error "); 47 exit(1); 48 } 49 50 char buff[MAXLINE]; 51 52 while(fgets(buff,MAXLINE,stdin) != NULL){ 53 if(write(sockfd,buff,strlen(buff)) < strlen(buff)){ 54 perror("write error "); 55 exit(1); 56 } 57 } 58 59 close(sockfd); 60 }
內核並沒有檢測 setsockopt() 的參數,直接將自定義的選項復制到了IP報文選項部分。
我們可以通過 getsockopt() 函數可以直接讀取自定義的IP OPTION。
TCP自定義選項的設置和讀取相對於IP選項要麻煩一些,一般向TimeStamp,SACK等TCP選項並不需要用戶去讀取,因此也沒有開放用戶層訪問的接口,直接通過 getsockopt() 函數無法讀取自定義的TCP 選項,但我們可以通過修改內核 getsockopt() 來實現自定義TCP選項的讀取,我們知道內核 getsockopt() 函數底層是由 do_ip_getsockopt 和 do_tcp_getsockopt 等協議相關的接口組成的, 在 do_tcp_getsockopt 接口內我們可以添加用戶層訪問自定義TCP選項的接口,然后便可以在用戶層通過 getsockopt() 函數來訪問自定義的 TCP 選項了。
0x05 總結
TCP Option字段對於提升TCP性能有較大意義,因此需要了解常見的TCP Option字段的含義、開啟和關閉。IP Option字段一般來說不容易碰見,但在一些特殊的應用場景下,例如在局域網內捎帶報文控制數據還是很有用處的。
0x06 參考
Linux Kernel 4.12.13 https://elixir.bootlin.com/linux/v4.12.13/source