Estás en un contenedor intentando alcanzar algo aburrido pero esencial—una API interna, un VIP de base de datos, un host en otra subred—y obtienes el equivalente en redes
a un encogimiento de hombros: No route to host. Mientras tanto, el mismo destino funciona desde el host. Tu servicio está caído, el pager suena, y la
red de Docker ha decidido dar una lección de filosofía.
Este error suele atribuirse a que «Docker es raro», lo cual es una mentira reconfortante. En la práctica casi siempre es un problema claro de redes en Linux: rutas, reenvío,
políticas de firewall, filtrado de ruta inversa, o expectativas de NAT que no coinciden con la realidad. El truco es arreglarlo de forma que sobreviva reinicios, reinicios
del demonio, actualizaciones del SO y un compañero que «optimiza» tu firewall un viernes.
Guía rápida de diagnóstico
Cuando los contenedores lanzan No route to host, no empieces reiniciando Docker. No empieces reinstalando Docker. Empieza por averiguar qué capa miente.
Aquí está el orden que encuentra el cuello de botella rápidamente.
1) Confirma el modo de fallo desde dentro del contenedor
- Si
pingdice “Network is unreachable”, tienes un problema de enrutamiento en el namespace del contenedor. - Si
pingdice “No route to host”, el kernel cree que existe una ruta pero no puede entregar (a menudo ARP/vecino, rechazo de firewall o enrutamiento + rp_filter). - Si las conexiones TCP hacen timeout, sospecha de drop del firewall, NAT/mascarada faltante o problemas con la ruta de retorno.
2) Compara las rutas del namespace del contenedor con las del host
Los contenedores normalmente tienen una ruta por defecto vía la puerta de enlace del bridge de Docker (a menudo 172.17.0.1). Si el host tiene rutas
especiales (enrutamiento por políticas, rutas estáticas a segmentos RFC1918, interfaces VPN), el contenedor quizá no herede lo que crees que hereda.
3) Revisa el reenvío y la política filter en el host
Los paquetes del contenedor salen de su namespace y llegan a la cadena FORWARD del host. Si FORWARD está en DROP (común en baselines endurecidos), tu contenedor puede
tener rutas perfectas y aun así no llegar a ninguna parte.
4) Verifica expectativas de NAT
Si el destino está fuera de la subred del bridge de Docker, Docker normalmente confía en NAT (MASQUERADE) a menos que uses redes enrutadas. La falta de NAT es un
clásico escenario de «funciona en el host, falla en el contenedor».
5) Revisa rp_filter si tienes rutas asimétricas
Si el tráfico sale por una interfaz (por ejemplo, una VPN) y regresa por otra (por ejemplo, la puerta de enlace por defecto), el filtrado estricto de ruta inversa
podrá descartarlo. Esto es dolorosamente común en redes corporativas con túneles divididos y enrutamiento interno.
Qué significa realmente “No route to host” (y qué no significa)
La frase parece «falta la ruta en la tabla», pero Linux la usa para varias situaciones. El kernel puede devolver EHOSTUNREACH («No route to host»)
cuando:
- Existe una ruta pero el siguiente salto es inalcanzable (fallo de resolución de vecino/ARP en L2).
- Un ICMP host unreachable vuelve desde un router y se refleja al socket.
- Una regla de firewall rechaza explícitamente con
icmp-host-prohibitedo similar. - El enrutamiento por políticas envía el paquete a un agujero negro o a una ruta de tipo unreachable.
- rp_filter descarta el tráfico de retorno tan agresivamente que la pila se comporta como si el camino estuviera roto.
No significa de forma fiable «olvidaste añadir una ruta». A veces tienes una ruta y la red sigue sin cooperar. Linux es honesto, pero no siempre hablador.
Una cita práctica que funciona en salas de incidentes: La esperanza no es una estrategia.
— General Gordon R. Sullivan.
Cuando el error es “No route”, depurar con esperanza sale especialmente caro.
Cómo el tráfico de contenedores realmente sale del equipo (modo bridge)
La mayoría de hosts Docker todavía ejecutan la red bridge por defecto (docker0) o un bridge definido por el usuario. Dentro de un contenedor obtienes un par veth:
un extremo en el namespace del contenedor (a menudo eth0), otro extremo en el host (con nombre tipo vethabc123). El host lo puentea dentro de
docker0.
Desde ahí los paquetes pasan por el enrutamiento normal de Linux en el host, además de iptables/nftables. Ese es el punto crítico: la red de contenedores no es «magia de Docker»,
es redes de Linux con reglas extra.
En modo bridge, el acceso saliente a internet/interno normalmente depende de NAT:
- IP origen del contenedor: 172.17.x.y
- MASQUERADE del host la reescribe a la IP de salida del host
- El tráfico de retorno vuelve al host y se des-NATea al contenedor
Si esperas redes enrutadas en su lugar (sin NAT), debes proporcionar rutas reales en la red upstream hacia las subredes de contenedores y debes permitir el reenvío.
Muchos equipos construyen accidentalmente un esquema «medio enrutado, medio NAT» que funciona hasta que deja de hacerlo.
Datos interesantes y contexto histórico (por qué este problema vuelve a aparecer)
- Los namespaces de red en Linux llegaron al kernel mainline en 2008–2009. Los contenedores son en gran parte namespaces más cgroups; Docker no inventó el camino del paquete.
- Docker inicialmente dependía mucho de iptables para ensamblar la red porque era ubicuo y scriptable; nftables llegó después y complicó la historia.
- La subred bridge por defecto de Docker (172.17.0.0/16) choca con redes corporativas reales mucho más a menudo de lo que nadie quiere admitir.
-
La política por defecto de la cadena FORWARD en DROP se hizo más común a medida que se endurecían las bases de seguridad. Eso es genial para los hosts, hasta que olvidas
que los contenedores son tráfico «reenviado». - firewalld y ufw se hicieron populares como gestores de firewall «más amigables»; ambos pueden sobrescribir o reordenar las reglas de Docker si no los integras explícitamente.
- El filtrado de ruta inversa (rp_filter) fue diseñado para reducir el spoofing IP. En servidores multi-homed modernos (VPNs, múltiples uplinks), puede penalizar el enrutamiento legítimo asimétrico.
- conntrack (seguimiento de conexiones) hizo viable el firewall con estado a escala, pero también introdujo modos de fallo: el agotamiento de la tabla se parece a intermitencias de red aleatorias.
- A medida que creció la red en la nube, el enrutamiento por políticas (múltiples tablas de enrutamiento, reglas por origen) se volvió común. Los contenedores raramente coinciden con las suposiciones integradas en esas reglas.
Broma #1: La red de Docker es como el Wi‑Fi de la oficina: cuando falla, todo el mundo de repente se convierte en ingeniero de redes por 20 minutos.
Tareas prácticas: comandos, salidas y qué decisión tomar
A continuación están las tareas reales que correrían durante un incidente. Cada una incluye un comando, salida representativa, qué significa y la siguiente decisión. Ejecútalas
en orden hasta que encuentres la mentira. Arreglarás el problema más rápido y sabrás qué arreglaste.
Tarea 1: Reproducir desde dentro del contenedor (no adivines)
cr0x@server:~$ docker exec -it app1 sh -lc 'ip route; echo ---; ping -c1 -W1 10.20.30.40; echo ---; nc -vz -w2 10.20.30.40 5432'
default via 172.18.0.1 dev eth0
172.18.0.0/16 dev eth0 scope link src 172.18.0.12
---
PING 10.20.30.40 (10.20.30.40): 56 data bytes
ping: sendto: No route to host
---
nc: connect to 10.20.30.40 port 5432 (tcp) failed: No route to host
Significado: El contenedor cree que tiene una ruta por defecto, pero el kernel no puede entregar paquetes a ese destino. Esto no es «falta de puerta de enlace por defecto».
Decisión: Cambia la atención al host: reenvío, bridge, rechazo del firewall, resolución de vecinos o política de enrutamiento del host.
Tarea 2: Confirma la red Docker y el mapeo de la puerta de enlace
cr0x@server:~$ docker inspect -f '{{.Name}} {{range .NetworkSettings.Networks}}{{.NetworkID}} {{.IPAddress}} gw={{.Gateway}}{{end}}' app1
/app1 9c2d3a7f6c8a 172.18.0.12 gw=172.18.0.1
Significado: Ahora sabes la subred del contenedor (172.18.0.0/16) y la IP de la puerta de enlace.
Decisión: Mira el bridge y el veth correspondientes en el host.
Tarea 3: Identifica la interfaz bridge y su estado
cr0x@server:~$ ip -br link show | egrep 'docker0|br-'
docker0 UP 5a:3f:2b:aa:bb:cc <BROADCAST,MULTICAST,UP,LOWER_UP>
br-9c2d3a7f6c8a UP 02:42:7c:8e:11:22 <BROADCAST,MULTICAST,UP,LOWER_UP>
Significado: La red definida por el usuario creó un bridge br-9c2d3a7f6c8a, y está arriba.
Decisión: Verifica que el bridge tenga la IP y rutas esperadas.
Tarea 4: Verifica direcciones IP del bridge (la puerta de enlace debe existir en el host)
cr0x@server:~$ ip -4 addr show br-9c2d3a7f6c8a
8: br-9c2d3a7f6c8a: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-9c2d3a7f6c8a
valid_lft forever preferred_lft forever
Significado: La puerta de enlace existe y es correcta.
Decisión: Si la puerta de enlace faltara, reiniciarías la red de Docker o arreglarías el arranque del demonio. Como está presente, sigue adelante.
Tarea 5: Comprueba sysctls de reenvío en el host
cr0x@server:~$ sysctl net.ipv4.ip_forward net.ipv4.conf.all.forwarding
net.ipv4.ip_forward = 0
net.ipv4.conf.all.forwarding = 0
Significado: El host no está reenviando paquetes IPv4. Los contenedores pueden hablar con el host, pero no a través de él.
Decisión: Habilita el reenvío de forma persistente (no con un sysctl de una sola vez que desaparezca al reiniciar).
Tarea 6: Activa el reenvío (persistente) y verifica
cr0x@server:~$ sudo install -d -m 0755 /etc/sysctl.d
cr0x@server:~$ printf '%s\n' 'net.ipv4.ip_forward=1' 'net.ipv4.conf.all.forwarding=1' | sudo tee /etc/sysctl.d/99-docker-forwarding.conf
net.ipv4.ip_forward=1
net.ipv4.conf.all.forwarding=1
cr0x@server:~$ sudo sysctl --system | tail -n 5
* Applying /etc/sysctl.d/99-docker-forwarding.conf ...
net.ipv4.ip_forward = 1
net.ipv4.conf.all.forwarding = 1
Significado: El reenvío ahora está habilitado y sobrevivirá a reinicios.
Decisión: Vuelve a probar desde el contenedor. Si sigue roto, entras en territorio de firewall/NAT/rp_filter.
Tarea 7: Inspecciona la política de FORWARD y las cadenas de Docker (iptables)
cr0x@server:~$ sudo iptables -S FORWARD
-P FORWARD DROP
-A FORWARD -j DOCKER-USER
-A FORWARD -j DOCKER-ISOLATION-STAGE-1
-A FORWARD -o br-9c2d3a7f6c8a -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -o br-9c2d3a7f6c8a -j DOCKER
-A FORWARD -i br-9c2d3a7f6c8a ! -o br-9c2d3a7f6c8a -j ACCEPT
-A FORWARD -i br-9c2d3a7f6c8a -o br-9c2d3a7f6c8a -j ACCEPT
Significado: El DROP por defecto está bien solo si las reglas de ACCEPT cubren tu tráfico. El -i br-... ! -o br-... ACCEPT debería permitir el egress de contenedores.
Decisión: Si esas reglas ACCEPT faltan, la inserción de reglas de Docker está siendo bloqueada/sobrescrita. Investiga integración con firewalld/ufw/nftables.
Tarea 8: Revisa la cadena DOCKER-USER (a menudo viven ahí las reglas de seguridad de tu organización)
cr0x@server:~$ sudo iptables -S DOCKER-USER
-N DOCKER-USER
-A DOCKER-USER -i br-9c2d3a7f6c8a -d 10.0.0.0/8 -j REJECT --reject-with icmp-host-prohibited
-A DOCKER-USER -j RETURN
Significado: Alguien está rechazando explícitamente el tráfico de contenedores hacia 10/8 con un icmp-host-prohibited. Muchas aplicaciones reportan eso como “No route to host.”
Decisión: Elimina/ajusta la regla de reject, o añade una excepción de allow para la subred/puertos reales de destino.
Tarea 9: Confirma que NAT/MASQUERADE existe para la subred del contenedor
cr0x@server:~$ sudo iptables -t nat -S POSTROUTING | egrep 'MASQUERADE|172\.18\.0\.0/16'
-A POSTROUTING -s 172.18.0.0/16 ! -o br-9c2d3a7f6c8a -j MASQUERADE
Significado: El NAT está configurado para egress (todo lo que salga por fuera del bridge queda enmascarado).
Decisión: Si falta, o Docker no está gestionando iptables, o algún otro gestor de firewall limpió las reglas nat. Decide si quieres NAT o redes enrutadas.
Tarea 10: Verifica la configuración iptables de Docker (chequeo de «por qué no programó reglas»)
cr0x@server:~$ docker info --format '{{json .SecurityOptions}} {{.Name}}' | head -n 1
["name=seccomp,profile=default","name=cgroupns"] server
cr0x@server:~$ sudo cat /etc/docker/daemon.json 2>/dev/null || echo "no /etc/docker/daemon.json"
{
"iptables": false
}
Significado: Docker está configurado para no tocar iptables. Eso puede ser intencional, pero ahora tú asumes la responsabilidad de todas las reglas para NAT y reenvío.
Decisión: O pones "iptables": true (y gestionas la integración), o implementas reglas equivalentes completas y las haces persistentes.
Tarea 11: Inspecciona la selección de ruta al destino desde el host
cr0x@server:~$ ip route get 10.20.30.40
10.20.30.40 via 10.20.0.1 dev tun0 src 10.20.10.5 uid 0
cache
Significado: El host enruta ese destino vía tun0 (VPN). El tráfico del contenedor también probablemente egresará vía tun0 tras reenvío/NAT.
Decisión: Si hay una VPN involucrada, sospecha inmediatamente de rp_filter y desajustes de enrutamiento por políticas. Continúa con las comprobaciones de rp_filter y reglas.
Tarea 12: Revisa reglas de enrutamiento por políticas (los contenedores pueden caer en la tabla equivocada)
cr0x@server:~$ ip rule show
0: from all lookup local
100: from 10.20.10.5 lookup vpn
32766: from all lookup main
32767: from all lookup default
Significado: Hay una regla basada en origen: solo el tráfico con origen 10.20.10.5 usa la tabla vpn. El NAT puede reescribir el origen a otra cosa.
Decisión: Decide si quieres NAT (el tráfico de contenedores se vuelve la IP del host y coincide con la regla) o enrutamiento sin NAT (el origen del contenedor es 172.18/16 y no coincidirá).
Si es lo último, debes añadir ip rule para las subredes de contenedores o enrutarlas correctamente.
Tarea 13: Verifica configuraciones de rp_filter (global e interfaz)
cr0x@server:~$ sysctl net.ipv4.conf.all.rp_filter net.ipv4.conf.default.rp_filter net.ipv4.conf.tun0.rp_filter
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.default.rp_filter = 1
net.ipv4.conf.tun0.rp_filter = 1
Significado: rp_filter estricto está habilitado. Si la ruta de retorno para tráfico reenviado/NATeado no coincide con la idea del kernel de «mejor ruta», los paquetes se descartan.
Decisión: Para hosts multi-homed/VPN, establece rp_filter en modo loose (2) al menos en las interfaces implicadas, de forma persistente.
Tarea 14: Observa contadores mientras reproduces (conteo de hits en reglas iptables)
cr0x@server:~$ sudo iptables -L DOCKER-USER -v -n --line-numbers
Chain DOCKER-USER (1 references)
num pkts bytes target prot opt in out source destination
1 120 7200 REJECT all -- br-9c2d3a7f6c8a * 0.0.0.0/0 10.0.0.0/8 reject-with icmp-host-prohibited
2 900 54000 RETURN all -- * * 0.0.0.0/0 0.0.0.0/0
Significado: La regla de reject está coincidiendo activamente con tráfico (pkts/bytes aumentando). Esto no es teórico.
Decisión: Cambia esa regla. No toques Docker. No toques rutas. Arregla la política que causa el rechazo.
Tarea 15: Confirma el comportamiento de vecinos/ARP en el bridge del host
cr0x@server:~$ ip neigh show dev br-9c2d3a7f6c8a | head
172.18.0.12 lladdr 02:42:ac:12:00:0c REACHABLE
Significado: El host ve la MAC del contenedor y la entrada de vecino está sana.
Decisión: Si ves FAILED/INCOMPLETE repetidamente, sospecha problemas L2: misconfiguración del bridge, flapping del veth o problemas raros de MTU.
Tarea 16: Captura el camino de un paquete (tcpdump que responda una pregunta)
cr0x@server:~$ sudo tcpdump -ni br-9c2d3a7f6c8a host 10.20.30.40
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on br-9c2d3a7f6c8a, link-type EN10MB (Ethernet), snapshot length 262144 bytes
14:21:18.019393 IP 172.18.0.12.48522 > 10.20.30.40.5432: Flags [S], seq 1234567890, win 64240, options [mss 1460,sackOK,TS val 123 ecr 0,nop,wscale 7], length 0
Significado: El paquete sale del contenedor y llega al bridge. A continuación, comprueba la interfaz de salida (por ejemplo, eth0 o tun0)
para ver si se reenvía/NATea.
Decisión: Si lo ves en el bridge pero no en la egress, el firewall/reenvío del host está bloqueando. Si lo ves egress pero no hay respuesta, es enrutamiento/ruta de retorno upstream.
Broma #2: La manera más rápida de encontrar una regla de firewall rota es decir en voz alta «no puede ser el firewall» en el canal de incidentes.
iptables vs nftables vs firewalld: quién está realmente a cargo
Las interrupciones de “no route to host” más duraderas son políticas: múltiples sistemas piensan que son dueños del firewall. Docker escribe reglas. firewalld escribe reglas.
Tu agente de seguridad escribe reglas. Alguien añade nftables directamente porque leyó un post. Luego una actualización del SO cambia el backend de iptables-legacy a iptables-nft
y todos fingen que nada cambió.
Conoce tu backend: iptables-legacy o iptables-nft
En muchas distribuciones modernas, iptables es un wrapper de compatibilidad sobre nftables. Docker ha mejorado aquí, pero «mejorado» no es lo mismo que «inmune a políticas locales».
cr0x@server:~$ sudo update-alternatives --display iptables 2>/dev/null | sed -n '1,12p'
iptables - auto mode
link best version is /usr/sbin/iptables-nft
link currently points to /usr/sbin/iptables-nft
Significado: Estás usando el backend nft. Las reglas pueden ser visibles con nft list ruleset.
Decisión: Si debugueas con iptables pero la aplicación en producción es nftables en una tabla distinta, perseguirás fantasmas. Verifica también con nft.
cr0x@server:~$ sudo nft list ruleset | sed -n '1,40p'
table inet filter {
chain forward {
type filter hook forward priority filter; policy drop;
jump DOCKER-USER
ct state related,established accept
}
}
Significado: nftables está aplicando una política forward drop. Docker puede seguir insertando reglas, pero tu política base importa.
Decisión: Asegura que los accepts de Docker estén presentes y en el orden correcto, o permite explícitamente las subredes de bridge en tu capa de firewall gestionada.
Integración con firewalld: no lo enfrentes, configúralo
firewalld no es malvado. Es solo confiado. Si firewalld está activo y además esperas que Docker gestione iptables libremente, necesitas un plan explícito. El plan suele ser:
dejar que Docker maneje sus cadenas y asegurar que tus zonas y políticas permitan el reenvío desde los bridges de Docker.
cr0x@server:~$ sudo systemctl is-active firewalld
active
cr0x@server:~$ sudo firewall-cmd --get-active-zones
public
interfaces: eth0
Significado: firewalld está activo y gestiona al menos eth0. Los bridges de Docker pueden no estar en ninguna zona, o por defecto en una zona restrictiva.
Decisión: Coloca las interfaces bridge de Docker en una zona trusted/permitida (o crea una zona dedicada) y hazlo permanente.
cr0x@server:~$ sudo firewall-cmd --permanent --zone=trusted --add-interface=br-9c2d3a7f6c8a
success
cr0x@server:~$ sudo firewall-cmd --reload
success
Significado: El bridge está ahora en la zona trusted tras reboots y recargas.
Decisión: Vuelve a probar la conectividad de contenedores. Si tienes requisitos de cumplimiento, no uses trusted; usa una zona personalizada con reglas explícitas.
Soluciones de enrutamiento que perduran (no solo “ip route add”)
La solución más tentadora es la menos duradera: añade una ruta estática en el host, ves que funciona y declaras victoria. Luego ocurre un reinicio. O NetworkManager reaplica perfiles. O el cliente VPN reconfigura rutas.
Tu incidente vuelve, pero ahora a las 3 a.m.
Categoría A: Tu subred de contenedores colisiona con la red real
Si tu red corporativa usa 172.16/12 intensivamente, los valores por defecto de Docker son una mina terrestre. El enrutamiento se vuelve ambiguo. Paquetes que deberían ir a una subred remota
pueden interpretarse como locales en el bridge, o viceversa.
Mejor práctica: elige un CIDR de contenedores específico del sitio que sepas que no colisionará, y ponlo en la configuración del demonio desde el principio.
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"bip": "10.203.0.1/16",
"default-address-pools": [
{ "base": "10.203.0.0/16", "size": 24 }
]
}
Significado: La IP del bridge de Docker y las redes definidas por el usuario se extraerán de 10.203.0.0/16 con redes /24.
Decisión: Esto es disruptivo. Hazlo en hosts nuevos o durante una ventana de migración; las redes/contendedores existentes pueden necesitar recreación.
Categoría B: Necesitas contenedores enrutados (sin NAT)
Algunos entornos detestan el NAT. Los auditores quieren IPs reales de origen. Equipos de red quieren enrutar los CIDR de contenedores como cualquier otra subred. Puedes hacerlo, pero debes comprometerte.
- Los routers upstream deben tener rutas hacia las subredes de contenedores vía los hosts Docker.
- El host Docker debe permitir el reenvío y no debe enmascarar esas subredes.
- El tráfico de retorno debe ser lo bastante simétrico como para evitar drops por rp_filter.
Si intentas «casi enrutar» y aún tienes reglas NAT por «si acaso», crearás comportamiento intermitente que te hará cuestionar tu carrera.
Categoría C: El enrutamiento por políticas debe incluir orígenes de contenedores
Si tu host usa múltiples tablas de enrutamiento, los contenedores no son especiales. Son simplemente redes de origen adicionales. Añade reglas intencionalmente.
cr0x@server:~$ sudo ip rule add from 172.18.0.0/16 lookup vpn priority 110
cr0x@server:~$ sudo ip rule show | sed -n '1,6p'
0: from all lookup local
100: from 10.20.10.5 lookup vpn
110: from 172.18.0.0/16 lookup vpn
32766: from all lookup main
32767: from all lookup default
Significado: El tráfico con origen en los contenedores ahora usará la tabla vpn, coincidiendo con la intención del host.
Decisión: Hazlo persistente con la herramienta de red de tu distribución (scripts dispatcher de NetworkManager, systemd-networkd, o scripts estáticos). Las ip rules de una sola vez mueren al reinicio.
Patrones de persistencia que no se descomponen
- systemd-networkd: declara rutas y políticas de enrutamiento en archivos
.network. - NetworkManager: usa
nmcli connection modifypara añadir rutas y reglas al perfil. - Clientes VPN: si empujan rutas, configura hooks «route-up» para manejar también los orígenes de contenedores, o deshabilita las rutas empujadas y gestiona centralmente.
Si no sabes qué herramienta gestiona tus rutas, no tienes una configuración de enrutamiento. Tienes una vibra de enrutamiento.
Correcciones de iptables/nftables que perduran
Las correcciones persistentes de firewall se tratan de propiedad. Elige un sistema para que sea el dueño de las reglas e integra los demás. La peor opción es «Docker tiene algunas, el agente de seguridad tiene algunas y nosotros añadimos unas pocas durante incidentes».
Opción 1 (común): Deja que Docker gestione sus reglas, pero constriñe con DOCKER-USER
Docker inserta sus cadenas y saltos. El lugar soportado para tu política es la cadena DOCKER-USER. Se evalúa antes que las reglas de accept de Docker.
Ahí es donde añades listas blancas/negras sin pelear con el generador de reglas de Docker.
Si usas DOCKER-USER, ten cuidado con REJECT vs DROP. REJECT produce fallos inmediatos tipo “No route to host”, lo cual es bueno para la experiencia de usuario pero terrible para el diagnóstico.
DROP produce timeouts, que es peor para la experiencia de usuario pero a veces más claro para la postura de seguridad. Elige con intención y documéntalo.
Opción 2: Docker con iptables deshabilitado; tú lo gestionas todo
Esto es válido en entornos cerrados, pero entonces debes implementar:
- Reglas de aceptación de reenvío entre bridge e interfaces de salida
- NAT/mascarada para subredes de contenedores (si quieres NAT)
- Aceptación de established/related para tráfico de retorno
- Reglas de aislamiento si te importa la segmentación entre bridges
La ventaja: política predecible. La desventaja: ahora eres el ingeniero de firewall de Docker, te guste o no.
Persiste reglas iptables correctamente
En Debian/Ubuntu, la gente suele usar iptables-persistent. En sistemas RHEL-like, firewalld/nftables es la vía habitual. La meta es: las reglas reaparezcan tras el reinicio en el orden correcto.
cr0x@server:~$ sudo iptables-save | sed -n '1,35p'
*filter
:INPUT ACCEPT [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:DOCKER-USER - [0:0]
-A FORWARD -j DOCKER-USER
-A DOCKER-USER -j RETURN
COMMIT
Significado: Esto es lo que se restaurará si lo persistes. Observa también: el orden importa.
Decisión: Si tu mecanismo de persistencia restaura reglas antes de que Docker arranque, puede que necesites ajustar el ordering de systemd para que Docker pueda recrear cadenas antes de aplicar la política (o tu política debe tolerar cadenas ausentes).
NAT que sobrevive recargas de firewall
Las recargas de firewalld pueden limpiar y volver a aplicar reglas. Si el NAT de Docker desaparece después de una recarga, verás «funcionaba ayer, roto después de la ventana de cambios». Si usas firewalld,
prefiere configurar masquerade y la política de forwarding vía firewalld para la(s) zona(s) relevante(s), o asegura que la integración de Docker sea soportada en tu distro.
cr0x@server:~$ sudo firewall-cmd --zone=public --query-masquerade
no
cr0x@server:~$ sudo firewall-cmd --permanent --zone=public --add-masquerade
success
cr0x@server:~$ sudo firewall-cmd --reload
success
cr0x@server:~$ sudo firewall-cmd --zone=public --query-masquerade
yes
Significado: La mascarada está habilitada a nivel del gestor de firewall, así que una recarga no eliminará silenciosamente el comportamiento de NAT de salida.
Decisión: Haz esto solo si tu entorno espera NAT para esa zona; no actives masquerade a ciegas en hosts que deberían estar estrictamente enrutados.
rp_filter y enrutamiento asimétrico: el asesino silencioso de contenedores
Si ejecutas un host simple de una sola NIC con una puerta de enlace por defecto, puedes ignorar mayormente rp_filter. Los sistemas de producción rara vez se mantienen tan simples. Añade una interfaz VPN,
un segundo uplink, una política de enrutamiento tipo VRF con ip rules, y rp_filter estricto se convierte en una máquina de drops.
Cómo falla: el tráfico del contenedor entra al host por br-*, se reenvía por tun0, y las respuestas vuelven por eth0
(o viceversa). rp_filter estricto comprueba si la dirección de origen del paquete entrante sería enrutada de vuelta por la misma interfaz; si no, el paquete puede descartarse.
Ves “No route to host” o timeouts. Tus logs están silenciosos. Todos culpan a Docker.
El modo loose suele ser el compromiso correcto
rp_filter=2 (loose) verifica que el origen sea alcanzable por alguna interfaz, no necesariamente por la que llegó. Eso es generalmente aceptable para servidores en
entornos de enrutamiento complejos.
cr0x@server:~$ printf '%s\n' \
'net.ipv4.conf.all.rp_filter=2' \
'net.ipv4.conf.default.rp_filter=2' \
'net.ipv4.conf.tun0.rp_filter=2' \
| sudo tee /etc/sysctl.d/99-rpfilter-loose.conf
net.ipv4.conf.all.rp_filter=2
net.ipv4.conf.default.rp_filter=2
net.ipv4.conf.tun0.rp_filter=2
cr0x@server:~$ sudo sysctl --system | tail -n 6
* Applying /etc/sysctl.d/99-rpfilter-loose.conf ...
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.tun0.rp_filter = 2
Significado: rp_filter en modo loose ahora es persistente.
Decisión: Si estás en un entorno hostil donde el spoofing es una gran preocupación, considera alternativas: enrutamiento más explícito y simétrico, o relajar rp_filter
solo en interfaces específicas implicadas en el reenvío.
Tres microhistorias corporativas desde la trinchera
1) El incidente causado por una suposición equivocada: «El host puede alcanzarlo, así que los contenedores también.»
Un equipo de plataforma desplegó una nueva canalización interna de métricas. El colector corría en Docker, hacía scrape de servicios en varios rangos RFC1918 y enviaba datos a un
clúster central. El primer día funcionó en staging y en una pequeña porción de producción.
Luego se amplió. Un grupo de hosts empezó a registrar No route to host cuando el colector intentaba alcanzar un conjunto de servicios detrás de una interfaz VPN. El host
en sí podía hacer curl a esos servicios sin problema. La suposición del equipo se endureció hasta convertirse en diagnóstico: «El enrutamiento de Docker está roto.»
La realidad: el host tenía reglas de enrutamiento por origen que solo se aplicaban a la IP de origen de la VPN del host. El tráfico originado en el host usaba esa IP, coincidía con la tabla vpn
y salía por el túnel. El tráfico de contenedores a veces se NATeaba, otras veces se enroutaba (dependiendo de qué nodo tuviera qué baseline de firewall), y en el caso fallido no coincidía con la regla.
Salió por la puerta de enlace por defecto en su lugar, llegó a un router que lo rechazó y el colector reportó «No route to host.»
La solución no fue «reinicia Docker». Fue añadir una regla de enrutamiento por origen para las subredes de contenedores en la clase afectada de nodos y hacerla persistente en el mismo
sistema que gestionaba las rutas de la VPN. Tras eso, los contenedores se comportaron como el host porque la política de red realmente se les aplicó.
Acción postmortem que importó: siempre que un host use enrutamiento basado en origen, los CIDR de contenedores se tratan como orígenes de primera clase en el diseño de enrutamiento. No son especiales,
no son un apéndice y no son «problema de otro».
2) La optimización que salió mal: «Apaguemos iptables de Docker por rendimiento.»
Un equipo de seguridad y un ingeniero orientado al rendimiento acordaron un cambio: poner "iptables": false para que Docker no mutara el firewall, y reemplazarlo con un conjunto
de reglas nftables gestionado centralmente. La propuesta sonaba bien: menos partes móviles, evaluación de reglas más rápida, mejor cumplimiento.
El despliegue pareció ir bien en un puñado de nodos ejecutando servicios HTTP simples. Semanas después, otra clase de servicio—contenedores que necesitaban alcanzar bases de datos internas
por rutas no por defecto—empezó a fallar durante una recarga rutinaria del firewall. Las apps registraban timeouts y ocasionalmente No route to host. Los ingenieros alternaron reinicios de contenedores,
reinicios de hosts e incluso reemplazaron nodos. Los síntomas se movían.
El fallo fue sutil: el conjunto de reglas gestionado centralmente tenía accepts en FORWARD, pero el NAT era incompleto para uno de los pools de bridge definidos por el usuario. Bajo ciertas combinaciones de origen/destino,
los paquetes salían con orígenes 172.x inroutables. Algunos routers upstream rechazaban rápido (aparecía como «no route»), otros los dejaban caer (timeouts). Tras una recarga, el orden de cadenas también cambió y brevemente
colocó un REJECT por delante del accept.
La solución no fue «vuelve a activar iptables» (aunque eso habría funcionado). En su lugar codificaron el NAT y el comportamiento de forward para cada pool de direcciones en la configuración nftables,
añadieron pruebas de regresión que validaban la presencia de reglas tras la recarga y estandarizaron los CIDR de bridge en la flota para reducir la deriva.
Lección: puedes asumir perfectamente la propiedad del firewall. Pero una vez que quitas el volante a Docker, no puedes sorprenderte cuando te estampes contra el NAT.
3) La práctica aburrida pero correcta que salvó el día: «Escribimos un runbook y aplicamos invariantes.»
Un equipo cercano a pagos ejecutaba una flota de hosts Docker con mezcla de servicios legacy y contenedores nuevos. Su entorno incluía firewalld, prevención de intrusiones local y un cliente VPN corporativo
que a veces actualizaba rutas. En otras palabras: un generador perfecto de tormentas.
Tras una interrupción desagradable implementaron una práctica poco emocionante: cada nodo al arrancar ejecutaba una comprobación de salud que validaba un puñado de invariantes—ip_forward=1,
que la cadena FORWARD contenga accepts para los bridges de Docker, que exista NAT masquerade para cada pool configurado, que rp_filter esté en loose en las interfaces VPN implicadas, y que los CIDR de bridge de Docker
no se solapen con rutas del sitio. La comprobación emitía una sola línea de estado y sacaba el nodo de rotación si alguna invariante fallaba.
Meses después, una actualización de imagen base cambió un perfil sysctl que deshabilitó el reenvío. La mitad de la flota habría quedado inútil. En su lugar, la comprobación de salud marcó los nodos inmediatamente después del reboot,
antes de que tomaran tráfico de producción. El equipo ajustó el drop-in de sysctl, desplegó y los usuarios ni se enteraron.
Nadie recibió aplausos por «verificamos sysctls». Está bien. La corrección aburrida es como compras fines de semana.
Errores comunes: síntomas → causa raíz → solución
1) Síntoma: “No route to host” solo desde contenedores; el host funciona
Causa raíz: net.ipv4.ip_forward=0 o reenvío deshabilitado vía baseline de sysctl.
Solución: Habilita el reenvío persistentemente con archivos /etc/sysctl.d/*.conf y aplica con sysctl --system.
2) Síntoma: fallo inmediato, no timeout; contadores de iptables suben en un REJECT
Causa raíz: La cadena DOCKER-USER contiene un REJECT (a menudo «bloquear redes privadas desde contenedores»).
Solución: Reemplaza por una política basada en allowlist, o delimítala por subred/puerto; verifica contadores de hits.
3) Síntoma: funciona hasta que firewalld recarga; luego los contenedores pierden salida
Causa raíz: Se están limpiando reglas NAT/forward; las reglas de Docker no se reinsertan o se sobreescriben.
Solución: Integra los bridges de Docker en zonas de firewalld y configura masquerade/forwarding en firewalld, o asegura que Docker pueda gestionar reglas.
4) Síntoma: solo ciertos destinos fallan (especialmente vía VPN); “No route” intermitente
Causa raíz: Reglas de enrutamiento por políticas no coinciden con el CIDR de origen del contenedor; el tráfico sale por la interfaz equivocada y es rechazado.
Solución: Añade ip rule para los CIDR de contenedores a la tabla de enrutamiento correcta; persiste vía el gestor de red.
5) Síntoma: SYN sale del host, no hay respuestas; curl desde host funciona
Causa raíz: Falta MASQUERADE; upstream no sabe enrutar la subred 172/10 de contenedores de vuelta.
Solución: Añade NAT para el pool de contenedores, o implementa subredes de contenedores enrutadas con rutas upstream.
6) Síntoma: respuestas llegan por otra interfaz; conntrack muestra drops; hay VPN
Causa raíz: rp_filter estricto descarta tráfico de retorno asimétrico.
Solución: Establece rp_filter en loose (2) para all/default y para interfaces clave, o haz el enrutamiento simétrico.
7) Síntoma: los contenedores alcanzan algunas subredes privadas pero no otras; “Network is unreachable”
Causa raíz: Colisión de rutas (el bridge de Docker se solapa con la red real), causando selección de ruta incorrecta.
Solución: Cambia los pools de direcciones de Docker (bip, default-address-pools) a rangos no solapados y recrea las redes.
8) Síntoma: tras upgrade del SO, las reglas aparecen en iptables pero no se aplican (o al revés)
Causa raíz: Mismatch de backend (iptables-legacy vs iptables-nft); estás debugueando el plano equivocado.
Solución: Confirma el backend con alternatives; usa nft list ruleset cuando nft esté activo; estandariza en la flota.
Listas de verificación / plan paso a paso
Paso a paso: Arreglar “No route to host” de contenedores de forma durable
-
Reproduce dentro del contenedor: ejecuta
ip route, luego una conexión TCP mínima al IP:puerto exacto. Anota si es “No route” o timeout. -
Identifica la subred del contenedor y el bridge: via
docker inspectyip link. Anota el nombre del bridge y el CIDR. -
Verifica el reenvío del host: comprueba
net.ipv4.ip_forward. Si está apagado, habilítalo persistentemente con un drop-in sysctl. - Revisa la política FORWARD y DOCKER-USER: busca defaults en DROP sin reglas accept, y REJECTs explícitos. Usa contadores para probar coincidencias.
- Confirma NAT (si esperas NAT): verifica POSTROUTING MASQUERADE para la subred del contenedor. Decide: NAT o enrutado. Elige uno.
-
Revisa el enrutamiento al destino desde el host:
ip route get <dest>. Si usa VPN o camino no por defecto, revisa reglas de enrutamiento por políticas. - Comprueba rp_filter: si eres multi-homed o tienes VPN, ponlo en loose donde corresponda, persistentemente.
- Integra el gestor de firewall: si hay firewalld/ufw, asigna bridges de Docker a una zona y configura masquerade/forwarding en ese sistema.
- Vuelve a probar y captura paquetes una vez: tcpdump en bridge y egress para confirmar dónde se detienen los paquetes.
- Haz que perdure: codifica los cambios en gestión de configuración (sysctl.d, perfiles de NetworkManager, configuración permanente de firewalld, docker daemon.json).
Checklist operacional: invariantes que vale la pena monitorizar
net.ipv4.ip_forward=1en hosts con contenedores- La cadena FORWARD acepta tráfico bridge→egress y retorno established
- La cadena DOCKER-USER no contiene REJECTs amplios sin excepciones explícitas
- Mascarada NAT existe para cada pool de contenedores cuando se usa bridge networking con NAT
- Los CIDR de contenedores no se solapan con subredes corporativas/privadas o rutas VPN
- rp_filter configurado apropiadamente para diseños de enrutamiento multi-homed
- Única fuente de verdad para el firewall (Docker + DOCKER-USER, o firewalld/nftables central — pero no una pelea)
Preguntas frecuentes
¿Por qué obtengo “No route to host” en lugar de un timeout?
Porque algo está rechazando activamente o declarando al host inalcanzable (REJECT de iptables, ICMP unreachable de un router, fallo de vecino). Los timeouts suelen indicar DROP.
Los contenedores pueden llegar a Internet pero no a una subred privada. ¿Qué tiene de especial los rangos privados?
Los rangos privados a menudo implican enrutamiento por políticas (VPN), políticas explícitas de firewall o CIDR solapados. Internet suele ser solo ruta por defecto + NAT; las redes privadas son donde
empieza lo «especial» de tu organización.
¿Es buena idea deshabilitar la gestión de iptables de Docker?
Solo si estás listo para asumir totalmente NAT/reenvío/reglas de aislamiento y probarlas tras recargas y upgrades. Está bien en entornos disciplinados; es caos en entornos ad hoc.
¿Cuál es la forma más rápida de probar que es el firewall?
Revisa DOCKER-USER y contadores de FORWARD mientras reproduces el fallo. Si los contadores aumentan en una regla REJECT/DROP, tienes prueba sin debate filosófico.
¿Cambiar el CIDR del bridge de Docker requiere recrear contenedores?
Normalmente sí. Las redes y contenedores existentes fueron creados con los pools antiguos. Planea una migración: drena cargas, recrea redes, redeploy.
¿Cómo se manifiesta rp_filter como un problema de contenedor?
Los contenedores son tráfico reenviado. Si las respuestas vuelven por una interfaz inesperada, rp_filter estricto puede descartarlas. La app lo experimenta como inalcanzable o con timeouts.
¿Es diferente para macvlan o host networking?
Sí. macvlan evita el bridge de Docker y el modelo NAT y depende de la vecindad L2 y del comportamiento del switch upstream; host networking evita el enrutamiento del namespace pero aún enfrenta
el firewall/política de enrutamiento del host. Las causas de «No route» cambian, pero el enfoque de depuración (rutas, reenvío, firewall, rp_filter) sigue siendo relevante.
Estoy en nftables. ¿Debería dejar de usar comandos iptables por completo?
Usa lo que coincida con la aplicación. Si iptables es el wrapper de nft, los comandos iptables suelen funcionar pero pueden ocultar la estructura específica de nft. En caso de duda,
inspecciona ambos y estandariza en la flota para reducir confusión.
¿Por qué funciona justo después de reiniciar Docker y luego se rompe más tarde?
Porque algo más reaplica políticas de firewall o sysctl después de que Docker arranque (recarga de firewalld, agente de seguridad, cloud-init, gestión de configuración). Arregla la propiedad y el ordering.
Conclusión: próximos pasos que reducen el dolor futuro
“No route to host” desde contenedores rara vez es un misterio de Docker y casi siempre una verdad de redes Linux que aún no has documentado. La solución durable no es un comando mágico;
es hacer explícitas y persistentes tus intenciones de enrutamiento y firewall.
- Elige CIDR de contenedores no solapados y estandarízalos temprano.
- Decide NAT vs contenedores enrutados e impléméntalo de forma consistente.
- Haz explícitos y persistentes el reenvío, la postura de rp_filter y las reglas de enrutamiento por políticas.
- Elige un dueño del firewall (Docker+DOCKER-USER, o firewalld/nftables) e integra en lugar de competir.
- Automatiza comprobaciones de invariantes para que «funcionaba hasta el reinicio» deje de ser un género recurrente.
Si solo haces una cosa: añade una comprobación de salud que valide reenvío, presencia de NAT y política DOCKER-USER tras cada arranque y recarga de firewall. Es aburrido. Es correcto.
Te salvará después.