跳转到内容

DNS

etemenanki-app 为每份运行中的配置创建一个解析器。出站需要把域名转换成地址时,例如 freedom 出站拨号 example.com,或者 VLESS 客户端连接 proxy.example.com,就会向这个解析器查询。唯一的例外是 WireGuard 的 endpoint,见不经过解析器的查询。[dns] 表决定解析器从哪里获得答案:主机自身的解析器(默认)、基于普通 UDP 的 DNS 服务器、DNS over TLS,或 DNS over HTTPS。无论使用哪种后端,前面都有一层缓存。

如果你希望查询发往指定的服务器、经过加密或到达私有解析服务器,或者需要弄清某个域名为什么解析成了现在的结果,请阅读本页。

后端 传输方式 是否读取 /etc/hosts、nsswitch.conf 和搜索域 答案缓存多久 需要的键
system(默认) 主机解析器(getaddrinfo) 是 60 秒 无
udp 普通 DNS,UDP 否 记录的 TTL,限制在 5 秒到 3600 秒之间 server
tls DNS over TLS(RFC 7858) 否 记录的 TTL,限制在 5 秒到 3600 秒之间 server、server_name
https DNS over HTTPS(RFC 8484) 否 记录的 TTL,限制在 5 秒到 3600 秒之间 server、url

system 之所以是默认值,是因为只有它遵循主机自身的域名解析配置。换成 DNS 客户端后,某些域名的解析结果会变化:/etc/hosts 中的条目和搜索域都不再生效。如果查询内容本身不能在网络上被读到,请选择 tls 或 https;如果只是想使用某台特定服务器而不在意隐私,选择 udp。

下面的配置运行一个本地 SOCKS 代理,直接拨号目标,并通过 DNS over HTTPS 解析所有域名:

config.toml
# A local SOCKS proxy that sends traffic straight out and looks up every
# name over DNS over HTTPS instead of the host resolver.
# DNS over HTTPS (RFC 8484). `server` is the address that is dialed and must be
# an IP literal with a port; `url` supplies the TLS name, the Host header and
# the request path. The certificate is checked against the system CA store.
[dns]
backend = "https"
server = "1.1.1.1:443"
url = "https://cloudflare-dns.com/dns-query"
# Accept SOCKS 4/4a/5 clients on the loopback interface only.
[[inbound]]
tag = "socks-in"
protocol = "socks"
listen = "127.0.0.1"
port = 1080
# Dial the requested destination directly. When the client asks for a name,
# freedom resolves it through [dns] above.
[[outbound]]
tag = "direct"
protocol = "freedom"

server 是 etemenanki-app 实际连接的地址。url 提供用于校验证书的名称、Host 头以及请求路径。两者有意分开:如果从 URL 中取地址,就需要先解析解析服务器本身,而此时没有任何东西可以用来解析它。

[dns] 表是可选的。没有它时,解析器使用 system 后端。该表只接受下面列出的键;出现其他键就是解析错误,错误信息会指出所在行,例如 unknown field `backends`, expected one of `backend`, `server`, `server_name`, `url`, `ca_file` 。

