Skip to content

Architecture

cs-routeros-bouncer acts as a bridge between CrowdSec’s threat intelligence and MikroTik’s firewall.

CrowdSec LAPI sends ban and unban decisions to cs-routeros-bouncer, which applies them to the MikroTik router through the RouterOS API, exposes Prometheus metrics, and serves health checks. The router applies the changes to its firewall rules and address lists.

The bouncer is composed of several internal packages:

PackageResponsibility
cmd/cs-routeros-bouncerCLI entrypoint, subcommand routing
internal/configConfiguration loading, validation, environment variable binding
internal/crowdsecCrowdSec LAPI streaming client
internal/routerosRouterOS API client (addresses, firewall rules)
internal/managerCentral orchestrator — ties everything together
internal/metricsPrometheus metrics and health endpoint
At startup the bouncer connects to CrowdSec and MikroTik, creates firewall rules, fetches active decisions, and reconciles address lists. During the runtime loop it adds or removes IPs as CrowdSec reports new or expired decisions, checking its address cache first. On a fixed interval it reconciles again to repair drift. On shutdown (SIGTERM) it removes its firewall rules while address-list entries expire via their own timeout.

All resources created by the bouncer in MikroTik are tagged with a structured comment:

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

Examples:

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

This allows the bouncer to precisely identify and manage its own resources without affecting user-created rules.

When processing a ban decision, the bouncer first checks its in-memory address cache:

  1. If the address is already in cache, the RouterOS API call is skipped entirely.
  2. If the address is not in cache, try to add it directly (~1–3 ms).
  3. If RouterOS returns already have such entry, treat it as a device-level conflict, keep the connection open, find the existing entry, and update its timeout/comment.

This is significantly faster than the “check-first” approach (~400 ms per IP), which would require listing all entries first.

The bouncer maintains a configurable pool of persistent RouterOS API connections. During reconciliation, work is distributed across the pool using the generic ParallelExec helper; on an RB5009 with pool_size: 10, full RouterOS bulk-add work for ~28,700 CAPI-origin entries completed in ~35–36 s.

For initial reconciliation, the bouncer generates RouterOS scripts that add entries in chunks of 100 IPs per script. Each entry uses :do { ... } on-error={} to gracefully skip duplicates. This approach is ~97× faster than individual sequential API calls for large lists — measured on an RB5009UG+S+ (RouterOS 7.22.1) with ~28,700 CAPI entries; see Benchmarking for the methodology.

An in-memory map (map[string]struct{} with sync.RWMutex) tracks all addresses currently on the router. This provides:

  • O(1) unban lookups: When an IP is unbanned, the cache is checked first. If the IP is not in the cache (e.g., already expired on the router), the API call is skipped entirely.
  • O(1) duplicate-ban fast path: Repeated ban events for addresses already known to be on the router return immediately without creating RouterOS management/API churn.
  • Pre-filtering during startup: Deletes received during initial decision collection are pre-filtered against incoming bans to avoid unnecessary work.

Unlike some bouncers that create timestamped lists, cs-routeros-bouncer uses a single named address list per protocol:

  • crowdsec-banned for IPv4
  • crowdsec6-banned for IPv6

Firewall rules reference these lists by name, which is more efficient and avoids the duplication problem.

On startup, and then periodically at crowdsec.reconciliation_interval, the bouncer performs a diff between CrowdSec’s active decisions and MikroTik’s current address list state:

  1. Fetch all active decisions from CrowdSec
  2. Fetch all entries in the address list from MikroTik
  3. Compare the two sets
  4. Add missing entries (in CrowdSec but not in MikroTik)
  5. Remove stale entries (in MikroTik but not in CrowdSec)

This keeps membership synchronized regardless of how the bouncer was stopped, what happened while it was offline, or whether RouterOS-side entries expired while CrowdSec still considered them active.