Phase D.6.4a findings — H2-reply FALSIFIED; real discovery is a parallel second service on iPhone:50367¶
Status: verified — 2026-04-24
Decisive reference capture via Q2 methodology. Temporarily
swapped tunneld out of SPIKE mode (back to stock, pointing at
the real iPhone tunnel), ran tcpdump on utun4, executed three
non-destructive triggers (rsd-info baseline + lockdown info +
devicectl device info details), then swapped back to SPIKE.
H2-reply (we're missing follow-up HTTP/2 frames on the
greeting stream) is FALSIFIED: real iPhone emits exactly
the iter-01 10-frame corpus and then falls silent on the
greeting HTTP/2 session — byte-identical to our SPIKE Go
backend's output. Apparatus restored cleanly: SPIKE tunneld
PID 2337 advertising [::1]:34719.
Real discovery: in stock mode, the iPhone accepts a second parallel TCP connection on iPhone:50367 carrying non-HTTP/2 bytes (appears to be raw XPC / lockdown-over-TCP, ~9.5 KB from iPhone to client). SPIKE mode does not provide this secondary service surface. That's where CDS gets stuck — not waiting for more frames from us, waiting for a second service connection that SPIKE backend doesn't host.
TL;DR¶
After iter-7 proved UUID was a session gate and iter-8/9 proved CDS doesn't back-connect to advertised Services ports, the working hypothesis was H2-reply: iPhone in real pair-flow must emit post-handshake frames that we (having only the Q2 rsd-info corpus) are missing. That hypothesis is now falsified.
Real-iPhone reference pcap shows:
- Greeting HTTP/2 session (port 52889 RSD): 10 server
frames, identical to our SPIKE output. iPhone goes silent on
stream 1 after its
#8 big Handshake. No follow-up. Same behaviour as SPIKE. - Parallel non-HTTP/2 service (port 50367): 9,532 B iPhone
→ client, 924 B client → iPhone. This is a second TCP flow
that devicectl opens in parallel with the greeting session.
SPIKE mode has no equivalent — tunneld advertises only
one
tunnel-port:34719, which goes to our backend's greeting-only handler.
This reframes the entire D.6.x investigation: CDS's "silent- post-handshake" state is not a frame-level protocol bug on the greeting stream. It's CDS waiting for a parallel service connection that our SPIKE infrastructure doesn't expose.
The capture¶
Apparatus transitions¶
| stage | tunneld mode | iPhone tunnel port | PID |
|---|---|---|---|
| pre-capture | SPIKE | 34719 (our backend) | 1913 |
| during capture | stock | 52889 (real iPhone) | 2162 |
| post-capture | SPIKE (restored) | 34719 | 2337 |
Stock tunnel ULA: fd45:3648:8491::1 (host side: ::2).
Interface: utun4, MTU 16000.
Pcap shape¶
- 64,144 B total, 249 packets captured over ~120 s window
- 28 TCP packets with payload (the substantive HTTP/2 + XPC flows)
- 175 IPv6 hop-by-hop options + 68 UDP router-discovery (kernel noise, not protocol)
- Pcap held local at
/home/op/backups/iosmux/pcaps/iosmux-d10-real.pcap, NOT committed — contains real iPhone UDID in binary form.
Flows observed¶
Four flow directions matter:
Flow A (baseline rsd-info):
client [host]:50346 → iPhone:52889 (RSD HTTP/2 greeting)
iPhone:52889 → client [host]:50346 (RSD HTTP/2 greeting, reply)
Flow B (devicectl via CDS):
client [host]:50356 → iPhone:52889 (RSD HTTP/2 greeting, second session)
iPhone:52889 → client [host]:50356
Flow C (the discovery):
client [host]:50367 → iPhone:50367 (parallel non-HTTP/2, 924 B)
iPhone:50367 → client [host]:50367 (parallel non-HTTP/2, 9532 B)
Flows A and B have byte-identical shape to the iter-01 corpus and to our SPIKE backend output (iter-⅞/9 verbose logs).
Flow C is the novelty.
Flow A / B — the HTTP/2 greeting (no surprises)¶
Both sessions, both directions, 9+10 frames exactly as iter-01 documented:
client → iPhone (204 B, 9 frames):
PREFACE + SETTINGS + WINDOW_UPDATE + HEADERS(s1) + DATA(s1,44) +
DATA(s1,24 sync) + HEADERS(s3) + DATA(s3,24 INIT_HANDSHAKE) +
SETTINGS(ACK)
iPhone → client (14,326 B, 10 frames):
SETTINGS + WINDOW_UPDATE + SETTINGS(ACK) + HEADERS(s1) +
DATA(s1,44 empty-dict) + DATA(s1,24 sync 0x0201) + HEADERS(s3) +
DATA(s3,24 INIT_HANDSHAKE mirror) + DATA(s1,14124 big Handshake
msgid=2) + RST_STREAM(s1, STREAM_CLOSED)
After the RST_STREAM, the iPhone emits nothing more on this TCP connection. No PINGs, no WINDOW_UPDATE top-ups, no new DATA, no new HEADERS on stream 3 or any other stream. The connection stays open (stream 3 remains half-open) until the client closes it.
Implication: our SPIKE backend's greeting behaviour is correct at the wire level — there is literally no more on the greeting channel to emit.
Flow C — the parallel service (port 50367)¶
Bytes on the wire, per direction:
| direction | bytes | shape |
|---|---|---|
| client → iPhone | 924 | non-HTTP/2, raw TCP payload |
| iPhone → client | 9,532 | non-HTTP/2, raw TCP payload |
This flow does NOT start with the HTTP/2 preface PRI *
HTTP/2.0\r\n\r\nSM\r\n\r\n. It's a different protocol on top
of TCP.
Likely identity: classic lockdown-over-TCP. pymobiledevice3 implements this separately from RSD — it's the legacy XPC framing that Apple used before RSD (iOS ≤16.x). iOS 17+ retains it for lockdown-level operations (pair, certificate exchange, DDI mount) while using RSD HTTP/2 for the newer service discovery and remote-XPC layer.
Confirmation would require tcpdump'ing a clean
pymobiledevice3 lockdown info flow in isolation and matching
byte patterns against pymobiledevice3's lockdown.py client code.
That's a D.6.5 task, not this iter. This iter's scientific output is: such a service exists, port 50367 in this capture, 9.5 KB of iPhone→client XPC-ish content.
Partial success of devicectl¶
devicectl device info details --device E8A190DD-... returned:
hardwareModel: iPhone14,6
marketingName: iPhone SE (3rd generation)
identifier: 00008110-0004596E22A0401E
pairingState: unpaired
tunnelState: unavailable
Error: An error occurred while communicating with a remote
process. (com.apple.dt.CoreDeviceError error 3)
The connection was interrupted. (com.apple.Mercury.error
error 1000)
Significant: hardwareModel + marketingName ARE read successfully (through the RSD greeting + Services dict lookup). But pair state + tunnel state fail with the familiar Mercury 1000 error.
This is a clean bifurcation:
- "Read device identity" → works because it's satisfied by the Handshake Properties dict (our greeting output contains these fields in the big Handshake we embed)
- "Read pair/tunnel state" → fails because it requires the lockdown-over-TCP service on port 50367 that SPIKE doesn't expose
Exactly matches iter-⅞/9 in SPIKE mode: handshake works, pair state queries fail.
UDID / identifier scan on pcap¶
| pattern | count |
|---|---|
UDID upper ASCII 00008110-0004596E22A0401E |
3 |
| UDID lower ASCII | 0 |
| devicectl UUID upper | 0 |
| devicectl UUID lower | 0 |
VM username nullweft |
0 |
| UDID binary form | 0 |
UDID embedded in the Properties dict of the Handshake DATA
frame × 2 sessions (+ one more occurrence, probably in the
parallel service flow bytes). Pcap MUST stay in
/home/op/backups/iosmux/pcaps/, is gitignored, NEVER
committed.
What this closes and what it opens¶
Closed¶
- H2-reply falsified: the iter-01 corpus IS the complete iPhone greeting output. Our backend already emits it byte- exact; nothing is missing there.
- iter-01 replay methodology validated once more: real iPhone still emits the same 10 frames as Q2 captured a month ago. No protocol drift across iOS versions for the greeting.
- CDS's silent-post-handshake state is NOT a greeting-stream problem.
Opened¶
- The parallel service surface: port 50367 in this capture
is what devicectl opens for pair/tunnel operations. SPIKE
backend does not host anything analogous. D.6.5 needs to
either (a) emulate this service in our Go backend at a
predictable port that we also advertise in Handshake
Services, or (b) pass-through this flow to the real iPhone via stock tunneld (but that defeats the SPIKE isolation). - Protocol shape of the parallel service: raw non-HTTP/2
TCP payload. Needs decoding. Likely lockdown-over-TCP (cross-
reference with
pymobiledevice3/lockdown.py) but not verified by this capture alone.
Implication for the overall Phase D plan¶
The D.6.1 UUID patch + D.6.2/D.6.3/D.6.4a findings paint a coherent picture:
- iPhone's RSD greeting (HTTP/2 on one port) is a discovery channel only — it publishes identity + service-port list + session UUID, then goes silent
- Actual service operations go through separate TCP connections to ports advertised in the Handshake Services dict
- Our SPIKE backend provides only the greeting on
[::1]:34719and nothing at any Services port - Xcode's CoreDeviceService works against this split model — reads identity from greeting, opens service connections for operations. When no service is reachable, it reports "connection interrupted" at the application layer
D.6.5 picks up the first service to emulate. Most likely candidate: lockdown (the one that port 50367 carried in this capture), since pair-state and tunnel-state queries specifically need it.
Next steps (ranked by ROI)¶
- D.6.5a — decode the port 50367 byte stream from the
held-local pcap. Write a decoder against pymobiledevice3's
lockdown.pyprotocol definitions. Identify what CDS/ devicectl sends in the first ~100 bytes of the client→iPhone direction. That's the request byte that tells us which RemoteXPC method CDS invokes first after Handshake. - D.6.5b — check whether the iPhone's Handshake
Servicesdict advertises port 50367 as one of the 62 services. If yes, cross-reference which service (lockdown? something else?). Our fixtureiphone_replay_bytes.pyhas the sameServicesdict zeroed-at-source for redaction — we can decode the real one from the iter-10 pcap once to confirm the mapping, then document. - D.6.5c (future, gated on a/b) — implement a second listener in our Go backend binding whatever port CDS expects for that service, and either proxy to stock tunneld or synthesize replies from known data.
Artifacts committed in this directory¶
findings.md— this documentindex.md— one-page summary
Raw pcap NOT committed: /home/op/backups/iosmux/pcaps/iosmux-d10-real.pcap
(64,144 B). Contains real iPhone UDID, stays host-local.
How to reproduce (script-based, no pipes/heredocs)¶
All scripts live at /tmp/iosmux-d10-*.sh and
/tmp/iosmux-d10-*.py. Execution pattern:
scp /tmp/iosmux-d10-check-state.sh havoc:/tmp/iosmux-d10-check-state.shssh havoc bash /tmp/iosmux-d10-check-state.shscp /tmp/iosmux-d10-swap-to-stock.sh havoc-root:/tmp/iosmux-d10-swap-to-stock.shssh havoc-root bash /tmp/iosmux-d10-swap-to-stock.shscp /tmp/iosmux-d10-start-tcpdump.sh havoc-root:/tmp/iosmux-d10-start-tcpdump.shssh havoc-root bash /tmp/iosmux-d10-start-tcpdump.shscp /tmp/iosmux-d10-trigger.sh havoc:/tmp/iosmux-d10-trigger.shssh havoc bash /tmp/iosmux-d10-trigger.shscp /tmp/iosmux-d10-stop-tcpdump.sh havoc-root:/tmp/iosmux-d10-stop-tcpdump.shssh havoc-root bash /tmp/iosmux-d10-stop-tcpdump.shscp /tmp/iosmux-d10-swap-to-spike.sh havoc-root:/tmp/iosmux-d10-swap-to-spike.shssh havoc-root bash /tmp/iosmux-d10-swap-to-spike.shscp havoc-root:/tmp/iosmux-d10-real.pcap /tmp/iosmux-d10-real.pcapbash /tmp/iosmux-d10-move-pcap.shto move to backups/home/op/venvs/iosmux-research/bin/python3 /tmp/iosmux-d10-decode-v2.pyfor the flow-table decode/home/op/venvs/iosmux-research/bin/python3 /tmp/iosmux-d10-udid-scan.pyfor the identifier scan
Script-only workflow — no inline pipes, no heredocs, everything matches the wildcarded permission allowlist.
Status of D.6.4a¶
- Step A (apparatus swap to stock): delivered
- Step B (reference capture): delivered, 64 KB pcap
- Step C (apparatus restore to SPIKE): delivered, tunneld PID 2337 verified on port 34719
- Step D (decode + document): this document
iter-10 closes H2-reply falsification. D.6.5 shifts focus from "missing follow-up frames" to "missing parallel service surface at port 50367 (or equivalent)".