Skip to main content

sozu_command_lib/
config.rs

1//! # Sōzu's configuration
2//!
3//! This module is responsible for parsing the `config.toml` provided by the flag `--config`
4//! when starting Sōzu.
5//!
6//! Here is the workflow for generating a working config:
7//!
8//! ```text
9//!     config.toml   ->   FileConfig    ->  ConfigBuilder   ->  Config
10//! ```
11//!
12//! `config.toml` is parsed to `FileConfig`, a structure that itself contains a lot of substructures
13//! whose names start with `File-` and end with `-Config`, like `FileHttpFrontendConfig` for instance.
14//!
15//! The instance of `FileConfig` is then passed to a `ConfigBuilder` that populates a final `Config`
16//! with listeners and clusters.
17//!
18//! To illustrate:
19//!
20//! ```no_run
21//! use sozu_command_lib::config::{FileConfig, ConfigBuilder};
22//!
23//! let file_config = FileConfig::load_from_path("../config.toml")
24//!     .expect("Could not load config.toml");
25//!
26//! let config = ConfigBuilder::new(file_config, "../assets/config.toml")
27//!     .into_config()
28//!     .expect("Could not build config");
29//! ```
30//!
31//! Note that the path to `config.toml` is used twice: the first time, to parse the file,
32//! the second time, to keep the path in the config for later use.
33//!
34//! However, there is a simpler way that combines all this:
35//!
36//! ```no_run
37//! use sozu_command_lib::config::Config;
38//!
39//! let config = Config::load_from_path("../assets/config.toml")
40//!     .expect("Could not build config from the path");
41//! ```
42//!
43//! ## How values are chosen
44//!
45//! Values are chosen in this order of priority:
46//!
47//! 1. values defined in a section of the TOML file, for instance, timeouts for a specific listener
48//! 2. values defined globally in the TOML file, like timeouts or buffer size
49//! 3. if a variable has not been set in the TOML file, it will be set to a default defined here
50use std::{
51    collections::{BTreeMap, HashMap, HashSet},
52    env, fmt,
53    fs::{File, create_dir_all, metadata},
54    io::{ErrorKind, Read},
55    net::SocketAddr,
56    ops::Range,
57    path::PathBuf,
58};
59
60use crate::{
61    ObjectKind,
62    certificate::split_certificate_chain,
63    logging::AccessLogFormat,
64    proto::command::{
65        ActivateListener, AddBackend, AddCertificate, CertificateAndKey, Cluster,
66        CustomHttpAnswers, Header, HeaderPosition, HealthCheckConfig, HstsConfig,
67        HttpListenerConfig, HttpsListenerConfig, ListenerType, LoadBalancingAlgorithms,
68        LoadBalancingParams, LoadMetric, MetricDetail, MetricsConfiguration, PathRule,
69        ProtobufAccessLogFormat, ProxyProtocolConfig, RedirectPolicy, RedirectScheme, Request,
70        RequestHttpFrontend, RequestTcpFrontend, RequestUdpFrontend, RulePosition, ServerConfig,
71        ServerMetricsConfig, SocketAddress, TcpListenerConfig, TlsVersion, UdpAffinityKey,
72        UdpClusterConfig, UdpHealthConfig, UdpHealthMode, UdpListenerConfig, WorkerRequest,
73        request::RequestType,
74    },
75};
76
77/// Authoritative list of default cipher suites for all rustls-based TLS providers.
78///
79/// These use rustls naming conventions and are supported by all three crypto providers
80/// (ring, aws-lc-rs, rustls-openssl). Order follows ANSSI recommendations: AES-256
81/// preferred over AES-128, ECDSA preferred over RSA, TLS 1.3 preferred over TLS 1.2.
82///
83/// See the [documentation](https://docs.rs/rustls/latest/rustls/static.ALL_CIPHER_SUITES.html)
84pub const DEFAULT_CIPHER_LIST: [&str; 9] = [
85    // TLS 1.3 cipher suites
86    "TLS13_AES_256_GCM_SHA384",
87    "TLS13_AES_128_GCM_SHA256",
88    "TLS13_CHACHA20_POLY1305_SHA256",
89    // TLS 1.2 cipher suites
90    "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
91    "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
92    "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
93    "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
94    "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
95    "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
96];
97
98pub const DEFAULT_SIGNATURE_ALGORITHMS: [&str; 9] = [
99    "ECDSA+SHA256",
100    "ECDSA+SHA384",
101    "ECDSA+SHA512",
102    "RSA+SHA256",
103    "RSA+SHA384",
104    "RSA+SHA512",
105    "RSA-PSS+SHA256",
106    "RSA-PSS+SHA384",
107    "RSA-PSS+SHA512",
108];
109
110pub const DEFAULT_GROUPS_LIST: [&str; 4] = ["X25519MLKEM768", "x25519", "P-256", "P-384"];
111
112/// Default ALPN protocols advertised by HTTPS listeners.
113/// Both HTTP/2 and HTTP/1.1 are enabled, allowing clients to negotiate either.
114pub const DEFAULT_ALPN_PROTOCOLS: [&str; 2] = ["h2", "http/1.1"];
115
116/// maximum time of inactivity for a frontend socket (60 seconds)
117pub const DEFAULT_FRONT_TIMEOUT: u32 = 60;
118
119/// maximum time of inactivity for a backend socket (30 seconds)
120pub const DEFAULT_BACK_TIMEOUT: u32 = 30;
121
122/// maximum time to connect to a backend server (3 seconds)
123pub const DEFAULT_CONNECT_TIMEOUT: u32 = 3;
124
125/// maximum time allowed to receive enough bytes of the TLS ClientHello to
126/// read the SNI extension on a TCP listener (5 seconds). Only relevant when
127/// at least one SNI-scoped `TcpFrontendConfig` targets the listener. Must
128/// match the proto default on `TcpListenerConfig.sni_preread_timeout`.
129pub const DEFAULT_SNI_PREREAD_TIMEOUT: u32 = 5;
130
131/// maximum number of bytes buffered while prereading the TLS ClientHello
132/// looking for the SNI extension on a TCP listener (16 KB — matches the
133/// H2 frame ceiling used elsewhere in this file). Only relevant when at
134/// least one SNI-scoped `TcpFrontendConfig` targets the listener; clamped by
135/// the global `buffer_size` (see `ConfigError::SniPrereadMaxBytesExceedsBufferSize`).
136/// Must match the proto default on
137/// `TcpListenerConfig.sni_preread_max_bytes`.
138pub const DEFAULT_SNI_PREREAD_MAX_BYTES: u32 = 16384;
139
140/// minimum allowed `sni_preread_max_bytes` on a TCP listener targeted by an
141/// SNI frontend (5 bytes — a full TLS record header: 1-byte `ContentType` +
142/// 2-byte `ProtocolVersion` + 2-byte length, RFC 8446 §5.1). `0` (and every
143/// value below this floor) makes the preread shell issue reads that can
144/// never accumulate enough bytes to parse even the outer record framing,
145/// spinning until the event-loop iteration guard (`MAX_LOOP_ITERATIONS`)
146/// trips instead of ever reaching a routing decision. See
147/// `ConfigError::SniPrereadMaxBytesTooSmall`.
148pub const MIN_SNI_PREREAD_MAX_BYTES: u32 = 5;
149
150/// maximum time to receive a request since the connection started (10 seconds)
151pub const DEFAULT_REQUEST_TIMEOUT: u32 = 10;
152
153/// client/upstream flow idle timeout for a UDP listener (30 seconds)
154pub const DEFAULT_UDP_FRONT_TIMEOUT: u32 = 30;
155
156/// upstream flow idle timeout for a UDP listener (30 seconds)
157pub const DEFAULT_UDP_BACK_TIMEOUT: u32 = 30;
158
159/// maximum received datagram size for a UDP listener, in bytes (1500 = a
160/// typical Ethernet MTU). Capped at the effective `buffer_size` at runtime.
161pub const DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE: u32 = 1500;
162
163/// maximum number of concurrent UDP flows per listener. `0` selects the
164/// runtime auto policy (~70% of the soft `RLIMIT_NOFILE`).
165pub const DEFAULT_UDP_MAX_FLOWS: u32 = 0;
166
167/// maximum time to wait for a worker to respond, until it is deemed NotAnswering (10 seconds)
168pub const DEFAULT_WORKER_TIMEOUT: u32 = 10;
169
170/// a name applied to sticky sessions ("SOZUBALANCEID")
171pub const DEFAULT_STICKY_NAME: &str = "SOZUBALANCEID";
172
173/// Interval between checking for zombie sessions, (30 minutes)
174pub const DEFAULT_ZOMBIE_CHECK_INTERVAL: u32 = 1_800;
175
176/// timeout to accept connection events in the accept queue (60 seconds)
177pub const DEFAULT_ACCEPT_QUEUE_TIMEOUT: u32 = 60;
178
179/// Default `Strict-Transport-Security: max-age` value (1 year, 31_536_000
180/// seconds) substituted at config-load when an [hsts] block sets
181/// `enabled = true` but omits `max_age`. Matches the HSTS preload list
182/// minimum (https://hstspreload.org/) and the Caddy / Nginx community
183/// recommendation. Operators can override with any `u32`; `max_age = 0`
184/// is the RFC 6797 §11.4 kill switch and is allowed silently.
185pub const DEFAULT_HSTS_MAX_AGE: u32 = 31_536_000;
186
187/// whether to evict least-recently-active sessions when the accept queue is
188/// saturated (false). Defaults to false because during a DDoS the existing
189/// connections are more likely to be legitimate clients than the queued ones;
190/// evicting them would serve the attacker. Enable when overload is dominated
191/// by normal traffic spikes rather than attacks.
192pub const DEFAULT_EVICT_ON_QUEUE_FULL: bool = false;
193
194/// number of workers, i.e. Sōzu processes that scale horizontally (2)
195pub const DEFAULT_WORKER_COUNT: u16 = 2;
196
197/// wether a worker is automatically restarted when it crashes (true)
198pub const DEFAULT_WORKER_AUTOMATIC_RESTART: bool = true;
199
200/// wether to save the state automatically (false)
201pub const DEFAULT_AUTOMATIC_STATE_SAVE: bool = false;
202
203/// minimum number of buffers (1)
204pub const DEFAULT_MIN_BUFFERS: u64 = 1;
205
206/// maximum number of buffers (1 000)
207pub const DEFAULT_MAX_BUFFERS: u64 = 1_000;
208
209/// size of the buffers, in bytes (16 KB)
210pub const DEFAULT_BUFFER_SIZE: u64 = 16_393;
211
212/// minimum buffer size required when any HTTPS listener advertises H2 ALPN.
213///
214/// RFC 9113 §6.5.2 caps `SETTINGS_MAX_FRAME_SIZE` at 16 384 bytes by default;
215/// the on-wire H2 frame header is a fixed 9 bytes (§4.1), so the kawa storage
216/// must be able to hold 16 384 + 9 = 16 393 bytes before forwarding. A smaller
217/// `buffer_size` causes the H2 mux to deadlock on full-size frames (no panic,
218/// no obvious log) until the session timeout fires. Validated at config-load
219/// time in `ConfigBuilder::into_config` so a typo in TOML is rejected at boot,
220/// not discovered under traffic.
221pub const H2_MIN_BUFFER_SIZE: u64 = 16_393;
222
223/// maximum number of simultaneous connections (10 000)
224pub const DEFAULT_MAX_CONNECTIONS: usize = 10_000;
225
226/// size of the buffer for the channels, in bytes. Must be bigger than the size of the data received. (1 MB)
227pub const DEFAULT_COMMAND_BUFFER_SIZE: u64 = 1_000_000;
228
229/// maximum size of the buffer for the channels, in bytes. (2 MB)
230pub const DEFAULT_MAX_COMMAND_BUFFER_SIZE: u64 = 2_000_000;
231
232/// wether to avoid register cluster metrics in the local drain
233pub const DEFAULT_DISABLE_CLUSTER_METRICS: bool = false;
234
235pub const MAX_LOOP_ITERATIONS: usize = 100000;
236
237/// Number of TLS 1.3 tickets to send to a client when establishing a connection.
238/// The tickets allow the client to resume a session. This protects the client
239/// agains session tracking. Increases the number of getrandom syscalls,
240/// with little influence on performance. Defaults to 4.
241pub const DEFAULT_SEND_TLS_13_TICKETS: u64 = 4;
242
243/// for both logs and access logs
244pub const DEFAULT_LOG_TARGET: &str = "stdout";
245
246/// Default per-(cluster, source-IP) connection limit. `0` means unlimited.
247/// Counts are kept per `(cluster_id, source_ip)` so two clusters never
248/// share a counter even from the same IP. Per-cluster overrides on the
249/// `Cluster` message take precedence.
250pub const DEFAULT_MAX_CONNECTIONS_PER_IP: u64 = 0;
251
252/// Default `Retry-After` header value (seconds) on HTTP 429 responses
253/// emitted when a per-(cluster, source-IP) connection limit is hit. `0`
254/// omits the header — `Retry-After: 0` invites an immediate retry that
255/// defeats the limit. TCP rejections do not emit this value (no HTTP
256/// envelope), but the field is accepted for symmetry.
257pub const DEFAULT_RETRY_AFTER: u32 = 60;
258
259#[derive(Debug)]
260pub enum IncompatibilityKind {
261    PublicAddress,
262    ProxyProtocol,
263}
264
265#[derive(Debug)]
266pub enum MissingKind {
267    Field(String),
268    Protocol,
269    SavedState,
270}
271
272#[derive(thiserror::Error, Debug)]
273pub enum ConfigError {
274    #[error("env path not found: {0}")]
275    Env(String),
276    #[error("Could not open file {path_to_open}: {io_error}")]
277    FileOpen {
278        path_to_open: String,
279        io_error: std::io::Error,
280    },
281    #[error("Could not read file {path_to_read}: {io_error}")]
282    FileRead {
283        path_to_read: String,
284        io_error: std::io::Error,
285    },
286    #[error(
287        "the field {kind:?} of {object:?} with id or address {id} is incompatible with the rest of the options"
288    )]
289    Incompatible {
290        kind: IncompatibilityKind,
291        object: ObjectKind,
292        id: String,
293    },
294    #[error("Invalid '{0}' field for a TCP frontend")]
295    InvalidFrontendConfig(String),
296    #[error("invalid path {0:?}")]
297    InvalidPath(PathBuf),
298    #[error("listening address {0:?} is already used in the configuration")]
299    ListenerAddressAlreadyInUse(SocketAddr),
300    #[error("missing {0:?}")]
301    Missing(MissingKind),
302    #[error("could not get parent directory for file {0}")]
303    NoFileParent(String),
304    #[error("Could not get the path of the saved state")]
305    SaveStatePath(String),
306    #[error("Can not determine path to sozu socket: {0}")]
307    SocketPathError(String),
308    #[error("toml decoding error: {0}")]
309    DeserializeToml(String),
310    #[error("Can not set this frontend on a {0:?} listener")]
311    WrongFrontendProtocol(ListenerProtocol),
312    #[error("Can not build a {expected:?} listener from a {found:?} config")]
313    WrongListenerProtocol {
314        expected: ListenerProtocol,
315        found: Option<ListenerProtocol>,
316    },
317    #[error("Invalid ALPN protocol '{0}'. Valid values: \"h2\", \"http/1.1\"")]
318    InvalidAlpnProtocol(String),
319    /// `disable_http11 = true` and `alpn_protocols` containing `"http/1.1"`
320    /// are mutually exclusive: the proxy advertises `http/1.1` to peers,
321    /// then refuses every connection that negotiates
322    /// it. The combination is a self-DoS at handshake time. Either drop
323    /// `http/1.1` from `alpn_protocols` or unset `disable_http11`.
324    #[error(
325        "disable_http11 = true is incompatible with alpn_protocols containing \"http/1.1\" \
326         on listener {address}. The proxy would advertise http/1.1 then refuse every \
327         connection that negotiates it. Drop \"http/1.1\" from alpn_protocols or unset \
328         disable_http11."
329    )]
330    DisableHttp11WithHttp11Alpn { address: String },
331    /// `buffer_size` is below the H2 minimum (16 393 bytes) but at least one
332    /// HTTPS listener advertises `h2` in its ALPN list. The H2 mux requires
333    /// 16 384-byte frame payload + 9-byte header to fit in a single kawa
334    /// buffer; smaller values deadlock streams that carry full-size frames.
335    /// Either raise `buffer_size` to ≥ 16 393 or remove `h2` from the
336    /// affected listeners' `alpn_protocols`.
337    #[error(
338        "buffer_size = {buffer_size} is below the H2 minimum of {minimum} but \
339         {listeners} HTTPS listener(s) advertise H2 ALPN. The H2 mux deadlocks \
340         on full-size frames with smaller buffers. Raise buffer_size to >= {minimum} \
341         or remove \"h2\" from those listeners' alpn_protocols."
342    )]
343    BufferSizeTooSmallForH2 {
344        buffer_size: u64,
345        minimum: u64,
346        listeners: usize,
347    },
348    /// `redirect = "<value>"` on a frontend used a value the parser doesn't
349    /// recognise. Accepted values are `forward`, `permanent`, `unauthorized`
350    /// (case-insensitive).
351    #[error(
352        "invalid redirect policy '{0}'. Valid values: \"forward\", \"permanent\", \"unauthorized\""
353    )]
354    InvalidRedirectPolicy(String),
355    /// `redirect_scheme = "<value>"` on a frontend used a value the parser
356    /// doesn't recognise. Accepted values are `use-same`, `use-http`,
357    /// `use-https` (case-insensitive).
358    #[error(
359        "invalid redirect scheme '{0}'. Valid values: \"use-same\", \"use-http\", \"use-https\""
360    )]
361    InvalidRedirectScheme(String),
362    /// A `[[clusters.<id>.frontends.headers]]` entry carried an unknown
363    /// `position` value. Accepted values are `request`, `response`, `both`
364    /// (case-insensitive).
365    #[error(
366        "invalid header position '{position}' at headers[{index}]. Valid values: \"request\", \"response\", \"both\""
367    )]
368    InvalidHeaderPosition { index: usize, position: String },
369    /// A `[[clusters.<id>.frontends.headers]]` entry contains a forbidden
370    /// byte (NUL, CR, LF, or another C0 control) in its key or value.
371    /// Accepting these would produce HTTP request/response splitting on
372    /// the wire (CWE-113) — the worker's H2 emission path filters them
373    /// at runtime, but the H1 path serialises raw, so we reject at
374    /// config-load time as a defense in depth.
375    #[error(
376        "invalid header bytes in {field} at headers[{index}]: control characters \
377         (NUL / CR / LF / other C0) are forbidden in header keys and values"
378    )]
379    InvalidHeaderBytes { index: usize, field: &'static str },
380    /// An `[hsts]` block populated `max_age`, `include_subdomains`, or
381    /// `preload` but did not set `enabled`. The TOML representation requires
382    /// `enabled` to be present whenever the block is — that single field
383    /// disambiguates "preserve current" / "explicit disable" / "enable" on
384    /// hot-reconfig partial updates.
385    #[error("invalid HSTS config at {0}: `enabled` is required when an [hsts] block is present")]
386    HstsEnabledRequired(String),
387    /// An `[hsts]` block on an HTTP-only listener or frontend. RFC 6797
388    /// §7.2 forbids emitting `Strict-Transport-Security` over plaintext
389    /// HTTP; sozu rejects the configuration at load time so the
390    /// non-conformant policy never ships to a worker.
391    #[error(
392        "invalid HSTS config at {0}: HSTS is only valid on HTTPS listeners and frontends \
393         (RFC 6797 §7.2 forbids the header over plaintext HTTP)"
394    )]
395    HstsOnPlainHttp(String),
396    /// A TCP frontend's `hostname` (mapped to the wire `sni` field) is
397    /// neither an exact hostname nor a single leading `*.` wildcard label
398    /// (sozu-proxy/sozu#1279). Rejects `*.*.example.com`, an embedded `*`
399    /// anywhere but the leading label, an empty label, and any `/` — never
400    /// valid in a hostname, and a leftmost `/.../` label would otherwise be
401    /// inserted into the `pattern_trie` route table as a REGEX segment.
402    #[error(
403        "invalid SNI pattern '{sni}' for a TCP frontend: expected an exact hostname or a \
404         single leading \"*.\" wildcard label (e.g. \"example.com\" or \"*.example.com\"); \
405         '/' and non-leading '*' are rejected"
406    )]
407    InvalidSniPattern { sni: String },
408    /// A TCP frontend's SNI pattern contains non-ASCII characters. On-wire
409    /// SNI is always an ASCII A-label (RFC 6066 §3 / IDNA), so a Unicode
410    /// U-label in the config would load fine but never match any
411    /// ClientHello — a silent routing failure. Rejected loudly until IDNA
412    /// normalization is supported at config-load; the operator must write
413    /// the punycode A-label form instead.
414    #[error(
415        "non-ASCII SNI pattern '{sni}' for a TCP frontend: on-wire SNI is always an ASCII \
416         A-label (RFC 6066), so this pattern would never match a ClientHello. Write the \
417         punycode A-label form instead (e.g. \"xn--mnchen-3ya.example\" for \
418         \"münchen.example\")"
419    )]
420    NonAsciiSniPattern { sni: String },
421    /// A TCP frontend set `alpn` but left `hostname` (mapped to the wire
422    /// `sni` field) unset. An ALPN matcher only ever gets consulted from
423    /// within the SNI-scoped preread route table; a frontend with no `sni`
424    /// installs the worker's raw no-SNI catch-all path instead
425    /// (`TcpListener::cluster_id`), which never looks at `alpn` at all --
426    /// the configured protocol list would silently never be enforced.
427    /// Reject at config-load rather than mis-routing every connection
428    /// through unconditionally.
429    #[error(
430        "TCP frontend {address} sets alpn but no hostname (sni): alpn only matches within an \
431         SNI-scoped preread, so a frontend without hostname would silently ignore its alpn list. \
432         Set hostname or drop alpn."
433    )]
434    AlpnWithoutSni { address: SocketAddr },
435    /// Two TCP frontends on the same `(address, sni)` advertise an
436    /// overlapping ALPN protocol. Routing on a listener must be
437    /// deterministic: if both frontends could match the same ClientHello,
438    /// which cluster receives the connection would depend on iteration
439    /// order rather than configuration.
440    #[error(
441        "TCP frontends on {address} with sni {sni:?} both match ALPN protocol '{protocol}': \
442         ALPN matchers for the same (address, sni) must not overlap"
443    )]
444    TcpFrontendAlpnOverlap {
445        address: SocketAddr,
446        sni: Option<String>,
447        protocol: String,
448    },
449    /// More than one TCP frontend on the same `(address, sni)` left `alpn`
450    /// empty. An empty `alpn` is the catch-all match for that `sni`; two
451    /// catch-alls on the same `(address, sni)` are as ambiguous as two
452    /// frontends sharing an explicit protocol.
453    #[error(
454        "more than one TCP frontend on {address} with sni {sni:?} leaves alpn empty (the \
455         catch-all match): at most one frontend per (address, sni) may omit alpn"
456    )]
457    TcpFrontendMultipleAlpnCatchAll {
458        address: SocketAddr,
459        sni: Option<String>,
460    },
461    /// A TCP listener address is targeted by both a no-SNI frontend and at
462    /// least one SNI-scoped frontend. An SNI-enabled listener prereads the
463    /// ClientHello before choosing a backend; a raw-TCP fallback frontend
464    /// with no SNI to match against would be unreachable for any client
465    /// that doesn't send SNI, and ambiguous for any that does, so the two
466    /// shapes cannot share a listener.
467    #[error(
468        "TCP listener {address} is targeted by both a no-SNI frontend and at least one \
469         SNI-scoped frontend: an SNI-enabled listener must not also have a raw-TCP fallback \
470         frontend on the same address"
471    )]
472    TcpListenerMixesSniAndNoSni { address: SocketAddr },
473    /// `sni_preread_timeout` on a TCP listener exceeds that listener's
474    /// `front_timeout`. The preread phase is bounded by the frontend's own
475    /// inactivity timeout, so a preread budget longer than the timeout that
476    /// would kill the connection anyway can never fully elapse — it is
477    /// either a config mistake or silently dead configuration.
478    #[error(
479        "sni_preread_timeout = {sni_preread_timeout}s on TCP listener {address} exceeds its \
480         front_timeout = {front_timeout}s: the preread phase cannot outlive the frontend \
481         inactivity timeout that would already have closed the connection"
482    )]
483    SniPrereadTimeoutExceedsFrontTimeout {
484        address: SocketAddr,
485        sni_preread_timeout: u32,
486        front_timeout: u32,
487    },
488    /// `sni_preread_max_bytes` on a TCP listener targeted by at least one
489    /// SNI frontend exceeds the global `buffer_size`. The preread buffer is
490    /// carved out of the same per-session buffer the proxy uses for
491    /// relaying, so a preread ceiling above `buffer_size` can never be
492    /// reached in practice and signals a misconfiguration.
493    #[error(
494        "sni_preread_max_bytes = {sni_preread_max_bytes} on TCP listener {address} exceeds \
495         buffer_size = {buffer_size}: raise buffer_size to >= {sni_preread_max_bytes} or lower \
496         sni_preread_max_bytes"
497    )]
498    SniPrereadMaxBytesExceedsBufferSize {
499        address: SocketAddr,
500        sni_preread_max_bytes: u32,
501        buffer_size: u64,
502    },
503    /// `sni_preread_max_bytes` on a TCP listener targeted by at least one
504    /// SNI frontend is below [`MIN_SNI_PREREAD_MAX_BYTES`]. `0` in
505    /// particular makes the preread shell issue zero-length reads that can
506    /// never make progress -- the session spins until the event-loop
507    /// iteration guard (`MAX_LOOP_ITERATIONS`) trips instead of ever
508    /// completing a preread decision. The floor is a full TLS record
509    /// header (1-byte `ContentType` + 2-byte `ProtocolVersion` + 2-byte
510    /// length, RFC 8446 §5.1): below that, the shell cannot even learn how
511    /// many more bytes to wait for.
512    #[error(
513        "sni_preread_max_bytes = {sni_preread_max_bytes} on TCP listener {address} is below the \
514         minimum of {minimum} bytes (a full TLS record header): the preread shell could never \
515         read enough bytes to make progress. Raise sni_preread_max_bytes to >= {minimum}"
516    )]
517    SniPrereadMaxBytesTooSmall {
518        address: SocketAddr,
519        sni_preread_max_bytes: u32,
520        minimum: u32,
521    },
522}
523
524/// An HTTP, HTTPS or TCP listener as parsed from the `Listeners` section in the toml
525#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
526#[serde(deny_unknown_fields)]
527pub struct ListenerBuilder {
528    pub address: SocketAddr,
529    pub protocol: Option<ListenerProtocol>,
530    pub public_address: Option<SocketAddr>,
531    pub answer_301: Option<String>,
532    pub answer_400: Option<String>,
533    pub answer_401: Option<String>,
534    pub answer_404: Option<String>,
535    pub answer_408: Option<String>,
536    pub answer_413: Option<String>,
537    /// RFC 9110 §15.5.20 — returned when the request's `:authority` / `Host`
538    /// host does not match the TLS SNI negotiated for this connection.
539    pub answer_421: Option<String>,
540    pub answer_502: Option<String>,
541    pub answer_503: Option<String>,
542    pub answer_504: Option<String>,
543    pub answer_507: Option<String>,
544    /// RFC 6585 §4 — emitted when a request would have reached a backend
545    /// but the per-(cluster, source-IP) connection limit is full. Honoured
546    /// like the other deprecated `answer_NNN` fields: copies into the
547    /// listener-level `answers` map at the matching status.
548    pub answer_429: Option<String>,
549    pub tls_versions: Option<Vec<TlsVersion>>,
550    pub cipher_list: Option<Vec<String>>,
551    pub cipher_suites: Option<Vec<String>>,
552    pub groups_list: Option<Vec<String>>,
553    pub expect_proxy: Option<bool>,
554    #[serde(default = "default_sticky_name")]
555    pub sticky_name: String,
556    pub certificate: Option<String>,
557    pub certificate_chain: Option<String>,
558    pub key: Option<String>,
559    /// maximum time of inactivity for a frontend socket
560    pub front_timeout: Option<u32>,
561    /// maximum time of inactivity for a backend socket
562    pub back_timeout: Option<u32>,
563    /// maximum time to connect to a backend server
564    pub connect_timeout: Option<u32>,
565    /// maximum time to receive a request since the connection started
566    pub request_timeout: Option<u32>,
567    /// A [Config] to pull defaults from
568    pub config: Option<Config>,
569    /// Number of TLS 1.3 tickets to send to a client when establishing a connection.
570    /// The ticket allow the client to resume a session. This protects the client
571    /// agains session tracking. Defaults to 4.
572    pub send_tls13_tickets: Option<u64>,
573    /// ALPN protocols to advertise during TLS handshake, in order of preference.
574    /// Valid values: "h2", "http/1.1". Defaults to ["h2", "http/1.1"].
575    pub alpn_protocols: Option<Vec<String>>,
576    /// H2 flood detection: max RST_STREAM frames per second window (CVE-2023-44487, CVE-2019-9514)
577    pub h2_max_rst_stream_per_window: Option<u32>,
578    /// H2 flood detection: max PING frames per second window (CVE-2019-9512)
579    pub h2_max_ping_per_window: Option<u32>,
580    /// H2 flood detection: max SETTINGS frames per second window (CVE-2019-9515)
581    pub h2_max_settings_per_window: Option<u32>,
582    /// H2 flood detection: max empty DATA frames per second window (CVE-2019-9518)
583    pub h2_max_empty_data_per_window: Option<u32>,
584    /// H2 flood detection: max connection-level (stream 0) WINDOW_UPDATE
585    /// frames per sliding window. Caps non-zero stream-0 WINDOW_UPDATE floods
586    /// that would otherwise stay under the generic glitch counter. Default: 100.
587    pub h2_max_window_update_stream0_per_window: Option<u32>,
588    /// Name of the correlation header Sozu injects into every request and
589    /// response. Default: `Sozu-Id`. Operators can rebrand (e.g. `X-Edge-Id`)
590    /// without touching code.
591    pub sozu_id_header: Option<String>,
592    /// H2 flood detection: max CONTINUATION frames per header block (CVE-2024-27316)
593    pub h2_max_continuation_frames: Option<u32>,
594    /// H2 flood detection: max accumulated protocol anomalies before ENHANCE_YOUR_CALM
595    pub h2_max_glitch_count: Option<u32>,
596    /// H2 connection-level receive window size in bytes (RFC 9113 §6.9.2). Default: 1048576 (1MB).
597    pub h2_initial_connection_window: Option<u32>,
598    /// Maximum concurrent H2 streams (SETTINGS_MAX_CONCURRENT_STREAMS). Default: 100.
599    pub h2_max_concurrent_streams: Option<u32>,
600    /// Shrink threshold ratio for recycled stream slots. Default: 2.
601    pub h2_stream_shrink_ratio: Option<u32>,
602    /// H2 flood detection: absolute lifetime cap on RST_STREAM frames
603    /// received on a single connection (CVE-2023-44487). Default: 10000.
604    pub h2_max_rst_stream_lifetime: Option<u64>,
605    /// H2 flood detection: lifetime cap on "abusive" (pre-response-start)
606    /// RST_STREAM frames (Rapid Reset signature, CVE-2023-44487). Default: 50.
607    pub h2_max_rst_stream_abusive_lifetime: Option<u64>,
608    /// H2 flood detection: absolute lifetime cap on **server-emitted**
609    /// RST_STREAM frames (CVE-2025-8671 "MadeYouReset"). Only non-`NoError`
610    /// resets count — graceful cancels are exempt. Default: 500.
611    pub h2_max_rst_stream_emitted_lifetime: Option<u64>,
612    /// H2 flood detection: maximum accumulated HPACK-decoded header list
613    /// size per request (SETTINGS_MAX_HEADER_LIST_SIZE, RFC 9113 §6.5.2).
614    /// Default: 65536.
615    pub h2_max_header_list_size: Option<u32>,
616    /// Maximum HPACK dynamic table size (SETTINGS_HEADER_TABLE_SIZE) accepted
617    /// from the peer. Caps the value the peer advertises in SETTINGS frames to
618    /// prevent unbounded HPACK encoder memory growth. Default: 65536.
619    pub h2_max_header_table_size: Option<u32>,
620    /// Maximum number of materialized header fields per request — HPACK fields
621    /// plus expanded cookie crumbs (RFC 9113 §8.2.3). Bounds the HPACK
622    /// indexed-reference header bomb. Default: 128.
623    pub h2_max_header_fields: Option<u32>,
624    /// Per-stream idle timeout, in seconds. An open H2 stream that makes no
625    /// forward progress for this duration is cancelled (RST_STREAM / CANCEL)
626    /// to defend against slow-multiplex Slowloris. Default: 30.
627    pub h2_stream_idle_timeout_seconds: Option<u32>,
628    /// Maximum wall-clock seconds to wait for in-flight H2 streams after
629    /// `GOAWAY(NO_ERROR)` has been sent during soft-stop. Once the deadline
630    /// elapses the connection is forcibly closed with a final GOAWAY. Set to
631    /// `0` to wait for streams to finish (no forced close). Default: 5.
632    pub h2_graceful_shutdown_deadline_seconds: Option<u32>,
633    /// When true, every HTTP request served on this listener must have its
634    /// `:authority` / `Host` host exact-match the TLS SNI negotiated at
635    /// handshake (CWE-346 / CWE-444). Applies to HTTPS listeners only;
636    /// plaintext HTTP listeners never have an SNI to compare against.
637    /// Default: true.
638    pub strict_sni_binding: Option<bool>,
639    /// When true, this HTTPS listener only accepts HTTP/2 connections;
640    /// clients that do not negotiate `h2` via TLS ALPN (including those
641    /// that omit ALPN entirely) are dropped at handshake instead of
642    /// silently downgrading to HTTP/1.1. Default: false.
643    pub disable_http11: Option<bool>,
644    /// When true, any client-supplied `X-Real-IP` header is stripped from
645    /// requests before forwarding (anti-spoofing). Independently combinable
646    /// with `send_x_real_ip`. Default: false.
647    pub elide_x_real_ip: Option<bool>,
648    /// When true, a proxy-generated `X-Real-IP` header carrying the
649    /// connection peer IP (post-PROXY-v2 unwrap, i.e. the original client
650    /// IP) is appended to every forwarded request. Independently combinable
651    /// with `elide_x_real_ip`. Default: false.
652    pub send_x_real_ip: Option<bool>,
653    /// Per-status HTTP answer templates at listener scope — the **global
654    /// default** that fires whenever no cluster-level override matches.
655    /// Map key is the HTTP status code (e.g. `"503"`); map value is
656    /// either a filesystem path or an `inline:<body>` literal, see
657    /// [`resolve_answer_source`]. Loaded into
658    /// [`HttpListenerConfig::answers`] / [`HttpsListenerConfig::answers`]
659    /// at build time via [`load_answers`].
660    ///
661    /// Cluster-level [`FileClusterConfig::answers`] entries override the
662    /// matching status here for requests routed to that cluster.
663    ///
664    /// The deprecated per-status `answer_NNN` fields are still honoured
665    /// for backwards compatibility but are equivalent to a one-line entry
666    /// in this map; new configs should prefer `[listeners.answers]`.
667    pub answers: Option<BTreeMap<String, String>>,
668    /// Listener-default HSTS (RFC 6797) policy. When set, every HTTPS
669    /// frontend on this listener that does not declare its own `[hsts]`
670    /// block inherits this value. Per RFC 6797 §7.2 HSTS is rejected on
671    /// HTTP listeners at config-load time; this field is only meaningful
672    /// for HTTPS listeners. Defaults to `None` (no HSTS).
673    pub hsts: Option<FileHstsConfig>,
674    /// UDP listener only: maximum received datagram size, in bytes. Capped
675    /// at the effective `buffer_size` at config-load (clamp + warn when
676    /// larger). Defaults to [`DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE`].
677    pub max_rx_datagram_size: Option<u32>,
678    /// UDP listener only: maximum number of concurrent flows. `0` (the
679    /// default) selects the runtime auto policy (~70% soft RLIMIT_NOFILE);
680    /// a warning is emitted at config-load when an explicit value exceeds
681    /// that bound.
682    pub max_flows: Option<u32>,
683    /// TCP listener only: time allowed to receive enough bytes of the TLS
684    /// ClientHello to read the SNI extension, in seconds. Only meaningful
685    /// when at least one SNI-scoped frontend targets this listener.
686    /// Defaults to [`DEFAULT_SNI_PREREAD_TIMEOUT`].
687    pub sni_preread_timeout: Option<u32>,
688    /// TCP listener only: maximum number of bytes buffered while prereading
689    /// the TLS ClientHello looking for the SNI extension. Only meaningful
690    /// when at least one SNI-scoped frontend targets this listener; must
691    /// not exceed the global `buffer_size` (validated at config-load).
692    /// Defaults to [`DEFAULT_SNI_PREREAD_MAX_BYTES`].
693    pub sni_preread_max_bytes: Option<u32>,
694}
695
696pub fn default_sticky_name() -> String {
697    DEFAULT_STICKY_NAME.to_string()
698}
699
700impl ListenerBuilder {
701    /// starts building an HTTP Listener with config values for timeouts,
702    /// or defaults if no config is provided
703    pub fn new_http(address: SocketAddress) -> ListenerBuilder {
704        Self::new(address, ListenerProtocol::Http)
705    }
706
707    /// starts building an HTTPS Listener with config values for timeouts,
708    /// or defaults if no config is provided
709    pub fn new_tcp(address: SocketAddress) -> ListenerBuilder {
710        Self::new(address, ListenerProtocol::Tcp)
711    }
712
713    /// starts building a TCP Listener with config values for timeouts,
714    /// or defaults if no config is provided
715    pub fn new_https(address: SocketAddress) -> ListenerBuilder {
716        Self::new(address, ListenerProtocol::Https)
717    }
718
719    /// starts building a UDP Listener with config values for timeouts,
720    /// or defaults if no config is provided
721    pub fn new_udp(address: SocketAddress) -> ListenerBuilder {
722        Self::new(address, ListenerProtocol::Udp)
723    }
724
725    /// starts building a Listener
726    fn new(address: SocketAddress, protocol: ListenerProtocol) -> ListenerBuilder {
727        ListenerBuilder {
728            address: address.into(),
729            answer_301: None,
730            answer_401: None,
731            answer_400: None,
732            answer_404: None,
733            answer_408: None,
734            answer_413: None,
735            answer_421: None,
736            answer_502: None,
737            answer_503: None,
738            answer_504: None,
739            answer_507: None,
740            answer_429: None,
741            back_timeout: None,
742            certificate_chain: None,
743            certificate: None,
744            cipher_list: None,
745            cipher_suites: None,
746            groups_list: None,
747            config: None,
748            connect_timeout: None,
749            expect_proxy: None,
750            front_timeout: None,
751            key: None,
752            protocol: Some(protocol),
753            public_address: None,
754            request_timeout: None,
755            send_tls13_tickets: None,
756            sticky_name: DEFAULT_STICKY_NAME.to_string(),
757            tls_versions: None,
758            alpn_protocols: None,
759            h2_max_rst_stream_per_window: None,
760            h2_max_ping_per_window: None,
761            h2_max_settings_per_window: None,
762            h2_max_empty_data_per_window: None,
763            h2_max_window_update_stream0_per_window: None,
764            sozu_id_header: None,
765            h2_max_continuation_frames: None,
766            h2_max_glitch_count: None,
767            h2_initial_connection_window: None,
768            h2_max_concurrent_streams: None,
769            h2_stream_shrink_ratio: None,
770            h2_max_rst_stream_lifetime: None,
771            h2_max_rst_stream_abusive_lifetime: None,
772            h2_max_rst_stream_emitted_lifetime: None,
773            h2_max_header_list_size: None,
774            h2_max_header_table_size: None,
775            h2_max_header_fields: None,
776            h2_stream_idle_timeout_seconds: None,
777            h2_graceful_shutdown_deadline_seconds: None,
778            strict_sni_binding: None,
779            disable_http11: None,
780            elide_x_real_ip: None,
781            send_x_real_ip: None,
782            answers: None,
783            hsts: None,
784            max_rx_datagram_size: None,
785            max_flows: None,
786            sni_preread_timeout: None,
787            sni_preread_max_bytes: None,
788        }
789    }
790
791    pub fn with_public_address(&mut self, public_address: Option<SocketAddr>) -> &mut Self {
792        if let Some(address) = public_address {
793            self.public_address = Some(address);
794        }
795        self
796    }
797
798    pub fn with_answer_404_path<S>(&mut self, answer_404_path: Option<S>) -> &mut Self
799    where
800        S: ToString,
801    {
802        if let Some(path) = answer_404_path {
803            self.answer_404 = Some(path.to_string());
804        }
805        self
806    }
807
808    pub fn with_answer_503_path<S>(&mut self, answer_503_path: Option<S>) -> &mut Self
809    where
810        S: ToString,
811    {
812        if let Some(path) = answer_503_path {
813            self.answer_503 = Some(path.to_string());
814        }
815        self
816    }
817
818    pub fn with_tls_versions(&mut self, tls_versions: Vec<TlsVersion>) -> &mut Self {
819        self.tls_versions = Some(tls_versions);
820        self
821    }
822
823    pub fn with_cipher_list(&mut self, cipher_list: Option<Vec<String>>) -> &mut Self {
824        self.cipher_list = cipher_list;
825        self
826    }
827
828    pub fn with_cipher_suites(&mut self, cipher_suites: Option<Vec<String>>) -> &mut Self {
829        self.cipher_suites = cipher_suites;
830        self
831    }
832
833    pub fn with_alpn_protocols(&mut self, alpn_protocols: Option<Vec<String>>) -> &mut Self {
834        self.alpn_protocols = alpn_protocols;
835        self
836    }
837
838    /// When true, strip any client-supplied `X-Real-IP` header from
839    /// forwarded requests (anti-spoofing). Default: false.
840    pub fn with_elide_x_real_ip(&mut self, elide_x_real_ip: bool) -> &mut Self {
841        self.elide_x_real_ip = Some(elide_x_real_ip);
842        self
843    }
844
845    /// When true, append a proxy-generated `X-Real-IP` header carrying the
846    /// connection peer IP (post-PROXY-v2 unwrap) to every forwarded request.
847    /// Default: false.
848    pub fn with_send_x_real_ip(&mut self, send_x_real_ip: bool) -> &mut Self {
849        self.send_x_real_ip = Some(send_x_real_ip);
850        self
851    }
852
853    pub fn with_expect_proxy(&mut self, expect_proxy: bool) -> &mut Self {
854        self.expect_proxy = Some(expect_proxy);
855        self
856    }
857
858    pub fn with_sticky_name<S>(&mut self, sticky_name: Option<S>) -> &mut Self
859    where
860        S: ToString,
861    {
862        if let Some(name) = sticky_name {
863            self.sticky_name = name.to_string();
864        }
865        self
866    }
867
868    pub fn with_certificate<S>(&mut self, certificate: S) -> &mut Self
869    where
870        S: ToString,
871    {
872        self.certificate = Some(certificate.to_string());
873        self
874    }
875
876    pub fn with_certificate_chain(&mut self, certificate_chain: String) -> &mut Self {
877        self.certificate = Some(certificate_chain);
878        self
879    }
880
881    pub fn with_key<S>(&mut self, key: String) -> &mut Self
882    where
883        S: ToString,
884    {
885        self.key = Some(key);
886        self
887    }
888
889    pub fn with_front_timeout(&mut self, front_timeout: Option<u32>) -> &mut Self {
890        self.front_timeout = front_timeout;
891        self
892    }
893
894    pub fn with_back_timeout(&mut self, back_timeout: Option<u32>) -> &mut Self {
895        self.back_timeout = back_timeout;
896        self
897    }
898
899    pub fn with_connect_timeout(&mut self, connect_timeout: Option<u32>) -> &mut Self {
900        self.connect_timeout = connect_timeout;
901        self
902    }
903
904    pub fn with_request_timeout(&mut self, request_timeout: Option<u32>) -> &mut Self {
905        self.request_timeout = request_timeout;
906        self
907    }
908
909    /// Register a single per-status answer template file path on this
910    /// listener. The path is read off disk into the resulting listener's
911    /// `answers` map at build time via [`load_answers`]. Repeated calls
912    /// with the same status code overwrite the prior entry.
913    pub fn with_answer<S, P>(&mut self, code: S, path: P) -> &mut Self
914    where
915        S: ToString,
916        P: ToString,
917    {
918        self.answers
919            .get_or_insert_with(BTreeMap::new)
920            .insert(code.to_string(), path.to_string());
921        self
922    }
923
924    /// Replace the listener-scope answer-template path map. See
925    /// [`Self::with_answer`].
926    pub fn with_answers(&mut self, answers: BTreeMap<String, String>) -> &mut Self {
927        self.answers = Some(answers);
928        self
929    }
930
931    /// Get the custom HTTP answers from the file system using the provided paths
932    fn get_http_answers(&self) -> Result<Option<CustomHttpAnswers>, ConfigError> {
933        let http_answers = CustomHttpAnswers {
934            answer_301: read_http_answer_file(&self.answer_301)?,
935            answer_400: read_http_answer_file(&self.answer_400)?,
936            answer_401: read_http_answer_file(&self.answer_401)?,
937            answer_404: read_http_answer_file(&self.answer_404)?,
938            answer_408: read_http_answer_file(&self.answer_408)?,
939            answer_413: read_http_answer_file(&self.answer_413)?,
940            answer_421: read_http_answer_file(&self.answer_421)?,
941            answer_502: read_http_answer_file(&self.answer_502)?,
942            answer_503: read_http_answer_file(&self.answer_503)?,
943            answer_504: read_http_answer_file(&self.answer_504)?,
944            answer_507: read_http_answer_file(&self.answer_507)?,
945            answer_429: read_http_answer_file(&self.answer_429)?,
946        };
947        Ok(Some(http_answers))
948    }
949
950    /// Build the proto-side `answers` map for this listener.
951    ///
952    /// Merges, in order:
953    /// 1. legacy per-status `answer_NNN` fields (if set), so legacy state
954    ///    files round-trip into the new shape;
955    /// 2. the explicit `[listeners.answers]` map (loaded via [`load_answers`]),
956    ///    so new entries take precedence over legacy ones.
957    fn get_listener_answers(&self) -> Result<BTreeMap<String, String>, ConfigError> {
958        let mut out = BTreeMap::new();
959
960        // Pull bodies from the legacy per-status fields first so the new map
961        // takes precedence on collision. Empty bodies are skipped to keep the
962        // proto map minimal.
963        macro_rules! merge_legacy {
964            ($code:literal, $field:ident) => {
965                if let Some(body) = read_http_answer_file(&self.$field)? {
966                    out.insert($code.to_owned(), body);
967                }
968            };
969        }
970        merge_legacy!("301", answer_301);
971        merge_legacy!("400", answer_400);
972        merge_legacy!("401", answer_401);
973        merge_legacy!("404", answer_404);
974        merge_legacy!("408", answer_408);
975        merge_legacy!("413", answer_413);
976        merge_legacy!("421", answer_421);
977        merge_legacy!("502", answer_502);
978        merge_legacy!("503", answer_503);
979        merge_legacy!("504", answer_504);
980        merge_legacy!("507", answer_507);
981        merge_legacy!("429", answer_429);
982
983        if let Some(map) = &self.answers {
984            let loaded = load_answers(map)?;
985            out.extend(loaded);
986        }
987        Ok(out)
988    }
989
990    /// Assign the timeouts of the config to this listener, only if timeouts did not exist
991    fn assign_config_timeouts(&mut self, config: &Config) {
992        self.front_timeout = Some(self.front_timeout.unwrap_or(config.front_timeout));
993        self.back_timeout = Some(self.back_timeout.unwrap_or(config.back_timeout));
994        self.connect_timeout = Some(self.connect_timeout.unwrap_or(config.connect_timeout));
995        self.request_timeout = Some(self.request_timeout.unwrap_or(config.request_timeout));
996    }
997
998    /// build an HTTP listener with config timeouts, using defaults if no config is provided
999    pub fn to_http(&mut self, config: Option<&Config>) -> Result<HttpListenerConfig, ConfigError> {
1000        if self.protocol != Some(ListenerProtocol::Http) {
1001            return Err(ConfigError::WrongListenerProtocol {
1002                expected: ListenerProtocol::Http,
1003                found: self.protocol.to_owned(),
1004            });
1005        }
1006
1007        // RFC 6797 §7.2: `Strict-Transport-Security` MUST NOT appear on
1008        // plaintext-HTTP responses. Reject an `[hsts]` block on an HTTP
1009        // listener at config-load — `HttpListenerConfig` has no `hsts`
1010        // field, so silently dropping the operator's intent would be a
1011        // worse failure mode than a typed error here.
1012        if self.hsts.is_some() {
1013            return Err(ConfigError::HstsOnPlainHttp(format!(
1014                "HTTP listener {}",
1015                self.address
1016            )));
1017        }
1018
1019        if let Some(config) = config {
1020            self.assign_config_timeouts(config);
1021        }
1022
1023        let http_answers = self.get_http_answers()?;
1024        let answers = self.get_listener_answers()?;
1025
1026        let configuration = HttpListenerConfig {
1027            address: self.address.into(),
1028            public_address: self.public_address.map(|a| a.into()),
1029            expect_proxy: self.expect_proxy.unwrap_or(false),
1030            sticky_name: self.sticky_name.clone(),
1031            front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1032            back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1033            connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1034            request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1035            http_answers,
1036            answers,
1037            h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1038            h2_max_ping_per_window: self.h2_max_ping_per_window,
1039            h2_max_settings_per_window: self.h2_max_settings_per_window,
1040            h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1041            h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1042            h2_max_continuation_frames: self.h2_max_continuation_frames,
1043            h2_max_glitch_count: self.h2_max_glitch_count,
1044            h2_initial_connection_window: self.h2_initial_connection_window,
1045            h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1046            h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1047            h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1048            h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1049            h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1050            h2_max_header_list_size: self.h2_max_header_list_size,
1051            h2_max_header_table_size: self.h2_max_header_table_size,
1052            h2_max_header_fields: self.h2_max_header_fields,
1053            h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1054            h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1055            sozu_id_header: self.sozu_id_header.clone(),
1056            elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1057            send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1058            ..Default::default()
1059        };
1060
1061        // POST: the built listener binds exactly the address that was
1062        // requested — a listener whose address drifted here would bind the
1063        // wrong socket. (We reached this point only because the protocol guard
1064        // at entry confirmed this is an HTTP listener.)
1065        debug_assert_eq!(
1066            configuration.address,
1067            self.address.into(),
1068            "HTTP listener must bind the requested address"
1069        );
1070        Ok(configuration)
1071    }
1072
1073    /// build an HTTPS listener using defaults if no config or values were provided upstream
1074    pub fn to_tls(&mut self, config: Option<&Config>) -> Result<HttpsListenerConfig, ConfigError> {
1075        if self.protocol != Some(ListenerProtocol::Https) {
1076            return Err(ConfigError::WrongListenerProtocol {
1077                expected: ListenerProtocol::Https,
1078                found: self.protocol.to_owned(),
1079            });
1080        }
1081
1082        let default_cipher_list = DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect();
1083
1084        let cipher_list = self.cipher_list.clone().unwrap_or(default_cipher_list);
1085
1086        let cipher_suites = self
1087            .cipher_suites
1088            .clone()
1089            .unwrap_or_else(|| DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect());
1090
1091        let signature_algorithms: Vec<String> = DEFAULT_SIGNATURE_ALGORITHMS
1092            .into_iter()
1093            .map(String::from)
1094            .collect();
1095
1096        let groups_list = self
1097            .groups_list
1098            .clone()
1099            .unwrap_or_else(|| DEFAULT_GROUPS_LIST.into_iter().map(String::from).collect());
1100
1101        let alpn_protocols: Vec<String> = match &self.alpn_protocols {
1102            Some(protos) if !protos.is_empty() => {
1103                for proto in protos {
1104                    match proto.as_str() {
1105                        "h2" | "http/1.1" => {}
1106                        other => return Err(ConfigError::InvalidAlpnProtocol(other.to_owned())),
1107                    }
1108                }
1109                // disable_http11 + http/1.1 ALPN is a self-DoS — every
1110                // connection negotiates http/1.1 then is
1111                // immediately refused at `https.rs::upgrade_handshake`.
1112                // Reject the combination at config load.
1113                if self.disable_http11.unwrap_or(false) && protos.iter().any(|p| p == "http/1.1") {
1114                    return Err(ConfigError::DisableHttp11WithHttp11Alpn {
1115                        address: self.address.to_string(),
1116                    });
1117                }
1118                if !protos.iter().any(|p| p == "http/1.1") {
1119                    warn!(
1120                        "ALPN protocols do not include 'http/1.1'. Clients without H2 support will fail TLS negotiation."
1121                    );
1122                }
1123                // Deduplicate while preserving order
1124                let mut seen = std::collections::HashSet::new();
1125                protos
1126                    .iter()
1127                    .filter(|p| seen.insert(p.as_str()))
1128                    .cloned()
1129                    .collect()
1130            }
1131            _ => {
1132                // Same self-DoS check on the default ALPN list (which
1133                // contains "http/1.1") — `disable_http11 = true` with the
1134                // implicit default ALPN must also be rejected.
1135                if self.disable_http11.unwrap_or(false)
1136                    && DEFAULT_ALPN_PROTOCOLS.contains(&"http/1.1")
1137                {
1138                    return Err(ConfigError::DisableHttp11WithHttp11Alpn {
1139                        address: self.address.to_string(),
1140                    });
1141                }
1142                DEFAULT_ALPN_PROTOCOLS
1143                    .iter()
1144                    .map(|s| s.to_string())
1145                    .collect()
1146            }
1147        };
1148
1149        let versions = match self.tls_versions {
1150            None => vec![TlsVersion::TlsV12 as i32, TlsVersion::TlsV13 as i32],
1151            Some(ref v) => v.iter().map(|v| *v as i32).collect(),
1152        };
1153
1154        let key = self.key.as_ref().and_then(|path| {
1155            Config::load_file(path)
1156                .map_err(|e| {
1157                    error!("cannot load key at path '{}': {:?}", path, e);
1158                    e
1159                })
1160                .ok()
1161        });
1162        let certificate = self.certificate.as_ref().and_then(|path| {
1163            Config::load_file(path)
1164                .map_err(|e| {
1165                    error!("cannot load certificate at path '{}': {:?}", path, e);
1166                    e
1167                })
1168                .ok()
1169        });
1170        let certificate_chain = self
1171            .certificate_chain
1172            .as_ref()
1173            .and_then(|path| {
1174                Config::load_file(path)
1175                    .map_err(|e| {
1176                        error!("cannot load certificate chain at path '{}': {:?}", path, e);
1177                        e
1178                    })
1179                    .ok()
1180            })
1181            .map(split_certificate_chain)
1182            .unwrap_or_default();
1183
1184        let http_answers = self.get_http_answers()?;
1185        let answers = self.get_listener_answers()?;
1186
1187        if let Some(config) = config {
1188            self.assign_config_timeouts(config);
1189        }
1190
1191        let https_listener_config = HttpsListenerConfig {
1192            address: self.address.into(),
1193            sticky_name: self.sticky_name.clone(),
1194            public_address: self.public_address.map(|a| a.into()),
1195            cipher_list,
1196            versions,
1197            expect_proxy: self.expect_proxy.unwrap_or(false),
1198            key,
1199            certificate,
1200            certificate_chain,
1201            front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1202            back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1203            connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1204            request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1205            cipher_suites,
1206            signature_algorithms,
1207            groups_list,
1208            active: false,
1209            send_tls13_tickets: self
1210                .send_tls13_tickets
1211                .unwrap_or(DEFAULT_SEND_TLS_13_TICKETS),
1212            http_answers,
1213            answers,
1214            alpn_protocols,
1215            h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1216            h2_max_ping_per_window: self.h2_max_ping_per_window,
1217            h2_max_settings_per_window: self.h2_max_settings_per_window,
1218            h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1219            h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1220            h2_max_continuation_frames: self.h2_max_continuation_frames,
1221            h2_max_glitch_count: self.h2_max_glitch_count,
1222            h2_initial_connection_window: self.h2_initial_connection_window,
1223            h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1224            h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1225            h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1226            h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1227            h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1228            h2_max_header_list_size: self.h2_max_header_list_size,
1229            h2_max_header_table_size: self.h2_max_header_table_size,
1230            h2_max_header_fields: self.h2_max_header_fields,
1231            strict_sni_binding: self.strict_sni_binding,
1232            disable_http11: self.disable_http11,
1233            h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1234            h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1235            sozu_id_header: self.sozu_id_header.clone(),
1236            elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1237            send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1238            hsts: match self.hsts.as_ref() {
1239                Some(h) => Some(h.to_proto("listener")?),
1240                None => None,
1241            },
1242        };
1243
1244        // POST: the built listener binds the requested address and starts
1245        // inactive (the protocol guard at entry confirmed this is an HTTPS
1246        // listener).
1247        debug_assert_eq!(
1248            https_listener_config.address,
1249            self.address.into(),
1250            "HTTPS listener must bind the requested address"
1251        );
1252        debug_assert!(
1253            !https_listener_config.active,
1254            "a freshly built HTTPS listener must start inactive"
1255        );
1256        // POST: the resolved ALPN list is non-empty, contains only the two
1257        // protocols Sōzu speaks, and is duplicate-free — the validation/dedup
1258        // branches above are the sole producers, so a malformed list here would
1259        // mean an unvalidated path slipped through.
1260        debug_assert!(
1261            !https_listener_config.alpn_protocols.is_empty(),
1262            "resolved ALPN list must not be empty"
1263        );
1264        debug_assert!(
1265            https_listener_config
1266                .alpn_protocols
1267                .iter()
1268                .all(|p| p == "h2" || p == "http/1.1"),
1269            "resolved ALPN list must contain only h2 and http/1.1"
1270        );
1271        debug_assert!(
1272            {
1273                let mut seen = std::collections::HashSet::new();
1274                https_listener_config
1275                    .alpn_protocols
1276                    .iter()
1277                    .all(|p| seen.insert(p))
1278            },
1279            "resolved ALPN list must be duplicate-free"
1280        );
1281        // POST: disable_http11 + http/1.1 in ALPN is a self-DoS that the
1282        // validation above rejects — an Ok return must never carry that combo.
1283        debug_assert!(
1284            !(self.disable_http11.unwrap_or(false)
1285                && https_listener_config
1286                    .alpn_protocols
1287                    .iter()
1288                    .any(|p| p == "http/1.1")),
1289            "disable_http11 with http/1.1 in ALPN must have been rejected"
1290        );
1291        Ok(https_listener_config)
1292    }
1293
1294    /// build an HTTPS listener using defaults if no config or values were provided upstream
1295    pub fn to_tcp(&mut self, config: Option<&Config>) -> Result<TcpListenerConfig, ConfigError> {
1296        if self.protocol != Some(ListenerProtocol::Tcp) {
1297            return Err(ConfigError::WrongListenerProtocol {
1298                expected: ListenerProtocol::Tcp,
1299                found: self.protocol.to_owned(),
1300            });
1301        }
1302
1303        if let Some(config) = config {
1304            self.assign_config_timeouts(config);
1305        }
1306
1307        let tcp_listener_config = TcpListenerConfig {
1308            address: self.address.into(),
1309            public_address: self.public_address.map(|a| a.into()),
1310            expect_proxy: self.expect_proxy.unwrap_or(false),
1311            front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1312            back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1313            connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1314            active: false,
1315            sni_preread_timeout: Some(
1316                self.sni_preread_timeout
1317                    .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT),
1318            ),
1319            sni_preread_max_bytes: Some(
1320                self.sni_preread_max_bytes
1321                    .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES),
1322            ),
1323        };
1324
1325        // POST: the built listener binds exactly the requested address and is
1326        // created inactive (activation is a later, explicit step).
1327        debug_assert_eq!(
1328            tcp_listener_config.address,
1329            self.address.into(),
1330            "TCP listener must bind the requested address"
1331        );
1332        debug_assert!(
1333            !tcp_listener_config.active,
1334            "a freshly built TCP listener must start inactive"
1335        );
1336        Ok(tcp_listener_config)
1337    }
1338
1339    /// build a UDP listener. UDP has no `expect_proxy` / `connect_timeout`;
1340    /// flows are keyed by 4-tuple and torn down on idle.
1341    ///
1342    /// Timeouts: an unset `front_timeout` / `back_timeout` falls back to the
1343    /// UDP-specific defaults ([`DEFAULT_UDP_FRONT_TIMEOUT`] /
1344    /// [`DEFAULT_UDP_BACK_TIMEOUT`], both 30 s) — *not* the global HTTP/TCP
1345    /// `front_timeout` (60 s) / `back_timeout`. This keeps the effective
1346    /// default in lock-step with the CLI help and the proto
1347    /// `UdpListenerConfig` defaults (both 30 s). The global config is consulted
1348    /// only for `buffer_size` (the `max_rx_datagram_size` cap).
1349    ///
1350    /// Validation:
1351    /// * `max_rx_datagram_size` is clamped to the effective `buffer_size`
1352    ///   (with a warning) so a datagram can never exceed the pool buffer.
1353    /// * an explicit non-zero `max_flows` that exceeds ~70% of the soft
1354    ///   `RLIMIT_NOFILE` emits a warning (the per-flow connected sockets
1355    ///   would otherwise risk EMFILE).
1356    pub fn to_udp(&mut self, config: Option<&Config>) -> Result<UdpListenerConfig, ConfigError> {
1357        if self.protocol != Some(ListenerProtocol::Udp) {
1358            return Err(ConfigError::WrongListenerProtocol {
1359                expected: ListenerProtocol::Udp,
1360                found: self.protocol.to_owned(),
1361            });
1362        }
1363
1364        let mut max_rx_datagram_size = self
1365            .max_rx_datagram_size
1366            .unwrap_or(DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE);
1367        let buffer_size = config.map(|c| c.buffer_size).unwrap_or(DEFAULT_BUFFER_SIZE);
1368        if u64::from(max_rx_datagram_size) > buffer_size {
1369            warn!(
1370                "UDP listener {}: max_rx_datagram_size = {} exceeds buffer_size = {}, clamping to buffer_size",
1371                self.address, max_rx_datagram_size, buffer_size
1372            );
1373            max_rx_datagram_size = buffer_size as u32;
1374        }
1375
1376        let max_flows = self.max_flows.unwrap_or(DEFAULT_UDP_MAX_FLOWS);
1377        if max_flows > 0
1378            && let Some(soft_limit) = soft_rlimit_nofile()
1379        {
1380            let advisory = soft_limit.saturating_mul(7) / 10;
1381            if u64::from(max_flows) > advisory {
1382                warn!(
1383                    "UDP listener {}: max_flows = {} exceeds ~70% of the soft RLIMIT_NOFILE ({}); \
1384                         per-flow connected sockets may hit EMFILE",
1385                    self.address, max_flows, advisory
1386                );
1387            }
1388        }
1389
1390        Ok(UdpListenerConfig {
1391            address: self.address.into(),
1392            public_address: self.public_address.map(|a| a.into()),
1393            front_timeout: self.front_timeout.unwrap_or(DEFAULT_UDP_FRONT_TIMEOUT),
1394            back_timeout: self.back_timeout.unwrap_or(DEFAULT_UDP_BACK_TIMEOUT),
1395            max_rx_datagram_size,
1396            max_flows,
1397            active: false,
1398        })
1399    }
1400}
1401
1402/// Read the soft `RLIMIT_NOFILE` (max open file descriptors). Used as an
1403/// advisory ceiling for `max_flows` on UDP listeners. Returns `None` when
1404/// the limit cannot be read so callers skip the advisory check rather than
1405/// failing config-load.
1406fn soft_rlimit_nofile() -> Option<u64> {
1407    let mut limit = libc::rlimit {
1408        rlim_cur: 0,
1409        rlim_max: 0,
1410    };
1411    // SAFETY: `getrlimit` writes into the provided `rlimit` out-parameter and
1412    // does not retain the pointer. A non-zero return means failure, in which
1413    // case we ignore the (uninitialised-by-contract) value and return None.
1414    let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) };
1415    if rc == 0 {
1416        // `rlim_cur` is `rlim_t`, which is `u64` on the Tier-1 targets sozu
1417        // builds for, so it already matches the `Option<u64>` return type.
1418        Some(limit.rlim_cur)
1419    } else {
1420        None
1421    }
1422}
1423
1424/// read a custom HTTP answer from a file
1425fn read_http_answer_file(path: &Option<String>) -> Result<Option<String>, ConfigError> {
1426    match path {
1427        Some(path) => {
1428            let mut content = String::new();
1429            let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
1430                path_to_open: path.to_owned(),
1431                io_error,
1432            })?;
1433
1434            file.read_to_string(&mut content)
1435                .map_err(|io_error| ConfigError::FileRead {
1436                    path_to_read: path.to_owned(),
1437                    io_error,
1438                })?;
1439
1440            Ok(Some(content))
1441        }
1442        None => Ok(None),
1443    }
1444}
1445
1446/// Resolve a single `answers` map entry into the literal template body
1447/// the proto layer expects.
1448///
1449/// The same resolution rule applies to entries at every layer:
1450/// * **Listener-level** `[listeners.<id>.answers]` — the global default
1451///   that fires whenever no more specific override matches.
1452/// * **Cluster-level** `[clusters.<id>.answers]` — overrides the
1453///   listener-level default for the matching status code on requests
1454///   routed to that cluster.
1455///
1456/// Two source forms are accepted:
1457/// * **Filesystem path** — the value starts with the `file://` URI
1458///   scheme. Everything after the prefix is treated as a path; the
1459///   path is opened and read into a string. Mirrors the on-disk
1460///   loading the per-status [`read_http_answer_file`] helper performs
1461///   for the deprecated `answer_301`..`answer_507` fields.
1462/// * **Inline literal** (default) — anything else. The value is taken
1463///   verbatim as the template body, including an empty string (a
1464///   0-byte response payload, typical with `Connection: close` and no
1465///   headers). The bare-string default keeps the common case — a
1466///   short canned response — typing-light; operators who need a file
1467///   say so explicitly with `file://`.
1468pub fn resolve_answer_source(value: &str) -> Result<String, ConfigError> {
1469    if let Some(path) = value.strip_prefix("file://") {
1470        let mut content = String::new();
1471        let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
1472            path_to_open: path.to_owned(),
1473            io_error,
1474        })?;
1475        file.read_to_string(&mut content)
1476            .map_err(|io_error| ConfigError::FileRead {
1477                path_to_read: path.to_owned(),
1478                io_error,
1479            })?;
1480        return Ok(content);
1481    }
1482    Ok(value.to_owned())
1483}
1484
1485/// Load every per-status template referenced by `answers`.
1486///
1487/// `answers` maps an HTTP status code (e.g. `"503"`) to either a
1488/// filesystem path or an `inline:<body>` literal — see
1489/// [`resolve_answer_source`] for the resolution rules. Each entry is
1490/// resolved into a body string and inserted into the returned map
1491/// under the same key, ready to be assigned to the proto-level
1492/// `answers` field on a [`HttpListenerConfig`] / [`HttpsListenerConfig`]
1493/// / [`Cluster`]. Empty values are skipped (treated as "preserve
1494/// current") so the caller can use them as a no-op stub in example
1495/// configs.
1496///
1497/// Errors map to the existing `ConfigError::FileOpen` /
1498/// `ConfigError::FileRead` variants so the operator gets the same
1499/// diagnostics whether the path comes from this map or from the
1500/// deprecated per-status `answer_301`..`answer_507` fields.
1501pub fn load_answers(
1502    answers: &BTreeMap<String, String>,
1503) -> Result<BTreeMap<String, String>, ConfigError> {
1504    let mut out = BTreeMap::new();
1505    for (code, value) in answers {
1506        if value.is_empty() {
1507            continue;
1508        }
1509        out.insert(code.to_owned(), resolve_answer_source(value)?);
1510    }
1511    // POST: the loaded map never invents a status code (every output key is an
1512    // input key) and never grows past the input — empty-valued entries are
1513    // skipped, so |out| <= |answers|.
1514    debug_assert!(
1515        out.len() <= answers.len(),
1516        "load_answers must not synthesize entries"
1517    );
1518    debug_assert!(
1519        out.keys().all(|k| answers.contains_key(k)),
1520        "every loaded status code must come from the input map"
1521    );
1522    Ok(out)
1523}
1524
1525/// Cardinality knob for metrics labels in the StatsD network drain.
1526///
1527/// Mirrors HAProxy's `process|frontend|backend|server` extra-counters opt-in.
1528/// Operators choose the lowest level that satisfies their dashboards so that
1529/// the keyspace stays bounded. Each level is a SUPERSET of the previous one:
1530///
1531/// - `process` — proxy-only counters (no listener, cluster, or backend label).
1532/// - `frontend` — adds per-listener (frontend) breakdown.
1533/// - `cluster` — adds per-cluster aggregation. **Default** (preserves the
1534///   pre-knob behaviour).
1535/// - `backend` — adds per-backend aggregation (cluster + backend, highest
1536///   cardinality).
1537#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
1538#[serde(rename_all = "lowercase")]
1539#[derive(Default)]
1540pub enum MetricDetailLevel {
1541    Process,
1542    Frontend,
1543    #[default]
1544    Cluster,
1545    Backend,
1546}
1547
1548impl From<MetricDetailLevel> for MetricDetail {
1549    fn from(level: MetricDetailLevel) -> Self {
1550        match level {
1551            MetricDetailLevel::Process => MetricDetail::DetailProcess,
1552            MetricDetailLevel::Frontend => MetricDetail::DetailFrontend,
1553            MetricDetailLevel::Cluster => MetricDetail::DetailCluster,
1554            MetricDetailLevel::Backend => MetricDetail::DetailBackend,
1555        }
1556    }
1557}
1558
1559impl From<MetricDetail> for MetricDetailLevel {
1560    /// Reverse of [`From<MetricDetailLevel> for MetricDetail`] — used by the
1561    /// worker side to convert the protobuf wire enum back into the
1562    /// configuration enum before passing it to `sozu_lib::metrics::setup`.
1563    fn from(detail: MetricDetail) -> Self {
1564        match detail {
1565            MetricDetail::DetailProcess => MetricDetailLevel::Process,
1566            MetricDetail::DetailFrontend => MetricDetailLevel::Frontend,
1567            MetricDetail::DetailCluster => MetricDetailLevel::Cluster,
1568            MetricDetail::DetailBackend => MetricDetailLevel::Backend,
1569        }
1570    }
1571}
1572
1573#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1574#[serde(deny_unknown_fields)]
1575pub struct MetricsConfig {
1576    pub address: SocketAddr,
1577    #[serde(default)]
1578    pub tagged_metrics: bool,
1579    #[serde(default)]
1580    pub prefix: Option<String>,
1581    /// Cardinality knob for label-aware metrics. Defaults to `cluster` to
1582    /// preserve historical behaviour. See [`MetricDetailLevel`].
1583    #[serde(default)]
1584    pub detail: MetricDetailLevel,
1585}
1586
1587#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1588#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1589#[serde(deny_unknown_fields)]
1590pub enum PathRuleType {
1591    Prefix,
1592    Regex,
1593    Equals,
1594}
1595
1596#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1597#[serde(deny_unknown_fields)]
1598pub struct FileClusterFrontendConfig {
1599    pub address: SocketAddr,
1600    pub hostname: Option<String>,
1601    /// TCP frontend only (sozu-proxy/sozu#1279): ALPN protocol names to
1602    /// match, read from the TLS ClientHello during the same preread that
1603    /// resolves `hostname` (mapped to the wire `sni` field for TCP
1604    /// frontends). Empty (the default) is the catch-all for this
1605    /// frontend's `hostname`/SNI on its listener. Rejected on HTTP/HTTPS
1606    /// frontends — ALPN there is negotiated by the listener's
1607    /// `alpn_protocols`, not per-frontend.
1608    #[serde(default)]
1609    pub alpn: Vec<String>,
1610    /// creates a path routing rule where the request URL path has to match this
1611    pub path: Option<String>,
1612    /// declares whether the path rule is Prefix (default), Regex, or Equals
1613    pub path_type: Option<PathRuleType>,
1614    pub method: Option<String>,
1615    pub certificate: Option<String>,
1616    pub key: Option<String>,
1617    pub certificate_chain: Option<String>,
1618    #[serde(default)]
1619    pub tls_versions: Vec<TlsVersion>,
1620    #[serde(default)]
1621    pub position: RulePosition,
1622    pub tags: Option<BTreeMap<String, String>>,
1623    /// Frontend-level redirect policy. Accepted values are `forward`
1624    /// (default — route to the backend), `permanent` (return 301 with the
1625    /// computed `Location`), or `unauthorized` (return 401 with
1626    /// `WWW-Authenticate: Basic realm=…`). Case-insensitive.
1627    pub redirect: Option<String>,
1628    /// Scheme used when emitting a permanent redirect's `Location`. Accepted
1629    /// values are `use-same` (default — preserve request scheme), `use-http`,
1630    /// `use-https`. Case-insensitive.
1631    pub redirect_scheme: Option<String>,
1632    /// Optional template applied to the emitted permanent-redirect response
1633    /// body. Supports the `%REDIRECT_LOCATION` and other variables
1634    /// documented in `doc/configure.md`.
1635    pub redirect_template: Option<String>,
1636    /// Rewrite host template. Supports `$HOST[n]` / `$PATH[n]` placeholders
1637    /// populated from regex captures collected during routing.
1638    pub rewrite_host: Option<String>,
1639    /// Rewrite path template. Same grammar as `rewrite_host`.
1640    pub rewrite_path: Option<String>,
1641    /// Optional literal port override on the rewritten URL.
1642    pub rewrite_port: Option<u32>,
1643    /// When true, requests routed through this frontend must carry a valid
1644    /// `Authorization: Basic <user:pass>` header whose hash matches one of
1645    /// the cluster's `authorized_hashes`. Default: false.
1646    pub required_auth: Option<bool>,
1647    /// Header mutations applied to requests and/or responses passing through
1648    /// this frontend. See [`HeaderEditConfig`] for the empty-value-deletes
1649    /// semantics (HAProxy `del-header` parity).
1650    pub headers: Option<Vec<HeaderEditConfig>>,
1651    /// Per-frontend HSTS (RFC 6797) policy. When set, overrides any
1652    /// listener-default HSTS for this frontend. Set `enabled = false`
1653    /// to suppress an inherited listener default. Per RFC 6797 §7.2,
1654    /// HSTS is rejected on plain-HTTP frontends at config-load time.
1655    pub hsts: Option<FileHstsConfig>,
1656}
1657
1658/// A single header mutation as serialised under
1659/// `[[clusters.<id>.frontends.headers]]`. Maps to the proto [`Header`]
1660/// message at request-build time.
1661///
1662/// `position` accepts `request`, `response`, or `both` (case-insensitive).
1663/// An empty `value` deletes the header by name (HAProxy `del-header` parity).
1664#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1665#[serde(deny_unknown_fields)]
1666pub struct HeaderEditConfig {
1667    pub position: String,
1668    pub key: String,
1669    pub value: String,
1670}
1671
1672/// HSTS (HTTP Strict Transport Security, RFC 6797) policy as serialised
1673/// under `[https.listeners.default.hsts]` (listener default) or
1674/// `[clusters.<id>.frontends.hsts]` (per-frontend override).
1675///
1676/// `enabled` is REQUIRED whenever the block is present — its presence vs
1677/// absence disambiguates "preserve current" / "explicit disable" / "enable"
1678/// on hot-reconfig partial updates.
1679///
1680/// When `enabled = true` and `max_age` is omitted, sozu substitutes
1681/// [`DEFAULT_HSTS_MAX_AGE`] (1 year) at config-load time.
1682#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1683#[serde(deny_unknown_fields)]
1684pub struct FileHstsConfig {
1685    /// REQUIRED. `true` enables HSTS for this scope; `false` suppresses
1686    /// any inherited listener default (explicit-disable signal).
1687    pub enabled: Option<bool>,
1688    /// `Strict-Transport-Security: max-age=<seconds>`. Optional —
1689    /// defaults to [`DEFAULT_HSTS_MAX_AGE`] when `enabled = true`.
1690    /// `max_age = 0` is the RFC 6797 §11.4 kill switch and is allowed
1691    /// silently; `0 < max_age < 86400` warns at config-load.
1692    pub max_age: Option<u32>,
1693    /// Append `; includeSubDomains` to the rendered header.
1694    pub include_subdomains: Option<bool>,
1695    /// Append `; preload` to the rendered header. Opt-in only — see RFC
1696    /// 6797 §14.2 and <https://hstspreload.org/>.
1697    pub preload: Option<bool>,
1698    /// Operator opt-in to override any backend-supplied
1699    /// `Strict-Transport-Security` header. RFC 6797 §6.1 default
1700    /// behaviour is to PRESERVE the backend's value (sozu's edit uses
1701    /// `HeaderEditMode::SetIfAbsent`). Set this to `true` to harden a
1702    /// stale or weak upstream HSTS policy centrally — the materialiser
1703    /// then uses `HeaderEditMode::Set`, replacing any backend STS with
1704    /// sozu's rendered value.
1705    pub force_replace_backend: Option<bool>,
1706}
1707
1708impl FileHstsConfig {
1709    /// Validate and convert the file-level [`FileHstsConfig`] into the
1710    /// proto [`HstsConfig`]. `scope` is a human-readable string (e.g.
1711    /// "listener" or "frontend api/example.com") surfaced into errors
1712    /// and warnings so the operator can pinpoint the offending block.
1713    ///
1714    /// Validation:
1715    /// - `enabled` is required when any other field is set
1716    ///   (`HstsEnabledRequired`); the parser returns the typed error so
1717    ///   callers can fail fast.
1718    /// - `enabled = true && max_age = None` substitutes
1719    ///   [`DEFAULT_HSTS_MAX_AGE`].
1720    /// - `0 < max_age < 86400` warns (likely misconfig — sub-day max-age
1721    ///   is useful only for testing).
1722    /// - `preload = true` with `max_age < DEFAULT_HSTS_MAX_AGE` or
1723    ///   `include_subdomains != Some(true)` warns (the Chrome HSTS
1724    ///   preload list will reject the host).
1725    /// - `max_age = 0` is allowed silently (RFC 6797 §11.4 kill switch).
1726    pub fn to_proto(&self, scope: &str) -> Result<HstsConfig, ConfigError> {
1727        let enabled = match self.enabled {
1728            Some(v) => v,
1729            None => return Err(ConfigError::HstsEnabledRequired(scope.to_owned())),
1730        };
1731
1732        let max_age = match (enabled, self.max_age) {
1733            (true, None) => Some(DEFAULT_HSTS_MAX_AGE),
1734            (_, m) => m,
1735        };
1736
1737        if let Some(value) = max_age
1738            && value > 0
1739            && value < 86_400
1740        {
1741            warn!(
1742                "HSTS max_age = {}s on {} is below 1 day — this is almost certainly a \
1743                 misconfiguration. RFC 6797 §11.4 reserves max_age = 0 as the explicit kill \
1744                 switch.",
1745                value, scope
1746            );
1747        }
1748
1749        let include_subdomains = self.include_subdomains;
1750        let preload = self.preload;
1751
1752        if matches!(preload, Some(true)) {
1753            let max_age_value = max_age.unwrap_or(0);
1754            if max_age_value < DEFAULT_HSTS_MAX_AGE {
1755                warn!(
1756                    "HSTS preload = true on {} with max_age = {}s; the Chrome HSTS preload \
1757                     list requires max_age >= {} (https://hstspreload.org/).",
1758                    scope, max_age_value, DEFAULT_HSTS_MAX_AGE
1759                );
1760            }
1761            if include_subdomains != Some(true) {
1762                warn!(
1763                    "HSTS preload = true on {} without include_subdomains = true; the Chrome \
1764                     HSTS preload list requires includeSubDomains \
1765                     (https://hstspreload.org/).",
1766                    scope
1767                );
1768            }
1769        }
1770
1771        let config = HstsConfig {
1772            enabled: Some(enabled),
1773            max_age,
1774            include_subdomains,
1775            preload,
1776            force_replace_backend: self.force_replace_backend,
1777        };
1778
1779        // POST: a built HstsConfig always records an explicit `enabled` flag
1780        // (the `None` case errored above), and an enabled policy always carries
1781        // a max_age — defaulted to DEFAULT_HSTS_MAX_AGE when the operator left
1782        // it unset, so the worker never emits an `max-age`-less STS header.
1783        debug_assert_eq!(
1784            config.enabled,
1785            Some(enabled),
1786            "built HSTS config must record the resolved enabled flag"
1787        );
1788        debug_assert!(
1789            !enabled || config.max_age.is_some(),
1790            "an enabled HSTS policy must carry a max_age"
1791        );
1792        Ok(config)
1793    }
1794}
1795
1796impl FileClusterFrontendConfig {
1797    pub fn to_tcp_front(&self) -> Result<TcpFrontendConfig, ConfigError> {
1798        if self.path.is_some() {
1799            return Err(ConfigError::InvalidFrontendConfig(
1800                "path_prefix".to_string(),
1801            ));
1802        }
1803        if self.certificate.is_some() {
1804            return Err(ConfigError::InvalidFrontendConfig(
1805                "certificate".to_string(),
1806            ));
1807        }
1808        if self.certificate_chain.is_some() {
1809            return Err(ConfigError::InvalidFrontendConfig(
1810                "certificate_chain".to_string(),
1811            ));
1812        }
1813
1814        // `hostname` maps to the TCP frontend's `sni` (sozu-proxy/sozu#1279):
1815        // an SNI-scoped frontend reads the same TOML key HTTP frontends use
1816        // for their routing hostname, validated for the TCP-specific
1817        // exact-or-single-wildcard shape.
1818        let sni = match &self.hostname {
1819            Some(hostname) => Some(validate_sni_pattern(hostname)?),
1820            None => None,
1821        };
1822
1823        // `alpn` only ever gets consulted from within the SNI-scoped
1824        // preread route table (sozu-proxy/sozu#1279 hardening): a frontend
1825        // with no `hostname`/`sni` installs the worker's raw no-SNI
1826        // catch-all path instead, which never looks at `alpn` at all. Left
1827        // unchecked, this would load fine and then silently never enforce
1828        // the configured protocol list.
1829        if sni.is_none() && !self.alpn.is_empty() {
1830            return Err(ConfigError::AlpnWithoutSni {
1831                address: self.address,
1832            });
1833        }
1834
1835        let tcp_front = TcpFrontendConfig {
1836            address: self.address,
1837            tags: self.tags.clone(),
1838            sni,
1839            alpn: self.alpn.clone(),
1840            // Resolved against `known_addresses` in `populate_clusters`; a
1841            // bare `to_tcp_front` (no listener context) defaults to TCP.
1842            udp: false,
1843        };
1844        // POST: a TCP frontend binds exactly the requested address and carries
1845        // no HTTP-only attributes — the guards above reject path / certificate,
1846        // so an Ok return is a witness that none leaked through (an L7
1847        // attribute on an L4 frontend is a config-shape violation). `hostname`
1848        // is intentionally excluded from this witness: it is consumed above
1849        // into `sni`, not rejected.
1850        debug_assert_eq!(
1851            tcp_front.address, self.address,
1852            "TCP frontend must bind the requested address"
1853        );
1854        debug_assert!(
1855            self.path.is_none() && self.certificate.is_none() && self.certificate_chain.is_none(),
1856            "a built TCP frontend must carry no HTTP-only attributes"
1857        );
1858        debug_assert!(
1859            tcp_front.sni.is_some() || tcp_front.alpn.is_empty(),
1860            "a built TCP frontend without sni must never carry a non-empty alpn"
1861        );
1862        Ok(tcp_front)
1863    }
1864
1865    pub fn to_http_front(&self, _cluster_id: &str) -> Result<HttpFrontendConfig, ConfigError> {
1866        if !self.alpn.is_empty() {
1867            return Err(ConfigError::InvalidFrontendConfig("alpn".to_string()));
1868        }
1869
1870        let hostname = match &self.hostname {
1871            Some(hostname) => hostname.to_owned(),
1872            None => {
1873                return Err(ConfigError::Missing(MissingKind::Field(
1874                    "hostname".to_string(),
1875                )));
1876            }
1877        };
1878
1879        let key_opt = match self.key.as_ref() {
1880            None => None,
1881            Some(path) => {
1882                let key = Config::load_file(path)?;
1883                Some(key)
1884            }
1885        };
1886
1887        let certificate_opt = match self.certificate.as_ref() {
1888            None => None,
1889            Some(path) => {
1890                let certificate = Config::load_file(path)?;
1891                Some(certificate)
1892            }
1893        };
1894
1895        let certificate_chain = match self.certificate_chain.as_ref() {
1896            None => None,
1897            Some(path) => {
1898                let certificate_chain = Config::load_file(path)?;
1899                Some(split_certificate_chain(certificate_chain))
1900            }
1901        };
1902
1903        let path = match (self.path.as_ref(), self.path_type.as_ref()) {
1904            (None, _) => PathRule::prefix("".to_string()),
1905            (Some(s), Some(PathRuleType::Prefix)) => PathRule::prefix(s.to_string()),
1906            (Some(s), Some(PathRuleType::Regex)) => PathRule::regex(s.to_string()),
1907            (Some(s), Some(PathRuleType::Equals)) => PathRule::equals(s.to_string()),
1908            (Some(s), None) => PathRule::prefix(s.clone()),
1909        };
1910
1911        let redirect = match self.redirect.as_deref() {
1912            Some(v) => Some(parse_redirect_policy(v)?),
1913            None => None,
1914        };
1915        let redirect_scheme = match self.redirect_scheme.as_deref() {
1916            Some(v) => Some(parse_redirect_scheme(v)?),
1917            None => None,
1918        };
1919
1920        let headers = match self.headers.as_ref() {
1921            Some(entries) => {
1922                let mut out = Vec::with_capacity(entries.len());
1923                for (index, entry) in entries.iter().enumerate() {
1924                    out.push(parse_header_edit(index, entry)?);
1925                }
1926                out
1927            }
1928            None => Vec::new(),
1929        };
1930
1931        // RFC 6797 §7.2: `Strict-Transport-Security` MUST NOT appear on
1932        // plaintext-HTTP responses. A frontend without a key+certificate
1933        // pair generates `RequestType::AddHttpFrontend` in
1934        // `HttpFrontendConfig::generate_requests`, so HSTS configured
1935        // there would silently target an HTTP frontend. Reject at
1936        // config-load before the cert-presence branch can consume it.
1937        let frontend_serves_https = key_opt.is_some() && certificate_opt.is_some();
1938        let hsts = match self.hsts.as_ref() {
1939            Some(h) => {
1940                if !frontend_serves_https {
1941                    return Err(ConfigError::HstsOnPlainHttp(format!(
1942                        "frontend {_cluster_id}/{hostname}"
1943                    )));
1944                }
1945                Some(h.to_proto(&format!("frontend {_cluster_id}/{hostname}"))?)
1946            }
1947            None => None,
1948        };
1949
1950        Ok(HttpFrontendConfig {
1951            address: self.address,
1952            hostname,
1953            certificate: certificate_opt,
1954            key: key_opt,
1955            certificate_chain,
1956            tls_versions: self.tls_versions.clone(),
1957            position: self.position,
1958            path,
1959            method: self.method.clone(),
1960            tags: self.tags.clone(),
1961            redirect,
1962            redirect_scheme,
1963            redirect_template: self.redirect_template.clone(),
1964            rewrite_host: self.rewrite_host.clone(),
1965            rewrite_path: self.rewrite_path.clone(),
1966            rewrite_port: self.rewrite_port,
1967            required_auth: self.required_auth,
1968            headers,
1969            hsts,
1970        })
1971    }
1972}
1973
1974/// Validates and normalizes a TCP frontend's SNI pattern
1975/// (sozu-proxy/sozu#1279): either an exact hostname or a single leading
1976/// `*.` wildcard label. Rejects an embedded `*` anywhere else (so
1977/// `*.*.example.com` and `foo.*.com` are both invalid), a bare `*`, any
1978/// empty label (leading/trailing/consecutive dots), and any non-ASCII
1979/// character ([`ConfigError::NonAsciiSniPattern`]) — on-wire SNI is always
1980/// an ASCII A-label (RFC 6066 §3 / IDNA), so a Unicode U-label would load
1981/// fine but never match at runtime, a silent routing failure. Full IDNA/
1982/// punycode transformation at config-load is intentionally out of scope
1983/// (no `idna` dependency in this crate); operators write the A-label form
1984/// directly, consistent with what the HTTP router produces via
1985/// `idna::domain_to_ascii` in `lib/src/router/mod.rs`. ASCII-lowercases
1986/// the accepted pattern for case-insensitive comparison.
1987///
1988/// Public: this is the SINGLE SNI shape validator shared by TOML config
1989/// load (this module) and the worker's request boundary
1990/// (`TcpListener::validate_new_tcp_front` in `lib/src/tcp.rs`), which an
1991/// `AddTcpFrontend` sent directly over the command socket, or a
1992/// `LoadState` replay, can reach without ever going through config-load —
1993/// both call sites must reject the identical set of malformed shapes.
1994pub fn validate_sni_pattern(sni: &str) -> Result<String, ConfigError> {
1995    let invalid = || ConfigError::InvalidSniPattern {
1996        sni: sni.to_string(),
1997    };
1998
1999    if sni.is_empty() {
2000        return Err(invalid());
2001    }
2002
2003    if !sni.is_ascii() {
2004        return Err(ConfigError::NonAsciiSniPattern {
2005            sni: sni.to_string(),
2006        });
2007    }
2008
2009    // Only a single leading "*." wildcard label is accepted; strip it (if
2010    // present) before checking the remainder is otherwise plain and
2011    // non-empty.
2012    let remainder = sni.strip_prefix("*.").unwrap_or(sni);
2013
2014    // `/` is never valid in a hostname — and the SNI route table is a
2015    // `pattern_trie::TrieNode`, whose insert treats a leftmost label wrapped
2016    // in `/.../` as a REGEX segment; letting one through would silently
2017    // widen routing beyond the documented "exact host or one leading `*.`"
2018    // contract. Checking `remainder` covers the whole pattern: the only
2019    // stripped prefix is the literal `*.`, which cannot contain `/`.
2020    if remainder.is_empty() || remainder.contains('*') || remainder.contains('/') {
2021        return Err(invalid());
2022    }
2023    if remainder.split('.').any(|label| label.is_empty()) {
2024        return Err(invalid());
2025    }
2026
2027    let normalized = sni.to_ascii_lowercase();
2028    // POST: the normalized pattern carries exactly one '*' (the leading
2029    // wildcard marker) or none at all — never more, since the checks above
2030    // reject any '*' in the remainder — and is pure ASCII, since non-ASCII
2031    // input was rejected before normalization (a non-ASCII pattern can
2032    // never match the ASCII A-label SNI on the wire).
2033    debug_assert!(
2034        normalized.matches('*').count() <= 1,
2035        "a validated SNI pattern must carry at most one wildcard marker"
2036    );
2037    debug_assert!(
2038        normalized.is_ascii(),
2039        "a validated SNI pattern must be pure ASCII"
2040    );
2041    Ok(normalized)
2042}
2043
2044/// Parse a `redirect` TOML value (case-insensitive) into the proto enum.
2045pub(crate) fn parse_redirect_policy(value: &str) -> Result<RedirectPolicy, ConfigError> {
2046    match value.to_ascii_lowercase().as_str() {
2047        "forward" => Ok(RedirectPolicy::Forward),
2048        "permanent" => Ok(RedirectPolicy::Permanent),
2049        "unauthorized" => Ok(RedirectPolicy::Unauthorized),
2050        _ => Err(ConfigError::InvalidRedirectPolicy(value.to_owned())),
2051    }
2052}
2053
2054/// Parse a `redirect_scheme` TOML value (case-insensitive) into the proto enum.
2055pub(crate) fn parse_redirect_scheme(value: &str) -> Result<RedirectScheme, ConfigError> {
2056    match value.to_ascii_lowercase().as_str() {
2057        "use-same" | "use_same" => Ok(RedirectScheme::UseSame),
2058        "use-http" | "use_http" => Ok(RedirectScheme::UseHttp),
2059        "use-https" | "use_https" => Ok(RedirectScheme::UseHttps),
2060        _ => Err(ConfigError::InvalidRedirectScheme(value.to_owned())),
2061    }
2062}
2063
2064/// Parse a `[[clusters.<id>.frontends.headers]]` entry into the proto
2065/// [`Header`] message. `index` is the zero-based position of `entry` in
2066/// the source array — surfaced into the error so a multi-entry config
2067/// pinpoints the bad row instead of just naming the unknown position.
2068/// An empty `value` is the HAProxy `del-header` parity (deletes the
2069/// header by name); the proto carries the empty string verbatim.
2070pub(crate) fn parse_header_edit(
2071    index: usize,
2072    entry: &HeaderEditConfig,
2073) -> Result<Header, ConfigError> {
2074    let position = match entry.position.to_ascii_lowercase().as_str() {
2075        "request" => HeaderPosition::Request,
2076        "response" => HeaderPosition::Response,
2077        "both" => HeaderPosition::Both,
2078        _ => {
2079            return Err(ConfigError::InvalidHeaderPosition {
2080                index,
2081                position: entry.position.clone(),
2082            });
2083        }
2084    };
2085    if !header_name_is_valid_token(entry.key.as_bytes()) {
2086        return Err(ConfigError::InvalidHeaderBytes {
2087            index,
2088            field: "key",
2089        });
2090    }
2091    if header_value_contains_forbidden_controls(entry.value.as_bytes()) {
2092        return Err(ConfigError::InvalidHeaderBytes {
2093            index,
2094            field: "value",
2095        });
2096    }
2097    let header = Header {
2098        position: position as i32,
2099        key: entry.key.clone(),
2100        val: entry.value.clone(),
2101    };
2102    // POST: a Header that escapes this function carries a key that is a valid
2103    // RFC 9110 token and a value free of the forbidden control bytes — the two
2104    // guards above are the sole gate, so an emitted Header can never inject a
2105    // CRLF or a bad token onto the H1 wire (mirrors the runtime filter in
2106    // mux/converter.rs).
2107    debug_assert!(
2108        header_name_is_valid_token(header.key.as_bytes()),
2109        "an emitted header key must be a valid token"
2110    );
2111    debug_assert!(
2112        !header_value_contains_forbidden_controls(header.val.as_bytes()),
2113        "an emitted header value must be free of forbidden control bytes"
2114    );
2115    Ok(header)
2116}
2117
2118/// Field names follow the RFC 9110 §5.1 `token` grammar: non-empty,
2119/// composed of `tchar` bytes (alphanumeric plus a closed punctuation
2120/// list). HTAB and SP are NOT tchar — they belong to field-value
2121/// grammar and must be rejected in the name. Reusing the more
2122/// permissive value-side filter would let `Host\t` slip through and
2123/// produce an invalid header line on the H1 wire (security review
2124/// LISA-002 follow-up).
2125pub(crate) fn header_name_is_valid_token(bytes: &[u8]) -> bool {
2126    if bytes.is_empty() {
2127        return false;
2128    }
2129    bytes.iter().all(|&b| is_tchar(b))
2130}
2131
2132/// `tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." /
2133/// "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA` per RFC 9110 §5.6.2.
2134fn is_tchar(b: u8) -> bool {
2135    b.is_ascii_alphanumeric()
2136        || matches!(
2137            b,
2138            b'!' | b'#'
2139                | b'$'
2140                | b'%'
2141                | b'&'
2142                | b'\''
2143                | b'*'
2144                | b'+'
2145                | b'-'
2146                | b'.'
2147                | b'^'
2148                | b'_'
2149                | b'`'
2150                | b'|'
2151                | b'~'
2152        )
2153}
2154
2155/// Reject any byte that would let a header injection escape the value
2156/// block on the wire (RFC 9110 §5.5 / RFC 9113 §8.2.1):
2157/// `\0..=\x08`, `\x0A..=\x1F`, and `\x7F` — the entire C0 control set
2158/// minus horizontal tab `\x09`, which RFC 9110 explicitly permits in
2159/// field values. Mirrors the runtime filter at
2160/// `lib/src/protocol/mux/converter.rs::call` so config-load and runtime
2161/// agree on which header values may travel.
2162pub(crate) fn header_value_contains_forbidden_controls(bytes: &[u8]) -> bool {
2163    bytes
2164        .iter()
2165        .any(|&b| matches!(b, 0x00..=0x08 | 0x0A..=0x1F | 0x7F))
2166}
2167
2168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2169#[serde(deny_unknown_fields, rename_all = "lowercase")]
2170pub enum ListenerProtocol {
2171    Http,
2172    Https,
2173    Tcp,
2174    Udp,
2175}
2176
2177#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2178#[serde(deny_unknown_fields, rename_all = "lowercase")]
2179pub enum FileClusterProtocolConfig {
2180    Http,
2181    Tcp,
2182}
2183
2184fn default_health_check_interval() -> u32 {
2185    10
2186}
2187fn default_health_check_timeout() -> u32 {
2188    5
2189}
2190fn default_health_check_threshold() -> u32 {
2191    3
2192}
2193
2194#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2195#[serde(deny_unknown_fields)]
2196pub struct FileHealthCheckConfig {
2197    pub uri: String,
2198    #[serde(default = "default_health_check_interval")]
2199    pub interval: u32,
2200    #[serde(default = "default_health_check_timeout")]
2201    pub timeout: u32,
2202    #[serde(default = "default_health_check_threshold")]
2203    pub healthy_threshold: u32,
2204    #[serde(default = "default_health_check_threshold")]
2205    pub unhealthy_threshold: u32,
2206    #[serde(default)]
2207    pub expected_status: u32,
2208}
2209
2210impl FileHealthCheckConfig {
2211    pub fn to_proto(&self) -> HealthCheckConfig {
2212        let proto = HealthCheckConfig {
2213            uri: self.uri.to_owned(),
2214            interval: self.interval,
2215            timeout: self.timeout,
2216            healthy_threshold: self.healthy_threshold,
2217            unhealthy_threshold: self.unhealthy_threshold,
2218            expected_status: self.expected_status,
2219        };
2220        // POST: the proto mirrors the file config exactly — the URI and all
2221        // timing knobs are carried through verbatim (no clamping or defaulting
2222        // happens at this layer; defaults are applied by serde at parse time).
2223        debug_assert_eq!(proto.uri, self.uri, "proto URI must mirror the file config");
2224        debug_assert!(
2225            proto.interval == self.interval
2226                && proto.timeout == self.timeout
2227                && proto.healthy_threshold == self.healthy_threshold
2228                && proto.unhealthy_threshold == self.unhealthy_threshold,
2229            "proto timing knobs must mirror the file config"
2230        );
2231        proto
2232    }
2233}
2234
2235/// Validate a [`HealthCheckConfig`] for the rules every layer relies on:
2236/// strict positive thresholds and a URI that cannot smuggle a second
2237/// HTTP message on the wire (RFC 9110 §5.1 — request-target). Used by
2238/// the CLI request builder and the worker `SetHealthCheck` handler so
2239/// off-channel inputs (TOML reload, third-party clients) are
2240/// constrained the same way as `sozu cluster health-check set`.
2241///
2242/// The function is intentionally [`Result<(), &'static str>`] rather
2243/// than carrying a structured error: the diagnostics only flow into
2244/// CLI output / worker error responses where the message is the value.
2245pub fn validate_health_check_config(cfg: &HealthCheckConfig) -> Result<(), &'static str> {
2246    if cfg.interval == 0 {
2247        return Err("health check interval must be > 0");
2248    }
2249    if cfg.timeout == 0 {
2250        return Err("health check timeout must be > 0");
2251    }
2252    if cfg.healthy_threshold == 0 {
2253        return Err("health check healthy_threshold must be > 0");
2254    }
2255    if cfg.unhealthy_threshold == 0 {
2256        return Err("health check unhealthy_threshold must be > 0");
2257    }
2258    if !cfg.uri.starts_with('/') {
2259        return Err("health check URI must start with '/'");
2260    }
2261    if cfg
2262        .uri
2263        .bytes()
2264        .any(|b| b == b'\r' || b == b'\n' || b == 0 || (b < 0x20 && b != b'\t'))
2265    {
2266        return Err("health check URI must not contain CR, LF, NUL, or other C0 control bytes");
2267    }
2268    // POST: a validated config has strictly-positive timing knobs (a zero
2269    // interval/timeout/threshold would make the health-check loop spin or
2270    // never converge) and a request-target the worker can splice into an HTTP
2271    // probe without smuggling a second message. Every Ok return is a witness
2272    // for all of these.
2273    debug_assert!(
2274        cfg.interval > 0
2275            && cfg.timeout > 0
2276            && cfg.healthy_threshold > 0
2277            && cfg.unhealthy_threshold > 0,
2278        "validated health-check thresholds must all be strictly positive"
2279    );
2280    debug_assert!(
2281        cfg.uri.starts_with('/'),
2282        "validated health-check URI must be an absolute path"
2283    );
2284    Ok(())
2285}
2286
2287#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2288#[serde(deny_unknown_fields)]
2289pub struct FileClusterConfig {
2290    pub frontends: Vec<FileClusterFrontendConfig>,
2291    pub backends: Vec<BackendConfig>,
2292    pub protocol: FileClusterProtocolConfig,
2293    pub sticky_session: Option<bool>,
2294    pub https_redirect: Option<bool>,
2295    #[serde(default)]
2296    pub send_proxy: Option<bool>,
2297    #[serde(default)]
2298    pub load_balancing: LoadBalancingAlgorithms,
2299    pub answer_503: Option<String>,
2300    #[serde(default)]
2301    pub load_metric: Option<LoadMetric>,
2302    /// Backend-capability hint: `true` when the backend speaks HTTP/2 (h2c or h2+TLS once #1218 lands).
2303    /// Does NOT gate H2 at the frontend — frontend H2 is ALPN-negotiated independently (see `alpn_protocols`).
2304    pub http2: Option<bool>,
2305    /// Per-cluster HTTP answer template overrides keyed by HTTP status
2306    /// code (e.g. `"503"`). Each value is either a filesystem path or an
2307    /// `inline:<body>` literal — see [`resolve_answer_source`]. Loaded
2308    /// into [`Cluster::answers`] at build time via [`load_answers`].
2309    ///
2310    /// Layering: an entry here overrides the listener-level
2311    /// `[listeners.<id>.answers]` default for the matching status on
2312    /// requests routed to this cluster. The listener-level map is the
2313    /// global default; the cluster-level map is the per-cluster
2314    /// override.
2315    pub answers: Option<BTreeMap<String, String>>,
2316    /// Optional explicit port to use when building the `Location` header
2317    /// for an `https_redirect`. When unset, the listener's effective HTTPS
2318    /// port is used. Lets operators front a non-standard HTTPS port (e.g.
2319    /// 8443) on the redirect target while keeping `https_redirect = true`.
2320    pub https_redirect_port: Option<u32>,
2321    /// Authorized credentials for HTTP basic authentication, formatted as
2322    /// `username:hex(sha256(password))` (lower-case hex). Empty list
2323    /// disables auth even when a frontend sets `required_auth = true` —
2324    /// such requests are rejected with 401.
2325    pub authorized_hashes: Option<Vec<String>>,
2326    /// Realm string emitted in `WWW-Authenticate: Basic realm="…"` when
2327    /// an unauthenticated request is rejected. Treated as an opaque
2328    /// value (no template substitution).
2329    pub www_authenticate: Option<String>,
2330    /// Override the global per-(cluster, source-IP) connection limit for
2331    /// this cluster. `None` (field absent) inherits the global default
2332    /// `max_connections_per_ip`. `Some(0)` is explicit "unlimited for
2333    /// this cluster". `Some(n > 0)` overrides with the cluster-specific
2334    /// limit. The source IP is taken from the parsed proxy-protocol
2335    /// header when present, else `peer_addr`.
2336    pub max_connections_per_ip: Option<u64>,
2337    /// Override the global `Retry-After` header value (seconds) emitted
2338    /// on HTTP 429 responses for this cluster. `None` inherits the global
2339    /// default. `Some(0)` omits the header. TCP clusters carry this
2340    /// field for shape uniformity but never emit the header (no HTTP
2341    /// envelope).
2342    pub retry_after: Option<u32>,
2343    /// Optional HTTP health-check configuration. The probe wire format
2344    /// follows `cluster.http2`: HTTP/1.1 when false, HTTP/2 prior-knowledge
2345    /// (h2c) when true.
2346    #[serde(default)]
2347    pub health_check: Option<FileHealthCheckConfig>,
2348    /// Optional UDP-specific cluster configuration, parsed from a
2349    /// `[clusters.<id>.udp]` block. Additive: clusters without a `udp`
2350    /// block produce `udp: None` on the resulting [`Cluster`].
2351    #[serde(default)]
2352    pub udp: Option<FileUdpClusterConfig>,
2353}
2354
2355/// UDP backend health-check configuration, parsed from
2356/// `[clusters.<id>.udp.health]`.
2357#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2358#[serde(deny_unknown_fields)]
2359pub struct FileUdpHealthConfig {
2360    /// probe mode, parsed in SCREAMING_SNAKE_CASE: `"HEALTH_OFF"`,
2361    /// `"TCP_PROBE"`, or `"UDP_PROBE"`. Defaults to `TCP_PROBE` when a
2362    /// `udp.health` block is present.
2363    pub mode: Option<UdpHealthMode>,
2364    pub tcp_port: Option<u32>,
2365    pub rise: Option<u32>,
2366    pub fall: Option<u32>,
2367    pub fail_open: Option<bool>,
2368    /// hex-free literal payload sent for a UDP probe.
2369    pub udp_probe_payload: Option<String>,
2370    pub probe_interval_seconds: Option<u32>,
2371    pub probe_timeout_seconds: Option<u32>,
2372}
2373
2374impl FileUdpHealthConfig {
2375    pub fn to_proto(&self) -> UdpHealthConfig {
2376        UdpHealthConfig {
2377            mode: self.mode.map(|m| m as i32),
2378            tcp_port: self.tcp_port,
2379            rise: self.rise,
2380            fall: self.fall,
2381            fail_open: self.fail_open,
2382            udp_probe_payload: self
2383                .udp_probe_payload
2384                .as_ref()
2385                .map(|p| p.as_bytes().to_owned()),
2386            probe_interval_seconds: self.probe_interval_seconds,
2387            probe_timeout_seconds: self.probe_timeout_seconds,
2388        }
2389    }
2390}
2391
2392/// UDP-specific cluster knobs, parsed from a `[clusters.<id>.udp]` block.
2393#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2394#[serde(deny_unknown_fields)]
2395pub struct FileUdpClusterConfig {
2396    /// flow affinity key, parsed in SCREAMING_SNAKE_CASE: `"SOURCE_IP"`
2397    /// (default) or `"SOURCE_IP_PORT"`.
2398    pub affinity_key: Option<UdpAffinityKey>,
2399    /// expected replies per flow; 0 = unlimited.
2400    pub responses: Option<u32>,
2401    /// max client datagrams per flow; 0 = unlimited.
2402    pub requests: Option<u32>,
2403    /// send a PROXY protocol v2 header to the backend.
2404    pub send_proxy_protocol: Option<bool>,
2405    /// prepend PPv2 to every datagram; false = first-datagram only.
2406    pub proxy_protocol_every_datagram: Option<bool>,
2407    /// optional backend health-check configuration.
2408    pub health: Option<FileUdpHealthConfig>,
2409}
2410
2411impl FileUdpClusterConfig {
2412    pub fn to_proto(&self) -> UdpClusterConfig {
2413        UdpClusterConfig {
2414            affinity_key: self.affinity_key.map(|k| k as i32),
2415            responses: self.responses,
2416            requests: self.requests,
2417            send_proxy_protocol: self.send_proxy_protocol,
2418            proxy_protocol_every_datagram: self.proxy_protocol_every_datagram,
2419            health: self.health.as_ref().map(|h| h.to_proto()),
2420        }
2421    }
2422}
2423
2424#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2425#[serde(deny_unknown_fields)]
2426pub struct BackendConfig {
2427    pub address: SocketAddr,
2428    pub weight: Option<u8>,
2429    pub sticky_id: Option<String>,
2430    pub backup: Option<bool>,
2431    pub backend_id: Option<String>,
2432}
2433
2434impl FileClusterConfig {
2435    pub fn to_cluster_config(
2436        self,
2437        cluster_id: &str,
2438        expect_proxy: &HashSet<SocketAddr>,
2439    ) -> Result<ClusterConfig, ConfigError> {
2440        // PRE: every frontend that converts cleanly must survive into the built
2441        // cluster — no frontend is silently dropped during conversion.
2442        let requested_frontend_count = self.frontends.len();
2443        match self.protocol {
2444            FileClusterProtocolConfig::Tcp => {
2445                let mut has_expect_proxy = None;
2446                let mut frontends = Vec::new();
2447                for f in self.frontends {
2448                    if expect_proxy.contains(&f.address) {
2449                        match has_expect_proxy {
2450                            Some(true) => {}
2451                            Some(false) => {
2452                                return Err(ConfigError::Incompatible {
2453                                    object: ObjectKind::Cluster,
2454                                    id: cluster_id.to_owned(),
2455                                    kind: IncompatibilityKind::ProxyProtocol,
2456                                });
2457                            }
2458                            None => has_expect_proxy = Some(true),
2459                        }
2460                    } else {
2461                        match has_expect_proxy {
2462                            Some(false) => {}
2463                            Some(true) => {
2464                                return Err(ConfigError::Incompatible {
2465                                    object: ObjectKind::Cluster,
2466                                    id: cluster_id.to_owned(),
2467                                    kind: IncompatibilityKind::ProxyProtocol,
2468                                });
2469                            }
2470                            None => has_expect_proxy = Some(false),
2471                        }
2472                    }
2473                    let tcp_frontend = f.to_tcp_front()?;
2474                    frontends.push(tcp_frontend);
2475                }
2476
2477                let send_proxy = self.send_proxy.unwrap_or(false);
2478                let expect_proxy = has_expect_proxy.unwrap_or(false);
2479                let proxy_protocol = match (send_proxy, expect_proxy) {
2480                    (true, true) => Some(ProxyProtocolConfig::RelayHeader),
2481                    (true, false) => Some(ProxyProtocolConfig::SendHeader),
2482                    (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
2483                    _ => None,
2484                };
2485
2486                let answers = match self.answers.as_ref() {
2487                    Some(map) => load_answers(map)?,
2488                    None => BTreeMap::new(),
2489                };
2490
2491                let udp = self.udp.as_ref().map(|u| u.to_proto());
2492                // POST: every requested frontend converted (none dropped), and
2493                // the resolved proxy-protocol mode is the documented function of
2494                // the (send, expect) pair — expect-only must never resolve to a
2495                // send-header mode and vice versa, which would corrupt the wire
2496                // framing.
2497                debug_assert_eq!(
2498                    frontends.len(),
2499                    requested_frontend_count,
2500                    "every TCP frontend must survive conversion"
2501                );
2502                debug_assert_eq!(
2503                    proxy_protocol,
2504                    match (send_proxy, expect_proxy) {
2505                        (true, true) => Some(ProxyProtocolConfig::RelayHeader),
2506                        (true, false) => Some(ProxyProtocolConfig::SendHeader),
2507                        (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
2508                        (false, false) => None,
2509                    },
2510                    "proxy_protocol must be the (send, expect) function"
2511                );
2512
2513                Ok(ClusterConfig::Tcp(TcpClusterConfig {
2514                    cluster_id: cluster_id.to_string(),
2515                    frontends,
2516                    backends: self.backends,
2517                    proxy_protocol,
2518                    load_balancing: self.load_balancing,
2519                    load_metric: self.load_metric,
2520                    answers,
2521                    https_redirect_port: self.https_redirect_port,
2522                    authorized_hashes: self.authorized_hashes.unwrap_or_default(),
2523                    www_authenticate: self.www_authenticate,
2524                    max_connections_per_ip: self.max_connections_per_ip,
2525                    retry_after: self.retry_after,
2526                    health_check: self.health_check.as_ref().map(|hc| hc.to_proto()),
2527                    udp,
2528                }))
2529            }
2530            FileClusterProtocolConfig::Http => {
2531                let mut frontends = Vec::new();
2532                for frontend in self.frontends {
2533                    let http_frontend = frontend.to_http_front(cluster_id)?;
2534                    frontends.push(http_frontend);
2535                }
2536
2537                let answer_503 = self.answer_503.as_ref().and_then(|path| {
2538                    Config::load_file(path)
2539                        .map_err(|e| {
2540                            error!("cannot load 503 error page at path '{}': {:?}", path, e);
2541                            e
2542                        })
2543                        .ok()
2544                });
2545
2546                let answers = match self.answers.as_ref() {
2547                    Some(map) => load_answers(map)?,
2548                    None => BTreeMap::new(),
2549                };
2550
2551                let udp = self.udp.as_ref().map(|u| u.to_proto());
2552                // POST: every requested HTTP frontend converted — none dropped
2553                // (a dropped frontend would silently stop routing a hostname).
2554                debug_assert_eq!(
2555                    frontends.len(),
2556                    requested_frontend_count,
2557                    "every HTTP frontend must survive conversion"
2558                );
2559
2560                Ok(ClusterConfig::Http(HttpClusterConfig {
2561                    cluster_id: cluster_id.to_string(),
2562                    frontends,
2563                    backends: self.backends,
2564                    sticky_session: self.sticky_session.unwrap_or(false),
2565                    https_redirect: self.https_redirect.unwrap_or(false),
2566                    load_balancing: self.load_balancing,
2567                    load_metric: self.load_metric,
2568                    answer_503,
2569                    http2: self.http2,
2570                    answers,
2571                    https_redirect_port: self.https_redirect_port,
2572                    authorized_hashes: self.authorized_hashes.unwrap_or_default(),
2573                    www_authenticate: self.www_authenticate,
2574                    max_connections_per_ip: self.max_connections_per_ip,
2575                    retry_after: self.retry_after,
2576                    health_check: self.health_check.as_ref().map(|hc| hc.to_proto()),
2577                    udp,
2578                }))
2579            }
2580        }
2581    }
2582}
2583
2584#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2585#[serde(deny_unknown_fields)]
2586pub struct HttpFrontendConfig {
2587    pub address: SocketAddr,
2588    pub hostname: String,
2589    pub path: PathRule,
2590    pub method: Option<String>,
2591    pub certificate: Option<String>,
2592    pub key: Option<String>,
2593    pub certificate_chain: Option<Vec<String>>,
2594    #[serde(default)]
2595    pub tls_versions: Vec<TlsVersion>,
2596    #[serde(default)]
2597    pub position: RulePosition,
2598    pub tags: Option<BTreeMap<String, String>>,
2599    /// Resolved redirect policy. `None` keeps the proto-default `FORWARD`.
2600    #[serde(default)]
2601    pub redirect: Option<RedirectPolicy>,
2602    /// Resolved redirect scheme. `None` keeps the proto-default `USE_SAME`.
2603    #[serde(default)]
2604    pub redirect_scheme: Option<RedirectScheme>,
2605    #[serde(default)]
2606    pub redirect_template: Option<String>,
2607    #[serde(default)]
2608    pub rewrite_host: Option<String>,
2609    #[serde(default)]
2610    pub rewrite_path: Option<String>,
2611    #[serde(default)]
2612    pub rewrite_port: Option<u32>,
2613    #[serde(default)]
2614    pub required_auth: Option<bool>,
2615    /// Header mutations applied to requests and/or responses passing through
2616    /// this frontend. Empty by default.
2617    #[serde(default)]
2618    pub headers: Vec<Header>,
2619    /// Resolved per-frontend HSTS (RFC 6797) policy. `None` means inherit
2620    /// the listener default at frontend-add time in the worker.
2621    #[serde(default)]
2622    pub hsts: Option<HstsConfig>,
2623}
2624
2625impl fmt::Debug for HttpFrontendConfig {
2626    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2627        let certificate = self.certificate.as_ref().map(|_| "[redacted]");
2628        let certificate_len = self.certificate.as_ref().map(String::len);
2629        let key = self.key.as_ref().map(|_| "[redacted]");
2630        let key_len = self.key.as_ref().map(String::len);
2631        let certificate_chain = self.certificate_chain.as_ref().map(|_| "[redacted]");
2632        let certificate_chain_count = self.certificate_chain.as_ref().map(Vec::len);
2633        let certificate_chain_len = self.certificate_chain.as_ref().map(|chain| {
2634            chain
2635                .iter()
2636                .map(String::len)
2637                .fold(0usize, usize::saturating_add)
2638        });
2639        let method_len = self.method.as_ref().map(String::len);
2640        let redirect_template_len = self.redirect_template.as_ref().map(String::len);
2641        let rewrite_host_len = self.rewrite_host.as_ref().map(String::len);
2642        let rewrite_path_len = self.rewrite_path.as_ref().map(String::len);
2643
2644        f.debug_struct("HttpFrontendConfig")
2645            .field("address", &self.address)
2646            .field("hostname_len", &self.hostname.len())
2647            .field("path_kind", &self.path.kind)
2648            .field("path_len", &self.path.value.len())
2649            .field("method_len", &method_len)
2650            .field("certificate", &certificate)
2651            .field("certificate_len", &certificate_len)
2652            .field("key", &key)
2653            .field("key_len", &key_len)
2654            .field("certificate_chain", &certificate_chain)
2655            .field("certificate_chain_count", &certificate_chain_count)
2656            .field("certificate_chain_len", &certificate_chain_len)
2657            .field("tls_versions_count", &self.tls_versions.len())
2658            .field("position", &self.position)
2659            .field(
2660                "tags_count",
2661                &self.tags.as_ref().map(BTreeMap::len).unwrap_or_default(),
2662            )
2663            .field("redirect", &self.redirect)
2664            .field("redirect_scheme", &self.redirect_scheme)
2665            .field("redirect_template_len", &redirect_template_len)
2666            .field("rewrite_host_len", &rewrite_host_len)
2667            .field("rewrite_path_len", &rewrite_path_len)
2668            .field("rewrite_port", &self.rewrite_port)
2669            .field("required_auth", &self.required_auth)
2670            .field("headers_count", &self.headers.len())
2671            .field("hsts", &self.hsts)
2672            .finish()
2673    }
2674}
2675
2676impl HttpFrontendConfig {
2677    pub fn generate_requests(&self, cluster_id: &str) -> Vec<Request> {
2678        let mut v = Vec::new();
2679
2680        let tags = self.tags.clone().unwrap_or_default();
2681
2682        if self.key.is_some() && self.certificate.is_some() {
2683            v.push(
2684                RequestType::AddCertificate(AddCertificate {
2685                    address: self.address.into(),
2686                    certificate: CertificateAndKey {
2687                        key: self.key.clone().unwrap(),
2688                        certificate: self.certificate.clone().unwrap(),
2689                        certificate_chain: self.certificate_chain.clone().unwrap_or_default(),
2690                        versions: self.tls_versions.iter().map(|v| *v as i32).collect(),
2691                        // This field is used to override the certificate subject and san, we should not set it when
2692                        // loading the configuration, as we may provide a wildcard certificate for a specific domain.
2693                        // As a result, we will reject legit traffic for others domains as the certificate resolver will
2694                        // not load twice the same certificate and then do not register the certificate for others domains.
2695                        names: vec![],
2696                    },
2697                    expired_at: None,
2698                })
2699                .into(),
2700            );
2701
2702            v.push(
2703                RequestType::AddHttpsFrontend(RequestHttpFrontend {
2704                    cluster_id: Some(cluster_id.to_string()),
2705                    address: self.address.into(),
2706                    hostname: self.hostname.clone(),
2707                    path: self.path.clone(),
2708                    method: self.method.clone(),
2709                    position: self.position.into(),
2710                    tags,
2711                    redirect: self.redirect.map(|r| r as i32),
2712                    required_auth: self.required_auth,
2713                    redirect_scheme: self.redirect_scheme.map(|s| s as i32),
2714                    redirect_template: self.redirect_template.clone(),
2715                    rewrite_host: self.rewrite_host.clone(),
2716                    rewrite_path: self.rewrite_path.clone(),
2717                    rewrite_port: self.rewrite_port,
2718                    headers: self.headers.clone(),
2719                    hsts: self.hsts,
2720                })
2721                .into(),
2722            );
2723        } else {
2724            //create the front both for HTTP and HTTPS if possible
2725            v.push(
2726                RequestType::AddHttpFrontend(RequestHttpFrontend {
2727                    cluster_id: Some(cluster_id.to_string()),
2728                    address: self.address.into(),
2729                    hostname: self.hostname.clone(),
2730                    path: self.path.clone(),
2731                    method: self.method.clone(),
2732                    position: self.position.into(),
2733                    tags,
2734                    redirect: self.redirect.map(|r| r as i32),
2735                    required_auth: self.required_auth,
2736                    redirect_scheme: self.redirect_scheme.map(|s| s as i32),
2737                    redirect_template: self.redirect_template.clone(),
2738                    rewrite_host: self.rewrite_host.clone(),
2739                    rewrite_path: self.rewrite_path.clone(),
2740                    rewrite_port: self.rewrite_port,
2741                    headers: self.headers.clone(),
2742                    hsts: self.hsts,
2743                })
2744                .into(),
2745            );
2746        }
2747
2748        v
2749    }
2750}
2751
2752#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2753#[serde(deny_unknown_fields)]
2754pub struct HttpClusterConfig {
2755    pub cluster_id: String,
2756    pub frontends: Vec<HttpFrontendConfig>,
2757    pub backends: Vec<BackendConfig>,
2758    pub sticky_session: bool,
2759    pub https_redirect: bool,
2760    pub load_balancing: LoadBalancingAlgorithms,
2761    pub load_metric: Option<LoadMetric>,
2762    pub answer_503: Option<String>,
2763    pub http2: Option<bool>,
2764    /// Per-status template body map (already loaded from disk). Maps to
2765    /// the proto [`Cluster::answers`] field.
2766    #[serde(default)]
2767    pub answers: BTreeMap<String, String>,
2768    #[serde(default)]
2769    pub https_redirect_port: Option<u32>,
2770    #[serde(default)]
2771    pub authorized_hashes: Vec<String>,
2772    #[serde(default)]
2773    pub www_authenticate: Option<String>,
2774    /// Per-cluster override of the global `max_connections_per_ip`. See
2775    /// [`FileClusterConfig::max_connections_per_ip`] for semantics.
2776    #[serde(default)]
2777    pub max_connections_per_ip: Option<u64>,
2778    /// Per-cluster override of the global `retry_after` HTTP-429 header
2779    /// value (seconds). See [`FileClusterConfig::retry_after`].
2780    #[serde(default)]
2781    pub retry_after: Option<u32>,
2782    /// Optional HTTP health-check configuration. The probe wire format
2783    /// follows `cluster.http2`: HTTP/1.1 when false, HTTP/2 prior-knowledge
2784    /// (h2c) when true.
2785    #[serde(default)]
2786    pub health_check: Option<HealthCheckConfig>,
2787    /// Optional UDP-specific cluster configuration. Always `None` for HTTP
2788    /// clusters; carried for shape uniformity with the proto [`Cluster`].
2789    #[serde(default)]
2790    pub udp: Option<UdpClusterConfig>,
2791}
2792
2793impl HttpClusterConfig {
2794    pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2795        let mut v: Vec<Request> = vec![
2796            RequestType::AddCluster(Cluster {
2797                cluster_id: self.cluster_id.clone(),
2798                sticky_session: self.sticky_session,
2799                https_redirect: self.https_redirect,
2800                proxy_protocol: None,
2801                load_balancing: self.load_balancing as i32,
2802                answer_503: self.answer_503.clone(),
2803                load_metric: self.load_metric.map(|s| s as i32),
2804                http2: self.http2,
2805                answers: self.answers.clone(),
2806                https_redirect_port: self.https_redirect_port,
2807                authorized_hashes: self.authorized_hashes.clone(),
2808                www_authenticate: self.www_authenticate.clone(),
2809                max_connections_per_ip: self.max_connections_per_ip,
2810                retry_after: self.retry_after,
2811                health_check: self.health_check.clone(),
2812                udp: self.udp.clone(),
2813            })
2814            .into(),
2815        ];
2816
2817        for frontend in &self.frontends {
2818            let mut orders = frontend.generate_requests(&self.cluster_id);
2819            v.append(&mut orders);
2820        }
2821
2822        for (backend_count, backend) in self.backends.iter().enumerate() {
2823            let load_balancing_parameters = Some(LoadBalancingParams {
2824                weight: backend.weight.unwrap_or(100) as i32,
2825            });
2826
2827            v.push(
2828                RequestType::AddBackend(AddBackend {
2829                    cluster_id: self.cluster_id.clone(),
2830                    backend_id: backend.backend_id.clone().unwrap_or_else(|| {
2831                        format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
2832                    }),
2833                    address: backend.address.into(),
2834                    load_balancing_parameters,
2835                    sticky_id: backend.sticky_id.clone(),
2836                    backup: backend.backup,
2837                })
2838                .into(),
2839            );
2840        }
2841
2842        // POST: the order stream begins with exactly one AddCluster and emits
2843        // exactly one AddBackend per configured backend — a missing AddCluster
2844        // would orphan every backend, and a miscounted backend set would
2845        // silently drop or duplicate a backend registration.
2846        debug_assert!(
2847            matches!(
2848                v.first().and_then(|r| r.request_type.as_ref()),
2849                Some(RequestType::AddCluster(_))
2850            ),
2851            "HTTP cluster orders must lead with an AddCluster"
2852        );
2853        debug_assert_eq!(
2854            v.iter()
2855                .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
2856                .count(),
2857            self.backends.len(),
2858            "one AddBackend order per configured backend"
2859        );
2860        Ok(v)
2861    }
2862}
2863
2864#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2865pub struct TcpFrontendConfig {
2866    pub address: SocketAddr,
2867    pub tags: Option<BTreeMap<String, String>>,
2868    /// `true` when this frontend's address resolves to a `protocol = "udp"`
2869    /// listener. Resolved at config-load in [`ConfigBuilder::populate_clusters`]
2870    /// from `known_addresses`; selects `AddUdpFrontend` over `AddTcpFrontend`
2871    /// in [`TcpClusterConfig::generate_requests`]. A UDP cluster is declared as
2872    /// a `protocol = "tcp"` cluster whose frontends point at UDP listeners and
2873    /// whose datagram knobs live under `[clusters.<id>.udp]`.
2874    #[serde(default)]
2875    pub udp: bool,
2876    /// SNI hostname this frontend matches, validated and normalized by
2877    /// [`FileClusterFrontendConfig::to_tcp_front`] (sozu-proxy/sozu#1279).
2878    /// `None` matches regardless of SNI (a raw-TCP fallback).
2879    #[serde(default)]
2880    pub sni: Option<String>,
2881    /// ALPN protocol names this frontend matches; empty is the catch-all
2882    /// for its `sni` on this listener.
2883    #[serde(default)]
2884    pub alpn: Vec<String>,
2885}
2886
2887#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2888pub struct TcpClusterConfig {
2889    pub cluster_id: String,
2890    pub frontends: Vec<TcpFrontendConfig>,
2891    pub backends: Vec<BackendConfig>,
2892    #[serde(default)]
2893    pub proxy_protocol: Option<ProxyProtocolConfig>,
2894    pub load_balancing: LoadBalancingAlgorithms,
2895    pub load_metric: Option<LoadMetric>,
2896    /// Per-status template body map (already loaded from disk). Even
2897    /// though TCP clusters do not emit HTTP responses, the field is
2898    /// carried for shape uniformity with [`HttpClusterConfig`].
2899    #[serde(default)]
2900    pub answers: BTreeMap<String, String>,
2901    #[serde(default)]
2902    pub https_redirect_port: Option<u32>,
2903    #[serde(default)]
2904    pub authorized_hashes: Vec<String>,
2905    #[serde(default)]
2906    pub www_authenticate: Option<String>,
2907    /// Per-cluster override of the global `max_connections_per_ip`. See
2908    /// [`FileClusterConfig::max_connections_per_ip`] for semantics.
2909    #[serde(default)]
2910    pub max_connections_per_ip: Option<u64>,
2911    /// Per-cluster override of the global `retry_after`. TCP listeners
2912    /// never emit `Retry-After`; the field is carried for shape
2913    /// uniformity with [`HttpClusterConfig`].
2914    #[serde(default)]
2915    pub retry_after: Option<u32>,
2916    /// Optional HTTP health-check configuration. TCP clusters carry this
2917    /// field for shape uniformity with [`HttpClusterConfig`]; probes are
2918    /// HTTP/1.1 only and TCP-only backends should leave this absent.
2919    #[serde(default)]
2920    pub health_check: Option<HealthCheckConfig>,
2921    /// Optional UDP-specific cluster configuration, parsed from a
2922    /// `[clusters.<id>.udp]` block on this cluster.
2923    #[serde(default)]
2924    pub udp: Option<UdpClusterConfig>,
2925}
2926
2927impl TcpClusterConfig {
2928    pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2929        let mut v: Vec<Request> = vec![
2930            RequestType::AddCluster(Cluster {
2931                cluster_id: self.cluster_id.clone(),
2932                sticky_session: false,
2933                https_redirect: false,
2934                proxy_protocol: self.proxy_protocol.map(|s| s as i32),
2935                load_balancing: self.load_balancing as i32,
2936                load_metric: self.load_metric.map(|s| s as i32),
2937                answer_503: None,
2938                http2: None,
2939                answers: self.answers.clone(),
2940                https_redirect_port: self.https_redirect_port,
2941                authorized_hashes: self.authorized_hashes.clone(),
2942                www_authenticate: self.www_authenticate.clone(),
2943                max_connections_per_ip: self.max_connections_per_ip,
2944                retry_after: self.retry_after,
2945                health_check: self.health_check.clone(),
2946                udp: self.udp.clone(),
2947            })
2948            .into(),
2949        ];
2950
2951        for frontend in &self.frontends {
2952            // A frontend whose address resolves to a `protocol = "udp"`
2953            // listener (flagged in `populate_clusters`) is added as a UDP
2954            // frontend; all others stay TCP. Mixed TCP/UDP frontends on the
2955            // same cluster are supported.
2956            if frontend.udp {
2957                v.push(
2958                    RequestType::AddUdpFrontend(RequestUdpFrontend {
2959                        cluster_id: self.cluster_id.clone(),
2960                        address: frontend.address.into(),
2961                        tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
2962                    })
2963                    .into(),
2964                );
2965            } else {
2966                v.push(
2967                    RequestType::AddTcpFrontend(RequestTcpFrontend {
2968                        cluster_id: self.cluster_id.clone(),
2969                        address: frontend.address.into(),
2970                        tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
2971                        sni: frontend.sni.clone(),
2972                        alpn: frontend.alpn.clone(),
2973                    })
2974                    .into(),
2975                );
2976            }
2977        }
2978
2979        for (backend_count, backend) in self.backends.iter().enumerate() {
2980            let load_balancing_parameters = Some(LoadBalancingParams {
2981                weight: backend.weight.unwrap_or(100) as i32,
2982            });
2983
2984            v.push(
2985                RequestType::AddBackend(AddBackend {
2986                    cluster_id: self.cluster_id.clone(),
2987                    backend_id: backend.backend_id.clone().unwrap_or_else(|| {
2988                        format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
2989                    }),
2990                    address: backend.address.into(),
2991                    load_balancing_parameters,
2992                    sticky_id: backend.sticky_id.clone(),
2993                    backup: backend.backup,
2994                })
2995                .into(),
2996            );
2997        }
2998
2999        // POST: the order stream leads with one AddCluster and emits exactly
3000        // one AddTcpFrontend per frontend and one AddBackend per backend — the
3001        // worker reconstructs the cluster topology solely from these counts.
3002        debug_assert!(
3003            matches!(
3004                v.first().and_then(|r| r.request_type.as_ref()),
3005                Some(RequestType::AddCluster(_))
3006            ),
3007            "TCP cluster orders must lead with an AddCluster"
3008        );
3009        debug_assert_eq!(
3010            v.iter()
3011                .filter(|r| matches!(
3012                    r.request_type,
3013                    Some(RequestType::AddTcpFrontend(_)) | Some(RequestType::AddUdpFrontend(_))
3014                ))
3015                .count(),
3016            self.frontends.len(),
3017            "one AddTcpFrontend or AddUdpFrontend order per configured frontend"
3018        );
3019        debug_assert_eq!(
3020            v.iter()
3021                .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
3022                .count(),
3023            self.backends.len(),
3024            "one AddBackend order per configured backend"
3025        );
3026        Ok(v)
3027    }
3028}
3029
3030#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3031pub enum ClusterConfig {
3032    Http(HttpClusterConfig),
3033    Tcp(TcpClusterConfig),
3034}
3035
3036impl ClusterConfig {
3037    pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
3038        match *self {
3039            ClusterConfig::Http(ref http) => http.generate_requests(),
3040            ClusterConfig::Tcp(ref tcp) => tcp.generate_requests(),
3041        }
3042    }
3043}
3044
3045/// Parsed from the TOML config provided by the user.
3046#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
3047pub struct FileConfig {
3048    pub command_socket: Option<String>,
3049    pub command_buffer_size: Option<u64>,
3050    pub max_command_buffer_size: Option<u64>,
3051    pub max_connections: Option<usize>,
3052    pub min_buffers: Option<u64>,
3053    pub max_buffers: Option<u64>,
3054    pub buffer_size: Option<u64>,
3055    /// Slab-entries-per-connection multiplier. `None` keeps the compile-time
3056    /// default of 4. Operator-visible escape hatch for fan-out topologies
3057    /// that exceed 4 backends per session — clamped to [2, 32] at load.
3058    #[serde(default)]
3059    pub slab_entries_per_connection: Option<u64>,
3060    /// Maximum length, in bytes, of a base64-decoded `Authorization: Basic`
3061    /// payload accepted by the worker's `mux::auth` module. Caps the
3062    /// per-failed-auth allocation so a hostile peer cannot force the worker
3063    /// to decode arbitrarily large tokens. RFC 7617 imposes no upper bound
3064    /// — defaults to 4096, which is well above the realistic
3065    /// `username:password` shape. Operators running hardened tenants can
3066    /// lower this to e.g. 256 or 512 to bound the allocation tighter.
3067    /// Values >= `buffer_size / 3` emit a warning at config-load time
3068    /// (the credential cap shouldn't dominate the per-frontend buffer).
3069    #[serde(default)]
3070    pub basic_auth_max_credential_bytes: Option<u64>,
3071    /// Default per-(cluster, source-IP) connection limit. `None` keeps
3072    /// `0` (unlimited). Each cluster may override via its own
3073    /// `max_connections_per_ip`. The source IP is taken from the parsed
3074    /// proxy-protocol header when present, else `peer_addr`. When the
3075    /// limit is reached, HTTP requests are answered with `429 Too Many
3076    /// Requests` (with optional `Retry-After`) and TCP sessions are
3077    /// closed gracefully without dialing the backend.
3078    #[serde(default)]
3079    pub max_connections_per_ip: Option<u64>,
3080    /// Default `Retry-After` header value (seconds) sent on HTTP 429
3081    /// responses. `Some(0)` or `None` keeping the default `0` omits the
3082    /// header (rendering `Retry-After: 0` invites an immediate retry that
3083    /// defeats the limit). Per-cluster overrides apply for HTTP listeners
3084    /// only. TCP listeners ignore this value (no HTTP envelope).
3085    #[serde(default)]
3086    pub retry_after: Option<u32>,
3087    /// Requested kernel-pipe capacity, in bytes, for each `splice(2)`
3088    /// zero-copy direction (Linux only, `splice` feature). `None` keeps
3089    /// the kernel default (64 KiB). Applied via `fcntl(F_SETPIPE_SZ)`;
3090    /// the kernel rounds up to a page boundary and clamps at
3091    /// `/proc/sys/fs/pipe-max-size` (default 1 MiB unprivileged). The
3092    /// realised capacity is read back via `fcntl(F_GETPIPE_SZ)` and
3093    /// drives the per-call `len` for `splice_in`. Ignored on non-Linux
3094    /// targets and on builds without the `splice` feature.
3095    #[serde(default)]
3096    pub splice_pipe_capacity_bytes: Option<u64>,
3097    /// Optional UID allowlist for command-socket requests. `None` (default)
3098    /// preserves historical behaviour: any same-UID local process can
3099    /// invoke any verb. When set, requests whose `SO_PEERCRED` UID is not
3100    /// in the list are rejected. Use to restrict mutating verbs to a
3101    /// specific operator UID even when other same-UID daemons coexist
3102    /// (CI runners, monitoring).
3103    #[serde(default)]
3104    pub command_allowed_uids: Option<Vec<u32>>,
3105    pub saved_state: Option<String>,
3106    #[serde(default)]
3107    pub automatic_state_save: Option<bool>,
3108    pub log_level: Option<String>,
3109    pub log_target: Option<String>,
3110    #[serde(default)]
3111    pub log_colored: bool,
3112    /// Dedicated file path for the control-plane audit log. When set, every
3113    /// emitted `[AUDIT]` / `Command(...)` line is also appended to this file
3114    /// opened `O_APPEND | O_CREAT` with mode `0o640` (owner read+write,
3115    /// group read, world nothing) so operators can separate the audit trail
3116    /// from the main log stream and protect it with group-scoped ACLs /
3117    /// logrotate. Independent of the standard `log_target`. `None` keeps
3118    /// audit lines routed only through the standard logger.
3119    #[serde(default)]
3120    pub audit_logs_target: Option<String>,
3121    /// Dedicated file path for a JSON-encoded mirror of the audit log.
3122    /// One JSON object per line so SIEM pipelines (Wazuh, Elastic, Loki)
3123    /// ingest without bespoke parsers. Same `O_APPEND | O_CREAT | 0o640`
3124    /// as `audit_logs_target`. `None` disables the JSON mirror.
3125    #[serde(default)]
3126    pub audit_logs_json_target: Option<String>,
3127    #[serde(default)]
3128    pub access_logs_target: Option<String>,
3129    #[serde(default)]
3130    pub access_logs_format: Option<AccessLogFormat>,
3131    #[serde(default)]
3132    pub access_logs_colored: Option<bool>,
3133    pub worker_count: Option<u16>,
3134    pub worker_automatic_restart: Option<bool>,
3135    pub metrics: Option<MetricsConfig>,
3136    pub disable_cluster_metrics: Option<bool>,
3137    pub listeners: Option<Vec<ListenerBuilder>>,
3138    pub clusters: Option<HashMap<String, FileClusterConfig>>,
3139    pub handle_process_affinity: Option<bool>,
3140    pub ctl_command_timeout: Option<u64>,
3141    pub pid_file_path: Option<String>,
3142    pub activate_listeners: Option<bool>,
3143    #[serde(default)]
3144    pub front_timeout: Option<u32>,
3145    #[serde(default)]
3146    pub back_timeout: Option<u32>,
3147    #[serde(default)]
3148    pub connect_timeout: Option<u32>,
3149    #[serde(default)]
3150    pub zombie_check_interval: Option<u32>,
3151    #[serde(default)]
3152    pub accept_queue_timeout: Option<u32>,
3153    #[serde(default)]
3154    pub evict_on_queue_full: Option<bool>,
3155    #[serde(default)]
3156    pub request_timeout: Option<u32>,
3157    #[serde(default)]
3158    pub worker_timeout: Option<u32>,
3159}
3160
3161impl FileConfig {
3162    pub fn load_from_path(path: &str) -> Result<FileConfig, ConfigError> {
3163        let data = Config::load_file(path)?;
3164
3165        let config: FileConfig = match toml::from_str(&data) {
3166            Ok(config) => config,
3167            Err(e) => {
3168                display_toml_error(&data, &e);
3169                return Err(ConfigError::DeserializeToml(e.to_string()));
3170            }
3171        };
3172
3173        let mut reserved_address: HashSet<SocketAddr> = HashSet::new();
3174
3175        if let Some(listeners) = config.listeners.as_ref() {
3176            for listener in listeners.iter() {
3177                if reserved_address.contains(&listener.address) {
3178                    return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
3179                }
3180                reserved_address.insert(listener.address);
3181            }
3182        }
3183
3184        //FIXME: verify how clusters and listeners share addresses
3185        /*
3186        if let Some(ref clusters) = config.clusters {
3187          for (key, cluster) in clusters.iter() {
3188            if let (Some(address), Some(port)) = (cluster.ip_address.clone(), cluster.port) {
3189              let addr = (address, port);
3190              if reserved_address.contains(&addr) {
3191                println!("TCP cluster '{}' listening address ( {}:{} ) is already used in the configuration",
3192                  key, addr.0, addr.1);
3193                return Err(Error::new(
3194                  ErrorKind::InvalidData,
3195                  format!("TCP cluster '{}' listening address ( {}:{} ) is already used in the configuration",
3196                    key, addr.0, addr.1)));
3197              } else {
3198                reserved_address.insert(addr.clone());
3199              }
3200            }
3201          }
3202        }
3203        */
3204
3205        Ok(config)
3206    }
3207}
3208
3209/// A builder that converts [FileConfig] to [Config]
3210pub struct ConfigBuilder {
3211    file: FileConfig,
3212    known_addresses: HashMap<SocketAddr, ListenerProtocol>,
3213    expect_proxy_addresses: HashSet<SocketAddr>,
3214    built: Config,
3215}
3216
3217impl ConfigBuilder {
3218    /// starts building a [Config] with values from a [FileConfig], or defaults.
3219    ///
3220    /// please provide a config path, usefull for rebuilding the config later.
3221    pub fn new<S>(file_config: FileConfig, config_path: S) -> Self
3222    where
3223        S: ToString,
3224    {
3225        let built = Config {
3226            accept_queue_timeout: file_config
3227                .accept_queue_timeout
3228                .unwrap_or(DEFAULT_ACCEPT_QUEUE_TIMEOUT),
3229            evict_on_queue_full: file_config
3230                .evict_on_queue_full
3231                .unwrap_or(DEFAULT_EVICT_ON_QUEUE_FULL),
3232            activate_listeners: file_config.activate_listeners.unwrap_or(true),
3233            automatic_state_save: file_config
3234                .automatic_state_save
3235                .unwrap_or(DEFAULT_AUTOMATIC_STATE_SAVE),
3236            back_timeout: file_config.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
3237            buffer_size: file_config.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE),
3238            command_buffer_size: file_config
3239                .command_buffer_size
3240                .unwrap_or(DEFAULT_COMMAND_BUFFER_SIZE),
3241            config_path: config_path.to_string(),
3242            connect_timeout: file_config
3243                .connect_timeout
3244                .unwrap_or(DEFAULT_CONNECT_TIMEOUT),
3245            ctl_command_timeout: file_config.ctl_command_timeout.unwrap_or(1_000),
3246            front_timeout: file_config.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
3247            handle_process_affinity: file_config.handle_process_affinity.unwrap_or(false),
3248            access_logs_target: file_config.access_logs_target.clone(),
3249            audit_logs_target: file_config.audit_logs_target.clone(),
3250            audit_logs_json_target: file_config.audit_logs_json_target.clone(),
3251            access_logs_format: file_config.access_logs_format.clone(),
3252            access_logs_colored: file_config.access_logs_colored,
3253            log_level: file_config
3254                .log_level
3255                .clone()
3256                .unwrap_or_else(|| String::from("info")),
3257            log_target: file_config
3258                .log_target
3259                .clone()
3260                .unwrap_or_else(|| String::from("stdout")),
3261            log_colored: file_config.log_colored,
3262            max_buffers: file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
3263            max_command_buffer_size: file_config
3264                .max_command_buffer_size
3265                .unwrap_or(DEFAULT_MAX_COMMAND_BUFFER_SIZE),
3266            max_connections: file_config
3267                .max_connections
3268                .unwrap_or(DEFAULT_MAX_CONNECTIONS),
3269            metrics: file_config.metrics.clone(),
3270            disable_cluster_metrics: file_config
3271                .disable_cluster_metrics
3272                .unwrap_or(DEFAULT_DISABLE_CLUSTER_METRICS),
3273            min_buffers: std::cmp::min(
3274                file_config.min_buffers.unwrap_or(DEFAULT_MIN_BUFFERS),
3275                file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
3276            ),
3277            pid_file_path: file_config.pid_file_path.clone(),
3278            request_timeout: file_config
3279                .request_timeout
3280                .unwrap_or(DEFAULT_REQUEST_TIMEOUT),
3281            saved_state: file_config.saved_state.clone(),
3282            worker_automatic_restart: file_config
3283                .worker_automatic_restart
3284                .unwrap_or(DEFAULT_WORKER_AUTOMATIC_RESTART),
3285            worker_count: file_config.worker_count.unwrap_or(DEFAULT_WORKER_COUNT),
3286            zombie_check_interval: file_config
3287                .zombie_check_interval
3288                .unwrap_or(DEFAULT_ZOMBIE_CHECK_INTERVAL),
3289            worker_timeout: file_config.worker_timeout.unwrap_or(DEFAULT_WORKER_TIMEOUT),
3290            slab_entries_per_connection: file_config.slab_entries_per_connection.map(|n| {
3291                n.clamp(
3292                    ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION,
3293                    ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION,
3294                )
3295            }),
3296            command_allowed_uids: file_config.command_allowed_uids.clone(),
3297            basic_auth_max_credential_bytes: file_config.basic_auth_max_credential_bytes,
3298            max_connections_per_ip: file_config
3299                .max_connections_per_ip
3300                .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_IP),
3301            retry_after: file_config.retry_after.unwrap_or(DEFAULT_RETRY_AFTER),
3302            splice_pipe_capacity_bytes: file_config.splice_pipe_capacity_bytes,
3303            ..Default::default()
3304        };
3305
3306        // POST: the buffer free-list floor is clamped to never exceed the
3307        // ceiling — the `std::cmp::min(min_buffers, max_buffers)` above is the
3308        // sole guarantor of this, so assert it held.
3309        debug_assert!(
3310            built.min_buffers <= built.max_buffers,
3311            "min_buffers must be clamped to <= max_buffers in the builder"
3312        );
3313        // POST: an explicit slab override, if present, was clamped into the
3314        // documented [MIN, MAX] window; an absent override stays None.
3315        debug_assert!(
3316            built.slab_entries_per_connection.is_none_or(|n| {
3317                (ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION
3318                    ..=ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION)
3319                    .contains(&n)
3320            }),
3321            "a set slab_entries_per_connection must be clamped into [MIN, MAX]"
3322        );
3323
3324        Self {
3325            file: file_config,
3326            known_addresses: HashMap::new(),
3327            expect_proxy_addresses: HashSet::new(),
3328            built,
3329        }
3330    }
3331
3332    fn push_tls_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3333        let listener = listener.to_tls(Some(&self.built))?;
3334        self.built.https_listeners.push(listener);
3335        Ok(())
3336    }
3337
3338    fn push_http_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3339        let listener = listener.to_http(Some(&self.built))?;
3340        self.built.http_listeners.push(listener);
3341        Ok(())
3342    }
3343
3344    fn push_tcp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3345        let listener = listener.to_tcp(Some(&self.built))?;
3346        self.built.tcp_listeners.push(listener);
3347        Ok(())
3348    }
3349
3350    fn push_udp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3351        let listener = listener.to_udp(Some(&self.built))?;
3352        self.built.udp_listeners.push(listener);
3353        Ok(())
3354    }
3355
3356    fn populate_listeners(&mut self, listeners: Vec<ListenerBuilder>) -> Result<(), ConfigError> {
3357        for listener in listeners.iter() {
3358            if self.known_addresses.contains_key(&listener.address) {
3359                return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
3360            }
3361
3362            let protocol = listener
3363                .protocol
3364                .ok_or(ConfigError::Missing(MissingKind::Protocol))?;
3365
3366            self.known_addresses.insert(listener.address, protocol);
3367            if listener.expect_proxy == Some(true) {
3368                self.expect_proxy_addresses.insert(listener.address);
3369            }
3370
3371            if listener.public_address.is_some() && listener.expect_proxy == Some(true) {
3372                return Err(ConfigError::Incompatible {
3373                    object: ObjectKind::Listener,
3374                    id: listener.address.to_string(),
3375                    kind: IncompatibilityKind::PublicAddress,
3376                });
3377            }
3378
3379            match protocol {
3380                ListenerProtocol::Https => self.push_tls_listener(listener.clone())?,
3381                ListenerProtocol::Http => self.push_http_listener(listener.clone())?,
3382                ListenerProtocol::Tcp => self.push_tcp_listener(listener.clone())?,
3383                ListenerProtocol::Udp => self.push_udp_listener(listener.clone())?,
3384            }
3385        }
3386        Ok(())
3387    }
3388
3389    fn populate_clusters(
3390        &mut self,
3391        mut file_cluster_configs: HashMap<String, FileClusterConfig>,
3392    ) -> Result<(), ConfigError> {
3393        for (id, file_cluster_config) in file_cluster_configs.drain() {
3394            let mut cluster_config =
3395                file_cluster_config.to_cluster_config(id.as_str(), &self.expect_proxy_addresses)?;
3396
3397            match cluster_config {
3398                ClusterConfig::Http(ref mut http) => {
3399                    for frontend in http.frontends.iter_mut() {
3400                        match self.known_addresses.get(&frontend.address) {
3401                            Some(ListenerProtocol::Tcp) => {
3402                                return Err(ConfigError::WrongFrontendProtocol(
3403                                    ListenerProtocol::Tcp,
3404                                ));
3405                            }
3406                            Some(ListenerProtocol::Udp) => {
3407                                return Err(ConfigError::WrongFrontendProtocol(
3408                                    ListenerProtocol::Udp,
3409                                ));
3410                            }
3411                            Some(ListenerProtocol::Http) => {
3412                                if frontend.certificate.is_some() {
3413                                    return Err(ConfigError::WrongFrontendProtocol(
3414                                        ListenerProtocol::Http,
3415                                    ));
3416                                }
3417                            }
3418                            Some(ListenerProtocol::Https) => {
3419                                if frontend.certificate.is_none() {
3420                                    if let Some(https_listener) =
3421                                        self.built.https_listeners.iter().find(|listener| {
3422                                            listener.address == frontend.address.into()
3423                                                && listener.certificate.is_some()
3424                                        })
3425                                    {
3426                                        //println!("using listener certificate for {:}", frontend.address);
3427                                        frontend
3428                                            .certificate
3429                                            .clone_from(&https_listener.certificate);
3430                                        frontend.certificate_chain =
3431                                            Some(https_listener.certificate_chain.clone());
3432                                        frontend.key.clone_from(&https_listener.key);
3433                                    }
3434                                    if frontend.certificate.is_none() {
3435                                        debug!("known addresses: {:?}", self.known_addresses);
3436                                        debug!("frontend: {:?}", frontend);
3437                                        return Err(ConfigError::WrongFrontendProtocol(
3438                                            ListenerProtocol::Https,
3439                                        ));
3440                                    }
3441                                }
3442                            }
3443                            None => {
3444                                // create a default listener for that front
3445                                let file_listener_protocol = if frontend.certificate.is_some() {
3446                                    self.push_tls_listener(ListenerBuilder::new(
3447                                        frontend.address.into(),
3448                                        ListenerProtocol::Https,
3449                                    ))?;
3450
3451                                    ListenerProtocol::Https
3452                                } else {
3453                                    self.push_http_listener(ListenerBuilder::new(
3454                                        frontend.address.into(),
3455                                        ListenerProtocol::Http,
3456                                    ))?;
3457
3458                                    ListenerProtocol::Http
3459                                };
3460                                self.known_addresses
3461                                    .insert(frontend.address, file_listener_protocol);
3462                            }
3463                        }
3464                    }
3465                }
3466                ClusterConfig::Tcp(ref mut tcp) => {
3467                    //FIXME: verify that different TCP clusters do not request the same address
3468                    for frontend in tcp.frontends.iter_mut() {
3469                        match self.known_addresses.get(&frontend.address) {
3470                            Some(ListenerProtocol::Http) | Some(ListenerProtocol::Https) => {
3471                                return Err(ConfigError::WrongFrontendProtocol(
3472                                    ListenerProtocol::Http,
3473                                ));
3474                            }
3475                            Some(ListenerProtocol::Udp) => {
3476                                // A `protocol = "tcp"` cluster whose frontend
3477                                // points at a `protocol = "udp"` listener is a
3478                                // UDP cluster (datagram knobs under
3479                                // `[clusters.<id>.udp]`). Mark the frontend so
3480                                // `generate_requests` emits `AddUdpFrontend`.
3481                                frontend.udp = true;
3482                            }
3483                            Some(ListenerProtocol::Tcp) => {}
3484                            None => {
3485                                // create a default listener for that front
3486                                self.push_tcp_listener(ListenerBuilder::new(
3487                                    frontend.address.into(),
3488                                    ListenerProtocol::Tcp,
3489                                ))?;
3490                                self.known_addresses
3491                                    .insert(frontend.address, ListenerProtocol::Tcp);
3492                            }
3493                        }
3494                    }
3495                }
3496            }
3497
3498            self.built.clusters.insert(id, cluster_config);
3499        }
3500        Ok(())
3501    }
3502
3503    /// Builds a [`Config`], populated with listeners and clusters
3504    pub fn into_config(&mut self) -> Result<Config, ConfigError> {
3505        if let Some(listeners) = &self.file.listeners {
3506            self.populate_listeners(listeners.clone())?;
3507        }
3508
3509        if let Some(file_cluster_configs) = &self.file.clusters {
3510            self.populate_clusters(file_cluster_configs.clone())?;
3511        }
3512
3513        // TCP SNI/ALPN routing invariants (sozu-proxy/sozu#1279). Collect
3514        // every TCP frontend's (address, sni, alpn) across all clusters,
3515        // then validate:
3516        //   (c) a listener must not mix a no-SNI frontend with any
3517        //       SNI-scoped frontend — an SNI-enabled listener prereads the
3518        //       ClientHello, so a raw-TCP fallback on the same address is
3519        //       unreachable for clients that don't send SNI and ambiguous
3520        //       for those that do;
3521        //   (b) two frontends on the same (address, sni) must not share an
3522        //       ALPN protocol, and at most one may leave alpn empty (the
3523        //       catch-all) — otherwise routing on that listener would
3524        //       depend on iteration order rather than configuration;
3525        //   (d)/(e) sni_preread_timeout must not exceed front_timeout, and
3526        //       sni_preread_max_bytes must not exceed buffer_size, on any
3527        //       listener an SNI frontend targets. Both are gated on
3528        //       SNI-presence (not just any TCP listener) so a legacy
3529        //       TCP-only config with a low front_timeout or small
3530        //       buffer_size — set long before this feature existed and
3531        //       never opting into SNI — keeps loading byte-identically.
3532        type SniAlpnByAddress = HashMap<SocketAddr, Vec<(Option<String>, Vec<String>)>>;
3533        let mut frontends_by_address: SniAlpnByAddress = HashMap::new();
3534        let mut addresses_with_no_sni_frontend: HashSet<SocketAddr> = HashSet::new();
3535        for cluster in self.built.clusters.values() {
3536            if let ClusterConfig::Tcp(tcp) = cluster {
3537                for frontend in &tcp.frontends {
3538                    // A `protocol = "tcp"` cluster frontend resolved against a
3539                    // `protocol = "udp"` listener (`frontend.udp`, set in
3540                    // `populate_clusters`) emits `AddUdpFrontend`, which
3541                    // carries no `sni`/`alpn` on the wire — it is not a real
3542                    // SNI-preread TCP frontend and must not participate in
3543                    // these TCP-only invariants.
3544                    if frontend.udp {
3545                        continue;
3546                    }
3547                    if frontend.sni.is_none() {
3548                        addresses_with_no_sni_frontend.insert(frontend.address);
3549                    }
3550                    frontends_by_address
3551                        .entry(frontend.address)
3552                        .or_default()
3553                        .push((frontend.sni.clone(), frontend.alpn.clone()));
3554                }
3555            }
3556        }
3557
3558        let mut addresses_with_sni_frontend: HashSet<SocketAddr> = HashSet::new();
3559        for (address, frontends) in &frontends_by_address {
3560            if !frontends.iter().any(|(sni, _)| sni.is_some()) {
3561                continue;
3562            }
3563            addresses_with_sni_frontend.insert(*address);
3564
3565            if addresses_with_no_sni_frontend.contains(address) {
3566                return Err(ConfigError::TcpListenerMixesSniAndNoSni { address: *address });
3567            }
3568
3569            let mut alpn_lists_by_sni: HashMap<Option<String>, Vec<&Vec<String>>> = HashMap::new();
3570            for (sni, alpn) in frontends {
3571                alpn_lists_by_sni.entry(sni.clone()).or_default().push(alpn);
3572            }
3573            for (sni, alpn_lists) in alpn_lists_by_sni {
3574                let mut seen_protocols: HashSet<&str> = HashSet::new();
3575                let mut catch_all_count = 0usize;
3576                for alpn in alpn_lists {
3577                    if alpn.is_empty() {
3578                        catch_all_count += 1;
3579                        if catch_all_count > 1 {
3580                            return Err(ConfigError::TcpFrontendMultipleAlpnCatchAll {
3581                                address: *address,
3582                                sni: sni.clone(),
3583                            });
3584                        }
3585                        continue;
3586                    }
3587                    for protocol in alpn {
3588                        if !seen_protocols.insert(protocol.as_str()) {
3589                            return Err(ConfigError::TcpFrontendAlpnOverlap {
3590                                address: *address,
3591                                sni: sni.clone(),
3592                                protocol: protocol.clone(),
3593                            });
3594                        }
3595                    }
3596                }
3597            }
3598        }
3599
3600        for listener in &self.built.tcp_listeners {
3601            let address: SocketAddr = listener.address.into();
3602            if !addresses_with_sni_frontend.contains(&address) {
3603                continue;
3604            }
3605            let sni_preread_timeout = listener
3606                .sni_preread_timeout
3607                .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT);
3608            if sni_preread_timeout > listener.front_timeout {
3609                return Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
3610                    address,
3611                    sni_preread_timeout,
3612                    front_timeout: listener.front_timeout,
3613                });
3614            }
3615            let sni_preread_max_bytes = listener
3616                .sni_preread_max_bytes
3617                .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES);
3618            if sni_preread_max_bytes < MIN_SNI_PREREAD_MAX_BYTES {
3619                return Err(ConfigError::SniPrereadMaxBytesTooSmall {
3620                    address,
3621                    sni_preread_max_bytes,
3622                    minimum: MIN_SNI_PREREAD_MAX_BYTES,
3623                });
3624            }
3625            if u64::from(sni_preread_max_bytes) > self.built.buffer_size {
3626                return Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
3627                    address,
3628                    sni_preread_max_bytes,
3629                    buffer_size: self.built.buffer_size,
3630                });
3631            }
3632        }
3633
3634        // RFC 9113 §6.5.2 + §4.1: the H2 mux must accept up to
3635        // SETTINGS_MAX_FRAME_SIZE (16 384) + 9-byte frame header in a single
3636        // kawa buffer. If any HTTPS listener advertises "h2" in its ALPN list
3637        // and the global buffer_size is below H2_MIN_BUFFER_SIZE, the mux
3638        // deadlocks on full-size DATA / HEADERS / CONTINUATION frames until
3639        // the session timeout fires. Reject at config load so the failure
3640        // mode surfaces at boot, not under traffic.
3641        // Long-form rationale: `lib/src/protocol/mux/LIFECYCLE.md`.
3642        let h2_listeners = self
3643            .built
3644            .https_listeners
3645            .iter()
3646            .filter(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
3647            .count();
3648        if h2_listeners > 0 && self.built.buffer_size < H2_MIN_BUFFER_SIZE {
3649            return Err(ConfigError::BufferSizeTooSmallForH2 {
3650                buffer_size: self.built.buffer_size,
3651                minimum: H2_MIN_BUFFER_SIZE,
3652                listeners: h2_listeners,
3653            });
3654        }
3655
3656        // Warn (no hard reject) when the configured Basic-auth credential
3657        // cap is large enough to dominate the per-frontend buffer. The
3658        // worker copies a decoded credential into a transient allocation
3659        // sized by this cap; values >= 33% of `buffer_size` mean a single
3660        // failed-auth attempt can hold a third of the buffer's worth of
3661        // bytes, which combined with in-flight request/response framing
3662        // pushes the buffer toward back-pressure under load. Log only —
3663        // operators with deliberate threat models may choose this
3664        // trade-off, but the surprise needs to be visible.
3665        if let Some(cap) = self.built.basic_auth_max_credential_bytes {
3666            let third = self.built.buffer_size / 3;
3667            if cap >= third {
3668                warn!(
3669                    "basic_auth_max_credential_bytes = {} is >= buffer_size / 3 ({}); \
3670                     a hostile peer can pin ~33% of the per-frontend buffer per failed auth \
3671                     attempt. Consider lowering basic_auth_max_credential_bytes (typical \
3672                     credentials are <100 bytes) or raising buffer_size.",
3673                    cap, third
3674                );
3675            }
3676        }
3677
3678        // The eviction batch is `(max_connections / 100).max(1)` — a 1% ratio
3679        // by design. Below 100 connections the floor of 1 means each cap
3680        // event evicts a larger share than 1% of capacity (e.g. 4% at
3681        // max_connections=25), which can surprise an operator who reads the
3682        // knob as "1% per round". Warn at config load so the discrepancy is
3683        // visible at boot, not under traffic.
3684        if self.built.evict_on_queue_full && self.built.max_connections < 100 {
3685            let pct = 100usize.div_ceil(self.built.max_connections);
3686            warn!(
3687                "evict_on_queue_full enabled with max_connections = {}; the eviction batch \
3688                 clamps to 1, equivalent to ~{}% of capacity per cap event (the knob is \
3689                 documented as 1%). Confirm this is intended.",
3690                self.built.max_connections, pct
3691            );
3692        }
3693
3694        let command_socket_path = self.file.command_socket.clone().unwrap_or({
3695            let mut path = env::current_dir().map_err(|e| ConfigError::Env(e.to_string()))?;
3696            path.push("sozu.sock");
3697            let verified_path = path
3698                .to_str()
3699                .ok_or(ConfigError::InvalidPath(path.clone()))?;
3700            verified_path.to_owned()
3701        });
3702
3703        if let (None, Some(true)) = (&self.file.saved_state, &self.file.automatic_state_save) {
3704            return Err(ConfigError::Missing(MissingKind::SavedState));
3705        }
3706
3707        let config = Config {
3708            command_socket: command_socket_path,
3709            ..self.built.clone()
3710        };
3711
3712        // POST: a successfully built config satisfies the buffer-pool
3713        // invariants every worker relies on.
3714        // 1. min_buffers <= max_buffers — guaranteed by the `std::cmp::min`
3715        //    clamp in `new`; a violation would let a worker size its free-list
3716        //    floor above its ceiling.
3717        debug_assert!(
3718            config.min_buffers <= config.max_buffers,
3719            "min_buffers must not exceed max_buffers"
3720        );
3721        // 2. If any HTTPS listener advertises h2 in its ALPN, the global
3722        //    buffer_size is at least the H2 minimum — otherwise the early
3723        //    return above would have produced a BufferSizeTooSmallForH2 error
3724        //    rather than this Ok. (Recomputed here so the assert is independent
3725        //    of the local `h2_listeners` binding above.)
3726        debug_assert!(
3727            !config
3728                .https_listeners
3729                .iter()
3730                .any(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
3731                || config.buffer_size >= H2_MIN_BUFFER_SIZE,
3732            "an h2-advertising config must satisfy the H2 minimum buffer size"
3733        );
3734        Ok(config)
3735    }
3736}
3737
3738/// Sōzu configuration, populated with clusters and listeners.
3739///
3740/// This struct is used on startup to generate `WorkerRequest`s
3741#[derive(Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
3742pub struct Config {
3743    pub config_path: String,
3744    pub command_socket: String,
3745    pub command_buffer_size: u64,
3746    pub max_command_buffer_size: u64,
3747    pub max_connections: usize,
3748    pub min_buffers: u64,
3749    pub max_buffers: u64,
3750    pub buffer_size: u64,
3751    pub saved_state: Option<String>,
3752    #[serde(default)]
3753    pub automatic_state_save: bool,
3754    pub log_level: String,
3755    pub log_target: String,
3756    pub log_colored: bool,
3757    /// Optional dedicated file path for the control-plane audit log. See
3758    /// `FileConfig::audit_logs_target` for rationale.
3759    #[serde(default)]
3760    pub audit_logs_target: Option<String>,
3761    /// Optional JSON mirror of the audit log; see
3762    /// `FileConfig::audit_logs_json_target`.
3763    #[serde(default)]
3764    pub audit_logs_json_target: Option<String>,
3765    #[serde(default)]
3766    pub access_logs_target: Option<String>,
3767    pub access_logs_format: Option<AccessLogFormat>,
3768    pub access_logs_colored: Option<bool>,
3769    pub worker_count: u16,
3770    pub worker_automatic_restart: bool,
3771    pub metrics: Option<MetricsConfig>,
3772    #[serde(default = "default_disable_cluster_metrics")]
3773    pub disable_cluster_metrics: bool,
3774    pub http_listeners: Vec<HttpListenerConfig>,
3775    pub https_listeners: Vec<HttpsListenerConfig>,
3776    pub tcp_listeners: Vec<TcpListenerConfig>,
3777    #[serde(default)]
3778    pub udp_listeners: Vec<UdpListenerConfig>,
3779    pub clusters: HashMap<String, ClusterConfig>,
3780    pub handle_process_affinity: bool,
3781    pub ctl_command_timeout: u64,
3782    pub pid_file_path: Option<String>,
3783    pub activate_listeners: bool,
3784    #[serde(default = "default_front_timeout")]
3785    pub front_timeout: u32,
3786    #[serde(default = "default_back_timeout")]
3787    pub back_timeout: u32,
3788    #[serde(default = "default_connect_timeout")]
3789    pub connect_timeout: u32,
3790    #[serde(default = "default_zombie_check_interval")]
3791    pub zombie_check_interval: u32,
3792    #[serde(default = "default_accept_queue_timeout")]
3793    pub accept_queue_timeout: u32,
3794    #[serde(default = "default_evict_on_queue_full")]
3795    pub evict_on_queue_full: bool,
3796    #[serde(default = "default_request_timeout")]
3797    pub request_timeout: u32,
3798    #[serde(default = "default_worker_timeout")]
3799    pub worker_timeout: u32,
3800    /// Slab-entries-per-connection multiplier exposed for operators with
3801    /// fan-out topologies that exceed the default 4 backends per session.
3802    /// `None` means the default (4) applies; set values are clamped to
3803    /// [`ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION`,
3804    /// `ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION`] = [2, 32]. Slab
3805    /// capacity is `10 + slab_entries_per_connection * max_connections`.
3806    #[serde(default)]
3807    pub slab_entries_per_connection: Option<u64>,
3808    /// Optional allowlist of UIDs permitted to invoke command-socket
3809    /// requests. `None` keeps the historical "any same-UID local process"
3810    /// behaviour. When `Some`, every request whose `SO_PEERCRED` UID is
3811    /// not in the list is rejected before reaching dispatch.
3812    #[serde(default)]
3813    pub command_allowed_uids: Option<Vec<u32>>,
3814    /// Maximum length, in bytes, of a base64-decoded `Authorization: Basic`
3815    /// payload accepted by `mux::auth`. `None` keeps the compile-time
3816    /// default of 4096. Set once on each worker at boot via
3817    /// [`ServerConfig::basic_auth_max_credential_bytes`].
3818    #[serde(default)]
3819    pub basic_auth_max_credential_bytes: Option<u64>,
3820    /// Default per-(cluster, source-IP) connection limit. `0` means
3821    /// unlimited. Each cluster may override via its own
3822    /// `max_connections_per_ip`. Source IP attribution honours the
3823    /// proxy-protocol header when present.
3824    #[serde(default = "default_max_connections_per_ip")]
3825    pub max_connections_per_ip: u64,
3826    /// Default `Retry-After` header value (seconds) emitted on HTTP 429
3827    /// responses. `0` omits the header.
3828    #[serde(default = "default_retry_after")]
3829    pub retry_after: u32,
3830    /// Requested kernel-pipe capacity, in bytes, for each `splice(2)`
3831    /// zero-copy direction. `None` keeps the kernel default of 64 KiB.
3832    /// Applied via `fcntl(F_SETPIPE_SZ)` per pipe at `SplicePipe::new`;
3833    /// the kernel rounds up to a page boundary and clamps at
3834    /// `/proc/sys/fs/pipe-max-size`. Linux-only; ignored on builds
3835    /// without the `splice` feature.
3836    #[serde(default)]
3837    pub splice_pipe_capacity_bytes: Option<u64>,
3838}
3839
3840fn default_front_timeout() -> u32 {
3841    DEFAULT_FRONT_TIMEOUT
3842}
3843
3844fn default_back_timeout() -> u32 {
3845    DEFAULT_BACK_TIMEOUT
3846}
3847
3848fn default_connect_timeout() -> u32 {
3849    DEFAULT_CONNECT_TIMEOUT
3850}
3851
3852fn default_request_timeout() -> u32 {
3853    DEFAULT_REQUEST_TIMEOUT
3854}
3855
3856fn default_zombie_check_interval() -> u32 {
3857    DEFAULT_ZOMBIE_CHECK_INTERVAL
3858}
3859
3860fn default_accept_queue_timeout() -> u32 {
3861    DEFAULT_ACCEPT_QUEUE_TIMEOUT
3862}
3863
3864fn default_evict_on_queue_full() -> bool {
3865    DEFAULT_EVICT_ON_QUEUE_FULL
3866}
3867
3868fn default_disable_cluster_metrics() -> bool {
3869    DEFAULT_DISABLE_CLUSTER_METRICS
3870}
3871
3872fn default_worker_timeout() -> u32 {
3873    DEFAULT_WORKER_TIMEOUT
3874}
3875
3876fn default_max_connections_per_ip() -> u64 {
3877    DEFAULT_MAX_CONNECTIONS_PER_IP
3878}
3879
3880fn default_retry_after() -> u32 {
3881    DEFAULT_RETRY_AFTER
3882}
3883
3884impl Config {
3885    /// Parse a TOML file and build a config out of it
3886    pub fn load_from_path(path: &str) -> Result<Config, ConfigError> {
3887        let file_config = FileConfig::load_from_path(path)?;
3888
3889        let mut config = ConfigBuilder::new(file_config, path).into_config()?;
3890
3891        // replace saved_state with a verified path
3892        config.saved_state = config.saved_state_path()?;
3893
3894        Ok(config)
3895    }
3896
3897    /// yields requests intended to recreate a proxy that match the config
3898    pub fn generate_config_messages(&self) -> Result<Vec<WorkerRequest>, ConfigError> {
3899        let mut v = Vec::new();
3900        let mut count = 0u8;
3901
3902        for listener in &self.http_listeners {
3903            v.push(WorkerRequest {
3904                id: format!("CONFIG-{count}"),
3905                content: RequestType::AddHttpListener(listener.clone()).into(),
3906            });
3907            count += 1;
3908        }
3909
3910        for listener in &self.https_listeners {
3911            v.push(WorkerRequest {
3912                id: format!("CONFIG-{count}"),
3913                content: RequestType::AddHttpsListener(listener.clone()).into(),
3914            });
3915            count += 1;
3916        }
3917
3918        for listener in &self.tcp_listeners {
3919            v.push(WorkerRequest {
3920                id: format!("CONFIG-{count}"),
3921                content: RequestType::AddTcpListener(*listener).into(),
3922            });
3923            count += 1;
3924        }
3925
3926        for listener in &self.udp_listeners {
3927            v.push(WorkerRequest {
3928                id: format!("CONFIG-{count}"),
3929                content: RequestType::AddUdpListener(*listener).into(),
3930            });
3931            count += 1;
3932        }
3933
3934        for cluster in self.clusters.values() {
3935            let mut orders = cluster.generate_requests()?;
3936            for content in orders.drain(..) {
3937                v.push(WorkerRequest {
3938                    id: format!("CONFIG-{count}"),
3939                    content,
3940                });
3941                count += 1;
3942            }
3943        }
3944
3945        if self.activate_listeners {
3946            for listener in &self.http_listeners {
3947                v.push(WorkerRequest {
3948                    id: format!("CONFIG-{count}"),
3949                    content: RequestType::ActivateListener(ActivateListener {
3950                        address: listener.address,
3951                        proxy: ListenerType::Http.into(),
3952                        from_scm: false,
3953                    })
3954                    .into(),
3955                });
3956                count += 1;
3957            }
3958
3959            for listener in &self.https_listeners {
3960                v.push(WorkerRequest {
3961                    id: format!("CONFIG-{count}"),
3962                    content: RequestType::ActivateListener(ActivateListener {
3963                        address: listener.address,
3964                        proxy: ListenerType::Https.into(),
3965                        from_scm: false,
3966                    })
3967                    .into(),
3968                });
3969                count += 1;
3970            }
3971
3972            for listener in &self.tcp_listeners {
3973                v.push(WorkerRequest {
3974                    id: format!("CONFIG-{count}"),
3975                    content: RequestType::ActivateListener(ActivateListener {
3976                        address: listener.address,
3977                        proxy: ListenerType::Tcp.into(),
3978                        from_scm: false,
3979                    })
3980                    .into(),
3981                });
3982                count += 1;
3983            }
3984
3985            for listener in &self.udp_listeners {
3986                v.push(WorkerRequest {
3987                    id: format!("CONFIG-{count}"),
3988                    content: RequestType::ActivateListener(ActivateListener {
3989                        address: listener.address,
3990                        proxy: ListenerType::Udp.into(),
3991                        from_scm: false,
3992                    })
3993                    .into(),
3994                });
3995                count += 1;
3996            }
3997        }
3998
3999        if self.disable_cluster_metrics {
4000            v.push(WorkerRequest {
4001                id: format!("CONFIG-{count}"),
4002                content: RequestType::ConfigureMetrics(MetricsConfiguration::Disabled.into())
4003                    .into(),
4004            });
4005            // count += 1; // uncomment if code is added below
4006        }
4007
4008        Ok(v)
4009    }
4010
4011    /// Get the path of the UNIX socket used to communicate with Sōzu
4012    pub fn command_socket_path(&self) -> Result<String, ConfigError> {
4013        let config_path_buf = PathBuf::from(self.config_path.clone());
4014        let mut config_dir = config_path_buf
4015            .parent()
4016            .ok_or(ConfigError::NoFileParent(
4017                config_path_buf.to_string_lossy().to_string(),
4018            ))?
4019            .to_path_buf();
4020
4021        let socket_path = PathBuf::from(self.command_socket.clone());
4022
4023        let mut socket_parent_dir = match socket_path.parent() {
4024            // if the socket path is of the form "./sozu.sock",
4025            // then the parent is the directory where config.toml is situated
4026            None => config_dir,
4027            Some(path) => {
4028                // concatenate the config directory and the relative path of the socket
4029                config_dir.push(path);
4030                // canonicalize to remove double dots like /path/to/config/directory/../../path/to/socket/directory/
4031                config_dir.canonicalize().map_err(|io_error| {
4032                    ConfigError::SocketPathError(format!(
4033                        "Could not canonicalize path {config_dir:?}: {io_error}"
4034                    ))
4035                })?
4036            }
4037        };
4038
4039        let socket_name = socket_path
4040            .file_name()
4041            .ok_or(ConfigError::SocketPathError(format!(
4042                "could not get command socket file name from {socket_path:?}"
4043            )))?;
4044
4045        // concatenate parent directory and socket file name
4046        socket_parent_dir.push(socket_name);
4047
4048        let command_socket_path = socket_parent_dir
4049            .to_str()
4050            .ok_or(ConfigError::SocketPathError(format!(
4051                "Invalid socket path {socket_parent_dir:?}"
4052            )))?
4053            .to_string();
4054
4055        Ok(command_socket_path)
4056    }
4057
4058    /// Get the path of where the state will be saved
4059    fn saved_state_path(&self) -> Result<Option<String>, ConfigError> {
4060        let path = match self.saved_state.as_ref() {
4061            Some(path) => path,
4062            None => return Ok(None),
4063        };
4064
4065        debug!("saved_stated path in the config: {}", path);
4066        let config_path = PathBuf::from(self.config_path.clone());
4067
4068        debug!("Config path buffer: {:?}", config_path);
4069        let config_dir = config_path
4070            .parent()
4071            .ok_or(ConfigError::SaveStatePath(format!(
4072                "Could get parent directory of config file {config_path:?}"
4073            )))?;
4074
4075        debug!("Config folder: {:?}", config_dir);
4076        if !config_dir.exists() {
4077            create_dir_all(config_dir).map_err(|io_error| {
4078                ConfigError::SaveStatePath(format!(
4079                    "failed to create state parent directory '{config_dir:?}': {io_error}"
4080                ))
4081            })?;
4082        }
4083
4084        let mut saved_state_path_raw = config_dir.to_path_buf();
4085        saved_state_path_raw.push(path);
4086        debug!(
4087            "Looking for saved state on the path {:?}",
4088            saved_state_path_raw
4089        );
4090
4091        match metadata(path) {
4092            Err(err) if matches!(err.kind(), ErrorKind::NotFound) => {
4093                info!("Create an empty state file at '{}'", path);
4094                File::create(path).map_err(|io_error| {
4095                    ConfigError::SaveStatePath(format!(
4096                        "failed to create state file '{path:?}': {io_error}"
4097                    ))
4098                })?;
4099            }
4100            _ => {}
4101        }
4102
4103        saved_state_path_raw.canonicalize().map_err(|io_error| {
4104            ConfigError::SaveStatePath(format!(
4105                "could not get saved state path from config file input {path:?}: {io_error}"
4106            ))
4107        })?;
4108
4109        let stringified_path = saved_state_path_raw
4110            .to_str()
4111            .ok_or(ConfigError::SaveStatePath(format!(
4112                "Invalid path {saved_state_path_raw:?}"
4113            )))?
4114            .to_string();
4115
4116        Ok(Some(stringified_path))
4117    }
4118
4119    /// read any file to a string
4120    pub fn load_file(path: &str) -> Result<String, ConfigError> {
4121        std::fs::read_to_string(path).map_err(|io_error| ConfigError::FileRead {
4122            path_to_read: path.to_owned(),
4123            io_error,
4124        })
4125    }
4126
4127    /// read any file to bytes
4128    pub fn load_file_bytes(path: &str) -> Result<Vec<u8>, ConfigError> {
4129        std::fs::read(path).map_err(|io_error| ConfigError::FileRead {
4130            path_to_read: path.to_owned(),
4131            io_error,
4132        })
4133    }
4134}
4135
4136impl fmt::Debug for Config {
4137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4138        f.debug_struct("Config")
4139            .field("config_path", &self.config_path)
4140            .field("command_socket", &self.command_socket)
4141            .field("command_buffer_size", &self.command_buffer_size)
4142            .field("max_command_buffer_size", &self.max_command_buffer_size)
4143            .field("max_connections", &self.max_connections)
4144            .field("min_buffers", &self.min_buffers)
4145            .field("max_buffers", &self.max_buffers)
4146            .field("buffer_size", &self.buffer_size)
4147            .field("saved_state", &self.saved_state)
4148            .field("automatic_state_save", &self.automatic_state_save)
4149            .field("log_level", &self.log_level)
4150            .field("log_target", &self.log_target)
4151            .field("access_logs_target", &self.access_logs_target)
4152            .field("audit_logs_target", &self.audit_logs_target)
4153            .field("audit_logs_json_target", &self.audit_logs_json_target)
4154            .field("access_logs_format", &self.access_logs_format)
4155            .field("worker_count", &self.worker_count)
4156            .field("worker_automatic_restart", &self.worker_automatic_restart)
4157            .field("metrics", &self.metrics)
4158            .field("disable_cluster_metrics", &self.disable_cluster_metrics)
4159            .field("handle_process_affinity", &self.handle_process_affinity)
4160            .field("ctl_command_timeout", &self.ctl_command_timeout)
4161            .field("pid_file_path", &self.pid_file_path)
4162            .field("activate_listeners", &self.activate_listeners)
4163            .field("front_timeout", &self.front_timeout)
4164            .field("back_timeout", &self.back_timeout)
4165            .field("connect_timeout", &self.connect_timeout)
4166            .field("zombie_check_interval", &self.zombie_check_interval)
4167            .field("accept_queue_timeout", &self.accept_queue_timeout)
4168            .field("evict_on_queue_full", &self.evict_on_queue_full)
4169            .field("request_timeout", &self.request_timeout)
4170            .field("worker_timeout", &self.worker_timeout)
4171            .finish()
4172    }
4173}
4174
4175fn display_toml_error(file: &str, error: &toml::de::Error) {
4176    println!("error parsing the configuration file '{file}': {error}");
4177    if let Some(Range { start, end }) = error.span() {
4178        print!("error parsing the configuration file '{file}' at position: {start}, {end}");
4179    }
4180}
4181
4182impl ServerConfig {
4183    /// Default number of slab entries per connection. Set to 4 to accommodate
4184    /// H2 multiplexing (1 frontend + up to 3 backend connections per
4185    /// frontend with stream multiplexing). Previous value was 2 for H1-only
4186    /// operation. Operators with topologies that fan out across more
4187    /// clusters per session can override via `slab_entries_per_connection`
4188    /// in the config (clamped to [2, 32]).
4189    pub const DEFAULT_SLAB_ENTRIES_PER_CONNECTION: u64 = 4;
4190    /// Lower bound for the runtime knob. Below 2 the slab cannot hold one
4191    /// frontend + one backend per session.
4192    pub const MIN_SLAB_ENTRIES_PER_CONNECTION: u64 = 2;
4193    /// Upper bound for the runtime knob. 32 caps memory blow-up from a
4194    /// runaway config; 32 backends per frontend covers any sane topology.
4195    pub const MAX_SLAB_ENTRIES_PER_CONNECTION: u64 = 32;
4196
4197    /// Effective slab-entries-per-connection. Applies the [MIN, MAX] clamp
4198    /// and falls back to the default when the proto field is absent or 0.
4199    pub fn effective_slab_entries_per_connection(&self) -> u64 {
4200        let effective = match self.slab_entries_per_connection {
4201            Some(0) | None => Self::DEFAULT_SLAB_ENTRIES_PER_CONNECTION,
4202            Some(n) => n.clamp(
4203                Self::MIN_SLAB_ENTRIES_PER_CONNECTION,
4204                Self::MAX_SLAB_ENTRIES_PER_CONNECTION,
4205            ),
4206        };
4207        // POST: the effective value is always inside the documented clamp
4208        // window [MIN, MAX], regardless of the raw config input. The default
4209        // itself sits inside that window, so every branch satisfies the bound.
4210        debug_assert!(
4211            (Self::MIN_SLAB_ENTRIES_PER_CONNECTION..=Self::MAX_SLAB_ENTRIES_PER_CONNECTION)
4212                .contains(&effective),
4213            "effective slab entries per connection must stay within [MIN, MAX]"
4214        );
4215        effective
4216    }
4217
4218    /// Size of the slab for the Session manager.
4219    ///
4220    /// With HTTP/2 multiplexing, each frontend session can have multiple backend
4221    /// connections (one per cluster), so we allocate
4222    /// [`Self::effective_slab_entries_per_connection`] entries per connection
4223    /// instead of the old H1-only multiplier of 2.
4224    pub fn slab_capacity(&self) -> u64 {
4225        let per_conn = self.effective_slab_entries_per_connection();
4226        let capacity = 10 + per_conn * self.max_connections;
4227        // POST: the slab always reserves the 10-entry base (listeners, command
4228        // channel, etc.) plus at least MIN entries per connection, so it can
4229        // never be smaller than the base. Strict `>` for any non-zero
4230        // max_connections since per_conn >= MIN >= 2.
4231        debug_assert!(
4232            capacity >= 10,
4233            "slab capacity must reserve the base entries"
4234        );
4235        debug_assert!(
4236            self.max_connections == 0 || capacity > 10,
4237            "a non-zero connection cap must reserve per-connection slab entries"
4238        );
4239        capacity
4240    }
4241}
4242
4243/// reduce the config to the bare minimum needed by a worker
4244impl From<&Config> for ServerConfig {
4245    fn from(config: &Config) -> Self {
4246        let metrics = config.metrics.clone().map(|m| ServerMetricsConfig {
4247            address: m.address.to_string(),
4248            tagged_metrics: m.tagged_metrics,
4249            prefix: m.prefix,
4250            detail: Some(MetricDetail::from(m.detail) as i32),
4251        });
4252        let server_config = Self {
4253            max_connections: config.max_connections as u64,
4254            front_timeout: config.front_timeout,
4255            back_timeout: config.back_timeout,
4256            connect_timeout: config.connect_timeout,
4257            zombie_check_interval: config.zombie_check_interval,
4258            accept_queue_timeout: config.accept_queue_timeout,
4259            min_buffers: config.min_buffers,
4260            max_buffers: config.max_buffers,
4261            buffer_size: config.buffer_size,
4262            log_level: config.log_level.clone(),
4263            log_target: config.log_target.clone(),
4264            access_logs_target: config.access_logs_target.clone(),
4265            audit_logs_target: config.audit_logs_target.clone(),
4266            audit_logs_json_target: config.audit_logs_json_target.clone(),
4267            command_buffer_size: config.command_buffer_size,
4268            max_command_buffer_size: config.max_command_buffer_size,
4269            metrics,
4270            access_log_format: ProtobufAccessLogFormat::from(&config.access_logs_format) as i32,
4271            log_colored: config.log_colored,
4272            slab_entries_per_connection: config.slab_entries_per_connection,
4273            basic_auth_max_credential_bytes: config.basic_auth_max_credential_bytes,
4274            evict_on_queue_full: Some(config.evict_on_queue_full),
4275            max_connections_per_ip: Some(config.max_connections_per_ip),
4276            retry_after: Some(config.retry_after),
4277            splice_pipe_capacity_bytes: config.splice_pipe_capacity_bytes,
4278        };
4279
4280        // POST: the worker-facing config preserves the buffer-pool invariant
4281        // (min <= max) and carries the sizing knobs through unchanged — a
4282        // worker derives its slab and buffer pool straight from these, so any
4283        // drift here would desynchronize the master's view from the worker's.
4284        debug_assert!(
4285            server_config.min_buffers <= server_config.max_buffers,
4286            "ServerConfig must preserve min_buffers <= max_buffers"
4287        );
4288        debug_assert_eq!(
4289            server_config.buffer_size, config.buffer_size,
4290            "ServerConfig buffer_size must mirror the source config"
4291        );
4292        debug_assert_eq!(
4293            server_config.max_connections, config.max_connections as u64,
4294            "ServerConfig max_connections must mirror the source config"
4295        );
4296        server_config
4297    }
4298}
4299
4300#[cfg(test)]
4301mod tests {
4302    use toml::to_string;
4303
4304    use super::*;
4305
4306    #[test]
4307    fn http_frontend_debug_redacts_pem_material() {
4308        const CERTIFICATE_SECRET: &str = "HTTP_FRONTEND_CERTIFICATE_PEM_SECRET_SENTINEL";
4309        const CHAIN_SECRET: &str = "HTTP_FRONTEND_CHAIN_PEM_SECRET_SENTINEL";
4310        const KEY_SECRET: &str = "HTTP_FRONTEND_KEY_PEM_SECRET_SENTINEL";
4311        const HOSTNAME_SECRET: &str = "HTTP_FRONTEND_HOSTNAME_SECRET_SENTINEL";
4312        const PATH_SECRET: &str = "HTTP_FRONTEND_PATH_SECRET_SENTINEL";
4313        const METHOD_SECRET: &str = "HTTP_FRONTEND_METHOD_SECRET_SENTINEL";
4314        const TAG_KEY_SECRET: &str = "HTTP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
4315        const TAG_SECRET: &str = "HTTP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
4316        const HEADER_KEY_SECRET: &str = "HTTP_FRONTEND_HEADER_KEY_SECRET_SENTINEL";
4317        const HEADER_SECRET: &str = "HTTP_FRONTEND_HEADER_VALUE_SECRET_SENTINEL";
4318        const REDIRECT_TEMPLATE_SECRET: &str = "HTTP_FRONTEND_REDIRECT_TEMPLATE_SECRET_SENTINEL";
4319        const REWRITE_HOST_SECRET: &str = "HTTP_FRONTEND_REWRITE_HOST_SECRET_SENTINEL";
4320        const REWRITE_PATH_SECRET: &str = "HTTP_FRONTEND_REWRITE_PATH_SECRET_SENTINEL";
4321
4322        let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
4323        let certificate = long_value(CERTIFICATE_SECRET);
4324        let certificate_chain = long_value(CHAIN_SECRET);
4325        let key = long_value(KEY_SECRET);
4326        let hostname = long_value(HOSTNAME_SECRET);
4327        let path = long_value(PATH_SECRET);
4328        let method = long_value(METHOD_SECRET);
4329        let redirect_template = long_value(REDIRECT_TEMPLATE_SECRET);
4330        let rewrite_host = long_value(REWRITE_HOST_SECRET);
4331        let rewrite_path = long_value(REWRITE_PATH_SECRET);
4332
4333        let frontend = HttpFrontendConfig {
4334            address: "127.0.0.1:8443".parse().unwrap(),
4335            hostname,
4336            path: PathRule::prefix(path),
4337            method: Some(method),
4338            certificate: Some(certificate),
4339            key: Some(key),
4340            certificate_chain: Some(vec![certificate_chain]),
4341            tls_versions: vec![TlsVersion::TlsV13],
4342            position: RulePosition::Tree,
4343            tags: Some(BTreeMap::from([(
4344                long_value(TAG_KEY_SECRET),
4345                long_value(TAG_SECRET),
4346            )])),
4347            redirect: None,
4348            redirect_scheme: None,
4349            redirect_template: Some(redirect_template),
4350            rewrite_host: Some(rewrite_host),
4351            rewrite_path: Some(rewrite_path),
4352            rewrite_port: None,
4353            required_auth: None,
4354            headers: vec![Header {
4355                position: HeaderPosition::Request as i32,
4356                key: long_value(HEADER_KEY_SECRET),
4357                val: long_value(HEADER_SECRET),
4358            }],
4359            hsts: None,
4360        };
4361
4362        let output = format!("{frontend:?}");
4363
4364        let secrets = [
4365            CERTIFICATE_SECRET,
4366            CHAIN_SECRET,
4367            KEY_SECRET,
4368            HOSTNAME_SECRET,
4369            PATH_SECRET,
4370            METHOD_SECRET,
4371            TAG_KEY_SECRET,
4372            TAG_SECRET,
4373            HEADER_KEY_SECRET,
4374            HEADER_SECRET,
4375            REDIRECT_TEMPLATE_SECRET,
4376            REWRITE_HOST_SECRET,
4377            REWRITE_PATH_SECRET,
4378        ];
4379        for secret in secrets {
4380            assert!(
4381                !output.contains(secret),
4382                "HttpFrontendConfig Debug leaked secret marker {secret}: {output}"
4383            );
4384        }
4385        let expected_metadata = [
4386            "address: 127.0.0.1:8443".to_owned(),
4387            format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
4388            format!("path_kind: {}", PathRule::prefix(String::new()).kind),
4389            format!("path_len: {}", long_value(PATH_SECRET).len()),
4390            format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
4391            "certificate: Some(\"[redacted]\")".to_owned(),
4392            format!(
4393                "certificate_len: Some({})",
4394                long_value(CERTIFICATE_SECRET).len()
4395            ),
4396            "key: Some(\"[redacted]\")".to_owned(),
4397            format!("key_len: Some({})", long_value(KEY_SECRET).len()),
4398            "certificate_chain: Some(\"[redacted]\")".to_owned(),
4399            "certificate_chain_count: Some(1)".to_owned(),
4400            format!(
4401                "certificate_chain_len: Some({})",
4402                long_value(CHAIN_SECRET).len()
4403            ),
4404            "tls_versions_count: 1".to_owned(),
4405            "tags_count: 1".to_owned(),
4406            format!(
4407                "redirect_template_len: Some({})",
4408                long_value(REDIRECT_TEMPLATE_SECRET).len()
4409            ),
4410            format!(
4411                "rewrite_host_len: Some({})",
4412                long_value(REWRITE_HOST_SECRET).len()
4413            ),
4414            format!(
4415                "rewrite_path_len: Some({})",
4416                long_value(REWRITE_PATH_SECRET).len()
4417            ),
4418            "headers_count: 1".to_owned(),
4419        ];
4420        for safe_metadata in expected_metadata {
4421            assert!(
4422                output.contains(&safe_metadata),
4423                "HttpFrontendConfig Debug omitted safe metadata {safe_metadata}: {output}"
4424            );
4425        }
4426        assert!(
4427            output.len() <= 1024,
4428            "HttpFrontendConfig Debug output is not bounded: {} bytes",
4429            output.len()
4430        );
4431
4432        for (index, request) in frontend
4433            .generate_requests("safe-cluster-id")
4434            .into_iter()
4435            .enumerate()
4436        {
4437            let generated_output = format!("{request:?}");
4438            for secret in secrets {
4439                assert!(
4440                    !generated_output.contains(secret),
4441                    "generated frontend request {index} Debug leaked secret marker {secret}: {generated_output}"
4442                );
4443            }
4444            assert!(
4445                generated_output.len() <= 2048,
4446                "generated frontend request {index} Debug output is not bounded: {} bytes",
4447                generated_output.len()
4448            );
4449        }
4450    }
4451
4452    #[test]
4453    fn hsts_to_proto_enabled_substitutes_default_max_age() {
4454        let cfg = FileHstsConfig {
4455            enabled: Some(true),
4456            max_age: None,
4457            include_subdomains: None,
4458            preload: None,
4459            force_replace_backend: None,
4460        };
4461        let proto = cfg.to_proto("test").expect("should validate");
4462        assert_eq!(proto.enabled, Some(true));
4463        assert_eq!(proto.max_age, Some(DEFAULT_HSTS_MAX_AGE));
4464    }
4465
4466    #[test]
4467    fn hsts_to_proto_explicit_max_age_kept() {
4468        let cfg = FileHstsConfig {
4469            enabled: Some(true),
4470            max_age: Some(63_072_000),
4471            include_subdomains: Some(true),
4472            preload: Some(true),
4473            force_replace_backend: None,
4474        };
4475        let proto = cfg.to_proto("test").expect("should validate");
4476        assert_eq!(proto.max_age, Some(63_072_000));
4477        assert_eq!(proto.include_subdomains, Some(true));
4478        assert_eq!(proto.preload, Some(true));
4479    }
4480
4481    #[test]
4482    fn hsts_to_proto_disabled_keeps_zero_intent() {
4483        // `enabled = false` means "explicit disable" — the materialiser
4484        // in `Frontend::new` won't append an edit, so the proto still
4485        // round-trips with `enabled = Some(false)`.
4486        let cfg = FileHstsConfig {
4487            enabled: Some(false),
4488            max_age: None,
4489            include_subdomains: None,
4490            preload: None,
4491            force_replace_backend: None,
4492        };
4493        let proto = cfg.to_proto("test").expect("should validate");
4494        assert_eq!(proto.enabled, Some(false));
4495    }
4496
4497    #[test]
4498    fn hsts_to_proto_kill_switch_max_age_zero_allowed() {
4499        // RFC 6797 §11.4: `max-age=0` instructs the UA to "cease
4500        // regarding the host as a Known HSTS Host". Explicit operator
4501        // intent — must NOT warn or fail.
4502        let cfg = FileHstsConfig {
4503            enabled: Some(true),
4504            max_age: Some(0),
4505            include_subdomains: None,
4506            preload: None,
4507            force_replace_backend: None,
4508        };
4509        let proto = cfg.to_proto("test").expect("kill-switch must validate");
4510        assert_eq!(proto.max_age, Some(0));
4511    }
4512
4513    #[test]
4514    fn hsts_to_proto_missing_enabled_errors() {
4515        let cfg = FileHstsConfig {
4516            enabled: None,
4517            max_age: Some(31_536_000),
4518            include_subdomains: None,
4519            preload: None,
4520            force_replace_backend: None,
4521        };
4522        match cfg.to_proto("test").unwrap_err() {
4523            ConfigError::HstsEnabledRequired(scope) => assert_eq!(scope, "test"),
4524            other => panic!("expected HstsEnabledRequired, got {other:?}"),
4525        }
4526    }
4527
4528    #[test]
4529    fn hsts_rejected_on_http_listener() {
4530        // RFC 6797 §7.2: an [hsts] block on an HTTP listener must be
4531        // rejected at TOML config-load — `HttpListenerConfig` carries no
4532        // `hsts` field and silently dropping the operator's intent
4533        // would be a worse failure mode than a typed error.
4534        let mut listener = ListenerBuilder::new(
4535            SocketAddress::new_v4(127, 0, 0, 1, 8080),
4536            ListenerProtocol::Http,
4537        );
4538        listener.hsts = Some(FileHstsConfig {
4539            enabled: Some(true),
4540            max_age: Some(31_536_000),
4541            include_subdomains: None,
4542            preload: None,
4543            force_replace_backend: None,
4544        });
4545        match listener.to_http(None).unwrap_err() {
4546            ConfigError::HstsOnPlainHttp(scope) => assert!(
4547                scope.contains("HTTP listener"),
4548                "expected scope to mention 'HTTP listener', got {scope:?}"
4549            ),
4550            other => panic!("expected HstsOnPlainHttp, got {other:?}"),
4551        }
4552    }
4553
4554    #[test]
4555    fn hsts_rejected_on_http_frontend() {
4556        // A `FileClusterFrontendConfig` without a key+certificate pair
4557        // generates `RequestType::AddHttpFrontend` in
4558        // `HttpFrontendConfig::generate_requests`. RFC 6797 §7.2 forbids
4559        // HSTS on plaintext HTTP, so an `[hsts]` block on a cert-less
4560        // (HTTP-bound) frontend must be rejected at TOML config-load.
4561        let frontend = FileClusterFrontendConfig {
4562            address: "127.0.0.1:8080".parse().unwrap(),
4563            hostname: Some("example.com".to_owned()),
4564            alpn: vec![],
4565            path: None,
4566            path_type: None,
4567            method: None,
4568            certificate: None,
4569            key: None,
4570            certificate_chain: None,
4571            tls_versions: vec![],
4572            position: RulePosition::Tree,
4573            tags: None,
4574            redirect: None,
4575            redirect_scheme: None,
4576            redirect_template: None,
4577            rewrite_host: None,
4578            rewrite_path: None,
4579            rewrite_port: None,
4580            required_auth: None,
4581            headers: None,
4582            hsts: Some(FileHstsConfig {
4583                enabled: Some(true),
4584                max_age: Some(31_536_000),
4585                include_subdomains: None,
4586                preload: None,
4587                force_replace_backend: None,
4588            }),
4589        };
4590        match frontend.to_http_front("api").unwrap_err() {
4591            ConfigError::HstsOnPlainHttp(scope) => {
4592                assert!(
4593                    scope.contains("api") && scope.contains("example.com"),
4594                    "expected scope to mention 'api' and 'example.com', got {scope:?}"
4595                );
4596            }
4597            other => panic!("expected HstsOnPlainHttp, got {other:?}"),
4598        }
4599    }
4600
4601    #[test]
4602    fn serialize() {
4603        let http = ListenerBuilder::new(
4604            SocketAddress::new_v4(127, 0, 0, 1, 8080),
4605            ListenerProtocol::Http,
4606        )
4607        .with_answer_404_path(Some("404.html"))
4608        .to_owned();
4609        println!("http: {:?}", to_string(&http));
4610
4611        let https = ListenerBuilder::new(
4612            SocketAddress::new_v4(127, 0, 0, 1, 8443),
4613            ListenerProtocol::Https,
4614        )
4615        .with_answer_404_path(Some("404.html"))
4616        .to_owned();
4617        println!("https: {:?}", to_string(&https));
4618
4619        let listeners = vec![http, https];
4620        let config = FileConfig {
4621            command_socket: Some(String::from("./command_folder/sock")),
4622            worker_count: Some(2),
4623            worker_automatic_restart: Some(true),
4624            max_connections: Some(500),
4625            min_buffers: Some(1),
4626            max_buffers: Some(500),
4627            buffer_size: Some(16393),
4628            metrics: Some(MetricsConfig {
4629                address: "127.0.0.1:8125".parse().unwrap(),
4630                tagged_metrics: false,
4631                prefix: Some(String::from("sozu-metrics")),
4632                detail: MetricDetailLevel::default(),
4633            }),
4634            listeners: Some(listeners),
4635            ..Default::default()
4636        };
4637
4638        println!("config: {:?}", to_string(&config));
4639        let encoded = to_string(&config).unwrap();
4640        println!("conf:\n{encoded}");
4641    }
4642
4643    #[test]
4644    fn parse() {
4645        let path = "assets/config.toml";
4646        let config = Config::load_from_path(path).unwrap_or_else(|load_error| {
4647            panic!("Cannot load config from path {path}: {load_error:?}")
4648        });
4649        println!("config: {config:#?}");
4650        //panic!();
4651    }
4652
4653    #[test]
4654    fn multiple_listeners_preserve_per_address_expect_proxy() {
4655        let toml_content = r#"
4656            command_socket = "/tmp/sozu_test.sock"
4657            worker_count = 1
4658
4659            [[listeners]]
4660            protocol = "http"
4661            address = "172.16.20.1:80"
4662            expect_proxy = true
4663
4664            [[listeners]]
4665            protocol = "http"
4666            address = "10.22.0.1:80"
4667            expect_proxy = false
4668
4669            [[listeners]]
4670            protocol = "https"
4671            address = "192.168.1.1:443"
4672            expect_proxy = true
4673
4674            [[listeners]]
4675            protocol = "https"
4676            address = "192.168.2.1:443"
4677            expect_proxy = false
4678        "#;
4679
4680        let file_config: FileConfig =
4681            toml::from_str(toml_content).expect("Could not parse TOML config");
4682
4683        let listeners = file_config.listeners.as_ref().expect("No listeners found");
4684        assert_eq!(listeners.len(), 4);
4685
4686        let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4687            .into_config()
4688            .expect("Could not build config");
4689
4690        assert_eq!(config.http_listeners.len(), 2);
4691        assert_eq!(config.https_listeners.len(), 2);
4692
4693        // HTTP listeners
4694        let http_proxy = config
4695            .http_listeners
4696            .iter()
4697            .find(|l| SocketAddr::from(l.address) == "172.16.20.1:80".parse().unwrap())
4698            .expect("Listener on 172.16.20.1:80 not found");
4699        let http_direct = config
4700            .http_listeners
4701            .iter()
4702            .find(|l| SocketAddr::from(l.address) == "10.22.0.1:80".parse().unwrap())
4703            .expect("Listener on 10.22.0.1:80 not found");
4704
4705        assert!(http_proxy.expect_proxy);
4706        assert!(!http_direct.expect_proxy);
4707
4708        // HTTPS listeners
4709        let https_proxy = config
4710            .https_listeners
4711            .iter()
4712            .find(|l| SocketAddr::from(l.address) == "192.168.1.1:443".parse().unwrap())
4713            .expect("Listener on 192.168.1.1:443 not found");
4714        let https_direct = config
4715            .https_listeners
4716            .iter()
4717            .find(|l| SocketAddr::from(l.address) == "192.168.2.1:443".parse().unwrap())
4718            .expect("Listener on 192.168.2.1:443 not found");
4719
4720        assert!(https_proxy.expect_proxy);
4721        assert!(!https_direct.expect_proxy);
4722    }
4723
4724    #[test]
4725    fn multiple_listeners_generate_correct_worker_requests() {
4726        let toml_content = r#"
4727            command_socket = "/tmp/sozu_test.sock"
4728            worker_count = 1
4729            activate_listeners = true
4730
4731            [[listeners]]
4732            protocol = "http"
4733            address = "172.16.20.1:80"
4734            expect_proxy = true
4735
4736            [[listeners]]
4737            protocol = "http"
4738            address = "10.22.0.1:80"
4739            expect_proxy = false
4740        "#;
4741
4742        let file_config: FileConfig =
4743            toml::from_str(toml_content).expect("Could not parse TOML config");
4744
4745        let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4746            .into_config()
4747            .expect("Could not build config");
4748
4749        let messages = config
4750            .generate_config_messages()
4751            .expect("Could not generate config messages");
4752
4753        let add_listener_count = messages
4754            .iter()
4755            .filter(|m| {
4756                matches!(
4757                    m.content.request_type,
4758                    Some(RequestType::AddHttpListener(_))
4759                )
4760            })
4761            .count();
4762
4763        let activate_listener_count = messages
4764            .iter()
4765            .filter(|m| {
4766                matches!(
4767                    m.content.request_type,
4768                    Some(RequestType::ActivateListener(ActivateListener {
4769                        proxy,
4770                        ..
4771                    })) if proxy == ListenerType::Http as i32
4772                )
4773            })
4774            .count();
4775
4776        assert_eq!(add_listener_count, 2);
4777        assert_eq!(activate_listener_count, 2);
4778    }
4779
4780    #[test]
4781    fn documented_udp_dns_example_loads_and_emits_udp_requests() {
4782        // The DNS example from doc/configure.md ("#### UDP clusters"): a
4783        // `protocol = "udp"` listener + a `protocol = "tcp"` cluster whose
4784        // frontend points at that UDP listener address, with datagram knobs
4785        // under `[clusters.dns.udp]`. This must load without a
4786        // `WrongFrontendProtocol` error and emit an `AddUdpListener` and an
4787        // `AddUdpFrontend` (not `AddTcpFrontend`).
4788        let toml_content = r#"
4789            command_socket = "/tmp/sozu_test.sock"
4790            worker_count = 1
4791            activate_listeners = true
4792
4793            [[listeners]]
4794            protocol = "udp"
4795            address  = "0.0.0.0:53"
4796
4797            [clusters.dns]
4798            protocol       = "tcp"
4799            load_balancing = "HRW"
4800            frontends = [
4801              { address = "0.0.0.0:53" }
4802            ]
4803            backends = [
4804              { address = "10.0.0.10:53" },
4805              { address = "10.0.0.11:53" }
4806            ]
4807
4808            [clusters.dns.udp]
4809            affinity_key        = "SOURCE_IP"
4810            responses           = 1
4811            requests            = 0
4812            send_proxy_protocol = true
4813
4814            [clusters.dns.udp.health]
4815            mode      = "TCP_PROBE"
4816            tcp_port  = 53
4817            rise      = 2
4818            fall      = 3
4819            fail_open = true
4820        "#;
4821
4822        let file_config: FileConfig =
4823            toml::from_str(toml_content).expect("Could not parse documented DNS TOML");
4824
4825        let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4826            .into_config()
4827            .expect("documented UDP DNS example must load without WrongFrontendProtocol");
4828
4829        // The UDP listener was registered.
4830        assert_eq!(
4831            config.udp_listeners.len(),
4832            1,
4833            "the protocol=\"udp\" listener must be built"
4834        );
4835
4836        let messages = config
4837            .generate_config_messages()
4838            .expect("Could not generate config messages");
4839
4840        let add_udp_listener_count = messages
4841            .iter()
4842            .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpListener(_))))
4843            .count();
4844        assert_eq!(
4845            add_udp_listener_count, 1,
4846            "must emit exactly one AddUdpListener"
4847        );
4848
4849        let add_udp_frontend_count = messages
4850            .iter()
4851            .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
4852            .count();
4853        assert_eq!(
4854            add_udp_frontend_count, 1,
4855            "the cluster frontend on the UDP listener must emit AddUdpFrontend"
4856        );
4857
4858        // It must NOT have been emitted as a TCP frontend.
4859        let add_tcp_frontend_count = messages
4860            .iter()
4861            .filter(|m| matches!(m.content.request_type, Some(RequestType::AddTcpFrontend(_))))
4862            .count();
4863        assert_eq!(
4864            add_tcp_frontend_count, 0,
4865            "a UDP-listener-addressed frontend must not be emitted as AddTcpFrontend"
4866        );
4867
4868        // The AddUdpFrontend carries the cluster id and address.
4869        let udp_frontend = messages
4870            .iter()
4871            .find_map(|m| match &m.content.request_type {
4872                Some(RequestType::AddUdpFrontend(f)) => Some(f),
4873                _ => None,
4874            })
4875            .expect("AddUdpFrontend must be present");
4876        assert_eq!(udp_frontend.cluster_id, "dns");
4877        assert_eq!(
4878            SocketAddr::from(udp_frontend.address),
4879            "0.0.0.0:53".parse().unwrap()
4880        );
4881
4882        // The UDP cluster knobs from [clusters.dns.udp] survive onto the
4883        // AddCluster request.
4884        let cluster = messages
4885            .iter()
4886            .find_map(|m| match &m.content.request_type {
4887                Some(RequestType::AddCluster(c)) if c.cluster_id == "dns" => Some(c),
4888                _ => None,
4889            })
4890            .expect("AddCluster for 'dns' must be present");
4891        let udp = cluster
4892            .udp
4893            .as_ref()
4894            .expect("[clusters.dns.udp] block must carry onto the cluster");
4895        assert_eq!(udp.responses, Some(1));
4896    }
4897
4898    #[test]
4899    fn duplicate_listener_address_rejected() {
4900        let toml_content = r#"
4901            command_socket = "/tmp/sozu_test.sock"
4902            worker_count = 1
4903
4904            [[listeners]]
4905            protocol = "http"
4906            address = "0.0.0.0:80"
4907
4908            [[listeners]]
4909            protocol = "http"
4910            address = "0.0.0.0:80"
4911        "#;
4912
4913        let file_config: FileConfig =
4914            toml::from_str(toml_content).expect("Could not parse TOML config");
4915
4916        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4917
4918        assert!(
4919            result.is_err(),
4920            "Should reject duplicate listener addresses"
4921        );
4922    }
4923
4924    #[test]
4925    fn buffer_size_below_h2_minimum_rejected() {
4926        // Default ALPN ["h2", "http/1.1"] + buffer_size = 8192 must error.
4927        let toml_content = r#"
4928            command_socket = "/tmp/sozu_test.sock"
4929            worker_count = 1
4930            buffer_size = 8192
4931
4932            [[listeners]]
4933            protocol = "https"
4934            address = "127.0.0.1:8443"
4935        "#;
4936        let file_config: FileConfig =
4937            toml::from_str(toml_content).expect("Could not parse TOML config");
4938        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4939        match result {
4940            Err(ConfigError::BufferSizeTooSmallForH2 {
4941                buffer_size: 8192,
4942                minimum: 16_393,
4943                listeners: 1,
4944            }) => {}
4945            other => panic!("expected BufferSizeTooSmallForH2, got {other:?}"),
4946        }
4947    }
4948
4949    #[test]
4950    fn buffer_size_below_h2_minimum_accepted_when_no_h2_listener() {
4951        // Drop "h2" from ALPN — buffer_size = 8192 is now valid.
4952        let toml_content = r#"
4953            command_socket = "/tmp/sozu_test.sock"
4954            worker_count = 1
4955            buffer_size = 8192
4956
4957            [[listeners]]
4958            protocol = "https"
4959            address = "127.0.0.1:8443"
4960            alpn_protocols = ["http/1.1"]
4961        "#;
4962        let file_config: FileConfig =
4963            toml::from_str(toml_content).expect("Could not parse TOML config");
4964        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4965        assert!(
4966            result.is_ok(),
4967            "non-H2 HTTPS listener with sub-16393 buffer should be accepted: {result:?}"
4968        );
4969    }
4970
4971    #[test]
4972    fn buffer_size_at_h2_minimum_accepted() {
4973        let toml_content = r#"
4974            command_socket = "/tmp/sozu_test.sock"
4975            worker_count = 1
4976            buffer_size = 16393
4977
4978            [[listeners]]
4979            protocol = "https"
4980            address = "127.0.0.1:8443"
4981        "#;
4982        let file_config: FileConfig =
4983            toml::from_str(toml_content).expect("Could not parse TOML config");
4984        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4985        assert!(
4986            result.is_ok(),
4987            "buffer_size at the H2 minimum should be accepted: {result:?}"
4988        );
4989    }
4990
4991    #[test]
4992    fn alpn_protocols_default() {
4993        let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4994        let config = builder.to_tls(None).expect("to_tls should succeed");
4995        assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4996    }
4997
4998    #[test]
4999    fn alpn_protocols_custom() {
5000        let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5001        builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned()]));
5002        let config = builder.to_tls(None).expect("to_tls should succeed");
5003        assert_eq!(config.alpn_protocols, vec!["http/1.1"]);
5004    }
5005
5006    #[test]
5007    fn alpn_protocols_invalid_rejected() {
5008        let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5009        builder.with_alpn_protocols(Some(vec!["h3".to_owned()]));
5010        let result = builder.to_tls(None);
5011        assert!(result.is_err());
5012        let err = result.unwrap_err();
5013        assert!(
5014            err.to_string().contains("h3"),
5015            "error should mention the invalid protocol: {err}"
5016        );
5017    }
5018
5019    #[test]
5020    fn alpn_protocols_empty_uses_default() {
5021        let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5022        builder.with_alpn_protocols(Some(vec![]));
5023        let config = builder.to_tls(None).expect("to_tls should succeed");
5024        assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
5025    }
5026
5027    #[test]
5028    fn alpn_protocols_deduplicated() {
5029        let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5030        builder.with_alpn_protocols(Some(vec![
5031            "h2".to_owned(),
5032            "h2".to_owned(),
5033            "http/1.1".to_owned(),
5034        ]));
5035        let config = builder.to_tls(None).expect("to_tls should succeed");
5036        assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
5037    }
5038
5039    #[test]
5040    fn alpn_protocols_order_preserved() {
5041        let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5042        builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned(), "h2".to_owned()]));
5043        let config = builder.to_tls(None).expect("to_tls should succeed");
5044        assert_eq!(config.alpn_protocols, vec!["http/1.1", "h2"]);
5045    }
5046
5047    /// CRLF or NUL in a `[[clusters.<id>.frontends.headers]]` value
5048    /// would let an operator-supplied config splice arbitrary
5049    /// header / request lines into the H1 wire on the backend side
5050    /// (CWE-113). The H2 emission path filters at runtime; we reject
5051    /// at config-load time as a defense in depth.
5052    #[test]
5053    fn parse_header_edit_rejects_crlf_in_value() {
5054        let entry = HeaderEditConfig {
5055            position: "request".to_owned(),
5056            key: "X-Test".to_owned(),
5057            value: "value\r\nEvil-Header: stolen".to_owned(),
5058        };
5059        let err = parse_header_edit(0, &entry).expect_err("CRLF in value must be rejected");
5060        match err {
5061            ConfigError::InvalidHeaderBytes { index, field } => {
5062                assert_eq!(index, 0);
5063                assert_eq!(field, "value");
5064            }
5065            other => panic!("expected InvalidHeaderBytes, got {other:?}"),
5066        }
5067    }
5068
5069    #[test]
5070    fn parse_header_edit_rejects_lf_in_key() {
5071        let entry = HeaderEditConfig {
5072            position: "response".to_owned(),
5073            key: "X-\nTest".to_owned(),
5074            value: "ok".to_owned(),
5075        };
5076        let err = parse_header_edit(2, &entry).expect_err("LF in key must be rejected");
5077        match err {
5078            ConfigError::InvalidHeaderBytes { index, field } => {
5079                assert_eq!(index, 2);
5080                assert_eq!(field, "key");
5081            }
5082            other => panic!("expected InvalidHeaderBytes, got {other:?}"),
5083        }
5084    }
5085
5086    #[test]
5087    fn parse_header_edit_rejects_nul() {
5088        let entry = HeaderEditConfig {
5089            position: "both".to_owned(),
5090            key: "X-Test".to_owned(),
5091            value: "with\0nul".to_owned(),
5092        };
5093        assert!(matches!(
5094            parse_header_edit(0, &entry),
5095            Err(ConfigError::InvalidHeaderBytes { .. })
5096        ));
5097    }
5098
5099    /// Horizontal tab `\t` (0x09) is permitted in field values per
5100    /// RFC 9110 §5.5 (folded-header obs-fold parts). The value-side
5101    /// validator must NOT reject it — otherwise legitimate operator
5102    /// configs (e.g. `Authorization: Basic\tCREDENTIALS`) become
5103    /// unusable. The key-side validator IS stricter (token grammar).
5104    #[test]
5105    fn parse_header_edit_accepts_tab_in_value() {
5106        let entry = HeaderEditConfig {
5107            position: "request".to_owned(),
5108            key: "X-Test".to_owned(),
5109            value: "with\ttab".to_owned(),
5110        };
5111        let header = parse_header_edit(0, &entry).expect("tab in value must be accepted");
5112        assert_eq!(header.val, "with\ttab");
5113    }
5114
5115    /// Header NAMES follow `token` grammar per RFC 9110 §5.1. HTAB and
5116    /// SP are NOT tchar; the key-side validator must reject them even
5117    /// though the value-side validator permits HTAB. Without this,
5118    /// an operator entry like `key = "Host\t"` would emit `Host\t: …`
5119    /// on the H1 wire and produce an invalid (but parser-tolerant)
5120    /// header line that some backends silently accept as `Host:`.
5121    #[test]
5122    fn parse_header_edit_rejects_tab_in_key() {
5123        let entry = HeaderEditConfig {
5124            position: "request".to_owned(),
5125            key: "Host\t".to_owned(),
5126            value: "ok".to_owned(),
5127        };
5128        let err = parse_header_edit(0, &entry).expect_err("HTAB in key must be rejected");
5129        match err {
5130            ConfigError::InvalidHeaderBytes { field, .. } => assert_eq!(field, "key"),
5131            other => panic!("expected InvalidHeaderBytes{{field=\"key\"}}, got {other:?}"),
5132        }
5133    }
5134
5135    #[test]
5136    fn parse_header_edit_rejects_space_in_key() {
5137        let entry = HeaderEditConfig {
5138            position: "request".to_owned(),
5139            key: "X Test".to_owned(),
5140            value: "ok".to_owned(),
5141        };
5142        let err = parse_header_edit(0, &entry).expect_err("SP in key must be rejected");
5143        assert!(matches!(err, ConfigError::InvalidHeaderBytes { .. }));
5144    }
5145
5146    #[test]
5147    fn parse_header_edit_rejects_empty_key() {
5148        let entry = HeaderEditConfig {
5149            position: "request".to_owned(),
5150            key: String::new(),
5151            value: "ok".to_owned(),
5152        };
5153        let err = parse_header_edit(0, &entry).expect_err("empty key must be rejected");
5154        assert!(matches!(
5155            err,
5156            ConfigError::InvalidHeaderBytes { field: "key", .. }
5157        ));
5158    }
5159
5160    #[test]
5161    fn parse_header_edit_accepts_clean_value() {
5162        let entry = HeaderEditConfig {
5163            position: "request".to_owned(),
5164            key: "X-Tenant".to_owned(),
5165            value: "alpha".to_owned(),
5166        };
5167        let header = parse_header_edit(0, &entry).expect("clean value must be accepted");
5168        assert_eq!(header.key, "X-Tenant");
5169        assert_eq!(header.val, "alpha");
5170    }
5171
5172    /// A bare string with no scheme prefix is the inline literal body.
5173    /// This is the common case — short canned responses inline in TOML
5174    /// or a `--answer` flag, no disk I/O.
5175    #[test]
5176    fn resolve_answer_source_bare_string_is_literal() {
5177        let body = resolve_answer_source("HTTP/1.1 503 Service Unavailable\r\n\r\nbusy")
5178            .expect("bare-string source must resolve");
5179        assert_eq!(body, "HTTP/1.1 503 Service Unavailable\r\n\r\nbusy");
5180    }
5181
5182    #[test]
5183    fn resolve_answer_source_empty_string_is_legitimate() {
5184        let body = resolve_answer_source("").expect("empty source must resolve");
5185        assert_eq!(body, "");
5186    }
5187
5188    /// `file://` opts into reading the path off disk. A non-existent
5189    /// path bubbles up as `ConfigError::FileOpen` so the operator gets
5190    /// the same diagnostics as the existing per-status `answer_NNN`
5191    /// flow.
5192    #[test]
5193    fn resolve_answer_source_file_scheme_missing_file_errors() {
5194        let err = resolve_answer_source("file:///nonexistent/sozu-test/never.http")
5195            .expect_err("missing path must error");
5196        assert!(matches!(err, ConfigError::FileOpen { .. }));
5197    }
5198
5199    /// `file://` strips the scheme; an empty path after the scheme is
5200    /// rejected (empty path on filesystem read).
5201    #[test]
5202    fn resolve_answer_source_file_scheme_empty_path_errors() {
5203        let err = resolve_answer_source("file://").expect_err("empty path must error");
5204        assert!(matches!(err, ConfigError::FileOpen { .. }));
5205    }
5206
5207    // ── TCP SNI/ALPN frontends (sozu-proxy/sozu#1279) ───────────────────────
5208
5209    /// A legacy-style TCP frontend TOML (no `hostname`/`alpn` keys at all)
5210    /// must still parse, and the built frontend/listener must carry the
5211    /// same values as before this feature existed: `sni = None`,
5212    /// `alpn = []`, and the proto default SNI-preread knobs (unused since
5213    /// no SNI frontend targets the listener).
5214    #[test]
5215    fn legacy_tcp_frontend_toml_without_sni_alpn_parses_unchanged() {
5216        let toml_content = r#"
5217            command_socket = "/tmp/sozu_test.sock"
5218            worker_count = 1
5219
5220            [[listeners]]
5221            protocol = "tcp"
5222            address  = "127.0.0.1:9000"
5223
5224            [clusters.legacy]
5225            protocol       = "tcp"
5226            load_balancing = "ROUND_ROBIN"
5227            frontends = [
5228              { address = "127.0.0.1:9000" }
5229            ]
5230            backends = [
5231              { address = "10.0.0.1:9000" }
5232            ]
5233        "#;
5234        let file_config: FileConfig =
5235            toml::from_str(toml_content).expect("Could not parse legacy TCP TOML");
5236        let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
5237            .into_config()
5238            .expect("legacy TCP config without sni/alpn must load unchanged");
5239
5240        assert_eq!(config.tcp_listeners.len(), 1);
5241        let listener = &config.tcp_listeners[0];
5242        assert_eq!(
5243            listener.sni_preread_timeout,
5244            Some(DEFAULT_SNI_PREREAD_TIMEOUT),
5245            "proto default sni_preread_timeout must be populated even though unused"
5246        );
5247        assert_eq!(
5248            listener.sni_preread_max_bytes,
5249            Some(DEFAULT_SNI_PREREAD_MAX_BYTES),
5250            "proto default sni_preread_max_bytes must be populated even though unused"
5251        );
5252
5253        let messages = config
5254            .generate_config_messages()
5255            .expect("Could not generate config messages");
5256        let tcp_frontend = messages
5257            .iter()
5258            .find_map(|m| match &m.content.request_type {
5259                Some(RequestType::AddTcpFrontend(f)) => Some(f),
5260                _ => None,
5261            })
5262            .expect("AddTcpFrontend must be present");
5263        assert_eq!(tcp_frontend.sni, None, "legacy frontend must carry no sni");
5264        assert!(
5265            tcp_frontend.alpn.is_empty(),
5266            "legacy frontend must carry no alpn"
5267        );
5268    }
5269
5270    /// `hostname` on a TCP frontend maps to the wire `sni` field, exact
5271    /// hostnames and a single leading `*.` wildcard are both accepted.
5272    #[test]
5273    fn tcp_frontend_hostname_maps_to_sni_exact_and_wildcard() {
5274        let toml_content = r#"
5275            command_socket = "/tmp/sozu_test.sock"
5276            worker_count = 1
5277
5278            [[listeners]]
5279            protocol = "tcp"
5280            address  = "127.0.0.1:9010"
5281
5282            [clusters.exact]
5283            protocol       = "tcp"
5284            load_balancing = "ROUND_ROBIN"
5285            frontends = [
5286              { address = "127.0.0.1:9010", hostname = "example.com", alpn = ["h2"] }
5287            ]
5288            backends = [ { address = "10.0.0.1:9010" } ]
5289
5290            [clusters.wildcard]
5291            protocol       = "tcp"
5292            load_balancing = "ROUND_ROBIN"
5293            frontends = [
5294              { address = "127.0.0.1:9010", hostname = "*.example.com" }
5295            ]
5296            backends = [ { address = "10.0.0.2:9010" } ]
5297        "#;
5298        let file_config: FileConfig =
5299            toml::from_str(toml_content).expect("Could not parse TOML config");
5300        let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
5301            .into_config()
5302            .expect("exact + wildcard SNI frontends on distinct sni must load");
5303
5304        let messages = config
5305            .generate_config_messages()
5306            .expect("Could not generate config messages");
5307        let mut frontends: Vec<_> = messages
5308            .iter()
5309            .filter_map(|m| match &m.content.request_type {
5310                Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
5311                _ => None,
5312            })
5313            .collect();
5314        frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
5315
5316        assert_eq!(frontends.len(), 2);
5317        assert_eq!(frontends[0].sni, Some("example.com".to_string()));
5318        assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
5319        assert_eq!(frontends[1].sni, Some("*.example.com".to_string()));
5320        assert!(frontends[1].alpn.is_empty());
5321    }
5322
5323    /// (a) An SNI pattern with more than one wildcard label, an embedded
5324    /// `*`, an empty label, or any `/` (a leftmost `/.../` label would be
5325    /// inserted into the `pattern_trie` route table as a REGEX segment,
5326    /// silently widening routing) is rejected at config-load.
5327    #[test]
5328    fn tcp_frontend_invalid_sni_pattern_rejected() {
5329        for invalid in [
5330            "*.*.example.com",
5331            "foo.*.com",
5332            "*",
5333            "example..com",
5334            "",
5335            "/[a-z]+/.example.com",
5336            "foo/bar.example.com",
5337        ] {
5338            let frontend = FileClusterFrontendConfig {
5339                address: "127.0.0.1:8080".parse().unwrap(),
5340                hostname: Some(invalid.to_string()),
5341                alpn: vec![],
5342                path: None,
5343                path_type: None,
5344                method: None,
5345                certificate: None,
5346                key: None,
5347                certificate_chain: None,
5348                tls_versions: vec![],
5349                position: RulePosition::Tree,
5350                tags: None,
5351                redirect: None,
5352                redirect_scheme: None,
5353                redirect_template: None,
5354                rewrite_host: None,
5355                rewrite_path: None,
5356                rewrite_port: None,
5357                required_auth: None,
5358                headers: None,
5359                hsts: None,
5360            };
5361            match frontend.to_tcp_front() {
5362                Err(ConfigError::InvalidSniPattern { sni }) => assert_eq!(sni, invalid),
5363                other => panic!("expected InvalidSniPattern for {invalid:?}, got {other:?}"),
5364            }
5365        }
5366    }
5367
5368    /// (a) A non-ASCII SNI pattern is rejected loudly: on-wire SNI is
5369    /// always an ASCII A-label (RFC 6066 / IDNA), so a Unicode U-label in
5370    /// the config would load fine but never match a ClientHello — a
5371    /// silent routing failure. The error names the punycode form the
5372    /// operator must write instead.
5373    #[test]
5374    fn tcp_frontend_non_ascii_sni_pattern_rejected() {
5375        for non_ascii in ["münchen.example", "*.bücher.example", "日本.example"] {
5376            let frontend = FileClusterFrontendConfig {
5377                address: "127.0.0.1:8080".parse().unwrap(),
5378                hostname: Some(non_ascii.to_string()),
5379                alpn: vec![],
5380                path: None,
5381                path_type: None,
5382                method: None,
5383                certificate: None,
5384                key: None,
5385                certificate_chain: None,
5386                tls_versions: vec![],
5387                position: RulePosition::Tree,
5388                tags: None,
5389                redirect: None,
5390                redirect_scheme: None,
5391                redirect_template: None,
5392                rewrite_host: None,
5393                rewrite_path: None,
5394                rewrite_port: None,
5395                required_auth: None,
5396                headers: None,
5397                hsts: None,
5398            };
5399            match frontend.to_tcp_front() {
5400                Err(ConfigError::NonAsciiSniPattern { sni }) => assert_eq!(sni, non_ascii),
5401                other => panic!("expected NonAsciiSniPattern for {non_ascii:?}, got {other:?}"),
5402            }
5403        }
5404    }
5405
5406    /// The punycode A-label form of an internationalized hostname — what
5407    /// the NonAsciiSniPattern error tells the operator to write — is
5408    /// accepted, both exact and wildcarded, and case-normalized like any
5409    /// other ASCII pattern.
5410    #[test]
5411    fn tcp_frontend_punycode_sni_pattern_accepted() {
5412        assert_eq!(
5413            validate_sni_pattern("xn--mnchen-3ya.example").expect("A-label must be accepted"),
5414            "xn--mnchen-3ya.example"
5415        );
5416        assert_eq!(
5417            validate_sni_pattern("*.xn--bcher-kva.example")
5418                .expect("wildcarded A-label must be accepted"),
5419            "*.xn--bcher-kva.example"
5420        );
5421        assert_eq!(
5422            validate_sni_pattern("XN--MNCHEN-3YA.Example")
5423                .expect("mixed-case A-label must be accepted"),
5424            "xn--mnchen-3ya.example",
5425            "A-label patterns are ASCII-lowercased like any other pattern"
5426        );
5427    }
5428
5429    /// `alpn` is a TCP-only concept; setting it on an HTTP frontend is
5430    /// rejected rather than silently ignored.
5431    #[test]
5432    fn alpn_rejected_on_http_frontend() {
5433        let frontend = FileClusterFrontendConfig {
5434            address: "127.0.0.1:8080".parse().unwrap(),
5435            hostname: Some("example.com".to_owned()),
5436            alpn: vec!["h2".to_string()],
5437            path: None,
5438            path_type: None,
5439            method: None,
5440            certificate: None,
5441            key: None,
5442            certificate_chain: None,
5443            tls_versions: vec![],
5444            position: RulePosition::Tree,
5445            tags: None,
5446            redirect: None,
5447            redirect_scheme: None,
5448            redirect_template: None,
5449            rewrite_host: None,
5450            rewrite_path: None,
5451            rewrite_port: None,
5452            required_auth: None,
5453            headers: None,
5454            hsts: None,
5455        };
5456        match frontend.to_http_front("api") {
5457            Err(ConfigError::InvalidFrontendConfig(field)) => assert_eq!(field, "alpn"),
5458            other => panic!("expected InvalidFrontendConfig(\"alpn\"), got {other:?}"),
5459        }
5460    }
5461
5462    /// A TCP frontend that sets `alpn` but leaves `hostname` (the wire
5463    /// `sni` field) unset is a config error: an ALPN matcher only ever gets
5464    /// consulted from within the SNI-scoped preread route table, so a
5465    /// no-SNI frontend would install the raw catch-all path and silently
5466    /// never enforce the configured protocol list.
5467    #[test]
5468    fn tcp_frontend_alpn_without_sni_rejected() {
5469        let toml_content = r#"
5470            command_socket = "/tmp/sozu_test.sock"
5471            worker_count = 1
5472
5473            [[listeners]]
5474            protocol = "tcp"
5475            address  = "127.0.0.1:9019"
5476
5477            [clusters.a]
5478            protocol       = "tcp"
5479            load_balancing = "ROUND_ROBIN"
5480            frontends = [
5481              { address = "127.0.0.1:9019", alpn = ["h2"] }
5482            ]
5483            backends = [ { address = "10.0.0.1:9019" } ]
5484        "#;
5485        let file_config: FileConfig =
5486            toml::from_str(toml_content).expect("Could not parse TOML config");
5487        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5488        match result {
5489            Err(ConfigError::AlpnWithoutSni { address }) => {
5490                assert_eq!(address.to_string(), "127.0.0.1:9019");
5491            }
5492            other => panic!("expected AlpnWithoutSni, got {other:?}"),
5493        }
5494    }
5495
5496    /// (b) Two TCP frontends on the same (address, sni) advertising an
5497    /// overlapping ALPN protocol is a config error — routing on that
5498    /// listener would otherwise depend on iteration order.
5499    #[test]
5500    fn tcp_frontend_alpn_overlap_rejected() {
5501        let toml_content = r#"
5502            command_socket = "/tmp/sozu_test.sock"
5503            worker_count = 1
5504
5505            [[listeners]]
5506            protocol = "tcp"
5507            address  = "127.0.0.1:9020"
5508
5509            [clusters.a]
5510            protocol       = "tcp"
5511            load_balancing = "ROUND_ROBIN"
5512            frontends = [
5513              { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2"] }
5514            ]
5515            backends = [ { address = "10.0.0.1:9020" } ]
5516
5517            [clusters.b]
5518            protocol       = "tcp"
5519            load_balancing = "ROUND_ROBIN"
5520            frontends = [
5521              { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2", "http/1.1"] }
5522            ]
5523            backends = [ { address = "10.0.0.2:9020" } ]
5524        "#;
5525        let file_config: FileConfig =
5526            toml::from_str(toml_content).expect("Could not parse TOML config");
5527        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5528        match result {
5529            Err(ConfigError::TcpFrontendAlpnOverlap { protocol, .. }) => {
5530                assert_eq!(protocol, "h2");
5531            }
5532            other => panic!("expected TcpFrontendAlpnOverlap, got {other:?}"),
5533        }
5534    }
5535
5536    /// (b) At most one TCP frontend per (address, sni) may leave `alpn`
5537    /// empty (the catch-all match); a second is as ambiguous as an
5538    /// overlapping explicit protocol.
5539    #[test]
5540    fn tcp_frontend_multiple_alpn_catch_all_rejected() {
5541        let toml_content = r#"
5542            command_socket = "/tmp/sozu_test.sock"
5543            worker_count = 1
5544
5545            [[listeners]]
5546            protocol = "tcp"
5547            address  = "127.0.0.1:9021"
5548
5549            [clusters.a]
5550            protocol       = "tcp"
5551            load_balancing = "ROUND_ROBIN"
5552            frontends = [
5553              { address = "127.0.0.1:9021", hostname = "example.com" }
5554            ]
5555            backends = [ { address = "10.0.0.1:9021" } ]
5556
5557            [clusters.b]
5558            protocol       = "tcp"
5559            load_balancing = "ROUND_ROBIN"
5560            frontends = [
5561              { address = "127.0.0.1:9021", hostname = "example.com" }
5562            ]
5563            backends = [ { address = "10.0.0.2:9021" } ]
5564        "#;
5565        let file_config: FileConfig =
5566            toml::from_str(toml_content).expect("Could not parse TOML config");
5567        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5568        assert!(
5569            matches!(
5570                result,
5571                Err(ConfigError::TcpFrontendMultipleAlpnCatchAll { .. })
5572            ),
5573            "expected TcpFrontendMultipleAlpnCatchAll, got {result:?}"
5574        );
5575    }
5576
5577    /// (c) A listener targeted by both a no-SNI frontend and an SNI-scoped
5578    /// frontend is a config error: an SNI-enabled listener must not also
5579    /// carry a raw-TCP fallback.
5580    #[test]
5581    fn tcp_listener_mixes_sni_and_no_sni_rejected() {
5582        let toml_content = r#"
5583            command_socket = "/tmp/sozu_test.sock"
5584            worker_count = 1
5585
5586            [[listeners]]
5587            protocol = "tcp"
5588            address  = "127.0.0.1:9022"
5589
5590            [clusters.a]
5591            protocol       = "tcp"
5592            load_balancing = "ROUND_ROBIN"
5593            frontends = [
5594              { address = "127.0.0.1:9022", hostname = "example.com" }
5595            ]
5596            backends = [ { address = "10.0.0.1:9022" } ]
5597
5598            [clusters.b]
5599            protocol       = "tcp"
5600            load_balancing = "ROUND_ROBIN"
5601            frontends = [
5602              { address = "127.0.0.1:9022" }
5603            ]
5604            backends = [ { address = "10.0.0.2:9022" } ]
5605        "#;
5606        let file_config: FileConfig =
5607            toml::from_str(toml_content).expect("Could not parse TOML config");
5608        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5609        assert!(
5610            matches!(result, Err(ConfigError::TcpListenerMixesSniAndNoSni { .. })),
5611            "expected TcpListenerMixesSniAndNoSni, got {result:?}"
5612        );
5613    }
5614
5615    /// (d) `sni_preread_timeout` (proto default: 5s) exceeding the
5616    /// listener's `front_timeout` is rejected — but only when an SNI
5617    /// frontend actually targets that listener, so legacy TCP-only
5618    /// configs with a low front_timeout keep loading unchanged.
5619    #[test]
5620    fn sni_preread_timeout_exceeding_front_timeout_rejected() {
5621        let toml_content = r#"
5622            command_socket = "/tmp/sozu_test.sock"
5623            worker_count = 1
5624
5625            [[listeners]]
5626            protocol      = "tcp"
5627            address       = "127.0.0.1:9030"
5628            front_timeout = 2
5629
5630            [clusters.a]
5631            protocol       = "tcp"
5632            load_balancing = "ROUND_ROBIN"
5633            frontends = [
5634              { address = "127.0.0.1:9030", hostname = "example.com" }
5635            ]
5636            backends = [ { address = "10.0.0.1:9030" } ]
5637        "#;
5638        let file_config: FileConfig =
5639            toml::from_str(toml_content).expect("Could not parse TOML config");
5640        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5641        match result {
5642            Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
5643                sni_preread_timeout: 5,
5644                front_timeout: 2,
5645                ..
5646            }) => {}
5647            other => panic!("expected SniPrereadTimeoutExceedsFrontTimeout, got {other:?}"),
5648        }
5649    }
5650
5651    /// (e) `sni_preread_max_bytes` (proto default: 16384) exceeding the
5652    /// global `buffer_size` is rejected — again only when an SNI frontend
5653    /// targets the listener.
5654    #[test]
5655    fn sni_preread_max_bytes_exceeding_buffer_size_rejected() {
5656        let toml_content = r#"
5657            command_socket = "/tmp/sozu_test.sock"
5658            worker_count = 1
5659            buffer_size = 8192
5660
5661            [[listeners]]
5662            protocol = "tcp"
5663            address  = "127.0.0.1:9031"
5664
5665            [clusters.a]
5666            protocol       = "tcp"
5667            load_balancing = "ROUND_ROBIN"
5668            frontends = [
5669              { address = "127.0.0.1:9031", hostname = "example.com" }
5670            ]
5671            backends = [ { address = "10.0.0.1:9031" } ]
5672        "#;
5673        let file_config: FileConfig =
5674            toml::from_str(toml_content).expect("Could not parse TOML config");
5675        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5676        match result {
5677            Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
5678                sni_preread_max_bytes: 16384,
5679                buffer_size: 8192,
5680                ..
5681            }) => {}
5682            other => panic!("expected SniPrereadMaxBytesExceedsBufferSize, got {other:?}"),
5683        }
5684    }
5685
5686    /// `sni_preread_max_bytes = 0` on an SNI-enabled listener is rejected:
5687    /// the preread shell would issue zero-length reads that never make
5688    /// progress, spinning until the event-loop iteration guard trips
5689    /// instead of ever reaching a routing decision.
5690    #[test]
5691    fn sni_preread_max_bytes_zero_rejected() {
5692        let toml_content = r#"
5693            command_socket = "/tmp/sozu_test.sock"
5694            worker_count = 1
5695
5696            [[listeners]]
5697            protocol             = "tcp"
5698            address              = "127.0.0.1:9033"
5699            sni_preread_max_bytes = 0
5700
5701            [clusters.a]
5702            protocol       = "tcp"
5703            load_balancing = "ROUND_ROBIN"
5704            frontends = [
5705              { address = "127.0.0.1:9033", hostname = "example.com" }
5706            ]
5707            backends = [ { address = "10.0.0.1:9033" } ]
5708        "#;
5709        let file_config: FileConfig =
5710            toml::from_str(toml_content).expect("Could not parse TOML config");
5711        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5712        match result {
5713            Err(ConfigError::SniPrereadMaxBytesTooSmall {
5714                sni_preread_max_bytes: 0,
5715                minimum: 5,
5716                ..
5717            }) => {}
5718            other => panic!("expected SniPrereadMaxBytesTooSmall, got {other:?}"),
5719        }
5720    }
5721
5722    /// The floor itself (`MIN_SNI_PREREAD_MAX_BYTES` = 5 bytes) must load
5723    /// successfully — only values strictly below it are rejected.
5724    #[test]
5725    fn sni_preread_max_bytes_at_the_floor_loads() {
5726        let toml_content = r#"
5727            command_socket = "/tmp/sozu_test.sock"
5728            worker_count = 1
5729
5730            [[listeners]]
5731            protocol             = "tcp"
5732            address              = "127.0.0.1:9034"
5733            sni_preread_max_bytes = 5
5734
5735            [clusters.a]
5736            protocol       = "tcp"
5737            load_balancing = "ROUND_ROBIN"
5738            frontends = [
5739              { address = "127.0.0.1:9034", hostname = "example.com" }
5740            ]
5741            backends = [ { address = "10.0.0.1:9034" } ]
5742        "#;
5743        let file_config: FileConfig =
5744            toml::from_str(toml_content).expect("Could not parse TOML config");
5745        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5746        assert!(
5747            result.is_ok(),
5748            "sni_preread_max_bytes at the exact floor must load: {result:?}"
5749        );
5750    }
5751
5752    /// Gating proof for (d)/(e): a TCP listener with a low front_timeout
5753    /// and small buffer_size, but *no* SNI frontend, must load unchanged —
5754    /// the new knobs are dead weight on a listener that never prereads.
5755    #[test]
5756    fn sni_preread_validation_ignored_without_sni_frontend() {
5757        let toml_content = r#"
5758            command_socket = "/tmp/sozu_test.sock"
5759            worker_count = 1
5760            buffer_size = 8192
5761
5762            [[listeners]]
5763            protocol      = "tcp"
5764            address       = "127.0.0.1:9032"
5765            front_timeout = 2
5766
5767            [clusters.a]
5768            protocol       = "tcp"
5769            load_balancing = "ROUND_ROBIN"
5770            frontends = [
5771              { address = "127.0.0.1:9032" }
5772            ]
5773            backends = [ { address = "10.0.0.1:9032" } ]
5774        "#;
5775        let file_config: FileConfig =
5776            toml::from_str(toml_content).expect("Could not parse TOML config");
5777        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5778        assert!(
5779            result.is_ok(),
5780            "a no-SNI TCP listener must ignore sni_preread validation entirely: {result:?}"
5781        );
5782    }
5783
5784    /// The actual use case sozu-proxy/sozu#1279 exists for: two TCP
5785    /// frontends sharing the same `(address, sni)` with disjoint,
5786    /// non-empty `alpn` lists must load successfully and both must be
5787    /// individually reachable — ALPN multiplexing within one SNI. The
5788    /// negative tests above only prove overlap is rejected; this proves
5789    /// non-overlap actually works.
5790    #[test]
5791    fn tcp_frontend_disjoint_alpn_same_sni_both_load() {
5792        let toml_content = r#"
5793            command_socket = "/tmp/sozu_test.sock"
5794            worker_count = 1
5795
5796            [[listeners]]
5797            protocol = "tcp"
5798            address  = "127.0.0.1:9040"
5799
5800            [clusters.h2_cluster]
5801            protocol       = "tcp"
5802            load_balancing = "ROUND_ROBIN"
5803            frontends = [
5804              { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["h2"] }
5805            ]
5806            backends = [ { address = "10.0.0.1:9040" } ]
5807
5808            [clusters.http11_cluster]
5809            protocol       = "tcp"
5810            load_balancing = "ROUND_ROBIN"
5811            frontends = [
5812              { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["http/1.1"] }
5813            ]
5814            backends = [ { address = "10.0.0.2:9040" } ]
5815        "#;
5816        let file_config: FileConfig =
5817            toml::from_str(toml_content).expect("Could not parse TOML config");
5818        let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
5819            .into_config()
5820            .expect("disjoint non-empty ALPN lists on the same (address, sni) must load");
5821
5822        let messages = config
5823            .generate_config_messages()
5824            .expect("Could not generate config messages");
5825        let mut frontends: Vec<_> = messages
5826            .iter()
5827            .filter_map(|m| match &m.content.request_type {
5828                Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
5829                _ => None,
5830            })
5831            .collect();
5832        frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
5833
5834        assert_eq!(frontends.len(), 2, "both frontends must be emitted");
5835        assert_eq!(frontends[0].cluster_id, "h2_cluster");
5836        assert_eq!(frontends[0].sni, Some("example.com".to_string()));
5837        assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
5838        assert_eq!(frontends[1].cluster_id, "http11_cluster");
5839        assert_eq!(frontends[1].sni, Some("example.com".to_string()));
5840        assert_eq!(frontends[1].alpn, vec!["http/1.1".to_string()]);
5841    }
5842
5843    /// A `protocol = "tcp"` cluster frontend resolved against a
5844    /// `protocol = "udp"` listener (`TcpFrontendConfig.udp = true`, set in
5845    /// `populate_clusters`) emits `AddUdpFrontend`, not `AddTcpFrontend` —
5846    /// it carries no `sni`/`alpn` on the wire and must not participate in
5847    /// the TCP-only mixing-ban / ALPN-overlap invariants. Two "tcp"
5848    /// clusters at the same UDP-listener address, one with `hostname` set
5849    /// and one without, must load successfully rather than spuriously
5850    /// tripping `TcpListenerMixesSniAndNoSni`.
5851    #[test]
5852    fn udp_routed_tcp_frontends_are_excluded_from_sni_invariants() {
5853        let toml_content = r#"
5854            command_socket = "/tmp/sozu_test.sock"
5855            worker_count = 1
5856
5857            [[listeners]]
5858            protocol = "udp"
5859            address  = "127.0.0.1:9050"
5860
5861            [clusters.a]
5862            protocol       = "tcp"
5863            load_balancing = "ROUND_ROBIN"
5864            frontends = [
5865              { address = "127.0.0.1:9050", hostname = "example.com" }
5866            ]
5867            backends = [ { address = "10.0.0.1:9050" } ]
5868
5869            [clusters.b]
5870            protocol       = "tcp"
5871            load_balancing = "ROUND_ROBIN"
5872            frontends = [
5873              { address = "127.0.0.1:9050" }
5874            ]
5875            backends = [ { address = "10.0.0.2:9050" } ]
5876        "#;
5877        let file_config: FileConfig =
5878            toml::from_str(toml_content).expect("Could not parse TOML config");
5879        let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5880        assert!(
5881            result.is_ok(),
5882            "UDP-routed tcp-cluster frontends must not trip the SNI mixing-ban: {result:?}"
5883        );
5884
5885        let config = result.expect("checked is_ok above");
5886        let messages = config
5887            .generate_config_messages()
5888            .expect("Could not generate config messages");
5889        let add_udp_frontend_count = messages
5890            .iter()
5891            .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
5892            .count();
5893        assert_eq!(
5894            add_udp_frontend_count, 2,
5895            "both cluster frontends on the udp listener must emit AddUdpFrontend"
5896        );
5897    }
5898}