mosdns自建智能DNS:缓存、内网域名与广告拦截配置实践

发布时间:2026/11/4 21:33:21
mosdns自建智能DNS:缓存、内网域名与广告拦截配置实践

DNS 这东西,平时没人想起它,一旦出问题,全家所有设备一起遭殃:网页转圈、视频卡顿、智能音箱喊半天不应。我家网络最早就是运营商默认下发的 DNS,用了两年,某天开始某些域名解析结果怪得很,同一台设备重启光猫后解析到的地址就变了,排查半天才定位到是本地链路的解析环节被改写。后来索性自建了一台 DNS 服务,用 mosdns 做统一入口,把缓存、内网域名、广告拦截、上游选择全部收拢到一份配置文件里。这套东西跑了一年多,最直观的收益是三件事:常用域名的解析几乎全部命中本地缓存,首包时间从 30~60ms 降到 1ms 以内;内网的 NAS、打印机、开发机统一用*.lan域名访问,再也不用记 IP;家里小孩的设备上那堆乱七八糟的追踪域名,直接在解析层被掐掉,页面加载反而更快。

mosdns 是一个用 Go 写的开源 DNS 转发与处理工具,核心卖点是插件化的处理链:你可以把"缓存""匹配域名集合""转发到指定上游""改写响应"这些动作像积木一样拼起来,拼出完全符合自己网络情况的解析逻辑。它体积小、单文件就能跑、内存占用低,扔在路由器、迷你主机、甚至一台常年开机的旧笔记本上都不心疼。这篇内容面向三类人:一是家里网络设备多、想让解析行为可控可查的折腾型用户;二是小型办公网络里需要维护内网域名和统一出口的管理员;三是想学习 DNS 处理链设计、拿它当练手项目的开发者。下面我按"为什么这么设计—配置怎么写—怎么验证—坑在哪"的顺序,把这一年的实践完整摊开。

1. 先想清楚:为什么要在自己网络里放一台智能 DNS

1.1 三个真实痛点,决定了这个方案的必要性

很多人第一次听说自建 DNS,反应是"运营商给的 DNS 不是能用吗"。能用,但只是"能用"。我把自己踩过的三类问题列出来,你对照一下有没有中招。

第一类是解析结果不理想。公共 DNS 服务为了覆盖全国用户,通常按递归出口的位置返回结果。你家宽带的实际链路和这个递归出口不一定一致,尤其是自建了多线接入、或者所在区域网络结构比较特殊的场景,拿到的解析地址可能绕了一大圈。表现出来就是:明明带宽很足,某些站点打开就是慢一拍。

第二类是内网域名缺失。家里的 NAS、监控、开发机、打印机,默认只能靠 IP 访问。IP 会变,DHCP 一续租就乱套,家里其他人根本记不住。路由器的 DHCP 有时能配本地域名,但功能往往很弱,不支持通配、不支持多级后缀、更不支持按域名决定走哪个上游。

第三类是无法观测。出问题时你完全不知道是解析慢、连接慢还是服务端慢。浏览器开发者工具只能看到一个总耗时,看不到 DNS 查询走了谁、花了多久、有没有命中缓存。一台自己的 DNS 服务,天然带日志和统计,出问题第一时间能定位到环节。

mosdns 恰好把这三件事都覆盖了:它自带缓存插件、支持域名集合匹配、支持多上游并发、能输出结构化日志。这就是我选它而不是直接用路由器自带 DNS 转发的根本原因。

1.2 几种常见做法的横向对比,以及我为什么落在这一个

在决定用 mosdns 之前,我把能想到的方案都试了一遍。这里做个对照,方便你按自己的实际情况选。

方案优点明显短板适合谁
直接用上游公共 DNS零配置,改个 IP 就行无缓存、无分流、无日志、结果不可控只求能上网的用户
路由器自带 DNS 转发不用额外设备功能极简,多数不支持域名集合匹配,日志几乎没有单线路、需求简单的家庭
通用 DNS 服务软件(传统权威/递归一体)功能全、生态成熟配置重、资源占用高、插件扩展不灵活有一定规模的正规网络环境
mosdns单文件、插件链灵活、资源占用低、日志清晰配置是声明式的,需要理解执行顺序;文档需要对照版本读家庭折腾、小型办公、学习者

