Cilium & eBPF
iptables vs eBPF · Service lookup at scale
Only the first packet of a connection walks the chain — conntrack handles the rest — so the pain shows up as connection-setup latency, CPU on busy nodes, and slow rule syncs when Services churn. kube-proxy's nftables mode (GA in Kubernetes 1.33) also fixes the O(n) match with verdict maps; eBPF additionally bypasses netfilter altogether, keeping its own connection tracking in BPF maps.
Load-balance once, at the socket
Per-packet NAT vs socket-level load balancing
A BPF program attached to the cgroup's connect() / sendmsg() hooks picks a backend and rewrites the socket's destination once. Every packet is born addressed to the pod; no per-packet DNAT, no reverse NAT on replies. getpeername() is translated back, so the app still believes it talks to the ClusterIP.
Where the programs attach
The eBPF hook points Cilium uses
- Socket layercgroup BPF: connect4/6, sendmsg, recvmsg, getpeername
- netfilter / iptablesbypassed by Cilium (kube-proxy lives here)
- tc (traffic control)bpf_lxc on each pod veth · bpf_host on eth0
- XDPin the NIC driver, before an skb is even allocated
- NICeth0 · hardware
Pod → ClusterIP is resolved at the socket, before a packet exists (socket LB). With sidecar meshes like Istio you set socketLB.hostNamespaceOnly=true so pods see the ClusterIP; translation then falls back to tc.
Identity, not IPs — and Hubble to watch it
Security identities → policy verdicts → Hubble flow log
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata: { name: backend, namespace: shop }
spec:
endpointSelector: { matchLabels: { app: backend } }
ingress:
- fromEndpoints: [{ matchLabels: { app: frontend } }]
toPorts:
- ports: [{ port: "8080", protocol: TCP }]
rules:
http: [{ method: GET, path: "/api/.*" }] # L7, via EnvoyLoadBalancer Services without a cloud
Announcing 192.168.1.240
A type: LoadBalancer Service gets 192.168.1.240 from a CiliumLoadBalancerIPPool (LB IPAM). Then something has to tell the network where that IP lives.
L2 announcements: nodes race for a Kubernetes Lease per Service; the holder answers ARP for the VIP, so all traffic enters through one node (then eBPF spreads it to backends). Kill the leader: the lease expires and another node takes over and sends gratuitous ARP — a few seconds of disruption. Perfect for a homelab / Proxmox LAN with no BGP router.
apiVersion: cilium.io/v2
kind: CiliumLoadBalancerIPPool
metadata: { name: lan }
spec:
blocks:
- start: 192.168.1.240
stop: 192.168.1.250
---
apiVersion: cilium.io/v2alpha1
kind: CiliumL2AnnouncementPolicy
metadata: { name: lan }
spec:
loadBalancerIPs: true
interfaces: ["^eth[0-9]+", "^ens.*"]
nodeSelector:
matchExpressions:
- { key: node-role.kubernetes.io/control-plane, operator: DoesNotExist }# BGP control plane (v2 API; older releases used cilium.io/v2alpha1)
apiVersion: cilium.io/v2
kind: CiliumBGPClusterConfig
metadata: { name: tor }
spec:
nodeSelector: { matchLabels: { bgp: "true" } }
bgpInstances:
- name: main
localASN: 65001
peers:
- name: tor
peerASN: 65000
peerAddress: 192.168.1.1
peerConfigRef: { name: tor }
# + CiliumBGPPeerConfig (timers, families) and
# CiliumBGPAdvertisement { advertisementType: Service, service.addresses: [LoadBalancerIP] }Beyond one cluster, beyond networking
Installing on Talos
# cilium-values.yaml — Talos (cluster.network.cni.name: none, cluster.proxy.disabled: true)
ipam:
mode: kubernetes # use node.spec.podCIDR from Talos' podSubnets
kubeProxyReplacement: true
k8sServiceHost: localhost # KubePrism: Talos' local API LB on every node
k8sServicePort: 7445
securityContext:
capabilities:
ciliumAgent: [CHOWN, KILL, NET_ADMIN, NET_RAW, IPC_LOCK, SYS_ADMIN, SYS_RESOURCE, DAC_OVERRIDE, FOWNER, SETGID, SETUID]
cleanCiliumState: [NET_ADMIN, SYS_ADMIN, SYS_RESOURCE]
cgroup:
autoMount: { enabled: false } # Talos already mounts cgroup v2
hostRoot: /sys/fs/cgroup
# optional extras
routingMode: tunnel # or "native" + autoDirectNodeRoutes / BGP
tunnelProtocol: vxlan
bpf:
masquerade: true
l2announcements:
enabled: true # LB IPs on a flat LAN (homelab / Proxmox)
bgpControlPlane:
enabled: false # or peer with your ToR instead
hubble:
relay: { enabled: true }
ui: { enabled: true }
encryption:
enabled: false
type: wireguard
gatewayAPI:
enabled: false # needs the Gateway API CRDs installed firsthelm install cilium cilium/cilium --version 1.18.x -n kube-system -f cilium-values.yaml
cilium status --wait # agents, operator, KPR mode
cilium connectivity test # end-to-end conformance checks
kubectl -n kube-system exec ds/cilium -- cilium-dbg service list # the eBPF service map
cilium hubble port-forward & hubble observe -n shop --verdict DROPPED- why it matterseBPF swaps O(n) iptables chains for O(1) map lookups and moves Service translation to the socket, so load balancing costs one rewrite per connection instead of two per packet — and stays flat at thousands of Services.
- why it mattersIdentity-based policy survives pod churn: labels map to a numeric identity, policy is enforced on identities, and scaling or rescheduling pods doesn't touch any rules. Hubble shows you each verdict as it happens.
- why it mattersOn Talos, Cilium is a natural fit: set cni: none and disable kube-proxy, point it at KubePrism, and you also get LoadBalancer IPs (L2 or BGP), encryption and Gateway API without extra components.