adding dns override
All checks were successful
Check scripts syntax / check-scripts-syntax (push) Successful in 41s

This commit is contained in:
2026-05-16 11:09:03 -03:00
parent 253c8abee3
commit c800ec1888
2 changed files with 717 additions and 0 deletions

View File

@@ -12,6 +12,32 @@ This script is used to clean some of the files, docker dangling images, and dock
This PowerShell script is used to backup files from a Windows machine to a remote Samba server. It uses `7zip` to create the zip file, and then sends over the network using SMB protocol. This PowerShell script is used to backup files from a Windows machine to a remote Samba server. It uses `7zip` to create the zip file, and then sends over the network using SMB protocol.
### `dns-override.sh`
Aggressive, best-effort DNS override script for Linux hosts with support for multiple resolver stacks (`systemd-resolved`, `NetworkManager`, `resolvconf/openresolv`, `dhclient`, `systemd-networkd`, and `ifupdown`) plus direct `/etc/resolv.conf` writes.
It prompts for:
- 2 required DNS servers
- 1 optional third DNS server
Validation/features:
- Accepts IPv4 and IPv6 literals
- Rejects invalid or duplicate entries
- Performs reachability checks (warn-only, does not block execution)
- Can optionally lock `/etc/resolv.conf` with `chattr +i`
Run with `curl | bash`:
```bash
curl -sSL https://git.ivanch.me/ivanch/server-scripts/raw/branch/main/dns-override.sh | bash
```
Important notes:
- Requires interactive TTY input (prompts are read from `/dev/tty`)
- Requires root privileges (`sudo` is used/re-execed when possible)
- Script is intentionally aggressive and may restart/reload network services
- On heavily managed systems (custom agents, immutable images, enterprise policy), behavior is still best-effort
## Haven Notify ## Haven Notify
It's a small internal service designed to send notifications to a specified Discord channel via webhooks. It's written in Go and can be easily deployed. It's a small internal service designed to send notifications to a specified Discord channel via webhooks. It's written in Go and can be easily deployed.

691
dns-override.sh Normal file
View File

@@ -0,0 +1,691 @@
#!/usr/bin/env bash
set -euo pipefail
readonly SCRIPT_NAME="dns-override.sh"
readonly SCRIPT_VERSION="1.0.0"
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=""
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
}
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")"
}
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"
}
add_manager() {
local manager="$1"
if ! contains_value "$manager" "${DETECTED_MANAGERS[@]}"; then
DETECTED_MANAGERS+=("$manager")
fi
}
detect_managers() {
DETECTED_MANAGERS=()
if command_exists systemctl && systemctl is-active --quiet systemd-resolved 2>/dev/null; then
add_manager "systemd-resolved"
elif [[ -L /etc/resolv.conf ]] && readlink /etc/resolv.conf 2>/dev/null | grep -Eq 'systemd/resolve'; then
add_manager "systemd-resolved"
fi
if command_exists nmcli || [[ -d /etc/NetworkManager ]]; then
add_manager "NetworkManager"
fi
if command_exists resolvconf || [[ -d /etc/resolvconf ]] || [[ -f /etc/resolvconf.conf ]]; then
add_manager "resolvconf/openresolv"
fi
if command_exists dhclient || [[ -f /etc/dhcp/dhclient.conf ]]; then
add_manager "dhclient"
fi
if command_exists systemctl && systemctl is-active --quiet systemd-networkd 2>/dev/null; then
add_manager "systemd-networkd"
elif [[ -d /etc/systemd/network ]]; then
add_manager "systemd-networkd"
fi
if [[ -f /etc/network/interfaces ]] || [[ -d /etc/network/interfaces.d ]]; 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 not applicable on this system"
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 ! command_exists systemctl && ! command_exists resolvectl && [[ ! -d /etc/systemd ]]; then
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 ! command_exists nmcli && [[ ! -d /etc/NetworkManager ]]; then
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
run_root nmcli connection up "$conn" >/dev/null 2>&1 || true
done < <(nmcli -g NAME connection show --active 2>/dev/null || true)
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 || run_root systemctl restart NetworkManager >/dev/null 2>&1 || true
fi
return 0
}
apply_resolvconf_openresolv() {
if ! command_exists resolvconf && [[ ! -d /etc/resolvconf ]] && [[ ! -f /etc/resolvconf.conf ]]; then
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 ! command_exists dhclient && [[ ! -f /etc/dhcp/dhclient.conf ]] && [[ ! -d /etc/dhcp ]]; then
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 ! command_exists systemctl && [[ ! -d /etc/systemd/network ]]; then
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
run_root systemctl restart systemd-networkd >/dev/null 2>&1 || run_root systemctl try-restart systemd-networkd >/dev/null 2>&1 || true
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 [[ ! -f /etc/network/interfaces ]] && [[ ! -d /etc/network/interfaces.d ]]; then
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
answer="$(read_tty_input "${YELLOW}${EMOJI_LOCK} Make /etc/resolv.conf immutable with chattr +i? [y/N]: ${NC}")" || return 1
case "$answer" in
y|Y|yes|YES)
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
;;
*)
log_info "Immutable lock skipped."
;;
esac
}
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 "$@"
prompt_dns_servers
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
prompt_immutable_lock || log_warning "Immutable lock prompt failed; continuing."
print_summary
echo
log_success "DNS override flow completed."
}
main "$@"