Cómo enrutar solo el tráfico de contenedores Docker por OpenVPN
Deje el host en su enlace habitual y meta una sola subred de contenedores en el túnel: orden de NAT, reglas de origen, iroute de OpenVPN y comprobaciones de MSS.
El paquete del contenedor nunca llega a tun0, y las capacidades adicionales del contenedor no lo cambiarán. Entre el invitado y el túnel hay dos consultas de ruta: una dentro del espacio de nombres de red del invitado, otra en el host después de que la trama aterrice en el puente, y la segunda consulta una tabla distinta con reglas de coincidencia distintas. Mi disposición de referencia es un host Linux con un OpenVPN cliente que mantiene 10.8.0.10 en tun0, un puente Docker en 172.18.0.0/16, y una dirección privada a la que solo puede llegar el lado del túnel. El objetivo sigue siendo estrecho: esa subred invitada sale por el túnel mientras el host conserva su enlace habitual.
Ejecutar el cliente VPN dentro del contenedor es otro diseño, no un atajo. El contenedor pasa a poseer su túnel y sus credenciales, y el trabajo se traslada a un Dockerfile en lugar de quedarse en iptables.
El reenvío elige una ruta por destino, y luego NAT reescribe el origen
Con net.ipv4.ip_forward=1 configurado, un paquete reenviado se enruta solo por destino, en la tabla principal. El origen no participa en esa decisión. Solo después de elegir la ruta el núcleo ejecuta POSTROUTING, y ahí es donde está la regla propia de Docker: -s 172.18.0.0/16 ! -o br-appnet -j MASQUERADE. Todo lo que sale del puente hacia otra interfaz se traduce, y la dirección en la que se convierte depende de la interfaz a la que la ruta lo envió.
Esa regla explica la mayoría de los síntomas por los que me preguntan. Si la tabla principal no tiene ruta para el destino privado, el paquete sale por el enlace con la dirección física del host. Su proveedor descarta el destino, o el sitio remoto descarta un origen RFC 1918 que nunca esperó, y el contenedor agota el tiempo mientras el túnel sigue levantado y sano. Una captura en tun0 no muestra nada, y esa es la señal. Si la ruta sí apunta a tun0, MASQUERADE reescribe el origen a 10.8.0.10 y el mismo contenedor funciona sin que usted toque nada.
Pida la consulta que nombra el origen del invitado. Sin from y iif el host responde por su propio socket e informa de un camino que el paquete reenviado nunca tomará:
ip route get 192.168.11.222 from 172.18.0.5 iif br-appnetSi esa respuesta nombra el enlace, el túnel no está en la imagen y ningún ajuste del lado del túnel ayudará.
Lleve al invitado a la dirección propia del túnel
El cambio mínimo que funciona es una reescritura explícita del origen a la salida. Insértela en lugar de añadirla: Docker ya está a la cabeza de POSTROUTING, así que una regla añadida con -A se evalúa detrás de la de Docker y nunca se dispara.
iptables -t nat -I POSTROUTING 1 -s 172.18.0.0/16 -o tun0 -j SNAT --to-source 10.8.0.10Dos condiciones deciden si esto se sostiene. La dirección debe ser una que el cliente realmente tenga, así que fíjela con una entrada por cliente que contenga ifconfig-push 10.8.0.10 255.255.255.0; una regla que nombra una dirección que nunca se asignó produce descartes silenciosos, no un error. Y el lado remoto debe aceptar ese origen y enrutar las respuestas de vuelta, por eso una lista de permitidos allí nombra la dirección del túnel y no la subred invitada. Cuando la dirección del cliente es dinámica, -j MASQUERADE con -o tun0 sigue lo que tenga la interfaz, a costa de una política remota contra la que no puede escribir un valor fijo.
El filtrado es la otra mitad. Docker pone la política FORWARD en drop y vacía las cadenas que posee en cada reinicio del demonio, así que ponga la política local en DOCKER-USER, que sobrevive:
iptables -I DOCKER-USER -i br-appnet -o tun0 -j ACCEPT
iptables -I DOCKER-USER -i tun0 -o br-appnet -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPTLa regla de vuelta importa: una respuesta que llega a tun0 para una conexión abierta por el puente no se acepta solo porque la dirección de salida lo hiciera.
Mantenga el host directo con una regla de origen
Cuando todo el puente debe usar el túnel pero nada más del host debe hacerlo, sáltese NAT y dé a ese origen su propia tabla. Las direcciones invitadas quedan entonces visibles de extremo a extremo.
echo "200 vpnbox" >> /etc/iproute2/rt_tables
ip rule add from 172.18.0.0/16 lookup vpnbox pref 1000
ip route add default dev tun0 table vpnboxLas reglas se consultan antes de la tabla principal, y from coincide con el origen. Los paquetes externos propios del túnel llevan el origen físico del host, así que nunca coinciden con from 172.18.0.0/16 y el túnel no puede enrutarse a sí mismo de vuelta. Esa autoexclusión es lo que hace estable la disposición.
El camino de vuelta ahora necesita ayuda. El lado remoto ve una 172.18.0.x sin traducir y no tiene motivo para saber dónde vive. En el servidor, un archivo por cliente necesita iroute 172.18.0.0 255.255.0.0 para que la tabla interna del servidor entregue ese prefijo a este cliente, y un route 172.18.0.0 255.255.0.0 empujado para que los demás clientes sepan que existe.
Dos comprobaciones antes de confiar en ello. Ejecute ip route show default: con redirect-gateway def1 la tabla tiene 0.0.0.0/1 y 128.0.0.0/1 vía tun0, y el host no es directo en absoluto, porque esos medios predeterminados ganan sin reemplazar default. Las reglas añadidas con ip rule add también desaparecen al reiniciar, así que llévelas en un script up.
El tamaño del paquete es el segundo fallo que encontrará
Una interfaz de puente a 1500 bytes da al túnel más de lo que puede llevar una vez añadidas las cabeceras UDP e IP externas. Los intercambios pequeños pasan, así que DNS resuelve y el apretón de manos TLS se completa, y luego una subida mayor se cuelga sin ningún error en ninguna parte. El emisor sigue reintentando el mismo tamaño de segmento porque el mensaje ICMP de necesidad de fragmentación, que le diría que reduzca, nunca llega.
Arreglelo en la interfaz, en el MSS o en ambos. El clavado no necesita la colaboración del invitado:
ip link set dev tun0 mtu 1400
iptables -t mangle -A FORWARD -o tun0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtuEl clavado solo reescribe TCP. UDP, QUIC incluido, necesita en cambio una interfaz invitada más baja, con --opt com.docker.network.driver.mtu=1400 al crear la red o la clave de compose correspondiente.
DNS y las comprobaciones que vale la pena repetir
La resolución de nombres sigue el mismo camino. El resolutor integrado de Docker en 127.0.0.11 reenvía a los servidores configurados del host, así que un contenedor hereda lo que use el host. Cuando la zona privada solo la responde un resolutor alcanzable por el túnel, apunte al invitado a esa dirección, y pruebe con getent hosts dentro del contenedor en lugar de en el host.
- Ejecute la petición que falla dentro del invitado; una prueba en el lado del host usa otra dirección de origen y esconde el problema.
- Lea los contadores de las reglas NAT y de filtrado que añadió. Una regla sin coincidencias es un error de enrutamiento, no de política.
- Confirme que la dirección del túnel en su regla SNAT es una que el cliente realmente tiene.
- Baje la MTU del invitado o clave el MSS antes de culpar al servidor de un cuelgue.
Recorra esa lista y deténgase en el primer fallo. Esa capa es la que hay que arreglar; cualquier cosa añadida más abajo solo esconde la frontera que se rompe.