回显请求与回显应答是两种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首部即可。
当.2收到来自.1的icmp出错报文时,在协议栈的ip_local_deliver_finish函数中,会找到inet_protos [protocol&(MAX_INET_PROTOS - 1)]指向的icmp报文接收处理函数。因为在协议栈初始化的时候,通过函数调用inet_add_protocol( &icmp_protocol, IPPROTO_ICMP ),把结构体struct net_protocol icmp_protocol的地址直接赋给了数组inet_protos[IPPROTO_ICMP]指针。而icmp_protocol的初始化如下:
static struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
};
所以,收到的数据报交给了icmp_rcv。icmp_rcv首先检查skb->ip_summed,如果需要软件执行校验和(值为 CHECKSUM_NONE),则先进行校验和验证。然后从icmp首部中取出类型,对类型的正确性进行验证。如果收到的icmp数据报是一个广播报文或 组播报文,则需要对类型进行进一步的检查,如果类型是ICMP_ECHO(请求回显)或者ICMP_TIMESTAMP(时间戳请求),并且 sysctl_icmp_echo_ignore_broadcasts不为0(即要求忽略这两类的广播报文),则不处理,直接出错返回,如果类型值不在 范围ICMP_ECHO,ICMP_TIMESTAMP,ICMP_ADDRESS(地址掩码请求),ICMP_ADDRESSREPLY(地址掩码应 答)这四类之中,也立即出错返回。即除了这四种类型,其它类型的icmp报文都不允许广播或组播。检查通过之后,调用icmp_pointers [icmph->type].handler(skb)进行处理,对于ICMP_DEST_UNREACH来说,该处理函数即 icmp_unreach。
icmp_unreach首先检查在剥去了icmp差错报文的IP首部,ICMP首部后,余下的内容是否还够一个IP首部的长度(即icmp差错报文携带 的那个出错数据报,至少要保留一个完整的IP首部),不够就出错返回。检查通过后,取出携带的那个出错报文的IP首部,取其协议号,根据协议号计算一个哈 希值hash=protocol&(MAX_INET_PROTOS-1),首先检查原始套接字的socket哈希表raw_v4_htable [hash],看是否有socket需要处理这个icmp差错报文,有就调用raw_err进行处理。然后调用inet_protos[hash]- >err_handler处理。因为所携带的IP数据报即刚刚172.16.48.2发出的那个IP数据报,首部中的协议号是 IPPROTO_DUMMY,所以,实际上调用了函数dummy_err。
icmp_err_convert是一个专门针对ICMP_DEST_UNREACH类型的数组,共有16项,针对该类型的每一种代码给出一个错误号,和错误的严重性(用0和1表示),比如本实例涉及到的协议不可达(代码为2),它在该数组中就是:
icmp_err_convert[2] = {
.errno = ENOPROTOOPT /* ICMP_PROT_UNREACH */,
.fatal = 1,
}
即:协议不可达,为严重错误。dummy_err先取到错误号,和错误的严重性等级,如果不是严重错误(比如主机不可达,它是有可能恢复的),或者是严重 错误,但是socket未处于连接建立状态,则不处理这个错误报文,直接结束,导致的结果是应用程序一直在那里等待数据,直至某项结束条件成立。
因为协议不可达是属于严重错误,所以需要报错,首先置发送的那个socket的成员sk_err为ENOPROTOOPT,然后调用struct sock->sk_error_report(sk)报错。sk_error_report只是唤醒在该socket上睡眠的进程,并没有做其它事 情,所以,还需要回过来看看发送这个dummy数据的应用程序。
应用程序在调用send发送完数据报后,调用recv开始等待接收数据,recv系统调用最终会到dummy_recvmsg,现在使用最为简单的单进程 阻塞方式,并且未设置超时时间,所以dummy_recvmsg调用skb_recv_datagram,并阻塞在那里。
skb_recv_datagram首先检查sk->sk_err是否有值,如果有,则直接设置错误号,并返回无数据报。然后取超时时间,如果进程 是非阻塞方式的,则超时时间取0,否则取sk->sk_rcvtimeo,如果没有设置过超时时间,这个值是 MAX_SCHEDULE_TIMEOUT,这是一个最大的32位无符号数,也就是永远不会超时。取完超时时间,skb_recv_datagream进 入一个循环,去取接收队列sk->sk_receive_queue,如果能取到skb,则直接把skb返回,否则,置错误号为-EAGAIN,如 果超时时间为0,则直接返回(非阻塞模式),否则,调用wait_for_packet进行等待,wait_for_packet在sk-> sk_sleep上睡眠等待,一旦被唤醒,立即取sk->sk_err,如果有错误号,则立即返回。dummy_recvmsg把错误号返回给应用 程序,应用程序就得到了-ENOPROTOOPT的错误号。整个通讯尝试至此以失败告终。