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#[macro_use]
308extern crate sozu_command_lib as sozu_command;
309
310#[macro_use]
311pub mod util;
312#[macro_use]
313pub mod metrics;
314
315pub mod backends;
316pub mod crypto;
317pub mod features;
318pub mod health_check;
319pub mod http;
320pub mod load_balancing;
321pub mod pool;
322pub mod protocol;
323pub mod retry;
324pub mod router;
325pub mod socket;
326pub mod timer;
327pub mod tls;
328
329/// Linux zero-copy TCP forwarder. Used by `protocol::pipe::Pipe` when
330/// the listener is `Protocol::TCP` and the `splice` feature is enabled.
331#[cfg(all(target_os = "linux", feature = "splice"))]
332pub(crate) mod splice;
333
334pub mod server;
335pub mod tcp;
336pub mod udp;
337
338pub mod https;
339
340use std::{
341 cell::RefCell,
342 collections::{BTreeMap, HashMap},
343 fmt::{self, Display, Formatter},
344 net::SocketAddr,
345 rc::Rc,
346 str,
347 time::{Duration, Instant, SystemTime},
348};
349
350use backends::BackendError;
351use hex::FromHexError;
352use mio::{Interest, Token, net::TcpStream};
353use protocol::http::{answers::HttpAnswers, answers::TemplateError, parser::Method};
354use router::RouterError;
355use socket::ServerBindError;
356use sozu_command::{
357 AsStr, ObjectKind,
358 logging::{CachedTags, LogContext},
359 proto::command::{Cluster, ListenerType, RequestHttpFrontend, WorkerRequest, WorkerResponse},
360 ready::Ready,
361 state::ClusterId,
362};
363use tls::CertificateResolverError;
364
365use crate::{backends::BackendMap, metrics::names, router::RouteResult};
366
367/// Anything that can be registered in mio (subscribe to kernel events)
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum Protocol {
370 HTTP,
371 HTTPS,
372 TCP,
373 UDP,
374 HTTPListen,
375 HTTPSListen,
376 TCPListen,
377 UDPListen,
378 Channel,
379 Metrics,
380 Timer,
381}
382
383/// trait that must be implemented by listeners and client sessions
384pub trait ProxySession {
385 /// indicates the protocol associated with the session
386 ///
387 /// this is used to distinguish sessions from listenrs, channels, metrics
388 /// and timers
389 fn protocol(&self) -> Protocol;
390 /// if a session received an event or can still execute, the event loop will
391 /// call this method. Its result indicates if it can still execute, needs to
392 /// connect to a backend server, close the session
393 fn ready(&mut self, session: Rc<RefCell<dyn ProxySession>>) -> SessionIsToBeClosed;
394 /// if the event loop got an event for a token associated with the session,
395 /// it will call this method on the session
396 fn update_readiness(&mut self, token: Token, events: Ready);
397 /// close a session, frontend and backend sockets,
398 /// remove the entries from the session manager slab
399 fn close(&mut self);
400 /// if a timeout associated with the session triggers, the event loop will
401 /// call this method with the timeout's token
402 fn timeout(&mut self, t: Token) -> SessionIsToBeClosed;
403 /// last time the session got an event
404 fn last_event(&self) -> Instant;
405 /// display the session's internal state (for debugging purpose)
406 fn print_session(&self);
407 /// get the token associated with the frontend
408 fn frontend_token(&self) -> Token;
409 /// tell the session it has to shut down if possible
410 ///
411 /// if the session handles HTTP requests, it will not close until the response
412 /// is completely sent back to the client
413 fn shutting_down(&mut self) -> SessionIsToBeClosed;
414 /// Best-effort identifier of the cluster currently routed to by this
415 /// session. Returns `None` for `ListenSession` (no per-session
416 /// cluster), and for client sessions before routing has resolved.
417 /// H2 sessions multiplex many streams over one frontend token and may
418 /// touch several clusters; the returned value is whichever cluster
419 /// the session most recently keep-alive'd to. Used for log/metric
420 /// attribution, not for accounting (the tracker keeps the canonical
421 /// per-stream `(cluster, IP)` set).
422 fn cluster_id(&self) -> Option<String> {
423 None
424 }
425 /// Source address as observed by Sōzu, with proxy-protocol awareness.
426 /// HTTP/HTTPS/TCP client sessions return the parsed PROXY-protocol
427 /// source when present, else `peer_addr`. `ListenSession` returns
428 /// `None`. Used to attribute per-(cluster, source-IP) tracking and
429 /// access logs to the real client behind a layer-4 PROXY frontend.
430 fn session_address(&self) -> Option<SocketAddr> {
431 None
432 }
433}
434
435#[macro_export]
436macro_rules! branch {
437 (if $($value:ident)? == $expected:ident { $($then:tt)* } else { $($else:tt)* }) => {
438 macro_rules! expect {
439 ($expected) => {$($then)*};
440 ($a:ident) => {$($else)*};
441 () => {$($else)*}
442 }
443 expect!($($value)?);
444 };
445 (if $($value:ident)? == $expected:ident { $($then:tt)* } ) => {
446 macro_rules! expect {
447 ($expected) => {$($then)*};
448 }
449 expect!($($value)?);
450 };
451}
452
453#[macro_export]
454macro_rules! fallback {
455 ({} $($default:tt)*) => {
456 $($default)*
457 };
458 ({$($value:tt)+} $($default:tt)*) => {
459 $($value)+
460 };
461}
462
463#[macro_export]
464macro_rules! StateMachineBuilder {
465 (
466 ($d:tt)
467 $(#[$($state_macros:tt)*])*
468 enum $state_name:ident $(impl $trait:ident)? {
469 $($(#[$($variant_macros:tt)*])*
470 $variant_name:ident($state:ty$(,$($aux:ty),+)?) $(-> $override:expr)?),+ $(,)?
471 }
472 ) => {
473 /// A summary of the last valid State
474 #[derive(Clone, Copy, Debug)]
475 pub enum StateMarker {
476 $($variant_name,)+
477 }
478
479 $(#[$($state_macros)*])*
480 #[allow(clippy::large_enum_variant)]
481 pub enum $state_name {
482 $(
483 $(#[$($variant_macros)*])*
484 $variant_name($state$(,$($aux),+)?),
485 )+
486 /// Informs about upgrade failure, contains a summary the last valid State
487 FailedUpgrade(StateMarker),
488 }
489
490 macro_rules! _fn_impl {
491 ($function:ident(&$d($mut:ident)?, self $d(,$arg_name:ident: $arg_type:ty)*) $d(-> $ret:ty)? $d(| $marker:tt => $fail:expr)?) => {
492 fn $function(&$d($mut)? self $d(,$arg_name: $arg_type)*) $d(-> $ret)? {
493 match self {
494 $($state_name::$variant_name(_state, ..) => $crate::fallback!({$($override)?} _state.$function($d($arg_name),*)),)+
495 $state_name::FailedUpgrade($crate::fallback!({$d($marker)?} _)) => $crate::fallback!({$d($fail)?} unreachable!())
496 }
497 }
498 };
499 }
500
501 impl $state_name {
502 /// Informs about the last valid State before upgrade failure
503 fn marker(&self) -> StateMarker {
504 match self {
505 $($state_name::$variant_name(..) => StateMarker::$variant_name,)+
506 $state_name::FailedUpgrade(marker) => *marker,
507 }
508 }
509 /// Returns wether or not the State is FailedUpgrade
510 fn failed(&self) -> bool {
511 match self {
512 $state_name::FailedUpgrade(_) => true,
513 _ => false,
514 }
515 }
516 /// Gives back an owned version of the State,
517 /// leaving a FailedUpgrade in its place.
518 /// The FailedUpgrade retains the marker of the previous State.
519 fn take(&mut self) -> $state_name {
520 let mut owned_state = $state_name::FailedUpgrade(self.marker());
521 std::mem::swap(&mut owned_state, self);
522 owned_state
523 }
524 _fn_impl!{front_socket(&, self) -> &mio::net::TcpStream}
525 }
526
527 $crate::branch!{
528 if $($trait)? == SessionState {
529 impl SessionState for $state_name {
530 _fn_impl!{ready(&mut, self, session: Rc<RefCell<dyn ProxySession>>, proxy: Rc<RefCell<dyn L7Proxy>>, metrics: &mut SessionMetrics) -> SessionResult}
531 _fn_impl!{update_readiness(&mut, self, token: Token, events: Ready)}
532 _fn_impl!{timeout(&mut, self, token: Token, metrics: &mut SessionMetrics) -> StateResult}
533 _fn_impl!{cancel_timeouts(&mut, self)}
534 _fn_impl!{print_state(&, self, context: &str) | marker => error!("{} Session(FailedUpgrade({:?}))", context, marker)}
535 _fn_impl!{close(&mut, self, proxy: Rc<RefCell<dyn L7Proxy>>, metrics: &mut SessionMetrics) | _ => {}}
536 _fn_impl!{shutting_down(&mut, self) -> SessionIsToBeClosed | _ => true}
537 }
538 } else {}
539 }
540 };
541 ($($tt:tt)+) => {
542 StateMachineBuilder!{($) $($tt)+}
543 }
544}
545
546pub trait ListenerHandler {
547 fn get_addr(&self) -> &SocketAddr;
548
549 fn get_tags(&self, key: &str) -> Option<&CachedTags>;
550
551 fn get_concatenated_tags(&self, key: &str) -> Option<&str> {
552 self.get_tags(key).map(|tags| tags.concatenated.as_str())
553 }
554
555 fn set_tags(&mut self, key: String, tags: Option<BTreeMap<String, String>>);
556
557 fn protocol(&self) -> Protocol;
558
559 fn public_address(&self) -> SocketAddr;
560}
561
562#[derive(thiserror::Error, Debug)]
563pub enum FrontendFromRequestError {
564 #[error("Could not parse hostname from '{host}': {error}")]
565 HostParse { host: String, error: String },
566 #[error("invalid remaining chars after hostname. Host: {0}")]
567 InvalidCharsAfterHost(String),
568 #[error("no cluster: {0}")]
569 NoClusterFound(RouterError),
570}
571
572pub trait L7ListenerHandler {
573 fn get_sticky_name(&self) -> &str;
574
575 /// Name of the correlation header Sozu injects into every request and
576 /// response body. Default: `"Sozu-Id"`. Operators can rebrand via the
577 /// `sozu_id_header` listener config knob.
578 fn get_sozu_id_header(&self) -> &str {
579 "Sozu-Id"
580 }
581
582 fn get_connect_timeout(&self) -> u32;
583
584 /// retrieve a frontend by parsing a request's hostname, uri and method
585 fn frontend_from_request(
586 &self,
587 host: &str,
588 uri: &str,
589 method: &Method,
590 ) -> Result<RouteResult, FrontendFromRequestError>;
591
592 /// retrieve the listener's configured HTTP answers (templates)
593 fn get_answers(&self) -> &Rc<RefCell<HttpAnswers>>;
594
595 /// H2 flood detection thresholds from the listener config.
596 /// Returns the default config when the listener does not provide custom values.
597 fn get_h2_flood_config(&self) -> protocol::mux::H2FloodConfig {
598 protocol::mux::H2FloodConfig::default()
599 }
600
601 /// H2 connection tuning from the listener config.
602 /// Returns the default config when the listener does not provide custom values.
603 fn get_h2_connection_config(&self) -> protocol::mux::H2ConnectionConfig {
604 protocol::mux::H2ConnectionConfig::default()
605 }
606
607 /// Whether requests must have their `:authority` / `Host` exact-match
608 /// the TLS SNI negotiated at handshake (CWE-346 / CWE-444).
609 ///
610 /// Defaults to `true` — the safe setting that closes the
611 /// CWE-346 / CWE-444 cross-SNI smuggling vector. Operators can opt
612 /// out per-listener via `HttpsListenerConfig::strict_sni_binding =
613 /// false` when cross-SNI routing is explicitly required. Plaintext
614 /// HTTP listeners return the default value; they never have an SNI
615 /// to compare against, so the routing-layer check short-circuits on
616 /// `tls_server_name: None`.
617 fn get_strict_sni_binding(&self) -> bool {
618 true
619 }
620
621 /// Whether to strip any client-supplied `X-Real-IP` header from
622 /// forwarded requests (anti-spoofing).
623 ///
624 /// Defaults to `false` — preserves the historical pass-through
625 /// behaviour. Operators opt in via
626 /// `HttpListenerConfig::elide_x_real_ip = true` (and the equivalent on
627 /// HTTPS listeners). Independent of [`Self::get_send_x_real_ip`]: the
628 /// two flags can be combined freely (anti-spoof only, send only, both,
629 /// or neither). The elision branch lives in
630 /// `HttpContext::on_request_headers`, so it covers H1 and H2 alike.
631 fn get_elide_x_real_ip(&self) -> bool {
632 false
633 }
634
635 /// Whether to append a proxy-generated `X-Real-IP` header carrying the
636 /// connection peer IP (post-PROXY-v2 unwrap, i.e. the original client
637 /// IP) to every forwarded request.
638 ///
639 /// Defaults to `false` — preserves the historical no-injection
640 /// behaviour. Operators opt in via
641 /// `HttpListenerConfig::send_x_real_ip = true` (and the equivalent on
642 /// HTTPS listeners). Independent of [`Self::get_elide_x_real_ip`]: the
643 /// two flags can be combined freely. The injection branch lives next
644 /// to the existing X-Forwarded-For / Forwarded synthesis in
645 /// `HttpContext::on_request_headers`.
646 fn get_send_x_real_ip(&self) -> bool {
647 false
648 }
649
650 /// Per-stream idle timeout for H2 connections. An open stream that makes
651 /// no forward progress for this duration is cancelled (RST_STREAM / CANCEL).
652 /// Mitigates slow-multiplex Slowloris where a client keeps connection-level
653 /// activity high (resetting the connection idle timer on every frame) while
654 /// pinning streams for the full nominal connection timeout.
655 ///
656 /// Listeners inherit `max(30s, back_timeout)` when `h2_stream_idle_timeout_seconds`
657 /// is absent so operators who raised the socket-level backend budget do not
658 /// have to duplicate the value here; the 30 s floor preserves the baseline
659 /// slow-multiplex mitigation when `back_timeout` is shorter. Set the knob
660 /// explicitly to cap the per-stream deadline below `back_timeout` (useful
661 /// when under a slow-multiplex attack).
662 fn get_h2_stream_idle_timeout(&self) -> std::time::Duration {
663 std::time::Duration::from_secs(30)
664 }
665
666 /// Wall-clock budget granted to in-flight H2 streams after soft-stop sent
667 /// the initial `GOAWAY(NO_ERROR)`. Once the deadline elapses the mux
668 /// transitions to a forced close (final GOAWAY + session teardown).
669 ///
670 /// Returning `None` disables the forced close entirely — shutdown waits
671 /// for every stream to drain naturally. Returning `Some(d)` enforces the
672 /// budget. Default: `Some(Duration::from_secs(5))` (matches the historic
673 /// hard-coded 5 s deadline). Listeners expose the
674 /// `h2_graceful_shutdown_deadline_seconds` knob; value `0` maps to `None`.
675 fn get_h2_graceful_shutdown_deadline(&self) -> Option<std::time::Duration> {
676 Some(std::time::Duration::from_secs(5))
677 }
678}
679
680#[derive(Clone, Copy, Debug, PartialEq, Eq)]
681pub enum BackendConnectionStatus {
682 NotConnected,
683 Connecting(Instant),
684 Connected,
685}
686
687impl BackendConnectionStatus {
688 pub fn is_connecting(&self) -> bool {
689 matches!(self, BackendConnectionStatus::Connecting(_))
690 }
691}
692
693#[derive(Debug, PartialEq, Eq)]
694pub enum BackendConnectAction {
695 New,
696 Reuse,
697 Replace,
698}
699
700#[derive(thiserror::Error, Debug)]
701pub enum BackendConnectionError {
702 #[error("Not found: {0:?}")]
703 NotFound(ObjectKind),
704 #[error("Too many connections on cluster {0:?}")]
705 MaxConnectionRetries(Option<String>),
706 #[error("the sessions slab has reached maximum capacity")]
707 MaxSessionsMemory,
708 #[error("error from the backend: {0}")]
709 Backend(BackendError),
710 #[error("failed to retrieve the cluster: {0}")]
711 RetrieveClusterError(RetrieveClusterError),
712 #[error("maximum number of buffers reached")]
713 MaxBuffers,
714 /// Per-(cluster, source-IP) connection limit reached. The protocol
715 /// layer translates this into HTTP 429 Too Many Requests (with an
716 /// optional `Retry-After`) for HTTP/HTTPS sessions, or a graceful TCP
717 /// close for raw TCP. The `cluster_id` is included so log/metric
718 /// pipelines can attribute the rejection.
719 #[error("per-(cluster, source-IP) connection limit reached for cluster {cluster_id:?}")]
720 TooManyConnectionsPerIp { cluster_id: String },
721}
722
723/// used in kawa_h1 module for the Http session state
724#[derive(thiserror::Error, Debug)]
725pub enum RetrieveClusterError {
726 #[error("No method given")]
727 NoMethod,
728 #[error("No host given")]
729 NoHost,
730 #[error("No path given")]
731 NoPath,
732 #[error("unauthorized route")]
733 UnauthorizedRoute,
734 #[error("{0}")]
735 RetrieveFrontend(FrontendFromRequestError),
736 #[error("HTTPS redirect required")]
737 HttpsRedirect,
738 /// The HTTP `:authority` / `Host` host does not match the TLS SNI that was
739 /// negotiated for this connection, which would cross the TLS trust boundary.
740 /// Maps to HTTP 421 Misdirected Request (RFC 9110 §15.5.20).
741 #[error("TLS SNI {sni:?} does not match HTTP authority {authority:?}")]
742 SniAuthorityMismatch { sni: String, authority: String },
743}
744
745/// Used in sessions
746#[derive(Debug, PartialEq, Eq)]
747pub enum AcceptError {
748 IoError,
749 TooManySessions,
750 WouldBlock,
751 RegisterError,
752 WrongSocketAddress,
753 BufferCapacityReached,
754}
755
756/// returned by the HTTP, HTTPS and TCP listeners
757#[derive(thiserror::Error, Debug)]
758pub enum ListenerError {
759 #[error("failed to handle certificate request, got a resolver error, {0}")]
760 Resolver(CertificateResolverError),
761 #[error("failed to parse pem, {0}")]
762 PemParse(String),
763 #[error("failed to parse template {0:?}: {1}")]
764 TemplateParse(String, TemplateError),
765 #[error("failed to build rustls context, {0}")]
766 BuildRustls(String),
767 #[error("could not activate listener with address {address:?}: {error}")]
768 Activation { address: SocketAddr, error: String },
769 #[error("Could not register listener socket: {0}")]
770 SocketRegistration(std::io::Error),
771 #[error("could not add frontend: {0}")]
772 AddFrontend(RouterError),
773 #[error("could not remove frontend: {0}")]
774 RemoveFrontend(RouterError),
775 #[error("invalid value for field '{field}': {reason}")]
776 InvalidValue {
777 field: &'static str,
778 reason: &'static str,
779 },
780 /// `UpdateHttpsListenerConfig.hsts` was present but its `enabled`
781 /// field was unset. Per the partial-update contract, `enabled` is
782 /// the explicit-disambiguator between "explicit disable" (false) and
783 /// "explicit enable" (true); the patch handler refuses an
784 /// `enabled = None` block rather than silently picking one.
785 #[error(
786 "UpdateHttpsListenerConfig.hsts is present but `enabled` is unset; the partial-update \
787 contract requires `enabled` whenever the `hsts` block is present"
788 )]
789 HstsEnabledRequired,
790}
791
792/// Lift control-plane validation errors into listener-level errors so the
793/// worker can surface the same message without duplicating the match.
794/// Non-`InvalidValue` variants fall back to a generic `InvalidValue` — they
795/// are not expected on the worker's `update_config` path (state lookups
796/// happen on the master) but we avoid panicking if one slips through.
797impl From<sozu_command::state::StateError> for ListenerError {
798 fn from(err: sozu_command::state::StateError) -> Self {
799 match err {
800 sozu_command::state::StateError::InvalidValue { field, reason } => {
801 ListenerError::InvalidValue { field, reason }
802 }
803 _ => ListenerError::InvalidValue {
804 field: "state",
805 reason: "unexpected state error on worker path",
806 },
807 }
808 }
809}
810
811/// Returned by the HTTP, HTTPS and TCP proxies
812#[derive(thiserror::Error, Debug)]
813pub enum ProxyError {
814 #[error("error while soft stopping {proxy_protocol} proxy: {error}")]
815 SoftStop {
816 proxy_protocol: String,
817 error: String,
818 },
819 #[error("error while hard stopping {proxy_protocol} proxy: {error}")]
820 HardStop {
821 proxy_protocol: String,
822 error: String,
823 },
824 #[error("found no listener with address {0:?}")]
825 NoListenerFound(SocketAddr),
826 #[error("a listener is already present for this token")]
827 ListenerAlreadyPresent,
828 #[error("could not add listener: {0}")]
829 AddListener(ListenerError),
830 #[error("could not add cluster: {0}")]
831 AddCluster(ListenerError),
832 #[error("failed to activate listener with address {address:?}: {listener_error}")]
833 ListenerActivation {
834 address: SocketAddr,
835 listener_error: ListenerError,
836 },
837 #[error("can not add frontend {front:?}: {error}")]
838 WrongInputFrontend {
839 front: Box<RequestHttpFrontend>,
840 error: String,
841 },
842 #[error("could not add frontend: {0}")]
843 AddFrontend(ListenerError),
844 #[error("could not remove frontend: {0}")]
845 RemoveFrontend(ListenerError),
846 #[error("could not add certificate: {0}")]
847 AddCertificate(CertificateResolverError),
848 #[error("could not remove certificate: {0}")]
849 RemoveCertificate(CertificateResolverError),
850 #[error("could not replace certificate: {0}")]
851 ReplaceCertificate(CertificateResolverError),
852 #[error("wrong certificate fingerprint: {0}")]
853 WrongCertificateFingerprint(FromHexError),
854 #[error("this request is not supported by the proxy")]
855 UnsupportedMessage,
856 #[error("failed to acquire the lock, {0}")]
857 Lock(String),
858 #[error("could not bind to socket {0:?}: {1}")]
859 BindToSocket(SocketAddr, ServerBindError),
860 #[error("error registering socket of listener: {0}")]
861 RegisterListener(std::io::Error),
862 #[error("the listener is not activated")]
863 UnactivatedListener,
864 /// HSTS (RFC 6797) was attached to a frontend on a plain-HTTP
865 /// listener. RFC 6797 §7.2 forbids `Strict-Transport-Security` on
866 /// plaintext-HTTP responses; the worker rejects the request rather
867 /// than ship a non-conformant policy. The TOML loader rejects the
868 /// same shape at config-load time
869 /// (`command/src/config.rs::ConfigError::HstsOnPlainHttp`); this
870 /// arm catches the same misconfiguration when the request reaches
871 /// the worker over the IPC channel without going through the TOML
872 /// path (e.g. via `sozu frontend http add`).
873 #[error(
874 "HSTS is only valid on HTTPS frontends; rejecting AddHttpFrontend with hsts.enabled = \
875 true on address {0:?} (RFC 6797 §7.2)"
876 )]
877 HstsOnPlainHttp(SocketAddr),
878 /// A TCP `AddTcpFrontend` would corrupt this listener's SNI/ALPN
879 /// routing invariants (sozu-proxy/sozu#1279): mixing a no-SNI
880 /// catch-all with SNI-scoped routes, an `alpn`-scoped frontend with no
881 /// `sni` to preread against, or an ALPN protocol / catch-all that
882 /// overlaps an existing route on the same `(address, sni)`. TOML
883 /// config-load already rejects the same shapes
884 /// (`command/src/config.rs::ConfigError`), but `AddTcpFrontend` can
885 /// also arrive directly over the command socket or via `LoadState`
886 /// replay of a hand-edited/stale state file, bypassing config.rs
887 /// entirely — see `TcpListener::validate_new_tcp_front`
888 /// (`lib/src/tcp.rs`).
889 #[error("rejected AddTcpFrontend on listener {address:?}: {reason}")]
890 InvalidTcpFrontend { address: SocketAddr, reason: String },
891}
892
893use self::server::ListenToken;
894pub trait ProxyConfiguration {
895 fn notify(&mut self, message: WorkerRequest) -> WorkerResponse;
896 fn accept(&mut self, token: ListenToken) -> Result<TcpStream, AcceptError>;
897 fn create_session(
898 &mut self,
899 socket: TcpStream,
900 token: ListenToken,
901 wait_time: Duration,
902 proxy: Rc<RefCell<Self>>,
903 // should we insert the tags here?
904 ) -> Result<(), AcceptError>;
905}
906
907pub trait L7Proxy {
908 fn kind(&self) -> ListenerType;
909
910 fn register_socket(
911 &self,
912 socket: &mut TcpStream,
913 token: Token,
914 interest: Interest,
915 ) -> Result<(), std::io::Error>;
916
917 fn deregister_socket(&self, tcp_stream: &mut TcpStream) -> Result<(), std::io::Error>;
918
919 fn add_session(&self, session: Rc<RefCell<dyn ProxySession>>) -> Token;
920
921 /// Remove the session from the session manager slab.
922 /// Returns true if the session was actually there before deletion
923 fn remove_session(&self, token: Token) -> bool;
924
925 fn backends(&self) -> Rc<RefCell<BackendMap>>;
926
927 fn clusters(&self) -> &HashMap<ClusterId, Cluster>;
928
929 /// Access the worker's [`SessionManager`] for per-(cluster, source-IP)
930 /// connection-limit accounting. The mux uses this to track / untrack
931 /// stream-granular `(cluster_id, ip)` entries and consult the
932 /// `cluster_ip_at_limit` gate before each backend connect.
933 fn sessions(&self) -> Rc<RefCell<crate::server::SessionManager>>;
934}
935
936#[derive(Debug, PartialEq, Eq)]
937pub enum RequiredEvents {
938 FrontReadBackNone,
939 FrontWriteBackNone,
940 FrontReadWriteBackNone,
941 FrontNoneBackNone,
942 FrontReadBackRead,
943 FrontWriteBackRead,
944 FrontReadWriteBackRead,
945 FrontNoneBackRead,
946 FrontReadBackWrite,
947 FrontWriteBackWrite,
948 FrontReadWriteBackWrite,
949 FrontNoneBackWrite,
950 FrontReadBackReadWrite,
951 FrontWriteBackReadWrite,
952 FrontReadWriteBackReadWrite,
953 FrontNoneBackReadWrite,
954}
955
956impl RequiredEvents {
957 pub fn front_readable(&self) -> bool {
958 matches!(
959 *self,
960 RequiredEvents::FrontReadBackNone
961 | RequiredEvents::FrontReadWriteBackNone
962 | RequiredEvents::FrontReadBackRead
963 | RequiredEvents::FrontReadWriteBackRead
964 | RequiredEvents::FrontReadBackWrite
965 | RequiredEvents::FrontReadWriteBackWrite
966 | RequiredEvents::FrontReadBackReadWrite
967 | RequiredEvents::FrontReadWriteBackReadWrite
968 )
969 }
970
971 pub fn front_writable(&self) -> bool {
972 matches!(
973 *self,
974 RequiredEvents::FrontWriteBackNone
975 | RequiredEvents::FrontReadWriteBackNone
976 | RequiredEvents::FrontWriteBackRead
977 | RequiredEvents::FrontReadWriteBackRead
978 | RequiredEvents::FrontWriteBackWrite
979 | RequiredEvents::FrontReadWriteBackWrite
980 | RequiredEvents::FrontWriteBackReadWrite
981 | RequiredEvents::FrontReadWriteBackReadWrite
982 )
983 }
984
985 pub fn back_readable(&self) -> bool {
986 matches!(
987 *self,
988 RequiredEvents::FrontReadBackRead
989 | RequiredEvents::FrontWriteBackRead
990 | RequiredEvents::FrontReadWriteBackRead
991 | RequiredEvents::FrontNoneBackRead
992 | RequiredEvents::FrontReadBackReadWrite
993 | RequiredEvents::FrontWriteBackReadWrite
994 | RequiredEvents::FrontReadWriteBackReadWrite
995 | RequiredEvents::FrontNoneBackReadWrite
996 )
997 }
998
999 pub fn back_writable(&self) -> bool {
1000 matches!(
1001 *self,
1002 RequiredEvents::FrontReadBackWrite
1003 | RequiredEvents::FrontWriteBackWrite
1004 | RequiredEvents::FrontReadWriteBackWrite
1005 | RequiredEvents::FrontNoneBackWrite
1006 | RequiredEvents::FrontReadBackReadWrite
1007 | RequiredEvents::FrontWriteBackReadWrite
1008 | RequiredEvents::FrontReadWriteBackReadWrite
1009 | RequiredEvents::FrontNoneBackReadWrite
1010 )
1011 }
1012}
1013
1014/// Signals transitions between states of a given Protocol
1015#[derive(Debug, PartialEq, Eq)]
1016pub enum StateResult {
1017 /// Signals to the Protocol to close its backend
1018 CloseBackend,
1019 /// Signals to the parent Session to close itself
1020 CloseSession,
1021 /// Signals to the Protocol to connect to backend
1022 ConnectBackend,
1023 /// Signals to the Protocol to continue
1024 Continue,
1025 /// Signals to the parent Session to upgrade to the next Protocol
1026 Upgrade,
1027}
1028
1029/// Signals transitions between states of a given Session
1030#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1031pub enum SessionResult {
1032 /// Signals to the Session to close itself
1033 Close,
1034 /// Signals to the Session to continue
1035 Continue,
1036 /// Signals to the Session to upgrade its Protocol
1037 Upgrade,
1038}
1039
1040#[derive(Debug, PartialEq, Eq)]
1041pub enum SocketType {
1042 Listener,
1043 FrontClient,
1044}
1045
1046type SessionIsToBeClosed = bool;
1047
1048#[derive(Clone)]
1049pub struct Readiness {
1050 /// the current readiness
1051 pub event: Ready,
1052 /// the readiness we wish to attain
1053 pub interest: Ready,
1054}
1055
1056impl Display for Readiness {
1057 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1058 let i = &mut [b'-'; 4];
1059 let r = &mut [b'-'; 4];
1060 let mixed = &mut [b'-'; 4];
1061
1062 display_ready(i, self.interest);
1063 display_ready(r, self.event);
1064 display_ready(mixed, self.interest & self.event);
1065
1066 write!(
1067 f,
1068 "I({:?})&R({:?})=M({:?})",
1069 String::from_utf8_lossy(i),
1070 String::from_utf8_lossy(r),
1071 String::from_utf8_lossy(mixed)
1072 )
1073 }
1074}
1075
1076impl Default for Readiness {
1077 fn default() -> Self {
1078 Self::new()
1079 }
1080}
1081
1082impl Readiness {
1083 /// Mask of every bit `Ready` defines (READABLE | WRITABLE | ERROR | HUP).
1084 /// Any bit outside this set in `event` or `interest` is a corrupted
1085 /// readiness word — checked by [`Self::check_invariants`]. Not
1086 /// `#[cfg(debug_assertions)]`-gated: it is read from inside `debug_assert!`s
1087 /// whose arguments must still compile in release (HARD RULE 2 / E0425).
1088 const KNOWN_BITS: Ready =
1089 Ready(Ready::READABLE.0 | Ready::WRITABLE.0 | Ready::ERROR.0 | Ready::HUP.0);
1090
1091 pub const fn new() -> Readiness {
1092 Readiness {
1093 event: Ready::EMPTY,
1094 interest: Ready::EMPTY,
1095 }
1096 }
1097
1098 /// Cross-field invariant sweep: neither `event` nor `interest` may carry a
1099 /// bit `Ready` does not define. A stray bit would silently widen
1100 /// `filter_interest`'s mask and wake (or starve) a session on a phantom
1101 /// readiness. Cheap enough to call as a postcondition from every mutator.
1102 #[cfg(debug_assertions)]
1103 fn check_invariants(&self) {
1104 debug_assert_eq!(
1105 self.event & Self::KNOWN_BITS,
1106 self.event,
1107 "Readiness.event carries a bit outside READABLE|WRITABLE|ERROR|HUP"
1108 );
1109 debug_assert_eq!(
1110 self.interest & Self::KNOWN_BITS,
1111 self.interest,
1112 "Readiness.interest carries a bit outside READABLE|WRITABLE|ERROR|HUP"
1113 );
1114 }
1115
1116 pub fn reset(&mut self) {
1117 self.event = Ready::EMPTY;
1118 self.interest = Ready::EMPTY;
1119 // Post-condition: a reset clears *both* words — a half-reset leaves a
1120 // session armed on stale interest after teardown.
1121 debug_assert!(
1122 self.event.is_empty() && self.interest.is_empty(),
1123 "reset must clear both event and interest"
1124 );
1125 #[cfg(debug_assertions)]
1126 self.check_invariants();
1127 }
1128
1129 /// filters the readiness we actually want
1130 pub fn filter_interest(&self) -> Ready {
1131 // Pre-condition: both source words must be well-formed before we mask —
1132 // a stray bit upstream would leak through the intersection and wake (or
1133 // starve) a session on a phantom readiness.
1134 #[cfg(debug_assertions)]
1135 self.check_invariants();
1136 let filtered = self.event & self.interest;
1137 // Post-condition: the result is a subset of the recognized bits and can
1138 // only contain bits the session both saw AND asked for.
1139 debug_assert_eq!(
1140 filtered & Self::KNOWN_BITS,
1141 filtered,
1142 "filter_interest must not yield an unknown bit"
1143 );
1144 debug_assert!(
1145 self.interest.contains(filtered) && self.event.contains(filtered),
1146 "filtered readiness must be present in both interest and event"
1147 );
1148 filtered
1149 }
1150
1151 /// Signal that the socket has buffered data to write (e.g., TLS internal
1152 /// buffers) that won't generate a new epoll WRITABLE event.
1153 pub fn signal_pending_write(&mut self) {
1154 // Snapshot the unrelated bits (everything but WRITABLE) so we can prove
1155 // we flipped exactly the WRITABLE bit. `Ready` exposes no `!`, so mask on
1156 // the public `.0` word.
1157 let other_event_before = Ready(self.event.0 & !Ready::WRITABLE.0);
1158 self.event.insert(Ready::WRITABLE);
1159 debug_assert!(
1160 self.event.is_writable(),
1161 "signal_pending_write must set the WRITABLE event bit"
1162 );
1163 debug_assert_eq!(
1164 Ready(self.event.0 & !Ready::WRITABLE.0),
1165 other_event_before,
1166 "signal_pending_write must touch only the WRITABLE bit"
1167 );
1168 #[cfg(debug_assertions)]
1169 self.check_invariants();
1170 }
1171
1172 /// Signal that the socket has buffered data to read (e.g., TLS plaintext
1173 /// buffer after a 1xx clear) that won't generate a new epoll READABLE event.
1174 pub fn signal_pending_read(&mut self) {
1175 let other_event_before = Ready(self.event.0 & !Ready::READABLE.0);
1176 self.event.insert(Ready::READABLE);
1177 debug_assert!(
1178 self.event.is_readable(),
1179 "signal_pending_read must set the READABLE event bit"
1180 );
1181 debug_assert_eq!(
1182 Ready(self.event.0 & !Ready::READABLE.0),
1183 other_event_before,
1184 "signal_pending_read must touch only the READABLE bit"
1185 );
1186 #[cfg(debug_assertions)]
1187 self.check_invariants();
1188 }
1189
1190 /// Pair `Ready::WRITABLE` insert with `signal_pending_write` — the canonical
1191 /// invariant-15 form for any path that writes bytes to sozu-owned buffers
1192 /// under edge-triggered epoll. See `lib/src/protocol/mux/LIFECYCLE.md`.
1193 #[inline]
1194 pub fn arm_writable(&mut self) {
1195 // Snapshot the non-WRITABLE bits of both words: arm_writable must set the
1196 // WRITABLE bit in *both* interest and event and leave everything else as-is.
1197 let other_interest_before = Ready(self.interest.0 & !Ready::WRITABLE.0);
1198 let other_event_before = Ready(self.event.0 & !Ready::WRITABLE.0);
1199 self.interest.insert(Ready::WRITABLE);
1200 self.signal_pending_write();
1201 debug_assert!(
1202 self.interest.is_writable() && self.event.is_writable(),
1203 "arm_writable must set WRITABLE in both interest and event"
1204 );
1205 debug_assert_eq!(
1206 Ready(self.interest.0 & !Ready::WRITABLE.0),
1207 other_interest_before,
1208 "arm_writable must touch only the WRITABLE interest bit"
1209 );
1210 debug_assert_eq!(
1211 Ready(self.event.0 & !Ready::WRITABLE.0),
1212 other_event_before,
1213 "arm_writable must touch only the WRITABLE event bit"
1214 );
1215 #[cfg(debug_assertions)]
1216 self.check_invariants();
1217 }
1218}
1219
1220#[cfg(test)]
1221mod readiness_tests {
1222 use super::{Readiness, Ready};
1223
1224 #[test]
1225 fn arm_writable_sets_interest_and_event() {
1226 let mut r = Readiness::new();
1227 r.arm_writable();
1228 assert!(r.interest.is_writable());
1229 assert!(r.event.is_writable());
1230 }
1231
1232 #[test]
1233 fn arm_writable_is_idempotent() {
1234 let mut r = Readiness::new();
1235 r.arm_writable();
1236 r.arm_writable();
1237 assert_eq!(r.interest, Ready::WRITABLE);
1238 assert_eq!(r.event, Ready::WRITABLE);
1239 }
1240}
1241
1242pub fn display_ready(s: &mut [u8], readiness: Ready) {
1243 if readiness.is_readable() {
1244 s[0] = b'R';
1245 }
1246 if readiness.is_writable() {
1247 s[1] = b'W';
1248 }
1249 if readiness.is_error() {
1250 s[2] = b'E';
1251 }
1252 if readiness.is_hup() {
1253 s[3] = b'H';
1254 }
1255}
1256
1257pub fn ready_to_string(readiness: Ready) -> String {
1258 let s = &mut [b'-'; 4];
1259 display_ready(s, readiness);
1260 String::from_utf8(s.to_vec()).unwrap()
1261}
1262
1263impl fmt::Debug for Readiness {
1264 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1265 let i = &mut [b'-'; 4];
1266 let r = &mut [b'-'; 4];
1267 let mixed = &mut [b'-'; 4];
1268
1269 display_ready(i, self.interest);
1270 display_ready(r, self.event);
1271 display_ready(mixed, self.interest & self.event);
1272
1273 write!(
1274 f,
1275 "Readiness {{ interest: {}, readiness: {}, mixed: {} }}",
1276 str::from_utf8(i).unwrap(),
1277 str::from_utf8(r).unwrap(),
1278 str::from_utf8(mixed).unwrap()
1279 )
1280 }
1281}
1282
1283#[derive(Clone, Debug)]
1284pub struct SessionMetrics {
1285 /// date at which we started handling that request
1286 pub start: Option<Instant>,
1287 /// wall-clock timestamp captured alongside `start`, for access-log
1288 /// consumers that need an absolute start time (e.g. OTel span
1289 /// reconstruction) without subtracting a monotonic duration from a
1290 /// wall-clock end time — which mixes two unsynchronised clock sources.
1291 pub start_wall: Option<SystemTime>,
1292 /// time actually spent handling the request
1293 pub service_time: Duration,
1294 /// time spent waiting for its turn
1295 pub wait_time: Duration,
1296 /// bytes received by the frontend
1297 pub bin: usize,
1298 /// bytes sent by the frontend
1299 pub bout: usize,
1300
1301 /// date at which we started working on the request
1302 pub service_start: Option<Instant>,
1303 pub wait_start: Instant,
1304
1305 pub backend_id: Option<String>,
1306 pub backend_start: Option<Instant>,
1307 pub backend_connected: Option<Instant>,
1308 pub backend_stop: Option<Instant>,
1309 pub backend_bin: usize,
1310 pub backend_bout: usize,
1311}
1312
1313impl SessionMetrics {
1314 pub fn new(wait_time: Option<Duration>) -> SessionMetrics {
1315 SessionMetrics {
1316 start: Some(Instant::now()),
1317 start_wall: Some(SystemTime::now()),
1318 service_time: Duration::from_secs(0),
1319 wait_time: wait_time.unwrap_or_else(|| Duration::from_secs(0)),
1320 bin: 0,
1321 bout: 0,
1322 service_start: None,
1323 wait_start: Instant::now(),
1324 backend_id: None,
1325 backend_start: None,
1326 backend_connected: None,
1327 backend_stop: None,
1328 backend_bin: 0,
1329 backend_bout: 0,
1330 }
1331 }
1332
1333 pub fn reset(&mut self) {
1334 self.start = None;
1335 self.start_wall = None;
1336 self.service_time = Duration::from_secs(0);
1337 self.wait_time = Duration::from_secs(0);
1338 self.bin = 0;
1339 self.bout = 0;
1340 self.service_start = None;
1341 self.backend_start = None;
1342 self.backend_connected = None;
1343 self.backend_stop = None;
1344 self.backend_bin = 0;
1345 self.backend_bout = 0;
1346 }
1347
1348 pub fn service_start(&mut self) {
1349 let now = if self.start.is_none() {
1350 self.mark_request_start()
1351 } else {
1352 Instant::now()
1353 };
1354 self.service_start = Some(now);
1355 self.wait_time += now - self.wait_start;
1356 }
1357
1358 pub fn service_stop(&mut self) {
1359 if let Some(start) = self.service_start.take() {
1360 let duration = Instant::now() - start;
1361 self.service_time += duration;
1362 }
1363 }
1364
1365 pub fn wait_start(&mut self) {
1366 self.wait_start = Instant::now();
1367 }
1368
1369 pub fn service_time(&self) -> Duration {
1370 match self.service_start {
1371 Some(start) => {
1372 let last_duration = Instant::now() - start;
1373 self.service_time + last_duration
1374 }
1375 None => self.service_time,
1376 }
1377 }
1378
1379 /// Arm both the monotonic and wall-clock start timestamps together.
1380 /// This must be the single place that sets `start` + `start_wall` outside
1381 /// of `new()`, so the two fields can never desynchronize.
1382 /// Returns the monotonic instant so callers that need it (e.g.
1383 /// `service_start`) can reuse it without a second syscall.
1384 pub fn mark_request_start(&mut self) -> Instant {
1385 let now = Instant::now();
1386 self.start = Some(now);
1387 self.start_wall = Some(SystemTime::now());
1388 now
1389 }
1390
1391 /// time elapsed since the beginning of the session
1392 pub fn request_time(&self) -> Duration {
1393 match self.start {
1394 Some(start) => Instant::now() - start,
1395 None => Duration::from_secs(0),
1396 }
1397 }
1398
1399 /// Wall-clock start time as nanoseconds since the Unix epoch, or `None`
1400 /// if the monotonic start has not been set yet (post-`reset()`, pre-`service_start()`).
1401 pub fn start_wall_ns(&self) -> Option<i128> {
1402 self.start_wall.and_then(|t| {
1403 t.duration_since(SystemTime::UNIX_EPOCH)
1404 .ok()
1405 .map(|d| d.as_nanos() as i128)
1406 })
1407 }
1408
1409 pub fn backend_start(&mut self) {
1410 self.backend_start = Some(Instant::now());
1411 }
1412
1413 pub fn backend_connected(&mut self) {
1414 self.backend_connected = Some(Instant::now());
1415 }
1416
1417 pub fn backend_stop(&mut self) {
1418 self.backend_stop = Some(Instant::now());
1419 }
1420
1421 pub fn backend_response_time(&self) -> Option<Duration> {
1422 match (self.backend_connected, self.backend_stop) {
1423 (Some(start), Some(end)) => Some(end - start),
1424 (Some(start), None) => Some(Instant::now() - start),
1425 _ => None,
1426 }
1427 }
1428
1429 pub fn backend_connection_time(&self) -> Option<Duration> {
1430 match (self.backend_start, self.backend_connected) {
1431 (Some(start), Some(end)) => Some(end - start),
1432 _ => None,
1433 }
1434 }
1435
1436 pub fn register_end_of_session(&self, context: &LogContext) {
1437 let request_time = self.request_time();
1438 let service_time = self.service_time();
1439
1440 if let Some(cluster_id) = context.cluster_id {
1441 time!(
1442 names::event_loop::REQUEST_TIME,
1443 cluster_id,
1444 request_time.as_millis()
1445 );
1446 time!(
1447 names::event_loop::SERVICE_TIME,
1448 cluster_id,
1449 service_time.as_millis()
1450 );
1451 }
1452 time!(names::event_loop::REQUEST_TIME, request_time.as_millis());
1453 time!(names::event_loop::SERVICE_TIME, service_time.as_millis());
1454
1455 if let Some(backend_id) = self.backend_id.as_ref()
1456 && let Some(backend_response_time) = self.backend_response_time()
1457 {
1458 record_backend_metrics!(
1459 context.cluster_id.as_str_or("-"),
1460 backend_id,
1461 backend_response_time.as_millis(),
1462 self.backend_connection_time(),
1463 self.backend_bin,
1464 self.backend_bout
1465 );
1466 }
1467
1468 incr!(
1469 names::access_logs::COUNT,
1470 context.cluster_id,
1471 context.backend_id
1472 );
1473 }
1474}
1475
1476/// exponentially weighted moving average with high sensibility to latency bursts
1477///
1478/// cf Finagle for the original implementation: <https://github.com/twitter/finagle/blob/9cc08d15216497bb03a1cafda96b7266cfbbcff1/finagle-core/src/main/scala/com/twitter/finagle/loadbalancer/PeakEwma.scala>
1479#[derive(Debug, PartialEq, Clone)]
1480pub struct PeakEWMA {
1481 /// decay in nanoseconds
1482 ///
1483 /// higher values will make the EWMA decay slowly to 0
1484 pub decay: f64,
1485 /// estimated RTT in nanoseconds
1486 ///
1487 /// must be set to a high enough default value so that new backends do not
1488 /// get all the traffic right away
1489 pub rtt: f64,
1490 /// last modification
1491 pub last_event: Instant,
1492}
1493
1494impl Default for PeakEWMA {
1495 fn default() -> Self {
1496 Self::new()
1497 }
1498}
1499
1500impl PeakEWMA {
1501 // hardcoded default values for now
1502 pub fn new() -> Self {
1503 PeakEWMA {
1504 // 1s
1505 decay: 1_000_000_000f64,
1506 // 50ms
1507 rtt: 50_000_000f64,
1508 last_event: Instant::now(),
1509 }
1510 }
1511
1512 pub fn observe(&mut self, rtt: f64) {
1513 let now = Instant::now();
1514 let dur = now - self.last_event;
1515
1516 // if latency is rising, we will immediately raise the cost
1517 if rtt > self.rtt {
1518 self.rtt = rtt;
1519 } else {
1520 // new_rtt = old_rtt * e^(-elapsed/decay) + observed_rtt * (1 - e^(-elapsed/decay))
1521 let weight = (-(dur.as_nanos() as f64) / self.decay).exp();
1522 self.rtt = self.rtt * weight + rtt * (1.0 - weight);
1523 }
1524
1525 self.last_event = now;
1526 }
1527
1528 pub fn get(&mut self, active_requests: usize) -> f64 {
1529 // decay the current value
1530 // (we might not have seen a request in a long time)
1531 self.observe(0.0);
1532
1533 (active_requests + 1) as f64 * self.rtt
1534 }
1535}
1536
1537pub mod testing {
1538 pub use std::{cell::RefCell, os::fd::IntoRawFd, rc::Rc};
1539
1540 pub use anyhow::Context;
1541 pub use mio::{Poll, Registry, Token, net::UnixStream};
1542 pub use slab::Slab;
1543 pub use sozu_command::{
1544 proto::command::{
1545 HttpListenerConfig, HttpsListenerConfig, ServerConfig, TcpListenerConfig,
1546 },
1547 scm_socket::{Listeners, ScmSocket},
1548 };
1549
1550 pub use crate::{
1551 Protocol, ProxySession,
1552 backends::BackendMap,
1553 http::HttpProxy,
1554 https::HttpsProxy,
1555 pool::Pool,
1556 server::{ListenSession, ProxyChannel, Server, SessionManager},
1557 tcp::TcpProxy,
1558 };
1559
1560 use std::sync::atomic::{AtomicU16, Ordering};
1561
1562 /// Port counter for sozu listener addresses in lib tests.
1563 /// Starts at 10000 to avoid collision with:
1564 /// - Privileged ports (<1024)
1565 /// - e2e suite (starts at 2000)
1566 /// - Ephemeral port range (typically 32768+)
1567 static PORT_PROVIDER: AtomicU16 = AtomicU16::new(10000);
1568
1569 /// Get a unique port for a sozu listener address.
1570 /// Each call returns a different port, safe for parallel test execution.
1571 pub fn provide_port() -> u16 {
1572 PORT_PROVIDER.fetch_add(1, Ordering::SeqCst)
1573 }
1574
1575 /// Everything needed to create a Server
1576 pub struct ServerParts {
1577 pub event_loop: Poll,
1578 pub registry: Registry,
1579 pub sessions: Rc<RefCell<SessionManager>>,
1580 pub pool: Rc<RefCell<Pool>>,
1581 pub backends: Rc<RefCell<BackendMap>>,
1582 pub client_scm_socket: ScmSocket,
1583 pub server_scm_socket: ScmSocket,
1584 pub server_config: ServerConfig,
1585 }
1586
1587 /// Setup a standalone server, for testing purposes
1588 pub fn prebuild_server(
1589 max_buffers: usize,
1590 buffer_size: usize,
1591 send_scm: bool,
1592 ) -> anyhow::Result<ServerParts> {
1593 let event_loop = Poll::new().with_context(|| "Failed at creating event loop")?;
1594 let backends = Rc::new(RefCell::new(BackendMap::new()));
1595 let server_config = ServerConfig {
1596 max_connections: max_buffers as u64,
1597 ..Default::default()
1598 };
1599
1600 let pool = Rc::new(RefCell::new(Pool::with_capacity(
1601 1,
1602 max_buffers,
1603 buffer_size,
1604 )));
1605
1606 let mut sessions: Slab<Rc<RefCell<dyn ProxySession>>> = Slab::with_capacity(max_buffers);
1607 {
1608 let entry = sessions.vacant_entry();
1609 info!("taking token {:?} for channel", entry.key());
1610 entry.insert(Rc::new(RefCell::new(ListenSession {
1611 protocol: Protocol::Channel,
1612 })));
1613 }
1614 {
1615 let entry = sessions.vacant_entry();
1616 info!("taking token {:?} for timer", entry.key());
1617 entry.insert(Rc::new(RefCell::new(ListenSession {
1618 protocol: Protocol::Timer,
1619 })));
1620 }
1621 {
1622 let entry = sessions.vacant_entry();
1623 info!("taking token {:?} for metrics", entry.key());
1624 entry.insert(Rc::new(RefCell::new(ListenSession {
1625 protocol: Protocol::Metrics,
1626 })));
1627 }
1628 // Test fixture: feature disabled (max_connections_per_ip = 0).
1629 let sessions = SessionManager::new(sessions, max_buffers, 0, 0);
1630
1631 let registry = event_loop
1632 .registry()
1633 .try_clone()
1634 .with_context(|| "Failed at creating a registry")?;
1635
1636 let (scm_server, scm_client) =
1637 UnixStream::pair().with_context(|| "Failed at creating scm unix stream")?;
1638 let client_scm_socket = ScmSocket::new(scm_client.into_raw_fd())
1639 .with_context(|| "Failed at creating the scm client socket")?;
1640 let server_scm_socket = ScmSocket::new(scm_server.into_raw_fd())
1641 .with_context(|| "Failed at creating the scm server socket")?;
1642 if send_scm {
1643 client_scm_socket
1644 .send_listeners(&Listeners::default())
1645 .with_context(|| "Failed at sending empty listeners")?;
1646 }
1647
1648 Ok(ServerParts {
1649 event_loop,
1650 registry,
1651 sessions,
1652 pool,
1653 backends,
1654 client_scm_socket,
1655 server_scm_socket,
1656 server_config,
1657 })
1658 }
1659}