家里的网络有个特点:出问题的时候你不在家,等你发现的时候问题已经过去了。上周我连续两天觉得晚上刷视频有点卡,但打开各种监控面板一看,曲线早被时间冲淡了,谁也说不出到底是哪个环节抖了。
我手上其实不缺监控:NAS 上有 Grafana,路由器上有 Netdata,各有一堆漂亮曲线。但它们有个共同的问题——得我主动去看。而我想反过来:让数据主动来找我,每天早上在邮箱里躺着一封"昨天全屋网络体检报告",几百字、一眼能看完,有异常才提醒。
于是我在 iStoreOS 软路由上自己攒了一套:脚本每 5 分钟采一次指标,每天 8 点汇总成 HTML 邮件发到 QQ 邮箱。做完之后顺手又给它写了个 LuCI 应用,把收件人、发送时间、告警阈值都搬到了 WebUI 里——现在改配置不用再 SSH 改文件了。
这篇记录完整过程:为什么要这么做、三层架构怎么拆、怎么部署、以及我踩到的 9 个坑(其中几个挺阴的,属于"配置看着生效了其实没生效"那种)。如果你也在用 iStoreOS / OpenWrt 做家用主路由,可以直接抄。
先说结论
- 要的不是又一个监控面板,而是一封"推力"的日报。面板负责随时可查,邮件负责"没事不打扰、有事必达"。
- 三层就够:采集(cron 每 5 分钟)→ 汇总发信(cron 每天一次)→ 配置界面(LuCI 应用)。每层都能单独替换,坏一层不影响另外两层。
- iStoreOS 的软件源里没有现成的"日报"插件(只有 msmtp 客户端、邮件中继服务、vnstat 这类图表),所以是自建。
- LuCI 应用不用写 SDK、不用编译——它就是几个文本文件放进指定目录,重启下 rpcd 就能在菜单里看到。这是本文最"性价比"的一段。
我为什么非要一封邮件日报
先把需求说清楚,不然后面容易做成"又一个面板"。
| 我想知道的事 | 面板能做到吗 | 日报能做到吗 |
|---|---|---|
| 昨天上行/下行各跑了多少 | 要自己去翻曲线 | 一行数字直接给结论 |
| AdGuard Home 有没有大量反查噪音 | 要翻统计页 | 命中数量 + 一句话解释 |
| 关键服务(DNS/代理/SQM)有没有掉过 | 得看"服务是否在跑" | 直接列"有 N 个服务未运行" |
| 日志里有没有真正的错误 | 基本不会去看 | 按类型归类后给条数 |
| 有没有异常关机 / 硬断电 | 看不出来 | 开机记录里能看出来 |
结论很清楚:我要的是一份能被"读完"的摘要,而不是一个需要"解读"的面板。顺便说,如果你已经有 Grafana 那套,这套日报和它不冲突——Grafana 管细节,日报管"你该知道了"。

