Skip to content

Commit 767a89d

Browse files
committed
Expose TLS handshake bytes from Incoming
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`.
1 parent 41dce31 commit 767a89d

3 files changed

Lines changed: 83 additions & 2 deletions

File tree

quinn-proto/src/endpoint.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1198,6 +1198,75 @@ impl Incoming {
11981198
pub fn orig_dst_cid(&self) -> ConnectionId {
11991199
self.token.orig_dst_cid
12001200
}
1201+
1202+
/// The TLS handshake bytes carried by the Initial packet, suitable for
1203+
/// inspecting the ClientHello before deciding whether to
1204+
/// [`accept`](Endpoint::accept) or [`ignore`](Endpoint::ignore) this
1205+
/// connection attempt.
1206+
///
1207+
/// This decrypts the Initial packet's payload using the (publicly
1208+
/// derivable) Initial keys, walks the QUIC frames, and concatenates the
1209+
/// data carried by `CRYPTO` frames in offset order. Per RFC 9001 §4.1.3,
1210+
/// QUIC carries TLS handshake messages directly inside `CRYPTO` frames
1211+
/// with no intervening TLS record layer, so the returned bytes begin with
1212+
/// the TLS handshake message header (`0x01` = ClientHello, then a 3-byte
1213+
/// length, then the body).
1214+
///
1215+
/// Returns an error if the packet fails AEAD authentication, if its
1216+
/// frames cannot be parsed, or if the `CRYPTO` frames are non-contiguous
1217+
/// from offset 0. Non-`CRYPTO` frames (e.g. `PADDING`, `PING`) are
1218+
/// ignored.
1219+
pub fn handshake_bytes(&self) -> Result<Vec<u8>, HandshakeBytesError> {
1220+
let packet_number = self.packet.header.number.expand(0);
1221+
let mut payload = self.packet.payload.clone();
1222+
self.crypto
1223+
.packet
1224+
.remote
1225+
.decrypt(packet_number, &self.packet.header_data, &mut payload)
1226+
.map_err(|_| HandshakeBytesError::Decrypt)?;
1227+
1228+
let mut chunks: Vec<(u64, Bytes)> = Vec::new();
1229+
let iter =
1230+
frame::Iter::new(payload.freeze()).map_err(|_| HandshakeBytesError::MalformedFrames)?;
1231+
for frame in iter {
1232+
// PADDING, PING, ACK, CONNECTION_CLOSE are legal in Initials —
1233+
// skip them. Anything else would be a protocol violation, but we
1234+
// leave that judgement to the post-accept handshake path.
1235+
if let frame::Frame::Crypto(c) =
1236+
frame.map_err(|_| HandshakeBytesError::MalformedFrames)?
1237+
{
1238+
chunks.push((c.offset, c.data));
1239+
}
1240+
}
1241+
1242+
chunks.sort_by_key(|(off, _)| *off);
1243+
let mut out = Vec::new();
1244+
let mut expected = 0u64;
1245+
for (off, data) in chunks {
1246+
if off != expected {
1247+
return Err(HandshakeBytesError::NonContiguous);
1248+
}
1249+
expected += data.len() as u64;
1250+
out.extend_from_slice(&data);
1251+
}
1252+
1253+
Ok(out)
1254+
}
1255+
}
1256+
1257+
/// Error returned by [`Incoming::handshake_bytes`].
1258+
#[derive(Debug, Error)]
1259+
pub enum HandshakeBytesError {
1260+
/// The Initial packet failed AEAD authentication.
1261+
#[error("initial packet failed authentication")]
1262+
Decrypt,
1263+
/// The Initial packet's frames could not be parsed.
1264+
#[error("malformed frames in initial packet")]
1265+
MalformedFrames,
1266+
/// `CRYPTO` frames in the Initial packet did not form a contiguous run
1267+
/// starting at offset 0.
1268+
#[error("CRYPTO frames in initial packet are not contiguous from offset 0")]
1269+
NonContiguous,
12011270
}
12021271

12031272
impl fmt::Debug for Incoming {

quinn-proto/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ pub use crate::frame::{ApplicationClose, ConnectionClose, Datagram, FrameType};
7171

7272
mod endpoint;
7373
pub use crate::endpoint::{
74-
AcceptError, ConnectError, ConnectionHandle, DatagramEvent, Endpoint, Incoming, RetryError,
74+
AcceptError, ConnectError, ConnectionHandle, DatagramEvent, Endpoint, HandshakeBytesError,
75+
Incoming, RetryError,
7576
};
7677

7778
mod packet;

quinn/src/incoming.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use std::{
66
task::{Context, Poll},
77
};
88

9-
use proto::{ConnectionError, ConnectionId, ServerConfig};
9+
use proto::{ConnectionError, ConnectionId, HandshakeBytesError, ServerConfig};
1010
use thiserror::Error;
1111

1212
use crate::{
@@ -98,6 +98,17 @@ impl Incoming {
9898
pub fn orig_dst_cid(&self) -> ConnectionId {
9999
self.0.as_ref().unwrap().inner.orig_dst_cid()
100100
}
101+
102+
/// The TLS handshake bytes carried by the Initial packet.
103+
///
104+
/// See [`proto::Incoming::handshake_bytes`] for the full description.
105+
/// The returned bytes begin with the TLS handshake message header (`0x01`
106+
/// for `ClientHello`) and can be passed to a TLS parser to inspect
107+
/// extensions such as SNI or ALPN before deciding whether to
108+
/// [`accept`](Self::accept) or [`ignore`](Self::ignore) the connection.
109+
pub fn handshake_bytes(&self) -> Result<Vec<u8>, HandshakeBytesError> {
110+
self.0.as_ref().unwrap().inner.handshake_bytes()
111+
}
101112
}
102113

103114
impl Drop for Incoming {

0 commit comments

Comments
 (0)