<#
Cloudflare DNS Update (Windows 11 / PowerShell)
- Updatet NUR genau einen Hostnamen: $RecordName (z.B. xxx.kaspat.xyz)
- Nutzt Cloudflare API Token (Bearer)
- Updatet A (IPv4) und optional AAAA (IPv6)
- Legt den Record optional an, falls er fehlt (CreateIfMissing)

Voraussetzungen:
- PowerShell 5.1+ (Windows 11 hat das)
- Cloudflare API Token mit: Zone -> DNS -> Edit, Zone Resource: Specific zone kaspat.xyz
#>

# =========================
# Konfiguration
# =========================
$ApiToken       = "PASTE_YOUR_CLOUDFLARE_API_TOKEN_HERE"
$ZoneName       = "kaspat.xyz"          # Zone (Root-Domain in Cloudflare)
$RecordName     = "xxx.kaspat.xyz"      # GENAU dieser Host wird aktualisiert
$UpdateIPv4     = $true
$UpdateIPv6     = $false               # auf $true setzen, wenn AAAA gewünscht
$TTL            = 300                  # 1..86400, oder 1 für "Auto" (je nach Plan)
$Proxied        = $false               # true = orange cloud (nur sinnvoll wenn du es willst)
$CreateIfMissing= $true                # Record anlegen, wenn nicht vorhanden

# IP-Check Dienste (fallback enthalten)
$IPv4Services = @(
  "https://api.ipify.org",
  "https://ifconfig.me/ip",
  "https://checkip.amazonaws.com"
)
$IPv6Services = @(
  "https://api64.ipify.org",
  "https://ifconfig.me/ip"
)

# Optional: lokales Log
$LogFile = "$env:ProgramData\CloudflareDNS\cf-dns-update.log"

# =========================
# Hilfsfunktionen
# =========================
function Write-Log {
  param([string]$Message, [ValidateSet("INFO","WARN","ERROR")] [string]$Level="INFO")
  $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
  $line = "[$ts] [$Level] $Message"
  Write-Host $line
  try {
    $dir = Split-Path -Parent $LogFile
    if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
    Add-Content -Path $LogFile -Value $line -Encoding UTF8
  } catch { }
}

function Invoke-CfApi {
  param(
    [Parameter(Mandatory)][ValidateSet("GET","POST","PUT","DELETE")][string]$Method,
    [Parameter(Mandatory)][string]$Path,
    [object]$Body = $null
  )

  if ([string]::IsNullOrWhiteSpace($ApiToken) -or $ApiToken -like "PASTE_*") {
    throw "ApiToken ist nicht gesetzt."
  }

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

  $uri = "https://api.cloudflare.com/client/v4$Path"

  try {
    if ($null -ne $Body) {
      $json = ($Body | ConvertTo-Json -Depth 10 -Compress)
      return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -Body $json -TimeoutSec 20
    } else {
      return Invoke-RestMethod -Method $Method -Uri $uri -Headers $headers -TimeoutSec 20
    }
  } catch {
    $msg = $_.Exception.Message
    if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $msg += " | " + $_.ErrorDetails.Message }
    throw "Cloudflare API Fehler ($Method $Path): $msg"
  }
}

function Get-PublicIP {
  param(
    [ValidateSet("IPv4","IPv6")][string]$Family
  )

  $services = if ($Family -eq "IPv6") { $IPv6Services } else { $IPv4Services }

  foreach ($u in $services) {
    try {
      $ip = (Invoke-RestMethod -Uri $u -TimeoutSec 10).ToString().Trim()
      if ($Family -eq "IPv4") {
        if ($ip -match '^(?:\d{1,3}\.){3}\d{1,3}$') { return $ip }
      } else {
        # einfache IPv6-Validierung (reicht für Public-Check)
        if ($ip -match '^[0-9a-fA-F:]+$' -and $ip.Contains(":")) { return $ip }
      }
    } catch {
      continue
    }
  }
  return $null
}

function Assert-RecordNameInZone {
  # Sicherheitsgurt: verhindert, dass aus Versehen ein fremder Host aktualisiert wird.
  if (-not $RecordName.ToLower().EndsWith($ZoneName.ToLower())) {
    throw "RecordName '$RecordName' passt nicht zur Zone '$ZoneName'. Abbruch."
  }
}

