Skip to content

Commit a202b28

Browse files
committed
feat: add event and function to wait for resumption tickets
1 parent a9d4377 commit a202b28

File tree

8 files changed

+202
-1
lines changed

8 files changed

+202
-1
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ rand = "0.9"
3838
rcgen = "0.14"
3939
ring = "0.17"
4040
rustc-hash = "2"
41-
rustls = { version = "0.23.5", default-features = false, features = ["std"] }
41+
rustls = { version = "0.23.31", default-features = false, features = ["std"] }
4242
rustls-pemfile = "2"
4343
rustls-platform-verifier = "0.6"
4444
rustls-pki-types = "1.7"

perf/src/noprotection.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ impl crypto::Session for NoProtectionSession {
127127
) -> Result<(), crypto::ExportKeyingMaterialError> {
128128
self.inner.export_keying_material(output, label, context)
129129
}
130+
131+
fn resumption_tickets_received(&self) -> Option<u32> {
132+
self.inner.resumption_tickets_received()
133+
}
130134
}
131135

132136
impl crypto::ClientConfig for NoProtectionClientConfig {

quinn-proto/src/connection/mod.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,8 @@ pub struct Connection {
238238
stats: ConnectionStats,
239239
/// QUIC version used for the connection.
240240
version: u32,
241+
/// True if we emitted the event for received resumption tickets
242+
resumption_tickets_received: bool,
241243
}
242244

243245
impl Connection {
@@ -355,6 +357,7 @@ impl Connection {
355357
rng,
356358
stats: ConnectionStats::default(),
357359
version,
360+
resumption_tickets_received: false,
358361
};
359362
if path_validated {
360363
this.on_path_validated();
@@ -2113,6 +2116,12 @@ impl Connection {
21132116
}
21142117
}
21152118

2119+
if !self.resumption_tickets_received && self.crypto.resumption_tickets_received() > Some(0)
2120+
{
2121+
self.resumption_tickets_received = true;
2122+
self.events.push_back(Event::ResumptionEnabled)
2123+
}
2124+
21162125
Ok(())
21172126
}
21182127

@@ -4000,6 +4009,13 @@ pub enum Event {
40004009
DatagramReceived,
40014010
/// One or more application datagrams have been sent after blocking
40024011
DatagramsUnblocked,
4012+
/// Resumption of the cryptographic session is now possible.
4013+
///
4014+
/// When using the rustls TLS session provider, this event is emitted when one or more
4015+
/// TLS session resumption tickets have been received.
4016+
///
4017+
/// It is only emitted on the client, and is emitted at most once per connection.
4018+
ResumptionEnabled,
40034019
}
40044020

40054021
fn instant_saturating_sub(x: Instant, y: Instant) -> Duration {

quinn-proto/src/crypto.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ pub trait Session: Send + Sync + 'static {
9191
label: &[u8],
9292
context: &[u8],
9393
) -> Result<(), ExportKeyingMaterialError>;
94+
95+
/// Returns the number of TLS1.3 session resumption tickets that were received
96+
///
97+
/// Returns `None` on the server side.
98+
fn resumption_tickets_received(&self) -> Option<u32>;
9499
}
95100

96101
/// A pair of keys for bidirectional communication

quinn-proto/src/crypto/rustls.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,13 @@ impl crypto::Session for TlsSession {
201201
.map_err(|_| ExportKeyingMaterialError)?;
202202
Ok(())
203203
}
204+
205+
fn resumption_tickets_received(&self) -> Option<u32> {
206+
match &self.inner {
207+
Connection::Client(conn) => Some(conn.tls13_tickets_received()),
208+
Connection::Server(_) => None,
209+
}
210+
}
204211
}
205212

206213
const RETRY_INTEGRITY_KEY_DRAFT: [u8; 16] = [

quinn-proto/src/tests/mod.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ fn lifecycle() {
128128
let _guard = subscribe();
129129
let mut pair = Pair::default();
130130
let (client_ch, server_ch) = pair.connect();
131+
assert_matches!(
132+
pair.client_conn_mut(client_ch).poll(),
133+
Some(Event::ResumptionEnabled)
134+
);
131135
assert_matches!(pair.client_conn_mut(client_ch).poll(), None);
132136
assert!(pair.client_conn_mut(client_ch).using_ecn());
133137
assert!(pair.server_conn_mut(server_ch).using_ecn());
@@ -161,6 +165,10 @@ fn draft_version_compat() {
161165
let mut pair = Pair::default();
162166
let (client_ch, server_ch) = pair.connect_with(client_config);
163167

168+
assert_matches!(
169+
pair.client_conn_mut(client_ch).poll(),
170+
Some(Event::ResumptionEnabled)
171+
);
164172
assert_matches!(pair.client_conn_mut(client_ch).poll(), None);
165173
assert!(pair.client_conn_mut(client_ch).using_ecn());
166174
assert!(pair.server_conn_mut(server_ch).using_ecn());
@@ -206,6 +214,10 @@ fn server_stateless_reset() {
206214
pair.client.connections.get_mut(&client_ch).unwrap().ping();
207215
info!("resetting");
208216
pair.drive();
217+
assert_matches!(
218+
pair.client_conn_mut(client_ch).poll(),
219+
Some(Event::ResumptionEnabled)
220+
);
209221
assert_matches!(
210222
pair.client_conn_mut(client_ch).poll(),
211223
Some(Event::ConnectionLost {
@@ -327,6 +339,10 @@ fn finish_stream_simple() {
327339
pair.client_send(client_ch, s).finish().unwrap();
328340
pair.drive();
329341

342+
assert_matches!(
343+
pair.client_conn_mut(client_ch).poll(),
344+
Some(Event::ResumptionEnabled)
345+
);
330346
assert_matches!(
331347
pair.client_conn_mut(client_ch).poll(),
332348
Some(Event::Stream(StreamEvent::Finished { id })) if id == s
@@ -379,6 +395,10 @@ fn reset_stream() {
379395
let mut chunks = recv.read(false).unwrap();
380396
assert_matches!(chunks.next(usize::MAX), Err(ReadError::Reset(ERROR)));
381397
let _ = chunks.finalize();
398+
assert_matches!(
399+
pair.client_conn_mut(client_ch).poll(),
400+
Some(Event::ResumptionEnabled)
401+
);
382402
assert_matches!(pair.client_conn_mut(client_ch).poll(), None);
383403
}
384404

@@ -597,6 +617,10 @@ fn zero_rtt_happypath() {
597617
);
598618
let _ = chunks.finalize();
599619
assert_eq!(pair.client_conn_mut(client_ch).stats().path.lost_packets, 0);
620+
assert_matches!(
621+
pair.client_conn_mut(client_ch).poll(),
622+
Some(Event::ResumptionEnabled)
623+
);
600624
}
601625

602626
#[test]
@@ -905,6 +929,10 @@ fn stream_id_limit() {
905929
pair.client_send(client_ch, s).write(MSG).unwrap();
906930
pair.client_send(client_ch, s).finish().unwrap();
907931
pair.drive();
932+
assert_matches!(
933+
pair.client_conn_mut(client_ch).poll(),
934+
Some(Event::ResumptionEnabled)
935+
);
908936
assert_matches!(
909937
pair.client_conn_mut(client_ch).poll(),
910938
Some(Event::Stream(StreamEvent::Finished { id })) if id == s
@@ -1192,6 +1220,10 @@ fn idle_timeout() {
11921220
}
11931221

11941222
assert!(pair.time - start < Duration::from_millis(2 * IDLE_TIMEOUT));
1223+
assert_matches!(
1224+
pair.client_conn_mut(client_ch).poll(),
1225+
Some(Event::ResumptionEnabled)
1226+
);
11951227
assert_matches!(
11961228
pair.client_conn_mut(client_ch).poll(),
11971229
Some(Event::ConnectionLost {
@@ -1271,6 +1303,10 @@ fn migration() {
12711303
assert_ne!(pair.server_conn_mut(server_ch).total_recvd(), 0);
12721304

12731305
pair.drive();
1306+
assert_matches!(
1307+
pair.client_conn_mut(client_ch).poll(),
1308+
Some(Event::ResumptionEnabled)
1309+
);
12741310
assert_matches!(pair.client_conn_mut(client_ch).poll(), None);
12751311
assert_eq!(
12761312
pair.server_conn_mut(server_ch).remote_address(),
@@ -1657,6 +1693,10 @@ fn finish_stream_flow_control_reordered() {
16571693
pair.server.finish_delay(); // Add flow control packets after
16581694
pair.drive();
16591695

1696+
assert_matches!(
1697+
pair.client_conn_mut(client_ch).poll(),
1698+
Some(Event::ResumptionEnabled)
1699+
);
16601700
assert_matches!(
16611701
pair.client_conn_mut(client_ch).poll(),
16621702
Some(Event::Stream(StreamEvent::Finished { id })) if id == s
@@ -1749,6 +1789,10 @@ fn stop_during_finish() {
17491789
pair.drive_server();
17501790
pair.client_send(client_ch, s).finish().unwrap();
17511791
pair.drive_client();
1792+
assert_matches!(
1793+
pair.client_conn_mut(client_ch).poll(),
1794+
Some(Event::ResumptionEnabled)
1795+
);
17521796
assert_matches!(
17531797
pair.client_conn_mut(client_ch).poll(),
17541798
Some(Event::Stream(StreamEvent::Stopped { id, error_code: ERROR })) if id == s
@@ -2036,6 +2080,10 @@ fn finish_acked() {
20362080
// Send FIN, receive data ack
20372081
info!("client receives ACK, sends FIN");
20382082
pair.drive_client();
2083+
assert_matches!(
2084+
pair.client_conn_mut(client_ch).poll(),
2085+
Some(Event::ResumptionEnabled)
2086+
);
20392087
// Check for premature finish from data ack
20402088
assert_matches!(pair.client_conn_mut(client_ch).poll(), None);
20412089
// Process FIN ack
@@ -2074,6 +2122,10 @@ fn finish_retransmit() {
20742122
// Receive FIN ack, but no data ack
20752123
pair.drive_client();
20762124
// Check for premature finish from FIN ack
2125+
assert_matches!(
2126+
pair.client_conn_mut(client_ch).poll(),
2127+
Some(Event::ResumptionEnabled)
2128+
);
20772129
assert_matches!(pair.client_conn_mut(client_ch).poll(), None);
20782130
// Recover
20792131
pair.drive();

quinn/src/connection.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,37 @@ impl Connection {
651651
// May need to send MAX_STREAMS to make progress
652652
conn.wake();
653653
}
654+
655+
/// Waits until the connection received TLS resumption tickets
656+
///
657+
/// Yields `true` once resumption tickets were received. Resolves immediately
658+
/// if tickets were already received, otherwise it resolves once tickets arrive.
659+
/// If the server does not send any tickets, the returned future will remain pending forever.
660+
///
661+
/// This should only be used on the client side. On the server side, it will
662+
/// always resolve immediately and yield `false`.
663+
pub fn resumption_tickets_received(&self) -> impl Future<Output = bool> + Send + 'static {
664+
let conn = self.0.clone();
665+
async move {
666+
let notify;
667+
let (mut notified, out) = {
668+
let conn = conn.state.lock("resumption_tickets_received");
669+
let (notified, out) = match conn.resumption_tickets.as_ref() {
670+
Some(ResumptionTicketState::Received) => (None, true),
671+
Some(ResumptionTicketState::Pending(n)) => {
672+
notify = n.clone();
673+
(Some(notify.notified()), true)
674+
}
675+
None => (None, false),
676+
};
677+
(notified, out)
678+
};
679+
if let Some(notified) = notified.take() {
680+
notified.await;
681+
}
682+
out
683+
}
684+
}
654685
}
655686

656687
pin_project! {
@@ -885,6 +916,10 @@ impl ConnectionRef {
885916
socket: Arc<dyn AsyncUdpSocket>,
886917
runtime: Arc<dyn Runtime>,
887918
) -> Self {
919+
let resumption_tickets = match conn.side() {
920+
Side::Client => Some(ResumptionTicketState::Pending(Default::default())),
921+
Side::Server => None,
922+
};
888923
Self(Arc::new(ConnectionInner {
889924
state: Mutex::new(State {
890925
inner: conn,
@@ -907,6 +942,7 @@ impl ConnectionRef {
907942
runtime,
908943
send_buffer: Vec::new(),
909944
buffered_transmit: None,
945+
resumption_tickets,
910946
}),
911947
shared: Shared::default(),
912948
}))
@@ -989,6 +1025,8 @@ pub(crate) struct State {
9891025
send_buffer: Vec<u8>,
9901026
/// We buffer a transmit when the underlying I/O would block
9911027
buffered_transmit: Option<proto::Transmit>,
1028+
/// Whether we received resumption tickets. None on the server side.
1029+
resumption_tickets: Option<ResumptionTicketState>,
9921030
}
9931031

9941032
impl State {
@@ -1123,6 +1161,14 @@ impl State {
11231161
wake_all_notify(&mut self.stopped);
11241162
}
11251163
}
1164+
ResumptionEnabled => {
1165+
if let Some(ResumptionTicketState::Pending(notify)) =
1166+
self.resumption_tickets.as_mut()
1167+
{
1168+
notify.notify_waiters();
1169+
self.resumption_tickets = Some(ResumptionTicketState::Received);
1170+
}
1171+
}
11261172
ConnectionLost { reason } => {
11271173
self.terminate(reason, shared);
11281174
}
@@ -1293,6 +1339,12 @@ fn wake_all_notify(wakers: &mut FxHashMap<StreamId, Arc<Notify>>) {
12931339
.for_each(|(_, notify)| notify.notify_waiters())
12941340
}
12951341

1342+
#[derive(Debug)]
1343+
enum ResumptionTicketState {
1344+
Received,
1345+
Pending(Arc<Notify>),
1346+
}
1347+
12961348
/// Errors that can arise when sending a datagram
12971349
#[derive(Debug, Error, Clone, Eq, PartialEq)]
12981350
pub enum SendDatagramError {

quinn/src/tests.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,71 @@ async fn zero_rtt() {
384384
endpoint.wait_idle().await;
385385
}
386386

387+
#[tokio::test]
388+
async fn zero_rtt_resumption() {
389+
let _guard = subscribe();
390+
let endpoint = endpoint();
391+
392+
let endpoint2 = endpoint.clone();
393+
tokio::spawn(async move {
394+
for _ in 0..12 {
395+
let incoming = endpoint2.accept().await.unwrap().accept().unwrap();
396+
let (connection, _established) =
397+
incoming.into_0rtt().unwrap_or_else(|_| unreachable!());
398+
connection.closed().await;
399+
}
400+
});
401+
402+
let connect_0rtt = || {
403+
endpoint
404+
.connect(endpoint.local_addr().unwrap(), "localhost")
405+
.unwrap()
406+
.into_0rtt()
407+
.unwrap_or_else(|_| panic!("missing 0-RTT keys"))
408+
};
409+
410+
let connect_0rtt_fail = || {
411+
endpoint
412+
.connect(endpoint.local_addr().unwrap(), "localhost")
413+
.unwrap()
414+
.into_0rtt()
415+
.err()
416+
.expect("0-RTT succeeded without keys")
417+
};
418+
419+
let connect_full = || async {
420+
endpoint
421+
.connect(endpoint.local_addr().unwrap(), "localhost")
422+
.unwrap()
423+
.into_0rtt()
424+
.err()
425+
.expect("0-RTT succeeded without keys")
426+
.await
427+
.expect("connect")
428+
};
429+
430+
// 0rtt without full connection should fail
431+
connect_0rtt_fail();
432+
// now do a full connection
433+
connect_full().await;
434+
// we received two tickets, so we should be able to resume two times, and then fail
435+
connect_0rtt();
436+
connect_0rtt();
437+
connect_0rtt_fail();
438+
439+
// now do another full connection
440+
connect_full().await;
441+
connect_0rtt();
442+
// this time we wait to receive resumption tickets on the zero-rtt connection
443+
let (conn, _0rtt_accepted) = connect_0rtt();
444+
conn.resumption_tickets_received().await;
445+
// and we can do two more 0rtt conns
446+
connect_0rtt();
447+
connect_0rtt();
448+
// and then fail again
449+
connect_0rtt_fail();
450+
}
451+
387452
#[test]
388453
#[cfg_attr(
389454
any(target_os = "solaris", target_os = "illumos"),

0 commit comments

Comments
 (0)