Node.js 实现阿里云域名的动态解析

家用宽带申请公网 IP 往往是动态的公网 IP ,会经常变动,只能借助域名和 DDNS 使服务器可以随时被访问。 使用 Node.js 做 DDNS 也很简单,借助阿里云的 OpenAPI Explorer 可以做一个简单实现。

在线查找域名解析记录的 RecordID

这步没有必要每次都用代码实现,因为域名解析记录的 ID 在修改的过程中是不会发生变化的。 所以直接通过 OpenAPI Explorer 来在线查询 RecordID 即可。

首先在 OpenAPI Explorer 中选择“云解析”的接口,找到 DescribeDomainRecords 接口,填入指定的参数, 点击“发起调用”,即可看到所有的域名解析记录。

参数栏中, DomainName 一处填写要修改解析记录的域名。右侧找到要修改的域名记录,记下 RecordID

动态修改域名解析记录

这里需要借助以下三个接口:

淘宝的接口用 Axios 库调用,阿里云的两个接口可以通过阿里云提供的 SDK 调用。

包的引入和对象声明

这部分引入包,并声明一些变量,以便后面调用。

参数含义
newIP新的 IP 地址
client阿里云 SDK 对象
describeDomainRecordInfoParams调用 DescribeDomainRecordInfo 接口的参数
updateDomainRecordParams调用 UpdateDomainRecord 接口的参数
requestOption阿里云 SDK 请求配置
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
const Core = require('@alicloud/pop-core');
const axios = require('axios').default;
const moment = require('moment');

var newIP = "127.0.0.1";

var client = new Core({
accessKeyId: '<accessKeyId>',
accessKeySecret: '<accessKeySecret>',
endpoint: 'https://alidns.aliyuncs.com',
apiVersion: '2015-01-09'
});

var describeDomainRecordInfoParams = {
"RecordId": "<RecordId>"
};

var updateDomainRecordParams = {
"RecordId": "<RecordId>",
"RR": "@",
"Type": "A"
}

var requestOption = {
method: 'POST'
}

查询当前公网 IP 地址

调用淘宝接口进行查询,结果用正则表达式进行匹配。

