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}