家里有台服务器或者NAS的朋友肯定遇到过这样的问题:每次重启路由器或者重新拨号,公网IP地址就会变化。以前IPv4时代还能申请个固定IP,现在IPv4资源紧张,运营商基本不给个人用户分配固定IP了。好在现在三大运营商都普及了IPv6,每个设备都能获得公网IPv6地址,这给我们提供了新的解决方案。
但是IPv6地址又长又难记,比如"2408:8207:7890:abcd:1234:5678:9012:3456"这样的地址,根本没法直接用来访问家里的设备。这时候就需要DDNS(动态域名解析)服务了,它能把变动的IP地址绑定到一个固定的域名上。阿里云提供了完善的DNS解析服务,配合他们的SDK,我们可以自己写个工具实现自动化更新。
我去年给家里的NAS配置IPv6访问时就遇到了这个问题。当时找了一圈发现现成的工具要么收费,要么需要上传自己的API密钥到第三方服务器,总觉得不太安全。最后决定自己用阿里云SDK写个工具,实测下来效果很不错,现在分享给大家完整的实现方案。
首先需要有个阿里云账号,没有的话去官网注册一个。登录后进入控制台,找到"AccessKey管理"页面,创建一个新的AccessKey。这个Key相当于你的账号密码,一定要妥善保管,建议只给"读取和修改DNS解析记录"的权限。
我建议专门为这个DDNS工具创建一个子账号,权限控制更安全。具体操作:进入"访问控制RAM"服务,新建用户,勾选"编程访问",然后给这个用户添加"AliyunDNSFullAccess"权限策略。这样即使Key泄露,损失也能控制在DNS解析范围内。
在阿里云域名服务购买个便宜域名,比如.top/.xyz后缀的,一年也就十几块钱。买好后进入"域名解析DNS"控制台,添加一条AAAA记录(IPv6用AAAA,IPv4是A记录)。记录值可以先随便填个合法的IPv6地址,比如"2400::1"。
这里有个小技巧:主机记录可以设置成"home"或者"nas"这样的子域名,比如"home.yourdomain.com"。这样既方便记忆,又不影响主域名的正常使用。TTL建议设置短一点,比如10分钟,这样IP变更后能更快生效。
我们需要准备以下开发环境:
安装方法:在VS中右键项目→管理NuGet程序包→搜索并安装上述两个包。我实测过最新版本都能正常工作,如果遇到问题可以尝试降低版本号。
获取本机IPv6地址是整个程序的第一步,也是最容易出问题的地方。Windows系统一个网卡可能有多个IPv6地址,我们需要找到那个真正的公网地址。
csharp复制public static string GetIPv6Address(string mac)
{
var interfaces = NetworkInterface.GetAllNetworkInterfaces();
foreach (var adapter in interfaces)
{
// 匹配指定MAC地址的网卡
if (adapter.GetPhysicalAddress().ToString() == mac.Replace(":", "").Replace("-", ""))
{
var ipProps = adapter.GetIPProperties();
foreach (var ip in ipProps.UnicastAddresses)
{
// 筛选IPv6地址且不以fe80开头(fe80是本地链路地址)
if (ip.Address.AddressFamily == AddressFamily.InterNetworkV6
&& !ip.Address.ToString().StartsWith("fe80"))
{
return ip.Address.ToString();
}
}
}
}
throw new Exception("未找到有效的IPv6地址,请检查网卡MAC地址配置");
}
这个函数需要传入网卡的MAC地址作为参数。怎么查MAC地址呢?在cmd里输入"ipconfig /all",找到你正在使用的网卡,里面的"物理地址"就是MAC。注意要把冒号或横杠去掉,比如"00-1A-2B-3C-4D-5E"要写成"001A2B3C4D5E"。
拿到本地IPv6地址后,需要查询阿里云上当前的解析记录,看看是否需要更新。
csharp复制public static DescribeSubDomainRecordsResponse GetDNSRecord(string domain, string accessKeyId, string accessKeySecret)
{
// 初始化客户端
var profile = DefaultProfile.GetProfile("cn-hangzhou", accessKeyId, accessKeySecret);
var client = new DefaultAcsClient(profile);
// 构建查询请求
var request = new DescribeSubDomainRecordsRequest
{
SubDomain = domain,
Type = "AAAA" // 查询IPv6记录
};
try
{
return client.GetAcsResponse(request);
}
catch (Exception ex)
{
throw new Exception($"查询DNS记录失败: {ex.Message}");
}
}
这里有几个关键点:
如果发现本地IP和云端记录不一致,就需要调用更新接口:
csharp复制public static void UpdateDNSRecord(string recordId, string rr, string ip, string accessKeyId, string accessKeySecret)
{
var profile = DefaultProfile.GetProfile("cn-hangzhou", accessKeyId, accessKeySecret);
var client = new DefaultAcsClient(profile);
var request = new UpdateDomainRecordRequest
{
RecordId = recordId,
RR = rr,
Type = "AAAA",
Value = ip
};
try
{
var response = client.GetAcsResponse(request);
Console.WriteLine($"记录更新成功: {rr} → {ip}");
}
catch (Exception ex)
{
throw new Exception($"更新DNS记录失败: {ex.Message}");
}
}
RecordId是每条解析记录的唯一标识,RR是主机记录(比如"home"),这两个值都可以从查询结果中获取。更新操作是即时生效的,但受TTL影响,客户端缓存可能需要时间刷新。
把上面的功能组合起来,主程序的逻辑应该是这样的:
csharp复制static void Main(string[] args)
{
try
{
// 1. 读取配置
var config = LoadConfig("config.json");
// 2. 获取本机IPv6
string localIp = GetIPv6Address(config.MacAddress);
Console.WriteLine($"本机IPv6地址: {localIp}");
// 3. 查询云端记录
var records = GetDNSRecord(config.SubDomain, config.AccessKeyId, config.AccessKeySecret);
var record = records.DomainRecords.First(); // 取第一条记录
// 4. 比较并更新
if (record.Value != localIp)
{
UpdateDNSRecord(record.RecordId, record.RR, localIp,
config.AccessKeyId, config.AccessKeySecret);
}
else
{
Console.WriteLine("IP地址未变化,无需更新");
}
}
catch (Exception ex)
{
Console.WriteLine($"出错啦: {ex.Message}");
// 这里可以添加邮件/短信通知逻辑
}
}
建议使用JSON格式的配置文件,方便修改和维护:
json复制{
"SubDomain": "home.yourdomain.com",
"AccessKeyId": "你的AccessKey ID",
"AccessKeySecret": "你的AccessKey Secret",
"MacAddress": "001A2B3C4D5E"
}
读取配置文件的代码:
csharp复制public class Config
{
public string SubDomain { get; set; }
public string AccessKeyId { get; set; }
public string AccessKeySecret { get; set; }
public string MacAddress { get; set; }
}
public static Config LoadConfig(string path)
{
string json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<Config>(json);
}
好的日志能帮我们快速定位问题。推荐使用NLog这个库,配置简单功能强大。先在NuGet安装NLog和NLog.Config,然后添加nlog.config文件:
xml复制<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<targets>
<target name="logfile" xsi:type="File" fileName="logs/${shortdate}.log" />
<target name="console" xsi:type="Console" />
</targets>
<rules>
<logger name="*" minlevel="Info" writeTo="logfile,console" />
</rules>
</nlog>
在代码中使用:
csharp复制private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
// 记录信息
Logger.Info("程序启动");
// 记录错误
Logger.Error(ex, "更新DNS记录失败");
手动运行程序太麻烦,我们可以用Windows自带的计划任务实现自动化:
我建议先测试手动运行是否正常,再设置计划任务。任务创建后可以右键"运行"立即测试,查看日志确认是否正常工作。
虽然程序已经做了基本异常处理,但最好再加个通知功能,出问题时能及时知道。简单的方法是用SMTP发邮件:
csharp复制public static void SendAlertEmail(string error)
{
var client = new SmtpClient("smtp.163.com", 25)
{
Credentials = new NetworkCredential("yourmail@163.com", "password"),
EnableSsl = true
};
client.Send("yourmail@163.com", "admin@yourdomain.com",
"DDNS更新出错啦!",
$"错误信息:{error}\n时间:{DateTime.Now}");
}
然后在catch块中调用这个方法。更高级的做法可以接入钉钉机器人或者企业微信通知。
可能原因:
错误信息通常包含"InvalidAccessKeyId"或"Forbidden":
可能原因:
可能原因:
我建议把程序放在内网服务器上运行,而不是暴露在公网。如果必须放在有公网访问的设备上,至少要设置好防火墙规则,限制访问来源。