Containersnetwork namespacenetnsvethlinuxcontainer

Network Namespaces

A Linux network namespace is a private copy of the whole network stack — interfaces, addresses, routes, firewall rules, ports — so two containers can each bind :80 and never meet; a veth pair is the virtual cable that connects a namespace back to the host.

Linux
▶ Interactive
Progress

The problem

Two containers on the same host both run a web server on port 80. bind() on the same port twice on one kernel fails with EADDRINUSE. Yet both start, both serve, and neither sees the other. What did the kernel give each of them that makes the second bind() succeed?

One kernel, several network stacks

Linux

Ports belong to a network stack, not to a machine. In Linux the network stack — its interfaces, IP addresses, routing table, conntrack and firewall rules, socket table and net.* sysctls — is one instance of a network namespace, and the kernel can hold many. A process belongs to exactly one; every socket it creates is created in that namespace and is invisible to sockets in others. Two namespaces each have their own port 80, their own lo with 127.0.0.1, their own default route. bind(:80) in namespace A and bind(:80) in namespace B do not conflict because they are entries in two different tables.

This is the same mechanism that gives a container its own PID 1, its own mount table and its own hostname — see Process Isolation: One Kernel, Many PID 1s for the namespace family — applied to the network. A container is not a VM with a virtual NIC and its own kernel; it is a process group that the shared kernel shows a private view of the network. That is why a container has no network devices at all until someone creates some for it, and why docker run --network none is a real, working configuration: a namespace with only lo.

veth: the virtual cable

Linux

A new namespace has a loopback interface and nothing else, so it cannot talk to anything. To connect it, the kernel offers a veth pair: two virtual Ethernet interfaces created together, where a frame sent into one comes out of the other, like the two ends of a patch cable. Put one end in the container’s namespace (it becomes its eth0) and leave the other in the host namespace (veth3a1b…). Give the container end an address, add a default route through it, and the container has a link to the host.

The host end is usually plugged into a bridge — a software switch, docker0 — so that many containers’ veth ends share one L2 segment and can reach each other by address, exactly like ports on a physical switch (Ethernet: Delivery on One Local Network). Frames that need to leave the host go bridge → host routing table → physical NIC, with NAT on the way (Container Networking). From inside, the container sees a normal eth0, a normal gateway, ARP, everything; the "network" behind it is entirely in the host kernel.

Container A and B to the host
  1. Container A processbinds :80 in netns A
  2. netns A: eth0 (veth end)172.17.0.2/16, default via 172.17.0.1
  3. host: vethA ↔ bridge docker0 ↔ vethBa software switch in the host namespace, address 172.17.0.1
  4. netns B: eth0 (veth end)172.17.0.3/16 — also binds :80; different socket table
  5. host routing table + NATbridge traffic leaving the host is masqueraded to the host IP
  6. host NIC eth0the only interface the physical network ever sees

Doing it by hand

Linux

Everything Docker or a CNI plugin does at this layer is a handful of ip commands. Building a namespace by hand once removes most of the mystery: you can see the veth pair appear in both namespaces, watch the second bind(:80) succeed, and observe that ss inside the namespace shows only its own sockets.

Note that ip netns shows only namespaces it created, because it names them with a bind mount under /run/netns. Docker creates namespaces without names; to inspect a container’s use nsenter -t <pid> -n ss -ltn with the container’s PID, or symlink /proc/<pid>/ns/net into /run/netns so the ip netns tooling can find it.

A namespace with a veth pair, bridged to the host (Linux; needs root)
# a namespace, a veth pair, one end moved into it
ip netns add blue
ip link add veth-blue type veth peer name eth0 netns blue
ip link set veth-blue up

# address and route inside; loopback up (it is down by default in a new namespace)
ip -n blue link set lo up
ip -n blue link set eth0 up
ip -n blue addr add 172.17.0.2/16 dev eth0
ip -n blue route add default via 172.17.0.1

# host end onto a bridge that owns the gateway address
ip link add br0 type bridge && ip link set br0 up
ip addr add 172.17.0.1/16 dev br0
ip link set veth-blue master br0

# separate stacks: both listeners on :80, each only sees its own
ip netns exec blue python3 -m http.server 80 &
python3 -m http.server 80 &            # host namespace — no EADDRINUSE
ip netns exec blue ss -ltn             # shows only the namespace’s listener
ip netns exec blue ip route            # its own routing table
ip netns exec blue nft list ruleset    # its own (empty) firewall

