Skip to content

Commit c59b843

Browse files
authored
Merge pull request #14 from smoetje/ipv4-ipv6-fixes
Update cloudflare.php
2 parents 25d3e4f + 4b0b6c0 commit c59b843

2 files changed

Lines changed: 122 additions & 52 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
* Supports single domains, multidomains, subdomains and regional domains, or any combination thereof (example: dev.my.domain.com.au, domain.com.uk etc)
1919
* Easy instalation process
2020
* Based on CloudFlare API v4
21-
* [Supports IPv4 and IPv6](https://github.com/mrikirill/SynologyDDNSCloudflareMultidomain/pull/13)
21+
* [Supports dual stack IPv4 and IPv6](https://github.com/mrikirill/SynologyDDNSCloudflareMultidomain/pull/13)
2222

2323
## Before you begin
2424

cloudflare.php

Lines changed: 121 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
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
@@ -17,66 +39,76 @@
1739
class 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

Comments
 (0)