# bpftools: turning packet captures into BPF drop rules

> Cloudflare's bpftools is a small Python toolkit for analysing pcap dumps and generating BPF filters from them, with bpfgen producing the bytecode and iptables_bpf turning a domain list into a drop rule. The scripts are old enough to assume Python 2, which is both the tool's main limitation and the clearest signal of how far the project has moved since it was written.

**cloudflare/bpftools** — BPF Tools - packet analyst toolkit

- Repository: https://github.com/cloudflare/bpftools
- Stars: 1,233 · Forks: 141
- Language: Python
- License: BSD-3-Clause
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/cloudflare-bpftools

## The stated aim: filters that match and drop hostile traffic

The repository describes itself as a set of tools for analysing and processing pcap traffic dumps, and it names the goal without hedging: to help create BPF rules that will match, and drop, malicious traffic.

That framing matters. This is not a general purpose packet analysis toolkit that happens to be able to emit filters. The workflow it is built for runs from evidence to enforcement. You capture traffic, you look at what is in the capture, you decide what pattern is hostile, and then you want that pattern enforced at the kernel rather than logged and inspected later.

The scripts in the repository cover the pipeline in pieces. There are converters between pcap and hex, pcap2hex and hex2pcap, a DNS parser called parsedns, a filter script, an offset utility, and two generators: bpfgen, which produces the BPF bytecode, and iptables_bpf and iptables_bpf_chain, which produce the rules that install it.

The two Cloudflare blog posts linked from the README give the background: one on BPF as the forgotten bytecode, and one introducing these tools. That is the intended reading order, and it tells you the audience: people who already know what a packet filter is and want to industrialise the process of writing one.

The code is Python, and the licence file in the repository is named LICENSE-BSD-CloudFlare, so the terms are BSD style.

## The build chain: kernel headers, libpcap, and two C binaries

Running the scripts needs three things, and the README lists them as prerequisites.

Kernel headers, ideally from a 3.10 or newer kernel:

```bash
sudo apt-get install linux-headers-generic
```

The dependencies, which include the libpcap and readline development packages along with the build tools for the C parts:

```bash
sudo apt-get install python-setuptools libpcap-dev libreadline-dev binutils-dev bison flex
sudo easy_install pcappy
```

And then the binary tools, which are built inside the linux_tools directory:

```bash
make
```

The Makefile explains what that produces. The default target builds linux_tools/bpf_dbg and linux_tools/bpf_asm, each by descending into linux_tools with its own make invocation, and then runs a single sanity check, python -c "import pcappy", which fails the build if the pcap binding is not importable. There is also a test target that runs python -m unittest discover tests/.

So the toolchain is a hybrid. The Python scripts are the interface, but the BPF assembler and the debugger are C programs that have to be compiled against your kernel headers, which is why the header requirement is stated first. The build is not hermetic either: easy_install and pcappy are the dependency path the README gives, and both belong to an older generation of Python packaging.

## bpfgen is the core, and every filter has its own help

The README calls bpfgen the core script, the one that generates the BPF bytecode, and then points at the help output rather than explaining the options in prose. Four invocations are given:

```bash
$ ./bpfgen --help
$ ./bpfgen dns -- --help
$ ./bpfgen dns_validate -- --help
$ ./bpfgen suffix -- --help
```

The double dash separator is the pattern to notice. The first argument selects the filter, and the second group is passed to the filter's own parser, so bpfgen dns is a subcommand with its own options rather than a single tool with a pile of flags. dns is the DNS matcher, dns_validate is the validator for it, and suffix matches on a domain suffix.

The instruction to read the help rather than a manual is honest about the state of the documentation. There is no wiki, no per filter guide and no example corpus in the repository, so the help text is the reference. A reader who skips it will not know which filters exist beyond the three named in the README.

The naming also hints at the design. bpfgen emits bytecode, and bytecode needs an assembler, which is why the C binaries in linux_tools have to exist before the Python layer can produce anything useful. The offset script and the filter script in the repository root sit alongside the generators as the pieces that let you inspect what the generated filter actually matches.

## iptables_bpf dns turns a domain into a drop script

The other generator is the one that produces something you can deploy. iptables_bpf generates a simple bash script containing iptables rules that drop traffic based on selected parameters, and the README walks through the cases.

A single domain:

```bash
$ ./iptables_bpf dns -- example.com
Generated file 'bpf_dns_ip4_example_com.sh'
```

The same with IPv6, using the -6 flag:

```bash
$ ./iptables_bpf -6 dns -- example.com
Generated file 'bpf_dns_ip6_example_com.sh'
```

Several domains at once, where the file name simply concatenates them:

```bash
$ ./iptables_bpf dns -- example.com example1.com example2.com
Generated file 'bpf_dns_ip4_example_com_example1_com_example2_com.sh'
```

The output naming is a small usability point. A generated script called bpf_dns_ip4_example_com.sh tells you what it matches and which address family it applies to, which matters when you end up with a directory of rules and need to know which one to remove.

Applying and removing are the same script with different arguments, and both need root:

```bash
$ sudo ./bpf_dns_ip4_example_com.sh 1.2.3.4/32
$ sudo ./bpf_dns_ip4_example_com.sh --delete
```

The first form takes one or more addresses, in CIDR notation, to match the rule against. The second removes it.

## The star wildcard only works in one position

Subdomain matching is done with a star, and the README is precise about the limitation: this will only work if the star is the only character in a domain part.

