use super::*; pub(super) fn validate(config: &mut ProxyConfig) -> Result<()> { if !(1..=MAX_API_REQUEST_BODY_LIMIT_BYTES).contains(&config.server.api.request_body_limit_bytes) { return Err(ProxyError::Config( "server.api.request_body_limit_bytes must be within [1, 1048576]".to_string(), )); } if config.server.api.minimal_runtime_cache_ttl_ms > 60_000 { return Err(ProxyError::Config( "server.api.minimal_runtime_cache_ttl_ms must be within [0, 60000]".to_string(), )); } if config.server.api.runtime_edge_cache_ttl_ms > 60_000 { return Err(ProxyError::Config( "server.api.runtime_edge_cache_ttl_ms must be within [0, 60000]".to_string(), )); } if !(1..=1000).contains(&config.server.api.runtime_edge_top_n) { return Err(ProxyError::Config( "server.api.runtime_edge_top_n must be within [1, 1000]".to_string(), )); } if !(16..=4096).contains(&config.server.api.runtime_edge_events_capacity) { return Err(ProxyError::Config( "server.api.runtime_edge_events_capacity must be within [16, 4096]".to_string(), )); } if config.server.api.listen.parse::().is_err() { return Err(ProxyError::Config( "server.api.listen must be in IP:PORT format".to_string(), )); } if config.server.proxy_protocol_header_timeout_ms == 0 { return Err(ProxyError::Config( "server.proxy_protocol_header_timeout_ms must be > 0".to_string(), )); } if config.server.listen_backlog == 0 || config.server.listen_backlog > i32::MAX as u32 { return Err(ProxyError::Config(format!( "server.listen_backlog must be within [1, {}]", i32::MAX ))); } config .server .client_mss_value() .map_err(|error| ProxyError::Config(format!("server.client_mss {error}")))?; config .server .client_mss_bulk_value() .map_err(|error| ProxyError::Config(format!("server.client_mss_bulk {error}")))?; for (idx, listener) in config.server.listeners.iter().enumerate() { if listener.client_mss.is_some() { listener .effective_client_mss(&config.server) .map_err(|error| { ProxyError::Config(format!("server.listeners[{idx}].client_mss {error}")) })?; } if listener.synlimit_seconds == 0 { return Err(ProxyError::Config(format!( "server.listeners[{idx}].synlimit_seconds must be > 0" ))); } if listener.synlimit_hitcount == 0 { return Err(ProxyError::Config(format!( "server.listeners[{idx}].synlimit_hitcount must be > 0" ))); } if listener.synlimit_burst == 0 { return Err(ProxyError::Config(format!( "server.listeners[{idx}].synlimit_burst must be > 0" ))); } if listener.synlimit_ios_seconds == 0 { return Err(ProxyError::Config(format!( "server.listeners[{idx}].synlimit_ios_seconds must be > 0" ))); } if listener.synlimit_ios_hitcount == 0 { return Err(ProxyError::Config(format!( "server.listeners[{idx}].synlimit_ios_hitcount must be > 0" ))); } if listener.synlimit_ios_burst == 0 { return Err(ProxyError::Config(format!( "server.listeners[{idx}].synlimit_ios_burst must be > 0" ))); } if listener.synlimit_hashlimit_expire_ms == 0 { return Err(ProxyError::Config(format!( "server.listeners[{idx}].synlimit_hashlimit_expire_ms must be > 0" ))); } if listener.synlimit_hashlimit_size == 0 { return Err(ProxyError::Config(format!( "server.listeners[{idx}].synlimit_hashlimit_size must be > 0" ))); } } if config.server.accept_permit_timeout_ms > 60_000 { return Err(ProxyError::Config( "server.accept_permit_timeout_ms must be within [0, 60000]".to_string(), )); } if config.server.conntrack_control.pressure_high_watermark_pct == 0 || config.server.conntrack_control.pressure_high_watermark_pct > 100 { return Err(ProxyError::Config( "server.conntrack_control.pressure_high_watermark_pct must be within [1, 100]" .to_string(), )); } if config.server.conntrack_control.pressure_low_watermark_pct >= config.server.conntrack_control.pressure_high_watermark_pct { return Err(ProxyError::Config( "server.conntrack_control.pressure_low_watermark_pct must be < pressure_high_watermark_pct" .to_string(), )); } if config.server.conntrack_control.delete_budget_per_sec == 0 { return Err(ProxyError::Config( "server.conntrack_control.delete_budget_per_sec must be > 0".to_string(), )); } if matches!(config.server.conntrack_control.mode, ConntrackMode::Hybrid) && config .server .conntrack_control .hybrid_listener_ips .is_empty() { return Err(ProxyError::Config( "server.conntrack_control.hybrid_listener_ips must be non-empty in mode=hybrid" .to_string(), )); } if config.general.effective_me_pool_force_close_secs() > 0 && config.general.effective_me_pool_force_close_secs() < config.general.me_pool_drain_ttl_secs { warn!( me_pool_drain_ttl_secs = config.general.me_pool_drain_ttl_secs, me_reinit_drain_timeout_secs = config.general.effective_me_pool_force_close_secs(), "force-close timeout is lower than drain TTL; bumping force-close timeout to TTL" ); config.general.me_reinit_drain_timeout_secs = config.general.me_pool_drain_ttl_secs; } // Validate secrets. for (user, secret) in &config.access.users { if !secret.chars().all(|c| c.is_ascii_hexdigit()) || secret.len() != 32 { return Err(ProxyError::InvalidSecret { user: user.clone(), reason: "Must be 32 hex characters".to_string(), }); } } config.censorship.tls_domain = normalize_domain_to_ascii(&config.censorship.tls_domain, "censorship.tls_domain")?; // Validate mask_unix_sock. if let Some(ref sock_path) = config.censorship.mask_unix_sock { if sock_path.is_empty() { return Err(ProxyError::Config( "mask_unix_sock cannot be empty".to_string(), )); } #[cfg(unix)] if sock_path.len() > 107 { return Err(ProxyError::Config(format!( "mask_unix_sock path too long: {} bytes (max 107)", sock_path.len() ))); } #[cfg(not(unix))] return Err(ProxyError::Config( "mask_unix_sock is only supported on Unix platforms".to_string(), )); if config.censorship.mask_host.is_some() { return Err(ProxyError::Config( "mask_unix_sock and mask_host are mutually exclusive".to_string(), )); } } if let Some(mask_host) = config.censorship.mask_host.as_mut() { *mask_host = normalize_mask_host_to_ascii(mask_host, "censorship.mask_host")?; } for (domain, target) in &config.censorship.exclusive_mask { if !is_valid_tls_domain_name(domain) { return Err(ProxyError::Config(format!( "Invalid censorship.exclusive_mask domain: '{}'. Must be a valid domain name", domain ))); } if parse_exclusive_mask_target(target).is_none() { return Err(ProxyError::Config(format!( "Invalid censorship.exclusive_mask target for '{}': '{}'. Expected host:port with port > 0", domain, target ))); } } Ok(()) }