Linux内核中的ICMP处理

在INET域支持三种类型的套接字:流套接字(SOCK_STREAM),数据报套接字(SOCK_DGRAM),原始套接字(SOCK_RAW)。流套 接字支持传输层的TCP协议,数据报套接字支持传输层的UDP协议,原始套接字则支持网络层的附属协议ICMP,IGMP等。在前面,已经为TCP/IP module加上了对原始套接字(SOCK_RAW)的支持,现在再为它加上对ICMP协议的支持。
    结构体struct net_protocol表示一个协议(包括传输层协议和网络层附属协议)的接收处理函数集,一般包括一个正常接收函数,和一个出错接收函数。icmp协议的该结构体内容如下:
    struct net_protocol icmp_protocol = {
        .handler = icmp_rcv,
        .err_handler = NULL;
        .no_policy = 0;
    };
    因为icmp本身就是一个控制报文协议,为其它协议提供差错报文传递及其它需要注意的信息的传递服务,所以,它就没有出错接收处理函数,只有一个正常接收函数。
    要让系统支持icmp协议,需要做的第一步事情就是把icmp_protocol添加到inet_protos数组中,inet_protos是INET 域全部协议的接收处理函数集的一个数组,每个协议以自己的协议号(比如icmp协议就以IPPROTO_ICMP,也就是数字1)作为下标,把自己添加到 该数组。inet_protos数组的意义在于,可以让协议栈的底层在收到一个IP数据报时(ARP数据会直接进入arp_rcv处理),能够根据IP首 部中的协议类型字段提供的协议号n,找到inet_protos[n]->handler(),即对应协议的接收函数,这样就把数据报顺利交给了正 确的协议处理函数。所以,一个需要本地接收的IP数据报到达ip_rcv之后的处理流程是:由ip_rcv交给 ip_rcv_finish,ip_rcv_finish查询输入路由后,交给dst_input,dst_input调用 skb->dst->input(一般为ip_local_deliver),然后再交给ip_local_deliver_finish, 该函数除了在原始套接字的哈希表中寻找要处理该数据报的socket,会根据inet_protos数组中的指示,把数据报交给正确的协议接收处理函数。
    所以添加了icmp_protocol之后,我们能够确保系统接收到的icmp数据报会到达icmp_rcv了,包括系统自己给环回接口发的icmp报文。

    除了能够接收icmp数据报,还需要能够发送icmp数据报,应用层发送icmp数据,只要这样调用socket系统调用socket(AF_INET, SOCK_RAW, IPPROTO_ICMP),建立一个原始套接字,然后在应用程序中自己构建ICMP首部作为应用数据的一部分,通过原始套接字的fd,发送即可,系统工 具ping就是这么实现的。但仅仅通过应用层发送icmp报文还是不够的。更多的时候,需要协议栈自身能够发起icmp报文,比如,主机A向主机B的端口 n发送一个UDP数据报,但主机B在接收到数据报后,发现本机并没有端口n对应的进程需要接收处理这个数据报,那么主机B的协议栈便会自动向主机A回应一 个类别为目的不可达(3),代码为端口不可达(3)的icmp差错报文;另外一种比较普遍的情况是,主机A向IP地址AA.BB.CC.DD发送IP数据 报,在发送第一个数据报时,需要ARP解析对方的MAC地址,但发出ARP请求后,并没有收到回应(该IP地址对应的主机当前不在工作状态,或不在可达范 围内),三次ARP请求未响应后,协议栈会ARP报错,把邻居的超时时间设为0,以备下一次垃圾回收时被回收掉,同时向本机发送一个ICMP报文,报文类 型为目的不可达(3),代码为主机不可达(1)。
    由于协议栈本身有发送ICMP数据报的需求,所以,需要在协议栈中创建内核态的原始套接字,用于发送ICMP数据报,这个事情在协议栈初始化时,由 icmp_init函数完成。它为每个CPU都创建一个icmp_socket,创建工作由sock_create_kern函数完成,创建流程跟应用层 创建socket完全一致。

 

 

