Skip to content

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 build from commit 114548a.
  • 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.py is 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_SPIKE tunneld 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 under internal/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.