# =========================
# Hauptlogik
# =========================
try {
  Assert-RecordNameInZone
  Write-Log "Starte DNS-Update für $RecordName (Zone: $ZoneName) ..."

  # Zone-ID holen
  $zoneResp = Invoke-CfApi -Method GET -Path "/zones?name=$([uri]::EscapeDataString($ZoneName))&status=active"
  if (-not $zoneResp.success -or -not $zoneResp.result -or $zoneResp.result.Count -lt 1) {
    throw "Zone '$ZoneName' nicht gefunden oder nicht aktiv."
  }
  $zoneId = $zoneResp.result[0].id
  Write-Log "ZoneId: $zoneId"

  function Upsert-Record {
    param(
      [ValidateSet("A","AAAA")][string]$Type,
      [Parameter(Mandatory)][string]$Content
    )

    # Record suchen (genau Name + Typ)
    $qName = [uri]::EscapeDataString($RecordName)
    $qType = [uri]::EscapeDataString($Type)
    $recResp = Invoke-CfApi -Method GET -Path "/zones/$zoneId/dns_records?type=$qType&name=$qName"

    $record = $null
    if ($recResp.success -and $recResp.result -and $recResp.result.Count -ge 1) {
      # Wenn mehrere, nimm den ersten (normalerweise ist es genau einer)
      $record = $recResp.result[0]
    }

    if ($null -eq $record) {
      if (-not $CreateIfMissing) {
        Write-Log "$Type Record für $RecordName existiert nicht (CreateIfMissing=false). Überspringe." "WARN"
        return
      }

      Write-Log "$Type Record existiert nicht, lege neu an ..."
      $createBody = @{
        type    = $Type
        name    = $RecordName
        content = $Content
        ttl     = $TTL
        proxied = $Proxied
      }
      $createResp = Invoke-CfApi -Method POST -Path "/zones/$zoneId/dns_records" -Body $createBody
      if (-not $createResp.success) { throw "Anlegen des $Type Records fehlgeschlagen." }
      Write-Log "$Type Record angelegt: $RecordName -> $Content"
      return
    }

    # Wenn Content gleich ist: nichts tun
    if ($record.content -eq $Content) {
      Write-Log "$Type unverändert ($Content). Kein Update nötig."
      return
    }

    # Update (PUT auf Record-ID)
    $recId = $record.id
    Write-Log "$Type ändert sich: $($record.content) -> $Content (RecordId: $recId)"
    $updateBody = @{
      type    = $Type
      name    = $RecordName
      content = $Content
      ttl     = $TTL
      proxied = $Proxied
    }
    $updResp = Invoke-CfApi -Method PUT -Path "/zones/$zoneId/dns_records/$recId" -Body $updateBody
    if (-not $updResp.success) { throw "Update des $Type Records fehlgeschlagen." }
    Write-Log "$Type Update OK: $RecordName -> $Content"
  }

  if ($UpdateIPv4) {
    $ipv4 = Get-PublicIP -Family IPv4
    if (-not $ipv4) { Write-Log "Konnte keine öffentliche IPv4 ermitteln." "ERROR" }
    else { Upsert-Record -Type A -Content $ipv4 }
  }

  if ($UpdateIPv6) {
    $ipv6 = Get-PublicIP -Family IPv6
    if (-not $ipv6) { Write-Log "Konnte keine öffentliche IPv6 ermitteln." "ERROR" }
    else { Upsert-Record -Type AAAA -Content $ipv6 }
  }

  Write-Log "Fertig."
  exit 0
}
catch {
  Write-Log $_.Exception.Message "ERROR"
  exit 1
}

Wenn du das als geplante Aufgabe laufen lassen willst (z. B. alle 10 Minuten), sag kurz Bescheid – dann gebe ich dir den passenden schtasks-Befehl inkl. “run whether user is logged on or not” und sauberem Pfad (z. B. C:\Scripts\cf-dns-update.ps1).

ja, alle 5 minuten auf update checken unabhängig von der benutzeranmeldung, pfad wie vorgeschlagen

Perfekt 👍 Hier ist das saubere, praxiserprobte Setup, genau so wie du es willst: alle 5 Minuten, ohne Benutzeranmeldung, Windows 11 Pro, Pfad C:\Scripts.

1️⃣ Script ablegen

Speichere dein Script als:

C:\Scripts\cf-dns-update.ps1

⚠️ Achte darauf:

Datei nicht blockiert (Rechtsklick → Eigenschaften → ggf. „Zulassen“)

API-Token ist korrekt gesetzt

2️⃣ Ordner für Logs (optional, empfohlen)

Das Script nutzt:

C:\ProgramData\CloudflareDNS\cf-dns-update.log

Der Ordner wird automatisch erstellt – kein Handgriff nötig.

3️⃣ Geplante Aufgabe anlegen (SYSTEM, ohne Login) 🔹 PowerShell als Administrator öffnen

Dann exakt diesen Befehl ausführen:

schtasks /create ^ /tn „Cloudflare DNS Update xxx.kaspat.xyz“ ^ /tr „powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\cf-dns-update.ps1“ ^ /sc minute ^ /mo 5 ^ /ru SYSTEM ^ /rl HIGHEST ^ /f

✅ läuft alle 5 Minuten ✅ läuft auch ohne Benutzeranmeldung ✅ läuft mit SYSTEM-Rechten ✅ keine Probleme mit ExecutionPolicy

4️⃣ Aufgabe sofort testen schtasks /run /tn „Cloudflare DNS Update xxx.kaspat.xyz“

Dann Log prüfen:

type C:\ProgramData\CloudflareDNS\cf-dns-update.log

Beispiel:

[2026-02-04 15:12:01] [INFO] A unverändert (77.3.xxx.xxx). Kein Update nötig.

5️⃣ Typische Fehler & Fixes ❌ Script läuft manuell, aber nicht im Task

➡️ Pfad prüfen:

C:\Scripts\cf-dns-update.ps1

➡️ powershell.exe ohne Pfad ist korrekt (SYSTEM kennt ihn)

❌ Keine IPv6 gefunden

👉 normal bei vielen Anschlüssen ➡️ $UpdateIPv6 = $false ist absolut ok

❌ Keine Logs

➡️ Prüfen:

schtasks /query /tn „Cloudflare DNS Update xxx.kaspat.xyz“ /v /fo list

🔐 Sicherheits-Check (du bist hier vorbildlich)

API Token: nur DNS Edit

✔ Zone: kaspat.xyz

✔ Script: hart auf xxx.kaspat.xyz verdrahtet

✔ Task: kein User-Kontext

✔ Optional: IP-Restriction im Token

➡️ Das ist State of the Art DynDNS auf Windows.

Wenn du willst, erweitere ich das Script noch um:

📩 Telegram / Mail nur bei echter Änderung

🧠 State-File (damit bei Neustart kein unnötiger PUT kommt)

🔕 Silent Mode (keine Logs ohne Änderung)

🌐 A + AAAA getrennt steuerbar per Switch

Sag einfach Bescheid 💪

DokuWiki Appliance - Powered by TurnKey Linux