前言
最近因为电价飞涨,家里服务器的电费算下来每个月要将近1000元人民币,实在是吃不消了。于是关机后搞了个西部数据的My Cloud EX2 Ultra,装上黑群晖之后实际功耗只有10W。虽然DSM只有6.0版本,但是也是够用了,不喜欢折腾,低功耗 稳定 性能够用就已经非常的棒了。后面如果有空的话会写个流程记录一下。
在国内家里的DS1517 的QC用着就和很舒服,不论是在家中还是在外面都随时可以使用家里的数据和查找照片,奈何实在是囊中羞涩,临时用黑群以后再回归吧。没有QC其实用DDNS NAT端口映射也能完全替代,这里先解决DDNS问题,NAT只要运营商支持的话在路由器中设置一下就行了,没什么门槛。
实际效果如上,由于我使用了Plesk的伪CF-Pro,所以DNS是无法直接在网页版的CloudFlare面板中修改的,使用云筏科技的Partner面板可以正常查看到DNS信息:
https://www.cloudraft.cn/cfp/
我这里用的脚本是:mrikirill/SynologyDDNSCloudflareMultidomain
教程开始
下载脚本
首先SSH登录群晖,使用管理员账户,这一步需要提前在控制面板中开启SSH功能:
然后执行如下命令:
wget https://raw.githubusercontent.com/mrikirill/SynologyDDNSCloudflareMultidomain/master/cloudflare.php -O /usr/syno/bin/ddns/cloudflare.php && sudo chmod 755 /usr/syno/bin/ddns/cloudflare.php
这里为了防止代码丢失我贴一下这个php脚本的源码:
#!/usr/bin/php -d open_basedir=/usr/syno/bin/ddns
<?php
// Normally $argv suffices: $argc seems a bit pointless because amount of arguments & array elements should be same
if ($argc !== 5 || count($argv) != 5) {
echo Output::INSUFFICIENT_OR_UNKNOWN_PARAMETERS;
exit();
}
$cf = new updateCFDDNS($argv);
$cf->makeUpdateDNS();
class Output
{
// Confirmed & logged interpreted/translated messages by Synology
const SUCCESS = 'good'; // geeft niets? - geeft succesfully registered in logs
const NO_CHANGES = 'nochg'; // geeft niets? - geeft succesfully registered in logs
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]
const HOSTNAME_BLOCKED = 'abuse'; // [The hostname specified is blocked for update abuse]
const HOSTNAME_FORMAT_IS_INCORRECT = 'notfqdn'; // [The format of hostname is not correct]
const AUTHENTICATION_FAILED = 'badauth'; // [Authentication failed]
const DDNS_PROVIDER_DOWN = '911'; // [Server is broken][De DDNS-server is tijdelijk buiten dienst. Neem contact op met de Internet-provider.]
const BAD_HTTP_REQUEST = 'badagent'; // [DDNS function needs to be modified, please contact synology support]
const HOSTNAME_FORMAT_INCORRECT = 'badparam'; // [The format of hostname is not correct]
const BAD_PARAMS = 'badparam';
// Not logged messages, didn't trigger/work while testing on DSM
const PROVIDER_ADDRESS_NOT_RESOLVED = 'badresolv';
const PROVIDER_TIMEOUT_CONNECTION = 'badconn';
// Console only - custom error messages (not triggered by DSM)
const INSUFFICIENT_OR_UNKNOWN_PARAMETERS = 'Insufficient parameters';
}
/**
* DDNS auto updater for Synology NAS
* Base on Cloudflare API v4
* Supports multidomains and sundomains
*/
class updateCFDDNS
{
const API_URL = 'https://api.cloudflare.com';
var $account, $apiKey, $hostList, $ipv4; // argument properties - $ipv4 is provided by DSM itself
var $ip, $dnsRecordIdList = array(), $ipv6 = false;
function __construct($argv)
{
// Not used: $account ($argv[1]), Used: $apikey ($argv[2]), $hostslist ($argv[3]), $ipv4 ($argv[4])
$this->apiKey = (string) $argv[2]; // CF Global API Key
$hostnames = (string) $argv[3]; // example: example.com.uk---sundomain.example1.com---example2.com
$this->ipv6 = $this->getIpAddressIpify();
if($this->ipv6)
$this->validateIp((string) $this->ipv6); // Validates IPV6
// Since DSM is only providing an IP(v4) address (DSM 6/7 doesn't deliver IPV6)
// I override above IPV4 detection & rely on DSM instead for now
$this->validateIp((string) $argv[4]);
// Before runs DNS update checks API token is valid or not
if(!$this->isCFTokenValid()) {
$this->badParam();
}
// safer than explode: in case of wrong formatting with --- separations (empty elements removed automatically)
$arHost = preg_split('/(---)/', $hostnames, -1, PREG_SPLIT_NO_EMPTY);
// parse each array element to check if every dns hostname is properly formatted, unset any garbage element
foreach ($arHost as $value) {
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)) {
echo Output::HOSTNAME_FORMAT_INCORRECT;
exit();
}
$this->hostList[$value] = [
'hostname' => '',
'fullname' => $value,
'zoneId' => '',
];
}
$this->setZones();
foreach ($this->hostList as $arHost) {
$this->setRecord($arHost, $this->ipv4, 'A');
if($this->ipv6) {
$this->setRecord($arHost, $this->ipv6, 'AAAA');
}
}
}
/**
* Checks CF API Token is valid
*
* @return bool
*/
function isCFTokenValid()
{
$res = $this->callCFapi("GET", "client/v4/user/tokens/verify");
if ($res['success']) {
return true;
}
return false;
}
/**
* Update CF DNS records
*/
function makeUpdateDNS()
{
if(empty($this->hostList)) {
$this->badParam('empty host list');
}
foreach($this->dnsRecordIdList as $recordId => $dnsRecord) {
$zoneId = $dnsRecord['zoneId'];
unset($dnsRecord['zoneId']);
$json = $this->callCFapi("PATCH", "client/v4/zones/${zoneId}/dns_records/${recordId}", $dnsRecord);
if (!$json['success']) {
echo Output::BAD_HTTP_REQUEST;
exit();
}
}
echo Output::SUCCESS;
}
function badParam($msg = '')
{
echo (strlen($msg) > 0) ? $msg : Output::BAD_PARAMS;
exit();
}
/**
* Evaluates IP address type and assigns to the correct IP property type
* Only public addresses accessible from the internet are valid options
*
* @param $ip
* @return bool
*/
function validateIp($ip)
{
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE )) {
$this->ipv6 = $ip;
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE )) {
$this->ipv4 = $ip;
} else {
$this->badParam('invalid ip-address');
}
return true;
}
/*
* Get ip from ipify.org
* Returns IPV6 address or false boolean in case IP6V is not found
*/
function getIpAddressIpify() {
$curlhandle = curl_init();
curl_setopt($curlhandle, CURLOPT_URL, "https://api64.ipify.org");
curl_setopt($curlhandle, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6);
curl_setopt($curlhandle, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($curlhandle, CURLOPT_TIMEOUT, 30);
curl_setopt($curlhandle, CURLOPT_VERBOSE, false);
curl_setopt($curlhandle, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($curlhandle);
curl_close($curlhandle);
return $result;
}
/**
* Set ZoneID for each hosts
*/
function setZones()
{
$json = $this->callCFapi("GET", "client/v4/zones");
if (!$json['success']) {
if(isset($json['errors'][0]['code'])) {
if($json['errors'][0]['code'] == 9109 || $json['errors'][0]['code'] == 6003) {
echo Output::AUTHENTICATION_FAILED;
exit();
}
}
$this->badParam('getZone unsuccessful response');
}
$arZones = [];
foreach ($json['result'] as $ar) {
$arZones[] = [
'hostname' => $ar['name'],
'zoneId' => $ar['id']
];
}
foreach ($this->hostList as $hostname => $arHost) {
$res = $this->isZonesContainFullname($arZones, $arHost['fullname']);
if(!empty($res)) {
$this->hostList[$hostname]['zoneId'] = $res['zoneId'];
$this->hostList[$hostname]['hostname'] = $res['hostname'];
}
}
}
/**
* Find hostname for full domain name
* example: domain.com.uk --> vpn.domain.com.uk
*/
function isZonesContainFullname($arZones, $fullname)
{
$res = [];
foreach($arZones as $arZone) {
if (strpos($fullname, $arZone['hostname']) !== false) {
$res = $arZone;
break;
}
}
return $res;
}
/**
* Set A Records for each host
*/
function setRecord($arHostData, $ip, $type)
{
if (empty($arHostData['fullname'])) {
return false;
}
$fullname = $arHostData['fullname'];
if (empty($arHostData['zoneId'])) {
unset($this->hostList[$fullname]);
return false;
}
$zoneId = $arHostData['zoneId'];
$json = $this->callCFapi("GET", "client/v4/zones/${zoneId}/dns_records?type=${type}&name=${fullname}");
if (!$json['success']) {
$this->badParam('unsuccessful response for getRecord host: ' . $fullname);
}
if(isset($json['result']['0'])){
$this->dnsRecordIdList[$json['result']['0']['id']]['type'] = $type;
$this->dnsRecordIdList[$json['result']['0']['id']]['name'] = $arHostData['fullname'];
$this->dnsRecordIdList[$json['result']['0']['id']]['content'] = $ip;
$this->dnsRecordIdList[$json['result']['0']['id']]['zoneId'] = $arHostData['zoneId'];
$this->dnsRecordIdList[$json['result']['0']['id']]['ttl'] = $json['result']['0']['ttl'];
$this->dnsRecordIdList[$json['result']['0']['id']]['proxied'] = $json['result']['0']['proxied'];
}
}
/**
* Call CloudFlare v4 API @link https://api.cloudflare.com/#getting-started-endpoints
*/
function callCFapi($method, $path, $data = [])
{
$options = [
CURLOPT_URL => self::API_URL . '/' . $path,
CURLOPT_HTTPHEADER => ["Authorization: Bearer $this->apiKey", "Content-Type: application/json"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => false,
CURLOPT_VERBOSE => false,
];
if(empty($method)){
$this->badParam('Empty method');
}
switch($method) {
case "GET":
$options[CURLOPT_HTTPGET] = true;
break;
case "POST":
$options[CURLOPT_POST] = true;
$options[CURLOPT_HTTPGET] = false;
$options[CURLOPT_POSTFIELDS] = json_encode($data);
break;
case "PUT":
$options[CURLOPT_POST] = false;
$options[CURLOPT_HTTPGET] = false;
$options[CURLOPT_CUSTOMREQUEST] = "PUT";
$options[CURLOPT_POSTFIELDS] = json_encode($data);
break;
case "PATCH":
$options[CURLOPT_POST] = false;
$options[CURLOPT_HTTPGET] = false;
$options[CURLOPT_CUSTOMREQUEST] = "PATCH";
$options[CURLOPT_POSTFIELDS] = json_encode($data);
break;
}
$req = curl_init();
curl_setopt_array($req, $options);
$res = curl_exec($req);
curl_close($req);
return json_decode($res, true);
}
}
?>
增加API选项
还是SSH中,使用vi修改文件/etc.defaults/ddns_provider.conf,添加如下三行内容:
[Cloudflare]
modulepath=/usr/syno/bin/ddns/cloudflare.php
queryurl=https://www.cloudflare.com/
CloudFlare中添加域名和获取API-Token
在CloudFlare中添加需要解析的域名,如果要同时使用IPv4和IPv6的话,需要同一个子域名添加两个记录,分别是A记录和AAAA记录。
API-Token需要在这里获取:https://dash.cloudflare.com/profile/api-tokens 权限只需要Zone.DNS
DSM面板中添加DDNS信息
hostname就填子域名就好了,多个的话用---分隔。
用户名填邮箱,或者直接null就行了,新版本的API不需要邮箱。
密码填写API-Token就好了。
完成后测试
我是直接把A记录和AAAA记录都改成一个其他的值,然后看看是否更新正确,实测IPv4和IPv6都成功更新了。
Please quote the original link:https://www.liujason.com/article/1202.html





