This commit is contained in:
2026-10-29 10:53:37 +01:00
2 changed files with 484 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
# Debian 13: Systemresolver von systemd-resolved auf lokalen BIND9 umstellen
**Stand:** 27. Oktober 2026
**Beispielsystem:** `lists.mx` (produktiver Mail-/Listenserver, Debian 13)
**Skript:** `switch-resolver-to-bind9.sh`
## 1. Ziel und Hintergrund
Auf dem Host läuft bereits ein rekursiver BIND9 auf `127.0.0.1:53` und `[::1]:53`, einschließlich eigener DNSSEC-Validierung (`dnssec-validation auto;`). Bislang nutzten Programme `/etc/resolv.conf -> /run/systemd/resolve/stub-resolv.conf` und damit `127.0.0.53` (`systemd-resolved`), der wiederum an BIND9 weiterleitete.
Beobachtetes Problem: Die IPv6-PTR-Abfrage für `2607:f8b0:4864:20::72e` ergab über `127.0.0.53` `SERVFAIL` mit `DNSSEC validation failed: failed-auxiliary`, über `127.0.0.1` jedoch korrekt `mail-qk1-x72e.google.com.`. Mit `DNSSEC=no` in `systemd-resolved` funktionierte die PTR-Auflösung, aber Anfragen für die absichtlich DNSSEC-fehlerhafte Zone `www.dnssec-failed.org` liefen über `127.0.0.53` weiterhin in einen Timeout. Ein Loopback-Mitschnitt zeigte vielfach wiederholte Abfragen trotz `SERVFAIL`-Antworten von BIND9. Der DNS-Proxy auf `127.0.0.54` übergab dagegen `SERVFAIL` korrekt.
**Ziel:** BIND9 direkt über die glibc-/NSS-DNS-Auflösung nutzen; `systemd-resolved` nach erfolgreichem Test stoppen und deaktivieren. **Keine Änderung** an Maildiensten, BIND-Konfiguration, Interfaces, Firewall oder Routing.
## 2. Zielkonfiguration
`/etc/nsswitch.conf`:
```text
hosts: files myhostname dns
```
`/etc/resolv.conf` (reguläre Datei statt Symlink):
```text
# Local BIND9 recursive resolver; DNSSEC validated by BIND9.
nameserver 127.0.0.1
options edns0 trust-ad
search warenform.de
```
`search warenform.de` ist **systemspezifisch**. Das Skript übernimmt eine bereits vorhandene `search`-Zeile aus der bestehenden Konfiguration; es erfindet keine Suchdomäne. Bei `lists.mx` ist sie bereits gesetzt. `trust-ad` bedeutet **nicht**, dass glibc selbst DNSSEC prüft. Das validierende Glied ist BIND9. Programme mit eigenen Resolver-Bibliotheken können eine abweichende DNS-Konfiguration haben; dies ggf. anwendungsspezifisch prüfen.
Die lokale DNS-Auflösung setzt die Verfügbarkeit von BIND9 voraus. Mit nur `127.0.0.1` gibt es bewusst **keinen externen Fallback**, der lokale DNSSEC-Validierung umgehen könnte.
## 3. Voraussetzungen
- Debian 13, Root-Shell; BIND9 ist installiert, gestartet und lauscht auf IPv4-Loopback `127.0.0.1:53`.
- `dig`, `getent`, `systemctl`, die üblichen Coreutils sowie Bash sind verfügbar.
- BIND9 kann rekursiv auflösen und validiert DNSSEC unabhängig von `systemd-resolved`.
- Änderungen werden in einem Wartungsfenster bzw. mit vorhandener administrativer Konsole vorgenommen. Keine Netzwerkdienste oder Interfaces neu starten.
- Vorhandene dynamische DNS-Konfiguration (DHCP, VPN, Split-DNS, interne Domains) prüfen; das Skript bildet **keine** `systemd-resolved`-Link-Konfiguration nach.
- Eventuelle lokale Abhängigkeiten von `systemd-resolved`, `resolvectl`, LLMNR/mDNS und DNS-Stub-Adressen `127.0.0.53`/`127.0.0.54` vor dem Abschalten ausschließen.
**Achtung:** Auf `lists.mx` wurde `networking.service` zuvor als `failed` gemeldet. Das ist ein **separates** Thema und wird von diesem Skript ausdrücklich nicht behandelt. Insbesondere kein `systemctl restart networking` ausführen.
## 4. DNSSEC-Vorabprüfung
```bash
dig @127.0.0.1 cloudflare.com A +dnssec +adflag
dig @127.0.0.1 www.dnssec-failed.org A +time=5 +tries=1
dig @127.0.0.1 -x 2607:f8b0:4864:20::72e
```
Erwartung: `cloudflare.com` → `NOERROR` und Flag `ad`, `www.dnssec-failed.org` → `SERVFAIL` (nicht Timeout), PTR → `mail-qk1-x72e.google.com.` (Stand des beobachteten Beispiels; DNS-Daten können sich ändern). Das Skript führt einen Positiv- und Negativ-DNSSEC-Test zusätzlich selbst aus. Die Testdomain ist absichtlich fehlerhaft signiert.
## 5. Skriptinstallation und Verwendung
Datei an einen geeigneten Ort kopieren, beispielsweise:
```bash
install -o root -g root -m 0755 switch-resolver-to-bind9.sh \
/usr/local/sbin/switch-resolver-to-bind9.sh
bash -n /usr/local/sbin/switch-resolver-to-bind9.sh
# Nur lesen und prüfen (Standard):
/usr/local/sbin/switch-resolver-to-bind9.sh --check
# Umstellung mit interaktiver Bestätigung (exakt 'yes'):
/usr/local/sbin/switch-resolver-to-bind9.sh --apply
# Rückbau des durch dieses Skript vorgenommenen Zustands:
/usr/local/sbin/switch-resolver-to-bind9.sh --rollback
```
Statusausgaben verwenden die bekannten Muster `[ Info ]`, `[ ok ]`, `[ failed ]`, `[ skipped ]`. Keine automatische Paketinstallation und kein Reboot. Die Änderung erfolgt **nicht** schon beim Aufruf ohne Parameter.
### Was `--apply` tut
1. Prüft Root-Rechte, benötigte Kommandos und BIND9; DNSSEC-Positiv- und Negativtest.
2. Zeigt den geplanten Eingriff und verlangt `Einverstanden [yes/no, default: no]?`.
3. Legt eine Sicherung von **Datei bzw. Symlink** `/etc/resolv.conf`, `/etc/nsswitch.conf` und dem bisherigen `systemd-resolved`-Dienststatus unter `/var/backups/oopen-bind9-direct-resolver/run-.../` an (Verzeichnis nur für Root).
4. Ändert, soweit erforderlich, die `hosts:`-NSS-Zeile und erstellt `/etc/resolv.conf` mit `nameserver 127.0.0.1`. Eine vorhandene `search`-Zeile wird übernommen.
5. Prüft normale Vorwärts- und Reverse-Auflösung.
6. Stoppt und deaktiviert `systemd-resolved` und prüft BIND9/DNSSEC erneut.
7. Bei einem Fehler versucht das Skript automatisch einen Rückbau der gesicherten Dateien und des gespeicherten Dienstzustands. Ein automatischer Rückbau ist eine Schutzmaßnahme, **keine Garantie** bei Stromausfall oder hartem Prozessabbruch.
Bereits vollständig umgestellte Systeme werden ohne Änderungen mit `[ skipped ]` erkannt. Existiert noch ein offener, vom Skript angelegter Rückbauzustand, bricht ein weiterer `--apply`-Lauf ab. So wird verhindert, dass die ursprüngliche Sicherung unbemerkt überschrieben wird.
### Besonderheit bei `lists.mx` (bereits manuell umgestellt)
Auf `lists.mx` wurden `/etc/nsswitch.conf` und `/etc/resolv.conf` am 27.10.2026 bereits manuell auf direkten BIND9 umgestellt. Das Skript erkennt dies und kann bei `--apply` den Dienst `systemd-resolved` noch stoppen und deaktivieren, sofern dies noch nicht erfolgt ist. **Der Skript-Rollback stellt dann den Zustand unmittelbar vor diesem Skriptlauf wieder her, nicht automatisch den historischen systemd-resolved-Zustand vom Morgen.**
Die zuvor manuell erstellte Sicherung liegt unter:
```text
/root/dns-resolver-backup-20261027-120829
```
Dort ist `resolv.conf` ein relativer Symlink. Er muss bei manueller Wiederherstellung an seinem **ursprünglichen Ort** `/etc/resolv.conf` wieder angelegt werden. Der historische Zustand lässt sich mit gesichertem `nsswitch.conf` und dem ursprünglichen Symlink wiederherstellen; dafür `systemd-resolved` zuerst wieder aktivieren.
## 6. Abschließende Prüfungen
```bash
cat /etc/resolv.conf
grep '^hosts:' /etc/nsswitch.conf
systemctl is-active bind9
systemctl is-active systemd-resolved
systemctl is-enabled systemd-resolved
getent ahostsv4 oopen.de
getent hosts 2607:f8b0:4864:20::72e
dig cloudflare.com A +dnssec +adflag
dig www.dnssec-failed.org A +time=5 +tries=1
```
Erwartung: BIND9 `active`, `systemd-resolved` `inactive` und `disabled`, erfolgreiche normale Auflösung, DNSSEC-Positivtest `ad`, Negativtest `SERVFAIL`. Bei `dig` muss `SERVER: 127.0.0.1#53` erscheinen. Zusätzlich Mail-Logs und anwendungsspezifische Namensauflösung kontrollieren. Änderungen bestehen über einen Neustart fort, **sofern** kein anderer Netzwerkdienst `/etc/resolv.conf` ersetzt und BIND9 beim Boot zuverlässig startet; einen Neustart des Produktivservers führt das Skript nicht aus.
## 7. Rollback
Falls `--apply` dieses Skripts zuvor erfolgreich gelaufen ist:
```bash
/usr/local/sbin/switch-resolver-to-bind9.sh --rollback
```
Das stellt die bei **diesem Skriptlauf** gesicherten Dateien und den Dienststatus nach Bestätigung wieder her. Die Sicherungsdateien bleiben archiviert. Falls die Dateien **vor dem ersten Skriptlauf** schon direkt BIND9 benutzten, bringt dieser Rollback folgerichtig nicht den ursprünglichen systemd-Stub zurück.
**Manueller Rückbau auf `lists.mx` in den früheren Zustand** (nur bei gezieltem Bedarf und mit konsistenter DNS-Prüfung):
```bash
# Ursprünglichen NSS-Zustand wiederherstellen:
cp -a /root/dns-resolver-backup-20261027-120829/nsswitch.conf /etc/nsswitch.conf
# Dienst starten, Stub-Datei prüfen:
systemctl enable --now systemd-resolved
test -f /run/systemd/resolve/stub-resolv.conf
# Ziel-Symlink atomar am ursprünglichen Ort wiederherstellen:
ln -s /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf.rollback-link
mv -Tf /etc/resolv.conf.rollback-link /etc/resolv.conf
# Ergebnis prüfen:
getent ahostsv4 oopen.de
dig -x 2607:f8b0:4864:20::72e
```
Die frühere Einstellung `DNSSEC=no` in `/etc/systemd/resolved.conf.d/90-local-bind9.conf` bleibt bei diesem manuellen Rückbau erhalten, solange sie nicht separat geändert wird. DNSSEC bleibt dann bei BIND9 aktiv.
## 8. Grenzen und Sicherheit
- Das Skript setzt einen bereits gesunden lokalen BIND9 voraus und ist **kein** BIND9-Installationsskript.
- Kein automatisches Umschalten auf öffentliche Resolver; bei BIND-Ausfall würde lokale DNS-Auflösung ausfallen.
- Ein vereinfachter `hosts:`-Eintrag `files myhostname dns` ist auf diesem Server geprüft. Systeme mit weiteren NSS-Modulen wie `mymachines`, `mdns` oder LDAP benötigen eine angepasste Variante.
- `systemd-resolved` kann von speziellen Diensten per D-Bus oder `resolvectl` verwendet werden. Das muss vor dem Deaktivieren separat berücksichtigt werden.
- `systemd-resolved` wird nur gestoppt/deaktiviert, **nicht deinstalliert oder maskiert**.
- Bei fehlgeschlagenem Rollback nicht mehrfach blind ausführen: Ausgabe und gesichertes Backup-Verzeichnis überprüfen.
## 9. Quellen
- [systemd-resolved – Modes für /etc/resolv.conf](https://www.freedesktop.org/software/systemd/man/latest/systemd-resolved.service.html)
- [nss-resolve – NSS-Reihenfolge und systemd-resolved](https://www.freedesktop.org/software/systemd/man/latest/nss-resolve.html)
- [Debian: resolvconf-Kompatibilitätsmodus von resolvectl](https://manpages.debian.org/trixie/systemd-resolved/resolvconf.1.en.html)
+321
View File
@@ -0,0 +1,321 @@
#!/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|--repair-postfix|--help) ;; *) echo "Usage: $0 [--check|--apply|--rollback|--repair-postfix|--help]" >&2; exit 2;; esac
# Colorize status labels on terminals; respect NO_COLOR and redirected output.
if [[ -t 1 && ! -v NO_COLOR && "${TERM:-}" != dumb ]]; then
GREEN=$'\033[32m'
GREY=$'\033[90m'
RED=$'\033[31m'
YELLOW=$'\033[33m'
RESET=$'\033[0m'
else
GREEN='' GREY='' RED='' YELLOW='' RESET=''
fi
info() { printf '[ Info ] %s\n' "$*"; }
ok() { printf '%s[ ok ]%s %s\n' "$GREEN" "$RESET" "$*"; }
skipped() { printf '%s[ skipped ]%s %s\n' "$GREY" "$RESET" "$*"; }
failed() { printf '%s[ failed ]%s %s\n' "$RED" "$RESET" "$*" >&2; }
# Delimited summary at the end of --check / before an interactive --apply.
status_banner(){
local color="$1" message="$2" detail="${3:-}"
printf '\n%s------------------------------------------------------------%s\n' "$color" "$RESET"
printf '%s%s%s\n' "$color" "$message" "$RESET"
[[ -z "$detail" ]] || printf '%s%s%s\n' "$color" "$detail" "$RESET"
printf '%s------------------------------------------------------------%s\n' "$color" "$RESET"
}
# Every invocation finishes with one additional empty output line.
trap 'printf "\n"' EXIT
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; failed 'Local BIND9 does not resolve oopen.de'; return 1; 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; failed 'BIND9 DNSSEC positive test failed'; return 1; 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; failed 'BIND9 does not reject invalid DNSSEC test zone'; return 1; fi
}
# Debian Postfix: systemd instance startup refreshes the chroot resolver files.
# This is deliberately limited to running Postfix instances. No data_directory
# files are ever removed. A queue flush remains a separate admin decision.
postfix_units(){
local unit
while read -r unit _; do
[[ "$unit" == postfix@*.service ]] || continue
printf '%s\n' "$unit"
done < <(systemctl list-units --type=service --state=active --no-legend --plain 'postfix@*.service' 2>/dev/null)
}
sync_postfix(){
local -a units=()
local unit queue_dir chroot_resolv
if ! command -v postconf >/dev/null 2>&1; then
skipped 'Postfix not installed; no MTA restart required'
return 0
fi
mapfile -t units < <(postfix_units)
if (( ${#units[@]} == 0 )); then
if systemctl is-active --quiet postfix.service; then
units=(postfix.service)
else
skipped 'No active Postfix service; no MTA restart required'
return 0
fi
fi
info 'Restarting active Postfix systemd service(s) to refresh chroot DNS'
for unit in "${units[@]}"; do
systemctl restart "$unit" || { failed "Postfix restart failed: $unit"; return 1; }
if ! systemctl is-active --quiet "$unit"; then
failed "Postfix service not active after restart: $unit"
return 1
fi
ok "Postfix restarted: $unit"
done
# Only assert a chroot resolver copy when it exists; custom setups may
# use multiple queue directories or run SMTP clients without chroot.
queue_dir=$(postconf -h queue_directory 2>/dev/null || true)
if [[ -n "$queue_dir" && -f "$queue_dir/etc/resolv.conf" ]]; then
chroot_resolv="$queue_dir/etc/resolv.conf"
if cmp -s "$RESOLV" "$chroot_resolv"; then
ok "Postfix chroot resolver synchronized: $chroot_resolv"
else
failed "Postfix chroot resolver differs: $chroot_resolv"
failed 'Check Debian postfix configure-instance.sh / active instances'
return 1
fi
else
info 'No default Postfix chroot resolv.conf found (possibly non-chroot or custom layout)'
fi
info 'Deferred mail is not flushed automatically; if appropriate run: postqueue -f'
}
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)"
local readiness=0
if preflight; then readiness=1; fi
if is_fully_switched; then
if (( readiness )); then
status_banner "$GREY" 'Umstellung auf BIND9 bereits durchgeführt.'
else
status_banner "$YELLOW" 'Umstellung bereits durchgeführt, aber DNS-Prüfung fehlgeschlagen.'
fi
elif [[ -f "$CURRENT_FILE" ]]; then
status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' \
'Ein vorheriger Rollback-Stand ist vorhanden; zunächst prüfen oder zurückbauen.'
elif (( readiness )); then
status_banner "$GREEN" 'Umstellung kann durchgeführt werden.' \
"Ausführen mit: $0 --apply"
else
status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' \
'BIND9-/DNSSEC-Voraussetzungen nicht erfüllt; bitte Fehler prüfen.'
fi
if [[ -f "$CURRENT_FILE" ]]; then
printf '\n=== Rollback information ===\n'
info 'Backup from before the last --apply is available'
info "Backup: $(state_dir_from_marker)"
info "Restore with: $0 --rollback"
fi
}
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'
sync_postfix
}
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 status_banner "$GREY" 'Umstellung auf BIND9 bereits durchgeführt.'; skipped 'No changes'; exit 0; fi
status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' 'Vorheriger Rollback-Stand vorhanden.'
fatal 'An earlier script run has rollback state. Use --check or --rollback first.';
}
if is_fully_switched; then status_banner "$GREY" 'Umstellung auf BIND9 bereits durchgeführt.'; skipped 'No changes'; exit 0; fi
info 'Preflight checks (no changes)'
if ! preflight; then
status_banner "$YELLOW" 'Umstellung kann nicht ausgeführt werden.' 'BIND9-/DNSSEC-Voraussetzungen nicht erfüllt.'
return 1
fi
status_banner "$GREEN" 'Umstellung kann durchgeführt werden.' \
"Ausführen mit: $0 --apply"
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 and BIND9 are unchanged; active Postfix will restart.'
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
sync_postfix
trap - ERR INT TERM
ok 'Switch completed successfully'
info "Rollback available: $0 --rollback"
}
repair_postfix(){
require_root
need systemctl; need postconf; need cmp
if ! is_direct; then
fatal 'Resolver is not configured for direct BIND9; use --check first'
fi
if ! preflight; then
fatal 'Local DNSSEC preflight failed; Postfix unchanged'
fi
sync_postfix
ok 'Postfix DNS synchronization completed'
}
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|--repair-postfix|--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
--repair-postfix restart active Postfix instance(s) to refresh chroot DNS
--help this help
After Postfix restart: consider postqueue -f to retry deferred messages.
Notes: run as root; requires BIND9 already working; no package changes.
HELP
;;
--check) show_check ;;
--apply) apply ;;
--rollback) rollback ;;
--repair-postfix) repair_postfix ;;
esac