Pasé buena parte de una mañana esta primavera persiguiendo un contenedor que no lograba resolver un solo nombre interno mientras el navegador de al lado abría la wiki sin quejarse. El túnel estaba activo, el navegador lo demostraba, y el contenedor se quedaba en otro espacio de nombres de red con otra tabla de rutas y otro resolvedor. Lo que sigue es el orden que compruebo ahora: primero las tablas de rutas, después la resolución, luego el reenvío y por último la plataforma anfitriona. Deténgase en el primer paso que ya falla, porque añadir reglas más abajo en la pila esconde la frontera en lugar de moverla.

Compare las tres tablas de rutas antes de cambiar una regla

Un contenedor en el puente predeterminado de Docker tiene una única ruta predeterminada que apunta a la puerta de enlace docker0 y no tiene ni idea de que tun0 existe. El host tiene tun0, las rutas que empujó la VPN y quizá una regla de política que envía el tráfico de prueba por una interfaz distinta de la que espera. WSL añade una tercera tabla detrás de su propio adaptador virtual. Imprima las tres antes de editar cualquiera de ellas.

ContextCommandWhat a wrong answer looks like
Hostip route get 10.8.0.20Leaves a physical interface instead of tun0
Containerip routeOnly 172.17.0.0/16 via 172.17.0.1
Container DNScat /etc/resolv.confA public resolver, or an upstream the tunnel never pushed
WSLip addr; ip routeOnly the WSL subnet, no Windows VPN adapter

Si la única ruta del contenedor es el puente y tun0 nunca aparece en su espacio de nombres, nada de lo que sigue ayudará: ningún cambio de servidor de nombres crea una ruta, y ninguna ruta crea la identidad de túnel que el par aceptará. El orden importa porque un fallo de DNS y un fallo de ruta son idénticos desde dentro de una shell.

Apunte el resolvedor al túnel en lugar de a uno público

En el host, resolvectl status enumera los servidores DNS por enlace, y readlink -f /etc/resolv.conf indica si el archivo es el stub de systemd, un archivo de NetworkManager o algo que reescribió el cliente de un proveedor. Dentro de un contenedor en una red definida por el usuario, /etc/resolv.conf nombra 127.0.0.11, el resolvedor integrado de Docker, que reenvía al upstream con el que se configuró el demonio. Un contenedor en el puente predeterminado recibió en cambio una copia del archivo del host cuando se creó, y la conserva.

Esa copia es la razón de que un túnel que se levanta después de arrancar el contenedor deje al contenedor consultando el upstream antiguo. Pase el resolvedor de la VPN explícitamente con docker run --dns 10.8.0.53, o establezca "dns": ["10.8.0.53"] en /etc/docker/daemon.json y reinicie el demonio, o vuelva a crear el contenedor una vez que el túnel esté activo. Recrearlo suele ser más barato que depurar un resolvedor que era correcto hace una hora.

Cambiar a un resolvedor público es la reparación que mantiene el síntoma. Resuelve internet y falla en todas las zonas privadas, y puede saltarse en silencio el filtrado que el túnel existe para imponer. Si intenta escribir dentro del /etc/resolv.conf de un contenedor en ejecución, espere Device or resource busy: Docker monta ese archivo con bind, así que la edición requiere primero un desmontaje. Establecer --dns al crear el contenedor es la secuencia que sobrevive a un reinicio.

Reenvíe y traduzca cuando los contenedores tomen prestado el túnel

Con el puente predeterminado, el host es un enrutador entre docker0 y tun0, y por defecto no hace ninguna de las dos tareas. Active el reenvío y traduzca la dirección de origen para que el par vea una dirección de túnel del host en lugar de una a la que no tiene ruta de vuelta (sustituya tun0 por el nombre que su cliente dé a la interfaz):

sysctl -w net.ipv4.ip_forward=1
iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE
iptables -A FORWARD -i docker0 -o tun0 -j ACCEPT
iptables -A FORWARD -i tun0 -o docker0 -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

Las reglas FORWARD importan en hosts cuya política de reenvío es DROP. Docker inserta sus propias cadenas, así que pruebe con un paquete en lugar de leer iptables -S: desde el contenedor, ip route get 10.8.0.20 debe nombrar la puerta de enlace del puente, y nc -vz 10.8.0.20 443 debe conectar o agotar el tiempo en el túnel en lugar de en el resolvedor.

