diff options
| author | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-02-18 22:30:08 -0800 |
|---|---|---|
| committer | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-02-18 22:30:08 -0800 |
| commit | 97bf42e6461fa9631086c3bd7f89cd761ca2e9c3 (patch) | |
| tree | 2e615c80150a7b3983501719113a53406cc430bb | |
| parent | 9ef2d8b2721cc691af1f9aef09e86ac8d40ada09 (diff) | |
| download | hetzner-ddns-97bf42e6461fa9631086c3bd7f89cd761ca2e9c3.tar.gz hetzner-ddns-97bf42e6461fa9631086c3bd7f89cd761ca2e9c3.tar.xz | |
| -rwxr-xr-x | hetzner-ddns | 68 |
1 files changed, 42 insertions, 26 deletions
diff --git a/hetzner-ddns b/hetzner-ddns index 6d70e69..0c78fa0 100755 --- a/hetzner-ddns +++ b/hetzner-ddns @@ -6,63 +6,79 @@ # HETZNER_TOKEN_FILE=/path/to/token hetzner-ddns <domain> # systemctl enable --now "hetzner-ddns@$(systemd-escape <domain>).timer" -IP_RESOLVER='https://ifconfig.co' +IP_RESOLVER='https://ip.hetzner.com' TARGET="${1}" shopt -s extglob die() { - [[ -n "${@}" ]] && echo "${@}" >&2 + [[ -n "${@}" ]] && echo "${0##*/}: ${@}" >&2 exit 1 } hetzcurl() { - curl -sfH "Auth-API-Token: ${HETZNER_TOKEN}" \ - "https://dns.hetzner.com/api/v1/${1}" \ + curl -sfm 30 --connect-timeout 10 \ + -H "Authorization: Bearer ${HETZNER_TOKEN}" \ + "https://api.hetzner.cloud/v1/${1}" \ "${@:2}" } if [[ -z "${HETZNER_TOKEN}" ]] && [[ -n "${HETZNER_TOKEN_FILE}" ]]; then - [[ -f "${HETZNER_TOKEN_FILE}" ]] || die 'Specified token file' \ - "(${HETZNER_TOKEN_FILE}) does not exist" + [[ -f "${HETZNER_TOKEN_FILE}" ]] || die \ + "specified token file does not exist: ${HETZNER_TOKEN_FILE}" HETZNER_TOKEN="$(<"${HETZNER_TOKEN_FILE}")" fi -[[ -n "${HETZNER_TOKEN}" ]] || die 'Missing token' +[[ -n "${HETZNER_TOKEN}" ]] || die 'no token specified' -ip="$(curl -sf4 "${IP_RESOLVER}")" || die 'IP lookup failed' +ip="$(curl -sf4m 30 --connect-timeout 10 "${IP_RESOLVER}")" || die \ + 'IP lookup failed' +res="$(hetzcurl "zones")" || die 'zones lookup failed' zone_re="${TARGET}" while [[ "${zone_re}" =~ ^([^\\]*)\.(.*)$ ]]; do zone_re="(${BASH_REMATCH[1]}\\.)?${BASH_REMATCH[2]}" done - -res="$(hetzcurl "zones")" || die 'Zones lookup failed' IFS='|' read zone_id zone_name < <(jq -er --arg re "${zone_re}" \ '[.zones[] | select(.name | test("\\A" + $re + "\\z"))] | if . == [] then (null | halt_error) else . end | max_by(.name | length) | [.id, .name] | join("|")' \ - <<< "${res}") || die 'Zone not found' + <<< "${res}") || die 'zone not found' rec_name="${TARGET%%?(.)${zone_name}}" rec_name="${rec_name:-@}" -res="$(hetzcurl "records?zone_id=${zone_id}")" || die 'Records lookup failed' -rec_id="$(jq -er --arg name "${rec_name}" \ - 'first(.records[] | select(.type == "A" and .name == $name)) | .id' \ - <<< "${res}")" || die 'Record not found' - -res="$(hetzcurl "records/${rec_id}")" || die 'Record lookup failed' -old_ip="$(jq -r '.record.value' <<< "${res}")" - +res="$(hetzcurl "zones/${zone_id}/rrsets/${rec_name}/A")" || die \ + 'records lookup failed' +old_ip="$(jq -er '.rrset.records[0].value' <<< "${res}")" || die \ + 'record not found' if [[ "${old_ip}" == "${ip}" ]]; then echo "IP unchanged from ${ip}" exit fi -data="$(jq -c --arg ip "${ip}" \ - '.record | {zone_id, type, name, value: $ip }' <<< "${res}")" -res="$(hetzcurl "records/${rec_id}" \ - -H 'Content-Type: application/json' \ - -X PUT -d "${data}")" || die 'Record update failed' -new_ip="$(jq -r '.record.value' <<< "${res}")" +task="IP change from ${old_ip} to ${ip}" +data="$(jq -c --arg ip "${ip}" --arg name "${0##*/}" \ + '{records: [{value: $ip, comment: + $name + (now | strftime(" %Y-%m-%d %H:%M:%S"))}]}' \ + <<< "${res}")" +res="$(hetzcurl "zones/${zone_id}/rrsets/${rec_name}/A/actions/set_records" \ + -H 'Content-Type: application/json' -X POST -d "${data}")" || die \ + "record update failed for ${task}" +action_id="$(jq -r '.action.id' <<< "${res}")" +task="${task} (action id: ${action_id})" +c=0 +while true; do + status="$(jq -r '.action.status' <<< "${res}")" + case "${status}" in + 'success') + break ;; + 'running') + ((++c > 16)) && die "timed out confirming ${task}" + sleep "$((c > 8 ? 4 : 1))" ;; + *) + die "record update ${status:-failed} for ${task}" ;; + esac -echo "IP changed from ${old_ip} to ${new_ip}" + res="$(hetzcurl "actions/${action_id}")" || die \ + "action lookup failed for ${task}" +done +echo "Completed ${task}" |
