En la lección 03-05 usaste las redes de Docker como una caja negra: creabas una red, conectabas contenedores y los nombres resolvían. Funcionaba, pero no sabías por qué. Esta lección abre esa caja: puentes de Linux, pares veth, reglas de iptables, un servidor DNS oculto en 127.0.0.11 y drivers que dan a un contenedor una IP propia en tu LAN física.

Todo lo que sigue se demuestra sobre la plataforma Aurora Libros en marcha. Los comandos del host asumen Linux; si usas Docker Desktop, se ejecutan dentro de la máquina virtual (lección 05-07).

Contenido

  1. Anatomía de una red bridge: puentes de Linux
  2. Los pares veth y cómo emparejar los extremos
  3. El camino de un paquete, paso a paso
  4. iptables: las cadenas que crea Docker
  5. NAT de salida y DNAT de publicación
  6. El cortafuegos del host, saltado (aviso de seguridad)
  7. DOCKER-USER: recuperar el control
  8. El DNS embebido en 127.0.0.11
  9. Direccionamiento: subredes, rangos, IP fija y MTU
  10. Los drivers host y none en profundidad
  11. macvlan: una IP propia en la LAN física
  12. ipvlan en modo L2 y L3
  13. Tabla comparativa de los seis drivers
  14. IPv6 en Docker
  15. Diagnóstico real: tcpdump y nsenter

  1. Anatomía de una red bridge: puentes de Linux

Una red bridge de Docker es un puente de software del kernel de Linux: un conmutador Ethernet virtual. La red por defecto usa el puente docker0; cada red de usuario crea uno llamado br-<12 primeros caracteres del ID de red>.

docker network ls --filter driver=bridge --format "{{.ID}}\t{{.Name}}"
ip -brief link show type bridge
9f3a1c7d5e2b   aurora-libros_frontal
c81be4f0a7d9   aurora-libros_trasera
docker0          UP  02:42:8e:1c:44:a1 <BROADCAST,MULTICAST,UP,LOWER_UP>
br-9f3a1c7d5e2b  UP  02:42:3b:9c:10:7e <BROADCAST,MULTICAST,UP,LOWER_UP>
br-c81be4f0a7d9  UP  02:42:af:22:d5:03 <BROADCAST,MULTICAST,UP,LOWER_UP>

La correspondencia es directa y comprobable: los doce caracteres tras br- son el principio del ID de la red. El puente tiene su propia dirección IP en el host, que es la puerta de enlace de los contenedores de esa red:

ip -brief addr show br-9f3a1c7d5e2b
docker network inspect aurora-libros_frontal --format '{{(index .IPAM.Config 0).Subnet}} gw={{(index .IPAM.Config 0).Gateway}}'
br-9f3a1c7d5e2b UP 172.21.0.1/16
172.21.0.0/16 gw=172.21.0.1

Y bridge link show master br-9f3a1c7d5e2b lista los interfaces enchufados a ese puente: uno por cada contenedor conectado a la red.

  1. Los pares veth y cómo emparejar los extremos

Un veth (virtual Ethernet) es un par de interfaces conectados por un cable virtual: lo que entra por un extremo sale por el otro. Docker crea uno por cada conexión contenedor-red: un extremo se queda en el host y se enchufa al puente; el otro se mueve dentro del contenedor y se renombra eth0. El emparejamiento no es evidente, porque los nombres son aleatorios; la clave está en el índice de interfaz, ya que cada extremo apunta al índice del otro mediante iflink.

docker compose exec aurora-api cat /sys/class/net/eth0/iflink   # -> 7
ip link show | grep '^7:'                                       # ¿qué interfaz es la 7?
7: veth3a1f9c2@if6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 master br-9f3a1c7d5e2b

Ya tienes el par: eth0 de aurora-apiveth3a1f9c2 en el host. La notación @if6 del propio nombre confirma la simetría: el extremo del host apunta al índice 6, que es el eth0 del contenedor en su propio espacio de nombres de red.

Con el par identificado, puedes medir el tráfico de un contenedor concreto desde el host, sin entrar en él:

