Preface¶
A core feature of the common tcpdump packet capture tool is its support for the pcap-filter syntax. This lets it filter traffic and capture only specific packets matching the filter.
When developing network-related tools with eBPF, supporting the pcap-filter syntax would greatly improve user experience. That's why the ptcpdump tool I developed includes built-in support for the pcap-filter syntax.
Adding pcap-filter support to eBPF programs the usual way involves complex logic. However, if your project uses cilium/ebpf as its eBPF loader, you can use the elibpcap Go package introduced in this article. This lets you add built-in pcap-filter syntax support to your project with just a few lines of code.
Project Introduction¶
As mentioned before, using elibpcap allows you to add pcap-filter support to an eBPF project with just a few lines of code.
Here is a project introduction summarized by AI:
elibpcap is a Go package that provides functionality for compiling pcap filter expressions into eBPF bytecode and injecting these filters into existing eBPF programs. It bridges the world of human-readable pcap filter expressions (like those used in tools such as tcpdump) with the power and efficiency of eBPF programs running in the Linux kernel.
Project Repository¶
Usage Example¶
Usage Pattern¶
To use elibpcap, we need to make changes in three areas:
- First, modify the eBPF program. Add placeholder logic like the following where packet filtering is needed:
// 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;
}
// ...
}
- Second, modify the Go program. Call the elibpcap package to add the logic for integrating pcap-filter capability:
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
Where:
- AtBpf2Bpf specifies the name of the static __noinline bool pcap_filter(...) function defined in our eBPF program.
- DirectRead specifies if the skb data passed to the pcap_filter function in our eBPF program supports direct access reading.
- L2Skb specifies if the skb data passed to the pcap_filter function in our eBPF program includes L2 (link layer) related data.
Finally, because elibpcap depends on libpcap, you need to enable CGO during compilation:
Static linking example:
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'"
Dynamic linking example:
CGO_ENABLED=1 go build -o main -tags dynamic
The following sections will show how to integrate elibpcap with examples for different common eBPF program types.
tc/tcx¶
Below are the changes required in the tc eBPF program:
// 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;
}
The main changes are:
- Added a pcap_filter function.
- Added an if (!pcap_filter((void *)skb, (void *)skb, (void *)skb, data, data_end)) check.
Below are the corresponding changes required in the Go program:
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 {
// ...
}
See the complete example program: tc
xdp¶
The modifications needed to integrate pcap-filter capability into XDP programs are basically the same as for tc/tcx programs.
- c program:
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 program:
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
}
// ...
See the complete example program: xdp
cgroup-skb¶
The modifications needed for cgroup-skb programs are also basically the same as those for the previous tc/tcx/xdp programs.
- c program:
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 program:
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
}
One point to note: Because the skb data in cgroup-skb programs does not contain L2 data, set L2Skb: false here.
See the complete example program: cgroup-skb
kprobe/fentry/tracepoint¶
elibpcap hides the data reading differences between kprobe/fentry/tracepoint programs and the tc/tcx/xdp programs mentioned earlier. This allows us to integrate them in a mostly consistent way.
- c program:
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 program:
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
}
// ...
Here are two things to note:
- Because kprobe programs cannot read packet data from skb using "direct packet access", we need to set DirectRead: false.
- Because our kprobe program checks L3 data, we need to set L2Skb: false here.
See the complete example program: kprobe , fentry , tracepoint
Conclusion¶
If you want to add pcap-filter support to your Go and eBPF network project but don't know where to start, try integrating the clever and powerful elibpcap project.
Comments