Skip to main content

sozu_lib/protocol/
h2c.rs

1//! Cleartext HTTP/2 prior-knowledge detection (sozu-proxy/sozu#1911).
2//!
3//! An HTTP listener with `h2c_prior_knowledge = true` does not know, at
4//! accept time, whether the client speaks HTTP/1.x or HTTP/2: over cleartext
5//! there is no ALPN. RFC 9113 §3.3 lets a client that knows the server speaks
6//! HTTP/2 open the connection with the 24-byte preface
7//! `PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n` directly. [`H2cPrefaceSniff`] is the
8//! session stage that tells the two apart before any byte is consumed.
9//!
10//! The sniff reads with `MSG_PEEK` (`TcpStream::peek`), never with `recv`:
11//! the bytes stay in the kernel receive queue, so the mux connection built
12//! next reads them itself — `Connection::new_h2_server`
13//! (`lib/src/protocol/mux/connection.rs`) parses and validates the preface
14//! exactly as it does after a TLS ALPN `h2`, and `Connection::new_h1_server`
15//! parses the HTTP/1.x request from its first byte. Nothing is copied, so no
16//! buffer hand-off between the stages exists to get wrong.
17//!
18//! Decision rule ([`classify_preface`]): the peeked bytes are compared with
19//! the preface prefix of the same length. A divergence at any byte means
20//! HTTP/1.x; the whole preface means HTTP/2; a strict prefix means "wait for
21//! more". Waiting is bounded by the session's request timeout, armed at
22//! accept and carried into the mux unchanged, exactly as for the PROXY
23//! protocol stage (`ExpectProxyProtocol`,
24//! `lib/src/protocol/proxy_protocol/expect.rs`).
25//!
26//! Edge-triggered readiness: a peek drains nothing, but the kernel raises a
27//! fresh EPOLLIN edge for every segment that arrives, so parking on a partial
28//! prefix with READABLE cleared is safe. The reverse also holds: once the
29//! sniff decides, the bytes it peeked raise no new edge, so the session must
30//! hand READABLE to the mux frontend itself (`HttpSession::upgrade_sniff`,
31//! `lib/src/http.rs`).
32
33use std::{cell::RefCell, io::ErrorKind, net::SocketAddr, rc::Rc};
34
35use mio::{Token, net::TcpStream};
36use rusty_ulid::Ulid;
37use sozu_command::{config::MAX_LOOP_ITERATIONS, logging::ansi_palette};
38
39use crate::{
40    L7Proxy, ProxySession, Readiness, SessionMetrics, StateResult,
41    metrics::names,
42    protocol::{SessionResult, SessionState, mux::parser::CLIENT_PREFACE_MAGIC},
43    sozu_command::ready::Ready,
44    timer::TimeoutContainer,
45};
46
47/// Length of the HTTP/2 client connection preface magic: the most bytes the
48/// sniff ever needs to see.
49pub const H2C_PREFACE_LEN: usize = CLIENT_PREFACE_MAGIC.len();
50
51/// Per-session prefix for log lines emitted with an [`H2cPrefaceSniff`] in
52/// scope. Renders the canonical `HTTP-H2C\tSession(...)\t >>>` envelope keyed
53/// by the frontend token, so the lines correlate with the `HTTP` and `MUX-*`
54/// lines of the same connection.
55macro_rules! log_context {
56    ($self:expr) => {{
57        let (open, reset, grey, gray, white) = ansi_palette();
58        format!(
59            "{open}HTTP-H2C{reset}\t{grey}Session{reset}({gray}frontend{reset}={white}{frontend}{reset}, {gray}readiness{reset}={white}{readiness}{reset})\t >>>",
60            open = open,
61            reset = reset,
62            grey = grey,
63            gray = gray,
64            white = white,
65            frontend = $self.frontend_token.0,
66            readiness = $self.frontend_readiness,
67        )
68    }};
69}
70
71/// What the first bytes of a cleartext connection say about its protocol.
72#[derive(Clone, Copy, Debug, PartialEq, Eq)]
73pub enum PrefaceVerdict {
74    /// The full 24-byte HTTP/2 preface: serve the connection as HTTP/2.
75    H2,
76    /// A byte diverged from the preface: serve the connection as HTTP/1.x.
77    Http1,
78    /// Every byte so far matches the preface, but fewer than 24 arrived.
79    NeedMore,
80}
81
82/// Classify the first bytes of a cleartext connection.
83///
84/// Only the first [`H2C_PREFACE_LEN`] bytes are considered; anything after
85/// the preface (the client SETTINGS frame) is the H2 connection's business.
86/// An empty slice is [`PrefaceVerdict::NeedMore`]: end of stream is the
87/// caller's to detect, from the read that returned nothing.
88pub fn classify_preface(peeked: &[u8]) -> PrefaceVerdict {
89    let seen = peeked.len().min(H2C_PREFACE_LEN);
90    let verdict = if peeked[..seen] != CLIENT_PREFACE_MAGIC[..seen] {
91        PrefaceVerdict::Http1
92    } else if seen == H2C_PREFACE_LEN {
93        PrefaceVerdict::H2
94    } else {
95        PrefaceVerdict::NeedMore
96    };
97    // H2 exactly when the whole preface was seen and matched.
98    debug_assert_eq!(
99        verdict == PrefaceVerdict::H2,
100        seen == H2C_PREFACE_LEN && peeked[..seen] == CLIENT_PREFACE_MAGIC[..],
101        "H2 iff all {H2C_PREFACE_LEN} preface bytes were seen and matched"
102    );
103    // NeedMore never hides a divergence, and never fires on a full preface.
104    debug_assert!(
105        verdict != PrefaceVerdict::NeedMore
106            || (seen < H2C_PREFACE_LEN && peeked[..seen] == CLIENT_PREFACE_MAGIC[..seen]),
107        "NeedMore only for a strict, matching prefix of the preface"
108    );
109    verdict
110}
111
112/// The session stage of an `h2c_prior_knowledge` HTTP listener that decides
113/// between HTTP/2 and HTTP/1.x before the mux is built. See the module
114/// documentation.
115pub struct H2cPrefaceSniff {
116    pub container_frontend_timeout: TimeoutContainer,
117    pub frontend: TcpStream,
118    pub frontend_readiness: Readiness,
119    pub frontend_token: Token,
120    /// Address the session is attributed to: the `accept(2)` peer, or the
121    /// PROXY-advertised source when the listener also expects PROXY.
122    pub session_address: Option<SocketAddr>,
123    /// Address the session was received on, as the mux context records it.
124    pub public_address: SocketAddr,
125    /// Request id of the first HTTP/1.x request, minted at accept.
126    pub request_id: Ulid,
127    /// `Some(H2 | Http1)` once decided; never `Some(NeedMore)`.
128    pub verdict: Option<PrefaceVerdict>,
129}
130
131impl H2cPrefaceSniff {
132    /// Instantiate the sniff stage with interest READABLE | HUP | ERROR,
133    /// carrying `event` (the readiness already observed on the socket, EMPTY
134    /// for a freshly accepted one).
135    pub fn new(
136        container_frontend_timeout: TimeoutContainer,
137        frontend: TcpStream,
138        frontend_token: Token,
139        request_id: Ulid,
140        session_address: Option<SocketAddr>,
141        public_address: SocketAddr,
142        event: Ready,
143    ) -> Self {
144        H2cPrefaceSniff {
145            container_frontend_timeout,
146            frontend,
147            frontend_readiness: Readiness {
148                interest: Ready::READABLE | Ready::HUP | Ready::ERROR,
149                event,
150            },
151            frontend_token,
152            session_address,
153            public_address,
154            request_id,
155            verdict: None,
156        }
157    }
158
159    pub fn front_socket(&self) -> &TcpStream {
160        &self.frontend
161    }
162
163    /// Peek at the socket and try to decide. Returns `Upgrade` once decided,
164    /// `Continue` to wait for more bytes, `Close` on end of stream or error.
165    fn readable(&mut self) -> SessionResult {
166        debug_assert!(
167            self.verdict.is_none(),
168            "the sniff must not peek again after it decided"
169        );
170        let mut window = [0u8; H2C_PREFACE_LEN];
171        match self.frontend.peek(&mut window) {
172            Ok(0) => {
173                // The client closed before sending a byte: a bare TCP health
174                // check. Close now rather than at the request timeout.
175                trace!(
176                    "{} socket closed with 0 bytes, closing session",
177                    log_context!(self)
178                );
179                SessionResult::Close
180            }
181            Ok(peeked) => {
182                debug_assert!(
183                    peeked <= H2C_PREFACE_LEN,
184                    "peek cannot return more than the window it was given"
185                );
186                match classify_preface(&window[..peeked]) {
187                    PrefaceVerdict::NeedMore => {
188                        trace!(
189                            "{} {} of {} preface bytes so far, waiting for more",
190                            log_context!(self),
191                            peeked,
192                            H2C_PREFACE_LEN
193                        );
194                        // Nothing more to see until the next segment, which
195                        // raises its own edge.
196                        self.frontend_readiness.event.remove(Ready::READABLE);
197                        SessionResult::Continue
198                    }
199                    verdict => {
200                        debug!(
201                            "{} connection preface verdict: {:?}",
202                            log_context!(self),
203                            verdict
204                        );
205                        if verdict == PrefaceVerdict::H2 {
206                            incr!(names::http::H2C_PRIOR_KNOWLEDGE);
207                        }
208                        self.verdict = Some(verdict);
209                        SessionResult::Upgrade
210                    }
211                }
212            }
213            Err(e) if e.kind() == ErrorKind::WouldBlock => {
214                self.frontend_readiness.event.remove(Ready::READABLE);
215                SessionResult::Continue
216            }
217            // Retried by the next turn of the bounded `ready` loop: READABLE
218            // is still set.
219            Err(e) if e.kind() == ErrorKind::Interrupted => SessionResult::Continue,
220            Err(e) => {
221                debug!(
222                    "{} front socket peek error, closing the connection: {:?}",
223                    log_context!(self),
224                    e
225                );
226                self.frontend_readiness.reset();
227                SessionResult::Close
228            }
229        }
230    }
231}
232
233impl SessionState for H2cPrefaceSniff {
234    fn ready(
235        &mut self,
236        _session: Rc<RefCell<dyn ProxySession>>,
237        _proxy: Rc<RefCell<dyn L7Proxy>>,
238        _metrics: &mut SessionMetrics,
239    ) -> SessionResult {
240        let mut counter = 0;
241
242        while counter < MAX_LOOP_ITERATIONS {
243            let frontend_interest = self.frontend_readiness.filter_interest();
244
245            trace!("{} {:?}", log_context!(self), self.frontend_readiness);
246
247            // A client half-close arrives in the same `READABLE | HUP` event
248            // as the bytes it follows, so HUP closes only once a peek has
249            // looked at them: a complete preface or a diverging byte still
250            // upgrades and hands HUP on to the mux. `readable` clears
251            // READABLE on a strict prefix, so a client that half-closes
252            // mid-preface reaches this close on the next turn.
253            if self.frontend_readiness.event.is_hup() && !frontend_interest.is_readable() {
254                return SessionResult::Close;
255            }
256
257            if frontend_interest.is_empty() {
258                break;
259            }
260
261            if frontend_interest.is_readable() {
262                let session_result = self.readable();
263                if session_result != SessionResult::Continue {
264                    // Upgrade carries a decision, Close none.
265                    debug_assert_eq!(
266                        session_result == SessionResult::Upgrade,
267                        self.verdict.is_some(),
268                        "the sniff upgrades exactly when it has decided"
269                    );
270                    return session_result;
271                }
272            }
273
274            if frontend_interest.is_error() {
275                error!("{} front error, disconnecting", log_context!(self));
276                self.frontend_readiness.interest = Ready::EMPTY;
277                return SessionResult::Close;
278            }
279
280            counter += 1;
281        }
282
283        if counter >= MAX_LOOP_ITERATIONS {
284            error!(
285                "{} handling session went through {} iterations, there's a probable infinite loop bug, closing the connection",
286                log_context!(self),
287                MAX_LOOP_ITERATIONS
288            );
289            incr!(names::http::INFINITE_LOOP_ERROR);
290            self.print_state("");
291            return SessionResult::Close;
292        }
293
294        debug_assert!(
295            self.verdict.is_none(),
296            "an undecided sniff is the only one that keeps waiting"
297        );
298        SessionResult::Continue
299    }
300
301    fn update_readiness(&mut self, token: Token, events: Ready) {
302        if self.frontend_token == token {
303            self.frontend_readiness.event |= events;
304        }
305    }
306
307    fn timeout(&mut self, token: Token, _metrics: &mut SessionMetrics) -> StateResult {
308        if self.frontend_token == token {
309            debug!(
310                "{} request timeout before the connection preface was decided",
311                log_context!(self)
312            );
313            self.container_frontend_timeout.triggered();
314            return StateResult::CloseSession;
315        }
316
317        error!(
318            "{} got timeout for an invalid token: {:?}",
319            log_context!(self),
320            token
321        );
322        StateResult::CloseSession
323    }
324
325    fn cancel_timeouts(&mut self) {
326        self.container_frontend_timeout.cancel();
327    }
328
329    fn print_state(&self, context: &str) {
330        error!(
331            "{} {} Session(H2cPrefaceSniff)\n\tFrontend:\n\t\ttoken: {:?}\treadiness: {:?}\tverdict: {:?}",
332            log_context!(self),
333            context,
334            self.frontend_token,
335            self.frontend_readiness,
336            self.verdict
337        );
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use std::{
344        io::Write,
345        net::{SocketAddr, TcpStream as StdTcpStream},
346        time::{Duration, Instant},
347    };
348
349    use mio::net::TcpListener;
350
351    use super::*;
352
353    #[test]
354    fn full_preface_is_h2() {
355        assert_eq!(
356            classify_preface(&CLIENT_PREFACE_MAGIC[..]),
357            PrefaceVerdict::H2
358        );
359    }
360
361    #[test]
362    fn preface_followed_by_settings_is_h2() {
363        let mut wire = CLIENT_PREFACE_MAGIC.to_vec();
364        // An empty SETTINGS frame header: what every client sends next.
365        wire.extend_from_slice(&[0, 0, 0, 4, 0, 0, 0, 0, 0]);
366        assert_eq!(classify_preface(&wire), PrefaceVerdict::H2);
367    }
368
369    #[test]
370    fn every_strict_prefix_needs_more() {
371        for len in 0..H2C_PREFACE_LEN {
372            assert_eq!(
373                classify_preface(&CLIENT_PREFACE_MAGIC[..len]),
374                PrefaceVerdict::NeedMore,
375                "a {len}-byte prefix of the preface must wait for more"
376            );
377        }
378    }
379
380    #[test]
381    fn a_divergence_at_any_byte_is_http1() {
382        for at in 0..H2C_PREFACE_LEN {
383            let mut wire = CLIENT_PREFACE_MAGIC.to_vec();
384            wire[at] ^= 0x20;
385            // The divergence decides as soon as it is seen, short or full.
386            for len in (at + 1)..=H2C_PREFACE_LEN {
387                assert_eq!(
388                    classify_preface(&wire[..len]),
389                    PrefaceVerdict::Http1,
390                    "a divergence at byte {at} must decide HTTP/1.x after {len} bytes"
391                );
392            }
393        }
394    }
395
396    #[test]
397    fn http1_requests_are_http1() {
398        for request in [
399            &b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"[..],
400            b"POST /upload HTTP/1.1\r\n",
401            b"PUT",
402            b"PRI * HTTP/1.1\r\n\r\n",
403            b"PRI / HTTP/2.0\r\n\r\nSM\r\n\r\n",
404        ] {
405            assert_eq!(
406                classify_preface(request),
407                PrefaceVerdict::Http1,
408                "{:?} must be served as HTTP/1.x",
409                String::from_utf8_lossy(request)
410            );
411        }
412    }
413
414    /// Loopback pair: the accepted, non-blocking mio side and the client.
415    fn connected_pair() -> (TcpStream, StdTcpStream) {
416        let listener = TcpListener::bind("127.0.0.1:0".parse::<SocketAddr>().unwrap())
417            .expect("bind loopback listener");
418        let client = StdTcpStream::connect(listener.local_addr().unwrap())
419            .expect("connect to loopback listener");
420        let deadline = Instant::now() + Duration::from_secs(5);
421        loop {
422            match listener.accept() {
423                Ok((server, _)) => return (server, client),
424                Err(e) if e.kind() == ErrorKind::WouldBlock && Instant::now() < deadline => {
425                    std::thread::sleep(Duration::from_millis(5));
426                }
427                Err(e) => panic!("accept failed: {e}"),
428            }
429        }
430    }
431
432    fn sniff_over(frontend: TcpStream) -> H2cPrefaceSniff {
433        H2cPrefaceSniff::new(
434            TimeoutContainer::new_empty(Duration::from_secs(10)),
435            frontend,
436            Token(1),
437            Ulid::generate(),
438            None,
439            "127.0.0.1:80".parse().unwrap(),
440            Ready::READABLE,
441        )
442    }
443
444    /// Peek until the kernel shows at least `want` bytes, then classify once.
445    fn readable_after(sniff: &mut H2cPrefaceSniff, want: usize) -> SessionResult {
446        let deadline = Instant::now() + Duration::from_secs(5);
447        let mut probe = [0u8; H2C_PREFACE_LEN];
448        while sniff.frontend.peek(&mut probe).unwrap_or(0) < want {
449            assert!(Instant::now() < deadline, "bytes never arrived");
450            std::thread::sleep(Duration::from_millis(5));
451        }
452        sniff.frontend_readiness.event.insert(Ready::READABLE);
453        sniff.readable()
454    }
455
456    /// The sniff decides on a split preface without consuming a byte: the
457    /// mux built next must read the preface itself.
458    #[test]
459    fn a_split_preface_waits_then_decides_h2_without_consuming() {
460        let (frontend, mut client) = connected_pair();
461        let mut sniff = sniff_over(frontend);
462
463        client.write_all(&CLIENT_PREFACE_MAGIC[..10]).unwrap();
464        assert_eq!(readable_after(&mut sniff, 10), SessionResult::Continue);
465        assert_eq!(sniff.verdict, None);
466        assert!(
467            !sniff.frontend_readiness.event.is_readable(),
468            "a strict prefix must park READABLE until the next edge"
469        );
470
471        client.write_all(&CLIENT_PREFACE_MAGIC[10..]).unwrap();
472        assert_eq!(
473            readable_after(&mut sniff, H2C_PREFACE_LEN),
474            SessionResult::Upgrade
475        );
476        assert_eq!(sniff.verdict, Some(PrefaceVerdict::H2));
477
478        let mut left = [0u8; H2C_PREFACE_LEN];
479        assert_eq!(
480            sniff.frontend.peek(&mut left).unwrap(),
481            H2C_PREFACE_LEN,
482            "the sniff must leave every preface byte in the socket"
483        );
484        assert_eq!(&left, CLIENT_PREFACE_MAGIC);
485    }
486
487    #[test]
488    fn an_http1_request_decides_http1_without_consuming() {
489        let (frontend, mut client) = connected_pair();
490        let mut sniff = sniff_over(frontend);
491
492        let request = b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n";
493        client.write_all(request).unwrap();
494        assert_eq!(readable_after(&mut sniff, 1), SessionResult::Upgrade);
495        assert_eq!(sniff.verdict, Some(PrefaceVerdict::Http1));
496
497        let mut left = vec![0u8; request.len()];
498        let deadline = Instant::now() + Duration::from_secs(5);
499        while sniff.frontend.peek(&mut left).unwrap() < request.len() {
500            assert!(Instant::now() < deadline, "request bytes never arrived");
501            std::thread::sleep(Duration::from_millis(5));
502        }
503        assert_eq!(&left[..], &request[..]);
504    }
505
506    #[test]
507    fn a_close_before_any_byte_closes_the_session() {
508        let (frontend, client) = connected_pair();
509        let mut sniff = sniff_over(frontend);
510        drop(client);
511
512        let deadline = Instant::now() + Duration::from_secs(5);
513        let result = loop {
514            sniff.frontend_readiness.event.insert(Ready::READABLE);
515            match sniff.readable() {
516                SessionResult::Continue if Instant::now() < deadline => {
517                    std::thread::sleep(Duration::from_millis(5));
518                }
519                other => break other,
520            }
521        };
522        assert_eq!(result, SessionResult::Close);
523        assert_eq!(sniff.verdict, None);
524    }
525}