针对 MosDNS 缺少可视化统计的问题,教程介绍在另一台机器部署开源实时面板:通过 SSH 只读调用 stats_collector 接口,不改主路由、不侵入 MosDNS,用 Python 标准库起 HTTPS 与 SSE 推送,支持登录、区间统计、设备排行和域名溯源。方案解耦稳定,面板可随时停用卸载。
在旁路由上装完 MosDNS,最常见的遗憾是:它跑得很稳,但你不知道它到底干了什么——今天拦了多少广告、缓存命中率有多高、哪个设备在疯狂发查询。MosDNS 自带的 stats_collector 插件其实已经把这些数据算好了,只是缺一个好看的界面。
这篇教程带你在另一台机器上从零搭一个 MosDNS 实时统计面板:不动 MosDNS 本体、不改主路由任何配置,通过 SSH 只读取数,用 Python 标准库起一个 HTTPS 站点,前端用 SSE 秒级推送,带登录认证、五个时间区间统计、设备级流量排行和域名溯源查询。如果你还没搭过 MosDNS 本身,建议先看 OpenWrt + MosDNS + AdGuard Home 全网去广告搭建教程。
github 开源项目地址:https://github.com/996kuku/mosdns-dashboard
这套方案的核心取舍
面板完全不侵入 MosDNS:只通过 SSH 调用它的 stats_collector HTTP API(默认只监听 127.0.0.1:9091),拿到 JSON 后在本地建库。好处是主路由零改动、随时可以停掉面板;代价是需要配一把免密 SSH 密钥。
一 · 先看效果
面板分为八个区块,全部实装、全部实时。下面按实际界面顺序过一遍。







二 · 取数原理:为什么不直接读 MosDNS
MosDNS 的 stats_collector 插件会把自己的统计数据算好,通过 HTTP 暴露出来。但它在主路由上,而且默认只监听回环地址。面板要拿数据,有三条路:
- 改 MosDNS 配置把 9091 暴露到局域网 —— 动了主网关,不推荐;
- 在 MosDNS 同一台机器上装面板 —— 会往主路由塞 Python 运行环境,也不推荐;
- SSH 只读调用 —— 面板装在另一台机器上,通过 SSH 在 MosDNS 本机执行
curl拿 JSON。
本教程采用第三条。整条数据链路是这样的:
读本地库→SSH 免密
到 MosDNS→curl 127.0.0.1:9091
/stats_collector/*→JSON 回传
解析入库→SSE 推送
浏览器重绘
为什么这样最稳
面板和 MosDNS 完全解耦:主路由上不新增任何常驻进程、不新增监听端口,面板挂了不影响 DNS,MosDNS 挂了面板只是显示旧数据。要卸载时删掉面板目录即可。
stats_collector 提供哪些接口
MosDNS 的 stats_collector 暴露了一组 REST 接口,面板主要用其中四个:(想换上游 DNS 的话,这份 DoH/DoT 服务汇总可以直接抄)
| 接口 | 作用 | 面板对应区块 |
|---|---|---|
/api/v1/stats |
累计统计:总查询、缓存命中数/率、拦截数/占比、平均延迟、QPS | 运行概览 KPI |
/api/v1/history |
按小时的历史点(total / cached / blocked) | 查询趋势曲线 + 区间统计 |
/api/v1/top |
TOP 域名 / TOP 被拦截域名 / TOP 客户端 | 两个 TOP 50 榜单 |
/api/v1/logs |
最近的解析日志明细 | 最近解析日志 |
其中 /stats 的返回长这样,字段名后面会原样透传到前端:
{
"total_queries": 1167722,
"cached_queries": 1117240,
"cached_percentage": 95.68,
"blocked_queries": 86,
"blocked_percentage": 0.01,
"avg_latency_ms": 1.1,
"qps": 0,
"hosts_queries": 0,
"arbitrary_queries": 0
}
三 · 环境准备
1. 机器选型
面板本身很轻(单进程、标准库、无第三方依赖),一台 1 核 512M 的小机器绰绰有余。可以放在家里的 NAS、另一台软路由、树莓派,或者云服务器。唯一要求是能 SSH 到 MosDNS 所在的主机。
| 项目 | 要求 | 说明 |
|---|---|---|
| 操作系统 | Debian 12/13、Ubuntu 22.04+ 等现代 Linux | 本教程在 Debian 13 验证 |
| Python | 3.9 及以上 | 只用标准库,不需要 pip 装任何包 |
| 磁盘 | ≥ 200 MB | 主要是本地 SQLite 日志库 |
| 网络 | 能访问 MosDNS 主机的 SSH 端口 | 建议走内网 |
2. 配置免密 SSH
面板要频繁(默认 5 秒一次)去 MosDNS 主机取数,必须用密钥免密登录。没配过免密登录的话可以先看 SSH 密钥登录新手教程:从生成密钥到启用。在面板机器上生成密钥并推送到MosDNS 主机:
ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519_mosdns -N "" -C "dashboard@local"
ssh-copy-id -i /root/.ssh/id_ed25519_mosdns.pub root@192.168.11.253
推送完先手动验证一次,确认不需要输密码:
ssh -i /root/.ssh/id_ed25519_mosdns root@192.168.11.253 "echo OK"
只读原则
面板对 MosDNS 主机只做读操作。建议在 MosDNS 主机的 ~/.ssh/authorized_keys 里给这把密钥加限制,禁止端口转发和交互式 shell 以外的一切:把
from="面板IP",no-agent-forwarding,no-X11-forwarding,no-port-forwarding 加在对应那一行的行首。
3. 先手工验证一次取数
登录 MosDNS 主机,确认接口能通、字段符合预期:
curl -s --noproxy "*" http://127.0.0.1:9091/plugins/stats_collector/api/v1/stats
curl -s --noproxy "*" http://127.0.0.1:9091/plugins/stats_collector/api/v1/top | head -c 400
为什么一定要带 –noproxy “*”
MosDNS 主机上通常跑着透明代理,curl 可能继承 http_proxy 环境变量而把回环请求也发出去,表现为超时或 502。加 --noproxy "*" 可强制直连,是本方案第一个必须记住的细节。
四 · 部署面板
1. 目录结构
mkdir -p /opt/mosdns-dashboard
cd /opt/mosdns-dashboard
# 放置以下文件:
# server.py 后端(Python 标准库,单文件)
# index.html 前端页面(单文件)
# login.html 登录页
# agh.conf AdGuard Home 只读凭据(可选,600)
# auth.conf 面板自身的登录凭据(600)
# ssl/ TLS 证书(可选)
2. 后端启动参数
后端全部参数都可以用环境变量覆盖,方便不改代码调整:
| 环境变量 | 默认值 | 作用 |
|---|---|---|
DASH_HOST |
0.0.0.0 | 监听地址 |
DASH_PORT |
8088 | 监听端口 |
MOSDNS_HOST |
192.168.11.253 | MosDNS 主机 |
MOSDNS_SSH_KEY |
/root/.ssh/id_ed25519_mosdns | SSH 私钥路径 |
INGEST_INTERVAL |
5 | 后端取数间隔(秒) |
PUSH_INTERVAL |
2 | SSE 检测间隔(秒) |
PUSH_IDLE_INTERVAL |
10 | 无人观看时的检测间隔 |
DASH_TLS_CERT / DASH_TLS_KEY |
空 | 填了就以 HTTPS 启动 |
取数频率怎么定
实测单次取数 = SSH(日志) 约 148 ms + SSH(统计) 约 138 ms + AdGuard 日志约 202 ms。5 秒间隔下 MosDNS 侧每分钟多耗约 4.2 秒 CPU,在一台 8 核、日常 load 0.07 的主机上,调成 5 秒后 load 仅升到 0.09 —— 完全可以忽略。如果要更省,把 INGEST_INTERVAL 调大即可,前端会自动降频重绘。手里没有多余机器的话,在 PVE 里用 qm importdisk 装一台轻量虚拟机是成本最低的做法。
3. systemd 单元
[Unit]
Description=MosDNS Dashboard
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/opt/mosdns-dashboard
ExecStart=/usr/bin/python3 -u /opt/mosdns-dashboard/server.py
Restart=on-failure
RestartSec=3
StandardOutput=journal
StandardError=journal
SyslogIdentifier=mosdns-dashboard
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now mosdns-dashboard
systemctl status mosdns-dashboard --no-pager
journalctl -u mosdns-dashboard -f
五 · 把「实时」做真:SSE 推送
大多数仪表盘的「实时」其实是前端每 N 秒轮询一次 HTTP 接口,数据没变也照样请求,白白浪费带宽和服务端算力。这套面板用的是 SSE(Server-Sent Events):
1. 服务端怎么推
后端起一个后台线程,固定间隔去取一次数据,算一个指纹(总量、缓存命中、拦截数、榜首域名、历史点数、缓存条数等拼成的哈希)。只有指纹变了,才向所有订阅者广播一帧。没有订阅者时自动降频,避免白白去 SSH 打扰 MosDNS。
每 2 秒取数→算指纹
与上次比对→有变化
广播给所有订阅→无变化
只发心跳保活→无订阅者
降到 10 秒
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache
X-Accel-Buffering: no # 关键:禁止反代缓冲,否则数据被攒住不吐出来
Connection: keep-alive
2. 前端怎么接
前端用浏览器原生的 EventSource 接长连接,收到一帧就重绘一次。同时做三重降级:连续 3 次 onerror 就主动 close() 并退回 20 秒轮询;页面切到后台时跳过重绘,切回来补一帧。
const ES = new EventSource('/api/stream');
ES.onmessage = (ev) => { applyPush(JSON.parse(ev.data)); };
ES.onerror = () => { if (++errCount >= 3) { ES.close(); fallbackPoll(); } };
document.addEventListener('visibilitychange', () => {
if (!document.hidden) refreshRangeSilent(); // 回到前台补一帧
});
反代一定要关缓冲
如果你在面板前面挂了 nginx,必须让 SSE 那条路径带 proxy_buffering off;,并且后端响应头里要有 X-Accel-Buffering: no。少任何一个,SSE 都会「连上了但半天不吐数据」,表现和卡死一样。
3. 五个时间区间
面板支持 实时 / 今天 / 近 7 天 / 近 30 天 / 自建库以来五个区间。实时档直接显示 MosDNS 自身的累计值,其余四档来自面板本地建的时序库。本地库分三层,避免全量扫表:
| 表名 | 粒度 | 用途 |
|---|---|---|
stats_min |
分钟增量 | 今天 / 近 7 天的精细曲线 |
stats_hour |
小时 rollup | 近 30 天 / 自建库以来的长期曲线 |
top_hour |
小时 | 各区间下的 TOP 域名榜 |
domain_total |
累计 | 域名总查询量,供溯源查询用 |
一个很容易踩的细节:键名一定要对齐
每个区间的汇总对象(sum)字段名刻意与 MosDNS 的 /stats 返回完全一致(total_queries、cached_percentage……)。这样前端切换区间时只是换数据源,渲染逻辑一行都不用改 —— 也顺手避免了「切了区间数字不变」这类经典问题。
六 · 登录认证:别裸奔
面板一旦通过内网穿透(frp)暴露到公网,就必须有登录。这里没有引入任何 web 框架,用标准库实现了完整的一套:
- 凭据不落明文:
auth.conf里存 PBKDF2-SHA256 哈希 + 随机 salt,权限 600; - 会话用 HMAC 签名:
用户名|过期时间戳|签名,改一个字符就失效; - Cookie 加固:
HttpOnly+SameSite=Lax,HTTPS 下自动加Secure; - 防爆破:按来源 IP 记连续失败次数,超阈值直接拦。
/usr/local/bin/dashboard-setpass # 交互式改密码
/usr/local/bin/dashboard-setpass -d # 临时关闭认证(仅调试用)
部署后必须复查认证有没有被覆盖
如果面板代码会被反复重新部署,认证逻辑很容易被新版本覆盖掉而你不知道。稳妥做法是把它做成启动前自检:写一个幂等脚本挂在 systemd 的 ExecStartPre=- 上(注意那个 -,表示自检失败也不能阻止服务启动),启动时检查代码里是否还有认证相关符号,缺了就往日志里报警。
七 · HTTPS 与部署自愈
完整链路可以做成:
https://dns.example.com→接入层 nginx
443 按 SNI 分流→frps
隧道端口→面板机器 frpc→面板
127.0.0.1:8088
面板自身也可以直接跑 HTTPS —— 把证书路径填进环境变量即可,支持标准的自签 CA 或 ACME 证书。几个实践要点:
- 证书链顺序:证书文件必须叶子证书在前、CA 在后,反了会直接握手失败;
- 接入层别校验证书:如果后端用自签证书,前面 nginx 的 upstream 要写
proxy_ssl_verify off;; - 把 TLS 逻辑做成启动前自愈:与其把 TLS 代码写死在业务文件里(重新部署就被覆盖),不如放一个幂等脚本到
ExecStartPre,每次启动前检查、缺失就补上。
自愈脚本带来的好处
面板代码可以随便重新部署,重启服务即自动恢复 HTTPS。对「经常改一改、随手重启一下」的自建面板来说,这比把配置写进代码里省心得多。如果后端用的是自签证书、希望浏览器不报红,用 PVE 内部 CA 给内网签证书是最省事的一条路。
八 · 两个进阶细节
1. 局域网设备表的客户端 IP 从哪来
一个反直觉的现象:在 MosDNS 那层看到的客户端 IP 永远是 127.0.0.1。原因是转发的 dnsmasq 会把源地址改写掉。想要真实的设备级统计,就得换一个能看到真实客户端的地方取数。
真实 IP→AdGuard Home
监听 5553→MosDNS
127.0.0.1:5335→上游
DoH / DNS
所以设备表的数据取自 AdGuard Home 的查询日志(它能看到真实客户端),MAC 地址则来自 MosDNS 主机的邻居表(ip neigh),两者按 IP 关联。设备名优先取爱快 DHCP 租约里的备注名,没有就退回主机名。
# /opt/mosdns-dashboard/agh.conf (权限 600)
AGH_URL=https://127.0.0.1:3443
AGH_USER=
AGH_PASS=
# 说明:该账号仅用于拉 querylog / stats,请单独建一个,不要用管理员账号
2. 表头三态排序
局域网设备表的五列(IP / MAC / 设备名 / 查询量 / 被拦截)都支持升序 → 降序 → 恢复默认三态循环。IP 列按数值排序而不是字符串,否则 192.168.11.9 会排到 192.168.11.10 后面。
function ipNum(s){
const m = String(s).match(/^(d+).(d+).(d+).(d+)$/);
if(!m) return null; // 非 IPv4 恒沉底
return ((+m[1])<<24 | (+m[2])<<16 | (+m[3])<>> 0;
}
九 · 双主题配色规范
面板同时提供暗色与浅色两套主题,用 html[data-theme] 切根节点的 CSS 变量。这里有一条很容易翻车的经验:
文字/背景千万不要写死浅色 hex
在暗色主题下,任何写死的浅色底(例如 #f1f4f9、#eff6ff)都会变成刺眼的「白疙瘩」。正确做法是容器/表头/标签条一律用中性半透明:rgba(127,140,160,0.10 ~ 0.30),文字用 color:inherit 或 opacity:.6 ~ .8。彩色提示框则用「边框同色 0.62 + 底色同色 0.12」的搭配,两套主题下都自然。
| 元素 | 暗色主题 | 浅色主题 |
|---|---|---|
| 页面背景 | 深色 | 白色 |
| 卡片 / 表头底 | rgba(127,140,160,0.10~0.30) |
同上(半透明自动适配) |
| 主强调色 | 青色 | 蓝色 |
| 示例按钮悬停 | #D29922(琥珀金底 + 深字) | #409EFF(蓝底 + 白字) |
十 · 上线验收清单
部署完照这份清单过一遍,基本不会漏:
| 检查项 | 期望结果 | 怎么验 |
|---|---|---|
| 服务状态 | active (running) | systemctl status mosdns-dashboard |
| 免密 SSH | 无交互返回 OK | ssh -i 私钥 ... "echo OK" |
| 数据接口 | 返回 JSON 且字段完整 | curl -sk https://127.0.0.1:8088/api/stats |
| SSE 通道 | 有数据持续吐出 | curl -skN https://127.0.0.1:8088/api/stream |
| 登录拦截 | 未登录访问跳登录页 / 401 | 隐身窗口直接访问首页 |
| 五个区间 | 切换后 KPI 与曲线同步变化 | 点四个历史按钮逐个核对 |
| 双主题 | 暗/浅切换后文字清晰可读 | 两种主题各看一遍所有卡片 |
| 移动端 | 窄屏下表格可横向滚动 | 浏览器切设备模拟 |
调优后的实测负载
面板取数间隔调到 5 秒后,MosDNS 主机(8 核)单核占用约 13.9%,整机 load 从 0.07 升到 0.09,内存无明显变化 —— 对一台专职跑 DNS 的机器来说完全无感。
十一 · 小结
整套方案的思路可以概括成三句话:
- 取数解耦:不去改 DNS 服务本身,用 SSH 只读调用它的官方统计接口;
- 推优于拉:SSE 长连接 + 指纹比对,有变化才推,没变化只发心跳;
- 可自愈:TLS、认证这类容易被覆盖的东西做成启动前幂等补丁,重新部署也不怕。
面板代码是纯 Python 标准库单文件,前端也是单文件,部署就是拷两个文件 + 一个 systemd 单元,没有 Node、没有 npm、没有容器。想改哪个区块,直接改就行。
延伸阅读
- OpenWrt + MosDNS + AdGuard Home 全网去广告搭建教程 —— 先把 MosDNS 本身跑起来,面板才有数据可看。
- PVE 系统 img 格式转换教程:qm importdisk 安装 OpenWrt —— 没有独立机器跑面板时,可以在 PVE 里开一个轻量 LXC/虚拟机。
- SSH 密钥登录新手教程:从生成密钥到启用 —— 免密 SSH 是本方案的前置条件,没配过的先从这篇开始。
- 局域网 HTTPS 绿锁教程:用 PVE 内部 CA 给内网 IP 签证书 —— 想让面板直接跑 HTTPS 又要浏览器不报警,看这篇。
- 国内外常用 DoH/DoT 服务汇总(含 IPv4/IPv6 与接入地址) —— 给 MosDNS 换上游时可以直接抄这份清单。
- 文章名称:MosDNS 实时统计面板从零搭建:SSE 秒级推送 + 五区间统计 + 设备级流量
- 文章永久网址:https://www.997wan.com/mosdns-realtime-dashboard-tutorial/
- 本网站的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系站长 997笔记 进行删除处理。
- 本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
- 本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向站长举报
- 本站资源大多存储在云盘,如发现链接失效,请联系我们我们会第一时间更新。
- 如无特别声明本文即为原创文章仅代表个人观点,版权归《997笔记》所有,欢迎转载,转载请保留原文链接。