0. 简要介绍
0.1 思路说明
AliDDNSNet 是基于 .NET Core 开发的动态 DNS 解析工具, 借助于阿里云的 DNS API 来实现域名与动态 IP 的绑定功能. 工具核心就是调用了阿里云 DNS 的两个 API , 一个 API 获取指定域名的所有解析记录, 然后通过比对与当前公网 IP 是否一致, 一致则不进行更改, 不一致则通过另外一个修改 API 来修改指定子域名的修改记录.
0.2 使用说明
使用时请更改同目录下的
settings.json.example
为 settings.json 文件, 同时也可以显示通过 -f 参数来制定配置文件路径. 例如:
- dotnet ./AliDDNSNet.dll -f ./settings.json2
- ./AliDDNSNet -f ./settings.json3
NAS 运行效果图:
0.3. 配置说明
通过更改 settings.json/
settings.json.example
的内容来实现 DDNS 更新.
- {
- // 阿里云的 Access Id
- "access_id": "",
- // 阿里云的 Access Key
- "access_key": "",
- // TTL 时间
- "interval": 600,
- // 主域名
- "domain": "example.com",
- // 子域名前缀
- "sub_domain": "test",
- // 记录类型
- "type": "A"
- }
其中 Access Id 与 Access Key 可以登录阿里云之后在右上角可以得到.
1. 代码说明
1.1 主程序流程
主要流程代码在 Program.cs 文件当中编写, 这里依次讲解一下.
首先加载配置文件, 如果用户传入了 -f 参数, 则使用用户传入的配置文件路径, 否则的话直接使用当前目录的默认 settings.json 配置文件, 读取成功之后存放到 Utils.config 属性当中以便 Utils 使用.
- // 加载配置文件:
- var filePath = attachments.HasValue()
- ? attachments.Value()
- : $"{Environment.CurrentDirectory}{Path.DirectorySeparatorChar}settings.json";
- if (!File.Exists(filePath))
- {
- Console.WriteLine("当前目录没有配置文件, 或者配置文件位置不正确.");
- return -1;
- }
- var config = await Utils.ReadConfigFile(filePath);
- Utils.config = config;
之后通过
Utils.GetCurentPublicIP()
方法获取到当前设备的公网 IP, 再判断指定的二级域名解析是否存在, 如果不存在的话, 则直接返回, 这里并没有做新增解析操作, 后续版本可能会加上.
- // 获得当前 IP
- var currentIP = (await Utils.GetCurentPublicIP()).Replace("\n", "");
- var subDomains = JObject.Parse(await Utils.SendGetRequest(new DescribeDomainRecordsRequest(config.domain)));
- if (subDomains.SelectToken($"$.DomainRecords.Record[?(@.RR =='{config.sub_domain}')]") == null)
- {
- Console.WriteLine("指定的子域名不存在, 请新建一个子域名解析.");
- return 0;
- }
如果找到了对应二级域名的解析, 则输出当前解析的记录值, 然后进行比较, 如果当前主机的公网 IP 与记录值一样则无需进行变更.
- Console.WriteLine("已经找到对应的域名与解析");
- Console.WriteLine("======================");
- Console.WriteLine($"子域名:{config.sub_domain}{config.domain}");
- var dnsIp = subDomains.SelectToken($"$.DomainRecords.Record[?(@.RR =='{config.sub_domain}')].Value").Value<string>();
- Console.WriteLine($"目前的 A 记录解析 IP 地址:{dnsIp}");
- if (currentIP == dnsIp)
- {
- Console.WriteLine("解析地址与当前主机 IP 地址一致, 无需更改.");
- return 0;
- }
当阿里云 DNS 解析记录与当前主机公网 IP 不一致的时候调用更新 API, 传入之前的域名的 rrId 去进行变更, 完成即退出.
- Console.WriteLine("检测到 IP 地址不一致, 正在更改中......");
- var rrId = subDomains.SelectToken($"$.DomainRecords.Record[?(@.RR =='{config.sub_domain}')].RecordId").Value<string>();
- var response = await Utils.SendGetRequest(new UpdateDomainRecordRequest(rrId, config.sub_domain, config.type, currentIP, config.interval.ToString()));
- var resultRRId = JObject.Parse(response).SelectToken("$.RecordId").Value<string>();
- if (resultRRId == null || resultRRId != rrId)
- {
- Console.WriteLine("更改记录失败, 请稍后再试.");
- }
- else
- {
- Console.WriteLine("更改记录成功.");
- }
- return 0;
1.2 Utils 详解
Utils.cs 主要存放一些功能性方法, 比如说将 SortedDictionary 字典转为请求字符串, 还有就是加密方法, 请求方法等.
1.2.1 生成通用参数字典
因为 API 请求的时候有很多共有参数, 所以这里单独用了一个静态方法来生成这个公有请求参数的字典.
- /// <summary>
- /// 生成通用参数字典
- /// </summary>
- public static SortedDictionary<string, string> GenerateGenericParameters()
- {
- var dict = new SortedDictionary<string, string>(StringComparer.Ordinal)
- {
- {"Format", "json"},
- {"AccessKeyId", config.access_id},
- {"SignatureMethod", "HMAC-SHA1"},
- {"SignatureNonce", Guid.NewGuid().ToString()},
- {"Version", "2015-01-09"},
- {"SignatureVersion", "1.0"},
- {"Timestamp", DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")}
- };
- return dict;
- }
可以看到这里使用了
SortedDictionary<string,string>
来处理, 这是因为阿里云 API 必须要求按大小写敏感来排序请求参数, 所以这里直接使用了 ```SortedDictionary 来处理这种情况.
1.2.2 根据字典构建请求字符串
因为阿里云 DNS 的 API 基本上都是 GET 请求, 所以通过这个方法可以将之前的
SortedDictionary<string,string>
字典构建成请求字符串.
- /// <summary>
- /// 根据字典构建请求字符串
- /// </summary>
- /// <param name="parameters"> 参数字典 </param>
- /// <returns></returns>
- public static string BuildRequestString(this SortedDictionary<string, string> parameters)
- {
- var sb = new StringBuilder();
- foreach (var kvp in parameters)
- {
- sb.Append("&");
- sb.Append(HttpUtility.UrlEncode(kvp.Key));
- sb.Append("=");
- sb.Append(HttpUtility.UrlEncode(kvp.Value));
- }
- return sb.ToString().Substring(1);
- }
核心就是遍历这个字典, 通过 StringBuilder 来构建这个请求字符串.
1.2.3 生成请求签名
这一步也是最重要的一步, 因为阿里云所有的 API 接口都需要传递签名参数, 这个签名参数是根据你提交的参数集合 AccessKey 来进行计算的.
- /// <summary>
- /// 生成请求签名
- /// </summary>
- /// <param name="srcStr"> 请求体 </param>
- /// <returns>HMAC-SHA1 的 Base64 编码 </returns>
- public static string GenerateSignature(this string srcStr)
- {
- var signStr = $"GET&{HttpUtility.UrlEncode("/")}&{HttpUtility.UrlEncode(srcStr)}";
- // 替换已编码的 URL 字符为大写字符
- signStr = signStr.Replace("/", "/").Replace("=", "=").Replace("+", "+")
- .Replace("%3a", "%3A");
- var hmac = new HMACSHA1(Encoding.UTF8.GetBytes($"{config.access_key}&"));
- return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(signStr)));
- }
这里之前我是按照阿里云 API 来进行开发的, 不过有一点需要注意的是, 返回的 Signature 值是不需要进行 URL 编码的. 就因为这一点, 我白白浪费了 3 个小时来排查问题, 看看官方 API 文档说的:
说需要将签名值编码之后再提交, 扯淡, 如果编码之后再提交的话, 接口会一直返回:
Specified signature is not matched with our calculation.
这里直接返回 HMACSHA1 加密结果的 Base64 字符串即可.
1.2.4 发送请求
构建好一切之后我们就需要发送请求了, 这里统一是使用的 SendRequest() 方法来进行处理, 可以看到我们先获得签名, 然后将获取到的签名追加到请求体内部, 一起进行请求.
- /// <summary>
- /// 追加签名参数
- /// </summary>
- /// <param name="parameters"> 参数列表 </param>
- public static string AppendSignature(this SortedDictionary<string, string> parameters, string sign)
- {
- parameters.Add("Signature", sign);
- return parameters.BuildRequestString();
- }
- /// <summary>
- /// 对阿里云 API 发送 GET 请求
- /// </summary>
- public static async Task<string> SendGetRequest(IRequest request)
- {
- var sign = request.Parameters.BuildRequestString().GenerateSignature();
- var postUri = $"http://alidns.aliyuncs.com/?{request.Parameters.AppendSignature(sign)}";
- using (var client = new HttpClient())
- {
- using (var resuest = new HttpRequestMessage(HttpMethod.Get, postUri))
- {
- using (var response = await client.SendAsync(resuest))
- {
- return await response.Content.ReadAsStringAsync();
- }
- }
- }
- }
这里传入的 IRequest 接口, 是有具体实现的, 可以转到 Main 方法里面看一下:
- await Utils.SendGetRequest(new DescribeDomainRecordsRequest(config.domain));
- await Utils.SendGetRequest(new UpdateDomainRecordRequest(rrId, config.sub_domain, config.type, currentIP, config.interval.ToString()));
这里的
DescribeDomainRecordsRequest
与
UpdateDomainRecordRequest
就是具体的请求体, 定义很简单, 就是实现了 IRequest 接口而已, 然后在各自的内部添加一些特殊的参数.
1.3 异步 Main 方法
异步的 Main 方法需要 C# 7.1 以上版本才能支持, 你只需要右键你的项目选择属性, 左侧栏选择生成, 找到高级按钮, 更改当前 C# 语言版本即可.
效果如下:
- static async Task<int> Main(string[] args)
- {
- // 代码....
- return await Task.FromResult(0);
- }
1.4 好用的 CommandLine 库
编写控制台程序, 最主要的是接受参数然后处理, 而
Microsoft.Extensions.CommandLineUtils
库提供了方便快捷的方式来为我们处理用户输入的参数.
使用方法如下:
- using System;
- using McMaster.Extensions.CommandLineUtils;
- public class Program
- {
- public static int Main(string[] args)
- {
- var app = new CommandLineApplication();
- app.HelpOption();
- var optionSubject = app.Option("-s|--subject <SUBJECT>", "The subject", CommandOptionType.SingleValue);
- var optionRepeat = app.Option<int>("-n|--count <N>", "Repeat", CommandOptionType.SingleValue);
- // 启动时执行的委托
- app.OnExecute(() =>
- {
- // 接收参数
- var subject = optionSubject.HasValue()
- ? optionSubject.Value()
- : "world";
- var count = optionRepeat.HasValue() ? optionRepeat.ParsedValue : 1;
- for (var i = 0; i < count; i++)
- {
- Console.WriteLine($"Hello {subject}!");
- }
- // 执行完毕返回状态 0
- return 0;
- });
- // 真正启动控制台程序
- return app.Execute(args);
- }
- }
2.GITHUB 开源地址
https://github.com/GameBelial/AliDDNSNet
有兴趣的朋友可以 star 关注一下.
3. 二进制程序下载地址
程序打包了 Linux-x64 与 Linux arm 环境的二进制可执行文件, 你可以直接下载对应的压缩包解压到你的路由器或者 NAS 里面进行运行.
如果你的设备支持 Docker 环境, 建议通过 Docker 运行 .NET Core 2.1 环境来执行本程序.
下载地址在这儿 https://github.com/GameBelial/AliDDNSNet/releases
来源: https://www.cnblogs.com/myzony/p/9349578.html