SYSTEM UPLINK // DYNAMIC DNS PROTOCOL

CLOUDFLARE IPv6 DDNS

▸ WIN11 · NSSM · POWERSHELL · AAAA RECORD UPDATER ◂
Your ISP hands out a new IPv6 every reboot like free candy. Your server disappears into the void. This script pulls it back from oblivion — automatically, silently, every time you dare to restart. You're basically duct-taping your identity onto the internet and hoping nobody notices.

00 Overview

Your Windows 11 machine has a public IPv6 address — great. The problem is your ISP changes it whenever it feels like it (which is often, and always at the worst possible moment, like a cat knocking things off shelves at 3 AM). This setup keeps your Cloudflare DNS record synchronized automatically, so potatosips.yourdomain.win always resolves to wherever your machine actually is in the IPv6 universe.

WIN11 PC
IPv6 changes
PS1 SCRIPT
detects IP
CLOUDFLARE API
AAAA record
DNS RESOLVED
world sees you
Two files live in C:\Potatosips_DDNS_Cloudflare\ and a Windows service (via NSSM) runs the updater script on every system start. No scheduler needed. No crying required.

01 Prerequisites

COMPONENTREQUIREMENTNOTES
OS Windows 11 PowerShell 5.1+ included
IPv6 Public IPv6 on this machine Check via ipconfig — look for a non-fe80:: address
NSSM Placed at C:\tools\nssm.exe nssm.cc/download
Domain Managed by Cloudflare NS records must point to Cloudflare
Cloudflare acct Free tier is fine You need API token access
No public IPv6? This whole setup is useless to you. Check with your ISP. Some of them are still living in 2003 and serve IPv4 only. Sympathies.

02 Cloudflare Setup

▸ Get Your Zone ID

  1. Log into dash.cloudflare.com
  2. Click your domain (e.g. yourdomain.win)
  3. On the Overview page, scroll the right sidebar
  4. Copy the Zone ID — it looks like 08bd92043d60dfb6a705add7fad4c147

▸ Create an API Token

  1. Go to My Profile → API Tokens → Create Token
  2. Choose Create Custom Token
  3. Set Permission: Zone › DNS › Edit
  4. Zone Resource: Include → Specific zone → your domain
  5. Click Continue to summary → Create Token
  6. Copy the token immediately — Cloudflare will never show it again. Classic.
Lost your API token? Cloudflare can't retrieve it either. Just delete it and make a new one. Much like your dignity after spending 4 hours debugging DNS, some things simply cannot be recovered.

▸ Add the AAAA Record

  1. In your domain → DNS → Records → Add record
  2. Type: AAAA
  3. Name: potatosips (or whatever subdomain you want)
  4. IPv6 address: any valid address for now — the script will overwrite it
  5. Proxy status: DNS only (grey cloud ☁) — orange proxy breaks IPv6
  6. TTL: 2 min (120 seconds) — so changes propagate fast
  7. Save it
Proxy must be OFF (grey cloud). Cloudflare's proxy doesn't support IPv6 passthrough properly. Orange cloud = your IPv6 vanishes into Cloudflare's data centers. Grey cloud = pure DNS. Use grey.

03 Config File

Create this file first. It holds all your credentials and settings.

C:\Potatosips_DDNS_Cloudflare\potatosips-update-cloudflare-dns_conf.ps1
potatosips-update-cloudflare-dns_conf.ps1 // POWERSHELL
##### Config

## Which IP should be used: internal or external
$what_ip = "external"

## AAAA DNS record name (full FQDN)
$dns_record = "potatosips.yourdomain.win"

## Cloudflare Zone ID (Overview sidebar)
$zoneid = "YOUR_ZONE_ID_HERE"

## Cloudflare API Token (Zone › DNS › Edit)
$cloudflare_zone_api_token = "YOUR_API_TOKEN_HERE"

## Proxy: $false = grey cloud (required for IPv6), $true = orange cloud (breaks IPv6)
$proxied = $false

## TTL in seconds: 120-7200, or 1 for Auto
$ttl = 120
VARIABLEVALUEDESCRIPTION
$what_ip"external"Use your public IPv6 (recommended). Use "internal" for LAN-only setups.
$dns_record"sub.yourdomain.win"Full FQDN of the AAAA record to update
$zoneid32-char hex stringFrom Cloudflare Overview sidebar
$cloudflare_zone_api_tokenYour API tokenKeep this secret. Seriously.
$proxied$falseMust be false for IPv6. Orange cloud = sadness.
$ttl120Seconds until DNS refresh. Lower = faster propagation.

