Lesson 06 of 12 · Kubernetes Dataplane
Services, kube-proxy & eBPF
How a ClusterIP actually works on every node: kube-proxy's iptables, IPVS and nftables modes, eBPF replacements like Cilium, and the Service settings that change traffic paths and preserve client IPs.
One API, several dataplanes
A Service is just an API object: a virtual IP plus a selector. Something on every node must turn it into packet forwarding. That's kube-proxy (in one of its modes), or an eBPF dataplane that replaces it.
A Service is a phone number for a team. How the call gets to a team member depends on the office's phone system: an old switchboard operator with a long paper list (iptables), a computerised exchange (IPVS or nftables), or smart phones that already know where to route before the call even leaves the desk (eBPF). The number never changes; only the machinery does.
The modes compared
| Mode | How it works | At scale |
|---|---|---|
| iptables | NAT chains per Service/endpoint; random probability rules | Long chains; slower updates with many Services |
| IPVS | Kernel L4 load balancer with hash tables; several balancing algorithms | Faster lookups; still uses iptables for some parts |
| nftables | Newer kube-proxy mode using nftables sets and maps | Designed to scale better than iptables; the modern default direction |
| eBPF (e.g. Cilium) | Load balancing in eBPF, often at socket connect time | No per-Service chains; excellent visibility |
$ curl -s localhost:10249/proxyMode # on a node running kube-proxy
iptables
$ sudo ipvsadm -Ln # if in IPVS mode
TCP 10.96.142.17:80 rr
-> 10.244.1.7:80 Masq 1 0 0
-> 10.244.2.4:80 Masq 1 0 0
Paths through a Service
ClusterIP (inside the cluster): the client's node picks a backend and DNATs to it (lesson 02).
NodePort / LoadBalancer, with the two traffic policies:
externalTrafficPolicy |
Path | Client IP seen by the pod | Balance |
|---|---|---|---|
| Cluster (default) | Any node → possibly another node's pod (extra hop, SNAT) | Node IP (lost) | Even across all pods |
| Local | Only to pods on the receiving node; nodes without pods fail the LB health check | Preserved | Depends on pods per node |
internalTrafficPolicy: Local does the same for in-cluster traffic (e.g. talk to the node-local log agent).
Other Service settings that change behaviour:
sessionAffinity: ClientIP: stick a client to one pod (see Kubernetes Administration, lesson 08).- Topology-aware routing (
trafficDistribution: PreferClosein newer versions): prefer endpoints in the same zone to cut cross-zone cost and latency. - Headless (
clusterIP: None): no load balancing at all; DNS returns pod IPs (lesson 07).
eBPF: the replacement
With Cilium's kube-proxy replacement, Services are handled by eBPF programs. For pod-to-Service traffic, the translation can happen when the socket connects (the pod connects straight to a backend), so there's no per-packet NAT and no conntrack entry for the ClusterIP. It also gives consistent handling for NodePort, LoadBalancer and externalTrafficPolicy, plus flow visibility with Hubble.
$ cilium service list | head -4
ID Frontend Service Type Backend
1 10.96.0.1:443 ClusterIP 1 => 172.18.0.2:6443
2 10.96.142.17:80 ClusterIP 1 => 10.244.1.7:80
2 => 10.244.2.4:80
Try it: the same Service, three ways
- In your kind cluster, check the proxy mode and find the
webService's rules on a node (iptables or nftables). - Recreate the cluster with
networking.kubeProxyMode: ipvsin the kind config, and inspect the Service withipvsadm -Ln(installipvsadmin the node container if needed). - Create a Cilium cluster with kube-proxy replacement enabled (kind with
kubeProxyMode: none, thencilium installwith kube-proxy replacement), and runcilium service list. - Expose
webas NodePort withexternalTrafficPolicy: Local, curl each node's NodePort, and note which nodes answer.
Going deeper: Service dataplane operations
- Endpoint changes propagate asynchronously to every node. During rollouts, readiness probes and a short preStop delay avoid sending traffic to terminating pods.
- Large endpoint sets rely on EndpointSlices (100 endpoints per slice by default), which keeps updates cheap.
- Health-check
externalTrafficPolicy: LocalServices correctly (healthCheckNodePort), or cloud LBs will send traffic to nodes without pods. - When migrating dataplanes (iptables → nftables or eBPF), leftover rules can conflict. Follow the CNI's migration guide and drain nodes one at a time.
Recap
- Services are virtual IPs implemented per node by kube-proxy (iptables, IPVS, nftables) or an eBPF replacement.
- iptables scales worst; IPVS, nftables and eBPF handle thousands of Services better.
externalTrafficPolicy: Localpreserves client IPs at the cost of balance;Clusterbalances but SNATs.- Topology-aware routing, session affinity and headless Services change traffic paths deliberately.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.