Router Set Up and Working Except for Port Forwarding From LAN

The router is set up very similar to defaults, except

  • DNS and DHCP are disabled (both are on pihole)


  • ether5 is my WAN interface, as I wanted to power the router from a switch on my lan via POE


  • port forwarding NAT rules have been added

Other than than the Firewall filter rules are the default ones.

When trying to access forwarded services (80,443,etc) from within the LAN the forwarding takes me to the router (ex. webfig on 80) and not to the IP I have forwarded to.

The port forwarding works as expected when the request actually originates from the outside (tested from mobile connection), but not when it originates from LAN.

Any suggestions on how I, from the LAN, can access the forwarded services from my WAN address as though I was coming from externally?

Config:

# dec/02/2020 09:21:32 by RouterOS 6.47.8
#
# model = RB760iGS
/interface bridge
add admin-mac=48:8F:5A:D9:47:80 auto-mac=no name=bridge
/interface list
add name=WAN
add name=LAN
/interface wireless security-profiles
set [ find default=yes ] supplicant-identity=MikroTik
/ip hotspot profile
set [ find default=yes ] html-directory=flash/hotspot
/interface bridge port
add bridge=bridge interface=ether1
add bridge=bridge interface=ether2
add bridge=bridge interface=ether3
add bridge=bridge interface=ether4
/ip neighbor discovery-settings
set discover-interface-list=LAN
/interface list member
add interface=bridge list=LAN
add interface=ether5 list=WAN
/ip address
add address=192.168.10.1/24 interface=bridge network=192.168.10.0
/ip dhcp-client
add disabled=no interface=ether5
/ip firewall filter
add action=accept chain=input comment=\
    "defconf: accept established,related,untracked" connection-state=\
    established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=\
    invalid
add action=accept chain=input comment="defconf: accept ICMP" protocol=icmp
add action=accept chain=input comment=\
    "defconf: accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
add action=drop chain=input comment="defconf: drop all not coming from LAN" \
    in-interface-list=!LAN
add action=accept chain=forward comment="defconf: accept in ipsec policy" \
    ipsec-policy=in,ipsec
add action=accept chain=forward comment="defconf: accept out ipsec policy" \
    ipsec-policy=out,ipsec
add action=fasttrack-connection chain=forward comment="defconf: fasttrack" \
    connection-state=established,related
add action=accept chain=forward comment=\
    "defconf: accept established,related, untracked" connection-state=\
    established,related,untracked
add action=drop chain=forward comment="defconf: drop invalid" connection-state=\
    invalid
add action=drop chain=forward comment="defconf: drop all from WAN not DSTNATed" \
    connection-nat-state=!dstnat connection-state=new in-interface-list=WAN
/ip firewall nat
add action=masquerade chain=srcnat ipsec-policy=out,none out-interface-list=WAN
add action=dst-nat chain=dstnat comment=HTTP dst-port=80 in-interface-list=WAN \
    protocol=tcp to-addresses=192.168.10.2 to-ports=80
add action=dst-nat chain=dstnat comment=HTTPS dst-port=443 in-interface-list=\
    WAN protocol=tcp to-addresses=192.168.10.2 to-ports=443
add action=dst-nat chain=dstnat comment=RDP-desktop dst-port=3390 \
    in-interface-list=WAN protocol=tcp to-addresses=192.168.10.4 to-ports=3389
add action=dst-nat chain=dstnat comment=Minecraft dst-port=25565-25569 \
    in-interface-list=WAN protocol=tcp to-addresses=192.168.10.2 to-ports=\
    25565-25569
add action=dst-nat chain=dstnat comment=Minecraft dst-port=25565-25569 \
    in-interface-list=WAN protocol=udp to-addresses=192.168.10.2 to-ports=\
    25565-25569
add action=dst-nat chain=dstnat dst-port=45454 in-interface-list=WAN protocol=\
    tcp to-addresses=192.168.10.2 to-ports=45454
add action=dst-nat chain=dstnat dst-port=45454 in-interface-list=WAN protocol=\
    udp to-addresses=192.168.10.2 to-ports=45454
/ip service
set telnet disabled=yes
set ftp disabled=yes
set www disabled=yes port=8000
set ssh disabled=yes
set www-ssl port=4343
set api disabled=yes
set api-ssl disabled=yes
/system clock
set time-zone-name=America/Los_Angeles
/tool mac-server
set allowed-interface-list=LAN
/tool mac-server mac-winbox
set allowed-interface-list=LAN

