Motivation
There is a perfectly fine article on this topic in the official RouterOS documentation. This one takes a different tack for two reasons:
- I prefer the RouterOS CLI to any of its GUIs for this purpose.
- The article assumes the happy path, where everything else is in order. This article focuses on the question, “Okay, that didn’t work, what next?”
If that last frightens you, do realize that in principle, this is a simple task, but that there are many things that can interfere. I won’t claim this article includes a comprehensive list of the possibilities and their workarounds, but if your problem isn’t covered here, it might be time to consider cloud hosting.
The Basic Rule
The colloquial term port-forwarding is technically a type of destination NAT addressing, also called “DNAT.”
Do not confuse this with source NAT or “SNAT,” a topic we will return to later. When you see the term “NAT” alone, it generally refers to a type of SNAT. It is important to be clear about this distinction in this article’s context.
The immediate point is that RouterOS uses the proper terminology in this instance, which is why a basic port-forwarding rule doesn’t use the term “port-forwarding” at all:
/ip firewall nat
add action=dst-nat chain=dstnat \
comment="forward SSH to our internal server" \
dst-port=22 protocol=tcp \
in-interface-list=WAN \
to-addresses=192.168.88.11
You can write that on a single line by leaving out the backslashes, but I’ve broken it up here for readability.
What this single rule tells RouterOS is that when a TCP connection attempt comes into the router on any of its WAN interfaces destined for TCP port 22 — the default SSH service port — we want it to forward the packet to internal host 192.168.88.11 on the same TCP port. If that host answers, the router will notice this and remember this as an “established” connection, whereupon a different default firewall rule will take effect.
In many common cases, this is all you need. The rest of this guide covers the many possible exception cases.
Before we get into that, there is one misconfiguration worth addressing early:
❌ /ip firewall filter
❌ add action=accept chain=forward \
❌ dst-port=22 protocol=tcp \
❌ in-interface-list=WAN
That is unnecessary under the default firewall configuration because its forward chain makes an exception for dstnatted traffic. The connection-nat-state marker it refers to is applied very early in the packet processing flow, before it begins processing regular firewall rules like this one. (DNAT also happens before processing begins in the input chain.) Unless you have added firewall rules above this one in the forward chain that block the incoming connection, you may consider the dstnat rule above to be the “accept” action in cases like this.
Port Translation
What you see above is called address translation, but another element of DNAT is port translation. It is not merely possible to change the TCP port in this process, it is outright advisable. Consider this improved rule:
/ip firewall nat
add action=dst-nat chain=dstnat \
comment="forward SSH to our internal server" \
dst-port=12345 protocol=tcp \
in-interface-list=WAN \
to-addresses=192.168.88.11 \
to-port=22
Why is it better? Because it’s guaranteed to frustrate the bulk of script kiddies and dumb bots.
This rule says, “When a connection attempt comes into any of the router’s WAN interfaces on TCP port 12345, send it to the SSH port (22) on the internal host 192.168.88.11.” The only difference from the first rule above is that we’ve provided the to-port parameter, which defaults to dst-port when not given in a rule like this.
There are those who will tell you that this amounts to security through obscurity, but this derisive term is only deserved when the obscurity is the sole element of security being provided. When added to actual security — as is the case with SSH — it is properly termed camouflage.
An attacker cannot hit targets they fail to see.1
One may argue that all it would take to defeat this camouflage is to scan all 65535 possible TCP ports on the target IP. The thing is, most attackers are lazy and consequently will not even try, a fact you can easily prove to yourself. Take a service that is constantly under attack and move it to a random high-numbered port. Observe that the attack rate immediately drops and remains dropped!
Why does this work? Because attackers are looking for easy targets, and as soon as you make yourself one of the less-easy targets, you fall off their to-do list. A common analogy is a petty thief walking down a residential street rattling doorknobs. If your door doesn’t even have a visible knob, a typical attacker will not view this as a puzzle worth solving, but as a fruitless time sink. In all likelihood, they will immediately move on to other potential targets.
There are a few good reasons not to move your public ports to random high-numbered ones:
- The client user’s convenience is paramount; or
- The client implementation does not allow specifying a custom port.
In all other cases, I highly encourage adding this additional bit of obscurity. If for no other reason, do it to keep the amount of noise in your logs down, so that you can more easily find events worth pursuing.
Dynamic DNS
At the basis of the above advice is that there is a way for your clients to find your router’s public IP. If you are using a typical Internet service provider, they’ve put you behind a SNAT layer, meaning your public IP changes with some frequency, which varies with the type of ISP.
In the US, it is common for always-on Internet links to grant ongoing use of the same IPv4 address over long periods. The thing is, the only time this is guaranteed is when you’re paying extra for the privilege; otherwise, typical ISP agreements allow them to change it on you at any time, at their merest whim.
The practice of keeping dynamic customer IPs stable over long periods has a few bad side effects. Irrelevant to our topic here is that it makes it easier for snoopers to track your Internet usage. The on-topic negative side effect is that it creates a bad incentive for the end user, encouraging lazy network admins to hard-code whichever IP their ISP happens to have given out at the moment. The ISP then lets the client keep that IP long enough for the lazy admin to forget this hard-coding they perpetrated, but then…surprise! Three months down the path, it changes, and the admin is left working out what happened from fuzzy memories, likely in the middle of a crisis where they damn well need the service to work!
This is not a good strategy.
In the bad old dial-up days, the IP changed with every call to the ISP, strongly incentivizing the use of dynamic DNS services. This remains a good idea even today, even if you’re lucky enough to have an ISP that give you a long-term stable dynamic IP.
Because I am the only one in my home who cares to use forwarded ports into my home LAN, my policy is to set the update time to approximately half the time between when I leave my house and arrive at the nearest working spot where I am liable to need those forwarded ports to work reliably. This gives the update a minimum of two tries to succeed, and typically many more, reducing the chance that the DNS record will be outdated while I’m away to acceptably near zero.
For RouterOS users, the go-to DDNS service is the free one MikroTik offers its customers, bundled with its confusingly-named Cloud service set. Based on forum traffic, this service does occasionally go down — thankfully, rarely — but it can take days to get it back again. It might be worth investigating the various paid service offerings if reliability is critical.2
ISP Interference
Okay, so you’ve got your DNAT rule set up, and your DDNS record is updating reliably, but you still cannot get your port-forward rule to activate. What’s going on?
For common ports like HTTP[S] and mail, the likeliest reason is that your ISP is blocking them on purpose since they are commonly used in botnet attacks. The simplest way to dodge this is to add port translation to the mix.
If that is impossible, it may be worth asking your ISP to make an exception for you. Beware, however, that typical ISP agreements preclude running servers over their links, and that merely asking risks reprisal. Therefore, think hard about how “impossible” port translation is in your situation before giving up on it.
Common RouterOS Ports
Let’s shift from our SSH example above to HTTP. Let us further say you’ve enabled WebFig on your router, but you also want to port-forward TCP port 80 to an internal web server. How does that work?
It works perfectly fine for the most part, is how.
Why?
It’s for the same packet flow reason as above: the dstnat chain is processed before TCP connection acceptance on the router.
This is why it’s critical to include the in-interface-list qualifier or similar. If you leave that off your dst-nat rule, it will affect packets from the LAN as well, which in this example effectively bounces all attempts to access WebFig back inside, toward your internal web server. If WebFig was the last RouterOS configuration service running, you just locked yourself out of the router!
With this exception in place, TCP port 80 is handled differently for hosts out on the WAN than for those on the LAN. Internal ones see the router’s WebFig interface, while external ones see your internal web server.
There is one major exception to all this:3 if you’re using RouterOS’s Let’s Encrypt feature to get free TLS certificates, the router needs to be accessible via HTTP from the WAN side. It is possible to arrange matters to allow you to have your cake and eat it, too, but you should consider adding port translation to your internal web host config in this situation.
Hairpin NAT
That brings us to a common problem with port-forwarding. What if you have a service that needs to be visible both inside the LAN and outside it, but you want to use the same DDNS name to access it? The symptom is classic: port-forwarding works from the outside, but not inside.
If you thought connecting to the public DDNS name from inside the LAN would fool RouterOS into believing the connection comes from outside, consider that it’s based on Linux, with decades of development history, covering a tremendous range of strange behaviors. It takes one heck of a lot more cleverness to fool a network stack as mature as the one in the Linux kernel. What actually happens in this case is that the router says, “Oh, that’s me; I’ll respond on the LAN interface the request came in on.”
What happens next gets complicated. It suffices to say that your connection request packet gets trapped between the default SNAT rule pointing outward and the DNAT rule pointing inward, causing the attempt to time out.
The prior link also gives the solution, but allow me to boil it down to essentials here:
/ip firewall nat
add action=masquerade chain=srcnat \
dst-address=192.168.88.11 protocol=tcp \
out-interface=LAN \
src-address=192.168.88.0/24
This is called a hairpin NAT rule because it causes connection attempts that end up at the router but destined for LAN host 192.168.88.11 to be bounced right back along the same path, preventing the packets from getting stuck bouncing between NAT rules.
Because this takes place in the srcnat chain, it needs to be placed above the default masquerade rule to override it, presuming your configuration needs it.
All this having been said, there’s a simpler alternative to hairpin NAT trickery: use the internal host IP/name while on the LAN. All the badness above happens because you attempted to connect to the public IP from the private LAN, either via direct IP or via a DDNS lookup. If you connect straight to the host on a typical home LAN, you bypass it all way down at the ARP layer.
“But I want to use a host name, not a raw IP!”
Fine then: use RouterOS to set up a LAN DNS server. You wanted to anyway, right? From inside, you connect to “http://www/” which looks up your internal web server’s LAN-side IP, 192.168.88.11 in this example. ARP then says, “Oh, that host is here on the LAN, with MAC BL:AH:DE:EE:BL:AH. Enjoy!” Problem solved.
The primary problem comes when you want to use the same logical name regardless of whether you’re on the LAN or coming in from the Internet. (A so-called “road-warrior” configuration.) If you cannot train your users to change their URLs depending on their location, you’re stuck with hairpin NAT.
PPPoE
A common failure with DNAT comes up with people using PPPoE who’ve gotten “clever” in writing the first rule above like this:
/ip firewall nat
add action=dst-nat chain=dstnat \
comment="forward SSH to our internal server" \
dst-port=22 protocol=tcp \
in-interface=ether1 \ ❌
to-addresses=192.168.88.11
Presuming ether1 is indeed the one toward the ISP, this works only if you have a direct Ethernet connection to said ISP, as with typical cable modems and fiber ONTs. With PPPoE, you’re running everything through a tunnel, which means the rule must target said tunnel, not the raw interface:
/ip firewall nat
add action=dst-nat chain=dstnat \
comment="forward SSH to our internal server" \
dst-port=22 protocol=tcp \
in-interface=pppoe-out1 \ ✅
to-addresses=192.168.88.11
But this side-steps the real issue: why is pppoe-out1 not in the default WAN list? There are other things in the default RouterOS configuration that refer to the WAN list besides what we’re covering in this article. Perhaps you have correctly converted everything to the tunnel interface name, but perhaps not. Instead of guessing, use this feature as intended: put your WAN-side interfaces into the default “WAN” list, and leave the rules referring to it alone.
CGNAT
None of the above will work if your ISP is using CGNAT, as is most common with cellular Internet connections.
Everything above depends on the NAT layers being fully under control of a single entity, your RouterOS border gateway in this article’s framing. If you need public access to an internal server in a situation like this, you need a VPN, not port-forwarding. RouterOS offers a plethora of choices, but given this article’s focus, I think you should start with its Back to Home service first, investigating the alternatives only if that won’t work for some reason.
License
This work is © 2025 by Warren Young and is licensed under CC BY-NC-SA 4.0
- ^ …except by accident, which is why your country’s army does not paint their armored rolling stock high-viz orange.
- ^ Alas, I am not in a good position to recommend a commercial DDNS service to you. The one I use has me grandfathered into a sweetheart plan that new users cannot get, which not only leaves me with every incentive to continue using a service I cannot recommend to others, I’m left with no reason to evaluate alternatives.
- ^ …which explains why I chose this point in the article to shift from SSH to HTTP in the examples