Skip to main content

sozu_lib/protocol/
rustls.rs

1//! Rustls handshake driver.
2//!
3//! Owns the per-session `rustls::ServerConnection` during the TLS
4//! handshake: pumps `read_tls`/`write_tls`, surfaces handshake completion
5//! to the parent state, and emits handshake-completion metrics. Cipher /
6//! ALPN / SNI binding decisions live in `lib/src/https.rs`; certificate
7//! resolution and dynamic cert reload live in `lib/src/tls.rs`.
8
9use std::{cell::RefCell, io::ErrorKind, net::SocketAddr, rc::Rc, time::Instant};
10
11use mio::{Token, net::TcpStream};
12use rustls::{Error as RustlsError, ServerConnection};
13use rusty_ulid::Ulid;
14use sozu_command::{
15    config::MAX_LOOP_ITERATIONS,
16    logging::{LogContext, ansi_palette},
17};
18
19use crate::metrics::names;
20use crate::{
21    Readiness, Ready, SessionMetrics, SessionResult, StateResult, protocol::SessionState,
22    timer::TimeoutContainer,
23};
24
25/// This macro is defined uniquely in this module to help the tracking of tls
26/// issues inside Sōzu. When the logger emits to a TTY the protocol label is
27/// bold bright-white (uniform across every protocol), the `Session` keyword is
28/// light grey, attribute keys are gray and values are bright white. ANSI codes
29/// are skipped when output goes to a file or otherwise non-colored sink. The
30/// `[ulid - - -]` context prefix comes first to keep column alignment with
31/// `MUX-*` and `SOCKET` logs.
32macro_rules! log_context {
33    ($self:expr) => {{
34        let (open, reset, grey, gray, white) = ansi_palette();
35        format!(
36            "{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 >>>",
37            open = open,
38            reset = reset,
39            grey = grey,
40            gray = gray,
41            white = white,
42            ctx = $self.log_context(),
43            sni_bytes = $self.session.server_name().map(str::len),
44            alpn_bytes = $self.session.alpn_protocol().map(|bytes| bytes.len()),
45            version = $self.session.protocol_version(),
46            source = $self
47                .peer_address
48                .map(|addr| addr.to_string())
49                .unwrap_or_else(|| "<none>".to_string()),
50            frontend = $self.frontend_token.0,
51            readiness = $self.frontend_readiness,
52        )
53    }};
54}
55
56pub enum TlsState {
57    Initial,
58    Handshake,
59    Established,
60    Error,
61}
62
63pub struct TlsHandshake {
64    pub container_frontend_timeout: TimeoutContainer,
65    pub frontend_readiness: Readiness,
66    frontend_token: Token,
67    pub peer_address: Option<SocketAddr>,
68    pub request_id: Ulid,
69    pub session: ServerConnection,
70    pub stream: TcpStream,
71    /// Wall-clock anchor for the `tls.handshake_ms` histogram. Captured the
72    /// first time the handshake state actually does I/O (not at construction,
73    /// because the session may sit in the accept queue or in expect-proxy for
74    /// an unbounded amount of time before the TLS bytes start flowing).
75    handshake_started_at: Option<Instant>,
76}
77
78impl TlsHandshake {
79    /// Instantiate a new TlsHandshake SessionState with:
80    ///
81    /// - frontend_interest: READABLE | HUP | ERROR
82    /// - frontend_event: EMPTY
83    ///
84    /// Remember to set the events from the previous State!
85    pub fn new(
86        container_frontend_timeout: TimeoutContainer,
87        session: ServerConnection,
88        stream: TcpStream,
89        frontend_token: Token,
90        request_id: Ulid,
91        peer_address: Option<SocketAddr>,
92    ) -> TlsHandshake {
93        TlsHandshake {
94            container_frontend_timeout,
95            frontend_readiness: Readiness {
96                interest: Ready::READABLE | Ready::HUP | Ready::ERROR,
97                event: Ready::EMPTY,
98            },
99            frontend_token,
100            peer_address,
101            request_id,
102            session,
103            stream,
104            handshake_started_at: None,
105        }
106    }
107
108    /// Returns the elapsed handshake duration in milliseconds and clears the
109    /// captured start instant so the histogram is only recorded once. Returns
110    /// `None` when no I/O happened (e.g. the connection closed mid-handshake
111    /// before any bytes were exchanged); callers should not emit
112    /// `tls.handshake_ms` in that case.
113    fn record_handshake_duration_ms(&mut self) -> Option<u128> {
114        let was_anchored = self.handshake_started_at.is_some();
115        let elapsed = self
116            .handshake_started_at
117            .take()
118            .map(|t| t.elapsed().as_millis());
119        // `take()` is idempotent-disarming: the anchor is always cleared so the
120        // histogram is recorded at most once, and a duration is returned iff an
121        // anchor existed.
122        debug_assert!(
123            self.handshake_started_at.is_none(),
124            "handshake anchor must be cleared after recording the duration"
125        );
126        debug_assert_eq!(
127            elapsed.is_some(),
128            was_anchored,
129            "a duration is returned iff the handshake had been anchored"
130        );
131        elapsed
132    }
133
134    pub fn readable(&mut self) -> SessionResult {
135        // Anchor the handshake duration the first time we observe TLS bytes
136        // moving in either direction. Using `get_or_insert_with` keeps the
137        // anchor sticky across `WouldBlock` retries and across the
138        // readable/writable boundary.
139        self.handshake_started_at.get_or_insert_with(Instant::now);
140        // The anchor is sticky once set: this method must never run unanchored.
141        debug_assert!(
142            self.handshake_started_at.is_some(),
143            "handshake anchor must be set before driving TLS I/O"
144        );
145
146        // rustls handshake completion is monotonic (`true → false`, never
147        // back). Snapshot it so the exit assertions can prove we never resurrect
148        // a finished handshake.
149        let was_handshaking = self.session.is_handshaking();
150
151        let mut can_read = true;
152
153        loop {
154            let mut can_work = false;
155
156            if self.session.wants_read() && can_read {
157                can_work = true;
158
159                match self.session.read_tls(&mut self.stream) {
160                    Ok(0) => {
161                        error!("{} Connection closed during handshake", log_context!(self));
162                        return SessionResult::Close;
163                    }
164                    Ok(_) => {}
165                    Err(e) => match e.kind() {
166                        ErrorKind::WouldBlock => {
167                            self.frontend_readiness.event.remove(Ready::READABLE);
168                            can_read = false
169                        }
170                        _ => {
171                            error!(
172                                "{} Could not perform handshake: {:?}",
173                                log_context!(self),
174                                e
175                            );
176                            return SessionResult::Close;
177                        }
178                    },
179                }
180
181                if let Err(e) = self.session.process_new_packets() {
182                    self.log_handshake_error(&e);
183                    return SessionResult::Close;
184                }
185            }
186
187            if !can_work {
188                break;
189            }
190        }
191
192        // Handshake completion is monotonic: a handshake that had already
193        // finished at entry cannot become unfinished by pumping `read_tls`.
194        debug_assert!(
195            was_handshaking || !self.session.is_handshaking(),
196            "rustls handshake must not regress from finished back to handshaking"
197        );
198
199        // Readiness must mirror rustls's own wants: we only drop READABLE
200        // interest when the session no longer wants to read.
201        if !self.session.wants_read() {
202            self.frontend_readiness.interest.remove(Ready::READABLE);
203        }
204        debug_assert!(
205            self.session.wants_read() || !self.frontend_readiness.interest.is_readable(),
206            "READABLE interest must be cleared once rustls stops wanting reads"
207        );
208
209        if self.session.wants_write() {
210            self.frontend_readiness.interest.insert(Ready::WRITABLE);
211        }
212
213        if self.session.is_handshaking() {
214            SessionResult::Continue
215        } else {
216            // handshake might be finished, but we still have something to send
217            if self.session.wants_write() {
218                SessionResult::Continue
219            } else {
220                // Upgrade is only signalled once the handshake is complete and
221                // there is nothing left to flush to the peer.
222                debug_assert!(
223                    !self.session.is_handshaking() && !self.session.wants_write(),
224                    "Upgrade requires a completed handshake with no pending output"
225                );
226                self.frontend_readiness.interest.insert(Ready::READABLE);
227                self.frontend_readiness.event.insert(Ready::READABLE);
228                self.frontend_readiness.interest.insert(Ready::WRITABLE);
229                if let Some(elapsed_ms) = self.record_handshake_duration_ms() {
230                    time!(names::tls::HANDSHAKE_MS, elapsed_ms);
231                }
232                SessionResult::Upgrade
233            }
234        }
235    }
236
237    pub fn writable(&mut self) -> SessionResult {
238        // Same anchor logic as `readable()` — see the comment there.
239        self.handshake_started_at.get_or_insert_with(Instant::now);
240        debug_assert!(
241            self.handshake_started_at.is_some(),
242            "handshake anchor must be set before driving TLS I/O"
243        );
244
245        // Snapshot handshake completion for the monotonicity post-condition.
246        let was_handshaking = self.session.is_handshaking();
247
248        let mut can_write = true;
249
250        loop {
251            let mut can_work = false;
252
253            if self.session.wants_write() && can_write {
254                can_work = true;
255
256                match self.session.write_tls(&mut self.stream) {
257                    Ok(_) => {}
258                    Err(e) => match e.kind() {
259                        ErrorKind::WouldBlock => {
260                            self.frontend_readiness.event.remove(Ready::WRITABLE);
261                            can_write = false
262                        }
263                        _ => {
264                            error!(
265                                "{} Could not perform handshake: {:?}",
266                                log_context!(self),
267                                e
268                            );
269                            return SessionResult::Close;
270                        }
271                    },
272                }
273
274                if let Err(e) = self.session.process_new_packets() {
275                    self.log_handshake_error(&e);
276                    return SessionResult::Close;
277                }
278            }
279
280            if !can_work {
281                break;
282            }
283        }
284
285        // Handshake completion is monotonic: pumping `write_tls` can finish a
286        // handshake but never un-finish one.
287        debug_assert!(
288            was_handshaking || !self.session.is_handshaking(),
289            "rustls handshake must not regress from finished back to handshaking"
290        );
291
292        // Readiness mirrors rustls's wants: WRITABLE interest is only dropped
293        // once the session no longer wants to write.
294        if !self.session.wants_write() {
295            self.frontend_readiness.interest.remove(Ready::WRITABLE);
296        }
297        debug_assert!(
298            self.session.wants_write() || !self.frontend_readiness.interest.is_writable(),
299            "WRITABLE interest must be cleared once rustls stops wanting writes"
300        );
301
302        if self.session.wants_read() {
303            self.frontend_readiness.interest.insert(Ready::READABLE);
304        }
305
306        if self.session.is_handshaking() {
307            SessionResult::Continue
308        } else if self.session.wants_read() {
309            // Upgrade after a completed handshake; the session still wants to
310            // read application data, which the upgraded state will drive.
311            debug_assert!(
312                !self.session.is_handshaking(),
313                "Upgrade requires a completed handshake"
314            );
315            self.frontend_readiness.interest.insert(Ready::READABLE);
316            if let Some(elapsed_ms) = self.record_handshake_duration_ms() {
317                time!(names::tls::HANDSHAKE_MS, elapsed_ms);
318            }
319            SessionResult::Upgrade
320        } else {
321            debug_assert!(
322                !self.session.is_handshaking(),
323                "Upgrade requires a completed handshake"
324            );
325            self.frontend_readiness.interest.insert(Ready::WRITABLE);
326            self.frontend_readiness.interest.insert(Ready::READABLE);
327            if let Some(elapsed_ms) = self.record_handshake_duration_ms() {
328                time!(names::tls::HANDSHAKE_MS, elapsed_ms);
329            }
330            SessionResult::Upgrade
331        }
332    }
333
334    pub fn log_context(&self) -> LogContext<'_> {
335        LogContext {
336            session_id: self.request_id,
337            request_id: None,
338            cluster_id: None,
339            backend_id: None,
340        }
341    }
342
343    pub fn front_socket(&self) -> &TcpStream {
344        &self.stream
345    }
346
347    /// Tiered logging for TLS handshake errors surfaced by `process_new_packets`.
348    ///
349    /// - `AlertReceived(_)`: remote peer rejected our cert/config (e.g. old
350    ///   CA bundle, scanner, cert-pinning client). Not actionable per-connection
351    ///   on a public endpoint, so log at `debug!`.
352    /// - Peer protocol violations (`PeerIncompatible`, `PeerMisbehaved`,
353    ///   `InvalidMessage`, inappropriate message / handshake message,
354    ///   oversized record, ALPN mismatch, bad client cert, `DecryptError`,
355    ///   `NoCertificatesPresented`): occasionally useful to spot buggy
356    ///   clients or stale roots, so log at `warn!`.
357    /// - Everything else (local/config/provider failures like `EncryptError`,
358    ///   `General`, `Other`, CRL issues, missing entropy): genuine server-side
359    ///   problems, stay at `error!`.
360    ///
361    /// Each tier additionally bumps `tls.handshake.failed.<reason>` so dashboards
362    /// can split spikes by category without having to grep logs.
363    fn log_handshake_error(&self, err: &RustlsError) {
364        let reason = handshake_failure_reason(err);
365        // Every reason must stay inside the bounded `tls.handshake.failed.*`
366        // namespace so statsd cardinality is predictable — unknown variants
367        // collapse to `.other`, never an unnamespaced key.
368        debug_assert!(
369            reason.starts_with("tls.handshake.failed."),
370            "handshake failure metric {reason} escaped the tls.handshake.failed. namespace"
371        );
372        match err {
373            RustlsError::AlertReceived(_) => debug!(
374                "{} Could not perform handshake: {:?}",
375                log_context!(self),
376                err
377            ),
378            RustlsError::PeerIncompatible(_)
379            | RustlsError::PeerMisbehaved(_)
380            | RustlsError::InvalidMessage(_)
381            | RustlsError::InappropriateMessage { .. }
382            | RustlsError::InappropriateHandshakeMessage { .. }
383            | RustlsError::PeerSentOversizedRecord
384            | RustlsError::NoApplicationProtocol
385            | RustlsError::InvalidCertificate(_)
386            | RustlsError::DecryptError
387            | RustlsError::NoCertificatesPresented => warn!(
388                "{} Could not perform handshake: {:?}",
389                log_context!(self),
390                err
391            ),
392            _ => error!(
393                "{} Could not perform handshake: {:?}",
394                log_context!(self),
395                err
396            ),
397        }
398        count!(reason, 1);
399    }
400}
401
402/// Compile-time literal `tls.handshake.failed.<reason>` keys for every variant
403/// the proxy can observe. Free function (rather than a method) so unit tests
404/// can drive it without constructing a real `ServerConnection`. The set of
405/// suffixes is bounded — anything outside the explicit `match` arms collapses
406/// to `tls.handshake.failed.other` so statsd cardinality stays predictable.
407fn handshake_failure_reason(err: &RustlsError) -> &'static str {
408    match err {
409        RustlsError::AlertReceived(_) => "tls.handshake.failed.alert_received",
410        RustlsError::PeerIncompatible(_) => "tls.handshake.failed.peer_incompatible",
411        RustlsError::PeerMisbehaved(_) => "tls.handshake.failed.peer_misbehaved",
412        RustlsError::InvalidMessage(_) => "tls.handshake.failed.invalid_message",
413        RustlsError::InappropriateMessage { .. } => "tls.handshake.failed.inappropriate_message",
414        RustlsError::InappropriateHandshakeMessage { .. } => {
415            "tls.handshake.failed.inappropriate_handshake_message"
416        }
417        RustlsError::PeerSentOversizedRecord => "tls.handshake.failed.oversized_record",
418        RustlsError::NoApplicationProtocol => "tls.handshake.failed.no_alpn",
419        RustlsError::InvalidCertificate(_) => "tls.handshake.failed.invalid_certificate",
420        RustlsError::DecryptError => "tls.handshake.failed.decrypt_error",
421        RustlsError::NoCertificatesPresented => "tls.handshake.failed.no_certificates_present",
422        _ => "tls.handshake.failed.other",
423    }
424}
425
426impl SessionState for TlsHandshake {
427    fn ready(
428        &mut self,
429        _session: Rc<RefCell<dyn crate::ProxySession>>,
430        _proxy: Rc<RefCell<dyn crate::L7Proxy>>,
431        _metrics: &mut SessionMetrics,
432    ) -> SessionResult {
433        let mut counter = 0;
434
435        if self.frontend_readiness.event.is_hup() {
436            return SessionResult::Close;
437        }
438
439        while counter < MAX_LOOP_ITERATIONS {
440            let frontend_interest = self.frontend_readiness.filter_interest();
441
442            trace!("{} Interest({:?})", log_context!(self), frontend_interest);
443            if frontend_interest.is_empty() {
444                break;
445            }
446
447            if frontend_interest.is_readable() {
448                let protocol_result = self.readable();
449                if protocol_result != SessionResult::Continue {
450                    return protocol_result;
451                }
452            }
453
454            if frontend_interest.is_writable() {
455                let protocol_result = self.writable();
456                if protocol_result != SessionResult::Continue {
457                    return protocol_result;
458                }
459            }
460
461            if frontend_interest.is_error() {
462                error!("{} Front socket error, disconnecting", log_context!(self));
463                self.frontend_readiness.interest = Ready::EMPTY;
464                return SessionResult::Close;
465            }
466
467            counter += 1;
468        }
469
470        if counter >= MAX_LOOP_ITERATIONS {
471            error!(
472                "{}\tHandling session went through {} iterations, there's a probable infinite loop bug, closing the connection",
473                log_context!(self),
474                MAX_LOOP_ITERATIONS
475            );
476
477            incr!(names::http::INFINITE_LOOP_ERROR);
478            self.print_state("HTTPS");
479
480            return SessionResult::Close;
481        }
482
483        SessionResult::Continue
484    }
485
486    fn update_readiness(&mut self, token: Token, events: Ready) {
487        if self.frontend_token == token {
488            self.frontend_readiness.event |= events;
489        }
490    }
491
492    fn timeout(&mut self, token: Token, _metrics: &mut SessionMetrics) -> StateResult {
493        // relevant timeout is still stored in the Session as front_timeout.
494        if self.frontend_token == token {
495            self.container_frontend_timeout.triggered();
496            return StateResult::CloseSession;
497        }
498
499        error!(
500            "{}, Expect state: got timeout for an invalid token: {:?}",
501            log_context!(self),
502            token
503        );
504        StateResult::CloseSession
505    }
506
507    fn cancel_timeouts(&mut self) {
508        self.container_frontend_timeout.cancel();
509    }
510
511    fn print_state(&self, context: &str) {
512        error!(
513            "{} Session(Handshake)\n\tFrontend:\n\t\ttoken: {:?}\treadiness: {:?}",
514            context, self.frontend_token, self.frontend_readiness
515        );
516    }
517}
518
519// -----------------------------------------------------------------------------
520// Unit tests
521
522#[cfg(test)]
523mod tests {
524    use std::collections::HashSet;
525
526    use rustls::{
527        AlertDescription, CertificateError, ContentType, Error as RustlsError, HandshakeType,
528        InvalidMessage, PeerIncompatible, PeerMisbehaved,
529    };
530
531    use super::handshake_failure_reason;
532
533    /// Every rustls error variant the proxy can observe must map to a distinct,
534    /// compile-time literal `tls.handshake.failed.<reason>` key. Unknown
535    /// variants (future rustls additions, `General`, `Other`, CRL errors, etc.)
536    /// collapse to `tls.handshake.failed.other` so statsd cardinality stays
537    /// bounded. This test also guards against accidental duplicate keys.
538    #[test]
539    fn handshake_failure_reason_maps_every_variant_to_unique_namespaced_key() {
540        let cases: &[(RustlsError, &str)] = &[
541            (
542                RustlsError::AlertReceived(AlertDescription::HandshakeFailure),
543                "tls.handshake.failed.alert_received",
544            ),
545            (
546                RustlsError::PeerIncompatible(PeerIncompatible::NoCipherSuitesInCommon),
547                "tls.handshake.failed.peer_incompatible",
548            ),
549            (
550                RustlsError::PeerMisbehaved(PeerMisbehaved::IllegalMiddleboxChangeCipherSpec),
551                "tls.handshake.failed.peer_misbehaved",
552            ),
553            (
554                RustlsError::InvalidMessage(InvalidMessage::InvalidContentType),
555                "tls.handshake.failed.invalid_message",
556            ),
557            (
558                RustlsError::InappropriateMessage {
559                    expect_types: vec![ContentType::Handshake],
560                    got_type: ContentType::ApplicationData,
561                },
562                "tls.handshake.failed.inappropriate_message",
563            ),
564            (
565                RustlsError::InappropriateHandshakeMessage {
566                    expect_types: vec![HandshakeType::ClientHello],
567                    got_type: HandshakeType::Finished,
568                },
569                "tls.handshake.failed.inappropriate_handshake_message",
570            ),
571            (
572                RustlsError::PeerSentOversizedRecord,
573                "tls.handshake.failed.oversized_record",
574            ),
575            (
576                RustlsError::NoApplicationProtocol,
577                "tls.handshake.failed.no_alpn",
578            ),
579            (
580                RustlsError::InvalidCertificate(CertificateError::Expired),
581                "tls.handshake.failed.invalid_certificate",
582            ),
583            (
584                RustlsError::DecryptError,
585                "tls.handshake.failed.decrypt_error",
586            ),
587            (
588                RustlsError::NoCertificatesPresented,
589                "tls.handshake.failed.no_certificates_present",
590            ),
591            // `Other` bucket — any variant not in the explicit list collapses here.
592            (
593                RustlsError::General("test".to_owned()),
594                "tls.handshake.failed.other",
595            ),
596            (RustlsError::EncryptError, "tls.handshake.failed.other"),
597            (
598                RustlsError::FailedToGetCurrentTime,
599                "tls.handshake.failed.other",
600            ),
601            (
602                RustlsError::HandshakeNotComplete,
603                "tls.handshake.failed.other",
604            ),
605        ];
606
607        let mut seen = HashSet::new();
608        for (err, expected) in cases {
609            let got = handshake_failure_reason(err);
610            assert_eq!(got, *expected, "variant {err:?} → {got}, want {expected}");
611            assert!(
612                got.starts_with("tls.handshake.failed."),
613                "reason {got} missing tls.handshake.failed. namespace"
614            );
615            seen.insert(got);
616        }
617
618        // 11 explicit buckets + 1 shared `other` bucket = 12 distinct keys.
619        assert_eq!(seen.len(), 12, "unexpected key set: {seen:?}");
620    }
621}