Sin MASQUERADE el par necesita una ruta de vuelta a 172.17.0.0/16, que la mayoría de las VPN de acceso remoto no empujarán. Aquí es también donde un cliente basado en políticas rompe el plan: si solo captura el tráfico de UID o cgroups concretos, sus reglas de reenvío nunca ven el paquete dentro del túnel, y el arreglo pertenece a la política de enrutamiento del cliente y no a iptables. Las reglas añadidas a mano desaparecen al reiniciar, así que póngalas en una unidad de systemd o en una tabla de netfilter-persistent y vuelva a probar tras un reinicio, ya que un túnel que se levanta tarde puede vaciarlas.

Ponga el túnel dentro del contenedor cuando el objetivo sea el aislamiento

Si un contenedor tiene que salir por un nodo concreto, ejecute el cliente en ese contenedor. Necesita --cap-add NET_ADMIN y --device /dev/net/tun, y entonces posee su propio tun0, sus rutas y su cortafuegos. Si otros contenedores pasan a través de él, también necesita reenvío y una regla MASQUERADE dentro del contenedor, y esos pares necesitan una ruta o un proxy explícitos que apunten a él; el puente predeterminado de Docker no añade esa ruta por usted.

La red del host, docker run --network host, es la versión barata: el contenedor se une al espacio de nombres del host y ve las rutas del túnel directamente. También elimina el aislamiento de red, invita a colisiones de puertos y en la práctica es una opción de Linux. Un puerto publicado no es un sustituto. -p 8080:80 hace que un servicio sea alcanzable desde el host; no hace que las direcciones internas 10.x sean alcanzables desde dentro del contenedor, y confundir ambas cosas es cómo un problema de enrutamiento se convierte en un puerto de base de datos expuesto.

Distinga un fallo de nombres de una subred superpuesta

El puente predeterminado de Docker es 172.17.0.0/16, y muchos rangos de VPN corporativas están dentro de 172.16.0.0/12. Una vez que el túnel posee ese rango, el tráfico destinado a un contenedor puede enrutarse hacia el túnel. Los nombres fallan, el error parece de DNS y el problema son las direcciones. Compruebe el destino, no el resolvedor: ip route get 172.17.0.5 en el host, y luego compare el prefijo del puente con el rango de la VPN y con docker network inspect bridge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'.

La reparación es mover uno de los rangos, ya sea un rango de direcciones predeterminado distinto para Docker en daemon.json o un rango distinto en el concentrador. Añadir otro servidor de nombres no hace nada aquí, y tampoco lo hace ssh -b: vincular el cliente a una interfaz cambia la dirección de origen, no una ruta de destino que apunta hacia el túnel. Un reparto rápido de tareas: un nombre que se resuelve en el host pero no en el contenedor es un problema de resolvedor, mientras que una conexión IP directa que se cuelga en el contenedor pero funciona en el host es un problema de ruta o de cadena de reenvío.

WSL y Docker Desktop no siguen las reglas de Linux

En su modo NAT predeterminado, WSL está detrás de su propio adaptador virtual y no recibe las rutas internas ni los sufijos DNS de la VPN de Windows, que es por lo que el navegador funciona y ping dentro de la distribución no. La red reflejada, disponible en compilaciones recientes de Windows 11, comparte las interfaces del host, pero hay que habilitarla en .wslconfig y reiniciar la distribución con wsl --shutdown. Confirme el modo activo con wslinfo --networking-mode antes de aplicar un arreglo, porque la respuesta NAT es incorrecta para el modo reflejado y viceversa.

Docker Desktop añade otra capa. Los contenedores Linux en Windows están detrás de una implementación NAT llamada históricamente vpnkit, y su ruta difiere de la de un host Linux nativo. Los contenedores Windows se conectan mediante un conmutador virtual de Hyper-V, y un cliente corporativo puede bloquear el tráfico de ese conmutador, así que los contenedores Linux de la misma máquina pueden llegar a la red mientras los contenedores Windows no tienen ninguna. Un conmutador externo vinculado al adaptador físico es el remedio habitual, y cambia la identidad de red del contenedor de formas que la política puede no permitir.

Antes de escribir otra regla, recorra las cuatro capas en orden y deténgase donde una de ellas esté mal:

  • Dentro del contenedor: ¿ip route llega al prefijo privado, y /etc/resolv.conf nombra un upstream que lo resuelve?
  • En el host: ¿está activado el reenvío, está NAT vinculado a la interfaz del túnel y acepta la cadena de reenvío el tráfico del puente al túnel?
  • En el par: ¿conoce una ruta de vuelta, o confía en que NAT oculte la subred del contenedor?
  • En la plataforma: ¿está WSL en modo NAT o reflejado, y el contenedor es Linux o Windows?

Repita cada prueba en un contenedor o una shell nuevos. Un proceso de larga duración con una respuesta en caché o una conexión establecida seguirá fallando cuando la configuración ya sea correcta, y ese fallo se parece exactamente a un arreglo que no funcionó.

Artículos relacionados