2012-04-25 17:14 5639人阅读 评论(1) 收藏 举报
 分类:
内核协议栈(22) 

版权声明:本文为博主原创文章,未经博主允许不得转载。

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

  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格式如下:

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

最后记录,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的报文,最后转发出去的报文选项如下图所示:

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

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

Linux内核分析 - 网络[十四]:IP选项的更多相关文章

  1. 《Linux内核分析》 第四节 扒开系统调用的三层皮(上)

    <Linux内核分析> 第四节 扒开系统调用的三层皮(上) 张嘉琪 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com ...

  2. Linux内核分析——第十八章 调试

    第十八章    调试 18.1 准备开始 1.在用户级的程序里,bug表现比较直接:在内核中却不清晰. 2.内核级开发的调试工作远比用户级开发艰难的多. 3.准备工作需要的是: (1)一个bug (2 ...

  3. Linux内核分析第十八章读书笔记

    第十八章 调试 调试工作艰难是内核级开发区别于用户级开发的一个显著特点. 18.1 准备开始 我们需要什么? 一个bug 一个藏匿bug的内核版本 思路:假定能够让bug重现 在用户级程序中,bug直 ...

  4. Linux内核分析 - 网络

    http://blog.csdn.net/column/details/network-kernel-yoyo.html

  5. 《Linux内核分析》实践4

    <Linux内核分析> 实践四--ELF文件格式分析 20135211李行之 一.概述 1.ELF全称Executable and Linkable Format,可执行连接格式,ELF格 ...

  6. Linux内核分析(四)----进程管理|网络子系统|虚拟文件系统|驱动简介

    原文:Linux内核分析(四)----进程管理|网络子系统|虚拟文件系统|驱动简介 Linux内核分析(四) 两天没有更新了,上次博文我们分析了linux的内存管理子系统,本来我不想对接下来的进程管理 ...

  7. 《Linux内核分析》读书笔记(四章)

    <Linux内核分析>读书笔记(四章) 标签(空格分隔): 20135328陈都 第四章 进程调度 调度程序负责决定将哪个进程投入运行,何时运行以及运行多长时间,进程调度程序可看做在可运行 ...

  8. Linux内核分析第四章 读书笔记

    Linux内核分析第四章 读书笔记 第一部分--进程调度 进程调度:操作系统规定下的进程选取模式 面临问题:多任务选择问题 多任务操作系统就是能同时并发地交互执行多个进程的操作系统,在单处理器机器上这 ...

  9. 【MOOC EXP】Linux内核分析实验四报告

    程涵  原创博客 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 [使用库函数API和C代码中嵌入汇编代 ...

随机推荐

  1. Loj 114 k大异或和

    Loj 114 k大异或和 构造线性基时有所变化.试图构造一个线性基,使得从高到低位走,异或上一个非 \(0\) 的数,总能变大. 构造时让任意两个 \(bas\) 上有值的 \(i,j\) ,满足 ...

  2. WebHttpBinding.ReaderQuotas 无法设置或者无法点出来

    项目需要引用System.Runtime.Serialization.dll 才能设置各项值: binding.ReaderQuotas.MaxDepth = ; binding.ReaderQuot ...

  3. Makefile中进行宏定义-***

    实际上是gcc命令支持-D宏定义,相当于C中的全局#define: gcc -D name gcc -D name=definition Makefile中可以定义变量(和宏很像),但是是给make解 ...

  4. C语言 字符串处理函数 转自 http://blog.chinaunix.net/uid-25885064-id-3175049.html

     C字符串处理函数 2012-04-13 18:14:16 分类: C/C++ void *memccpy (void *dest, const void *src, int c, size_t n) ...

  5. ACM-Teleportation

    我的代码: #include <bits/stdc++.h> using namespace std; int main() { int a,b,x,y; cin>>a> ...

  6. WinForm中Application.Idle事件用法

    Application.Idle 事件 描述:当应用程序完成处理并即将进入空闲状态时发生.如果您有必须执行的任务在线程变为空闲之前,请将它们附加到此事件. public partial class F ...

  7. bzoj4891: [Tjoi2017]龙舟

    求$\frac{b_1b_2b_3...b_m}{a_1a_2a_3...a_m}\%M$ M<=1e18,m<=100000,数据组数<=50 用pollard-rho分解M的质因 ...

  8. 花瓶使用笔记 (抓数据时,记得添加host,不然抓不了包的)

    情况一: 有时候抓不了app的数据,那么把app的host 添加一下就可以了 proxy > SSL Proxying Settings 情况二: 开了 翻 墙 是抓不了包的! (掉了一次坑)

  9. java web 程序---javaBean

    1.JavaBean 是Java中的一个组件技术,类似于微软的COM组件 SUN公司将JavaBean定义为:可以重复利用的软件组件 2.JavaBean的种类:a.可视化JavaBean   b.非 ...

  10. Linux下添加php的zip模块

    今天早上开发的人员过来跟我说,测试机上的XX项目报了个错: include(ZipArchive.php): failed to open stream: No such file or direct ...