Linux icmp功能分析之一 icmp协议相关的格式
ICMP协议是网络层中一个非常重要的协议,其全称为Internet Control Message Protocol(因特网控制报文协议),ICMP协议弥补了IP的缺限,它使用IP协议进行信息传递,向数据包中的源端节点提供发生在网络层的错误信息 反馈。
在实现中,路由器会使用该协议来报告问题,而主机则会使用该机制来测试目的站是否可达。该报文的最终目的地不是一个应用程序或者目的设备上的用户,而是目的设备上的网际协议软件,一般icmp报文的接收是linux内核里的icmp接收模块来处理的,而icmp请求报文的发送即可以是内核里相关子系统也可以是应用层的程序发送(比如ping应用)。
1、 ICMP报文的格式
各种ICMP报文的前32bits都是三个长度固定的字段,为8bit的type字段、8bit的code字段、16bit的校验和字段(包括icmp数据字段的校验和),而对于不同类型的icmp报文,其余下字段的含义则是不同的。
a) type类型
icmp类型目前有40个,下面几个是比较常用的,也是目前linux支持的类型。
0 回显应答(ECHO-REPLY)
3 不可到达
4 源站抑制
5 重定向
8 回显请求(ECHO-REQUEST)
11 数据报超时
12 参数失灵
13 时间戳请求
14 时间戳应答
15 信息请求(已不再使用)
16 信息应答(已不再使用)
17 地址掩码请求(已不再使用)
18 地址掩码应答(已不再使用)
对于 以上类型,比较重要的有:回显请求与应答(type 0、8)、不可到达(3)、源站抑制(4)、路由重定向(5)、时间戳请求与应答(13、14)
2、 主要的ICMP格式
a) 回显请求与应答
其中type值表示是一个回显请求或应答,code值为0,而identifier在linux的实现为进程pid(因为ping请求是应用程序,通过该值能够确认是机器上的哪一个应用程序执行的ping操作,能够对进行的接收数据进行匹配操作),而sequence则为一个计数器,主要是为每一个回显请求数据包设置序列值。Option是可选数据,其大小是可变的。
TYPE(8/0) |
CODE(0) |
Checksum |
identifier |
Sequence |
|
Option |
b) 目的站不可达
TYPE(3) |
CODE(0-15) |
Checksum |
Not used (must set 0) |
||
Option |
由于目的站不可达的原因很多,所以需要用code来进行进一步细分。对于option字段,其值为ip头部(包括可选项)加上原始ip数据部分的前8个字节。
而code的定义如下:
#define ICMP_NET_UNREACH0 /* Network Unreachable */
#define ICMP_HOST_UNREACH1 /* Host Unreachable */
#define ICMP_PROT_UNREACH2 /* Protocol Unreachable */
#define ICMP_PORT_UNREACH3 /* Port Unreachable */
#define ICMP_FRAG_NEEDED4 /* Fragmentation Needed/DF set */
#define ICMP_SR_FAILED5 /* Source Route failed */
#define ICMP_NET_UNKNOWN6
#define ICMP_HOST_UNKNOWN7
#define ICMP_HOST_ISOLATED8
#define ICMP_NET_ANO9
#define ICMP_HOST_ANO10
#define ICMP_NET_UNR_TOS11
#define ICMP_HOST_UNR_TOS12
#define ICMP_PKT_FILTERED13 /* Packet filtered */
#define ICMP_PREC_VIOLATION14 /* Precedence violation */
#define ICMP_PREC_CUTOFF15 /* Precedence cut off */
#define NR_ICMP_UNREACH15 /* instead of hardcoding immediate value */
c)重定向
TYPE(5) |
CODE(0-3) |
Checksum |
Route’s ip |
||
Option |
对于option字段,其值为ip头部(包括可选项)加上原始ip数据部分的前8个字节。
第二个32bits代表路由器的wan側地址。
Code类型如下:
#defineICMP_REDIR_NET 0 /* Redirect Net */
#defineICMP_REDIR_HOST 1 /* Redirect Host */
#defineICMP_REDIR_NETTOS 2 /* Redirect Net for TOS */
#defineICMP_REDIR_HOSTTOS 3 /* Redirect Host for TOS */
重定向报文仅限于在直接连接到同一网络上的路由器与主机间交互。
d)数据包超时
因为每一个ip数据包都有一个ttl计数器,即跳数计数器,当数据包中的ttl的值为0时,就丢弃数据包,并发送一个数据包超时的icmp 报文。下面即是icmp 数据包超时报文的格式
TYPE(11) |
CODE(0-1) |
Checksum |
Not used(must set 0) |
||
Option |
对于option字段,其值为ip头部(包括可选项)加上原始ip数据部分的前8个字节。
对于命令traceroute(windows 下为tracert),即是根据ttl来实现查找到目的站点所有跳点的ip地址的。即先发送3个ttl为1的数据包,根据接收到的icmp 数据包超时报文获取到第一个下一跳地址;然后再发送3个ttl为的数据包,根据接收到的数据包超时报文获取到第二个下一跳地址;依此类推直到找到所有的跳点地址或者已经到了ttl的max值还没有到目的站点则程序返回。
基本上这4个icmp报文是最重要的了。
3、 linux中icmp相关的数据结构
#defineICMP_ECHOREPLY 0 /* Echo Reply */
#defineICMP_DEST_UNREACH 3 /* Destination Unreachable */
#defineICMP_SOURCE_QUENCH 4 /* Source Quench */
#defineICMP_REDIRECT 5 /* Redirect (change route) */
#defineICMP_ECHO 8 /* Echo Request */
#defineICMP_TIME_EXCEEDED 11 /* Time Exceeded */
#defineICMP_PARAMETERPROB 12 /* Parameter Problem */
#defineICMP_TIMESTAMP 13 /* Timestamp Request */
#defineICMP_TIMESTAMPREPLY 14 /* Timestamp Reply */
#defineICMP_INFO_REQUEST 15 /* Information Request */
#defineICMP_INFO_REPLY 16 /* Information Reply */
#defineICMP_ADDRESS 17 /* Address Mask Request */
#defineICMP_ADDRESSREPLY 18 /* Address Mask Reply */
#defineNR_ICMP_TYPES 18
/*Codes for UNREACH. */
#defineICMP_NET_UNREACH 0 /* Network Unreachable */
#defineICMP_HOST_UNREACH 1 /* Host Unreachable */
#defineICMP_PROT_UNREACH 2 /* Protocol Unreachable */
#defineICMP_PORT_UNREACH 3 /* Port Unreachable */
#defineICMP_FRAG_NEEDED 4 /* Fragmentation Needed/DF set */
#defineICMP_SR_FAILED 5 /* Source Route failed */
#defineICMP_NET_UNKNOWN 6
#defineICMP_HOST_UNKNOWN 7
#defineICMP_HOST_ISOLATED 8
#defineICMP_NET_ANO 9
#defineICMP_HOST_ANO 10
#defineICMP_NET_UNR_TOS 11
#defineICMP_HOST_UNR_TOS 12
#defineICMP_PKT_FILTERED 13 /* Packet filtered */
#defineICMP_PREC_VIOLATION 14 /* Precedence violation */
#defineICMP_PREC_CUTOFF 15 /* Precedence cut off */
#defineNR_ICMP_UNREACH 15 /* instead of hardcoding immediate value */
/*Codes for REDIRECT. */
#defineICMP_REDIR_NET 0 /* Redirect Net */
#defineICMP_REDIR_HOST 1 /* Redirect Host */
#defineICMP_REDIR_NETTOS 2 /* Redirect Net for TOS */
#defineICMP_REDIR_HOSTTOS 3 /* Redirect Host for TOS */
/*Codes for TIME_EXCEEDED. */
#defineICMP_EXC_TTL 0 /* TTL count exceeded */
#defineICMP_EXC_FRAGTIME 1 /* Fragment Reass time exceeded */
Icmp头部定义:
structicmphdr {
__u8 type;
__u8 code;
__sum16 checksum;
union {
struct {
__be16 id;
__be16 sequence;
} echo;
__be32 gateway;
struct {
__be16 __unused;
__be16 mtu;
} frag;
} un;
};
在该数据结构中,前32bits的定义是一样的,而后面32bits的定义,因回显请求与应答、重定向等报文定义不同而有不同的含义。
发送icmp报文相关的数据结构。
structicmp_bxm {
struct sk_buff *skb;//接收到的icmp报文
int offset;//选项数据在icmp数据中的偏移量
int data_len;//icmp数据报文长度
struct {
struct icmphdr icmph;/icmp头部/
__be32 times[3];
} data;
int head_len;//icmp头部长度
struct ip_options replyopts;//存储的接收icmp报文的选项数据,待发送时使用
unsigned char optbuf[40];
};
在分析icmp数据包处理流程之前,我有如下疑问:
1、为什么要为每一个cpu创建一个仅用于发送icmp报文的socket呢,不使用socket不也是可以把icmp报文发送出去吗?
2、ping的工作原理是什么呢?
3、Traceroute的工作原理是什么呢?
一、imcp协议的初始化
1)ICMP接收处理函数的初始化
我们知道icmp协议是附属于ip层的3层协议,且是将icmp数据存放于ip数据包的数据部分的3层协议。而tcp、udp也是将tcp、udp数据存放于ip数据包的数据部分的4层协议。
虽然icmp与tcp等协议不属于同一个网络层,但是都是在3层ip协议处理完以后,才会交给icmp、tcp的处理函数去处理。因此在linux中,都是调用inet_add_protocol将其接收处理函数相关的数据结构添加到数组inet_protos中去的(关于三、四层接收数据处理函数的注册相关的知识请参看http://blog.csdn.net/lickylin/article/details/22900401)。
Icmp的接收处理函数相关的结构体定义如下:
static const struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
.no_policy = 1,
.netns_ok = 1,
};
在inet_init初始化时,即会调用inet_add_protocol将tcp、udp、icmp、igmp等协议相关的接收处理结构体注册,并保存在数组inet_protos中。
当接收的数据包的协议为icmp时,即会调用icmp_rcv进行后续处理。
2) icmp协议模块的初始化
主要是调用函数register_pernet_subsys(关于该函数的工作流程,请参看http://blog.csdn.net/lickylin/article/details/18013879),将icmp协议模块注册到网络命令空间中,并调用ops->init进行协议初始化相关的代码。
对于icmp,其pernet_operations的定义如下:
static structpernet_operations __net_initdata icmp_sk_ops = {
.init = icmp_sk_init,
.exit = icmp_sk_exit,
};
在调用register_pernet_subsys将icmp协议模块注册到网络命名空间后,即会调用icmp_sk_init进行icmp协议初始化相关的功能,我们分析下icmp_sk_init。
该函数主要实现以下功能:
/*
1、 为每一个cpu创建一个用于发生icmp数据包的socket
2、 设置一些限制条件,包括速率限制、接收数据包的条件等
*/
static int __net_init icmp_sk_init(struct net *net)
{
int i, err;
/*为icmp_sk申请空间,该icmp_sk数组中存放了所有cpu相关的socket指针*/
net->ipv4.icmp_sk =
kzalloc(nr_cpu_ids *sizeof(struct sock *), GFP_KERNEL);
if (net->ipv4.icmp_sk ==NULL)
return -ENOMEM;
/*为每一个cpu创建一个RAW套接字*/
for_each_possible_cpu(i) {
struct sock *sk;
/*
创建一个 RAW 类型的套接字,并调用(*sk)->sk_prot->unhash,将该socket从hash链表raw_v4_hashinfo.ht[RAW_HTABLE_SIZE]中删除与该socket的关联
*/
err =inet_ctl_sock_create(&sk, PF_INET,
SOCK_RAW, IPPROTO_ICMP, net);
if (err <0)
goto fail;
net->ipv4.icmp_sk[i]= sk;
/* Enough space for2 64K ICMP packets, including
* sk_buff struct overhead.
*/
sk->sk_sndbuf =
(2 * ((64 *1024) + sizeof(struct sk_buff)));
/*
* Speedup sock_wfree()
*/
sock_set_flag(sk,SOCK_USE_WRITE_QUEUE);
inet_sk(sk)->pmtudisc= IP_PMTUDISC_DONT;
}
net->ipv4.sysctl_icmp_echo_ignore_all= 0;
/*忽略广播的echo请求 */
net->ipv4.sysctl_icmp_echo_ignore_broadcasts= 1;
/* 忽略广播的icmp 错误回复信息*/
net->ipv4.sysctl_icmp_ignore_bogus_error_respOnses= 1;
net->ipv4.sysctl_icmp_ratelimit= 1 * HZ; //速率限制值
/*进行速率限制的icmp数据包类型,主要有dest unreachable 、source quench time exceeded 、parameter problem*/
net->ipv4.sysctl_icmp_ratemask= 0x1818;
net->ipv4.sysctl_icmp_errors_use_inbound_ifaddr= 0;
return 0;
fail:
for_each_possible_cpu(i)
inet_ctl_sock_destroy(net->ipv4.icmp_sk[i]);
kfree(net->ipv4.icmp_sk);
return err;
}
疑问:当新创建的socket时,为什么要将其从hash链表raw_v4_hashinfo.ht[RAW_HTABLE_SIZE]中删除呢?
因为我们只使用这个socket进行发送数据包,而不需要使用该socket接收数据包。所以此处将其从hash链表raw_v4_hashinfo.ht[RAW_HTABLE_SIZE]中删除。
为什么不使用该socket直接接收icmp报文呢,我的理解是如果使用该socket接收报文,就需要在kernel创建一个内核线程,用于侦听是否有数据到达该socket,然后再进行处理。
而直接使用内核四层协议接收处理函数的注册流程,可以很方便的就能对接收的icmp报文进行处理,而且使用的内核资源比较少,所以对于kernel创建的socket,其接收操作基本上是使用内核四层协议接收处理函数的注册流程实现的。而对于应用层创建的icmp相关的socket则不会执行上述操作。
二、ICMP协议的接收处理函数
Icmp接收处理函数为icmp_rcv,下面分析这个函数。
主要功能:
1、 对数据包进行合理性检查
2、 根据icmp的类型,
int icmp_rcv(struct sk_buff *skb)
{
structicmphdr *icmph;
structrtable *rt = skb_rtable(skb);
structnet *net = dev_net(rt->u.dst.dev);
/*
基于策略的高扩展性的网络安全架构,对于这个内核子架构不清楚
此处分析不了,跳过。
*/
if(!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb)) {
structsec_path *sp = skb_sec_path(skb);
intnh;
if(!(sp && sp->xvec[sp->len - 1]->props.flags &
XFRM_STATE_ICMP))
gotodrop;
if(!pskb_may_pull(skb, sizeof(*icmph) + sizeof(struct iphdr)))
gotodrop;
nh= skb_network_offset(skb);
skb_set_network_header(skb,sizeof(*icmph));
if(!xfrm4_policy_check_reverse(NULL, XFRM_POLICY_IN, skb))
gotodrop;
skb_set_network_header(skb,nh);
}
ICMP_INC_STATS_BH(net,ICMP_MIB_INMSGS);
/*验证校验和信息*/
switch(skb->ip_summed) {
caseCHECKSUM_COMPLETE:
if(!csum_fold(skb->csum))
break;
/*fall through */
caseCHECKSUM_NONE:
skb->csum= 0;
if(__skb_checksum_complete(skb))
gotoerror;
}
if(!pskb_pull(skb, sizeof(*icmph)))
gotoerror;
/*获取icmp头部*/
icmph= icmp_hdr(skb);
ICMPMSGIN_INC_STATS_BH(net,icmph->type);
/*
对于不支持的icmp报文,直接丢掉
*/
if(icmph->type > NR_ICMP_TYPES)
gotoerror;
/*
判断是否丢弃掉多播类型的icmp数据包
1、只处理echo、timestamp、address_mask_request、address_mask_reply类型的多播icmp数据包
*/
if(rt->rt_flags & (RTCF_BROADCAST | RTCF_MULTICAST)) {
/*
* RFC1122: 3.2.2.6 An ICMP_ECHO to broadcast MAY be
* silently ignored (we let user decide with asysctl).
* RFC1122: 3.2.2.8 An ICMP_TIMESTAMP MAY be silently
* discarded if to broadcast/multicast.
*/
if((icmph->type == ICMP_ECHO ||
icmph->type == ICMP_TIMESTAMP)&&
net->ipv4.sysctl_icmp_echo_ignore_broadcasts) {
gotoerror;
}
if(icmph->type != ICMP_ECHO &&
icmph->type != ICMP_TIMESTAMP &&
icmph->type != ICMP_ADDRESS &&
icmph->type != ICMP_ADDRESSREPLY) {
gotoerror;
}
}
/*根据icmp数据包类型,调用相应的处理函数*/
icmp_pointers[icmph->type].handler(skb);
drop:
kfree_skb(skb);
return0;
error:
ICMP_INC_STATS_BH(net,ICMP_MIB_INERRORS);
gotodrop;
}
对于icmp_pointers的定义如下:
/*
* This table is the definition of how wehandle ICMP.
*/
static const struct icmp_controlicmp_pointers[NR_ICMP_TYPES + 1] = {
[ICMP_ECHOREPLY]= {
.handler= icmp_discard,
},
[1]= {
.handler= icmp_discard,
.error= 1,
},
[2]= {
.handler= icmp_discard,
.error= 1,
},
[ICMP_DEST_UNREACH]= {
.handler= icmp_unreach,
.error= 1,
},
[ICMP_SOURCE_QUENCH]= {
.handler= icmp_unreach,
.error= 1,
},
[ICMP_REDIRECT]= {
.handler= icmp_redirect,
.error= 1,
},
[6]= {
.handler= icmp_discard,
.error= 1,
},
[7]= {
.handler= icmp_discard,
.error= 1,
},
[ICMP_ECHO]= {
.handler= icmp_echo,
},
[9]= {
.handler= icmp_discard,
.error= 1,
},
[10]= {
.handler= icmp_discard,
.error= 1,
},
[ICMP_TIME_EXCEEDED]= {
.handler= icmp_unreach,
.error= 1,
},
[ICMP_PARAMETERPROB]= {
.handler= icmp_unreach,
.error= 1,
},
[ICMP_TIMESTAMP]= {
.handler= icmp_timestamp,
},
[ICMP_TIMESTAMPREPLY]= {
.handler= icmp_discard,
},
[ICMP_INFO_REQUEST]= {
.handler= icmp_discard,
},
[ICMP_INFO_REPLY]= {
.handler= icmp_discard,
},
[ICMP_ADDRESS]= {
.handler= icmp_address,
},
[ICMP_ADDRESSREPLY]= {
.handler= icmp_address_reply,
},
};
目前内核处理的icmp报文有icmp_unreach、icmp_address、icmp_address_reply、icmp_timestamp、icmp_echo、icmp_redirect。
icmp_echo
/*
该函数主要是将icmp的type设置为ICMP_ECHOREPLY,并调用icmp_reply将该数据包发送出去
*/
static void icmp_echo(struct sk_buff *skb)
{
structnet *net;
net= dev_net(skb_dst(skb)->dev);
if(!net->ipv4.sysctl_icmp_echo_ignore_all) {
structicmp_bxm icmp_param;
icmp_param.data.icmph =*icmp_hdr(skb);
icmp_param.data.icmph.type= ICMP_ECHOREPLY;
icmp_param.skb = skb;
icmp_param.offset = 0;
icmp_param.data_len =skb->len;
icmp_param.head_len =sizeof(struct icmphdr);
icmp_reply(&icmp_param,skb);
}
}
Timestamp
/*
设置时间戳的值,并将icmp的type设置为ICMP_TIMESTAMPREPLY,并通过icmp_reply发送出去
*/
static void icmp_timestamp(struct sk_buff*skb)
{
structtimespec tv;
structicmp_bxm icmp_param;
/*
* Tooshort.
*/
if(skb->len <4)
gotoout_err;
/*
* Fillin the current time as ms since midnight UT:
*/
getnstimeofday(&tv);
icmp_param.data.times[1]= htonl((tv.tv_sec % 86400) * MSEC_PER_SEC +
tv.tv_nsec / NSEC_PER_MSEC);
icmp_param.data.times[2]= icmp_param.data.times[1];
if(skb_copy_bits(skb, 0, &icmp_param.data.times[0], 4))
BUG();
icmp_param.data.icmph =*icmp_hdr(skb);
icmp_param.data.icmph.type= ICMP_TIMESTAMPREPLY;
icmp_param.data.icmph.code= 0;
icmp_param.skb = skb;
icmp_param.offset = 0;
icmp_param.data_len =0;
icmp_param.head_len =sizeof(struct icmphdr) + 12;
icmp_reply(&icmp_param,skb);
out:
return;
out_err:
ICMP_INC_STATS_BH(dev_net(skb_dst(skb)->dev),ICMP_MIB_INERRORS);
gotoout;
}
Unreach 数据处理
功能:根据icmp中有效载荷数据的值,调用传输层的错误处理函数进行处理
static void icmp_unreach(struct sk_buff*skb)
{
structiphdr *iph;
structicmphdr *icmph;
inthash, protocol;
conststruct net_protocol *ipprot;
u32info = 0;
structnet *net;
net= dev_net(skb_dst(skb)->dev);
/*
* Incompleteheader ?
* Onlychecks for the IP header, there should be an
* additionalcheck for longer headers in upper levels.
*/
if(!pskb_may_pull(skb, sizeof(struct iphdr)))
gotoout_err;
/*获取icmp首部*/
icmph= icmp_hdr(skb);
iph = (struct iphdr *)skb->data;
/*判断ip首部是否完整*/
if(iph->ihl <5) /* Mangled header, drop. */
gotoout_err;
/*仅处理type类型为3或者12的数据包
1、当类型为3时,仅处理code为frag needed的报文
a)当系统不支持pmtu时,丢弃该数据包
b)当系统支持pmtu时,调用ip_rt_frag_needed修改pmtu的值
2、当type类型为12时,则通过icmph->un.gateway获取出错偏移值(相对于数据包)
*/
if(icmph->type == ICMP_DEST_UNREACH) {
switch(icmph->code & 15) {
caseICMP_NET_UNREACH:
caseICMP_HOST_UNREACH:
caseICMP_PROT_UNREACH:
caseICMP_PORT_UNREACH:
break;
caseICMP_FRAG_NEEDED:
if(ipv4_config.no_pmtu_disc) {
LIMIT_NETDEBUG(KERN_INFO"ICMP: %pI4: fragmentation needed and DF set.\n",
&iph->daddr);
}else {
info= ip_rt_frag_needed(net, iph,
ntohs(icmph->un.frag.mtu),
skb->dev);
if(!info)
gotoout;
}
break;
caseICMP_SR_FAILED:
LIMIT_NETDEBUG(KERN_INFO"ICMP: %pI4: Source Route Failed.\n",
&iph->daddr);
break;
default:
break;
}
if(icmph->code > NR_ICMP_UNREACH)
gotoout;
}else if (icmph->typ