OpenWrt + MosDNS + AdGuard Home 全网去广告搭建教程

AI 摘要大番茄AI

在 OpenWrt 旁路由上,用 MosDNS 与 AdGuard Home 构建双层 DNS 广告拦截,通过 nft 劫持 53 端口实现全屋无感去广告;配置持久化并配守护脚本,附三套面板、验收清单与回滚手册,命令真机验证可直接复制执行。

此内容由AI根据文章内容自动生成。

从零开始,把全屋设备的 DNS 广告拦截做到路由器层。本文聚焦路由器层的拦截;如果你想进一步了解加密 DNS 上游,可以先看 国内外常用 DoH/DoT 服务汇总。每一步都基于一台真实运行中的生产环境(OpenWrt 旁路由 192.168.11.253)逐步还原,所有命令均在该真机上验证过,可直接复制执行。

适用环境:OpenWrt 24.10 x86_64 | MosDNS 5.3.4(luci-app 1.7.14) | AdGuard Home v0.107.79 | 旁路由模式 | 2026-09-29 整理

〇 · 教程说明与最终效果

先搞清楚要搭什么、搭完什么样,再动手。

你将得到什么

  • 全屋无感去广告:手机、电脑、电视、App 内广告,只要走 DNS 解析就能拦,设备端无需装任何东西;
  • 双层拦截:AdGuard Home 用 55 万条规则库过滤常见广告域名,MosDNS 再用黑名单”点名”拦特定广告 SDK(如穿山甲/CSJ);
  • 三套观测面板:AGH 看拦截明细,MosDNS 看聚合统计,浏览器直接访问即可;
  • 不怕断电重启:劫持规则、统计、过滤配置全部持久化,配守护脚本自动自愈。

搭成之后的 DNS 链路(本教程最终形态)

局域网设备手机 / PC / TV无需任何设置
→
nft 劫持 :53OpenWrt 253优先级 -115 抢先截胡
→
AdGuard Home :555355 万条规则过滤广告域名在此被拦
→
MosDNS :5335黑名单 + 3 天缓存再拦一层、加速
→
上游分流国内直连 / 国外走代理

注意顺序:AdGuard Home 先拦,MosDNS 后解析。AGH 是”门卫”,MosDNS 是”查询部”。不是反过来。

三个面板的分工

面板 地址 看什么
AdGuard Home 面板 http://192.168.11.253:3000 每条 DNS 查询明细、哪个设备查了什么、拦了哪条规则、拦截率
MosDNS 统计 API http://192.168.11.253:9091 总查询量、缓存命中率、Top 榜单(聚合数据,看不到客户端 IP)
LuCI 管理页 http://192.168.11.253(或 https,见第 9 步) 服务启停、uci 配置、系统状态
约定:下文所有命令除非特别标注,均在 SSH 登录路由器后执行:ssh root@192.168.11.253。界面路径以 LuCI 中文界面为准。不想每次输密码的话,配一把密钥:SSH 密钥登录新手教程。

一 · 前置条件与网络架构

本教程的拓扑是”主路由 + 旁路由”模式,先确认你的环境一致,命令才能对号入座。

设备 IP 角色
iKuai 主路由 192.168.11.1 PPPoE 拨号、DHCP 下发、IPv6 前缀下发,不参与 DNS 过滤
OpenWrt 24.10(PVE 虚拟机) 192.168.11.253 旁路由:DNS 劫持 + 广告过滤 + 国内外分流,本教程全部操作都在它上面(虚拟机还没搭好的话,可参考 物理机安装 PVE 9.1 图文攻略 与 PVE 安装 OpenWrt 虚拟机)
局域网设备 DHCP 自动获取 把 DNS 指向 253(或在主路由 DHCP 里把 DNS 改成 253)

旁路由劫持的工作前提(重要)

nft 的 53 端口劫持规则作用在 253 这台机器的 prerouting 链上,它只能截获流经 253 的 DNS 查询。所以:

  • 客户端 DNS 填 192.168.11.253 → 查询流经 253 → 会被劫持 ✅
  • 客户端把 253 设为网关(旁路由模式)→ 所有流量包括 DNS 都流经 253 → 会被劫持 ✅
  • 客户端 DNS 填 192.168.11.1 或公网 DNS 且网关不走 253 → 流量根本不经过 253 → 拦不到 ❌
最省心的做法:在主路由 DHCP 设置里,把下发的 DNS 服务器改成 192.168.11.253。这样全屋新接入的设备自动被覆盖。

端口规划(全程要用,先记下)

端口 占用者 用途
53 dnsmasq(OpenWrt 自带) 名义监听,实际入站查询被 nft 重定向走
5553 AdGuard Home DNS 过滤入口(避开 53,防止与 dnsmasq 冲突)
5335 MosDNS DNS 解析 + 缓存 + 分流(sbwml 版 luci-app-mosdns 默认端口)
3000 AdGuard Home 网页管理面板
9091 MosDNS 统计 API / 面板数据
80 / 443 uhttpd LuCI 管理页

第 1 步 · 确认 MosDNS 就绪

MosDNS 是整条链路的”解析引擎”,先让它稳定跑起来,AGH 才有上游可指。

1.1 安装

推荐直接在 iStore 应用商店搜 luci-app-mosdns 安装(sbwml 维护版);或命令行:

253 路由器
opkg update
opkg install luci-app-mosdns luci-i18n-mosdns-zh-cn

1.2 关键配置(LuCI → 服务 → MosDNS)

本教程真机使用的 uci 配置如下(uci show mosdns 可查看),重点几项:

uci 项 值 含义
listen_address / listen_port 0.0.0.0 / 5335 MosDNS 监听 5335,不直接占 53
listen_port_api 9091 统计 API 端口
cache / cache_size / lazy_cache_ttl 1 / 100000 / 259200 开缓存,10 万条上限,3 天乐观缓存(过期也能先答再刷新)
dump_file / dump_interval 1 / 300 缓存落盘,每 300 秒写一次,重启不丢缓存
stats_collector / stats_capacity 1 / 2000 开统计采集
stats_dump_file / stats_dump_interval 1 / 120 统计落盘,每 120 秒写一次,重启不清零

改完点”保存并应用”,或命令行:

253 路由器
uci set mosdns.config.cache='1'
uci set mosdns.config.lazy_cache_ttl='259200'
uci set mosdns.config.stats_dump_file='1'
uci set mosdns.config.stats_dump_interval='120'
uci commit mosdns
/etc/init.d/mosdns restart

1.3 53 端口劫持补丁(关键)

sbwml 版 luci-app-mosdns 自带”53 劫持”:通过 nft 表把入站 53 端口的查询重定向到自己的 5335。但默认实现有两个问题,需要打补丁:

  • nft 钩子优先级默认 -95,排在 passwall(-1)之后没问题,但为了给后续 AGH 劫持(-115)让位、且防止升级 luci-app 后被覆盖回 -95,固定改成 -110;
  • 默认只劫持 UDP 53,漏掉 TCP 53(部分客户端在响应过大时会切 TCP),补成 TCP+UDP 双协议。

写成自动补丁脚本,升级 luci-app 后如果补丁被覆盖会自动恢复:

253 路由器 · 创建 /usr/bin/mosdns-patch.sh
cat > /usr/bin/mosdns-patch.sh <<'EOF'
#!/bin/sh
# luci-app-mosdns 劫持补丁自动恢复:优先级 -110 + TCP/UDP 双协议劫持
INIT=/etc/init.d/mosdns
changed=0

# 补丁 1:nft 钩子优先级排在 passwall 之前
grep -q 'priority -110' "$INIT" || {
    sed -i 's/priority -95/priority -110/' "$INIT" && changed=1
}

# 补丁 2:劫持规则同时匹配 TCP 和 UDP
grep -q 'meta l4proto { tcp, udp } th dport 53' "$INIT" || {
    sed -i 's/meta nfproto { ipv4, ipv6 } udp dport 53 counter redirect/meta nfproto { ipv4, ipv6 } meta l4proto { tcp, udp } th dport 53 counter redirect/' "$INIT" && changed=1
}

if [ "$changed" -eq 1 ]; then
    logger -t mosdns-patch "检测到补丁丢失,已自动恢复并重启 mosdns"
    /etc/init.d/mosdns restart
fi
EOF
chmod +x /usr/bin/mosdns-patch.sh
/usr/bin/mosdns-patch.sh

1.4 验证

253 路由器
# 端口在监听
netstat -lnp | grep -E '5335|9091'
# 期望两行:mosdns ... :::5335 和 mosdns ... 127.0.0.1:9091

# 劫持规则已带 -110 和 tcp,udp
nft list chain inet fw4 dstnat 2>/dev/null | grep -n 'dport 53' || nft list ruleset | grep 'dport 53'

# 真实解析测试
nslookup www.baidu.com 192.168.11.253
通过标准:5335/9091 都在监听;劫持规则含 meta l4proto { tcp, udp } th dport 53 且优先级 -110;nslookup 返回百度真实 IP。

第 2 步 · MosDNS 黑名单拦截广告域名

在装 AGH 之前,先用 MosDNS 自带的黑名单管道,精准”点名”拦截广告 SDK。

2.1 工作原理

sbwml 版 MosDNS 的查询管道里有一段 query_is_reject_domain:

  • 先查 白名单 $whitelist(文件 /etc/mosdns/rule/whitelist.txt)→ 命中就直接放行解析;
  • 再查 黑名单 $blocklist(文件 /etc/mosdns/rule/blocklist.txt)→ 命中就返回 black_hole,即 0.0.0.0 / ::。

白名单优先于黑名单——所以万一误伤,往白名单里加一行就能放行。

不要写错文件:另一个拦截源 $adlist 指向 /var/mosdns/disable-ads.txt,它在 tmpfs 上,重启就清空,且可能被插件更新覆盖。自定义拦截一律写 /etc/mosdns/rule/blocklist.txt(持久盘)。

2.2 添加拦截:穿山甲(字节系 Pangle/CSJ)广告 SDK

先备份,再整块追加:

253 路由器
cp /etc/mosdns/rule/blocklist.txt /etc/mosdns/rule/blocklist.txt.bak.$(date +%Y%m%d)

cat >> /etc/mosdns/rule/blocklist.txt <<'EOF'
# Pangle/CSJ adblock 2026-09-28
domain:pangle.io
domain:pangolin-sdk-toutiao.com
domain:csjbi.com
domain:ad.oceanengine.com
domain:analytics.oceanengine.com
domain:lad.oceanengine.com
domain:pglstatp-toutiao.com
domain:dsp.toutiao.com
domain:isplusurl.com
EOF

/etc/init.d/mosdns restart

条目格式说明:domain:xxx.com 表示后缀匹配,会同时拦住 xxx.com 本身和所有 *.xxx.com 子域。注释行用 # 开头。

2.3 添加拦截:虎牙直播埋点域名(含功能域甄别)

拦直播站广告要先分清”广告/埋点域”和”功能域”。虎牙甄别结果:

域名 判定 说明
e-ad-monitor.huya.com 🚫 拦 广告监测上报
e-stat.huya.com 🚫 拦 行为统计
metric.huya.com 🚫 拦 指标埋点
ylog.huya.com 🚫 拦 日志上报
udblog.huya.com 🚫 拦 用户行为日志(注意 ≠ udblgn 登录域)
statwup.huya.com / stat.wup.huya.com 🚫 拦 统计上报
l.web.huya.com 🚫 拦 页面埋点
df.huya.com 🚫 拦 数据上报
*.va.huya.com / ext.huya.com ✅ 保留 直播拉流、CDN,拦了没画面
api / wsapi.huya.com ✅ 保留 接口,拦了页面空白
udblgn.huya.com ✅ 保留 登录(和 udblog 只差两个字母!)
253 路由器
cat >> /etc/mosdns/rule/blocklist.txt <<'EOF'
# Huya ad/tracking block 2026-09-28
domain:e-ad-monitor.huya.com
domain:e-stat.huya.com
domain:metric.huya.com
domain:ylog.huya.com
domain:udblog.huya.com
domain:statwup.huya.com
domain:stat.wup.huya.com
domain:l.web.huya.com
domain:df.huya.com
EOF

/etc/init.d/mosdns restart

2.4 验证

253 路由器 或 局域网内任意 PC
nslookup ad.oceanengine.com 192.168.11.253
# 期望:Address 返回 0.0.0.0

nslookup e-ad-monitor.huya.com 192.168.11.253
# 期望:0.0.0.0

nslookup www.huya.com 192.168.11.253
# 期望:正常 IP(功能域没被误伤)

nslookup udblgn.huya.com 192.168.11.253
# 期望:正常 IP(登录域没被误伤)

2.5 误伤放行与回滚

253 路由器
# 某域名被误拦:往白名单加一行(白名单优先于黑名单,立即生效)
echo 'domain:被误拦的域名.com' >> /etc/mosdns/rule/whitelist.txt
/etc/init.d/mosdns restart

# 整体回滚:用备份覆盖
cp /etc/mosdns/rule/blocklist.txt.bak.20260928 /etc/mosdns/rule/blocklist.txt
/etc/init.d/mosdns restart

第 3 步 · 安装并启用 AdGuard Home

AGH 负责用海量规则库过滤广告,是”门卫”层。这一步最大的坑:装完默认是禁用状态。

3.1 安装

253 路由器
opkg update
opkg install luci-app-adguardhome luci-i18n-adguardhome-zh-cn

(iStore 里搜 AdGuard Home 安装也可以。核心二进制 /usr/bin/AdGuardHome,配置 /etc/AdGuardHome.yaml。)

3.2 启用——90% 的人卡在这一步

大坑:OpenWrt 上 AGH 装完 enabled 默认是 0,点”启动”只会在日志里回一句 “service disabled” 然后毫无反应。必须先把 uci 开关打开:
253 路由器
uci set AdGuardHome.AdGuardHome.enabled='1'
uci commit AdGuardHome
/etc/init.d/AdGuardHome enable
/etc/init.d/AdGuardHome start

# 验证进程与端口
pidof AdGuardHome
netstat -lnp | grep 5553
首次启动时,配置文件 /etc/AdGuardHome.yaml 的 schema_version 若是旧值(比如 10),AGH 会自动迁移到新版本(本机迁到了 34),日志里有提示,属正常现象不用管。

3.3 初始化向导

  1. 浏览器打开 http://192.168.11.253:3000;
  2. 按向导点”开始配置”;
  3. DNS 监听接口选 0.0.0.0(所有接口),DNS 端口填 5553——不要填 53,53 留给 dnsmasq,避免冲突;
  4. 面板端口保持 3000;
  5. 设置管理员用户名和密码(密码之后可改,见第 8 步);
  6. 完成,进入仪表盘。

3.4 验证

253 路由器
netstat -lnp | grep -E '5553|3000'
# 期望:AdGuardHome 监听 :::5553 和 :::3000

nslookup www.baidu.com 192.168.11.253:5553
# 直接问 AGH,此时它还没有上游规则,能答即算活着

第 4 步 · AGH 上游指向 MosDNS + 配置规则库

让 AGH 拦完之后把”干净查询”交给 MosDNS 去解析——串联的核心一步。

4.1 改上游

AGH 面板 → 设置 → DNS 设置 →”上游 DNS 服务器”,把默认的公共 DNS 全部删掉,只填一行:

填入 AGH 上游 DNS 设置
127.0.0.1:5335

同时设置:Bootstrap DNS 服务器填 223.5.5.5 和 119.29.29.29;上游模式选”负载均衡”或”并行请求”均可(只有一个上游时无实际区别)。想换更快的 bootstrap 上游可参考 DoH/DoT 公共服务汇总。拉到底点”保存”。

对应的 yaml(/etc/AdGuardHome.yaml)长这样,直接编辑文件的可以对照:

/etc/AdGuardHome.yaml 关键段(真机实际值)
dns:
  bind_hosts:
    - 0.0.0.0
  port: 5553
  anonymize_client_ip: false
  ratelimit: 0
  refuse_any: false
  upstream_dns:
    - 127.0.0.1:5335
  bootstrap_dns:
    - 223.5.5.5
    - 119.29.29.29
  fallback_dns: []
  upstream_mode: load_balance
顺手把面板 → 设置 → DNS 设置里的 DNS 请求的 DDR(自动发现)关掉,对应 yaml 里 handle_ddr: false。后续第 9 步避坑章节会再次提到它。

4.2 挂规则库

AGH 面板 → 过滤器 → DNS 黑名单 → 添加黑名单列表。本机启用了 5 条(按需增减,规则越多拦截越全、内存占用越大):

规则名 URL 用途
AdGuard DNS filter https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt AdGuard 官方 DNS 拦截主库
EasyList https://easylist-downloads.adblockplus.org/easylist.txt 国际网页广告
EasyList China https://easylist-downloads.adblockplus.org/easylistchina.txt 国内网页广告补充
anti-AD https://anti-ad.net/easylist.txt 国内命中率最高的 DNS 拦截库
AdGuard DNS filter(HostlistsRegistry 版) https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt 与第一条同源镜像,二选一即可(都开着约多占 100MB 内存,可关掉一个)

添加后点每条规则旁边的”检查更新”,等状态变绿。全部启用后在仪表盘能看到”过滤规则总数”约 50 万+。

4.3 验证串联

253 路由器
nslookup www.baidu.com 192.168.11.253:5553
# AGH → MosDNS 链路通了,返回真实 IP

# AGH 面板(:3000)的"查询日志"应能看到这条查询,客户端 IP 显示为 127.0.0.1
# (因为查询是本机发的);等第 5 步劫持生效后就能看到真实设备 IP
改完上游若面板报错或解析失败,先检查 MosDNS 是否活着:netstat -lnp | grep 5335。AGH 的上游必须写成 127.0.0.1:5335,写成 localhost:5335 在部分环境会走 IPv6 导致连不上。

第 5 步 · nft 劫持全网 53 端口到 AGH

设备不会主动把 DNS 发给 AGH 的 5553,所以在 253 上用 nft 把所有入站 53 查询”截胡”重定向给 AGH。

5.1 优先级设计(为什么是 -115)

nft 优先级 归属 动作
-115 AGH 劫持表 inet agh_redir(本步创建) 53 → 5553(AGH)
-110 MosDNS 劫持(第 1 步补丁后的默认) 53 → 5335(AGH 挂了时的备胎)
-1 passwall 代理分流

nft 优先级数字越小越先执行:AGH(-115)先吃掉全部 53 流量;只有 AGH 进程挂掉、重定向到 5553 无人应答时,流量才被 MosDNS 的 -110 兜住,DNS 不会全瘫。实测运行中 -110 兜底链几乎为 0 包,符合预期。

5.2 创建劫持规则

253 路由器
nft add table inet agh_redir
nft add chain inet agh_redir prerouting '{ type nat hook prerouting priority -115; policy accept; }'
nft add rule inet agh_redir prerouting 'meta nfproto { ipv4, ipv6 } meta l4proto { tcp, udp } th dport 53 counter redirect to :5553 comment "AGH HIJACK"'

三条分别:建表 → 建链(nat 类型、prerouting 钩子、优先级 -115)→ 加规则(IPv4+IPv6、TCP+UDP、目标端口 53 一律重定向到本机 5553,带计数器便于观测)。

5.3 验证

253 路由器 + 局域网 PC
# 1) 表已存在
nft list table inet agh_redir

# 2) 局域网 PC 上(DNS 填 253)随便查个域名
nslookup www.baidu.com 192.168.11.253

# 3) 回到 253 看计数器在涨
nft list chain inet agh_redir prerouting
# packets 数字应在增长

# 4) AGH 面板(:3000)查询日志里出现刚才的查询,
#    且客户端 IP 显示为发起设备的真实 IP(如 192.168.11.200)
通过标准:第 4 步末尾客户端显示的是 127.0.0.1,现在变成设备真实 IP——说明查询是”设备 → 劫持 → AGH”全程流经 253 的,劫持生效。

第 6 步 · 守护脚本与开机自启

nft 表和进程都可能”消失”(重启、崩溃、插件更新),不设守护的话第二天劫持就静默失效。

6.1 守护脚本(进程保活 + nft 表重建)

253 路由器 · 创建 /usr/bin/agh_redir.sh
cat > /usr/bin/agh_redir.sh <<'EOF'
#!/bin/sh
# AGH 守护:进程存活 + 劫持 nft 表重建
PID=$(pidof AdGuardHome)
if [ -z "$PID" ]; then
    logger -t agh-guard "AdGuardHome 进程丢失,自动重启"
    /etc/init.d/AdGuardHome restart >/dev/null 2>&1
    sleep 5
fi
nft list table inet agh_redir >/dev/null 2>&1 || {
    nft add table inet agh_redir
    nft add chain inet agh_redir prerouting '{ type nat hook prerouting priority -115; }'
    nft add rule inet agh_redir prerouting 'meta nfproto { ipv4, ipv6 } meta l4proto { tcp, udp } th dport 53 counter redirect to :5553 comment "AGH HIJACK"'
    logger -t agh-guard "agh_redir nft 表丢失,已重建"
}
EOF
chmod +x /usr/bin/agh_redir.sh

逻辑:每 5 分钟被 cron 拉起一次,发现 AGH 进程没了就重启它;发现 nft 表没了就原样重建,并把事件写进系统日志(logread | grep agh-guard 可查)。

6.2 挂到 cron(每 5 分钟)

253 路由器
grep -q agh_redir /etc/crontabs/root || echo '*/5 * * * * /usr/bin/agh_redir.sh' >> /etc/crontabs/root
/etc/init.d/cron restart
busybox 冷知识:验证 cron 是否在跑,用 pidof crond(进程名带 d)。pidof cron 会假阴性——明明在跑却查不到,别被它骗了。

6.3 挂到 rc.local(开机先跑一次)

253 路由器
sed -i '/^exit 0/i /usr/bin/agh_redir.sh >/dev/null 2>&1' /etc/rc.local
# 同时确认 mosdns 补丁脚本也在开机链上(第 1 步装过就不用动):
grep mosdns-patch /etc/rc.local || sed -i '/^exit 0/i /usr/bin/mosdns-patch.sh' /etc/rc.local

6.4 实战验证(最可靠的方式:真的重启一次)

253 路由器
reboot
# 等 2~3 分钟重连后:
logread | grep agh-guard
# 期望看到:agh_redir nft 表丢失,已重建

nft list table inet agh_redir
pidof AdGuardHome
nslookup www.baidu.com 127.0.0.1
本教程真机已实测:重启后守护脚本自动重建 nft 表并留下日志,劫持无缝恢复。

第 7 步 · 统计持久化与每周自动清零

让 MosDNS 统计重启不清零、可累计观察,再配一个每周日凌晨自动清零的脚本,避免数据无限膨胀。

7.1 持久化配置(第 1 步已做,这里说明原理)

  • mosdns.config.stats_dump_file='1' + stats_dump_interval='120':统计每 120 秒落盘到 /etc/mosdns/stats.dump,启动时读回;
  • mosdns.config.dump_file='1':DNS 缓存同理落盘(cache.dump)。
想清零不能只删文件:运行中的 MosDNS 会按 120 秒周期把内存里的旧统计写回 dump 文件。正确姿势是”先停进程 → 删 dump → 再启动”。

7.2 每周自动清零脚本

253 路由器 · 创建 /root/stats_weekly_reset.sh
cat > /root/stats_weekly_reset.sh <<'EOF'
#!/bin/sh
# 每周清空一次 MosDNS 统计(累计计数 + 查询日志 + Top 榜单)
# 原理:必须先停进程再删 dump,否则会被 120 秒周期写回。
# 注意:本文件名不要含 "mosdns.uc" —— /etc/init.d/mosdns 的 delcron/setcron
# 每次重启都会 sed -i '/mosdns.uc/d' /etc/crontabs/root,会误删 cron 行。
LOG_TAG="stats-weekly"
DUMP=/etc/mosdns/stats.dump

before=$(wget -q -O - "http://127.0.0.1:9091/plugins/stats_collector/api/v1/stats" 2>/dev/null \
         | sed -n 's/.*"total_queries":\([0-9]*\).*/\1/p')
[ -z "$before" ] && before=0

/etc/init.d/mosdns stop >/dev/null 2>&1
sleep 2
rm -f "$DUMP"
/etc/init.d/mosdns start >/dev/null 2>&1
sleep 3

after=$(wget -q -O - "http://127.0.0.1:9091/plugins/stats_collector/api/v1/stats" 2>/dev/null \
        | sed -n 's/.*"total_queries":\([0-9]*\).*/\1/p')
[ -z "$after" ] && after=0

logger -t "$LOG_TAG" "MosDNS 统计已清空(周清):清空前 total=$before,清空后 total=$after"
echo "$(date '+%Y-%m-%d %H:%M:%S') 清空前=$before 清空后=$after" >> /root/stats_reset.log
EOF
chmod +x /root/stats_weekly_reset.sh

# 每周日凌晨 4 点执行
grep -q stats_weekly /etc/crontabs/root || echo '0 4 * * 0 /root/stats_weekly_reset.sh' >> /etc/crontabs/root
/etc/init.d/cron restart

7.3 手动清零(临时想从今天开始重新观察)

253 路由器
/etc/init.d/mosdns stop; sleep 2
rm -f /etc/mosdns/stats.dump
/etc/init.d/mosdns start
# 验证:curl http://127.0.0.1:9091/plugins/stats_collector/api/v1/stats
# total_queries 应回到 0

7.4 一个必须提前知道的”怪现象”

MosDNS 面板的客户端(top_clients)全是 127.0.0.1,看不到设备 IP?这是本架构的必然结果,不是故障:AGH 劫持串联后,真正到 MosDNS 的查询全部来自本机的 AGH(127.0.0.1)。想看”哪台设备查了什么”,去 AGH 面板 :3000 的查询日志,那里记录的是劫持前的真实来源 IP。

第 8 步 · 修改 AGH 面板密码

AGH v0.107.79 的命令行还没有 --hash-password 参数,用 Python bcrypt 生成新哈希直接写进配置。

8.1 生成 bcrypt 哈希(在本机电脑上执行)

本机 Windows(需已装 Python)
python -m pip install bcrypt
python -c "import bcrypt; print(bcrypt.hashpw('你的新密码'.encode(), bcrypt.gensalt()).decode())"
# 输出形如:$2b$10$xxxxxxxx...
Windows 下若用 venv,python 在 envs\default\Scripts\python.exe(Scripts 目录下),别写成 envs\default\python.exe。

8.2 写入配置(在 253 上执行)

AdGuard Home 识别 $2y$ 前缀的 bcrypt 哈希,Python 生成的是 $2b$,两者算法相同,改个前缀即可:

253 路由器
cp /etc/AdGuardHome.yaml /etc/AdGuardHome.yaml.bak.$(date +%Y%m%d)

# 把下面两个占位符替换成真实值再执行
HASH='$2y$10$把上一步输出的$2b$改成$2y$粘贴到这里'

python3 - "$HASH" <<'PYEOF'
import sys, re
h = sys.argv[1]
p = '/etc/AdGuardHome.yaml'
s = open(p, encoding='utf-8').read()
s = re.sub(r'(^\s*- name: admin\s*\n\s*password: ).*$', r'\g<1>' + h.replace('$','\\$'), s, count=1, flags=re.M)
open(p, 'w', encoding='utf-8').write(s)
print('password updated')
PYEOF

/etc/init.d/AdGuardHome restart

嫌脚本麻烦的话,手动编辑也可以:vi /etc/AdGuardHome.yaml,找到 users: 段里 name: admin 下面的 password: 行,整行替换为新哈希,保存后重启服务。

8.3 验证

本机浏览器 / 253 路由器
curl -s -o /dev/null -w '%{http_code}\n' -u admin:旧密码 http://192.168.11.253:3000/login
# 期望 401

curl -s -o /dev/null -w '%{http_code}\n' -u admin:新密码 http://192.168.11.253:3000/login
# 期望 200
教程和截图里永远不要出现真实密码哈希——哈希虽不可逆,但发到网上等于把口令交给彩虹表撞库。

第 9 步 · 给 LuCI 挂真证书上 HTTPS(可选)

用一张受信任 CA 签发的域名证书(本例为 *.997wan.com 通配符证书)替换 LuCI 自签证书,想走 acme.sh 自动签发而不是手动传证书,另有专文 OpenWrt 配置 SSL 证书开启 HTTPS(内含自签方案见 RouterOS/OpenWrt 自签 SSL 全流程)。浏览器访问管理页不再弹”不安全”。

9.1 证书准备(本机操作)

  • 需要 包含完整证书链的 PEM 文件:叶子证书 → 中间证书 → (根)。CA 下载目录里 nginx/ 文件夹的 full.pem(约 8KB、含 4 张证书)就是现成的完整链;
  • 只有叶子证书会报 x509: certificate signed by unknown authority 类错误,务必用 fullchain;
  • 私钥文件(.key)对应证书的公钥,两者必须配对。
本机 Windows · 上传到 253
scp C:\路径\full.pem root@192.168.11.253:/etc/certs/997wan-full.pem
scp C:\路径\private.key root@192.168.11.253:/etc/certs/997wan.key

9.2 253 上设置权限并配置 uhttpd

253 路由器
chmod 600 /etc/certs/997wan.key

# 备份原配置
uci show uhttpd.main > /root/uhttpd.bak.$(date +%Y%m%d)

# 监听 443(IPv4 + IPv6),挂新证书
uci set uhttpd.main.listen_https='0.0.0.0:443'
uci add_list uhttpd.main.listen_https='[::]:443'
uci set uhttpd.main.cert='/etc/certs/997wan-full.pem'
uci set uhttpd.main.key='/etc/certs/997wan.key'
uci commit uhttpd
/etc/init.d/uhttpd restart
原自签证书 /etc/uhttpd.crt / /etc/uhttpd.key 不删不覆盖,回滚时把 cert/key 改回去即可。

9.3 验证(两条路)

本机
# 命令行验证:把域名解析指到 253,访问时不带 -k(-k 会掩盖证书问题)
curl --resolve a.997wan.com:443:192.168.11.253 https://a.997wan.com/ -o /dev/null -w '%{http_code}\n'
# 期望:200(无证书告警输出)

# 浏览器验证:本机 hosts 加一行
#   192.168.11.253  a.997wan.com
# 然后访问 https://a.997wan.com/ → 地址栏直接是锁图标,零警告

回滚:

253 路由器
uci set uhttpd.main.cert='/etc/uhttpd.crt'
uci set uhttpd.main.key='/etc/uhttpd.key'
uci commit uhttpd
/etc/init.d/uhttpd restart

避坑 · 为什么这张证书不能直接给 AGH 原生 TLS 用

AGH 面板里有”加密设置”可以直接挂证书开 HTTPS/DoT,但本教程的证书挂上去会失败——这是版本硬限制,不是操作错误。

两个报错现场(实测)

报错 原因
x509: certificate signed by unknown authority 喂给 AGH 的 crt 只有叶子证书、缺中间链。解法是用含完整链的 full.pem。
certificates has no IP addresses; DNS-over-TLS won't be advertised via DDR AGH v0.107.79 的硬限制:validateCertificatePair 要求证书 SAN 里必须含 IP 地址(用于 DoT/DDR 自动宣告)。纯域名证书(哪怕是真的、链完整的)直接拒绝加载。

面板里把 “禁用 DDR / handle_ddr” 设为 false 也绕不过这个校验——校验发生在证书加载阶段,与 DDR 开关无关。

结论与最终取舍

组件 配置
AdGuard Home 原生 TLS 放弃(yaml 里 tls.enabled: false),内网用 http 访问 :3000 即可
LuCI 管理页 HTTPS 真证书挂 uhttpd 443(第 9 步),这是内网管理入口,值得上 HTTPS
将来若想给 AGH 上 TLS:要么签一张含内网 IP SAN 的证书(公网 CA 一般不给 IP 签,需自建 CA),要么升级到取消该校验的新版本。

验收 · 最终验收清单

全部搭完后,逐条跑一遍。任何一条不过,回对应章节排查。

# 检查项 命令 期望结果
1 黑名单生效(MosDNS 层) nslookup ad.oceanengine.com 192.168.11.253 0.0.0.0
2 黑名单生效(虎牙埋点) nslookup e-ad-monitor.huya.com 192.168.11.253 0.0.0.0
3 功能域未误伤 nslookup udblgn.huya.com 192.168.11.253 正常 IP(≠0.0.0.0)
4 正常解析 nslookup www.baidu.com 192.168.11.253 真实 IP
5 AGH 过滤层 AGH 面板 :3000 仪表盘 拦截率 > 0,过滤规则 50 万+
6 劫持链路 nft list chain inet agh_redir prerouting 存在,packets 持续增长
7 真实客户端可见 AGH 面板查询日志 客户端 IP 是设备真 IP(非全 127.0.0.1)
8 端口全景 netstat -lnp | grep -E ':53 |5553|5335|3000|9091' dnsmasq/AGH/mosdns 各就各位
9 守护自愈 logread | grep agh-guard 重启过则有”已重建”记录
10 缓存命中 同一域名连续 nslookup 两次 第二次延迟明显更低(命中 3 天乐观缓存)
11 LuCI HTTPS(若做了第 9 步) curl --resolve a.997wan.com:443:192.168.11.253 https://a.997wan.com/ -o /dev/null -w '%{http_code}' 200,无证书告警

回滚 · 回滚手册

每一步都留了退路。按你要撤销的部分对号入座,全部命令在 253 上执行。

要撤销的 操作
MosDNS 黑名单 cp /etc/mosdns/rule/blocklist.txt.bak.20260928 /etc/mosdns/rule/blocklist.txt && /etc/init.d/mosdns restart
误伤单个域名 echo 'domain:xxx.com' >> /etc/mosdns/rule/whitelist.txt && /etc/init.d/mosdns restart
AGH 整个下线 uci set AdGuardHome.AdGuardHome.enabled='0' && uci commit AdGuardHome && /etc/init.d/AdGuardHome stop && nft delete table inet agh_redir
之后 53 流量回落到 MosDNS 劫持(-110),DNS 不断
AGH 劫持但保留 AGH 只删 nft 表:nft delete table inet agh_redir,再从 crontab/rc.local 删掉 agh_redir.sh 相关行
守护脚本 删 /usr/bin/agh_redir.sh,删 crontab 里 agh_redir 行,删 rc.local 里对应行
周清任务 删 crontab 里 stats_weekly 行 + /etc/init.d/cron restart(脚本文件可留在 /root 不碍事)
统计持久化 uci set mosdns.config.stats_dump_file='0' && uci commit mosdns && /etc/init.d/mosdns restart
LuCI 真证书 cert/key 改回 /etc/uhttpd.crt / /etc/uhttpd.key,commit 后重启 uhttpd
AGH 密码 恢复备份:cp /etc/AdGuardHome.yaml.bak.20260928 /etc/AdGuardHome.yaml && /etc/init.d/AdGuardHome restart
53 劫持补丁 删 /usr/bin/mosdns-patch.sh 及 crontab/rc.local 里的对应行(注意:升级 luci-app-mosdns 后优先级会被覆盖回 -95,反而需要重跑补丁)

FAQ · 常见问题

Q1:MosDNS 面板里客户端全是 127.0.0.1,谁的查询都分不清?

不是故障,是串联架构的必然:到 MosDNS 的查询全部来自本机 AGH。真实设备 IP 在 AGH 面板 :3000 的查询日志里看。

Q2:为什么某些 App(如字节系)里的广告还是没拦住?

字节系 App 内置 HTTPDNS:通过 443 端口的加密 HTTP 请求直接向它们的 DNS 服务器查询域名,根本不走系统 DNS,路由器层的 DNS 拦截天然够不到。这类广告只能靠 App 内规则或火绒等端侧工具。本教程拦的是”走系统 DNS 的那部分”,对网页广告、第三方 SDK、大部分视频贴片依然非常有效。

Q3:某个网站突然打不开/功能异常,怎么排查是不是拦错了?

  1. 打开 AGH 面板 → 查询日志,搜该网站的域名,看是否被某条规则拦了(红色);
  2. 是规则误伤 → AGH 面板”自定义过滤规则”里加 @@||被误拦域名^$dnstype 放行,或直接禁用命中规则的某条黑名单;
  3. 怀疑 MosDNS 层 → 在 /etc/mosdns/rule/whitelist.txt 加 domain:被误拦域名 重启;
  4. 都查不到 → 可能是代理分流问题,与广告拦截无关,另查。

Q4:AGH 的 AdGuard DNS filter 挂了两条几乎一样的?

是的,本机同时启用了 AdGuardSDNSFilter 和 HostlistsRegistry 的 filter_1(同源不同镜像),重复约 10 万条规则。想省内存(约 100MB)可以在过滤器里关掉其中一条,拦截效果几乎不变。

Q5:改了配置”不生效”的排查顺序?

  1. pidof AdGuardHome / pidof mosdns——进程活着吗?
  2. netstat -lnp | grep 5553——端口在听吗?
  3. nft list table inet agh_redir——劫持表还在吗?(重启后最容易丢)
  4. logread | grep -E 'agh-guard|mosdns-patch'——守护脚本有没有报事件?
  5. AGH 面板查询日志里搜目标域名——被拦了还是没查到?

Q6:想再加一个网站的拦截,怎么操作?

和第 2 步一样:先甄别该站的广告/埋点域与功能域(拦错功能域会坏功能),然后追加到 /etc/mosdns/rule/blocklist.txt 重启即可。也可以直接依赖 AGH 的 55 万规则库——多数网站不加黑名单也已经被拦得七七八八,黑名单只用来补刀规则库覆盖不到的。

附录 · 本次改动文件清单(真机现状)

动手前先看这张表,知道教程会碰哪些文件、备份在哪。

文件 / 位置 作用 备份
/etc/mosdns/rule/blocklist.txt MosDNS 黑名单:穿山甲 9 条 + 虎牙 9 条 blocklist.txt.bak.20260928(原空模板)
/etc/mosdns/rule/whitelist.txt MosDNS 白名单(误伤放行) 未改动
/etc/AdGuardHome.yaml AGH 主配置:上游 5335、规则库、密码 .bak.20260928、.bak.tls-20260928
/usr/bin/agh_redir.sh AGH 守护脚本(进程保活 + nft 重建) 新建
/usr/bin/mosdns-patch.sh MosDNS 劫持补丁自动恢复 新建
/root/stats_weekly_reset.sh MosDNS 统计周清脚本 新建
/etc/crontabs/root 4 个任务:mosdns-patch(*/5)、geosite_guard(*/10)、周清(每周日4点)、agh_redir(*/5) 追加式,删行即回滚
/etc/rc.local 开机先跑 mosdns-patch.sh 和 agh_redir.sh 插入式,删行即回滚
/etc/certs/997wan-full.pem + 997wan.key LuCI HTTPS 证书(600 权限) 原自签 /etc/uhttpd.crt 保留未动
uci:AdGuardHome.AdGuardHome.enabled ‘1’(修复装完默认禁用的坑) 改回 ‘0’ 即回滚
uci:uhttpd.main(listen_https/cert/key) 443 双栈监听 + 真证书 /root/uhttpd.bak.*
uci:mosdns.config(cache/stats_* 系列) 缓存与统计持久化参数 见第 1 步表格逐项回改
nft:inet agh_redir 表 53→5553 劫持(运行时规则) 删表即回滚,守护脚本可重建
版本信息(真机实测):OpenWrt 24.10 x86_64 · MosDNS 5.3.4-r14 · luci-app-mosdns 1.7.14-r1 · AdGuard Home v0.107.79。其他版本遇到行为差异时,优先怀疑 luci-app 更新覆盖了 init 脚本——重跑第 1 步补丁脚本即可。

以上均为 997笔记(大番茄 997wan)原创整理,按本教程搭好后建议实际执行一次 reboot 验证守护脚本是否真的自愈。

上一篇 三角洲行动 S11 顶级武器排行:M7、AS Val、AWM 谁才是版本答案?
下一篇 25—29 岁连续 13 年成结婚主力军:民政部 2025 年公报里的几组关键数据