use crate::{ acl::{Acl, CompiledAcl}, headers::{CompiledHeaders, HeaderRules}, }; use anyhow::{ensure, Context, Result}; use http::{header::HeaderValue, uri::Authority}; use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, net::{IpAddr, Ipv4Addr, SocketAddr}, path::PathBuf, sync::Arc, }; use url::{Host, Url}; pub const MAX_CONFIG_BYTES: usize = 2_097_152; pub const MAX_BACKENDS: usize = 128; pub const MAX_ROUTES: usize = 256; fn legacy_server_header_default() -> crate::settings::ServerHeader { crate::settings::ServerHeader::legacy_compat_default() } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { #[serde(default)] pub administration: crate::settings::Administration, #[serde(default)] pub logging: crate::logging::Settings, #[serde(default)] pub error_log: crate::logging::ErrorLogSettings, #[serde(default)] pub prometheus: crate::prometheus::Settings, #[serde(default)] pub geoip_updates: crate::geoip::UpdateSettings, #[serde(default)] pub listeners: crate::settings::Listeners, #[serde(default)] pub http2: crate::settings::Http2Connections, #[serde(default)] pub docker_agents: crate::docker::Settings, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub streams: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub redirects: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub not_found_hosts: Vec, #[serde(default)] pub cache: crate::cache::Settings, #[serde(default)] pub transform: crate::transform::Settings, #[serde(default)] pub pages: crate::pages::GlobalPages, #[serde(default)] pub compression: crate::compression::Settings, #[serde(default = "legacy_server_header_default")] pub server_header: crate::settings::ServerHeader, #[serde(default)] pub diagnostic_headers: crate::settings::DiagnosticHeaders, #[serde(default)] pub forwarding: crate::forwarding::Settings, #[serde(default, skip_serializing_if = "crate::tls::Settings::is_empty")] pub tls: crate::tls::Settings, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub web_services: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub php_fpm_profiles: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub vhosts: Vec, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub host_policies: Vec, #[serde( default, rename = "user_agent_blocking", skip_serializing_if = "Option::is_none" )] pub legacy_user_agent_blocking: Option, #[serde(default)] pub global_security: GlobalSecurity, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub acl_profiles: Vec, #[serde(default, skip_serializing_if = "HeaderRules::is_empty")] pub headers: HeaderRules, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct GlobalSecurity { pub user_agent_blocking: Option, pub country_access: crate::rate_limit::CountryAccess, pub acl: Option, pub method_policy: crate::vhost::MethodPolicy, pub content_type_policy: crate::vhost::ContentTypePolicy, pub rate_limit: crate::rate_limit::Settings, pub security_headers: crate::security::SecurityHeaders, } #[derive(Debug, Clone)] pub struct CompiledGlobalSecurity { pub user_agent_blocking: crate::user_agent::CompiledPolicy, pub country_access: crate::rate_limit::CountryAccess, pub acl: Option, pub method_policy: crate::vhost::MethodPolicy, pub content_type_policy: crate::vhost::ContentTypePolicy, pub rate_limit: crate::rate_limit::Settings, pub headers: crate::security::CompiledSecurity, } impl Config { pub fn migrate_legacy_user_agent_blocking(&mut self) -> Result<()> { if let Some(legacy) = self.legacy_user_agent_blocking.take() { match &self.global_security.user_agent_blocking { Some(current) => ensure!( *current == legacy, "conflicting legacy user_agent_blocking and global_security.user_agent_blocking policies" ), None => self.global_security.user_agent_blocking = Some(legacy), } } Ok(()) } } impl GlobalSecurity { pub fn compile(&mut self) -> Result { let user_agent_blocking = self .user_agent_blocking .get_or_insert_with(Default::default) .compile()?; self.country_access.validate()?; self.method_policy.validate()?; self.content_type_policy.validate()?; self.rate_limit.validate()?; let acl = self.acl.as_ref().map(Acl::compile).transpose()?; let headers = self.security_headers.compile()?; Ok(CompiledGlobalSecurity { user_agent_blocking, country_access: self.country_access.clone(), acl, method_policy: self.method_policy.clone(), content_type_policy: self.content_type_policy.clone(), rate_limit: self.rate_limit.clone(), headers, }) } } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct HostPolicy { pub hosts: Vec, pub acl: Acl, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(default, deny_unknown_fields)] pub struct AclProfile { pub id: String, pub name: String, pub acl: Option, pub country_access: crate::rate_limit::CountryAccess, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Backend { #[serde(default)] pub protocol: UpstreamProtocol, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub address_allowlist: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] pub tls_ca_pem: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub tls_server_name: Option, #[serde(default)] pub tls_verify: bool, pub id: String, pub url: String, #[serde(default)] pub allow_private: bool, #[serde(default)] pub accept_local_listener_risk: bool, #[serde(default)] pub preserve_host: bool, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum UpstreamProtocol { #[default] Http1, Http2, Auto, H2, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Envelope { pub revision: u64, pub config: Config, } #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] pub struct Update { pub expected_revision: u64, pub config: Config, } #[derive(Debug, Clone)] pub struct ValidationPolicy { pub tls_addr: Option, pub cert_dir: Option, pub data_dir: Option, pub proxy_addr: SocketAddr, pub admin_addr: SocketAddr, } #[derive(Debug)] pub struct CompiledBackend { pub address_policy: crate::upstream::AddressPolicy, pub client: crate::upstream::ProxyClient, pub websocket_client: crate::upstream::ProxyClient, pub id: String, pub origin: String, pub request_origin: String, pub request_origin_uri: http::Uri, pub host_header: HeaderValue, pub preserve_host: bool, pub protocol: UpstreamProtocol, pub tls_verify: bool, pub accept_local_listener_risk: bool, pub targets_admin_listener: bool, pub health_probe: Option>, } #[derive(Debug, Clone)] pub enum CompiledSource { ReverseProxy, WebServer(Arc), } #[derive(Debug, Clone)] pub struct CompiledLocationRedirect { pub status: u16, pub target: String, pub rewrite_regex: Option, pub preserve_query: bool, } #[derive(Debug, Clone)] pub struct CompiledRoute { pub backend_id: Option, pub bindings: Vec, pub frontend: Option>, pub pool: Option>, pub cache: Option, pub name: String, pub id: String, pub internal: bool, pub path_prefix: String, pub match_type: crate::vhost::PathMatch, pub path_regex: Option, pub rewrite: crate::vhost::PathRewrite, pub response: Option, pub redirect: Option, pub error_fallback: Option, pub source: CompiledSource, pub backend: Option>, pub acl_profile_id: Option, pub profile_acl: Option, pub profile_country_access: crate::rate_limit::CountryAccess, pub acl: Option, pub country_access: crate::rate_limit::CountryAccess, pub method_policy: crate::vhost::MethodPolicy, pub headers: CompiledHeaders, pub unavailable: bool, } #[derive(Debug)] pub struct CompiledFrontend { pub websockets: bool, pub websocket_legacy_support: bool, pub websocket_rfc8441: bool, pub websocket_rfc9220: bool, pub force_ssl: bool, pub www_redirect: Option<(String, String)>, pub forward_client_ip: bool, pub https_redirect_port: Option, pub http2_enabled: bool, pub http3_enabled: bool, pub tls_policy: crate::tls::ProtocolPolicy, pub tls: Option>, pub tls_http1: Option>, pub cloudflare_client_ip: bool, pub timing_headers: bool, pub backend_header: bool, pub backend_timing_header: bool, pub rate_limit: crate::rate_limit::Settings, pub method_policy: crate::vhost::MethodPolicy, pub content_type_policy: crate::vhost::ContentTypePolicy, pub basic_auth: Option, pub metrics: Arc, pub server_header: crate::settings::ServerHeader, pub diagnostic_headers: crate::settings::DiagnosticHeaders, pub auth_provider: Option, pub id: String, pub bindings: Vec, pub acl_profile_id: Option, pub profile_acl: Option, pub profile_country_access: crate::rate_limit::CountryAccess, pub profile_policies: Vec, pub acl: Option, pub headers: CompiledHeaders, pub security: crate::security::CompiledSecurity, pub compression: crate::compression::Overrides, pub cache: crate::cache::Settings, pub force_cache: crate::cache::UnsafeForce, pub transform: crate::transform::Settings, pub pages: crate::pages::VhostPages, pub proxy_timeouts: crate::vhost::ProxyTimeouts, pub user_agent_blocking: crate::user_agent::CompiledPolicy, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedLogVhost { pub vhost: String, pub matched_by: &'static str, } pub struct Snapshot { pub web_php_services: HashMap>, pub vhost_metrics: std::collections::BTreeMap>, pub forwarding: crate::forwarding::Compiled, pub quic_tls: Option>, pub listeners: Vec, pub pools: Vec>, pub tls_http1: Option>, frontends: HashMap>>, pub tls: Option>, pub envelope: Envelope, routes: HashMap>>, fallbacks: HashMap>>, redirects: HashMap>>, not_found_hosts: HashMap>>, host_acls: HashMap>, pub global_security: CompiledGlobalSecurity, pub headers: CompiledHeaders, } impl std::fmt::Debug for Snapshot { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Snapshot") .field("revision", &self.envelope.revision) .field("listeners", &self.listeners) .field("quic_tls", &self.quic_tls.is_some()) .finish_non_exhaustive() } }