现在的位置: 首页 > 综合 > 正文

linux内核学习笔记——ip选项处理(二)

2014年09月05日 ⁄ 综合 ⁄ 共 4248字 ⁄ 字号 评论关闭

在以前的笔记中讲过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来处理。

抱歉!评论已关闭.