sozu_lib/lib.rs
1//! ## What this library does
2//!
3//! This library provides tools to build and start HTTP, HTTPS and TCP reverse proxies.
4//!
5//! The proxies handles network polling, HTTP parsing, TLS in a fast single threaded event
6//! loop.
7//!
8//! Each proxy is designed to receive configuration changes at runtime instead of
9//! reloading from a file regularly. The event loop runs in its own thread
10//! and receives commands through a message queue.
11//!
12//! ## Difference with the crate `sozu`
13//!
14//! To create several workers and manage them all at once (which is the most common way to
15//! use Sōzu), the crate `sozu` is more indicated than using the lib directly.
16//!
17//! The crate `sozu` provides a binary called the main process.
18//! The main process uses `sozu_lib` to start and manage workers.
19//! Each worker can handle HTTP, HTTPS and TCP traffic.
20//! The main process receives synchronizes the state of all workers, using UNIX sockets
21//! and custom channels to communicate with them.
22//! The main process itself is is configurable with a file, and has a CLI.
23//!
24//! ## How to use this library directly
25//!
26//! This documentation here explains how to write a binary that will start a single Sōzu
27//! worker and give it orders. The method has two steps:
28//!
29//! 1. Starts a Sōzu worker in a distinct thread
30//! 2. sends instructions to the worker on a UNIX socket via a Sōzu channel
31//!
32//! ### How to start a Sōzu worker
33//!
34//! Before creating an HTTP proxy, we first need to create an HTTP listener.
35//! The listener is an abstraction around a TCP socket provided by the kernel.
36//! We need the `sozu_command_lib` to build a listener.
37//!
38//! ```
39//! use sozu_command_lib::{config::ListenerBuilder, proto::command::SocketAddress};
40//!
41//! let address = SocketAddress::new_v4(127,0,0,1,8080);
42//! let http_listener = ListenerBuilder::new_http(address)
43//! .to_http(None)
44//! .expect("Could not create HTTP listener");
45//! ```
46//!
47//! The `http_listener` is of the type `HttpListenerConfig`, that we can be sent to the worker
48//! to start the proxy.
49//!
50//! Then create a pair of channels to communicate with the proxy.
51//! The channel is a wrapper around a unix socket.
52//!
53//! ```ignore
54//! use sozu_command_lib::{
55//! channel::Channel,
56//! proto::command::{WorkerRequest, WorkerResponse},
57//! };
58//!
59//! let (mut command_channel, proxy_channel): (
60//! Channel<WorkerRequest, WorkerResponse>,
61//! Channel<WorkerResponse, WorkerRequest>,
62//! ) = Channel::generate(1000, 10000).expect("should create a channel");
63//!```
64//!
65//! Here, the `command_channel` end is blocking, it sends `WorkerRequest`s and receives
66//! `WorkerResponses`, while the `proxy_channel` end is non-blocking, and the types are reversed.
67//! Writing the types here isn't even necessary thanks to the compiler,
68//! but it brings the point accross.
69//!
70//! You can now launch the worker in a separate thread, providing the HTTP listener config,
71//! the proxy end of the channel, and your custom number of buffers and their size:
72//!
73//! ```ignore
74//! use std::thread;
75//!
76//! let worker_thread_join_handle = thread::spawn(move || {
77//! let max_buffers = 500;
78//! let buffer_size = 16384;
79//! sozu_lib::http::testing::start_http_worker(http_listener, proxy_channel, max_buffers, buffer_size);
80//! });
81//! ```
82//!
83//! ### Send orders
84//!
85//! Once the thread is launched, the proxy worker will start its event loop and handle
86//! events on the listening interface and port specified when building the HTTP Listener.
87//! Since no frontends or backends were specified for the proxy, it will receive
88//! the connections, parse the requests, then send a default (but configurable)
89//! answer.
90//!
91//! Before defining a frontend and backends, we need to define a cluster, which describes
92//! a routing configuration. A cluster contains:
93//!
94//! - one frontend
95//! - one or several backends
96//! - routing rules
97//!
98//! A cluster is identified by its `cluster_id`, which will be used to define frontends
99//! and backends later on.
100//!
101//! ```
102//! use sozu_command_lib::proto::command::{Cluster, LoadBalancingAlgorithms};
103//!
104//! let cluster = Cluster {
105//! cluster_id: "my-cluster".to_string(),
106//! sticky_session: false,
107//! https_redirect: false,
108//! load_balancing: LoadBalancingAlgorithms::RoundRobin as i32,
109//! answer_503: Some("A custom forbidden message".to_string()),
110//! ..Default::default()
111//! };
112//! ```
113//!
114//! The defaults are sensible, so we could define only the `cluster_id`.
115//!
116//! We can now define a frontend. A frontend is a way to recognize a request and match
117//! it to a `cluster_id`, depending on the hostname and the beginning of the URL path.
118//! The `address` field must match the one of the HTTP listener we defined before:
119//!
120//! ```
121//! use std::collections::BTreeMap;
122//!
123//! use sozu_command_lib::proto::command::{PathRule, RequestHttpFrontend, RulePosition, SocketAddress};
124//!
125//! let http_front = RequestHttpFrontend {
126//! cluster_id: Some("my-cluster".to_string()),
127//! address: SocketAddress::new_v4(127,0,0,1,8080),
128//! hostname: "example.com".to_string(),
129//! path: PathRule::prefix(String::from("/")),
130//! position: RulePosition::Pre.into(),
131//! tags: BTreeMap::from([
132//! ("owner".to_owned(), "John".to_owned()),
133//! ("id".to_owned(), "my-own-http-front".to_owned()),
134//! ]),
135//! ..Default::default()
136//! };
137//! ```
138//!
139//! The `tags` are keys and values that will appear in the access logs,
140//! which can come in handy.
141//!
142//! Now let's define a backend.
143//! A backend is an instance of a backend application we want to route traffic to.
144//! The `address` field must match the IP and port of the backend server.
145//!
146//! ```
147//! use sozu_command_lib::proto::command::{AddBackend, LoadBalancingParams, SocketAddress};
148//!
149//! let http_backend = AddBackend {
150//! cluster_id: "my-cluster".to_string(),
151//! backend_id: "test-backend".to_string(),
152//! address: SocketAddress::new_v4(127,0,0,1,8000),
153//! load_balancing_parameters: Some(LoadBalancingParams::default()),
154//! ..Default::default()
155//! };
156//! ```
157//!
158//! A cluster can have multiple backend servers, and they can be added or
159//! removed while the proxy is running. If a backend is removed from the configuration
160//! while the proxy is handling a request to that server, it will finish that
161//! request and stop sending new traffic to that server.
162//!
163//!
164//! Now we can use the other end of the channel to send all these requests to the worker,
165//! using the WorkerRequest type:
166//!
167//! ```ignore
168//! use sozu_command_lib::{
169//! proto::command::{Request, request::RequestType, WorkerRequest},
170//! };
171//!
172//! command_channel
173//! .write_message(&WorkerRequest {
174//! id: String::from("add-the-cluster"),
175//! content: RequestType::AddCluster(cluster).into(),
176//! })
177//! .expect("Could not send AddHttpFrontend request");
178//!
179//! command_channel
180//! .write_message(&WorkerRequest {
181//! id: String::from("add-the-frontend"),
182//! content: RequestType::AddHttpFrontend(http_front).into(),
183//! })
184//! .expect("Could not send AddHttpFrontend request");
185//!
186//! command_channel
187//! .write_message(&WorkerRequest {
188//! id: String::from("add-the-backend"),
189//! content: RequestType::AddBackend(http_backend).into(),
190//! })
191//! .expect("Could not send AddBackend request");
192//!
193//! println!("HTTP -> {:?}", command_channel.read_message());
194//! println!("HTTP -> {:?}", command_channel.read_message());
195//! println!("HTTP -> {:?}", command_channel.read_message());
196//! ```
197//!
198//!
199//! The event loop of the worker will process these instructions and add them to
200//! its state, and the worker will send back an acknowledgement
201//! message.
202//!
203//! Now we can let the worker thread run in the background:
204//!
205//! ```ignore
206//! let _ = worker_thread_join_handle.join();
207//! ```
208//!
209//! Here is the complete example for reference, it matches the `examples/http.rs` example:
210//!
211//! ```
212//! #[macro_use]
213//! extern crate sozu_command_lib;
214//!
215//! use std::{collections::BTreeMap, env, io::stdout, thread};
216//!
217//! use anyhow::Context;
218//! use sozu_command_lib::{
219//! channel::Channel,
220//! config::ListenerBuilder,
221//! logging::setup_default_logging,
222//! proto::command::{
223//! request::RequestType, AddBackend, Cluster, LoadBalancingAlgorithms, LoadBalancingParams,
224//! PathRule, Request, RequestHttpFrontend, RulePosition, SocketAddress,WorkerRequest,
225//! },
226//! };
227//!
228//! fn main() -> anyhow::Result<()> {
229//! setup_default_logging(true, "info", "EXAMPLE").with_context(|| "could not setup logging")?;
230//!
231//! info!("starting up");
232//!
233//! let http_listener = ListenerBuilder::new_http(SocketAddress::new_v4(127,0,0,1,8080))
234//! .to_http(None)
235//! .expect("Could not create HTTP listener");
236//!
237//! let (mut command_channel, proxy_channel) =
238//! Channel::generate(1000, 10000).with_context(|| "should create a channel")?;
239//!
240//! let worker_thread_join_handle = thread::spawn(move || {
241//! let max_buffers = 500;
242//! let buffer_size = 16384;
243//! sozu_lib::http::testing::start_http_worker(http_listener, proxy_channel, max_buffers, buffer_size)
244//! .expect("The worker could not be started, or shut down");
245//! });
246//!
247//! let cluster = Cluster {
248//! cluster_id: "my-cluster".to_string(),
249//! sticky_session: false,
250//! https_redirect: false,
251//! load_balancing: LoadBalancingAlgorithms::RoundRobin as i32,
252//! answer_503: Some("A custom forbidden message".to_string()),
253//! ..Default::default()
254//! };
255//!
256//! let http_front = RequestHttpFrontend {
257//! cluster_id: Some("my-cluster".to_string()),
258//! address: SocketAddress::new_v4(127,0,0,1,8080),
259//! hostname: "example.com".to_string(),
260//! path: PathRule::prefix(String::from("/")),
261//! position: RulePosition::Pre.into(),
262//! tags: BTreeMap::from([
263//! ("owner".to_owned(), "John".to_owned()),
264//! ("id".to_owned(), "my-own-http-front".to_owned()),
265//! ]),
266//! ..Default::default()
267//! };
268//! let http_backend = AddBackend {
269//! cluster_id: "my-cluster".to_string(),
270//! backend_id: "test-backend".to_string(),
271//! address: SocketAddress::new_v4(127,0,0,1,8000),
272//! load_balancing_parameters: Some(LoadBalancingParams::default()),
273//! ..Default::default()
274//! };
275//!
276//! command_channel
277//! .write_message(&WorkerRequest {
278//! id: String::from("add-the-cluster"),
279//! content: RequestType::AddCluster(cluster).into(),
280//! })
281//! .expect("Could not send AddHttpFrontend request");
282//!
283//! command_channel
284//! .write_message(&WorkerRequest {
285//! id: String::from("add-the-frontend"),
286//! content: RequestType::AddHttpFrontend(http_front).into(),
287//! })
288//! .expect("Could not send AddHttpFrontend request");
289//!
290//! command_channel
291//! .write_message(&WorkerRequest {
292//! id: String::from("add-the-backend"),
293//! content: RequestType::AddBackend(http_backend).into(),
294//! })
295//! .expect("Could not send AddBackend request");
296//!
297//! println!("HTTP -> {:?}", command_channel.read_message());
298//! println!("HTTP -> {:?}", command_channel.read_message());
299//!
300//! // uncomment to let it run in the background
301//! // let _ = worker_thread_join_handle.join();
302//! info!("good bye");
303//! Ok(())
304//! }
305//! ```
306//!
307//! # Protected health-check headers
308//!
309//! An `HTTP` health check may send custom request headers whose values are
310//! credentials. The ordinary `SetHealthCheck` order does not carry a value: it
311//! names the headers with a keyed fingerprint, and a health check that names
312//! headers whose values the worker does not hold sends no probe. Each skipped
313//! probe is counted by the `health_check.missing_secret` metric; it is neither a
314//! success nor a failure and does not change the health of a backend.
315//!
316//! The values reach a worker only through the protected transaction of
317//! [`protected_health_check`]: a `Prepare` order, the single carrier of the clear
318//! values, then `Commit`, `Abort` or `Release`, each acknowledged. An embedder
319//! that drives a worker directly, as in the example above, is that boundary: it
320//! must ask the worker whether it supports the protocol before it hands it a
321//! value. The values live in memory only, in buffers that Sōzu zeroizes when it
322//! releases them; this does not reach copies the allocator, the kernel, a socket
323//! buffer or swap may hold. They never appear in the configuration, the state, a
324//! query response, a log line or `Debug` output.
325
326#[macro_use]
327extern crate sozu_command_lib as sozu_command;
328
329#[macro_use]
330pub mod util;
331#[macro_use]
332pub mod metrics;
333
334pub mod backends;
335pub mod crypto;
336pub mod features;
337pub mod health_check;
338pub mod http;
339pub mod load_balancing;
340pub mod pool;
341pub mod protected_health_check;
342pub mod protocol;
343pub mod retry;
344pub mod router;
345pub mod socket;
346pub mod timer;
347pub mod tls;
348
349/// Linux zero-copy TCP forwarder. Used by `protocol::pipe::Pipe` when
350/// the listener is `Protocol::TCP` and the `splice` feature is enabled.
351#[cfg(all(target_os = "linux", feature = "splice"))]
352pub(crate) mod splice;
353
354pub mod server;
355pub mod tcp;
356pub mod udp;
357
358pub mod https;
359
360use std::{
361 cell::RefCell,
362 collections::{BTreeMap, HashMap},
363 fmt::{self, Display, Formatter},
364 net::SocketAddr,
365 rc::Rc,
366 str,
367 time::{Duration, Instant, SystemTime},
368};
369
370use backends::BackendError;
371use hex::FromHexError;
372use mio::{Interest, Token, net::TcpStream};
373use protocol::http::{answers::HttpAnswers, answers::TemplateError, parser::Method};
374use router::RouterError;
375use socket::ServerBindError;
376use sozu_command::{
377 AsStr, ObjectKind,
378 logging::{CachedTags, LogContext},
379 proto::command::{
380 Cluster, ForwardedHeaders, ListenerType, RequestHttpFrontend, WorkerRequest, WorkerResponse,
381 },
382 ready::Ready,
383 state::ClusterId,
384};
385use tls::CertificateResolverError;
386
387use crate::{metrics::names, router::RouteResult};
388
389/// Anything that can be registered in mio (subscribe to kernel events)
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391pub enum Protocol {
392 HTTP,
393 HTTPS,
394 TCP,
395 UDP,
396 HTTPListen,
397 HTTPSListen,
398 TCPListen,
399 UDPListen,
400 Channel,
401 Metrics,
402 Timer,
403}
404
405/// trait that must be implemented by listeners and client sessions
406pub trait ProxySession {
407 /// indicates the protocol associated with the session
408 ///
409 /// this is used to distinguish sessions from listenrs, channels, metrics
410 /// and timers
411 fn protocol(&self) -> Protocol;
412 /// if a session received an event or can still execute, the event loop will
413 /// call this method. Its result indicates if it can still execute, needs to
414 /// connect to a backend server, close the session
415 fn ready(&mut self, session: Rc<RefCell<dyn ProxySession>>) -> SessionIsToBeClosed;
416 /// if the event loop got an event for a token associated with the session,
417 /// it will call this method on the session
418 fn update_readiness(&mut self, token: Token, events: Ready);
419 /// close a session, frontend and backend sockets,
420 /// remove the entries from the session manager slab
421 fn close(&mut self);
422 /// if a timeout associated with the session triggers, the event loop will
423 /// call this method with the timeout's token
424 fn timeout(&mut self, t: Token) -> SessionIsToBeClosed;
425 /// last time the session got an event
426 fn last_event(&self) -> Instant;
427 /// display the session's internal state (for debugging purpose)
428 fn print_session(&self);
429 /// get the token associated with the frontend
430 fn frontend_token(&self) -> Token;
431 /// tell the session it has to shut down if possible
432 ///
433 /// if the session handles HTTP requests, it will not close until the response
434 /// is completely sent back to the client
435 fn shutting_down(&mut self) -> SessionIsToBeClosed;
436 /// Best-effort identifier of the cluster currently routed to by this
437 /// session. Returns `None` for `ListenSession` (no per-session
438 /// cluster), and for client sessions before routing has resolved.
439 /// H2 sessions multiplex many streams over one frontend token and may
440 /// touch several clusters; the returned value is whichever cluster
441 /// the session most recently keep-alive'd to. Used for log/metric
442 /// attribution, not for accounting (the tracker keeps the canonical
443 /// per-stream `(cluster, IP)` set).
444 fn cluster_id(&self) -> Option<ClusterId> {
445 None
446 }
447 /// Source address as observed by Sōzu, with proxy-protocol awareness.
448 /// HTTP/HTTPS/TCP client sessions return the parsed PROXY-protocol
449 /// source when present, else `peer_addr`. `ListenSession` returns
450 /// `None`. Used to attribute per-(cluster, source-IP) tracking and
451 /// access logs to the real client behind a layer-4 PROXY frontend.
452 fn session_address(&self) -> Option<SocketAddr> {
453 None
454 }
455 /// Whether the session has work that only a [`Self::ready`] pass can do
456 /// although no socket event will arrive to trigger one.
457 ///
458 /// Asked by `Server::timeout` after a timeout that left the session open.
459 /// A backend whose connect timed out is the case: the timer is the only
460 /// signal a blackholed dial ever produces, and failing it over to another
461 /// backend (dialling, registering the new socket) needs the `ready` path
462 /// (sozu-proxy/sozu#1800).
463 fn needs_ready_pass(&self) -> bool {
464 false
465 }
466 /// Whether the last [`Self::ready`] pass stopped at its iteration budget
467 /// while still forwarding bytes, with work left that no socket event will
468 /// announce again. Reading it clears it.
469 ///
470 /// `Server::ready` and `Server::timeout` queue such a session, and
471 /// `Server::resume_yielded_sessions` runs its `ready` pass again after the
472 /// next poll, so a fast transfer neither holds the single-threaded worker
473 /// nor stalls (sozu-proxy/sozu#1906). Only the mux-backed HTTP and HTTPS
474 /// sessions yield.
475 fn yielded(&mut self) -> bool {
476 false
477 }
478 /// Whether this session holds a backend connection `target` names, which
479 /// makes it one of the sessions a `CloseSessions` request closes.
480 ///
481 /// `false` for listeners, channels and every session that has no backend
482 /// connection yet, such as one still in its TLS handshake.
483 fn targets(&self, _target: &SessionTarget) -> bool {
484 false
485 }
486}
487
488/// The backend connections a `CloseSessions` request selects: those to
489/// `cluster_id`, narrowed by `backend_id` and `address` when they are set.
490#[derive(Clone, Debug, PartialEq, Eq)]
491pub struct SessionTarget {
492 pub cluster_id: String,
493 pub backend_id: Option<String>,
494 pub address: Option<SocketAddr>,
495}
496
497impl SessionTarget {
498 /// Whether a backend connection to `backend_id` at `address` in
499 /// `cluster_id` is one this target selects. Every field the target sets
500 /// must match; a connection that does not know a field the target sets
501 /// does not match.
502 pub fn matches(
503 &self,
504 cluster_id: &str,
505 backend_id: Option<&str>,
506 address: Option<SocketAddr>,
507 ) -> bool {
508 self.cluster_id == cluster_id
509 && self
510 .backend_id
511 .as_deref()
512 .is_none_or(|wanted| backend_id == Some(wanted))
513 && self.address.is_none_or(|wanted| address == Some(wanted))
514 }
515}
516
517impl From<&sozu_command::proto::command::CloseSessions> for SessionTarget {
518 fn from(request: &sozu_command::proto::command::CloseSessions) -> Self {
519 SessionTarget {
520 cluster_id: request.cluster_id.to_owned(),
521 backend_id: request.backend_id.to_owned(),
522 address: request.address.map(SocketAddr::from),
523 }
524 }
525}
526
527#[macro_export]
528macro_rules! branch {
529 (if $($value:ident)? == $expected:ident { $($then:tt)* } else { $($else:tt)* }) => {
530 macro_rules! expect {
531 ($expected) => {$($then)*};
532 ($a:ident) => {$($else)*};
533 () => {$($else)*}
534 }
535 expect!($($value)?);
536 };
537 (if $($value:ident)? == $expected:ident { $($then:tt)* } ) => {
538 macro_rules! expect {
539 ($expected) => {$($then)*};
540 }
541 expect!($($value)?);
542 };
543}
544
545#[macro_export]
546macro_rules! fallback {
547 ({} $($default:tt)*) => {
548 $($default)*
549 };
550 ({$($value:tt)+} $($default:tt)*) => {
551 $($value)+
552 };
553}
554
555#[macro_export]
556macro_rules! StateMachineBuilder {
557 (
558 ($d:tt)
559 $(#[$($state_macros:tt)*])*
560 enum $state_name:ident $(impl $trait:ident)? {
561 $($(#[$($variant_macros:tt)*])*
562 $variant_name:ident($state:ty$(,$($aux:ty),+)?) $(-> $override:expr)?),+ $(,)?
563 }
564 ) => {
565 /// A summary of the last valid State
566 #[derive(Clone, Copy, Debug)]
567 pub enum StateMarker {
568 $($variant_name,)+
569 }
570
571 $(#[$($state_macros)*])*
572 #[allow(clippy::large_enum_variant)]
573 pub enum $state_name {
574 $(
575 $(#[$($variant_macros)*])*
576 $variant_name($state$(,$($aux),+)?),
577 )+
578 /// Informs about upgrade failure, contains a summary the last valid State
579 FailedUpgrade(StateMarker),
580 }
581
582 macro_rules! _fn_impl {
583 ($function:ident(&$d($mut:ident)?, self $d(,$arg_name:ident: $arg_type:ty)*) $d(-> $ret:ty)? $d(| $marker:tt => $fail:expr)?) => {
584 fn $function(&$d($mut)? self $d(,$arg_name: $arg_type)*) $d(-> $ret)? {
585 match self {
586 $($state_name::$variant_name(_state, ..) => $crate::fallback!({$($override)?} _state.$function($d($arg_name),*)),)+
587 $state_name::FailedUpgrade($crate::fallback!({$d($marker)?} _)) => $crate::fallback!({$d($fail)?} unreachable!())
588 }
589 }
590 };
591 }
592
593 impl $state_name {
594 /// Informs about the last valid State before upgrade failure
595 fn marker(&self) -> StateMarker {
596 match self {
597 $($state_name::$variant_name(..) => StateMarker::$variant_name,)+
598 $state_name::FailedUpgrade(marker) => *marker,
599 }
600 }
601 /// Returns wether or not the State is FailedUpgrade
602 fn failed(&self) -> bool {
603 match self {
604 $state_name::FailedUpgrade(_) => true,
605 _ => false,
606 }
607 }
608 /// Gives back an owned version of the State,
609 /// leaving a FailedUpgrade in its place.
610 /// The FailedUpgrade retains the marker of the previous State.
611 fn take(&mut self) -> $state_name {
612 let mut owned_state = $state_name::FailedUpgrade(self.marker());
613 std::mem::swap(&mut owned_state, self);
614 owned_state
615 }
616 _fn_impl!{front_socket(&, self) -> &mio::net::TcpStream}
617 }
618
619 $crate::branch!{
620 if $($trait)? == SessionState {
621 impl SessionState for $state_name {
622 _fn_impl!{ready(&mut, self, session: Rc<RefCell<dyn ProxySession>>, proxy: Rc<RefCell<dyn L7Proxy>>, metrics: &mut SessionMetrics) -> SessionResult}
623 _fn_impl!{update_readiness(&mut, self, token: Token, events: Ready)}
624 _fn_impl!{timeout(&mut, self, token: Token, metrics: &mut SessionMetrics) -> StateResult}
625 _fn_impl!{cancel_timeouts(&mut, self)}
626 _fn_impl!{print_state(&, self, context: &str) | marker => error!("{} Session(FailedUpgrade({:?}))", context, marker)}
627 _fn_impl!{close(&mut, self, proxy: Rc<RefCell<dyn L7Proxy>>, metrics: &mut SessionMetrics) | _ => {}}
628 _fn_impl!{shutting_down(&mut, self) -> SessionIsToBeClosed | _ => true}
629 _fn_impl!{targets(&, self, target: &$crate::SessionTarget) -> bool | _ => false}
630 }
631 } else {}
632 }
633 };
634 ($($tt:tt)+) => {
635 StateMachineBuilder!{($) $($tt)+}
636 }
637}
638
639pub trait ListenerHandler {
640 fn get_addr(&self) -> &SocketAddr;
641
642 fn get_tags(&self, key: &str) -> Option<&CachedTags>;
643
644 fn get_concatenated_tags(&self, key: &str) -> Option<&str> {
645 self.get_tags(key).map(|tags| tags.concatenated.as_str())
646 }
647
648 fn set_tags(&mut self, key: String, tags: Option<BTreeMap<String, String>>);
649
650 fn protocol(&self) -> Protocol;
651
652 fn public_address(&self) -> SocketAddr;
653}
654
655#[derive(thiserror::Error, Debug)]
656pub enum FrontendFromRequestError {
657 #[error("Could not parse hostname from '{host}': {error}")]
658 HostParse { host: String, error: String },
659 #[error("invalid remaining chars after hostname. Host: {0}")]
660 InvalidCharsAfterHost(String),
661 #[error("no cluster: {0}")]
662 NoClusterFound(RouterError),
663}
664
665pub trait L7ListenerHandler {
666 fn get_sticky_name(&self) -> &str;
667
668 /// Name of the correlation header Sozu injects into every request and
669 /// response body. Default: `"Sozu-Id"`. Operators can rebrand via the
670 /// `sozu_id_header` listener config knob.
671 fn get_sozu_id_header(&self) -> &str {
672 "Sozu-Id"
673 }
674
675 fn get_connect_timeout(&self) -> u32;
676
677 /// retrieve a frontend by parsing a request's hostname, uri and method,
678 /// with its headers for the frontends that carry header predicates
679 /// (sozu-proxy/sozu#1253)
680 fn frontend_from_request(
681 &self,
682 host: &str,
683 uri: &str,
684 method: &Method,
685 headers: &dyn router::RequestHeaders,
686 ) -> Result<RouteResult, FrontendFromRequestError>;
687
688 /// retrieve the listener's configured HTTP answers (templates)
689 fn get_answers(&self) -> &Rc<RefCell<HttpAnswers>>;
690
691 /// H2 flood detection thresholds from the listener config.
692 /// Returns the default config when the listener does not provide custom values.
693 fn get_h2_flood_config(&self) -> protocol::mux::H2FloodConfig {
694 protocol::mux::H2FloodConfig::default()
695 }
696
697 /// H2 connection tuning from the listener config.
698 /// Returns the default config when the listener does not provide custom values.
699 fn get_h2_connection_config(&self) -> protocol::mux::H2ConnectionConfig {
700 protocol::mux::H2ConnectionConfig::default()
701 }
702
703 /// Whether requests must have their `:authority` / `Host` exact-match
704 /// the TLS SNI negotiated at handshake (CWE-346 / CWE-444).
705 ///
706 /// Defaults to `true` — the safe setting that closes the
707 /// CWE-346 / CWE-444 cross-SNI smuggling vector. Operators can opt
708 /// out per-listener via `HttpsListenerConfig::strict_sni_binding =
709 /// false` when cross-SNI routing is explicitly required. Plaintext
710 /// HTTP listeners return the default value; they never have an SNI
711 /// to compare against, so the routing-layer check short-circuits on
712 /// `tls_server_name: None`.
713 fn get_strict_sni_binding(&self) -> bool {
714 true
715 }
716
717 /// Whether to strip any client-supplied `X-Real-IP` header from
718 /// forwarded requests (anti-spoofing).
719 ///
720 /// Defaults to `false` — preserves the historical pass-through
721 /// behaviour. Operators opt in via
722 /// `HttpListenerConfig::elide_x_real_ip = true` (and the equivalent on
723 /// HTTPS listeners). Independent of [`Self::get_send_x_real_ip`]: the
724 /// two flags can be combined freely (anti-spoof only, send only, both,
725 /// or neither). The elision branch lives in
726 /// `HttpContext::on_request_headers`, so it covers H1 and H2 alike.
727 fn get_elide_x_real_ip(&self) -> bool {
728 false
729 }
730
731 /// Whether to append a proxy-generated `X-Real-IP` header carrying the
732 /// connection peer IP (post-PROXY-v2 unwrap, i.e. the original client
733 /// IP) to every forwarded request.
734 ///
735 /// Defaults to `false` — preserves the historical no-injection
736 /// behaviour. Operators opt in via
737 /// `HttpListenerConfig::send_x_real_ip = true` (and the equivalent on
738 /// HTTPS listeners). Independent of [`Self::get_elide_x_real_ip`]: the
739 /// two flags can be combined freely. The injection branch lives next
740 /// to the existing X-Forwarded-For / Forwarded synthesis in
741 /// `HttpContext::on_request_headers`.
742 fn get_send_x_real_ip(&self) -> bool {
743 false
744 }
745
746 /// Which forwarding header family to add to every forwarded request:
747 /// `X-Forwarded-*`, RFC 7239 `Forwarded`, both, or none.
748 ///
749 /// Defaults to [`ForwardedHeaders::Both`] — the historical behaviour.
750 /// Operators choose another mode via
751 /// `HttpListenerConfig::forwarded_headers` (and the equivalent on HTTPS
752 /// listeners). Independent of [`Self::get_elide_x_real_ip`] and
753 /// [`Self::get_send_x_real_ip`]. The emission and elision branches live
754 /// in `HttpContext::on_request_headers`, so they cover H1 and H2 alike.
755 fn get_forwarded_headers(&self) -> ForwardedHeaders {
756 ForwardedHeaders::Both
757 }
758
759 /// Per-stream idle timeout for H2 connections. An open stream that makes
760 /// no forward progress for this duration is cancelled (RST_STREAM / CANCEL).
761 /// Mitigates slow-multiplex Slowloris where a client keeps connection-level
762 /// activity high (resetting the connection idle timer on every frame) while
763 /// pinning streams for the full nominal connection timeout.
764 ///
765 /// Listeners inherit `max(30s, back_timeout)` when `h2_stream_idle_timeout_seconds`
766 /// is absent so operators who raised the socket-level backend budget do not
767 /// have to duplicate the value here; the 30 s floor preserves the baseline
768 /// slow-multiplex mitigation when `back_timeout` is shorter. Set the knob
769 /// explicitly to cap the per-stream deadline below `back_timeout` (useful
770 /// when under a slow-multiplex attack).
771 fn get_h2_stream_idle_timeout(&self) -> std::time::Duration {
772 std::time::Duration::from_secs(30)
773 }
774
775 /// Wall-clock budget granted to in-flight H2 streams after soft-stop sent
776 /// the initial `GOAWAY(NO_ERROR)`. Once the deadline elapses the mux
777 /// transitions to a forced close (final GOAWAY + session teardown).
778 ///
779 /// Returning `None` disables the forced close entirely — shutdown waits
780 /// for every stream to drain naturally. Returning `Some(d)` enforces the
781 /// budget. Default: `Some(Duration::from_secs(5))` (matches the historic
782 /// hard-coded 5 s deadline). Listeners expose the
783 /// `h2_graceful_shutdown_deadline_seconds` knob; value `0` maps to `None`.
784 fn get_h2_graceful_shutdown_deadline(&self) -> Option<std::time::Duration> {
785 Some(std::time::Duration::from_secs(5))
786 }
787}
788
789#[derive(Clone, Copy, Debug, PartialEq, Eq)]
790pub enum BackendConnectionStatus {
791 NotConnected,
792 Connecting(Instant),
793 Connected,
794}
795
796impl BackendConnectionStatus {
797 pub fn is_connecting(&self) -> bool {
798 matches!(self, BackendConnectionStatus::Connecting(_))
799 }
800}
801
802#[derive(Debug, PartialEq, Eq)]
803pub enum BackendConnectAction {
804 New,
805 Reuse,
806 Replace,
807}
808
809#[derive(thiserror::Error, Debug)]
810pub enum BackendConnectionError {
811 #[error("Not found: {0:?}")]
812 NotFound(ObjectKind),
813 #[error("Too many connections on cluster {0:?}")]
814 MaxConnectionRetries(Option<ClusterId>),
815 #[error("the sessions slab has reached maximum capacity")]
816 MaxSessionsMemory,
817 #[error("error from the backend: {0}")]
818 Backend(BackendError),
819 #[error("failed to retrieve the cluster: {0}")]
820 RetrieveClusterError(RetrieveClusterError),
821 #[error("maximum number of buffers reached")]
822 MaxBuffers,
823 /// Per-(cluster, source-IP) connection limit reached. The protocol
824 /// layer translates this into HTTP 429 Too Many Requests (with an
825 /// optional `Retry-After`) for HTTP/HTTPS sessions, or a graceful TCP
826 /// close for raw TCP. The `cluster_id` is included so log/metric
827 /// pipelines can attribute the rejection.
828 #[error("per-(cluster, source-IP) connection limit reached for cluster {cluster_id:?}")]
829 TooManyConnectionsPerIp { cluster_id: ClusterId },
830 /// A stale-upstream replay could not be carried out. `Stream::retry_buffer`
831 /// holds H1 wire bytes captured under the FIRST attempt's routing
832 /// decision, so the replay is valid only while that decision still holds:
833 /// the cluster must still be known and must still speak H1. When it does
834 /// not, the protocol layer answers 502 Bad Gateway — the same answer the
835 /// stream received before the replay existed (sozu-proxy/sozu#1442) —
836 /// rather than framing raw H1 text as an HTTP/2 DATA payload or
837 /// re-routing a front kawa whose blocks are already drained.
838 #[error("stale-upstream replay refused: {0}")]
839 ReplayRefused(&'static str),
840}
841
842/// Route-extraction failures raised by `kawa_h1::editor::HttpContext` and
843/// consumed by the mux router.
844#[derive(thiserror::Error)]
845pub enum RetrieveClusterError {
846 #[error("No method given")]
847 NoMethod,
848 #[error("No host given")]
849 NoHost,
850 #[error("No path given")]
851 NoPath,
852 #[error("unauthorized route")]
853 UnauthorizedRoute,
854 #[error("{0}")]
855 RetrieveFrontend(FrontendFromRequestError),
856 #[error("HTTPS redirect required")]
857 HttpsRedirect,
858 /// The frontend's `RedirectPolicy::SilentClose` asks for no response at
859 /// all (sozu-proxy/sozu#767): the H1 connection is closed, the H2 stream
860 /// reset.
861 #[error("silent close requested by the frontend")]
862 SilentClose,
863 /// The HTTP `:authority` / `Host` host does not match the TLS SNI that was
864 /// negotiated for this connection, which would cross the TLS trust boundary.
865 /// Maps to HTTP 421 Misdirected Request (RFC 9110 §15.5.20).
866 #[error("TLS SNI does not match HTTP authority: sni_bytes={} authority_bytes={}", .sni.len(), .authority.len())]
867 SniAuthorityMismatch { sni: String, authority: String },
868}
869
870impl fmt::Debug for RetrieveClusterError {
871 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
872 fmt::Display::fmt(self, f)
873 }
874}
875
876/// Used in sessions
877#[derive(Debug, PartialEq, Eq)]
878pub enum AcceptError {
879 IoError,
880 TooManySessions,
881 WouldBlock,
882 RegisterError,
883 WrongSocketAddress,
884 BufferCapacityReached,
885}
886
887/// What a proxy would do with a listening socket handed to its
888/// `activate_listener(address)`.
889///
890/// `Server::notify_activate_listener` asks BEFORE it pulls a descriptor out of
891/// the SCM table, because `Listeners::get_*` removes the entry as it hands it
892/// over while `activate()` returns on its `if self.active` guard without
893/// consuming anything. Taking a descriptor that nothing will consume drops the
894/// owning wrapper on the floor and closes the inherited socket unused — the
895/// accept backlog or receive buffer with it (sozu#1342).
896#[derive(Clone, Copy, Debug, PartialEq, Eq)]
897pub enum InheritedSocketFate {
898 /// A listener exists at the address and is not active: it adopts the
899 /// socket. Take the descriptor.
900 Adopted,
901 /// A listener exists at the address and is already active on a socket of
902 /// its own. `activate()` short-circuits, so nothing consumes this
903 /// descriptor **now**. Close it deliberately.
904 ///
905 /// Not because it could never be adopted: `give_back_listener` clears
906 /// `active` and keeps the listener object, so `DeactivateListener` followed
907 /// by `ActivateListener` — an operator-reachable feature, covered by
908 /// `e2e/src/tests/listener_reactivation_tests.rs` — brings this address back
909 /// to [`Adopted`](Self::Adopted). Closing here is therefore irreversible:
910 /// `scm_listeners` is filled once in `Server::new` and never refilled, so a
911 /// reactivation after this point binds a fresh socket.
912 ///
913 /// It is a deliberate trade, not a claim about the lifetime. The only thing
914 /// this descriptor carries is the connections queued on it at hand-off, and
915 /// their value decays — the peers behind them time out or reset while the
916 /// address is served by the listener's own socket. The cost of keeping it is
917 /// continuous instead: the descriptor is the retiring worker's listening
918 /// socket, still bound with `SO_REUSEPORT` and registered with no event
919 /// loop, so the kernel is expected to keep giving it a share of incoming
920 /// connections that nobody accepts. (That last step follows from
921 /// `SO_REUSEPORT` load-balancing semantics; nothing here measures it.) A
922 /// decaying backlog is not worth an indefinite share of new connections.
923 Refused,
924 /// No listener at the address yet. Keep the descriptor in the table: an
925 /// `AddListener` + `ActivateListener` pair can still arrive and adopt it,
926 /// which is exactly what an upgrade does when the initial state carries the
927 /// listeners inactive and activates them afterwards.
928 Unclaimed,
929}
930
931/// returned by the HTTP, HTTPS and TCP listeners
932#[derive(thiserror::Error)]
933pub enum ListenerError {
934 #[error("failed to handle certificate request, got a resolver error, {0}")]
935 Resolver(CertificateResolverError),
936 #[error("failed to parse pem, {0}")]
937 PemParse(String),
938 #[error(
939 "failed to parse template key_bytes={key_bytes}: {1}",
940 key_bytes = .0.len()
941 )]
942 TemplateParse(String, TemplateError),
943 #[error("failed to build rustls context, {0}")]
944 BuildRustls(String),
945 #[error("failed to configure client certificate authentication (mTLS): {0}")]
946 ClientAuth(String),
947 #[error("could not activate listener with address {address:?}: {error}")]
948 Activation { address: SocketAddr, error: String },
949 #[error("Could not register listener socket: {0}")]
950 SocketRegistration(std::io::Error),
951 #[error("could not add frontend: {0}")]
952 AddFrontend(RouterError),
953 #[error("could not remove frontend: {0}")]
954 RemoveFrontend(RouterError),
955 #[error("invalid value for field '{field}': {reason}")]
956 InvalidValue {
957 field: &'static str,
958 reason: &'static str,
959 },
960 /// `UpdateHttpsListenerConfig.hsts` was present but its `enabled`
961 /// field was unset. Per the partial-update contract, `enabled` is
962 /// the explicit-disambiguator between "explicit disable" (false) and
963 /// "explicit enable" (true); the patch handler refuses an
964 /// `enabled = None` block rather than silently picking one.
965 #[error(
966 "UpdateHttpsListenerConfig.hsts is present but `enabled` is unset; the partial-update \
967 contract requires `enabled` whenever the `hsts` block is present"
968 )]
969 HstsEnabledRequired,
970}
971
972impl fmt::Debug for ListenerError {
973 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
974 fmt::Display::fmt(self, f)
975 }
976}
977
978/// Lift control-plane validation errors into listener-level errors so the
979/// worker can surface the same message without duplicating the match.
980/// Non-`InvalidValue` variants fall back to a generic `InvalidValue` — they
981/// are not expected on the worker's `update_config` path (state lookups
982/// happen on the master) but we avoid panicking if one slips through.
983impl From<sozu_command::state::StateError> for ListenerError {
984 fn from(err: sozu_command::state::StateError) -> Self {
985 match err {
986 sozu_command::state::StateError::InvalidValue { field, reason } => {
987 ListenerError::InvalidValue { field, reason }
988 }
989 _ => ListenerError::InvalidValue {
990 field: "state",
991 reason: "unexpected state error on worker path",
992 },
993 }
994 }
995}
996
997/// Returned by the HTTP, HTTPS and TCP proxies
998#[derive(thiserror::Error, Debug)]
999pub enum ProxyError {
1000 #[error("error while soft stopping {proxy_protocol} proxy: {error}")]
1001 SoftStop {
1002 proxy_protocol: String,
1003 error: String,
1004 },
1005 #[error("error while hard stopping {proxy_protocol} proxy: {error}")]
1006 HardStop {
1007 proxy_protocol: String,
1008 error: String,
1009 },
1010 /// Names the listener key: the address, plus `%<interface>` for a
1011 /// listener bound to one.
1012 #[error("found no listener at {0}")]
1013 NoListenerFound(sozu_command::listener_key::ListenerKey),
1014 #[error("a listener is already present for this token")]
1015 ListenerAlreadyPresent,
1016 #[error("could not add listener: {0}")]
1017 AddListener(ListenerError),
1018 #[error("could not add cluster: {0}")]
1019 AddCluster(ListenerError),
1020 #[error("failed to activate listener with address {address:?}: {listener_error}")]
1021 ListenerActivation {
1022 address: SocketAddr,
1023 listener_error: ListenerError,
1024 },
1025 #[error("can not add frontend {front:?}: {error}")]
1026 WrongInputFrontend {
1027 front: Box<RequestHttpFrontend>,
1028 error: String,
1029 },
1030 #[error("could not add frontend: {0}")]
1031 AddFrontend(ListenerError),
1032 #[error("could not remove frontend: {0}")]
1033 RemoveFrontend(ListenerError),
1034 #[error("could not add certificate: {0}")]
1035 AddCertificate(CertificateResolverError),
1036 #[error("could not remove certificate: {0}")]
1037 RemoveCertificate(CertificateResolverError),
1038 #[error("could not replace certificate: {0}")]
1039 ReplaceCertificate(CertificateResolverError),
1040 #[error("wrong certificate fingerprint: {0}")]
1041 WrongCertificateFingerprint(FromHexError),
1042 #[error("this request is not supported by the proxy")]
1043 UnsupportedMessage,
1044 #[error("failed to acquire the lock, {0}")]
1045 Lock(String),
1046 #[error("could not bind to socket {0:?}: {1}")]
1047 BindToSocket(SocketAddr, ServerBindError),
1048 #[error("error registering socket of listener: {0}")]
1049 RegisterListener(std::io::Error),
1050 #[error("the listener is not activated")]
1051 UnactivatedListener,
1052 /// HSTS (RFC 6797) was attached to a frontend on a plain-HTTP
1053 /// listener. RFC 6797 §7.2 forbids `Strict-Transport-Security` on
1054 /// plaintext-HTTP responses; the worker rejects the request rather
1055 /// than ship a non-conformant policy. The TOML loader rejects the
1056 /// same shape at config-load time
1057 /// (`command/src/config.rs::ConfigError::HstsOnPlainHttp`); this
1058 /// arm catches the same misconfiguration when the request reaches
1059 /// the worker over the IPC channel without going through the TOML
1060 /// path (e.g. via `sozu frontend http add`).
1061 #[error(
1062 "HSTS is only valid on HTTPS frontends; rejecting AddHttpFrontend with hsts.enabled = \
1063 true on address {0:?} (RFC 6797 §7.2)"
1064 )]
1065 HstsOnPlainHttp(SocketAddr),
1066 /// A TCP `AddTcpFrontend` would corrupt this listener's SNI/ALPN
1067 /// routing invariants (sozu-proxy/sozu#1279): mixing a no-SNI
1068 /// catch-all with SNI-scoped routes, an `alpn`-scoped frontend with no
1069 /// `sni` to preread against, or an ALPN protocol / catch-all that
1070 /// overlaps an existing route on the same `(address, sni)`. TOML
1071 /// config-load already rejects the same shapes
1072 /// (`command/src/config.rs::ConfigError`), but `AddTcpFrontend` can
1073 /// also arrive directly over the command socket or via `LoadState`
1074 /// replay of a hand-edited/stale state file, bypassing config.rs
1075 /// entirely — see `TcpListener::validate_new_tcp_front`
1076 /// (`lib/src/tcp.rs`).
1077 #[error("rejected AddTcpFrontend on listener {address:?}: {reason}")]
1078 InvalidTcpFrontend { address: SocketAddr, reason: String },
1079}
1080
1081use self::server::ListenToken;
1082pub trait ProxyConfiguration {
1083 fn notify(&mut self, message: WorkerRequest) -> WorkerResponse;
1084 /// Accept one connection on the listener behind `token`, returning the
1085 /// socket together with the peer address `accept(2)` reported for it.
1086 fn accept(&mut self, token: ListenToken) -> Result<(TcpStream, SocketAddr), AcceptError>;
1087 /// Build the session for a socket [`Self::accept`] returned. `peer` is the
1088 /// address `accept(2)` returned with it, so the session never has to ask
1089 /// the kernel again with `getpeername(2)`.
1090 fn create_session(
1091 &mut self,
1092 socket: TcpStream,
1093 peer: SocketAddr,
1094 token: ListenToken,
1095 wait_time: Duration,
1096 proxy: Rc<RefCell<Self>>,
1097 // should we insert the tags here?
1098 ) -> Result<(), AcceptError>;
1099}
1100
1101pub trait L7Proxy {
1102 fn kind(&self) -> ListenerType;
1103
1104 fn register_socket(
1105 &self,
1106 socket: &mut TcpStream,
1107 token: Token,
1108 interest: Interest,
1109 ) -> Result<(), std::io::Error>;
1110
1111 fn add_session(&self, session: Rc<RefCell<dyn ProxySession>>) -> Token;
1112
1113 /// Remove the session from the session manager slab.
1114 /// Returns true if the session was actually there before deletion
1115 fn remove_session(&self, token: Token) -> bool;
1116
1117 fn clusters(&self) -> &HashMap<ClusterId, Cluster>;
1118
1119 /// Access the worker's [`crate::server::SessionManager`] for per-(cluster, source-IP)
1120 /// connection-limit accounting. The mux uses this to track / untrack
1121 /// stream-granular `(cluster_id, ip)` entries and consult the
1122 /// `cluster_ip_at_limit` gate before each backend connect.
1123 fn sessions(&self) -> Rc<RefCell<crate::server::SessionManager>>;
1124}
1125
1126#[derive(Debug, PartialEq, Eq)]
1127pub enum RequiredEvents {
1128 FrontReadBackNone,
1129 FrontWriteBackNone,
1130 FrontReadWriteBackNone,
1131 FrontNoneBackNone,
1132 FrontReadBackRead,
1133 FrontWriteBackRead,
1134 FrontReadWriteBackRead,
1135 FrontNoneBackRead,
1136 FrontReadBackWrite,
1137 FrontWriteBackWrite,
1138 FrontReadWriteBackWrite,
1139 FrontNoneBackWrite,
1140 FrontReadBackReadWrite,
1141 FrontWriteBackReadWrite,
1142 FrontReadWriteBackReadWrite,
1143 FrontNoneBackReadWrite,
1144}
1145
1146impl RequiredEvents {
1147 pub fn front_readable(&self) -> bool {
1148 matches!(
1149 *self,
1150 RequiredEvents::FrontReadBackNone
1151 | RequiredEvents::FrontReadWriteBackNone
1152 | RequiredEvents::FrontReadBackRead
1153 | RequiredEvents::FrontReadWriteBackRead
1154 | RequiredEvents::FrontReadBackWrite
1155 | RequiredEvents::FrontReadWriteBackWrite
1156 | RequiredEvents::FrontReadBackReadWrite
1157 | RequiredEvents::FrontReadWriteBackReadWrite
1158 )
1159 }
1160
1161 pub fn front_writable(&self) -> bool {
1162 matches!(
1163 *self,
1164 RequiredEvents::FrontWriteBackNone
1165 | RequiredEvents::FrontReadWriteBackNone
1166 | RequiredEvents::FrontWriteBackRead
1167 | RequiredEvents::FrontReadWriteBackRead
1168 | RequiredEvents::FrontWriteBackWrite
1169 | RequiredEvents::FrontReadWriteBackWrite
1170 | RequiredEvents::FrontWriteBackReadWrite
1171 | RequiredEvents::FrontReadWriteBackReadWrite
1172 )
1173 }
1174
1175 pub fn back_readable(&self) -> bool {
1176 matches!(
1177 *self,
1178 RequiredEvents::FrontReadBackRead
1179 | RequiredEvents::FrontWriteBackRead
1180 | RequiredEvents::FrontReadWriteBackRead
1181 | RequiredEvents::FrontNoneBackRead
1182 | RequiredEvents::FrontReadBackReadWrite
1183 | RequiredEvents::FrontWriteBackReadWrite
1184 | RequiredEvents::FrontReadWriteBackReadWrite
1185 | RequiredEvents::FrontNoneBackReadWrite
1186 )
1187 }
1188
1189 pub fn back_writable(&self) -> bool {
1190 matches!(
1191 *self,
1192 RequiredEvents::FrontReadBackWrite
1193 | RequiredEvents::FrontWriteBackWrite
1194 | RequiredEvents::FrontReadWriteBackWrite
1195 | RequiredEvents::FrontNoneBackWrite
1196 | RequiredEvents::FrontReadBackReadWrite
1197 | RequiredEvents::FrontWriteBackReadWrite
1198 | RequiredEvents::FrontReadWriteBackReadWrite
1199 | RequiredEvents::FrontNoneBackReadWrite
1200 )
1201 }
1202}
1203
1204/// Signals transitions between states of a given Protocol
1205#[derive(Debug, PartialEq, Eq)]
1206pub enum StateResult {
1207 /// Signals to the Protocol to close its backend
1208 CloseBackend,
1209 /// Signals to the parent Session to close itself
1210 CloseSession,
1211 /// Signals to the Protocol to connect to backend
1212 ConnectBackend,
1213 /// Signals to the Protocol to continue
1214 Continue,
1215 /// Signals to the parent Session to upgrade to the next Protocol
1216 Upgrade,
1217}
1218
1219/// Signals transitions between states of a given Session
1220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1221pub enum SessionResult {
1222 /// Signals to the Session to close itself
1223 Close,
1224 /// Signals to the Session to continue
1225 Continue,
1226 /// Signals to the Session to upgrade its Protocol
1227 Upgrade,
1228}
1229
1230#[derive(Debug, PartialEq, Eq)]
1231pub enum SocketType {
1232 Listener,
1233 FrontClient,
1234}
1235
1236type SessionIsToBeClosed = bool;
1237
1238#[derive(Clone)]
1239pub struct Readiness {
1240 /// the current readiness
1241 pub event: Ready,
1242 /// the readiness we wish to attain
1243 pub interest: Ready,
1244}
1245
1246impl Display for Readiness {
1247 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1248 let i = &mut [b'-'; 5];
1249 let r = &mut [b'-'; 5];
1250 let mixed = &mut [b'-'; 5];
1251
1252 display_ready(i, self.interest);
1253 display_ready(r, self.event);
1254 display_ready(mixed, self.interest & self.event);
1255
1256 write!(
1257 f,
1258 "I({:?})&R({:?})=M({:?})",
1259 String::from_utf8_lossy(i),
1260 String::from_utf8_lossy(r),
1261 String::from_utf8_lossy(mixed)
1262 )
1263 }
1264}
1265
1266impl Default for Readiness {
1267 fn default() -> Self {
1268 Self::new()
1269 }
1270}
1271
1272impl Readiness {
1273 /// Mask of every bit `Ready` defines (READABLE | WRITABLE | ERROR | HUP |
1274 /// WRITE_CLOSED).
1275 /// Any bit outside this set in `event` or `interest` is a corrupted
1276 /// readiness word — checked by [`Self::check_invariants`]. Not
1277 /// `#[cfg(debug_assertions)]`-gated: it is read from inside `debug_assert!`s
1278 /// whose arguments must still compile in release (HARD RULE 2 / E0425).
1279 const KNOWN_BITS: Ready = Ready(
1280 Ready::READABLE.0
1281 | Ready::WRITABLE.0
1282 | Ready::ERROR.0
1283 | Ready::HUP.0
1284 | Ready::WRITE_CLOSED.0,
1285 );
1286
1287 pub const fn new() -> Readiness {
1288 Readiness {
1289 event: Ready::EMPTY,
1290 interest: Ready::EMPTY,
1291 }
1292 }
1293
1294 /// Cross-field invariant sweep: neither `event` nor `interest` may carry a
1295 /// bit `Ready` does not define. A stray bit would silently widen
1296 /// `filter_interest`'s mask and wake (or starve) a session on a phantom
1297 /// readiness. Cheap enough to call as a postcondition from every mutator.
1298 #[cfg(debug_assertions)]
1299 fn check_invariants(&self) {
1300 debug_assert_eq!(
1301 self.event & Self::KNOWN_BITS,
1302 self.event,
1303 "Readiness.event carries a bit outside READABLE|WRITABLE|ERROR|HUP|WRITE_CLOSED"
1304 );
1305 debug_assert_eq!(
1306 self.interest & Self::KNOWN_BITS,
1307 self.interest,
1308 "Readiness.interest carries a bit outside READABLE|WRITABLE|ERROR|HUP|WRITE_CLOSED"
1309 );
1310 }
1311
1312 pub fn reset(&mut self) {
1313 self.event = Ready::EMPTY;
1314 self.interest = Ready::EMPTY;
1315 // Post-condition: a reset clears *both* words — a half-reset leaves a
1316 // session armed on stale interest after teardown.
1317 debug_assert!(
1318 self.event.is_empty() && self.interest.is_empty(),
1319 "reset must clear both event and interest"
1320 );
1321 #[cfg(debug_assertions)]
1322 self.check_invariants();
1323 }
1324
1325 /// filters the readiness we actually want
1326 pub fn filter_interest(&self) -> Ready {
1327 // Pre-condition: both source words must be well-formed before we mask —
1328 // a stray bit upstream would leak through the intersection and wake (or
1329 // starve) a session on a phantom readiness.
1330 #[cfg(debug_assertions)]
1331 self.check_invariants();
1332 let filtered = self.event & self.interest;
1333 // Post-condition: the result is a subset of the recognized bits and can
1334 // only contain bits the session both saw AND asked for.
1335 debug_assert_eq!(
1336 filtered & Self::KNOWN_BITS,
1337 filtered,
1338 "filter_interest must not yield an unknown bit"
1339 );
1340 debug_assert!(
1341 self.interest.contains(filtered) && self.event.contains(filtered),
1342 "filtered readiness must be present in both interest and event"
1343 );
1344 filtered
1345 }
1346
1347 /// Signal that the socket has buffered data to write (e.g., TLS internal
1348 /// buffers) that won't generate a new epoll WRITABLE event.
1349 pub fn signal_pending_write(&mut self) {
1350 // Snapshot the unrelated bits (everything but WRITABLE) so we can prove
1351 // we flipped exactly the WRITABLE bit. `Ready` exposes no `!`, so mask on
1352 // the public `.0` word.
1353 let other_event_before = Ready(self.event.0 & !Ready::WRITABLE.0);
1354 self.event.insert(Ready::WRITABLE);
1355 debug_assert!(
1356 self.event.is_writable(),
1357 "signal_pending_write must set the WRITABLE event bit"
1358 );
1359 debug_assert_eq!(
1360 Ready(self.event.0 & !Ready::WRITABLE.0),
1361 other_event_before,
1362 "signal_pending_write must touch only the WRITABLE bit"
1363 );
1364 #[cfg(debug_assertions)]
1365 self.check_invariants();
1366 }
1367
1368 /// Signal that the socket has buffered data to read (e.g., TLS plaintext
1369 /// buffer after a 1xx clear) that won't generate a new epoll READABLE event.
1370 pub fn signal_pending_read(&mut self) {
1371 let other_event_before = Ready(self.event.0 & !Ready::READABLE.0);
1372 self.event.insert(Ready::READABLE);
1373 debug_assert!(
1374 self.event.is_readable(),
1375 "signal_pending_read must set the READABLE event bit"
1376 );
1377 debug_assert_eq!(
1378 Ready(self.event.0 & !Ready::READABLE.0),
1379 other_event_before,
1380 "signal_pending_read must touch only the READABLE bit"
1381 );
1382 #[cfg(debug_assertions)]
1383 self.check_invariants();
1384 }
1385
1386 /// Pair `Ready::WRITABLE` insert with `signal_pending_write` — the canonical
1387 /// invariant-15 form for any path that writes bytes to sozu-owned buffers
1388 /// under edge-triggered epoll. See `lib/src/protocol/mux/LIFECYCLE.md`.
1389 #[inline]
1390 pub fn arm_writable(&mut self) {
1391 // Snapshot the non-WRITABLE bits of both words: arm_writable must set the
1392 // WRITABLE bit in *both* interest and event and leave everything else as-is.
1393 let other_interest_before = Ready(self.interest.0 & !Ready::WRITABLE.0);
1394 let other_event_before = Ready(self.event.0 & !Ready::WRITABLE.0);
1395 self.interest.insert(Ready::WRITABLE);
1396 self.signal_pending_write();
1397 debug_assert!(
1398 self.interest.is_writable() && self.event.is_writable(),
1399 "arm_writable must set WRITABLE in both interest and event"
1400 );
1401 debug_assert_eq!(
1402 Ready(self.interest.0 & !Ready::WRITABLE.0),
1403 other_interest_before,
1404 "arm_writable must touch only the WRITABLE interest bit"
1405 );
1406 debug_assert_eq!(
1407 Ready(self.event.0 & !Ready::WRITABLE.0),
1408 other_event_before,
1409 "arm_writable must touch only the WRITABLE event bit"
1410 );
1411 #[cfg(debug_assertions)]
1412 self.check_invariants();
1413 }
1414}
1415
1416#[cfg(test)]
1417mod session_target_tests {
1418 use std::net::SocketAddr;
1419
1420 use sozu_command::proto::command::CloseSessions;
1421
1422 use super::SessionTarget;
1423
1424 const ADDRESS: &str = "10.0.0.1:8080";
1425
1426 fn target(backend_id: Option<&str>, address: Option<&str>) -> SessionTarget {
1427 SessionTarget::from(&CloseSessions {
1428 cluster_id: "cluster".to_owned(),
1429 backend_id: backend_id.map(ToOwned::to_owned),
1430 address: address.map(|a| a.parse::<SocketAddr>().unwrap().into()),
1431 })
1432 }
1433
1434 #[test]
1435 fn a_cluster_target_selects_every_backend_of_the_cluster_only() {
1436 let address = ADDRESS.parse().ok();
1437 let cluster = target(None, None);
1438 assert!(cluster.matches("cluster", Some("a"), address));
1439 assert!(cluster.matches("cluster", None, None));
1440 assert!(!cluster.matches("other", Some("a"), address));
1441 assert!(!cluster.matches("cluster-2", Some("a"), address));
1442 }
1443
1444 #[test]
1445 fn a_backend_target_selects_only_what_matches_every_field_it_sets() {
1446 let address = ADDRESS.parse().ok();
1447 let elsewhere = "10.0.0.2:8080".parse().ok();
1448
1449 let by_id = target(Some("a"), None);
1450 assert!(by_id.matches("cluster", Some("a"), elsewhere));
1451 assert!(!by_id.matches("cluster", Some("b"), address));
1452 assert!(!by_id.matches("cluster", None, address));
1453 assert!(!by_id.matches("other", Some("a"), address));
1454
1455 let by_address = target(None, Some(ADDRESS));
1456 assert!(by_address.matches("cluster", Some("b"), address));
1457 assert!(!by_address.matches("cluster", Some("a"), elsewhere));
1458 assert!(!by_address.matches("cluster", Some("a"), None));
1459
1460 let by_both = target(Some("a"), Some(ADDRESS));
1461 assert!(by_both.matches("cluster", Some("a"), address));
1462 assert!(!by_both.matches("cluster", Some("b"), address));
1463 assert!(!by_both.matches("cluster", Some("a"), elsewhere));
1464 }
1465}
1466
1467#[cfg(test)]
1468mod readiness_tests {
1469 use super::{Readiness, Ready};
1470
1471 #[test]
1472 fn arm_writable_sets_interest_and_event() {
1473 let mut r = Readiness::new();
1474 r.arm_writable();
1475 assert!(r.interest.is_writable());
1476 assert!(r.event.is_writable());
1477 }
1478
1479 #[test]
1480 fn arm_writable_is_idempotent() {
1481 let mut r = Readiness::new();
1482 r.arm_writable();
1483 r.arm_writable();
1484 assert_eq!(r.interest, Ready::WRITABLE);
1485 assert_eq!(r.event, Ready::WRITABLE);
1486 }
1487
1488 /// A trace line tells a half-close from a hang-up: WRITE_CLOSED renders
1489 /// as `C` beside HUP's `H`.
1490 #[test]
1491 fn readiness_rendering_tells_a_hang_up_from_a_half_close() {
1492 assert_eq!(
1493 super::ready_to_string(Ready::READABLE | Ready::HUP),
1494 "R--H-"
1495 );
1496 assert_eq!(
1497 super::ready_to_string(Ready::READABLE | Ready::HUP | Ready::WRITE_CLOSED),
1498 "R--HC"
1499 );
1500 let readiness = Readiness {
1501 event: Ready::HUP | Ready::WRITE_CLOSED,
1502 interest: Ready::READABLE | Ready::HUP,
1503 };
1504 assert_eq!(
1505 readiness.to_string(),
1506 "I(\"R--H-\")&R(\"---HC\")=M(\"---H-\")"
1507 );
1508 assert_eq!(
1509 format!("{readiness:?}"),
1510 "Readiness { interest: R--H-, readiness: ---HC, mixed: ---H- }"
1511 );
1512 }
1513}
1514
1515pub fn display_ready(s: &mut [u8], readiness: Ready) {
1516 if readiness.is_readable() {
1517 s[0] = b'R';
1518 }
1519 if readiness.is_writable() {
1520 s[1] = b'W';
1521 }
1522 if readiness.is_error() {
1523 s[2] = b'E';
1524 }
1525 if readiness.is_hup() {
1526 s[3] = b'H';
1527 }
1528 // `H` alone is a half-close or a hang-up; `C` says the peer can receive
1529 // nothing more (WRITE_CLOSED), which only a hang-up raises.
1530 if readiness.is_write_closed() {
1531 s[4] = b'C';
1532 }
1533}
1534
1535pub fn ready_to_string(readiness: Ready) -> String {
1536 let s = &mut [b'-'; 5];
1537 display_ready(s, readiness);
1538 String::from_utf8(s.to_vec()).unwrap()
1539}
1540
1541impl fmt::Debug for Readiness {
1542 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1543 let i = &mut [b'-'; 5];
1544 let r = &mut [b'-'; 5];
1545 let mixed = &mut [b'-'; 5];
1546
1547 display_ready(i, self.interest);
1548 display_ready(r, self.event);
1549 display_ready(mixed, self.interest & self.event);
1550
1551 write!(
1552 f,
1553 "Readiness {{ interest: {}, readiness: {}, mixed: {} }}",
1554 str::from_utf8(i).unwrap(),
1555 str::from_utf8(r).unwrap(),
1556 str::from_utf8(mixed).unwrap()
1557 )
1558 }
1559}
1560
1561#[derive(Clone, Debug)]
1562pub struct SessionMetrics {
1563 /// date at which we started handling that request
1564 pub start: Option<Instant>,
1565 /// wall-clock timestamp captured alongside `start`, for access-log
1566 /// consumers that need an absolute start time (e.g. OTel span
1567 /// reconstruction) without subtracting a monotonic duration from a
1568 /// wall-clock end time — which mixes two unsynchronised clock sources.
1569 pub start_wall: Option<SystemTime>,
1570 /// time actually spent handling the request
1571 pub service_time: Duration,
1572 /// time spent waiting for its turn
1573 pub wait_time: Duration,
1574 /// bytes received by the frontend
1575 pub bin: usize,
1576 /// bytes sent by the frontend
1577 pub bout: usize,
1578
1579 /// date at which we started working on the request
1580 pub service_start: Option<Instant>,
1581 pub wait_start: Instant,
1582
1583 /// The id of the backend serving this request. An `Rc<str>` so the mux
1584 /// shares the one its `BackendId` carries instead of copying it (#1579).
1585 pub backend_id: Option<Rc<str>>,
1586 pub backend_start: Option<Instant>,
1587 pub backend_connected: Option<Instant>,
1588 /// date at which the backend's response headers finished parsing — the
1589 /// time-to-first-header-byte anchor nginx calls `$upstream_header_time`.
1590 /// Set by the L7 read paths only; raw TCP has no headers and leaves it
1591 /// `None` (see [`SessionMetrics::backend_header_time`]).
1592 pub backend_headers_received: Option<Instant>,
1593 pub backend_stop: Option<Instant>,
1594 pub backend_bin: usize,
1595 pub backend_bout: usize,
1596}
1597
1598impl SessionMetrics {
1599 pub fn new(wait_time: Option<Duration>) -> SessionMetrics {
1600 SessionMetrics {
1601 start: Some(Instant::now()),
1602 start_wall: Some(SystemTime::now()),
1603 service_time: Duration::from_secs(0),
1604 wait_time: wait_time.unwrap_or_else(|| Duration::from_secs(0)),
1605 bin: 0,
1606 bout: 0,
1607 service_start: None,
1608 wait_start: Instant::now(),
1609 backend_id: None,
1610 backend_start: None,
1611 backend_connected: None,
1612 backend_headers_received: None,
1613 backend_stop: None,
1614 backend_bin: 0,
1615 backend_bout: 0,
1616 }
1617 }
1618
1619 pub fn reset(&mut self) {
1620 self.start = None;
1621 self.start_wall = None;
1622 self.service_time = Duration::from_secs(0);
1623 self.wait_time = Duration::from_secs(0);
1624 self.bin = 0;
1625 self.bout = 0;
1626 self.service_start = None;
1627 self.backend_start = None;
1628 self.backend_connected = None;
1629 self.backend_headers_received = None;
1630 self.backend_stop = None;
1631 self.backend_bin = 0;
1632 self.backend_bout = 0;
1633 }
1634
1635 pub fn service_start(&mut self) {
1636 let now = if self.start.is_none() {
1637 self.mark_request_start()
1638 } else {
1639 Instant::now()
1640 };
1641 self.service_start = Some(now);
1642 self.wait_time += now - self.wait_start;
1643 }
1644
1645 pub fn service_stop(&mut self) {
1646 if let Some(start) = self.service_start.take() {
1647 let duration = Instant::now() - start;
1648 self.service_time += duration;
1649 }
1650 }
1651
1652 pub fn wait_start(&mut self) {
1653 self.wait_start = Instant::now();
1654 }
1655
1656 pub fn service_time(&self) -> Duration {
1657 match self.service_start {
1658 Some(start) => {
1659 let last_duration = Instant::now() - start;
1660 self.service_time + last_duration
1661 }
1662 None => self.service_time,
1663 }
1664 }
1665
1666 /// Arm both the monotonic and wall-clock start timestamps together.
1667 /// This must be the single place that sets `start` + `start_wall` outside
1668 /// of `new()`, so the two fields can never desynchronize.
1669 /// Returns the monotonic instant so callers that need it (e.g.
1670 /// `service_start`) can reuse it without a second syscall.
1671 pub fn mark_request_start(&mut self) -> Instant {
1672 let now = Instant::now();
1673 self.start = Some(now);
1674 self.start_wall = Some(SystemTime::now());
1675 now
1676 }
1677
1678 /// time elapsed since the beginning of the session
1679 pub fn request_time(&self) -> Duration {
1680 match self.start {
1681 Some(start) => Instant::now() - start,
1682 None => Duration::from_secs(0),
1683 }
1684 }
1685
1686 /// Wall-clock start time as nanoseconds since the Unix epoch, or `None`
1687 /// if the monotonic start has not been set yet (post-`reset()`, pre-`service_start()`).
1688 pub fn start_wall_ns(&self) -> Option<i128> {
1689 self.start_wall.and_then(|t| {
1690 t.duration_since(SystemTime::UNIX_EPOCH)
1691 .ok()
1692 .map(|d| d.as_nanos() as i128)
1693 })
1694 }
1695
1696 pub fn backend_start(&mut self) {
1697 self.backend_start = Some(Instant::now());
1698 }
1699
1700 pub fn backend_connected(&mut self) {
1701 self.backend_connected = Some(Instant::now());
1702 }
1703
1704 /// The backend's response headers have just finished parsing. Called from
1705 /// the L7 read paths at the header -> body transition, once per response.
1706 ///
1707 /// Overwrites, exactly like [`Self::backend_stop`] and for the same
1708 /// reason: a 1xx informational response is forwarded and the back buffer
1709 /// is then cleared — `ConnectionH1::writable` (`lib/src/protocol/mux/h1.rs`)
1710 /// in its 100-Continue and other-interim (102-199) arms, `ConnectionH2::handle_1xx_reset`
1711 /// (`lib/src/protocol/mux/h2.rs`) — so the final response re-enters that
1712 /// transition from the initial parsing phase. Last write wins, which anchors
1713 /// this on the FINAL response headers — the same response `backend_stop`
1714 /// anchors on, and what nginx reports in `$upstream_header_time`. A
1715 /// set-once guard would anchor the pair on two different responses and
1716 /// floor the metric at ~0ms for every `Expect: 100-continue` client,
1717 /// hiding the upstream think time the metric exists to expose.
1718 pub fn backend_headers_received(&mut self) {
1719 self.backend_headers_received = Some(Instant::now());
1720 }
1721
1722 pub fn backend_stop(&mut self) {
1723 self.backend_stop = Some(Instant::now());
1724 }
1725
1726 pub fn backend_response_time(&self) -> Option<Duration> {
1727 match (self.backend_connected, self.backend_stop) {
1728 (Some(start), Some(end)) => Some(end - start),
1729 (Some(start), None) => Some(Instant::now() - start),
1730 _ => None,
1731 }
1732 }
1733
1734 pub fn backend_connection_time(&self) -> Option<Duration> {
1735 match (self.backend_start, self.backend_connected) {
1736 (Some(start), Some(end)) => Some(end - start),
1737 _ => None,
1738 }
1739 }
1740
1741 /// Time from the established backend connection to the moment its response
1742 /// headers finished parsing — nginx's `$upstream_header_time`, the third
1743 /// of the three upstream timings of sozu-proxy/sozu#426.
1744 ///
1745 /// Deliberately WITHOUT the elapsed-so-far fallback that
1746 /// [`Self::backend_response_time`] carries for its `backend_stop`-is-None
1747 /// case. That fallback serves a live read of a response still streaming;
1748 /// a time-to-first-header-byte has no in-flight meaning, because no header
1749 /// byte has arrived. Returning `Instant::now() - connected` anyway would
1750 /// publish a value that grows with the wait and then freezes wherever the
1751 /// session ended — indistinguishable, on a dashboard, from a slow backend
1752 /// that did answer. `None` is the honest answer, and it is also what keeps
1753 /// raw TCP (`lib/src/tcp.rs`, no headers at all) recording nothing here
1754 /// instead of recording a zero.
1755 pub fn backend_header_time(&self) -> Option<Duration> {
1756 match (self.backend_connected, self.backend_headers_received) {
1757 (Some(connected), Some(headers)) => Some(headers - connected),
1758 _ => None,
1759 }
1760 }
1761
1762 pub fn register_end_of_session(&self, context: &LogContext) {
1763 let request_time = self.request_time();
1764 let service_time = self.service_time();
1765
1766 if let Some(cluster_id) = context.cluster_id {
1767 time!(
1768 names::event_loop::REQUEST_TIME,
1769 cluster_id,
1770 request_time.as_millis()
1771 );
1772 time!(
1773 names::event_loop::SERVICE_TIME,
1774 cluster_id,
1775 service_time.as_millis()
1776 );
1777 }
1778 time!(names::event_loop::REQUEST_TIME, request_time.as_millis());
1779 time!(names::event_loop::SERVICE_TIME, service_time.as_millis());
1780
1781 if let Some(backend_id) = self.backend_id.as_ref()
1782 && let Some(backend_response_time) = self.backend_response_time()
1783 {
1784 record_backend_metrics!(
1785 context.cluster_id.as_str_or("-"),
1786 backend_id,
1787 backend_response_time.as_millis(),
1788 self.backend_connection_time(),
1789 self.backend_header_time(),
1790 self.backend_bin,
1791 self.backend_bout
1792 );
1793 }
1794
1795 incr!(
1796 names::access_logs::COUNT,
1797 context.cluster_id,
1798 context.backend_id
1799 );
1800 }
1801
1802 pub(crate) fn register_end_of_session_for_incarnation(
1803 &self,
1804 context: &LogContext,
1805 incarnation: metrics::ClusterMetricsCapture,
1806 ) {
1807 let request_time = self.request_time();
1808 let service_time = self.service_time();
1809
1810 if let Some(cluster_id) = context.cluster_id {
1811 time!(
1812 names::event_loop::REQUEST_TIME,
1813 cluster_id,
1814 request_time.as_millis(),
1815 incarnation
1816 );
1817 time!(
1818 names::event_loop::SERVICE_TIME,
1819 cluster_id,
1820 service_time.as_millis(),
1821 incarnation
1822 );
1823 }
1824 time!(names::event_loop::REQUEST_TIME, request_time.as_millis());
1825 time!(names::event_loop::SERVICE_TIME, service_time.as_millis());
1826
1827 if let Some(backend_id) = self.backend_id.as_ref()
1828 && let Some(backend_response_time) = self.backend_response_time()
1829 {
1830 record_backend_metrics!(
1831 context.cluster_id.as_str_or("-"),
1832 backend_id,
1833 backend_response_time.as_millis(),
1834 self.backend_connection_time(),
1835 self.backend_header_time(),
1836 self.backend_bin,
1837 self.backend_bout,
1838 incarnation
1839 );
1840 }
1841
1842 incr!(
1843 names::access_logs::COUNT,
1844 context.cluster_id,
1845 context.backend_id,
1846 incarnation
1847 );
1848 }
1849}
1850
1851/// exponentially weighted moving average with high sensibility to latency bursts
1852///
1853/// It reads no clock: construction, [`Self::observe`] and [`Self::get`] each
1854/// take the instant they happen at, so the decay a simulator observes is a
1855/// function of the instants it supplies (#1684).
1856///
1857/// cf Finagle for the original implementation: <https://github.com/twitter/finagle/blob/9cc08d15216497bb03a1cafda96b7266cfbbcff1/finagle-core/src/main/scala/com/twitter/finagle/loadbalancer/PeakEwma.scala>
1858#[derive(Debug, PartialEq, Clone)]
1859pub struct PeakEWMA {
1860 /// decay in nanoseconds
1861 ///
1862 /// higher values will make the EWMA decay slowly to 0
1863 pub decay: f64,
1864 /// estimated RTT in nanoseconds
1865 ///
1866 /// must be set to a high enough default value so that new backends do not
1867 /// get all the traffic right away
1868 pub rtt: f64,
1869 /// last modification
1870 pub last_event: Instant,
1871}
1872
1873impl PeakEWMA {
1874 /// A fresh average created at `now`, which the first decay is measured
1875 /// from. Hardcoded default values for now.
1876 pub fn new(now: Instant) -> Self {
1877 PeakEWMA {
1878 // 1s
1879 decay: 1_000_000_000f64,
1880 // 50ms
1881 rtt: 50_000_000f64,
1882 last_event: now,
1883 }
1884 }
1885
1886 /// Fold an observed `rtt` (in nanoseconds) made at `now` into the average.
1887 ///
1888 /// An instant earlier than the last event decays nothing: the elapsed
1889 /// time saturates at zero.
1890 pub fn observe(&mut self, rtt: f64, now: Instant) {
1891 let dur = now.saturating_duration_since(self.last_event);
1892
1893 // if latency is rising, we will immediately raise the cost
1894 if rtt > self.rtt {
1895 self.rtt = rtt;
1896 } else {
1897 // new_rtt = old_rtt * e^(-elapsed/decay) + observed_rtt * (1 - e^(-elapsed/decay))
1898 let weight = (-(dur.as_nanos() as f64) / self.decay).exp();
1899 self.rtt = self.rtt * weight + rtt * (1.0 - weight);
1900 }
1901
1902 self.last_event = now;
1903 }
1904
1905 /// The cost of a backend with `active_requests` in flight, as seen at
1906 /// `now`.
1907 pub fn get(&mut self, active_requests: usize, now: Instant) -> f64 {
1908 // decay the current value
1909 // (we might not have seen a request in a long time)
1910 self.observe(0.0, now);
1911
1912 (active_requests + 1) as f64 * self.rtt
1913 }
1914}
1915
1916pub mod testing {
1917 pub use std::{cell::RefCell, os::fd::IntoRawFd, rc::Rc};
1918
1919 pub use anyhow::Context;
1920 pub use mio::{Poll, Registry, Token, net::UnixStream};
1921 pub use slab::Slab;
1922 pub use sozu_command::{
1923 proto::command::{
1924 HttpListenerConfig, HttpsListenerConfig, ServerConfig, TcpListenerConfig,
1925 },
1926 scm_socket::{Listeners, ScmSocket},
1927 };
1928
1929 pub use crate::{
1930 Protocol, ProxySession,
1931 backends::BackendMap,
1932 http::HttpProxy,
1933 https::HttpsProxy,
1934 pool::Pool,
1935 server::{ListenSession, ProxyChannel, Server, SessionManager},
1936 tcp::TcpProxy,
1937 };
1938
1939 use std::{
1940 collections::VecDeque,
1941 net::{SocketAddr, UdpSocket},
1942 sync::{Mutex, PoisonError},
1943 };
1944
1945 use socket2::{Domain, Socket, Type};
1946
1947 /// Ports handed to lib tests, below Linux's default ephemeral range
1948 /// (32768–60999) so no `127.0.0.1:0` client socket is auto-bound onto one
1949 /// between allocation and use, and above the fixed 10000-based sequence
1950 /// older revisions of this function used. The e2e suite draws from
1951 /// 20000–65000 through its own registry (`e2e/src/port_registry.rs`),
1952 /// which reserves through [`reserve_port`] too.
1953 const TEST_PORT_FIRST: u16 = 12_000;
1954 const TEST_PORT_LAST: u16 = 19_999;
1955
1956 /// Next candidate for [`provide_port`].
1957 static NEXT_TEST_PORT: Mutex<u16> = Mutex::new(TEST_PORT_FIRST);
1958
1959 /// How many reservations [`reserve_port`] holds at once. Each one is a
1960 /// descriptor: the full e2e suite reserves thousands of ports, which would
1961 /// exceed a default soft `RLIMIT_NOFILE` of 1024 if every reservation
1962 /// lived until the process exits. The oldest reservation is dropped to
1963 /// make room, so a test that releases its listener and binds it again
1964 /// only after this process has reserved this many further ports has lost
1965 /// its reservation in between.
1966 const MAX_HELD_RESERVATIONS: usize = 256;
1967
1968 /// The sockets [`reserve_port`] holds, oldest first.
1969 static PORT_RESERVATIONS: Mutex<VecDeque<Socket>> = Mutex::new(VecDeque::new());
1970
1971 /// Reserve `127.0.0.1:port` against the other processes on this machine
1972 /// that also reserve their ports through this function, until this
1973 /// process has reserved 256 more ports. Returns `false`, reserving
1974 /// nothing, when the port is already in use.
1975 ///
1976 /// A port counter alone is unique within one test binary only: two
1977 /// binaries running concurrently (several worktrees, parallel CI jobs,
1978 /// pre-push hooks) walked the same sequence, and since `server_bind` and
1979 /// `udp_bind` set `SO_REUSEPORT`, both processes bound the same address
1980 /// without error and the kernel split connections and datagrams between
1981 /// them. A port a test releases on purpose, for instance by deactivating
1982 /// a listener before activating it again, could likewise be taken by
1983 /// another process in between.
1984 ///
1985 /// The port is reserved only when an exclusive TCP bind (no
1986 /// `SO_REUSEADDR`, no `SO_REUSEPORT`) and an exclusive UDP bind on it both
1987 /// succeed. The TCP socket is then kept, bound and not listening, with
1988 /// `SO_REUSEADDR` and `SO_REUSEPORT` set so this process's own
1989 /// `server_bind`, `std::net::TcpListener::bind` and their equivalents
1990 /// still succeed on the port. Another process's exclusive TCP bind fails
1991 /// on it, so a process reserving through this function skips it; the
1992 /// reservation never listens, so it accepts no connection and a connect
1993 /// to an otherwise unused reserved port is still refused.
1994 ///
1995 /// Those same socket options mean the reservation does not stop a process
1996 /// that binds without reserving first: a `server_bind` (`SO_REUSEPORT`)
1997 /// from a test binary built before this function existed, or a
1998 /// `std::net::TcpListener::bind` (`SO_REUSEADDR`) while nothing listens on
1999 /// the port, still succeeds on it, including between a deactivate and the
2000 /// re-activate of a test's listener. The guarantee holds only between
2001 /// processes that all reserve their ports through this function, and only
2002 /// while the reservation is held (see `MAX_HELD_RESERVATIONS`). The UDP
2003 /// side is only probed, never held: a bound UDP socket sharing the port
2004 /// through `SO_REUSEPORT` would receive a share of the datagrams.
2005 pub fn reserve_port(port: u16) -> bool {
2006 let address = SocketAddr::from(([127, 0, 0, 1], port));
2007 let reservation = Socket::new(Domain::IPV4, Type::STREAM, None)
2008 .expect("could not create a port reservation socket");
2009 if reservation.bind(&address.into()).is_err() || UdpSocket::bind(address).is_err() {
2010 return false;
2011 }
2012 reservation
2013 .set_reuse_address(true)
2014 .and_then(|()| reservation.set_reuse_port(true))
2015 .expect("could not share the port reservation with this process");
2016 let mut reservations = PORT_RESERVATIONS
2017 .lock()
2018 .unwrap_or_else(PoisonError::into_inner);
2019 if reservations.len() == MAX_HELD_RESERVATIONS {
2020 reservations.pop_front();
2021 }
2022 reservations.push_back(reservation);
2023 true
2024 }
2025
2026 /// Get a localhost port for a test listener, held by [`reserve_port`]:
2027 /// unique across concurrent processes that all allocate their ports
2028 /// through [`reserve_port`], within that function's limits.
2029 pub fn provide_port() -> u16 {
2030 let mut next_port = NEXT_TEST_PORT
2031 .lock()
2032 .unwrap_or_else(PoisonError::into_inner);
2033 for _ in TEST_PORT_FIRST..=TEST_PORT_LAST {
2034 let candidate = *next_port;
2035 *next_port = if candidate == TEST_PORT_LAST {
2036 TEST_PORT_FIRST
2037 } else {
2038 candidate + 1
2039 };
2040 if reserve_port(candidate) {
2041 return candidate;
2042 }
2043 }
2044 panic!("no free localhost port left in {TEST_PORT_FIRST}..={TEST_PORT_LAST}");
2045 }
2046
2047 /// Everything needed to create a Server
2048 pub struct ServerParts {
2049 pub event_loop: Poll,
2050 pub registry: Registry,
2051 pub sessions: Rc<RefCell<SessionManager>>,
2052 pub pool: Rc<RefCell<Pool>>,
2053 pub backends: Rc<RefCell<BackendMap>>,
2054 pub client_scm_socket: ScmSocket,
2055 pub server_scm_socket: ScmSocket,
2056 pub server_config: ServerConfig,
2057 }
2058
2059 /// Setup a standalone server, for testing purposes
2060 pub fn prebuild_server(
2061 max_buffers: usize,
2062 buffer_size: usize,
2063 send_scm: bool,
2064 ) -> anyhow::Result<ServerParts> {
2065 let event_loop = Poll::new().with_context(|| "Failed at creating event loop")?;
2066 let backends = Rc::new(RefCell::new(BackendMap::new()));
2067 let server_config = ServerConfig {
2068 max_connections: max_buffers as u64,
2069 ..Default::default()
2070 };
2071
2072 let pool = Rc::new(RefCell::new(Pool::with_capacity(
2073 1,
2074 max_buffers,
2075 buffer_size,
2076 )));
2077
2078 let mut sessions: Slab<Rc<RefCell<dyn ProxySession>>> = Slab::with_capacity(max_buffers);
2079 {
2080 let entry = sessions.vacant_entry();
2081 info!("taking token {:?} for channel", entry.key());
2082 entry.insert(Rc::new(RefCell::new(ListenSession {
2083 protocol: Protocol::Channel,
2084 })));
2085 }
2086 {
2087 let entry = sessions.vacant_entry();
2088 info!("taking token {:?} for timer", entry.key());
2089 entry.insert(Rc::new(RefCell::new(ListenSession {
2090 protocol: Protocol::Timer,
2091 })));
2092 }
2093 {
2094 let entry = sessions.vacant_entry();
2095 info!("taking token {:?} for metrics", entry.key());
2096 entry.insert(Rc::new(RefCell::new(ListenSession {
2097 protocol: Protocol::Metrics,
2098 })));
2099 }
2100 // Test fixture: both limiters disabled (max_connections_per_ip
2101 // = 0, max_connections_per_subnet = 0) and the subnet prefixes
2102 // at their no-op widths.
2103 let sessions = SessionManager::new(sessions, max_buffers, 0, 0, 0, 32, 128);
2104
2105 let registry = event_loop
2106 .registry()
2107 .try_clone()
2108 .with_context(|| "Failed at creating a registry")?;
2109
2110 let (scm_server, scm_client) =
2111 UnixStream::pair().with_context(|| "Failed at creating scm unix stream")?;
2112 let client_scm_socket = ScmSocket::new(scm_client.into_raw_fd())
2113 .with_context(|| "Failed at creating the scm client socket")?;
2114 let server_scm_socket = ScmSocket::new(scm_server.into_raw_fd())
2115 .with_context(|| "Failed at creating the scm server socket")?;
2116 if send_scm {
2117 client_scm_socket
2118 .send_listeners(&Listeners::default())
2119 .with_context(|| "Failed at sending empty listeners")?;
2120 }
2121
2122 Ok(ServerParts {
2123 event_loop,
2124 registry,
2125 sessions,
2126 pool,
2127 backends,
2128 client_scm_socket,
2129 server_scm_socket,
2130 server_config,
2131 })
2132 }
2133}
2134
2135/// The counting global allocator of this crate's unit-test binary, so a test
2136/// can pin how many heap allocations a hot-path call makes.
2137///
2138/// A binary has exactly one global allocator, so every unit test in
2139/// `sozu-lib` that counts allocations goes through this module: read
2140/// [`allocations`] before and after the code under test and assert on the
2141/// difference. Never declare a second `#[global_allocator]` under
2142/// `#[cfg(test)]`; the test build then fails with "cannot define multiple
2143/// global allocators". [`bytes`] counts the sizes those allocations asked
2144/// for, for the code whose cost is the size of one allocation rather than
2145/// their number.
2146///
2147/// Every method forwards to [`std::alloc::System`] unchanged; the counter is
2148/// thread-local, so the harness's other test threads cannot pollute a
2149/// measurement. Same shape as the allocator `lib/tests/backend_selection.rs`
2150/// installs in its own binary.
2151#[cfg(test)]
2152pub(crate) mod test_allocations {
2153 use std::{
2154 alloc::{GlobalAlloc, Layout, System},
2155 cell::Cell,
2156 };
2157
2158 struct CountingAllocator;
2159
2160 thread_local! {
2161 static ALLOCATIONS: Cell<usize> = const { Cell::new(0) };
2162 static BYTES: Cell<usize> = const { Cell::new(0) };
2163 }
2164
2165 fn record_allocation(size: usize) {
2166 // `try_with`: the slot may already be torn down while a thread exits.
2167 let _ = ALLOCATIONS.try_with(|count| count.set(count.get() + 1));
2168 let _ = BYTES.try_with(|bytes| bytes.set(bytes.get() + size));
2169 }
2170
2171 // SAFETY: every method forwards to `System` unchanged; the only addition
2172 // is a thread-local counter increment, which neither allocates nor unwinds.
2173 unsafe impl GlobalAlloc for CountingAllocator {
2174 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
2175 record_allocation(layout.size());
2176 unsafe { System.alloc(layout) }
2177 }
2178
2179 unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
2180 record_allocation(layout.size());
2181 unsafe { System.alloc_zeroed(layout) }
2182 }
2183
2184 unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
2185 record_allocation(new_size);
2186 unsafe { System.realloc(ptr, layout, new_size) }
2187 }
2188
2189 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
2190 unsafe { System.dealloc(ptr, layout) }
2191 }
2192 }
2193
2194 #[global_allocator]
2195 static GLOBAL: CountingAllocator = CountingAllocator;
2196
2197 /// Heap allocations the calling thread has made so far.
2198 pub(crate) fn allocations() -> usize {
2199 ALLOCATIONS.with(Cell::get)
2200 }
2201
2202 /// Bytes the calling thread's allocations have asked for so far: the
2203 /// requested size of each allocation, and the new size of each
2204 /// reallocation.
2205 pub(crate) fn bytes() -> usize {
2206 BYTES.with(Cell::get)
2207 }
2208}
2209
2210#[cfg(test)]
2211pub(crate) fn capture_test_logs(run: impl FnOnce() + Send + 'static) -> String {
2212 capture_test_logs_at_level("info", run)
2213}
2214
2215#[cfg(test)]
2216pub(crate) fn capture_test_logs_at_level(
2217 level: &'static str,
2218 run: impl FnOnce() + Send + 'static,
2219) -> String {
2220 let receiver = std::net::UdpSocket::bind("127.0.0.1:0")
2221 .expect("test log receiver must bind to a loopback port");
2222 let target = format!(
2223 "udp://{}",
2224 receiver
2225 .local_addr()
2226 .expect("test log receiver must have a local address")
2227 );
2228
2229 std::thread::spawn(move || {
2230 sozu_command::logging::Logger::init(
2231 "log-redaction-test".to_owned(),
2232 level,
2233 &target,
2234 false,
2235 None,
2236 None,
2237 None,
2238 )
2239 .expect("test logger must initialize");
2240 run();
2241 })
2242 .join()
2243 .expect("log-producing test thread must not panic");
2244
2245 receiver
2246 .set_nonblocking(true)
2247 .expect("test log receiver must become nonblocking");
2248 let mut output = String::new();
2249 let mut datagram = vec![0; 65_507];
2250 loop {
2251 match receiver.recv(&mut datagram) {
2252 Ok(length) => output.push_str(&String::from_utf8_lossy(&datagram[..length])),
2253 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
2254 Err(error) => panic!("test log receiver failed: {error}"),
2255 }
2256 }
2257 assert!(!output.is_empty(), "test log capture received no datagrams");
2258 output
2259}
2260
2261#[cfg(test)]
2262mod log_redaction_tests {
2263 use super::*;
2264
2265 #[test]
2266 fn listener_template_parse_error_redacts_custom_answer_key() {
2267 const KEY_SECRET: &str = "CUSTOM_ANSWER_KEY_SECRET_SENTINEL";
2268
2269 let key = format!("{KEY_SECRET}{}", "x".repeat(4096));
2270 let key_len = key.len();
2271 let error = ListenerError::TemplateParse(key, TemplateError::InvalidType);
2272
2273 for (label, output) in [
2274 ("Display", error.to_string()),
2275 ("Debug", format!("{error:?}")),
2276 ] {
2277 assert!(
2278 !output.contains(KEY_SECRET),
2279 "ListenerError {label} leaked custom-answer key {KEY_SECRET}"
2280 );
2281 assert!(
2282 output.contains(&format!("key_bytes={key_len}")),
2283 "ListenerError {label} omitted the bounded key length: {output}"
2284 );
2285 assert!(
2286 output.len() <= 256,
2287 "ListenerError {label} output is not bounded: {} bytes",
2288 output.len()
2289 );
2290 }
2291 }
2292
2293 #[test]
2294 fn sni_authority_mismatch_retains_inputs_but_bounds_textual_formatting() {
2295 const SNI_SECRET: &str = "SNI_MISMATCH_SECRET_SENTINEL";
2296 const AUTHORITY_SECRET: &str = "AUTHORITY_MISMATCH_SECRET_SENTINEL";
2297
2298 let sni = format!("{SNI_SECRET}{}", "x".repeat(4096));
2299 let authority = format!("{AUTHORITY_SECRET}{}", "x".repeat(4096));
2300 let error = RetrieveClusterError::SniAuthorityMismatch {
2301 sni: sni.clone(),
2302 authority: authority.clone(),
2303 };
2304
2305 match &error {
2306 RetrieveClusterError::SniAuthorityMismatch {
2307 sni: retained_sni,
2308 authority: retained_authority,
2309 } => {
2310 assert_eq!(retained_sni, &sni);
2311 assert_eq!(retained_authority, &authority);
2312 }
2313 other => panic!("expected SniAuthorityMismatch, got {other:?}"),
2314 }
2315
2316 for output in [error.to_string(), format!("{error:?}")] {
2317 for secret in [SNI_SECRET, AUTHORITY_SECRET] {
2318 assert!(
2319 !output.contains(secret),
2320 "SNI mismatch formatting leaked {secret}: {output}"
2321 );
2322 }
2323 for metadata in [
2324 format!("sni_bytes={}", sni.len()),
2325 format!("authority_bytes={}", authority.len()),
2326 ] {
2327 assert!(
2328 output.contains(&metadata),
2329 "SNI mismatch formatting omitted {metadata}: {output}"
2330 );
2331 }
2332 assert!(
2333 output.len() <= 256,
2334 "SNI mismatch formatting is not bounded: {} bytes",
2335 output.len()
2336 );
2337 }
2338 }
2339}
2340
2341#[cfg(test)]
2342mod backend_header_time_tests {
2343 use super::*;
2344
2345 /// A protocol with no response headers at all — raw TCP
2346 /// (`sozu_lib::tcp::TcpSession`, which calls
2347 /// `SessionMetrics::register_end_of_session` like the L7 paths do) — must
2348 /// record NOTHING under `backend_header_time`, not a zero and not an
2349 /// elapsed-so-far value.
2350 ///
2351 /// This is the whole of the `None`-fallback decision, in its only
2352 /// observable shape: `backend_response_time` deliberately answers
2353 /// `Instant::now() - connected` when `backend_stop` is absent, and
2354 /// `backend_header_time` deliberately does not. Without the distinction a
2355 /// TCP session would publish a header time that is really its whole
2356 /// lifetime, under a key whose name promises a time to first header byte.
2357 ///
2358 /// To SEE THIS RED: add `(Some(connected), None) => Some(Instant::now() -
2359 /// connected),` to the match in `SessionMetrics::backend_header_time`,
2360 /// mirroring the arm `SessionMetrics::backend_response_time` carries.
2361 #[test]
2362 fn a_session_that_received_no_response_headers_reports_no_header_time() {
2363 let mut metrics = SessionMetrics::new(None);
2364 metrics.backend_start();
2365 metrics.backend_connected();
2366 metrics.backend_stop();
2367
2368 // Premise: this session is otherwise fully timed, so a `None` below
2369 // cannot come from a missing connection instant.
2370 assert!(
2371 metrics.backend_connection_time().is_some(),
2372 "premise: the connection time must be available"
2373 );
2374 assert!(
2375 metrics.backend_response_time().is_some(),
2376 "premise: the response time must be available"
2377 );
2378
2379 assert_eq!(
2380 metrics.backend_header_time(),
2381 None,
2382 "a session whose backend produced no response headers must report \
2383 no header time"
2384 );
2385 }
2386
2387 /// `SessionMetrics::reset` recycles one `SessionMetrics` across the
2388 /// requests of an H1 keep-alive connection, so a field it forgets is a
2389 /// field the NEXT request inherits. `backend_response_time` was miscounted
2390 /// exactly this way once (commit `5f3371b7`); this pins that the new
2391 /// instant joins the three it sits beside.
2392 ///
2393 /// To SEE THIS RED: drop `self.backend_headers_received = None;` from
2394 /// `SessionMetrics::reset`.
2395 #[test]
2396 fn reset_clears_the_backend_headers_instant_with_its_siblings() {
2397 let mut metrics = SessionMetrics::new(None);
2398 metrics.backend_start();
2399 metrics.backend_connected();
2400 metrics.backend_headers_received();
2401 assert!(
2402 metrics.backend_header_time().is_some(),
2403 "premise: the first request must have a header time to forget"
2404 );
2405
2406 metrics.reset();
2407
2408 assert_eq!(
2409 metrics.backend_headers_received, None,
2410 "reset must clear the headers instant, or the next keep-alive \
2411 request inherits the previous response's headers"
2412 );
2413 // Restaged as a fresh request would: connected now, no headers yet.
2414 metrics.backend_connected();
2415 assert_eq!(
2416 metrics.backend_header_time(),
2417 None,
2418 "a recycled SessionMetrics must not report a header time earned by \
2419 the previous request"
2420 );
2421 }
2422}