Skip to content

Commit 044e824

Browse files
committed
server: extend handshake timeout to LazyConfigAcceptor + into_stream
1 parent 3cdbe0d commit 044e824

2 files changed

Lines changed: 119 additions & 11 deletions

File tree

src/server.rs

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ use std::time::Duration;
1212
use rustls::server::AcceptedAlert;
1313
use rustls::{ServerConfig, ServerConnection};
1414
use tokio::io::{AsyncBufRead, AsyncRead, AsyncWrite, ReadBuf};
15+
use tokio::time::Instant;
1516

1617
use crate::common::{
17-
HandshakeFuture, IoSession, MidHandshake, Stream, SyncReadAdapter, SyncWriteAdapter, TlsState,
18+
HandshakeFuture, IoSession, MidHandshake, Stream, SyncReadAdapter, SyncWriteAdapter, Timeout,
19+
TlsState,
1820
};
1921

2022
/// A wrapper around a `rustls::ServerConfig`, providing an async `accept` method.
@@ -39,9 +41,11 @@ impl TlsAcceptor {
3941
/// `None` disables the handshake timeout.
4042
///
4143
/// The timeout applies to handshakes started via [`TlsAcceptor::accept`] and
42-
/// [`TlsAcceptor::accept_with`]. It does not apply to handshakes resumed via
43-
/// [`LazyConfigAcceptor`] / [`StartHandshake::into_stream`], which proceed
44-
/// without a timeout.
44+
/// [`TlsAcceptor::accept_with`]. For the lazy-acceptor flow, set the timeout
45+
/// on the [`LazyConfigAcceptor`] itself via
46+
/// [`LazyConfigAcceptor::with_handshake_timeout`]. The deadline established
47+
/// there is inherited by the [`Accept`] returned from
48+
/// [`StartHandshake::into_stream`], so a single timeout covers both phases.
4549
pub fn with_handshake_timeout(mut self, timeout: Option<Duration>) -> Self {
4650
self.handshake_timeout = timeout;
4751
self
@@ -97,6 +101,7 @@ pub struct LazyConfigAcceptor<IO> {
97101
acceptor: rustls::server::Acceptor,
98102
io: Option<IO>,
99103
alert: Option<(rustls::Error, AcceptedAlert)>,
104+
timeout: Option<Timeout>,
100105
}
101106

102107
impl<IO> LazyConfigAcceptor<IO>
@@ -109,9 +114,30 @@ where
109114
acceptor,
110115
io: Some(io),
111116
alert: None,
117+
timeout: None,
112118
}
113119
}
114120

