Linux内核分析 - 网络[十四]:IP选项

时间:2021-11-29 21:18:35
2012-04-25 17:14 5639人阅读 评论(1) 收藏 举报
Linux内核分析 - 网络[十四]:IP选项 分类:
内核协议栈(22) 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]
      源站路由选项在各个主机上的情况:

Linux内核分析 - 网络[十四]:IP选项

该图与<TCP/IP卷一>上的示例不同,因为这里的选项[#R1, R2, D]是以实际传输中的形式标注的,下图是源站路由选项在此过程中的具体形式:

Linux内核分析 - 网络[十四]:IP选项

创建socket时,可以使用setsockopt()来设置创建socket的各种属性,setsockopt()最终调用系统接口sys_setsockopt()。
sys_setsockopt()
      level(级别)指定系统中解释选项的代码:通用的套接口代码,或某个特定协议的代码。level==SOL_SOCKET是通用的套接口选项,即不是针对于某个协议的套接口的,使用通过函数sock_setsockopt()来设置选项;level其它值:IPPROTO_IP, IPPROTO_ICMPV6, IPPROTO_IPV6则是特定协议套接口的,使用sock->ops->setsockopt(套接字特定函数)来设置选项。

  1. if (level == SOL_SOCKET)
  2. err = sock_setsockopt(sock, level, optname, optval, optlen);
  3. else
  4. err = sock->ops->setsockopt(sock, level, optname, optval, optlen);

下面具体说明这个例子,生成选项 - 使用setsockopt()可以设置IP选项,形式如下:

  1. setsockopt(fd, IPPROTO_IP, IP_OPTIONS, &opt, optlen);

其中传入的opt格式如下:

Linux内核分析 - 网络[十四]:IP选项

无论是何种报文(对应不同的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。

  1. switch (optname) {
  2. case IP_OPTIONS:

ip_options_get_from_use()根据用户传入值optval生成选项结构opt,xchg()这句将inet->opt和opt进行了交换,即将opt赋值给了inet->opt,同时将inet->opt作为结果返回。

  1. err = ip_options_get_from_user(sock_net(sk), &opt, optval, optlen);
  2. opt = xchg(&inet->opt, opt);
  3. kfree(opt);

ip_options_get_from_user()
      分配内存给IP选项,struct ip_options记录了选项相关的一些内部数据结构,最后的属性__data[0]才指向真正的IP选项。因此在分配空间时是struct ip_options大小加上optlen大小,当然,还要做4字节对齐。

  1. struct ip_options *opt = ip_options_get_alloc(optlen);
  2. static struct ip_options *ip_options_get_alloc(const int optlen)
  3. {
  4. return kzalloc(sizeof(struct ip_options) + ((optlen + 3) & ~3), GFP_KERNEL);
  5. }

分配空间后,拷贝用户设置的IP选项到opt->__data中;最后调用ip_options_get_finish()完成选项的处理,包括了用户传入选项的再处理、一些内部数据的填写,下面会进行详细讲解。

  1. copy_from_user(opt->__data, data, optlen);
  2. 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()。

  1. while (optlen & 3)
  2. opt->__data[optlen++] = IPOPT_END;
  3. opt->optlen = optlen;

ip_options_compile()实际完成选项的处理,它在两个地方被调用:生成带IP选项的报文时被调用,此时处理的是用户传入的选项;接收带有IP选项的报文时被调用,此时处理的是报文中的IP选项,下面详细看下该函数,以LSRR选项为例子。

  1. ip_options_compile(net, opt, NULL);
  2. kfree(*optp);
  3. *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所指向的位置是没有意义的。

  1. if (skb != NULL) {
  2. rt = skb_rtable(skb);
  3. optptr = (unsigned char *)&(ip_hdr(skb)[1]);
  4. } else
  5. optptr = opt->__data;
  6. 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之后。

  1. for (l = opt->optlen; l > 0; ) {
  2. switch (*optptr) {
  3. case IPOPT_END: ….
  4. case IPOPT_NOOP: ...
  5. …...
  6. optlen = optptr[1];
  7. if (optlen<2 || optlen>l) {
  8. pp_ptr = optptr;
  9. goto error;
  10. }
  11. case …...
  12. …...// 处理代码段
  13. }
  14. l -= optlen;
  15. optptr += optlen;
  16. }

还是以宽松源路由为例子:

  1. case IPOPT_LSRR:

首先会作一些检查,选项长度optlen不能比3小,到少有3字节的头部:code, len, ptr。指针ptr不能比4小,因为头部就有4字节。这里optlen是去除了头部的IPOPT_NOOP后的长度,而ptr的计算是包括IPOPT_NOOP的,因此一个是3一个是4;另外,选项中只能有一个源路由选项,因此当srr有值时,表示正在处理的是第二个源路由选项,则有错误。

  1. if (optlen < 3) {
  2. pp_ptr = optptr + 1;
  3. goto error;
  4. }
  5. if (optptr[2] < 4) {
  6. pp_ptr = optptr + 2;
  7. goto error;
  8. }
  9. /* NB: cf RFC-1812 5.2.4.1 */
  10. if (opt->srr) {
  11. pp_ptr = optptr;
  12. goto error;
  13. }

当skb==NULL,对应于第一种情况(生成报文选项时);取出源路由选项的第一跳,记录到选项opt的faddr中,作为下一跳地址;源路由选项依次前移。对应于开头给出的例子,这里处理后结果如图所示:

  1. if (!skb) {
  2. if (optptr[2] != 4 || optlen < 7 || ((optlen-3) & 3)) {
  3. pp_ptr = optptr + 1;
  4. goto error;
  5. }
  6. memcpy(&opt->faddr, &optptr[3], 4);
  7. if (optlen > 7)
  8. memmove(&optptr[3], &optptr[7], optlen-7);
  9. }

Linux内核分析 - 网络[十四]:IP选项

最后记录,is_strictroute是否是严格的路由选路,srr表示选项到IP报头的距离,同样,它只对处理收到的报文中选项时有效。

  1. opt->is_strictroute = (optptr[0] == IPOPT_SSRR);
  2. 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选项有关。

  1. memset(IPCB(skb), 0, sizeof(struct inet_skb_parm));
  2. struct inet_skb_parm {
  3. struct ip_options opt;  /* Compiled IP options  */
  4. unsigned char  flags;
  5. };

ip_rcv_finish()中如果头部长度字段ihl大于4,则表示含有IP选项,此时调用ip_rcv_optins()来接收IP选项。

  1. if (iph->ihl > 5 && ip_rcv_options(skb))
  2. goto drop;

ip_rcv_options()
      iph指向IP头;opt指向控制数据的opt,对IP选项处理的结构会存放在此,作为skb的一部分,在其它地方起作用;设置opt->optlen选项长度,这里的长度包括了开头的IPOPT_NOOP字段,是4的整数倍。

  1. iph = ip_hdr(skb);
  2. opt = &(IPCB(skb)->opt);
  3. 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选项的处理要留带到发送报文时。

  1. if (ip_options_compile(dev_net(dev), opt, skb)) {
  2. IP_INC_STATS_BH(dev_net(dev), IPSTATS_MIB_INHDRERRORS);
  3. goto drop;
  4. }

如果是LSRR,opt->srr在上一步中被设置,为选项到报头的距离,对于带SSRR或LSRR选项的报文来说,opt->srr值不为0,进入调用ip_options_rcv_srr()完成LSRR选项的处理。

  1. if (unlikely(opt->srr)) {
  2. ……
  3. if (ip_options_rcv_srr(skb))
  4. goto drop;
  5. }
  6. return 0;

ip_options_rcv_srr()
      该函数的主要作用是根据源站选项重新设置skb的路由项,从而改变报文的正常流程。它不会对选项进行其它操作,真正的操作在发送时完成。
      首先会进行一些检查,报文的目的MAC必须是本主机,这里检查skb->pkt_type==PACKET_HOST;如果报文的目的IP不是本机(而是在本机的邻居),则本主只是源路径的一个中转站,此时不用再次查找路由表,直接返回,这里检查rt->rt_type==RTN_UNICAST,这种情况在LSRR中是允许的,SSRR是不允许的;如果报文的目的IP对本机来说不是直接可达,则错误返回。

  1. if (skb->pkt_type != PACKET_HOST)
  2. return -EINVAL;
  3. if (rt->rt_type == RTN_UNICAST) {
  4. if (!opt->is_strictroute)
  5. return 0;
  6. icmp_send(skb, ICMP_PARAMETERPROB, 0, htonl(16<<24));
  7. return -EINVAL;
  8. }
  9. if (rt->rt_type != RTN_LOCAL)
  10. return -EINVAL;

从LSRR选项中取出下一跳地址,记录到nexthop中,并查询路由表从saddr到nexthop的路由项,记录到skb中。如果没有这样的路由项,则返回错误;如果有这样的路由项且不是本机(如果下一跳是本机,则表示报文到达目的主机了),则break跳出循环;如果下一跳就是本机,则拷贝下一跳地址到iph->daddr中。
      需要注意的是这里重新查找了一次路由表(ip_route_input)。而我们知道,在IP层会查找路由表(ip_rcv_finish函数中),它决定报文是否该被接收还是该被转发。而这里重查一次路由表也是源站选项的意义所在,IP报头中的目的地址并不是最终地址,它只决定路径中的一站,真正的目的地由选项中的值决定,因此需要根据选项中的值作为目的地址再查找一次,以便决定接下来的动作,用查找到的路由项rt2作为报文skb的路由项。

  1. for (srrptr=optptr[2], srrspace = optptr[1]; srrptr <= srrspace; srrptr += 4) {
  2. memcpy(&nexthop, &optptr[srrptr-1], 4);
  3. rt = skb_rtable(skb);
  4. skb_dst_set(skb, NULL);
  5. err = ip_route_input(skb, nexthop, iph->saddr, iph->tos, skb->dev);
  6. rt2 = skb_rtable(skb);
  7. if (err || (rt2->rt_type != RTN_UNICAST && rt2->rt_type != RTN_LOCAL)) {
  8. ip_rt_put(rt2);
  9. skb_dst_set(skb, &rt->u.dst);
  10. return -EINVAL;
  11. }
  12. ip_rt_put(rt);
  13. if (rt2->rt_type != RTN_LOCAL)
  14. break;
  15. /* Superfast 8) loopback forward */
  16. memcpy(&iph->daddr, &optptr[srrptr-1], 4);
  17. opt->is_changed = 1;
  18. }

IP选项中的srr_is_hit和is_changed含义是不同的,srr_is_hit表示下一跳地址是从源路由选项中提取的,换言之,本机仍不是目的主机;is_changed表示IP报头是否被改变,被改变的话就需要重新计算IP报头的校验和(这里由于IP选项LSRR可能会改变IP报头的目的地址或选项LSRR中的值)。

  1. if (srrptr <= srrspace) {
  2. opt->srr_is_hit = 1;
  3. opt->is_changed = 1;
  4. }

根据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个字节。

  1. if (opt->srr_is_hit) {
  2. int srrptr, srrspace;
  3. optptr = raw + opt->srr;
  4. for ( srrptr=optptr[2], srrspace = optptr[1]; srrptr <= srrspace; srrptr += 4 ) {
  5. if (srrptr + 3 > srrspace)
  6. break;
  7. if (memcmp(&rt->rt_dst, &optptr[srrptr-1], 4) == 0)
  8. break;
  9. }
  10. if (srrptr + 3 <= srrspace) {
  11. opt->is_changed = 1;
  12. ip_rt_get_source(&optptr[srrptr-1], rt);
  13. ip_hdr(skb)->daddr = rt->rt_dst;
  14. optptr[2] = srrptr+4;
  15. } else if (net_ratelimit())
  16. printk(KERN_CRIT "ip_forward(): Argh! Destination lost!\n");
  17. ……
  18. }

还是以开头的例子为例,在主机192.168.1.2上收到来自192.168.1.1的报文,最后转发出去的报文选项如下图所示:

Linux内核分析 - 网络[十四]:IP选项

再看接收的情况,主机是报文的最终地址,调用ip_local_deliver()像处理正常IP报文一样处理该报文,接下来的流程与”IP协议”章节中描述的一样。最终主机192.168.1.100收到的报文选项如下图所示:

Linux内核分析 - 网络[十四]:IP选项

总结:
      生成源站路由选项时,最后两项地址是相同的,都是192.168.1.100
      源站路由实现是依靠两次路由查找改变了报文的流程
      源站路由的更改需要重新计算校验和