Skip to content

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:

  1. 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
  2. Actual service operations go through separate TCP connections to ports advertised in the Handshake Services dict
  3. Our SPIKE backend provides only the greeting on [::1]:34719 and nothing at any Services port
  4. 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)

  1. D.6.5a — decode the port 50367 byte stream from the held-local pcap. Write a decoder against pymobiledevice3's lockdown.py protocol 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.
  2. D.6.5b — check whether the iPhone's Handshake Services dict advertises port 50367 as one of the 62 services. If yes, cross-reference which service (lockdown? something else?). Our fixture iphone_replay_bytes.py has the same Services dict zeroed-at-source for redaction — we can decode the real one from the iter-10 pcap once to confirm the mapping, then document.
  3. 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

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:

  1. scp /tmp/iosmux-d10-check-state.sh havoc:/tmp/iosmux-d10-check-state.sh
  2. ssh havoc bash /tmp/iosmux-d10-check-state.sh
  3. scp /tmp/iosmux-d10-swap-to-stock.sh havoc-root:/tmp/iosmux-d10-swap-to-stock.sh
  4. ssh havoc-root bash /tmp/iosmux-d10-swap-to-stock.sh
  5. scp /tmp/iosmux-d10-start-tcpdump.sh havoc-root:/tmp/iosmux-d10-start-tcpdump.sh
  6. ssh havoc-root bash /tmp/iosmux-d10-start-tcpdump.sh
  7. scp /tmp/iosmux-d10-trigger.sh havoc:/tmp/iosmux-d10-trigger.sh
  8. ssh havoc bash /tmp/iosmux-d10-trigger.sh
  9. scp /tmp/iosmux-d10-stop-tcpdump.sh havoc-root:/tmp/iosmux-d10-stop-tcpdump.sh
  10. ssh havoc-root bash /tmp/iosmux-d10-stop-tcpdump.sh
  11. scp /tmp/iosmux-d10-swap-to-spike.sh havoc-root:/tmp/iosmux-d10-swap-to-spike.sh
  12. ssh havoc-root bash /tmp/iosmux-d10-swap-to-spike.sh
  13. scp havoc-root:/tmp/iosmux-d10-real.pcap /tmp/iosmux-d10-real.pcap
  14. bash /tmp/iosmux-d10-move-pcap.sh to move to backups
  15. /home/op/venvs/iosmux-research/bin/python3 /tmp/iosmux-d10-decode-v2.py for the flow-table decode
  16. /home/op/venvs/iosmux-research/bin/python3 /tmp/iosmux-d10-udid-scan.py for 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)".