Skip to content

Instantly share code, notes, and snippets.

@Septrum101
Last active May 29, 2026 16:23
Show Gist options
  • Select an option

  • Save Septrum101/7467bdf972a22c26af5396b43ec2d767 to your computer and use it in GitHub Desktop.

Select an option

Save Septrum101/7467bdf972a22c26af5396b43ec2d767 to your computer and use it in GitHub Desktop.
RouterOS v7 DDNS script
# ================================================================
# Cloudflare DDNS Update Script
# Firmware: RouterOS v7.22+ (also works on v7.23)
# Features: In-memory only | No temp files | Auto fetch zone/record IDs
# Config: Only cfToken and cfDomain required
# ================================================================
# ============ User Config (modify these) =============
:global cfDDNSToken "your_cloudflare_api_token";
:global cfDDNSDomain "ddns.example.com";
:global cfDDNSZoneLabels 2;
:global cfDDNSInterface "pppoe-out1";
:global cfDDNSType "A";
# ---- Cache IDs (Auto-managed) ----
:global cfDDNSZoneID;
:global cfDDNSRecordID;
# dnsType: "A" for IPv4 (/ip address), "AAAA" for IPv6 (/ipv6 address)
# ========================================================
# ---- Optional Parameters ----
:local dnsTTL 1; # TTL in seconds, 1 = auto
:local dnsProxied false; # Enable CF proxy (usually false for DDNS)
:local cfToken $cfDDNSToken;
:local cfDomain $cfDDNSDomain;
:local cfZoneLabels $cfDDNSZoneLabels;
:local cfIntf $cfDDNSInterface;
:local cfType $cfDDNSType;
# ========================================
# 1. Get local IP from interface
# "A" -> /ip address (IPv4)
# "AAAA" -> /ipv6 address (IPv6)
# ========================================
:local currentIP "";
:if ($cfType = "AAAA") do={
:local addr [/ipv6 address get [find interface=$cfIntf global] address];
:set currentIP [:pick $addr 0 [:find $addr "/"]];
} else={
:local addr [/ip address get [find interface=$cfIntf] address];
:set currentIP [:pick $addr 0 [:find $addr "/"]];
}
:if ([:len $currentIP] = 0) do={
:log warning ("CF-DDNS: Failed to get " . $cfType . " address from interface [" . $cfIntf . "]");
:return 0;
}
# ========================================
# 2. Extract zone name from domain
# cfDDNSZoneLabels=2:
# example.com -> example.com
# cfDDNSZoneLabels=3:
# my.work.example.co.uk -> example.co.uk
# ========================================
:local zoneName $cfDomain;
:local dotCount 0;
:local pos 0;
:while ([:typeof [:find $cfDomain "." $pos]] = "num") do={
:set pos ([:find $cfDomain "." $pos] + 1);
:set dotCount ($dotCount + 1);
}
:local labelsCount ($dotCount + 1);
:if (($cfZoneLabels > 0) && ($labelsCount > $cfZoneLabels)) do={
:local needSkip ($labelsCount - $cfZoneLabels);
:local skipped 0;
:local start 0;
:while ($skipped < $needSkip) do={
:local dotPos [:find $cfDomain "." $start];
:set start ($dotPos + 1);
:set skipped ($skipped + 1);
}
:set zoneName [:pick $cfDomain $start [:len $cfDomain]];
}
:log info ("CF-DDNS: Zone name [" . $zoneName . "]");
# ========================================
# 3. Fetch Zone ID online (with Cache)
# ========================================
:local zoneId $cfDDNSZoneID;
:local authHeader ("Authorization: Bearer " . $cfToken);
:if ([:len $zoneId] = 0) do={
:local zoneUrl ("https://api.cloudflare.com/client/v4/zones?name=" . $zoneName);
:local zoneResp [/tool fetch url=$zoneUrl http-method=get \
http-header-field=$authHeader as-value output=user];
:local zoneObj [:deserialize from=json value=($zoneResp->"data")];
:if (($zoneObj->"success") != true) do={
:log warning "CF-DDNS: Failed to get zone (check your token)";
:return 0;
}
:local zones ($zoneObj->"result");
:if ([:len $zones] = 0) do={
:log warning ("CF-DDNS: Zone not found [" . $zoneName . "]");
:return 0;
}
:foreach z in=$zones do={
:set zoneId ($z->"id");
:break;
}
:set cfDDNSZoneID $zoneId;
:log info ("CF-DDNS: Fetched and cached Zone ID: " . $zoneId);
}
# ========================================
# 4. Fetch DNS Record ID online (with Cache)
# ========================================
:local recordId $cfDDNSRecordID;
:local oldIP "";
:if ([:len $recordId] = 0) do={
:local recUrl ("https://api.cloudflare.com/client/v4/zones/" . $zoneId . "/dns_records?name=" . $cfDomain . "&type=" . $cfType);
:local recResp [/tool fetch url=$recUrl http-method=get \
http-header-field=$authHeader as-value output=user];
:local recObj [:deserialize from=json value=($recResp->"data")];
:if (($recObj->"success") != true) do={
:log warning "CF-DDNS: Failed to get DNS record";
:return 0;
}
:local records ($recObj->"result");
:if ([:len $records] = 0) do={
:log warning ("CF-DDNS: " . $cfType . " record [" . $cfDomain . "] not found, create it manually in Cloudflare first");
:return 0;
}
:foreach r in=$records do={
:set recordId ($r->"id");
:set oldIP ($r->"content");
:break;
}
:set cfDDNSRecordID $recordId;
:log info ("CF-DDNS: Fetched and cached Record ID: " . $recordId);
} else={
# If using cached ID, we still need oldIP to check if update is needed
:local recUrl ("https://api.cloudflare.com/client/v4/zones/" . $zoneId . "/dns_records/" . $recordId);
:local recResp [/tool fetch url=$recUrl http-method=get \
http-header-field=$authHeader as-value output=user];
:local recObj [:deserialize from=json value=($recResp->"data")];
:if (($recObj->"success") = true) do={
:set oldIP ($recObj->"result"->"content");
} else {
:log warning "CF-DDNS: Cached Record ID failed, clearing cache and retrying next time";
:set cfDDNSRecordID "";
:return 0;
}
}
# ========================================
# 5. Skip if IP unchanged
# ========================================
:if ($oldIP = $currentIP) do={
:log info ("CF-DDNS: IP unchanged (" . $currentIP . "), skip update");
:return 0;
}
# ========================================
# 6. Update DNS record
# ========================================
:local updUrl ("https://api.cloudflare.com/client/v4/zones/" . $zoneId . "/dns_records/" . $recordId);
:local proxiedStr (:tostr $dnsProxied);
:local putBody ("{\"type\":\"" . $cfType . "\",\"name\":\"" . $cfDomain . "\",\"content\":\"" . $currentIP . "\",\"ttl\":" . $dnsTTL . ",\"proxied\":" . $proxiedStr . "}");
:local updResp [/tool fetch url=$updUrl http-method=put \
http-header-field="Content-Type: application/json, $authHeader" \
http-data=$putBody \
as-value output=user];
:local updObj [:deserialize from=json value=($updResp->"data")];
:if (($updObj->"success") = true) do={
:log info ("CF-DDNS: " . $cfDomain . " " . $oldIP . " -> " . $currentIP);
} else={
:log error "CF-DDNS: Update failed";
}
# ================================================================
# Usage
#
# 1. Import script:
# Paste into RouterOS terminal or upload .rsc via WinBox
#
# 2. Set global variables (once):
# /global cfDDNSToken "your_cloudflare_api_token"
# /global cfDDNSDomain "ddns.example.com"
# /global cfDDNSInterface "pppoe-out1"
# /global cfDDNSType "A"
# /global cfDDNSZoneLabels 2
#
# Note: cfDDNSZoneID and cfDDNSRecordID are managed automatically.
# To force refresh IDs, run: /set cfDDNSZoneID ""; /set cfDDNSRecordID "";
#
# cfDDNSType: "A" for IPv4, "AAAA" for IPv6
#
# 3. Add scheduler (every 5 minutes):
# /system scheduler add name="cf-ddns-update" \
# interval=5m \
# on-event="/system script run cloudflare-ddns" \
# policy=read,write,policy,test
#
# 4. Prerequisites:
# - Cloudflare API Token permissions:
# Zone - DNS - Edit
# Zone - Zone - Read
# - Create the A record in Cloudflare dashboard first
#
# 5. To change token/domain/interface/type, just update the global
# variables, no need to restart the scheduler
# ================================================================
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment