Published on

DNS 折腾记录

Authors
  • avatar
    Name
    WoadZS
    Twitter

本文是一篇偏流水账的记录文章,如果你有和我类似的需求,请先确保自己对折腾 Linux 和 DNS 有一定的基础知识。
本次折腾,阅读了很多 pmkol 大佬的 Blog,也有幸在他群里学到了很多知识。如果有自建 DNS 需求的小伙伴建议强烈学习他的文章和 Github. 在此特别感谢大佬!

另外,如果不想折腾,请尝试大佬的开源无污染DNS(DoH),然后就可以关闭本页面了。

本套方案所需要的程序

  • AdguardHome,提供面板和去广告功能;
  • mosdns v4,提供分流,作为本套系统本地部署的最上游;
  • frp,使用 frp(个人用的是公益 frp 服务)将内网 DNS 服务提供到公网访问;
  • go-mmproxy,提供 frp 外网请求时透传请求者 IP 的功能,这是外网访问时启用 EDNS Client Subnet(ECS) 所必须的;

下面的一些程序为可选程序,可根据自己需求使用:

  • nginx,提供反代,更灵活的 DOH 配置等,也能帮你屏蔽外网对 AdguardHome 面板的请求。若没有 DOH, DOT 和 DOQ 等加密 DNS 的需求,可以不使用 nginx;
  • unbound,提供 ECS 缓存和预取(Prefetch)功能。有大量公网请求时可以选用;
  • smartdns,如果没有外网请求且量级不大,内网使用 smartdns 取代 unbound 是明智的选择。之所以本方案不使用 smartdns,是因为 smartdns 对带有 ECS 的请求缓存不够理想。
  • Redis,数据库,可以给 unbound 和 mosdns 做缓存持久化。

如果纯内网使用,强烈建议上 smartdns 体验 Prefetch. 若要使用 unbound 或 smartdns,需要配置在 AdguardHome 和 mosdns 之间。

上述程序运行在 Ubuntu 22.04 LTS 系统上,下文有些命令和脚本可根据自己的系统更改。

这套方案能满足什么样的需求?

本方案支持如下特性:

  • 由 AdguardHome 提供的简单易用的面板和广告屏蔽控制;
  • 内网外网访问均能附带正确的 ECS 向上游发起请求;
  • mosdns 带来的无污染分流体验;
  • 针对某些 CDN 如 Akamai 和 Fastly 不遵守 ECS 扩展时尽可能准确的解析;
  • 公网依照 frp 提供商和自己的需求,可灵活选择 DNS over TCP/UDP 或对应的加密 DNS 请求方式。

配置流水账

本流水账不会贴出每一个步骤所需要的命令和配置,因为实在有太多的作业可以抄写。但是我在这里还是会列出一些基于自己需求所做出的修改和遇到的暗坑。

总体请求流程和思路

DNS流程.webp

局域网

从内网设备发起请求时,请求按照如下路径流动:

  1. Adguardhome 收到内网请求(5553),并将所有内网请求经过广告过滤后转发至 mosdns 用于处理内网 ECS 的端口(5353);
  2. mosdns 为内网请求打上本地宽带 ISP 的 ECS 的附加请求,并将请求按照分流规则转发给对应的公共 DNS.
    在上面的逻辑中,使用 mosdns 附带上 ISP 的 ECS 信息是为了避免内网传输时,可能传到最后 ECS 就变成内网地址的情况发生,比如传了个 10.10.0.0./24 给上游和在远端解析时 ECS 变成远端出口的情况。

公网

公网首先需要找到一个支持 Proxy Protocol 的 frp 提供商新建一个节点。之后的流程是:

  1. 经由 frp 转发的公网请求经过 go-mmproxy 透传至 AdguardHome 端口(5553),此时 AdguardHome 可以获取到公网请求的来源 IP;
  2. AdguardHome 将来源 IP 设置为 ECS (或使用传入的 ECS,不冲突)并经过广告过滤后转发至 mosdns 主流程端口(5335);
  3. mosdns 接收到附带了公网 ECS 信息的请求按照分流规则转发给对应的公共 DNS.
    由此可见,公网和内网请求的区别在于,需要在请求进入 AdguardHome 前获取公网请求的来源 IP,这是获取 ECS 的关键。

程序配置

按照请求从上游往下游的配置顺序:

mosdns

mosdns 使用的是 v4 版本。总的来说安装配置可以抄 EasyMosdns EDNS开源方案 的作业。
EasyMosdns 是一个主要面向公网的 mosdns 分流配置,因此我们需要做一些基本的修改让他在处理内网时不会出现问题。简单的做法就是新建一个监听端口,并对发往该端口的请求附带上 ISP 的网段作为 ECS.
这里有一个迷惑的点在于,如果不带 ECS 请求上游会发生什么。这里会有两种可能的情况,均会导致解析的准确性下降:

  1. 错误的配置导致上游公共 DNS 收到的 ECS 是内网地址,比如 10.10.0.0./24
  2. 当进行分流时,请求境外网站域名可能会走代理在远端进行解析,不指定 ECS 会导致海外上游直接将远端服务器 IP 作为请求源 IP 进行解析,导致解析的结果是相对离海外 VPS 更近;

相关的 mosdns 配置是:

config.yaml
  # 调整ECS的插件
  - tag: ecs_client
    type: ecs
    args:
      auto: false
      ipv4: "219.141.136.0" # 填写本地宽带的 IP 地址
      mask4: 24
      force_overwrite: false # 关闭强制复写,否则内网使用 dig 等工具将无法使用 +subnet= 的命令  

  # 接收到内网请求的处理逻辑 
  - tag: lan_sequence
    type: sequence
    args:
      exec:
        # 统计
        - met
        - _query_summary

        # 域名映射IP
        - hosts

        # 修改 ECS
        - ecs_client #此处会将内网请求附带上配置的本地 ISP 网段作为 ECS
        - _edns0_filter_ecs_only

        # 使用主流程进行后续查询
        - main_sequence #分流之类的都可以填在 main_sequence 里

  # 接收到外网请求的处理逻辑 
  - tag: wlan_sequence
    type: sequence
    args:
      exec:
        # 统计
        - met
        - _query_summary

        # 域名映射IP
        - hosts

        # 公网访问自动获取 ECS
        # 此处不需启用类似ecs_auto的插件,因为已经由下游 AdguardHome 传输上来了
        #- ecs_auto
        - _edns0_filter_ecs_only

        # 使用主流程进行后续查询
        - main_sequence #分流之类的都可以填在 main_sequence 里

  # 内网访问使用的端口
  - exec: lan_sequence
    timeout: 5
    listeners:
      - protocol: udp
        addr: "0.0.0.0:5353"
      - protocol: tcp
        addr: "0.0.0.0:5353"
  # 公网使用的端口,可以按需求自行配置 DOH 等
  - exec: wlan_sequence
    timeout: 5
    listeners:
      - protocol: udp
        addr: "0.0.0.0:5335"
      - protocol: tcp
        addr: "0.0.0.0:5335"
   

其余的 mosdns 配置都可以参考 EasyMosdns EDNS开源方案 及其仓库的作业。

mosdns 如何解决不遵守 ECS 的 CDN 解析

市面上有部分 CDN 提供商,比如 AkamaiFastly,是不完全遵守 ECS 请求的,他们针对请求返回的 IP 是基于 DNS 查询出口的 IP 地址,而非请求中附带 ECS 信息。
为了解决这个问题,我们需要了解市面上公共 DNS 常见的两种相应 ECS 的处理方式:

  1. 将 ECS 信息发送到权威 DNS 服务器,由权威 DNS 服务器返回基于 ECS 判断的解析;
  2. 将 ECS 提取,找到距离请求中 ECS 网段最近的出口 DNS 进行查询,并返回解析。

其中,Google Public DNS 和国内腾讯的 DNSPod Public DNS 主要采用的是方案一,而 阿里公共 DNSloudflare Gateway DNS (不是1.1.1.1)主要采用的是方案二。很显然,针对权威不太理睬 ECS 请求的 CDN 解析,方案二更容易拿到相对准确的解析地址。

那么对应的思路就是:

  1. 使用任意配置的纯净 DNS 进行解析后判断,若解析结果命中 Akamai 和 Fastly 的 IP 地址不是境内 IP,则将解析重新发送到 Cloudflare Gateway DNS 进行解析;
  2. 若命中若纯净 DNS 返回境内 IP,则丢弃结果转为境内 DNS 解析;
  3. 境内交由阿里公共 DNS 解析,拿到结果后判断,若是返回的污染 IP,则重新发给境外解析并返回给客户端。

其中如果第一步就用的 Cloudflare Gateway DNS 进行预先解析,结果命中 Akamai 和 Fastly 的 IP 地址不是境内 IP 那结果就可以直接返回给客户端。

