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 头开始的输入。
这对 skb 和 mbuf 尤其重要。若 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 等常用条件。
实现难点主要集中在组合语义和安全性。
-
控制流:
and、or、not需要保持优先级和短路语义。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 基础设施把网络观测扩展到了 skb、mbuf 和 RDMA/RoCE 数据通路。而 go-pcap,则提供一项纯 Go、可测试、可嵌入、可编程的报文过滤能力。