Skip to content

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:167await 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:

92 0b b0 29 01 02 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

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:

92 0b b0 29 01 00 40 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

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:173await 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.build helpers 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.