diff --git a/AGENTS.consumer.md b/AGENTS.consumer.md index ca08e6a..287c366 100644 --- a/AGENTS.consumer.md +++ b/AGENTS.consumer.md @@ -84,7 +84,7 @@ A route map is `std::unordered_map/` (a native crafter-build uses `~/.cache/crafter.build/`): several minutes the first time, then shared by every project with the same spec. msquic is configured with `QUIC_TLS_LIB=openssl` against the OpenSSL the sandbox provides, 3.5 or newer (the Flatpak runtime has 3.5.9), as a shared `libmsquic.so` that the test binaries and consumers find through an absolute runpath into that cache (a directory named after msquic's spec hash, which `crafter-build clean` never touches). msquic links `libnuma` when the build host has it: a native crafter-build on Arch does, the Flatpak does not. -What the suite needs beyond that: the `openssl` CLI (the QUIC self-signed certificate and its hash; the TLS partition makes its own certificate in-process), `curl` and `python3` for the interop tests, and outbound UDP/443 to `cloudflare-quic.com` for `ShouldSend`. The Flatpak sandbox has `openssl`, `curl`, `python3` and `wasmtime` and no `node`; its `/tmp` is a per-app directory under `$XDG_RUNTIME_DIR/.flatpak/net.catcrafts.CrafterBuild/tmp`, shared by instances running at the same time and discarded once none is, so the QUIC certificate cache under `/tmp` rarely survives between runs; and it sees the host filesystem but not the host's `PATH` (`/run/host/usr/bin` is the host's `/usr/bin`). The interop tests print `skipping the ... half` and pass when a tool is missing: a pass that tested less, so read the log. +What the suite needs beyond that: `curl` and `python3` for the interop tests, the `openssl` CLI for the mutual-TLS half of `ShouldRequireClientCertificateHTTPS1` (both development certificates are made in-process), and outbound UDP/443 to `cloudflare-quic.com` for `ShouldSend`. The Flatpak sandbox has `openssl`, `curl`, `python3` and `wasmtime` and no `node`; its `/tmp` is a per-app directory under `$XDG_RUNTIME_DIR/.flatpak/net.catcrafts.CrafterBuild/tmp`, shared by instances running at the same time and discarded once none is; its `XDG_CACHE_HOME` is `~/.var/app/net.catcrafts.CrafterBuild/cache`, so the QUIC tests' certificate lives in `crafter-network/quic-cert/` there and survives between runs (delete the directory to start cold; `ShouldShareSelfSignedCertificateQUIC` points `XDG_CACHE_HOME` at a scratch directory of its own); and it sees the host filesystem but not the host's `PATH` (`/run/host/usr/bin` is the host's `/usr/bin`). The interop tests print `skipping the ... half` and pass when a tool is missing: a pass that tested less, so read the log. Build outputs go to `bin/crafter-network----/` (the PCMs, `std.pcm` and `libcrafter-network.a`; the wasm variant also carries `network-env.js`) and `build//`. The hash covers the variant, so the default build, `--no-quic` and the browser build keep separate directories, and `bin/` also holds one directory per test. The Flatpak's host triple is `x86_64-unknown-linux-gnu`; the README's `x86_64-pc-linux-gnu` default is a native crafter-build's, and `--target=` with either is the same build in another directory. `crafter-build clean` removes them all. @@ -51,7 +51,7 @@ crafter-build # native: TCP, TLS, HTTP/1.1, QUI crafter-build --no-quic # native without QUIC, HTTP/3, WebTransport and msquic crafter-build --local # Crafter.Thread from ../Crafter.Thread crafter-build --target=wasm32-wasip1 # the browser library -crafter-build test # the suite, native: 32 tests +crafter-build test # the suite, native: 33 tests crafter-build test 'Should*HTTP1' # a name glob crafter-build test --no-quic # the TCP, URL, TLS and HTTP/1.1 tests plus ShouldBuildWithoutQUIC: 19 crafter-build test --list @@ -119,8 +119,8 @@ Decided deliberately; change them only with the maintainer's say-so. ## Gotchas - **`ThreadPool::Enqueue` before `ThreadPool::Start()` is a null dereference**, and most of the library enqueues: every `*Async`, every QUIC `OnStream` and `OnDatagram` delivery, `ClientHTTP` and `ListenerHTTP` entirely. The crash has no message; in a test it shows as ๐Ÿ’ฅ. -- **Two self-signed certificates.** `QUICServerCredentials::selfSigned` is made by the `openssl` CLI and cached at `/tmp/crafter-network-quic-cert/`, reused across restarts while valid so a browser's pinned hash survives a restart, except inside the Flatpak, whose `/tmp` goes away with its last instance (see "Dev environment"); `TLSServerCredentials::selfSigned` is made in-process by `:TLS` and is new every start. `GetSelfSignedCertificatePath()` is the first, `GetSelfSignedCertificatePem()` the second. -- **`ComputeCertificateHashSHA256` and the QUIC certificate shell out to `openssl`** (`std::system`, `popen`) and throw `QUICException` without the CLI. `:TLS` could do both in-process; nobody has moved them yet. +- **Two self-signed certificates, both made in-process by `:TLS`.** `QUICServerCredentials::selfSigned` is kept per user at `$XDG_CACHE_HOME/crafter-network/quic-cert/` (else `~/.cache/...`, else `$XDG_RUNTIME_DIR/...`), reused across restarts until a day before it expires so a browser's pinned hash survives a restart, and shared by processes that start together through a record lock on `lock` there; the listener hands msquic the pair as an in-memory PKCS#12, not the paths. `TLSServerCredentials::selfSigned` is new every start and carries `keyEncipherment` the QUIC one lacks. `GetSelfSignedCertificatePath()` is the first, `GetSelfSignedCertificatePem()` the second. To start the QUIC one cold, delete that directory. +- **`:TLS` declares module-internal functions** (`MakeWebTransportCertificatePem`, `CertificatePairExpiry`, `EncodePkcs12`, `CertificateSha256`) without `export`: `:ListenerQUIC` imports `:TLS` for them so libssl stays behind one partition, and a consumer never sees them. - **msquic's registration is closed by a static destructor that waits for every handle.** A test that returns from `main` with an accepted peer alive waits two minutes, the idle timeout; the tests `_Exit`. The exception, `ShouldSurviveConnectionChurn`, exists because a leaked stream handle once hung that close forever (`fix(quic)` in `git log`, with the `SHUTDOWN_ON_FAIL` story). - **Callbacks the interfaces say run "on the msquic worker" run as pool tasks**: `ClientQUIC::OnStream` and `OnDatagram` both enqueue. What arrives before a handler is set is queued: streams are delivered on `OnStream`; datagrams go to `RecieveDatagramSync`'s queue and never to a later `OnDatagram`. - **`ListenerQUIC::ListenAsync*` run the accept loop on a `std::thread`**, not the pool as their comments say, and `ListenerHTTP::Listen` is `ListenSyncAsync` underneath, so a `ListenerHTTP`'s per-connection setup is a pool task. A saturated pool delays a new connection's SETTINGS; the early streams are buffered meanwhile (`ShouldNotDropEarlyStreams`). @@ -128,7 +128,7 @@ Decided deliberately; change them only with the maintainer's say-so. - **`QUICSettings::peerUnidiStreamCount` is the first thing to raise** when a peer stalls opening streams; it was a hardcoded 16 until `ShouldHonourQUICSettings`. - **The comments about `-fno-exceptions` in `project.cpp`, the umbrella and the browser unit are history.** crafter-build 4.0.0 compiles wasm with exceptions on and refuses `-fno-exceptions`; `:HTTP3` stays out of the browser build and the browser units still report through callbacks because the JS side has no exception channel, not because `throw` would not compile. - **The header comment of `Crafter.Network-HTTP3.cppm` is stale**: the partition is exported from the umbrella natively and has a Huffman decoder. The README is right. -- **More comments the code outgrew**: `-ListenerQUIC.cpp` says the certificate pair lives in a `mkdtemp`'d directory for the life of the process (it is the fixed `/tmp/crafter-network-quic-cert/`, reused) and is valid 13 days (`-days 10`); `ClientQUIC.cppm` calls `selfSigned` an in-memory certificate (it is on disk, made by the CLI); `ListenerHTTP.cppm` says `Listen()` dispatches on the calling thread (it is `ListenSyncAsync`); the link-flag comment in `project.cpp` still has msquic bundling quictls (it links the system `libssl`). Fix a comment when you touch its code. +- **More comments the code outgrew**: `ListenerHTTP.cppm` says `Listen()` dispatches on the calling thread (it is `ListenSyncAsync`); the link-flag comment in `project.cpp` still has msquic bundling quictls (it links the system `libssl`). Fix a comment when you touch its code. - **`WebTransportSession::OpenStream(true)` does not throw** as its comment says; it writes the unidirectional prefix. Peer-initiated unidirectional WebTransport streams are drained by the listener and never reach a session. - **`ListenerTCP::concurrentClientLimit` is stored and never read**; only `totalClientLimit` ends the loop. - **`ListenerTCP(port, ...)` listens on every IPv4 address**; given an address it binds that one, IPv4 or IPv6, and port 0 asks the kernel for one. `ClientTCP` resolves for both families and tries every address; QUIC connects on either family (`QUIC_ADDRESS_FAMILY_UNSPEC`), and `ListenerQUIC(port, ...)` listens on both. `ListenerQUIC(address, port, ...)` listens on that address only, but msquic binds the dual-stack wildcard UDP socket for the port anyway and filters in `QuicBindingGetListener`: a connection to another local address is closed with `no_application_protocol` (the client sees `0x5c` in milliseconds), not ignored, and no other UDP socket can take that port on another address. diff --git a/README.md b/README.md index 7ed2eb0..6d36806 100644 --- a/README.md +++ b/README.md @@ -299,7 +299,7 @@ Browser-initiated bidi/unidi WebTransport streams arrive via `session.OnStream(. ### Self-signed certs & browser peers -Passing `QUICServerCredentials{selfSigned = true}` makes the listener generate (and cache) an ephemeral cert at `/tmp/crafter-network-quic-cert/{cert,key}.pem` and reuse it across server restarts while it's still valid. The cert is shaped to satisfy Chromium's `WebTransport.serverCertificateHashes` rules: ECDSA P-256, signed with ECDSA-SHA256, validity โ‰ค14 days (10 in practice), `BasicConstraints CA:FALSE`, `KeyUsage digitalSignature`, `EKU serverAuth`, `SAN=DNS:localhost,IP:127.0.0.1,IP:::1`. To let a browser peer pin this cert without trusting a CA: +Passing `QUICServerCredentials{selfSigned = true}` makes the listener serve a development certificate it makes in-process (no `openssl` CLI) and keeps per user at `$XDG_CACHE_HOME/crafter-network/quic-cert/{cert,key}.pem` (`~/.cache/...` without `XDG_CACHE_HOME`, `$XDG_RUNTIME_DIR/...` without a home), in a directory only that user can enter. It is reused across server restarts until it has less than a day left, then replaced by the next process that needs it. Processes starting at the same moment share one pair: a lock covers the check and the replacement, each file is renamed into place whole, and the listener loads the pair from memory, so none ends up with one run's key and another's certificate. The cert is shaped to satisfy Chromium's `WebTransport.serverCertificateHashes` rules: ECDSA P-256, signed with ECDSA-SHA256, validity โ‰ค14 days (10 in practice), `BasicConstraints CA:FALSE`, `KeyUsage digitalSignature`, `EKU serverAuth`, `SAN=DNS:localhost,IP:127.0.0.1,IP:::1`. To let a browser peer pin this cert without trusting a CA: ```cpp auto certPath = Crafter::GetSelfSignedCertificatePath(); @@ -341,6 +341,7 @@ The library includes tests covering: - 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 loopback listeners (`ShouldBindLoopbackQUIC`) โ€” a listener on `127.0.0.1` serves loopback clients and refuses, at once, clients through the host's LAN address or `::1`; `::1` works too, and a name is refused +- The QUIC development certificate (`ShouldShareSelfSignedCertificateQUIC`) โ€” six processes start `selfSigned` listeners together from an empty cache, with no `openssl` on `PATH`, and all serve one certificate; its directory is private, a later process reuses it, and a mismatched pair on disk is replaced - 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 @@ -367,8 +368,7 @@ The external-interop test requires outbound UDP/443; if your network blocks it t - **Crafter.Thread**: Thread pool management for asynchronous operations. - **msquic** (native target only) โ€” fetched and built automatically as a Crafter `ExternalDependency` (no system install required). The build clones `microsoft/msquic` at a pinned release (v2.6.3), with the submodules that release records, into the per-project external cache, configures it via CMake (`QUIC_TLS_LIB=openssl` against the system OpenSSL, tests/tools/perf disabled), and links the produced `libmsquic` into the QUIC and HTTP/3 modules. Skipped entirely on browser builds. - On Linux msquic links against `libnuma` (provided by the `numactl` package on most distros). -- The self-signed-cert path used by tests/dev shells out to the `openssl` CLI; install `openssl` if you intend to use `QUICServerCredentials{selfSigned = true}`. The same path also produces the cert hash that browser peers need for `serverCertificateHashes`. -- **libssl / libcrypto** (OpenSSL 3, native target only) โ€” a *system* package, unlike msquic, linked via `-lssl -lcrypto`. Backs `Crafter.Network:TLS`, i.e. `https://` on the HTTP/1.1 client and listener. It is not built here on purpose, and msquic uses it too, so the process has one OpenSSL: msquic's QUIC support needs OpenSSL 3.5 or newer (Arch has it, as does the Flatpak runtime crafter-build builds in). msquic's own bundled fork (quictls) is not used: it is an older OpenSSL that still reads the system's `openssl.cnf`, and a config written for the system's newer OpenSSL (the Flatpak runtime's loads a PKCS#11 engine, with `config_diagnostics` on) made its initialisation fail. Install your distro's OpenSSL development package (`openssl` on Arch, `libssl-dev` on Debian/Ubuntu). Where that OpenSSL is older than 3.5 (Debian 12 and Ubuntu 24.04 ship 3.0), msquic doesn't build against it: build with `--no-quic`, which keeps TCP, TLS and HTTP/1.1. Certificate generation for `TLSServerCredentials{selfSigned = true}` happens in-process through this library โ€” no `openssl` CLI needed. +- **libssl / libcrypto** (OpenSSL 3, native target only) โ€” a *system* package, unlike msquic, linked via `-lssl -lcrypto`. Backs `Crafter.Network:TLS`, i.e. `https://` on the HTTP/1.1 client and listener. It is not built here on purpose, and msquic uses it too, so the process has one OpenSSL: msquic's QUIC support needs OpenSSL 3.5 or newer (Arch has it, as does the Flatpak runtime crafter-build builds in). msquic's own bundled fork (quictls) is not used: it is an older OpenSSL that still reads the system's `openssl.cnf`, and a config written for the system's newer OpenSSL (the Flatpak runtime's loads a PKCS#11 engine, with `config_diagnostics` on) made its initialisation fail. Install your distro's OpenSSL development package (`openssl` on Arch, `libssl-dev` on Debian/Ubuntu). Where that OpenSSL is older than 3.5 (Debian 12 and Ubuntu 24.04 ship 3.0), msquic doesn't build against it: build with `--no-quic`, which keeps TCP, TLS and HTTP/1.1. Certificate generation for `TLSServerCredentials{selfSigned = true}` and `QUICServerCredentials{selfSigned = true}`, and the hash `ComputeCertificateHashSHA256` gives browser peers, happen in-process through this library โ€” no `openssl` CLI needed. - Plaintext HTTP/1.1 needs nothing beyond POSIX sockets: `ClientHTTP1`/`ListenerHTTP1` built without credentials pull in no msquic and touch no TLS code path. - **Browser build** has no extra dependencies beyond Crafter.Build's `wasi-browser` runtime: HTTP delegates to the browser's `fetch()`, QUIC to its `WebTransport`. The JS glue lives in `additional/network-env.js` and is shipped alongside the produced `.wasm`.