visar.log
Technical notes from building things
← all posts

Fixing iPhone Wireless Backup & Automating Photo Extraction

The Setup

Z440 workstation running Linux Mint with a systemd user service (iphone-backup.service) that does daily wireless iPhone backups. The stack:

  • netmuxd (jkcoxson/netmuxd) — Rust-based network muxer that discovers the iPhone via mDNS
  • libimobiledevice (idevicebackup2, ideviceinfo, idevicepair) — the actual backup tools
  • avahi-daemon — mDNS/DNS-SD for device discovery
  • Custom Python script (~/Documents/scripts/services/iphone_backup.py) — orchestrates everything: starts netmuxd, probes for the device, runs backup, monitors connectivity

The service runs at boot, checks every 5 minutes for the phone, and does one full incremental backup per day.

The Problem

Last successful backup: 2025-10-25 — 4 months ago. The service was running but every attempt failed with “Probe failed. Device is not ready.”

Debugging

Step 1: Check if mDNS discovery works

avahi-browse -r -t _apple-mobdev2._tcp

This showed the iPhone on the network. So the phone was visible — netmuxd just wasn’t picking it up.

Step 2: Run netmuxd with debug logging

RUST_LOG=debug netmuxd --disable-unix --host 127.0.0.1

First finding: netmuxd was looking up WiFiMACAddress in the pairing plist at /var/lib/lockdown/<UDID>.plist to match the mDNS-discovered device. There was another iPhone on the network (Erdal’s) with a different MAC that was confusing the investigation.

Step 3: Re-pair via USB

After correctly identifying my phone (Visars-iPhone at 5c:87:30:b4:22:d1), debug logs showed:

Error: "InvalidHostID"
Request: "StartSession"

The pairing certificates had expired after 4 months of no contact. iOS invalidated the trust relationship. Re-pairing via USB fixed it:

idevicepair pair
# Tap "Trust" on the iPhone

Step 4: Verify wireless connection

USBMUXD_SOCKET_ADDRESS=127.0.0.1:27015 ideviceinfo -n

Returned device info successfully. Restarted the service and backup began immediately at ~23 MB/s over 5GHz WiFi.

Automating Photo Extraction

The Goal

After each successful backup, automatically create a browsable folder of camera photos sorted chronologically (like the Photos app) — without needing to manually extract the backup.

How iPhone Backups Store Files

idevicebackup2 stores files as SHA-1 hashes in <UDID>/<first2chars>/<hash>. To find anything, you query Manifest.db:

SELECT fileID, relativePath FROM Files
WHERE domain = 'CameraRollDomain'
AND relativePath LIKE 'Media/PhotoData/CPLAssets/group%'
AND relativePath LIKE '%.HEIC';

This gives you mappings like:

df9b64732af57ad7c20c84fb991166a2d279d311 -> Media/PhotoData/CPLAssets/group134/0E4A44FA.HEIC

The actual file lives at <UDID>/df/df9b64732af57ad7c20c84fb991166a2d279d311.

Getting Photo Dates

Photos.sqlite is also in the raw backup (find its hash via Manifest.db). It has creation dates:

SELECT ZFILENAME, datetime(ZDATECREATED + 978307200, 'unixepoch')
FROM ZASSET
WHERE ZFILENAME IS NOT NULL AND ZFILENAME NOT LIKE 'IMG_%'
ORDER BY ZDATECREATED;

The 978307200 offset converts Apple’s Core Data timestamp (seconds since 2001-01-01) to Unix epoch.

IMG_% files are screenshots — filtered out to keep only actual camera photos.

The Solution

Added extract_camera_photos() to iphone_backup.py that:

  1. Opens Manifest.db from the raw backup
  2. Finds Photos.sqlite hash and opens it
  3. Builds a filename-to-hash map for all CPLAssets photos
  4. Queries dates from Photos.sqlite
  5. Creates symlinks in ~/iPhone-backup/camera-photos/ named YYYY-MM-DD_HH-MM-SS_<original>.HEIC

Sorting by filename in any file manager now matches the Photos app order. Runs after every successful backup, wrapped in try/except so it never breaks the backup loop.

Key Detail: CPLAssets vs DCIM

  • DCIM/100APPLE/ — screenshots and locally-shot photos with IMG_ names
  • PhotoData/CPLAssets/group/* — iCloud Photo Library synced camera photos with UUID names

The real camera photos are in CPLAssets, scattered across numbered group directories. The extraction flattens them into one folder.

Files Changed

  • ~/Documents/scripts/services/iphone_backup.py — added sqlite3 import, extract_camera_photos() function, post-backup hook
  • ~/iPhone-backup/camera-photos/ — auto-generated symlink folder
  • /var/lib/lockdown/<UDID>.plist — refreshed by USB re-pairing

Cleanup

~/iPhone-backup/extracted_backup/ (848MB) is a stale one-time extraction from Oct 2025. No longer needed — safe to delete.