1 概述
IPv4允许在20字节首部固定部分之后跟以最多共40个字节的选项。尽管已经定义的的IPv4选项共有10种,最常见的却是源路径选项。这些选项的访问途径是存取IP_OPTIONS套接字选项,我们将以一个使用源路由的例子展示这个访问方式。
IPv6允许在固定长度的40个字节IPv6首部和传输层首部(例如ICMPv6、TCP或UDP)之间出现扩展首部(extension header)。目前定义了6种不同的扩展首部。不同于IPv6的是,IPv6扩展首部的访问途径是函数接口,而不是强求用户理解这些首部如何呈现在IPv6分组中的真是细节。
2 Ipv4选项
我们在图1-1展示出IPv4的选项(options)字段跟在20字节IPv4首部固定部分之后。
IP层提供无连接不可靠的数据报递送服务(RFC 791).它会尽最大努力把IP数据报递送到指定的目的地,然而并不保证它们一定到达,也不保证它们的到达顺序与发送顺序一致,还不保证每个IP数据报只到达一次。任何期望的可靠性(即无差错按顺序不重复地递送用户数据)必须由上层提供支持。对于TCP(或SCTP)应用程序而言,这由TCP(或SCTP)本身完成。对于UDP应用程序而言,这得由应用程序完成,因为UDP是不可靠的。
IP层最重要的功能之一是路由(routing)。每个IP数据报包含一个源地址和一个目的地址。每个IP数据报包含一个源地址和一个目的地址。图1-1展示了IPv4数据报首部的格式。
图1-1 IPv4首部格式
-
4位版本(version)字段值为4.这是自20世纪80世纪早期以来一直在使用的IP版本。
-
首部长度(header length)字段是包括任何选项在内的整个IP首部的32位字长度。这个4位字段的最大取值为15,因而IP首部的最大长度为60字节。扣除首部固定部分所占据的20字节外,它最多允许40个字节的选项。
-
历史性的8位服务类型(type-of-server,TOS)字段(RFC 1349)已被替换为两个字段:6位区分服务码点(Differentiated Services Code Point,DSCP,RFC 2474)和2位显式拥塞通知(Explicit Congestion Notification,ECN,RFC3168).我们可以使用IP_TOS套接字选项设置该字段,虽然内盒可能覆盖为了实施Diffserv策略或实现ECN而设置的值。
-
16位总长度(total length)字段是包括IPv4首部在内的整个IP数据报的字节长度。数据报中的数据量就是本字段减掉4乘以首部长度(首部长度都是32位或4字节的整数倍)。本字段是必需的,因为有些数据链路要求把帧垫补成某个最小长度(例如以太网),因而有效IP数据报的大小有可能小于数据链路的最小长度。
-
16位标志(identification)字段由IP模块为每个IP数据报设置成不同的值,用于分片和重组。该字段必须就源IPv4地址、目的IPv4地址和协议这三个字段至少在数据报的网络存活期唯一标识每个IP数据报。如果分组不会被分片(但如设置了DF位),那么就不需要设置此字段。
-
DF(表示don't fragment,不要分片)位、MF(表示more fragment,还有片段)位和13位片段偏移(fragment offset)字段也用于分片和重组。DF位还用于路径MTU发现。
-
8位存活时间(time-to-live,TTL)字段由本IP数据报的发送者设置,并由转发它的每个路由器递减(即减去1).当被减到0时,相应路由器就丢弃该数据报。任何IP数据报的生命期限定为最多255跳。本字段的常用默认值为64,不过我们可以使用套接字选项IP_TTL和IP_MULTICAST_TTL查询和修改这个默认值。
-
8位协议(protocol)字段指定包含在本IP数据报中的数据类型。它的典型值有(ICMPv4)、2(IGMPv4)、6(TCP)和17(UDP)。这些值由IANA的“Protocol Numbers”注册处登记并提供查询。
-
16位首部检验和(header checksum)字段只对IP首部(包括任何选项)进行计算。其算法是标准的网际校验和算法,即简单的16位反码加法(16-bit ones-complement addition)。
-
源IPv4地址(source IPv4 address)和目的IPv4地址(destination IPv4 address)都是32位字段。
-
选项(options)字段。
IPv4定义了10种不同的选项。
-
NOP:no-operation。单字节选项,典型的用途是为后续选项落在4字节边界上提供填充。
-
EOL:end-of-list。单字节选项,终止选项的处理。既然各个IP选项的总长度必须为4字节的倍数,因此最后一个有效选项之后可能跟以0~3个EOL字节。
-
LSRR:loose source and record route。
-
SSRR:strict source and record route。
-
Timestamp。
-
Record route。
-
Basic security(已作废)。
-
Extended security(已作废)。
-
Stream identifier(已作废)。
-
Router alert。包含该选项的IP数据报要求所有转发路由器都查看其内容。
3 Ipv4源路径选项
源路径(source route)是由IP数据报的发送者指定的一个IP地址列表。如果源路径是严格的(strict),那么数据报必须且只能逐一经过所列的节点。也就是说列在源路径中的所有节点必须前后互为邻居。如果源路径是宽松的(loose),那么数据报必须逐一经过所列的节点,不过也可以经过未列在源路径中的其他节点。
IPv4源路径成为源和记录路径(source and record routes,SRR,其中LSRR表示宽松的选项,SSRR表示严格的选项),因为随着数据报逐一经过所列的节点,每个节点都把列在源路径中的自己的地址替换为外出接口的地址。SRR允许接收者逆转新的列表的顺序,得到沿反方向回到发送者的路径。
我们把源路径指定为一个IPv4地址数组,并冠以3个单字节字段,如图1-2所示。
图1-2 向内核传递的源路径
我们在源路径选项之前放置一个NOP选项,使得所有IP地址在各自的4字节边界对齐。这么做并非必须,不过无须占用额外空间(IP选项总是填充为4字节的倍数),还对齐了地址。
code字段对于LSRR为0x83,对于SSRR为0x89。len字段用于指定选项的字节长度,包括3字节选项首部和处于末尾的额外的最终目的地址(该地址不属于源路径)。对于由1个IP地址构成的源路径len为11,对于由2个IP地址构成的源路径len为15,以此类推,直到9个IP地址构成的源路径len为最大值43.NOP不属于本SSR选项(它自成一个单字节IP选项),因而不包括在len字段的涵盖范围之内,不过包括在给setsockopt指定的缓冲区大小之中。当源路径地址列表中的第一个地址被移走并置于IP首部的目的地址字段时,这个len值被减去4。ptr是一个指针,也就是路径中下一个待处理IP地址的偏移量,初始值为4,表示指向第一个IP地址。该字段的值随着IP数据报被每个所列节点处理而逐次加上4。
我们现在开发3个函数,分别初始化、创建和处理一个源路径选项。这个函数只是处理源路径IP选项。尽管源路径结合其他IP选项(例如路由器警告)也是可能的,但这样的组合很少使用。
第一个函数inet_strct_init以及用于构造选项内容的一些静态变量。
#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
uchar * inet_srcrt_init( int type )
{
// 分配一个最大长度(44字节)的缓冲区并将它清零。EOL选项的值为0,因此清零操作把整个选项缓冲区初始化为EOL
// 字节。接着按照图1-2设置源路径选项首部,包括用于对齐的NOP、源路径类型(LSRR或SSRR)、长度和指针。保存
// 指向len字段的一个指针,以后每往地址列表中加入一个地址,就在该字段中存入新值。把指向选项缓冲区的指针返
// 回给调用者,以便作为第四个参数传递给setsockopt。
optr = malloc( 44 ); // NOP,code.len,up to 10 addresses
bzero( optr, 44 ); // guaratees EOLs at end
ocnt = 0;
*optr++ = IPOPT_NOP; // NOP for alignment
*optr++ = type ? IPOPT_SSRR : IPOPT_LSRR;
lenptr = optr++; // we fill in length later
*optr++ = 4; // offset to first address
return( optr - 4 ); // pointer for setsockopt()
}
// 参数指向一个主机名或一个点分十进制数串IP地址
int inet_stcrt_add( char *hostptr )
{
int len;
struct addrinfo *ai;
strcut sockaddr_in *sin;
// 我们检查尚未指定过多的地址,如果这是第一个地址则将其初始化
if( ocnt > 9 )
err_quit( " too many source routes whit : %s ", hostptr );
// 调用我们的host_serv函数转换主机或点分十进制数串,并把最终的二进制地址存如路径地址列表。更改len字段的值,
// 返回缓冲区的总长度(包括NOP),以便调用者把它作为第五个参数传递给setsockopt。
ai = host_serv( hostptr, NULL, AF_INET, 0 );
sin = ( strcut sockaddr_in * ) ai->ai_addr;
memcpy( optr, &sin->sin_sin_addr, sizeof( struct in_addr ) );
freeaddrinfo( ai );
optr += sizeof( struct in_addr );
ocnt++;
len = 3 + ( ocnt * sizeof( struct in_addr ) );
*lenptr = len;
return( lem + 1 ); // size of setsockopt()
}
通过getsockopt调用返回给应用进程的接收源路径格式不同于图1-2所示的发送源路径。图1-3展示了接收格式。
图1-3 getsockopt返回的源路径选项格式
首先,地址的顺序是所收取的源路径被内核逆转后的顺序。这里“逆转”指的是如果所收取的源路径按顺序包括A,B,C和D四个地址,该路径的逆转顺序就是D,C,B和A。头4个字节是该列表的第一个IP地址,后跟一个单字节NOP(为了对齐),再跟以3字节源路径选项首部,最后跟以其余的IP地址。3字节选项首部之后最多可以跟以9个地址,所返回首部中len字段相应的最大值为39.NOP始终存在,因此由getsockopt返回的长度于是总为4字节的倍数。
下一个源路径函数取得图1-3所示格式的一个接收源路径并显示该信息。
void inet_srcrt_print( u_char *ptr, int len )
{
u_char c;
char str[ INET_ADDRSTRLEN ];
strcut in_addr hop1;
// 保存缓冲区中的第一个IP地址,跳过后跟的任何NOP
memcpy( &hop1, ptr, sizeof( struct in_addr ) );
ptr += sizeof( strcut in_addr );
while( ( c = *ptr++ ) == IPOPT_NOP ); // skip any leading NOPS
//我们只显示源路径,从3字节首部中,我们检查code,取出len,并跳过ptr。我们接着显示跟在3字节首部之后的
// 所有IP地址,不过末尾那个目的IP地址除外。
if( c == OPOPT_LSRR )
printf( " received LSRR: " );
else if ( c == IPOPT_SSRR )
printf( " received SSRR: " );
else
{
printf( " received option type %d\n ", c );
return;
}
printf( " %s ", inet_ntop( AF_INET,&hop1, str, sizeof( str ) ) );
len = *ptr++ - sizeof( struct in_addr ); // subtract dest IP addr
ptr++; // skipoverpointer
while( len > 0 )
{
printf( " %s ", inet_ntop( AF_INET, ptr, str, sizeof( str ) ) );
ptr += sizeof( struct in_addr );
lem -= sizeof( struct in_addr );
}
printf( " \n " );
}