1mod parser;
28pub mod shell;
29
30use std::{
31 collections::BTreeSet,
32 net::SocketAddr,
33 time::{Duration, Instant},
34};
35
36use nom::Err as NomErr;
37use sozu_command::state::ClusterId;
38
39use crate::{protocol::proxy_protocol::parser::parse_v2_header, router::pattern_trie::TrieNode};
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum AlpnMatcher {
44 Any,
48 OneOf(BTreeSet<Vec<u8>>),
51}
52
53#[derive(Debug, Clone, Copy)]
57pub struct PrereadConfig<'t> {
58 pub routes: &'t TrieNode<Vec<(AlpnMatcher, ClusterId)>>,
62 pub inbound_proxy: bool,
64 pub max_bytes: usize,
70 pub timeout: Duration,
72 pub accept_wildcard: bool,
76}
77
78#[derive(Debug)]
80pub enum Input<'a> {
81 Bytes { buf: &'a [u8], now: Instant },
84 Timeout { now: Instant },
86 FrontClosed,
88}
89
90#[derive(Debug, Clone, PartialEq)]
92pub enum Output {
93 NeedMore { deadline: Instant },
97 Routed {
102 cluster: ClusterId,
103 content_offset: usize,
104 proxy_source: Option<SocketAddr>,
105 sni: String,
106 alpn: Vec<Vec<u8>>,
107 matched_sni_pattern: String,
114 matched_alpn: AlpnMatcher,
117 },
118 Reject(RejectReason),
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum RejectReason {
125 NotTls,
127 MalformedRecord,
130 MalformedHandshake,
151 Fragmented,
153 TooLarge,
158 NoSni,
161 EchOuterAbsent,
165 SniUnmatched,
167 AlpnUnmatched,
171 ProxyHeaderInvalid,
173 FrontClosed,
175}
176
177#[derive(Debug, Default)]
183pub struct SniPrereadCore {
184 decided: Option<Output>,
189 deadline: Option<Instant>,
192}
193
194impl SniPrereadCore {
195 pub fn new() -> Self {
197 SniPrereadCore {
198 decided: None,
199 deadline: None,
200 }
201 }
202
203 pub fn handle_input(&mut self, cfg: &PrereadConfig<'_>, input: Input<'_>) -> Output {
206 #[cfg(debug_assertions)]
207 let deadline_before = self.deadline;
208 #[cfg(debug_assertions)]
209 let was_decided = self.decided.is_some();
210 self.debug_assert_invariants();
211
212 let output = match input {
213 Input::Bytes { buf, now } => self.on_bytes(cfg, buf, now),
214 Input::Timeout { now } => self.on_timeout(now),
215 Input::FrontClosed => self.on_front_closed(),
216 };
217
218 #[cfg(debug_assertions)]
222 {
223 if let Some(before) = deadline_before {
224 debug_assert_eq!(
225 self.deadline,
226 Some(before),
227 "preread deadline must be set at most once and never rewound"
228 );
229 }
230 debug_assert!(
231 !was_decided || self.decided.is_some(),
232 "a decided core must never become undecided (monotonic latch)"
233 );
234 }
235 self.debug_assert_invariants();
236
237 output
238 }
239
240 fn on_bytes(&mut self, cfg: &PrereadConfig<'_>, buf: &[u8], now: Instant) -> Output {
241 if self.deadline.is_none() {
242 self.deadline = Some(now + cfg.timeout);
243 }
244 let deadline = self
245 .deadline
246 .expect("deadline was just armed unconditionally above if it was None");
247
248 if let Some(decided) = &self.decided {
249 return decided.clone();
250 }
251
252 if now >= deadline {
264 return self.decide(Output::Reject(RejectReason::Fragmented));
265 }
266
267 let (content_offset, proxy_source) = if cfg.inbound_proxy {
277 match parse_v2_header(buf) {
278 Ok((rest, header)) => {
279 let consumed = buf.len() - rest.len();
280 debug_assert!(
281 consumed <= buf.len(),
282 "a parsed PROXY-v2 header cannot consume more than the buffer holds"
283 );
284 (consumed, header.addr.source())
285 }
286 Err(NomErr::Incomplete(_)) => {
287 return self.need_more_or_too_large(cfg, buf, deadline);
288 }
289 Err(_) => return self.decide(Output::Reject(RejectReason::ProxyHeaderInvalid)),
290 }
291 } else {
292 (0, None)
293 };
294
295 match parser::parse_client_hello(&buf[content_offset..]) {
296 parser::ParseOutcome::NeedMore => self.need_more_or_too_large(cfg, buf, deadline),
297 parser::ParseOutcome::Reject(reason) => self.decide(Output::Reject(reason)),
298 parser::ParseOutcome::ClientHello {
299 sni,
300 alpn,
301 ech_present,
302 } => self.route(
303 cfg,
304 buf,
305 content_offset,
306 proxy_source,
307 sni,
308 alpn,
309 ech_present,
310 ),
311 }
312 }
313
314 fn on_timeout(&mut self, _now: Instant) -> Output {
315 if let Some(decided) = &self.decided {
319 return decided.clone();
320 }
321 self.decide(Output::Reject(RejectReason::Fragmented))
322 }
323
324 fn on_front_closed(&mut self) -> Output {
325 if let Some(decided) = &self.decided {
326 return decided.clone();
327 }
328 self.decide(Output::Reject(RejectReason::FrontClosed))
329 }
330
331 fn need_more_or_too_large(
338 &mut self,
339 cfg: &PrereadConfig<'_>,
340 buf: &[u8],
341 deadline: Instant,
342 ) -> Output {
343 if buf.len() >= cfg.max_bytes {
344 return self.decide(Output::Reject(RejectReason::TooLarge));
345 }
346 Output::NeedMore { deadline }
347 }
348
349 #[allow(clippy::too_many_arguments)]
352 fn route(
353 &mut self,
354 cfg: &PrereadConfig<'_>,
355 buf: &[u8],
356 content_offset: usize,
357 proxy_source: Option<SocketAddr>,
358 sni_raw: Option<String>,
359 alpn: Vec<Vec<u8>>,
360 ech_present: bool,
361 ) -> Output {
362 let sni = sni_raw.map(normalize_sni);
363 let sni = match sni {
364 Some(s) if !s.is_empty() => s,
365 _ => {
366 let reason = if ech_present {
367 RejectReason::EchOuterAbsent
368 } else {
369 RejectReason::NoSni
370 };
371 return self.decide(Output::Reject(reason));
372 }
373 };
374
375 let Some((matched_key, entries)) = cfg
376 .routes
377 .domain_lookup(sni.as_bytes(), cfg.accept_wildcard)
378 else {
379 return self.decide(Output::Reject(RejectReason::SniUnmatched));
380 };
381
382 let Some((matched_alpn, cluster)) = resolve_alpn(entries, &alpn) else {
383 return self.decide(Output::Reject(RejectReason::AlpnUnmatched));
384 };
385
386 #[cfg(debug_assertions)]
393 {
394 let (_, replay_entries) = cfg
395 .routes
396 .domain_lookup(sni.as_bytes(), cfg.accept_wildcard)
397 .expect("a route that just matched must match again");
398 let replay_entry = resolve_alpn(replay_entries, &alpn);
399 debug_assert_eq!(
400 replay_entry,
401 Some((matched_alpn, cluster)),
402 "route lookup + ALPN resolution must be deterministic for the same (sni, alpn)"
403 );
404 }
405 debug_assert!(
406 content_offset <= buf.len(),
407 "content_offset must never exceed the buffer it was derived from"
408 );
409
410 self.decide(Output::Routed {
411 cluster: cluster.to_owned(),
412 content_offset,
413 proxy_source,
414 sni,
415 alpn,
416 matched_sni_pattern: String::from_utf8_lossy(matched_key).into_owned(),
420 matched_alpn: matched_alpn.clone(),
421 })
422 }
423
424 fn decide(&mut self, output: Output) -> Output {
428 debug_assert!(
429 self.decided.is_none(),
430 "decide() must be called at most once per lifetime -- the latch is monotonic"
431 );
432 debug_assert!(
433 !matches!(output, Output::NeedMore { .. }),
434 "only a terminal Output (Routed/Reject) may be latched via decide()"
435 );
436 self.decided = Some(output.clone());
437 debug!(
445 "{} latched terminal preread decision: {:?}",
446 log_context!(self),
447 output
448 );
449 output
450 }
451
452 #[cfg(debug_assertions)]
457 fn check_invariants(&self) {
458 debug_assert!(
462 !matches!(self.decided, Some(Output::NeedMore { .. })),
463 "decided latch must never store NeedMore -- it is not a terminal verdict"
464 );
465 if let Some(Output::Routed {
471 sni,
472 matched_sni_pattern,
473 ..
474 }) = &self.decided
475 {
476 debug_assert_eq!(
477 sni,
478 &sni.to_ascii_lowercase(),
479 "latched SNI must already be lowercase-normalized"
480 );
481 debug_assert!(
482 !sni.ends_with('.'),
483 "latched SNI must not carry a trailing dot after normalization"
484 );
485 debug_assert_eq!(
486 matched_sni_pattern,
487 &matched_sni_pattern.to_ascii_lowercase(),
488 "latched matched_sni_pattern must already be lowercase (trie keys are lowercased at insert)"
489 );
490 debug_assert!(
491 matched_sni_pattern == sni
492 || matched_sni_pattern
493 .strip_prefix("*.")
494 .is_some_and(|suffix| sni.ends_with(suffix)),
495 "matched_sni_pattern must be the SNI itself (exact route) or a wildcard the SNI falls under"
496 );
497 }
498 }
499
500 #[inline]
501 fn debug_assert_invariants(&self) {
502 #[cfg(debug_assertions)]
503 self.check_invariants();
504 }
505}
506
507fn resolve_alpn<'r>(
518 entries: &'r [(AlpnMatcher, ClusterId)],
519 offered: &[Vec<u8>],
520) -> Option<(&'r AlpnMatcher, &'r ClusterId)> {
521 for protocol in offered {
522 for (matcher, cluster) in entries {
523 if let AlpnMatcher::OneOf(set) = matcher
524 && set.contains(protocol)
525 {
526 return Some((matcher, cluster));
527 }
528 }
529 }
530 entries
531 .iter()
532 .find(|(matcher, _)| matches!(matcher, AlpnMatcher::Any))
533 .map(|(matcher, cluster)| (matcher, cluster))
534}
535
536fn normalize_sni(mut sni: String) -> String {
542 sni.make_ascii_lowercase();
543 if sni.ends_with('.') {
544 sni.pop();
545 }
546 sni
547}
548
549#[allow(unused_macros)]
555macro_rules! log_context {
556 ($self:expr) => {{
557 let (open, reset, grey, gray, white) = sozu_command::logging::ansi_palette();
558 format!(
559 "[- - - -]\t{open}TCP-SNI{reset}\t{grey}Preread{reset}({gray}decided{reset}={white}{decided}{reset}, {gray}deadline_armed{reset}={white}{deadline}{reset})\t >>>",
560 open = open,
561 reset = reset,
562 grey = grey,
563 gray = gray,
564 white = white,
565 decided = $self.decided.is_some(),
566 deadline = $self.deadline.is_some(),
567 )
568 }};
569}
570
571#[allow(unused_macros)]
577macro_rules! log_context_lite {
578 ($sni:expr) => {{
579 let (open, reset, grey, gray, white) = sozu_command::logging::ansi_palette();
580 format!(
581 "[- - - -]\t{open}TCP-SNI{reset}\t{grey}Route{reset}({gray}sni{reset}={white}{sni:?}{reset})\t >>>",
582 open = open,
583 reset = reset,
584 grey = grey,
585 gray = gray,
586 white = white,
587 sni = $sni,
588 )
589 }};
590}
591
592#[allow(unused_imports)]
593use {log_context, log_context_lite};
594
595#[cfg(test)]
596mod tests {
597 use std::net::{IpAddr, Ipv4Addr};
598
599 use super::*;
600 use crate::protocol::proxy_protocol::header::{Command, HeaderV2};
601
602 fn cfg<'t>(routes: &'t TrieNode<Vec<(AlpnMatcher, ClusterId)>>) -> PrereadConfig<'t> {
603 PrereadConfig {
604 routes,
605 inbound_proxy: false,
606 max_bytes: 16 * 1024,
607 timeout: Duration::from_secs(3),
608 accept_wildcard: true,
609 }
610 }
611
612 fn one_of(protocols: &[&[u8]]) -> AlpnMatcher {
613 AlpnMatcher::OneOf(protocols.iter().map(|p| p.to_vec()).collect())
614 }
615
616 fn hello(sni: &str, alpn: &[&[u8]]) -> Vec<u8> {
617 parser::build_client_hello_wire(&[
618 parser::encode_sni_extension(sni),
619 parser::encode_alpn_extension(alpn),
620 ])
621 }
622
623 fn hello_no_alpn(sni: &str) -> Vec<u8> {
624 parser::build_client_hello_wire(&[parser::encode_sni_extension(sni)])
625 }
626
627 fn feed(core: &mut SniPrereadCore, cfg: &PrereadConfig<'_>, buf: &[u8]) -> Output {
628 core.handle_input(
629 cfg,
630 Input::Bytes {
631 buf,
632 now: Instant::now(),
633 },
634 )
635 }
636
637 #[test]
640 fn byte_replay_untouched_through_the_core() {
641 let mut routes = TrieNode::root();
642 routes.domain_insert(
643 b"example.com".to_vec(),
644 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
645 );
646 let cfg = cfg(&routes);
647 let wire = hello("example.com", &[]);
648 let before = wire.clone();
649 let mut core = SniPrereadCore::new();
650 let _ = feed(&mut core, &cfg, &wire);
651 assert_eq!(wire, before, "the core must never mutate the fed buffer");
652 }
653
654 #[test]
657 fn too_large_is_reachable() {
658 let routes = TrieNode::root();
659 let mut small_cfg = cfg(&routes);
660 small_cfg.max_bytes = 8;
661 let wire = hello_no_alpn("example.com");
662 let mut core = SniPrereadCore::new();
663 let out = feed(&mut core, &small_cfg, &wire[..10]);
665 assert_eq!(out, Output::Reject(RejectReason::TooLarge));
666 }
667
668 #[test]
672 fn exact_cap_boundary_complete_routes_incomplete_rejects() {
673 let mut routes = TrieNode::root();
674 routes.domain_insert(
675 b"example.com".to_vec(),
676 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
677 );
678 let wire = hello_no_alpn("example.com");
679
680 let mut exact_cfg = cfg(&routes);
681 exact_cfg.max_bytes = wire.len();
682 let mut core = SniPrereadCore::new();
683 match feed(&mut core, &exact_cfg, &wire) {
684 Output::Routed { cluster, .. } => assert_eq!(cluster, "cluster-a"),
685 other => panic!("expected Routed at exactly the cap, got {other:?}"),
686 }
687
688 let mut tight_cfg = cfg(&routes);
689 tight_cfg.max_bytes = wire.len() - 1;
690 let mut core = SniPrereadCore::new();
691 assert_eq!(
692 feed(&mut core, &tight_cfg, &wire[..wire.len() - 1]),
693 Output::Reject(RejectReason::TooLarge),
694 "an incomplete window at exactly the cap must reject TooLarge"
695 );
696 }
697
698 #[test]
708 fn complete_hello_with_trailing_bytes_over_cap_routes() {
709 let mut routes = TrieNode::root();
710 routes.domain_insert(
711 b"example.com".to_vec(),
712 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
713 );
714 let wire = hello("example.com", &[b"h2"]);
715 let mut over = wire.clone();
716 over.extend_from_slice(&[0xAB; 64]); let mut small_cfg = cfg(&routes);
719 small_cfg.max_bytes = wire.len() + 16; assert!(over.len() > small_cfg.max_bytes, "test setup: total > cap");
721
722 let mut core = SniPrereadCore::new();
723 match feed(&mut core, &small_cfg, &over) {
724 Output::Routed {
725 cluster,
726 content_offset,
727 sni,
728 ..
729 } => {
730 assert_eq!(cluster, "cluster-a");
731 assert_eq!(content_offset, 0);
732 assert_eq!(sni, "example.com");
733 }
734 other => panic!("a complete hello must route despite trailing bytes, got {other:?}"),
735 }
736 }
737
738 #[test]
742 fn proxy_incomplete_at_cap_is_too_large() {
743 let routes = TrieNode::root();
744 let mut proxy_cfg = cfg(&routes);
745 proxy_cfg.inbound_proxy = true;
746 proxy_cfg.max_bytes = 10;
747
748 let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
749 let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 9)), 443);
750 let proxy_header = HeaderV2::new(Command::Proxy, src, dst).into_bytes();
751
752 let mut core = SniPrereadCore::new();
753 let out = feed(&mut core, &proxy_cfg, &proxy_header[..10]);
754 assert_eq!(out, Output::Reject(RejectReason::TooLarge));
755 }
756
757 #[test]
758 fn empty_buffer_needs_more_and_sets_deadline() {
759 let routes = TrieNode::root();
760 let cfg = cfg(&routes);
761 let mut core = SniPrereadCore::new();
762 match feed(&mut core, &cfg, &[]) {
763 Output::NeedMore { .. } => {}
764 other => panic!("expected NeedMore on empty input, got {other:?}"),
765 }
766 }
767
768 #[test]
769 fn timeout_while_undecided_is_fragmented() {
770 let routes = TrieNode::root();
771 let cfg = cfg(&routes);
772 let mut core = SniPrereadCore::new();
773 let wire = hello_no_alpn("example.com");
775 let _ = feed(&mut core, &cfg, &wire[..wire.len() - 1]);
776 let out = core.handle_input(
777 &cfg,
778 Input::Timeout {
779 now: Instant::now(),
780 },
781 );
782 assert_eq!(out, Output::Reject(RejectReason::Fragmented));
783 }
784
785 #[test]
786 fn front_closed_before_decision_is_front_closed() {
787 let routes = TrieNode::root();
788 let cfg = cfg(&routes);
789 let mut core = SniPrereadCore::new();
790 let out = core.handle_input(&cfg, Input::FrontClosed);
791 assert_eq!(out, Output::Reject(RejectReason::FrontClosed));
792 }
793
794 #[test]
795 fn decided_latch_replays_the_same_terminal_forever() {
796 let mut routes = TrieNode::root();
797 routes.domain_insert(
798 b"example.com".to_vec(),
799 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
800 );
801 let cfg = cfg(&routes);
802 let mut core = SniPrereadCore::new();
803 let wire = hello_no_alpn("example.com");
804 let first = feed(&mut core, &cfg, &wire);
805 let second = feed(&mut core, &cfg, &[0xff; 3]);
807 assert_eq!(first, second);
808 let third = core.handle_input(
809 &cfg,
810 Input::Timeout {
811 now: Instant::now(),
812 },
813 );
814 assert_eq!(first, third);
815 }
816
817 #[test]
818 fn not_tls_is_reachable_through_the_core() {
819 let routes = TrieNode::root();
820 let cfg = cfg(&routes);
821 let mut core = SniPrereadCore::new();
822 let out = feed(&mut core, &cfg, &[0x16 + 1, 0x03, 0x03, 0x00, 0x00]);
823 assert_eq!(out, Output::Reject(RejectReason::NotTls));
824 }
825
826 #[test]
827 fn no_sni_is_reachable() {
828 let mut routes = TrieNode::root();
829 routes.domain_insert(
830 b"example.com".to_vec(),
831 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
832 );
833 let cfg = cfg(&routes);
834 let mut core = SniPrereadCore::new();
835 let wire = parser::build_client_hello_wire(&[]);
836 assert_eq!(
837 feed(&mut core, &cfg, &wire),
838 Output::Reject(RejectReason::NoSni)
839 );
840 }
841
842 #[test]
843 fn ech_outer_absent_is_reachable() {
844 let mut routes = TrieNode::root();
845 routes.domain_insert(
846 b"example.com".to_vec(),
847 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
848 );
849 let cfg = cfg(&routes);
850 let mut core = SniPrereadCore::new();
851 let wire = parser::build_client_hello_wire(&[parser::encode_extension(
852 0xfe0d,
853 &[0x00, 0x01, 0x02],
854 )]);
855 assert_eq!(
856 feed(&mut core, &cfg, &wire),
857 Output::Reject(RejectReason::EchOuterAbsent)
858 );
859 }
860
861 #[test]
862 fn sni_unmatched_is_reachable() {
863 let mut routes = TrieNode::root();
864 routes.domain_insert(
865 b"example.com".to_vec(),
866 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
867 );
868 let cfg = cfg(&routes);
869 let mut core = SniPrereadCore::new();
870 let wire = hello_no_alpn("unknown.example.net");
871 assert_eq!(
872 feed(&mut core, &cfg, &wire),
873 Output::Reject(RejectReason::SniUnmatched)
874 );
875 }
876
877 #[test]
878 fn alpn_unmatched_is_reachable() {
879 let mut routes = TrieNode::root();
880 routes.domain_insert(
881 b"example.com".to_vec(),
882 vec![(one_of(&[b"h2"]), "cluster-a".to_owned())],
883 );
884 let cfg = cfg(&routes);
885 let mut core = SniPrereadCore::new();
886 let wire = hello("example.com", &[b"http/1.1"]);
887 assert_eq!(
888 feed(&mut core, &cfg, &wire),
889 Output::Reject(RejectReason::AlpnUnmatched)
890 );
891 }
892
893 #[test]
894 fn proxy_header_invalid_is_reachable() {
895 let routes = TrieNode::root();
896 let mut proxy_cfg = cfg(&routes);
897 proxy_cfg.inbound_proxy = true;
898 let mut core = SniPrereadCore::new();
899 let out = feed(&mut core, &proxy_cfg, &[0xAAu8; 16]);
902 assert_eq!(out, Output::Reject(RejectReason::ProxyHeaderInvalid));
903 }
904
905 #[test]
906 fn malformed_record_is_reachable() {
907 let routes = TrieNode::root();
908 let cfg = cfg(&routes);
909 let mut core = SniPrereadCore::new();
910 let record = [0x16, 0x03, 0x03, 0xFF, 0xFF];
912 assert_eq!(
913 feed(&mut core, &cfg, &record),
914 Output::Reject(RejectReason::MalformedRecord)
915 );
916 }
917
918 #[test]
919 fn malformed_handshake_is_reachable() {
920 let routes = TrieNode::root();
921 let cfg = cfg(&routes);
922 let mut core = SniPrereadCore::new();
923 let hs = parser::wrap_handshake(&[]); let mut bad_hs = hs;
925 bad_hs[0] = 2; let record = parser::wrap_record(22, &bad_hs);
927 assert_eq!(
928 feed(&mut core, &cfg, &record),
929 Output::Reject(RejectReason::MalformedHandshake)
930 );
931 }
932
933 #[test]
936 fn drip_feed_needs_more_then_routes() {
937 let mut routes = TrieNode::root();
938 routes.domain_insert(
939 b"example.com".to_vec(),
940 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
941 );
942 let cfg = cfg(&routes);
943 let wire = hello("example.com", &[b"h2"]);
944 let mut core = SniPrereadCore::new();
945 for i in 0..wire.len() {
946 match feed(&mut core, &cfg, &wire[..i]) {
947 Output::NeedMore { .. } => {}
948 other => panic!("prefix {i} of {} must NeedMore, got {other:?}", wire.len()),
949 }
950 }
951 match feed(&mut core, &cfg, &wire) {
952 Output::Routed { cluster, sni, .. } => {
953 assert_eq!(cluster, "cluster-a");
954 assert_eq!(sni, "example.com");
955 }
956 other => panic!("expected Routed at full length, got {other:?}"),
957 }
958 }
959
960 #[test]
972 fn bytes_at_or_after_the_latched_deadline_reject_fragmented_without_moving_it() {
973 let mut routes = TrieNode::root();
974 routes.domain_insert(
975 b"example.com".to_vec(),
976 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
977 );
978 let mut cfg = cfg(&routes);
979 cfg.timeout = Duration::from_secs(5);
980 let wire = hello_no_alpn("example.com");
981 let incomplete = &wire[..wire.len() - 1];
982
983 let mut core = SniPrereadCore::new();
984 let start = Instant::now();
985
986 let deadline = match core.handle_input(
988 &cfg,
989 Input::Bytes {
990 buf: incomplete,
991 now: start,
992 },
993 ) {
994 Output::NeedMore { deadline } => deadline,
995 other => panic!("expected NeedMore on the first incomplete fragment, got {other:?}"),
996 };
997 assert_eq!(deadline, start + cfg.timeout);
998
999 let almost_deadline = deadline - Duration::from_millis(1);
1002 match core.handle_input(
1003 &cfg,
1004 Input::Bytes {
1005 buf: incomplete,
1006 now: almost_deadline,
1007 },
1008 ) {
1009 Output::NeedMore { deadline: d } => {
1010 assert_eq!(d, deadline, "the latched deadline must never move")
1011 }
1012 other => panic!("expected NeedMore just before the deadline, got {other:?}"),
1013 }
1014
1015 assert_eq!(
1019 core.handle_input(
1020 &cfg,
1021 Input::Bytes {
1022 buf: &wire,
1023 now: deadline,
1024 },
1025 ),
1026 Output::Reject(RejectReason::Fragmented),
1027 "a complete hello arriving at/after the latched deadline must reject Fragmented, not route"
1028 );
1029
1030 assert_eq!(
1034 core.handle_input(
1035 &cfg,
1036 Input::Bytes {
1037 buf: &wire,
1038 now: deadline + Duration::from_secs(1),
1039 },
1040 ),
1041 Output::Reject(RejectReason::Fragmented)
1042 );
1043 }
1044
1045 #[test]
1046 fn multi_record_client_hello_routes_through_the_core() {
1047 let mut routes = TrieNode::root();
1048 routes.domain_insert(
1049 b"split.example.com".to_vec(),
1050 vec![(AlpnMatcher::Any, "cluster-split".to_owned())],
1051 );
1052 let cfg = cfg(&routes);
1053 let wire = hello("split.example.com", &[b"h2"]);
1054 let split = parser::split_into_records(&wire, 4);
1055 let mut core = SniPrereadCore::new();
1056 match feed(&mut core, &cfg, &split) {
1057 Output::Routed { cluster, .. } => assert_eq!(cluster, "cluster-split"),
1058 other => panic!("expected Routed for a multi-record ClientHello, got {other:?}"),
1059 }
1060 }
1061
1062 #[test]
1065 fn proxy_v2_prefix_yields_content_offset_and_source() {
1066 let mut routes = TrieNode::root();
1067 routes.domain_insert(
1068 b"example.com".to_vec(),
1069 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
1070 );
1071 let mut proxy_cfg = cfg(&routes);
1072 proxy_cfg.inbound_proxy = true;
1073
1074 let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
1075 let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 9)), 443);
1076 let proxy_header = HeaderV2::new(Command::Proxy, src, dst).into_bytes();
1077 assert_eq!(proxy_header.len(), 28);
1080
1081 let mut wire = proxy_header.clone();
1082 wire.extend_from_slice(&hello_no_alpn("example.com"));
1083
1084 let mut core = SniPrereadCore::new();
1085 match feed(&mut core, &proxy_cfg, &wire) {
1086 Output::Routed {
1087 cluster,
1088 content_offset,
1089 proxy_source,
1090 ..
1091 } => {
1092 assert_eq!(cluster, "cluster-a");
1093 assert_eq!(content_offset, proxy_header.len());
1094 assert_eq!(proxy_source, Some(src));
1095 }
1096 other => panic!("expected Routed behind a PROXY-v2 header, got {other:?}"),
1097 }
1098 }
1099
1100 #[test]
1101 fn proxy_v2_prefix_drip_feed_needs_more_until_complete() {
1102 let mut routes = TrieNode::root();
1103 routes.domain_insert(
1104 b"example.com".to_vec(),
1105 vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
1106 );
1107 let mut proxy_cfg = cfg(&routes);
1108 proxy_cfg.inbound_proxy = true;
1109
1110 let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
1111 let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 9)), 443);
1112 let proxy_header = HeaderV2::new(Command::Proxy, src, dst).into_bytes();
1113 let mut wire = proxy_header.clone();
1114 wire.extend_from_slice(&hello_no_alpn("example.com"));
1115
1116 let mut core = SniPrereadCore::new();
1117 for i in 0..proxy_header.len() {
1118 match feed(&mut core, &proxy_cfg, &wire[..i]) {
1119 Output::NeedMore { .. } => {}
1120 other => panic!("PROXY-header prefix {i} must NeedMore, got {other:?}"),
1121 }
1122 }
1123 }
1124
1125 #[test]
1128 fn exact_sni_beats_wildcard_before_alpn() {
1129 let mut routes = TrieNode::root();
1130 routes.domain_insert(
1131 b"*.example.com".to_vec(),
1132 vec![(AlpnMatcher::Any, "wildcard-cluster".to_owned())],
1133 );
1134 routes.domain_insert(
1135 b"a.example.com".to_vec(),
1136 vec![(one_of(&[b"h2"]), "exact-cluster".to_owned())],
1137 );
1138 let cfg = cfg(&routes);
1139
1140 let wire = hello("a.example.com", &[b"http/1.1"]);
1144 let mut core = SniPrereadCore::new();
1145 assert_eq!(
1146 feed(&mut core, &cfg, &wire),
1147 Output::Reject(RejectReason::AlpnUnmatched)
1148 );
1149 }
1150
1151 #[test]
1152 fn wildcard_matches_one_level_not_two_not_apex() {
1153 let mut routes = TrieNode::root();
1154 routes.domain_insert(
1155 b"*.example.com".to_vec(),
1156 vec![(AlpnMatcher::Any, "wildcard-cluster".to_owned())],
1157 );
1158 let cfg = cfg(&routes);
1159
1160 let mut core = SniPrereadCore::new();
1161 match feed(&mut core, &cfg, &hello_no_alpn("a.example.com")) {
1162 Output::Routed {
1163 cluster,
1164 sni,
1165 matched_sni_pattern,
1166 matched_alpn,
1167 ..
1168 } => {
1169 assert_eq!(cluster, "wildcard-cluster");
1170 assert_eq!(sni, "a.example.com");
1174 assert_eq!(matched_sni_pattern, "*.example.com");
1175 assert_eq!(matched_alpn, AlpnMatcher::Any);
1176 }
1177 other => panic!("*.example.com must match a.example.com, got {other:?}"),
1178 }
1179
1180 let mut core = SniPrereadCore::new();
1181 assert_eq!(
1182 feed(&mut core, &cfg, &hello_no_alpn("a.b.example.com")),
1183 Output::Reject(RejectReason::SniUnmatched),
1184 "*.example.com must NOT match a.b.example.com"
1185 );
1186
1187 let mut core = SniPrereadCore::new();
1188 assert_eq!(
1189 feed(&mut core, &cfg, &hello_no_alpn("example.com")),
1190 Output::Reject(RejectReason::SniUnmatched),
1191 "*.example.com must NOT match the apex example.com"
1192 );
1193 }
1194
1195 #[test]
1196 fn alpn_client_preference_order_wins() {
1197 let mut routes = TrieNode::root();
1198 routes.domain_insert(
1199 b"example.com".to_vec(),
1200 vec![
1201 (one_of(&[b"http/1.1"]), "cluster-http11".to_owned()),
1202 (one_of(&[b"h2"]), "cluster-h2".to_owned()),
1203 ],
1204 );
1205 let cfg = cfg(&routes);
1206
1207 let mut core = SniPrereadCore::new();
1210 match feed(
1211 &mut core,
1212 &cfg,
1213 &hello("example.com", &[b"h2", b"http/1.1"]),
1214 ) {
1215 Output::Routed { cluster, .. } => assert_eq!(cluster, "cluster-h2"),
1216 other => panic!("expected the client's first preference, got {other:?}"),
1217 }
1218
1219 let mut core = SniPrereadCore::new();
1220 match feed(
1221 &mut core,
1222 &cfg,
1223 &hello("example.com", &[b"http/1.1", b"h2"]),
1224 ) {
1225 Output::Routed { cluster, .. } => assert_eq!(cluster, "cluster-http11"),
1226 other => panic!("expected the client's first preference, got {other:?}"),
1227 }
1228 }
1229
1230 #[test]
1231 fn alpn_any_catch_all_and_no_alpn_client_matches_any_only() {
1232 let mut routes = TrieNode::root();
1233 routes.domain_insert(
1234 b"example.com".to_vec(),
1235 vec![
1236 (one_of(&[b"h2"]), "cluster-h2".to_owned()),
1237 (AlpnMatcher::Any, "cluster-default".to_owned()),
1238 ],
1239 );
1240 let cfg = cfg(&routes);
1241
1242 let mut core = SniPrereadCore::new();
1244 match feed(&mut core, &cfg, &hello("example.com", &[b"spdy/1"])) {
1245 Output::Routed { cluster, .. } => assert_eq!(cluster, "cluster-default"),
1246 other => panic!("expected the Any catch-all, got {other:?}"),
1247 }
1248
1249 let mut core = SniPrereadCore::new();
1251 match feed(&mut core, &cfg, &hello_no_alpn("example.com")) {
1252 Output::Routed { cluster, alpn, .. } => {
1253 assert_eq!(cluster, "cluster-default");
1254 assert!(alpn.is_empty());
1255 }
1256 other => panic!("expected the Any catch-all for a no-ALPN client, got {other:?}"),
1257 }
1258 }
1259
1260 #[test]
1261 fn no_any_and_no_matching_one_of_is_alpn_unmatched() {
1262 let mut routes = TrieNode::root();
1263 routes.domain_insert(
1264 b"example.com".to_vec(),
1265 vec![(one_of(&[b"h2"]), "cluster-h2".to_owned())],
1266 );
1267 let cfg = cfg(&routes);
1268 let mut core = SniPrereadCore::new();
1269 assert_eq!(
1270 feed(&mut core, &cfg, &hello_no_alpn("example.com")),
1271 Output::Reject(RejectReason::AlpnUnmatched),
1272 "a no-ALPN client with no Any entry must be unmatched, not silently routed"
1273 );
1274 }
1275}