首页 > 代码库 > Linux内核分析 - 网络[十四]:IP选项

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

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

标签: linux内核网络structsocketdst
技术分享 分类:

内核版本: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(套接字特定函数)来设置选项。

[cpp] view plain copy
 
  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选项,形式如下:

[cpp] view plain copy
 
  1. 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。

[cpp] view plain copy
 
  1. switch (optname) {  
  2.  case IP_OPTIONS:  

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

[cpp] view plain copy
 
  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字节对齐。

[cpp] view plain copy
 
  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()完成选项的处理,包括了用户传入选项的再处理、一些内部数据的填写,下面会进行详细讲解。

[cpp] view plain copy
 
  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()。

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

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

[cpp] view plain copy
 
  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所指向的位置是没有意义的。

[cpp] view plain copy
 
  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之后。

[cpp] view plain copy
 
  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. }  

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

[cpp] view plain copy
 
  1. case IPOPT_LSRR:  

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

[cpp] view plain copy
 
  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中,作为下一跳地址;源路由选项依次前移。对应于开头给出的例子,这里处理后结果如图所示:

[cpp] view plain copy
 
  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. }  

技术分享

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

[cpp] view plain copy
 
  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选项有关。

[cpp] view plain copy
 
  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选项。

[cpp] view plain copy
 
  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的整数倍。

[cpp] view plain copy
 
  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选项的处理要留带到发送报文时。

[cpp] view plain copy
 
  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选项的处理。

[cpp] view plain copy
 
  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对本机来说不是直接可达,则错误返回。

[cpp] view plain copy
 
  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的路由项。

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

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

[cpp] view plain copy
 
  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个字节。

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

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

技术分享

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

技术分享

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

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