UNIX网络编程卷一 学习笔记 第二十七章 IP选项

IPv4允许在20字节的首部固定部分后跟最多共40字节的选项。尽管已经定义了10种IPv4选项,但最常用的是源路径选项。我们可通过存取IP_OPTIONS套接字选项访问这些选项,我们存取该套接字选项时,所用的缓冲区中的值就是它们置于IP数据报中的格式。

IPv6允许在固定长度40字节的IPv6首部之后,传输层首部(如ICMPv6、TCP、UDP)之前出现扩展首部,目前定义了6种扩展首部。与IPv4不同的是,IPv6扩展首部的访问途径是函数接口,而非强求用户理解这些首部如何呈现在IPv6分组中的真实细节。

IPv4选项跟在20字节IPv4首部固定部分之后,且IPv4首部中的首部长度字段长4位,首部长度字段的单位是32位字,因此IP首部的总长度为15个32位字(60字节),因此IPv4选项字段最长40字节。IPv4定义了10种不同的选项:

1.NOP:no-operation。单字节选项,用途是为某个后续选项落在4字节边界上提供填充。

2.EOL:end-of-list。单字节选项,终止选项列表。既然各个IP选项的总长度必须是4字节的倍数,因此最后一个有效选项后可能跟以0~3个EOL字节。

3.LSRR:loose source and record route。

4.SSRR:strict source and record route。

5.Timestamp。

6.Record route。

7.Basic security(已作废)。

8.Extended security(已作废)。

9.Stream identifier(已作废)。

10.Router alert。它是在RFC 2113中叙述的一种选项,所有转发数据报的路由器都应查看该选项。

RFC 1108给出了以上两种安全选项(7和8)的细节,但它们没有得到广泛使用。

读取和设置IP选项字段使用getsockopt和setsockopt函数,level参数为IPPROTO_IP,optname参数为IP_OPTIONS。这两个函数的第4个参数是指向某个缓冲区(大小为44字节)的一个指针,第5个参数是该缓冲区的大小。该缓冲区大小可以比选项字段的最大长度多出4字节是由于源路径选项的处理方式,稍后介绍。除了两种源路径选项外,其他选项在该缓冲区中的格式就是把它们置于IP数据报中的格式。

使用setsockopt函数设置IP选项后,相应套接字上发送的所有IP数据报都包含这些选项。可以在TCP、UDP、原始IP套接字上设置IP选项。清除这些选项同样使用setsockopt函数,既可把第4个参数设为空指针,也可把第5个参数设为0。

对于已经设置了IP_HDRINCL套接字选项的一个原始IP套接字,并非所有实现都支持再为它设置IP选项。许多源自Berkeley的实现在IP_HDRINCL选项开启时不发送使用IP_OPTIONS设置的IP选项,因为应用可能在它构造的IP首部中设置了它自己的IP选项,其他系统(如FreeBSD)允许应用进程或使用IP_OPTIONS套接字选项设置IP选项,或者通过开启IP_HDRINCL并在自己构造的IP首部中包括IP选项达到设置目的,但不能混用两种方式。

当调用getsockopt获取由accept函数创建的某个已连接TCP套接字的IP选项时,返回的是在相应监听套接字上收到的客户SYN分节所在IP数据报中可能出现的源路径选项的逆转,源路径被TCP自动逆转顺序,因为由客户指定的是从客户到服务器的源路径,服务器需要在发送到客户的数据报中使用该路径的逆转,如果没有源路径伴随SYN分节,那么由getsockopt函数返回的第5个值-结果参数长度为0。对于所有其他TCP套接字、所有UDP套接字、原始IP套接字,调用getsockopt获取IP选项返回的是以前对于同一个套接字调用setsockopt设置的IP选项的一个副本。对于一个原始IP套接字,输入函数(读函数)总是返回包括IP选项在内的接收到的IP首部,因此到达的IP选项总是可得的。

源自Berkeley的内核从不为UDP套接字返回所收取的源路径选项或其他任何IP选项,TCPv2中所示的返回IP选项的代码从BSD 4.3 Reno以来一直存在,但它一直被注释掉,因为这段代码有问题,是无效的,这使得UDP接收进程不可能在发送响应IP数据报时使用接收路径的逆转。

许多源自Berkeley的内核在为原始IP套接字调用getsockopt或setsockopt时发生系统停机,普通用户无法使用该手段攻击系统,因为创建原始IP套接字要求具备超级用户权限,而超级用户权限拥有者本就可以对系统进行更为恶意的活动。

源路径是由IP数据报的发送者指定的一个IP地址列表,如果源路径是严格的,那么数据报只能逐一经过所列的节点,即列在源路径中的节点必须前后互为邻居,如果源路径是宽松的,那么数据报需要逐一经过所列节点,但两个节点之间可经过其他节点。

