Files
bind/switch-resolver-to-bind9.sh
T

204 lines
8.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Debian 13: switch system name resolution from systemd-resolved to local BIND9.
# No package installation, BIND configuration or networking changes.
set -Eeuo pipefail
umask 077
readonly STATE_DIR=/var/backups/oopen-bind9-direct-resolver
readonly CURRENT_FILE="$STATE_DIR/current"
readonly RESOLV=/etc/resolv.conf
readonly NSS=/etc/nsswitch.conf
readonly EXPECTED_NSS='hosts: files myhostname dns'
MODE="${1:---check}"
case "$MODE" in --check|--apply|--rollback|--help) ;; *) echo "Usage: $0 [--check|--apply|--rollback|--help]" >&2; exit 2;; esac
info() { printf '[ Info ] %s\n' "$*"; }
ok() { printf '[ ok ] %s\n' "$*"; }
skipped() { printf '[ skipped ] %s\n' "$*"; }
failed() { printf '[ failed ] %s\n' "$*" >&2; }
fatal() { failed "$*"; exit 1; }
echononl(){ printf '%-56s' "$*"; }
require_root(){ [[ "${EUID}" -eq 0 ]] || fatal 'Please run as root.'; }
need(){ command -v "$1" >/dev/null 2>&1 || fatal "Missing command: $1"; }
confirm(){ local answer; read -r -p 'Einverstanden [yes/no, default: no]? ' answer || true; [[ "$answer" == yes ]]; }
# Only this script's state file controls rollback. Older manual backups are untouched.
# Each run's backup retains the exact file or symlink, along with active/enabled state.
state_dir_from_marker(){
[[ -f "$CURRENT_FILE" ]] || return 1
local p
IFS= read -r p < "$CURRENT_FILE" || true
[[ "$p" == "$STATE_DIR"/run-* && -d "$p" && -f "$p/metadata" ]] || fatal 'Invalid resolver rollback marker.'
printf '%s\n' "$p"
}
is_direct(){
[[ -f "$RESOLV" && ! -L "$RESOLV" ]] &&
[[ "$(awk '$1=="nameserver" {print $2}' "$RESOLV")" == '127.0.0.1' ]] &&
grep -Eq '^[[:space:]]*hosts:[[:space:]]*files[[:space:]]+myhostname[[:space:]]+dns[[:space:]]*$' "$NSS"
}
is_fully_switched(){
is_direct && ! systemctl is-active --quiet systemd-resolved &&
[[ "$(systemctl is-enabled systemd-resolved 2>/dev/null || true)" == disabled ]]
}
# BIND must reject a known bogus signature, not merely answer normal queries.
preflight(){
echononl 'BIND9 on 127.0.0.1:53'
if dig @127.0.0.1 oopen.de A +time=3 +tries=1 +short | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then ok 'reachable'; else echo; fatal 'Local BIND9 does not resolve oopen.de'; fi
echononl 'DNSSEC positive test (AD flag)'
local reply
reply=$(dig @127.0.0.1 cloudflare.com A +dnssec +adflag +time=5 +tries=1)
if grep -q 'status: NOERROR' <<< "$reply" && grep -Eq 'flags:.*[[:space:]]ad[;[:space:]]' <<< "$reply"; then ok 'validated'; else echo; fatal 'BIND9 DNSSEC positive test failed'; fi
echononl 'DNSSEC negative test (SERVFAIL)'
reply=$(dig @127.0.0.1 www.dnssec-failed.org A +time=5 +tries=1)
if grep -q 'status: SERVFAIL' <<< "$reply"; then ok 'rejected as expected'; else echo; fatal 'BIND9 does not reject invalid DNSSEC test zone'; fi
}
show_check(){
require_root
need dig; need systemctl; need getent
printf '\n=== DNS resolver status ===\n'
printf 'resolv.conf: '; if [[ -L "$RESOLV" ]]; then readlink "$RESOLV"; else echo 'regular file'; fi
grep -E '^[[:space:]]*(nameserver|search|options)[[:space:]]' "$RESOLV" || true
grep -E '^hosts:' "$NSS" || true
printf 'systemd-resolved active: %s\n' "$(systemctl is-active systemd-resolved 2>/dev/null || true)"
printf 'systemd-resolved enabled: %s\n' "$(systemctl is-enabled systemd-resolved 2>/dev/null || true)"
if is_fully_switched; then ok 'Already directly using BIND9 (resolved disabled)';
elif is_direct; then info 'BIND9 direct configuration is present; resolved still active/enabled';
else info 'Not fully configured for direct BIND9'; fi
if [[ -f "$CURRENT_FILE" ]]; then info "Rollback state: $(state_dir_from_marker)"; fi
preflight
}
restore_files(){
local path="$1"
[[ -d "$path" && -e "$path/nsswitch.conf" ]] || fatal 'Incomplete backup: NSS'
[[ -e "$path/resolv.conf" || -L "$path/resolv.conf" ]] || fatal 'Incomplete backup: resolv.conf'
cp -a --remove-destination "$path/nsswitch.conf" "$NSS"
cp -a --remove-destination "$path/resolv.conf" "$RESOLV"
}
restore_services(){
local path="$1" enabled active
enabled=$(sed -n 's/^resolved_enabled=//p' "$path/metadata")
active=$(sed -n 's/^resolved_active=//p' "$path/metadata")
case "$enabled" in
enabled|enabled-runtime) systemctl enable systemd-resolved >/dev/null ;;
disabled) systemctl disable systemd-resolved >/dev/null ;;
static|indirect|masked|alias|'') info "Previous enabled state '$enabled'; no enable-state change" ;;
*) info "Unrecognized previous enabled state '$enabled'; no enable-state change" ;;
esac
case "$active" in
active) systemctl start systemd-resolved ;;
inactive|failed) systemctl stop systemd-resolved ;;
*) info "Previous active state '$active'; no service-state change" ;;
esac
}
rollback_to(){
local path="$1"
info "Restoring config from $path"
# Stop resolved first only when it was previously not active; otherwise start after files.
restore_files "$path"
restore_services "$path"
ok 'Configuration and resolved service state restored'
}
apply(){
require_root
for cmd in dig getent systemctl cp mv mktemp grep awk sed date; do need "$cmd"; done
[[ -f "$NSS" ]] || fatal "$NSS missing"
[[ -e "$RESOLV" || -L "$RESOLV" ]] || fatal "$RESOLV missing"
[[ ! -f "$CURRENT_FILE" ]] || {
if is_fully_switched; then skipped 'Already installed; no changes'; exit 0; fi
fatal 'An earlier script run has rollback state. Use --check or --rollback first.';
}
if is_fully_switched; then skipped 'Already directly using BIND9; no changes'; exit 0; fi
info 'Preflight checks (no changes)'
preflight
printf '\nPlanned changes:\n hosts: files myhostname dns\n resolv.conf: nameserver 127.0.0.1\n stop and disable systemd-resolved\n'
info 'Network interfaces, routes, BIND9 and MTA are not changed.'
confirm || { skipped 'Abbruch durch User'; exit 0; }
mkdir -p "$STATE_DIR"
chmod 700 "$STATE_DIR"
local path timestamp
timestamp=$(date +%Y%m%d-%H%M%S)
path=$(mktemp -d "$STATE_DIR/run-${timestamp}-XXXXXX")
cp -a "$RESOLV" "$path/resolv.conf"
cp -a "$NSS" "$path/nsswitch.conf"
{
printf 'resolved_active=%s\n' "$(systemctl is-active systemd-resolved 2>/dev/null || true)"
printf 'resolved_enabled=%s\n' "$(systemctl is-enabled systemd-resolved 2>/dev/null || true)"
} > "$path/metadata"
printf '%s\n' "$path" > "$CURRENT_FILE"
info "Backup: $path"
# Roll back this transaction automatically if a command or signal fails.
trap 'rc=$?; trap - ERR INT TERM; failed "Apply failed (exit $rc). Rolling back"; rollback_to "$path" || failed "Automatic rollback failed; backup: $path"; exit "$rc"' ERR
trap 'exit 130' INT
trap 'exit 143' TERM
if ! grep -Eq '^[[:space:]]*hosts:[[:space:]]*files[[:space:]]+myhostname[[:space:]]+dns[[:space:]]*$' "$NSS"; then
sed -i -E '/^[[:space:]]*hosts:/c\hosts: files myhostname dns' "$NSS"
else skipped 'NSS already configured'; fi
if [[ ! -f "$RESOLV" || -L "$RESOLV" ]] || ! grep -Eq '^nameserver[[:space:]]+127\.0\.0\.1$' "$RESOLV"; then
local search_domain=""
search_domain=$(awk '$1=="search" { $1=""; sub(/^[[:space:]]+/, ""); print; exit }' "$RESOLV" 2>/dev/null || true)
rm -f "$RESOLV"
{
printf '# Local BIND9 recursive resolver; DNSSEC validated by BIND9.\n'
printf 'nameserver 127.0.0.1\noptions edns0 trust-ad\n'
[[ -z "$search_domain" ]] || printf 'search %s\n' "$search_domain"
} > "$RESOLV"
chmod 644 "$RESOLV"
else skipped 'resolv.conf already uses local BIND9'; fi
echononl 'NSS forward resolution'
getent ahostsv4 oopen.de >/dev/null && ok 'works' || { echo; false; }
echononl 'NSS reverse resolution'
getent hosts 2607:f8b0:4864:20::72e >/dev/null && ok 'works' || { echo; false; }
echononl 'Resolver configuration'
is_direct && ok 'direct BIND9' || { echo; false; }
systemctl stop systemd-resolved
systemctl disable systemd-resolved >/dev/null
echononl 'systemd-resolved disabled'
is_fully_switched && ok 'yes' || { echo; false; }
preflight
trap - ERR INT TERM
ok 'Switch completed successfully'
info "Rollback available: $0 --rollback"
}
rollback(){
require_root
local path
path=$(state_dir_from_marker) || fatal 'No rollback state from this script exists'
info "Rollback backup: $path"
confirm || { skipped 'Abbruch durch User'; exit 0; }
rollback_to "$path"
rm -f "$CURRENT_FILE"
ok 'Rollback completed; preserved backup remains in place'
}
case "$MODE" in
--help)
cat <<'HELP'
Usage: switch-resolver-to-bind9.sh [--check|--apply|--rollback|--help]
--check read-only checks (default)
--apply verify BIND9 DNSSEC; back up; switch DNS/NSS; stop+disable resolved
--rollback restore files and resolved service state saved before last --apply
--help this help
Notes: run as root; requires BIND9 already working; no package changes.
HELP
;;
--check) show_check ;;
--apply) apply ;;
--rollback) rollback ;;
esac