Skip to content

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)

50 52 49 20 2A 20 48 54 54 50 2F 32 2E 30 0D 0A
0D 0A 53 4D 0D 0A 0D 0A

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)

00 00 0C 04 00 00 00 00 00 00 03 00 00 00 64 00
04 00 10 00 00

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-info round-trip. Dropping the HTTP/2 frame headers and running the DATA payloads through XpcWrapper.parse (the same pipeline Q1 used on the CDS spike capture) yields an authoritative dispatch table for at least the rsd-info handshake. 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 the XpcWrapper magic 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.