🚀 2025 年中国信息通信研究院《OSCAR 尖峰开源项目及开源社区》、腾讯科技《杰出贡献奖》、CNCF Landscape。欢迎 GitHub ❤️

博客

AI 时代,我们复活了这个开源项目

tcpdump 和 libpcap 已经很好地解决了传统抓包问题。但 Go 应用使用 libpcap 需依赖 CGO 和系统库,也增加了静态构建与交叉编译的复杂度。

AI 基础设施中的报文不只来自网卡。内核丢包观测处理的是 skb,DPDK 应用处理的是 rte_mbuf,RDMA/RoCE 流量还可能经过用户态数据通路。这需要可嵌入不同观测链路的过滤编译能力。

因此,我们重新维护 go-pcap:用纯 Go 将 tcpdump 表达式编译为 cBPF,无需 libpcap 和 CGO,可用于实时抓包、内核观测和用户态报文过滤。

解决什么问题

go-pcap 分为两个相对独立的部分:

  • 根包 pcap 负责打开设备、管理抓包句柄、挂载过滤器和读取报文;
  • filter 包负责解析表达式,根据链路类型和编译目标生成 cBPF 指令。

主要执行路径如下:

tcpdump 表达式
解析器 → AST → LinkType/Target → cBPF
                                  ├─ capture socket
                                  ├─ skb/eBPF
                                  └─ mbuf/eBPF(扩展方向)

过滤编译与采集入口分离后,同一套表达式可以用于不同观测场景,而每个 backend 只需负责定位报文字节、转换指令并注入目标程序。

go-pcap 不计划完整替代 tcpdump 或 libpcap。当前目标是实现常用、可验证、适合嵌入的过滤能力。

实时抓包:纯 Go 编译和挂载过滤器

常规抓包可以通过 OpenLive 打开设备,再用 SetBPFFilter 设置过滤条件:

handle, err := pcap.OpenLive(
	context.Background(),
	"eth0",
	1600,
	true,
	time.Second,
	pcap.DefaultSyscalls,
)
if err != nil {
	return err
}
defer handle.Close()

if err := handle.SetBPFFilter("tcp and port 443"); err != nil {
	return err
}

表达式在 Go 程序中编译为 cBPF,并挂载到 capture socket。未命中的报文在内核侧被过滤,应用只读取匹配结果。

仓库还提供 pcap 命令行工具,支持 -i-c-n/-nn-q-v-e-X-A-s-p 等常见参数,但目前不支持 pcap 文件读写,也不定位为完整的 tcpdump 替代品。

skb 丢包过滤

HUATUO dropwatch 监听 tracepoint/skb/kfree_skb。例如,只观察 eth0 上目标端口为 443 的 TCP 丢包:

sudo dropwatch \
  --bpf-path bpf/dropwatch.o \
  --device eth0 \
  --filter "tcp dst port 443" \
  --duration 60 \
  --output json

加载 eBPF 程序时,HUATUO 使用 go-pcap 为二层和三层输入生成 cBPF,再完成 cBPF 到 eBPF 的转换、verifier 适配和程序注入。

运行时,dropwatch 根据 skb->mac_len 判断从 MAC 头还是网络层头开始过滤。未命中的事件在内核侧直接返回,不进入后续限速和用户态输出。

双方的职责边界如下:

  • go-pcap:解析表达式并为指定报文布局生成 cBPF;
  • HUATUO:定位 skb 数据、转换和注入 eBPF,以及输出丢包原因、设备、协议和调用栈。

这是 go-pcap 当前已经落地的非传统抓包场景。

mbuf 与 RDMA/RoCE 扩展

DPDK 场景中的报文位于 rte_mbuf。观测工具可以通过 uprobe 或 USDT 获取 mbuf,定位 packet bytes,并在事件提交到 perf/ringbuf 前完成过滤。

ByteDance netcap 已支持这种观测方式,但它目前仍使用 gopacket/pcap 和 libpcap 编译表达式,该方式面临着部署依赖问题。当前 go-pcap 完全可以平替 libpcap。

场景 当前状态 go-pcap 的职责
实时抓包 已内置 打开句柄、编译和挂载过滤器、读取报文
HUATUO dropwatch 已接入 为 L2/L3 skb 输入编译 cBPF
DPDK mbuf / RDMA 扩展方向 提供过滤编译,不包含 mbuf backend

关键问题

cBPF 按字节偏移读取字段。编译器必须知道 byte 0 是 Ethernet 头还是 IP 头,否则生成的偏移会出错。

同一条 tcp and port 443,在两种输入中的布局不同:

Ethernet:Ethernet 头 | IP 头 | TCP 头 | 数据
RAW:                 IP 头 | TCP 头 | 数据

go-pcap 要求调用方显式指定链路类型:

ethernet, err := filter.Compile(
	"tcp and port 443",
	filter.LinkTypeEthernet,
)

raw, err := filter.Compile(
	"tcp and port 443",
	filter.LinkTypeRaw,
)

LinkTypeEthernet 对应 DLT_EN10MB,适用于包含 Ethernet 头的输入;LinkTypeRaw 对应 DLT_RAW,适用于从 IPv4 或 IPv6 头开始的输入。

这对 skbmbuf 尤其重要。若 rte_mbuf.data 指向 Ethernet header,应按 Ethernet 编译;若观测点传入的是已剥离二层头的 IP packet,则应按 RAW 编译。

二层规则不能用于 RAW 输入。编译器会返回明确错误,而不是生成结果不确定的程序:

_, err := filter.Compile("arp", filter.LinkTypeRaw)
if errors.Is(err, filter.ErrL2OnlyLinkType) {
	// 拒绝规则,或切换到 Ethernet 布局。
}

几个技术点

go-pcap 当前支持 IPv4/IPv6、ARP/RARP、TCP、UDP、SCTP、ICMP/ICMP6、VLAN/QinQ、MPLS,以及 host、net、port、portrange、报文字节访问、算术比较和 len 等常用条件。

实现难点主要集中在组合语义和安全性。

  • 控制流andornot 需要保持优先级和短路语义。go-pcap 先生成带符号标签的控制流,再统一解析为 cBPF 相对跳转,避免在递归生成过程中手工维护跳转偏移。

  • 可变协议偏移:VLAN 和 MPLS 会改变后续协议头的位置。编译器使用不可变游标处理表达式分支,避免一个分支推进后的偏移影响另一个分支。Linux 上还需处理 VLAN offload:VLAN tag 可能位于报文内,也可能已被硬件剥离并保存在 metadata。socket 目标同时支持这两条路径。

  • 边界检查:报文字节访问会生成长度检查。遇到截断报文时,过滤结果是不匹配,不能将越界读取当作 0 继续计算。

验证结果

不同编译器生成的 cBPF 指令不必完全一致,关键是对同一报文给出相同的接受或拒绝结果。

当前测试包括:

  • 解析、校验和指令生成单元测试;
  • cBPF VM 执行测试;
  • IPv4/IPv6、L2/L3/L4、VLAN/MPLS 和组合表达式测试;
  • tcpdump 4.99.0 / libpcap 1.10.0 golden fixture 对照;
  • loopback 实时抓包集成测试。

项目还提供解析、编译、VM 命中与未命中的 benchmark,用于重复测量,不作为脱离运行环境的性能结论。

当前边界

目前尚未实现的能力包括:

  • 数字协议号和 protochain
  • IPv6 扩展头遍历;
  • 依赖接口 netmask 的 broadcast

已识别但不支持的特性会返回 ErrUnsupportedFeature。后续会优先补充有真实需求和样本的链路类型。

结语

AI 基础设施把网络观测扩展到了 skbmbufRDMA/RoCE 数据通路。而 go-pcap,则提供一项纯 Go、可测试、可嵌入、可编程的报文过滤能力。

项目地址:github.com/huatuo-ai/go-pcap

华佗项目:https://github.com/ccfos/huatuo