04 Main Script

This is the script that does the actual work. It reads the config, detects your IPv6, compares it to what Cloudflare has, and updates only if something changed. Efficient. Ruthless. Like a good soldier who never sends unnecessary API calls.

C:\Potatosips_DDNS_Cloudflare\potatosips-update-cloudflare-dns-ipv6-only.ps1
potatosips-update-cloudflare-dns-ipv6-only.ps1 // POWERSHELL
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

try {
    . "$PSScriptRoot\potatosips-update-cloudflare-dns_conf.ps1"
} catch {
    Write-Error "Missing or invalid config file"
    exit 1
}

# Validate TTL
if (($ttl -lt 120) -or (($ttl -gt 7200) -and ($ttl -ne 1))) {
    Write-Error 'ttl must be 120-7200, or 1 for Auto'
    exit 1
}

# Validate proxied flag
if (!([string]$proxied) -or ($proxied.GetType().Name.Trim() -ne 'Boolean')) {
    Write-Error 'proxied must be $true or $false'
    exit 1
}

# Validate what_ip
if (($what_ip -ne 'external') -and ($what_ip -ne 'internal')) {
    Write-Error 'what_ip must be "external" or "internal"'
    exit 1
}

if (($what_ip -eq 'internal') -and $proxied) {
    Write-Error 'Internal IP cannot be proxied'
    exit 1
}

# Test if a string is a valid IPv6 address
function Test-IPv6Address {
    param([string]$Address)
    $out = [System.Net.IPAddress]::None
    if (-not [System.Net.IPAddress]::TryParse($Address, [ref]$out)) { return $false }
    return ($out.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6)
}

# Get external IPv6 from public providers
function Get-ExternalIPv6 {
    $ipv6Providers = @(
        'https://api64.ipify.org',
        'https://ifconfig.co/ip',
        'https://icanhazip.com'
    )
    foreach ($provider in $ipv6Providers) {
        try {
            $candidate = (Invoke-RestMethod -Uri $provider -TimeoutSec 10 -Headers @{ Accept = 'text/plain' }).Trim()
            if (Test-IPv6Address $candidate) { return $candidate }
        } catch { continue }
    }
    return $null
}

# Get internal IPv6 via routing table
function Get-InternalIPv6 {
    try {
        $route = Find-NetRoute -RemoteIPAddress '2606:4700:4700::1111' -ErrorAction Stop |
            Sort-Object RouteMetric, InterfaceMetric | Select-Object -First 1
        $ip = Get-NetIPAddress -InterfaceIndex $route.InterfaceIndex -AddressFamily IPv6 |
            Where-Object {
                $_.IPAddress -notlike 'fe80:*' -and
                $_.IPAddress -ne '::1' -and
                $_.PrefixOrigin -ne 'WellKnown'
            } |
            Sort-Object SkipAsSource, PrefixOrigin |
            Select-Object -First 1 -ExpandProperty IPAddress
        if (Test-IPv6Address $ip) { return $ip }
    } catch { return $null }
    return $null
}

# Resolve current IP
if ($what_ip -eq 'external') {
    $ip = Get-ExternalIPv6
    if (-not $ip) { Write-Error 'Could not determine external IPv6'; exit 1 }
} else {
    $ip = Get-InternalIPv6
    if (-not $ip) { Write-Error 'Could not determine internal IPv6'; exit 1 }
}

$headers = @{
    Authorization = "Bearer $cloudflare_zone_api_token"
    'Content-Type' = 'application/json'
}

# Query Cloudflare for current AAAA record
try {
    $response = Invoke-RestMethod `
        -Uri "https://api.cloudflare.com/client/v4/zones/$zoneid/dns_records?type=AAAA&name=$dns_record" `
        -Headers $headers -Method GET
} catch { Write-Error 'Failed to query Cloudflare'; exit 1 }

if ($response.success -ne $true) { Write-Error 'Cloudflare API error'; exit 1 }
if (-not $response.result -or $response.result.Count -eq 0) {
    Write-Error "No AAAA record found for $dns_record"; exit 1
}

