11#!/usr/bin/php -d open_basedir=/usr/syno/bin/ddns
22<?php
33
4- if ($ argc !== 5 ) {
5- echo 'badparam ' ;
4+ // Normally $argv suffices: $argc seems a bit pointless because amount of arguments & array elements should be same
5+ if ($ argc !== 5 || count ($ argv ) != 5 ) {
6+ echo Output::INSUFFICIENT_OR_UNKNOWN_PARAMETERS ;
67 exit ();
78}
89
910$ cf = new updateCFDDNS ($ argv );
1011$ cf ->makeUpdateDNS ();
1112
13+ class Output
14+ {
15+ // Confirmed & logged interpreted/translated messages by Synology
16+ const SUCCESS = 'good ' ; // geeft niets? - geeft succesfully registered in logs
17+ const NO_CHANGES = 'nochg ' ; // geeft niets? - geeft succesfully registered in logs
18+ const HOSTNAME_DOES_NOT_EXIST = 'nohost ' ; // [The hostname specified does not exist. Check if you created the hostname on the website of your DNS provider]
19+ const HOSTNAME_BLOCKED = 'abuse ' ; // [The hostname specified is blocked for update abuse]
20+ const HOSTNAME_FORMAT_IS_INCORRECT = 'notfqdn ' ; // [The format of hostname is not correct]
21+ const AUTHENTICATION_FAILED = 'badauth ' ; // [Authentication failed]
22+ const DDNS_PROVIDER_DOWN = '911 ' ; // [Server is broken][De DDNS-server is tijdelijk buiten dienst. Neem contact op met de Internet-provider.]
23+ const BAD_HTTP_REQUEST = 'badagent ' ; // [DDNS function needs to be modified, please contact synology support]
24+ const HOSTNAME_FORMAT_INCORRECT = 'badparam ' ; // [The format of hostname is not correct]
25+
26+ // Not logged messages, didn't trigger/work while testing on DSM
27+ const PROVIDER_ADDRESS_NOT_RESOLVED = 'badresolv ' ;
28+ const PROVIDER_TIMEOUT_CONNECTION = 'badconn ' ;
29+
30+ // Console only - custom error messages (not triggered by DSM)
31+ const INSUFFICIENT_OR_UNKNOWN_PARAMETERS = 'Insufficient parameters ' ;
32+ }
33+
1234/**
1335 * DDNS auto updater for Synology NAS
1436 * Base on Cloudflare API v4
1739class updateCFDDNS
1840{
1941 const API_URL = 'https://api.cloudflare.com ' ;
20- var $ account , $ apiKey , $ hostList , $ ip ;
42+ var $ account , $ apiKey , $ hostList , $ ipv4 ; // argument properties - $ipv4 is provided by DSM itself
43+ var $ ip , $ dnsRecordIdList = array (), $ ipv6 = false ;
2144
2245 function __construct ($ argv )
2346 {
24- if (count ($ argv ) != 5 ) {
25- $ this ->badParam ('wrong parameter count ' );
26- }
27-
47+ // Not used: $account ($argv[1]), Used: $apikey ($argv[2]), $hostslist ($argv[3]), $ipv4 ($argv[4])
2848 $ this ->apiKey = (string ) $ argv [2 ]; // CF Global API Key
29- $ hostname = (string ) $ argv [3 ]; // example: example.com.uk---sundomain.example1.com---example2.com
30- $ this ->ip = (string ) $ this ->getIpAddressIpify ();
49+ $ hostnames = (string ) $ argv [3 ]; // example: example.com.uk---sundomain.example1.com---example2.com
3150
32- $ this ->validateIp ( $ this ->ip );
51+ $ this ->ipv6 = $ this ->getIpAddressIpify ( );
3352
34- $ arHost = explode ('--- ' , $ hostname );
35- if (empty ($ arHost )) {
36- $ this ->badParam ('empty host list ' );
37- }
53+ if ($ this ->ipv6 )
54+ $ this ->validateIp ((string ) $ this ->ipv6 ); // Validates IPV6
55+
56+ // Test address to force-enable IPV6 manually to simulate ipv6 "found":
57+ //$this->ipv6 = "2222:7e01::f03c:91ff:fe99:b41d";
58+
59+ // Since DSM is only providing an IP(v4) address (DSM 6/7 doesn't deliver IPV6)
60+ // I override above IPV4 detection & rely on DSM instead for now
61+ $ this ->validateIp ((string ) $ argv [4 ]);
3862
63+ // safer than explode: in case of wrong formatting with --- separations (empty elements removed automatically)
64+ $ arHost = preg_split ('/(---)/ ' , $ hostnames , -1 , PREG_SPLIT_NO_EMPTY );
65+
66+ // parse each array element to check if every dns hostname is properly formatted, unset any garbage element
3967 foreach ($ arHost as $ value ) {
68+ if (!preg_match ("/^(?!-)(?:(?:[a-zA-Z\d][a-zA-Z\d\-]{0,61})?[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63}$/ " , $ value )) {
69+ echo Output::HOSTNAME_FORMAT_INCORRECT ;
70+ exit ();
71+ }
72+
4073 $ this ->hostList [$ value ] = [
4174 'hostname ' => '' ,
4275 'fullname ' => $ value ,
4376 'zoneId ' => '' ,
44- 'recordId ' => '' ,
45- 'proxied ' => true ,
4677 ];
4778 }
4879
4980 $ this ->setZones ();
81+
5082 foreach ($ this ->hostList as $ arHost ) {
51- $ this ->setRecord ($ arHost ['fullname ' ], $ arHost ['zoneId ' ]);
83+ $ this ->setRecord ($ arHost , $ this ->ipv4 , 'A ' );
84+ if ($ this ->ipv6 ) {
85+ $ this ->setRecord ($ arHost , $ this ->ipv6 , 'AAAA ' );
86+ }
5287 }
5388 }
5489
5590 /**
56- * Update CF DNS records
91+ * Update CF DNS records
5792 */
5893 function makeUpdateDNS ()
5994 {
6095 if (empty ($ this ->hostList )) {
6196 $ this ->badParam ('empty host list ' );
6297 }
6398
64- foreach ($ this ->hostList as $ arHost ) {
65- $ post = [
66- 'type ' => $ this ->getZoneTypeByIp ($ this ->ip ),
67- 'name ' => $ arHost ['fullname ' ],
68- 'content ' => $ this ->ip ,
69- 'ttl ' => 1 ,
70- 'proxied ' => $ arHost ['proxied ' ],
71- ];
99+ foreach ($ this ->dnsRecordIdList as $ recordId => $ dnsRecord ) {
100+ $ zoneId = $ dnsRecord ['zoneId ' ];
101+ unset($ dnsRecord ['zoneId ' ]);
102+
103+ $ json = $ this ->callCFapi ("PATCH " , "client/v4/zones/ $ {zoneId}/dns_records/ $ {recordId}" , $ dnsRecord );
72104
73- $ json = $ this ->callCFapi ("PUT " , "client/v4/zones/ " . $ arHost ['zoneId ' ] . "/dns_records/ " . $ arHost ['recordId ' ], $ post );
74105 if (!$ json ['success ' ]) {
75106 echo 'Update Record failed ' ;
76107 exit ();
77108 }
78109 }
79- echo "good " ;
110+
111+ echo Output::SUCCESS ;
80112 }
81113
82114 function badParam ($ msg = '' )
@@ -85,29 +117,42 @@ function badParam($msg = '')
85117 exit ();
86118 }
87119
120+ /**
121+ * Evaluates IP address type and assigns to the correct IP property type
122+ * Only public addresses accessible from the internet are valid options
123+ *
124+ * @param $ip
125+ * @return bool
126+ */
88127 function validateIp ($ ip )
89128 {
90- if (!filter_var ($ ip , FILTER_VALIDATE_IP )) {
129+ if (filter_var ($ ip , FILTER_VALIDATE_IP , FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE )) {
130+ $ this ->ipv6 = $ ip ;
131+ } elseif (filter_var ($ ip , FILTER_VALIDATE_IP , FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE )) {
132+ $ this ->ipv4 = $ ip ;
133+ } else {
91134 $ this ->badParam ('invalid ip-address ' );
92135 }
136+
93137 return true ;
94138 }
139+
95140 /*
96- * get ip from ipify.org
141+ * Get ip from ipify.org
142+ * Returns IPV6 address or false boolean in case IP6V is not found
97143 */
98144 function getIpAddressIpify () {
99- return file_get_contents ('https://api64.ipify.org ' );
100- }
101145
102- /*
103- * IPv4 = zone A, IPv6 = zone AAAA
104- * @link https://www.cloudflare.com/en-au/learning/dns/dns-records/dns-a-record/
105- */
106- function getZoneTypeByIp ($ ip ) {
107- if (filter_var ($ ip , FILTER_VALIDATE_IP , FILTER_FLAG_IPV6 )) {
108- return 'AAAA ' ;
109- }
110- return 'A ' ;
146+ $ curlhandle = curl_init ();
147+ curl_setopt ($ curlhandle , CURLOPT_URL , "https://api64.ipify.org " );
148+ curl_setopt ($ curlhandle , CURLOPT_IPRESOLVE , CURL_IPRESOLVE_V6 );
149+ curl_setopt ($ curlhandle , CURLOPT_CONNECTTIMEOUT , 10 );
150+ curl_setopt ($ curlhandle , CURLOPT_TIMEOUT , 30 );
151+ curl_setopt ($ curlhandle , CURLOPT_VERBOSE , false );
152+ curl_setopt ($ curlhandle , CURLOPT_RETURNTRANSFER , true );
153+ $ result = curl_exec ($ curlhandle );
154+ curl_close ($ curlhandle );
155+ return $ result ;
111156 }
112157
113158 /**
@@ -117,6 +162,13 @@ function setZones()
117162 {
118163 $ json = $ this ->callCFapi ("GET " , "client/v4/zones " );
119164 if (!$ json ['success ' ]) {
165+ if (isset ($ json ['errors ' ][0 ]['code ' ])) {
166+ if ($ json ['errors ' ][0 ]['code ' ] == 9109 || $ json ['errors ' ][0 ]['code ' ] == 6003 ) {
167+ echo Output::AUTHENTICATION_FAILED ;
168+ exit ();
169+ }
170+ }
171+
120172 $ this ->badParam ('getZone unsuccessful response ' );
121173 }
122174 $ arZones = [];
@@ -152,25 +204,37 @@ function isZonesContainFullname($arZones, $fullname){
152204 }
153205
154206 /**
155- * Set Records for each hosts
207+ * Set A Records for each host
156208 */
157- function setRecord ($ fullname , $ zoneId )
209+ function setRecord ($ arHostData , string $ ip , $ type )
158210 {
159- if (empty ($ fullname )) {
211+ if (empty ($ arHostData [ ' fullname ' ] )) {
160212 return false ;
161213 }
162214
163- if (empty ($ zoneId )) {
215+ $ fullname = $ arHostData ['fullname ' ];
216+
217+ if (empty ($ arHostData ['zoneId ' ])) {
164218 unset($ this ->hostList [$ fullname ]);
165219 return false ;
166220 }
167221
168- $ json = $ this ->callCFapi ("GET " , "client/v4/zones/ $ {zoneId}/dns_records?type=A&name= $ {fullname}" );
222+ $ zoneId = $ arHostData ['zoneId ' ];
223+
224+ $ json = $ this ->callCFapi ("GET " , "client/v4/zones/ $ {zoneId}/dns_records?type= $ {type}&name= $ {fullname}" );
225+
169226 if (!$ json ['success ' ]) {
170227 $ this ->badParam ('unsuccessful response for getRecord host: ' . $ fullname );
171228 }
172- $ this ->hostList [$ fullname ]['recordId ' ] = $ json ['result ' ]['0 ' ]['id ' ];
173- $ this ->hostList [$ fullname ]['proxied ' ] = $ json ['result ' ]['0 ' ]['proxied ' ];
229+
230+ if (isset ($ json ['result ' ]['0 ' ])){
231+ $ this ->dnsRecordIdList [$ json ['result ' ]['0 ' ]['id ' ]]['type ' ] = $ type ;
232+ $ this ->dnsRecordIdList [$ json ['result ' ]['0 ' ]['id ' ]]['name ' ] = $ arHostData ['fullname ' ];
233+ $ this ->dnsRecordIdList [$ json ['result ' ]['0 ' ]['id ' ]]['content ' ] = $ ip ;
234+ $ this ->dnsRecordIdList [$ json ['result ' ]['0 ' ]['id ' ]]['zoneId ' ] = $ arHostData ['zoneId ' ];
235+ $ this ->dnsRecordIdList [$ json ['result ' ]['0 ' ]['id ' ]]['ttl ' ] = $ json ['result ' ]['0 ' ]['ttl ' ];
236+ $ this ->dnsRecordIdList [$ json ['result ' ]['0 ' ]['id ' ]]['proxied ' ] = $ json ['result ' ]['0 ' ]['proxied ' ];
237+ }
174238 }
175239
176240 /**
@@ -192,20 +256,26 @@ function callCFapi($method, $path, $data = []) {
192256 switch ($ method ) {
193257 case "GET " :
194258 $ options [CURLOPT_HTTPGET ] = true ;
195- break ;
259+ break ;
196260
197261 case "POST " :
198262 $ options [CURLOPT_POST ] = true ;
199263 $ options [CURLOPT_HTTPGET ] = false ;
200264 $ options [CURLOPT_POSTFIELDS ] = json_encode ($ data );
201- break ;
265+ break ;
202266
203267 case "PUT " :
204268 $ options [CURLOPT_POST ] = false ;
205269 $ options [CURLOPT_HTTPGET ] = false ;
206270 $ options [CURLOPT_CUSTOMREQUEST ] = "PUT " ;
207271 $ options [CURLOPT_POSTFIELDS ] = json_encode ($ data );
208- break ;
272+ break ;
273+ case "PATCH " :
274+ $ options [CURLOPT_POST ] = false ;
275+ $ options [CURLOPT_HTTPGET ] = false ;
276+ $ options [CURLOPT_CUSTOMREQUEST ] = "PATCH " ;
277+ $ options [CURLOPT_POSTFIELDS ] = json_encode ($ data );
278+ break ;
209279 }
210280
211281 $ req = curl_init ();
0 commit comments