resolver-clean.sh 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. #!/usr/bin/env bash
  2. # net-ops :: macos/resolver-clean.sh
  3. # Safely remove orphaned /etc/resolver/* files left behind by disconnected VPNs.
  4. # NEVER removes Tailscale or current-VPN-tunnel entries.
  5. #
  6. # Defaults to DRY RUN — pass --apply to actually delete.
  7. # Requires sudo.
  8. set -eu
  9. APPLY=0
  10. PROTECT_PATTERNS="${PROTECT_PATTERNS:-100\.100\.100\.100}"
  11. for arg in "$@"; do
  12. case "$arg" in
  13. --apply) APPLY=1 ;;
  14. --protect=*) PROTECT_PATTERNS="${arg#--protect=}" ;;
  15. --help|-h)
  16. cat <<EOF
  17. Usage: $0 [--apply] [--protect=REGEX]
  18. --apply Actually delete (default: dry-run only)
  19. --protect=REGEX Nameserver pattern to protect (default: Tailscale's 100.100.100.100)
  20. Examples:
  21. $0 # show what would be removed
  22. $0 --apply # remove orphan resolvers, protecting Tailscale
  23. $0 --apply --protect='100\\.\\.|192\\.168\\.1\\.' # also protect 192.168.1.x
  24. EOF
  25. exit 0 ;;
  26. esac
  27. done
  28. if [[ ! -d /etc/resolver ]] || [[ -z "$(ls -A /etc/resolver 2>/dev/null)" ]]; then
  29. echo "/etc/resolver/ is empty. Nothing to do."
  30. exit 0
  31. fi
  32. echo "=== BEFORE ==="
  33. for f in /etc/resolver/*; do
  34. [[ -f "$f" ]] || continue
  35. ns=$(awk '/^nameserver/{print $2}' "$f" | tr '\n' ',')
  36. echo " $f -> ${ns%,}"
  37. done
  38. TARGETS=()
  39. for f in /etc/resolver/*; do
  40. [[ -f "$f" ]] || continue
  41. if awk '/^nameserver/{print $2}' "$f" | grep -qE "$PROTECT_PATTERNS"; then
  42. continue
  43. fi
  44. TARGETS+=("$f")
  45. done
  46. if [[ "${#TARGETS[@]}" -eq 0 ]]; then
  47. echo
  48. echo "No orphan resolver files (all match protected nameserver pattern). Nothing to clean."
  49. exit 0
  50. fi
  51. echo
  52. echo "=== TARGETS FOR REMOVAL ==="
  53. for f in "${TARGETS[@]}"; do
  54. echo " $f"
  55. done
  56. if [[ "$APPLY" -eq 0 ]]; then
  57. echo
  58. echo "DRY RUN — pass --apply to actually remove the files above."
  59. exit 0
  60. fi
  61. # Apply
  62. if [[ "$EUID" -ne 0 ]]; then
  63. echo "Need root. Re-running with sudo..."
  64. exec sudo "$0" --apply --protect="$PROTECT_PATTERNS"
  65. fi
  66. echo
  67. echo "=== REMOVING ==="
  68. for f in "${TARGETS[@]}"; do
  69. if rm -f "$f"; then
  70. echo "[OK] $f"
  71. else
  72. echo "[FAIL] $f"
  73. fi
  74. done
  75. echo
  76. echo "=== FLUSHING DNS CACHE ==="
  77. dscacheutil -flushcache
  78. killall -HUP mDNSResponder 2>/dev/null || true
  79. echo " done."
  80. echo
  81. echo "=== VERIFICATION ==="
  82. if out=$(dscacheutil -q host -a name google.com 2>&1) && echo "$out" | grep -q "ip_address:"; then
  83. addr=$(echo "$out" | awk '/ip_address:/{print $2; exit}')
  84. echo "[PASS] dscacheutil google.com -> $addr"
  85. else
  86. echo "[FAIL] dscacheutil still broken. Drill into scutil --dns and configuration profiles."
  87. fi
  88. if curl -sS -o /dev/null -w "[PASS] HTTPS google.com -> HTTP %{http_code}\n" --max-time 8 https://www.google.com 2>&1; then
  89. :
  90. else
  91. echo "[FAIL] HTTPS still broken."
  92. fi
  93. echo
  94. echo "=== AFTER ==="
  95. if [[ -n "$(ls -A /etc/resolver 2>/dev/null)" ]]; then
  96. for f in /etc/resolver/*; do
  97. [[ -f "$f" ]] || continue
  98. ns=$(awk '/^nameserver/{print $2}' "$f" | tr '\n' ',')
  99. echo " $f -> ${ns%,}"
  100. done
  101. else
  102. echo " /etc/resolver/ is now empty."
  103. fi