-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathddns-hetzner.rsc
executable file
·268 lines (218 loc) · 9.52 KB
/
ddns-hetzner.rsc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# -------------------------------------------------------------------------------
# DDNS update script for Hetzner's DNS API
#
# by Philip 'ShokiNN' Henning <[email protected]>
# Version 1.0
# last update: 10.11.2023
# License: MIT
#
# Compatibility: RouterOS 7
# -------------------------------------------------------------------------------
# --- 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";