TCP头部和IPV4头部除了固定的20字节外,都设置了 OPTION 字段用于存储自定义的数据,由于TCP头部和IPV4的报文长度字段均为4字节,所表示的最大值为15, 乘4,报文头部最大长度为60字节,所以Option字段最大长度为40字节,足够存储大量的报文控制信息。TCP和IPV4 OPTION的格式均为(标识字段 - 长度 - 数据)格式,通常采起4字节对齐存储。linux
目前 IP Option应用场景较少,且公网路由器对 IP Option的检查较为严格,通常都会直接丢弃带有 IP Option 的报文。TCP Option 的应用场景则较为普遍,常见的包括 TimeStamp(应用于时延测量), TCP_Window_Scalling(长肥网络下,TCP接收窗口须要足够大才能达到瓶颈带宽,此时须要 Window_Scaling 来表示一个更大的接收窗口),TCP_SACK(选择性确认,能够大幅提升TCP在丢包时的性能)等等,这些选项通常都会默认开启,路由器、端主机对这些选项的支持度也较高。本文主要介绍 TCP & IPV4 Option的处理逻辑,而后介绍一种经过 IP Option 字段在内网传输报文控制数据的方法。网络
步骤1: 构造TCP Option,计算存储空间socket
Linux把TCP选项的处理逻辑分为了SYN报文的选项和普通报文的选项两个部分,在TCP报文构造函数 static int tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask) 中有下面这段逻辑tcp
步骤2:分配 TCP 头部须要的存储空间,包括20个字节的标准头部加上TCP Option的部分。ide
tcp_options_size 为TCP选项部分的总长度,tcp_transmit_skb 接下来在 skb_push 中为TCP头部分配存储空间。函数
步骤3 : 向TCP头部写入构造好的TCP Option。 性能
tcp_transmit_skb 接下来经过 tcp_options_write() 函数把构造好的TCP Option 从 opts 中读出并写入TCP报文首部ui
步骤4 : TCP Option 的构造逻辑完成,报文进入IP层的处理逻辑。spa
步骤5:TCP Option 的解析在 tcp_parse_options() 函数 中完成,协议栈在接收到TCP报文后,会调用该函数完成报文头部的OPTION字段的解析。3d

IP Option的构造与TCP Option相似。
步骤1:分配存储空间,在IP报文构造函数 ip_queue_xmit 中,有下面这一段逻辑,分配IP报文头部空间和OPTION字段的存储空间
步骤2 : 构造OPTION字段,具体逻辑在 ip_options_build 中,与 TCP Option的逻辑相似。
步骤3: 解析, IP Option的解析在 ip_options_compile 中完成
在咱们的应用场景下,例如向路由器通告一些信息等,报文除了传输数据外,还要传输一些控制信息,咱们就是经过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 选项了。
TCP Option字段对于提高TCP性能有较大意义,所以须要了解常见的TCP Option字段的含义、开启和关闭。IP Option字段通常来讲不容易遇见,但在一些特殊的应用场景下,例如在局域网内捎带报文控制数据仍是颇有用处的。
Linux Kernel 4.12.13 https://elixir.bootlin.com/linux/v4.12.13/source