前言¶
我们常用的 tcpdump 抓包工具的一个核心能力是支持使用 pcap-filter 包过滤语法对流量进行过滤,只对符合条件的特定流量进行抓包。
当我们使用 eBPF 技术开发网络相关的工具的时候,如果也能支持 pcap-filter 包过滤语法的话, 想必会极大的提升用户体验。 因此,我开发的 ptcpdump 工具也内置了对 pcap-filter 包过滤语法的支持。
使用常规方法为 eBPF 程序增加 pcap-filter 支持会需要实现复杂的逻辑。 但是,如果你的项目正在使用 cilium/ebpf 作为 eBPF Loader, 通过使用本文介绍的 elibpcap 这个 Go package , 你只需要增加几行代码就能为你的项目内置支持 pcap-filter 包过滤语法的能力。
项目介绍¶
如前面所说,通过使用 elibpcap,只需几行代码就可以为 eBPF 项目增加支持 pcap-filter 的能力。
下面是 AI 总结的项目介绍:
elibpcap 是一个 Go 语言包,它提供的功能是:将 pcap 过滤表达式编译为 eBPF 字节码, 并将这些过滤器注入到现有的 eBPF 程序中。它将人类可读的 pcap 过滤表达式 (例如 tcpdump 等工具中使用的那些表达式)的世界与在 Linux 内核中运行的 eBPF 程序的强大能力和高效率连接了起来。
使用示例¶
使用模式¶
为了使用 elibpcap ,我们需要做三方面的修改:
- 一个是,需要修改 eBPF 程序,在需要实现数据包过滤的位置增加类似如下的 place holder 逻辑:
// add this
static __noinline bool pcap_filter(void *_skb, void *__skb, void *___skb, void *data, void *data_end) {
return data != data_end && _skb == __skb && __skb == ___skb;
}
SEC("tc")
int sample_prog(struct __sk_buff *skb){
// ...
// add this
if (!pcap_filter((void *)skb, (void *)skb, (void *)skb, data, data_end)) {
bpf_printk("pcap_filter not match");
goto out;
}
// ...
}
- 另一个是,需要修改 Go 程序,调用 elibpcap package 增加集成 pcap-filter 包过滤能力的逻辑:
oldInsts := spec.Programs["sample_prog"].Instructions
newInsts, err := elibpcap.Inject(expr, oldInsts, elibpcap.Options{
AtBpf2Bpf: "pcap_filter",
DirectRead: true,
L2Skb: true,
})
if err != nil {
return err
}
spec.Programs["sample_prog"].Instructions = newInsts
其中:
- AtBpf2Bpf 用于指定是我们在 eBPF 程序中定义的 static __noinline bool pcap_filter(...) 函数的名称。
- DirectRead 用于指定我们的 eBPF 程序中 pcap_filter 函数传入的 skb 数据是否支持通过 direct access 的方式读取数据。
- L2Skb 用于指定我们的 eBPF 程序中 pcap_filter 函数传入的 skb 数据是否包含 L2(链路层) 相关的数据。
最后是,因为 elibpcap 依赖了 libpcap ,因此需要在编译时启用 CGO:
静态链接编译示例:
CGO_CFLAGS="-I/path/to/libpcap/include" \ CGO_LDFLAGS="-L/path/to/libpcap/lib -lpcap /path/to/libpcap.a" \ CGO_ENABLED=1 go build -o main \ -tags static -ldflags "-linkmode 'external' -extldflags '-static'"
动态链接编译示例:
CGO_ENABLED=1 go build -o main -tags dynamic
下面将以常见的不同 eBPF 程序类型为例介绍如何集成 elibpcap。
tc/tcx¶
下面是 tc eBPF 程序中需要做的修改:
// add this
static __noinline bool pcap_filter(void *_skb, void *__skb, void *___skb, void *data, void *data_end) {
return data != data_end && _skb == __skb && __skb == ___skb;
}
SEC("tc")
int sample_prog(struct __sk_buff *skb){
bpf_skb_pull_data(skb, 0);
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
// add this
if (!pcap_filter((void *)skb, (void *)skb, (void *)skb, data, data_end)) {
bpf_printk("pcap_filter not match");
goto out;
}
bpf_printk("Hello from tc after pcap filter");
out:
return TC_ACT_UNSPEC;
}
可以看到主要的修改点是:
- 新增了一个 pcap_filter 函数。
- 新增了一个 if (!pcap_filter((void *)skb, (void *)skb, (void *)skb, data, data_end)) 的判断。
下面是对应的 Go 程序所需要做的修改:
func injectFilter(spec *ebpf.CollectionSpec, expr string) error {
if expr == "" {
return nil
}
log.Printf("inject pcap filter: %s", expr)
// add this
oldInsts := spec.Programs["sample_prog"].Instructions
newInsts, err := elibpcap.Inject(expr, oldInsts, elibpcap.Options{
AtBpf2Bpf: "pcap_filter",
DirectRead: true,
L2Skb: true,
})
if err != nil {
return err
}
spec.Programs["sample_prog"].Instructions = newInsts
// end
return nil
}
// add this
if err := injectFilter(spec, expr); err != nil {
log.Fatal(err)
}
if err := spec.LoadAndAssign(&objs, nil); err != nil {
// ...
}
完整的示例程序详见:tc
xdp¶
xdp 程序集成 pcap-filter 能力所需要做的修改与 tc/tcx 程序基本一致。
- c 程序:
SEC("xdp")
int sample_prog(struct xdp_md *ctx){
// ...
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
if (!pcap_filter((void *)ctx, (void *)ctx, (void *)ctx, data, data_end)) {
bpf_printk("pcap_filter not match");
goto out;
}
// ...
}
- go 程序:
func injectFilter(spec *ebpf.CollectionSpec, expr string) error {
// ...
oldInsts := spec.Programs["sample_prog"].Instructions
newInsts, err := elibpcap.Inject(expr, oldInsts, elibpcap.Options{
AtBpf2Bpf: "pcap_filter",
DirectRead: true,
L2Skb: true,
})
if err != nil {
return err
}
spec.Programs["sample_prog"].Instructions = newInsts
return nil
}
// ...
完整的示例程序详见:xdp
cgroup-skb¶
cgroup-skb 程序与前面 tc/tcx/xdp 程序所需要做的修改也是基本一致。
- c 程序:
SEC("cgroup_skb/egress")
int sample_prog(struct __sk_buff *skb){
void *data = (void *)(long)skb->data;
void *data_end = (void *)(long)skb->data_end;
if (!pcap_filter((void *)skb, (void *)skb, (void *)skb, data, data_end)) {
goto out;
}
// ...
}
- go 程序:
func injectFilter(spec *ebpf.CollectionSpec, expr string) error {
// ...
oldInsts := spec.Programs["sample_prog"].Instructions
newInsts, err := elibpcap.Inject(expr, oldInsts, elibpcap.Options{
AtBpf2Bpf: "pcap_filter",
DirectRead: true,
L2Skb: false,
})
if err != nil {
return err
}
spec.Programs["sample_prog"].Instructions = newInsts
return nil
}
这里有一个需要注意的点:
- 因为 cgroup-skb 程序中 skb 的数据里未包含 L2 的数据,因此这里需要设置 L2Skb: false 。
完整的示例程序详见:cgroup-skb
kprobe/fentry/tracepoint¶
elibpcap 帮助我们屏蔽了 kprobe/fentry/tracepoint 程序与前面的 tc/tcx/xdp 程序 在数据读取方面的差异,让我们可以以基本一致的方式进行集成。、
- c 程序:
SEC("kprobe/__dev_queue_xmit")
int BPF_KPROBE(sample_prog, struct sk_buff *skb){
void *skb_head = BPF_CORE_READ(skb, head);
void *data = skb_head + BPF_CORE_READ(skb, network_header);
void *data_end = skb_head + BPF_CORE_READ(skb, tail);
if (!pcap_filter((void *)skb, (void *)skb, (void *)skb, data, data_end)) {
goto out;
}
// ...
}
- go 程序:
func injectFilter(spec *ebpf.CollectionSpec, expr string) error {
// ...
oldInsts := spec.Programs["sample_prog"].Instructions
newInsts, err := elibpcap.Inject(expr, oldInsts, elibpcap.Options{
AtBpf2Bpf: "pcap_filter",
DirectRead: false,
L2Skb: false,
})
if err != nil {
return err
}
spec.Programs["sample_prog"].Instructions = newInsts
return nil
}
// ...
这里有两个需要注意的点:
- 因为在 kprobe 程序中无法通过 “direct packet access“ 的方式读取 skb 中的包数据, 因此我们需要设置 DirectRead: false 。
- 因为我们的 kprobe 程序中检查的是 L3 的数据,因此这里需要设置 L2Skb: false 。
完整的示例程序详见: kprobe , fentry , tracepoint
结束语¶
如果你的使用 Go 和 eBPF 技术开发的网络项目想要集成 pcap-filter 的能力,但却苦于无从下手, 推荐尝试集成 elibpcap 这个巧妙而强大的项目。
Comments