quinn is now treated as a flawless-required interop target alongside
aioquic, picoquic, and quic-go. Most Rust-based Nostr/MoQ relays our
users run their servers on are built on quinn, so an interop regression
there is a user-visible regression.
Validated by a 3-round flakiness sweep (1 full matrix + 2 audio-critical
subsets) — zero result flakiness across 528 test executions across all
four peers, with two environmental docker-compose stall classes documented
separately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`LC_CTYPE=C tr -dc '[:alnum:]' </dev/urandom` in the upstream runner's
certs.sh trips "tr: Illegal byte sequence" on macOS — LC_CTYPE alone
doesn't override the runtime locale chain. Only the amplificationlimit
testcase exercises this loop (chain length > 1 → fakedns SAN inflation),
but if it aborts the runner stops the whole matrix BEFORE any later
test in TESTCASES_QUIC runs: handshakeloss, transferloss,
handshakecorruption, transfercorruption, ipv6, v2, rebind-port,
rebind-addr, connectionmigration, and the goodput/crosstraffic
measurements all silently never execute. The post-mortem summary
shows the 12 testcases that ran before the abort and looks deceptively
complete.
Add an idempotent `sed` step to run-matrix.sh that rewrites the line
to `LC_ALL=C tr` on every invocation, plus a plan-file note so the
next person hitting the deceptive summary doesn't repeat the
diagnosis. The patch is a working-tree edit to ../quic-interop-runner/,
not a fork; re-applied on every clone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The QPACK_MAX_TABLE_CAPACITY=0 advertisement worsened the result
(1 file → 0 files written). Empirical: empty SETTINGS = spec defaults
(both 0) and aioquic was already sending literal-encoded responses
under that condition.
Real bug isn't QPACK — diagnostic showed 1359 GETs processed in 58s
(~23 streams/sec). Throughput is hard-bottlenecked by :quic's single
conn.lock serializing send loop + read loop + openBidiStream across
1999 waiting coroutines. Even at maximum throughput we'd only complete
~1400 of 1999 in 60s. The 1 file we managed previously was just the
first response landing before lock contention spiked.
Filed multiplexing as a known-throughput-limit follow-up. Possible
fixes (per-level lock split / Semaphore-bounded concurrency / QPACK
dynamic-table support) are all non-trivial and the testcase is a
stress test, not a real-world load shape.
For now: revert to empty SETTINGS, accept multiplexing as the lone
deferred testcase. Aioquic + picoquic stay at 6/7.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
Documents:
- Three overnight agents merged (VN defense in :quic, qlog observer
+ JSON-NDJSON writer wired into the prod endpoint, peer-uni-stream
drainer fixing the multiplexing tear-down).
- The agent-A versionnegotiation testcase mismatch — runner doesn't
have that name; v2 is the closest, tests QUIC v2 which we don't
speak. VN code stays as defensive support.
- Open issues for tomorrow: retry test failure (qlog now available
for diagnosis), v2 (deferred), re-runs against all three peers
with the new fixes.
- Documented run-matrix.sh's concurrency limit.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
Three agents in flight in worktrees:
A — versionnegotiation testcase + configurable initial-version writer
B — qlog observer infrastructure (QlogObserver interface in :quic,
JSON-NDJSON writer in :quic-interop, hooks at packet/key/recovery
sites, reads $QLOGDIR the runner already sets)
C — multiplexing channel-saturation fix (consume server's peer uni
streams in Http3GetClient — RFC 9114 §6.2 says clients MUST
process incoming control + QPACK streams)
Plan doc now also captures the latest test matrix (pre-acfe815e1
multi-ALPN-offer fix) and the predictions for the next run.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
Two more testcases now dispatched through runTransferTest:
- retry — exercises the RFC 9000 §17.2.5 + RFC 9001 §5.8 Retry
handling that landed in d03e17981 / 671f9c705 (DCID swap,
integrity-tag verify, key re-derivation, token threading).
- ipv6 — same flow over an IPv6 socket. JDK's DatagramChannel.connect
handles the v6 address resolution natively; if anything
breaks it'll be an actual bug worth surfacing.
Plan doc updated to reflect Phase 3 landings (ALPN per testcase,
HqInteropGetClient, multi-stream FIN delivery fix) and the current
validation matrix across aioquic / picoquic / quic-go.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
make smoke was the bisector for "is the bug in :quic or in the runner
environment?" while we were debugging the initial close-path / PTO /
padding issues. Now that the runner reliably runs the matrix end-to-end
(handshake + chacha20 green vs aioquic), smoke's only job — running
picoquic outside the runner — is unneeded, and the picoquic image
keeps changing its entrypoint / required args in ways that make smoke
finicky to maintain.
Removes:
- make smoke / smoke-down targets
- SMOKE_NET / SMOKE_PICOQUIC / SMOKE_CLIENT vars
- SMOKE_MODE handling in run_endpoint.sh (just always tolerates
/setup.sh failure now — same effect, less ceremony)
- Plan doc note about make smoke updated to reflect removal.
If we ever need a non-runner bisector again, it's two `docker run`
commands; not worth permanent maintenance.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
Upstream quic-interop-runner split its config into implementations_quic.json
+ implementations_webtransport.json so QUIC and WebTransport endpoint
registrations don't collide. Schema is unchanged; just the filename.
Also updated the Makefile comment + plan doc for consistency.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
One-shot wrapper that clones quic-interop-runner alongside this repo if
missing, sets up its venv, merges our implementations.json snippet, builds
the endpoint image, then invokes run.py with passed-through args. Every
step is idempotent so repeated invocations just iterate.
Designed to script only the per-run loop, not first-time tooling install
(Docker Desktop, Homebrew, Wireshark prefs) — those are GUI / one-time
steps where a script either fails awkwardly or hides what the user is
consenting to.
Cross-platform install hints (apt-get on Linux, brew on macOS) on missing
prereqs. Daemon liveness check (docker info) catches the common macOS
"Docker Desktop installed but not running" trap. SKIP_BUILD=1 escape
hatch for tight image-unchanged inner loops.
Plan doc updated: run-matrix.sh is now the documented happy path; the
manual jq-merge sequence is kept as a fallback.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
The quic-interop-runner exposes several testcases that drive the same
client logic but vary the network conditions injected by the ns-3 sim.
These don't need new client code on our side — they exercise the
existing handshake / transfer paths under loss / corruption / high-RTT /
cross-traffic, which is exactly the bug-finding signal we want.
Aliases added:
- transferloss, transfercorruption, longrtt, goodput, crosstraffic
→ transfer (H3 GET against varying sim configs)
- handshakeloss → handshake
Plan doc now lists every standard testcase and either marks it landed,
aliased, or explicitly unsupported with a written reason — so anyone
returning to this knows what's left and why each gap exists. Unsupported
set covers: versionnegotiation (writer hard-codes V1), resumption /
zerortt (no session ticket / 0-RTT), keyupdate (no KEY_PHASE handling),
retry (parser exists but not wired to feed-loop), rebinding-* (no client
migration), amplificationlimit (server-side), blackhole (inverse test),
ipv6 (UdpSocket v6 path unverified).
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
Adds a minimal Http3GetClient (in :quic-interop, NOT :quic — interop-test
surface, not a production HTTP/3 client) that opens the three required
client uni streams (control + QPACK encoder + QPACK decoder per RFC 9114
§6.2.1), sends an empty SETTINGS, and per request opens a bidi stream,
encodes the four pseudo-headers via the existing literal-only QpackEncoder,
FINs, and reassembles HEADERS+DATA frames from the response.
Wires three testcases:
- transfer: GET each REQUESTS URL sequentially, write body to
\$DOWNLOADS/<basename>. status != 200 fails.
- http3: identical to transfer.
- multiplexing: same fetches but issued in parallel via
coroutineScope { async { … } } so request streams genuinely
overlap on the wire (what tshark verifies).
Out-of-scope (deliberate): GOAWAY, PUSH_PROMISE, dynamic QPACK table,
trailers, priority — none are required by these testcases.
Unit-tests round-trip the request encoding through the existing
Http3FrameReader + QpackDecoder so the wire format is verified without
needing a real peer.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
Refactors SslKeyLogger to take the ClientHello random at flush() time
(removes the lookup-lambda + bindConnection ceremony) so the formatter
is testable without spinning up a real QuicConnection. Adds three
unit tests covering: the four NSS Key Log lines emitted in the right
order, flush idempotency, and lower-case hex encoding.
Also drops a checked-in implementations.json snippet
(quic-interop-runner-snippet.json) so a sibling clone of the runner
can be registered with one jq merge instead of hand-edited JSON.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
Adds two debugging hooks to :quic so the interop endpoint can produce
artifacts that make the runner actually useful for finding bugs:
- TlsClient.clientRandom: capture and expose the 32-byte ClientHello
random so a SSLKEYLOG line can correlate to this connection.
- QuicConnection.extraSecretsListener: optional chained secrets
listener (default null, no-op for production callers). Fires
alongside the connection's own key-installation listener at every
encryption-level transition.
- QuicConnection.cipherSuites: knob to override the offered TLS
cipher suites in ClientHello.
InteropClient now:
- Writes NSS Key Log Format lines to $SSLKEYLOGFILE when set, so
Wireshark can decrypt the sim's pcap captures.
- Implements the `chacha20` testcase by offering only
TLS_CHACHA20_POLY1305_SHA256 — exercises the ChaCha20 AEAD path
end-to-end against a peer.
Defers QLOGDIR (needs qlog observer infrastructure across packet/
frame/recovery layers — own design doc) and `versionnegotiation`
(needs the writer to accept a configurable initial QUIC version).
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT
Adds :quic-interop, a JVM-only module at quic/interop/ that implements
the quic-interop-runner Docker contract so we can drive matrix tests
against aioquic / quiche / picoquic / msquic / ngtcp2 / etc. as a
bug-finding harness.
Phase 0 supports only the `handshake` testcase; everything else returns
127 so the runner skips rather than fails. SSLKEYLOGFILE / QLOGDIR
plumbing is deferred to Phase 1.
https://claude.ai/code/session_01HcvfQq1ttPV9PkRoJb4nyT