整体设计:采集 → 汇总 → 通知,三层拆开
| 层 | 干什么 | 跑在哪 | 产物 |
|---|---|---|---|
| 采集层 | 每 5 分钟读流量、服务状态、日志异常计数 | crontab */5 * * * * | /mnt/data/sysreport/metrics-YYYY-MM.csv |
| 汇总层 | 每天固定时间汇总前一天,渲染 HTML 邮件并发送 | crontab 每天一次 | 邮件 + 发送标记 .sent-日期 |
| 配置层 | 改收件人/时间/阈值/凭据,手动测试、预览、补发 | LuCI 应用(服务 → 邮件日报) | UCI 配置 + 渲染出的运行配置 |
为什么不塞进一个脚本里?因为我吃过亏:采集和分析一旦耦合,某天分析逻辑写错了,历史数据也跟着一起废了。分层之后,采集层无脑写裸数据,汇总层怎么改都不影响历史。
数据落在 /mnt/data(也就是我的 NVMe 分区)而不是 overlay 上,是因为这台路由器的 overlay 只有 2G,系统升级、插件缓存都挤在那儿;服务数据一律放大分区,这是我这台机器的铁律。
第一步:每 5 分钟采一次指标
采集脚本要做的事很朴素:把「此刻值得记录的数字」追加到当月 CSV。crontab 一行:
*/5 * * * * /usr/bin/router-metrics
这里有两个我特意确认过的细节,都是"不确认就会算错"的类型:
- 流量口径取物理 WAN 口(eth0),不取 pppoe-wan。PPPoE 重拨时虚拟接口计数器会清零,一旦算错,日报里会出现一次莫名其妙的"流量归零"。物理口只在重启时清零,口径才稳定。
- CSV 按月份分文件(
metrics-YYYY-MM.csv)。单文件长期追加会越来越难查,按月切分天然带归档效果,脚本读"上个月最后一天"时也只多一层判断。
第二步:生成 HTML 日报并发邮件
汇总脚本(/usr/bin/router-daily-report)做四件事:读一天的 CSV → 按阈值判定结论 → 把异常日志归类 → 渲染 HTML 邮件发送。报告里固定包含这些块:
| 区块 | 内容 |
|---|---|
| 头部结论 | ✅ 一切正常 / ⚠️ 有需要注意的情况(由阈值决定) |
| 流量卡 | 下行 / 上行 / 合计,以及数据覆盖率(采样点 / 288) |
| 服务状态 | AdGuard Home、Netdata、dnsmasq、代理、SQM 是否在跑 |
| 需关注事项 | 真正的错误日志条数 + 开机记录是否异常 |
| 运维提示 | 把"看着像错误、其实不用管"的东西解释清楚 |
其中最有价值的是最后一块。日报最大的敌人不是漏报,而是狼来了——每天报 300 条"错误",第三天你就再也不看邮件了。所以我给日志做了三分法:
| 类别 | 典型内容 | 处理方式 |
|---|---|---|
| 组件自检噪音 | 监控组件探测虚拟网卡、UPnP 映射热更新竞态、内核模块未加载 | 白名单,不计入异常 |
| 设计冗余 | AdGuard 把 in-addr.arpa 反查转给 dnsmasq,本地网段之外空等超时 | 归类 + 在"运维提示"里解释一次 |
| 需要关注 | DNS 上游限流、代理链路被拒、真正的服务报错 | 计入条数并写进"需关注事项" |
还有一个实现细节值得记一笔:异常匹配要锚定 syslog 的字段位(HH:MM:SS 年份 facility.level),而不是裸搜 error 关键字。否则日志里任何一段正常文本里出现 error 都会被算成异常——我第一版就是这么写的,日报常年"异常 200+ 条",纯噪音。
QQ 邮箱发信配置
QQ 邮箱用 SMTP + SSL 发信,端口 465。注意用的是授权码,不是登录密码(在邮箱设置的"账户"页里开 SMTP 服务后生成):
# /etc/msmtprc —— 权限务必 600
account default
host smtp.qq.com
port 465
tls on
tls_starttls off
auth on
user 你的QQ号@qq.com
password 这里填授权码
from 你的QQ号@qq.com
set_from_header on
allow_from_override off
syslog LOG_MAIL
发信命令就一句:msmtp -a default 收件人@qq.com < 邮件正文。脚本里我做了三次重试 + 失败落盘(/mnt/data/sysreport/failed/),这样偶发网络抖动不会丢报告。
坑 ①:QQ SMTP 卡在 "500 Line too long",一封都发不出去
第一封邮件就被拒了,连续重试三次全失败,日志里是这么一行:
msmtp: server message: 500 Line too long, exceeds the 998-octet text line limit defined in RFC 5321.
msmtp: could not send mail (account default from /etc/msmtprc)
RFC 5321 规定邮件正文单行不能超过 998 字节,QQ 是严格执行的。而我那封 HTML 邮件里,把 4 条"运维提示"拼在同一个变量里输出成了一行,长度 1089 字节——刚好越线。
修法有两层,建议都做:
- 根因修:每条提示独立成行,别做字符串拼接。
- 保险修:发送前扫一遍正文,超过 900 字节的行按空格边界折行。注意只折正文、不折邮件头——邮件头里的中文标题是 base64 编码的,中间插换行会把编码弄坏。
# 只处理空行之后的正文部分,扫到超长行就折
if awk 'length($0)>900{f=1} END{exit (f?0:1)}' "$MAIL"; then
awk 'BEGIN{h=1} h{print; if($0=="")h=0; next}
{ s=$0
while(length(s)>900){ i=900; while(i>1 && substr(s,i,1)!=" ")i--; if(i<=1)i=900
print substr(s,1,i-1); s=substr(s,i+1) }
print s }' "$MAIL" > "$MAIL.fold" && mv "$MAIL.fold" "$MAIL"
fi
改完之后同样的报告一次就过。这类"服务器明确拒收并给出原因"的错误其实最好修——怕的是对方收了但内容坏了。

