Homelab 网络系列:自动化和可恢复性:订阅生成、健康检查、配置同步、回滚

系列导航

  1. Homelab 网络系列:开篇 - 家庭网络为什么会变复杂
  2. Homelab 网络系列:RouterOS 主网关和 WRT 旁路由为什么要分工
  3. Homelab 网络系列:透明代理 VIP、VRRP 和 Fallback DNS
  4. Homelab 网络系列:NetBird ACL:Mobile Devices / MacBook 如何访问 Homelab / Hometown / Office 网络
  5. Homelab 网络系列:Backup Path:ZeroTier P2P 和 Sing-box Inbound 为什么还留着
  6. 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.shsubscribe-refresh.shrestart-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。

它的顺序不是随便排的:

  1. 先检查目标 WRT 是否可 SSH、是否有 /etc/sing-box、是否能执行 Sing-box、是否有 Init 脚本
  2. 再跑订阅生成
  3. 检查生成出来的 Gateway Config 是否存在且非空
  4. 部署前再做一次 WRT Preflight
  5. 分发新配置到目标 WRT
  6. 在目标 WRT 上执行 sing-box check
  7. 备份旧配置,替换新配置,重启服务
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

这个 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 是否存在
  • 客户端:proxyproxy-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。

这些机制合在一起,才让前面几篇的网络设计可维护。否则再好的拓扑,最后都会败给一次漏同步、一次错误重启,或者一次没有证据的手工修复。