Self-Hosting Minecraft Behind Double NAT - MikroTik and Cloudflare Guide

#MikroTik #minecraft |

If you’ve ever tried to host a service behind an ISP router (Double NAT), you know the pain: external players can’t connect, or worse, you can’t even connect using your own domain while sitting at home. I personally ran into this issue when trying to expose my minecraft server to the internet.

In this post, I’ll show you how I automated my Cloudflare and MikroTik DDNS updates and fixed Hairpin NAT on a MikroTik router so that the server is reachable from anywhere using a single domain.


The Challenge

  1. Double NAT: My MikroTik is behind an ISP router, meaning its WAN IP is a local address (like 192.168.x.x).
  2. Dynamic IP: My public IP changes, which breaks the Cloudflare DNS record.
  3. The Loopback Problem: Without “Hairpin NAT,” devices inside the network can’t use the public domain to connect to the server.
+----------+       +-----------------+       +--------------------------------+
| INTERNET | ----> | ISP MODEM (NAT) | ----> | MIKROTIK ROUTER (The Hub)      |
| PublicIP |       | Fwd -> MikroTik |       | LAN GW: 10.0.0.1               |
+----------+       +-----------------+       | [WAN_IP List holds PublicIP]   |
                                             | [DST-NAT & Hairpin Rules on]   |
                                             +---------------+----------------+
                                                             |
(1)Traffic to "xx.yourdomain.com" or (2)Hairpinned Traffic.  |
                                 (Dst: PublicIP)             |     (Src:10.0.0.1)
          +--------------------------------------------------<---------+
          |                                                            |
+---------+-------------------+                   +----------------------------+
| INTERNAL PLAYER PC          |                   | MINECRAFT SERVER           |
| IP: 10.0.0.50               |                   | IP: 172.16.222.142         |
| Looks up domain -> PublicIP |                   | Sees conn from 10.0.0.1,   | +-----------------------------+                   | replies OK                 |
                                                  +----------------------------+

FLOW LEGEND:
(1) Player on LAN tries to reach the server via the Public Domain/IP.
(2) MikroTik recognizes Public IP dst, changes dst to Server IP (DST-NAT), AND
    changes src to its own LAN IP (Hairpin SRC-NAT) so traffic loops back correctly.

The Architecture: Choosing Your DNS Strategy

Before diving into the MikroTik configuration, decide how you want your domain (myminecraft.armand.nz) to find your house:

Option 1: The Cloudflare API Method (A-Record)

Your MikroTik runs a script that “pushes” your public IP directly to Cloudflare.

Option 2: The “Set and Forget” Method (CNAME)

Use MikroTik’s built-in DDNS service (/ip cloud) as an intermediary.


The “Must-Have” Common Denominator: The WAN_IP List

Regardless of which option you choose, you still need a script on your MikroTik. Because you are behind Double NAT, your router doesn’t “know” its own public IP. Without a script to identify that IP and populate a Firewall Address List, your Hairpin NAT will not work. You’ll be able to host for friends, but you won’t be able to connect to your own server using your domain name while at home.


1. The Script: update_ddns_ip

Option 1: Cloudflare API + Local Firewall Sync

This script handles the Cloudflare API update and the local Firewall sync simultaneously.

Prerequisites: * Cloudflare API Token (DNS:Edit permissions).

# ** CONFIGURE SECTION **
:local token    "YOUR_CLOUDFLARE_TOKEN"
:local zoneId   "YOUR_ZONE_ID"
:local hostId   "YOUR_HOST_ID"
:local hostName "your.hostname.com"
:local cloudDNS "your-id.sn.mynetname.net"
# ** END OF CONFIGURE SECTION **

:global ip4wan

# 1. Resolve current Public IP
:local ip4new
:do {
    :set ip4new [:resolve $cloudDNS]
} on-error={
    :log error "[Cloudflare] DNS Resolution failed for $cloudDNS"
}