最后一项的短板我特意写清楚:mosdns 的配置是 YAML 声明式的,它的执行模型是"一条链从上往下走,命中即执行对应动作"。如果你按面向过程的思路去理解,很容易写出"看起来对但不生效"的配置。这也是我后面要花大篇幅讲sequence的原因。

另外补一句选型经验:别一上来就追求"完美分流"。我最早的配置写了 200 多行,规则集一堆,结果维护成本高到自己都不想改。现在这份配置只有 80 行左右,覆盖了 95% 的日常场景,改起来一目了然。配置文件是给自己看的,可维护性比功能完备更重要。

1.3 部署位置的选择:路由器、软路由还是独立小主机

这一条我建议你在动手前就想清楚,因为装完再搬家挺烦的。

把 mosdns 跑在路由器上,好处是不增加设备、网络路径最短、所有下游设备自动受益。前提是路由器能跑第三方固件或者支持容器,且剩余存储和内存够用。我试过在一台内存 256MB 的老路由器上跑,日常没问题,但开日志加上大体积域名集合之后,内存会紧张,偶尔触发 OOM。

跑在独立小主机或者一台常年开机的设备上,好处是资源充裕、升级方便、日志能长期留存。代价是多一个设备要维护,而且必须保证它的 IP 固定,否则整个网络的 DNS 就跟着漂。

我现在用的是第二种:一台低功耗小主机,固定内网 IP,用容器方式运行。选容器不选裸二进制的原因很实在——升级时换镜像标签就行,回滚也是一条命令;配置文件用卷挂载,容器重建不影响数据。如果你手上只有一台设备,裸二进制也完全可以,它没有外部依赖,解压即用。

注意:不管部署在哪,这台设备的 IP 必须固定。DHCP 保留、静态配置都行,但千万别让它飘。DNS 服务地址一变,整个网络所有设备都会解析失败,那种"全屋断网"的体感比宽带故障还吓人。

2. 环境准备:从零到能跑通的最小闭环

2.1 系统、端口与权限这几个前置条件

mosdns 对系统要求极低,主流的 Linux 发行版、容器环境都能跑。需要确认的只有三件事。

一是53 端口必须能拿到。DNS 默认走 UDP 53 和 TCP 53。如果在 Linux 上直接跑,需要绑定特权端口,容器方式则要给相应权限。这里有个特别常见的坑:很多发行版预装了本地解析服务,它已经占着 53 端口,你启动 mosdns 会直接报"地址被占用"。解决办法是先停掉并禁用它,把 53 端口完整让出来。

二是监听地址要写对。如果只监听127.0.0.1:53,那只有本机能用,局域网里其他设备访问不到。要让整个网络使用,必须监听0.0.0.0:53或者指定内网网卡地址。我第一次配置就犯了这个错,本机测试一切正常,换到手机上就解析失败,查了半小时才反应过来。

三是上游连通性。mosdns 本身不做递归解析,它靠上游。所以要保证这台设备能正常访问你选定的上游地址,UDP 53 和 TCP 53 都要通——有些上游在响应包较大时会要求走 TCP 重查,只开 UDP 会出现"大记录查不到"的诡异现象。

准备工作清单我整理成这样:

检查项命令 / 做法通过标准
53 端口是否被占用ss -lunp | grep :53无输出,或输出中只有你自己的服务
本地解析服务是否已停systemctl status相关服务处于 inactive / disabled
上游 UDP 可达用dig @上游地址 域名有正常 ANSWER
上游 TCP 可达同上加+tcp参数有正常 ANSWER
设备 IP 是否固定查看当前地址与租约重启后地址不变

2.2 二进制与容器两条路,我为什么推荐容器

二进制方式最直接:下载对应架构的压缩包,解压出可执行文件和示例配置,改完配置直接跑。优点是零依赖、启动快、排查问题时中间层少。缺点是升级要手动替换文件,配置文件路径、日志路径、规则文件路径都得自己维护好。

容器方式多了一层,但换来的是可复现。我的做法是把配置文件、规则文件、日志目录分别用卷挂载进容器,容器本身只当运行环境。这样升级时改一下镜像标签,配置文件完全不动;配置写坏了想回滚,把旧配置目录换回来重启即可。

启动命令大致是这样:

docker run -d \ --name mosdns \ --restart unless-stopped \ --network host \ -v /opt/mosdns/config.yaml:/etc/mosdns/config.yaml:ro \ -v /opt/mosdns/rules:/etc/mosdns/rules:ro \ -v /opt/mosdns/logs:/var/log/mosdns \ mosdns/mosdns:latest \ start -c /etc/mosdns/config.yaml