You have to implement hairpin NAT.

OR
You could put the server itself on a different subnet (and only give yourself the admin, direct access to the server via firewall rules.)
Normal NAT rules would then apply.

define a different subnet and put it on ether1 for example
create firewall rule allow admin on original subnet access to new subnet.
adjust dsn-nat rules to new to-addresses IP.

@mkx Thanks, that looks like exactly what I need. One issue, I’ve implemented it and I’m still not hitting my local server on 192.168.10.2. I’ve tried putting the priority first second and last with no luck. The NAT rule I added, see any issues I’m missing?:

 1    chain=srcnat action=masquerade protocol=tcp src-address=192.168.10.0/24 
      dst-address=192.168.10.2 out-interface=bridge dst-port=80,443 log=no 
      log-prefix=""

I’m not seeing any packets hitting the NAT rule.

1 chain=srcnat action=masquerade protocol=tcp src-address=192.168.10.0/24
dst-address=192.168.10.2 out-interface=bridge dst-port=80,443 log=no
log-prefix=“”

should be

chain=srcnat action=masquerade source-address=192.168.10.0/24 dst-address=192.168.10.0/24

still no packets hitting it, this looks like it should work though :frowning:

now the config is:

Flags: X - disabled, I - invalid, D - dynamic 
 0    chain=srcnat action=masquerade src-address=192.168.10.0/24 dst-address=192.168.10.0/24 log=no log-prefix="" 

 1    chain=srcnat action=masquerade out-interface-list=WAN log=no log-prefix="" ipsec-policy=out,none 

 2    ;;; HTTP
      chain=dstnat action=dst-nat to-addresses=192.168.10.2 to-ports=80 protocol=tcp in-interface-list=WAN dst-port=80 log=no log-prefix="" 

 3    ;;; HTTPS
      chain=dstnat action=dst-nat to-addresses=192.168.10.2 to-ports=443 protocol=tcp in-interface-list=WAN dst-port=443 log=no log-prefix="" 

 4    ;;; RDP-desktop
      chain=dstnat action=dst-nat to-addresses=192.168.10.4 to-ports=3389 protocol=tcp in-interface-list=WAN dst-port=3390 log=no log-prefix="" 

 5    ;;; Minecraft
      chain=dstnat action=dst-nat to-addresses=192.168.10.2 to-ports=25565-25569 protocol=tcp in-interface-list=WAN dst-port=25565-25569 log=no log-prefix="" 

 6    ;;; Minecraft
      chain=dstnat action=dst-nat to-addresses=192.168.10.2 to-ports=25565-25569 protocol=udp in-interface-list=WAN dst-port=25565-25569 log=no log-prefix="" 

 7    chain=dstnat action=dst-nat to-addresses=192.168.10.2 to-ports=45454 protocol=tcp in-interface-list=WAN dst-port=45454 log=no log-prefix="" 

 8    chain=dstnat action=dst-nat to-addresses=192.168.10.2 to-ports=45454 protocol=udp in-interface-list=WAN dst-port=45454 log=no log-prefix=""

edit: here is a tcp ping to wan address then to server just to show it is actually listening:

MkX didnt tell you the full story, you need to add the srcnat rule I noted in my previous post, the very clean short srcnat rule, above or below the current srcnat rule doesnt matter and then you need to modify each dst-nat rule.
Thats why I suggested the other method may be easier LOL.

One doesnt have to change all the dstnat rules if you have a fixed/static WANIP and then all the dstnat rules which are made slightly different (as default rules assume dynamic wanip) but do not have to be changed when encountering a hairpin nat scenario..
If you have a dynamic wanip then it gets a bit hairy with regard to dst-nat rules as the default ones dont work in the hairpin nat scenario and have to be modifed!.
BUT
EVEN IF your WANIP is dynamic but doesnt change very often then you could use the format of which I speak (with example to follow) remembering to change them when given a new WANIP…

format (minecraft)
add chain=dstnat action=dst-nat dst-address=WANIP dst-port=25565-25569 protocol=tcp to addresses=192.168.10.2

note: you dont need to-ports if the same as dst-ports

as far as I can tell only the one 192.168.10.0/24 masquerade rule is required, I’m going with the broader one.

