#!/usr/bin/env bash # Script Name: dns-watch.v1.sh # Beschreibung: Überwacht A/AAAA DNS-Records für definierte Hosts und meldet Änderungen per Mail und/oder ntfy. # Autor: Patrick Asmus # Web: https://www.cleveradmin.de # Git-Reposit.: https://git.techniverse.net/scriptos/dns-watch # Version: 1.0 # Datum: 18.08.2025 # Modifikation: Initial ##################################################### set -euo pipefail ##################################### # KONFIG # ##################################### # Liste der zu überwachenden Hosts HOSTS=( "domain.com" "sub.domain.com" ) # Welche Record-Typen prüfen? RECORD_TYPES=("A" "AAAA") # Optional: Eigener DNS-Resolver (leer = System-Resolver) # Beispielformat: "1.1.1.1" oder "9.9.9.9" DNS_RESOLVER="1.1.1.1" # Zustandsablage (wird automatisch erstellt) STATE_DIR="./states" # Logdatei (optional; leer lassen, wenn nur stdout) LOG_FILE="/var/log/dns-watch.log" # --- Benachrichtigungen --- # Mail MAIL_ENABLED=false MAIL_TO="" MAIL_FROM="" MAIL_SUBJECT_PREFIX="[DNS-Watch]" MAIL_BIN="${MAIL_BIN:-/usr/bin/mail}" # /usr/bin/mail (bsd-mailx / mailutils / msmtp-mta) # ntfy NTFY_ENABLED=true NTFY_SERVER="https://ntfy.sh" NTFY_TOPIC="dns-watch" NTFY_TOKEN="" NTFY_TITLE_PREFIX="[DNS-Watch]" NTFY_PRIORITY="default" # options: min|low|default|high|max NTFY_TAGS="satellite" # Komma-getrennt, z. B. "satellite,dns,warning" # Lockfile gegen Parallelstarts LOCK_FILE="/tmp/dns-watch.lock" ##################################### # HILFSFUNKTIONEN # ##################################### log() { local ts ts="$(date '+%Y-%m-%d %H:%M:%S')" local line="[$ts] $*" if [[ -n "${LOG_FILE}" ]]; then echo "${line}" >> "${LOG_FILE}" else echo "${line}" fi } with_lock() { exec 9>"${LOCK_FILE}" if ! flock -n 9; then log "Bereits laufende Instanz erkannt. Beende." exit 0 fi } ensure_dirs() { mkdir -p "${STATE_DIR}" if [[ -n "${LOG_FILE}" ]]; then mkdir -p "$(dirname "${LOG_FILE}")" touch "${LOG_FILE}" fi } # DNS-Abfrage für Host/Typ, gibt sortierte, eindeutige Liste (zeilenweise) zurück. resolve_records() { local host="$1" local rtype="$2" local dig_args=("+short" "${host}" "${rtype}") if [[ -n "${DNS_RESOLVER}" ]]; then dig_args=("@${DNS_RESOLVER}" "${host}" "${rtype}" "+short") fi # Ausführen, IPv4/IPv6-Adressen filtern, sortieren, eindeutige Einträge local out if ! out="$(dig "${dig_args[@]}" 2>/dev/null | sed 's/\s\+$//' | grep -E '^[0-9a-fA-F:\.]+$' || true)"; then out="" fi if [[ -z "${out}" ]]; then # Als Platzhalter "EMPTY" speichern, damit Änderungen erkennbar sind echo "EMPTY" else # Sortierte, eindeutige Liste echo "${out}" | sort -u fi } # Zustandspfad für Host/Typ state_file_path() { local host="$1" local rtype="$2" local safe_host safe_host="$(echo -n "${host}" | tr '/:@' '___')" echo "${STATE_DIR}/${safe_host}__${rtype}.state" } # Vergleicht Alt/Neu; gibt 0 zurück, wenn identisch compare_sets() { local old_file="$1" local tmp_new="$2" # Falls kein Altzustand existiert, als Unterschied werten if [[ ! -s "${old_file}" ]]; then return 1 fi # diff -q gibt 0 zurück, wenn gleich; sonst != 0 if diff -q "${old_file}" "${tmp_new}" >/dev/null 2>&1; then return 0 else return 1 fi } ##################################### # BENACHRICHTIGUNGEN # ##################################### notify_mail() { # usage: notify_mail "Betreff" "Nachrichtentext" local subject="$1" local body="$2" if [[ "${MAIL_ENABLED}" != "true" ]]; then return 0 fi if [[ ! -x "${MAIL_BIN}" ]]; then log "WARN: ${MAIL_BIN} nicht gefunden/ausführbar, Mailbenachrichtigung übersprungen." return 0 fi # Versuch über -a "From:" Header (bei mailutils/bsd-mailx/msmtp-mta häufig verfügbar) { echo -e "${body}" } | "${MAIL_BIN}" -a "From: ${MAIL_FROM}" -s "${MAIL_SUBJECT_PREFIX} ${subject}" "${MAIL_TO}" || { log "WARN: Mailversand fehlgeschlagen." } } notify_ntfy() { # usage: notify_ntfy "Titel" "Body" "priority" "tags" local title="$1" local body="$2" local prio="${3:-${NTFY_PRIORITY}}" local tags="${4:-${NTFY_TAGS}}" if [[ "${NTFY_ENABLED}" != "true" ]]; then return 0 fi if [[ -z "${NTFY_SERVER}" || -z "${NTFY_TOPIC}" ]]; then log "WARN: ntfy SERVER/TOPIC nicht gesetzt, ntfy-Notify übersprungen." return 0 fi local url="${NTFY_SERVER%/}/${NTFY_TOPIC}" # Headers: # Title: Betreffzeile # Priority: min|low|default|high|max # Tags: emoji/labels (kommagetrennt) # Authorization: Bearer local auth_header=() if [[ -n "${NTFY_TOKEN}" && "${NTFY_TOKEN}" != "PLACE_YOUR_NTFY_API_TOKEN_HERE" ]]; then auth_header=(-H "Authorization: Bearer ${NTFY_TOKEN}") fi curl -sS -X POST \ -H "Title: ${NTFY_TITLE_PREFIX} ${title}" \ -H "Priority: ${prio}" \ -H "Tags: ${tags}" \ "${auth_header[@]}" \ --data-binary "${body}" \ "${url}" >/dev/null || { log "WARN: ntfy-POST fehlgeschlagen." } } ##################################### # LOGIK # ##################################### process_host_type() { local host="$1" local rtype="$2" local new_records new_records="$(resolve_records "${host}" "${rtype}")" # Temporäre Datei für neuen Zustand local tmp_new tmp_new="$(mktemp)" printf "%s\n" "${new_records}" > "${tmp_new}" local state_file state_file="$(state_file_path "${host}" "${rtype}")" if compare_sets "${state_file}" "${tmp_new}"; then # Keine Änderung rm -f "${tmp_new}" return 0 fi # Änderung erkannt local old="(kein vorheriger Zustand)" if [[ -s "${state_file}" ]]; then old="$(cat "${state_file}")" fi local now now="$(cat "${tmp_new}")" # State aktualisieren mv -f "${tmp_new}" "${state_file}" # Meldung bauen local subject="Änderung: ${host} ${rtype}" local body body=$(cat <