这里用--network host是为了让容器直接占用宿主机的 53 端口,省去端口映射在 UDP 上的麻烦。如果你更希望隔离网络,改成端口映射-p 53:53/udp -p 53:53/tcp也能用,但要确认宿主机没有其他服务抢这个端口。

提示:镜像标签建议锁定到具体版本号而不是latest。DNS 是整个网络的基础设施,某天自动拉到一个行为有变化的新版本,全家断网再去查日志,代价太大。

2.3 把 DNS 从"别处"切过来:切换顺序很重要

配置写好了别急着让全网用,给自己留条退路。我的切换顺序是这样的:

  1. 先在 mosdns 里监听一个非 53 的高位端口,比如 5353,本机用dig -p 5353验证配置正确;
  2. 配置正确后改成 53 端口重启,让它在局域网可用;
  3. 先手动改一台设备(我用自己的手机)的 DNS 指向它,观察一两天;
  4. 确认稳定后,再改路由器的 DHCP 下发设置,让新设备自动拿到这个地址;
  5. 老设备重启或者续租之后才会生效,别指望立刻全网切换。

第 3 步那"一两天"很关键。DNS 的问题往往是特定域名、特定时段才暴露,比如某个 CDN 域名在你所在区域解析到了一个很远的节点,白天正常晚上慢。观察期能把这些抓出来。

3. 吃透核心概念:插件、插件链和执行顺序

3.1 插件化模型:一切皆插件,配置就是声明实例

mosdns 的配置模型很干净:全是插件实例。你在配置里声明一堆插件,每个插件有一个tag(唯一标识)和一个type(插件类型),再加args(参数)。然后是各种"入口插件"——UDP 服务、TCP 服务、HTTP 服务——它们把收到的请求交给某条链处理。

理解这一点之后,配置文件就不再是"神秘的一坨",而是一张依赖关系图:入口插件指向处理链,处理链里的步骤引用其他插件。我脑子里画的示意大致是:

udp_server(入口) -> sequence(主链) -> cache -> 匹配器 -> forward(上游)

这里有个细节值得强调:插件之间靠tag引用,写配置时用$tag的形式。写错一个字母,启动时就会报"找不到插件",好在它报错很直接,不会静默失败。这也是我喜欢它的原因——配置错误在启动阶段就暴露,而不是运行中悄悄走了错误的分支。

另外,插件声明顺序不等于执行顺序。很多人(包括我)一开始误以为写在前面就先执行。实际执行顺序完全由sequence里的步骤顺序决定,声明顺序只影响初始化。记住这句话能省下大量调试时间。

3.2 sequence 的执行逻辑,以及"命中即终止"这个隐含规则

sequence是整份配置的心脏。它接收一串步骤(args是一个列表),从上往下执行。每个步骤有两种形态:

  • - exec: $某插件:无条件执行这个插件的动作;
  • - matches: 条件加exec: $某插件:条件成立才执行。

关键在于中止语义。链的执行不是"全部跑一遍然后汇总",而是"遇到能产生最终响应的动作就结束"。具体来说,如果某个步骤里的转发插件拿到了响应,或者缓存命中了,链就到此为止,后面的步骤不再执行。这个设计非常合理——它让"缓存优先、命中即返回"变成自然结果,而不是需要额外判断的逻辑。

我最初的配置犯过一个典型错误:把缓存插件放在链的末尾,想着"最后兜底"。结果缓存形同虚设,因为前面的转发步骤已经拿到响应并终止了链,缓存插件根本没机会执行。这个坑值得单独拎出来说:

注意:缓存插件必须放在链的最前面。它的设计是"先查,命中就短路返回;没命中就放行,等后续步骤产生响应后由它自己写入"。放错位置的后果是缓存永远不命中,但日志里看不出任何异常。

3.3 匹配器怎么选:qname、qtype、响应侧判断

匹配器决定"这个请求该不该走这条分支",是分流逻辑的基础。常用的几类我按使用频率列一下。

qname匹配域名集合,最常用,配合domain_set插件加载的域名列表使用。qtype匹配查询类型,比如你想对 AAAA 查询做特殊处理。has_resp判断当前是否已拿到响应,用于在响应侧做加工。响应侧的resp_ip匹配响应里的 IP 集合,这个在"按解析结果决定是否接受"的场景里非常有用,比如上游返回的地址质量不佳时改走另一个上游重查。