IPv4的源路由是有争议的,尽管它可能对网络排障非常有用,但也可能用于源地址欺骗等攻击中。[ Cheswick, Bellovin, and Rubin 2003 ]倡议在所有路由器上禁用此特性,许多组织机构和服务提供商也这么做了。源路由的合理用途之一是使用traceroute程序检测非对称的路径,但随着因特网上越来越多的路由器禁用源路由,这个用途也将消失。不论如何指定和收取源路径是套接字API的一部分。

IPv4源路径称为源和记录路径(source and record routes,SRR,其中LSRR表示宽松的选项,SSRR表示严格的选项),因为随着数据报逐一经过所列的节点,每个节点都把列在源路径中的自己的地址替换为外出接口的地址。SRR允许接收者逆转新的列表的顺序,得到沿相反方向回到发送者的路径。

我们把源路径指定为一个IPv4地址数组,并冠以3个单字节字段,下图就是我们传递给setsockopt函数的缓冲区的格式:

我们在源路径选项之前放置一个NOP选项,使得所有IP地址在各自的4字节边界对齐,这么做并非必须,但这样做无须占用额外空间(IP选项总是填充成4字节的倍数),还对齐了地址。如果我们没有在缓冲区开始处放置一个NOP,由于setsockopt函数设置IP_OPTIONS套接字选项时指定的缓冲区长度必须是4字节的倍数,因此我们可以在缓冲区末尾放置一个EOL(值为0的单个字节)。

上图我们展示了源路径最多有10个IP地址,但所列的第一个地址将在相应套接字的每个外出IP数据报即将离开源主机时被移出源路径选项,并成为IP数据报的目的地址。40字节的IP选项空间只能存放9个IP地址(还有3字节的选项首部所占空间)。

code字段对于LSRR为0x83,对于SSRR为0x89。len字段指定选项的字节长度,包括3字节选项首部和处于末尾的额外的目的IP地址(该地址不属于源路径),由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选项(如路由器警告)也是可能的,以下是这3个函数其中两个的实现:

c 复制代码
#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 */

// 函数inet_srcrt_init为构建一个源路径进行初始化
u_char *inet_srcrt_init(int type) {
    // 分配一个长44字节的缓冲区
    optr = Malloc(44);    /* NOP, code, len, ptr, up to 10 addresses */
    // EOL选项的值为0,清零操作把整个选项缓冲区初始化为EOL字节
    bzero(optr, 44);    /* guarantees EOLS at end */
    ocnt = 0;
    *optr++ = IPOPT_NOP;    /* NOP for alignment */
    *optr++ = type ? IPOPT_SSRR : IPOPT_LSRR;
    // 保存指向len字段的指针,以后每往地址列表中加入一个地址,就在该字段中存入新值
    lenptr = optr++;    /* we fill in length later */
    *optr++ = 4;    /* offset to first address */

    // 把指向选项缓冲区的指针返回给调用者,以便作为第4个参数传递给setsockopt函数
    return optr - 4;    /* pointer for setsockopt() */
}

// 向源路径中加入一个IPv4地址,参数指向一个主机名或点分十进制数串IP地址
int inet_srcrt_add(char *hostptr) {
    int len;
    struct addrinfo *ai;
    struct sockaddr_in *sin;

    if (ocnt > 9) {
        err_quit("too many source routes with: %s", hostptr);
    }

    // 调用自定义的host_serv函数转换主机名或点分十进制数串到二进制地址
    ai = Host_serv(hostptr, NULL, AF_INET, 0);
    sin = (struct sockaddr_in *)ai->ai_addr;
    // 把二进制地址存入地址列表
    memcpy(optr, &sin->sin_addr, sizeof(struct in_addr));
    freeaddrinfo(ai);

    // 更新len字段值
    optr += sizeof(struct in_addr);
    ++ocnt;
    len = 3 + (ocnt * sizeof(struct in_addr));
    *lenptr = len;
    // 返回缓冲区总长度,包括NOP,以便调用者把该长度作为第5个参数传递给setsockopt函数
    return len + 1;    /* size for setsockopt() */
}

以下是getsockopt函数返回给应用的接收到的源路径格式,它不同于图27-1所示的发送源路径格式:

返回给应用进程的地址顺序是所收取的源路径被内核逆转后的顺序,如果收到的源路径按顺序包含A、B、C、D 4个地址,该路径返回给应用的顺序(即逆转后的顺序)是D、C、B、A。如上图,前4个字节是该列表的第一个IP地址(即收到的源路径中的地址D),后跟一个单字节NOP(为了对齐),再跟以3字节源路径选项首部,最后再跟以其余的IP地址,3字节选项首部后最多可跟以9个IP地址,所返回首部中len字段的最大值为39。由于存在NOP做填充,因此由getsockopt函数返回的长度总是4字节的倍数。

上图格式在netinet/ip_var.h头文件中定义为如下结构:

