Operating Systems · Isolation and Namespaces
Network Namespaces and Virtual Ethernet Pairs
How a process gets a private set of interfaces, routes and firewall rules, and how a veth pair connects that private stack to the host without either side sharing a socket table.
A new network namespace begins with a loopback interface and nothing else. No Ethernet device, no route beyond the local subnet, no ARP cache, no listening socket inherited from the parent. The state that a network stack accumulates — the routing table, the neighbour table, the conntrack table, the set of bound ports — lives in the namespace, so two processes in different namespaces can bind port 80 on the same host address without conflict. Connecting such a namespace to anything requires a virtual device, and the device of choice is a pair whose two ends live in different namespaces.
The stack is per-namespace state
struct net holds the routing tables, the device list, the per-protocol state, the netfilter tables, and the socket hash tables. CLONE_NEWNET allocates a fresh one. Because the socket hash lives there, port numbers are namespace-scoped: a listening socket in namespace A occupies (address, port) in A’s table only, and a socket in namespace B can occupy the same pair. The same applies to ephemeral port allocation, which draws from the namespace’s range of 32768 to 60999 by default.
The loopback device exists in every namespace but starts administratively down, with address 127.0.0.1/8 assigned. Nothing in the kernel brings it up automatically, because bringing up an interface is an administrative action and a new namespace is supposed to contain no configuration. The omission is the most common cause of a container that cannot reach its own services: a client connecting to 127.0.0.1 inside such a namespace fails rather than reaching the service that believes it is listening, and the diagnosis is one ip link invocation away.
# In the new namespace, loopback must be enabled explicitly.
ip netns add box
ip -n box link set lo up
ip -n box addr show lo # 127.0.0.1/8, state UNKNOWN
A pair is one device with two ends
A veth pair is created as a single object with two network devices. Frames transmitted on one end are received on the other, inside the receiving end’s namespace, with no intermediate medium and no shared memory beyond the skb itself. The driver is not a tunnel: there is no encapsulation, no extra header, and the MTU of both ends is the same, 1500 bytes by default. What one end sends, the other receives, and the direction is fixed — a frame sent on end A always arrives at end B.
The two ends must be in different namespaces for the pair to be useful. Placing one end in the host namespace and the other in the container namespace gives the container a real Ethernet device with a real link to the host’s stack. The host end is then configured like any other interface: an address, a route, and either a bridge membership or a NAT rule.
ip link add host0 type veth peer name eth0
ip link set eth0 netns box
ip -n box link set eth0 up
ip -n box addr add 10.20.0.2/24 dev eth0
ip -n box route add default via 10.20.0.1
ip addr add 10.20.0.1/24 dev host0
ip link set host0 up
The peer name argument is evaluated in the namespace where the command runs, and the netns move happens afterwards, so the peer is created in the host namespace and then relocated. Once moved, the device belongs to the target namespace and can only be configured from there or through ip -n. Moving a device that is up causes the kernel to bring it down, because the link state of the old namespace no longer applies; the example brings it up after the move for that reason.
Address resolution across the pair
A veth end behaves like an Ethernet port for everything above the driver. ARP requests emitted by the container’s stack travel the pair, arrive on host0, and are answered by the host’s stack if the address is local to it. The neighbour entry is created in the namespace where the answer is received. Nothing about the pair shortcuts this: the host must have the container’s address on an interface in its own namespace, or a proxy ARP entry, or a route that sends the traffic elsewhere.
Two configurations are common, and they differ in where the routing decision happens. With a bridge, both veth host ends are attached to a Linux bridge, which forwards frames between them at layer two; the bridge holds the gateway address and each container has an address on the same subnet. With routing, the host end gets a /31 or a small subnet and the host routes to the container address over that link; the container’s default route points at the host end. The bridge approach costs a forwarding lookup per frame but allows a single subnet to span many containers and supports broadcast, which some protocols need. The routing approach avoids the bridge’s learning table and keeps broadcast confined to the pair, at the cost of one route per container and a requirement that the host have enough addresses.
NAT is a separate decision. Masquerading the container subnet behind the host’s uplink makes outbound connections work without any coordination with the upstream network, and it makes inbound connections impossible without an explicit port forward. That asymmetry is the reason container networks default to it: the failure mode of a missing inbound path is a refused connection, which is easier to diagnose than a routing loop.
Namespace identity and device lifetime
A network namespace is identified by an inode number reachable through /proc/<pid>/ns/net, and the namespace persists while any reference to it exists: a process in it, an open file descriptor for that procfs entry, or a bind mount of the entry. ip netns add creates a namespace with no process in it and keeps it alive by bind-mounting its procfs entry under /run/netns, which is why the named namespace survives between commands.
Device lifetime follows the namespace. Destroying a namespace destroys its devices, which destroys the veth pair, which removes the peer in the other namespace as well — a veth cannot exist with only one end. This is the intended teardown path for a container network: deleting the container’s namespace removes its interface from the host bridge without any explicit cleanup. The reverse does not hold: deleting the host end explicitly also destroys the container end, and the container loses connectivity with no visible change inside its namespace other than a device that disappeared.
The kernel caps the number of network namespaces indirectly through memory and through the per-namespace cost of the netfilter tables, which are allocated on first use. Each namespace with netfilter rules carries its own conntrack table, and conntrack entries are not shared, so a container that opens many connections consumes state charged to its namespace. Whether that state appears in the container’s memory cgroup depends on the kernel version and on how the runtime configured the hierarchy, and a container that opens large numbers of short-lived connections can therefore hold memory that its own memory.current does not show.
What the namespace does not isolate
Sockets are namespace-scoped, but the underlying network is not. A container with a veth can send packets to any address the host can reach, including link-local metadata services and other containers on the same bridge, unless a firewall rule prevents it. The namespace provides separation of the stack, not of the wire, and the wire is shared by construction.
Kernel-level network resources remain global in several places. The sysctl values under net.ipv4 are per-namespace for the parameters that affect the stack, but a number of them are global, and the boundary is not obvious from the file layout. Interface names are per-namespace, so the same name can appear in many namespaces. The routing cache and the neighbour table are per-namespace, while the neighbour table’s garbage collection parameters are partly global.
The most consequential shared resource is the host’s routing and firewall policy. A packet leaving a container passes through the host’s FORWARD chain and its routing table, so a host rule that drops forwarded traffic silently breaks container connectivity, and a host rule that accepts it may expose a container that was assumed to be unreachable. Reading iptables -t nat -L on the host is not optional when debugging a container network; the container’s own rules only govern traffic that stays inside its namespace.
Cross-namespace communication uses the same mechanisms as cross-host communication, which means the tools for one apply to the other. A veth pair with addresses on both ends looks like a point-to-point link; a bridge looks like a switch; a NAT rule looks like a router. This is why a container network can be reasoned about with the same mental model as a small physical network, and why the failure modes are the familiar ones: missing route, wrong mask, ARP not resolving, firewall dropping the return path. The mechanisms described in Mount Namespaces and the Propagation of Mount Events and PID Namespaces and the Special Status of PID 1 cover the other two axes a container needs; the network axis is the one where the boundary is most visibly permeable, because packets by definition leave the namespace they were created in.
References
- network_namespaces(7) — Linux manual page — man7.org
- veth(4) — Linux manual page — man7.org
- ip-link(8) — Linux manual page — man7.org