ethtool -S veth3a1f9c2 | head -5
NIC statistics:
     peer_ifindex: 6
     rx_packets: 18422
     tx_packets: 21095

Fíjate en peer_ifindex: es la forma más directa de confirmar el emparejamiento. Y ojo con la inversión de sentido: lo que el contenedor envía es rx en el veth del host, porque el paquete entra al host por ese extremo.

  1. El camino de un paquete, paso a paso

flowchart LR
  subgraph NSAPI["Namespace de red de aurora-api"]
    P["proceso node<br/>172.21.0.5"] --> E0["eth0 (if6)"]
  end
  E0 -->|"cable veth"| V["veth3a1f9c2 (if7)"]
  subgraph HOST["Host"]
    V --> BR["br-9f3a1c7d5e2b<br/>172.21.0.1"]
    BR --> IPT["iptables: nat + filter"]
    IPT --> ETH["eth0 del host"]
  end
  BR -->|"mismo puente:<br/>conmutación directa"| V2["veth5d80a3f"]
  V2 --> DB["aurora-db 172.22.0.3"]
  ETH --> NET(("Internet"))

Dos caminos muy distintos conviven aquí:

Destino Recorrido Coste
Otro contenedor de la misma red eth0veth → puente → vetheth0 Conmutación en el kernel, sin NAT
Otra red o Internet eth0veth → puente → enrutadoiptables (MASQUERADE) → eth0 del host NAT y seguimiento de conexiones
Desde fuera hacia un puerto publicado eth0 del host → iptables (DNAT) → puente → vetheth0 NAT de destino

Que la comunicación aurora-apiaurora-db sea pura conmutación explica por qué su rendimiento es prácticamente el de la red del host: no hay traducción de direcciones en ese camino.

  1. iptables: las cadenas que crea Docker

Al arrancar, dockerd instala un conjunto de cadenas propias. Conocerlas por nombre te ahorra horas de depuración:

Cadena Tabla Función
DOCKER nat y filter Reglas DNAT de los puertos publicados y su permiso de reenvío
DOCKER-USER filter Vacía y reservada para ti; se evalúa antes que DOCKER
DOCKER-ISOLATION-STAGE-1 filter Detecta tráfico que sale de un puente hacia otro
DOCKER-ISOLATION-STAGE-2 filter Lo descarta: es lo que aísla las redes entre sí
DOCKER-INGRESS nat y filter Solo con Swarm (lección 06-03)
sudo iptables -t filter -L DOCKER-ISOLATION-STAGE-2 -n --line-numbers
num  target     prot opt source               destination
1    DROP       all  --  0.0.0.0/0            0.0.0.0/0
2    RETURN     all  --  0.0.0.0/0            0.0.0.0/0

Ahí está, en dos líneas, el aislamiento que comprobaste en la lección 04-04 cuando aurora-web no podía llegar a aurora-db: un paquete que sale de un puente e intenta entrar en otro cae en el DROP.

  1. NAT de salida y DNAT de publicación

sudo iptables -t nat -L -n
Chain PREROUTING (policy ACCEPT)
DOCKER      all  --  0.0.0.0/0        0.0.0.0/0     ADDRTYPE match dst-type LOCAL
Chain POSTROUTING (policy ACCEPT)
MASQUERADE  all  --  172.21.0.0/16    0.0.0.0/0
MASQUERADE  all  --  172.22.0.0/16    0.0.0.0/0
MASQUERADE  tcp  --  172.21.0.2       172.21.0.2    tcp dpt:80
Chain DOCKER (2 references)
DNAT        tcp  --  0.0.0.0/0        0.0.0.0/0     tcp dpt:8080 to:172.21.0.2:80

Tres reglas, tres mecanismos:

  • MASQUERADE de subred: todo lo que sale de 172.21.0.0/16 hacia fuera se reescribe con la IP del host. Es la razón de que un contenedor pueda descargar de Internet sin tener IP pública.
  • DNAT de publicación: el -p 8080:80 de aurora-web se traduce en esta regla. Cualquier paquete que llegue al puerto 8080 de cualquier dirección local se redirige a 172.21.0.2:80.
  • MASQUERADE con origen y destino iguales: el llamado hairpin NAT, que permite que un contenedor se alcance a sí mismo a través del puerto publicado del host.

