mod parser;
pub mod shell;
use std::{
collections::BTreeSet,
net::SocketAddr,
time::{Duration, Instant},
};
use nom::Err as NomErr;
use sozu_command::state::ClusterId;
use crate::{protocol::proxy_protocol::parser::parse_v2_header, router::pattern_trie::TrieNode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AlpnMatcher {
Any,
OneOf(BTreeSet<Vec<u8>>),
}
#[derive(Debug, Clone, Copy)]
pub struct PrereadConfig<'t> {
pub routes: &'t TrieNode<Vec<(AlpnMatcher, ClusterId)>>,
pub inbound_proxy: bool,
pub max_bytes: usize,
pub timeout: Duration,
pub accept_wildcard: bool,
}
#[derive(Debug)]
pub enum Input<'a> {
Bytes { buf: &'a [u8], now: Instant },
Timeout { now: Instant },
FrontClosed,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Output {
NeedMore { deadline: Instant },
Routed {
cluster: ClusterId,
content_offset: usize,
proxy_source: Option<SocketAddr>,
sni: String,
alpn: Vec<Vec<u8>>,
matched_sni_pattern: String,
matched_alpn: AlpnMatcher,
},
Reject(RejectReason),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RejectReason {
NotTls,
MalformedRecord,
MalformedHandshake,
Fragmented,
TooLarge,
NoSni,
EchOuterAbsent,
SniUnmatched,
AlpnUnmatched,
ProxyHeaderInvalid,
FrontClosed,
}
#[derive(Debug, Default)]
pub struct SniPrereadCore {
decided: Option<Output>,
deadline: Option<Instant>,
}
impl SniPrereadCore {
pub fn new() -> Self {
SniPrereadCore {
decided: None,
deadline: None,
}
}
pub fn handle_input(&mut self, cfg: &PrereadConfig<'_>, input: Input<'_>) -> Output {
#[cfg(debug_assertions)]
let deadline_before = self.deadline;
#[cfg(debug_assertions)]
let was_decided = self.decided.is_some();
self.debug_assert_invariants();
let output = match input {
Input::Bytes { buf, now } => self.on_bytes(cfg, buf, now),
Input::Timeout { now } => self.on_timeout(now),
Input::FrontClosed => self.on_front_closed(),
};
#[cfg(debug_assertions)]
{
if let Some(before) = deadline_before {
debug_assert_eq!(
self.deadline,
Some(before),
"preread deadline must be set at most once and never rewound"
);
}
debug_assert!(
!was_decided || self.decided.is_some(),
"a decided core must never become undecided (monotonic latch)"
);
}
self.debug_assert_invariants();
output
}
fn on_bytes(&mut self, cfg: &PrereadConfig<'_>, buf: &[u8], now: Instant) -> Output {
if self.deadline.is_none() {
self.deadline = Some(now + cfg.timeout);
}
let deadline = self
.deadline
.expect("deadline was just armed unconditionally above if it was None");
if let Some(decided) = &self.decided {
return decided.clone();
}
if now >= deadline {
return self.decide(Output::Reject(RejectReason::Fragmented));
}
let (content_offset, proxy_source) = if cfg.inbound_proxy {
match parse_v2_header(buf) {
Ok((rest, header)) => {
let consumed = buf.len() - rest.len();
debug_assert!(
consumed <= buf.len(),
"a parsed PROXY-v2 header cannot consume more than the buffer holds"
);
(consumed, header.addr.source())
}
Err(NomErr::Incomplete(_)) => {
return self.need_more_or_too_large(cfg, buf, deadline);
}
Err(_) => return self.decide(Output::Reject(RejectReason::ProxyHeaderInvalid)),
}
} else {
(0, None)
};
match parser::parse_client_hello(&buf[content_offset..]) {
parser::ParseOutcome::NeedMore => self.need_more_or_too_large(cfg, buf, deadline),
parser::ParseOutcome::Reject(reason) => self.decide(Output::Reject(reason)),
parser::ParseOutcome::ClientHello {
sni,
alpn,
ech_present,
} => self.route(
cfg,
buf,
content_offset,
proxy_source,
sni,
alpn,
ech_present,
),
}
}
fn on_timeout(&mut self, _now: Instant) -> Output {
if let Some(decided) = &self.decided {
return decided.clone();
}
self.decide(Output::Reject(RejectReason::Fragmented))
}
fn on_front_closed(&mut self) -> Output {
if let Some(decided) = &self.decided {
return decided.clone();
}
self.decide(Output::Reject(RejectReason::FrontClosed))
}
fn need_more_or_too_large(
&mut self,
cfg: &PrereadConfig<'_>,
buf: &[u8],
deadline: Instant,
) -> Output {
if buf.len() >= cfg.max_bytes {
return self.decide(Output::Reject(RejectReason::TooLarge));
}
Output::NeedMore { deadline }
}
#[allow(clippy::too_many_arguments)]
fn route(
&mut self,
cfg: &PrereadConfig<'_>,
buf: &[u8],
content_offset: usize,
proxy_source: Option<SocketAddr>,
sni_raw: Option<String>,
alpn: Vec<Vec<u8>>,
ech_present: bool,
) -> Output {
let sni = sni_raw.map(normalize_sni);
let sni = match sni {
Some(s) if !s.is_empty() => s,
_ => {
let reason = if ech_present {
RejectReason::EchOuterAbsent
} else {
RejectReason::NoSni
};
return self.decide(Output::Reject(reason));
}
};
let Some((matched_key, entries)) = cfg
.routes
.domain_lookup(sni.as_bytes(), cfg.accept_wildcard)
else {
return self.decide(Output::Reject(RejectReason::SniUnmatched));
};
let Some((matched_alpn, cluster)) = resolve_alpn(entries, &alpn) else {
return self.decide(Output::Reject(RejectReason::AlpnUnmatched));
};
#[cfg(debug_assertions)]
{
let (_, replay_entries) = cfg
.routes
.domain_lookup(sni.as_bytes(), cfg.accept_wildcard)
.expect("a route that just matched must match again");
let replay_entry = resolve_alpn(replay_entries, &alpn);
debug_assert_eq!(
replay_entry,
Some((matched_alpn, cluster)),
"route lookup + ALPN resolution must be deterministic for the same (sni, alpn)"
);
}
debug_assert!(
content_offset <= buf.len(),
"content_offset must never exceed the buffer it was derived from"
);
self.decide(Output::Routed {
cluster: cluster.to_owned(),
content_offset,
proxy_source,
sni,
alpn,
matched_sni_pattern: String::from_utf8_lossy(matched_key).into_owned(),
matched_alpn: matched_alpn.clone(),
})
}
fn decide(&mut self, output: Output) -> Output {
debug_assert!(
self.decided.is_none(),
"decide() must be called at most once per lifetime -- the latch is monotonic"
);
debug_assert!(
!matches!(output, Output::NeedMore { .. }),
"only a terminal Output (Routed/Reject) may be latched via decide()"
);
self.decided = Some(output.clone());
debug!(
"{} latched terminal preread decision: {:?}",
log_context!(self),
output
);
output
}
#[cfg(debug_assertions)]
fn check_invariants(&self) {
debug_assert!(
!matches!(self.decided, Some(Output::NeedMore { .. })),
"decided latch must never store NeedMore -- it is not a terminal verdict"
);
if let Some(Output::Routed {
sni,
matched_sni_pattern,
..
}) = &self.decided
{
debug_assert_eq!(
sni,
&sni.to_ascii_lowercase(),
"latched SNI must already be lowercase-normalized"
);
debug_assert!(
!sni.ends_with('.'),
"latched SNI must not carry a trailing dot after normalization"
);
debug_assert_eq!(
matched_sni_pattern,
&matched_sni_pattern.to_ascii_lowercase(),
"latched matched_sni_pattern must already be lowercase (trie keys are lowercased at insert)"
);
debug_assert!(
matched_sni_pattern == sni
|| matched_sni_pattern
.strip_prefix("*.")
.is_some_and(|suffix| sni.ends_with(suffix)),
"matched_sni_pattern must be the SNI itself (exact route) or a wildcard the SNI falls under"
);
}
}
#[inline]
fn debug_assert_invariants(&self) {
#[cfg(debug_assertions)]
self.check_invariants();
}
}
fn resolve_alpn<'r>(
entries: &'r [(AlpnMatcher, ClusterId)],
offered: &[Vec<u8>],
) -> Option<(&'r AlpnMatcher, &'r ClusterId)> {
for protocol in offered {
for (matcher, cluster) in entries {
if let AlpnMatcher::OneOf(set) = matcher
&& set.contains(protocol)
{
return Some((matcher, cluster));
}
}
}
entries
.iter()
.find(|(matcher, _)| matches!(matcher, AlpnMatcher::Any))
.map(|(matcher, cluster)| (matcher, cluster))
}
fn normalize_sni(mut sni: String) -> String {
sni.make_ascii_lowercase();
if sni.ends_with('.') {
sni.pop();
}
sni
}
#[allow(unused_macros)]
macro_rules! log_context {
($self:expr) => {{
let (open, reset, grey, gray, white) = sozu_command::logging::ansi_palette();
format!(
"[- - - -]\t{open}TCP-SNI{reset}\t{grey}Preread{reset}({gray}decided{reset}={white}{decided}{reset}, {gray}deadline_armed{reset}={white}{deadline}{reset})\t >>>",
open = open,
reset = reset,
grey = grey,
gray = gray,
white = white,
decided = $self.decided.is_some(),
deadline = $self.deadline.is_some(),
)
}};
}
#[allow(unused_macros)]
macro_rules! log_context_lite {
($sni:expr) => {{
let (open, reset, grey, gray, white) = sozu_command::logging::ansi_palette();
format!(
"[- - - -]\t{open}TCP-SNI{reset}\t{grey}Route{reset}({gray}sni{reset}={white}{sni:?}{reset})\t >>>",
open = open,
reset = reset,
grey = grey,
gray = gray,
white = white,
sni = $sni,
)
}};
}
#[allow(unused_imports)]
use {log_context, log_context_lite};
#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr};
use super::*;
use crate::protocol::proxy_protocol::header::{Command, HeaderV2};
fn cfg<'t>(routes: &'t TrieNode<Vec<(AlpnMatcher, ClusterId)>>) -> PrereadConfig<'t> {
PrereadConfig {
routes,
inbound_proxy: false,
max_bytes: 16 * 1024,
timeout: Duration::from_secs(3),
accept_wildcard: true,
}
}
fn one_of(protocols: &[&[u8]]) -> AlpnMatcher {
AlpnMatcher::OneOf(protocols.iter().map(|p| p.to_vec()).collect())
}
fn hello(sni: &str, alpn: &[&[u8]]) -> Vec<u8> {
parser::build_client_hello_wire(&[
parser::encode_sni_extension(sni),
parser::encode_alpn_extension(alpn),
])
}
fn hello_no_alpn(sni: &str) -> Vec<u8> {
parser::build_client_hello_wire(&[parser::encode_sni_extension(sni)])
}
fn feed(core: &mut SniPrereadCore, cfg: &PrereadConfig<'_>, buf: &[u8]) -> Output {
core.handle_input(
cfg,
Input::Bytes {
buf,
now: Instant::now(),
},
)
}
#[test]
fn byte_replay_untouched_through_the_core() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let cfg = cfg(&routes);
let wire = hello("example.com", &[]);
let before = wire.clone();
let mut core = SniPrereadCore::new();
let _ = feed(&mut core, &cfg, &wire);
assert_eq!(wire, before, "the core must never mutate the fed buffer");
}
#[test]
fn too_large_is_reachable() {
let routes = TrieNode::root();
let mut small_cfg = cfg(&routes);
small_cfg.max_bytes = 8;
let wire = hello_no_alpn("example.com");
let mut core = SniPrereadCore::new();
let out = feed(&mut core, &small_cfg, &wire[..10]);
assert_eq!(out, Output::Reject(RejectReason::TooLarge));
}
#[test]
fn exact_cap_boundary_complete_routes_incomplete_rejects() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let wire = hello_no_alpn("example.com");
let mut exact_cfg = cfg(&routes);
exact_cfg.max_bytes = wire.len();
let mut core = SniPrereadCore::new();
match feed(&mut core, &exact_cfg, &wire) {
Output::Routed { cluster, .. } => assert_eq!(cluster, "cluster-a"),
other => panic!("expected Routed at exactly the cap, got {other:?}"),
}
let mut tight_cfg = cfg(&routes);
tight_cfg.max_bytes = wire.len() - 1;
let mut core = SniPrereadCore::new();
assert_eq!(
feed(&mut core, &tight_cfg, &wire[..wire.len() - 1]),
Output::Reject(RejectReason::TooLarge),
"an incomplete window at exactly the cap must reject TooLarge"
);
}
#[test]
fn complete_hello_with_trailing_bytes_over_cap_routes() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let wire = hello("example.com", &[b"h2"]);
let mut over = wire.clone();
over.extend_from_slice(&[0xAB; 64]);
let mut small_cfg = cfg(&routes);
small_cfg.max_bytes = wire.len() + 16; assert!(over.len() > small_cfg.max_bytes, "test setup: total > cap");
let mut core = SniPrereadCore::new();
match feed(&mut core, &small_cfg, &over) {
Output::Routed {
cluster,
content_offset,
sni,
..
} => {
assert_eq!(cluster, "cluster-a");
assert_eq!(content_offset, 0);
assert_eq!(sni, "example.com");
}
other => panic!("a complete hello must route despite trailing bytes, got {other:?}"),
}
}
#[test]
fn proxy_incomplete_at_cap_is_too_large() {
let routes = TrieNode::root();
let mut proxy_cfg = cfg(&routes);
proxy_cfg.inbound_proxy = true;
proxy_cfg.max_bytes = 10;
let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 9)), 443);
let proxy_header = HeaderV2::new(Command::Proxy, src, dst).into_bytes();
let mut core = SniPrereadCore::new();
let out = feed(&mut core, &proxy_cfg, &proxy_header[..10]);
assert_eq!(out, Output::Reject(RejectReason::TooLarge));
}
#[test]
fn empty_buffer_needs_more_and_sets_deadline() {
let routes = TrieNode::root();
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
match feed(&mut core, &cfg, &[]) {
Output::NeedMore { .. } => {}
other => panic!("expected NeedMore on empty input, got {other:?}"),
}
}
#[test]
fn timeout_while_undecided_is_fragmented() {
let routes = TrieNode::root();
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
let wire = hello_no_alpn("example.com");
let _ = feed(&mut core, &cfg, &wire[..wire.len() - 1]);
let out = core.handle_input(
&cfg,
Input::Timeout {
now: Instant::now(),
},
);
assert_eq!(out, Output::Reject(RejectReason::Fragmented));
}
#[test]
fn front_closed_before_decision_is_front_closed() {
let routes = TrieNode::root();
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
let out = core.handle_input(&cfg, Input::FrontClosed);
assert_eq!(out, Output::Reject(RejectReason::FrontClosed));
}
#[test]
fn decided_latch_replays_the_same_terminal_forever() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
let wire = hello_no_alpn("example.com");
let first = feed(&mut core, &cfg, &wire);
let second = feed(&mut core, &cfg, &[0xff; 3]);
assert_eq!(first, second);
let third = core.handle_input(
&cfg,
Input::Timeout {
now: Instant::now(),
},
);
assert_eq!(first, third);
}
#[test]
fn not_tls_is_reachable_through_the_core() {
let routes = TrieNode::root();
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
let out = feed(&mut core, &cfg, &[0x16 + 1, 0x03, 0x03, 0x00, 0x00]);
assert_eq!(out, Output::Reject(RejectReason::NotTls));
}
#[test]
fn no_sni_is_reachable() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
let wire = parser::build_client_hello_wire(&[]);
assert_eq!(
feed(&mut core, &cfg, &wire),
Output::Reject(RejectReason::NoSni)
);
}
#[test]
fn ech_outer_absent_is_reachable() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
let wire = parser::build_client_hello_wire(&[parser::encode_extension(
0xfe0d,
&[0x00, 0x01, 0x02],
)]);
assert_eq!(
feed(&mut core, &cfg, &wire),
Output::Reject(RejectReason::EchOuterAbsent)
);
}
#[test]
fn sni_unmatched_is_reachable() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
let wire = hello_no_alpn("unknown.example.net");
assert_eq!(
feed(&mut core, &cfg, &wire),
Output::Reject(RejectReason::SniUnmatched)
);
}
#[test]
fn alpn_unmatched_is_reachable() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(one_of(&[b"h2"]), "cluster-a".to_owned())],
);
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
let wire = hello("example.com", &[b"http/1.1"]);
assert_eq!(
feed(&mut core, &cfg, &wire),
Output::Reject(RejectReason::AlpnUnmatched)
);
}
#[test]
fn proxy_header_invalid_is_reachable() {
let routes = TrieNode::root();
let mut proxy_cfg = cfg(&routes);
proxy_cfg.inbound_proxy = true;
let mut core = SniPrereadCore::new();
let out = feed(&mut core, &proxy_cfg, &[0xAAu8; 16]);
assert_eq!(out, Output::Reject(RejectReason::ProxyHeaderInvalid));
}
#[test]
fn malformed_record_is_reachable() {
let routes = TrieNode::root();
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
let record = [0x16, 0x03, 0x03, 0xFF, 0xFF];
assert_eq!(
feed(&mut core, &cfg, &record),
Output::Reject(RejectReason::MalformedRecord)
);
}
#[test]
fn malformed_handshake_is_reachable() {
let routes = TrieNode::root();
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
let hs = parser::wrap_handshake(&[]); let mut bad_hs = hs;
bad_hs[0] = 2; let record = parser::wrap_record(22, &bad_hs);
assert_eq!(
feed(&mut core, &cfg, &record),
Output::Reject(RejectReason::MalformedHandshake)
);
}
#[test]
fn drip_feed_needs_more_then_routes() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let cfg = cfg(&routes);
let wire = hello("example.com", &[b"h2"]);
let mut core = SniPrereadCore::new();
for i in 0..wire.len() {
match feed(&mut core, &cfg, &wire[..i]) {
Output::NeedMore { .. } => {}
other => panic!("prefix {i} of {} must NeedMore, got {other:?}", wire.len()),
}
}
match feed(&mut core, &cfg, &wire) {
Output::Routed { cluster, sni, .. } => {
assert_eq!(cluster, "cluster-a");
assert_eq!(sni, "example.com");
}
other => panic!("expected Routed at full length, got {other:?}"),
}
}
#[test]
fn bytes_at_or_after_the_latched_deadline_reject_fragmented_without_moving_it() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let mut cfg = cfg(&routes);
cfg.timeout = Duration::from_secs(5);
let wire = hello_no_alpn("example.com");
let incomplete = &wire[..wire.len() - 1];
let mut core = SniPrereadCore::new();
let start = Instant::now();
let deadline = match core.handle_input(
&cfg,
Input::Bytes {
buf: incomplete,
now: start,
},
) {
Output::NeedMore { deadline } => deadline,
other => panic!("expected NeedMore on the first incomplete fragment, got {other:?}"),
};
assert_eq!(deadline, start + cfg.timeout);
let almost_deadline = deadline - Duration::from_millis(1);
match core.handle_input(
&cfg,
Input::Bytes {
buf: incomplete,
now: almost_deadline,
},
) {
Output::NeedMore { deadline: d } => {
assert_eq!(d, deadline, "the latched deadline must never move")
}
other => panic!("expected NeedMore just before the deadline, got {other:?}"),
}
assert_eq!(
core.handle_input(
&cfg,
Input::Bytes {
buf: &wire,
now: deadline,
},
),
Output::Reject(RejectReason::Fragmented),
"a complete hello arriving at/after the latched deadline must reject Fragmented, not route"
);
assert_eq!(
core.handle_input(
&cfg,
Input::Bytes {
buf: &wire,
now: deadline + Duration::from_secs(1),
},
),
Output::Reject(RejectReason::Fragmented)
);
}
#[test]
fn multi_record_client_hello_routes_through_the_core() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"split.example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-split".to_owned())],
);
let cfg = cfg(&routes);
let wire = hello("split.example.com", &[b"h2"]);
let split = parser::split_into_records(&wire, 4);
let mut core = SniPrereadCore::new();
match feed(&mut core, &cfg, &split) {
Output::Routed { cluster, .. } => assert_eq!(cluster, "cluster-split"),
other => panic!("expected Routed for a multi-record ClientHello, got {other:?}"),
}
}
#[test]
fn proxy_v2_prefix_yields_content_offset_and_source() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let mut proxy_cfg = cfg(&routes);
proxy_cfg.inbound_proxy = true;
let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 9)), 443);
let proxy_header = HeaderV2::new(Command::Proxy, src, dst).into_bytes();
assert_eq!(proxy_header.len(), 28);
let mut wire = proxy_header.clone();
wire.extend_from_slice(&hello_no_alpn("example.com"));
let mut core = SniPrereadCore::new();
match feed(&mut core, &proxy_cfg, &wire) {
Output::Routed {
cluster,
content_offset,
proxy_source,
..
} => {
assert_eq!(cluster, "cluster-a");
assert_eq!(content_offset, proxy_header.len());
assert_eq!(proxy_source, Some(src));
}
other => panic!("expected Routed behind a PROXY-v2 header, got {other:?}"),
}
}
#[test]
fn proxy_v2_prefix_drip_feed_needs_more_until_complete() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(AlpnMatcher::Any, "cluster-a".to_owned())],
);
let mut proxy_cfg = cfg(&routes);
proxy_cfg.inbound_proxy = true;
let src = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(203, 0, 113, 7)), 51234);
let dst = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(198, 51, 100, 9)), 443);
let proxy_header = HeaderV2::new(Command::Proxy, src, dst).into_bytes();
let mut wire = proxy_header.clone();
wire.extend_from_slice(&hello_no_alpn("example.com"));
let mut core = SniPrereadCore::new();
for i in 0..proxy_header.len() {
match feed(&mut core, &proxy_cfg, &wire[..i]) {
Output::NeedMore { .. } => {}
other => panic!("PROXY-header prefix {i} must NeedMore, got {other:?}"),
}
}
}
#[test]
fn exact_sni_beats_wildcard_before_alpn() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"*.example.com".to_vec(),
vec![(AlpnMatcher::Any, "wildcard-cluster".to_owned())],
);
routes.domain_insert(
b"a.example.com".to_vec(),
vec![(one_of(&[b"h2"]), "exact-cluster".to_owned())],
);
let cfg = cfg(&routes);
let wire = hello("a.example.com", &[b"http/1.1"]);
let mut core = SniPrereadCore::new();
assert_eq!(
feed(&mut core, &cfg, &wire),
Output::Reject(RejectReason::AlpnUnmatched)
);
}
#[test]
fn wildcard_matches_one_level_not_two_not_apex() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"*.example.com".to_vec(),
vec![(AlpnMatcher::Any, "wildcard-cluster".to_owned())],
);
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
match feed(&mut core, &cfg, &hello_no_alpn("a.example.com")) {
Output::Routed {
cluster,
sni,
matched_sni_pattern,
matched_alpn,
..
} => {
assert_eq!(cluster, "wildcard-cluster");
assert_eq!(sni, "a.example.com");
assert_eq!(matched_sni_pattern, "*.example.com");
assert_eq!(matched_alpn, AlpnMatcher::Any);
}
other => panic!("*.example.com must match a.example.com, got {other:?}"),
}
let mut core = SniPrereadCore::new();
assert_eq!(
feed(&mut core, &cfg, &hello_no_alpn("a.b.example.com")),
Output::Reject(RejectReason::SniUnmatched),
"*.example.com must NOT match a.b.example.com"
);
let mut core = SniPrereadCore::new();
assert_eq!(
feed(&mut core, &cfg, &hello_no_alpn("example.com")),
Output::Reject(RejectReason::SniUnmatched),
"*.example.com must NOT match the apex example.com"
);
}
#[test]
fn alpn_client_preference_order_wins() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![
(one_of(&[b"http/1.1"]), "cluster-http11".to_owned()),
(one_of(&[b"h2"]), "cluster-h2".to_owned()),
],
);
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
match feed(
&mut core,
&cfg,
&hello("example.com", &[b"h2", b"http/1.1"]),
) {
Output::Routed { cluster, .. } => assert_eq!(cluster, "cluster-h2"),
other => panic!("expected the client's first preference, got {other:?}"),
}
let mut core = SniPrereadCore::new();
match feed(
&mut core,
&cfg,
&hello("example.com", &[b"http/1.1", b"h2"]),
) {
Output::Routed { cluster, .. } => assert_eq!(cluster, "cluster-http11"),
other => panic!("expected the client's first preference, got {other:?}"),
}
}
#[test]
fn alpn_any_catch_all_and_no_alpn_client_matches_any_only() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![
(one_of(&[b"h2"]), "cluster-h2".to_owned()),
(AlpnMatcher::Any, "cluster-default".to_owned()),
],
);
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
match feed(&mut core, &cfg, &hello("example.com", &[b"spdy/1"])) {
Output::Routed { cluster, .. } => assert_eq!(cluster, "cluster-default"),
other => panic!("expected the Any catch-all, got {other:?}"),
}
let mut core = SniPrereadCore::new();
match feed(&mut core, &cfg, &hello_no_alpn("example.com")) {
Output::Routed { cluster, alpn, .. } => {
assert_eq!(cluster, "cluster-default");
assert!(alpn.is_empty());
}
other => panic!("expected the Any catch-all for a no-ALPN client, got {other:?}"),
}
}
#[test]
fn no_any_and_no_matching_one_of_is_alpn_unmatched() {
let mut routes = TrieNode::root();
routes.domain_insert(
b"example.com".to_vec(),
vec![(one_of(&[b"h2"]), "cluster-h2".to_owned())],
);
let cfg = cfg(&routes);
let mut core = SniPrereadCore::new();
assert_eq!(
feed(&mut core, &cfg, &hello_no_alpn("example.com")),
Output::Reject(RejectReason::AlpnUnmatched),
"a no-ALPN client with no Any entry must be unmatched, not silently routed"
);
}
}