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
23/// What the start of a tunnel says about the server it wants.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum Hello {
26    /// More bytes are needed.
27    Incomplete,
28    /// A complete `ClientHello`; `None` when it carries no SNI.
29    ServerName(Option<String>),
30}
31
32/// Why the start of a tunnel is not a `ClientHello` the proxy can check.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
34pub enum HelloError {
35    /// The bytes are not a TLS handshake record.
36    #[error("the tunnel does not start with a TLS handshake")]
37    NotTls,
38    /// A TLS handshake that is not a well-formed `ClientHello`.
39    #[error("malformed TLS ClientHello")]
40    Malformed,
41    /// Larger than [`MAX_HELLO`].
42    #[error("TLS ClientHello larger than {MAX_HELLO} bytes")]
43    TooLarge,
44}
45
46/// Read the SNI from the TLS records at the start of `buf`.
47pub fn server_name(buf: &[u8]) -> Result<Hello, HelloError> {
48    let mut handshake = Vec::new();
49    let mut pos = 0;
50    loop {
51        let Some(&[content, major, _, len_hi, len_lo]) = buf.get(pos..pos + RECORD_HEADER) else {
52            return Ok(Hello::Incomplete);
53        };
54        if content != CONTENT_HANDSHAKE || major != 3 {
55            return Err(HelloError::NotTls);
56        }
57        let len = usize::from(u16::from_be_bytes([len_hi, len_lo]));
58        if len == 0 || len > MAX_RECORD {
59            return Err(HelloError::Malformed);
60        }
61        let start = pos + RECORD_HEADER;
62        let Some(fragment) = buf.get(start..start + len) else {
63            return Ok(Hello::Incomplete);
64        };
65        handshake.extend_from_slice(fragment);
66        pos = start + len;
67
68        if let Some(&[kind, len0, len1, len2]) = handshake.get(..4) {
69            if kind != HANDSHAKE_CLIENT_HELLO {
70                return Err(HelloError::Malformed);
71            }
72            let body_len = usize::from(len0) << 16 | usize::from(len1) << 8 | usize::from(len2);
73            if body_len > MAX_HELLO {
74                return Err(HelloError::TooLarge);
75            }
76            if let Some(body) = handshake.get(4..4 + body_len) {
77                return client_hello_sni(body).map(Hello::ServerName);
78            }
79        }
80    }
81}
82
83fn client_hello_sni(body: &[u8]) -> Result<Option<String>, HelloError> {
84    let mut r = Reader(body);
85    r.take(2 + 32)?; // legacy_version, random
86    let session_id_len = r.u8()?;
87    r.take(session_id_len.into())?;
88    let suites_len = r.u16()?;
89    r.take(suites_len.into())?;
90    let compression_len = r.u8()?;
91    r.take(compression_len.into())?;
92    if r.0.is_empty() {
93        return Ok(None);
94    }
95    let ext_len = r.u16()?;
96    let mut exts = Reader(r.take(ext_len.into())?);
97    r.end()?;
98    let mut name = None;
99    while !exts.0.is_empty() {
100        let kind = exts.u16()?;
101        let len = exts.u16()?;
102        let data = exts.take(len.into())?;
103        if kind == EXT_SERVER_NAME {
104            if name.is_some() {
105                return Err(HelloError::Malformed);
106            }
107            name = Some(server_name_list(data)?);
108        }
109    }
110    Ok(name.flatten())
111}
112
113/// `ServerNameList`: at most one `host_name` entry (RFC 6066 §3).
114fn server_name_list(data: &[u8]) -> Result<Option<String>, HelloError> {
115    let mut r = Reader(data);
116    let list_len = r.u16()?;
117    let mut list = Reader(r.take(list_len.into())?);
118    r.end()?;
119    let mut host = None;
120    while !list.0.is_empty() {
121        let kind = list.u8()?;
122        let len = list.u16()?;
123        let value = list.take(len.into())?;
124        if kind != NAME_TYPE_HOST {
125            continue;
126        }
127        if host.is_some() {
128            return Err(HelloError::Malformed);
129        }
130        // The CONNECT host went through the same normalisation, so a name that
131        // is not a valid DNS name (empty, `.`, non-ASCII) could never match it.
132        let name = std::str::from_utf8(value)
133            .ok()
134            .and_then(normalise_name)
135            .ok_or(HelloError::Malformed)?;
136        host = Some(name);
137    }
138    Ok(host)
139}
140
141/// Bounds-checked big-endian reads; every short read is [`HelloError::Malformed`].
142struct Reader<'a>(&'a [u8]);
143
144impl<'a> Reader<'a> {
145    fn take(&mut self, n: usize) -> Result<&'a [u8], HelloError> {
146        let (head, rest) = self.0.split_at_checked(n).ok_or(HelloError::Malformed)?;
147        self.0 = rest;
148        Ok(head)
149    }
150
151    fn array<const N: usize>(&mut self) -> Result<[u8; N], HelloError> {
152        let (head, rest) = self.0.split_first_chunk().ok_or(HelloError::Malformed)?;
153        self.0 = rest;
154        Ok(*head)
155    }
156
157    fn u8(&mut self) -> Result<u8, HelloError> {
158        let [b] = self.array()?;
159        Ok(b)
160    }
161
162    fn u16(&mut self) -> Result<u16, HelloError> {
163        self.array().map(u16::from_be_bytes)
164    }
165
166    fn end(&self) -> Result<(), HelloError> {
167        if self.0.is_empty() {
168            Ok(())
169        } else {
170            Err(HelloError::Malformed)
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests;