tc-bpf vs XDP vs netfilter: choosing your attach point
April 15, 2026 · kernel notes · ebpf · tc · netfilter · linux · networking
Linux has three serious places where you can intercept and decide on packets: XDP, tc-bpf, and netfilter (iptables/nftables). They’re often discussed as if they’re alternatives. They’re not, exactly. They run at different points in the stack, see different things, and have different tradeoffs.
Here’s how to pick.
The path of a packet
For an incoming TCP packet on a Linux box, the rough order is:
- NIC receives the packet, raises an interrupt.
- XDP programs run, in NIC driver context. The packet hasn’t been allocated as an
sk_buffyet. - The driver builds an
sk_buff. - tc ingress runs. BPF programs attached here see the
sk_buff. - netfilter PREROUTING chain runs (
iptables -t nat -A PREROUTING,iptables -A PREROUTING). - Routing decision: is this packet for us, or to be forwarded?
- If for us: netfilter INPUT chain, then handed to socket layer.
- If forwarded: netfilter FORWARD chain, then netfilter POSTROUTING.
Outgoing packets traverse it in reverse.
XDP: line-rate filtering at the door
XDP programs run before any kernel allocation. That’s why they’re fast. It’s also why they see less than tc.
What XDP gets:
- Raw bytes from the wire
- Source/destination MAC, IP, ports if the packet has them
- The interface the packet arrived on
What XDP does not get:
- Connection state (no conntrack)
- Socket lookups (the kernel hasn’t done one yet)
- Reassembled fragments
- Tunnel decapsulation (mostly — depends on driver)
Use XDP when:
- You need to drop traffic before the kernel commits resources to it (DDoS mitigation, blocklists)
- You need maximum throughput (hundreds of millions of packets per second on modern NICs)
- Your decision can be made from L2-L4 information alone
Don’t use XDP when:
- You need TCP state (“only after handshake, only on established connections”)
- You need to make decisions per-application or per-socket
- You’re working with virtual interfaces in a lab (XDP on veth works but tells you nothing about real performance)
tc-bpf: the practical default for Kubernetes
tc-bpf attaches to the traffic control subsystem, after the sk_buff is allocated but before netfilter runs. This is where Cilium and most Kubernetes networking solutions actually live.
What tc gets that XDP doesn’t:
- The
sk_buffstructure itself, with kernel metadata - Helper functions for socket lookup, connection tracking, redirect to another interface
- Both ingress and egress hooks
What tc loses compared to XDP:
- Latency: the
sk_buffallocation already happened, so you can’t do “drop before allocation” - Throughput: you’re 50-200% slower than XDP under heavy load
Use tc-bpf when:
- You’re in a container/Kubernetes environment and working with veth pairs
- You need helpers like
bpf_sk_lookup_tcpto find the socket for a packet - You want to redirect packets between interfaces (
bpf_redirect_peerfor fast container egress)
Cilium uses tc-bpf as its default datapath because it’s the right tradeoff for K8s. The container interfaces are veth pairs; XDP on veth doesn’t have driver-level optimization, so the throughput advantage shrinks. The tc API is also more flexible.
netfilter: when you actually want stateful firewalling
iptables and nftables are netfilter front-ends. They use the same kernel infrastructure. Modern code should target nftables; iptables is deprecated as a control plane (the kernel hooks remain identical).
What netfilter gets that BPF approaches don’t easily:
- Stateful connection tracking (conntrack), free
- NAT, free
- Connection-scoped match types (CT state, helper-based, etc.)
What netfilter is bad at:
- Performance under millions of rules: linear chain traversal hurts
- Complex programmatic logic: nftables expressions are powerful but limited
- Modern observability: getting metrics out of nftables is harder than BPF
Use netfilter when:
- You want stateful firewalling and don’t want to write conntrack yourself
- You need NAT (DNAT, SNAT, masquerade)
- Your rules fit in a few hundred entries
Don’t reach for netfilter when:
- You need millions of dynamic rules (use BPF maps)
- You need to filter based on L7 content (you’ll need BPF or DPI tools)
- You’re building per-packet observability (BPF maps and ringbuf are better at this)
Practical advice
For a Kubernetes node agent: start with tc-bpf. You get verbose enough information, you can attach to host and pod interfaces, you have access to kernel helpers. XDP is an optimization for hotpaths once you’ve validated functionality.
For a host-level firewall on a server: netfilter (nftables) plus targeted BPF. nftables for the basic rules and conntrack; BPF for whatever specific high-rate observability or filtering you need.
For DDoS mitigation: XDP. There’s no substitute for dropping before the kernel commits.
For “I want to see all the packets and make decisions”: probably tc-bpf or kprobes on tcp_v4_rcv and friends, depending on whether you need per-packet or per-connection visibility.
The right answer is rarely just one of these. Production stacks combine them.