use std::{
cell::RefCell,
io::{ErrorKind, Read, Write},
net::SocketAddr,
rc::Rc,
time::Instant,
};
use mio::{Token, net::TcpStream};
use rustls::{Error as RustlsError, ServerConnection};
use rusty_ulid::Ulid;
use sozu_command::{
config::MAX_LOOP_ITERATIONS,
logging::{LogContext, ansi_palette},
};
use crate::metrics::names;
use crate::{
Readiness, Ready, SessionMetrics, SessionResult, StateResult, protocol::SessionState,
socket::ShortReadProbe, timer::TimeoutContainer,
};
macro_rules! log_context {
($self:expr) => {{
let (open, reset, grey, gray, white) = ansi_palette();
format!(
"{gray}{ctx}{reset}\t{open}RUSTLS{reset}\t{grey}Session{reset}({gray}sni_bytes{reset}={white}{sni_bytes:?}{reset}, {gray}alpn_bytes{reset}={white}{alpn_bytes:?}{reset}, {gray}version{reset}={white}{version:?}{reset}, {gray}source{reset}={white}{source:?}{reset}, {gray}frontend{reset}={white}{frontend}{reset}, {gray}readiness{reset}={white}{readiness}{reset})\t >>>",
open = open,
reset = reset,
grey = grey,
gray = gray,
white = white,
ctx = $self.log_context(),
sni_bytes = $self.session.server_name().map(str::len),
alpn_bytes = $self.session.alpn_protocol().map(|bytes| bytes.len()),
version = $self.session.protocol_version(),
source = $self
.peer_address
.map(|addr| addr.to_string())
.unwrap_or_else(|| "<none>".to_string()),
frontend = $self.frontend_token.0,
readiness = $self.frontend_readiness,
)
}};
}
#[derive(Debug)]
enum HandshakeReadFault {
Closed,
ReadTls(std::io::Error),
ProcessPackets(RustlsError),
}
fn handshake_read<R: Read>(
session: &mut ServerConnection,
stream: &mut R,
event: &mut Ready,
) -> Result<(), HandshakeReadFault> {
let mut can_read = true;
while can_read && session.wants_read() {
let mut probe = ShortReadProbe::new(&mut *stream);
let read = loop {
match session.read_tls(&mut probe) {
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
read => break read,
}
};
match read {
Ok(0) => return Err(HandshakeReadFault::Closed),
Ok(_) => {
if probe.was_short() {
event.remove(Ready::READABLE);
can_read = false;
}
}
Err(e) if e.kind() == ErrorKind::WouldBlock => {
event.remove(Ready::READABLE);
can_read = false;
}
Err(e) => {
if matches!(
e.kind(),
ErrorKind::ConnectionReset | ErrorKind::ConnectionAborted
) {
incr!(names::rustls::READ_RESET);
}
return Err(HandshakeReadFault::ReadTls(e));
}
}
session
.process_new_packets()
.map_err(HandshakeReadFault::ProcessPackets)?;
}
debug_assert!(
can_read || !event.is_readable(),
"the handshake stopped reading on an empty queue but kept READABLE"
);
Ok(())
}
fn handshake_write<W: Write>(
session: &mut ServerConnection,
stream: &mut W,
) -> std::io::Result<usize> {
loop {
match session.write_tls(stream) {
Err(e) if e.kind() == ErrorKind::Interrupted => continue,
written => break written,
}
}
}
pub enum TlsState {
Initial,
Handshake,
Established,
Error,
}
pub struct TlsHandshake {
pub container_frontend_timeout: TimeoutContainer,
pub frontend_readiness: Readiness,
frontend_token: Token,
pub peer_address: Option<SocketAddr>,
pub request_id: Ulid,
pub session: ServerConnection,
pub stream: TcpStream,
handshake_started_at: Option<Instant>,
}
impl TlsHandshake {
pub fn new(
container_frontend_timeout: TimeoutContainer,
session: ServerConnection,
stream: TcpStream,
frontend_token: Token,
request_id: Ulid,
peer_address: Option<SocketAddr>,
) -> TlsHandshake {
TlsHandshake {
container_frontend_timeout,
frontend_readiness: Readiness {
interest: Ready::READABLE | Ready::HUP | Ready::ERROR,
event: Ready::EMPTY,
},
frontend_token,
peer_address,
request_id,
session,
stream,
handshake_started_at: None,
}
}
fn record_handshake_duration_ms(&mut self) -> Option<u128> {
let was_anchored = self.handshake_started_at.is_some();
let elapsed = self
.handshake_started_at
.take()
.map(|t| t.elapsed().as_millis());
debug_assert!(
self.handshake_started_at.is_none(),
"handshake anchor must be cleared after recording the duration"
);
debug_assert_eq!(
elapsed.is_some(),
was_anchored,
"a duration is returned iff the handshake had been anchored"
);
elapsed
}
pub fn readable(&mut self) -> SessionResult {
self.handshake_started_at.get_or_insert_with(Instant::now);
debug_assert!(
self.handshake_started_at.is_some(),
"handshake anchor must be set before driving TLS I/O"
);
let was_handshaking = self.session.is_handshaking();
match handshake_read(
&mut self.session,
&mut self.stream,
&mut self.frontend_readiness.event,
) {
Ok(()) => {}
Err(HandshakeReadFault::Closed) => {
debug!("{} Connection closed during handshake", log_context!(self));
return SessionResult::Close;
}
Err(HandshakeReadFault::ReadTls(e))
if matches!(
e.kind(),
ErrorKind::ConnectionReset | ErrorKind::ConnectionAborted
) =>
{
debug!(
"{} Connection reset during handshake: {:?}",
log_context!(self),
e
);
return SessionResult::Close;
}
Err(HandshakeReadFault::ReadTls(e)) => {
error!(
"{} Could not perform handshake: {:?}",
log_context!(self),
e
);
return SessionResult::Close;
}
Err(HandshakeReadFault::ProcessPackets(e)) => {
self.log_handshake_error(&e);
return SessionResult::Close;
}
}
debug_assert!(
was_handshaking || !self.session.is_handshaking(),
"rustls handshake must not regress from finished back to handshaking"
);
if self.frontend_readiness.event.is_hup() && self.session.is_handshaking() {
debug!(
"{} Connection closed before the handshake completed",
log_context!(self)
);
return SessionResult::Close;
}
if !self.session.wants_read() {
self.frontend_readiness.interest.remove(Ready::READABLE);
}
debug_assert!(
self.session.wants_read() || !self.frontend_readiness.interest.is_readable(),
"READABLE interest must be cleared once rustls stops wanting reads"
);
if self.session.wants_write() {
self.frontend_readiness.interest.insert(Ready::WRITABLE);
}
if self.session.is_handshaking() {
SessionResult::Continue
} else {
if self.session.wants_write() {
SessionResult::Continue
} else {
debug_assert!(
!self.session.is_handshaking() && !self.session.wants_write(),
"Upgrade requires a completed handshake with no pending output"
);
self.frontend_readiness.interest.insert(Ready::READABLE);
self.frontend_readiness.interest.insert(Ready::WRITABLE);
if let Some(elapsed_ms) = self.record_handshake_duration_ms() {
time!(names::tls::HANDSHAKE_MS, elapsed_ms);
}
SessionResult::Upgrade
}
}
}
pub fn writable(&mut self) -> SessionResult {
self.handshake_started_at.get_or_insert_with(Instant::now);
debug_assert!(
self.handshake_started_at.is_some(),
"handshake anchor must be set before driving TLS I/O"
);
let was_handshaking = self.session.is_handshaking();
let mut can_write = true;
loop {
let mut can_work = false;
if self.session.wants_write() && can_write {
can_work = true;
match handshake_write(&mut self.session, &mut self.stream) {
Ok(_) => {}
Err(e) => match e.kind() {
ErrorKind::WouldBlock => {
self.frontend_readiness.event.remove(Ready::WRITABLE);
can_write = false
}
_ => {
if matches!(
e.kind(),
ErrorKind::ConnectionReset | ErrorKind::ConnectionAborted
) {
incr!(names::rustls::WRITE_RESET);
}
error!(
"{} Could not perform handshake: {:?}",
log_context!(self),
e
);
return SessionResult::Close;
}
},
}
if let Err(e) = self.session.process_new_packets() {
self.log_handshake_error(&e);
return SessionResult::Close;
}
}
if !can_work {
break;
}
}
debug_assert!(
was_handshaking || !self.session.is_handshaking(),
"rustls handshake must not regress from finished back to handshaking"
);
if !self.session.wants_write() {
self.frontend_readiness.interest.remove(Ready::WRITABLE);
}
debug_assert!(
self.session.wants_write() || !self.frontend_readiness.interest.is_writable(),
"WRITABLE interest must be cleared once rustls stops wanting writes"
);
if self.session.wants_read() {
self.frontend_readiness.interest.insert(Ready::READABLE);
}
if self.session.is_handshaking() {
SessionResult::Continue
} else if self.session.wants_read() {
debug_assert!(
!self.session.is_handshaking(),
"Upgrade requires a completed handshake"
);
self.frontend_readiness.interest.insert(Ready::READABLE);
if let Some(elapsed_ms) = self.record_handshake_duration_ms() {
time!(names::tls::HANDSHAKE_MS, elapsed_ms);
}
SessionResult::Upgrade
} else {
debug_assert!(
!self.session.is_handshaking(),
"Upgrade requires a completed handshake"
);
self.frontend_readiness.interest.insert(Ready::WRITABLE);
self.frontend_readiness.interest.insert(Ready::READABLE);
if let Some(elapsed_ms) = self.record_handshake_duration_ms() {
time!(names::tls::HANDSHAKE_MS, elapsed_ms);
}
SessionResult::Upgrade
}
}
pub fn log_context(&self) -> LogContext<'_> {
LogContext {
session_id: self.request_id,
request_id: None,
cluster_id: None,
backend_id: None,
}
}
pub fn front_socket(&self) -> &TcpStream {
&self.stream
}
fn log_handshake_error(&self, err: &RustlsError) {
let reason = handshake_failure_reason(err);
debug_assert!(
reason.starts_with("tls.handshake.failed."),
"handshake failure metric {reason} escaped the tls.handshake.failed. namespace"
);
match err {
RustlsError::AlertReceived(_) => debug!(
"{} Could not perform handshake: {:?}",
log_context!(self),
err
),
RustlsError::PeerIncompatible(_)
| RustlsError::PeerMisbehaved(_)
| RustlsError::InvalidMessage(_)
| RustlsError::InappropriateMessage { .. }
| RustlsError::InappropriateHandshakeMessage { .. }
| RustlsError::PeerSentOversizedRecord
| RustlsError::NoApplicationProtocol
| RustlsError::InvalidCertificate(_)
| RustlsError::DecryptError
| RustlsError::NoCertificatesPresented => warn!(
"{} Could not perform handshake: {:?}",
log_context!(self),
err
),
_ => error!(
"{} Could not perform handshake: {:?}",
log_context!(self),
err
),
}
count!(reason, 1);
}
}
fn handshake_failure_reason(err: &RustlsError) -> &'static str {
match err {
RustlsError::AlertReceived(_) => "tls.handshake.failed.alert_received",
RustlsError::PeerIncompatible(_) => "tls.handshake.failed.peer_incompatible",
RustlsError::PeerMisbehaved(_) => "tls.handshake.failed.peer_misbehaved",
RustlsError::InvalidMessage(_) => "tls.handshake.failed.invalid_message",
RustlsError::InappropriateMessage { .. } => "tls.handshake.failed.inappropriate_message",
RustlsError::InappropriateHandshakeMessage { .. } => {
"tls.handshake.failed.inappropriate_handshake_message"
}
RustlsError::PeerSentOversizedRecord => "tls.handshake.failed.oversized_record",
RustlsError::NoApplicationProtocol => "tls.handshake.failed.no_alpn",
RustlsError::InvalidCertificate(_) => "tls.handshake.failed.invalid_certificate",
RustlsError::DecryptError => "tls.handshake.failed.decrypt_error",
RustlsError::NoCertificatesPresented => "tls.handshake.failed.no_certificates_present",
_ => "tls.handshake.failed.other",
}
}
#[cfg(test)]
thread_local! {
static HANDSHAKE_PASSES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
impl TlsHandshake {
fn drive(&mut self) -> SessionResult {
let mut counter = 0;
if self.frontend_readiness.event.is_hup()
&& !self.frontend_readiness.filter_interest().is_readable()
{
return SessionResult::Close;
}
while counter < MAX_LOOP_ITERATIONS {
#[cfg(test)]
HANDSHAKE_PASSES.with(|passes| passes.set(passes.get() + 1));
let frontend_interest = self.frontend_readiness.filter_interest();
trace!("{} Interest({:?})", log_context!(self), frontend_interest);
if !frontend_interest.is_readable()
&& !frontend_interest.is_writable()
&& !frontend_interest.is_error()
{
break;
}
if frontend_interest.is_readable() {
let protocol_result = self.readable();
if protocol_result != SessionResult::Continue {
return protocol_result;
}
}
if frontend_interest.is_writable() {
let protocol_result = self.writable();
if protocol_result != SessionResult::Continue {
return protocol_result;
}
}
if frontend_interest.is_error() {
error!("{} Front socket error, disconnecting", log_context!(self));
self.frontend_readiness.interest = Ready::EMPTY;
return SessionResult::Close;
}
counter += 1;
}
if counter >= MAX_LOOP_ITERATIONS {
error!(
"{}\tHandling session went through {} iterations, there's a probable infinite loop bug, closing the connection",
log_context!(self),
MAX_LOOP_ITERATIONS
);
incr!(names::http::INFINITE_LOOP_ERROR);
self.print_state("HTTPS");
return SessionResult::Close;
}
SessionResult::Continue
}
}
impl SessionState for TlsHandshake {
fn ready(
&mut self,
_session: Rc<RefCell<dyn crate::ProxySession>>,
_proxy: Rc<RefCell<dyn crate::L7Proxy>>,
_metrics: &mut SessionMetrics,
) -> SessionResult {
self.drive()
}
fn update_readiness(&mut self, token: Token, events: Ready) {
if self.frontend_token == token {
self.frontend_readiness.event |= events;
}
}
fn timeout(&mut self, token: Token, _metrics: &mut SessionMetrics) -> StateResult {
if self.frontend_token == token {
self.container_frontend_timeout.triggered();
return StateResult::CloseSession;
}
error!(
"{}, Expect state: got timeout for an invalid token: {:?}",
log_context!(self),
token
);
StateResult::CloseSession
}
fn cancel_timeouts(&mut self) {
self.container_frontend_timeout.cancel();
}
fn print_state(&self, context: &str) {
error!(
"{} Session(Handshake)\n\tFrontend:\n\t\ttoken: {:?}\treadiness: {:?}",
context, self.frontend_token, self.frontend_readiness
);
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use rustls::{
AlertDescription, CertificateError, ContentType, Error as RustlsError, HandshakeType,
InvalidMessage, PeerIncompatible, PeerMisbehaved,
};
use mio::{Token, net::TcpStream};
use rusty_ulid::Ulid;
use super::{
HandshakeReadFault, TlsHandshake, handshake_failure_reason, handshake_read, handshake_write,
};
use crate::{
Ready, SessionResult, metrics::names, protocol::SessionState, timer::TimeoutContainer,
};
use std::io::ErrorKind;
#[test]
fn handshake_failure_reason_maps_every_variant_to_unique_namespaced_key() {
let cases: &[(RustlsError, &str)] = &[
(
RustlsError::AlertReceived(AlertDescription::HandshakeFailure),
"tls.handshake.failed.alert_received",
),
(
RustlsError::PeerIncompatible(PeerIncompatible::NoCipherSuitesInCommon),
"tls.handshake.failed.peer_incompatible",
),
(
RustlsError::PeerMisbehaved(PeerMisbehaved::IllegalMiddleboxChangeCipherSpec),
"tls.handshake.failed.peer_misbehaved",
),
(
RustlsError::InvalidMessage(InvalidMessage::InvalidContentType),
"tls.handshake.failed.invalid_message",
),
(
RustlsError::InappropriateMessage {
expect_types: vec![ContentType::Handshake],
got_type: ContentType::ApplicationData,
},
"tls.handshake.failed.inappropriate_message",
),
(
RustlsError::InappropriateHandshakeMessage {
expect_types: vec![HandshakeType::ClientHello],
got_type: HandshakeType::Finished,
},
"tls.handshake.failed.inappropriate_handshake_message",
),
(
RustlsError::PeerSentOversizedRecord,
"tls.handshake.failed.oversized_record",
),
(
RustlsError::NoApplicationProtocol,
"tls.handshake.failed.no_alpn",
),
(
RustlsError::InvalidCertificate(CertificateError::Expired),
"tls.handshake.failed.invalid_certificate",
),
(
RustlsError::DecryptError,
"tls.handshake.failed.decrypt_error",
),
(
RustlsError::NoCertificatesPresented,
"tls.handshake.failed.no_certificates_present",
),
(
RustlsError::General("test".to_owned()),
"tls.handshake.failed.other",
),
(RustlsError::EncryptError, "tls.handshake.failed.other"),
(
RustlsError::FailedToGetCurrentTime,
"tls.handshake.failed.other",
),
(
RustlsError::HandshakeNotComplete,
"tls.handshake.failed.other",
),
];
let mut seen = HashSet::new();
for (err, expected) in cases {
let got = handshake_failure_reason(err);
assert_eq!(got, *expected, "variant {err:?} → {got}, want {expected}");
assert!(
got.starts_with("tls.handshake.failed."),
"reason {got} missing tls.handshake.failed. namespace"
);
seen.insert(got);
}
assert_eq!(seen.len(), 12, "unexpected key set: {seen:?}");
}
fn flight<D>(conn: &mut rustls::ConnectionCommon<D>) -> Vec<u8> {
let mut wire = Vec::new();
while conn.wants_write() {
conn.write_tls(&mut wire)
.expect("a TLS flight must serialize into memory");
}
wire
}
fn deliver(client: &mut rustls::ClientConnection, wire: &[u8]) {
let mut cursor = std::io::Cursor::new(wire);
while (cursor.position() as usize) < wire.len() {
client
.read_tls(&mut cursor)
.expect("the server flight must be readable from memory");
client
.process_new_packets()
.expect("the server flight must process cleanly");
}
}
#[test]
fn a_short_handshake_read_stops_without_an_eagain() {
use crate::socket::rustls_read_tests::{CountingTransport, fresh_pair};
let (mut server, mut client) = fresh_pair(Vec::new());
let hello = flight(&mut client);
assert!(
hello.len() < 4096,
"premise: the ClientHello is a short read"
);
let mut transport = CountingTransport {
wire: hello.into_iter().collect(),
..Default::default()
};
let mut event = Ready::READABLE;
handshake_read(&mut server, &mut transport, &mut event)
.expect("the ClientHello must process");
assert_eq!((transport.reads, transport.eagains), (1, 0));
assert!(
!event.is_readable(),
"a short read proved the queue empty: READABLE must go"
);
assert!(server.is_handshaking());
deliver(&mut client, &flight(&mut server));
transport.wire.extend(flight(&mut client));
event = Ready::READABLE;
handshake_read(&mut server, &mut transport, &mut event)
.expect("the client Finished must process");
assert_eq!(
(transport.reads, transport.eagains),
(2, 0),
"the Finished costs one recv and no EAGAIN"
);
assert!(!event.is_readable());
assert!(!server.is_handshaking(), "the handshake is complete");
}
#[test]
fn a_reset_during_the_handshake_counts_a_read_reset() {
use crate::socket::{rustls_read_tests::fresh_pair, tests};
let (mut server, _client) = fresh_pair(Vec::new());
let mut stream = tests::reset_stream();
let before = tests::proxy_count(names::rustls::READ_RESET);
let mut event = Ready::READABLE;
let fault = handshake_read(&mut server, &mut stream, &mut event)
.expect_err("a reset socket cannot carry a handshake");
assert!(
matches!(&fault, HandshakeReadFault::ReadTls(e) if e.kind() == ErrorKind::ConnectionReset),
"the reset surfaces as a read fault, got {fault:?}"
);
assert_eq!(tests::proxy_count(names::rustls::READ_RESET) - before, 1);
}
#[test]
fn a_full_handshake_read_reads_again() {
use crate::socket::rustls_read_tests::{CountingTransport, fresh_pair};
let alpn = (0..64u8)
.map(|index| vec![b'a' + index % 26; 100])
.collect();
let (mut server, mut client) = fresh_pair(alpn);
let hello = flight(&mut client);
assert!(
4096 < hello.len() && hello.len() < 8192,
"premise: one full read and one short one, got {} bytes",
hello.len()
);
let mut transport = CountingTransport {
wire: hello.into_iter().collect(),
..Default::default()
};
let mut event = Ready::READABLE;
handshake_read(&mut server, &mut transport, &mut event)
.expect("the padded ClientHello must process");
assert_eq!(
(transport.reads, transport.eagains),
(2, 0),
"a full read is followed by one more recv, which is short"
);
assert!(!event.is_readable());
assert!(server.wants_write(), "the whole ClientHello was processed");
}
struct InterruptedOnce<T> {
inner: T,
calls: usize,
}
impl<T> InterruptedOnce<T> {
fn new(inner: T) -> Self {
InterruptedOnce { inner, calls: 0 }
}
fn interrupt_first(&mut self) -> std::io::Result<()> {
self.calls += 1;
if self.calls == 1 {
return Err(ErrorKind::Interrupted.into());
}
Ok(())
}
}
impl<T: std::io::Read> std::io::Read for InterruptedOnce<T> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.interrupt_first()?;
self.inner.read(buf)
}
}
impl<T: std::io::Write> std::io::Write for InterruptedOnce<T> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.interrupt_first()?;
self.inner.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.inner.flush()
}
}
#[test]
fn an_interrupted_handshake_read_is_retried() {
use crate::socket::rustls_read_tests::{CountingTransport, fresh_pair};
let (mut server, mut client) = fresh_pair(Vec::new());
let mut transport = InterruptedOnce::new(CountingTransport {
wire: flight(&mut client).into_iter().collect(),
..Default::default()
});
let mut event = Ready::READABLE;
handshake_read(&mut server, &mut transport, &mut event)
.expect("an interrupted read must be retried, not reported");
assert_eq!(transport.calls, 2, "one interrupted read, one retry");
assert_eq!(
(transport.inner.reads, transport.inner.eagains),
(1, 0),
"the retry is the only recv to reach the socket"
);
assert!(
transport.inner.wire.is_empty(),
"the retry read the ClientHello"
);
assert!(
!event.is_readable(),
"the retried read was short: READABLE must go"
);
assert!(server.wants_write(), "the whole ClientHello was processed");
}
#[test]
fn an_interrupted_handshake_write_is_retried() {
use crate::socket::rustls_read_tests::fresh_pair;
let (mut server, mut client) = fresh_pair(Vec::new());
let hello = flight(&mut client);
server
.read_tls(&mut std::io::Cursor::new(hello))
.expect("the ClientHello must be readable from memory");
server
.process_new_packets()
.expect("the ClientHello must process cleanly");
assert!(server.wants_write(), "premise: the server flight is queued");
let mut transport = InterruptedOnce::new(Vec::new());
let written = handshake_write(&mut server, &mut transport)
.expect("an interrupted write must be retried, not reported");
assert_eq!(transport.calls, 2, "one interrupted write, one retry");
assert!(written > 0 && transport.inner.len() == written);
let mut wire = transport.inner;
wire.extend(flight(&mut server));
deliver(&mut client, &wire);
assert!(
client.wants_write(),
"the client got the whole server flight and answers it"
);
}
fn hello_then_fin(hello_part: impl Fn(usize) -> usize, event: Ready) -> (SessionResult, usize) {
use std::io::Write as _;
use crate::socket::rustls_read_tests::fresh_pair;
let listener = std::net::TcpListener::bind("127.0.0.1:0")
.expect("test listener must bind to a loopback port");
let mut peer = std::net::TcpStream::connect(
listener
.local_addr()
.expect("test listener must report its local address"),
)
.expect("loopback connect must complete");
let (accepted, _) = listener.accept().expect("the connection must be accepted");
accepted
.set_nonblocking(true)
.expect("mio requires a nonblocking stream");
let (server, mut client) = fresh_pair(Vec::new());
let hello = flight(&mut client);
peer.write_all(&hello[..hello_part(hello.len())])
.expect("the client must send");
peer.shutdown(std::net::Shutdown::Write)
.expect("the client must send its FIN");
let token = Token(7);
let mut handshake = TlsHandshake::new(
TimeoutContainer::new_empty(std::time::Duration::from_secs(10)),
server,
TcpStream::from_std(accepted),
token,
Ulid::generate(),
None,
);
handshake.update_readiness(token, event);
super::HANDSHAKE_PASSES.with(|passes| passes.set(0));
let result = handshake.drive();
(result, super::HANDSHAKE_PASSES.with(|passes| passes.get()))
}
#[test]
fn a_partial_client_hello_then_fin_closes_without_spinning() {
let (result, passes) = hello_then_fin(|len| len / 2, Ready::READABLE | Ready::HUP);
assert_eq!(result, SessionResult::Close, "the EOF closes the handshake");
assert!(
passes < 4,
"one event settles in a few passes, took {passes}"
);
}
#[test]
fn a_full_client_hello_then_fin_closes_without_spinning() {
let (result, passes) = hello_then_fin(|len| len, Ready::READABLE | Ready::HUP);
assert_eq!(result, SessionResult::Close, "the EOF closes the handshake");
assert!(
passes < 4,
"one event settles in a few passes, took {passes}"
);
}
#[test]
fn a_full_client_hello_then_fin_with_writable_closes_without_spinning() {
let (result, passes) =
hello_then_fin(|len| len, Ready::READABLE | Ready::WRITABLE | Ready::HUP);
assert_eq!(result, SessionResult::Close, "the EOF closes the handshake");
assert!(
passes < 4,
"one event settles in a few passes, took {passes}"
);
}
fn drive(handshake: &mut TlsHandshake) -> SessionResult {
for _ in 0..16 {
let interest = handshake.frontend_readiness.filter_interest();
if interest.is_empty() {
return SessionResult::Continue;
}
if interest.is_readable() {
let result = handshake.readable();
if result != SessionResult::Continue {
return result;
}
}
if interest.is_writable() {
let result = handshake.writable();
if result != SessionResult::Continue {
return result;
}
}
}
panic!("the handshake did not settle within 16 passes");
}
#[test]
fn a_client_hello_in_three_segments_completes_on_a_loopback_socket() {
use std::io::{Read as _, Write as _};
use crate::socket::rustls_read_tests::fresh_pair;
let token = Token(7);
let mut poll = mio::Poll::new().expect("a poll instance must open");
let mut events = mio::Events::with_capacity(8);
let listener = std::net::TcpListener::bind("127.0.0.1:0")
.expect("test listener must bind to a loopback port");
let mut peer = std::net::TcpStream::connect(
listener
.local_addr()
.expect("test listener must report its local address"),
)
.expect("loopback connect must complete");
peer.set_nodelay(true)
.expect("the client must disable Nagle to send each segment at once");
let (accepted, _) = listener.accept().expect("the connection must be accepted");
accepted
.set_nonblocking(true)
.expect("mio requires a nonblocking stream");
let mut stream = TcpStream::from_std(accepted);
poll.registry()
.register(&mut stream, token, mio::Interest::READABLE)
.expect("the stream must register");
let (server, mut client) = fresh_pair(Vec::new());
let mut handshake = TlsHandshake::new(
TimeoutContainer::new_empty(std::time::Duration::from_secs(10)),
server,
stream,
token,
Ulid::generate(),
None,
);
let mut edge_within = |poll: &mut mio::Poll, timeout: std::time::Duration| {
poll.poll(&mut events, Some(timeout))
.expect("poll must succeed");
events
.iter()
.any(|event| event.token() == token && event.is_readable())
};
let edge = std::time::Duration::from_secs(5);
let quiet = std::time::Duration::from_millis(50);
let hello = flight(&mut client);
let third = hello.len() / 3;
let segments = [
&hello[..third],
&hello[third..2 * third],
&hello[2 * third..],
];
for (index, segment) in segments.iter().enumerate() {
peer.write_all(segment).expect("the client must send");
assert!(
edge_within(&mut poll, edge),
"segment {index} must raise an edge"
);
handshake.update_readiness(token, Ready::READABLE);
assert_eq!(drive(&mut handshake), SessionResult::Continue);
assert!(
!handshake.frontend_readiness.event.is_readable(),
"segment {index}: the short read must drop READABLE"
);
if index + 1 < segments.len() {
assert!(handshake.session.is_handshaking());
assert!(
!edge_within(&mut poll, quiet),
"segment {index}: edge-triggered, nothing new, no event"
);
let mut probe = [0u8; 1];
assert!(
matches!(
handshake.stream.read(&mut probe),
Err(ref e) if e.kind() == ErrorKind::WouldBlock
),
"segment {index}: nothing is left queued"
);
}
}
assert!(
handshake.session.wants_write(),
"the whole ClientHello was read and processed"
);
handshake.update_readiness(token, Ready::WRITABLE);
assert_eq!(drive(&mut handshake), SessionResult::Continue);
assert!(!handshake.session.wants_write(), "the server flight is out");
peer.set_read_timeout(Some(std::time::Duration::from_secs(5)))
.expect("the client read timeout must set");
while client.is_handshaking() {
client
.read_tls(&mut peer)
.expect("the client must read the server flight");
client
.process_new_packets()
.expect("the server flight must process cleanly");
}
peer.write_all(&flight(&mut client))
.expect("the client must send its Finished");
assert!(
edge_within(&mut poll, edge),
"the Finished must raise an edge"
);
handshake.update_readiness(token, Ready::READABLE);
assert_eq!(drive(&mut handshake), SessionResult::Upgrade);
assert!(!handshake.session.is_handshaking());
assert!(
!handshake.frontend_readiness.event.is_readable(),
"the socket is empty: the upgrade must not be handed a READABLE"
);
}
}