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)]
458 fn check_invariants(&self) {
459 debug_assert!(
463 !matches!(self.decided, Some(Output::NeedMore { .. })),
464 "decided latch must never store NeedMore -- it is not a terminal verdict"
465 );
466 if let Some(Output::Routed {
478 sni,
479 matched_sni_pattern,
480 ..
481 }) = &self.decided
482 {
483 debug_assert_eq!(
484 sni,
485 &sni.to_ascii_lowercase(),
486 "latched SNI must already be lowercase-normalized"
487 );
488 debug_assert!(
489 !sni.ends_with('.'),
490 "latched SNI must not carry a trailing dot after normalization"
491 );
492 debug_assert_eq!(
493 matched_sni_pattern,
494 &matched_sni_pattern.to_ascii_lowercase(),
495 "latched matched_sni_pattern must already be lowercase (trie keys are lowercased at insert)"
496 );
497 debug_assert!(
498 matched_sni_pattern == sni
499 || matched_sni_pattern
500 .strip_prefix("*.")
501 .is_some_and(|suffix| sni.ends_with(suffix))
502 || matched_sni_pattern
512 .rfind('/')
513 .is_some_and(|close| sni.ends_with(&matched_sni_pattern[close + 1..])),
514 "matched_sni_pattern must be the SNI itself (exact route), a wildcard the SNI \
515 falls under, or a `/regex/` segment route whose literal tail the SNI ends with"
516 );
517 }
518 }
519
520 #[inline]
521 fn debug_assert_invariants(&self) {
522 #[cfg(debug_assertions)]
523 self.check_invariants();
524 }
525}
526
527fn resolve_alpn<'r>(
538 entries: &'r [(AlpnMatcher, ClusterId)],
539 offered: &[Vec<u8>],
540) -> Option<(&'r AlpnMatcher, &'r ClusterId)> {
541 for protocol in offered {
542 for (matcher, cluster) in entries {
543 if let AlpnMatcher::OneOf(set) = matcher
544 && set.contains(protocol)
545 {
546 return Some((matcher, cluster));
547 }
548 }
549 }
550 entries
551 .iter()
552 .find(|(matcher, _)| matches!(matcher, AlpnMatcher::Any))
553 .map(|(matcher, cluster)| (matcher, cluster))
554}
555
556fn normalize_sni(mut sni: String) -> String {
562 sni.make_ascii_lowercase();
563 if sni.ends_with('.') {
564 sni.pop();
565 }
566 sni
567}
568
569#[allow(unused_macros)]
575macro_rules! log_context {
576 ($self:expr) => {{
577 let (open, reset, grey, gray, white) = sozu_command::logging::ansi_palette();
578 format!(
579 "[- - - -]\t{open}TCP-SNI{reset}\t{grey}Preread{reset}({gray}decided{reset}={white}{decided}{reset}, {gray}deadline_armed{reset}={white}{deadline}{reset})\t >>>",
580 open = open,
581 reset = reset,
582 grey = grey,
583 gray = gray,
584 white = white,
585 decided = $self.decided.is_some(),
586 deadline = $self.deadline.is_some(),
587 )
588 }};
589}
590
591#[allow(unused_macros)]
597macro_rules! log_context_lite {
598 ($sni:expr) => {{
599 let (open, reset, grey, gray, white) = sozu_command::logging::ansi_palette();
600 format!(
601 "[- - - -]\t{open}TCP-SNI{reset}\t{grey}Route{reset}({gray}sni{reset}={white}{sni:?}{reset})\t >>>",
602 open = open,
603 reset = reset,
604 grey = grey,
605 gray = gray,
606 white = white,
607 sni = $sni,
608 )
609 }};
610}
611
612#[allow(unused_imports)]
613use {log_context, log_context_lite};
614
615#[cfg(test)]
616mod tests {
617 use std::net::{IpAddr, Ipv4Addr};
618
619 use super::*;
620 use crate::protocol::proxy_protocol::header::{Command, HeaderV2};
621
622 fn cfg<'t>(routes: &'t TrieNode<Vec<(AlpnMatcher, ClusterId)>>) -> PrereadConfig<'t> {
623 PrereadConfig {
624 routes,
625 inbound_proxy: false,
626 max_bytes: 16 * 1024,
627 timeout: Duration::from_secs(3),
628 accept_wildcard: true,
629 }
630 }
631
632 fn one_of(protocols: &[&[u8]]) -> AlpnMatcher {
633 AlpnMatcher::OneOf(protocols.iter().map(|p| p.to_vec()).collect())
634 }
635
636 fn hello(sni: &str, alpn: &[&[u8]]) -> Vec<u8> {
637 parser::build_client_hello_wire(&[
638 parser::encode_sni_extension(sni),
639 parser::encode_alpn_extension(alpn),
640 ])
641 }
642
643 fn hello_no_alpn(sni: &str) -> Vec<u8> {
644 parser::build_client_hello_wire(&[parser::encode_sni_extension(sni)])
645 }
646
647 fn feed(core: &mut SniPrereadCore, cfg: &PrereadConfig<'_>, buf: &[u8]) -> Output {
648 core.handle_input(
649 cfg,
650 Input::Bytes {
651 buf,
652 now: Instant::now(),
653 },
654 )
655 }
656
657 #[test]
660 fn byte_replay_untouched_through_the_core() {
661 let mut routes = TrieNode::root();
662 routes.domain_insert(
663 b"example.com".to_vec(),
664 vec![(AlpnMatcher::Any, "cluster-a".into())],
665 );
666 let cfg = cfg(&routes);
667 let wire = hello("example.com", &[]);
668 let before = wire.clone();
669 let mut core = SniPrereadCore::new();
670 let _ = feed(&mut core, &cfg, &wire);
671 assert_eq!(wire, before, "the core must never mutate the fed buffer");
672 }
673
674 #[test]
677 fn too_large_is_reachable() {
678 let routes = TrieNode::root();
679 let mut small_cfg = cfg(&routes);
680 small_cfg.max_bytes = 8;
681 let wire = hello_no_alpn("example.com");
682 let mut core = SniPrereadCore::new();
683 let out = feed(&mut core, &small_cfg, &wire[..10]);
685 assert_eq!(out, Output::Reject(RejectReason::TooLarge));
686 }
687
688 #[test]
692 fn exact_cap_boundary_complete_routes_incomplete_rejects() {
693 let mut routes = TrieNode::root();
694 routes.domain_insert(
695 b"example.com".to_vec(),
696 vec![(AlpnMatcher::Any, "cluster-a".into())],
697 );
698 let wire = hello_no_alpn("example.com");
699
700 let mut exact_cfg = cfg(&routes);
701 exact_cfg.max_bytes = wire.len();
702 let mut core = SniPrereadCore::new();
703 match feed(&mut core, &exact_cfg, &wire) {
704 Output::Routed { cluster, .. } => assert_eq!(&*cluster, "cluster-a"),
705 other => panic!("expected Routed at exactly the cap, got {other:?}"),
706 }
707
708 let mut tight_cfg = cfg(&routes);
709 tight_cfg.max_bytes = wire.len() - 1;
710 let mut core = SniPrereadCore::new();
711 assert_eq!(
712 feed(&mut core, &tight_cfg, &wire[..wire.len() - 1]),
713 Output::Reject(RejectReason::TooLarge),
714 "an incomplete window at exactly the cap must reject TooLarge"
715 );
716 }
717
718 #[test]
728 fn complete_hello_with_trailing_bytes_over_cap_routes() {
729 let mut routes = TrieNode::root();
730 routes.domain_insert(
731 b"example.com".to_vec(),
732 vec![(AlpnMatcher::Any, "cluster-a".into())],
733 );
734 let wire = hello("example.com", &[b"h2"]);
735 let mut over = wire.clone();
736 over.extend_from_slice(&[0xAB; 64]); let mut small_cfg = cfg(&routes);
739 small_cfg.max_bytes = wire.len() + 16; assert!(over.len() > small_cfg.max_bytes, "test setup: total > cap");
741
742 let mut core = SniPrereadCore::new();
743 match feed(&mut core, &small_cfg, &over) {
744 Output::Routed {
745 cluster,
746 content_offset,
747 sni,
748 ..
749 } => {
750 assert_eq!(&*cluster, "cluster-a");
751 assert_eq!(content_offset, 0);
752 assert_eq!(sni, "example.com");
753 }
754 other => panic!("a complete hello must route despite trailing bytes, got {other:?}"),
755 }
756 }
757
758 #[test]
762 fn proxy_incomplete_at_cap_is_too_large() {
763 let routes = TrieNode::root();
764 let mut proxy_cfg = cfg(&routes);
765 proxy_cfg.inbound_proxy = true;
766 proxy_cfg.max_bytes = 10;
767
768 let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
769 let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 9)), 443);
770 let proxy_header = HeaderV2::new(Command::Proxy, src, dst).into_bytes();
771
772 let mut core = SniPrereadCore::new();
773 let out = feed(&mut core, &proxy_cfg, &proxy_header[..10]);
774 assert_eq!(out, Output::Reject(RejectReason::TooLarge));
775 }
776
777 #[test]
778 fn empty_buffer_needs_more_and_sets_deadline() {
779 let routes = TrieNode::root();
780 let cfg = cfg(&routes);
781 let mut core = SniPrereadCore::new();
782 match feed(&mut core, &cfg, &[]) {
783 Output::NeedMore { .. } => {}
784 other => panic!("expected NeedMore on empty input, got {other:?}"),
785 }
786 }
787
788 #[test]
789 fn timeout_while_undecided_is_fragmented() {
790 let routes = TrieNode::root();
791 let cfg = cfg(&routes);
792 let mut core = SniPrereadCore::new();
793 let wire = hello_no_alpn("example.com");
795 let _ = feed(&mut core, &cfg, &wire[..wire.len() - 1]);
796 let out = core.handle_input(
797 &cfg,
798 Input::Timeout {
799 now: Instant::now(),
800 },
801 );
802 assert_eq!(out, Output::Reject(RejectReason::Fragmented));
803 }
804
805 #[test]
806 fn front_closed_before_decision_is_front_closed() {
807 let routes = TrieNode::root();
808 let cfg = cfg(&routes);
809 let mut core = SniPrereadCore::new();
810 let out = core.handle_input(&cfg, Input::FrontClosed);
811 assert_eq!(out, Output::Reject(RejectReason::FrontClosed));
812 }
813
814 #[test]
815 fn decided_latch_replays_the_same_terminal_forever() {
816 let mut routes = TrieNode::root();
817 routes.domain_insert(
818 b"example.com".to_vec(),
819 vec![(AlpnMatcher::Any, "cluster-a".into())],
820 );
821 let cfg = cfg(&routes);
822 let mut core = SniPrereadCore::new();
823 let wire = hello_no_alpn("example.com");
824 let first = feed(&mut core, &cfg, &wire);
825 let second = feed(&mut core, &cfg, &[0xff; 3]);
827 assert_eq!(first, second);
828 let third = core.handle_input(
829 &cfg,
830 Input::Timeout {
831 now: Instant::now(),
832 },
833 );
834 assert_eq!(first, third);
835 }
836
837 #[test]
838 fn not_tls_is_reachable_through_the_core() {
839 let routes = TrieNode::root();
840 let cfg = cfg(&routes);
841 let mut core = SniPrereadCore::new();
842 let out = feed(&mut core, &cfg, &[0x16 + 1, 0x03, 0x03, 0x00, 0x00]);
843 assert_eq!(out, Output::Reject(RejectReason::NotTls));
844 }
845
846 #[test]
847 fn no_sni_is_reachable() {
848 let mut routes = TrieNode::root();
849 routes.domain_insert(
850 b"example.com".to_vec(),
851 vec![(AlpnMatcher::Any, "cluster-a".into())],
852 );
853 let cfg = cfg(&routes);
854 let mut core = SniPrereadCore::new();
855 let wire = parser::build_client_hello_wire(&[]);
856 assert_eq!(
857 feed(&mut core, &cfg, &wire),
858 Output::Reject(RejectReason::NoSni)
859 );
860 }
861
862 #[test]
863 fn ech_outer_absent_is_reachable() {
864 let mut routes = TrieNode::root();
865 routes.domain_insert(
866 b"example.com".to_vec(),
867 vec![(AlpnMatcher::Any, "cluster-a".into())],
868 );
869 let cfg = cfg(&routes);
870 let mut core = SniPrereadCore::new();
871 let wire = parser::build_client_hello_wire(&[parser::encode_extension(
872 0xfe0d,
873 &[0x00, 0x01, 0x02],
874 )]);
875 assert_eq!(
876 feed(&mut core, &cfg, &wire),
877 Output::Reject(RejectReason::EchOuterAbsent)
878 );
879 }
880
881 #[test]
882 fn sni_unmatched_is_reachable() {
883 let mut routes = TrieNode::root();
884 routes.domain_insert(
885 b"example.com".to_vec(),
886 vec![(AlpnMatcher::Any, "cluster-a".into())],
887 );
888 let cfg = cfg(&routes);
889 let mut core = SniPrereadCore::new();
890 let wire = hello_no_alpn("unknown.example.net");
891 assert_eq!(
892 feed(&mut core, &cfg, &wire),
893 Output::Reject(RejectReason::SniUnmatched)
894 );
895 }
896
897 #[test]
918 fn an_alternating_regex_route_segment_matches_the_whole_sni_label_only() {
919 let mut routes = TrieNode::root();
920 routes.domain_insert(
921 b"/a|b|c/.example.com".to_vec(),
922 vec![(AlpnMatcher::Any, "cluster-alt".into())],
923 );
924 let cfg = cfg(&routes);
925
926 for whole in ["a.example.com", "b.example.com", "c.example.com"] {
927 let mut core = SniPrereadCore::new();
928 match feed(&mut core, &cfg, &hello_no_alpn(whole)) {
929 Output::Routed { cluster, .. } => assert_eq!(&*cluster, "cluster-alt"),
930 other => panic!("expected Routed for {whole}, got {other:?}"),
931 }
932 }
933
934 for leak in [
935 "axx.example.com",
936 "xxc.example.com",
937 "zzbzz.example.com",
938 "bzz.example.com",
939 "zzb.example.com",
940 ] {
941 let mut core = SniPrereadCore::new();
942 let out = feed(&mut core, &cfg, &hello_no_alpn(leak));
943 assert_eq!(
944 out,
945 Output::Reject(RejectReason::SniUnmatched),
946 "expected SniUnmatched for {leak}, got {out:?}",
947 );
948 }
949 }
950
951 #[test]
952 fn alpn_unmatched_is_reachable() {
953 let mut routes = TrieNode::root();
954 routes.domain_insert(
955 b"example.com".to_vec(),
956 vec![(one_of(&[b"h2"]), "cluster-a".into())],
957 );
958 let cfg = cfg(&routes);
959 let mut core = SniPrereadCore::new();
960 let wire = hello("example.com", &[b"http/1.1"]);
961 assert_eq!(
962 feed(&mut core, &cfg, &wire),
963 Output::Reject(RejectReason::AlpnUnmatched)
964 );
965 }
966
967 #[test]
968 fn proxy_header_invalid_is_reachable() {
969 let routes = TrieNode::root();
970 let mut proxy_cfg = cfg(&routes);
971 proxy_cfg.inbound_proxy = true;
972 let mut core = SniPrereadCore::new();
973 let out = feed(&mut core, &proxy_cfg, &[0xAAu8; 16]);
976 assert_eq!(out, Output::Reject(RejectReason::ProxyHeaderInvalid));
977 }
978
979 #[test]
980 fn malformed_record_is_reachable() {
981 let routes = TrieNode::root();
982 let cfg = cfg(&routes);
983 let mut core = SniPrereadCore::new();
984 let record = [0x16, 0x03, 0x03, 0xFF, 0xFF];
986 assert_eq!(
987 feed(&mut core, &cfg, &record),
988 Output::Reject(RejectReason::MalformedRecord)
989 );
990 }
991
992 #[test]
993 fn malformed_handshake_is_reachable() {
994 let routes = TrieNode::root();
995 let cfg = cfg(&routes);
996 let mut core = SniPrereadCore::new();
997 let hs = parser::wrap_handshake(&[]); let mut bad_hs = hs;
999 bad_hs[0] = 2; let record = parser::wrap_record(22, &bad_hs);
1001 assert_eq!(
1002 feed(&mut core, &cfg, &record),
1003 Output::Reject(RejectReason::MalformedHandshake)
1004 );
1005 }
1006
1007 #[test]
1010 fn drip_feed_needs_more_then_routes() {
1011 let mut routes = TrieNode::root();
1012 routes.domain_insert(
1013 b"example.com".to_vec(),
1014 vec![(AlpnMatcher::Any, "cluster-a".into())],
1015 );
1016 let cfg = cfg(&routes);
1017 let wire = hello("example.com", &[b"h2"]);
1018 let mut core = SniPrereadCore::new();
1019 for i in 0..wire.len() {
1020 match feed(&mut core, &cfg, &wire[..i]) {
1021 Output::NeedMore { .. } => {}
1022 other => panic!("prefix {i} of {} must NeedMore, got {other:?}", wire.len()),
1023 }
1024 }
1025 match feed(&mut core, &cfg, &wire) {
1026 Output::Routed { cluster, sni, .. } => {
1027 assert_eq!(&*cluster, "cluster-a");
1028 assert_eq!(sni, "example.com");
1029 }
1030 other => panic!("expected Routed at full length, got {other:?}"),
1031 }
1032 }
1033
1034 #[test]
1046 fn bytes_at_or_after_the_latched_deadline_reject_fragmented_without_moving_it() {
1047 let mut routes = TrieNode::root();
1048 routes.domain_insert(
1049 b"example.com".to_vec(),
1050 vec![(AlpnMatcher::Any, "cluster-a".into())],
1051 );
1052 let mut cfg = cfg(&routes);
1053 cfg.timeout = Duration::from_secs(5);
1054 let wire = hello_no_alpn("example.com");
1055 let incomplete = &wire[..wire.len() - 1];
1056
1057 let mut core = SniPrereadCore::new();
1058 let start = Instant::now();
1059
1060 let deadline = match core.handle_input(
1062 &cfg,
1063 Input::Bytes {
1064 buf: incomplete,
1065 now: start,
1066 },
1067 ) {
1068 Output::NeedMore { deadline } => deadline,
1069 other => panic!("expected NeedMore on the first incomplete fragment, got {other:?}"),
1070 };
1071 assert_eq!(deadline, start + cfg.timeout);
1072
1073 let almost_deadline = deadline - Duration::from_millis(1);
1076 match core.handle_input(
1077 &cfg,
1078 Input::Bytes {
1079 buf: incomplete,
1080 now: almost_deadline,
1081 },
1082 ) {
1083 Output::NeedMore { deadline: d } => {
1084 assert_eq!(d, deadline, "the latched deadline must never move")
1085 }
1086 other => panic!("expected NeedMore just before the deadline, got {other:?}"),
1087 }
1088
1089 assert_eq!(
1093 core.handle_input(
1094 &cfg,
1095 Input::Bytes {
1096 buf: &wire,
1097 now: deadline,
1098 },
1099 ),
1100 Output::Reject(RejectReason::Fragmented),
1101 "a complete hello arriving at/after the latched deadline must reject Fragmented, not route"
1102 );
1103
1104 assert_eq!(
1108 core.handle_input(
1109 &cfg,
1110 Input::Bytes {
1111 buf: &wire,
1112 now: deadline + Duration::from_secs(1),
1113 },
1114 ),
1115 Output::Reject(RejectReason::Fragmented)
1116 );
1117 }
1118
1119 #[test]
1120 fn multi_record_client_hello_routes_through_the_core() {
1121 let mut routes = TrieNode::root();
1122 routes.domain_insert(
1123 b"split.example.com".to_vec(),
1124 vec![(AlpnMatcher::Any, "cluster-split".into())],
1125 );
1126 let cfg = cfg(&routes);
1127 let wire = hello("split.example.com", &[b"h2"]);
1128 let split = parser::split_into_records(&wire, 4);
1129 let mut core = SniPrereadCore::new();
1130 match feed(&mut core, &cfg, &split) {
1131 Output::Routed { cluster, .. } => assert_eq!(&*cluster, "cluster-split"),
1132 other => panic!("expected Routed for a multi-record ClientHello, got {other:?}"),
1133 }
1134 }
1135
1136 #[test]
1139 fn proxy_v2_prefix_yields_content_offset_and_source() {
1140 let mut routes = TrieNode::root();
1141 routes.domain_insert(
1142 b"example.com".to_vec(),
1143 vec![(AlpnMatcher::Any, "cluster-a".into())],
1144 );
1145 let mut proxy_cfg = cfg(&routes);
1146 proxy_cfg.inbound_proxy = true;
1147
1148 let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
1149 let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 9)), 443);
1150 let proxy_header = HeaderV2::new(Command::Proxy, src, dst).into_bytes();
1151 assert_eq!(proxy_header.len(), 28);
1154
1155 let mut wire = proxy_header.clone();
1156 wire.extend_from_slice(&hello_no_alpn("example.com"));
1157
1158 let mut core = SniPrereadCore::new();
1159 match feed(&mut core, &proxy_cfg, &wire) {
1160 Output::Routed {
1161 cluster,
1162 content_offset,
1163 proxy_source,
1164 ..
1165 } => {
1166 assert_eq!(&*cluster, "cluster-a");
1167 assert_eq!(content_offset, proxy_header.len());
1168 assert_eq!(proxy_source, Some(src));
1169 }
1170 other => panic!("expected Routed behind a PROXY-v2 header, got {other:?}"),
1171 }
1172 }
1173
1174 #[test]
1194 fn proxy_v2_local_command_attributes_no_source() {
1195 let mut routes = TrieNode::root();
1196 routes.domain_insert(
1197 b"example.com".to_vec(),
1198 vec![(AlpnMatcher::Any, "cluster-a".into())],
1199 );
1200 let mut proxy_cfg = cfg(&routes);
1201 proxy_cfg.inbound_proxy = true;
1202
1203 let forged_src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
1204 let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 9)), 443);
1205 let proxy_header = HeaderV2::new(Command::Local, forged_src, dst).into_bytes();
1206 assert_eq!(proxy_header.len(), 28);
1209 assert_eq!(proxy_header[12], 0x20, "ver/cmd must be LOCAL");
1210 assert_eq!(proxy_header[13], 0x11, "family must be AF_INET over STREAM");
1211
1212 let mut wire = proxy_header.clone();
1213 wire.extend_from_slice(&hello_no_alpn("example.com"));
1214
1215 let mut core = SniPrereadCore::new();
1216 match feed(&mut core, &proxy_cfg, &wire) {
1217 Output::Routed {
1218 cluster,
1219 content_offset,
1220 proxy_source,
1221 ..
1222 } => {
1223 assert_eq!(&*cluster, "cluster-a");
1226 assert_eq!(content_offset, proxy_header.len());
1227 assert_eq!(
1228 proxy_source, None,
1229 "a LOCAL header must attribute no source, so TcpSession falls back to peer_addr"
1230 );
1231 }
1232 other => panic!("expected Routed behind a PROXY-v2 LOCAL header, got {other:?}"),
1233 }
1234 }
1235
1236 #[test]
1237 fn proxy_v2_prefix_drip_feed_needs_more_until_complete() {
1238 let mut routes = TrieNode::root();
1239 routes.domain_insert(
1240 b"example.com".to_vec(),
1241 vec![(AlpnMatcher::Any, "cluster-a".into())],
1242 );
1243 let mut proxy_cfg = cfg(&routes);
1244 proxy_cfg.inbound_proxy = true;
1245
1246 let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
1247 let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 9)), 443);
1248 let proxy_header = HeaderV2::new(Command::Proxy, src, dst).into_bytes();
1249 let mut wire = proxy_header.clone();
1250 wire.extend_from_slice(&hello_no_alpn("example.com"));
1251
1252 let mut core = SniPrereadCore::new();
1253 for i in 0..proxy_header.len() {
1254 match feed(&mut core, &proxy_cfg, &wire[..i]) {
1255 Output::NeedMore { .. } => {}
1256 other => panic!("PROXY-header prefix {i} must NeedMore, got {other:?}"),
1257 }
1258 }
1259 }
1260
1261 #[test]
1264 fn exact_sni_beats_wildcard_before_alpn() {
1265 let mut routes = TrieNode::root();
1266 routes.domain_insert(
1267 b"*.example.com".to_vec(),
1268 vec![(AlpnMatcher::Any, "wildcard-cluster".into())],
1269 );
1270 routes.domain_insert(
1271 b"a.example.com".to_vec(),
1272 vec![(one_of(&[b"h2"]), "exact-cluster".into())],
1273 );
1274 let cfg = cfg(&routes);
1275
1276 let wire = hello("a.example.com", &[b"http/1.1"]);
1280 let mut core = SniPrereadCore::new();
1281 assert_eq!(
1282 feed(&mut core, &cfg, &wire),
1283 Output::Reject(RejectReason::AlpnUnmatched)
1284 );
1285 }
1286
1287 #[test]
1288 fn wildcard_matches_one_level_not_two_not_apex() {
1289 let mut routes = TrieNode::root();
1290 routes.domain_insert(
1291 b"*.example.com".to_vec(),
1292 vec![(AlpnMatcher::Any, "wildcard-cluster".into())],
1293 );
1294 let cfg = cfg(&routes);
1295
1296 let mut core = SniPrereadCore::new();
1297 match feed(&mut core, &cfg, &hello_no_alpn("a.example.com")) {
1298 Output::Routed {
1299 cluster,
1300 sni,
1301 matched_sni_pattern,
1302 matched_alpn,
1303 ..
1304 } => {
1305 assert_eq!(&*cluster, "wildcard-cluster");
1306 assert_eq!(sni, "a.example.com");
1310 assert_eq!(matched_sni_pattern, "*.example.com");
1311 assert_eq!(matched_alpn, AlpnMatcher::Any);
1312 }
1313 other => panic!("*.example.com must match a.example.com, got {other:?}"),
1314 }
1315
1316 let mut core = SniPrereadCore::new();
1317 assert_eq!(
1318 feed(&mut core, &cfg, &hello_no_alpn("a.b.example.com")),
1319 Output::Reject(RejectReason::SniUnmatched),
1320 "*.example.com must NOT match a.b.example.com"
1321 );
1322
1323 let mut core = SniPrereadCore::new();
1324 assert_eq!(
1325 feed(&mut core, &cfg, &hello_no_alpn("example.com")),
1326 Output::Reject(RejectReason::SniUnmatched),
1327 "*.example.com must NOT match the apex example.com"
1328 );
1329 }
1330
1331 #[test]
1332 fn alpn_client_preference_order_wins() {
1333 let mut routes = TrieNode::root();
1334 routes.domain_insert(
1335 b"example.com".to_vec(),
1336 vec![
1337 (one_of(&[b"http/1.1"]), "cluster-http11".into()),
1338 (one_of(&[b"h2"]), "cluster-h2".into()),
1339 ],
1340 );
1341 let cfg = cfg(&routes);
1342
1343 let mut core = SniPrereadCore::new();
1346 match feed(
1347 &mut core,
1348 &cfg,
1349 &hello("example.com", &[b"h2", b"http/1.1"]),
1350 ) {
1351 Output::Routed { cluster, .. } => assert_eq!(&*cluster, "cluster-h2"),
1352 other => panic!("expected the client's first preference, got {other:?}"),
1353 }
1354
1355 let mut core = SniPrereadCore::new();
1356 match feed(
1357 &mut core,
1358 &cfg,
1359 &hello("example.com", &[b"http/1.1", b"h2"]),
1360 ) {
1361 Output::Routed { cluster, .. } => assert_eq!(&*cluster, "cluster-http11"),
1362 other => panic!("expected the client's first preference, got {other:?}"),
1363 }
1364 }
1365
1366 #[test]
1367 fn alpn_any_catch_all_and_no_alpn_client_matches_any_only() {
1368 let mut routes = TrieNode::root();
1369 routes.domain_insert(
1370 b"example.com".to_vec(),
1371 vec![
1372 (one_of(&[b"h2"]), "cluster-h2".into()),
1373 (AlpnMatcher::Any, "cluster-default".into()),
1374 ],
1375 );
1376 let cfg = cfg(&routes);
1377
1378 let mut core = SniPrereadCore::new();
1380 match feed(&mut core, &cfg, &hello("example.com", &[b"spdy/1"])) {
1381 Output::Routed { cluster, .. } => assert_eq!(&*cluster, "cluster-default"),
1382 other => panic!("expected the Any catch-all, got {other:?}"),
1383 }
1384
1385 let mut core = SniPrereadCore::new();
1387 match feed(&mut core, &cfg, &hello_no_alpn("example.com")) {
1388 Output::Routed { cluster, alpn, .. } => {
1389 assert_eq!(&*cluster, "cluster-default");
1390 assert!(alpn.is_empty());
1391 }
1392 other => panic!("expected the Any catch-all for a no-ALPN client, got {other:?}"),
1393 }
1394 }
1395
1396 #[test]
1397 fn no_any_and_no_matching_one_of_is_alpn_unmatched() {
1398 let mut routes = TrieNode::root();
1399 routes.domain_insert(
1400 b"example.com".to_vec(),
1401 vec![(one_of(&[b"h2"]), "cluster-h2".into())],
1402 );
1403 let cfg = cfg(&routes);
1404 let mut core = SniPrereadCore::new();
1405 assert_eq!(
1406 feed(&mut core, &cfg, &hello_no_alpn("example.com")),
1407 Output::Reject(RejectReason::AlpnUnmatched),
1408 "a no-ALPN client with no Any entry must be unmatched, not silently routed"
1409 );
1410 }
1411}