第三步:把配置搬进 WebUI(一个纯文件的 LuCI 应用)
到这儿日报已经能跑了。但每次改收件人、改发送时间、临时补发一份,都得 SSH 上来编辑文件、改 crontab——对我自己还行,对家里人就是灾难。
我先确认了仓库里确实没有现成插件:iStoreOS 软件源里跟"通知"沾边的只有 msmtp 客户端(没有界面)、luci-app-email(是自建 SMTP 中继服务器,不是发通知)、以及 vnstat / statistics 这类只出网页图表的插件。那就自己做一个。
关键认知:LuCI 应用不用编译
很多人以为做 OpenWrt 插件必须装 SDK 交叉编译。其实纯界面型应用就是几个文本文件,丢进对应目录、清一下菜单缓存就能用。我照着机器上已经装着的第三方插件(一个同样是纯文件实现的代理面板)抄了结构,一共 6 个文件:
| 文件 | 作用 |
|---|---|
/usr/lib/lua/luci/controller/routerreport.lua | 注册菜单 + 四个接口(保存 / 发送 / 状态 / 预览) |
/usr/lib/lua/luci/view/routerreport/main.htm | 页面:状态卡、配置表单、按钮、预览 iframe |
/usr/share/rpcd/acl.d/luci-app-routerreport.json | 权限声明(同时决定菜单是否可见) |
/etc/config/routerreport | UCI 配置 |
/usr/bin/router-report-apply | 渲染脚本:UCI → 运行配置 + 维护 crontab 行 |
install.sh / deploy.sh | 一键安装 / 一键部署(电脑侧发起) |
装完在 LuCI 的「服务」菜单下就多了一个入口,页面上是这些东西:收件人、发件人、发送时间、告警阈值、SMTP 主机端口账号密码,加四个按钮——保存并应用、立即测试发送、生成预览、补发指定日期。



