24.1. 概述
IPv4允许在固定的20字节头部之后跟以40字节的选项。虽然定义了10个不同的选项,但最常用的是源路径选项,这些选项透过IP_OPTIONS套接口选项访问。
IPv6允许在固定的40字节IPv6头部和传输层头部(如ICMPv6,TCP或UDP)之间出现扩展头部(extension header)。和IPv4不同的是,访问IPv6扩展头部的途径不是强迫用户去理解头部如何出现在IPv6分组中的实际细节,而是通过函数接口进行。
24.2. IPv4选项
getsockopt和setsockopt(level参数为IPPROTO_IP,optname参数为IP_OPTIONS)读取和设置IP选项。getsockopt和setsockopt的第四个参数是指向一个缓冲区(大于小于等于44字节)的指针,第五个参数是该缓冲区的大小。该缓冲区的大小之所以可以比选项的最大总长度还大4个字节是源路径选项的处理方法使然,我们将很快描述到这一点。除了两个源路径选项之外,该缓冲区的格式就是IP数据报中放置的选项的格式。
当用setsockopt设置了IP选项后,在该套接口上发出的所有IP数据报都将包括这些选项。套接口可以是TCP,UDP或原始IP套接口。为了清除这些选项,可以调用setsockopt,置第四个参数为空指针,或者置第五个参数(长度)为0
当调用getsockopt获取一个由accept创建的已连接TCP套接口的IP选项时,返回的是在监听套接口上收到的客户的SYN中源路径选项的倒序值。源路径自动被TCP倒序,因为客户说明的是从客户到服务器的源路径,服务器需要使用该路径的倒序值,以发送从服务器到客户的数据报。如果SYN中没有源路径,那么getsockopt通过第五个参数返回的值-结果参数长度将为0。对所有其他的TCP套接口,所有的UDP套接口以及原始IP套接口而言,getsockopt只返回用setsockopt设置的IP选项的拷贝。注意,对一个原始IP套接口来说,收到的IP头部,包括任何IP选项,总是由输入函数返回的,所以收到的IP选项总是可得的。
24.3. IP源路径选项
源路径是IP数据报的发送者说明的一组IP地址。如果源路径是严格的(strict),那么数据报必须且只能经过所列的节点。如果源路径是宽松的(loose),数据报必须经过所列的节点但也可以经过未在源路径中列出的节点。
IPv4源路径称为源和记录路径(source and record route, SRR),宽松源路径选项称为LSRR,严格源路径选项称为SSRR,原因是当数据报穿过所列的全部节点到来时,每个节点都将自己的所列地址替换为自己的外出接口地址。这允许接收者将新的列表倒序以沿逆向的路径回到发送者。这两个源路径的例子以及相应的tcpdump输出在TCPv1的8.5节
我们将源路径说明为一个IPv4地址数组,前边附以三个1字节字段(如图24.1所示),这是我们传递给setsockopt的缓冲区格式。
在源路径选项之前,我们放置一个NOP,这使得所有的IP地址与4字节的边界对齐。这不是必须的,但并不用额外的空间(IP选项总是填充成4字节的倍数),而且对齐了地址。
在本图中,我们展示了路径上的10个IP地址,但是所列的第一个地址被从源路径选项中挪出,在IP数据报离开源主机时作为它的目的地址。虽然在40字节的IP选项空间中只有9个IP地址的位置(别忘记我们刚要讨论的3个字节选项头部),但是加上目的地址(不是指源路径的最终目的地址,而是按源路径转发的各个IP数据报的目的地址字段地址),IPv4头部中实际上有10个IP地址。
code对LSRR为0x83,对SSRR为0x89。len用于说明选项的字节长度,包括3字节头部和尾部额外的目的地址。对包括1个IP地址的路径,len为11,2个IP地址的路径,len为15,依此类推,len最大为43.NOP不是选项的一部分,所以不包括在len字段,但是却包括在给setsockopt指定的缓冲区的大小中。当源路径选项的第一个地址挪走并被置入IP头部的目的地址字段后,这个len值将减4。ptr是一个指向路径中下一个要处理的IP地址的指针或偏移量,其初始值为4,即指向第一个IP地址,该字段的值每经过一个所列节点将加4。
现在我们开发三个函数以初始化,创建和处理一个源路径选项。我们的函数只处理一个源路径选项,虽然有可能将源路径和其他IP选项(例如timestamp)结合起来,但是除了两个源路径选项之外,其他选项很少使用。图24.2 是第一个函数inet_srcrt_int以及构造选项要用到的一些静态变量。
#include "unp.h" #include <netinet/in_systm.h> #include <netinet/ip.h> static u_char * optr; /* pointer into options being formed */ static u_char * lenptr; /* pointer to length byte in SRR option */ static int ocnt; /* count of # addresses */ u_char * inet_srcrt_init(void) {/*分配一个最大为44字节的缓冲区并且将其初置为0,EOL选项的值为0,所以这将整个选项初始化为EOL字节。指向选项的指针返回给调用者以便作为第四个参数传给setsockopt */ optr = Malloc(44); /* NOP, code, len, ptr, up to 10 addresses */ bzero(optr, 44); /* guarantees EOLs at end */ ocnt = 0; return(optr); /* pointer for setsockopt() */ }
下一个函数inet_srcrt_add,把一个IPv4地址加到正在构造的源路径上
int inet_srcrt_add(char * hostptr, int type) {/*第一个参数指向一个主机名或者点分十进制数形式的IP地址,第二个参数对宽松源路径为0,对严格源路径为非0.我们将看到,加到路径中的第一个地址的类型决定路径是松散的还是严格的 */ int len; struct addinfo * ai; struct sockaddr_in * sin; /*我们检查没有指定太多的地址,如果是第一个地址则进行初始化。我们已经提到过,源路径选项前总是放一个NOP。我们保存一个指向len字段的指针,当每个地址加入列表时,存入这个长度值 */ if(ocnt > 9) err_quit("too many source routes with: %s", hostptr); if(ocnt == 0) { *optr++ = IPOPT_NOP; /* NOP for alignment */ *optr++ = type ? IPOPT_SSRR : IPOPT_LSRR; lenptr = optr++; /* we fill in the length later */ *optr++ = 4; /* offset to first address */ } /*我们的host_serv函数处理主机名或者点分十进制数串,并将最终的二进制地址存入列表。修改len字段的值,并返回缓冲区的大小(包括NOP)以便调用者将其传给setsockopt */ ai = Host_serv(hostptr, " ", AF_INET, 0); sin = (struct sockaddr_in *) ai->ai_addr; memcpy(optr, &sin->sin_addr, sizeof(sturct in_addr)); freeaddrinfo(ai); optr += sizeof(struct in_addr); ocnt++; len = 3 + ( ocnt * sizeof(struct in_addr) ); * lenptr = len; return(len+1); /* size for setsockopt() */ }
当一个收到的源路径通过getsockopt返回给应用进程时,其格式和图24.1不同,图24.4展示了这一格式
首先,地址的顺序是收到的源路径被内核颠倒后的顺序。“颠倒”是值如果收到的源路由包括四个地址ABCD,则颠倒的顺序为D,C,B,A,前4字节是列表中的第一个IP地址,后面是1字节的NOP(为了对齐),再后面是3个字节源路径选项头部,最后跟以其余的IP地址。最多有9个IP地址可以跟在3字节的头后,返回的len域最大值为39.因为NOP总是出现的,所以getsockopt返回的长度总是4的倍数。
图24.4所示的格式在<netinet/ip_var.h>中定义为下述结构:
#define MAX_IPOPTLEN 40 struct ipoption { struct in_addr ipopt_dst; /* first_hop dst if source routed */ char ipopt_list[MAX_IPOPTLEN]; /* Options proper */ };
这个返回的格式与我们传递给setsockopt的格式是不同的。如果我们想把图24.4中的格式转化为图24.1的格式,我们必须将头4个字节和随后的4个字节对换,再给len字段加4。所幸的是我们不必这样做,因为源自Berkeley的实现自动地使用在TCP套接口收到的源路径的倒序值。换句话说,图24.4展示的信息是getsockopt返回的纯粹信息,我们不必调用setsockopt高速内核使用该路径传送该TCP连接上的IP数据报,内核会自动这么做。我们很快就会在我们的TCP服务器程序例子中看到这一点。
下一个源路径函数是取得收到的图24.4格式的源路径,并输出该信息。图24.5给出的是我们的这个函数inet_srcrt_print
void inet_srcrt_print(u_char * ptr, int len) { u_char c; char str[INET_ADDRSTRLEN]; struct in_addr hop1; memcpy(&hop1, ptr, sizeof(struct in_addr)); /*将第1个IP地址存放在缓冲区,跳过任何后面的NOP */ ptr += sizeof(struct in_addr); while( (c = * ptr++) == IPOPT_NOP ); /* skip any leading NOPs */ if( c == IPOPT_LSRR) /*我们只输出源路径信息,从3字节头部中,我们检查code,取出len,并跳过ptr。然后我们输出3字节头部后的所有IP地址,最终的目的地址除外。 */ printf("received LSRR:"); else if (c == IPOPT_SSRR) printf("received SSRR:"); else { printf("received option type %d\n", c); return; } printf("%s", inet_htop(AF_INET, &hop1, str, sizeof(str))); len = * ptr++ - sizeof(struct in_addr); /* subtract dest ip address */ ptr++; /* skip over pointer */ while(len > 0) { printf("%s", inet_ntop(AF_INET, ptr, str, sizeof(str))); ptr += sizeof(struct in_addr); len -= sizeof(struct in_addr); } printf("\n"); }
现在我们修改TCP回射客户程序以说明源路径,修改TCP回射服务器程序以输出收到的源路径。图24.6是我们的客户程序。
#include "unp.h" int main(int argc, char * * argv) { int c, sockfd, len = 0; u_char * ptr; struct addrinfo * ai; if( argc < 2 ) /*我们调用inet_srcrt_init函数初始化源路径,路径的每跳由-g选项(宽松)或-G选项(严格)指定。第1个IP地址的类型说明了源路径的类型。inet_srcrt_add函数将每个地址加到路径中。 */ err_quit("usage: tcpcli01 [ -[gG] <hostname> ... ] <hostname>"); ptr = inet_srcrt_init(); opterr = 0; /* dont want getopt() writing to stderr */ while( ( c = getopt(argc, argv, "g: G:")) != -1 ) { swith(c) { case 'g': /* loose soure route */ len = inet_srcrt_add(optarg, 0); break; case 'G': /* strict source route */ len = inet_srcrt_add(optarg, 1); break; case '?': err_quit("unrecognized optin: %c", c); } } if ( optind ! = argc - 1) /*最后一个命令行参数是主机名或者服务器的点分十进制数地址,由函数host_serv处理。我们不能够调用tcp_connect函数,因为在socket和connect调用之间必须指定源路径。connect启动了三路握手,但我们想要最初的SYN和后续的分组都是用这个源路径 */ err_quit("missing <hostname>"); ai = Host_serv(argv[optind], SERV_PORT_STR, AF_INET, SOCK_STREAM); sockfd = Socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if(len>0) /*如果指定一个源路径,我们必须将服务器的IP地址加到IP地址列表的末尾,setsockopt给套接口安装源路径,然后我们调用connect,紧接着是str_cli函数。 */ { len = inet_srcrt_add(argv[optind], 0); /* des at end */ Setsockopt(sockfd, IPPROTO_IP, IP_OPTIONS, ptr, len); free(ptr); } Connect(sockfd, ai->ai_addr, ai->ai_addrlen); str_cli(stdin, sockfd); /* do it all */ exit(0); }
我们的TCP服务器程序几乎和图5.12给出的代码相同,只有以下几点变化。第一我们给选项分配空间:
int len; u_char * opts; opts = Malloc(44);
然后在调用accept之后,fork之前获取IP选项:
len = 44; Getsockopt(connfd, IPPROTO_IP, IP_OPTIONS, opts, &len); if (len > 0) { printf("received IP options, len = %d\n", len); inet_srcrt_print(opts, len); }
如果从客户收到的SYN未包括任何IP选项,从getsockopt返回的len变量将0(len是一个值-结果参数)。我们早先提到,我们不必做任何事情使TCP使用收到的源路径的倒序值,这是TCP自动做的。我们调用setsockopt所做的全部工作就是取得一份收到的源路径的拷贝。如果我们不想让TCP使用这条路径,我们可以在accept返回之后调用setsockopt,将第五个参数置为0,这将取消正在使用的IP选项。TCP在三路握手的第二个参数已经使用了源路径,但如果我们取消了该选项,IP将给送往客户的以后的分组使用计算出的任何路径。
24.4. IPv6扩展头部
1. 步跳(hop_by_hop)选项必须紧跟40字节的IPv6头部。目前没有定义这种可供应用程序使用的选项。
2. 目的(destination)选项:目前没有定义这种可供应用程序使用的选项。
3. 路由头部(routing header):这是一个源路由选项,在概念上类似于我们在24.3节中描述的IPv4源路径选项。
4. 分片头部(fragmentation hearder):该头部由将IPv6数据报分片的主机自动生成,有最终的目的主机重组片段时处理。
5. 认证头部(authentication header, AH):该头部的用法由RFC 1826[Atkinson 1995a]和[Kent and Atkinson 1997a]说明。
6. 封装安全有效负载(encapsulating security payload, ESP)头部:该头部的用法在RFC 1827[Atkinson 1995b]和[Kent and Atkinson 1997b]中说明。
我们说过分片头部全部由内核处理,[McDonald 1997]建议用套接口选项处理AH和ESP头部。这样只剩下了前三个选项,我们将在下两节中讨论。
24.5. IPv6步跳选项和目的选项
24.6. IPv6路由头部
24.7. IPv6粘附选项
24.8. 小结