Commit Graph

4 Commits

Author SHA1 Message Date
Claude c65eef6927 feat(quic): heap-sampling soak test, FD-leak canary, phantom-stream guard, loss harness
Follow-up to b8c6e080 addressing the gaps I called out in the
"is this the best we can do?" reply.

1. **Heap-sampling soak test** (`QuicHeapSoakTest`). Long-form, default-
   skipped via `-PquicSoakSeconds=N` propagated by `quic/build.gradle.kts`
   to the jvmTest task. Without the property the test early-returns with
   a printed SKIPPED line, so `./gradlew test` stays fast for CI. With
   the property, drives moq-lite-shaped peer-uni churn at ~50 streams/s
   for N seconds, samples `totalMemory - freeMemory` six times across
   the run, and fails if the post-warmup → final delta exceeds 10 MB
   (the acceptance threshold from the audio-rooms soak prompt).
   Production use: `-PquicSoakSeconds=1800` for the 30-minute soak.

2. **FD-leak canary** added to `QuicConnectionDriverLifecycleTest`. On
   Linux, samples `/proc/self/fd` size before / after the 100-session
   loop; banded at +16 entries for ambient JVM noise. macOS / Windows
   silently no-op because /proc isn't there. Catches socket / pipe
   leaks the thread-count check would miss.

3. **Phantom-stream guard.** Added `retiredStreamIdSet` (capped FIFO
   ring at 4 096 entries, ~80 s of moq-lite churn) plus
   `isStreamIdRetiredLocked` on the connection. Parser checks before
   `getOrCreatePeerStreamLocked` and drops STREAM frames the peer
   retransmits on already-retired streams. Eliminates the
   "duplicate ACK lost → peer retransmits FIN → we mint a phantom
   QuicStream" edge case I papered over in the previous commit.
   Pinned by `phantomGuardDropsRetransmitOnRetiredPeerStream`.

4. **moq-lite loss harness** (`MoqLiteLossHarnessTest`). First pass at
   soak target #5: drive 50 best-effort group streams with 5%
   uniform packet loss, assert the listener surfaces ≥ 90% with
   payloads intact and the connection stays CONNECTED. Out of scope
   here: reorder injection, latency-under-loss measurement, full
   end-to-end with a real moq-lite publisher.

Tests:
 - `QuicHeapSoakTest` — gated, validates 10MB heap acceptance band.
 - `QuicConnectionDriverLifecycleTest::repeatedSessionLifecycleDoesNotLeakThreads`
   — now also enforces /proc/self/fd bound.
 - `StreamRetirementSoakTest::phantomGuardDropsRetransmitOnRetiredPeerStream`
   — pins the duplicate-frame drop semantics.
 - `MoqLiteLossHarnessTest` — 2 cases (lossy + lossRate=0 baseline).

https://claude.ai/code/session_018KPKWRg5baX5Anf7zfEyec
2026-05-07 23:44:13 +00:00
Claude d2a10c9e4a feat(quic): live interop harness — picoquic Docker + InteropRunner main
Setup for driving the pure-Kotlin QUIC client against real reference
servers, kicking off the live-interop validation that closes the loop on
the audit cycles.

PermissiveCertificateValidator (commonMain test-only):
  Accepts any cert chain and any signature. For local dev servers (picoquic,
  quic-go, nests-rs in Docker) where the system trust store would reject the
  self-signed cert. Documented as test-only — must never be wired into
  production code.

InteropRunner.main (jvmTest):
  Standalone runnable that opens a UDP socket, builds a QuicConnection with
  PermissiveCertificateValidator, drives the handshake under a configurable
  timeout, and reports CONNECTED / HandshakeFailed / Timeout / UdpFailed
  with peer transport parameters on success. Exit code 0 on connect, 1 on
  any failure mode — wirable into CI.

`quic/scripts/run-picoquic.sh`:
  One-line Docker harness that runs Christian Huitema's picoquic reference
  server on UDP 4433. Picoquic is the IETF QUIC WG's reference impl and
  the most permissive target for a fresh client (clear qlog traces, accepts
  a wide range of transport params).

