本文介绍一种基于 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虚拟机获取当前公网IPv6查询Cloudflare AAAA记录IPv6是否变化结束调用Cloudflare API更新AAAA记录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→添加记录

类型内容
AAAAIPv6地址
名称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 自带的「任务计划程序」即可满足需求。

  1. 打开任务计划程序

按:

Win + R

输入:

taskschd.msc

打开:

任务计划程序

  1. 创建任务

右侧:

创建任务

不要选择:

创建基本任务

因为高级选项较少。

  1. 常规设置

名称:

Cloudflare IPv6 DDNS

建议勾选:

使用最高权限运行

原因:

脚本需要读取网络接口信息。

选择:

不管用户是否登录都要运行

十四、设置启动执行

切换:

触发器

点击:

新建

选择:

开始任务:

启动时建议:设置延迟:1分钟

原因:Windows启动流程:

Windows启动→VirtIO虚拟网卡加载→IPv6 RA获取地址→执行DDNS

如果立即运行,可能出现:没有公网IPv6

此作者没有提供个人介绍。
最后更新于 2026-07-11