Every compromised MikroTik I’ve been asked to look at had one of two firewalls: none, or a pile of accepted-everything rules someone copied from a forum in 2015. The RouterOS firewall engine itself — netfilter under the hood — is perfectly capable. The failure mode is always human: rules added ad hoc over years, order never reasoned about, and a fasttrack rule at the top quietly bypassing half of what’s written below it.
A firewall you can trust has three properties. It drops by default, so a forgotten rule fails closed instead of open. It’s built on connection state, so you write policy about who may initiate, not about every packet direction. And you understand exactly which traffic bypasses it — because on RouterOS, with fasttrack enabled, some traffic genuinely does.
This article builds that firewall for a typical edge router, and spends real time on the parts people get wrong: rule order, fasttrack’s side effects, and why I think port knocking is mostly theater. First boot and service hardening were covered in the previous article of this series; I’m assuming that baseline here.
The Three Chains
RouterOS filter rules live in /ip firewall filter and land in one of three built-in chains, defined by where the traffic is going relative to the router:
- input — packets addressed to the router itself. SSH to the router, DNS queries to the router, ping to its interfaces.
- forward — packets passing through the router. All LAN-to-internet traffic. This is where your users live.
- output — packets the router itself originates. Almost nobody filters output, and for a router that’s usually fine.
The distinction matters because the most common firewall mistake on RouterOS is protecting forward while leaving input wide open — your users are firewalled, but the router’s own management plane is naked. Attackers don’t want your users; they want the router.
Rules in a chain evaluate top to bottom, first match wins. Order is policy. Any time you edit a RouterOS firewall, print it and re-read it as a sequence, not a set.
Connection Tracking: The State Machine You Build On
The connection tracker watches every flow through the router and assigns each packet a state:
new— first packet of a connection the tracker hasn’t seen.established— belongs to a connection that has seen traffic both ways.related— a new connection spawned by an existing one (ICMP errors, FTP data).invalid— packets that match no known connection and can’t legitimately start one. TCP ACKs from nowhere, stray RSTs, mid-stream packets after a tracker restart.untracked— traffic explicitly excluded from tracking via raw rules.
You can watch the table live:
/ip firewall connection print/ip firewall connection tracking printThe entire strategy of a stateful firewall is: accept established,related first and unconditionally, drop invalid, and then spend your actual policy rules only on new. This makes the expensive decision — may this connection exist? — exactly once per flow, and it means return traffic never needs its own rules.
Drop invalid early and always. Invalid packets are never legitimate on a healthy network, and several classes of scanning and spoofing show up as exactly this state.
The Input Chain: Drop by Default
Here’s my input chain for an edge router. It’s a superset of the RouterOS default configuration’s logic, made explicit:
/ip firewall address-listadd list=mgmt address=10.10.99.0/24 comment="management hosts"
/ip firewall filter# 1. Fast path for everything already approvedadd chain=input action=accept connection-state=established,related,untracked \ comment="accept established/related"
# 2. Garbage disposaladd chain=input action=drop connection-state=invalid \ comment="drop invalid"
# 3. ICMP - yes, allow it. Path MTU discovery needs it.add chain=input action=accept protocol=icmp \ comment="accept ICMP"
# 4. Management, only from the management listadd chain=input action=accept protocol=tcp dst-port=22,8291 \ src-address-list=mgmt comment="ssh+winbox from mgmt"
# 5. Services the LAN legitimately asks of the routeradd chain=input action=accept protocol=udp dst-port=53 \ in-interface-list=LAN comment="DNS from LAN"add chain=input action=accept protocol=udp dst-port=123 \ in-interface-list=LAN comment="NTP from LAN"
# 6. Everything else diesadd chain=input action=drop comment="drop all other input"Two deliberate opinions here. First, ICMP is accepted wholesale — the “block ping” reflex breaks path MTU discovery and buys you nothing; anyone scanning you will find the router anyway. If unlimited ICMP offends you, rate-limit it instead of dropping it. Second, the final rule is an explicit drop, not a reliance on default behavior. RouterOS chains default to accept when no rule matches. Without rule 6, this firewall is a suggestion.
Note the in-interface-list=LAN matchers: interface lists (created under /interface list) let the same ruleset survive hardware changes and port reshuffles. Never write firewall rules against ether3 when you can write them against a list.
The Forward Chain and Fasttrack
The forward chain protects your users and — critically — decides what the internet may initiate toward them:
/ip firewall filter# 1. Fasttrack established flows (read the caveats below!)add chain=forward action=fasttrack-connection hw-offload=yes \ connection-state=established,related comment="fasttrack"
# 2. The rule fasttrack shadows still must exist for non-fasttracked packetsadd chain=forward action=accept connection-state=established,related,untracked \ comment="accept established/related"
# 3. Garbageadd chain=forward action=drop connection-state=invalid \ comment="drop invalid"
# 4. The internet may not initiate connections inward,# except flows that dstnat explicitly created (port forwards)add chain=forward action=drop connection-state=new \ connection-nat-state=!dstnat in-interface-list=WAN \ comment="drop WAN-originated except port forwards"That last rule is elegant and worth understanding: instead of enumerating your port forwards in the filter table, it drops any WAN-originated connection that didn’t pass through a dstnat rule. Add a port forward in NAT, and the firewall automatically permits it. Delete the forward, access closes. One less place for the two tables to drift apart. (NAT itself is the next article in this series.)
What Fasttrack Actually Does — and Breaks
Fasttrack marks a connection so its subsequent packets take an abbreviated path through the kernel, skipping most packet processing. On a small CPU pushing gigabit NAT, it’s the difference between line rate and a melted router — and with hw-offload=yes, capable switch chips forward the flow entirely in hardware.
The price: fasttracked packets bypass nearly everything. Specifically, they skip:
- the rest of the firewall filter rules,
- mangle — so policy routing marks and MSS clamping don’t apply,
- simple queues and queue trees — your QoS silently sees only a trickle of packets,
- IP accounting and most per-packet features.
The failure mode is insidious: you deploy queues, they appear to work in testing (the first few packets of each connection aren’t fasttracked yet), and under real load nothing is shaped. If you run QoS, per-connection mangle, or traffic accounting on a box, don’t fasttrack the traffic you need to touch — either remove the fasttrack rule or exclude specific traffic from it with matchers before it.
Fasttrack is not all-or-nothing per router; it’s per-connection, decided by your rule’s matchers. A common compromise: fasttrack everything except the subnets subject to queues.
Address Lists: Your Firewall’s Data Structures
Address lists decouple policy (rules) from data (who the rule applies to). Static use is obvious:
/ip firewall address-listadd list=bogons address=10.0.0.0/8add list=bogons address=172.16.0.0/12add list=bogons address=192.168.0.0/16add list=bogons address=100.64.0.0/10
/ip firewall filteradd chain=input action=drop in-interface-list=WAN src-address-list=bogons \ place-before=0 comment="drop spoofed sources from WAN"The dynamic use is where RouterOS shines: rules can write to lists via add-src-to-address-list, with timeouts. Classic self-populating blocklist for SSH scanners:
/ip firewall filter# Anyone hitting tcp/22 from WAN goes on a blocklist for a dayadd chain=input action=add-src-to-address-list protocol=tcp dst-port=22 \ in-interface-list=WAN address-list=ssh_scanners \ address-list-timeout=1d comment="tag ssh scanners" place-before=0add chain=input action=drop src-address-list=ssh_scanners \ place-before=0 comment="drop tagged scanners"Entries added with a timeout are dynamic — they age out on their own and don’t survive reboot, which is exactly what you want for scanner lists. Check what you’ve caught:
/ip firewall address-list print where list=ssh_scannersPort Knocking: A Skeptic’s Note
Every MikroTik tutorial site has the port-knocking recipe: a chain of add-src-to-address-list rules where hitting secret port A then secret port B within a timeout adds you to an allowed list. It works, and the RouterOS primitives make it genuinely easy to build.
I don’t deploy it, and I’d argue you shouldn’t either. The knock sequence is a static shared secret transmitted in cleartext — anyone who can observe your traffic once (coffee-shop WiFi, a compromised upstream) owns the sequence forever. It adds real operational friction (try explaining knocking to the colleague on call), and it protects a service that should be protected properly instead.
The modern answer is WireGuard: a silent UDP port that doesn’t respond to unauthenticated packets at all gives you the same “invisible service” property, but with actual cryptography instead of a party trick. Put management behind WireGuard plus a src-address-list, and delete the knocking rules. Port knocking made sense when the alternative was exposing SSH; that hasn’t been the alternative for a decade.
Logging: Enough to Investigate, Not Enough to Drown
Every filter rule takes log=yes log-prefix="...". The temptation is to log the final drop rule; on an edge router, resist it — WAN background radiation will bury anything useful. Log deliberately:
/ip firewall filter# Log NEW WAN attempts to management ports specifically - that's signaladd chain=input action=drop protocol=tcp dst-port=22,8291 \ in-interface-list=WAN log=yes log-prefix="MGMT-PROBE: " \ place-before=[find comment="drop all other input"]And send firewall logs somewhere that survives a reboot, because the on-box memory buffer doesn’t:
/system logging action set remote remote=10.10.99.50 remote-port=514/system logging add topics=firewall action=remoteA useful pattern during changes: temporarily add log=yes to a rule you’re debugging, watch /log print follow where topics~"firewall", then turn it off. Permanent logging is for the handful of events you’d actually page on.
Verification
A firewall you haven’t tested is a hypothesis. Three checks:
# 1. Are rules matching what you think? Counters tell the truth./ip firewall filter print stats
# 2. Watch live traffic on the WAN interface - torch is your friend/tool torch interface=ether1 protocol=tcp port=any
# 3. Confirm state handling: this should show your flows with states/ip firewall connection print where tcp-state=establishedThen test from outside: nmap the WAN address from a VPS and confirm only intended ports answer. Reset counters (/ip firewall filter reset-counters-all), generate known traffic, and confirm the right rule’s counter moves — a rule with a permanently zero counter is either dead weight or shadowed by something above it, and both deserve investigation.
Closing Thoughts
The ruleset in this article is maybe twenty lines. That’s a feature. Every rule you can’t explain is a rule that will eventually surprise you, and firewalls fail through accumulated surprises. Drop by default, accept by state, keep policy in address lists rather than in rule sprawl, and treat fasttrack as a performance contract with known exclusions — not magic.
Rule order recap, because it’s the thing that bites: established/related first, invalid dropped second, explicit accepts, explicit final drop. Fasttrack, if used, sits above it all and exempts what it matches from everything below — including the QoS you haven’t deployed yet but will.
Next in this series: NAT beyond the default masquerade rule — including how that connection-nat-state=!dstnat trick from the forward chain connects the two tables, and the hairpin problem everyone hits the first time they host anything. For the VyOS take on stateful firewalling, my VyOS Guide covers the equivalent zone-based design.