MikroTik RouterOS Firewall: Chains, Filters and Address Lists - 夜莺博客

MikroTik RouterOS Firewall: Chains, Filters and Address Lists

The RouterOS firewall filter is the traffic gatekeeper on every MikroTik router, and its three built-in chains map cleanly onto the packet's journey: input for packets destined to the router itself, forward for packets passing through it, and output for packets the router originates. Combined with connection tracking and address lists, a handful of rules protects both the router and the LAN behind it. This guide builds that firewall from scratch with RouterOS commands, then explains how packets actually flow through the stack, why connection states matter, how to auto-ban attackers with dynamic address lists, and how FastTrack, mangle and NAT interact with the rules you write.

The Three Chains

  • input - packets entering the router with a destination IP that belongs to the router (management traffic, WAN pings to the router...).
  • forward - packets passing through the router (LAN to internet).
  • output - packets originating from the router itself.

The distinction sounds academic until the first rule lands in the wrong chain. A drop rule in forward will never stop a Winbox login attempt, and an accept rule in input will never let a LAN client browse the web. Traffic that terminates on the router and traffic that transits the router are handled by two completely separate rule sets, and most "my firewall does not work" tickets come down to exactly this mismatch.

How a Packet Travels Through RouterOS

Before writing rules it helps to know the order in which RouterOS touches a packet. A frame arrives on an interface and is evaluated first by the raw table, where you can selectively bypass connection tracking. It then reaches mangle in prerouting, then connection tracking, then the nat table for destination NAT and port forwarding, and only afterwards the filter table. From there the packet is routed and dispatched to either input (local destination) or forward (remote destination); locally generated traffic enters at output.

The practical consequence is a strict precedence order: raw → mangle → conntrack → nat → filter. If a packet is marked notrack in the raw table it will show untracked as its connection state and will never match connection-state=established. If a packet is fast-tracked in the filter table, mangle marking and simple queues are bypassed for that connection. Understanding this pipeline is what turns a random collection of rules into a firewall you can reason about.

Connection States and Why They Matter

RouterOS tracks every flow in the connection table. Each rule can match on the state of the flow, and getting the states right is what keeps a firewall both secure and cheap to run:

  • new — the first packet of a flow, or a packet that does not belong to any known flow.
  • established — a packet belonging to a flow that has already seen traffic in both directions (or at least in the tracked direction for UDP).
  • related — a new flow that is associated with an existing one, such as ICMP errors or an FTP data channel.
  • invalid — a packet with no valid state, an out-of-window TCP packet, or a malformed flag combination. Attackers and broken stacks generate these in bulk.
  • untracked — a packet that bypassed connection tracking via a raw rule.

Two rules follow from this. First, always drop invalid early — it is free protection and it stops scans from consuming your state table. Second, accept established,related generously, because inspecting every packet of a flow already permitted is wasted CPU. Only genuinely new connections should have to pass the full gauntlet of port and protocol matchers.

Protecting the Router Itself (input chain)

The rule of thumb: work with new connections only (established/related already passed), accept what you need, drop the rest:

/ip firewall filter
add action=accept chain=input comment="accept established and related" connection-state=established,related
add action=accept chain=input protocol=icmp comment="allow ping to router"
add action=drop chain=input comment="drop everything else"

To allow management only from a known block, define an address list and match on it instead of accepting from anywhere:

/ip firewall address-list
add address=192.168.88.2-192.168.88.254 list=allowed_to_router

/ip firewall filter
add action=accept chain=input src-address-list=allowed_to_router
add action=accept chain=input protocol=icmp
add action=drop chain=input

In production, express "management" as an interface list rather than a list of subnets, so that adding a new management VLAN later is a one-line change:

/interface list
add name=MGMT
add name=WAN
/interface list member
add interface=bridge list=MGMT
add interface=ether1 list=WAN

/ip firewall filter
add action=drop chain=input connection-state=invalid comment="drop invalid first"
add action=accept chain=input connection-state=established,related comment="allow established"
add action=accept chain=input in-interface-list=MGMT comment="management from LAN only"
add action=accept chain=input protocol=icmp limit=10,20:packet comment="rate-limit ICMP"
add action=drop chain=input comment="drop all other new input"

Note the ICMP rule: an unlimited ping rule invites a trivial ICMP flood against the router's CPU, and the router's CPU is the one resource you cannot over-provision. Rate-limiting ICMP to something like ten packets per second keeps the diagnostic value and removes the abuse case. The same logic applies even more strongly to SSH, Winbox and the API: restrict dst-port=22, 8291 and 8728 to the management list instead of exposing them to the WAN.

Dynamic Address Lists: Auto-Blocking Attackers

Address lists are not just static groups - mangle/filter rules can add source addresses to a list with a timeout, creating an automatic ban. This classic example drops everyone who tries telnet to the router for 5 minutes:

