首页 > 代码库 > Linux内核分析 - 网络[十四]:IP选项
Linux内核分析 - 网络[十四]:IP选项
Linux内核分析 - 网络[十四]:IP选项
版权声明:本文为博主原创文章,未经博主允许不得转载。
内核版本:2.6.34
在发送报文时,可以调用函数setsockopt()来设置相应的选项,本文主要分析IP选项的生成,发送以及接收所执行的流程,选取了LSRR为例子进行说明,主要分为选项的生成、选项的转发、选项的接收三部分。
先看一个源站路由选项的例子,下文的说明都将以此为例。
主机IP:192.168.1.99
源路由:192.168.1.1 192.168.1.2 192.168.1.100[dest ip]
源站路由选项在各个主机上的情况:
该图与<TCP/IP卷一>上的示例不同,因为这里的选项[#R1, R2, D]是以实际传输中的形式标注的,下图是源站路由选项在此过程中的具体形式:
创建socket时,可以使用setsockopt()来设置创建socket的各种属性,setsockopt()最终调用系统接口sys_setsockopt()。
sys_setsockopt()
level(级别)指定系统中解释选项的代码:通用的套接口代码,或某个特定协议的代码。level==SOL_SOCKET是通用的套接口选项,即不是针对于某个协议的套接口的,使用通过函数sock_setsockopt()来设置选项;level其它值:IPPROTO_IP, IPPROTO_ICMPV6, IPPROTO_IPV6则是特定协议套接口的,使用sock->ops->setsockopt(套接字特定函数)来设置选项。
- if (level == SOL_SOCKET)
- err = sock_setsockopt(sock, level, optname, optval, optlen);
- else
- err = sock->ops->setsockopt(sock, level, optname, optval, optlen);
下面具体说明这个例子,生成选项 - 使用setsockopt()可以设置IP选项,形式如下:
- setsockopt(fd, IPPROTO_IP, IP_OPTIONS, &opt, optlen);
其中传入的opt格式如下:
无论是何种报文(对应不同的sock),设置IP选项最终都会调用ip_setsockopt()。比如创建的UDP socket,则调用流程为:sock->ops->setsockopt() => udp_setsockopt() -> ip_setsockopt()。而处理IP选项的主要是由do_ip_setsockopt()来完成的。
do_ip_setsockopt() 处理ip选项
根据optname来决定处理何种类型的选项,决定setsockopt()中参数的optval如何解释。当是IP_OPTIONS时为IP选项,按IP选项来处理optval。
- switch (optname) {
- case IP_OPTIONS:
ip_options_get_from_use()根据用户传入值optval生成选项结构opt,xchg()这句将inet->opt和opt进行了交换,即将opt赋值给了inet->opt,同时将inet->opt作为结果返回。
- err = ip_options_get_from_user(sock_net(sk), &opt, optval, optlen);
- opt = xchg(&inet->opt, opt);
- kfree(opt);
ip_options_get_from_user()
分配内存给IP选项,struct ip_options记录了选项相关的一些内部数据结构,最后的属性__data[0]才指向真正的IP选项。因此在分配空间时是struct ip_options大小加上optlen大小,当然,还要做4字节对齐。
- struct ip_options *opt = ip_options_get_alloc(optlen);
- static struct ip_options *ip_options_get_alloc(const int optlen)
- {
- return kzalloc(sizeof(struct ip_options) + ((optlen + 3) & ~3), GFP_KERNEL);
- }
分配空间后,拷贝用户设置的IP选项到opt->__data中;最后调用ip_options_get_finish()完成选项的处理,包括了用户传入选项的再处理、一些内部数据的填写,下面会进行详细讲解。
- copy_from_user(opt->__data, data, optlen);
- return ip_options_get_finish(net, optp, opt, optlen);
ip_options_get_finish()
选项头部的空字节用IPOPT_NOOP来补齐,选项尾部的空字节用IPOPT_END来补齐,IPOPT_NOOP和IPOPT_END都占用1字节,因此optlen递增,记录选项长度到opt中。然后调用ip_options_compile()。
- while (optlen & 3)
- opt->__data[optlen++] = IPOPT_END;
- opt->optlen = optlen;
ip_options_compile()实际完成选项的处理,它在两个地方被调用:生成带IP选项的报文时被调用,此时处理的是用户传入的选项;接收带有IP选项的报文时被调用,此时处理的是报文中的IP选项,下面详细看下该函数,以LSRR选项为例子。
- ip_options_compile(net, opt, NULL);
- kfree(*optp);
- *optp = opt;
ip_options_compile()
这里对应于该函数应用的两种情况:
1. 如果是生成带IP选项的报文,传入的参数skb为空(此时skb还没有创建),optptr指向opt->__data,而上面已经看到用户设置的选项在函数ip_options_get_from_user()中被拷贝到其中;
2. 如果接收到带IP选项的报文,传入skb不为空(收到报文时就创建了),optptr指向报文中IP选项的位置。iph指向IP报头的位置,当然,如果是生成选项,iph所指向的位置是没有意义的。
- if (skb != NULL) {
- rt = skb_rtable(skb);
- optptr = (unsigned char *)&(ip_hdr(skb)[1]);
- } else
- optptr = opt->__data;
- iph = optptr - sizeof(struct iphdr);
IP选项是按[code, len, ptr, data]这样的块排列的,每个块代表一个选项内容,多个选项可以共存,每个块4字节对齐,不足的用IPOPT_NOOP补齐。for循环处理每个选项,其中IPOPT_END和IPOPT_NOOP只是特殊的占位符,需要另外处理。然后按照选项块的格式,取出选项长度len到optlen,再根据选项的code分别进行处理,可以看到获取选项块长度的代码段在IPOPT_END和IPOPT_NOOP之后。
- for (l = opt->optlen; l > 0; ) {
- switch (*optptr) {
- case IPOPT_END: ….
- case IPOPT_NOOP: ...
- …...
- optlen = optptr[1];
- if (optlen<2 || optlen>l) {
- pp_ptr = optptr;
- goto error;
- }
- case …...
- …...// 处理代码段
- }
- l -= optlen;
- optptr += optlen;
- }
还是以宽松源路由为例子:
- case IPOPT_LSRR:
首先会作一些检查,选项长度optlen不能比3小,到少有3字节的头部:code, len, ptr。指针ptr不能比4小,因为头部就有4字节。这里optlen是去除了头部的IPOPT_NOOP后的长度,而ptr的计算是包括IPOPT_NOOP的,因此一个是3一个是4;另外,选项中只能有一个源路由选项,因此当srr有值时,表示正在处理的是第二个源路由选项,则有错误。
- if (optlen < 3) {
- pp_ptr = optptr + 1;
- goto error;
- }
- if (optptr[2] < 4) {
- pp_ptr = optptr + 2;
- goto error;
- }
- /* NB: cf RFC-1812 5.2.4.1 */
- if (opt->srr) {
- pp_ptr = optptr;
- goto error;
- }
当skb==NULL,对应于第一种情况(生成报文选项时);取出源路由选项的第一跳,记录到选项opt的faddr中,作为下一跳地址;源路由选项依次前移。对应于开头给出的例子,这里处理后结果如图所示:
- if (!skb) {
- if (optptr[2] != 4 || optlen < 7 || ((optlen-3) & 3)) {
- pp_ptr = optptr + 1;
- goto error;
- }
- memcpy(&opt->faddr, &optptr[3], 4);
- if (optlen > 7)
- memmove(&optptr[3], &optptr[7], optlen-7);
- }
最后记录,is_strictroute是否是严格的路由选路,srr表示选项到IP报头的距离,同样,它只对处理收到的报文中选项时有效。
- opt->is_strictroute = (optptr[0] == IPOPT_SSRR);
- opt->srr = optptr - iph;
以上是关于IP选项报文的生成,下面从ip_rcv()来看IP选项报文的接收。
ip_rcv() -> ip_rcv_finish()
ip_rcv()中重置IP的控制数据struct inet_skb_param为0,在IP章节已经说过,控制数据是skb中48字节的一个字段,在各层协议中含义不同,在IP层,它被解释为inet_skb_parm,包含opt和flags,其中前者与IP选项有关。
- memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
- struct inet_skb_parm {
- struct ip_options opt; /* Compiled IP options */
- unsigned char flags;
- };
ip_rcv_finish()中如果头部长度字段ihl大于4,则表示含有IP选项,此时调用ip_rcv_optins()来接收IP选项。
- if (iph->ihl > 5 && ip_rcv_options(skb))
- goto drop;
ip_rcv_options()
iph指向IP头;opt指向控制数据的opt,对IP选项处理的结构会存放在此,作为skb的一部分,在其它地方起作用;设置opt->optlen选项长度,这里的长度包括了开头的IPOPT_NOOP字段,是4的整数倍。
- iph = ip_hdr(skb);
- opt = &(IPCB(skb)->opt);
- opt->optlen = iph->ihl*4 - sizeof(struct iphdr);
调用ip_options_compile()处理选项,这是该函数被调用的第二种情况(收到带IP选项报文时),传入参数skb是报文的skb,函数的详细说明见上文(还是以LSRR为例),实际上ip_options_compile()在这种情况下只相应设置了opt->is_strictroute和opt->srr,而不像在生成选项时对IP选项进行处理,对接收到IP选项的处理要留带到发送报文时。
- if (ip_options_compile(dev_net(dev), opt, skb)) {
- IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
- goto drop;
- }
如果是LSRR,opt->srr在上一步中被设置,为选项到报头的距离,对于带SSRR或LSRR选项的报文来说,opt->srr值不为0,进入调用ip_options_rcv_srr()完成LSRR选项的处理。
- if (unlikely(opt->srr)) {
- ……
- if (ip_options_rcv_srr(skb))
- goto drop;
- }
- return 0;
ip_options_rcv_srr()
该函数的主要作用是根据源站选项重新设置skb的路由项,从而改变报文的正常流程。它不会对选项进行其它操作,真正的操作在发送时完成。
首先会进行一些检查,报文的目的MAC必须是本主机,这里检查skb->pkt_type==PACKET_HOST;如果报文的目的IP不是本机(而是在本机的邻居),则本主只是源路径的一个中转站,此时不用再次查找路由表,直接返回,这里检查rt->rt_type==RTN_UNICAST,这种情况在LSRR中是允许的,SSRR是不允许的;如果报文的目的IP对本机来说不是直接可达,则错误返回。
- if (skb->pkt_type != PACKET_HOST)
- return -EINVAL;
- if (rt->rt_type == RTN_UNICAST) {
- if (!opt->is_strictroute)
- return 0;
- icmp_send(skb, ICMP_PARAMETERPROB, 0, htonl(16<<24));
- return -EINVAL;
- }
- if (rt->rt_type != RTN_LOCAL)
- return -EINVAL;
从LSRR选项中取出下一跳地址,记录到nexthop中,并查询路由表从saddr到nexthop的路由项,记录到skb中。如果没有这样的路由项,则返回错误;如果有这样的路由项且不是本机(如果下一跳是本机,则表示报文到达目的主机了),则break跳出循环;如果下一跳就是本机,则拷贝下一跳地址到iph->daddr中。
需要注意的是这里重新查找了一次路由表(ip_route_input)。而我们知道,在IP层会查找路由表(ip_rcv_finish函数中),它决定报文是否该被接收还是该被转发。而这里重查一次路由表也是源站选项的意义所在,IP报头中的目的地址并不是最终地址,它只决定路径中的一站,真正的目的地由选项中的值决定,因此需要根据选项中的值作为目的地址再查找一次,以便决定接下来的动作,用查找到的路由项rt2作为报文skb的路由项。
- for (srrptr=optptr[2], srrspace = optptr[1]; srrptr <= srrspace; srrptr += 4) {
- memcpy(&nexthop, &optptr[srrptr-1], 4);
- rt = skb_rtable(skb);
- skb_dst_set(skb, NULL);
- err = ip_route_input(skb, nexthop, iph->saddr, iph->tos, skb->dev);
- rt2 = skb_rtable(skb);
- if (err || (rt2->rt_type != RTN_UNICAST && rt2->rt_type != RTN_LOCAL)) {
- ip_rt_put(rt2);
- skb_dst_set(skb, &rt->u.dst);
- return -EINVAL;
- }
- ip_rt_put(rt);
- if (rt2->rt_type != RTN_LOCAL)
- break;
- /* Superfast 8) loopback forward */
- memcpy(&iph->daddr, &optptr[srrptr-1], 4);
- opt->is_changed = 1;
- }
IP选项中的srr_is_hit和is_changed含义是不同的,srr_is_hit表示下一跳地址是从源路由选项中提取的,换言之,本机仍不是目的主机;is_changed表示IP报头是否被改变,被改变的话就需要重新计算IP报头的校验和(这里由于IP选项LSRR可能会改变IP报头的目的地址或选项LSRR中的值)。
- if (srrptr <= srrspace) {
- opt->srr_is_hit = 1;
- opt->is_changed = 1;
- }
根据ip_options_rcv_srr()处理的结果,即再次查询路由表的结果rt2,决定报文是进行转发还是进行接收。转发的话input=ip_forward(),表明主机只是到达目的地址的中转站;接收的话,input=ip_local_deliver(),表明主机是目的地址。
先看转发的情况,主机只是到达目的地址的中转站,调用ip_forward() -> ip_forward_finish() -> ip_forward_options(),该函数完成IP选项的处理。
ip_forward_options()
optptr指向IP选项头的位置,其中的for循环找出LSRR选项中与路由项下一跳地址rt->rt_dst相同的选项,记录在srrptr中。ip_rt_get_source()将本机地址填入LSRR选项(源站选项要求用主机的地址取代选项中的地址),然后设置IP报头的目的地址为LSRR选项中的下一跳地址,最后LSRR中指针optptr[2]右移4个字节。
- if (opt->srr_is_hit) {
- int srrptr, srrspace;
- optptr = raw + opt->srr;
- for ( srrptr=optptr[2], srrspace = optptr[1]; srrptr <= srrspace; srrptr += 4 ) {
- if (srrptr + 3 > srrspace)
- break;
- if (memcmp(&rt->rt_dst, &optptr[srrptr-1], 4) == 0)
- break;
- }
- if (srrptr + 3 <= srrspace) {
- opt->is_changed = 1;
- ip_rt_get_source(&optptr[srrptr-1], rt);
- ip_hdr(skb)->daddr = rt->rt_dst;
- optptr[2] = srrptr+4;
- } else if (net_ratelimit())
- printk(KERN_CRIT "ip_forward(): Argh! Destination lost!\n");
- ……
- }
还是以开头的例子为例,在主机192.168.1.2上收到来自192.168.1.1的报文,最后转发出去的报文选项如下图所示:
再看接收的情况,主机是报文的最终地址,调用ip_local_deliver()像处理正常IP报文一样处理该报文,接下来的流程与”IP协议”章节中描述的一样。最终主机192.168.1.100收到的报文选项如下图所示:
总结:
生成源站路由选项时,最后两项地址是相同的,都是192.168.1.100
源站路由实现是依靠两次路由查找改变了报文的流程
源站路由的更改需要重新计算校验和
Linux内核分析 - 网络[十四]:IP选项