键类型必填默认值说明
backendstring (enum)否"system"解析结果的来源:system(主机解析器,即 getaddrinfo)、udp(基于 UDP 的普通 DNS)、tls(DNS over TLS,RFC 7858)或 https(DNS over HTTPS,RFC 8484)。只接受小写;其他值会报错 dns: unknown backend "…" (expected "system", "udp", "tls" or "https")。
serverstring视情况—udp、tls、https 必填。解析服务器的 socket 地址,写成带端口的 IP 字面量,例如 "192.0.2.53:53" 或 "[2001:db8::53]:853"。写主机名或漏写端口会被拒绝,报 dns: invalid server address: invalid socket address syntax,因为此时还没有可以用来解析它的解析器。对 https 而言,实际拨号的就是这个地址,与 url 里的主机和端口无关。system 忽略此项。
server_namestring视情况—tls 必填。作为 SNI 发送、并用来校验解析服务器证书的名称。它不会从 server 推断;缺少时报 dns: the tls backend needs a server name to verify against。其他后端都忽略此项,包括 https,后者从 url 中取名称。
urlstring视情况—https 必填,必须以 https:// 开头。其中的主机名作为 SNI 和 Host 头发送,也用于校验证书;从第一个 / 起的部分是请求路径,没有路径时使用 /dns-query。URL 中的端口会被忽略;主机在第一个 : 处截断,所以请写成域名:IPv6 字面量能通过配置检查,但每次查询都会失败。带 userinfo(user@)或主机为空的 URL 会被拒绝。其他后端忽略此项。
ca_filepath否—额外信任的 CA 证书(PEM),供 tls 和 https 在系统信任库之外使用,适合使用私有证书的解析服务器。只要设置了这个键,无论哪种后端都会读取该文件,所以即使是 system 或 udp,文件不存在也会导致配置失败;只有 tls 和 https 会解析其内容,文件中没有证书时报 no certificate in CA PEM bundle。相对路径以进程的工作目录为基准。
键 system udp tls https
server 忽略 必填 必填 必填
server_name 忽略 忽略 必填 忽略
url 忽略 忽略 忽略 必填
ca_file 读取但不使用 读取但不使用 使用 使用

所选后端不使用的键会被接受并忽略,即使它的值对其他后端来说无效也一样:在 backend = "system" 时,server = "garbage" 能通过 --test。例外是 ca_file:只要设置了它,就总会从磁盘读取,所以无论使用哪种后端,路径不存在都会导致配置失败。

config.toml
# 与不写 [dns] 相同。
[dns]
backend = "system"

每次查询都经过 getaddrinfo,因此主机的解析器配置、超时和重试设置都会生效。主机解析器不报告 TTL,所以每个答案固定缓存 60 秒。

如果解析服务器的证书由你自己的 CA 签发,把 ca_file 指向这个 CA。它会在系统信任库之外额外受信任,而不是替代系统信任库:

config.toml
[dns]
backend = "tls"
server = "192.0.2.53:853"
server_name = "dns.example.com"
ca_file = "/etc/etemenanki/dns-ca.pem"

请使用绝对路径。相对路径以进程的工作目录为基准,而在服务管理器下运行时,工作目录往往是 /。

flowchart TB
  Q["待解析的域名"] --> LIT{"是 IP 字面量?"}
  LIT -- 是 --> USE["直接使用"]
  LIT -- 否 --> HIT{"缓存中有未过期的条目?"}
  HIT -- 是 --> FILTER
  HIT -- 否 --> BACK["询问后端"]
  BACK --> ANY{"有地址吗?"}
  ANY -- 否 --> ERR["查询失败,不缓存"]
  ANY -- 是 --> STORE["按限制后的 TTL 缓存"]
  STORE --> FILTER["出站应用自己的 address_family"]
  • 地址字面量不经过解析器。 写成 IP 地址的目标或服务器会被直接使用,不发出查询,也不产生缓存条目。
  • 域名缓存不区分大小写。 Example.COM 和 example.com 共用一个条目。
  • 只缓存答案。 失败的查询或没有返回地址的查询不会被记住,下次请求该域名时会再次询问后端。
  • 缓存同时保存两种地址族。 发起查询的出站再按自己的 address_family 策略过滤并排序这些地址,见出站。策略不同的出站可以共用同一个缓存条目。
  • 并发的未命中不会合并。 如果多个连接同时请求同一个未缓存的域名,每个连接都会各自查询后端。
属性 值
容量 8192 个域名。缓存满后,会保留最常用的域名:新域名可能在旧条目的 TTL 结束前把它挤出去,也可能根本不被存入。
作用范围 整份配置共用一个缓存,所有出站和所有负载均衡器探测共享。
生命周期 缓存属于运行中的配置。重载会构建一个新的解析器,缓存从空开始。