相应的我们可以给 easymosdns 配置 rules 目录下的 update 的脚本新增获取 Akamai 和 Fastly 的 IP 地址命令:

update

# 前面是其他curl命令
# 获取 Fastly IP 地址段,首次使用 jq 命令可能需要先用 apt 安装
curl https://api.fastly.com/public-ip-list | jq --raw-output '.addresses[],.ipv6_addresses[]' > /tmp/easymosdns/fastlyip.txt && \

# 获取 Github 上他人整理的 AkamaiCloud IP地址段,后面 sed 是让 ChatGPT 写的,可能有更简洁的写法。
curl https://raw.githubusercontent.com/blackmatrix7/ios_rule_script/master/rule/Surge/Cloud/AkamaiCloud/AkamaiCloud_Resolve.list | sed '/^#/d;s/IP-CIDR,//g;s/IP-CIDR6,//g;s/^[ \t]*//;s/[ \t]*$//' > /tmp/easymosdns/akamai_ip.txt && \

# 注意,上述 AkamaiCloud IP地址段由于没有找到官方的地址,因此可能存在不完全准确的情况。

# 合并两个文件
cat /tmp/easymosdns/akamai_ip.txt /tmp/easymosdns/fastlyip.txt | sort -u > /tmp/easymosdns/CDN_not_follow_ecs.txt && \
rm -rf /tmp/easymosdns/akamai_ip.txt && \
rm -rf /tmp/easymosdns/fastlyip.txt && \

# 后面是将 tmp 目录中文件复制到正式文件夹的命令

对应 mosdns config.yaml 文件中关于剩余 DNS 解析的处理也可以稍作修改:

config.yaml
   - tag: CDN_not_follow_ecs
      file: ./rules/CDN_not_follow_ecs.txt

   # 匹配不遵守ECS协议的CDN的IP范围
   - tag: response_non_ECS_CDN_IP
      type: response_matcher
      args:
         ip:
         - "provider:CDN_not_follow_ecs"

   # 下面的逻辑直接修改于 easymosdns 原始配置,请根据自己配置调整

   # 剩下的未知域名用IP分流,分流原理请参考 fallback 的工作流程
   # primary 从远程服务器获取应答,丢弃本地IP的结果
      - primary:
         - _prefer_ipv4
         - ecs_global
         - forward_googledns
         - ttl_short
         # 这里进行检查是否命中 Akamai 和 Fastly 的 IP 地址且是境外 IP 时,转发给 Cloudflare Gateway DNS 解析。
         - if: "(! response_has_local_ip) && (response_non_ECS_CDN_IP)" 
            exec:
               - forward_CFgateway         
               - ttl_long
               _ _return
         # 如果海外解析返回了境内 IP,则丢弃解析等待 fallback
         - if: response_has_local_ip 
            exec:
               - _drop_response

         # secondary 从本地服务器获取应答
         secondary:
         - ecs_auto
         # 境内改成直接使用阿里 DNS 解析
         - forward_alidns
         # 命中污染 IP 则重新发回境外进行解析
         - if: "(! response_has_local_ip) && [_response_valid_answer] || (response_has_gfw_ip)"
            exec:
               - _prefer_ipv4
               - ecs_cn
               # 修改为发往 Cloudflare Gateway DNS 进行处理
               - forward_CFgateway
               - ttl_long
         # 如果 primary 超过这个延时还没响应,可以假设 primary 出现了问题
         # 这时会采用 secondary 应答。单位: 毫秒
         fast_fallback: 2500
         always_standby: true #这里改成 true 让 primary 和 secondary 并发请求,再根据逻辑判断选用谁的解析

AdguardHome

AdguardHome 在本系统中主要是提供面板和广告过滤的功能,安装和使用都比较简单,也不需要启用任何缓存(包括乐观缓存)。
安装可以参考 AdguardHome 的官方仓库

但在使用 AdguardHome 时,需要做的特殊配置为:

  • 需要在 设置 - DNS 设置 中,开启 启用 EDNS 客户端子网,但不要勾选 为 EDNS 使用自定义 IP
  • 需要在 设置 - DNS 设置 - 上游 DNS 服务器 中,填写 mosdns 的监听 IP 和端口,如 127.0.0.1:5335
  • 设置 - 客户端设置 中,在 持久客户端 中新增一个标定所在局域网子网的客户端。标识符填写局域网 IP CIDR, 如 192.168.0.0/24, 同时指定 上游 DNS 服务器 为前面 mosdns 配置中专门用来处理内网 ECS 的 IP 和端口,比如 127.0.0.1:5353
