Q4 — pymobiledevice3 server-side utilities: what exists, what's reusable¶
Status: verified — 2026-04-18
Straight source read of pymobiledevice3 upstream v9.9.1
(local clone at /home/op/dev/repos/pymobiledevice3). No live
runs, no inference — every claim below cites a specific file
and line.
TL;DR — no ready-made RemoteXPC server, but the client-side codec is fully reusable¶
- pymobiledevice3 ships no "pretend to be iPhone" server fixture.
- Its test suite contains
fake_*helpers (tests/cli/developer/dvt/sysmon/test_process_unit.pyfake_print_json,fake_create, etc.), but these are UI-callback stubs on the client side — they do NOT speak RemoteXPC over a socket. pymobiledevice3/remote/remotexpc.pyis the client. It has no server counterpart in the codebase.pymobiledevice3/cli/remote.pyhas atunneldsubcommand (TunneldRunnerat line 37 of that file), but it is a tunnel coordinator HTTP server, not a RemoteXPC endpoint.
The good news: every one of the XpcWrapper.build /
_build_xpc_object helpers in
pymobiledevice3/remote/xpc_message.py is direction-agnostic —
they produce the bytes for a whole XPC-wrapped payload regardless
of whether you intend to send as client or as server. For Q3's
incremental dialog bisection, this means we can mirror CDS's request
shapes back as responses without writing a codec from scratch.
Findings by grep¶
server / listen / bind / fixture / mock / fake in remote/ and tests/¶
grep -rnE -i "class.*Server|listen\(|accept\(|bind\(|def.*fixture|def.*mock|def.*fake" \
pymobiledevice3/remote/ tests/
Only hits: tests/cli/developer/dvt/sysmon/test_process_unit.py with
fake_print_json, fake_create, fake_prompt_selection. All of
them are pytest monkey-patches that replace UI-callback functions.
None listen on a socket.
pymobiledevice3/remote/ contains no Server class and no
asyncio.start_server call. Verified separately:
XpcWrapper.build — the reusable construct¶
pymobiledevice3/remote/remotexpc.py uses XpcWrapper.build in
three places inside _do_handshake and _open_channel:
- Line 165 — empty header-only sync frame on
ROOT_CHANNEL(flags0x0201,payload=None) - Line 181 — generic
_open_channelhelper that builds header-only frames with arbitrary flags - (Plus an implicit build call inside
send_requestfor dict-bearing frames)
The matching _build_xpc_* primitives in xpc_message.py cover
every XPC type we are likely to need:
| Helper | Handles | Line |
|---|---|---|
_build_xpc_dictionary |
dict |
245 |
_build_xpc_array |
list |
237 |
_build_xpc_string |
str |
266 |
_build_xpc_bool |
bool |
259 |
_build_xpc_data |
bytes |
273 |
_build_xpc_double |
float |
280 |
_build_xpc_uuid |
uuid.UUID |
287 |
_build_xpc_null |
None |
294 |
_build_xpc_uint64 |
XpcUInt64Type |
301 |
_build_xpc_int64 |
XpcInt64Type |
308 |
_build_xpc_object |
dispatch above by type | 315 |
These compose into whatever payload shape Q3 needs to replay or synthesize on the server side.
pymobiledevice3 remote CLI subcommands¶
Just one file. Its subcommands are client-oriented:
tunneld— theTunneldRunnerHTTP server (pymobiledevice3/tunneld/server.py). This is what we already run on havoc viascripts/iosmux-restore.sh.rsd-info,developer, etc — client commands against a live iPhone.
No serve, mock-server, or replay subcommand exists.
Tests that open sockets¶
grep -rnE "socket\(|start_server" tests/
# tests/test_service_connection.py:31: sock = socket.socket()
# tests/test_usbmux.py:7: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
Both of these are test-helper sockets that connect out, not bind. Not usable as server-side fixtures.
Implications for Q3¶
Verdict: Q3's dispatch table stays hand-crafted, but the
hand-crafting is not deep work — each response we need to
synthesize gets built via XpcWrapper.build({"size": N, "flags": F,
"payload": _build_xpc_object(PY_DICT)}), where the PY_DICT is
whatever Python shape we want to serialize. The hard work is
deciding what to reply with, not how to encode it.
For the generic-handshake phase (the three frames captured in Q1), a minimal mirror looks like:
from pymobiledevice3.remote.xpc_message import XpcWrapper, XpcFlags
# Reply to ROOT_CHANNEL empty-dict request (mirror shape):
reply_root_empty = XpcWrapper.build({
"size": 0, "flags": XpcFlags.ALWAYS_SET, "payload": None
})
# Reply to REPLY_CHANNEL INIT_HANDSHAKE (mirror flag):
reply_reply_init = XpcWrapper.build({
"size": 0,
"flags": XpcFlags.ALWAYS_SET | XpcFlags.INIT_HANDSHAKE,
"payload": None,
})
These bytes can be fed into our spike listener's send path as the response-side dispatch for the already-captured handshake frames. CDS should then proceed to its first pair-specific request, which Q3 will characterize.
What is NOT here — the honest gap¶
There is no tested-on-real-iPhone reference for what the pair-flow specific responses look like. Beyond the generic handshake phase, Q3 must source each reply from either:
- a live server capture (Q2 — plaintext on utun4 ideally), or
- a post-TLS decryption (Q5 — if Q2 shows TLS), or
- halt on a named gap if neither is feasible.
pymobiledevice3's client code does NOT encode what the server sends after the handshake (only what the server sends it, processed via parse, not the reverse). So while the codec is reusable, the protocol knowledge past the handshake still has to come from live observation.