Files
server-scripts/dns-override.sh
Jose Henrique 4976227ce8
All checks were successful
Check scripts syntax / check-scripts-syntax (push) Successful in 4s
updating dns override
2026-05-16 11:16:59 -03:00

817 lines
22 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_NAME="dns-override.sh"
readonly SCRIPT_VERSION="1.0.2"
readonly IMMUTABLE_PROMPT_TIMEOUT_SEC=20
readonly NC='\033[0m'
readonly RED='\033[1;31m'
readonly GREEN='\033[1;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[1;34m'
readonly GREY='\033[0;37m'
readonly EMOJI_INFO=''
readonly EMOJI_OK='✅'
readonly EMOJI_WARN='⚠️'
readonly EMOJI_ERROR='❌'
readonly EMOJI_GEAR='🛠️'
readonly EMOJI_ROCKET='🚀'
readonly EMOJI_NET='🌐'
readonly EMOJI_LOCK='🔒'
SUDO_BIN=""
IS_REMOTE_SSH=0
IMMUTABLE_LOCK_REQUESTED=0
declare -a DNS_SERVERS=()
declare -a DETECTED_MANAGERS=()
declare -a ACTIONS_ATTEMPTED=()
declare -a ACTIONS_SUCCEEDED=()
declare -a ACTIONS_FAILED=()
declare -a ACTIONS_SKIPPED=()
log_info() { printf "%b%s %s%b\n" "$GREY" "$EMOJI_INFO" "$1" "$NC"; }
log_success() { printf "%b%s %s%b\n" "$GREEN" "$EMOJI_OK" "$1" "$NC"; }
log_warning() { printf "%b%s %s%b\n" "$YELLOW" "$EMOJI_WARN" "$1" "$NC"; }
log_error() { printf "%b%s %s%b\n" "$RED" "$EMOJI_ERROR" "$1" "$NC" >&2; }
log_step() { printf "%b%s %s%b\n" "$BLUE" "$EMOJI_GEAR" "$1" "$NC"; }
die() {
log_error "$1"
exit 1
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
binary_or_path_exists() {
local bin_name="$1"
shift
if command_exists "$bin_name"; then
return 0
fi
local path_candidate
for path_candidate in "$@"; do
[[ -x "$path_candidate" ]] && return 0
done
return 1
}
systemd_unit_exists() {
local unit_name="$1"
command_exists systemctl || return 1
systemctl list-unit-files "$unit_name" >/dev/null 2>&1
}
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
contains_value() {
local needle="$1"
shift
local item
for item in "$@"; do
[[ "$item" == "$needle" ]] && return 0
done
return 1
}
is_valid_ipv4() {
local ip="$1"
[[ "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] || return 1
local o1 o2 o3 o4
IFS='.' read -r o1 o2 o3 o4 <<<"$ip"
local octet
for octet in "$o1" "$o2" "$o3" "$o4"; do
[[ "$octet" =~ ^[0-9]+$ ]] || return 1
((octet >= 0 && octet <= 255)) || return 1
done
return 0
}
is_valid_ipv6() {
local ip="$1"
local ipv6_re='^(([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:)|([0-9A-Fa-f]{1,4}:){1,7}:|([0-9A-Fa-f]{1,4}:){1,6}:[0-9A-Fa-f]{1,4}|([0-9A-Fa-f]{1,4}:){1,5}(:[0-9A-Fa-f]{1,4}){1,2}|([0-9A-Fa-f]{1,4}:){1,4}(:[0-9A-Fa-f]{1,4}){1,3}|([0-9A-Fa-f]{1,4}:){1,3}(:[0-9A-Fa-f]{1,4}){1,4}|([0-9A-Fa-f]{1,4}:){1,2}(:[0-9A-Fa-f]{1,4}){1,5}|[0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){1,6}|:((:[0-9A-Fa-f]{1,4}){1,7}|:))(%.+)?$'
[[ "$ip" =~ $ipv6_re ]]
}
is_valid_ip() {
local ip="$1"
is_valid_ipv4 "$ip" || is_valid_ipv6 "$ip"
}
require_tty() {
[[ -r /dev/tty && -w /dev/tty ]] || die "No interactive TTY found. This script needs a terminal for prompts (even when using curl | bash)."
}
read_tty_input() {
local prompt="$1"
local value
printf "%b" "$prompt" >/dev/tty
IFS= read -r value </dev/tty || return 1
printf '%s' "$(trim "$value")"
}
read_tty_input_timeout() {
local prompt="$1"
local timeout_sec="$2"
local value
printf "%b" "$prompt" >/dev/tty
IFS= read -r -t "$timeout_sec" value </dev/tty || return $?
printf '%s' "$(trim "$value")"
}
run_root() {
if [[ -n "$SUDO_BIN" ]]; then
"$SUDO_BIN" "$@"
else
"$@"
fi
}
write_root_file() {
local target="$1"
local content="$2"
if [[ -n "$SUDO_BIN" ]]; then
printf '%s' "$content" | "$SUDO_BIN" tee "$target" >/dev/null
else
printf '%s' "$content" >"$target"
fi
}
append_root_file() {
local target="$1"
local content="$2"
if [[ -n "$SUDO_BIN" ]]; then
printf '%s' "$content" | "$SUDO_BIN" tee -a "$target" >/dev/null
else
printf '%s' "$content" >>"$target"
fi
}
ensure_root_context() {
if [[ "$EUID" -eq 0 ]]; then
return 0
fi
command_exists sudo || die "This script must run as root or with sudo available."
local source_path="${BASH_SOURCE[0]:-}"
if [[ -n "$source_path" && -f "$source_path" ]]; then
log_info "Re-launching with sudo for full root context..."
exec sudo -E bash "$source_path" "$@"
fi
log_warning "Running from stdin (likely curl | bash). Falling back to sudo for privileged operations."
sudo -v || die "Could not acquire sudo privileges."
SUDO_BIN="sudo"
}
detect_runtime_context() {
if [[ -n "${SSH_CONNECTION:-}" || -n "${SSH_TTY:-}" ]]; then
IS_REMOTE_SSH=1
log_warning "SSH session detected. Using safer apply behavior to reduce connection drops."
fi
}
has_systemd_resolved() {
binary_or_path_exists "resolvectl" "/usr/bin/resolvectl" "/bin/resolvectl" && return 0
systemd_unit_exists "systemd-resolved.service" && return 0
binary_or_path_exists "systemd-resolved" "/lib/systemd/systemd-resolved" "/usr/lib/systemd/systemd-resolved" && return 0
return 1
}
has_networkmanager() {
binary_or_path_exists "nmcli" "/usr/bin/nmcli" "/bin/nmcli" && return 0
binary_or_path_exists "NetworkManager" "/usr/sbin/NetworkManager" "/sbin/NetworkManager" && return 0
systemd_unit_exists "NetworkManager.service" && return 0
return 1
}
has_resolvconf_openresolv() {
binary_or_path_exists "resolvconf" "/sbin/resolvconf" "/usr/sbin/resolvconf" && return 0
return 1
}
has_dhclient() {
binary_or_path_exists "dhclient" "/sbin/dhclient" "/usr/sbin/dhclient" && return 0
return 1
}
has_systemd_networkd() {
systemd_unit_exists "systemd-networkd.service" && return 0
binary_or_path_exists "systemd-networkd" "/lib/systemd/systemd-networkd" "/usr/lib/systemd/systemd-networkd" && return 0
return 1
}
has_ifupdown() {
binary_or_path_exists "ifup" "/sbin/ifup" "/usr/sbin/ifup" && return 0
[[ -f /etc/network/interfaces || -d /etc/network/interfaces.d ]] && return 0
return 1
}
add_manager() {
local manager="$1"
if ! contains_value "$manager" "${DETECTED_MANAGERS[@]}"; then
DETECTED_MANAGERS+=("$manager")
fi
}
detect_managers() {
DETECTED_MANAGERS=()
if has_systemd_resolved; then
add_manager "systemd-resolved"
fi
if has_networkmanager; then
add_manager "NetworkManager"
fi
if has_resolvconf_openresolv; then
add_manager "resolvconf/openresolv"
fi
if has_dhclient; then
add_manager "dhclient"
fi
if has_systemd_networkd; then
add_manager "systemd-networkd"
fi
if has_ifupdown; then
add_manager "ifupdown"
fi
}
prompt_dns_servers() {
DNS_SERVERS=()
local input
local prompt
local index
for index in 1 2; do
while true; do
prompt="${BLUE}${EMOJI_NET} Enter DNS server ${index} (required): ${NC}"
input="$(read_tty_input "$prompt")" || die "Failed to read DNS input from TTY."
[[ -n "$input" ]] || {
log_warning "DNS server ${index} is required."
continue
}
if ! is_valid_ip "$input"; then
log_warning "Invalid IP literal: '$input'. Please enter a valid IPv4 or IPv6 address."
continue
fi
if contains_value "$input" "${DNS_SERVERS[@]}"; then
log_warning "Duplicate DNS server '$input' is not allowed."
continue
fi
DNS_SERVERS+=("$input")
break
done
done
while true; do
prompt="${BLUE}${EMOJI_NET} Enter DNS server 3 (optional, press Enter to skip): ${NC}"
input="$(read_tty_input "$prompt")" || die "Failed to read DNS input from TTY."
if [[ -z "$input" ]]; then
break
fi
if ! is_valid_ip "$input"; then
log_warning "Invalid IP literal: '$input'. Please enter a valid IPv4 or IPv6 address."
continue
fi
if contains_value "$input" "${DNS_SERVERS[@]}"; then
log_warning "Duplicate DNS server '$input' is not allowed."
continue
fi
DNS_SERVERS+=("$input")
break
done
log_success "DNS server list accepted: ${DNS_SERVERS[*]}"
}
check_dns_connectivity() {
local dns="$1"
if command_exists dig; then
dig @"$dns" example.com A +time=2 +tries=1 +short >/dev/null 2>&1 && return 0
dig @"$dns" . NS +time=2 +tries=1 +short >/dev/null 2>&1 && return 0
return 1
fi
if command_exists drill; then
drill @"$dns" example.com A >/dev/null 2>&1 && return 0
drill @"$dns" . NS >/dev/null 2>&1 && return 0
return 1
fi
if command_exists nslookup; then
nslookup -timeout=2 example.com "$dns" >/dev/null 2>&1 && return 0
nslookup example.com "$dns" >/dev/null 2>&1 && return 0
return 1
fi
if command_exists nc; then
nc -z -w2 "$dns" 53 >/dev/null 2>&1 && return 0
return 1
fi
if [[ "$dns" != *:* ]] && command_exists timeout; then
timeout 2 bash -c "exec 3<>/dev/tcp/$dns/53" >/dev/null 2>&1 && return 0
return 1
fi
return 2
}
run_connectivity_checks() {
log_step "Running best-effort connectivity checks for supplied DNS servers..."
local dns
local rc
for dns in "${DNS_SERVERS[@]}"; do
if check_dns_connectivity "$dns"; then
log_success "Reachability check passed for $dns"
continue
fi
rc=$?
if [[ "$rc" -eq 2 ]]; then
log_warning "No available tool for probing $dns on this host. Proceeding anyway."
else
log_warning "Could not reach DNS server $dns during probe. Proceeding as requested."
fi
done
}
dns_nameserver_lines() {
local lines=""
local dns
for dns in "${DNS_SERVERS[@]}"; do
lines+="nameserver ${dns}"$'\n'
done
printf '%s' "$lines"
}
dns_space_list() {
printf '%s' "${DNS_SERVERS[*]}"
}
dns_comma_list() {
local out=""
local dns
for dns in "${DNS_SERVERS[@]}"; do
if [[ -z "$out" ]]; then
out="$dns"
else
out+=",${dns}"
fi
done
printf '%s' "$out"
}
run_action() {
local action="$1"
shift
ACTIONS_ATTEMPTED+=("$action")
log_step "$action"
local rc=0
if "$@"; then
ACTIONS_SUCCEEDED+=("$action")
log_success "$action completed"
return 0
fi
rc=$?
if [[ "$rc" -eq 2 ]]; then
ACTIONS_SKIPPED+=("$action")
log_info "$action skipped: target manager is not present on this host"
return 0
fi
ACTIONS_FAILED+=("$action")
log_warning "$action failed (best-effort mode: continuing)"
return 0
}
ensure_resolv_conf_mutable_if_needed() {
if command_exists lsattr && command_exists chattr; then
local attr_line attr_field
attr_line="$(run_root lsattr /etc/resolv.conf 2>/dev/null || true)"
attr_field="${attr_line%% *}"
if [[ "$attr_field" == *i* ]]; then
log_warning "/etc/resolv.conf is immutable. Temporarily removing immutable flag for update."
run_root chattr -i /etc/resolv.conf || return 1
fi
fi
return 0
}
apply_static_resolv_conf() {
ensure_resolv_conf_mutable_if_needed || return 1
local now lines content
now="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
lines="$(dns_nameserver_lines)"
content="# Managed by ${SCRIPT_NAME} v${SCRIPT_VERSION} at ${now}"$'\n'
content+="$lines"
content+="options timeout:2 attempts:2 rotate"$'\n'
write_root_file "/etc/resolv.conf" "$content" || return 1
run_root chmod 644 /etc/resolv.conf || true
return 0
}
apply_systemd_resolved() {
if ! has_systemd_resolved; then
log_info "Skipping systemd-resolved override: systemd-resolved is not present."
return 2
fi
local dns_line content
dns_line="$(dns_space_list)"
content="[Resolve]"$'\n'
content+="DNS=${dns_line}"$'\n'
content+="FallbackDNS="$'\n'
content+="Domains=~."$'\n'
run_root mkdir -p /etc/systemd/resolved.conf.d || return 1
write_root_file "/etc/systemd/resolved.conf.d/99-dns-override.conf" "$content" || return 1
if command_exists systemctl; then
run_root systemctl daemon-reload >/dev/null 2>&1 || true
run_root systemctl restart systemd-resolved >/dev/null 2>&1 || run_root systemctl try-restart systemd-resolved >/dev/null 2>&1 || true
fi
if command_exists resolvectl; then
if command_exists ip; then
local link
while IFS= read -r link; do
[[ -n "$link" && "$link" != "lo" ]] || continue
run_root resolvectl dns "$link" "${DNS_SERVERS[@]}" >/dev/null 2>&1 || true
run_root resolvectl domain "$link" "~." >/dev/null 2>&1 || true
done < <(ip -o link show | awk -F': ' '{print $2}' | cut -d'@' -f1)
fi
run_root resolvectl flush-caches >/dev/null 2>&1 || true
fi
return 0
}
apply_networkmanager() {
if ! has_networkmanager; then
log_info "Skipping NetworkManager override: NetworkManager is not present."
return 2
fi
local dns_csv nm_conf
dns_csv="$(dns_comma_list)"
nm_conf="[main]"$'\n'
nm_conf+="dns=default"$'\n'
nm_conf+="rc-manager=symlink"$'\n'
run_root mkdir -p /etc/NetworkManager/conf.d || return 1
write_root_file "/etc/NetworkManager/conf.d/90-dns-override.conf" "$nm_conf" || return 1
if command_exists nmcli; then
local conn
while IFS= read -r conn; do
[[ -n "$conn" ]] || continue
run_root nmcli connection modify "$conn" ipv4.ignore-auto-dns yes >/dev/null 2>&1 || true
run_root nmcli connection modify "$conn" ipv6.ignore-auto-dns yes >/dev/null 2>&1 || true
run_root nmcli connection modify "$conn" ipv4.dns "$dns_csv" >/dev/null 2>&1 || true
run_root nmcli connection modify "$conn" ipv6.dns "$dns_csv" >/dev/null 2>&1 || true
done < <(nmcli -g NAME connection show --active 2>/dev/null || true)
run_root nmcli connection reload >/dev/null 2>&1 || true
if [[ "$IS_REMOTE_SSH" -eq 1 ]]; then
log_warning "Skipping NetworkManager reconnect/restart operations in SSH session."
local dev
while IFS= read -r dev; do
[[ -n "$dev" && "$dev" != "lo" ]] || continue
run_root nmcli device reapply "$dev" >/dev/null 2>&1 || true
done < <(nmcli -t -f DEVICE,STATE device status 2>/dev/null | awk -F: '$2=="connected" || $2=="connecting" {print $1}')
else
while IFS= read -r conn; do
[[ -n "$conn" ]] || continue
run_root nmcli connection up "$conn" >/dev/null 2>&1 || true
done < <(nmcli -g NAME connection show --active 2>/dev/null || true)
fi
run_root nmcli general reload >/dev/null 2>&1 || true
fi
if command_exists systemctl; then
run_root systemctl reload NetworkManager >/dev/null 2>&1 || true
fi
return 0
}
apply_resolvconf_openresolv() {
if ! has_resolvconf_openresolv; then
log_info "Skipping resolvconf/openresolv override: resolvconf/openresolv is not present."
return 2
fi
local lines
lines="$(dns_nameserver_lines)"
run_root mkdir -p /etc/resolvconf/resolv.conf.d || true
write_root_file "/etc/resolvconf/resolv.conf.d/head" "$lines" || true
write_root_file "/etc/resolvconf/resolv.conf.d/base" "$lines" || true
if command_exists resolvconf; then
printf '%s' "$lines" | run_root resolvconf -a dns-override >/dev/null 2>&1 || true
run_root resolvconf -u >/dev/null 2>&1 || true
fi
return 0
}
apply_dhclient() {
if ! has_dhclient; then
log_info "Skipping dhclient override: dhclient is not present."
return 2
fi
local conf_path="/etc/dhcp/dhclient.conf"
local dns_csv block
dns_csv="$(dns_comma_list)"
block=$'\n'"# dns-override.sh managed begin"$'\n'
block+="supersede domain-name-servers ${dns_csv};"$'\n'
block+="prepend domain-name-servers ${dns_csv};"$'\n'
block+="# dns-override.sh managed end"$'\n'
run_root mkdir -p /etc/dhcp || true
run_root touch "$conf_path" || return 1
run_root sed -i '/# dns-override.sh managed begin/,/# dns-override.sh managed end/d' "$conf_path" || true
append_root_file "$conf_path" "$block" || return 1
return 0
}
apply_systemd_networkd() {
if ! has_systemd_networkd; then
log_info "Skipping systemd-networkd override: systemd-networkd is not present."
return 2
fi
local dns_lines dropin_content
dns_lines=""
local dns
for dns in "${DNS_SERVERS[@]}"; do
dns_lines+="DNS=${dns}"$'\n'
done
run_root mkdir -p /etc/systemd/network || return 1
local has_network_file=0
local net_file
for net_file in /etc/systemd/network/*.network; do
[[ -e "$net_file" ]] || continue
has_network_file=1
run_root mkdir -p "${net_file}.d" || true
dropin_content="[Network]"$'\n'"${dns_lines}Domains=~."$'\n'
write_root_file "${net_file}.d/99-dns-override.conf" "$dropin_content" || true
done
if [[ "$has_network_file" -eq 0 ]]; then
dropin_content="[Match]"$'\n'"Name=*"$'\n\n'"[Network]"$'\n'"${dns_lines}Domains=~."$'\n'
write_root_file "/etc/systemd/network/99-dns-override.network" "$dropin_content" || true
fi
if command_exists systemctl; then
run_root systemctl daemon-reload >/dev/null 2>&1 || true
if command_exists networkctl; then
run_root networkctl reload >/dev/null 2>&1 || true
if [[ "$IS_REMOTE_SSH" -eq 0 ]]; then
run_root networkctl reconfigure --all >/dev/null 2>&1 || true
else
log_warning "Skipping networkctl reconfigure --all in SSH session."
fi
elif [[ "$IS_REMOTE_SSH" -eq 0 ]]; then
run_root systemctl restart systemd-networkd >/dev/null 2>&1 || run_root systemctl try-restart systemd-networkd >/dev/null 2>&1 || true
else
log_warning "Skipping systemd-networkd restart in SSH session."
fi
fi
return 0
}
patch_ifupdown_file() {
local file_path="$1"
[[ -f "$file_path" ]] || return 1
local dns_line
dns_line="$(dns_space_list)"
local temp_file
temp_file="$(mktemp)"
set +e
awk -v dns="$dns_line" '
BEGIN { iface_count = 0 }
/^[[:space:]]*dns-nameservers[[:space:]]+/ { next }
{
print
if ($0 ~ /^[[:space:]]*iface[[:space:]]+/ && $0 !~ /[[:space:]]lo[[:space:]]/) {
print " dns-nameservers " dns
iface_count++
}
}
END {
if (iface_count == 0) {
exit 2
}
}
' "$file_path" >"$temp_file"
local rc=$?
set -e
if [[ "$rc" -eq 2 ]]; then
rm -f "$temp_file"
return 2
fi
[[ "$rc" -eq 0 ]] || {
rm -f "$temp_file"
return 1
}
if [[ -n "$SUDO_BIN" ]]; then
"$SUDO_BIN" mv "$temp_file" "$file_path"
else
mv "$temp_file" "$file_path"
fi
return 0
}
apply_ifupdown() {
if ! has_ifupdown; then
log_info "Skipping ifupdown override: ifupdown is not present."
return 2
fi
local touched=0
local patch_rc=0
if [[ -f /etc/network/interfaces ]]; then
if patch_ifupdown_file /etc/network/interfaces; then
touched=1
else
patch_rc=$?
[[ "$patch_rc" -eq 2 ]] || return 1
fi
fi
if [[ -d /etc/network/interfaces.d ]]; then
local iface_file rc
for iface_file in /etc/network/interfaces.d/*; do
[[ -f "$iface_file" ]] || continue
if patch_ifupdown_file "$iface_file"; then
touched=1
else
rc=$?
[[ "$rc" -eq 2 ]] || return 1
fi
done
fi
if [[ "$touched" -eq 0 ]]; then
log_warning "ifupdown detected, but no writable iface stanzas were found to patch automatically."
log_info "Actionable hint: add 'dns-nameservers $(dns_space_list)' inside each target iface block."
fi
return 0
}
prompt_immutable_lock() {
command_exists chattr || {
log_info "chattr not available; skipping immutable lock prompt."
return 0
}
local answer
if ! answer="$(read_tty_input_timeout "${YELLOW}${EMOJI_LOCK} Make /etc/resolv.conf immutable with chattr +i? [y/N] (auto No in ${IMMUTABLE_PROMPT_TIMEOUT_SEC}s): ${NC}" "$IMMUTABLE_PROMPT_TIMEOUT_SEC")"; then
local rc=$?
if [[ "$rc" -gt 128 ]]; then
log_warning "No response received for immutable lock prompt; defaulting to No."
IMMUTABLE_LOCK_REQUESTED=0
return 0
fi
return 1
fi
case "$answer" in
y|Y|yes|YES)
IMMUTABLE_LOCK_REQUESTED=1
log_info "Immutable lock requested. It will be applied after DNS updates."
;;
*)
IMMUTABLE_LOCK_REQUESTED=0
log_info "Immutable lock skipped."
;;
esac
}
apply_immutable_lock_if_requested() {
if [[ "$IMMUTABLE_LOCK_REQUESTED" -ne 1 ]]; then
return 0
fi
if run_root chattr +i /etc/resolv.conf; then
log_success "Immutable lock applied to /etc/resolv.conf."
log_warning "To edit DNS later, run: sudo chattr -i /etc/resolv.conf"
else
log_warning "Failed to apply immutable lock."
fi
}
print_summary() {
echo
log_step "Execution summary"
log_info "Detected managers: ${DETECTED_MANAGERS[*]:-(none detected)}"
log_info "Actions attempted: ${#ACTIONS_ATTEMPTED[@]}"
log_success "Actions succeeded: ${#ACTIONS_SUCCEEDED[@]}"
log_warning "Actions failed: ${#ACTIONS_FAILED[@]}"
log_info "Actions skipped: ${#ACTIONS_SKIPPED[@]}"
if [[ "${#ACTIONS_FAILED[@]}" -gt 0 ]]; then
local failed
for failed in "${ACTIONS_FAILED[@]}"; do
log_warning "Failed action: $failed"
done
fi
echo
log_step "Final /etc/resolv.conf"
run_root cat /etc/resolv.conf 2>/dev/null || cat /etc/resolv.conf 2>/dev/null || log_warning "Could not read /etc/resolv.conf"
if command_exists resolvectl; then
echo
log_step "resolvectl status snapshot"
run_root resolvectl status 2>/dev/null || resolvectl status 2>/dev/null || log_warning "Could not get resolvectl status."
fi
if command_exists nmcli; then
echo
log_step "NetworkManager DNS snapshot"
run_root nmcli -f GENERAL.DEVICE,IP4.DNS,IP6.DNS device show 2>/dev/null || nmcli -f GENERAL.DEVICE,IP4.DNS,IP6.DNS device show 2>/dev/null || log_warning "Could not get nmcli DNS snapshot."
fi
}
main() {
echo
printf "%b%s %s v%s%b\n" "$BLUE" "$EMOJI_ROCKET" "$SCRIPT_NAME" "$SCRIPT_VERSION" "$NC"
log_info "Aggressive best-effort DNS override for Linux hosts."
log_warning "This script edits multiple DNS manager configs and may restart networking services."
echo
require_tty
ensure_root_context "$@"
detect_runtime_context
prompt_dns_servers
prompt_immutable_lock || log_warning "Immutable lock prompt failed; defaulting to unlocked."
run_connectivity_checks
detect_managers
run_action "Apply systemd-resolved override" apply_systemd_resolved
run_action "Apply NetworkManager override" apply_networkmanager
run_action "Apply resolvconf/openresolv override" apply_resolvconf_openresolv
run_action "Apply dhclient override" apply_dhclient
run_action "Apply systemd-networkd override" apply_systemd_networkd
run_action "Apply ifupdown override" apply_ifupdown
run_action "Write /etc/resolv.conf directly" apply_static_resolv_conf
run_action "Apply immutable /etc/resolv.conf lock (if requested)" apply_immutable_lock_if_requested
print_summary
echo
log_success "DNS override flow completed."
}
main "$@"