Expose TLS handshake bytes from Incoming#2644
Conversation
02b3be2 to
83b362c
Compare
Adds `Incoming::handshake_bytes()`, which decrypts the Initial packet using the publicly-derivable Initial keys, walks the QUIC frames, and returns the concatenated `CRYPTO` frame contents in offset order. Per RFC 9001 §4.1.3, TLS handshake messages travel inside `CRYPTO` frames with no intervening TLS record layer, so the returned bytes start directly with the TLS handshake message header (`0x01` for ClientHello, followed by a 3-byte length and the body). Callers can feed this to a TLS parser to inspect extensions such as SNI or ALPN before deciding whether to `accept()`, `retry()`, `refuse()`, or `ignore()` the connection. The motivating use case is port-knocking style authentication: a server that wants to silently drop probes from any client that doesn't carry a pre-shared token (e.g. an HMAC in a custom ALPN entry) needs to inspect the ClientHello before any response goes on the wire. The existing `Incoming` API already provides `ignore()` for the silent-drop side; this adds the inspection side so the decision can be informed. The high-level `quinn::Incoming` wrapper forwards the new method, and a new `HandshakeBytesError` enum is re-exported from `quinn-proto`.
83b362c to
767a89d
Compare
Adds optional ALPN-based handshake authentication: when QUICSSH_AUTH_SECRET is set on both client and server, the client embeds an HMAC-SHA256 token (keyed by the secret and a 30s time window) in the TLS ALPN extension of its Initial packet, and the server inspects each incoming Initial's ClientHello before any response goes on the wire. Unauthenticated connection attempts are silently dropped at the UDP layer — the port appears closed to scanners. Verified by packet capture: zero response packets are sent to clients presenting a wrong or missing token; matching clients see a normal QUIC handshake. Tolerates ±30s clock skew between client and server. Implementation notes: - Bumps quinn 0.10 → 0.11, rustls 0.21 → 0.23, rcgen 0.12 → 0.13. - Uses quinn::Incoming::ignore() for the silent-drop path. ClientHello inspection relies on Incoming::handshake_bytes(), added in a small patch to quinn vendored at vendor/quinn (submodule on the aiguy110/quinn fork; upstream PR quinn-rs/quinn#2644). - Server rotates the valid token set every WINDOW_SECS/2 to keep the current ±1 windows accepted at all times. - When QUICSSH_AUTH_SECRET is unset, behaviour is identical to v0.1.x: no authentication, any client can connect.
Adds optional ALPN-based handshake authentication: when QUICSSH_AUTH_SECRET is set on both client and server, the client embeds an HMAC-SHA256 token (keyed by the secret and a 30s time window) in the TLS ALPN extension of its Initial packet, and the server inspects each incoming Initial's ClientHello before any response goes on the wire. Unauthenticated connection attempts are silently dropped at the UDP layer — the port appears closed to scanners. Verified by packet capture: zero response packets are sent to clients presenting a wrong or missing token; matching clients see a normal QUIC handshake. Tolerates ±30s clock skew between client and server. Implementation notes: - Bumps quinn 0.10 → 0.11, rustls 0.21 → 0.23, rcgen 0.12 → 0.13. - Uses quinn::Incoming::ignore() for the silent-drop path. ClientHello inspection relies on Incoming::handshake_bytes(), added in a small patch to quinn vendored at vendor/quinn (submodule on the aiguy110/quinn fork; upstream PR quinn-rs/quinn#2644). - Server rotates the valid token set every WINDOW_SECS/2 to keep the current ±1 windows accepted at all times. - When QUICSSH_AUTH_SECRET is unset, behaviour is identical to v0.1.x: no authentication, any client can connect.
Adds optional ALPN-based handshake authentication: when QUICSSH_AUTH_SECRET is set on both client and server, the client embeds an HMAC-SHA256 token (keyed by the secret and a 30s time window) in the TLS ALPN extension of its Initial packet, and the server inspects each incoming Initial's ClientHello before any response goes on the wire. Unauthenticated connection attempts are silently dropped at the UDP layer — the port appears closed to scanners. Verified by packet capture: zero response packets are sent to clients presenting a wrong or missing token; matching clients see a normal QUIC handshake. Tolerates ±30s clock skew between client and server. Implementation notes: - Bumps quinn 0.10 → 0.11, rustls 0.21 → 0.23, rcgen 0.12 → 0.13. - Uses quinn::Incoming::ignore() for the silent-drop path. ClientHello inspection relies on Incoming::handshake_bytes(), added in a small patch to quinn vendored at vendor/quinn (submodule on the aiguy110/quinn fork; upstream PR quinn-rs/quinn#2644). - Server rotates the valid token set every WINDOW_SECS/2 to keep the current ±1 windows accepted at all times. - When QUICSSH_AUTH_SECRET is unset, behaviour is identical to v0.1.x: no authentication, any client can connect.
Adds optional ALPN-based handshake authentication: when QUICSSH_AUTH_SECRET is set on both client and server, the client embeds an HMAC-SHA256 token (keyed by the secret and a 30s time window) in the TLS ALPN extension of its Initial packet, and the server inspects each incoming Initial's ClientHello before any response goes on the wire. Unauthenticated connection attempts are silently dropped at the UDP layer — the port appears closed to scanners. Verified by packet capture: zero response packets are sent to clients presenting a wrong or missing token; matching clients see a normal QUIC handshake. Tolerates ±30s clock skew between client and server. Implementation notes: - Bumps quinn 0.10 → 0.11, rustls 0.21 → 0.23, rcgen 0.12 → 0.13. - Uses quinn::Incoming::ignore() for the silent-drop path. ClientHello inspection relies on Incoming::handshake_bytes(), added in a small patch to quinn vendored at vendor/quinn (submodule on the aiguy110/quinn fork; upstream PR quinn-rs/quinn#2644). - Server rotates the valid token set every WINDOW_SECS/2 to keep the current ±1 windows accepted at all times. - When QUICSSH_AUTH_SECRET is unset, behaviour is identical to v0.1.x: no authentication, any client can connect.
Adds optional ALPN-based handshake authentication: when QUICSSH_AUTH_SECRET is set on both client and server, the client embeds an HMAC-SHA256 token (keyed by the secret and a 30s time window) in the TLS ALPN extension of its Initial packet, and the server inspects each incoming Initial's ClientHello before any response goes on the wire. Unauthenticated connection attempts are silently dropped at the UDP layer — the port appears closed to scanners. Verified by packet capture: zero response packets are sent to clients presenting a wrong or missing token; matching clients see a normal QUIC handshake. Tolerates ±30s clock skew between client and server. Implementation notes: - Bumps quinn 0.10 → 0.11, rustls 0.21 → 0.23, rcgen 0.12 → 0.13. - Uses quinn::Incoming::ignore() for the silent-drop path. ClientHello inspection relies on Incoming::handshake_bytes(), added in a small patch to quinn vendored at vendor/quinn (submodule on the aiguy110/quinn fork; upstream PR quinn-rs/quinn#2644). - Server rotates the valid token set every WINDOW_SECS/2 to keep the current ±1 windows accepted at all times. - When QUICSSH_AUTH_SECRET is unset, behaviour is identical to v0.1.x: no authentication, any client can connect.
|
This is a really cool idea! The use case makes perfect sense to me, and is something I'm happy to support. I particularly appreciate the design to yield unprocessed plaintext bytes; that's good layering. One alternative strategy might be to have the crypto layer parse them into something that's then passed back with type erasure, like we've done for I do have some reservations about the frame processing strategy:
|
|
In my mind this is (mostly) a poor man's version of I'd prefer to get that done holistically, including making sure that callers don't have to commit to a |
Summary
Adds
Incoming::handshake_bytes()(and a forwarding method on the high-levelquinn::Incomingwrapper), which decrypts the Initial packet using the publicly-derivable Initial keys, walks the QUIC frames, and returns the concatenatedCRYPTOframe contents in offset order.Per RFC 9001 §4.1.3, TLS handshake messages travel inside
CRYPTOframes with no intervening TLS record layer, so the returned bytes start directly with the TLS handshake message header (0x01forClientHello, then a 3-byte length, then the body). Callers can feed this to a TLS parser to inspect extensions such as SNI or ALPN before deciding whether toaccept(),retry(),refuse(), orignore()the connection.Motivation
Incoming(from #2076) gave users the ability to make accept/refuse/retry/ignore decisions before any QUIC response goes on the wire — a very nice primitive for hardening. The decision can today be informed by remote address, address-validation status, original DCID, and local IP. What's missing is any ability to look at the ClientHello itself.Concrete use case I bumped into: port-knocking style authentication on a UDP service. The goal is for the port to appear closed to scanners that don't carry a pre-shared token (an HMAC, say, embedded in a custom ALPN entry). Without ClientHello inspection, the closest one can do today is gate at the rustls layer via a custom
ResolvesServerCert— but by that point a TLS alert has already left the box, so the service is observable. Withhandshake_bytes()+Incoming::ignore(), the port becomes truly silent for invalid clients (WireGuard-grade posture), while still being a normal QUIC service for valid ones. I've verified this end-to-end with packet capture: zero response packets to clients failing the check.Other plausible uses: SNI-keyed rate limiting before TLS handshake, ClientHello-fingerprint denylists, GREASE/ECH-aware admission, eBPF-style port-knocking with cleaner state handling than out-of-band knock daemons.
API shape
Re-exported through
quinn-proto::Incomingandquinn::Incoming.Design choices / alternatives considered
ClientHello? Keepsquinn-protofirmly in QUIC framing and out of the TLS layer. Callers already have rustls (or webpki-types, or whatever) available for the TLS-side parsing. Avoids growingquinn-proto's API surface with a TLS data model.Incomingrather than exposing internal frame parsing?Iter/Frame/Cryptoare currentlypub(crate); making them public is a much larger commitment. A single inspection method is the smallest user-facing surface for the same capability.Vec<u8>and notBytes/&[u8]? The CRYPTO frames need to be concatenated in offset order, so the result is inherently owned. Happy to switch toBytesif that's preferred.CRYPTOframes (PADDING,PING,ACK,CONNECTION_CLOSE) are silently skipped — they're legal in Initials per RFC 9000 §17.2.2.What's not in this PR
0x01and parse as a ClientHello would be straightforward. Held off pending design feedback on the API shape.accept()/ignore()/etc. The method is purely an additional read-only inspection point.Compatibility
Additive only. No existing methods change behaviour or signatures.
Happy to convert this to an issue / discussion if you'd prefer to align on the API shape before reviewing code. Also happy to iterate on naming (
handshake_bytesvsclient_hello_bytesvsinitial_cryptoetc.), error type granularity, or return type.