Q1 — decoded: the 204-byte CDS → spike-listener capture¶
Status: verified — 2026-04-18
Every byte below was parsed through pymobiledevice3 v9.9.1's own
XpcWrapper / XpcPayload constructors. Input file:
results/iosmux-capture/session-01-recv.bin
(204 bytes, captured 2026-04-13 from a real CDS process).
Summary — classification¶
Every byte CDS sent before stalling is the generic RemoteXPC
connection init sequence, NOT pair-specific. The sequence is a
byte-exact match for pymobiledevice3's own client handshake in
pymobiledevice3/remote/remotexpc.py
method _do_handshake. Pair-specific traffic has not been observed
yet because our spike listener stopped at the HTTP/2 handshake and
did not reply with the XPC-level frames CDS needs to proceed.
Framing breakdown¶
The 204 bytes split into:
- HTTP/2 connection preface (24 bytes,
PRI * HTTP/2.0...) - 8 HTTP/2 frames (180 bytes total)
| # | Offset | Type | Stream | Length | Flags | What it is |
|---|---|---|---|---|---|---|
| 0 | 0x0018 | SETTINGS | 0 | 12 | 0x00 | MAX_CONCURRENT_STREAMS=100, INITIAL_WINDOW_SIZE=0x100000 |
| 1 | 0x002d | WINDOW_UPDATE | 0 | 4 | 0x00 | window_increment=983041 (DEFAULT_WIN_SIZE_INCR) |
| 2 | 0x003a | HEADERS | 1 | 0 | 0x04 | END_HEADERS, empty — opens ROOT_CHANNEL |
| 3 | 0x0043 | SETTINGS | 0 | 0 | 0x01 | ACK — acknowledging our server SETTINGS |
| 4 | 0x004c | DATA | 1 | 44 | 0x00 | XpcWrapper #0 — ROOT_CHANNEL empty-dict |
| 5 | 0x0081 | HEADERS | 3 | 0 | 0x04 | END_HEADERS, empty — opens REPLY_CHANNEL |
| 6 | 0x008a | DATA | 1 | 24 | 0x00 | XpcWrapper #1 — ROOT_CHANNEL sync flag 0x0201 |
| 7 | 0x00ab | DATA | 3 | 24 | 0x00 | XpcWrapper #2 — REPLY_CHANNEL INIT_HANDSHAKE |
DATA frame #0 — ROOT_CHANNEL empty dict (44 bytes)¶
Hex:
92 0b b0 29 01 00 00 00 14 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 42 37 13 42 05 00 00 00
00 f0 00 00 04 00 00 00 00 00 00 00
Parsed (pymobiledevice3.remote.xpc_message.XpcWrapper.parse):
Container(
magic=0x290bb092, # XpcWrapper magic
flags=Container(ALWAYS_SET=True, ...), # only ALWAYS_SET (bit 0)
message=Container(
message_id=0,
payload=Container(
magic=0x42133742, # XpcPayload magic
protocol_version=5,
obj=Container(
type=uEnumIntegerString.new(61440, 'DICTIONARY'), # XPC_DICTIONARY (0xf000)
data=Container(count=0, entries=None), # empty dict
),
),
),
)
Source match: remotexpc.py:167 — await self.send_request({}).
This is pymobiledevice3's first call after the HTTP/2 handshake.
The wire bytes CDS emitted are byte-exact with what
XpcWrapper.build({"size": N, "flags": XpcFlags.ALWAYS_SET,
"payload": {"obj": {}, "protocol_version": 5}}) produces.
DATA frame #1 — ROOT_CHANNEL sync flag (24 bytes)¶
Hex:
Parsed:
Container(
magic=0x290bb092,
flags=Container(ALWAYS_SET=True, PING=False, DATA_PRESENT=False,
WANTING_REPLY=False, REPLY=False,
FILE_TX_STREAM_REQUEST=False,
FILE_TX_STREAM_RESPONSE=False,
INIT_HANDSHAKE=False),
message=Container(message_id=0, payload=None), # header-only
)
The raw flag field is 0x00000201 (little-endian bytes 01 02 00 00),
which combines ALWAYS_SET (0x1) with an unnamed 0x200 bit.
pymobiledevice3's XpcFlags enum does not have a named constant
for 0x200, which is why the parser reports all named flags as
False despite the raw bit being set.
Source match: remotexpc.py:165
DataFrame(stream_id=ROOT_CHANNEL,
data=XpcWrapper.build({"size": 0, "flags": 0x0201,
"payload": None}))
pymobiledevice3 uses 0x0201 as a raw magic value here. It is almost
certainly a "wanting-sync" or header-sync bit, but formally
undocumented in their source. Do not assume semantics without
further evidence. For Q3 purposes, emitting the same raw value back
is the safe replay path.
DATA frame #2 — REPLY_CHANNEL INIT_HANDSHAKE (24 bytes)¶
Hex:
Parsed:
Container(
magic=0x290bb092,
flags=Container(ALWAYS_SET=True, INIT_HANDSHAKE=True, ...others False),
message=Container(message_id=0, payload=None),
)
Flag value 0x00400001 = XpcFlags.ALWAYS_SET | XpcFlags.INIT_HANDSHAKE.
Source match: remotexpc.py:173 —
await self._open_channel(REPLY_CHANNEL, XpcFlags.INIT_HANDSHAKE).
The _open_channel helper ORs in ALWAYS_SET internally and then
builds a header-only XpcWrapper on the given stream ID.
Implications for Q3¶
- The dialog so far is generic, not pair-specific. CDS has not yet sent any byte that identifies this connection as "the one for the pair request". It has only completed the RemoteXPC connection init.
- Server-side replay is mechanically trivial for this phase —
the same
XpcWrapper.buildhelpers in pymobiledevice3 produce the correct bytes when we need to mirror a server's handshake reply. See Q4 for the reusable helpers. - Next-roundtrip characterization is still open. Once our spike listener replies at the XPC level (empty dict on stream 1, INIT_HANDSHAKE ack on stream 3), CDS should proceed to send its first pair-specific command. Those bytes are the subject of Q3.
- The 0x200 "unnamed" flag is a known unknown. Not a blocker for Q3 — we mirror the raw value. But worth naming as a small gap for any future session that needs to alter flag semantics.
How to reproduce¶
Research venv is at /home/op/venvs/iosmux-research with
pymobiledevice3 installed editable from
/home/op/dev/repos/pymobiledevice3 (pinned at upstream v9.9.1).
The decoder script used for this writeup is NOT committed (one-shot
research tool), but reproducing takes 10 lines:
from pymobiledevice3.remote.xpc_message import XpcWrapper
raw = open("results/iosmux-capture/session-01-recv.bin", "rb").read()
# strip 24-byte HTTP/2 preface, walk 9-byte frame headers,
# pick type=0x00 DATA frames, pass payload to XpcWrapper.parse().
See also Q4-server-side.md which summarizes the pymobiledevice3 source patterns that are reusable for Q3's dispatch table.