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
- Anatomía de una red bridge: puentes de Linux
- Los pares
vethy cómo emparejar los extremos - El camino de un paquete, paso a paso
iptables: las cadenas que crea Docker- NAT de salida y DNAT de publicación
- El cortafuegos del host, saltado (aviso de seguridad)
DOCKER-USER: recuperar el control- El DNS embebido en
127.0.0.11 - Direccionamiento: subredes, rangos, IP fija y MTU
- Los drivers
hostynoneen profundidad macvlan: una IP propia en la LAN físicaipvlanen modo L2 y L3- Tabla comparativa de los seis drivers
- IPv6 en Docker
- Diagnóstico real:
tcpdumpynsenter
- 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 bridge9f3a1c7d5e2b 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}}'Y bridge link show master br-9f3a1c7d5e2b lista los interfaces enchufados a ese puente: uno por cada contenedor conectado a la red.
- Los pares
veth y cómo emparejar los extremos
veth y cómo emparejar los extremosUn 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?Ya tienes el par: eth0 de aurora-api ↔ veth3a1f9c2 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:
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.
- 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 | eth0 → veth → puente → veth → eth0 |
Conmutación en el kernel, sin NAT |
| Otra red o Internet | eth0 → veth → puente → enrutado → iptables (MASQUERADE) → eth0 del host |
NAT y seguimiento de conexiones |
| Desde fuera hacia un puerto publicado | eth0 del host → iptables (DNAT) → puente → veth → eth0 |
NAT de destino |
Que la comunicación aurora-api ↔ aurora-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.
iptables: las cadenas que crea Docker
iptables: las cadenas que crea DockerAl 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) |
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/0Ahí 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.
- NAT de salida y DNAT de publicació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:80Tres reglas, tres mecanismos:
MASQUERADEde subred: todo lo que sale de172.21.0.0/16hacia 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.DNATde publicación: el-p 8080:80deaurora-webse traduce en esta regla. Cualquier paquete que llegue al puerto 8080 de cualquier dirección local se redirige a172.21.0.2:80.MASQUERADEcon 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:
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".
- 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/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.yamlen solitario.
DOCKER-USER: recuperar el control
DOCKER-USER: recuperar el controlDocker 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-numbersnum 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/0Dos 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.
- El DNS embebido en
127.0.0.11
127.0.0.11En 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-dbEsa 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:
- ¿Es un nombre de servicio o alias de una red que compartimos? → responde el daemon con la IP del contenedor.
- ¿Es un contenedor de la misma red por su nombre o
container_name? → igual. - 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-webno resuelveaurora-dbporque 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 condocker compose up -d --scale aurora-api=3seguido degetent hosts aurora-api. options ndots:0evita 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.
- 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.
- Los drivers
host y none en profundidad
host y none en profundidadhost 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 3000El 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.
macvlan: una IP propia en la LAN física
macvlan: una IP propia en la LAN físicamacvlan 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 eth0Desde otro equipo de la oficina, http://192.168.1.241/ responde sin haber publicado ningún puerto.
Las tres trampas, en orden de frecuencia:
- 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- 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.
- Colisión con el DHCP. Docker no habla DHCP: reserva un
--ip-rangefuera del ámbito del servidor DHCP o acabarás con dos equipos compartiendo IP.
ipvlan en modo L2 y L3
ipvlan en modo L2 y L3ipvlan 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 | Sí | Sí | 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.
- 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) | Sí | -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 | Sí | 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.
- 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/64En 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.
- Diagnóstico real:
tcpdump y nsenter
tcpdump y nsenterLa 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'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 6IP 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 1284Las 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 establishedEs 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: 4877El 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 expuesto0
host: 200
remoto: 200
host tras la regla: 200
curl: (28) Connection timed out after 3001 millisecondsLa 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 capturedLa 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 Linux —docker0 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
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
