第3章 Internel 控制消息协议(ICMP)
第3章 Internel 控制消息协议(ICMP)
1. ICMPv4简述
有的教材认为ICMP是第三层协议(网络层),有的认为是第四层协议(传输控制层),我更倾向于它是网络层(L3)协议。ICMP协议向源端点反馈发生在网络中的问题和反馈,是一个简单,但是重要的网络协议。
ICMPv4 消息分两类:错误消息
和 信息消息
。
著名的工具:ping
和 traceroute
。
本文介绍的都是ICMPv4
的协议内容。因此以下简称ICMP
都将指代ICMPv4
2. ICMPv4 初始化
2.1 ICMPv4 收包处理函数初始化
ICMPv4 是附属于ip层的3层协议。数据包将套上ip地址。所以都是在ip协议处理完之后,才会分流到各个协议。比如ICMP、TCP、UDP。所以在注册ICMPv4协议的时候,都是放在ipv4初始化协议inet_init()
中。和TCP、UDP等一起初始化。
ICMPv4协议结构体:
struct net_protocol icmp_protocol = {
.handler = icmp_rcv,
.err_handler = icmp_err,
.no_policy = 1,
.netns_ok = 1,
};
icmp_rcv
ICMPv4 接收回调函数。在处理IP报文的时候,如果协议字段为IPPROTO_ICMP(0x01)
,就使用icmp_rcv()
来处理。icmp_err
类似错误报文处理函数?no_policy
这个字段在前面有介绍过,为1时,表示无需执行ipsec安全校验。netns_ok
协议支持命名空间。
static int __init inet_init(void)
{
...
if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
pr_crit("%s: Cannot add ICMP protocol\n", __func__);
...
if (icmp_init() < 0)
panic("Failed to create the ICMP control socket.\n");
...
}
在ipv4初始化中。注册了ICMPv4接收处理函数结构体icmp_protocol
,并保存在数组inet_protos
中。
然后执行icmp_init()
对icmp内核模块进行初始化。
2.2 ICMPv4 内核模块初始化
static struct pernet_operations __net_initdata icmp_sk_ops = {
.init = icmp_sk_init,
.exit = icmp_sk_exit,
};
int icmp_init(void)
{
return register_pernet_subsys(&icmp_sk_ops);
}
在icmp_init()
中,就是注册ICMPv4协议模块注册到网络命名空间,然后通过回调函数ops->init()
来执行ICMPv4协议模块的初始化函数icmp_sk_init()
函数。这个函数主要完成2个功能:
- 为每个cpu创建一个用于发生icmp 数据包的socket;见附录2
- 初始化icmp的一些参数,包括速率限制、行为管控(丢弃、过滤等)
int icmp_sk_init(struct net *net)
{
//申请足够的buff
net->ipv4.icmp_sk = alloc_percpu(struct sock *);
for_each_possible_cpu(i) {
struct sock *sk;
//为每个cpu注册ICMPv4 socket。
err = inet_ctl_sock_create(&sk, PF_INET,
SOCK_RAW, IPPROTO_ICMP, net);
*per_cpu_ptr(net->ipv4.icmp_sk, i) = sk;
...
}
/* Control parameters for ECHO replies. */
// 关闭忽略所有icmp的echo请求。如果开启则丢弃所有icmp echo请求。
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;
// 以下为icmp报文限速配置:
net->ipv4.sysctl_icmp_ratelimit = 1 * HZ; //速率限制
// 速率限制的报文类型有 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;
}
-
inet_ctl_sock_create(&sk, PF_INET, SOCK_RAW, IPPROTO_ICMP, net);
这个函数创建一个原始套接字,并调用(*sk)->sk_prot->unhash(*sk)
(注销接口: inet_unhash() ) 将这个原始套接字从raw_v4_hashinfo.ht[RAW_HTABLE_SIZE]中删除于该socket的关联。为什么这么做?见附录3.int inet_ctl_sock_create(struct sock **sk, unsigned short family, unsigned short type, unsigned char protocol, struct net *net) { ... int rc = sock_create_kern(net, family, type, protocol, &sock); ... (*sk)->sk_prot->unhash(*sk); ... }
-
net->ipv4.sysctl_icmp_xxxx
这个字段是可以在procfs中配置的。所以这里是进行一个初始化操作。
到这里,ICMPv4原始套接字和内核协议就初始化完成了。后面将开始介绍ICMPv4的报文格式和收发流程。
3. 附录
附录 1. 为什么不能把ICMPv4(v6)构建为内核模块
ICMPv4是IPv4协议族的一部分,用于在网络中发送错误、控制和状态信息。通常情况下,ICMPv4在Linux内核中是作为一个内置功能实现的,而不是一个单独的可加载模块。这意味着ICMPv4的功能代码直接编译进了内核,并且在内核启动时就已经可用,不需要通过modprobe 或 insmod等命令动态加载。
当提到“不能将ICMPv4构建为内核模块”时,这句话意味着在特定的Linux内核配置中,ICMPv4不是作为一个可以选择性加载或卸载的模块存在,而是内核核心功能的一部分。如果需要修改或扩展ICMPv4的相关功能,通常需要重新编译整个内核或针对内核源码进行相应的配置和编译。
附录 2. 怎么理解 为每个cpu创建一个用于发生icmp 数据包的socket
在Linux内核中,为了更高效地处理和发送 ICMP 数据包,尤其是对于像ping这样的工具或系统级别的网络消息响应,通常会为每个CPU核心创建一个专用的socket。这是因为现代多核处理器能够并行执行任务,这样设计可以减少锁竞争和上下文切换带来的开销。
具体来说:
-
并发处理:当网络事件发生时,例如接收到需要回应的ICMP请求(如Echo Request),如果每个CPU有自己的socket,那么相应的CPU可以直接使用自己的socket来快速生成和发送ICMP Echo Reply,而无需跨CPU同步。
-
缓存亲和性:由于socket缓冲区和相关数据结构可能驻留在本地CPU缓存中,所以给每个CPU分配一个socket有助于提高缓存命中率,从而提升性能。
-
无锁化操作:避免多个CPU共享同一socket带来的锁保护需求,可以减少潜在的并发问题。
-
I/O调度优化:通过将网络处理工作负载均衡到各个CPU上,操作系统可以更好地利用硬件资源,提高整体系统的吞吐量和响应速度。
因此,在Linux内核实现中,可能会有一个全局的数据结构数组或者其他形式的组织方式,其中每个元素对应着与单个CPU关联的socket,这些socket专门用来处理和发送ICMP报文。这样做的目的就是为了提高效率和可扩展性。
附录 3. 为什么ICMPv4在创建原始套接字之后,要执行unhash()从raw数组中注销?
int inet_ctl_sock_create(struct sock **sk, unsigned short family, unsigned short type, unsigned char protocol, struct net *net)
{
...
int rc = sock_create_kern(net, family, type, protocol, &sock);
...
(*sk)->sk_prot->unhash(*sk);
...
}
在计算机网络和操作系统中,当创建或初始化与ICMP(Internet Control Message Protocol)相关的原始套接字(RAW socket)时,执行unhash()
操作的目的通常是将这个套接字从内核中的套接字哈希表中移除。这种操作通常发生在以下场景:
-
避免冲突:在Linux等操作系统中,为了快速查找和处理套接字,内核会对已存在的套接字进行哈希化存储。但ICMP协议处理的是特殊类型的网络消息,例如错误报告和诊断请求,它并不像TCP或UDP那样直接对应到端口通信。因此,对于监听ICMP的套接字而言,我们不希望它参与常规的数据包接收过程,以免与其他网络套接字产生混淆或不必要的处理开销。
-
控制流量:ICMP套接字通常只用于发送和接收特定类型的ICMP消息,而不是所有到达系统的IP包。通过 unhash()操作,我们可以确保这个套接字不会响应那些不应由ICMP处理的IP数据包。
-
安全性和权限:对于一些高级用途,如网络诊断和traceroute工具,只有管理员级别的程序才允许创建并使用这样的原始套接字。将其从哈希表中移除有助于内核更好地管理和控制哪些程序可以访问和处理ICMP消息。
综上所述,在初始化ICMP套接字时执行unhash() 操作是为了让这个套接字专门处理ICMP协议相关的控制消息,而不参与标准的网络数据流处理,同时也体现了内核对资源管理和安全性的精细控制。