use anyhow::{ensure, Context as _, Result}; use http::{HeaderMap, HeaderValue}; use serde::{Deserialize, Serialize}; use std::{net::SocketAddr, time::Duration}; #[derive(Debug, Clone, Copy, Default)] pub struct StartupLocks { pub http: Option, pub https: Option, pub administration: Option, } impl StartupLocks { pub fn ensure_config(&self, config: &mut crate::config::Config) { if self.administration.is_some() { config.administration.listen = None; } if let Some(address) = self.http { ensure_locked_listener(config, address, false); } if let Some(address) = self.https { ensure_locked_listener(config, address, true); } } pub fn validate( &self, config: &crate::config::Config, startup_admin: SocketAddr, ) -> Result<()> { if let Some(required) = self.http { ensure!( config.listeners.endpoints.iter().any(|endpoint| { endpoint.enabled && endpoint.bindings().iter().any(|binding| binding.address == required && !binding.tls) }), "HTTP listener {required} is fixed by systemd/startup and cannot be removed, disabled, or changed" ); } if let Some(required) = self.https { ensure!( config.listeners.endpoints.iter().any(|endpoint| { endpoint.enabled && endpoint.bindings().iter().any(|binding| binding.address == required && binding.tls) }), "HTTPS listener {required} is fixed by systemd/startup and cannot be removed, disabled, or changed" ); } if let Some(required) = self.administration { ensure!( config.administration.listen.is_none() && config.administration.resolved_listen(startup_admin) == required, "administration listener {required} is fixed by systemd/startup and cannot be changed" ); } Ok(()) } } fn ensure_locked_listener(config: &mut crate::config::Config, address: SocketAddr, tls: bool) { if let Some(endpoint) = config.listeners.endpoints.iter_mut().find(|endpoint| { endpoint .bindings() .iter() .any(|binding| binding.address == address && binding.tls == tls) }) { endpoint.enabled = true; return; } let canonical_id = if tls { "https" } else { "http" }; if let Some(endpoint) = config .listeners .endpoints .iter_mut() .find(|endpoint| endpoint.id == canonical_id && endpoint.binding.tls == tls) { endpoint.binding.address = address; endpoint.additional_addresses.clear(); endpoint.enabled = true; return; } let base = if tls { "systemd-https" } else { "systemd-http" }; let mut id = base.to_owned(); let mut suffix = 2usize; while config .listeners .endpoints .iter() .any(|endpoint| endpoint.id == id) { id = format!("{base}-{suffix}"); suffix += 1; } config.listeners.endpoints.push(NamedListener { id, name: if tls { "HTTPS (startup locked)".into() } else { "HTTP (startup locked)".into() }, scope: if address.ip().is_loopback() { ListenerScope::Internal } else { ListenerScope::Public }, enabled: true, binding: crate::vhost::Listener { address, tls, http2: tls, http2_settings: tls.then(Http2Connections::default), h2c: false, http3: tls.then(crate::http3::Settings::default), }, additional_addresses: Vec::new(), }); } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(default, deny_unknown_fields)] pub struct Http2Connections { pub idle_timeout_seconds: u64, pub max_connection_lifetime_seconds: u64, pub drain_timeout_seconds: u64, } impl Default for Http2Connections { fn default() -> Self { Self { idle_timeout_seconds: 300, max_connection_lifetime_seconds: 0, drain_timeout_seconds: 10, } } } impl Http2Connections { pub fn validate(&self) -> Result<()> { ensure!( (1..=86_400).contains(&self.idle_timeout_seconds), "HTTP/2 idle_timeout_seconds must be 1..86400" ); ensure!( self.max_connection_lifetime_seconds == 0 || (1..=2_592_000).contains(&self.max_connection_lifetime_seconds), "HTTP/2 max_connection_lifetime_seconds must be 0..2592000 (0 disables the limit)" ); ensure!( (1..=3_600).contains(&self.drain_timeout_seconds), "HTTP/2 drain_timeout_seconds must be 1..3600" ); Ok(()) } pub fn idle_timeout(self) -> Duration { Duration::from_secs(self.idle_timeout_seconds) } pub fn max_connection_lifetime(self) -> Option { (self.max_connection_lifetime_seconds > 0) .then(|| Duration::from_secs(self.max_connection_lifetime_seconds)) } pub fn drain_timeout(self) -> Duration { Duration::from_secs(self.drain_timeout_seconds) } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(default, deny_unknown_fields)] pub struct PanelCompression { pub zstd: bool, pub br: bool, pub gzip: bool, } impl Default for PanelCompression { fn default() -> Self { Self { zstd: true, br: true, gzip: true, } } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[serde(default, deny_unknown_fields)] pub struct Administration { pub listen: Option, pub tls: bool, pub certificate_id: Option, pub allowed_hosts: Vec, pub public_origin: Option, pub session_days: u16, pub panel_compression: PanelCompression, } impl Default for Administration { fn default() -> Self { Self { listen: None, tls: false, certificate_id: None, allowed_hosts: Vec::new(), public_origin: None, session_days: 30, panel_compression: PanelCompression::default(), } } } impl Administration { pub fn resolved_listen(&self, startup: SocketAddr) -> SocketAddr { self.listen.unwrap_or(startup) } pub fn session_seconds(&self) -> i64 { i64::from(self.session_days) * 24 * 60 * 60 } pub fn restart_differs(&self, active: &Self) -> bool { self.listen != active.listen || self.tls != active.tls || self.certificate_id != active.certificate_id || self.allowed_hosts != active.allowed_hosts || self.public_origin != active.public_origin } pub fn validate(&mut self, tls: &crate::tls::Settings) -> Result<()> { if let Some(addr) = self.listen { ensure!( addr.port() > 0, "administration listener port must be nonzero" ); ensure!( !addr.ip().is_multicast(), "administration listener must use a unicast/wildcard IP" ); } if let Some(id) = &self.certificate_id { ensure!( self.tls, "administration certificate selection requires administration TLS" ); ensure!( tls.certificates .iter() .any(|certificate| &certificate.id == id), "administration certificate references an unknown certificate id" ); } ensure!( (7..=30).contains(&self.session_days), "administration session_days must be 7..30" ); let mut seen = std::collections::HashSet::new(); for host in &mut self.allowed_hosts { if host.parse::().is_err() { *host = crate::config::normalize_host(host)?; } ensure!( seen.insert(host.clone()), "duplicate administration hostname" ); } if let Some(public_origin) = self.public_origin.as_mut() { *public_origin = normalize_public_admin_origin(public_origin)?; } if let Some(host) = self.public_origin_host().map(str::to_owned) { self.allowed_hosts .retain(|allowed| !allowed.eq_ignore_ascii_case(&host)); } Ok(()) } } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct Listeners { pub endpoints: Vec, } impl Listeners { pub fn from_startup(http: SocketAddr, https: Option) -> Self { let mut endpoints = vec![NamedListener { id: "http".into(), name: "HTTP".into(), scope: ListenerScope::Internal, enabled: true, binding: crate::vhost::Listener { address: http, tls: false, http2: false, http2_settings: None, h2c: false, http3: None, }, additional_addresses: Vec::new(), }]; if let Some(address) = https { endpoints.push(NamedListener { id: "https".into(), name: "HTTPS".into(), scope: ListenerScope::Internal, enabled: true, binding: crate::vhost::Listener { address, tls: true, http2: true, http2_settings: Some(Http2Connections::default()), h2c: false, http3: Some(crate::http3::Settings::default()), }, additional_addresses: Vec::new(), }); } Self { endpoints } } pub fn validate(&self, policy: &crate::config::ValidationPolicy) -> Result<()> { ensure!(self.endpoints.len() <= 64, "at most 64 named listeners"); let mut ids = std::collections::HashSet::new(); for endpoint in &self.endpoints { ensure!( crate::config::valid_id(&endpoint.id) && ids.insert(&endpoint.id), "invalid/duplicate listener id" ); ensure!( endpoint.name.len() <= 96 && !endpoint.name.chars().any(char::is_control), "listener name must be at most 96 bytes without controls" ); ensure!( endpoint.additional_addresses.len() <= 15, "listener allows at most 16 bind addresses" ); let bindings = endpoint.bindings(); let mut addresses = std::collections::HashSet::new(); for binding in &bindings { crate::vhost::validate_listener(*binding)?; ensure!( binding.address.port() != policy.admin_addr.port(), "listener cannot share administration port" ); ensure!( addresses.insert(binding.address), "duplicate address in named listener" ); } if bindings.len() > 1 { ensure!( bindings.iter().all(|binding| !binding.address.ip().is_unspecified()), "multi-address listeners require explicit IPs; do not combine 0.0.0.0/:: with selected addresses" ); } } Ok(()) } } #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ListenerScope { #[default] Internal, Public, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct NamedListener { pub id: String, #[serde(default)] pub name: String, #[serde(default)] pub scope: ListenerScope, #[serde(default = "enabled")] pub enabled: bool, pub binding: crate::vhost::Listener, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub additional_addresses: Vec, } impl NamedListener { pub fn bindings(&self) -> Vec { let mut out = Vec::with_capacity(1 + self.additional_addresses.len()); out.push(self.binding); out.extend(self.additional_addresses.iter().map(|address| { let mut binding = self.binding; binding.address = *address; binding })); out } } fn enabled() -> bool { true } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct DiagnosticHeaders { pub request_id: bool, } impl DiagnosticHeaders { pub fn apply(&self, headers: &mut HeaderMap, request_id: &str) { headers.remove("x-request-id"); if self.request_id { if let Ok(value) = request_id.parse() { headers.insert("x-request-id", value); } } } } #[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ServerMode { #[default] Preserve, Hide, Custom, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct ServerHeader { #[serde(default)] pub mode: ServerMode, #[serde(default = "legacy_server_header_value")] pub value: String, } fn legacy_server_header_value() -> String { format!("RustProxy/v{}", env!("CARGO_PKG_VERSION")) } impl ServerHeader { pub(crate) fn legacy_compat_default() -> Self { Self { mode: ServerMode::Preserve, value: legacy_server_header_value(), } } } impl Default for ServerHeader { fn default() -> Self { Self { mode: ServerMode::Custom, value: "RustProxy".into(), } } } impl ServerHeader { pub fn validate(&self) -> Result<()> { ensure!( self.value.len() <= 256, "Server header value exceeds 256 bytes" ); if matches!(self.mode, ServerMode::Custom) { ensure!( !self.value.trim().is_empty(), "custom Server header cannot be empty" ); self.value.parse::()?; } Ok(()) } pub fn apply(&self, headers: &mut HeaderMap) { match self.mode { ServerMode::Preserve => {} ServerMode::Hide => { headers.remove(::http::header::SERVER); } ServerMode::Custom => { if let Some(value) = cached_header_value(&self.value) { headers.insert(::http::header::SERVER, value); } } } } } pub(crate) fn cached_header_value(value: &str) -> Option { const CAPACITY: usize = 16; thread_local! { static CACHE: std::cell::RefCell, HeaderValue)>> = const { std::cell::RefCell::new(Vec::new()) }; } CACHE.with(|cache| { let mut cache = cache.borrow_mut(); if let Some((_, header)) = cache.iter().find(|(key, _)| key.as_ref() == value) { return Some(header.clone()); } let header = HeaderValue::from_maybe_shared(bytes::Bytes::copy_from_slice(value.as_bytes())).ok()?; if cache.len() >= CAPACITY { cache.remove(0); } cache.push((value.into(), header.clone())); Some(header) }) } include!("settings/public_origin.rs"); #[cfg(test)] include!("settings/tests.rs");