Marcar el tráfico de un cgroup es la parte fácil. Tres decisiones separadas tienen que alinearse antes de que una sola aplicación salga por la interfaz física mientras todo lo demás permanece en el túnel: qué socket pertenece a la excepción, qué consulta de enrutamiento desencadena la marca y qué dirección de origen lleva el paquete cuando sale. La mayoría de los intentos fallidos aciertan la primera, aciertan a medias la segunda y nunca miran la tercera.

Tres decisiones, no una

La clasificación responde quién está exento. El enrutamiento responde adónde va en cambio el paquete marcado. La selección de origen responde qué verá el extremo remoto y adónde debe volver la respuesta.

Estas fallan de forma independiente. Un paquete puede estar clasificado, reenrutado a la interfaz física y aun así llevar una dirección privada que ningún router aguas arriba pueda devolver, lo que produce un tiempo de espera que parece exactamente un bloqueo de cortafuegos. Nada en el paso de clasificación te avisa de eso. Nada en el paso de enrutamiento tampoco.

El orden también importa. La clasificación ocurre cuando se genera el paquete, el enrutamiento ocurre después y la traducción ocurre en el punto de salida.

Si no puedes nombrar cuál de las tres etapas está rota, no estás depurando, estás reordenando reglas y esperando.

Haz coincidir un cgroup que siga existiendo mañana

nftables puede comprobar la pertenencia de un socket a cgroup v2, lo cual es realmente útil porque sigue al proceso y no al destino. La trampa es el identificador. Una aplicación de escritorio lanzada desde un menú suele vivir en un scope transitorio cuyo nombre cambia en el siguiente lanzamiento.

nft list ruleset
systemctl status session.slice 2>/dev/null
cat /proc/self/cgroup

Copiar el nombre de un scope de una sesión y codificarlo de forma fija produce una regla que funciona hoy y mañana deja de coincidir en silencio. Coloca la aplicación, en cambio, en un contexto de ejecución deliberado y estable, como un scope o una unit dedicada que controles, y haz que coincida con eso. Así la regla sobrevive a reinicios y actualizaciones de paquetes. También se convierte en algo sobre lo que un colega puede razonar sin tu historial de sesiones.

Sé también preciso sobre qué mecanismo estás usando. La clasificación net_cls de cgroup v1 y la coincidencia de socket de cgroup v2 son cosas distintas, y un campo con un nombre parecido en la documentación no las hace intercambiables.

Es una cadena de tipo route la que desencadena la segunda consulta

Para el tráfico generado localmente, el tipo de cadena decide si ocurre algo después de poner la marca. Una cadena de salida de tipo filter puede marcar un paquete y el núcleo seguirá usando la ruta que ya eligió. Una cadena de tipo route es el mecanismo que pide una consulta nueva.

table inet bypass {
  chain mark {
    type route hook output priority mangle; policy accept;
    socket cgroupv2 level 2 "app.slice" meta mark set 0x1 comment "app bypass"
  }
}
ip rule add fwmark 0x1 lookup 200
ip route add default via 203.0.113.1 dev wlan0 table 200

La marca necesita entonces una regla de política que la envíe a una tabla, y esa tabla debe estar completa: una ruta por defecto, cualquier destino local que la aplicación siga necesitando y una puerta de enlace aguas arriba que realmente responda.

Elige el valor de la marca con intención. WireGuard usa su propio fwmark para el tráfico de transporte exterior, y aterrizar por accidente en el mismo número puede interferir con la excepción de enrutamiento del propio túnel de formas que son dolorosas de diagnosticar.

La dirección de origen no sigue a la ruta

Un socket que ya eligió una dirección de origen para la ruta del túnel conserva esa dirección tras el reenrutamiento. Nada en el cambio de enrutamiento la reescribe, por eso el paquete sale por la interfaz física con un origen privado que la red aguas arriba no puede devolver.

El síntoma visible es una aplicación que parece enviar datos y nunca recibe respuesta, y un registro de cortafuegos que no muestra nada porque el paquete nunca volvió.

La corrección estrecha es una regla de traducción de origen limitada a la marca de esa aplicación y a su interfaz de salida real, integrada con el NAT que ya exista en lugar de sustituirlo:

table inet bypass {
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;
    meta mark 0x1 oifname "wlan0" masquerade
  }
}

Traduce la excepción y nada más. Una regla masquerade general arregla el síntoma y destruye tu capacidad de decir qué tráfico tomó qué camino. Haz coincidir la marca, la interfaz de salida real y, si puedes, la dirección propia de la aplicación. Las reglas amplias son fáciles de escribir y casi imposibles de auditar después.

Qué sigue escapándose de la excepción

La resolución de nombres puede venir de un proceso resolver separado en un cgroup distinto, así que clasificar el socket del navegador no clasifica sus consultas. Si las consultas del resolver se quedan en el túnel mientras la conexión sale fuera de él, las respuestas split-horizon pueden no coincidir con el camino. Esa discrepancia produce fallos que parecen errores de DNS y no lo son.

Los procesos hijos heredan el cgroup en la mayoría de configuraciones, pero un helper que se mueve a su propio scope no lo hace. Comprueba en lugar de suponer, sobre todo para cualquier cosa que se reejecute.

Por último, decide qué debe pasar cuando el túnel está caído. Una excepción que se convierte en silencio en un bypass completo es una postura de seguridad distinta de una que falla cerrada, y solo una de las dos es la que querías.

Artículos relacionados