236 lines
11 KiB
Text
236 lines
11 KiB
Text
#!rsc by RouterOS
|
|
# RouterOS script: ddns-hetzner
|
|
# Version 2.0.2
|
|
# Copyright (c) 2024-2026 Philip 'ShokiNN' Henning <mail@philip-henning.com>
|
|
# https://git.s1q.dev/phg/routeros-scripts-custom/about/COPYING.md
|
|
#
|
|
# requires RouterOS, version=7.18
|
|
#
|
|
# Updates periodically DNS entries on Hetzner's DNS service with the Router's public IPs
|
|
# https://git.s1q.dev/phg/routeros-scripts-custom/src/branch/main/doc/ddns-hetzner.md
|
|
|
|
:local ExitOK false;
|
|
onerror Err {
|
|
:global GlobalConfigReady; :global GlobalFunctionsReady; :global GlobalFunctionsCustomPhgReady;
|
|
:retry { :if ($GlobalConfigReady != true || $GlobalFunctionsReady != true || $GlobalFunctionsCustomPhgReady != true) \
|
|
do={ :error ("Global config and/or functions not ready."); };
|
|
} delay=500ms max=50;
|
|
|
|
:local ScriptName [ :jobname ];
|
|
|
|
:global LogPrint;
|
|
:global ParseKeyValueStore;
|
|
:global ScriptLock;
|
|
|
|
# Local/global script specific variables
|
|
:global PhgDDNSHetznerAPIToken;
|
|
:global PhgDDNSHetznerDomainEntryConfig;
|
|
:local APIUrl "https://api.hetzner.cloud/v1";
|
|
|
|
:if ([ $ScriptLock $ScriptName ] = false) do={
|
|
:set ExitOK true;
|
|
:error false;
|
|
}
|
|
|
|
:local GetLocalIPv4 do={
|
|
:local IP [/ip/address/get [:pick [find interface="$WANInterface"] 0] address];
|
|
:return [:pick $IP 0 [:find $IP /]];
|
|
}
|
|
|
|
:local GetLocalIPv6 do={
|
|
:local IP [/ipv6/address/get [:pick [find interface="$WANInterface" from-pool="$PublicIPv6Pool" !link-local] 0] address];
|
|
:return [:pick $IP 0 [:find $IP /]];
|
|
}
|
|
|
|
:local GetAnnouncedIP do={
|
|
:local Records;
|
|
:local AnnouncedIP;
|
|
|
|
:onerror GetAnnouncedIPErr in={
|
|
$LogPrint debug $ScriptName ("GetAnnouncedIP - started");
|
|
|
|
:set Records ([:deserialize from=json value=([/tool/fetch "$APIUrl/zones/$ZoneName/rrsets/$RecordName/$RecordType" http-method=get http-header-field="Authorization: Bearer $APIToken" output=user as-value]->"data")]->"rrset"->"records");
|
|
$LogPrint debug $ScriptName ("GetAnnouncedIP - Records received: " . [:len $Records]);
|
|
foreach rec in=$Records do={
|
|
$LogPrint debug $ScriptName ("GetAnnouncedIP - Record: Name: \"" . $RecordName . "\", Type: \"" . $RecordType . "\", Value: \"" . ($rec->"value") . "\", Comment: \"" . ($rec->"comment") . "\"");
|
|
}
|
|
|
|
:if ([:len $Records] > 1) do={
|
|
:error ("Multiple records found for \"$RecordName.$ZoneName\", RecordType: $RecordType. This is not supported.");
|
|
} else={
|
|
:if ([:len $Records] = 1) do={
|
|
:set AnnouncedIP ($Records->0->"value");
|
|
}
|
|
}
|
|
$LogPrint debug $ScriptName ("GetAnnouncedIP - Announced IP is: " . $AnnouncedIP);
|
|
|
|
:return $AnnouncedIP;
|
|
} do={
|
|
$LogPrint debug $ScriptName ("GetAnnouncedIP - Error Message: " . $GetAnnouncedIPErr);
|
|
|
|
:if ([:find "$GetAnnouncedIPErr" "status 404";] >= 1) do={
|
|
$LogPrint debug $ScriptName ("GetAnnouncedIP - Announced IP is not set");
|
|
:return false;
|
|
}
|
|
:error ("GetAnnouncedIP - API Error - $GetAnnouncedIPErr");
|
|
}
|
|
:return $AnnouncedIP;
|
|
}
|
|
|
|
:local APISetRecord do={
|
|
:local APIResponse;
|
|
|
|
:onerror APISetRecordErr in={
|
|
$LogPrint debug $ScriptName ("APISetRecord - started");
|
|
|
|
:local Records;
|
|
:local Record;
|
|
:local Payload;
|
|
|
|
:onerror GetRecordsErr in={
|
|
:set Records ([:deserialize from=json value=([/tool/fetch "$APIUrl/zones/$ZoneName/rrsets/$RecordName/$RecordType" http-method=get http-header-field="Authorization: Bearer $APIToken" output=user as-value]->"data")]->"rrset"->"records");
|
|
} do={
|
|
:if ([:find "$GetRecordsErr" "status 404";] >= 1) do={
|
|
:set Records [:toarray ""];
|
|
} else={
|
|
$LogPrint error $ScriptName ("APISetRecord - Could not get record from API - $GetRecordsErr");
|
|
}
|
|
}
|
|
$LogPrint debug $ScriptName ("APISetRecord - Records received: " . [:len $Records]);
|
|
foreach rec in=$Records do={
|
|
$LogPrint debug $ScriptName ("APISetRecord - Record: Name: \"" . $RecordName . "\", Type: \"" . $RecordType . "\", Value: \"" . ($rec->"value") . "\", Comment: \"" . ($rec->"comment") . "\"");
|
|
}
|
|
|
|
:if ([:len $Records] > 1) do={
|
|
:error ("Multiple records found for \"$RecordName.$ZoneName\", RecordType: $RecordType. This is not supported.");
|
|
} else={
|
|
:if ([:len $Records] = 1) do={
|
|
:set Record ($Records->0);
|
|
}
|
|
}
|
|
|
|
:local RecordDebugLogOutput;
|
|
foreach key,value in=$Record do={
|
|
:if ([:typeof $RecordDebugLogOutput ] != "str" || $RecordDebugLogOutput = "") do={
|
|
:set RecordDebugLogOutput ($key . ": \"" . $value . "\"");
|
|
} else={
|
|
:set RecordDebugLogOutput ($RecordDebugLogOutput . ", " . $key . ": \"" . $value . "\"");
|
|
}
|
|
}
|
|
$LogPrint debug $ScriptName ("APISetRecord - Picked Record: " . $RecordDebugLogOutput);
|
|
|
|
:if ([:typeof $Record] != "nothing") do={
|
|
:set Payload "{\"records\":[{\"value\":\"$InterfaceIP\",\"comment\":\"Updated by RouterOS DDNS Script\"}]}";
|
|
$LogPrint debug $ScriptName ("APISetRecord - Payload: " . $Payload);
|
|
$LogPrint debug $ScriptName ("APISetRecord - Updating existing record - URL: $APIUrl/zones/$ZoneName/rrsets/$RecordName/$RecordType/actions/set_records");
|
|
:set APIResponse ([/tool/fetch "$APIUrl/zones/$ZoneName/rrsets/$RecordName/$RecordType/actions/set_records" http-method=post http-header-field="Content-Type: application/json,Authorization: Bearer $APIToken" http-data=$Payload output=user as-value]->"status");
|
|
} else={
|
|
:set Payload "{\"name\":\"$RecordName\",\"type\":\"$RecordType\",\"ttl\":$([:tonum $RecordTTL]),\"records\":[{\"value\":\"$InterfaceIP\",\"comment\":\"Updated by RouterOS DDNS Script\"}]}";
|
|
$LogPrint debug $ScriptName ("APISetRecord - Payload: " . $Payload);
|
|
$LogPrint debug $ScriptName ("APISetRecord - Creating new record - URL: $APIUrl/zones/$ZoneName/rrsets");
|
|
:set APIResponse ([/tool/fetch "$APIUrl/zones/$ZoneName/rrsets" http-method=post http-header-field="Content-Type: application/json,Authorization: Bearer $APIToken" http-data=$Payload output=user as-value]->"status");
|
|
}
|
|
$LogPrint debug $ScriptName ("APISetRecord - APIResponse: " . $APIResponse);
|
|
|
|
$LogPrint debug $ScriptName ("APISetRecord - finished");
|
|
:return $APIResponse;
|
|
} do={
|
|
#TODO Send error via Notification system
|
|
$LogPrint error $ScriptName ("Could not set record - Zone: " . $ZoneName . ", RecordName: " . $RecordName . ", RecordType: " . $RecordType . " - API Error: " . $APISetRecordErr);
|
|
}
|
|
:return $APIResponse;
|
|
}
|
|
|
|
|
|
$LogPrint debug $ScriptName ("Begin DDNS update process");
|
|
|
|
:local index 0;
|
|
:foreach i in=$PhgDDNSHetznerDomainEntryConfig do={
|
|
:local WANInterface ("$($i->0)");
|
|
:local PublicIPv6Pool ("$($i->1)");
|
|
:local ZoneName ("$($i->2)");
|
|
:local RecordType ("$($i->3)");
|
|
:local RecordName ("$($i->4)");
|
|
:local RecordTTL ("$($i->5)");
|
|
:local FQDN;
|
|
:local InterfaceIP;
|
|
:local DNSIP;
|
|
:local StartLogMsg "Start configuring domain: ";
|
|
:local EndLogMsg "Finished configuring domain: ";
|
|
|
|
:if ($RecordName = "@") do={
|
|
:set FQDN ("$($i->2)");
|
|
} else={
|
|
:set FQDN ("$($i->4).$($i->2)");
|
|
}
|
|
|
|
:if ($RecordType = "A") do={
|
|
$LogPrint debug $ScriptName ($StartLogMsg . $FQDN . " - Type A Record");
|
|
|
|
:set InterfaceIP [$GetLocalIPv4 WANInterface=$WANInterface];
|
|
:set DNSIP [$GetAnnouncedIP APIUrl=$APIUrl APIToken=$PhgDDNSHetznerAPIToken ZoneName=$ZoneName RecordType=$RecordType RecordName=$RecordName LogPrint=$LogPrint ScriptName=$ScriptName];
|
|
$LogPrint debug $ScriptName ("Domain: " . $FQDN . " - Announced DNS IP: " . $DNSIP);
|
|
|
|
:if ($InterfaceIP != $DNSIP) do={
|
|
:if ($DNSIP = false) do={
|
|
$LogPrint debug $ScriptName ("Domain: " . $FQDN . " - local IP: " . $InterfaceIP . ", differs from DNS IP: none");
|
|
} else={
|
|
$LogPrint debug $ScriptName ("Domain: " . $FQDN . " - local IP: " . $InterfaceIP . ", differs from DNS IP: " . $DNSIP);
|
|
}
|
|
$LogPrint debug $ScriptName ("Domain: " . $FQDN . " - Updating A Record to " . $InterfaceIP);
|
|
|
|
:local ResponseSetRecord [$APISetRecord APIUrl=$APIUrl APIToken=$PhgDDNSHetznerAPIToken ZoneName=$ZoneName RecordType=$RecordType RecordName=$RecordName RecordTTL=$RecordTTL InterfaceIP=$InterfaceIP LogPrint=$LogPrint ScriptName=$ScriptName];
|
|
$LogPrint debug $ScriptName ("ResponseSetRecord: " . $ResponseSetRecord);
|
|
|
|
:if ($ResponseSetRecord = "finished") do={
|
|
$LogPrint info $ScriptName ("Domain: " . $FQDN . " - Updating A Record to " . $InterfaceIP . " successful");
|
|
}
|
|
} else={
|
|
$LogPrint debug $ScriptName ("Domain: " . $FQDN . " - local IP: " . $InterfaceIP . ", is equal to DNS IP: " . $DNSIP . " - Nothing to do");
|
|
}
|
|
|
|
$LogPrint debug $ScriptName ($EndLogMsg . $FQDN . " - Type A Record");
|
|
}
|
|
|
|
:if ($RecordType = "AAAA") do={
|
|
$LogPrint debug $ScriptName ($StartLogMsg . $FQDN . " - Type AAAA Record");
|
|
|
|
:set InterfaceIP [$GetLocalIPv6 WANInterface=$WANInterface PublicIPv6Pool=$PublicIPv6Pool];
|
|
:set DNSIP [$GetAnnouncedIP APIUrl=$APIUrl APIToken=$PhgDDNSHetznerAPIToken ZoneName=$ZoneName RecordType=$RecordType RecordName=$RecordName LogPrint=$LogPrint ScriptName=$ScriptName];
|
|
$LogPrint debug $ScriptName ("Domain: " . $FQDN . " - Announced DNS IP: " . $DNSIP);
|
|
|
|
:if ($InterfaceIP != $DNSIP) do={
|
|
:if ($DNSIP = false) do={
|
|
$LogPrint debug $ScriptName ("Domain: " . $FQDN . " - local IP: " . $InterfaceIP . ", differs from DNS IP: none");
|
|
} else={
|
|
$LogPrint debug $ScriptName ("Domain: " . $FQDN . " - local IP: " . $InterfaceIP . ", differs from DNS IP: " . $DNSIP);
|
|
}
|
|
$LogPrint debug $ScriptName ("Domain: " . $FQDN . " - Updating AAAA Record to " . $InterfaceIP);
|
|
|
|
:local ResponseSetRecord [$APISetRecord APIUrl=$APIUrl APIToken=$PhgDDNSHetznerAPIToken ZoneName=$ZoneName RecordType=$RecordType RecordName=$RecordName RecordTTL=$RecordTTL InterfaceIP=$InterfaceIP LogPrint=$LogPrint ScriptName=$ScriptName];
|
|
$LogPrint debug $ScriptName ("ResponseSetRecord: " . $ResponseSetRecord);
|
|
|
|
:if ($ResponseSetRecord = "finished") do={
|
|
$LogPrint info $ScriptName ("Domain: " . $FQDN . " - Updating AAAA Record to " . $InterfaceIP . " successful");
|
|
}
|
|
} else={
|
|
$LogPrint debug $ScriptName ("Domain: " . $FQDN . " - local IP: " . $InterfaceIP . ", is equal to DNS IP: " . $DNSIP . " - Nothing to do");
|
|
}
|
|
|
|
$LogPrint debug $ScriptName ($EndLogMsg . $FQDN . " - Type AAAA Record");
|
|
}
|
|
|
|
|
|
:if (($RecordType != "A") && ($RecordType != "AAAA")) do={
|
|
$LogPrint error $ScriptName ("Wrong Record type for array index number " . $index . " (Value: " . $RecordType . ")");
|
|
}
|
|
|
|
:set index ($index+1);
|
|
}
|
|
:set index;
|
|
|
|
$LogPrint debug $ScriptName ("Finished DDNS update process");
|
|
|
|
} do={
|
|
:global ExitError; $ExitError $ExitOK [ :jobname ] $Err;
|
|
}
|