x/net/bpf 实现的是 cBPF 解释器,不支持 eBPF 的 map、辅助函数、JIT 编译及 bpftool 加载等核心特性;它仅适用于 socket 层包过滤(如 pcap 或 raw socket),无法用于 tracepoint、kprobe 等现代 eBPF 场景,且指令集与 eBPF 不兼容。

为什么不能直接用 x/net/bpf 写 eBPF 程序
x/net/bpf 实现的是经典的 cBPF(classic BPF)字节码解释器,不是现代 Linux 的 eBPF。它不支持 bpf_map、辅助函数(如 bpf_get_current_pid_tgid)、JIT 编译或 bpftool 加载——所有这些都属于内核 eBPF 子系统。如果你的目标是过滤网络包并注入到 Go 程序里(比如抓包时预筛),x/net/bpf 是可行的;但想做 socket 过滤以外的事(如 tracepoint、kprobe),这条路走不通。
常见错误现象:invalid BPF instruction 或程序看似运行但没生效,往往是因为误把 eBPF 汇编(如 mov r0, 1)喂给了 x/net/bpf 解释器——它只认 cBPF 指令集(ld, jeq, ret 等)。
- 使用场景:仅限于
net.ListenPacket("ip4:tcp")或pcap.Handle.SetBPFFilter()这类支持 cBPF 的 socket 层过滤 - 参数差异:
x/net/bpf.Assemble()输入的是[]bpf.Instruction,不是字符串或 ELF - 兼容性影响:该包不依赖内核 eBPF 支持,纯 Go 实现,跨平台(Linux/macOS/FreeBSD)可用,但功能受限
怎么写一个能跑通的 cBPF 过滤器(以 ICMP 为例)
核心是构造符合 cBPF 指令格式的 []bpf.Instruction,然后用 bpf.Assemble() 编译,再通过 syscall.Socket 或 pcap 设置到 socket 上。不要试图手写十六进制指令——用结构化方式组装更可靠。
示例:只接收 ICMPv4 Echo Request(type=8, code=0):
filter := []bpf.Instruction{
bpf.LoadAbsolute{Off: 20, Size: 1}, // 加载 IP 协议字段(IP header offset 20)
bpf.JumpIf{Cond: bpf.JumpEqual, Val: 1, SkipTrue: 10, SkipFalse: 0}, // 不是 ICMP?跳过后续
bpf.LoadAbsolute{Off: 22, Size: 1}, // 加载 ICMP type(IP payload offset 22)
bpf.JumpIf{Cond: bpf.JumpEqual, Val: 8, SkipTrue: 0, SkipFalse: 3}, // type != 8?跳到 ret 0
bpf.LoadAbsolute{Off: 23, Size: 1}, // 加载 ICMP code(offset 23)
bpf.JumpIf{Cond: bpf.JumpEqual, Val: 0, SkipTrue: 0, SkipFalse: 1}, // code != 0?跳到 ret 0
bpf.RetConstant{Val: 65535}, // 允许整包通过(最大截取长度)
bpf.RetConstant{Val: 0}, // 拒绝
}
prog, err := bpf.Assemble(filter)
if err != nil {
log.Fatal(err)
}
- 注意 IP header 长度可变(IHL 字段),上面假设标准 20 字节;真实环境建议用
LoadIndirect处理可变头长,否则可能错位读取 ICMP 字段 -
RetConstant{Val: 65535}表示“复制最多 65535 字节”,不是丢包——socket 层仍需自行处理截断逻辑 - 偏移量单位是字节,且从链路层开始算:以太网帧中,IP header 通常从 offset 14 开始;但
x/net/bpf在 socket filter 场景下默认从 IP header 起始(即已跳过链路层),所以上例用 20
如何把 BPF 程序挂到 raw socket 上
x/net/bpf 本身不提供 socket 绑定能力,必须配合 syscall 或第三方库(如 gopacket/pcap)。直接调 syscall 风险高,推荐优先走 pcap 路径——它内部做了平台适配和错误检查。
- 用
pcap:先pcap.OpenLive(iface, 65535, true, 10*time.Millisecond),再handle.SetBPFFilter(prog)(注意:prog 是bpf.RawProgram类型,需从bpf.Assemble()结果转换) - 手动 syscall(Linux):需
SO_ATTACH_FILTER,且 socket 必须是AF_PACKET+SOCK_RAW,还要确保 CAP_NET_RAW 权限;setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, unsafe.Pointer(&filter), unix.SizeofSockFprog) - 容易踩的坑:
SetBPFFilter()会覆盖已有 filter,多次调用前需清空;macOS 上仅部分接口支持 BPF(如en0),且不支持AF_PACKET
调试失败的 BPF 过滤器最有效的办法
别靠猜。cBPF 没有内核日志输出,唯一可靠手段是:用 tcpdump -dd 生成对应汇编,再人工对照 x/net/bpf 的指令结构还原。
例如:tcpdump -dd 'icmp[icmptype] == icmp-echo' 输出一串数字,就是 cBPF 指令数组。把它转成 bpf.Instruction 列表,再用 bpf.Assemble() —— 如果失败,说明某条指令不被 x/net/bpf 支持(比如某些平台特有指令)。
- 性能影响:cBPF 解释执行比内核 JIT eBPF 慢一个数量级,但对千兆以下流量足够;瓶颈通常在 Go 的 packet 处理逻辑,而非 BPF 本身
- 真正容易被忽略的是:BPF 过滤只作用于进入 socket 的包,不拦截发出去的包;若需双向控制,得在两个 socket 上分别设 filter 或改用 netfilter
- macOS 上
bpf设备路径是/dev/bpf*,权限需chmod 600,且每次打开后要ioctl(BIOCSETIF)绑定接口——x/net/bpf不处理这些,得自己补

















