Berik Ashimov // senior.it.engineer
Download CV
POSTS / MIKROTIK

DDoS on RouterOS 7: What a CPU-Routed Box Can Actually Absorb

// series · MikroTik RouterOS 7 Guide part 31 of 35
  1. ... 28 earlier
  2. Centralized AAA on RouterOS 7: RADIUS for Logins, VPN and User Manager
  3. Locking Down the RouterOS Management Plane: The Attack Surface You Forgot
  4. DDoS on RouterOS 7: What a CPU-Routed Box Can Actually Absorb
  5. The RouterOS Troubleshooting Toolkit: Torch, Sniffer, Profile, and a Method
  6. RoMON and Out-of-Band Access: Reaching Routers You Just Broke
  7. ... 2 later
view the full series →

The first thing that dies on a MikroTik during a flood is not the link. It’s connection tracking. A hAP or even a CCR will happily forward line-rate garbage right up until the conntrack table fills, and then it stops forwarding everything — including the legitimate traffic the attack was aimed at. The attacker didn’t saturate your pipe; they exhausted a state table you forgot you had.

That’s the mental model for this whole article. DDoS defense on RouterOS is not about “blocking the attack.” It’s about keeping the router’s own resources — CPU cycles and conntrack entries — from becoming the bottleneck before the link does. Everything cheap happens in the raw table, before connection tracking. Everything expensive happens after. Your job is to move as much classification as possible into the cheap zone.

And be honest about the ceiling: a CPU-routed box cannot scrub a volumetric attack bigger than its upstream link. I covered the real answers to that — RTBH and BGP Flowspec — in the VyOS Guide, and they apply unchanged here. RouterOS is the edge layer, not the scrubbing center.

Where Packets Get Expensive

RouterOS processes an inbound packet roughly in this order: raw prerouting, connection tracking, mangle, filter, queues. The cost curve is steep:

  • A raw table drop costs almost nothing — no conntrack entry is ever created.
  • A filter table drop already paid for a conntrack lookup and possibly a new entry.
  • A packet that reaches queues paid for everything.

So the strategy writes itself: detect in filter (where you have connection state), block in raw (where blocking is cheap), and exempt trusted high-volume flows from conntrack entirely.

SYN Flood: Cookies First

RouterOS 7 exposes the kernel’s SYN cookie mechanism, and it should be on at any Internet edge:

Terminal window
/ip settings set tcp-syncookies=yes

With cookies enabled, the router (for connections terminating on it) and hosts behind it are no longer trivially DoS-able by half-open connections — the SYN backlog stops being a finite resource for the router’s own services. This protects input traffic: your WinBox, SSH, API, BGP sessions.

For forwarded SYN floods aimed at servers behind the router, cookies on the router don’t help — the server’s stack has to cope. What the router can do is cap the per-source new-connection rate before the flood reaches the server:

Terminal window
/ip firewall filter
# Cap new TCP connections per source; offenders go to a list
add chain=forward protocol=tcp tcp-flags=syn connection-state=new \
limit=200,50:packet action=accept comment="SYN budget"
add chain=forward protocol=tcp tcp-flags=syn connection-state=new \
action=add-src-to-address-list address-list=syn-flooders \
address-list-timeout=10m comment="over budget -> list"

Then the actual drop lives in raw, where it’s cheap:

Terminal window
/ip firewall raw
add chain=prerouting src-address-list=syn-flooders action=drop \
comment="drop SYN flooders pre-conntrack"

This split matters. The filter rules run only on packets that survived raw, so once a source is listed, its packets never touch conntrack again. The 10-minute timeout makes the block self-healing — spoofed or recycled addresses age out on their own.

The Official Detect-and-Drop Pattern

MikroTik’s own DDoS Protection guide uses a dst-limit jump chain to identify attacker/target pairs, and it’s a solid pattern worth using verbatim. The idea: any source exceeding 32 new connections per second to one destination is an attacker; the destination is a target; the raw table drops the intersection.

Terminal window
/ip firewall filter
add chain=forward connection-state=new action=jump jump-target=detect-ddos
add chain=detect-ddos dst-limit=32,32,src-and-dst-addresses/10s action=return
add chain=detect-ddos action=add-dst-to-address-list \
address-list=ddos-targets address-list-timeout=10m
add chain=detect-ddos action=add-src-to-address-list \
address-list=ddos-attackers address-list-timeout=10m
/ip firewall raw
add chain=prerouting src-address-list=ddos-attackers \
dst-address-list=ddos-targets action=drop

Read the dst-limit carefully: 32 packets per second with a burst of 32, tracked per source-destination pair, with a 10-second expiration on the tracking entry. Sources under the limit hit the return and never reach the list rules. Tune 32/32 to your traffic — a busy NAT gateway in front of hundreds of users needs a much higher budget than a leaf site.

The elegance is the pairing: you don’t block an attacker globally, you block them toward the victim. False positives against a busy but legitimate source only cost them access to the attacked host, not the whole network.

UDP Floods and Conntrack Exhaustion

UDP floods are nastier than SYN floods on RouterOS specifically because of connection tracking. Every spoofed UDP packet with a fresh source tuple creates a conntrack entry that lingers for the UDP timeout. Watch the table during an incident:

