Skip to content
Guilherme Nogueira
Go back

How a Packet Actually Reaches Your Pod

5 min read

Kubernetes networking feels like magic until it breaks, then it feels like a crime scene with no witnesses. A request works from one place and not another, nothing logs an error, the packet just does not arrive. The fix is always the same: stop treating the cluster as one thing, and trace the path a packet takes, hop by hop.

Table of contents

Open Table of contents

The short version

The path, hop by hop

“The pod cannot reach the service” sounds like a single arrow. It is not. It is a sequence of handoffs, each done by a component that knows nothing about the others.

A vertical flow of one request: the client reaches the Service ClusterIP, kube-proxy rules on the node run a netfilter DNAT that rewrites the ClusterIP to a real pod IP, then a routing decision either delivers locally over a veth pair or takes a cross-node hop through the CNI. The packet then hits a NetworkPolicy ingress check that either allows it into the pod or drops it silently. A control plane box shows the API server and kube-proxy programming those rules without ever carrying traffic.

The load balancer picks a node without knowing which nodes hold a healthy pod. The node gets a packet for a ClusterIP that belongs to no real interface, and the kernel’s netfilter chain rewrites that destination (a DNAT) to one real endpoint pod. Then a routing decision: if the pod is local, the packet crosses a veth pair straight into the pod’s network namespace; if not, it takes one more hop through the CNI to another node. Hold that sequence and the question stops being “why is the network broken” and becomes “which hop did the packet not survive.”

The service is a fiction, and kube-proxy just writes it down

The one thing to internalize: a pod has a real IP, a service does not. A service is a couple of API objects (a ClusterIP and a list of endpoints) that kube-proxy turns into rules on every node. That is the whole separation between control plane and data plane.

A control plane and data plane split by a divider. On top, the API server holds the Service object (ClusterIP 10.96.0.10) and an EndpointSlice listing real pod IPs; kube-proxy on each node watches those objects. Dashed control arrows show kube-proxy programming rules down into each node. On the bottom, Node A and Node B each run netfilter or IPVS rules in the kernel, and a solid data-path arrow shows the actual packet flowing through those kernel rules into a pod. kube-proxy has only dashed control arrows, never a solid data-path arrow.

This is why you cannot usefully ping a service but curl works: the rules rewrite the destination to a real pod first. It is a promise kept by rules, not a machine, so it survives every pod behind it being replaced. And notice what kube-proxy is doing: it watches the objects and writes the rules, then steps out. It never carries a packet. So if you are staring at the kube-proxy pod hunting for a dropped request, you are in the wrong place. The rules live on the node, written one of three ways:

ModeHow it steers trafficWhere it bites
iptablesA netfilter rule chain per serviceEvaluated in sequence, so huge clusters pay as rules grow
IPVSAn in-kernel load balancer with hashingScales to many services better, one more subsystem to learn
eBPFPrograms in the kernel datapathFastest and most flexible, newer, needs a capable stack

Why the failures are silent

Most cluster network failures produce no error. They produce absence. A rule drops the packet, and something upstream times out with a message pointing nowhere near the cause.

Warning

A policy that matches nothing is indistinguishable from a policy that denies everything, right up until you check the selector.

The classic trap: a network policy whose label selector does not match the target. Instead of allowing traffic, it allows nothing, and the default-deny behind it drops everything. I lost an afternoon to exactly this once, a selector referencing a label that was not present yet on a fresh cluster. It read correctly, matched zero pods, permitted zero traffic, no log line. When you select something, verify the selector matches what you think. Do not assume.

Debugging is just walking the path

The path is a known sequence, so debugging is procedure, not inspiration. Bisect it in order:

Each check eliminates a hop. You are not searching, you are bisecting, and a dropped packet cannot hide from a bisection.

Final takeaway

Kubernetes networking stops being magic once you refuse to treat it as one thing. The pod is real, the service is a fiction held up by rules, kube-proxy wrote those rules but never carries your traffic. Hold the path in your head and a dropped packet is not a mystery. It is a location you have not checked yet.


Share this post:

Previous Post
AWS Multi-Account Is Not About Accounts. It Is About Boundaries.
Next Post
A Terraform provider for AWS WorkMail, because ClickOps was not an option