Last active
October 6, 2025 00:42
-
-
Save MohamedElashri/b051792140e4f3ad66f20e25df6e9119 to your computer and use it in GitHub Desktop.
Change DNS servers on Mac based on SSID
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" | |
| "http://www.apple.com/DTDs/PropertyList-1.0.dtd"\> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>Label</key><string>com.melashri.ssid-dns</string> | |
| <key>ProgramArguments</key> | |
| <array> | |
| <string>/Users/melashri/.local/bin/ssid-dns</string> | |
| </array> | |
| <key>RunAtLoad</key><true/> | |
| <key>KeepAlive</key> | |
| <dict> | |
| <key>NetworkState</key><true/> | |
| </dict> | |
| <key>StartInterval</key><integer>60</integer> | |
| <key>StandardOutPath</key><string>/tmp/ssid-dns.out</string> | |
| <key>StandardErrorPath</key><string>/tmp/ssid-dns.err</string> | |
| </dict> | |
| </plist> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # --- Policy --- | |
| HOME_SSIDS=("HomeNetworkI" "HomeNetworkII") # home Wi-Fi names | |
| CERN_SSID_REGEX='^(CERN|eduroam|CERN-Visitor)$' | |
| CERN_DNS="137.138.16.5 137.138.17.5" | |
| FALLBACK_DNS="9.9.9.9" | |
| # CERN network ranges (add as needed) | |
| CERN_NETS=( "137.138.0.0/16" "128.142.0.0/16" "188.184.0.0/15" "194.12.128.0/17" ) | |
| # --- Helpers --- | |
| get_wifi_device() { | |
| /usr/sbin/networksetup -listallhardwareports | awk ' | |
| /^Hardware Port: (Wi-Fi|AirPort)$/ {f=1; next} | |
| f && /^Device:/ {print $2; exit} | |
| ' | |
| } | |
| get_service_for_device() { | |
| local dev="$1" | |
| /usr/sbin/networksetup -listnetworkserviceorder | awk -v d="$dev" ' | |
| /^\([0-9]+\)/ {svc=$0; sub(/^\([0-9]+\) /,"",svc); gsub(/[[:space:]]+$/,"",svc)} | |
| /Device: / && $0 ~ "Device: " d {print svc; exit} | |
| ' | |
| } | |
| get_default_if() { | |
| /sbin/route -n get default 2>/dev/null | awk '/interface:/{print $2; exit}' | |
| } | |
| get_gateway_ip() { | |
| /sbin/route -n get default 2>/dev/null | awk '/gateway:/{print $2; exit}' | |
| } | |
| cidr_match() { # cidr_match <ip> <cidr> | |
| python3 - "$1" "$2" <<'PY' | |
| import ipaddress,sys | |
| try: | |
| print(ipaddress.ip_address(sys.argv[1]) in ipaddress.ip_network(sys.argv[2], strict=False)) | |
| except Exception: | |
| print(False) | |
| PY | |
| } | |
| get_search_domains() { | |
| /usr/sbin/scutil --dns | awk -F': ' ' | |
| /^resolver #[0-9]+$/ {blk=1; next} | |
| blk && NF==0 {blk=0} | |
| blk && /^[[:space:]]*search domain/ {print $2} | |
| blk && /^[[:space:]]*domain/ {print $2} | |
| ' | sed 's/[[:space:]]*$//' | |
| } | |
| get_ssid() { | |
| local ssid dev | |
| dev="$(get_wifi_device || true)" | |
| # 0) networksetup with device (handles most cases) | |
| if [[ -n "${dev:-}" ]]; then | |
| if out=$(/usr/sbin/networksetup -getairportnetwork "$dev" 2>/dev/null); then | |
| # "Current Wi-Fi Network: <SSID>" or "You are not associated ..." | |
| if [[ "$out" =~ ^Current[[:space:]]Wi-?Fi[[:space:]]Network:\ (.*)$ ]]; then | |
| printf '%s\n' "${BASH_REMATCH[1]}"; return 0 | |
| fi | |
| fi | |
| fi | |
| # 1) system_profiler | |
| ssid="$(/usr/sbin/system_profiler SPAirPortDataType 2>/dev/null | awk ' | |
| /Current Network Information:/ {f=1; next} | |
| f && /^[[:space:]]*SSID:/ {sub(/^[[:space:]]*SSID:[[:space:]]*/,""); print; exit} | |
| f && NF==0 {f=0} | |
| ')" | |
| [[ -n "${ssid:-}" ]] && { printf '%s\n' "$ssid"; return 0; } | |
| # 2) ioreg | |
| ssid="$(/usr/sbin/ioreg -l | awk -F\" '/SSID_STR/ {print $4; exit}')" | |
| [[ -n "${ssid:-}" ]] && { printf '%s\n' "$ssid"; return 0; } | |
| # 3) scutil --nwi | |
| ssid="$(/usr/sbin/scutil --nwi 2>/dev/null | awk -F': ' '/SSID:/ {print $2; exit}')" | |
| [[ -n "${ssid:-}" ]] && { printf '%s\n' "$ssid"; return 0; } | |
| # 4) airport fallbacks | |
| for p in \ | |
| /System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport \ | |
| /System/Library/PrivateFrameworks/Apple80211.framework/Versions/A/Resources/airport \ | |
| /System/Library/PrivateFrameworks/AppleWiFi.framework/Versions/Current/Resources/airport | |
| do | |
| [[ -x "$p" ]] || continue | |
| ssid="$("$p" -I 2>/dev/null | awk -F': ' '/ SSID/ {print $2; exit}')" | |
| [[ -n "${ssid:-}" ]] && { printf '%s\n' "$ssid"; return 0; } | |
| done | |
| return 1 | |
| } | |
| get_current_dns() { | |
| /usr/sbin/networksetup -getdnsservers "$1" 2>/dev/null | awk ' | |
| /^There (are|aren.t) any DNS Servers/ {print "Empty"; exit} | |
| {print} | |
| ' | paste -sd' ' - | |
| } | |
| set_dns_if_needed() { | |
| local service="$1" target="$2" current | |
| current="$(get_current_dns "$service")" | |
| if [[ "$target" == "$current" ]]; then | |
| echo "[dns] No change ($current) on $service" | |
| return | |
| fi | |
| if [[ "$target" == "Empty" ]]; then | |
| /usr/sbin/networksetup -setdnsservers "$service" Empty | |
| else | |
| # shellcheck disable=SC2086 | |
| /usr/sbin/networksetup -setdnsservers "$service" $target | |
| fi | |
| echo "[dns] Applied: $target on $service" | |
| } | |
| # --- Detect environment --- | |
| wifi_dev="$(get_wifi_device || true)" | |
| def_if="$(get_default_if || true)" | |
| prim_svc="$(get_service_for_device "${def_if:-}" || true)" | |
| ssid="$(get_ssid || true)" | |
| gw="$(get_gateway_ip || true)" | |
| domains="$(get_search_domains || true)" | |
| # CERN heuristics (any true -> use CERN DNS) | |
| cern_ssid=false | |
| cern_gw=false | |
| cern_domain=false | |
| [[ -n "${ssid:-}" && "$ssid" =~ $CERN_SSID_REGEX ]] && cern_ssid=true | |
| if [[ -n "${gw:-}" ]]; then | |
| for net in "${CERN_NETS[@]}"; do | |
| if [[ "$(cidr_match "$gw" "$net")" == "True" ]]; then cern_gw=true; break; fi | |
| done | |
| fi | |
| if printf '%s\n' "$domains" | grep -qiE '(^|[[:space:]])([^.]*\.)?cern\.ch$'; then cern_domain=true; fi | |
| echo "[ssid-dns] def_if=${def_if:-?} prim_svc=${prim_svc:-?} ssid=${ssid:-<none>} gw=${gw:-<none>}" | |
| echo "[ssid-dns] domains: $(printf '%s' "$domains" | tr '\n' ' | ')" | |
| echo "[ssid-dns] flags: CERN_SSID=$cern_ssid CERN_GW=$cern_gw CERN_DOMAIN=$cern_domain" | |
| # --- Decide target DNS --- | |
| target="$FALLBACK_DNS" # default | |
| if [[ -n "${ssid:-}" ]] && printf '%s\n' "${HOME_SSIDS[@]}" | grep -qx -- "$ssid"; then | |
| target="Empty" # home Wi-Fi -> router DNS | |
| elif $cern_ssid || $cern_gw || $cern_domain; then | |
| target="$CERN_DNS" # CERN -> CERN DNS | |
| fi | |
| # --- Apply --- | |
| if [[ -n "${prim_svc:-}" ]]; then | |
| set_dns_if_needed "$prim_svc" "$target" | |
| else | |
| echo "[dns] Could not resolve primary service; skipping change." | |
| fi | |
| # --- Show effective default resolver (resolver #1) --- | |
| /usr/sbin/scutil --dns | awk ' | |
| $0=="resolver #1" {f=1; next} | |
| f && NF==0 {f=0} | |
| f && /nameserver\[/ {print} | |
| ' |
Comments are disabled for this gist.