装完 AdGuard Home 那阵子我挺得意:53 端口全接管,路由器上看什么都干净。但过了一两个月,我开始有点疑心——手机上那些开屏广告,好像一条都没少。
原因不难想。过滤只对"从 AdGuard Home 走的查询"生效,而现实里一大半设备和 App 压根不理你下发的 DNS:有的自己写死一个 8.8.8.8,有的系统层开了 DoT/DoH,加密着就出去了。路由器在中间干看着,一点办法没有。
麻烦的地方在于你根本不知道是谁在绕,所以我没急着拦,先干了个笨办法:挂条规则只记账、什么都不拦,让它自己跑,大半天之后数据出来——家里有 4 台设备在绕,其中一台还是"绕了也白绕",它的 DNS 指着一个早就删掉的旧网段。
这篇记录完整过程:怎么把绕的人揪出来、为什么不能用 OpenWrt 自带那个「DNS 重定向」开关、规则怎么写、以及怎么证明它真的生效了(这一步最反直觉,我差点自己骗自己)。后来我把这套东西做成了 LuCI 应用,开关都搬进界面了。文末有 4 个坑,前三个都属于"配置看着生效了其实没生效"那种。如果你也在用 iStoreOS / OpenWrt 当家做主路由,可以直接抄。
先说结论
- 先观察,别急着拦。直接拦会把"本来就解析失败"的设备一起掩盖掉。我这一步救回了一台手机。
- OpenWrt 自带的「DNS 重定向」开关,在我这儿是禁区。它重定向的目标是 dnsmasq 自己的端口。我这台机器 53 已经归 AdGuard Home、dnsmasq 退到 5353 了,打开那个开关等于让全屋绕开 AdGuard Home,广告过滤集体失效。
- 正确做法是在 nftables 层做透明重定向。把发往外部
:53的 TCP/UDP 查询改道到本机。客户端毫无感觉,硬编码 DNS 的设备一个设置都不用改。 - 真正难的不是写规则,是"证明它生效了"。透明重定向会让客户端回显依旧显示"服务器:8.8.8.8"——照着客户端看,你永远测不出它有没有在工作。
- 改 DNS 得给自己留后路。改错了就是全屋上不了网,而且你连路由器都进不去(管理界面也得靠域名解析)。所以我加了三道保险,后面单独说。
第一步:先挂个只记账的规则,观察了大半天
观察模式特别简单:挂一条只计数、只记日志、绝不拦截的规则,相当于给防火墙装了个"谁在看外面 DNS"的计数器,别的什么都不干。
## /etc/nftables.d/20-dns-guard.nft —— 观察模式(policy accept,只记录)
chain dns_guard_pre_forward {
type filter hook forward priority -1; policy accept;
iifname "br-lan" oifname "pppoe-wan" meta l4proto { tcp, udp } \
th dport { 53, 853 } counter log prefix "DNS-BYPASS-53853: "
iifname "br-lan" oifname "pppoe-wan" tcp dport 443 \
ip daddr @doh_servers counter log prefix "DNS-BYPASS-DOH: "
}
有三个细节我特意确认过,都属于"不确认就会看错数"的类型:
- 只管"LAN → 外网"这一个方向(
iifname br-lan+oifname pppoe-wan),不然路由器自己向上游发的正常查询也会被一起抓进来,数字直接翻倍。 - 53 和 853 一起看。853 是 DoT,看不见内容,但能看见"有人在用"。
- DoH 只能按 IP 猜。443 是加密的,判断不了内容,只能自己维护一份"已知 DoH 服务商地址"的集合(
doh_servers)。所以这一块天生比前两个糙。
另外我还顺手抓了一份包(tcpdump 过滤 br-lan 上非内网目标的 53/853,以及发往 DoH 地址的 443)。这一步后来证明是"认人"的关键——日志只能告诉你"192.168.100.198 查了 120.76.196.169",抓包才能告诉你"它查的是哪个域名"。
观察结果:4 台设备在绕
6 小时 34 分钟(11:32 → 18:19),两个计数器一共记下 4932 条,按端口拆开是:明文 53 有 1537 条、DoT 853 有 2162 条、DoH 有 1233 条。按设备归完类,全集中在 4 台:
| 设备 | 量级 | 绕过方式 | 主要目标 | 推断用途 |
|---|---|---|---|---|
| 192.168.100.201 我的电脑 | 3743 (76%) | 853 + DoH + 53 三管齐下 | 223.5.5.5:443、1.1.1.1:853、1.12.12.12:853、120.53.53.53:853、2400:3200::1:853 | 代理客户端(mihomo 内核)加 DNS 测速优选;dot.pub、dns.cloudflare.com 的 SNI 都对得上 |
| 192.168.100.32 nas-vbr-lan1 | 568 (11%) | 53 写死 + DoH | 8.8.8.8:53、8.8.4.4:53、223.5.5.5:443 | NAS 上跑着一个完整 Firefox;另外有「节点小宝」的域名 |
| 192.168.100.198 DBR-W10 | 403 (8%) | 明文 53 | 120.76.196.169:53、8.8.8.8:53 | 大量 ANY? 查询加字节跳动系域名 |
| 192.168.100.123 REDMI-K80 | 86 | 明文 53 | 192.168.5.1、192.168.5.12 | 指向一个已经不存在的旧网段 —— 查了也白查 |
剩下几台只有个位数到几十次,属于偶发或探活:Xiaomi-17(68 次,114.114.114.114:853 只有 SYN、对端不应答)、打印机(7 次)、三台身份没认出来的设备(各 1~4 次)。没有别的系统性绕过。
但光看 IP 和次数是认不出"是谁"的。抓包把域名带出来之后,故事才算完整:

