Stage S2 Phase C — self-research queue (Q1..Q5)¶
Status: draft — each question is an open empirical task. No answer is accepted into any downstream document without a raw artifact or a direct upstream source quote to back it up. Hard rule: no guessing. An answer that cannot be reached empirically on havoc or through a source read of pymobiledevice3 or Apple's documentation stays labelled UNKNOWN, and the gap gets named explicitly in the Stage S2 plan and in the final Phase C synthesis document. Only after all five questions have been exhausted do we consider the friend-capture fallback from the earlier plan version — and only for questions that genuinely require a physical Mac + iPhone pair that havoc cannot reproduce.
This file is the authoritative tracker for the five questions.
It is referenced from plan-stage2-pair-flow.md
Phase C section.
Context (what we already know — one paragraph)¶
The Stage S2.C self-experiment on havoc confirmed empirically
that CoreDeviceService speaks plain prior-knowledge HTTP/2
cleartext to the address tunneld advertises, with no TLS and no
ALPN, and that the RemoteXPC framing above HTTP/2 uses the same
XpcWrapper (magic 0x290bb092) + XpcPayload
(magic 0x42133742) byte layout that pymobiledevice3's
remote/xpc_message.py already implements. The sandbox probe
confirmed Shape B's fd-passing primitives are legal inside
CoreDeviceService. The inject LC_LOAD_DYLIB reaches the same
PID that opens the pair nw_connection. Full hex-level detail
in research/s2c-self-experiment/FINDINGS.md.
What the experiment did not tell us: what bytes the other end (the real iPhone's RSD endpoint, or the plaintext side of pymobiledevice3's tunnel) actually returns in response to CDS's first XPC payload. Our spike listener replies with just the HTTP/2 handshake and nothing at the application layer, so CDS stalls after its first real DATA frame. The five questions below are the ordered plan for closing that gap on havoc.
Q1 — Decode the captured 44-byte XPC payload¶
What we want to know: the semantic meaning of the 44 bytes
CDS sent on stream 1 (ROOT_CHANNEL) right after the HTTP/2
handshake. Specifically: which RemoteXPC message type, which
XpcFlags bits are set, and what the 20-byte body decodes to
when parsed as a bplist16 xpc_object.
Why it matters: we need to know if CDS's first move is a generic RemoteXPC handshake (in which case every future session opens the same way), a pair-specific request (in which case we are already inside the pair handshake), or something else. This classifies the entire subsequent dialog.
How to answer (empirical, no guessing):
- Take the raw bytes in
research/protocol/s2c-self-experiment/results/iosmux-capture/session-01-recv.bin. - Drop the HTTP/2 frame headers to isolate the DATA frame
payloads per stream. The 44-byte payload is the first
DATA stream=1 len=44frame. - Run the isolated payload through pymobiledevice3's own parser:
from pymobiledevice3.remote.xpc_message import XpcWrapper
# assuming `payload` is the 44-byte bytes object
wrapper = XpcWrapper.parse(payload)
# inspect wrapper.flags, wrapper.size, wrapper.message, etc.
(Exact class / method names confirmed via
pymobiledevice3/remote/xpc_message.py in the upstream
checkout.)
4. Same for the 24-byte and 24-byte DATA frames that follow on
streams 1 and 3.
Done criteria:
- A short section in a per-question findings doc
docs/research/s2c-self-experiment/Q1-decode.mdwith: - The exact pymobiledevice3 parser call used
- The decoded Python representation of each of the three DATA frames (stream 1 len=44, stream 1 len=24, stream 3 len=24)
- A classification: is this a pair request, a generic open, or something else?
- If pymobiledevice3's parser refuses any payload, that is itself an answer — document the refusal and the reason.
- The finding gets linked back from this file with status
verifiedand a link to the Q1 doc.
If Q1 reveals the classification: proceed to Q3 with a known starting point. If Q1 decodes cleanly but the classification is ambiguous: go to Q2 in parallel — getting the server side may disambiguate. If Q1 refuses to parse: something is wrong with either our byte isolation or our understanding of the framing. Halt and investigate; do not guess.
Q2 — Capture pymobiledevice3 client traffic on utun4¶
What we want to know: whether the bytes on the VM's utun4
interface, as produced by a working pymobiledevice3 remote
rsd-info / mounter list --tunnel / developer dvt ls /
command, are plaintext HTTP/2 (transparent tunnel) or
TLS-encrypted (termination is deeper than utun4).
Why it matters: if utun4 carries plaintext, we can capture the real server responses the iPhone sends over a live session and feed those straight into Q3's listener replies. That is the gold standard and it does not require any friend. If utun4 is TLS-encrypted, we need Q5 (post-TLS hook) to get plaintext — a more invasive but still self-contained fallback.
How to answer (empirical, no guessing):
- Restart havoc tunneld in stock (non-spike) mode so it
advertises the real iPhone RSD endpoint.
iosmux-restore.shalready does this; confirm viacurl -sm 2 http://127.0.0.1:49151/showing a non-::1tunnel address. - On havoc-root, start
tcpdumponutun4capturing to a pcap file, filtered to the specific tunnel IPv6 address / port pair (from the tunneld JSON response):
sudo tcpdump -i utun4 -s 0 -U -w /tmp/utun4-rsdinfo.pcap \
'host <tunnel-ipv6> and port <tunnel-port>'
- In another havoc shell run:
Let it complete, then stop tcpdump.
4. Copy the pcap to
docs/research/s2c-self-experiment/results/q2-utun4-rsdinfo.pcap
and inspect locally with tshark -r ... -V or Wireshark.
Done criteria:
- A per-question findings doc
docs/research/s2c-self-experiment/Q2-utun4-capture.mdwith: - First 64 bytes hex of the first packet client→iPhone
- First 64 bytes hex of the first packet iPhone→client
- Explicit classification: plaintext HTTP/2 (preface visible, or HTTP/2 SETTINGS frame visible) or TLS-encrypted (TLS record header 0x16 0x03 visible) or something else (note and halt).
- pcap committed to
results/as evidence (confirmed clean of personal data — typically a pcap of RSD traffic is just IPv6 plus TCP plus HTTP/2 frames, no UDIDs in cleartext, but verify before committing).
If plaintext HTTP/2: rejoice — Q3's listener replies can be synthesized directly from this capture, and Q5 is not needed. If TLS: document the TLS version/cipher/PSK hint visible in the ClientHello and proceed to Q5 as the next step. If neither: halt and investigate; do not guess the protocol.
Q3 — Incremental dialog bisection via the spike listener¶
What we want to know: the full request/response pair sequence that CDS goes through for a working pair flow, one step at a time. Each step is empirically grounded — we never reply to CDS with made-up bytes.
Why it matters: this is the actual "write down the Apple protocol" work that unblocks Phase D implementation. By advancing one step at a time and only replying with bytes we can point to a source for (from Q1, Q2, Q4, or direct upstream pymobiledevice3 reference), we walk the whole pair handshake without guessing.
How to answer:
- Extend
docs/research/s2c-self-experiment/iosmux-spike-listener.py(or replace it — user decision at that time) to keep a per-session dispatch table mapping(stream_id, message_type)to a hand-crafted response payload. For the first iteration, respond to CDS's initial stream-1 44-byte payload with the single reply that Q1+Q2 justify as "what the real server would send here". - Restart the spike environment (IOSMUX_SPIKE=1 tunneld,
listener, CDS), click Pair (or run
devicectl list devices), capture the new state. - Observe the new request CDS sends once our first reply lands. Decode it via Q1's methodology. Find or derive the correct reply. Add to the dispatch table. Repeat.
- Each iteration gets its own short finding doc:
docs/research/s2c-self-experiment/Q3-iter-NN.mdwith: - Incoming request hex + decoded structure
- Source for the outgoing reply (Q1 bytes, Q2 bytes, or a pymobiledevice3 source file path + line numbers)
- CDS's observed next behavior
Done criteria:
- Either: the full pair handshake is walked end-to-end without a gap, producing a sequence of request/response pairs sufficient for Phase D implementation to be fully specified.
- Or: an iteration reveals a request whose correct reply we cannot source from Q1, Q2, Q4, or pymobiledevice3. That is a named gap — document the exact request and halt. Do not guess.
If the dialog walks to completion: Phase D can start. The Go backend's initial handler set is a direct translation of the dispatch table Q3 produced. If the dialog hits a named gap: either Q5 (post-TLS byte capture of a live session) closes it, or — as the last resort — the friend-capture track is revisited.
Q4 — Does pymobiledevice3 ship server-side utilities?¶
What we want to know: whether pymobiledevice3's source tree contains any code that implements the server side of the RemoteXPC protocol — test fixtures, mocks, unit test servers, or any "pretend to be an iPhone" utilities — that we could reuse directly to drive Q3's replies instead of hand-crafting them from protocol reference reads.
Why it matters: if it exists, every hour of Q3 iteration saved by running a ready-made server is an hour Phase D comes sooner. If it does not, Q3's hand-crafted approach remains the plan, no worse off.
How to answer (straight source read, no guessing):
- Grep the pymobiledevice3 upstream checkout for candidates:
cd /home/op/dev/myrepos/pymobiledevice3-scratch # or the havoc checkout
grep -rn -iE "server|listen|mock|fixture|fake" \
pymobiledevice3/remote/ tests/
grep -rn "start_server\|listen(\|accept(" pymobiledevice3/
grep -rn "XpcWrapper.build" pymobiledevice3/
- Specifically check
tests/for any fixture that opens a listening socket and speaks RemoteXPC back to a client. - Also check if there is a
pymobiledevice3 remote serveCLI subcommand or anything similar inpymobiledevice3/cli/.
Done criteria:
- A short finding doc
docs/research/s2c-self-experiment/Q4-server-side.mdlisting whatever was found (paths, what they do, usability rating) or explicitly concluding "no server-side code in pymobiledevice3; Q3 remains hand-crafted". - If any fixture is found usable, the exact call sequence to drive it is documented.
This question is independent of Q1/Q2/Q3 — it can be done in parallel or first. It has no empirical dependencies beyond reading source.
Q5 — Post-TLS byte capture of a live pymobiledevice3 session¶
What we want to know: if Q2 reveals utun4 carries
TLS-encrypted bytes, the plaintext equivalent of those bytes as
pymobiledevice3 sees them internally after TLS termination.
Why it matters: this is the fallback path for Q3's server replies if Q2 does not give us plaintext directly. The pymobiledevice3 process already has the PSK and the decrypted stream — we just need to observe what it reads and writes.
How to answer (last-resort self-research, no guessing):
Two alternatives, tried in order of invasiveness:
- SSLKEYLOGFILE + Wireshark. Some Python TLS stacks honor
SSLKEYLOGFILEto dump session keys for offline decryption. Check if pymobiledevice3's_create_client_socketpath uses a TLS implementation (sslstdlib or a third-party PSK library) that supports this. If yes, run the capture with the env var set and decrypt the Q2 pcap in Wireshark. - recv() hook via site-packages edit. If SSLKEYLOGFILE is
not honored, add a small instrumentation patch to the
pymobiledevice3 upstream clone (under
docs/patches/pymobiledevice3/following the same workflow as patch 0001) that wraps the TLS socket'srecv()/sendall()methods with a logger. Restart tunneld and CLI commands; capture the decrypted bytes to/tmp/iosmux-pymob-plaintext.log.
Done criteria:
- A per-question finding doc
docs/research/s2c-self-experiment/Q5-post-tls.mdwith: - Which alternative was used
- The captured plaintext bytes (hex) for a complete
remote rsd-infosession in both directions - Cross-reference: these bytes should match (modulo header framing) what Q3 needs for its dispatch table replies.
- If neither alternative works, this is a hard gap and the friend-capture fallback is back on the table.
Friend-capture fallback (last resort only)¶
Gap if Q1-Q5 all fail — friend capture is the ONLY escape
If Q1 cannot decode the bytes and Q2 shows utun4 is encrypted and Q5 cannot extract plaintext and Q3's bisection hits a request with no reference answer — then a physical Mac + iPhone pair outside our environment is the only source of ground truth left. Nothing in this document grants permission to start that track while any of Q1-Q5 is still open. Halting here is the correct behavior; proposing plausible-but-invented bytes is not.
Only triggered if Q1-Q5 all fail to close the protocol gap and there is still an empirical gap in the Q3 dialog table that cannot be answered on havoc. In that case, the earlier friend-capture methodology (zero-deps bash + tcpdump script, documented in the Stage S2 plan and in the earlier research prompts) becomes the path — and the packet returned by the friend gets processed through the same parsing pipeline as Q2.
Nothing in this file grants permission to start the friend-capture track while any of Q1-Q5 is still open.
Mechanics¶
- Each question has its own short finding doc under
docs/research/s2c-self-experiment/once answered. This tracker is updated with a link and the final status. - Artifacts (hex dumps, pcaps, decoded JSON, screenshots) live
alongside the finding doc in
results/orq{N}-...files. - Before any per-question doc is committed, a personal-data sweep (UDID, hostname, MAC-derived IPv6, serial) is mandatory. Redact before commit; do not rely on repo privacy.
- Each answered question closes with a one-line update here:
Q{N}: verified — see Q{N}-doc.mdwith a link. Until then the question staysdraft.