El cliente VPN se conecta, el navegador funciona, y el único comando que de verdad te importa sigue saliendo por tu conexión de casa. O pasa lo contrario: el túnel se queda con la ruta por defecto y el servidor de archivos local desaparece del escritorio. Me topé con esto en un agente de compilación que tenía que llegar a un espejo interno de artefactos mientras el resto del host se quedaba en la LAN de la oficina, y el cliente ofrecía exactamente dos opciones: todo el tráfico o nada. La petición en ambos sentidos es la misma, este programa y no toda la máquina, y la mayoría de los clientes no tienen casilla para ello, porque la tabla de rutas nunca ha sabido qué es un programa.

Por qué la tabla de rutas no puede ver tu programa

Cuando un proceso envía un paquete, la pila busca la dirección de destino en la tabla de rutas. Ni el nombre del ejecutable ni el puerto entran en esa búsqueda. Los nombres de dominio fallan por la misma razón: un túnel lleva el tráfico a una dirección, y para cuando un nombre se resuelve en una, el enrutamiento es la única palanca que le queda al núcleo. Por eso «VPN solo para este sitio» acaba significando un proxy, y «VPN solo para este programa» acaba significando uno de un pequeño conjunto de mecanismos.

MechanismWhat selectsCoversOn tunnel drop
Socket bindsource addressa client you controlconnections fail
UID rule (Linux)owning userevery process of one accountconnections fail
Firewall profile (Windows)adapter profileone programfails closed
Network namespacenamespace membershipeverything started insideconnections fail
SOCKS hopapp proxy settingone app or one browser profileconnections fail

Nada de esto es una ruta por proceso. Cada uno traslada la decisión a una capa que puede ver el programa, el usuario o la propia configuración de la aplicación.

Un solo destino sigue siendo una ruta, no una regla de programa. En macOS la receta habitual es desmarcar «Enviar todo el tráfico por VPN» y añadir una ruta de host: sudo route add -host internal-tuner.example -interface ppp0, que se quita otra vez con sudo route delete -host internal-tuner.example -interface ppp0. Lea primero el nombre de la interfaz en ifconfig, porque ppp0 es solo un ejemplo. Los comodines no funcionan en una ruta de host, así que un servicio que habla con varios nombres necesita una entrada por nombre, y la regla cubre todos los procesos de la máquina, no el que tenía en mente.

Vincule el socket a la dirección del túnel

Cuando controlas el cliente, vincular es el selector más barato. Un socket vinculado a la dirección local del túnel sale por esa interfaz mientras exista allí una ruta hacia el destino, y todo lo demás de la máquina conserva su camino normal. La sutileza es que la vinculación elige la dirección de origen mientras la tabla de rutas sigue eligiendo la salida. La vinculación por sí sola no mueve el tráfico que no tiene ruta hacia el túnel, y ese es el error detrás de la mayoría de los informes de «lo vinculé y sigue saliendo por el lado equivocado».

import socket
s = socket.socket()
s.bind(("10.8.0.2", 0))
s.connect(("build.internal.example", 443))

Las herramientas de más alto nivel exponen la misma idea: curl --interface 10.8.0.2, CURLOPT_INTERFACE en PHP y el argumento source_address en los clientes HTTP de Python. La dirección tiene que pertenecer al túnel, y una errata aquí falla como un error de conexión corriente, no como una queja de enrutamiento. Vincular a un nombre de dispositivo en lugar de a una dirección es un privilegio de Linux y no se traslada a Windows ni a macOS, por eso los scripts portátiles pasan la dirección.

Linux: haga coincidir al propietario, no el nombre del programa

El núcleo puede hacer coincidir al usuario propietario de un socket, así que una cuenta dedicada se convierte en el selector. Ponga el túnel en su propia tabla de rutas y envíe allí solo el tráfico de esa cuenta:

ip rule add uidrange 1001-1001 lookup 100
ip route add default dev tun0 table 100

Esto solo funciona si el túnel no es ya la ruta por defecto. Inicie el cliente con --route-nopull, o filtre la redirección empujada con --pull-filter ignore "redirect-gateway" en OpenVPN 2.4 y posteriores, y luego añada usted mismo la entrada en la tabla. Si no, existen las dos rutas por defecto y gana la de menor prioridad. La versión deliberada de eso es apuntar la ruta por defecto a la puerta de enlace del túnel, o hacerlo desde un script de desconexión: mantiene el tráfico fuera de internet cuando el túnel cae, pero eso es un interruptor de seguridad, no un selector, y se queda hasta que alguien lo quita.

