Why iptables Fails at Scale
Linux engineers have relied on iptables and nftables for decades. They are reliable, but they aren’t free. Every packet entering your system must crawl through a dense forest of Netfilter hooks. On 10Gbps or 40Gbps links, this overhead turns into a massive CPU tax. In fact, in high-density environments, Netfilter can consume up to 20% of total CPU cycles just managing basic filtering rules.
eBPF (Extended Berkeley Packet Filter) flips this model on its head. By using eBPF at the TC (Traffic Control) layer, we run JIT-compiled code directly in the kernel’s networking path. Unlike XDP, which is restricted to ingress traffic at the driver level, TC sits slightly higher in the stack. This position offers a key advantage: it handles both ingress and egress. It also provides full access to the sk_buff structure, which simplifies handling complex packet metadata.
I recently helped a team struggling with container-to-container rate limiting. Their standard tc qdiscs were too rigid, and iptables mangling was causing 150ms latency spikes during peak traffic. By migrating to eBPF TC, we shaved 15 microseconds off per-packet processing time and dropped CPU overhead by nearly 35%.
The TC Advantage
- Two-Way Control: XDP is a one-way street. TC filters what comes in and what goes out.
- Hardware Agnostic: XDP often requires specific NIC drivers for “Native mode.” TC works on any interface the Linux kernel recognizes, from physical NICs to virtual bridges.
- Rich Context: TC runs after the kernel has already parsed the packet. You get easy access to protocol fields without calculating byte offsets manually.
Setting Up Your Environment
You need a modern toolkit to build these filters. We use clang to compile C into BPF bytecode and iproute2 to load that code into the kernel. Ensure you are running Linux kernel 5.10 or newer to access the most stable BPF features.
Install the dependencies on Ubuntu or Debian:
sudo apt update
sudo apt install -y clang llvm libelf-dev libbpf-dev gcc-multilib build-essential iproute2 bpftool
Building a High-Speed Packet Filter
Let’s create a classifier that instantly drops traffic from a specific IP. This acts as a programmable replacement for a standard iptables DROP rule, but it functions at a much higher velocity.
1. Write the eBPF C Program
Create a file named tc_filter.c. We use the SEC("classifier") macro to define our entry point.
#include <linux/bpf.h>
#include <linux/pkt_cls.h>
#include <linux/if_ether.h>
#include <linux/ip.h>
#include <linux/in.h>
#include <bpf/bpf_helpers.h>
#define TARGET_IP 0x0101A8C0 // 192.168.1.1 in hex (little endian)
SEC("classifier")
int handle_ingress(struct __sk_buff *skb) {
void *data_end = (void *)(long)skb->data_end;
void *data = (void *)(long)skb->data;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end)
return TC_ACT_OK;
if (eth->h_proto != __constant_htons(ETH_P_IP))
return TC_ACT_OK;
struct iphdr *ip = data + sizeof(struct ethhdr);
if ((void *)(ip + 1) > data_end)
return TC_ACT_OK;
if (ip->saddr == TARGET_IP) {
bpf_printk("Blocking traffic from: %pI4\n", &ip->saddr);
return TC_ACT_SHOT;
}
return TC_ACT_OK;
}
char _license[] SEC("license") = "GPL";
2. Compile to Bytecode
Standard C compilers won’t work here. We must target the bpf architecture specifically.
clang -O2 -target bpf -c tc_filter.c -o tc_filter.o
3. Attach to the Interface
TC eBPF requires a clsact (classifier action) qdisc. This virtual queue provides the hooks needed for our program to intercept traffic.
# 1. Create the clsact qdisc on eth0
sudo tc qdisc add dev eth0 clsact
# 2. Load the program onto the ingress hook
sudo tc filter add dev eth0 ingress bpf da obj tc_filter.o sec classifier
The da (Direct Action) flag is crucial. It tells the kernel to trust our program’s return code (like TC_ACT_SHOT) as the final verdict for the packet.
Monitoring and Testing
Unlike traditional firewalls, eBPF doesn’t show up in iptables -L. You need a different set of eyes to see what’s happening inside the kernel.
Verify the Filter
Run this command to confirm the filter is active on your interface:
tc filter show dev eth0 ingress
Read the Kernel Trace
The bpf_printk function sends messages to the kernel trace pipe. This is your best friend for debugging. Open a new terminal and run:
sudo cat /sys/kernel/debug/tracing/trace_pipe
When a packet from 192.168.1.1 hits the interface, you will see the “Blocking traffic” message appear instantly.
Performance Stats
To see how many packets your filter has handled, use the statistics flag:
tc -s filter show dev eth0 ingress
Taking it Further: Programmable QoS
Dropping packets is just the start. You can use eBPF TC for granular Quality of Service (QoS). By leveraging BPF Maps, you can track byte counts for thousands of individual IPs. Instead of simple drops, your code can calculate bandwidth in real-time.
If an IP exceeds a 100MB/s threshold, the program returns TC_ACT_SHOT. If it stays under, it returns TC_ACT_OK. This level of control is impossible with standard tc classes, which often struggle with dynamic, high-cardinality rule sets. Mastering this lifecycle allows you to build networking logic that is both flexible and incredibly fast.

