Phase D.5 smoke — Go backend reproduces iter-4 quiescent state byte-exact¶
Status: verified — 2026-04-19
First real-wire test of the Go backend (cmd/iosmux-backend/,
commit 114548a) against real CoreDeviceService. Binary
cross-compiled for darwin/amd64, deployed to havoc at
/tmp/iosmux-backend, run in place of the iter-4 Python
listener on [::1]:34719 with SPIKE tunneld still pointing
CDS at it. CDS triggered via killall CoreDeviceService +
devicectl list devices. Session captured via tcpdump on
lo0 and via backend stdout logs. Both artifacts decoded and
diffed against iter-4's results.
Verdict: the Go backend emits the exact same 14,313 bytes
on the wire as iter-4's Python listener did. cmp is
clean across the full server-side send buffer. CDS reaches
the same quiescent state (no GOAWAY, no RST_STREAM, silent
wait).
Update 2026-04-19: session validity resolved in D.6.1-B
The D.5 smoke pkill'd the backend within ~13 s of handshake
completion, well before CDS's natural response window. D.6.0-B
then observed that the Go backend (and the Python listener)
see CDS close TCP at ~30 s when left running. D.6.1-B
identified and fixed the cause: the 16-byte zero UUID in
#8 big Handshake, redacted at source in
iphone_replay_bytes.py. With IOSMUX_HANDSHAKE_UUID=random
the Go backend holds the connection ESTABLISHED for 130 s+
with no CDS-side disconnect. The D.5 wire-parity claim
still holds (14,313 B cmp clean vs iter-4 Python). The
Go backend now has a second superiority over the Python
listener: it can patch the UUID per session, unblocking
handshake acceptance. See
iter-07-uuid-patched/findings.md.
TL;DR¶
This is the phase gate from Python-spike research into Go
production implementation. The listener scaffolding in
docs/research/protocol/s2c-self-experiment/iosmux-spike-listener.py
— the 400-line zero-deps Python HTTP/2 replayer that drove
iter-1 through iter-4 — is now functionally replaced by a
300-line Go dispatcher in internal/backend/dispatcher.go built
on golang.org/x/net/http2.Framer and the existing XPC codec in
internal/xpc/.
The replacement is byte-exact on the server→client direction, not just structurally similar.
The session¶
Setup¶
- Binary: cross-compiled with
GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go buildfrom commit114548a. - Size: 5,736,800 bytes (5.47 MiB).
- File type:
Mach-O 64-bit x86_64 executable, flags:<|DYLDLINK|PIE>. - SPIKE tunneld (PID 1952 from iter-1 chain) unchanged.
- Python iter-4 listener not running (was killed at end of iter-4).
- Go backend launched with
nohup /tmp/iosmux-backend -listen [::1]:34719, logs to/tmp/iosmux-backend.log.
What the Go backend did¶
From the backend's stdout log (full flow, 21 lines):
iosmux-backend listening on [::1]:34719
accepted: remote=[::1]:54170 session=1
session=1 4-tuple local=[::1]:34719 remote=[::1]:54170
session=1 server handshake sent (SETTINGS + WINDOW_UPDATE)
session=1 frame type=SETTINGS stream=0 len=12 flags=0x00
session=1 sent SETTINGS-ACK
session=1 frame type=WINDOW_UPDATE stream=0 len=4 flags=0x00
session=1 frame type=HEADERS stream=1 len=0 flags=0x04
session=1 dispatcher: emit #3 HEADERS(s1, len=0)
session=1 frame type=SETTINGS stream=0 len=0 flags=0x01
session=1 recv SETTINGS-ACK
session=1 frame type=DATA stream=1 len=44 flags=0x00
session=1 dispatcher: emit #4 DATA(s1, 44 B empty-dict)
session=1 frame type=HEADERS stream=3 len=0 flags=0x04
session=1 dispatcher: emit #6 HEADERS(s3, len=0)
session=1 frame type=DATA stream=1 len=24 flags=0x00
session=1 dispatcher: emit #5 DATA(s1, 24 B sync)
session=1 frame type=DATA stream=3 len=24 flags=0x00
session=1 dispatcher: emit #7 DATA(s3, 24 B INIT_HANDSHAKE mirror)
session=1 dispatcher: emit #8 DATA(s1, 14124 B big Handshake)
session=1 read loop: local close
Byte-exact server-side match¶
Reassembled TCP payloads from the pcap, compared with
iter-04/results/session-01-send.bin:
| iter-4 (Python) | D.5 smoke (Go) | |
|---|---|---|
| Bytes emitted | 14,313 | 14,313 |
| Frame count | 9 | 9 |
| Payload lens in order | 12, 4, 0, 0, 44, 0, 24, 24, 14124 | 12, 4, 0, 0, 44, 0, 24, 24, 14124 |
cmp vs iter-4 send bin |
— | clean (identical) |
Client-side deviation (CDS-side, not ours)¶
Total client→server bytes: 204 B (matches iter-4 exactly in
total), but 30 of those 204 bytes are in a different position.
CDS emitted its SETTINGS-ACK(stream=0, len=0) one frame earlier
in the D.5 run compared to iter-4:
iter-4 order: HEADERS(s1) DATA(s1,44) SETTINGS-ACK HEADERS(s3) DATA(s1,24) DATA(s3,24)
D.5 order: HEADERS(s1) SETTINGS-ACK DATA(s1,44) HEADERS(s3) DATA(s1,24) DATA(s3,24)
Both are legal HTTP/2 sequences (SETTINGS-ACK on stream 0 is independent of stream ⅓ frames). Same 9 frames, identical payloads, identical set. This is CDS-side run-to-run variation, not anything the backend did differently. Our server response is byte-exact in both runs, and CDS's total response (14,313 B, same content) proves CDS handled both orderings identically.
Final CDS state¶
Neither RST_STREAM nor GOAWAY observed. After CDS ACKs our
14124 B big Handshake, it goes silent — the exact iter-4
quiescent state. The backend's read loop was ended by our
manual pkill iosmux-backend, not by EOF from CDS.
Artifacts¶
Under
iter-05-go-backend/
(this directory):
smoke-01.log— 1,525 B. Backend stdout from the first run (no pcap). Identical to smoke-02.log.smoke-02.log— 1,525 B. Backend stdout from the second run (with pcap). Identical to smoke-01.log. Kept both to demonstrate deterministic behaviour.iosmux-d5-smoke.pcap— 18,061 B. Full loopback capture during the second run. Scanned for UDID/serial/ECID patterns — zero matches (CDS cannot read those fields without a mounted DDI, and they are absent from the loopback h2c stream in plaintext form). Kept as binary artifact, no redaction needed per iter-02/03/04 policy.
Implications¶
Phase D.5 is done. The Go backend byte-exactly reproduces the most advanced state the Python spike listener reached (iter-4 quiescent). Any further forward progress belongs to Phase D.6: per-service handlers that synthesise RemoteXPC replies to whatever CDS requests after the handshake.
What unblocks now¶
- The spike listener in
docs/research/protocol/s2c-self-experiment/iosmux-spike-listener.pyis no longer necessary for reproducing iter-4's quiescent state. It can stay as a historical reference but no production code depends on it. - The
IOSMUX_SPIKEtunneld patch (Phase D.7 scope) is still pointing CDS at the Go backend's address. When we generalise the advertisement in D.7, the backend takes full ownership. - The iter-01 replay corpus (
iphone_replay_bytes.py) is now available in two forms: the Python source and the Go embed underinternal/backend/fixtures/. We can migrate consumers from the Python form at our own pace.
What still needs empirical grounding for D.6¶
CDS is silent after the handshake in our current dispatch. To
learn what it expects next, we need to trigger a different
user-visible action: devicectl pair, the Xcode Pair button, or
any command that initiates an interactive service call. Once CDS
sends its first post-handshake RemoteXPC request, D.6 has a
concrete target to dispatch-bisect against.
Status of the D.5 chain¶
- Step A (Go code: backend skeleton + h2c framer + XPC codec +
dispatcher): commits
bc0b248,ea59afd,207b729,114548a. - Step B (cross-compile for darwin/amd64): delivered, 5.47 MiB binary.
- Step C (deploy to havoc + trigger CDS + capture): delivered, one clean session.
- Step D (decode + byte-exact diff with iter-4): this document. Verdict: identical on the server side, legal ordering variation on the client side.
D.5 closes. Phase D.6 (per-service handlers, starting with
the first thing CDS requests after a devicectl pair trigger)
is the next implementation pass.