Hoppa till innehållet

Dynadot

Från Plutten
Version från den 21 januari 2026 kl. 08.35 av 192.168.1.1 (diskussion) (Add this:)
(skillnad) ← Äldre version | Nuvarande version (skillnad) | Nyare version → (skillnad)

Mall:Short description

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]
  1. Log in to Dynadot
  2. Go to My Domains
  3. Click the domain
  4. Set DNSDynadot DNS

2. Enable Dynamic DNS

[redigera | redigera wikitext]
  1. Domain settings → Dynamic DNS
  2. Enable DDNS
  3. Generate a DDNS password
  4. 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
# ===== 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
  • Dynadot Help – Dynamic DNS
  • Dynadot API documentation