domain_set的亮点是它的匹配规则表达能力。它支持完整匹配、后缀匹配、关键字匹配、正则表达式,还支持从文件加载。我在内网域名集合里用了后缀匹配,一条lan的后缀规则就覆盖了所有*.lan,不用逐个列。对比一下,如果用的是只能精确匹配的方案,光内网域名就得维护几十行。

这里说一个实操经验:域名集合文件用独立文件维护,不要内联在配置里。原因很简单,域名列表会频繁变动(尤其广告拦截列表),而配置结构基本不动。分开之后,更新列表只需替换文件、重载配置,主配置一个字符都不用碰,出错概率低得多。

4. 手写一份能跑的配置:逐段拆解

4.1 最小可用骨架,先跑通再加功能

我一直建议"先跑通最小的,再叠功能"。下面这份配置是能直接用的最小骨架,包含缓存、内网域名、广告拦截、公网上游四块,逻辑清晰到能当模板。

log: level: info file: "/var/log/mosdns/mosdns.log" api: http: "127.0.0.1:9091" plugins: # 1) 缓存:必须放链首 - tag: cache_main type: cache args: size: 8192 lazy_cache_ttl: 86400 cache_everything: false # 2) 域名集合:内网 - tag: set_intranet type: domain_set args: files: - "/etc/mosdns/rules/intranet.txt" # 3) 域名集合:广告与追踪 - tag: set_adblock type: domain_set args: files: - "/etc/mosdns/rules/adblock.txt" # 4) 广告域名统一拒绝 - tag: block_ads type: black_hole args: - "127.0.0.1" # 5) 内网上游:指向局域网内既有解析服务 - tag: fwd_intranet type: forward args: concurrent: 1 upstreams: - addr: "192.168.10.1" # 6) 公网上游:两个地址并发 - tag: fwd_public type: forward args: concurrent: 2 upstreams: - addr: "223.5.5.5" - addr: "119.29.29.29" # 7) 主链 - tag: main type: sequence args: - exec: $cache_main - matches: qname $set_adblock exec: $block_ads - matches: qname $set_intranet exec: $fwd_intranet - exec: $fwd_public # 8) 入口 - tag: udp_in type: udp_server args: entry: main listen: "0.0.0.0:53" - tag: tcp_in type: tcp_server args: entry: main listen: "0.0.0.0:53"

上游地址用的是两个公开解析服务的地址,这只是示例,请替换成你所在网络实测最优的。测法很简单:拿一堆你常访问的域名,分别对不同上游dig,对比解析耗时和返回地址,选最快的两个。别照抄别人的推荐,网络环境差异比想象中大。

主链的执行顺序读起来就是一句话:先查缓存;命中广告集合就拒绝;命中内网集合就走内网上游;其余走公网上游。四行配置,逻辑自洽,这也是我最终收敛到的形态。

4.2 缓存参数怎么定:size 和 lazy_cache_ttl 的算法

这块是新手最容易拍脑袋填数字的地方。我说说自己的计算方法。

size是缓存条目数。一个家庭网络,日常活跃域名有多少?我的实测数据:一台路由器 + 8 台设备,连续跑 7 天,去重后的查询域名约 2500 个。所以 8192 条已经相当宽裕,能覆盖几天的活跃集合,还能容纳长尾。如果你管理的是小型办公网络,几十台设备,那 16384 到 32768 比较稳妥。别一上来就填 100 万——缓存条目是要占内存的,每条记录包含域名、类型、响应记录、时间戳,粗算每条 200~500 字节,10 万条就是几十 MB,对一台 256MB 内存的设备是实打实的压力。

lazy_cache_ttl是"惰性缓存"的存活时间。它的作用是:某个域名缓存过期之后,如果还有请求进来,先返回这份过期记录,同时后台再去上游更新。这叫"stale-while-revalidate",用一点点数据陈旧换取极低的响应延迟。我设的是 86400 秒(一天)。

这个值设大设小有什么影响?设太小,热门域名过期后要等上游,延迟抖动明显;设太大,某些变更频繁的域名(比如负载均衡后面经常换 IP 的服务)可能长时间返回旧地址。一天的折中我觉得合适,毕竟绝大多数域名的解析地址在一天内的变化对你没有实际影响。如果你的网络里有那种"必须实时准确"的内部服务域名,把它放进内网集合走单独上游,别让它进这个惰性缓存。