以上getsockopt函数返回的源路径选项的结构不同于我们传递给setsockopt函数的格式,如果要把图27-4中的格式转换为27-1中的格式,我们需要对换头4个字节和随后4个字节,再给len字段加4,但我们并非必须这么做,源自Berkeley的实现对于TCP套接字自动使用来自SYN所在IP数据报的接收源路径的逆转,即我们不必调用setsockopt告诉内核使用该路径发送相应TCP连接上的外出IP数据报,内核会自动这么做,图27-4展示的由getsockopt函数返回的源路径信息纯粹用于了解目的。

以下函数显示一个接收到的源路径:

c 复制代码
void inet_srcrt_print(u_char *ptr, int len) {
    u_char c;
    char str[INET_ADDRSTRLEN];
    struct in_addr hop1;

    // 保存缓冲区中的第1个IP地址
    memcpy(&hop1, ptr, sizeof(struct in_addr));
    ptr += sizeof(struct in_addr);

    // 跳过后续的NOP
    while ((c = *ptr++) == IPOPT_NOP);    /* skip any leading NOPs */

    if (c == IPOPT_LSRR) {
        printf("received LSRR: ");
    } else if (c == IPOPT_SSRR) {
        printf("received SSRR: ");
    } else {
        printf("received option type %s\n", c);
        return;
    }
    printf("%s ", Inet_ntop(AF_INET, &hop1, str, sizeof(str)));

    // 不显示末尾的那个目的IP地址
    len = *ptr++ - sizeof(struct in_addr);    /* subtract dest IP addr */
    ++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回射服务器程序为显示一个接收源路径:

c 复制代码
#include "unp.h"

int main(int argc, char **argv) {
    int c, sockfd, len = 0;
    u_char *ptr = NULL;
    struct addrinfo *ai;

    if (argc < 2) {
        err_quit("usage: tcpcli01 [ -[gG] <hostname> ... ] <hostname>");
    }

    opterr = 0;    /* don't want getopt() writing to stderr */
    while ((c = getopt(argc, argv, "gG")) != -1) {
        switch (c) {
        case 'g':    /* loose source route */
            if (ptr) {
                err_quit("can't use both -g and -G");
            }
            // 初始化源路径
            ptr = inet_srcrt_init(0);
            break;

        case 'G':    /* strict source route */
            if (ptr) {
                err_quit("can't use both -g and -G");
            }
            // 初始化源路径
            ptr = inet_srcrt_init(1);
            break;
        
        case '?':
            err_quit("unrecognized option: %c", c);
        }
    }

    // 如果初始化成功,ptr不为空
    if (ptr) {
        // 把命令行指定的每个中间地址加到源路径中
        while (optind < argc - 1) {
            len = inet_srcrt_add(argv[optind++]);
        }
    // 否则如果剩余命令行参数不止一个,意味着用户指定了路径却没有指定其类型,显示错误消息并退出
    } else if (optind < argc - 1) {
        err_quit("need -g or -G to specify route");
    }

    // 最后一个参数是服务器主机的主机名或点分十进制数串地址
    if (optind != argc - 1) {
        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);
    // 如果用户指定了源路径,我们需要把服务器的IP地址加到地址列表末尾
    if (ptr) {
        len = inet_srcrt_add(argv[optind]);    /* dest at end */
        // 设置外出分节的IP首部选项字段
        Setsockopt(sockfd, IPPROTO_IP, IP_OPTIONS, ptr, len);
        free(ptr);
    }
    // connect函数将发起三路握手,我们期望SYN分节所在初始外出分组和后续外出分组都使用此源路径
    Connect(sockfd, ai->ai_addr, ai->ai_addrlen);
    str_cli(stdin, sockfd);    /* do it all */
    exit(0);
}

而TCP服务器程序几乎等同于第五章中的版本,只有两处改动,首先为IP选项分配空间:

c 复制代码
int len;
u_char *opts;

opts = Malloc(44);

然后在调用accept后,调用fork前获取并显示IP选项:

c 复制代码
len = 44;
Getsockopt(connfd, IPROTO_IP, IP_IPTIONS, opts, &len);
if (len > 0) {
    printf("received IP options, len = %d\n", len);
    inet_srcrt_print(opts, len);
}

如果所收取的来自客户SYN分节所在IP数据报中不包含IP选项,则由getsockopt函数返回的len变量结果将是0(len是一个值-结果参数)。为了使用所收取源路径的逆转作为回送数据报的选项,我们不必做任何事,这是由内核自动完成的。我们调用getsockopt只是为了获取逆转后的接收源路径的一个副本,如果不希望TCP使用此路径,我们可在accept函数返回后通过指定第5个参数(长度)为0调用setsockopt,从而去除当前正在用的IP选项。TCP已在三路握手的第二个分节所在IP数据报中使用接收源路径的逆转,但我们可以在accept函数返回后去除这些选项,去除后相应TCP连接中以后发送到客户的分组将由客户IP地址确定外出路径。

运行以上指定源路径的回射客户和服务器程序,我们在主机freebsd4上按以下方式运行客户:

该命令导致IP数据报从freebsd4转发到macosx,再转发回freebsd4,最后到达服务器主机macosx。macosx和freebsd4必须进行适当配置,以接受并转发源路由的数据报,从而使本例工作。

连接建立时服务器的输出如下:

172.24.37.94是freebsd4的IP,172.24.37.78是macosx的IP,可见显示的第一个IP地址是逆转路径的第一跳。

如果上例使用-G代替-g,则不会有任何变化,因为上例中所有系统都是邻居,因此严格的源路径等同于宽松的源路径。

不幸的是,IP_OPTIONS套接字选项的操作从未有过正式文档,因此在不是源自Berkeley源代码的系统上可能会有变化,例如,Solaris 2.5上由getsockopt函数返回的缓冲区的头部地址不是反转路径的第一跳的地址,而是对端主机的地址,但TCP使用的逆转路径仍是正确的。另外,Solaris 2.5总是在源路径选项前填充4个NOP,从而限制源路径最多有8个地址,而非9个。

但源路径对于单纯使用源IP地址进行认证的服务器程序来说存在一个安全漏洞,如果某个黑客作为客户发送的分组以一个受服务器信任的地址作为源地址,又把本地地址包括在源路径中,那么由服务器使用逆转后的接收源路径返回的分组将无需经过列在源路径中的所有节点就到达黑客的本地主机。从Net/1版本(一个早期的Unix操作系统版本)开始,rlogind和rshd这两个服务器程序就有了如下类似代码:

c 复制代码
u_char buf[44];
char lbuf[BUFSIZ];
int optsize;

optsize = sizeof(buf);
// 如果到达的已完成连接含有IP选项(即getsockopt函数返回的optsize不为0)
if (getsockopt(0, IPPROTO_IP, IP_OPTIONS, buf, &optsize) == 0 && optsize != 0) {
    /* format the options as hex numbers to print in lbuf[] */
    // 使用syslog函数登记一条消息
    syslog(LOG_NOTICE, "Connection received using IP options (ignored): %s", lbuf);
    // 调用setsockopt去除选项,防止该连接上以后发送的TCP分节使用接收源路径的逆转(实际由承载TCP分节的IP数据报使用)
    setsockopt(0, IPPROTO_IP, IP_OPTIONS, NULL, 0);
}

以上代码中,清除源路径选项的代码曾经是这样的:

c 复制代码
optsize = 0;
setsockopt(0, IPPROTO_IP, IP_OPTIONS, NULL, &optsize);

区别在于第5个参数是指向长度的指针,而非长度本身,这是一个bug,它可能在开始使用ANSI C原型时被修复,但这个bug是无害的,因为禁止IP_OPTIONS套接字选项既可指定一个空指针作为第4个参数,也可使用0值作为第5个参数。

以上代码中,getsockopt和setsockopt函数的描述符参数为0,这是由于rlogind是由inetd派生的,而描述符0正是通往客户的套接字。

以上代码所用技巧是不充分的,因为到应用进程接受该连接前,TCP三路握手已经完成,而三路握手的第二个分节已经沿所收取源路径的逆转回到客户(或回到了列在源路径中的某个黑客所在的中间节点),既然黑客已经看到两个方向上的TCP序列号,即使来自服务器的后续分组不再使用源路径发送,黑客仍能够以正确的序列号向服务器发送分组。

解决以上问题的唯一方法是:当使用源IP地址进行某种形式的认证时,禁止使用源路径到达的所有TCP连接。在以上给出的代码中,把setsockopt函数替换为关闭刚接受的连接并终止新派生的服务器,这样尽管三路握手的第二个分节已经送出,但连接不会仍然打开着。

IPv6首部后可跟如下几种可选的扩展首部:

1.步跳选项(hop_by_hop options)。如果有此选项,它必须紧跟40字节的IPv6首部。目前没有定义可供应用程序使用的此类选项。

2.目的地选项(destination options)。目前没有定义可供应用程序使用的此类选项。

3.路由首部(routing header)。类似于IPv4源路径选项。

4.分片首部(fragmentation header)。该首部由对IPv6数据报执行分片的主机自动产生,然后由最终目的主机在重组片段时处理。

5.认证首部(authentication header,AH)。该首部用法在RFC 2402中说明。

6.安全净荷封装(encapsulating security payload,ESP)。该首部用法在RFC 2406中说明。

其中分片首部完全由内核处理,AH和ESP这两个首部可以由内核基于SADB和SPDB自动处理,SADB和SPDB使用PF_KEY套接字维护(第十九章)。RFC 3542定义了指定和获取这些扩展首部的API。

步跳选项和目的地选项有类似的格式,如下图:

8位的下一个首部字段标识出跟在本扩展首部之后的下一个扩展首部。8位的首部扩展长度字段是本扩展首部的长度,以8字节为单位,但不包括它自己,例如,如果本扩展首部(不包含下一个首部字段)占8字节,其首部扩展长度字段值就为0,如果本扩展首部占16字节,其首部扩展长度字段值就为1。这两种首部都被填充成8字节的整数倍,所用填充方式有两种:pad1和padN。

步跳选项首部和目的地选项首部都容纳任意数量的个体选项,个体选项的格式如下:

个体选项的编排格式称为TVL编码,因为每个个体选项都由类型(type)、长度(length)、值(value)三个字段组成。8位的类型字段标识选项的类型,此外,该字段的高2位指定IPv6节点在不理解本选项时如何处理它:

1.00:跳过本个体选项,继续处理本首部。

2.01:丢弃本分组。

3.10:丢弃本分组,且不论本分组的目的地址是否为一个多播地址,均发送一个ICMPv6错误给发送者,ICMPv6消息类型为4,代码为2,下图是部分ICMPv6消息:

4.11:丢弃本分组,且只有当本分组的目的地址不是多播地址时,发送一个ICMPv6错误给发送者,ICMPv6消息类型为4,代码为2。

下一个高序位(第3高序位)指定本个体选项的数据在途中是否会有变化:

1.0:选项数据在途中无变化。

2.1:选项数据在途中可能变化。

低序5位指定选项本身,但低序5位不能标识一个选项,而是需要由高序3位共同标识,尽管如此,类型字段的赋值仍尽可能保持低序5位的唯一性。

8位的长度字段指定个体选项数据字段的字节长度,类型字段和本长度字段不计算在内。

pad1和padN两种填充方式定义在RFC 2460中,在步跳选项首部和目的地选项首部中都可使用。特大净荷长度是一个步跳选项,定义在RFC 2675中,它完全由内核在需要时产生,在收到时处理。路由器告警也是一个步跳选项,它定义在RFC 2711中,类似于IPv4的路由器告警。下图展示了这些选项:

pad1选项是唯一没有长度和值字段的选项,它提供1字节的填充。padN选项用于需要2个或多个字节填充的场合,对于2字节填充,本选项的长度字段为0,整个选项只由类型和长度这两个字段构成;对于3字节填充,本选项的长度字段值为1,后跟1字节的0值。特大净荷长度选项提供一个32位的数据报长度,用于展示16位净荷长度字段不够大的场合。路由器告警选项指示本分组应由沿途路由器截取,其值指出哪些路由器需关注本分组。

我们展示以上选项的原因在于,每个步跳选项和目的地选项都有一个对齐要求,可用xn+y表示,含义为这个个体选项必须出现在距离所在扩展首部开始处x字节整数倍加y字节的位置,例如,特大净荷长度选项的对齐要求是4n+2,该要求迫使4字节的选项值处于某个4字节边界,y值取2是因为在选项值字段之前有各1字节的类型字段和长度字段。路由器告警选项的对齐要求是2n+0,该要求迫使2字节的选项值处于某2字节边界。

步跳选项和目的地选项通常作为辅助数据通过sendmsg函数指定,并由recvmsg函数作为辅助数据返回。进程无需为发送这两类选项做任何特别之事,只需在某个sendmsg调用中指定它们。为了接收这两类选项,应用必须开启对应的套接字选项,步跳选项对应IPV6_RECVHOPOPTS,目的地选项对应IPV6_RECVDSTOPTS。允许这两类选项都返回的代码如下:

c 复制代码
const int on = 1;

setsockopt(sockfd, IPPROTO_IPV6, IPV6_RECVHOPOPTS, &on, sizeof(on));
setsockopt(sockfd, IPOROTO_IPV6, IPV6_RECVDSTOPTS, &on, sizeof(on));

下图展示了用于发送和接收步跳选项和目的地选项的辅助数据对象的格式:

这两类选项首部的实际内容作为辅助数据对象的cmsg_data部分在进程和内核之间传递,为了避免直接定义上图内容,相关API定义了7个用于创建和处理这些辅助数据对象数据部分的函数。以下4个函数用于构造待发送的选项:

inet6_opt_init函数返回容纳一个空扩展首部(即没有任何选项的首部)所需的字节数,如果extbuf参数非空,会在其指向的缓冲区中初始化这个扩展首部,此时,如果extlen参数不是8的倍数(所有IPv6步跳和目的地选项扩展首部必须是8的倍数),inet6_opt_init函数会返回-1。

inet6_opt_append函数返回添加指定的个体选项后的扩展首部总长度,如果extbuf参数非空,本函数会初始化该个体选项并按对齐要求插入必要的填充,如果所提供的的缓冲区放不下新选项,它就失败返回-1。offset参数是当前extbuf参数指向的缓冲区中扩展首部的总长度,该值必须是先前某个inet6_opt_init或inet6_opt_append调用的返回值。type和len参数分别指定了选项的类型和长度,并被直接复制到选项首部中。align参数指定对齐要求,即xn+y中的x值,而y值可由align参数和len参数算出,不用显式指定。databufp参数用于返回指向所添加选项值的填写位置的指针,调用者随后可使用inet6_opt_set_val函数或其他方法往这个位置复制选项值。

inet6_opt_finish函数用于结束一个扩展首部的设置,添加任何必要的填充,使得总长度为8字节的倍数。如果extbuf参数非空,就把填充真正插入缓冲区中,否则只是计算并返回新的总长度。offset参数是当前extbuf参数指向的缓冲区中扩展首部的总长度,该值必须是先前某个inet6_opt_init或inet6_opt_append调用的返回值。本函数返回已完成设置的扩展首部总长度,但如果所提供的缓冲区放不下所需的填充,则返回-1。

inet6_opt_set_val函数用于把给定的选项值复制到由inet6_opt_addend函数返回的数据缓冲区中。databuf参数是由inet6_opt_addend函数返回的指针。offset参数指定选项值要插入到缓冲区中的位置,该参数是前一个inet6_opt_set_val函数的返回值,首次调用时,该参数必须初始化为0。参数val和valen用于指定复制到选项值缓冲区中的值。

以上4个函数的期望用法是遍历两趟待添加的个体选项列表,第一趟用于计算预期的长度,第二趟用于把各个选项实际构造到大小合适的缓冲区中。两趟都是先调用inet6_opt_init,再为每个待添加的选项调用一次inet6_opt_append,最后以调用inet6_opt_finish结束。第一趟中传递给extbuf和extlen参数的值分别为NULL和0。第一趟结束后使用由inet6_opt_finish函数返回的大熊动态分配用于存放选项扩展首部的缓冲区,第二趟中就使用指向该缓冲区的一个指针及该缓冲区长度作为extbuf和extlen参数的值。第二趟中,每个选项的值或者手动复制,或者调用inet6_opt_set_val复制。我们也可预先分配一个足够大的缓冲区,从而省略掉第一趟,但缓冲区的大小有时不易预估,省略第一趟时有可能导致第二趟失败。

以下函数用于处理所接收的选项:

inet6_opt_next函数处理某缓冲区中的下一个选项。extbuf和extlen参数用于指定存放扩展首部的缓冲区。offset参数是当前extbuf参数指向的缓冲区中当前处理到的选项偏移,与inet6_opt_append函数的offset函数类似,首次调用本函数时应将offset参数指定为0,后续调用就使用前一个调用的返回值。typep、lenp、databufp参数分别用于返回当前处理到的选项的类型、长度、值。如果缓冲区中内容不符合选项扩展首部格式或已经到达该缓冲区末尾,函数就返回-1。

inet6_opt_find函数类似inet6_opt_next函数,但让调用者指定待搜索的选项类型(type参数)。

inet6_opt_get_val函数从databuf参数指定的某个选项中获取值(有些选项值中有多个字段)。databuf参数是由inet6_opt_next或inet6_opt_find函数返回的databufp参数指针。offset参数类似inet_6_opt_next函数,首次调用时需要用0作为参数值,后续调用使用前一个调用的返回值作为offset参数。

IPv6路由首部用于IPv6的源路由:

下一个首部和首部扩展长度字段与图27-7中的含义相同。路由类型字段当前只定义了一种类型,它的此字段值为0。剩余网段字段是所列节点中还有几个需要拜访(即尚未路由到)。

路由首部中可以出现的地址数目仅受限于分组允许长度等外在因素,剩余网段字段的值必须小于等于所列的地址数目。RFC 2460说明了一个具有路由首部的分组在发送到最终目的地的过程中,各个途径节点如何处理该路由首部的具体细节。

路由首部通常作为辅助数据由sendmsg函数指定,并由recvmsg函数作为辅助数据返回。应用发送此首部时,需要做的仅仅是在某个sendmsg调用中指定它。为了接收路由首部,应用进程需要开启IPV6_RECVRTHDR套接字选项:

c 复制代码
const int on = 1;
setsockopt(sockfd, IPPROTO_IPV6, IPV6_RECVRTHDR, &on, sizeof(on));

用于发送和接收路由首部的辅助数据对象的格式:

相关API为创建和处理路由首部定义了6个函数,以下3个函数用于构造待发送的路由首部:

inet6_rth_space函数返回容纳一个类型为type参数(该参数通常为IPV6_RTHDR_TYPE_0),网段总数为segments参数值的路由首部所需的字节数。

inet6_rth_init函数初始化由rthbuf参数指向的缓冲区,以容纳一个类型为type参数值,网段总数为segments参数值的路由首部。返回值是指向该缓冲区的指针,但如果发生错误(如所提供的缓冲区不够大)则为空指针。非空指针的返回值用作inet6_rth_add函数的第1个参数。

inet6_rth_add函数把addr参数指向的IPv6地址加到构建中的路由首部的末尾,调用成功时,该路由首部的剩余网段字段会被更新为新的地址数目。

以下3个函数用于处理所接收的路由首部:

inet6_rth_reverse函数的in参数所指缓冲区中存放的是某个接收路由首部,本函数将该路由首部逆转,并存放在由out参数所指的缓冲区中,以便接收进程沿逆转的路径发送回数据报。in和out参数可以指向同一个缓冲区,即原地逆转。

inet6_rth_segments函数返回由rhbuf所指路由首部中的网段数目,调用成功时返回值应大于0。

inet6_rth_getaddr函数用于返回由rthbuf参数所指的路由首部中,索引号为index的那个IPv6地址,返回值是指向该地址所在位置的指针。index参数的值必须在0和inet6_rth_segments函数的返回值减去1为界限的闭区间内。

为展示IPv6路由首部的用法,我们编写一对UDP客户程序和服务器程序,客户程序从命令行接受一个源路径,服务器程序显示所接收IPv6数据报的接收源路径,再把该数据报沿接收源路径的逆转发送回客户。以下是客户程序:

c 复制代码
#include "unp.h"

int main(int argc, char **argv) {
    int sockfd, len = 0;
    u_char *ptr = NULL;
    struct addrinfo *ai;

    if (argc < 2) {
        // 如果提供的主机名参数不止一个,那么源路径由除最后一个之外的所有参数构成
        err_quit("usage: udpcli01 [ <hostname> ... ] <hostname>");
    }

    if (argc > 2) {
        int i;
        
        // 首先调用inet6_rth_space确定创建路由首部需要多大空间
        len = Inet6_rth_space(IPV6_RTHDR_TYPE_0, argc - 2);
        // 分配所需空间
        ptr = Malloc(len);
        // 初始化所分配的缓冲区
        Inet6_rth_init(ptr, len, IPV6_RTHDR_TYPE_0, argc - 2);
        for (i = 1; i < argc - 1; ++i) {
            // 对源路径中每个地址先调用host_serv把它转换成in6_addr结构
            ai = Host_serv(argv[i], NULL, AF_INET6, 0);
            // 把该源路径添加到构建中的源路径中
            Inet6_rth_add(ptr, &((struct sockaddr_in6 *)ai->ai_addr)->sin6_addr);
        }
    }

    // 获取目的主机名的套接字地址结构
    ai = Host_serv(argv[argc - 1], SERV_PORT_STR, AF_INET6, SOCK_DGRAM);

    sockfd = Socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);

    if (ptr) {
        // 通过设置IPV6_RTHDR套接字选项可以把一个路由首部应用于从某个套接字发送的所有分组
        // 以取代为每个分组发送同样的辅助数据的做法
        Setsockopt(sockfd, IPPROTO_IPV6, IPV6_RTHDR, ptr, len);
        free(ptr);
    }

    dg_cli(stdin, sockfd, ai->ai_addr, ai->ai_addrlen);    /* do it all */

    exit(0);
}