Comprueba la correspondencia con la publicación real:

sudo iptables -t nat -S DOCKER | grep 8080
-A DOCKER ! -i br-9f3a1c7d5e2b -p tcp -m tcp --dport 8080 -j DNAT --to-destination 172.21.0.2:80

Si publicas restringiendo la interfaz —"127.0.0.1:8080:80"—, la regla cambia y añade -d 127.0.0.1. Esa diferencia de una palabra es la que separa "accesible desde mi portátil" de "accesible desde toda la red de la oficina".

  1. El cortafuegos del host, saltado (aviso de seguridad)

Este es el punto más importante de la lección y el que más sorpresas desagradables causa en producción.

Cuando publicas un puerto, Docker inserta su DNAT en PREROUTING, que se evalúa antes que las reglas de la cadena INPUT donde ufw o firewalld ponen las suyas. El tráfico redirigido pasa por FORWARD, no por INPUT. Resultado: tu cortafuegos no ve ese tráfico y no lo bloquea.

sudo ufw status | head -4
docker run -d --name expuesto -p 5000:80 nginx:alpine
curl -s -o /dev/null -w '%{http_code}\n' http://<IP-del-host>:5000/
Status: active
22/tcp                     ALLOW       Anywhere
200

El cortafuegos dice que solo el 22 está abierto. El puerto 5000 responde igualmente desde cualquier máquina de la red. Servidores enteros de PostgreSQL y Redis han acabado en Internet exactamente así.

Las tres defensas, de más simple a más completa:

Defensa Cómo Alcance
Publicar solo en local "127.0.0.1:5432:5432" Impide el acceso remoto de raíz. La primera opción siempre
No publicar Usar redes internas y docker compose exec Lo que hace aurora-db desde la lección 04-04
Reglas en DOCKER-USER Filtrar el tráfico reenviado Control fino por origen

Advertencia de seguridad. La exposición de puertos y las reglas de cortafuegos de un servidor deben revisarse con el responsable de seguridad o de compliance de tu organización. Lo que aquí se muestra explica el mecanismo; la política concreta —qué se expone, a quién y con qué controles compensatorios— no la decide quien escribe el compose.yaml en solitario.

  1. DOCKER-USER: recuperar el control

Docker nunca toca DOCKER-USER y la evalúa antes que sus propias reglas. Es el lugar previsto para tus políticas:

# Permitir solo desde la red corporativa; descartar el resto
sudo iptables -I DOCKER-USER -i eth0 -j DROP
sudo iptables -I DOCKER-USER -i eth0 -s 10.0.0.0/8 -j RETURN
sudo iptables -L DOCKER-USER -n --line-numbers
num  target     prot opt source               destination
1    RETURN     all  --  10.0.0.0/8           0.0.0.0/0
2    DROP       all  --  0.0.0.0/0            0.0.0.0/0

Dos advertencias imprescindibles. La primera: el orden importa, -I inserta al principio, así que la regla permisiva debe insertarse en último lugar para acabar arriba. La segunda: estas reglas no sobreviven a un reinicio; hay que persistirlas con iptables-persistent, nftables o la herramienta de tu distribución. Y si piensas desactivar la manipulación de iptables por completo ("iptables": false en /etc/docker/daemon.json), asume que pierdes la salida a Internet de los contenedores y la publicación de puertos hasta que escribas tú todas las reglas: rara vez compensa.

  1. El DNS embebido en 127.0.0.11

En una red de usuario, los nombres de servicio resuelven porque Docker ejecuta un servidor DNS por contenedor escuchando en 127.0.0.11:53.

docker compose exec aurora-api cat /etc/resolv.conf
docker compose exec aurora-api getent hosts aurora-db
nameserver 127.0.0.11
options ndots:0
172.22.0.3   aurora-db