Terminal window
/ip firewall connection tracking print
# max-entries is derived from RAM; when total-entries approaches it,
# the router starts dropping NEW flows -- all of them, not just attack traffic
/ip firewall connection print count-only where protocol=udp

Two defenses, in order of preference.

First: notrack for known-good, high-volume flows. If you run a DNS resolver, a VoIP platform, or a WireGuard concentrator behind this router, that traffic doesn’t need stateful tracking — you know exactly what it is. Exempt it in raw:

Terminal window
/ip firewall raw
# DNS server at 10.10.20.53 -- don't track queries or responses
add chain=prerouting dst-address=10.10.20.53 protocol=udp dst-port=53 \
action=notrack
add chain=prerouting src-address=10.10.20.53 protocol=udp src-port=53 \
action=notrack

Two things to know before you deploy notrack. Untracked packets show connection-state=untracked, so your filter rules must accept them explicitly — a policy of “accept established,related, drop the rest” will silently kill notracked flows. And NAT depends on conntrack, so you cannot notrack traffic that must be translated. This is exactly the same trade I described for VyOS; the mechanics are identical, only the syntax differs.

Second: drop obvious garbage in raw. Anything that can’t legitimately arrive on your WAN — bogons as source, UDP to ports you don’t serve — should die in prerouting:

Terminal window
/ip firewall address-list
add list=bogons address=10.0.0.0/8
add list=bogons address=172.16.0.0/12
add list=bogons address=192.168.0.0/16
add list=bogons address=100.64.0.0/10
add list=bogons address=127.0.0.0/8
add list=bogons address=169.254.0.0/16
/ip firewall raw
add chain=prerouting in-interface-list=WAN src-address-list=bogons action=drop
add chain=prerouting in-interface-list=WAN protocol=udp \
dst-port=19,111,123,1900,11211 action=drop \
comment="amplification vectors we never serve on WAN"

You can also shorten UDP conntrack timeouts during an incident to make the table churn faster:

Terminal window
/ip firewall connection tracking set udp-timeout=10s generic-timeout=1m

Concurrent Connection Limits

Rate limits catch fast attacks; connection-limit catches slow ones — a botnet holding thousands of open connections to your services:

Terminal window
/ip firewall filter
add chain=forward protocol=tcp connection-limit=200,32 \
action=add-src-to-address-list address-list=conn-hogs \
address-list-timeout=10m comment="over 200 concurrent conns per /32"
/ip firewall raw
add chain=prerouting src-address-list=conn-hogs action=drop

The ,32 is the netmask for grouping — count per host. Use ,24 to count per /24 if attackers rotate within a subnet. Again: NAT gateways and proxies legitimately hold hundreds of connections; whitelist them with an accept above this rule or you’ll blackhole your own infrastructure.

Rate-Limiting ICMP

ICMP floods are rarely the main event, but capping them costs nothing:

Terminal window
/ip firewall filter
add chain=input protocol=icmp limit=20,10:packet action=accept
add chain=input protocol=icmp action=drop

Twenty ICMP packets per second with a burst of ten is generous for legitimate ping and PMTUD, and irrelevant to a flood. Do not drop ICMP wholesale — fragmentation-needed messages are load-bearing, especially with tunnels in the path. RouterOS also has a kernel-level icmp-rate-limit under /ip settings governing the router’s own ICMP generation; the default is fine, leave it alone.

Verification Under Fire

During an incident, three commands tell you what’s happening:

Terminal window
# Who is being blocked right now, and how many
/ip firewall address-list print count-only where list=ddos-attackers
/ip firewall address-list print where list=ddos-targets
# Are the raw drops actually catching traffic
/ip firewall raw print stats
# Is conntrack surviving
/ip firewall connection tracking print

If raw rule counters are climbing and total-entries has stopped growing, the design is working: garbage dies before it becomes state. Then check CPU:

Terminal window
/tool profile duration=10

If firewall or networking is pinning a core while raw rules do the dropping, you’ve hit the platform’s ceiling — the packets are cheap individually, but there are simply too many of them.

The Honest Ceiling

Everything above assumes the attack fits through your upstream link and the router has CPU to classify it. Neither holds for a real volumetric attack. A 40 Gbps flood into a 1 Gbps circuit is decided at your ISP’s edge, not yours — the raw table cannot drop packets that never arrive because the link ahead of you is full.

That’s when you need the tools that operate upstream: RTBH to sacrifice one destination and save the rest, and BGP Flowspec to push a match-and-drop rule into your provider’s hardware. I wrote both up in the VyOS Guide, and the BGP side works the same from RouterOS — announce the victim /32 with your provider’s blackhole community and the traffic dies in their core. Have that community documented and the filter rule pre-built before the incident; 3 AM is a bad time to read your transit provider’s communities page.

Closing Thoughts

Layer the defense in cost order. SYN cookies: free, always on. Raw-table drops for bogons and never-served ports: near-free, always on. Notrack for your known heavy flows: removes your biggest self-inflicted risk, conntrack exhaustion. Detect-in-filter, block-in-raw with timed address lists: the workhorse that makes blocking self-healing. And a rehearsed RTBH/Flowspec escalation path for the day the flood is bigger than the pipe — because eventually it will be, and no firewall rule on a CPU-routed box changes that arithmetic.

// share