基於阿里雲 DNS API 實現的 DDNS 工具

0.簡要介紹

0.1 思路說明

AliDDNSNet 是基於 .NET Core 開發的動態 DNS 解析工具,藉助於阿里雲的 DNS API 來實現域名與動態 IP 的綁定功能。工具核心就是調用了阿里雲 DNS 的兩個 API ,一個 API 獲取指定域名的全部解析記錄,而後經過比對與當前公網 IP 是否一致,一致則不進行更改,不一致則經過另一個修改 API 來修改指定子域名的修改記錄。git

0.2 使用說明

使用時請更改同目錄下的 settings.json.examplesettings.json 文件,同時也能夠顯示經過 -f 參數來制定配置文件路徑。例如:github

dotnet ./AliDDNSNet.dll -f ./settings.json2
./AliDDNSNet -f ./settings.json3

NAS 運行效果圖:shell

0.3.配置說明

經過更改 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

1.代碼說明

1.1 主程序流程

主要流程代碼在 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;

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()));

這裏的 DescribeDomainRecordsRequestUpdateDomainRecordRequest 就是具體的請求體,定義很簡單,就是實現了 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 環境來執行本程序。

下載地址在這兒

相關文章
相關標籤/搜索