/ip firewall address-list
add address=192.0.34.166/32 list=drop_traffic

/ip firewall mangle
add action=add-src-to-address-list address-list=drop_traffic address-list-timeout=5m chain=prerouting dst-port=23 protocol=tcp

/ip firewall filter
add action=drop chain=input src-address-list=drop_traffic

Entries added by rules are dynamic (marked D in /ip firewall address-list print) and stored in RAM only - they vanish on reboot, while static entries persist on disk.

The more useful pattern is a staged ban, where a client that keeps knocking gets moved through progressively harsher lists. This is the standard defence against SSH and Winbox brute forcing, and it scales to thousands of attempts per minute without touching the CPU:

/ip firewall filter
add chain=input protocol=tcp dst-port=22 src-address-list=ssh_blacklist action=drop \
    comment="drop blacklisted SSH, checked every packet"
add chain=input protocol=tcp dst-port=22 connection-state=new src-address-list=ssh_stage3 \
    action=add-src-to-address-list address-list=ssh_blacklist address-list-timeout=1d
add chain=input protocol=tcp dst-port=22 connection-state=new src-address-list=ssh_stage2 \
    action=add-src-to-address-list address-list=ssh_stage3 address-list-timeout=1m
add chain=input protocol=tcp dst-port=22 connection-state=new src-address-list=ssh_stage1 \
    action=add-src-to-address-list address-list=ssh_stage2 address-list-timeout=1m
add chain=input protocol=tcp dst-port=22 connection-state=new \
    action=add-src-to-address-list address-list=ssh_stage1 address-list-timeout=1m

Read it bottom-up: the first SSH attempt puts the source in ssh_stage1. A second attempt within a minute promotes it to ssh_stage2, a third to ssh_stage3, and a fourth lands it in ssh_blacklist for a full day. Because the blacklist drop rule is evaluated at the top of the chain, an abusive source is discarded with a single hash lookup rather than by walking the rest of the rule set. Duplicate lists for Winbox (dst-port=8291) and the API cost nothing extra and close the two doors most automated scanners actually try.

Protecting the LAN (forward chain)

LAN protection is the inverse: drop the undesirable, accept the rest. A hardened baseline drops LAN clients trying to reach non-public (RFC6890) address space, and drops incoming WAN packets that are not part of an established NAT session:

/ip firewall address-list
add address=10.0.0.0/8 comment="RFC1918" list=not_in_internet
add address=172.16.0.0/12 comment="RFC1918" list=not_in_internet
add address=192.168.0.0/16 comment="RFC1918" list=not_in_internet
add address=0.0.0.0/8 comment=RFC6890 list=not_in_internet
add address=169.254.0.0/16 comment="RFC6890 link-local" list=not_in_internet
add address=100.64.0.0/10 comment="CGNAT" list=not_in_internet

/ip firewall filter
add action=accept chain=forward connection-state=established,related
add action=drop chain=forward connection-state=invalid
add action=drop chain=forward comment="drop bogon from LAN" src-address-list=not_in_internet in-interface=bridge
add action=drop chain=forward comment="drop non-NATed new from WAN" connection-nat-state=!dstnat connection-state=new in-interface=ether1

Order matters: rules are evaluated top-down until a match with a terminating action. A common throughput optimization is FastTrack on established/related forward traffic so the CPU only inspects new connections - see the RouterOS docs for the exact FastTrack rule placement.

The "drop bogon from LAN" rule deserves a word of explanation because it looks backwards. Blocking private source addresses on traffic arriving from the LAN stops spoofed packets and stops a compromised host from probing internal ranges it has no business reaching, including your own management subnet. Add 224.0.0.0/4 and 240.0.0.0/4 to the same list for full coverage. The final rule handles the classic asymmetric-NAT hole: a new connection arriving on the WAN side that has no matching destination-NAT entry is either a misconfigured port forward or a scan, and both are better dropped than routed.

FastTrack: Throughput vs Visibility

FastTrack moves packets of an established flow into a fast path handled by the switch/NIC path instead of the Linux network stack. On an entry-level ARM board this can be the difference between 300 Mbit/s and 1 Gbit/s of routed throughput:

/ip firewall filter
add action=fasttrack-connection chain=forward connection-state=established,related \
    comment="FastTrack established" hw-offload=yes
add action=accept chain=forward connection-state=established,related

The FastTrack rule must be placed before the plain accept rule for the same states, otherwise the accept rule terminates the evaluation first and offload never happens. The cost is visibility and control: fast-tracked packets bypass mangle, simple queues and the queue tree, so any shaping or marking that must see individual packets of an established flow will silently stop working. If your design depends on per-connection bandwidth limits or DSCP remarking mid-flow, exclude the affected traffic from FastTrack with a matcher (for example by marking it with a connection mark first and adding connection-mark=no-mark to the FastTrack rule).

Mangle, Connection Marks and Policy Routing