What you suggested for adding the wan ip to the source works, but I do have a dynamic wan ip that changes more frequently than I want to go and fix :slight_smile:

Also I thought I was being clever and removed most of the filter conditions for the forwarding rules. This did allow me to reach the web server on port 80, but seemed to break all other web traffic from within the lan, so that’s a no go.

So this leads to a couple follow up questions.

  • do you have any links or resources that show how to do what you suggested with the vlans? I’m not super comfortable winging it, also the server is on another switch so that might add a bit of complexity.


  • Could I use a scheduler job to run a script that checks my wan ip and updates all the forwarding rules with it if it has changed? The scripting language looks relatively simple, but I’m new to mikrotik, so I’m not sure if my assumptions are correct.

good questions but I dont like working blind LOL
please export your latest config

/export hide-sensitive file=anynameyouwish

sure thing, sorry.

I replaced my wan ip with “WANIP”
note: if I remove the “dst-address=WANIP” condition from the 80,443 nat rules thats what ended up with me breaking the rest of my web traffic.

This current config does work for 80 and 443 both from internal and external.

# dec/02/2020 12:33:05 by RouterOS 6.47.8
#
# model = RB760iGS
/interface bridge
add admin-mac=48:8F:5A:D9:47:80 auto-mac=no name=bridge
/interface list
add name=WAN
add name=LAN
/interface wireless security-profiles
set [ find default=yes ] supplicant-identity=MikroTik
/ip hotspot profile
set [ find default=yes ] html-directory=flash/hotspot
/interface bridge port
add bridge=bridge interface=ether1
add bridge=bridge interface=ether2
add bridge=bridge interface=ether3
add bridge=bridge interface=ether4
/ip neighbor discovery-settings
set discover-interface-list=LAN
/interface list member
add interface=bridge list=LAN
add interface=ether5 list=WAN
/ip address
add address=192.168.10.1/24 interface=bridge network=192.168.10.0
/ip dhcp-client
add disabled=no interface=ether5
/ip firewall filter
add action=accept chain=input comment=\
    "defconf: accept established,related,untracked" connection-state=\
    established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=\
    invalid
add action=accept chain=input comment="defconf: accept ICMP" protocol=icmp
add action=accept chain=input comment=\
    "defconf: accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
add action=drop chain=input comment="defconf: drop all not coming from LAN" \
    in-interface-list=!LAN
add action=accept chain=forward comment="defconf: accept in ipsec policy" \
    ipsec-policy=in,ipsec
add action=accept chain=forward comment="defconf: accept out ipsec policy" \
    ipsec-policy=out,ipsec
add action=fasttrack-connection chain=forward comment="defconf: fasttrack" \
    connection-state=established,related
add action=accept chain=forward comment=\
    "defconf: accept established,related, untracked" connection-state=\
    established,related,untracked
add action=drop chain=forward comment="defconf: drop invalid" connection-state=\
    invalid
add action=drop chain=forward comment="defconf: drop all from WAN not DSTNATed" \
    connection-nat-state=!dstnat connection-state=new in-interface-list=WAN
/ip firewall nat
add action=masquerade chain=srcnat ipsec-policy=out,none out-interface-list=WAN
add action=masquerade chain=srcnat dst-address=192.168.10.0/24 src-address=\
    192.168.10.0/24
add action=dst-nat chain=dstnat comment=HTTP dst-address=WANIP dst-port=\
    80 protocol=tcp to-addresses=192.168.10.2
add action=dst-nat chain=dstnat comment=HTTPS dst-address=WANIP dst-port=\
    443 protocol=tcp to-addresses=192.168.10.2
add action=dst-nat chain=dstnat comment=RDP-desktop disabled=yes dst-port=3390 \
    in-interface-list=WAN protocol=tcp to-addresses=192.168.10.4 to-ports=3389
add action=dst-nat chain=dstnat comment=Minecraft disabled=yes dst-port=\
    25565-25569 protocol=tcp to-addresses=192.168.10.2
add action=dst-nat chain=dstnat comment=Minecraft disabled=yes dst-port=\
    25565-25569 protocol=udp to-addresses=192.168.10.2
add action=dst-nat chain=dstnat disabled=yes dst-port=45454 protocol=tcp \
    to-addresses=192.168.10.2
