今年一月,我写过一篇《用 Split DNS 提升跨系统双机开发体验》。当时的做法很简单:在 macOS 的 /etc/resolver 里放一个文件,把 *.dev.test 交给 Ubuntu 上的 DNS 服务,其他域名继续使用当前网络的默认 DNS。

这套配置目前使用起来还不错,只是设备和项目多起来以后,手工写文件开始有点麻烦:覆盖数据前要记得备份,修改后要刷新系统缓存,解析不对时还要在 digscutildscacheutil 之间来回切换。

于是我写了一个开源小工具: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/udp53/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.testtest 的配置。按照 Apple 的 resolver(5) 的规则,当查找匹配域名的时候,同时存在多条匹配规则时,系统会优先使用匹配更具体的一条,因此 api.dev.test 会命中 dev.test 配置。

其实,如果你只有一条长期不变的规则,使用手工配置的方式完全够用。但如果你需要长期管理多条内容,以及针对配置信息进行快速排错,splitdns 就会方便很多。

比如,我们第一次使用 Splite DNS 功能,想要创建第一条规则,可以使用 add 命令。但是,如果你已经按照上一篇文章手工创建了 /etc/resolver/dev.test,其实就不需要重复执行 add了(--force 可以强制覆盖,程序会自动保存备份)。

如果当前已经有规则文件,可以使用 showchecktest 来进行操作,以及使用 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,再配合 dscacheutilping

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