121+
/// Set the maximum amount of time to allow for the TLS handshake.
122+
///
123+
/// `None` disables the handshake timeout.
124+
///
125+
/// The deadline is fixed to `Instant::now() + duration` when this builder
126+
/// is called and spans both phases of the lazy-acceptor flow:
127+
///
128+
/// 1. The ClientHello phase driven by this `LazyConfigAcceptor`.
129+
/// 2. The post-ClientHello phase driven by the [`Accept`] returned from
130+
/// [`StartHandshake::into_stream`].
131+
///
132+
/// Both phases race against the same wall-clock deadline, so a single
133+
/// timeout covers the full handshake. To set a timeout on handshakes
134+
/// driven via [`TlsAcceptor::accept`] instead, use
135+
/// [`TlsAcceptor::with_handshake_timeout`].
136+
pub fn with_handshake_timeout(mut self, timeout: Option<Duration>) -> Self {
137+
self.timeout = timeout.map(Timeout::from_duration);
138+
self
139+
}
140+
115141
/// Takes back the client connection. Will return `None` if called more than once or if the
116142
/// connection has been accepted.
117143
///
@@ -181,7 +207,7 @@ where
181207
match alert.write(&mut SyncWriteAdapter { io, cx }) {
182208
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
183209
this.alert = Some((err, alert));
184-
return Poll::Pending;
210+
return poll_pending_or_timeout(&mut this.timeout, cx);
185211
}
186212
Ok(0) | Err(_) => {
187213
return Poll::Ready(Err(io::Error::new(io::ErrorKind::InvalidData, err)));
@@ -197,14 +223,20 @@ where
197223
match this.acceptor.read_tls(&mut reader) {
198224
Ok(0) => return Err(io::ErrorKind::UnexpectedEof.into()).into(),
199225
Ok(_) => {}
200-
Err(e) if e.kind() == io::ErrorKind::WouldBlock => return Poll::Pending,
226+
Err(e) if e.kind() == io::ErrorKind::WouldBlock => {
227+
return poll_pending_or_timeout(&mut this.timeout, cx);
228+
}
201229
Err(e) => return Err(e).into(),
202230
}
203231

204232
match this.acceptor.accept() {
205233
Ok(Some(accepted)) => {
206234
let io = this.io.take().unwrap();
207-
return Poll::Ready(Ok(StartHandshake { accepted, io }));
235+
return Poll::Ready(Ok(StartHandshake {
236+
accepted,
237+
io,
238+
deadline: this.timeout.as_ref().map(Timeout::deadline),
239+
}));
208240
}
209241
Ok(None) => {}
210242
Err((err, alert)) => {
@@ -215,6 +247,25 @@ where
215247
}
216248
}
217249

250+
/// Returns `Poll::Pending`, unless the timeout has elapsed, in which case a `TimedOut`
251+
/// error is returned. With no timeout configured, always returns `Poll::Pending`.
252+
fn poll_pending_or_timeout<T>(
253+
timeout: &mut Option<Timeout>,
254+
cx: &mut Context<'_>,
255+
) -> Poll<Result<T, io::Error>> {
256+
let timeout = match timeout {
257+
Some(timeout) => timeout,
258+
None => return Poll::Pending,
259+
};
260+
if timeout.poll_deadline(cx).is_pending() {
261+
return Poll::Pending;
262+
}
263+
Poll::Ready(Err(io::Error::new(
264+
io::ErrorKind::TimedOut,
265+
"TLS handshake timed out",
266+
)))
267+
}
268+
218269
/// An incoming connection received through [`LazyConfigAcceptor`].
219270
///
220271
/// This contains the generic `IO` asynchronous transport,
@@ -226,6 +277,10 @@ where
226277
pub struct StartHandshake<IO> {
227278
pub accepted: rustls::server::Accepted,
228279
pub io: IO,
280+
// Handshake deadline inherited from the LazyConfigAcceptor, if any.
281+
// Propagated into the Accept returned by into_stream so the timeout
282+
// spans both phases of the handshake.
283+
pub(crate) deadline: Option<Instant>,
229284
}
230285

231286
impl<IO> StartHandshake<IO>
@@ -237,6 +292,7 @@ where
237292
Self {
238293
accepted,
239294
io: transport,
295+
deadline: None,
240296
}
241297
}
242298

