用 Debian VM 搭建 iPhone Wi‑Fi 自动备份服务

作者: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;
  • 设备连接层:本地编译安装的 usbmuxd2libimobiledevice
  • 控制层: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 对普通上网没问题,但不适合这种设备发现和配对场景。

私有工具链,而不是替换系统包

为了避免污染系统环境,我没有替换系统里的 usbmuxdlibimobiledevice,而是把整套工具链编译安装到项目自己的目录中,例如:

~/iphone_backup/_local

构建顺序大致是:

libplist
libgeneral
libimobiledevice-glue
libusbmuxd
libtatsu
libimobiledevice
usbmuxd2

运行时再通过 LD_LIBRARY_PATHPATH 和 systemd unit 显式指向这个私有 prefix。

这样做的好处是边界清楚:系统包仍然保持原样,备份服务使用自己的工具链。以后排查、升级或回滚都更可控。

构建过程中还遇到了 Debian 13 / GCC 14 下的一个编译问题:usbmuxd2 某处断连日志使用了带 varargs 的错误路径,GCC 14 无法通过。最后把这条日志改成固定字符串,只损失了 client fd 这个日志细节,不影响断连处理行为。

第一次配对:USB 不通时怎么做

第一次配对时,我先测试了好几个让 VM 直接连接 iPhone 的方法,但都无法稳定拿到 USB 直通。既然 VM 直接拿 USB 这条路走不通,就换了一个间接方案:先借助本地电脑完成 USB 可见性,再把本地 usbmuxd socket 转发给备份 VM 使用。

具体流程是:

  1. 先把 iPhone 插到本地电脑上;
  2. 本地电脑确认 USB 下能看到设备;
  3. 通过 SSH 把本地 usbmuxd socket 转发到备份 VM;
  4. 在备份 VM 上完成 pairing validation;
  5. 关闭临时转发;
  6. 之后让 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 备份目录;文档中也只保留脱敏后的配置形态,不记录真实地址、密码、私钥或完整设备标识。

最后的经验

这次折腾下来,最重要的经验不是某一条命令,而是几个边界判断:

  1. 系统自带 usbmuxd 不等于可用的 Wi‑Fi 备份方案;
  2. Wi‑Fi 备份依赖局域网发现,VM 应该使用桥接网络;
  3. 自编译工具链要收口到私有 prefix,不要污染系统包;
  4. NAS 权限必须从备份 VM 视角验证;
  5. iOS 私有 Wi‑Fi 地址会影响 pairing record;
  6. plist 这类结构化状态文件不能靠 grep;
  7. MQTT 控制面不能被长时间备份任务阻塞;
  8. 家庭服务器的开机网络状态要按“不可靠”设计。

最终,这套服务已经形成了一个可用闭环:远程触发、状态查询、防重复执行、日志追踪、开机恢复和成功状态确认。

相比手动插线备份,这个方案搭建成本确实更高。但一旦跑稳,iPhone 备份就从一个容易被忘记的手动动作,变成了家庭服务器里一个可以长期运行的基础服务。