极空间 OTA 升级体系:双通道、按型号隔离与自更新保护

2026/05/22

基于 upgrader 服务配置、构建脚本、文件系统布局的逆向分析 分析日期:2026-05-22


一、核心结论:双通道 + 按型号隔离 + 自更新保护

极空间的 OTA 体系有三个关键设计:

  1. 服务固件和系统固件是两个独立通道,互不干扰
  2. 每个设备型号(T020/Z022/Z423/x86)各自独立升级,同一个 Go 二进制在 ARM 和 x86 上不是同一个包
  3. upgrader 本身也可以自我更新,有独立的回滚保护
升级服务器: upgrade.zenithspace.net + proxy.zenithspace.net
                │
    ┌───────────┴───────────┐
    │                       │
    ▼                       ▼
T020_SERVICE            T020_SYSTEM
(应用 + Go 服务)         (内核 + rootfs)
版本: V1.2.0200482      版本: ZOS V1.2.0200068
存于 app_version        存于 /etc/os-release

二、upgrader 服务架构

核心进程

upgrader 是一个 10MB 的 Go 二进制,运行在后台(-d=true),监听在:

接口方式说明
/upgrade/Unix Socket (/dev/shm/<bootid>.upgrader.socket)本地 Web 管理界面通过 nginx 代理
:8012TCP fallbacksocket 不可用时降级到 TCP

nginx 配置体现了一个精巧的设计:

# 优先走 Unix socket(零开销)
set_by_lua_block $socket_upgrader {
    return "unix:/dev/shm/"..ngx.shared.dmrwuris:get("bootid")..".upgrader.socket"
}

location ~ /upgrade/ {
    proxy_pass http://$socket_upgrader;   # 主路径
    error_page 502 = @upgrader_fallback;  # 失败降级
}

location @upgrader_fallback {
    proxy_pass http://127.0.0.1:8012;     # TCP 降级
}

bootid 每次重启变化,所以 socket 名是动态的。这保证了不会连接到旧进程的残留 socket。

配置要点

{
  "app_id": "T020_UPGRADER",
  "port": 8012,
  "upgrade_srv_addr": "https://upgrade.zenithspace.net",
  "srv_conf_url": "https://proxy.zenithspace.net/platform/config/upgrade.t020",
  "upgrade_cache_path_list": [
    "/zspace/update",          // eMMC 系统盘
    "/zspace/zsrp/upgrader"    // NVMe RAID1 数据盘
  ],
  "check_service_list": ["storage", "zfilev2", "zalbumv2", "sftp"],
  "upgrade_backup_path": "/zspace/update/bak/upgrader",
  "version_check_duation": 600  // 每 600 秒检查一次更新
}

三、服务固件升级(T020_SERVICE)

升级粒度:服务级别

服务固件升级不是整个系统的镜像,而是逐个服务的包更新。

每个服务目录下都有 filehash.zspace.upgrade.json:

{
  "service_md5": {
    "go-baseset": "d9f2280fc205846c10b79bf0be4d73b2",
    "install.sh": "faa25c6c7940f930d70538c673a39726",
    "start.sh": "f7892b257385cc6ece15583eabd40093",
    ...
  }
}

升级流程(推断)

1. upgrader 每 600 秒向 upgrade.zenithspace.net 查询
   POST /platform/config/upgrade.t020
   Body: { "app_id": "T020_SERVICE", "current_version": "V1.2.0200482" }

2. 服务器返回更新信息:
   {
     "version": "V1.2.0200500",
     "packages": [
       { "service": "go-baseset", "url": "...", "md5": "..." },
       { "service": "zfilev2",   "url": "...", "md5": "..." }
     ]
   }

3. upgrader 检查磁盘空间,按优先级选择缓存路径:
   - 优先 /zspace/zsrp/upgrader (NVMe, 20GB)
   - 降级 /zspace/update (eMMC, 12GB)
   - 历史版本也有 /tmp/zfsv2/... (存储池)

4. 下载包到缓存路径 → MD5 校验 → 解压到临时目录

5. 按 filehash.zspace.upgrade.json 逐文件校验:
   for each file in package:
       if md5(file) != expected_md5:
           abort update

6. 检查服务状态(check_service_list 中的服务必须健康)
   → 如果 storage/zfilev2/zalbumv2/sftp 异常,拒绝升级

7. 停止目标服务 → 替换文件 → 启动服务
   (通过 systemctl stop/start)

8. 更新版本号到 /zspace/system/version/app_version

关键:数据安全

服务升级不会动用户数据。原因:

服务升级只替换 /zspace 下的文件,永远不会碰到用户数据分区。

安装脚本的角色

大多数 Go 服务的 install.sh 只有一行:

#!/bin/sh
exit 0