对 udp、tls 和 https,答案的缓存时间取其 A 和 AAAA 记录中最小的 TTL,并限制在至少 5 秒、至多 3600 秒。下限防止 TTL 为零时缓存失效;上限防止过长的 TTL 把一个过时的地址固定好几个小时。system 不报告 TTL,因此使用固定的 60 秒。

udp、tls 和 https 后端共用同一条查询路径:

  • A 和 AAAA 并行查询。 一个失败不会丢弃另一个的结果:只有 A 记录的主机在 AAAA 查询失败时仍能解析。只有两个查询都没有得到地址时,查询才算失败。结果中 IPv4 答案在前,IPv6 答案在后,并去除重复项。
  • 每个查询限时 5 秒。 这个时限覆盖整个交互过程,对 tls 和 https 还包括 TCP 连接和 TLS 握手。超时后查询失败,报 dns: query timed out。不会重试。
  • 每个查询打开自己的连接。 查询之间不做连接池,也不保持连接,所以一次未命中缓存的查询需要两个连接(A 和 AAAA 各一个),对 tls 和 https 还需要两次 TLS 握手。缓存预热之后,很少需要询问解析服务器。
  • 查询是一个标准的递归查询,带随机 ID。CNAME 链交给服务器处理;解析器读取答案中的 A 和 AAAA 记录,跳过其他类型的记录。ID 不一致的响应,或者响应码非零(例如 NXDOMAIN)的响应,都视为错误。

各后端不同的细节:

后端 线路上的行为
udp 向 server 发送一个数据报,答案读入 512 字节的缓冲区。查询不带 EDNS,因此符合规范的服务器会把答案控制在 512 字节以内。不检查截断(TC)标志,也不会改用 TCP 重试:被截断的答案只产生其中仍保留的地址记录。
tls TLS 1.2 或更高版本,不使用 ALPN,以 server_name 作为 SNI。报文带 2 字节长度前缀发送,与 DNS over TCP 相同。
https TLS 1.2 或更高版本,ALPN 为 http/1.1。向 URL 的路径发送一个 HTTP/1.1 POST,带 Content-Type: application/dns-message、Accept: application/dns-message 和 Connection: close。响应体是头部之后的全部内容,一直读到连接结束;200 以外的状态码都视为错误。

DNS over HTTPS 客户端只支持 HTTP/1.1,因此服务器必须接受 HTTP/1.1 请求。它也不解码 Transfer-Encoding: chunked:如果服务器发送分块响应,分块的帧格式会被当作 DNS 报文的一部分读入,查询随之失败。

位置 查询的内容
freedom 出站,TCP 客户端请求的是域名时,查询该目标域名。
freedom 出站,UDP 每个目标域名,每个 UDP 流查询一次。第一个可用地址会在该流的剩余时间内一直使用,即使超过 TTL 也是如此;无法解析的域名,其数据报在该流的剩余时间内都会被丢弃。一个流同一时间只查询一个域名,所以发往第二个域名的数据报要等第一个查询结束。
SOCKS、HTTP、Shadowsocks、Trojan、VLESS 和 VMess 出站 出站自己的 server,用于连接上游代理。
Hysteria 2 出站 出站的 server,在建立 QUIC 连接时查询。
WireGuard 出站 隧道内的目标域名。UDP 目标的行为与 freedom 相同:每个流中每个域名查询一次。
负载均衡器健康探测 被探测成员的服务器地址。查询耗时计入探测的超时。
  • WireGuard 的 endpoint。 隧道对端的地址用主机解析器查询,绕过 [dns] 及其缓存,并使用返回的第一个地址。把 endpoint 写成 IP 地址,就不依赖任何一个解析器。见 WireGuard。
  • 发给上游代理的目标。 SOCKS、HTTP、Shadowsocks、Trojan、VLESS、VMess 或 Hysteria 2 出站会把目标域名原样交给代理服务器,由服务器解析。
  • 路由。 路由器从不解析域名。IP 和 GeoIP 规则只在目标是 IP 地址时才会匹配。嗅探得到的域名用于匹配域名规则,同样不会被查询。见路由。

