Ir al contenido

Arquitectura

cs-routeros-bouncer actúa como puente entre la inteligencia de amenazas de CrowdSec y el firewall de MikroTik.

La LAPI de CrowdSec envía decisiones de ban y unban a cs-routeros-bouncer, que las aplica al router MikroTik a través de la API de RouterOS, expone métricas de Prometheus y sirve comprobaciones de salud. El router aplica los cambios a sus reglas de firewall y listas de direcciones.

Las dos ramas de la derecha de ese diagrama no son adorno. /metrics es el endpoint de Prometheus que lee el dashboard incluido, y /health es el endpoint que consulta la comprobación de salud de un contenedor; ambos los sirve el mismo servidor HTTP, que arranca incluso con las métricas desactivadas.

El dashboard de Grafana incluido con cs-routeros-bouncer, con paneles de decisiones activas, ritmo de decisiones por origen, contadores de tráfico del firewall y salud del sistema RouterOS.El dashboard de Grafana incluido con cs-routeros-bouncer, con paneles de decisiones activas, ritmo de decisiones por origen, contadores de tráfico del firewall y salud del sistema RouterOS.
La rama /metrics, ya renderizada: grafana/dashboard.json en el repositorio.

Todos los diagramas de este sitio trazan la misma distinción, porque es la que importa cuando intentas averiguar qué le hace el bouncer a tu router:

  • Una línea continua es una escritura. Cambia el estado del router: aparece o desaparece una entrada de address-list, se crea, se mueve o se elimina una regla de firewall.
  • Una línea discontinua es una lectura, una comprobación o un descarte. Nada cambia en el router: llegan decisiones por el stream de la LAPI, se lista una address-list, se mide la deriva, se descarta una decisión.

Tres canales llevan esa distinción, para que el diagrama siga leyéndose en escala de grises, con cualquier tipo de deficiencia en la visión del color y sobre papel: el patrón de la línea, después el color y, por último, una leyenda dibujada con esas mismas dos líneas dentro de cada diagrama de flujo. El diagrama de secuencia es la excepción: ahí una flecha continua es una petición y una discontinua la respuesta, que es la convención propia de Mermaid y no la distinción lectura/escritura, así que rotularlo con esta leyenda lo haría ilegible. La convención está escrita en docs/src/styles/diagram.css; los colores salen de docs/src/styles/theme.css en tiempo de compilación, así que los diagramas siguen la paleta en lugar de fijar la suya.

El bouncer está compuesto por varios paquetes internos:

PaqueteResponsabilidad
cmd/cs-routeros-bouncerPunto de entrada de la CLI, enrutamiento de subcomandos
internal/configCarga de configuración, validación, vinculación de variables de entorno
internal/crowdsecCliente de streaming de la LAPI de CrowdSec
internal/routerosCliente de la API de RouterOS (direcciones, reglas de firewall)
internal/managerOrquestador central: conecta todas las piezas
internal/metricsMétricas de Prometheus y endpoint de salud
Al arrancar, el bouncer se conecta a CrowdSec y a MikroTik, crea las reglas de firewall, obtiene las decisiones activas y reconcilia las listas de direcciones. Durante el bucle de ejecución añade o elimina IPs a medida que CrowdSec informa de decisiones nuevas o expiradas, consultando primero su caché de direcciones. En un intervalo fijo vuelve a reconciliar para reparar la deriva. Al apagarse (SIGTERM) elimina sus reglas de firewall mientras las entradas de las address-lists expiran por su propio timeout.

Parar el bouncer elimina sus reglas de firewall y deja bloqueadas las direcciones. Esa asimetría es deliberada: las reglas son baratas de recrear y desconciertan si aparecen abandonadas en un router, mientras que son las entradas de la address-list las que sostienen la protección de verdad, y tirar decenas de miles en cada reinicio abriría un hueco y habría que pagarlo otra vez al volver.

Un SIGINT o un SIGTERM cancela el contexto raíz, lo que detiene el stream de decisiones y el ticker de reconciliación. El manager elimina entonces todas las reglas de firewall que creó esta ejecución, cierra el pool de conexiones y la conexión principal de la API y, por último, da cinco segundos al servidor de salud y métricas para detenerse. Una regla cuyo comentario no se puede analizar se queda en el router y se elimina en el siguiente arranque, que busca por la firma fija del bouncer. Las entradas de las address-lists no se tocan durante el apagado: una entrada escrita con timeout expira sola, mientras que una entrada creada a partir de una decisión cuya duración resolvió a cero o menos no tiene timeout alguno y permanece hasta que una reconciliación posterior o un operador la elimine. Una decisión que no trae campo de duración se descarta antes de este punto.

La eliminación se guía por los identificadores que esta ejecución anotó al crear o adoptar cada regla, y el comentario se analiza a la inversa para saber en qué menú vive la regla. Un comentario que el analizador no reconoce —por ejemplo, tras cambiar comment_prefix a mitad de ejecución— se registra y se omite, que es justo el caso para el que existe el barrido del arranque siguiente: busca por la firma fija @cs-routeros-bouncer, así que encuentra las reglas del bouncer las escribiera el prefijo que las escribiera. Ese mismo barrido es el que limpia tras una caída, donde Shutdown no llegó a ejecutarse.

Al cerrar la conexión con RouterOS el bouncer también se marca como desconectado, de modo que /health informa del estado real durante lo que queda de la ventana de apagado.

Todos los recursos creados por el bouncer en MikroTik se etiquetan con un comentario estructurado:

{comment_prefix}:{type}-{chain}-{direction}-{protocol} @cs-routeros-bouncer

Ejemplos:

  • crowdsec-bouncer:filter-input-input-v4 @cs-routeros-bouncer
  • crowdsec-bouncer:raw-prerouting-input-v6 @cs-routeros-bouncer

Esto permite al bouncer identificar y gestionar con precisión sus propios recursos sin afectar a las reglas creadas por el usuario.

Al procesar una decisión de ban, el bouncer consulta primero su caché de direcciones en memoria:

  1. Si la dirección ya está en la caché, la llamada a la API de RouterOS se omite por completo.
  2. Si la dirección no está en la caché, intenta añadirla directamente (~1–3 ms).
  3. Si RouterOS devuelve already have such entry, lo trata como un conflicto a nivel de dispositivo, mantiene la conexión abierta, localiza la entrada existente y actualiza su timeout/comentario.

Esto es significativamente más rápido que el enfoque de “comprobar primero” (~400 ms por IP), que exigiría listar antes todas las entradas.

El bouncer mantiene un pool configurable de conexiones persistentes a la API de RouterOS. El pool solo se usa para las eliminaciones: durante la reconciliación, las entradas obsoletas se eliminan de forma concurrente por el pool mediante el helper genérico ParallelExec. En la prueba de CAPI con el RB5009, eliminar ~26.800 entradas exclusivas de CAPI llevó ~77 s de trabajo de eliminación en RouterOS. Si el pool no se puede abrir, las eliminaciones recurren a la conexión principal.

Para la reconciliación inicial, el bouncer genera scripts de RouterOS que añaden entradas en bloques de 100 IPs por script. Cada entrada usa :do { ... } on-error={} para omitir los duplicados de forma controlada. Las adiciones masivas no usan el pool de conexiones: se ejecutan de forma secuencial por la conexión principal. Aun así, este enfoque es ~97× más rápido que las llamadas individuales secuenciales a la API para listas grandes — medido en un RB5009UG+S+ (RouterOS 7.22.1), donde ~28.700 entradas de CAPI supusieron ~35–36 s de trabajo de adición masiva en RouterOS; consulta Benchmarking para la metodología.

Un mapa en memoria (map[string]struct{} con sync.RWMutex) registra todas las direcciones presentes actualmente en el router. Esto proporciona:

  • Búsquedas de unban en O(1): cuando se levanta el ban de una IP, primero se consulta la caché. Si la IP no está en la caché (por ejemplo, ya expiró en el router), la llamada a la API se omite por completo.
  • Ruta rápida en O(1) para bans duplicados: los eventos de ban repetidos para direcciones que ya se sabe que están en el router se resuelven de inmediato sin generar tráfico innecesario de gestión/API en RouterOS.
  • Prefiltrado durante el arranque: las eliminaciones recibidas durante la recogida inicial de decisiones se prefiltran contra los bans entrantes para evitar trabajo innecesario.

A diferencia de algunos bouncers que crean listas con marca de tiempo, cs-routeros-bouncer usa una única address-list con nombre por protocolo:

  • crowdsec-banned para IPv4
  • crowdsec6-banned para IPv6

Las reglas de firewall referencian estas listas por nombre, lo que es más eficiente y evita el problema de la duplicación.

Al arrancar, y después periódicamente según crowdsec.reconciliation_interval, el bouncer calcula la diferencia entre las decisiones activas de CrowdSec y el estado actual de la address-list de MikroTik:

  1. Obtener todas las decisiones activas de CrowdSec
  2. Obtener todas las entradas de la address-list de MikroTik
  3. Comparar ambos conjuntos
  4. Añadir las entradas que faltan (están en CrowdSec pero no en MikroTik)
  5. Eliminar las entradas obsoletas (están en MikroTik pero no en CrowdSec)

Esto mantiene la pertenencia sincronizada con independencia de cómo se detuvo el bouncer, de lo que ocurriera mientras estaba fuera de línea o de si las entradas del lado de RouterOS expiraron mientras CrowdSec aún las consideraba activas.