Esa dirección de loopback no existe en ninguna interfaz visible. El truco: dentro del espacio de nombres del contenedor hay una regla DNAT (-A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp --dport 53 -j DNAT --to-destination 127.0.0.11:38765) que redirige el tráfico a un puerto alto donde escucha un proceso del daemon.

El algoritmo de resolución, en orden:

  1. ¿Es un nombre de servicio o alias de una red que compartimos? → responde el daemon con la IP del contenedor.
  2. ¿Es un contenedor de la misma red por su nombre o container_name? → igual.
  3. Si no, reenvía a los servidores DNS del host (o a los de dns: del servicio).

Puntos que conviene tener claros:

  • La resolución está acotada a las redes compartidas. aurora-web no resuelve aurora-db porque no comparten red: no es un fallo de DNS, es el diseño.
  • Con --scale, un mismo nombre devuelve varias direcciones en orden rotatorio: un balanceo rudimentario del lado del cliente. Compruébalo con docker compose up -d --scale aurora-api=3 seguido de getent hosts aurora-api.
  • options ndots:0 evita que se añadan sufijos de búsqueda y ahorra consultas fallidas.
  • Muchas librerías cachean la resolución. Si un contenedor se recrea y cambia de IP, una aplicación que resolvió una vez puede quedarse hablando con una IP muerta. Es un motivo real para reiniciar clientes tras recrear un servicio.

  1. Direccionamiento: subredes, rangos, IP fija y MTU

Por defecto Docker elige subredes de 172.17.0.0/16 en adelante, lo que choca con muchas VPN corporativas. Puedes fijarlas:

docker network create --driver bridge \
  --subnet 10.80.0.0/24 --gateway 10.80.0.1 --ip-range 10.80.0.128/25 \
  --aux-address "reservada-nas=10.80.0.20" \
  --opt com.docker.network.driver.mtu=1450 \
  --opt com.docker.network.bridge.name=br-aurora \
  aurora-fija
Opción Qué hace
--subnet Rango completo de la red
--gateway IP del puente en el host
--ip-range Subconjunto del que Docker asigna automáticamente; el resto queda libre para IP fijas
--aux-address Direcciones que Docker no debe asignar (equipos reales dentro del rango)
mtu Tamaño máximo de trama; bajarlo evita fragmentación tras VPN o túneles
bridge.name Nombre legible del puente en lugar de br-<id>

Asignar una IP fija exige que la red tenga subred propia: docker run -d --network aurora-fija --ip 10.80.0.10 nginx:alpine. En Compose se declara con ipv4_address dentro de networks. Aun así, la buena práctica sigue siendo depender de los nombres DNS y no de las IP: las IP fijas se reservan para casos donde algo externo (una regla de cortafuegos, una licencia, un equipo antiguo) exige una dirección estable.

El valor por defecto de la MTU se define en /etc/docker/daemon.json con "mtu": 1450. El síntoma típico de una MTU mal ajustada es cruel: las conexiones se establecen y las peticiones pequeñas funcionan, pero las respuestas grandes se quedan colgadas.

  1. Los drivers host y none en profundidad

host elimina el espacio de nombres de red: el contenedor usa directamente la pila del host.

docker run -d --name api-host --network host auroralibros/aurora-api:1.2.0
docker exec api-host ip -brief addr | head -2 && ss -tlnp | grep 3000
lo     UNKNOWN  127.0.0.1/8
eth0   UP       192.168.1.42/24
LISTEN 0  511  *:3000  users:(("node",pid=48122,fd=21))

El proceso escucha directamente en el puerto 3000 del host. Lo que ganas y lo que pierdes:

Ganas Pierdes
Sin NAT: menor latencia y más rendimiento en cargas de muchos paquetes pequeños Aislamiento de red: cero
Acceso a interfaces reales (útil en monitorización, VPN, DHCP) Resolución por nombre de servicio: no existe
Sin límite práctico de puertos publicados Colisiones de puertos entre contenedores
Multicast y protocolos que NAT rompe -p se ignora con un aviso

Casos legítimos: agentes de monitorización (node-exporter, lección 05-06), balanceadores de muy alto rendimiento y herramientas de diagnóstico. Para aurora-api sería un error: perdería el aislamiento sin ganar nada apreciable.

