Includes homelab deploy tooling, image position/zoom manifest persistence, and full trading/casino stack. Co-authored-by: Cursor <cursoragent@cursor.com>
75 lines
2.8 KiB
Bash
75 lines
2.8 KiB
Bash
#!/usr/bin/env bash
|
|
# Cloudflare Dynamic DNS — aktualisiert A/AAAA wenn sich die Fritz!Box-WAN-IP aendert.
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ENV_FILE="${SCRIPT_DIR}/env.homelab"
|
|
STATE_FILE="${SCRIPT_DIR}/.cf-ddns-last-ip"
|
|
|
|
[[ -f "${ENV_FILE}" ]] || { echo "cloudflare-ddns: ${ENV_FILE} fehlt" >&2; exit 1; }
|
|
# shellcheck source=/dev/null
|
|
source <(sed 's/\r$//' "${ENV_FILE}")
|
|
|
|
: "${CLOUDFLARE_API_TOKEN:?CLOUDFLARE_API_TOKEN fehlt}"
|
|
: "${CLOUDFLARE_ZONE_ID:?CLOUDFLARE_ZONE_ID fehlt}"
|
|
|
|
APP_DOMAIN="${APP_DOMAIN:-trilogyhub.de}"
|
|
DDNS_RECORD="${DDNS_RECORD_NAME:-home.${APP_DOMAIN}}"
|
|
DDNS_PROXIED="${DDNS_PROXIED:-true}"
|
|
|
|
IPV4="$(curl -4 -fsS --max-time 15 https://api.ipify.org 2>/dev/null || true)"
|
|
IPV6="$(curl -6 -fsS --max-time 15 https://api64.ipify.org 2>/dev/null || true)"
|
|
[[ -n "${IPV4}" || -n "${IPV6}" ]] || { echo "cloudflare-ddns: keine IP" >&2; exit 1; }
|
|
|
|
CURRENT="${IPV4}|${IPV6}"
|
|
LAST="$(cat "${STATE_FILE}" 2>/dev/null || true)"
|
|
if [[ "${CURRENT}" == "${LAST}" ]]; then
|
|
echo "$(date -Is) cloudflare-ddns ${DDNS_RECORD}: unveraendert v4=${IPV4:-none}"
|
|
exit 0
|
|
fi
|
|
|
|
export CF_API="https://api.cloudflare.com/client/v4"
|
|
export CF_ZONE="${CLOUDFLARE_ZONE_ID}"
|
|
export CF_TOKEN="${CLOUDFLARE_API_TOKEN}"
|
|
export CF_PROXIED="${DDNS_PROXIED}"
|
|
|
|
upsert() {
|
|
local rtype="$1" ip="$2"
|
|
[[ -n "${ip}" ]] || return 0
|
|
python3 - "${DDNS_RECORD}" "${rtype}" "${ip}" <<'PY'
|
|
import json, os, sys, urllib.request
|
|
name, rtype, ip = sys.argv[1:4]
|
|
api = os.environ["CF_API"]
|
|
zone = os.environ["CF_ZONE"]
|
|
token = os.environ["CF_TOKEN"]
|
|
proxied = os.environ.get("CF_PROXIED", "true").lower() == "true"
|
|
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
|
|
list_url = f"{api}/zones/{zone}/dns_records?type={rtype}&name={name}"
|
|
with urllib.request.urlopen(urllib.request.Request(list_url, headers=headers), timeout=20) as r:
|
|
data = json.load(r)
|
|
if not data.get("success"):
|
|
raise SystemExit(f"list failed: {data}")
|
|
body = json.dumps({"type": rtype, "name": name, "content": ip, "ttl": 120, "proxied": proxied}).encode()
|
|
recs = data.get("result", [])
|
|
if recs:
|
|
url = f"{api}/zones/{zone}/dns_records/{recs[0]['id']}"
|
|
req = urllib.request.Request(url, data=body, headers=headers, method="PUT")
|
|
act = "aktualisiert"
|
|
else:
|
|
url = f"{api}/zones/{zone}/dns_records"
|
|
req = urllib.request.Request(url, data=body, headers=headers, method="POST")
|
|
act = "angelegt"
|
|
with urllib.request.urlopen(req, timeout=20) as r:
|
|
out = json.load(r)
|
|
if not out.get("success"):
|
|
raise SystemExit(f"write failed: {out}")
|
|
print(f"{rtype} {name} -> {ip} ({act})")
|
|
PY
|
|
}
|
|
|
|
upsert A "${IPV4}"
|
|
upsert AAAA "${IPV6}"
|
|
echo "${CURRENT}" > "${STATE_FILE}"
|
|
chmod 600 "${STATE_FILE}" 2>/dev/null || true
|
|
echo "$(date -Is) cloudflare-ddns fertig"
|