Q3 iteration 0 — dispatch table sourced from Q2 pcap¶
Status: verified — 2026-04-18
All bytes below were extracted from /tmp/iosmux-q2-utun-capture.pcap
(the Q2 live capture of pymobiledevice3 remote rsd-info) and
decoded through pymobiledevice3 v9.9.1's own
XpcWrapper.parse(). No guessing, no synthesis — these are real
iPhone response bytes. Personal identifiers (UDID, serial, MAC,
UUIDs, tunnel ULA) are redacted per the iosmux personal-data
policy; ports, OS/model strings, entitlement names and service
bundle IDs are kept verbatim because they characterize the
protocol shape.
Summary¶
- Pcap: 31 packets, DLT_NULL (BSD loopback), 14530 bytes of application-layer payload across both directions.
- client→iPhone: 9 TCP-payload packets, 204 bytes total
- iPhone→client: 3 TCP-payload packets, 14326 bytes total
- HTTP/2 frames observed:
- c2s: 8 frames (
SETTINGS×2,WINDOW_UPDATE×1,HEADERS×2,DATA×3) - s2c: 10 frames (
SETTINGS×2,WINDOW_UPDATE×1,HEADERS×2,DATA×4,RST_STREAM×1) - DATA frames c2s: 3 — all generic RemoteXPC handshake
- DATA frames s2c: 4 — three mirror the c2s handshake, one is the
substantive
rsd-inforesponse - Streams observed:
1(ROOT_CHANNEL),3(REPLY_CHANNEL) - Cross-check with Q1: the c2s DATA bytes are byte-exact with the CDS capture already decoded in Q1-decode.md. pymobiledevice3's client and CoreDeviceService speak the identical RemoteXPC handshake envelope.
The critical finding¶
The iPhone's first substantive DATA frame (s2c frame #8, stream 1,
14124 bytes) carries MessageType = "Handshake" with the full
Services directory inline. It is not just a mirror of the
client's empty-dict handshake — it is both the mirror sequence (empty
dict, sync 0x0201, INIT_HANDSHAKE on stream 3) and the first
real payload that pymobiledevice3 consumes as the rsd-info reply.
The MessagingProtocolVersion is 7, the device advertises 62
services, and the top-level dict has message_id=2 (the first two
handshake frames are message_id=0). See
pymobiledevice3.remote.remotexpc.RemoteXPCConnection._do_handshake
— that method reads exactly this dict and never sends a separate
rsd-info request frame. rsd-info is the handshake.
Implication for Q3 iter-1+: a spike listener that wants CDS to
progress past "connection ready" must, after the three-frame XPC
handshake mirror, emit one additional DATA frame on stream 1 carrying
a MessageType=Handshake dictionary with at least Services,
Properties, MessagingProtocolVersion and UUID. The exact
ServiceVersion/port values do not need to match a real device — the
shape does.
Per-frame table — iPhone → client (the reference for Q3 replies)¶
iPhone frame #4 — DATA, stream=1, length=44 bytes, off=0x0034¶
Raw payload 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
Decoded (XpcWrapper.parse):
Container:
magic = 0x29B00B92
flags = Container:
ALWAYS_SET = True
message = Container:
message_id = 0x0000000000000000
payload = Container:
magic = 0x42133742
protocol_version = 0x00000005
obj = Container:
type = (enum) DICTIONARY 61440
data = Container:
count = 0x00000000
entries = None
Semantic: ROOT_CHANNEL empty-dict reply — mirrors the client's
c2s frame #3. pymobiledevice3's server-side in RemoteXPCConnection
sends this as its first post-preface DATA frame; the iPhone mirrors
it back to acknowledge the channel.
Pair to CDS client frame: byte-exact with
Q1-decode.md DATA frame #0
(session-01-recv.bin offset 0x004c).
iPhone frame #5 — DATA, stream=1, length=24 bytes, off=0x0069¶
Raw payload hex:
Decoded (XpcWrapper.parse):
Container:
magic = 0x29B00B92
flags = Container:
ALWAYS_SET = True
message = Container:
message_id = 0x0000000000000000
payload = None
Raw flags field 0x00000201 — ALWAYS_SET plus an unnamed 0x200
bit that pymobiledevice3's XpcFlags enum does not name. Exact same
byte pattern as the c2s frame and as Q1's DATA frame #1.
Pair to CDS client frame: byte-exact with
Q1-decode.md DATA frame #1 (offset 0x008a).
iPhone frame #7 — DATA, stream=3, length=24 bytes, off=0x0093¶
Raw payload hex:
Decoded (XpcWrapper.parse):
Container:
magic = 0x29B00B92
flags = Container:
ALWAYS_SET = True
INIT_HANDSHAKE = True
message = Container:
message_id = 0x0000000000000000
payload = None
Flag value 0x00400001 = ALWAYS_SET | INIT_HANDSHAKE. Sent after
the iPhone opens REPLY_CHANNEL (HEADERS frame #6, stream 3).
Pair to CDS client frame: byte-exact with
Q1-decode.md DATA frame #2 (offset 0x00ab).
iPhone frame #8 — DATA, stream=1, length=14124 bytes, off=0x00b4 — the rsd-info Handshake¶
First 64 bytes of raw payload:
92 0b b0 29 01 01 00 00 14 37 00 00 00 00 00 00
02 00 00 00 00 00 00 00 42 37 13 42 05 00 00 00
00 f0 00 00 04 37 00 00 05 00 00 00 4d 65 73 73
61 67 65 54 79 70 65 00 00 90 00 00 0a 00 00 00
XpcWrapper header fields (from those 64 bytes):
- magic
92 0b b0 29→0x290bb092 - flags
01 01 00 00→0x00000101=ALWAYS_SET | DATA_PRESENT - size
14 37 00 00 00 00 00 00→0x3714= 14100 (XPC payload size = DATA length 14124 minus the 24-byte wrapper header) - message_id
02 00 00 00 00 00 00 00→2 - XpcPayload magic
42 37 13 42, protocol_version5 - root object: type
0x0000f000= DICTIONARY, payload size0x3704= 14084, 5 top-level entries - first entry key
"MessageType"(len 5), first value type0x00009000= STRING, len 10, data starts at48 61 6e 64 73 68 61 6b 65(Handshake\0)
Too large to hex-dump in full inside a markdown frame; the parser
script at /tmp/iosmux-q3-parse-pcap.py prints the complete hex
block.
Decoded top-level shape (XpcWrapper.parse) — 5 entries:
| key | type | value (redacted where needed) |
|---|---|---|
MessageType |
STRING | "Handshake" |
MessagingProtocolVersion |
UINT64 | 7 |
Services |
DICTIONARY | 62 entries — full table below |
Properties |
DICTIONARY | 46 entries — table below |
UUID |
UUID | [redacted-16-bytes] |
Properties — 46 entries. Device-identifying values
(UniqueDeviceID, SerialNumber, EthernetMacAddress,
UniqueChipID, BootSessionUUID) are redacted; OS/model/fuse
strings are kept verbatim because they characterize the protocol
shape.
| key | type | value |
|---|---|---|
ThinningProductType |
STRING | "iPhone14,6" |
HumanReadableProductVersionString |
STRING | "26.4.1" |
HasSEP |
BOOL | true |
HWModel |
STRING | "D49AP" |
EthernetMacAddress |
STRING | "[redacted-mac]" |
EffectiveProductionStatusSEP |
BOOL | true |
ChipID |
UINT64 | 33040 |
CPUArchitecture |
STRING | "arm64e" |
AppleInternal |
BOOL | false |
HWModelDescriptionForUserVisibility |
STRING | "D49AP" |
RegionInfo |
STRING | "LL/A" |
IsUIBuild |
BOOL | true |
SupplementalBuildVersion |
STRING | "23E254" |
SigningFuse |
BOOL | true |
EffectiveSecurityModeAp |
BOOL | true |
DeviceSupportsLockdown |
BOOL | true |
RestoreLongVersion |
STRING | "23.5.254.0.0,0" |
SerialNumber |
STRING | "[redacted-serial]" |
ProductType |
STRING | "iPhone14,6" |
ProductName |
STRING | "iPhone OS" |
MobileDeviceMinimumVersion |
STRING | "1827.100.14" |
Image4CryptoHashMethod |
STRING | "sha2-384" |
SensitivePropertiesVisible |
BOOL | true |
OSVersion |
STRING | "26.4.1" |
BuildVersion |
STRING | "23E254" |
ProductTypeDescForUserVisibility |
STRING | "iPhone14,6" |
UniqueDeviceID |
STRING | "[UDID]" |
UniqueChipID |
UINT64 | [redacted-ecid] |
StoreDemoMode |
BOOL | false |
EffectiveSecurityModeSEP |
BOOL | true |
EffectiveProductionStatusAp |
BOOL | true |
DeviceColor |
STRING | "1" |
BoardId |
UINT64 | 16 |
BootSessionUUID |
UUID | [redacted-uuid] |
SecurityDomain |
UINT64 | 1 |
RegionCode |
STRING | "LL" |
ModelNumber |
STRING | "MMX83" |
DeviceEnclosureColor |
STRING | "1" |
DeviceClass |
STRING | "iPhone" |
CertificateSecurityMode |
BOOL | true |
CertificateProductionStatus |
BOOL | true |
RemoteXPCVersionFlags |
UINT64 | 72057594037927942 (0x0100000000000006) |
OSInstallEnvironment |
BOOL | false |
IsVirtualDevice |
BOOL | false |
Image4Supported |
BOOL | true |
HardwarePlatform |
STRING | "t8110" |
Services — 62 entries. Each service value is a DICTIONARY with
three keys: Port (STRING, tunneld-assigned), Entitlement (STRING)
and Properties (DICTIONARY with UsesRemoteXPC: BOOL and
optionally ServiceVersion: INT64). Port numbers are tunneld's
ephemeral TCP ports on the iPhone side of the tunnel — they change
across reboots and are not personal identifiers.
| idx | service name | Port | UsesRemoteXPC | SvcVer | Entitlement |
|---|---|---|---|---|---|
| 0 | com.apple.springboardservices.shim.remote |
55495 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 1 | com.apple.RestoreRemoteServices.restoreserviced |
55487 |
true | 2 | com.apple.private.RestoreRemoteServices.restoreservice.remote |
| 2 | com.apple.osanalytics.logTransfer |
55485 |
true | — | com.apple.ReportCrash.antenna-access |
| 3 | com.apple.crashreportmover.shim.remote |
55469 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 4 | com.apple.mobile.lockdown.remote.untrusted |
55464 |
false | 1 | com.apple.mobile.lockdown.remote.untrusted |
| 5 | com.apple.accessibility.axAuditDaemon.remoteAXService |
55455 |
true | 1 | AppleInternal |
| 6 | com.apple.mobile.notification_proxy.shim.remote |
55452 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 7 | com.apple.mobile.house_arrest.shim.remote |
55450 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 8 | com.apple.mobile.mobile_image_mounter.shim.remote |
55445 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 9 | com.apple.GPUTools.MobileService.shim.remote |
55444 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 10 | com.apple.backgroundassets.lockdownservice.shim.remote |
55434 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 11 | com.apple.mobile.insecure_notification_proxy.remote |
55492 |
true | 1 | com.apple.mobile.insecure_notification_proxy.remote |
| 12 | com.apple.sysdiagnose.remote.trusted |
55490 |
true | — | com.apple.private.sysdiagnose.remote.trusted |
| 13 | com.apple.webinspector.shim.remote |
55482 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 14 | com.apple.afc.shim.remote |
55471 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 15 | com.apple.pcapd.shim.remote |
55470 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 16 | com.apple.internal.devicecompute.CoreDeviceProxy.shim.remote |
55461 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 17 | com.apple.amfi.lockdown.shim.remote |
55435 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 18 | com.apple.mobile.lockdown.remote.trusted |
55493 |
false | 1 | com.apple.mobile.lockdown.remote.trusted |
| 19 | com.apple.PurpleReverseProxy.Conn.shim.remote |
55481 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 20 | com.apple.mobilebackup2.shim.remote |
55474 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 21 | com.apple.modelmanager.remote |
55466 |
true | — | AppleInternal |
| 22 | com.apple.mobile.notification_proxy.remote |
55465 |
true | 1 | com.apple.mobile.notification_proxy.remote |
| 23 | com.apple.carkit.remote-iap.service |
55458 |
true | — | AppleInternal |
| 24 | com.apple.crashreportcopymobile.shim.remote |
55446 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 25 | com.apple.commcenter.mobile-helper-cbupdateservice.shim.remote |
55441 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 26 | com.apple.iosdiagnostics.relay.shim.remote |
55438 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 27 | com.apple.carkit.service.shim.remote |
55486 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 28 | com.apple.mobile.assertion_agent.shim.remote |
55479 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 29 | com.apple.atc.shim.remote |
55476 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 30 | com.apple.mobile.installation_proxy.shim.remote |
55462 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 31 | com.apple.security.cryptexd.remote |
55457 |
true | 3 | com.apple.private.security.cryptexd.remote |
| 32 | com.apple.instruments.dtservicehub |
55454 |
— | — | com.apple.private.dt.instruments.dtservicehub.client |
| 33 | com.apple.mobile.heartbeat.shim.remote |
55453 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 34 | com.apple.mobilesync.shim.remote |
55448 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 35 | com.apple.mobile.MCInstall.shim.remote |
55442 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 36 | com.apple.mobile.file_relay.shim.remote |
55437 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 37 | com.apple.fusion.remote.service |
55491 |
true | 1 | com.apple.fusion.remote.service |
| 38 | com.apple.streaming_zip_conduit.shim.remote |
55473 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 39 | com.apple.atc2.shim.remote |
55468 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 40 | com.apple.idamd.shim.remote |
55467 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 41 | com.apple.syslog_relay.shim.remote |
55440 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 42 | com.apple.companion_proxy.shim.remote |
55436 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 43 | com.apple.mobile.storage_mounter_proxy.bridge |
55494 |
true | 1 | com.apple.private.mobile_storage.remote.allowedSPI |
| 44 | com.apple.corecaptured.remoteservice |
55488 |
true | — | com.apple.corecaptured.remoteservice-access |
| 45 | com.apple.dt.ViewHierarchyAgent.remote |
55484 |
true | — | com.apple.private.dt.ViewHierarchyAgent.client |
| 46 | com.apple.preboardservice.shim.remote |
55483 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 47 | com.apple.dt.remotepairingdeviced.lockdown.shim.remote |
55480 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 48 | com.apple.misagent.shim.remote |
55472 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 49 | com.apple.bluetooth.BTPacketLogger.shim.remote |
55447 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 50 | com.apple.os_trace_relay.shim.remote |
55443 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 51 | com.apple.PurpleReverseProxy.Ctrl.shim.remote |
55439 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 52 | com.apple.internal.dt.coredevice.untrusted.tunnelservice |
55489 |
true | 2 | com.apple.dt.coredevice.tunnelservice.client |
| 53 | com.apple.mobile.insecure_notification_proxy.shim.remote |
55478 |
false | — | com.apple.mobile.lockdown.remote.untrusted |
| 54 | com.apple.accessibility.axAuditDaemon.remoteserver.shim.remote |
55477 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 55 | com.apple.preboardservice_v2.shim.remote |
55475 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 56 | com.apple.remote.installcoordination_proxy |
55463 |
true | 1 | com.apple.private.InstallCoordinationRemote |
| 57 | com.apple.internal.devicecompute.CoreDeviceProxy |
55460 |
false | 1 | AppleInternal |
| 58 | com.apple.dt.remoteFetchSymbols |
55459 |
true | 1 | com.apple.private.dt.remoteFetchSymbols.client |
| 59 | com.apple.sysdiagnose.remote |
55456 |
true | — | com.apple.private.sysdiagnose.remote |
| 60 | com.apple.mobileactivationd.shim.remote |
55451 |
false | — | com.apple.mobile.lockdown.remote.trusted |
| 61 | com.apple.mobile.diagnostics_relay.shim.remote |
55449 |
false | — | com.apple.mobile.lockdown.remote.trusted |
Pair to CDS client frame: no direct CDS equivalent exists in the Q1 capture because CDS stalled before reaching this stage — this is the frame the spike listener needs to synthesize to unblock Q1's stall.
iPhone frame #9 — RST_STREAM, stream=1, length=4 bytes, off=0x37e9¶
Raw payload hex:
error_code = 0x00000005 (STREAM_CLOSED). The iPhone closes
stream 1 immediately after delivering the Handshake DATA frame on
that same stream. The RST_STREAM with STREAM_CLOSED (not
NO_ERROR, as an earlier revision of this doc mistakenly stated)
reads in RFC 7540 §7 as "a frame was received on a stream in the
half-closed (remote) or closed state" — consistent with the server
finishing its one-shot reply and signalling "I am done with this
stream, don't send anything else on it". pymobiledevice3 consumes
the Handshake dict and exits — rsd-info is a one-shot round-trip,
not a long-lived channel. Stream 3 (REPLY_CHANNEL) is left open.
Per-frame table — client → iPhone (for reference, not dispatch)¶
The client's c2s DATA frames are byte-exact with Q1's
session-01-recv.bin. Only the HTTP/2 offsets differ (the c2s
preface here is at offset 0; in Q1 the preface was also at offset 0).
c2s frame #3 — DATA, stream=1, length=44 bytes, off=0x0043¶
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
Decoded: empty XPC dictionary, ALWAYS_SET only — byte-exact match
with Q1 DATA frame #0 and with
pymobiledevice3.remote.remotexpc.RemoteXPCConnection._do_handshake's
await self.send_request({}).
c2s frame #4 — DATA, stream=1, length=24 bytes, off=0x0078¶
Header-only wrapper with flags 0x00000201 (ALWAYS_SET + unnamed
0x200). Byte-exact with Q1 DATA frame #1.
c2s frame #6 — DATA, stream=3, length=24 bytes, off=0x00a2¶
Flags 0x00400001 = ALWAYS_SET | INIT_HANDSHAKE. Sent after opening
REPLY_CHANNEL via HEADERS on stream 3. Byte-exact with Q1 DATA frame #2.
Parse refusals¶
None. All 7 DATA frames (3 c2s + 4 s2c) parsed cleanly through
pymobiledevice3.remote.xpc_message.XpcWrapper.parse. No exceptions,
no magic mismatches.
Implications for Q3 iterations 1+¶
-
The spike listener's reply sequence is completely specified by this capture. After CDS sends its three DATA frames (empty dict on stream 1, sync
0x0201on stream 1,INIT_HANDSHAKEon stream 3), the listener must emit:- HTTP/2 SETTINGS/WINDOW_UPDATE/SETTINGS-ACK/HEADERS on stream 1 (already done by Q1's spike)
- DATA on stream 1 = empty-dict XpcWrapper (44 bytes,
message_id=0) - DATA on stream 1 = sync XpcWrapper (24 bytes, flags
0x0201) - HEADERS on stream 3 opening REPLY_CHANNEL
- DATA on stream 3 =
INIT_HANDSHAKEXpcWrapper (24 bytes, flags0x00400001) - DATA on stream 1 = the
Handshakedictionary (MessageType/MessagingProtocolVersion/Services/Properties/UUID,message_id=2, flags0x00000101) - RST_STREAM on stream 1, error_code 5 (
STREAM_CLOSED)
-
message_idprogression is0, 0, 0, 2on stream 1. The sync frame (0x0201) and the Handshake DATA sharemessage_id=0andmessage_id=2respectively.message_id=1is never used in this capture — unclear whether CDS expects it on a different path (e.g. for anINIT_HANDSHAKEack) or whether the iPhone simply skips it. Known gap. Not a blocker for Q3 (mirroring the observed values is enough), but worth flagging. -
Servicesvalues (ports 554xx) can be any non-conflicting ephemeral ports. The listener does not need to match the real device — CDS validates the dict shape, not the port values. Entitlement strings and bundle IDs, however, are likely referenced by CDS code paths and should be preserved verbatim. -
Properties.RemoteXPCVersionFlags = 0x0100000000000006andMessagingProtocolVersion = 7are the version contract. A spike listener that wants to keep CDS progressing must advertise the same (or at least CDS-compatible) values. Lower values may cause CDS to reject the device as too-old. -
No pair-specific traffic is observed.
rsd-infois device discovery, not pairing. Q3's subsequent iterations must capture apymobiledevice3 pairor equivalent command to see what bytes CDS will send after the Handshake dict. That is Q3 iter-2+ scope. -
The 0x200 flag bit is still unnamed. Same gap already recorded in Q1-decode.md. Not a blocker.
How to reproduce¶
source /home/op/venvs/iosmux-research/bin/activate
pip install scapy # if not already present in the venv
python3 /tmp/iosmux-q3-parse-pcap.py
The script reads /tmp/iosmux-q2-utun-capture.pcap directly, strips
the 4-byte BSD loopback (DLT_NULL, AF_INET6) prefix, reassembles
both TCP directions by sequence number, walks HTTP/2 frames, and
prints the same table as above. The script is kept on /tmp and is
not committed (research one-shot tool). The pcap itself stays at
/tmp/iosmux-q2-utun-capture.pcap and is not committed (contains
the unredacted UDID/serial/MAC).