A CrowdSec remediation component (bouncer) for MikroTik RouterOS that automatically manages firewall rules and address lists via the RouterOS API.
/metrics), structured logging, health endpoint (/health), LAPI usage metrics (active decisions, dropped traffic)Existing MikroTik bouncers have significant limitations that this project addresses:
| Feature | funkolab (archived) | nvtkaszpir-alt | cs-routeros-bouncer |
|---|---|---|---|
| Auto-create firewall rules | ❌ | ❌ | ✅ |
| Individual IP add/remove | ✅ | ❌ (bulk re-upload) | ✅ |
| No duplicate IPs | ✅ | ❌ | ✅ |
| State reconciliation on restart | ❌ | ❌ | ✅ |
| Remove rules on shutdown | ❌ | ❌ | ✅ |
| IPv6 support | ✅ | ✅ | ✅ |
| Output blocking | ❌ | ✅ | ✅ |
| Origin filtering (local-only mode) | ❌ | ❌ | ✅ |
| Prometheus metrics | ❌ | ✅ | ✅ |
| LAPI usage metrics (dropped traffic) | ❌ | ❌ | ✅ |
| Health endpoint | ❌ | ❌ | ✅ |
| Go (compiled, low resource usage) | ❌ (Python) | ❌ (Python) | ✅ |
1sudo cscli bouncers add cs-routeros-bouncer
Save the API key shown in the output.
Connect to your MikroTik router and create a dedicated user:
1/user group add name=crowdsec policy=read,write,api,sensitive,!ftp,!local,!ssh,!reboot,!policy,!test,!password,!sniff,!romon,!rest-api2/user add name=crowdsec group=crowdsec password=YOUR_SECURE_PASSWORD
Choose your preferred installation method below.
1services:2 cs-routeros-bouncer:3 image: ghcr.io/jmrplens/cs-routeros-bouncer:latest4 container_name: cs-routeros-bouncer5 restart: unless-stopped6 ports:7 - "2112:2112" # Prometheus metrics (optional)8 environment:9 CROWDSEC_URL: "http://crowdsec:8080/"10 CROWDSEC_BOUNCER_API_KEY: "your-bouncer-api-key"11 MIKROTIK_HOST: "192.168.0.1:8728"12 MIKROTIK_USER: "crowdsec"13 MIKROTIK_PASS: "your-password"14 # Or mount a config file:15 # volumes:16 # - ./config.yaml:/etc/cs-routeros-bouncer/config.yaml
1docker compose up -d
Download the latest release from the Releases page:
Automatic setup (recommended):
1# Download (replace with your architecture: amd64, arm64, armv7)2wget https://github.com/jmrplens/cs-routeros-bouncer/releases/latest/download/cs-routeros-bouncer_linux_amd64.tar.gz3tar xzf cs-routeros-bouncer_linux_amd64.tar.gz45# Automated install: copies binary, creates config, installs and starts systemd service6sudo ./cs-routeros-bouncer setup78# Edit configuration with your CrowdSec API key and MikroTik credentials9sudo nano /etc/cs-routeros-bouncer/cs-routeros-bouncer.yaml1011# Restart after editing config12sudo systemctl restart cs-routeros-bouncer
The setup subcommand accepts optional flags:
| Flag | Default | Description |
|---|---|---|
-bin | /usr/local/bin/cs-routeros-bouncer | Installation path for the binary |
-config-dir | /etc/cs-routeros-bouncer | Directory for configuration files |
To uninstall:
1sudo cs-routeros-bouncer uninstall # Keeps config files2sudo cs-routeros-bouncer uninstall -purge # Also removes config
1# Download2wget https://github.com/jmrplens/cs-routeros-bouncer/releases/latest/download/cs-routeros-bouncer_linux_amd64.tar.gz3tar xzf cs-routeros-bouncer_linux_amd64.tar.gz45# Install6sudo install -m 755 cs-routeros-bouncer /usr/local/bin/7sudo mkdir -p /etc/cs-routeros-bouncer8sudo cp cs-routeros-bouncer.yaml /etc/cs-routeros-bouncer/cs-routeros-bouncer.yaml910# Edit configuration11sudo nano /etc/cs-routeros-bouncer/cs-routeros-bouncer.yaml1213# Install systemd service14sudo tee /etc/systemd/system/cs-routeros-bouncer.service > /dev/null << 'EOF'15[Unit]16Description=CrowdSec RouterOS Bouncer17After=network-online.target crowdsec.service18Wants=network-online.target1920[Service]21Type=simple22ExecStart=/usr/local/bin/cs-routeros-bouncer -c /etc/cs-routeros-bouncer/cs-routeros-bouncer.yaml23Restart=on-failure24RestartSec=102526[Install]27WantedBy=multi-user.target28EOF2930sudo systemctl daemon-reload31sudo systemctl enable --now cs-routeros-bouncer
1git clone https://github.com/jmrplens/cs-routeros-bouncer.git2cd cs-routeros-bouncer3make build45# Option 1: Automated install6sudo bin/cs-routeros-bouncer setup78# Option 2: Manual install9sudo install -m 755 bin/cs-routeros-bouncer /usr/local/bin/
All options can be set via YAML config file or environment variables. Environment variables override config file values.
See config/cs-routeros-bouncer.yaml for the full annotated reference.
The essential settings to get the bouncer running. Most deployments only need these.
| Config Key | Env Variable | Default | Description |
|---|---|---|---|
crowdsec.api_url | CROWDSEC_URL | http://localhost:8080/ | CrowdSec LAPI URL |
crowdsec.api_key | CROWDSEC_BOUNCER_API_KEY | (required) | Bouncer API key |
mikrotik.address | MIKROTIK_HOST | 192.168.0.1:8728 | RouterOS API address (host:port) |
mikrotik.username | MIKROTIK_USER | crowdsec | API username |
mikrotik.password | MIKROTIK_PASS | (required) | API password |
firewall.ipv4.enabled | FIREWALL_IPV4_ENABLED | true | Enable IPv4 blocking |
firewall.ipv6.enabled | FIREWALL_IPV6_ENABLED | true | Enable IPv6 blocking |
firewall.filter.enabled | FIREWALL_FILTER_ENABLED | true | Create filter firewall rules |
firewall.raw.enabled | FIREWALL_RAW_ENABLED | true | Create raw/prerouting rules |
firewall.deny_action | FIREWALL_DENY_ACTION | drop | Action: drop or reject |
logging.level | LOG_LEVEL | info | Log level: debug, info, warn, error |
Fine-tuning options for decision filtering, TLS, performance, firewall customization, and observability. The defaults work well for most setups.
| Config Key | Env Variable | Default | Description |
|---|---|---|---|
crowdsec.update_frequency | CROWDSEC_UPDATE_FREQUENCY | 10s | Poll interval for decision updates |
crowdsec.lapi_metrics_interval | CROWDSEC_LAPI_METRICS_INTERVAL | 15m | LAPI usage metrics interval: active decisions, dropped traffic (0 = disabled) |
crowdsec.origins | CROWDSEC_ORIGINS | [] (all) | Filter by origin (["crowdsec","cscli"] = local only) |
crowdsec.scopes | CROWDSEC_SCOPES | ["ip","range"] | Decision scopes to process |
crowdsec.supported_decisions_types | CROWDSEC_DECISIONS_TYPES | ["ban"] | Decision types to process (only ban is implemented — see details) |
crowdsec.scenarios_containing | CROWDSEC_SCENARIOS_CONTAINING | [] | Only process decisions matching these scenarios |
crowdsec.scenarios_not_containing | CROWDSEC_SCENARIOS_NOT_CONTAINING | [] | Exclude decisions matching these scenarios |
crowdsec.retry_initial_connect | CROWDSEC_RETRY_INITIAL_CONNECT | true | Retry LAPI connection on startup failure |
crowdsec.insecure_skip_verify | CROWDSEC_INSECURE_SKIP_VERIFY | false | Skip TLS certificate verification for LAPI |
crowdsec.cert_path | CROWDSEC_CERT_PATH | TLS client certificate path | |
crowdsec.key_path | CROWDSEC_KEY_PATH | TLS client key path | |
crowdsec.ca_cert_path | CROWDSEC_CA_CERT_PATH | TLS CA certificate path |
| Config Key | Env Variable | Default | Description |
|---|---|---|---|
mikrotik.tls | MIKROTIK_TLS | false | Use TLS (port 8729) |
mikrotik.tls_insecure | MIKROTIK_TLS_INSECURE | false | Skip TLS certificate verification for RouterOS |
mikrotik.connection_timeout | MIKROTIK_CONN_TIMEOUT | 10s | Connection timeout |
mikrotik.command_timeout | MIKROTIK_CMD_TIMEOUT | 30s | Command execution timeout |
mikrotik.pool_size | MIKROTIK_POOL_SIZE | 4 | Number of parallel API connections for bulk operations (1–20) |
Auto-capping: On startup the bouncer queries the router's
max-sessionsfor the API service and automatically reducespool_sizeif it would exceed the router limit. To check or change the limit on your router:1# Check current max-sessions for the API service2/ip/service/print where name=api34# Increase the limit (default is 20, maximum 1000)5/ip/service/set api max-sessions=1000
| Config Key | Env Variable | Default | Description |
|---|---|---|---|
firewall.ipv4.address_list | FIREWALL_IPV4_ADDRESS_LIST | crowdsec-banned | IPv4 address list name in MikroTik |
firewall.ipv6.address_list | FIREWALL_IPV6_ADDRESS_LIST | crowdsec6-banned | IPv6 address list name in MikroTik |
firewall.filter.chains | FIREWALL_FILTER_CHAINS | ["input"] | Chains for filter rules |
firewall.raw.chains | FIREWALL_RAW_CHAINS | ["prerouting"] | Chains for raw rules |
firewall.rule_placement | FIREWALL_RULE_PLACEMENT | top | Placement: top or bottom |
firewall.comment_prefix | FIREWALL_COMMENT_PREFIX | crowdsec-bouncer | Comment prefix for managed resources |
firewall.log | FIREWALL_LOG | false | Enable RouterOS logging on firewall rules |
firewall.log_prefix | FIREWALL_LOG_PREFIX | crowdsec-bouncer | Global prefix for RouterOS log entries |
firewall.reject_with | FIREWALL_REJECT_WITH | Reject type when deny_action=reject (e.g. tcp-reset, icmp-admin-prohibited) | |
firewall.filter.log_prefix | FIREWALL_FILTER_LOG_PREFIX | Override global log prefix for filter rules | |
firewall.filter.connection_state | FIREWALL_FILTER_CONNECTION_STATE | Connection-state matcher for filter rules (e.g. new, new,invalid) | |
firewall.raw.log_prefix | FIREWALL_RAW_LOG_PREFIX | Override global log prefix for raw rules | |
firewall.block_input.interface | FIREWALL_INPUT_INTERFACE | Restrict input/raw rules to this interface (empty = all) | |
firewall.block_input.interface_list | FIREWALL_INPUT_INTERFACE_LIST | Restrict input/raw rules to this interface list (empty = all) | |
firewall.block_input.whitelist | FIREWALL_INPUT_WHITELIST | Address-list name for input whitelist (accept rule before drop) | |
firewall.block_output.enabled | FIREWALL_BLOCK_OUTPUT | false | Block outbound traffic to banned IPs |
firewall.block_output.interface | FIREWALL_OUTPUT_INTERFACE | WAN interface for output rules | |
firewall.block_output.interface_list | FIREWALL_OUTPUT_INTERFACE_LIST | WAN interface list for output rules | |
firewall.block_output.log_prefix | FIREWALL_OUTPUT_LOG_PREFIX | Override global log prefix for output rules | |
firewall.block_output.passthrough_v4 | FIREWALL_OUTPUT_PASSTHROUGH_V4 | IPv4 client IP to bypass output blocking (src-address=!IP) | |
firewall.block_output.passthrough_v4_list | FIREWALL_OUTPUT_PASSTHROUGH_V4_LIST | IPv4 address-list to bypass output blocking (precedence over IP) | |
firewall.block_output.passthrough_v6 | FIREWALL_OUTPUT_PASSTHROUGH_V6 | IPv6 client IP to bypass output blocking | |
firewall.block_output.passthrough_v6_list | FIREWALL_OUTPUT_PASSTHROUGH_V6_LIST | IPv6 address-list to bypass output blocking (precedence over IP) |
| Config Key | Env Variable | Default | Description |
|---|---|---|---|
logging.format | LOG_FORMAT | text | Log format: text or json |
logging.file | LOG_FILE | Log to file (empty = stdout only) | |
metrics.enabled | METRICS_ENABLED | false | Enable Prometheus /metrics endpoint |
metrics.listen_addr | METRICS_ADDR | 0.0.0.0 | Metrics server listen address |
metrics.listen_port | METRICS_PORT | 2112 | Metrics server listen port |
metrics.routeros_poll_interval | METRICS_ROUTEROS_POLL_INTERVAL | 30s | RouterOS system metrics poll interval (0 to disable) |
metrics.track_processed | METRICS_TRACK_PROCESSED | true | Track processed (non-blocked) traffic via passthrough counting rules |
1crowdsec:2 api_url: "http://localhost:8080/"3 api_key: "your-key"4mikrotik:5 address: "192.168.0.1:8728"6 username: "crowdsec"7 password: "your-password"8firewall:9 ipv6:10 enabled: false11 raw:12 enabled: false
1crowdsec:2 api_url: "http://localhost:8080/"3 api_key: "your-key"4mikrotik:5 address: "192.168.0.1:8729"6 username: "crowdsec"7 password: "your-password"8 tls: true9firewall:10 ipv4:11 enabled: true12 ipv6:13 enabled: true14 filter:15 enabled: true16 chains: ["input"]17 raw:18 enabled: true19 chains: ["prerouting"]20 deny_action: "drop"21 rule_placement: "top"22 block_input:23 interface_list: "WAN"24 block_output:25 enabled: true26 interface_list: "WAN"27metrics:28 enabled: true29 listen_port: 211230logging:31 level: "info"
1crowdsec:2 api_url: "http://localhost:8080/"3 api_key: "your-key"4 origins: ["crowdsec", "cscli"]5mikrotik:6 address: "192.168.0.1:8728"7 username: "crowdsec"8 password: "your-password"
The bouncer creates rules with descriptive comments for identification:
1;;; crowdsec-bouncer:filter-input-input-v4 @cs-routeros-bouncer2chain=input action=drop src-address-list=crowdsec-banned34;;; crowdsec-bouncer:raw-prerouting-input-v4 @cs-routeros-bouncer5chain=prerouting action=drop src-address-list=crowdsec-banned
Rules are placed at the top of the chain by default (rule_placement: top) to ensure they are evaluated first. If dynamic/built-in rules occupy the top positions (e.g., RouterOS fasttrack counters), the bouncer iterates through subsequent positions until it finds one where the rule can be placed.
Tested on a MikroTik RB5009UG+S+ (ARM64, 4 cores @ 1400 MHz, 1 GB RAM, RouterOS 7.21.3) with the bouncer running on a separate Linux host connected via the RouterOS API (plaintext, port 8728).
The bouncer uses a connection pool (4 parallel API connections), script-based bulk add (chunks of 100 entries), and an in-memory address cache for O(1) lookups during unban operations.
| Scenario | IPs synced | Time | Throughput | Router CPU peak |
|---|---|---|---|---|
| Local decisions only | 1,510 (IPv4 + IPv6) | ~9 s | ~168 IPs/s | 14% |
| Local + CAPI community | 25,059 (24,490 IPv4 + 569 IPv6) | ~2 min 50 s | ~147 IPs/s | 23% |
| Scenario | Existing IPs | Time | Router CPU peak |
|---|---|---|---|
| Restart, all IPs already present | 25,065 | ~10 s | 16% |
| Restart, 5 expired during downtime | 25,065 (added 1, removed 4) | ~10 s | 16% |
| Removed | Remaining | Time | Throughput | Router CPU peak |
|---|---|---|---|---|
| 23,548 (22,980 IPv4 + 568 IPv6) | 1,519 IPv4 + 1 IPv6 | ~3 min 45 s | ~105 removes/s | 22% |
| Operation | Typical latency | Notes |
|---|---|---|
| Ban (add IP) | ~1–3 ms | Optimistic-add, no lookup needed |
| Ban (duplicate IP, new duration) | ~5–8 ms | Detects "already have" → updates timeout to new duration |
| Unban (remove IP) | ~7 s end-to-end | Includes LAPI polling interval (15 s max). API call itself ~2 ms |
| Unban cache fast-path | < 1 ms | IP not in cache → skip API call entirely |
| Metric | Value |
|---|---|
| Firewall rules created | 4 rules in ~2 s |
| Bouncer memory (steady state) | ~30 MB |
| Bouncer CPU (steady state) | < 1% |
| Router CPU (steady state, local-only) | 8–11% |
| Router CPU (steady state, local + CAPI) | 15–20% |
Note: All benchmarks measured on a real RB5009UG+S+ with production traffic. Router CPU includes SNMP monitoring (10 s interval) and normal network forwarding. Individual add/remove operations are typically 1–3 ms per IP (median). Occasional latency spikes (p95 up to ~50 ms) are caused by RouterOS internal scheduling on large address lists.
When CrowdSec sends a new ban decision for an IP that already exists in the MikroTik address list (e.g., a 24-hour ban is replaced by a 7-day ban), the bouncer handles it automatically:
This ensures the most recent ban duration always takes effect, without creating duplicate entries. The operation typically takes ~5–8 ms (find + update).
1curl http://localhost:2112/health2# {"status":"ok","routeros_connected":true,"version":"vX.Y.Z"}
Enable with metrics.enabled: true. Available at http://localhost:2112/metrics.
| Metric | Type | Description |
|---|---|---|
crowdsec_bouncer_info | Gauge | Build info (version, RouterOS identity) |
crowdsec_bouncer_start_time_seconds | Gauge | Unix timestamp of bouncer startup |
crowdsec_bouncer_active_decisions | Gauge | Active decisions by protocol (ipv4/ipv6) |
crowdsec_bouncer_active_decisions_by_origin | Gauge | Active decisions by CrowdSec origin (crowdsec/cscli/CAPI) |
crowdsec_bouncer_decisions_total | Counter | Total decisions processed (action, protocol, origin) |
crowdsec_bouncer_errors_total | Counter | Total errors by type (api/routeros/reconcile) |
crowdsec_bouncer_operation_duration_seconds | Histogram | Operation latency (add/remove/reconcile) |
crowdsec_bouncer_routeros_connected | Gauge | RouterOS connection status (1/0) |
crowdsec_bouncer_routeros_cpu_load | Gauge | RouterOS CPU load percentage (0–100) |
crowdsec_bouncer_routeros_memory_used_bytes | Gauge | RouterOS used memory in bytes |
crowdsec_bouncer_routeros_memory_total_bytes | Gauge | RouterOS total memory in bytes |
crowdsec_bouncer_routeros_cpu_temperature_celsius | Gauge | RouterOS CPU temperature (°C) |
crowdsec_bouncer_reconciliation_total | Counter | Total reconciliation actions (added/removed) |
crowdsec_bouncer_dropped_bytes_total | Gauge | Cumulative bytes dropped by firewall rules |
crowdsec_bouncer_dropped_packets_total | Gauge | Cumulative packets dropped by firewall rules |
crowdsec_bouncer_dropped_bytes_by_proto | Gauge | Dropped bytes by protocol (ipv4/ipv6) |
crowdsec_bouncer_dropped_packets_by_proto | Gauge | Dropped packets by protocol |
crowdsec_bouncer_processed_bytes_total | Gauge | Cumulative bytes processed (evaluated) by firewall rules |
crowdsec_bouncer_processed_packets_total | Gauge | Cumulative packets processed by firewall rules |
crowdsec_bouncer_processed_bytes_by_proto | Gauge | Processed bytes by protocol (ipv4/ipv6) |
crowdsec_bouncer_processed_packets_by_proto | Gauge | Processed packets by protocol |
Note:
dropped_bytes_totalanddropped_packets_totaluse the_totalsuffix despite being Gauges. This is because they reflect cumulative counters read from RouterOS — the bouncer sets (not increments) the value each cycle, making Gauge the correct instrument type. The_totalsuffix is retained for semantic clarity.
The bouncer reports usage metrics directly to the CrowdSec LAPI (default: every 15 min). These metrics appear in the CrowdSec Console and include:
crowdsec, cscli, CAPI) and per-IP-type (ipv4, ipv6)cs-routeros-bouncer), version, OS info, startup timestampConfigure with crowdsec.lapi_metrics_interval (set to 0 to disable).
A ready-to-use Grafana dashboard is included at grafana/dashboard.json.
Import steps:
grafana/dashboard.json or paste its contentsThe dashboard provides real-time visibility into the bouncer's operation:
Dashboard panels (27 panels in 8 rows):
| Row | Panels |
|---|---|
| Overview | RouterOS Connected, Active Decisions (IPv4/IPv6/Total), Uptime, Bouncer Info |
| Active Decisions | Active Decisions Over Time, IPv4/IPv6 Ratio |
| Decision Processing | Decisions Processed (Rate), Cumulative Decisions |
| Performance & Operations | Operation Latency (p50/p95/p99), Operation Rate |
| Errors & Reconciliation | Error Rate, Total Errors, RouterOS Connection, Last Reconciliation, Reconciliation Duration |
| Dropped Traffic | Dropped Bytes, Dropped Packets, Dropped Traffic Rate, Dropped Traffic (Cumulative) |
| Processed Traffic | Processed Traffic Rate (Bytes/s, Packets/s), Drop Rate % |
| Decisions by Origin | Active Decisions by Origin, Decisions by Origin (Rate), Cumulative Decisions by Origin |
| Process Resources | Memory Usage, CPU Usage, Goroutines & File Descriptors |
/ip/service/print — api should be enabled on your routerapi policymikrotik.tls: true and the correct port (8729)/ip/firewall/filter/print on the routerfirewall.rule_placement: "top" is set in your configsudo cscli decisions listlogging.level: debug for detailed decision processing logscrowdsec.origins, ensure it includes the expected sourcescrowdsec.origins: ["crowdsec", "cscli"] to sync only local decisionsSee CONTRIBUTING.md for development setup and guidelines.
1make build # Build binary2make test # Run tests3make lint # Run linter4make docker-build # Build Docker image
A comprehensive Bash test suite validates the compiled binary against a
real MikroTik router. Tests use SSH, cscli, systemctl, and SNMP —
no Go internals are imported.
1# Setup: copy and fill in your environment2cp tests/functional/.env.example tests/functional/.env3# Edit .env with your MikroTik SSH credentials, CrowdSec API key, etc.45# Run all groups (except CAPI stress test)6tests/functional/run_tests.sh78# Run specific groups9tests/functional/run_tests.sh t1 t21011# Include CAPI stress test (~25k IPs — takes several minutes)12tests/functional/run_tests.sh --capi1314# List available groups15tests/functional/run_tests.sh --list
| Group | Tests | Description |
|---|---|---|
t1 | 7 | Data integrity — IP completeness, format, comments |
t2 | 7 | Cache consistency — live ban/unban, expiry, fast-path |
t3 | 6 | Bulk operations — reconciliation, partial sync, orphans |
t4 | 3 | Connection pool — establishment, shutdown |
t5 | 6 | Edge cases — duplicates, rapid cycle, restart idempotency |
t6 | 3 | CPU monitoring — steady-state, peak, recovery |
t7 | 5 | Timing — reconciliation time, ban/unban latency |
t8 | 10 | CAPI stress ~25k IPs (requires --capi) |
t9 | 13 | Advanced firewall config — reject-with, connection-state, log-prefix, whitelist, passthrough |
See SECURITY.md for the security policy and responsible disclosure process.