# xdpcap: a tcpdump style capture tool for XDP programs

> xdpcap is a Go tool that captures packets and the return codes of XDP programs by having the program feed a hook map, then applies ordinary tcpdump and libpcap filter expressions to what it collected. It collects at the eXpress Data Path hook where the XDP program returns, so the captured data reflects what the program actually decided.

**cloudflare/xdpcap** — tcpdump like XDP packet capture

- Repository: https://github.com/cloudflare/xdpcap
- Stars: 787 · Forks: 78
- Language: Go
- License: BSD-3-Clause
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/cloudflare-xdpcap

## What it captures that tcpdump cannot

xdpcap is a tcpdump like tool for the eXpress Data Path, and it captures two things rather than one: the packets and the actions or return codes of the XDP programs that processed them, using standard tcpdump and libpcap filter expressions.

The distinction matters because an XDP program usually runs before the ordinary packet capture point. If a program drops a packet, a normal capture will not show it, and if it redirects a packet the decision is invisible in a post-hoc trace. xdpcap collects at the point where the program itself returns, so the verdict and the packet travel together.

The implementation is Go, the module path is github.com/cloudflare/xdpcap, and the licence is BSD-3-Clause. The dependencies are the cilium eBPF loader at v0.22.0, the Cloudflare cbpfc library, gopacket for packet parsing, the pkg/errors package and the x/net and x/sys standard extensions. The module targets Go 1.25.0.

The repository layout is small: a cmd directory for the command, hook.go and hook.h for the instrumentation side, an internal directory, a testdata directory with a full example, and a hook_test.go. The last push was on 2026-07-30 and there are no GitHub releases, so the tool is consumed as source.

## Instrumenting a program to feed the hook

The instrumentation is the part that makes this work, and it is not automatic. An XDP program has to expose at least one hook point, declared as a program array map:

```C
struct bpf_map_def xdpcap_hook = {
	.type = BPF_MAP_TYPE_PROG_ARRAY,
	.key_size = sizeof(int),
	.value_size = sizeof(int),
	.max_entries = 4, // The max value of XDP_* constants
};
```

The map has to be pinned inside a bpffs, because the tool attaches to it by path and a pinned map keeps its lifetime independent of the process that created it.

For the common case, a header called hook.h provides a convenience macro that declares the same map in one line:

```C
#include "hook.h"

struct bpf_map_def xdpcap_hook = XDPCAP_HOOK();
```

The second change is in the return path. A statement that returns one of the XDP action constants has to be replaced with a call that feeds the hook, so the action reaches the capture tool:

```C
#include "hook.h"

struct bpf_map_def xdpcap_hook = XDPCAP_HOOK();

int xdp_main(struct xdp_md *ctx) {
	return xdpcap_exit(ctx, &xdpcap_hook, XDP_PASS);
}
```

A complete version of this is in testdata/xdp_hook.c. The granularity is a design choice, since a program can expose multiple hook points, or the same underlying map can be reused across programs, which lets you capture from several programs without duplicating the map.

## Running the capture and piping it to tcpdump

Installation is a single go get:

```bash
go get -u github.com/cloudflare/xdpcap/cmd/xdpcap
```

The usage is two forms, and they are the same tool with different output targets. Capturing packets to a pcap file:

```bash
xdpcap /path/to/pinned/map dump.pcap "tcp and port 80"
```

And displaying them, which pipes the capture straight into tcpdump:

```bash
sudo xdpcap /path/to/pinned/map - "tcp and port 80" | sudo tcpdump -r -
```

The dash as the output path is what makes the second form work, and the fact that the filter expression is the same syntax in both cases is the reason the tool calls itself tcpdump like. Anything you can write as a libpcap filter works here, and the README is careful to say the filters are the same standard expressions rather than a new filter language.

The root privileges in the second example are not incidental. The tool attaches to a pinned map and reads the packets the program processed, and attaching to a BPF map and reading raw packet data both need privileges that a normal user does not have.

