felix/design/bpf-overview.md
This file is the always-pulled overview of Calico's eBPF
dataplane: the packet-path mental model, the fast-path performance
discipline, and the cross-cutting review notes that apply to every
BPF change. Per-area design content (TC program layout, XDP,
service NAT, conntrack, encapsulation, observability, and so on)
lives in sibling files under felix/design/ — see
felix/DESIGN.md for the full sub-design index
and the path-to-doc mapping.
If you are editing BPF dataplane code or reviewing a BPF dataplane
PR, read this file plus whichever sibling sub-designs match the
paths the change touches. The Copilot path-scoped instruction
files in .github/instructions/ do
this matching automatically; humans should consult
felix/DESIGN.md's table.
The BPF dataplane is one mode of Felix's single Linux dataplane
codebase, not a separate program: it reuses the shared manager/driver
framework, the InternalDataplane main loop, the OnUpdate/apply()
cycle, and the restart/resync mark-and-sweep doctrine. Those are
documented in dataplane.md; this BPF family covers
only what is BPF-specific — the packet path, the BPF maps, and the
mode's own managers. A BPF dataplane PR therefore usually needs both.
*tables means "the legacy netfilter dataplane, iptables or
nftables", i.e. any non-BPF Linux dataplane Felix can program.
Where a statement is specific to one backend, the backend is
named.cali* veth on the host side, with
a pod on the other side). "HEP" = host endpoint (a physical or
tunnel interface the cluster administrator has put under policy).
Felix treats a small number of special interfaces (the main
route interface, tunnel devices, the bpfnat veth pair) as
HEP-like even when no HostEndpoint CRD exists.cali* veth).Felix attaches BPF programs to every interface it cares about on the host. The attach mechanism varies — TC clsact, TCX, or the netkit attach API for workload netkit interfaces — but the packet-handling code is the same. See bpf-tc-programs.md → Attach mechanisms for the per-mechanism details. The set of attach points is:
cali*), both host-ingress
(TC ingress hook — a packet coming from the pod, policy-egress from
the pod's point of view) and host-egress (TC egress hook — a packet
going to the pod, policy-ingress).BPFDataIfacePattern regex. At minimum the node's main cluster
interface is included; any interface the cluster admin has put under
HEP policy is too.bpfnat veth pair (see bpf-host-networking.md → Host-networked workaround (bpfnat veth)) and the loopback device, which Felix
programs as HEP-like even when the admin hasn't defined a
HostEndpoint for them.XDP programs are attached earlier in the receive path on HEPs that support XDP; they carry untracked policy and can fast-drop hostile traffic before it reaches TC. Connect-time load-balancer programs (bpf-services.md → Connect-Time Load Balancer (CTLB)) are attached to cgroup hooks rather than interfaces.
On host-ingress (a packet arriving on an interface) the TC ingress
hook runs before netfilter. On host-egress (a packet leaving an
interface) the TC egress hook runs after netfilter. BPF
therefore sees the packet before *tables on the way in and after
*tables on the way out, so it can overrule netfilter on ingress
and see netfilter's output on egress.
The practical consequence is that BPF can bypass the host network
stack completely when it has enough information to forward directly.
A packet forwarded via bpf_redirect, bpf_redirect_neigh, or
bpf_redirect_peer skips the host's routing, conntrack and
iptables/nftables chains entirely. bpf_redirect_peer goes a step
further: on a veth destination it hands the packet straight to the
pod side without running the program attached to the host-side peer,
which is the usual way into a local workload on the fast path.
bpf_redirect_peer is only safe in TC ingress context (the helper
checks skb_at_tc_ingress); netkit-attached programs run in xmit
context and would silently drop the packet, so for netkit workload
attach points Felix forces bpf_redirect_peer off and the FIB path
uses plain bpf_redirect. See
bpf-tc-programs.md → Attach mechanisms.
bpf_redirect_peer also leaves the L2 header untouched, so the packet
arrives addressed to the veth's host side. Ordinary pods do not care —
the kernel has already classified it PACKET_HOST — but a workload
that bridges its veth onward, such as a KubeVirt VM, drops the frame as
PACKET_OTHERHOST. Those workloads, and any using ingress QoS (which
needs the host qdisc), set SkipRedir.Ingress; Felix propagates it as
CALI_RT_SKIP_INGRESS_REDIRECT on the route and
CALI_CT_FLAG_SKIP_REDIR_PEER on the conntrack entry, pinning the flow
to the FIB path, which does rewrite the MAC. The opt-out is the
destination's, so it applies to both from-HEP and from-WEP.
BPF does not try to handle every case. It hands packets to the host stack (or lets them continue through it) when it cannot:
bpf_fib_lookup fails, BPF lets the kernel
route the packet so the kernel can populate its FIB and neigh
caches; subsequent packets of the same flow can then be forwarded
directly. This case used to be frequent because bpf_fib_lookup
returned NO_NEIGH whenever the neigh cache was cold, forcing a
host-stack detour even when the route was known. Since Calico's
minimum kernel is 5.10 the bpf_redirect_neigh helper is always
available and BPF resolves the neighbour itself on the fast path,
so the host-stack detour is now the exception.nat-outgoing). Full port-preserving SNAT
requires port allocation state that BPF cannot safely produce; the
*tables SNAT chain handles it. BPF signals this path by marking the
packet (CALI_SKB_MARK_NAT_OUT / CALI_SKB_MARK_MASQ).CALI_SKB_MARK_FALLTHROUGH and lets *tables CT match
against its own entry; see bpf-conntrack-flowstate.md → Switching from *tables to eBPF.*tables raw/nat chain. BPF
cooperates via the SKIP_FIB mark; see bpf-conntrack-flowstate.md → 3rd-party DNAT on host traffic.bpfnat veth routes these packets back through BPF after
*tables has had a chance to MASQUERADE them; see bpf-host-networking.md → Host-networked workaround (bpfnat veth). This only
applies to service traffic; host-originated traffic whose endpoint
is in the host namespace (host-networked peer, host-local socket)
obviously cannot skip the host stack and does not use this path.When BPF can answer the question — FIB hit, known local pod, existing BPF conntrack entry, matched NAT frontend with an acceptable backend — it forwards directly and the host stack never sees the packet.
BPF and *tables communicate via the top bits of the skb mark. The
full table is in felix/bpf-gpl/bpf.h (enum calico_skb_mark); the
marks a reviewer encounters most often are:
| Mark | Set by | Meaning |
|---|---|---|
CALI_SKB_MARK_SEEN | Any BPF program | At least one BPF program has already processed this packet. |
CALI_SKB_MARK_BYPASS | BPF after policy | Packet is approved; downstream BPF does not need to re-validate. |
CALI_SKB_MARK_FALLTHROUGH | BPF on host ingress | No BPF CT entry — let *tables decide based on its CT state. |
CALI_SKB_MARK_CT_ESTABLISHED | *tables rule | *tables CT saw this as part of an established flow. |
CALI_SKB_MARK_SKIP_FIB | BPF or *tables | Do not run the BPF FIB lookup; hand the packet to the host stack. |
CALI_SKB_MARK_NAT_OUT / CALI_SKB_MARK_MASQ | BPF | Flow needs SNAT; iptables MASQUERADE will handle it. |
CALI_SKB_MARK_FROM_NAT_IFACE_OUT | BPF on bpfnatout egress | Packet has passed through the host-networking workaround veth. |
Felix reserves the top three nibbles of the mark (0x1FF00000) for BPF
use. IptablesMarkMask must include this range and leave room for any
non-BPF *tables rules; Felix refuses to start if it does not.
*tables
needs a bit in enum calico_skb_mark and, if *tables has to set
or match it, a matching change in the rule generators under
felix/rules/ (and the nftables variant if applicable). The mark
must also fit inside the reserved mask.The eBPF dataplane's performance comes from keeping the per-packet work on the fast path small. The fast path is everything a packet on an established flow hits: the preamble, the main program's conntrack lookup, a mark check or two, and (on allow) forwarding. Work added here is paid at packet rate — on a hot link, every extra instruction and every extra map lookup is paid millions of times a second.
A change that adds per-packet work to the fast path is not acceptable without explicit justification. The bar is higher than for any other area of the dataplane because the fast path carries all of the production throughput.
ctx->state or per-CPU
state, arithmetic on values the program already holds. These cost
a handful of instructions and no map operation.Where the new work lands matters more than what it is:
The single most common way a PR regresses this dataplane is by
moving work from "flow-creation" to "main" — adding a check or a
lookup that looks cheap but runs on every packet instead of only
when a flow is being established. A reviewer should check whether
a new check could be gated on
state->ct_result.rc == CALI_CT_NEW (or an equivalent "first
packet" condition); if it could and isn't, that's a red flag.
CALI_CT_FLAG_* in conntrack_types.h) at flow creation and
read it on the fast path. DSCP (bpf-observability.md → QoS), Maglev (bpf-services.md → Maglev load balancer) and SVC_SELF
(bpf-services.md → Intra-cluster traffic & service NAT) all follow this pattern.CALI_F_* / HAS_* guard in
bpf.h eliminates the code at verification time. A runtime
global flag costs a load per packet — cheap but not free.The per-section review notes cover what a reviewer should check inside a given topic. This final section collects the handful of checks that don't belong to any single topic — they come up repeatedly in BPF dataplane review because several subsystems happen to share them.
The repo-wide doc-update rule
(.claude/CLAUDE.md → Documentation map,
mirrored in
.github/copilot-instructions.md)
applies. For the BPF dataplane, "changes how it works" means a
new sub-program, a new CT flag, a new mark bit, a new map or map
field, a new config knob affecting any of those, or any change
to the packet path or forwarding decision. The relevant section
of the matching sub-design (and bpf-overview.md if cross-cutting
content is affected) must be updated in the same PR. This file
and its sibling sub-designs under felix/design/ are the
source of truth.
MapParameters.Version in
felix/bpf/.../map.go only when new programs are not
compatible with the old map. Repurposing reserved or padding
bytes so that new programs still read and write the old map
correctly does not require a bump — the old map is still a
valid layout for the new programs, and old programs simply
don't know about the new field. Changes that move fields,
widen the key, shrink the value, or depend on a field that old
programs write as zero do require a bump. The kernel refuses
to pin two different layouts under one name, so a missed bump
blocks upgrades; an unnecessary bump discards warm map state
(cold conntrack, empty NAT, etc.) across the transition.cali_qos), per-CPU (like cali_v4_frgtmp), or LRU with
idempotent semantics (like the conntrack maps). Ad-hoc
hash-with-no-locking is almost never the right choice for
programs that write.See bpf-tc-programs.md → TC program layout for the full list. The short version: a new sub-program needs
an index enum in jump.h, a matching _DEBUG variant, a
SubProg* constant and symbol name in felix/bpf/hook/map.go, and
filtering in GetApplicableSubProgs if it is not universally
applicable. Forgetting any of these leaves the program un-called on
the debug path or on some attach types.
The top nibbles of the skb mark (0x1FF00000) are the BPF-owned
communication channel with *tables. Everything else is either
local to the BPF path (conntrack flags, per-CPU state) or
*tables-internal. A new signal should reuse an existing mark
bit if its semantics match, or allocate a new one — never reuse a
mark bit for a different purpose, and never put a BPF signal in a
bit outside 0x1FF00000 (Felix validates IptablesMarkMask at
startup and will refuse to run if the reserved range isn't
protected).
Every time a packet could be forwarded directly by BPF or handed to the host stack, the decision has to consider:
nat-outgoing, tunnel SNAT)? If yes,
defer — BPF cannot safely allocate source ports.*tables CT entry? If yes, defer or
honour the CALI_SKB_MARK_CT_ESTABLISHED mark.SKIP_FIB.*tables the packet might need?
If its destination is the local host, defer (SKIP_FIB).A PR that "improves performance by forwarding earlier" should list which of these it has considered.
*tablesThe *tables side in BPF mode is thinner than in the pure-*tables
dataplane — most policy is in BPF. Rules that remain are primarily:
*tables to eBPF).A rule added for BPF mode should be conditional on BPFEnabled (or
a more specific flag) and should not duplicate work that BPF already
does (policy, conntrack). If it needs to do both, there is usually
a bug.
Several BPF features depend on kernel version:
cali_progs_ing vs cali_progs_egr is the workaround (bpf-tc-programs.md → TC program layout).tc.IsNetkitSupported) and falls
back to TCX/clsact when not supported. See
bpf-tc-programs.md → Attach mechanisms.bpf_redirect_neigh availability — bpf-host-networking.md → Host-networked workaround (bpfnat veth)'s bpfnat turnaround falls
back to bounce-off-the-veth when this helper isn't available.SubProgIPFrag) — older verifiers reject
it; the loader retries without it (bpf-encap-fragments-icmp.md → IP fragmentation).A PR that uses a new kernel-version-dependent helper must have a fallback path, or the dataplane will fail to load on older kernels Calico still supports.