今年一月,我写过一篇《用 Split DNS 提升跨系统双机开发体验》。当时的做法很简单:在 macOS 的 /etc/resolver 里放一个文件,把 *.dev.test 交给 Ubuntu 上的 DNS 服务,其他域名继续使用当前网络的默认 DNS。
这套配置目前使用起来还不错,只是设备和项目多起来以后,手工写文件开始有点麻烦:覆盖数据前要记得备份,修改后要刷新系统缓存,解析不对时还要在 dig、scutil 和 dscacheutil 之间来回切换。
于是我写了一个开源小工具:soulteary/splitdns,用来创建、修改和检查 Mac 上的 resolver 配置。
写在前面
我的日常环境是一台 MacBook 加一台 Ubuntu 移动工作站。MacBook 负责日常输入、浏览和轻量开发,Ubuntu 运行网站、CI 以及更适合 Linux 的服务。两台设备通过雷电网桥、网线或同一个局域网连接,Ubuntu 在这条链路上的固定地址是 192.168.123.200。
连上以后,我希望 api.dev.test 这类开发域名自动指向 Ubuntu,同时不修改 Mac 的全局 DNS,也不在两台机器上反复维护 /etc/hosts:
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 节将 .test 这类域名本身就是保留给测试环境的。不过它不会通过起名的方式自动生效,需要像本文一样给私有 DNS 配置应答响应。
有的朋友可能喜欢使用 .local 来表达类似的语义,但是 .local 有被 RFC 6762 定义 mDNS语义,会被 Bonjour 服务特殊处理,所以其实不适合作为单播 DNS 使用。
除此之外,很多时候我只带 Mac 出门,单机的时候,我也希望开发域名继续可用。比如,在我的本机上,通常 *.lab.dev 都会被自动指向到 127.0.0.1。
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,我们可以直接使用 Homebrew 进行安装:
# 安装 splitdns
brew tap soulteary/tap
brew install soulteary/tap/splitdns
# 查看版本
splitdns version
第一次使用前,我们还需要准备 /etc/resolver 目录:
sudo install -d -m 0755 /etc/resolver
双机使用:macOS 和 Ubuntu 设备联动
先来聊聊双机使用场景,再说单机使用吧。
先让 Ubuntu 响应开发域名
如果你已经参考上一篇文章,配置好了 Ubuntu 环境,那么这一章节可以快速扫一遍。调试的时候,我通常先确认 Ubuntu 能否正常响应,再检查 macOS 有没有请求到正确的地方,省得两边来回改。
在 Ubuntu 中,需要安装 dnsmasq 和 dig:
sudo apt update
sudo apt install -y dnsmasq dnsutils
然后,创建一份开发域名配置:
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 手册我们可以了解到配置的含义。local=/dev.test/ 会阻止这个后缀的信息查询继续送往上游,no-resolv 是不从 /etc/resolv.conf 自动读取上游信息。本文没有配置 server=,因此也不会从公网进行递归。
这个配置只将 dev.test 用于本地开发后缀,完全本地化。
完成配置文件的修改后,我们首先来检查配置,然后进行服务重启,让配置生效,以便稍后可以开始测试:
sudo dnsmasq --test
sudo systemctl restart dnsmasq
sudo systemctl enable dnsmasq
sudo systemctl status dnsmasq --no-pager
因为 DNS 不是只有 UDP,在 RFC 7766 第 5 节里,通用 DNS 实现需要同时支持 UDP 和 TCP,所以我们两种方式都需要试一次。
先从 Ubuntu 本机查询:
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:
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 监听、双机链路地址以及防火墙:
sudo ss -ltnp 'sport = :53'
sudo ss -lunp 'sport = :53'
启动失败时,可以先用 ss 看清楚是谁占用了端口,先不要直接停掉 systemd-resolved。如果 192.168.123.200 还没有挂到接口上,bind-interfaces 也会让 dnsmasq 启动失败。
让 Mac 把 dev.test 交给 Ubuntu
之前文章里,我们手工创建 /etc/resolver/dev.test,文件内容只有两行:
nameserver 192.168.123.200
port 53
macOS 环境中提到的 “Split DNS”,其实简单来说,就是按域名后缀选择 resolver 配置。比如,在 splitdns 生成的文件里,我们可以分别创建 domain 都是 dev.test 和 test 的配置。按照 Apple 的 resolver(5) 的规则,当查找匹配域名的时候,同时存在多条匹配规则时,系统会优先使用匹配更具体的一条,因此 api.dev.test 会命中 dev.test 配置。
其实,如果你只有一条长期不变的规则,使用手工配置的方式完全够用。但如果你需要长期管理多条内容,以及针对配置信息进行快速排错,splitdns 就会方便很多。
比如,我们第一次使用 Splite DNS 功能,想要创建第一条规则,可以使用 add 命令。但是,如果你已经按照上一篇文章手工创建了 /etc/resolver/dev.test,其实就不需要重复执行 add了(--force 可以强制覆盖,程序会自动保存备份)。
如果当前已经有规则文件,可以使用 show、check 和 test 来进行操作,以及使用 set 对已有文件进行内容调整。
新建规则时,我习惯先跑一次 --dry-run,确认目标文件和内容是否正确:
splitdns add dev.test \
--nameserver 192.168.123.200 \
--port 53 \
--dry-run
执行完毕后,会提示:
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
确认无误后,再进行文件写入:
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 手册 写得很直接:当我们没有指定 @server 时,它会查询 /etc/resolv.conf 里的服务器,并不会完整经过 macOS 的多 resolver 选择。
想要检查系统解析路径,我通常使用 Apple 文档里的 scutil --dns,再配合 dscacheutil 和 ping:
scutil --dns
dscacheutil -q host -a name api.dev.test
ping -c 1 api.dev.test
现在有了 splitdns 以后,我通常直接运行:
splitdns check dev.test
splitdns test api.dev.test
使用 check 看配置,test 对照系统解析和 DNS 的直接回答。之前需要来回复制几条命令,现在一屏基本就能看出问题在哪边。
我的判断顺序通常是:
- 直接查询失败,回 Ubuntu 检查 dnsmasq、网络和防火墙;
- 直接查询正常,但
scutil没有规则,检查/etc/resolver/dev.test; - 系统解析正常,只有浏览器不对,检查浏览器自己的安全 DNS 或 DoH。
单机使用:只有一台 Mac 时
很多时候,我只带 Mac 出门,也希望本机相关的开发域名,不论在什么网络环境下,都可以继续使用。最简单的办法是在 Mac 本机,也运行 dnsmasq。
下面我们使用 lab.dev 为例,进行说明。和前面的 dev.test 是两套独立配置。
首先,在 macOS 上安装 dnsmasq:
brew install dnsmasq
接下来,我们使用 127.0.0.1:53 这个服务地址提供本地开发使用的 DNS 服务。注意,不要顺手把端口换成 5353:Homebrew 的说明明确提到,localhost 配合非 53 端口可能无法正常使用 scoped resolver。如果当前系统仍不稳定,可以再参考其中的 lo0 非 localhost 地址方案;切换时,需要把下文的 listen-address 和 --nameserver 一起改成新的地址,并继续使用 53 端口。
完成安装后,我们需要更新 dnsmasq.conf 配置,通常配置文件会在这个位置:
"$(brew --prefix)/etc/dnsmasq.conf"
我们在配置中增加下面的内容:
# 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 作为系统服务启动:
sudo "$(brew --prefix)/sbin/dnsmasq" --test
sudo brew services start dnsmasq
如果后续我们还想修改配置,在文件更新完毕后,再执行 sudo brew services restart dnsmasq 命令,重新加载配置文件即可。
在确认 dnsmasq 正常运行后,可以通过 splitdns add 添加 resolver 规则:
sudo splitdns add lab.dev \
--nameserver 127.0.0.1 \
--port 53
执行完毕,会提示配置添加成功,并且系统缓存也已经刷新。
# 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 命令,可以看到目前所有的配置信息:
# 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 具体域名,可以进行完整的检查。
# 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 命令将会把存在的问题都明确的暴露出来,方便你进行调试排查:
# 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 命令来完成:
# 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 将展示某个域名的具体信息:
# splitdns show lab.dev
File: /etc/resolver/lab.dev
Domain: lab.dev
Nameservers: [127.0.0.1]
Port: 53
Managed: true
也可以通过增加 --raw 参数,查看文件的具体内容
splitdns show dev.test --raw
# Managed by splitdns
domain dev.test
nameserver 192.168.123.200
port 53
假如我们只希望调整某个配置的某项内容,我们可以使用 set 和某个具体字段来完成配置修改。例如,只有 DNS 地址变化了 192.168.123.201:
sudo splitdns set dev.test --nameserver 192.168.123.201
set 命令会只修改明确传入的字段,原有其他指令和内容都会保留,对文件进行覆盖操作前也会备份旧文件。
如果某条配置,我们不再需要,也可以执行 remove:
sudo splitdns remove dev.test
配置删除前,会展示内容并要求确认。如果你希望静默处理,命令里需要显式增加 --yes;删除文件本身不会创建备份,重要配置建议先用上面提到的 show --raw 留一份。
splitdns show dev.test --raw > some-domain.dev
最后
本文先写到这里,我们下篇文章再见。
–EOF