坑 ②:两个 UCI 节同名,整个配置直接不可解析
我最初把配置写成这样,看着很自然:
config report 'main'
option recipient 'you@qq.com'
config smtp 'main' # ← 名字也叫 main
option host 'smtp.qq.com'
结果 uci show 直接报错:Parse error: section of different type overwrites prior section with same name。UCI 里两个节的名字必须唯一,跟类型无关。
要命的是它的表现方式:uci -q set 是静默失败的——安装脚本一路打印"已把现有配置迁移进 UCI",界面上却全是空值,看起来像是"读不到配置",很难联想到是节名冲突。修法就是把节名改成唯一(main / smtp),并在安装脚本里加一道硬自检:
uci -q show routerreport >/dev/null 2>&1 || { echo "配置无法解析,已中止安装"; exit 1; }
# 写入后回读确认,别信"命令没报错"
echo "自检 recipient='$(uci -q get routerreport.main.recipient)'"
坑 ③:菜单不显示,因为 ACL 键名对不上
新版 LuCI(JS 界面)会按 rpcd 的 ACL 过滤菜单。控制器里 entry(...) 上挂的 acl_depends 名字,必须和 /usr/share/rpcd/acl.d/*.json 里的顶层键完全一致,差一个字符菜单就不出现——而且没有任何报错提示。
另外改完记得清菜单缓存,否则重启服务也不生效:
rm -f /tmp/luci-indexcache*
/etc/init.d/rpcd reload
/etc/init.d/uhttpd reload
坑 ④:/etc/config 的权限是"混合"的,别指望它自己安全
我原以为 UCI 配置写进去都是 600。实际在同一台机器上统计了一下:有的配置文件是 600,有的是 644——权限并不由 uci 保证。
而我的配置里存着 SMTP 授权码。所以渲染脚本和保存流程里都显式 chmod 600,不依赖默认行为:
chmod 600 /etc/config/routerreport
chmod 600 /etc/msmtprc
坑 ⑤:渲染脚本每保存一次,就往 crontab 里留一行垃圾
渲染脚本要按"开关 + 发送时间"维护 crontab 里那一行。第一版只删除含脚本路径的行,结果自己写的那行注释不在删除范围内,每保存一次就多一行注释,攒上十几次之后没人敢动那个文件。
解法很简单:给自己留一个固定标记,清理时连标记一起删。
MARK="#ROUTERREPORT-CRON"
grep -vE "$MARK|/usr/bin/router-daily-report" "$CRON" > "$CRON.new"
printf '%s 每日运行报告(由「邮件日报」应用维护,请勿手改)\n' "$MARK" >> "$CRON.new"
验收标准也很明确:连跑两次渲染,第二次必须报"无变化"。这个"幂等性测试"后来帮我挡住了一次配置漂移。
坑 ⑥:补发失败,界面却说"已发送"
日报脚本发成功后会在数据目录里留一个 .sent-日期 标记,用来防重复发送。我最初就用"这个标记存不存在"来判断手动补发的成败——于是上次成功留下的旧标记,会把这次失败报成成功。
修法是发送前先把标记删掉,发完再判定,这样标记的语义就回到"这次到底发出去了没有"。
坑 ⑦:界面上改 SMTP 主机/端口,怎么改都不生效
这个坑最隐蔽:控制器里我把 SMTP 主机和端口写进了配置的 main 节(smtp_host / smtp_port),而渲染脚本读的是 smtp 节(host / port)。界面保存提示成功、配置也真的写进去了,但发信时用的还是旧值——因为压根没人读那两行。
我是怎么发现的:直接给控制器"打桩"跑接口(伪造 HTTP 表单参数,绕过浏览器调用真实代码路径),然后 uci show 对比,一眼看到 main 节下多出两个不该存在的选项。修完顺手加了"历史误写自动清理",并在保存后回读校验。

部署步骤(三步,可直接抄)
1. 放采集与日报脚本
# 采集脚本 + 日报脚本
scp router-metrics router-daily-report root@路由器IP:/usr/bin/
ssh root@路由器IP 'chmod +x /usr/bin/router-metrics /usr/bin/router-daily-report'
# crontab:两条任务,5 分钟采集 + 每天 8 点发报
ssh root@路由器IP 'cat >> /etc/crontabs/root <<"EOF"
*/5 * * * * /usr/bin/router-metrics
0 8 * * * /usr/bin/router-daily-report
EOF
/etc/init.d/cron restart'
2. 配好 SMTP 凭据
ssh root@路由器IP 'cat > /etc/msmtprc <<"EOF"
account default
host smtp.qq.com
port 465
tls on
tls_starttls off
auth on
user 你的QQ号@qq.com
password 授权码
from 你的QQ号@qq.com
set_from_header true
syslog LOG_MAIL
EOF
chmod 600 /etc/msmtprc'
# 先手动跑一次,确认能发出去
ssh root@路由器IP '/usr/bin/router-daily-report --date 今天 --force'
3. 装 WebUI 应用(可选,但强烈建议)
# 在电脑上(应用目录含 root/ 与 install.sh)
sh deploy.sh --check # 只做语法检查:shell / json / Lua,不碰路由器
sh deploy.sh --install # 打包上传 → 备份旧文件 → 安装 → 清菜单缓存
安装脚本会自动做三件省事的事:把旧文件备份到 /mnt/data/<应用名>-backup/<时间戳>/、首次安装把现有运行配置回填进 UCI(打开界面就是当前值,不用重填)、清掉菜单缓存。装完打开 LuCI 的「服务 → 邮件日报」即可。
怎么用:四个按钮 + 一个阈值
| 操作 | 干了什么 | 什么时候用 |
|---|---|---|
| 保存并应用 | 写 UCI → 渲染运行配置与 msmtprc → 按开关/时间维护 crontab → 重启 cron | 改配置后 |
| 立即测试发送 | 用今天的数据跑一次真实发送(--force) | 刚配好 SMTP、想确认通不通 |
| 生成预览 | 只渲染正文不发信,直接显示在页面里 | 调版式、看措辞 |
| 补发这一天 | 对指定日期强制重发 | 某天没收到、或想回看历史 |