The mangle table is where you paint flows with metadata. Connection marks are the efficient unit of work — mark the first packet of a flow, then match all subsequent packets against the mark without re-evaluating the original conditions:

/ip firewall mangle
add chain=prerouting in-interface=bridge dst-address-list=not_in_internet \
    action=mark-connection new-connection-mark=lan-local passthrough=yes
add chain=prerouting connection-mark=lan-local action=mark-packet \
    new-packet-mark=lan-traffic passthrough=no
add chain=prerouting connection-mark=no-mark action=mark-routing \
    new-routing-mark=via-secondary passthrough=yes

Three rules are worth internalising. Mark the connection first and the packet second, or you re-run the expensive match on every packet. Keep mangle rules in prerouting for downstream marking and output for router-generated traffic, because postrouting mangle runs after the routing decision and is useless for policy routing. And always end a chain that only adds marks with action=passthrough rather than accept, since accept in mangle ends processing of later mangle rules but does not actually forward anything.

NAT and the Firewall

NAT lives in its own table and is evaluated per-connection, not per-packet, which is why a translation is installed once and then honoured for the whole flow:

/ip firewall nat
add chain=srcnat action=masquerade out-interface-list=WAN comment="hide LAN behind WAN IP"
add chain=dstnat action=dst-nat to-addresses=192.168.88.10 to-ports=3389 \
    protocol=tcp dst-port=3389 in-interface-list=WAN comment="RDP forward"

/ip firewall filter
add action=accept chain=forward connection-nat-state=dstnat in-interface-list=WAN \
    connection-state=new comment="allow forwarded ports"

Prefer a plain src-nat with an explicit to-addresses over masquerade when the WAN address is static: masquerade re-resolves the outbound address on every new connection and adds latency, while src-nat is a single lookup. The filter-side counterpart is critical — adding a dstnat rule alone does nothing useful, because the forward chain will still hit your final drop. A dedicated accept for connection-nat-state=dstnat keeps port forwards working without punching a general hole in the forward policy.

IPv6: Do Not Forget the Other Stack

RouterOS keeps IPv6 filtering in a separate hierarchy with its own chains, and a router with IPv6 enabled but unfiltered is a wide-open host on the internet:

/ipv6 firewall filter
add action=accept chain=input connection-state=established,related
add action=accept chain=input protocol=icmpv6 comment="ND, RA, PMTU all need ICMPv6"
add action=drop chain=input connection-state=invalid
add action=drop chain=input
add action=accept chain=forward connection-state=established,related
add action=drop chain=forward connection-state=invalid
add action=drop chain=forward in-interface-list=WAN comment="no inbound new flows"

Never blanket-drop ICMPv6. Neighbour discovery, router advertisement and path MTU discovery all depend on it, and rate-limited ICMPv6 is the correct approach rather than a plain drop. Note also that address lists for IPv6 are separate objects created under /ipv6 firewall address-list; the IPv4 lists you built earlier are not visible to the IPv6 tables.

Verification

/ip firewall filter print
/ip firewall filter print stats          # packet/byte counters per rule
/ip firewall address-list print
/ip firewall connection print            # conntrack table

Counters tell the whole story: a rule with zero hits either never matches (check chain and matchers) or is shadowed by an earlier rule. The same chain logic generalizes to any vendor - compare with the multi-vendor CLI cheat sheet and, for Linux hosts instead of routers, the nftables configuration guide. VLAN-aware bridging on MikroTik is covered separately in our bridge VLAN filtering guide.

Three more commands earn their place in any troubleshooting session. /ip firewall filter print stats where chain=input narrows the counter view to one chain and turns a wall of output into a shortlist. /ip firewall connection print count-only gives you the current size of the state table, which is the number to watch when a device starts reporting "no free connection slots". And /tool torch interface=ether1 shows live traffic flows with their source and destination, which is the fastest way to confirm whether a blocked flow is even reaching the router.

Rule Hygiene: Comments, Backups and Change Control

A firewall you cannot read is a firewall you cannot safely change. Two habits prevent most outages. First, comment every rule with the reason for its existence, not just its function — "drop bogon from LAN, ticket 4412" survives staff turnover, "drop rule" does not. Second, export before you change and keep the export off the device:

/ip firewall filter export file=firewall-2026-09
/export file=full-config-2026-09
/file print where name~"firewall"
/file copy firewall-2026-09.rsc disk1/

Because /import merges rules rather than replacing them, restore order matters: start from an empty chain (/ip firewall filter remove [find]) when rolling back a broken change, otherwise you end up with duplicated rules whose counters are reset and whose ordering is unpredictable. Finally, remember that RouterOS evaluates rules in order and does not reorder them for you — a subnet placed in an address list is matched by the list's rule position, so inserting a new deny in the middle of a chain is exactly as sensitive an operation as it looks.

原文链接:https://help.mikrotik.com/docs/spaces/ROS/pages/48660574/Filter