Skip to main content

sozu_lib/protocol/mux/
router.rs

1//! Backend routing and connection reuse for the mux layer.
2//!
3//! [`Router`] owns the map of token -> backend [`Connection`] and centralises
4//! the logic for picking (or opening) the right backend for an incoming
5//! request. The H2 reuse strategy prefers the least-loaded non-draining
6//! connection of the target cluster; H1 falls back to keep-alive reuse.
7//!
8//! ## Determinism (nothing mandates a tie-break order; issue #1338 does)
9//!
10//! [`Router::backends`] used to be a `HashMap<Token, _>`, and
11//! `Router::plan_connect` scans it with a plain `for (token, backend) in
12//! &self.backends` loop, in the map's iteration order — seeded per-`HashMap`
13//! (`RandomState`), so **which backend served a request was non-deterministic
14//! across process restarts**, for the identical set of backend connections and
15//! the identical request. That is a strictly wider leak than the wire-order
16//! ones `h2_flow_control` and `h2_stream_table` closed: it
17//! does not reorder bytes on one connection, it sends them to a different
18//! machine.
19//!
20//! Three separate decisions inside that one loop read the order, and they do
21//! not read it the same way:
22//!
23//! - the H2 least-loaded arm compares with a strict `<`, so the FIRST
24//!   connection at the minimum stream count wins — on a tie, map order alone
25//!   picks the backend;
26//! - the H2 `BackendStatus::Connecting` fallback assigns with neither a
27//!   `break` nor an "already chosen" guard, so the LAST matching connecting
28//!   connection wins;
29//! - the H1 `BackendStatus::KeepAlive` arm assigns and breaks, so the FIRST
30//!   matching keep-alive socket wins.
31//!
32//! The map is now a [`BackendConnections`] (a `BTreeMap` behind one inline
33//! slot for the lowest token, #1610), so the scan is the total order on
34//! `Token` — fixed, reproducible, and free (its iteration is already sorted;
35//! no extra sort step, no injected seed to thread through construction and
36//! tests). The three decisions above resolve to the lowest `Token`, the
37//! highest `Token` and the lowest `Token` respectively. Pinning is all this
38//! does: the last-wins shape of the connecting fallback is preserved, not
39//! corrected, because changing which backend is preferred is a routing change
40//! and not a determinism one.
41//!
42//! A `Token` is the slab index the proxy's session manager handed the backend
43//! socket at dial time, and it carries no fairness or recency meaning of its
44//! own: `slab::Slab::try_remove` pushes the freed index onto the head of its
45//! vacant list (`self.next = key`) and `insert` pops that head, so a dial
46//! reuses the most recently RELEASED index, not the lowest and not the
47//! oldest. The total order is a stable arbitrary label. It does not need to
48//! be more than that for the
49//! least-loaded arm, and for the reason `h2_stream_table`'s doc had
50//! to spell out and `h2_flow_control`'s could not: a tie there does
51//! not persist across requests. Attaching the stream increments the winner's
52//! `ConnectionH2::stream_count`, so the next request sees it above the minimum
53//! and takes the next token — the load counter is itself the fairness cursor
54//! `pending_window_updates` had to note the absence of. The connecting
55//! fallback has no such counter, but every candidate it ranks is a connection
56//! that cannot serve anything yet, and it concentrated every waiting stream on
57//! one of them before this change too; the total order fixes WHICH one, it
58//! does not change how many.
59//!
60//! ## Complexity — UNMEASURED beyond the default configuration
61//!
62//! `n` here is the number of live backend connections on ONE frontend session,
63//! not a worker-wide or cluster-wide count. An H2 frontend reaching an H2
64//! cluster holds roughly one multiplex slot per cluster it touches. An H2
65//! frontend reaching an H1 cluster cannot bundle streams, so it holds up to
66//! one socket per concurrent stream — bounded by the `max_concurrent_streams`
67//! this proxy advertises, `DEFAULT_MAX_CONCURRENT_STREAMS` (100) by default
68//! and clamped by `H2ConnectionConfig::new` to `MAX_SAFE_CONCURRENT_STREAMS`
69//! (10_000).
70//!
71//! What changes complexity class is the point lookup: `get` / `get_mut` /
72//! `insert` / `remove` / `contains_key` go from `HashMap`'s amortized `O(1)`
73//! to `BTreeMap`'s `O(log n)` (the inline slot adds one comparison), and
74//! `Mux::ready` does one `get_mut`
75//! per backend event through `EndpointClient`. Every iteration site —
76//! `Router::plan_connect`'s scan, `Mux::reschedule`, the two
77//! `Mux::ready` sweeps — already walks all `n` and keeps its `O(n)`. At the
78//! default ceiling (`n <= 100`) a lookup is ~7 `usize` comparisons against one
79//! SipHash-1-3 of a `usize` plus a bucket probe, which is not expected to be
80//! measurable; at a listener configured near `MAX_SAFE_CONCURRENT_STREAMS`
81//! (`n` up to 10_000, ~14 comparisons) it stops being self-evidently
82//! negligible. That expectation has **not been benchmarked**, neither before
83//! nor after this change. Closing it requires a before/after run of this
84//! repository's `Bombardier bench` CI job (or an equivalent local
85//! `bombardier` run) against an H1 cluster behind an H2 frontend at or near
86//! that ceiling; no such run has been produced.
87
88use std::{
89    cell::RefCell,
90    collections::{BTreeMap, HashMap},
91    net::{IpAddr, SocketAddr},
92    rc::Rc,
93    time::Duration,
94};
95
96use kawa::Block;
97use mio::Token;
98use sozu_command::{
99    logging::ansi_palette,
100    proto::command::{
101        Cluster, ListenerType, LoadBalancingAlgorithms, RedirectPolicy, RedirectScheme,
102    },
103    state::ClusterId,
104};
105
106#[cfg(debug_assertions)]
107use super::DebugEvent;
108use super::{
109    BackendId, BackendStatus, Connection, Context, GenericHttpStream, GlobalStreamId, Position,
110    StreamState,
111};
112use crate::{
113    BackendConnectionError, L7ListenerHandler, ListenerHandler, Readiness, RetrieveClusterError,
114    backends::BackendError,
115    load_balancing::{affinity_key_from_ip, affinity_key_from_value},
116    protocol::http::{
117        editor::{HeaderEditMode, HeaderEditSnapshot, HttpContext, emits_x_forwarded},
118        parser::compare_no_case,
119    },
120    router::{HeaderEdit, RequestHeaders, RouteResult},
121    socket::SessionTcpStream,
122};
123
124use crate::metrics::names;
125
126/// Module-level prefix used on every log line emitted from the router.
127///
128/// Two arms:
129/// * `log_module_context!()` — zero-arg, legacy `MUX-ROUTER\t >>>` output.
130///   Kept for sites without an `HttpContext` in scope. No call site in this
131///   module currently uses this arm (every one has an `HttpContext` reachable
132///   via [`Context::http_context`] or a direct `&mut HttpContext`
133///   parameter), but the arm is retained so the macro name stays stable for
134///   future sessionless callers.
135/// * `log_module_context!($http_context)` — rich form. `$http_context` must be
136///   `&HttpContext` (or coerce to one). Produces the same
137///   `[session req cluster backend]` bracket as RUSTLS/PIPE/TCP followed by a
138///   `Session(frontend=..., method=..., authority_bytes=...)` block, so router
139///   lines are filterable by session ULID or request ULID. `cluster_id` is
140///   already carried by the bracket's third slot — not duplicated inside
141///   `Session(...)`.
142macro_rules! log_module_context {
143    () => {{
144        let (open, reset, _, _, _) = ansi_palette();
145        format!("{open}MUX-ROUTER{reset}\t >>>", open = open, reset = reset)
146    }};
147    ($http_context:expr) => {{
148        let (open, reset, grey, gray, white) = ansi_palette();
149        let http_ctx: &HttpContext = &$http_context;
150        let ctx = http_ctx.log_context();
151        format!(
152            "{gray}{ctx}{reset}\t{open}MUX-ROUTER{reset}\t{grey}Session{reset}({gray}frontend{reset}={white}{frontend:?}{reset}, {gray}method{reset}={white}{method:?}{reset}, {gray}authority_bytes{reset}={white}{authority_bytes:?}{reset})\t >>>",
153            open = open,
154            reset = reset,
155            grey = grey,
156            gray = gray,
157            white = white,
158            ctx = ctx,
159            frontend = http_ctx.session_address,
160            method = http_ctx.method,
161            authority_bytes = http_ctx.authority.as_ref().map(String::len),
162        )
163    }};
164}
165
166fn log_coalescing_accepted(context: &HttpContext, authority: &str, sni: &str, matched_name: &str) {
167    debug!(
168        "{} accepted coalesced authority (authority_bytes={}, sni_bytes={}, matched_san_kind={}, matched_san_bytes={})",
169        log_module_context!(context),
170        authority.len(),
171        sni.len(),
172        if matched_name.starts_with("*.") {
173            "wildcard"
174        } else {
175            "exact"
176        },
177        matched_name.len(),
178    );
179}
180
181fn log_sni_authority_mismatch(
182    context: &HttpContext,
183    authority: &str,
184    sni: &str,
185    certificate_names: Option<&[String]>,
186) {
187    let certificate_sans_count = certificate_names.map_or(0, <[String]>::len);
188    let certificate_sans_bytes = certificate_names
189        .into_iter()
190        .flatten()
191        .map(String::len)
192        .fold(0usize, usize::saturating_add);
193    warn!(
194        "{} rejecting request: TLS cert SANs do not cover authority (authority_bytes={}, sni_bytes={}, certificate_sans_count={}, certificate_sans_bytes={})",
195        log_module_context!(context),
196        authority.len(),
197        sni.len(),
198        certificate_sans_count,
199        certificate_sans_bytes,
200    );
201}
202
203/// The cluster-side configuration the routing decision reads, borrowed from
204/// the embedder for the length of one decision.
205///
206/// Question 6 of [#1340](https://github.com/sozu-proxy/sozu/issues/1340) puts
207/// routing inside the core; this is how the data that routing needs gets
208/// there without the core holding the handle it used to reach through.
209/// `L7Proxy::clusters` and `L7Proxy::kind` were two of the three reads
210/// `Router::plan_connect` made against `Rc<RefCell<dyn L7Proxy>>`, and both
211/// are pure. They arrive as this instead.
212///
213/// The mechanism is Question 12's, applied a second time rather than
214/// reinvented: a view in with a defined staleness, a result out. The
215/// staleness here is **one routing decision** — the view is built once per
216/// `Router::plan_connect` call and every read inside that call sees the same
217/// cluster map.
218///
219/// That is marginally *stronger* than what it replaces, and deliberately so:
220/// the two sites used to take separate `proxy.borrow()`s, one in
221/// `Router::plan_connect` and one in `Router::route_from_request`, so a
222/// cluster update landing between them would have been half-observed. No
223/// such update can interleave — `clusters()` is owned by the worker's
224/// command loop and a stream is drained from `pending_links` inside the very
225/// `ready()` pass that queued it — so this closes a window that was never
226/// open rather than changing a behaviour that was.
227///
228/// A simulator can build one of these from whatever cluster map it likes,
229/// which is what Q6's determinism argument wants and what an
230/// `Rc<RefCell<dyn L7Proxy>>` could never give it.
231pub(super) struct RoutingView<'a> {
232    clusters: &'a HashMap<ClusterId, Cluster>,
233    listener_kind: ListenerType,
234    /// Whether the backend a pooled connection was dialled to has left the
235    /// configuration. `None` treats every backend as current.
236    backend_retired: Option<&'a dyn Fn(&BackendId) -> bool>,
237    /// The worker's backend connection attempt budget, for a cluster that
238    /// sets no `max_connection_attempts` of its own.
239    max_connection_attempts: u32,
240}
241
242impl<'a> RoutingView<'a> {
243    pub(super) fn new(
244        clusters: &'a HashMap<ClusterId, Cluster>,
245        listener_kind: ListenerType,
246    ) -> Self {
247        Self {
248            clusters,
249            listener_kind,
250            backend_retired: None,
251            max_connection_attempts: sozu_command::config::DEFAULT_MAX_CONNECTION_ATTEMPTS,
252        }
253    }
254
255    /// Use `attempts` as the backend connection attempt budget of a request
256    /// whose cluster sets none: the worker's global value,
257    /// `SessionManager::max_connection_attempts` (`lib/src/server.rs`).
258    pub(super) fn with_max_connection_attempts(mut self, attempts: u32) -> Self {
259        self.max_connection_attempts = attempts;
260        self
261    }
262
263    /// The backend connection attempt budget of a request routed to
264    /// `cluster_id`: the cluster's own `max_connection_attempts` when it sets
265    /// one, the worker's otherwise (sozu-proxy/sozu#1800).
266    fn max_connection_attempts(&self, cluster_id: Option<&str>) -> u32 {
267        cluster_id
268            .and_then(|cluster_id| self.cluster(cluster_id))
269            .and_then(|cluster| cluster.max_connection_attempts)
270            .unwrap_or(self.max_connection_attempts)
271    }
272
273    /// Answer the pool-reuse scan's question "is this connection's backend
274    /// still configured?" with `retired`.
275    ///
276    /// A backend connection names its cluster by id, and a cluster id outlives
277    /// a `RemoveCluster` followed by an `AddCluster` of the same id, as it
278    /// outlives a `RemoveBackend`. Only the embedder can tell whether the
279    /// backend behind a [`BackendId`] is still part of the configuration, so
280    /// the reuse scan asks it here and never reuses a connection whose backend
281    /// is retired: that connection drains the streams it carries and takes no
282    /// new one.
283    pub(super) fn with_backend_retired(mut self, retired: &'a dyn Fn(&BackendId) -> bool) -> Self {
284        self.backend_retired = Some(retired);
285        self
286    }
287
288    /// Whether `backend` has left the configuration since a connection was
289    /// dialled to it.
290    fn is_retired(&self, backend: &BackendId) -> bool {
291        self.backend_retired.is_some_and(|retired| retired(backend))
292    }
293
294    /// The cluster `cluster_id` names, if the worker still holds one.
295    fn cluster(&self, cluster_id: &str) -> Option<&Cluster> {
296        self.clusters.get(cluster_id)
297    }
298
299    /// Whether this listener is plaintext HTTP, which is what both legacy
300    /// `https_redirect` sites gate on.
301    fn is_http_listener(&self) -> bool {
302        matches!(self.listener_kind, ListenerType::Http)
303    }
304}
305
306/// What [`Router::plan_connect`] answers: either a finished decision, or a
307/// step it needs the embedder to perform before it can finish.
308///
309/// The per-`(cluster, source-IP)` limit gate is the reason this exists. It is
310/// not a read — it consults `SessionManager::cluster_ip_at_limit` **and**
311/// calls `SessionManager::track_cluster_ip`, which mutates
312/// `connections_per_cluster_ip`, worker-global state keyed on
313/// `(cluster, ip)` across every session and read again by `lib/src/tcp.rs`'s
314/// own gate. A read-only view cannot carry a mutation, so the gate becomes a
315/// returned step instead: the core resolves the cluster, hands the embedder
316/// what to ask, and the embedder answers.
317#[derive(Debug)]
318pub(super) enum ConnectStep {
319    /// No gate consultation was needed — the stream carries no source IP —
320    /// so this is already the decision.
321    Decided(ConnectPlan),
322    /// The core resolved a cluster and needs the limit consulted, and
323    /// updated, before it can go on. Answer with
324    /// [`Router::plan_connect_resume`].
325    CheckIpLimit(ConnectResume),
326}
327
328/// The phase-one results the core needs handed back to finish deciding, plus
329/// what the embedder needs to consult the gate.
330///
331/// Opaque on purpose: the embedder reads the gate inputs through accessors
332/// and cannot reach — or forge — the routing results the core parked here.
333/// The cluster the gate is keyed on is not among them: `plan_connect` has
334/// already stored it in the stream's `HttpContext`, and the embedder borrows
335/// it from there rather than the resume carrying a copy of its own (#1583).
336/// [`Router::plan_connect_resume`] takes it **by value**, so a resume cannot
337/// be replayed; that is `H2Shell::settled` taking its parked target, applied
338/// to this seam.
339#[derive(Debug)]
340pub(super) struct ConnectResume {
341    h2: bool,
342    frontend_should_stick: bool,
343    ip: IpAddr,
344    max_connections_per_ip: Option<u64>,
345    cluster_retry_after: Option<u32>,
346    max_connections_per_subnet: Option<u64>,
347}
348
349impl ConnectResume {
350    /// The source IP the gate is keyed on — proxy-protocol-aware when
351    /// present, falling back to `peer_addr`.
352    pub(super) fn ip(&self) -> IpAddr {
353        self.ip
354    }
355    /// The cluster's per-IP connection limit override, if it set one.
356    pub(super) fn max_connections_per_ip(&self) -> Option<u64> {
357        self.max_connections_per_ip
358    }
359    /// The cluster's `Retry-After` override, if it set one.
360    pub(super) fn cluster_retry_after(&self) -> Option<u32> {
361        self.cluster_retry_after
362    }
363    /// The cluster's per-SUBNET connection limit override, if it set
364    /// one. Independent of `max_connections_per_ip`: the embedder
365    /// consults both caps and admits only when both allow it.
366    pub(super) fn max_connections_per_subnet(&self) -> Option<u64> {
367        self.max_connections_per_subnet
368    }
369}
370
371/// The embedder's answer to [`ConnectStep::CheckIpLimit`].
372///
373/// `Admitted` carries no payload on purpose: it means the embedder has
374/// already *performed* the track, so there is nothing left for the core to
375/// decide about it and no binding a later edit could reuse to ask a question
376/// this variant has answered — the discipline
377/// `H2WritableStateTarget::Flush` set by carrying no `bool`.
378#[derive(Debug)]
379pub(super) enum IpGateVerdict {
380    /// Under the limit, and the embedder has tracked this `(cluster, ip)`.
381    Admitted,
382    /// At the limit. Carries the resolved `Retry-After` the core stashes on
383    /// the stream for the 429 mapping to render or elide.
384    AtLimit { retry_after: u32 },
385}
386
387/// What [`Router::plan_connect`] decided, for the embedder to fulfil.
388///
389/// Question 6 of [#1340](https://github.com/sozu-proxy/sozu/issues/1340): the
390/// core decides and the embedder performs. This is the "returned request"
391/// half of that, in the shape the socket-boundary extraction used four times
392/// (`finalize_write` / `H2FinalizeTarget`, `flush_pending_control_frames` /
393/// `H2ControlFlushTarget`, `dispatch_writable_state` / `H2WritableStateTarget`,
394/// and `force_disconnect`'s inversion): the core answers a step, the caller
395/// performs the effect, the caller calls back with the answer —
396/// [`Router::commit_dialed`] here.
397#[derive(Debug)]
398pub(super) enum ConnectPlan {
399    /// The stream was attached to a backend connection the router already
400    /// held, and nothing is left to perform. Carries no token on purpose: the
401    /// attach is complete, so there is no binding a later edit could reuse to
402    /// ask a question this variant has already answered — the discipline
403    /// `H2WritableStateTarget::Flush` set by carrying no `bool`.
404    Attached,
405    /// No connection could be reused. The embedder must select a backend,
406    /// dial it, build the `Connection`, register it, and hand the result
407    /// back to [`Router::commit_dialed`].
408    Dial {
409        /// The cluster routing resolved for this stream.
410        cluster_id: ClusterId,
411        /// Whether that cluster's backends speak HTTP/2.
412        h2: bool,
413        /// Whether the frontend asked for sticky-session affinity.
414        frontend_should_stick: bool,
415    },
416}
417
418/// The backend connections of one session, keyed and iterated by [`Token`].
419///
420/// A session almost always holds exactly one backend connection, and a
421/// `Connection<SessionTcpStream>` is about 1.7 KiB. A `BTreeMap` stores its
422/// values inline in 11-slot leaves, so the first insertion into one allocated
423/// ~18.8 KiB to hold that single connection (#1610). The lowest-token
424/// connection therefore lives inline in `first`, and only the others reach
425/// the `BTreeMap`: one backend costs no allocation at all, and every point
426/// operation stays `O(log n)` for the multi-backend sessions the module
427/// header sizes.
428///
429/// Invariant: `first` is `None` only when `rest` is empty, and its token is
430/// below every token in `rest`. Iteration is `first` then `rest`, which is the
431/// total order on `Token` #1338 requires.
432///
433/// HAProxy needs no container here because a stream has one server
434/// connection, reached by pointer (`include/haproxy/stream-t.h`, `scb`); a
435/// mux session can hold several, hence the overflow map.
436pub struct BackendConnections {
437    first: Option<(Token, Connection<SessionTcpStream>)>,
438    rest: BTreeMap<Token, Connection<SessionTcpStream>>,
439}
440
441impl BackendConnections {
442    pub const fn new() -> Self {
443        Self {
444            first: None,
445            rest: BTreeMap::new(),
446        }
447    }
448
449    pub fn len(&self) -> usize {
450        usize::from(self.first.is_some()) + self.rest.len()
451    }
452
453    pub fn is_empty(&self) -> bool {
454        self.first.is_none()
455    }
456
457    pub fn contains_key(&self, token: &Token) -> bool {
458        self.get(token).is_some()
459    }
460
461    pub fn get(&self, token: &Token) -> Option<&Connection<SessionTcpStream>> {
462        match &self.first {
463            Some((first, connection)) if first == token => Some(connection),
464            _ => self.rest.get(token),
465        }
466    }
467
468    pub fn get_mut(&mut self, token: &Token) -> Option<&mut Connection<SessionTcpStream>> {
469        match &mut self.first {
470            Some((first, connection)) if first == token => Some(connection),
471            _ => self.rest.get_mut(token),
472        }
473    }
474
475    /// Insert `connection` under `token`, returning the one it replaces.
476    pub fn insert(
477        &mut self,
478        token: Token,
479        connection: Connection<SessionTcpStream>,
480    ) -> Option<Connection<SessionTcpStream>> {
481        match &mut self.first {
482            None => {
483                self.first = Some((token, connection));
484                None
485            }
486            Some((first, held)) if *first == token => Some(std::mem::replace(held, connection)),
487            Some((first, held)) if token < *first => {
488                let displaced_token = std::mem::replace(first, token);
489                let displaced = std::mem::replace(held, connection);
490                self.rest.insert(displaced_token, displaced)
491            }
492            Some(_) => self.rest.insert(token, connection),
493        }
494    }
495
496    /// Remove and return the connection under `token`.
497    pub fn remove(&mut self, token: &Token) -> Option<Connection<SessionTcpStream>> {
498        match &self.first {
499            Some((first, _)) if first == token => {
500                let removed = std::mem::replace(&mut self.first, self.rest.pop_first());
501                removed.map(|(_, connection)| connection)
502            }
503            _ => self.rest.remove(token),
504        }
505    }
506
507    /// The connections in ascending `Token` order.
508    pub fn iter(&self) -> BackendConnectionsIter<'_> {
509        let split: fn(&BackendEntry) -> (&Token, &Connection<SessionTcpStream>) =
510            |(token, connection)| (token, connection);
511        self.first.iter().map(split).chain(self.rest.iter())
512    }
513
514    /// The connections in ascending `Token` order, mutably.
515    pub fn iter_mut(&mut self) -> BackendConnectionsIterMut<'_> {
516        let split: fn(&mut BackendEntry) -> (&Token, &mut Connection<SessionTcpStream>) =
517            |(token, connection)| (&*token, connection);
518        self.first.iter_mut().map(split).chain(self.rest.iter_mut())
519    }
520
521    pub fn keys(&self) -> impl Iterator<Item = &Token> {
522        self.iter().map(|(token, _)| token)
523    }
524
525    pub fn values_mut(&mut self) -> impl Iterator<Item = &mut Connection<SessionTcpStream>> {
526        self.iter_mut().map(|(_, connection)| connection)
527    }
528}
529
530type BackendEntry = (Token, Connection<SessionTcpStream>);
531
532/// [`BackendConnections::iter`]: the inline connection, then the overflow map.
533pub type BackendConnectionsIter<'a> = std::iter::Chain<
534    std::iter::Map<
535        std::option::Iter<'a, BackendEntry>,
536        fn(&'a BackendEntry) -> (&'a Token, &'a Connection<SessionTcpStream>),
537    >,
538    std::collections::btree_map::Iter<'a, Token, Connection<SessionTcpStream>>,
539>;
540
541/// [`BackendConnections::iter_mut`]: the inline connection, then the overflow map.
542pub type BackendConnectionsIterMut<'a> = std::iter::Chain<
543    std::iter::Map<
544        std::option::IterMut<'a, BackendEntry>,
545        fn(&'a mut BackendEntry) -> (&'a Token, &'a mut Connection<SessionTcpStream>),
546    >,
547    std::collections::btree_map::IterMut<'a, Token, Connection<SessionTcpStream>>,
548>;
549
550impl<'a> IntoIterator for &'a BackendConnections {
551    type Item = (&'a Token, &'a Connection<SessionTcpStream>);
552    type IntoIter = BackendConnectionsIter<'a>;
553
554    fn into_iter(self) -> Self::IntoIter {
555        self.iter()
556    }
557}
558
559impl<'a> IntoIterator for &'a mut BackendConnections {
560    type Item = (&'a Token, &'a mut Connection<SessionTcpStream>);
561    type IntoIter = BackendConnectionsIterMut<'a>;
562
563    fn into_iter(self) -> Self::IntoIter {
564        self.iter_mut()
565    }
566}
567
568impl Default for BackendConnections {
569    fn default() -> Self {
570        Self::new()
571    }
572}
573
574impl std::fmt::Debug for BackendConnections {
575    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
576        f.debug_map().entries(self.iter()).finish()
577    }
578}
579
580#[derive(Debug)]
581pub struct Router {
582    pub backends: BackendConnections,
583    pub configured_backend_timeout: Duration,
584    pub configured_connect_timeout: Duration,
585    /// Fallback readiness used when a backend token is missing from the map.
586    /// This prevents panicking in the Endpoint trait methods that return references.
587    pub(super) fallback_readiness: Readiness,
588    /// Tokens of the backend connections a `Mux::ready` pass found dead,
589    /// awaiting their removal from [`Self::backends`] later in that pass.
590    ///
591    /// Emptied at the start of every sweep and kept for its capacity, so a
592    /// session whose backends close one after another allocates it once
593    /// rather than once per pass that closes one (#1610).
594    pub(super) dead_backends: Vec<Token>,
595}
596
597impl Router {
598    pub fn new(configured_backend_timeout: Duration, configured_connect_timeout: Duration) -> Self {
599        Self {
600            backends: BackendConnections::new(),
601            configured_backend_timeout,
602            configured_connect_timeout,
603            fallback_readiness: Readiness::new(),
604            dead_backends: Vec::new(),
605        }
606    }
607
608    /// Decide what this stream needs, without performing any of it.
609    ///
610    /// Routing, the cluster gate, the per-(cluster, source-IP) limit and the
611    /// pool-reuse scan all happen here. Dialling, the slab session and the
612    /// epoll registration do not: those are the embedder's, and this answers
613    /// a [`ConnectPlan`] asking for them instead of reaching a
614    /// `Rc<RefCell<dyn ProxySession>>` to do them itself.
615    ///
616    /// `session` is gone from this signature entirely — its single use was
617    /// `L7Proxy::add_session`, which now happens in `Mux::ready_inner`.
618    /// `proxy` remains for the three reads this still makes (`clusters`,
619    /// `kind`, `sessions`) and for backend selection, both of which Question
620    /// 6's borrowed-view step takes next.
621    pub(super) fn plan_connect<L: ListenerHandler + L7ListenerHandler>(
622        &mut self,
623        stream_id: GlobalStreamId,
624        context: &mut Context<L>,
625        // The cluster-side configuration this decision reads, borrowed from
626        // the embedder for the length of the call.
627        view: &RoutingView<'_>,
628    ) -> Result<ConnectStep, BackendConnectionError> {
629        let stream = &mut context.streams[stream_id];
630        // when reused, a stream should be detached from its old connection, if not we could end
631        // with concurrent connections on a single endpoint
632        if !matches!(stream.state, StreamState::Link) {
633            error!(
634                "{} stream {} expected to be in Link state, got {:?}",
635                log_module_context!(stream.context),
636                stream_id,
637                stream.state
638            );
639            return Err(BackendConnectionError::MaxSessionsMemory);
640        }
641        #[cfg(debug_assertions)]
642        context
643            .debug
644            .push(DebugEvent::Str(stream.context.get_route()));
645        // The budget is resolved from the cluster the previous attempt routed
646        // to. The first attempt has none yet, and needs none: every budget
647        // allows at least one attempt.
648        let max_attempts = view.max_connection_attempts(stream.context.cluster_id.as_deref());
649        if u32::from(stream.attempts) >= max_attempts {
650            incr!(
651                names::backend::CONNECT_RETRIES_EXHAUSTED,
652                stream.context.cluster_id.as_deref(),
653                stream.context.backend_id.as_deref(),
654                stream.context.cluster_metrics_incarnation()
655            );
656            return Err(BackendConnectionError::MaxConnectionRetries(
657                stream.context.cluster_id.clone(),
658            ));
659        }
660        stream.attempts += 1;
661
662        // Borrow front mutably (so route_from_request can rewrite the request
663        // line authority/path and inject request-side header edits before we
664        // forward to the backend) plus context mutably (so it can stash
665        // redirect_location / www_authenticate / original_authority /
666        // headers_response). We split-borrow manually to keep the rest of
667        // `connect` working with `stream_context` aliasing `stream.context`.
668        //
669        // A REPLAY skips routing entirely. `front.consumed` is the exact
670        // discriminator: it is false on every first connect and on the
671        // untouched-request `EndStreamAction::Reconnect`, and true only once
672        // request bytes have left the front kawa — which is precisely the
673        // `EndStreamAction::ReplayOnFreshBackend` case. Routing already ran
674        // on the first attempt and every decision it made (cluster, legacy
675        // HTTPS redirect, SNI/authority binding, authentication, request-side
676        // header edits) is baked into the captured bytes.
677        //
678        // Re-running it against a front kawa whose blocks are drained cannot
679        // reproduce them: the request line and headers it rewrites are no
680        // longer in `front.blocks`, and `end_of_headers_index` has no anchor
681        // to place injected headers at. Whatever the next `prepare` did emit
682        // would land BEHIND the replayed bytes, because `Kawa::prepare`
683        // appends to `out` while `Stream::queue_upstream_replay` prepends —
684        // a second, partial copy of the request trailing the first on the
685        // same connection.
686        //
687        // So a replay whose routing decision did not survive is refused
688        // outright rather than re-routed against that drained kawa: 502 is
689        // what the stream would have received before the replay existed.
690        //
691        // The routed id, a handle on the route table's `ClusterId` and not a
692        // copy, moves into `HttpContext::cluster_id`; everything after this
693        // borrows or re-counts it, the embedder's gate included (#1583).
694        let replaying = stream.front.consumed;
695        if !replaying {
696            let (front_ref, stream_context_ref) = {
697                let stream_split = &mut *stream;
698                (&mut stream_split.front, &mut stream_split.context)
699            };
700            let routed = self
701                .route_from_request(stream_context_ref, front_ref, &context.listener, view)
702                .map_err(BackendConnectionError::RetrieveClusterError)?;
703            // Derived here, where the routed cluster and the request's header
704            // blocks are both in hand, and stored beside the cluster id: a
705            // replay finds the blocks drained and reuses this key, as it
706            // reuses the cluster.
707            let context = &stream.context;
708            let key = view.cluster(&routed).and_then(|cluster| {
709                affinity_key(
710                    cluster,
711                    &stream.front,
712                    context.session_address,
713                    StickyCookie {
714                        name: &context.sticky_name,
715                        value: context.sticky_session_found.as_deref(),
716                    },
717                )
718            });
719            stream.context.affinity_key = key;
720            stream.context.set_cluster_metrics_incarnation(
721                crate::metrics::capture_cluster_metrics(routed.as_ref()),
722            );
723            stream.context.cluster_id = Some(routed);
724        }
725        let stream_context = &stream.context;
726        // Only a replay can find it unset: routing has just stored it otherwise.
727        let Some(cluster_id) = stream_context.cluster_id.as_deref() else {
728            return Err(BackendConnectionError::ReplayRefused(
729                "no cluster_id survived the first attempt",
730            ));
731        };
732
733        let (
734            frontend_should_stick,
735            frontend_should_redirect_https,
736            h2,
737            cluster_max_connections_per_ip,
738            cluster_retry_after,
739            cluster_max_connections_per_subnet,
740        ) = view
741            .cluster(cluster_id)
742            .map(|cluster| {
743                (
744                    cluster.sticky_session,
745                    cluster.https_redirect,
746                    cluster.http2.unwrap_or(false),
747                    cluster.max_connections_per_ip,
748                    cluster.retry_after,
749                    cluster.max_connections_per_subnet,
750                )
751            })
752            .unwrap_or((false, false, false, None, None, None));
753
754        // A replay carries H1 wire bytes in `front.out` (the H1 write path is
755        // the only one that captures), so its cluster must still resolve to
756        // H1 or the H2 converter would frame raw H1 text as a DATA payload.
757        // It is expected to: `clusters()` is owned by the worker's command
758        // loop, and a replay is drained from `pending_links` inside the very
759        // `ready()` pass that queued it, so no cluster update should
760        // interleave. That argument is not load-bearing here — the cost of it
761        // being wrong is silent protocol corruption on the wire, which is not
762        // a thing to leave to an assertion that compiles out of every release
763        // build. Refuse the replay instead and let the caller answer 502.
764        if replaying && h2 {
765            return Err(BackendConnectionError::ReplayRefused(
766                "the cluster switched to HTTP/2 between attempts",
767            ));
768        }
769
770        // ── Legacy `cluster.https_redirect` short-circuit ──
771        //
772        // Resolve the legacy HTTP→HTTPS redirect BEFORE per-(cluster,
773        // source-IP) accounting so a redirect-only request never
774        // consumes an IP slot. Otherwise a same-IP client iterating an
775        // HTTP→HTTPS hop could trip 429 ahead of the 301 even though no
776        // backend would have been opened. A duplicate guard that lived
777        // here previously (rebase artefact — two identical
778        // `if frontend_should_redirect_https && …` blocks back-to-back)
779        // is folded into this single early-return.
780        // Frontend-scoped `RedirectPolicy::PERMANENT` already returns
781        // from `route_from_request` with the same error, so this only
782        // handles the legacy cluster-level path that doesn't surface
783        // from `route_from_request`.
784        if frontend_should_redirect_https && view.is_http_listener() {
785            return Err(BackendConnectionError::RetrieveClusterError(
786                RetrieveClusterError::HttpsRedirect,
787            ));
788        }
789
790        // Per-(cluster, source-IP) connection limit gate. Runs AFTER cluster
791        // resolution AND legacy redirect emission (so a 401/421/redirect
792        // frontend never trips the limit) and BEFORE any backend selection
793        // (so a rejection consumes neither a backend pool slot nor a retry
794        // budget). The check uses the source IP from the per-stream
795        // `HttpContext.session_address`, which is the proxy-protocol-aware
796        // client address when present, falling back to `peer_addr`. The
797        // limit governs distinct **frontend connections** per
798        // `(cluster, ip)`: an H2 session multiplexing N streams to the same
799        // cluster from the same IP still consumes a single slot.
800        // This is where phase one ends. The gate needs `SessionManager`, which
801        // the core does not hold and cannot be handed as a read-only view
802        // because consulting it is only half of what happens here — the other
803        // half is `track_cluster_ip`, a mutation of worker-global state. So
804        // the core stops, says what to ask, and resumes on the answer.
805        //
806        // A stream with no source address skips the gate entirely, exactly as
807        // before, and is decided outright.
808        let Some(ip) = stream_context.session_address.map(|sa| sa.ip()) else {
809            return self
810                .decide_after_gate(stream_id, context, view, h2, frontend_should_stick)
811                .map(ConnectStep::Decided);
812        };
813        Ok(ConnectStep::CheckIpLimit(ConnectResume {
814            h2,
815            frontend_should_stick,
816            ip,
817            max_connections_per_ip: cluster_max_connections_per_ip,
818            cluster_retry_after,
819            max_connections_per_subnet: cluster_max_connections_per_subnet,
820        }))
821    }
822
823    /// Finish a decision the embedder paused at
824    /// [`ConnectStep::CheckIpLimit`].
825    ///
826    /// Takes `resume` **by value**, so a parked decision cannot be resumed
827    /// twice. The return type is [`ConnectPlan`], not [`ConnectStep`], so a
828    /// second gate consultation is not merely wrong but unrepresentable.
829    pub(super) fn plan_connect_resume<L: ListenerHandler + L7ListenerHandler>(
830        &mut self,
831        stream_id: GlobalStreamId,
832        context: &mut Context<L>,
833        view: &RoutingView<'_>,
834        resume: ConnectResume,
835        verdict: IpGateVerdict,
836    ) -> Result<ConnectPlan, BackendConnectionError> {
837        if let IpGateVerdict::AtLimit { retry_after } = verdict {
838            // Stash the resolved retry value on the stream so the mux's
839            // `BackendConnectionError` -> 429 mapping can render (or elide)
840            // the `Retry-After` header without re-deriving the override
841            // chain. The embedder resolved it; the core only records it.
842            context.streams[stream_id].context.retry_after_seconds =
843                Some(retry_after).filter(|v| *v > 0);
844            return Err(BackendConnectionError::TooManyConnectionsPerIp {
845                cluster_id: context.streams[stream_id]
846                    .context
847                    .cluster_id
848                    .clone()
849                    .unwrap_or_default(),
850            });
851        }
852        self.decide_after_gate(
853            stream_id,
854            context,
855            view,
856            resume.h2,
857            resume.frontend_should_stick,
858        )
859    }
860
861    /// Everything the decision does once the gate has answered: the pool
862    /// reuse scan, and the dial request when nothing is reusable.
863    ///
864    /// One body shared by both the gated and the ungated entry, so the two
865    /// cannot drift — the thing a split like this most easily gets wrong.
866    ///
867    /// The cluster is read from the stream's `HttpContext`, where
868    /// `plan_connect` stored the routed id, rather than handed in as a copy.
869    fn decide_after_gate<L: ListenerHandler + L7ListenerHandler>(
870        &mut self,
871        stream_id: GlobalStreamId,
872        context: &mut Context<L>,
873        view: &RoutingView<'_>,
874        h2: bool,
875        frontend_should_stick: bool,
876    ) -> Result<ConnectPlan, BackendConnectionError> {
877        let stream_context = &context.streams[stream_id].context;
878        let Some(cluster_id) = stream_context.cluster_id.as_ref() else {
879            error!(
880                "{} stream {} reached the backend decision without a routed cluster",
881                log_module_context!(stream_context),
882                stream_id
883            );
884            return Err(BackendConnectionError::MaxSessionsMemory);
885        };
886
887        /*
888        H2 connecting strategy (least-loaded):
889        - look at every backend connection
890        - among connected backends for this cluster, pick the one with the fewest active streams
891        - fall back to a connecting backend if no connected one exists
892        - if no backend is to reuse, ask the router for a socket to the "next in line" backend
893
894        H1 strategy: reuse the first KeepAlive backend for this cluster.
895
896        Either way, a connection whose backend has left the configuration is
897        never reused, even when a cluster of the same id is configured again.
898         */
899
900        let mut reuse_token = None;
901        let mut best_h2_stream_count = usize::MAX;
902        for (token, backend) in &self.backends {
903            match (h2, backend.position()) {
904                (_, Position::Server) => {
905                    error!(
906                        "{} Backend connection unexpectedly behaves like a server",
907                        log_module_context!(stream_context)
908                    );
909                    continue;
910                }
911                (_, Position::Client(_, _, BackendStatus::Disconnecting)) => {}
912                // The cluster id matches, the backend no longer does.
913                (_, Position::Client(other_cluster_id, backend, _))
914                    if other_cluster_id == cluster_id && view.is_retired(backend) => {}
915
916                (true, Position::Client(other_cluster_id, _, BackendStatus::Connected)) => {
917                    if other_cluster_id == cluster_id && !backend.is_draining() {
918                        // Pick the non-draining H2 connection with the fewest active streams
919                        let Connection::H2(h2c) = backend else {
920                            continue;
921                        };
922                        let stream_count = h2c.core.stream_count();
923                        if stream_count
924                            >= h2c.core.peer_settings.settings_max_concurrent_streams as usize
925                        {
926                            continue;
927                        }
928                        if stream_count < best_h2_stream_count {
929                            best_h2_stream_count = stream_count;
930                            reuse_token = Some(*token);
931                        }
932                    }
933                }
934                (true, Position::Client(other_cluster_id, _, BackendStatus::Connecting(_))) => {
935                    // Only use a connecting backend if no connected one was found
936                    if other_cluster_id == cluster_id
937                        && best_h2_stream_count == usize::MAX
938                        && matches!(backend, Connection::H2(_))
939                    {
940                        reuse_token = Some(*token)
941                    }
942                }
943                (true, Position::Client(other_cluster_id, _, BackendStatus::KeepAlive)) => {
944                    if other_cluster_id == cluster_id && matches!(backend, Connection::H2(_)) {
945                        error!(
946                            "{} ConnectionH2 unexpectedly behaves like H1 with KeepAlive",
947                            log_module_context!(stream_context)
948                        );
949                    }
950                }
951
952                (false, Position::Client(old_cluster_id, _, BackendStatus::KeepAlive)) => {
953                    if old_cluster_id == cluster_id {
954                        reuse_token = Some(*token);
955                        break;
956                    }
957                }
958                // can't bundle H1 streams together
959                (false, Position::Client(_, _, BackendStatus::Connected))
960                | (false, Position::Client(_, _, BackendStatus::Connecting(_))) => {}
961            }
962        }
963        trace!(
964            "{} connect: (stick={}, h2={}) -> (reuse={:?})",
965            log_module_context!(stream_context),
966            frontend_should_stick,
967            h2,
968            reuse_token
969        );
970
971        if let Some(token) = reuse_token {
972            // Pool reuse: an existing backend connection (H2 multiplex slot or
973            // H1 keep-alive socket) is being reattached to this stream. Pair
974            // with `backend.pool.miss` below — together they describe the
975            // pool's hit/miss ratio. Counted before any commit so the metric
976            // is consistent with the trace log.
977            incr!(names::backend::POOL_HIT);
978            trace!(
979                "{} reused backend: {:#?}",
980                log_module_context!(stream_context),
981                self.backends.get(&token)
982            );
983            // Link backend to stream for the reused connection path. We check
984            // that the backend can accept a new stream before committing any
985            // per-stream state.
986            let Some(backend_conn) = self.backends.get_mut(&token) else {
987                error!(
988                    "{} reused backend token {:?} missing from backends map",
989                    log_module_context!(stream_context),
990                    token
991                );
992                return Err(BackendConnectionError::MaxSessionsMemory);
993            };
994            if !backend_conn.start_stream(stream_id, context) {
995                // Use `context.http_context(stream_id)` instead of reusing
996                // `stream_context`: `start_stream` above takes `&mut
997                // context`, which reborrows the slab mutably and ends any
998                // outstanding `stream_context` reference. A fresh shared
999                // borrow via the accessor is borrow-check clean.
1000                error!(
1001                    "{} Backend rejected stream start (max concurrent streams reached)",
1002                    log_module_context!(context.http_context(stream_id))
1003                );
1004                return Err(BackendConnectionError::MaxSessionsMemory);
1005            }
1006            // For reused backends: set context fields and metrics lifecycle.
1007            // Both values are read off the connection's own `BackendId`; the id
1008            // is an `Rc` clone, not a heap copy (#1579). They are identity,
1009            // immutable for the life of the registry entry: the answer the
1010            // registry borrow used to give, without the core holding the handle.
1011            if let Some(backend_conn) = self.backends.get(&token)
1012                && let Position::Client(_, backend, _) = backend_conn.position()
1013            {
1014                let stream = &mut context.streams[stream_id];
1015                stream.context.backend_id = Some(Rc::clone(&backend.backend_id));
1016                stream.context.backend_address = Some(backend.address);
1017                stream.metrics.backend_id = Some(Rc::clone(&backend.backend_id));
1018                stream.metrics.backend_start();
1019                stream.metrics.backend_connected();
1020            }
1021            context.link_stream(stream_id, token);
1022            return Ok(ConnectPlan::Attached);
1023        }
1024
1025        // New-backend path: no reusable connection was found (no live H2
1026        // multiplex slot for this cluster, no H1 keep-alive socket), so a
1027        // fresh TCP dial and full backend handshake have to follow.
1028        //
1029        // Pool miss, counted here rather than after the dial so the count
1030        // includes attempts that fail at backend selection
1031        // (`BackendError::NoBackendForCluster`, etc.) — every miss is a slot
1032        // we did not save. Pair with `backend.pool.hit` above. The dial
1033        // itself may still fail, in which case `backend.pool.size` is never
1034        // bumped but the miss is already counted.
1035        incr!(names::backend::POOL_MISS);
1036
1037        // Everything past this point was the embedder's work all along: the
1038        // selection, the `connect(2)`, `set_nodelay`, the slab session, the
1039        // epoll registration and their rollback. It now reads that way.
1040        // `Mux::ready_inner` performs it in the same order and calls back
1041        // into `Router::commit_dialed`.
1042        //
1043        // The new connection's `Position::Client` shares the routed id: the
1044        // plan carries a handle on it and `Mux::attach_dialed` moves it there.
1045        Ok(ConnectPlan::Dial {
1046            cluster_id: cluster_id.clone(),
1047            h2,
1048            frontend_should_stick,
1049        })
1050    }
1051
1052    /// Take ownership of a backend connection the embedder dialled, built and
1053    /// registered, and attach `stream_id` to it.
1054    ///
1055    /// The reply half of [`ConnectPlan::Dial`]. By the time this runs the
1056    /// embedder has already proved the connection usable — `start_stream`
1057    /// returned true and the epoll registration succeeded — so this performs
1058    /// the two steps that are the router's own: taking the connection into
1059    /// the backend map under its token, and linking the stream to it.
1060    ///
1061    /// Deliberately infallible. Every way the dial could fail is decided
1062    /// before the caller reaches here, which is what keeps the CWE-400
1063    /// rollback discipline in one place instead of split across the seam.
1064    pub(super) fn commit_dialed<L: ListenerHandler + L7ListenerHandler>(
1065        &mut self,
1066        stream_id: GlobalStreamId,
1067        context: &mut Context<L>,
1068        token: Token,
1069        connection: Connection<SessionTcpStream>,
1070    ) {
1071        // No timer arming here: the connection arms its own connect-timeout
1072        // DEADLINE at construction and the `Mux` adapter reflects it onto the
1073        // wheel when `ready()` reschedules, which is the same pass this runs
1074        // in. Until that reschedule no wheel entry exists — which is what the
1075        // caller's rollback paths want, since they drop the connection
1076        // without ever reaching here.
1077        self.backends.insert(token, connection);
1078        context.link_stream(stream_id, token);
1079    }
1080
1081    fn route_from_request<L: ListenerHandler + L7ListenerHandler>(
1082        &mut self,
1083        context: &mut HttpContext,
1084        front: &mut super::GenericHttpStream,
1085        listener: &Rc<RefCell<L>>,
1086        view: &RoutingView<'_>,
1087    ) -> Result<ClusterId, RetrieveClusterError> {
1088        let (host, uri, method) = match context.extract_route() {
1089            Ok(tuple) => tuple,
1090            Err(cluster_error) => {
1091                // we are past kawa parsing if it succeeded this can't fail
1092                // if the request was malformed it was caught by kawa and we sent a 400
1093                error!(
1094                    "{} Malformed request in connect (should be caught at parsing) {:?}: {}",
1095                    log_module_context!(context),
1096                    context,
1097                    cluster_error
1098                );
1099                return Err(cluster_error);
1100            }
1101        };
1102        // ── TLS cert SAN ↔ HTTP :authority binding ────────────────────────
1103        // Reject any request whose `:authority` is not covered by a SAN of
1104        // the certificate Sōzu actually served at the TLS handshake, with
1105        // RFC 6125 §6.4.3 wildcard handling. Without this binding, an
1106        // attacker holding a valid certificate for tenant A could open TLS
1107        // with SNI=A then send an H2 stream with `:authority=tenantB.…` and
1108        // reach tenant B's backend, crossing the TLS trust boundary
1109        // (CWE-346 / CWE-444). The H2 spec explicitly allows browsers to
1110        // coalesce streams onto a connection whenever the server is
1111        // authoritative for the new origin (RFC 7540 §9.1.1 / RFC 9113
1112        // §9.1.1), which "authoritative" means "covered by a SAN of the
1113        // served cert"; rejecting coalesced streams as 421 caused the
1114        // user-visible bug this predicate fixes (RFC 9110 §15.5.20).
1115        //
1116        // Plaintext listeners bypass the check (SNI is always `None`).
1117        // Connections where SNI was sent but no cert matched (rustls served
1118        // the default cert) carry `Some(empty)` SAN snapshot, so every
1119        // authority is rejected — Sōzu is not authoritative for any name.
1120        // Connections with no SNI fall back to the legacy exact-SNI match
1121        // predicate (`authority_matches_sni`) for parity with pre-fix
1122        // behaviour on the pathological "no SNI" case.
1123        // Operators may opt out per-listener via
1124        // `HttpsListenerConfig::strict_sni_binding = false`.
1125        if let Some(sni) = context
1126            .tls_server_name
1127            .as_deref()
1128            .filter(|_| context.strict_sni_binding)
1129        {
1130            let matched: Option<&str> = match context.tls_cert_names.as_deref() {
1131                Some(cert_names) => authority_matched_cert_name(host, cert_names),
1132                None => {
1133                    if authority_matches_sni(host, sni) {
1134                        Some(sni)
1135                    } else {
1136                        None
1137                    }
1138                }
1139            };
1140            match matched {
1141                Some(matched_name) => {
1142                    // Real coalescing = matched SAN differs from the SNI's
1143                    // value after the matcher's port-strip + ASCII case
1144                    // folding. Same-name requests are the common
1145                    // non-coalesced path; do not pollute the counter or
1146                    // logs with them. The ALPN=`h2` gate is a defensive
1147                    // guard, not load-bearing under current invariants —
1148                    // every request reaching `route_from_request` on an
1149                    // HTTPS listener with `tls_cert_names` populated has
1150                    // already gone through the H2 mux (ALPN=h2 by
1151                    // construction). Kept explicit so a future routing
1152                    // refactor that funnels H1 keep-alive through the
1153                    // same predicate doesn't silently double-count
1154                    // sequential `Host:` reuse as "coalescing".
1155                    if !authority_matches_sni(host, sni) && context.tls_alpn == Some("h2") {
1156                        incr!(names::h2::COALESCING_ACCEPTED);
1157                        log_coalescing_accepted(context, host, sni, matched_name);
1158                    }
1159                }
1160                None => {
1161                    incr!(names::http::SNI_AUTHORITY_MISMATCH);
1162                    log_sni_authority_mismatch(
1163                        context,
1164                        host,
1165                        sni,
1166                        context.tls_cert_names.as_deref().map(Vec::as_slice),
1167                    );
1168                    return Err(RetrieveClusterError::SniAuthorityMismatch {
1169                        sni: sni.to_owned(),
1170                        authority: host.to_owned(),
1171                    });
1172                }
1173            }
1174        }
1175
1176        let route_result =
1177            listener
1178                .borrow()
1179                .frontend_from_request(host, uri, method, &FrontRequestHeaders(front));
1180
1181        let route = match route_result {
1182            Ok(route) => route,
1183            Err(frontend_error) => {
1184                trace!("{} {}", log_module_context!(context), frontend_error);
1185                return Err(RetrieveClusterError::RetrieveFrontend(frontend_error));
1186            }
1187        };
1188
1189        // ── Resolve the routing decision ──────────────────────────────────
1190        // Snapshot the policy fields we need before consuming `route`, then
1191        // map each policy outcome to either an early-error variant (which
1192        // the caller turns into a default answer) or a cluster_id (which
1193        // proceeds to backend connect).
1194        let RouteResult {
1195            cluster_id,
1196            redirect,
1197            redirect_scheme,
1198            redirect_template,
1199            rewritten_host,
1200            rewritten_path,
1201            rewritten_port,
1202            headers_request,
1203            headers_response,
1204            required_auth: frontend_required_auth,
1205            tags,
1206        } = route;
1207
1208        // The matched frontend rule is the only correct owner of the
1209        // access-log tags, so stash them here — before every early return
1210        // below, so a redirect, a 401 and a backend-connect failure log
1211        // them too. The listener's `BTreeMap<String, CachedTags>` cannot
1212        // do this job: it is written under the frontend rule's hostname
1213        // and read under the request's authority, two spellings that only
1214        // coincide for an exact literal frontend (sozu#1379).
1215        //
1216        // No `..` in the destructure above, deliberately: that wildcard is
1217        // exactly how `tags` was dropped on the floor for as long as the
1218        // bug lived. A future `RouteResult` field now has to be routed
1219        // here explicitly or the build breaks.
1220        context.tags = tags;
1221
1222        // ── 0. Explicit `RedirectPolicy::SILENT_CLOSE` ────────────────────
1223        // nginx `return 444` parity (sozu-proxy/sozu#767): the request is
1224        // refused without writing a single response byte. Resolved before
1225        // every other policy, and before the cluster lookup it does not need,
1226        // so it holds whether or not a cluster is bound; the caller resets the
1227        // H2 stream or closes the H1 connection.
1228        if matches!(redirect, RedirectPolicy::SilentClose) {
1229            trace!("{} RouteResult::silent_close", log_module_context!(context));
1230            return Err(RetrieveClusterError::SilentClose);
1231        }
1232
1233        // ── HSTS (RFC 6797) snapshot hoist for HTTPS ──────────────────────
1234        // The response snapshot is built in two passes so HSTS reaches
1235        // every HTTPS response code (RFC 6797 §8.1 — including
1236        // proxy-generated 3xx / 401 / 5xx default answers) WITHOUT
1237        // changing the pre-PR scope of operator-defined `Append`
1238        // response headers (which only apply on the regular forward
1239        // path).
1240        //
1241        // Pass 1 (here, before any early return): for HTTPS only, copy
1242        // ONLY the HSTS-class typed edits (`SetIfAbsent | Set`). These
1243        // need to land on default answers — `set_default_answer_with_retry_after`
1244        // bypasses the post-forward copy below.
1245        //
1246        // Pass 2 (post-forward, end of function): copy EVERY edit
1247        // (including operator `Append` headers). Runs only on the
1248        // regular forward path because the early returns short-circuit
1249        // before reaching it.
1250        //
1251        // Plain-HTTP listeners are skipped here per RFC 6797 §7.2 (no
1252        // STS over plaintext) — defense in depth on top of the
1253        // TOML-time `ConfigError::HstsOnPlainHttp` and the worker IPC
1254        // `ProxyError::HstsOnPlainHttp` rejects.
1255        if matches!(context.protocol, crate::Protocol::HTTPS) {
1256            snapshot_response_edits(&mut context.headers_response, &headers_response, |e| {
1257                matches!(e.mode, HeaderEditMode::SetIfAbsent | HeaderEditMode::Set)
1258            });
1259        }
1260
1261        // Look up cluster-side policy knobs once. The values we need are:
1262        //  - `https_redirect` (legacy) and `https_redirect_port` for the 301 location URL
1263        //  - `authorized_hashes` and `www_authenticate` for the 401 path
1264        let (legacy_https_redirect, https_redirect_port, authorized_hashes, www_authenticate) =
1265            match cluster_id.as_deref() {
1266                Some(id) => view
1267                    .cluster(id)
1268                    .map(|c| {
1269                        (
1270                            c.https_redirect,
1271                            c.https_redirect_port,
1272                            c.authorized_hashes.clone(),
1273                            c.www_authenticate.clone(),
1274                        )
1275                    })
1276                    .unwrap_or((false, None, Vec::new(), None)),
1277                None => (false, None, Vec::new(), None),
1278            };
1279
1280        // ── 1. Explicit redirect policies (PERMANENT / FOUND / PERMANENT_REDIRECT) ──
1281        // Resolved BEFORE the clusterless-deny branch so a frontend that
1282        // declares `redirect = permanent | found | permanent_redirect`
1283        // emits the matching 3xx even when no cluster is bound. This is
1284        // the canonical "moved" shape from the original proposal in
1285        // #1161 and is the only way to express "this hostname has moved"
1286        // without standing up a dummy cluster. The block does not read
1287        // `cluster_id`; per-cluster values (`https_redirect_port`,
1288        // `www_authenticate`, …) default to safe sentinels at the cluster
1289        // lookup above when `cluster_id` is `None`, so the reorder is
1290        // data-flow-safe.
1291        //
1292        // Status code mapping (closes #1009):
1293        //   Permanent          → 301 (RFC 9110 §15.4.2)
1294        //   Found              → 302 (RFC 9110 §15.4.3) — UA may rewrite POST→GET
1295        //   PermanentRedirect  → 308 (RFC 9110 §15.4.9) — method MUST be preserved
1296        let redirect_status = match redirect {
1297            RedirectPolicy::Permanent => Some(301u16),
1298            RedirectPolicy::Found => Some(302u16),
1299            RedirectPolicy::PermanentRedirect => Some(308u16),
1300            // Forward / Unauthorized are handled by other branches
1301            // below; keeping them named here forces an exhaustive
1302            // match so a future RedirectPolicy variant doesn't
1303            // silently fall through to `None`.
1304            RedirectPolicy::Forward
1305            | RedirectPolicy::Unauthorized
1306            | RedirectPolicy::SilentClose => None,
1307        };
1308        if let Some(status_code) = redirect_status {
1309            let scheme = resolve_redirect_scheme(redirect_scheme, context);
1310            let port = rewritten_port.map(|p| p as u32).or(https_redirect_port);
1311            // Feed the rewritten host AND path into the `Location` URL
1312            // when the frontend's RewriteParts populated them. Without
1313            // this, a `redirect = permanent` frontend with
1314            // `rewrite_host = "new.example.com"` would serve clients
1315            // back to the original `Host:` header, defeating the
1316            // documented `old → new` shape.
1317            // The host_override path also keeps `:port` stripping
1318            // intact: `build_redirect_location` removes any `:port` on
1319            // the override before reapplying `port_suffix`.
1320            context.redirect_location = Some(build_redirect_location(
1321                scheme,
1322                context,
1323                port,
1324                rewritten_host.as_deref(),
1325                rewritten_path.as_deref(),
1326            ));
1327            // Stash the frontend's `redirect_template` (when set) so the
1328            // 3xx default-answer path can render it via
1329            // `HttpAnswers::render_inline_redirect` instead of the
1330            // listener / cluster default. Without this stash the field
1331            // flows into `RouteResult` only to be dropped by the
1332            // wildcard destructure below, so the operator-supplied
1333            // template has no observable effect on the rendered
1334            // redirect.
1335            context.frontend_redirect_template = redirect_template;
1336            // Stash the resolved status so the answer engine picks the
1337            // matching default template (`http.301.redirection` /
1338            // `http.302.redirection` / `http.308.redirection`).
1339            context.redirect_status = Some(status_code);
1340            return Err(RetrieveClusterError::HttpsRedirect);
1341        }
1342
1343        // ── 2. Explicit `RedirectPolicy::UNAUTHORIZED` or clusterless deny ─
1344        // Reached when the frontend either explicitly asks for 401 or has
1345        // no backing cluster and no `Permanent` redirect to honour. The
1346        // `Forward + cluster_id == None` combination collapses here so
1347        // legacy clusterless frontends still emit 401 by default.
1348        if matches!(redirect, RedirectPolicy::Unauthorized) || cluster_id.is_none() {
1349            context.www_authenticate = www_authenticate.clone();
1350            trace!("{} RouteResult::deny", log_module_context!(context));
1351            return Err(RetrieveClusterError::UnauthorizedRoute);
1352        }
1353
1354        let Some(cluster_id) = cluster_id else {
1355            // Guarded by the clusterless-deny branch immediately above;
1356            // the `is_none()` arm has already returned `UnauthorizedRoute`
1357            // by the time control reaches here.
1358            unreachable!("cluster_id was checked Some above")
1359        };
1360
1361        // ── 3. Legacy `cluster.https_redirect` (HTTP-only listeners) ───────
1362        // The caller (`Router::plan_connect`) emits the actual 301 only on
1363        // `ListenerType::Http`; gate the URL stash on the same predicate
1364        // so an HTTPS listener never carries a stale `redirect_location`
1365        // into a downstream default-answer path.
1366        if legacy_https_redirect && view.is_http_listener() {
1367            let port = https_redirect_port;
1368            context.redirect_location =
1369                Some(build_redirect_location("https", context, port, None, None));
1370        }
1371
1372        // ── 4. Basic auth check (only when `required_auth` was set) ────────
1373        // The check iterates the full hash list in constant time (see
1374        // `crate::protocol::mux::auth::check_basic`) so the time spent
1375        // does not leak which hash matched, or whether any did at all.
1376        // On failure, stash the cluster's `www_authenticate` realm so the
1377        // 401 default-answer can render the matching `WWW-Authenticate`
1378        // header. An empty realm causes the template engine to elide the
1379        // header entirely (`or_elide_header = true`).
1380        if frontend_required_auth
1381            && !crate::protocol::mux::auth::check_basic(front, &authorized_hashes)
1382        {
1383            context.www_authenticate = www_authenticate.clone();
1384            trace!(
1385                "{} basic-auth check failed; emitting 401",
1386                log_module_context!(context)
1387            );
1388            return Err(RetrieveClusterError::UnauthorizedRoute);
1389        }
1390
1391        // ── 5. Request-side mutations on the front kawa ────────────────────
1392        // From here on the route is a Forward — apply the frontend's
1393        // rewrite + header policy to the request kawa so the backend
1394        // wire carries the operator-configured shape.
1395        apply_request_rewrites_and_headers(
1396            front,
1397            context,
1398            rewritten_host.as_deref(),
1399            rewritten_path.as_deref(),
1400            &headers_request,
1401        );
1402
1403        // Pass 2 of the response-snapshot copy (see the HSTS hoist
1404        // above). Runs unconditionally on the regular forward path
1405        // (the early returns above bypass this site, which keeps the
1406        // default-answer scope as HSTS-only). Copies EVERY edit so
1407        // operator-defined `Append` response headers reach
1408        // backend-served responses on both HTTP and HTTPS listeners,
1409        // preserving their pre-PR scope.
1410        snapshot_response_edits(&mut context.headers_response, &headers_response, |_| true);
1411
1412        Ok(cluster_id)
1413    }
1414
1415    /// Pick the backend for an already-routed request and stamp its identity
1416    /// onto the request's [`HttpContext`].
1417    ///
1418    /// Takes no listener handle: every listener-derived value this needs —
1419    /// `sticky_name` above all — is already on the `HttpContext` that
1420    /// `Context::create_stream` filled when the request arrived, and reading
1421    /// the listener again here would be exactly the mid-request reload leak
1422    /// `LIFECYCLE.md` §2.5 rules out.
1423    ///
1424    /// Takes no proxy handle either: the backend set arrives as `selector`, a
1425    /// capability borrowed for this one call (see [`BackendSelector`]).
1426    ///
1427    /// Returns the chosen backend, reserved but not dialled: the caller
1428    /// connects to [`BackendId::address`] (#1684).
1429    pub fn backend_from_request(
1430        &mut self,
1431        cluster_id: &str,
1432        frontend_should_stick: bool,
1433        context: &mut HttpContext,
1434        selector: &mut dyn BackendSelector,
1435    ) -> Result<BackendId, BackendConnectionError> {
1436        // Spelled out rather than folded into the selector's signature: a
1437        // cookie the client sent to a frontend that does not stick is
1438        // ignored, exactly as the `(false, Some(_))` arm of the match this
1439        // replaced ignored it.
1440        let affinity = if frontend_should_stick {
1441            Affinity::Sticky(context.sticky_session_found.as_deref())
1442        } else {
1443            Affinity::Unpinned
1444        };
1445        let selected = selector
1446            .select(cluster_id, affinity, context.affinity_key)
1447            .map_err(|backend_error| {
1448                trace!("{} {}", log_module_context!(context), backend_error);
1449                BackendConnectionError::Backend(backend_error)
1450            })?;
1451        debug_assert_eq!(
1452            selected.sticky_session.is_some(),
1453            frontend_should_stick,
1454            "a selector resolves a sticky cookie exactly when the frontend sticks"
1455        );
1456
1457        // `context.sticky_name` is the name `Context::create_stream` captured
1458        // when this request arrived, and it stays that name to the end of the
1459        // request. Re-reading the listener here used to hand an in-flight
1460        // request a cookie name an operator installed after it started — the
1461        // request would then have been matched on the old name and answered
1462        // with a `Set-Cookie` under the new one. A reload applies from the
1463        // next request (`LIFECYCLE.md` §2.5).
1464        if let Some(sticky_session) = selected.sticky_session {
1465            context.sticky_session = Some(sticky_session);
1466        }
1467
1468        context.backend_id = Some(Rc::clone(&selected.backend.backend_id));
1469        context.backend_address = Some(selected.backend.address);
1470
1471        Ok(selected.backend)
1472    }
1473}
1474
1475/// The worker's backend set, as the one capability
1476/// [`Router::backend_from_request`] needs from it: pick a backend and reserve
1477/// a connection on it.
1478///
1479/// Question 12 of [#1340](https://github.com/sozu-proxy/sozu/issues/1340)
1480/// asked for a borrowed view of backend load state, "the same shape"
1481/// `RoutingView` gave cluster configuration. That shape cannot select.
1482/// `RoutingView` is an immutable borrow of immutable data, and selection
1483/// mutates: the round-robin cursor, the fail-open latch, `PeakEWMA` under
1484/// `LoadMetric::ConnectionTime`, the Maglev table and the cluster
1485/// availability latch. A snapshot of the load counters cannot be taken
1486/// without allocating either — each backend sits behind its own `RefCell`, so
1487/// lending the core N of them at once needs a container of N `Ref`s. The shape
1488/// that fits is the UDP core's `BackendSource` (`lib/src/protocol/udp/mod.rs`),
1489/// a `&mut dyn` the core calls and the embedder implements.
1490///
1491/// # Selection reserves, a failed dial releases
1492///
1493/// The selector returns a backend and no socket: the embedder dials
1494/// [`BackendId::address`] itself, which is what lets a selector exist without
1495/// an OS socket type (#1684). What keeps the load balancer honest is that
1496/// selecting **reserves**: the embedder's selector raises the chosen
1497/// backend's `active_connections` under the same borrow as the choice
1498/// (`BackendMap::reserve_backend`), so the second stream linking in the same
1499/// pass weighs the first one's connection before it has been dialled, exactly
1500/// as it weighed the connection `Backend::try_connect` used to count at the
1501/// dial. A `connect(2)` that fails releases the reservation through the
1502/// ledger (`BackendChange::DialFailed`), and LIFECYCLE §9 invariant 14's
1503/// drain before each selection applies it before anything reads the counters
1504/// again. A dial that connects but is abandoned before its connection is
1505/// registered — no buffer for an H2 client, a refused stream start, a failed
1506/// `register_socket` — releases it through `BackendChange::ConnectionClosed`,
1507/// recording no failure (#1713). A dial that succeeds keeps the reservation
1508/// as its connection's count, and the connection's close releases it.
1509///
1510/// # Why a trait and not `&mut BackendMap`
1511///
1512/// A bare `&mut BackendMap` would be smaller, and it would still leave
1513/// [`Router::backend_from_request`] handing back an `Rc<RefCell<Backend>>`,
1514/// because turning that handle into the opaque [`BackendId`] takes the
1515/// session's `BackendRegistry` (`lib/src/protocol/mux/mod.rs`) as well. The
1516/// embedder's implementation holds both, so no registry handle reaches this
1517/// file at all. It is also what lets a simulator stand in for the backend set:
1518/// such a selector mints its ids with [`BackendId::new`] and needs no socket.
1519///
1520/// `Router` has no lifetime parameter, so it cannot keep the `&mut` it is
1521/// lent; the `Rc<RefCell<dyn L7Proxy>>` this replaced was `'static` and
1522/// `Clone`, and nothing stopped it being stored.
1523pub trait BackendSelector {
1524    /// Select a backend of `cluster_id` and reserve a connection on it.
1525    ///
1526    /// Under [`Affinity::Sticky`] the result carries the cookie value to
1527    /// answer with, read from the chosen backend under the same borrow as the
1528    /// selection. It is resolved here rather than carried on [`BackendId`]
1529    /// because `Backend::sticky_id` is not immutable: `BackendList::add_backend`
1530    /// (`lib/src/backends.rs`) rewrites it in place on the live registry entry
1531    /// when an existing backend is re-added, and `BackendId` carries only what
1532    /// cannot change for the life of an entry.
1533    ///
1534    /// `key` is the client affinity key `affinity_key` in this module derived
1535    /// for the request, `None` unless the cluster selects with `HRW` or
1536    /// `MAGLEV`. It decides where the load balancer sends a request the sticky
1537    /// cookie did not pin, and nothing else: a cookie naming a live backend
1538    /// still wins.
1539    fn select(
1540        &mut self,
1541        cluster_id: &str,
1542        affinity: Affinity<'_>,
1543        key: Option<u64>,
1544    ) -> Result<SelectedBackend, BackendError>;
1545}
1546
1547/// Whether selection for `cluster` reads a client affinity key: its policy
1548/// is `HRW` or `MAGLEV`, or it shards (`shard_percent`), which ranks each
1549/// client's shard by the key under any policy (sozu-proxy/sozu#524). Every
1550/// other cluster derives no key and pays nothing.
1551pub(crate) fn cluster_reads_affinity_key(cluster: &Cluster) -> bool {
1552    cluster.shard_percent.is_some()
1553        || matches!(
1554            cluster.load_balancing(),
1555            LoadBalancingAlgorithms::Hrw | LoadBalancingAlgorithms::Maglev
1556        )
1557}
1558
1559/// The client affinity key of a request routed to `cluster`, for the `HRW`
1560/// and `MAGLEV` policies to pin the client with and for shuffle sharding to
1561/// rank its shard by; `None` for every other cluster, which would ignore it
1562/// ([`cluster_reads_affinity_key`]).
1563///
1564/// The key is the hash of the cluster's `affinity_header` value (first
1565/// occurrence, name matched case-insensitively) or `affinity_cookie` value
1566/// (name matched case-sensitively, RFC 6265), and, when the request does not
1567/// carry that header or cookie, or carries it empty, of the client's source
1568/// IP — `session_address`, the PROXY-protocol source when the listener
1569/// expects one. With no source address either, there is no key, and the
1570/// policies fall back to round-robin as they always did.
1571///
1572/// A cookie named like the listener's sticky-session cookie is read from the
1573/// value `HttpContext::on_request_headers` captured, since that cookie is
1574/// elided from the jar before routing. Values are hashed raw, as sent: a
1575/// quoted cookie value and its unquoted spelling are two different keys.
1576///
1577/// It reads the header blocks and cookie jar where they lie in the request
1578/// buffer and hashes the bytes in place: no copy, no allocation. The hash is
1579/// the seeded FNV of `crate::load_balancing`, so every worker and every
1580/// restart derives the same key for the same client.
1581///
1582/// A header or cookie value is chosen by the client, so a client can choose
1583/// its backend under this option — exactly as it can with a sticky-session
1584/// cookie. It spreads clients; it does not isolate them from one another.
1585pub(super) fn affinity_key(
1586    cluster: &Cluster,
1587    front: &GenericHttpStream,
1588    session_address: Option<SocketAddr>,
1589    sticky: StickyCookie<'_>,
1590) -> Option<u64> {
1591    if !cluster_reads_affinity_key(cluster) {
1592        return None;
1593    }
1594    // `validate_affinity_key` (`command/src/config.rs`) refuses a cluster
1595    // naming both on every `AddCluster` the master accepts; the header is
1596    // read first should a worker ever receive both.
1597    let buf = front.storage.buffer();
1598    let value = if let Some(name) = cluster.affinity_header.as_deref() {
1599        front.blocks.iter().find_map(|block| match block {
1600            Block::Header(header)
1601                if !header.is_elided()
1602                    && compare_no_case(header.key.data(buf), name.as_bytes()) =>
1603            {
1604                Some(header.val.data(buf))
1605            }
1606            _ => None,
1607        })
1608    } else if let Some(name) = cluster.affinity_cookie.as_deref() {
1609        if name == sticky.name {
1610            // `HttpContext::on_request_headers` has already elided this
1611            // cookie from the jar (Sōzu's own session cookie never reaches a
1612            // backend) and captured its value, so the key reads the capture.
1613            sticky.value.map(str::as_bytes)
1614        } else {
1615            front.detached.jar.iter().find_map(|cookie| {
1616                (!cookie.is_elided() && cookie.key.data(buf) == name.as_bytes())
1617                    .then(|| cookie.val.data(buf))
1618            })
1619        }
1620    } else {
1621        None
1622    };
1623    // Pre: a request value is read only for a cluster that names one.
1624    debug_assert!(
1625        value.is_none() || cluster.affinity_header.is_some() || cluster.affinity_cookie.is_some(),
1626        "a header or cookie value keys only a cluster that configured its name"
1627    );
1628    let key = match value.filter(|value| !value.is_empty()) {
1629        Some(value) => Some(affinity_key_from_value(value)),
1630        None => session_address.map(|address| affinity_key_from_ip(address.ip())),
1631    };
1632    // Post: under a keyed policy the only request left without a key is one
1633    // that carries no usable value AND has no source address.
1634    debug_assert!(
1635        key.is_some() || session_address.is_none(),
1636        "a request with a source address must always be keyed"
1637    );
1638    key
1639}
1640
1641/// The request headers of a front stream, as a router lookup reads them to
1642/// evaluate a frontend's header predicates (sozu-proxy/sozu#1253).
1643///
1644/// Same idiom as [`affinity_key`]: the header blocks are read where they
1645/// lie in the request buffer, an elided block is skipped, names compare
1646/// case-insensitively, and the first occurrence answers, raw as sent. No
1647/// copy, no allocation — and nothing at all for a lookup that meets no
1648/// predicate, which never calls it. Pseudo-headers and cookies are not
1649/// header blocks, which is why `validate_header_predicate`
1650/// (`command/src/config.rs`) refuses `Host` and `Cookie`.
1651struct FrontRequestHeaders<'a>(&'a GenericHttpStream);
1652
1653impl RequestHeaders for FrontRequestHeaders<'_> {
1654    fn header(&self, name: &[u8]) -> Option<&[u8]> {
1655        let buf = self.0.storage.buffer();
1656        self.0.blocks.iter().find_map(|block| match block {
1657            Block::Header(header)
1658                if !header.is_elided() && compare_no_case(header.key.data(buf), name) =>
1659            {
1660                Some(header.val.data(buf))
1661            }
1662            _ => None,
1663        })
1664    }
1665}
1666
1667/// The listener's sticky-session cookie as the request arrived with it: its
1668/// name, and the value `HttpContext::on_request_headers` captured before
1669/// eliding it from the cookie jar. [`affinity_key`] reads it when a cluster's
1670/// `affinity_cookie` names that same cookie.
1671#[derive(Clone, Copy, Debug)]
1672pub(super) struct StickyCookie<'a> {
1673    pub(super) name: &'a str,
1674    pub(super) value: Option<&'a str>,
1675}
1676
1677/// Whether a request is pinned to a backend by its sticky-session cookie.
1678#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1679pub enum Affinity<'a> {
1680    /// The frontend does not stick: any cookie the client sent is ignored.
1681    Unpinned,
1682    /// The frontend sticks. `Some` is the cookie the request carried, tried
1683    /// first; `None`, or a cookie naming no live backend, falls back to the
1684    /// cluster's load balancer. Either way the answer carries a cookie.
1685    Sticky(Option<&'a str>),
1686}
1687
1688/// What [`BackendSelector::select`] answers: the chosen backend, whose
1689/// [`BackendId::address`] the caller dials, and — only under
1690/// [`Affinity::Sticky`] — the cookie value that pins the client to it.
1691#[derive(Debug)]
1692pub struct SelectedBackend {
1693    pub backend: BackendId,
1694    pub sticky_session: Option<String>,
1695}
1696
1697/// Apply the frontend's request-side rewrite + header policy to the
1698/// request kawa. Mutations land before backend connect so the backend
1699/// wire carries the rewritten shape:
1700///
1701/// 1. If `rewritten_host` is set, replace the request-line authority
1702///    with the rewritten value, replace any existing `Host` request
1703///    header (so H1 backends see the same value the H2 `:authority`
1704///    would carry), and inject `X-Forwarded-Host` carrying the
1705///    pre-rewrite authority. The X-Forwarded-Host injection ONLY fires
1706///    when `rewritten_host` is set — without a rewrite there is no host
1707///    swap to disclose, and HAProxy's `option forwardfor` style
1708///    headers (`X-Forwarded-For`, `X-Forwarded-Proto`) still flow from
1709///    the kawa parser. The pre-rewrite authority is read from
1710///    `context.authority`, which no rewrite touches, and recorded in
1711///    `context.original_authority` only here, so a request without a
1712///    host rewrite copies no authority. `X-Forwarded-Host` belongs to the
1713///    `X-Forwarded-*` family, so it is injected (and a client one
1714///    replaced) only when the listener's `forwarded_headers` mode emits
1715///    that family (`both`, `x_forwarded`); `rfc7239` and `none` convey no
1716///    pre-rewrite authority, and under `none` a client `X-Forwarded-Host`
1717///    passes through as sent.
1718///    Dedup rule: the synthetic Host AND any pre-existing Host header
1719///    are dropped in the retain pass below before the rewritten Host is
1720///    appended, so the wire never carries two `Host:` headers.
1721/// 2. If `rewritten_path` is set, replace both the abstract path
1722///    (consumed by H2 `:path`) and the request-line URI (consumed by
1723///    the H1 converter) so cardinality H1↔H1, H1↔H2, H2↔H1, H2↔H2 all
1724///    propagate the rewritten target.
1725/// 3. For every `headers_request` edit:
1726///    - empty `val` → remove every existing header with the matching
1727///      name from `kawa.blocks` (HAProxy `del-header` parity);
1728///    - non-empty `val` → append the header before the `end_header`
1729///      flag block. Set/replace semantics: callers that want to replace
1730///      a header pass two edits (one delete with empty val, one set
1731///      with the new value).
1732fn apply_request_rewrites_and_headers(
1733    kawa: &mut super::GenericHttpStream,
1734    context: &mut HttpContext,
1735    rewritten_host: Option<&str>,
1736    rewritten_path: Option<&str>,
1737    headers_request: &[HeaderEdit],
1738) {
1739    use kawa::{Block, Pair, Store};
1740
1741    if rewritten_host.is_none() && rewritten_path.is_none() && headers_request.is_empty() {
1742        return;
1743    }
1744
1745    // `context.authority` is the authority the client sent: the rewrites
1746    // below mutate the kawa status line and headers, never the context. It
1747    // is copied only when the host is rewritten, for the X-Forwarded-Host
1748    // injection, and the copy then moves into `context.original_authority`.
1749    let original_authority: Option<String> = if rewritten_host.is_some() {
1750        context.authority.clone()
1751    } else {
1752        None
1753    };
1754
1755    // ── status-line authority / path rewrites ─────────────────────────
1756    // The kawa request status line carries both `path` and `uri` —
1757    // `path` is the abstract path (consumed by the H2 converter to
1758    // emit `:path`) while `uri` is the request-line URI (consumed by
1759    // the H1 converter at `kawa::protocol::h1::converter`). Both must
1760    // be mutated so an H1 frontend forwarding to an H1 backend AND an
1761    // H2 frontend forwarding to an H1 backend (or vice versa) see the
1762    // rewritten target on the wire.
1763    if (rewritten_host.is_some() || rewritten_path.is_some())
1764        && let kawa::StatusLine::Request {
1765            authority,
1766            path,
1767            uri,
1768            ..
1769        } = &mut kawa.detached.status_line
1770    {
1771        if let Some(new_host) = rewritten_host {
1772            *authority = Store::from_string(new_host.to_owned());
1773        }
1774        if let Some(new_path) = rewritten_path {
1775            *path = Store::from_string(new_path.to_owned());
1776            *uri = Store::from_string(new_path.to_owned());
1777        }
1778    }
1779
1780    // ── single-pass split: deletes vs. sets ───────────────────────────
1781    // Walk `headers_request` once and separate each edit into either the
1782    // delete list (empty val) or the insert list (non-empty val). Two
1783    // passes was wasteful when an operator stacks many `--header` flags;
1784    // one pass keeps the allocation profile flat.
1785    let host_lower = b"host";
1786    let xfh_lower = b"x-forwarded-host";
1787    let rewriting_host = rewritten_host.is_some();
1788    // X-Forwarded-Host discloses the rewrite only in a mode that emits the
1789    // X-Forwarded-* family; `rfc7239` already elided a client one in
1790    // `HttpContext::on_request_headers`, and `none` leaves it as sent.
1791    let synthesises_xfh = rewriting_host && emits_x_forwarded(context.forwarded_headers);
1792    let mut keys_to_drop: Vec<Vec<u8>> = Vec::with_capacity(headers_request.len() + 2);
1793    let mut to_insert: Vec<Block> = Vec::with_capacity(headers_request.len() + 2);
1794    // Track whether any operator-supplied edit names Host or
1795    // X-Forwarded-Host so we always dedup the existing kawa Host header
1796    // before inserting the operator's value. Without this, an operator
1797    // who sets `--header request=Host=evil` on a frontend WITHOUT
1798    // `--rewrite-host` lands TWO `Host:` headers on the backend wire —
1799    // a request-smuggling primitive on backends that pick last-Host
1800    // (CWE-444 cousin).
1801    let mut operator_overrides_host = false;
1802    let mut operator_overrides_xfh = false;
1803    for edit in headers_request {
1804        let key_is_host = edit.key.eq_ignore_ascii_case(host_lower);
1805        let key_is_xfh = edit.key.eq_ignore_ascii_case(xfh_lower);
1806        operator_overrides_host |= key_is_host;
1807        operator_overrides_xfh |= key_is_xfh;
1808        if edit.val.is_empty() {
1809            keys_to_drop.push(edit.key.iter().map(u8::to_ascii_lowercase).collect());
1810        } else {
1811            to_insert.push(Block::Header(Pair {
1812                key: Store::from_slice(&edit.key),
1813                val: Store::from_slice(&edit.val),
1814            }));
1815        }
1816    }
1817    if rewriting_host || operator_overrides_host {
1818        keys_to_drop.push(host_lower.to_vec());
1819    }
1820    if synthesises_xfh || operator_overrides_xfh {
1821        keys_to_drop.push(xfh_lower.to_vec());
1822    }
1823
1824    // ── delete pass on existing blocks ────────────────────────────────
1825    let buf_ptr = kawa.storage.buffer();
1826    if !keys_to_drop.is_empty() {
1827        // Read `key.data(buf_ptr)` only on non-elided headers — kawa's
1828        // earlier passes (HPACK decoder, H1 header parser) tag suppressed
1829        // headers with `Store::Empty` rather than removing them, and
1830        // calling `.data()` on `Store::Empty` panics in
1831        // `kawa-0.6.8/src/storage/repr.rs`. Pinning the guard explicitly
1832        // until kawa changes its policy.
1833        let buf = buf_ptr;
1834        kawa.blocks.retain(|block| {
1835            if let Block::Header(Pair { key, val: _ }) = block {
1836                if matches!(key, Store::Empty) {
1837                    return true;
1838                }
1839                let key_bytes = key.data(buf);
1840                // Both `keys_to_drop` and `key_lower` are pre-lowercased,
1841                // so a byte-equality compare is sufficient — a second
1842                // ASCII-fold pass via `compare_no_case` would just burn
1843                // cycles re-folding bytes that are already canonical.
1844                let key_lower: Vec<u8> = key_bytes.iter().map(u8::to_ascii_lowercase).collect();
1845                !keys_to_drop
1846                    .iter()
1847                    .any(|k| k.as_slice() == key_lower.as_slice())
1848            } else {
1849                true
1850            }
1851        });
1852    }
1853
1854    // ── insertion before the end-of-headers flag ──────────────────────
1855    // Every header we add (rewritten Host, X-Forwarded-Host,
1856    // operator-supplied set/append edits) must land before
1857    // `Block::Flags { end_header: true }` so the converter emits them
1858    // as part of the request header block. Synthetic Host/X-Forwarded-Host
1859    // are prepended (they describe the rewrite, not an operator policy).
1860    let end_header_idx = super::shared::end_of_headers_index(kawa);
1861
1862    if rewriting_host {
1863        let mut synth: Vec<Block> = Vec::with_capacity(2);
1864        if let Some(new_host) = rewritten_host {
1865            synth.push(Block::Header(Pair {
1866                key: Store::Static(b"Host"),
1867                val: Store::from_string(new_host.to_owned()),
1868            }));
1869        }
1870        if synthesises_xfh && let Some(orig) = original_authority.as_deref() {
1871            synth.push(Block::Header(Pair {
1872                key: Store::Static(b"X-Forwarded-Host"),
1873                val: Store::from_string(orig.to_owned()),
1874            }));
1875        }
1876        context.original_authority = original_authority;
1877        synth.append(&mut to_insert);
1878        to_insert = synth;
1879    }
1880    if !to_insert.is_empty() {
1881        let insert_at = end_header_idx.unwrap_or(kawa.blocks.len());
1882        for (offset, block) in to_insert.into_iter().enumerate() {
1883            kawa.blocks.insert(insert_at + offset, block);
1884        }
1885    }
1886}
1887
1888/// Copy a per-frontend response-edit slice into the per-stream
1889/// `HttpContext.headers_response` snapshot, applying `filter` to each
1890/// edit. The snapshot is cleared before the copy so a second pass on
1891/// the same context (the HSTS hoist + post-forward pattern in
1892/// `route_from_request`) overrides any earlier partial copy.
1893fn snapshot_response_edits<F>(target: &mut Vec<HeaderEditSnapshot>, src: &[HeaderEdit], filter: F)
1894where
1895    F: Fn(&HeaderEdit) -> bool,
1896{
1897    target.clear();
1898    for edit in src.iter().filter(|e| filter(e)) {
1899        target.push(HeaderEditSnapshot {
1900            key: edit.key.to_vec(),
1901            val: edit.val.to_vec(),
1902            mode: edit.mode,
1903        });
1904    }
1905}
1906
1907/// Resolve the protocol scheme to use when emitting a redirect's `Location`
1908/// header. Maps the proto enum onto `"http"` / `"https"`, with `USE_SAME`
1909/// preserving the request's scheme (HTTPS for TLS listeners, HTTP otherwise).
1910fn resolve_redirect_scheme(scheme: RedirectScheme, context: &HttpContext) -> &'static str {
1911    match scheme {
1912        RedirectScheme::UseHttps => "https",
1913        RedirectScheme::UseHttp => "http",
1914        RedirectScheme::UseSame => {
1915            if context.tls_server_name.is_some() {
1916                "https"
1917            } else {
1918                "http"
1919            }
1920        }
1921    }
1922}
1923
1924/// Build the `Location` URL for a redirect response. Defaults the port
1925/// suffix only when the operator provided one or when scheme defaults
1926/// would mismatch (port 80 on https / 443 on http stays implicit).
1927///
1928/// `host_override` and `path_override` carry the frontend's
1929/// `RewriteParts::run` output for `RedirectPolicy::PERMANENT` flows so
1930/// the 301 `Location` reflects `rewrite_host` / `rewrite_path` instead
1931/// of the original `:authority` / `:path`. The legacy
1932/// `cluster.https_redirect` path passes `None` for both — it has no
1933/// per-frontend rewrite knobs.
1934fn build_redirect_location(
1935    scheme: &str,
1936    context: &HttpContext,
1937    port: Option<u32>,
1938    host_override: Option<&str>,
1939    path_override: Option<&str>,
1940) -> String {
1941    let authority = host_override
1942        .or(context.authority.as_deref())
1943        .unwrap_or_default();
1944    let path = path_override.or(context.path.as_deref()).unwrap_or("/");
1945    // Strip an existing `:port` from the authority — operators typically
1946    // configure `https_redirect_port` precisely because the listener's
1947    // port differs from the redirect target. Bracketed IPv6 literals
1948    // like `[::1]` survive intact: `rsplit_once(':')` only triggers when
1949    // the suffix after the final `:` is entirely ASCII digits.
1950    let host_only = match authority.rsplit_once(':') {
1951        Some((host, port_part))
1952            if !port_part.is_empty() && port_part.bytes().all(|b| b.is_ascii_digit()) =>
1953        {
1954            host
1955        }
1956        _ => authority,
1957    };
1958    let port_suffix = match port {
1959        Some(80) if scheme == "http" => String::new(),
1960        Some(443) if scheme == "https" => String::new(),
1961        Some(p) => format!(":{p}"),
1962        None => String::new(),
1963    };
1964    format!("{scheme}://{host_only}{port_suffix}{path}")
1965}
1966
1967/// Exact-match test between an HTTP `:authority` / `Host` value and a TLS SNI.
1968///
1969/// Matching rules:
1970///   * The authority is stripped of its optional `:port` suffix. RFC 6066 §3
1971///     forbids a port in the SNI extension, so the SNI is compared against
1972///     the host component only.
1973///   * The comparison is case-insensitive (RFC 9110 §4.2.3 — hosts are
1974///     case-insensitive). The SNI is assumed to be already lowercased by
1975///     the caller (see `https.rs::upgrade_handshake`); only the authority
1976///     side needs on-the-fly `to_ascii_lowercase`.
1977///   * No wildcard logic: if the operator serves a wildcard certificate,
1978///     the SNI negotiated by the client is still the specific name that
1979///     client sent, and the request `:authority` must equal that specific
1980///     name exactly. This is the tightest possible TLS trust boundary.
1981///
1982/// The `:port` suffix is only stripped when the suffix is non-empty and
1983/// entirely ASCII digits. This keeps bracketed IPv6 literals like `[::1]`
1984/// intact: `rsplit_once(':')` would otherwise mis-split them.
1985pub(crate) fn authority_matches_sni(authority: &str, sni_lowercased: &str) -> bool {
1986    let host = strip_authority_port(authority);
1987    if host.len() != sni_lowercased.len() {
1988        return false;
1989    }
1990    host.as_bytes()
1991        .iter()
1992        .zip(sni_lowercased.as_bytes())
1993        .all(|(a, b)| a.to_ascii_lowercase() == *b)
1994}
1995
1996/// Strip the optional `:port` suffix from an authority value. Bracketed
1997/// IPv6 literals (`[::1]`, `[::1]:8443`) keep their inner colons intact:
1998/// the suffix is only stripped when the tail after the last `:` is
1999/// non-empty and entirely ASCII digits.
2000fn strip_authority_port(authority: &str) -> &str {
2001    match authority.rsplit_once(':') {
2002        Some((h, port)) if !port.is_empty() && port.bytes().all(|b| b.is_ascii_digit()) => h,
2003        _ => authority,
2004    }
2005}
2006
2007/// RFC 6125 §6.4.3 wildcard-aware match of `:authority` against a SAN set
2008/// snapshot taken at TLS handshake.
2009///
2010/// Returns the matched SAN entry on success so the caller can log it.
2011///
2012/// Matching rules:
2013///   * Port suffix on the authority is stripped (same logic as
2014///     [`authority_matches_sni`], IPv6-bracket safe).
2015///   * Compare is ASCII case-insensitive (`:authority` is ASCII per
2016///     RFC 9113 §8.3.1; SAN entries are stored pre-lowercased by
2017///     `https.rs::upgrade_handshake`).
2018///   * `*.suffix` matches exactly one DNS label at the leftmost position
2019///     and only when that label is non-empty: it does NOT match the apex,
2020///     does NOT cross dots, and embedded wildcards (`foo.*.example.com`,
2021///     `*foo.example.com`) are forbidden.
2022///   * Empty `names` ⇒ `None` (default-cert path — Sōzu is not
2023///     authoritative for any name).
2024pub(crate) fn authority_matched_cert_name<'a>(
2025    authority: &str,
2026    names: &'a [String],
2027) -> Option<&'a str> {
2028    let mut host = strip_authority_port(authority);
2029    // RFC 1034 §3.1 absolute-form: `example.com.` and `example.com` name
2030    // the same host. The SAN snapshot already strips trailing dots at
2031    // `https.rs::upgrade_handshake`, and the SNI side strips them at the
2032    // same site; strip on the authority side so a client emitting
2033    // absolute-form `:authority` (or H1 `Host`) does not get a false 421.
2034    // Only one trailing dot is removed because RFC 1034 forbids multiple
2035    // trailing dots on a domain literal.
2036    if let Some(trimmed) = host.strip_suffix('.') {
2037        host = trimmed;
2038    }
2039    if host.is_empty() {
2040        return None;
2041    }
2042    for entry in names {
2043        if let Some(suffix) = entry.strip_prefix("*.") {
2044            // RFC 6125 §6.4.3: the wildcard label is the *entire* left-most
2045            // label. Embedded wildcards (`f*.example.com`, `*f.example.com`)
2046            // are rejected because we reach this branch only when the entry
2047            // starts with the exact two bytes `*.`. We still must reject
2048            // wildcards anywhere else in the entry by requiring no further
2049            // `*` in `suffix`.
2050            if suffix.contains('*') {
2051                continue;
2052            }
2053            // Authority has the form `<left-most-label>.<rest>`; the
2054            // wildcard substitutes for exactly that left-most label, which
2055            // must be non-empty and contain no dot.
2056            let Some((leftmost, rest)) = host.split_once('.') else {
2057                continue;
2058            };
2059            if leftmost.is_empty() {
2060                continue;
2061            }
2062            if rest.eq_ignore_ascii_case(suffix) {
2063                return Some(entry);
2064            }
2065            continue;
2066        }
2067        if entry.contains('*') {
2068            // Internal wildcards (`foo.*.example.com`) are not RFC 6125-
2069            // valid. Skip rather than mis-match.
2070            continue;
2071        }
2072        if host.eq_ignore_ascii_case(entry) {
2073            return Some(entry);
2074        }
2075    }
2076    None
2077}
2078
2079#[cfg(test)]
2080mod tests {
2081    use std::{cell::RefCell, collections::BTreeMap, rc::Rc, time::Duration};
2082
2083    use sozu_command::{
2084        config::ListenerBuilder,
2085        proto::command::{
2086            ForwardedHeaders, PathRule, RedirectPolicy, RequestHttpFrontend, RulePosition,
2087            SocketAddress,
2088        },
2089    };
2090
2091    use super::{Router, RoutingView, authority_matches_sni};
2092    use crate::{
2093        L7Proxy,
2094        http::HttpProxy,
2095        protocol::{
2096            http::{editor::HttpContext, parser::Method},
2097            mux::{buffer_source::PoolBufferSource, stream::Stream},
2098        },
2099    };
2100
2101    /// A frontend declared as `*.example.com` and carrying `--tags` must put
2102    /// those tags on the access-log line of a request whose authority is
2103    /// `foo.example.com` (sozu#1379).
2104    ///
2105    /// The bug this pins: the tag cache was WRITTEN under the frontend
2106    /// RULE's hostname (`listener.set_tags(front.hostname, …)` in
2107    /// `lib/src/http.rs` / `lib/src/https.rs`) and READ back under the
2108    /// REQUEST's authority (`listener.get_tags(hostname)` in
2109    /// `lib/src/protocol/mux/stream.rs`), against a `BTreeMap<String,
2110    /// CachedTags>` that does exact-key lookup only. `*.example.com` is a
2111    /// key `foo.example.com` can never produce, so every access-log line of
2112    /// every wildcard, regex, ported or differently-cased frontend lost its
2113    /// tags silently. The fix resolves the tags through the router, which
2114    /// already knows which frontend rule matched.
2115    ///
2116    /// The test drives the production add path (`HttpProxy::add_http_frontend`),
2117    /// the production routing path (`Router::route_from_request`) and the
2118    /// production emit path (`Stream::generate_access_log`), and asserts on
2119    /// the rendered access-log line rather than on any intermediate field,
2120    /// so it stays valid whatever the resolution mechanism becomes.
2121    ///
2122    /// To SEE THIS RED: in `Router::route_from_request`
2123    /// (`lib/src/protocol/mux/router.rs`), replace the
2124    /// `context.tags = tags;` stash that follows the `RouteResult`
2125    /// destructure with `let _ = tags;`. The access log then falls back to
2126    /// the authority-keyed listener map, which holds only the
2127    /// `*.example.com` key, and the line is emitted with an empty `[]` tag
2128    /// field. Dropping `|| front.tags.is_some()` from `has_policy` in
2129    /// `Router::add_http_front_with_hsts_origin`
2130    /// (`lib/src/router/mod.rs`) reddens it the same way, one step
2131    /// earlier: the frontend is then stored as a tagless
2132    /// `Route::ClusterId` and there is nothing left for the stash to
2133    /// carry.
2134    #[test]
2135    fn a_wildcard_frontends_tags_reach_the_access_log_of_a_concrete_authority() {
2136        const TAG: &str = "owner=team-wildcard-1379";
2137
2138        let output = crate::capture_test_logs(|| {
2139            let port = crate::testing::provide_port();
2140            let address = SocketAddress::new_v4(127, 0, 0, 1, port);
2141            let config = ListenerBuilder::new_http(address)
2142                .to_http(None)
2143                .expect("test http listener config must build");
2144            let parts = crate::testing::prebuild_server(10, 16_384, false)
2145                .expect("test server parts must build");
2146            let pool = parts.pool.clone();
2147            let mut proxy =
2148                HttpProxy::new(parts.registry, parts.sessions, parts.pool, parts.backends);
2149            let token = mio::Token(0);
2150            proxy
2151                .add_listener(config, token)
2152                .expect("test listener must register");
2153            proxy
2154                .add_http_frontend(RequestHttpFrontend {
2155                    cluster_id: Some("cluster-1379".to_owned()),
2156                    address,
2157                    hostname: "*.example.com".to_owned(),
2158                    path: PathRule::prefix("/".to_owned()),
2159                    position: RulePosition::Tree.into(),
2160                    tags: BTreeMap::from([("owner".to_owned(), "team-wildcard-1379".to_owned())]),
2161                    ..Default::default()
2162                })
2163                .expect("the wildcard frontend must register");
2164            let listener = proxy
2165                .get_listener(&token)
2166                .expect("the registered listener must be reachable");
2167            let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(proxy));
2168
2169            let mut context = HttpContext::new(
2170                rusty_ulid::Ulid::generate(),
2171                rusty_ulid::Ulid::generate(),
2172                crate::Protocol::HTTP,
2173                "127.0.0.1:80"
2174                    .parse()
2175                    .expect("test public address must parse"),
2176                Some(
2177                    "127.0.0.1:12345"
2178                        .parse()
2179                        .expect("test session address must parse"),
2180                ),
2181                "SOZUBALANCEID".to_owned(),
2182                "Sozu-Id".to_owned(),
2183                false,
2184                false,
2185            );
2186            context.authority = Some("foo.example.com".to_owned());
2187            context.path = Some("/".to_owned());
2188            context.method = Some(Method::Get);
2189
2190            let mut stream = Stream::new(
2191                &mut PoolBufferSource::new(Rc::downgrade(&pool)),
2192                context,
2193                crate::protocol::mux::test_support::test_answers(),
2194                65_535,
2195            )
2196            .expect("test stream must check out its buffers");
2197            let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
2198            let (front, stream_context) = {
2199                let split = &mut stream;
2200                (&mut split.front, &mut split.context)
2201            };
2202            let proxy_ref = proxy.borrow();
2203            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
2204            router
2205                .route_from_request(stream_context, front, &listener, &view)
2206                .expect("the wildcard frontend must route foo.example.com");
2207            drop(proxy_ref);
2208
2209            // This test asserts on the access log, not on metrics.
2210            let _ = stream.generate_access_log(false, None, listener, None, None);
2211        });
2212
2213        assert!(
2214            output.contains(TAG),
2215            "the access log of a request routed by `*.example.com` must carry that frontend's tags, got: {output}"
2216        );
2217    }
2218
2219    /// `HttpContext::original_authority` records the authority the client
2220    /// sent only when the frontend rewrites the host, the one case where it
2221    /// is sent on as `X-Forwarded-Host`; a request without a host rewrite
2222    /// leaves it `None` instead of paying a copy of the authority (#1589).
2223    ///
2224    /// To SEE THIS RED: stamp `context.original_authority` with a copy of
2225    /// `host` in `Router::route_from_request` before the frontend lookup,
2226    /// as it was before #1589. The plain request then carries
2227    /// `Some("plain.example.com")`.
2228    #[test]
2229    fn only_a_host_rewrite_records_the_original_authority() {
2230        let port = crate::testing::provide_port();
2231        let address = SocketAddress::new_v4(127, 0, 0, 1, port);
2232        let config = ListenerBuilder::new_http(address)
2233            .to_http(None)
2234            .expect("test http listener config must build");
2235        let parts = crate::testing::prebuild_server(10, 16_384, false)
2236            .expect("test server parts must build");
2237        let pool = parts.pool.clone();
2238        let mut proxy = HttpProxy::new(parts.registry, parts.sessions, parts.pool, parts.backends);
2239        let token = mio::Token(0);
2240        proxy
2241            .add_listener(config, token)
2242            .expect("test listener must register");
2243        for (hostname, rewrite_host) in [
2244            ("rewrite.example.com", Some("internal.example.com")),
2245            ("plain.example.com", None),
2246        ] {
2247            proxy
2248                .add_http_frontend(RequestHttpFrontend {
2249                    cluster_id: Some("cluster-1589".to_owned()),
2250                    address,
2251                    hostname: hostname.to_owned(),
2252                    path: PathRule::prefix("/".to_owned()),
2253                    position: RulePosition::Tree.into(),
2254                    rewrite_host: rewrite_host.map(str::to_owned),
2255                    ..Default::default()
2256                })
2257                .unwrap_or_else(|error| panic!("{hostname} must register: {error}"));
2258        }
2259        let listener = proxy
2260            .get_listener(&token)
2261            .expect("the registered listener must be reachable");
2262        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(proxy));
2263        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
2264
2265        for (authority, expected) in [
2266            ("rewrite.example.com", Some("rewrite.example.com")),
2267            ("plain.example.com", None),
2268        ] {
2269            let mut context = HttpContext::new(
2270                rusty_ulid::Ulid::generate(),
2271                rusty_ulid::Ulid::generate(),
2272                crate::Protocol::HTTP,
2273                "127.0.0.1:80"
2274                    .parse()
2275                    .expect("test public address must parse"),
2276                None,
2277                "SOZUBALANCEID".to_owned(),
2278                "Sozu-Id".to_owned(),
2279                false,
2280                false,
2281            );
2282            context.authority = Some(authority.to_owned());
2283            context.path = Some("/".to_owned());
2284            context.method = Some(Method::Get);
2285            let mut stream = Stream::new(
2286                &mut PoolBufferSource::new(Rc::downgrade(&pool)),
2287                context,
2288                crate::protocol::mux::test_support::test_answers(),
2289                65_535,
2290            )
2291            .expect("test stream must check out its buffers");
2292            let split = &mut stream;
2293            let proxy_ref = proxy.borrow();
2294            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
2295            router
2296                .route_from_request(&mut split.context, &mut split.front, &listener, &view)
2297                .unwrap_or_else(|error| panic!("{authority} must route: {error}"));
2298
2299            assert_eq!(
2300                split.context.original_authority.as_deref(),
2301                expected,
2302                "{authority}: original_authority is recorded exactly when the host is rewritten",
2303            );
2304            let forwarded_host = split.front.blocks.iter().find_map(|block| match block {
2305                kawa::Block::Header(pair)
2306                    if pair.key.data(split.front.storage.buffer()) == b"X-Forwarded-Host" =>
2307                {
2308                    Some(pair.val.data(split.front.storage.buffer()).to_vec())
2309                }
2310                _ => None,
2311            });
2312            assert_eq!(
2313                forwarded_host.as_deref(),
2314                expected.map(str::as_bytes),
2315                "{authority}: X-Forwarded-Host carries the authority the client sent, on a rewrite only",
2316            );
2317        }
2318    }
2319
2320    /// A frontend whose policy is `RedirectPolicy::SilentClose` refuses the
2321    /// request with `RetrieveClusterError::SilentClose` whether or not it is
2322    /// bound to a cluster, before any redirect or 401 decision, so the caller
2323    /// writes no response at all (sozu-proxy/sozu#767).
2324    ///
2325    /// To SEE THIS RED: delete the `RedirectPolicy::SilentClose` early return
2326    /// that opens the policy steps of `Router::route_from_request`
2327    /// (`lib/src/protocol/mux/router.rs`). The bound frontend then forwards
2328    /// (`Ok`) and the clusterless one answers `UnauthorizedRoute`.
2329    #[test]
2330    fn a_silent_close_frontend_refuses_without_an_answer_bound_or_clusterless() {
2331        let port = crate::testing::provide_port();
2332        let address = SocketAddress::new_v4(127, 0, 0, 1, port);
2333        let config = ListenerBuilder::new_http(address)
2334            .to_http(None)
2335            .expect("test http listener config must build");
2336        let parts = crate::testing::prebuild_server(10, 16_384, false)
2337            .expect("test server parts must build");
2338        let pool = parts.pool.clone();
2339        let mut proxy = HttpProxy::new(parts.registry, parts.sessions, parts.pool, parts.backends);
2340        let token = mio::Token(0);
2341        proxy
2342            .add_listener(config, token)
2343            .expect("test listener must register");
2344        for (hostname, cluster_id) in [
2345            ("bound.example.com", Some("cluster-767")),
2346            ("clusterless.example.com", None),
2347        ] {
2348            proxy
2349                .add_http_frontend(RequestHttpFrontend {
2350                    cluster_id: cluster_id.map(str::to_owned),
2351                    address,
2352                    hostname: hostname.to_owned(),
2353                    path: PathRule::prefix("/".to_owned()),
2354                    position: RulePosition::Tree.into(),
2355                    redirect: Some(RedirectPolicy::SilentClose as i32),
2356                    ..Default::default()
2357                })
2358                .unwrap_or_else(|error| panic!("{hostname} must register: {error}"));
2359        }
2360        let listener = proxy
2361            .get_listener(&token)
2362            .expect("the registered listener must be reachable");
2363        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(proxy));
2364        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
2365
2366        for authority in ["bound.example.com", "clusterless.example.com"] {
2367            let mut context = HttpContext::new(
2368                rusty_ulid::Ulid::generate(),
2369                rusty_ulid::Ulid::generate(),
2370                crate::Protocol::HTTP,
2371                "127.0.0.1:80"
2372                    .parse()
2373                    .expect("test public address must parse"),
2374                None,
2375                "SOZUBALANCEID".to_owned(),
2376                "Sozu-Id".to_owned(),
2377                false,
2378                false,
2379            );
2380            context.authority = Some(authority.to_owned());
2381            context.path = Some("/".to_owned());
2382            context.method = Some(Method::Get);
2383            let mut stream = Stream::new(
2384                &mut PoolBufferSource::new(Rc::downgrade(&pool)),
2385                context,
2386                crate::protocol::mux::test_support::test_answers(),
2387                65_535,
2388            )
2389            .expect("test stream must check out its buffers");
2390            let split = &mut stream;
2391            let proxy_ref = proxy.borrow();
2392            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
2393            let result =
2394                router.route_from_request(&mut split.context, &mut split.front, &listener, &view);
2395
2396            assert!(
2397                matches!(result, Err(crate::RetrieveClusterError::SilentClose)),
2398                "{authority}: a silent-close frontend must refuse with SilentClose, got {result:?}",
2399            );
2400            assert_eq!(
2401                split.context.redirect_status, None,
2402                "{authority}: a silent close must not stage a redirect answer",
2403            );
2404        }
2405    }
2406
2407    /// On a host rewrite, `X-Forwarded-Host` follows the listener's
2408    /// `forwarded_headers` mode: `both` and `x_forwarded` replace a client
2409    /// one with the authority the client sent, `rfc7239` and `none` inject
2410    /// none, and `none` leaves a client one as sent. (`rfc7239` elided the
2411    /// client one earlier, in `HttpContext::on_request_headers`.)
2412    ///
2413    /// To SEE THIS RED: in `apply_request_rewrites_and_headers`, set
2414    /// `synthesises_xfh` to `rewriting_host` alone. `rfc7239` and `none` then
2415    /// carry `X-Forwarded-Host: rewrite.example.com`.
2416    #[test]
2417    fn a_host_rewrite_discloses_x_forwarded_host_only_in_x_forwarded_modes() {
2418        let port = crate::testing::provide_port();
2419        let address = SocketAddress::new_v4(127, 0, 0, 1, port);
2420        let config = ListenerBuilder::new_http(address)
2421            .to_http(None)
2422            .expect("test http listener config must build");
2423        let parts = crate::testing::prebuild_server(10, 16_384, false)
2424            .expect("test server parts must build");
2425        let pool = parts.pool.clone();
2426        let mut proxy = HttpProxy::new(parts.registry, parts.sessions, parts.pool, parts.backends);
2427        let token = mio::Token(0);
2428        proxy
2429            .add_listener(config, token)
2430            .expect("test listener must register");
2431        proxy
2432            .add_http_frontend(RequestHttpFrontend {
2433                cluster_id: Some("cluster-322".to_owned()),
2434                address,
2435                hostname: "rewrite.example.com".to_owned(),
2436                path: PathRule::prefix("/".to_owned()),
2437                position: RulePosition::Tree.into(),
2438                rewrite_host: Some("internal.example.com".to_owned()),
2439                ..Default::default()
2440            })
2441            .expect("the rewriting frontend must register");
2442        let listener = proxy
2443            .get_listener(&token)
2444            .expect("the registered listener must be reachable");
2445        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(proxy));
2446        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
2447
2448        for (mode, client_xfh, expected) in [
2449            (
2450                ForwardedHeaders::Both,
2451                true,
2452                vec![b"rewrite.example.com".to_vec()],
2453            ),
2454            (
2455                ForwardedHeaders::XForwarded,
2456                true,
2457                vec![b"rewrite.example.com".to_vec()],
2458            ),
2459            (ForwardedHeaders::Rfc7239, false, vec![]),
2460            (
2461                ForwardedHeaders::None,
2462                true,
2463                vec![b"client.example".to_vec()],
2464            ),
2465        ] {
2466            let mut context = HttpContext::new(
2467                rusty_ulid::Ulid::generate(),
2468                rusty_ulid::Ulid::generate(),
2469                crate::Protocol::HTTP,
2470                "127.0.0.1:80"
2471                    .parse()
2472                    .expect("test public address must parse"),
2473                None,
2474                "SOZUBALANCEID".to_owned(),
2475                "Sozu-Id".to_owned(),
2476                false,
2477                false,
2478            );
2479            context.forwarded_headers = mode;
2480            context.authority = Some("rewrite.example.com".to_owned());
2481            context.path = Some("/".to_owned());
2482            context.method = Some(Method::Get);
2483            let mut stream = Stream::new(
2484                &mut PoolBufferSource::new(Rc::downgrade(&pool)),
2485                context,
2486                crate::protocol::mux::test_support::test_answers(),
2487                65_535,
2488            )
2489            .expect("test stream must check out its buffers");
2490            if client_xfh {
2491                stream.front.push_block(kawa::Block::Header(kawa::Pair {
2492                    key: kawa::Store::Static(b"X-Forwarded-Host"),
2493                    val: kawa::Store::Static(b"client.example"),
2494                }));
2495            }
2496            let split = &mut stream;
2497            let proxy_ref = proxy.borrow();
2498            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
2499            router
2500                .route_from_request(&mut split.context, &mut split.front, &listener, &view)
2501                .unwrap_or_else(|error| panic!("{mode:?} must route: {error}"));
2502
2503            let buffer = split.front.storage.buffer();
2504            let forwarded_hosts: Vec<Vec<u8>> = split
2505                .front
2506                .blocks
2507                .iter()
2508                .filter_map(|block| match block {
2509                    kawa::Block::Header(pair)
2510                        if !pair.is_elided()
2511                            && pair
2512                                .key
2513                                .data(buffer)
2514                                .eq_ignore_ascii_case(b"X-Forwarded-Host") =>
2515                    {
2516                        Some(pair.val.data(buffer).to_vec())
2517                    }
2518                    _ => None,
2519                })
2520                .collect();
2521            assert_eq!(
2522                forwarded_hosts, expected,
2523                "{mode:?}: X-Forwarded-Host after a host rewrite"
2524            );
2525        }
2526    }
2527
2528    // Asserts on `debug!` output: gate on the same condition that compiles it in.
2529    #[cfg(any(debug_assertions, feature = "logs-debug", feature = "logs-trace"))]
2530    #[test]
2531    fn routing_runtime_logs_bound_method_authority_sni_and_certificate_names() {
2532        use super::{log_coalescing_accepted, log_sni_authority_mismatch};
2533
2534        const METHOD_SECRET: &str = "MUX_ROUTE_METHOD_SECRET_SENTINEL";
2535        const AUTHORITY_SECRET: &str = "MUX_ROUTE_AUTHORITY_SECRET_SENTINEL";
2536        const SNI_SECRET: &str = "MUX_ROUTE_SNI_SECRET_SENTINEL";
2537        const SAN_SECRET: &str = "MUX_ROUTE_SAN_SECRET_SENTINEL";
2538
2539        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
2540        let method = long_value(METHOD_SECRET);
2541        let authority = long_value(AUTHORITY_SECRET);
2542        let sni = long_value(SNI_SECRET);
2543        let san = long_value(SAN_SECRET);
2544        let method_len = method.len();
2545        let authority_len = authority.len();
2546        let sni_len = sni.len();
2547        let san_len = san.len();
2548
2549        let output = crate::capture_test_logs_at_level("debug", move || {
2550            let mut context = HttpContext::new(
2551                rusty_ulid::Ulid::generate(),
2552                rusty_ulid::Ulid::generate(),
2553                crate::Protocol::HTTPS,
2554                "127.0.0.1:443"
2555                    .parse()
2556                    .expect("test public address must parse"),
2557                Some(
2558                    "127.0.0.1:12345"
2559                        .parse()
2560                        .expect("test session address must parse"),
2561                ),
2562                String::new(),
2563                String::new(),
2564                false,
2565                false,
2566            );
2567            context.method = Some(crate::protocol::http::parser::Method::Custom(method));
2568            context.authority = Some(authority.clone());
2569            context.tls_cert_names = Some(std::sync::Arc::new(vec![san.clone()]));
2570
2571            log_coalescing_accepted(&context, &authority, &sni, &san);
2572            log_sni_authority_mismatch(
2573                &context,
2574                &authority,
2575                &sni,
2576                context.tls_cert_names.as_deref().map(Vec::as_slice),
2577            );
2578        });
2579
2580        for secret in [METHOD_SECRET, AUTHORITY_SECRET, SNI_SECRET, SAN_SECRET] {
2581            assert!(
2582                !output.contains(secret),
2583                "mux routing runtime log leaked {secret}: {output}"
2584            );
2585        }
2586        for metadata in [
2587            format!("bytes={method_len}"),
2588            format!("authority_bytes=Some({authority_len})"),
2589            format!("authority_bytes={authority_len}"),
2590            format!("sni_bytes={sni_len}"),
2591            format!("matched_san_bytes={san_len}"),
2592            "certificate_sans_count=1".to_owned(),
2593            format!("certificate_sans_bytes={san_len}"),
2594        ] {
2595            assert!(
2596                output.contains(&metadata),
2597                "mux routing runtime log omitted {metadata}: {output}"
2598            );
2599        }
2600        assert!(
2601            output.len() <= 2048,
2602            "mux routing runtime capture is not bounded: {} bytes",
2603            output.len()
2604        );
2605    }
2606
2607    #[test]
2608    fn match_exact() {
2609        assert!(authority_matches_sni("example.com", "example.com"));
2610    }
2611
2612    #[test]
2613    fn match_different_case() {
2614        assert!(authority_matches_sni("Example.COM", "example.com"));
2615    }
2616
2617    #[test]
2618    fn match_authority_with_port() {
2619        assert!(authority_matches_sni("example.com:8443", "example.com"));
2620    }
2621
2622    #[test]
2623    fn reject_different_host() {
2624        assert!(!authority_matches_sni(
2625            "tenant-b.example.com",
2626            "tenant-a.example.com"
2627        ));
2628    }
2629
2630    #[test]
2631    fn reject_substring_attack() {
2632        // Length check guards against an authority that is a prefix or
2633        // suffix of the SNI (or vice versa).
2634        assert!(!authority_matches_sni("example.co", "example.com"));
2635        assert!(!authority_matches_sni("example.commons", "example.com"));
2636    }
2637
2638    #[test]
2639    fn reject_wildcard_not_expanded() {
2640        // Wildcard cert selection happens at the cert-resolver layer; the SNI
2641        // we see here is the concrete name the client sent. Do not silently
2642        // accept `*.example.com` as matching `foo.example.com`.
2643        assert!(!authority_matches_sni("foo.example.com", "*.example.com"));
2644    }
2645
2646    #[test]
2647    fn ipv6_bracketed_literal_with_port() {
2648        // `[::1]:8443` must still match the SNI `[::1]`; only the trailing
2649        // `:8443` is a port (all digits → stripped).
2650        assert!(authority_matches_sni("[::1]:8443", "[::1]"));
2651    }
2652
2653    #[test]
2654    fn ipv6_bracketed_without_port() {
2655        // The `:` characters inside the brackets must not be mistaken for a
2656        // port separator: the tail after the last `:` is `1]`, not all
2657        // digits, so it is NOT stripped and the whole string compares.
2658        assert!(authority_matches_sni("[::1]", "[::1]"));
2659    }
2660}
2661
2662#[cfg(test)]
2663mod authority_matched_cert_name_tests {
2664    use super::authority_matched_cert_name;
2665
2666    #[test]
2667    fn cert_name_match_exact_single_san() {
2668        let names = vec!["example.com".to_owned()];
2669        assert_eq!(
2670            authority_matched_cert_name("example.com", &names),
2671            Some("example.com"),
2672        );
2673    }
2674
2675    #[test]
2676    fn cert_name_match_wildcard_left_most() {
2677        let names = vec!["*.cleverapps.io".to_owned()];
2678        assert_eq!(
2679            authority_matched_cert_name("staging-3.cleverapps.io", &names),
2680            Some("*.cleverapps.io"),
2681        );
2682    }
2683
2684    #[test]
2685    fn cert_name_reject_wildcard_apex() {
2686        // RFC 6125 §6.4.3: `*.example.com` does NOT cover the apex
2687        // `example.com` — the wildcard label must consume exactly one
2688        // non-empty label.
2689        let names = vec!["*.example.com".to_owned()];
2690        assert_eq!(authority_matched_cert_name("example.com", &names), None);
2691    }
2692
2693    #[test]
2694    fn cert_name_reject_wildcard_two_labels() {
2695        // `*.example.com` cannot cross dots: `a.b.example.com` has two
2696        // labels before `example.com` and must be rejected.
2697        let names = vec!["*.example.com".to_owned()];
2698        assert_eq!(authority_matched_cert_name("a.b.example.com", &names), None,);
2699    }
2700
2701    #[test]
2702    fn cert_name_reject_wildcard_not_left_most() {
2703        // Embedded wildcards (`foo.*.example.com`) are not RFC 6125-valid
2704        // and must be skipped, not mis-matched.
2705        let names = vec!["foo.*.example.com".to_owned()];
2706        assert_eq!(
2707            authority_matched_cert_name("foo.bar.example.com", &names),
2708            None,
2709        );
2710    }
2711
2712    #[test]
2713    fn cert_name_match_case_insensitive() {
2714        // ASCII case folding only — `:authority` is ASCII per RFC 9113
2715        // §8.3.1 and the snapshot is pre-lowercased at handshake.
2716        let names = vec!["EXAMPLE.com".to_owned()];
2717        assert!(authority_matched_cert_name("Example.COM", &names).is_some());
2718    }
2719
2720    #[test]
2721    fn cert_name_match_with_port() {
2722        // The port suffix on `:authority` must be stripped before the
2723        // SAN compare.
2724        let names = vec!["example.com".to_owned()];
2725        assert!(authority_matched_cert_name("example.com:8443", &names).is_some());
2726    }
2727
2728    #[test]
2729    fn cert_name_match_absolute_form_trailing_dot() {
2730        // RFC 1034 §3.1: an absolute-form domain literal carries one
2731        // trailing dot (`example.com.`) and resolves to the same host as
2732        // the relative form. The SAN snapshot stores the relative form
2733        // (https.rs strips the trailing dot at handshake), so the matcher
2734        // must strip it on the authority side too — otherwise a client
2735        // emitting an absolute-form `:authority` gets a false 421.
2736        let names = vec!["example.com".to_owned()];
2737        assert!(authority_matched_cert_name("example.com.", &names).is_some());
2738        // And with both port and trailing dot.
2739        assert!(authority_matched_cert_name("example.com.:8443", &names).is_some());
2740        // The wildcard branch must also accept the absolute form.
2741        let wildcard = vec!["*.example.com".to_owned()];
2742        assert!(authority_matched_cert_name("foo.example.com.", &wildcard).is_some());
2743    }
2744
2745    #[test]
2746    fn cert_name_match_idn_a_label() {
2747        // IDNA A-labels (xn--…) are ASCII and compare byte-for-byte once
2748        // the snapshot is lowercased.
2749        let names = vec!["xn--bcher-kva.example.com".to_owned()];
2750        assert!(authority_matched_cert_name("xn--bcher-kva.example.com", &names).is_some());
2751    }
2752
2753    #[test]
2754    fn cert_name_reject_empty_names() {
2755        // Empty snapshot = default cert served = Sōzu is not
2756        // authoritative for any name; every authority must miss.
2757        assert_eq!(authority_matched_cert_name("example.com", &[]), None);
2758    }
2759
2760    #[test]
2761    fn cert_name_match_multi_san_one_hit() {
2762        let names = vec!["foo.com".to_owned(), "*.example.org".to_owned()];
2763        assert_eq!(
2764            authority_matched_cert_name("bar.example.org", &names),
2765            Some("*.example.org"),
2766        );
2767    }
2768
2769    #[test]
2770    fn cert_name_reject_substring_attack() {
2771        // `*.example.com` must not match `example.commons` — the suffix
2772        // after the first label is `commons`, not `example.com`.
2773        let names = vec!["*.example.com".to_owned()];
2774        assert_eq!(authority_matched_cert_name("example.commons", &names), None,);
2775    }
2776
2777    #[test]
2778    fn cert_name_ipv6_bracketed_literal_with_port() {
2779        // The `:` characters inside the brackets must not be mistaken for
2780        // a port separator: only the trailing `:8443` is stripped, and
2781        // `[::1]` compares equal to `[::1]`.
2782        let names = vec!["[::1]".to_owned()];
2783        assert!(authority_matched_cert_name("[::1]:8443", &names).is_some());
2784    }
2785}
2786
2787/// Backend-selection order: `Router::plan_connect` must resolve a tie the same way
2788/// on every process, for the identical set of backend connections and the
2789/// identical request (issue #1338).
2790///
2791/// **Why these assert a SPECIFIC token rather than "the same one twice".**
2792/// `RandomState` is seeded per `HashMap`, not per iteration: two walks of one
2793/// live `HashMap` agree, so a test that runs the selection twice inside one
2794/// process and compares passes against a `HashMap` by construction. What does
2795/// NOT agree is two SEPARATELY CONSTRUCTED maps — `RandomState::default()`
2796/// bumps a thread-local key on every instantiation — so each round below
2797/// builds a fresh [`Router`], and therefore a fresh hasher, and asserts the
2798/// token the total order on `Token` picks.
2799///
2800/// That makes the red statistical rather than structural, and the bound is
2801/// stated so it can be checked rather than trusted: with `n` staged backends
2802/// and `r` rounds a `HashMap` survives with probability about `n.pow(-r)`,
2803/// which is `4^-24` here — about one run in 2.8e14. A `BTreeMap` survives
2804/// with probability 1. Measured on the pre-image, with the three production
2805/// lines reverted and these guards untouched: three consecutive runs failed
2806/// all three at round 0 or round 1. The least-loaded guard was handed
2807/// `Token(23)` and `Token(13)` where the total order gives `Token(7)`, and
2808/// the last-wins fallback `Token(7)` where it gives `Token(41)`.
2809///
2810/// **To SEE THESE RED:** in this module, put `Router::backends` back to
2811/// `HashMap<Token, Connection<SessionTcpStream>>`, `Router::new`'s initialiser
2812/// back to `HashMap::new()` and the import back to `collections::HashMap`
2813/// (the `BackendConnections` type then has no user left; delete it).
2814/// Reverting the file wholesale deletes these tests instead of reddening them.
2815#[cfg(test)]
2816mod backend_selection_order_tests {
2817    use std::{cell::RefCell, collections::HashMap, net::SocketAddr, rc::Rc, time::Duration};
2818
2819    use mio::Token;
2820    use rusty_ulid::Ulid;
2821    use sozu_command::{
2822        config::ListenerBuilder,
2823        proto::command::{
2824            Cluster, ListenerType, PathRule, RequestHttpFrontend, RulePosition, SocketAddress,
2825        },
2826    };
2827
2828    use super::Router;
2829    use crate::{
2830        BackendConnectionError, L7Proxy,
2831        backends::Backend,
2832        http::{HttpListener, HttpProxy},
2833        pool::Pool,
2834        protocol::{
2835            http::parser::Method,
2836            mux::{
2837                BackendId, BackendRegistry, BackendStatus, Connection, Context, Position,
2838                StreamState,
2839                buffer_source::PoolBufferSource,
2840                h2::H2ConnectionConfig,
2841                h2_flood_detector::H2FloodConfig,
2842                router::{ConnectPlan, ConnectStep, IpGateVerdict, RoutingView},
2843            },
2844        },
2845        socket::SessionTcpStream,
2846    };
2847
2848    /// Unwrap a step these fixtures can only produce one way.
2849    ///
2850    /// Every `Context` here is built with `session_address: None`, so the
2851    /// per-(cluster, source-IP) gate is skipped and `Router::plan_connect`
2852    /// must decide outright. A `CheckIpLimit` would mean the gate fired
2853    /// without a source IP, which is its own bug.
2854    fn decided(step: ConnectStep) -> ConnectPlan {
2855        match step {
2856            ConnectStep::Decided(plan) => plan,
2857            ConnectStep::CheckIpLimit(_) => panic!(
2858                "a stream with no session address must not reach the \
2859                 per-(cluster, source-IP) gate"
2860            ),
2861        }
2862    }
2863
2864    /// Staged deliberately out of order and non-contiguous, so a green run
2865    /// cannot be an artefact of insertion order or of a dense key space.
2866    const STAGED_TOKENS: [usize; 4] = [23, 7, 41, 13];
2867    const LOWEST_TOKEN: usize = 7;
2868    const HIGHEST_TOKEN: usize = 41;
2869    /// See the module-level note for the `n.pow(-r)` bound this feeds.
2870    const ROUNDS: usize = 24;
2871
2872    const H2_CLUSTER: &str = "cluster-1338-h2";
2873    const H1_CLUSTER: &str = "cluster-1338-h1";
2874    const H2_AUTHORITY: &str = "h2.backend-order.example.com";
2875    const H1_AUTHORITY: &str = "h1.backend-order.example.com";
2876
2877    /// Which arm of `Router::plan_connect`'s scan the staged backends land in.
2878    enum Staged {
2879        /// `Position::Client(_, _, BackendStatus::Connected)` on an H2
2880        /// connection — the least-loaded arm, strict `<`, first-at-minimum.
2881        ConnectedH2,
2882        /// `BackendStatus::Connecting` on an H2 connection — the fallback
2883        /// arm, assigned with no `break` and no "already chosen" guard.
2884        ConnectingH2,
2885        /// `BackendStatus::KeepAlive` on an H1 connection — assigned with a
2886        /// `break`, so first-seen.
2887        KeepAliveH1,
2888    }
2889
2890    impl Staged {
2891        fn cluster(&self) -> &'static str {
2892            match self {
2893                Staged::ConnectedH2 | Staged::ConnectingH2 => H2_CLUSTER,
2894                Staged::KeepAliveH1 => H1_CLUSTER,
2895            }
2896        }
2897
2898        fn authority(&self) -> &'static str {
2899            match self {
2900                Staged::ConnectedH2 | Staged::ConnectingH2 => H2_AUTHORITY,
2901                Staged::KeepAliveH1 => H1_AUTHORITY,
2902            }
2903        }
2904    }
2905
2906    /// The proxy, listener, clusters, frontends and buffer pool
2907    /// `Router::plan_connect` needs. Built once and shared by every round:
2908    /// the order under test belongs to `Router::backends`, which each round
2909    /// rebuilds from scratch.
2910    struct RoutingFixture {
2911        proxy: Rc<RefCell<dyn L7Proxy>>,
2912        /// The map `proxy` was built over, which a `Mux` receives at
2913        /// construction.
2914        backends: Rc<RefCell<crate::backends::BackendMap>>,
2915        listener: Rc<RefCell<HttpListener>>,
2916        pool: Rc<RefCell<Pool>>,
2917    }
2918
2919    fn routing_fixture() -> RoutingFixture {
2920        let port = crate::testing::provide_port();
2921        let address = SocketAddress::new_v4(127, 0, 0, 1, port);
2922        let config = ListenerBuilder::new_http(address)
2923            .to_http(None)
2924            .expect("test http listener config must build");
2925        let parts =
2926            crate::testing::prebuild_server(32, 65_536, false).expect("test server must build");
2927        let pool = parts.pool.clone();
2928        let backends = Rc::clone(&parts.backends);
2929        let mut proxy = HttpProxy::new(parts.registry, parts.sessions, parts.pool, parts.backends);
2930        let listener_token = Token(0);
2931        proxy
2932            .add_listener(config, listener_token)
2933            .expect("test listener must register");
2934        for (cluster_id, authority, http2) in [
2935            (H2_CLUSTER, H2_AUTHORITY, Some(true)),
2936            (H1_CLUSTER, H1_AUTHORITY, None),
2937        ] {
2938            proxy
2939                .add_cluster(Cluster {
2940                    cluster_id: cluster_id.to_owned(),
2941                    http2,
2942                    ..Default::default()
2943                })
2944                .expect("the test cluster must register");
2945            proxy
2946                .add_http_frontend(RequestHttpFrontend {
2947                    cluster_id: Some(cluster_id.to_owned()),
2948                    address,
2949                    hostname: authority.to_owned(),
2950                    path: PathRule::prefix("/".to_owned()),
2951                    position: RulePosition::Tree.into(),
2952                    ..Default::default()
2953                })
2954                .expect("the test frontend must register");
2955        }
2956        let listener = proxy
2957            .get_listener(&listener_token)
2958            .expect("the registered listener must be reachable");
2959        RoutingFixture {
2960            proxy: Rc::new(RefCell::new(proxy)),
2961            backends,
2962            listener,
2963            pool,
2964        }
2965    }
2966
2967    /// One backend connection in the requested arm, plus the accepted peer the
2968    /// caller must keep alive for the length of the round.
2969    fn staged_backend(
2970        pool: &Rc<RefCell<Pool>>,
2971        staged: &Staged,
2972        backend_registry: &mut BackendRegistry,
2973    ) -> (Connection<SessionTcpStream>, std::net::TcpStream) {
2974        let (socket, peer) = super::super::test_support::connected_socket();
2975        let backend_address: SocketAddr =
2976            "127.0.0.1:2".parse().expect("backend address must parse");
2977        let backend = Rc::new(RefCell::new(Backend::new(
2978            "test-backend",
2979            backend_address,
2980            None,
2981            None,
2982            None,
2983        )));
2984        // Through the embedder's table, exactly as a real dial does: the
2985        // handle stops here and the connection carries the opaque id.
2986        let backend = backend_registry.id_for(&backend);
2987        let session_ulid = Ulid::generate();
2988        let socket = SessionTcpStream::new(socket, session_ulid, Some(backend_address));
2989        let mut connection = match staged {
2990            Staged::ConnectedH2 | Staged::ConnectingH2 => Connection::new_h2_client(
2991                session_ulid,
2992                socket,
2993                staged.cluster().into(),
2994                backend,
2995                &mut PoolBufferSource::new(Rc::downgrade(pool)),
2996                Duration::from_secs(30),
2997                H2FloodConfig::default(),
2998                H2ConnectionConfig::default(),
2999                Duration::from_secs(30),
3000                None,
3001            )
3002            .expect("the test pool must hand out a buffer for the H2 backend"),
3003            Staged::KeepAliveH1 => Connection::new_h1_client(
3004                session_ulid,
3005                socket,
3006                staged.cluster().into(),
3007                backend,
3008                Duration::from_secs(30),
3009            ),
3010        };
3011        // `new_h*_client` opens in `Connecting`; the other two arms are
3012        // reached by moving the status the way a completed dial does.
3013        if let Position::Client(_, _, status) = connection.position_mut() {
3014            match staged {
3015                Staged::ConnectedH2 => *status = BackendStatus::Connected,
3016                Staged::ConnectingH2 => {}
3017                Staged::KeepAliveH1 => *status = BackendStatus::KeepAlive,
3018            }
3019        }
3020        (connection, peer)
3021    }
3022
3023    /// Stage [`STAGED_TOKENS`] into a FRESH [`Router`] — a fresh `RandomState`
3024    /// in the pre-image — drive one `Router::plan_connect`, and report the token it
3025    /// attached the stream to.
3026    fn selected_backend_token(fixture: &RoutingFixture, staged: &Staged) -> Token {
3027        let pool = &fixture.pool;
3028        let mut context = Context::new(
3029            Ulid::generate(),
3030            Rc::downgrade(pool),
3031            fixture.listener.clone(),
3032            // No session address: the per-(cluster, source-IP) gate is
3033            // skipped, so this never reaches the proxy's session manager.
3034            None,
3035            "127.0.0.1:80"
3036                .parse()
3037                .expect("test public address must parse"),
3038        );
3039        let stream_id = context
3040            .create_stream(Ulid::generate(), 65_535)
3041            .expect("the test pool must hand out a stream");
3042        {
3043            let stream = &mut context.streams[stream_id];
3044            stream.state = StreamState::Link;
3045            stream.context.authority = Some(staged.authority().to_owned());
3046            stream.context.path = Some("/".to_owned());
3047            stream.context.method = Some(Method::Get);
3048        }
3049
3050        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3051        let mut backend_registry = BackendRegistry::default();
3052        let mut peers = Vec::with_capacity(STAGED_TOKENS.len());
3053        for token in STAGED_TOKENS {
3054            let (connection, peer) = staged_backend(pool, staged, &mut backend_registry);
3055            peers.push(peer);
3056            router.backends.insert(Token(token), connection);
3057        }
3058
3059        // Every staged backend is reusable, so the plan must be `Attached`:
3060        // the router completed the attach itself and asked the embedder for
3061        // nothing. A `Dial` here would mean the reuse scan missed, which is
3062        // the failure this harness exists to catch.
3063        let proxy_ref = fixture.proxy.borrow();
3064        let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
3065        let plan = router
3066            .plan_connect(stream_id, &mut context, &view)
3067            .map(decided)
3068            .expect("the router must reuse one of the staged backends");
3069        assert!(
3070            matches!(plan, ConnectPlan::Attached),
3071            "the staged backends are all reusable, so no dial may be requested: got {plan:?}"
3072        );
3073
3074        match context.streams[stream_id].state {
3075            StreamState::Linked(token) => token,
3076            other => panic!("connect must link the stream to a backend, got {other:?}"),
3077        }
3078    }
3079
3080    /// Drive one `Router::plan_connect` against a single staged connection of
3081    /// the `staged` arm, whose backend is retired when `retired` is set, and
3082    /// return the plan.
3083    fn plan_against_one_staged(
3084        fixture: &RoutingFixture,
3085        staged: &Staged,
3086        retired: bool,
3087    ) -> (ConnectPlan, StreamState) {
3088        let mut context = Context::new(
3089            Ulid::generate(),
3090            Rc::downgrade(&fixture.pool),
3091            fixture.listener.clone(),
3092            None,
3093            "127.0.0.1:80"
3094                .parse()
3095                .expect("test public address must parse"),
3096        );
3097        let stream_id = context
3098            .create_stream(Ulid::generate(), 65_535)
3099            .expect("the test pool must hand out a stream");
3100        {
3101            let stream = &mut context.streams[stream_id];
3102            stream.state = StreamState::Link;
3103            stream.context.authority = Some(staged.authority().to_owned());
3104            stream.context.path = Some("/".to_owned());
3105            stream.context.method = Some(Method::Get);
3106        }
3107        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3108        let mut backend_registry = BackendRegistry::default();
3109        let (connection, _peer) = staged_backend(&fixture.pool, staged, &mut backend_registry);
3110        if retired {
3111            // What `BackendList::remove_backend` and `BackendMap::remove_cluster`
3112            // do to the handle the session's registry still holds.
3113            let Position::Client(_, backend, _) = connection.position() else {
3114                unreachable!("a staged backend is a client connection")
3115            };
3116            backend_registry
3117                .handle(backend)
3118                .expect("the staged backend was interned")
3119                .borrow_mut()
3120                .set_closing();
3121        }
3122        router.backends.insert(Token(LOWEST_TOKEN), connection);
3123        let is_retired = |backend: &BackendId| backend_registry.is_retired(backend);
3124        let proxy_ref = fixture.proxy.borrow();
3125        let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind())
3126            .with_backend_retired(&is_retired);
3127        let plan = router
3128            .plan_connect(stream_id, &mut context, &view)
3129            .map(decided)
3130            .expect("routing must resolve the staged cluster");
3131        (plan, context.streams[stream_id].state)
3132    }
3133
3134    /// The pool-reuse scan skips a connection whose backend has left the
3135    /// configuration, in each of its three reusable arms: H2 `Connected`, H2
3136    /// `Connecting` and H1 `KeepAlive`. The same connection with a live
3137    /// backend is reused, so a `Dial` here is the skip and not a scan that
3138    /// would have missed anyway.
3139    #[test]
3140    fn a_retired_backend_connection_is_never_reused() {
3141        let fixture = routing_fixture();
3142        for (staged, arm) in [
3143            (Staged::ConnectedH2, "H2 Connected"),
3144            (Staged::ConnectingH2, "H2 Connecting"),
3145            (Staged::KeepAliveH1, "H1 KeepAlive"),
3146        ] {
3147            let (plan, state) = plan_against_one_staged(&fixture, &staged, false);
3148            assert!(
3149                matches!(plan, ConnectPlan::Attached),
3150                "premise, {arm}: a connection to a live backend is reused, got {plan:?}"
3151            );
3152            assert_eq!(state, StreamState::Linked(Token(LOWEST_TOKEN)));
3153
3154            let (plan, state) = plan_against_one_staged(&fixture, &staged, true);
3155            assert!(
3156                matches!(&plan, ConnectPlan::Dial { cluster_id, .. } if &**cluster_id == staged.cluster()),
3157                "{arm}: a connection to a retired backend must not be reused, got {plan:?}"
3158            );
3159            assert_eq!(
3160                state,
3161                StreamState::Link,
3162                "{arm}: the stream must not be linked to the retired connection"
3163            );
3164        }
3165    }
3166
3167    /// `Mux::ready_inner` closes a retired connection only once it is idle:
3168    /// an H1 keep-alive, or a connected H2 connection with no stream. A dial
3169    /// still in progress is left to its own connect path.
3170    #[test]
3171    fn idle_pooled_backend_names_only_streamless_pooled_connections() {
3172        let fixture = routing_fixture();
3173        for (staged, arm, idle) in [
3174            (Staged::ConnectedH2, "H2 Connected", true),
3175            (Staged::ConnectingH2, "H2 Connecting", false),
3176            (Staged::KeepAliveH1, "H1 KeepAlive", true),
3177        ] {
3178            let mut backend_registry = BackendRegistry::default();
3179            let (connection, _peer) = staged_backend(&fixture.pool, &staged, &mut backend_registry);
3180            assert_eq!(
3181                connection.idle_pooled_backend().is_some(),
3182                idle,
3183                "{arm}: idle_pooled_backend must answer {idle}"
3184            );
3185        }
3186    }
3187
3188    /// An admitted stream must have been **counted** before the decision
3189    /// resumes — the ordering the gate-as-a-step split has to preserve.
3190    ///
3191    /// `SessionManager::track_cluster_ip` mutates
3192    /// `connections_per_cluster_ip`, which is keyed on `(cluster, ip)`
3193    /// **across every session on the worker** and read again by
3194    /// `lib/src/tcp.rs`'s own gate. So an `IpGateVerdict::Admitted` is an
3195    /// assertion that the slot has already been taken. Resume as `Admitted`
3196    /// without having tracked and the stream goes through uncounted, and the
3197    /// next session from the same IP finds a slot that should be gone.
3198    ///
3199    /// The test drives the real production sequence — `Router::plan_connect`
3200    /// → `super::consult_ip_gate` → `Router::plan_connect_resume` — for two
3201    /// DIFFERENT frontend tokens from the SAME source IP, against a limit of
3202    /// one. Two tokens, not one: the gate is deliberately idempotent within a
3203    /// token (an H2 session multiplexing many streams to one cluster holds a
3204    /// single slot), so a second stream on the same token could never trip
3205    /// the limit and would prove nothing.
3206    ///
3207    /// To SEE THIS RED: delete the `track_cluster_ip` call from
3208    /// `super::consult_ip_gate`, leaving it to return
3209    /// `IpGateVerdict::Admitted` without counting. The second token is then
3210    /// admitted too and the `AtLimit` assertion below fires.
3211    #[test]
3212    fn an_admitted_stream_is_counted_before_the_decision_resumes() {
3213        let fixture = routing_fixture();
3214        let sessions = fixture.proxy.borrow().sessions();
3215        let source: SocketAddr = "203.0.113.7:51000"
3216            .parse()
3217            .expect("test source address must parse");
3218
3219        // One connection per (cluster, ip).
3220        let mut clusters = HashMap::new();
3221        clusters.insert(
3222            H2_CLUSTER.into(),
3223            Cluster {
3224                cluster_id: H2_CLUSTER.to_owned(),
3225                max_connections_per_ip: Some(1),
3226                ..Default::default()
3227            },
3228        );
3229        let view = RoutingView::new(&clusters, ListenerType::Http);
3230
3231        // Drive one stream all the way through the three-step sequence and
3232        // report what the gate said and what the decision became.
3233        let run = |token: Token| {
3234            let mut context = Context::new(
3235                Ulid::generate(),
3236                Rc::downgrade(&fixture.pool),
3237                fixture.listener.clone(),
3238                Some(source),
3239                "127.0.0.1:80"
3240                    .parse()
3241                    .expect("test public address must parse"),
3242            );
3243            let stream_id = context
3244                .create_stream(Ulid::generate(), 65_535)
3245                .expect("the test pool must hand out a stream");
3246            {
3247                let stream = &mut context.streams[stream_id];
3248                stream.state = StreamState::Link;
3249                stream.context.authority = Some(H2_AUTHORITY.to_owned());
3250                stream.context.path = Some("/".to_owned());
3251                stream.context.method = Some(Method::Get);
3252            }
3253            let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3254            let step = router
3255                .plan_connect(stream_id, &mut context, &view)
3256                .expect("routing must resolve the staged cluster");
3257            let ConnectStep::CheckIpLimit(resume) = step else {
3258                panic!("a stream WITH a session address must reach the per-IP gate")
3259            };
3260            let verdict = super::super::consult_ip_gate(
3261                &sessions,
3262                token,
3263                context
3264                    .http_context(stream_id)
3265                    .cluster_id
3266                    .as_ref()
3267                    .expect("plan_connect stamps the routed cluster"),
3268                &resume,
3269            );
3270            let admitted = matches!(verdict, IpGateVerdict::Admitted);
3271            let outcome =
3272                router.plan_connect_resume(stream_id, &mut context, &view, resume, verdict);
3273            let retry_after = context.streams[stream_id].context.retry_after_seconds;
3274            (admitted, outcome, retry_after)
3275        };
3276
3277        let (admitted, outcome, _) = run(Token(1));
3278        assert!(
3279            admitted,
3280            "premise: the first token must be under the limit, or the second proves nothing"
3281        );
3282        assert!(
3283            matches!(outcome, Ok(ConnectPlan::Dial { .. })),
3284            "an admitted stream must go on to request a dial, got {outcome:?}"
3285        );
3286
3287        let (admitted, outcome, retry_after) = run(Token(2));
3288        assert!(
3289            !admitted,
3290            "a SECOND token from the same source IP must find the slot already counted — \
3291             an Admitted verdict here means the first stream resumed without being tracked"
3292        );
3293        assert!(
3294            matches!(
3295                outcome,
3296                Err(BackendConnectionError::TooManyConnectionsPerIp { .. })
3297            ),
3298            "the refused stream must surface TooManyConnectionsPerIp, got {outcome:?}"
3299        );
3300        assert_eq!(
3301            retry_after, None,
3302            "with no cluster or global override the resolved Retry-After is 0, which is \
3303             stashed as None so the 429 mapping elides the header rather than sending 0"
3304        );
3305    }
3306
3307    /// A source holding a whole subnet defeats the per-(cluster, source-IP)
3308    /// cap outright: it takes a fresh address for every connection, so the
3309    /// per-address counter never reaches 2. The per-(cluster,
3310    /// source-SUBNET) cap is the answer, and this pins that it fires on
3311    /// DISTINCT addresses drawn from one masked network
3312    /// (sozu-proxy/sozu#1270).
3313    ///
3314    /// Three tokens, three different `203.0.113.x` addresses — every one a
3315    /// distinct `/32`, all inside `203.0.113.0/24`. The per-IP cap is set
3316    /// to 10, comfortably above the 1 connection each address holds, so it
3317    /// cannot be what refuses the fourth: only the subnet counter can.
3318    /// That is the whole point of the two caps being independent.
3319    ///
3320    /// To SEE THIS RED: delete the `|| self.cluster_subnet_at_limit(...)`
3321    /// disjunct from `SessionManager::cluster_connection_at_limit` in
3322    /// `lib/src/server.rs`, leaving every other piece of the feature —
3323    /// config keys, proto fields, masking, tracking — in place. The fourth
3324    /// address is then admitted and the `AtLimit` assertion below fires.
3325    #[test]
3326    fn a_subnet_cap_refuses_a_fresh_address_from_an_already_counted_subnet() {
3327        let fixture = routing_fixture();
3328        let sessions = fixture.proxy.borrow().sessions();
3329        {
3330            let mut manager = sessions.borrow_mut();
3331            // /24, so 203.0.113.* all mask to one key.
3332            manager.subnet_ipv4_prefix = 24;
3333            manager.max_connections_per_subnet = 3;
3334            // Deliberately far above what any single address will hold.
3335            manager.max_connections_per_ip = 10;
3336        }
3337
3338        let mut clusters = HashMap::new();
3339        clusters.insert(
3340            H2_CLUSTER.into(),
3341            Cluster {
3342                cluster_id: H2_CLUSTER.to_owned(),
3343                ..Default::default()
3344            },
3345        );
3346        let view = RoutingView::new(&clusters, ListenerType::Http);
3347
3348        // Drive one stream, from its own source address on its own token,
3349        // through the real three-step production sequence.
3350        let run = |token: Token, source: SocketAddr| {
3351            let mut context = Context::new(
3352                Ulid::generate(),
3353                Rc::downgrade(&fixture.pool),
3354                fixture.listener.clone(),
3355                Some(source),
3356                "127.0.0.1:80"
3357                    .parse()
3358                    .expect("test public address must parse"),
3359            );
3360            let stream_id = context
3361                .create_stream(Ulid::generate(), 65_535)
3362                .expect("the test pool must hand out a stream");
3363            {
3364                let stream = &mut context.streams[stream_id];
3365                stream.state = StreamState::Link;
3366                stream.context.authority = Some(H2_AUTHORITY.to_owned());
3367                stream.context.path = Some("/".to_owned());
3368                stream.context.method = Some(Method::Get);
3369            }
3370            let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3371            let step = router
3372                .plan_connect(stream_id, &mut context, &view)
3373                .expect("routing must resolve the staged cluster");
3374            let ConnectStep::CheckIpLimit(resume) = step else {
3375                panic!("a stream WITH a session address must reach the connection gate")
3376            };
3377            let verdict = super::super::consult_ip_gate(
3378                &sessions,
3379                token,
3380                context
3381                    .http_context(stream_id)
3382                    .cluster_id
3383                    .as_ref()
3384                    .expect("plan_connect stamps the routed cluster"),
3385                &resume,
3386            );
3387            let admitted = matches!(verdict, IpGateVerdict::Admitted);
3388            let outcome =
3389                router.plan_connect_resume(stream_id, &mut context, &view, resume, verdict);
3390            (admitted, outcome)
3391        };
3392
3393        let address = |host: u8| -> SocketAddr {
3394            format!("203.0.113.{host}:51000")
3395                .parse()
3396                .expect("test source address must parse")
3397        };
3398
3399        // Premise: three DISTINCT addresses inside one /24 fill the cap.
3400        for (index, host) in [1u8, 2, 3].into_iter().enumerate() {
3401            let (admitted, outcome) = run(Token(index + 1), address(host));
3402            assert!(
3403                admitted,
3404                "premise: 203.0.113.{host} is the {} of 3 allowed in the subnet and must be \
3405                 admitted, or the refusal below proves nothing",
3406                index + 1
3407            );
3408            assert!(
3409                matches!(outcome, Ok(ConnectPlan::Dial { .. })),
3410                "an admitted stream must go on to request a dial, got {outcome:?}"
3411            );
3412        }
3413
3414        // Premise: no single address is anywhere near the per-IP cap, so
3415        // the per-IP counter cannot be what refuses the next one.
3416        {
3417            let manager = sessions.borrow();
3418            for host in [1u8, 2, 3] {
3419                let ip = address(host).ip();
3420                assert!(
3421                    !manager.cluster_ip_at_limit(Token(99), H2_CLUSTER, &ip, None),
3422                    "premise: 203.0.113.{host} holds 1 of 10 per-IP slots and must be under \
3423                     its own cap — otherwise the refusal below is the per-IP cap, not the subnet"
3424                );
3425            }
3426        }
3427
3428        // A FOURTH, never-seen address in the same /24 is refused.
3429        let (admitted, outcome) = run(Token(4), address(4));
3430        assert!(
3431            !admitted,
3432            "a fresh address from an already-counted /24 must be refused — an Admitted \
3433             verdict here is the #1270 bypass: a new address per connection defeats the cap"
3434        );
3435        assert!(
3436            matches!(
3437                outcome,
3438                Err(BackendConnectionError::TooManyConnectionsPerIp { .. })
3439            ),
3440            "the refused stream must surface the too-many-connections error, got {outcome:?}"
3441        );
3442
3443        // Negative space: the same fourth address in a DIFFERENT /24 is
3444        // admitted, so the refusal above is the subnet key and not a
3445        // blanket cap on new addresses.
3446        let (admitted, _) = run(
3447            Token(5),
3448            "198.51.100.4:51000"
3449                .parse()
3450                .expect("test source address must parse"),
3451        );
3452        assert!(
3453            admitted,
3454            "an address in an UNCOUNTED subnet must still be admitted — otherwise the cap \
3455             is refusing on something other than the subnet key"
3456        );
3457    }
3458
3459    /// The default must change nothing. With `max_connections_per_subnet`
3460    /// left at its `0` default, the admission path must behave exactly as
3461    /// it did before this feature existed — and, because the subnet
3462    /// tracking is skipped outright rather than merely ignored, it must
3463    /// also leave no bookkeeping behind.
3464    ///
3465    /// The same four addresses in one /24 that the test above refuses are
3466    /// all admitted here, and the per-IP cap still fires on its own terms
3467    /// when a single address exceeds it.
3468    #[test]
3469    fn an_unset_subnet_cap_leaves_the_admission_path_untouched() {
3470        let fixture = routing_fixture();
3471        let sessions = fixture.proxy.borrow().sessions();
3472        {
3473            let mut manager = sessions.borrow_mut();
3474            // A /24 prefix is configured but the cap is OFF: the prefix
3475            // alone must not gate anything.
3476            manager.subnet_ipv4_prefix = 24;
3477            manager.max_connections_per_subnet = 0;
3478            manager.max_connections_per_ip = 1;
3479        }
3480
3481        let mut clusters = HashMap::new();
3482        clusters.insert(
3483            H2_CLUSTER.into(),
3484            Cluster {
3485                cluster_id: H2_CLUSTER.to_owned(),
3486                ..Default::default()
3487            },
3488        );
3489        let view = RoutingView::new(&clusters, ListenerType::Http);
3490
3491        let run = |token: Token, source: SocketAddr| {
3492            let mut context = Context::new(
3493                Ulid::generate(),
3494                Rc::downgrade(&fixture.pool),
3495                fixture.listener.clone(),
3496                Some(source),
3497                "127.0.0.1:80"
3498                    .parse()
3499                    .expect("test public address must parse"),
3500            );
3501            let stream_id = context
3502                .create_stream(Ulid::generate(), 65_535)
3503                .expect("the test pool must hand out a stream");
3504            {
3505                let stream = &mut context.streams[stream_id];
3506                stream.state = StreamState::Link;
3507                stream.context.authority = Some(H2_AUTHORITY.to_owned());
3508                stream.context.path = Some("/".to_owned());
3509                stream.context.method = Some(Method::Get);
3510            }
3511            let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3512            let step = router
3513                .plan_connect(stream_id, &mut context, &view)
3514                .expect("routing must resolve the staged cluster");
3515            let ConnectStep::CheckIpLimit(resume) = step else {
3516                panic!("a stream WITH a session address must reach the connection gate")
3517            };
3518            let verdict = super::super::consult_ip_gate(
3519                &sessions,
3520                token,
3521                context
3522                    .http_context(stream_id)
3523                    .cluster_id
3524                    .as_ref()
3525                    .expect("plan_connect stamps the routed cluster"),
3526                &resume,
3527            );
3528            matches!(verdict, IpGateVerdict::Admitted)
3529        };
3530
3531        // Four distinct addresses in one /24 — the exact traffic the test
3532        // above refuses — are all admitted while the cap is off.
3533        for (index, host) in [1u8, 2, 3, 4].into_iter().enumerate() {
3534            let source: SocketAddr = format!("203.0.113.{host}:51000")
3535                .parse()
3536                .expect("test source address must parse");
3537            assert!(
3538                run(Token(index + 1), source),
3539                "with max_connections_per_subnet = 0, 203.0.113.{host} must be admitted — \
3540                 the default must not gate traffic the previous release allowed"
3541            );
3542        }
3543
3544        // The subnet accounting must be genuinely untouched, not merely
3545        // consulted-and-ignored: a disabled limiter allocates nothing.
3546        assert!(
3547            sessions.borrow().subnet_tracking_is_empty(),
3548            "a disabled subnet limiter must leave both subnet maps empty — a populated one \
3549             means every deployment that never enables this feature pays for it anyway"
3550        );
3551
3552        // And the pre-existing per-IP cap still fires on its own terms:
3553        // a SECOND token from an address already counted once is refused
3554        // at a per-IP limit of 1.
3555        let repeat: SocketAddr = "203.0.113.1:51000"
3556            .parse()
3557            .expect("test source address must parse");
3558        assert!(
3559            !run(Token(90), repeat),
3560            "the per-IP cap must still refuse a second connection from the same address — \
3561             this feature must not have relaxed the limiter that already existed"
3562        );
3563    }
3564
3565    /// The routing decision reads cluster configuration from the view it was
3566    /// handed, not from the `L7Proxy` handle it still carries.
3567    ///
3568    /// Question 6 of [#1340](https://github.com/sozu-proxy/sozu/issues/1340)
3569    /// puts routing inside the core, and the only thing that makes that worth
3570    /// anything is that the data routing reads is supplied rather than
3571    /// fetched — otherwise a simulator still cannot drive the decision. This
3572    /// hands `Router::plan_connect` a view that DISAGREES with the proxy
3573    /// about the one cluster knob the plan surfaces, and asserts the plan
3574    /// follows the view.
3575    ///
3576    /// The fixture registers `H2_CLUSTER` with `http2: Some(true)`. The view
3577    /// below says `Some(false)` for the same cluster id. A `Dial { h2: true }`
3578    /// would mean the decision went back to `L7Proxy::clusters` behind the
3579    /// view's back.
3580    ///
3581    /// To SEE THIS RED: restore either cluster read to the handle — in
3582    /// `Router::plan_connect` write `proxy.borrow().clusters().get(&cluster_id)`
3583    /// in place of `view.cluster(&cluster_id)` — and the assertion below
3584    /// reports `h2: true`.
3585    #[test]
3586    fn the_routing_decision_reads_cluster_config_from_the_view() {
3587        let fixture = routing_fixture();
3588        let pool = &fixture.pool;
3589        let mut context = Context::new(
3590            Ulid::generate(),
3591            Rc::downgrade(pool),
3592            fixture.listener.clone(),
3593            // No session address: the per-(cluster, source-IP) gate — the one
3594            // read still taken through the handle — is skipped, so this test
3595            // isolates the reads that moved.
3596            None,
3597            "127.0.0.1:80"
3598                .parse()
3599                .expect("test public address must parse"),
3600        );
3601        let stream_id = context
3602            .create_stream(Ulid::generate(), 65_535)
3603            .expect("the test pool must hand out a stream");
3604        {
3605            let stream = &mut context.streams[stream_id];
3606            stream.state = StreamState::Link;
3607            stream.context.authority = Some(H2_AUTHORITY.to_owned());
3608            stream.context.path = Some("/".to_owned());
3609            stream.context.method = Some(Method::Get);
3610        }
3611
3612        // A cluster map that is NOT the proxy's, disagreeing on `http2`.
3613        let mut clusters = HashMap::new();
3614        clusters.insert(
3615            H2_CLUSTER.into(),
3616            Cluster {
3617                cluster_id: H2_CLUSTER.to_owned(),
3618                http2: Some(false),
3619                ..Default::default()
3620            },
3621        );
3622        assert_eq!(
3623            fixture
3624                .proxy
3625                .borrow()
3626                .clusters()
3627                .get(H2_CLUSTER)
3628                .and_then(|c| c.http2),
3629            Some(true),
3630            "premise: the proxy and the view must disagree, or this proves nothing"
3631        );
3632        let view = RoutingView::new(&clusters, ListenerType::Http);
3633
3634        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3635        let plan = router
3636            .plan_connect(stream_id, &mut context, &view)
3637            .map(decided)
3638            .expect("routing must resolve the staged cluster");
3639
3640        match plan {
3641            ConnectPlan::Dial {
3642                ref cluster_id, h2, ..
3643            } => {
3644                assert_eq!(
3645                    &**cluster_id, H2_CLUSTER,
3646                    "premise: routing resolved a cluster"
3647                );
3648                assert!(
3649                    !h2,
3650                    "the plan must carry the view's `http2`, not the proxy's — \
3651                     `h2: true` means the decision read `L7Proxy::clusters` behind the view"
3652                );
3653            }
3654            ConnectPlan::Attached => {
3655                panic!("premise: an empty router has nothing to reuse, so it must ask for a dial")
3656            }
3657        }
3658    }
3659
3660    /// A plan is a decision, not a deed: `ConnectPlan::Dial` must leave the
3661    /// router and the stream exactly as it found them.
3662    ///
3663    /// This is the property Question 6 of
3664    /// [#1340](https://github.com/sozu-proxy/sozu/issues/1340) is actually
3665    /// about — the core decides and the embedder performs — and it is the one
3666    /// a later edit is most likely to erode, by "just" doing one small effect
3667    /// inline because the data is right there. Asserting on the return value
3668    /// alone would not catch that: a `Dial` that had already dialled is still
3669    /// a `Dial`. So this asserts on the state the router and the stream are
3670    /// left in.
3671    ///
3672    /// To SEE THIS RED: perform any effect in `Router::plan_connect`'s
3673    /// new-backend path before it returns — inserting into `self.backends`,
3674    /// or calling `context.link_stream(stream_id, token)` — and the
3675    /// corresponding assertion below fires.
3676    ///
3677    /// SCOPE, because the name would otherwise over-read.
3678    /// `Router::plan_connect` is NOT pure yet. It still performs one registry
3679    /// mutation: `SessionManager::track_cluster_ip`, reached through
3680    /// `L7Proxy::sessions` in the per-(cluster, source-IP) limit gate. That is
3681    /// the mutation sitting *between* cluster resolution and backend
3682    /// selection that #1340 recorded as Question 6's first wrinkle, and it is
3683    /// deliberately unchanged here — it moves when the borrowed view lands,
3684    /// as either a second returned step or part of that view.
3685    ///
3686    /// This `Context` is therefore built with `session_address: None`, which
3687    /// skips the gate entirely, and the assertions below cover the router's
3688    /// backend map, the stream's link state, the backend-stream reverse index
3689    /// and the accounting ledger — NOT the session manager. Read the
3690    /// assertion list rather than the test name.
3691    #[test]
3692    fn a_dial_plan_performs_no_effect_of_its_own() {
3693        let fixture = routing_fixture();
3694        let pool = &fixture.pool;
3695        let mut context = Context::new(
3696            Ulid::generate(),
3697            Rc::downgrade(pool),
3698            fixture.listener.clone(),
3699            // No session address, so the per-(cluster, source-IP) gate is
3700            // skipped and this never reaches the proxy's session manager.
3701            None,
3702            "127.0.0.1:80"
3703                .parse()
3704                .expect("test public address must parse"),
3705        );
3706        let stream_id = context
3707            .create_stream(Ulid::generate(), 65_535)
3708            .expect("the test pool must hand out a stream");
3709        {
3710            let stream = &mut context.streams[stream_id];
3711            stream.state = StreamState::Link;
3712            stream.context.authority = Some(H2_AUTHORITY.to_owned());
3713            stream.context.path = Some("/".to_owned());
3714            stream.context.method = Some(Method::Get);
3715        }
3716
3717        // An EMPTY router: nothing to reuse, so routing must fall through to
3718        // the new-backend path and ask the embedder for a dial.
3719        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3720        let proxy_ref = fixture.proxy.borrow();
3721        let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
3722        let plan = router
3723            .plan_connect(stream_id, &mut context, &view)
3724            .map(decided)
3725            .expect("routing must resolve the staged cluster");
3726
3727        match &plan {
3728            ConnectPlan::Dial { cluster_id, .. } => assert_eq!(
3729                &**cluster_id, H2_CLUSTER,
3730                "the dial request must name the cluster routing resolved"
3731            ),
3732            ConnectPlan::Attached => {
3733                panic!("premise: an empty router has nothing to reuse, so it must ask for a dial")
3734            }
3735        }
3736
3737        assert!(
3738            router.backends.is_empty(),
3739            "a plan must not have taken a backend connection into the router: {:?}",
3740            router.backends.keys().collect::<Vec<_>>()
3741        );
3742        assert_eq!(
3743            context.streams[stream_id].state,
3744            StreamState::Link,
3745            "a plan must not have linked the stream — the embedder has not dialled yet"
3746        );
3747        assert!(
3748            context.backend_streams.is_empty(),
3749            "a plan must not have touched the backend-stream reverse index"
3750        );
3751        assert!(
3752            context.backend_deltas.is_empty(),
3753            "a plan must not have charged any backend accounting: {:?}",
3754            context.backend_deltas
3755        );
3756    }
3757
3758    /// The leak with the most user-visible consequence: the H2 least-loaded
3759    /// arm compares stream counts with a strict `<`, so every staged backend
3760    /// sitting at the same count leaves the choice entirely to iteration
3761    /// order — hash order decided which machine served the request.
3762    #[test]
3763    fn an_h2_stream_count_tie_always_picks_the_lowest_backend_token() {
3764        let fixture = routing_fixture();
3765        for round in 0..ROUNDS {
3766            let chosen = selected_backend_token(&fixture, &Staged::ConnectedH2);
3767            assert_eq!(
3768                chosen,
3769                Token(LOWEST_TOKEN),
3770                "round {round}: a stream-count tie must resolve to the lowest token, \
3771                 not to whatever the map iterated first"
3772            );
3773        }
3774    }
3775
3776    /// The same scan's fallback arm, and it does NOT bias the same way:
3777    /// it assigns with neither a `break` nor an "already chosen" guard, so
3778    /// the LAST matching connecting backend wins. The total order pins that
3779    /// to the highest token — asserting the lowest here would be red against
3780    /// both containers.
3781    #[test]
3782    fn an_all_connecting_h2_fallback_always_picks_the_highest_backend_token() {
3783        let fixture = routing_fixture();
3784        for round in 0..ROUNDS {
3785            let chosen = selected_backend_token(&fixture, &Staged::ConnectingH2);
3786            assert_eq!(
3787                chosen,
3788                Token(HIGHEST_TOKEN),
3789                "round {round}: the connecting-backend fallback assigns last-wins, \
3790                 so the total order must land on the highest token"
3791            );
3792        }
3793    }
3794
3795    /// The H1 keep-alive arm carries the same first-seen bias as the H2
3796    /// least-loaded one — `reuse_token = Some(*token); break;` — and it is
3797    /// live wherever an H2 frontend fans several concurrent streams onto an
3798    /// H1 cluster: each stream dials its own socket, every socket parks
3799    /// `KeepAlive` for the same cluster, and the next request picks among
3800    /// them by map order.
3801    #[test]
3802    fn an_h1_keep_alive_reuse_always_picks_the_lowest_backend_token() {
3803        let fixture = routing_fixture();
3804        for round in 0..ROUNDS {
3805            let chosen = selected_backend_token(&fixture, &Staged::KeepAliveH1);
3806            assert_eq!(
3807                chosen,
3808                Token(LOWEST_TOKEN),
3809                "round {round}: the first matching keep-alive socket wins, so the total \
3810                 order must land on the lowest token"
3811            );
3812        }
3813    }
3814
3815    /// #1579: the routing decision for a request that reuses a pooled
3816    /// backend connection allocates nothing of its own, in steady state.
3817    ///
3818    /// The reuse branch of `Router::decide_after_gate` stamps the stream's
3819    /// `HttpContext` and `SessionMetrics` with the id the connection's
3820    /// `BackendId` carries. That id is an `Rc<str>`, so the stamp is a
3821    /// reference-count increment; a `String` copy there costs one heap
3822    /// allocation per field and per request.
3823    ///
3824    /// Measured against a control that performs the same mux bookkeeping the
3825    /// branch triggers — `Connection::start_stream` then
3826    /// `Context::link_stream` on the same keep-alive socket — and nothing
3827    /// else. Each request is released the way production releases it
3828    /// (`Context::unlink_stream`, then the ledger drained through
3829    /// `BackendRegistry::apply_all` as a Mux pass drains it), so the control
3830    /// carries exactly the bookkeeping's costs and the difference is what the
3831    /// decision adds.
3832    ///
3833    /// Both are also held at zero outright (#1583): the reverse-index entry
3834    /// keeps its emptied `Vec` for the next `link_stream`, and the ledger is
3835    /// drained in place, so the bookkeeping itself allocates nothing either.
3836    ///
3837    /// TO SEE THIS RED: stamp either field with
3838    /// `Some(backend.backend_id.to_string().into())` instead of the `Rc`
3839    /// clone; the difference is then two allocations per request per field
3840    /// (the `String`, then the `Rc<str>` built from it). For the absolute
3841    /// half, make `remove_backend_stream` drop the emptied entry again, or
3842    /// make `BackendRegistry::apply_all` iterate `mem::take(deltas)`: each
3843    /// costs one allocation per request in both measurements.
3844    #[test]
3845    fn a_request_on_a_reused_backend_connection_allocates_nothing() {
3846        use std::hint::black_box;
3847
3848        use crate::test_allocations::allocations;
3849
3850        const REQUESTS: usize = 64;
3851        let backend_token = Token(LOWEST_TOKEN);
3852
3853        let fixture = routing_fixture();
3854        let mut context = Context::new(
3855            Ulid::generate(),
3856            Rc::downgrade(&fixture.pool),
3857            fixture.listener.clone(),
3858            None,
3859            "127.0.0.1:80"
3860                .parse()
3861                .expect("test public address must parse"),
3862        );
3863        let stream_id = context
3864            .create_stream(Ulid::generate(), 65_535)
3865            .expect("the test pool must hand out a stream");
3866        context.streams[stream_id].context.method = Some(Method::Get);
3867        // Where `Router::plan_connect` leaves the routed cluster for the
3868        // decision to read.
3869        context.streams[stream_id].context.cluster_id = Some(H1_CLUSTER.into());
3870
3871        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3872        let mut backend_registry = BackendRegistry::default();
3873        let (connection, _peer) =
3874            staged_backend(&fixture.pool, &Staged::KeepAliveH1, &mut backend_registry);
3875        router.backends.insert(backend_token, connection);
3876
3877        // Released as `ConnectionH1::end_stream` releases a terminated
3878        // response on a keep-alive backend, then the ledger drained as the
3879        // next Mux pass drains it.
3880        let backend_registry = &backend_registry;
3881        let release = |router: &mut Router, context: &mut Context<HttpListener>| {
3882            assert_eq!(
3883                context.streams[stream_id].state,
3884                StreamState::Linked(backend_token)
3885            );
3886            context.unlink_stream(stream_id);
3887            backend_registry.apply_all(&mut context.backend_deltas, &fixture.backends);
3888            context.streams[stream_id].forget_upstream_replay();
3889            context.streams[stream_id].state = StreamState::Link;
3890            let Some(Connection::H1(h1)) = router.backends.get_mut(&backend_token) else {
3891                unreachable!("the staged backend is an H1 connection")
3892            };
3893            h1.stream = None;
3894            if let Position::Client(_, _, status) = &mut h1.position {
3895                *status = BackendStatus::KeepAlive;
3896            }
3897        };
3898        // The worker's retired-backend check rides the decision, so it is
3899        // measured with it.
3900        let clusters = HashMap::new();
3901        let retired = |backend: &super::BackendId| backend_registry.is_retired(backend);
3902        let view = RoutingView::new(&clusters, ListenerType::Http).with_backend_retired(&retired);
3903        // One request through the routing decision.
3904        let decision = |router: &mut Router, context: &mut Context<HttpListener>| -> usize {
3905            let before = allocations();
3906            let plan =
3907                router.decide_after_gate(stream_id, black_box(&mut *context), &view, false, false);
3908            let allocated = allocations() - before;
3909            assert!(
3910                matches!(plan, Ok(ConnectPlan::Attached)),
3911                "the keep-alive socket must be reused, got {plan:?}"
3912            );
3913            release(router, context);
3914            allocated
3915        };
3916        // The same socket attached by hand: only the mux bookkeeping.
3917        let control = |router: &mut Router, context: &mut Context<HttpListener>| -> usize {
3918            let before = allocations();
3919            let started = router
3920                .backends
3921                .get_mut(&backend_token)
3922                .expect("the staged backend is registered")
3923                .start_stream(stream_id, black_box(&mut *context));
3924            context.link_stream(stream_id, backend_token);
3925            let allocated = allocations() - before;
3926            assert!(started, "the keep-alive socket must accept the stream");
3927            release(router, context);
3928            allocated
3929        };
3930
3931        // Warm-up: metric keys and tables are sized by the first request.
3932        decision(&mut router, &mut context);
3933        control(&mut router, &mut context);
3934        let (mut decided, mut controlled) = (0, 0);
3935        for _ in 0..REQUESTS {
3936            decided += decision(&mut router, &mut context);
3937            controlled += control(&mut router, &mut context);
3938        }
3939
3940        decision(&mut router, &mut context);
3941        let stream = &context.streams[stream_id];
3942        assert_eq!(stream.context.backend_id.as_deref(), Some("test-backend"));
3943        assert_eq!(stream.metrics.backend_id.as_deref(), Some("test-backend"));
3944        assert_eq!(
3945            decided.saturating_sub(controlled),
3946            0,
3947            "{REQUESTS} requests on a reused keep-alive backend made {decided} \
3948             heap allocations in the routing decision against {controlled} for \
3949             the mux bookkeeping alone, expected no difference"
3950        );
3951        assert_eq!(
3952            (decided, controlled),
3953            (0, 0),
3954            "{REQUESTS} requests on a reused keep-alive backend must allocate \
3955             nothing in the decision nor in the mux bookkeeping it triggers"
3956        );
3957    }
3958
3959    /// #1583: the whole reuse branch copies no cluster id, gated or not.
3960    ///
3961    /// `ClusterId` is a reference-counted `Arc<str>` in sozu-command-lib:
3962    /// `route_from_request` hands `Router::plan_connect` a handle on the
3963    /// route table's own id, `plan_connect` moves it into the stream's
3964    /// `HttpContext`, and every later reader borrows it from there or bumps
3965    /// its count — the per-(cluster, source-IP) gate's `SessionManager`
3966    /// bookkeeping included, whose maps are keyed by the same handle. With
3967    /// the reverse-index entry and the delta ledger keeping their capacity,
3968    /// the branch allocates nothing at all past the debug build's history.
3969    ///
3970    /// Measured end to end — `plan_connect`, then on the gated path
3971    /// `consult_ip_gate` with its `SessionManager::track_cluster_connection`
3972    /// and `plan_connect_resume`, then the production release — against a
3973    /// control made of the one cost this branch does not own and which runs
3974    /// inside `plan_connect`: the debug-build `DebugEvent::Str` history push,
3975    /// absent from a release build, where the control is zero. The rest of
3976    /// `route_from_request` is measured, not controlled: it copies no
3977    /// authority and the route lookup records its trie segments on the stack
3978    /// (#1589). Both paths run, because production traffic always carries a
3979    /// source address and so always takes the gated one, while the fixtures
3980    /// above never do.
3981    ///
3982    /// TO SEE THIS RED: stamp `stream_context.cluster_id` in `plan_connect`
3983    /// with a copy (`Some(ClusterId::from(&*cluster_id))`) instead of moving
3984    /// the routed handle in: one allocation per request, on both paths. Hand
3985    /// `SessionManager::track_cluster_connection` such a copy in
3986    /// `consult_ip_gate` and the gated path alone pays one more. The same
3987    /// count comes back if `route_from_request` stamps
3988    /// `HttpContext::original_authority` with a copy of the authority on a
3989    /// request without a host rewrite, or if `Router::lookup` hands the trie
3990    /// a `Vec::with_capacity(16)` again.
3991    #[test]
3992    fn a_routed_request_on_a_reused_backend_connection_copies_no_cluster_id() {
3993        use std::hint::black_box;
3994
3995        use crate::test_allocations::allocations;
3996
3997        const REQUESTS: usize = 64;
3998        let backend_token = Token(LOWEST_TOKEN);
3999        let fixture = routing_fixture();
4000        let sessions = fixture.proxy.borrow().sessions();
4001
4002        for gated in [false, true] {
4003            let mut context = Context::new(
4004                Ulid::generate(),
4005                Rc::downgrade(&fixture.pool),
4006                fixture.listener.clone(),
4007                gated.then(|| {
4008                    "127.0.0.1:4242"
4009                        .parse()
4010                        .expect("test session address must parse")
4011                }),
4012                "127.0.0.1:80"
4013                    .parse()
4014                    .expect("test public address must parse"),
4015            );
4016            let stream_id = context
4017                .create_stream(Ulid::generate(), 65_535)
4018                .expect("the test pool must hand out a stream");
4019            {
4020                let stream = &mut context.streams[stream_id];
4021                stream.state = StreamState::Link;
4022                stream.context.authority = Some(H1_AUTHORITY.to_owned());
4023                stream.context.path = Some("/".to_owned());
4024                stream.context.method = Some(Method::Get);
4025            }
4026            let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
4027            let mut backend_registry = BackendRegistry::default();
4028            let (connection, _peer) =
4029                staged_backend(&fixture.pool, &Staged::KeepAliveH1, &mut backend_registry);
4030            router.backends.insert(backend_token, connection);
4031            let proxy_ref = fixture.proxy.borrow();
4032            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
4033
4034            // One request through the whole branch, the gate's bookkeeping
4035            // included, then released as production releases it.
4036            let branch = |router: &mut Router, context: &mut Context<HttpListener>| -> usize {
4037                let before = allocations();
4038                let step = router
4039                    .plan_connect(stream_id, black_box(&mut *context), &view)
4040                    .expect("the reused request must be planned");
4041                let mut allocated = allocations() - before;
4042                let plan = match step {
4043                    ConnectStep::Decided(plan) => {
4044                        assert!(!gated, "a stream with a source address must be gated");
4045                        plan
4046                    }
4047                    ConnectStep::CheckIpLimit(resume) => {
4048                        assert!(gated, "a stream without a source address must not be gated");
4049                        let before = allocations();
4050                        let verdict = super::super::consult_ip_gate(
4051                            &sessions,
4052                            Token(0),
4053                            context
4054                                .http_context(stream_id)
4055                                .cluster_id
4056                                .as_ref()
4057                                .expect("plan_connect stamps the routed cluster"),
4058                            &resume,
4059                        );
4060                        let plan = router.plan_connect_resume(
4061                            stream_id,
4062                            &mut *context,
4063                            &view,
4064                            resume,
4065                            verdict,
4066                        );
4067                        allocated += allocations() - before;
4068                        plan.expect("the gate is unlimited, so the stream is admitted")
4069                    }
4070                };
4071                assert!(
4072                    matches!(plan, ConnectPlan::Attached),
4073                    "the keep-alive socket must be reused, got {plan:?}"
4074                );
4075                let before = allocations();
4076                context.unlink_stream(stream_id);
4077                backend_registry.apply_all(&mut context.backend_deltas, &fixture.backends);
4078                allocated += allocations() - before;
4079                let stream = &mut context.streams[stream_id];
4080                stream.forget_upstream_replay();
4081                stream.state = StreamState::Link;
4082                stream.attempts = 0;
4083                let Some(Connection::H1(h1)) = router.backends.get_mut(&backend_token) else {
4084                    unreachable!("the staged backend is an H1 connection")
4085                };
4086                h1.stream = None;
4087                if let Position::Client(_, _, status) = &mut h1.position {
4088                    *status = BackendStatus::KeepAlive;
4089                }
4090                allocated
4091            };
4092            // The one cost `plan_connect` carries that is not this branch's:
4093            // the debug history push. Routing hands over a handle on the
4094            // route table's cluster id, so nothing else may allocate.
4095            let control = |context: &mut Context<HttpListener>| -> usize {
4096                let before = allocations();
4097                #[cfg(debug_assertions)]
4098                {
4099                    use crate::protocol::mux::DebugEvent;
4100
4101                    let route = context.streams[stream_id].context.get_route();
4102                    context.debug.push(DebugEvent::Str(route));
4103                }
4104                #[cfg(not(debug_assertions))]
4105                let _ = &context;
4106                allocations() - before
4107            };
4108
4109            // Warm-up: metric keys, the reverse index and the ledger are
4110            // sized by the first request.
4111            branch(&mut router, &mut context);
4112            control(&mut context);
4113            let (mut branched, mut controlled) = (0, 0);
4114            for _ in 0..REQUESTS {
4115                branched += branch(&mut router, &mut context);
4116                controlled += control(&mut context);
4117            }
4118
4119            assert_eq!(
4120                context.http_context(stream_id).cluster_id.as_deref(),
4121                Some(H1_CLUSTER)
4122            );
4123            assert_eq!(
4124                branched, controlled,
4125                "gated={gated}: {REQUESTS} requests on a reused keep-alive backend \
4126                 made {branched} heap allocations through the reuse branch against \
4127                 {controlled} for the debug history, expected no difference"
4128            );
4129        }
4130    }
4131
4132    /// #1583: a dial copies no cluster id: the new connection shares the
4133    /// routed one.
4134    ///
4135    /// `ClusterId` is a reference-counted `Arc<str>` in sozu-command-lib, so
4136    /// the route table, the stream's `HttpContext::cluster_id`, the
4137    /// `ConnectPlan::Dial` and the new connection's `Position::Client` all
4138    /// point at the one allocation the route table made when the frontend was
4139    /// added. `Mux::attach_dialed` moves the plan's handle into the connection.
4140    ///
4141    /// TO SEE THIS RED: build the connection in `Mux::attach_dialed` from
4142    /// `ClusterId::from(&*cluster_id)`, or stamp the plan in
4143    /// `Router::decide_after_gate` with one; the connection then owns a
4144    /// second copy at another address.
4145    #[test]
4146    fn a_dial_moves_the_planned_cluster_id_into_the_backend_connection() {
4147        use crate::{Protocol, ProxySession, protocol::mux::Mux, server::ListenSession};
4148
4149        let fixture = routing_fixture();
4150        let upstream =
4151            std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback listener must bind");
4152        fixture.backends.borrow_mut().add_backend(
4153            H1_CLUSTER,
4154            Backend::new(
4155                "dial-backend",
4156                upstream
4157                    .local_addr()
4158                    .expect("a bound listener has an address"),
4159                None,
4160                None,
4161                None,
4162            ),
4163        );
4164        let mut context = Context::new(
4165            Ulid::generate(),
4166            Rc::downgrade(&fixture.pool),
4167            fixture.listener.clone(),
4168            None,
4169            "127.0.0.1:80"
4170                .parse()
4171                .expect("test public address must parse"),
4172        );
4173        let stream_id = context
4174            .create_stream(Ulid::generate(), 65_535)
4175            .expect("the test pool must hand out a stream");
4176        {
4177            let stream = &mut context.streams[stream_id];
4178            stream.state = StreamState::Link;
4179            stream.context.authority = Some(H1_AUTHORITY.to_owned());
4180            stream.context.path = Some("/".to_owned());
4181            stream.context.method = Some(Method::Get);
4182        }
4183        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
4184        let mut backend_registry = BackendRegistry::default();
4185
4186        let plan = {
4187            let proxy_ref = fixture.proxy.borrow();
4188            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
4189            router
4190                .plan_connect(stream_id, &mut context, &view)
4191                .map(decided)
4192                .expect("an empty router must ask for a dial")
4193        };
4194        let ConnectPlan::Dial {
4195            cluster_id,
4196            h2,
4197            frontend_should_stick,
4198        } = plan
4199        else {
4200            panic!("premise: an empty router has nothing to reuse, so it must ask for a dial")
4201        };
4202        let planned = cluster_id.as_ptr();
4203
4204        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(ListenSession {
4205            protocol: Protocol::HTTPListen,
4206        }));
4207        Mux::<mio::net::TcpStream, HttpListener>::dial_backend(
4208            &mut router,
4209            &mut backend_registry,
4210            &fixture.backends,
4211            stream_id,
4212            &mut context,
4213            &session,
4214            &fixture.proxy,
4215            cluster_id,
4216            h2,
4217            frontend_should_stick,
4218        )
4219        .expect("the loopback backend must dial");
4220
4221        let token = match context.streams[stream_id].state {
4222            StreamState::Linked(token) => token,
4223            other => panic!("the dial must link the stream, got {other:?}"),
4224        };
4225        let Some(Position::Client(owned, _, _)) =
4226            router.backends.get(&token).map(Connection::position)
4227        else {
4228            panic!("the dialled connection must be a client")
4229        };
4230        assert_eq!(&**owned, H1_CLUSTER);
4231        assert_eq!(
4232            context.http_context(stream_id).cluster_id.as_deref(),
4233            Some(H1_CLUSTER)
4234        );
4235        assert!(
4236            std::ptr::eq(owned.as_ptr(), planned),
4237            "the connection must own the id the plan carried, not a copy of it"
4238        );
4239        let routed = context
4240            .http_context(stream_id)
4241            .cluster_id
4242            .as_ref()
4243            .expect("plan_connect stamps the routed cluster");
4244        assert!(
4245            std::ptr::eq(owned.as_ptr(), routed.as_ptr()),
4246            "the connection must share the stream's routed id, not a copy of it"
4247        );
4248    }
4249
4250    /// Dial one fresh stream of `H1_CLUSTER` through `Mux::dial_backend`
4251    /// against a single backend at `address`, then drain the ledger as the
4252    /// next pass would. Returns the dial's result and the backend.
4253    fn dial_one(
4254        address: std::net::SocketAddr,
4255    ) -> (Result<(), BackendConnectionError>, Rc<RefCell<Backend>>) {
4256        dial_with(address, DialFault::None)
4257    }
4258
4259    /// A local failure `dial_with` stages between the plan and the dial, each
4260    /// reaching one exit of `Mux::attach_dialed`, past a successful `connect(2)`.
4261    enum DialFault {
4262        /// No fault: the dial runs as it does in production.
4263        None,
4264        /// Every buffer of the pool is checked out, so
4265        /// `Connection::new_h2_client` returns `None`. Dials `H2_CLUSTER`.
4266        PoolExhausted,
4267        /// The request already holds output encoded for another connection,
4268        /// so `ConnectionH2::start_stream` refuses it (#1632). Dials
4269        /// `H2_CLUSTER`.
4270        StartRefused,
4271        /// `L7Proxy::register_socket` fails, as it does under fd pressure.
4272        /// Dials `H1_CLUSTER`.
4273        RegisterRefused,
4274    }
4275
4276    /// The fixture's proxy, except that registering a socket fails.
4277    ///
4278    /// Everything `Mux::dial_backend` calls besides `register_socket` goes to
4279    /// the real proxy, so the slab session it adds is removed from it again.
4280    struct RegisterRefuses(Rc<RefCell<dyn L7Proxy>>);
4281
4282    impl L7Proxy for RegisterRefuses {
4283        fn kind(&self) -> ListenerType {
4284            self.0.borrow().kind()
4285        }
4286        fn register_socket(
4287            &self,
4288            _socket: &mut mio::net::TcpStream,
4289            _token: Token,
4290            _interest: mio::Interest,
4291        ) -> Result<(), std::io::Error> {
4292            Err(std::io::Error::from_raw_os_error(24)) // EMFILE
4293        }
4294        fn add_session(&self, session: Rc<RefCell<dyn crate::ProxySession>>) -> Token {
4295            self.0.borrow().add_session(session)
4296        }
4297        fn remove_session(&self, token: Token) -> bool {
4298            self.0.borrow().remove_session(token)
4299        }
4300        fn clusters(&self) -> &HashMap<sozu_command::state::ClusterId, Cluster> {
4301            unreachable!("Mux::dial_backend never reads the cluster table")
4302        }
4303        fn sessions(&self) -> Rc<RefCell<crate::server::SessionManager>> {
4304            self.0.borrow().sessions()
4305        }
4306    }
4307
4308    /// [`dial_one`] with `fault` staged before the dial.
4309    fn dial_with(
4310        address: std::net::SocketAddr,
4311        fault: DialFault,
4312    ) -> (Result<(), BackendConnectionError>, Rc<RefCell<Backend>>) {
4313        use crate::{Protocol, ProxySession, protocol::mux::Mux, server::ListenSession};
4314
4315        let (cluster, authority) = match fault {
4316            DialFault::PoolExhausted | DialFault::StartRefused => (H2_CLUSTER, H2_AUTHORITY),
4317            DialFault::None | DialFault::RegisterRefused => (H1_CLUSTER, H1_AUTHORITY),
4318        };
4319        let fixture = routing_fixture();
4320        fixture.backends.borrow_mut().add_backend(
4321            cluster,
4322            Backend::new("dial-once", address, None, None, None),
4323        );
4324        let backend = fixture
4325            .backends
4326            .borrow_mut()
4327            .backends
4328            .get_mut(cluster)
4329            .and_then(|list| list.find_backend(&address).cloned())
4330            .expect("the backend was just added");
4331        let mut context = Context::new(
4332            Ulid::generate(),
4333            Rc::downgrade(&fixture.pool),
4334            fixture.listener.clone(),
4335            None,
4336            "127.0.0.1:80"
4337                .parse()
4338                .expect("test public address must parse"),
4339        );
4340        let stream_id = context
4341            .create_stream(Ulid::generate(), 65_535)
4342            .expect("the test pool must hand out a stream");
4343        {
4344            let stream = &mut context.streams[stream_id];
4345            stream.state = StreamState::Link;
4346            stream.context.authority = Some(authority.to_owned());
4347            stream.context.path = Some("/".to_owned());
4348            stream.context.method = Some(Method::Get);
4349        }
4350        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
4351        let mut backend_registry = BackendRegistry::default();
4352        let plan = {
4353            let proxy_ref = fixture.proxy.borrow();
4354            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
4355            router
4356                .plan_connect(stream_id, &mut context, &view)
4357                .map(decided)
4358                .expect("an empty router must ask for a dial")
4359        };
4360        let ConnectPlan::Dial {
4361            cluster_id,
4362            h2,
4363            frontend_should_stick,
4364        } = plan
4365        else {
4366            panic!("premise: an empty router has nothing to reuse, so it must ask for a dial")
4367        };
4368        assert_eq!(
4369            h2,
4370            cluster == H2_CLUSTER,
4371            "premise: the plan must dial the protocol the cluster declares"
4372        );
4373        // Held until the dial returns: dropping a checkout gives it back.
4374        let mut exhausted = Vec::new();
4375        let mut proxy = fixture.proxy.clone();
4376        match fault {
4377            DialFault::None => {}
4378            DialFault::PoolExhausted => {
4379                while let Some(buffer) = fixture.pool.borrow_mut().checkout() {
4380                    exhausted.push(buffer);
4381                }
4382            }
4383            DialFault::StartRefused => {
4384                context.streams[stream_id]
4385                    .front
4386                    .out
4387                    .push_back(kawa::OutBlock::Store(kawa::Store::Static(b"encoded")));
4388            }
4389            DialFault::RegisterRefused => {
4390                proxy = Rc::new(RefCell::new(RegisterRefuses(fixture.proxy.clone())));
4391            }
4392        }
4393        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(ListenSession {
4394            protocol: Protocol::HTTPListen,
4395        }));
4396        let dialed = Mux::<mio::net::TcpStream, HttpListener>::dial_backend(
4397            &mut router,
4398            &mut backend_registry,
4399            &fixture.backends,
4400            stream_id,
4401            &mut context,
4402            &session,
4403            &proxy,
4404            cluster_id,
4405            h2,
4406            frontend_should_stick,
4407        );
4408        drop(exhausted);
4409        assert!(
4410            dialed.is_ok() || router.backends.is_empty(),
4411            "a dial that failed must keep no backend connection, got {dialed:?}"
4412        );
4413        backend_registry.apply_all(&mut context.backend_deltas, &fixture.backends);
4414        (dialed, backend)
4415    }
4416
4417    /// #1684: a dial that connects counts its connection once. Selection
4418    /// reserved it, and the dial keeps that reservation as the connection's
4419    /// count rather than taking a second one.
4420    ///
4421    /// TO SEE THIS RED: call `inc_connections` on the dialled backend in
4422    /// `Mux::dial_backend` after the connect succeeds, as
4423    /// `Backend::try_connect` does for the TCP proxy; the backend then holds
4424    /// two connections for one dial.
4425    #[test]
4426    fn a_successful_dial_counts_its_connection_once() {
4427        let upstream =
4428            std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback listener must bind");
4429        let (dialed, backend) = dial_one(
4430            upstream
4431                .local_addr()
4432                .expect("a bound listener has an address"),
4433        );
4434        dialed.expect("the loopback backend must dial");
4435        let backend = backend.borrow();
4436        assert_eq!(
4437            backend.active_connections, 1,
4438            "one dial must hold exactly one connection on its backend"
4439        );
4440        assert_eq!(backend.failures, 0, "a successful dial records no failure");
4441    }
4442
4443    /// #1684: a `connect(2)` that fails synchronously releases the reservation
4444    /// selection took and records the failure, once the ledger is drained.
4445    ///
4446    /// `255.255.255.255` is the limited broadcast address: connecting a TCP
4447    /// socket to it fails before any packet leaves (`ENETUNREACH`, or
4448    /// `EACCES` where a default route exists). The dial is asserted to fail,
4449    /// so a host where it somehow connected fails this test loudly instead of
4450    /// passing it vacuously.
4451    ///
4452    /// TO SEE THIS RED: return the error from `Mux::dial_backend` without
4453    /// pushing the `BackendChange::DialFailed` delta; the reservation then
4454    /// outlives the failed dial.
4455    #[test]
4456    fn a_synchronously_failed_dial_releases_its_reservation() {
4457        let (dialed, backend) = dial_one("255.255.255.255:80".parse().expect("a literal address"));
4458        assert!(
4459            matches!(
4460                dialed,
4461                Err(BackendConnectionError::Backend(
4462                    crate::backends::BackendError::ConnectionFailures { failures: 1, .. }
4463                ))
4464            ),
4465            "a connect to the broadcast address must fail synchronously, got {dialed:?}"
4466        );
4467        let backend = backend.borrow();
4468        assert_eq!(
4469            backend.active_connections, 0,
4470            "the failed dial must release the reservation selection took"
4471        );
4472        assert_eq!(
4473            backend.failures, 1,
4474            "the failed dial must count one failure"
4475        );
4476    }
4477
4478    /// #1713: dial one stream with `fault` staged against a listening
4479    /// backend, and assert the dial failed with `expected` and left the
4480    /// backend exactly as it found it: the reservation selection took is
4481    /// released, and no failure is recorded, because the backend did not fail.
4482    fn assert_a_local_failure_releases_the_reservation(
4483        fault: DialFault,
4484        expected: fn(&BackendConnectionError) -> bool,
4485    ) {
4486        use crate::retry::{RetryAction, RetryPolicy};
4487
4488        let upstream =
4489            std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback listener must bind");
4490        let (dialed, backend) = dial_with(
4491            upstream
4492                .local_addr()
4493                .expect("a bound listener has an address"),
4494            fault,
4495        );
4496        assert!(
4497            dialed.as_ref().is_err_and(expected),
4498            "premise: the staged fault must fail the dial at its exit, got {dialed:?}"
4499        );
4500        let backend = backend.borrow();
4501        assert_eq!(
4502            backend.active_connections, 0,
4503            "a dial abandoned after its connect must release the reservation \
4504             selection took"
4505        );
4506        assert_eq!(
4507            backend.failures, 0,
4508            "a local failure must not count against the backend"
4509        );
4510        assert_eq!(
4511            backend.retry_policy.can_try(std::time::Instant::now()),
4512            Some(RetryAction::OKAY),
4513            "a local failure must not arm the backend's backoff"
4514        );
4515    }
4516
4517    /// #1713: `Connection::new_h2_client` returning `None` (buffer pool
4518    /// exhausted) releases the reservation.
4519    ///
4520    /// TO SEE THIS RED: in `Mux::dial_backend`, return the error of
4521    /// `Mux::attach_dialed` without pushing the `BackendChange::ConnectionClosed`
4522    /// delta; the backend then keeps `active_connections == 1`.
4523    #[test]
4524    fn an_h2_client_the_pool_cannot_build_releases_its_reservation() {
4525        assert_a_local_failure_releases_the_reservation(DialFault::PoolExhausted, |error| {
4526            matches!(error, BackendConnectionError::MaxBuffers)
4527        });
4528    }
4529
4530    /// #1713: a refused `Connection::start_stream` releases the reservation.
4531    ///
4532    /// TO SEE THIS RED: as for
4533    /// [`an_h2_client_the_pool_cannot_build_releases_its_reservation`].
4534    #[test]
4535    fn a_refused_stream_start_releases_its_reservation() {
4536        assert_a_local_failure_releases_the_reservation(DialFault::StartRefused, |error| {
4537            matches!(error, BackendConnectionError::MaxSessionsMemory)
4538        });
4539    }
4540
4541    /// #1713: the `register_socket` rollback releases the reservation, not
4542    /// only the gauges, `active_requests` and the slab session.
4543    ///
4544    /// TO SEE THIS RED: as for
4545    /// [`an_h2_client_the_pool_cannot_build_releases_its_reservation`].
4546    #[test]
4547    fn a_socket_the_poller_refuses_releases_its_reservation() {
4548        assert_a_local_failure_releases_the_reservation(DialFault::RegisterRefused, |error| {
4549            matches!(error, BackendConnectionError::MaxSessionsMemory)
4550        });
4551    }
4552
4553    /// #1610: a session's first backend connection is stored without a heap
4554    /// allocation, and the connections still iterate in `Token` order however
4555    /// they were inserted and removed.
4556    ///
4557    /// `Router::backends` was a `BTreeMap`, whose first insertion allocates an
4558    /// 11-slot leaf of ~1.7 KiB connections, ~18.8 KiB, for the one backend a
4559    /// session almost always has.
4560    ///
4561    /// TO SEE THIS RED: in `BackendConnections::insert`, send the `None` arm
4562    /// to `self.rest.insert(token, connection)` instead of filling `first`
4563    /// (and let `remove` and `is_empty` read `rest` alone). The first
4564    /// insertion then makes one allocation.
4565    #[test]
4566    fn the_first_backend_connection_of_a_session_allocates_nothing() {
4567        use crate::test_allocations::allocations;
4568
4569        let fixture = routing_fixture();
4570        let mut backend_registry = BackendRegistry::default();
4571        let mut staged = |router: &mut Router, token: usize| {
4572            let (connection, peer) =
4573                staged_backend(&fixture.pool, &Staged::KeepAliveH1, &mut backend_registry);
4574            let before = allocations();
4575            let replaced = router.backends.insert(Token(token), connection);
4576            let allocated = allocations() - before;
4577            assert!(replaced.is_none(), "Token({token}) was not held yet");
4578            (allocated, peer)
4579        };
4580
4581        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
4582        let (first, _first_peer) = staged(&mut router, LOWEST_TOKEN + 2);
4583        assert_eq!(
4584            first, 0,
4585            "the first backend connection of a session must not allocate"
4586        );
4587
4588        // A lower token takes the inline slot, a higher one goes to the map.
4589        let (_, _lower_peer) = staged(&mut router, LOWEST_TOKEN);
4590        let (_, _higher_peer) = staged(&mut router, LOWEST_TOKEN + 5);
4591        let order = |router: &Router| router.backends.keys().map(|t| t.0).collect::<Vec<_>>();
4592        assert_eq!(
4593            order(&router),
4594            [LOWEST_TOKEN, LOWEST_TOKEN + 2, LOWEST_TOKEN + 5]
4595        );
4596        assert_eq!(router.backends.len(), 3);
4597
4598        // Removing the inline connection promotes the lowest remaining one.
4599        assert!(router.backends.remove(&Token(LOWEST_TOKEN)).is_some());
4600        assert_eq!(order(&router), [LOWEST_TOKEN + 2, LOWEST_TOKEN + 5]);
4601        assert!(router.backends.contains_key(&Token(LOWEST_TOKEN + 2)));
4602        assert!(!router.backends.contains_key(&Token(LOWEST_TOKEN)));
4603        assert!(router.backends.remove(&Token(LOWEST_TOKEN + 5)).is_some());
4604        assert!(router.backends.remove(&Token(LOWEST_TOKEN + 2)).is_some());
4605        assert!(router.backends.is_empty());
4606    }
4607}
4608
4609/// [`Router::backend_from_request`] against the embedder's real selector.
4610///
4611/// Question 12 of [#1340](https://github.com/sozu-proxy/sozu/issues/1340)
4612/// replaced the proxy handle this function took with a backend-set capability,
4613/// now a [`BackendSelector`] (#1684); it had no test before that change. Each
4614/// fixture owns its own `BackendMap` and `BackendRegistry`, so nothing here
4615/// depends on a proxy, a listener or a session — and, since selection no
4616/// longer dials, on any socket.
4617#[cfg(test)]
4618mod backend_selector_tests {
4619    use std::{
4620        cell::RefCell,
4621        net::SocketAddr,
4622        time::{Duration, Instant},
4623    };
4624
4625    use sozu_command::proto::command::{LoadBalancingAlgorithms, LoadMetric};
4626
4627    use super::Router;
4628    use crate::{
4629        backends::{Backend, BackendMap},
4630        protocol::{
4631            http::editor::HttpContext,
4632            mux::{BackendChange, BackendDelta, BackendId, BackendRegistry, RegistrySelector},
4633        },
4634        retry::{RetryAction, RetryPolicy},
4635    };
4636
4637    const CLUSTER: &str = "cluster-1340-q12";
4638    const STICKY_B: &str = "sticky-b";
4639
4640    /// Two backends under `policy` on connections. The addresses are never
4641    /// dialled: selection reserves, and only a caller of the selector connects.
4642    /// `LeastLoaded` is the default because round-robin would alternate
4643    /// whatever the counters said, and a test that passes without reading the
4644    /// load observes nothing.
4645    struct Fixture {
4646        backends: RefCell<BackendMap>,
4647        registry: BackendRegistry,
4648        router: Router,
4649        now: Instant,
4650    }
4651
4652    fn fixture_with(policy: LoadBalancingAlgorithms) -> Fixture {
4653        let now = Instant::now();
4654        let address = |port: u16| SocketAddr::from(([127, 0, 0, 1], port));
4655        let mut backends = BackendMap::with_seed(0x1684);
4656        backends.set_load_balancing_policy_for_cluster(
4657            CLUSTER,
4658            policy,
4659            Some(LoadMetric::Connections),
4660        );
4661        backends.add_backend(
4662            CLUSTER,
4663            Backend::new_at("backend-a", address(9501), None, None, None, now),
4664        );
4665        backends.add_backend(
4666            CLUSTER,
4667            Backend::new_at(
4668                "backend-b",
4669                address(9502),
4670                Some(STICKY_B.to_owned()),
4671                None,
4672                None,
4673                now,
4674            ),
4675        );
4676        Fixture {
4677            backends: RefCell::new(backends),
4678            registry: BackendRegistry::default(),
4679            router: Router::new(Duration::from_secs(10), Duration::from_secs(10)),
4680            now,
4681        }
4682    }
4683
4684    fn fixture() -> Fixture {
4685        fixture_with(LoadBalancingAlgorithms::LeastLoaded)
4686    }
4687
4688    fn request(cookie: Option<&str>) -> HttpContext {
4689        let mut context = HttpContext::new(
4690            rusty_ulid::Ulid::generate(),
4691            rusty_ulid::Ulid::generate(),
4692            crate::Protocol::HTTP,
4693            "127.0.0.1:80"
4694                .parse()
4695                .expect("test public address must parse"),
4696            None,
4697            "SOZUBALANCEID".to_owned(),
4698            "Sozu-Id".to_owned(),
4699            false,
4700            false,
4701        );
4702        context.sticky_session_found = cookie.map(ToOwned::to_owned);
4703        context
4704    }
4705
4706    impl Fixture {
4707        fn select(&mut self, should_stick: bool, context: &mut HttpContext) -> BackendId {
4708            let mut backends = self.backends.borrow_mut();
4709            let mut selector = RegistrySelector {
4710                backends: &mut backends,
4711                registry: &mut self.registry,
4712                now: self.now,
4713                exclude: &[],
4714            };
4715            let backend = self
4716                .router
4717                .backend_from_request(CLUSTER, should_stick, context, &mut selector)
4718                .expect("a cluster with two available backends must select");
4719            assert_eq!(
4720                context.backend_id.as_deref(),
4721                Some(&*backend.backend_id),
4722                "the request must be stamped with the backend it was reserved on"
4723            );
4724            backend
4725        }
4726
4727        fn backend(&self, id: &BackendId) -> std::cell::Ref<'_, Backend> {
4728            self.registry
4729                .handle(id)
4730                .expect("an id this fixture minted must resolve")
4731                .borrow()
4732        }
4733
4734        fn active_connections(&self, backend: &BackendId) -> usize {
4735            self.backend(backend).active_connections
4736        }
4737    }
4738
4739    /// Two streams linking in one pass must spread under `LeastLoaded`, and
4740    /// no dial happens in between.
4741    ///
4742    /// The second selection reads `active_connections`, which only the first
4743    /// selection's reservation can have raised: nothing here connects, and no
4744    /// delta drain runs. So this holds only if selecting reserves under the
4745    /// same borrow as the choice (#1684). A selector that stopped reserving —
4746    /// counting the connection at the dial instead — would send both streams
4747    /// to `backend-a`.
4748    ///
4749    /// TO SEE THIS RED: make `Backend::reserve_connection` return `Ok(())`
4750    /// without calling `inc_connections`.
4751    #[test]
4752    fn a_second_selection_observes_the_first_selections_reservation() {
4753        let mut fixture = fixture();
4754
4755        let first = fixture.select(false, &mut request(None));
4756        let second = fixture.select(false, &mut request(None));
4757
4758        assert_eq!(
4759            &*first.backend_id, "backend-a",
4760            "with both backends idle the first minimum wins"
4761        );
4762        assert_ne!(
4763            first.backend_id,
4764            second.backend_id,
4765            "the second selection landed on the first one's backend: the first \
4766             selection did not reserve its connection \
4767             (first={} active={}, second={} active={})",
4768            first.backend_id,
4769            fixture.active_connections(&first),
4770            second.backend_id,
4771            fixture.active_connections(&second),
4772        );
4773        assert_eq!(
4774            (
4775                fixture.active_connections(&first),
4776                fixture.active_connections(&second)
4777            ),
4778            (1, 1),
4779            "each selection must reserve exactly one connection on the backend it chose"
4780        );
4781    }
4782
4783    /// #1684: power-of-two-choices weighs every reservation too.
4784    ///
4785    /// With two backends the sample is the whole set, so each selection must
4786    /// take the one with fewer reserved connections, and a tie resolves by
4787    /// coin flip. Over any run of selections the two counts therefore never
4788    /// drift more than one apart, and together they count every selection.
4789    ///
4790    /// TO SEE THIS RED: make `Backend::reserve_connection` return `Ok(())`
4791    /// without calling `inc_connections`; nothing is reserved and the total
4792    /// stays at zero.
4793    #[test]
4794    fn power_of_two_weighs_every_reservation() {
4795        const SELECTIONS: usize = 64;
4796        let mut fixture = fixture_with(LoadBalancingAlgorithms::PowerOfTwo);
4797        let first = fixture.select(false, &mut request(None));
4798        let mut ids = vec![first];
4799        for selections in 2..=SELECTIONS {
4800            let before: Vec<usize> = ids
4801                .iter()
4802                .map(|id| fixture.active_connections(id))
4803                .collect();
4804            let picked = fixture.select(false, &mut request(None));
4805            if !ids.iter().any(|id| id.backend_id == picked.backend_id) {
4806                ids.push(picked.clone());
4807            }
4808            let loads: Vec<usize> = ids
4809                .iter()
4810                .map(|id| fixture.active_connections(id))
4811                .collect();
4812            assert_eq!(
4813                loads.iter().sum::<usize>(),
4814                selections,
4815                "{selections} selections must hold {selections} reservations, got {loads:?}"
4816            );
4817            if ids.len() == 2 {
4818                let (low, high) = (loads[0].min(loads[1]), loads[0].max(loads[1]));
4819                assert!(
4820                    high - low <= 1,
4821                    "power-of-two let the reservations drift apart: {loads:?} (before {before:?})"
4822                );
4823            }
4824        }
4825        assert_eq!(ids.len(), 2, "both backends must be used");
4826    }
4827
4828    /// #1684: a dial whose `connect(2)` fails releases its reservation and
4829    /// records the failure, once the ledger is drained.
4830    ///
4831    /// TO SEE THIS RED: drop `self.dec_connections()` from
4832    /// `Backend::release_failed_dial`; the reservation then outlives the
4833    /// failed dial.
4834    #[test]
4835    fn a_failed_dial_releases_its_reservation_and_records_the_failure() {
4836        let mut fixture = fixture();
4837        let backend = fixture.select(false, &mut request(None));
4838        assert_eq!(
4839            fixture.active_connections(&backend),
4840            1,
4841            "selection reserves"
4842        );
4843
4844        let mut deltas = vec![BackendDelta {
4845            slot: backend.slot(),
4846            change: BackendChange::DialFailed(fixture.now),
4847        }];
4848        fixture.registry.apply_all(&mut deltas, &fixture.backends);
4849
4850        assert!(deltas.is_empty(), "the drain empties the ledger");
4851        let released = fixture.backend(&backend);
4852        assert_eq!(
4853            released.active_connections, 0,
4854            "a failed dial must release the reservation selection took"
4855        );
4856        assert_eq!(released.failures, 1, "a failed dial must count one failure");
4857        assert_eq!(
4858            released.retry_policy.can_try(fixture.now),
4859            Some(RetryAction::WAIT),
4860            "a failed dial must arm the backoff at the instant it failed"
4861        );
4862    }
4863
4864    /// A cookie pins the request only when its frontend sticks.
4865    ///
4866    /// The call this replaced matched on `(frontend_should_stick, cookie)`
4867    /// and sent `(false, Some(_))` to the load balancer; the selector now takes
4868    /// an [`super::Affinity`] built by the caller, so that arm is the one a
4869    /// signature change could flip without a compile error.
4870    #[test]
4871    fn a_cookie_pins_only_a_frontend_that_sticks() {
4872        let mut unpinned = fixture();
4873        let mut context = request(Some(STICKY_B));
4874        let backend = unpinned.select(false, &mut context);
4875        assert_eq!(
4876            &*backend.backend_id, "backend-a",
4877            "a frontend that does not stick must ignore the client's cookie"
4878        );
4879        assert_eq!(
4880            context.sticky_session, None,
4881            "a frontend that does not stick must not answer with a cookie"
4882        );
4883
4884        let mut pinned = fixture();
4885        let mut context = request(Some(STICKY_B));
4886        let backend = pinned.select(true, &mut context);
4887        assert_eq!(
4888            &*backend.backend_id, "backend-b",
4889            "a sticky frontend must follow the cookie to its backend"
4890        );
4891        assert_eq!(context.sticky_session.as_deref(), Some(STICKY_B));
4892        assert_eq!(
4893            pinned.active_connections(&backend),
4894            1,
4895            "a sticky selection reserves like any other"
4896        );
4897
4898        let mut fallback = fixture();
4899        let mut context = request(None);
4900        let backend = fallback.select(true, &mut context);
4901        assert_eq!(
4902            context.sticky_session.as_deref(),
4903            Some(&*backend.backend_id),
4904            "a sticky frontend with no cookie answers with the chosen \
4905             backend's id when it has no sticky id of its own"
4906        );
4907    }
4908
4909    /// [`fixture`] under `HRW`, the policy that reads the affinity key.
4910    fn hrw_fixture() -> Fixture {
4911        fixture_with(LoadBalancingAlgorithms::Hrw)
4912    }
4913
4914    /// A request whose `plan_connect` derived `key`.
4915    fn keyed_request(cookie: Option<&str>, key: u64) -> HttpContext {
4916        let mut context = request(cookie);
4917        context.affinity_key = Some(key);
4918        context
4919    }
4920
4921    /// #524: the key a request carries reaches the load balancer, so `HRW`
4922    /// sends one key to one backend selection after selection.
4923    ///
4924    /// TO SEE THIS RED: pass `None` instead of `context.affinity_key` to
4925    /// `BackendSelector::select` in `Router::backend_from_request`;
4926    /// `HRW` then round-robins and the second selection of each key lands on
4927    /// the other backend.
4928    #[test]
4929    fn a_keyed_request_stays_on_one_backend_under_hrw() {
4930        let mut fixture = hrw_fixture();
4931        for key in 0..16u64 {
4932            let first = fixture.select(false, &mut keyed_request(None, key));
4933            for _ in 0..3 {
4934                let again = fixture.select(false, &mut keyed_request(None, key));
4935                assert_eq!(
4936                    again.backend_id, first.backend_id,
4937                    "key {key} must stay on one backend under HRW"
4938                );
4939            }
4940        }
4941    }
4942
4943    /// #524: a sticky cookie naming a live backend wins over the key; the key
4944    /// only decides where a request the cookie did not pin lands.
4945    #[test]
4946    fn a_sticky_cookie_wins_over_the_affinity_key() {
4947        let mut fixture = hrw_fixture();
4948        let key = (0..64u64)
4949            .find(|&key| {
4950                &*fixture
4951                    .select(false, &mut keyed_request(None, key))
4952                    .backend_id
4953                    == "backend-a"
4954            })
4955            .expect("HRW sends some of 64 keys to each of two backends");
4956
4957        let mut context = keyed_request(Some(STICKY_B), key);
4958        let backend = fixture.select(true, &mut context);
4959        assert_eq!(
4960            &*backend.backend_id, "backend-b",
4961            "the cookie must pin the request whatever its key says"
4962        );
4963
4964        let mut context = keyed_request(None, key);
4965        let backend = fixture.select(true, &mut context);
4966        assert_eq!(
4967            &*backend.backend_id, "backend-a",
4968            "without a cookie a sticky frontend follows the key"
4969        );
4970    }
4971
4972    /// #1579: stamping a selected backend onto the request allocates nothing.
4973    ///
4974    /// `Router::backend_from_request` runs once per backend dial and writes
4975    /// the chosen backend's id into `HttpContext::backend_id`. The id arrives
4976    /// as the `Rc<str>` of the selected `BackendId`, so the stamp is a
4977    /// reference-count increment. The selector is a stub that always answers
4978    /// one id, so the count covers the router's own work and not the load
4979    /// balancer. It needs no socket: a selector does not dial (#1684).
4980    ///
4981    /// TO SEE THIS RED: stamp `Some(selected.backend.backend_id.to_string().into())`
4982    /// instead of the `Rc` clone; the assertion then reports two allocations
4983    /// per dial.
4984    #[test]
4985    fn stamping_a_dialled_backend_allocates_nothing() {
4986        use std::hint::black_box;
4987
4988        use super::{Affinity, BackendSelector, SelectedBackend};
4989        use crate::{backends::BackendError, test_allocations::allocations};
4990
4991        const DIALS: usize = 64;
4992
4993        /// Always answers `backend`, minted as a simulator would mint it.
4994        struct StubSelector {
4995            backend: BackendId,
4996        }
4997
4998        impl BackendSelector for StubSelector {
4999            fn select(
5000                &mut self,
5001                _cluster_id: &str,
5002                _affinity: Affinity<'_>,
5003                _key: Option<u64>,
5004            ) -> Result<SelectedBackend, BackendError> {
5005                Ok(SelectedBackend {
5006                    backend: self.backend.clone(),
5007                    sticky_session: None,
5008                })
5009            }
5010        }
5011
5012        let mut selector = StubSelector {
5013            backend: BackendId::new(
5014                0,
5015                std::rc::Rc::from("stamped-backend"),
5016                SocketAddr::from(([127, 0, 0, 1], 9503)),
5017            ),
5018        };
5019        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
5020        let mut context = request(None);
5021
5022        // Warm-up: the first stamp fills the slot the later ones overwrite.
5023        drop(
5024            router
5025                .backend_from_request(CLUSTER, false, &mut context, &mut selector)
5026                .expect("the stub selector always selects"),
5027        );
5028        let mut allocated = 0;
5029        for _ in 0..DIALS {
5030            let before = allocations();
5031            let selected = router.backend_from_request(
5032                black_box(CLUSTER),
5033                false,
5034                black_box(&mut context),
5035                &mut selector,
5036            );
5037            allocated += allocations() - before;
5038            drop(selected.expect("the stub selector always selects"));
5039        }
5040
5041        assert_eq!(context.backend_id.as_deref(), Some("stamped-backend"));
5042        assert_eq!(
5043            allocated, 0,
5044            "{DIALS} dials made {allocated} heap allocations stamping the \
5045             backend onto the request, expected none"
5046        );
5047    }
5048}
5049
5050/// [`affinity_key`]: which request bytes key `HRW` and `MAGLEV`, and that
5051/// deriving the key costs no allocation (#524).
5052#[cfg(test)]
5053mod affinity_key_tests {
5054    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
5055
5056    use sozu_command::proto::command::{Cluster, LoadBalancingAlgorithms};
5057
5058    use super::{StickyCookie, affinity_key};
5059    use crate::{
5060        load_balancing::{affinity_key_from_ip, affinity_key_from_value},
5061        pool::Pool,
5062        protocol::mux::GenericHttpStream,
5063    };
5064
5065    /// A listener sticky cookie the requests below do not carry.
5066    const NO_STICKY: StickyCookie<'static> = StickyCookie {
5067        name: "SOZUBALANCEID",
5068        value: None,
5069    };
5070
5071    const CLIENT: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10)), 40_000);
5072
5073    /// A request kawa parsed from `request`, on its own pool buffer.
5074    fn parsed(pool: &mut Pool, request: &[u8]) -> GenericHttpStream {
5075        let mut kawa: GenericHttpStream = kawa::Kawa::new(
5076            kawa::Kind::Request,
5077            kawa::Buffer::new(
5078                pool.checkout()
5079                    .expect("the test pool must hand out a buffer"),
5080            ),
5081        );
5082        kawa.storage.space()[..request.len()].copy_from_slice(request);
5083        kawa.storage.fill(request.len());
5084        kawa::h1::parse(&mut kawa, &mut kawa::h1::NoCallbacks);
5085        assert!(kawa.is_main_phase(), "premise: the request must parse");
5086        kawa
5087    }
5088
5089    fn cluster(
5090        policy: LoadBalancingAlgorithms,
5091        header: Option<&str>,
5092        cookie: Option<&str>,
5093    ) -> Cluster {
5094        Cluster {
5095            cluster_id: "affinity".to_owned(),
5096            load_balancing: policy as i32,
5097            affinity_header: header.map(ToOwned::to_owned),
5098            affinity_cookie: cookie.map(ToOwned::to_owned),
5099            ..Default::default()
5100        }
5101    }
5102
5103    const REQUEST: &[u8] = b"GET / HTTP/1.1\r\nHost: example.com\r\nx-tenant: acme\r\n\
5104        Cookie: theme=dark; tenant=globex\r\nX-Empty: \r\n\r\n";
5105
5106    /// Only `HRW`, `MAGLEV` and a sharded cluster read a key; every other
5107    /// cluster gets `None`, pays nothing and selects exactly as before.
5108    ///
5109    /// TO SEE THIS RED: drop the `shard_percent` arm of
5110    /// `cluster_reads_affinity_key`; the sharded round-robin cluster then
5111    /// derives no key and is silently not sharded.
5112    #[test]
5113    fn only_hrw_maglev_and_sharded_clusters_derive_a_key() {
5114        let mut pool = Pool::with_capacity(1, 1, 4096);
5115        let front = parsed(&mut pool, REQUEST);
5116        for policy in [
5117            LoadBalancingAlgorithms::RoundRobin,
5118            LoadBalancingAlgorithms::Random,
5119            LoadBalancingAlgorithms::LeastLoaded,
5120            LoadBalancingAlgorithms::PowerOfTwo,
5121        ] {
5122            assert_eq!(
5123                affinity_key(
5124                    &cluster(policy, Some("X-Tenant"), None),
5125                    &front,
5126                    Some(CLIENT),
5127                    NO_STICKY
5128                ),
5129                None,
5130                "{policy:?} does not read a key"
5131            );
5132        }
5133        // #524: a sharded cluster reads the key under any policy, to rank
5134        // the client's shard by.
5135        let sharded_round_robin = Cluster {
5136            shard_percent: Some(25),
5137            ..cluster(LoadBalancingAlgorithms::RoundRobin, None, None)
5138        };
5139        assert_eq!(
5140            affinity_key(&sharded_round_robin, &front, Some(CLIENT), NO_STICKY),
5141            Some(affinity_key_from_ip(CLIENT.ip())),
5142            "a sharded cluster derives a key whatever its policy"
5143        );
5144        for policy in [
5145            LoadBalancingAlgorithms::Hrw,
5146            LoadBalancingAlgorithms::Maglev,
5147        ] {
5148            assert_eq!(
5149                affinity_key(
5150                    &cluster(policy, None, None),
5151                    &front,
5152                    Some(CLIENT),
5153                    NO_STICKY
5154                ),
5155                Some(affinity_key_from_ip(CLIENT.ip())),
5156                "{policy:?} keys on the source IP by default"
5157            );
5158        }
5159    }
5160
5161    /// The configured header keys the request, its name matched without
5162    /// regard to case; a header the request lacks, or carries empty, falls
5163    /// back to the source IP; with no source address either there is no key.
5164    #[test]
5165    fn a_configured_header_keys_the_request_and_the_source_ip_is_the_fallback() {
5166        let mut pool = Pool::with_capacity(1, 1, 4096);
5167        let front = parsed(&mut pool, REQUEST);
5168        let hrw = LoadBalancingAlgorithms::Hrw;
5169        assert_eq!(
5170            affinity_key(
5171                &cluster(hrw, Some("X-Tenant"), None),
5172                &front,
5173                Some(CLIENT),
5174                NO_STICKY
5175            ),
5176            Some(affinity_key_from_value(b"acme"))
5177        );
5178        assert_eq!(
5179            affinity_key(
5180                &cluster(hrw, Some("X-Absent"), None),
5181                &front,
5182                Some(CLIENT),
5183                NO_STICKY
5184            ),
5185            Some(affinity_key_from_ip(CLIENT.ip()))
5186        );
5187        assert_eq!(
5188            affinity_key(
5189                &cluster(hrw, Some("X-Empty"), None),
5190                &front,
5191                Some(CLIENT),
5192                NO_STICKY
5193            ),
5194            Some(affinity_key_from_ip(CLIENT.ip())),
5195            "an empty value keys nothing and falls back to the source IP"
5196        );
5197        assert_eq!(
5198            affinity_key(
5199                &cluster(hrw, Some("X-Absent"), None),
5200                &front,
5201                None,
5202                NO_STICKY
5203            ),
5204            None,
5205            "no header and no source address: no key, and the policy round-robins"
5206        );
5207    }
5208
5209    /// The configured cookie keys the request, its name matched exactly
5210    /// (RFC 6265 cookie names are case-sensitive).
5211    #[test]
5212    fn a_configured_cookie_keys_the_request() {
5213        let mut pool = Pool::with_capacity(1, 1, 4096);
5214        let front = parsed(&mut pool, REQUEST);
5215        let maglev = LoadBalancingAlgorithms::Maglev;
5216        assert_eq!(
5217            affinity_key(
5218                &cluster(maglev, None, Some("tenant")),
5219                &front,
5220                Some(CLIENT),
5221                NO_STICKY
5222            ),
5223            Some(affinity_key_from_value(b"globex"))
5224        );
5225        assert_eq!(
5226            affinity_key(
5227                &cluster(maglev, None, Some("Tenant")),
5228                &front,
5229                Some(CLIENT),
5230                NO_STICKY
5231            ),
5232            Some(affinity_key_from_ip(CLIENT.ip())),
5233            "a cookie name differing in case is another cookie"
5234        );
5235    }
5236
5237    /// #524: deriving the key allocates nothing: the header blocks and the
5238    /// cookie jar are read where they lie and hashed in place.
5239    ///
5240    /// TO SEE THIS RED: copy the value out before hashing it in
5241    /// `affinity_key`, e.g. `affinity_key_from_value(&value.to_vec())`; each
5242    /// derivation then makes one allocation.
5243    #[test]
5244    fn deriving_an_affinity_key_allocates_nothing() {
5245        use std::hint::black_box;
5246
5247        use crate::test_allocations::allocations;
5248
5249        const DERIVATIONS: usize = 64;
5250        let mut pool = Pool::with_capacity(1, 1, 4096);
5251        let front = parsed(&mut pool, REQUEST);
5252        let clusters = [
5253            cluster(LoadBalancingAlgorithms::Hrw, None, None),
5254            cluster(LoadBalancingAlgorithms::Hrw, Some("X-Tenant"), None),
5255            cluster(LoadBalancingAlgorithms::Maglev, None, Some("tenant")),
5256            cluster(LoadBalancingAlgorithms::Maglev, Some("X-Absent"), None),
5257        ];
5258        let mut allocated = 0;
5259        for cluster in &clusters {
5260            for _ in 0..DERIVATIONS {
5261                let before = allocations();
5262                let key = affinity_key(
5263                    black_box(cluster),
5264                    black_box(&front),
5265                    Some(CLIENT),
5266                    NO_STICKY,
5267                );
5268                allocated += allocations() - before;
5269                assert!(key.is_some());
5270            }
5271        }
5272        assert_eq!(
5273            allocated,
5274            0,
5275            "{} derivations made {allocated} heap allocations, expected none",
5276            DERIVATIONS * clusters.len()
5277        );
5278    }
5279
5280    /// A cluster keying on the listener's own sticky-session cookie reads the
5281    /// value captured before that cookie was elided from the jar; without the
5282    /// capture it would never find the cookie and always fall back to the
5283    /// source IP.
5284    ///
5285    /// TO SEE THIS RED: drop the sticky-name arm of `affinity_key`, so the
5286    /// lookup reads only the jar.
5287    #[test]
5288    fn an_affinity_cookie_named_like_the_sticky_cookie_reads_its_captured_value() {
5289        let mut pool = Pool::with_capacity(1, 1, 4096);
5290        // The sticky cookie is already gone from the jar, as
5291        // `on_request_headers` leaves it.
5292        let front = parsed(&mut pool, REQUEST);
5293        let hrw = cluster(LoadBalancingAlgorithms::Hrw, None, Some("SOZUBALANCEID"));
5294        assert_eq!(
5295            affinity_key(
5296                &hrw,
5297                &front,
5298                Some(CLIENT),
5299                StickyCookie {
5300                    name: "SOZUBALANCEID",
5301                    value: Some("backend-7"),
5302                },
5303            ),
5304            Some(affinity_key_from_value(b"backend-7"))
5305        );
5306        assert_eq!(
5307            affinity_key(&hrw, &front, Some(CLIENT), NO_STICKY),
5308            Some(affinity_key_from_ip(CLIENT.ip())),
5309            "no sticky cookie on the request: the source IP keys it"
5310        );
5311    }
5312}