none deja al contenedor únicamente con lo: docker run --rm --network none alpine:3 ping -c1 -W1 8.8.8.8 responde Network is unreachable. Es la opción correcta para procesos de cálculo puro o para tratar ficheros de origen dudoso: si no hay red, no hay exfiltración.

  1. macvlan: una IP propia en la LAN física

macvlan da al contenedor su propia dirección MAC en la red física. Para el resto de la oficina, el contenedor es un equipo más: obtiene una IP del rango real y no necesita NAT ni publicación de puertos.

# 1) Averigua la interfaz física y la subred reales
ip -brief addr show eth0

# 2) Crea la red macvlan sobre esa interfaz
docker network create -d macvlan \
  --subnet 192.168.1.0/24 \
  --gateway 192.168.1.1 \
  --ip-range 192.168.1.240/28 \
  -o parent=eth0 \
  lan-oficina

# 3) Un contenedor con IP de la LAN
docker run -d --name web-lan --network lan-oficina --ip 192.168.1.241 nginx:alpine
docker exec web-lan ip -brief addr show eth0
eth0   UP   192.168.1.241/24

Desde otro equipo de la oficina, http://192.168.1.241/ responde sin haber publicado ningún puerto.

Las tres trampas, en orden de frecuencia:

  1. El host no puede hablar con sus propios contenedores macvlan. No es un fallo: el kernel impide el tráfico entre la interfaz padre y sus interfaces macvlan hijas. La solución es crear un interfaz macvlan adicional en el host y enrutar hacia él:
sudo ip link add macvlan-host link eth0 type macvlan mode bridge
sudo ip addr add 192.168.1.250/32 dev macvlan-host && sudo ip link set macvlan-host up
sudo ip route add 192.168.1.240/28 dev macvlan-host
  1. Modo promiscuo. El conmutador o el hipervisor deben aceptar varias MAC por puerto. En la mayoría de servicios de nube (y en muchas redes Wi-Fi) está prohibido, y macvlan sencillamente no funciona.
  2. Colisión con el DHCP. Docker no habla DHCP: reserva un --ip-range fuera del ámbito del servidor DHCP o acabarás con dos equipos compartiendo IP.

  1. ipvlan en modo L2 y L3

ipvlan resuelve el problema del modo promiscuo: todos los contenedores comparten la MAC del interfaz padre y se distinguen por IP.

# L2: mismo dominio de difusión que el host, sin MAC propia
docker network create -d ipvlan --subnet 192.168.1.0/24 --gateway 192.168.1.1 \
  -o parent=eth0 -o ipvlan_mode=l2 lan-l2
# L3: el host enruta; sin difusión, subred independiente
docker network create -d ipvlan --subnet 10.90.1.0/24 \
  -o parent=eth0 -o ipvlan_mode=l3 lan-l3
Aspecto macvlan ipvlan L2 ipvlan L3
MAC Una por contenedor Compartida con el padre Compartida
Modo promiscuo Necesario No No
Difusión y DHCP No
Enrutado Vía gateway de la LAN Vía gateway de la LAN Lo hace el host
Rutas externas Automáticas Automáticas Hay que añadirlas en el router

El modo L3 es el más limpio en centros de datos con enrutamiento controlado, pero exige que alguien enseñe al router cómo llegar a 10.90.1.0/24. Si no lo haces, los contenedores salen y nada vuelve.

  1. Tabla comparativa de los seis drivers

Driver Aislamiento Rendimiento DNS interno Publicación de puertos Caso de uso típico
bridge (usuario) Alto Bueno (NAT en la salida) -p con DNAT El 95 % de los casos, incluida Aurora Libros
bridge por defecto Medio Igual No (solo --link, obsoleto) -p Compatibilidad; evítala
host Ninguno El mejor No No aplica Agentes de monitorización, alto rendimiento
none Total No aplica No No Procesos sin red
macvlan Medio (LAN plana) Muy bueno, sin NAT Sí, en la red Docker Innecesaria Integrar con equipos de la LAN o sistemas antiguos
ipvlan Medio Muy bueno Innecesaria Como macvlan donde el modo promiscuo está prohibido

