在以前的笔记中讲过ip数据报的处理,里面提到过ip_rcv_finish这个函数,这个函数会调用ip_rcv_options来解析并处理iP首部中的ip选项。
if (iph->ihl > 5 && ip_rcv_options(skb)) goto drop;
在ip_rcv_finish中会判断ip首部长度是否大于5,只有首部长度大于20的情况下才会有ip选项,并调用ip_rcv_options处理;
static inline int ip_rcv_options(struct sk_buff *skb) { ...... if (skb_cow(skb, skb_headroom(skb))) { IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INDISCARDS); goto drop; } opt = &(IPCB(skb)->opt); opt->optlen = iph->ihl*4 - sizeof(struct iphdr); if (ip_options_compile(dev_net(dev), opt, skb)) { IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS); goto drop; } if (unlikely(opt->srr)) { struct in_device *in_dev = in_dev_get(dev); if (in_dev) { if (!IN_DEV_SOURCE_ROUTE(in_dev)) { if (IN_DEV_LOG_MARTIANS(in_dev) && net_ratelimit()) printk(KERN_INFO "source route option %pI4 -> %pI4\n", &iph->saddr, &iph->daddr); in_dev_put(in_dev); goto drop; } in_dev_put(in_dev); } if (ip_options_rcv_srr(skb)) goto drop; } return 0; drop: return -1; }
首先会确保数据报足够的头部空间
紧接着会获取skb中ip选项,以及ip选项的长度
接着调用ip_options_compile()解析skb的IP首部中IP选项到skb的cb中,IP层的私有数据cb为一个IP选项信息块。
如果存在源路由选项,并且系统允许接收带源路由选项的数据包就会调用ip_options_rcv_srr处理,否则就会丢弃该报文;
下面来看下ip_options_compile是怎么解析ip选项报文的
ip_options_compile()会被两个函数调用:ip_options_get_finish()和 ip_rcv_options(),分别对应了发送和接收两个方向。注意: 在发送时的调用方式是ip_option_compile(opt,NULL),而在接收 时其调用语句是ip_option_compile(NULL,skb),这是因为发送 和接收时,待解析的IP选项以及解析后的IP选项信息块 * 所存储的位置是不同的--发送时IP选项时存储在参数opt 的__data字段起始的区域中,解析得到的信息会保存在opt中;接收时IP选项存储在参数skb中指向的IP首部中, 解析得到的信息则保存在SKB的cb中。因此,opt和skb两者不能同时为NULL
int ip_options_compile(struct net *net, struct ip_options * opt, struct sk_buff * skb) { ...... if (skb != NULL) { rt = skb_rtable(skb); optptr = (unsigned char *)&(ip_hdr(skb)[1]); } else optptr = opt->__data; ...... for (l = opt->optlen; l > 0; ) { switch (*optptr) { case IPOPT_END: for (optptr++, l--; l>0; optptr++, l--) { if (*optptr != IPOPT_END) { *optptr = IPOPT_END; opt->is_changed = 1; } } goto eol; case IPOPT_NOOP: l--; optptr++; continue; } switch (*optptr) { case IPOPT_SSRR: case IPOPT_LSRR: /* * 校验待处理源路由选项的 * 长度是否有效。 */ 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 */ /* * IP选项信息块opt中源路由选项 * 若已处理过,则无需再处理。 */ if (opt->srr) { pp_ptr = optptr; goto error; } /* * 显然这是针对发送的,先再次校验选项中 * 的指针及长度的有效性:对于选项指针值其 * 最小值为4,对于选项长度值,除了选项 * 类型、选项长度以及选项指针的三字节外, * 至少应该可以容纳一个IP地址,且扣除了 * 前面的三字节4字节对齐。作为发送方, * 应取出第一个地址作为下一跳地址,并在 * 路径列表中多于一个地址时,将剩余的所有 * 地址往前移动一个位置。 */ 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); } /* * 根据选项类型标识是不是严格源路由 * 选项,并记录源路由选项在IP首部中 * 的偏移量。 */ opt->is_strictroute = (optptr[0] == IPOPT_SSRR); opt->srr = optptr - iph; break; ...... }
这里只列出了源路由选项的处理。
在判断skb是否为空的时候就指定这是为发送构建ip选项还是接收ip数据报时处理ip选项。如果skb为空说明是发送,这时是构建ip选项到参数opt的_data中。否则就是解析skb的cb;
紧接着就是一个循环处理ip选项,这个for循环是为接收ip报文解析skb报文中的ip选项进行处理;发送会在eol处退出这个函数。
解析ip选项的时候如果如果检测到选项列表结束符,则将后面所剩余的全部空间都设置为结束符,因为修改了选项内容,从而需要计算校验和,因此设置opt->is_changed为1,然后结束解析返回。如果检测到空操作符,则修改循环变量l,并将指针移动到下一个选项处后,直接跳入下一次循环。
而后面处理源路由选项的时候就需要对照源路由选项的格式看。这样容易理解点。源码中的注释也讲的比较清楚,这里就不罗嗦了。
ip_options_rcv_srr函数检查输入数据报中的源路由信息,并根据源路由选项更新ip数据报的下一跳地址
int ip_options_rcv_srr(struct sk_buff *skb) { ...... struct ip_options *opt = &(IPCB(skb)->opt); unsigned char *optptr = skb_network_header(skb) + opt->srr; struct rtable *rt = skb_rtable(skb); ...... if (!opt->srr) return 0; ...... for (srrptr=optptr[2], srrspace = optptr[1]; srrptr <= srrspace; srrptr += 4) { /* * 校验源路由选项的路径列表是否还能至少容纳 * 下一个IP地址值,如果不能,则给发送方发送 * 一个参数问题ICMP差错报文。 */ if (srrptr + 3 > srrspace) { icmp_send(skb, ICMP_PARAMETERPROB, 0, htonl((opt->srr+2)<<24)); return -EINVAL; } /* * 通过输入路由方式来判断是否抵达源路由选项中 * 的某一站,一旦确定本地为源路由选项中的某一 * 站,则获取下一跳的IP地址作为IP数据包的目的地址, * 并设置is_changed,表示该IP数据包作了修改。 */ 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 * 标志,标识需重新计算IP数据包的首部校验和。 */ if (srrptr <= srrspace) { opt->srr_is_hit = 1; opt->is_changed = 1; } }
首先ip_options_rcv_srr会进行一些有效性的检查,最后会通过一个for循环来查找源路由选项,如果找到了。就根据找到的地址信息去路由表和路由缓存信息中查找,如果查找到就设置下一跳目的地址为源路由地址
udp和raw套接口都是通过控制信息来生成ip选项。通过ip_options_get函数完成这个功能,最后还是调用ip_options_compile来处理。