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, 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 = listener.borrow().frontend_from_request(host, uri, method);
1177
1178        let route = match route_result {
1179            Ok(route) => route,
1180            Err(frontend_error) => {
1181                trace!("{} {}", log_module_context!(context), frontend_error);
1182                return Err(RetrieveClusterError::RetrieveFrontend(frontend_error));
1183            }
1184        };
1185
1186        // ── Resolve the routing decision ──────────────────────────────────
1187        // Snapshot the policy fields we need before consuming `route`, then
1188        // map each policy outcome to either an early-error variant (which
1189        // the caller turns into a default answer) or a cluster_id (which
1190        // proceeds to backend connect).
1191        let RouteResult {
1192            cluster_id,
1193            redirect,
1194            redirect_scheme,
1195            redirect_template,
1196            rewritten_host,
1197            rewritten_path,
1198            rewritten_port,
1199            headers_request,
1200            headers_response,
1201            required_auth: frontend_required_auth,
1202            tags,
1203        } = route;
1204
1205        // The matched frontend rule is the only correct owner of the
1206        // access-log tags, so stash them here — before every early return
1207        // below, so a redirect, a 401 and a backend-connect failure log
1208        // them too. The listener's `BTreeMap<String, CachedTags>` cannot
1209        // do this job: it is written under the frontend rule's hostname
1210        // and read under the request's authority, two spellings that only
1211        // coincide for an exact literal frontend (sozu#1379).
1212        //
1213        // No `..` in the destructure above, deliberately: that wildcard is
1214        // exactly how `tags` was dropped on the floor for as long as the
1215        // bug lived. A future `RouteResult` field now has to be routed
1216        // here explicitly or the build breaks.
1217        context.tags = tags;
1218
1219        // ── HSTS (RFC 6797) snapshot hoist for HTTPS ──────────────────────
1220        // The response snapshot is built in two passes so HSTS reaches
1221        // every HTTPS response code (RFC 6797 §8.1 — including
1222        // proxy-generated 3xx / 401 / 5xx default answers) WITHOUT
1223        // changing the pre-PR scope of operator-defined `Append`
1224        // response headers (which only apply on the regular forward
1225        // path).
1226        //
1227        // Pass 1 (here, before any early return): for HTTPS only, copy
1228        // ONLY the HSTS-class typed edits (`SetIfAbsent | Set`). These
1229        // need to land on default answers — `set_default_answer_with_retry_after`
1230        // bypasses the post-forward copy below.
1231        //
1232        // Pass 2 (post-forward, end of function): copy EVERY edit
1233        // (including operator `Append` headers). Runs only on the
1234        // regular forward path because the early returns short-circuit
1235        // before reaching it.
1236        //
1237        // Plain-HTTP listeners are skipped here per RFC 6797 §7.2 (no
1238        // STS over plaintext) — defense in depth on top of the
1239        // TOML-time `ConfigError::HstsOnPlainHttp` and the worker IPC
1240        // `ProxyError::HstsOnPlainHttp` rejects.
1241        if matches!(context.protocol, crate::Protocol::HTTPS) {
1242            snapshot_response_edits(&mut context.headers_response, &headers_response, |e| {
1243                matches!(e.mode, HeaderEditMode::SetIfAbsent | HeaderEditMode::Set)
1244            });
1245        }
1246
1247        // Look up cluster-side policy knobs once. The values we need are:
1248        //  - `https_redirect` (legacy) and `https_redirect_port` for the 301 location URL
1249        //  - `authorized_hashes` and `www_authenticate` for the 401 path
1250        let (legacy_https_redirect, https_redirect_port, authorized_hashes, www_authenticate) =
1251            match cluster_id.as_deref() {
1252                Some(id) => view
1253                    .cluster(id)
1254                    .map(|c| {
1255                        (
1256                            c.https_redirect,
1257                            c.https_redirect_port,
1258                            c.authorized_hashes.clone(),
1259                            c.www_authenticate.clone(),
1260                        )
1261                    })
1262                    .unwrap_or((false, None, Vec::new(), None)),
1263                None => (false, None, Vec::new(), None),
1264            };
1265
1266        // ── 1. Explicit redirect policies (PERMANENT / FOUND / PERMANENT_REDIRECT) ──
1267        // Resolved BEFORE the clusterless-deny branch so a frontend that
1268        // declares `redirect = permanent | found | permanent_redirect`
1269        // emits the matching 3xx even when no cluster is bound. This is
1270        // the canonical "moved" shape from the original proposal in
1271        // #1161 and is the only way to express "this hostname has moved"
1272        // without standing up a dummy cluster. The block does not read
1273        // `cluster_id`; per-cluster values (`https_redirect_port`,
1274        // `www_authenticate`, …) default to safe sentinels at the cluster
1275        // lookup above when `cluster_id` is `None`, so the reorder is
1276        // data-flow-safe.
1277        //
1278        // Status code mapping (closes #1009):
1279        //   Permanent          → 301 (RFC 9110 §15.4.2)
1280        //   Found              → 302 (RFC 9110 §15.4.3) — UA may rewrite POST→GET
1281        //   PermanentRedirect  → 308 (RFC 9110 §15.4.9) — method MUST be preserved
1282        let redirect_status = match redirect {
1283            RedirectPolicy::Permanent => Some(301u16),
1284            RedirectPolicy::Found => Some(302u16),
1285            RedirectPolicy::PermanentRedirect => Some(308u16),
1286            // Forward / Unauthorized are handled by other branches
1287            // below; keeping them named here forces an exhaustive
1288            // match so a future RedirectPolicy variant doesn't
1289            // silently fall through to `None`.
1290            RedirectPolicy::Forward | RedirectPolicy::Unauthorized => None,
1291        };
1292        if let Some(status_code) = redirect_status {
1293            let scheme = resolve_redirect_scheme(redirect_scheme, context);
1294            let port = rewritten_port.map(|p| p as u32).or(https_redirect_port);
1295            // Feed the rewritten host AND path into the `Location` URL
1296            // when the frontend's RewriteParts populated them. Without
1297            // this, a `redirect = permanent` frontend with
1298            // `rewrite_host = "new.example.com"` would serve clients
1299            // back to the original `Host:` header, defeating the
1300            // documented `old → new` shape.
1301            // The host_override path also keeps `:port` stripping
1302            // intact: `build_redirect_location` removes any `:port` on
1303            // the override before reapplying `port_suffix`.
1304            context.redirect_location = Some(build_redirect_location(
1305                scheme,
1306                context,
1307                port,
1308                rewritten_host.as_deref(),
1309                rewritten_path.as_deref(),
1310            ));
1311            // Stash the frontend's `redirect_template` (when set) so the
1312            // 3xx default-answer path can render it via
1313            // `HttpAnswers::render_inline_redirect` instead of the
1314            // listener / cluster default. Without this stash the field
1315            // flows into `RouteResult` only to be dropped by the
1316            // wildcard destructure below, so the operator-supplied
1317            // template has no observable effect on the rendered
1318            // redirect.
1319            context.frontend_redirect_template = redirect_template;
1320            // Stash the resolved status so the answer engine picks the
1321            // matching default template (`http.301.redirection` /
1322            // `http.302.redirection` / `http.308.redirection`).
1323            context.redirect_status = Some(status_code);
1324            return Err(RetrieveClusterError::HttpsRedirect);
1325        }
1326
1327        // ── 2. Explicit `RedirectPolicy::UNAUTHORIZED` or clusterless deny ─
1328        // Reached when the frontend either explicitly asks for 401 or has
1329        // no backing cluster and no `Permanent` redirect to honour. The
1330        // `Forward + cluster_id == None` combination collapses here so
1331        // legacy clusterless frontends still emit 401 by default.
1332        if matches!(redirect, RedirectPolicy::Unauthorized) || cluster_id.is_none() {
1333            context.www_authenticate = www_authenticate.clone();
1334            trace!("{} RouteResult::deny", log_module_context!(context));
1335            return Err(RetrieveClusterError::UnauthorizedRoute);
1336        }
1337
1338        let Some(cluster_id) = cluster_id else {
1339            // Guarded by the clusterless-deny branch immediately above;
1340            // the `is_none()` arm has already returned `UnauthorizedRoute`
1341            // by the time control reaches here.
1342            unreachable!("cluster_id was checked Some above")
1343        };
1344
1345        // ── 3. Legacy `cluster.https_redirect` (HTTP-only listeners) ───────
1346        // The caller (`Router::plan_connect`) emits the actual 301 only on
1347        // `ListenerType::Http`; gate the URL stash on the same predicate
1348        // so an HTTPS listener never carries a stale `redirect_location`
1349        // into a downstream default-answer path.
1350        if legacy_https_redirect && view.is_http_listener() {
1351            let port = https_redirect_port;
1352            context.redirect_location =
1353                Some(build_redirect_location("https", context, port, None, None));
1354        }
1355
1356        // ── 4. Basic auth check (only when `required_auth` was set) ────────
1357        // The check iterates the full hash list in constant time (see
1358        // `crate::protocol::mux::auth::check_basic`) so the time spent
1359        // does not leak which hash matched, or whether any did at all.
1360        // On failure, stash the cluster's `www_authenticate` realm so the
1361        // 401 default-answer can render the matching `WWW-Authenticate`
1362        // header. An empty realm causes the template engine to elide the
1363        // header entirely (`or_elide_header = true`).
1364        if frontend_required_auth
1365            && !crate::protocol::mux::auth::check_basic(front, &authorized_hashes)
1366        {
1367            context.www_authenticate = www_authenticate.clone();
1368            trace!(
1369                "{} basic-auth check failed; emitting 401",
1370                log_module_context!(context)
1371            );
1372            return Err(RetrieveClusterError::UnauthorizedRoute);
1373        }
1374
1375        // ── 5. Request-side mutations on the front kawa ────────────────────
1376        // From here on the route is a Forward — apply the frontend's
1377        // rewrite + header policy to the request kawa so the backend
1378        // wire carries the operator-configured shape.
1379        apply_request_rewrites_and_headers(
1380            front,
1381            context,
1382            rewritten_host.as_deref(),
1383            rewritten_path.as_deref(),
1384            &headers_request,
1385        );
1386
1387        // Pass 2 of the response-snapshot copy (see the HSTS hoist
1388        // above). Runs unconditionally on the regular forward path
1389        // (the early returns above bypass this site, which keeps the
1390        // default-answer scope as HSTS-only). Copies EVERY edit so
1391        // operator-defined `Append` response headers reach
1392        // backend-served responses on both HTTP and HTTPS listeners,
1393        // preserving their pre-PR scope.
1394        snapshot_response_edits(&mut context.headers_response, &headers_response, |_| true);
1395
1396        Ok(cluster_id)
1397    }
1398
1399    /// Pick the backend for an already-routed request and stamp its identity
1400    /// onto the request's [`HttpContext`].
1401    ///
1402    /// Takes no listener handle: every listener-derived value this needs —
1403    /// `sticky_name` above all — is already on the `HttpContext` that
1404    /// `Context::create_stream` filled when the request arrived, and reading
1405    /// the listener again here would be exactly the mid-request reload leak
1406    /// `LIFECYCLE.md` §2.5 rules out.
1407    ///
1408    /// Takes no proxy handle either: the backend set arrives as `selector`, a
1409    /// capability borrowed for this one call (see [`BackendSelector`]).
1410    ///
1411    /// Returns the chosen backend, reserved but not dialled: the caller
1412    /// connects to [`BackendId::address`] (#1684).
1413    pub fn backend_from_request(
1414        &mut self,
1415        cluster_id: &str,
1416        frontend_should_stick: bool,
1417        context: &mut HttpContext,
1418        selector: &mut dyn BackendSelector,
1419    ) -> Result<BackendId, BackendConnectionError> {
1420        // Spelled out rather than folded into the selector's signature: a
1421        // cookie the client sent to a frontend that does not stick is
1422        // ignored, exactly as the `(false, Some(_))` arm of the match this
1423        // replaced ignored it.
1424        let affinity = if frontend_should_stick {
1425            Affinity::Sticky(context.sticky_session_found.as_deref())
1426        } else {
1427            Affinity::Unpinned
1428        };
1429        let selected = selector
1430            .select(cluster_id, affinity, context.affinity_key)
1431            .map_err(|backend_error| {
1432                trace!("{} {}", log_module_context!(context), backend_error);
1433                BackendConnectionError::Backend(backend_error)
1434            })?;
1435        debug_assert_eq!(
1436            selected.sticky_session.is_some(),
1437            frontend_should_stick,
1438            "a selector resolves a sticky cookie exactly when the frontend sticks"
1439        );
1440
1441        // `context.sticky_name` is the name `Context::create_stream` captured
1442        // when this request arrived, and it stays that name to the end of the
1443        // request. Re-reading the listener here used to hand an in-flight
1444        // request a cookie name an operator installed after it started — the
1445        // request would then have been matched on the old name and answered
1446        // with a `Set-Cookie` under the new one. A reload applies from the
1447        // next request (`LIFECYCLE.md` §2.5).
1448        if let Some(sticky_session) = selected.sticky_session {
1449            context.sticky_session = Some(sticky_session);
1450        }
1451
1452        context.backend_id = Some(Rc::clone(&selected.backend.backend_id));
1453        context.backend_address = Some(selected.backend.address);
1454
1455        Ok(selected.backend)
1456    }
1457}
1458
1459/// The worker's backend set, as the one capability
1460/// [`Router::backend_from_request`] needs from it: pick a backend and reserve
1461/// a connection on it.
1462///
1463/// Question 12 of [#1340](https://github.com/sozu-proxy/sozu/issues/1340)
1464/// asked for a borrowed view of backend load state, "the same shape"
1465/// `RoutingView` gave cluster configuration. That shape cannot select.
1466/// `RoutingView` is an immutable borrow of immutable data, and selection
1467/// mutates: the round-robin cursor, the fail-open latch, `PeakEWMA` under
1468/// `LoadMetric::ConnectionTime`, the Maglev table and the cluster
1469/// availability latch. A snapshot of the load counters cannot be taken
1470/// without allocating either — each backend sits behind its own `RefCell`, so
1471/// lending the core N of them at once needs a container of N `Ref`s. The shape
1472/// that fits is the UDP core's `BackendSource` (`lib/src/protocol/udp/mod.rs`),
1473/// a `&mut dyn` the core calls and the embedder implements.
1474///
1475/// # Selection reserves, a failed dial releases
1476///
1477/// The selector returns a backend and no socket: the embedder dials
1478/// [`BackendId::address`] itself, which is what lets a selector exist without
1479/// an OS socket type (#1684). What keeps the load balancer honest is that
1480/// selecting **reserves**: the embedder's selector raises the chosen
1481/// backend's `active_connections` under the same borrow as the choice
1482/// (`BackendMap::reserve_backend`), so the second stream linking in the same
1483/// pass weighs the first one's connection before it has been dialled, exactly
1484/// as it weighed the connection `Backend::try_connect` used to count at the
1485/// dial. A `connect(2)` that fails releases the reservation through the
1486/// ledger (`BackendChange::DialFailed`), and LIFECYCLE §9 invariant 14's
1487/// drain before each selection applies it before anything reads the counters
1488/// again. A dial that connects but is abandoned before its connection is
1489/// registered — no buffer for an H2 client, a refused stream start, a failed
1490/// `register_socket` — releases it through `BackendChange::ConnectionClosed`,
1491/// recording no failure (#1713). A dial that succeeds keeps the reservation
1492/// as its connection's count, and the connection's close releases it.
1493///
1494/// # Why a trait and not `&mut BackendMap`
1495///
1496/// A bare `&mut BackendMap` would be smaller, and it would still leave
1497/// [`Router::backend_from_request`] handing back an `Rc<RefCell<Backend>>`,
1498/// because turning that handle into the opaque [`BackendId`] takes the
1499/// session's `BackendRegistry` (`lib/src/protocol/mux/mod.rs`) as well. The
1500/// embedder's implementation holds both, so no registry handle reaches this
1501/// file at all. It is also what lets a simulator stand in for the backend set:
1502/// such a selector mints its ids with [`BackendId::new`] and needs no socket.
1503///
1504/// `Router` has no lifetime parameter, so it cannot keep the `&mut` it is
1505/// lent; the `Rc<RefCell<dyn L7Proxy>>` this replaced was `'static` and
1506/// `Clone`, and nothing stopped it being stored.
1507pub trait BackendSelector {
1508    /// Select a backend of `cluster_id` and reserve a connection on it.
1509    ///
1510    /// Under [`Affinity::Sticky`] the result carries the cookie value to
1511    /// answer with, read from the chosen backend under the same borrow as the
1512    /// selection. It is resolved here rather than carried on [`BackendId`]
1513    /// because `Backend::sticky_id` is not immutable: `BackendList::add_backend`
1514    /// (`lib/src/backends.rs`) rewrites it in place on the live registry entry
1515    /// when an existing backend is re-added, and `BackendId` carries only what
1516    /// cannot change for the life of an entry.
1517    ///
1518    /// `key` is the client affinity key `affinity_key` in this module derived
1519    /// for the request, `None` unless the cluster selects with `HRW` or
1520    /// `MAGLEV`. It decides where the load balancer sends a request the sticky
1521    /// cookie did not pin, and nothing else: a cookie naming a live backend
1522    /// still wins.
1523    fn select(
1524        &mut self,
1525        cluster_id: &str,
1526        affinity: Affinity<'_>,
1527        key: Option<u64>,
1528    ) -> Result<SelectedBackend, BackendError>;
1529}
1530
1531/// Whether selection for `cluster` reads a client affinity key: its policy
1532/// is `HRW` or `MAGLEV`, or it shards (`shard_percent`), which ranks each
1533/// client's shard by the key under any policy (sozu-proxy/sozu#524). Every
1534/// other cluster derives no key and pays nothing.
1535pub(crate) fn cluster_reads_affinity_key(cluster: &Cluster) -> bool {
1536    cluster.shard_percent.is_some()
1537        || matches!(
1538            cluster.load_balancing(),
1539            LoadBalancingAlgorithms::Hrw | LoadBalancingAlgorithms::Maglev
1540        )
1541}
1542
1543/// The client affinity key of a request routed to `cluster`, for the `HRW`
1544/// and `MAGLEV` policies to pin the client with and for shuffle sharding to
1545/// rank its shard by; `None` for every other cluster, which would ignore it
1546/// ([`cluster_reads_affinity_key`]).
1547///
1548/// The key is the hash of the cluster's `affinity_header` value (first
1549/// occurrence, name matched case-insensitively) or `affinity_cookie` value
1550/// (name matched case-sensitively, RFC 6265), and, when the request does not
1551/// carry that header or cookie, or carries it empty, of the client's source
1552/// IP — `session_address`, the PROXY-protocol source when the listener
1553/// expects one. With no source address either, there is no key, and the
1554/// policies fall back to round-robin as they always did.
1555///
1556/// A cookie named like the listener's sticky-session cookie is read from the
1557/// value `HttpContext::on_request_headers` captured, since that cookie is
1558/// elided from the jar before routing. Values are hashed raw, as sent: a
1559/// quoted cookie value and its unquoted spelling are two different keys.
1560///
1561/// It reads the header blocks and cookie jar where they lie in the request
1562/// buffer and hashes the bytes in place: no copy, no allocation. The hash is
1563/// the seeded FNV of `crate::load_balancing`, so every worker and every
1564/// restart derives the same key for the same client.
1565///
1566/// A header or cookie value is chosen by the client, so a client can choose
1567/// its backend under this option — exactly as it can with a sticky-session
1568/// cookie. It spreads clients; it does not isolate them from one another.
1569pub(super) fn affinity_key(
1570    cluster: &Cluster,
1571    front: &GenericHttpStream,
1572    session_address: Option<SocketAddr>,
1573    sticky: StickyCookie<'_>,
1574) -> Option<u64> {
1575    if !cluster_reads_affinity_key(cluster) {
1576        return None;
1577    }
1578    // `validate_affinity_key` (`command/src/config.rs`) refuses a cluster
1579    // naming both on every `AddCluster` the master accepts; the header is
1580    // read first should a worker ever receive both.
1581    let buf = front.storage.buffer();
1582    let value = if let Some(name) = cluster.affinity_header.as_deref() {
1583        front.blocks.iter().find_map(|block| match block {
1584            Block::Header(header)
1585                if !header.is_elided()
1586                    && compare_no_case(header.key.data(buf), name.as_bytes()) =>
1587            {
1588                Some(header.val.data(buf))
1589            }
1590            _ => None,
1591        })
1592    } else if let Some(name) = cluster.affinity_cookie.as_deref() {
1593        if name == sticky.name {
1594            // `HttpContext::on_request_headers` has already elided this
1595            // cookie from the jar (Sōzu's own session cookie never reaches a
1596            // backend) and captured its value, so the key reads the capture.
1597            sticky.value.map(str::as_bytes)
1598        } else {
1599            front.detached.jar.iter().find_map(|cookie| {
1600                (!cookie.is_elided() && cookie.key.data(buf) == name.as_bytes())
1601                    .then(|| cookie.val.data(buf))
1602            })
1603        }
1604    } else {
1605        None
1606    };
1607    // Pre: a request value is read only for a cluster that names one.
1608    debug_assert!(
1609        value.is_none() || cluster.affinity_header.is_some() || cluster.affinity_cookie.is_some(),
1610        "a header or cookie value keys only a cluster that configured its name"
1611    );
1612    let key = match value.filter(|value| !value.is_empty()) {
1613        Some(value) => Some(affinity_key_from_value(value)),
1614        None => session_address.map(|address| affinity_key_from_ip(address.ip())),
1615    };
1616    // Post: under a keyed policy the only request left without a key is one
1617    // that carries no usable value AND has no source address.
1618    debug_assert!(
1619        key.is_some() || session_address.is_none(),
1620        "a request with a source address must always be keyed"
1621    );
1622    key
1623}
1624
1625/// The listener's sticky-session cookie as the request arrived with it: its
1626/// name, and the value `HttpContext::on_request_headers` captured before
1627/// eliding it from the cookie jar. [`affinity_key`] reads it when a cluster's
1628/// `affinity_cookie` names that same cookie.
1629#[derive(Clone, Copy, Debug)]
1630pub(super) struct StickyCookie<'a> {
1631    pub(super) name: &'a str,
1632    pub(super) value: Option<&'a str>,
1633}
1634
1635/// Whether a request is pinned to a backend by its sticky-session cookie.
1636#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1637pub enum Affinity<'a> {
1638    /// The frontend does not stick: any cookie the client sent is ignored.
1639    Unpinned,
1640    /// The frontend sticks. `Some` is the cookie the request carried, tried
1641    /// first; `None`, or a cookie naming no live backend, falls back to the
1642    /// cluster's load balancer. Either way the answer carries a cookie.
1643    Sticky(Option<&'a str>),
1644}
1645
1646/// What [`BackendSelector::select`] answers: the chosen backend, whose
1647/// [`BackendId::address`] the caller dials, and — only under
1648/// [`Affinity::Sticky`] — the cookie value that pins the client to it.
1649#[derive(Debug)]
1650pub struct SelectedBackend {
1651    pub backend: BackendId,
1652    pub sticky_session: Option<String>,
1653}
1654
1655/// Apply the frontend's request-side rewrite + header policy to the
1656/// request kawa. Mutations land before backend connect so the backend
1657/// wire carries the rewritten shape:
1658///
1659/// 1. If `rewritten_host` is set, replace the request-line authority
1660///    with the rewritten value, replace any existing `Host` request
1661///    header (so H1 backends see the same value the H2 `:authority`
1662///    would carry), and inject `X-Forwarded-Host` carrying the
1663///    pre-rewrite authority. The X-Forwarded-Host injection ONLY fires
1664///    when `rewritten_host` is set — without a rewrite there is no host
1665///    swap to disclose, and HAProxy's `option forwardfor` style
1666///    headers (`X-Forwarded-For`, `X-Forwarded-Proto`) still flow from
1667///    the kawa parser. The pre-rewrite authority is read from
1668///    `context.authority`, which no rewrite touches, and recorded in
1669///    `context.original_authority` only here, so a request without a
1670///    host rewrite copies no authority. `X-Forwarded-Host` belongs to the
1671///    `X-Forwarded-*` family, so it is injected (and a client one
1672///    replaced) only when the listener's `forwarded_headers` mode emits
1673///    that family (`both`, `x_forwarded`); `rfc7239` and `none` convey no
1674///    pre-rewrite authority, and under `none` a client `X-Forwarded-Host`
1675///    passes through as sent.
1676///    Dedup rule: the synthetic Host AND any pre-existing Host header
1677///    are dropped in the retain pass below before the rewritten Host is
1678///    appended, so the wire never carries two `Host:` headers.
1679/// 2. If `rewritten_path` is set, replace both the abstract path
1680///    (consumed by H2 `:path`) and the request-line URI (consumed by
1681///    the H1 converter) so cardinality H1↔H1, H1↔H2, H2↔H1, H2↔H2 all
1682///    propagate the rewritten target.
1683/// 3. For every `headers_request` edit:
1684///    - empty `val` → remove every existing header with the matching
1685///      name from `kawa.blocks` (HAProxy `del-header` parity);
1686///    - non-empty `val` → append the header before the `end_header`
1687///      flag block. Set/replace semantics: callers that want to replace
1688///      a header pass two edits (one delete with empty val, one set
1689///      with the new value).
1690fn apply_request_rewrites_and_headers(
1691    kawa: &mut super::GenericHttpStream,
1692    context: &mut HttpContext,
1693    rewritten_host: Option<&str>,
1694    rewritten_path: Option<&str>,
1695    headers_request: &[HeaderEdit],
1696) {
1697    use kawa::{Block, Pair, Store};
1698
1699    if rewritten_host.is_none() && rewritten_path.is_none() && headers_request.is_empty() {
1700        return;
1701    }
1702
1703    // `context.authority` is the authority the client sent: the rewrites
1704    // below mutate the kawa status line and headers, never the context. It
1705    // is copied only when the host is rewritten, for the X-Forwarded-Host
1706    // injection, and the copy then moves into `context.original_authority`.
1707    let original_authority: Option<String> = if rewritten_host.is_some() {
1708        context.authority.clone()
1709    } else {
1710        None
1711    };
1712
1713    // ── status-line authority / path rewrites ─────────────────────────
1714    // The kawa request status line carries both `path` and `uri` —
1715    // `path` is the abstract path (consumed by the H2 converter to
1716    // emit `:path`) while `uri` is the request-line URI (consumed by
1717    // the H1 converter at `kawa::protocol::h1::converter`). Both must
1718    // be mutated so an H1 frontend forwarding to an H1 backend AND an
1719    // H2 frontend forwarding to an H1 backend (or vice versa) see the
1720    // rewritten target on the wire.
1721    if (rewritten_host.is_some() || rewritten_path.is_some())
1722        && let kawa::StatusLine::Request {
1723            authority,
1724            path,
1725            uri,
1726            ..
1727        } = &mut kawa.detached.status_line
1728    {
1729        if let Some(new_host) = rewritten_host {
1730            *authority = Store::from_string(new_host.to_owned());
1731        }
1732        if let Some(new_path) = rewritten_path {
1733            *path = Store::from_string(new_path.to_owned());
1734            *uri = Store::from_string(new_path.to_owned());
1735        }
1736    }
1737
1738    // ── single-pass split: deletes vs. sets ───────────────────────────
1739    // Walk `headers_request` once and separate each edit into either the
1740    // delete list (empty val) or the insert list (non-empty val). Two
1741    // passes was wasteful when an operator stacks many `--header` flags;
1742    // one pass keeps the allocation profile flat.
1743    let host_lower = b"host";
1744    let xfh_lower = b"x-forwarded-host";
1745    let rewriting_host = rewritten_host.is_some();
1746    // X-Forwarded-Host discloses the rewrite only in a mode that emits the
1747    // X-Forwarded-* family; `rfc7239` already elided a client one in
1748    // `HttpContext::on_request_headers`, and `none` leaves it as sent.
1749    let synthesises_xfh = rewriting_host && emits_x_forwarded(context.forwarded_headers);
1750    let mut keys_to_drop: Vec<Vec<u8>> = Vec::with_capacity(headers_request.len() + 2);
1751    let mut to_insert: Vec<Block> = Vec::with_capacity(headers_request.len() + 2);
1752    // Track whether any operator-supplied edit names Host or
1753    // X-Forwarded-Host so we always dedup the existing kawa Host header
1754    // before inserting the operator's value. Without this, an operator
1755    // who sets `--header request=Host=evil` on a frontend WITHOUT
1756    // `--rewrite-host` lands TWO `Host:` headers on the backend wire —
1757    // a request-smuggling primitive on backends that pick last-Host
1758    // (CWE-444 cousin).
1759    let mut operator_overrides_host = false;
1760    let mut operator_overrides_xfh = false;
1761    for edit in headers_request {
1762        let key_is_host = edit.key.eq_ignore_ascii_case(host_lower);
1763        let key_is_xfh = edit.key.eq_ignore_ascii_case(xfh_lower);
1764        operator_overrides_host |= key_is_host;
1765        operator_overrides_xfh |= key_is_xfh;
1766        if edit.val.is_empty() {
1767            keys_to_drop.push(edit.key.iter().map(u8::to_ascii_lowercase).collect());
1768        } else {
1769            to_insert.push(Block::Header(Pair {
1770                key: Store::from_slice(&edit.key),
1771                val: Store::from_slice(&edit.val),
1772            }));
1773        }
1774    }
1775    if rewriting_host || operator_overrides_host {
1776        keys_to_drop.push(host_lower.to_vec());
1777    }
1778    if synthesises_xfh || operator_overrides_xfh {
1779        keys_to_drop.push(xfh_lower.to_vec());
1780    }
1781
1782    // ── delete pass on existing blocks ────────────────────────────────
1783    let buf_ptr = kawa.storage.buffer();
1784    if !keys_to_drop.is_empty() {
1785        // Read `key.data(buf_ptr)` only on non-elided headers — kawa's
1786        // earlier passes (HPACK decoder, H1 header parser) tag suppressed
1787        // headers with `Store::Empty` rather than removing them, and
1788        // calling `.data()` on `Store::Empty` panics in
1789        // `kawa-0.6.8/src/storage/repr.rs`. Pinning the guard explicitly
1790        // until kawa changes its policy.
1791        let buf = buf_ptr;
1792        kawa.blocks.retain(|block| {
1793            if let Block::Header(Pair { key, val: _ }) = block {
1794                if matches!(key, Store::Empty) {
1795                    return true;
1796                }
1797                let key_bytes = key.data(buf);
1798                // Both `keys_to_drop` and `key_lower` are pre-lowercased,
1799                // so a byte-equality compare is sufficient — a second
1800                // ASCII-fold pass via `compare_no_case` would just burn
1801                // cycles re-folding bytes that are already canonical.
1802                let key_lower: Vec<u8> = key_bytes.iter().map(u8::to_ascii_lowercase).collect();
1803                !keys_to_drop
1804                    .iter()
1805                    .any(|k| k.as_slice() == key_lower.as_slice())
1806            } else {
1807                true
1808            }
1809        });
1810    }
1811
1812    // ── insertion before the end-of-headers flag ──────────────────────
1813    // Every header we add (rewritten Host, X-Forwarded-Host,
1814    // operator-supplied set/append edits) must land before
1815    // `Block::Flags { end_header: true }` so the converter emits them
1816    // as part of the request header block. Synthetic Host/X-Forwarded-Host
1817    // are prepended (they describe the rewrite, not an operator policy).
1818    let end_header_idx = super::shared::end_of_headers_index(kawa);
1819
1820    if rewriting_host {
1821        let mut synth: Vec<Block> = Vec::with_capacity(2);
1822        if let Some(new_host) = rewritten_host {
1823            synth.push(Block::Header(Pair {
1824                key: Store::Static(b"Host"),
1825                val: Store::from_string(new_host.to_owned()),
1826            }));
1827        }
1828        if synthesises_xfh && let Some(orig) = original_authority.as_deref() {
1829            synth.push(Block::Header(Pair {
1830                key: Store::Static(b"X-Forwarded-Host"),
1831                val: Store::from_string(orig.to_owned()),
1832            }));
1833        }
1834        context.original_authority = original_authority;
1835        synth.append(&mut to_insert);
1836        to_insert = synth;
1837    }
1838    if !to_insert.is_empty() {
1839        let insert_at = end_header_idx.unwrap_or(kawa.blocks.len());
1840        for (offset, block) in to_insert.into_iter().enumerate() {
1841            kawa.blocks.insert(insert_at + offset, block);
1842        }
1843    }
1844}
1845
1846/// Copy a per-frontend response-edit slice into the per-stream
1847/// `HttpContext.headers_response` snapshot, applying `filter` to each
1848/// edit. The snapshot is cleared before the copy so a second pass on
1849/// the same context (the HSTS hoist + post-forward pattern in
1850/// `route_from_request`) overrides any earlier partial copy.
1851fn snapshot_response_edits<F>(target: &mut Vec<HeaderEditSnapshot>, src: &[HeaderEdit], filter: F)
1852where
1853    F: Fn(&HeaderEdit) -> bool,
1854{
1855    target.clear();
1856    for edit in src.iter().filter(|e| filter(e)) {
1857        target.push(HeaderEditSnapshot {
1858            key: edit.key.to_vec(),
1859            val: edit.val.to_vec(),
1860            mode: edit.mode,
1861        });
1862    }
1863}
1864
1865/// Resolve the protocol scheme to use when emitting a redirect's `Location`
1866/// header. Maps the proto enum onto `"http"` / `"https"`, with `USE_SAME`
1867/// preserving the request's scheme (HTTPS for TLS listeners, HTTP otherwise).
1868fn resolve_redirect_scheme(scheme: RedirectScheme, context: &HttpContext) -> &'static str {
1869    match scheme {
1870        RedirectScheme::UseHttps => "https",
1871        RedirectScheme::UseHttp => "http",
1872        RedirectScheme::UseSame => {
1873            if context.tls_server_name.is_some() {
1874                "https"
1875            } else {
1876                "http"
1877            }
1878        }
1879    }
1880}
1881
1882/// Build the `Location` URL for a redirect response. Defaults the port
1883/// suffix only when the operator provided one or when scheme defaults
1884/// would mismatch (port 80 on https / 443 on http stays implicit).
1885///
1886/// `host_override` and `path_override` carry the frontend's
1887/// `RewriteParts::run` output for `RedirectPolicy::PERMANENT` flows so
1888/// the 301 `Location` reflects `rewrite_host` / `rewrite_path` instead
1889/// of the original `:authority` / `:path`. The legacy
1890/// `cluster.https_redirect` path passes `None` for both — it has no
1891/// per-frontend rewrite knobs.
1892fn build_redirect_location(
1893    scheme: &str,
1894    context: &HttpContext,
1895    port: Option<u32>,
1896    host_override: Option<&str>,
1897    path_override: Option<&str>,
1898) -> String {
1899    let authority = host_override
1900        .or(context.authority.as_deref())
1901        .unwrap_or_default();
1902    let path = path_override.or(context.path.as_deref()).unwrap_or("/");
1903    // Strip an existing `:port` from the authority — operators typically
1904    // configure `https_redirect_port` precisely because the listener's
1905    // port differs from the redirect target. Bracketed IPv6 literals
1906    // like `[::1]` survive intact: `rsplit_once(':')` only triggers when
1907    // the suffix after the final `:` is entirely ASCII digits.
1908    let host_only = match authority.rsplit_once(':') {
1909        Some((host, port_part))
1910            if !port_part.is_empty() && port_part.bytes().all(|b| b.is_ascii_digit()) =>
1911        {
1912            host
1913        }
1914        _ => authority,
1915    };
1916    let port_suffix = match port {
1917        Some(80) if scheme == "http" => String::new(),
1918        Some(443) if scheme == "https" => String::new(),
1919        Some(p) => format!(":{p}"),
1920        None => String::new(),
1921    };
1922    format!("{scheme}://{host_only}{port_suffix}{path}")
1923}
1924
1925/// Exact-match test between an HTTP `:authority` / `Host` value and a TLS SNI.
1926///
1927/// Matching rules:
1928///   * The authority is stripped of its optional `:port` suffix. RFC 6066 §3
1929///     forbids a port in the SNI extension, so the SNI is compared against
1930///     the host component only.
1931///   * The comparison is case-insensitive (RFC 9110 §4.2.3 — hosts are
1932///     case-insensitive). The SNI is assumed to be already lowercased by
1933///     the caller (see `https.rs::upgrade_handshake`); only the authority
1934///     side needs on-the-fly `to_ascii_lowercase`.
1935///   * No wildcard logic: if the operator serves a wildcard certificate,
1936///     the SNI negotiated by the client is still the specific name that
1937///     client sent, and the request `:authority` must equal that specific
1938///     name exactly. This is the tightest possible TLS trust boundary.
1939///
1940/// The `:port` suffix is only stripped when the suffix is non-empty and
1941/// entirely ASCII digits. This keeps bracketed IPv6 literals like `[::1]`
1942/// intact: `rsplit_once(':')` would otherwise mis-split them.
1943pub(crate) fn authority_matches_sni(authority: &str, sni_lowercased: &str) -> bool {
1944    let host = strip_authority_port(authority);
1945    if host.len() != sni_lowercased.len() {
1946        return false;
1947    }
1948    host.as_bytes()
1949        .iter()
1950        .zip(sni_lowercased.as_bytes())
1951        .all(|(a, b)| a.to_ascii_lowercase() == *b)
1952}
1953
1954/// Strip the optional `:port` suffix from an authority value. Bracketed
1955/// IPv6 literals (`[::1]`, `[::1]:8443`) keep their inner colons intact:
1956/// the suffix is only stripped when the tail after the last `:` is
1957/// non-empty and entirely ASCII digits.
1958fn strip_authority_port(authority: &str) -> &str {
1959    match authority.rsplit_once(':') {
1960        Some((h, port)) if !port.is_empty() && port.bytes().all(|b| b.is_ascii_digit()) => h,
1961        _ => authority,
1962    }
1963}
1964
1965/// RFC 6125 §6.4.3 wildcard-aware match of `:authority` against a SAN set
1966/// snapshot taken at TLS handshake.
1967///
1968/// Returns the matched SAN entry on success so the caller can log it.
1969///
1970/// Matching rules:
1971///   * Port suffix on the authority is stripped (same logic as
1972///     [`authority_matches_sni`], IPv6-bracket safe).
1973///   * Compare is ASCII case-insensitive (`:authority` is ASCII per
1974///     RFC 9113 §8.3.1; SAN entries are stored pre-lowercased by
1975///     `https.rs::upgrade_handshake`).
1976///   * `*.suffix` matches exactly one DNS label at the leftmost position
1977///     and only when that label is non-empty: it does NOT match the apex,
1978///     does NOT cross dots, and embedded wildcards (`foo.*.example.com`,
1979///     `*foo.example.com`) are forbidden.
1980///   * Empty `names` ⇒ `None` (default-cert path — Sōzu is not
1981///     authoritative for any name).
1982pub(crate) fn authority_matched_cert_name<'a>(
1983    authority: &str,
1984    names: &'a [String],
1985) -> Option<&'a str> {
1986    let mut host = strip_authority_port(authority);
1987    // RFC 1034 §3.1 absolute-form: `example.com.` and `example.com` name
1988    // the same host. The SAN snapshot already strips trailing dots at
1989    // `https.rs::upgrade_handshake`, and the SNI side strips them at the
1990    // same site; strip on the authority side so a client emitting
1991    // absolute-form `:authority` (or H1 `Host`) does not get a false 421.
1992    // Only one trailing dot is removed because RFC 1034 forbids multiple
1993    // trailing dots on a domain literal.
1994    if let Some(trimmed) = host.strip_suffix('.') {
1995        host = trimmed;
1996    }
1997    if host.is_empty() {
1998        return None;
1999    }
2000    for entry in names {
2001        if let Some(suffix) = entry.strip_prefix("*.") {
2002            // RFC 6125 §6.4.3: the wildcard label is the *entire* left-most
2003            // label. Embedded wildcards (`f*.example.com`, `*f.example.com`)
2004            // are rejected because we reach this branch only when the entry
2005            // starts with the exact two bytes `*.`. We still must reject
2006            // wildcards anywhere else in the entry by requiring no further
2007            // `*` in `suffix`.
2008            if suffix.contains('*') {
2009                continue;
2010            }
2011            // Authority has the form `<left-most-label>.<rest>`; the
2012            // wildcard substitutes for exactly that left-most label, which
2013            // must be non-empty and contain no dot.
2014            let Some((leftmost, rest)) = host.split_once('.') else {
2015                continue;
2016            };
2017            if leftmost.is_empty() {
2018                continue;
2019            }
2020            if rest.eq_ignore_ascii_case(suffix) {
2021                return Some(entry);
2022            }
2023            continue;
2024        }
2025        if entry.contains('*') {
2026            // Internal wildcards (`foo.*.example.com`) are not RFC 6125-
2027            // valid. Skip rather than mis-match.
2028            continue;
2029        }
2030        if host.eq_ignore_ascii_case(entry) {
2031            return Some(entry);
2032        }
2033    }
2034    None
2035}
2036
2037#[cfg(test)]
2038mod tests {
2039    use std::{cell::RefCell, collections::BTreeMap, rc::Rc, time::Duration};
2040
2041    use sozu_command::{
2042        config::ListenerBuilder,
2043        proto::command::{
2044            ForwardedHeaders, PathRule, RequestHttpFrontend, RulePosition, SocketAddress,
2045        },
2046    };
2047
2048    use super::{Router, RoutingView, authority_matches_sni};
2049    use crate::{
2050        L7Proxy,
2051        http::HttpProxy,
2052        protocol::{
2053            http::{editor::HttpContext, parser::Method},
2054            mux::{buffer_source::PoolBufferSource, stream::Stream},
2055        },
2056    };
2057
2058    /// A frontend declared as `*.example.com` and carrying `--tags` must put
2059    /// those tags on the access-log line of a request whose authority is
2060    /// `foo.example.com` (sozu#1379).
2061    ///
2062    /// The bug this pins: the tag cache was WRITTEN under the frontend
2063    /// RULE's hostname (`listener.set_tags(front.hostname, …)` in
2064    /// `lib/src/http.rs` / `lib/src/https.rs`) and READ back under the
2065    /// REQUEST's authority (`listener.get_tags(hostname)` in
2066    /// `lib/src/protocol/mux/stream.rs`), against a `BTreeMap<String,
2067    /// CachedTags>` that does exact-key lookup only. `*.example.com` is a
2068    /// key `foo.example.com` can never produce, so every access-log line of
2069    /// every wildcard, regex, ported or differently-cased frontend lost its
2070    /// tags silently. The fix resolves the tags through the router, which
2071    /// already knows which frontend rule matched.
2072    ///
2073    /// The test drives the production add path (`HttpProxy::add_http_frontend`),
2074    /// the production routing path (`Router::route_from_request`) and the
2075    /// production emit path (`Stream::generate_access_log`), and asserts on
2076    /// the rendered access-log line rather than on any intermediate field,
2077    /// so it stays valid whatever the resolution mechanism becomes.
2078    ///
2079    /// To SEE THIS RED: in `Router::route_from_request`
2080    /// (`lib/src/protocol/mux/router.rs`), replace the
2081    /// `context.tags = tags;` stash that follows the `RouteResult`
2082    /// destructure with `let _ = tags;`. The access log then falls back to
2083    /// the authority-keyed listener map, which holds only the
2084    /// `*.example.com` key, and the line is emitted with an empty `[]` tag
2085    /// field. Dropping `|| front.tags.is_some()` from `has_policy` in
2086    /// `Router::add_http_front_with_hsts_origin`
2087    /// (`lib/src/router/mod.rs`) reddens it the same way, one step
2088    /// earlier: the frontend is then stored as a tagless
2089    /// `Route::ClusterId` and there is nothing left for the stash to
2090    /// carry.
2091    #[test]
2092    fn a_wildcard_frontends_tags_reach_the_access_log_of_a_concrete_authority() {
2093        const TAG: &str = "owner=team-wildcard-1379";
2094
2095        let output = crate::capture_test_logs(|| {
2096            let port = crate::testing::provide_port();
2097            let address = SocketAddress::new_v4(127, 0, 0, 1, port);
2098            let config = ListenerBuilder::new_http(address)
2099                .to_http(None)
2100                .expect("test http listener config must build");
2101            let parts = crate::testing::prebuild_server(10, 16_384, false)
2102                .expect("test server parts must build");
2103            let pool = parts.pool.clone();
2104            let mut proxy =
2105                HttpProxy::new(parts.registry, parts.sessions, parts.pool, parts.backends);
2106            let token = mio::Token(0);
2107            proxy
2108                .add_listener(config, token)
2109                .expect("test listener must register");
2110            proxy
2111                .add_http_frontend(RequestHttpFrontend {
2112                    cluster_id: Some("cluster-1379".to_owned()),
2113                    address,
2114                    hostname: "*.example.com".to_owned(),
2115                    path: PathRule::prefix("/".to_owned()),
2116                    position: RulePosition::Tree.into(),
2117                    tags: BTreeMap::from([("owner".to_owned(), "team-wildcard-1379".to_owned())]),
2118                    ..Default::default()
2119                })
2120                .expect("the wildcard frontend must register");
2121            let listener = proxy
2122                .get_listener(&token)
2123                .expect("the registered listener must be reachable");
2124            let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(proxy));
2125
2126            let mut context = HttpContext::new(
2127                rusty_ulid::Ulid::generate(),
2128                rusty_ulid::Ulid::generate(),
2129                crate::Protocol::HTTP,
2130                "127.0.0.1:80"
2131                    .parse()
2132                    .expect("test public address must parse"),
2133                Some(
2134                    "127.0.0.1:12345"
2135                        .parse()
2136                        .expect("test session address must parse"),
2137                ),
2138                "SOZUBALANCEID".to_owned(),
2139                "Sozu-Id".to_owned(),
2140                false,
2141                false,
2142            );
2143            context.authority = Some("foo.example.com".to_owned());
2144            context.path = Some("/".to_owned());
2145            context.method = Some(Method::Get);
2146
2147            let mut stream = Stream::new(
2148                &mut PoolBufferSource::new(Rc::downgrade(&pool)),
2149                context,
2150                crate::protocol::mux::test_support::test_answers(),
2151                65_535,
2152            )
2153            .expect("test stream must check out its buffers");
2154            let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
2155            let (front, stream_context) = {
2156                let split = &mut stream;
2157                (&mut split.front, &mut split.context)
2158            };
2159            let proxy_ref = proxy.borrow();
2160            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
2161            router
2162                .route_from_request(stream_context, front, &listener, &view)
2163                .expect("the wildcard frontend must route foo.example.com");
2164            drop(proxy_ref);
2165
2166            // This test asserts on the access log, not on metrics.
2167            let _ = stream.generate_access_log(false, None, listener, None, None);
2168        });
2169
2170        assert!(
2171            output.contains(TAG),
2172            "the access log of a request routed by `*.example.com` must carry that frontend's tags, got: {output}"
2173        );
2174    }
2175
2176    /// `HttpContext::original_authority` records the authority the client
2177    /// sent only when the frontend rewrites the host, the one case where it
2178    /// is sent on as `X-Forwarded-Host`; a request without a host rewrite
2179    /// leaves it `None` instead of paying a copy of the authority (#1589).
2180    ///
2181    /// To SEE THIS RED: stamp `context.original_authority` with a copy of
2182    /// `host` in `Router::route_from_request` before the frontend lookup,
2183    /// as it was before #1589. The plain request then carries
2184    /// `Some("plain.example.com")`.
2185    #[test]
2186    fn only_a_host_rewrite_records_the_original_authority() {
2187        let port = crate::testing::provide_port();
2188        let address = SocketAddress::new_v4(127, 0, 0, 1, port);
2189        let config = ListenerBuilder::new_http(address)
2190            .to_http(None)
2191            .expect("test http listener config must build");
2192        let parts = crate::testing::prebuild_server(10, 16_384, false)
2193            .expect("test server parts must build");
2194        let pool = parts.pool.clone();
2195        let mut proxy = HttpProxy::new(parts.registry, parts.sessions, parts.pool, parts.backends);
2196        let token = mio::Token(0);
2197        proxy
2198            .add_listener(config, token)
2199            .expect("test listener must register");
2200        for (hostname, rewrite_host) in [
2201            ("rewrite.example.com", Some("internal.example.com")),
2202            ("plain.example.com", None),
2203        ] {
2204            proxy
2205                .add_http_frontend(RequestHttpFrontend {
2206                    cluster_id: Some("cluster-1589".to_owned()),
2207                    address,
2208                    hostname: hostname.to_owned(),
2209                    path: PathRule::prefix("/".to_owned()),
2210                    position: RulePosition::Tree.into(),
2211                    rewrite_host: rewrite_host.map(str::to_owned),
2212                    ..Default::default()
2213                })
2214                .unwrap_or_else(|error| panic!("{hostname} must register: {error}"));
2215        }
2216        let listener = proxy
2217            .get_listener(&token)
2218            .expect("the registered listener must be reachable");
2219        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(proxy));
2220        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
2221
2222        for (authority, expected) in [
2223            ("rewrite.example.com", Some("rewrite.example.com")),
2224            ("plain.example.com", None),
2225        ] {
2226            let mut context = HttpContext::new(
2227                rusty_ulid::Ulid::generate(),
2228                rusty_ulid::Ulid::generate(),
2229                crate::Protocol::HTTP,
2230                "127.0.0.1:80"
2231                    .parse()
2232                    .expect("test public address must parse"),
2233                None,
2234                "SOZUBALANCEID".to_owned(),
2235                "Sozu-Id".to_owned(),
2236                false,
2237                false,
2238            );
2239            context.authority = Some(authority.to_owned());
2240            context.path = Some("/".to_owned());
2241            context.method = Some(Method::Get);
2242            let mut stream = Stream::new(
2243                &mut PoolBufferSource::new(Rc::downgrade(&pool)),
2244                context,
2245                crate::protocol::mux::test_support::test_answers(),
2246                65_535,
2247            )
2248            .expect("test stream must check out its buffers");
2249            let split = &mut stream;
2250            let proxy_ref = proxy.borrow();
2251            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
2252            router
2253                .route_from_request(&mut split.context, &mut split.front, &listener, &view)
2254                .unwrap_or_else(|error| panic!("{authority} must route: {error}"));
2255
2256            assert_eq!(
2257                split.context.original_authority.as_deref(),
2258                expected,
2259                "{authority}: original_authority is recorded exactly when the host is rewritten",
2260            );
2261            let forwarded_host = split.front.blocks.iter().find_map(|block| match block {
2262                kawa::Block::Header(pair)
2263                    if pair.key.data(split.front.storage.buffer()) == b"X-Forwarded-Host" =>
2264                {
2265                    Some(pair.val.data(split.front.storage.buffer()).to_vec())
2266                }
2267                _ => None,
2268            });
2269            assert_eq!(
2270                forwarded_host.as_deref(),
2271                expected.map(str::as_bytes),
2272                "{authority}: X-Forwarded-Host carries the authority the client sent, on a rewrite only",
2273            );
2274        }
2275    }
2276
2277    /// On a host rewrite, `X-Forwarded-Host` follows the listener's
2278    /// `forwarded_headers` mode: `both` and `x_forwarded` replace a client
2279    /// one with the authority the client sent, `rfc7239` and `none` inject
2280    /// none, and `none` leaves a client one as sent. (`rfc7239` elided the
2281    /// client one earlier, in `HttpContext::on_request_headers`.)
2282    ///
2283    /// To SEE THIS RED: in `apply_request_rewrites_and_headers`, set
2284    /// `synthesises_xfh` to `rewriting_host` alone. `rfc7239` and `none` then
2285    /// carry `X-Forwarded-Host: rewrite.example.com`.
2286    #[test]
2287    fn a_host_rewrite_discloses_x_forwarded_host_only_in_x_forwarded_modes() {
2288        let port = crate::testing::provide_port();
2289        let address = SocketAddress::new_v4(127, 0, 0, 1, port);
2290        let config = ListenerBuilder::new_http(address)
2291            .to_http(None)
2292            .expect("test http listener config must build");
2293        let parts = crate::testing::prebuild_server(10, 16_384, false)
2294            .expect("test server parts must build");
2295        let pool = parts.pool.clone();
2296        let mut proxy = HttpProxy::new(parts.registry, parts.sessions, parts.pool, parts.backends);
2297        let token = mio::Token(0);
2298        proxy
2299            .add_listener(config, token)
2300            .expect("test listener must register");
2301        proxy
2302            .add_http_frontend(RequestHttpFrontend {
2303                cluster_id: Some("cluster-322".to_owned()),
2304                address,
2305                hostname: "rewrite.example.com".to_owned(),
2306                path: PathRule::prefix("/".to_owned()),
2307                position: RulePosition::Tree.into(),
2308                rewrite_host: Some("internal.example.com".to_owned()),
2309                ..Default::default()
2310            })
2311            .expect("the rewriting frontend must register");
2312        let listener = proxy
2313            .get_listener(&token)
2314            .expect("the registered listener must be reachable");
2315        let proxy: Rc<RefCell<dyn L7Proxy>> = Rc::new(RefCell::new(proxy));
2316        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
2317
2318        for (mode, client_xfh, expected) in [
2319            (
2320                ForwardedHeaders::Both,
2321                true,
2322                vec![b"rewrite.example.com".to_vec()],
2323            ),
2324            (
2325                ForwardedHeaders::XForwarded,
2326                true,
2327                vec![b"rewrite.example.com".to_vec()],
2328            ),
2329            (ForwardedHeaders::Rfc7239, false, vec![]),
2330            (
2331                ForwardedHeaders::None,
2332                true,
2333                vec![b"client.example".to_vec()],
2334            ),
2335        ] {
2336            let mut context = HttpContext::new(
2337                rusty_ulid::Ulid::generate(),
2338                rusty_ulid::Ulid::generate(),
2339                crate::Protocol::HTTP,
2340                "127.0.0.1:80"
2341                    .parse()
2342                    .expect("test public address must parse"),
2343                None,
2344                "SOZUBALANCEID".to_owned(),
2345                "Sozu-Id".to_owned(),
2346                false,
2347                false,
2348            );
2349            context.forwarded_headers = mode;
2350            context.authority = Some("rewrite.example.com".to_owned());
2351            context.path = Some("/".to_owned());
2352            context.method = Some(Method::Get);
2353            let mut stream = Stream::new(
2354                &mut PoolBufferSource::new(Rc::downgrade(&pool)),
2355                context,
2356                crate::protocol::mux::test_support::test_answers(),
2357                65_535,
2358            )
2359            .expect("test stream must check out its buffers");
2360            if client_xfh {
2361                stream.front.push_block(kawa::Block::Header(kawa::Pair {
2362                    key: kawa::Store::Static(b"X-Forwarded-Host"),
2363                    val: kawa::Store::Static(b"client.example"),
2364                }));
2365            }
2366            let split = &mut stream;
2367            let proxy_ref = proxy.borrow();
2368            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
2369            router
2370                .route_from_request(&mut split.context, &mut split.front, &listener, &view)
2371                .unwrap_or_else(|error| panic!("{mode:?} must route: {error}"));
2372
2373            let buffer = split.front.storage.buffer();
2374            let forwarded_hosts: Vec<Vec<u8>> = split
2375                .front
2376                .blocks
2377                .iter()
2378                .filter_map(|block| match block {
2379                    kawa::Block::Header(pair)
2380                        if !pair.is_elided()
2381                            && pair
2382                                .key
2383                                .data(buffer)
2384                                .eq_ignore_ascii_case(b"X-Forwarded-Host") =>
2385                    {
2386                        Some(pair.val.data(buffer).to_vec())
2387                    }
2388                    _ => None,
2389                })
2390                .collect();
2391            assert_eq!(
2392                forwarded_hosts, expected,
2393                "{mode:?}: X-Forwarded-Host after a host rewrite"
2394            );
2395        }
2396    }
2397
2398    // Asserts on `debug!` output: gate on the same condition that compiles it in.
2399    #[cfg(any(debug_assertions, feature = "logs-debug", feature = "logs-trace"))]
2400    #[test]
2401    fn routing_runtime_logs_bound_method_authority_sni_and_certificate_names() {
2402        use super::{log_coalescing_accepted, log_sni_authority_mismatch};
2403
2404        const METHOD_SECRET: &str = "MUX_ROUTE_METHOD_SECRET_SENTINEL";
2405        const AUTHORITY_SECRET: &str = "MUX_ROUTE_AUTHORITY_SECRET_SENTINEL";
2406        const SNI_SECRET: &str = "MUX_ROUTE_SNI_SECRET_SENTINEL";
2407        const SAN_SECRET: &str = "MUX_ROUTE_SAN_SECRET_SENTINEL";
2408
2409        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
2410        let method = long_value(METHOD_SECRET);
2411        let authority = long_value(AUTHORITY_SECRET);
2412        let sni = long_value(SNI_SECRET);
2413        let san = long_value(SAN_SECRET);
2414        let method_len = method.len();
2415        let authority_len = authority.len();
2416        let sni_len = sni.len();
2417        let san_len = san.len();
2418
2419        let output = crate::capture_test_logs_at_level("debug", move || {
2420            let mut context = HttpContext::new(
2421                rusty_ulid::Ulid::generate(),
2422                rusty_ulid::Ulid::generate(),
2423                crate::Protocol::HTTPS,
2424                "127.0.0.1:443"
2425                    .parse()
2426                    .expect("test public address must parse"),
2427                Some(
2428                    "127.0.0.1:12345"
2429                        .parse()
2430                        .expect("test session address must parse"),
2431                ),
2432                String::new(),
2433                String::new(),
2434                false,
2435                false,
2436            );
2437            context.method = Some(crate::protocol::http::parser::Method::Custom(method));
2438            context.authority = Some(authority.clone());
2439            context.tls_cert_names = Some(std::sync::Arc::new(vec![san.clone()]));
2440
2441            log_coalescing_accepted(&context, &authority, &sni, &san);
2442            log_sni_authority_mismatch(
2443                &context,
2444                &authority,
2445                &sni,
2446                context.tls_cert_names.as_deref().map(Vec::as_slice),
2447            );
2448        });
2449
2450        for secret in [METHOD_SECRET, AUTHORITY_SECRET, SNI_SECRET, SAN_SECRET] {
2451            assert!(
2452                !output.contains(secret),
2453                "mux routing runtime log leaked {secret}: {output}"
2454            );
2455        }
2456        for metadata in [
2457            format!("bytes={method_len}"),
2458            format!("authority_bytes=Some({authority_len})"),
2459            format!("authority_bytes={authority_len}"),
2460            format!("sni_bytes={sni_len}"),
2461            format!("matched_san_bytes={san_len}"),
2462            "certificate_sans_count=1".to_owned(),
2463            format!("certificate_sans_bytes={san_len}"),
2464        ] {
2465            assert!(
2466                output.contains(&metadata),
2467                "mux routing runtime log omitted {metadata}: {output}"
2468            );
2469        }
2470        assert!(
2471            output.len() <= 2048,
2472            "mux routing runtime capture is not bounded: {} bytes",
2473            output.len()
2474        );
2475    }
2476
2477    #[test]
2478    fn match_exact() {
2479        assert!(authority_matches_sni("example.com", "example.com"));
2480    }
2481
2482    #[test]
2483    fn match_different_case() {
2484        assert!(authority_matches_sni("Example.COM", "example.com"));
2485    }
2486
2487    #[test]
2488    fn match_authority_with_port() {
2489        assert!(authority_matches_sni("example.com:8443", "example.com"));
2490    }
2491
2492    #[test]
2493    fn reject_different_host() {
2494        assert!(!authority_matches_sni(
2495            "tenant-b.example.com",
2496            "tenant-a.example.com"
2497        ));
2498    }
2499
2500    #[test]
2501    fn reject_substring_attack() {
2502        // Length check guards against an authority that is a prefix or
2503        // suffix of the SNI (or vice versa).
2504        assert!(!authority_matches_sni("example.co", "example.com"));
2505        assert!(!authority_matches_sni("example.commons", "example.com"));
2506    }
2507
2508    #[test]
2509    fn reject_wildcard_not_expanded() {
2510        // Wildcard cert selection happens at the cert-resolver layer; the SNI
2511        // we see here is the concrete name the client sent. Do not silently
2512        // accept `*.example.com` as matching `foo.example.com`.
2513        assert!(!authority_matches_sni("foo.example.com", "*.example.com"));
2514    }
2515
2516    #[test]
2517    fn ipv6_bracketed_literal_with_port() {
2518        // `[::1]:8443` must still match the SNI `[::1]`; only the trailing
2519        // `:8443` is a port (all digits → stripped).
2520        assert!(authority_matches_sni("[::1]:8443", "[::1]"));
2521    }
2522
2523    #[test]
2524    fn ipv6_bracketed_without_port() {
2525        // The `:` characters inside the brackets must not be mistaken for a
2526        // port separator: the tail after the last `:` is `1]`, not all
2527        // digits, so it is NOT stripped and the whole string compares.
2528        assert!(authority_matches_sni("[::1]", "[::1]"));
2529    }
2530}
2531
2532#[cfg(test)]
2533mod authority_matched_cert_name_tests {
2534    use super::authority_matched_cert_name;
2535
2536    #[test]
2537    fn cert_name_match_exact_single_san() {
2538        let names = vec!["example.com".to_owned()];
2539        assert_eq!(
2540            authority_matched_cert_name("example.com", &names),
2541            Some("example.com"),
2542        );
2543    }
2544
2545    #[test]
2546    fn cert_name_match_wildcard_left_most() {
2547        let names = vec!["*.cleverapps.io".to_owned()];
2548        assert_eq!(
2549            authority_matched_cert_name("staging-3.cleverapps.io", &names),
2550            Some("*.cleverapps.io"),
2551        );
2552    }
2553
2554    #[test]
2555    fn cert_name_reject_wildcard_apex() {
2556        // RFC 6125 §6.4.3: `*.example.com` does NOT cover the apex
2557        // `example.com` — the wildcard label must consume exactly one
2558        // non-empty label.
2559        let names = vec!["*.example.com".to_owned()];
2560        assert_eq!(authority_matched_cert_name("example.com", &names), None);
2561    }
2562
2563    #[test]
2564    fn cert_name_reject_wildcard_two_labels() {
2565        // `*.example.com` cannot cross dots: `a.b.example.com` has two
2566        // labels before `example.com` and must be rejected.
2567        let names = vec!["*.example.com".to_owned()];
2568        assert_eq!(authority_matched_cert_name("a.b.example.com", &names), None,);
2569    }
2570
2571    #[test]
2572    fn cert_name_reject_wildcard_not_left_most() {
2573        // Embedded wildcards (`foo.*.example.com`) are not RFC 6125-valid
2574        // and must be skipped, not mis-matched.
2575        let names = vec!["foo.*.example.com".to_owned()];
2576        assert_eq!(
2577            authority_matched_cert_name("foo.bar.example.com", &names),
2578            None,
2579        );
2580    }
2581
2582    #[test]
2583    fn cert_name_match_case_insensitive() {
2584        // ASCII case folding only — `:authority` is ASCII per RFC 9113
2585        // §8.3.1 and the snapshot is pre-lowercased at handshake.
2586        let names = vec!["EXAMPLE.com".to_owned()];
2587        assert!(authority_matched_cert_name("Example.COM", &names).is_some());
2588    }
2589
2590    #[test]
2591    fn cert_name_match_with_port() {
2592        // The port suffix on `:authority` must be stripped before the
2593        // SAN compare.
2594        let names = vec!["example.com".to_owned()];
2595        assert!(authority_matched_cert_name("example.com:8443", &names).is_some());
2596    }
2597
2598    #[test]
2599    fn cert_name_match_absolute_form_trailing_dot() {
2600        // RFC 1034 §3.1: an absolute-form domain literal carries one
2601        // trailing dot (`example.com.`) and resolves to the same host as
2602        // the relative form. The SAN snapshot stores the relative form
2603        // (https.rs strips the trailing dot at handshake), so the matcher
2604        // must strip it on the authority side too — otherwise a client
2605        // emitting an absolute-form `:authority` gets a false 421.
2606        let names = vec!["example.com".to_owned()];
2607        assert!(authority_matched_cert_name("example.com.", &names).is_some());
2608        // And with both port and trailing dot.
2609        assert!(authority_matched_cert_name("example.com.:8443", &names).is_some());
2610        // The wildcard branch must also accept the absolute form.
2611        let wildcard = vec!["*.example.com".to_owned()];
2612        assert!(authority_matched_cert_name("foo.example.com.", &wildcard).is_some());
2613    }
2614
2615    #[test]
2616    fn cert_name_match_idn_a_label() {
2617        // IDNA A-labels (xn--…) are ASCII and compare byte-for-byte once
2618        // the snapshot is lowercased.
2619        let names = vec!["xn--bcher-kva.example.com".to_owned()];
2620        assert!(authority_matched_cert_name("xn--bcher-kva.example.com", &names).is_some());
2621    }
2622
2623    #[test]
2624    fn cert_name_reject_empty_names() {
2625        // Empty snapshot = default cert served = Sōzu is not
2626        // authoritative for any name; every authority must miss.
2627        assert_eq!(authority_matched_cert_name("example.com", &[]), None);
2628    }
2629
2630    #[test]
2631    fn cert_name_match_multi_san_one_hit() {
2632        let names = vec!["foo.com".to_owned(), "*.example.org".to_owned()];
2633        assert_eq!(
2634            authority_matched_cert_name("bar.example.org", &names),
2635            Some("*.example.org"),
2636        );
2637    }
2638
2639    #[test]
2640    fn cert_name_reject_substring_attack() {
2641        // `*.example.com` must not match `example.commons` — the suffix
2642        // after the first label is `commons`, not `example.com`.
2643        let names = vec!["*.example.com".to_owned()];
2644        assert_eq!(authority_matched_cert_name("example.commons", &names), None,);
2645    }
2646
2647    #[test]
2648    fn cert_name_ipv6_bracketed_literal_with_port() {
2649        // The `:` characters inside the brackets must not be mistaken for
2650        // a port separator: only the trailing `:8443` is stripped, and
2651        // `[::1]` compares equal to `[::1]`.
2652        let names = vec!["[::1]".to_owned()];
2653        assert!(authority_matched_cert_name("[::1]:8443", &names).is_some());
2654    }
2655}
2656
2657/// Backend-selection order: `Router::plan_connect` must resolve a tie the same way
2658/// on every process, for the identical set of backend connections and the
2659/// identical request (issue #1338).
2660///
2661/// **Why these assert a SPECIFIC token rather than "the same one twice".**
2662/// `RandomState` is seeded per `HashMap`, not per iteration: two walks of one
2663/// live `HashMap` agree, so a test that runs the selection twice inside one
2664/// process and compares passes against a `HashMap` by construction. What does
2665/// NOT agree is two SEPARATELY CONSTRUCTED maps — `RandomState::default()`
2666/// bumps a thread-local key on every instantiation — so each round below
2667/// builds a fresh [`Router`], and therefore a fresh hasher, and asserts the
2668/// token the total order on `Token` picks.
2669///
2670/// That makes the red statistical rather than structural, and the bound is
2671/// stated so it can be checked rather than trusted: with `n` staged backends
2672/// and `r` rounds a `HashMap` survives with probability about `n.pow(-r)`,
2673/// which is `4^-24` here — about one run in 2.8e14. A `BTreeMap` survives
2674/// with probability 1. Measured on the pre-image, with the three production
2675/// lines reverted and these guards untouched: three consecutive runs failed
2676/// all three at round 0 or round 1. The least-loaded guard was handed
2677/// `Token(23)` and `Token(13)` where the total order gives `Token(7)`, and
2678/// the last-wins fallback `Token(7)` where it gives `Token(41)`.
2679///
2680/// **To SEE THESE RED:** in this module, put `Router::backends` back to
2681/// `HashMap<Token, Connection<SessionTcpStream>>`, `Router::new`'s initialiser
2682/// back to `HashMap::new()` and the import back to `collections::HashMap`
2683/// (the `BackendConnections` type then has no user left; delete it).
2684/// Reverting the file wholesale deletes these tests instead of reddening them.
2685#[cfg(test)]
2686mod backend_selection_order_tests {
2687    use std::{cell::RefCell, collections::HashMap, net::SocketAddr, rc::Rc, time::Duration};
2688
2689    use mio::Token;
2690    use rusty_ulid::Ulid;
2691    use sozu_command::{
2692        config::ListenerBuilder,
2693        proto::command::{
2694            Cluster, ListenerType, PathRule, RequestHttpFrontend, RulePosition, SocketAddress,
2695        },
2696    };
2697
2698    use super::Router;
2699    use crate::{
2700        BackendConnectionError, L7Proxy,
2701        backends::Backend,
2702        http::{HttpListener, HttpProxy},
2703        pool::Pool,
2704        protocol::{
2705            http::parser::Method,
2706            mux::{
2707                BackendId, BackendRegistry, BackendStatus, Connection, Context, Position,
2708                StreamState,
2709                buffer_source::PoolBufferSource,
2710                h2::H2ConnectionConfig,
2711                h2_flood_detector::H2FloodConfig,
2712                router::{ConnectPlan, ConnectStep, IpGateVerdict, RoutingView},
2713            },
2714        },
2715        socket::SessionTcpStream,
2716    };
2717
2718    /// Unwrap a step these fixtures can only produce one way.
2719    ///
2720    /// Every `Context` here is built with `session_address: None`, so the
2721    /// per-(cluster, source-IP) gate is skipped and `Router::plan_connect`
2722    /// must decide outright. A `CheckIpLimit` would mean the gate fired
2723    /// without a source IP, which is its own bug.
2724    fn decided(step: ConnectStep) -> ConnectPlan {
2725        match step {
2726            ConnectStep::Decided(plan) => plan,
2727            ConnectStep::CheckIpLimit(_) => panic!(
2728                "a stream with no session address must not reach the \
2729                 per-(cluster, source-IP) gate"
2730            ),
2731        }
2732    }
2733
2734    /// Staged deliberately out of order and non-contiguous, so a green run
2735    /// cannot be an artefact of insertion order or of a dense key space.
2736    const STAGED_TOKENS: [usize; 4] = [23, 7, 41, 13];
2737    const LOWEST_TOKEN: usize = 7;
2738    const HIGHEST_TOKEN: usize = 41;
2739    /// See the module-level note for the `n.pow(-r)` bound this feeds.
2740    const ROUNDS: usize = 24;
2741
2742    const H2_CLUSTER: &str = "cluster-1338-h2";
2743    const H1_CLUSTER: &str = "cluster-1338-h1";
2744    const H2_AUTHORITY: &str = "h2.backend-order.example.com";
2745    const H1_AUTHORITY: &str = "h1.backend-order.example.com";
2746
2747    /// Which arm of `Router::plan_connect`'s scan the staged backends land in.
2748    enum Staged {
2749        /// `Position::Client(_, _, BackendStatus::Connected)` on an H2
2750        /// connection — the least-loaded arm, strict `<`, first-at-minimum.
2751        ConnectedH2,
2752        /// `BackendStatus::Connecting` on an H2 connection — the fallback
2753        /// arm, assigned with no `break` and no "already chosen" guard.
2754        ConnectingH2,
2755        /// `BackendStatus::KeepAlive` on an H1 connection — assigned with a
2756        /// `break`, so first-seen.
2757        KeepAliveH1,
2758    }
2759
2760    impl Staged {
2761        fn cluster(&self) -> &'static str {
2762            match self {
2763                Staged::ConnectedH2 | Staged::ConnectingH2 => H2_CLUSTER,
2764                Staged::KeepAliveH1 => H1_CLUSTER,
2765            }
2766        }
2767
2768        fn authority(&self) -> &'static str {
2769            match self {
2770                Staged::ConnectedH2 | Staged::ConnectingH2 => H2_AUTHORITY,
2771                Staged::KeepAliveH1 => H1_AUTHORITY,
2772            }
2773        }
2774    }
2775
2776    /// The proxy, listener, clusters, frontends and buffer pool
2777    /// `Router::plan_connect` needs. Built once and shared by every round:
2778    /// the order under test belongs to `Router::backends`, which each round
2779    /// rebuilds from scratch.
2780    struct RoutingFixture {
2781        proxy: Rc<RefCell<dyn L7Proxy>>,
2782        /// The map `proxy` was built over, which a `Mux` receives at
2783        /// construction.
2784        backends: Rc<RefCell<crate::backends::BackendMap>>,
2785        listener: Rc<RefCell<HttpListener>>,
2786        pool: Rc<RefCell<Pool>>,
2787    }
2788
2789    fn routing_fixture() -> RoutingFixture {
2790        let port = crate::testing::provide_port();
2791        let address = SocketAddress::new_v4(127, 0, 0, 1, port);
2792        let config = ListenerBuilder::new_http(address)
2793            .to_http(None)
2794            .expect("test http listener config must build");
2795        let parts =
2796            crate::testing::prebuild_server(32, 65_536, false).expect("test server must build");
2797        let pool = parts.pool.clone();
2798        let backends = Rc::clone(&parts.backends);
2799        let mut proxy = HttpProxy::new(parts.registry, parts.sessions, parts.pool, parts.backends);
2800        let listener_token = Token(0);
2801        proxy
2802            .add_listener(config, listener_token)
2803            .expect("test listener must register");
2804        for (cluster_id, authority, http2) in [
2805            (H2_CLUSTER, H2_AUTHORITY, Some(true)),
2806            (H1_CLUSTER, H1_AUTHORITY, None),
2807        ] {
2808            proxy
2809                .add_cluster(Cluster {
2810                    cluster_id: cluster_id.to_owned(),
2811                    http2,
2812                    ..Default::default()
2813                })
2814                .expect("the test cluster must register");
2815            proxy
2816                .add_http_frontend(RequestHttpFrontend {
2817                    cluster_id: Some(cluster_id.to_owned()),
2818                    address,
2819                    hostname: authority.to_owned(),
2820                    path: PathRule::prefix("/".to_owned()),
2821                    position: RulePosition::Tree.into(),
2822                    ..Default::default()
2823                })
2824                .expect("the test frontend must register");
2825        }
2826        let listener = proxy
2827            .get_listener(&listener_token)
2828            .expect("the registered listener must be reachable");
2829        RoutingFixture {
2830            proxy: Rc::new(RefCell::new(proxy)),
2831            backends,
2832            listener,
2833            pool,
2834        }
2835    }
2836
2837    /// One backend connection in the requested arm, plus the accepted peer the
2838    /// caller must keep alive for the length of the round.
2839    fn staged_backend(
2840        pool: &Rc<RefCell<Pool>>,
2841        staged: &Staged,
2842        backend_registry: &mut BackendRegistry,
2843    ) -> (Connection<SessionTcpStream>, std::net::TcpStream) {
2844        let (socket, peer) = super::super::test_support::connected_socket();
2845        let backend_address: SocketAddr =
2846            "127.0.0.1:2".parse().expect("backend address must parse");
2847        let backend = Rc::new(RefCell::new(Backend::new(
2848            "test-backend",
2849            backend_address,
2850            None,
2851            None,
2852            None,
2853        )));
2854        // Through the embedder's table, exactly as a real dial does: the
2855        // handle stops here and the connection carries the opaque id.
2856        let backend = backend_registry.id_for(&backend);
2857        let session_ulid = Ulid::generate();
2858        let socket = SessionTcpStream::new(socket, session_ulid, Some(backend_address));
2859        let mut connection = match staged {
2860            Staged::ConnectedH2 | Staged::ConnectingH2 => Connection::new_h2_client(
2861                session_ulid,
2862                socket,
2863                staged.cluster().into(),
2864                backend,
2865                &mut PoolBufferSource::new(Rc::downgrade(pool)),
2866                Duration::from_secs(30),
2867                H2FloodConfig::default(),
2868                H2ConnectionConfig::default(),
2869                Duration::from_secs(30),
2870                None,
2871            )
2872            .expect("the test pool must hand out a buffer for the H2 backend"),
2873            Staged::KeepAliveH1 => Connection::new_h1_client(
2874                session_ulid,
2875                socket,
2876                staged.cluster().into(),
2877                backend,
2878                Duration::from_secs(30),
2879            ),
2880        };
2881        // `new_h*_client` opens in `Connecting`; the other two arms are
2882        // reached by moving the status the way a completed dial does.
2883        if let Position::Client(_, _, status) = connection.position_mut() {
2884            match staged {
2885                Staged::ConnectedH2 => *status = BackendStatus::Connected,
2886                Staged::ConnectingH2 => {}
2887                Staged::KeepAliveH1 => *status = BackendStatus::KeepAlive,
2888            }
2889        }
2890        (connection, peer)
2891    }
2892
2893    /// Stage [`STAGED_TOKENS`] into a FRESH [`Router`] — a fresh `RandomState`
2894    /// in the pre-image — drive one `Router::plan_connect`, and report the token it
2895    /// attached the stream to.
2896    fn selected_backend_token(fixture: &RoutingFixture, staged: &Staged) -> Token {
2897        let pool = &fixture.pool;
2898        let mut context = Context::new(
2899            Ulid::generate(),
2900            Rc::downgrade(pool),
2901            fixture.listener.clone(),
2902            // No session address: the per-(cluster, source-IP) gate is
2903            // skipped, so this never reaches the proxy's session manager.
2904            None,
2905            "127.0.0.1:80"
2906                .parse()
2907                .expect("test public address must parse"),
2908        );
2909        let stream_id = context
2910            .create_stream(Ulid::generate(), 65_535)
2911            .expect("the test pool must hand out a stream");
2912        {
2913            let stream = &mut context.streams[stream_id];
2914            stream.state = StreamState::Link;
2915            stream.context.authority = Some(staged.authority().to_owned());
2916            stream.context.path = Some("/".to_owned());
2917            stream.context.method = Some(Method::Get);
2918        }
2919
2920        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
2921        let mut backend_registry = BackendRegistry::default();
2922        let mut peers = Vec::with_capacity(STAGED_TOKENS.len());
2923        for token in STAGED_TOKENS {
2924            let (connection, peer) = staged_backend(pool, staged, &mut backend_registry);
2925            peers.push(peer);
2926            router.backends.insert(Token(token), connection);
2927        }
2928
2929        // Every staged backend is reusable, so the plan must be `Attached`:
2930        // the router completed the attach itself and asked the embedder for
2931        // nothing. A `Dial` here would mean the reuse scan missed, which is
2932        // the failure this harness exists to catch.
2933        let proxy_ref = fixture.proxy.borrow();
2934        let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
2935        let plan = router
2936            .plan_connect(stream_id, &mut context, &view)
2937            .map(decided)
2938            .expect("the router must reuse one of the staged backends");
2939        assert!(
2940            matches!(plan, ConnectPlan::Attached),
2941            "the staged backends are all reusable, so no dial may be requested: got {plan:?}"
2942        );
2943
2944        match context.streams[stream_id].state {
2945            StreamState::Linked(token) => token,
2946            other => panic!("connect must link the stream to a backend, got {other:?}"),
2947        }
2948    }
2949
2950    /// Drive one `Router::plan_connect` against a single staged connection of
2951    /// the `staged` arm, whose backend is retired when `retired` is set, and
2952    /// return the plan.
2953    fn plan_against_one_staged(
2954        fixture: &RoutingFixture,
2955        staged: &Staged,
2956        retired: bool,
2957    ) -> (ConnectPlan, StreamState) {
2958        let mut context = Context::new(
2959            Ulid::generate(),
2960            Rc::downgrade(&fixture.pool),
2961            fixture.listener.clone(),
2962            None,
2963            "127.0.0.1:80"
2964                .parse()
2965                .expect("test public address must parse"),
2966        );
2967        let stream_id = context
2968            .create_stream(Ulid::generate(), 65_535)
2969            .expect("the test pool must hand out a stream");
2970        {
2971            let stream = &mut context.streams[stream_id];
2972            stream.state = StreamState::Link;
2973            stream.context.authority = Some(staged.authority().to_owned());
2974            stream.context.path = Some("/".to_owned());
2975            stream.context.method = Some(Method::Get);
2976        }
2977        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
2978        let mut backend_registry = BackendRegistry::default();
2979        let (connection, _peer) = staged_backend(&fixture.pool, staged, &mut backend_registry);
2980        if retired {
2981            // What `BackendList::remove_backend` and `BackendMap::remove_cluster`
2982            // do to the handle the session's registry still holds.
2983            let Position::Client(_, backend, _) = connection.position() else {
2984                unreachable!("a staged backend is a client connection")
2985            };
2986            backend_registry
2987                .handle(backend)
2988                .expect("the staged backend was interned")
2989                .borrow_mut()
2990                .set_closing();
2991        }
2992        router.backends.insert(Token(LOWEST_TOKEN), connection);
2993        let is_retired = |backend: &BackendId| backend_registry.is_retired(backend);
2994        let proxy_ref = fixture.proxy.borrow();
2995        let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind())
2996            .with_backend_retired(&is_retired);
2997        let plan = router
2998            .plan_connect(stream_id, &mut context, &view)
2999            .map(decided)
3000            .expect("routing must resolve the staged cluster");
3001        (plan, context.streams[stream_id].state)
3002    }
3003
3004    /// The pool-reuse scan skips a connection whose backend has left the
3005    /// configuration, in each of its three reusable arms: H2 `Connected`, H2
3006    /// `Connecting` and H1 `KeepAlive`. The same connection with a live
3007    /// backend is reused, so a `Dial` here is the skip and not a scan that
3008    /// would have missed anyway.
3009    #[test]
3010    fn a_retired_backend_connection_is_never_reused() {
3011        let fixture = routing_fixture();
3012        for (staged, arm) in [
3013            (Staged::ConnectedH2, "H2 Connected"),
3014            (Staged::ConnectingH2, "H2 Connecting"),
3015            (Staged::KeepAliveH1, "H1 KeepAlive"),
3016        ] {
3017            let (plan, state) = plan_against_one_staged(&fixture, &staged, false);
3018            assert!(
3019                matches!(plan, ConnectPlan::Attached),
3020                "premise, {arm}: a connection to a live backend is reused, got {plan:?}"
3021            );
3022            assert_eq!(state, StreamState::Linked(Token(LOWEST_TOKEN)));
3023
3024            let (plan, state) = plan_against_one_staged(&fixture, &staged, true);
3025            assert!(
3026                matches!(&plan, ConnectPlan::Dial { cluster_id, .. } if &**cluster_id == staged.cluster()),
3027                "{arm}: a connection to a retired backend must not be reused, got {plan:?}"
3028            );
3029            assert_eq!(
3030                state,
3031                StreamState::Link,
3032                "{arm}: the stream must not be linked to the retired connection"
3033            );
3034        }
3035    }
3036
3037    /// `Mux::ready_inner` closes a retired connection only once it is idle:
3038    /// an H1 keep-alive, or a connected H2 connection with no stream. A dial
3039    /// still in progress is left to its own connect path.
3040    #[test]
3041    fn idle_pooled_backend_names_only_streamless_pooled_connections() {
3042        let fixture = routing_fixture();
3043        for (staged, arm, idle) in [
3044            (Staged::ConnectedH2, "H2 Connected", true),
3045            (Staged::ConnectingH2, "H2 Connecting", false),
3046            (Staged::KeepAliveH1, "H1 KeepAlive", true),
3047        ] {
3048            let mut backend_registry = BackendRegistry::default();
3049            let (connection, _peer) = staged_backend(&fixture.pool, &staged, &mut backend_registry);
3050            assert_eq!(
3051                connection.idle_pooled_backend().is_some(),
3052                idle,
3053                "{arm}: idle_pooled_backend must answer {idle}"
3054            );
3055        }
3056    }
3057
3058    /// An admitted stream must have been **counted** before the decision
3059    /// resumes — the ordering the gate-as-a-step split has to preserve.
3060    ///
3061    /// `SessionManager::track_cluster_ip` mutates
3062    /// `connections_per_cluster_ip`, which is keyed on `(cluster, ip)`
3063    /// **across every session on the worker** and read again by
3064    /// `lib/src/tcp.rs`'s own gate. So an `IpGateVerdict::Admitted` is an
3065    /// assertion that the slot has already been taken. Resume as `Admitted`
3066    /// without having tracked and the stream goes through uncounted, and the
3067    /// next session from the same IP finds a slot that should be gone.
3068    ///
3069    /// The test drives the real production sequence — `Router::plan_connect`
3070    /// → `super::consult_ip_gate` → `Router::plan_connect_resume` — for two
3071    /// DIFFERENT frontend tokens from the SAME source IP, against a limit of
3072    /// one. Two tokens, not one: the gate is deliberately idempotent within a
3073    /// token (an H2 session multiplexing many streams to one cluster holds a
3074    /// single slot), so a second stream on the same token could never trip
3075    /// the limit and would prove nothing.
3076    ///
3077    /// To SEE THIS RED: delete the `track_cluster_ip` call from
3078    /// `super::consult_ip_gate`, leaving it to return
3079    /// `IpGateVerdict::Admitted` without counting. The second token is then
3080    /// admitted too and the `AtLimit` assertion below fires.
3081    #[test]
3082    fn an_admitted_stream_is_counted_before_the_decision_resumes() {
3083        let fixture = routing_fixture();
3084        let sessions = fixture.proxy.borrow().sessions();
3085        let source: SocketAddr = "203.0.113.7:51000"
3086            .parse()
3087            .expect("test source address must parse");
3088
3089        // One connection per (cluster, ip).
3090        let mut clusters = HashMap::new();
3091        clusters.insert(
3092            H2_CLUSTER.into(),
3093            Cluster {
3094                cluster_id: H2_CLUSTER.to_owned(),
3095                max_connections_per_ip: Some(1),
3096                ..Default::default()
3097            },
3098        );
3099        let view = RoutingView::new(&clusters, ListenerType::Http);
3100
3101        // Drive one stream all the way through the three-step sequence and
3102        // report what the gate said and what the decision became.
3103        let run = |token: Token| {
3104            let mut context = Context::new(
3105                Ulid::generate(),
3106                Rc::downgrade(&fixture.pool),
3107                fixture.listener.clone(),
3108                Some(source),
3109                "127.0.0.1:80"
3110                    .parse()
3111                    .expect("test public address must parse"),
3112            );
3113            let stream_id = context
3114                .create_stream(Ulid::generate(), 65_535)
3115                .expect("the test pool must hand out a stream");
3116            {
3117                let stream = &mut context.streams[stream_id];
3118                stream.state = StreamState::Link;
3119                stream.context.authority = Some(H2_AUTHORITY.to_owned());
3120                stream.context.path = Some("/".to_owned());
3121                stream.context.method = Some(Method::Get);
3122            }
3123            let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3124            let step = router
3125                .plan_connect(stream_id, &mut context, &view)
3126                .expect("routing must resolve the staged cluster");
3127            let ConnectStep::CheckIpLimit(resume) = step else {
3128                panic!("a stream WITH a session address must reach the per-IP gate")
3129            };
3130            let verdict = super::super::consult_ip_gate(
3131                &sessions,
3132                token,
3133                context
3134                    .http_context(stream_id)
3135                    .cluster_id
3136                    .as_ref()
3137                    .expect("plan_connect stamps the routed cluster"),
3138                &resume,
3139            );
3140            let admitted = matches!(verdict, IpGateVerdict::Admitted);
3141            let outcome =
3142                router.plan_connect_resume(stream_id, &mut context, &view, resume, verdict);
3143            let retry_after = context.streams[stream_id].context.retry_after_seconds;
3144            (admitted, outcome, retry_after)
3145        };
3146
3147        let (admitted, outcome, _) = run(Token(1));
3148        assert!(
3149            admitted,
3150            "premise: the first token must be under the limit, or the second proves nothing"
3151        );
3152        assert!(
3153            matches!(outcome, Ok(ConnectPlan::Dial { .. })),
3154            "an admitted stream must go on to request a dial, got {outcome:?}"
3155        );
3156
3157        let (admitted, outcome, retry_after) = run(Token(2));
3158        assert!(
3159            !admitted,
3160            "a SECOND token from the same source IP must find the slot already counted — \
3161             an Admitted verdict here means the first stream resumed without being tracked"
3162        );
3163        assert!(
3164            matches!(
3165                outcome,
3166                Err(BackendConnectionError::TooManyConnectionsPerIp { .. })
3167            ),
3168            "the refused stream must surface TooManyConnectionsPerIp, got {outcome:?}"
3169        );
3170        assert_eq!(
3171            retry_after, None,
3172            "with no cluster or global override the resolved Retry-After is 0, which is \
3173             stashed as None so the 429 mapping elides the header rather than sending 0"
3174        );
3175    }
3176
3177    /// A source holding a whole subnet defeats the per-(cluster, source-IP)
3178    /// cap outright: it takes a fresh address for every connection, so the
3179    /// per-address counter never reaches 2. The per-(cluster,
3180    /// source-SUBNET) cap is the answer, and this pins that it fires on
3181    /// DISTINCT addresses drawn from one masked network
3182    /// (sozu-proxy/sozu#1270).
3183    ///
3184    /// Three tokens, three different `203.0.113.x` addresses — every one a
3185    /// distinct `/32`, all inside `203.0.113.0/24`. The per-IP cap is set
3186    /// to 10, comfortably above the 1 connection each address holds, so it
3187    /// cannot be what refuses the fourth: only the subnet counter can.
3188    /// That is the whole point of the two caps being independent.
3189    ///
3190    /// To SEE THIS RED: delete the `|| self.cluster_subnet_at_limit(...)`
3191    /// disjunct from `SessionManager::cluster_connection_at_limit` in
3192    /// `lib/src/server.rs`, leaving every other piece of the feature —
3193    /// config keys, proto fields, masking, tracking — in place. The fourth
3194    /// address is then admitted and the `AtLimit` assertion below fires.
3195    #[test]
3196    fn a_subnet_cap_refuses_a_fresh_address_from_an_already_counted_subnet() {
3197        let fixture = routing_fixture();
3198        let sessions = fixture.proxy.borrow().sessions();
3199        {
3200            let mut manager = sessions.borrow_mut();
3201            // /24, so 203.0.113.* all mask to one key.
3202            manager.subnet_ipv4_prefix = 24;
3203            manager.max_connections_per_subnet = 3;
3204            // Deliberately far above what any single address will hold.
3205            manager.max_connections_per_ip = 10;
3206        }
3207
3208        let mut clusters = HashMap::new();
3209        clusters.insert(
3210            H2_CLUSTER.into(),
3211            Cluster {
3212                cluster_id: H2_CLUSTER.to_owned(),
3213                ..Default::default()
3214            },
3215        );
3216        let view = RoutingView::new(&clusters, ListenerType::Http);
3217
3218        // Drive one stream, from its own source address on its own token,
3219        // through the real three-step production sequence.
3220        let run = |token: Token, source: SocketAddr| {
3221            let mut context = Context::new(
3222                Ulid::generate(),
3223                Rc::downgrade(&fixture.pool),
3224                fixture.listener.clone(),
3225                Some(source),
3226                "127.0.0.1:80"
3227                    .parse()
3228                    .expect("test public address must parse"),
3229            );
3230            let stream_id = context
3231                .create_stream(Ulid::generate(), 65_535)
3232                .expect("the test pool must hand out a stream");
3233            {
3234                let stream = &mut context.streams[stream_id];
3235                stream.state = StreamState::Link;
3236                stream.context.authority = Some(H2_AUTHORITY.to_owned());
3237                stream.context.path = Some("/".to_owned());
3238                stream.context.method = Some(Method::Get);
3239            }
3240            let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3241            let step = router
3242                .plan_connect(stream_id, &mut context, &view)
3243                .expect("routing must resolve the staged cluster");
3244            let ConnectStep::CheckIpLimit(resume) = step else {
3245                panic!("a stream WITH a session address must reach the connection gate")
3246            };
3247            let verdict = super::super::consult_ip_gate(
3248                &sessions,
3249                token,
3250                context
3251                    .http_context(stream_id)
3252                    .cluster_id
3253                    .as_ref()
3254                    .expect("plan_connect stamps the routed cluster"),
3255                &resume,
3256            );
3257            let admitted = matches!(verdict, IpGateVerdict::Admitted);
3258            let outcome =
3259                router.plan_connect_resume(stream_id, &mut context, &view, resume, verdict);
3260            (admitted, outcome)
3261        };
3262
3263        let address = |host: u8| -> SocketAddr {
3264            format!("203.0.113.{host}:51000")
3265                .parse()
3266                .expect("test source address must parse")
3267        };
3268
3269        // Premise: three DISTINCT addresses inside one /24 fill the cap.
3270        for (index, host) in [1u8, 2, 3].into_iter().enumerate() {
3271            let (admitted, outcome) = run(Token(index + 1), address(host));
3272            assert!(
3273                admitted,
3274                "premise: 203.0.113.{host} is the {} of 3 allowed in the subnet and must be \
3275                 admitted, or the refusal below proves nothing",
3276                index + 1
3277            );
3278            assert!(
3279                matches!(outcome, Ok(ConnectPlan::Dial { .. })),
3280                "an admitted stream must go on to request a dial, got {outcome:?}"
3281            );
3282        }
3283
3284        // Premise: no single address is anywhere near the per-IP cap, so
3285        // the per-IP counter cannot be what refuses the next one.
3286        {
3287            let manager = sessions.borrow();
3288            for host in [1u8, 2, 3] {
3289                let ip = address(host).ip();
3290                assert!(
3291                    !manager.cluster_ip_at_limit(Token(99), H2_CLUSTER, &ip, None),
3292                    "premise: 203.0.113.{host} holds 1 of 10 per-IP slots and must be under \
3293                     its own cap — otherwise the refusal below is the per-IP cap, not the subnet"
3294                );
3295            }
3296        }
3297
3298        // A FOURTH, never-seen address in the same /24 is refused.
3299        let (admitted, outcome) = run(Token(4), address(4));
3300        assert!(
3301            !admitted,
3302            "a fresh address from an already-counted /24 must be refused — an Admitted \
3303             verdict here is the #1270 bypass: a new address per connection defeats the cap"
3304        );
3305        assert!(
3306            matches!(
3307                outcome,
3308                Err(BackendConnectionError::TooManyConnectionsPerIp { .. })
3309            ),
3310            "the refused stream must surface the too-many-connections error, got {outcome:?}"
3311        );
3312
3313        // Negative space: the same fourth address in a DIFFERENT /24 is
3314        // admitted, so the refusal above is the subnet key and not a
3315        // blanket cap on new addresses.
3316        let (admitted, _) = run(
3317            Token(5),
3318            "198.51.100.4:51000"
3319                .parse()
3320                .expect("test source address must parse"),
3321        );
3322        assert!(
3323            admitted,
3324            "an address in an UNCOUNTED subnet must still be admitted — otherwise the cap \
3325             is refusing on something other than the subnet key"
3326        );
3327    }
3328
3329    /// The default must change nothing. With `max_connections_per_subnet`
3330    /// left at its `0` default, the admission path must behave exactly as
3331    /// it did before this feature existed — and, because the subnet
3332    /// tracking is skipped outright rather than merely ignored, it must
3333    /// also leave no bookkeeping behind.
3334    ///
3335    /// The same four addresses in one /24 that the test above refuses are
3336    /// all admitted here, and the per-IP cap still fires on its own terms
3337    /// when a single address exceeds it.
3338    #[test]
3339    fn an_unset_subnet_cap_leaves_the_admission_path_untouched() {
3340        let fixture = routing_fixture();
3341        let sessions = fixture.proxy.borrow().sessions();
3342        {
3343            let mut manager = sessions.borrow_mut();
3344            // A /24 prefix is configured but the cap is OFF: the prefix
3345            // alone must not gate anything.
3346            manager.subnet_ipv4_prefix = 24;
3347            manager.max_connections_per_subnet = 0;
3348            manager.max_connections_per_ip = 1;
3349        }
3350
3351        let mut clusters = HashMap::new();
3352        clusters.insert(
3353            H2_CLUSTER.into(),
3354            Cluster {
3355                cluster_id: H2_CLUSTER.to_owned(),
3356                ..Default::default()
3357            },
3358        );
3359        let view = RoutingView::new(&clusters, ListenerType::Http);
3360
3361        let run = |token: Token, source: SocketAddr| {
3362            let mut context = Context::new(
3363                Ulid::generate(),
3364                Rc::downgrade(&fixture.pool),
3365                fixture.listener.clone(),
3366                Some(source),
3367                "127.0.0.1:80"
3368                    .parse()
3369                    .expect("test public address must parse"),
3370            );
3371            let stream_id = context
3372                .create_stream(Ulid::generate(), 65_535)
3373                .expect("the test pool must hand out a stream");
3374            {
3375                let stream = &mut context.streams[stream_id];
3376                stream.state = StreamState::Link;
3377                stream.context.authority = Some(H2_AUTHORITY.to_owned());
3378                stream.context.path = Some("/".to_owned());
3379                stream.context.method = Some(Method::Get);
3380            }
3381            let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3382            let step = router
3383                .plan_connect(stream_id, &mut context, &view)
3384                .expect("routing must resolve the staged cluster");
3385            let ConnectStep::CheckIpLimit(resume) = step else {
3386                panic!("a stream WITH a session address must reach the connection gate")
3387            };
3388            let verdict = super::super::consult_ip_gate(
3389                &sessions,
3390                token,
3391                context
3392                    .http_context(stream_id)
3393                    .cluster_id
3394                    .as_ref()
3395                    .expect("plan_connect stamps the routed cluster"),
3396                &resume,
3397            );
3398            matches!(verdict, IpGateVerdict::Admitted)
3399        };
3400
3401        // Four distinct addresses in one /24 — the exact traffic the test
3402        // above refuses — are all admitted while the cap is off.
3403        for (index, host) in [1u8, 2, 3, 4].into_iter().enumerate() {
3404            let source: SocketAddr = format!("203.0.113.{host}:51000")
3405                .parse()
3406                .expect("test source address must parse");
3407            assert!(
3408                run(Token(index + 1), source),
3409                "with max_connections_per_subnet = 0, 203.0.113.{host} must be admitted — \
3410                 the default must not gate traffic the previous release allowed"
3411            );
3412        }
3413
3414        // The subnet accounting must be genuinely untouched, not merely
3415        // consulted-and-ignored: a disabled limiter allocates nothing.
3416        assert!(
3417            sessions.borrow().subnet_tracking_is_empty(),
3418            "a disabled subnet limiter must leave both subnet maps empty — a populated one \
3419             means every deployment that never enables this feature pays for it anyway"
3420        );
3421
3422        // And the pre-existing per-IP cap still fires on its own terms:
3423        // a SECOND token from an address already counted once is refused
3424        // at a per-IP limit of 1.
3425        let repeat: SocketAddr = "203.0.113.1:51000"
3426            .parse()
3427            .expect("test source address must parse");
3428        assert!(
3429            !run(Token(90), repeat),
3430            "the per-IP cap must still refuse a second connection from the same address — \
3431             this feature must not have relaxed the limiter that already existed"
3432        );
3433    }
3434
3435    /// The routing decision reads cluster configuration from the view it was
3436    /// handed, not from the `L7Proxy` handle it still carries.
3437    ///
3438    /// Question 6 of [#1340](https://github.com/sozu-proxy/sozu/issues/1340)
3439    /// puts routing inside the core, and the only thing that makes that worth
3440    /// anything is that the data routing reads is supplied rather than
3441    /// fetched — otherwise a simulator still cannot drive the decision. This
3442    /// hands `Router::plan_connect` a view that DISAGREES with the proxy
3443    /// about the one cluster knob the plan surfaces, and asserts the plan
3444    /// follows the view.
3445    ///
3446    /// The fixture registers `H2_CLUSTER` with `http2: Some(true)`. The view
3447    /// below says `Some(false)` for the same cluster id. A `Dial { h2: true }`
3448    /// would mean the decision went back to `L7Proxy::clusters` behind the
3449    /// view's back.
3450    ///
3451    /// To SEE THIS RED: restore either cluster read to the handle — in
3452    /// `Router::plan_connect` write `proxy.borrow().clusters().get(&cluster_id)`
3453    /// in place of `view.cluster(&cluster_id)` — and the assertion below
3454    /// reports `h2: true`.
3455    #[test]
3456    fn the_routing_decision_reads_cluster_config_from_the_view() {
3457        let fixture = routing_fixture();
3458        let pool = &fixture.pool;
3459        let mut context = Context::new(
3460            Ulid::generate(),
3461            Rc::downgrade(pool),
3462            fixture.listener.clone(),
3463            // No session address: the per-(cluster, source-IP) gate — the one
3464            // read still taken through the handle — is skipped, so this test
3465            // isolates the reads that moved.
3466            None,
3467            "127.0.0.1:80"
3468                .parse()
3469                .expect("test public address must parse"),
3470        );
3471        let stream_id = context
3472            .create_stream(Ulid::generate(), 65_535)
3473            .expect("the test pool must hand out a stream");
3474        {
3475            let stream = &mut context.streams[stream_id];
3476            stream.state = StreamState::Link;
3477            stream.context.authority = Some(H2_AUTHORITY.to_owned());
3478            stream.context.path = Some("/".to_owned());
3479            stream.context.method = Some(Method::Get);
3480        }
3481
3482        // A cluster map that is NOT the proxy's, disagreeing on `http2`.
3483        let mut clusters = HashMap::new();
3484        clusters.insert(
3485            H2_CLUSTER.into(),
3486            Cluster {
3487                cluster_id: H2_CLUSTER.to_owned(),
3488                http2: Some(false),
3489                ..Default::default()
3490            },
3491        );
3492        assert_eq!(
3493            fixture
3494                .proxy
3495                .borrow()
3496                .clusters()
3497                .get(H2_CLUSTER)
3498                .and_then(|c| c.http2),
3499            Some(true),
3500            "premise: the proxy and the view must disagree, or this proves nothing"
3501        );
3502        let view = RoutingView::new(&clusters, ListenerType::Http);
3503
3504        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3505        let plan = router
3506            .plan_connect(stream_id, &mut context, &view)
3507            .map(decided)
3508            .expect("routing must resolve the staged cluster");
3509
3510        match plan {
3511            ConnectPlan::Dial {
3512                ref cluster_id, h2, ..
3513            } => {
3514                assert_eq!(
3515                    &**cluster_id, H2_CLUSTER,
3516                    "premise: routing resolved a cluster"
3517                );
3518                assert!(
3519                    !h2,
3520                    "the plan must carry the view's `http2`, not the proxy's — \
3521                     `h2: true` means the decision read `L7Proxy::clusters` behind the view"
3522                );
3523            }
3524            ConnectPlan::Attached => {
3525                panic!("premise: an empty router has nothing to reuse, so it must ask for a dial")
3526            }
3527        }
3528    }
3529
3530    /// A plan is a decision, not a deed: `ConnectPlan::Dial` must leave the
3531    /// router and the stream exactly as it found them.
3532    ///
3533    /// This is the property Question 6 of
3534    /// [#1340](https://github.com/sozu-proxy/sozu/issues/1340) is actually
3535    /// about — the core decides and the embedder performs — and it is the one
3536    /// a later edit is most likely to erode, by "just" doing one small effect
3537    /// inline because the data is right there. Asserting on the return value
3538    /// alone would not catch that: a `Dial` that had already dialled is still
3539    /// a `Dial`. So this asserts on the state the router and the stream are
3540    /// left in.
3541    ///
3542    /// To SEE THIS RED: perform any effect in `Router::plan_connect`'s
3543    /// new-backend path before it returns — inserting into `self.backends`,
3544    /// or calling `context.link_stream(stream_id, token)` — and the
3545    /// corresponding assertion below fires.
3546    ///
3547    /// SCOPE, because the name would otherwise over-read.
3548    /// `Router::plan_connect` is NOT pure yet. It still performs one registry
3549    /// mutation: `SessionManager::track_cluster_ip`, reached through
3550    /// `L7Proxy::sessions` in the per-(cluster, source-IP) limit gate. That is
3551    /// the mutation sitting *between* cluster resolution and backend
3552    /// selection that #1340 recorded as Question 6's first wrinkle, and it is
3553    /// deliberately unchanged here — it moves when the borrowed view lands,
3554    /// as either a second returned step or part of that view.
3555    ///
3556    /// This `Context` is therefore built with `session_address: None`, which
3557    /// skips the gate entirely, and the assertions below cover the router's
3558    /// backend map, the stream's link state, the backend-stream reverse index
3559    /// and the accounting ledger — NOT the session manager. Read the
3560    /// assertion list rather than the test name.
3561    #[test]
3562    fn a_dial_plan_performs_no_effect_of_its_own() {
3563        let fixture = routing_fixture();
3564        let pool = &fixture.pool;
3565        let mut context = Context::new(
3566            Ulid::generate(),
3567            Rc::downgrade(pool),
3568            fixture.listener.clone(),
3569            // No session address, so the per-(cluster, source-IP) gate is
3570            // skipped and this never reaches the proxy's session manager.
3571            None,
3572            "127.0.0.1:80"
3573                .parse()
3574                .expect("test public address must parse"),
3575        );
3576        let stream_id = context
3577            .create_stream(Ulid::generate(), 65_535)
3578            .expect("the test pool must hand out a stream");
3579        {
3580            let stream = &mut context.streams[stream_id];
3581            stream.state = StreamState::Link;
3582            stream.context.authority = Some(H2_AUTHORITY.to_owned());
3583            stream.context.path = Some("/".to_owned());
3584            stream.context.method = Some(Method::Get);
3585        }
3586
3587        // An EMPTY router: nothing to reuse, so routing must fall through to
3588        // the new-backend path and ask the embedder for a dial.
3589        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3590        let proxy_ref = fixture.proxy.borrow();
3591        let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
3592        let plan = router
3593            .plan_connect(stream_id, &mut context, &view)
3594            .map(decided)
3595            .expect("routing must resolve the staged cluster");
3596
3597        match &plan {
3598            ConnectPlan::Dial { cluster_id, .. } => assert_eq!(
3599                &**cluster_id, H2_CLUSTER,
3600                "the dial request must name the cluster routing resolved"
3601            ),
3602            ConnectPlan::Attached => {
3603                panic!("premise: an empty router has nothing to reuse, so it must ask for a dial")
3604            }
3605        }
3606
3607        assert!(
3608            router.backends.is_empty(),
3609            "a plan must not have taken a backend connection into the router: {:?}",
3610            router.backends.keys().collect::<Vec<_>>()
3611        );
3612        assert_eq!(
3613            context.streams[stream_id].state,
3614            StreamState::Link,
3615            "a plan must not have linked the stream — the embedder has not dialled yet"
3616        );
3617        assert!(
3618            context.backend_streams.is_empty(),
3619            "a plan must not have touched the backend-stream reverse index"
3620        );
3621        assert!(
3622            context.backend_deltas.is_empty(),
3623            "a plan must not have charged any backend accounting: {:?}",
3624            context.backend_deltas
3625        );
3626    }
3627
3628    /// The leak with the most user-visible consequence: the H2 least-loaded
3629    /// arm compares stream counts with a strict `<`, so every staged backend
3630    /// sitting at the same count leaves the choice entirely to iteration
3631    /// order — hash order decided which machine served the request.
3632    #[test]
3633    fn an_h2_stream_count_tie_always_picks_the_lowest_backend_token() {
3634        let fixture = routing_fixture();
3635        for round in 0..ROUNDS {
3636            let chosen = selected_backend_token(&fixture, &Staged::ConnectedH2);
3637            assert_eq!(
3638                chosen,
3639                Token(LOWEST_TOKEN),
3640                "round {round}: a stream-count tie must resolve to the lowest token, \
3641                 not to whatever the map iterated first"
3642            );
3643        }
3644    }
3645
3646    /// The same scan's fallback arm, and it does NOT bias the same way:
3647    /// it assigns with neither a `break` nor an "already chosen" guard, so
3648    /// the LAST matching connecting backend wins. The total order pins that
3649    /// to the highest token — asserting the lowest here would be red against
3650    /// both containers.
3651    #[test]
3652    fn an_all_connecting_h2_fallback_always_picks_the_highest_backend_token() {
3653        let fixture = routing_fixture();
3654        for round in 0..ROUNDS {
3655            let chosen = selected_backend_token(&fixture, &Staged::ConnectingH2);
3656            assert_eq!(
3657                chosen,
3658                Token(HIGHEST_TOKEN),
3659                "round {round}: the connecting-backend fallback assigns last-wins, \
3660                 so the total order must land on the highest token"
3661            );
3662        }
3663    }
3664
3665    /// The H1 keep-alive arm carries the same first-seen bias as the H2
3666    /// least-loaded one — `reuse_token = Some(*token); break;` — and it is
3667    /// live wherever an H2 frontend fans several concurrent streams onto an
3668    /// H1 cluster: each stream dials its own socket, every socket parks
3669    /// `KeepAlive` for the same cluster, and the next request picks among
3670    /// them by map order.
3671    #[test]
3672    fn an_h1_keep_alive_reuse_always_picks_the_lowest_backend_token() {
3673        let fixture = routing_fixture();
3674        for round in 0..ROUNDS {
3675            let chosen = selected_backend_token(&fixture, &Staged::KeepAliveH1);
3676            assert_eq!(
3677                chosen,
3678                Token(LOWEST_TOKEN),
3679                "round {round}: the first matching keep-alive socket wins, so the total \
3680                 order must land on the lowest token"
3681            );
3682        }
3683    }
3684
3685    /// #1579: the routing decision for a request that reuses a pooled
3686    /// backend connection allocates nothing of its own, in steady state.
3687    ///
3688    /// The reuse branch of `Router::decide_after_gate` stamps the stream's
3689    /// `HttpContext` and `SessionMetrics` with the id the connection's
3690    /// `BackendId` carries. That id is an `Rc<str>`, so the stamp is a
3691    /// reference-count increment; a `String` copy there costs one heap
3692    /// allocation per field and per request.
3693    ///
3694    /// Measured against a control that performs the same mux bookkeeping the
3695    /// branch triggers — `Connection::start_stream` then
3696    /// `Context::link_stream` on the same keep-alive socket — and nothing
3697    /// else. Each request is released the way production releases it
3698    /// (`Context::unlink_stream`, then the ledger drained through
3699    /// `BackendRegistry::apply_all` as a Mux pass drains it), so the control
3700    /// carries exactly the bookkeeping's costs and the difference is what the
3701    /// decision adds.
3702    ///
3703    /// Both are also held at zero outright (#1583): the reverse-index entry
3704    /// keeps its emptied `Vec` for the next `link_stream`, and the ledger is
3705    /// drained in place, so the bookkeeping itself allocates nothing either.
3706    ///
3707    /// TO SEE THIS RED: stamp either field with
3708    /// `Some(backend.backend_id.to_string().into())` instead of the `Rc`
3709    /// clone; the difference is then two allocations per request per field
3710    /// (the `String`, then the `Rc<str>` built from it). For the absolute
3711    /// half, make `remove_backend_stream` drop the emptied entry again, or
3712    /// make `BackendRegistry::apply_all` iterate `mem::take(deltas)`: each
3713    /// costs one allocation per request in both measurements.
3714    #[test]
3715    fn a_request_on_a_reused_backend_connection_allocates_nothing() {
3716        use std::hint::black_box;
3717
3718        use crate::test_allocations::allocations;
3719
3720        const REQUESTS: usize = 64;
3721        let backend_token = Token(LOWEST_TOKEN);
3722
3723        let fixture = routing_fixture();
3724        let mut context = Context::new(
3725            Ulid::generate(),
3726            Rc::downgrade(&fixture.pool),
3727            fixture.listener.clone(),
3728            None,
3729            "127.0.0.1:80"
3730                .parse()
3731                .expect("test public address must parse"),
3732        );
3733        let stream_id = context
3734            .create_stream(Ulid::generate(), 65_535)
3735            .expect("the test pool must hand out a stream");
3736        context.streams[stream_id].context.method = Some(Method::Get);
3737        // Where `Router::plan_connect` leaves the routed cluster for the
3738        // decision to read.
3739        context.streams[stream_id].context.cluster_id = Some(H1_CLUSTER.into());
3740
3741        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3742        let mut backend_registry = BackendRegistry::default();
3743        let (connection, _peer) =
3744            staged_backend(&fixture.pool, &Staged::KeepAliveH1, &mut backend_registry);
3745        router.backends.insert(backend_token, connection);
3746
3747        // Released as `ConnectionH1::end_stream` releases a terminated
3748        // response on a keep-alive backend, then the ledger drained as the
3749        // next Mux pass drains it.
3750        let backend_registry = &backend_registry;
3751        let release = |router: &mut Router, context: &mut Context<HttpListener>| {
3752            assert_eq!(
3753                context.streams[stream_id].state,
3754                StreamState::Linked(backend_token)
3755            );
3756            context.unlink_stream(stream_id);
3757            backend_registry.apply_all(&mut context.backend_deltas, &fixture.backends);
3758            context.streams[stream_id].forget_upstream_replay();
3759            context.streams[stream_id].state = StreamState::Link;
3760            let Some(Connection::H1(h1)) = router.backends.get_mut(&backend_token) else {
3761                unreachable!("the staged backend is an H1 connection")
3762            };
3763            h1.stream = None;
3764            if let Position::Client(_, _, status) = &mut h1.position {
3765                *status = BackendStatus::KeepAlive;
3766            }
3767        };
3768        // The worker's retired-backend check rides the decision, so it is
3769        // measured with it.
3770        let clusters = HashMap::new();
3771        let retired = |backend: &super::BackendId| backend_registry.is_retired(backend);
3772        let view = RoutingView::new(&clusters, ListenerType::Http).with_backend_retired(&retired);
3773        // One request through the routing decision.
3774        let decision = |router: &mut Router, context: &mut Context<HttpListener>| -> usize {
3775            let before = allocations();
3776            let plan =
3777                router.decide_after_gate(stream_id, black_box(&mut *context), &view, false, false);
3778            let allocated = allocations() - before;
3779            assert!(
3780                matches!(plan, Ok(ConnectPlan::Attached)),
3781                "the keep-alive socket must be reused, got {plan:?}"
3782            );
3783            release(router, context);
3784            allocated
3785        };
3786        // The same socket attached by hand: only the mux bookkeeping.
3787        let control = |router: &mut Router, context: &mut Context<HttpListener>| -> usize {
3788            let before = allocations();
3789            let started = router
3790                .backends
3791                .get_mut(&backend_token)
3792                .expect("the staged backend is registered")
3793                .start_stream(stream_id, black_box(&mut *context));
3794            context.link_stream(stream_id, backend_token);
3795            let allocated = allocations() - before;
3796            assert!(started, "the keep-alive socket must accept the stream");
3797            release(router, context);
3798            allocated
3799        };
3800
3801        // Warm-up: metric keys and tables are sized by the first request.
3802        decision(&mut router, &mut context);
3803        control(&mut router, &mut context);
3804        let (mut decided, mut controlled) = (0, 0);
3805        for _ in 0..REQUESTS {
3806            decided += decision(&mut router, &mut context);
3807            controlled += control(&mut router, &mut context);
3808        }
3809
3810        decision(&mut router, &mut context);
3811        let stream = &context.streams[stream_id];
3812        assert_eq!(stream.context.backend_id.as_deref(), Some("test-backend"));
3813        assert_eq!(stream.metrics.backend_id.as_deref(), Some("test-backend"));
3814        assert_eq!(
3815            decided.saturating_sub(controlled),
3816            0,
3817            "{REQUESTS} requests on a reused keep-alive backend made {decided} \
3818             heap allocations in the routing decision against {controlled} for \
3819             the mux bookkeeping alone, expected no difference"
3820        );
3821        assert_eq!(
3822            (decided, controlled),
3823            (0, 0),
3824            "{REQUESTS} requests on a reused keep-alive backend must allocate \
3825             nothing in the decision nor in the mux bookkeeping it triggers"
3826        );
3827    }
3828
3829    /// #1583: the whole reuse branch copies no cluster id, gated or not.
3830    ///
3831    /// `ClusterId` is a reference-counted `Arc<str>` in sozu-command-lib:
3832    /// `route_from_request` hands `Router::plan_connect` a handle on the
3833    /// route table's own id, `plan_connect` moves it into the stream's
3834    /// `HttpContext`, and every later reader borrows it from there or bumps
3835    /// its count — the per-(cluster, source-IP) gate's `SessionManager`
3836    /// bookkeeping included, whose maps are keyed by the same handle. With
3837    /// the reverse-index entry and the delta ledger keeping their capacity,
3838    /// the branch allocates nothing at all past the debug build's history.
3839    ///
3840    /// Measured end to end — `plan_connect`, then on the gated path
3841    /// `consult_ip_gate` with its `SessionManager::track_cluster_connection`
3842    /// and `plan_connect_resume`, then the production release — against a
3843    /// control made of the one cost this branch does not own and which runs
3844    /// inside `plan_connect`: the debug-build `DebugEvent::Str` history push,
3845    /// absent from a release build, where the control is zero. The rest of
3846    /// `route_from_request` is measured, not controlled: it copies no
3847    /// authority and the route lookup records its trie segments on the stack
3848    /// (#1589). Both paths run, because production traffic always carries a
3849    /// source address and so always takes the gated one, while the fixtures
3850    /// above never do.
3851    ///
3852    /// TO SEE THIS RED: stamp `stream_context.cluster_id` in `plan_connect`
3853    /// with a copy (`Some(ClusterId::from(&*cluster_id))`) instead of moving
3854    /// the routed handle in: one allocation per request, on both paths. Hand
3855    /// `SessionManager::track_cluster_connection` such a copy in
3856    /// `consult_ip_gate` and the gated path alone pays one more. The same
3857    /// count comes back if `route_from_request` stamps
3858    /// `HttpContext::original_authority` with a copy of the authority on a
3859    /// request without a host rewrite, or if `Router::lookup` hands the trie
3860    /// a `Vec::with_capacity(16)` again.
3861    #[test]
3862    fn a_routed_request_on_a_reused_backend_connection_copies_no_cluster_id() {
3863        use std::hint::black_box;
3864
3865        use crate::test_allocations::allocations;
3866
3867        const REQUESTS: usize = 64;
3868        let backend_token = Token(LOWEST_TOKEN);
3869        let fixture = routing_fixture();
3870        let sessions = fixture.proxy.borrow().sessions();
3871
3872        for gated in [false, true] {
3873            let mut context = Context::new(
3874                Ulid::generate(),
3875                Rc::downgrade(&fixture.pool),
3876                fixture.listener.clone(),
3877                gated.then(|| {
3878                    "127.0.0.1:4242"
3879                        .parse()
3880                        .expect("test session address must parse")
3881                }),
3882                "127.0.0.1:80"
3883                    .parse()
3884                    .expect("test public address must parse"),
3885            );
3886            let stream_id = context
3887                .create_stream(Ulid::generate(), 65_535)
3888                .expect("the test pool must hand out a stream");
3889            {
3890                let stream = &mut context.streams[stream_id];
3891                stream.state = StreamState::Link;
3892                stream.context.authority = Some(H1_AUTHORITY.to_owned());
3893                stream.context.path = Some("/".to_owned());
3894                stream.context.method = Some(Method::Get);
3895            }
3896            let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
3897            let mut backend_registry = BackendRegistry::default();
3898            let (connection, _peer) =
3899                staged_backend(&fixture.pool, &Staged::KeepAliveH1, &mut backend_registry);
3900            router.backends.insert(backend_token, connection);
3901            let proxy_ref = fixture.proxy.borrow();
3902            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
3903
3904            // One request through the whole branch, the gate's bookkeeping
3905            // included, then released as production releases it.
3906            let branch = |router: &mut Router, context: &mut Context<HttpListener>| -> usize {
3907                let before = allocations();
3908                let step = router
3909                    .plan_connect(stream_id, black_box(&mut *context), &view)
3910                    .expect("the reused request must be planned");
3911                let mut allocated = allocations() - before;
3912                let plan = match step {
3913                    ConnectStep::Decided(plan) => {
3914                        assert!(!gated, "a stream with a source address must be gated");
3915                        plan
3916                    }
3917                    ConnectStep::CheckIpLimit(resume) => {
3918                        assert!(gated, "a stream without a source address must not be gated");
3919                        let before = allocations();
3920                        let verdict = super::super::consult_ip_gate(
3921                            &sessions,
3922                            Token(0),
3923                            context
3924                                .http_context(stream_id)
3925                                .cluster_id
3926                                .as_ref()
3927                                .expect("plan_connect stamps the routed cluster"),
3928                            &resume,
3929                        );
3930                        let plan = router.plan_connect_resume(
3931                            stream_id,
3932                            &mut *context,
3933                            &view,
3934                            resume,
3935                            verdict,
3936                        );
3937                        allocated += allocations() - before;
3938                        plan.expect("the gate is unlimited, so the stream is admitted")
3939                    }
3940                };
3941                assert!(
3942                    matches!(plan, ConnectPlan::Attached),
3943                    "the keep-alive socket must be reused, got {plan:?}"
3944                );
3945                let before = allocations();
3946                context.unlink_stream(stream_id);
3947                backend_registry.apply_all(&mut context.backend_deltas, &fixture.backends);
3948                allocated += allocations() - before;
3949                let stream = &mut context.streams[stream_id];
3950                stream.forget_upstream_replay();
3951                stream.state = StreamState::Link;
3952                stream.attempts = 0;
3953                let Some(Connection::H1(h1)) = router.backends.get_mut(&backend_token) else {
3954                    unreachable!("the staged backend is an H1 connection")
3955                };
3956                h1.stream = None;
3957                if let Position::Client(_, _, status) = &mut h1.position {
3958                    *status = BackendStatus::KeepAlive;
3959                }
3960                allocated
3961            };
3962            // The one cost `plan_connect` carries that is not this branch's:
3963            // the debug history push. Routing hands over a handle on the
3964            // route table's cluster id, so nothing else may allocate.
3965            let control = |context: &mut Context<HttpListener>| -> usize {
3966                let before = allocations();
3967                #[cfg(debug_assertions)]
3968                {
3969                    use crate::protocol::mux::DebugEvent;
3970
3971                    let route = context.streams[stream_id].context.get_route();
3972                    context.debug.push(DebugEvent::Str(route));
3973                }
3974                #[cfg(not(debug_assertions))]
3975                let _ = &context;
3976                allocations() - before
3977            };
3978
3979            // Warm-up: metric keys, the reverse index and the ledger are
3980            // sized by the first request.
3981            branch(&mut router, &mut context);
3982            control(&mut context);
3983            let (mut branched, mut controlled) = (0, 0);
3984            for _ in 0..REQUESTS {
3985                branched += branch(&mut router, &mut context);
3986                controlled += control(&mut context);
3987            }
3988
3989            assert_eq!(
3990                context.http_context(stream_id).cluster_id.as_deref(),
3991                Some(H1_CLUSTER)
3992            );
3993            assert_eq!(
3994                branched, controlled,
3995                "gated={gated}: {REQUESTS} requests on a reused keep-alive backend \
3996                 made {branched} heap allocations through the reuse branch against \
3997                 {controlled} for the debug history, expected no difference"
3998            );
3999        }
4000    }
4001
4002    /// #1583: a dial copies no cluster id: the new connection shares the
4003    /// routed one.
4004    ///
4005    /// `ClusterId` is a reference-counted `Arc<str>` in sozu-command-lib, so
4006    /// the route table, the stream's `HttpContext::cluster_id`, the
4007    /// `ConnectPlan::Dial` and the new connection's `Position::Client` all
4008    /// point at the one allocation the route table made when the frontend was
4009    /// added. `Mux::attach_dialed` moves the plan's handle into the connection.
4010    ///
4011    /// TO SEE THIS RED: build the connection in `Mux::attach_dialed` from
4012    /// `ClusterId::from(&*cluster_id)`, or stamp the plan in
4013    /// `Router::decide_after_gate` with one; the connection then owns a
4014    /// second copy at another address.
4015    #[test]
4016    fn a_dial_moves_the_planned_cluster_id_into_the_backend_connection() {
4017        use crate::{Protocol, ProxySession, protocol::mux::Mux, server::ListenSession};
4018
4019        let fixture = routing_fixture();
4020        let upstream =
4021            std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback listener must bind");
4022        fixture.backends.borrow_mut().add_backend(
4023            H1_CLUSTER,
4024            Backend::new(
4025                "dial-backend",
4026                upstream
4027                    .local_addr()
4028                    .expect("a bound listener has an address"),
4029                None,
4030                None,
4031                None,
4032            ),
4033        );
4034        let mut context = Context::new(
4035            Ulid::generate(),
4036            Rc::downgrade(&fixture.pool),
4037            fixture.listener.clone(),
4038            None,
4039            "127.0.0.1:80"
4040                .parse()
4041                .expect("test public address must parse"),
4042        );
4043        let stream_id = context
4044            .create_stream(Ulid::generate(), 65_535)
4045            .expect("the test pool must hand out a stream");
4046        {
4047            let stream = &mut context.streams[stream_id];
4048            stream.state = StreamState::Link;
4049            stream.context.authority = Some(H1_AUTHORITY.to_owned());
4050            stream.context.path = Some("/".to_owned());
4051            stream.context.method = Some(Method::Get);
4052        }
4053        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
4054        let mut backend_registry = BackendRegistry::default();
4055
4056        let plan = {
4057            let proxy_ref = fixture.proxy.borrow();
4058            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
4059            router
4060                .plan_connect(stream_id, &mut context, &view)
4061                .map(decided)
4062                .expect("an empty router must ask for a dial")
4063        };
4064        let ConnectPlan::Dial {
4065            cluster_id,
4066            h2,
4067            frontend_should_stick,
4068        } = plan
4069        else {
4070            panic!("premise: an empty router has nothing to reuse, so it must ask for a dial")
4071        };
4072        let planned = cluster_id.as_ptr();
4073
4074        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(ListenSession {
4075            protocol: Protocol::HTTPListen,
4076        }));
4077        Mux::<mio::net::TcpStream, HttpListener>::dial_backend(
4078            &mut router,
4079            &mut backend_registry,
4080            &fixture.backends,
4081            stream_id,
4082            &mut context,
4083            &session,
4084            &fixture.proxy,
4085            cluster_id,
4086            h2,
4087            frontend_should_stick,
4088        )
4089        .expect("the loopback backend must dial");
4090
4091        let token = match context.streams[stream_id].state {
4092            StreamState::Linked(token) => token,
4093            other => panic!("the dial must link the stream, got {other:?}"),
4094        };
4095        let Some(Position::Client(owned, _, _)) =
4096            router.backends.get(&token).map(Connection::position)
4097        else {
4098            panic!("the dialled connection must be a client")
4099        };
4100        assert_eq!(&**owned, H1_CLUSTER);
4101        assert_eq!(
4102            context.http_context(stream_id).cluster_id.as_deref(),
4103            Some(H1_CLUSTER)
4104        );
4105        assert!(
4106            std::ptr::eq(owned.as_ptr(), planned),
4107            "the connection must own the id the plan carried, not a copy of it"
4108        );
4109        let routed = context
4110            .http_context(stream_id)
4111            .cluster_id
4112            .as_ref()
4113            .expect("plan_connect stamps the routed cluster");
4114        assert!(
4115            std::ptr::eq(owned.as_ptr(), routed.as_ptr()),
4116            "the connection must share the stream's routed id, not a copy of it"
4117        );
4118    }
4119
4120    /// Dial one fresh stream of `H1_CLUSTER` through `Mux::dial_backend`
4121    /// against a single backend at `address`, then drain the ledger as the
4122    /// next pass would. Returns the dial's result and the backend.
4123    fn dial_one(
4124        address: std::net::SocketAddr,
4125    ) -> (Result<(), BackendConnectionError>, Rc<RefCell<Backend>>) {
4126        dial_with(address, DialFault::None)
4127    }
4128
4129    /// A local failure `dial_with` stages between the plan and the dial, each
4130    /// reaching one exit of `Mux::attach_dialed`, past a successful `connect(2)`.
4131    enum DialFault {
4132        /// No fault: the dial runs as it does in production.
4133        None,
4134        /// Every buffer of the pool is checked out, so
4135        /// `Connection::new_h2_client` returns `None`. Dials `H2_CLUSTER`.
4136        PoolExhausted,
4137        /// The request already holds output encoded for another connection,
4138        /// so `ConnectionH2::start_stream` refuses it (#1632). Dials
4139        /// `H2_CLUSTER`.
4140        StartRefused,
4141        /// `L7Proxy::register_socket` fails, as it does under fd pressure.
4142        /// Dials `H1_CLUSTER`.
4143        RegisterRefused,
4144    }
4145
4146    /// The fixture's proxy, except that registering a socket fails.
4147    ///
4148    /// Everything `Mux::dial_backend` calls besides `register_socket` goes to
4149    /// the real proxy, so the slab session it adds is removed from it again.
4150    struct RegisterRefuses(Rc<RefCell<dyn L7Proxy>>);
4151
4152    impl L7Proxy for RegisterRefuses {
4153        fn kind(&self) -> ListenerType {
4154            self.0.borrow().kind()
4155        }
4156        fn register_socket(
4157            &self,
4158            _socket: &mut mio::net::TcpStream,
4159            _token: Token,
4160            _interest: mio::Interest,
4161        ) -> Result<(), std::io::Error> {
4162            Err(std::io::Error::from_raw_os_error(24)) // EMFILE
4163        }
4164        fn add_session(&self, session: Rc<RefCell<dyn crate::ProxySession>>) -> Token {
4165            self.0.borrow().add_session(session)
4166        }
4167        fn remove_session(&self, token: Token) -> bool {
4168            self.0.borrow().remove_session(token)
4169        }
4170        fn clusters(&self) -> &HashMap<sozu_command::state::ClusterId, Cluster> {
4171            unreachable!("Mux::dial_backend never reads the cluster table")
4172        }
4173        fn sessions(&self) -> Rc<RefCell<crate::server::SessionManager>> {
4174            self.0.borrow().sessions()
4175        }
4176    }
4177
4178    /// [`dial_one`] with `fault` staged before the dial.
4179    fn dial_with(
4180        address: std::net::SocketAddr,
4181        fault: DialFault,
4182    ) -> (Result<(), BackendConnectionError>, Rc<RefCell<Backend>>) {
4183        use crate::{Protocol, ProxySession, protocol::mux::Mux, server::ListenSession};
4184
4185        let (cluster, authority) = match fault {
4186            DialFault::PoolExhausted | DialFault::StartRefused => (H2_CLUSTER, H2_AUTHORITY),
4187            DialFault::None | DialFault::RegisterRefused => (H1_CLUSTER, H1_AUTHORITY),
4188        };
4189        let fixture = routing_fixture();
4190        fixture.backends.borrow_mut().add_backend(
4191            cluster,
4192            Backend::new("dial-once", address, None, None, None),
4193        );
4194        let backend = fixture
4195            .backends
4196            .borrow_mut()
4197            .backends
4198            .get_mut(cluster)
4199            .and_then(|list| list.find_backend(&address).cloned())
4200            .expect("the backend was just added");
4201        let mut context = Context::new(
4202            Ulid::generate(),
4203            Rc::downgrade(&fixture.pool),
4204            fixture.listener.clone(),
4205            None,
4206            "127.0.0.1:80"
4207                .parse()
4208                .expect("test public address must parse"),
4209        );
4210        let stream_id = context
4211            .create_stream(Ulid::generate(), 65_535)
4212            .expect("the test pool must hand out a stream");
4213        {
4214            let stream = &mut context.streams[stream_id];
4215            stream.state = StreamState::Link;
4216            stream.context.authority = Some(authority.to_owned());
4217            stream.context.path = Some("/".to_owned());
4218            stream.context.method = Some(Method::Get);
4219        }
4220        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
4221        let mut backend_registry = BackendRegistry::default();
4222        let plan = {
4223            let proxy_ref = fixture.proxy.borrow();
4224            let view = RoutingView::new(proxy_ref.clusters(), proxy_ref.kind());
4225            router
4226                .plan_connect(stream_id, &mut context, &view)
4227                .map(decided)
4228                .expect("an empty router must ask for a dial")
4229        };
4230        let ConnectPlan::Dial {
4231            cluster_id,
4232            h2,
4233            frontend_should_stick,
4234        } = plan
4235        else {
4236            panic!("premise: an empty router has nothing to reuse, so it must ask for a dial")
4237        };
4238        assert_eq!(
4239            h2,
4240            cluster == H2_CLUSTER,
4241            "premise: the plan must dial the protocol the cluster declares"
4242        );
4243        // Held until the dial returns: dropping a checkout gives it back.
4244        let mut exhausted = Vec::new();
4245        let mut proxy = fixture.proxy.clone();
4246        match fault {
4247            DialFault::None => {}
4248            DialFault::PoolExhausted => {
4249                while let Some(buffer) = fixture.pool.borrow_mut().checkout() {
4250                    exhausted.push(buffer);
4251                }
4252            }
4253            DialFault::StartRefused => {
4254                context.streams[stream_id]
4255                    .front
4256                    .out
4257                    .push_back(kawa::OutBlock::Store(kawa::Store::Static(b"encoded")));
4258            }
4259            DialFault::RegisterRefused => {
4260                proxy = Rc::new(RefCell::new(RegisterRefuses(fixture.proxy.clone())));
4261            }
4262        }
4263        let session: Rc<RefCell<dyn ProxySession>> = Rc::new(RefCell::new(ListenSession {
4264            protocol: Protocol::HTTPListen,
4265        }));
4266        let dialed = Mux::<mio::net::TcpStream, HttpListener>::dial_backend(
4267            &mut router,
4268            &mut backend_registry,
4269            &fixture.backends,
4270            stream_id,
4271            &mut context,
4272            &session,
4273            &proxy,
4274            cluster_id,
4275            h2,
4276            frontend_should_stick,
4277        );
4278        drop(exhausted);
4279        assert!(
4280            dialed.is_ok() || router.backends.is_empty(),
4281            "a dial that failed must keep no backend connection, got {dialed:?}"
4282        );
4283        backend_registry.apply_all(&mut context.backend_deltas, &fixture.backends);
4284        (dialed, backend)
4285    }
4286
4287    /// #1684: a dial that connects counts its connection once. Selection
4288    /// reserved it, and the dial keeps that reservation as the connection's
4289    /// count rather than taking a second one.
4290    ///
4291    /// TO SEE THIS RED: call `inc_connections` on the dialled backend in
4292    /// `Mux::dial_backend` after the connect succeeds, as
4293    /// `Backend::try_connect` does for the TCP proxy; the backend then holds
4294    /// two connections for one dial.
4295    #[test]
4296    fn a_successful_dial_counts_its_connection_once() {
4297        let upstream =
4298            std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback listener must bind");
4299        let (dialed, backend) = dial_one(
4300            upstream
4301                .local_addr()
4302                .expect("a bound listener has an address"),
4303        );
4304        dialed.expect("the loopback backend must dial");
4305        let backend = backend.borrow();
4306        assert_eq!(
4307            backend.active_connections, 1,
4308            "one dial must hold exactly one connection on its backend"
4309        );
4310        assert_eq!(backend.failures, 0, "a successful dial records no failure");
4311    }
4312
4313    /// #1684: a `connect(2)` that fails synchronously releases the reservation
4314    /// selection took and records the failure, once the ledger is drained.
4315    ///
4316    /// `255.255.255.255` is the limited broadcast address: connecting a TCP
4317    /// socket to it fails before any packet leaves (`ENETUNREACH`, or
4318    /// `EACCES` where a default route exists). The dial is asserted to fail,
4319    /// so a host where it somehow connected fails this test loudly instead of
4320    /// passing it vacuously.
4321    ///
4322    /// TO SEE THIS RED: return the error from `Mux::dial_backend` without
4323    /// pushing the `BackendChange::DialFailed` delta; the reservation then
4324    /// outlives the failed dial.
4325    #[test]
4326    fn a_synchronously_failed_dial_releases_its_reservation() {
4327        let (dialed, backend) = dial_one("255.255.255.255:80".parse().expect("a literal address"));
4328        assert!(
4329            matches!(
4330                dialed,
4331                Err(BackendConnectionError::Backend(
4332                    crate::backends::BackendError::ConnectionFailures { failures: 1, .. }
4333                ))
4334            ),
4335            "a connect to the broadcast address must fail synchronously, got {dialed:?}"
4336        );
4337        let backend = backend.borrow();
4338        assert_eq!(
4339            backend.active_connections, 0,
4340            "the failed dial must release the reservation selection took"
4341        );
4342        assert_eq!(
4343            backend.failures, 1,
4344            "the failed dial must count one failure"
4345        );
4346    }
4347
4348    /// #1713: dial one stream with `fault` staged against a listening
4349    /// backend, and assert the dial failed with `expected` and left the
4350    /// backend exactly as it found it: the reservation selection took is
4351    /// released, and no failure is recorded, because the backend did not fail.
4352    fn assert_a_local_failure_releases_the_reservation(
4353        fault: DialFault,
4354        expected: fn(&BackendConnectionError) -> bool,
4355    ) {
4356        use crate::retry::{RetryAction, RetryPolicy};
4357
4358        let upstream =
4359            std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback listener must bind");
4360        let (dialed, backend) = dial_with(
4361            upstream
4362                .local_addr()
4363                .expect("a bound listener has an address"),
4364            fault,
4365        );
4366        assert!(
4367            dialed.as_ref().is_err_and(expected),
4368            "premise: the staged fault must fail the dial at its exit, got {dialed:?}"
4369        );
4370        let backend = backend.borrow();
4371        assert_eq!(
4372            backend.active_connections, 0,
4373            "a dial abandoned after its connect must release the reservation \
4374             selection took"
4375        );
4376        assert_eq!(
4377            backend.failures, 0,
4378            "a local failure must not count against the backend"
4379        );
4380        assert_eq!(
4381            backend.retry_policy.can_try(std::time::Instant::now()),
4382            Some(RetryAction::OKAY),
4383            "a local failure must not arm the backend's backoff"
4384        );
4385    }
4386
4387    /// #1713: `Connection::new_h2_client` returning `None` (buffer pool
4388    /// exhausted) releases the reservation.
4389    ///
4390    /// TO SEE THIS RED: in `Mux::dial_backend`, return the error of
4391    /// `Mux::attach_dialed` without pushing the `BackendChange::ConnectionClosed`
4392    /// delta; the backend then keeps `active_connections == 1`.
4393    #[test]
4394    fn an_h2_client_the_pool_cannot_build_releases_its_reservation() {
4395        assert_a_local_failure_releases_the_reservation(DialFault::PoolExhausted, |error| {
4396            matches!(error, BackendConnectionError::MaxBuffers)
4397        });
4398    }
4399
4400    /// #1713: a refused `Connection::start_stream` releases the reservation.
4401    ///
4402    /// TO SEE THIS RED: as for
4403    /// [`an_h2_client_the_pool_cannot_build_releases_its_reservation`].
4404    #[test]
4405    fn a_refused_stream_start_releases_its_reservation() {
4406        assert_a_local_failure_releases_the_reservation(DialFault::StartRefused, |error| {
4407            matches!(error, BackendConnectionError::MaxSessionsMemory)
4408        });
4409    }
4410
4411    /// #1713: the `register_socket` rollback releases the reservation, not
4412    /// only the gauges, `active_requests` and the slab session.
4413    ///
4414    /// TO SEE THIS RED: as for
4415    /// [`an_h2_client_the_pool_cannot_build_releases_its_reservation`].
4416    #[test]
4417    fn a_socket_the_poller_refuses_releases_its_reservation() {
4418        assert_a_local_failure_releases_the_reservation(DialFault::RegisterRefused, |error| {
4419            matches!(error, BackendConnectionError::MaxSessionsMemory)
4420        });
4421    }
4422
4423    /// #1610: a session's first backend connection is stored without a heap
4424    /// allocation, and the connections still iterate in `Token` order however
4425    /// they were inserted and removed.
4426    ///
4427    /// `Router::backends` was a `BTreeMap`, whose first insertion allocates an
4428    /// 11-slot leaf of ~1.7 KiB connections, ~18.8 KiB, for the one backend a
4429    /// session almost always has.
4430    ///
4431    /// TO SEE THIS RED: in `BackendConnections::insert`, send the `None` arm
4432    /// to `self.rest.insert(token, connection)` instead of filling `first`
4433    /// (and let `remove` and `is_empty` read `rest` alone). The first
4434    /// insertion then makes one allocation.
4435    #[test]
4436    fn the_first_backend_connection_of_a_session_allocates_nothing() {
4437        use crate::test_allocations::allocations;
4438
4439        let fixture = routing_fixture();
4440        let mut backend_registry = BackendRegistry::default();
4441        let mut staged = |router: &mut Router, token: usize| {
4442            let (connection, peer) =
4443                staged_backend(&fixture.pool, &Staged::KeepAliveH1, &mut backend_registry);
4444            let before = allocations();
4445            let replaced = router.backends.insert(Token(token), connection);
4446            let allocated = allocations() - before;
4447            assert!(replaced.is_none(), "Token({token}) was not held yet");
4448            (allocated, peer)
4449        };
4450
4451        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
4452        let (first, _first_peer) = staged(&mut router, LOWEST_TOKEN + 2);
4453        assert_eq!(
4454            first, 0,
4455            "the first backend connection of a session must not allocate"
4456        );
4457
4458        // A lower token takes the inline slot, a higher one goes to the map.
4459        let (_, _lower_peer) = staged(&mut router, LOWEST_TOKEN);
4460        let (_, _higher_peer) = staged(&mut router, LOWEST_TOKEN + 5);
4461        let order = |router: &Router| router.backends.keys().map(|t| t.0).collect::<Vec<_>>();
4462        assert_eq!(
4463            order(&router),
4464            [LOWEST_TOKEN, LOWEST_TOKEN + 2, LOWEST_TOKEN + 5]
4465        );
4466        assert_eq!(router.backends.len(), 3);
4467
4468        // Removing the inline connection promotes the lowest remaining one.
4469        assert!(router.backends.remove(&Token(LOWEST_TOKEN)).is_some());
4470        assert_eq!(order(&router), [LOWEST_TOKEN + 2, LOWEST_TOKEN + 5]);
4471        assert!(router.backends.contains_key(&Token(LOWEST_TOKEN + 2)));
4472        assert!(!router.backends.contains_key(&Token(LOWEST_TOKEN)));
4473        assert!(router.backends.remove(&Token(LOWEST_TOKEN + 5)).is_some());
4474        assert!(router.backends.remove(&Token(LOWEST_TOKEN + 2)).is_some());
4475        assert!(router.backends.is_empty());
4476    }
4477}
4478
4479/// [`Router::backend_from_request`] against the embedder's real selector.
4480///
4481/// Question 12 of [#1340](https://github.com/sozu-proxy/sozu/issues/1340)
4482/// replaced the proxy handle this function took with a backend-set capability,
4483/// now a [`BackendSelector`] (#1684); it had no test before that change. Each
4484/// fixture owns its own `BackendMap` and `BackendRegistry`, so nothing here
4485/// depends on a proxy, a listener or a session — and, since selection no
4486/// longer dials, on any socket.
4487#[cfg(test)]
4488mod backend_selector_tests {
4489    use std::{
4490        cell::RefCell,
4491        net::SocketAddr,
4492        time::{Duration, Instant},
4493    };
4494
4495    use sozu_command::proto::command::{LoadBalancingAlgorithms, LoadMetric};
4496
4497    use super::Router;
4498    use crate::{
4499        backends::{Backend, BackendMap},
4500        protocol::{
4501            http::editor::HttpContext,
4502            mux::{BackendChange, BackendDelta, BackendId, BackendRegistry, RegistrySelector},
4503        },
4504        retry::{RetryAction, RetryPolicy},
4505    };
4506
4507    const CLUSTER: &str = "cluster-1340-q12";
4508    const STICKY_B: &str = "sticky-b";
4509
4510    /// Two backends under `policy` on connections. The addresses are never
4511    /// dialled: selection reserves, and only a caller of the selector connects.
4512    /// `LeastLoaded` is the default because round-robin would alternate
4513    /// whatever the counters said, and a test that passes without reading the
4514    /// load observes nothing.
4515    struct Fixture {
4516        backends: RefCell<BackendMap>,
4517        registry: BackendRegistry,
4518        router: Router,
4519        now: Instant,
4520    }
4521
4522    fn fixture_with(policy: LoadBalancingAlgorithms) -> Fixture {
4523        let now = Instant::now();
4524        let address = |port: u16| SocketAddr::from(([127, 0, 0, 1], port));
4525        let mut backends = BackendMap::with_seed(0x1684);
4526        backends.set_load_balancing_policy_for_cluster(
4527            CLUSTER,
4528            policy,
4529            Some(LoadMetric::Connections),
4530        );
4531        backends.add_backend(
4532            CLUSTER,
4533            Backend::new_at("backend-a", address(9501), None, None, None, now),
4534        );
4535        backends.add_backend(
4536            CLUSTER,
4537            Backend::new_at(
4538                "backend-b",
4539                address(9502),
4540                Some(STICKY_B.to_owned()),
4541                None,
4542                None,
4543                now,
4544            ),
4545        );
4546        Fixture {
4547            backends: RefCell::new(backends),
4548            registry: BackendRegistry::default(),
4549            router: Router::new(Duration::from_secs(10), Duration::from_secs(10)),
4550            now,
4551        }
4552    }
4553
4554    fn fixture() -> Fixture {
4555        fixture_with(LoadBalancingAlgorithms::LeastLoaded)
4556    }
4557
4558    fn request(cookie: Option<&str>) -> HttpContext {
4559        let mut context = HttpContext::new(
4560            rusty_ulid::Ulid::generate(),
4561            rusty_ulid::Ulid::generate(),
4562            crate::Protocol::HTTP,
4563            "127.0.0.1:80"
4564                .parse()
4565                .expect("test public address must parse"),
4566            None,
4567            "SOZUBALANCEID".to_owned(),
4568            "Sozu-Id".to_owned(),
4569            false,
4570            false,
4571        );
4572        context.sticky_session_found = cookie.map(ToOwned::to_owned);
4573        context
4574    }
4575
4576    impl Fixture {
4577        fn select(&mut self, should_stick: bool, context: &mut HttpContext) -> BackendId {
4578            let mut backends = self.backends.borrow_mut();
4579            let mut selector = RegistrySelector {
4580                backends: &mut backends,
4581                registry: &mut self.registry,
4582                now: self.now,
4583                exclude: &[],
4584            };
4585            let backend = self
4586                .router
4587                .backend_from_request(CLUSTER, should_stick, context, &mut selector)
4588                .expect("a cluster with two available backends must select");
4589            assert_eq!(
4590                context.backend_id.as_deref(),
4591                Some(&*backend.backend_id),
4592                "the request must be stamped with the backend it was reserved on"
4593            );
4594            backend
4595        }
4596
4597        fn backend(&self, id: &BackendId) -> std::cell::Ref<'_, Backend> {
4598            self.registry
4599                .handle(id)
4600                .expect("an id this fixture minted must resolve")
4601                .borrow()
4602        }
4603
4604        fn active_connections(&self, backend: &BackendId) -> usize {
4605            self.backend(backend).active_connections
4606        }
4607    }
4608
4609    /// Two streams linking in one pass must spread under `LeastLoaded`, and
4610    /// no dial happens in between.
4611    ///
4612    /// The second selection reads `active_connections`, which only the first
4613    /// selection's reservation can have raised: nothing here connects, and no
4614    /// delta drain runs. So this holds only if selecting reserves under the
4615    /// same borrow as the choice (#1684). A selector that stopped reserving —
4616    /// counting the connection at the dial instead — would send both streams
4617    /// to `backend-a`.
4618    ///
4619    /// TO SEE THIS RED: make `Backend::reserve_connection` return `Ok(())`
4620    /// without calling `inc_connections`.
4621    #[test]
4622    fn a_second_selection_observes_the_first_selections_reservation() {
4623        let mut fixture = fixture();
4624
4625        let first = fixture.select(false, &mut request(None));
4626        let second = fixture.select(false, &mut request(None));
4627
4628        assert_eq!(
4629            &*first.backend_id, "backend-a",
4630            "with both backends idle the first minimum wins"
4631        );
4632        assert_ne!(
4633            first.backend_id,
4634            second.backend_id,
4635            "the second selection landed on the first one's backend: the first \
4636             selection did not reserve its connection \
4637             (first={} active={}, second={} active={})",
4638            first.backend_id,
4639            fixture.active_connections(&first),
4640            second.backend_id,
4641            fixture.active_connections(&second),
4642        );
4643        assert_eq!(
4644            (
4645                fixture.active_connections(&first),
4646                fixture.active_connections(&second)
4647            ),
4648            (1, 1),
4649            "each selection must reserve exactly one connection on the backend it chose"
4650        );
4651    }
4652
4653    /// #1684: power-of-two-choices weighs every reservation too.
4654    ///
4655    /// With two backends the sample is the whole set, so each selection must
4656    /// take the one with fewer reserved connections, and a tie resolves by
4657    /// coin flip. Over any run of selections the two counts therefore never
4658    /// drift more than one apart, and together they count every selection.
4659    ///
4660    /// TO SEE THIS RED: make `Backend::reserve_connection` return `Ok(())`
4661    /// without calling `inc_connections`; nothing is reserved and the total
4662    /// stays at zero.
4663    #[test]
4664    fn power_of_two_weighs_every_reservation() {
4665        const SELECTIONS: usize = 64;
4666        let mut fixture = fixture_with(LoadBalancingAlgorithms::PowerOfTwo);
4667        let first = fixture.select(false, &mut request(None));
4668        let mut ids = vec![first];
4669        for selections in 2..=SELECTIONS {
4670            let before: Vec<usize> = ids
4671                .iter()
4672                .map(|id| fixture.active_connections(id))
4673                .collect();
4674            let picked = fixture.select(false, &mut request(None));
4675            if !ids.iter().any(|id| id.backend_id == picked.backend_id) {
4676                ids.push(picked.clone());
4677            }
4678            let loads: Vec<usize> = ids
4679                .iter()
4680                .map(|id| fixture.active_connections(id))
4681                .collect();
4682            assert_eq!(
4683                loads.iter().sum::<usize>(),
4684                selections,
4685                "{selections} selections must hold {selections} reservations, got {loads:?}"
4686            );
4687            if ids.len() == 2 {
4688                let (low, high) = (loads[0].min(loads[1]), loads[0].max(loads[1]));
4689                assert!(
4690                    high - low <= 1,
4691                    "power-of-two let the reservations drift apart: {loads:?} (before {before:?})"
4692                );
4693            }
4694        }
4695        assert_eq!(ids.len(), 2, "both backends must be used");
4696    }
4697
4698    /// #1684: a dial whose `connect(2)` fails releases its reservation and
4699    /// records the failure, once the ledger is drained.
4700    ///
4701    /// TO SEE THIS RED: drop `self.dec_connections()` from
4702    /// `Backend::release_failed_dial`; the reservation then outlives the
4703    /// failed dial.
4704    #[test]
4705    fn a_failed_dial_releases_its_reservation_and_records_the_failure() {
4706        let mut fixture = fixture();
4707        let backend = fixture.select(false, &mut request(None));
4708        assert_eq!(
4709            fixture.active_connections(&backend),
4710            1,
4711            "selection reserves"
4712        );
4713
4714        let mut deltas = vec![BackendDelta {
4715            slot: backend.slot(),
4716            change: BackendChange::DialFailed(fixture.now),
4717        }];
4718        fixture.registry.apply_all(&mut deltas, &fixture.backends);
4719
4720        assert!(deltas.is_empty(), "the drain empties the ledger");
4721        let released = fixture.backend(&backend);
4722        assert_eq!(
4723            released.active_connections, 0,
4724            "a failed dial must release the reservation selection took"
4725        );
4726        assert_eq!(released.failures, 1, "a failed dial must count one failure");
4727        assert_eq!(
4728            released.retry_policy.can_try(fixture.now),
4729            Some(RetryAction::WAIT),
4730            "a failed dial must arm the backoff at the instant it failed"
4731        );
4732    }
4733
4734    /// A cookie pins the request only when its frontend sticks.
4735    ///
4736    /// The call this replaced matched on `(frontend_should_stick, cookie)`
4737    /// and sent `(false, Some(_))` to the load balancer; the selector now takes
4738    /// an [`super::Affinity`] built by the caller, so that arm is the one a
4739    /// signature change could flip without a compile error.
4740    #[test]
4741    fn a_cookie_pins_only_a_frontend_that_sticks() {
4742        let mut unpinned = fixture();
4743        let mut context = request(Some(STICKY_B));
4744        let backend = unpinned.select(false, &mut context);
4745        assert_eq!(
4746            &*backend.backend_id, "backend-a",
4747            "a frontend that does not stick must ignore the client's cookie"
4748        );
4749        assert_eq!(
4750            context.sticky_session, None,
4751            "a frontend that does not stick must not answer with a cookie"
4752        );
4753
4754        let mut pinned = fixture();
4755        let mut context = request(Some(STICKY_B));
4756        let backend = pinned.select(true, &mut context);
4757        assert_eq!(
4758            &*backend.backend_id, "backend-b",
4759            "a sticky frontend must follow the cookie to its backend"
4760        );
4761        assert_eq!(context.sticky_session.as_deref(), Some(STICKY_B));
4762        assert_eq!(
4763            pinned.active_connections(&backend),
4764            1,
4765            "a sticky selection reserves like any other"
4766        );
4767
4768        let mut fallback = fixture();
4769        let mut context = request(None);
4770        let backend = fallback.select(true, &mut context);
4771        assert_eq!(
4772            context.sticky_session.as_deref(),
4773            Some(&*backend.backend_id),
4774            "a sticky frontend with no cookie answers with the chosen \
4775             backend's id when it has no sticky id of its own"
4776        );
4777    }
4778
4779    /// [`fixture`] under `HRW`, the policy that reads the affinity key.
4780    fn hrw_fixture() -> Fixture {
4781        fixture_with(LoadBalancingAlgorithms::Hrw)
4782    }
4783
4784    /// A request whose `plan_connect` derived `key`.
4785    fn keyed_request(cookie: Option<&str>, key: u64) -> HttpContext {
4786        let mut context = request(cookie);
4787        context.affinity_key = Some(key);
4788        context
4789    }
4790
4791    /// #524: the key a request carries reaches the load balancer, so `HRW`
4792    /// sends one key to one backend selection after selection.
4793    ///
4794    /// TO SEE THIS RED: pass `None` instead of `context.affinity_key` to
4795    /// `BackendSelector::select` in `Router::backend_from_request`;
4796    /// `HRW` then round-robins and the second selection of each key lands on
4797    /// the other backend.
4798    #[test]
4799    fn a_keyed_request_stays_on_one_backend_under_hrw() {
4800        let mut fixture = hrw_fixture();
4801        for key in 0..16u64 {
4802            let first = fixture.select(false, &mut keyed_request(None, key));
4803            for _ in 0..3 {
4804                let again = fixture.select(false, &mut keyed_request(None, key));
4805                assert_eq!(
4806                    again.backend_id, first.backend_id,
4807                    "key {key} must stay on one backend under HRW"
4808                );
4809            }
4810        }
4811    }
4812
4813    /// #524: a sticky cookie naming a live backend wins over the key; the key
4814    /// only decides where a request the cookie did not pin lands.
4815    #[test]
4816    fn a_sticky_cookie_wins_over_the_affinity_key() {
4817        let mut fixture = hrw_fixture();
4818        let key = (0..64u64)
4819            .find(|&key| {
4820                &*fixture
4821                    .select(false, &mut keyed_request(None, key))
4822                    .backend_id
4823                    == "backend-a"
4824            })
4825            .expect("HRW sends some of 64 keys to each of two backends");
4826
4827        let mut context = keyed_request(Some(STICKY_B), key);
4828        let backend = fixture.select(true, &mut context);
4829        assert_eq!(
4830            &*backend.backend_id, "backend-b",
4831            "the cookie must pin the request whatever its key says"
4832        );
4833
4834        let mut context = keyed_request(None, key);
4835        let backend = fixture.select(true, &mut context);
4836        assert_eq!(
4837            &*backend.backend_id, "backend-a",
4838            "without a cookie a sticky frontend follows the key"
4839        );
4840    }
4841
4842    /// #1579: stamping a selected backend onto the request allocates nothing.
4843    ///
4844    /// `Router::backend_from_request` runs once per backend dial and writes
4845    /// the chosen backend's id into `HttpContext::backend_id`. The id arrives
4846    /// as the `Rc<str>` of the selected `BackendId`, so the stamp is a
4847    /// reference-count increment. The selector is a stub that always answers
4848    /// one id, so the count covers the router's own work and not the load
4849    /// balancer. It needs no socket: a selector does not dial (#1684).
4850    ///
4851    /// TO SEE THIS RED: stamp `Some(selected.backend.backend_id.to_string().into())`
4852    /// instead of the `Rc` clone; the assertion then reports two allocations
4853    /// per dial.
4854    #[test]
4855    fn stamping_a_dialled_backend_allocates_nothing() {
4856        use std::hint::black_box;
4857
4858        use super::{Affinity, BackendSelector, SelectedBackend};
4859        use crate::{backends::BackendError, test_allocations::allocations};
4860
4861        const DIALS: usize = 64;
4862
4863        /// Always answers `backend`, minted as a simulator would mint it.
4864        struct StubSelector {
4865            backend: BackendId,
4866        }
4867
4868        impl BackendSelector for StubSelector {
4869            fn select(
4870                &mut self,
4871                _cluster_id: &str,
4872                _affinity: Affinity<'_>,
4873                _key: Option<u64>,
4874            ) -> Result<SelectedBackend, BackendError> {
4875                Ok(SelectedBackend {
4876                    backend: self.backend.clone(),
4877                    sticky_session: None,
4878                })
4879            }
4880        }
4881
4882        let mut selector = StubSelector {
4883            backend: BackendId::new(
4884                0,
4885                std::rc::Rc::from("stamped-backend"),
4886                SocketAddr::from(([127, 0, 0, 1], 9503)),
4887            ),
4888        };
4889        let mut router = Router::new(Duration::from_secs(10), Duration::from_secs(10));
4890        let mut context = request(None);
4891
4892        // Warm-up: the first stamp fills the slot the later ones overwrite.
4893        drop(
4894            router
4895                .backend_from_request(CLUSTER, false, &mut context, &mut selector)
4896                .expect("the stub selector always selects"),
4897        );
4898        let mut allocated = 0;
4899        for _ in 0..DIALS {
4900            let before = allocations();
4901            let selected = router.backend_from_request(
4902                black_box(CLUSTER),
4903                false,
4904                black_box(&mut context),
4905                &mut selector,
4906            );
4907            allocated += allocations() - before;
4908            drop(selected.expect("the stub selector always selects"));
4909        }
4910
4911        assert_eq!(context.backend_id.as_deref(), Some("stamped-backend"));
4912        assert_eq!(
4913            allocated, 0,
4914            "{DIALS} dials made {allocated} heap allocations stamping the \
4915             backend onto the request, expected none"
4916        );
4917    }
4918}
4919
4920/// [`affinity_key`]: which request bytes key `HRW` and `MAGLEV`, and that
4921/// deriving the key costs no allocation (#524).
4922#[cfg(test)]
4923mod affinity_key_tests {
4924    use std::net::{IpAddr, Ipv4Addr, SocketAddr};
4925
4926    use sozu_command::proto::command::{Cluster, LoadBalancingAlgorithms};
4927
4928    use super::{StickyCookie, affinity_key};
4929    use crate::{
4930        load_balancing::{affinity_key_from_ip, affinity_key_from_value},
4931        pool::Pool,
4932        protocol::mux::GenericHttpStream,
4933    };
4934
4935    /// A listener sticky cookie the requests below do not carry.
4936    const NO_STICKY: StickyCookie<'static> = StickyCookie {
4937        name: "SOZUBALANCEID",
4938        value: None,
4939    };
4940
4941    const CLIENT: SocketAddr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10)), 40_000);
4942
4943    /// A request kawa parsed from `request`, on its own pool buffer.
4944    fn parsed(pool: &mut Pool, request: &[u8]) -> GenericHttpStream {
4945        let mut kawa: GenericHttpStream = kawa::Kawa::new(
4946            kawa::Kind::Request,
4947            kawa::Buffer::new(
4948                pool.checkout()
4949                    .expect("the test pool must hand out a buffer"),
4950            ),
4951        );
4952        kawa.storage.space()[..request.len()].copy_from_slice(request);
4953        kawa.storage.fill(request.len());
4954        kawa::h1::parse(&mut kawa, &mut kawa::h1::NoCallbacks);
4955        assert!(kawa.is_main_phase(), "premise: the request must parse");
4956        kawa
4957    }
4958
4959    fn cluster(
4960        policy: LoadBalancingAlgorithms,
4961        header: Option<&str>,
4962        cookie: Option<&str>,
4963    ) -> Cluster {
4964        Cluster {
4965            cluster_id: "affinity".to_owned(),
4966            load_balancing: policy as i32,
4967            affinity_header: header.map(ToOwned::to_owned),
4968            affinity_cookie: cookie.map(ToOwned::to_owned),
4969            ..Default::default()
4970        }
4971    }
4972
4973    const REQUEST: &[u8] = b"GET / HTTP/1.1\r\nHost: example.com\r\nx-tenant: acme\r\n\
4974        Cookie: theme=dark; tenant=globex\r\nX-Empty: \r\n\r\n";
4975
4976    /// Only `HRW`, `MAGLEV` and a sharded cluster read a key; every other
4977    /// cluster gets `None`, pays nothing and selects exactly as before.
4978    ///
4979    /// TO SEE THIS RED: drop the `shard_percent` arm of
4980    /// `cluster_reads_affinity_key`; the sharded round-robin cluster then
4981    /// derives no key and is silently not sharded.
4982    #[test]
4983    fn only_hrw_maglev_and_sharded_clusters_derive_a_key() {
4984        let mut pool = Pool::with_capacity(1, 1, 4096);
4985        let front = parsed(&mut pool, REQUEST);
4986        for policy in [
4987            LoadBalancingAlgorithms::RoundRobin,
4988            LoadBalancingAlgorithms::Random,
4989            LoadBalancingAlgorithms::LeastLoaded,
4990            LoadBalancingAlgorithms::PowerOfTwo,
4991        ] {
4992            assert_eq!(
4993                affinity_key(
4994                    &cluster(policy, Some("X-Tenant"), None),
4995                    &front,
4996                    Some(CLIENT),
4997                    NO_STICKY
4998                ),
4999                None,
5000                "{policy:?} does not read a key"
5001            );
5002        }
5003        // #524: a sharded cluster reads the key under any policy, to rank
5004        // the client's shard by.
5005        let sharded_round_robin = Cluster {
5006            shard_percent: Some(25),
5007            ..cluster(LoadBalancingAlgorithms::RoundRobin, None, None)
5008        };
5009        assert_eq!(
5010            affinity_key(&sharded_round_robin, &front, Some(CLIENT), NO_STICKY),
5011            Some(affinity_key_from_ip(CLIENT.ip())),
5012            "a sharded cluster derives a key whatever its policy"
5013        );
5014        for policy in [
5015            LoadBalancingAlgorithms::Hrw,
5016            LoadBalancingAlgorithms::Maglev,
5017        ] {
5018            assert_eq!(
5019                affinity_key(
5020                    &cluster(policy, None, None),
5021                    &front,
5022                    Some(CLIENT),
5023                    NO_STICKY
5024                ),
5025                Some(affinity_key_from_ip(CLIENT.ip())),
5026                "{policy:?} keys on the source IP by default"
5027            );
5028        }
5029    }
5030
5031    /// The configured header keys the request, its name matched without
5032    /// regard to case; a header the request lacks, or carries empty, falls
5033    /// back to the source IP; with no source address either there is no key.
5034    #[test]
5035    fn a_configured_header_keys_the_request_and_the_source_ip_is_the_fallback() {
5036        let mut pool = Pool::with_capacity(1, 1, 4096);
5037        let front = parsed(&mut pool, REQUEST);
5038        let hrw = LoadBalancingAlgorithms::Hrw;
5039        assert_eq!(
5040            affinity_key(
5041                &cluster(hrw, Some("X-Tenant"), None),
5042                &front,
5043                Some(CLIENT),
5044                NO_STICKY
5045            ),
5046            Some(affinity_key_from_value(b"acme"))
5047        );
5048        assert_eq!(
5049            affinity_key(
5050                &cluster(hrw, Some("X-Absent"), None),
5051                &front,
5052                Some(CLIENT),
5053                NO_STICKY
5054            ),
5055            Some(affinity_key_from_ip(CLIENT.ip()))
5056        );
5057        assert_eq!(
5058            affinity_key(
5059                &cluster(hrw, Some("X-Empty"), None),
5060                &front,
5061                Some(CLIENT),
5062                NO_STICKY
5063            ),
5064            Some(affinity_key_from_ip(CLIENT.ip())),
5065            "an empty value keys nothing and falls back to the source IP"
5066        );
5067        assert_eq!(
5068            affinity_key(
5069                &cluster(hrw, Some("X-Absent"), None),
5070                &front,
5071                None,
5072                NO_STICKY
5073            ),
5074            None,
5075            "no header and no source address: no key, and the policy round-robins"
5076        );
5077    }
5078
5079    /// The configured cookie keys the request, its name matched exactly
5080    /// (RFC 6265 cookie names are case-sensitive).
5081    #[test]
5082    fn a_configured_cookie_keys_the_request() {
5083        let mut pool = Pool::with_capacity(1, 1, 4096);
5084        let front = parsed(&mut pool, REQUEST);
5085        let maglev = LoadBalancingAlgorithms::Maglev;
5086        assert_eq!(
5087            affinity_key(
5088                &cluster(maglev, None, Some("tenant")),
5089                &front,
5090                Some(CLIENT),
5091                NO_STICKY
5092            ),
5093            Some(affinity_key_from_value(b"globex"))
5094        );
5095        assert_eq!(
5096            affinity_key(
5097                &cluster(maglev, None, Some("Tenant")),
5098                &front,
5099                Some(CLIENT),
5100                NO_STICKY
5101            ),
5102            Some(affinity_key_from_ip(CLIENT.ip())),
5103            "a cookie name differing in case is another cookie"
5104        );
5105    }
5106
5107    /// #524: deriving the key allocates nothing: the header blocks and the
5108    /// cookie jar are read where they lie and hashed in place.
5109    ///
5110    /// TO SEE THIS RED: copy the value out before hashing it in
5111    /// `affinity_key`, e.g. `affinity_key_from_value(&value.to_vec())`; each
5112    /// derivation then makes one allocation.
5113    #[test]
5114    fn deriving_an_affinity_key_allocates_nothing() {
5115        use std::hint::black_box;
5116
5117        use crate::test_allocations::allocations;
5118
5119        const DERIVATIONS: usize = 64;
5120        let mut pool = Pool::with_capacity(1, 1, 4096);
5121        let front = parsed(&mut pool, REQUEST);
5122        let clusters = [
5123            cluster(LoadBalancingAlgorithms::Hrw, None, None),
5124            cluster(LoadBalancingAlgorithms::Hrw, Some("X-Tenant"), None),
5125            cluster(LoadBalancingAlgorithms::Maglev, None, Some("tenant")),
5126            cluster(LoadBalancingAlgorithms::Maglev, Some("X-Absent"), None),
5127        ];
5128        let mut allocated = 0;
5129        for cluster in &clusters {
5130            for _ in 0..DERIVATIONS {
5131                let before = allocations();
5132                let key = affinity_key(
5133                    black_box(cluster),
5134                    black_box(&front),
5135                    Some(CLIENT),
5136                    NO_STICKY,
5137                );
5138                allocated += allocations() - before;
5139                assert!(key.is_some());
5140            }
5141        }
5142        assert_eq!(
5143            allocated,
5144            0,
5145            "{} derivations made {allocated} heap allocations, expected none",
5146            DERIVATIONS * clusters.len()
5147        );
5148    }
5149
5150    /// A cluster keying on the listener's own sticky-session cookie reads the
5151    /// value captured before that cookie was elided from the jar; without the
5152    /// capture it would never find the cookie and always fall back to the
5153    /// source IP.
5154    ///
5155    /// TO SEE THIS RED: drop the sticky-name arm of `affinity_key`, so the
5156    /// lookup reads only the jar.
5157    #[test]
5158    fn an_affinity_cookie_named_like_the_sticky_cookie_reads_its_captured_value() {
5159        let mut pool = Pool::with_capacity(1, 1, 4096);
5160        // The sticky cookie is already gone from the jar, as
5161        // `on_request_headers` leaves it.
5162        let front = parsed(&mut pool, REQUEST);
5163        let hrw = cluster(LoadBalancingAlgorithms::Hrw, None, Some("SOZUBALANCEID"));
5164        assert_eq!(
5165            affinity_key(
5166                &hrw,
5167                &front,
5168                Some(CLIENT),
5169                StickyCookie {
5170                    name: "SOZUBALANCEID",
5171                    value: Some("backend-7"),
5172                },
5173            ),
5174            Some(affinity_key_from_value(b"backend-7"))
5175        );
5176        assert_eq!(
5177            affinity_key(&hrw, &front, Some(CLIENT), NO_STICKY),
5178            Some(affinity_key_from_ip(CLIENT.ip())),
5179            "no sticky cookie on the request: the source IP keys it"
5180        );
5181    }
5182}