1
2
3
4
5
6
7
8
9
10
11
axios.get("http://www.taobao.com/help/getip.php?t=" + moment().format("x")).then((response) => {
if (response.data) {
var matched = /(\d{1,3}\.){3}\d{1,3}/.exec(response.data);
if (matched && matched.length) {
newIP = matched[0];
}
// ......
}).catch((reason) => {
console.error("Get public IPv4 error:", reason);
process.exit(1);
});

匹配IP地址的正则表达式的写法不止 /(\d{1,3}\.){3}\d{1,3}/ 这一种。 可以用更简化但匹配范围更广的写法,或用更严格的写法,只要达到目的即可。

查询域名解析记录中记录的 IP 地址

调用阿里云接口,将结果与当前公网 IP 进行对比。

1
2
3
4
5
6
7
client.request("DescribeDomainRecordInfo", describeDomainRecordInfoParams, requestOption).then((result) => {
var oldIP = result.Value;
// ......
}).catch((reason) => {
console.error("DescribeDomainRecordInfo error:", reason);
process.exit(1);
})

修改解析记录

如果当前公网 IP 发生了变化,那么调用接口修改解析记录。

1
2
3
4
5
6
7
8
9
10
11
12
if (oldIP != newIP) {
updateDomainRecordParams = {
...updateDomainRecordParams,
"Value": newIP
};
client.request("UpdateDomainRecord", updateDomainRecordParams, requestOption).then(() => {
process.exit(0);
}).catch((reason) => {
console.error("UpdateDomainRecord error:", reason);
process.exit(1);
});
}

修改成功可以做一个输出,也可以不要。

如果修改前后解析记录相同,则阿里云接口会返回一个字段 Message , 值为 The DNS record already exists 。但是对于整个过程没有什么影响,因此没有对此错误进行处理。 但是为了避免频繁调用 API ,还是应该在调用接口发现 IP 地址发生变化之后再调用修改解析记录的接口。

完整代码

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
const Core = require('@alicloud/pop-core');
const axios = require('axios').default;
const moment = require('moment');

var newIP = "127.0.0.1";

var client = new Core({
accessKeyId: '<accessKeyId>',
accessKeySecret: '<accessKeySecret>',
endpoint: 'https://alidns.aliyuncs.com',
apiVersion: '2015-01-09'
});

var describeDomainRecordInfoParams = {
"RecordId": "<RecordId>"
};

var updateDomainRecordParams = {
"RecordId": "<RecordId>",
"RR": "@",
"Type": "A"
}

var requestOption = {
method: 'POST'
}

axios.get("http://www.taobao.com/help/getip.php?t=" + moment().format("x")).then((response) => {
if (response.data) {
var matched = /(\d{1,3}\.){3}\d{1,3}/.exec(response.data);
if (matched && matched.length) {
newIP = matched[0];
}

client.request("DescribeDomainRecordInfo", describeDomainRecordInfoParams, requestOption).then((result) => {
var oldIP = result.Value;
if (oldIP != newIP) {
updateDomainRecordParams = {
...updateDomainRecordParams,
"Value": newIP
};
client.request("UpdateDomainRecord", updateDomainRecordParams, requestOption).then(() => {
process.exit(0);
}).catch((reason) => {
console.error("UpdateDomainRecord error:", reason);
process.exit(1);
});
}
}).catch((reason) => {
console.error("DescribeDomainRecordInfo error:", reason);
process.exit(1);
})
}
}).catch((reason) => {
console.error("Get public IPv4 error:", reason);
process.exit(1);
});

基于 Docker 服务化

在威联通等一些 NAS 系统上,系统本身提供了获取 IP 地址的功能,而且支持向 DDNS 服务器发送请求以更改 IP 地址。 一些路由器固件中已经内置了修改阿里云 DNS 解析记录的功能,但是威联通自带的 DDNS 并不支持直接修改阿里云 DNS。 但是威联通支持通过自定义 DDNS 请求,请求中可通过 %IP% 字符串代替当前 IP 地址,例如这样的一个请求:

1
https://ddns.ddd.qnap.net/?hostname=%HOST%&username=%USER%&password=%PASS%&IP=%IP%

因此我们可以将上述 DDNS 代码通过构建 Docker 镜像发布成服务,使威联通 NAS 通过该服务修改 DNS 解析记录。

服务接口实现

要实现一个网络服务接口,方法很多。因为对 Node.js 比较熟,笔者这里采用了 Express 服务器框架。 总体来说,只需要写一个路由即可。 阿里云 API 所需要的 ACCESSKEY_ID 和 ACCESSKEY_SECRET 通过环境变量进行设置。 为了支持多个域名的 DDNS 而不需要部署过多的 Docker 容器,RecordID、Type、RR、IP 等参数都从请求地址中获取。

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
// routes/ddns.js
const Core = require('@alicloud/pop-core');
const axios = require('axios').default;
const moment = require('moment');
const express = require("express");
var router = express.Router();

function updateDNS(ip, record, type, rr) {
var client = new Core({
accessKeyId: process.env.ACCESSKEY_ID,
accessKeySecret: process.env.ACCESSKEY_SECRET,
endpoint: 'https://alidns.aliyuncs.com',
apiVersion: '2015-01-09'
});
var describeDomainRecordInfoParams = {
"RecordId": record
};
var requestOption = {
method: "POST"
};
return new Promise((resolve, reject) => {
client.request("DescribeDomainRecordInfo", describeDomainRecordInfoParams, requestOption).then((result) => {
var oldIP = result.Value;
if (oldIP != ip) {
var updateDomainRecordParams = {
"RecordId": record,
"RR": rr,
"Type": type,
"Value": ip
};
client.request("UpdateDomainRecord", updateDomainRecordParams, requestOption).then(() => {
resolve("OK");
}).catch((reason) => {
reject("UpdateDomainRecord error: " + reason);
});
}
}).catch((reason) => {
reject("DescribeDomainRecordInfo error: " + reason);
});
});
}

router.get('/qnap', function (req, res) {
var query_keys = ["ip", "record", "type", "rr"];
var query_keys_check = query_keys.every(item => (item in req.query));
if (!query_keys_check) {
console.error("Missing:", query_keys.filter(item => !(item in req.query)).join(","));
res.sendStatus(500);
return;
}
console.log(req.query);
var newIP = req.query.ip;
var recordID = req.query.record;
var recordType = req.query.type;
var recordRR = req.query.rr;
updateDNS(newIP, recordID, recordType, recordRR).then((response) => {
console.log(response);
res.sendStatus(200);
}).catch((reason) => {
console.error(reason);
res.sendStatus(500);
});
})

module.exports = router;

当然这里也可以添加一个自动获取 IP 地址并更新 DNS 解析记录的接口。

实现服务器后,只需要写一个简单的 Dockerfile 即可。

1
2
3
4
5
6
7
8
FROM node:10

COPY . /srv
WORKDIR /srv
RUN npm install
EXPOSE 3000

CMD [ "node", "bin/www" ]

具体实现请参考 Github 仓库

感谢您的阅读,本文由 HPDell 的个人博客 版权所有。如若转载,请注明出处:HPDell 的个人博客(http://hpdell.github.io/编程/nodejs-ddns/
Common Connotations of Western Fantasy Movies
《爱乐之城》的“过度”解读