El driver overlay, para comunicar contenedores de varios hosts, se cubre en la lección 06-03 junto con la malla de enrutamiento de Swarm.

  1. IPv6 en Docker

Desde la versión 27, IPv6 se habilita por red sin tocar el daemon:

docker network create --ipv6 --subnet 2001:db8:aur::/64 aurora-v6
docker run --rm --network aurora-v6 alpine:3 ip -6 -brief addr show eth0
# eth0  UP  2001:db8:aur::2/64  fe80::42:acff:fe14:2/64

En Compose basta con enable_ipv6: true en la red. Tres avisos: usa direcciones ULA (fd00::/8) si no tienes un prefijo público delegado; activa "ip6tables": true en /etc/docker/daemon.json para que funcionen el NAT y la publicación; y comprueba que tus reglas de cortafuegos existen también en IPv6, porque una política solo IPv4 deja una puerta abierta que nadie mira.

  1. Diagnóstico real: tcpdump y nsenter

La imagen nicolaka/netshoot con --network container: (lección 03-04) te pone dentro del espacio de nombres de red del contenedor con todas las herramientas:

docker run --rm -it --network container:aurora-libros-aurora-api-1 nicolaka/netshoot \
  sh -c 'ip -brief addr; ip route; ss -tlnp'
eth0   UP   172.21.0.5/16
eth1   UP   172.22.0.4/16
default via 172.21.0.1 dev eth0
LISTEN 0 511 *:3000

Dos interfaces, porque aurora-api está en frontal y en trasera; la ruta por defecto sale por frontal, ya que trasera es internal y no tiene puerta de enlace.

Capturar el tráfico real entre la API y la base de datos:

docker run --rm --net container:aurora-libros-aurora-api-1 nicolaka/netshoot \
  timeout 10 tcpdump -n -i any 'host 172.22.0.3 and port 5432' -c 6
IP 172.22.0.4.51442 > 172.22.0.3.5432: Flags [S], seq 2451
IP 172.22.0.3.5432 > 172.22.0.4.51442: Flags [S.], seq 8891, ack 2452
IP 172.22.0.4.51442 > 172.22.0.3.5432: Flags [P.], length 96
IP 172.22.0.3.5432 > 172.22.0.4.51442: Flags [P.], length 1284

Las direcciones son las de la red trasera y no hay ninguna traducción: comunicación directa a través del puente, tal como anticipaba el apartado 3. Desde el host, sin entrar en ningún contenedor, la captura equivalente es sudo tcpdump -n -i veth3a1f9c2 -c 5 'tcp port 5432'.

nsenter es la vía de más bajo nivel: entra en el espacio de nombres de red de un proceso usando las herramientas del host.

pid=$(docker inspect --format '{{.State.Pid}}' aurora-libros-aurora-api-1)
sudo nsenter -t "$pid" -n ip -brief addr
sudo nsenter -t "$pid" -n ss -tnp state established
eth0   UP   172.21.0.5/16
eth1   UP   172.22.0.4/16
ESTAB  0  0  172.22.0.4:51442  172.22.0.3:5432

Es la técnica que salva el día cuando un contenedor es distroless y no tiene ni sh (lección 05-03): tú no necesitas nada dentro, porque usas los binarios del host sobre su namespace. El porqué exacto de que esto funcione está en la lección 05-07.

Errores Comunes y Consejos

Creer que el cortafuegos del host protege los puertos publicados. No lo hace: el DNAT de Docker se evalúa antes. Publica en 127.0.0.1 o filtra en DOCKER-USER.

Añadir reglas en INPUT para bloquear un contenedor. El tráfico va por FORWARD. La cadena correcta es DOCKER-USER.

Escribir reglas dentro de la cadena DOCKER. Docker la reescribe al arrancar o al publicar un puerto, y tus reglas desaparecen.

Depender de las IP de los contenedores. Cambian al recrear. Usa nombres de servicio y reserva las IP fijas para requisitos externos reales.