The Go package also provides a wrapper for creating and pinning the hook maps using the newtools eBPF loader, so you do not have to do the pinning by hand if you would rather use the library. There is one more behaviour worth knowing: xdpcap supports attaching to XDP programs loaded with the BPF_F_XDP_HAS_FRAGS flag, annotated as xdp.frags, and it attempts to attach as usual first and retries with that flag if the first attempt fails.

## The two limitations that shape what you can trust

The README lists two limitations and both of them change how you read a capture.

The first is ordering. Filters run after the instrumented XDP program, so if the program modifies the packet, the filter matches the modified packet and not the original input packet. A filter written against the packet as it arrived will not match, and the reason is that the capture happens downstream of the program's own edits. This is the kind of thing that produces an empty capture and a long afternoon of doubt.

The second is multi-buffer packets. xdpcap is currently unable to capture more than the first page of a packet, and if the instrumented program is loaded with the BPF_F_XDP_HAS_FRAGS flag then packets spanning multiple physical pages will not be entirely captured. For a program handling jumbo frames, that is a data loss condition rather than a display limitation.

The test suite has the same privilege requirement as the tool itself, and the README gives the command as `sudo -E $(which go) test`. The -E flag matters because the eBPF loading path needs the environment preserved.

## Conclusion

xdpcap fits a team debugging an XDP program that is dropping or redirecting traffic and needs to see both the packets and the verdict the program returned, especially where the decision happened before the ordinary capture point. It does not fit a general purpose packet capture need, since a standard tcpdump on the interface is simpler for that. Before using it, read the instrumentation section rather than the usage section, because the tool only works on programs that have been modified to feed the hook map, and remember the documented limitation that the filter runs after the instrumented program, so a filter that matches the original packet will not match a modified one.

## FAQ

### What does xdpcap capture that a normal packet capture does not?

It captures the packets and the actions or return codes of the XDP programs that processed them, which is what an ordinary capture cannot show when a program drops or redirects traffic before the usual capture point. Filters use standard tcpdump and libpcap expressions.

### How do I instrument an XDP program for xdpcap?

The program must expose a hook point, a bpf map of type BPF_MAP_TYPE_PROG_ARRAY pinned inside a bpffs. The hook.h header provides the XDPCAP_HOOK() macro for the declaration, and the return statement has to be replaced with a call that feeds the hook, such as xdpcap_exit with the map and the XDP action, as shown in the testdata example.

### How do I install and run xdpcap?

Install it with `go get -u github.com/cloudflare/xdpcap/cmd/xdpcap`. To write a pcap use `xdpcap /path/to/pinned/map dump.pcap "tcp and port 80"`, and to display packets pipe it to tcpdump with `sudo xdpcap /path/to/pinned/map - "tcp and port 80" | sudo tcpdump -r -`.

### What are the limitations of xdpcap?

Filters run after the instrumented XDP program, so a filter must match the modified packet rather than the original input packet. And xdpcap cannot capture more than the first page of a packet, so with programs loaded using the BPF_F_XDP_HAS_FRAGS flag, packets spanning multiple physical pages are not captured entirely.

### What is xdpcap written in and what does it depend on?

It is Go, with the module path github.com/cloudflare/xdpcap, targeting Go 1.25.0 and licensed BSD-3-Clause. The dependencies are the cilium eBPF loader, the Cloudflare cbpfc library, gopacket, pkg/errors, and the x/net and x/sys standard extensions.

## Sources

- [cloudflare/xdpcap on GitHub](https://github.com/cloudflare/xdpcap)
- [Issues](https://github.com/cloudflare/xdpcap/issues)
- [License: BSD-3-Clause](https://github.com/cloudflare/xdpcap/blob/master/LICENSE)
- [README](https://github.com/cloudflare/xdpcap/blob/master/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cloudflare-xdpcap
