Skip to main content

openmoat_proxy/
sni.rs

1//! The server name (SNI) a TLS client sends in its `ClientHello` (RFC 8446 §4.1.2,
2//! RFC 6066 §3).
3//!
4//! The proxy reads the `ClientHello` a tunnel starts with, compares its SNI with
5//! the CONNECT host and only then forwards it, unchanged. Nothing is decrypted.
6//! A `ClientHello` may be split over several TLS records; they are reassembled up
7//! to [`MAX_HELLO`] bytes.
8
9use crate::request::normalise_name;
10
11/// Largest `ClientHello` handshake message accepted. Real ones are 0.5–2 KiB,
12/// more with post-quantum key shares; 64 KiB leaves room without letting a
13/// client make the proxy buffer without bound.
14pub const MAX_HELLO: usize = 64 * 1024;
15
16const RECORD_HEADER: usize = 5;
17const MAX_RECORD: usize = 16 * 1024 + 2048;
18const CONTENT_HANDSHAKE: u8 = 22;
19const HANDSHAKE_CLIENT_HELLO: u8 = 1;
20const EXT_SERVER_NAME: u16 = 0;
21const NAME_TYPE_HOST: u8 = 0;
22/// `encrypted_client_hello` (RFC 9849): the real server name travels in an
23/// encrypted inner `ClientHello`, the cleartext one may name a fronting host.
24const EXT_ECH: u16 = 0xfe0d;
25/// `encrypted_server_name`, the ECH drafts' predecessor (draft-ietf-tls-esni-02).
26const EXT_ESNI: u16 = 0xffce;
27
28/// What the start of a tunnel says about the server it wants.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum Hello {
31    /// More bytes are needed.
32    Incomplete,
33    /// A complete `ClientHello`; `None` when it carries no SNI.
34    ServerName(Option<String>),
35    /// A complete `ClientHello` that carries an encrypted server name (ECH or
36    /// ESNI), with its cleartext SNI. The name the server acts on may be another.
37    Encrypted(Option<String>),
38}
39
40/// Why the start of a tunnel is not a `ClientHello` the proxy can check.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
42#[non_exhaustive]
43pub enum HelloError {
44    /// The bytes are not a TLS handshake record.
45    #[error("the tunnel does not start with a TLS handshake")]
46    NotTls,
47    /// A TLS handshake that is not a well-formed `ClientHello`.
48    #[error("malformed TLS ClientHello")]
49    Malformed,
50    /// Larger than [`MAX_HELLO`].
51    #[error("TLS ClientHello larger than {MAX_HELLO} bytes")]
52    TooLarge,
53}
54
55/// Read the SNI from the TLS records at the start of `buf`.
56pub fn server_name(buf: &[u8]) -> Result<Hello, HelloError> {
57    let mut handshake = Vec::new();
58    let mut pos = 0;
59    loop {
60        let Some(&[content, major, _, len_hi, len_lo]) = buf.get(pos..pos + RECORD_HEADER) else {
61            return Ok(Hello::Incomplete);
62        };
63        if content != CONTENT_HANDSHAKE || major != 3 {
64            return Err(HelloError::NotTls);
65        }
66        let len = usize::from(u16::from_be_bytes([len_hi, len_lo]));
67        if len == 0 || len > MAX_RECORD {
68            return Err(HelloError::Malformed);
69        }
70        let start = pos + RECORD_HEADER;
71        let Some(fragment) = buf.get(start..start + len) else {
72            return Ok(Hello::Incomplete);
73        };
74        handshake.extend_from_slice(fragment);
75        pos = start + len;
76
77        if let Some(&[kind, len0, len1, len2]) = handshake.get(..4) {
78            if kind != HANDSHAKE_CLIENT_HELLO {
79                return Err(HelloError::Malformed);
80            }
81            let body_len = usize::from(len0) << 16 | usize::from(len1) << 8 | usize::from(len2);
82            if body_len > MAX_HELLO {
83                return Err(HelloError::TooLarge);
84            }
85            if let Some(body) = handshake.get(4..4 + body_len) {
86                return client_hello_sni(body).map(|(name, encrypted)| {
87                    if encrypted {
88                        Hello::Encrypted(name)
89                    } else {
90                        Hello::ServerName(name)
91                    }
92                });
93            }
94        }
95    }
96}
97
98/// The cleartext SNI, and whether an ECH or ESNI extension is present.
99fn client_hello_sni(body: &[u8]) -> Result<(Option<String>, bool), HelloError> {
100    let mut r = Reader(body);
101    r.take(2 + 32)?; // legacy_version, random
102    let session_id_len = r.u8()?;
103    r.take(session_id_len.into())?;
104    let suites_len = r.u16()?;
105    r.take(suites_len.into())?;
106    let compression_len = r.u8()?;
107    r.take(compression_len.into())?;
108    if r.0.is_empty() {
109        return Ok((None, false));
110    }
111    let ext_len = r.u16()?;
112    let mut exts = Reader(r.take(ext_len.into())?);
113    r.end()?;
114    let mut name = None;
115    let mut encrypted = false;
116    while !exts.0.is_empty() {
117        let kind = exts.u16()?;
118        let len = exts.u16()?;
119        let data = exts.take(len.into())?;
120        if kind == EXT_SERVER_NAME {
121            if name.is_some() {
122                return Err(HelloError::Malformed);
123            }
124            name = Some(server_name_list(data)?);
125        }
126        encrypted |= kind == EXT_ECH || kind == EXT_ESNI;
127    }
128    Ok((name.flatten(), encrypted))
129}
130
131/// `ServerNameList`: at most one `host_name` entry (RFC 6066 §3).
132fn server_name_list(data: &[u8]) -> Result<Option<String>, HelloError> {
133    let mut r = Reader(data);
134    let list_len = r.u16()?;
135    let mut list = Reader(r.take(list_len.into())?);
136    r.end()?;
137    let mut host = None;
138    while !list.0.is_empty() {
139        let kind = list.u8()?;
140        let len = list.u16()?;
141        let value = list.take(len.into())?;
142        if kind != NAME_TYPE_HOST {
143            continue;
144        }
145        if host.is_some() {
146            return Err(HelloError::Malformed);
147        }
148        // The CONNECT host went through the same normalisation, so a name that
149        // is not a valid DNS name (empty, `.`, non-ASCII) could never match it.
150        let name = std::str::from_utf8(value)
151            .ok()
152            .and_then(normalise_name)
153            .ok_or(HelloError::Malformed)?;
154        host = Some(name);
155    }
156    Ok(host)
157}
158
159/// Bounds-checked big-endian reads; every short read is [`HelloError::Malformed`].
160struct Reader<'a>(&'a [u8]);
161
162impl<'a> Reader<'a> {
163    fn take(&mut self, n: usize) -> Result<&'a [u8], HelloError> {
164        let (head, rest) = self.0.split_at_checked(n).ok_or(HelloError::Malformed)?;
165        self.0 = rest;
166        Ok(head)
167    }
168
169    fn array<const N: usize>(&mut self) -> Result<[u8; N], HelloError> {
170        let (head, rest) = self.0.split_first_chunk().ok_or(HelloError::Malformed)?;
171        self.0 = rest;
172        Ok(*head)
173    }
174
175    fn u8(&mut self) -> Result<u8, HelloError> {
176        let [b] = self.array()?;
177        Ok(b)
178    }
179
180    fn u16(&mut self) -> Result<u16, HelloError> {
181        self.array().map(u16::from_be_bytes)
182    }
183
184    fn end(&self) -> Result<(), HelloError> {
185        if self.0.is_empty() {
186            Ok(())
187        } else {
188            Err(HelloError::Malformed)
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests;