Netfilter NFQueue – Firewalling en user-space

Muy buenas !

Continuamos en nuestra senda del networking path 😉 pero como siempre, orientado hacia GNU/Linux, es decir: ip fighters on the real linux world 😉  No se trata de nada que tenga que ver con el alto rendimiento o decisiones de escala a nivel de CPD, o análisis de tiempos estadísticos de convergencia de multi-links, o de la importancia de TCP Multipath en nuestro futuro cercano. Lo que queremos presentaros son pruebas de concepto de como “elevar” la perspectiva de un firewall GNU/Linux para que tome decisiones de forma dinámica en user-space, sin ser una configuración estática cerrada.

Generalmente, los frontends o pseudo-appliance full instalables tanto para Netfilter/Iptables, como para BSD/Packet Filter estilo nuestro gran amigo PFSense, guardan su config en un XM/SQLite, y cada vez que se hacen cambios re-invocan por debajo los comandos que procedan para flushear/añadir/modificar rules, para acabar teniendo en memoria todas las reglas ya configuradas. Es decir, realmente son reglas estáticas. Aunque claro está, la propia regla puede tener cierto “dinamismo” como el hecho de poder limitar las conexiones por segundo o similar, o tirar de connection tracking si lo tenemos habilitado. En este post de nuestro nuevo blog, nos referimos mas bien a algo que sea totalmente impredecible, es decir, como si fuera un proceso hijo que se lanza con system() o popen() y que él decida con su exit code lo que hacer. O incluso si nos ponemos en plan nostálgicos, algo estilo RealtimeBattle, muy usado antiguamente en las party’s y tal, pero hasta donde recuerdo/voy lurkeando – prácticamente olvidado en los FP’s y Universidades cercanos, esto la verdad es que se merecería casi un post completo para revindicar su uso educativo 😉

Bueno, basta ya de preámbulos, a lo que nos referimos en concreto es a NFQUEUE de Netfiler, mecanismo compuesto del Target correspondiente en iptables para encolar paquetes y una librería linkable desde user-space para poder acceder a esa cola de paquetes; Y, lo mas importante: No sólo poder acceder, que para eso nos bastaría un sniffer y nos dejamos de batallas, sino poder sentenciar con un un veredicto sobre el paquete e informar a Netfilter de si debe aceptar o no el paquete, lo cual es realmente magic!

Es decir, como todo en este universo de GNU/Linux y del open source en general: Las piezas están muchas ya hechas, bienvenidos todos al mundo de los puzzles – tuberías – relaciones – xxx para unirlo todo y hacer que funcione la maquinaria 😉 y, como no podía ser menos: firewalling tb ! Algo que generalmente suele estar menos integrado con la Web 5.0 o las cosas modernas jijiji.

 

Es decir, recapitulando: Podemos tomar la decisión de que hacer con nuestro paquete en un programa nuestro, que puede a su vez preguntárselo a /dev/urandom, recorrerse una lista de tropecientos teras para ver que hacer o tener un algoritmo hyperoptimizado para poder decidir muy rápido lo que hay que hacer. Es decir, para bien o para mal, depende la decisión de algo que podemos construir nosotros.

¿Cómo funciona exactamente?

Parte IPtables

Con iptables, podemos decidir enviar el paquete a la cola de decisión en base a los criterios de matching habituales, es decir: ip origen/destino, puertos, estado o incluso por país(geoip) o layer7 si tenemos los patches correspondientes aplicados. A modo de ejemplo, para enviar cualquier ping que recibamos en entrada (es decir, no en routing):

 

Con este ejemplo, estamos diciendo a Netfilter que todos los paquetes que tengan como destino el propio sistema (localhost), entren por el interfaz/nic que sea, y tengan como protocolo ICMP, sea enviados al QUEUING de userspace. En este caso, no hemos especificado Queue, así que por defecto lo envía a la cola 0. Si quisiéramos enviar a una cola diferente bastaría con usar: –queue-num X

Podemos comprobar que estamos matcheando bien los paquetes lanzando un listado de rules y sus counters de matching:

Como podemos ver en el ejemplo, “algo” está matcheando, dado que el contador de paquetes sube y tal.

Igualmente, tb podemos comprobar que está encolando, dado que: El comportamiento por defecto a nivel de veredicto es el de denegar. Esto supone que si encolamos paquetes y no tenemos una aplicación que los va recuperando y emitiendo un veredicto positivo: se  hará drop.

Libnfqueue

Esta librería es la que nos permite vincular nuestra aplicación a las colas y actuar como juez 😉 Cabe destacar que para los Pythonistas – Saghul 😉 – y Perl power rangers existen bindings específicos.

Si estamos en Debian 8, por ejemplo, a nivel de soft a instalar tendríamos:

Una vez tenemos instalado, nuestro hello world tendría una estructura tal que:

  1. Obtener el handle a la librería NFQ.
  2. Quitar posibles vinculaciones de aplicaciones o sistemas anteriores.
  3. Vincularse a la cola.
  4. Entrar en bucle de: recepción=>análisis=>veredicto.
  5. Al acabar: desvincularse, cerrar handle de librería y tal.

En este caso, nuestro primer hello world emitiremos un resultado siempre positivo, quedando el código mínimo viable para un hello world algo así como (perdonar por las tabulaciones, parece que se han perdido en cpy-psting)

Como podéis ver en este ejemplo, hemos comentado las líneas de debugging, ya que realmente se nota el rendimiento. De hecho en nuestras pruebas se ve claramente.

Para compilarlo:

Primeras pruebas

Para probar el rendimiento, nos basaremos en el conocido IPERF  , así que lo que haremos será hacer matching en el puerto tcp  5001, en entrada y en salida y arrancar nuestro programa compilado:

Siendo los resultados rápidos:

Sin pasar por NFQUEUE

Pasando por NFQUEUE con debugging on-screen

Pasando por NFQUEUE sin debugging

Conclusiones preliminares de las primeras pruebas

Bueno, a parte de lo básico: It Works! ,  con nuestro mini código, que no hace nada, se puede decir que no hay nada de merma de rendimiento en una red gigabit, únicamente tener en cuenta que el debugging on-screen con el printf y fflush y tal ralentiza todo mucho (habrá que estar al loro en posteriores pruebas).

 

Otros aspectos interesantes antes de bajar más al barro

Cabe destacar esta lista de opciones:

–queue-bypass

Esta opción/extensión (a partir de kernel 2.6.39 / iptables 1.4.11) permite cambiar el comportamiento por defecto, de tal forma que si no hay “nadie escuchando” (no hay programa arrancado vinculado a la cola), el comportamiento será el de continuar a la siguiente rule de iptables, no descartar el paquete.

En función de lo que queramos montar puede ser interesante o peligrosa la opción

--queue-balance

Nos permite distribuir en varias colas (de forma lineal) los matchs, de tal forma que luego podríamos tener varios procesos diferentes / hilos y gestionar mejor el multi processing o lo que queramos montar. Es importante comentar que el mismo flujo, acorde al source code, acaba en la misma cola.

–fail-open

Si la cola donde se mandan los paquetes está llena / va con delay porque el proceso en user space está ralentizándose o similar, el comportamiento por defecto

en vez de descartar todos los paquetes cuando la cola está llena, los acepta. Aquí lo mismo que queue-bypass, es posible que nos interese, pero tb puede suponer un riesgo. Es decir, si hemos optado por meternos en este “fregado” es porque queremos “decidir”, no solo hacer análisis, que para eso tiramos de libpcap. Con lo que si hay que decidir, hay que estar presente (tener cola vinculada y ser capaces de procesar al ritmo que nos pase el kernel)

Interface en /proc

De cara a comprobaciones, scripting de monitorización y tal, Netfilter nos aporta una interface RO en el pseudo-fs PROC

Manos a la obra: Probando el rendimiento

Hipótesis y planteamiento

Lo que sobre-entendemos (dado que nunca lo hemos tenido que hacer, “hasta ahora”) es que querremos usar estos mecanismos de NFQUEUE para algo gestionable desde el panorama web. Así que nos pondremos dos situaciones:

  • MySQL. Esto sería el caso de base, que cualquiera que está empezando quisiera hacer “Todo gestionable 100% en BBDD, que lo pueda cambiar desde cualquier lado, gestionable 100%”, sin pensar demasiado en rendimiento o en miles de paquetes por segundo.
  • Memcached. Como evolución de lo anterior, vamos a hacer lo mismo pero en lugar de atacar una BBDD, atacaremos un servicio Memcache.

Estas pruebas son únicamente para ir viendo, no estamos haciendo una tesis doctoral y buscar el límite máximo, ni ejecutando un proyecto (mas que nada porque vamos sin requisitos, haciendo directamente lo que nos va gustando jijiji 😉 ).

Prueba con memcached

Preparación de entorno

Lo primero, obviamente, instalar memcached, Debian style 😉 :

Vamos a guardar una IP en la key: autorizada, del tal forma que luego nuestro programa consultará por la key ‘autorizada’ y sacará su valor para compararlo, siguiendo algunas tips vistas en algunas páginas, lo hacemos estilo oneliner, desde Bash jiji :

y para confirmar que existe:

Es decir, ya tenemos nuestro server memcached y una key metida 🙂

Código de prueba

Partiendo del código Hello World expuesto anteriormente, nuestra función de callback debe poder obtener la IP del paquete, para ello:

  1. Importante: Se debe actuar en modo NFQNL_COPY_PACKET, por las pruebas que hemos hecho, en modo COPY_META no llegan.
  2. En la función del callback hay que obtener el paquete completo (nf_packet).
  3. A partir del paquete completo, obtenemos ip header, a partir de ahí sacamos la IP

A modo de ejemplo la función de callback en modo debug nos quedaría:

Y si lo ejecutamos (acordarse de seguir con el target Queue en iptables), veremos algo así como en modo debug:

En lo que respecta la comprobación contra Memcached, primero hay que instalarlo ( libmemcached-dev) y el código de prueba sería:

Variables globales, para usarlas desde la función de callback cómodamente:

Conectarse antes del callback (para ahorrar tiempo en cada paquete a analizar):

En el main, algo así como:

 

Realizar la comprobación en cada llamada a callback

Tras obtener el paquete, IP origen y tal, una comprobación rápida:

(no os preocupéis que al final del post colgaremos el código completo en un link fácil de wgetear :D) )

(y sí, tendríamos que hacer este código más seguro 😉 )

Resultados con Memcache

Tras realizar la prueba, y repetirla varias veces, observamos que mas o menos siempre andamos por el mismo valor, unos 140Mbits:

Y, lo mas importante, nuestro juez de paquetes tb empieza a hacer overflow en su cola (se muestran los ! printeados en el fallo de recepción).

Pruebas con MySQL

Antes que nada, recordar que este es un post ilustrativo, MySQL está instalado del APT como quien instala el VIM 😉 Vamos, que no se ha puesto ni query_cache ni nada, así que los MySQL Ninjas que no lancen todavía sus misiles 😉

Preparación de entorno

Pues eso, a lo fácil: apt-get install mysql-server libmysqlclient-dev

Y en lo que respecta la estructura y valores, estilo one line:

Código de prueba

Conexión y arranque inicial en Main

Sería algo así como (teniendo con de tipo MYSQL):

Validación en la función de callback

Un ejemplo de como se podría hacer la validación, sin grandes optimizaciones ni nada:

Resultados con MySQL

Con MySQL los resultados son la instalación por defecto (query caché activada):

Si optamos por deshabilitar la query caché totalmente:

 

Comparativa final

No podía faltar un gráfico ! Si hablamos de rendimiento, algo tenemos que poner para comparar jiji Aunque está claro que no está nada optimizado, es todo a grosso-modo, máquinas virtuales, … Pero bueno, a modo representativo:
graficas_nfqueue

Notas importantes:

  • Lo que no aparece en esta gráfica es el consumo de CPU, en todos los casos salvo el Direct Accept (la función que retorna siempre accept), se ha disparado todo bastante, tanto de Memcached como de Mysql y del propio proceso.
  • Esto realmente es un poco “prensa sensacionalista” jiji, esas gráficas no reflejan la realidad de los máximos posibles, el proceso es single-threaded, se podría haber hecho sacando hilos y seguramente el rendimiento obtenido en MySQL o Memcached hubiera sido muchísimo más alto.
  • Desde luego, no es válido para comparar MySQL y Memcached, ya que era una única tabla con una única entrada, en el mundo ideal de BBDD esto no suele ser nunca así (aunque bueno, al menos, la query se direccionaría con algún index y debería ir rápido tb).

Conclusión genérica

Por lo que hemos visto, con un ejecutable compilado que prácticamente no haga nada conseguimos casi Gigabit/Sec, y si ya tenemos algo de I/O con Memcache, todo decrece, pero seguimos teniendo 100-200Mbits sin haber realizado ni siquiera un enfoque multi-hilo / proceso. Es decir, haciendo de la peor manera posible se consigue esas cifras (nada despreciables).

De hecho, para simplificarlo, podríamos hacer incluso que nuestra aplicación no sea ni siquiera SMP, podríamos tener varias aplicaciones, cada una recibiendo paquetes de una cola diferente, y así tendríamos uso de los diferentes procesadores sin complicarnos con fork() o phtread_create ! Súper KISS total !

Y sobre todo, que aquí estamos matcheando todo el tráfico de la prueba con IPERF, pero en posibles planteamientos quizás sería sólo con match de state y state NEW por ejemplo (para saber si las nuevas sesiones las permitimos) y las ESTABLISHED/RELATED las dejamos pasar .. Todo esto, es especulación, ya que como os contábamos, hemos montado este escenario por amor al arte, no porque tenga que cumplir unos requisitos con SLA’s y derivados 😉

¿Y si quisiéramos hacer routing dinámico?

Hasta aquí, los ejemplos son únicamente para ACCEPT o DROP, pero que pasaría si lo que quisiéramos es hacer policy routing ? Es decir, encaminar por un lado o por otro ? De una forma pseudo-resumida, para el tema de policy routing en GNU/Linux, lo más cómodo es usar Netfilter marks para enviar a una tabla u otra de iproute2 (estilo VRF):

  • Creamos las diferentes tablas en /etc/iproute2/rt_tables
  • Con ip rule indicamos que tipo de marca tiene que tener un paquete para ir a una tabla concreta: ip rule add fwmark 1 table XXXX
  • Con Netfitler tenemos que marcar los paquetes (vía NFQUEUE, que es la gracia de todo esto)

Para esa marca vía NFQueue, en lugar de retornar de la función de análisis de paquetes en callback con nfq_set_verdict, deberíamos retornar con:

Es decir, en detalle por pasos si quisiéramos decidir por dónde enrutar en base a un software externo:

  1. Declaramos un par de tablas de routing: lento,rapido
  2. En dichas tablas metemos como ruta por defecto una diferente (una por Fiber y la otra por cslip or ejemplo jiji).
  3. Con iptables envíamos los paquetes en FORWARD hacia NFQUEUE
  4. Construímos un software que reciba de la cola y tome decisión de ACCEPT pero aplicando marcas.
  5. Con ip rule mandamos el paquete a una tabla u otra de routing.

Y fin, ya nos hemos construido un super router IP que rutará en base a lo que nos dé la gana 😉

Como si queremos rutar en base a si el tamaño del paquete es par o impar;  O mas real life:  si coincide con que un algoritmo de una red social importantísima ha predicho que cuando sea de día en USA hay un contenido que se está haciendo viral y se necesitará más ancho de banda en este path u en este otro y hay que re-enrutar 😉

Un micro-inciso: ¿No es alucinante el poder de GNU/Linux?

Posible uso especial

A parte de los usos localizables facilmente, como el que se le da en Suricata por ejemplo, se nos ocurre proponer:

Anti voice-spam (SPIT) o protector de palabras “mal sonantes”

jejeje, esto de llevar años ya orbitando por las galaxias y planetas del mundo VoIP, nos genera cierta inclinación a procastinar sobre estas área 😉 Comentando por aquí se nos ha ocurrido algo que parece bastante viable:

  1. Paquetes que matchean puertos RTP se mandan a NFQUEUE
  2. Aplicación contra libnfqueue los va recibiendo
  3. Dicha aplicación tiene el payload  completo, así que tirando de librtp o similar parece víable obtener una estructura con un WAV
  4. Llegados al WAV, podemos tirar de VoxLibx de Verbio Technologies para realizar WordSpotting y detectar palabras mal sonantes o spam.

Es decir, si hubiese tiempo y alguien que lo quiera, no parece una meta inalcanzable 🙂 Para rizar el rizo, se podría inyectar un “beep” por ejemplo, pero habría que ir trackeando RTCP (secuencias, etc …) estilo MiTM, algo que ya requiere mas súper poderes 😉

Esto básicamente tb lo puede hacer un sniffer, pero luego para colgar o bloquear el audio tendría que tirar contra Kamailio / Asterisk / X … Eso sí, quizás sería bastante mas sencillo :D)

 

Despedida y cierre 😉

Pues nada, esperemos que os haya gustado  esta aventurilla con la que hemos estado peleando en esta ocasión. Ha sido algo bastante específico de Netfilter, pero la verdad es que nunca se sabe cuando puedes llegar a necesitar algo de nuestro amado Kernel y aplicaciones GNU o derivadas, lo mismo en un proyecto con hardware embebido programando  una función X de seguridad nos viene bien este mecanismo, o lo mismo nos piden construir algo tipo software que enrute paquetes IP en base a geolocalización de twitters 😉 o actuar como un auténtico BOFH:  sonriendo en una pantalla si un paquete te gusta o no XDDD

 

Enlace al código de pruebas en: GITHUB/IRONTEC



¿Te gusta este post? Es solo un ejemplo de cómo podemos ayudar a tu empresa...
Sobre Gorka Gorrotxategi

CTO en Irontec, en el frente técnico desde un par de lustros ya, para todo lo que tenga que ver con Networking, VoIP y Sistemas, en ese orden :D) Desde @zetagor escribo algo, pero poco verbose la verdad

Queremos tu opinión :)