Firewall Rules
How the bouncer creates and manages RouterOS firewall rules.
Rules the daemon writes
Section titled “Rules the daemon writes”The bouncer does not write one rule per table. It writes an ordered block per chain, and it writes that block once for every protocol family it is enabled for — firewall.ipv4.enabled and firewall.ipv6.enabled are both true by default, so the stock configuration builds every rule below twice, against crowdsec-banned for IPv4 and crowdsec6-banned for IPv6. A block is also built once per entry in firewall.filter.chains and firewall.raw.chains; add a chain there and the whole block is repeated in it.
Inside a block the order is fixed — whitelist, counting, deny — and the block is only moved into position once every rule in it exists, so RouterOS never evaluates a half-built block.
Created by default
Section titled “Created by default”8 rules on the router with a stock configuration: 4 rule kinds × 2 protocol families.
Filter countingcounts only
Counts every packet the filter chain hands to the bouncer's block, which is what the processed byte and packet metrics report.
/ip/firewall/filter add chain=input action=passthrough comment="crowdsec-bouncer:filter-input-counting-v4 @cs-routeros-bouncer"/ipv6/firewall/filter add chain=input action=passthrough comment="crowdsec-bouncer:filter-input-counting-v6 @cs-routeros-bouncer"Created by default. Gated on
metrics.track_processed(defaulttrue).It matches no address list and takes no decision — passthrough only increments counters — which is exactly why it looks like a stray rule on the router.
Attributes that appear only when configured:
in-interface(firewall.block_input.interface),in-interface-list(firewall.block_input.interface_list)Filter denydrops
Drops inbound traffic whose source address is on the banned list, after connection tracking has run.
/ip/firewall/filter add chain=input action=drop src-address-list=crowdsec-banned comment="crowdsec-bouncer:filter-input-input-v4 @cs-routeros-bouncer"/ipv6/firewall/filter add chain=input action=drop src-address-list=crowdsec6-banned comment="crowdsec-bouncer:filter-input-input-v6 @cs-routeros-bouncer"Created by default. Gated on
firewall.filter.enabled(defaulttrue).Set firewall.deny_action to reject and this rule rejects instead, carrying reject-with when one is configured.
Attributes that appear only when configured:
connection-state(firewall.filter.connection_state),in-interface(firewall.block_input.interface),in-interface-list(firewall.block_input.interface_list),reject-with(firewall.reject_with),log(firewall.log),log-prefix(firewall.log_prefix)Raw countingcounts only
Counts every packet the raw chain hands to the bouncer's block, feeding the same processed metrics as its filter twin.
/ip/firewall/raw add chain=prerouting action=passthrough comment="crowdsec-bouncer:raw-prerouting-counting-v4 @cs-routeros-bouncer"/ipv6/firewall/raw add chain=prerouting action=passthrough comment="crowdsec-bouncer:raw-prerouting-counting-v6 @cs-routeros-bouncer"Created by default. Gated on
metrics.track_processed(defaulttrue).Like the filter counting rule it matches no address list; its counters are what makes “evaluated” and “dropped” two separate numbers.
Attributes that appear only when configured:
in-interface(firewall.block_input.interface),in-interface-list(firewall.block_input.interface_list)Raw denydrops
Drops banned sources before connection tracking, which is the cheapest place in RouterOS to discard a flood.
/ip/firewall/raw add chain=prerouting action=drop src-address-list=crowdsec-banned comment="crowdsec-bouncer:raw-prerouting-input-v4 @cs-routeros-bouncer"/ipv6/firewall/raw add chain=prerouting action=drop src-address-list=crowdsec6-banned comment="crowdsec-bouncer:raw-prerouting-input-v6 @cs-routeros-bouncer"Created by default. Gated on
firewall.raw.enabled(defaulttrue).RouterOS raw rules cannot reject, so firewall.deny_action: reject is written here as drop — this is the one rule whose action does not follow that setting.
Attributes that appear only when configured:
in-interface(firewall.block_input.interface),in-interface-list(firewall.block_input.interface_list),log(firewall.log),log-prefix(firewall.log_prefix)
The two passthrough counting rules are the ones no earlier version of this page listed, and they are the reason a stock router shows eight bouncer rules where the documentation used to describe four. They are created by metrics.track_processed, which defaults to true.
A counting rule matches no address list, carries no src-address-list, and takes no decision: passthrough in RouterOS only increments the rule’s byte and packet counters and moves on. That is deliberate — the bouncer reads those counters to report how much traffic its chains evaluated, which is the denominator for how much of it was dropped. It is also why the rule looks, on a router you are auditing by eye, exactly like somebody’s forgotten scratch rule. It is not one.
metrics.track_processed is read on its own, not behind metrics.enabled — which defaults to false. A router whose bouncer exposes no metrics endpoint at all still carries both counting rules, quietly counting for nobody. Setting metrics.track_processed: false removes them, and with them the crowdsec_bouncer_processed_* metrics; the dropped counters, which come from the deny rules themselves, are unaffected.
Created only when a setting asks for them
Section titled “Created only when a setting asks for them”Filter whitelistaccepts
Accepts traffic from an address list you control before any bouncer rule can drop it, so a source you have vouched for is never blocked by a CrowdSec decision.
/ip/firewall/filter add chain=input action=accept src-address-list=crowdsec-whitelist comment="crowdsec-bouncer:filter-input-whitelist-v4 @cs-routeros-bouncer"/ipv6/firewall/filter add chain=input action=accept src-address-list=crowdsec-whitelist comment="crowdsec-bouncer:filter-input-whitelist-v6 @cs-routeros-bouncer"Not created by default. Requires
firewall.block_input.whitelist(defaultunset).It is first in the block on purpose: RouterOS evaluates a chain top to bottom, so an accept placed after the drop would never be reached.
Attributes that appear only when configured:
connection-state(firewall.filter.connection_state),in-interface(firewall.block_input.interface),in-interface-list(firewall.block_input.interface_list),log(firewall.log),log-prefix(firewall.log_prefix)Raw whitelistaccepts
The same exemption as the filter whitelist, one step earlier in the pipeline, so a vouched-for source is never dropped before connection tracking either.
/ip/firewall/raw add chain=prerouting action=accept src-address-list=crowdsec-whitelist comment="crowdsec-bouncer:raw-prerouting-whitelist-v4 @cs-routeros-bouncer"/ipv6/firewall/raw add chain=prerouting action=accept src-address-list=crowdsec-whitelist comment="crowdsec-bouncer:raw-prerouting-whitelist-v6 @cs-routeros-bouncer"Not created by default. Requires
firewall.block_input.whitelist(defaultunset).The raw table has no connection tracking, so this rule never carries connection-state even when firewall.filter.connection_state is set.
Attributes that appear only when configured:
in-interface(firewall.block_input.interface),in-interface-list(firewall.block_input.interface_list),log(firewall.log),log-prefix(firewall.log_prefix)Output blockdrops
Stops the router itself from opening traffic towards a banned destination — the only rule here that matches on destination rather than source.
/ip/firewall/filter add chain=output action=drop dst-address-list=crowdsec-banned comment="crowdsec-bouncer:filter-output-output-v4 @cs-routeros-bouncer"/ipv6/firewall/filter add chain=output action=drop dst-address-list=crowdsec6-banned comment="crowdsec-bouncer:filter-output-output-v6 @cs-routeros-bouncer"Not created by default. Requires
firewall.block_output.enabled(defaultfalse).Its block gets no counting rule, so output traffic never appears in the processed metrics.
Attributes that appear only when configured:
src-address (negated)(firewall.block_output.passthrough_v4),src-address-list (negated)(firewall.block_output.passthrough_v4_list),out-interface(firewall.block_output.interface),out-interface-list(firewall.block_output.interface_list),reject-with(firewall.reject_with),log(firewall.log),log-prefix(firewall.log_prefix)
Rule creation flow
Section titled “Rule creation flow”On startup, the bouncer follows this sequence:
-
Check for existing rules
Scans for bouncer-managed rules by matching the comment pattern.
-
Reuse or create
A rule whose exact comment is already on the router is adopted, not duplicated; anything missing is created.
-
Place the block at the configured position
Places the managed block according to the effective
rule_placementfor that protocol and table: top, bottom, a numeric RouterOS position, or an anchor comment.
Rule placement
Section titled “Rule placement”The bouncer places related rules as ordered blocks. A typical input block includes the following rules in order: whitelist (when configured), counting (when processed metrics are enabled), then the deny/reject rule. Placement happens after all rules in the block exist, so the internal order stays stable.
Placement is menu-local. A filter block is positioned within /ip firewall filter or /ipv6 firewall filter; a raw block is positioned within /ip firewall raw or /ipv6 firewall raw.
The bouncer can place its managed block (the related rules it creates and moves together) at the top, at the bottom, at a numeric RouterOS print position, or relative to an existing comment owned by another rule. If output blocking is enabled, the output block is moved with the same internal ordering. By default, the bouncer reuses the same placement strategy for IPv4 and IPv6. Table and protocol overrides can send filter, raw, IPv4, and IPv6 blocks to different locations. Precedence is global placement, global table override, protocol override, then protocol table override.
Placement runs against the candidates: every rule already in that menu whose comment does not carry the @cs-routeros-bouncer signature. The bouncer’s own rules are excluded from that count, so a numeric position means the same place whether or not the block is already on the router. The diagram below is the whole mechanism, retries included — each rung that fails falls to the next, and the last rung is always leave the block where RouterOS appended it. Placement never aborts startup.
Two rungs are worth reading twice. The retry is per candidate, not per attempt: when RouterOS refuses to move the block in front of a rule — a dynamic or built-in rule, for instance — the bouncer targets the next rule down and tries again, which is why a top block can settle a few positions lower than the name suggests. And fallback only exists for the comment strategies; a numeric position past the end has no anchor to miss, so it appends instead.
A move is not atomic across the block either. Each rule is moved on its own, in block order, so a failure part-way through can leave the block split around the target; the ordering is completed by the next placement pass.
| Strategy | Behavior |
|---|---|
top | Moves the block before the first usable non-bouncer rule. If RouterOS refuses the position, for example before dynamic or built-in rules, the bouncer retries lower positions. |
bottom | Leaves newly-created rules appended at the end. |
position | Requires position and uses zero-based RouterOS print numbering, excluding existing bouncer rules. Out-of-range positions append at bottom. |
before_comment | Moves the block before the first non-bouncer rule whose comment matches the configured anchor. |
after_comment | Moves the block after the matched anchor by inserting before the next non-bouncer rule; if the anchor is last, the block remains appended. |
Comment placement uses fallback (top by default, or bottom) when the anchor is missing or cannot be used. Numeric position ignores fallback because an out-of-range position naturally means append.
Rule identification
Section titled “Rule identification”Rules are identified by a structured comment:
{prefix}:{type}-{chain}-{direction}-{protocol} @cs-routeros-bouncer| Part | Values |
|---|---|
prefix | Configurable via comment_prefix (default: crowdsec-bouncer) |
type | filter or raw |
chain | input, forward, prerouting, output |
direction | input, output, whitelist, or counting |
protocol | v4 or v6 |
RouterOS prints the comment above the rule it belongs to, so the whole managed block is visible in one query:
/ip/firewall/filter print where comment~"crowdsec"# 0 ;;; crowdsec-bouncer:filter-input-counting-v4 @cs-routeros-bouncer# chain=input action=passthrough# 1 ;;; crowdsec-bouncer:filter-input-input-v4 @cs-routeros-bouncer# chain=input action=drop src-address-list=crowdsec-bannedCleanup, and what is left behind
Section titled “Cleanup, and what is left behind”When the bouncer receives SIGTERM or SIGINT:
-
Remove the rules
Every firewall rule the bouncer created is removed, counting rules included.
-
Leave the address lists alone
Address-list entries are not removed. They expire on the MikroTik timeout they were written with, which is the duration of the CrowdSec decision that created them.
This design means protection continues after the bouncer stops, for whatever is left of each entry’s timeout, and a quick restart never leaves the router unprotected in the gap. It also means no mass delete has to run during shutdown.