docs(quic): refresh stack-status plan to match shipped 2026-05-09 reality
Plan had drifted ~2 weeks behind shipping work. Rewrote to reflect the features that landed since 2026-04-26 (path validation, 0-RTT, key update, ECN, session resumption, lock-split refactor, DoS hardening) and added a fresh RFC compliance gaps section catalogued from a four-agent audit (RFC 9000, 9001, 9002, 9221+9114+9204). https://claude.ai/code/session_01XGmmaVuy2wsSZQx2AqN2ik
This commit is contained in:
@@ -1,16 +1,24 @@
|
|||||||
# QUIC + WebTransport stack — current state (2026-04-26)
|
# QUIC + WebTransport stack — current state
|
||||||
|
|
||||||
This document is the post-implementation snapshot of `:quic`. It supersedes
|
Living status doc for `:quic`. First written 2026-04-26 as the post-implementation
|
||||||
the original [pure-kotlin QUIC + WebTransport plan](../../docs/plans/2026-04-22-pure-kotlin-quic-webtransport-plan.md)
|
snapshot of the [pure-kotlin QUIC + WebTransport
|
||||||
which was written before any code shipped and is now historical.
|
plan](../../docs/plans/2026-04-22-pure-kotlin-quic-webtransport-plan.md);
|
||||||
|
last full rewrite **2026-05-09** after a four-RFC compliance audit (RFC 9000,
|
||||||
|
9001, 9002, 9221 + 9114 + 9204) found the doc had drifted ~2 weeks behind
|
||||||
|
shipping work. The original plan is now historical.
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
|
||||||
`:quic` is a self-contained Kotlin-Multiplatform module that speaks QUIC v1
|
`:quic` is a self-contained Kotlin-Multiplatform module that speaks QUIC v1
|
||||||
+ HTTP/3 + WebTransport against real-world servers (aioquic + picoquic
|
+ HTTP/3 + WebTransport against real-world servers (aioquic + picoquic +
|
||||||
verified; nestsClient/MoQ on top). It uses Quartz crypto primitives only —
|
quic-go interop verified, including 0-RTT). Pure Kotlin: Quartz crypto
|
||||||
no BouncyCastle, no JNI. ~8.5k lines of production code, ~5k lines of
|
primitives + JCA AEAD only — no BouncyCastle, no JNI. ~17k lines of
|
||||||
tests, 39 test files, five rounds of parallel audit + fix passes.
|
production code, 81 test files, ten-plus rounds of audit + fix passes
|
||||||
|
since 2026-04-26.
|
||||||
|
|
||||||
|
The 2026-05-09 audit confirmed the core handshake / packet protection /
|
||||||
|
loss recovery / frame routing surface is RFC-grade. Real gaps are listed
|
||||||
|
in [§ RFC compliance gaps](#rfc-compliance-gaps-2026-05-09).
|
||||||
|
|
||||||
## What shipped vs the original plan
|
## What shipped vs the original plan
|
||||||
|
|
||||||
@@ -20,18 +28,18 @@ tests, 39 test files, five rounds of parallel audit + fix passes.
|
|||||||
| B. TLS 1.3 | 3 wk | done | RFC 8446 client state machine, X25519 ECDHE, RFC 8448 §3 vectors pass bit-for-bit |
|
| B. TLS 1.3 | 3 wk | done | RFC 8446 client state machine, X25519 ECDHE, RFC 8448 §3 vectors pass bit-for-bit |
|
||||||
| C. Initial + Handshake packets | 2 wk | done | RFC 9001 §A.2/§A.3 vectors pass; ChaCha20 per §A.5 |
|
| C. Initial + Handshake packets | 2 wk | done | RFC 9001 §A.2/§A.3 vectors pass; ChaCha20 per §A.5 |
|
||||||
| D. 1-RTT + STREAM | 1 wk | done | Stream offset reassembly, FIN, fuzzed |
|
| D. 1-RTT + STREAM | 1 wk | done | Stream offset reassembly, FIN, fuzzed |
|
||||||
| E. ACK + flow control | 1 wk | done | MAX_DATA / MAX_STREAM_DATA / MAX_STREAMS routing + writer enforcement |
|
| E. ACK + flow control | 1 wk | done | MAX_DATA / MAX_STREAM_DATA / MAX_STREAMS routing + writer enforcement + per-stream receive enforcement |
|
||||||
| F. Loss recovery + congestion control | 1 wk | done (no CC) | RFC 9002 §5/§6 RTT estimator + packet/time-threshold loss detection + PTO + per-frame retransmit shipped 2026-05-05; congestion control still TBD |
|
| F. Loss recovery + congestion control | 1 wk | done (no CC) | RFC 9002 §5/§6 RTT estimator + packet/time-threshold loss detection + PTO + per-frame retransmit shipped 2026-05-05; congestion control still TBD |
|
||||||
| G. Datagram extension | ½ wk | done | RFC 9221 frames + bounded incoming queue |
|
| G. Datagram extension | ½ wk | done | RFC 9221 frames + bounded incoming queue |
|
||||||
| H. Connection lifecycle | 1 wk | done | CONNECTION_CLOSE, idle timeout, draining/closing, idempotent driver close |
|
| H. Connection lifecycle | 1 wk | done | CONNECTION_CLOSE, draining/closing, idempotent driver close (idle-timeout enforcement still missing — see gaps) |
|
||||||
| I. HTTP/3 | 2 wk | done | Control stream + SETTINGS + GOAWAY (with id-regression check) + duplicate-id rejection |
|
| I. HTTP/3 | 2 wk | done | Control stream + SETTINGS + GOAWAY (with id-regression check) + duplicate-id rejection |
|
||||||
| J. QPACK | 2 wk | done | Static-table + Huffman + integer codec (RFC 7541 §B + RFC 9204 §B.1 vectors) |
|
| J. QPACK | 2 wk | done | Static-table + Huffman + integer codec (RFC 7541 §B + RFC 9204 §B.1 vectors) |
|
||||||
| K. Extended CONNECT + WT | 1 wk | done | Stream-type prefixes, HTTP Datagram + WT_CLOSE_SESSION capsule |
|
| K. Extended CONNECT + WT | 1 wk | done | Stream-type prefixes, HTTP Datagram + WT_CLOSE_SESSION capsule |
|
||||||
| L. Interop + hardening | 2 wk | done + much more | Live interop against aioquic Docker; **5 rounds of audit + fix** beyond the plan |
|
| L. Interop + hardening | 2 wk | done + much more | Live interop against aioquic + picoquic + quic-go; **10+ rounds of audit + fix**, multiplexing, 0-RTT, key update, ECN, path migration |
|
||||||
|
|
||||||
The original plan estimated 17–19 weeks. We ran the full sequence plus five
|
The original plan estimated 17–19 weeks. We shipped the full sequence plus
|
||||||
unscheduled audit rounds. Every audit found real bugs; the suite is what
|
a long tail of audit-driven hardening. Every audit found real bugs; the
|
||||||
caught them on regression.
|
test suite is what catches them on regression.
|
||||||
|
|
||||||
## What's actually in the module
|
## What's actually in the module
|
||||||
|
|
||||||
@@ -43,51 +51,61 @@ quic/
|
|||||||
│ ├── Buffer.kt ← QuicReader / QuicWriter
|
│ ├── Buffer.kt ← QuicReader / QuicWriter
|
||||||
│ ├── Varint.kt ← RFC 9000 §16
|
│ ├── Varint.kt ← RFC 9000 §16
|
||||||
│ ├── connection/ ← QuicConnection orchestrator + Driver
|
│ ├── connection/ ← QuicConnection orchestrator + Driver
|
||||||
│ │ ├── QuicConnection.kt (≈ 600 lines, the hub)
|
│ │ ├── QuicConnection.kt (≈ 2800 lines, the hub)
|
||||||
│ │ ├── QuicConnectionDriver.kt (read/send loops + close)
|
│ │ ├── QuicConnectionDriver.kt (read/send loops + close)
|
||||||
│ │ ├── QuicConnectionParser.kt (feedDatagram + dispatchFrames)
|
│ │ ├── QuicConnectionParser.kt (feedDatagram + dispatchFrames)
|
||||||
│ │ ├── QuicConnectionWriter.kt (drainOutbound + flow-control updates)
|
│ │ ├── QuicConnectionWriter.kt (drainOutbound + flow-control updates)
|
||||||
|
│ │ ├── PathValidator.kt ← RFC 9000 §5.1.2 + §8.2 + §9
|
||||||
│ │ ├── PacketProtection.kt + builder
|
│ │ ├── PacketProtection.kt + builder
|
||||||
│ │ ├── PacketNumberSpace.kt
|
│ │ ├── PacketNumberSpace.kt
|
||||||
│ │ ├── ConnectionId.kt + TransportParameters.kt
|
│ │ ├── ConnectionId.kt + TransportParameters.kt
|
||||||
│ │ └── EncryptionLevel.kt + LevelState.kt
|
│ │ ├── EncryptionLevel.kt + LevelState.kt
|
||||||
│ ├── crypto/ ← Aead, header protection, HKDF helpers, AesEcbHeaderProtection,
|
│ │ └── recovery/ ← RecoveryToken, SentPacket,
|
||||||
│ │ ChaCha20HeaderProtection, ChaCha20Poly1305Aead, InitialSecrets,
|
│ │ AckedPackets, QuicLossDetection
|
||||||
|
│ ├── crypto/ ← Aead, header protection, HKDF helpers,
|
||||||
|
│ │ AesEcbHeaderProtection, ChaCha20HeaderProtection,
|
||||||
|
│ │ ChaCha20Poly1305Aead, InitialSecrets,
|
||||||
│ │ PlatformAesOneBlock, PlatformChaCha20Block (expect),
|
│ │ PlatformAesOneBlock, PlatformChaCha20Block (expect),
|
||||||
│ │ bestAes128GcmAead (expect)
|
│ │ bestAes128GcmAead (expect)
|
||||||
│ ├── frame/ ← Frame.kt sealed hierarchy + FrameFuzzerTest target
|
│ ├── frame/ ← Frame.kt sealed hierarchy + FrameFuzzerTest target
|
||||||
│ │ includes RESET_STREAM / STOP_SENDING / NEW_TOKEN
|
│ │ includes RESET_STREAM / STOP_SENDING / NEW_TOKEN /
|
||||||
|
│ │ PATH_CHALLENGE / PATH_RESPONSE / NEW_CONNECTION_ID /
|
||||||
|
│ │ RETIRE_CONNECTION_ID / ACK_ECN
|
||||||
│ ├── http3/ ← Http3FrameReader + Http3Settings + frame types
|
│ ├── http3/ ← Http3FrameReader + Http3Settings + frame types
|
||||||
│ ├── packet/ ← LongHeaderPacket, ShortHeaderPacket, RetryPacket, peekHeader
|
│ ├── packet/ ← LongHeaderPacket, ShortHeaderPacket (with
|
||||||
|
│ │ reserved-bit + key-phase peek), RetryPacket, peekHeader
|
||||||
│ ├── qpack/ ← QpackDecoder, QpackEncoder, QpackHuffman, QpackInteger,
|
│ ├── qpack/ ← QpackDecoder, QpackEncoder, QpackHuffman, QpackInteger,
|
||||||
│ │ QpackStaticTable
|
│ │ QpackStaticTable
|
||||||
│ ├── connection/recovery/ ← RecoveryToken + SentPacket + QuicLossDetection
|
|
||||||
│ │ (RFC 9002 §5/§6 RTT estimator + loss detection +
|
|
||||||
│ │ PTO; per-frame retransmit dispatch)
|
|
||||||
│ ├── recovery/ ← AckTracker (with ack-eliciting gating)
|
│ ├── recovery/ ← AckTracker (with ack-eliciting gating)
|
||||||
│ ├── stream/ ← QuicStream, ReceiveBuffer (with FIN-fully-read), SendBuffer, StreamId
|
│ ├── stream/ ← QuicStream, ReceiveBuffer (with FIN-fully-read +
|
||||||
│ ├── tls/ ← TlsClient state machine + ClientHello/ServerHello/EE/Cert/CV/Finished
|
│ │ per-stream receive-limit), SendBuffer, StreamId
|
||||||
│ │ codecs, TlsKeySchedule, TlsTranscriptHash (incremental), TlsConstants,
|
│ ├── tls/ ← TlsClient state machine + ClientHello (incl. PSK +
|
||||||
|
│ │ early_data resumption variant) / ServerHello / EE /
|
||||||
|
│ │ Cert / CV / Finished / NewSessionTicket codecs,
|
||||||
|
│ │ TlsKeySchedule (incl. 0-RTT + resumption_master_secret),
|
||||||
|
│ │ TlsTranscriptHash (incremental), TlsConstants,
|
||||||
│ │ PermissiveCertificateValidator, TlsRunningSha256 (expect)
|
│ │ PermissiveCertificateValidator, TlsRunningSha256 (expect)
|
||||||
│ ├── transport/ ← UdpSocket (expect)
|
│ ├── transport/ ← UdpSocket (expect)
|
||||||
│ └── webtransport/ ← QuicWebTransportFactory + QuicWebTransportSessionState +
|
│ └── webtransport/ ← QuicWebTransportFactory + QuicWebTransportSessionState +
|
||||||
│ WtPeerStreamDemux + WtCapsule + WtDatagram + ExtendedConnect
|
│ WtPeerStreamDemux + WtCapsule + WtDatagram + ExtendedConnect
|
||||||
└── jvmAndroid/kotlin/com/vitorpamplona/quic/
|
└── jvmAndroid/kotlin/com/vitorpamplona/quic/
|
||||||
├── crypto/JcaAesGcmAead.kt ← cached JCA Cipher per direction with IV-reuse fallback
|
├── crypto/JcaAesGcmAead.kt ← cached JCA Cipher per direction with IV-reuse fallback
|
||||||
|
├── crypto/JcaChaCha20Poly1305Aead.kt ← JCA ChaCha20-Poly1305 (with Quartz fallback)
|
||||||
├── crypto/PlatformCrypto.kt ← actuals
|
├── crypto/PlatformCrypto.kt ← actuals
|
||||||
├── tls/JdkCertificateValidator.kt ← system-trust-store chain validation + RSA-PSS / ECDSA / Ed25519
|
├── tls/JdkCertificateValidator.kt ← system-trust-store chain validation + PSL filter +
|
||||||
|
│ RSA-PSS / ECDSA / Ed25519
|
||||||
├── tls/TlsRunningSha256.kt ← MessageDigest.clone()-based incremental hash
|
├── tls/TlsRunningSha256.kt ← MessageDigest.clone()-based incremental hash
|
||||||
└── transport/UdpSocket.kt ← DatagramChannel + suspend wrapper
|
└── transport/UdpSocket.kt ← DatagramChannel + suspend wrapper
|
||||||
```
|
```
|
||||||
|
|
||||||
## Crypto surface
|
## Crypto surface
|
||||||
|
|
||||||
Quartz primitives only:
|
Quartz primitives + JCA only:
|
||||||
|
|
||||||
| Primitive | Source |
|
| Primitive | Source |
|
||||||
|---|---|
|
|---|---|
|
||||||
| AES-128-GCM | `JcaAesGcmAead` (jvmAndroid) — cached `Cipher` per direction; `Aes128Gcm` singleton (commonMain) for non-hot paths |
|
| AES-128-GCM | `JcaAesGcmAead` (jvmAndroid) — cached `Cipher` per direction; `Aes128Gcm` singleton (commonMain) for non-hot paths |
|
||||||
| ChaCha20-Poly1305 | Quartz `ChaCha20Poly1305` (commonMain pure-Kotlin) wrapped in `ChaCha20Poly1305Aead` |
|
| ChaCha20-Poly1305 | `JcaChaCha20Poly1305Aead` (jvmAndroid, JCA fast path) with Quartz pure-Kotlin fallback in `ChaCha20Poly1305Aead` |
|
||||||
| HKDF-Extract / Expand-Label | Quartz `Hkdf` + `MacInstance`; thin RFC 8446 §7.1 helper in `crypto/HkdfHelpers.kt` |
|
| HKDF-Extract / Expand-Label | Quartz `Hkdf` + `MacInstance`; thin RFC 8446 §7.1 helper in `crypto/HkdfHelpers.kt` |
|
||||||
| SHA-256 (one-shot) | Quartz `sha256(...)` |
|
| SHA-256 (one-shot) | Quartz `sha256(...)` |
|
||||||
| SHA-256 (incremental, for transcript) | `TlsRunningSha256` (expect/actual; jvmAndroid wraps `MessageDigest.clone()`) |
|
| SHA-256 (incremental, for transcript) | `TlsRunningSha256` (expect/actual; jvmAndroid wraps `MessageDigest.clone()`) |
|
||||||
@@ -101,32 +119,58 @@ X.509 chain validation, hostname verification, signature verification all
|
|||||||
delegate to JDK `TrustManagerFactory` / `Signature.getInstance(...)`.
|
delegate to JDK `TrustManagerFactory` / `Signature.getInstance(...)`.
|
||||||
`CertificateValidator` is a non-null typed parameter — tests pass an
|
`CertificateValidator` is a non-null typed parameter — tests pass an
|
||||||
explicit `PermissiveCertificateValidator`; production passes
|
explicit `PermissiveCertificateValidator`; production passes
|
||||||
`JdkCertificateValidator`.
|
`JdkCertificateValidator` (with PSL-subset wildcard rules).
|
||||||
|
|
||||||
## What we deliberately don't do
|
## What we deliberately don't do
|
||||||
|
|
||||||
- **QUIC server role.** Client-only.
|
- **QUIC server role.** Client-only.
|
||||||
- **0-RTT / session resumption.** No PSK extension offered; an arriving
|
|
||||||
ServerFinished without prior Certificate is hard-failed.
|
|
||||||
- **Connection migration / preferred address / multiple paths.** `NEW_CONNECTION_ID`
|
|
||||||
and `PATH_*` frames decode (so peers don't break us) but aren't acted on.
|
|
||||||
- **Path MTU discovery.** Fixed 1200-byte ceiling per RFC 9000 §14.
|
- **Path MTU discovery.** Fixed 1200-byte ceiling per RFC 9000 §14.
|
||||||
|
- **Anti-amplification limits (server side).** We're a client; the server
|
||||||
|
applies its own.
|
||||||
- **HTTP/3 server push.**
|
- **HTTP/3 server push.**
|
||||||
- **QPACK dynamic-table inserts on the encoder.** We send literal-only;
|
- **QPACK dynamic-table inserts on the encoder.** We send literal +
|
||||||
decoder accepts dynamic-table indexed lines.
|
static-indexed only; decoder accepts dynamic-table indexed lines.
|
||||||
- **ECN / anti-amplification limits.** We're a client.
|
- **Congestion control (NewReno / CUBIC / BBR).** Loss detection is
|
||||||
- **TLS Key-Update / NewSessionTicket.** Detected and refused (KeyUpdate
|
in place (RFC 9002 §5/§6) but no rate-limiting feedback loop reacts
|
||||||
fails the connection rather than silently desynchronising).
|
to losses; we send as fast as the application provides bytes. See
|
||||||
- **Congestion control (NewReno / CUBIC / BBR).** Loss detection is in
|
[`2026-05-05-congestion-control.md`](2026-05-05-congestion-control.md)
|
||||||
place (RFC 9002 §5/§6) but no rate-limiting feedback loop reacts to
|
(parked indefinitely; acceptable for moq-lite audio).
|
||||||
losses; we send as fast as the application provides bytes. Independent
|
|
||||||
follow-up.
|
## What's now shipped that the original "deliberately don't do" list disclaimed
|
||||||
|
|
||||||
|
The plan's pre-rewrite version listed the following as out-of-scope. They
|
||||||
|
shipped between 2026-04-26 and 2026-05-09 and now WORK:
|
||||||
|
|
||||||
|
- **0-RTT / session resumption.** TLS 1.3 PSK + `early_data` extension on
|
||||||
|
both ends; client caches `resumption_master_secret`, parses
|
||||||
|
`NewSessionTicket`, and replays 0-RTT data with prior-connection ALPN
|
||||||
|
+ transport params. Verified against picoquic and quic-go.
|
||||||
|
- **TLS Key-Update / NewSessionTicket.** RFC 9001 §6 1-RTT key update —
|
||||||
|
client-initiated AND peer-initiated. `KEY_PHASE`-bit peek before AEAD,
|
||||||
|
in-flight gate, PN-regression check on the rotation packet, proper
|
||||||
|
HKDF roll of `application_traffic_secret_N+1`.
|
||||||
|
- **NEW_CONNECTION_ID / RETIRE_CONNECTION_ID handling, including
|
||||||
|
`retire_prior_to`** per RFC 9000 §5.1.2 + §19.15 (`PathValidator.recordPeerNewConnectionId`
|
||||||
|
with full state machine: stale-offer retire, watermark monotonicity,
|
||||||
|
pool-full bound, duplicate-with-mismatch rejection).
|
||||||
|
- **Path validation (client- and server-initiated).** RFC 9000 §8.2 +
|
||||||
|
§9 — full PATH_CHALLENGE / PATH_RESPONSE round-trip, 3*PTO timeout
|
||||||
|
per §8.2.4, abrupt migration to a new DCID on success, forced
|
||||||
|
rotation when `retire_prior_to` exceeds the active CID.
|
||||||
|
- **ECN (1-RTT only).** ECT(0) on outbound, `ACK_ECN` (0x03) frames
|
||||||
|
with truthful CE / ECT(0) / ECT(1) counters. Initial / Handshake
|
||||||
|
ACKs intentionally skip ECN counts (interop conservatism).
|
||||||
|
|
||||||
|
The remaining pre-rewrite "don't do" items (server role, PMTU, HTTP/3
|
||||||
|
push, QPACK encoder dynamic table, CC) are still out-of-scope.
|
||||||
|
|
||||||
## Verified interop
|
## Verified interop
|
||||||
|
|
||||||
- **aioquic** (Python): `quic-interop-runner`-style Docker setup; full
|
- **aioquic** (Python): `quic-interop-runner`-style Docker setup; full
|
||||||
handshake + Extended CONNECT + h3 datagram round-trip.
|
handshake + multiplexing + Extended CONNECT + h3 datagram round-trip.
|
||||||
- **picoquic** (C): Docker image, lightweight HTTP/3 GET.
|
- **picoquic** (C): Docker image, h3, 0-RTT, key-update, longRTT, ECN
|
||||||
|
test cases pass.
|
||||||
|
- **quic-go** (Go): 0-RTT pass.
|
||||||
- **In-memory pipe** (`InMemoryQuicPipe`, modeled on Cloudflare quiche's
|
- **In-memory pipe** (`InMemoryQuicPipe`, modeled on Cloudflare quiche's
|
||||||
`Pipe`) drives both sides of the handshake in one JVM for fast tests
|
`Pipe`) drives both sides of the handshake in one JVM for fast tests
|
||||||
without sockets.
|
without sockets.
|
||||||
@@ -137,13 +181,19 @@ gates on the audio-rooms completion plan
|
|||||||
|
|
||||||
## Audit summary
|
## Audit summary
|
||||||
|
|
||||||
| Round | Focus | Findings | Status |
|
| Round | Date | Focus | Findings | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 1 | Initial review (pre-interop) | 6 critical correctness/security bugs | all fixed |
|
| 1 | 2026-04-22 | Initial review (pre-interop) | 6 critical correctness/security bugs | all fixed |
|
||||||
| 2 | TLS hardening + lifecycle | hangs + TLS edge cases | all fixed |
|
| 2 | 2026-04-23 | TLS hardening + lifecycle | hangs + TLS edge cases | all fixed |
|
||||||
| 3 | Performance + concurrency | cipher caching, polling, transcript O(n²) | all fixed |
|
| 3 | 2026-04-24 | Performance + concurrency | cipher caching, polling, transcript O(n²) | all fixed |
|
||||||
| 4 | Core + TLS + perf + coverage gaps (4 parallel agents) | ~30 items including 4 CRITICAL interop blockers | all fixed; comprehensive regression tests added |
|
| 4 | 2026-04-25 | Core + TLS + perf + coverage gaps (4 parallel agents) | ~30 items including 4 CRITICAL interop blockers | all fixed; comprehensive regression tests added |
|
||||||
| 5 | Regression check + concurrency-specific (2 parallel agents) | 1 CRITICAL ackEliciting regression I'd just introduced + WT scope leak + others | all fixed |
|
| 5 | 2026-04-26 | Regression check + concurrency-specific (2 parallel agents) | 1 CRITICAL ackEliciting regression I'd just introduced + WT scope leak + others | all fixed |
|
||||||
|
| 6 | 2026-05-04 | Per-frame retransmit + SendBuffer rewrite | retain-until-ACK + control-frame retx + STREAM/CRYPTO recovery | all fixed; see [`2026-05-04-control-frame-retransmit.md`](2026-05-04-control-frame-retransmit.md) |
|
||||||
|
| 7 | 2026-05-05 | Loss-detection timer + PSK rejection signal | RFC 9002 §6.1.2 timer; PSK rejection recover-in-place | all fixed |
|
||||||
|
| 8 | 2026-05-06 | Path validation pass 1 + 2 | DCID rotation, retire-prior-to monotonicity, abrupt migration | all fixed |
|
||||||
|
| 9 | 2026-05-07 | DoS hardening + reserved-bit checks + TLS bounds + SendBuffer shrink | bound peer-controlled buffers and channels | all fixed |
|
||||||
|
| 10 | 2026-05-08 | Lock-split design + close-under-load + key-update PN gate | streamsLock + lifecycleLock split | all fixed; see [`2026-05-08-lock-split-design.md`](2026-05-08-lock-split-design.md) |
|
||||||
|
| 11 | 2026-05-09 | Four-RFC compliance audit (4 parallel agents: 9000, 9001, 9002, 9221+9114+9204) | ~15 items, mostly receive-side validation gaps | catalogued in [§ RFC compliance gaps](#rfc-compliance-gaps-2026-05-09) below |
|
||||||
|
|
||||||
Every fix carries an inline `audit-N #M` reference comment so the regression
|
Every fix carries an inline `audit-N #M` reference comment so the regression
|
||||||
test → fix → comment chain is auditable. The whole audit corpus is in the
|
test → fix → comment chain is auditable. The whole audit corpus is in the
|
||||||
@@ -151,35 +201,48 @@ git log (commits whose subject starts with `fix(quic):` or `perf(quic):`).
|
|||||||
|
|
||||||
## Test inventory
|
## Test inventory
|
||||||
|
|
||||||
Roughly grouped:
|
81 `*Test.kt` files. Roughly grouped:
|
||||||
|
|
||||||
- **RFC vectors:** RFC 8448 §3 (TLS handshake), RFC 9001 §A.1–A.5 (Initial
|
- **RFC vectors:** RFC 8448 §3 (TLS handshake), RFC 9001 §A.1–A.5 (Initial
|
||||||
encrypt/decrypt, Retry, ChaCha20, server-side HP), RFC 9204 §B.1 (QPACK),
|
encrypt/decrypt, Retry, ChaCha20, server-side HP), RFC 9204 §B.1 (QPACK),
|
||||||
RFC 7541 (Huffman).
|
RFC 7541 (Huffman).
|
||||||
- **End-to-end pipe tests:** `InMemoryQuicPipeTest`, `CoalescedPacketSkipTest`,
|
- **End-to-end pipe tests:** `InMemoryQuicPipeTest`, `CoalescedPacketSkipTest`,
|
||||||
`ReceiveLimitEnforcementTest`, `PeerStreamLimitTest`, `FrameRoutingTest`,
|
`ReceiveLimitEnforcementTest`, `PeerStreamLimitTest`, `FrameRoutingTest`,
|
||||||
`AckElicitingFramesTest`.
|
`AckElicitingFramesTest`, `MultiplexingRoundTripTest`,
|
||||||
|
`MultiplexingThroughputTest`, `MultiplexingCoalescingTest`,
|
||||||
|
`MultiStreamFinDeliveryTest`, `MultiplexingAioquicTpsTest`.
|
||||||
- **Adversarial:** `FrameFuzzerTest`, `HostilePacketInputTest`,
|
- **Adversarial:** `FrameFuzzerTest`, `HostilePacketInputTest`,
|
||||||
`TlsSecurityPropertiesTest`, `HelloRetryRequestTest`.
|
`TlsSecurityPropertiesTest`, `HelloRetryRequestTest`,
|
||||||
|
`CloseDatagramRfcComplianceTest`, `CloseUnderLoadTest`.
|
||||||
- **Crypto:** `JcaAesGcmAeadTest`, `ChaCha20Poly1305AeadTest`,
|
- **Crypto:** `JcaAesGcmAeadTest`, `ChaCha20Poly1305AeadTest`,
|
||||||
`TlsTranscriptHashTest`.
|
`TlsTranscriptHashTest`.
|
||||||
- **WT / HTTP/3:** `CapsuleReaderTest`, `WtPeerStreamDemuxTest`,
|
- **WT / HTTP/3:** `CapsuleReaderTest`, `WtPeerStreamDemuxTest`,
|
||||||
`WtFramingTest`.
|
`WtFramingTest`.
|
||||||
- **Recovery:** `AckTrackerCoalescedTest`, `AckTrackerGatingTest`,
|
- **Recovery:** `AckTrackerCoalescedTest`, `AckTrackerGatingTest`,
|
||||||
`RecoveryTokenTest`, `SentPacketTest`, `ReceiverFlowControlTest` (9
|
`AckTrackerPurgeOnAckOfAckTest`, `RecoveryTokenTest`, `SentPacketTest`,
|
||||||
cases mirrored from neqo `fc.rs`), `QuicLossDetectionTest`,
|
`ReceiverFlowControlTest` (9 cases mirrored from neqo `fc.rs`),
|
||||||
`PtoTest`, `QuicConnectionRetransmitTest`,
|
`QuicLossDetectionTest`, `PtoTest`, `PtoCryptoRetransmitTest`,
|
||||||
`SendBufferRetainUntilAckTest` (14 cases for the rewrite),
|
`QuicConnectionRetransmitTest`, `RetransmitIntegrationTest`,
|
||||||
`StreamRetransmitTest`, `CryptoRetransmitTest`,
|
`SendBufferRetainUntilAckTest` (14 cases), `StreamRetransmitTest`,
|
||||||
`ResetStopSendingEmitTest` (7 cases).
|
`CryptoRetransmitTest`, `ResetStopSendingEmitTest` (7 cases),
|
||||||
- **Interop:** `InteropRunner` (jvmTest, drives a real socket against a
|
`OnTokensLostTest`, `MoqLiteLossHarnessTest`.
|
||||||
Dockerised aioquic; opt-in, not in CI).
|
- **Path validation / migration:** `PathValidatorTest`,
|
||||||
|
`PathValidationTest`, `ClientPathMigrationTest`.
|
||||||
|
- **Key lifecycle:** `KeyDiscardTest`, `KeyUpdatePeerInitiatedTest`.
|
||||||
|
- **Concurrency / soak:** `BatchedOpenLockContractTest`,
|
||||||
|
`StreamRetirementSoakTest`, `QuicHeapSoakTest`,
|
||||||
|
`QuicConnectionDriverLifecycleTest`.
|
||||||
|
- **Misc:** `VersionNegotiationTest`, `RetryHandlingTest`,
|
||||||
|
`PacketNumberSpaceTest`, `ConnectionIdTest`,
|
||||||
|
`FlowControlSnapshotTest`, `PendingFlowControlEmitTest`,
|
||||||
|
`PeerStreamCreditExtensionTest`, `PeerUniStreamDrainTest`.
|
||||||
|
- **Interop (jvmTest, opt-in):** `InteropRunner` drives a real socket
|
||||||
|
against a Dockerised aioquic / picoquic / quic-go. Not in CI.
|
||||||
|
|
||||||
## Known limitations / deferred work
|
## Known limitations / deferred work
|
||||||
|
|
||||||
These are the items future audit rounds keep flagging that we've
|
These are items future audit rounds keep flagging that we've consciously
|
||||||
consciously not tackled — all confined to the steady-state path that audio
|
not tackled:
|
||||||
rooms don't exercise heavily:
|
|
||||||
|
|
||||||
1. ~~**No STREAM retransmit on loss** (audit-4 #10).~~ **Resolved 2026-05-05** —
|
1. ~~**No STREAM retransmit on loss** (audit-4 #10).~~ **Resolved 2026-05-05** —
|
||||||
`SendBuffer` rewritten for retain-until-ACK with three-state range
|
`SendBuffer` rewritten for retain-until-ACK with three-state range
|
||||||
@@ -188,27 +251,85 @@ rooms don't exercise heavily:
|
|||||||
and RESET_STREAM / STOP_SENDING / NEW_CONNECTION_ID retransmit.
|
and RESET_STREAM / STOP_SENDING / NEW_CONNECTION_ID retransmit.
|
||||||
See [`2026-05-04-control-frame-retransmit.md`](2026-05-04-control-frame-retransmit.md).
|
See [`2026-05-04-control-frame-retransmit.md`](2026-05-04-control-frame-retransmit.md).
|
||||||
2. ~~**`SendBuffer` doesn't retain bytes until ACK.**~~ Resolved with #1.
|
2. ~~**`SendBuffer` doesn't retain bytes until ACK.**~~ Resolved with #1.
|
||||||
3. ~~**No Initial / Handshake key discard.**~~ **Resolved 2026-05-05** —
|
3. ~~**No Initial / Handshake key discard.**~~ **Resolved 2026-05-05.**
|
||||||
`LevelState.discardKeys()` nulls the AEAD protection, replaces the
|
4. ~~**No path validation for `NEW_CONNECTION_ID`.**~~ **Resolved 2026-05-06–08** —
|
||||||
per-level CRYPTO buffers and ack-tracker with empty instances, and
|
full client-initiated DCID rotation + server-initiated path
|
||||||
clears the sent-packet map. Initial keys discarded by the writer
|
validation per RFC 9000 §8.2 + §9. See `PathValidator.kt` and the
|
||||||
after the first Handshake packet is built (RFC 9001 §4.9.1);
|
three test files (`PathValidatorTest`, `PathValidationTest`,
|
||||||
Handshake keys discarded by the parser on receipt of HANDSHAKE_DONE
|
`ClientPathMigrationTest`).
|
||||||
(RFC 9001 §4.9.2 + §4.1.2 client-side handshake confirmation).
|
5. **Stateless reset detection.** Stateless-reset tokens are now
|
||||||
4. **No path validation for `NEW_CONNECTION_ID`.** We don't migrate.
|
*recorded* (per `PeerConnectionIdEntry`) but no inbound check
|
||||||
5. **Stateless reset detection.** Stateless-reset packets look like
|
compares "looks-like-noise" datagrams against the token list. RFC
|
||||||
corruption to us.
|
9000 §10.3.
|
||||||
6. ~~**`AckTracker.purgeBelow` threshold semantics.**~~ **Resolved
|
6. ~~**`AckTracker.purgeBelow` threshold semantics.**~~ **Resolved
|
||||||
2026-05-05** — `RecoveryToken.Ack` now carries `(level, largestAcked)`;
|
2026-05-05.**
|
||||||
the writer captures these from the AckFrame at emit time, and
|
|
||||||
`QuicConnection.onTokensAcked` purges the matching per-level inbound
|
|
||||||
tracker on ACK-of-ACK. The wrong purge in the parser is gone.
|
|
||||||
7. **Driver direct unit tests** require turning `UdpSocket` from `expect
|
7. **Driver direct unit tests** require turning `UdpSocket` from `expect
|
||||||
class` into an interface so the test side can stub. The driver is
|
class` into an interface so the test side can stub. The driver is
|
||||||
covered indirectly by every pipe-based test plus the live interop
|
covered indirectly by every pipe-based test plus the live interop
|
||||||
runner.
|
runner.
|
||||||
8. **No congestion control.** Loss detection is wired but nothing
|
8. **No congestion control.** Loss detection is wired but nothing
|
||||||
throttles send rate in response. Independent ~1-2 wk project.
|
throttles send rate in response. Independent ~1–2 wk project.
|
||||||
|
See [`2026-05-05-congestion-control.md`](2026-05-05-congestion-control.md).
|
||||||
|
|
||||||
|
## RFC compliance gaps (2026-05-09)
|
||||||
|
|
||||||
|
Catalogued from a four-agent compliance audit (RFC 9000 / 9001 / 9002 /
|
||||||
|
9221+9114+9204). Each item lists the spec section, the gap, and
|
||||||
|
file:line evidence. Severity:
|
||||||
|
|
||||||
|
- 🔴 **High** — exploitable by a hostile peer (resource exhaustion,
|
||||||
|
silent desync, RTT poisoning).
|
||||||
|
- 🟡 **Medium** — interop or correctness risk; cooperative peers
|
||||||
|
unaffected.
|
||||||
|
- 🟦 **Low / cosmetic** — diagnostic loss, doesn't break the wire.
|
||||||
|
|
||||||
|
### Receive-side validation gaps
|
||||||
|
|
||||||
|
| RFC § | Sev | Gap | Evidence |
|
||||||
|
|---|---|---|---|
|
||||||
|
| RFC 9000 §17.3 | 🔴 | **Inbound short-header fixed-bit (0x40) not validated.** RFC says fixed-bit MUST be 1 on receive. Form-bit (0x80) and reserved-bit (0x18) ARE checked, but 0x40 is never asserted post-HP-unmask. | `ShortHeaderPacket.kt:163-187` (form-bit checked, fixed-bit not) |
|
||||||
|
| RFC 9000 §10.1 | 🔴 | **`max_idle_timeout` not enforced.** Decoded into config and advertised, but no idle deadline tracking, no auto-close on silence. Connection lives forever if no I/O happens. | `QuicConnection.kt:1040,1054,1069`; `QuicConnectionDriver.kt` (no idle timer) |
|
||||||
|
| RFC 9000 §4.1 | 🟡 | **Connection-level inbound flow control missing.** Per-stream `receiveLimit` IS enforced (`QuicConnectionParser.kt:655`) — peer overshoot closes the connection. But the aggregate connection-level cap (matching what we advertised in `initial_max_data`) is not tracked on the receive side. | `QuicConnectionParser.kt:735-743` (only updates send-side credit on MAX_DATA) |
|
||||||
|
| RFC 9000 §3 | 🟡 | **Stream state machine implicit, not validated.** The 10-state machine (Ready / Send / Data Sent / Data Recvd / Reset Sent / Reset Recvd × directions) is encoded as flags + buffer content. STREAM frames arriving after a RESET_STREAM are silently absorbed instead of raising STREAM_STATE_ERROR. (FIN-size mismatch *is* caught — `QuicConnectionParser.kt:672-685`.) | `QuicStream.kt:35-51` (no explicit state field) |
|
||||||
|
| RFC 9000 §10.2 | 🟡 | **No DRAINING state.** Closing transitions go CONNECTED → CLOSING → CLOSED without the 3 × PTO hold the spec mandates. Late inbound packets after our own CONNECTION_CLOSE may not get a re-emitted close. | `QuicConnection.kt:301,1441-1531` (Status enum has no DRAINING) |
|
||||||
|
| RFC 9000 §10.3 | 🟡 | **Stateless reset detection missing.** Tokens are stored in `PeerConnectionIdEntry` but no inbound matcher compares fail-to-decrypt datagrams against the token list. Spoofed peer-side resets look like noise. | `PathValidator.kt:35-43`; no consumer in parser |
|
||||||
|
| RFC 9001 §6.6 | 🟡 | **AEAD invocation limit not tracked.** AES-128-GCM is bounded at 2^23 invocations (~8M packets). Past the limit, key rotation is mandatory. We don't count, don't rotate, don't close with AEAD_LIMIT_REACHED. ChaCha20 is effectively unbounded so less critical. | `Aead.kt`, `JcaAesGcmAead.kt` (no counter) |
|
||||||
|
|
||||||
|
### Transport-parameter bounds
|
||||||
|
|
||||||
|
| RFC § | Sev | Gap | Evidence |
|
||||||
|
|---|---|---|---|
|
||||||
|
| RFC 9000 §18.2 | 🟡 | **`max_udp_payload_size < 1200` not rejected.** Spec says minimum 1200; smaller values MUST close with TRANSPORT_PARAMETER_ERROR. | `TransportParameters.kt:165` (decode-only) |
|
||||||
|
| RFC 9000 §18.2 | 🟡 | **`ack_delay_exponent > 20` not rejected.** Spec maximum is 20. | `TransportParameters.kt:166` (decode-only) |
|
||||||
|
| RFC 9000 §18.2 | 🟡 | **`active_connection_id_limit < 2` not rejected.** Spec minimum is 2. (We already cap our own pool at the peer's value via `PathValidator.maxUnusedCids`, so the runtime risk is bounded — just no explicit error.) | `TransportParameters.kt:168`; `QuicConnection.kt:582` |
|
||||||
|
| RFC 9000 §13.2.5 | 🟦 | **`ack_delay_exponent` decode uses our config, not peer's advertised value.** Default (3) matches both ends in practice, but a peer that advertises a non-default exponent gets misinterpreted ACK delays. The shift IS overflow-safe (`QuicConnectionParser.kt:540-553`), just keyed off the wrong side's parameter. | `QuicConnectionParser.kt:547` (uses `conn.config.ackDelayExponent`) |
|
||||||
|
|
||||||
|
### HTTP/3 + DATAGRAM gaps
|
||||||
|
|
||||||
|
| RFC § | Sev | Gap | Evidence |
|
||||||
|
|---|---|---|---|
|
||||||
|
| RFC 9114 §7.2.4.1 | 🟡 | **HTTP/2 reserved SETTINGS ids 0x02/0x03/0x04/0x05 not rejected.** RFC says receipt MUST close with H3_SETTINGS_ERROR. We accept and store. | `Http3Settings.kt:85-121` |
|
||||||
|
| RFC 9221 §3 | 🟡 | **Outbound DATAGRAM size not gated by peer's `max_datagram_frame_size`.** Encoder will send oversize frames; spec-conformant peers will close with PROTOCOL_VIOLATION. | `Frame.kt:278-291` (no size check on send) |
|
||||||
|
| RFC 9114 §6.2.1 | 🟦 | **Closing the control stream surfaces a flag rather than auto-closing.** Spec says closing the control stream is H3_CLOSED_CRITICAL_STREAM. Caller must poll `peerH3ProtocolError` and close. | `Http3FrameReader.kt`; `WtPeerStreamDemux.kt` |
|
||||||
|
|
||||||
|
### TLS / error reporting cosmetics
|
||||||
|
|
||||||
|
| RFC § | Sev | Gap | Evidence |
|
||||||
|
|---|---|---|---|
|
||||||
|
| RFC 9001 §4.8 | 🟦 | **TLS alerts not mapped to CRYPTO_ERROR + alert_code.** Connection still closes (a generic `QuicCodecException` propagates) but the alert code is lost — debuggability hit, not a wire-spec violation. | `TlsClient.kt:311-317` (catches as `Throwable`) |
|
||||||
|
| RFC 9000 §22 | 🟦 | **Several error codes never emitted.** INVALID_TOKEN (no Retry token validation), CRYPTO_BUFFER_EXCEEDED (no buffer limit), KEY_UPDATE_ERROR (uses generic exception path on KeyUpdate failures, despite key update being implemented). | various |
|
||||||
|
|
||||||
|
### Gaps the 2026-05-09 audit got WRONG
|
||||||
|
|
||||||
|
For audit traceability — three audit findings fingered code that's
|
||||||
|
actually compliant. Listed here so future audit rounds don't repeat
|
||||||
|
them:
|
||||||
|
|
||||||
|
| Audit claim | Reality |
|
||||||
|
|---|---|
|
||||||
|
| "Inbound flow-control enforcement missing." | Per-stream IS enforced. `QuicConnectionParser.kt:655` raises a connection close when STREAM offset+len exceeds `stream.receiveLimit`. The actual gap is *connection-level* (above). |
|
||||||
|
| "ack_delay_exponent not applied on decode." | IS applied at `QuicConnectionParser.kt:540-553`, with explicit overflow protection per audit-4. The actual gap is using *our* exponent instead of the peer's. |
|
||||||
|
| "Short-header reserved bits not validated." | ARE validated at `ShortHeaderPacket.kt:182-187` — `QuicProtocolViolationException` raised on any of the 0x18 bits set. The `0x30` mask the audit cited was a mis-read of RFC 9000 §17.3 (reserved bits are 0x18). |
|
||||||
|
|
||||||
## Pointers
|
## Pointers
|
||||||
|
|
||||||
@@ -216,5 +337,7 @@ rooms don't exercise heavily:
|
|||||||
- Audio-rooms NIP draft: `docs/plans/2026-04-22-nip-audio-rooms-draft.md`
|
- Audio-rooms NIP draft: `docs/plans/2026-04-22-nip-audio-rooms-draft.md`
|
||||||
- Completion plan: `nestsClient/plans/2026-04-26-audio-rooms-completion.md`
|
- Completion plan: `nestsClient/plans/2026-04-26-audio-rooms-completion.md`
|
||||||
- Retransmit plan + implementation log: [`2026-05-04-control-frame-retransmit.md`](2026-05-04-control-frame-retransmit.md)
|
- Retransmit plan + implementation log: [`2026-05-04-control-frame-retransmit.md`](2026-05-04-control-frame-retransmit.md)
|
||||||
|
- Congestion control deferral rationale: [`2026-05-05-congestion-control.md`](2026-05-05-congestion-control.md)
|
||||||
|
- Lock-split design: [`2026-05-08-lock-split-design.md`](2026-05-08-lock-split-design.md)
|
||||||
- Live interop runner: `quic/src/jvmTest/.../interop/InteropRunner.kt`
|
- Live interop runner: `quic/src/jvmTest/.../interop/InteropRunner.kt`
|
||||||
- Audit history: `git log --grep='audit' -- quic/`
|
- Audit history: `git log --grep='audit\|fix(quic)\|feat(quic)' -- quic/`
|
||||||
|
|||||||
Reference in New Issue
Block a user