What is and is not isolated

Linux

Isolated per namespace: interfaces and their addresses, the routing tables and policy rules, conntrack and the nftables/iptables rule sets, the socket and port tables, the ARP/neighbour cache, and network sysctls such as net.ipv4.ip_forward and the socket buffer limits. A container that sets net.core.somaxconn sets it for itself. A container that runs iptables -F flushes only its own rules.

Not isolated: the kernel itself, the physical NIC and its driver queues, the total number of conntrack entries the kernel will allocate, and CPU time spent in the network stack. A container that saturates the NIC starves the others; a namespace with a huge conntrack table competes for the same memory. Namespaces partition names and tables, not hardware — the same limit that Containers Are Processes With the Kernel’s View Narrowed describes for the OS as a whole.

  • Per namespace: interfaces, addresses, routes, firewall rules, conntrack, ports/sockets, neighbour cache, net.* sysctls.
  • Shared: the kernel, the NIC, driver queues, kernel memory for network state, CPU spent in softirq.
  • A namespace with only lo is a fully functional stack that reaches nothing — --network none.

Key points

  • A network namespace is a private instance of the Linux network stack: its own interfaces, routes, firewall rules, conntrack and port table.
  • Two containers can both bind :80 because each bind() happens in a different socket table.
  • A new namespace has only lo; a veth pair is the virtual cable that gives it a link to the host.
  • Host-side veth ends plug into a bridge (docker0), a software switch; traffic leaves the host through the host routing table and NAT.
  • Everything a container runtime does at this layer is ip link, ip addr, ip route and a bridge; build one by hand once.
  • Namespaces isolate tables and names, not the NIC, the kernel or its memory.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why a whole network stack per container rather than just separate ports?

Because applications assume they own the stack: they bind well-known ports, read the routing table, expect 127.0.0.1 to be private. Giving each container a full private view lets unmodified software run as if alone on a machine.

Why a veth pair instead of giving the container the real NIC?

The NIC is one device and there are many containers. A pair of virtual interfaces is a cable the kernel can manufacture on demand, one per container, all plugging into one software switch.

Why does `docker run --network none` still have `127.0.0.1`?

Loopback is part of every stack instance; the namespace exists and works, it simply has no cable to anything else.

Network namespaces

Network namespaces
Two containers, each with its own eth0, routing table and port table — both on :80. Click a box for the view from inside.
Trace
Linux
host namespace (click)container A (netns)eth0 172.17.0.2 · :80container B (netns)eth0 172.17.0.3 · :80veth-aveth-bdocker0 bridge172.17.0.1/16 · learns MACs like a switchrouting + nftables NATip_forward=1 · masquerade / dnateth0192.0.2.10↓ internet
Packet at this hop
172.17.0.2:41000 → 93.184.216.34:443
App in A sends. A's socket API, A's routing table: default via 172.17.0.1 dev eth0. The container thinks it has a normal machine.
Inside container A — ip addr
lo:   127.0.0.1/8
eth0: 172.17.0.2/16   (one end of veth-a)
ip route
default via 172.17.0.1 dev eth0
172.17.0.0/16 dev eth0 src 172.17.0.2
ss -ltn
LISTEN 0.0.0.0:80   nginx
Each namespace is a separate copy of the kernel's network stack: interfaces, routes, iptables/nftables, conntrack and the bind table. Two processes can both own 0.0.0.0:80 because "0.0.0.0" means every address in my namespace, and A's and B's namespaces share none. nsenter -t PID -n ss -ltn or docker exec is how you look inside.
1/5 · App in A sends

How it fails

What the failure looks like from inside real software.

  • A debugging container attached to a pod with --network container:<id> cannot reach a service that the target container reaches over localhost: it is in a different namespace than assumed, or the target listens only on the pod interface.
  • ip netns list is empty on a Docker host that runs twenty containers: Docker does not register named namespaces; use nsenter -t <pid> -n.
  • Container starts but has no connectivity: the veth pair was created but the host end never joined the bridge, or lo was left down inside the namespace.
  • Firewall rule added on the host does not affect a container’s inbound traffic: the container has its own rule set; the host’s INPUT chain never sees traffic addressed to the container interface (it is forwarded, not delivered).
  • One container’s traffic storm slows every container: the NIC and softirq CPU are shared; namespaces partition addresses, not bandwidth.