本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2026年08月09日 统计字数: 8979字 阅读时间: 18分钟阅读 本文链接: https://soulteary.com/2026/08/09/using-splitdns-to-manage-dev-domain-resolution-on-macos.html ----- # 用 splitdns 管理 macOS 的开发域名解析 今年一月,我写过一篇《[用 Split DNS 提升跨系统双机开发体验](https://soulteary.com/2026/01/03/split-dns-for-cross-platform-dual-machine-dev.html)》。当时的做法很简单:在 macOS 的 `/etc/resolver` 里放一个文件,把 `*.dev.test` 交给 Ubuntu 上的 DNS 服务,其他域名继续使用当前网络的默认 DNS。 这套配置目前使用起来还不错,只是设备和项目多起来以后,手工写文件开始有点麻烦:覆盖数据前要记得备份,修改后要刷新系统缓存,解析不对时还要在 `dig`、`scutil` 和 `dscacheutil` 之间来回切换。 于是我写了一个开源小工具:[soulteary/splitdns](https://github.com/soulteary/splitdns),用来创建、修改和检查 Mac 上的 resolver 配置。 ## 写在前面 我的日常环境是一台 MacBook 加一台 Ubuntu 移动工作站。MacBook 负责日常输入、浏览和轻量开发,Ubuntu 运行网站、CI 以及更适合 Linux 的服务。两台设备通过雷电网桥、网线或同一个局域网连接,Ubuntu 在这条链路上的固定地址是 `192.168.123.200`。 连上以后,我希望 `api.dev.test` 这类开发域名自动指向 Ubuntu,同时不修改 Mac 的全局 DNS,也不在两台机器上反复维护 `/etc/hosts`: ```text api.dev.test → 192.168.123.200:53 app.dev.test → 192.168.123.200:53 console.dev.test → 192.168.123.200:53 soulteary.com → 当前网络的默认 DNS ``` 为什么使用 `.test` 域名呢,因为在 [RFC 6761 第 6.2 节](https://www.rfc-editor.org/rfc/rfc6761.html#section-6.2)将 `.test` 这类域名本身就是保留给测试环境的。不过它不会通过起名的方式自动生效,需要像本文一样给私有 DNS 配置应答响应。 有的朋友可能喜欢使用 `.local` 来表达类似的语义,但是 `.local` 有被 [RFC 6762 定义 mDNS](https://www.rfc-editor.org/rfc/rfc6762.html#section-3)语义,会被 Bonjour 服务特殊处理,所以其实不适合作为单播 DNS 使用。 除此之外,很多时候我只带 Mac 出门,单机的时候,我也希望开发域名继续可用。比如,在我的本机上,通常 `*.lab.dev` 都会被自动指向到 `127.0.0.1`。 ```text console.lab.dev → 127.0.0.1 traefik.console.lab.dev → 127.0.0.1 ``` ## 安装 splitdns `splitdns` 不是 DNS 服务器,也不会常驻后台转发请求。它只是一个简单的小工具,负责管理 macOS 中 `/etc/resolver` 里的配置。至于选择使用哪一个 resolver ,如何使用 resolver 配置的仍然是 macOS 操作系统。 本文使用的程序版本是 [`v1.1.0`](https://github.com/soulteary/splitdns/releases/tag/v1.1.0),我们可以直接使用 Homebrew 进行安装: ```bash # 安装 splitdns brew tap soulteary/tap brew install soulteary/tap/splitdns # 查看版本 splitdns version ``` 第一次使用前,我们还需要准备 `/etc/resolver` 目录: ```bash sudo install -d -m 0755 /etc/resolver ``` ## 双机使用:macOS 和 Ubuntu 设备联动 先来聊聊双机使用场景,再说单机使用吧。 ### 先让 Ubuntu 响应开发域名 如果你已经参考[上一篇文章](https://soulteary.com/2026/01/03/split-dns-for-cross-platform-dual-machine-dev.html),配置好了 Ubuntu 环境,那么这一章节可以快速扫一遍。调试的时候,我通常先确认 Ubuntu 能否正常响应,再检查 macOS 有没有请求到正确的地方,省得两边来回改。 在 Ubuntu 中,需要安装 dnsmasq 和 `dig`: ```bash sudo apt update sudo apt install -y dnsmasq dnsutils ``` 然后,创建一份开发域名配置: ```bash sudo tee /etc/dnsmasq.d/dev-domains.conf >/dev/null <<'EOF' # dev.test 及其子域名都指向 Ubuntu 开发机 address=/dev.test/192.168.123.200 # 不把私有后缀的其他记录类型送往公共上游 local=/dev.test/ # 不从 /etc/resolv.conf 自动加载上游 no-resolv # 只监听本机和双机网络地址 listen-address=127.0.0.1 listen-address=192.168.123.200 bind-interfaces EOF ``` 通过 [dnsmasq 手册](https://thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html)我们可以了解到配置的含义。`local=/dev.test/` 会阻止这个后缀的信息查询继续送往上游,`no-resolv` 是不从 `/etc/resolv.conf` 自动读取上游信息。本文没有配置 `server=`,因此也不会从公网进行递归。 这个配置只将 `dev.test` 用于本地开发后缀,完全本地化。 完成配置文件的修改后,我们首先来检查配置,然后进行服务重启,让配置生效,以便稍后可以开始测试: ```bash sudo dnsmasq --test sudo systemctl restart dnsmasq sudo systemctl enable dnsmasq sudo systemctl status dnsmasq --no-pager ``` 因为 DNS 不是只有 UDP,在 [RFC 7766 第 5 节](https://www.rfc-editor.org/rfc/rfc7766.html#section-5)里,通用 DNS 实现需要同时支持 UDP 和 TCP,所以我们两种方式都需要试一次。 先从 Ubuntu 本机查询: ```bash dig @127.0.0.1 foo.dev.test A +short dig +tcp @127.0.0.1 foo.dev.test A +short # 预期返回 192.168.123.200 ``` 再到 Mac 上直接询问 Ubuntu: ```bash dig @192.168.123.200 foo.dev.test A +short dig +tcp @192.168.123.200 foo.dev.test A +short # 预期返回 192.168.123.200 ``` 这一步如果失败,先不要碰 Mac 的 resolver。检查 Ubuntu dnsmasq 状态、Ubuntu 的 `53/udp` 和 `53/tcp` 监听、双机链路地址以及防火墙: ```bash sudo ss -ltnp 'sport = :53' sudo ss -lunp 'sport = :53' ``` 启动失败时,可以先用 `ss` 看清楚是谁占用了端口,先不要直接停掉 [`systemd-resolved`](https://www.freedesktop.org/software/systemd/man/latest/systemd-resolved.service.html)。如果 `192.168.123.200` 还没有挂到接口上,`bind-interfaces` 也会让 dnsmasq 启动失败。 ### 让 Mac 把 dev.test 交给 Ubuntu 之前文章里,我们手工创建 `/etc/resolver/dev.test`,文件内容只有两行: ```text nameserver 192.168.123.200 port 53 ``` macOS 环境中提到的 “Split DNS”,其实简单来说,就是按域名后缀选择 resolver 配置。比如,在 `splitdns` 生成的文件里,我们可以分别创建 `domain` 都是 `dev.test` 和 `test` 的配置。按照 Apple 的 [`resolver(5)`](https://github.com/apple-oss-distributions/libresolv/blob/main/resolver.5) 的规则,当查找匹配域名的时候,同时存在多条匹配规则时,系统会优先使用匹配更具体的一条,因此 `api.dev.test` 会命中 `dev.test` 配置。 其实,如果你只有一条长期不变的规则,使用手工配置的方式完全够用。但如果你需要长期管理多条内容,以及针对配置信息进行快速排错,`splitdns` 就会方便很多。 比如,我们第一次使用 Splite DNS 功能,想要创建第一条规则,可以使用 `add` 命令。但是,如果你已经按照上一篇文章手工创建了 `/etc/resolver/dev.test`,其实就不需要重复执行 `add`了(`--force` 可以强制覆盖,程序会自动保存备份)。 如果当前已经有规则文件,可以使用 `show`、`check` 和 `test` 来进行操作,以及使用 `set` 对已有文件进行内容调整。 新建规则时,我习惯先跑一次 `--dry-run`,确认目标文件和内容是否正确: ```bash splitdns add dev.test \ --nameserver 192.168.123.200 \ --port 53 \ --dry-run ``` 执行完毕后,会提示: ```text Dry run — no changes written. Target: /etc/resolver/dev.test Planned content: # Managed by splitdns domain dev.test nameserver 192.168.123.200 port 53 Planned cache flush: - dscacheutil -flushcache - killall -HUP mDNSResponder ``` 确认无误后,再进行文件写入: ```bash sudo splitdns add dev.test \ --nameserver 192.168.123.200 \ --port 53 ``` 这里不能省略 `--nameserver`,因为它的默认值是 `127.0.0.1`,不适合当前的双机配置。 如果已经存在配置,可以使用 `set` 修改已有配置。当我们修改规则后,`splitdns` 会自动刷新 macOS 的 DNS 缓存。 因为文件的写入和系统信息刷新是两件事,即便遇到刷新报错,前面的文件的修改通常也已经修改完毕,不会因为报错而自动撤回。所以如果遇到刷新报错,需要手动先处理文件问题,当处理好问题后,再运行一次 `sudo splitdns flush` 即可。 ### 再次确认 macOS 请求正确 前面的 `dig @192.168.123.200` 只证明 Ubuntu DNS 和网络链路正常,还不能说明 `/etc/resolver/dev.test` 已经生效。 普通的 `dig api.dev.test` 命令其实不适合拿来下这个结论。[BIND 的 `dig` 手册](https://bind9.readthedocs.io/en/stable/manpages.html#dig-dns-lookup-utility) 写得很直接:当我们没有指定 `@server` 时,它会查询 `/etc/resolv.conf` 里的服务器,并不会完整经过 macOS 的多 resolver 选择。 想要检查系统解析路径,我通常使用 Apple 文档里的 [`scutil --dns`](https://github.com/apple-oss-distributions/configd/blob/main/scutil.tproj/scutil.8),再配合 `dscacheutil` 和 `ping`: ```bash scutil --dns dscacheutil -q host -a name api.dev.test ping -c 1 api.dev.test ``` 现在有了 `splitdns` 以后,我通常直接运行: ```bash splitdns check dev.test splitdns test api.dev.test ``` 使用 `check` 看配置,`test` 对照系统解析和 DNS 的直接回答。之前需要来回复制几条命令,现在一屏基本就能看出问题在哪边。 我的判断顺序通常是: - 直接查询失败,回 Ubuntu 检查 dnsmasq、网络和防火墙; - 直接查询正常,但 `scutil` 没有规则,检查 `/etc/resolver/dev.test`; - 系统解析正常,只有浏览器不对,检查浏览器自己的安全 DNS 或 [DoH](https://www.rfc-editor.org/rfc/rfc8484.html)。 ## 单机使用:只有一台 Mac 时 很多时候,我只带 Mac 出门,也希望本机相关的开发域名,不论在什么网络环境下,都可以继续使用。最简单的办法是在 Mac 本机,也运行 dnsmasq。 下面我们使用 `lab.dev` 为例,进行说明。和前面的 `dev.test` 是两套独立配置。 首先,在 macOS 上安装 dnsmasq: ```bash brew install dnsmasq ``` 接下来,我们使用 `127.0.0.1:53` 这个服务地址提供本地开发使用的 DNS 服务。注意,不要顺手把端口换成 5353:[Homebrew 的说明](https://formulae.brew.sh/formula/dnsmasq)明确提到,localhost 配合非 53 端口可能无法正常使用 scoped resolver。如果当前系统仍不稳定,可以再参考其中的 `lo0` 非 localhost 地址方案;切换时,需要把下文的 `listen-address` 和 `--nameserver` 一起改成新的地址,并继续使用 53 端口。 完成安装后,我们需要更新 `dnsmasq.conf` 配置,通常配置文件会在这个位置: ```bash "$(brew --prefix)/etc/dnsmasq.conf" ``` 我们在配置中增加下面的内容: ```text # lab.dev 及其子域名都指向本机 address=/lab.dev/127.0.0.1 local=/lab.dev/ # 不配置公共上游 no-resolv # 只服务本机 listen-address=127.0.0.1 bind-interfaces ``` 上面的配置生效后,我们不论是访问 `traefik.console.lab.dev` 还是 `console.lab.dev`,都是可以正确指向 `127.0.0.1` 的。 我们修改完配置文件后,通常需要先检查配置有效性,测试没有问题后。我们将 dnsmasq 作为系统服务启动: ```bash sudo "$(brew --prefix)/sbin/dnsmasq" --test sudo brew services start dnsmasq ``` 如果后续我们还想修改配置,在文件更新完毕后,再执行 `sudo brew services restart dnsmasq` 命令,重新加载配置文件即可。 在确认 dnsmasq 正常运行后,可以通过 `splitdns add` 添加 resolver 规则: ```bash sudo splitdns add lab.dev \ --nameserver 127.0.0.1 \ --port 53 ``` 执行完毕,会提示配置添加成功,并且系统缓存也已经刷新。 ```bash # sudo splitdns add lab.dev --nameserver 127.0.0.1 --port 53 Wrote /etc/resolver/lab.dev [ok] dscacheutil -flushcache [ok] killall -HUP mDNSResponder ``` 使用 `list` 命令,可以看到目前所有的配置信息: ```bash # splitdns list NAME DOMAIN NAMESERVERS PORT MANAGED NOTE -------- -------- --------------- ---- ------- ---- dev.test dev.test 192.168.123.200 53 yes lab.dev lab.dev 127.0.0.1 53 yes ... ``` 如果你想检查配置是否有效,通过 `splitdns check` 具体域名,可以进行完整的检查。 ```bash # splitdns check lab.dev [OK] platform: running on macOS [OK] resolver-dir: /etc/resolver exists and is readable [OK] file:lab.dev:type: regular file [OK] file:lab.dev:perms: permissions 644 [OK] file:lab.dev:scutil: resolver for "lab.dev" is loaded by scutil [OK] file:lab.dev:reachable: 127.0.0.1:53 answered a DNS query [OK] hosts: no /etc/hosts entries affect "lab.dev" ``` 如果你不指定任何域名,会检查你配置中的所有配置,当我们拔掉网线或雷电线、脱离某个局域网,单机使用的时候,上面的多机配置就不生效了,那么 `check` 命令将会把存在的问题都明确的暴露出来,方便你进行调试排查: ```bash # splitdns check [OK] platform: running on macOS [OK] resolver-dir: /etc/resolver exists and is readable [OK] file:dev.test:type: regular file [OK] file:dev.test:perms: permissions 644 [OK] file:dev.test:scutil: resolver for "dev.test" is loaded by scutil [WARN] file:dev.test:reachable: no DNS response from 192.168.123.200:53 (DNS server returned rcode 3) hint: ensure the DNS server is running and listening [OK] file:lab.dev:type: regular file [OK] file:lab.dev:perms: permissions 644 [OK] file:lab.dev:scutil: resolver for "lab.dev" is loaded by scutil [OK] file:lab.dev:reachable: 127.0.0.1:53 answered a DNS query ... ``` 如果我们想测试某个具体域名的情况,可以通过 `splitdns test` 命令来完成: ```bash # splitdns test console.lab.dev Hostname: console.lab.dev Matched suffix: lab.dev Resolver file: /etc/resolver/lab.dev Nameservers: 127.0.0.1 Port: 53 scutil loaded: yes dscacheutil addresses: 127.0.0.1 direct DNS addresses: 127.0.0.1 consistent: yes ``` 相比较之前,是不是简单了很多呢? ## 其他:日常修改和清理 如果你想要查看当前所有的规则配置信息(不需要`sudo`),除了刚刚提到的使用 `splitdns list` 查看列表之外,`splitdns show` 将展示某个域名的具体信息: ```bash # splitdns show lab.dev File: /etc/resolver/lab.dev Domain: lab.dev Nameservers: [127.0.0.1] Port: 53 Managed: true ``` 也可以通过增加 `--raw` 参数,查看文件的具体内容 ```bash splitdns show dev.test --raw # Managed by splitdns domain dev.test nameserver 192.168.123.200 port 53 ``` 假如我们只希望调整某个配置的某项内容,我们可以使用 `set` 和某个具体字段来完成配置修改。例如,只有 DNS 地址变化了 `192.168.123.201`: ```bash sudo splitdns set dev.test --nameserver 192.168.123.201 ``` `set` 命令会只修改明确传入的字段,原有其他指令和内容都会保留,对文件进行覆盖操作前也会备份旧文件。 如果某条配置,我们不再需要,也可以执行 `remove`: ```bash sudo splitdns remove dev.test ``` 配置删除前,会展示内容并要求确认。如果你希望静默处理,命令里需要显式增加 `--yes`;删除文件本身不会创建备份,重要配置建议先用上面提到的 `show --raw` 留一份。 ```bash splitdns show dev.test --raw > some-domain.dev ``` ## 最后 本文先写到这里,我们下篇文章再见。 --EOF