基于 upgrader 服务配置、构建脚本、文件系统布局的逆向分析 分析日期:2026-05-22
一、核心结论:双通道 + 按型号隔离 + 自更新保护
极空间的 OTA 体系有三个关键设计:
- 服务固件和系统固件是两个独立通道,互不干扰
- 每个设备型号(T020/Z022/Z423/x86)各自独立升级,同一个 Go 二进制在 ARM 和 x86 上不是同一个包
- 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 代理 |
:8012 | TCP fallback | socket 不可用时降级到 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/applications/services/(eMMC 系统盘) - 用户数据在
/data_n001(NVMe RAID1 数据盘) - 数据库文件在
/zspace/zsrp/(NVMe RAID1,另 20GB 分区) - 三个物理分区完全独立
服务升级只替换 /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 说明引导流程支持多种模式。系统升级时:
- 下载新系统镜像到备用分区
- 设置引导标记,下次重启进入 recovery
- recovery 模式下完成分区切换
- 这需要 bootloader 支持(Rockchip 的 Android BSP 原生支持该机制)
五、平台差异化升级
型号隔离
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 |
|---|---|---|---|
| T2 | T020_SYSTEM | T020_SERVICE | upgrade.t020 |
| Z2 | Z022_SYSTEM | Z022_SERVICE | upgrade.z022 |
| Z4 | Z423_SYSTEM | Z423_SERVICE | upgrade.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
这就是原子替换 + 看门狗回滚模式:
- 下载新版 upgrader → 备份旧版到 bak → 替换二进制 → 重启
- 如果新版崩溃 → cron 看门狗检测到进程消失 → 从 bak 恢复旧版
- 如果恢复也失败 → 至少还有
/usr/zfrpc/的出厂版本兜底
为什么其他服务不需要这么复杂?
因为 upgrader 是基础设施——它挂了就没法升级任何东西。其他服务挂了最多影响一个功能(相册打不开),upgrader 挂了影响整个系统的可持续性。
七、数据安全保证
分区隔离是最根本的安全机制
┌──────────────────────────────────────────────┐
│ eMMC (系统盘,升级会改动) │
│ │
│ / → 根文件系统(系统升级改动) │
│ /zspace → 应用层(服务升级改动) │
│ /zspace/.../logs → 日志(独立分区,不受影响) │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ NVMe RAID1 (数据盘,升级不碰) │
│ │
│ /zspace/zsrp → 数据库/配置(运行时数据) │
│ /data_n001 → 用户数据(btrfs + 快照保护) │
└──────────────────────────────────────────────┘
用户数据 /data_n001 在升级过程中从未被以可写状态访问——upgrader 的缓存路径只有 emmc 和 zsrp,从不包含 data_n001。
服务升级的安全措施
- MD5 校验:每个文件下载后和部署前各校验一次
- 存储就绪检查:
storage_complete_file: /tmp/zfsv3/.zfsv3必须存在才允许升级,防止存储池未就绪时误操作 - 关键服务健康检查:storage、zfilev2、zalbumv2、sftp 必须正常运行才升级
- 磁盘空间检查:
emmc_disk_check_path: /zspace/update保证有足够空间缓存更新包
系统升级的额外保护
- 用户确认:
system_block: 1阻止自动系统升级 - 签名验证:
system_update.sign一次性签名文件 - 分区级回滚:eMMC 有专门的 backup/recovery 分区(见第 2 篇)
- 版本号不可逆检查:防止降级攻击
八、升级前检查清单
从配置中可以看到 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 看门狗接管。