Docker搭建阿里云DDNS
原创2023年7月13日大约 4 分钟
在NAS自动同步DDNS上使用的是chenhw2/aliyun-ddns-cli:latest,使用的是Go。
作为一名普通的C#开发者,有必要自己来实现,以及顺便学习下Docker,原代码地址。
已升级到.NET 8,AOT对Web开发很好,本机AOT限制很多....
资料
docker-compose.yml
services:
neverland.aliyun.ddns:
image: ${DOCKER_REGISTRY-}ali.ddns:1.0.0
build:
context: .
dockerfile: Dockerfile
docker-compose.override.yml
services:
neverland.aliyun.ddns:
container_name: ALIYUN-DDNS
networks:
- product-network
environment:
- ALIKID=
- ALIKSCT=
- ALIDOMAIN=ilyl.life
- ALITTL=600
networks:
product-network:
driver: bridge
Dockerfile
提示
最简单的方式是使用VS2022,添加Docker支持,自动生成
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base
USER app
WORKDIR /app
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
# Install clang/zlib1g-dev dependencies for publishing to native
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
clang zlib1g-dev
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY ["neverland.aliyun.ddns.csproj", "."]
RUN dotnet restore "./././neverland.aliyun.ddns.csproj"
COPY . .
WORKDIR "/src/."
RUN dotnet build "./neverland.aliyun.ddns.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "./neverland.aliyun.ddns.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=true
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0 AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["./neverland.aliyun.ddns"]
LABEL MAINTAINER=乌龙茶有点甜<ly2@ilyl.life>
ENV ALIKID= \
ALIKSCT= \
ALIDOMAIN=ilyl.life \
ALITTL=600
公网IP
有很多种获取外网IP的方式,这里选用ip-api,个人够用。
利用http://ip-api.com/json/?lang=zh-CN&fields=status,query
获取到公网地址。
注意
.NET 8 开启AOT时,ReadFromJsonAsync
方法警告,JSON序列化受限
因此需要禁用
using (var client = new HttpClient())
{
client.BaseAddress = new Uri(Contracts.QUERY_IPADDRESS_URL);
//不使用这个是因为有使用限制,https://ip-api.com/docs/api:json
//var query = await client.GetFromJsonAsync<IPModelResult>(Contracts.QUERY_IPADDRESS_RESOURCE, cancelllationToken)
// .ConfigureAwait(false);
try
{
using var response = await client.GetAsync(Contracts.QUERY_IPADDRESS_RESOURCE, cancelllationToken)
.ConfigureAwait(false);
response.EnsureSuccessStatusCode().WriteRequestToConsole();
//检查受限情况
var ri = response.Headers.FirstOrDefault(it => it.Key == Contracts.QUERY_IPADDRESS_HEADER_RI).Value;
if (ri != null)
{
if (ri.ElementAt(0) == "0")
{
var ttl = response.Headers.FirstOrDefault(it => it.Key == Contracts.QUERY_IPADDRESS_HEADER_TTL).Value;
Console.WriteLine($"{Contracts.TITLE}ip地址查询受限,等待{ttl.ElementAt(0)}秒后重试");
return string.Empty;
}
}
var jsonResponse = await response.Content.ReadFromJsonAsync<IPModelResult>();
if (jsonResponse != null)
{
if (jsonResponse.Status != null && jsonResponse.Status == "success") {
var networkIp = jsonResponse.Query!;
Console.WriteLine($"公网IP:{networkIp}");
return networkIp;
}
}
}
catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException tex)
{
Console.WriteLine($"Timed out: {ex.Message}, {tex.Message}");
}
catch (HttpRequestException ex) when (ex is { StatusCode: HttpStatusCode.NotFound })
{
Console.WriteLine($"Not found: {ex.Message}");
}
catch (HttpRequestException ex) when (ex is { StatusCode: null })
{
Console.WriteLine($"网络连接失败: {ex.Message}");
}
}
return string.Empty;
环境变量
使用Environment.GetEnvironmentVariable获取环境变量
一是因为账号安全问题
二是Docker运行配置
提示
- 项目
属性
,调试
,打开调试启动配置文件UI
- 通过Environment.SetEnvironmentVariable配合
#if DEBUG
条件编译设置环境变量
云解析
客户端
Client CreateClient(string accessKey, string accessKeySecret)
{
Config config = new Config
{
// 必填,您的 AccessKey ID
AccessKeyId = accessKey,
// 必填,您的 AccessKey Secret
AccessKeySecret = accessKeySecret,
};
// Endpoint 请参考 https://api.aliyun.com/product/Alidns
config.Endpoint = Contracts.DEFAULT_ALIBABA_ENDPOINT;
return new Client(config);
}
查询云解析记录
DescribeDomainRecordsResponse? QueryDns(Client client, string domain = Contracts.DEBUG_ALIBABA_DOMAIN, string type = Contracts.DEFAULT_ALIBABA_REQUEST_TYPE_4)
{
DescribeDomainRecordsRequest describeDomainRecordsRequest = new DescribeDomainRecordsRequest()
{
DomainName = domain,
Type = type
};
RuntimeOptions runtime = new RuntimeOptions();
try
{
// 复制代码运行请自行打印 API 的返回值
var response = client.DescribeDomainRecordsWithOptions(describeDomainRecordsRequest, runtime);
return response;
}
catch (TeaException error)
{
// 如有需要,请打印 error
var msg = Common.AssertAsString(error.Message);
Console.WriteLine($"{Contracts.TITLE}查询云解析失败,{msg}");
}
catch (Exception _error)
{
TeaException error = new TeaException(new Dictionary<string, object>
{
{ "message", _error.Message }
});
// 如有需要,请打印 error
var msg = Common.AssertAsString(error.Message);
Console.WriteLine($"{Contracts.TITLE}查询云解析失败,{msg}");
}
return null;
}
新增云解析记录
AddDomainRecordResponse? AddDns(Client client, string newIp, string domain = Contracts.DEBUG_ALIBABA_DOMAIN, int ttl = Contracts.DEFAULT_ALIBABA_REQUEST_TTL)
{
//参数
AddDomainRecordRequest addDomainRecordRequest = new AddDomainRecordRequest()
{
DomainName = domain,//域名名称
RR = Contracts.DEFAULT_ALIBABA_REQUEST_RR,//主机记录
Type = Contracts.DEFAULT_ALIBABA_REQUEST_TYPE_4,//解析记录类型
Value = newIp,//记录值
TTL = ttl
};
//运行时高级配置
RuntimeOptions runtime = new RuntimeOptions();
try
{
// 复制代码运行请自行打印 API 的返回值
var response = client.AddDomainRecordWithOptions(addDomainRecordRequest, runtime);
return response;
}
catch (TeaException error)
{
// 如有需要,请打印 error
var msg = Common.AssertAsString(error.Message);
Console.WriteLine($"{Contracts.TITLE}新增云解析失败,{msg}");
}
catch (Exception _error)
{
TeaException error = new TeaException(new Dictionary<string, object>
{
{ "message", _error.Message }
});
// 如有需要,请打印 error
var msg = Common.AssertAsString(error.Message);
Console.WriteLine($"{Contracts.TITLE}新增云解析失败,{msg}");
}
return null;
}
更新云解析记录
UpdateDomainRecordResponse? UpdateDns(Client client, string recordId, string newIp, string RR = Contracts.DEFAULT_ALIBABA_REQUEST_RR, string type = Contracts.DEFAULT_ALIBABA_REQUEST_TYPE_4)
{
UpdateDomainRecordRequest updateDomainRecordRequest = new UpdateDomainRecordRequest()
{
RecordId = recordId,
RR = RR,
Type = type,
Value = newIp,
};
RuntimeOptions runtime = new RuntimeOptions();
try
{
// 复制代码运行请自行打印 API 的返回值
var response = client.UpdateDomainRecordWithOptions(updateDomainRecordRequest, runtime);
return response;
}
catch (TeaException error)
{
// 如有需要,请打印 error
var msg = Common.AssertAsString(error.Message);
Console.WriteLine($"{Contracts.TITLE}修改云解析失败,{msg}");
}
catch (Exception _error)
{
TeaException error = new TeaException(new Dictionary<string, object>
{
{ "message", _error.Message }
});
// 如有需要,请打印 error
var msg = Common.AssertAsString(error.Message);
Console.WriteLine($"{Contracts.TITLE}修改云解析失败,{msg}");
}
return null;
}