跳过正文
  1. 博客/

macOS 下让特定域名的解析使用指定的 DNS Nameserver

·1325 字·7 分钟·
MacOS DNS
目录

要实现标题所描述的目的,在 macOS 上有两种手段。

本文所述命令操作均使用 macOS Ventura 13.0 完成。

BSD style,resolver file
#

从很古老的 OS X 时期起,mac 上就支持通过配置 resolver 文件的方式来设置 DNS。文件的具体格式可以通过 man 5 resolver 了解。

resolver man page

具体用法可以简要概述为,只需要在 /etc/resolver 文件夹下(需要自行创建这个文件夹,默认不存在)下创建一个含有 resolver 格式内容的文件,比如 /etc/resolver/example.com,就能让 example.com 的域名解析都使用这个文件内指定的 DNS 服务器地址。不再需要的话只要删除掉对应文件即可。

以我家中网络使用的 lan 这个 TLD 为例,文件最简单的内容只需要写成这样:

domain lan
search lan
nameserver 192.168.15.1

创建或修改了这个文件后执行 scutil --dns 查看是否出现了对应的 resolver:

$ scutil --dns
DNS configuration

resolver #1
  nameserver[0] : fe80::800c:67ff:fe1d:fd64%12d
  nameserver[1] : 172.20.10.1
  if_index : 12 (en1)
  flags    : Request A records, Request AAAA records
  reach    : 0x00020002 (Reachable,Directly Reachable Address)

resolver #2
  domain   : local
  options  : mdns
  timeout  : 5
  flags    : Request A records, Request AAAA records
  reach    : 0x00000000 (Not Reachable)
  order    : 300000

resolver #3
  domain   : 254.169.in-addr.arpa
  options  : mdns
  timeout  : 5
  flags    : Request A records, Request AAAA records
  reach    : 0x00000000 (Not Reachable)
  order    : 300200

resolver #4
  domain   : 8.e.f.ip6.arpa
  options  : mdns
  timeout  : 5
  flags    : Request A records, Request AAAA records
  reach    : 0x00000000 (Not Reachable)
  order    : 300400

resolver #5
  domain   : 9.e.f.ip6.arpa
  options  : mdns
  timeout  : 5
  flags    : Request A records, Request AAAA records
  reach    : 0x00000000 (Not Reachable)
  order    : 300600

resolver #6
  domain   : a.e.f.ip6.arpa
  options  : mdns
  timeout  : 5
  flags    : Request A records, Request AAAA records
  reach    : 0x00000000 (Not Reachable)
  order    : 300800

resolver #7
  domain   : b.e.f.ip6.arpa
  options  : mdns
  timeout  : 5
  flags    : Request A records, Request AAAA records
  reach    : 0x00000000 (Not Reachable)
  order    : 301000

resolver #8
  domain   : lan
  search domain[0] : lan
  nameserver[0] : 192.168.15.1
  flags    : Request A records, Request AAAA records
  reach    : 0x00000002 (Reachable)

DNS configuration (for scoped queries)

resolver #1
  nameserver[0] : fe80::800c:67ff:fe1d:fd64%12d
  nameserver[1] : 172.20.10.1
  if_index : 12 (en1)
  flags    : Scoped, Request A records, Request AAAA records
  reach    : 0x00020002 (Reachable,Directly Reachable Address)

可以看见 #8,表明配置已经生效。

这个 scutil 命令将在下一章描述。

如果是 macOS 10.10.4+ 的系统,在创建或修改了这个文件后,建议给 mDNSResponder 发送 HUP 信号来清除系统的 DNS 缓存。

killall -HUP mDNSResponder

测试配置是否生效可以使用 dns-sd 命令:

$ dns-sd -q netbox
DATE: ---Wed 09 Nov 2022---
23:21:40.760  ...STARTING...
Timestamp     A/R  Flags         IF  Name                          Type   Class  Rdata
23:21:40.836  Add  3              0  netbox.lan.                   CNAME  IN     unicorn.lan.
23:21:40.836  Add  2              0  unicorn.lan.                  Addr   IN     192.168.15.2
^C

也可以在 -G 后指定 v4 / v6 来查询 A / AAAA

$ dns-sd -G v4v6 netbox
DATE: ---Wed 09 Nov 2022---
23:23:41.669  ...STARTING...
Timestamp     A/R  Flags         IF  Hostname                               Address                                      TTL
23:23:41.686  Add  2              0  unicorn.lan.                           192.168.15.2                                 15
23:23:41.700  Add  2              0  netbox.                                0000:0000:0000:0000:0000:0000:0000:0000%<0>  77   No Such Record
^C

System Configuration Framework,scutil
#

