Problema
En entornos Docker Compose donde un contenedor actúa como gateway entre dos redes (por ejemplo, una red privada y una pública), es habitual usar iptables con la regla MASQUERADE para que el tráfico saliente parezca provenir de la interfaz externa del gateway. Cuando la regla no se aplica, los paquetes siguen mostrando la IP interna del contenedor origen, lo que provoca fallos de conectividad y respuestas rechazadas por los servidores remotos. El síntoma típico es observar, con tcpdump, la IP del contenedor interno en la capa IP del tráfico que sale por la interfaz pública del gateway.
Este comportamiento no es exclusivo de un caso concreto; aparece en cualquier arquitectura que combine:
- varios bridges definidos en
docker-compose.yml, - un contenedor con
network_mode: "service:<gateway>", - una regla
iptables -t nat -A POSTROUTING -o <iface> -j MASQUERADE.
El problema se manifiesta tanto en entornos de desarrollo como en despliegues de producción ligera (homelab, CI/CD pipelines, etc.).
Causa
Varios factores pueden impedir que la regla MASQUERADE surta efecto:
-
Interfaz equivocada
Docker renombra las interfaces dentro del contenedor (eth0,eth1, …). Si la regla usa-o eth0pero el tráfico sale poreth1, la coincidencia falla y el paquete no se traduce. -
Falta de
net.ipv4.ip_forward
El kernel necesita el flagip_forward=1para reenviar paquetes entre interfaces. En contenedores sin privilegios o sin la sysctl adecuada, el reenvío se bloquea antes de queiptablesactúe. -
Privilegios insuficientes
iptablesrequiereCAP_NET_ADMIN. Ejecutar el contenedor sin--cap-add=NET_ADMIN(o sinprivileged: true) impide crear o modificar reglas. -
Reglas de Docker que sobrescriben la cadena
Docker inserta sus propias reglas enDOCKER,DOCKER-USERyPOSTROUTING. Si la regla MASQUERADE se coloca antes de la regla-j MASQUERADEde Docker, puede ser anulada o nunca alcanzada. -
Orden de inserción
En la tabla nat, las reglas se evalúan secuencialmente. Una regla anterior que haga-j RETURNo que limite el rango de origen puede impedir que la regla MASQUERADE se vea. -
Falta del paquete
iptables
Algunas imágenes minimalistas (Alpine, BusyBox) no incluyeniptablespor defecto. Intentar ejecutar la regla sin el binario disponible genera un error silencioso si el script no verifica el código de salida. -
Uso de
network_mode: "service:<gateway>"sin exponer la interfaz externa
Cuando el contenedor cliente comparte la pila de red del gateway, ambos comparten la misma tabla de rutas. Si la interfaz externa no está conectada al bridge público, no hay salida real para la regla. -
Política de firewall del host
El host puede tenerufwofirewalldque bloquean el tráfico NAT antes de que llegue al contenedor, creando la ilusión de que la regla no funciona.
Solución
Una solución robusta debe abordar cada punto anterior y quedar reutilizable para cualquier composición que requiera NAT. El flujo recomendado es:
1. Preparar el contenedor gateway
- Usa una imagen que incluya
iptableso instálalo en tiempo de arranque. - Concede
NET_ADMIN(oprivileged: truesi el entorno lo permite). - Habilita el reenvío IP mediante
sysctl.
Ejemplo de fragmento docker-compose.yml:
services:
nat-gateway:
image: alpine:3.23.5
privileged: true # o cap_add: ["NET_ADMIN"]
sysctls:
- net.ipv4.ip_forward=1
volumes:
- ./nat-init.sh:/usr/local/bin/nat-init.sh:ro
command: /usr/local/bin/nat-init.sh
networks:
- private-vpc
- public-vpc
api:
image: alpine:3.23.5
command: sleep infinity
network_mode: "service:nat-gateway"
depends_on:
- nat-gateway
networks:
- private-vpc
networks:
private-vpc:
internal: true
public-vpc:
driver: bridge
2. Detectar la interfaz externa
Dentro del script de inicialización, determina cuál es la interfaz conectada al bridge público. En la mayoría de los contenedores será eth0 o eth1. Un método rápido:
#!/bin/sh
set -e
# Instalar iptables si la imagen es mínima
apk add --no-cache iptables
# Detectar la interfaz que tiene una ruta predeterminada
EXTERNAL_IF=$(ip route show default | awk '{print $5}')
echo "Interfaz externa detectada: $EXTERNAL_IF"
# Aplicar la regla MASQUERADE solo si no existe ya
if ! iptables -t nat -C POSTROUTING -s 172.19.0.0/16 -o "$EXTERNAL_IF" -j MASQUERADE 2>/dev/null; then
iptables -t nat -A POSTROUTING -s 172.19.0.0/16 -o "$EXTERNAL_IF" -j MASQUERADE
echo "Regla MASQUERADE añadida"
else
echo "Regla MASQUERADE ya presente"
fi
exec sleep infinity
Puntos clave del script:
- Detección automática de la interfaz evita errores de coincidencia.
- Rango de origen (
-s 172.19.0.0/16) limita la regla a la subred privada del proyecto, evitando colisiones con otras redes del host. - Comprobación de existencia (
iptables -C) previene duplicados que pueden saturar la tabla.
3. Ordenar la regla después de las de Docker
Docker inserta sus reglas en la cadena POSTROUTING bajo el nombre MASQUERADE. Para asegurarse de que la regla del gateway se evalúe después, añádela a la cadena POSTROUTING después de la regla -j MASQUERADE de Docker, o inserta la regla en DOCKER-USER que se procesa antes de cualquier regla de Docker:
iptables -t nat -I DOCKER-USER 1 -s 172.19.0.0/16 -o "$EXTERNAL_IF" -j MASQUERADE
4. Persistir la configuración (opcional)
Los contenedores se reinician frecuentemente. Para que la regla sobreviva a un reinicio del contenedor, guarda el estado en un archivo y recárgalo al iniciar:
iptables-save > /etc/iptables.rules
# En el arranque:
iptables-restore < /etc/iptables.rules
5. Verificar que el host no bloquee la NAT
Revisa que el host no tenga reglas que descarten el tráfico NAT:
iptables -L INPUT -n -v
iptables -L FORWARD -n -v
Si encuentras una política DROP en FORWARD, añade una excepción:
iptables -I FORWARD -i "$EXTERNAL_IF" -o eth0 -j ACCEPT
iptables -I FORWARD -i eth0 -o "$EXTERNAL_IF" -j ACCEPT
Cuándo aplicar esta solución
Utiliza este enfoque cuando:
- Necesites un contenedor que actúe como router/NAT entre dos bridges de Docker.
- Quieras exponer servicios internos sin usar puertos publicados individualmente.
- La topología incluya múltiples subredes privadas y una única salida a Internet.
- No puedas o no quieras depender de la opción
--publishde Docker para cada servicio.
No es necesario si:
- Solo necesitas publicar puertos concretos mediante
ports:endocker-compose.yml. - La arquitectura ya usa
hostnetwork omacvlanpara exponer directamente los contenedores. - El host gestiona NAT a nivel de red y los contenedores se quedan en modo
bridgesin requerir reescritura de IP.
Código
#!/bin/sh
set -e
# Instalar iptables en Alpine minimalista
apk add --no-cache iptables
# Detectar la interfaz con ruta por defecto
EXTERNAL_IF=$(ip route show default | awk '{print $5}')
echo "Interfaz externa detectada: $EXTERNAL_IF"
# Subred privada del proyecto (ajustar según sea necesario)
PRIVATE_SUBNET="172.19.0.0/16"
# Insertar regla en DOCKER-USER para que se ejecute antes de las reglas