Fijar un solo programa a una VPN en Linux es sobre todo un problema de enrutamiento, y la regla del cortafuegos solo decide qué ocurre después de elegir la ruta. Si haces coincidir el UID del programa en OUTPUT antes de que el túnel tenga la ruta predeterminada, la regla descarta justo el tráfico que querías proteger. La misma regla tampoco ve el proceso del resolutor que responde a las consultas DNS del programa. Ordena bien las cosas y una sola cuenta sin privilegios se convierte en un selector fiable del túnel dividido.

Dale a la cuenta una ruta antes del veredicto

La coincidencia de propietario responde una pregunta: ¿construyó el paquete un socket local perteneciente a este UID? No elige interfaz y no puede mover el tráfico que la tabla principal ya envía por la NIC física. Por eso una regla UID sin ruta correspondiente bloquea el programa en lugar de redirigirlo.

Envía la cuenta primero por su propia tabla y luego deja que el cortafuegos confirme el resultado. Esto funciona en un host cuyo iproute2 entiende uidrange, lo que ip rule help informa en una línea. La tabla 100 contiene una ruta predeterminada por el túnel, y la regla envía solo el UID 1000 a esa tabla. Todo lo demás sigue en la tabla principal y sigue usando el enlace físico.

ip rule add uidrange 1000-1000 table 100
ip route add default dev tun0 table 100

Una segunda cuenta que necesita otro túnel recibe la tabla 101 y su propia regla, porque dos perfiles no pueden poseer ambos la ruta predeterminada en la misma tabla. Los perfiles de NetworkManager son de todo el sistema, así que esta parte sigue siendo manual, y las reglas desaparecen al reiniciar salvo que algo las restaure. Escribe las tablas en un script de arranque junto a la definición del túnel para que una reconexión no pueda dejar una cuenta en el camino equivocado.

Por qué la excepción de loopback mata el túnel

La mayoría de las versiones copiadas de la regla descartan cada paquete que no sale por loopback. El túnel no es loopback, así que esa forma también descarta el tráfico del túnel, y el resultado parece un VPN muerto más que un error del cortafuegos.

Cuando se ejecuta OUTPUT, el núcleo ya eligió una interfaz de salida, así que -o se compara con la respuesta de la ruta y no con la cabecera del paquete. La coincidencia de propietario solo se consulta en OUTPUT y POSTROUTING, y los paquetes reenviados no llevan ningún socket para que la lea. Esa combinación significa que la excepción debe nombrar la interfaz del túnel y negar esa coincidencia, en vez de negar loopback.

Loopback todavía merece su propio ACCEPT por encima del DROP, porque el primer paquete DNS del programa va al stub local a través de lo. Pon las dos reglas en este orden y la cuenta llega al stub, luego al túnel, y a ningún otro sitio.

iptables -I OUTPUT 1 -o lo -m owner --uid-owner vpnapp -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner vpnapp ! -o tun0 -j DROP

IPv6 es un conjunto de reglas aparte, y un par solo para v4 deja ip6tables intacto. Un programa que prefiera registros AAAA saldría entonces por el enlace físico sin llegar nunca al DROP. Refleja ambas reglas en ip6tables, o desactiva IPv6 en el host cuando el túnel no lleve ninguna dirección v6. Las reglas añadidas a mano también desaparecen al reiniciar, así que guárdalas con netfilter-persistent save o reconstruye la misma lógica como una tabla nftables cargada por la unidad de arranque.

Las respuestas DNS vienen de otro proceso

La consulta del programa a 127.0.0.53 pasa el ACCEPT de loopback, y ahí es donde se detiene la coincidencia de UID. El resolutor stub reenvía hacia arriba desde su propia cuenta de servicio, así que una regla vinculada a vpnapp nunca ve la segunda consulta. Si el resolutor elige un servidor LAN para el enlace, las respuestas vuelven por la NIC física mientras los paquetes propios del programa se quedan en el túnel. El resultado visible es un túnel que funciona con nombres rotos, o nombres respondidos por un servidor que no elegiste.

Corrige la resolución de nombres en el enlace del túnel, no en la aplicación. Da al perfil del túnel un servidor DNS y un dominio de enrutamiento, y luego confirma con resolvectl query y resolvectl status tun0 que el resolutor lo usa para los nombres que importan. Dentro de un espacio de nombres de red el problema desaparece, porque el resolutor del espacio solo conoce el túnel.

Comprueba la regla desde la propia cuenta

Pregunta al núcleo qué ruta recibe un UID, luego haz una petición como esa cuenta y compara la dirección de salida con la de tu propia cuenta. Si ambas informan la misma dirección, la regla coincide con algo distinto del tráfico que probaste. El primer comando de abajo muestra la consulta de ruta sin enviar un paquete, lo que separa un error de enrutamiento de un error de cortafuegos.

ip route get 1.1.1.1 uid 1000
sudo -u vpnapp curl -s https://ifconfig.co

Los contadores muestran si una regla se disparó alguna vez. Ejecuta iptables -vnL OUTPUT, lee el recuento de paquetes junto a cada regla y trata un cero junto al DROP después de tráfico real como un error de coincidencia.

El tráfico reenviado queda fuera de este diseño. Un contenedor o una VM detrás de un puente no tiene socket local propiedad de vpnapp, así que la coincidencia de propietario nunca se dispara y esos paquetes siguen la política FORWARD. Si esa carga debe cubrirse, pruébala por separado y escribe una política vinculada al puente o a la dirección del contenedor. El diseño aguanta cuando la tabla, la excepción de interfaz, el resolutor y las reglas v6 apuntan todos en la misma dirección; deja fuera cualquiera de ellos y la cuenta o filtra o pierde conectividad.

Artículos relacionados