Q2 — utun4 capture: plaintext HTTP/2 (h2c), no TLS¶
Status: verified — 2026-04-18
Pcap captured on havoc's utun4 interface during a live
pymobiledevice3 remote rsd-info round-trip that returned a full
iPhone property dictionary. Raw capture (31 packets, 17 406 bytes)
is held locally at /tmp/iosmux-q2-utun-capture.pcap on the Linux
host. It is not committed to the repo — the iPhone's rsd-info
reply embeds the device UDID in cleartext inside the combined
HTTP/2 DATA frames of packet 24, and per the iosmux personal-data
policy pcaps containing device-identifying bytes stay local.
Verdict¶
The tunnel between the pymobiledevice3 client and the iPhone, as seen
on havoc's utun4 interface (the tunneld-created virtual interface
with local address [tunnel-ULA]::2, remote [tunnel-ULA]::1),
carries plaintext HTTP/2 in prior-knowledge mode (h2c). No TLS
record layer is present. The very first application-layer byte the
client sends is the HTTP/2 connection preface PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n,
and the iPhone's first reply is a standard HTTP/2 SETTINGS frame
(type 0x04, no length prefix ambiguity, no TLS wrapper). This
matches the framing already documented for iosmux's own spike in
FINDINGS.md — the bytes on the wire to the real
iPhone are indistinguishable at the HTTP/2 layer from what
CoreDeviceService speaks to iosmux's spike listener.
The RemoteXPC wrapper magic 0x290bb092 is visible in packet 24's
DATA frame bodies (little-endian byte sequence 92 0B B0 29),
confirming that on top of h2c the iPhone speaks the same RemoteXPC
envelope layout that pymobiledevice3/remote/xpc_message.py
parses.
Evidence — first 64 bytes of the first payload each direction¶
Captured via tshark -r /tmp/iosmux-q2-utun-capture.pcap -Y 'frame.number==N' -T fields -e tcp.payload.
No redaction was needed in either hex dump — both are pure framing
bytes; neither contains the UDID, the VM hostname, nor the tunnel
IPv6 address. (The UDID only appears deeper inside packet 24, in the
rsd-info response body, which is why the pcap itself stays out of
the repo.)
Client → iPhone — packet 4, first application-layer payload (24 bytes)¶
ASCII: PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n — the 24-byte HTTP/2
connection preface (RFC 7540 §3.5). Subsequent client packets
immediately speak SETTINGS[0], WINDOW_UPDATE[0], HEADERS[1], DATA[1]
— plain h2c, no upgrade dance.
iPhone → client — packet 21, first iPhone application-layer payload (21 bytes)¶
Parsed as HTTP/2 frame: length=0x00000C (12), type=0x04
(SETTINGS), flags=0x00, stream_id=0x00000000. Payload contains
two SETTINGS entries: SETTINGS_MAX_CONCURRENT_STREAMS (id 0x03) =
100 and SETTINGS_INITIAL_WINDOW_SIZE (id 0x04) = 0x00100000. A TLS
record header would start with 0x16 0x03 0x0X (Handshake,
TLSv1.0/1.1/1.2/1.3) or 0x17 0x03 0x0X (ApplicationData); neither
is present.
Supporting: packet 24 — first 64 bytes of the combined iPhone response¶
00 00 04 08 00 00 00 00 00 00 0F 00 01 00 00 00
04 01 00 00 00 00 00 00 00 01 04 00 00 00 01 00
00 2C 00 00 00 00 00 01 92 0B B0 29 01 00 00 00
14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Interleaved HTTP/2 frames: WINDOW_UPDATE[0] (length 4, type 0x08),
SETTINGS ACK[0] (length 0, type 0x04, flags 0x01), HEADERS[1]
(length 1, type 0x01, flags 0x04 END_HEADERS) and the opening
DATA[1] whose body starts with the RemoteXPC XpcWrapper magic
92 0B B0 29 (little-endian 0x290bb092). This is the exact same
magic iosmux's captured session-01 already documents in
Q1-decode.md.
Implications for Q3 / Q5¶
- Q5 is not needed. The tunnel is not TLS-encrypted; there is no key material to extract, no SSLKEYLOGFILE path to explore, no site-packages recv() hook to write.
- Q3 replies can be synthesized directly from this capture. The
held-local pcap gives us genuine iPhone-side HTTP/2 + RemoteXPC
responses to a real
rsd-inforound-trip. Dropping the HTTP/2 frame headers and running the DATA payloads throughXpcWrapper.parse(the same pipeline Q1 used on the CDS spike capture) yields an authoritative dispatch table for at least thersd-infohandshake. Additional iPhone commands (mounter list --tunnel,developer dvt ls /, etc.) can be re-captured with the same method to extend Q3's dispatch table without any hand-crafted bytes. - Cross-check against Q1. The RemoteXPC magic seen here
(
0x290bb092) is identical to theXpcWrappermagic Q1 decoded from CDS's captured stream — confirming both ends of the tunnel speak the same RemoteXPC envelope format. This rules out any TLS-termination-masquerading-as-plaintext scenario.
How to reproduce¶
All commands below ran on the Linux host; havoc and havoc-root
are SSH host aliases (key auth, no password prompt).
# 1. Confirm tunneld is up and pick the tunnel endpoint.
ssh havoc 'curl -sm 2 http://127.0.0.1:49151/'
# -> {"<UDID>":[{"tunnel-address":"[tunnel-ULA]::1",
# "tunnel-port":<PORT>,
# "interface":"fe80::.../en0"}]}
# 2. Find the matching utun interface on havoc (the one carrying
# [tunnel-ULA]::2 as its local inet6 address).
ssh havoc 'ifconfig | grep -E "^utun|inet6 fd"'
# 3. Start tcpdump on havoc as root (havoc-root already == root,
# no sudo needed), filtered to the tunnel endpoint.
ssh havoc-root 'nohup tcpdump -i utun4 -s 0 -U \
-w /tmp/iosmux-q2-utun-capture.pcap \
"host [tunnel-ULA]::1 and port <PORT>" \
> /tmp/iosmux-q2-tcpdump.log 2>&1 &'
# 4. Trigger one rsd-info round-trip.
ssh havoc '/Users/[vm-user]/pymobiledevice3-venv/bin/pymobiledevice3 remote rsd-info'
# 5. Stop tcpdump and copy the pcap to the Linux host (/tmp only,
# do NOT copy it into the repo tree).
ssh havoc-root 'pkill -INT -f "tcpdump -i utun4"'
scp havoc-root:/tmp/iosmux-q2-utun-capture.pcap /tmp/iosmux-q2-utun-capture.pcap
# 6. Classification: inspect first client and first iPhone payload.
tshark -r /tmp/iosmux-q2-utun-capture.pcap -n \
-Y 'frame.number==4' -T fields -e tcp.payload # HTTP/2 preface
tshark -r /tmp/iosmux-q2-utun-capture.pcap -n \
-Y 'frame.number==21' -T fields -e tcp.payload # SETTINGS frame
Bracketed [tunnel-ULA]::1, <PORT>, <UDID> redacted per the
personal-data policy; the real values are recorded in the pcap that
stays on /tmp and in the terminal transcript.