前言¶
我们常用的 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 程序,在需要实现数据包过滤的位置增加类似如下的 placeholder 逻辑:
// 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",
PacketAccessMode: elibpcap.Direct,
L2Skb: true,
})
if err != nil {
return err
}
spec.Programs["sample_prog"].Instructions = newInsts
其中:
- AtBpf2Bpf 用于指定是我们在 eBPF 程序中定义的 static __noinline bool pcap_filter(...) 函数的名称。
- PacketAccessMode 用于指定 elibpcap 在生成的字节码时,应当使用哪种方式读取我们在 eBPF 程序中向 pcap_filter 函数所传入的 skb 数据。
取值如下:
- elibpcap.Direct :使用 direct access 的方式读取数据,即,直接通过指针操作读取数据。
- elibpcap.BpfProbeReadKernel :使用辅助函数 bpf_probe_read_kernel 读取数据。
- elibpcap.BpfSkbLoadBytes :使用辅助函数 bpf_skb_load_bytes 读取数据。
- 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",
PacketAccessMode: elibpcap.Direct,
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",
PacketAccessMode: elibpcap.Direct,
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",
PacketAccessMode: elibpcap.Direct,
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",
PacketAccessMode: elibpcap.BpfProbeReadKernel,
L2Skb: false,
})
if err != nil {
return err
}
spec.Programs["sample_prog"].Instructions = newInsts
return nil
}
// ...
这里有两个需要注意的点:
- 因为在 kprobe 程序中无法通过 “direct packet access“ 的方式读取 skb 中的包数据, 因此我们选择设置 PacketAccessMode: elibpcap.BpfProbeReadKernel 使用 bpf_probe_read_kernel 读取数据。
- 因为我们的 kprobe 程序中检查的是 L3 的数据,因此这里需要设置 L2Skb: false 。
完整的示例程序详见: kprobe , fentry , tracepoint
socket filter¶
socket filter 程序的一个限制是无法使用 direct access 或 bpf_probe_read_kernel 的方式读取数据包, 只能使用类似 bpf_skb_load_bytes 的方式读取数据包。
下面是使用 socket filter 程序集成 elibpcap 的示例代码:
- c 程序:
SEC("socket")
int sample_prog(struct __sk_buff *skb){
void *data = (void *)(long)skb;
char dummy[1];
bpf_skb_load_bytes(skb, 0, &dummy, sizeof(dummy));
if (!pcap_filter((void *)skb, (void *)skb, (void *)skb, (void *)data, (void *)dummy)) {
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",
PacketAccessMode: elibpcap.BpfSkbLoadBytes,
L2Skb: true,
})
if err != nil {
return err
}
spec.Programs["sample_prog"].Instructions = newInsts
return nil
}
// ...
这里有三个需要注意的点:
- 我们需要使用 bpf_skb_load_bytes 读取数据,因此 PacketAccessMode 被设置为 elibpcap.BpfSkbLoadBytes 。
- 在 socket filter 程序中,我们是直接从 skb 而不是从 skb->data 中读取的数据,因此 data 被设置为 skb。
- 在 socket filter 程序中,我们无法直接获取 skb->data_end, 并且 pcap_filter 中的 data_end 在使用 BpfSkbLoadBytes 访问方式时只是一个用于申请一个栈变量的 placeholder 变量, 因此我们可以直接使用一个 dummy 变量作为 date_end。
完整的示例程序详见: socket-filter 。
结束语¶
如果你的使用 Go 和 eBPF 技术开发的网络项目想要集成 pcap-filter 的能力,但却苦于无从下手, 推荐尝试集成 elibpcap 这个巧妙而强大的项目。
Comments