家用宽带申请公网 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 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 仓库 。