告警阈值是这一版新加的:默认 1,意思是"只要有 1 条需关注异常,标题就带 ⚠️"。如果你觉得日常噪音还是偏多,把它调到 5 或 10,标题就不再天天报警,但正文里的明细照旧保留——阈值只影响"定级",不影响"取证"。
我踩的 9 个坑,汇总成表
| # | 现象 | 根因 | 解法 |
|---|---|---|---|
| 1 | 邮件一封都发不出去,返回 500 | 正文单行 1089 字节 > RFC 5321 的 998 字节上限 | 每条提示独立成行 + 发送前按 900 字节折行(只折正文) |
| 2 | 界面全是空值,安装脚本却说迁移成功 | UCI 两个节同名 → 整个配置不可解析,且 uci -q set 静默失败 | 节名唯一 + 安装时做解析自检与回读 |
| 3 | 菜单里找不到应用 | acl_depends 名字与 acl.d 顶层键不一致 | 对齐键名 + 清 /tmp/luci-indexcache* |
| 4 | 担心凭据泄露 | /etc/config 权限 600/644 混杂,uci 不保证 | 落盘后显式 chmod 600 |
| 5 | crontab 里注释越攒越多 | 渲染脚本只删业务行,不删自己写的注释 | 固定标记 + 连标记一起清理;连跑两次应报 unchanged |
| 6 | 补发失败却提示"已发送" | 用上次成功留下的 .sent 标记判断本次结果 | 发送前删标记,发完再判定 |
| 7 | 改 SMTP 主机/端口不生效 | 写进了 main 节,渲染脚本读的是 smtp 节 | 打桩调接口 + uci show 对比,修正落点并清理误写 |
| 8 | 点了按钮"没反应" | 结果只输出在页面最底部,按钮旁边什么都没有 | 按钮上方加结果横幅(成功/失败/进行中)+ 按钮禁用态 + 明细折叠 |
| 9 | 页面元素贴着左边缘,很丑 | 主题的 .cbi-section-node 横向内边距是 0 | 自定义块统一补 padding: 0 1rem,与表单行对齐 |
说实话,这 9 条里有 5 条属于同一类问题:"命令没报错,所以你以为成功了"。UCI 静默失败、acl 不匹配、写错配置节、标记复用——它们全都不会给你红字提示。所以后来我养成了一个习惯:任何写操作,写完必须回读一次再打印出来。
顺便聊聊验证:不登录 WebUI 也能验大部分东西
LuCI 页面要登录才能看到,而我又不想每次都靠"人肉点一遍"来验证。摸索出一套能在命令行里跑完的验证组合,效果出奇地好:
- 给控制器打桩跑接口:伪造 HTTP 表单参数直接调用真实动作函数,能覆盖参数校验、错误分支、JSON 是否合法——上面坑 ⑦ 就是靠这个抓出来的,比点按钮覆盖得还全。
- 渲染脚本幂等测试:连跑两次,第二次必须"无变化"。
- 业务链路快照对比:安装前把
/etc/msmtprc、crontab 拷到/tmp,装完cmp逐字节比对——确认"只装应用、没动业务"。 - 本地复刻渲染截图:把真实主题 CSS + 真实页面容器结构拉下来,本地渲染我的页面片段,用无头浏览器截图。不用登录就能看到像素级效果,坑 ⑨ 就是这么发现并当场改掉的。
安全提醒(家用软路由必看)
- 管理面绝不映射公网。LuCI 与 SSH 只在内网可达,WAN 侧策略是"零放行";需要外网访问走 WireGuard。
- IPv6 要单独想一遍。现在运营商普遍下发公网 IPv6,而且没有 NAT 兜底——IPv4 那边"内网地址外网不可达"的天然保护在 IPv6 不存在。我这条踩过坑(有专门的投诉记录),所以防火墙 WAN 侧的拒绝策略是底线,不能动。
- 授权码当密码对待。文件 600、不进 Git、不写进任何脚本注释。
- WebUI 走 HTTP 明文(内网),所以别在里面用你在别处也用过的密码。
常见问题
日报发不出去,第一步查什么?
先看 msmtp 的日志尾。有明确服务器返回码的(比如 500、535)直接按码排查;535 一般是授权码错或没开 SMTP 服务;如果日志里什么都没有,说明脚本根本没跑到发信那一步,检查 crontab 与脚本权限。
不做 WebUI,只用命令行行不行?
完全可以,前两步就是完整可用的系统。WebUI 解决的是"改配置要会 SSH"这件事,如果你只有自己用,可以先跳过。
会不会很吃路由器性能?
几乎不。采集是 5 分钟一次的读文件操作,发报一天一次,CPU 占用可以忽略;真正需要考虑成本的是监控组件本身(比如我另外装的 Netdata 走的是"低开销 + 关闭 health 检查"的配置)。相比之下,日志分析比数据采集重得多,所以脚本里对日志做了白名单过滤。
数据会不会因为重启丢掉?
不会——前提是把数据放在持久分区(我的做法是 /mnt/data)而不是默认的 overlay 或 /tmp。OpenWrt 系里 /tmp 是内存盘、重启即空,很多"历史数据莫名消失"都是这个原因。
这套东西能移植到别的路由器吗?
脚本本身是 POSIX shell + busybox 工具集,理论上任何 OpenWrt 都能跑;界面用的是 LuCI 通用的控制器与模板接口,需要注意的只有主题样式差异。下一步我打算把它打成 .apk / .ipk 包,那样就能在软件包页里一键安装、也能分享给别人用了。
小结
整套东西的成本比我预想的低:两个 shell 脚本 + 一个周末写出来的界面。收益却很直接——现在家里的网络状况是"推"给我的,而不是等我"想起来去看"。而且我把它做得足够透明:每条异常都有出处(哪条日志、哪个服务),每条提示都有解释,所以它既不会漏报,也不至于天天喊狼来了。
如果你也在折腾家用软路由,建议从"每天一封邮件"这种小目标开始。等你习惯了数据主动来敲门,就很难回去了。

Comments NOTHING