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