diff --git a/README.md b/README.md index fc9e918..bb3f866 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,8 @@ Do not worry that the command is not shown - that happens because it contains a For basic verification we rename the certificate and print it by fingerprint. Make sure exactly this one certificate ("ISRG-Root-X1") is shown. -```rsc /certificate/set name="ISRG-Root-X1" [ find where common-name="ISRG Root X1" ]; /certificate/print proplist=name,fingerprint where fingerprint="96bcec06264976f37460779acf28c5a7cfe8a3c0aae11a8ffcee05c0bddf08c6"; -``` Always make sure there are no certificates installed you do not know or want! @@ -87,7 +85,7 @@ $ScriptInstallUpdate ddns-hetzner,dns-to-ipv6-subnet-resolver "base-url=https:// ## Available scripts -- [DDNS (DynDNS) Hetzner update script](doc/ddns-hetzner.md) +- [Hello World](doc/hello-world.md) - [DNS to IPv6 subnet resolver](doc/dns-to-ipv6-subnet-resolver.md) ## License and warranty diff --git a/ddns-hetzner.rsc b/ddns-hetzner.rsc deleted file mode 100644 index cdeb4f2..0000000 --- a/ddns-hetzner.rsc +++ /dev/null @@ -1,240 +0,0 @@ -#!rsc by RouterOS -# RouterOS script: ddns-hetzner -# Version 2.0 -# Copyright (c) 2024-2026 Philip 'ShokiNN' Henning -# 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; - :global SafeResolve; - - # 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={ - :do { - $LogPrint debug $ScriptName ("GetAnnouncedIP - started"); - [/system/script/run "JParseFunctions"; global JSONLoad; global JSONLoads; global JSONUnload]; - $LogPrint debug $ScriptName ("GetAnnouncedIP - JParseFunctions loaded"); - - :local Records; - :local AnnouncedIP; - - :set Records ([$JSONLoads ([/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; - } on-error={ - :local Err $message; - - :if ([:find $Err "404"] != -1) do={ - $LogPrint debug $ScriptName ("GetAnnouncedIP - Announced IP is not set"); - :return false; - } - :error ("GetAnnouncedIP - API Error - $Err"); - } - } - - :local APISetRecord do={ - :do { - $LogPrint debug $ScriptName ("APISetRecord - started"); - [/system/script/run "JParseFunctions"; global JSONLoad; global JSONLoads; global JSONUnload]; - $LogPrint debug $ScriptName ("APISetRecord - JParseFunctions loaded"); - - :local Records; - :local Record; - :local APIResponse; - :local Payload; - - :do { - :set Records ([$JSONLoads ([/tool/fetch "$APIUrl/zones/$ZoneName/rrsets/$RecordName/$RecordType" http-method=get http-header-field="Authorization: Bearer $APIToken" output=user as-value]->"data")]->"rrset"->"records"); - } on-error={ - :if ([:find $message "404"] != -1) do={ - :set Records [:toarray ""]; - } else={ - $LogPrint error $ScriptName ("APISetRecord - Could not get record from API - $message"); - } - } - $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); - - $JSONUnload; - $LogPrint debug $ScriptName ("APISetRecord - JSONUnload done"); - $LogPrint debug $ScriptName ("APISetRecord - finished"); - return $APIResponse; - } on-error={ - #TODO Send error via Notification system - $LogPrint error $ScriptName ("Could not set record - $message"); - } - } - - - $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; -} diff --git a/dns-to-ipv6-subnet-resolver.rsc b/dns-to-ipv6-subnet-resolver.rsc index 764b217..af342e8 100644 --- a/dns-to-ipv6-subnet-resolver.rsc +++ b/dns-to-ipv6-subnet-resolver.rsc @@ -6,7 +6,7 @@ # RouterOS compatibility: 7+ # Version 1.1 # last update: 03.01.2026 -# https://git.s1q.dev/phg/routeros-scripts-custom/src/branch/main/doc/dns-to-ipv6-subnet-resolver.md +# https://git.s1q.dev/phg/routeros-scripts-custom/about/doc/dns-to-ipv6-subnet-resolver.md # ------------------------------------------------------------------------------- :local ExitOK false; diff --git a/doc/ddns-hetzner.md b/doc/ddns-hetzner.md deleted file mode 100644 index 4a628dc..0000000 --- a/doc/ddns-hetzner.md +++ /dev/null @@ -1,146 +0,0 @@ -# DDNS (DynDNS) Hetzner update script - -[⬅️ Go back to main README](../README.md) - -> ℹ️ **Info**: This script can not be used on its own but requires the base -> installation. See [main README](../README.md) for details. - -## Table of Contents - -- [DDNS (DynDNS) Hetzner update script](#ddns-dyndns-hetzner-update-script) - - [Table of Contents](#table-of-contents) - - [Description](#description) - - [Requirements and installation](#requirements-and-installation) - - [Dependencies](#dependencies) - - [Installation](#installation) - - [Pre requisites](#pre-requisites) - - [Installation](#installation-1) - - [Configuration](#configuration) - - [`PhgDDNSHetznerAPIToken`](#phgddnshetznerapitoken) - - [`PhgDDNSHetznerDomainEntryConfig`](#phgddnshetznerdomainentryconfig) - - [Usage and invocation](#usage-and-invocation) - - [See also](#see-also) - -## Description - -This Mikrotik RouterOS 7 script for updating DNS entries via Hetzner's Cloud API. - -The script is currently only compatible with RouterOS 7. -RouterOS 6 isn't and won't be supported! - -## Requirements and installation - -### Dependencies - -This script requires [Winand](https://github.com/Winand)'s [mikrotik-json-parser](https://github.com/Winand/mikrotik-json-parser) to be installed. - -#### Installation - -Create another new script: - - 1. Name: `JParseFunctions` - 2. Policy: `read`, `write`, `test` uncheck everything else - 3. Source: The content of [mikrotik-json-parser](https://github.com/Winand/mikrotik-json-parser/blob/master/JParseFunctions) - -### Pre requisites - -> [!IMPORTANT] -> **It's strongly recommended to create a separate Project just for your DNS Zone!** -> Because the API Token you will create will have Read/Write access to the whole Project it can't be restricted to specific services like DNS. - -Create a [API token for Hetzner's Cloud API](https://docs.hetzner.cloud/reference/cloud#getting-started). - -The API token can be created at: -`Your cloud project` -> `Security` -> `API-Tokens` - -### Installation - -Just install the script: - -```rsc -$ScriptInstallUpdate ddns-hetzner "base-url=https://git.s1q.dev/phg/routeros-scripts-custom/raw/branch/main/"; -/system/script/set [find name="ddns-hetzner"] policy=read,write,test -``` - -## Configuration - -Edit `global-config-overlay` and add the following variables. - -| Variable name | Requried | Data type | Example | Description | -| :-------------------------------- | :------- | :-------------------- | :------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PhgDDNSHetznerAPIToken` | true | `string` | `LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj` | This variable requires a valid API token for the [Hetzner DNS API](https://docs.hetzner.cloud/reference/cloud#getting-started). You can create an API token in you project settings. | -| `PhgDDNSHetznerDomainEntryConfig` | true | `array`s of `string`s | `{{"pppoe-out1";"";"example.com";"A";"@";"300";};{"pppoe-out1";"pool-ipv6";"example.com";"AAAA";"@";"300";};}` | See below how to format the arrays correctly. | - -### `PhgDDNSHetznerAPIToken` - -Example: - -```rsc -:global PhgDDNSHetznerAPIToken "LRK9DAWQ1ZAEFSrCNEEzLCUwhYX1U3g7wMg4dTlkkDC96fyDuyJ39nVbVjCKSDfj"; -``` - -### `PhgDDNSHetznerDomainEntryConfig` - -The `domainEntryConfig` array consists of multiple arrays. Each of the is configuring a DNS record for a given domain in a zone. - -The data sheet below describes the formatting of the DNS records arrays. - - -| Array index | Data | Data type | Example | Description | -| ----------: | :------------ | :-------- | :------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `0` | `interface` | `string` | `"pppoe-out1"` | Name of the interface where the IP which is currently configured is fetched from. | -| `1` | `pool` | `string` | `"pool-ipv6"` | The prefix delegation pool which is used to automatically setup the IPv6 interface IP. Use "" when you don't use a pool to set your interface ip or for a type A record. | -| `2` | `zone` | `string` | `"domain.com"` | Zone which should be used to set a record to. | -| `3` | `record type` | `string` | `"A"` | Valid values `A`, `AAAA`. The type of record which will be set. Also determines which IP (v4/v6) will be fetched. | -| `4` | `record name` | `string` | `"@"` | The record name which should be updated. Use `@` for the root of your domain. | -| `5` | `record TTL` | `string` | `"300"` | TTL value of the record in seconds, for a dynamic entry a short lifetime like 300 is recommended. | - -Example: - -```rsc -:global PhgDDNSHetznerDomainEntryConfig { - { - "pppoe-out1"; - ""; - "example.com"; - "A"; - "@"; - "300"; - }; - {"pppoe-out1";"pool-ipv6";"example.com";"AAAA";"@";"300";}; - {"pppoe-out1";"";"example.net";"A";"ddns";"300";}; - {"pppoe-out1";"pool-ipv6";"example.org";"AAAA";"ddns";"300";}; -}; -``` - -This example will create & update those DNS records: - -- example.com - - IPv4 - - IPv6 -- example.net - - IPv4 -- example.org - - IPv6 - -## Usage and invocation - -How to run the script manually: - -```rsc -/system/script/run ddns-hetzner; -``` - -Setup a Scheduler to run the script regularly: - -```rsc -/system/scheduler/add name="ddns-hetzner" interval="00:05:00" policy="read,write,test" on-event="/system/script/run ddns-hetzner;"; -``` - -## See also - -* ... - ---- -[⬅️ Go back to main README](../README.md) -[⬆️ Go back to top](#top) diff --git a/doc/dns-to-ipv6-subnet-resolver.md b/doc/dns-to-ipv6-subnet-resolver.md index a5d8923..77b725e 100644 --- a/doc/dns-to-ipv6-subnet-resolver.md +++ b/doc/dns-to-ipv6-subnet-resolver.md @@ -32,7 +32,7 @@ $ScriptInstallUpdate dns-to-ipv6-subnet-resolver "base-url=https://git.s1q.dev/p ## Configuration -Edit `global-config-overlay` and add the following variables. +Edit `global-config-overlay` and Add the following variables. | Variable name | Required | Data type | Example | Description | | :-------------------------------- | :------- | :-------- | :---------------------------------- | :--------------------------------------------------------------------------- | diff --git a/doc/hello-world.md b/doc/hello-world.md index c289867..fee4685 100644 --- a/doc/hello-world.md +++ b/doc/hello-world.md @@ -7,13 +7,13 @@ Hello World > installation. See [main README](../README.md) for details. Description ------------ +----------- This is a demo script. Invoked from terminal it writes to system log and terminal, or sends a notification otherwise. Requirements and installation ------------------------------ +----------------------------- Just install the script: @@ -26,17 +26,17 @@ There's no specific configuration in `global-config-overlay`, other than general configuration for notifications. Usage and invocation --------------------- +-------------------- Just run the script: /system/script/run hello-world; -See also +See also -------- * ... ---- -[⬅️ Go back to main README](../README.md) +--- +[⬅️ Go back to main README](../README.md) [⬆️ Go back to top](#top) diff --git a/hello-world.rsc b/hello-world.rsc index dc983df..ce61050 100644 --- a/hello-world.rsc +++ b/hello-world.rsc @@ -1,16 +1,16 @@ #!rsc by RouterOS # RouterOS script: hello-world -# Copyright (c) 2023-2026 Christian Hesse +# Copyright (c) 2023-2025 Christian Hesse # https://git.eworm.de/cgit/routeros-scripts-custom/about/COPYING.md # # hello-world demo script # https://git.eworm.de/cgit/routeros-scripts-custom/about/doc/hello-world.md +:global GlobalFunctionsReady; +:while ($GlobalFunctionsReady != true) do={ :delay 500ms; } + :local ExitOK false; -onerror Err { - :global GlobalConfigReady; :global GlobalFunctionsReady; - :retry { :if ($GlobalConfigReady != true || $GlobalFunctionsReady != true) \ - do={ :error ("Global config and/or functions not ready."); }; } delay=500ms max=50; +:do { :local ScriptName [ :jobname ]; :global LogPrint; @@ -22,6 +22,6 @@ onerror Err { } else={ $SendNotification2 ({ origin=$ScriptName; subject="Hello..."; message="... world!" }); } -} do={ - :global ExitError; $ExitError $ExitOK [ :jobname ] $Err; +} on-error={ + :global ExitError; $ExitError $ExitOK [ :jobname ]; }