在 macOS 上,还有一套称为 System Configuration Framework 的高级系统框架,最早可能可以追述到 Mac OS X Leopard 时代 1。这套框架用于动态管理系统中的各个配置项,当然也包括配置系统 DNS 的能力。根据官方文档 System Configuration 页面上的描述,它基本支持了 Apple 所有的系统,如 iOS / iPadOS / macOS 等等。像 iOS 上的 APP 可以通过调用这套框架对应的系统 API SCDynamicStore 来设置 DNS,而对 macOS 上的管理员 / 用户来说,更实用的手段是通过上一章提到过的 scutil 命令。

scSystem Configuration 首字母的缩写。

通过查看 scutil 的 man page,可以在 --dns 这个选项下的描述中发现,系统上的 DNS 解析库依然保持着对旧有的 BSD 风格 resolver 文件的兼容和适配:

scutil man page

结合上一章节中的实际表现也确实如此。只是按照 Apple 的风格,突然未来的某一天系统更新就不支持这个特性了也说不准,所以提前适应新工具也是有一定必要的。下面就展示下如何使用 scutil 进行 DNS 的配置。

scutil 执行指令的方法是交互式的,执行它会进入一个类似 shell 的场景。但是你也可以像调用其他普通命令那样来调用它,比如使用管道方法传递 stdin:

$ echo help | scutil