add action=dst-nat chain=dstnat disabled=yes dst-port=45454 protocol=udp \
    to-addresses=192.168.10.2
/ip service
set telnet disabled=yes
set ftp disabled=yes
set www disabled=yes
set ssh disabled=yes
set api disabled=yes
set api-ssl disabled=yes
/system clock
set time-zone-name=America/Los_Angeles
/tool mac-server
set allowed-interface-list=LAN
/tool mac-server mac-winbox
set allowed-interface-list=LAN

You can replace dst-address=WANIP with dst-address-type=local (it will match any address assigned to router). And if you want to use some of forwarded ports also for some service on router itself (e.g. your port 80, which is by default used by WebFig), you can exclude some address using additional dst-address=!192.168.10.1 (“!” means “not”).

Other way would be to use address list with your DDNS hostname and then dst-address-list=<list_with_WANIP>. Downside of this is that it won’t work immediatelly after address changes.

Good, so options are setup dstnat rules for hairpin nat, options below,
OR
set up different subnet or vlans and put your servers on a different subnet from users.

\

  1. Masquerade Rule (substitute for whatever is your lan subnet)
    /ip firewall nat
    add chain=srcnat src-address=192.168.88.0/24 dst-address=192.168.88.0/24 action=masquerade

2(a) Contorted Rule Method - dst-address-type=local for any address on router. Here you enter in the LANIP gateway of the subnet that the server is on, but but the ! symbol in front of it, which means basically anything but this subnet! Since you stated destination local address but have removed the LANIP of the subnet as an option, this only leaves the local WANIP on the router as the alternative).

/ip firewall nat
add chain=dstnat action=dst-nat dst-address=!192.168.88.1
dst-address-type=local protocol=tcp dst-port=xxxx to-address=IPofServer to port= (if need translation)

2(b) MT Cloud DDNS method very popular, and made famous on youtube by stevo (our favourite git, or is that Brit). One creates a firewall address list and puts in the name of your IP Cloud DDNS server. The router will resolve the name to your WANIP. The only downside is a very slight delay in updating your IP when and if it changes. Reliance on outside source (MT service) could be another. On the plus side if your router does not have a public IP this is the better method.
(https://www.youtube.com/watch?v=_kw_bQyX-3U)

/ip firewall address-list
add list=external_wan address=
/firewall nat
add chain=dstnat action=dst-nat dst-port=xxxx protocol=tcp dst-address-list=external_wan address
to-addresses=IPofServer to-ports={only required if translating to a different port number}

In this method, one simply goes to one IP Cloud DDNS server and copies the name provided (after enabling service) into the firewall address list.
add address=name of cloud ddns list=external_wan address


2(c). This is Sobs favourite dish (mine is paella). If you are comfortable with scripts this is the best method, otherwise 2b is preferred.

The dstnat rule is similar to the DDNS cloud method as both access a firewall address list entry. The only difference in the firewall address list part is that you add comment of your choice (has to match script text) to the firewall address list entry and in the example below ‘wan1ip’ is used. The script entry basically says, check if the wan IP is bound and if so stick the address into the firewall address list.

Code: Select all

/firewall nat
add chain=dst-nat action=dstnat dst-port=xxxx protocol=tcp dst-address-list=external_wan-address
to-addresses=IPofServer to-ports={only required if translating to a different port number}

This is what one enters in the DHCP client
/ip dhcp-client
add interface= script=“”

2| :if ($bound=1) do={
3| /ip firewall address-list set [/ip firewall address-list find where comment=“wan1ip”] address=$“lease-address” disabled=no
4| } else={
5| /ip firewall address-list set [/ip firewall address-list find where comment=“wan1ip”] disabled=yes
6| }

and finally the firewall address list entry would look like
/ip firewall address-list
add address=x.x.x.x comment=“wan1ip” list=external_wan-address
(where x.x.x.x is the current valid wanip)


++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
In summary,
2a looks funny and basically says use the interface thats local but is not your subnet (works if there are no other subnets).
2b is kewl as you learn about how to use the free DDNS service on the router.
2c is the most elegant and you tell the router to take the wanip bound in ip dhcp client and stick into the destination address called wan1ip

This destination address rule is used in each destination nat rule just like 2b (much like using dst-address=IPofWan- which you proved workds for 80,.443)),.

Okay, that gives me a lot of options to be looking at… I’m going to try out 2/c and If I can’t figure it out I’ll go with one of the others. I am using a ddns service with freeDNS currenly, I’ll see how I can fit that in with this router as well.

I’ll update the thread with my findings and what I end up using for posterity, but I think I’ll mark this thread answered.

thank you anav and everyone.

Few corrections and clarifications:

No, it says “when destination address is any address on router, except that one”. It has nothing to do with subnets, you can have ten of them and it will work from all, you just have to use that one address to access router, even from other subnets (unless you’d use variant of this with dst-address-list=!, or simply excluded e.g. whole 192.168.0.0/16, if all your LANs fit into that).

It works, but there’s that delay after address change, because it needs to wait until TTL of previous record expires. It’s usually not long, MikroTik’s DDNS has only one minute, I think. But it’s there. One case when you do want this is when you need hairpin NAT and your router is behind another NAT (including dynamic NAT 1:1).

I’d say it’s for purists, who must have dstnat rule acting only for single destination address, which unfortunatelly is dynamic, and wouldn’t stand anything else. Otherwise the simplest, most foolproof and reliable is 2a.

Looked at 2b, dont want a .mynetname address, but I did like the video, it helped explain things a bit to me. Also LOL at https://youtu.be/_kw_bQyX-3U?t=192 it is exactly what I did.

Ended up implementing 2a, so thanks for suggesting that @Sob! Do you know if there is any kind of performance impact just putting a huge /8 mask network on the address list? Right now I just have it pointing to my /24, since that’s all I have, vlans scare me :slight_smile:

Will be looking into 2c at some point, but for now this method is definitely simplest and quickest.

Here is my current config:

# dec/02/2020 21:43:16 by RouterOS 6.47.8
#
# model = RB760iGS
/interface bridge
add admin-mac=48:8F:5A:D9:47:80 auto-mac=no name=bridge
/interface list
add name=WAN
add name=LAN
/interface wireless security-profiles
set [ find default=yes ] supplicant-identity=MikroTik
/ip hotspot profile
set [ find default=yes ] html-directory=flash/hotspot
/interface bridge port
add bridge=bridge interface=ether1
add bridge=bridge interface=ether2
add bridge=bridge interface=ether3
add bridge=bridge interface=ether4
/ip neighbor discovery-settings
set discover-interface-list=LAN
/interface list member
add interface=bridge list=LAN
add interface=ether5 list=WAN
/ip address
add address=192.168.10.1/24 interface=bridge network=192.168.10.0
/ip dhcp-client
add disabled=no interface=ether5
/ip firewall address-list
add address=192.168.10.0/24 list=lanlist
/ip firewall filter
add action=accept chain=input comment="defconf: accept established,related,untracked" connection-state=established,related,untracked
add action=drop chain=input comment="defconf: drop invalid" connection-state=invalid
add action=accept chain=input comment="defconf: accept ICMP" protocol=icmp
add action=accept chain=input comment="defconf: accept to local loopback (for CAPsMAN)" dst-address=127.0.0.1
add action=drop chain=input comment="defconf: drop all not coming from LAN" in-interface-list=!LAN
add action=accept chain=forward comment="defconf: accept in ipsec policy" ipsec-policy=in,ipsec
add action=accept chain=forward comment="defconf: accept out ipsec policy" ipsec-policy=out,ipsec
add action=fasttrack-connection chain=forward comment="defconf: fasttrack" connection-state=established,related
add action=accept chain=forward comment="defconf: accept established,related, untracked" connection-state=established,related,untracked
add action=drop chain=forward comment="defconf: drop invalid" connection-state=invalid
add action=drop chain=forward comment="defconf: drop all from WAN not DSTNATed" connection-nat-state=!dstnat connection-state=new in-interface-list=WAN
/ip firewall nat
add action=masquerade chain=srcnat comment="Hairpin NAT" dst-address-list=lanlist src-address-list=lanlist
add action=masquerade chain=srcnat comment=NAT ipsec-policy=out,none out-interface-list=WAN
add action=dst-nat chain=dstnat comment=HTTP dst-address-list=!lanlist dst-address-type=local dst-port=80 protocol=tcp to-addresses=192.168.10.2
add action=dst-nat chain=dstnat comment=HTTPS dst-address-list=!lanlist dst-address-type=local dst-port=443 protocol=tcp to-addresses=192.168.10.2
add action=dst-nat chain=dstnat comment=Minecraft dst-address-list=!lanlist dst-address-type=local dst-port=25565-25569 protocol=udp to-addresses=\
    192.168.10.2
