Cuando un contenedor de Docker arranca, recibe su propia IP, su propia tabla de rutas y su propio stack de red, en milisegundos y sobre el mismo kernel que el resto del sistema. Eso se consigue con dos piezas del kernel: los network namespaces, que aíslan, y los veth pairs, que conectan.
Primero verás de dónde salieron, de OpenVZ a Docker y Kubernetes. Después los montarás a mano en la terminal y verás cómo encaja el bridge.

La pregunta de partida
Como sabes, cuando un contenedor de Docker arranca, Docker le da su propia IP, su propia tabla de rutas y su stack de red completo. Y lo hace en milisegundos, sobre el mismo kernel que el resto del sistema.
¿Cómo lo consigue? Gracias a dos primitivas del kernel que nacieron en un proyecto de contenedores llamado OpenVZ, años antes de que Docker existiera. La lección tiene dos partes: primero un poco de historia para tener contexto, y después una demo para ver cómo funcionan veth y los network namespaces.
2001: una sola red para todos los procesos
Estamos en 2001. Linux está creciendo, pero tiene un problema importante: el kernel tiene una sola tabla de red para todo. Todos los procesos comparten las mismas interfaces, la misma tabla de rutas y el mismo stack de red.
O sea, si dos aplicaciones quieren escuchar en el mismo puerto, hay conflicto. Si dos procesos quieren la misma IP 10.0.0.1, hay conflicto. Solo hay una red para todos.
Si querías correr aplicaciones aisladas, por ejemplo en un hosting compartido, tenías que levantar una máquina virtual completa, con un kernel entero por cada entorno. Era costoso y no escalaba.
SWsoft y OpenVZ: aislar dentro del kernel que ya tienes
Hay una empresa que lo ve de otra forma: SWsoft, una empresa de virtualización fundada en 1997. Y se hacen una pregunta clave: ¿y si, en lugar de levantar un kernel nuevo, aíslas los recursos dentro del kernel que ya tienes?
En 2005, SWsoft libera como open source su tecnología de contenedores: OpenVZ. Docker y Kubernetes todavía no existen, pero OpenVZ ya está corriendo contenedores en producción, en servidores de hosting de todo el mundo.
El problema es que, para hacerlo bien, necesitan que el kernel oficial soporte aislamiento de red real.
Eric W. Biederman y los network namespaces
Uno de los que empuja esto es Eric W. Biederman, un desarrollador independiente del kernel que lleva años dándole vueltas a cómo aislar recursos dentro del kernel.
En 2006 manda una propuesta grande al kernel: namespaces para aislar la red, los procesos, los usuarios y los nombres de host. La comunidad del kernel la recibe con escepticismo, y Biederman se pasa dos años mandando revisiones y ajustando el diseño.
En enero de 2008 sale Linux 2.6.24 con los network namespaces incluidos. A partir de ahí, un proceso puede tener su propio stack de red privado, separado del resto del sistema.
Pero un namespace aislado no puede hablar con nadie. Necesitas algo que lo conecte con el exterior.
Pavel Emelyanov y el driver veth
En ese mismo release, Pavel Emelyanov, ingeniero de SWsoft en el equipo de OpenVZ, mete un driver nuevo en el kernel: el driver veth.
Un veth son dos interfaces virtuales conectadas entre sí, como los dos extremos de un cable Ethernet. Lo que entra por un lado sale por el otro.
commit e314dbdc — Author: Pavel Emelyanov <xemul@openvz.org>
Date: Thu Oct 11 2007
[NET]: Virtual ethernet device driver.
Los namespaces y el veth son la base de la red de contenedores. Es lo que corre hoy dentro de Docker y dentro de Kubernetes.
De LXC a Docker y Kubernetes
Fuera del mundo del kernel, todo esto pasa desapercibido. OpenVZ usa estas primitivas en sus propios servidores, pero casi nadie fuera de ese entorno las conoce.
Ese mismo año, 2008, aparece LXC (Linux Containers). LXC coge los namespaces y el veth y los mete en una CLI para crear contenedores. Por primera vez puedes levantar un contenedor sin tocar el código del kernel.
Pero la CLI es compleja, tienes que configurar muchas cosas a mano y la curva de aprendizaje es alta. Sigue siendo cosa de especialistas.
Eso cambia en marzo de 2013. Solomon Hykes está en el escenario de PyCon, la conferencia anual de Python, en Santa Clara, California. Hace una demo de cinco minutos de una herramienta interna que estaban desarrollando en dotCloud, y la llaman Docker.
En esa demo, Docker arranca un contenedor, le asigna un namespace de red, crea un veth pair y conecta el contenedor al host, todo en milisegundos. Por debajo es lo mismo de antes, namespaces y veth pairs, pero ahora cualquiera puede usarlo con un solo comando. En menos de un año, Docker tiene miles de estrellas en GitHub.
Un año después, en 2014, Beda, Burns y McLuckie publican Kubernetes, y de repente todo el mundo necesita orquestar contenedores.
2002 — Primer namespace en Linux (mount namespace, Al Viro)
2006 — Biederman propone network namespaces completos
2007 — Commit del driver veth de Emelyanov (incluido en Linux 2.6.24)
2008 — Linux 2.6.24 publicado: namespaces + veth en el kernel oficial
2008 — LXC: primera herramienta usable sobre estas primitivas
2013 — Docker: democratización de los contenedores
2014 — Kubernetes: orquestación a escala
2016 — Cilium: eBPF en el otro extremo del veth
Qué es un network namespace
Ya sabemos de dónde viene esto. Ahora vamos a ver qué es un network namespace por dentro.
Un network namespace es una copia aislada del stack de red del kernel. Cuando creas uno, dentro tiene de todo: tabla de rutas, interfaces, tabla ARP, reglas de iptables y hasta su propio loopback.
Los procesos de dentro solo ven lo suyo y no saben nada del host. Pero un namespace aislado no puede hablar con nadie, y para eso existe el veth pair.
Qué es un veth pair
Un veth pair son dos interfaces de red que el kernel conecta entre sí. Lo que entra por una sale por la otra. El kernel las vincula al crearlas y el paquete pasa directamente de una a la otra, y por eso la latencia es de centésimas de milisegundo.
Ahora viene la parte que lo conecta con los namespaces. Una interfaz de red solo puede pertenecer a un namespace, y puedes mover un extremo del veth a otro namespace.
Los dos extremos siguen vinculados aunque estén en namespaces distintos, porque el kernel mantiene esa conexión. El resultado es que el namespace tiene su stack completamente aislado, con una sola salida al exterior, que es el veth. Solo pasa lo que tú configuras.
Host namespace Container namespace
┌────────────────┐ ┌────────────────────┐
│ │ │ │
│ veth-A ◄───┼──────┼───► veth-B │
│ │ │ │
└────────────────┘ └────────────────────┘
En resumen, el namespace aísla y el veth conecta. Vamos a construirlo en la terminal antes de añadir nada más.
Demo: dos namespaces conectados con un veth
Estado inicial
Empezamos mirando qué tiene el host. Primero listo todas las interfaces de red que existen ahora mismo:
ip link show
Tenemos lo y eth0, nada más. Hay un solo stack de red y todo está compartido.
Ahora listo los namespaces de red que hay en el sistema:
ip netns list
La lista sale vacía. Solo existe el namespace raíz del host, que es implícito y no aparece aquí.
Crear los namespaces
Creo dos namespaces, que voy a llamar ns-rojo y ns-azul:
sudo ip netns add ns-rojo
sudo ip netns add ns-azul
Los listo para confirmar que el kernel los ha registrado:
ip netns list
Ahí están. Ahora miro dentro de ns-rojo para ver en qué estado está:
sudo ip netns exec ns-rojo ip link show
Solo hay un loopback, y está en DOWN. El namespace existe y tiene su propio stack de red, pero no tiene ninguna interfaz real y no puede comunicarse con nadie.
Crear el veth pair
Ahora creo el cable. Los dos extremos se crean en una sola llamada y se llaman veth-rojo y veth-azul:
sudo ip link add veth-rojo type veth peer name veth-azul
Listo las interfaces de tipo veth para ver que los dos extremos están ahí:
ip link show type veth
Los dos extremos existen y, en este momento, están los dos en el host. Fíjate en la @: veth-azul@veth-rojo significa «veth-azul, cuyo par es veth-rojo». El kernel ya los tiene vinculados.
Mover cada extremo a su namespace
Ahora muevo cada extremo al namespace que le corresponde. Cuando ejecuto esto, la interfaz desaparece del host, porque no se copia, se mueve:
sudo ip link set veth-rojo netns ns-rojo
sudo ip link set veth-azul netns ns-azul
Compruebo que el host ya no los ve:
ip link show type veth
La lista sale vacía, así que han desaparecido del host. Ahora miro dentro de ns-rojo para confirmar que el extremo está ahí:
sudo ip netns exec ns-rojo ip link show
Ahí está veth-rojo. Fíjate en el @if4: es el kernel diciéndote que el par de esta interfaz vive en el índice 4 de otro namespace. El vínculo entre los dos extremos sigue intacto aunque estén en stacks de red distintos.
Asignar IPs y levantar las interfaces
Configuro cada namespace en tres pasos: asigno la IP, levanto la interfaz y levanto el loopback.
sudo ip netns exec ns-rojo ip addr add 10.10.0.1/24 dev veth-rojo
sudo ip netns exec ns-rojo ip link set veth-rojo up
sudo ip netns exec ns-rojo ip link set lo up
Hago lo mismo en ns-azul, con la IP .2:
sudo ip netns exec ns-azul ip addr add 10.10.0.2/24 dev veth-azul
sudo ip netns exec ns-azul ip link set veth-azul up
sudo ip netns exec ns-azul ip link set lo up
Verifico el estado de ns-rojo:
sudo ip netns exec ns-rojo ip addr show
Solo ve lo y veth-rojo con su IP. No aparece el eth0 del host ni veth-azul. El aislamiento es real.
Comprobar la conexión con ping
Hago ping desde ns-rojo hacia la IP de veth-azul:
sudo ip netns exec ns-rojo ping -c 3 10.10.0.2
Salen 3 paquetes, con 0 pérdidas y 0.04 milisegundos. Son dos stacks de red aislados, conectados por un cable virtual.
Ver el tráfico con tcpdump
Para ver el tráfico en tiempo real desde dentro de ns-azul, abro una segunda terminal y arranco el sniffer dentro de ese namespace, filtrando solo ICMP:
sudo ip netns exec ns-azul tcpdump -i veth-azul icmp
Mientras escucha, vuelvo a la primera terminal y lanzo el ping:
sudo ip netns exec ns-rojo ping -c 3 10.10.0.2
En la segunda terminal los paquetes aparecen en tiempo real. El echo request sale de ns-rojo, cruza el cable y llega a veth-azul en ns-azul. El echo reply vuelve por el mismo camino.
Si pusieras este sniffer en el host, no verías nada, porque los extremos ya no están ahí.
La tabla de rutas del namespace
Antes de pasar al bridge, miro la tabla de rutas de ns-rojo:
sudo ip netns exec ns-rojo ip route show
Tiene una sola entrada: la subred 10.10.0.0/24 por veth-rojo. El namespace solo sabe llegar a su subred local, y no tiene ruta al host ni a internet. Para salir fuera haría falta una ruta por defecto, que es justo lo que añade Docker cuando levanta un contenedor.
Cuando termines, puedes borrar los dos namespaces:
sudo ip netns del ns-rojo
sudo ip netns del ns-azul
Cuando hay más de dos contenedores: el bridge
Lo que hemos construido funciona para dos namespaces que hablan directamente. Pero si tienes diez contenedores que necesitan comunicarse entre sí, esta configuración no escala. Necesitas un punto central que los conecte a todos, y eso es el bridge.
ns-rojo ns-azul
┌─────────────┐ ┌─────────────┐
│ veth-rojo │ │ veth-azul │
│ 10.20.0.1 │ │ 10.20.0.2 │
└──────┬──────┘ └──────┬──────┘
│ │
veth-rojo-h veth-azul-h
(host) (host)
│ │
└────────────┬────────────────────┘
br-demo
10.20.0.254
│
eth0
Un Linux bridge es un switch Ethernet en software. Mantiene una tabla de MACs, es decir, aprende qué dirección MAC está detrás de cada uno de sus puertos. Cuando le llega un frame, lo manda directamente por el puerto correcto, sin hacer broadcast a todos.
Sus puertos son simplemente interfaces de red. En este caso, los extremos de los veth pairs que se quedan en el host.
La diferencia con la demo está en dónde queda cada extremo. Antes, los dos extremos del cable estaban dentro de los namespaces y el host no tenía ninguno. Aquí, un extremo entra al namespace y el otro se queda en el host como puerto del bridge.
Es lo que hace Docker. docker0 es una interfaz del host como cualquier otra, con su propia IP: 172.17.0.1. El host forma parte de esa subred, así que si desde tu terminal haces ping a la IP de un contenedor, por ejemplo 172.17.0.2, el paquete sale por docker0, cruza el veth y llega al contenedor.
El contenedor tiene una ruta por defecto que apunta al bridge. Todo lo que no sabe entregar lo manda a ese default gateway, y el host decide si lo reenvía a otro contenedor o si lo saca a internet con NAT a través de eth0.
Lo que hemos visto
- Hasta 2008, Linux tenía una sola red para todos los procesos, y aislar aplicaciones obligaba a levantar una máquina virtual completa.
- Los network namespaces (Biederman) y el driver veth (Emelyanov, de OpenVZ) entraron en Linux 2.6.24, y LXC, Docker y Kubernetes se construyeron encima.
- Un network namespace es una copia aislada del stack de red, y un veth pair son dos interfaces unidas por el kernel cuyos extremos puedes mover a namespaces distintos.
- Con
ip netnseip linkpuedes crear dos namespaces, conectarlos con un veth y hacer ping entre ellos. - Para conectar muchos contenedores, Docker deja un extremo de cada veth en el host como puerto del bridge
docker0, que reenvía el tráfico o lo saca a internet con NAT.