使用 elibpcap 为网络相关 eBPF 程序实现 pcap-filter 包过滤语法支持

前言

我们常用的 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 ,我们需要做三方面的修改:

  1. 一个是,需要修改 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;
    }
    // ...
}
  1. 另一个是,需要修改 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(链路层) 相关的数据。
  1. 最后是,因为 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;
}

可以看到主要的修改点是:

  1. 新增了一个 pcap_filter 函数。
  2. 新增了一个 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 程序基本一致。

  1. 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;
    }
  // ...
}
  1. 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 程序所需要做的修改也是基本一致。

  1. 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;
    }
    // ...
}
  1. 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
}

这里有一个需要注意的点:

  1. 因为 cgroup-skb 程序中 skb 的数据里未包含 L2 的数据,因此这里需要设置 L2Skb: false

完整的示例程序详见:cgroup-skb

kprobe/fentry/tracepoint

elibpcap 帮助我们屏蔽了 kprobe/fentry/tracepoint 程序与前面的 tc/tcx/xdp 程序 在数据读取方面的差异,让我们可以以基本一致的方式进行集成。、

  1. 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;
    }
    // ...
}
  1. 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
}
// ...

这里有两个需要注意的点:

  1. 因为在 kprobe 程序中无法通过 “direct packet access“ 的方式读取 skb 中的包数据, 因此我们需要设置 DirectRead: false
  2. 因为我们的 kprobe 程序中检查的是 L3 的数据,因此这里需要设置 L2Skb: false

完整的示例程序详见: kprobe , fentry , tracepoint

结束语

如果你的使用 Go 和 eBPF 技术开发的网络项目想要集成 pcap-filter 的能力,但却苦于无从下手, 推荐尝试集成 elibpcap 这个巧妙而强大的项目。


Comments