服务器程序比较简单,它打开一个UDP套接字并调用dg_echo,我们不给出其main函数,但给出它调用的dg_echo函数版本,该函数如果收到一个携带源路径的分组,就打印这个源路径,并逆转它用于回射该分组:

c 复制代码
#include "unp.h"

void dg_echo(int sockfd, SA *pcliaddr, socklen_t clilen) {
    int n;
    char mesg[MAXLINE];
    int on;
    char control[MAXLINE];
    struct msghdr msg;
    struct cmsghdr *cmsg;
    struct iovec iov[1];

    // 为接收外来源路径,必须开启IPV6_RECVRTHDR套接字选项
    on = 1;
    Setsockopt(sockfd, IPPROTO_IPV6, IPV6_RECVRTHDR, &on, sizeof(on));

    // 设置用于接收外来源路径的msghdr结构中不变的字段
    bzero(&msg, sizeof(msg));
    iov[0].iov_base = mesg;
    msg.msg_name = pcliaddr;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;
    msg.msg_control = control;
    for (; ; ) {
        // msghdr结构中的以下3项会被recvmsg函数修改,每次调用recvmsg前重置它们
        msg.msg_namelen = clilen;
        msg.msg_controllen = sizeof(control);
        iov[0].iov_len = MAXLINE;
        n = Recvmsg(sockfd, &msg, 0);

        // 遍历辅助数据以寻找路由首部
        for (cmsg = CMSG_FIRSTHDR(&msg); cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
            if (cmsg->cmsg_level == IPPROTO_IPV6 && cmsg->cmsg_type == IPV6_RTHDR) {
                // 调用inet6_srcrt_print打印接收到的源路径
                inet6_srcrt_print(CMSG_DATA(cmsg));
                // 逆转接收到的源路径,以便沿同样的路径返送所接收的分组,此处原地逆转路径
                Inet6_rth_reverse(CMSG_DATA(cmsg), CMSG_DATA(cmsg));
            }
        }

        // 设置回送数据的长度,然后回射所接收的分组
        iov[0].iov_len = n;
        Sendmsg(sockfd, &msg, 0);
    }
}

