Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

bci/Clean-Routes-macOS

Open more actions menu

Repository files navigation

macOS Route & DNS Management Suite

CI

A collection of bash scripts for managing macOS network routes and conditional DNS resolvers on macOS. Designed for split-DNS VPN environments — site-to-site Meraki MX setups, client VPNs, and local-development domain isolation — where you need fine-grained control over which DNS servers answer for which domains and which traffic bypasses VPN tunnels via local routing.

All scripts are macOS-only, require no Homebrew dependencies, and use only tools that ship with macOS (bash, python3, networksetup, netstat, route, dscacheutil, scutil).


Contents

Script Root? Purpose
dns-macos-routes.sh write ops Manage /etc/resolver/ profiles from a JSON config
add-macos-routes.sh Add, list, diff, and delete named route sets
clean-macos-routes.sh Remove static routes by network pattern or set
diagnose-macos-routes.sh Read-only diagnostics (routing table, DNS, VPN, ARP)
report-macos-routes.sh Snapshot report: conditional DNS, per-service routes, kernel table, VPN tunnels
watch-macos-routes.sh Poll routing table and /etc/resolver/ for changes
backup-restore-routes.sh restore: ✅ JSON snapshot, restore, diff, and prune for routes
reset-macos-network.sh Flush routes, caches, cycle interfaces, reset to DHCP
lib/common.sh Shared helpers (sourced by all scripts; not executed directly)

local/ — Site-specific shortcut scripts live here (excluded from git via .gitignore). See Local Shortcut Scripts below.


