ClusterIP via kube-proxy

No Pod owns a ClusterIP and no single host answers for it, so how does traffic to it reach a Pod? The kube-proxy on each Node watches the Service and its EndpointSlices and writes Service rules into the Node dataplane, and those rules, not kube-proxy, DNAT a packet sent to that address to one backing Pod. Connection tracking pins each connection to its backend and rewrites the reply back to the ClusterIP, so the client sees one address.

Play the animation 7 steps, animated, best on a wide screen

ClusterIP via kube-proxy, Kubernetes diagram

ClusterIP via kube-proxy, step by step

  1. The ClusterIP is a field of the Service web, stored in the API server. No Pod holds 10.96.0.20 and no single host answers for it, and in the default iptables mode no interface carries it at all.

    Play from step 1
  2. The kube-proxy on the Node watches the Service and its EndpointSlices, then writes Service rules into the Node dataplane: a packet to 10.96.0.20:80 is DNAT-ed to one of the two Pod IPs. It writes rules and never forwards a packet itself.

    Play from step 2
  3. The client opens a connection to 10.96.0.20:80. The packet never passes through kube-proxy: the Service rules in the Node dataplane match that address the moment the packet crosses from the client Pod into the Node.

    Play from step 3
  4. The kernel picks one backend as the Service rules direct and rewrites the destination to it, here 10.244.2.7:8080. Connection tracking records the mapping so every later packet of the flow takes the same backend, and the packet reaches the chosen Pod.

    Play from step 4
  5. The Pod replies from its own IP, and conntrack in the same Node dataplane reverses the translation so the source reads 10.96.0.20 again. The client only ever sees the ClusterIP it dialed, never the Pod address that served it.

    Play from step 5
  6. A second connection to the ClusterIP is a new flow, and with no session affinity the kernel may pick the other backend. It DNATs this one to 10.244.3.9 and conntrack pins it there, while the first flow stays on 10.244.2.7. Each connection keeps its own Pod.

    Play from step 6
  7. The second Pod replies from 10.244.3.9, and conntrack reverses this flow the same way, rewriting the source back to 10.96.0.20 before the reply reaches the client. Two Pods served two connections, and the client only ever saw one ClusterIP.

    Play from step 7

Sources