Actionable shell-scripting guidelines for automating DNS configuration, testing, and troubleshooting on macOS.
Transform tedious DNS management into reliable, scriptable workflows that eliminate configuration drift and network downtime across your macOS fleet.
DNS management on macOS typically involves clicking through System Settings, manually entering server addresses, and hoping changes stick across network switches. For teams managing multiple machines or frequent network changes, this approach creates several critical problems:
Configuration Drift: Manual changes lead to inconsistent DNS settings across devices, causing mysterious resolution failures that waste hours of debugging time.
Network Switching Chaos: Moving between office, home, and public networks often breaks custom DNS configurations, forcing you to reconfigure settings repeatedly.
Zero Automation: No reliable way to audit, backup, or bulk-apply DNS settings across a team or fleet of machines.
Security Gaps: Manual configuration makes it nearly impossible to enforce encrypted DNS (DoH/DoT) consistently or detect when devices fall back to insecure resolvers.
These Cursor Rules provide battle-tested shell scripting patterns for automating every aspect of macOS DNS management. Instead of clicking through settings panels, you get:
Idempotent Scripts: Run the same script multiple times without breaking existing configurations or creating duplicate entries.
Automatic Rollback: If DNS changes break connectivity, scripts automatically restore previous settings and flush caches to recover immediately.
Encrypted DNS Support: Seamlessly configure DNS over HTTPS (DoH) and DNS over TLS (DoT) with proper fallback handling.
Enterprise Integration: Generate MDM payloads and integrate with device management workflows for consistent fleet-wide DNS policies.
Instead of manually reconfiguring DNS when switching networks:
# Automatically detects network type and applies appropriate DNS
./dns-adaptive.sh
# Office network: Uses enterprise DoH endpoint
# Public WiFi: Forces encrypted DNS with privacy focus
# Home network: Optimizes for speed with local caching
Replace manual checking with automated compliance verification:
# Audit all interfaces and generate compliance report
./dns-audit.sh --export-json > dns-compliance-$(date +%F).json
# Automatically flags insecure configurations
# Identifies DNS leak risks across your fleet
Eliminate the "will this break my internet?" anxiety:
# Changes are tested and rolled back automatically if they fail
sudo ./dns-set.sh 1.1.1.1 1.0.0.1
# Automatic backup before changes
# Connectivity test with automatic rollback
# DNS cache flush and verification
Before: Each developer manually configures DNS, leading to inconsistent development environment behavior and hard-to-reproduce networking issues.
After: Single script deploys standardized DNS configuration across the entire team:
#!/usr/bin/env bash
# team-dns-setup.sh - Deploy team-wide DNS standards
readonly TEAM_DNS_SERVERS=("1.1.1.1" "1.0.0.1")
readonly BACKUP_DIR="/var/log/dns-backups"
# Backup current config with timestamp
backup_current_dns() {
mkdir -p "${BACKUP_DIR}"
scutil --dns > "${BACKUP_DIR}/dns-backup-$(date +%F-%H%M%S).txt"
}
apply_team_dns() {
for interface in $(networksetup -listallnetworkservices | tail -n +2); do
networksetup -setdnsservers "${interface}" "${TEAM_DNS_SERVERS[@]}"
printf "Applied team DNS to %s\n" "${interface}"
done
}
Before: Connecting to coffee shop WiFi exposes DNS queries to potential interception and manipulation.
After: Automatic detection and enforcement of encrypted DNS on untrusted networks:
# Detects public networks and enforces DoH automatically
detect_network_security() {
local ssid=$(networksetup -getairportnetwork en0 | cut -d' ' -f4-)
case "${ssid}" in
"Corporate-WiFi"|"Home-Network") apply_trusted_dns ;;
*) enforce_encrypted_dns ;;
esac
}
Before: Slow DNS resolution impacts local development, Docker builds, and package installations.
After: Intelligent DNS caching and performance optimization:
# Sets up local DNS cache with fallback to fastest public resolvers
setup_dev_dns() {
# Configure local unbound cache
configure_local_cache
# Benchmark and select fastest resolvers
benchmark_resolvers
# Apply optimized configuration
apply_dev_optimized_dns
}
Create your base DNS management script:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
readonly INTERFACE="${1:-Wi-Fi}"
readonly DNS_SERVERS=("${@:2}" "1.1.1.1" "1.0.0.1")
readonly BACKUP_DIR="/var/log/dns-backups"
readonly LOG_FILE="/var/log/dns-management.log"
# Ensure backup directory exists
mkdir -p "${BACKUP_DIR}"
log_action() {
printf "[%s] %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$1" | tee -a "${LOG_FILE}"
}
check_root() {
[[ $EUID -eq 0 ]] || {
echo "This script requires sudo privileges"
exit 1
}
}
backup_current_dns() {
local backup_file="${BACKUP_DIR}/dns-backup-$(date +%s).txt"
scutil --dns > "${backup_file}"
log_action "DNS configuration backed up to ${backup_file}"
}
validate_dns_servers() {
local ipv4_regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
for server in "${DNS_SERVERS[@]}"; do
[[ "${server}" =~ ${ipv4_regex} ]] || {
log_action "Invalid DNS server: ${server}"
exit 1
}
done
}
apply_dns_configuration() {
networksetup -setdnsservers "${INTERFACE}" "${DNS_SERVERS[@]}"
log_action "Applied DNS servers: ${DNS_SERVERS[*]} to ${INTERFACE}"
}
flush_dns_cache() {
sudo killall -HUP mDNSResponder
log_action "DNS cache flushed"
}
verify_dns_applied() {
scutil --dns | grep -q "${DNS_SERVERS[0]}" || {
log_action "DNS verification failed - rolling back"
networksetup -setdnsservers "${INTERFACE}" Empty
flush_dns_cache
exit 1
}
log_action "DNS configuration verified successfully"
}
cleanup() {
log_action "Error detected - rolling back DNS configuration"
networksetup -setdnsservers "${INTERFACE}" Empty
flush_dns_cache
}
main() {
trap cleanup ERR INT TERM
check_root
validate_dns_servers
backup_current_dns
apply_dns_configuration
flush_dns_cache
verify_dns_applied
log_action "DNS configuration completed successfully"
}
main "$@"
Extend your script to handle different network contexts:
detect_network_context() {
local ssid=$(networksetup -getairportnetwork en0 2>/dev/null | cut -d' ' -f4- || echo "Unknown")
local ip_info=$(scutil --get State:/Network/Interface/en0/IPv4 2>/dev/null || echo "")
case "${ssid}" in
"Corporate-"*) echo "corporate" ;;
"Home-"*|"Trusted-"*) echo "trusted" ;;
*) echo "public" ;;
esac
}
apply_context_aware_dns() {
local context=$(detect_network_context)
case "${context}" in
"corporate")
DNS_SERVERS=("10.0.1.1" "10.0.1.2") # Enterprise DNS
;;
"trusted")
DNS_SERVERS=("1.1.1.1" "1.0.0.1") # Fast public DNS
;;
"public")
DNS_SERVERS=("1.1.1.1" "1.0.0.1") # Privacy-focused
enforce_encrypted_dns
;;
esac
}
Build comprehensive testing into your workflow:
#!/usr/bin/env bash
# dns-test.sh - Comprehensive DNS functionality testing
test_dns_resolution() {
local test_domains=("google.com" "github.com" "cloudflare.com")
for domain in "${test_domains[@]}"; do
if ! ping -c1 -W1000 "${domain}" >/dev/null 2>&1; then
printf "DNS resolution failed for %s\n" "${domain}"
return 1
fi
done
printf "All DNS resolution tests passed\n"
return 0
}
check_dns_leaks() {
local expected_dns="1.1.1.1"
local actual_dns=$(dig +short @resolver1.opendns.com myip.opendns.com)
# Additional leak detection logic here
printf "DNS leak test completed\n"
}
benchmark_performance() {
# Use dig to test response times
for server in "${DNS_SERVERS[@]}"; do
local response_time=$(dig @"${server}" google.com | grep "Query time" | awk '{print $4}')
printf "Server %s response time: %s ms\n" "${server}" "${response_time}"
done
}
These rules transform DNS management from a manual, error-prone process into a reliable, auditable system that scales with your team's growth and security requirements.
You are an expert in macOS DNS Management automation using Bash/zsh, Apple command-line tooling (networksetup, scutil, mDNSResponder), and encrypted DNS protocols (DoH/DoT).
Key Principles
- Scripts must be idempotent: re-running them should not change state if the desired configuration already exists.
- Never assume interactive input; default to flags & environment variables.
- Always back up current DNS settings before modification (e.g. `scutil --dns > ~/dns-backup-$(date +%F).txt`).
- Prefer reputable public DNS (1.1.1.1, 8.8.8.8) or enterprise DoH endpoints; always include a secondary resolver for redundancy.
- Flush DNS and restart the affected interface after changes to guarantee propagation.
- Log every mutation to syslog or a well-known log file under `/var/log` with timestamps.
- Adopt encrypted DNS (DoH/DoT) or MDM payloads wherever possible for privacy and policy consistency.
Shell (bash / zsh)
- Start every script with `#!/usr/bin/env bash`, followed by `set -euo pipefail`.
- Use `$( ... )` for command subs; avoid legacy back-ticks.
- Quote all variable expansions: `"${var}"`.
- Prefer `printf` over `echo` for predictable output.
- Group reusable logic inside functions; export only `main`.
- Use lowercase, dash-separated filenames (`dns-set.sh`).
- Validate IPv4 with regex `^([0-9]{1,3}\.){3}[0-9]{1,3}$` and IPv6 with `^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$`.
- Keep configuration arrays at the top:
```bash
readonly DNS_SERVERS=("1.1.1.1" "1.0.0.1")
readonly INTERFACE="Wi-Fi"
```
- Use early returns inside functions to reduce nesting:
```bash
check_root || return 0
```
Error Handling and Validation
- Detect root: `[[ $EUID -eq 0 ]] || { echo "Run with sudo"; exit 1; }`.
- Verify interface exists via `networksetup -listallnetworkservices` before changes.
- Validate every supplied IP; exit if any resolver is malformed.
- On error, rollback with the backed-up settings using `networksetup -setdnsservers "$INTERFACE" "Empty"`.
- Use `trap 'cleanup' ERR INT TERM` to guarantee flush & rollback on failure.
- After changes, run:
```bash
scutil --dns | grep "nameserver\[[0-9]\]" || {
echo "DNS not applied"; exit 1;
}
sudo killall -HUP mDNSResponder
```
macOS DNS Tooling Framework
- networksetup
- `networksetup -setdnsservers "$INTERFACE" ${DNS_SERVERS[*]}` to apply.
- `networksetup -setdnsservers "$INTERFACE" Empty` to revert to DHCP.
- scutil
- Use `scutil --dns` for post-change verification.
- Modify per-domain resolvers via `/etc/resolver/<domain>` files.
- mDNSResponder
- Always flush cache with `sudo killall -HUP mDNSResponder`.
- DoH / DoT
- Configure via per-interface profiles or `profiles install -type configuration encrypted_dns.mobileconfig`.
- Ensure fallback traditional DNS is removed to avoid leaks.
- MDM
- Use the DNS Settings payload; sample plist keys:
```xml
<key>DNS</key>
<dict>
<key>DomainName</key><string>example.com</string>
<key>ServerAddresses</key>
<array><string>1.1.1.1</string><string>1.0.0.1</string></array>
</dict>
```
Additional Sections
Testing
- Benchmark options with NameBench; parse and store JSON output for CI trend tracking.
- Quick resolution test: `ping -c1 example.com` and check for non-zero RC.
- Use public DNS leak sites and `curl -s https://cloudflare-dns.com/dns-query -H 'accept: application/dns-json' --data-urlencode "name=example.com"` to ensure DoH.
Performance
- Local caching: enable `unbound` in recursive mode on `127.0.0.1` and prepend it to resolver list.
- Periodically benchmark and rotate fastest resolvers via cron.
Security
- Enforce DoH when on public Wi-Fi; detect network type via `scutil --get "State:/Network/Interface/${INTERFACE}/IPv4"`.
- Store any credentials (for private DoH) in the Keychain, accessed with `security find-generic-password`.
Documentation
- Include usage header at top of each script:
```bash
# dns-set.sh --idempotently configure DNS on macOS
# Usage: sudo ./dns-set.sh 1.1.1.1 1.0.0.1
```
- Reference Apple docs: `man networksetup`, `man scutil`, Apple Platform Deployment Guide (MDM payloads).
Example Minimal Script Skeleton
```bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
readonly INTERFACE="Wi-Fi"
readonly DNS_SERVERS=("${@:-1.1.1.1}" "1.0.0.1")
check_root() { [[ $EUID -eq 0 ]]; }
backup() { scutil --dns > "/tmp/dns-backup-$(date +%s).txt"; }
apply_dns() { networksetup -setdnsservers "$INTERFACE" "${DNS_SERVERS[@]}"; }
flush() { killall -HUP mDNSResponder; }
cleanup() { echo "Error detected; reverting."; networksetup -setdnsservers "$INTERFACE" Empty; flush; }
trap cleanup ERR INT TERM
check_root || { echo "Please run with sudo"; exit 1; }
backup
apply_dns
flush
scutil --dns | grep "${DNS_SERVERS[0]}" && echo "DNS applied successfully."
```