ADGHclientConf.webp

在完成上述配置后,可以将内网所有的 DNS 查询上游配置为 AdguardHome 的 UDP 查询地址和端口,如 192.168.0.1:5553
这时内网发起的查询,在 AdguardHome 识别到是已经配置的持久化客户端后,将请求转发至 mosdns 内网 sequence 端口;而公网的请求在下文提到正确配置条件下,将可以得到外网请求的客户端 IP 并将其作为 ECS 附加信息直接传递给 mosdns 公网 sequence 端口

frp & go-mmproxy

这两个可以一起记录,因为想要向内网传递公网请求 IP 二者缺一不可。不过,当你希望使用明文 DNSDOT 查询时才需要用到 go-mmproxy, 如果你希望使用的是 DOH 这种基于 HTTPS 协议的查询方式,使用 frp + nginx 反代才是正确的解决方法。

frp 向内网透传公网 IP 需要用到 Proxy Protocolnginxgo-mmproxy 可以读取该协议传回真实 IP. Proxy Protocolv1v2 两个版本,实际使用 v2 即可。该选项需要在 frp 服务端提前设置好。
在内网机器上需要使用 go-mmproxy 需要确保后端(此处即 AdguardHome)和go-mmproxy属于同一 IP 地址,可通过 127.0.0.1 访问。

go-mmproxy 的编译安装和使用可以直接照搬其 Github 上的说明,在编译安装完成后可以直接输入命令:

ip rule add from 127.0.0.1/8 iif lo table 123
ip route add local 0.0.0.0/0 dev lo table 123

ip -6 rule add from ::1/128 iif lo table 123
ip -6 route add local ::/0 dev lo table 123

注意上面的路由修改是临时性的,重启失效,若想效果持久化可自行 Google.
为了方便管理 go-mmproxy, 我们也可以为他写一个 systemd 服务:

sudo vi /etc/systemd/system/go-mmproxy.service

然后写入以下内容:

go-mmproxy.service
[Unit]
Description=go-mmproxy

[Service]
Type=simple
Restart=always
RestartSec=3
# 可以在运行 go-mmproxy 前用其他命令或脚本重置对应路由,
# 确保 go-mmproxy 接管 frpc 流量成功。
ExecStartPre=/etc/mosdns/mmproxy_route_reset.sh
# 此处假定 go-mmproxy 位于 /root/work/bin/ 下,
# frpc 客户端端口为 8443, AdguardHome 监听端口为 5553.
ExecStart=/root/work/bin/go-mmproxy -l 0.0.0.0:8443 -4 127.0.0.1:5553

[Install]
WantedBy=multi-user.target

然后在设置启动即可。

sudo systemctl daemon-reload
sudo systemctl start go-mmproxy
sudo systemctl status go-mmproxy
sudo systemctl enable go-mmproxy

在完成上述所有步骤后,就可以使用 frp 的公网 IP 和端口进行查询了。并且 AdguardHome 也能正确识别到外网请求的来源 IP.

至此,你已经拥有了一个同时面向内网和公网的 DNS 服务器,之后的小节是一些补充性的说明。

补充

smartdns

smartdns 是一个好东西,其最大的优点是可以进行 DNS Prefetch,即在 DNS TTL 即将到期前进行预取,保持缓存的新鲜度。但缺点是针对附带 ECS 的请求缓存效果不甚理想。
因此纯内网使用时,在 AdguardHome 和 mosdns 增加一个 smartdns 可以有效提升体验。对应的配置为:

smartdns.conf
# AdguardHome 需要将请求发送到这里
bind [::]:1053 -no-dualstack-selection -no-speed-check
bind-tcp [::]:1053 -no-dualstack-selection -no-speed-check

server 127.0.0.1:5335 # 这里是 mosdns 的监听端口

cache-size 5000
cache-persist no
prefetch-domain yes
serve-expired no

当使用 smartdns 时,缓存设置为:

  • AdguardHome 无缓存
  • smartdns 有缓存但禁用乐观缓存,且开启 Prefetch
  • mosdns 开启缓存且开启乐观缓存

unbound

