splitdns is a small, safe command-line tool for managing macOS
/etc/resolver configuration to enable suffix-based Split DNS: routing
queries for specific domain suffixes (e.g. lab.dev, corp.example.com) to a
dedicated DNS server, while everything else uses your normal resolvers.
It is macOS-only, ships as a single static binary, and never shells out through
sh -c — every external command is invoked with separated arguments.
Read this in another language: 简体中文
- Create, update, remove, list and inspect
/etc/resolverentries safely. - Atomic writes (same-directory temp file →
fsync→chmod 0644→rename) with automatic backup before any overwrite. - Symlink protection and strict path containment: operations are refused if
the target is a symlink, is not a regular file, or would escape
/etc/resolver. - Config-preserving parser: comments, blank lines, ordering, duplicate nameservers and unknown-but-valid directives are all retained.
- Diagnostics (
check) and an end-to-end resolution test (test) that probes DNS directly using a hand-written minimal DNS query (UDP with timeout, TCP fallback) — never relying on a bare TCP connect. - Clear separation between "config written" and "cache flushed", so partial success is always visible.
- Machine-readable
--jsonoutput for every command.
- macOS (Apple Silicon or Intel).
scutil,dscacheutil,killall(part of macOS).- Write operations (
add,set,remove,flush) modify/etc/resolverand therefore requiresudo.splitdnsnever auto-elevates; it prints the exactsudo splitdns …command to run.
brew tap soulteary/tap
brew install soulteary/tap/splitdnsVerify:
splitdns versiongo install github.com/soulteary/splitdns@latestOr build locally:
git clone https://github.com/soulteary/splitdns.git
cd splitdns
make build # produces ./splitdnsPrebuilt darwin/amd64 and darwin/arm64 archives are published via
GoReleaser on the releases page.
splitdns <command> [flags]
Commands:
add Add a new resolver entry for a domain suffix
set Update fields of an existing resolver entry
remove Remove a resolver entry (alias: rm)
list List resolver entries (alias: ls)
show Show a resolver entry
check Run configuration and environment diagnostics
test Test resolution of a hostname through the split-DNS layers
flush Flush macOS DNS caches
completion Generate shell completion script
version Print version information
Global flags:
--dry-run Show planned changes without modifying the system
--json Emit machine-readable JSON
--quiet Suppress non-essential output
--no-color Disable colored output
Route lab.dev to a local DNS server on port 53:
sudo splitdns add lab.dev --nameserver 127.0.0.1 --port 53Point multiple nameservers and a custom port:
sudo splitdns add corp.example.com \
--nameserver 10.0.0.53 --nameserver 10.0.1.53 --port 5353Preview changes without writing anything:
splitdns add lab.dev --dry-runUpdate only the port of an existing entry (other fields and comments preserved):
sudo splitdns set lab.dev --port 5353List and inspect:
splitdns list
splitdns show lab.dev
splitdns show lab.dev --raw # original file contentsRemove (with confirmation, or --yes for automation):
sudo splitdns remove lab.dev
sudo splitdns remove lab.dev --yesDiagnose and test:
splitdns check # validate all entries + environment
splitdns check lab.dev # focus on one suffix
splitdns test host.lab.dev # three-layer resolution testFlush caches manually:
sudo splitdns flushCreates a new resolver file. Fails if one already exists unless --force
(which backs up the existing file first).
| Flag | Default | Description |
|---|---|---|
--nameserver |
127.0.0.1 |
Nameserver IP (repeatable) |
--port |
53 |
DNS server port |
--search-order |
search_order value |
|
--timeout |
timeout value (seconds) |
|
--force |
Allow overwrite and relax .local/single-label warnings |
|
--no-flush |
Do not flush DNS caches after writing | |
--backup-dir |
system temp | Directory for pre-overwrite backups |
The global --dry-run flag previews planned changes (target path, planned
content and cache-flush commands) without writing anything.
Updates an existing entry. Fails if the file is absent. Only the fields you specify are changed; all other directives and comments are preserved. A backup is taken before the update.
Shows the target file, then deletes it. Prompts for confirmation on a TTY; use
--yes for automation. In a non-interactive session it refuses to delete
without --yes. Honors the global --dry-run flag and supports --no-flush.
Prints a table of entries (name, domain, nameservers, port, managed flag).
--json emits a stable array.
Prints a structured view; --raw prints the original file verbatim.
Runs diagnostics: platform, resolver directory, filename validity, regular-file
/ symlink checks, permissions, syntax, nameserver/port validity, .local
usage, overlapping suffixes, identical configs, DNS reachability (via a real DNS
query), scutil --dns load status, and /etc/hosts entries affecting the
domain. Any ERROR status yields a non-zero exit code.
Three layers: (1) dscacheutil -q host, (2) scutil --dns load status,
(3) a direct DNS query against the configured nameserver(s). Reports the
longest matched suffix, resolver file, nameservers, whether the rule is loaded,
system vs. direct addresses, consistency, and troubleshooting hints.
Runs dscacheutil -flushcache then killall -HUP mDNSResponder. A missing
mDNSResponder process is treated as a benign no-op. With the global
--dry-run flag it prints the planned commands without executing them.
| Code | Meaning |
|---|---|
0 |
Success |
2 |
Argument / usage error |
3 |
Permission error (needs sudo) |
4 |
Configuration error |
5 |
Runtime check failure (e.g. a check ERROR, or write OK but flush failed) |
- Domains are normalized (lowercased, trailing dot stripped) and rejected if
they contain
/,\,.., NUL, whitespace or control characters. - Target paths are cleaned and must stay strictly inside
/etc/resolver. - Files are verified to be regular files (via
Lstat) before any read, write or delete; symlinks are refused. - No
sh -cand no shell string concatenation; commands useos/execwith separated arguments. splitdnsnever auto-elevates privileges.
- macOS only. There is no Linux/Windows support.
splitdnsmanages/etc/resolverfiles only. It does not run a DNS server, edit/etc/hosts, change global network/DNS settings, or install dnsmasq/CoreDNS.- Entries created by other tools are readable and can be adopted (a
# Managed by splitdnsmarker is added on write), butsplitdnswill not reformat unrelated files.
make fmt-check # gofmt
make vet # go vet
make test # unit tests (hermetic; no real system commands)
make lint # golangci-lint
make build # build ./splitdns
# Read-only integration tests (macOS; observes real DNS state):
make integrationUnit tests are hermetic: all filesystem access uses temp directories and all
external commands go through an injectable fake runner. Integration tests use
the //go:build integration tag and never modify /etc/resolver.
Apache-2.0 © soulteary