Usar --network host para "que vaya más rápido". Pierdes el aislamiento y la resolución por nombre, y en una API HTTP la diferencia suele ser despreciable. Mide antes.

Montar macvlan sin comprobar el modo promiscuo, o olvidar bajar la MTU tras una VPN: en el primer caso todo parece bien configurado y nada responde; en el segundo, las conexiones se abren pero se cuelgan al transferir datos grandes.

Consejo: ante un problema de red, sigue siempre el mismo orden: ¿resuelve el nombre? (getent hosts), ¿hay ruta? (ip route), ¿escucha alguien? (ss -tlnp dentro del destino), ¿llega el paquete? (tcpdump). Cada paso descarta una capa entera.

Ejercicios

Ejercicio 1. Con la plataforma levantada, identifica el par veth de aurora-db, demuestra que su extremo del host está enchufado al puente de la red trasera y mide el tráfico que ha pasado por él. Explica por qué rx en el host corresponde a lo que envía el contenedor.

Ejercicio 2. Demuestra el salto del cortafuegos: publica un contenedor en un puerto, comprueba que ufw no lo declara abierto y que aun así responde desde otra máquina; después bloquéalo correctamente con DOCKER-USER y verifica que sigue funcionando desde localhost.

Ejercicio 3. Captura con tcpdump una consulta real entre aurora-api y aurora-db provocada por curl /libros, y demuestra con docker exec sobre Redis por qué la segunda petición no genera tráfico hacia la base de datos.

Soluciones

Solución 1.

idx=$(docker compose exec -T aurora-db cat /sys/class/net/eth0/iflink | tr -d '\r')
veth=$(ip -o link | awk -v i="$idx:" '$1==i {sub(/@.*/,"",$2); print $2}')
red=$(docker network inspect aurora-libros_trasera --format '{{.Id}}' | cut -c1-12)
echo "veth=$veth  puente esperado=br-$red"
ip -o link show "$veth" | grep -o 'master [^ ]*'
ethtool -S "$veth" | grep -E 'peer_ifindex|rx_packets|tx_packets'
veth=veth5d80a3f  puente esperado=br-c81be4f0a7d9
master br-c81be4f0a7d9
     peer_ifindex: 10
     rx_packets: 3921
     tx_packets: 4877

El master coincide exactamente con el puente derivado del ID de la red trasera: ese veth es el cable de aurora-db hacia esa red y no hacia otra. Y peer_ifindex: 10 apunta al índice del eth0 de dentro del contenedor.

Sobre la inversión de sentido: las estadísticas se leen desde el punto de vista del host. Cuando aurora-db responde a una consulta, el paquete sale por su eth0, viaja por el cable virtual y entra al host por veth5d80a3f; para el host eso es recepción, luego rx. Confundirlo lleva a diagnósticos invertidos: si ves rx disparado en el veth de una base de datos, no es que le estén enviando mucho, es que está devolviendo mucho, y probablemente falte un LIMIT en alguna consulta.

Solución 2.

docker run -d --name expuesto -p 5000:80 nginx:alpine
sudo ufw status | grep -c 5000            # 0: ufw no sabe nada de ese puerto
curl -s -o /dev/null -w 'host: %{http_code}\n' http://127.0.0.1:5000/
# desde otra máquina: curl -s -o /dev/null -w 'remoto: %{http_code}\n' http://192.168.1.42:5000/
sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 5000 -j DROP
curl -s -o /dev/null -w 'host tras la regla: %{http_code}\n' http://127.0.0.1:5000/
# desde otra máquina: curl -m 3 http://192.168.1.42:5000/
sudo iptables -D DOCKER-USER -i eth0 -p tcp --dport 5000 -j DROP && docker rm -f expuesto
0
host: 200
remoto: 200
host tras la regla: 200
curl: (28) Connection timed out after 3001 milliseconds

La regla filtra por interfaz de entrada (-i eth0), así que el tráfico que llega por la interfaz física se descarta mientras que el que nace en el propio host —y por tanto no entra por eth0— sigue pasando. Ese matiz es lo que la hace utilizable: bloquea el exterior sin romper la depuración local.

