
做网络调试或者学网络协议的时候很多朋友应该都有过这样的经历tcpdump的输出密密麻麻看不懂Wireshark 功能强大但跑在服务器上又嫌重想自己写个抓包工具又不知道从哪下手。今天想聊的开源项目 CaveMan就是专门解决这个痛点的。它是一个用 C 语言写的轻量级网络数据包嗅探器基于 libpcap 开发能把经过网卡的每个数据包按以太网帧、IP 层、TCP/UDP/ICMP 层逐层拆开再以清晰文本和十六进制对照的形式打印出来。对刚接触 TCP/IP 协议栈的新手它是一份活教材对需要快速验证网络环境的开发者它又是一个随手能改的调试工具。这篇文章我会把它从设计思路、核心原理到编译、抓包、排错、扩展的完整链路拆开讲一遍全程按实际操作来你照着敲就能跑起来。1. 项目全貌CaveMan 到底在解决什么问题1.1 一句话定位让“看不见”的字节流现出原形网络上的数据是以比特流形式在网线里跑的普通应用根本感知不到原始报文长什么样。CaveMan 做的事情简单说就是“截流 拆包”它通过 libpcap 把网卡收到的数据包复制一份出来然后自己动手解析以太网头部、IP 头部、传输层头部最后把每一层的关键字段打印到终端上。这一点对学习网络协议特别有价值。你日常上网时浏览器、操作系统、路由器各自完成了复杂的封装和解封装中间过程全被隐藏了。CaveMan 把这些中间过程强行摊开给你看源 MAC 是谁、目的 MAC 是谁、IP 包的 TTL 是多少、TCP 的标志位有没有置 SYN、校验和对不对全都能直接看到。整个项目体量很小核心代码就几个 C 文件阅读起来不会有负担。1.2 它不是 Wireshark也不是 tcpdump 的替代品很多朋友拿到一个新工具会习惯性问它能替代 XXX 吗我的看法是CaveMan 的价值恰恰在于它不追求替代任何人。Wireshark 的图形界面和协议解码器非常强大但它的重量级也让不少人望而却步tcpdump命令行的过滤能力很强可默认输出格式对新手并不友好字段含义全靠经验去对应。CaveMan 正好卡在中间位置它保留了命令行工具的轻量、直接输出格式又故意做成“一眼能看懂”的逐层缩进文本。更重要的是它是开源的代码量和复杂度控制在一个人能通读的规模内。你想搞清楚“一个数据包从网卡到应用层到底经历了哪几步”读 CaveMan 的源码比读 Wireshark 的源码容易太多了。1.3 适合谁用、能用在什么场景我实测下来有三类人最适合拿它当切入点正在学 TCP/IP 协议栈的学生用它配合教材验证三次握手、四次挥手、MTU 分片这些概念的真实报文长什么样。做局域网调试的开发者比如验证两台设备之间到底通没通、某个端口是否在监听、自定义协议字段有没有被正确填充。嵌入式研发或运维同学在资源受限、没有图形界面的环境里一个几十 KB 的静态编译工具就能完成基本的抓包验证。我自己最常用的场景是“对照教材抓包”开着 CaveMan 抓一次ping再抓一次curl然后把打印结果和课本上的报文格式图逐字段比对。这个过程比单纯看文档高效得多。2. 核心设计为什么是 libpcap C以及各层怎么拆2.1 为什么选 libpcap抓包这行的“标准答案”抓包工具要工作第一步是拿到数据包的副本这涉及网卡驱动、内核协议栈旁路、权限控制等一系列底层机制。自己从零实现会很痛苦而 libpcap 就是解决这个问题的跨平台事实标准它统一了 Linux、macOS、BSD 上的抓包接口。CaveMan 选择基于 libpcap相当于站在巨人的肩膀上把最难的“从网卡拿包”这步交给一个成熟稳定、被全世界验证过的库来完成。打个比方libpcap 就像水表入户的管道接口它把网卡这个“市政水管”里的数据分流出一份给用户态程序而 CaveMan 就像你家里装的计量表负责把这份数据计数、显示、拆解。开发时只需要调用pcap_open_live、pcap_next_ex、pcap_compile这几个 API剩下的解析逻辑完全可以自己控制透明又可改。2.2 以太网帧一切解析的起点一个数据包从网卡进来正常情况下第一层就是以太网帧。以太网头固定 14 字节目的 MAC 6 字节、源 MAC 6 字节、EtherType 2 字节。CaveMan 的解析逻辑很直白把收到的缓冲区前 14 字节按这个布局切开再用指针直接读取各个字段。EtherType 决定了下一步往哪走0x0800表示后面是 IPv4 包0x0806是 ARP0x86DD是 IPv6。这里有个初学者容易忽略的细节MAC 地址是 6 字节打印时每个字节用十六进制两位表示中间加冒号所以常见的 MAC 地址格式aa:bb:cc:dd:ee:ff恰好 17 个字符解析时别按普通字符串去处理。2.3 IP 头真正网络路由的“收件信息”IP 头就比以太网帧复杂一些了。标准的 IPv4 头最少 20 字节第一个字节的高四位是版本号IPv4 就是 4低四位是 IHLInternet Header Length单位是“4 字节”。所以 IHL 等于 5 表示真正的头是 5×420 字节没有任何选项字段如果看到 IHL 是 6 或更大说明带了一些 IP 选项解析偏移就得往后挪。IP 头里的“协议”字段在第九个字节值 6 表示上层是 TCP17 表示 UDP1 表示 ICMP。源 IP 在第 12 到 15 字节目的 IP 在第 16 到 19 字节而且都是网络字节序存储。CaveMan 这里要注意的是字节序转换否则你打印出来的 IP 地址会颠倒错乱。另外 IP 头还有 TTL、总长度、校验和这些字段对排查网络问题非常有用比如 TTL 每经过一个路由器就减 1可以通过它判断经过了几个跳。2.4 TCP/UDP/ICMP端口与状态怎么读到了传输层解析规则又不一样。TCP 头最小 20 字节源端口在第 0-1 字节目的端口在第 2-3 字节序号和确认序号各占 4 字节数据偏移在第 12 字节的高四位标志位在第 13 字节。UDP 头就简单得多固定 8 字节源端口、目的端口、长度、校验和各占 2 字节。很多人第一次看报文会觉得眼花其实只需要抓住“端口找服务、标志位看状态”这个要点。TCP 的 SYN、ACK、FIN、RST 这些标志位在抓包结果里能把三次握手的过程完整还原出来第一次握手是 SYN第二次是 SYNACK第三次是 ACK。CaveMan 的代码里通常会把标志位逐个拆出来按位判断再打印这也是我自己觉得它比单纯看十六进制转储更友好的核心原因。2.5 打印层的设计人眼友好的关键光会解析还不够输出格式决定了一个工具好不好用。CaveMan 输出通常会包含三块解析后的文本摘要、十六进制转储、以及可读 ASCII 对照。十六进制转储部分会按 16 字节一行排列左侧打印偏移量中间是字节的十六进制值右侧尽可能显示可打印字符不可打印的用.代替。这个设计在排查二进制协议时特别有用。比如你怀疑某个设备填充的字段错了十六进制一眼就能看出某几个字节不符合预期。解析文本负责“讲人话”十六进制负责“看细节”两者结合就能快速完成大部分网络问题定位。3. 上手实操从拉代码到抓到第一个包3.1 环境准备与依赖CaveMan 依赖非常少Linux 环境下只要准备三样东西C 编译器gcc、make、以及 libpcap 开发库。Ubuntu/Debian 上直接装sudo apt update sudo apt install build-essential libpcap-dev注意一定要装libpcap-dev这个开发包它包含头文件pcap.h和链接库只装libpcap0.8的话没有头文件编译会直接报pcap.h: No such file or directory。如果是 CentOS/RHEL 系列对应的包名是libpcap-devel。3.2 拉取源码与编译从开源社区找到 CaveMan 的代码仓库后克隆到本地git clone 仓库地址 caveman cd caveman make如果仓库里带了 Makefile一般直接make就能编出可执行文件。没有 Makefile 也没关系核心代码就一两个 C 文件手动编译也不难gcc -o caveman caveman.c -lpcap编译完先跑一下./caveman -h看帮助信息确认参数再下手。我遇到过好几个人源码刚拉下来不看说明直接./caveman结果程序提示缺少参数或没有权限误以为编译失败了。3.3 参数说明先搞清楚每个选项再动手CaveMan 的命令行参数典型的有这么几个参数作用示例-i指定要监听的网卡接口-i eth0-c抓多少个包后退出-c 10-fBPF 过滤表达式-f tcp port 80-s每个包捕获的最大字节数snaplen-s 128第一次使用建议先查一下网卡名用ip addr或者ifconfig -a看。服务器上常见的接口名是eth0、ens33笔记本上通常是wlan0或en0。抓包几乎都需要 root 权限因为要打开混杂模式读取所有流经网卡的包所以运行命令一般是sudo ./caveman -i eth0 -c 20这里-c 20的意思是抓到 20 个包就自动退出避免长时间输出刷屏。3.4 实战一用 ping 看 ICMP 报文长什么样我先演示一个最简单的验证流程。打开一个终端跑sudo ./caveman -i eth0再开一个终端执行ping -c 4 192.168.1.1CaveMan 会打印出四条 ICMP 请求和四条对应的应答。重点观察输出里的这些字段源 IP 是你的机器地址目的 IP 是网关地址协议字段显示 ICMPtype/code 是 8/0请求和 0/0应答。你还能看到以太网两端的 MAC对比一下源和目的就能理解“数据包先到网关的 MAC 再到外网”这句话在报文层面是什么意思。3.5 实战二用 curl 看 TCP 三次握手再来一个经典场景验证 TCP 三次握手。在 CaveMan 运行时执行curl -v http://example.com/这次输出会明显不同你会看到一串 TCP 包标志位依次是 SYN、SYNACK、ACK接下来才是 HTTP 的数据传输。有些版本输出里还会直接标注 [SYN]、[ACK] 这样的可读状态这比在 Wireshark 里对照着时间戳找三次握手更直观。如果没看到第二次握手的 SYNACK说明目的端口可能没监听或者中间有防火墙丢包这是排查网络连通性的一个非常好用的手段。4. 深入拆解核心代码逻辑与关键函数4.1 主流程初始化、循环捕获、程序收尾CaveMan 的主函数并不神秘大致分三段。第一段解析命令行参数第二段初始化 libpcap 的句柄并设置过滤器第三段进入循环不断抓包打印。用伪代码描述就是这样int main(int argc, char **argv) { // 1. 参数解析得到接口名、包数量、过滤器 // 2. 调用 pcap_open_live 打开接口 // 3. 如果有 -f 参数调用 pcap_compile pcap_setfilter 设置 BPF // 4. 循环调用 pcap_next_ex 拿包交给 handle_packet() 处理 // 5. 达到包数量后pcap_close 释放资源程序退出 }整个架构的优点在于“数据获取”和“数据解析”彻底分离。读取数据包的逻辑由 libpcap 封装好CaveMan 只需要在回调里处理原始字节如果你想改成“只统计包数量、不打印内容”也只需要改一小段处理函数完全不用动底层代码。4.2 以太网帧解析结构体还是指针偏移解析以太网头部有两种常见写法一种是定义 struct 然后用指针强转另一种是纯字节偏移。CaveMan 这类教学项目一般偏向后者因为结构体方案受编译器对齐和填充影响在跨平台解析网络报文时容易踩坑。字节偏移写法则完全没有对齐问题可读性也不差struct eth_hdr { unsigned char dst_mac[6]; unsigned char src_mac[6]; unsigned short ether_type; }; // 使用时直接强转 struct eth_hdr *eth (struct eth_hdr *)packet;这里要留意ether_type从网络里读出来是大端序x86 和 ARM 小端序主机上直接读取会得到反过来的值必须用ntohs()转换。很多初学抓包工具的人看到 EtherType 变成0x0008而不是0x0800其实就是忘了做转换。4.3 IP 头解析别忘了 IHL 和协议字段IP 头解析的代码一般长这样struct ip_hdr { unsigned char version_ihl; // 高4位版本低4位IHL unsigned char tos; unsigned short total_len; unsigned short id; unsigned short frag_offset; unsigned char ttl; unsigned char protocol; unsigned short checksum; unsigned int src_ip; unsigned int dst_ip; };解析时先读取version_ihl用ihl (version_ihl 0x0F) * 4算出 IP 头实际长度再用它作为传输层解析的起点偏移。这一步很容易被忽略如果直接按 20 字节写死碰到带选项的 IP 包后面所有字段都会错位。protocol字段则决定了下一层调用 TCP 解析器还是 UDP 解析器通常是一个switch分支。4.4 TCP/UDP/ICMP 解析的难点与陷阱传输层解析相对直接但有几个陷阱要特别说明。第一个是捕获长度问题。pcap_open_live里的 snaplen 参数控制每个包最多拷贝多少字节如果设置太小比如 64 字节一个完整的 MTU 1500 包会被截断解析 TCP 头可能够用但解析 HTTP 负载就看不到了。第二是字节序问题端口、序号、长度统统是大端序全部要转换后再打印。第三是指针越界风险如果收到一个畸形包长度字段和实际数据不符直接按偏移读取可能读到非法内存严谨的实现会先把长度字段和首部长度做校验。ICMP 的解析更简单类型和代码各占 1 字节CaveMan 一般会把常见的类型值翻译成英文描述比如 8 表示 Echo Request、0 表示 Echo Reply、3 表示 Destination Unreachable。这个翻译逻辑虽然简单但用来定位“ping 不通到底是网络不可达还是被拒绝”非常直观。4.5 十六进制转储一个 20 行以内的小函数打印十六进制的函数不算复杂核心思路是“按 16 字节分组先打偏移量再打十六进制字节最后打 ASCII”。我见过很多实现要点就两个一是用isprint()判断字节是否可打印避免终端被乱码刷屏二是宽度对齐用printf(%04x , offset)保证偏移列整齐。这些细节虽小但直接决定工具用起来舒不舒服。5. 常见问题与排查技巧实录5.1 权限报错Operation not permitted 怎么办第一个坑基本是权限。libpcap 打开网卡需要CAP_NET_RAW能力普通用户直接运行会报类似You dont have permission to perform this operation的错误。最简单的方法是加sudo但如果你在自动化脚本里不想每次都提权可以给编译好的二进制单独加能力sudo setcap cap_net_raw,cap_net_admineip ./caveman加上之后普通用户就能跑了。要注意的是setcap设置的是二进制文件自身的能力换机器或重新编译后需要重新设置一遍。5.2 抓不到包或者只抓到自己发出去的包这个问题我见最多。其实有两个原因第一个是网卡没开混杂模式只能收到发给本机 MAC 的包第二个是接口选错比如明明流量走的是eth0却拿-i lo去抓回环接口自然什么都看不到。tcpdump -D或ip addr可以列出全部接口先确认流量走的是哪一个。libpcap 的pcap_open_live里有个 promisc 参数置 1 开启混杂模式CaveMan 一般默认开启但如果代码逻辑里写死了 0你就需要在别的网段测试时收到全量流量的包。另外提醒一句在交换机环境下即使开了混杂模式也未必能收到其他设备的单播流量。现代交换机按 MAC 地址表只把单播帧转发到对应端口这里要涉及端口镜像属于网络环境配置问题不是工具本身的锅。5.3 端口号和 IP 地址显示反了、数值巨大如果看到端口号从一个正常值变成类似45232这种不可理解的数字或者 IP 地址出现了46384这种数值基本就是字节序没转换。网络传输用的是大端序而 x86/ARM 处理器默认是小端序读出来要经过ntohs()16 位或ntohl()32 位转换。CaveMan 这类项目如果代码里漏了转换就会出现这种经典 bug。排查时直接在printf外面包一层转换函数问题立刻消失。5.4 高流量下输出卡顿、丢包严重默认情况下 libpcap 会分配一块缓冲区流量一大用户态没来得及处理内核缓冲区的包就可能被丢弃。解决办法有三个方向一是用pcap_set_buffer_size调大缓冲区二是用-s减小 snaplen只拷头部不拷负载能大幅降低内存拷贝量三是用 BPF 过滤器在进入 libpcap 之前就只保留你关心的包比如-f tcp port 443这样无关流量根本不进缓冲区。我自己在 10G 网卡环境里测试时把 snaplen 设成 96 字节、过滤器只留 TCP 端口 80 和 443 后丢包率从肉眼可见的严重降到接近于 0。这个调优思路对所有基于 libpcap 的工具都通用。5.5 输出全乱码或长度截断如果你看到的以太网目的 MAC 全是00:00:00:00:00:00或者 IP 版本号是乱值大概率是 snaplen 设置太小导致包被截断解析器读到了半截数据。解决方法是把 snaplen 至少设到 64 字节能覆盖以太网 IP TCP 头如果要看 HTTP 内容就设成 1514 或者更大一点覆盖标准 MTU 包。6. 玩出花给 CaveMan 做点小扩展6.1 把结果存成 pcap 文件丢给 Wireshark 继续分析CaveMan 打印到终端很直观但如果你想做更深入的分析或者把抓包结果发给同事这时候 pcap 文件格式就是通用语言。libpcap 本身自带一套落盘 API扩展起来很简单pcap_dumper_t *dumper pcap_dump_open(handle, out.pcap); pcap_dump((unsigned char *)dumper, header, packet); // 结束时 pcap_dump_close(dumper);注意此处的header是pcap_pkthdr里面包含时间戳和长度信息。生成的文件可以直接用 Wireshark 打开也能用tcpdump -r out.pcap -n重放查看。加这个功能不影响原有的终端打印等于多了一条输出链路。6.2 给 TCP 负载做个简单的 HTTP 请求行识别教材里的抓包工具如果只能看到 TCP 层总觉得差点意思。实际上只需要在 TCP 解析之后拿到负载的起始指针和长度然后用strncmp或memmem判断前几个字节是不是GET、POST、HTTP/1.1是的话就把前 80 个字节打印出来。这个扩展我在一次 Web 服务联调时帮过大忙两个服务之间的 HTTP 请求被网关改写了 URL用这个扩展一眼就看出了实际发出的请求行是什么。实现不复杂但收益非常直观。6.3 加一个每秒包量统计功能运维场景里我们经常需要快速判断“当前网络上流量是不是异常”。要满足这个需求可以在主循环里维护一个计数器每收到一个包就自增然后用time()或者clock_gettime()判断是否跨了一秒跨了就打印当前计数并清零。这个功能对 CaveMan 来说只需要十几行代码但它把一个教学演示工具变成了一个轻量流量观察器。我实际测试过配合 BPF 过滤只统计某个端口基本能替代大部分简单的流量监控需求。6.4 别越界扩展功能也要守住合规底线最后必须强调一点抓包工具是双刃剑CaveMan 以及你由此扩展出的任何功能都只能在你拥有设备或获得明确授权的网络环境里使用。局域网抓包、自建实验环境完全没问题但未经授权抓取他人流量涉嫌违反相关法律这条红线千万别碰。我在项目里读到任何开源抓包工具第一件事都是查看许可证和作者的使用说明养成这个习惯非常必要。我个人实际使用下来CaveMan 给我最大的收获不是“多了一个抓包命令”而是把 TCP/IP 那些抽象的字段变成了看得见摸得着的字节序列。以前记不住的 TCP 标志位组合现在不用刻意背因为抓包里三次握手的样子已经刻在脑子里了。如果你正好在学协议栈或者调试网络问题不妨把它拉下来编译一遍再对照课本抓几个包看看。最后再分享一个小习惯每次跑抓包工具前先在命令行把过滤条件写清楚抓多少包、针对哪个端口、用哪个接口都固定下来这样你得到的输出才是有意义的而不是一堆垃圾流量。这个习惯我后来带新人的时候也会反复跟他们强调——工具是死的思路是活的。