本文介绍一种基于 IPv6 + Cloudflare API 的自建 DDNS 方案。
在没有公网 IPv4 的家庭宽带环境下,通过 IPv6 公网地址,实现 PVE 虚拟机中 Windows 11 的稳定远程访问。
一、为什么需要 IPv6 DDNS?
近年来,越来越多家庭宽带开始提供公网 IPv6。
相比 IPv4:
IPv6 地址数量充足;
不需要 NAT 端口映射;
可以直接访问家庭网络设备。
但是家庭宽带 IPv6 通常不是永久固定地址。
二、环境说明
| 项目 | 说明 |
|---|---|
| 虚拟化平台 | PVE |
| 虚拟机 | Windows 11 |
| 虚拟网卡 | VirtIO |
| 网络 | 家庭 IPv6 |
| DNS服务 | Cloudflare |
| DDNS程序 | PowerShell |
| 通知 | Telegram Bot |
三、DDNS工作原理
flowchart LR
A[Windows 11虚拟机]
-->B[获取当前公网IPv6]
B
-->C[查询Cloudflare AAAA记录]
C
-->D{IPv6是否变化}
D
--否-->
E[结束]
D
--是-->
F[调用Cloudflare API]
F
-->G[更新AAAA记录]
G
-->H[Telegram通知]Windows 11虚拟机每10分钟检查一次当前IPv6,如果发生变化,通过Cloudflare API更新AAAA记录。
四、准备 Cloudflare API Token
Parse error on line 1: ^ Expecting 'NEWLINE', 'SPACE', 'GRAPH', got 'EOF'
Cloudflare 控制台→用户中心→API Tokens→Create Token
⚠️ 注意:API Token 等同于密码,不要发布到博客,不要上传 GitHub。
五、创建 Cloudflare AAAA记录
进入 Cloudflare→DNS→添加记录
| 类型 | 内容 |
|---|---|
| AAAA | IPv6地址 |
| 名称 | rdp |
| 代理状态 | DNS only |
不要开启:Proxied
原因:Cloudflare CDN 不代理 RDP 等非 HTTP 服务。
六、Windows IPv6稳定地址设置
Windows 默认开启 IPv6 隐私地址:
例如:临时IPv6:2001:db8::abcd
这个地址可能变化。
DDNS 不应该使用临时地址。
关闭IPv6临时地址
管理员 PowerShell:
netsh interface ipv6 set privacy state=disabled
禁止IPv6随机接口标识
执行:
netsh interface ipv6 set global randomizeidentifiers=disabled store=active
netsh interface ipv6 set global randomizeidentifiers=disabled store=persistent
查看:
ipconfig
正常:
IPv6 地址:
2001:db8:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx
不再出现:
临时 IPv6 地址
七、创建DDNS目录结构
Windows 创建:
C:\RDP_DDNS
最终:
C:\RDP_DDNS
│
├── cloudflare-ddns.ps1
│
├── config.ps1
│
└── cloudflare_ddns.log
说明:
| 文件 | 作用 |
| cloudflare-ddns.ps1 | 主程序 |
| config.ps1 | 配置文件 |
| cloudflare_ddns.log | 运行日志 |
八、配置文件
创建:
C:\RDP_DDNS\config.ps1
# Cloudflare API Token
$apiToken="YOUR_API_TOKEN"
# Cloudflare Zone ID
$zoneId="YOUR_ZONE_ID"
# AAAA记录
$recordName="rdp.example.com"
# 日志
$logFilePath="C:\RDP_DDNS\cloudflare_ddns.log"
# Telegram通知
$telegramEnable=$true
$telegramBotToken="YOUR_TELEGRAM_BOT_TOKEN"
$telegramChatId="YOUR_CHAT_ID"
九、PowerShell执行权限
管理员 PowerShell:
Set-ExecutionPolicy RemoteSigned
查看:
Get-ExecutionPolicy
结果:
RemoteSigned
十、运行测试
执行:
C:\RDP_DDNS\cloudflare-ddns.ps1
成功:
开始检测IPv6
当前IPv6:
2001:db8::xxxx
Cloudflare当前IPv6:
2001:db8::xxxx
IPv6没有变化,无需更新
执行完成
IPv6变化:
IPv6变化
旧:
2001:db8:old::1
新:
2001:db8:new::1
Cloudflare更新成功
十一、cloudflare-ddns.ps1 完整代码
点击展开 cloudflare-ddns.ps1 完整代码
# =====================================================
# Cloudflare IPv6 DDNS
# Windows 11 + PVE VirtIO optimized
#
# Features:
# IPv6 stable address detection
# Cloudflare API update
# Telegram notification
# Log rotation
#
# PowerShell 5.1 compatible
# =====================================================
# 设置控制台编码
[Console]::OutputEncoding =
[System.Text.Encoding]::UTF8
# 配置文件
$config="C:\RDP_DDNS\config.ps1"
if(!(Test-Path $config))
{
Write-Host "配置文件不存在: $config"
exit
}
# 加载配置
. $config
# =====================================================
# 日志
# =====================================================
function Write-Log
{
param(
[string]$Message
)
$line =
"[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message"
Write-Host $line
try
{
Add-Content `
-Path $logFilePath `
-Value $line `
-Encoding UTF8
}
catch
{
}
}
# =====================================================
# 日志轮换
# =====================================================
if(Test-Path $logFilePath)
{
if((Get-Item $logFilePath).Length -gt 5MB)
{
Rename-Item `
-Path $logFilePath `
-NewName "$logFilePath.old" `
-Force
}
}
# =====================================================
# 获取公网 IPv6
# =====================================================
function Get-PublicIPv6
{
Write-Log "开始检测IPv6"
$ips =
Get-NetIPAddress `
-AddressFamily IPv6 `
-AddressState Preferred
# -------------------------------------------------
# 优先寻找稳定IPv6地址
#
# Windows关闭隐私地址后
# VirtIO网卡生成稳定接口ID
#
# 如果你的地址后缀固定:
#
# xxxx:xxxx:xxxx
#
# 可以填写到这里
# -------------------------------------------------
$stable =
$ips |
Where-Object {
$_.IPAddress -match "^[23]" `
-and $_.IPAddress -notlike "fe80::*" `
-and $_.IPAddress -notlike "fc00::*" `
-and $_.IPAddress -notlike "fd00::*"
} |
Select-Object -First 1
if($stable)
{
Write-Log "使用稳定公网IPv6"
return $stable.IPAddress
}
# -------------------------------------------------
# 备用方案
# -------------------------------------------------
try
{
$ip =
Invoke-RestMethod `
-Uri "https://api6.ipify.org" `
-TimeoutSec 10
if($ip -match ":")
{
Write-Log "使用IPv6 API"
return $ip.Trim()
}
}
catch
{
}
return $null
}
# =====================================================
# 获取IPv6
# =====================================================
$ipv6 =
Get-PublicIPv6
if(!$ipv6)
{
Write-Log "错误:没有公网IPv6"
exit
}
Write-Log "当前IPv6: $ipv6"
# =====================================================
# Cloudflare API
# =====================================================
$headers=@{
Authorization=
"Bearer $apiToken"
"Content-Type"=
"application/json"
}
$nameEncoded =
[uri]::EscapeDataString($recordName)
$getUrl =
"https://api.cloudflare.com/client/v4/zones/" +
$zoneId +
"/dns_records?type=AAAA&name=" +
$nameEncoded
try
{
$result =
Invoke-RestMethod `
-Uri $getUrl `
-Headers $headers `
-Method Get `
-TimeoutSec 15
if($result.result.Count -ne 1)
{
throw "AAAA记录数量异常"
}
}
catch
{
Write-Log "获取DNS失败: $($_.Exception.Message)"
exit
}
$record =
$result.result[0]
$recordId =
$record.id
$oldIP =
$record.content
Write-Log "Cloudflare当前IPv6: $oldIP"
# =====================================================
# 判断是否变化
# =====================================================
if($oldIP -eq $ipv6)
{
Write-Log "IPv6没有变化,无需更新"
exit
}
Write-Log "IPv6发生变化"
Write-Log "旧: $oldIP"
Write-Log "新: $ipv6"
# =====================================================
# 更新Cloudflare
# =====================================================
$updateUrl =
"https://api.cloudflare.com/client/v4/zones/" +
$zoneId +
"/dns_records/" +
$recordId
$body =
@{
type="AAAA"
name=$recordName
content=$ipv6
ttl=1
proxied=$false
} |
ConvertTo-Json
try
{
$update =
Invoke-RestMethod `
-Uri $updateUrl `
-Headers $headers `
-Method Put `
-Body $body `
-TimeoutSec 20
if($update.success)
{
Write-Log "Cloudflare更新成功"
$status="成功"
}
else
{
Write-Log "Cloudflare更新失败"
$status="失败"
}
}
catch
{
Write-Log "更新异常: $($_.Exception.Message)"
$status="异常"
}
# =====================================================
# Telegram通知
# =====================================================
if($telegramEnable -eq $true)
{
$msg =
"Cloudflare IPv6 DDNS更新`n" +
"状态: $status`n" +
"域名: $recordName`n" +
"旧IP: $oldIP`n" +
"新IP: $ipv6`n" +
"时间: $(Get-Date)"
try
{
$null =
Invoke-RestMethod `
-Uri "https://api.telegram.org/bot$telegramBotToken/sendMessage" `
-Method Post `
-Body @{
chat_id=$telegramChatId
text=$msg
}
}
catch
{
}
}
Write-Log "执行完成"
十二、代码说明
1.为什么分成两个文件?
采用:
C:\RDP_DDNS
├── cloudflare-ddns.ps1
└── config.ps1
结构。
原因:
主程序公开
cloudflare-ddns.ps1
可以上传博客。
配置文件私有
config.ps1
保存:
API Token
Zone ID
域名
Telegram信息
避免泄露。
2.日志示例
正常:
[2026-01-01 12:00:01] 开始检测IPv6
[2026-01-01 12:00:01]
当前IPv6:
2001:db8::1234
[2026-01-01 12:00:01]
IPv6没有变化,无需更新
变化:
[2026-01-01 18:00:01]
IPv6发生变化
旧:
2001:db8::1111
新:
2001:db8::2222
Cloudflare更新成功
十三、设置 Windows 任务计划自动运行 DDNS
前面已经完成脚本测试,但是如果希望长期稳定运行,需要让 Windows 自动执行。
Windows 自带的「任务计划程序」即可满足需求。
- 打开任务计划程序
按:
Win + R
输入:
taskschd.msc
打开:
任务计划程序
- 创建任务
右侧:
创建任务
不要选择:
创建基本任务
因为高级选项较少。
- 常规设置
名称:
Cloudflare IPv6 DDNS
建议勾选:
使用最高权限运行
原因:
脚本需要读取网络接口信息。
选择:
不管用户是否登录都要运行
十四、设置启动执行
切换:
触发器
点击:
新建
选择:
开始任务:
启动时建议:设置延迟:1分钟
原因:Windows启动流程:
Windows启动→VirtIO虚拟网卡加载→IPv6 RA获取地址→执行DDNS
如果立即运行,可能出现:没有公网IPv6

Comments NOTHING