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