与其他改动一样,对 [dns] 的修改在下一次重载时生效:新配置获得一个新的解析器,其缓存从空开始。如果新的 [dns] 表无效,重载会被拒绝,报 reload: build failed, keeping current config: …,运行中的配置保留原有的解析器和缓存。日志中的重载摘要只列出入站、出站、路由和日志的变化,所以只修改了 [dns] 的重载会记录 config reload: no changes,尽管改动已经生效。ca_file 在构建配置时读取,因此只修改 CA 文件本身,要等配置文件自身发生变化时才会生效。见热重载。

这些错误出现在 --test 的输出、启动时,以及被拒绝的重载的日志中。

错误 原因 解决办法
unknown field `…`, expected one of `backend`, `server`, `server_name`, `url`, `ca_file` [dns] 中的键拼写错误。 使用列出的键之一。
dns: unknown backend "UDP" (expected "system", "udp", "tls" or "https") backend 不是这四个值之一,或者不是小写。 用小写写出取值。
dns: the udp backend needs a server address udp、tls 或 https 缺少 server。错误信息会指出是哪个后端。 添加 server。
dns: invalid server address: invalid socket address syntax server 是主机名,或者没有端口。 写成 IP 地址加端口,例如 "192.0.2.53:53"。
dns: the tls backend needs a server name to verify against tls 缺少 server_name。 添加解析服务器证书上的名称。
dns: the https backend needs the resolver's url https 缺少 url。 添加 url。
dns: "http://dns.example.com/dns-query" is not an https:// url url 不以 https:// 开头。 使用 https:// URL。
dns: "https://user@dns.example.com/dns-query" has no usable host url 包含 userinfo,或者主机为空。 去掉 user@,或者补上主机。
No such file or directory (os error 2) ca_file 不存在。错误信息不会指出文件名。 检查路径;使用绝对路径。
no certificate in CA PEM bundle ca_file 中没有 PEM 证书(仅 tls 和 https)。 把它指向一个 PEM 文件。

查询失败会导致需要它的 TCP 连接失败。在 TCP 入站上,错误会在连接结束时以 debug 级别记录,例如 socks connection from Some(127.0.0.1) ended: dns: query timed out。在 freedom 的 UDP 流中,查询失败不会结束该流;对应的数据报会被丢弃,每次丢弃都以 debug 级别记录为 freedom: dropping a datagram to an unresolvable …。设置 [log].level = "debug" 才能看到这些日志行。

错误 含义
failed to lookup address information: Name or service not known system 后端无法解析该域名。
dns: query timed out 服务器没有在 5 秒内回答;对 tls 和 https 而言,也可能是连接或握手没有及时完成。
Connection refused (os error 111) server 的端口上没有服务在监听(udp),或者 TCP 连接被拒绝(tls、https)。
dns: server returned rcode 3 服务器返回了错误码:3 是 NXDOMAIN(域名不存在),2 是 SERVFAIL。
dns: example.com did not resolve 服务器作了回答,但没有 A 或 AAAA 记录。
dns: doh server answered "HTTP/1.1 502 Bad Gateway" DNS over HTTPS 服务器返回了 200 以外的状态。检查 url 中的路径。
dns: no http header DNS over HTTPS 服务器在发送完整的 HTTP 响应头之前关闭了连接。
dns: response id … does not match query … 回复不是这次查询的答案,例如 DNS over HTTPS 返回了分块编码的响应体。
dial: no usable ipv6_only destination address for example.com:443 域名解析成功了,但没有出站的 address_family 允许的地址。见出站。
提到 certificate verify failed 的 OpenSSL 错误 解析服务器的证书无法链接到受信任的 CA,或者与 server_name 或 url 中的主机不匹配。用 ca_file 添加它的 CA,或者改正名称。