@@ -255,28 +311,28 @@ where
255311
let mut conn = match self.accepted.into_connection(config) {
256312
Ok(conn) => conn,
257313
Err((error, alert)) => {
258-
return Accept(HandshakeFuture::new(
314+
return Accept(HandshakeFuture::from_deadline(
259315
MidHandshake::SendAlert {
260316
io: self.io,
261317
alert,
262318
// TODO(eliza): should this really return an `io::Error`?
263319
// Probably not...
264320
error: io::Error::new(io::ErrorKind::InvalidData, error),
265321
},
266-
None,
322+
self.deadline,
267323
));
268324
}
269325
};
270326
f(&mut conn);
271327

272-
Accept(HandshakeFuture::new(
328+
Accept(HandshakeFuture::from_deadline(
273329
MidHandshake::Handshaking(TlsStream {
274330
session: conn,
275331
io: self.io,
276332
state: TlsState::Stream,
277333
need_flush: false,
278334
}),
279-
None,
335+
self.deadline,
280336
))
281337
}
282338
}

tests/test.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,58 @@ async fn accept_handshake_timeout_fallible() {
206206
assert_eq!(err.kind(), ErrorKind::TimedOut);
207207
}
208208

209+
#[tokio::test(start_paused = true)]
210+
async fn lazy_config_acceptor_handshake_timeout() {
211+
// The client end is held but never written to. The lazy acceptor will block
212+
// waiting on the ClientHello and with all tasks pending, the start_paused
213+
// runtime advances to the handshake timeout's deadline.
214+
let (_client, server) = tokio::io::duplex(4096);
215+
let acceptor = LazyConfigAcceptor::new(rustls::server::Acceptor::default(), server)
216+
.with_handshake_timeout(Some(HANDSHAKE_TIMEOUT));
217+
218+
let err = acceptor.await.unwrap_err();
219+
assert_eq!(err.kind(), ErrorKind::TimedOut);
220+
}
221+
222+
/// lazy_config_acceptor_handshake_timeout_into_stream confirms the deadline established
223+
/// on a LazyConfigAcceptor propagates through StartHandshake::into_stream.
224+
///
225+
/// A valid ClientHello is pre-loaded into the duplex and the lazy acceptor
226+
/// parses it instantly. Afterwards, into_stream returns an Accept that blocks trying to
227+
/// read the next client message which will never arrive. With all tasks pending,
228+
/// the start_paused runtime advances to the inherited deadline.
229+
#[tokio::test(start_paused = true)]
230+
async fn lazy_config_acceptor_handshake_timeout_into_stream() {
231+
// Real TLS 1.2 ClientHello, sourced from https://tls12.xargs.org/#client-hello/annotated
232+
// (the same bytes used by acceptor_alert, without the minor-version tweak).
233+
let client_hello = [
234+
0x16, 0x03, 0x01, 0x00, 0xa5, 0x01, 0x00, 0x00, 0xa1, 0x03, 0x03, 0x00, 0x01, 0x02, 0x03,
235+
0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12,
236+
0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x00, 0x00,
237+
0x20, 0xcc, 0xa8, 0xcc, 0xa9, 0xc0, 0x2f, 0xc0, 0x30, 0xc0, 0x2b, 0xc0, 0x2c, 0xc0, 0x13,
238+
0xc0, 0x09, 0xc0, 0x14, 0xc0, 0x0a, 0x00, 0x9c, 0x00, 0x9d, 0x00, 0x2f, 0x00, 0x35, 0xc0,
239+
0x12, 0x00, 0x0a, 0x01, 0x00, 0x00, 0x58, 0x00, 0x00, 0x00, 0x18, 0x00, 0x16, 0x00, 0x00,
240+
0x13, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x75, 0x6c, 0x66, 0x68, 0x65, 0x69,
241+
0x6d, 0x2e, 0x6e, 0x65, 0x74, 0x00, 0x05, 0x00, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
242+
0x0a, 0x00, 0x0a, 0x00, 0x08, 0x00, 0x1d, 0x00, 0x17, 0x00, 0x18, 0x00, 0x19, 0x00, 0x0b,
243+
0x00, 0x02, 0x01, 0x00, 0x00, 0x0d, 0x00, 0x12, 0x00, 0x10, 0x04, 0x01, 0x04, 0x03, 0x05,
244+
0x01, 0x05, 0x03, 0x06, 0x01, 0x06, 0x03, 0x02, 0x01, 0x02, 0x03, 0xff, 0x01, 0x00, 0x01,
245+
0x00, 0x00, 0x12, 0x00, 0x00,
246+
];
247+
248+
let (sconfig, _) = utils::make_configs();
249+
// Size generously so the server's ServerHello + cert chain + ServerHelloDone fit
250+
// without backpressuring the write side.
251+
let (mut cstream, sstream) = tokio::io::duplex(16 * 1024);
252+
cstream.write_all(&client_hello).await.unwrap();
253+
254+
let acceptor = LazyConfigAcceptor::new(rustls::server::Acceptor::default(), sstream)
255+
.with_handshake_timeout(Some(HANDSHAKE_TIMEOUT));
256+
let start = acceptor.await.unwrap();
257+
let err = start.into_stream(Arc::new(sconfig)).await.unwrap_err();
258+
assert_eq!(err.kind(), ErrorKind::TimedOut);
259+
}
260+
209261
const HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
210262

211263
#[tokio::test]

0 commit comments

Comments
 (0)