家里的网络有个特点:出问题的时候你不在家,等你发现的时候问题已经过去了。上周我连续两天觉得晚上刷视频有点卡,但打开各种监控面板一看,曲线早被时间冲淡了,谁也说不出到底是哪个环节抖了。

我手上其实不缺监控: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 管细节,日报管"你该知道了"。

iStoreOS 软路由邮件日报示例:日报邮件标题、需关注事项与流量卡片
每天 8 点收到的日报:标题带结论,正文先给需关注事项和流量

整体设计:采集 → 汇总 → 通知,三层拆开

层干什么跑在哪产物
采集层每 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

这里有两个我特意确认过的细节,都是"不确认就会算错"的类型:

  1. 流量口径取物理 WAN 口(eth0),不取 pppoe-wan。PPPoE 重拨时虚拟接口计数器会清零,一旦算错,日报里会出现一次莫名其妙的"流量归零"。物理口只在重启时清零,口径才稳定。
  2. 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 字节——刚好越线。

修法有两层,建议都做:

  1. 根因修:每条提示独立成行,别做字符串拼接。
  2. 保险修:发送前扫一遍正文,超过 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/routerreportUCI 配置
/usr/bin/router-report-apply渲染脚本:UCI → 运行配置 + 维护 crontab 行
install.sh / deploy.sh一键安装 / 一键部署(电脑侧发起)

装完在 LuCI 的「服务」菜单下就多了一个入口,页面上是这些东西:收件人、发件人、发送时间、告警阈值、SMTP 主机端口账号密码,加四个按钮——保存并应用、立即测试发送、生成预览、补发指定日期。

iStoreOS LuCI 服务菜单里的邮件日报应用界面,含当前状态与配置表单
入口在 LuCI 的「服务 → 邮件日报」,打开就能看到当前生效的配置
邮件日报的收件人、发件人、发送时间与告警阈值配置区
收件人、发送时间、告警阈值都在这里改,保存即生效
邮件日报的 SMTP 发信配置:主机、端口、账号与授权码
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 节下多出两个不该存在的选项。修完顺手加了"历史误写自动清理",并在保存后回读校验。

uci show routerreport 显示 main 与 smtp 两个配置节,选项各自独立
修正后:main 与 smtp 两个节各自独立,SMTP 选项不再落到 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
5crontab 里注释越攒越多渲染脚本只删业务行,不删自己写的注释固定标记 + 连标记一起清理;连跑两次应报 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 脚本 + 一个周末写出来的界面。收益却很直接——现在家里的网络状况是"推"给我的,而不是等我"想起来去看"。而且我把它做得足够透明:每条异常都有出处(哪条日志、哪个服务),每条提示都有解释,所以它既不会漏报,也不至于天天喊狼来了。

如果你也在折腾家用软路由,建议从"每天一封邮件"这种小目标开始。等你习惯了数据主动来敲门,就很难回去了。