Skip to content

Firewall Rules

How the bouncer creates and manages RouterOS firewall rules.

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.

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"
    Table
    /ip/firewall/filter · /ipv6/firewall/filter
    Chain
    input
    Action
    passthrough
    Address list
    none

    Created by default. Gated on metrics.track_processed (default true).

    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"
    Table
    /ip/firewall/filter · /ipv6/firewall/filter
    Chain
    input
    Action
    drop
    Address list
    crowdsec-banned · crowdsec6-banned

    Created by default. Gated on firewall.filter.enabled (default true).

    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"
    Table
    /ip/firewall/raw · /ipv6/firewall/raw
    Chain
    prerouting
    Action
    passthrough
    Address list
    none

    Created by default. Gated on metrics.track_processed (default true).

    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"
    Table
    /ip/firewall/raw · /ipv6/firewall/raw
    Chain
    prerouting
    Action
    drop
    Address list
    crowdsec-banned · crowdsec6-banned

    Created by default. Gated on firewall.raw.enabled (default true).

    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.

  • 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"
    Table
    /ip/firewall/filter · /ipv6/firewall/filter
    Chain
    input
    Action
    accept
    Address list
    crowdsec-whitelist (example; the name is yours)

    Not created by default. Requires firewall.block_input.whitelist (default unset).

    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"
    Table
    /ip/firewall/raw · /ipv6/firewall/raw
    Chain
    prerouting
    Action
    accept
    Address list
    crowdsec-whitelist (example; the name is yours)

    Not created by default. Requires firewall.block_input.whitelist (default unset).

    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"
    Table
    /ip/firewall/filter · /ipv6/firewall/filter
    Chain
    output
    Action
    drop
    Address list
    crowdsec-banned · crowdsec6-banned

    Not created by default. Requires firewall.block_output.enabled (default false).

    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)

On startup, the bouncer follows this sequence:

  1. Check for existing rules

    Scans for bouncer-managed rules by matching the comment pattern.

  2. Reuse or create

    A rule whose exact comment is already on the router is adopted, not duplicated; anything missing is created.

  3. Place the block at the configured position

    Places the managed block according to the effective rule_placement for that protocol and table: top, bottom, a numeric RouterOS position, or an anchor comment.

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.

Once every rule in a block exists, the bouncer reads the configured strategy. The bottom strategy stops there. Every other strategy first lists the rules in that firewall menu and keeps the ones without the bouncer signature as candidates; if the listing fails, the block is left at the bottom. Top and position target candidate zero or the configured index, and an index past the last candidate leaves the block at the bottom. The comment strategies look for an anchor comment, matched exactly or as a substring, and target the anchor itself for before_comment or the candidate after it for after_comment; an after_comment anchor that is the last candidate leaves the block at the bottom. A missing anchor, or a move RouterOS refuses, falls back to top by default or to bottom if configured. The move walks down the candidate list, trying the next candidate each time RouterOS refuses, and leaves the block at the bottom once no candidate is left.

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.

StrategyBehavior
topMoves 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.
bottomLeaves newly-created rules appended at the end.
positionRequires position and uses zero-based RouterOS print numbering, excluding existing bouncer rules. Out-of-range positions append at bottom.
before_commentMoves the block before the first non-bouncer rule whose comment matches the configured anchor.
after_commentMoves 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.

Rules are identified by a structured comment:

{prefix}:{type}-{chain}-{direction}-{protocol} @cs-routeros-bouncer
PartValues
prefixConfigurable via comment_prefix (default: crowdsec-bouncer)
typefilter or raw
chaininput, forward, prerouting, output
directioninput, output, whitelist, or counting
protocolv4 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-banned

When the bouncer receives SIGTERM or SIGINT:

  1. Remove the rules

    Every firewall rule the bouncer created is removed, counting rules included.

  2. 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.