La versión más fuerte es un espacio de nombres de red. Créelo, ejecute el cliente dentro y arranque la aplicación con ip netns exec. Nada fuera del espacio de nombres ve la interfaz del túnel, así que la aplicación no puede volver por accidente a la ruta del host. El coste es real: el espacio de nombres necesita un par veth para llegar a la red física y su propio archivo de resolución, porque el espacio de nombres de montaje no comparte los ajustes de resolución del host.

Dos modos de fallo explican la mayoría de las fugas. La resolución de nombres sigue ocurriendo en el resolutor del host, así que la consulta sale fuera del túnel incluso cuando la conexión TCP entra dentro. Y IPv6 necesita su propia ip -6 rule, porque una regla v4 no la cubre.

Windows y macOS: acote el cortafuegos o ceda un proxy

Windows tampoco tiene selector de ruta por aplicación, pero sus reglas de cortafuegos pueden acotarse a un perfil, y un adaptador VPN puede clasificarse como «público» mientras la LAN de casa sigue «privada». Cree reglas de bloqueo de salida para el programa en los perfiles «privado» y «dominio», deje «público» abierto, y la aplicación solo podrá llegar a la red por el túnel. Fije primero la categoría de red de la conexión y luego confirme que la regla coincidió en el panel de supervisión del cortafuegos. Es un bloqueo, no una ruta: falla cerrado cuando el túnel cae, y no puede detener una aplicación que ignora el cortafuegos.

macOS sigue la misma lógica con otras herramientas. Un oyente SOCKS alcanzado por SSH es la opción menos invasiva, porque ssh -D 1080 user@host crea un puerto local y solo las aplicaciones configuradas para usarlo toman ese camino. Una regla pf puede bloquear un programa por usuario o grupo, pero sigue sin poder elegir el túnel solo para ese programa. Tunnelblick no puede filtrar por dominio en absoluto, porque las rutas trabajan con direcciones; los usuarios de OpenConnect recurren a vpn-slice cuando el servidor empuja una lista de prefijos demasiado amplia.

Dos soluciones alternativas cubren lo que las herramientas de la plataforma pasan por alto. Las extensiones de navegador que cambian el proxy por dominio son la respuesta práctica para «solo este sitio», porque actúan en la capa de aplicación, donde los nombres aún existen; cubren un navegador y nada más. La solución tosca es ejecutar el cliente del proveedor dentro de una máquina virtual para que el invitado sea dueño del cambio de ruta mientras el anfitrión conserva su camino, a costa de dos pilas de red que mantener.

Un salto SOCKS para herramientas sin opción de vinculación

La mayoría de las aplicaciones pueden apuntarse a un proxy HTTP o SOCKS incluso cuando no saben nada de interfaces. Un cliente SSH proporciona uno sin ningún cambio en el lado del servidor:

ssh -N -D 127.0.0.1:9999 [email protected]

Todo lo configurado para usar ese puerto sale en el bastión mientras el resto del escritorio conserva su conexión normal. Una instancia local de 3proxy vinculada a la dirección del túnel hace lo mismo para un puerto sin SSH, y stunnel cubre un único puerto envuelto en TLS. Los límites son prácticos: un proxy solo transporta protocolos que saben usarlo, UDP necesita una asociación UDP de SOCKS5 que el ajuste «SOCKS5» de muchos clientes no activa, y el DNS debe resolverlo el proxy (socks5h en curl) o el nombre se filtra al resolutor local.

Mantenga el oyente en loopback salvo que otra máquina lo necesite.

Verifique el camino con una captura, no con un mensaje de estado

Las ventanas de estado de la aplicación describen el ajuste, no el paquete. Capture en ambas interfaces y compare:

tcpdump -i any -n port 443
ss -tnp | grep 443

Si la conexión aparece en la interfaz física sin una entrada correspondiente en el túnel, el selector no está en efecto, diga lo que diga el cliente. Compruebe IPv6 y el resolutor por separado, y busque un segundo camino como un proceso auxiliar o un proxy de sistema heredado.

Luego elija la frontera que encaje con el requisito: un proxy basta para un sitio, una regla de UID basta para una cuenta, un perfil de cortafuegos basta cuando la conexión debe fallar cerrada, y un espacio de nombres es la única opción que esconde el túnel de todo lo que está fuera.

Artículos relacionados