docs(listener): ListenerQUIC's bind address, and how msquic enforces it
The README's QUIC section and the consumer notes show the new constructor and say what binding an address means under msquic: the UDP port is bound on every address anyway, and a connection sent to another one is refused during its first flight (0x5c, in milliseconds), so a client of a 127.0.0.1 listener dials 127.0.0.1, not `localhost`. The agent notes' family gotcha says the same and counts the new test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a50e3f415c
commit
5068f0acd7
3 changed files with 10 additions and 3 deletions
|
|
@ -82,7 +82,7 @@ A route map is `std::unordered_map<std::string, std::function<HTTPResponse(const
|
|||
- **`QUICStream`** is move-only, with `connection`, `canSend`, `canReceive` (a unidirectional stream has one of them false) and the msquic `handle`. `SendSync(buffer, size, finish = false)` blocks until msquic has taken the buffer, throws `QUICClosedException` on a closed stream or connection and `QUICException` on a failed send; `finish` closes the send side after it (the peer's `RecieveUntilCloseSync` returns). `RecieveSync()` waits for at least one byte and throws `QUICClosedException` once the peer closed its side and the buffer is drained; `RecieveUntilCloseSync()`; `RecieveUntilFullSync(n)` takes exactly `n` or throws and leaves what arrived for the next receive. `SendAsync(buffer, size, finish, onSent(bool))` copies the buffer and tells the callback whether the stream took it; `RecieveAsync`, `RecieveUntilCloseAsync` and `RecieveUntilFullAsync(n, ...)` give a closed stream an empty chunk and an early close what arrived, never an exception. A default-constructed `QUICStream` behaves as closed. `PrependReceived(bytes)` pushes bytes back for the next receive (a demuxer that peeked a prefix); `GetStreamId()` throws `QUICException` before the stream has started; `Stop()` shuts both directions down (the send side with a FIN, the receive side abortively, else a peer holding its side open keeps the handle alive), completes asynchronously, and is what the destructor calls.
|
||||
- Lifetimes: `~ClientQUIC` first destroys peer streams nobody claimed, then shuts the connection down and closes it, waiting up to 5 s for every stream on it to finish closing. Streams may outlive their connection, dead; destroy or `Stop()` them first when you can. msquic has two process-wide registrations here, one for clients and one for listeners and the peers they accept, each closed by a static destructor at exit, and each close blocks until every connection and stream handle opened under it is closed: an accepted peer you never deleted holds the listener's until the idle timeout, two minutes by default. Delete or `Stop()` what you own before `main` returns, or end with `std::_Exit` after flushing, as the library's own tests do.
|
||||
|
||||
**`ListenerQUIC(port, alpn, QUICServerCredentials, connectCallback(ClientQUIC*), concurrentClientLimit, totalClientLimit)`** listens on every address of both families and throws `QUICException` when it cannot. `Port()` is the UDP port it bound: pass port 0 to let msquic pick a free one and read it back (still valid after `Stop()`, 0 on a moved-from listener). The callback's `ClientQUIC*` is heap-allocated and yours: set `OnStream` / `OnDatagram` on it, keep it as long as the connection should live, `Stop()` or `delete` it to shut the connection down, and `delete` it in any case, since only that frees it (a connection the peer or the idle timeout ended has its msquic handle closed for it, but the object stays). `ListenSyncSync()` runs the accept loop on the calling thread and the callback there; `ListenSyncAsync()` runs the callback as a pool task; `ListenAsyncSync()` and `ListenAsyncAsync()` run the loop on a thread of their own, the second with the callback on the pool. `Stop()` stops accepting; the destructor stops, joins, and deletes peers accepted but not yet handed out. `totalClientLimit` ends the hand-off loop after that many connections (later ones queue until the listener is destroyed); `concurrentClientLimit` is stored and not enforced.
|
||||
**`ListenerQUIC(port, alpn, QUICServerCredentials, connectCallback(ClientQUIC*), concurrentClientLimit, totalClientLimit)`** listens on every address of both families and throws `QUICException` when it cannot. **`ListenerQUIC(bindAddress, port, alpn, ...)`**, same arguments after the address, listens on one: numeric only (`127.0.0.1` or `::1` for this machine alone, `0.0.0.0` or `::` for every address of one family; `localhost` throws `QUICException`). msquic still binds the UDP port on every address and drops, at its own layer, a connection sent to another one: the client fails within milliseconds with `QUIC_STATUS_ALPN_NEG_FAILURE` (0x5c) and connectCallback never hears of it. A listener on `127.0.0.1` is not reachable through `::1`, so its clients dial `127.0.0.1`, not `localhost`. `Port()` is the UDP port it bound: pass port 0 to let msquic pick a free one and read it back (still valid after `Stop()`, 0 on a moved-from listener). The callback's `ClientQUIC*` is heap-allocated and yours: set `OnStream` / `OnDatagram` on it, keep it as long as the connection should live, `Stop()` or `delete` it to shut the connection down, and `delete` it in any case, since only that frees it (a connection the peer or the idle timeout ended has its msquic handle closed for it, but the object stays). `ListenSyncSync()` runs the accept loop on the calling thread and the callback there; `ListenSyncAsync()` runs the callback as a pool task; `ListenAsyncSync()` and `ListenAsyncAsync()` run the loop on a thread of their own, the second with the callback on the pool. `Stop()` stops accepting; the destructor stops, joins, and deletes peers accepted but not yet handed out. `totalClientLimit` ends the hand-off loop after that many connections (later ones queue until the listener is destroyed); `concurrentClientLimit` is stored and not enforced.
|
||||
|
||||
- `selfSigned` shells out to the `openssl` CLI (install it, or pass `certPath`/`keyPath`; without it the constructor throws `QUICException`) and caches the pair at `/tmp/crafter-network-quic-cert/{cert,key}.pem`, reused across restarts while valid (inside crafter-build's Flatpak, `/tmp` is a per-app directory discarded once no instance is running, so there the pair rarely survives between runs; pass `certPath`/`keyPath` for a stable certificate): ECDSA P-256, `CN=localhost`, SANs `localhost`, `127.0.0.1`, `::1`, 10 days, with the extensions Chromium's `serverCertificateHashes` demands. `GetSelfSignedCertificatePath()` is that file, and `ComputeCertificateHashSHA256(certPath)` (also through `openssl`) is the 32-byte digest a browser pins, see "Browser build". A different certificate from the TLS partition's: a client of both needs both.
|
||||
|
||||
|
|
|
|||
|
|
@ -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: 31 tests
|
||||
crafter-build test # the suite, native: 32 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
|
||||
|
|
@ -131,7 +131,7 @@ Decided deliberately; change them only with the maintainer's say-so.
|
|||
- **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.
|
||||
- **`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 listens and connects on either family (`QUIC_ADDRESS_FAMILY_UNSPEC`).
|
||||
- **`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.
|
||||
- **The Flatpak's host triple is `x86_64-unknown-linux-gnu`**, and the tests register on any Linux triple since the TCP-only commit; before it they never ran inside the Flatpak.
|
||||
- **A port in use from a crashed run** is `Could not bind port <n>: Address already in use` on the HTTP/1.1 side and a `ListenerStart failed` `QUICException` on the QUIC side; find the stray process rather than changing the port.
|
||||
- **A warning is printed only when its unit actually recompiles**; a build with nothing to do is silent and says nothing about warnings.
|
||||
|
|
|
|||
|
|
@ -102,6 +102,12 @@ 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.
|
||||
|
||||
The port-only constructor listens on every address of both families. To keep a listener to this machine, name the address first, as with `ListenerTCP`; it must be numeric (`127.0.0.1`, `::1`, `0.0.0.0`, `::`), and a name such as `localhost` is refused. msquic still binds the UDP port on every address, but turns away a connection sent to any other address of the machine during its first flight, so such a client fails at once rather than at its handshake timeout. A client of a listener on `127.0.0.1` must dial `127.0.0.1`: `::1` is another address.
|
||||
|
||||
```cpp
|
||||
Crafter::ListenerQUIC local("127.0.0.1", 0, "myproto/1", creds, onConnect);
|
||||
```
|
||||
|
||||
### 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).
|
||||
|
|
@ -334,6 +340,7 @@ The library includes tests covering:
|
|||
- 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 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
|
||||
- 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
|
||||
|
|
|
|||
Loading…
Reference in a new issue