作者:Felix(Haro 协作整理)
我一直想把 iPhone 备份这件事从“想起来才插线一次”变成一个可以长期运行的自动化服务:手机在家、网络可达、条件满足时,就可以远程触发备份;备份过程可查询;失败时也能通过日志定位问题。
最后做成的方案是:在 NAS 上运行一台 Debian VM,通过 Wi‑Fi 发现 iPhone,使用本地编译的 libimobiledevice / usbmuxd2 执行备份,再把备份数据写入 NAS 的 SMB 共享目录。控制面则通过 MQTT 完成远程触发和状态回报。
为什么不是直接用系统自带 usbmuxd
一开始我并不是直接选择 usbmuxd2,而是先测试了 Linux 发行版自带的 usbmuxd / libimobiledevice 路线。
这条路线在传统 USB 有线连接场景下没有问题,但无法在 iPhone 使用 Wi‑Fi 的场景下工作。也就是说,系统自带 usbmuxd 可以满足有线连接,但无法支撑这次目标里的 iPhone Wi‑Fi 备份。
所以最终方案切换到了 usbmuxd2:让它负责 Wi‑Fi 场景下的 iPhone 发现和连接,再让新版 libimobiledevice 工具通过这个本地 usbmuxd2 实例执行备份。
usbmuxd2 本身资料不算多,本机编译和问题排查过程中,我使用了 Codex 辅助分析依赖关系、构建顺序和编译错误。它比较适合这类“资料分散、源码链条长、需要边试边修”的工程问题。
整体架构
整套系统可以拆成四层:
- 备份主机:运行在 NAS 上的 Debian VM;
- 设备连接层:本地编译安装的
usbmuxd2和libimobiledevice; - 控制层:Python MQTT worker,负责接收备份请求、返回状态;
- 存储层:NAS 上的 SMB 共享目录。
数据流大概是这样:
MQTT request
-> backup worker
-> usbmuxd2 + libimobiledevice
-> iPhone over Wi‑Fi
-> NAS SMB backup directory
这里有一个很重要的前提:VM 必须使用桥接网络。iPhone Wi‑Fi 备份依赖 Bonjour / mDNS 发现,备份 VM 和 iPhone 需要处在同一个局域网广播环境里。NAT 对普通上网没问题,但不适合这种设备发现和配对场景。
私有工具链,而不是替换系统包
为了避免污染系统环境,我没有替换系统里的 usbmuxd 或 libimobiledevice,而是把整套工具链编译安装到项目自己的目录中,例如:
~/iphone_backup/_local
构建顺序大致是:
libplist
libgeneral
libimobiledevice-glue
libusbmuxd
libtatsu
libimobiledevice
usbmuxd2
运行时再通过 LD_LIBRARY_PATH、PATH 和 systemd unit 显式指向这个私有 prefix。
这样做的好处是边界清楚:系统包仍然保持原样,备份服务使用自己的工具链。以后排查、升级或回滚都更可控。
构建过程中还遇到了 Debian 13 / GCC 14 下的一个编译问题:usbmuxd2 某处断连日志使用了带 varargs 的错误路径,GCC 14 无法通过。最后把这条日志改成固定字符串,只损失了 client fd 这个日志细节,不影响断连处理行为。
第一次配对:USB 不通时怎么做
第一次配对时,我先测试了好几个让 VM 直接连接 iPhone 的方法,但都无法稳定拿到 USB 直通。既然 VM 直接拿 USB 这条路走不通,就换了一个间接方案:先借助本地电脑完成 USB 可见性,再把本地 usbmuxd socket 转发给备份 VM 使用。
具体流程是:
- 先把 iPhone 插到本地电脑上;
- 本地电脑确认 USB 下能看到设备;
- 通过 SSH 把本地
usbmuxdsocket 转发到备份 VM; - 在备份 VM 上完成 pairing validation;
- 关闭临时转发;
- 之后让 VM 上的
usbmuxd2通过 Wi‑Fi 发现 iPhone。
这个方法的关键是:配对记录最终要落在备份 VM 可用的位置,后续 Wi‑Fi 连接才能由 VM 自己完成。
另一个隐藏坑是 iOS 的“私有 Wi‑Fi 地址”。这个地址是按 SSID 生成的,并且可能变化。如果 mDNS 能看到 iPhone,但 idevice_id -n 不显示设备,就要检查当前广播出来的 Wi‑Fi MAC 是否和配对记录里的 WiFiMACAddress 一致。不一致时,需要刷新配对记录或重新配对。
NAS 存储:能挂载不等于能备份
备份目标目录挂载在:
/srv/iphone-backups
一开始容易以为 SMB 共享能 mount 就算完成了,但实际不是。
iPhone 增量备份会读取已有 manifest,也会遍历、移动或清理旧的 hashed block 目录。如果专用 SMB 用户只能创建新文件,却没有权限读取或写入旧备份目录,备份可能会在后期失败或卡住。
所以 NAS 权限一定要从备份 VM 侧验证,而不是只看 NAS 管理界面。至少要抽样检查旧备份目录是否可以 list 和 write。
另外,SMB 挂载使用 systemd automount,而不是开机时硬挂载。这样可以避免系统刚启动时网络未就绪导致的 Network is unreachable,真正访问备份目录时再触发挂载,更适合家庭服务器这种开机时序不完全可控的环境。
MQTT Worker:控制面不能被备份阻塞
备份 worker 使用 Python 和 paho-mqtt 编写,支持远程请求,例如:
test
status
backup
force-backup
普通 backup 会检查 iPhone 是否外接电源;force-backup 只绕过外接电源检查,但仍然要求 NAS 挂载可用、iPhone 在 Wi‑Fi 上可见。
这里修过两个关键问题。
第一个是成功判断。旧实现把 Status.plist 当文本搜索,结果一次实际成功的备份被误报失败。后来改成用 Python plistlib 解析 plist,并检查:
SnapshotState == "finished"
这才是可靠的成功判断。
第二个是执行模型。idevicebackup2 运行时间可能很长,如果直接在 MQTT 回调里执行,会阻塞整个 MQTT 主循环。备份期间再发 status,worker 就像卡死一样。
现在的做法是把备份命令放到后台线程中执行,MQTT 主循环保持响应。备份运行时查询状态会返回:开始时间、进程 PID、日志路径、日志大小、电源策略等信息。如果重复发送备份请求,也不会启动第二个备份,而是返回当前运行中的任务状态。
开机恢复和安全边界
实际运行中还遇到过开机后 DNS 暂时不可用的问题。worker 第一次启动时解析 MQTT Broker 失败,直接退出。后来在 worker 内部加入了连接重试:失败后每 5 秒重试,直到网络和 DNS 可用。
配合 SMB automount 后,系统对开机时序的依赖少了很多。
安全上,MQTT 密码和 SMB 密码都放在 root-only 文件里,不出现在命令行参数中;SMB 使用专用低权限用户,只授权 iPhone 备份目录;文档中也只保留脱敏后的配置形态,不记录真实地址、密码、私钥或完整设备标识。
最后的经验
这次折腾下来,最重要的经验不是某一条命令,而是几个边界判断:
- 系统自带
usbmuxd不等于可用的 Wi‑Fi 备份方案; - Wi‑Fi 备份依赖局域网发现,VM 应该使用桥接网络;
- 自编译工具链要收口到私有 prefix,不要污染系统包;
- NAS 权限必须从备份 VM 视角验证;
- iOS 私有 Wi‑Fi 地址会影响 pairing record;
- plist 这类结构化状态文件不能靠 grep;
- MQTT 控制面不能被长时间备份任务阻塞;
- 家庭服务器的开机网络状态要按“不可靠”设计。
最终,这套服务已经形成了一个可用闭环:远程触发、状态查询、防重复执行、日志追踪、开机恢复和成功状态确认。
相比手动插线备份,这个方案搭建成本确实更高。但一旦跑稳,iPhone 备份就从一个容易被忘记的手动动作,变成了家庭服务器里一个可以长期运行的基础服务。