回显请求与回显应答是两种icmp报文类型,类型号分别是8和0,这两种类型下都只有一种代码0。这两种icmp报文属查询报文,主要用于测试网络中另一 台主机是否可达,向欲测试主机发送一份ICMP回显请求,并等待返回ICMP回显应答,如果能收到,表明该主机可达。这也是网络工具ping程序的实现原 理,下面通过ping程序的实现来分析这两种icmp报文的实现原理。
    建立ping程序,首先要创建一个INET域的类型为RAW_SOCK的原始套接字,绑定协议为icmp。前面讲过,icmp首部除前4个字节分别用于表 示类型,代码,校验和之外,余下4个字节长度随报文类型不同而有所不同,下面是类型为回显请求与回显应答的icmp报文的首部:
    struct icmphdr {
        __u8        type;
        __u8        code;
        __u16       checksum;
        __u16       id;
        __u16       sequence;
    };
    id是发送回显请求的应用进程写入的一个唯一值,对端主机发送回显应答时,保持这个字段值不变,被该进程用于判断收到的回显应答是否是给自己的。一般,进 程会把自己的进程号写入该字段,以保证在系统中是唯一的。sequence是请求进程写入的一个单调递增的值(一般从1开始),用于判断到对端主机的丢包 率等。icmp首部需要应用程序自己添加,并作为应用数据的一部分通过send系统调用传给协议栈。
    当收到来自对端主机的icmp回显应答时,ip_local_deliver_finish函数会先检查哈希表raw_v4_htable。因为在创建 socket时,inet_create会把协议号IPPROTO_ICMP的值赋给socket的成员num,并以num为键值,把socket存入哈 项表raw_v4_htable,raw_v4_htable[IPPROTO_ICMP&(MAX_INET_PROTOS-1)]上即存放了 这个socket,实际上是一个socket的链表,如果其它还有socket要处理这个回显应答,也会被放到这里,组成一个链 表,ip_local_deliver_finish收到数据报后,取出这个socket链表(目前实际上只有一项),调用raw_v4_input,把 skb交给每一个socket进行处理。然后,还需要把数据报交给inet_protos[IPPROTO_ICMP& (MAX_INET_PROTOS-1)],即icmp_rcv处理,因为对于icmp报文,每一个都是需要经过协议栈处理的,但对回显应 答,icmp_rcv只是简单丢弃,并未实际处理。   
    重点来看raw_v4_input,该函数遍历raw_v4_htable[protocol&(MAX_INET_PROTOS-1)]链表, 找出协议号(inet->num),目的ip地址,源ip地址,输入设备接口都匹配的socket,克隆一个skb交给它处理。但协议号是 IPPROTO_ICMP(我们当前正处理的情况)时,则还需要判断该icmp类型的报文是否是被过滤掉的,如果是,则不处理。结构体raw_sock有 一个成员struct icmp_filter filter,它实际上是一个32位无符号数,如果某位为零,则相应的该类型的icmp报文被屏蔽,不能被应用层接收到,icmp报文类型号的最大值为 18,所以32位是足够的,ping程序需要能够收到icmp的回显应答报文,所以,需要关掉屏蔽位(相应位设为1),这可以通过socket命令字 ICMP_FILTER来实现,所以,ping程序的实现中需要这样的源代码:
     struct icmp_filter filter.data = 1 << ICMP_ECHOREPLY;
     int err = setsockopt( sock, SOL_RAW, ICMP_FILTER, &filter, sizeof(struct icmp_filter) );
     if( err < 0 )
        perror("error: ");
    但实际上,ICMP_ECHOREPLY的值是0,即协议栈是永远不会屏蔽回显应答报文的,所以,这步操作其实是没有必要的。
    收到的skb交给raw_rcv处理,raw_rcv调用raw_rcv_skb,把收到的skb放入socket的接收队列。应用程序收到数据报,解析icmp首部即可。

 

 

转自:http://hi.baidu.com/linux%5Fkernel/blog/category/icmp%D0%AD%D2%E9

你可能感兴趣的:(TCP/IP网络协议)