|
| 1 | +#!/usr/bin/env bash |
| 2 | +# set-dns.sh — Lightweight, polite, bilingual DNS setter ✨ |
| 3 | +# Works with Debian/Ubuntu, RHEL/CentOS/Rocky, Arch, NixOS (with systemd-resolved), etc. |
| 4 | + |
| 5 | +set -euo pipefail |
| 6 | + |
| 7 | +# ===== Defaults (env or CLI can override) ===== |
| 8 | +DNS_SERVERS_CSV="${DNS_SERVERS_CSV:-151.240.12.10}" |
| 9 | +TEST_DOMAIN="${TEST_DOMAIN:-unlock.isif.net}" |
| 10 | +EXPECT_IP="${EXPECT_IP:-1.1.1.1}" |
| 11 | +LANG_CHOICE="${LANG_CHOICE:-auto}" # auto|zh|en |
| 12 | +FORCE_METHOD="${FORCE_METHOD:-auto}" # auto|resolved|nm|resolv |
| 13 | +DRY_RUN="${DRY_RUN:-0}" # 1 to simulate |
| 14 | +QUIET="${QUIET:-0}" # 1 for minimal output |
| 15 | +ONLY_IPV4="${ONLY_IPV4:-0}" # 1 to keep only IPv4 DNSes |
| 16 | +ONLY_IPV6="${ONLY_IPV6:-0}" # 1 to keep only IPv6 DNSes |
| 17 | + |
| 18 | +# ===== i18n ===== |
| 19 | +pick_lang() { |
| 20 | + case "$LANG_CHOICE" in |
| 21 | + zh|ZH|cn|CN|zh_CN|zh_TW) echo zh ;; |
| 22 | + en|EN|us|US|en_US|en_GB) echo en ;; |
| 23 | + auto|Auto|AUTO|*) |
| 24 | + case "${LANG:-}" in zh*|ZH*) echo zh ;; *) echo en ;; esac ;; |
| 25 | + esac |
| 26 | +} |
| 27 | +LANG_USE="$(pick_lang)" |
| 28 | + |
| 29 | +say_zh() { |
| 30 | + case "$1" in |
| 31 | + need_root) echo "请使用 root 运行(sudo $0)。";; |
| 32 | + start) echo "开始设置 DNS(优雅模式)。";; |
| 33 | + parsed_args) echo "参数就绪:准备出发 🚀";; |
| 34 | + using_resolved) echo "检测到 systemd-resolved,采用 drop-in 方式设置。";; |
| 35 | + using_nm) echo "检测到 NetworkManager,按活动连接设置手动 DNS。";; |
| 36 | + using_resolv) echo "未检测到前两者,直接写入 /etc/resolv.conf(已备份)。";; |
| 37 | + backed_up) echo "已备份 /etc/resolv.conf ->";; |
| 38 | + flush_cache) echo "刷新 DNS 缓存中…";; |
| 39 | + testing) echo "正在解析域名:";; |
| 40 | + hits) echo "解析结果如下:";; |
| 41 | + success) echo "设置成功 ✅:命中期望 IP";; |
| 42 | + fail) echo "可能尚未生效 ❗:未命中期望 IP,请稍后重试或检查网络管理器。";; |
| 43 | + dryrun) echo "演练模式(不会真的修改系统),下面只是预览动作:";; |
| 44 | + done) echo "全部完成,祝你网络清清爽爽 🌊";; |
| 45 | + no_active_nm) echo "未发现活动的 NetworkManager 连接,跳过 nm 配置。";; |
| 46 | + restored) echo "已尝试还原最近的 resolv.conf 备份。";; |
| 47 | + installing) echo "正在安装为 /usr/local/bin/set-dns …";; |
| 48 | + installed) echo "安装完成:现在可以直接运行 set-dns";; |
| 49 | + usage) |
| 50 | + cat <<'EOF' |
| 51 | +用法: |
| 52 | + sudo ./set-dns.sh [选项] |
| 53 | +
|
| 54 | +选项: |
| 55 | + --dns <CSV> 要设置的 DNS,逗号分隔(默认: 1.1.1.1,2606:4700:4700::1111) |
| 56 | + --domain <域名> 测试解析的域名(默认: unlock.isif.net) |
| 57 | + --expect <IP> 期望命中的 IP(默认: 1.1.1.1) |
| 58 | + --lang <auto|zh|en> 语言(默认: auto) |
| 59 | + --method <auto|resolved|nm|resolv> 强制方式(默认: auto) |
| 60 | + --only-ipv4 仅使用 IPv4 DNS |
| 61 | + --only-ipv6 仅使用 IPv6 DNS |
| 62 | + --dry-run 演练,不修改系统 |
| 63 | + --quiet 安静输出 |
| 64 | + --restore 还原最近的 /etc/resolv.conf 备份 |
| 65 | + --install 安装为 /usr/local/bin/set-dns |
| 66 | + -h,--help 显示帮助 |
| 67 | +EOF |
| 68 | + ;; |
| 69 | + esac |
| 70 | +} |
| 71 | + |
| 72 | +say_en() { |
| 73 | + case "$1" in |
| 74 | + need_root) echo "Please run as root (sudo $0).";; |
| 75 | + start) echo "Starting DNS setup (polite mode).";; |
| 76 | + parsed_args) echo "Args parsed: ready to roll 🚀";; |
| 77 | + using_resolved) echo "systemd-resolved detected, applying drop-in override.";; |
| 78 | + using_nm) echo "NetworkManager detected, setting per active connection.";; |
| 79 | + using_resolv) echo "Fallback: writing /etc/resolv.conf directly (backed up).";; |
| 80 | + backed_up) echo "Backed up /etc/resolv.conf ->";; |
| 81 | + flush_cache) echo "Flushing DNS caches…";; |
| 82 | + testing) echo "Querying domain:";; |
| 83 | + hits) echo "Answers:";; |
| 84 | + success) echo "Success ✅: expected IP matched";; |
| 85 | + fail) echo "May not be applied yet ❗: expected IP not found. Retry later / check network manager.";; |
| 86 | + dryrun) echo "Dry-run mode (no changes). Previewing actions only:";; |
| 87 | + done) echo "All set. May your packets flow smoothly 🌊";; |
| 88 | + no_active_nm) echo "No active NetworkManager connections; skipping nm config.";; |
| 89 | + restored) echo "Attempted to restore the latest resolv.conf backup.";; |
| 90 | + installing) echo "Installing to /usr/local/bin/set-dns …";; |
| 91 | + installed) echo "Installed. Now you can run: set-dns";; |
| 92 | + usage) |
| 93 | + cat <<'EOF' |
| 94 | +Usage: |
| 95 | + sudo ./set-dns.sh [options] |
| 96 | +
|
| 97 | +Options: |
| 98 | + --dns <CSV> DNS servers (comma-separated), default: 1.1.1.1,2606:4700:4700::1111 |
| 99 | + --domain <name> Domain to test (default: unlock.isif.net) |
| 100 | + --expect <IP> Expected IP (default: 1.1.1.1) |
| 101 | + --lang <auto|zh|en> Language (default: auto) |
| 102 | + --method <auto|resolved|nm|resolv> Force method (default: auto) |
| 103 | + --only-ipv4 Use IPv4 DNS only |
| 104 | + --only-ipv6 Use IPv6 DNS only |
| 105 | + --dry-run Simulate only, no system changes |
| 106 | + --quiet Minimal output |
| 107 | + --restore Restore the latest /etc/resolv.conf backup |
| 108 | + --install Install to /usr/local/bin/set-dns |
| 109 | + -h,--help Show help |
| 110 | +EOF |
| 111 | + ;; |
| 112 | + esac |
| 113 | +} |
| 114 | + |
| 115 | +_() { if [ "${LANG_USE}" = zh ]; then say_zh "$@"; else say_en "$@"; fi; } |
| 116 | +log() { [ "$QUIET" -eq 1 ] || printf "[%s] %s\n" "$(date +'%F %T')" "$*" >&2; } |
| 117 | +have() { command -v "$1" >/dev/null 2>&1; } |
| 118 | +is_active() { systemctl is-active --quiet "$1" 2>/dev/null; } |
| 119 | + |
| 120 | +# ===== arg parsing ===== |
| 121 | +RESTORE=0; INSTALL=0 |
| 122 | +while [ $# -gt 0 ]; do |
| 123 | + case "$1" in |
| 124 | + --dns) DNS_SERVERS_CSV="$2"; shift 2;; |
| 125 | + --domain) TEST_DOMAIN="$2"; shift 2;; |
| 126 | + --expect) EXPECT_IP="$2"; shift 2;; |
| 127 | + --lang) LANG_CHOICE="$2"; LANG_USE="$(pick_lang)"; shift 2;; |
| 128 | + --method) FORCE_METHOD="$2"; shift 2;; |
| 129 | + --dry-run) DRY_RUN=1; shift;; |
| 130 | + --quiet) QUIET=1; shift;; |
| 131 | + --only-ipv4) ONLY_IPV4=1; shift;; |
| 132 | + --only-ipv6) ONLY_IPV6=1; shift;; |
| 133 | + --restore) RESTORE=1; shift;; |
| 134 | + --install) INSTALL=1; shift;; |
| 135 | + -h|--help) _ usage; exit 0;; |
| 136 | + *) echo "Unknown arg: $1" >&2; _ usage; exit 1;; |
| 137 | + esac |
| 138 | +done |
| 139 | +[ "$ONLY_IPV4" -eq 1 ] && ONLY_IPV6=0 |
| 140 | +[ "$ONLY_IPV6" -eq 1 ] && ONLY_IPV4=0 |
| 141 | + |
| 142 | +# ===== helpers ===== |
| 143 | +IFS=',' read -r -a DNS_LIST <<<"$DNS_SERVERS_CSV" |
| 144 | + |
| 145 | +filter_dns_family() { |
| 146 | + local out=() |
| 147 | + for ns in "${DNS_LIST[@]}"; do |
| 148 | + if [ "$ONLY_IPV4" -eq 1 ] && [[ "$ns" != *:* ]]; then out+=("$ns"); fi |
| 149 | + if [ "$ONLY_IPV6" -eq 1 ] && [[ "$ns" == *:* ]]; then out+=("$ns"); fi |
| 150 | + if [ "$ONLY_IPV4" -eq 0 ] && [ "$ONLY_IPV6" -eq 0 ]; then out+=("$ns"); fi |
| 151 | + done |
| 152 | + DNS_LIST=("${out[@]}") |
| 153 | +} |
| 154 | + |
| 155 | +join_by() { local IFS="$1"; shift; echo "$*"; } |
| 156 | + |
| 157 | +backup_resolv_conf() { |
| 158 | + local ts="/etc/resolv.conf.bak.$(date +%Y%m%d%H%M%S)" |
| 159 | + if [ -e /etc/resolv.conf ]; then |
| 160 | + [ "$DRY_RUN" -eq 1 ] || cp -a /etc/resolv.conf "$ts" || true |
| 161 | + log "$(_ backed_up) $ts" |
| 162 | + fi |
| 163 | +} |
| 164 | + |
| 165 | +restore_latest_resolv() { |
| 166 | + local latest |
| 167 | + latest="$(ls -1t /etc/resolv.conf.bak.* 2>/dev/null | head -n1 || true)" |
| 168 | + if [ -n "$latest" ]; then |
| 169 | + [ "$DRY_RUN" -eq 1 ] || cp -a "$latest" /etc/resolv.conf |
| 170 | + log "$(_ restored)" |
| 171 | + fi |
| 172 | +} |
| 173 | + |
| 174 | +write_resolv_conf() { |
| 175 | + # Handle immutable / symlink |
| 176 | + if have chattr; then [ "$DRY_RUN" -eq 1 ] || chattr -i /etc/resolv.conf 2>/dev/null || true; fi |
| 177 | + [ -L /etc/resolv.conf ] && [ "$DRY_RUN" -eq 0 ] && rm -f /etc/resolv.conf |
| 178 | + [ "$DRY_RUN" -eq 0 ] || return 0 |
| 179 | + |
| 180 | + { |
| 181 | + echo "# Generated by set-dns.sh @ $(date)" |
| 182 | + for ns in "${DNS_LIST[@]}"; do echo "nameserver $ns"; done |
| 183 | + echo "options timeout:2 attempts:2" |
| 184 | + } >/etc/resolv.conf |
| 185 | + log "$(_ using_resolv)" |
| 186 | +} |
| 187 | + |
| 188 | +set_dns_systemd_resolved() { |
| 189 | + local dir="/etc/systemd/resolved.conf.d" |
| 190 | + local dns_space |
| 191 | + dns_space="$(join_by ' ' "${DNS_LIST[@]}")" |
| 192 | + [ "$DRY_RUN" -eq 1 ] || mkdir -p "$dir" |
| 193 | + if [ "$DRY_RUN" -eq 0 ]; then |
| 194 | + { |
| 195 | + echo "[Resolve]" |
| 196 | + echo "DNS=$dns_space" |
| 197 | + echo "FallbackDNS=" |
| 198 | + } >"$dir/99-override.conf" |
| 199 | + systemctl restart systemd-resolved |
| 200 | + fi |
| 201 | + log "$(_ using_resolved)" |
| 202 | +} |
| 203 | + |
| 204 | +set_dns_nmcli() { |
| 205 | + local dns4=() dns6=() |
| 206 | + for ns in "${DNS_LIST[@]}"; do |
| 207 | + case "$ns" in *:*) dns6+=("$ns");; *) dns4+=("$ns");; esac |
| 208 | + done |
| 209 | + local dns4_csv="$(join_by , "${dns4[@]:-}")" |
| 210 | + local dns6_csv="$(join_by , "${dns6[@]:-}")" |
| 211 | + |
| 212 | + local active |
| 213 | + active="$(nmcli -t -f NAME,DEVICE connection show --active 2>/dev/null | awk -F: '$2!=""{print $1}')" |
| 214 | + if [ -z "$active" ]; then log "$(_ no_active_nm)"; return 0; fi |
| 215 | + |
| 216 | + while IFS= read -r name; do |
| 217 | + [ -z "$name" ] && continue |
| 218 | + if [ "$DRY_RUN" -eq 0 ]; then |
| 219 | + nmcli connection modify "$name" ipv4.ignore-auto-dns yes || true |
| 220 | + nmcli connection modify "$name" ipv6.ignore-auto-dns yes || true |
| 221 | + [ -n "$dns4_csv" ] && nmcli connection modify "$name" ipv4.dns "$dns4_csv" || true |
| 222 | + [ -n "$dns6_csv" ] && nmcli connection modify "$name" ipv6.dns "$dns6_csv" || true |
| 223 | + nmcli connection up "$name" >/dev/null || true |
| 224 | + fi |
| 225 | + log "nm: $name -> dns4=[${dns4_csv:-∅}] dns6=[${dns6_csv:-∅}]" |
| 226 | + done <<<"$active" |
| 227 | + log "$(_ using_nm)" |
| 228 | +} |
| 229 | + |
| 230 | +flush_dns_cache() { |
| 231 | + log "$(_ flush_cache)" |
| 232 | + if have resolvectl; then |
| 233 | + [ "$DRY_RUN" -eq 1 ] || resolvectl flush-caches || true |
| 234 | + elif have systemd-resolve; then |
| 235 | + [ "$DRY_RUN" -eq 1 ] || systemd-resolve --flush-caches || true |
| 236 | + fi |
| 237 | +} |
| 238 | + |
| 239 | +test_resolution() { |
| 240 | + log "$(_ testing) $TEST_DOMAIN" |
| 241 | + local hits |
| 242 | + hits="$(getent ahosts "$TEST_DOMAIN" | awk '{print $1}' | sort -u || true)" |
| 243 | + [ "$QUIET" -eq 1 ] || { log "$(_ hits)"; printf "%s\n" "$hits" | sed 's/^/ - /' >&2; } |
| 244 | + if printf "%s\n" "$hits" | grep -q -F -- "$EXPECT_IP"; then |
| 245 | + echo "$(_ success): $TEST_DOMAIN -> $EXPECT_IP" |
| 246 | + return 0 |
| 247 | + else |
| 248 | + echo "$(_ fail) ($TEST_DOMAIN -> $EXPECT_IP)" |
| 249 | + return 1 |
| 250 | + fi |
| 251 | +} |
| 252 | + |
| 253 | +choose_method() { |
| 254 | + case "$FORCE_METHOD" in |
| 255 | + resolved|nm|resolv) echo "$FORCE_METHOD"; return;; |
| 256 | + auto|*) |
| 257 | + if have systemctl && is_active systemd-resolved && have resolvectl; then |
| 258 | + echo resolved |
| 259 | + elif have nmcli && have systemctl && is_active NetworkManager; then |
| 260 | + echo nm |
| 261 | + else |
| 262 | + echo resolv |
| 263 | + fi |
| 264 | + ;; |
| 265 | + esac |
| 266 | +} |
| 267 | + |
| 268 | +ensure_root() { |
| 269 | + if [ "$(id -u)" -ne 0 ]; then |
| 270 | + echo "$(_ need_root)" >&2 |
| 271 | + exit 1 |
| 272 | + fi |
| 273 | +} |
| 274 | + |
| 275 | +install_self() { |
| 276 | + log "$(_ installing)" |
| 277 | + [ "$DRY_RUN" -eq 1 ] && return 0 |
| 278 | + install -m 0755 "$0" /usr/local/bin/set-dns |
| 279 | + log "$(_ installed)" |
| 280 | +} |
| 281 | + |
| 282 | +main() { |
| 283 | + ensure_root |
| 284 | + filter_dns_family |
| 285 | + |
| 286 | + [ "$RESTORE" -eq 1 ] && { restore_latest_resolv; exit 0; } |
| 287 | + [ "$INSTALL" -eq 1 ] && { install_self; exit 0; } |
| 288 | + |
| 289 | + [ "$QUIET" -eq 1 ] || log "$(_ start)" |
| 290 | + [ "$DRY_RUN" -eq 1 ] && log "$(_ dryrun)" |
| 291 | + log "$(_ parsed_args) DNS=[${DNS_LIST[*]}] domain=$TEST_DOMAIN expect=$EXPECT_IP lang=$LANG_USE method=$FORCE_METHOD" |
| 292 | + |
| 293 | + local method |
| 294 | + method="$(choose_method)" |
| 295 | + |
| 296 | + case "$method" in |
| 297 | + resolved) backup_resolv_conf; set_dns_systemd_resolved ;; |
| 298 | + nm) backup_resolv_conf; set_dns_nmcli ;; |
| 299 | + resolv) backup_resolv_conf; write_resolv_conf ;; |
| 300 | + esac |
| 301 | + |
| 302 | + flush_dns_cache |
| 303 | + sleep 0.5 |
| 304 | + test_resolution |
| 305 | + [ "$QUIET" -eq 1 ] || log "$(_ done)" |
| 306 | +} |
| 307 | + |
| 308 | +main |
0 commit comments