Dynadot: Skillnad mellan sidversioner
Utseende
Skapade sidan med '{{Short description|Dynadot Dynamic DNS setup and Ubuntu update script}} = Dynadot Dynamic DNS (DDNS) Setup = This page documents how to configure Dynamic DNS for Dynadot and automatically update the A record when the public IP address changes. == Requirements == * Dynadot account * Domain using ''Dynadot DNS'' * DDNS password generated in Dynadot * Ubuntu server with outbound HTTPS access == Dynadot configuration == === 1. Enable Dynadot DNS === # Log in to Dynadot...' |
|||
| (5 mellanliggande sidversioner av samma användare visas inte) | |||
| Rad 61: | Rad 61: | ||
<pre> | <pre> | ||
# | nano .env | ||
</pre> | |||
==== Add this: ==== | |||
<pre> | |||
# ===== Dynadot settings ===== | |||
# SUBDOMAIN should be empty for root domain | |||
DOMAIN=viberq.org | |||
SUBDOMAIN= | |||
TYPE=A | |||
PASSWORD=<hash pw> | |||
TTL=300 | |||
</pre> | |||
<pre> | |||
chmod 600 .env | |||
</pre> | |||
<pre> | |||
#!/usr/bin/env bash | |||
set -euo pipefail | |||
if [ - | # ===== PATHS / CONFIG (override via env) ===== | ||
exit 1 | SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
ENV_LOCAL="${ENV_LOCAL:-$SCRIPT_DIR/.env}" | |||
ENV_DISCORD="${ENV_DISCORD:-$SCRIPT_DIR/../bots/discord/.env}" | |||
LOG_FILE="${LOG_FILE:-$SCRIPT_DIR/log.txt}" | |||
LOG_KEEP="${LOG_KEEP:-5}" # keep log.txt.1 .. log.txt.$LOG_KEEP | |||
STATE_DIR="${STATE_DIR:-$SCRIPT_DIR/state}" | |||
IP_FILE="${IP_FILE:-$STATE_DIR/dynadot_last_ip.txt}" | |||
SSL_NOTIFY_STATE="${SSL_NOTIFY_STATE:-$STATE_DIR/ssl_expiry_last_notified.txt}" | |||
DISCORD_API_VERSION="${DISCORD_API_VERSION:-9}" | |||
ENABLE_DDNS="${ENABLE_DDNS:-1}" | |||
ENABLE_SSL="${ENABLE_SSL:-1}" | |||
# ===== ARGS ===== | |||
FORCE=0 | |||
DDNS_ONLY=0 | |||
SSL_ONLY=0 | |||
while [[ $# -gt 0 ]]; do | |||
case "$1" in | |||
--force) FORCE=1; shift ;; | |||
--ddns-only) DDNS_ONLY=1; shift ;; | |||
--ssl-only) SSL_ONLY=1; shift ;; | |||
-h|--help) | |||
cat <<EOF | |||
Usage: $0 [--force] [--ddns-only|--ssl-only] | |||
--force Force update/reload even if unchanged | |||
--ddns-only Run only DDNS task | |||
--ssl-only Run only SSL task | |||
EOF | |||
exit 0 | |||
;; | |||
*) | |||
echo "Unknown arg: $1" >&2 | |||
exit 2 | |||
;; | |||
esac | |||
done | |||
# ===== HELPERS ===== | |||
mkdir -p "$STATE_DIR" | |||
rotate_logs() { | |||
local i | |||
for ((i=LOG_KEEP; i>=1; i--)); do | |||
if [[ -f "${LOG_FILE}.${i}" ]]; then | |||
if (( i == LOG_KEEP )); then | |||
rm -f "${LOG_FILE}.${i}" | |||
else | |||
mv -f "${LOG_FILE}.${i}" "${LOG_FILE}.$((i+1))" | |||
fi | |||
fi | |||
done | |||
[[ -f "$LOG_FILE" ]] && mv -f "$LOG_FILE" "${LOG_FILE}.1" | |||
} | |||
log() { | |||
local msg="${1:-}" | |||
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$msg" >> "$LOG_FILE" | |||
} | |||
die() { | |||
local msg="${1:-}" | |||
log "ERROR: $msg" | |||
echo "ERROR: $msg" >&2 | |||
exit 1 | |||
} | |||
require_vars() { | |||
local v | |||
for v in "$@"; do | |||
[[ -n "${!v:-}" ]] || die "Missing required env var: $v" | |||
done | |||
} | |||
has_vars() { | |||
local v | |||
for v in "$@"; do | |||
[[ -n "${!v:-}" ]] || return 1 | |||
done | |||
return 0 | |||
} | |||
load_env_files_shell_safe() { | |||
[[ -f "$ENV_LOCAL" ]] || die "Missing local .env: $ENV_LOCAL" | |||
[[ -f "$ENV_DISCORD" ]] || die "Missing discord .env: $ENV_DISCORD" | |||
# Only use with trusted, shell-safe KEY=value files. | |||
set -a | |||
# shellcheck source=/dev/null | |||
source "$ENV_LOCAL" | |||
# shellcheck source=/dev/null | |||
source "$ENV_DISCORD" | |||
set +a | |||
} | |||
curl_get() { | |||
curl --fail --show-error --silent --location "$1" | |||
} | |||
curl_get_to_file() { | |||
local url="$1" | |||
local out="$2" | |||
curl --fail --show-error --silent --location -o "$out" "$url" | |||
} | |||
run_root() { | |||
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then | |||
"$@" | |||
else | |||
command -v sudo >/dev/null 2>&1 || die "sudo not found (need root for: $*)" | |||
sudo -n "$@" || die "sudo requires a password (configure non-interactive sudo or run as root): $*" | |||
fi | |||
} | |||
discord_send_embed() { | |||
local author="${1:-Monitor}" | |||
local title="${2:-}" | |||
local desc="${3:-}" | |||
local color="${4:-16753920}" | |||
require_vars DISCORD_BOT_TOKEN LOGS_CHANNEL_ID | |||
local channel_id="$LOGS_CHANNEL_ID" | |||
local payload | |||
payload="$(python3 - "$author" "$title" "$desc" "$color" <<'PY' | |||
import json, sys | |||
author, title, desc, color = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4]) | |||
print(json.dumps({ | |||
"embeds": [{ | |||
"title": title, | |||
"description": desc, | |||
"color": color, | |||
"author": {"name": author}, | |||
}] | |||
})) | |||
PY | |||
)" | |||
curl --fail --show-error --silent \ | |||
-H "Authorization: Bot ${DISCORD_BOT_TOKEN}" \ | |||
-H "Content-Type: application/json" \ | |||
-X POST \ | |||
-d "$payload" \ | |||
"https://discord.com/api/v${DISCORD_API_VERSION}/channels/${channel_id}/messages" \ | |||
> /dev/null || true | |||
} | |||
sha256_file() { | |||
local f="$1" | |||
if command -v sha256sum >/dev/null 2>&1; then | |||
sha256sum "$f" | awk '{print $1}' | |||
else | |||
shasum -a 256 "$f" | awk '{print $1}' | |||
fi | |||
} | |||
# ===== START ===== | |||
rotate_logs | |||
log "---- Script started ----" | |||
load_env_files_shell_safe | |||
require_vars DISCORD_BOT_TOKEN LOGS_CHANNEL_ID | |||
# ===== TASK: DDNS ===== | |||
ddns_task() { | |||
require_vars DOMAIN PASSWORD TTL | |||
local current_ip last_ip subdomain type update_url response | |||
current_ip="$(curl_get "https://checkip.amazonaws.com" | tr -d '[:space:]')" || { | |||
local desc | |||
printf -v desc "Could not fetch public IP." | |||
discord_send_embed "DDNS" "DDNS ERROR" "$desc" 15158332 | |||
die "Could not fetch public IP" | |||
} | |||
[[ -f "$IP_FILE" ]] || : > "$IP_FILE" | |||
last_ip="$(cat "$IP_FILE" 2>/dev/null || true)" | |||
log "DDNS current IP: $current_ip" | |||
log "DDNS last IP: ${last_ip:-<empty>}" | |||
if [[ "$current_ip" == "$last_ip" && "$FORCE" -ne 1 ]]; then | |||
log "DDNS: IP unchanged; no update." | |||
return 0 | |||
fi | |||
log "DDNS: proceeding (force=$FORCE)." | |||
subdomain="${SUBDOMAIN:-}" | |||
type="${TYPE:-A}" | |||
update_url="https://www.dynadot.com/set_ddns?containRoot=true&domain=${DOMAIN}&subDomain=${subdomain}&type=${type}&ip=${current_ip}&pwd=${PASSWORD}&ttl=${TTL}" | |||
response="$(curl_get "$update_url")" || { | |||
local desc | |||
printf -v desc "Dynadot request failed.\nDomain: %s\nOld IP: %s\nNew IP: %s\nForced: %s" \ | |||
"$DOMAIN" "${last_ip:-N/A}" "$current_ip" "$([[ "$FORCE" -eq 1 ]] && echo yes || echo no)" | |||
discord_send_embed "DDNS" "DDNS FAIL" "$desc" 15158332 | |||
die "Dynadot request failed" | |||
} | |||
log "DDNS Dynadot response: $response" | |||
if echo "$response" | grep -qi "success"; then | |||
echo "$current_ip" > "$IP_FILE" | |||
log "DDNS: IP updated and saved." | |||
local desc | |||
printf -v desc "Domain: %s\nOld IP: %s\nNew IP: %s\nForced: %s" \ | |||
"$DOMAIN" "${last_ip:-N/A}" "$current_ip" "$([[ "$FORCE" -eq 1 ]] && echo yes || echo no)" | |||
discord_send_embed "DDNS" "DDNS UPDATED" "$desc" 3066993 | |||
else | |||
log "DDNS: update failed." | |||
local desc | |||
printf -v desc "Domain: %s\nOld IP: %s\nNew IP: %s\nForced: %s\nResponse: %s" \ | |||
"$DOMAIN" "${last_ip:-N/A}" "$current_ip" "$([[ "$FORCE" -eq 1 ]] && echo yes || echo no)" "$response" | |||
discord_send_embed "DDNS" "DDNS FAIL" "$desc" 15158332 | |||
fi | |||
} | |||
# ===== TASK: SSL ===== | |||
ssl_task() { | |||
require_vars DYNADOT_API_KEY DYNADOT_DOMAIN SSL_DIR WEB_SERVER EXPIRY_WARN_DAYS | |||
local cert_file key_file tmp_file url new_hash old_hash | |||
cert_file="$SSL_DIR/fullchain.pem" | |||
key_file="$SSL_DIR/privkey.pem" | |||
[[ -f "$key_file" ]] || die "Missing private key at $key_file" | |||
run_root mkdir -p "$SSL_DIR" | |||
tmp_file="$(mktemp)" | |||
trap 'rm -f "$tmp_file"' RETURN | |||
url="https://www.dynadot.com/letsencrypt/download_cert?key=${DYNADOT_API_KEY}&domain=${DYNADOT_DOMAIN}" | |||
if ! curl_get_to_file "$url" "$tmp_file"; then | |||
local desc | |||
printf -v desc "Failed to download certificate.\nDomain: %s" "$DYNADOT_DOMAIN" | |||
discord_send_embed "SSL Monitor" "SSL ERROR" "$desc" 15158332 | |||
die "Failed to download certificate" | |||
fi | |||
if ! openssl x509 -noout -in "$tmp_file" >/dev/null 2>&1; then | |||
local desc | |||
printf -v desc "Downloaded file is not a valid x509 cert.\nDomain: %s" "$DYNADOT_DOMAIN" | |||
discord_send_embed "SSL Monitor" "SSL ERROR" "$desc" 15158332 | |||
die "Downloaded file invalid" | |||
fi | |||
new_hash="$(sha256_file "$tmp_file")" | |||
old_hash="" | |||
[[ -f "$cert_file" ]] && old_hash="$(sha256_file "$cert_file")" | |||
if [[ -n "$old_hash" && "$new_hash" == "$old_hash" && "$FORCE" -ne 1 ]]; then | |||
log "SSL: cert unchanged; no install/reload." | |||
else | |||
log "SSL: proceeding (force=$FORCE)." | |||
run_root install -o root -g root -m 644 "$tmp_file" "$cert_file" | |||
log "SSL: cert installed." | |||
if systemctl is-active --quiet "$WEB_SERVER"; then | |||
run_root systemctl reload "$WEB_SERVER" | |||
log "SSL: reloaded service: $WEB_SERVER" | |||
local desc | |||
printf -v desc "Installed certificate.\nDomain: %s\nService: %s reloaded.\nForced: %s" \ | |||
"$DYNADOT_DOMAIN" "$WEB_SERVER" "$([[ "$FORCE" -eq 1 ]] && echo yes || echo no)" | |||
discord_send_embed "SSL Monitor" "SSL UPDATED" "$desc" 3066993 | |||
else | |||
log "SSL: WARNING - service not active: $WEB_SERVER" | |||
local desc | |||
printf -v desc "%s not running after SSL update.\nDomain: %s" "$WEB_SERVER" "$DYNADOT_DOMAIN" | |||
discord_send_embed "SSL Monitor" "SSL WARNING" "$desc" 16753920 | |||
fi | |||
fi | |||
local expiry_date expiry_epoch now_epoch days_left last_notified | |||
expiry_date="$(openssl x509 -enddate -noout -in "$cert_file" | cut -d= -f2 || true)" | |||
[[ -n "$expiry_date" ]] || die "Could not read cert expiry from: $cert_file" | |||
expiry_epoch="$(date -d "$expiry_date" +%s)" | |||
now_epoch="$(date +%s)" | |||
days_left=$(( (expiry_epoch - now_epoch) / 86400 )) | |||
log "SSL: expiry: $expiry_date (${days_left} days left)" | |||
last_notified="$(cat "$SSL_NOTIFY_STATE" 2>/dev/null || true)" | |||
if (( days_left <= EXPIRY_WARN_DAYS )); then | |||
if [[ "$last_notified" != "$expiry_date" ]]; then | |||
local desc | |||
printf -v desc "Domain: %s\nExpires in: %s days\nExpiry: %s" "$DYNADOT_DOMAIN" "$days_left" "$expiry_date" | |||
discord_send_embed "SSL Monitor" "SSL EXPIRY WARNING" "$desc" 16753920 | |||
echo "$expiry_date" > "$SSL_NOTIFY_STATE" | |||
log "SSL: expiry warning sent (state updated)." | |||
else | |||
log "SSL: expiry warning already sent for this expiry date." | |||
fi | |||
fi | |||
} | |||
# ===== RUN ===== | |||
if [[ "$DDNS_ONLY" -eq 1 ]]; then | |||
ENABLE_SSL=0 | |||
fi | |||
if [[ "$SSL_ONLY" -eq 1 ]]; then | |||
ENABLE_DDNS=0 | |||
fi | fi | ||
if [ | if [[ "$ENABLE_DDNS" == "1" ]]; then | ||
ddns_task | |||
else | else | ||
log "DDNS: disabled" | |||
fi | fi | ||
if [ "$ | if [[ "$ENABLE_SSL" == "1" ]]; then | ||
# Skip SSL gracefully if env vars aren't present | |||
if has_vars DYNADOT_API_KEY DYNADOT_DOMAIN SSL_DIR WEB_SERVER EXPIRY_WARN_DAYS; then | |||
ssl_task | |||
else | |||
log "SSL: skipped (missing SSL env vars). Tip: add SSL vars or run with --ddns-only." | |||
fi | |||
else | |||
log "SSL: disabled" | |||
fi | fi | ||
log "---- Script finished ----" | |||
echo | |||
</pre> | </pre> | ||
Nuvarande version från 21 januari 2026 kl. 08.35
Dynadot Dynamic DNS (DDNS) Setup
[redigera | redigera wikitext]This page documents how to configure Dynamic DNS for Dynadot and automatically update the A record when the public IP address changes.
Requirements
[redigera | redigera wikitext]- Dynadot account
- Domain using Dynadot DNS
- DDNS password generated in Dynadot
- Ubuntu server with outbound HTTPS access
Dynadot configuration
[redigera | redigera wikitext]1. Enable Dynadot DNS
[redigera | redigera wikitext]- Log in to Dynadot
- Go to My Domains
- Click the domain
- Set DNS → Dynadot DNS
2. Enable Dynamic DNS
[redigera | redigera wikitext]- Domain settings → Dynamic DNS
- Enable DDNS
- Generate a DDNS password
- Save it securely
3. Required values
[redigera | redigera wikitext]| Field | Example |
|---|---|
| Domain | example.com |
| Subdomain | @ or www |
| Record type | A (IPv4) or AAAA (IPv6) |
| DDNS password | (generated in Dynadot) |
Dynadot update endpoint
[redigera | redigera wikitext]Dynadot accepts HTTP GET updates:
https://www.dynadot.com/set_ddns
Parameters:
- domain
- subDomain
- type
- ip
- pwd
- ttl (optional)
Ubuntu auto-update script
[redigera | redigera wikitext]Script: dynadot-ddns.sh
[redigera | redigera wikitext]nano .env
Add this:
[redigera | redigera wikitext]# ===== Dynadot settings ===== # SUBDOMAIN should be empty for root domain DOMAIN=viberq.org SUBDOMAIN= TYPE=A PASSWORD=<hash pw> TTL=300
chmod 600 .env
#!/usr/bin/env bash
set -euo pipefail
# ===== PATHS / CONFIG (override via env) =====
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_LOCAL="${ENV_LOCAL:-$SCRIPT_DIR/.env}"
ENV_DISCORD="${ENV_DISCORD:-$SCRIPT_DIR/../bots/discord/.env}"
LOG_FILE="${LOG_FILE:-$SCRIPT_DIR/log.txt}"
LOG_KEEP="${LOG_KEEP:-5}" # keep log.txt.1 .. log.txt.$LOG_KEEP
STATE_DIR="${STATE_DIR:-$SCRIPT_DIR/state}"
IP_FILE="${IP_FILE:-$STATE_DIR/dynadot_last_ip.txt}"
SSL_NOTIFY_STATE="${SSL_NOTIFY_STATE:-$STATE_DIR/ssl_expiry_last_notified.txt}"
DISCORD_API_VERSION="${DISCORD_API_VERSION:-9}"
ENABLE_DDNS="${ENABLE_DDNS:-1}"
ENABLE_SSL="${ENABLE_SSL:-1}"
# ===== ARGS =====
FORCE=0
DDNS_ONLY=0
SSL_ONLY=0
while [[ $# -gt 0 ]]; do
case "$1" in
--force) FORCE=1; shift ;;
--ddns-only) DDNS_ONLY=1; shift ;;
--ssl-only) SSL_ONLY=1; shift ;;
-h|--help)
cat <<EOF
Usage: $0 [--force] [--ddns-only|--ssl-only]
--force Force update/reload even if unchanged
--ddns-only Run only DDNS task
--ssl-only Run only SSL task
EOF
exit 0
;;
*)
echo "Unknown arg: $1" >&2
exit 2
;;
esac
done
# ===== HELPERS =====
mkdir -p "$STATE_DIR"
rotate_logs() {
local i
for ((i=LOG_KEEP; i>=1; i--)); do
if [[ -f "${LOG_FILE}.${i}" ]]; then
if (( i == LOG_KEEP )); then
rm -f "${LOG_FILE}.${i}"
else
mv -f "${LOG_FILE}.${i}" "${LOG_FILE}.$((i+1))"
fi
fi
done
[[ -f "$LOG_FILE" ]] && mv -f "$LOG_FILE" "${LOG_FILE}.1"
}
log() {
local msg="${1:-}"
printf '%s - %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$msg" >> "$LOG_FILE"
}
die() {
local msg="${1:-}"
log "ERROR: $msg"
echo "ERROR: $msg" >&2
exit 1
}
require_vars() {
local v
for v in "$@"; do
[[ -n "${!v:-}" ]] || die "Missing required env var: $v"
done
}
has_vars() {
local v
for v in "$@"; do
[[ -n "${!v:-}" ]] || return 1
done
return 0
}
load_env_files_shell_safe() {
[[ -f "$ENV_LOCAL" ]] || die "Missing local .env: $ENV_LOCAL"
[[ -f "$ENV_DISCORD" ]] || die "Missing discord .env: $ENV_DISCORD"
# Only use with trusted, shell-safe KEY=value files.
set -a
# shellcheck source=/dev/null
source "$ENV_LOCAL"
# shellcheck source=/dev/null
source "$ENV_DISCORD"
set +a
}
curl_get() {
curl --fail --show-error --silent --location "$1"
}
curl_get_to_file() {
local url="$1"
local out="$2"
curl --fail --show-error --silent --location -o "$out" "$url"
}
run_root() {
if [[ "${EUID:-$(id -u)}" -eq 0 ]]; then
"$@"
else
command -v sudo >/dev/null 2>&1 || die "sudo not found (need root for: $*)"
sudo -n "$@" || die "sudo requires a password (configure non-interactive sudo or run as root): $*"
fi
}
discord_send_embed() {
local author="${1:-Monitor}"
local title="${2:-}"
local desc="${3:-}"
local color="${4:-16753920}"
require_vars DISCORD_BOT_TOKEN LOGS_CHANNEL_ID
local channel_id="$LOGS_CHANNEL_ID"
local payload
payload="$(python3 - "$author" "$title" "$desc" "$color" <<'PY'
import json, sys
author, title, desc, color = sys.argv[1], sys.argv[2], sys.argv[3], int(sys.argv[4])
print(json.dumps({
"embeds": [{
"title": title,
"description": desc,
"color": color,
"author": {"name": author},
}]
}))
PY
)"
curl --fail --show-error --silent \
-H "Authorization: Bot ${DISCORD_BOT_TOKEN}" \
-H "Content-Type: application/json" \
-X POST \
-d "$payload" \
"https://discord.com/api/v${DISCORD_API_VERSION}/channels/${channel_id}/messages" \
> /dev/null || true
}
sha256_file() {
local f="$1"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$f" | awk '{print $1}'
else
shasum -a 256 "$f" | awk '{print $1}'
fi
}
# ===== START =====
rotate_logs
log "---- Script started ----"
load_env_files_shell_safe
require_vars DISCORD_BOT_TOKEN LOGS_CHANNEL_ID
# ===== TASK: DDNS =====
ddns_task() {
require_vars DOMAIN PASSWORD TTL
local current_ip last_ip subdomain type update_url response
current_ip="$(curl_get "https://checkip.amazonaws.com" | tr -d '[:space:]')" || {
local desc
printf -v desc "Could not fetch public IP."
discord_send_embed "DDNS" "DDNS ERROR" "$desc" 15158332
die "Could not fetch public IP"
}
[[ -f "$IP_FILE" ]] || : > "$IP_FILE"
last_ip="$(cat "$IP_FILE" 2>/dev/null || true)"
log "DDNS current IP: $current_ip"
log "DDNS last IP: ${last_ip:-<empty>}"
if [[ "$current_ip" == "$last_ip" && "$FORCE" -ne 1 ]]; then
log "DDNS: IP unchanged; no update."
return 0
fi
log "DDNS: proceeding (force=$FORCE)."
subdomain="${SUBDOMAIN:-}"
type="${TYPE:-A}"
update_url="https://www.dynadot.com/set_ddns?containRoot=true&domain=${DOMAIN}&subDomain=${subdomain}&type=${type}&ip=${current_ip}&pwd=${PASSWORD}&ttl=${TTL}"
response="$(curl_get "$update_url")" || {
local desc
printf -v desc "Dynadot request failed.\nDomain: %s\nOld IP: %s\nNew IP: %s\nForced: %s" \
"$DOMAIN" "${last_ip:-N/A}" "$current_ip" "$([[ "$FORCE" -eq 1 ]] && echo yes || echo no)"
discord_send_embed "DDNS" "DDNS FAIL" "$desc" 15158332
die "Dynadot request failed"
}
log "DDNS Dynadot response: $response"
if echo "$response" | grep -qi "success"; then
echo "$current_ip" > "$IP_FILE"
log "DDNS: IP updated and saved."
local desc
printf -v desc "Domain: %s\nOld IP: %s\nNew IP: %s\nForced: %s" \
"$DOMAIN" "${last_ip:-N/A}" "$current_ip" "$([[ "$FORCE" -eq 1 ]] && echo yes || echo no)"
discord_send_embed "DDNS" "DDNS UPDATED" "$desc" 3066993
else
log "DDNS: update failed."
local desc
printf -v desc "Domain: %s\nOld IP: %s\nNew IP: %s\nForced: %s\nResponse: %s" \
"$DOMAIN" "${last_ip:-N/A}" "$current_ip" "$([[ "$FORCE" -eq 1 ]] && echo yes || echo no)" "$response"
discord_send_embed "DDNS" "DDNS FAIL" "$desc" 15158332
fi
}
# ===== TASK: SSL =====
ssl_task() {
require_vars DYNADOT_API_KEY DYNADOT_DOMAIN SSL_DIR WEB_SERVER EXPIRY_WARN_DAYS
local cert_file key_file tmp_file url new_hash old_hash
cert_file="$SSL_DIR/fullchain.pem"
key_file="$SSL_DIR/privkey.pem"
[[ -f "$key_file" ]] || die "Missing private key at $key_file"
run_root mkdir -p "$SSL_DIR"
tmp_file="$(mktemp)"
trap 'rm -f "$tmp_file"' RETURN
url="https://www.dynadot.com/letsencrypt/download_cert?key=${DYNADOT_API_KEY}&domain=${DYNADOT_DOMAIN}"
if ! curl_get_to_file "$url" "$tmp_file"; then
local desc
printf -v desc "Failed to download certificate.\nDomain: %s" "$DYNADOT_DOMAIN"
discord_send_embed "SSL Monitor" "SSL ERROR" "$desc" 15158332
die "Failed to download certificate"
fi
if ! openssl x509 -noout -in "$tmp_file" >/dev/null 2>&1; then
local desc
printf -v desc "Downloaded file is not a valid x509 cert.\nDomain: %s" "$DYNADOT_DOMAIN"
discord_send_embed "SSL Monitor" "SSL ERROR" "$desc" 15158332
die "Downloaded file invalid"
fi
new_hash="$(sha256_file "$tmp_file")"
old_hash=""
[[ -f "$cert_file" ]] && old_hash="$(sha256_file "$cert_file")"
if [[ -n "$old_hash" && "$new_hash" == "$old_hash" && "$FORCE" -ne 1 ]]; then
log "SSL: cert unchanged; no install/reload."
else
log "SSL: proceeding (force=$FORCE)."
run_root install -o root -g root -m 644 "$tmp_file" "$cert_file"
log "SSL: cert installed."
if systemctl is-active --quiet "$WEB_SERVER"; then
run_root systemctl reload "$WEB_SERVER"
log "SSL: reloaded service: $WEB_SERVER"
local desc
printf -v desc "Installed certificate.\nDomain: %s\nService: %s reloaded.\nForced: %s" \
"$DYNADOT_DOMAIN" "$WEB_SERVER" "$([[ "$FORCE" -eq 1 ]] && echo yes || echo no)"
discord_send_embed "SSL Monitor" "SSL UPDATED" "$desc" 3066993
else
log "SSL: WARNING - service not active: $WEB_SERVER"
local desc
printf -v desc "%s not running after SSL update.\nDomain: %s" "$WEB_SERVER" "$DYNADOT_DOMAIN"
discord_send_embed "SSL Monitor" "SSL WARNING" "$desc" 16753920
fi
fi
local expiry_date expiry_epoch now_epoch days_left last_notified
expiry_date="$(openssl x509 -enddate -noout -in "$cert_file" | cut -d= -f2 || true)"
[[ -n "$expiry_date" ]] || die "Could not read cert expiry from: $cert_file"
expiry_epoch="$(date -d "$expiry_date" +%s)"
now_epoch="$(date +%s)"
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
log "SSL: expiry: $expiry_date (${days_left} days left)"
last_notified="$(cat "$SSL_NOTIFY_STATE" 2>/dev/null || true)"
if (( days_left <= EXPIRY_WARN_DAYS )); then
if [[ "$last_notified" != "$expiry_date" ]]; then
local desc
printf -v desc "Domain: %s\nExpires in: %s days\nExpiry: %s" "$DYNADOT_DOMAIN" "$days_left" "$expiry_date"
discord_send_embed "SSL Monitor" "SSL EXPIRY WARNING" "$desc" 16753920
echo "$expiry_date" > "$SSL_NOTIFY_STATE"
log "SSL: expiry warning sent (state updated)."
else
log "SSL: expiry warning already sent for this expiry date."
fi
fi
}
# ===== RUN =====
if [[ "$DDNS_ONLY" -eq 1 ]]; then
ENABLE_SSL=0
fi
if [[ "$SSL_ONLY" -eq 1 ]]; then
ENABLE_DDNS=0
fi
if [[ "$ENABLE_DDNS" == "1" ]]; then
ddns_task
else
log "DDNS: disabled"
fi
if [[ "$ENABLE_SSL" == "1" ]]; then
# Skip SSL gracefully if env vars aren't present
if has_vars DYNADOT_API_KEY DYNADOT_DOMAIN SSL_DIR WEB_SERVER EXPIRY_WARN_DAYS; then
ssl_task
else
log "SSL: skipped (missing SSL env vars). Tip: add SSL vars or run with --ddns-only."
fi
else
log "SSL: disabled"
fi
log "---- Script finished ----"
echo
Installation
[redigera | redigera wikitext]1. Save script
[redigera | redigera wikitext]sudo nano /usr/local/bin/dynadot-ddns.sh
Paste script and edit variables.
2. Make executable
[redigera | redigera wikitext]sudo chmod +x /usr/local/bin/dynadot-ddns.sh
3. Test manually
[redigera | redigera wikitext]/usr/local/bin/dynadot-ddns.sh
4. Add cron job (every 5 minutes)
[redigera | redigera wikitext]crontab -e
Add:
*/5 * * * * /usr/local/bin/dynadot-ddns.sh
Security notes
[redigera | redigera wikitext]- Store DDNS password securely
- Limit file permissions
- Do not commit the script to public repositories
References
[redigera | redigera wikitext]- Dynadot Help – Dynamic DNS
- Dynadot API documentation