inet6_srcrt_print函数:

c 复制代码
#include "unp.h"

void inet6_srcrt_print(void *ptr) {
    int i, segments;
    char str[INET6_ADDRSTRLEN];

    // 调用inet6_rth_segments确定源路径中存在的网段数
    segments = Inet6_rth_segments(ptr);
    printf("received source route: ");
    for (i = 0; i < segments; ++i) {
        // 调用inet_ntop把IP地址从网络字节序(in6_addr结构)转换为点分十进制格式
        printf("%s ", Inet_ntop(AF_INET6, Inet6_rth_getaddr(ptr, i), str, sizeof(str)));
    }
    printf("\n");
}

处理IPv6原路径的客户和服务器程序无需了解源路径在分组中是如何格式化的,API提供的库函数隐藏了分组格式的细节。

我们已经讲解了sendmsg和recvmsg函数发送和接收的7种IPv6辅助数据对象:

1.IPv6分组信息:in6_pktinfo结构或者包含目的地址和外出接口索引,或者包含源地址和到达接口索引。(第二十二章)

2.外出跳限或接收跳限。(第二十二章)

3.下一跳地址。只能发送不能接受。(第二十二章)

4.外出流通类别或接收流通类别。(第二十二章)

5.步跳选项。(第二十七章)

6.目的地选项。(第二十七章)

