Pod Networking on a Single Node
Two pods on the same node talk to each other. No overlay, no routing protocol, no cloud involvement. Understanding this simplest case gives you the vocabulary for every harder one.
Everything later in this course is a variation on what happens here. Cross node traffic adds a way to get between machines. Services add address rewriting. Ingress adds a path in from outside. All of them end with the delivery described in this lesson, so this is the piece worth being able to picture precisely.
What is actually happening
A pod has its own network namespace. That is the single fact underneath everything else.
A network namespace is a complete, independent copy of the kernel's networking stack: its own interfaces, its own routing table, its own firewall rules, its own connection tracking, its own sockets. The pod is not simulated and it is not sharing the host's stack with some filtering applied. It has a real one of its own.
Inside that namespace, the pod sees two interfaces. Loopback, and one interface named eth0 with the pod's address on it. From inside, this is indistinguishable from a small machine with one network card.
That eth0 is one end of a virtual ethernet pair, which is the mechanism connecting the pod's namespace to the host's. A veth pair is a virtual cable with two ends. Whatever is written into one end comes out of the other. One end lives in the pod namespace as eth0, the other lives in the host namespace with a generated name.
That is the entire boundary. A pod is a namespace with a cable running out of it.
The boundary between a pod and its host is a pair of connected interfaces, nothing more. There is no proxy, no translation layer, and nothing inspecting traffic by default. When you are debugging and you need to know where the pod's view ends and the host's begins, it is that cable. Everything the pod can see is on its side; everything about how the packet then reaches its destination is on the other.
The namespace mechanism itself, and virtual interfaces as kernel primitives, are the subject of Container Internals and Runtime Engineering. What matters here is what your plugin did with them.
The Kubernetes model versus the CNI's choice
Kubernetes requires that the pod has its own address and that other pods can reach it. It says nothing about how the host end of that cable is wired, and there are three common answers.
Attached to a bridge. The host end of every pod's veth is plugged into a software bridge. All pods on the node share one layer 2 segment, and pod to pod traffic on the node is switched by the bridge, exactly as if they were machines on the same physical switch. Simple to picture and the reason most diagrams show a bridge.
Routed individually. No bridge at all. The host end of each veth has no address, and the plugin installs a route per pod pointing at that specific interface. Pod to pod traffic on the same node is routed by the host rather than switched. Less intuitive, and it scales better and gives the plugin a per-pod attachment point for policy.
Attached to a program. The host end has a program attached to it that handles forwarding and policy in the kernel. Common in eBPF based dataplanes, which are covered in depth in eBPF and Cilium for Platform Engineers.
All three satisfy the model. They look completely different when you go looking on the node, and the commands you reach for differ accordingly, which is why the previous lesson insisted you find out which one you have before you need to know.
How it works in practice
Trace it yourself on a running cluster. Start from inside the pod:
kubectl exec -it payments-api-0 -- ip addr show eth0
kubectl exec -it payments-api-0 -- ip route
The routing table is the interesting part. It tells you what the plugin decided the pod's gateway is, and that single line distinguishes the designs above.
Now find the other end. The pod's eth0 reports a peer index, and that number is the host interface it is connected to:
# Inside the pod, the number after the @ on eth0 is the host side index
kubectl exec -it payments-api-0 -- cat /sys/class/net/eth0/iflink
14
# On the node, that index is the other end of the same cable
ip link | grep '^14:'
Then ask what that host end is attached to. If the answer names a bridge, you have the first design. If it names nothing and there is a route pointing at the interface, you have the second:
# On the node
ip link show master <bridge-name> # design one: is it enslaved to a bridge?
ip route | grep 10.0 # design two: is there a route per pod?
When a pod cannot reach anything, test in this order: can it reach itself on loopback, can it reach its own gateway, can it reach another pod on the same node, can it reach a pod on a different node. Each step adds exactly one mechanism. The first one that fails tells you which mechanism is broken, and it is a much faster path than starting from the application and working outward.
Common failure modes
The process is bound to loopback. The application listens on 127.0.0.1 rather than all interfaces. Inside the pod it works perfectly. From anywhere else the connection is refused, because the pod's loopback is genuinely its own and nothing outside the namespace can reach it. This is the single most common "my pod is unreachable" cause and it is not a networking problem at all.
The pod has an address but no route. The plugin assigned an address and failed to install the default route, so the pod can reach its immediate neighbours and nothing else. Rare, and it produces an extremely confusing partial connectivity.
A stale veth on the host. A pod goes away and its host side interface is not cleaned up. Usually harmless, occasionally the cause of address reuse problems, and always confusing when you are reading ip link output looking for the right one.
Assuming pods on a node can see each other's traffic. With a bridge they share a segment, and with per-pod routes they do not. Anyone writing a sidecar that expects to observe neighbouring traffic is depending on a plugin choice.
A service worked in the developer's local cluster and refused every connection in staging. The manifests were identical, the image was identical, and the pod was running and ready. The application bound to 127.0.0.1 by default and read an environment variable to bind more widely. Locally the developer ran it directly on their machine, where loopback and the real interface were both reachable from their browser. In the cluster, the pod's loopback belonged to the pod's namespace alone, so a connection from anywhere else had nothing to reach. The pod was healthy because its readiness probe used loopback too, which meant everything reported green. The fix was one environment variable. The lesson is that the pod's loopback is not the node's loopback and is not shared with anything.
What people get wrong
Thinking the pod's network is a filtered view of the host's. It is a separate stack. A rule on the host does not apply inside the pod, and connection tracking is per namespace.
Expecting a bridge to exist. Many diagrams show one, plenty of plugins do not use one, and looking for a bridge that was never there wastes real time.
Confusing the pod's loopback with the node's. They are different interfaces in different namespaces that happen to share an address.
Treating same node traffic as a special case in the model. Kubernetes makes no distinction. A pod addresses another pod the same way whether it is next door or on another machine, which is the point of the model. What differs is the mechanism underneath, and that is your plugin's business.
A pod is Running and Ready. Its readiness probe passes. Every connection to it from another pod is refused immediately rather than timing out. What is the most likely cause?
A pod is a network namespace with a virtual cable to the host. Get that picture solid and every later mechanism in this course is a modification to one end of it or to what sits between the cables.
Trace a packet from one pod to another on the same node. What does the pod see, what does the host see, and where is the boundary between them?