1use std::{
51 collections::{BTreeMap, HashMap, HashSet},
52 env, fmt,
53 fs::{File, create_dir_all, metadata},
54 io::{ErrorKind, Read},
55 net::SocketAddr,
56 ops::Range,
57 path::PathBuf,
58};
59
60use crate::{
61 ObjectKind,
62 certificate::split_certificate_chain,
63 listener_key::{InterfaceError, ListenerKey, validate_listener_interface},
64 logging::AccessLogFormat,
65 proto::command::{
66 ActivateListener, AddBackend, AddCertificate, CertificateAndKey, ClientAuthMode, Cluster,
67 CustomHttpAnswers, ForwardedHeaders, Header, HeaderPosition, HealthCheckConfig,
68 HealthCheckMode, HstsConfig, HttpListenerConfig, HttpStatusRange, HttpsListenerConfig,
69 ListenerType, LoadBalancingAlgorithms, LoadBalancingParams, LoadMetric, MetricDetail,
70 MetricsConfiguration, PathRule, ProtobufAccessLogFormat, ProxyProtocolConfig,
71 RedirectPolicy, RedirectScheme, Request, RequestHttpFrontend, RequestTcpFrontend,
72 RequestUdpFrontend, RulePosition, ServerConfig, ServerMetricsConfig, ShardMode,
73 SocketAddress, TcpListenerConfig, TlsVersion, UdpAffinityKey, UdpClusterConfig,
74 UdpHealthConfig, UdpHealthMode, UdpListenerConfig, WorkerRequest, request::RequestType,
75 },
76};
77
78pub const DEFAULT_CIPHER_LIST: [&str; 9] = [
86 "TLS13_AES_256_GCM_SHA384",
88 "TLS13_AES_128_GCM_SHA256",
89 "TLS13_CHACHA20_POLY1305_SHA256",
90 "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
92 "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
93 "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
94 "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
95 "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
96 "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
97];
98
99pub const DEFAULT_SIGNATURE_ALGORITHMS: [&str; 9] = [
100 "ECDSA+SHA256",
101 "ECDSA+SHA384",
102 "ECDSA+SHA512",
103 "RSA+SHA256",
104 "RSA+SHA384",
105 "RSA+SHA512",
106 "RSA-PSS+SHA256",
107 "RSA-PSS+SHA384",
108 "RSA-PSS+SHA512",
109];
110
111pub const DEFAULT_GROUPS_LIST: [&str; 4] = ["X25519MLKEM768", "x25519", "P-256", "P-384"];
112
113pub const DEFAULT_ALPN_PROTOCOLS: [&str; 2] = ["h2", "http/1.1"];
116
117pub const DEFAULT_FRONT_TIMEOUT: u32 = 60;
119
120pub const DEFAULT_COMMAND_SOCKET_MODE: u32 = 0o600;
123
124pub const MAX_COMMAND_SOCKET_MODE: u32 = 0o777;
127
128pub const DEFAULT_BACK_TIMEOUT: u32 = 30;
130
131pub const DEFAULT_CONNECT_TIMEOUT: u32 = 3;
133
134pub const DEFAULT_SNI_PREREAD_TIMEOUT: u32 = 5;
139
140pub const DEFAULT_SNI_PREREAD_MAX_BYTES: u32 = 16384;
148
149pub const MIN_SNI_PREREAD_MAX_BYTES: u32 = 5;
158
159pub const DEFAULT_REQUEST_TIMEOUT: u32 = 10;
161
162pub const DEFAULT_UDP_FRONT_TIMEOUT: u32 = 30;
164
165pub const DEFAULT_UDP_BACK_TIMEOUT: u32 = 30;
167
168pub const DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE: u32 = 1500;
171
172pub const DEFAULT_UDP_MAX_FLOWS: u32 = 0;
175
176pub const DEFAULT_WORKER_TIMEOUT: u32 = 10;
178
179pub const DEFAULT_STICKY_NAME: &str = "SOZUBALANCEID";
181
182pub const DEFAULT_ZOMBIE_CHECK_INTERVAL: u32 = 1_800;
184
185pub const DEFAULT_ACCEPT_QUEUE_TIMEOUT: u32 = 60;
187
188pub const DEFAULT_HSTS_MAX_AGE: u32 = 31_536_000;
195
196pub const DEFAULT_EVICT_ON_QUEUE_FULL: bool = false;
202
203pub const DEFAULT_WORKER_COUNT: u16 = 2;
205
206pub const DEFAULT_WORKER_AUTOMATIC_RESTART: bool = true;
208
209pub const DEFAULT_AUTOMATIC_STATE_SAVE: bool = false;
211
212pub const DEFAULT_MIN_BUFFERS: u64 = 1;
214
215pub const DEFAULT_MAX_BUFFERS: u64 = 1_000;
217
218pub const DEFAULT_BUFFER_SIZE: u64 = 16_393;
220
221pub const H2_MIN_BUFFER_SIZE: u64 = 16_393;
231
232pub const DEFAULT_MAX_CONNECTIONS: usize = 10_000;
234
235pub const DEFAULT_COMMAND_BUFFER_SIZE: u64 = 1_000_000;
237
238pub const DEFAULT_MAX_COMMAND_BUFFER_SIZE: u64 = 2_000_000;
240
241pub const DEFAULT_DISABLE_CLUSTER_METRICS: bool = false;
243
244pub const MAX_LOOP_ITERATIONS: usize = 100000;
245
246pub const DEFAULT_SEND_TLS_13_TICKETS: u64 = 4;
251
252pub const DEFAULT_LOG_TARGET: &str = "stdout";
254
255pub const DEFAULT_MAX_CONNECTIONS_PER_IP: u64 = 0;
260
261pub const DEFAULT_RETRY_AFTER: u32 = 60;
267
268pub const DEFAULT_MAX_CONNECTIONS_PER_SUBNET: u64 = 0;
277
278pub const DEFAULT_MAX_CONNECTION_ATTEMPTS: u32 = 5;
285
286pub const MAX_CONNECTION_ATTEMPTS_UPPER_BOUND: u32 = u8::MAX as u32;
289
290pub fn validate_max_connection_attempts(attempts: u32) -> Result<(), &'static str> {
294 if (1..=MAX_CONNECTION_ATTEMPTS_UPPER_BOUND).contains(&attempts) {
295 Ok(())
296 } else {
297 Err("max_connection_attempts must lie in 1..=255")
298 }
299}
300
301pub const DEFAULT_SUBNET_IPV4_PREFIX: u32 = 32;
306
307pub const DEFAULT_SUBNET_IPV6_PREFIX: u32 = 128;
311
312pub const MAX_SUBNET_IPV4_PREFIX: u32 = 32;
316
317pub const MAX_SUBNET_IPV6_PREFIX: u32 = 128;
321
322#[derive(Debug)]
323pub enum IncompatibilityKind {
324 PublicAddress,
325 ProxyProtocol,
326}
327
328#[derive(Debug)]
329pub enum MissingKind {
330 Field(String),
331 Protocol,
332 SavedState,
333}
334
335#[derive(thiserror::Error, Debug)]
336pub enum ConfigError {
337 #[error("env path not found: {0}")]
338 Env(String),
339 #[error("Could not open file {path_to_open}: {io_error}")]
340 FileOpen {
341 path_to_open: String,
342 io_error: std::io::Error,
343 },
344 #[error("Could not read file {path_to_read}: {io_error}")]
345 FileRead {
346 path_to_read: String,
347 io_error: std::io::Error,
348 },
349 #[error(
350 "the field {kind:?} of {object:?} with id or address {id} is incompatible with the rest of the options"
351 )]
352 Incompatible {
353 kind: IncompatibilityKind,
354 object: ObjectKind,
355 id: String,
356 },
357 #[error("Invalid '{0}' field for a TCP frontend")]
358 InvalidFrontendConfig(String),
359 #[error("invalid path {0:?}")]
360 InvalidPath(PathBuf),
361 #[error("listening address {0:?} is already used in the configuration")]
362 ListenerAddressAlreadyInUse(SocketAddr),
363 #[error("listener {0} is already declared in the configuration")]
364 ListenerAlreadyDeclared(ListenerKey),
365 #[error(
366 "listeners sharing the address {0} on different interfaces must all use the same protocol"
367 )]
368 ListenerProtocolMismatchOnAddress(SocketAddr),
369 #[error("listener {address}: {error}")]
370 ListenerInterface {
371 address: SocketAddr,
372 error: InterfaceError,
373 },
374 #[error("missing {0:?}")]
375 Missing(MissingKind),
376 #[error("could not get parent directory for file {0}")]
377 NoFileParent(String),
378 #[error("Could not get the path of the saved state")]
379 SaveStatePath(String),
380 #[error("Can not determine path to sozu socket: {0}")]
381 SocketPathError(String),
382 #[error("toml decoding error: {0}")]
383 DeserializeToml(String),
384 #[error("Can not set this frontend on a {0:?} listener")]
385 WrongFrontendProtocol(ListenerProtocol),
386 #[error("Can not build a {expected:?} listener from a {found:?} config")]
387 WrongListenerProtocol {
388 expected: ListenerProtocol,
389 found: Option<ListenerProtocol>,
390 },
391 #[error("Invalid ALPN protocol '{0}'. Valid values: \"h2\", \"http/1.1\"")]
392 InvalidAlpnProtocol(String),
393 #[error("cluster {cluster_id}: {reason}")]
396 InvalidAffinityKey {
397 cluster_id: String,
398 reason: &'static str,
399 },
400 #[error("cluster {cluster_id}: {reason}")]
402 InvalidShuffleSharding {
403 cluster_id: String,
404 reason: &'static str,
405 },
406 #[error("max_connection_attempts = {value}{}: {reason}", cluster_id.as_deref().map(|id| format!(" in cluster {id}")).unwrap_or_default())]
409 InvalidMaxConnectionAttempts {
410 cluster_id: Option<String>,
411 value: u32,
412 reason: &'static str,
413 },
414 #[error("cluster {cluster_id}: {reason}")]
417 InvalidHealthCheck {
418 cluster_id: String,
419 reason: &'static str,
420 },
421 #[error(
427 "{key} = {value} is out of range: a {family} prefix length must be between 0 and \
428 {maximum} bits inclusive."
429 )]
430 InvalidSubnetPrefix {
431 key: &'static str,
432 family: &'static str,
433 value: u32,
434 maximum: u32,
435 },
436 #[error("command_socket_mode = {value:?} is invalid: {reason}")]
439 InvalidCommandSocketMode { value: String, reason: &'static str },
440 #[error(
446 "disable_http11 = true is incompatible with alpn_protocols containing \"http/1.1\" \
447 on listener {address}. The proxy would advertise http/1.1 then refuse every \
448 connection that negotiates it. Drop \"http/1.1\" from alpn_protocols or unset \
449 disable_http11."
450 )]
451 DisableHttp11WithHttp11Alpn { address: String },
452 #[error(
459 "buffer_size = {buffer_size} is below the H2 minimum of {minimum} but \
460 {listeners} HTTPS listener(s) advertise H2 ALPN. The H2 mux deadlocks \
461 on full-size frames with smaller buffers. Raise buffer_size to >= {minimum} \
462 or remove \"h2\" from those listeners' alpn_protocols."
463 )]
464 BufferSizeTooSmallForH2 {
465 buffer_size: u64,
466 minimum: u64,
467 listeners: usize,
468 },
469 #[error(
477 "command_buffer_size = {command_buffer_size} exceeds max_command_buffer_size = \
478 {max_command_buffer_size}: lower command_buffer_size to <= {max_command_buffer_size} \
479 or raise max_command_buffer_size to >= {command_buffer_size}"
480 )]
481 CommandBufferSizeExceedsMax {
482 command_buffer_size: u64,
483 max_command_buffer_size: u64,
484 },
485 #[error(
528 "listener {address}: {key} = {value} is below the minimum of {minimum} the runtime can \
529 honour. Below the minimum the value does not relax the limit, it breaks it: the H2 flood \
530 detector compares count > threshold, so 0 is already exceeded the first time the counter \
531 it guards leaves zero. How far that reaches depends on the key — \
532 h2_max_header_list_size and h2_max_header_fields are also the HPACK decode budget, so 0 \
533 refuses every request on this listener with ENHANCE_YOUR_CALM (as a stream reset, or as \
534 a connection GOAWAY once a header block spans CONTINUATION frames), while a per-window \
535 or lifetime frame counter only trips once a client sends the frame it counts, and \
536 h2_max_concurrent_streams / h2_stream_shrink_ratio are instead promoted to their floor \
537 behind a warning. Raise {key} to >= {minimum}, or remove it to keep the default."
538 )]
539 H2ThresholdBelowMinimum {
540 address: String,
541 key: &'static str,
542 value: u64,
543 minimum: u64,
544 },
545 #[error(
549 "invalid redirect policy '{0}'. Valid values: \"forward\", \"permanent\", \"unauthorized\""
550 )]
551 InvalidRedirectPolicy(String),
552 #[error(
556 "invalid redirect scheme '{0}'. Valid values: \"use-same\", \"use-http\", \"use-https\""
557 )]
558 InvalidRedirectScheme(String),
559 #[error(
563 "invalid header position '{position}' at headers[{index}]. Valid values: \"request\", \"response\", \"both\""
564 )]
565 InvalidHeaderPosition { index: usize, position: String },
566 #[error(
573 "invalid header bytes in {field} at headers[{index}]: control characters \
574 (NUL / CR / LF / other C0) are forbidden in header keys and values"
575 )]
576 InvalidHeaderBytes { index: usize, field: &'static str },
577 #[error("invalid HSTS config at {0}: `enabled` is required when an [hsts] block is present")]
583 HstsEnabledRequired(String),
584 #[error(
589 "invalid HSTS config at {0}: HSTS is only valid on HTTPS listeners and frontends \
590 (RFC 6797 §7.2 forbids the header over plaintext HTTP)"
591 )]
592 HstsOnPlainHttp(String),
593 #[error(
598 "invalid client auth config at {0}: client_auth / client_ca_certificates / \
599 client_ca_crls are only valid on HTTPS listeners"
600 )]
601 ClientAuthOnNonHttps(String),
602 #[error(
606 "invalid client auth config at {0}: client_ca_certificates / client_ca_crls are set \
607 but client_auth is not; set client_auth to \"optional\" or \"required\", or to \
608 \"none\" to keep them unused"
609 )]
610 ClientAuthModeMissing(String),
611 #[error(
618 "invalid SNI pattern '{sni}' for a TCP frontend: expected an exact hostname or a \
619 single leading \"*.\" wildcard label (e.g. \"example.com\" or \"*.example.com\"); \
620 '/' and non-leading '*' are rejected"
621 )]
622 InvalidSniPattern { sni: String },
623 #[error(
630 "non-ASCII SNI pattern '{sni}' for a TCP frontend: on-wire SNI is always an ASCII \
631 A-label (RFC 6066), so this pattern would never match a ClientHello. Write the \
632 punycode A-label form instead (e.g. \"xn--mnchen-3ya.example\" for \
633 \"münchen.example\")"
634 )]
635 NonAsciiSniPattern { sni: String },
636 #[error(
645 "TCP frontend {address} sets alpn but no hostname (sni): alpn only matches within an \
646 SNI-scoped preread, so a frontend without hostname would silently ignore its alpn list. \
647 Set hostname or drop alpn."
648 )]
649 AlpnWithoutSni { address: SocketAddr },
650 #[error(
656 "TCP frontends on {address} with sni {sni:?} both match ALPN protocol '{protocol}': \
657 ALPN matchers for the same (address, sni) must not overlap"
658 )]
659 TcpFrontendAlpnOverlap {
660 address: SocketAddr,
661 sni: Option<String>,
662 protocol: String,
663 },
664 #[error(
669 "more than one TCP frontend on {address} with sni {sni:?} leaves alpn empty (the \
670 catch-all match): at most one frontend per (address, sni) may omit alpn"
671 )]
672 TcpFrontendMultipleAlpnCatchAll {
673 address: SocketAddr,
674 sni: Option<String>,
675 },
676 #[error(
683 "TCP listener {address} is targeted by both a no-SNI frontend and at least one \
684 SNI-scoped frontend: an SNI-enabled listener must not also have a raw-TCP fallback \
685 frontend on the same address"
686 )]
687 TcpListenerMixesSniAndNoSni { address: SocketAddr },
688 #[error(
694 "sni_preread_timeout = {sni_preread_timeout}s on TCP listener {address} exceeds its \
695 front_timeout = {front_timeout}s: the preread phase cannot outlive the frontend \
696 inactivity timeout that would already have closed the connection"
697 )]
698 SniPrereadTimeoutExceedsFrontTimeout {
699 address: SocketAddr,
700 sni_preread_timeout: u32,
701 front_timeout: u32,
702 },
703 #[error(
709 "sni_preread_max_bytes = {sni_preread_max_bytes} on TCP listener {address} exceeds \
710 buffer_size = {buffer_size}: raise buffer_size to >= {sni_preread_max_bytes} or lower \
711 sni_preread_max_bytes"
712 )]
713 SniPrereadMaxBytesExceedsBufferSize {
714 address: SocketAddr,
715 sni_preread_max_bytes: u32,
716 buffer_size: u64,
717 },
718 #[error(
728 "sni_preread_max_bytes = {sni_preread_max_bytes} on TCP listener {address} is below the \
729 minimum of {minimum} bytes (a full TLS record header): the preread shell could never \
730 read enough bytes to make progress. Raise sni_preread_max_bytes to >= {minimum}"
731 )]
732 SniPrereadMaxBytesTooSmall {
733 address: SocketAddr,
734 sni_preread_max_bytes: u32,
735 minimum: u32,
736 },
737}
738
739#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
745#[serde(rename_all = "lowercase")]
746pub enum ClientAuthConfig {
747 #[default]
748 None,
749 Optional,
750 Required,
751}
752
753impl From<ClientAuthConfig> for ClientAuthMode {
754 fn from(value: ClientAuthConfig) -> Self {
755 match value {
756 ClientAuthConfig::None => ClientAuthMode::ClientAuthNone,
757 ClientAuthConfig::Optional => ClientAuthMode::ClientAuthOptional,
758 ClientAuthConfig::Required => ClientAuthMode::ClientAuthRequired,
759 }
760 }
761}
762
763#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
765#[serde(deny_unknown_fields)]
766pub struct ListenerBuilder {
767 pub address: SocketAddr,
768 pub interface: Option<String>,
774 pub protocol: Option<ListenerProtocol>,
775 pub public_address: Option<SocketAddr>,
776 pub answer_301: Option<String>,
777 pub answer_400: Option<String>,
778 pub answer_401: Option<String>,
779 pub answer_404: Option<String>,
780 pub answer_408: Option<String>,
781 pub answer_413: Option<String>,
782 pub answer_421: Option<String>,
785 pub answer_502: Option<String>,
786 pub answer_503: Option<String>,
787 pub answer_504: Option<String>,
788 pub answer_507: Option<String>,
789 pub answer_429: Option<String>,
794 pub tls_versions: Option<Vec<TlsVersion>>,
795 pub cipher_list: Option<Vec<String>>,
796 pub cipher_suites: Option<Vec<String>>,
797 pub groups_list: Option<Vec<String>>,
798 pub expect_proxy: Option<bool>,
799 #[serde(default = "default_sticky_name")]
800 pub sticky_name: String,
801 pub certificate: Option<String>,
802 pub certificate_chain: Option<String>,
803 pub key: Option<String>,
804 pub front_timeout: Option<u32>,
806 pub back_timeout: Option<u32>,
808 pub connect_timeout: Option<u32>,
810 pub request_timeout: Option<u32>,
812 pub config: Option<Config>,
814 pub send_tls13_tickets: Option<u64>,
818 pub alpn_protocols: Option<Vec<String>>,
821 pub h2_max_rst_stream_per_window: Option<u32>,
823 pub h2_max_ping_per_window: Option<u32>,
825 pub h2_max_settings_per_window: Option<u32>,
827 pub h2_max_empty_data_per_window: Option<u32>,
829 pub h2_max_window_update_stream0_per_window: Option<u32>,
835 pub sozu_id_header: Option<String>,
839 pub h2_max_continuation_frames: Option<u32>,
841 pub h2_max_glitch_count: Option<u32>,
844 pub h2_initial_connection_window: Option<u32>,
846 pub h2_max_concurrent_streams: Option<u32>,
848 pub h2_stream_shrink_ratio: Option<u32>,
850 pub h2_max_rst_stream_lifetime: Option<u64>,
855 pub h2_max_rst_stream_abusive_lifetime: Option<u64>,
861 pub h2_max_rst_stream_emitted_lifetime: Option<u64>,
868 pub h2_max_header_list_size: Option<u32>,
872 pub h2_max_header_table_size: Option<u32>,
876 pub h2_max_header_fields: Option<u32>,
880 pub h2_stream_refusal_percent: Option<u32>,
887 pub h2_stream_idle_timeout_seconds: Option<u32>,
893 pub h2_graceful_shutdown_deadline_seconds: Option<u32>,
898 pub strict_sni_binding: Option<bool>,
904 pub disable_http11: Option<bool>,
909 pub elide_x_real_ip: Option<bool>,
913 pub send_x_real_ip: Option<bool>,
918 pub forwarded_headers: Option<ForwardedHeadersMode>,
922 pub answers: Option<BTreeMap<String, String>>,
937 pub hsts: Option<FileHstsConfig>,
943 pub max_rx_datagram_size: Option<u32>,
947 pub max_flows: Option<u32>,
952 pub sni_preread_timeout: Option<u32>,
957 pub sni_preread_max_bytes: Option<u32>,
963 pub client_auth: Option<ClientAuthConfig>,
967 pub client_ca_certificates: Option<Vec<String>>,
972 pub client_ca_crls: Option<Vec<String>>,
976}
977
978pub fn default_sticky_name() -> String {
979 DEFAULT_STICKY_NAME.to_string()
980}
981
982impl ListenerBuilder {
983 pub fn new_http(address: SocketAddress) -> ListenerBuilder {
986 Self::new(address, ListenerProtocol::Http)
987 }
988
989 pub fn new_tcp(address: SocketAddress) -> ListenerBuilder {
992 Self::new(address, ListenerProtocol::Tcp)
993 }
994
995 pub fn new_https(address: SocketAddress) -> ListenerBuilder {
998 Self::new(address, ListenerProtocol::Https)
999 }
1000
1001 pub fn new_udp(address: SocketAddress) -> ListenerBuilder {
1004 Self::new(address, ListenerProtocol::Udp)
1005 }
1006
1007 fn new(address: SocketAddress, protocol: ListenerProtocol) -> ListenerBuilder {
1009 ListenerBuilder {
1010 address: address.into(),
1011 interface: None,
1012 answer_301: None,
1013 answer_401: None,
1014 answer_400: None,
1015 answer_404: None,
1016 answer_408: None,
1017 answer_413: None,
1018 answer_421: None,
1019 answer_502: None,
1020 answer_503: None,
1021 answer_504: None,
1022 answer_507: None,
1023 answer_429: None,
1024 back_timeout: None,
1025 certificate_chain: None,
1026 certificate: None,
1027 cipher_list: None,
1028 cipher_suites: None,
1029 groups_list: None,
1030 config: None,
1031 connect_timeout: None,
1032 expect_proxy: None,
1033 front_timeout: None,
1034 key: None,
1035 protocol: Some(protocol),
1036 public_address: None,
1037 request_timeout: None,
1038 send_tls13_tickets: None,
1039 sticky_name: DEFAULT_STICKY_NAME.to_string(),
1040 tls_versions: None,
1041 alpn_protocols: None,
1042 h2_max_rst_stream_per_window: None,
1043 h2_max_ping_per_window: None,
1044 h2_max_settings_per_window: None,
1045 h2_max_empty_data_per_window: None,
1046 h2_max_window_update_stream0_per_window: None,
1047 sozu_id_header: None,
1048 h2_max_continuation_frames: None,
1049 h2_max_glitch_count: None,
1050 h2_initial_connection_window: None,
1051 h2_max_concurrent_streams: None,
1052 h2_stream_shrink_ratio: None,
1053 h2_max_rst_stream_lifetime: None,
1054 h2_max_rst_stream_abusive_lifetime: None,
1055 h2_max_rst_stream_emitted_lifetime: None,
1056 h2_max_header_list_size: None,
1057 h2_max_header_table_size: None,
1058 h2_max_header_fields: None,
1059 h2_stream_refusal_percent: None,
1060 h2_stream_idle_timeout_seconds: None,
1061 h2_graceful_shutdown_deadline_seconds: None,
1062 strict_sni_binding: None,
1063 disable_http11: None,
1064 elide_x_real_ip: None,
1065 send_x_real_ip: None,
1066 forwarded_headers: None,
1067 answers: None,
1068 hsts: None,
1069 max_rx_datagram_size: None,
1070 max_flows: None,
1071 sni_preread_timeout: None,
1072 sni_preread_max_bytes: None,
1073 client_auth: None,
1074 client_ca_certificates: None,
1075 client_ca_crls: None,
1076 }
1077 }
1078
1079 pub fn with_interface<S>(&mut self, interface: Option<S>) -> &mut Self
1081 where
1082 S: ToString,
1083 {
1084 self.interface = interface.map(|interface| interface.to_string());
1085 self
1086 }
1087
1088 fn validated_interface(&self) -> Result<Option<String>, ConfigError> {
1091 if let Some(interface) = &self.interface {
1092 validate_listener_interface(interface).map_err(|error| {
1093 ConfigError::ListenerInterface {
1094 address: self.address,
1095 error,
1096 }
1097 })?;
1098 }
1099 Ok(self.interface.clone())
1100 }
1101
1102 pub fn with_public_address(&mut self, public_address: Option<SocketAddr>) -> &mut Self {
1103 if let Some(address) = public_address {
1104 self.public_address = Some(address);
1105 }
1106 self
1107 }
1108
1109 pub fn with_answer_404_path<S>(&mut self, answer_404_path: Option<S>) -> &mut Self
1110 where
1111 S: ToString,
1112 {
1113 if let Some(path) = answer_404_path {
1114 self.answer_404 = Some(path.to_string());
1115 }
1116 self
1117 }
1118
1119 pub fn with_answer_503_path<S>(&mut self, answer_503_path: Option<S>) -> &mut Self
1120 where
1121 S: ToString,
1122 {
1123 if let Some(path) = answer_503_path {
1124 self.answer_503 = Some(path.to_string());
1125 }
1126 self
1127 }
1128
1129 pub fn with_tls_versions(&mut self, tls_versions: Vec<TlsVersion>) -> &mut Self {
1130 self.tls_versions = Some(tls_versions);
1131 self
1132 }
1133
1134 pub fn with_cipher_list(&mut self, cipher_list: Option<Vec<String>>) -> &mut Self {
1135 self.cipher_list = cipher_list;
1136 self
1137 }
1138
1139 pub fn with_cipher_suites(&mut self, cipher_suites: Option<Vec<String>>) -> &mut Self {
1140 self.cipher_suites = cipher_suites;
1141 self
1142 }
1143
1144 pub fn with_alpn_protocols(&mut self, alpn_protocols: Option<Vec<String>>) -> &mut Self {
1145 self.alpn_protocols = alpn_protocols;
1146 self
1147 }
1148
1149 pub fn with_elide_x_real_ip(&mut self, elide_x_real_ip: bool) -> &mut Self {
1152 self.elide_x_real_ip = Some(elide_x_real_ip);
1153 self
1154 }
1155
1156 pub fn with_send_x_real_ip(&mut self, send_x_real_ip: bool) -> &mut Self {
1160 self.send_x_real_ip = Some(send_x_real_ip);
1161 self
1162 }
1163
1164 pub fn with_forwarded_headers(&mut self, forwarded_headers: ForwardedHeadersMode) -> &mut Self {
1167 self.forwarded_headers = Some(forwarded_headers);
1168 self
1169 }
1170
1171 pub fn with_expect_proxy(&mut self, expect_proxy: bool) -> &mut Self {
1172 self.expect_proxy = Some(expect_proxy);
1173 self
1174 }
1175
1176 pub fn with_sticky_name<S>(&mut self, sticky_name: Option<S>) -> &mut Self
1177 where
1178 S: ToString,
1179 {
1180 if let Some(name) = sticky_name {
1181 self.sticky_name = name.to_string();
1182 }
1183 self
1184 }
1185
1186 pub fn with_certificate<S>(&mut self, certificate: S) -> &mut Self
1187 where
1188 S: ToString,
1189 {
1190 self.certificate = Some(certificate.to_string());
1191 self
1192 }
1193
1194 pub fn with_certificate_chain(&mut self, certificate_chain: String) -> &mut Self {
1195 self.certificate = Some(certificate_chain);
1196 self
1197 }
1198
1199 pub fn with_key<S>(&mut self, key: String) -> &mut Self
1200 where
1201 S: ToString,
1202 {
1203 self.key = Some(key);
1204 self
1205 }
1206
1207 pub fn with_front_timeout(&mut self, front_timeout: Option<u32>) -> &mut Self {
1208 self.front_timeout = front_timeout;
1209 self
1210 }
1211
1212 pub fn with_back_timeout(&mut self, back_timeout: Option<u32>) -> &mut Self {
1213 self.back_timeout = back_timeout;
1214 self
1215 }
1216
1217 pub fn with_connect_timeout(&mut self, connect_timeout: Option<u32>) -> &mut Self {
1218 self.connect_timeout = connect_timeout;
1219 self
1220 }
1221
1222 pub fn with_request_timeout(&mut self, request_timeout: Option<u32>) -> &mut Self {
1223 self.request_timeout = request_timeout;
1224 self
1225 }
1226
1227 pub fn with_answer<S, P>(&mut self, code: S, path: P) -> &mut Self
1232 where
1233 S: ToString,
1234 P: ToString,
1235 {
1236 self.answers
1237 .get_or_insert_with(BTreeMap::new)
1238 .insert(code.to_string(), path.to_string());
1239 self
1240 }
1241
1242 pub fn with_answers(&mut self, answers: BTreeMap<String, String>) -> &mut Self {
1245 self.answers = Some(answers);
1246 self
1247 }
1248
1249 fn get_http_answers(&self) -> Result<Option<CustomHttpAnswers>, ConfigError> {
1251 let http_answers = CustomHttpAnswers {
1252 answer_301: read_http_answer_file(&self.answer_301)?,
1253 answer_400: read_http_answer_file(&self.answer_400)?,
1254 answer_401: read_http_answer_file(&self.answer_401)?,
1255 answer_404: read_http_answer_file(&self.answer_404)?,
1256 answer_408: read_http_answer_file(&self.answer_408)?,
1257 answer_413: read_http_answer_file(&self.answer_413)?,
1258 answer_421: read_http_answer_file(&self.answer_421)?,
1259 answer_502: read_http_answer_file(&self.answer_502)?,
1260 answer_503: read_http_answer_file(&self.answer_503)?,
1261 answer_504: read_http_answer_file(&self.answer_504)?,
1262 answer_507: read_http_answer_file(&self.answer_507)?,
1263 answer_429: read_http_answer_file(&self.answer_429)?,
1264 };
1265 Ok(Some(http_answers))
1266 }
1267
1268 fn get_listener_answers(&self) -> Result<BTreeMap<String, String>, ConfigError> {
1276 let mut out = BTreeMap::new();
1277
1278 macro_rules! merge_legacy {
1282 ($code:literal, $field:ident) => {
1283 if let Some(body) = read_http_answer_file(&self.$field)? {
1284 out.insert($code.to_owned(), body);
1285 }
1286 };
1287 }
1288 merge_legacy!("301", answer_301);
1289 merge_legacy!("400", answer_400);
1290 merge_legacy!("401", answer_401);
1291 merge_legacy!("404", answer_404);
1292 merge_legacy!("408", answer_408);
1293 merge_legacy!("413", answer_413);
1294 merge_legacy!("421", answer_421);
1295 merge_legacy!("502", answer_502);
1296 merge_legacy!("503", answer_503);
1297 merge_legacy!("504", answer_504);
1298 merge_legacy!("507", answer_507);
1299 merge_legacy!("429", answer_429);
1300
1301 if let Some(map) = &self.answers {
1302 let loaded = load_answers(map)?;
1303 out.extend(loaded);
1304 }
1305 Ok(out)
1306 }
1307
1308 fn assign_config_timeouts(&mut self, config: &Config) {
1310 self.front_timeout = Some(self.front_timeout.unwrap_or(config.front_timeout));
1311 self.back_timeout = Some(self.back_timeout.unwrap_or(config.back_timeout));
1312 self.connect_timeout = Some(self.connect_timeout.unwrap_or(config.connect_timeout));
1313 self.request_timeout = Some(self.request_timeout.unwrap_or(config.request_timeout));
1314 }
1315
1316 fn validate_h2_thresholds(&self) -> Result<(), ConfigError> {
1338 macro_rules! require_at_least {
1339 ($builder:ident, $field:ident, $minimum:literal) => {
1340 if let Some(value) = $builder.$field
1341 && u64::from(value) < $minimum
1342 {
1343 return Err(ConfigError::H2ThresholdBelowMinimum {
1344 address: $builder.address.to_string(),
1345 key: stringify!($field),
1346 value: u64::from(value),
1347 minimum: $minimum,
1348 });
1349 }
1350 };
1351 }
1352 crate::for_each_h2_knob_floor!(require_at_least, self);
1353 Ok(())
1354 }
1355
1356 fn reject_client_auth_fields(&self, listener_kind: &str) -> Result<(), ConfigError> {
1361 let has_mtls = self
1362 .client_auth
1363 .is_some_and(|m| m != ClientAuthConfig::None)
1364 || self
1365 .client_ca_certificates
1366 .as_ref()
1367 .is_some_and(|v| !v.is_empty())
1368 || self.client_ca_crls.as_ref().is_some_and(|v| !v.is_empty());
1369 if has_mtls {
1370 return Err(ConfigError::ClientAuthOnNonHttps(format!(
1371 "{} listener {}",
1372 listener_kind, self.address
1373 )));
1374 }
1375 Ok(())
1376 }
1377
1378 pub fn to_http(&mut self, config: Option<&Config>) -> Result<HttpListenerConfig, ConfigError> {
1380 if self.protocol != Some(ListenerProtocol::Http) {
1381 return Err(ConfigError::WrongListenerProtocol {
1382 expected: ListenerProtocol::Http,
1383 found: self.protocol.to_owned(),
1384 });
1385 }
1386 self.reject_client_auth_fields("HTTP")?;
1387
1388 self.validate_h2_thresholds()?;
1389
1390 if self.hsts.is_some() {
1396 return Err(ConfigError::HstsOnPlainHttp(format!(
1397 "HTTP listener {}",
1398 self.address
1399 )));
1400 }
1401
1402 if let Some(config) = config {
1403 self.assign_config_timeouts(config);
1404 }
1405
1406 let http_answers = self.get_http_answers()?;
1407 let answers = self.get_listener_answers()?;
1408
1409 let interface = self.validated_interface()?;
1410 let configuration = HttpListenerConfig {
1411 address: self.address.into(),
1412 interface,
1413 public_address: self.public_address.map(|a| a.into()),
1414 expect_proxy: self.expect_proxy.unwrap_or(false),
1415 sticky_name: self.sticky_name.clone(),
1416 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1417 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1418 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1419 request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1420 http_answers,
1421 answers,
1422 h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1423 h2_max_ping_per_window: self.h2_max_ping_per_window,
1424 h2_max_settings_per_window: self.h2_max_settings_per_window,
1425 h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1426 h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1427 h2_max_continuation_frames: self.h2_max_continuation_frames,
1428 h2_max_glitch_count: self.h2_max_glitch_count,
1429 h2_initial_connection_window: self.h2_initial_connection_window,
1430 h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1431 h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1432 h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1433 h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1434 h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1435 h2_max_header_list_size: self.h2_max_header_list_size,
1436 h2_max_header_table_size: self.h2_max_header_table_size,
1437 h2_max_header_fields: self.h2_max_header_fields,
1438 h2_stream_refusal_percent: self.h2_stream_refusal_percent,
1439 h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1440 h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1441 sozu_id_header: self.sozu_id_header.clone(),
1442 elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1443 send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1444 forwarded_headers: Some(ForwardedHeaders::from(
1445 self.forwarded_headers.unwrap_or_default(),
1446 ) as i32),
1447 ..Default::default()
1448 };
1449
1450 debug_assert_eq!(
1455 configuration.address,
1456 self.address.into(),
1457 "HTTP listener must bind the requested address"
1458 );
1459 Ok(configuration)
1460 }
1461
1462 pub fn to_tls(&mut self, config: Option<&Config>) -> Result<HttpsListenerConfig, ConfigError> {
1464 if self.protocol != Some(ListenerProtocol::Https) {
1465 return Err(ConfigError::WrongListenerProtocol {
1466 expected: ListenerProtocol::Https,
1467 found: self.protocol.to_owned(),
1468 });
1469 }
1470
1471 self.validate_h2_thresholds()?;
1472
1473 let default_cipher_list = DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect();
1474
1475 let cipher_list = self.cipher_list.clone().unwrap_or(default_cipher_list);
1476
1477 let cipher_suites = self
1478 .cipher_suites
1479 .clone()
1480 .unwrap_or_else(|| DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect());
1481
1482 let signature_algorithms: Vec<String> = DEFAULT_SIGNATURE_ALGORITHMS
1483 .into_iter()
1484 .map(String::from)
1485 .collect();
1486
1487 let groups_list = self
1488 .groups_list
1489 .clone()
1490 .unwrap_or_else(|| DEFAULT_GROUPS_LIST.into_iter().map(String::from).collect());
1491
1492 let alpn_protocols: Vec<String> = match &self.alpn_protocols {
1493 Some(protos) if !protos.is_empty() => {
1494 for proto in protos {
1495 match proto.as_str() {
1496 "h2" | "http/1.1" => {}
1497 other => return Err(ConfigError::InvalidAlpnProtocol(other.to_owned())),
1498 }
1499 }
1500 if self.disable_http11.unwrap_or(false) && protos.iter().any(|p| p == "http/1.1") {
1505 return Err(ConfigError::DisableHttp11WithHttp11Alpn {
1506 address: self.address.to_string(),
1507 });
1508 }
1509 if !protos.iter().any(|p| p == "http/1.1") {
1510 warn!(
1511 "ALPN protocols do not include 'http/1.1'. Clients without H2 support will fail TLS negotiation."
1512 );
1513 }
1514 let mut seen = std::collections::HashSet::new();
1516 protos
1517 .iter()
1518 .filter(|p| seen.insert(p.as_str()))
1519 .cloned()
1520 .collect()
1521 }
1522 _ => {
1523 if self.disable_http11.unwrap_or(false)
1527 && DEFAULT_ALPN_PROTOCOLS.contains(&"http/1.1")
1528 {
1529 return Err(ConfigError::DisableHttp11WithHttp11Alpn {
1530 address: self.address.to_string(),
1531 });
1532 }
1533 DEFAULT_ALPN_PROTOCOLS
1534 .iter()
1535 .map(|s| s.to_string())
1536 .collect()
1537 }
1538 };
1539
1540 let versions = match self.tls_versions {
1541 None => vec![TlsVersion::TlsV12 as i32, TlsVersion::TlsV13 as i32],
1542 Some(ref v) => v.iter().map(|v| *v as i32).collect(),
1543 };
1544
1545 let key = self.key.as_ref().and_then(|path| {
1546 Config::load_file(path)
1547 .map_err(|e| {
1548 error!("cannot load key at path '{}': {:?}", path, e);
1549 e
1550 })
1551 .ok()
1552 });
1553 let certificate = self.certificate.as_ref().and_then(|path| {
1554 Config::load_file(path)
1555 .map_err(|e| {
1556 error!("cannot load certificate at path '{}': {:?}", path, e);
1557 e
1558 })
1559 .ok()
1560 });
1561 let certificate_chain = self
1562 .certificate_chain
1563 .as_ref()
1564 .and_then(|path| {
1565 Config::load_file(path)
1566 .map_err(|e| {
1567 error!("cannot load certificate chain at path '{}': {:?}", path, e);
1568 e
1569 })
1570 .ok()
1571 })
1572 .map(split_certificate_chain)
1573 .unwrap_or_default();
1574
1575 let has_client_auth_files = self
1588 .client_ca_certificates
1589 .as_ref()
1590 .is_some_and(|paths| !paths.is_empty())
1591 || self
1592 .client_ca_crls
1593 .as_ref()
1594 .is_some_and(|paths| !paths.is_empty());
1595 match self.client_auth {
1596 None if has_client_auth_files => {
1597 return Err(ConfigError::ClientAuthModeMissing(format!(
1598 "HTTPS listener {}",
1599 self.address
1600 )));
1601 }
1602 Some(ClientAuthConfig::None) if has_client_auth_files => {
1603 warn!(
1604 "HTTPS listener {}: client_auth is \"none\", so its client_ca_certificates \
1605 and client_ca_crls are ignored and clients are not authenticated",
1606 self.address
1607 );
1608 }
1609 _ => {}
1610 }
1611 let mode = self.client_auth.unwrap_or_default();
1612 let client_auth = self
1613 .client_auth
1614 .map(|mode| ClientAuthMode::from(mode) as i32);
1615 let load_pem_paths = |paths: &Option<Vec<String>>| -> Result<Vec<String>, ConfigError> {
1616 if mode == ClientAuthConfig::None {
1617 return Ok(Vec::new());
1618 }
1619 paths
1620 .as_ref()
1621 .map(|list| list.iter().map(|path| Config::load_file(path)).collect())
1622 .unwrap_or_else(|| Ok(Vec::new()))
1623 };
1624 let client_ca_certificates = load_pem_paths(&self.client_ca_certificates)?;
1625 let client_ca_crls = load_pem_paths(&self.client_ca_crls)?;
1626
1627 let http_answers = self.get_http_answers()?;
1628 let answers = self.get_listener_answers()?;
1629
1630 if let Some(config) = config {
1631 self.assign_config_timeouts(config);
1632 }
1633
1634 let https_listener_config = HttpsListenerConfig {
1635 interface: self.validated_interface()?,
1636 address: self.address.into(),
1637 sticky_name: self.sticky_name.clone(),
1638 public_address: self.public_address.map(|a| a.into()),
1639 cipher_list,
1640 versions,
1641 expect_proxy: self.expect_proxy.unwrap_or(false),
1642 key,
1643 certificate,
1644 certificate_chain,
1645 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1646 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1647 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1648 request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1649 cipher_suites,
1650 signature_algorithms,
1651 groups_list,
1652 active: false,
1653 send_tls13_tickets: self
1654 .send_tls13_tickets
1655 .unwrap_or(DEFAULT_SEND_TLS_13_TICKETS),
1656 http_answers,
1657 answers,
1658 alpn_protocols,
1659 h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1660 h2_max_ping_per_window: self.h2_max_ping_per_window,
1661 h2_max_settings_per_window: self.h2_max_settings_per_window,
1662 h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1663 h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1664 h2_max_continuation_frames: self.h2_max_continuation_frames,
1665 h2_max_glitch_count: self.h2_max_glitch_count,
1666 h2_initial_connection_window: self.h2_initial_connection_window,
1667 h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1668 h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1669 h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1670 h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1671 h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1672 h2_max_header_list_size: self.h2_max_header_list_size,
1673 h2_max_header_table_size: self.h2_max_header_table_size,
1674 h2_max_header_fields: self.h2_max_header_fields,
1675 h2_stream_refusal_percent: self.h2_stream_refusal_percent,
1676 strict_sni_binding: self.strict_sni_binding,
1677 disable_http11: self.disable_http11,
1678 h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1679 h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1680 sozu_id_header: self.sozu_id_header.clone(),
1681 elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1682 send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1683 forwarded_headers: Some(ForwardedHeaders::from(
1684 self.forwarded_headers.unwrap_or_default(),
1685 ) as i32),
1686 hsts: match self.hsts.as_ref() {
1687 Some(h) => Some(h.to_proto("listener")?),
1688 None => None,
1689 },
1690 client_auth,
1691 client_ca_certificates,
1692 client_ca_crls,
1693 };
1694
1695 debug_assert_eq!(
1699 https_listener_config.address,
1700 self.address.into(),
1701 "HTTPS listener must bind the requested address"
1702 );
1703 debug_assert!(
1704 !https_listener_config.active,
1705 "a freshly built HTTPS listener must start inactive"
1706 );
1707 debug_assert!(
1712 !https_listener_config.alpn_protocols.is_empty(),
1713 "resolved ALPN list must not be empty"
1714 );
1715 debug_assert!(
1716 https_listener_config
1717 .alpn_protocols
1718 .iter()
1719 .all(|p| p == "h2" || p == "http/1.1"),
1720 "resolved ALPN list must contain only h2 and http/1.1"
1721 );
1722 debug_assert!(
1723 {
1724 let mut seen = std::collections::HashSet::new();
1725 https_listener_config
1726 .alpn_protocols
1727 .iter()
1728 .all(|p| seen.insert(p))
1729 },
1730 "resolved ALPN list must be duplicate-free"
1731 );
1732 debug_assert!(
1735 !(self.disable_http11.unwrap_or(false)
1736 && https_listener_config
1737 .alpn_protocols
1738 .iter()
1739 .any(|p| p == "http/1.1")),
1740 "disable_http11 with http/1.1 in ALPN must have been rejected"
1741 );
1742 Ok(https_listener_config)
1743 }
1744
1745 pub fn to_tcp(&mut self, config: Option<&Config>) -> Result<TcpListenerConfig, ConfigError> {
1747 if self.protocol != Some(ListenerProtocol::Tcp) {
1748 return Err(ConfigError::WrongListenerProtocol {
1749 expected: ListenerProtocol::Tcp,
1750 found: self.protocol.to_owned(),
1751 });
1752 }
1753 self.reject_client_auth_fields("TCP")?;
1754
1755 if let Some(config) = config {
1756 self.assign_config_timeouts(config);
1757 }
1758
1759 let tcp_listener_config = TcpListenerConfig {
1760 interface: self.validated_interface()?,
1761 address: self.address.into(),
1762 public_address: self.public_address.map(|a| a.into()),
1763 expect_proxy: self.expect_proxy.unwrap_or(false),
1764 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1765 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1766 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1767 active: false,
1768 sni_preread_timeout: Some(
1769 self.sni_preread_timeout
1770 .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT),
1771 ),
1772 sni_preread_max_bytes: Some(
1773 self.sni_preread_max_bytes
1774 .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES),
1775 ),
1776 };
1777
1778 debug_assert_eq!(
1781 tcp_listener_config.address,
1782 self.address.into(),
1783 "TCP listener must bind the requested address"
1784 );
1785 debug_assert!(
1786 !tcp_listener_config.active,
1787 "a freshly built TCP listener must start inactive"
1788 );
1789 Ok(tcp_listener_config)
1790 }
1791
1792 pub fn to_udp(&mut self, config: Option<&Config>) -> Result<UdpListenerConfig, ConfigError> {
1810 if self.protocol != Some(ListenerProtocol::Udp) {
1811 return Err(ConfigError::WrongListenerProtocol {
1812 expected: ListenerProtocol::Udp,
1813 found: self.protocol.to_owned(),
1814 });
1815 }
1816 self.reject_client_auth_fields("UDP")?;
1817
1818 let mut max_rx_datagram_size = self
1819 .max_rx_datagram_size
1820 .unwrap_or(DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE);
1821 let buffer_size = config.map(|c| c.buffer_size).unwrap_or(DEFAULT_BUFFER_SIZE);
1822 if u64::from(max_rx_datagram_size) > buffer_size {
1823 warn!(
1824 "UDP listener {}: max_rx_datagram_size = {} exceeds buffer_size = {}, clamping to buffer_size",
1825 self.address, max_rx_datagram_size, buffer_size
1826 );
1827 max_rx_datagram_size = buffer_size as u32;
1828 }
1829
1830 let max_flows = self.max_flows.unwrap_or(DEFAULT_UDP_MAX_FLOWS);
1831 if max_flows > 0
1832 && let Some(soft_limit) = soft_rlimit_nofile()
1833 {
1834 let advisory = soft_limit.saturating_mul(7) / 10;
1835 if u64::from(max_flows) > advisory {
1836 warn!(
1837 "UDP listener {}: max_flows = {} exceeds ~70% of the soft RLIMIT_NOFILE ({}); \
1838 per-flow connected sockets may hit EMFILE",
1839 self.address, max_flows, advisory
1840 );
1841 }
1842 }
1843
1844 Ok(UdpListenerConfig {
1845 interface: self.validated_interface()?,
1846 address: self.address.into(),
1847 public_address: self.public_address.map(|a| a.into()),
1848 front_timeout: self.front_timeout.unwrap_or(DEFAULT_UDP_FRONT_TIMEOUT),
1849 back_timeout: self.back_timeout.unwrap_or(DEFAULT_UDP_BACK_TIMEOUT),
1850 max_rx_datagram_size,
1851 max_flows,
1852 active: false,
1853 })
1854 }
1855}
1856
1857fn soft_rlimit_nofile() -> Option<u64> {
1862 let mut limit = libc::rlimit {
1863 rlim_cur: 0,
1864 rlim_max: 0,
1865 };
1866 let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) };
1870 if rc == 0 {
1871 Some(limit.rlim_cur)
1874 } else {
1875 None
1876 }
1877}
1878
1879fn read_http_answer_file(path: &Option<String>) -> Result<Option<String>, ConfigError> {
1881 match path {
1882 Some(path) => {
1883 let mut content = String::new();
1884 let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
1885 path_to_open: path.to_owned(),
1886 io_error,
1887 })?;
1888
1889 file.read_to_string(&mut content)
1890 .map_err(|io_error| ConfigError::FileRead {
1891 path_to_read: path.to_owned(),
1892 io_error,
1893 })?;
1894
1895 Ok(Some(content))
1896 }
1897 None => Ok(None),
1898 }
1899}
1900
1901pub fn resolve_answer_source(value: &str) -> Result<String, ConfigError> {
1924 if let Some(path) = value.strip_prefix("file://") {
1925 let mut content = String::new();
1926 let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
1927 path_to_open: path.to_owned(),
1928 io_error,
1929 })?;
1930 file.read_to_string(&mut content)
1931 .map_err(|io_error| ConfigError::FileRead {
1932 path_to_read: path.to_owned(),
1933 io_error,
1934 })?;
1935 return Ok(content);
1936 }
1937 Ok(value.to_owned())
1938}
1939
1940pub fn load_answers(
1957 answers: &BTreeMap<String, String>,
1958) -> Result<BTreeMap<String, String>, ConfigError> {
1959 let mut out = BTreeMap::new();
1960 for (code, value) in answers {
1961 if value.is_empty() {
1962 continue;
1963 }
1964 out.insert(code.to_owned(), resolve_answer_source(value)?);
1965 }
1966 debug_assert!(
1970 out.len() <= answers.len(),
1971 "load_answers must not synthesize entries"
1972 );
1973 debug_assert!(
1974 out.keys().all(|k| answers.contains_key(k)),
1975 "every loaded status code must come from the input map"
1976 );
1977 Ok(out)
1978}
1979
1980#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
1993#[serde(rename_all = "lowercase")]
1994#[derive(Default)]
1995pub enum MetricDetailLevel {
1996 Process,
1997 Frontend,
1998 #[default]
1999 Cluster,
2000 Backend,
2001}
2002
2003impl From<MetricDetailLevel> for MetricDetail {
2004 fn from(level: MetricDetailLevel) -> Self {
2005 match level {
2006 MetricDetailLevel::Process => MetricDetail::DetailProcess,
2007 MetricDetailLevel::Frontend => MetricDetail::DetailFrontend,
2008 MetricDetailLevel::Cluster => MetricDetail::DetailCluster,
2009 MetricDetailLevel::Backend => MetricDetail::DetailBackend,
2010 }
2011 }
2012}
2013
2014impl From<MetricDetail> for MetricDetailLevel {
2015 fn from(detail: MetricDetail) -> Self {
2019 match detail {
2020 MetricDetail::DetailProcess => MetricDetailLevel::Process,
2021 MetricDetail::DetailFrontend => MetricDetailLevel::Frontend,
2022 MetricDetail::DetailCluster => MetricDetailLevel::Cluster,
2023 MetricDetail::DetailBackend => MetricDetailLevel::Backend,
2024 }
2025 }
2026}
2027
2028#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
2043#[serde(rename_all = "snake_case")]
2044pub enum ForwardedHeadersMode {
2045 #[default]
2046 Both,
2047 XForwarded,
2048 Rfc7239,
2049 None,
2050}
2051
2052impl From<ForwardedHeadersMode> for ForwardedHeaders {
2053 fn from(mode: ForwardedHeadersMode) -> Self {
2054 match mode {
2055 ForwardedHeadersMode::Both => ForwardedHeaders::Both,
2056 ForwardedHeadersMode::XForwarded => ForwardedHeaders::XForwarded,
2057 ForwardedHeadersMode::Rfc7239 => ForwardedHeaders::Rfc7239,
2058 ForwardedHeadersMode::None => ForwardedHeaders::None,
2059 }
2060 }
2061}
2062
2063impl From<ForwardedHeaders> for ForwardedHeadersMode {
2064 fn from(headers: ForwardedHeaders) -> Self {
2065 match headers {
2066 ForwardedHeaders::Both => ForwardedHeadersMode::Both,
2067 ForwardedHeaders::XForwarded => ForwardedHeadersMode::XForwarded,
2068 ForwardedHeaders::Rfc7239 => ForwardedHeadersMode::Rfc7239,
2069 ForwardedHeaders::None => ForwardedHeadersMode::None,
2070 }
2071 }
2072}
2073
2074#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2075#[serde(deny_unknown_fields)]
2076pub struct MetricsConfig {
2077 pub address: SocketAddr,
2078 #[serde(default)]
2079 pub tagged_metrics: bool,
2080 #[serde(default)]
2081 pub prefix: Option<String>,
2082 #[serde(default)]
2085 pub detail: MetricDetailLevel,
2086}
2087
2088#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2089#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2090#[serde(deny_unknown_fields)]
2091pub enum PathRuleType {
2092 Prefix,
2093 Regex,
2094 Equals,
2095}
2096
2097#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2098#[serde(deny_unknown_fields)]
2099pub struct FileClusterFrontendConfig {
2100 pub address: SocketAddr,
2101 pub hostname: Option<String>,
2102 #[serde(default)]
2110 pub alpn: Vec<String>,
2111 pub path: Option<String>,
2113 pub path_type: Option<PathRuleType>,
2115 pub method: Option<String>,
2116 pub certificate: Option<String>,
2117 pub key: Option<String>,
2118 pub certificate_chain: Option<String>,
2119 #[serde(default)]
2120 pub tls_versions: Vec<TlsVersion>,
2121 #[serde(default)]
2122 pub position: RulePosition,
2123 pub tags: Option<BTreeMap<String, String>>,
2124 pub redirect: Option<String>,
2129 pub redirect_scheme: Option<String>,
2133 pub redirect_template: Option<String>,
2137 pub rewrite_host: Option<String>,
2140 pub rewrite_path: Option<String>,
2142 pub rewrite_port: Option<u32>,
2144 pub required_auth: Option<bool>,
2148 pub headers: Option<Vec<HeaderEditConfig>>,
2152 pub hsts: Option<FileHstsConfig>,
2157}
2158
2159#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2166#[serde(deny_unknown_fields)]
2167pub struct HeaderEditConfig {
2168 pub position: String,
2169 pub key: String,
2170 pub value: String,
2171}
2172
2173#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2184#[serde(deny_unknown_fields)]
2185pub struct FileHstsConfig {
2186 pub enabled: Option<bool>,
2189 pub max_age: Option<u32>,
2194 pub include_subdomains: Option<bool>,
2196 pub preload: Option<bool>,
2199 pub force_replace_backend: Option<bool>,
2207}
2208
2209impl FileHstsConfig {
2210 pub fn to_proto(&self, scope: &str) -> Result<HstsConfig, ConfigError> {
2228 let enabled = match self.enabled {
2229 Some(v) => v,
2230 None => return Err(ConfigError::HstsEnabledRequired(scope.to_owned())),
2231 };
2232
2233 let max_age = match (enabled, self.max_age) {
2234 (true, None) => Some(DEFAULT_HSTS_MAX_AGE),
2235 (_, m) => m,
2236 };
2237
2238 if let Some(value) = max_age
2239 && value > 0
2240 && value < 86_400
2241 {
2242 warn!(
2243 "HSTS max_age = {}s on {} is below 1 day — this is almost certainly a \
2244 misconfiguration. RFC 6797 §11.4 reserves max_age = 0 as the explicit kill \
2245 switch.",
2246 value, scope
2247 );
2248 }
2249
2250 let include_subdomains = self.include_subdomains;
2251 let preload = self.preload;
2252
2253 if matches!(preload, Some(true)) {
2254 let max_age_value = max_age.unwrap_or(0);
2255 if max_age_value < DEFAULT_HSTS_MAX_AGE {
2256 warn!(
2257 "HSTS preload = true on {} with max_age = {}s; the Chrome HSTS preload \
2258 list requires max_age >= {} (https://hstspreload.org/).",
2259 scope, max_age_value, DEFAULT_HSTS_MAX_AGE
2260 );
2261 }
2262 if include_subdomains != Some(true) {
2263 warn!(
2264 "HSTS preload = true on {} without include_subdomains = true; the Chrome \
2265 HSTS preload list requires includeSubDomains \
2266 (https://hstspreload.org/).",
2267 scope
2268 );
2269 }
2270 }
2271
2272 let config = HstsConfig {
2273 enabled: Some(enabled),
2274 max_age,
2275 include_subdomains,
2276 preload,
2277 force_replace_backend: self.force_replace_backend,
2278 };
2279
2280 debug_assert_eq!(
2285 config.enabled,
2286 Some(enabled),
2287 "built HSTS config must record the resolved enabled flag"
2288 );
2289 debug_assert!(
2290 !enabled || config.max_age.is_some(),
2291 "an enabled HSTS policy must carry a max_age"
2292 );
2293 Ok(config)
2294 }
2295}
2296
2297impl FileClusterFrontendConfig {
2298 pub fn to_tcp_front(&self) -> Result<TcpFrontendConfig, ConfigError> {
2299 if self.path.is_some() {
2300 return Err(ConfigError::InvalidFrontendConfig(
2301 "path_prefix".to_string(),
2302 ));
2303 }
2304 if self.certificate.is_some() {
2305 return Err(ConfigError::InvalidFrontendConfig(
2306 "certificate".to_string(),
2307 ));
2308 }
2309 if self.certificate_chain.is_some() {
2310 return Err(ConfigError::InvalidFrontendConfig(
2311 "certificate_chain".to_string(),
2312 ));
2313 }
2314
2315 let sni = match &self.hostname {
2320 Some(hostname) => Some(validate_sni_pattern(hostname)?),
2321 None => None,
2322 };
2323
2324 if sni.is_none() && !self.alpn.is_empty() {
2331 return Err(ConfigError::AlpnWithoutSni {
2332 address: self.address,
2333 });
2334 }
2335
2336 let tcp_front = TcpFrontendConfig {
2337 address: self.address,
2338 tags: self.tags.clone(),
2339 sni,
2340 alpn: self.alpn.clone(),
2341 udp: false,
2344 };
2345 debug_assert_eq!(
2352 tcp_front.address, self.address,
2353 "TCP frontend must bind the requested address"
2354 );
2355 debug_assert!(
2356 self.path.is_none() && self.certificate.is_none() && self.certificate_chain.is_none(),
2357 "a built TCP frontend must carry no HTTP-only attributes"
2358 );
2359 debug_assert!(
2360 tcp_front.sni.is_some() || tcp_front.alpn.is_empty(),
2361 "a built TCP frontend without sni must never carry a non-empty alpn"
2362 );
2363 Ok(tcp_front)
2364 }
2365
2366 pub fn to_http_front(&self, _cluster_id: &str) -> Result<HttpFrontendConfig, ConfigError> {
2367 if !self.alpn.is_empty() {
2368 return Err(ConfigError::InvalidFrontendConfig("alpn".to_string()));
2369 }
2370
2371 let hostname = match &self.hostname {
2372 Some(hostname) => hostname.to_owned(),
2373 None => {
2374 return Err(ConfigError::Missing(MissingKind::Field(
2375 "hostname".to_string(),
2376 )));
2377 }
2378 };
2379
2380 let key_opt = match self.key.as_ref() {
2381 None => None,
2382 Some(path) => {
2383 let key = Config::load_file(path)?;
2384 Some(key)
2385 }
2386 };
2387
2388 let certificate_opt = match self.certificate.as_ref() {
2389 None => None,
2390 Some(path) => {
2391 let certificate = Config::load_file(path)?;
2392 Some(certificate)
2393 }
2394 };
2395
2396 let certificate_chain = match self.certificate_chain.as_ref() {
2397 None => None,
2398 Some(path) => {
2399 let certificate_chain = Config::load_file(path)?;
2400 Some(split_certificate_chain(certificate_chain))
2401 }
2402 };
2403
2404 let path = match (self.path.as_ref(), self.path_type.as_ref()) {
2405 (None, _) => PathRule::prefix("".to_string()),
2406 (Some(s), Some(PathRuleType::Prefix)) => PathRule::prefix(s.to_string()),
2407 (Some(s), Some(PathRuleType::Regex)) => PathRule::regex(s.to_string()),
2408 (Some(s), Some(PathRuleType::Equals)) => PathRule::equals(s.to_string()),
2409 (Some(s), None) => PathRule::prefix(s.clone()),
2410 };
2411
2412 let redirect = match self.redirect.as_deref() {
2413 Some(v) => Some(parse_redirect_policy(v)?),
2414 None => None,
2415 };
2416 let redirect_scheme = match self.redirect_scheme.as_deref() {
2417 Some(v) => Some(parse_redirect_scheme(v)?),
2418 None => None,
2419 };
2420
2421 let headers = match self.headers.as_ref() {
2422 Some(entries) => {
2423 let mut out = Vec::with_capacity(entries.len());
2424 for (index, entry) in entries.iter().enumerate() {
2425 out.push(parse_header_edit(index, entry)?);
2426 }
2427 out
2428 }
2429 None => Vec::new(),
2430 };
2431
2432 let frontend_serves_https = key_opt.is_some() && certificate_opt.is_some();
2439 let hsts = match self.hsts.as_ref() {
2440 Some(h) => {
2441 if !frontend_serves_https {
2442 return Err(ConfigError::HstsOnPlainHttp(format!(
2443 "frontend {_cluster_id}/{hostname}"
2444 )));
2445 }
2446 Some(h.to_proto(&format!("frontend {_cluster_id}/{hostname}"))?)
2447 }
2448 None => None,
2449 };
2450
2451 Ok(HttpFrontendConfig {
2452 address: self.address,
2453 hostname,
2454 certificate: certificate_opt,
2455 key: key_opt,
2456 certificate_chain,
2457 tls_versions: self.tls_versions.clone(),
2458 position: self.position,
2459 path,
2460 method: self.method.clone(),
2461 tags: self.tags.clone(),
2462 redirect,
2463 redirect_scheme,
2464 redirect_template: self.redirect_template.clone(),
2465 rewrite_host: self.rewrite_host.clone(),
2466 rewrite_path: self.rewrite_path.clone(),
2467 rewrite_port: self.rewrite_port,
2468 required_auth: self.required_auth,
2469 headers,
2470 hsts,
2471 })
2472 }
2473}
2474
2475pub fn validate_sni_pattern(sni: &str) -> Result<String, ConfigError> {
2496 let invalid = || ConfigError::InvalidSniPattern {
2497 sni: sni.to_string(),
2498 };
2499
2500 if sni.is_empty() {
2501 return Err(invalid());
2502 }
2503
2504 if !sni.is_ascii() {
2505 return Err(ConfigError::NonAsciiSniPattern {
2506 sni: sni.to_string(),
2507 });
2508 }
2509
2510 let remainder = sni.strip_prefix("*.").unwrap_or(sni);
2514
2515 if remainder.is_empty() || remainder.contains('*') || remainder.contains('/') {
2522 return Err(invalid());
2523 }
2524 if remainder.split('.').any(|label| label.is_empty()) {
2525 return Err(invalid());
2526 }
2527
2528 let normalized = sni.to_ascii_lowercase();
2529 debug_assert!(
2535 normalized.matches('*').count() <= 1,
2536 "a validated SNI pattern must carry at most one wildcard marker"
2537 );
2538 debug_assert!(
2539 normalized.is_ascii(),
2540 "a validated SNI pattern must be pure ASCII"
2541 );
2542 Ok(normalized)
2543}
2544
2545pub(crate) fn parse_redirect_policy(value: &str) -> Result<RedirectPolicy, ConfigError> {
2547 match value.to_ascii_lowercase().as_str() {
2548 "forward" => Ok(RedirectPolicy::Forward),
2549 "permanent" => Ok(RedirectPolicy::Permanent),
2550 "unauthorized" => Ok(RedirectPolicy::Unauthorized),
2551 _ => Err(ConfigError::InvalidRedirectPolicy(value.to_owned())),
2552 }
2553}
2554
2555pub(crate) fn parse_redirect_scheme(value: &str) -> Result<RedirectScheme, ConfigError> {
2557 match value.to_ascii_lowercase().as_str() {
2558 "use-same" | "use_same" => Ok(RedirectScheme::UseSame),
2559 "use-http" | "use_http" => Ok(RedirectScheme::UseHttp),
2560 "use-https" | "use_https" => Ok(RedirectScheme::UseHttps),
2561 _ => Err(ConfigError::InvalidRedirectScheme(value.to_owned())),
2562 }
2563}
2564
2565pub(crate) fn parse_header_edit(
2572 index: usize,
2573 entry: &HeaderEditConfig,
2574) -> Result<Header, ConfigError> {
2575 let position = match entry.position.to_ascii_lowercase().as_str() {
2576 "request" => HeaderPosition::Request,
2577 "response" => HeaderPosition::Response,
2578 "both" => HeaderPosition::Both,
2579 _ => {
2580 return Err(ConfigError::InvalidHeaderPosition {
2581 index,
2582 position: entry.position.clone(),
2583 });
2584 }
2585 };
2586 if !header_name_is_valid_token(entry.key.as_bytes()) {
2587 return Err(ConfigError::InvalidHeaderBytes {
2588 index,
2589 field: "key",
2590 });
2591 }
2592 if header_value_contains_forbidden_controls(entry.value.as_bytes()) {
2593 return Err(ConfigError::InvalidHeaderBytes {
2594 index,
2595 field: "value",
2596 });
2597 }
2598 let header = Header {
2599 position: position as i32,
2600 key: entry.key.clone(),
2601 val: entry.value.clone(),
2602 };
2603 debug_assert!(
2609 header_name_is_valid_token(header.key.as_bytes()),
2610 "an emitted header key must be a valid token"
2611 );
2612 debug_assert!(
2613 !header_value_contains_forbidden_controls(header.val.as_bytes()),
2614 "an emitted header value must be free of forbidden control bytes"
2615 );
2616 Ok(header)
2617}
2618
2619pub(crate) fn header_name_is_valid_token(bytes: &[u8]) -> bool {
2627 if bytes.is_empty() {
2628 return false;
2629 }
2630 bytes.iter().all(|&b| is_tchar(b))
2631}
2632
2633fn is_tchar(b: u8) -> bool {
2636 b.is_ascii_alphanumeric()
2637 || matches!(
2638 b,
2639 b'!' | b'#'
2640 | b'$'
2641 | b'%'
2642 | b'&'
2643 | b'\''
2644 | b'*'
2645 | b'+'
2646 | b'-'
2647 | b'.'
2648 | b'^'
2649 | b'_'
2650 | b'`'
2651 | b'|'
2652 | b'~'
2653 )
2654}
2655
2656pub(crate) fn header_value_contains_forbidden_controls(bytes: &[u8]) -> bool {
2664 bytes
2665 .iter()
2666 .any(|&b| matches!(b, 0x00..=0x08 | 0x0A..=0x1F | 0x7F))
2667}
2668
2669#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2670#[serde(deny_unknown_fields, rename_all = "lowercase")]
2671pub enum ListenerProtocol {
2672 Http,
2673 Https,
2674 Tcp,
2675 Udp,
2676}
2677
2678#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2679#[serde(deny_unknown_fields, rename_all = "lowercase")]
2680pub enum FileClusterProtocolConfig {
2681 Http,
2682 Tcp,
2683}
2684
2685fn default_health_check_interval() -> u32 {
2686 10
2687}
2688fn default_health_check_timeout() -> u32 {
2689 5
2690}
2691fn default_health_check_threshold() -> u32 {
2692 3
2693}
2694fn default_health_check_uri() -> String {
2695 "/".to_owned()
2696}
2697
2698#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2699#[serde(deny_unknown_fields)]
2700pub struct FileHealthCheckConfig {
2701 #[serde(default = "default_health_check_uri")]
2704 pub uri: String,
2705 #[serde(default = "default_health_check_interval")]
2706 pub interval: u32,
2707 #[serde(default = "default_health_check_timeout")]
2708 pub timeout: u32,
2709 #[serde(default = "default_health_check_threshold")]
2710 pub healthy_threshold: u32,
2711 #[serde(default = "default_health_check_threshold")]
2712 pub unhealthy_threshold: u32,
2713 #[serde(default)]
2714 pub expected_status: u32,
2715 #[serde(default)]
2718 pub mode: Option<HealthCheckMode>,
2719 #[serde(default)]
2722 pub accepted_statuses: Vec<String>,
2723}
2724
2725impl FileHealthCheckConfig {
2726 pub fn to_proto(&self) -> Result<HealthCheckConfig, &'static str> {
2730 let accepted_statuses = self
2731 .accepted_statuses
2732 .iter()
2733 .map(|spec| parse_http_status_range(spec))
2734 .collect::<Result<Vec<_>, _>>()?;
2735 let proto = HealthCheckConfig {
2736 uri: self.uri.to_owned(),
2737 interval: self.interval,
2738 timeout: self.timeout,
2739 healthy_threshold: self.healthy_threshold,
2740 unhealthy_threshold: self.unhealthy_threshold,
2741 expected_status: self.expected_status,
2742 mode: self.mode.unwrap_or(HealthCheckMode::Http) as i32,
2743 accepted_statuses,
2744 };
2745 debug_assert_eq!(proto.uri, self.uri, "proto URI must mirror the file config");
2749 debug_assert!(
2750 proto.interval == self.interval
2751 && proto.timeout == self.timeout
2752 && proto.healthy_threshold == self.healthy_threshold
2753 && proto.unhealthy_threshold == self.unhealthy_threshold,
2754 "proto timing knobs must mirror the file config"
2755 );
2756 debug_assert_eq!(
2757 proto.accepted_statuses.len(),
2758 self.accepted_statuses.len(),
2759 "every accepted status spec must map to exactly one range"
2760 );
2761 validate_health_check_config(&proto)?;
2762 Ok(proto)
2763 }
2764}
2765
2766pub const HTTP_STATUS_MIN: u32 = 100;
2769pub const HTTP_STATUS_MAX: u32 = 599;
2770
2771pub fn parse_http_status_range(spec: &str) -> Result<HttpStatusRange, &'static str> {
2780 const INVALID: &str = "health check accepted status must be a code (\"404\"), a range \
2781 (\"200-399\"), a class (\"2xx\") or \"any\"";
2782 let spec = spec.trim();
2783 let range = if spec.eq_ignore_ascii_case("any") {
2784 HttpStatusRange {
2785 start: HTTP_STATUS_MIN,
2786 end: HTTP_STATUS_MAX,
2787 }
2788 } else if let [class @ b'1'..=b'5', x1, x2] = spec.as_bytes()
2789 && x1.eq_ignore_ascii_case(&b'x')
2790 && x2.eq_ignore_ascii_case(&b'x')
2791 {
2792 let start = u32::from(class - b'0') * 100;
2793 HttpStatusRange {
2794 start,
2795 end: start + 99,
2796 }
2797 } else if let Some((start, end)) = spec.split_once('-') {
2798 HttpStatusRange {
2799 start: parse_status_code(start).ok_or(INVALID)?,
2800 end: parse_status_code(end).ok_or(INVALID)?,
2801 }
2802 } else {
2803 let code = parse_status_code(spec).ok_or(INVALID)?;
2804 HttpStatusRange {
2805 start: code,
2806 end: code,
2807 }
2808 };
2809 validate_http_status_range(&range)?;
2810 Ok(range)
2811}
2812
2813fn parse_status_code(code: &str) -> Option<u32> {
2816 let code = code.trim();
2817 if code.is_empty() || !code.bytes().all(|b| b.is_ascii_digit()) {
2818 return None;
2819 }
2820 code.parse().ok()
2821}
2822
2823fn validate_http_status_range(range: &HttpStatusRange) -> Result<(), &'static str> {
2824 if range.start < HTTP_STATUS_MIN || range.end > HTTP_STATUS_MAX {
2825 return Err("health check accepted statuses must lie within 100-599");
2826 }
2827 if range.start > range.end {
2828 return Err("health check accepted status range must not end before it starts");
2829 }
2830 debug_assert!(
2831 (HTTP_STATUS_MIN..=HTTP_STATUS_MAX).contains(&range.start)
2832 && (HTTP_STATUS_MIN..=HTTP_STATUS_MAX).contains(&range.end),
2833 "a validated range must lie within the valid status codes"
2834 );
2835 Ok(())
2836}
2837
2838pub fn validate_affinity_key(
2855 affinity_header: Option<&str>,
2856 affinity_cookie: Option<&str>,
2857) -> Result<(), &'static str> {
2858 match (affinity_header, affinity_cookie) {
2859 (Some(_), Some(_)) => {
2860 Err("affinity_header and affinity_cookie are mutually exclusive: set at most one")
2861 }
2862 (Some(name), None) if !header_name_is_valid_token(name.as_bytes()) => {
2863 Err("affinity_header must be a non-empty RFC 9110 token (a header field name)")
2864 }
2865 (Some(name), None) if name.eq_ignore_ascii_case("host") => Err(
2866 "affinity_header cannot be Host: the request authority is not a header Sōzu keeps \
2867 (H1 folds Host into it, H2 sends :authority)",
2868 ),
2869 (Some(name), None) if name.eq_ignore_ascii_case("cookie") => Err(
2870 "affinity_header cannot be Cookie: cookies are parsed into a jar; key on one with \
2871 affinity_cookie instead",
2872 ),
2873 (None, Some(name)) if !header_name_is_valid_token(name.as_bytes()) => {
2874 Err("affinity_cookie must be a non-empty RFC 6265 cookie name (an RFC 9110 token)")
2875 }
2876 _ => Ok(()),
2877 }
2878}
2879
2880pub const DEFAULT_SHARD_MIN_BACKENDS: u32 = 8;
2886
2887pub fn validate_shuffle_sharding(
2895 shard_percent: Option<u32>,
2896 shard_min_backends: Option<u32>,
2897 shard_mode: Option<i32>,
2898) -> Result<(), &'static str> {
2899 let Some(percent) = shard_percent else {
2900 if shard_min_backends.is_some() || shard_mode.is_some() {
2901 return Err("shard_min_backends and shard_mode require shard_percent");
2902 }
2903 return Ok(());
2904 };
2905 if !(1..=100).contains(&percent) {
2906 return Err("shard_percent must lie in 1..=100");
2907 }
2908 if shard_min_backends.is_some_and(|minimum| minimum < 2) {
2909 return Err("shard_min_backends must be at least 2, the smallest shard");
2910 }
2911 if shard_mode.is_some_and(|mode| ShardMode::try_from(mode).is_err()) {
2912 return Err("shard_mode must be FALLBACK or STRICT");
2913 }
2914 Ok(())
2915}
2916
2917pub fn validate_health_check_config(cfg: &HealthCheckConfig) -> Result<(), &'static str> {
2932 let Ok(mode) = HealthCheckMode::try_from(cfg.mode) else {
2933 return Err("health check mode must be HTTP or TCP");
2934 };
2935 if cfg.interval == 0 {
2936 return Err("health check interval must be > 0");
2937 }
2938 if cfg.timeout == 0 {
2939 return Err("health check timeout must be > 0");
2940 }
2941 if cfg.healthy_threshold == 0 {
2942 return Err("health check healthy_threshold must be > 0");
2943 }
2944 if cfg.unhealthy_threshold == 0 {
2945 return Err("health check unhealthy_threshold must be > 0");
2946 }
2947 if mode == HealthCheckMode::Http && !cfg.uri.starts_with('/') {
2952 return Err("health check URI must start with '/'");
2953 }
2954 if cfg
2955 .uri
2956 .bytes()
2957 .any(|b| b == b'\r' || b == b'\n' || b == 0 || (b < 0x20 && b != b'\t'))
2958 {
2959 return Err("health check URI must not contain CR, LF, NUL, or other C0 control bytes");
2960 }
2961 for range in &cfg.accepted_statuses {
2962 validate_http_status_range(range)?;
2963 }
2964 if cfg.expected_status != 0 && !cfg.accepted_statuses.is_empty() {
2965 return Err(
2966 "health check expected_status and accepted_statuses are mutually exclusive: \
2967 set at most one",
2968 );
2969 }
2970 if mode == HealthCheckMode::Tcp
2973 && (cfg.expected_status != 0 || !cfg.accepted_statuses.is_empty())
2974 {
2975 return Err(
2976 "health check expected_status and accepted_statuses apply to HTTP mode only: \
2977 leave them unset with mode TCP",
2978 );
2979 }
2980 debug_assert!(
2986 cfg.interval > 0
2987 && cfg.timeout > 0
2988 && cfg.healthy_threshold > 0
2989 && cfg.unhealthy_threshold > 0,
2990 "validated health-check thresholds must all be strictly positive"
2991 );
2992 debug_assert!(
2993 mode == HealthCheckMode::Tcp || cfg.uri.starts_with('/'),
2994 "a validated HTTP health-check URI must be an absolute path"
2995 );
2996 debug_assert!(
2997 cfg.expected_status == 0 || cfg.accepted_statuses.is_empty(),
2998 "a validated health check sets at most one of expected_status and accepted_statuses"
2999 );
3000 debug_assert!(
3001 mode == HealthCheckMode::Http
3002 || (cfg.expected_status == 0 && cfg.accepted_statuses.is_empty()),
3003 "a validated TCP health check sets no status field"
3004 );
3005 Ok(())
3006}
3007
3008#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3009#[serde(deny_unknown_fields)]
3010pub struct FileClusterConfig {
3011 pub frontends: Vec<FileClusterFrontendConfig>,
3012 pub backends: Vec<BackendConfig>,
3013 pub protocol: FileClusterProtocolConfig,
3014 pub sticky_session: Option<bool>,
3015 pub https_redirect: Option<bool>,
3016 #[serde(default)]
3017 pub send_proxy: Option<bool>,
3018 #[serde(default)]
3019 pub load_balancing: LoadBalancingAlgorithms,
3020 pub answer_503: Option<String>,
3021 #[serde(default)]
3022 pub load_metric: Option<LoadMetric>,
3023 pub http2: Option<bool>,
3026 pub answers: Option<BTreeMap<String, String>>,
3037 pub https_redirect_port: Option<u32>,
3042 pub authorized_hashes: Option<Vec<String>>,
3047 pub www_authenticate: Option<String>,
3051 pub max_connections_per_ip: Option<u64>,
3060 pub max_connections_per_subnet: Option<u64>,
3068 pub retry_after: Option<u32>,
3074 #[serde(default)]
3078 pub health_check: Option<FileHealthCheckConfig>,
3079 #[serde(default)]
3083 pub udp: Option<FileUdpClusterConfig>,
3084 #[serde(default)]
3090 pub affinity_header: Option<String>,
3091 #[serde(default)]
3093 pub affinity_cookie: Option<String>,
3094 #[serde(default)]
3099 pub shard_percent: Option<u32>,
3100 #[serde(default)]
3103 pub shard_min_backends: Option<u32>,
3104 #[serde(default)]
3107 pub shard_mode: Option<ShardMode>,
3108 #[serde(default)]
3113 pub max_connection_attempts: Option<u32>,
3114}
3115
3116#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3119#[serde(deny_unknown_fields)]
3120pub struct FileUdpHealthConfig {
3121 pub mode: Option<UdpHealthMode>,
3125 pub tcp_port: Option<u32>,
3126 pub rise: Option<u32>,
3127 pub fall: Option<u32>,
3128 pub fail_open: Option<bool>,
3129 pub udp_probe_payload: Option<String>,
3131 pub probe_interval_seconds: Option<u32>,
3132 pub probe_timeout_seconds: Option<u32>,
3133}
3134
3135impl FileUdpHealthConfig {
3136 pub fn to_proto(&self) -> UdpHealthConfig {
3137 UdpHealthConfig {
3138 mode: self.mode.map(|m| m as i32),
3139 tcp_port: self.tcp_port,
3140 rise: self.rise,
3141 fall: self.fall,
3142 fail_open: self.fail_open,
3143 udp_probe_payload: self
3144 .udp_probe_payload
3145 .as_ref()
3146 .map(|p| p.as_bytes().to_owned()),
3147 probe_interval_seconds: self.probe_interval_seconds,
3148 probe_timeout_seconds: self.probe_timeout_seconds,
3149 }
3150 }
3151}
3152
3153#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3155#[serde(deny_unknown_fields)]
3156pub struct FileUdpClusterConfig {
3157 pub affinity_key: Option<UdpAffinityKey>,
3160 pub responses: Option<u32>,
3162 pub requests: Option<u32>,
3164 pub send_proxy_protocol: Option<bool>,
3166 pub proxy_protocol_every_datagram: Option<bool>,
3168 pub health: Option<FileUdpHealthConfig>,
3170}
3171
3172impl FileUdpClusterConfig {
3173 pub fn to_proto(&self) -> UdpClusterConfig {
3174 UdpClusterConfig {
3175 affinity_key: self.affinity_key.map(|k| k as i32),
3176 responses: self.responses,
3177 requests: self.requests,
3178 send_proxy_protocol: self.send_proxy_protocol,
3179 proxy_protocol_every_datagram: self.proxy_protocol_every_datagram,
3180 health: self.health.as_ref().map(|h| h.to_proto()),
3181 }
3182 }
3183}
3184
3185#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3186#[serde(deny_unknown_fields)]
3187pub struct BackendConfig {
3188 pub address: SocketAddr,
3189 pub weight: Option<u8>,
3190 pub sticky_id: Option<String>,
3191 pub backup: Option<bool>,
3192 pub backend_id: Option<String>,
3193}
3194
3195impl FileClusterConfig {
3196 pub fn to_cluster_config(
3197 self,
3198 cluster_id: &str,
3199 expect_proxy: &HashSet<SocketAddr>,
3200 ) -> Result<ClusterConfig, ConfigError> {
3201 let requested_frontend_count = self.frontends.len();
3204 validate_affinity_key(
3205 self.affinity_header.as_deref(),
3206 self.affinity_cookie.as_deref(),
3207 )
3208 .map_err(|reason| ConfigError::InvalidAffinityKey {
3209 cluster_id: cluster_id.to_owned(),
3210 reason,
3211 })?;
3212 validate_shuffle_sharding(
3213 self.shard_percent,
3214 self.shard_min_backends,
3215 self.shard_mode.map(|mode| mode as i32),
3216 )
3217 .map_err(|reason| ConfigError::InvalidShuffleSharding {
3218 cluster_id: cluster_id.to_owned(),
3219 reason,
3220 })?;
3221 if let Some(attempts) = self.max_connection_attempts {
3222 validate_max_connection_attempts(attempts).map_err(|reason| {
3223 ConfigError::InvalidMaxConnectionAttempts {
3224 cluster_id: Some(cluster_id.to_owned()),
3225 value: attempts,
3226 reason,
3227 }
3228 })?;
3229 }
3230 let health_check = self
3231 .health_check
3232 .as_ref()
3233 .map(FileHealthCheckConfig::to_proto)
3234 .transpose()
3235 .map_err(|reason| ConfigError::InvalidHealthCheck {
3236 cluster_id: cluster_id.to_owned(),
3237 reason,
3238 })?;
3239 match self.protocol {
3240 FileClusterProtocolConfig::Tcp => {
3241 if self.affinity_header.is_some() || self.affinity_cookie.is_some() {
3245 return Err(ConfigError::InvalidAffinityKey {
3246 cluster_id: cluster_id.to_owned(),
3247 reason: "a TCP cluster always keys on the client source IP: \
3248 affinity_header and affinity_cookie apply to HTTP clusters only",
3249 });
3250 }
3251 let mut has_expect_proxy = None;
3252 let mut frontends = Vec::new();
3253 for f in self.frontends {
3254 if expect_proxy.contains(&f.address) {
3255 match has_expect_proxy {
3256 Some(true) => {}
3257 Some(false) => {
3258 return Err(ConfigError::Incompatible {
3259 object: ObjectKind::Cluster,
3260 id: cluster_id.to_owned(),
3261 kind: IncompatibilityKind::ProxyProtocol,
3262 });
3263 }
3264 None => has_expect_proxy = Some(true),
3265 }
3266 } else {
3267 match has_expect_proxy {
3268 Some(false) => {}
3269 Some(true) => {
3270 return Err(ConfigError::Incompatible {
3271 object: ObjectKind::Cluster,
3272 id: cluster_id.to_owned(),
3273 kind: IncompatibilityKind::ProxyProtocol,
3274 });
3275 }
3276 None => has_expect_proxy = Some(false),
3277 }
3278 }
3279 let tcp_frontend = f.to_tcp_front()?;
3280 frontends.push(tcp_frontend);
3281 }
3282
3283 let send_proxy = self.send_proxy.unwrap_or(false);
3284 let expect_proxy = has_expect_proxy.unwrap_or(false);
3285 let proxy_protocol = match (send_proxy, expect_proxy) {
3286 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
3287 (true, false) => Some(ProxyProtocolConfig::SendHeader),
3288 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
3289 _ => None,
3290 };
3291
3292 let answers = match self.answers.as_ref() {
3293 Some(map) => load_answers(map)?,
3294 None => BTreeMap::new(),
3295 };
3296
3297 let udp = self.udp.as_ref().map(|u| u.to_proto());
3298 debug_assert_eq!(
3304 frontends.len(),
3305 requested_frontend_count,
3306 "every TCP frontend must survive conversion"
3307 );
3308 debug_assert_eq!(
3309 proxy_protocol,
3310 match (send_proxy, expect_proxy) {
3311 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
3312 (true, false) => Some(ProxyProtocolConfig::SendHeader),
3313 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
3314 (false, false) => None,
3315 },
3316 "proxy_protocol must be the (send, expect) function"
3317 );
3318
3319 Ok(ClusterConfig::Tcp(TcpClusterConfig {
3320 cluster_id: cluster_id.to_string(),
3321 frontends,
3322 backends: self.backends,
3323 proxy_protocol,
3324 load_balancing: self.load_balancing,
3325 load_metric: self.load_metric,
3326 answers,
3327 https_redirect_port: self.https_redirect_port,
3328 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
3329 www_authenticate: self.www_authenticate,
3330 max_connections_per_ip: self.max_connections_per_ip,
3331 max_connections_per_subnet: self.max_connections_per_subnet,
3332 retry_after: self.retry_after,
3333 health_check,
3334 udp,
3335 shard_percent: self.shard_percent,
3336 shard_min_backends: self.shard_min_backends,
3337 shard_mode: self.shard_mode,
3338 max_connection_attempts: self.max_connection_attempts,
3339 }))
3340 }
3341 FileClusterProtocolConfig::Http => {
3342 let mut frontends = Vec::new();
3343 for frontend in self.frontends {
3344 let http_frontend = frontend.to_http_front(cluster_id)?;
3345 frontends.push(http_frontend);
3346 }
3347
3348 let answer_503 = self.answer_503.as_ref().and_then(|path| {
3349 Config::load_file(path)
3350 .map_err(|e| {
3351 error!("cannot load 503 error page at path '{}': {:?}", path, e);
3352 e
3353 })
3354 .ok()
3355 });
3356
3357 let answers = match self.answers.as_ref() {
3358 Some(map) => load_answers(map)?,
3359 None => BTreeMap::new(),
3360 };
3361
3362 let udp = self.udp.as_ref().map(|u| u.to_proto());
3363 debug_assert_eq!(
3366 frontends.len(),
3367 requested_frontend_count,
3368 "every HTTP frontend must survive conversion"
3369 );
3370
3371 Ok(ClusterConfig::Http(HttpClusterConfig {
3372 cluster_id: cluster_id.to_string(),
3373 frontends,
3374 backends: self.backends,
3375 sticky_session: self.sticky_session.unwrap_or(false),
3376 https_redirect: self.https_redirect.unwrap_or(false),
3377 load_balancing: self.load_balancing,
3378 load_metric: self.load_metric,
3379 answer_503,
3380 http2: self.http2,
3381 answers,
3382 https_redirect_port: self.https_redirect_port,
3383 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
3384 www_authenticate: self.www_authenticate,
3385 max_connections_per_ip: self.max_connections_per_ip,
3386 max_connections_per_subnet: self.max_connections_per_subnet,
3387 retry_after: self.retry_after,
3388 health_check,
3389 udp,
3390 affinity_header: self.affinity_header,
3391 affinity_cookie: self.affinity_cookie,
3392 shard_percent: self.shard_percent,
3393 shard_min_backends: self.shard_min_backends,
3394 shard_mode: self.shard_mode,
3395 max_connection_attempts: self.max_connection_attempts,
3396 }))
3397 }
3398 }
3399 }
3400}
3401
3402#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3403#[serde(deny_unknown_fields)]
3404pub struct HttpFrontendConfig {
3405 pub address: SocketAddr,
3406 pub hostname: String,
3407 pub path: PathRule,
3408 pub method: Option<String>,
3409 pub certificate: Option<String>,
3410 pub key: Option<String>,
3411 pub certificate_chain: Option<Vec<String>>,
3412 #[serde(default)]
3413 pub tls_versions: Vec<TlsVersion>,
3414 #[serde(default)]
3415 pub position: RulePosition,
3416 pub tags: Option<BTreeMap<String, String>>,
3417 #[serde(default)]
3419 pub redirect: Option<RedirectPolicy>,
3420 #[serde(default)]
3422 pub redirect_scheme: Option<RedirectScheme>,
3423 #[serde(default)]
3424 pub redirect_template: Option<String>,
3425 #[serde(default)]
3426 pub rewrite_host: Option<String>,
3427 #[serde(default)]
3428 pub rewrite_path: Option<String>,
3429 #[serde(default)]
3430 pub rewrite_port: Option<u32>,
3431 #[serde(default)]
3432 pub required_auth: Option<bool>,
3433 #[serde(default)]
3436 pub headers: Vec<Header>,
3437 #[serde(default)]
3440 pub hsts: Option<HstsConfig>,
3441}
3442
3443impl fmt::Debug for HttpFrontendConfig {
3444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3445 let certificate = self.certificate.as_ref().map(|_| "[redacted]");
3446 let certificate_len = self.certificate.as_ref().map(String::len);
3447 let key = self.key.as_ref().map(|_| "[redacted]");
3448 let key_len = self.key.as_ref().map(String::len);
3449 let certificate_chain = self.certificate_chain.as_ref().map(|_| "[redacted]");
3450 let certificate_chain_count = self.certificate_chain.as_ref().map(Vec::len);
3451 let certificate_chain_len = self.certificate_chain.as_ref().map(|chain| {
3452 chain
3453 .iter()
3454 .map(String::len)
3455 .fold(0usize, usize::saturating_add)
3456 });
3457 let method_len = self.method.as_ref().map(String::len);
3458 let redirect_template_len = self.redirect_template.as_ref().map(String::len);
3459 let rewrite_host_len = self.rewrite_host.as_ref().map(String::len);
3460 let rewrite_path_len = self.rewrite_path.as_ref().map(String::len);
3461
3462 f.debug_struct("HttpFrontendConfig")
3463 .field("address", &self.address)
3464 .field("hostname_len", &self.hostname.len())
3465 .field("path_kind", &self.path.kind)
3466 .field("path_len", &self.path.value.len())
3467 .field("method_len", &method_len)
3468 .field("certificate", &certificate)
3469 .field("certificate_len", &certificate_len)
3470 .field("key", &key)
3471 .field("key_len", &key_len)
3472 .field("certificate_chain", &certificate_chain)
3473 .field("certificate_chain_count", &certificate_chain_count)
3474 .field("certificate_chain_len", &certificate_chain_len)
3475 .field("tls_versions_count", &self.tls_versions.len())
3476 .field("position", &self.position)
3477 .field(
3478 "tags_count",
3479 &self.tags.as_ref().map(BTreeMap::len).unwrap_or_default(),
3480 )
3481 .field("redirect", &self.redirect)
3482 .field("redirect_scheme", &self.redirect_scheme)
3483 .field("redirect_template_len", &redirect_template_len)
3484 .field("rewrite_host_len", &rewrite_host_len)
3485 .field("rewrite_path_len", &rewrite_path_len)
3486 .field("rewrite_port", &self.rewrite_port)
3487 .field("required_auth", &self.required_auth)
3488 .field("headers_count", &self.headers.len())
3489 .field("hsts", &self.hsts)
3490 .finish()
3491 }
3492}
3493
3494impl HttpFrontendConfig {
3495 pub fn generate_requests(&self, cluster_id: &str) -> Vec<Request> {
3496 let mut v = Vec::new();
3497
3498 let tags = self.tags.clone().unwrap_or_default();
3499
3500 if self.key.is_some() && self.certificate.is_some() {
3501 v.push(
3502 RequestType::AddCertificate(AddCertificate {
3503 address: self.address.into(),
3504 certificate: CertificateAndKey {
3505 key: self.key.clone().unwrap(),
3506 certificate: self.certificate.clone().unwrap(),
3507 certificate_chain: self.certificate_chain.clone().unwrap_or_default(),
3508 versions: self.tls_versions.iter().map(|v| *v as i32).collect(),
3509 names: vec![],
3514 },
3515 expired_at: None,
3516 })
3517 .into(),
3518 );
3519
3520 v.push(
3521 RequestType::AddHttpsFrontend(RequestHttpFrontend {
3522 cluster_id: Some(cluster_id.to_string()),
3523 address: self.address.into(),
3524 hostname: self.hostname.clone(),
3525 path: self.path.clone(),
3526 method: self.method.clone(),
3527 position: self.position.into(),
3528 tags,
3529 redirect: self.redirect.map(|r| r as i32),
3530 required_auth: self.required_auth,
3531 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
3532 redirect_template: self.redirect_template.clone(),
3533 rewrite_host: self.rewrite_host.clone(),
3534 rewrite_path: self.rewrite_path.clone(),
3535 rewrite_port: self.rewrite_port,
3536 headers: self.headers.clone(),
3537 hsts: self.hsts,
3538 })
3539 .into(),
3540 );
3541 } else {
3542 v.push(
3544 RequestType::AddHttpFrontend(RequestHttpFrontend {
3545 cluster_id: Some(cluster_id.to_string()),
3546 address: self.address.into(),
3547 hostname: self.hostname.clone(),
3548 path: self.path.clone(),
3549 method: self.method.clone(),
3550 position: self.position.into(),
3551 tags,
3552 redirect: self.redirect.map(|r| r as i32),
3553 required_auth: self.required_auth,
3554 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
3555 redirect_template: self.redirect_template.clone(),
3556 rewrite_host: self.rewrite_host.clone(),
3557 rewrite_path: self.rewrite_path.clone(),
3558 rewrite_port: self.rewrite_port,
3559 headers: self.headers.clone(),
3560 hsts: self.hsts,
3561 })
3562 .into(),
3563 );
3564 }
3565
3566 v
3567 }
3568}
3569
3570#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3571#[serde(deny_unknown_fields)]
3572pub struct HttpClusterConfig {
3573 pub cluster_id: String,
3574 pub frontends: Vec<HttpFrontendConfig>,
3575 pub backends: Vec<BackendConfig>,
3576 pub sticky_session: bool,
3577 pub https_redirect: bool,
3578 pub load_balancing: LoadBalancingAlgorithms,
3579 pub load_metric: Option<LoadMetric>,
3580 pub answer_503: Option<String>,
3581 pub http2: Option<bool>,
3582 #[serde(default)]
3585 pub answers: BTreeMap<String, String>,
3586 #[serde(default)]
3587 pub https_redirect_port: Option<u32>,
3588 #[serde(default)]
3589 pub authorized_hashes: Vec<String>,
3590 #[serde(default)]
3591 pub www_authenticate: Option<String>,
3592 #[serde(default)]
3595 pub max_connections_per_ip: Option<u64>,
3596 #[serde(default)]
3601 pub max_connections_per_subnet: Option<u64>,
3602 #[serde(default)]
3605 pub retry_after: Option<u32>,
3606 #[serde(default)]
3610 pub health_check: Option<HealthCheckConfig>,
3611 #[serde(default)]
3614 pub udp: Option<UdpClusterConfig>,
3615 #[serde(default)]
3618 pub affinity_header: Option<String>,
3619 #[serde(default)]
3622 pub affinity_cookie: Option<String>,
3623 #[serde(default)]
3626 pub shard_percent: Option<u32>,
3627 #[serde(default)]
3629 pub shard_min_backends: Option<u32>,
3630 #[serde(default)]
3632 pub shard_mode: Option<ShardMode>,
3633 #[serde(default)]
3635 pub max_connection_attempts: Option<u32>,
3636}
3637
3638impl HttpClusterConfig {
3639 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
3640 let mut v: Vec<Request> = vec![
3641 RequestType::AddCluster(Cluster {
3642 cluster_id: self.cluster_id.clone(),
3643 sticky_session: self.sticky_session,
3644 https_redirect: self.https_redirect,
3645 proxy_protocol: None,
3646 load_balancing: self.load_balancing as i32,
3647 answer_503: self.answer_503.clone(),
3648 load_metric: self.load_metric.map(|s| s as i32),
3649 http2: self.http2,
3650 answers: self.answers.clone(),
3651 https_redirect_port: self.https_redirect_port,
3652 authorized_hashes: self.authorized_hashes.clone(),
3653 www_authenticate: self.www_authenticate.clone(),
3654 max_connections_per_ip: self.max_connections_per_ip,
3655 max_connections_per_subnet: self.max_connections_per_subnet,
3656 retry_after: self.retry_after,
3657 health_check: self.health_check.clone(),
3658 udp: self.udp.clone(),
3659 affinity_header: self.affinity_header.clone(),
3660 affinity_cookie: self.affinity_cookie.clone(),
3661 shard_percent: self.shard_percent,
3662 shard_min_backends: self.shard_min_backends,
3663 shard_mode: self.shard_mode.map(|mode| mode as i32),
3664 max_connection_attempts: self.max_connection_attempts,
3665 })
3666 .into(),
3667 ];
3668
3669 for frontend in &self.frontends {
3670 let mut orders = frontend.generate_requests(&self.cluster_id);
3671 v.append(&mut orders);
3672 }
3673
3674 for (backend_count, backend) in self.backends.iter().enumerate() {
3675 let load_balancing_parameters = Some(LoadBalancingParams {
3676 weight: backend.weight.unwrap_or(100) as i32,
3677 });
3678
3679 v.push(
3680 RequestType::AddBackend(AddBackend {
3681 cluster_id: self.cluster_id.clone(),
3682 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
3683 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
3684 }),
3685 address: backend.address.into(),
3686 load_balancing_parameters,
3687 sticky_id: backend.sticky_id.clone(),
3688 backup: backend.backup,
3689 })
3690 .into(),
3691 );
3692 }
3693
3694 debug_assert!(
3699 matches!(
3700 v.first().and_then(|r| r.request_type.as_ref()),
3701 Some(RequestType::AddCluster(_))
3702 ),
3703 "HTTP cluster orders must lead with an AddCluster"
3704 );
3705 debug_assert_eq!(
3706 v.iter()
3707 .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
3708 .count(),
3709 self.backends.len(),
3710 "one AddBackend order per configured backend"
3711 );
3712 Ok(v)
3713 }
3714}
3715
3716#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3717pub struct TcpFrontendConfig {
3718 pub address: SocketAddr,
3719 pub tags: Option<BTreeMap<String, String>>,
3720 #[serde(default)]
3727 pub udp: bool,
3728 #[serde(default)]
3732 pub sni: Option<String>,
3733 #[serde(default)]
3736 pub alpn: Vec<String>,
3737}
3738
3739#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3740pub struct TcpClusterConfig {
3741 pub cluster_id: String,
3742 pub frontends: Vec<TcpFrontendConfig>,
3743 pub backends: Vec<BackendConfig>,
3744 #[serde(default)]
3745 pub proxy_protocol: Option<ProxyProtocolConfig>,
3746 pub load_balancing: LoadBalancingAlgorithms,
3747 pub load_metric: Option<LoadMetric>,
3748 #[serde(default)]
3752 pub answers: BTreeMap<String, String>,
3753 #[serde(default)]
3754 pub https_redirect_port: Option<u32>,
3755 #[serde(default)]
3756 pub authorized_hashes: Vec<String>,
3757 #[serde(default)]
3758 pub www_authenticate: Option<String>,
3759 #[serde(default)]
3762 pub max_connections_per_ip: Option<u64>,
3763 #[serde(default)]
3768 pub max_connections_per_subnet: Option<u64>,
3769 #[serde(default)]
3773 pub retry_after: Option<u32>,
3774 #[serde(default)]
3778 pub health_check: Option<HealthCheckConfig>,
3779 #[serde(default)]
3782 pub udp: Option<UdpClusterConfig>,
3783 #[serde(default)]
3786 pub shard_percent: Option<u32>,
3787 #[serde(default)]
3789 pub shard_min_backends: Option<u32>,
3790 #[serde(default)]
3792 pub shard_mode: Option<ShardMode>,
3793 #[serde(default)]
3795 pub max_connection_attempts: Option<u32>,
3796}
3797
3798impl TcpClusterConfig {
3799 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
3800 let mut v: Vec<Request> = vec![
3801 RequestType::AddCluster(Cluster {
3802 cluster_id: self.cluster_id.clone(),
3803 sticky_session: false,
3804 https_redirect: false,
3805 proxy_protocol: self.proxy_protocol.map(|s| s as i32),
3806 load_balancing: self.load_balancing as i32,
3807 load_metric: self.load_metric.map(|s| s as i32),
3808 answer_503: None,
3809 http2: None,
3810 answers: self.answers.clone(),
3811 https_redirect_port: self.https_redirect_port,
3812 authorized_hashes: self.authorized_hashes.clone(),
3813 www_authenticate: self.www_authenticate.clone(),
3814 max_connections_per_ip: self.max_connections_per_ip,
3815 max_connections_per_subnet: self.max_connections_per_subnet,
3816 retry_after: self.retry_after,
3817 health_check: self.health_check.clone(),
3818 udp: self.udp.clone(),
3819 affinity_header: None,
3821 affinity_cookie: None,
3822 shard_percent: self.shard_percent,
3823 shard_min_backends: self.shard_min_backends,
3824 shard_mode: self.shard_mode.map(|mode| mode as i32),
3825 max_connection_attempts: self.max_connection_attempts,
3826 })
3827 .into(),
3828 ];
3829
3830 for frontend in &self.frontends {
3831 if frontend.udp {
3836 v.push(
3837 RequestType::AddUdpFrontend(RequestUdpFrontend {
3838 cluster_id: self.cluster_id.clone(),
3839 address: frontend.address.into(),
3840 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
3841 })
3842 .into(),
3843 );
3844 } else {
3845 v.push(
3846 RequestType::AddTcpFrontend(RequestTcpFrontend {
3847 cluster_id: self.cluster_id.clone(),
3848 address: frontend.address.into(),
3849 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
3850 sni: frontend.sni.clone(),
3851 alpn: frontend.alpn.clone(),
3852 })
3853 .into(),
3854 );
3855 }
3856 }
3857
3858 for (backend_count, backend) in self.backends.iter().enumerate() {
3859 let load_balancing_parameters = Some(LoadBalancingParams {
3860 weight: backend.weight.unwrap_or(100) as i32,
3861 });
3862
3863 v.push(
3864 RequestType::AddBackend(AddBackend {
3865 cluster_id: self.cluster_id.clone(),
3866 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
3867 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
3868 }),
3869 address: backend.address.into(),
3870 load_balancing_parameters,
3871 sticky_id: backend.sticky_id.clone(),
3872 backup: backend.backup,
3873 })
3874 .into(),
3875 );
3876 }
3877
3878 debug_assert!(
3882 matches!(
3883 v.first().and_then(|r| r.request_type.as_ref()),
3884 Some(RequestType::AddCluster(_))
3885 ),
3886 "TCP cluster orders must lead with an AddCluster"
3887 );
3888 debug_assert_eq!(
3889 v.iter()
3890 .filter(|r| matches!(
3891 r.request_type,
3892 Some(RequestType::AddTcpFrontend(_)) | Some(RequestType::AddUdpFrontend(_))
3893 ))
3894 .count(),
3895 self.frontends.len(),
3896 "one AddTcpFrontend or AddUdpFrontend order per configured frontend"
3897 );
3898 debug_assert_eq!(
3899 v.iter()
3900 .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
3901 .count(),
3902 self.backends.len(),
3903 "one AddBackend order per configured backend"
3904 );
3905 Ok(v)
3906 }
3907}
3908
3909#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3910pub enum ClusterConfig {
3911 Http(HttpClusterConfig),
3912 Tcp(TcpClusterConfig),
3913}
3914
3915impl ClusterConfig {
3916 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
3917 match *self {
3918 ClusterConfig::Http(ref http) => http.generate_requests(),
3919 ClusterConfig::Tcp(ref tcp) => tcp.generate_requests(),
3920 }
3921 }
3922}
3923
3924#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
3926pub struct FileConfig {
3927 pub command_socket: Option<String>,
3928 #[serde(default)]
3932 pub command_socket_mode: Option<String>,
3933 pub command_buffer_size: Option<u64>,
3934 pub max_command_buffer_size: Option<u64>,
3935 pub max_connections: Option<usize>,
3936 pub min_buffers: Option<u64>,
3937 pub max_buffers: Option<u64>,
3938 pub buffer_size: Option<u64>,
3939 #[serde(default)]
3943 pub slab_entries_per_connection: Option<u64>,
3944 #[serde(default)]
3954 pub basic_auth_max_credential_bytes: Option<u64>,
3955 #[serde(default)]
3963 pub max_connections_per_ip: Option<u64>,
3964 #[serde(default)]
3975 pub max_connections_per_subnet: Option<u64>,
3976 #[serde(default)]
3981 pub subnet_ipv4_prefix: Option<u32>,
3982 #[serde(default)]
3987 pub subnet_ipv6_prefix: Option<u32>,
3988 #[serde(default)]
3994 pub retry_after: Option<u32>,
3995 #[serde(default)]
4002 pub max_connection_attempts: Option<u32>,
4003 #[serde(default)]
4012 pub splice_pipe_capacity_bytes: Option<u64>,
4013 #[serde(default)]
4020 pub command_allowed_uids: Option<Vec<u32>>,
4021 pub saved_state: Option<String>,
4022 #[serde(default)]
4023 pub automatic_state_save: Option<bool>,
4024 pub log_level: Option<String>,
4025 pub log_target: Option<String>,
4026 #[serde(default)]
4027 pub log_colored: bool,
4028 #[serde(default)]
4036 pub audit_logs_target: Option<String>,
4037 #[serde(default)]
4042 pub audit_logs_json_target: Option<String>,
4043 #[serde(default)]
4044 pub access_logs_target: Option<String>,
4045 #[serde(default)]
4046 pub access_logs_format: Option<AccessLogFormat>,
4047 #[serde(default)]
4048 pub access_logs_colored: Option<bool>,
4049 pub worker_count: Option<u16>,
4050 pub worker_automatic_restart: Option<bool>,
4051 pub metrics: Option<MetricsConfig>,
4052 pub disable_cluster_metrics: Option<bool>,
4053 pub listeners: Option<Vec<ListenerBuilder>>,
4054 pub clusters: Option<HashMap<String, FileClusterConfig>>,
4055 pub handle_process_affinity: Option<bool>,
4056 pub ctl_command_timeout: Option<u64>,
4057 pub pid_file_path: Option<String>,
4058 pub activate_listeners: Option<bool>,
4059 #[serde(default)]
4060 pub front_timeout: Option<u32>,
4061 #[serde(default)]
4062 pub back_timeout: Option<u32>,
4063 #[serde(default)]
4064 pub connect_timeout: Option<u32>,
4065 #[serde(default)]
4066 pub zombie_check_interval: Option<u32>,
4067 #[serde(default)]
4068 pub accept_queue_timeout: Option<u32>,
4069 #[serde(default)]
4070 pub evict_on_queue_full: Option<bool>,
4071 #[serde(default)]
4072 pub request_timeout: Option<u32>,
4073 #[serde(default)]
4074 pub worker_timeout: Option<u32>,
4075}
4076
4077impl FileConfig {
4078 pub fn load_from_path(path: &str) -> Result<FileConfig, ConfigError> {
4079 let data = Config::load_file(path)?;
4080
4081 let config: FileConfig = match toml::from_str(&data) {
4082 Ok(config) => config,
4083 Err(e) => {
4084 display_toml_error(&data, &e);
4085 return Err(ConfigError::DeserializeToml(e.to_string()));
4086 }
4087 };
4088
4089 let mut reserved_address: HashSet<ListenerKey> = HashSet::new();
4090
4091 if let Some(listeners) = config.listeners.as_ref() {
4092 for listener in listeners.iter() {
4093 let key = ListenerKey::new(listener.address, listener.interface.as_deref());
4094 if reserved_address.contains(&key) {
4095 return Err(ConfigError::ListenerAlreadyDeclared(key));
4096 }
4097 reserved_address.insert(key);
4098 }
4099 }
4100
4101 Ok(config)
4123 }
4124}
4125
4126pub struct ConfigBuilder {
4128 file: FileConfig,
4129 known_addresses: HashMap<SocketAddr, ListenerProtocol>,
4130 expect_proxy_addresses: HashSet<SocketAddr>,
4131 built: Config,
4132}
4133
4134impl ConfigBuilder {
4135 pub fn new<S>(file_config: FileConfig, config_path: S) -> Self
4139 where
4140 S: ToString,
4141 {
4142 let built = Config {
4143 accept_queue_timeout: file_config
4144 .accept_queue_timeout
4145 .unwrap_or(DEFAULT_ACCEPT_QUEUE_TIMEOUT),
4146 evict_on_queue_full: file_config
4147 .evict_on_queue_full
4148 .unwrap_or(DEFAULT_EVICT_ON_QUEUE_FULL),
4149 activate_listeners: file_config.activate_listeners.unwrap_or(true),
4150 automatic_state_save: file_config
4151 .automatic_state_save
4152 .unwrap_or(DEFAULT_AUTOMATIC_STATE_SAVE),
4153 back_timeout: file_config.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
4154 buffer_size: file_config.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE),
4155 command_buffer_size: file_config
4156 .command_buffer_size
4157 .unwrap_or(DEFAULT_COMMAND_BUFFER_SIZE),
4158 config_path: config_path.to_string(),
4159 connect_timeout: file_config
4160 .connect_timeout
4161 .unwrap_or(DEFAULT_CONNECT_TIMEOUT),
4162 ctl_command_timeout: file_config.ctl_command_timeout.unwrap_or(1_000),
4163 front_timeout: file_config.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
4164 handle_process_affinity: file_config.handle_process_affinity.unwrap_or(false),
4165 access_logs_target: file_config.access_logs_target.clone(),
4166 audit_logs_target: file_config.audit_logs_target.clone(),
4167 audit_logs_json_target: file_config.audit_logs_json_target.clone(),
4168 access_logs_format: file_config.access_logs_format.clone(),
4169 access_logs_colored: file_config.access_logs_colored,
4170 log_level: file_config
4171 .log_level
4172 .clone()
4173 .unwrap_or_else(|| String::from("info")),
4174 log_target: file_config
4175 .log_target
4176 .clone()
4177 .unwrap_or_else(|| String::from("stdout")),
4178 log_colored: file_config.log_colored,
4179 max_buffers: file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
4180 max_command_buffer_size: file_config
4181 .max_command_buffer_size
4182 .unwrap_or(DEFAULT_MAX_COMMAND_BUFFER_SIZE),
4183 max_connections: file_config
4184 .max_connections
4185 .unwrap_or(DEFAULT_MAX_CONNECTIONS),
4186 metrics: file_config.metrics.clone(),
4187 disable_cluster_metrics: file_config
4188 .disable_cluster_metrics
4189 .unwrap_or(DEFAULT_DISABLE_CLUSTER_METRICS),
4190 min_buffers: std::cmp::min(
4191 file_config.min_buffers.unwrap_or(DEFAULT_MIN_BUFFERS),
4192 file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
4193 ),
4194 pid_file_path: file_config.pid_file_path.clone(),
4195 request_timeout: file_config
4196 .request_timeout
4197 .unwrap_or(DEFAULT_REQUEST_TIMEOUT),
4198 saved_state: file_config.saved_state.clone(),
4199 worker_automatic_restart: file_config
4200 .worker_automatic_restart
4201 .unwrap_or(DEFAULT_WORKER_AUTOMATIC_RESTART),
4202 worker_count: file_config.worker_count.unwrap_or(DEFAULT_WORKER_COUNT),
4203 zombie_check_interval: file_config
4204 .zombie_check_interval
4205 .unwrap_or(DEFAULT_ZOMBIE_CHECK_INTERVAL),
4206 worker_timeout: file_config.worker_timeout.unwrap_or(DEFAULT_WORKER_TIMEOUT),
4207 slab_entries_per_connection: file_config.slab_entries_per_connection.map(|n| {
4208 n.clamp(
4209 ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION,
4210 ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION,
4211 )
4212 }),
4213 command_allowed_uids: file_config.command_allowed_uids.clone(),
4214 basic_auth_max_credential_bytes: file_config.basic_auth_max_credential_bytes,
4215 max_connections_per_ip: file_config
4216 .max_connections_per_ip
4217 .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_IP),
4218 max_connections_per_subnet: file_config
4219 .max_connections_per_subnet
4220 .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_SUBNET),
4221 subnet_ipv4_prefix: file_config
4222 .subnet_ipv4_prefix
4223 .unwrap_or(DEFAULT_SUBNET_IPV4_PREFIX),
4224 subnet_ipv6_prefix: file_config
4225 .subnet_ipv6_prefix
4226 .unwrap_or(DEFAULT_SUBNET_IPV6_PREFIX),
4227 retry_after: file_config.retry_after.unwrap_or(DEFAULT_RETRY_AFTER),
4228 max_connection_attempts: file_config
4229 .max_connection_attempts
4230 .unwrap_or(DEFAULT_MAX_CONNECTION_ATTEMPTS),
4231 splice_pipe_capacity_bytes: file_config.splice_pipe_capacity_bytes,
4232 ..Default::default()
4233 };
4234
4235 debug_assert!(
4239 built.min_buffers <= built.max_buffers,
4240 "min_buffers must be clamped to <= max_buffers in the builder"
4241 );
4242 debug_assert!(
4245 built.slab_entries_per_connection.is_none_or(|n| {
4246 (ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION
4247 ..=ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION)
4248 .contains(&n)
4249 }),
4250 "a set slab_entries_per_connection must be clamped into [MIN, MAX]"
4251 );
4252
4253 Self {
4254 file: file_config,
4255 known_addresses: HashMap::new(),
4256 expect_proxy_addresses: HashSet::new(),
4257 built,
4258 }
4259 }
4260
4261 fn push_tls_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
4262 let listener = listener.to_tls(Some(&self.built))?;
4263 self.built.https_listeners.push(listener);
4264 Ok(())
4265 }
4266
4267 fn push_http_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
4268 let listener = listener.to_http(Some(&self.built))?;
4269 self.built.http_listeners.push(listener);
4270 Ok(())
4271 }
4272
4273 fn push_tcp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
4274 let listener = listener.to_tcp(Some(&self.built))?;
4275 self.built.tcp_listeners.push(listener);
4276 Ok(())
4277 }
4278
4279 fn push_udp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
4280 let listener = listener.to_udp(Some(&self.built))?;
4281 self.built.udp_listeners.push(listener);
4282 Ok(())
4283 }
4284
4285 fn populate_listeners(&mut self, listeners: Vec<ListenerBuilder>) -> Result<(), ConfigError> {
4286 let mut declared: HashSet<ListenerKey> = HashSet::new();
4287 for listener in listeners.iter() {
4288 let key = ListenerKey::new(listener.address, listener.interface.as_deref());
4291 if !declared.insert(key.clone()) {
4292 if key.interface.is_none() {
4293 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
4294 }
4295 return Err(ConfigError::ListenerAlreadyDeclared(key));
4296 }
4297
4298 let protocol = listener
4299 .protocol
4300 .ok_or(ConfigError::Missing(MissingKind::Protocol))?;
4301
4302 if let Some(known) = self.known_addresses.get(&listener.address)
4306 && *known != protocol
4307 {
4308 return Err(ConfigError::ListenerProtocolMismatchOnAddress(
4309 listener.address,
4310 ));
4311 }
4312
4313 self.known_addresses.insert(listener.address, protocol);
4314 if listener.expect_proxy == Some(true) {
4315 self.expect_proxy_addresses.insert(listener.address);
4316 }
4317
4318 if listener.public_address.is_some() && listener.expect_proxy == Some(true) {
4319 return Err(ConfigError::Incompatible {
4320 object: ObjectKind::Listener,
4321 id: listener.address.to_string(),
4322 kind: IncompatibilityKind::PublicAddress,
4323 });
4324 }
4325
4326 match protocol {
4327 ListenerProtocol::Https => self.push_tls_listener(listener.clone())?,
4328 ListenerProtocol::Http => self.push_http_listener(listener.clone())?,
4329 ListenerProtocol::Tcp => self.push_tcp_listener(listener.clone())?,
4330 ListenerProtocol::Udp => self.push_udp_listener(listener.clone())?,
4331 }
4332 }
4333 Ok(())
4334 }
4335
4336 fn populate_clusters(
4337 &mut self,
4338 mut file_cluster_configs: HashMap<String, FileClusterConfig>,
4339 ) -> Result<(), ConfigError> {
4340 for (id, file_cluster_config) in file_cluster_configs.drain() {
4341 let mut cluster_config =
4342 file_cluster_config.to_cluster_config(id.as_str(), &self.expect_proxy_addresses)?;
4343
4344 match cluster_config {
4345 ClusterConfig::Http(ref mut http) => {
4346 for frontend in http.frontends.iter_mut() {
4347 match self.known_addresses.get(&frontend.address) {
4348 Some(ListenerProtocol::Tcp) => {
4349 return Err(ConfigError::WrongFrontendProtocol(
4350 ListenerProtocol::Tcp,
4351 ));
4352 }
4353 Some(ListenerProtocol::Udp) => {
4354 return Err(ConfigError::WrongFrontendProtocol(
4355 ListenerProtocol::Udp,
4356 ));
4357 }
4358 Some(ListenerProtocol::Http) => {
4359 if frontend.certificate.is_some() {
4360 return Err(ConfigError::WrongFrontendProtocol(
4361 ListenerProtocol::Http,
4362 ));
4363 }
4364 }
4365 Some(ListenerProtocol::Https) => {
4366 if frontend.certificate.is_none() {
4367 if let Some(https_listener) =
4368 self.built.https_listeners.iter().find(|listener| {
4369 listener.address == frontend.address.into()
4370 && listener.certificate.is_some()
4371 })
4372 {
4373 frontend
4375 .certificate
4376 .clone_from(&https_listener.certificate);
4377 frontend.certificate_chain =
4378 Some(https_listener.certificate_chain.clone());
4379 frontend.key.clone_from(&https_listener.key);
4380 }
4381 if frontend.certificate.is_none() {
4382 debug!("known addresses: {:?}", self.known_addresses);
4383 debug!("frontend: {:?}", frontend);
4384 return Err(ConfigError::WrongFrontendProtocol(
4385 ListenerProtocol::Https,
4386 ));
4387 }
4388 }
4389 }
4390 None => {
4391 let file_listener_protocol = if frontend.certificate.is_some() {
4393 self.push_tls_listener(ListenerBuilder::new(
4394 frontend.address.into(),
4395 ListenerProtocol::Https,
4396 ))?;
4397
4398 ListenerProtocol::Https
4399 } else {
4400 self.push_http_listener(ListenerBuilder::new(
4401 frontend.address.into(),
4402 ListenerProtocol::Http,
4403 ))?;
4404
4405 ListenerProtocol::Http
4406 };
4407 self.known_addresses
4408 .insert(frontend.address, file_listener_protocol);
4409 }
4410 }
4411 }
4412 }
4413 ClusterConfig::Tcp(ref mut tcp) => {
4414 for frontend in tcp.frontends.iter_mut() {
4416 match self.known_addresses.get(&frontend.address) {
4417 Some(ListenerProtocol::Http) | Some(ListenerProtocol::Https) => {
4418 return Err(ConfigError::WrongFrontendProtocol(
4419 ListenerProtocol::Http,
4420 ));
4421 }
4422 Some(ListenerProtocol::Udp) => {
4423 frontend.udp = true;
4429 }
4430 Some(ListenerProtocol::Tcp) => {}
4431 None => {
4432 self.push_tcp_listener(ListenerBuilder::new(
4434 frontend.address.into(),
4435 ListenerProtocol::Tcp,
4436 ))?;
4437 self.known_addresses
4438 .insert(frontend.address, ListenerProtocol::Tcp);
4439 }
4440 }
4441 }
4442 }
4443 }
4444
4445 self.built.clusters.insert(id, cluster_config);
4446 }
4447 Ok(())
4448 }
4449
4450 pub fn into_config(&mut self) -> Result<Config, ConfigError> {
4452 if self.built.subnet_ipv4_prefix > MAX_SUBNET_IPV4_PREFIX {
4457 return Err(ConfigError::InvalidSubnetPrefix {
4458 key: "subnet_ipv4_prefix",
4459 family: "IPv4",
4460 value: self.built.subnet_ipv4_prefix,
4461 maximum: MAX_SUBNET_IPV4_PREFIX,
4462 });
4463 }
4464 if self.built.subnet_ipv6_prefix > MAX_SUBNET_IPV6_PREFIX {
4465 return Err(ConfigError::InvalidSubnetPrefix {
4466 key: "subnet_ipv6_prefix",
4467 family: "IPv6",
4468 value: self.built.subnet_ipv6_prefix,
4469 maximum: MAX_SUBNET_IPV6_PREFIX,
4470 });
4471 }
4472 validate_max_connection_attempts(self.built.max_connection_attempts).map_err(|reason| {
4473 ConfigError::InvalidMaxConnectionAttempts {
4474 cluster_id: None,
4475 value: self.built.max_connection_attempts,
4476 reason,
4477 }
4478 })?;
4479
4480 if let Some(listeners) = &self.file.listeners {
4481 self.populate_listeners(listeners.clone())?;
4482 }
4483
4484 if let Some(file_cluster_configs) = &self.file.clusters {
4485 self.populate_clusters(file_cluster_configs.clone())?;
4486 }
4487
4488 type SniAlpnByAddress = HashMap<SocketAddr, Vec<(Option<String>, Vec<String>)>>;
4508 let mut frontends_by_address: SniAlpnByAddress = HashMap::new();
4509 let mut addresses_with_no_sni_frontend: HashSet<SocketAddr> = HashSet::new();
4510 for cluster in self.built.clusters.values() {
4511 if let ClusterConfig::Tcp(tcp) = cluster {
4512 for frontend in &tcp.frontends {
4513 if frontend.udp {
4520 continue;
4521 }
4522 if frontend.sni.is_none() {
4523 addresses_with_no_sni_frontend.insert(frontend.address);
4524 }
4525 frontends_by_address
4526 .entry(frontend.address)
4527 .or_default()
4528 .push((frontend.sni.clone(), frontend.alpn.clone()));
4529 }
4530 }
4531 }
4532
4533 let mut addresses_with_sni_frontend: HashSet<SocketAddr> = HashSet::new();
4534 for (address, frontends) in &frontends_by_address {
4535 if !frontends.iter().any(|(sni, _)| sni.is_some()) {
4536 continue;
4537 }
4538 addresses_with_sni_frontend.insert(*address);
4539
4540 if addresses_with_no_sni_frontend.contains(address) {
4541 return Err(ConfigError::TcpListenerMixesSniAndNoSni { address: *address });
4542 }
4543
4544 let mut alpn_lists_by_sni: HashMap<Option<String>, Vec<&Vec<String>>> = HashMap::new();
4545 for (sni, alpn) in frontends {
4546 alpn_lists_by_sni.entry(sni.clone()).or_default().push(alpn);
4547 }
4548 for (sni, alpn_lists) in alpn_lists_by_sni {
4549 let mut seen_protocols: HashSet<&str> = HashSet::new();
4550 let mut catch_all_count = 0usize;
4551 for alpn in alpn_lists {
4552 if alpn.is_empty() {
4553 catch_all_count += 1;
4554 if catch_all_count > 1 {
4555 return Err(ConfigError::TcpFrontendMultipleAlpnCatchAll {
4556 address: *address,
4557 sni: sni.clone(),
4558 });
4559 }
4560 continue;
4561 }
4562 for protocol in alpn {
4563 if !seen_protocols.insert(protocol.as_str()) {
4564 return Err(ConfigError::TcpFrontendAlpnOverlap {
4565 address: *address,
4566 sni: sni.clone(),
4567 protocol: protocol.clone(),
4568 });
4569 }
4570 }
4571 }
4572 }
4573 }
4574
4575 for listener in &self.built.tcp_listeners {
4576 let address: SocketAddr = listener.address.into();
4577 if !addresses_with_sni_frontend.contains(&address) {
4578 continue;
4579 }
4580 let sni_preread_timeout = listener
4581 .sni_preread_timeout
4582 .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT);
4583 if sni_preread_timeout > listener.front_timeout {
4584 return Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
4585 address,
4586 sni_preread_timeout,
4587 front_timeout: listener.front_timeout,
4588 });
4589 }
4590 let sni_preread_max_bytes = listener
4591 .sni_preread_max_bytes
4592 .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES);
4593 if sni_preread_max_bytes < MIN_SNI_PREREAD_MAX_BYTES {
4594 return Err(ConfigError::SniPrereadMaxBytesTooSmall {
4595 address,
4596 sni_preread_max_bytes,
4597 minimum: MIN_SNI_PREREAD_MAX_BYTES,
4598 });
4599 }
4600 if u64::from(sni_preread_max_bytes) > self.built.buffer_size {
4601 return Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
4602 address,
4603 sni_preread_max_bytes,
4604 buffer_size: self.built.buffer_size,
4605 });
4606 }
4607 }
4608
4609 let h2_listeners = self
4618 .built
4619 .https_listeners
4620 .iter()
4621 .filter(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
4622 .count();
4623 if h2_listeners > 0 && self.built.buffer_size < H2_MIN_BUFFER_SIZE {
4624 return Err(ConfigError::BufferSizeTooSmallForH2 {
4625 buffer_size: self.built.buffer_size,
4626 minimum: H2_MIN_BUFFER_SIZE,
4627 listeners: h2_listeners,
4628 });
4629 }
4630
4631 if self.built.command_buffer_size > self.built.max_command_buffer_size {
4638 return Err(ConfigError::CommandBufferSizeExceedsMax {
4639 command_buffer_size: self.built.command_buffer_size,
4640 max_command_buffer_size: self.built.max_command_buffer_size,
4641 });
4642 }
4643
4644 if let Some(cap) = self.built.basic_auth_max_credential_bytes {
4654 let third = self.built.buffer_size / 3;
4655 if cap >= third {
4656 warn!(
4657 "basic_auth_max_credential_bytes = {} is >= buffer_size / 3 ({}); \
4658 a hostile peer can pin ~33% of the per-frontend buffer per failed auth \
4659 attempt. Consider lowering basic_auth_max_credential_bytes (typical \
4660 credentials are <100 bytes) or raising buffer_size.",
4661 cap, third
4662 );
4663 }
4664 }
4665
4666 if self.built.evict_on_queue_full && self.built.max_connections < 100 {
4673 let pct = 100usize.div_ceil(self.built.max_connections);
4674 warn!(
4675 "evict_on_queue_full enabled with max_connections = {}; the eviction batch \
4676 clamps to 1, equivalent to ~{}% of capacity per cap event (the knob is \
4677 documented as 1%). Confirm this is intended.",
4678 self.built.max_connections, pct
4679 );
4680 }
4681
4682 let command_socket_path = self.file.command_socket.clone().unwrap_or({
4683 let mut path = env::current_dir().map_err(|e| ConfigError::Env(e.to_string()))?;
4684 path.push("sozu.sock");
4685 let verified_path = path
4686 .to_str()
4687 .ok_or(ConfigError::InvalidPath(path.clone()))?;
4688 verified_path.to_owned()
4689 });
4690
4691 if let (None, Some(true)) = (&self.file.saved_state, &self.file.automatic_state_save) {
4692 return Err(ConfigError::Missing(MissingKind::SavedState));
4693 }
4694
4695 let command_socket_mode = match self.file.command_socket_mode.as_deref() {
4696 Some(mode) => parse_command_socket_mode(mode)?,
4697 None => DEFAULT_COMMAND_SOCKET_MODE,
4698 };
4699
4700 let config = Config {
4701 command_socket: command_socket_path,
4702 command_socket_mode,
4703 ..self.built.clone()
4704 };
4705
4706 debug_assert!(
4712 config.min_buffers <= config.max_buffers,
4713 "min_buffers must not exceed max_buffers"
4714 );
4715 debug_assert!(
4721 !config
4722 .https_listeners
4723 .iter()
4724 .any(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
4725 || config.buffer_size >= H2_MIN_BUFFER_SIZE,
4726 "an h2-advertising config must satisfy the H2 minimum buffer size"
4727 );
4728 Ok(config)
4729 }
4730}
4731
4732#[derive(Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
4736pub struct Config {
4737 pub config_path: String,
4738 pub command_socket: String,
4739 #[serde(default = "default_command_socket_mode")]
4744 pub command_socket_mode: u32,
4745 pub command_buffer_size: u64,
4746 pub max_command_buffer_size: u64,
4747 pub max_connections: usize,
4748 pub min_buffers: u64,
4749 pub max_buffers: u64,
4750 pub buffer_size: u64,
4751 pub saved_state: Option<String>,
4752 #[serde(default)]
4753 pub automatic_state_save: bool,
4754 pub log_level: String,
4755 pub log_target: String,
4756 pub log_colored: bool,
4757 #[serde(default)]
4760 pub audit_logs_target: Option<String>,
4761 #[serde(default)]
4764 pub audit_logs_json_target: Option<String>,
4765 #[serde(default)]
4766 pub access_logs_target: Option<String>,
4767 pub access_logs_format: Option<AccessLogFormat>,
4768 pub access_logs_colored: Option<bool>,
4769 pub worker_count: u16,
4770 pub worker_automatic_restart: bool,
4771 pub metrics: Option<MetricsConfig>,
4772 #[serde(default = "default_disable_cluster_metrics")]
4773 pub disable_cluster_metrics: bool,
4774 pub http_listeners: Vec<HttpListenerConfig>,
4775 pub https_listeners: Vec<HttpsListenerConfig>,
4776 pub tcp_listeners: Vec<TcpListenerConfig>,
4777 #[serde(default)]
4778 pub udp_listeners: Vec<UdpListenerConfig>,
4779 pub clusters: HashMap<String, ClusterConfig>,
4780 pub handle_process_affinity: bool,
4781 pub ctl_command_timeout: u64,
4782 pub pid_file_path: Option<String>,
4783 pub activate_listeners: bool,
4784 #[serde(default = "default_front_timeout")]
4785 pub front_timeout: u32,
4786 #[serde(default = "default_back_timeout")]
4787 pub back_timeout: u32,
4788 #[serde(default = "default_connect_timeout")]
4789 pub connect_timeout: u32,
4790 #[serde(default = "default_zombie_check_interval")]
4791 pub zombie_check_interval: u32,
4792 #[serde(default = "default_accept_queue_timeout")]
4793 pub accept_queue_timeout: u32,
4794 #[serde(default = "default_evict_on_queue_full")]
4795 pub evict_on_queue_full: bool,
4796 #[serde(default = "default_request_timeout")]
4797 pub request_timeout: u32,
4798 #[serde(default = "default_worker_timeout")]
4799 pub worker_timeout: u32,
4800 #[serde(default)]
4807 pub slab_entries_per_connection: Option<u64>,
4808 #[serde(default)]
4813 pub command_allowed_uids: Option<Vec<u32>>,
4814 #[serde(default)]
4819 pub basic_auth_max_credential_bytes: Option<u64>,
4820 #[serde(default = "default_max_connections_per_ip")]
4826 pub max_connections_per_ip: u64,
4827 #[serde(default = "default_max_connections_per_subnet")]
4833 pub max_connections_per_subnet: u64,
4834 #[serde(default = "default_subnet_ipv4_prefix")]
4837 pub subnet_ipv4_prefix: u32,
4838 #[serde(default = "default_subnet_ipv6_prefix")]
4841 pub subnet_ipv6_prefix: u32,
4842 #[serde(default = "default_retry_after")]
4845 pub retry_after: u32,
4846 #[serde(default = "default_max_connection_attempts")]
4849 pub max_connection_attempts: u32,
4850 #[serde(default)]
4857 pub splice_pipe_capacity_bytes: Option<u64>,
4858}
4859
4860fn default_command_socket_mode() -> u32 {
4861 DEFAULT_COMMAND_SOCKET_MODE
4862}
4863
4864pub fn parse_command_socket_mode(value: &str) -> Result<u32, ConfigError> {
4871 let invalid = |reason| ConfigError::InvalidCommandSocketMode {
4872 value: value.to_owned(),
4873 reason,
4874 };
4875 if value.is_empty() || value.len() > 4 || !value.bytes().all(|b| (b'0'..=b'7').contains(&b)) {
4876 return Err(invalid(
4877 "expected one to four octal digits, such as \"0660\" or \"660\"",
4878 ));
4879 }
4880 let mode =
4881 u32::from_str_radix(value, 8).map_err(|_| invalid("expected one to four octal digits"))?;
4882 if mode > MAX_COMMAND_SOCKET_MODE {
4883 return Err(invalid(
4884 "only permission bits (at most 0777) are allowed; setuid, setgid and sticky are refused",
4885 ));
4886 }
4887 if mode & 0o600 != 0o600 {
4888 return Err(invalid("the owner must keep read and write (0600)"));
4889 }
4890 Ok(mode)
4891}
4892
4893fn default_front_timeout() -> u32 {
4894 DEFAULT_FRONT_TIMEOUT
4895}
4896
4897fn default_back_timeout() -> u32 {
4898 DEFAULT_BACK_TIMEOUT
4899}
4900
4901fn default_connect_timeout() -> u32 {
4902 DEFAULT_CONNECT_TIMEOUT
4903}
4904
4905fn default_request_timeout() -> u32 {
4906 DEFAULT_REQUEST_TIMEOUT
4907}
4908
4909fn default_zombie_check_interval() -> u32 {
4910 DEFAULT_ZOMBIE_CHECK_INTERVAL
4911}
4912
4913fn default_accept_queue_timeout() -> u32 {
4914 DEFAULT_ACCEPT_QUEUE_TIMEOUT
4915}
4916
4917fn default_evict_on_queue_full() -> bool {
4918 DEFAULT_EVICT_ON_QUEUE_FULL
4919}
4920
4921fn default_disable_cluster_metrics() -> bool {
4922 DEFAULT_DISABLE_CLUSTER_METRICS
4923}
4924
4925fn default_worker_timeout() -> u32 {
4926 DEFAULT_WORKER_TIMEOUT
4927}
4928
4929fn default_max_connections_per_ip() -> u64 {
4930 DEFAULT_MAX_CONNECTIONS_PER_IP
4931}
4932
4933fn default_max_connections_per_subnet() -> u64 {
4934 DEFAULT_MAX_CONNECTIONS_PER_SUBNET
4935}
4936
4937fn default_subnet_ipv4_prefix() -> u32 {
4938 DEFAULT_SUBNET_IPV4_PREFIX
4939}
4940
4941fn default_subnet_ipv6_prefix() -> u32 {
4942 DEFAULT_SUBNET_IPV6_PREFIX
4943}
4944
4945fn default_retry_after() -> u32 {
4946 DEFAULT_RETRY_AFTER
4947}
4948
4949fn default_max_connection_attempts() -> u32 {
4950 DEFAULT_MAX_CONNECTION_ATTEMPTS
4951}
4952
4953impl Config {
4954 pub fn load_from_path(path: &str) -> Result<Config, ConfigError> {
4956 let file_config = FileConfig::load_from_path(path)?;
4957
4958 let mut config = ConfigBuilder::new(file_config, path).into_config()?;
4959
4960 config.saved_state = config.saved_state_path()?;
4962
4963 Ok(config)
4964 }
4965
4966 pub fn generate_config_messages(&self) -> Result<Vec<WorkerRequest>, ConfigError> {
4968 let mut v = Vec::new();
4969 let mut count = 0u8;
4970
4971 for listener in &self.http_listeners {
4972 v.push(WorkerRequest {
4973 id: format!("CONFIG-{count}"),
4974 content: RequestType::AddHttpListener(listener.clone()).into(),
4975 });
4976 count += 1;
4977 }
4978
4979 for listener in &self.https_listeners {
4980 v.push(WorkerRequest {
4981 id: format!("CONFIG-{count}"),
4982 content: RequestType::add_https_listener(listener.clone()).into(),
4983 });
4984 count += 1;
4985 }
4986
4987 for listener in &self.tcp_listeners {
4988 v.push(WorkerRequest {
4989 id: format!("CONFIG-{count}"),
4990 content: RequestType::AddTcpListener(listener.clone()).into(),
4991 });
4992 count += 1;
4993 }
4994
4995 for listener in &self.udp_listeners {
4996 v.push(WorkerRequest {
4997 id: format!("CONFIG-{count}"),
4998 content: RequestType::AddUdpListener(listener.clone()).into(),
4999 });
5000 count += 1;
5001 }
5002
5003 for cluster in self.clusters.values() {
5004 let mut orders = cluster.generate_requests()?;
5005 for content in orders.drain(..) {
5006 v.push(WorkerRequest {
5007 id: format!("CONFIG-{count}"),
5008 content,
5009 });
5010 count += 1;
5011 }
5012 }
5013
5014 if self.activate_listeners {
5015 for listener in &self.http_listeners {
5016 v.push(WorkerRequest {
5017 id: format!("CONFIG-{count}"),
5018 content: RequestType::ActivateListener(ActivateListener {
5019 interface: listener.interface.clone(),
5020 address: listener.address,
5021 proxy: ListenerType::Http.into(),
5022 from_scm: false,
5023 })
5024 .into(),
5025 });
5026 count += 1;
5027 }
5028
5029 for listener in &self.https_listeners {
5030 v.push(WorkerRequest {
5031 id: format!("CONFIG-{count}"),
5032 content: RequestType::ActivateListener(ActivateListener {
5033 interface: listener.interface.clone(),
5034 address: listener.address,
5035 proxy: ListenerType::Https.into(),
5036 from_scm: false,
5037 })
5038 .into(),
5039 });
5040 count += 1;
5041 }
5042
5043 for listener in &self.tcp_listeners {
5044 v.push(WorkerRequest {
5045 id: format!("CONFIG-{count}"),
5046 content: RequestType::ActivateListener(ActivateListener {
5047 interface: listener.interface.clone(),
5048 address: listener.address,
5049 proxy: ListenerType::Tcp.into(),
5050 from_scm: false,
5051 })
5052 .into(),
5053 });
5054 count += 1;
5055 }
5056
5057 for listener in &self.udp_listeners {
5058 v.push(WorkerRequest {
5059 id: format!("CONFIG-{count}"),
5060 content: RequestType::ActivateListener(ActivateListener {
5061 interface: listener.interface.clone(),
5062 address: listener.address,
5063 proxy: ListenerType::Udp.into(),
5064 from_scm: false,
5065 })
5066 .into(),
5067 });
5068 count += 1;
5069 }
5070 }
5071
5072 if self.disable_cluster_metrics {
5073 v.push(WorkerRequest {
5074 id: format!("CONFIG-{count}"),
5075 content: RequestType::ConfigureMetrics(MetricsConfiguration::Disabled.into())
5076 .into(),
5077 });
5078 }
5080
5081 Ok(v)
5082 }
5083
5084 pub fn command_socket_path(&self) -> Result<String, ConfigError> {
5086 let config_path_buf = PathBuf::from(self.config_path.clone());
5087 let mut config_dir = config_path_buf
5088 .parent()
5089 .ok_or(ConfigError::NoFileParent(
5090 config_path_buf.to_string_lossy().to_string(),
5091 ))?
5092 .to_path_buf();
5093
5094 let socket_path = PathBuf::from(self.command_socket.clone());
5095
5096 let mut socket_parent_dir = match socket_path.parent() {
5097 None => config_dir,
5100 Some(path) => {
5101 config_dir.push(path);
5103 config_dir.canonicalize().map_err(|io_error| {
5105 ConfigError::SocketPathError(format!(
5106 "Could not canonicalize path {config_dir:?}: {io_error}"
5107 ))
5108 })?
5109 }
5110 };
5111
5112 let socket_name = socket_path
5113 .file_name()
5114 .ok_or(ConfigError::SocketPathError(format!(
5115 "could not get command socket file name from {socket_path:?}"
5116 )))?;
5117
5118 socket_parent_dir.push(socket_name);
5120
5121 let command_socket_path = socket_parent_dir
5122 .to_str()
5123 .ok_or(ConfigError::SocketPathError(format!(
5124 "Invalid socket path {socket_parent_dir:?}"
5125 )))?
5126 .to_string();
5127
5128 Ok(command_socket_path)
5129 }
5130
5131 fn saved_state_path(&self) -> Result<Option<String>, ConfigError> {
5133 let path = match self.saved_state.as_ref() {
5134 Some(path) => path,
5135 None => return Ok(None),
5136 };
5137
5138 debug!("saved_stated path in the config: {}", path);
5139 let config_path = PathBuf::from(self.config_path.clone());
5140
5141 debug!("Config path buffer: {:?}", config_path);
5142 let config_dir = config_path
5143 .parent()
5144 .ok_or(ConfigError::SaveStatePath(format!(
5145 "Could get parent directory of config file {config_path:?}"
5146 )))?;
5147
5148 debug!("Config folder: {:?}", config_dir);
5149 if !config_dir.exists() {
5150 create_dir_all(config_dir).map_err(|io_error| {
5151 ConfigError::SaveStatePath(format!(
5152 "failed to create state parent directory '{config_dir:?}': {io_error}"
5153 ))
5154 })?;
5155 }
5156
5157 let mut saved_state_path_raw = config_dir.to_path_buf();
5158 saved_state_path_raw.push(path);
5159 debug!(
5160 "Looking for saved state on the path {:?}",
5161 saved_state_path_raw
5162 );
5163
5164 match metadata(path) {
5165 Err(err) if matches!(err.kind(), ErrorKind::NotFound) => {
5166 info!("Create an empty state file at '{}'", path);
5167 File::create(path).map_err(|io_error| {
5168 ConfigError::SaveStatePath(format!(
5169 "failed to create state file '{path:?}': {io_error}"
5170 ))
5171 })?;
5172 }
5173 _ => {}
5174 }
5175
5176 saved_state_path_raw.canonicalize().map_err(|io_error| {
5177 ConfigError::SaveStatePath(format!(
5178 "could not get saved state path from config file input {path:?}: {io_error}"
5179 ))
5180 })?;
5181
5182 let stringified_path = saved_state_path_raw
5183 .to_str()
5184 .ok_or(ConfigError::SaveStatePath(format!(
5185 "Invalid path {saved_state_path_raw:?}"
5186 )))?
5187 .to_string();
5188
5189 Ok(Some(stringified_path))
5190 }
5191
5192 pub fn load_file(path: &str) -> Result<String, ConfigError> {
5194 std::fs::read_to_string(path).map_err(|io_error| ConfigError::FileRead {
5195 path_to_read: path.to_owned(),
5196 io_error,
5197 })
5198 }
5199
5200 pub fn load_file_bytes(path: &str) -> Result<Vec<u8>, ConfigError> {
5202 std::fs::read(path).map_err(|io_error| ConfigError::FileRead {
5203 path_to_read: path.to_owned(),
5204 io_error,
5205 })
5206 }
5207}
5208
5209impl fmt::Debug for Config {
5210 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5211 f.debug_struct("Config")
5212 .field("config_path", &self.config_path)
5213 .field("command_socket", &self.command_socket)
5214 .field(
5215 "command_socket_mode",
5216 &format_args!("{:o}", self.command_socket_mode),
5217 )
5218 .field("command_buffer_size", &self.command_buffer_size)
5219 .field("max_command_buffer_size", &self.max_command_buffer_size)
5220 .field("max_connections", &self.max_connections)
5221 .field("min_buffers", &self.min_buffers)
5222 .field("max_buffers", &self.max_buffers)
5223 .field("buffer_size", &self.buffer_size)
5224 .field("saved_state", &self.saved_state)
5225 .field("automatic_state_save", &self.automatic_state_save)
5226 .field("log_level", &self.log_level)
5227 .field("log_target", &self.log_target)
5228 .field("access_logs_target", &self.access_logs_target)
5229 .field("audit_logs_target", &self.audit_logs_target)
5230 .field("audit_logs_json_target", &self.audit_logs_json_target)
5231 .field("access_logs_format", &self.access_logs_format)
5232 .field("worker_count", &self.worker_count)
5233 .field("worker_automatic_restart", &self.worker_automatic_restart)
5234 .field("metrics", &self.metrics)
5235 .field("disable_cluster_metrics", &self.disable_cluster_metrics)
5236 .field("handle_process_affinity", &self.handle_process_affinity)
5237 .field("ctl_command_timeout", &self.ctl_command_timeout)
5238 .field("pid_file_path", &self.pid_file_path)
5239 .field("activate_listeners", &self.activate_listeners)
5240 .field("front_timeout", &self.front_timeout)
5241 .field("back_timeout", &self.back_timeout)
5242 .field("connect_timeout", &self.connect_timeout)
5243 .field("zombie_check_interval", &self.zombie_check_interval)
5244 .field("accept_queue_timeout", &self.accept_queue_timeout)
5245 .field("evict_on_queue_full", &self.evict_on_queue_full)
5246 .field("request_timeout", &self.request_timeout)
5247 .field("worker_timeout", &self.worker_timeout)
5248 .finish()
5249 }
5250}
5251
5252fn display_toml_error(file: &str, error: &toml::de::Error) {
5253 println!("error parsing the configuration file '{file}': {error}");
5254 if let Some(Range { start, end }) = error.span() {
5255 print!("error parsing the configuration file '{file}' at position: {start}, {end}");
5256 }
5257}
5258
5259impl ServerConfig {
5260 pub const DEFAULT_SLAB_ENTRIES_PER_CONNECTION: u64 = 4;
5267 pub const MIN_SLAB_ENTRIES_PER_CONNECTION: u64 = 2;
5270 pub const MAX_SLAB_ENTRIES_PER_CONNECTION: u64 = 32;
5273
5274 pub fn effective_slab_entries_per_connection(&self) -> u64 {
5277 let effective = match self.slab_entries_per_connection {
5278 Some(0) | None => Self::DEFAULT_SLAB_ENTRIES_PER_CONNECTION,
5279 Some(n) => n.clamp(
5280 Self::MIN_SLAB_ENTRIES_PER_CONNECTION,
5281 Self::MAX_SLAB_ENTRIES_PER_CONNECTION,
5282 ),
5283 };
5284 debug_assert!(
5288 (Self::MIN_SLAB_ENTRIES_PER_CONNECTION..=Self::MAX_SLAB_ENTRIES_PER_CONNECTION)
5289 .contains(&effective),
5290 "effective slab entries per connection must stay within [MIN, MAX]"
5291 );
5292 effective
5293 }
5294
5295 pub fn slab_capacity(&self) -> u64 {
5302 let per_conn = self.effective_slab_entries_per_connection();
5303 let capacity = 10 + per_conn * self.max_connections;
5304 debug_assert!(
5309 capacity >= 10,
5310 "slab capacity must reserve the base entries"
5311 );
5312 debug_assert!(
5313 self.max_connections == 0 || capacity > 10,
5314 "a non-zero connection cap must reserve per-connection slab entries"
5315 );
5316 capacity
5317 }
5318}
5319
5320impl From<&Config> for ServerConfig {
5322 fn from(config: &Config) -> Self {
5323 let metrics = config.metrics.clone().map(|m| ServerMetricsConfig {
5324 address: m.address.to_string(),
5325 tagged_metrics: m.tagged_metrics,
5326 prefix: m.prefix,
5327 detail: Some(MetricDetail::from(m.detail) as i32),
5328 });
5329 let server_config = Self {
5330 max_connections: config.max_connections as u64,
5331 front_timeout: config.front_timeout,
5332 back_timeout: config.back_timeout,
5333 connect_timeout: config.connect_timeout,
5334 zombie_check_interval: config.zombie_check_interval,
5335 accept_queue_timeout: config.accept_queue_timeout,
5336 min_buffers: config.min_buffers,
5337 max_buffers: config.max_buffers,
5338 buffer_size: config.buffer_size,
5339 log_level: config.log_level.clone(),
5340 log_target: config.log_target.clone(),
5341 access_logs_target: config.access_logs_target.clone(),
5342 audit_logs_target: config.audit_logs_target.clone(),
5343 audit_logs_json_target: config.audit_logs_json_target.clone(),
5344 command_buffer_size: config.command_buffer_size,
5345 max_command_buffer_size: config.max_command_buffer_size,
5346 metrics,
5347 access_log_format: ProtobufAccessLogFormat::from(&config.access_logs_format) as i32,
5348 log_colored: config.log_colored,
5349 slab_entries_per_connection: config.slab_entries_per_connection,
5350 basic_auth_max_credential_bytes: config.basic_auth_max_credential_bytes,
5351 evict_on_queue_full: Some(config.evict_on_queue_full),
5352 max_connections_per_ip: Some(config.max_connections_per_ip),
5353 max_connections_per_subnet: Some(config.max_connections_per_subnet),
5354 subnet_ipv4_prefix: Some(config.subnet_ipv4_prefix),
5355 subnet_ipv6_prefix: Some(config.subnet_ipv6_prefix),
5356 retry_after: Some(config.retry_after),
5357 splice_pipe_capacity_bytes: config.splice_pipe_capacity_bytes,
5358 max_connection_attempts: Some(config.max_connection_attempts),
5359 };
5360
5361 debug_assert!(
5366 server_config.min_buffers <= server_config.max_buffers,
5367 "ServerConfig must preserve min_buffers <= max_buffers"
5368 );
5369 debug_assert_eq!(
5370 server_config.buffer_size, config.buffer_size,
5371 "ServerConfig buffer_size must mirror the source config"
5372 );
5373 debug_assert_eq!(
5374 server_config.max_connections, config.max_connections as u64,
5375 "ServerConfig max_connections must mirror the source config"
5376 );
5377 server_config
5378 }
5379}
5380
5381#[cfg(test)]
5382mod tests {
5383 use toml::to_string;
5384
5385 use super::*;
5386
5387 #[test]
5388 fn http_frontend_debug_redacts_pem_material() {
5389 const CERTIFICATE_SECRET: &str = "HTTP_FRONTEND_CERTIFICATE_PEM_SECRET_SENTINEL";
5390 const CHAIN_SECRET: &str = "HTTP_FRONTEND_CHAIN_PEM_SECRET_SENTINEL";
5391 const KEY_SECRET: &str = "HTTP_FRONTEND_KEY_PEM_SECRET_SENTINEL";
5392 const HOSTNAME_SECRET: &str = "HTTP_FRONTEND_HOSTNAME_SECRET_SENTINEL";
5393 const PATH_SECRET: &str = "HTTP_FRONTEND_PATH_SECRET_SENTINEL";
5394 const METHOD_SECRET: &str = "HTTP_FRONTEND_METHOD_SECRET_SENTINEL";
5395 const TAG_KEY_SECRET: &str = "HTTP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
5396 const TAG_SECRET: &str = "HTTP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
5397 const HEADER_KEY_SECRET: &str = "HTTP_FRONTEND_HEADER_KEY_SECRET_SENTINEL";
5398 const HEADER_SECRET: &str = "HTTP_FRONTEND_HEADER_VALUE_SECRET_SENTINEL";
5399 const REDIRECT_TEMPLATE_SECRET: &str = "HTTP_FRONTEND_REDIRECT_TEMPLATE_SECRET_SENTINEL";
5400 const REWRITE_HOST_SECRET: &str = "HTTP_FRONTEND_REWRITE_HOST_SECRET_SENTINEL";
5401 const REWRITE_PATH_SECRET: &str = "HTTP_FRONTEND_REWRITE_PATH_SECRET_SENTINEL";
5402
5403 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
5404 let certificate = long_value(CERTIFICATE_SECRET);
5405 let certificate_chain = long_value(CHAIN_SECRET);
5406 let key = long_value(KEY_SECRET);
5407 let hostname = long_value(HOSTNAME_SECRET);
5408 let path = long_value(PATH_SECRET);
5409 let method = long_value(METHOD_SECRET);
5410 let redirect_template = long_value(REDIRECT_TEMPLATE_SECRET);
5411 let rewrite_host = long_value(REWRITE_HOST_SECRET);
5412 let rewrite_path = long_value(REWRITE_PATH_SECRET);
5413
5414 let frontend = HttpFrontendConfig {
5415 address: "127.0.0.1:8443".parse().unwrap(),
5416 hostname,
5417 path: PathRule::prefix(path),
5418 method: Some(method),
5419 certificate: Some(certificate),
5420 key: Some(key),
5421 certificate_chain: Some(vec![certificate_chain]),
5422 tls_versions: vec![TlsVersion::TlsV13],
5423 position: RulePosition::Tree,
5424 tags: Some(BTreeMap::from([(
5425 long_value(TAG_KEY_SECRET),
5426 long_value(TAG_SECRET),
5427 )])),
5428 redirect: None,
5429 redirect_scheme: None,
5430 redirect_template: Some(redirect_template),
5431 rewrite_host: Some(rewrite_host),
5432 rewrite_path: Some(rewrite_path),
5433 rewrite_port: None,
5434 required_auth: None,
5435 headers: vec![Header {
5436 position: HeaderPosition::Request as i32,
5437 key: long_value(HEADER_KEY_SECRET),
5438 val: long_value(HEADER_SECRET),
5439 }],
5440 hsts: None,
5441 };
5442
5443 let output = format!("{frontend:?}");
5444
5445 let secrets = [
5446 CERTIFICATE_SECRET,
5447 CHAIN_SECRET,
5448 KEY_SECRET,
5449 HOSTNAME_SECRET,
5450 PATH_SECRET,
5451 METHOD_SECRET,
5452 TAG_KEY_SECRET,
5453 TAG_SECRET,
5454 HEADER_KEY_SECRET,
5455 HEADER_SECRET,
5456 REDIRECT_TEMPLATE_SECRET,
5457 REWRITE_HOST_SECRET,
5458 REWRITE_PATH_SECRET,
5459 ];
5460 for secret in secrets {
5461 assert!(
5462 !output.contains(secret),
5463 "HttpFrontendConfig Debug leaked secret marker {secret}: {output}"
5464 );
5465 }
5466 let expected_metadata = [
5467 "address: 127.0.0.1:8443".to_owned(),
5468 format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
5469 format!("path_kind: {}", PathRule::prefix(String::new()).kind),
5470 format!("path_len: {}", long_value(PATH_SECRET).len()),
5471 format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
5472 "certificate: Some(\"[redacted]\")".to_owned(),
5473 format!(
5474 "certificate_len: Some({})",
5475 long_value(CERTIFICATE_SECRET).len()
5476 ),
5477 "key: Some(\"[redacted]\")".to_owned(),
5478 format!("key_len: Some({})", long_value(KEY_SECRET).len()),
5479 "certificate_chain: Some(\"[redacted]\")".to_owned(),
5480 "certificate_chain_count: Some(1)".to_owned(),
5481 format!(
5482 "certificate_chain_len: Some({})",
5483 long_value(CHAIN_SECRET).len()
5484 ),
5485 "tls_versions_count: 1".to_owned(),
5486 "tags_count: 1".to_owned(),
5487 format!(
5488 "redirect_template_len: Some({})",
5489 long_value(REDIRECT_TEMPLATE_SECRET).len()
5490 ),
5491 format!(
5492 "rewrite_host_len: Some({})",
5493 long_value(REWRITE_HOST_SECRET).len()
5494 ),
5495 format!(
5496 "rewrite_path_len: Some({})",
5497 long_value(REWRITE_PATH_SECRET).len()
5498 ),
5499 "headers_count: 1".to_owned(),
5500 ];
5501 for safe_metadata in expected_metadata {
5502 assert!(
5503 output.contains(&safe_metadata),
5504 "HttpFrontendConfig Debug omitted safe metadata {safe_metadata}: {output}"
5505 );
5506 }
5507 assert!(
5508 output.len() <= 1024,
5509 "HttpFrontendConfig Debug output is not bounded: {} bytes",
5510 output.len()
5511 );
5512
5513 for (index, request) in frontend
5514 .generate_requests("safe-cluster-id")
5515 .into_iter()
5516 .enumerate()
5517 {
5518 let generated_output = format!("{request:?}");
5519 for secret in secrets {
5520 assert!(
5521 !generated_output.contains(secret),
5522 "generated frontend request {index} Debug leaked secret marker {secret}: {generated_output}"
5523 );
5524 }
5525 assert!(
5526 generated_output.len() <= 2048,
5527 "generated frontend request {index} Debug output is not bounded: {} bytes",
5528 generated_output.len()
5529 );
5530 }
5531 }
5532
5533 #[test]
5534 fn hsts_to_proto_enabled_substitutes_default_max_age() {
5535 let cfg = FileHstsConfig {
5536 enabled: Some(true),
5537 max_age: None,
5538 include_subdomains: None,
5539 preload: None,
5540 force_replace_backend: None,
5541 };
5542 let proto = cfg.to_proto("test").expect("should validate");
5543 assert_eq!(proto.enabled, Some(true));
5544 assert_eq!(proto.max_age, Some(DEFAULT_HSTS_MAX_AGE));
5545 }
5546
5547 #[test]
5548 fn hsts_to_proto_explicit_max_age_kept() {
5549 let cfg = FileHstsConfig {
5550 enabled: Some(true),
5551 max_age: Some(63_072_000),
5552 include_subdomains: Some(true),
5553 preload: Some(true),
5554 force_replace_backend: None,
5555 };
5556 let proto = cfg.to_proto("test").expect("should validate");
5557 assert_eq!(proto.max_age, Some(63_072_000));
5558 assert_eq!(proto.include_subdomains, Some(true));
5559 assert_eq!(proto.preload, Some(true));
5560 }
5561
5562 #[test]
5563 fn hsts_to_proto_disabled_keeps_zero_intent() {
5564 let cfg = FileHstsConfig {
5568 enabled: Some(false),
5569 max_age: None,
5570 include_subdomains: None,
5571 preload: None,
5572 force_replace_backend: None,
5573 };
5574 let proto = cfg.to_proto("test").expect("should validate");
5575 assert_eq!(proto.enabled, Some(false));
5576 }
5577
5578 #[test]
5579 fn hsts_to_proto_kill_switch_max_age_zero_allowed() {
5580 let cfg = FileHstsConfig {
5584 enabled: Some(true),
5585 max_age: Some(0),
5586 include_subdomains: None,
5587 preload: None,
5588 force_replace_backend: None,
5589 };
5590 let proto = cfg.to_proto("test").expect("kill-switch must validate");
5591 assert_eq!(proto.max_age, Some(0));
5592 }
5593
5594 #[test]
5595 fn hsts_to_proto_missing_enabled_errors() {
5596 let cfg = FileHstsConfig {
5597 enabled: None,
5598 max_age: Some(31_536_000),
5599 include_subdomains: None,
5600 preload: None,
5601 force_replace_backend: None,
5602 };
5603 match cfg.to_proto("test").unwrap_err() {
5604 ConfigError::HstsEnabledRequired(scope) => assert_eq!(scope, "test"),
5605 other => panic!("expected HstsEnabledRequired, got {other:?}"),
5606 }
5607 }
5608
5609 #[test]
5610 fn hsts_rejected_on_http_listener() {
5611 let mut listener = ListenerBuilder::new(
5616 SocketAddress::new_v4(127, 0, 0, 1, 8080),
5617 ListenerProtocol::Http,
5618 );
5619 listener.hsts = Some(FileHstsConfig {
5620 enabled: Some(true),
5621 max_age: Some(31_536_000),
5622 include_subdomains: None,
5623 preload: None,
5624 force_replace_backend: None,
5625 });
5626 match listener.to_http(None).unwrap_err() {
5627 ConfigError::HstsOnPlainHttp(scope) => assert!(
5628 scope.contains("HTTP listener"),
5629 "expected scope to mention 'HTTP listener', got {scope:?}"
5630 ),
5631 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
5632 }
5633 }
5634
5635 #[test]
5644 fn h2_flood_threshold_zero_rejected_on_http_listener() {
5645 let mut listener = ListenerBuilder::new(
5646 SocketAddress::new_v4(127, 0, 0, 1, 8080),
5647 ListenerProtocol::Http,
5648 );
5649 listener.h2_max_rst_stream_per_window = Some(0);
5650 match listener.to_http(None).unwrap_err() {
5651 ConfigError::H2ThresholdBelowMinimum {
5652 key,
5653 value,
5654 minimum,
5655 ..
5656 } => {
5657 assert_eq!(key, "h2_max_rst_stream_per_window");
5658 assert_eq!(value, 0);
5659 assert_eq!(minimum, 1);
5660 }
5661 other => panic!("expected H2ThresholdBelowMinimum, got {other:?}"),
5662 }
5663 }
5664
5665 #[test]
5669 fn h2_flood_threshold_zero_rejected_on_https_listener() {
5670 let mut listener = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5671 listener.h2_max_rst_stream_abusive_lifetime = Some(0);
5672 match listener.to_tls(None).unwrap_err() {
5673 ConfigError::H2ThresholdBelowMinimum {
5674 key,
5675 value,
5676 minimum,
5677 ..
5678 } => {
5679 assert_eq!(key, "h2_max_rst_stream_abusive_lifetime");
5680 assert_eq!(value, 0);
5681 assert_eq!(minimum, 1);
5682 }
5683 other => panic!("expected H2ThresholdBelowMinimum, got {other:?}"),
5684 }
5685 }
5686
5687 #[test]
5692 fn h2_stream_shrink_ratio_below_two_rejected_on_http_listener() {
5693 let mut listener = ListenerBuilder::new(
5694 SocketAddress::new_v4(127, 0, 0, 1, 8080),
5695 ListenerProtocol::Http,
5696 );
5697 listener.h2_stream_shrink_ratio = Some(1);
5698 match listener.to_http(None).unwrap_err() {
5699 ConfigError::H2ThresholdBelowMinimum {
5700 key,
5701 value,
5702 minimum,
5703 ..
5704 } => {
5705 assert_eq!(key, "h2_stream_shrink_ratio");
5706 assert_eq!(value, 1);
5707 assert_eq!(minimum, 2);
5708 }
5709 other => panic!("expected H2ThresholdBelowMinimum, got {other:?}"),
5710 }
5711 }
5712
5713 #[test]
5716 fn h2_thresholds_at_their_minimum_accepted() {
5717 let mut listener = ListenerBuilder::new(
5718 SocketAddress::new_v4(127, 0, 0, 1, 8080),
5719 ListenerProtocol::Http,
5720 );
5721 listener.h2_max_rst_stream_per_window = Some(1);
5722 listener.h2_max_rst_stream_abusive_lifetime = Some(1);
5723 listener.h2_stream_shrink_ratio = Some(2);
5724 let config = listener
5725 .to_http(None)
5726 .expect("thresholds at their minimum must be accepted");
5727 assert_eq!(config.h2_max_rst_stream_per_window, Some(1));
5728 assert_eq!(config.h2_max_rst_stream_abusive_lifetime, Some(1));
5729 assert_eq!(config.h2_stream_shrink_ratio, Some(2));
5730 }
5731
5732 #[test]
5733 fn hsts_rejected_on_http_frontend() {
5734 let frontend = FileClusterFrontendConfig {
5740 address: "127.0.0.1:8080".parse().unwrap(),
5741 hostname: Some("example.com".to_owned()),
5742 alpn: vec![],
5743 path: None,
5744 path_type: None,
5745 method: None,
5746 certificate: None,
5747 key: None,
5748 certificate_chain: None,
5749 tls_versions: vec![],
5750 position: RulePosition::Tree,
5751 tags: None,
5752 redirect: None,
5753 redirect_scheme: None,
5754 redirect_template: None,
5755 rewrite_host: None,
5756 rewrite_path: None,
5757 rewrite_port: None,
5758 required_auth: None,
5759 headers: None,
5760 hsts: Some(FileHstsConfig {
5761 enabled: Some(true),
5762 max_age: Some(31_536_000),
5763 include_subdomains: None,
5764 preload: None,
5765 force_replace_backend: None,
5766 }),
5767 };
5768 match frontend.to_http_front("api").unwrap_err() {
5769 ConfigError::HstsOnPlainHttp(scope) => {
5770 assert!(
5771 scope.contains("api") && scope.contains("example.com"),
5772 "expected scope to mention 'api' and 'example.com', got {scope:?}"
5773 );
5774 }
5775 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
5776 }
5777 }
5778
5779 #[test]
5780 fn serialize() {
5781 let http = ListenerBuilder::new(
5782 SocketAddress::new_v4(127, 0, 0, 1, 8080),
5783 ListenerProtocol::Http,
5784 )
5785 .with_answer_404_path(Some("404.html"))
5786 .to_owned();
5787 println!("http: {:?}", to_string(&http));
5788
5789 let https = ListenerBuilder::new(
5790 SocketAddress::new_v4(127, 0, 0, 1, 8443),
5791 ListenerProtocol::Https,
5792 )
5793 .with_answer_404_path(Some("404.html"))
5794 .to_owned();
5795 println!("https: {:?}", to_string(&https));
5796
5797 let listeners = vec![http, https];
5798 let config = FileConfig {
5799 command_socket: Some(String::from("./command_folder/sock")),
5800 worker_count: Some(2),
5801 worker_automatic_restart: Some(true),
5802 max_connections: Some(500),
5803 min_buffers: Some(1),
5804 max_buffers: Some(500),
5805 buffer_size: Some(16393),
5806 metrics: Some(MetricsConfig {
5807 address: "127.0.0.1:8125".parse().unwrap(),
5808 tagged_metrics: false,
5809 prefix: Some(String::from("sozu-metrics")),
5810 detail: MetricDetailLevel::default(),
5811 }),
5812 listeners: Some(listeners),
5813 ..Default::default()
5814 };
5815
5816 println!("config: {:?}", to_string(&config));
5817 let encoded = to_string(&config).unwrap();
5818 println!("conf:\n{encoded}");
5819 }
5820
5821 #[test]
5822 fn parse() {
5823 let path = "assets/config.toml";
5824 let config = Config::load_from_path(path).unwrap_or_else(|load_error| {
5825 panic!("Cannot load config from path {path}: {load_error:?}")
5826 });
5827 println!("config: {config:#?}");
5828 }
5830
5831 #[test]
5832 fn multiple_listeners_preserve_per_address_expect_proxy() {
5833 let toml_content = r#"
5834 command_socket = "/tmp/sozu_test.sock"
5835 worker_count = 1
5836
5837 [[listeners]]
5838 protocol = "http"
5839 address = "172.16.20.1:80"
5840 expect_proxy = true
5841
5842 [[listeners]]
5843 protocol = "http"
5844 address = "10.22.0.1:80"
5845 expect_proxy = false
5846
5847 [[listeners]]
5848 protocol = "https"
5849 address = "192.168.1.1:443"
5850 expect_proxy = true
5851
5852 [[listeners]]
5853 protocol = "https"
5854 address = "192.168.2.1:443"
5855 expect_proxy = false
5856 "#;
5857
5858 let file_config: FileConfig =
5859 toml::from_str(toml_content).expect("Could not parse TOML config");
5860
5861 let listeners = file_config.listeners.as_ref().expect("No listeners found");
5862 assert_eq!(listeners.len(), 4);
5863
5864 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
5865 .into_config()
5866 .expect("Could not build config");
5867
5868 assert_eq!(config.http_listeners.len(), 2);
5869 assert_eq!(config.https_listeners.len(), 2);
5870
5871 let http_proxy = config
5873 .http_listeners
5874 .iter()
5875 .find(|l| SocketAddr::from(l.address) == "172.16.20.1:80".parse().unwrap())
5876 .expect("Listener on 172.16.20.1:80 not found");
5877 let http_direct = config
5878 .http_listeners
5879 .iter()
5880 .find(|l| SocketAddr::from(l.address) == "10.22.0.1:80".parse().unwrap())
5881 .expect("Listener on 10.22.0.1:80 not found");
5882
5883 assert!(http_proxy.expect_proxy);
5884 assert!(!http_direct.expect_proxy);
5885
5886 let https_proxy = config
5888 .https_listeners
5889 .iter()
5890 .find(|l| SocketAddr::from(l.address) == "192.168.1.1:443".parse().unwrap())
5891 .expect("Listener on 192.168.1.1:443 not found");
5892 let https_direct = config
5893 .https_listeners
5894 .iter()
5895 .find(|l| SocketAddr::from(l.address) == "192.168.2.1:443".parse().unwrap())
5896 .expect("Listener on 192.168.2.1:443 not found");
5897
5898 assert!(https_proxy.expect_proxy);
5899 assert!(!https_direct.expect_proxy);
5900 }
5901
5902 #[test]
5905 fn an_affinity_header_reaches_the_add_cluster_order() {
5906 let file: FileClusterConfig = toml::from_str(
5907 r#"
5908 protocol = "http"
5909 load_balancing = "HRW"
5910 affinity_header = "X-Tenant"
5911 frontends = []
5912 backends = []
5913 "#,
5914 )
5915 .expect("the cluster must parse");
5916 let ClusterConfig::Http(http) = file
5917 .to_cluster_config("keyed", &HashSet::new())
5918 .expect("the cluster must build")
5919 else {
5920 panic!("an http cluster must build as one");
5921 };
5922 let orders = http.generate_requests().expect("the orders must build");
5923 let Some(RequestType::AddCluster(cluster)) =
5924 orders.first().and_then(|order| order.request_type.as_ref())
5925 else {
5926 panic!("the orders must lead with an AddCluster");
5927 };
5928 assert_eq!(cluster.affinity_header.as_deref(), Some("X-Tenant"));
5929 assert_eq!(cluster.affinity_cookie, None);
5930 }
5931
5932 #[test]
5935 fn a_tcp_cluster_refuses_an_affinity_header_or_cookie() {
5936 for key in [
5937 "affinity_header = \"X-Tenant\"",
5938 "affinity_cookie = \"tenant\"",
5939 ] {
5940 let file: FileClusterConfig = toml::from_str(&format!(
5941 "protocol = \"tcp\"\nload_balancing = \"MAGLEV\"\n{key}\nfrontends = []\nbackends = []\n"
5942 ))
5943 .expect("the cluster must parse");
5944 assert!(
5945 matches!(
5946 file.to_cluster_config("keyed", &HashSet::new()),
5947 Err(ConfigError::InvalidAffinityKey { .. })
5948 ),
5949 "a TCP cluster must refuse `{key}`"
5950 );
5951 }
5952 }
5953
5954 #[test]
5956 fn validate_affinity_key_accepts_one_token_and_refuses_the_rest() {
5957 assert!(validate_affinity_key(None, None).is_ok());
5958 assert!(validate_affinity_key(Some("X-Tenant"), None).is_ok());
5959 assert!(validate_affinity_key(None, Some("tenant_id")).is_ok());
5960 assert!(validate_affinity_key(Some("X-Tenant"), Some("tenant")).is_err());
5961 for unreachable in ["Host", "host", "HOST", "Cookie", "cookie"] {
5962 assert!(
5963 validate_affinity_key(Some(unreachable), None).is_err(),
5964 "header {unreachable:?} can never be found and must be refused"
5965 );
5966 }
5967 assert!(validate_affinity_key(None, Some("host")).is_ok());
5969 for bad in ["", "X Tenant", "X-Tenant:", "tenant\r\n", "t=1"] {
5970 assert!(
5971 validate_affinity_key(Some(bad), None).is_err(),
5972 "header {bad:?} must be refused"
5973 );
5974 assert!(
5975 validate_affinity_key(None, Some(bad)).is_err(),
5976 "cookie {bad:?} must be refused"
5977 );
5978 }
5979 }
5980
5981 #[test]
5984 fn shuffle_sharding_reaches_the_add_cluster_order_and_is_validated() {
5985 let parse = |extra: &str| -> FileClusterConfig {
5986 toml::from_str(&format!(
5987 "protocol = \"http\"\nfrontends = []\nbackends = []\n{extra}"
5988 ))
5989 .expect("the cluster must parse")
5990 };
5991 let ClusterConfig::Http(http) =
5992 parse("shard_percent = 25\nshard_min_backends = 4\nshard_mode = \"STRICT\"\n")
5993 .to_cluster_config("sharded", &HashSet::new())
5994 .expect("a valid sharded cluster builds")
5995 else {
5996 panic!("an http cluster must build as one");
5997 };
5998 let orders = http.generate_requests().expect("the orders must build");
5999 let Some(RequestType::AddCluster(cluster)) =
6000 orders.first().and_then(|order| order.request_type.as_ref())
6001 else {
6002 panic!("the orders must lead with an AddCluster");
6003 };
6004 assert_eq!(
6005 (
6006 cluster.shard_percent,
6007 cluster.shard_min_backends,
6008 cluster.shard_mode
6009 ),
6010 (Some(25), Some(4), Some(ShardMode::Strict as i32))
6011 );
6012 for bad in [
6013 "shard_percent = 0\n",
6014 "shard_percent = 101\n",
6015 "shard_percent = 25\nshard_min_backends = 1\n",
6016 "shard_min_backends = 8\n",
6017 "shard_mode = \"STRICT\"\n",
6018 ] {
6019 assert!(
6020 matches!(
6021 parse(bad).to_cluster_config("sharded", &HashSet::new()),
6022 Err(ConfigError::InvalidShuffleSharding { .. })
6023 ),
6024 "{bad:?} must be refused"
6025 );
6026 }
6027 }
6028
6029 #[test]
6032 fn http_status_range_parses_codes_ranges_classes_and_any() {
6033 let range = |start, end| HttpStatusRange { start, end };
6034 for (spec, expected) in [
6035 ("404", range(404, 404)),
6036 ("200-399", range(200, 399)),
6037 (" 200 - 399 ", range(200, 399)),
6038 ("2xx", range(200, 299)),
6039 ("5XX", range(500, 599)),
6040 ("any", range(100, 599)),
6041 ("ANY", range(100, 599)),
6042 ("100-599", range(100, 599)),
6043 ] {
6044 assert_eq!(parse_http_status_range(spec), Ok(expected), "{spec:?}");
6045 }
6046 for spec in [
6047 "", "abc", "99", "600", "0", "6xx", "0xx", "2x", "2xxx", "399-200", "200-", "-200",
6048 "+200", "200-600", "1-2-3", "2xx-3xx",
6049 ] {
6050 assert!(
6051 parse_http_status_range(spec).is_err(),
6052 "{spec:?} must be refused"
6053 );
6054 }
6055 }
6056
6057 #[test]
6060 fn http_status_range_display_round_trips_through_the_parser() {
6061 for spec in ["404", "200-399", "2xx", "any", "101-102"] {
6062 let range = parse_http_status_range(spec).unwrap();
6063 assert_eq!(range.to_string(), spec);
6064 assert_eq!(parse_http_status_range(&range.to_string()), Ok(range));
6065 }
6066 }
6067
6068 #[test]
6073 fn health_check_validation_covers_mode_uri_and_accepted_statuses() {
6074 let http = HealthCheckConfig {
6075 uri: "/health".to_owned(),
6076 interval: 10,
6077 timeout: 5,
6078 healthy_threshold: 3,
6079 unhealthy_threshold: 3,
6080 expected_status: 0,
6081 mode: HealthCheckMode::Http as i32,
6082 accepted_statuses: Vec::new(),
6083 };
6084 assert_eq!(validate_health_check_config(&http), Ok(()));
6085
6086 let tcp_no_uri = HealthCheckConfig {
6088 uri: String::new(),
6089 mode: HealthCheckMode::Tcp as i32,
6090 ..http.clone()
6091 };
6092 assert_eq!(validate_health_check_config(&tcp_no_uri), Ok(()));
6093 let http_no_uri = HealthCheckConfig {
6094 uri: String::new(),
6095 ..http.clone()
6096 };
6097 assert!(validate_health_check_config(&http_no_uri).is_err());
6098 let tcp_crlf = HealthCheckConfig {
6100 uri: "/a\r\nGET /b".to_owned(),
6101 ..tcp_no_uri.clone()
6102 };
6103 assert!(validate_health_check_config(&tcp_crlf).is_err());
6104
6105 let tcp_expected = HealthCheckConfig {
6107 expected_status: 200,
6108 ..tcp_no_uri.clone()
6109 };
6110 assert!(validate_health_check_config(&tcp_expected).is_err());
6111 let tcp_accepted = HealthCheckConfig {
6112 accepted_statuses: vec![HttpStatusRange {
6113 start: 100,
6114 end: 599,
6115 }],
6116 ..tcp_no_uri.clone()
6117 };
6118 assert!(validate_health_check_config(&tcp_accepted).is_err());
6119
6120 let unknown_mode = HealthCheckConfig {
6121 mode: 7,
6122 ..http.clone()
6123 };
6124 assert!(validate_health_check_config(&unknown_mode).is_err());
6125
6126 let accepted = HealthCheckConfig {
6127 accepted_statuses: vec![HttpStatusRange {
6128 start: 200,
6129 end: 499,
6130 }],
6131 ..http.clone()
6132 };
6133 assert_eq!(validate_health_check_config(&accepted), Ok(()));
6134 let both = HealthCheckConfig {
6135 expected_status: 200,
6136 ..accepted.clone()
6137 };
6138 assert!(
6139 validate_health_check_config(&both).is_err(),
6140 "expected_status and accepted_statuses must not both be set"
6141 );
6142 for (start, end) in [(99, 200), (200, 600), (300, 200)] {
6143 let bad_range = HealthCheckConfig {
6144 accepted_statuses: vec![HttpStatusRange { start, end }],
6145 ..http.clone()
6146 };
6147 assert!(
6148 validate_health_check_config(&bad_range).is_err(),
6149 "{start}-{end} must be refused"
6150 );
6151 }
6152 }
6153
6154 #[test]
6158 fn health_check_toml_mode_and_accepted_statuses_reach_the_add_cluster_order() {
6159 let parse = |block: &str| -> FileClusterConfig {
6160 toml::from_str(&format!(
6161 "protocol = \"http\"\nfrontends = []\nbackends = []\n[health_check]\n{block}"
6162 ))
6163 .expect("the cluster must parse")
6164 };
6165 let health_check = |block: &str| -> HealthCheckConfig {
6166 let ClusterConfig::Http(http) = parse(block)
6167 .to_cluster_config("checked", &HashSet::new())
6168 .expect("a valid health check builds")
6169 else {
6170 panic!("an http cluster must build as one");
6171 };
6172 let orders = http.generate_requests().expect("the orders must build");
6173 let Some(RequestType::AddCluster(cluster)) =
6174 orders.first().and_then(|order| order.request_type.as_ref())
6175 else {
6176 panic!("the orders must lead with an AddCluster");
6177 };
6178 cluster
6179 .health_check
6180 .clone()
6181 .expect("the health check must travel")
6182 };
6183
6184 let legacy = health_check("uri = \"/health\"\nexpected_status = 204\n");
6186 assert_eq!(legacy.mode, HealthCheckMode::Http as i32);
6187 assert_eq!(
6188 (legacy.uri.as_str(), legacy.expected_status),
6189 ("/health", 204)
6190 );
6191 assert!(legacy.accepted_statuses.is_empty());
6192
6193 let defaulted = health_check("");
6194 assert_eq!(defaulted.mode, HealthCheckMode::Http as i32);
6195 assert_eq!(defaulted.uri, "/");
6196
6197 let tcp = health_check("mode = \"TCP\"\n");
6198 assert_eq!(tcp.mode, HealthCheckMode::Tcp as i32);
6199
6200 let accepted = health_check("uri = \"/livez\"\naccepted_statuses = [\"2xx\", \"404\"]\n");
6201 assert_eq!(
6202 accepted.accepted_statuses,
6203 vec![
6204 HttpStatusRange {
6205 start: 200,
6206 end: 299
6207 },
6208 HttpStatusRange {
6209 start: 404,
6210 end: 404
6211 },
6212 ]
6213 );
6214
6215 for bad in [
6216 "accepted_statuses = [\"6xx\"]\n",
6217 "expected_status = 200\naccepted_statuses = [\"any\"]\n",
6218 "uri = \"health\"\n",
6219 "mode = \"TCP\"\nexpected_status = 200\n",
6220 "mode = \"TCP\"\naccepted_statuses = [\"any\"]\n",
6221 ] {
6222 assert!(
6223 matches!(
6224 parse(bad).to_cluster_config("checked", &HashSet::new()),
6225 Err(ConfigError::InvalidHealthCheck { .. })
6226 ),
6227 "{bad:?} must be refused"
6228 );
6229 }
6230 }
6231
6232 #[test]
6233 fn multiple_listeners_generate_correct_worker_requests() {
6234 let toml_content = r#"
6235 command_socket = "/tmp/sozu_test.sock"
6236 worker_count = 1
6237 activate_listeners = true
6238
6239 [[listeners]]
6240 protocol = "http"
6241 address = "172.16.20.1:80"
6242 expect_proxy = true
6243
6244 [[listeners]]
6245 protocol = "http"
6246 address = "10.22.0.1:80"
6247 expect_proxy = false
6248 "#;
6249
6250 let file_config: FileConfig =
6251 toml::from_str(toml_content).expect("Could not parse TOML config");
6252
6253 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
6254 .into_config()
6255 .expect("Could not build config");
6256
6257 let messages = config
6258 .generate_config_messages()
6259 .expect("Could not generate config messages");
6260
6261 let add_listener_count = messages
6262 .iter()
6263 .filter(|m| {
6264 matches!(
6265 m.content.request_type,
6266 Some(RequestType::AddHttpListener(_))
6267 )
6268 })
6269 .count();
6270
6271 let activate_listener_count = messages
6272 .iter()
6273 .filter(|m| {
6274 matches!(
6275 m.content.request_type,
6276 Some(RequestType::ActivateListener(ActivateListener {
6277 proxy,
6278 ..
6279 })) if proxy == ListenerType::Http as i32
6280 )
6281 })
6282 .count();
6283
6284 assert_eq!(add_listener_count, 2);
6285 assert_eq!(activate_listener_count, 2);
6286 }
6287
6288 #[test]
6289 fn documented_udp_dns_example_loads_and_emits_udp_requests() {
6290 let toml_content = r#"
6297 command_socket = "/tmp/sozu_test.sock"
6298 worker_count = 1
6299 activate_listeners = true
6300
6301 [[listeners]]
6302 protocol = "udp"
6303 address = "0.0.0.0:53"
6304
6305 [clusters.dns]
6306 protocol = "tcp"
6307 load_balancing = "HRW"
6308 frontends = [
6309 { address = "0.0.0.0:53" }
6310 ]
6311 backends = [
6312 { address = "10.0.0.10:53" },
6313 { address = "10.0.0.11:53" }
6314 ]
6315
6316 [clusters.dns.udp]
6317 affinity_key = "SOURCE_IP"
6318 responses = 1
6319 requests = 0
6320 send_proxy_protocol = true
6321
6322 [clusters.dns.udp.health]
6323 mode = "TCP_PROBE"
6324 tcp_port = 53
6325 rise = 2
6326 fall = 3
6327 fail_open = true
6328 "#;
6329
6330 let file_config: FileConfig =
6331 toml::from_str(toml_content).expect("Could not parse documented DNS TOML");
6332
6333 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
6334 .into_config()
6335 .expect("documented UDP DNS example must load without WrongFrontendProtocol");
6336
6337 assert_eq!(
6339 config.udp_listeners.len(),
6340 1,
6341 "the protocol=\"udp\" listener must be built"
6342 );
6343
6344 let messages = config
6345 .generate_config_messages()
6346 .expect("Could not generate config messages");
6347
6348 let add_udp_listener_count = messages
6349 .iter()
6350 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpListener(_))))
6351 .count();
6352 assert_eq!(
6353 add_udp_listener_count, 1,
6354 "must emit exactly one AddUdpListener"
6355 );
6356
6357 let add_udp_frontend_count = messages
6358 .iter()
6359 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
6360 .count();
6361 assert_eq!(
6362 add_udp_frontend_count, 1,
6363 "the cluster frontend on the UDP listener must emit AddUdpFrontend"
6364 );
6365
6366 let add_tcp_frontend_count = messages
6368 .iter()
6369 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddTcpFrontend(_))))
6370 .count();
6371 assert_eq!(
6372 add_tcp_frontend_count, 0,
6373 "a UDP-listener-addressed frontend must not be emitted as AddTcpFrontend"
6374 );
6375
6376 let udp_frontend = messages
6378 .iter()
6379 .find_map(|m| match &m.content.request_type {
6380 Some(RequestType::AddUdpFrontend(f)) => Some(f),
6381 _ => None,
6382 })
6383 .expect("AddUdpFrontend must be present");
6384 assert_eq!(udp_frontend.cluster_id, "dns");
6385 assert_eq!(
6386 SocketAddr::from(udp_frontend.address),
6387 "0.0.0.0:53".parse().unwrap()
6388 );
6389
6390 let cluster = messages
6393 .iter()
6394 .find_map(|m| match &m.content.request_type {
6395 Some(RequestType::AddCluster(c)) if c.cluster_id == "dns" => Some(c),
6396 _ => None,
6397 })
6398 .expect("AddCluster for 'dns' must be present");
6399 let udp = cluster
6400 .udp
6401 .as_ref()
6402 .expect("[clusters.dns.udp] block must carry onto the cluster");
6403 assert_eq!(udp.responses, Some(1));
6404 }
6405
6406 #[test]
6412 fn listener_interface_round_trips_from_the_config_file() {
6413 let toml_content = r#"
6414 command_socket = "/tmp/sozu_test.sock"
6415 worker_count = 1
6416 activate_listeners = true
6417
6418 [[listeners]]
6419 protocol = "http"
6420 address = "0.0.0.0:8080"
6421 interface = "wg0"
6422
6423 [[listeners]]
6424 protocol = "http"
6425 address = "0.0.0.0:8080"
6426 interface = "lo"
6427
6428 [[listeners]]
6429 protocol = "http"
6430 address = "0.0.0.0:8081"
6431
6432 [[listeners]]
6433 protocol = "https"
6434 address = "0.0.0.0:8443"
6435 interface = "wg0"
6436
6437 [[listeners]]
6438 protocol = "tcp"
6439 address = "0.0.0.0:9000"
6440 interface = "wg0"
6441
6442 [[listeners]]
6443 protocol = "udp"
6444 address = "0.0.0.0:5353"
6445 interface = "wg0"
6446 "#;
6447
6448 let file_config: FileConfig =
6449 toml::from_str(toml_content).expect("the interface key must parse");
6450 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
6451 .into_config()
6452 .expect("listeners on one address with different interfaces must build");
6453
6454 let http: Vec<(String, Option<&str>)> = config
6455 .http_listeners
6456 .iter()
6457 .map(|l| {
6458 (
6459 SocketAddr::from(l.address).to_string(),
6460 l.interface.as_deref(),
6461 )
6462 })
6463 .collect();
6464 assert_eq!(
6465 http,
6466 vec![
6467 ("0.0.0.0:8080".to_owned(), Some("wg0")),
6468 ("0.0.0.0:8080".to_owned(), Some("lo")),
6469 ("0.0.0.0:8081".to_owned(), None),
6470 ]
6471 );
6472 assert_eq!(config.https_listeners[0].interface.as_deref(), Some("wg0"));
6473 assert_eq!(config.tcp_listeners[0].interface.as_deref(), Some("wg0"));
6474 assert_eq!(config.udp_listeners[0].interface.as_deref(), Some("wg0"));
6475
6476 let activations: Vec<Option<String>> = config
6477 .generate_config_messages()
6478 .expect("generate the config messages")
6479 .into_iter()
6480 .filter_map(|request| match request.content.request_type {
6481 Some(RequestType::ActivateListener(activate)) => Some(activate.interface),
6482 _ => None,
6483 })
6484 .collect();
6485 assert_eq!(
6486 activations,
6487 vec![
6488 Some("wg0".to_owned()),
6489 Some("lo".to_owned()),
6490 None,
6491 Some("wg0".to_owned()),
6492 Some("wg0".to_owned()),
6493 Some("wg0".to_owned()),
6494 ]
6495 );
6496
6497 let duplicate = r#"
6498 command_socket = "/tmp/sozu_test.sock"
6499 worker_count = 1
6500
6501 [[listeners]]
6502 protocol = "http"
6503 address = "0.0.0.0:8080"
6504 interface = "wg0"
6505
6506 [[listeners]]
6507 protocol = "http"
6508 address = "0.0.0.0:8080"
6509 interface = "wg0"
6510 "#;
6511 let file_config: FileConfig = toml::from_str(duplicate).expect("parse");
6512 assert!(
6513 ConfigBuilder::new(file_config, "/tmp/test_config.toml")
6514 .into_config()
6515 .is_err(),
6516 "the same (address, interface) twice must be refused"
6517 );
6518 }
6519
6520 #[test]
6523 fn invalid_listener_interface_rejected_at_config_load() {
6524 let toml_content = r#"
6525 command_socket = "/tmp/sozu_test.sock"
6526 worker_count = 1
6527
6528 [[listeners]]
6529 protocol = "http"
6530 address = "0.0.0.0:8080"
6531 interface = "wg/0"
6532 "#;
6533 let file_config: FileConfig = toml::from_str(toml_content).expect("parse");
6534 let error = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
6535 .into_config()
6536 .expect_err("an invalid interface name must be refused");
6537 let message = error.to_string();
6538 assert!(message.contains("0.0.0.0:8080"), "{message}");
6539 assert!(message.contains("wg/0"), "{message}");
6540 }
6541
6542 #[test]
6543 fn duplicate_listener_address_rejected() {
6544 let toml_content = r#"
6545 command_socket = "/tmp/sozu_test.sock"
6546 worker_count = 1
6547
6548 [[listeners]]
6549 protocol = "http"
6550 address = "0.0.0.0:80"
6551
6552 [[listeners]]
6553 protocol = "http"
6554 address = "0.0.0.0:80"
6555 "#;
6556
6557 let file_config: FileConfig =
6558 toml::from_str(toml_content).expect("Could not parse TOML config");
6559
6560 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
6561
6562 assert!(
6563 result.is_err(),
6564 "Should reject duplicate listener addresses"
6565 );
6566 }
6567
6568 #[test]
6569 fn buffer_size_below_h2_minimum_rejected() {
6570 let toml_content = r#"
6572 command_socket = "/tmp/sozu_test.sock"
6573 worker_count = 1
6574 buffer_size = 8192
6575
6576 [[listeners]]
6577 protocol = "https"
6578 address = "127.0.0.1:8443"
6579 "#;
6580 let file_config: FileConfig =
6581 toml::from_str(toml_content).expect("Could not parse TOML config");
6582 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
6583 match result {
6584 Err(ConfigError::BufferSizeTooSmallForH2 {
6585 buffer_size: 8192,
6586 minimum: 16_393,
6587 listeners: 1,
6588 }) => {}
6589 other => panic!("expected BufferSizeTooSmallForH2, got {other:?}"),
6590 }
6591 }
6592
6593 #[test]
6594 fn buffer_size_below_h2_minimum_accepted_when_no_h2_listener() {
6595 let toml_content = r#"
6597 command_socket = "/tmp/sozu_test.sock"
6598 worker_count = 1
6599 buffer_size = 8192
6600
6601 [[listeners]]
6602 protocol = "https"
6603 address = "127.0.0.1:8443"
6604 alpn_protocols = ["http/1.1"]
6605 "#;
6606 let file_config: FileConfig =
6607 toml::from_str(toml_content).expect("Could not parse TOML config");
6608 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
6609 assert!(
6610 result.is_ok(),
6611 "non-H2 HTTPS listener with sub-16393 buffer should be accepted: {result:?}"
6612 );
6613 }
6614
6615 fn command_socket_mode_of(mode_line: &str) -> Result<Config, ConfigError> {
6616 let toml_content = format!(
6617 r#"
6618 command_socket = "/tmp/sozu_test.sock"
6619 worker_count = 1
6620 {mode_line}
6621 "#
6622 );
6623 let file_config: FileConfig =
6624 toml::from_str(&toml_content).expect("Could not parse TOML config");
6625 ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config()
6626 }
6627
6628 #[test]
6629 fn command_socket_mode_defaults_to_0600() {
6630 let config = command_socket_mode_of("").expect("config without the key must load");
6631 assert_eq!(config.command_socket_mode, 0o600);
6632 assert_eq!(DEFAULT_COMMAND_SOCKET_MODE, 0o600);
6633 }
6634
6635 #[test]
6636 fn command_socket_mode_accepts_octal_strings() {
6637 for (value, expected) in [
6638 ("0660", 0o660),
6639 ("660", 0o660),
6640 ("0600", 0o600),
6641 ("0666", 0o666),
6642 ("777", 0o777),
6643 ("0777", 0o777),
6644 ("600", 0o600),
6645 ("0640", 0o640),
6646 ] {
6647 let config = command_socket_mode_of(&format!("command_socket_mode = \"{value}\""))
6648 .unwrap_or_else(|e| panic!("{value:?} must load: {e}"));
6649 assert_eq!(config.command_socket_mode, expected, "{value:?}");
6650 }
6651 }
6652
6653 #[test]
6654 fn command_socket_mode_rejects_invalid_values() {
6655 for value in [
6656 "", "8", "0668", "abc", "0o660", "0x1b6", "-660", "+660", " 660", "660 ", "1660",
6657 "2770", "4755", "7777", "01000", "00660", "0", "0000", "0066", "0200", "0400", "0466",
6658 ] {
6659 match command_socket_mode_of(&format!("command_socket_mode = \"{value}\"")) {
6660 Err(ConfigError::InvalidCommandSocketMode {
6661 value: rejected, ..
6662 }) => {
6663 assert_eq!(rejected, value)
6664 }
6665 other => panic!("{value:?} must be rejected, got {other:?}"),
6666 }
6667 }
6668 }
6669
6670 #[test]
6671 fn command_socket_mode_rejects_a_toml_integer() {
6672 let dir = tempfile::tempdir().expect("tempdir");
6676 let path = dir.path().join("config.toml");
6677 std::fs::write(
6678 &path,
6679 "command_socket = \"/tmp/sozu_test.sock\"\ncommand_socket_mode = 660\n",
6680 )
6681 .expect("write config");
6682 match FileConfig::load_from_path(path.to_str().expect("utf-8 path")) {
6683 Err(ConfigError::DeserializeToml(message)) => assert!(
6684 message.contains("invalid type: integer"),
6685 "unexpected message: {message}"
6686 ),
6687 other => panic!("an integer mode must fail as DeserializeToml, got {other:?}"),
6688 }
6689 }
6690
6691 #[test]
6692 fn config_without_command_socket_mode_deserializes_to_0600() {
6693 let mut encoded = serde_json::to_value(Config::default()).expect("serialize Config");
6696 encoded
6697 .as_object_mut()
6698 .expect("Config serializes to an object")
6699 .remove("command_socket_mode")
6700 .expect("Config serializes command_socket_mode");
6701 let decoded: Config = serde_json::from_value(encoded).expect("deserialize Config");
6702 assert_eq!(decoded.command_socket_mode, 0o600);
6703 }
6704
6705 #[test]
6706 fn command_buffer_size_exceeds_max_rejected() {
6707 let toml_content = r#"
6712 command_socket = "/tmp/sozu_test.sock"
6713 worker_count = 1
6714 command_buffer_size = 2000000
6715 max_command_buffer_size = 1000000
6716 "#;
6717 let file_config: FileConfig =
6718 toml::from_str(toml_content).expect("Could not parse TOML config");
6719 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
6720 match result {
6721 Err(ConfigError::CommandBufferSizeExceedsMax {
6722 command_buffer_size: 2_000_000,
6723 max_command_buffer_size: 1_000_000,
6724 }) => {}
6725 other => panic!("expected CommandBufferSizeExceedsMax, got {other:?}"),
6726 }
6727 }
6728
6729 #[test]
6730 fn command_buffer_size_equal_to_max_accepted() {
6731 let toml_content = r#"
6734 command_socket = "/tmp/sozu_test.sock"
6735 worker_count = 1
6736 command_buffer_size = 1000000
6737 max_command_buffer_size = 1000000
6738 "#;
6739 let file_config: FileConfig =
6740 toml::from_str(toml_content).expect("Could not parse TOML config");
6741 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
6742 assert!(
6743 result.is_ok(),
6744 "command_buffer_size == max_command_buffer_size should be accepted: {result:?}"
6745 );
6746 }
6747
6748 #[test]
6749 fn buffer_size_at_h2_minimum_accepted() {
6750 let toml_content = r#"
6751 command_socket = "/tmp/sozu_test.sock"
6752 worker_count = 1
6753 buffer_size = 16393
6754
6755 [[listeners]]
6756 protocol = "https"
6757 address = "127.0.0.1:8443"
6758 "#;
6759 let file_config: FileConfig =
6760 toml::from_str(toml_content).expect("Could not parse TOML config");
6761 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
6762 assert!(
6763 result.is_ok(),
6764 "buffer_size at the H2 minimum should be accepted: {result:?}"
6765 );
6766 }
6767
6768 #[test]
6769 fn alpn_protocols_default() {
6770 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
6771 let config = builder.to_tls(None).expect("to_tls should succeed");
6772 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
6773 }
6774
6775 #[test]
6776 fn alpn_protocols_custom() {
6777 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
6778 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned()]));
6779 let config = builder.to_tls(None).expect("to_tls should succeed");
6780 assert_eq!(config.alpn_protocols, vec!["http/1.1"]);
6781 }
6782
6783 #[test]
6784 fn alpn_protocols_invalid_rejected() {
6785 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
6786 builder.with_alpn_protocols(Some(vec!["h3".to_owned()]));
6787 let result = builder.to_tls(None);
6788 assert!(result.is_err());
6789 let err = result.unwrap_err();
6790 assert!(
6791 err.to_string().contains("h3"),
6792 "error should mention the invalid protocol: {err}"
6793 );
6794 }
6795
6796 #[test]
6797 fn alpn_protocols_empty_uses_default() {
6798 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
6799 builder.with_alpn_protocols(Some(vec![]));
6800 let config = builder.to_tls(None).expect("to_tls should succeed");
6801 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
6802 }
6803
6804 #[test]
6805 fn alpn_protocols_deduplicated() {
6806 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
6807 builder.with_alpn_protocols(Some(vec![
6808 "h2".to_owned(),
6809 "h2".to_owned(),
6810 "http/1.1".to_owned(),
6811 ]));
6812 let config = builder.to_tls(None).expect("to_tls should succeed");
6813 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
6814 }
6815
6816 #[test]
6817 fn alpn_protocols_order_preserved() {
6818 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
6819 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned(), "h2".to_owned()]));
6820 let config = builder.to_tls(None).expect("to_tls should succeed");
6821 assert_eq!(config.alpn_protocols, vec!["http/1.1", "h2"]);
6822 }
6823
6824 #[test]
6830 fn parse_header_edit_rejects_crlf_in_value() {
6831 let entry = HeaderEditConfig {
6832 position: "request".to_owned(),
6833 key: "X-Test".to_owned(),
6834 value: "value\r\nEvil-Header: stolen".to_owned(),
6835 };
6836 let err = parse_header_edit(0, &entry).expect_err("CRLF in value must be rejected");
6837 match err {
6838 ConfigError::InvalidHeaderBytes { index, field } => {
6839 assert_eq!(index, 0);
6840 assert_eq!(field, "value");
6841 }
6842 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
6843 }
6844 }
6845
6846 #[test]
6847 fn parse_header_edit_rejects_lf_in_key() {
6848 let entry = HeaderEditConfig {
6849 position: "response".to_owned(),
6850 key: "X-\nTest".to_owned(),
6851 value: "ok".to_owned(),
6852 };
6853 let err = parse_header_edit(2, &entry).expect_err("LF in key must be rejected");
6854 match err {
6855 ConfigError::InvalidHeaderBytes { index, field } => {
6856 assert_eq!(index, 2);
6857 assert_eq!(field, "key");
6858 }
6859 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
6860 }
6861 }
6862
6863 #[test]
6864 fn parse_header_edit_rejects_nul() {
6865 let entry = HeaderEditConfig {
6866 position: "both".to_owned(),
6867 key: "X-Test".to_owned(),
6868 value: "with\0nul".to_owned(),
6869 };
6870 assert!(matches!(
6871 parse_header_edit(0, &entry),
6872 Err(ConfigError::InvalidHeaderBytes { .. })
6873 ));
6874 }
6875
6876 #[test]
6882 fn parse_header_edit_accepts_tab_in_value() {
6883 let entry = HeaderEditConfig {
6884 position: "request".to_owned(),
6885 key: "X-Test".to_owned(),
6886 value: "with\ttab".to_owned(),
6887 };
6888 let header = parse_header_edit(0, &entry).expect("tab in value must be accepted");
6889 assert_eq!(header.val, "with\ttab");
6890 }
6891
6892 #[test]
6899 fn parse_header_edit_rejects_tab_in_key() {
6900 let entry = HeaderEditConfig {
6901 position: "request".to_owned(),
6902 key: "Host\t".to_owned(),
6903 value: "ok".to_owned(),
6904 };
6905 let err = parse_header_edit(0, &entry).expect_err("HTAB in key must be rejected");
6906 match err {
6907 ConfigError::InvalidHeaderBytes { field, .. } => assert_eq!(field, "key"),
6908 other => panic!("expected InvalidHeaderBytes{{field=\"key\"}}, got {other:?}"),
6909 }
6910 }
6911
6912 #[test]
6913 fn parse_header_edit_rejects_space_in_key() {
6914 let entry = HeaderEditConfig {
6915 position: "request".to_owned(),
6916 key: "X Test".to_owned(),
6917 value: "ok".to_owned(),
6918 };
6919 let err = parse_header_edit(0, &entry).expect_err("SP in key must be rejected");
6920 assert!(matches!(err, ConfigError::InvalidHeaderBytes { .. }));
6921 }
6922
6923 #[test]
6924 fn parse_header_edit_rejects_empty_key() {
6925 let entry = HeaderEditConfig {
6926 position: "request".to_owned(),
6927 key: String::new(),
6928 value: "ok".to_owned(),
6929 };
6930 let err = parse_header_edit(0, &entry).expect_err("empty key must be rejected");
6931 assert!(matches!(
6932 err,
6933 ConfigError::InvalidHeaderBytes { field: "key", .. }
6934 ));
6935 }
6936
6937 #[test]
6938 fn parse_header_edit_accepts_clean_value() {
6939 let entry = HeaderEditConfig {
6940 position: "request".to_owned(),
6941 key: "X-Tenant".to_owned(),
6942 value: "alpha".to_owned(),
6943 };
6944 let header = parse_header_edit(0, &entry).expect("clean value must be accepted");
6945 assert_eq!(header.key, "X-Tenant");
6946 assert_eq!(header.val, "alpha");
6947 }
6948
6949 #[test]
6953 fn resolve_answer_source_bare_string_is_literal() {
6954 let body = resolve_answer_source("HTTP/1.1 503 Service Unavailable\r\n\r\nbusy")
6955 .expect("bare-string source must resolve");
6956 assert_eq!(body, "HTTP/1.1 503 Service Unavailable\r\n\r\nbusy");
6957 }
6958
6959 #[test]
6960 fn resolve_answer_source_empty_string_is_legitimate() {
6961 let body = resolve_answer_source("").expect("empty source must resolve");
6962 assert_eq!(body, "");
6963 }
6964
6965 #[test]
6970 fn resolve_answer_source_file_scheme_missing_file_errors() {
6971 let err = resolve_answer_source("file:///nonexistent/sozu-test/never.http")
6972 .expect_err("missing path must error");
6973 assert!(matches!(err, ConfigError::FileOpen { .. }));
6974 }
6975
6976 #[test]
6979 fn resolve_answer_source_file_scheme_empty_path_errors() {
6980 let err = resolve_answer_source("file://").expect_err("empty path must error");
6981 assert!(matches!(err, ConfigError::FileOpen { .. }));
6982 }
6983
6984 #[test]
6992 fn legacy_tcp_frontend_toml_without_sni_alpn_parses_unchanged() {
6993 let toml_content = r#"
6994 command_socket = "/tmp/sozu_test.sock"
6995 worker_count = 1
6996
6997 [[listeners]]
6998 protocol = "tcp"
6999 address = "127.0.0.1:9000"
7000
7001 [clusters.legacy]
7002 protocol = "tcp"
7003 load_balancing = "ROUND_ROBIN"
7004 frontends = [
7005 { address = "127.0.0.1:9000" }
7006 ]
7007 backends = [
7008 { address = "10.0.0.1:9000" }
7009 ]
7010 "#;
7011 let file_config: FileConfig =
7012 toml::from_str(toml_content).expect("Could not parse legacy TCP TOML");
7013 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
7014 .into_config()
7015 .expect("legacy TCP config without sni/alpn must load unchanged");
7016
7017 assert_eq!(config.tcp_listeners.len(), 1);
7018 let listener = &config.tcp_listeners[0];
7019 assert_eq!(
7020 listener.sni_preread_timeout,
7021 Some(DEFAULT_SNI_PREREAD_TIMEOUT),
7022 "proto default sni_preread_timeout must be populated even though unused"
7023 );
7024 assert_eq!(
7025 listener.sni_preread_max_bytes,
7026 Some(DEFAULT_SNI_PREREAD_MAX_BYTES),
7027 "proto default sni_preread_max_bytes must be populated even though unused"
7028 );
7029
7030 let messages = config
7031 .generate_config_messages()
7032 .expect("Could not generate config messages");
7033 let tcp_frontend = messages
7034 .iter()
7035 .find_map(|m| match &m.content.request_type {
7036 Some(RequestType::AddTcpFrontend(f)) => Some(f),
7037 _ => None,
7038 })
7039 .expect("AddTcpFrontend must be present");
7040 assert_eq!(tcp_frontend.sni, None, "legacy frontend must carry no sni");
7041 assert!(
7042 tcp_frontend.alpn.is_empty(),
7043 "legacy frontend must carry no alpn"
7044 );
7045 }
7046
7047 #[test]
7058 fn a_catch_all_post_frontend_loads_from_toml_and_rejects_a_mixed_case_position() {
7059 let catch_all = |position: &str| {
7060 format!(
7061 r#"
7062 command_socket = "/tmp/sozu_test.sock"
7063 worker_count = 1
7064
7065 [[listeners]]
7066 protocol = "http"
7067 address = "127.0.0.1:8080"
7068
7069 [clusters.fallback]
7070 protocol = "http"
7071 frontends = [
7072 {{ address = "127.0.0.1:8080", hostname = "*", position = "{position}" }}
7073 ]
7074 backends = [
7075 {{ address = "10.0.0.1:8000" }}
7076 ]
7077 "#
7078 )
7079 };
7080
7081 let file_config: FileConfig = toml::from_str(&catch_all("POST"))
7082 .expect("an upper-case `position = \"POST\"` must parse");
7083 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
7084 .into_config()
7085 .expect("a catch-all POST frontend must load");
7086 let messages = config
7087 .generate_config_messages()
7088 .expect("Could not generate config messages");
7089 let frontend = messages
7090 .iter()
7091 .find_map(|m| match &m.content.request_type {
7092 Some(RequestType::AddHttpFrontend(f)) => Some(f),
7093 _ => None,
7094 })
7095 .expect("AddHttpFrontend must be present");
7096 assert_eq!(frontend.hostname, "*");
7097 assert_eq!(frontend.position, RulePosition::Post as i32);
7098
7099 let error = toml::from_str::<FileConfig>(&catch_all("Post"))
7100 .expect_err("a mixed-case `position = \"Post\"` must be refused")
7101 .to_string();
7102 assert!(
7103 error.contains("unknown variant `Post`, expected one of `PRE`, `POST`, `TREE`"),
7104 "unexpected error: {error}",
7105 );
7106 }
7107
7108 #[test]
7114 fn forwarded_headers_parses_every_mode_defaults_to_both_and_rejects_others() {
7115 let listeners = |value: Option<&str>| {
7116 let line = value
7117 .map(|value| format!("forwarded_headers = \"{value}\""))
7118 .unwrap_or_default();
7119 format!(
7120 r#"
7121 command_socket = "/tmp/sozu_test.sock"
7122 worker_count = 1
7123
7124 [[listeners]]
7125 protocol = "http"
7126 address = "127.0.0.1:8080"
7127 {line}
7128
7129 [[listeners]]
7130 protocol = "https"
7131 address = "127.0.0.1:8443"
7132 {line}
7133 "#
7134 )
7135 };
7136 let load = |value: Option<&str>| {
7137 let file_config: FileConfig =
7138 toml::from_str(&listeners(value)).expect("the listeners must parse");
7139 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
7140 .into_config()
7141 .expect("the listeners must load");
7142 (
7143 config.http_listeners[0].forwarded_headers,
7144 config.https_listeners[0].forwarded_headers,
7145 )
7146 };
7147
7148 for (spelling, expected) in [
7149 (None, ForwardedHeaders::Both),
7150 (Some("both"), ForwardedHeaders::Both),
7151 (Some("x_forwarded"), ForwardedHeaders::XForwarded),
7152 (Some("rfc7239"), ForwardedHeaders::Rfc7239),
7153 (Some("none"), ForwardedHeaders::None),
7154 ] {
7155 let expected = Some(expected as i32);
7156 assert_eq!(load(spelling), (expected, expected), "{spelling:?}");
7157 }
7158
7159 for invalid in ["RFC7239", "x-forwarded", "forwarded", ""] {
7160 let error = toml::from_str::<FileConfig>(&listeners(Some(invalid)))
7161 .expect_err("an unknown forwarded_headers value must be refused")
7162 .to_string();
7163 assert!(
7164 error.contains(&format!(
7165 "unknown variant `{invalid}`, expected one of `both`, `x_forwarded`, `rfc7239`, `none`"
7166 )),
7167 "{invalid:?}: unexpected error: {error}",
7168 );
7169 }
7170 }
7171
7172 #[test]
7175 fn tcp_frontend_hostname_maps_to_sni_exact_and_wildcard() {
7176 let toml_content = r#"
7177 command_socket = "/tmp/sozu_test.sock"
7178 worker_count = 1
7179
7180 [[listeners]]
7181 protocol = "tcp"
7182 address = "127.0.0.1:9010"
7183
7184 [clusters.exact]
7185 protocol = "tcp"
7186 load_balancing = "ROUND_ROBIN"
7187 frontends = [
7188 { address = "127.0.0.1:9010", hostname = "example.com", alpn = ["h2"] }
7189 ]
7190 backends = [ { address = "10.0.0.1:9010" } ]
7191
7192 [clusters.wildcard]
7193 protocol = "tcp"
7194 load_balancing = "ROUND_ROBIN"
7195 frontends = [
7196 { address = "127.0.0.1:9010", hostname = "*.example.com" }
7197 ]
7198 backends = [ { address = "10.0.0.2:9010" } ]
7199 "#;
7200 let file_config: FileConfig =
7201 toml::from_str(toml_content).expect("Could not parse TOML config");
7202 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
7203 .into_config()
7204 .expect("exact + wildcard SNI frontends on distinct sni must load");
7205
7206 let messages = config
7207 .generate_config_messages()
7208 .expect("Could not generate config messages");
7209 let mut frontends: Vec<_> = messages
7210 .iter()
7211 .filter_map(|m| match &m.content.request_type {
7212 Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
7213 _ => None,
7214 })
7215 .collect();
7216 frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
7217
7218 assert_eq!(frontends.len(), 2);
7219 assert_eq!(frontends[0].sni, Some("example.com".to_string()));
7220 assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
7221 assert_eq!(frontends[1].sni, Some("*.example.com".to_string()));
7222 assert!(frontends[1].alpn.is_empty());
7223 }
7224
7225 #[test]
7230 fn tcp_frontend_invalid_sni_pattern_rejected() {
7231 for invalid in [
7232 "*.*.example.com",
7233 "foo.*.com",
7234 "*",
7235 "example..com",
7236 "",
7237 "/[a-z]+/.example.com",
7238 "foo/bar.example.com",
7239 ] {
7240 let frontend = FileClusterFrontendConfig {
7241 address: "127.0.0.1:8080".parse().unwrap(),
7242 hostname: Some(invalid.to_string()),
7243 alpn: vec![],
7244 path: None,
7245 path_type: None,
7246 method: None,
7247 certificate: None,
7248 key: None,
7249 certificate_chain: None,
7250 tls_versions: vec![],
7251 position: RulePosition::Tree,
7252 tags: None,
7253 redirect: None,
7254 redirect_scheme: None,
7255 redirect_template: None,
7256 rewrite_host: None,
7257 rewrite_path: None,
7258 rewrite_port: None,
7259 required_auth: None,
7260 headers: None,
7261 hsts: None,
7262 };
7263 match frontend.to_tcp_front() {
7264 Err(ConfigError::InvalidSniPattern { sni }) => assert_eq!(sni, invalid),
7265 other => panic!("expected InvalidSniPattern for {invalid:?}, got {other:?}"),
7266 }
7267 }
7268 }
7269
7270 #[test]
7276 fn tcp_frontend_non_ascii_sni_pattern_rejected() {
7277 for non_ascii in ["münchen.example", "*.bücher.example", "日本.example"] {
7278 let frontend = FileClusterFrontendConfig {
7279 address: "127.0.0.1:8080".parse().unwrap(),
7280 hostname: Some(non_ascii.to_string()),
7281 alpn: vec![],
7282 path: None,
7283 path_type: None,
7284 method: None,
7285 certificate: None,
7286 key: None,
7287 certificate_chain: None,
7288 tls_versions: vec![],
7289 position: RulePosition::Tree,
7290 tags: None,
7291 redirect: None,
7292 redirect_scheme: None,
7293 redirect_template: None,
7294 rewrite_host: None,
7295 rewrite_path: None,
7296 rewrite_port: None,
7297 required_auth: None,
7298 headers: None,
7299 hsts: None,
7300 };
7301 match frontend.to_tcp_front() {
7302 Err(ConfigError::NonAsciiSniPattern { sni }) => assert_eq!(sni, non_ascii),
7303 other => panic!("expected NonAsciiSniPattern for {non_ascii:?}, got {other:?}"),
7304 }
7305 }
7306 }
7307
7308 #[test]
7313 fn tcp_frontend_punycode_sni_pattern_accepted() {
7314 assert_eq!(
7315 validate_sni_pattern("xn--mnchen-3ya.example").expect("A-label must be accepted"),
7316 "xn--mnchen-3ya.example"
7317 );
7318 assert_eq!(
7319 validate_sni_pattern("*.xn--bcher-kva.example")
7320 .expect("wildcarded A-label must be accepted"),
7321 "*.xn--bcher-kva.example"
7322 );
7323 assert_eq!(
7324 validate_sni_pattern("XN--MNCHEN-3YA.Example")
7325 .expect("mixed-case A-label must be accepted"),
7326 "xn--mnchen-3ya.example",
7327 "A-label patterns are ASCII-lowercased like any other pattern"
7328 );
7329 }
7330
7331 #[test]
7334 fn alpn_rejected_on_http_frontend() {
7335 let frontend = FileClusterFrontendConfig {
7336 address: "127.0.0.1:8080".parse().unwrap(),
7337 hostname: Some("example.com".to_owned()),
7338 alpn: vec!["h2".to_string()],
7339 path: None,
7340 path_type: None,
7341 method: None,
7342 certificate: None,
7343 key: None,
7344 certificate_chain: None,
7345 tls_versions: vec![],
7346 position: RulePosition::Tree,
7347 tags: None,
7348 redirect: None,
7349 redirect_scheme: None,
7350 redirect_template: None,
7351 rewrite_host: None,
7352 rewrite_path: None,
7353 rewrite_port: None,
7354 required_auth: None,
7355 headers: None,
7356 hsts: None,
7357 };
7358 match frontend.to_http_front("api") {
7359 Err(ConfigError::InvalidFrontendConfig(field)) => assert_eq!(field, "alpn"),
7360 other => panic!("expected InvalidFrontendConfig(\"alpn\"), got {other:?}"),
7361 }
7362 }
7363
7364 #[test]
7370 fn tcp_frontend_alpn_without_sni_rejected() {
7371 let toml_content = r#"
7372 command_socket = "/tmp/sozu_test.sock"
7373 worker_count = 1
7374
7375 [[listeners]]
7376 protocol = "tcp"
7377 address = "127.0.0.1:9019"
7378
7379 [clusters.a]
7380 protocol = "tcp"
7381 load_balancing = "ROUND_ROBIN"
7382 frontends = [
7383 { address = "127.0.0.1:9019", alpn = ["h2"] }
7384 ]
7385 backends = [ { address = "10.0.0.1:9019" } ]
7386 "#;
7387 let file_config: FileConfig =
7388 toml::from_str(toml_content).expect("Could not parse TOML config");
7389 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
7390 match result {
7391 Err(ConfigError::AlpnWithoutSni { address }) => {
7392 assert_eq!(address.to_string(), "127.0.0.1:9019");
7393 }
7394 other => panic!("expected AlpnWithoutSni, got {other:?}"),
7395 }
7396 }
7397
7398 #[test]
7402 fn tcp_frontend_alpn_overlap_rejected() {
7403 let toml_content = r#"
7404 command_socket = "/tmp/sozu_test.sock"
7405 worker_count = 1
7406
7407 [[listeners]]
7408 protocol = "tcp"
7409 address = "127.0.0.1:9020"
7410
7411 [clusters.a]
7412 protocol = "tcp"
7413 load_balancing = "ROUND_ROBIN"
7414 frontends = [
7415 { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2"] }
7416 ]
7417 backends = [ { address = "10.0.0.1:9020" } ]
7418
7419 [clusters.b]
7420 protocol = "tcp"
7421 load_balancing = "ROUND_ROBIN"
7422 frontends = [
7423 { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2", "http/1.1"] }
7424 ]
7425 backends = [ { address = "10.0.0.2:9020" } ]
7426 "#;
7427 let file_config: FileConfig =
7428 toml::from_str(toml_content).expect("Could not parse TOML config");
7429 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
7430 match result {
7431 Err(ConfigError::TcpFrontendAlpnOverlap { protocol, .. }) => {
7432 assert_eq!(protocol, "h2");
7433 }
7434 other => panic!("expected TcpFrontendAlpnOverlap, got {other:?}"),
7435 }
7436 }
7437
7438 #[test]
7442 fn tcp_frontend_multiple_alpn_catch_all_rejected() {
7443 let toml_content = r#"
7444 command_socket = "/tmp/sozu_test.sock"
7445 worker_count = 1
7446
7447 [[listeners]]
7448 protocol = "tcp"
7449 address = "127.0.0.1:9021"
7450
7451 [clusters.a]
7452 protocol = "tcp"
7453 load_balancing = "ROUND_ROBIN"
7454 frontends = [
7455 { address = "127.0.0.1:9021", hostname = "example.com" }
7456 ]
7457 backends = [ { address = "10.0.0.1:9021" } ]
7458
7459 [clusters.b]
7460 protocol = "tcp"
7461 load_balancing = "ROUND_ROBIN"
7462 frontends = [
7463 { address = "127.0.0.1:9021", hostname = "example.com" }
7464 ]
7465 backends = [ { address = "10.0.0.2:9021" } ]
7466 "#;
7467 let file_config: FileConfig =
7468 toml::from_str(toml_content).expect("Could not parse TOML config");
7469 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
7470 assert!(
7471 matches!(
7472 result,
7473 Err(ConfigError::TcpFrontendMultipleAlpnCatchAll { .. })
7474 ),
7475 "expected TcpFrontendMultipleAlpnCatchAll, got {result:?}"
7476 );
7477 }
7478
7479 #[test]
7483 fn tcp_listener_mixes_sni_and_no_sni_rejected() {
7484 let toml_content = r#"
7485 command_socket = "/tmp/sozu_test.sock"
7486 worker_count = 1
7487
7488 [[listeners]]
7489 protocol = "tcp"
7490 address = "127.0.0.1:9022"
7491
7492 [clusters.a]
7493 protocol = "tcp"
7494 load_balancing = "ROUND_ROBIN"
7495 frontends = [
7496 { address = "127.0.0.1:9022", hostname = "example.com" }
7497 ]
7498 backends = [ { address = "10.0.0.1:9022" } ]
7499
7500 [clusters.b]
7501 protocol = "tcp"
7502 load_balancing = "ROUND_ROBIN"
7503 frontends = [
7504 { address = "127.0.0.1:9022" }
7505 ]
7506 backends = [ { address = "10.0.0.2:9022" } ]
7507 "#;
7508 let file_config: FileConfig =
7509 toml::from_str(toml_content).expect("Could not parse TOML config");
7510 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
7511 assert!(
7512 matches!(result, Err(ConfigError::TcpListenerMixesSniAndNoSni { .. })),
7513 "expected TcpListenerMixesSniAndNoSni, got {result:?}"
7514 );
7515 }
7516
7517 #[test]
7522 fn sni_preread_timeout_exceeding_front_timeout_rejected() {
7523 let toml_content = r#"
7524 command_socket = "/tmp/sozu_test.sock"
7525 worker_count = 1
7526
7527 [[listeners]]
7528 protocol = "tcp"
7529 address = "127.0.0.1:9030"
7530 front_timeout = 2
7531
7532 [clusters.a]
7533 protocol = "tcp"
7534 load_balancing = "ROUND_ROBIN"
7535 frontends = [
7536 { address = "127.0.0.1:9030", hostname = "example.com" }
7537 ]
7538 backends = [ { address = "10.0.0.1:9030" } ]
7539 "#;
7540 let file_config: FileConfig =
7541 toml::from_str(toml_content).expect("Could not parse TOML config");
7542 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
7543 match result {
7544 Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
7545 sni_preread_timeout: 5,
7546 front_timeout: 2,
7547 ..
7548 }) => {}
7549 other => panic!("expected SniPrereadTimeoutExceedsFrontTimeout, got {other:?}"),
7550 }
7551 }
7552
7553 #[test]
7557 fn sni_preread_max_bytes_exceeding_buffer_size_rejected() {
7558 let toml_content = r#"
7559 command_socket = "/tmp/sozu_test.sock"
7560 worker_count = 1
7561 buffer_size = 8192
7562
7563 [[listeners]]
7564 protocol = "tcp"
7565 address = "127.0.0.1:9031"
7566
7567 [clusters.a]
7568 protocol = "tcp"
7569 load_balancing = "ROUND_ROBIN"
7570 frontends = [
7571 { address = "127.0.0.1:9031", hostname = "example.com" }
7572 ]
7573 backends = [ { address = "10.0.0.1:9031" } ]
7574 "#;
7575 let file_config: FileConfig =
7576 toml::from_str(toml_content).expect("Could not parse TOML config");
7577 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
7578 match result {
7579 Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
7580 sni_preread_max_bytes: 16384,
7581 buffer_size: 8192,
7582 ..
7583 }) => {}
7584 other => panic!("expected SniPrereadMaxBytesExceedsBufferSize, got {other:?}"),
7585 }
7586 }
7587
7588 #[test]
7593 fn sni_preread_max_bytes_zero_rejected() {
7594 let toml_content = r#"
7595 command_socket = "/tmp/sozu_test.sock"
7596 worker_count = 1
7597
7598 [[listeners]]
7599 protocol = "tcp"
7600 address = "127.0.0.1:9033"
7601 sni_preread_max_bytes = 0
7602
7603 [clusters.a]
7604 protocol = "tcp"
7605 load_balancing = "ROUND_ROBIN"
7606 frontends = [
7607 { address = "127.0.0.1:9033", hostname = "example.com" }
7608 ]
7609 backends = [ { address = "10.0.0.1:9033" } ]
7610 "#;
7611 let file_config: FileConfig =
7612 toml::from_str(toml_content).expect("Could not parse TOML config");
7613 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
7614 match result {
7615 Err(ConfigError::SniPrereadMaxBytesTooSmall {
7616 sni_preread_max_bytes: 0,
7617 minimum: 5,
7618 ..
7619 }) => {}
7620 other => panic!("expected SniPrereadMaxBytesTooSmall, got {other:?}"),
7621 }
7622 }
7623
7624 #[test]
7627 fn sni_preread_max_bytes_at_the_floor_loads() {
7628 let toml_content = r#"
7629 command_socket = "/tmp/sozu_test.sock"
7630 worker_count = 1
7631
7632 [[listeners]]
7633 protocol = "tcp"
7634 address = "127.0.0.1:9034"
7635 sni_preread_max_bytes = 5
7636
7637 [clusters.a]
7638 protocol = "tcp"
7639 load_balancing = "ROUND_ROBIN"
7640 frontends = [
7641 { address = "127.0.0.1:9034", hostname = "example.com" }
7642 ]
7643 backends = [ { address = "10.0.0.1:9034" } ]
7644 "#;
7645 let file_config: FileConfig =
7646 toml::from_str(toml_content).expect("Could not parse TOML config");
7647 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
7648 assert!(
7649 result.is_ok(),
7650 "sni_preread_max_bytes at the exact floor must load: {result:?}"
7651 );
7652 }
7653
7654 #[test]
7658 fn sni_preread_validation_ignored_without_sni_frontend() {
7659 let toml_content = r#"
7660 command_socket = "/tmp/sozu_test.sock"
7661 worker_count = 1
7662 buffer_size = 8192
7663
7664 [[listeners]]
7665 protocol = "tcp"
7666 address = "127.0.0.1:9032"
7667 front_timeout = 2
7668
7669 [clusters.a]
7670 protocol = "tcp"
7671 load_balancing = "ROUND_ROBIN"
7672 frontends = [
7673 { address = "127.0.0.1:9032" }
7674 ]
7675 backends = [ { address = "10.0.0.1:9032" } ]
7676 "#;
7677 let file_config: FileConfig =
7678 toml::from_str(toml_content).expect("Could not parse TOML config");
7679 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
7680 assert!(
7681 result.is_ok(),
7682 "a no-SNI TCP listener must ignore sni_preread validation entirely: {result:?}"
7683 );
7684 }
7685
7686 #[test]
7693 fn tcp_frontend_disjoint_alpn_same_sni_both_load() {
7694 let toml_content = r#"
7695 command_socket = "/tmp/sozu_test.sock"
7696 worker_count = 1
7697
7698 [[listeners]]
7699 protocol = "tcp"
7700 address = "127.0.0.1:9040"
7701
7702 [clusters.h2_cluster]
7703 protocol = "tcp"
7704 load_balancing = "ROUND_ROBIN"
7705 frontends = [
7706 { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["h2"] }
7707 ]
7708 backends = [ { address = "10.0.0.1:9040" } ]
7709
7710 [clusters.http11_cluster]
7711 protocol = "tcp"
7712 load_balancing = "ROUND_ROBIN"
7713 frontends = [
7714 { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["http/1.1"] }
7715 ]
7716 backends = [ { address = "10.0.0.2:9040" } ]
7717 "#;
7718 let file_config: FileConfig =
7719 toml::from_str(toml_content).expect("Could not parse TOML config");
7720 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
7721 .into_config()
7722 .expect("disjoint non-empty ALPN lists on the same (address, sni) must load");
7723
7724 let messages = config
7725 .generate_config_messages()
7726 .expect("Could not generate config messages");
7727 let mut frontends: Vec<_> = messages
7728 .iter()
7729 .filter_map(|m| match &m.content.request_type {
7730 Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
7731 _ => None,
7732 })
7733 .collect();
7734 frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
7735
7736 assert_eq!(frontends.len(), 2, "both frontends must be emitted");
7737 assert_eq!(frontends[0].cluster_id, "h2_cluster");
7738 assert_eq!(frontends[0].sni, Some("example.com".to_string()));
7739 assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
7740 assert_eq!(frontends[1].cluster_id, "http11_cluster");
7741 assert_eq!(frontends[1].sni, Some("example.com".to_string()));
7742 assert_eq!(frontends[1].alpn, vec!["http/1.1".to_string()]);
7743 }
7744
7745 #[test]
7761 fn udp_routed_tcp_frontends_are_excluded_from_sni_invariants() {
7762 let toml_content = r#"
7763 command_socket = "/tmp/sozu_test.sock"
7764 worker_count = 1
7765
7766 [[listeners]]
7767 protocol = "udp"
7768 address = "127.0.0.1:9050"
7769
7770 [clusters.a]
7771 protocol = "tcp"
7772 load_balancing = "ROUND_ROBIN"
7773 frontends = [
7774 { address = "127.0.0.1:9050", hostname = "example.com" }
7775 ]
7776 backends = [ { address = "10.0.0.1:9050" } ]
7777
7778 [clusters.b]
7779 protocol = "tcp"
7780 load_balancing = "ROUND_ROBIN"
7781 frontends = [
7782 { address = "127.0.0.1:9050" }
7783 ]
7784 backends = [ { address = "10.0.0.2:9050" } ]
7785 "#;
7786 let file_config: FileConfig =
7787 toml::from_str(toml_content).expect("Could not parse TOML config");
7788 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
7789 assert!(
7790 result.is_ok(),
7791 "UDP-routed tcp-cluster frontends must not trip the SNI mixing-ban: {result:?}"
7792 );
7793
7794 let config = result.expect("checked is_ok above");
7795 let messages = config
7796 .generate_config_messages()
7797 .expect("Could not generate config messages");
7798 let add_udp_frontend_count = messages
7799 .iter()
7800 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
7801 .count();
7802 assert_eq!(
7803 add_udp_frontend_count, 2,
7804 "both cluster frontends on the udp listener must emit AddUdpFrontend"
7805 );
7806 }
7807
7808 #[test]
7813 fn subnet_connection_limit_keys_round_trip_from_toml() {
7814 let toml_content = r#"
7815 command_socket = "/tmp/sozu.sock"
7816 saved_state = "./state.json"
7817 worker_count = 1
7818
7819 max_connections_per_ip = 10
7820 max_connections_per_subnet = 100
7821 subnet_ipv4_prefix = 24
7822 subnet_ipv6_prefix = 56
7823
7824 [[listeners]]
7825 protocol = "http"
7826 address = "127.0.0.1:8080"
7827
7828 [clusters.api]
7829 protocol = "http"
7830 load_balancing = "ROUND_ROBIN"
7831 max_connections_per_subnet = 7
7832 frontends = [
7833 { address = "127.0.0.1:8080", hostname = "example.com" }
7834 ]
7835 backends = [ { address = "10.0.0.1:8080" } ]
7836 "#;
7837 let file_config: FileConfig =
7838 toml::from_str(toml_content).expect("Could not parse TOML config");
7839 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
7840 .into_config()
7841 .expect("a config with valid subnet prefixes must load");
7842
7843 assert_eq!(
7844 config.max_connections_per_subnet, 100,
7845 "the global subnet cap must survive the TOML round trip"
7846 );
7847 assert_eq!(
7848 config.subnet_ipv4_prefix, 24,
7849 "subnet_ipv4_prefix must survive the TOML round trip"
7850 );
7851 assert_eq!(
7852 config.subnet_ipv6_prefix, 56,
7853 "subnet_ipv6_prefix must survive the TOML round trip — a swap with the IPv6 \
7854 key would leave a cap that silently never fires"
7855 );
7856 assert_eq!(
7857 config.max_connections_per_ip, 10,
7858 "the pre-existing per-IP cap must be unaffected: the two are independent"
7859 );
7860
7861 let cluster = config
7862 .clusters
7863 .get("api")
7864 .expect("the staged cluster must be present");
7865 let override_value = match cluster {
7866 ClusterConfig::Http(http) => http.max_connections_per_subnet,
7867 other => panic!("expected an HTTP cluster, got {other:?}"),
7868 };
7869 assert_eq!(
7870 override_value,
7871 Some(7),
7872 "the per-cluster subnet override must reach the cluster config"
7873 );
7874 }
7875
7876 #[test]
7880 fn max_connection_attempts_defaults_to_five_and_round_trips_from_toml() {
7881 let config_with = |global: &str, cluster: &str| -> Config {
7882 let toml_content = format!(
7883 r#"
7884 command_socket = "/tmp/sozu.sock"
7885 saved_state = "./state.json"
7886 worker_count = 1
7887 {global}
7888
7889 [[listeners]]
7890 protocol = "http"
7891 address = "127.0.0.1:8080"
7892
7893 [clusters.api]
7894 protocol = "http"
7895 load_balancing = "ROUND_ROBIN"
7896 {cluster}
7897 frontends = [
7898 {{ address = "127.0.0.1:8080", hostname = "example.com" }}
7899 ]
7900 backends = [ {{ address = "10.0.0.1:8080" }} ]
7901 "#
7902 );
7903 let file_config: FileConfig =
7904 toml::from_str(&toml_content).expect("Could not parse TOML config");
7905 ConfigBuilder::new(file_config, "/tmp/test_config.toml")
7906 .into_config()
7907 .expect("a valid attempt budget must load")
7908 };
7909 let cluster_attempts = |config: &Config| match config.clusters.get("api") {
7910 Some(ClusterConfig::Http(http)) => http.max_connection_attempts,
7911 other => panic!("expected the HTTP cluster, got {other:?}"),
7912 };
7913 let add_cluster_attempts = |config: &Config| {
7914 config
7915 .generate_config_messages()
7916 .expect("Could not generate config messages")
7917 .into_iter()
7918 .find_map(|message| match message.content.request_type {
7919 Some(RequestType::AddCluster(cluster)) => Some(cluster.max_connection_attempts),
7920 _ => None,
7921 })
7922 .expect("the cluster is added")
7923 };
7924
7925 let defaults = config_with("", "");
7926 assert_eq!(DEFAULT_MAX_CONNECTION_ATTEMPTS, 5);
7927 assert_eq!(defaults.max_connection_attempts, 5);
7928 assert_eq!(
7929 ServerConfig::from(&defaults).max_connection_attempts,
7930 Some(5),
7931 "the worker receives the default"
7932 );
7933 assert_eq!(
7934 cluster_attempts(&defaults),
7935 None,
7936 "a cluster inherits by default"
7937 );
7938 assert_eq!(add_cluster_attempts(&defaults), None);
7939
7940 let set = config_with("max_connection_attempts = 2", "max_connection_attempts = 7");
7941 assert_eq!(set.max_connection_attempts, 2);
7942 assert_eq!(ServerConfig::from(&set).max_connection_attempts, Some(2));
7943 assert_eq!(cluster_attempts(&set), Some(7));
7944 assert_eq!(add_cluster_attempts(&set), Some(7));
7945 }
7946
7947 #[test]
7951 fn an_out_of_range_max_connection_attempts_is_rejected() {
7952 let load = |global: &str, cluster: &str| {
7953 let toml_content = format!(
7954 r#"
7955 command_socket = "/tmp/sozu.sock"
7956 saved_state = "./state.json"
7957 worker_count = 1
7958 {global}
7959
7960 [[listeners]]
7961 protocol = "http"
7962 address = "127.0.0.1:8080"
7963
7964 [clusters.api]
7965 protocol = "http"
7966 load_balancing = "ROUND_ROBIN"
7967 {cluster}
7968 frontends = [
7969 {{ address = "127.0.0.1:8080", hostname = "example.com" }}
7970 ]
7971 backends = [ {{ address = "10.0.0.1:8080" }} ]
7972 "#
7973 );
7974 let file_config: FileConfig =
7975 toml::from_str(&toml_content).expect("Could not parse TOML config");
7976 ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config()
7977 };
7978 for value in ["0", "256"] {
7979 let global = format!("max_connection_attempts = {value}");
7980 assert!(
7981 matches!(
7982 load(&global, ""),
7983 Err(ConfigError::InvalidMaxConnectionAttempts {
7984 cluster_id: None,
7985 ..
7986 })
7987 ),
7988 "a global budget of {value} must be rejected"
7989 );
7990 assert!(
7991 matches!(
7992 load("", &global),
7993 Err(ConfigError::InvalidMaxConnectionAttempts {
7994 cluster_id: Some(_),
7995 ..
7996 })
7997 ),
7998 "a cluster budget of {value} must be rejected"
7999 );
8000 }
8001 for value in ["1", "255"] {
8002 let budget = format!("max_connection_attempts = {value}");
8003 assert!(load(&budget, &budget).is_ok(), "{value} is in range");
8004 }
8005 }
8006
8007 #[test]
8011 fn an_out_of_range_subnet_prefix_is_rejected_not_clamped() {
8012 let with_prefixes = |v4: &str, v6: &str| -> String {
8013 format!(
8014 r#"
8015 command_socket = "/tmp/sozu.sock"
8016 saved_state = "./state.json"
8017 worker_count = 1
8018
8019 max_connections_per_subnet = 100
8020 subnet_ipv4_prefix = {v4}
8021 subnet_ipv6_prefix = {v6}
8022
8023 [[listeners]]
8024 protocol = "http"
8025 address = "127.0.0.1:8080"
8026 "#
8027 )
8028 };
8029
8030 for (v4, v6, key) in [
8031 ("33", "56", "subnet_ipv4_prefix"),
8032 ("24", "129", "subnet_ipv6_prefix"),
8033 ] {
8034 let file_config: FileConfig =
8035 toml::from_str(&with_prefixes(v4, v6)).expect("Could not parse TOML config");
8036 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8037 match result {
8038 Err(ConfigError::InvalidSubnetPrefix { key: reported, .. }) => {
8039 assert_eq!(
8040 reported, key,
8041 "the rejection must name the key that is out of range"
8042 );
8043 }
8044 other => panic!("{key} out of range must be rejected, got {other:?}"),
8045 }
8046 }
8047
8048 let file_config: FileConfig =
8051 toml::from_str(&with_prefixes("32", "128")).expect("Could not parse TOML config");
8052 assert!(
8053 ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8054 .into_config()
8055 .is_ok(),
8056 "the full-width prefixes are legal and are the defaults — they must load"
8057 );
8058 }
8059
8060 #[test]
8061 fn old_state_file_without_client_auth_fields_deserializes() {
8062 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
8067 let config = ListenerBuilder::new_https(address)
8068 .to_tls(None)
8069 .expect("default HTTPS listener config");
8070
8071 let mut json: serde_json::Value =
8072 serde_json::to_value(&config).expect("serialize HttpsListenerConfig");
8073 let obj = json.as_object_mut().expect("object");
8074 obj.remove("client_ca_certificates");
8075 obj.remove("client_ca_crls");
8076 obj.remove("client_auth");
8077
8078 let restored: HttpsListenerConfig =
8079 serde_json::from_value(json).expect("old state file must deserialize");
8080 assert!(restored.client_ca_certificates.is_empty());
8081 assert!(restored.client_ca_crls.is_empty());
8082 assert_eq!(restored.client_auth, None);
8083 }
8084
8085 #[test]
8086 fn client_auth_fields_rejected_on_non_https_listener() {
8087 let address = SocketAddress::new_v4(127, 0, 0, 1, 9000);
8090 let mut tcp = ListenerBuilder::new_tcp(address);
8091 tcp.client_auth = Some(ClientAuthConfig::Required);
8092 assert!(matches!(
8093 tcp.to_tcp(None),
8094 Err(ConfigError::ClientAuthOnNonHttps(_))
8095 ));
8096
8097 let mut http = ListenerBuilder::new_http(address);
8099 http.client_ca_certificates = Some(vec!["ca.pem".to_owned()]);
8100 assert!(matches!(
8101 http.to_http(None),
8102 Err(ConfigError::ClientAuthOnNonHttps(_))
8103 ));
8104 }
8105
8106 #[test]
8107 fn none_mode_ignores_stale_ca_paths() {
8108 let address = SocketAddress::new_v4(127, 0, 0, 1, 9443);
8111 let mut https = ListenerBuilder::new_https(address);
8112 https.client_auth = Some(ClientAuthConfig::None);
8113 https.client_ca_certificates = Some(vec!["/nonexistent/ca.pem".to_owned()]);
8114 https.client_ca_crls = Some(vec!["/nonexistent/crl.pem".to_owned()]);
8115 let config = https.to_tls(None).expect("NONE mode must ignore CA paths");
8116 assert!(config.client_ca_certificates.is_empty());
8117 assert!(config.client_ca_crls.is_empty());
8118 }
8119
8120 #[test]
8124 fn client_auth_listener_inlines_its_ca_and_crl_files_into_the_listener_request() {
8125 let asset =
8126 |name: &str| format!("{}/../lib/assets/mtls/{name}", env!("CARGO_MANIFEST_DIR"));
8127 let (ca_path, crl_path) = (asset("ca-cert.pem"), asset("crl-revoked.pem"));
8128 let toml_content = format!(
8129 r#"
8130 command_socket = "/tmp/sozu.sock"
8131 saved_state = "./state.json"
8132 worker_count = 1
8133
8134 [[listeners]]
8135 protocol = "https"
8136 address = "127.0.0.1:8443"
8137 client_auth = "required"
8138 client_ca_certificates = ["{ca_path}"]
8139 client_ca_crls = ["{crl_path}"]
8140 "#
8141 );
8142 let file_config: FileConfig =
8143 toml::from_str(&toml_content).expect("Could not parse TOML config");
8144 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8145 .into_config()
8146 .expect("a client auth listener with readable CA and CRL files must load");
8147
8148 let listeners: Vec<HttpsListenerConfig> = config
8149 .generate_config_messages()
8150 .expect("the config must produce its worker requests")
8151 .into_iter()
8152 .filter_map(|request| match request.content.request_type {
8153 Some(RequestType::AddHttpsListenerWithClientAuth(listener)) => Some(listener),
8154 Some(RequestType::AddHttpsListener(_)) => {
8155 panic!("a client auth listener must not go out as AddHttpsListener")
8156 }
8157 _ => None,
8158 })
8159 .collect();
8160 assert_eq!(listeners.len(), 1, "one HTTPS listener is configured");
8161 let listener = &listeners[0];
8162 assert_eq!(
8163 listener.client_auth,
8164 Some(ClientAuthMode::ClientAuthRequired as i32)
8165 );
8166 assert_eq!(
8167 listener.client_ca_certificates,
8168 vec![std::fs::read_to_string(&ca_path).expect("read the CA fixture")],
8169 "the CA file content, not its path, must reach the worker"
8170 );
8171 assert_eq!(
8172 listener.client_ca_crls,
8173 vec![std::fs::read_to_string(&crl_path).expect("read the CRL fixture")],
8174 "the CRL file content, not its path, must reach the worker"
8175 );
8176 assert!(
8177 listener.client_ca_crls[0].contains("BEGIN X509 CRL"),
8178 "the inlined CRL is the PEM body"
8179 );
8180 }
8181
8182 #[test]
8183 fn client_auth_listener_with_an_unreadable_crl_file_does_not_load() {
8184 let address = SocketAddress::new_v4(127, 0, 0, 1, 9443);
8187 let mut https = ListenerBuilder::new_https(address);
8188 https.client_auth = Some(ClientAuthConfig::Required);
8189 https.client_ca_certificates = Some(vec![format!(
8190 "{}/../lib/assets/mtls/ca-cert.pem",
8191 env!("CARGO_MANIFEST_DIR")
8192 )]);
8193 https.client_ca_crls = Some(vec!["/nonexistent/crl.pem".to_owned()]);
8194 assert!(
8195 https.to_tls(None).is_err(),
8196 "an unreadable CRL file must fail the listener, not drop the revocation check"
8197 );
8198 }
8199
8200 #[test]
8201 fn client_auth_files_without_a_mode_are_rejected() {
8202 let address = SocketAddress::new_v4(127, 0, 0, 1, 9443);
8205 for (cas, crls) in [
8206 (Some(vec!["/etc/sozu/client-ca.pem".to_owned()]), None),
8207 (None, Some(vec!["/etc/sozu/client-ca.crl.pem".to_owned()])),
8208 ] {
8209 let mut https = ListenerBuilder::new_https(address);
8210 https.client_ca_certificates = cas;
8211 https.client_ca_crls = crls;
8212 assert!(matches!(
8213 https.to_tls(None),
8214 Err(ConfigError::ClientAuthModeMissing(_))
8215 ));
8216 }
8217 let mut explicit_none = ListenerBuilder::new_https(address);
8220 explicit_none.client_auth = Some(ClientAuthConfig::None);
8221 explicit_none.client_ca_certificates = Some(vec!["/nonexistent/ca.pem".to_owned()]);
8222 assert!(explicit_none.to_tls(None).is_ok());
8223 assert!(ListenerBuilder::new_https(address).to_tls(None).is_ok());
8224 }
8225
8226 #[test]
8227 fn client_auth_config_parses_documented_lowercase_names() {
8228 assert_eq!(
8231 serde_json::from_str::<ClientAuthConfig>("\"required\"").unwrap(),
8232 ClientAuthConfig::Required
8233 );
8234 assert_eq!(
8235 serde_json::from_str::<ClientAuthConfig>("\"optional\"").unwrap(),
8236 ClientAuthConfig::Optional
8237 );
8238 assert_eq!(
8239 serde_json::from_str::<ClientAuthConfig>("\"none\"").unwrap(),
8240 ClientAuthConfig::None
8241 );
8242 assert_eq!(
8243 ClientAuthMode::from(ClientAuthConfig::Required),
8244 ClientAuthMode::ClientAuthRequired
8245 );
8246 }
8247}