简介面向网络协议学习与网络诊断实践的 ICMP 数据包构造资料适合希望理解底层通信机制、练习报文封装与抓包验证的开发者、网络管理员及计算机相关专业学生。内容围绕 ICMP 差错报告与查询报文展开涉及类型字段、代码字段和数据字段的含义以及把 ICMP 报文封装进 IP 数据报后发送再借助 Wireshark 过滤分析、核对字段是否符合预期的验证思路可用于课程实验、协议自学和故障排查入门。压缩包共 3 个文件以 cpp 源文件、h 头文件和 txt 说明为主整体约 5KB体积轻便源文件承载构造逻辑头文件提供必要声明文本说明辅助理解实验流程便于按结构阅读与复现。已有 1608 人学习可作为网络编程与协议分析的基础参考帮助读者把报文结构、封装过程、抓包结果和验证方法对应起来形成可迁移的排错思路。1. 从一次「ping 通了但抓包对不上」说起ICMP 数据包构造到底在造什么调过网络排查的人大概都遇到过这种场面系统ping明明返回正常可抓包一看报文里的 identifier 每次都不一样序列号也对不上自己脚本发的记录排查线索断在这里。要跨过这一步就得自己做 ICMP 数据包构造——把 type、code、checksum、identifier、sequence 这 8 个字节亲手填一遍再自己收原始套接字的回包并解析。这套小工具就是干这件事的纯 Python 实现 Echo 请求的构造、校验和计算、原始套接字收发外加一个 scapy 对照版本和 TTL 递增的路径探测。它适合三类人正在学协议、想把字段和字节对应起来的需要给防火墙规则或告警策略造可控测试样本的以及想搞清楚系统 ping 背后到底发了什么的人。代码量不大抓包能一行行对照。2. 项目定位与技术选型8 个字节的头为什么值得自己拼2.1 可控性才是需求不是「重造 ping」系统自带的 ping 命令足够好用但它只发一种报文type8、code0 的 Echo 请求其余字段全由内核填。当你需要构造别的组合时它就无能为力了。这个项目真正解决的场景有四类第一类是报文级探测。防火墙、IDS 规则、云厂商的安全组对 ICMP 的处理经常是按 type/code 分的。想验证「type3 code3端口不可达会不会被放行」「type11超时会不会触发告警」就必须能造出这两种包。系统 ping 做不到scapy 一行能做但很多人不清楚底层填了什么。第二类是学习验证。抓包软件里看到的08 00 4d 5a 00 01 00 01对应代码里的struct.pack(!BBHHH, 8, 0, chksum, 1, 1)这种一一对应关系是看文档看不出来的。第三类是记录与回放。把构造出的字节流存成十六进制字符串换台机器重放用来复现某个只在特定网络位置出现的问题。第四类是自定义负载。在 payload 里塞时间戳、trace id、机器编号批量探测时能把这些信息关联起来而不是对着一堆 IP 猜。提示这个项目的边界很明确——只做单播探测报文的构造与解析不做任何形式的封装转发、不伪造源地址、不试图穿透任何网络策略。它是诊断工具不是绕过工具。2.2 两条实现路线手写 struct 与 scapy项目里同时放了两套发送实现这不是冗余是刻意的。手写路线基于struct加原始套接字。优点是零第三方依赖、每个字段都看得见缺点是校验和得自己算、收到的数据得自己剥 IP 头、跨平台差异得自己处理。这是主链路。scapy 路线只有几十行用IP()/ICMP()叠出来就行校验和自动算。它的价值是对照同一个目标两条链路各发一次用抓包工具比对字节除了 identifier 和校验和其余应该完全一致。如果对不上说明手写版的某个字段填错了——这是最快的自检手段。运行环境上手写版需要 Python 3.8 以上以及原始套接字权限类 Unix 系统下是 root 或CAP_NET_RAW能力Windows 下是管理员终端。scapy 版本随requirements.txt装仅对照时用到。没有引入 click、rich 这类 CLI 框架argparse够用少一个依赖少一个装不上的理由。2.3 目录结构把「算字节」和「发字节」彻底分开分层是这个项目最值得学的地方——纯计算和 IO 完全隔离前者可以脱离网络单测后者出问题时只需要怀疑权限和路由。icmpforge/ ├── icmpforge/ │ ├── __init__.py │ ├── checksum.py # 16 位反码求和纯函数零依赖 │ ├── packet.py # 构造与解析 ICMP 报文纯函数不碰 socket │ ├── raw_sender.py # 原始套接字收发只负责 IO │ ├── scapy_sender.py # 对照实现用于交叉验证 │ ├── trace.py # TTL 递增探测编排层 │ └── cli.py # 命令行入口只做参数解析和输出 ├── tests/ │ ├── test_checksum.py # 校验和的边界用例 │ └── test_packet.py # 构造与解析的往返一致性 ├── requirements.txt └── README.mdchecksum.py和packet.py里没有一行import socket所有函数输入bytes、输出bytes或dict。这意味着写测试时不需要 root 权限、不需要网卡、不需要联网pytest直接跑。反过来raw_sender.py里不做任何字段计算拿到bytes就发收到bytes就剥头交给packet.parse_icmp。排查问题时这个边界非常好用构造出来的十六进制串对得上抓包问题一定在收发对不上问题一定在计算。3. 核心模块拆解从字段赋值到一串十六进制字节流3.1 8 字节定长头与 struct 打包格式ICMP 的 Echo 请求头是固定 8 字节后面跟任意长度 payload。字段定义如下字段长度说明type1 字节8 表示请求0 表示应答3 表示不可达11 表示超时code1 字节细分原因Echo 恒为 0checksum2 字节覆盖整个 ICMP 报文头 负载identifier2 字节用来匹配请求与应答通常取进程号sequence2 字节同一 identifier 下的递增序号构造代码本身很短容易踩坑的是格式串import struct ICMP_ECHO_REQUEST 8 ICMP_ECHO_REPLY 0 ICMP_HEADER_LEN 8 def build_echo_request(ident: int, seq: int, payload: bytes b) - bytes: 拼一个 Echo 请求报文checksum 先填 0算完再回填 # ! 强制网络字节序且不做对齐填充这个前缀不能省 header struct.pack(!BBHHH, ICMP_ECHO_REQUEST, 0, 0, ident, seq) assert len(header) ICMP_HEADER_LEN, 头长度必须正好 8 字节 packet header payload chksum checksum(packet) # 校验和覆盖整包含 payload return struct.pack(!BBHHH, ICMP_ECHO_REQUEST, 0, chksum, ident, seq) payload格式串!BBHHH逐个拆开看!表示大端网络字节序B是 1 字节无符号整数对应 type 和 codeH是 2 字节无符号整数对应 checksum、identifier、sequence。三个H加起来 6 字节两个B加起来 2 字节总共 8 字节和协议定义严丝合缝。!这个前缀是要命的地方。如果写成BBHHHPython 会按本机原生对齐规则补填充字节在 64 位平台上长度会变成 10 字节前 8 个字节里还混进了填充的零——抓包一看identifier 位置上是错的。这个坑我在两个项目里都见人踩过。参数上有一点值得说ident建议用os.getpid() 0xFFFF因为同时跑多个探测进程时内核回包只按 identifier 和 sequence 匹配进程号天然不重复。seq从 1 开始递增别从 0 开始——有些设备的实现会把 seq0 当成异常包丢掉。payload建议至少给 8 字节太短的包在某些老设备上会被判为畸形报文塞个时间戳进去还能顺便测时钟偏移。3.2 校验和16 位反码求和的两个折叠校验和是构造 ICMP 最容易出错的一步也是唯一一段「算错了不报错、只是包被静默丢弃」的逻辑。算法本身不复杂把报文按 16 位分组求和高位进位折回低 16 位最后取反。def checksum(data: bytes) - int: 16 位反码求和返回可直接写入头部的大端整数 if len(data) % 2: data b\x00 # 奇数字节必须补零漏了会少算最后一位 total 0 for i in range(0, len(data), 2): total (data[i] 8) | data[i 1] # 手工拼大端等价于 struct.unpack(!H) total (total 0xFFFF) (total 16) # 第一次折叠把进位收回低 16 位 total (total 0xFFFF) (total 16) # 第二次折叠折叠本身可能再产生进位 return ~total 0xFFFF几个细节值得逐条说清。len(data) % 2的判断。ICMP 报文头是 8 字节payload 如果是奇数长度整包就是奇数长度。按 16 位分组时最后一个字节凑不满一组必须补一个零字节再参与求和。这是最隐蔽的 bug——恰好 payload 是偶数时一切正常换成奇数就全部超时。两次折叠。第一次折叠把累加产生的溢出位加回低 16 位但这个加法本身可能又产生一次溢出比如total是0x1FFFE折叠后变0xFFFF 0x1 0x10000所以标准做法是折两次。很多简化实现只折一次在绝大多数数据上结果碰巧一样但在特定字节组合下会差 1。~total 0xFFFF取反并截断到 16 位得到最终两字节。验证比构造更容易写把算好的校验和一起放进整包再算一次结果应该是 0。这是 RFC 里的性质拿来做断言最省事。def verify(packet: bytes) - bool: 整包含已填入的 checksum重新求和应为 0 return checksum(packet) 0 # 往返测试构造 - 验证 pkt build_echo_request(0x1234, 1, bicmpforge) assert verify(pkt), 校验和回填有误tests/test_checksum.py里我一般固定三条用例空数据返回0xFFFF、全0xFF的偶数长度数据、奇数长度 payload 的完整报文。前两条卡住折叠逻辑第三条卡住补零逻辑。3.3 原始套接字收发剥 IP 头与 RTT 计算构造正确之后真正的麻烦在收发。原始套接字有一个反直觉的行为recvfrom拿到的数据在类 Unix 系统上通常包含 IP 头而你要解析的 ICMP 报文从第 20 字节才开始有选项时更长。import os, socket, struct, time from .packet import build_echo_request, parse_icmp def _strip_ipv4_header(data: bytes) - bytes: 收到的数据可能是 IP 头 ICMP也可能直接是 ICMP都要兼容 if len(data) 20 and (data[0] 4) 4 and data[9] 1: ihl (data[0] 0x0F) * 4 # 首部长度字段以 4 字节为单位 return data[ihl:] return data def ping(host: str, seq: int 1, timeout: float 2.0, payload: bytes bicmpforge, identNone): ident ident if ident is not None else (os.getpid() 0xFFFF) pkt build_echo_request(ident, seq, payload) sock socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) sock.settimeout(timeout) start time.perf_counter() try: sock.sendto(pkt, (host, 0)) # 原始套接字下端口号无意义填 0 while True: data, addr sock.recvfrom(2048) icmp _strip_ipv4_header(data) if len(icmp) 8 or icmp[0] ! 0: # 只看 Echo Reply continue _, _, _, rid, rseq struct.unpack(!BBHHH, icmp[:8]) if rid ! ident or rseq ! seq: # 不是自己发的那个包继续等 continue rtt (time.perf_counter() - start) * 1000 info parse_icmp(icmp) info.update({rtt_ms: round(rtt, 3), from: addr[0]}) return info except socket.timeout: return None finally: sock.close()_strip_ipv4_header里那个双重判断是必须的data[0] 4 4确认版本是 IPv4data[9] 1确认协议号是 ICMP。只有两个条件同时成立才认定带 IP 头。为什么不能只看长度因为有些平台的原始套接字直接返回 ICMP 部分如果 payload 恰好以0x45开头很常见比如E开头的字符串只判断版本号就会误剥整包解析全乱。这是我在跨平台测试里改了三版才稳定下来的写法。收包循环里的continue也不是可有可无。原始套接字会收到本机所有 ICMP 流量包括别的进程发的、系统后台探测的。不按 identifier 和 sequence 过滤就会拿到别人的回包RTT 算得莫名其妙。过滤条件两个字段都要比只比 sequence 在多进程场景下会串。RTT 用time.perf_counter()而不是time.time()前者是单调时钟不受系统时间调整影响。回到毫秒后保留三位小数够用了。解析函数parse_icmp是构造的逆运算同样只在头 8 字节上做文章def parse_icmp(data: bytes) - dict: 把字节流转成字段字典长度不足直接抛错别静默返回空 if len(data) ICMP_HEADER_LEN: raise ValueError(fICMP 报文长度不足: {len(data)}) type_, code, chksum, ident, seq struct.unpack(!BBHHH, data[:8]) return { type: type_, code: code, checksum: chksum, checksum_ok: verify(data), # 整包重算应为 0 ident: ident, seq: seq, payload: data[8:], }checksum_ok这个字段很实用。抓到的包如果校验和不对多半是中间设备改写或者抓包位置有问题比对着十六进制猜要快得多。3.4 scapy 对照实现用两条链路互相打脸对照版的代码量只有主链路的十分之一作用是交叉验证from scapy.all import IP, ICMP, Raw, sr1 def ping_scapy(host: str, seq: int 1, timeout: float 2.0): scapy 自动计算校验和字段名清晰适合当作参照物 pkt IP(dsthost, ttl64) / ICMP(type8, code0, seqseq) / Raw(loadbicmpforge) reply sr1(pkt, timeouttimeout, verbose0) if reply is None: return None return { from: reply[IP].src, type: reply[ICMP].type, seq: reply[ICMP].seq, ttl: reply[IP].ttl, chksum_ok: verify(bytes(reply[ICMP])), }sr1表示发一个收第一个匹配的响应比sr发一个收全部轻。verbose0关掉 scapy 默认的打印不然会污染 CLI 输出。Raw(load...)挂负载不写也行但写了才能和手写版的字节长度对齐。两条链路的对齐方法对同一台机器各跑一次同时开抓包。除了 identifierscapy 默认取进程相关值和 checksum跟着 identifier 变type、code、sequence、payload 应该完全一致。如果手写版的 type 是十进制 8 但抓包显示0x0a那一定是格式串少了!或者参数顺序写反了。这种交叉验证在改代码时特别省时间——改完手写版跑一次对照十秒内就知道有没有改坏。4. 跑起来三条命令、一次抓包、一张字节对照表4.1 环境准备与权限配置先建虚拟环境装依赖依赖只有 scapy 一个用于对照实现python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt # 内容scapy2.5.0 python -c from icmpforge.packet import build_echo_request; print(build_echo_request(0x1234, 1, bhi).hex())最后那条命令不需要任何权限纯计算。如果它报错问题在代码不在环境。正常输出类似0800????1234000101 6869校验和位置因人而异长度应该是8 2 10字节 20 个十六进制字符。权限是新手最容易卡住的一关。类 Unix 系统下原始套接字需要CAP_NET_RAW两种做法一是命令前加sudo二是给解释器加能力避免每次都要提权。sudo setcap cap_net_rawep $(readlink -f $(which python3))这条命令把能力绑到解释器二进制上之后普通用户也能开原始套接字。代价是这台机器上所有 Python 进程都拿到了原始套接字权限生产机上慎用。容器环境里对应的是--cap-addNET_RAWCompose 里写cap_add: [NET_RAW]。Windows 下没有这套机制用管理员身份打开终端即可不需要改任何代码。4.2 三条命令验证收敛按「本地 → 不可达 → 多跳」的顺序验证出问题能立刻定位到哪一层# 1) 回环地址验证构造、发送、收包、解析全链路 sudo python -m icmpforge.cli ping 127.0.0.1 -c 3 --payload hello # 2) 文档保留地址正常情况下无响应验证超时分支和异常处理 sudo python -m icmpforge.cli ping 192.0.2.1 -c 1 --timeout 1 # 3) 网关或公网地址TTL 递增验证 type11 超时报文的解析 sudo python -m icmpforge.cli trace 192.0.2.1 --max-hop 5第一条命令的输出大概长这样PING 127.0.0.1 (ident0x2a3f) 64 bytes from 127.0.0.1: icmp_seq1 type0 code0 ttl64 time0.043 ms checksumok 64 bytes from 127.0.0.1: icmp_seq2 type0 code0 ttl64 time0.039 ms checksumok 64 bytes from 127.0.0.1: icmp_seq3 type0 code0 ttl64 time0.041 ms checksumokchecksumok这里是关键信号。它说明回包能被正确验证意味着你的校验和算法、字节序处理、IP 头剥离三件事全对了。如果显示checksumbad别怀疑网络回去看checksum()的补零和折叠。第二条命令返回超时是预期结果编号192.0.2.0/24是专门的文档示例段公网不会有设备应答。它验证的是socket.timeout分支和 CLI 的退出码——超时应该有明确的提示语而不是抛一堆栈。第三条走 TTL 递增第一跳一般是网关返回的应该是type11 code0。如果所有跳都超时先确认本机traceroute或tracert能不能出结果排除掉网络策略的问题再回头看代码。4.3 抓包对齐把 20 个十六进制字符逐个念一遍CLI 打印得再漂亮也不如抓包看一眼实在sudo tcpdump -i any -nn -vvv -XX icmp and host 127.0.0.1-XX会同时打印 ASCII 和十六进制-vvv会把 ICMP 字段展开。你要找的是类似这一段0x0000: 4500 0042 1a2b 0000 4001 8f3e 7f00 0001 E..B......... 0x0010: 7f00 0001 0800 7a2f 1234 0001 6865 6c6c ......z/.4..hell 0x0020: 6f o拆开读前 20 字节45 00 ... 7f00 0001 7f00 0001是 IP 头源和目的都是回环地址。从08 00开始是 ICMP——08是 type8Echo 请求00是 code接下来7a2f是 checksum1234是 identifier0001是 sequence最后68 65 6c 6c 6f就是hello的 ASCII。拿这段和 Python 里build_echo_request(0x1234, 1, bhello).hex()的输出逐字节比对除了 checksum 之外应该一模一样。对不上就定位到具体字段type位置不对是参数顺序问题identifier位置不对是格式串少了!payload部分不对是拼接顺序问题有人先算了校验和再拼 payload。Wireshark 里用过滤条件icmp.type 8 icmp.code 0缩小范围展开 Internet Control Message Protocol 树每个字段都能点到对应的字节区间比看十六进制友好。它还会单独标出 checksum 状态是Good还是Illegal一眼就能看出构造有没有问题。4.4 扩成路径探测与批量扫描单发验证通过后路径探测只是换个发送参数——TTL 从 1 开始递增中间路由返回超时报文import socket, struct from .packet import build_echo_request, parse_icmp def trace(host: str, max_hop: int 30, timeout: float 1.5, identNone): 逐跳探测TTL 从 1 加到 max_hop中间路由回 type11 ident ident if ident is not None else (os.getpid() 0xFFFF) hops [] for ttl in range(1, max_hop 1): sock socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_ICMP) sock.setsockopt(socket.IPPROTO_IP, socket.IP_TTL, ttl) # 关键控制存活跳数 sock.settimeout(timeout) try: sock.sendto(build_echo_request(ident, ttl, btrace), (host, 0)) data, addr sock.recvfrom(2048) icmp _strip_ipv4_header(data) info parse_icmp(icmp) hops.append({ttl: ttl, from: addr[0], type: info[type]}) if info[type] 0: # 收到 Echo Reply说明到目标了 break except socket.timeout: hops.append({ttl: ttl, from: *, type: None}) finally: sock.close() return hopsIP_TTL这个套接字选项是路径探测的核心。TTL1 时第一个路由收到包会把它丢弃并回一个 type11 的超时报文TTL2 时第二个路由回依次递推直到目标主机回 type0。每一跳都新建套接字是为了干净地换 TTL 和超时虽然开销略大但避免了状态残留——复用同一个 socket 改 TTL 在某些平台上不生效这是实测过的。批量探测用线程池时有个坑identifer 必须按线程分片。所有线程共用一个 identifier收包时就分不清是哪个请求的回包了。做法是ident (os.getpid() worker_id) 0xFFFF或者干脆给每个任务分配独立的 seq 段。并发度别开太高几十个就够原始套接字在高并发下丢包率会明显上升反而更慢。5. 避坑与排查权限、校验和、字节序的五个翻车现场5.1 权限与环境类问题现象一脚本能跑抓包能看到 Echo Request但永远返回超时一个回包都没有。原因通常不在代码。可能是宿主环境出站 ICMP 被策略丢弃部分容器网络、云主机默认如此也可能是目标主机禁 ping。这两种情况抓包表现在外层本地抓包能看到请求发出但看不到任何回包。解决顺序是从近到远分层先ping 127.0.0.1通说明代码链路没问题再ping网关通说明本机网络策略放行再ping目标不通就是路径上或目标端的问题。如果全都通但脚本不通那才回头看代码。我一般还会用一条 TCP 探测做对照——TCP 通而 ICMP 不通基本可以断定是中间设备对 ICMP 做了限制跟构造逻辑无关。现象二PermissionError: [Errno 1] Operation not permitted或者socket.error: Operation not permitted。原因明确原始套接字需要CAP_NET_RAW普通用户没有。这个报错发生在socket.socket()那一行还没来得及发任何数据。解决办法已经在 4.1 写过临时用sudo长期用setcap容器里加--cap-addNET_RAW。要提醒的是setcap对软链接无效必须用readlink -f解析出真实路径这一点和文件权限的直觉不一样第一次弄很容易配了个寂寞。5.2 报文计算类问题现象三抓包工具把 checksum 标成incorrect但目标主机仍然回了包。原因在两个地方之一一是 payload 长度为奇数时漏了补零二是累加后只折叠了一次进位。第一种情况有欺骗性——payload 是偶数长度时一切正常换成奇数就全错所以很容易误判成「网络时好时坏」。解决就是回到checksum()那几行if len(data) % 2: data b\x00必须存在折叠必须做两次。验证方式写死成断言assert verify(build_echo_request(0x1234, 1, bodd))——故意用奇数长度的 payload 做用例比用偶数长度靠谱得多。顺带说一句有些主机对错误的校验和宽容内核直接忽略校验和字段这会让问题拖很久才被发现。别指望目标端帮你暴露 bug。现象四能收到回包但解析出来的 identifier 和 sequence 是乱七八糟的数字甚至和发出去的对不上。原因是收到的数据没剥 IP 头直接从头 8 字节按 ICMP 解析了。IP 头的前 8 个字节是版本、首部长度、服务类型、总长度、标识、标志、片偏移解析出来当然是一堆无意义的数字。解决办法是_strip_ipv4_header的双重判断版本号data[0] 4 4且协议号data[9] 1才剥剥的长度取(data[0] 0x0F) * 4而不是硬编码 20——IP 头带选项时会超过 20 字节。如果只判断长度不判断协议号碰到 payload 以0x45开头的包就会误剥这是另一个坑。现象五批量探测时结果串包A 目标的 RTT 被算到了 B 目标头上或者 seq 对不上。原因是多个线程共用一个 identifier收包时只按 identifier 过滤于是任何线程的回包都被第一个recvfrom接走了。原始套接字是「谁先读谁拿到」的模型不存在定向投递。解决办法是 identifier 和 sequence 双字段联合过滤并且 identifier 按线程分片。改完之后还要注意一点recvfrom的循环里必须continue而不是break因为收到不属于自己的包是常态直接退出循环就会漏掉真正的那一个。这个循环我一般还会加个最大迭代次数比如 32 次防止在嘈杂网络里无限打转。6. 二次开发把构造器拆成能复用的探测库要往上加功能最省事的做法是守住现有的分层边界checksum.py和packet.py继续保持零 IO、零全局状态任何新报文类型都只在这两个文件里加函数。这样新增的东西天生可单测也不用担心权限和环境。加新类型最常见的需求是时间戳请求type13/14它比 Echo 多 12 字节负载typecode用途构造要点8 / 00Echo 请求 / 应答8 字节头 任意 payload13 / 140时间戳请求 / 应答头后固定 12 字节发起、接收、发送时间戳各 4 字节毫秒值30-15目的不可达头后 4 字节未用 原始 IP 头前 8 字节110 / 1超时结构同 type3traceroute 依赖它时间戳报文的三个时间戳字段是「从 UTC 零点起的毫秒数」用struct.pack(!III, ...)拼校验和照样覆盖整包。不可达和超时这两类要复杂一点头后面嵌的是「触发它的那个原始 IP 报文的前 8 字节」解析时需要两级拆包先剥 ICMP 头再把剩下的当 IP 头读一次。我的建议是给packet.py加一个独立的parse_embedded_ip()别塞进parse_icmp()里否则 Echo 的解析路径也要跟着判分支。验证策略上我习惯固定三步一是pytest tests/跑纯函数用例构造→解析→再构造字段必须完全一致二是手写版和 scapy 版对同一目标各发一次tcpdump抓下来比对字节除了 identifier 和 checksum 必须一致三是故意构造一个错误校验和的包确认目标端的行为符合预期丢包还是回应这一步能验证前面的所有假设。# tests/test_packet.py from icmpforge.packet import build_echo_request, parse_icmp from icmpforge.checksum import verify def test_roundtrip(): for payload in (b, bx, bodd, bicmpforge): pkt build_echo_request(0x1234, 7, payload) # 奇偶长度都要覆盖 assert verify(pkt) info parse_icmp(pkt) assert info[type] 8 and info[code] 0 assert info[ident] 0x1234 and info[seq] 7 assert info[payload] payload这套测试跑一次不到 0.1 秒但拦住了我后续所有的低级错误。说个教训。早前改校验和函数时为了「性能」把两次折叠砍成一次本地回环测试全过抓包也是Good我直接提交了。三天后在一台特定型号的设备上批量探测成功率莫名其妙掉到 60%日志里只有超时一个错误都看不到——因为错的包被静默丢弃了连黑匣子都没留下。从那以后我每次动checksum.py都强制先跑一遍奇数长度 payload 的用例再用 scapy 对照抓一次包两个都过了才敢提交。这种「算错了不报错」的地方后悔药是没有的只能靠流程挡住。希望帮到你。本文还有配套的精品资源点击获取