Skip to main content

zentinel_config/
server.rs

1//! Server and listener configuration types
2//!
3//! This module contains configuration types for the proxy server itself
4//! and its listeners (ports/addresses it binds to).
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8use validator::Validate;
9
10use zentinel_common::types::{TlsVersion, TraceIdFormat};
11
12// ============================================================================
13// Server Configuration
14// ============================================================================
15
16/// Server-wide configuration
17#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
18pub struct ServerConfig {
19    /// Number of worker threads (0 = number of CPU cores)
20    #[serde(default = "default_worker_threads")]
21    pub worker_threads: usize,
22
23    /// Maximum number of connections
24    #[serde(default = "default_max_connections")]
25    pub max_connections: usize,
26
27    /// Graceful shutdown timeout
28    #[serde(default = "default_graceful_shutdown_timeout")]
29    pub graceful_shutdown_timeout_secs: u64,
30
31    /// Enable daemon mode
32    #[serde(default)]
33    pub daemon: bool,
34
35    /// PID file path
36    pub pid_file: Option<PathBuf>,
37
38    /// User to switch to after binding
39    pub user: Option<String>,
40
41    /// Group to switch to after binding
42    pub group: Option<String>,
43
44    /// Working directory
45    pub working_directory: Option<PathBuf>,
46
47    /// Trace ID format for request tracing
48    ///
49    /// - `tinyflake` (default): 11-char Base58, operator-friendly
50    /// - `uuid`: 36-char UUID v4, guaranteed unique
51    #[serde(default)]
52    pub trace_id_format: TraceIdFormat,
53
54    /// Enable automatic configuration reload on file changes
55    ///
56    /// When enabled, the proxy will watch the configuration file for changes
57    /// and automatically reload when modifications are detected.
58    #[serde(default)]
59    pub auto_reload: bool,
60}
61
62// ============================================================================
63// Listener Configuration
64// ============================================================================
65
66/// Listener configuration (port binding)
67#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
68pub struct ListenerConfig {
69    /// Unique identifier for this listener
70    pub id: String,
71
72    /// Socket address to bind to
73    #[validate(custom(function = "crate::validation::validate_socket_addr"))]
74    pub address: String,
75
76    /// Protocol (http, https)
77    pub protocol: ListenerProtocol,
78
79    /// TLS configuration (required for https)
80    pub tls: Option<TlsConfig>,
81
82    /// Default route if no other matches
83    pub default_route: Option<String>,
84
85    /// Request timeout
86    #[serde(default = "default_request_timeout")]
87    pub request_timeout_secs: u64,
88
89    /// Keep-alive timeout
90    #[serde(default = "default_keepalive_timeout")]
91    pub keepalive_timeout_secs: u64,
92
93    /// Maximum concurrent streams (HTTP/2)
94    #[serde(default = "default_max_concurrent_streams")]
95    pub max_concurrent_streams: u32,
96
97    /// Maximum requests per downstream connection before closing.
98    /// Equivalent to nginx's keepalive_requests. None = unlimited.
99    #[serde(default)]
100    pub keepalive_max_requests: Option<u32>,
101}
102
103/// Listener protocol
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "lowercase")]
106pub enum ListenerProtocol {
107    Http,
108    Https,
109    #[serde(rename = "h2")]
110    Http2,
111    #[serde(rename = "h3")]
112    Http3,
113}
114
115// ============================================================================
116// TLS Configuration
117// ============================================================================
118
119/// TLS configuration
120#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
121pub struct TlsConfig {
122    /// Default certificate file path (used when no SNI match)
123    /// Optional when ACME is configured
124    pub cert_file: Option<PathBuf>,
125
126    /// Default private key file path
127    /// Optional when ACME is configured
128    pub key_file: Option<PathBuf>,
129
130    /// Additional certificates for SNI support
131    /// Maps hostname patterns to certificate configurations
132    #[serde(default)]
133    pub additional_certs: Vec<SniCertificate>,
134
135    /// CA certificate file path for client verification (mTLS)
136    pub ca_file: Option<PathBuf>,
137
138    /// Minimum TLS version
139    #[serde(default = "default_min_tls_version")]
140    pub min_version: TlsVersion,
141
142    /// Maximum TLS version
143    pub max_version: Option<TlsVersion>,
144
145    /// Cipher suites (empty = use defaults)
146    #[serde(default)]
147    pub cipher_suites: Vec<String>,
148
149    /// Require client certificates (mTLS)
150    #[serde(default)]
151    pub client_auth: bool,
152
153    /// OCSP stapling
154    #[serde(default = "default_ocsp_stapling")]
155    pub ocsp_stapling: bool,
156
157    /// Session resumption
158    #[serde(default = "default_session_resumption")]
159    pub session_resumption: bool,
160
161    /// ACME automatic certificate management
162    /// When configured, cert_file and key_file become optional
163    pub acme: Option<AcmeConfig>,
164}
165
166/// ACME automatic certificate configuration
167///
168/// Enables zero-config TLS via Let's Encrypt and compatible CAs.
169/// When configured, Zentinel will automatically obtain, renew, and
170/// manage TLS certificates for the specified domains.
171///
172/// # Example
173///
174/// ```kdl
175/// tls {
176///     acme {
177///         email "admin@example.com"
178///         domains "example.com" "www.example.com"
179///         staging false
180///         storage "/var/lib/zentinel/acme"
181///         renew-before-days 30
182///         challenge-type "http-01"  // or "dns-01" for wildcards
183///
184///         // Required for DNS-01 challenges
185///         dns-provider {
186///             type "hetzner"
187///             credentials-file "/etc/zentinel/secrets/hetzner-dns.json"
188///             api-timeout-secs 30
189///
190///             propagation {
191///                 initial-delay-secs 10
192///                 check-interval-secs 5
193///                 timeout-secs 120
194///             }
195///         }
196///     }
197/// }
198/// ```
199#[derive(Debug, Clone, Serialize, Deserialize, Validate)]
200pub struct AcmeConfig {
201    /// Contact email for account registration and recovery
202    #[validate(email)]
203    pub email: String,
204
205    /// Domain names to obtain certificates for
206    #[validate(length(min = 1, message = "at least one domain is required"))]
207    pub domains: Vec<String>,
208
209    /// Optional custom ACME directory URL (e.g., ZeroSSL)
210    /// If not provided, defaults to Let's Encrypt
211    #[validate(url)]
212    pub server_url: Option<String>,
213
214    /// Use Let's Encrypt staging environment
215    /// Only used if server_url is not provided
216    #[serde(default)]
217    pub staging: bool,
218
219    /// External Account Binding (EAB) credentials
220    /// Required by some providers like ZeroSSL
221    pub eab: Option<ExternalAccountBinding>,
222
223    /// Directory for storing certificates and account keys
224    #[serde(default = "default_acme_storage")]
225    pub storage: PathBuf,
226
227    /// Days before expiry to trigger renewal
228    /// Let's Encrypt certificates are valid for 90 days
229    /// Default is 30 days before expiry
230    #[serde(default = "default_renewal_days")]
231    pub renew_before_days: u32,
232
233    /// Challenge type to use for domain validation
234    /// Defaults to HTTP-01, use DNS-01 for wildcard certificates
235    #[serde(default)]
236    pub challenge_type: AcmeChallengeType,
237
238    /// Key type to use for the certificate and account
239    /// Defaults to ECDSA P-256
240    #[serde(default)]
241    pub key_type: AcmeKeyType,
242
243    /// DNS provider configuration (required for DNS-01 challenges)
244    pub dns_provider: Option<DnsProviderConfig>,
245}
246
247/// ACME key type
248#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
249#[serde(rename_all = "kebab-case")]
250pub enum AcmeKeyType {
251    /// ECDSA P-256 (default)
252    #[default]
253    EcdsaP256,
254    /// ECDSA P-384
255    EcdsaP384,
256}
257
258impl AcmeKeyType {
259    /// Parse from a string with loose matching
260    pub fn from_str_loose(s: &str) -> Option<Self> {
261        match s.to_lowercase().as_str() {
262            "ecdsa-p256" | "p256" | "ecdsa" => Some(Self::EcdsaP256),
263            "ecdsa-p384" | "p384" => Some(Self::EcdsaP384),
264            _ => None,
265        }
266    }
267
268    /// Convert to string for KDL/display
269    pub fn as_str(&self) -> &'static str {
270        match self {
271            Self::EcdsaP256 => "ecdsa-p256",
272            Self::EcdsaP384 => "ecdsa-p384",
273        }
274    }
275}
276
277/// External Account Binding (EAB) credentials for ACME
278#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct ExternalAccountBinding {
280    /// Key ID (KID) provided by the ACME CA
281    pub kid: String,
282    /// HMAC Key (base64url-encoded) provided by the ACME CA
283    pub hmac_key: String,
284}
285
286/// ACME challenge type
287#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "kebab-case")]
289pub enum AcmeChallengeType {
290    /// HTTP-01 challenge (default)
291    /// Requires HTTP access on port 80
292    #[default]
293    Http01,
294
295    /// DNS-01 challenge
296    /// Required for wildcard certificates
297    /// Requires DNS provider configuration
298    Dns01,
299}
300
301impl AcmeChallengeType {
302    /// Check if this is DNS-01 challenge type
303    pub fn is_dns01(&self) -> bool {
304        matches!(self, Self::Dns01)
305    }
306
307    /// Check if this is HTTP-01 challenge type
308    pub fn is_http01(&self) -> bool {
309        matches!(self, Self::Http01)
310    }
311}
312
313/// DNS provider configuration for DNS-01 challenges
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct DnsProviderConfig {
316    /// DNS provider type
317    pub provider: DnsProviderType,
318
319    /// Path to credentials file
320    /// File should contain JSON: {"token": "..."} or {"api_key": "...", "api_secret": "..."}
321    pub credentials_file: Option<PathBuf>,
322
323    /// Environment variable containing credentials
324    pub credentials_env: Option<String>,
325
326    /// API request timeout in seconds
327    #[serde(default = "default_dns_api_timeout")]
328    pub api_timeout_secs: u64,
329
330    /// Propagation check configuration
331    #[serde(default)]
332    pub propagation: PropagationCheckConfig,
333}
334
335/// DNS provider type
336#[derive(Debug, Clone, Serialize, Deserialize)]
337#[serde(tag = "type", rename_all = "lowercase")]
338pub enum DnsProviderType {
339    /// Hetzner DNS API
340    Hetzner,
341
342    /// Cloudflare DNS API
343    Cloudflare,
344
345    /// Generic webhook provider
346    Webhook {
347        /// Webhook URL
348        url: String,
349        /// Optional custom auth header name
350        auth_header: Option<String>,
351    },
352}
353
354/// Configuration for DNS propagation checking
355#[derive(Debug, Clone, Serialize, Deserialize)]
356pub struct PropagationCheckConfig {
357    /// Initial delay before first check (seconds)
358    #[serde(default = "default_propagation_initial_delay")]
359    pub initial_delay_secs: u64,
360
361    /// Interval between propagation checks (seconds)
362    #[serde(default = "default_propagation_check_interval")]
363    pub check_interval_secs: u64,
364
365    /// Maximum time to wait for propagation (seconds)
366    #[serde(default = "default_propagation_timeout")]
367    pub timeout_secs: u64,
368
369    /// Custom nameservers to query (optional)
370    /// Defaults to Google (8.8.8.8), Cloudflare (1.1.1.1), Quad9 (9.9.9.9)
371    #[serde(default)]
372    pub nameservers: Vec<String>,
373}
374
375impl Default for PropagationCheckConfig {
376    fn default() -> Self {
377        Self {
378            initial_delay_secs: default_propagation_initial_delay(),
379            check_interval_secs: default_propagation_check_interval(),
380            timeout_secs: default_propagation_timeout(),
381            nameservers: Vec::new(),
382        }
383    }
384}
385
386/// SNI certificate configuration
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct SniCertificate {
389    /// Hostname patterns to match (e.g., "example.com", "*.example.com").
390    /// When set, only these hostnames are registered and SAN auto-extraction is skipped.
391    pub hostnames: Vec<String>,
392
393    /// Priority hostnames for tie-breaking when multiple certs match the same SNI.
394    /// When set, all SANs are still auto-extracted, but these hostnames win if
395    /// another cert also claims them. Mutually exclusive with `hostnames`.
396    pub priority_hostnames: Vec<String>,
397
398    /// Certificate file path
399    pub cert_file: PathBuf,
400
401    /// Private key file path
402    pub key_file: PathBuf,
403}
404
405// ============================================================================
406// Default Value Functions
407// ============================================================================
408
409pub(crate) fn default_worker_threads() -> usize {
410    0
411}
412
413pub(crate) fn default_max_connections() -> usize {
414    10000
415}
416
417pub(crate) fn default_graceful_shutdown_timeout() -> u64 {
418    30
419}
420
421pub(crate) fn default_request_timeout() -> u64 {
422    60
423}
424
425pub(crate) fn default_keepalive_timeout() -> u64 {
426    75
427}
428
429pub(crate) fn default_max_concurrent_streams() -> u32 {
430    100
431}
432
433fn default_min_tls_version() -> TlsVersion {
434    TlsVersion::Tls12
435}
436
437fn default_ocsp_stapling() -> bool {
438    true
439}
440
441fn default_session_resumption() -> bool {
442    true
443}
444
445pub(crate) fn default_acme_storage() -> PathBuf {
446    PathBuf::from("/var/lib/zentinel/acme")
447}
448
449pub(crate) fn default_renewal_days() -> u32 {
450    30
451}
452
453fn default_dns_api_timeout() -> u64 {
454    30
455}
456
457fn default_propagation_initial_delay() -> u64 {
458    10
459}
460
461fn default_propagation_check_interval() -> u64 {
462    5
463}
464
465fn default_propagation_timeout() -> u64 {
466    120
467}