简介:这是一份用于演示ICMP(互联网控制消息协议)数据包构造与发送的编程资源,面向学习网络编程、协议分析或从事网络诊断的开发人员,也可作为计算机网络课程实验的参考。资源由C++源文件、头文件及说明文档组成,共3个文件,压缩包仅5KB,代码结构精简,便于直接阅读和编译运行。其中C++源文件负责ICMP报文填充与校验和计算,头文件定义报文结构,说明文档补充了构造流程与抓包验证方法。已有1605人学习下载。通过阅读源码和说明,可以掌握ICMP报文头部的类型、代码字段组织方式,理解差错报告报文和查询报文的区别,以及如何将ICMP报文封装进IP数据报,并借助Wireshark抓包验证构造正确性。该资源能帮助读者从底层理解网络通信机制,为后续实现Ping工具、Traceroute或网络监测功能打下基础。 先说结论:如果你准备在网络排查、协议分析这类场景里自家尝试“造”一个ICMP包,这篇文章可以节省你至少一个下午的折腾时间。
作为常年跟网络抓包、设备联调打交道的人,我对ICMP一直有种“最熟悉的陌生人”的感觉。平时排查不通,第一反应就是ping一下,但真要自己从零构造一个ICMP报文,把Type、Code、Checksum这些字段逐一填对的时候,还是遇到不少细节问题。这篇就基于我实际调试过的经验,把ICMP数据包构造这件事拆开揉碎,从报文格式讲到抓包验证,再到常见设备的调试命令,一次说清楚。
这篇文章更适合这几类人看:刚接手网络维护、手里一堆ping不通故障单的运维新人;正在学TCP/IP协议栈、想用代码验证理论的学生;以及做网络工具开发、需要在程序里自定义探测包的开发者。不管是纯原理学习还是应付具体工作,下面这些内容都能直接落地去试。
1. 动手之前先摸清底细:ICMP报文格式与核心字段
构造数据包这事有个基本逻辑:你得先知道每个字节是干什么的,才能谈“构造”。ICMP虽然看起来简单,但它的报文结构里藏着不少门道。
1.1 ICMP报文整体布局
ICMP报文分为两部分:报文头和数据部分。报文头固定8个字节,由类型(Type)、代码(Code)、校验和(Checksum)三部分组成,后面跟着4字节的“剩余头部信息”,这个剩余头部内容会根据报文类型发生变化。
以最常用的 Echo Request(ping请求包)为例,整体布局是:
| 偏移量 | 字段 | 长度 | 说明 |
|---|---|---|---|
| 0 | Type | 1字节 | 标识报文类型,Echo Request为8 |
| 1 | Code | 1字节 | 类型下的细分代码,ping时为0 |
| 2 | Checksum | 2字节 | 对ICMP报文头和数据部分的校验和 |
| 4 | Identifier | 2字节 | 用于匹配请求和应答,通常填进程ID |
| 6 | Sequence Number | 2字节 | 序号,每发一个包递增 |
| 8 | Data | 可变 | 载荷数据,可以是任意内容 |
这里有一个新手容易忽略的点:ICMP报文本身不携带端口号,它依赖Identifier和Sequence Number来区分不同的“会话”。这也是为什么我们在构造时不能随便填这两个字段,否则对端回包时你可能认不出来。
1.2 Type与Code:决定报文“身份”的两个关键字节
真正的ICMP报文类型远不止ping用到的8和0。比如目标不可达是Type 3,它下面又细分了十几个Code,网络不可达是0、主机不可达是1、端口不可达是3、需要分片但设置了不分片标志是4。超时是Type 11,Code 0表示传输过程中TTL耗尽,Code 1表示分片重组超时,traceroute就是这个原理。
构造前一定要想清楚你要模拟的是哪种ICMP行为,然后对照RFC 792把Type和Code填对。填错了对端可能直接丢弃,也可能回复一个意外的差错报文,排查起来非常痛苦。
1.3 校验和计算逻辑
校验和是最容易翻车的地方。ICMP的Checksum计算范围和IP头校验和不同,它覆盖ICMP报文头加整个数据部分,而且是先置0再计算。具体步骤:
- 将校验和字段先置为0;
- 将ICMP报文(包括数据部分)按16位一组划分;
- 如果总长度是奇数,末尾补一个0字节;
- 把所有16位字进行二进制反码求和;
- 将结果取反,填入校验和字段。
这地方用Python的Scapy自动处理,问题不大。但如果用C语言的原始套接字裸写,校验和出错是最常见的问题。后面实操部分我会给出一份可以直接用的校验和计算函数。
2. 工具选型与环境准备:为什么我推荐Scapy
构造ICMP包的方案不少,各有各的适用场景。我在实测过程中把三种常见方案都试了一遍,用下来感受差异很明显。
2.1 三种常见实现方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 系统ping命令 | 零成本,最可靠 | 无法自定义Type/Code和载荷 | 日常连通性测试 |
| Python Scapy | 灵活,代码量小,支持网络层全字段定制 | 依赖第三方库,性能一般 | 协议研究、自动化脚本、快速构造任意ICMP包 |
| C语言原始套接字 | 性能高,完全可控,不依赖解释器 | 代码复杂,校验和要自己算,需要root权限 | 高性能探测工具、嵌入式网络设备开发 |
个人建议是:学习阶段直接上Scapy,因为Scapy已经帮你封装好了完整的数据包组装能力,构造一个ICMP包三行代码就搞定,还自带发送和接收函数,非常适合验证理论。如果你要封装成正式给生产环境用的工具,再考虑用C或Go重写,那属于工程优化范畴。
2.2 环境安装与最小验证
我用的环境是Ubuntu 22.04,Python和Scapy的安装属于标准操作:
sudo apt update sudo apt install python3 python3-pip sudo pip3 install scapy装完后可以先跑一个小测试,确认Scapy能正常发送和接收报文。测试之前要确保系统允许ping命令正常通行,有的安全策略会禁掉ICMP,那种环境下报文发出去可能没有回应。
运行下面的代码,如果能收到回包,说明环境没问题,就可以往下走了:
from scapy.all import * p = IP(dst="114.114.114.114")/ICMP() reply = sr1(p, timeout=2, verbose=False) if reply: print("收到回复,Type =", reply[ICMP].type) else: print("无回复")这里有个细节:Scapy在sr1返回的是完整应答包,我们要从应答里取ICMP层去判断Type,直接从结果里打印type就是0了,因为Echo Reply的Type就是0。
3. 从零构造ICMP数据包:三种典型场景实操
环境没问题后,我们正式进入构造环节。我挑了三种有代表性的场景,从最基础的ping包到自定义载荷再到纯手工C语言构造,每层难度递增,但每层都能让你对ICMP的理解加深一步。
3.1 场景一:标准Echo Request(ping包)
最标准的ICMP Echo Request就是ping命令发的包。用Scapy构造非常直观:
from scapy.all import * # 构造一个ICMP Echo Request icmp_packet = IP(dst="192.168.1.1")/ICMP(type=8, code=0) # 发送并等待回复 reply = sr1(icmp_packet, timeout=3, verbose=False) if reply: print("目标地址:", reply[IP].src) print("回复Type:", reply[ICMP].type) print("回复Code:", reply[ICMP].code) else: print("超时无回复")这里要注意一个点:ICMP(type=8)是请求,ICMP(type=0)是回复。构造时type一定要填8,你填0的话就成了应答包,在正常情况下没人会主动应答一个应答包。
另外前面说到的Identifier和Sequence Number,Scapy会自动帮你填默认值。实际使用中建议手动指定,比如用进程ID做Identifier,这样在抓包工具里便于区分是本机哪个程序发出的报文。
3.2 场景二:带自定义载荷的探测包
标准的ping包数据部分是固定的56字节内容,但ICMP对载荷部分并没有强制要求。这给了我们一个很重要的应用场景:在一些网络质量监测系统里,会用自定义载荷验证链路的数据完整性。
实际构造时可以这样:
from scapy.all import * # 构造一个携带自定义内容的ICMP包 payload = b"network-check-data-2024" packet = IP(dst="192.168.1.1")/ICMP(type=8, code=0)/payload reply = sr1(packet, timeout=3, verbose=False) if reply: # 对比回显的data部分是否一致 if payload in bytes(reply[Raw].load): print("载荷验证通过") else: print("载荷不一致,链路存在数据篡改风险") else: print("超时")这里需要补一个背景:ICMP Echo Reply会把Echo Request里的数据部分原样带回,正是因为这种机制,我们才能通过比对载荷内容来评估链路质量。曾经有客户反映某个跨地市专线业务时断时续,但普通ping一直是通的,后来就是用超长自定义载荷的ICMP包发现的MTU问题。
3.3 场景三:C语言版原始套接字构造
再上一点难度,用C语言的原始套接字手工构造。说实话现在很多年轻同行已经不怎么用这种底层写法了,但我一直觉得这是理解协议的最好方式,因为你必须亲自动手处理每一个字段。
核心代码框架如下:
#include <stdio.h> #include <string.h> #include <sys/socket.h> #include <netinet/ip.h> #include <netinet/ip_icmp.h> #include <arpa/inet.h> #include <unistd.h> // ICMP校验和计算 unsigned short cal_checksum(void *addr, int len) { unsigned short *buf = (unsigned short *)addr; unsigned int sum = 0; while (len > 1) { sum += *buf++; len -= 2; } if (len == 1) { sum += *(unsigned char *)buf; } sum = (sum >> 16) + (sum & 0xffff); sum += (sum >> 16); return (unsigned short)(~sum); } int main(int argc, char *argv[]) { if (argc != 2) { printf("Usage: %s <target_ip>\n", argv[0]); return 1; } int sockfd; char packet[64]; struct iphdr *ip_hdr = (struct iphdr *)packet; struct icmphdr *icmp_hdr = (struct icmphdr *)(packet + sizeof(struct iphdr)); struct sockaddr_in dst_addr; // 创建原始套接字,协议类型为ICMP sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_ICMP); if (sockfd < 0) { perror("socket error"); return 1; } memset(packet, 0, sizeof(packet)); // 填充IP头 ip_hdr->ihl = 5; ip_hdr->version = 4; ip_hdr->ttl = 64; ip_hdr->protocol = IPPROTO_ICMP; ip_hdr->saddr = inet_addr("0.0.0.0"); // 由系统自动填充 ip_hdr->daddr = inet_addr(argv[1]); ip_hdr->tot_len = htons(sizeof(packet)); // 填充ICMP头 icmp_hdr->type = ICMP_ECHO; icmp_hdr->code = 0; icmp_hdr->un.echo.id = htons(getpid()); icmp_hdr->un.echo.sequence = htons(1); icmp_hdr->checksum = 0; icmp_hdr->checksum = cal_checksum(icmp_hdr, sizeof(struct icmphdr)); // 填充目标地址 dst_addr.sin_family = AF_INET; dst_addr.sin_addr.s_addr = inet_addr(argv[1]); // 发送 if (sendto(sockfd, packet, sizeof(packet), 0, (struct sockaddr *)&dst_addr, sizeof(dst_addr)) < 0) { perror("sendto error"); return 1; } printf("ICMP Echo Request sent to %s\n", argv[1]); close(sockfd); return 0; }编译并执行:
gcc -o myping myping.c sudo ./myping 192.168.1.1注意这个程序需要root权限执行,因为SOCK_RAW原始套接字的创建本身就需要特权。
这里再解释一下为什么要手动算checksum。很多初学者第一次跑C语言版构造的ICMP包,发现对端不回包或者回包被丢弃,八九不离十是校验和错了。IP头里的校验和是为了校验IP头本身的完整性,ICMP头里的校验和是为了校验整个ICMP报文的完整性,两者计算内容和范围完全不同,别混为一谈。
4. 构造后的验证与抓包排查
包发出去不叫完事,抓包确认自己构造的包格式正确才算真正闭环。这一部分我结合自己的踩坑经历,讲讲验证环节的几个关键点。
4.1 用Wireshark验证自建包的格式
抓包工具最常用的是Wireshark,启动抓包后应用过滤条件:
icmp如果你构造的是标准Echo Request,抓到后展开ICMP层,应该能看到Type为8,Code为0,Checksum显示为correct。如果Checksum那里显示incorrect或者wrong,那就说明构造的包在格式上还有问题。
最好把发送端抓包和接收端抓包对比看。我自己调试时发现,从本机发送的包在Wireshark里看到的源IP可能不是你预期的那一个,比如你本机有多块网卡、多个IP,内核会自动选择路由出口,源IP由系统决定。这是正常现象,不需要太纠结。
4.2 常见误区:为什么设置了udp过滤却还能看到ICMP
热搜词里有一条很典型:“Wireshark添加了滤波条件udp但是我还是抓到了icmp的数据”。这个问题我见过不少回帖问过,实际上是因为Wireshark的显示过滤器分为两种:一种是显示过滤器(Display Filter),一种是在抓包前设置的捕获过滤器(Capture Filter)。
如果你在Wireshark最上方的显示过滤器里输入udp,理论上它只显示UDP报文,不会把ICMP显示出来。如果还是能看到ICMP,最常见的原因是输入过滤器的时候把语法弄错了,比如输入成了udp port 53,但抓包文件里同时存在DNS(UDP 53)和DNS相关的ICMP差错报文,某些差错报文里会携带原始DNS报文的内容,Wireshark会把这部分ICMP报文显示出来,导致看起来像“过滤失败”。
另一个可能原因是抓包是在远程接口上做的,本地Wireshark打开的抓包文件里过滤器没有正确生效,这时重新在显示过滤栏输入icmp并回车确认一下语法是否正确就可以。
如果确实想从源头只抓UDP包、不要任何ICMP,应该在抓包选项里用捕获过滤器写:
udp捕获过滤器是BPF语法,在Wireshark主界面选择“捕获”菜单,设置里填入上面这个表达式。这样在网卡层面就把ICMP丢弃了,根本不会进入抓包结果。
4.3 网络设备端调试:telnet debug icmp
很多情况下问题出在交换机、路由器等网络设备上,这时需要在设备上开启调试。常见厂商设备的操作思路类似,这里以华为设备为例,命令过程是:
<设备> system-view [设备] undo info-center enable [设备] quit <设备> debugging ip icmp <设备> terminal monitor <设备> terminal debugging打开之后,设备收发ICMP报文时控制台会打印详细信息。这个功能在工程师圈子里常被称为“debug icmp”,你可以直观看到设备是否收到了ICMP请求、回包失败的原因、丢弃报文的可能位置。这是排查“为什么我手工构造的ICMP包设备不回”的利器。
使用完一定要记得关掉:
<设备> undo debugging ip icmp <设备> undo terminal monitor注意在设备吞吐量大的业务高峰期临时开debug要谨慎。实际维护中我曾经在核心交换机上开过debug icmp,结果日志刷新速度快到屏幕根本看不清,不得已直接重启了终端会话才缓解。调试完立刻关掉是最稳妥的操作。
5. ICMP构造的实际应用场景与扩展
学会了构造ICMP包,能做的事情远不止“造一个ping包”。结合我自己的工作经历,说几个比较有价值的应用方向。
5.1 常规运维场景
最直接的应用是做网络连通性和质量检测平台。传统ping工具一次只能测一个目标,配合Scapy可以轻松写一个并发探测脚本,同时检测多个网络节点,把往返时延、丢包率汇总成报表。我在之前的项目里就是拿Scapy写了个内部监控脚本,定时探测十几个核心网元的ICMP可达性,发现问题自动告警,相当省事。
MTU问题排查也是重要场景。对于某些全链路MTU被调低,但普通ping(默认载荷只有56字节)又测不出来的链路,可以用超长ICMP载荷来试探实际可用MTU值。构造一个1400字节的ICMP包,然后不断降低载荷大小,观察哪个长度开始不再出现fragmentation needed消息,就能推断出路径上的最小MTU。
5.2 特殊场景与注意事项
有一点必须在最后强调:ICMP数据包构造能力属于双刃剑。用得好,它是高效的网络诊断工具;用不好,轻则触发网络设备的安全策略,重则涉及网络攻击行为。比如利用ICMP载荷隐藏数据的内容,虽然技术上行得通,但在大多数应用场景下属于违规行为,只应在自己的实验环境或者经过明确授权的网络测试中使用。
我在实践中的体会是,构造ICMP包的核心价值不在于“包本身”,而在于“你完全理解了报文里每个字节的含义之后,遇到问题时多了一种定位思路”。以后再遇到ping不通,你不再只会反复执行ping命令,而是能通过抓包、分析报文、构造特定探测包,一步步确认问题究竟出在哪一层。这种能够从底层视角观察和解决问题的能力,才是掌握数据包构造带来的真正提升。
本文还有配套的精品资源,点击获取