`./gradlew :quic:interop` Gradle task:
  Wraps the runner so live-interop is one command:
    ./gradlew :quic:interop -PinteropHost=127.0.0.1 -PinteropPort=4433
  Validated against a non-running server: returns Timeout cleanly with
  exit 1 (the failure-path proves the harness wires correctly).

Workflow documented in `quic/scripts/README.md` covering picoquic,
quic-go, aioquic, quiche, and the eventual graduation path to the
quic-interop-runner Docker matrix at https://interop.seemann.io.

Next step: actually run picoquic in Docker and chase whatever real-world
incompatibilities surface. Each will be a focused fix commit.

https://claude.ai/code/session_01EC1tfXfap8k8GyKvrxkxZx
2026-04-25 23:08:18 +00:00
Claude 692b034566 feat(quic): Phase B — TLS 1.3 client on Quartz primitives
Implement a TLS 1.3 client state machine that drives the QUIC handshake using
only Quartz's existing crypto. No BouncyCastle dependency.

- HKDF-Expand and HKDF-Expand-Label upstreamed to Quartz's Hkdf class with
  RFC 5869 + RFC 8448 test vectors covering them.
- :quic crypto stack: AEAD (AES-128-GCM via Quartz's AESGCM, ChaCha20-Poly1305
  via Quartz's pure-Kotlin impl), header protection (AES-ECB via JCA single
  block + ChaCha20 keystream), QUIC Initial-secret derivation matching
  RFC 9001 Appendix A.1 bit-for-bit.
- TLS 1.3 transcript hash, key schedule (early/handshake/master + per-direction
  client/server traffic secrets), Finished MAC.
- ClientHello + extension encoders carrying SNI, supported_versions=[TLS 1.3],
  supported_groups=[X25519], signature_algorithms covering ECDSA/RSA-PSS/Ed25519,
  X25519 key_share, psk_dhe_ke, ALPN=[h3], and the QUIC transport_parameters
  extension.
- ServerHello + EncryptedExtensions + Certificate + CertificateVerify + Finished
  parsers. The state machine handles the certificate path and the PSK-style
  no-cert path; certificate validation is wired through a CertificateValidator
  SPI (real impl lands in Phase L).
- Transport parameters codec covering all RFC 9000 §18.2 + RFC 9221 fields.
- QuicWriter/QuicReader buffer helpers shared across the rest of the stack.

Round-trip test: a minimal in-process TLS server built from the same primitives
drives a full ClientHello → ServerHello → EE → Finished → client Finished
exchange. Both sides reach handshake-complete and agree bit-for-bit on the
handshake & application traffic secrets. ALPN + transport parameters round-trip
through EncryptedExtensions cleanly.

https://claude.ai/code/session_01EC1tfXfap8k8GyKvrxkxZx
2026-04-25 17:33:54 +00:00
Claude 2d541c6fd4 feat(quic): Phase A — module foundations
Create the new :quic Gradle module (KMP, api(project(":quartz"))) and migrate
the QUIC varint codec out of :nestsClient where it was incidentally living.
Add the connection-ID, packet-number-space, and UDP socket primitives that
the rest of the QUIC client will build on.

Layer-by-layer plan in docs/plans/2026-04-22-pure-kotlin-quic-webtransport-plan.md.

- New :quic module wired into settings.gradle, with commonMain + jvmAndroid
  source sets mirroring :quartz's structure.
- Varint moves from com.vitorpamplona.nestsclient.moq to com.vitorpamplona.quic;
  MoqBuffer/MoqCodec updated to import the new path.
- ConnectionId enforces the 0..20 byte length range and ships a randomizer
  backed by Quartz's RandomInstance.
- PacketNumberSpaceState tracks per-space outbound allocation + largest-received
  tracking, and implements the RFC 9000 §A.3 truncated-PN decode formula plus
  the §17.1 minimum encode-length picker.
- UdpSocket is an expect class with a connected DatagramChannel actual on
  jvmAndroid using Dispatchers.IO (no Selector — one socket per connection).

All 12 tests pass on jvmTest. RFC 9000 §A.1 varint vectors and §A.3 truncated-PN
vector match bit-for-bit.

https://claude.ai/code/session_01EC1tfXfap8k8GyKvrxkxZx
2026-04-25 17:19:02 +00:00