Homelab 网络系列:自动化和可恢复性:订阅生成、健康检查、配置同步、回滚
系列导航
- Homelab 网络系列:开篇 - 家庭网络为什么会变复杂
- Homelab 网络系列:RouterOS 主网关和 WRT 旁路由为什么要分工
- Homelab 网络系列:透明代理 VIP、VRRP 和 Fallback DNS
- Homelab 网络系列:NetBird ACL:Mobile Devices / MacBook 如何访问 Homelab / Hometown / Office 网络
- Homelab 网络系列:Backup Path:ZeroTier P2P 和 Sing-box Inbound 为什么还留着
- Homelab 网络系列:自动化和可恢复性:订阅生成、健康检查、配置同步、回滚
背景
前面几篇讲的是网络形状:RouterOS 和 WRT 怎么分工,透明代理 VIP 怎么降级,NetBird 和 Backup Path 怎么分层。
这些设计如果只停留在文档里,很快就会变成另一个问题:配置一多,人手操作迟早会漏。
Homelab 网络里真正危险的改动通常不是“大改架构”,而是一些很小的日常操作:
- 订阅更新后,WRT 上的 Sing-box 配置没有同步
- 配置生成成功,但目标机器上的 Sing-box 版本不支持某个字段
- WRT 服务重启了,但 VRRP Health 还没恢复
- RouterOS 的 DHCP Option-set 改了,旧 Route 没有清理
- 远端站点 Route 能出去,但回程没有跟上
所以最后一篇不继续加功能,而是讲怎么让这些东西可恢复。
我的目标不是“全自动无人值守”,而是把高频操作变成有 Preflight、有校验、有备份、有回滚入口的流程。
自动化不是直接改生产
我不喜欢把自动化理解成“脚本一跑,生产就变了”。
这套网络里,自动化更像三层:
| 层级 | 负责什么 | 失败时应该怎样 |
|---|---|---|
| 生成层 | 从订阅和模板生成配置 | 失败就不部署 |
| 部署层 | 分发配置、目标端校验、重启服务 | 失败就停在当前可用配置 |
| 运行层 | Health Check、VRRP、Scheduler、日志 | 触发降级或留下排障证据 |
这也是为什么 subscriber.sh、subscribe-refresh.sh、restart-sing-box.sh 没有合成一个不可拆的大脚本。
平时用完整刷新入口;排障时可以拆开:
- 只生成配置
- 只检查生成结果
- 只分发并重启 Sing-box
- 只看 WRT Health
- 只跑 RouterOS Route Sync
自动化必须能拆开看,才有恢复价值。
订阅生成是控制面
订阅生成这部分跑在 Repo 侧控制面里,不直接等同于 WRT Runtime。
输入大概有几类:
- Provider 和 Bundle 定义
- Sing-box /
sing-box-compat/ Mihomo / Stash 模板 - 静态节点和运行时选择策略
- Operator 本地环境和输出目录
输出默认落到静态目录里:
- Sing-box Gateway / Client Configs
- Sing-box Compatibility Configs
- Mihomo GUI / Client Configs
- Stash GUI / Client Configs
- Raw 和 Logs 辅助输出
当前 WRT Runtime Data-plane 用的是 Sing-box。Mihomo 和 Stash 仍然是 Active Generated Output Families,但它们不是 WRT 上的主数据面服务。
这个边界很重要。否则很容易在排障时把“生成了某个 GUI 配置”和“网关上的 Sing-box Runtime 已经生效”混成一件事。
部署链路
日常更新走的是一个 Preflighted Refresh。
它的顺序不是随便排的:
- 先检查目标 WRT 是否可 SSH、是否有
/etc/sing-box、是否能执行 Sing-box、是否有 Init 脚本 - 再跑订阅生成
- 检查生成出来的 Gateway Config 是否存在且非空
- 部署前再做一次 WRT Preflight
- 分发新配置到目标 WRT
- 在目标 WRT 上执行
sing-box check - 备份旧配置,替换新配置,重启服务
sequenceDiagram participant Cron as PVE 10.1.1.100 Cron / Operator participant Refresh as subscribe-refresh.sh participant WRT as Homelab / Office-JT WRT participant Generator as subscriber.sh participant Static as Generated Static Outputs participant Service as Sing-box Service Cron->>Refresh: Start Refresh Refresh->>WRT: Preflight Before Generation WRT-->>Refresh: SSH + Sing-box Runtime OK Refresh->>Generator: Generate Configs Generator->>Static: Write Gateway/Client Outputs Refresh->>Static: Validate Gateway Config Exists Refresh->>WRT: Preflight Before Restart Refresh->>WRT: Copy config.json.new WRT->>WRT: Sing-box Check config.json.new WRT->>Service: Backup Current Config, Replace, Restart Service-->>Refresh: Restart Result
sequenceDiagram participant Cron as PVE 10.1.1.100 Cron / Operator participant Refresh as subscribe-refresh.sh participant WRT as Homelab / Office-JT WRT participant Generator as subscriber.sh participant Static as Generated Static Outputs participant Service as Sing-box Service Cron->>Refresh: Start Refresh Refresh->>WRT: Preflight Before Generation WRT-->>Refresh: SSH + Sing-box Runtime OK Refresh->>Generator: Generate Configs Generator->>Static: Write Gateway/Client Outputs Refresh->>Static: Validate Gateway Config Exists Refresh->>WRT: Preflight Before Restart Refresh->>WRT: Copy config.json.new WRT->>WRT: Sing-box Check config.json.new WRT->>Service: Backup Current Config, Replace, Restart Service-->>Refresh: Restart Result
sequenceDiagram participant Cron as PVE 10.1.1.100 Cron / Operator participant Refresh as subscribe-refresh.sh participant WRT as Homelab / Office-JT WRT participant Generator as subscriber.sh participant Static as Generated Static Outputs participant Service as Sing-box Service Cron->>Refresh: Start Refresh Refresh->>WRT: Preflight Before Generation WRT-->>Refresh: SSH + Sing-box Runtime OK Refresh->>Generator: Generate Configs Generator->>Static: Write Gateway/Client Outputs Refresh->>Static: Validate Gateway Config Exists Refresh->>WRT: Preflight Before Restart Refresh->>WRT: Copy config.json.new WRT->>WRT: Sing-box Check config.json.new WRT->>Service: Backup Current Config, Replace, Restart Service-->>Refresh: Restart Result
sequenceDiagram participant Cron as PVE 10.1.1.100 Cron / Operator participant Refresh as subscribe-refresh.sh participant WRT as Homelab / Office-JT WRT participant Generator as subscriber.sh participant Static as Generated Static Outputs participant Service as Sing-box Service Cron->>Refresh: Start Refresh Refresh->>WRT: Preflight Before Generation WRT-->>Refresh: SSH + Sing-box Runtime OK Refresh->>Generator: Generate Configs Generator->>Static: Write Gateway/Client Outputs Refresh->>Static: Validate Gateway Config Exists Refresh->>WRT: Preflight Before Restart Refresh->>WRT: Copy config.json.new WRT->>WRT: Sing-box Check config.json.new WRT->>Service: Backup Current Config, Replace, Restart Service-->>Refresh: Restart Result
这条链路最重要的点,是新配置必须先在目标端 sing-box check 通过,才会替换旧配置。
也就是说,订阅生成成功不代表部署成功;部署成功也不是“文件拷过去就行”。目标机器上的二进制版本、配置字段、服务脚本都必须在链路里被验证。
远端执行不要污染环境
订阅生成有两种运行模式。
本地开发时使用 Project Mode,通过项目环境运行,方便测试和开发。
远端或 Cron 路径使用 Ephemeral Mode,通过临时执行环境运行,避免在远端 Checkout 里长期留下 .venv 之类的状态。
这点看起来很小,但对可恢复性很有价值。
Homelab 的自动化脚本经常跑在 PVE Host 或远端管理机上。如果每次 Cron 都可能改本地虚拟环境,之后排障就很难判断问题来自代码、依赖、环境,还是一次失败的半成品更新。
所以远端路径尽量保持轻量:源码和配置是 Truth,运行环境临时解析,生成结果明确写到输出目录。
WRT Health 只让必要失败触发 VIP 降级
第三篇讲过透明代理 VIP 的 VRRP Fallback。这里补自动化视角。
WRT 上的 Health Check 分成两层:
- L1:决定是否还能持有 VIP
- L2:记录海外访问质量,但不直接让 WRT 释放 VIP
L1 看的是基础能力:
- Sing-box 进程状态
- DNS 配置是否符合预期
- 国内 DNS Lookup
- TUN 是否存在
- Nft Redirect 是否存在
- 可选的国内 HTTP Smoke
L2 看的是海外 DNS、Proxy DNS Smoke、海外 HTTP Smoke。它可以标记 overseas_degraded,但只要 L1 还过,就不应该把 VIP 让给 RouterOS。
这个分层背后的取舍是:海外路径不稳定时,不能轻易让所有代理客户端从 WRT 切到 RouterOS Fallback。RouterOS Fallback 解决的是 WRT / Sing-box 基础不可用,不解决所有海外质量问题。
Health Check 还要防止自己变成故障源。单次探测有 Timeout,Keepalived 的 Track Script 也有 Timeout;慢探测会被限制,不应该一直占着下一轮检查。
RouterOS Manifest 是主网关侧的 Truth
RouterOS 这边不能靠手工记忆。
Homelab RouterOS 的 Active State 由 Manifest 描述,里面包括:
- System Scripts
- Schedulers
- PPP Profile
- PPPoE Profile Binding
- 要删除的旧脚本、旧 Scheduler、旧文件
例如 NetBird DHCP Route Sync 在 RouterOS 侧是一个明确的 System Script 和 Scheduler:
- RouterOS 定时读取 WRT
10.1.1.254上的 NetBird Route Table - 只接受私有网段 Route
- 过滤掉 Homelab 自己的 LAN
- 生成 DHCP Option 121/249
proxy-netbird客户端拿到 Site Routes,下一跳指向10.1.1.254
这里 RouterOS 是 DHCP 控制点,不是 Overlay Data Plane。这个边界由脚本和 Scheduler 固化下来,比“我记得之前手动改过”可靠。
sequenceDiagram participant RouterOS as RouterOS 10.1.1.1 participant Manifest as Repo Manifest participant WRT as WRT 10.1.1.254 participant DHCP as DHCP Option-set participant Client as proxy-netbird Clients Manifest->>RouterOS: Install Scripts and Schedulers RouterOS->>WRT: Every 10m Read NetBird Route Table WRT-->>RouterOS: Private Site Routes RouterOS->>RouterOS: Filter Local LAN and Unchanged Payload RouterOS->>DHCP: Update Option 121/249 When Changed DHCP-->>Client: Site Routes -> 10.1.1.254
sequenceDiagram participant RouterOS as RouterOS 10.1.1.1 participant Manifest as Repo Manifest participant WRT as WRT 10.1.1.254 participant DHCP as DHCP Option-set participant Client as proxy-netbird Clients Manifest->>RouterOS: Install Scripts and Schedulers RouterOS->>WRT: Every 10m Read NetBird Route Table WRT-->>RouterOS: Private Site Routes RouterOS->>RouterOS: Filter Local LAN and Unchanged Payload RouterOS->>DHCP: Update Option 121/249 When Changed DHCP-->>Client: Site Routes -> 10.1.1.254
sequenceDiagram participant RouterOS as RouterOS 10.1.1.1 participant Manifest as Repo Manifest participant WRT as WRT 10.1.1.254 participant DHCP as DHCP Option-set participant Client as proxy-netbird Clients Manifest->>RouterOS: Install Scripts and Schedulers RouterOS->>WRT: Every 10m Read NetBird Route Table WRT-->>RouterOS: Private Site Routes RouterOS->>RouterOS: Filter Local LAN and Unchanged Payload RouterOS->>DHCP: Update Option 121/249 When Changed DHCP-->>Client: Site Routes -> 10.1.1.254
sequenceDiagram participant RouterOS as RouterOS 10.1.1.1 participant Manifest as Repo Manifest participant WRT as WRT 10.1.1.254 participant DHCP as DHCP Option-set participant Client as proxy-netbird Clients Manifest->>RouterOS: Install Scripts and Schedulers RouterOS->>WRT: Every 10m Read NetBird Route Table WRT-->>RouterOS: Private Site Routes RouterOS->>RouterOS: Filter Local LAN and Unchanged Payload RouterOS->>DHCP: Update Option 121/249 When Changed DHCP-->>Client: Site Routes -> 10.1.1.254
这个 Route Sync 失败时,目标不是立刻改一堆静态路由。更好的排障顺序是看 WRT Route Table、RouterOS Script Log、DHCP Option Payload、客户端实际 Route。
回滚不是一个按钮
Homelab 的回滚更像分层恢复,而不是一个万能按钮。
Sing-box 配置部署有上一份配置备份。新配置只有在目标端 Check 通过后才会替换;替换前旧配置会保留一份。出问题时,至少知道上一份 Runtime Config 在哪里。
RouterOS 侧有 Audit / Sync / Install 流程。Install 前后会留下运行证据,Manifest 里也能表达哪些旧脚本和旧文件应该删除。这样排障时可以比较“Repo 期望状态”和“RouterOS 当前状态”,而不是靠终端历史猜。
WRT 侧的 Health 状态写到临时状态文件,关键状态变化进系统日志。VRRP 切换时,看的是 Health 失败、Priority 变化、进入 BACKUP / MASTER 这些明确事件,而不是只看客户端说“好像断了”。
可恢复性的关键是证据链:
- 生成层知道自己生成了什么
- 部署层知道目标端有没有接受新配置
- 运行层知道为什么降级
- RouterOS 侧知道 Desired State 和 Live State 是否一致
验证
这类自动化要验证的是链路,不是单个脚本。
我会拆成几组:
- 生成:默认 Processor 是否能输出 Sing-box / Compatibility / Mihomo / Stash 结果
- 部署:WRT Preflight 是否通过,目标端
sing-box check是否通过 - 服务:新配置是否替换,Sing-box 是否重启成功
- 健康:L1 失败是否释放 VIP,L2 Degraded 是否只记录不抢 VIP
- RouterOS:Manifest Audit 是否能发现 Drift,NetBird Route Sync Scheduler 是否存在
- 客户端:
proxy和proxy-netbird拿到的 DHCP Option 是否不同
验证时还要刻意看失败路径:
- 目标 WRT 不可达时,不应该继续部署
- 生成配置为空时,不应该重启服务
- 目标端
sing-box check失败时,不应该替换旧配置 - Route Sync 读取 WRT 失败时,不应该清空已有 DHCP Route
- L2 海外探测慢时,不应该把 Keepalived Health Script 拖死
这些失败路径比 Happy Path 更能说明自动化有没有边界。
最后的结论
Homelab 网络复杂之后,自动化的价值不是少打几条命令,而是让高频操作有边界、有证据、有回滚入口。
订阅生成负责控制面输出,部署链路负责目标端校验和重启,WRT Health 负责代理 VIP 是否应该继续由 WRT 持有,RouterOS Manifest 负责主网关侧脚本和 Scheduler 的 Desired State。
这些机制合在一起,才让前面几篇的网络设计可维护。否则再好的拓扑,最后都会败给一次漏同步、一次错误重启,或者一次没有证据的手工修复。