$record        = $response.result | Select-Object -First 1
$dns_record_id = $record.id.Trim()
$dns_record_ip = $record.content.Trim()
$is_proxied    = $record.proxied

# Skip update if nothing changed
if ((Test-IPv6Address $dns_record_ip) -and ($dns_record_ip -eq $ip) -and ($is_proxied -eq $proxied)) {
    Write-Output "No update needed. $dns_record already points to $ip"
    exit 0
}

# Build and send update
$body = @{
    type    = 'AAAA'
    name    = $dns_record
    content = $ip
    ttl     = $ttl
    proxied = $proxied
} | ConvertTo-Json -Depth 5

try {
    $updateResponse = Invoke-RestMethod `
        -Uri "https://api.cloudflare.com/client/v4/zones/$zoneid/dns_records/$dns_record_id" `
        -Headers $headers -Method PUT -Body $body
} catch { Write-Error 'Failed to update AAAA record'; exit 1 }

if ($updateResponse.success -eq $true) {
    Write-Output "Updated $dns_record AAAA to $ip"
    exit 0
}

Write-Error 'Cloudflare update failed'
exit 1

05 Install as a Windows Service (NSSM)

NSSM (Non-Sucking Service Manager) wraps PowerShell into a proper Windows service so the script runs on every boot — before you even log in. Because you'd forget to run it manually. We all would. That's why services exist.

Run PowerShell as Administrator for all NSSM commands. Not regular PowerShell. The one with the little shield icon. Yes, that one. Right-click → Run as Administrator.

Paste everything at once if you're feeling reckless and/or caffeinated:

PowerShell (Administrator) // FULL INSTALL
& "C:\tools\nssm.exe" install CloudflareIpv6DDNS "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"
& "C:\tools\nssm.exe" set CloudflareIpv6DDNS AppDirectory    "C:\Potatosips_DDNS_Cloudflare"
& "C:\tools\nssm.exe" set CloudflareIpv6DDNS AppParameters   '-ExecutionPolicy Bypass -File "C:\Potatosips_DDNS_Cloudflare\potatosips-update-cloudflare-dns-ipv6-only.ps1"'
& "C:\tools\nssm.exe" set CloudflareIpv6DDNS DisplayName    "Cloudflare IPv6 DDNS"
& "C:\tools\nssm.exe" set CloudflareIpv6DDNS Description    "Updates Cloudflare AAAA record with current IPv6"
& "C:\tools\nssm.exe" set CloudflareIpv6DDNS Start          SERVICE_AUTO_START
& "C:\tools\nssm.exe" set CloudflareIpv6DDNS AppExit        Default Exit
& "C:\tools\nssm.exe" dump CloudflareIpv6DDNS
& "C:\tools\nssm.exe" start CloudflareIpv6DDNS

06 Manage the Service Later

PowerShell // SERVICE MANAGEMENT
# Stop the service
& "C:\tools\nssm.exe" stop CloudflareIpv6DDNS

# Restart the service
& "C:\tools\nssm.exe" restart CloudflareIpv6DDNS

# Remove the service entirely (nuclear option)
& "C:\tools\nssm.exe" remove CloudflareIpv6DDNS confirm

07 How It Works

The script logic runs as follows every time the service starts (i.e., on every boot):

  1. Loads config from _conf.ps1 via dot-sourcing (same scope, so variables are shared)
  2. Validates all config values — wrong TTL, wrong proxied type, invalid what_ip → hard exit
  3. Detects your IPv6: tries api64.ipify.org, ifconfig.co, icanhazip.com in order, validates each is actually an IPv6 (not IPv4 sneaking through)
  4. Calls Cloudflare API to get the current AAAA record content and its record ID
  5. Compares current IP vs DNS IP — if they match and proxy state matches → exits cleanly, no API call
  6. If different → sends a PUT request to update the record with the new IPv6, TTL, and proxied flag
  7. Logs success or failure via Write-Output / Write-Error (visible in NSSM logs)
The script only fires on service start. It doesn't loop or poll continuously. To run it periodically (e.g., every 5 minutes), wrap it in a Task Scheduler trigger or modify the service with AppThrottle / restart settings in NSSM. For most home setups, once-per-boot is enough.
If your ISP changes your IPv6 mid-session without rebooting your PC — this won't catch it until next restart. That's a known limitation. Your ISP is the villain here, not the script. Write them an angry letter. They won't read it, but it'll make you feel better.