AliDDNSNet 是基於 .NET Core 開發的動態 DNS 解析工具,藉助於阿里雲的 DNS API 來實現域名與動態 IP 的綁定功能。工具核心就是調用了阿里雲 DNS 的兩個 API ,一個 API 獲取指定域名的全部解析記錄,而後經過比對與當前公網 IP 是否一致,一致則不進行更改,不一致則經過另一個修改 API 來修改指定子域名的修改記錄。git
使用時請更改同目錄下的 settings.json.example
爲 settings.json
文件,同時也能夠顯示經過 -f
參數來制定配置文件路徑。例如:github
dotnet ./AliDDNSNet.dll -f ./settings.json2
./AliDDNSNet -f ./settings.json3
NAS 運行效果圖:shell
經過更改 settings.json
/settings.json.example
的內容來實現 DDNS 更新。json
{ // 阿里雲的 Access Id "access_id": "", // 阿里雲的 Access Key "access_key": "", // TTL 時間 "interval": 600, // 主域名 "domain": "example.com", // 子域名前綴 "sub_domain": "test", // 記錄類型 "type": "A" }
其中 Access Id 與 Access Key 能夠登陸阿里雲以後在右上角能夠獲得。app
主要流程代碼在 Program.cs 文件當中編寫,這裏依次講解一下。dom
首先加載配置文件,若是用戶傳入了 -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,再判斷指定的二級域名解析是否存在,若是不存在的話,則直接返回,這裏並無作新增解析操做,後續版本可能會加上。async
// 得到當前 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 去進行變動,完成即退出。post
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;
Utils.cs 主要存放一些功能性方法,好比說將 SortedDictionary
字典轉爲請求字符串,還有就是加密方法,請求方法等。
由於 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
來處理這種狀況。
由於阿里雲 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
來構建這個請求字符串。
這一步也是最重要的一步,由於阿里雲全部的 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 字符串便可。
構建好一切以後咱們就須要發送請求了,這裏統一是使用的 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
接口而已,而後在各自的內部添加一些特殊的參數。
異步的 Main 方法須要 C# 7.1 以上版本才能支持,你只須要右鍵你的項目選擇屬性,左側欄選擇生成,找到高級按鈕,更改當前 C# 語言版本便可。
效果以下:
static async Task<int> Main(string[] args) { // 代碼.... return await Task.FromResult(0); }
編寫控制檯程序,最主要的是接受參數而後處理,而 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); } }
https://github.com/GameBelial/AliDDNSNet
有興趣的朋友能夠 star 關注一下。
程序打包了 Linux-x64 與 Linux arm 環境的二進制可執行文件,你能夠直接下載對應的壓縮包解壓到你的路由器或者 NAS 裏面進行運行。
若是你的設備支持 Docker 環境,建議經過 Docker 運行 .NET Core 2.1 環境來執行本程序。