TunnelVision

Shared/Fixtures

Debug-only. A deterministic generator of a synthetic capture: flows, their packets, and real IP datagrams (Shared/IP emits them, the parser next door reads them back).

It exists because the packet tunnel extension only runs on a device, and it is the only writer of the history database, the pcap directory and the live ring. On the Simulator the app therefore has nothing to show — no Timeline rows, no scrub bar, no reachable Flow Inspector or packet screen, no captures — which is exactly the set of dense screens that M11’s remaining work needs someone to look at, listen to and profile.

The whole file is behind #if DEBUG, and not as a convenience: once written into the shared container, a synthetic capture is indistinguishable from a real one — the history does not record where a row came from — so the only guarantee that a user never sees invented traffic is that this code is absent from Release.

Two properties everything else rests on:

Addresses and hostnames are documentation-reserved (RFC 5737, RFC 3849, RFC 2606) because these captures end up in the public repo’s screenshots.

Most payloads are reproducible noise, and one flow’s are not. Nobody looks inside a segment of a synthetic download, so filling it with the seeded generator is right. The port-53 flow is the exception since roadmap step 10: a dissector reads those datagrams and the packet screen shows what they say, so FixtureLookups writes real DNS messages with DNSMessageFixture — four query→reply pairs, one for each reading a reply can have (addresses, a name, records that are not broken down, and a name that does not exist), by hand for the same reason the flow catalogue is by hand: a state that depends on the seed cannot be asserted in a test or promised in a screenshot.

Every TCP flow carries what a device would have read from its opening (2026-10-13): whether the stream began with an HTTP request or a TLS handshake and, for a handshake, both halves of it — what the client offered and what the server chose, with the source of that reading following who terminated the flow (the connection’s own ServerHello when it passes through, the tunnel’s upstream connection when it was inspected or the app refused the local certificate). All of it is derived from what the script already is, so no flow can state a handshake its inspection status contradicts. Two scripted flows exist for the readings an audit tells apart: www.example.com negotiates TLS 1.2, the only accepted version whose certificate travels in the clear, and carries a chain; reset.example.org ends at TLS 1.0 from a client that named only its ceiling — the one TLS-version finding in the whole capture. The seeded certificates expire after the capture ends, so a sample redone another day does not show an expiry nobody decided to show.

The audit project carries two versions (2026-10-13, AuditFixture): a baseline, the audited release and the one before it, each over a quarter of the capture’s flows, all three ended. The earlier release exists so that the comparison screen has something to compare; it is declared as started before the audited one, because the quarters overlap in time and the comparison calls “later” whichever session started later.

The QUIC flows are noise too, and that is a limit worth knowing. A UDP flow to port 443 is seeded with what the extension would have read from its opening packets — the QUIC version, and since 2026-10-13 the host name and the offer of its ClientHello (TLS 1.3 and h3, the only thing a client can offer inside QUIC) — but its datagrams are not Initial packets: the record states the reading, the bytes do not carry it. Every bulk QUIC flow announces its name and keeps the one DNS had given its address, which is the ordinary case; the scripted quic.example.com announces none, as a connection already open when the tunnel started would not, and is the one whose name is inferred from DNS, with two other candidates for its address.

Writing it down

CaptureFixture.make(spec) produces the capture without touching disk; FixtureSeeder is what puts it where the app reads — the history database (FlowStore) and the capture directory (PcapWriter). Four decisions live there:

Seeding replaces rather than accumulating. Two runs leave one capture, which is what makes the launch argument repeatable and — the part that matters — what keeps two Instruments runs on the same seed measuring the same work.

Using it

The seeder is driven from the app by a launch argument, -TVSeedFixture (FixtureSeeding in ../../TunnelVision/Services), handled at startup by a gate that holds the screens back until the writing is done:

xcrun simctl launch --console-pty <device> com.juanmmm21.tunnelvision -TVSeedFixture

or Edit Scheme → Run → Arguments in Xcode. It prints what it wrote when it finishes. The argument is honoured only on the Simulator: on a device the extension is the writer, and seeding would replace its owner’s real capture with invented traffic.

The live feed stays flat either way: the ring is an SPSC channel whose single producer is the extension, and faking it from the app would mean writing from the producer end of a contract that says there is only one. What fills up is the history.

The one screen the container could not reach

The same launch argument also seeds TunnelStatsFixture (2026-08-21), and it does not go through the container: Session diagnostics reads its counters off the control channel, so a seeded app still showed nothing but its “nothing to diagnose yet” card and six hundred points of empty canvas. It was the only screen in the app a Simulator session could not look at, and therefore the only one whose density and hierarchy had to be judged blind — which is exactly what the second design pass could not do. AppEnvironment builds the diagnostics view model over a seeded channel when the argument is present; the view model learns one thing from it, that the tunnel’s state (never .live on a Simulator) must not tear the screen down.

Its numbers are hand-written constants rather than a generator, and that is what makes them useful: each has to be believable and consistent with the others — decrypted, pinned and failed add up to the terminations opened, those plus the abandons and the skips add up to the candidates, and the outcomes of the name map (dnsNames) add up to the DNS replies the relay received, and the QUIC flows that were read (quicNames) fit inside the UDP flows that are not lookups, at two Initials a flow — because a table whose figures do not agree with each other is no good for judging how they read. It portrays a session that works, and what is imperfect in it is not invented either: hosts that refuse the certificate (ADR 0003), a handful of QUIC flows whose name was not read, and one capture failure, which is the case the screen’s error section exists for.

Milestone: M11