```bash
$ ./iptables_bpf dns -- *.example.com
Generated file 'bpf_dns_ip4_any_example_com.sh'
```

The naming convention encodes the wildcard as any, so bpf_dns_ip4_any_example_com.sh is the file that matches any subdomain of example.com. The same rule applies further left:

```bash
$ ./iptables_bpf dns -- *.example.*.gov.de
Generated file 'bpf_dns_ip4_any_example_any_gov_de.sh'
```

The constraint is a consequence of how a BPF filter has to compare a name. A match against a fixed suffix of a domain is a byte comparison, which is cheap and can be expressed as a length check plus a memcmp. A star in the middle of a part, such as ex*ple.com, would require matching a variable length segment inside a part, which the generated comparison cannot express, so the generator refuses to pretend otherwise.

For an operator this is a boundary worth knowing before you rely on the output. A rule that matches any label under a given suffix is expressible. A rule that matches a shape inside a label is not, and you would have to fall back on a different mechanism.

The alternative in the same family is iptables_bpf_chain, which the README lists alongside iptables_bpf for the chaining case, where a rule has to sit in a specific chain rather than at the top of the filter table.

## The Python 2 assumption runs through the build files

This is where the age of the project shows most clearly, and it is worth reading the build files rather than the README.

The setup.py in the repository root is Python 2 code. It contains the print statement form, print >> sys.stderr, "Type 'make' before packaging", which is a syntax error under Python 3, and the script is arranged to run make -C linux_tools for you if bpftools/bpf_asm is missing, copying the binary into the package directory and shipping it as package data under the name bpf_asm. The package it defines is bpftools, version 1.0, with a single package and a maintainer contact at Cloudflare.

The Makefile is Python 2 flavoured as well, with the build's sanity check written as python -c "import pcappy" and the test target as python -m unittest discover tests/.

The dependency instructions agree with that era. easy_install is the installer the README asks for, pcappy is the pcap binding, and on a current Python 3 environment none of that is a default.

The practical consequence is that this is source you read and adapt rather than a package you pip install. The setup.py path is not usable as written on Python 3, and the documented dependency chain will need replacing with a modern equivalent before anything runs on a current distribution. The upside is that the project is small, the scripts are plain Python, and the C parts are self contained in linux_tools, so adapting it is a matter of hours rather than a rewrite.

## Writing the filter expression yourself instead

The honest alternative is to skip the generators and write the filter yourself. The output of bpfgen is BPF bytecode, and the same filter language is what packet tools accept directly, so the question is whether the higher level generators buy you anything for your case.

What the generators give you is a specification instead of a filter expression. You say dns and a domain, and the tool works out the comparison, the length check and the address family, then hands back either the bytecode or a ready made iptables script. The cost is that you are running a Python 2 era toolchain, and the domain semantics are the generator's, not yours.

Writing the expression by hand inverts that. You control exactly what is compared, you can use whatever the underlying filter language supports, including patterns the generator refuses, and you need nothing beyond the filter language and your capture. The loss is the repeatability: the generator's value is that the same domain list always produces the same rule, with a name that tells you what it matches.

The other option is not to filter in the kernel at all. If the traffic is hostile rather than merely wasteful, dropping packets may not be the goal, and a userspace decision point is a different design. Nothing in the repository speaks to that case, which is worth remembering: bpftools is a tool for turning an observation into a kernel level rule, and nothing more.

## Conclusion

Adopt bpftools if you are investigating hostile traffic on a Linux host and want a repeatable way to turn an observed pattern into a kernel level drop rule, and if you are comfortable maintaining a small Python 2 era codebase. Do not adopt it for a greenfield filtering stack, for a language other than Python, or if you need ongoing releases, since the repository has no GitHub releases and the last push was on 2026-04-24. Verify first that your kernel headers are 3.10 or newer, that pcappy and libpcap are installed, that make produces linux_tools/bpf_asm, and that the generated shell script is one you are willing to run under sudo on a live machine.

## FAQ

### What do I need to build the Cloudflare bpftools scripts?

Kernel headers from a 3.10 or newer kernel, the development dependencies listed in the README including libpcap-dev, libreadline-dev, binutils-dev, bison and flex, the pcappy binding installed with easy_install, and a run of make in the linux_tools directory to build bpf_dbg and bpf_asm.

### How do I generate an iptables drop rule for a domain with bpftools?

Run ./iptables_bpf dns -- example.com, which writes a script named bpf_dns_ip4_example_com.sh. Apply it with sudo ./bpf_dns_ip4_example_com.sh 1.2.3.4/32 and remove it again with the --delete flag. Use -6 for the IPv6 variant.

### What are the limits of the star wildcard in iptables_bpf?

The README states that the star will only work if the star is the only character in a domain part, so *.example.com and *.example.*.gov.de work, while a pattern inside a label does not.

### Does bpftools work with Python 3?

The repository's setup.py uses the Python 2 print statement form and the Makefile calls python with pcappy and unittest discover, and the README asks for easy_install to install pcappy. The scripts are plain Python and small, but the packaging and dependency path as documented belongs to the Python 2 era.

## Sources

- [cloudflare/bpftools on GitHub](https://github.com/cloudflare/bpftools)
- [Issues](https://github.com/cloudflare/bpftools/issues)
- [License: BSD-3-Clause](https://github.com/cloudflare/bpftools/blob/master/LICENSE)
- [README](https://github.com/cloudflare/bpftools/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-bpftools
