一、先说痛点:MosDNS 的统计太"粗"了
家里软路由跑的是 OpenWrt + MosDNS(sbwml/luci-app-mosdns),分流、去广告都挺好,但它的统计能力只有两样:
- 一个
statsAPI,返回的是从启动到现在的累加计数
- 一份日志文件,看个大概
具体缺什么:
MosDNS 自带 | 缺什么 |
stats API 只有累加计数 | 没有明细、没有历史、重启就归零 |
日志里 | 看不出是哪台设备查的(请求是本机 dnsmasq 转发进来的) |
— | 没有 MAC / 设备名 |
只有"现在"的数字 | 想看趋势?没有 |
第 2 条最要命——家里十几台设备,我想知道"到底是谁在疯狂查某个域名",MosDNS 告诉我"都是 127.0.0.1 查的"。
所以就有了这个面板。
二、先看效果
📷【配图 1:主面板整页截图】图片文件:
mosdns-dashboard-oss/docs/dashboard.png(1920×3662 整页) 在 CSDN 编辑器里直接把这张图拖进去就行
📷【配图 2:登录页】图片文件:
mosdns-dashboard-oss/docs/login.png建议放在"HTTPS / 登录认证"那一节前面
说明:截图里的内网网段已替换成
192.168.1.x示例值,其余是真实运行数据。
三、它到底补上了什么
能力 | 实现方式 |
历史与趋势 | SQLite 落库,支持五个区间:实时(MosDNS 累计)/ 今天 / 近 7 天 / 近 30 天 / 自建库以来 |
真实客户端 IP | 用 AdGuard Home 的 querylog 按 |
MAC / 设备名 | 拉 iKuai(爱快)的 DHCP 租约表补上,再叠加一个手写 |
实时推送 | SSE 长连接,约 2 秒一帧,指纹变了才广播(没人看时自动降到 10 秒一轮,不空转) |
上游服务器表 | 清单的权威来源是路由器上的 |
按模块"清除重计" | 可以只重置 TOP 榜、或只重置统计,互不影响 |
8088 单端口同时收 HTTPS 和 HTTP | accept 后 peek 首字节判断,明文请求自动 301 跳 HTTPS(和 nginx |
登录认证 | PBKDF2 存密码,cookie 用 HMAC-SHA256 签名(HttpOnly / SameSite=Lax / Secure) |
最核心的一点:纯 Python 3 标准库,零第三方依赖。不用pip install任何东西,git clone下来改两行配置就能跑。
⚠️ 强调一下边界:本面板不参与解析、不改路由器任何配置,它只是读MosDNS 的统计接口,然后把数据攒进本地 SQLite 展示出来。
四、数据来源与架构
面板对路由器只做只读操作:SSH 过去读一个 JSON、拉一次 querylog、查一次邻居表,不写任何东西。
开销实测:单次 SSH 拉日志 148ms + AGH querylog 202ms,5 秒采集间隔下,路由器侧每分钟多耗约2.8 秒 CPU(占单核 13.9%)。路由器是 8 核、常态 load 0.07,余量充足。
五、5 分钟跑起来
前置条件
- Python 3.9+(无需 pip 安装任何包)
- 到路由器的 SSH 免密登录,建议单独生成一把只读用途的 key
- MosDNS 开启 stats_collector API——
luci-app-mosdns默认就开了,监听127.0.0.1:9091
安装
默认监听0.0.0.0:8088。没配证书时会自动回退成明文 HTTP(日志里会明确写一句)。
环境变量速查
变量 | 默认值 | 说明 |
|
| 监听地址 |
|
| 监听端口 |
|
| TLS 证书链 |
|
| TLS 私钥 |
|
| 路由器 IP |
|
| SSH 用户名 |
|
| SSH 私钥路径 |
|
| 采集间隔(秒) |
|
| 有人看时的 SSE 采样间隔(秒) |
|
| 没人看时的采样间隔(秒) |
|
| 上游清单配置缓存(秒) |
配置文件速查
文件 | 必需 | 作用 | 缺失时 |
| 否(但强烈建议) | 登录认证 | 不启用认证,任何人可看 |
| 否 | 反查真实客户端 IP | 客户端列全是 |
| 否 | MAC / 设备名 | 没有 MAC 列 |
| 否 | IP → 设备名手写映射 | 无 |
六、HTTPS:单端口同时收 HTTPS 和 HTTP
这个设计我觉得挺值得说一下。
一般做法是开两个端口(比如 8088 收 HTTPS、8089 收 HTTP 然后跳转)。但这样对使用者不友好——记错端口就访问不了。
所以做成了同一个端口:
直接访问http://host:8088/也会被 301 带到 HTTPS。
自签证书:
有正式证书(Let's Encrypt 等)就用DASH_TLS_CERT/DASH_TLS_KEY指过去。
📷【配图 2 建议放这里:登录页】
七、踩坑记录(这节是我最想写的)
坑 1:TLS 必须"按连接" wrap,不能包监听 socket
一开始我想省事,直接在监听 socket 上套一层ssl_ctx.wrap_socket(listen_sock)。结果明文 HTTP 请求直接握手失败,浏览器直接报 SSL 协议错误(Firefox 是SSL_ERROR_RX_RECORD_TOO_LONG)——压根不会给你跳转的机会。
正确做法是 accept 之后对那条连接单独 wrap。
坑 2:socket.socket没有__dict__,挂不上自定义属性
承接坑 1,我想给明文连接打个标记:
Python 的socket.socket是 C 扩展类型,没有__dict__。而且这不是"报个错就完事"——它发生在 accept 循环里,异常逃出去会让进程崩,systemd 再把它拉起来,然后再次崩,形成崩溃重启循环。
解法是包一层子类 +detach():
教训:重写accept()这类底层方法时,一定要确认异常不会逃出循环。
坑 3:实现 HTTP HEAD 不能"把self.wfile换成黑洞"
有个探测工具用 HEAD 请求探活,结果我的服务一直返回 501。补 HEAD 时我第一反应是:HEAD 不就是要"只回响应头不回 body"吗,那把wfile换成个黑洞对象不就行了?
大错特错。Python 3 的end_headers()是把 header 缓冲写进self.wfile的,你把 wfile 换掉,连响应头一起吞了,客户端拿到的是 EOF。
现象特别迷惑人:curl报SSL unexpected eof,前面挂了 nginx 反代的话直接502。
正确做法是加一个标记位,路由照常复用do_GET()(这样Content-Type/Content-Length和 GET 完全一致),只是在end_headers()之后不写 body:
顺带一提,测试 HEAD 要用curl -I,用curl -X HEAD会报unexpected eof——这是 curl 自己的行为(它去读 body),不是服务的问题。
坑 4:SSE 经过反向代理会被"憋住"
面板前面挂了反向代理(我这边是 nginx + frp 的链路),SSE 推不出去,前端一直等。
原因是 nginx 默认会缓冲响应。SSE 的响应必须显式关掉缓冲:
坑 5:「清除重计」点了完全没反应
这是最难受的一个 bug。用户反馈"点了清除,数字跳一下又回去了"。
排查了半天,最后发现是两个出口口径不一致:
- 推送线程算"数据指纹"用的是减过基线的值
- 而
/api/stream首帧取的是没减基线的原始数据
前端拿到归零数据、刚更新完界面,紧接着被下一帧原始累计值整个覆盖回去了。
教训:凡是给前端的快照出口,必须走同一套变换。光读代码发现不了——必须把两个出口的真实返回值拉出来对比。
坑 6:反向代理 +「内网免登录」= 对全网放行
面板有个AUTH_LAN_FREE选项,意思是"内网来的就不必登录"。
但"内网"是靠socket 远端地址判定的。一旦前面挂了 nginx / frp,服务端看到的来源 IP恒为127.0.0.1——于是"内网免登录"变成了对全网免登录。
而面板里是什么?是你家所有设备的完整上网记录。
要按来源放行,必须先让代理层把真实 IP 传进来(proxy_protocol或X-Forwarded-For),并且验证过它真的生效再说。我这边就踩了这个坑,香港入口拿不到真实客户端 IP,最后干脆把AUTH_LAN_FREE关掉,内外网一律要登录。
八、部署到公网前必读的安全提醒
- 默认要登录。
auth.conf缺失或AUTH_ENABLE != 1时面板对任何人开放。
- 不要把
AUTH_LAN_FREE=1和反向代理一起用(见坑 6)。
logs.db绝对不要提交到任何地方——里面是真实的域名查询历史。仓库的.gitignore已经排除它了。
- 面板本身不做访问控制之外的加固,暴露到公网请自己再加一层(防火墙 / Basic Auth / VPN)。
九、开源地址
平台 | 地址 |
GitHub | GitHub - 996kuku/mosdns-dashboard: Real-time DNS query dashboard for MosDNS on OpenWrt — per-device stats, upstream health, zero dependencies (pure Python 3 stdlib) · GitHub |
Gitee(国内推荐) | https://gitee.com/yilunn996/mosdns-dashboard |
AtomGit | mosdns-dashboard:MosDNS 统计面板:纯 Python 标准库实现,SSE 秒级推送,支持五区间统计、上游服务器表、设备级流量与域名反查。零依赖、单文件部署。 - AtomGit |
MIT 协议,随便用、随便改。
几个可能对你有用的细节:
- 零依赖:只用 Python 标准库,不用担心某天某个包不维护了
- 网站图标不进仓库(版权归各自站点),部署后按需抓取:
./tools/fetch-logos.py -n 200 -j 16,抓不到就退化成首字母色块,不影响使用
- 改了路由器 MosDNS 配置,面板最多 60 秒就能看到(
UP_CFG_TTL),不用重启
十、常见问题
Q:客户端那一列全是127.0.0.1?MosDNS 日志里的client确实是本机(dnsmasq 转进来的)。要真实 IP 必须配agh.conf,让面板用 AGH 的 querylog 去反查。
Q:上游服务器表里某个上游一直是 0?清单来自路由器的 MosDNS 配置,所以"配了没用过"的上游也会列出来(这是故意的)。统计来自本地日志库,只有真的发出过查询才有数。
Q:环境里没有 AdGuard Home / 爱快能用吗?能。这两个都是可选的,缺了只是拿不到真实客户端 IP 和 MAC / 设备名,核心统计照常。
最后
这个面板最初只是我为了解决"到底是谁在查这个域名"这个小问题写的,后来一点点加成了现在这样。
如果你也在用 MosDNS,欢迎试试;有问题直接提 Issue,或者评论区聊。觉得有用的话,去 Gitee / GitHub 点个 ⭐ 就是对我最大的鼓励。
本文同步发布于 997笔记(https://www.997wan.com)