- 我的电脑查的是
appav1.llguangli25n.com(74 次)、dot.pub(66 次)、appav1.apiv2.a047.com(63 次)。前一个是机场域名,后一个是 DNSPod 的 DoT 域名——代理客户端加 DNS 优选,实锤。 - NAS 查的是
firefox.settings.services.mozilla.com(182 次)、push.services.mozilla.com(154 次)、c.iepose.com(49 次)。一个完整的 Firefox 实例,外加「节点小宝」。 - DBR-W10 这个最怪:133 次
ANY?查询,连域名都没有,只有类型ANY。典型的字节系 SDK 行为。 - 红米 K80 查的是微信域名(
res.wx.qq.com、mmbiz.qpic.cn、wximg.wxs.qq.com),但全都发往192.168.5.1。
这一步救了一台手机
红米 K80 的 DNS 被静态设成了 192.168.5.1 和 192.168.5.12,大概是以前接别的网络时填的,而这个网段在我现在的网络里根本不存在。结果就是它那 86 次查询全都有去无回,一次都没成功过。
如果我第一步就上 REJECT,这台手机的表现会是"DNS 被拦了",看着就像我的规则干的。而它其实本来就在解析失败——我的规则只是让它失败得更彻底一点,然后永远查不出来。
反过来,透明重定向正好能治它:把发往 192.168.5.1 的查询也一并改道到 AdGuard Home 就行,手机一个设置都不用动。不过这也带出了下面那条很坑的规则。
OpenWrt 自带的「DNS 重定向」开关,为什么在我这儿是禁区
LuCI 的 DHCP/DNS 页面上有个「DNS 重定向」开关,看名字正是我要的功能。但我先去瞄了一眼它的实现(/etc/init.d/dnsmasq):
config_get dns_port "$cfg" port 53
nft add table inet dnsmasq
nft add chain inet dnsmasq prerouting "{ type nat hook prerouting priority -95; policy accept; }"
nft add rule inet dnsmasq prerouting "meta nfproto { ipv4, ipv6 } udp dport 53 counter redirect to :$dns_port"
问题出在 $dns_port 上——它取的是 dnsmasq 自己的监听端口。标准 OpenWrt 里的 53 归 dnsmasq,所以这个开关没毛病;但我这台机器上 53 已经交给 AdGuard Home,dnsmasq 的端口已经改成 5353,只管 DHCP 和 LAN 侧本地域名。于是打开这个开关的后果是:
| 缺陷 | 后果 |
|---|---|
| 重定向目标是 5353(dnsmasq) | 全屋绕开 AdGuard Home,广告过滤、恶意域名拦截一起失效 |
规则只匹配 udp dport 53 | TCP 53 完全没覆盖(现在不少客户端会走 TCP) |
没有任何 daddr 条件 | 连"发给内网某台 DNS 服务器"的查询也会被劫持 |
结论很清楚:在我这套架构下这个开关碰不得。它做的事情方向是对的(透明重定向),只是目标端口和覆盖面都不对,只能自己写。
自己写:让 53 查询无声无息地改道
规则本身很短,就两条(IPv4 加 IPv6):
chain dns_guard_redirect {
type nat hook prerouting priority -105; policy accept;
iifname "br-lan" meta l4proto { tcp, udp } th dport 53 \
ip daddr != @dns_never_hijack4 counter redirect to :53
iifname "br-lan" meta l4proto { tcp, udp } th dport 53 \
ip6 daddr != @dns_never_hijack6 counter redirect to :53
}
redirect to :53(只给端口不给地址)就是改道到本机 :53。目标端口写死 53,也就是 AdGuard Home——不是 dnsmasq 的 5353。这是它和自带开关最本质的区别。
为什么优先级是 -105
fw4 自己的 DNAT 链叫 dstnat,挂在 priority dstnat 上(数值等于 -100)。我要抢在它前面,所以写 -105。装完从设备上看是这样:
chain dns_guard_redirect {
type nat hook prerouting priority dstnat - 5; policy accept; # dstnat - 5 就是 -105
...
}
顺带记一个容易踩的细节:/etc/nftables.d/ 里的文件是被 include 进 table inet fw4 内部的(设备上那份 README 写得很清楚)。所以文件里只写 set 和 chain,千万别再包一层 table { },包了直接语法错误。
两个容易反着来的地方
- 排除段里不要放
192.168.0.0/16。看着挺合理("内网地址不该劫持"),但红米 K80 指的192.168.5.1正好落在这一段里,一排除它就会继续逃过重定向、继续打空。我的排除列表是故意只放自己真实的网段:192.168.100.0/24、172.16.0.0/12,加回环、链路本地和组播。 - 排除的目标要精确到"我这边的网段",而不是"所有私有网段"。后者看着更安全,实际是把真正需要救的设备留在了外面。
怎么证明它真的生效了(这里最容易骗自己)
规则写完、防火墙重载,一切看起来都正常,但有个很反直觉的坑我必须单独讲:
透明重定向是在 nat 层改道的,客户端看到的应答者永远是它自己填的那个地址。从电脑上执行 nslookup baidu.com 8.8.8.8,回显还是:
服务器: dns.google
Address: 8.8.8.8
哪怕它已经被改道到 AdGuard Home 了。照客户端去验证,你永远测不出它有没有工作——这跟上篇文章里那个"配置看着生效了其实没生效"是同一类事。能看的是路由器侧的两样东西:
- nat 链的 counter 涨没涨。改道发生在转发之前,计数会实打实往上走。
- AdGuard Home 的查询日志里有没有出现这条查询、有没有这台客户端。这条最硬——说明查询真的被 AdGuard Home 拿去解析了。
我的做法是先建基线再做对照:上线前从电脑查一次 8.8.8.8,AdGuard Home 日志命中 0;上线后同样再查一次,计数从 0 涨到 3,AdGuard Home 日志里出现了这条查询,从 upstream 字段还能看出实际是 dns.alidns.com 的 DoH 在解析它。

一个提前知道能省事的口径变化
重定向上线之后,前面那个观察计数会立刻接近归零。原因很简单:53 的流量在 prerouting 就被截走了,根本走不到 forward 链。我这边观察链的两条规则里,53/853 那条变成 0,只剩 DoH 那条在涨。
这不是坏消息,是功能正常的标志。但得记住:以后想知道"谁还在绕",要看的是重定向链的计数(被收编了多少)和 AdGuard Home 的客户端列表,原来那个 bypass 计数不管用了。
顺手把它做成了 LuCI 应用
命令行能跑之后老问题又来了:改个参数还得 SSH 上去 uci set 加 fw4 reload。上篇文章我也是这么想的,然后就写了个界面——LuCI 应用不用 SDK、不用编译,本质就是几个文本文件丢进对应目录。
这次的应用叫 luci-app-dnsguard,入口在服务 → DNS 防绕过。上半部分是状态表,一眼看出重定向到底生效没有;下半部分是开关和参数。


状态表里有一行是我特意加的:「53 透明重定向」。它会直接告诉你「已生效」,还是「配置为开启,但链不存在」。后者是我特意加上的——配置文件写对了不等于防火墙里真有这条链,这两种状态必须能分开。
四个按钮的分工:
| 按钮 | 干什么 | 什么时候用 |
|---|---|---|
| 保存并应用 | 写 UCI → 渲染规则 → 校验 → 重载防火墙 | 改完任何字段 |
| 按当前配置重新应用 | 不读表单,直接用现有配置重跑一遍 | 规则文件被别的东西改动了 |
| 健康自检 | 10 项只读检查,出表格 | 感觉 DNS 不对时的第一件事 |
| 预览将要生成的规则 | 只渲染到临时文件,不碰防火墙 | 想确认改动会写成什么样 |
另外「853 阻断」和「DoH 阻断」两个开关我也做进去了,但目前都关着(原因在最后一节)。
改 DNS 之前,我先给自己上了三道保险
这一节我觉得比规则本身更值得写。改 DNS 有个很讨厌的性质:改错了是全屋上不了网,而且你连路由器都进不去——因为连管理界面都得靠域名解析,只能靠记 IP 或者去插键盘。
第一道:三层校验,任一层不过就中止并还原
渲染出规则片段
→ 包进临时表名做 nft -c 语法预检 ← 语法错在这里就中止,/etc 一字未动
→ 落盘后跑 fw4 check(全量校验) ← 失败立即还原旧文件
→ fw4 reload
→ 核对目标链是否真的存在 ← 不存在则还原 + 重载,并报错
顺便说一个我原来不知道的事:设备上其实有个 fw4 check 命令,能把完整规则集校验一遍而不加载。这比只校验自己那段片段权威得多。另外 nft -f 是原子事务,所以重载失败不会留下半套规则。
第二道:依赖的服务没跑,就拒绝启用
重定向规则和 AdGuard Home 是"同生共死"的关系:AdGuard Home 挂了而重定向还在,等于把所有设备的 DNS 都指到一个死端口上。所以应用里加了个闸门——发现 AdGuard Home 进程不在,直接拒绝应用重定向,一个字都不写:
# AdGuard Home 未运行时的返回
{"status":"error","message":"AdGuard Home 未运行(pidof 为空)—— 已拒绝应用 53 透明重定向:
此时重定向会让全屋解析失败。请先启动 AdGuard Home。"}
我拿一个假的 pidof 顶掉真命令验证过:返回 error、退出码 3,而且规则文件的 md5 和改前逐字节相同。
第三道:延迟自动回滚——改完人走了,它也能自己退回去
这是我私心最喜欢的一道。当「53 重定向」从关改到开的那一刻,应用会自动做三件事:
- 把当前的 UCI 配置和规则文件一起拍成回滚点(必须在写配置之前拍——顺序反了的话快照里存的就是新配置,回滚等于没回滚);
- 起一个倒计时,默认 10 分钟;
- 到点如果没人点「确认保留」,就把 UCI 和规则文件一起还原,并重载防火墙。
我拿 1 分钟实测跑了一遍完整的生命周期:

回滚的判定逻辑也挺有意思:倒计时进程不靠"把自己杀干净"来保证正确性,而是以状态文件为唯一真相。点「确认保留」时把那个文件删掉,倒计时进程醒来一看记录没了就什么都不做。这样哪怕进程没杀干净,也不会出现"我明明确认了它却回滚了"。
我踩的 4 个坑
四个坑,前三个都属于"东西坏了但看起来好好的":
| # | 现象 | 原因与修法 |
|---|---|---|
| 1 | 配置里 5 个地址,生成的规则里只有 4 个。少的永远是每组的最后一个——224.0.0.0/4、ff00::/8、140.207.198.6 全没了 | printf '%s' 没补结尾换行,while read 循环把最后一行吞了。改成 printf '%s\n'。这个 bug 极具迷惑性:前面的全对,只少最后一项 |
| 2 | 每点一次「保存并应用」,防火墙就重载一次 | 生成的文件头有「生成时间」行,每次都不同,幂等比较永远判成"有变化"。修法是比较前先用 sed 剔除该行 |
| 3 | CIDR 被写坏:192.168.100.0/24 变成 192.168.100.024 | 控制器里做字符白名单清洗时漏了 /。这个是靠"打桩直调控制器、比对改前改后的 uci show"抓出来的;而且只因为渲染层还有一道 nft -c 校验,坏值才没写进防火墙 |
| 4 | 测试脚本报 ash: syntax error: unexpected "(" | 在(被封装过的)shell 里写 PATH=/x:$PATH cmd,会被改写成带 $( ) 的形式。改用 /usr/bin/env PATH=... cmd 就稳了 |
第 3 个坑我印象最深。它最后没造成任何损失,纯粹是因为还有第二道防线兜住了。所以我的体会是:校验层至少得有两层(界面一道、落地一道),单层迟早会被绕过——而它被绕过的那一次,往往就是你完全没注意到的那次。
现在的效果,和两件我故意没做的事
上线之后有个挺反直觉的地方:你什么都感觉不到。没有设备报错、没有 App 重连、没有任何提示——这正是透明重定向该有的样子。能看到的只有两处:路由器上重定向链的计数慢慢往上涨,以及 AdGuard Home 的查询日志里开始冒出原本不会出现的客户端。
有两件事我是故意没做的:
- 853(DoT)阻断先放着。DoT 没法透明重定向,只能拦。拦掉之后客户端通常会回落到明文 53、进而被重定向收编——但"通常"不等于"一定"。全网唯一的真实用户就是我这台电脑,而它还同时配着明文 53 上游,大概率能回落;可既然是自己的开发机,我打算单独确认一遍再开。
- DoH 的 IP 层阻断不做。按 IP 命中太糙了:
223.5.5.5:443只是"恰好也在 DoH 服务商地址列表里",拦掉就容易误伤跑在同一个 IP 上的其他服务。更精准的做法是在 AdGuard Home 里加域名规则(像||doh.pub^这种),可审计、可回滚。
所以现在的状态是:53 已经收编,853 和 DoH 的开关留在界面上等观察,各自可以单独开关。
常见问题
重定向会不会让网络变慢?
不会。改道发生在 nat 层,客户端到本机 AdGuard Home 比到 8.8.8.8 还近一点。而且这条规则只匹配"发往外部 :53 的查询",正常上网流量压根不经过它。观察期本身也是零影响——只计数不拦截,跑了 6 个多小时没任何设备出现重试或超时。
如果 AdGuard Home 挂了会怎样?
这才是真正要防的。手动保存时应用会直接拒绝启用;但如果 AdGuard Home 是在启用之后挂的,规则还在防火墙里躺着。这时全屋解析会一起失败——注意,没有重定向它们也一样会失败,因为 53 本来就归 AdGuard Home。所以正确的做法是 /etc/init.d/dnsguard stop 把重定向链临时摘掉(配置不动),然后去救 ADGuard Home。
能不能只对部分设备生效?
可以,界面里有「例外设备」一栏,填进去的 IP 不做重定向(IPv4/IPv6 混填),「永不劫持」集合也能改。但我的建议是尽量别加白名单——透明重定向本来就是为了消灭"某些设备需要特殊照顾"这件事。反正这次我一个例外都没加。
小结
回头看,这套东西真正的价值不在那两条 nftables 规则上——它们加起来不到十行。
一个是"先观察再动手"。如果六个多小时前我直接上拦,红米 K80 那台手机的问题会被永久掩盖;现在反倒是重定向顺手把它治好了。
另一个是"把改网络当成危险作业来做"。多层校验、依赖闸门、延迟自动回滚,这三样加起来大概是规则本身的十倍代码量。但改 DNS 失败的代价是全屋断网外加进不去管理界面,这个保险付得值。
最后一点体会:透明重定向这类东西,"看起来生效了"是最没有信息量的一句话。客户端回显不会变、设备不会报错、App 不会重连。你必须去路由器上看计数、去 DNS 服务日志里找那条查询——不然就只是在猜。
如果你也在折腾家用软路由的 DNS,建议从"先挂条只记账的规则"开始。那种"原来家里有这么多设备在自己找 DNS"的发现,比直接拦下来有意思多了。

Comments NOTHING