7.路由首部。(第二十七章)

这些辅助数据对象可通过设置相应的套接字选项,使得从某个套接字发送的所有分组都使用相同的辅助数据值,这些套接字选项所用常值与辅助数据对象一致,即调用setsockopt的level参数总是IPPROTO_IPV6,选项名参数按以上IPv6辅助数据列出的顺序分别为:IPV6_PKTINFO、IPV6_HOPLIMIT、IPV6_NEXTHOP、IPV6_TCLASS、IPV6_HOPOPTS、IPV6_DSTOPTS、IPV6_RTHDR。对于UDP套接字和原始IPv6套接字,我们可以通过在sendmsg调用中指定相应辅助数据,使得针对每个分组覆写这些粘附性选项。

粘附性选项的概念也适用于TCP,由于TCP套接字上不能使用sendmsg或recvmsg函数发送或接收辅助数据,可通过设置相应套接字选项指定辅助数据对象,这些对象随后影响在相应套接字上发送的所有分组。但如果某个分组需要重传,且发送原分组和重传的分组之间粘附性选项的设置发生变更,那么重传的分组上的粘附性选项既可能是旧的,也可能是新的。

希望在某个套接字上调用recvmsg接收辅助数据的进程,必须预先在该套接字上开启相应的套接字选项,选项名参数按以上IPv6辅助数据列出的顺序(跳过了下一跳地址选项,因为该选项不能接收只能发送)分别为:IPV6_RECVPKTINFO、IPV6_RECVHOPLIMIT、IPV6_RECVTCLASS、IPV6_RECVHOPOPTS、IPV6_RECVDSTOPTS、IPV6_RECVRTHDR。TCP应用也能使用同样的方式获取这些辅助数据对象,但TCP套接字上不能使用recvmsg函数与用户数据一道接收辅助数据,由recvmsg函数返回的这些粘附性选项实际来自最近收取的分节所在IPv6分组,这些选项除非对端TCP修改过,否则所有IPv6分组都具有相同的选项。

RFC 2292定义了本章中IPv6 API的一个早期版本。

ping程序会创建一个原始套接字,能通过recvfrom函数读入每个数据报的完整IP首部。

相关推荐
thesky1234566 分钟前
活着就好20241224
学习·算法
神的孩子都在歌唱10 分钟前
TCP/IP 模型中,网络层对 IP 地址的分配与路由选择
网络·tcp/ip·智能路由器
阿雄不会写代码13 分钟前
ubuntu安装nginx
linux·服务器·网络
蜗牛hb15 分钟前
VMware Workstation虚拟机网络模式
开发语言·学习·php
汤姆和杰瑞在瑞士吃糯米粑粑30 分钟前
【C++学习篇】AVL树
开发语言·c++·学习
starstarzz36 分钟前
计算机网络实验四:Cisco交换机配置VLAN
网络·计算机网络·智能路由器·vlan·虚拟局域网
虾球xz42 分钟前
游戏引擎学习第58天
学习·游戏引擎
奶香臭豆腐1 小时前
C++ —— 模板类具体化
开发语言·c++·学习
网安墨雨2 小时前
常用网络协议
网络·网络协议
波音彬要多做2 小时前
41 stack类与queue类
开发语言·数据结构·c++·学习·算法