Requirements

  • macOS Ventura (13) or later (tested; may work on Monterey)
  • zsh 5.8+ (ships with macOS 11+; all scripts use #!/usr/bin/env zsh)
  • python3 (ships with Xcode Command Line Tools; install with xcode-select --install)
  • sudo for any operation that writes routes or /etc/resolver/

Optional (development / testing)

  • timeout (GNU coreutils) — when present, make test limits each test file to 60 s so a hung test can't block CI or your terminal indefinitely. Not required to run the scripts themselves.

    brew install coreutils   # provides gtimeout (and timeout) on macOS

Installation

git clone https://github.com/<your-org>/macos-routes.git
cd macos-routes
chmod +x *.sh
make setup   # initialises bats submodules + installs pre-push hook

make setup only needs to be run once after cloning. After that, git push will automatically run make all (lint + tests) and block the push if either fails. To skip in an emergency: git push --no-verify.

No package manager, no virtualenv, no build step.


JSON Configuration File

All scripts that manage named sets or DNS profiles read from a shared JSON file.

Default location: ~/.config/macos-routes/routes.json

Override with --load <path> on any script.

Schema

{
  "sets": {
    "office": [
      { "dest": "10.0.0.0/8",    "gateway": "192.168.1.1" },
      { "dest": "172.16.0.0/12", "gateway": "192.168.1.1" }
    ]
  },
  "dns": {
    "local_router": "192.168.1.1",
    "profiles": {
      "corp": {
        "domain":      "corp.example.com",
        "nameservers": ["10.1.0.53", "10.1.0.54"],
        "networks":    ["10.1.0.0/16"],
        "test_host":   "dc01.corp.example.com",
        "ping_host":   "dc01.corp.example.com"
      },
      "dev": {
        "domain":      "dev.local",
        "nameservers": ["10.2.0.1"],
        "networks":    ["10.2.0.0/24"],
        "test_host":   "devserver.dev.local"
      }
    }
  }
}

Route set fields:

Field Required Description
dest Destination in CIDR notation
gateway Gateway IP
interface Bind to a specific interface (e.g. en0)

DNS profile fields:

Field Required Description
domain DNS suffix written to /etc/resolver/<domain>
nameservers Array of DNS server IPs
networks CIDRs to route via local_router when --with-routes is used
local_router Per-profile gateway; falls back to dns.local_router
test_host FQDN to probe with dscacheutil after applying
ping_host FQDN to ping -c2 after applying

Script Reference

dns-macos-routes.sh

Manage macOS conditional DNS profiles. Writes /etc/resolver/<domain> files so mDNSResponder uses specific nameservers for specific domains — without changing the global DNS configuration.

# List all profiles in routes.json
dns-macos-routes.sh --list

# Show current /etc/resolver/ files
dns-macos-routes.sh --show

# Apply resolver files + add static routes for networks
sudo dns-macos-routes.sh --apply corp dev --with-routes

# Apply resolver files only (VPN handles routing)
sudo dns-macos-routes.sh --apply corp dev

# Compare JSON profile vs live state
dns-macos-routes.sh --diff corp dev

# Remove resolver files + remove their routes
sudo dns-macos-routes.sh --remove corp dev --with-routes

# Remove all /etc/resolver/ files
sudo dns-macos-routes.sh --remove-all

# Capture a live /etc/resolver/ file into the JSON
dns-macos-routes.sh --save corp --domain corp.example.com

# Backup current /etc/resolver/ to JSON archive
dns-macos-routes.sh --backup

# Restore from archive
sudo dns-macos-routes.sh --restore ~/.config/macos-routes/backups/2026-01-01T12-00-00-dns.json

# Run DNS + ping tests
dns-macos-routes.sh --test corp dev

# Flush DNS cache manually
sudo dns-macos-routes.sh --flush-cache

Key options:

Flag Needs root Description
--with-routes Also add/remove networks routes via networksetup
--local-router <IP> Override the gateway IP for this invocation
--service <svc> Override which networksetup service receives routes
--load <path> Use a different routes.json file
-n, --dry-run Print commands without executing
-y, --yes Skip confirmation prompts

add-macos-routes.sh

Manage named sets of static routes stored in routes.json.

# Save current networksetup routes as a named set
add-macos-routes.sh --save office

# Apply a named set
sudo add-macos-routes.sh --apply office

# List all sets
add-macos-routes.sh --list

# Diff a set against the live routing table
add-macos-routes.sh --diff office

# Delete a set from JSON
add-macos-routes.sh --delete office

clean-macos-routes.sh

Remove static routes from the routing table by network pattern, set name, or all at once.

# Remove all static routes matching 10.
sudo clean-macos-routes.sh --network 10.

# Remove routes from a named set
sudo clean-macos-routes.sh --set office

# Remove all static (S-flag) routes
sudo clean-macos-routes.sh --all

# Dry-run preview
clean-macos-routes.sh --network 172. --dry-run

diagnose-macos-routes.sh

Read-only diagnostics. No root required.

# Full human-readable report (routing table, DNS, VPN, ARP, /etc/resolver/)
diagnose-macos-routes.sh

# Both IPv4 and IPv6
diagnose-macos-routes.sh --all

# JSON output for scripting
diagnose-macos-routes.sh --json | jq .conditional_dns

# Ping each default gateway
diagnose-macos-routes.sh --check-gateway

The JSON output includes a "conditional_dns" key mapping each filename in /etc/resolver/ to its raw content.


report-macos-routes.sh

Read-only snapshot report. No root required. Colour-aware (respects NO_COLOR and TTY detection).

Produces four sections in one pass:

Section Source
Conditional DNS Every file in /etc/resolver/ with its nameserver/domain lines
Static Routes networksetup -getadditionalroutes for every service; routes cross-checked against routes.json and tagged [OK] or [EXTRA]
Kernel Routing Table netstat -rn -f inet filtered to gateway/tunnel entries; noise (ARP, multicast, loopback) stripped
VPN / Tunnel Interfaces Any utun*, ppp*, or ipsec* interface with a bound IP, plus MTU
# Colour report (auto-detected)
report-macos-routes.sh

# Plain text (safe for logs / copy-paste)
report-macos-routes.sh --no-color

# Use a non-default routes.json for [OK]/[EXTRA] tagging
report-macos-routes.sh --routes-json /path/to/routes.json

Options:

Flag Description
--no-color Disable ANSI colour output
--routes-json <path> JSON file used to classify routes as [OK] vs [EXTRA] (default: ~/.config/macos-routes/routes.json)

watch-macos-routes.sh

Poll the routing table and /etc/resolver/ for changes.

# Watch routing table (5s interval)
watch-macos-routes.sh

# Watch both routes and /etc/resolver/
watch-macos-routes.sh --watch-dns

# Auto-reapply a named set when its routes disappear
sudo watch-macos-routes.sh --restore-set office --interval 10

# Auto-reapply a DNS profile when its resolver file is deleted
sudo watch-macos-routes.sh --watch-dns --restore-dns corp

# Filter to a specific prefix
watch-macos-routes.sh --filter 10. --watch-dns

# Single diff and exit
watch-macos-routes.sh --once

backup-restore-routes.sh

JSON snapshots of the routing table and optionally /etc/resolver/.

# Snapshot routes
backup-restore-routes.sh --backup

# Snapshot routes AND /etc/resolver/ files
backup-restore-routes.sh --backup --include-dns

# List available backups
backup-restore-routes.sh --list-backups

# Diff a backup vs current state (routes + DNS)
backup-restore-routes.sh --diff ~/.config/macos-routes/backups/2026-01-01.json --list-dns

# Restore routes
sudo backup-restore-routes.sh --restore ~/.config/macos-routes/backups/2026-01-01.json

# Restore routes AND /etc/resolver/ files
sudo backup-restore-routes.sh --restore ~/.config/macos-routes/backups/2026-01-01.json --include-dns

# Prune old backups, keep 5
backup-restore-routes.sh --prune 5

reset-macos-network.sh

Full network stack reset: flushes routing table, ARP/DNS caches, cycles interfaces, resets services to DHCP.

# Dry-run preview
sudo reset-macos-network.sh --dry-run

# Reset (with confirmation prompt)
sudo reset-macos-network.sh

# Skip prompt
sudo reset-macos-network.sh --yes

# Preserve default route after flush
sudo reset-macos-network.sh --keep-default

# Remove only static (S-flag) routes, then exit
sudo reset-macos-network.sh --flush-static

# Remove all /etc/resolver/ files, then exit
# (does NOT run as part of the default reset — opt-in only)
sudo reset-macos-network.sh --flush-dns-resolvers

# Snapshot routes before resetting
sudo reset-macos-network.sh --backup --yes

Local Shortcut Scripts

Place site-specific shortcut scripts in the local/ directory. This directory is excluded from git via .gitignore so credentials, real IPs, and internal domain names never leave your machine.

Example: home office context

Create local/home.sh:

#!/usr/bin/env bash
# Home office: site-to-site VPN is up.
# Applies DNS resolvers AND static routes via the local router.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

sudo "${SCRIPT_DIR}/dns-macos-routes.sh" \
  --apply corp dev \
  --with-routes \
  --local-router 192.168.1.1 \
  "$@"

"${SCRIPT_DIR}/dns-macos-routes.sh" --test corp dev

Example: travel / client VPN context

Create local/travel.sh:

#!/usr/bin/env bash
# Travel: client VPN is active, handles all routing.
# Applies DNS resolvers only — no static routes.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

# Remove any leftover home-office routes first
sudo "${SCRIPT_DIR}/dns-macos-routes.sh" --remove corp dev --with-routes "$@" || true

# Apply DNS-only (VPN handles routing)
sudo "${SCRIPT_DIR}/dns-macos-routes.sh" --apply corp dev "$@"

"${SCRIPT_DIR}/dns-macos-routes.sh" --test corp dev

Then make them executable and add to your shell profile if desired:

chmod +x local/home.sh local/travel.sh

How Conditional DNS Works on macOS

macOS's mDNSResponder reads files from /etc/resolver/. Each file is named after the DNS suffix it applies to, and contains nameserver lines:

nameserver 10.1.0.53
nameserver 10.1.0.54
domain corp.example.com

With this file in place, any query for *.corp.example.com goes to 10.1.0.53 (and 10.1.0.54 as a fallback). All other queries use the global DNS setting. No restart or reload is needed — the change takes effect immediately.

This is the same mechanism used by corporate MDM profiles and VPN clients, and it coexists with them cleanly.


Split-DNS VPN Patterns

Site-to-site VPN (home office / Meraki MX)

The VPN provides a direct Layer-3 path to the remote site. Traffic must be explicitly routed via the local router (not via any VPN default route):

                  ┌─────────────────────────────────────────────┐
  macOS laptop    │  Home router (Meraki MX)                    │
  ─────────────   │  ─────────────────────────────────────────  │
  DNS query for   │  MX site-to-site VPN                        │
  corp.example →  │  ─────────────────────────────────────────  │
    resolver      │  routes 10.1.0.0/16 via MX peer             │  → DNS @ 10.1.0.53
  file answers    │                                             │     (remote site)
                  └─────────────────────────────────────────────┘

Use --with-routes to configure both resolver and routing table together.

Client VPN (travel / GlobalProtect / AnyConnect / Meraki Client VPN)

The VPN client installs its own routes. You only need conditional DNS:

  ┌──────────────────────┐                        ┌─────────────────────┐
  │  macOS laptop        │   Client VPN tunnel    │  Remote site        │
  │  ─────────────────   │  ════════════════════  │  ─────────────────  │
  │  /etc/resolver/      │                        │                     │
  │  corp.example.com    │  DNS query for         │  DNS server         │
  │  → 10.1.0.53         │  corp.example  ──────→ │  10.1.0.53          │
  │                      │                        │                     │
  │  VPN client pushes   │  All other traffic     │                     │
  │  routes + resolver   │  uses global DNS  ───→ │  (not this site)    │
  └──────────────────────┘                        └─────────────────────┘

Use --apply without --with-routes. Avoid adding static routes that conflict with what the VPN client installs.


Diagnostics Quick Reference

# Quick snapshot of everything (DNS, routes, kernel table, VPN tunnels)
report-macos-routes.sh

# Same, plain text for sharing / pasting into a ticket
report-macos-routes.sh --no-color

# Are my resolver files in place?
diagnose-macos-routes.sh | grep -A10 "Conditional DNS"

# Are my static routes active?
diagnose-macos-routes.sh | grep -A10 "Additional Routes"

# Full diff of a DNS profile vs live state
dns-macos-routes.sh --diff corp

# Is DNS resolving correctly?
dscacheutil -q host -a name dc01.corp.example.com

# Are the nameservers reachable?
ping -c2 10.1.0.53

Common Workflows

Setting up a new site profile

# 1. Add profile to routes.json (or let --save capture a live file)
dns-macos-routes.sh --save corp --domain corp.example.com

# 2. Edit ~/.config/macos-routes/routes.json to add nameservers, networks, etc.

# 3. Apply
sudo dns-macos-routes.sh --apply corp --with-routes

# 4. Verify
dns-macos-routes.sh --diff corp

Switching from home to travel

# Remove routes (DNS resolvers stay unless you --remove them)
sudo dns-macos-routes.sh --remove corp dev --with-routes
# Then connect client VPN — it will push its own routes

Resetting everything cleanly

# Back up first
backup-restore-routes.sh --backup --include-dns

# Reset network stack
sudo reset-macos-network.sh --yes

# Remove conditional DNS resolvers
sudo reset-macos-network.sh --flush-dns-resolvers --yes

Design Principles

  • No dependencies — zsh + python3 + macOS system tools only
  • Dry-run first — every destructive command supports --dry-run
  • Atomic writes — resolver files and JSON are written to .tmp then mv'd
  • JSON-backed — all named sets and profiles are stored in one file; easy to version-control separately or share across machines
  • Composable — scripts are focused; combine them for complex workflows
  • Root-minimal — read-only operations never require root; writes are explicit

Compatibility

macOS Status
Sequoia (15) ✅ Tested
Sonoma (14) ✅ Tested
Ventura (13) ✅ Tested
Monterey (12) ⚠️ Should work; not regularly tested
< Monterey ❌ Not supported

License

MIT — see LICENSE if present, otherwise consider it freely reusable.

About

Ways to clean up the macOS routing tables

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Morty Proxy This is a proxified and sanitized view of the page, visit original site.