278 lines
6.4 KiB
Bash
278 lines
6.4 KiB
Bash
#!/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 <token>
|
|
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 <<EOF
|
|
DNS-Änderung erkannt
|
|
|
|
Host: ${host}
|
|
Typ: ${rtype}
|
|
Zeit: $(date -Iseconds)
|
|
|
|
Alt:
|
|
${old}
|
|
|
|
Neu:
|
|
${now}
|
|
|
|
Resolver: ${DNS_RESOLVER:-system-default}
|
|
EOF
|
|
)
|
|
|
|
log "Änderung erkannt für ${host} ${rtype}. Sende Benachrichtigungen."
|
|
notify_mail "${subject}" "${body}"
|
|
notify_ntfy "${subject}" "${body}" "${NTFY_PRIORITY}" "${NTFY_TAGS}"
|
|
}
|
|
|
|
main() {
|
|
with_lock
|
|
ensure_dirs
|
|
|
|
for host in "${HOSTS[@]}"; do
|
|
for rt in "${RECORD_TYPES[@]}"; do
|
|
process_host_type "${host}" "${rt}"
|
|
done
|
|
done
|
|
}
|
|
|
|
main "$@"
|