Available commands:

 help                          : list available commands
 f.read file                   : process commands from file
 quit                          : quit

 d.init                        : initialize (empty) dictionary
 d.show                        : show dictionary contents
 d.add key [*#?%] val [v2 ...]  : add information to dictionary
       (*=array,#=number,?=boolean,%=hex data)
 d.remove key                  : remove key from dictionary

 list [pattern]                : list keys in data store
 add key ["temporary"]         : add key in data store w/current dict
 get key                       : get dict from data store w/key
 set key                       : set key in data store w/current dict
 show key ["pattern"]          : show values in data store w/key
 remove key                    : remove key from data store
 notify key                    : notify key in data store

 n.list ["pattern"]            : list notification keys
 n.add key ["pattern"]         : add notification key
 n.remove key ["pattern"]      : remove notification key
 n.changes                     : list changed keys
 n.watch                       : watch for changes
 n.cancel                      : cancel notification requests

或者使用 heredoc:

$ scutil << EOF
heredoc> help
heredoc> EOF

Available commands:

 help                          : list available commands
 f.read file                   : process commands from file
 quit                          : quit

 d.init                        : initialize (empty) dictionary
 d.show                        : show dictionary contents
 d.add key [*#?%] val [v2 ...]  : add information to dictionary
       (*=array,#=number,?=boolean,%=hex data)
 d.remove key                  : remove key from dictionary

 list [pattern]                : list keys in data store
 add key ["temporary"]         : add key in data store w/current dict
 get key                       : get dict from data store w/key
 set key                       : set key in data store w/current dict
 show key ["pattern"]          : show values in data store w/key
 remove key                    : remove key from data store
 notify key                    : notify key in data store

 n.list ["pattern"]            : list notification keys
 n.add key ["pattern"]         : add notification key
 n.remove key ["pattern"]      : remove notification key
 n.changes                     : list changed keys
 n.watch                       : watch for changes
 n.cancel                      : cancel notification requests

如果编写脚本,建议使用 help 中提到的 f.read 直接从文件中读取指令,或者使用 heredoc 的方法。这两种方法在连续执行多个指令的情况下比管道传递 stdin 能够提供更好的阅读性。

管道传递 stdin 方法中多个指令之间需要包含 \n

具体如何配置 DNS,scutil 的文档并未做出过多解释,毕竟它也只是调用的 SystemConfiguration.framework SCDynamicStore APIs 去设置 K / V,但让人无语的是,框架的文档页只列出了配置 DNS 的 Key:DNS Entity Keys,具体这些 Key 代表什么,每个 Key 值的格式等也并未做出详细说明。

好在有 Stack Exchange 上的 mecki 的回答 2 3,给出了使用 scutil 配置的完整例子:

#!/bin/bash
sudo scutil << EOF
d.init
d.add ServerAddresses * 9.9.9.9
d.add SearchDomains * stackexchange.com
d.add SupplementalMatchDomains * stackexchange.com
set State:/Network/Service/whatever-you-want-as-long-as-unique/DNS
exit
EOF

注意 SearchDomains 这行,这个 key 在 mecki 的回答中没有提及,是我自己对照 DNS Entity Keys 后测试的,实际结果显示它没什么用。

对于 mecki 怎么知道使用什么 Key 这回事,从他能给出 Apple 这些组件源码在 GitHub 的地址 这情况看,十有八九是他看过代码。

语法中的 * 是一个特殊标识,用来表示后面值是 Array 类型。

d.add SearchDomains stackexchange.com 创建出的对象:

> get State:/Network/Service/whatever-you-want-as-long-as-unique/DNS
> d.show
<dictionary> {
 SearchDomains : stackexchange.com
 ServerAddresses : <array> {
   0 : 9.9.9.9
 }
 SupplementalMatchDomains : <array> {
   0 : stackexchange.com
 }
}

d.add SearchDomains * stackexchange.com 创建出的对象:

> get State:/Network/Service/whatever-you-want-as-long-as-unique/DNS
> d.show
<dictionary> {
  SearchDomains : <array> {
    0 : stackexchange.com
  }
  ServerAddresses : <array> {
    0 : 9.9.9.9
  }
  SupplementalMatchDomains : <array> {
    0 : stackexchange.com
  }
}

配置完后还是老样子,通过 scutil --dns 查看是否出现了对应的 resolver:

$ scutil --dns
DNS configuration

resolver #1
  search domain[0] : stackexchange.com
  search domain[1] : lan
  nameserver[0] : 192.168.15.1
  nameserver[1] : fd6f:9316:1db::1
  if_index : 12 (en1)
  flags    : Request A records, Request AAAA records
  reach    : 0x00020002 (Reachable,Directly Reachable Address)

resolver #2
  domain   : stackexchange.com
  nameserver[0] : 9.9.9.9
  flags    : Supplemental, Request A records, Request AAAA records
  reach    : 0x00000002 (Reachable)
  order    : 101800

...

可以看出明显的与 resolver file 配置时不一样的输出。

使用 dns-sd 测试:

$ dns-sd -q apple
DATE: ---Thu 10 Nov 2022---
00:13:03.209  ...STARTING...
Timestamp     A/R  Flags         IF  Name                          Type   Class  Rdata
00:13:03.211  Add  40000003       0  apple.stackexchange.com.      Addr   IN     172.64.144.30
00:13:03.211  Add  40000002       0  apple.stackexchange.com.      Addr   IN     104.18.43.226
^C

如果想要移除这个配置只需删除这个 Key 即可:

sudo scutil
remove State:/Network/Service/whatever-you-want-as-long-as-unique/DNS

要在 scutil 中查看还有其他哪些 DNS 的 Keys 可以使用 list ".*DNS"

$ echo 'list ".*DNS"' | scutil
  subKey [0] = State:/Network/Global/DNS
  subKey [1] = State:/Network/MulticastDNS
  subKey [2] = State:/Network/PrivateDNS
  subKey [3] = State:/Network/Service/C6BA5C2B-B3FB-4A34-99EA-EE62154A2BD6/DNS
  subKey [4] = State:/Network/Service/whatever-you-want-as-long-as-unique/DNS

其他使用 scutil 的例子可以参考:

Tinc integration
#

如果有读者像我一样使用 tinc,可以参考一下我简单的 up / down 脚本。

resolver file
#

/opt/homebrew/etc/tinc/z10n0110/tinc-up:

#!/bin/sh
ifconfig $INTERFACE 10.0.7.3 10.0.7.9 netmask 255.255.0.0
route add -net 10.0.7.0/24 -interface $INTERFACE
route add -net 192.168.15.0/24 -interface $INTERFACE

mkdir -p /etc/resolver
cat > /etc/resolver/lan << HERE
domain lan
search lan
nameserver 192.168.15.1
HERE

cat > /etc/resolver/z10n0110.men << HERE
domain z10n0110.men
search z10n0110.men
nameserver 192.168.15.1
HERE

killall -HUP mDNSResponder

/opt/homebrew/etc/tinc/z10n0110/tinc-down:

#!/bin/sh
route delete -net 192.168.15.0/24 -interface $INTERFACE
route delete -net 10.0.7.0/24 -interface $INTERFACE
ifconfig $INTERFACE down

if [ -f /etc/resolver/lan ]; then
    rm -rf /etc/resolver/lan
fi

if [ -f /etc/resolver/z10n0110.men ]; then
    rm -rf /etc/resolver/z10n0110.men
fi

killall -HUP mDNSResponder

scutil
#

/opt/homebrew/etc/tinc/z10n0110/tinc-up:

#!/bin/sh
ifconfig $INTERFACE 10.0.7.3 10.0.7.9 netmask 255.255.0.0
route add -net 10.0.7.0/24 -interface $INTERFACE
route add -net 192.168.15.0/24 -interface $INTERFACE

cat > /opt/homebrew/etc/tinc/z10n0110/up-cmds.scutil << HERE
d.init
d.add ServerAddresses * 192.168.15.1
d.add SupplementalMatchDomains * lan z10n0110.men
set State:/Network/Service/tinc-z10n0110/DNS
exit
HERE

echo 'f.read /opt/homebrew/etc/tinc/z10n0110/up-cmds.scutil' | scutil

killall -HUP mDNSResponder

/opt/homebrew/etc/tinc/z10n0110/tinc-down:

#!/bin/sh
route delete -net 192.168.15.0/24 -interface $INTERFACE
route delete -net 10.0.7.0/24 -interface $INTERFACE
ifconfig $INTERFACE down

echo 'remove State:/Network/Service/tinc-z10n0110/DNS' | scutil

killall -HUP mDNSResponder