La lección de fondo: ufw informaba de un sistema con un solo puerto abierto y era falso. Publicar un puerto en un servidor con IP pública equivale a abrirlo en Internet, salvo que ates la publicación a 127.0.0.1 o filtres explícitamente. Es exactamente el motivo por el que aurora-db no publica nada desde la lección 04-04.

Solución 3.

docker compose exec aurora-cache redis-cli FLUSHALL
docker run --rm --net container:aurora-libros-aurora-api-1 nicolaka/netshoot \
  timeout 12 tcpdump -n -i any 'host 172.22.0.3 and port 5432' &
sleep 1
curl -s http://localhost:8080/api/libros | head -c 58; echo    # primera: vacía la caché
curl -s http://localhost:8080/api/libros | head -c 58; echo    # segunda: caché caliente
docker compose exec aurora-cache redis-cli KEYS 'libros:*'
wait
{"origen":"db","total":9,"libros":[{"id":1,"titulo":"El
{"origen":"cache","total":9,"libros":[{"id":1,"titulo":
1) "libros:todos"
IP 172.22.0.4.51488 > 172.22.0.3.5432: Flags [P.], length 112
IP 172.22.0.3.5432 > 172.22.0.4.51488: Flags [P.], length 1284
2 packets captured

La primera petición devuelve origen: db y en la captura aparece el diálogo con PostgreSQL en el 5432, con direcciones de la red trasera sin traducir. La segunda devuelve origen: cache y no añade ni un paquete a la captura: el patrón cache-aside encuentra la clave libros:todos en Redis y ni siquiera abre una consulta.

Dos conclusiones útiles. La primera, de arquitectura: tcpdump acaba de convertir en observable algo que hasta ahora era un campo JSON en el que había que confiar. La segunda, de diagnóstico: si un día /libros respondiera origen: db siempre, esta misma captura te diría si el problema está en la escritura de la clave en Redis o en su lectura, sin tocar una línea de código.

Conclusión

Las redes de Docker han dejado de ser magia. Sabes que una red bridge es un puente de Linuxdocker0 o br-<id>— con un extremo veth por contenedor enchufado a él, y sabes emparejar los extremos con iflink y ethtool -S para medir el tráfico de un contenedor concreto desde el host. Conoces los dos caminos de un paquete: conmutación pura entre contenedores de la misma red, y MASQUERADE cuando sale al exterior. Y has leído las reglas reales: la cadena DOCKER con su DNAT por cada -p, y DOCKER-ISOLATION-STAGE-2 con el DROP de dos líneas que explica todo el aislamiento entre redes que llevas usando desde el módulo 3.

Te llevas también un aviso que vale más que muchas configuraciones: publicar un puerto salta el cortafuegos del host, porque el DNAT se evalúa antes que INPUT; la defensa es publicar en 127.0.0.1, no publicar en absoluto o escribir reglas en DOCKER-USER, la única cadena que Docker respeta. Sabes cómo resuelve realmente un nombre el DNS embebido de 127.0.0.11 y por qué la resolución está acotada a las redes compartidas; controlas el direccionamiento (--subnet, --ip-range, --aux-address, mtu) para convivir con VPN y equipos reales; conoces en profundidad los seis drivers, incluidos macvlan con su trampa del modo promiscuo e ipvlan en L2 y L3; y diagnosticas con netshoot, ss, tcpdump sobre el tráfico real entre aurora-api y aurora-db, y nsenter cuando dentro del contenedor no hay ni un sh.

La siguiente capa que hay que abrir es la de los datos. En la lección 05-02 verás el almacenamiento a fondo: qué es un storage driver y cómo funciona overlay2 por dentro con sus lowerdir, upperdir y merged; cuánto cuesta realmente el copy-on-write —medido— y por qué eso obliga a que una base de datos use volúmenes; los volume drivers y sus opciones, incluidos NFS y los volúmenes compartidos entre pilas; y una estrategia de copias de seguridad de Aurora Libros con restauración probada, porque una copia que nunca se ha restaurado no es una copia.

Docker: De Principiante a Avanzado

Módulo 1: Introducción a Docker

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados