MosDNS 实时统计面板从零搭建:SSE 秒级推送 + 五区间统计 + 设备级流量

AI 摘要997笔记AI

针对 MosDNS 缺少可视化统计的问题,教程介绍在另一台机器部署开源实时面板:通过 SSH 只读调用 stats_collector 接口,不改主路由、不侵入 MosDNS,用 Python 标准库起 HTTPS 与 SSE 推送,支持登录、区间统计、设备排行和域名溯源。方案解耦稳定,面板可随时停用卸载。

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

在旁路由上装完 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 统计面板 · 运行概览 KPI
运行概览:总查询、缓存命中率、拦截查询、拦截占比、平均延迟、当前 QPS、缓存记录数、本地 hosts
MosDNS 统计面板 · 热门与被拦截域名 TOP 50
双榜并列:热门域名 TOP 50 与被拦截域名 TOP 50,带相对占比条
MosDNS 统计面板 · 局域网设备表
局域网设备表:客户端 IP / MAC / 设备名 / 查询量 / 被拦截,五列均支持三态排序
MosDNS 统计面板 · 客户端查询日志
客户端查询日志:按时间倒序,支持类型 / 状态 / 拦截 / 缓存 / 耗时列筛选与全局搜索
MosDNS 统计面板 · 域名 IP 溯源查询
域名 / IP 查询:实时向 MosDNS 发一次查询,展示解析链路、规则命中、归属地与热度
MosDNS 统计面板 · 浅色主题
整套配色做了双主题:暗色与浅色一键切换,图表、卡片、表格全部跟随

二 · 取数原理:为什么不直接读 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 的返回长这样,字段名后面会原样透传到前端:

GET /plugins/stats_collector/api/v1/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 主机,确认接口能通、字段符合预期:

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 单元

/etc/systemd/system/mosdns-dashboard.service
[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。

push_loop 线程
每 2 秒取数
→算指纹
与上次比对
→有变化
广播给所有订阅
→无变化
只发心跳保活
→无订阅者
降到 10 秒
SSE 关键响应头
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 秒轮询;页面切到后台时跳过重绘,切回来补一帧。

前端接 SSE(节选)
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 租约里的备注名,没有就退回主机名。

AdGuard Home 只读凭据
# /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 后面。

IP 转 32 位整数排序
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、没有容器。想改哪个区块,直接改就行。

延伸阅读

上一篇 500万放余额宝一天收益多少?实测100万余利宝:一天29块
下一篇 给 MosDNS 做了个实时统计面板:纯 Python 标准库、零依赖,能按设备看谁在查什么