From fdb0ac2524302fb58b67bd989eb20846059fc20d Mon Sep 17 00:00:00 2001 From: Jorijn van der Graaf Date: Sun, 1 Nov 2026 01:46:29 +0100 Subject: [PATCH] docs(listener): ListenerQUIC::Port(), and port 0 for tests The README gains a QUIC section, which it never had: plain QUIC under an ALPN of your own, starting with a listener on port 0 and Port(). The consumer notes describe Port() and point tests at port 0 rather than at binding a socket by hand; the agent notes count the new test and make port 0 the default for a new one. Co-Authored-By: Claude Opus 5.5 --- AGENTS.consumer.md | 4 ++-- AGENTS.template.md | 4 ++-- README.md | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/AGENTS.consumer.md b/AGENTS.consumer.md index ae8486e..a9c9033 100644 --- a/AGENTS.consumer.md +++ b/AGENTS.consumer.md @@ -82,7 +82,7 @@ A route map is `std::unordered_map`; the `SendRecieve*` and `Interop*` families end in `HTTP1` or `HTTPS1` on the HTTP/1.1 side and `HTTP` or `QUIC` on the QUIC side, and `ShouldSend`, `ShouldNotDropEarlyStreams`, `ShouldFallbackUnknownRoutes` and `ShouldSurviveConnectionChurn` carry no suffix. Open `main.cpp` with a comment on the behaviour under test and the failure it guards against; `tests/ShouldRecieveTCP/main.cpp` and `tests/ShouldSurviveConnectionChurn/main.cpp` show the shape, and a regression test names the bug. - A `Check(ok, what)` helper in an anonymous namespace counts failures and prints them (`PASS`/`FAIL` lines in the newer tests), and `main` returns nonzero at the end; the oldest QUIC tests return at the first failure. Do not stop at the first failure unless later steps are meaningless without it. -- Each test owns fixed ports, distinct across the suite because the runner is parallel: 8090 to 8097 and 8110 to 8114 for HTTP/1.1 listeners, 8082 to 8086 and 8098 for HTTP/3, 9181, 9182, 9187 and 9188 for QUIC, 19200 to 19224 for the churn test; the two TCP tests bind ephemeral ports, and `ShouldBuildWithoutQUIC` and `ShouldSend` bind none. Pick an unused one for a new test, or bind an ephemeral port yourself (`LoopbackListener` in `ShouldConfigureTCP`). +- Each test owns fixed ports, distinct across the suite because the runner is parallel: 8090 to 8097 and 8110 to 8114 for HTTP/1.1 listeners, 8082 to 8086 and 8098 for HTTP/3, 9181, 9182, 9187 and 9188 for QUIC, 19200 to 19224 for the churn test; the two TCP tests bind ephemeral ports, as do the QUIC tests written since `ListenerQUIC::Port()` (port 0, read back), and `ShouldBuildWithoutQUIC` and `ShouldSend` bind none. A new test binds port 0 and reads `Port()` (`ListenerTCP` and `ListenerQUIC` both have it); a fixed port is for when it cannot. - `ThreadPool::Start()` first, for anything that enqueues: every `*Async` call, `ClientHTTP` (its constructor installs an `OnStream` drain that runs as pool tasks), `ListenerHTTP`, `ListenerQUIC`, and the `*Async`-dispatch `Listen*` variants of `ListenerTCP`; `ListenerHTTP1` and the sync TCP listeners need none, which is why ten tests never call it. `Start()` runs `hardware_concurrency() - 1` workers and `Start(1)` none at all (tasks then wait for a `JoinPool()` nobody calls), so don't use `Start(1)` to make callbacks deterministic. - A QUIC test ends in `std::_Exit(code)`, after `std::fflush(stdout)` since `_Exit` flushes nothing (`ShouldReportClosedQUICStream` does; the older ones skip the flush and can lose their last line in the log): msquic's two registrations are closed by static destructors that block until every connection is torn down, and the peers a listener accepted are heap-allocated for the callback's owner, so returning from `main` waits for the idle timeout. `ShouldSurviveConnectionChurn` is the exception on purpose: that wait is its assertion, so it deletes everything and returns, under a watchdog thread that calls `std::_Exit(1)` after 90 s, which the runner's 60 s timeout beats; a watchdog meant to fire goes under 60 s, as the 8 s one in `ShouldNotDropEarlyStreams` does. Any test that can hang gets one. - Credentials: `QUICServerCredentials{ .selfSigned = true }` against `QUICClientCredentials{ .insecureNoServerValidation = true }`; for TLS, `TLSServerCredentials{ .selfSigned = true }` against a client given `GetSelfSignedCertificatePem().certificate` as `caPem`, so verification is exercised rather than skipped. diff --git a/README.md b/README.md index ece27d0..d734e36 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,23 @@ std::thread([&] { broker.ListenSyncSync(); }).detach(); // ... broker.Stop() ends the loop from another thread. ``` +### QUIC Components + +Plain QUIC under an ALPN of your own, for a protocol that is not HTTP. A browser cannot reach these: it speaks QUIC only as WebTransport, see [WebTransport Components](#webtransport-components). + +#### ListenerQUIC +```cpp +Crafter::QUICServerCredentials creds; +creds.selfSigned = true; // dev-only, see "Self-signed certs & browser peers" +Crafter::ListenerQUIC listener(0, "myproto/1", creds, [](Crafter::ClientQUIC* peer) { + // An accepted connection, heap-allocated and yours to delete. +}); +listener.ListenAsyncAsync(); +const std::uint16_t port = listener.Port(); // the port msquic picked for port 0 +``` + +`Port()` reports the UDP port the listener is bound to, so port 0 is usable: the listener takes whatever port is free and tells you which. + ### HTTP/3 Components HTTP/3 runs over QUIC, which always requires TLS. Pass server credentials when constructing the listener (or set `selfSigned = true` for a development-only ephemeral cert) and matching client credentials when constructing the client (`insecureNoServerValidation = true` for self-signed servers). @@ -316,6 +333,7 @@ The library includes tests covering: - HTTP/3 external interop (`ShouldSend`) — live fetch from `cloudflare-quic.com:443`, exercises real TLS chain validation, mandatory control stream, peer-initiated unidi streams, and QPACK Huffman decoding - QUIC reliable streams (`ShouldSendRecieveQUICStream`) - QUIC unreliable datagrams (`ShouldSendRecieveQUICDatagram`) +- QUIC listener ports (`ShouldReportListenerPortQUIC`) — a listener on port 0 reports the port it got and is reachable there, a second one gets another, and a fixed port reads back as itself - QUIC connection teardown (`ShouldSurviveConnectionChurn`) — 25 build-up/tear-down cycles with reader threads parked inside `RecieveSync`, the shape a reconnect loop has; the rest of the QUIC tests end in `std::_Exit(0)` and so never run a `ClientQUIC` destructor - WebTransport echo (`ShouldEchoWebTransport`) — extended-CONNECT acceptance, draft-02 SETTINGS, bidi data stream framing (`WT_STREAM 0x41` + session-id varint), and byte-for-byte echo - HTTP/1.1 wire format (`ShouldParseHTTP1`) — serialisation, incremental parsing fed one byte at a time, chunked bodies with trailers, pipelining, interim 1xx, HTTP/1.0 and HEAD framing, and the malformed inputs the parser is required to reject