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("%2f", "%2F").Replace("%3d", "%3D").Replace("%2b", "%2B")
.Replace("%253a", "%253A");
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 環境來執行本程序。
