dns-audit.sh 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. #!/usr/bin/env bash
  2. # net-ops :: macos/dns-audit.sh
  3. # Deep DNS forensics for macOS. Use when probe.sh shows rung 4 (dig) PASS
  4. # but rung 5 (dscacheutil) FAIL — that signature points at a hook in the
  5. # macOS resolver chain.
  6. set -u
  7. # Shared terminal toolkit (skills/_lib/term.sh) — colorized, ASCII-aware section
  8. # headers. Dump/fixer output isn't a checklist, so it uses the bare-header style
  9. # (a deliberate exception per docs/TERMINAL-DESIGN.md), not the enclosing panel.
  10. __nlib="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../_lib" 2>/dev/null && pwd || true)"
  11. if [ -n "${__nlib:-}" ] && [ -f "$__nlib/term.sh" ]; then . "$__nlib/term.sh"; term_init
  12. else term_header() { printf '== %s ==\n' "${1:-}"; }; fi
  13. # shellcheck source=../_lib/redact.sh
  14. source "$(dirname "$0")/../_lib/redact.sh"
  15. parse_redact_flag "$@"
  16. maybe_redact_self "$@"
  17. term_header "scutil --dns (FULL)"
  18. scutil --dns 2>/dev/null
  19. echo
  20. term_header "/etc/resolver/* (per-domain DNS overrides — VPN clients use these)"
  21. if [[ -d /etc/resolver ]] && [[ -n "$(ls -A /etc/resolver 2>/dev/null)" ]]; then
  22. for f in /etc/resolver/*; do
  23. [[ -f "$f" ]] || continue
  24. echo "--- $f ---"
  25. echo " modified: $(stat -f '%Sm' "$f" 2>/dev/null || stat -c '%y' "$f" 2>/dev/null)"
  26. cat "$f" | sed 's/^/ /'
  27. done
  28. else
  29. echo "/etc/resolver/ empty or missing — no per-domain overrides."
  30. fi
  31. echo
  32. term_header "Configuration profiles with DNS settings"
  33. profiles list -type configuration 2>/dev/null | head -40
  34. echo
  35. echo " (run 'sudo profiles show -type configuration' for full payloads)"
  36. echo
  37. term_header "/etc/hosts (non-comment lines)"
  38. grep -vE '^\s*(#|$)' /etc/hosts 2>/dev/null || echo " (no custom entries)"
  39. echo
  40. term_header "/etc/resolv.conf (legacy, usually a stub on macOS)"
  41. if [[ -f /etc/resolv.conf ]]; then
  42. cat /etc/resolv.conf
  43. else
  44. echo " not present"
  45. fi
  46. echo
  47. term_header "mDNSResponder state"
  48. if pgrep -x mDNSResponder >/dev/null; then
  49. pid=$(pgrep -x mDNSResponder | head -1)
  50. echo "PID: $pid"
  51. ps -o pid,etime,command -p "$pid" 2>/dev/null
  52. fi
  53. echo
  54. term_header "Network services priority order"
  55. networksetup -listnetworkserviceorder 2>/dev/null | head -30
  56. echo
  57. term_header "DNS servers per active service"
  58. networksetup -listallnetworkservices 2>/dev/null | tail -n +2 | while read -r svc; do
  59. [[ "$svc" == \** ]] && continue # disabled
  60. dns=$(networksetup -getdnsservers "$svc" 2>/dev/null)
  61. echo " $svc: $dns"
  62. done
  63. echo
  64. term_header "Search domains per active service"
  65. networksetup -listallnetworkservices 2>/dev/null | tail -n +2 | while read -r svc; do
  66. [[ "$svc" == \** ]] && continue
  67. sd=$(networksetup -getsearchdomains "$svc" 2>/dev/null)
  68. echo " $svc: $sd"
  69. done
  70. echo
  71. term_header "Third-party network kexts loaded"
  72. kextstat 2>/dev/null | grep -iE 'cisco|anyconnect|proton|mullvad|nord|littlesnitch|lulu|nextdns|warp' || echo " (none detected)"
  73. echo
  74. term_header "ATTRIBUTION HINTS"
  75. # Aggregate every nameserver we can see across all resolver surfaces, then
  76. # pattern-match each unique entry to a known VPN/DNS client signature.
  77. ns_list=$( {
  78. [[ -d /etc/resolver ]] && grep -h '^nameserver' /etc/resolver/* 2>/dev/null | awk '{print $2}'
  79. scutil --dns 2>/dev/null | awk '/nameserver\[[0-9]+\]/{print $3}'
  80. networksetup -listallnetworkservices 2>/dev/null | tail -n +2 | while read -r svc; do
  81. [[ "$svc" == \** ]] && continue
  82. networksetup -getdnsservers "$svc" 2>/dev/null | grep -E '^[0-9a-f:.]+$' || true
  83. done
  84. } | sort -u | grep -v '^$' )
  85. if [[ -z "$ns_list" ]]; then
  86. echo " (no nameservers found)"
  87. fi
  88. while read -r n; do
  89. [[ -z "$n" ]] && continue
  90. case "$n" in
  91. 10.2.0.*) echo " $n :: likely Proton VPN gateway" ;;
  92. 10.64.0.*) echo " $n :: likely Mullvad gateway" ;;
  93. 10.211.*|10.212.*) echo " $n :: likely Cisco AnyConnect" ;;
  94. 10.5.0.*) echo " $n :: likely NordVPN gateway" ;;
  95. 100.100.100.100) echo " $n :: Tailscale MagicDNS (expected)" ;;
  96. 127.0.0.1|127.0.0.2|::1) echo " $n :: local DNS proxy (NextDNS, AdGuard, dnsmasq, etc.)" ;;
  97. 1.1.1.1|1.0.0.1) echo " $n :: Cloudflare public DNS" ;;
  98. 8.8.8.8|8.8.4.4) echo " $n :: Google public DNS" ;;
  99. 9.9.9.9|149.112.112.112) echo " $n :: Quad9 public DNS" ;;
  100. esac
  101. done <<< "$ns_list"