cache_everything这个开关要谨慎。它会让所有响应都进缓存,包括本该不缓存的类型。我没开,因为默认策略已经足够,开了反而可能缓存住一些不该缓存的记录,导致排查问题时被误导。

4.3 内网域名集合怎么写,才能覆盖得干净

内网集合文件是我维护频率最低但价值最高的一个文件。内容大致两类:本地后缀,以及少量必须本地解析的特殊域名。

# 本地后缀,覆盖 *.lan 及所有子域 lan local home.arpa # 特定设备别名 nas.lan printer.lan dev-box.lan

domain_set的匹配规则里,一行裸域名默认按"后缀匹配"理解,所以写lan就能命中nas.lan、a.b.lan这类。这个行为比很多人预期的宽松,正是我们要的。

不过有个坑得提醒:别把过于宽泛的后缀写进内网集合。我曾经顺手加了一行local,本意是覆盖xxx.local的局域网设备发现,结果某些公网域名也带这个后缀片段,被误判走内网上游,解析直接失败。排查时看着日志里"走了错误分支"的记录才恍然大悟。原则是:只写你确实拥有的后缀,公共域名后缀一个都别碰。

另一个实用技巧是用hosts插件给固定设备做直答,比走内网上游快一个数量级:

- tag: my_hosts type: hosts args: entries: - "nas.lan 192.168.10.20" - "printer.lan 192.168.10.31"

hosts的响应是本地的、零延迟的,对于那几台 IP 固定的核心设备,直接写死最省事。设备 IP 变了就改一行配置重载,比重启整个 DHCP 服务友好得多。

5. 智能分流的落地:从"能解析"到"解析得准"

5.1 分流不是玄学:先明确你要分什么

一说"智能 DNS",很多人脑子里浮现的是复杂的地理位置判断。实际落地时,分流的目标通常只有三类,想清楚就好办。

第一类是范围分流:内网域名走内网上游,公网域名走公网上游。这个前面已经实现了,是最基础也最有价值的一层。

第二类是质量分流:同一个公网域名,不同上游返回的地址质量差异很大。做法是把上游分成两组,主组两个地址并发,谁先回用谁;如果主组超时或者返回结果明显不合理(比如返回了私有网段地址,那多半是被中间设备改写了),自动切备用组。

第三类是拦截分流:广告、统计、追踪类域名直接返回一个无效地址,让请求快速失败,而不是等它超时。这类域名有个特点——请求量巨大且高度重复,拦掉之后收益立竿见影。我实测过,家里几台设备开着网页,拦截生效后页面整体加载时间的改善是能感觉到的,因为那些追踪脚本不再拖着主线程。

这里必须强调一个实操原则:用响应侧判断补足请求侧判断。只看域名决定走向,有时候不够。比如某个域名被你的规则判给了主组,但主组返回的地址在本网络下不可达,这时候响应侧的resp_ip判断就能派上用场——发现响应地址落在"质量不佳集合"里,就改走备用组重查一次。这套机制比单纯堆域名列表有效得多。

5.2 上游并发与超时:两个参数决定体感

concurrent是"并发查询几个上游"。设为 2 表示同时向两个上游发请求,谁先回就用谁。这个机制对体感的改善非常明显,因为它把"最慢的上游"变成了"最快的那两个里更快的那个"。

但并发不是越多越好。设成 5,你会发现上游的请求量翻了 2.5 倍,某些公共解析服务对高频请求有限制,可能直接给你限速或者返回错误。我的建议是 2,最多 3。两个上游的故障域已经足够分散——同时挂两个的概率低到可以忽略。

超时方面,默认值一般能用,但如果你的网络有链路抖动,可以适当放宽。判断依据看日志里的耗时分布:如果大量请求耗时集中在 800ms 以上,说明上游响应慢,先换上游;如果大量请求直接是超时记录,那可能是网络路径问题或者上游限制了你的请求频率。先看日志再调参数,别凭感觉改。

还有一个容易被忽略的点:上游要选不同类型的地址。比如一个走明文 UDP,一个走加密查询(DoH/DoT)。加密查询的好处是查询内容在网络路径上不明文暴露,避免被中间设备看到甚至改写。配置写法是用 URL 形式:

- tag: fwd_secure type: forward args: concurrent: 2 upstreams: - addr: "https://dns.example-provider.com/dns-query" - addr: "223.5.5.5"

(示例域名请替换为你信任的服务商地址。)这样即使加密那条超时,明文那条也能兜住,可用性和隐私性兼顾。

5.3 ECS 客户端子网:什么时候该用,什么时候是灾难

ECS(EDNS Client Subnet)的作用是告诉上游"我的客户端大概在哪个网段",让 CDN 类服务返回地理上更近的节点。听起来很美好,实际用起来要看情况。

该用的场景:你所在网络的上游递归出口离你很远,导致解析到的 CDN 节点绕路。开启 ECS 后上游会按你声明的网段返回就近地址,实测下载速度和首包延迟都有改善。

不该用的场景:你的网络对隐私敏感,不想把内网网段信息透露给上游;或者你已经在用多个上游并发,不同上游对 ECS 的处理策略不一致,反而带来了结果抖动。

我现在的做法是只在特定域名集合上开启。给视频类、下载类这些对节点位置敏感的域名走一个带 ECS 的上游组,其他域名走不带 ECS 的普通组。这样既拿到了就近解析的收益,又不会把全部查询都带上位置信息。配置上就是再建一个 forward 组,在链里用qname匹配器把对应域名集合指过去,逻辑和前面完全一样,只是多了一条分支。

这个分组的维护成本很低,域名集合文件里加几行的事。但它带来的收益是实打实的:同样是看视频,开启前后缓冲频率的差别,家里人是能直接感觉出来的。

6. 观测与验证:别猜,去看日志

6.1 日志分级和查询日志,怎么开才不淹死

mosdns 的日志分级很有必要认真设置。日常跑info就够,排查具体某个域名时临时调到debug,问题定位完立刻调回来。为什么强调这个?因为debug级别的日志会把每一个查询的完整决策过程都打出来,一个家庭网络一天几十万条查询,日志文件几小时就能涨到几百 MB,磁盘吃不消,日志读起来也费劲。

我的日志策略是分层的:

场景日志级别保留策略用途
日常运行info按天切分,保留 7 天看错误和异常
排查单域名debug临时开启,用完即关看完整决策链
长期统计info + 查询接口配合接口拉指标看缓存命中率、上游耗时

配合配置里的 HTTP 接口(api.http),可以拉取运行指标。我的习惯是每周瞄一眼缓存命中率:健康状态下,家庭网络的命中率应该在 70% 以上。如果掉到 50% 以下,通常说明缓存被频繁刷新,或者lazy_cache_ttl设得太小,或者有异常的大量随机域名查询(那可能是某台设备中了招,在疯狂请求随机域名)。

6.2 用 dig 做三层验证,一次到位

配置改完必须验证,而且不能只验证"能不能解析"。我做三层验证,一层比一层细。

第一层,基础连通:

dig @192.168.10.20 example.com +short

有正常应答就说明服务在跑、上游通。这一层不过,别往下看,先解决基础问题。

第二层,分支走向。验证某个域名是否真的走了你期望的分支。做法是查一个内网域名,看返回的地址是不是内网地址:

dig @192.168.10.20 nas.lan +short

如果返回的是192.168.10.20,说明内网分支生效。如果返回超时或者返回了公网地址,那说明你的域名集合没匹配上,去检查集合文件的后缀写法和文件路径。

第三层,延迟与缓存。连续查两次同一个域名,看耗时:

dig @192.168.10.20 example.com | grep "Query time" dig @192.168.10.20 example.com | grep "Query time"

第二次应该明显更快(1ms 以内)。如果两次都差不多,说明缓存没生效,回头看缓存插件是不是放在了链首。这一层是最容易被忽略的,但恰恰是最有价值的——它验证的是你优化工作的实际成果。

还有一个进阶验证:查一个不存在的域名,看返回码。正常应该是 NXDOMAIN。如果返回 NOERROR 但答案是空的,或者干脆超时,说明上游行为异常或者链路上有设备在改写响应。这种情况我遇到过,换成加密上游后立刻正常。

6.3 一台设备一个策略:灰度上线的具体操作

前面提过灰度,这里给个更具体的做法。我按设备重要性分了三批:

  1. 第一批:我自己的手机和笔记本。改 DNS 指向新服务,用三天。这三天我会特意访问各种类型的站点,把异常记下来。
  2. 第二批:家里的电视盒子、平板。这些设备使用场景单一,问题暴露慢但影响直观(视频卡不卡,一眼就知道)。用一周。
  3. 第三批:其余所有设备,通过 DHCP 统一下发。这时候前两批已经覆盖了大概率的坑。

灰度期间建议保留一份"快速回退"方案:把路由器的 DHCP DNS 设置改回原来的值,然后让设备重新连一次 WiFi 就能恢复。这个动作我演练过一次,确认 2 分钟内能恢复全网,心里才踏实。

7. 常见问题与排查实录

这一节是我一年里真实遇到的问题汇总,按"现象—可能原因—验证方法—处理"整理成表,方便你直接对号入座。

现象可能原因验证方法处理
启动报地址被占用53 端口被本地解析服务占用ss -lunp | grep :53停用并禁用占用服务,或先换高位端口验证
本机能查,其他设备查不了监听地址写成了127.0.0.1查看配置中listen字段改成0.0.0.0:53或内网网卡地址
缓存完全不命中缓存插件不在链首连续查询同一域名比耗时把缓存插件移到sequence第一步
内网域名解析失败集合文件路径错或后缀匹配没生效dig查内网域名看返回核对文件挂载路径,用裸后缀而不是通配符写法
某些域名解析到奇怪地址上游结果被中间设备改写换加密上游对比结果启用 DoH/DoT 上游,或更换上游
大记录查不到(如长 TXT)只开了 UDP,截断后未走 TCPdig +tcp对比确保 TCP 53 入口也配置并可用
内存持续增长缓存条目过多或日志级别为 debug看进程内存与日志文件大小下调size,日志改回 info
升级后行为变化新版本插件参数或默认值调整对比官方文档与版本变更说明锁定镜像版本,先在非 53 端口验证
广告拦截误伤正常站点拦截列表过于激进临时移除该域名从列表验证换更保守的列表,或加白名单
全网间歇性解析慢上游响应慢或并发过高被限速看日志耗时分布降concurrent到 2,换更近的上游

表格之外,还有几条不容易归类但很值得说的经验。

第一条:不要在同一条链里放两个 forward 且都用exec无条件执行。我最初想让"主组失败自动走备用组",写了两行无条件exec,结果第一个 forward 拿到响应就终止了链,第二个永远不会执行;如果第一个超时,第二个才开始,体感上就是"要么快要么等两倍超时"。正确做法是用matches加has_resp之类的条件判断,或者干脆把两个地址放进同一个 forward 的upstreams列表里用并发解决。后者简单得多,我现在就是后者。

第二条:规则文件要能一键更新,但不要自动更新。广告拦截列表这类文件更新频率高,我用一个脚本手动触发更新,更新前先备份旧文件。为什么不自动?因为某次上游列表更新引入了一条过宽的规则,把某个常用域名干掉了,自动更新的话你连"什么时候变的"都不知道。手动更新加备份,出问题 30 秒回滚。

第三条:给配置文件上版本管理。用 Git 管理/opt/mosdns目录,每次改配置提交一次,写清楚改了什么、为什么改。这条听起来小题大做,但当你半年后想不起"为什么这行规则在这儿"的时候,git log里的提交信息能救你。我配置里有一条奇怪的例外规则,翻提交记录才想起来是当时某台设备的固件有个解析怪癖。

第四条:DNS 服务本身的可用性要做兜底。万一 mosdns 挂了,全网解析就断了。我的兜底是路由器 DHCP 下发的第二个 DNS 地址指向一个公共解析服务。这样主服务挂掉时,设备会在一段时间后自动切到备用,虽然失去了缓存和拦截的收益,但至少不断网。这个细节在稳定的环境里感受不到价值,出一次故障你就知道有多重要。

第五条:别在高峰期改配置。这条听着像废话,但我真干过——晚上八点全家在看视频的时候改配置,重载失败导致全网断了几分钟,被投诉了一整晚。现在的规矩是:改配置先在备用端口起一份验证,验证通过再动正在跑的,动之前跟家里打个招呼。

最后分享一个我在长期维护中形成的小习惯:配置里每一条规则后面都写一行注释,说明"这条是为了解决什么"。不是为了别人看,是为了三个月后的自己。DNS 配置最麻烦的地方不是写,是半年后回头改的时候,完全不记得某条奇怪规则的存在理由。注释写得越直白越好,比如"某品牌电视固件会硬编码查询这个域名,必须走公网上游,否则开机卡 30 秒"——这种信息,除了你自己,没有第二个人能补上。