add action=dst-nat chain=dstnat comment=Minecraft dst-address-list=!lanlist dst-address-type=local dst-port=25565-25569 protocol=tcp to-addresses=\
    192.168.10.2
add action=dst-nat chain=dstnat comment=Torrent dst-address-list=!lanlist dst-address-type=local dst-port=45454 protocol=udp to-addresses=192.168.10.2
add action=dst-nat chain=dstnat comment=Torrent dst-address-list=!lanlist dst-address-type=local dst-port=45454 protocol=tcp to-addresses=192.168.10.2
add action=dst-nat chain=dstnat comment=RDP-desktop dst-address-list=!lanlist dst-address-type=local dst-port=3390 protocol=tcp to-addresses=192.168.10.4 \
    to-ports=3389
/ip service
set telnet disabled=yes
set ftp disabled=yes
set www disabled=yes
set ssh disabled=yes
set api disabled=yes
set api-ssl disabled=yes
/system clock
set time-zone-name=America/Los_Angeles
/tool mac-server
set allowed-interface-list=LAN
/tool mac-server mac-winbox
set allowed-interface-list=LAN

You now have dstnat rules that apply to all destination addresses except 192.168.10.0/24. With the config you posted, just 192.168.10.1 would be enough, because router doesn’t have any other address from 192.168.10.0/24 anyway. Using more than just 192.168.10.1 makes sense if there would be other addresses you want to exclude. For example, if you’d add VPN server or client, and router would get another address that should be ignored by dstnat rules. Otherwise it’s useless.

I don’t think you need to bother with 2c. The added value is debatable. If for any reason you must have dstnat rules for only the one address and nothing more, then go for it. Otherwise the added complexity with script, which additionally is “moving part” that has higher chance to break than static config, is probably not worth it.

Tip: If you want some optimization, then instead of repeating same conditions over and over:

/ip firewall nat
add action=dst-nat chain=dstnat comment=HTTP dst-address-list=!lanlist dst-address-type=local dst-port=80 protocol=tcp to-addresses=192.168.10.2
add action=dst-nat chain=dstnat comment=HTTPS dst-address-list=!lanlist dst-address-type=local dst-port=443 protocol=tcp to-addresses=192.168.10.2
add action=dst-nat chain=dstnat comment=Minecraft dst-address-list=!lanlist dst-address-type=local dst-port=25565-25569 protocol=udp to-addresses=192.168.10.2
...

You can use another chain:

/ip firewall nat
add action=jump chain=dstnat dst-address-list=!lanlist dst-address-type=local jump-target=port-forward
add action=dst-nat chain=port-forward comment=HTTP dst-port=80 protocol=tcp to-addresses=192.168.10.2
add action=dst-nat chain=port-forward comment=HTTPS dst-port=443 protocol=tcp to-addresses=192.168.10.2
add action=dst-nat chain=port-forward comment=Minecraft dst-port=25565-25569 protocol=udp to-addresses=192.168.10.2
...

It will make your rules shorter. And outgoing connections from LAN to internet won’t be unnecessarily checked against all rules (not that it would make any diffence with performance, unless you’d have thousands of them).

Does ‘port-forward’ as a chain name have any special meaning or is it just a arbitrary string we use to tie the dst-nat and jump rules together?

You can use any name you like. But of course the chain name and jump target must match.

Got it, okay.

thank you!

Update: Here is the FreeDNS script I am using, it could be extended to add the new IP to a address list, for now though I just have it updating. It needs test and read policy permissions. I thought I’d post it here in case anyone finds it useful in the future.

:local result [/tool fetch "https://freedns.afraid.org/dynamic/update.php\?<token>" as-value output=user];

:if ($result->"status" = "finished") do={
    # get first line of response
    :local msg [:pick ($result->"data") 0 [:find ($result->"data") "\n"]];

    # will return "ERROR:..." if IP has not changed.
    :if ([:pick $msg 0 [:find $msg ":"]] != "ERROR") do={
        :log info ("ddns: " . $msg);
    }
} else={
    :log info "Failed to connect to DDNS Service.";
}

note: it does not work as expected when very frequently for some reason, but it work when run every 2 minutes.