diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2025-10-06 09:55:22 +0300 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2025-10-06 10:42:16 +0300 |
| commit | 5adcc04e4c3441de8258b29216411bb53a709c0f (patch) | |
| tree | 5ccc8ad26eb5f632f79ccdc6f8d0190190ccde07 /templates/usr_local_bin_hetzner_ddns.sh.j2 | |
| parent | ae9be6373428471dd4c95f64741b990638c272c0 (diff) | |
Work on Hetzner DDNS
Diffstat (limited to 'templates/usr_local_bin_hetzner_ddns.sh.j2')
| -rw-r--r-- | templates/usr_local_bin_hetzner_ddns.sh.j2 | 153 |
1 files changed, 103 insertions, 50 deletions
diff --git a/templates/usr_local_bin_hetzner_ddns.sh.j2 b/templates/usr_local_bin_hetzner_ddns.sh.j2 index aee1016..7fed440 100644 --- a/templates/usr_local_bin_hetzner_ddns.sh.j2 +++ b/templates/usr_local_bin_hetzner_ddns.sh.j2 @@ -1,66 +1,119 @@ #!/bin/sh -# Minimal Hetzner DNS updater for pfSense/FreeBSD -# - Always updates on startup (no pre-check) -# - Then polls and only updates when IP changes -# - Keeps last IP only in memory (no files) +# +# Hetzner DNS A record updater (one-shot for cron) +# +# Intended to be run every minute via cron wrapped in: +# logto /var/log/hetzner_ddns /usr/local/bin/hetzner_ddns.sh +# +# Responsibilities: +# 1. Determine current IPv4 address from a specified interface (command). +# 2. Fetch existing DNS record value from Hetzner. +# 3. If different, issue PUT to update record; otherwise exit quietly. +# +# Exits: +# 0 = Successfully updated or already up-to-date. +# 1 = Transient issue (e.g., no local IP yet, API/network error). +# 2 = Permanent-ish configuration problem (e.g., missing token). +# +# Requirements: +# - curl +# - A file /usr/local/etc/hetzner_auth containing ONLY the API token. +# +# If you ever need to change the interface command, expose a variable +# and adapt IFACE_CMD below in the template. -# --- CONFIG --------------------------------------------------------------- -API_TOKEN="$(cat /usr/local/etc/hetzner_auth)" +set -eu + +# ---------------------------- Configuration --------------------------------- +API_TOKEN_FILE="/usr/local/etc/hetzner_auth" +API_BASE="https://dns.hetzner.com/api/v1" ZONE_ID="{{ hetzner_zone_id }}" RECORD_ID="{{ hetzner_record_id }}" -RECORD_NAME=pursotin +RECORD_NAME="pursotin" RECORD_TYPE="A" -IFACE_CMD="ifconfig epw1b" - -POLL_INTERVAL=60 -API_BASE="https://dns.hetzner.com/api/v1" TTL="300" -# -------------------------------------------------------------------------- +IFACE_CMD="jexec ingress ifconfig epw1b" +# --------------------------------------------------------------------------- -get_ip() { - # Expect FreeBSD-style ifconfig output; grab first IPv4 addr - # Example: 'inet 192.0.2.3 ...' - sh -c "$IFACE_CMD" 2>/dev/null | awk '/inet[[:space:]]/ {print $2; exit}' +fail() { + echo "ERROR: $*" >&2 + exit 1 } -update_record() { - ip="$1" - body=$(printf '{"type":"%s","name":"%s","value":"%s","zone_id":"%s","ttl":%s}' \ - "$RECORD_TYPE" "$RECORD_NAME" "$ip" "$ZONE_ID" "$TTL") +warn() { + echo "WARN: $*" >&2 +} - http_code=$( - curl -sS -o /dev/null -w "%{http_code}" -X PUT \ - -H "Content-Type: application/json" \ - -H "Auth-API-Token: ${API_TOKEN}" \ - --data "$body" \ - "${API_BASE}/records/${RECORD_ID}" - ) +info() { + echo "INFO: $*" +} - [ "$http_code" = "200" ] || { - echo "$(date -u +"%F %T") update failed (HTTP $http_code)" >&2 - return 1 - } +# Read token +if [ ! -r "${API_TOKEN_FILE}" ]; then + fail "Token file missing or unreadable: ${API_TOKEN_FILE}" +fi +API_TOKEN="$(cat "${API_TOKEN_FILE}" | tr -d '[:space:]')" +[ -n "${API_TOKEN}" ] || fail "API token is empty" - echo "$(date -u +"%F %T") updated ${RECORD_NAME} to ${ip}" - return 0 +# Get current IPv4 from interface output +get_local_ip() { + # Extract the first 'inet ' IPv4 (ignoring inet6) + sh -c "${IFACE_CMD}" 2>/dev/null | awk '/inet[[:space:]]/ {print $2; exit}' } -# --- Startup: always update once (no check) -------------------------------- -last_ip="" -ip="$(get_ip)" -if [ -n "$ip" ]; then - update_record "$ip" && last_ip="$ip" -else - echo "$(date -u +"%F %T") no IPv4 from: ${IFACE_CMD}; will retry..." >&2 +LOCAL_IP="$(get_local_ip || true)" +if [ -z "${LOCAL_IP}" ]; then + warn "No IPv4 address found via '${IFACE_CMD}' (interface not ready?)" + exit 1 fi -# --- Poll loop: update only on change -------------------------------------- -while :; do - ip="$(get_ip)" - if [ -n "$ip" ] && [ "$ip" != "$last_ip" ]; then - if update_record "$ip"; then - last_ip="$ip" - fi - fi - sleep "$POLL_INTERVAL" -done +# Fetch current DNS record value +fetch_record_ip() { + # We ONLY need the "value" field. Avoid jq dependency; use awk. + # Response shape (simplified): + # {"record":{"id":"...","value":"198.51.100.7", ...}} + curl -sS -H "Auth-API-Token: ${API_TOKEN}" \ + "${API_BASE}/records/${RECORD_ID}" \ + | awk -F'"' '/"value":"/ {print $4; exit}' +} + +REMOTE_IP="$(fetch_record_ip || true)" +if [ -z "${REMOTE_IP}" ]; then + warn "Failed to parse remote record value (network/API issue?)" + exit 1 +fi + +if [ "${REMOTE_IP}" = "${LOCAL_IP}" ]; then + info "No change (${LOCAL_IP})" + exit 0 +fi + +# Prepare JSON body (avoid subshell harm by using printf for escaping basics) +BODY=$(printf '{"type":"%s","name":"%s","value":"%s","zone_id":"%s","ttl":%s}' \ + "${RECORD_TYPE}" "${RECORD_NAME}" "${LOCAL_IP}" "${ZONE_ID}" "${TTL}") + +HTTP_CODE=$( + curl -sS -o /dev/null -w "%{http_code}" -X PUT \ + -H "Content-Type: application/json" \ + -H "Auth-API-Token: ${API_TOKEN}" \ + --data "${BODY}" \ + "${API_BASE}/records/${RECORD_ID}" +) + +case "${HTTP_CODE}" in + 200) + info "Updated ${RECORD_NAME} ${REMOTE_IP} -> ${LOCAL_IP}" + exit 0 + ;; + 4*) + fail "Client error from API (HTTP ${HTTP_CODE}) - check IDs/token/body" + ;; + 5*) + warn "Server error from API (HTTP ${HTTP_CODE})" + exit 1 + ;; + *) + warn "Unexpected HTTP status ${HTTP_CODE}" + exit 1 + ;; +esac |