# 2. Proceed if IP is found
:if ([:len $ip4new] > 0) do={
    :if ($ip4new != $ip4wan) do={
        :log info "[Cloudflare] IP change detected: $ip4new"

        # Update Address List (Cleanly remove and re-add)
        /ip firewall address-list remove [find list="WAN_IP"]
        /ip firewall address-list add list="WAN_IP" address=$ip4new comment="Updated by Script"

        # Prepare Cloudflare API Call
        :local url "[https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records/$hostId](https://api.cloudflare.com/client/v4/zones/$zoneId/dns_records/$hostId)"
        :local header "Authorization: Bearer $token,Content-Type: application/json"
        :local data "{\"type\":\"A\",\"name\":\"$hostName\",\"content\":\"$ip4new\",\"ttl\":120}"

        :do {
            :local response [/tool fetch url=$url http-method=put http-header-field=$header http-data=$data mode=https output=user as-value]
            :local fStatus ($response->"status")
            
            :if ($fStatus = "finished" || [:pick $fStatus 0 1] = "2") do={
                :log info "[Cloudflare] DNS success! $hostName now points to $ip4new"
                :set ip4wan $ip4new
            } else={
                :log error "[Cloudflare] Update failed. Status: $fStatus"
            }
        } on-error={
            :log error "[Cloudflare] API Call failed."
        }
    }
}

Option 2: The “Light” Script (Firewall Sync Only)

If you used a CNAME, use this version to keep your Hairpin NAT working.

# ** CONFIGURE SECTION **
:local cloudDNS "xxxxxxx.sn.mynetname.net"
# ** END OF CONFIGURE SECTION **

:global ip4wan

:local ip4new
:do {
    :set ip4new [:resolve $cloudDNS]
} on-error={
    :log error "[NAT Sync] DNS Resolution failed"
}

:if ([:len $ip4new] > 0 && $ip4new != $ip4wan) do={
    /ip firewall address-list remove [find list="WAN_IP"]
    /ip firewall address-list add list="WAN_IP" address=$ip4new comment="Updated by Sync Script"
    :set ip4wan $ip4new
    :log info "[NAT Sync] Firewall Address List 'WAN_IP' synced to $ip4new"
}

2. Automating the Sync

Set a scheduler to run your chosen script every 15 minutes.

/system scheduler
add name="Schedule-DDNS-update" interval=15m on-event="update_ddns_ip" \
    start-time=startup comment="Cloudflare and Hairpin NAT sync"

3. Fixing the Internal Connection (Hairpin NAT)

The secret is pointing NAT rules at an Address List instead of an Interface.

# 1. Minecraft Java Port Forwarding
/ip firewall nat
add action=dst-nat chain=dstnat comment="minecraft Java (TCP)" \
    dst-address-list=WAN_IP dst-port=25565 protocol=tcp \
    to-addresses=172.16.222.142 to-ports=25565

# 2. Minecraft Bedrock Port Forwarding
add action=dst-nat chain=dstnat comment="minecraft Bedrock (UDP)" \
    dst-address-list=WAN_IP dst-port=19132 protocol=udp \
    to-addresses=172.16.222.142 to-ports=19132

# 3. The Hairpin Masquerade
# Adjust src-address to match your local LAN subnet
add action=masquerade chain=srcnat comment="Hairpin NAT" \
    dst-address=172.16.222.142 src-address=10.0.0.0/8

How it works:

  1. External Player: Hits your public IP $\rightarrow$ MikroTik matches the WAN_IP list $\rightarrow$ Forwards to the server.
  2. Internal Player: Uses the domain $\rightarrow$ Traffic hits MikroTik $\rightarrow$ Rule triggers (destination matches WAN_IP) $\rightarrow$ Hairpin NAT “masks” the source IP so the server replies correctly through the gateway.

Summary of Success

comments powered byDisqus

Copyright © Armand