Revert to initial idea of copy-pasting but attirbuting the following script by https://github.com/K0p1-Git/cloudflare-ddns-updater/blob/main/cloudflare-template.sh

This commit is contained in:
Root User 2026-03-03 11:32:15 +01:00
parent 1f3594efa5
commit 4be80d2c42
Signed by: root
GPG key ID: 087F0A95E5766D72

View file

@ -5,39 +5,12 @@
...
}: let
choose = paths: builtins.head (builtins.filter (p: builtins.pathExists p) paths);
# Package the upstream cloudflare-ddns-updater from GitHub into the Nix store
upstreamOwner = "K0p1-Git";
upstreamRepo = "cloudflare-ddns-updater";
upstreamRev = "e9906c3aa0b73c26e3473618ad7a69db853e669d";
upstreamSrc = pkgs.fetchFromGitHub {
owner = upstreamOwner;
repo = upstreamRepo;
rev = upstreamRev;
sha256 = "0j1pv57hk9rhr1kxqqjxg71y0d2d1hi3b4yiq908x5kcv3abbina";
};
cloudflare-ddns-pkg = pkgs.stdenv.mkDerivation {
pname = "cloudflare-ddns-updater";
version = upstreamRev;
src = upstreamSrc;
# no build required for a script-only repo
buildPhase = '': '';
installPhase = ''
mkdir -p $out/bin $out/lib/cloudflare-ddns
cp -r $src/* $out/lib/cloudflare-ddns/ || true
if [ -x "$out/lib/cloudflare-ddns/update.sh" ]; then
cp "$out/lib/cloudflare-ddns/update.sh" $out/bin/cloudflare-ddns
chmod +x $out/bin/cloudflare-ddns
else
# If upstream layout changes, keep the repo contents available under $out/lib
true
fi
'';
meta = with pkgs.lib; {
description = "Cloudflare DDNS updater";
license = licenses.mit;
};
};
# Domain and Cloudflare DDNS records configured here. Update this list to add/remove records.
domain = "prg-radio.org";
records = ["git" "grafana" "anubis" "wavelog" "partdb" "mail" "mailadmin" "@"];
recordsStr = lib.concatStringsSep " " records;
zoneId = "9fde8d0fa53502f2d1b7e0b1d3765d49";
envFile = "/home/traefikprg/cloudflare/cloudflare.env";
in {
options.local = {
hostname = lib.mkOption {
@ -595,113 +568,178 @@ in {
};
};
# PRG Cloudflare DDNS updater
environment.etc."cloudflare-ddns/update.sh" = {
text = let
domain = "prg-radio.org";
records = ["git" "grafana" "anubis" "wavelog" "partdb" "mail" "mailadmin" "@"];
recordsStr = lib.concatStringsSep " " records;
zoneId = "9fde8d0fa53502f2d1b7e0b1d3765d49";
envFile = "/home/traefikprg/cloudflare/cloudflare.env";
in ''
#!/usr/bin/env bash
set -euuo pipefail
# PRG Cloudflare DDNS updater - split into a single-run upstream script and a wrapper that loops records
environment.etc."cloudflare-ddns/update-single.sh" = {
text = ''
#!/bin/bash
## K0p1-Git cloudflare-ddns-updater (packaged copy)
## Upstream: https://github.com/K0p1-Git/cloudflare-ddns-updater
## Author: K0p1-Git
## License: MIT
# Load environment variables (must contain CLOUDFLARE_API_TOKEN)
if [ -f "${envFile}" ]; then
# shellcheck disable=SC1090
source "${envFile}"
fi
## change to "bin/sh" when necessary
if [ -z "$${CLOUDFLARE_API_TOKEN:-}" ]; then
echo "CLOUDFLARE_API_TOKEN is not set in ${envFile}, aborting" >&2
exit 2
fi
auth_email="" # The email used to login 'https://dash.cloudflare.com'
auth_method="token" # Set to "global" for Global API Key or "token" for Scoped API Token
auth_key="" # Your API Token or Global API Key
zone_identifier="" # Can be found in the "Overview" tab of your domain
record_name="" # Which record you want to be synced
ttl=3600 # Set the DNS TTL (seconds)
proxy="false" # Set the proxy to true or false
sitename="" # Title of site "Example Site"
slackchannel="" # Slack Channel #example
slackuri="" # URI for Slack WebHook "https://hooks.slack.com/services/xxxxx"
discorduri="" # URI for Discord WebHook "https://discordapp.com/api/webhooks/xxxxx"
API_BASE="https://api.cloudflare.com/client/v4"
# get current public IPv4
CURRENT_IP=$(curl -4 -sS https://ipv4.icanhazip.com | tr -d '\n') || true
if [ -z "$CURRENT_IP" ]; then
echo "Failed to detect public IPv4 address" >&2
exit 3
fi
jq=${pkgs.jq}/bin/jq
update_or_create() {
local fqdn="$1"
# Query existing A records for fqdn
res=$(curl -sS -X GET "$API_BASE/zones/${zoneId}/dns_records?type=A&name=$fqdn" \
-H "Authorization: Bearer $${CLOUDFLARE_API_TOKEN:-}" \
-H "Content-Type: application/json")
ok=$(echo "$res" | $jq -r '.success')
if [ "$ok" != "true" ]; then
echo "Cloudflare API query failed for $fqdn: $res" >&2
return 1
fi
existing_ip=$(echo "$res" | $jq -r '.result[0].content // empty')
record_id=$(echo "$res" | $jq -r '.result[0].id // empty')
if [ -n "$existing_ip" ] && [ "$existing_ip" = "$CURRENT_IP" ]; then
echo "$fqdn is already set to $CURRENT_IP, skipping"
return 0
fi
payload=$(cat <<JSON
{"type":"A","name":"$fqdn","content":"$CURRENT_IP","ttl":1,"proxied":false}
JSON
###########################################
## Check if we have a public IP
###########################################
REGEX_IPV4="^(0*(1?[0-9]{1,2}|2([0-4][0-9]|5[0-5]))\\.){3}0*(1?[0-9]{1,2}|2([0-4][0-9]|5[0-5]))$"
IP_SERVICES=(
"https://api.ipify.org"
"https://ipv4.icanhazip.com"
"https://ipinfo.io/ip"
)
if [ -n "$record_id" ]; then
echo "Updating $fqdn ($record_id) -> $CURRENT_IP"
curl -sS -X PUT "$API_BASE/zones/${zoneId}/dns_records/$record_id" \
-H "Authorization: Bearer $${CLOUDFLARE_API_TOKEN:-}" \
-H "Content-Type: application/json" \
--data "$payload" | $jq -r '.'
else
echo "Creating record $fqdn -> $CURRENT_IP"
curl -sS -X POST "$API_BASE/zones/${zoneId}/dns_records" \
-H "Authorization: Bearer $${CLOUDFLARE_API_TOKEN:-}" \
-H "Content-Type: application/json" \
--data "$payload" | $jq -r '.'
fi
}
# Try all the ip services for a valid IPv4 address
for service in $${IP_SERVICES[@]}; do
RAW_IP=$(curl -s $service)
if [[ $RAW_IP =~ $REGEX_IPV4 ]]; then
CURRENT_IP=$BASH_REMATCH
logger -s "DDNS Updater: Fetched IP $CURRENT_IP"
break
else
logger -s "DDNS Updater: IP service $service failed."
fi
done
# Exit if IP fetching failed
if [[ -z "$${CURRENT_IP}" ]]; then
logger -s "DDNS Updater: Failed to find a valid IP."
exit 2
fi
###########################################
## Check and set the proper auth header
###########################################
if [[ "$${auth_method}" == "global" ]]; then
auth_header="X-Auth-Key:"
else
auth_header="Authorization: Bearer"
fi
###########################################
## Seek for the A record
###########################################
logger "DDNS Updater: Check Initiated"
record=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$${zone_identifier}/dns_records?type=A&name=$${record_name}" \
-H "X-Auth-Email: $${auth_email}" \
-H "$${auth_header} $${auth_key}" \
-H "Content-Type: application/json")
###########################################
## Check if the domain has an A record
###########################################
if [[ $record == *"\\"count\\":0"* ]]; then
logger -s "DDNS Updater: Record does not exist, perhaps create one first? ($${CURRENT_IP} for $${record_name})"
exit 1
fi
###########################################
## Get existing IP
###########################################
old_ip=$(echo "$record" | sed -E 's/.*"content":"(([0-9]{1,3}\\.){3}[0-9]{1,3})".*/\\1/')
# Compare if they're the same
if [[ $CURRENT_IP == $old_ip ]]; then
logger "DDNS Updater: IP ($CURRENT_IP) for $${record_name} has not changed."
exit 0
fi
###########################################
## Set the record identifier from result
###########################################
record_identifier=$(echo "$record" | sed -E 's/.*"id":"([A-Za-z0-9_]+)".*/\\1/')
###########################################
## Change the IP@Cloudflare using the API
###########################################
update=$(curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$${zone_identifier}/dns_records/$${record_identifier}" \
-H "X-Auth-Email: $${auth_email}" \
-H "$${auth_header} $${auth_key}" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"$${record_name}\",\"content\":\"$${CURRENT_IP}\",\"ttl\":$${ttl},\"proxied\":$${proxy}}")
###########################################
## Report the status (simplified payload construction to avoid nested-quote issues)
###########################################
case "$update" in
*"\\\"success\\\":false"*)
echo -e "DDNS Updater: $${CURRENT_IP} $${record_name} DDNS failed for $${record_identifier} ($${CURRENT_IP}). DUMPING RESULTS:\n$update" | logger -s
if [[ -n "$${slackuri}" ]]; then
msg="$${sitename} DDNS Update Failed: $${record_name}: $${record_identifier} ($${CURRENT_IP})."
curl -L -X POST "$${slackuri}" --data-raw "{\"channel\":\"$${slackchannel}\",\"text\":\"$${msg}\"}"
fi
if [[ -n "$${discorduri}" ]]; then
msg="$${sitename} DDNS Update Failed: $${record_name}: $${record_identifier} ($${CURRENT_IP})."
curl -i -H "Accept: application/json" -H "Content-Type:application/json" -X POST --data-raw "{\"content\":\"$${msg}\"}" "$${discorduri}"
fi
exit 1;;
*)
msg="$${sitename} Updated: $${record_name}'s new IP Address is $${CURRENT_IP}"
logger "DDNS Updater: $${CURRENT_IP} $${record_name} DDNS updated."
if [[ -n "$${slackuri}" ]]; then
curl -L -X POST "$${slackuri}" --data-raw "{\"channel\":\"$${slackchannel}\",\"text\":\"$${msg}\"}"
fi
if [[ -n "$${discorduri}" ]]; then
curl -i -H "Accept: application/json" -H "Content-Type:application/json" -X POST --data-raw "{\"content\":\"$${msg}\"}" "$${discorduri}"
fi
exit 0;;
esac
echo "Starting PRG Cloudflare DDNS updater: setting IP=$CURRENT_IP"
for r in ${recordsStr}; do
if [ "$r" = "@" ]; then
fqdn="${domain}"
else
fqdn="$r.${domain}"
fi
update_or_create "$fqdn" || true
done
'';
mode = "0755";
group = "root";
};
systemd.services."prg-cloudflare-ddns-updater" = {
description = "PRG Cloudflare DDNS updater";
wantedBy = ["multi-user.target"];
serviceConfig = {
Type = "oneshot";
EnvironmentFile = "/home/traefikprg/cloudflare/cloudflare.env";
ExecStart = "${pkgs.bash}/bin/bash /etc/cloudflare-ddns/update.sh";
};
};
environment.etc."cloudflare-ddns/update.sh" = {
text = ''
#!/usr/bin/env bash
set -euo pipefail
systemd.timers."prg-cloudflare-ddns-updater" = {
description = "Run PRG Cloudflare DDNS updater periodically";
wantedBy = ["timers.target"];
timerConfig = {
OnBootSec = "1m";
OnUnitActiveSec = "10m";
Persistent = true;
};
# Wrapper: source env, map tokens, loop declared records and call the upstream single-run script
if [ -f "${envFile}" ]; then
# shellcheck disable=SC1090
source "${envFile}"
fi
# Map env variables from the env file into auth_key/auth_email used by the upstream script
if [ -n "$${CLOUDFLARE_DNS_API_TOKEN:-}" ]; then
export auth_key="$${CLOUDFLARE_DNS_API_TOKEN:-}"
elif [ -n "$${CLOUDFLARE_API_TOKEN:-}" ]; then
export auth_key="$${CLOUDFLARE_API_TOKEN:-}"
fi
if [ -n "$${CLOUDFLARE_USERNAME:-}" ]; then
export auth_email="$${CLOUDFLARE_USERNAME:-}"
fi
# Ensure zone id is exported for the single-run script
export zone_identifier="${zoneId}"
# Loop records from the Nix list. "@" maps to the base domain
for r in ${recordsStr}; do
if [ "$r" = "@" ]; then
export record_name="${domain}"
else
export record_name="$r.${domain}"
fi
/etc/cloudflare-ddns/update-single.sh || true
done
'';
mode = "0755";
group = "root";
};
# Enable Tailscale for remote access to Traefik dashboard and configuration