Prosumer Router

MikroTik RouterOS DDNS with DynIP

MikroTik RouterOS integrates with DynIP through /tool fetch, the built-in HTTP client. A short script discovers the current public IP from DynIP's /ip endpoint and posts it back to /update; the scheduler re-runs the script every 10 minutes. No external packages, no dependencies — it's the same pattern that runs on thousands of production MikroTik units.

What you'll need

  • RouterOS 6.45 or newer, or any RouterOS 7. /tool fetch with HTTPS works on 6.45+; older firmware lacks the TLS support needed to reach api.dynip.dev.
  • A DynIP zone (free tier is enough). Open the zone in the dashboard and grab the TSIG key and the API hostname (api.dynip.dev) from the snippets-generator.
  • Outbound HTTPS from the router to api.dynip.dev. Most default RouterOS firewalls allow this; locked-down deployments may need a forward rule.

Configuration

A short RouterOS script that discovers the current IP via DynIP's /ip endpoint, then sends an authenticated HTTPS GET to /update. The scheduler re-runs it every 10 minutes; DynIP only writes when the IP actually changes, so this is cheap to run frequently.

/system script add name="dynip-update" source="
  :local currentIP ([/tool fetch url=\"https://api.dynip.dev/ip\" output=user as-value]->\"data\")
  :local updateUrl \"https://api.dynip.dev/update?domain=router.ddns.dynip.dev&ip=\$currentIP&key=YOUR_URL_ENCODED_TSIG_KEY\"
  /tool fetch url=\$updateUrl keep-result=no
"

/system scheduler add name="dynip-schedule" interval=10m on-event="dynip-update"

URL-encode the TSIG key when embedding it in the query string — the base64 alphabet includes + and / which are reserved in URLs. The dashboard's snippets-generator does this for you; if you're hand-rolling, replace + with %2B and / with %2F.

RouterOS scripts use $variable for substitution, but variable expansion inside a quoted source= argument has to be escaped as \$currentIP so the parser stores the literal $currentIP in the script body. Inside the body, the runtime then expands it for real.

Verification

Trigger the script manually:

[admin@MikroTik] > /system script run dynip-update
[admin@MikroTik] > /log print where topics~"script"
# Expected: no errors. A failure logs as a fetch error with a reason.

From any host with a resolver:

$ dig +short router.ddns.dynip.dev
203.0.113.42

IP discovery patterns

The default https://api.dynip.dev/ip endpoint returns whatever IP the request arrives on. Which IP that is depends on what the router has and what you want published.

Public dual-stack (most common)

No change from the script above. /tool fetch on a dual-stack router picks IPv4 or IPv6 based on the system's address-selection preferences (typically IPv6 first). Whatever address gets used is what DynIP records.

Pin to IPv4 or IPv6 explicitly

When you want to publish both an A and an AAAA, or want one family specifically because some consumers can't use the other, ask DynIP directly. /ip4 and /ip6 mirror /ip but require the request to arrive on that family — the call returns the matching address or HTTP 400 with a message pointing at the other endpoint. No fetch-layer flags needed; /tool fetch picks the right transport automatically because api.dynip.dev has both A and AAAA records.

# Two separate fetches, one per family. Run as two scheduled scripts.

# IPv4 discovery
:local v4 ([/tool fetch url="https://api.dynip.dev/ip4" output=user as-value]->"data")
/tool fetch url="https://api.dynip.dev/update?domain=router.ddns.dynip.dev&ip=$v4&key=YOUR_URL_ENCODED_KEY" keep-result=no

# IPv6 discovery
:local v6 ([/tool fetch url="https://api.dynip.dev/ip6" output=user as-value]->"data")
/tool fetch url="https://api.dynip.dev/update?domain=router.ddns.dynip.dev&ip=$v6&key=YOUR_URL_ENCODED_KEY" keep-result=no

DynIP detects the address family from the value submitted to /update and writes an A or AAAA accordingly — the same zone can hold both simultaneously, and each gets refreshed only when its respective family's IP changes. /ip still works for the common case of "give me whichever family the router used"; /ip4 and /ip6 are for when you specifically want one or want to discover both.

Private APN / internal-only addressing

On a private cellular APN or any closed network where the device has no internet-visible IP, the default api.dynip.dev/ip path doesn't apply — either the request never reaches the public internet, or it egresses through a NAT and the discovered IP isn't what you want recorded. The pattern is: run a small "what's my IP" endpoint inside the private network, have the router ask that for its internal address, then push that address to the public DynIP API.

The internal "whatami" endpoint is a one-line nginx config:

server {
    listen 80;
    server_name whatami.internal;
    location / {
        default_type text/plain;
        return 200 "$remote_addr\n";
    }
}

Run it on any always-on host reachable from the APN. Then on the router:

/system script add name="dynip-update" source="
  :local currentIP ([/tool fetch url=\"http://whatami.internal/\" output=user as-value]->\"data\")
  :local updateUrl \"https://api.dynip.dev/update?domain=locker.ddns.dynip.dev&ip=\$currentIP&key=YOUR_URL_ENCODED_KEY\"
  /tool fetch url=\$updateUrl keep-result=no
"

The /update call itself still goes out to api.dynip.dev over HTTPS — the router needs outbound HTTPS to the public API available. The published A record is the device's APN-internal IP, so the hostname only resolves usefully from inside the APN. That's usually exactly what you want: ops staff and central services on the same APN get stable names, the public DNS view is just metadata. See the Fleet Operations guide for the full pattern with worked examples.

CGNAT-only, no IPv6

DynIP can still publish what the ISP assigns, but the resulting A record is a CGNAT-range address that isn't reachable from outside the carrier. That's fine for tracking and for DNS-01 certificate issuance, both of which work on DNS alone, but not for inbound connections — for that you need a tunnel (Tailscale, WireGuard, Cloudflare Tunnel). DynIP and a tunnel layer together is a common, well-tested pattern; see the Tailscale guide for the canonical setup.

Common issues

The scheduler entry exists but never fires

Check /system scheduler print detail for disabled=yes or a start-date/start-time in the future. Default values create an enabled entry that fires immediately and then every interval thereafter. A common gotcha on RouterOS 7: scripts created via WinBox sometimes import with disabled schedulers.

/tool fetch returns "failure: closing connection: no error"

Either the router can't reach api.dynip.dev (firewall rule, broken DNS resolver on the router itself, or the WAN is down) or it's hitting a TLS issue on very old RouterOS. Test connectivity with /tool fetch url="https://api.dynip.dev/ip" directly — the error usually points at the root cause. If TLS is the issue, RouterOS 7's TLS stack is far more forgiving than 6.4x and is the right upgrade path.

/update returns 401 or 403

The TSIG key in the URL is wrong, missing, or not URL-encoded. Re-copy from the snippets-generator (which does the encoding for you) and confirm the key= query parameter matches exactly. If you're hand-rolling, remember that + becomes %2B and / becomes %2F.

Notes

Why not RFC 2136 / TSIG on MikroTik?

MikroTik's /tool dns-update doesn't support updating zone apex records — the tool requires a relative label that gets concatenated with the zone name. Since DynIP zones use the apex for the DDNS record, RouterOS's RFC 2136 tool can't address the right record. The HTTP API path documented above is the correct integration for MikroTik and works reliably.

Related guides

  • Fleet Operations — deploying this script across hundreds or thousands of MikroTik units, including the private-APN pattern with a worked postal-locker example.
  • FortiGate DDNS — if you also have FortiGate gear, RFC 2136 / TSIG works natively on FortiOS.
  • Tailscale + DynIP — the CGNAT escape hatch when the public IP isn't reachable.