mirror of
https://github.com/shokinn/hetzner-ddns-for-mikrotik.git
synced 2025-01-18 12:52:26 +00:00
inital commit
This commit is contained in:
commit
b03d1ac14a
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Philip Henning
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
90
README.md
Normal file
90
README.md
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
# hetzner-ddns-for-mikrotik
|
||||||
|
|
||||||
|
This Mikrotik RouterOS script for updating DNS entries via Hetzner's DNS API.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [hetzner-ddns-for-mikrotik](#hetzner-ddns-for-mikrotik)
|
||||||
|
- [Table of Contents](#table-of-contents)
|
||||||
|
- [How does is works?](#how-does-is-works)
|
||||||
|
- [Setup](#setup)
|
||||||
|
- [Script configuration data](#script-configuration-data)
|
||||||
|
- [`domainEntryConfig` array data sheet](#domainentryconfig-array-data-sheet)
|
||||||
|
|
||||||
|
## How does is works?
|
||||||
|
|
||||||
|
The scripts checks the defined interfaces' IP's for the configured [FQDN's](https://en.wikipedia.org/wiki/Fully_qualified_domain_name).
|
||||||
|
This is achieved via plain DNS.
|
||||||
|
|
||||||
|
If the IP from the interface differs from the IP configures in the DNS record, the DNS record will be updated accordingly to the interfaces' IP.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Dependencies
|
||||||
|
1. This script requires [Winand](https://github.com/Winand)'s [mikrotik-json-parser](https://github.com/Winand/mikrotik-json-parser).
|
||||||
|
2. Create a new Script `System -> Scripts`
|
||||||
|
1. Name: `ddns-hetzner`
|
||||||
|
2. Policy: `read`, `write`, `test`, uncheck everything else
|
||||||
|
3. Source: Copy the script here
|
||||||
|
3. Create a [API token for Hetzner's DNS API](https://dns.hetzner.com/settings/api-token)
|
||||||
|
4. Configure the script to your needs, check the description in the script or below for information how to configure it
|
||||||
|
5. 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)
|
||||||
|
6. Create a new Schedule `System -> Schedule`
|
||||||
|
1. Name: `ddns-hetzner`
|
||||||
|
2. Start Date: leave it as it is
|
||||||
|
3. Start Time: leave it as it is
|
||||||
|
4. Interval: `00:05:00`
|
||||||
|
5. Policy: `read`, `write`, `test` uncheck everything else
|
||||||
|
6. On Event: `ddns-hetzner`
|
||||||
|
|
||||||
|
### Script configuration data
|
||||||
|
|
||||||
|
| Variable name | Data type | Example | Description |
|
||||||
|
| ------------------: | :-------------------: | :------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
|
| `apiKey` | `string` | `:local apiKey "3su1OLc0gUhUdwxn1bmKFss5V19mBhBx";` | This variable requires a valid API token for the [Hetzner DNS API](https://dns.hetzner.com/api-docs). You can create an [API token here](https://dns.hetzner.com/settings/api-token). |
|
||||||
|
| `domainEntryConfig` | `array`s of `string`s | `:local domainEntryConfig {{"pppoe-out1";"";"domain.com";"A";"@";"300";};{"pppoe-out1";"pool-ipv6";"domain.com";"AAAA";"@";"300";};};` | See below how to format the arrays correctly. |
|
||||||
|
|
||||||
|
#### `domainEntryConfig` array data sheet
|
||||||
|
|
||||||
|
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. |
|
||||||
|
|
||||||
|
Configuration example:
|
||||||
|
|
||||||
|
```
|
||||||
|
:local domainEntryConfig {
|
||||||
|
{
|
||||||
|
"pppoe-out1";
|
||||||
|
"";
|
||||||
|
"domain.com";
|
||||||
|
"A";
|
||||||
|
"@";
|
||||||
|
"300";
|
||||||
|
};
|
||||||
|
{"pppoe-out1";"pool-ipv6";"domain.com";"AAAA";"@";"300";};
|
||||||
|
{"pppoe-out1";"";"example.de";"A";"ddns";"300";};
|
||||||
|
{"pppoe-out1";"pool-ipv6";"abc.xzy";"AAAA";"ddns";"300";};
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
This example will create & update those DNS records:
|
||||||
|
- domain.com
|
||||||
|
- IPv4
|
||||||
|
- IPv6
|
||||||
|
- example.de
|
||||||
|
- IPv4
|
||||||
|
- abc.xzy
|
||||||
|
- IPv6
|
266
ddns-hetzner.rsc
Executable file
266
ddns-hetzner.rsc
Executable file
|
@ -0,0 +1,266 @@
|
||||||
|
# -------------------------------------------------------------------------------
|
||||||
|
# DDNS update script for Hetzner's DNS API
|
||||||
|
#
|
||||||
|
# by Philip 'ShokiNN' Henning <mail@philip-henning.com>
|
||||||
|
# Version 1.0
|
||||||
|
# last update: 10.11.2023
|
||||||
|
# License: MIT
|
||||||
|
# -------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# --- Define variables -----------------------------------------------------------------------------------------
|
||||||
|
# Enter all required variables and secrets here. -- All secrets are stored unencrypted!
|
||||||
|
## API Key to authenticate to Hetzners API
|
||||||
|
### Data Type: String
|
||||||
|
:local apiKey ""; # Example: "3su1OLc0gUhUdwxn1bmKFss5V19mBhBx"; -- This one is invalid, you don't need to try ;)
|
||||||
|
|
||||||
|
## --- Domain config --------------------------------------------------------------------------------
|
||||||
|
# Interface
|
||||||
|
# The interface name where the IP should be fetched from
|
||||||
|
# Data Type: String
|
||||||
|
# Example: "pppoe-out1";
|
||||||
|
#
|
||||||
|
# Pool
|
||||||
|
# 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
|
||||||
|
# Data Type: String
|
||||||
|
# Example: "pool-ipv6";
|
||||||
|
#
|
||||||
|
# Zone
|
||||||
|
# Zone which should be used to set a record to
|
||||||
|
# Data Type: String
|
||||||
|
# Example: "domain.com";
|
||||||
|
#
|
||||||
|
# Record type
|
||||||
|
# The type of record which will be set
|
||||||
|
# Data Type: String
|
||||||
|
# Valid values: "A", "AAAA"
|
||||||
|
# Example: "A";
|
||||||
|
#
|
||||||
|
# Record name
|
||||||
|
# Record name to be used to set a DNS entry
|
||||||
|
# Data Type: String
|
||||||
|
# Example: "@"; -- use @ to setup an entry at the root of your domain, e.g. "domain.com"
|
||||||
|
#
|
||||||
|
# Record TTL
|
||||||
|
# TTL value of the record in seconds, for a dynamic entry a short lifetime like 300 is recommended
|
||||||
|
# Data Type: String
|
||||||
|
# Example: "300";
|
||||||
|
#
|
||||||
|
# Array structure
|
||||||
|
# {
|
||||||
|
# "pppoe-out1"; # Interface
|
||||||
|
# ""; # Pool
|
||||||
|
# "domain.com"; # Zone
|
||||||
|
# "A"; # Record type
|
||||||
|
# "@"; # Record name
|
||||||
|
# 300; # Record TTL
|
||||||
|
# };
|
||||||
|
## --------------------------------------------------------------------------------------------------
|
||||||
|
:local domainEntryConfig {
|
||||||
|
{
|
||||||
|
"pppoe-out1";
|
||||||
|
"";
|
||||||
|
"domain.com";
|
||||||
|
"A";
|
||||||
|
"@";
|
||||||
|
"300";
|
||||||
|
};
|
||||||
|
{"pppoe-out1";"pool-ipv6";"domain.com";"AAAA";"@";"300";};
|
||||||
|
};
|
||||||
|
# ---------------------------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
:local logPrefix "[Hetzner DDNS]";
|
||||||
|
:local apiUrl "https://dns.hetzner.com/api/v1";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
:local getLocalIpv4 do={
|
||||||
|
:local ip [/ip address get [:pick [find interface="$configInterface"] 0] address];
|
||||||
|
:return [:pick $ip 0 [:find $ip /]];
|
||||||
|
};
|
||||||
|
|
||||||
|
:local getLocalIpv6 do={
|
||||||
|
:local ip [/ipv6 address get [:pick [find interface="$configInterface" from-pool="$configInterfacePool" !link-local] 0] address];
|
||||||
|
:return [:pick $ip 0 [:find $ip /]];
|
||||||
|
};
|
||||||
|
|
||||||
|
:local getRemoteIpv4 do={
|
||||||
|
:do {
|
||||||
|
:local ip [:resolve "$configDomain"];
|
||||||
|
:return "$ip";
|
||||||
|
} on-error={
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
:local getRemoveIpv6 do={
|
||||||
|
:local result [:toarray ""]
|
||||||
|
:local maxwait 5
|
||||||
|
:local cnt 0
|
||||||
|
:local listname "tmp-resolve$cnt"
|
||||||
|
/ipv6 firewall address-list {
|
||||||
|
:do {
|
||||||
|
:while ([:len [find list=$listname]] > 0) do={
|
||||||
|
:set cnt ($cnt + 1);
|
||||||
|
:set listname "tmp-resolve$cnt";
|
||||||
|
};
|
||||||
|
:set cnt 0;
|
||||||
|
add list=$listname address=$1;
|
||||||
|
:while ([find list=$listname && dynamic] = "" && $cnt < $maxwait) do={
|
||||||
|
:delay 1;:set cnt ($cnt +1)
|
||||||
|
};
|
||||||
|
:foreach i in=[find list=$listname && dynamic] do={
|
||||||
|
:local rawip [get $i address];
|
||||||
|
:set result ($result, [:pick $rawip 0 [:find $rawip "/"]]);
|
||||||
|
};
|
||||||
|
remove [find list=$listname && !dynamic];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
:return $result;
|
||||||
|
};
|
||||||
|
|
||||||
|
:local apiGetZones do={
|
||||||
|
[/system script run "JParseFunctions"; global JSONLoad; global JSONLoads; global JSONUnload];
|
||||||
|
|
||||||
|
:local apiPage -0;
|
||||||
|
:local apiNextPage 1;
|
||||||
|
:local apiLastPage 0;
|
||||||
|
:local apiResponse "";
|
||||||
|
:local returnArr [:toarray ""];
|
||||||
|
|
||||||
|
:do {
|
||||||
|
:set apiResponse ([/tool/fetch "$apiUrl/zones?page=$apiNextPage&search_name=$configZone" http-method=get http-header-field="Auth-API-Token:$apiKey" output=user as-value]->"data");
|
||||||
|
|
||||||
|
:set apiPage ([$JSONLoads $apiResponse]->"meta"->"pagination"->"page");
|
||||||
|
:set apiNextPage ([$JSONLoads $apiResponse]->"meta"->"pagination"->"next_page");
|
||||||
|
:set apiLastPage ([$JSONLoads $apiResponse]->"meta"->"pagination"->"last_page");
|
||||||
|
|
||||||
|
:set returnArr ($returnArr , ([:toarray ([$JSONLoads $apiResponse]->"zones")]));
|
||||||
|
} while=($apiPage != $apiLastPage);
|
||||||
|
$JSONUnload;
|
||||||
|
|
||||||
|
:return $returnArr;
|
||||||
|
};
|
||||||
|
|
||||||
|
:local apiGetZoneId do={
|
||||||
|
:foreach responseZone in=$responseZones do={
|
||||||
|
:if (($responseZone->"name") = $configZone) do={
|
||||||
|
:return ($responseZone->"id");
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
:local apiSetRecord do={
|
||||||
|
#apiUrl=$apiUrl apiKey=$apiKey zoneId=$zoneId configType=$configType configRecord=$configRecord configTtl=$configTtl interfaceIp=$interfaceIp
|
||||||
|
[/system script run "JParseFunctions"; global JSONLoad; global JSONLoads; global JSONUnload];
|
||||||
|
|
||||||
|
:local recordId "";
|
||||||
|
:local apiResponse "";
|
||||||
|
:local payload "{\"zone_id\": \"$zoneId\",\"type\": \"$configType\",\"name\": \"$configRecord\",\"value\": \"$interfaceIp\",\"ttl\": $([:tonum $configTtl])}";
|
||||||
|
:local records ([$JSONLoads ([/tool/fetch "$apiUrl/records?zone_id=$zoneId" http-method=get http-header-field="Auth-API-Token:$apiKey" output=user as-value]->"data")]->"records");
|
||||||
|
|
||||||
|
:foreach record in=$records do={
|
||||||
|
:if ((($record->"name") = $configRecord) && (($record->"type") = $configType)) do={
|
||||||
|
:set recordId ($record->"id");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
:if ($recordId != "") do={
|
||||||
|
:set apiResponse ([/tool/fetch "$apiUrl/records/$recordId" http-method=put http-header-field="Content-Type:application/json,Auth-API-Token:$apiKey" http-data=$payload output=user as-value]->"status");
|
||||||
|
} else={
|
||||||
|
:set apiResponse ([/tool/fetch "$apiUrl/records" http-method=post http-header-field="Content-Type:application/json,Auth-API-Token:$apiKey" http-data=$payload output=user as-value]->"status");
|
||||||
|
};
|
||||||
|
|
||||||
|
$JSONUnload;
|
||||||
|
return $apiResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
# Log "run of script"
|
||||||
|
:log info "$logPrefix running";
|
||||||
|
|
||||||
|
:local index 0;
|
||||||
|
:foreach i in=$domainEntryConfig do={
|
||||||
|
:local configInterface ("$($i->0)");
|
||||||
|
:local configIpv6Pool ("$($i->1)");
|
||||||
|
:local configZone ("$($i->2)");
|
||||||
|
:local configType ("$($i->3)");
|
||||||
|
:local configRecord ("$($i->4)");
|
||||||
|
:local configTtl ("$($i->5)");
|
||||||
|
:local configDomain "";
|
||||||
|
:local interfaceIp "";
|
||||||
|
:local dnsIp "";
|
||||||
|
:local startLogMsg "$logPrefix Start configuring domain:";
|
||||||
|
:local endLogMsg "$logPrefix Finished configuring domain:";
|
||||||
|
|
||||||
|
|
||||||
|
:if ($configRecord = "@") do={
|
||||||
|
:set configDomain ("$($i->2)");
|
||||||
|
} else={
|
||||||
|
:set configDomain ("$($i->4).$($i->2)");
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
:if ($configType = "A") do={
|
||||||
|
:log info "$startLogMsg $configDomain - Type A record";
|
||||||
|
|
||||||
|
:set interfaceIp [$getLocalIpv4 configInterface=$configInterface];
|
||||||
|
:set dnsIp [$getRemoteIpv4 configDomain=$configDomain];
|
||||||
|
|
||||||
|
:if ($interfaceIp != $dnsIp) do={
|
||||||
|
:log info "$logPrefix $configDomain: local IP ($interfaceIp) differs from DNS IP ($dnsIp) - Updating entry";
|
||||||
|
|
||||||
|
:local responseZones [$apiGetZones apiUrl=$apiUrl apiKey=$apiKey configZone=$configZone];
|
||||||
|
:local zoneId [$apiGetZoneId responseZones=$responseZones configZone=$configZone];
|
||||||
|
:local responseSetRecord [$apiSetRecord apiUrl=$apiUrl apiKey=$apiKey zoneId=$zoneId configType=$configType configRecord=$configRecord configTtl=$configTtl interfaceIp=$interfaceIp];
|
||||||
|
:if ($responseSetRecord = "finished") do={
|
||||||
|
:log info "$logPrefix $configDomain: update successful"
|
||||||
|
};
|
||||||
|
} else={
|
||||||
|
:log info "$logPrefix $configDomain: local IP and DNS IP are equal - Nothing to do";
|
||||||
|
}
|
||||||
|
|
||||||
|
:log info "$endLogMsg $configDomain - Type A record";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
:if ($configType = "AAAA") do={
|
||||||
|
:log info "$startLogMsg $configDomain - Type AAAA record";
|
||||||
|
|
||||||
|
:set interfaceIp [$getLocalIpv6 configInterface=$configInterface configInterfacePool=$configIpv6Pool];
|
||||||
|
:set dnsIp [$getRemoveIpv6 $configDomain];
|
||||||
|
|
||||||
|
:if ($interfaceIp != $dnsIp) do={
|
||||||
|
:log info "$logPrefix $configDomain: local IP ($interfaceIp) differs from DNS IP ($dnsIp) - Updating entry";
|
||||||
|
|
||||||
|
:local responseZones [$apiGetZones apiUrl=$apiUrl apiKey=$apiKey configZone=$configZone];
|
||||||
|
:local zoneId [$apiGetZoneId responseZones=$responseZones configZone=$configZone];
|
||||||
|
:local responseSetRecord [$apiSetRecord apiUrl=$apiUrl apiKey=$apiKey zoneId=$zoneId configType=$configType configRecord=$configRecord configTtl=$configTtl interfaceIp=$interfaceIp];
|
||||||
|
:if ($responseSetRecord = "finished") do={
|
||||||
|
:log info "$logPrefix $configDomain: update successful"
|
||||||
|
};
|
||||||
|
} else={
|
||||||
|
:log info "$logPrefix $configDomain: local IP and DNS IP are equal - Nothing to do";
|
||||||
|
}
|
||||||
|
|
||||||
|
:log info "$endLogMsg $configDomain - Type AAAA record";
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
:if (($configType != "A") && ($configType != "AAAA")) do={
|
||||||
|
:log error ("$logPrefix Wrong record type for array index number " . $index . " (Value: $configType)");
|
||||||
|
};
|
||||||
|
|
||||||
|
:set index ($index+1);
|
||||||
|
};
|
||||||
|
:set index;
|
||||||
|
|
||||||
|
:log info "$logPrefix finished";
|
Loading…
Reference in a new issue