这说明 OTA 升级不是通过 install.sh 执行的。install.sh 只用于首次出厂安装或手动恢复。OTA 升级时,upgrader 直接管理文件替换(可能通过 ztools copy_dir 或直接的文件操作)。

少数有实质内容的 install.sh(如 samba 171 行、zfirewall 104 行)处理的是 systemd 注册、配置初始化等首次安装才需要的操作。升级时这些不需要重复执行。


四、系统固件升级(T020_SYSTEM)

这是真正危险的操作

系统固件升级涉及内核、rootfs、驱动等底层组件。版本号存储在 /etc/os-release:

ZOS_VERSION="V1.2.0200068"

与服务的差异

服务固件系统固件
版本文件/zspace/system/version/app_version/etc/os-release
升级内容Go 二进制 + 脚本 + 配置内核 + rootfs + 系统库
影响范围单个服务整个系统
回滚替换旧文件即可需要双分区或完整备份
用户数据安全天然安全(不同分区)需要特殊保护

系统升级的关键线索

1. 单独的分区设计就是为了系统升级

eMMC 分区:
p6 14GB → /         根文件系统
p7 128MB            recovery 相关分区
p8 12GB → /zspace   应用层(独立于根文件系统)
p10 64MB            新内核暂存区 (source_kernel)
p11 1.8GB           新 rootfs 暂存区 (source_rootfs)

第 2 篇《底层系统》已从分区标签证实了 p10/p11 的用途:系统升级采用 Recovery + Source Staging 模型,而非 A/B 双分区。

2. system_update.sign 文件

upgrader 的 reset.sh 中明确删除了 system_update.sign——这说明系统升级有签名验证,且是一次性的(升级完成后删除签名文件防止重复执行)。

3. 确认机制

系统更新不会自动执行,需要用户手动确认:

// local.conf
"default_block": {
    "system_block": 1,    // 默认阻止系统自动升级
    "service_block": 1    // 默认阻止服务自动升级
}

两个 block 都是 1(阻止),意味着即使用户开启了自动升级(global_auto_upgrade: 1),也需要额外确认才能执行。

4. Android 引导痕迹的用途

之前发现的 androidboot.mode=normal 说明引导流程支持多种模式。系统升级时:


五、平台差异化升级

型号隔离

upgrader 配置中硬编码了设备型号:

{
  "app_id": "T020_UPGRADER",
  "srv_conf_url": "https://proxy.zenithspace.net/platform/config/upgrade.t020",
  "local_conf": {
    "applications": [
      { "type": "system",  "app_id": "T020_SYSTEM" },
      { "type": "service", "app_id": "T020_SERVICE" }
    ]
  }
}

upgrade.t020 这个 URL 后缀说明每个型号有独立的升级配置:

设备系统更新 ID服务更新 ID升级配置 URL
T2T020_SYSTEMT020_SERVICEupgrade.t020
Z2Z022_SYSTEMZ022_SERVICEupgrade.z022
Z4Z423_SYSTEMZ423_SERVICEupgrade.z423
x86??upgrade.x86 (推测)

为什么要按型号隔离?

因为即使是同一个 Go 服务,ARM 和 x86 的二进制是不同的(交叉编译产物):

build.sh z2   → GOARCH=arm64 → 产物给 T020_SERVICE
build.sh      → GOARCH=amd64 → 产物给 x86_SERVICE

所以 T020_SERVICE 的更新包里是 arm64 二进制,x86_SERVICE 的包里是 amd64 二进制。同一个版本号在两个平台上是不同的物理文件。

OTA 包目录结构证实了这一点

/zspace/update/pkg/
├── T020_SERVICE/     ← T2 设备的服务更新包
├── T020_SYSTEM/      ← T2 设备的系统更新包
├── Z022_SERVICE/     ← Z2 设备的服务更新包
└── Z022_SYSTEM/      ← Z2 设备的系统更新包

没有"通用包"的概念,每个型号各一套。


六、自更新与回滚保护

upgrader 的自我更新

upgrader 是唯一一个需要自己升级自己的组件。设计了三层保护:

第一层:双版本共存

/usr/zfrpc/zfrpc              ← rootfs 内置版本
/zspace/applications/services/zfrp/zfrpc  ← OTA 可更新版本

启动时比较两个版本的版本号,用新的。

第二层:完整备份

/zspace/update/bak/upgrader/  ← 升级前备份整个 upgrader 目录

第三层:看门狗恢复

# check_upgrader.sh(由 cron 定期执行)
if upgrader 进程挂了 AND 备份目录存在:
    删除当前 upgrader
    从 bak/upgrader 恢复
    重启 upgrader

这就是原子替换 + 看门狗回滚模式:

  1. 下载新版 upgrader → 备份旧版到 bak → 替换二进制 → 重启
  2. 如果新版崩溃 → cron 看门狗检测到进程消失 → 从 bak 恢复旧版
  3. 如果恢复也失败 → 至少还有 /usr/zfrpc/ 的出厂版本兜底

