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


0.簡要介紹

0.1 思路說明

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

0.2 使用說明

使用時請更改同目錄下的 settings.json.examplesettings.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()));

這里的 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 環境來執行本程序。

下載地址在這兒


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM