1const CONTENT_TYPE_HANDSHAKE: u8 = 0x16;
16
17const HANDSHAKE_TYPE_CLIENT_HELLO: u8 = 0x01;
19
20const EXTENSION_SERVER_NAME: u16 = 0x0000;
22
23const NAME_TYPE_HOST_NAME: u8 = 0x00;
25
26#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum SniPeek {
29 Found(String),
32 Absent,
36 Incomplete,
39 NotTls,
42}
43
44struct Reader<'a> {
48 buf: &'a [u8],
49 pos: usize,
50}
51
52impl<'a> Reader<'a> {
53 fn new(buf: &'a [u8]) -> Self {
54 Self { buf, pos: 0 }
55 }
56
57 fn remaining(&self) -> usize {
58 self.buf.len().saturating_sub(self.pos)
59 }
60
61 fn u8(&mut self) -> Option<u8> {
62 let b = *self.buf.get(self.pos)?;
63 self.pos += 1;
64 Some(b)
65 }
66
67 fn u16(&mut self) -> Option<u16> {
68 let hi = self.u8()? as u16;
69 let lo = self.u8()? as u16;
70 Some((hi << 8) | lo)
71 }
72
73 fn u24(&mut self) -> Option<u32> {
74 let a = self.u8()? as u32;
75 let b = self.u8()? as u32;
76 let c = self.u8()? as u32;
77 Some((a << 16) | (b << 8) | c)
78 }
79
80 fn bytes(&mut self, n: usize) -> Option<&'a [u8]> {
81 let end = self.pos.checked_add(n)?;
82 let slice = self.buf.get(self.pos..end)?;
83 self.pos = end;
84 Some(slice)
85 }
86
87 fn skip_prefixed(&mut self, len_bytes: usize) -> Option<()> {
89 let len = match len_bytes {
90 1 => self.u8()? as usize,
91 2 => self.u16()? as usize,
92 _ => return None,
93 };
94 self.bytes(len).map(|_| ())
95 }
96}
97
98pub fn parse_sni(buf: &[u8]) -> SniPeek {
111 let mut records = Reader::new(buf);
112 let mut handshake: Vec<u8> = Vec::new();
113
114 loop {
115 if records.remaining() == 0 {
116 break;
117 }
118 if records.remaining() < 5 {
120 if !handshake.is_empty() {
125 break;
126 }
127 return match records.u8() {
128 Some(CONTENT_TYPE_HANDSHAKE) => SniPeek::Incomplete,
129 Some(_) => SniPeek::NotTls,
130 None => SniPeek::Incomplete,
131 };
132 }
133 let Some(content_type) = records.u8() else {
134 return SniPeek::Incomplete;
135 };
136 if content_type != CONTENT_TYPE_HANDSHAKE {
137 if !handshake.is_empty() {
141 break;
142 }
143 return SniPeek::NotTls;
144 }
145 if records.u16().is_none() {
148 return SniPeek::Incomplete;
149 }
150 let Some(len) = records.u16() else {
151 return SniPeek::Incomplete;
152 };
153 match records.bytes(len as usize) {
154 Some(payload) => handshake.extend_from_slice(payload),
155 None => break,
157 }
158 }
159
160 parse_handshake(&handshake)
161}
162
163fn parse_handshake(handshake: &[u8]) -> SniPeek {
165 let mut r = Reader::new(handshake);
166
167 match r.u8() {
168 Some(HANDSHAKE_TYPE_CLIENT_HELLO) => {}
169 Some(_) => return SniPeek::NotTls,
173 None => return SniPeek::Incomplete,
174 }
175
176 let Some(body_len) = r.u24() else {
177 return SniPeek::Incomplete;
178 };
179 if r.remaining() < body_len as usize {
180 return SniPeek::Incomplete;
181 }
182
183 if r.bytes(34).is_none() {
185 return SniPeek::Incomplete;
186 }
187 if r.skip_prefixed(1).is_none() || r.skip_prefixed(2).is_none() || r.skip_prefixed(1).is_none()
189 {
190 return SniPeek::Incomplete;
191 }
192
193 if r.remaining() == 0 {
195 return SniPeek::Absent;
196 }
197 let Some(ext_total) = r.u16() else {
198 return SniPeek::Incomplete;
199 };
200 let Some(ext_bytes) = r.bytes(ext_total as usize) else {
201 return SniPeek::Incomplete;
202 };
203
204 let mut ext = Reader::new(ext_bytes);
205 while ext.remaining() > 0 {
206 let (Some(ext_type), Some(ext_len)) = (ext.u16(), ext.u16()) else {
207 return SniPeek::Incomplete;
208 };
209 let Some(body) = ext.bytes(ext_len as usize) else {
210 return SniPeek::Incomplete;
211 };
212 if ext_type == EXTENSION_SERVER_NAME {
213 return parse_server_name_list(body);
214 }
215 }
216
217 SniPeek::Absent
218}
219
220fn parse_server_name_list(body: &[u8]) -> SniPeek {
222 let mut r = Reader::new(body);
223 let Some(list_len) = r.u16() else {
224 return SniPeek::Incomplete;
225 };
226 let Some(list) = r.bytes(list_len as usize) else {
227 return SniPeek::Incomplete;
228 };
229
230 let mut names = Reader::new(list);
231 while names.remaining() > 0 {
232 let (Some(name_type), Some(name_len)) = (names.u8(), names.u16()) else {
233 return SniPeek::Incomplete;
234 };
235 let Some(name) = names.bytes(name_len as usize) else {
236 return SniPeek::Incomplete;
237 };
238 if name_type != NAME_TYPE_HOST_NAME {
239 continue;
240 }
241 return match std::str::from_utf8(name) {
247 Ok(host) if is_routable_host_name(host) => {
248 SniPeek::Found(host.trim_end_matches('.').to_ascii_lowercase())
249 }
250 _ => SniPeek::Absent,
251 };
252 }
253
254 SniPeek::Absent
255}
256
257fn is_routable_host_name(host: &str) -> bool {
265 !host.is_empty()
266 && host.len() <= 253
267 && host
268 .bytes()
269 .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'.' | b'_'))
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 fn server_name_ext(host: &str) -> Vec<u8> {
278 let mut entry = vec![NAME_TYPE_HOST_NAME];
279 entry.extend_from_slice(&(host.len() as u16).to_be_bytes());
280 entry.extend_from_slice(host.as_bytes());
281
282 let mut body = (entry.len() as u16).to_be_bytes().to_vec();
283 body.extend_from_slice(&entry);
284 body
285 }
286
287 fn extension(ext_type: u16, body: &[u8]) -> Vec<u8> {
289 let mut out = ext_type.to_be_bytes().to_vec();
290 out.extend_from_slice(&(body.len() as u16).to_be_bytes());
291 out.extend_from_slice(body);
292 out
293 }
294
295 fn client_hello(extensions: Vec<Vec<u8>>) -> Vec<u8> {
297 let mut body = Vec::new();
298 body.extend_from_slice(&[0x03, 0x03]); body.extend_from_slice(&[0x11; 32]); body.push(0); body.extend_from_slice(&[0x00, 0x02, 0x13, 0x01]); body.extend_from_slice(&[0x01, 0x00]); let ext_bytes: Vec<u8> = extensions.concat();
305 body.extend_from_slice(&(ext_bytes.len() as u16).to_be_bytes());
306 body.extend_from_slice(&ext_bytes);
307
308 let mut msg = vec![HANDSHAKE_TYPE_CLIENT_HELLO];
309 let len = body.len() as u32;
310 msg.extend_from_slice(&[(len >> 16) as u8, (len >> 8) as u8, len as u8]);
311 msg.extend_from_slice(&body);
312 msg
313 }
314
315 fn record(payload: &[u8]) -> Vec<u8> {
317 let mut out = vec![CONTENT_TYPE_HANDSHAKE, 0x03, 0x01];
318 out.extend_from_slice(&(payload.len() as u16).to_be_bytes());
319 out.extend_from_slice(payload);
320 out
321 }
322
323 fn records_of(payload: &[u8], chunk: usize) -> Vec<u8> {
326 payload.chunks(chunk).flat_map(record).collect()
327 }
328
329 #[test]
330 fn test_parse_sni_finds_hostname() {
331 let hello = client_hello(vec![extension(
332 EXTENSION_SERVER_NAME,
333 &server_name_ext("api.localhost"),
334 )]);
335 assert_eq!(
336 parse_sni(&record(&hello)),
337 SniPeek::Found("api.localhost".to_string())
338 );
339 }
340
341 #[test]
342 fn test_parse_sni_lowercases_and_strips_root_dot() {
343 let hello = client_hello(vec![extension(
346 EXTENSION_SERVER_NAME,
347 &server_name_ext("API.LocalHost."),
348 )]);
349 assert_eq!(
350 parse_sni(&record(&hello)),
351 SniPeek::Found("api.localhost".to_string())
352 );
353 }
354
355 #[test]
356 fn test_parse_sni_skips_other_extensions() {
357 let hello = client_hello(vec![
358 extension(0x002b, &[0x02, 0x03, 0x04]), extension(EXTENSION_SERVER_NAME, &server_name_ext("app.localhost")),
360 extension(0x0010, &[0x00, 0x03, 0x02, b'h', b'2']), ]);
362 assert_eq!(
363 parse_sni(&record(&hello)),
364 SniPeek::Found("app.localhost".to_string())
365 );
366 }
367
368 #[test]
369 fn test_parse_sni_absent_without_extension() {
370 let hello = client_hello(vec![extension(0x002b, &[0x02, 0x03, 0x04])]);
372 assert_eq!(parse_sni(&record(&hello)), SniPeek::Absent);
373 }
374
375 #[test]
376 fn test_parse_sni_absent_with_empty_extension_list() {
377 let hello = client_hello(vec![]);
378 assert_eq!(parse_sni(&record(&hello)), SniPeek::Absent);
379 }
380
381 #[test]
382 fn test_parse_sni_absent_when_extensions_omitted_entirely() {
383 let full = client_hello(vec![]);
385 let mut body = full[4..].to_vec();
387 body.truncate(body.len() - 2);
388 let mut msg = vec![HANDSHAKE_TYPE_CLIENT_HELLO];
389 let len = body.len() as u32;
390 msg.extend_from_slice(&[(len >> 16) as u8, (len >> 8) as u8, len as u8]);
391 msg.extend_from_slice(&body);
392 assert_eq!(parse_sni(&record(&msg)), SniPeek::Absent);
393 }
394
395 #[test]
396 fn test_parse_sni_across_fragmented_records() {
397 let hello = client_hello(vec![extension(
400 EXTENSION_SERVER_NAME,
401 &server_name_ext("split.localhost"),
402 )]);
403 for chunk in [1usize, 5, 17, 64] {
404 assert_eq!(
405 parse_sni(&records_of(&hello, chunk)),
406 SniPeek::Found("split.localhost".to_string()),
407 "fragmented into {chunk}-byte records"
408 );
409 }
410 }
411
412 #[test]
413 fn test_parse_sni_incomplete_prefixes_of_a_fragmented_hello() {
414 let hello = client_hello(vec![extension(
417 EXTENSION_SERVER_NAME,
418 &server_name_ext("split.localhost"),
419 )]);
420 let wire = records_of(&hello, 7);
421 for n in 1..wire.len() {
422 assert_eq!(
423 parse_sni(&wire[..n]),
424 SniPeek::Incomplete,
425 "prefix of {n} bytes should be incomplete"
426 );
427 }
428 assert_eq!(
429 parse_sni(&wire),
430 SniPeek::Found("split.localhost".to_string())
431 );
432 }
433
434 #[test]
435 fn test_parse_sni_incomplete_on_truncated_record() {
436 let hello = client_hello(vec![extension(
437 EXTENSION_SERVER_NAME,
438 &server_name_ext("api.localhost"),
439 )]);
440 let wire = record(&hello);
441 assert_eq!(parse_sni(&wire[..wire.len() - 10]), SniPeek::Incomplete);
442 assert_eq!(parse_sni(&[]), SniPeek::Incomplete);
443 assert_eq!(parse_sni(&[CONTENT_TYPE_HANDSHAKE]), SniPeek::Incomplete);
444 }
445
446 #[test]
451 fn test_parse_sni_finds_hostname_before_a_non_handshake_record() {
452 let hello = client_hello(vec![extension(
453 EXTENSION_SERVER_NAME,
454 &server_name_ext("api.localhost"),
455 )]);
456
457 let mut wire = record(&hello);
459 wire.extend_from_slice(&[0x14, 0x03, 0x03, 0x00, 0x01, 0x01]);
460 assert_eq!(
461 parse_sni(&wire),
462 SniPeek::Found("api.localhost".to_string())
463 );
464
465 wire.extend_from_slice(&[0x17, 0x03, 0x03, 0x00, 0x03, 0xaa, 0xbb, 0xcc]);
467 assert_eq!(
468 parse_sni(&wire),
469 SniPeek::Found("api.localhost".to_string())
470 );
471
472 let mut fragmented = records_of(&hello, 9);
474 fragmented.extend_from_slice(&[0x14, 0x03, 0x03, 0x00, 0x01, 0x01]);
475 assert_eq!(
476 parse_sni(&fragmented),
477 SniPeek::Found("api.localhost".to_string())
478 );
479 }
480
481 #[test]
484 fn test_parse_sni_ignores_a_partial_trailing_record_header() {
485 let hello = client_hello(vec![extension(
486 EXTENSION_SERVER_NAME,
487 &server_name_ext("api.localhost"),
488 )]);
489 let mut wire = record(&hello);
490 wire.extend_from_slice(&[0x14, 0x03]);
491 assert_eq!(
492 parse_sni(&wire),
493 SniPeek::Found("api.localhost".to_string())
494 );
495 }
496
497 #[test]
498 fn test_parse_sni_rejects_non_tls() {
499 assert_eq!(parse_sni(b"GET / HTTP/1.1\r\n"), SniPeek::NotTls);
502 assert_eq!(
503 parse_sni(&[0x15, 0x03, 0x01, 0x00, 0x02, 0x01, 0x00]),
504 SniPeek::NotTls
505 );
506 assert_eq!(parse_sni(b"G"), SniPeek::NotTls);
507 }
508
509 #[test]
510 fn test_parse_sni_rejects_non_client_hello_handshake() {
511 let mut msg = vec![0x02, 0x00, 0x00, 0x02, 0x03, 0x03];
513 msg = record(&msg);
514 assert_eq!(parse_sni(&msg), SniPeek::NotTls);
515 }
516
517 #[test]
518 fn test_parse_sni_absent_for_non_ascii_hostname() {
519 let hello = client_hello(vec![extension(
522 EXTENSION_SERVER_NAME,
523 &server_name_ext("café.localhost"),
524 )]);
525 assert_eq!(parse_sni(&record(&hello)), SniPeek::Absent);
526 }
527
528 #[test]
531 fn test_parse_sni_absent_for_control_characters() {
532 for host in [
533 "api.localhost\n",
534 "api\r\nlocalhost",
535 "api.localhost\u{0}",
536 "api\tlocalhost",
537 "api localhost",
538 "api.localhost\u{7f}",
539 "\u{1b}[31mapi.localhost",
540 ] {
541 let hello = client_hello(vec![extension(
542 EXTENSION_SERVER_NAME,
543 &server_name_ext(host),
544 )]);
545 assert_eq!(
546 parse_sni(&record(&hello)),
547 SniPeek::Absent,
548 "{host:?} must not be routable"
549 );
550 }
551 }
552
553 #[test]
556 fn test_parse_sni_accepts_host_name_characters() {
557 for host in [
558 "api.localhost",
559 "api-2.my_project.localhost",
560 "API.LOCALHOST",
561 ] {
562 let hello = client_hello(vec![extension(
563 EXTENSION_SERVER_NAME,
564 &server_name_ext(host),
565 )]);
566 assert_eq!(
567 parse_sni(&record(&hello)),
568 SniPeek::Found(host.to_ascii_lowercase()),
569 "{host:?} must stay routable"
570 );
571 }
572 }
573
574 #[test]
576 fn test_parse_sni_absent_for_oversized_hostname() {
577 let host = format!("{}.localhost", "a".repeat(250));
578 let hello = client_hello(vec![extension(
579 EXTENSION_SERVER_NAME,
580 &server_name_ext(&host),
581 )]);
582 assert_eq!(parse_sni(&record(&hello)), SniPeek::Absent);
583 }
584
585 #[test]
586 fn test_parse_sni_absent_for_empty_hostname() {
587 let hello = client_hello(vec![extension(EXTENSION_SERVER_NAME, &server_name_ext(""))]);
588 assert_eq!(parse_sni(&record(&hello)), SniPeek::Absent);
589 }
590
591 #[test]
592 fn test_parse_sni_skips_unknown_name_types() {
593 let mut entries = vec![0x7f, 0x00, 0x02, 0xaa, 0xbb];
596 let host = "second.localhost";
597 entries.push(NAME_TYPE_HOST_NAME);
598 entries.extend_from_slice(&(host.len() as u16).to_be_bytes());
599 entries.extend_from_slice(host.as_bytes());
600 let mut body = (entries.len() as u16).to_be_bytes().to_vec();
601 body.extend_from_slice(&entries);
602
603 let hello = client_hello(vec![extension(EXTENSION_SERVER_NAME, &body)]);
604 assert_eq!(
605 parse_sni(&record(&hello)),
606 SniPeek::Found("second.localhost".to_string())
607 );
608 }
609
610 #[test]
611 fn test_parse_sni_does_not_panic_on_arbitrary_bytes() {
612 let hello = client_hello(vec![extension(
615 EXTENSION_SERVER_NAME,
616 &server_name_ext("api.localhost"),
617 )]);
618 let wire = record(&hello);
619 for i in 0..wire.len() {
620 for corruption in [0x00u8, 0xff, 0x7f] {
621 let mut bad = wire.clone();
622 bad[i] = corruption;
623 let _ = parse_sni(&bad);
624 }
625 }
626 }
627}