为什么其他服务不需要这么复杂?

因为 upgrader 是基础设施——它挂了就没法升级任何东西。其他服务挂了最多影响一个功能(相册打不开),upgrader 挂了影响整个系统的可持续性。


七、数据安全保证

分区隔离是最根本的安全机制

┌──────────────────────────────────────────────┐
│ eMMC (系统盘,升级会改动)                       │
│                                              │
│ /            → 根文件系统(系统升级改动)         │
│ /zspace      → 应用层(服务升级改动)            │
│ /zspace/.../logs → 日志(独立分区,不受影响)     │
└──────────────────────────────────────────────┘

┌──────────────────────────────────────────────┐
│ NVMe RAID1 (数据盘,升级不碰)                   │
│                                              │
│ /zspace/zsrp → 数据库/配置(运行时数据)         │
│ /data_n001   → 用户数据(btrfs + 快照保护)     │
└──────────────────────────────────────────────┘

用户数据 /data_n001 在升级过程中从未被以可写状态访问——upgrader 的缓存路径只有 emmc 和 zsrp,从不包含 data_n001。

服务升级的安全措施

  1. MD5 校验:每个文件下载后和部署前各校验一次
  2. 存储就绪检查:storage_complete_file: /tmp/zfsv3/.zfsv3 必须存在才允许升级,防止存储池未就绪时误操作
  3. 关键服务健康检查:storage、zfilev2、zalbumv2、sftp 必须正常运行才升级
  4. 磁盘空间检查:emmc_disk_check_path: /zspace/update 保证有足够空间缓存更新包

系统升级的额外保护

  1. 用户确认:system_block: 1 阻止自动系统升级
  2. 签名验证:system_update.sign 一次性签名文件
  3. 分区级回滚:eMMC 有专门的 backup/recovery 分区(见第 2 篇)
  4. 版本号不可逆检查:防止降级攻击

八、升级前检查清单

从配置中可以看到 upgrader 在升级前会检查:

// upgrader 的 pre-upgrade 检查项(从代码行为推断)
{
  "disk_space": "emmc_disk_check_path 所在分区剩余 > 包大小 * 2",
  "storage_ready": "/tmp/zfsv3/.zfsv3 标记文件存在",
  "service_health": "check_service_list 中的服务都正常运行",
  "version_valid": "目标版本 > 当前版本",
  "package_integrity": "所有文件 MD5 匹配 filehash.json",
  "user_consent": "service_block 和 system_block 设置允许",
  "upgrader_self_ok": "upgrader 自身进程正常(看门狗检查)"
}

所有检查通过后才开始实际的文件替换。


九、完整流程图

用户点击"检查更新" 或 upgrader 定时触发 (每 600s)
              │
              ▼
    ┌─────────────────────┐
    │ 1. 查询升级服务器     │
    │ upgrade.zenithspace │
    │ .net                │
    └────────┬────────────┘
             │
             ▼
    ┌─────────────────────┐
    │ 2. 获取更新清单       │
    │ (哪些服务有新版本)    │
    └────────┬────────────┘
             │
    ┌────────▼────────────┐
    │ 3. 前置检查           │
    │ ├─ 磁盘空间           │
    │ ├─ 存储池就绪         │  ← 任一失败则中止
    │ ├─ 关键服务健康       │
    │ └─ 用户确认           │
    └────────┬────────────┘
             │
    ┌────────▼────────────┐
    │ 4. 下载包             │
    │ 缓存到 zsrp/upgrader │
    │ 或 /zspace/update    │
    └────────┬────────────┘
             │
    ┌────────▼────────────┐
    │ 5. MD5 逐文件校验     │
    │ 比对 filehash.json   │  ← 任一不匹配则删除包
    └────────┬────────────┘
             │
    ┌────────▼────────────┐
    │ 6. 暂停目标服务       │
    │ systemctl stop xxx   │
    └────────┬────────────┘
             │
    ┌────────▼────────────┐
    │ 7. 替换文件           │
    │ ztools copy_dir      │
    │ 或直接文件操作        │
    └────────┬────────────┘
             │
    ┌────────▼────────────┐
    │ 8. 启动目标服务       │
    │ systemctl start xxx  │
    │ + 健康检查            │
    └────────┬────────────┘
             │
    ┌────────▼────────────┐
    │ 9. 更新版本号         │
    │ app_version 或       │
    │ /etc/os-release      │
    └────────┬────────────┘
             │
    ┌────────▼────────────┐
    │ 10. 上报升级结果      │
    │ cloud_report 目录    │
    └─────────────────────┘

如果是 upgrader 自身升级,步骤 6-8 的进程管理由 singleton.sh + cron 看门狗接管。