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.
IPv6 changes
detects IP
AAAA record
world sees you
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
| COMPONENT | REQUIREMENT | NOTES |
|---|---|---|
| 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 |
02 Cloudflare Setup
▸ Get Your Zone ID
- Log into dash.cloudflare.com
- Click your domain (e.g.
yourdomain.win) - On the Overview page, scroll the right sidebar
- Copy the Zone ID — it looks like
08bd92043d60dfb6a705add7fad4c147
▸ Create an API Token
- Go to My Profile → API Tokens → Create Token
- Choose Create Custom Token
- Set Permission:
Zone › DNS › Edit - Zone Resource: Include → Specific zone → your domain
- Click Continue to summary → Create Token
- Copy the token immediately — Cloudflare will never show it again. Classic.
▸ Add the AAAA Record
- In your domain → DNS → Records → Add record
- Type:
AAAA - Name:
potatosips(or whatever subdomain you want) - IPv6 address: any valid address for now — the script will overwrite it
- Proxy status: DNS only (grey cloud ☁) — orange proxy breaks IPv6
- TTL:
2 min(120 seconds) — so changes propagate fast - Save it
03 Config File
Create this file first. It holds all your credentials and settings.
##### 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
| VARIABLE | VALUE | DESCRIPTION |
|---|---|---|
| $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 |
| $zoneid | 32-char hex string | From Cloudflare Overview sidebar |
| $cloudflare_zone_api_token | Your API token | Keep this secret. Seriously. |
| $proxied | $false | Must be false for IPv6. Orange cloud = sadness. |
| $ttl | 120 | Seconds 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.
[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.
Paste everything at once if you're feeling reckless and/or caffeinated:
& "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
# 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):
- Loads config from
_conf.ps1via dot-sourcing (same scope, so variables are shared) - Validates all config values — wrong TTL, wrong proxied type, invalid what_ip → hard exit
- Detects your IPv6: tries
api64.ipify.org,ifconfig.co,icanhazip.comin order, validates each is actually an IPv6 (not IPv4 sneaking through) - Calls Cloudflare API to get the current AAAA record content and its record ID
- Compares current IP vs DNS IP — if they match and proxy state matches → exits cleanly, no API call
- If different → sends a PUT request to update the record with the new IPv6, TTL, and proxied flag
- Logs success or failure via
Write-Output/Write-Error(visible in NSSM logs)
AppThrottle / restart settings in NSSM. For most home setups, once-per-boot is enough.