unbound 是专业的 DNS 处理程序,连 Cloudflare 用了都说好的那种。它支持良好的 ECS 缓存且可以进行 DNS Prefetch,但配置相对复杂一些。若无公网使用的需求,还是不建议尝试。
由于使用包管理直接安装的 unbound 并不包含 ECS 支持,因此需要自行编译安装。但是万能的 pmkol 大佬已经在 Github 准备好了已经打包好的静态编译 unbound.
关于安装和配置,可以参考大佬的文章 AlmaLinux 编译安装支持EDNS的Unbound, 但是其中并不需要编译,可以直接前往大佬的 Github 仓库 Unbound Master Static Build 下载编译好的程序文件。
在 unbound 的配置中最重要的是确保如下几个配置正确:

unbound.conf
  send-client-subnet: 0.0.0.0/0
  send-client-subnet: ::0/0
  max-client-subnet-ipv4: 24
  max-client-subnet-ipv6: 56
  client-subnet-always-forward: yes 

  # subnetcache 一定要有,否则不会对 ECS 请求进行缓存
  module-config: "subnetcache cachedb iterator" 

不过要是参考 pmkol 大佬的配置,他都已经帮你配置好了。

关于 unbound 有一个潜在的坑点在于,内网的请求转发 ECS 时可能会导致 ECS 变成内网地址,如 10.10.0.0/24。因此内网的请求需要先使用 mosdns 附带上正确的 ECS 信息,再发往 unbound 缓存。而外网请求经过 AdguardHome 处理后已经附带了正确的 ECS,可以直接转发给 unbound.
因此按内外网请求流程是:

  • 内网请求→ AdguardHome → mosdns 附加 ECS 信息(ISP 网段)→ unbound 缓存 → mosdns 分流转发上游
  • 外网请求→ AdguardHome → unbound 缓存 → mosdns 分流转发上游

当使用 unbound 时,缓存设置为:

  • AdguardHome 无缓存
  • unbound 有缓存但禁用乐观缓存,且开启 Prefetch
  • mosdns 开启缓存且开启乐观缓存

nginx

nginx 可以和 acme.sh 配套,提供反代 AdguardHome DOH 的能力,同时屏蔽外网访问 AdguardHome 面板的风险(AdguardHome 开启 DOH 后可以在同一个端口访问面板)。并且 nginx 也原生支持 Proxy Protocol,意味着也不需要折腾 go-mmproxy了。
下面给一个简单的 nginx 基于 Proxy Protocol 向 AdguardHome 传递真实 IP 反代 DOH 的配置:

adguard.conf
server {
        listen 8443 ssl http2 proxy_protocol; # 需要加入 proxy_protocol 参数,8443 为 frpc 客户端端口
        server_name doh.example.com; #这里填写域名
        index index.php index.html index.htm default.php default.htm default.html
        root /usr/share/nginx/html; 

        # HTTPS 证书
        ssl_certificate /etc/nginx/cert/dns/fullchain.pem;
        ssl_certificate_key /etc/nginx/cert/dns/privkey.pem;

        # 开启 OCSP Stapling
        ssl_stapling on;
        ssl_stapling_verify on; # 启用OCSP响应验证,OCSP信息响应适用的证书
        resolver 223.5.5.5 119.29.29.29 valid=60s; #添加resolver解析OSCP响应服务器的主机名,valid表示缓存。
        resolver_timeout 2s; # resolver_timeout表示网络超时时间

        #反向代理 DoH
        location ^~ /dns-query {
                proxy_pass https://127.0.0.1:9443/dns-query; # 9443 为 AdguardHome DOH 端口
                proxy_set_header Host $host;
                # 通过 $proxy_protocol_addr 参数向 header 中添加真实 IP 信息
                proxy_set_header X-Real-IP $proxy_protocol_addr; 
                proxy_set_header X-Forwarded-For $proxy_protocol_addr;
                proxy_set_header REMOTE-HOST $proxy_protocol_addr;
        }

        #禁止远程访问仪表盘
        location / {
                return 403;
        }
}

后记

这一次折腾,也算是跟着 pmkol 大佬学了不少内容,上面的许多配置和处理方式都有许多可以改进的空间。同时由于是个人使用,因此也有很多本来很困难的地方都被无视了。比如如何做到更合理快速的缓存,如何确保更精确的分流,如何做到更高的并发……
DNS 有太多的学问,一个良好配置的 DNS 服务器确实能改善不少网络访问上各种稀奇古怪的问题。你问问现在爽不爽?公网解析非首次查询稳定小于 10ms (DNS over TCP)的无污染 DNS 你说爽不爽?

最后的最后,再给大佬打个广告,大佬也有自己的无污染DNS,也请关注大佬的 EasyMosdns EDNS开源方案

DNS 折腾记录
本文作者
WoadZS
发布时间
许可协议
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!