内核ICMP协议分析
一、发送、接收ICMPv4消息
ICMP主要作用是发送有关网络层错误和控制消息的机制,让你能够通过发送ICMP消息来获取有关通信环境中问题的反馈。由于ICMP协议是TCP/IP协议栈的核心组成部分,用于传递网络层的控制信息,是网络通信的底层基础已被深度集成到内核代码中,因此不能构建为内核模块动态加载和卸载。
ICMP也可以被用来发起各种安全攻击。比如,Smurf攻击是一种拒绝服务攻击,它使用ip广播地址,一广播方式向计算机网络中发送大量的ICMP数据包,并将受害者的ip地址用作源地址,因此每个收到ICMP请求的主机都会回复一个echo包,进而淹没源ip主机。
1.ICMPv4
ICMPv4消息分为两类:错误消息和信息消息(查询消息)。ICMPv4被用于ping和traceroute等诊断工具。
一、内容详细讲解
1. ping 工具
- 原理:属于用户空间程序(iputils 包),通过原始套接字发送
ICMP_ECHO
消息,目标主机收到后返回ICMP_REPLY
消息,以此检测网络连通性。- 核心功能:验证主机是否可达,测量网络延迟。
2. traceroute 工具
- 原理:
初始探测(TTL=1):
- traceroute 首先发送一个 TTL(生存时间)为 1 的数据包(默认用 UDP 协议)。当数据包到达第一跳路由器时,路由器会将 TTL 减 1,此时 TTL 变为 0,路由器无法继续转发该数据包,会向源主机返回
ICMP_TIME_EXCEED
消息(超时消息)。通过这个消息,traceroute 就记录下了第一跳路由的信息。逐跳探测(TTL 递增):
- 每次收到
ICMP_TIME_EXCEED
或ICMP_DEST_UNREACH
(目标不可达,如最终目标端口不可达)消息后,traceroute 会将新发送数据包的 TTL 加 1。例如,第二次发送 TTL=2 的数据包,此时数据包会经过第一跳路由,再到达第二跳路由,第二跳路由处理后 TTL 变 0,返回ICMP_TIME_EXCEED
,从而记录第二跳路由。重复此过程,直到数据包到达目标主机。目标响应阶段:
- 当 TTL 足够大,数据包最终到达目标主机。若目标主机发现 UDP 端口不可达(因 traceroute 默认用高位随机 UDP 端口,目标通常无对应服务),会返回
ICMP_DEST_UNREACH
消息,此时 traceroute 确认已到达目标,停止探测,并基于收集的超时消息生成完整的路由路径清单。- 核心功能:追踪数据包从源到目标主机的路由路径,展示经过的路由器。
二、案例演示
1. ping 案例
场景:检测百度服务器(www.baidu.com)是否连通。
命令:ping www.baidu.com
输出示例:
PING www.a.shifen.com (39.156.69.79) 56(84) bytes of data. 64 bytes from 39.156.69.79: icmp_seq=1 ttl=128 time=34.5 ms 64 bytes from 39.156.69.79: icmp_seq=2 ttl=128 time=32.1 ms # 按Ctrl+C停止,显示统计信息 --- www.a.shifen.com ping statistics --- 2 packets transmitted, 2 received, 0% packet loss, time 1001ms rtt min/avg/max/mdev = 32.104/33.302/34.500/1.198 ms
解释:发送
ICMP_ECHO
包,接收ICMP_REPLY
,显示延迟(time)和丢包率等。2. traceroute 案例
场景:追踪到百度服务器的路由路径。
命令(Linux/macOS):traceroute www.baidu.com
输出示例:
traceroute to www.a.shifen.com (39.156.69.79), 64 hops max, 52 byte packets 1 router.local 1.234 ms 0.987 ms 0.876 ms 2 192.168.1.1 5.678 ms 4.321 ms 3.456 ms 3 * * * 4 202.97.XXX.XXX 34.567 ms 32.123 ms 31.456 ms # 最终显示目标主机响应 64 39.156.69.79 45.678 ms
解释:
- 每一行显示一跳路由的 IP 和延迟,
*
表示未收到ICMP_TIME_EXCEED
消息。- 通过 TTL 递增和收集的 ICMP 消息,逐步构建路由路径,直到到达目标主机。
ICMP初始化操作是在引导阶段调用inet_init()中完成,具体如下:
2.ICMP报头
ICMPv4报头由类型(8位)、代码(8位)、校验和(16位)和32位的可变部分组成。(上图中有效载荷在ICMP报文头部之后,不属于头部信息)ICMPv4报头用结构icmphdr表示如下:
ICMPv4数据报在不超过576字节的前提下,有效载荷应该尽可能多的包含原始数据报的内容,因为 ICMP 的核心功能之一是反馈网络传输中的错误(如目标不可达、超时等)。携带原始数据报内容,能让发送方更精准地分析问题。例如,当路由设备因数据报过大无法转发时,ICMP 会包含原始数据报头部,发送方通过头部信息可判断是协议、端口或分片等问题。
同时将长度限制在576字节是因为RFC 791 作为 IPv4 的基础规范,规定 576 字节是一个 “最小保障值”。在早期网络环境中,许多设备(如路由器、主机)的处理能力有限,576 字节是平衡性能与功能的妥协值。无论设备性能如何,都能处理基础的 ICMP 消息(如错误报告、网络探测),避免因报文长度不兼容导致网络诊断功能失效。例如,ping
等工具依赖 ICMP,若主机不支持 576 字节,会导致基础网络检测功能无法使用。
3.ping相关
当主机接收到一个网络数据包时,数据包会从网络层开始处理,逐步经过一系列的处理函数,最终到达应用层或者完成相应的协议处理。对于 ICMP 回显应答消息,其处理流程大致如下:
- 数据包首先被网络设备接收,经过网络驱动层的处理后,进入网络协议栈的 IP 层。
- 在 IP 层,数据包会经过一系列的处理,详细过程见:(Linux内核网络层分析-CSDN博客),最终调用
ip_local_deliver_finish
函数,该函数负责将数据包从网络层交付给相应的上层协议处理。 - 因为接收到的是 ICMP 回显应答消息,所以会进入 ICMP 协议的处理流程,此时会调用
ping_rcv
函数来专门处理 ICMP 回显应答。
ip_local_deliver_finish
函数这个函数是 IP 层处理本地接收数据包的最后一步,它的主要任务是将数据包从 IP 层交付给上层协议。在处理 ICMP 数据包时,
ip_local_deliver_finish
会根据数据包的协议类型(这里是 ICMP),将数据包传递给 ICMP 协议的处理函数。具体来说,它会根据 ICMP 消息的类型调用不同的处理函数,对于 ICMP 回显应答消息,后续会触发相应的处理流程,最终可能会调用ping_rcv
函数。
ping_rcv
函数
ping_rcv
函数是专门用来处理 ICMP 回显应答消息的。当ip_local_deliver_finish
函数将 ICMP 回显应答消息传递给 ICMP 协议处理流程后,ping_rcv
函数会被调用,它会从数据包中提取 ICMP 头部信息,记录id
和sequence
等关键信息,完成对 ICMP 回显应答消息的处理。
而接下来 ping_rcv
处理完 ICMP 回显应答后,通过以下流程将数据交付给应用层:
1. 匹配目标套接字
- 应用层(如
ping
程序)会提前创建 原始套接字(SOCK_RAW
) 并绑定监听。内核维护一个套接字列表,记录所有监听 ICMP 协议的套接字。 ping_rcv
处理 ICMP 回显应答时,会根据报文中的关键信息(如id
、sequence
),匹配用户空间创建的原始套接字。例如,ping
程序发送请求时会设置唯一的id
,应答返回时通过该id
找到对应的套接字。
2. 数据从内核空间拷贝到用户空间
- 找到匹配的套接字后,内核通过套接字的发送队列,将
sk_buff
(存储 ICMP 应答数据的缓冲区)中的数据从内核空间拷贝到用户空间。 - 这一过程依赖套接字接口实现,应用层通过调用
recvfrom
等函数,从套接字缓冲区中读取数据。
3. 应用层处理数据
- 用户空间的
ping
程序调用recvfrom
等函数时,会阻塞等待数据(或非阻塞检查)。一旦数据拷贝完成,函数返回,ping
程序即可解析 ICMP 应答内容(如计算延迟、统计丢包率),最终将结果展示给用户(如显示time=XX ms
)。
- 传统方式:早期发送
ping
必须使用 原始套接字(SOCK_RAW
),需用户空间程序自己构造完整的 IP 头部、ICMP 头部,开发复杂且权限要求高(需管理员权限)。- 新方式:引入对 ICMP 协议的套接字支持后,可通过
socket(PF_INET, SOCK_DGRAM, PROT_ICMP)
创建套接字。这种方式类似 UDP 套接字(SOCK_DGRAM
),但实际基于 ICMP 协议。它简化了ping
开发:
- 内核自动处理部分底层细节(如 IP 头部、校验和计算),无需用户空间完全手动构造报文。
- 降低开发门槛,无需原始套接字的复杂操作,让普通程序也能更便捷地发送 ICMP 回显请求(
ping
数据包)。
4.接收ICMPv4消息
方法ip_local_deliver_finish()处理目的地址为当前机器的数据包。收到ICMP数据包后,此方法就将其交给注册ICMPv4协议的原始套接字。在方法icmp_rcv()中进行处理。
1. 消息接收的前提条件
- 传输方式:针对以 广播 或 组播 方式发送的 ICMPv4 数据包。
- 广播是向网络中所有主机发送数据;组播是向特定组内的主机发送数据,二者都是一对多的传输方式。
- 消息类型限定:仅处理
ICMP_ECHO
(回显请求,如ping
工具发送的请求)和ICMP_TIMESTAMP
(时间戳请求,用于获取网络延迟等时间信息)这两种类型的消息。2. 请求合法性检查
当收到符合上述条件的消息(广播 / 组播 +
ICMP_ECHO
/ICMP_TIMESTAMP
)时,系统会 读取特定变量(如系统配置中的访问控制参数),核实当前网络环境是否允许对 “广播 / 组播回答案请求” 做出响应。
- 这一检查是出于安全和策略考虑。例如,若允许随意响应广播 / 组播的
ICMP_ECHO
请求,可能被利用发起网络攻击(如前文提到的 Smurf 攻击),因此需通过配置判断是否放行。3.
ICMP_ECHO
消息的处理当收到
ICMP_ECHO
消息(即ping
工具发送的回显请求)时,系统会调用icmp_echo()
方法进行处理。
- 该方法的核心作用是生成
ICMP_ECHO_REPLY
应答消息,回复给发送方,完成ping
命令的响应流程,最终让用户通过ping
结果判断网络连通性。
5.发送ICMPv4消息
用于发送ICMPv4消息的方法有两个:一个是方法icmp_reply(),用于发送两种ICMP请求的相应;二是方法icmp_send(),用于发送当前机器在特定条件下主动发送的ICMPv4消息。
一、
icmp_reply
方法
- 功能:专门用于发送 ICMP 请求的响应消息,属于 “被动回复”。当系统接收到特定类型的 ICMP 请求(如
ICMP_ECHO
回显请求)时,通过此方法生成对应的应答消息。- 典型场景:
例如,主机收到ping
命令发送的ICMP_ECHO_REQUEST
消息后,调用icmp_reply
发送ICMP_ECHO_REPLY
消息,告知发送方 “目标可达”,完成一次ping
的响应流程。二、
icmp_send
方法
- 功能:用于在特定条件下主动发送 ICMPv4 消息,属于 “主动触发”。当系统检测到网络层的某些事件(如传输错误、状态变化)时,通过此方法主动生成并发送 ICMP 消息。
- 典型场景:
例如,当路由器处理数据包时发现目标不可达(如路由表无对应条目),会通过icmp_send
发送ICMP_DEST_UNREACH
(目标不可达)消息,通知源主机传输失败。三、二者的核心区别
对比维度 icmp_reply
icmp_send
触发方式 被动触发:基于接收到的 ICMP 请求 主动触发:基于系统内部网络事件或状态 应用场景 处理请求 - 响应场景(如 ping
应答)处理网络错误报告、状态通知等场景 消息类型关联 多与请求消息类型强绑定(如回复 ECHO
)支持更广泛的 ICMP 类型(如超时、重定向等
6.消息处理逻及错误场景分析
一、ICMPv4 消息接收处理逻辑
1. 处理范围与条件
当系统收到以 广播(发给网络中所有设备)或 组播(发给特定组内设备)方式传输的 ICMPv4 消息,且消息类型是
ICMP_ECHO
(回显请求,如ping
命令发送的请求)或ICMP_TIMESTAMP
(时间戳请求,用于获取网络时间信息)时,系统会先读取内部配置变量(如访问控制设置),判断是否允许对这类广播 / 组播请求进行回应。
- 举例:若某企业网络配置了禁止回应广播的
ICMP_ECHO
请求,当收到外部发送的广播ping
请求时,系统会根据配置拒绝回应,避免被利用发起攻击(如 Smurf 攻击)。2.
ICMP_ECHO
消息的处理如果收到的是
ICMP_ECHO
消息(即ping
命令的回显请求),系统会调用icmp_echo()
方法处理。该方法的核心是生成ICMP_ECHO_REPLY
应答消息,告诉发送方 “目标可达”。
- 举例:主机 A 对主机 B 发起
ping
,主机 B 收到ICMP_ECHO
请求后,通过icmp_echo()
组织回应消息,最后主机 A 收到应答,显示ping
成功。
二、ICMPv4 消息发送逻辑
1. 回应
ICMP_ECHO
和ICMP_TIMESTAMP
的发送前面提到的发送 ICMP 消息的方法(如
icmp_reply
等),最终都会调用icmp_push_reply()
函数,来回应ICMP_ECHO
和ICMP_TIMESTAMP
消息。它是生成并发送这两类应答消息的关键函数。
- 举例:主机收到
ICMP_ECHO
请求后,通过icmp_push_reply()
打包ICMP_ECHO_REPLY
消息,填充正确的头部信息(如类型、校验和),然后发送给请求方。2.
icmp_send()
的广泛应用在 IPv4 网络的多个场景(如网络过滤模块 Netfilter、数据包转发代码
ip_forward.c
、隧道技术ip_gre
等)中,若需要主动发送 ICMP 消息(如报告网络错误),会调用icmp_send()
。
- 举例:路由器转发数据包时,发现目标网络不可达,就通过
icmp_send()
发送ICMP_DEST_UNREACH
(目标不可达)消息,通知源主机传输失败。
三、ICMP_PROT_UNREACH(协议不可达)
当 IP 数据报头部的 “协议字段”(8 位,标识上层协议,如 TCP 是 6,UDP 是 17)指定的协议在接收方不存在时,接收方会向发送方发送
ICMP_DEST_UNREACH
消息,且代码为ICMP_PROT_UNREACH
(协议不可达)。
- 举例:发送方构造了一个 IP 数据报,协议字段填了一个不存在的协议值(如 255),当接收方解析时,发现没有对应的协议处理模块,就会发送
ICMP_PROT_UNREACH
消息给发送方,告知 “你指定的协议我不支持”。
由于ICMP主要作用是发送有关网络层错误和控制消息的机制,所以以下情况也是ICMP的功能:
1. 对于端口不可达的处理:
当应用层程序 A 使用 UDP 套接字,向目标 IP 地址的某端口(如端口 Y)发送数据时,若目标主机上的端口 Y 没有任何 UDP 套接字监听(即没有应用程序绑定该端口),目标主机接收该 UDP 数据包后会执行以下操作:
1.先检查数据包校验和,若校验和 正确,目标主机会通过 ICMP 协议向发送方(程序 A 所在主机)发送 ICMP_PORT_UNREACH
消息,告知 “端口不可达”;
2.若校验和 错误,则直接丢弃数据包,不触发 ICMP 消息发送。
举例:
开发一个 UDP 客户端程序,向服务器的 9999 端口发送数据。若服务器端未启动任何监听 9999 端口的程序,服务器收到客户端的 UDP 数据包后,因无匹配的 UDP 套接字,且若校验和正确,就会触发 ICMP 发送 “端口不可达” 消息。此时,客户端所在主机可能收到该 ICMP 消息,应用层可感知此次 UDP 通信因端口问题失败。
2.对于不存在协议的处理:
当 IPv4 报头中的协议号错误(不在协议号列表中),或内核不支持该协议(协议未注册,协议处理程序数组无对应条目),导致数据包无法处理时,接收方需向发送方回发 ICMPv4 “目的地不可达” 消息,并用代码ICMP_PROT_UNREACH
标明 “协议不可达” 的错误原因。
1.先通过xfrm4_policy_check
检测 IPsec 策略(非raw
模式时)。若策略检测通过,就调用icmp_send
发送 ICMP 消息,明确类型为ICMP_DEST_UNREACH
、代码为ICMP_PROT_UNREACH
,向发送方告知协议不可达;
2.最后通过kfree_skb
释放数据包缓冲区资源。
举例:
若开发一个网络程序 A,错误地将 IPv4 报头协议号设为无效值(如 255)并发送给目标主机。目标主机解析时发现协议号无效且自身不支持该协议,便触发此处理逻辑 —— 通过icmp_send
向程序 A 所在主机发送ICMP_PROT_UNREACH
消息。程序 A 所在主机接收该消息后,应用层可感知此次通信因协议号错误而失败,可能显示 “协议不可达,无法处理数据包” 之类的提示。
3.长度超过MTU
当转发数据包时,若数据包长度超过外出链路的 MTU(最大传输单元),且 IPv4 报头中未设置分段位(IP_DF
,即不允许分片),就会触发此错误。此时目标主机会丢弃数据包,并通过 ICMP 协议向发送方返回代码为 ICMP_FRAG_NEEDED
的 ICMP_DEST_UNREACH
消息,告知发送方 “数据包过大且不允许分片,无法转发”。
举例:服务器 A 向服务器 B 发送一个较大的 UDP 数据包,中间某路由器转发时发现数据包长度超过链路 MTU,且 IP 报头设置了不分片。路由器丢弃数据包后,向服务器 A 发送 ICMP_FRAG_NEEDED
消息。若服务器 A 的应用层监听此类消息,会感知到此次传输因 “数据包过大且不能分片” 而失败。
4.严格路由选择和网关选项被设置
转发数据包时,若数据包同时设置了严格路由选择(strict routing)和网关(gatewaying)选项,会触发此错误。目标主机会丢弃数据包,并发送代码为 ICMP_SR_FAILED
的 “目的地不可达” ICMP 消息,向发送方反馈 “因严格路由和网关选项冲突,无法转发”。
举例:开发一个网络程序,发送数据包时错误配置了严格路由选择和网关选项。当数据包到达转发设备,设备因这两个选项冲突无法处理,便丢弃数据包并向程序所在主机发送 ICMP_SR_FAILED
消息。程序接收该消息后,可判断传输失败原因是路由选项设置错误。
7.应用层代码案例
icmp_saw.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip_icmp.h>
#include <arpa/inet.h>
#include <time.h>
#include <sys/time.h>
#include <errno.h>
// 该校验和算法通过累加数据的 16 位分组,进行折叠操作并取反,得到一个 16 位的校验和,用于检测数据在传输过程中是否发生错误
unsigned short checksum(unsigned short *buf, int len) {
unsigned int sum = 0;
unsigned short result;
for (sum = 0; len > 1; len -= 2)
sum += *buf++;
if (len == 1)
sum += *(unsigned char *)buf;
sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);
result = ~sum;
return result;
}
// 发送 ICMP 请求
/*
这行代码定义了一个名为 send_icmp_packet 的函数,其返回值类型为 void,即不返回任何值。该函数接收三个参数:
sockfd:一个整数类型的参数,代表用于发送数据的套接字文件描述符。通过这个套接字可以进行网络通信。
dest_addr:一个指向 struct sockaddr_in 类型的指针,此结构体包含目标主机的 IP 地址和端口号等信息,这里主要是目标主机的 IPv4 地址。
seq:一个整数类型的参数,代表 ICMP 数据包的序列号,用于标识不同的 ICMP 请求。
*/
void send_icmp_packet(int sockfd, struct sockaddr_in *dest_addr, int seq) {
struct icmp icmp_packet;
memset(&icmp_packet, 0, sizeof(icmp_packet));
//icmp_packet.icmp_type = ICMP_ECHO;:将 ICMP 数据包的类型字段 icmp_type 设置为 ICMP_ECHO,这表明该数据包是一个 ICMP 回显请求(Ping 请求)。
icmp_packet.icmp_type = ICMP_ECHO;
//icmp_packet.icmp_code = 0;:将 ICMP 数据包的代码字段 icmp_code 设置为 0,对于 ICMP 回显请求,代码字段通常为 0。
icmp_packet.icmp_code = 0;
icmp_packet.icmp_id = getpid() & 0xFFFF;
//将 ICMP 数据包的序列号字段 icmp_seq 设置为传入的参数 seq,用于标识不同的 ICMP 请求,方便后续处理响应时进行匹配。
icmp_packet.icmp_seq = seq;
//将 ICMP 数据包的校验和字段 icmp_cksum 初始化为 0。在计算校验和时,需要先将该字段置为 0,然后再计算整个 ICMP 数据包的校验和
icmp_packet.icmp_cksum = 0;
icmp_packet.icmp_cksum = checksum((unsigned short *)&icmp_packet, sizeof(icmp_packet));
sendto(sockfd, &icmp_packet, sizeof(icmp_packet), 0, (struct sockaddr *)dest_addr, sizeof(*dest_addr));
}
// 接收 ICMP 响应
/*
参数说明:
sockfd:表示用于接收数据的套接字文件描述符。
src_addr:指向 struct sockaddr_in 结构体的指针,用于存储发送方的地址信息。
seq:ICMP 数据包的序列号,用于匹配请求和响应。
start_time:指向 struct timeval 结构体的指针,记录发送请求的时间,用于计算 RTT。
*/
int receive_icmp_packet(int sockfd, struct sockaddr_in *src_addr, int seq, struct timeval *start_time) {
char buffer[1024];
struct ip *ip_header;
struct icmp *icmp_header;
socklen_t addrlen = sizeof(*src_addr);
struct timeval end_time;
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
struct timeval timeout;
timeout.tv_sec = 2;
timeout.tv_usec = 0;
int select_result = select(sockfd + 1, &readfds, NULL, NULL, &timeout);
if (select_result == -1) {
perror("select");
return -1;
} else if (select_result == 0) {
printf("Request timed out.\n");
return -1;
}
while (1) {
ssize_t recv_len = recvfrom(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr *)src_addr, &addrlen);
if (recv_len == -1) {
perror("recvfrom");
return -1;
}
gettimeofday(&end_time, NULL);
ip_header = (struct ip *)buffer;
icmp_header = (struct icmp *)(buffer + (ip_header->ip_hl << 2)); // 跳过 IP 首部
//printf("Received packet: IP src=%s, ICMP type=%d, ICMP id=%d, ICMP seq=%d\n",
//inet_ntoa(src_addr->sin_addr), icmp_header->icmp_type, icmp_header->icmp_id, icmp_header->icmp_seq);
if (icmp_header->icmp_type == ICMP_ECHOREPLY && icmp_header->icmp_id == (getpid() & 0xFFFF) && icmp_header->icmp_seq == seq) {
double rtt = (end_time.tv_sec - start_time->tv_sec) * 1000.0 + (end_time.tv_usec - start_time->tv_usec) / 1000.0;
printf("Received ICMP reply from %s in %.2f ms\n", inet_ntoa(src_addr->sin_addr), rtt);
return 0;
}
}
return -1;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <destination_ip>\n", argv[0]);
return 1;
}
int sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP);
if (sockfd == -1) {
perror("socket");
return 1;
}
struct sockaddr_in dest_addr;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
if (inet_pton(AF_INET, argv[1], &dest_addr.sin_addr) <= 0) {
perror("inet_pton");
close(sockfd);
return 1;
}
int count = 0;
const int MAX_COUNT = 10; // 最大发送次数
const int INTERVAL = 1; // 发送间隔(秒)
while (count < MAX_COUNT) {
struct timeval start_time;
gettimeofday(&start_time, NULL);
send_icmp_packet(sockfd, &dest_addr, count + 1);
receive_icmp_packet(sockfd, &dest_addr, count + 1, &start_time);
sleep(INTERVAL);
count++;
}
close(sockfd);
return 0;
}
从下图不妨看出效果完美复刻:
二、发送/接收ICMPv6消息
1.ICMPv6初始化
ICMPv6是ipv6的有机组成部分,每个节点都必须全面实现,在ipv6中,ICMPv6除了用于错误处理和诊断外,还被用于邻居发现(ND)协议以组播侦听者发现(MLD)协议。
ICMPv6初始化是由方法icmpv6_init()和icmp_sk_init()完成。
ICMPv6和ICMPv4基本差不多,也为每个CPU都创建一个原始ICMPv6套接字,并将它们存储在一个数组中,要访问当前sk,可以调用方法icmpv6_sk()。
2.ICMPv6报头
在 ICMP(互联网控制消息协议)中,“类型” 字段用于区分报文种类,其取值通过最高位划分两大功能类别:
1. 最高位为 0(取值 0 - 127):错误消息
用于报告网络通信中的错误状态,例如:
ICMP_DEST_UNREACH
(值 1):目标不可达(如路由不可达、端口不可达);ICMP_PKT_TOOBIG
(值 2):数据包过长,需分片传输;
这类消息用于通知发送方 “通信过程中出现错误”,帮助排查网络故障。
2. 最高位为 1(取值 128 - 255):信息消息
用于实现网络测试、信息交互等功能,例如:
ICMP_ECHO_REQUEST
(值 128):回显请求(Ping 工具发送的请求包,主动询问对端是否可达);ICMP_ECHO_REPLY
(值 129):回显应答(对ECHO_REQUEST
的响应,告知发送方 “已收到请求”);
这类消息是主动发起的交互行为,用于测试网络连通性或获取特定信息。
用户提供的代码中,ICMPV6_ECHO_REQUEST
等宏定义对应信息消息类型,而 0 - 127 范围内的类型值(如代码中未完全展示的错误消息类型)则负责反馈网络传输中的异常情况。
3.接收ICMPv6消息方法
收到的ICMP数据包被交给方法icmpv6_rcv(),具体方法如下:
4.发送ICMPv6消息
发送ICMPv6消息的主要方法为icmpv6_send(),具体源码如下:
5.错误处理
1. ICMPv6 跳数限制超时
触发场景:IPv6 数据包转发过程中,每台主机都会将报头的 “跳数限制计数器” 减 1。当该计数器减为 0 时,触发此错误。
主机操作:
- 调用
icmpv6_send()
函数,发送一条代码为ICMPV6_EXC_HOPLIMIT
的ICMPV6_TIME_EXCEED
消息,告知发送方 “数据包因跳数限制超时被丢弃”; - 更新统计信息(如
IPSTATS_MIB_INHDRERRORS
),记录错误事件; - 丢弃当前数据包,终止其转发流程。
2. ICMPv6 分段重组超时
触发场景:处理 IPv6 数据包分段重组时,超过规定时间仍未完成重组(如分段丢失、重组条件不满足)。
主机操作:
- 调用
icmpv6_send()
函数,发送代码为ICMPV6_EXC_FRAGTIME
的ICMPV6_TIME_EXCEED
消息,向发送方报告 “分段重组超时”; - 丢弃与该重组相关的分段数据,释放资源,避免无效占用。
3. ICMPv6 参数问题
触发场景:解析 IPv6 扩展报头时,发现报头存在参数错误(如选项不识别、格式异常)。
主机操作:
- 调用
icmpv6_param_prob()
函数,通过icmpv6_send()
发送代码为ICMPV6_UNK_OPTION
的ICMPV6_PARAMPROB
消息,告知发送方 “报头参数存在问题”; - 丢弃当前有参数问题的数据包,阻止错误数据继续传输;
- 可能记录错误统计信息,辅助后续网络故障分析。