1use std::{
51 collections::{BTreeMap, HashMap, HashSet},
52 env, fmt,
53 fs::{File, create_dir_all, metadata},
54 io::{ErrorKind, Read},
55 net::{SocketAddr, ToSocketAddrs},
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, HeaderMatchKind, HeaderPosition,
68 HeaderPredicate, HealthCheckConfig, HealthCheckHeader, HealthCheckHeaderValue,
69 HealthCheckMode, HstsConfig, HttpListenerConfig, HttpStatusRange, HttpsListenerConfig,
70 ListenerType, LoadBalancingAlgorithms, LoadBalancingParams, LoadMetric, MetricDetail,
71 MetricsConfiguration, PathRule, ProtobufAccessLogFormat, ProxyProtocolConfig,
72 RedirectPolicy, RedirectScheme, Request, RequestHttpFrontend, RequestTcpFrontend,
73 RequestUdpFrontend, RulePosition, ServerConfig, ServerMetricsConfig, ShardMode,
74 SocketAddress, TcpListenerConfig, TlsVersion, UdpAffinityKey, UdpClusterConfig,
75 UdpHealthConfig, UdpHealthMode, UdpListenerConfig, WorkerRequest, request::RequestType,
76 },
77};
78
79pub const DEFAULT_CIPHER_LIST: [&str; 9] = [
87 "TLS13_AES_256_GCM_SHA384",
89 "TLS13_AES_128_GCM_SHA256",
90 "TLS13_CHACHA20_POLY1305_SHA256",
91 "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
93 "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
94 "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
95 "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
96 "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
97 "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
98];
99
100pub const DEFAULT_SIGNATURE_ALGORITHMS: [&str; 9] = [
101 "ECDSA+SHA256",
102 "ECDSA+SHA384",
103 "ECDSA+SHA512",
104 "RSA+SHA256",
105 "RSA+SHA384",
106 "RSA+SHA512",
107 "RSA-PSS+SHA256",
108 "RSA-PSS+SHA384",
109 "RSA-PSS+SHA512",
110];
111
112pub const DEFAULT_GROUPS_LIST: [&str; 4] = ["X25519MLKEM768", "x25519", "P-256", "P-384"];
113
114pub const DEFAULT_ALPN_PROTOCOLS: [&str; 2] = ["h2", "http/1.1"];
117
118pub const DEFAULT_FRONT_TIMEOUT: u32 = 60;
120
121pub const DEFAULT_COMMAND_SOCKET_MODE: u32 = 0o600;
124
125pub const MAX_COMMAND_SOCKET_MODE: u32 = 0o777;
128
129pub const DEFAULT_BACK_TIMEOUT: u32 = 30;
131
132pub const DEFAULT_CONNECT_TIMEOUT: u32 = 3;
134
135pub const DEFAULT_SNI_PREREAD_TIMEOUT: u32 = 5;
140
141pub const DEFAULT_SNI_PREREAD_MAX_BYTES: u32 = 16384;
149
150pub const MIN_SNI_PREREAD_MAX_BYTES: u32 = 5;
159
160pub const DEFAULT_REQUEST_TIMEOUT: u32 = 10;
162
163pub const DEFAULT_UDP_FRONT_TIMEOUT: u32 = 30;
165
166pub const DEFAULT_UDP_BACK_TIMEOUT: u32 = 30;
168
169pub const DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE: u32 = 1500;
172
173pub const DEFAULT_UDP_MAX_FLOWS: u32 = 0;
176
177pub const DEFAULT_WORKER_TIMEOUT: u32 = 10;
179
180pub const DEFAULT_STICKY_NAME: &str = "SOZUBALANCEID";
182
183pub const DEFAULT_ZOMBIE_CHECK_INTERVAL: u32 = 1_800;
185
186pub const DEFAULT_ACCEPT_QUEUE_TIMEOUT: u32 = 60;
188
189pub const DEFAULT_HSTS_MAX_AGE: u32 = 31_536_000;
196
197pub const DEFAULT_EVICT_ON_QUEUE_FULL: bool = false;
203
204pub const DEFAULT_WORKER_COUNT: u16 = 2;
206
207pub const DEFAULT_WORKER_AUTOMATIC_RESTART: bool = true;
209
210pub const DEFAULT_AUTOMATIC_STATE_SAVE: bool = false;
212
213pub const DEFAULT_MIN_BUFFERS: u64 = 1;
215
216pub const DEFAULT_MAX_BUFFERS: u64 = 1_000;
218
219pub const DEFAULT_BUFFER_SIZE: u64 = 16_393;
221
222pub const H2_MIN_BUFFER_SIZE: u64 = 16_393;
232
233pub const DEFAULT_MAX_CONNECTIONS: usize = 10_000;
235
236pub const DEFAULT_COMMAND_BUFFER_SIZE: u64 = 1_000_000;
238
239pub const DEFAULT_MAX_COMMAND_BUFFER_SIZE: u64 = 2_000_000;
241
242pub const DEFAULT_DISABLE_CLUSTER_METRICS: bool = false;
244
245pub const MAX_LOOP_ITERATIONS: usize = 100000;
246
247pub const DEFAULT_SEND_TLS_13_TICKETS: u64 = 4;
252
253pub const DEFAULT_LOG_TARGET: &str = "stdout";
255
256pub const DEFAULT_MAX_CONNECTIONS_PER_IP: u64 = 0;
261
262pub const DEFAULT_RETRY_AFTER: u32 = 60;
268
269pub const DEFAULT_MAX_CONNECTIONS_PER_SUBNET: u64 = 0;
278
279pub const DEFAULT_MAX_CONNECTION_ATTEMPTS: u32 = 5;
286
287pub const MAX_CONNECTION_ATTEMPTS_UPPER_BOUND: u32 = u8::MAX as u32;
290
291pub fn validate_max_connection_attempts(attempts: u32) -> Result<(), &'static str> {
295 if (1..=MAX_CONNECTION_ATTEMPTS_UPPER_BOUND).contains(&attempts) {
296 Ok(())
297 } else {
298 Err("max_connection_attempts must lie in 1..=255")
299 }
300}
301
302pub const HEALTH_CHECK_VALUE_FILE_UNSUPPORTED: &str = "health check header value_file references cannot be applied yet: the protected activation of their values is not available";
305
306pub const HEALTH_CHECK_PORT_UNIX_REFUSED: &str = "health_check.port targets a backend IP address and cannot be combined with a unix_path backend; unset the probe port or the unix_path";
310
311pub fn parse_backend_unix_path(
317 unix_path: &str,
318) -> Result<std::os::unix::net::SocketAddr, &'static str> {
319 const TOO_LONG: &str = "unix_path does not fit a sockaddr_un (107 bytes at most)";
320 if unix_path.as_bytes().contains(&0) {
321 return Err("unix_path must not contain a NUL byte");
322 }
323 let address = if let Some(name) = unix_path.strip_prefix('@') {
324 if name.is_empty() {
325 return Err("an abstract unix_path needs a name after '@'");
326 }
327 parse_abstract_unix_name(name.as_bytes())
328 .map_err(|unsupported| unsupported.unwrap_or(TOO_LONG))?
329 } else if unix_path.starts_with('/') {
330 std::os::unix::net::SocketAddr::from_pathname(unix_path).map_err(|_| TOO_LONG)?
331 } else {
332 return Err("unix_path must be an absolute path or an @abstract name");
333 };
334 debug_assert!(
337 !address.is_unnamed(),
338 "a parsed unix_path never yields an unnamed address"
339 );
340 debug_assert_eq!(
341 address.as_pathname().is_some(),
342 !unix_path.starts_with('@'),
343 "only a path without '@' parses to a pathname address"
344 );
345 Ok(address)
346}
347
348#[cfg(any(target_os = "linux", target_os = "android"))]
351fn parse_abstract_unix_name(
352 name: &[u8],
353) -> Result<std::os::unix::net::SocketAddr, Option<&'static str>> {
354 #[cfg(target_os = "android")]
355 use std::os::android::net::SocketAddrExt;
356 #[cfg(target_os = "linux")]
357 use std::os::linux::net::SocketAddrExt;
358 std::os::unix::net::SocketAddr::from_abstract_name(name).map_err(|_| None)
359}
360
361#[cfg(not(any(target_os = "linux", target_os = "android")))]
362fn parse_abstract_unix_name(
363 _name: &[u8],
364) -> Result<std::os::unix::net::SocketAddr, Option<&'static str>> {
365 Err(Some("an @abstract unix_path needs Linux"))
366}
367
368pub const DEFAULT_SUBNET_IPV4_PREFIX: u32 = 32;
373
374pub const DEFAULT_SUBNET_IPV6_PREFIX: u32 = 128;
378
379pub const MAX_SUBNET_IPV4_PREFIX: u32 = 32;
383
384pub const MAX_SUBNET_IPV6_PREFIX: u32 = 128;
388
389#[derive(Debug)]
390pub enum IncompatibilityKind {
391 PublicAddress,
392 ProxyProtocol,
393}
394
395#[derive(Debug)]
396pub enum MissingKind {
397 Field(String),
398 Protocol,
399 SavedState,
400}
401
402#[derive(thiserror::Error, Debug)]
403pub enum ConfigError {
404 #[error("env path not found: {0}")]
405 Env(String),
406 #[error("Could not open file {path_to_open}: {io_error}")]
407 FileOpen {
408 path_to_open: String,
409 io_error: std::io::Error,
410 },
411 #[error("Could not read file {path_to_read}: {io_error}")]
412 FileRead {
413 path_to_read: String,
414 io_error: std::io::Error,
415 },
416 #[error(
417 "the field {kind:?} of {object:?} with id or address {id} is incompatible with the rest of the options"
418 )]
419 Incompatible {
420 kind: IncompatibilityKind,
421 object: ObjectKind,
422 id: String,
423 },
424 #[error("Invalid '{0}' field for a TCP frontend")]
425 InvalidFrontendConfig(String),
426 #[error("invalid path {0:?}")]
427 InvalidPath(PathBuf),
428 #[error("listening address {0:?} is already used in the configuration")]
429 ListenerAddressAlreadyInUse(SocketAddr),
430 #[error("listener {0} is already declared in the configuration")]
431 ListenerAlreadyDeclared(ListenerKey),
432 #[error(
433 "listeners sharing the address {0} on different interfaces must all use the same protocol"
434 )]
435 ListenerProtocolMismatchOnAddress(SocketAddr),
436 #[error("listener {address}: {error}")]
437 ListenerInterface {
438 address: SocketAddr,
439 error: InterfaceError,
440 },
441 #[error("missing {0:?}")]
442 Missing(MissingKind),
443 #[error("could not get parent directory for file {0}")]
444 NoFileParent(String),
445 #[error("Could not get the path of the saved state")]
446 SaveStatePath(String),
447 #[error("Can not determine path to sozu socket: {0}")]
448 SocketPathError(String),
449 #[error("toml decoding error: {0}")]
450 DeserializeToml(String),
451 #[error("Can not set this frontend on a {0:?} listener")]
452 WrongFrontendProtocol(ListenerProtocol),
453 #[error("Can not build a {expected:?} listener from a {found:?} config")]
454 WrongListenerProtocol {
455 expected: ListenerProtocol,
456 found: Option<ListenerProtocol>,
457 },
458 #[error("Invalid ALPN protocol '{0}'. Valid values: \"h2\", \"http/1.1\"")]
459 InvalidAlpnProtocol(String),
460 #[error("cluster {cluster_id}: {reason}")]
463 InvalidAffinityKey {
464 cluster_id: String,
465 reason: &'static str,
466 },
467 #[error("cluster {cluster_id}: {reason}")]
469 InvalidShuffleSharding {
470 cluster_id: String,
471 reason: &'static str,
472 },
473 #[error("max_connection_attempts = {value}{}: {reason}", cluster_id.as_deref().map(|id| format!(" in cluster {id}")).unwrap_or_default())]
476 InvalidMaxConnectionAttempts {
477 cluster_id: Option<String>,
478 value: u32,
479 reason: &'static str,
480 },
481 #[error("cluster {cluster_id}, backend {address}: {reason}")]
485 InvalidBackendUnixPath {
486 cluster_id: String,
487 address: SocketAddr,
488 reason: &'static str,
489 },
490 #[error("cluster {cluster_id}: {reason}")]
493 InvalidHealthCheck {
494 cluster_id: String,
495 reason: &'static str,
496 },
497 #[error(
503 "{key} = {value} is out of range: a {family} prefix length must be between 0 and \
504 {maximum} bits inclusive."
505 )]
506 InvalidSubnetPrefix {
507 key: &'static str,
508 family: &'static str,
509 value: u32,
510 maximum: u32,
511 },
512 #[error("command_socket_mode = {value:?} is invalid: {reason}")]
515 InvalidCommandSocketMode { value: String, reason: &'static str },
516 #[error(
522 "disable_http11 = true is incompatible with alpn_protocols containing \"http/1.1\" \
523 on listener {address}. The proxy would advertise http/1.1 then refuse every \
524 connection that negotiates it. Drop \"http/1.1\" from alpn_protocols or unset \
525 disable_http11."
526 )]
527 DisableHttp11WithHttp11Alpn { address: String },
528 #[error(
535 "buffer_size = {buffer_size} is below the H2 minimum of {minimum} but \
536 {listeners} HTTPS listener(s) advertise H2 ALPN. The H2 mux deadlocks \
537 on full-size frames with smaller buffers. Raise buffer_size to >= {minimum} \
538 or remove \"h2\" from those listeners' alpn_protocols."
539 )]
540 BufferSizeTooSmallForH2 {
541 buffer_size: u64,
542 minimum: u64,
543 listeners: usize,
544 },
545 #[error(
553 "command_buffer_size = {command_buffer_size} exceeds max_command_buffer_size = \
554 {max_command_buffer_size}: lower command_buffer_size to <= {max_command_buffer_size} \
555 or raise max_command_buffer_size to >= {command_buffer_size}"
556 )]
557 CommandBufferSizeExceedsMax {
558 command_buffer_size: u64,
559 max_command_buffer_size: u64,
560 },
561 #[error(
604 "listener {address}: {key} = {value} is below the minimum of {minimum} the runtime can \
605 honour. Below the minimum the value does not relax the limit, it breaks it: the H2 flood \
606 detector compares count > threshold, so 0 is already exceeded the first time the counter \
607 it guards leaves zero. How far that reaches depends on the key — \
608 h2_max_header_list_size and h2_max_header_fields are also the HPACK decode budget, so 0 \
609 refuses every request on this listener with ENHANCE_YOUR_CALM (as a stream reset, or as \
610 a connection GOAWAY once a header block spans CONTINUATION frames), while a per-window \
611 or lifetime frame counter only trips once a client sends the frame it counts, and \
612 h2_max_concurrent_streams / h2_stream_shrink_ratio are instead promoted to their floor \
613 behind a warning. Raise {key} to >= {minimum}, or remove it to keep the default."
614 )]
615 H2ThresholdBelowMinimum {
616 address: String,
617 key: &'static str,
618 value: u64,
619 minimum: u64,
620 },
621 #[error(
625 "invalid redirect policy '{0}'. Valid values: \"forward\", \"permanent\", \"unauthorized\", \"silent_close\""
626 )]
627 InvalidRedirectPolicy(String),
628 #[error(
632 "invalid redirect scheme '{0}'. Valid values: \"use-same\", \"use-http\", \"use-https\""
633 )]
634 InvalidRedirectScheme(String),
635 #[error(
639 "invalid header position '{position}' at headers[{index}]. Valid values: \"request\", \"response\", \"both\""
640 )]
641 InvalidHeaderPosition { index: usize, position: String },
642 #[error(
649 "invalid header bytes in {field} at headers[{index}]: control characters \
650 (NUL / CR / LF / other C0) are forbidden in header keys and values"
651 )]
652 InvalidHeaderBytes { index: usize, field: &'static str },
653 #[error("invalid header predicate at header_predicates[{index}]: {reason}")]
658 InvalidHeaderPredicate { index: usize, reason: &'static str },
659 #[error("invalid HSTS config at {0}: `enabled` is required when an [hsts] block is present")]
665 HstsEnabledRequired(String),
666 #[error(
671 "invalid HSTS config at {0}: HSTS is only valid on HTTPS listeners and frontends \
672 (RFC 6797 §7.2 forbids the header over plaintext HTTP)"
673 )]
674 HstsOnPlainHttp(String),
675 #[error(
680 "invalid client auth config at {0}: client_auth / client_ca_certificates / \
681 client_ca_crls are only valid on HTTPS listeners"
682 )]
683 ClientAuthOnNonHttps(String),
684 #[error(
688 "invalid client auth config at {0}: client_ca_certificates / client_ca_crls are set \
689 but client_auth is not; set client_auth to \"optional\" or \"required\", or to \
690 \"none\" to keep them unused"
691 )]
692 ClientAuthModeMissing(String),
693 #[error(
700 "invalid SNI pattern '{sni}' for a TCP frontend: expected an exact hostname or a \
701 single leading \"*.\" wildcard label (e.g. \"example.com\" or \"*.example.com\"); \
702 '/' and non-leading '*' are rejected"
703 )]
704 InvalidSniPattern { sni: String },
705 #[error(
712 "non-ASCII SNI pattern '{sni}' for a TCP frontend: on-wire SNI is always an ASCII \
713 A-label (RFC 6066), so this pattern would never match a ClientHello. Write the \
714 punycode A-label form instead (e.g. \"xn--mnchen-3ya.example\" for \
715 \"münchen.example\")"
716 )]
717 NonAsciiSniPattern { sni: String },
718 #[error(
727 "TCP frontend {address} sets alpn but no hostname (sni): alpn only matches within an \
728 SNI-scoped preread, so a frontend without hostname would silently ignore its alpn list. \
729 Set hostname or drop alpn."
730 )]
731 AlpnWithoutSni { address: SocketAddr },
732 #[error(
738 "TCP frontends on {address} with sni {sni:?} both match ALPN protocol '{protocol}': \
739 ALPN matchers for the same (address, sni) must not overlap"
740 )]
741 TcpFrontendAlpnOverlap {
742 address: SocketAddr,
743 sni: Option<String>,
744 protocol: String,
745 },
746 #[error(
751 "more than one TCP frontend on {address} with sni {sni:?} leaves alpn empty (the \
752 catch-all match): at most one frontend per (address, sni) may omit alpn"
753 )]
754 TcpFrontendMultipleAlpnCatchAll {
755 address: SocketAddr,
756 sni: Option<String>,
757 },
758 #[error(
765 "TCP listener {address} is targeted by both a no-SNI frontend and at least one \
766 SNI-scoped frontend: an SNI-enabled listener must not also have a raw-TCP fallback \
767 frontend on the same address"
768 )]
769 TcpListenerMixesSniAndNoSni { address: SocketAddr },
770 #[error(
776 "sni_preread_timeout = {sni_preread_timeout}s on TCP listener {address} exceeds its \
777 front_timeout = {front_timeout}s: the preread phase cannot outlive the frontend \
778 inactivity timeout that would already have closed the connection"
779 )]
780 SniPrereadTimeoutExceedsFrontTimeout {
781 address: SocketAddr,
782 sni_preread_timeout: u32,
783 front_timeout: u32,
784 },
785 #[error(
791 "sni_preread_max_bytes = {sni_preread_max_bytes} on TCP listener {address} exceeds \
792 buffer_size = {buffer_size}: raise buffer_size to >= {sni_preread_max_bytes} or lower \
793 sni_preread_max_bytes"
794 )]
795 SniPrereadMaxBytesExceedsBufferSize {
796 address: SocketAddr,
797 sni_preread_max_bytes: u32,
798 buffer_size: u64,
799 },
800 #[error(
810 "sni_preread_max_bytes = {sni_preread_max_bytes} on TCP listener {address} is below the \
811 minimum of {minimum} bytes (a full TLS record header): the preread shell could never \
812 read enough bytes to make progress. Raise sni_preread_max_bytes to >= {minimum}"
813 )]
814 SniPrereadMaxBytesTooSmall {
815 address: SocketAddr,
816 sni_preread_max_bytes: u32,
817 minimum: u32,
818 },
819 #[error(
824 "cluster {cluster_id}: cannot resolve backend address {address:?}: {reason}. \
825 Expected an IP:port socket address or a resolvable host:port"
826 )]
827 BackendAddressResolution {
828 cluster_id: String,
829 address: String,
830 reason: String,
831 },
832}
833
834#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
840#[serde(rename_all = "lowercase")]
841pub enum ClientAuthConfig {
842 #[default]
843 None,
844 Optional,
845 Required,
846}
847
848impl From<ClientAuthConfig> for ClientAuthMode {
849 fn from(value: ClientAuthConfig) -> Self {
850 match value {
851 ClientAuthConfig::None => ClientAuthMode::ClientAuthNone,
852 ClientAuthConfig::Optional => ClientAuthMode::ClientAuthOptional,
853 ClientAuthConfig::Required => ClientAuthMode::ClientAuthRequired,
854 }
855 }
856}
857
858#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
860#[serde(deny_unknown_fields)]
861pub struct ListenerBuilder {
862 pub address: SocketAddr,
863 pub interface: Option<String>,
869 pub protocol: Option<ListenerProtocol>,
870 pub public_address: Option<SocketAddr>,
871 pub answer_301: Option<String>,
872 pub answer_400: Option<String>,
873 pub answer_401: Option<String>,
874 pub answer_404: Option<String>,
875 pub answer_408: Option<String>,
876 pub answer_413: Option<String>,
877 pub answer_421: Option<String>,
880 pub answer_502: Option<String>,
881 pub answer_503: Option<String>,
882 pub answer_504: Option<String>,
883 pub answer_507: Option<String>,
884 pub answer_429: Option<String>,
889 pub tls_versions: Option<Vec<TlsVersion>>,
890 pub cipher_list: Option<Vec<String>>,
891 pub cipher_suites: Option<Vec<String>>,
892 pub groups_list: Option<Vec<String>>,
893 pub expect_proxy: Option<bool>,
894 #[serde(default = "default_sticky_name")]
895 pub sticky_name: String,
896 pub certificate: Option<String>,
897 pub certificate_chain: Option<String>,
898 pub key: Option<String>,
899 pub front_timeout: Option<u32>,
901 pub back_timeout: Option<u32>,
903 pub connect_timeout: Option<u32>,
905 pub request_timeout: Option<u32>,
907 pub config: Option<Config>,
909 pub send_tls13_tickets: Option<u64>,
913 pub alpn_protocols: Option<Vec<String>>,
916 pub h2_max_rst_stream_per_window: Option<u32>,
918 pub h2_max_ping_per_window: Option<u32>,
920 pub h2_max_settings_per_window: Option<u32>,
922 pub h2_max_empty_data_per_window: Option<u32>,
924 pub h2_max_window_update_stream0_per_window: Option<u32>,
930 pub sozu_id_header: Option<String>,
934 pub h2_max_continuation_frames: Option<u32>,
936 pub h2_max_glitch_count: Option<u32>,
939 pub h2_initial_connection_window: Option<u32>,
941 pub h2_max_concurrent_streams: Option<u32>,
943 pub h2_stream_shrink_ratio: Option<u32>,
945 pub h2_max_rst_stream_lifetime: Option<u64>,
950 pub h2_max_rst_stream_abusive_lifetime: Option<u64>,
956 pub h2_max_rst_stream_emitted_lifetime: Option<u64>,
963 pub h2_max_header_list_size: Option<u32>,
967 pub h2_max_header_table_size: Option<u32>,
971 pub h2_max_header_fields: Option<u32>,
975 pub h2_stream_refusal_percent: Option<u32>,
982 pub h2_stream_idle_timeout_seconds: Option<u32>,
988 pub h2_graceful_shutdown_deadline_seconds: Option<u32>,
993 pub strict_sni_binding: Option<bool>,
999 pub disable_http11: Option<bool>,
1004 pub elide_x_real_ip: Option<bool>,
1008 pub send_x_real_ip: Option<bool>,
1013 pub forwarded_headers: Option<ForwardedHeadersMode>,
1017 pub answers: Option<BTreeMap<String, String>>,
1032 pub hsts: Option<FileHstsConfig>,
1038 pub max_rx_datagram_size: Option<u32>,
1042 pub max_flows: Option<u32>,
1047 pub sni_preread_timeout: Option<u32>,
1052 pub sni_preread_max_bytes: Option<u32>,
1058 pub client_auth: Option<ClientAuthConfig>,
1062 pub client_ca_certificates: Option<Vec<String>>,
1067 pub client_ca_crls: Option<Vec<String>>,
1071}
1072
1073pub fn default_sticky_name() -> String {
1074 DEFAULT_STICKY_NAME.to_string()
1075}
1076
1077impl ListenerBuilder {
1078 pub fn new_http(address: SocketAddress) -> ListenerBuilder {
1081 Self::new(address, ListenerProtocol::Http)
1082 }
1083
1084 pub fn new_tcp(address: SocketAddress) -> ListenerBuilder {
1087 Self::new(address, ListenerProtocol::Tcp)
1088 }
1089
1090 pub fn new_https(address: SocketAddress) -> ListenerBuilder {
1093 Self::new(address, ListenerProtocol::Https)
1094 }
1095
1096 pub fn new_udp(address: SocketAddress) -> ListenerBuilder {
1099 Self::new(address, ListenerProtocol::Udp)
1100 }
1101
1102 fn new(address: SocketAddress, protocol: ListenerProtocol) -> ListenerBuilder {
1104 ListenerBuilder {
1105 address: address.into(),
1106 interface: None,
1107 answer_301: None,
1108 answer_401: None,
1109 answer_400: None,
1110 answer_404: None,
1111 answer_408: None,
1112 answer_413: None,
1113 answer_421: None,
1114 answer_502: None,
1115 answer_503: None,
1116 answer_504: None,
1117 answer_507: None,
1118 answer_429: None,
1119 back_timeout: None,
1120 certificate_chain: None,
1121 certificate: None,
1122 cipher_list: None,
1123 cipher_suites: None,
1124 groups_list: None,
1125 config: None,
1126 connect_timeout: None,
1127 expect_proxy: None,
1128 front_timeout: None,
1129 key: None,
1130 protocol: Some(protocol),
1131 public_address: None,
1132 request_timeout: None,
1133 send_tls13_tickets: None,
1134 sticky_name: DEFAULT_STICKY_NAME.to_string(),
1135 tls_versions: None,
1136 alpn_protocols: None,
1137 h2_max_rst_stream_per_window: None,
1138 h2_max_ping_per_window: None,
1139 h2_max_settings_per_window: None,
1140 h2_max_empty_data_per_window: None,
1141 h2_max_window_update_stream0_per_window: None,
1142 sozu_id_header: None,
1143 h2_max_continuation_frames: None,
1144 h2_max_glitch_count: None,
1145 h2_initial_connection_window: None,
1146 h2_max_concurrent_streams: None,
1147 h2_stream_shrink_ratio: None,
1148 h2_max_rst_stream_lifetime: None,
1149 h2_max_rst_stream_abusive_lifetime: None,
1150 h2_max_rst_stream_emitted_lifetime: None,
1151 h2_max_header_list_size: None,
1152 h2_max_header_table_size: None,
1153 h2_max_header_fields: None,
1154 h2_stream_refusal_percent: None,
1155 h2_stream_idle_timeout_seconds: None,
1156 h2_graceful_shutdown_deadline_seconds: None,
1157 strict_sni_binding: None,
1158 disable_http11: None,
1159 elide_x_real_ip: None,
1160 send_x_real_ip: None,
1161 forwarded_headers: None,
1162 answers: None,
1163 hsts: None,
1164 max_rx_datagram_size: None,
1165 max_flows: None,
1166 sni_preread_timeout: None,
1167 sni_preread_max_bytes: None,
1168 client_auth: None,
1169 client_ca_certificates: None,
1170 client_ca_crls: None,
1171 }
1172 }
1173
1174 pub fn with_interface<S>(&mut self, interface: Option<S>) -> &mut Self
1176 where
1177 S: ToString,
1178 {
1179 self.interface = interface.map(|interface| interface.to_string());
1180 self
1181 }
1182
1183 fn validated_interface(&self) -> Result<Option<String>, ConfigError> {
1186 if let Some(interface) = &self.interface {
1187 validate_listener_interface(interface).map_err(|error| {
1188 ConfigError::ListenerInterface {
1189 address: self.address,
1190 error,
1191 }
1192 })?;
1193 }
1194 Ok(self.interface.clone())
1195 }
1196
1197 pub fn with_public_address(&mut self, public_address: Option<SocketAddr>) -> &mut Self {
1198 if let Some(address) = public_address {
1199 self.public_address = Some(address);
1200 }
1201 self
1202 }
1203
1204 pub fn with_answer_404_path<S>(&mut self, answer_404_path: Option<S>) -> &mut Self
1205 where
1206 S: ToString,
1207 {
1208 if let Some(path) = answer_404_path {
1209 self.answer_404 = Some(path.to_string());
1210 }
1211 self
1212 }
1213
1214 pub fn with_answer_503_path<S>(&mut self, answer_503_path: Option<S>) -> &mut Self
1215 where
1216 S: ToString,
1217 {
1218 if let Some(path) = answer_503_path {
1219 self.answer_503 = Some(path.to_string());
1220 }
1221 self
1222 }
1223
1224 pub fn with_tls_versions(&mut self, tls_versions: Vec<TlsVersion>) -> &mut Self {
1225 self.tls_versions = Some(tls_versions);
1226 self
1227 }
1228
1229 pub fn with_cipher_list(&mut self, cipher_list: Option<Vec<String>>) -> &mut Self {
1230 self.cipher_list = cipher_list;
1231 self
1232 }
1233
1234 pub fn with_cipher_suites(&mut self, cipher_suites: Option<Vec<String>>) -> &mut Self {
1235 self.cipher_suites = cipher_suites;
1236 self
1237 }
1238
1239 pub fn with_alpn_protocols(&mut self, alpn_protocols: Option<Vec<String>>) -> &mut Self {
1240 self.alpn_protocols = alpn_protocols;
1241 self
1242 }
1243
1244 pub fn with_elide_x_real_ip(&mut self, elide_x_real_ip: bool) -> &mut Self {
1247 self.elide_x_real_ip = Some(elide_x_real_ip);
1248 self
1249 }
1250
1251 pub fn with_send_x_real_ip(&mut self, send_x_real_ip: bool) -> &mut Self {
1255 self.send_x_real_ip = Some(send_x_real_ip);
1256 self
1257 }
1258
1259 pub fn with_forwarded_headers(&mut self, forwarded_headers: ForwardedHeadersMode) -> &mut Self {
1262 self.forwarded_headers = Some(forwarded_headers);
1263 self
1264 }
1265
1266 pub fn with_expect_proxy(&mut self, expect_proxy: bool) -> &mut Self {
1267 self.expect_proxy = Some(expect_proxy);
1268 self
1269 }
1270
1271 pub fn with_sticky_name<S>(&mut self, sticky_name: Option<S>) -> &mut Self
1272 where
1273 S: ToString,
1274 {
1275 if let Some(name) = sticky_name {
1276 self.sticky_name = name.to_string();
1277 }
1278 self
1279 }
1280
1281 pub fn with_certificate<S>(&mut self, certificate: S) -> &mut Self
1282 where
1283 S: ToString,
1284 {
1285 self.certificate = Some(certificate.to_string());
1286 self
1287 }
1288
1289 pub fn with_certificate_chain(&mut self, certificate_chain: String) -> &mut Self {
1290 self.certificate = Some(certificate_chain);
1291 self
1292 }
1293
1294 pub fn with_key<S>(&mut self, key: String) -> &mut Self
1295 where
1296 S: ToString,
1297 {
1298 self.key = Some(key);
1299 self
1300 }
1301
1302 pub fn with_front_timeout(&mut self, front_timeout: Option<u32>) -> &mut Self {
1303 self.front_timeout = front_timeout;
1304 self
1305 }
1306
1307 pub fn with_back_timeout(&mut self, back_timeout: Option<u32>) -> &mut Self {
1308 self.back_timeout = back_timeout;
1309 self
1310 }
1311
1312 pub fn with_connect_timeout(&mut self, connect_timeout: Option<u32>) -> &mut Self {
1313 self.connect_timeout = connect_timeout;
1314 self
1315 }
1316
1317 pub fn with_request_timeout(&mut self, request_timeout: Option<u32>) -> &mut Self {
1318 self.request_timeout = request_timeout;
1319 self
1320 }
1321
1322 pub fn with_answer<S, P>(&mut self, code: S, path: P) -> &mut Self
1327 where
1328 S: ToString,
1329 P: ToString,
1330 {
1331 self.answers
1332 .get_or_insert_with(BTreeMap::new)
1333 .insert(code.to_string(), path.to_string());
1334 self
1335 }
1336
1337 pub fn with_answers(&mut self, answers: BTreeMap<String, String>) -> &mut Self {
1340 self.answers = Some(answers);
1341 self
1342 }
1343
1344 fn get_http_answers(&self) -> Result<Option<CustomHttpAnswers>, ConfigError> {
1346 let http_answers = CustomHttpAnswers {
1347 answer_301: read_http_answer_file(&self.answer_301)?,
1348 answer_400: read_http_answer_file(&self.answer_400)?,
1349 answer_401: read_http_answer_file(&self.answer_401)?,
1350 answer_404: read_http_answer_file(&self.answer_404)?,
1351 answer_408: read_http_answer_file(&self.answer_408)?,
1352 answer_413: read_http_answer_file(&self.answer_413)?,
1353 answer_421: read_http_answer_file(&self.answer_421)?,
1354 answer_502: read_http_answer_file(&self.answer_502)?,
1355 answer_503: read_http_answer_file(&self.answer_503)?,
1356 answer_504: read_http_answer_file(&self.answer_504)?,
1357 answer_507: read_http_answer_file(&self.answer_507)?,
1358 answer_429: read_http_answer_file(&self.answer_429)?,
1359 };
1360 Ok(Some(http_answers))
1361 }
1362
1363 fn get_listener_answers(&self) -> Result<BTreeMap<String, String>, ConfigError> {
1371 let mut out = BTreeMap::new();
1372
1373 macro_rules! merge_legacy {
1377 ($code:literal, $field:ident) => {
1378 if let Some(body) = read_http_answer_file(&self.$field)? {
1379 out.insert($code.to_owned(), body);
1380 }
1381 };
1382 }
1383 merge_legacy!("301", answer_301);
1384 merge_legacy!("400", answer_400);
1385 merge_legacy!("401", answer_401);
1386 merge_legacy!("404", answer_404);
1387 merge_legacy!("408", answer_408);
1388 merge_legacy!("413", answer_413);
1389 merge_legacy!("421", answer_421);
1390 merge_legacy!("502", answer_502);
1391 merge_legacy!("503", answer_503);
1392 merge_legacy!("504", answer_504);
1393 merge_legacy!("507", answer_507);
1394 merge_legacy!("429", answer_429);
1395
1396 if let Some(map) = &self.answers {
1397 let loaded = load_answers(map)?;
1398 out.extend(loaded);
1399 }
1400 Ok(out)
1401 }
1402
1403 fn assign_config_timeouts(&mut self, config: &Config) {
1405 self.front_timeout = Some(self.front_timeout.unwrap_or(config.front_timeout));
1406 self.back_timeout = Some(self.back_timeout.unwrap_or(config.back_timeout));
1407 self.connect_timeout = Some(self.connect_timeout.unwrap_or(config.connect_timeout));
1408 self.request_timeout = Some(self.request_timeout.unwrap_or(config.request_timeout));
1409 }
1410
1411 fn validate_h2_thresholds(&self) -> Result<(), ConfigError> {
1433 macro_rules! require_at_least {
1434 ($builder:ident, $field:ident, $minimum:literal) => {
1435 if let Some(value) = $builder.$field
1436 && u64::from(value) < $minimum
1437 {
1438 return Err(ConfigError::H2ThresholdBelowMinimum {
1439 address: $builder.address.to_string(),
1440 key: stringify!($field),
1441 value: u64::from(value),
1442 minimum: $minimum,
1443 });
1444 }
1445 };
1446 }
1447 crate::for_each_h2_knob_floor!(require_at_least, self);
1448 Ok(())
1449 }
1450
1451 fn reject_client_auth_fields(&self, listener_kind: &str) -> Result<(), ConfigError> {
1456 let has_mtls = self
1457 .client_auth
1458 .is_some_and(|m| m != ClientAuthConfig::None)
1459 || self
1460 .client_ca_certificates
1461 .as_ref()
1462 .is_some_and(|v| !v.is_empty())
1463 || self.client_ca_crls.as_ref().is_some_and(|v| !v.is_empty());
1464 if has_mtls {
1465 return Err(ConfigError::ClientAuthOnNonHttps(format!(
1466 "{} listener {}",
1467 listener_kind, self.address
1468 )));
1469 }
1470 Ok(())
1471 }
1472
1473 pub fn to_http(&mut self, config: Option<&Config>) -> Result<HttpListenerConfig, ConfigError> {
1475 if self.protocol != Some(ListenerProtocol::Http) {
1476 return Err(ConfigError::WrongListenerProtocol {
1477 expected: ListenerProtocol::Http,
1478 found: self.protocol.to_owned(),
1479 });
1480 }
1481 self.reject_client_auth_fields("HTTP")?;
1482
1483 self.validate_h2_thresholds()?;
1484
1485 if self.hsts.is_some() {
1491 return Err(ConfigError::HstsOnPlainHttp(format!(
1492 "HTTP listener {}",
1493 self.address
1494 )));
1495 }
1496
1497 if let Some(config) = config {
1498 self.assign_config_timeouts(config);
1499 }
1500
1501 let http_answers = self.get_http_answers()?;
1502 let answers = self.get_listener_answers()?;
1503
1504 let interface = self.validated_interface()?;
1505 let configuration = HttpListenerConfig {
1506 address: self.address.into(),
1507 interface,
1508 public_address: self.public_address.map(|a| a.into()),
1509 expect_proxy: self.expect_proxy.unwrap_or(false),
1510 sticky_name: self.sticky_name.clone(),
1511 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1512 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1513 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1514 request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1515 http_answers,
1516 answers,
1517 h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1518 h2_max_ping_per_window: self.h2_max_ping_per_window,
1519 h2_max_settings_per_window: self.h2_max_settings_per_window,
1520 h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1521 h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1522 h2_max_continuation_frames: self.h2_max_continuation_frames,
1523 h2_max_glitch_count: self.h2_max_glitch_count,
1524 h2_initial_connection_window: self.h2_initial_connection_window,
1525 h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1526 h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1527 h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1528 h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1529 h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1530 h2_max_header_list_size: self.h2_max_header_list_size,
1531 h2_max_header_table_size: self.h2_max_header_table_size,
1532 h2_max_header_fields: self.h2_max_header_fields,
1533 h2_stream_refusal_percent: self.h2_stream_refusal_percent,
1534 h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1535 h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1536 sozu_id_header: self.sozu_id_header.clone(),
1537 elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1538 send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1539 forwarded_headers: Some(ForwardedHeaders::from(
1540 self.forwarded_headers.unwrap_or_default(),
1541 ) as i32),
1542 ..Default::default()
1543 };
1544
1545 debug_assert_eq!(
1550 configuration.address,
1551 self.address.into(),
1552 "HTTP listener must bind the requested address"
1553 );
1554 Ok(configuration)
1555 }
1556
1557 pub fn to_tls(&mut self, config: Option<&Config>) -> Result<HttpsListenerConfig, ConfigError> {
1559 if self.protocol != Some(ListenerProtocol::Https) {
1560 return Err(ConfigError::WrongListenerProtocol {
1561 expected: ListenerProtocol::Https,
1562 found: self.protocol.to_owned(),
1563 });
1564 }
1565
1566 self.validate_h2_thresholds()?;
1567
1568 let default_cipher_list = DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect();
1569
1570 let cipher_list = self.cipher_list.clone().unwrap_or(default_cipher_list);
1571
1572 let cipher_suites = self
1573 .cipher_suites
1574 .clone()
1575 .unwrap_or_else(|| DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect());
1576
1577 let signature_algorithms: Vec<String> = DEFAULT_SIGNATURE_ALGORITHMS
1578 .into_iter()
1579 .map(String::from)
1580 .collect();
1581
1582 let groups_list = self
1583 .groups_list
1584 .clone()
1585 .unwrap_or_else(|| DEFAULT_GROUPS_LIST.into_iter().map(String::from).collect());
1586
1587 let alpn_protocols: Vec<String> = match &self.alpn_protocols {
1588 Some(protos) if !protos.is_empty() => {
1589 for proto in protos {
1590 match proto.as_str() {
1591 "h2" | "http/1.1" => {}
1592 other => return Err(ConfigError::InvalidAlpnProtocol(other.to_owned())),
1593 }
1594 }
1595 if self.disable_http11.unwrap_or(false) && protos.iter().any(|p| p == "http/1.1") {
1600 return Err(ConfigError::DisableHttp11WithHttp11Alpn {
1601 address: self.address.to_string(),
1602 });
1603 }
1604 if !protos.iter().any(|p| p == "http/1.1") {
1605 warn!(
1606 "ALPN protocols do not include 'http/1.1'. Clients without H2 support will fail TLS negotiation."
1607 );
1608 }
1609 let mut seen = std::collections::HashSet::new();
1611 protos
1612 .iter()
1613 .filter(|p| seen.insert(p.as_str()))
1614 .cloned()
1615 .collect()
1616 }
1617 _ => {
1618 if self.disable_http11.unwrap_or(false)
1622 && DEFAULT_ALPN_PROTOCOLS.contains(&"http/1.1")
1623 {
1624 return Err(ConfigError::DisableHttp11WithHttp11Alpn {
1625 address: self.address.to_string(),
1626 });
1627 }
1628 DEFAULT_ALPN_PROTOCOLS
1629 .iter()
1630 .map(|s| s.to_string())
1631 .collect()
1632 }
1633 };
1634
1635 let versions = match self.tls_versions {
1636 None => vec![TlsVersion::TlsV12 as i32, TlsVersion::TlsV13 as i32],
1637 Some(ref v) => v.iter().map(|v| *v as i32).collect(),
1638 };
1639
1640 let key = self.key.as_ref().and_then(|path| {
1641 Config::load_file(path)
1642 .map_err(|e| {
1643 error!("cannot load key at path '{}': {:?}", path, e);
1644 e
1645 })
1646 .ok()
1647 });
1648 let certificate = self.certificate.as_ref().and_then(|path| {
1649 Config::load_file(path)
1650 .map_err(|e| {
1651 error!("cannot load certificate at path '{}': {:?}", path, e);
1652 e
1653 })
1654 .ok()
1655 });
1656 let certificate_chain = self
1657 .certificate_chain
1658 .as_ref()
1659 .and_then(|path| {
1660 Config::load_file(path)
1661 .map_err(|e| {
1662 error!("cannot load certificate chain at path '{}': {:?}", path, e);
1663 e
1664 })
1665 .ok()
1666 })
1667 .map(split_certificate_chain)
1668 .unwrap_or_default();
1669
1670 let has_client_auth_files = self
1683 .client_ca_certificates
1684 .as_ref()
1685 .is_some_and(|paths| !paths.is_empty())
1686 || self
1687 .client_ca_crls
1688 .as_ref()
1689 .is_some_and(|paths| !paths.is_empty());
1690 match self.client_auth {
1691 None if has_client_auth_files => {
1692 return Err(ConfigError::ClientAuthModeMissing(format!(
1693 "HTTPS listener {}",
1694 self.address
1695 )));
1696 }
1697 Some(ClientAuthConfig::None) if has_client_auth_files => {
1698 warn!(
1699 "HTTPS listener {}: client_auth is \"none\", so its client_ca_certificates \
1700 and client_ca_crls are ignored and clients are not authenticated",
1701 self.address
1702 );
1703 }
1704 _ => {}
1705 }
1706 let mode = self.client_auth.unwrap_or_default();
1707 let client_auth = self
1708 .client_auth
1709 .map(|mode| ClientAuthMode::from(mode) as i32);
1710 let load_pem_paths = |paths: &Option<Vec<String>>| -> Result<Vec<String>, ConfigError> {
1711 if mode == ClientAuthConfig::None {
1712 return Ok(Vec::new());
1713 }
1714 paths
1715 .as_ref()
1716 .map(|list| list.iter().map(|path| Config::load_file(path)).collect())
1717 .unwrap_or_else(|| Ok(Vec::new()))
1718 };
1719 let client_ca_certificates = load_pem_paths(&self.client_ca_certificates)?;
1720 let client_ca_crls = load_pem_paths(&self.client_ca_crls)?;
1721
1722 let http_answers = self.get_http_answers()?;
1723 let answers = self.get_listener_answers()?;
1724
1725 if let Some(config) = config {
1726 self.assign_config_timeouts(config);
1727 }
1728
1729 let https_listener_config = HttpsListenerConfig {
1730 interface: self.validated_interface()?,
1731 address: self.address.into(),
1732 sticky_name: self.sticky_name.clone(),
1733 public_address: self.public_address.map(|a| a.into()),
1734 cipher_list,
1735 versions,
1736 expect_proxy: self.expect_proxy.unwrap_or(false),
1737 key,
1738 certificate,
1739 certificate_chain,
1740 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1741 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1742 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1743 request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1744 cipher_suites,
1745 signature_algorithms,
1746 groups_list,
1747 active: false,
1748 send_tls13_tickets: self
1749 .send_tls13_tickets
1750 .unwrap_or(DEFAULT_SEND_TLS_13_TICKETS),
1751 http_answers,
1752 answers,
1753 alpn_protocols,
1754 h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1755 h2_max_ping_per_window: self.h2_max_ping_per_window,
1756 h2_max_settings_per_window: self.h2_max_settings_per_window,
1757 h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1758 h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1759 h2_max_continuation_frames: self.h2_max_continuation_frames,
1760 h2_max_glitch_count: self.h2_max_glitch_count,
1761 h2_initial_connection_window: self.h2_initial_connection_window,
1762 h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1763 h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1764 h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1765 h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1766 h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1767 h2_max_header_list_size: self.h2_max_header_list_size,
1768 h2_max_header_table_size: self.h2_max_header_table_size,
1769 h2_max_header_fields: self.h2_max_header_fields,
1770 h2_stream_refusal_percent: self.h2_stream_refusal_percent,
1771 strict_sni_binding: self.strict_sni_binding,
1772 disable_http11: self.disable_http11,
1773 h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1774 h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1775 sozu_id_header: self.sozu_id_header.clone(),
1776 elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1777 send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1778 forwarded_headers: Some(ForwardedHeaders::from(
1779 self.forwarded_headers.unwrap_or_default(),
1780 ) as i32),
1781 hsts: match self.hsts.as_ref() {
1782 Some(h) => Some(h.to_proto("listener")?),
1783 None => None,
1784 },
1785 client_auth,
1786 client_ca_certificates,
1787 client_ca_crls,
1788 };
1789
1790 debug_assert_eq!(
1794 https_listener_config.address,
1795 self.address.into(),
1796 "HTTPS listener must bind the requested address"
1797 );
1798 debug_assert!(
1799 !https_listener_config.active,
1800 "a freshly built HTTPS listener must start inactive"
1801 );
1802 debug_assert!(
1807 !https_listener_config.alpn_protocols.is_empty(),
1808 "resolved ALPN list must not be empty"
1809 );
1810 debug_assert!(
1811 https_listener_config
1812 .alpn_protocols
1813 .iter()
1814 .all(|p| p == "h2" || p == "http/1.1"),
1815 "resolved ALPN list must contain only h2 and http/1.1"
1816 );
1817 debug_assert!(
1818 {
1819 let mut seen = std::collections::HashSet::new();
1820 https_listener_config
1821 .alpn_protocols
1822 .iter()
1823 .all(|p| seen.insert(p))
1824 },
1825 "resolved ALPN list must be duplicate-free"
1826 );
1827 debug_assert!(
1830 !(self.disable_http11.unwrap_or(false)
1831 && https_listener_config
1832 .alpn_protocols
1833 .iter()
1834 .any(|p| p == "http/1.1")),
1835 "disable_http11 with http/1.1 in ALPN must have been rejected"
1836 );
1837 Ok(https_listener_config)
1838 }
1839
1840 pub fn to_tcp(&mut self, config: Option<&Config>) -> Result<TcpListenerConfig, ConfigError> {
1842 if self.protocol != Some(ListenerProtocol::Tcp) {
1843 return Err(ConfigError::WrongListenerProtocol {
1844 expected: ListenerProtocol::Tcp,
1845 found: self.protocol.to_owned(),
1846 });
1847 }
1848 self.reject_client_auth_fields("TCP")?;
1849
1850 if let Some(config) = config {
1851 self.assign_config_timeouts(config);
1852 }
1853
1854 let tcp_listener_config = TcpListenerConfig {
1855 interface: self.validated_interface()?,
1856 address: self.address.into(),
1857 public_address: self.public_address.map(|a| a.into()),
1858 expect_proxy: self.expect_proxy.unwrap_or(false),
1859 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1860 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1861 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1862 active: false,
1863 sni_preread_timeout: Some(
1864 self.sni_preread_timeout
1865 .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT),
1866 ),
1867 sni_preread_max_bytes: Some(
1868 self.sni_preread_max_bytes
1869 .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES),
1870 ),
1871 };
1872
1873 debug_assert_eq!(
1876 tcp_listener_config.address,
1877 self.address.into(),
1878 "TCP listener must bind the requested address"
1879 );
1880 debug_assert!(
1881 !tcp_listener_config.active,
1882 "a freshly built TCP listener must start inactive"
1883 );
1884 Ok(tcp_listener_config)
1885 }
1886
1887 pub fn to_udp(&mut self, config: Option<&Config>) -> Result<UdpListenerConfig, ConfigError> {
1905 if self.protocol != Some(ListenerProtocol::Udp) {
1906 return Err(ConfigError::WrongListenerProtocol {
1907 expected: ListenerProtocol::Udp,
1908 found: self.protocol.to_owned(),
1909 });
1910 }
1911 self.reject_client_auth_fields("UDP")?;
1912
1913 let mut max_rx_datagram_size = self
1914 .max_rx_datagram_size
1915 .unwrap_or(DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE);
1916 let buffer_size = config.map(|c| c.buffer_size).unwrap_or(DEFAULT_BUFFER_SIZE);
1917 if u64::from(max_rx_datagram_size) > buffer_size {
1918 warn!(
1919 "UDP listener {}: max_rx_datagram_size = {} exceeds buffer_size = {}, clamping to buffer_size",
1920 self.address, max_rx_datagram_size, buffer_size
1921 );
1922 max_rx_datagram_size = buffer_size as u32;
1923 }
1924
1925 let max_flows = self.max_flows.unwrap_or(DEFAULT_UDP_MAX_FLOWS);
1926 if max_flows > 0
1927 && let Some(soft_limit) = soft_rlimit_nofile()
1928 {
1929 let advisory = soft_limit.saturating_mul(7) / 10;
1930 if u64::from(max_flows) > advisory {
1931 warn!(
1932 "UDP listener {}: max_flows = {} exceeds ~70% of the soft RLIMIT_NOFILE ({}); \
1933 per-flow connected sockets may hit EMFILE",
1934 self.address, max_flows, advisory
1935 );
1936 }
1937 }
1938
1939 Ok(UdpListenerConfig {
1940 interface: self.validated_interface()?,
1941 address: self.address.into(),
1942 public_address: self.public_address.map(|a| a.into()),
1943 front_timeout: self.front_timeout.unwrap_or(DEFAULT_UDP_FRONT_TIMEOUT),
1944 back_timeout: self.back_timeout.unwrap_or(DEFAULT_UDP_BACK_TIMEOUT),
1945 max_rx_datagram_size,
1946 max_flows,
1947 active: false,
1948 })
1949 }
1950}
1951
1952fn soft_rlimit_nofile() -> Option<u64> {
1957 let mut limit = libc::rlimit {
1958 rlim_cur: 0,
1959 rlim_max: 0,
1960 };
1961 let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) };
1965 if rc == 0 {
1966 Some(limit.rlim_cur)
1969 } else {
1970 None
1971 }
1972}
1973
1974fn read_http_answer_file(path: &Option<String>) -> Result<Option<String>, ConfigError> {
1976 match path {
1977 Some(path) => {
1978 let mut content = String::new();
1979 let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
1980 path_to_open: path.to_owned(),
1981 io_error,
1982 })?;
1983
1984 file.read_to_string(&mut content)
1985 .map_err(|io_error| ConfigError::FileRead {
1986 path_to_read: path.to_owned(),
1987 io_error,
1988 })?;
1989
1990 Ok(Some(content))
1991 }
1992 None => Ok(None),
1993 }
1994}
1995
1996pub fn resolve_answer_source(value: &str) -> Result<String, ConfigError> {
2019 if let Some(path) = value.strip_prefix("file://") {
2020 let mut content = String::new();
2021 let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
2022 path_to_open: path.to_owned(),
2023 io_error,
2024 })?;
2025 file.read_to_string(&mut content)
2026 .map_err(|io_error| ConfigError::FileRead {
2027 path_to_read: path.to_owned(),
2028 io_error,
2029 })?;
2030 return Ok(content);
2031 }
2032 Ok(value.to_owned())
2033}
2034
2035pub fn load_answers(
2052 answers: &BTreeMap<String, String>,
2053) -> Result<BTreeMap<String, String>, ConfigError> {
2054 let mut out = BTreeMap::new();
2055 for (code, value) in answers {
2056 if value.is_empty() {
2057 continue;
2058 }
2059 out.insert(code.to_owned(), resolve_answer_source(value)?);
2060 }
2061 debug_assert!(
2065 out.len() <= answers.len(),
2066 "load_answers must not synthesize entries"
2067 );
2068 debug_assert!(
2069 out.keys().all(|k| answers.contains_key(k)),
2070 "every loaded status code must come from the input map"
2071 );
2072 Ok(out)
2073}
2074
2075#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
2088#[serde(rename_all = "lowercase")]
2089#[derive(Default)]
2090pub enum MetricDetailLevel {
2091 Process,
2092 Frontend,
2093 #[default]
2094 Cluster,
2095 Backend,
2096}
2097
2098impl From<MetricDetailLevel> for MetricDetail {
2099 fn from(level: MetricDetailLevel) -> Self {
2100 match level {
2101 MetricDetailLevel::Process => MetricDetail::DetailProcess,
2102 MetricDetailLevel::Frontend => MetricDetail::DetailFrontend,
2103 MetricDetailLevel::Cluster => MetricDetail::DetailCluster,
2104 MetricDetailLevel::Backend => MetricDetail::DetailBackend,
2105 }
2106 }
2107}
2108
2109impl From<MetricDetail> for MetricDetailLevel {
2110 fn from(detail: MetricDetail) -> Self {
2114 match detail {
2115 MetricDetail::DetailProcess => MetricDetailLevel::Process,
2116 MetricDetail::DetailFrontend => MetricDetailLevel::Frontend,
2117 MetricDetail::DetailCluster => MetricDetailLevel::Cluster,
2118 MetricDetail::DetailBackend => MetricDetailLevel::Backend,
2119 }
2120 }
2121}
2122
2123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
2138#[serde(rename_all = "snake_case")]
2139pub enum ForwardedHeadersMode {
2140 #[default]
2141 Both,
2142 XForwarded,
2143 Rfc7239,
2144 None,
2145}
2146
2147impl From<ForwardedHeadersMode> for ForwardedHeaders {
2148 fn from(mode: ForwardedHeadersMode) -> Self {
2149 match mode {
2150 ForwardedHeadersMode::Both => ForwardedHeaders::Both,
2151 ForwardedHeadersMode::XForwarded => ForwardedHeaders::XForwarded,
2152 ForwardedHeadersMode::Rfc7239 => ForwardedHeaders::Rfc7239,
2153 ForwardedHeadersMode::None => ForwardedHeaders::None,
2154 }
2155 }
2156}
2157
2158impl From<ForwardedHeaders> for ForwardedHeadersMode {
2159 fn from(headers: ForwardedHeaders) -> Self {
2160 match headers {
2161 ForwardedHeaders::Both => ForwardedHeadersMode::Both,
2162 ForwardedHeaders::XForwarded => ForwardedHeadersMode::XForwarded,
2163 ForwardedHeaders::Rfc7239 => ForwardedHeadersMode::Rfc7239,
2164 ForwardedHeaders::None => ForwardedHeadersMode::None,
2165 }
2166 }
2167}
2168
2169#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2170#[serde(deny_unknown_fields)]
2171pub struct MetricsConfig {
2172 pub address: SocketAddr,
2173 #[serde(default)]
2174 pub tagged_metrics: bool,
2175 #[serde(default)]
2176 pub prefix: Option<String>,
2177 #[serde(default)]
2180 pub detail: MetricDetailLevel,
2181}
2182
2183#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2184#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2185#[serde(deny_unknown_fields)]
2186pub enum PathRuleType {
2187 Prefix,
2188 Regex,
2189 Equals,
2190}
2191
2192#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2193#[serde(deny_unknown_fields)]
2194pub struct FileClusterFrontendConfig {
2195 pub address: SocketAddr,
2196 pub hostname: Option<String>,
2197 #[serde(default)]
2205 pub alpn: Vec<String>,
2206 pub path: Option<String>,
2208 pub path_type: Option<PathRuleType>,
2210 pub method: Option<String>,
2211 pub certificate: Option<String>,
2212 pub key: Option<String>,
2213 pub certificate_chain: Option<String>,
2214 #[serde(default)]
2215 pub tls_versions: Vec<TlsVersion>,
2216 #[serde(default)]
2217 pub position: RulePosition,
2218 pub tags: Option<BTreeMap<String, String>>,
2219 pub redirect: Option<String>,
2224 pub redirect_scheme: Option<String>,
2228 pub redirect_template: Option<String>,
2232 pub rewrite_host: Option<String>,
2235 pub rewrite_path: Option<String>,
2237 pub rewrite_port: Option<u32>,
2239 pub required_auth: Option<bool>,
2243 pub headers: Option<Vec<HeaderEditConfig>>,
2247 pub hsts: Option<FileHstsConfig>,
2252 pub header_predicates: Option<Vec<HeaderPredicateConfig>>,
2255}
2256
2257#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2265#[serde(deny_unknown_fields)]
2266pub struct HeaderPredicateConfig {
2267 pub name: String,
2268 #[serde(rename = "match")]
2269 pub kind: String,
2270 pub value: Option<String>,
2271}
2272
2273#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2280#[serde(deny_unknown_fields)]
2281pub struct HeaderEditConfig {
2282 pub position: String,
2283 pub key: String,
2284 pub value: String,
2285}
2286
2287#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2298#[serde(deny_unknown_fields)]
2299pub struct FileHstsConfig {
2300 pub enabled: Option<bool>,
2303 pub max_age: Option<u32>,
2308 pub include_subdomains: Option<bool>,
2310 pub preload: Option<bool>,
2313 pub force_replace_backend: Option<bool>,
2321}
2322
2323impl FileHstsConfig {
2324 pub fn to_proto(&self, scope: &str) -> Result<HstsConfig, ConfigError> {
2342 let enabled = match self.enabled {
2343 Some(v) => v,
2344 None => return Err(ConfigError::HstsEnabledRequired(scope.to_owned())),
2345 };
2346
2347 let max_age = match (enabled, self.max_age) {
2348 (true, None) => Some(DEFAULT_HSTS_MAX_AGE),
2349 (_, m) => m,
2350 };
2351
2352 if let Some(value) = max_age
2353 && value > 0
2354 && value < 86_400
2355 {
2356 warn!(
2357 "HSTS max_age = {}s on {} is below 1 day — this is almost certainly a \
2358 misconfiguration. RFC 6797 §11.4 reserves max_age = 0 as the explicit kill \
2359 switch.",
2360 value, scope
2361 );
2362 }
2363
2364 let include_subdomains = self.include_subdomains;
2365 let preload = self.preload;
2366
2367 if matches!(preload, Some(true)) {
2368 let max_age_value = max_age.unwrap_or(0);
2369 if max_age_value < DEFAULT_HSTS_MAX_AGE {
2370 warn!(
2371 "HSTS preload = true on {} with max_age = {}s; the Chrome HSTS preload \
2372 list requires max_age >= {} (https://hstspreload.org/).",
2373 scope, max_age_value, DEFAULT_HSTS_MAX_AGE
2374 );
2375 }
2376 if include_subdomains != Some(true) {
2377 warn!(
2378 "HSTS preload = true on {} without include_subdomains = true; the Chrome \
2379 HSTS preload list requires includeSubDomains \
2380 (https://hstspreload.org/).",
2381 scope
2382 );
2383 }
2384 }
2385
2386 let config = HstsConfig {
2387 enabled: Some(enabled),
2388 max_age,
2389 include_subdomains,
2390 preload,
2391 force_replace_backend: self.force_replace_backend,
2392 };
2393
2394 debug_assert_eq!(
2399 config.enabled,
2400 Some(enabled),
2401 "built HSTS config must record the resolved enabled flag"
2402 );
2403 debug_assert!(
2404 !enabled || config.max_age.is_some(),
2405 "an enabled HSTS policy must carry a max_age"
2406 );
2407 Ok(config)
2408 }
2409}
2410
2411impl FileClusterFrontendConfig {
2412 pub fn to_tcp_front(&self) -> Result<TcpFrontendConfig, ConfigError> {
2413 if self.path.is_some() {
2414 return Err(ConfigError::InvalidFrontendConfig(
2415 "path_prefix".to_string(),
2416 ));
2417 }
2418 if self.certificate.is_some() {
2419 return Err(ConfigError::InvalidFrontendConfig(
2420 "certificate".to_string(),
2421 ));
2422 }
2423 if self.certificate_chain.is_some() {
2424 return Err(ConfigError::InvalidFrontendConfig(
2425 "certificate_chain".to_string(),
2426 ));
2427 }
2428
2429 if self.header_predicates.is_some() {
2430 return Err(ConfigError::InvalidFrontendConfig(
2431 "header_predicates".to_string(),
2432 ));
2433 }
2434
2435 let sni = match &self.hostname {
2440 Some(hostname) => Some(validate_sni_pattern(hostname)?),
2441 None => None,
2442 };
2443
2444 if sni.is_none() && !self.alpn.is_empty() {
2451 return Err(ConfigError::AlpnWithoutSni {
2452 address: self.address,
2453 });
2454 }
2455
2456 let tcp_front = TcpFrontendConfig {
2457 address: self.address,
2458 tags: self.tags.clone(),
2459 sni,
2460 alpn: self.alpn.clone(),
2461 udp: false,
2464 };
2465 debug_assert_eq!(
2472 tcp_front.address, self.address,
2473 "TCP frontend must bind the requested address"
2474 );
2475 debug_assert!(
2476 self.path.is_none() && self.certificate.is_none() && self.certificate_chain.is_none(),
2477 "a built TCP frontend must carry no HTTP-only attributes"
2478 );
2479 debug_assert!(
2480 tcp_front.sni.is_some() || tcp_front.alpn.is_empty(),
2481 "a built TCP frontend without sni must never carry a non-empty alpn"
2482 );
2483 Ok(tcp_front)
2484 }
2485
2486 pub fn to_http_front(&self, _cluster_id: &str) -> Result<HttpFrontendConfig, ConfigError> {
2487 if !self.alpn.is_empty() {
2488 return Err(ConfigError::InvalidFrontendConfig("alpn".to_string()));
2489 }
2490
2491 let hostname = match &self.hostname {
2492 Some(hostname) => hostname.to_owned(),
2493 None => {
2494 return Err(ConfigError::Missing(MissingKind::Field(
2495 "hostname".to_string(),
2496 )));
2497 }
2498 };
2499
2500 let key_opt = match self.key.as_ref() {
2501 None => None,
2502 Some(path) => {
2503 let key = Config::load_file(path)?;
2504 Some(key)
2505 }
2506 };
2507
2508 let certificate_opt = match self.certificate.as_ref() {
2509 None => None,
2510 Some(path) => {
2511 let certificate = Config::load_file(path)?;
2512 Some(certificate)
2513 }
2514 };
2515
2516 let certificate_chain = match self.certificate_chain.as_ref() {
2517 None => None,
2518 Some(path) => {
2519 let certificate_chain = Config::load_file(path)?;
2520 Some(split_certificate_chain(certificate_chain))
2521 }
2522 };
2523
2524 let path = match (self.path.as_ref(), self.path_type.as_ref()) {
2525 (None, _) => PathRule::prefix("".to_string()),
2526 (Some(s), Some(PathRuleType::Prefix)) => PathRule::prefix(s.to_string()),
2527 (Some(s), Some(PathRuleType::Regex)) => PathRule::regex(s.to_string()),
2528 (Some(s), Some(PathRuleType::Equals)) => PathRule::equals(s.to_string()),
2529 (Some(s), None) => PathRule::prefix(s.clone()),
2530 };
2531
2532 let redirect = match self.redirect.as_deref() {
2533 Some(v) => Some(parse_redirect_policy(v)?),
2534 None => None,
2535 };
2536 let redirect_scheme = match self.redirect_scheme.as_deref() {
2537 Some(v) => Some(parse_redirect_scheme(v)?),
2538 None => None,
2539 };
2540
2541 let headers = match self.headers.as_ref() {
2542 Some(entries) => {
2543 let mut out = Vec::with_capacity(entries.len());
2544 for (index, entry) in entries.iter().enumerate() {
2545 out.push(parse_header_edit(index, entry)?);
2546 }
2547 out
2548 }
2549 None => Vec::new(),
2550 };
2551
2552 let header_predicates = match self.header_predicates.as_ref() {
2559 Some(entries) => entries
2560 .iter()
2561 .enumerate()
2562 .map(|(index, entry)| parse_header_predicate(index, entry))
2563 .collect::<Result<Vec<_>, _>>()?,
2564 None => Vec::new(),
2565 };
2566
2567 let frontend_serves_https = key_opt.is_some() && certificate_opt.is_some();
2568 let hsts = match self.hsts.as_ref() {
2569 Some(h) => {
2570 if !frontend_serves_https {
2571 return Err(ConfigError::HstsOnPlainHttp(format!(
2572 "frontend {_cluster_id}/{hostname}"
2573 )));
2574 }
2575 Some(h.to_proto(&format!("frontend {_cluster_id}/{hostname}"))?)
2576 }
2577 None => None,
2578 };
2579
2580 Ok(HttpFrontendConfig {
2581 address: self.address,
2582 hostname,
2583 certificate: certificate_opt,
2584 key: key_opt,
2585 certificate_chain,
2586 tls_versions: self.tls_versions.clone(),
2587 position: self.position,
2588 path,
2589 method: self.method.clone(),
2590 tags: self.tags.clone(),
2591 redirect,
2592 redirect_scheme,
2593 redirect_template: self.redirect_template.clone(),
2594 rewrite_host: self.rewrite_host.clone(),
2595 rewrite_path: self.rewrite_path.clone(),
2596 rewrite_port: self.rewrite_port,
2597 required_auth: self.required_auth,
2598 headers,
2599 hsts,
2600 header_predicates,
2601 })
2602 }
2603}
2604
2605pub fn validate_sni_pattern(sni: &str) -> Result<String, ConfigError> {
2626 let invalid = || ConfigError::InvalidSniPattern {
2627 sni: sni.to_string(),
2628 };
2629
2630 if sni.is_empty() {
2631 return Err(invalid());
2632 }
2633
2634 if !sni.is_ascii() {
2635 return Err(ConfigError::NonAsciiSniPattern {
2636 sni: sni.to_string(),
2637 });
2638 }
2639
2640 let remainder = sni.strip_prefix("*.").unwrap_or(sni);
2644
2645 if remainder.is_empty() || remainder.contains('*') || remainder.contains('/') {
2652 return Err(invalid());
2653 }
2654 if remainder.split('.').any(|label| label.is_empty()) {
2655 return Err(invalid());
2656 }
2657
2658 let normalized = sni.to_ascii_lowercase();
2659 debug_assert!(
2665 normalized.matches('*').count() <= 1,
2666 "a validated SNI pattern must carry at most one wildcard marker"
2667 );
2668 debug_assert!(
2669 normalized.is_ascii(),
2670 "a validated SNI pattern must be pure ASCII"
2671 );
2672 Ok(normalized)
2673}
2674
2675pub(crate) fn parse_redirect_policy(value: &str) -> Result<RedirectPolicy, ConfigError> {
2677 match value.to_ascii_lowercase().as_str() {
2678 "forward" => Ok(RedirectPolicy::Forward),
2679 "permanent" => Ok(RedirectPolicy::Permanent),
2680 "unauthorized" => Ok(RedirectPolicy::Unauthorized),
2681 "silent_close" => Ok(RedirectPolicy::SilentClose),
2682 _ => Err(ConfigError::InvalidRedirectPolicy(value.to_owned())),
2683 }
2684}
2685
2686pub(crate) fn parse_redirect_scheme(value: &str) -> Result<RedirectScheme, ConfigError> {
2688 match value.to_ascii_lowercase().as_str() {
2689 "use-same" | "use_same" => Ok(RedirectScheme::UseSame),
2690 "use-http" | "use_http" => Ok(RedirectScheme::UseHttp),
2691 "use-https" | "use_https" => Ok(RedirectScheme::UseHttps),
2692 _ => Err(ConfigError::InvalidRedirectScheme(value.to_owned())),
2693 }
2694}
2695
2696pub(crate) fn parse_header_edit(
2703 index: usize,
2704 entry: &HeaderEditConfig,
2705) -> Result<Header, ConfigError> {
2706 let position = match entry.position.to_ascii_lowercase().as_str() {
2707 "request" => HeaderPosition::Request,
2708 "response" => HeaderPosition::Response,
2709 "both" => HeaderPosition::Both,
2710 _ => {
2711 return Err(ConfigError::InvalidHeaderPosition {
2712 index,
2713 position: entry.position.clone(),
2714 });
2715 }
2716 };
2717 if !header_name_is_valid_token(entry.key.as_bytes()) {
2718 return Err(ConfigError::InvalidHeaderBytes {
2719 index,
2720 field: "key",
2721 });
2722 }
2723 if header_value_contains_forbidden_controls(entry.value.as_bytes()) {
2724 return Err(ConfigError::InvalidHeaderBytes {
2725 index,
2726 field: "value",
2727 });
2728 }
2729 let header = Header {
2730 position: position as i32,
2731 key: entry.key.clone(),
2732 val: entry.value.clone(),
2733 };
2734 debug_assert!(
2740 header_name_is_valid_token(header.key.as_bytes()),
2741 "an emitted header key must be a valid token"
2742 );
2743 debug_assert!(
2744 !header_value_contains_forbidden_controls(header.val.as_bytes()),
2745 "an emitted header value must be free of forbidden control bytes"
2746 );
2747 Ok(header)
2748}
2749
2750pub(crate) fn parse_header_predicate(
2754 index: usize,
2755 entry: &HeaderPredicateConfig,
2756) -> Result<HeaderPredicate, ConfigError> {
2757 let kind = match entry.kind.to_ascii_lowercase().as_str() {
2758 "exact" => HeaderMatchKind::Exact,
2759 "regex" => HeaderMatchKind::Regex,
2760 "present" => HeaderMatchKind::Present,
2761 _ => {
2762 return Err(ConfigError::InvalidHeaderPredicate {
2763 index,
2764 reason: "match must be \"exact\", \"regex\" or \"present\"",
2765 });
2766 }
2767 };
2768 let predicate = HeaderPredicate {
2769 name: entry.name.to_owned(),
2770 kind: kind as i32,
2771 value: entry.value.to_owned(),
2772 };
2773 validate_header_predicate(&predicate)
2774 .map_err(|reason| ConfigError::InvalidHeaderPredicate { index, reason })?;
2775 Ok(predicate)
2776}
2777
2778pub fn validate_header_predicate(predicate: &HeaderPredicate) -> Result<(), &'static str> {
2795 let name = predicate.name.as_str();
2796 if !header_name_is_valid_token(name.as_bytes()) {
2797 return Err("name must be a non-empty RFC 9110 token (a header field name)");
2798 }
2799 if name.eq_ignore_ascii_case("host") {
2800 return Err(
2801 "name cannot be Host: the request authority is not a header Sōzu keeps \
2802 (H1 folds Host into it, H2 sends :authority); route on the hostname instead",
2803 );
2804 }
2805 if name.eq_ignore_ascii_case("cookie") {
2806 return Err("name cannot be Cookie: cookies are parsed into a jar, not kept as a header");
2807 }
2808 match (
2809 HeaderMatchKind::try_from(predicate.kind),
2810 predicate.value.as_deref(),
2811 ) {
2812 (Err(_), _) => Err("unknown match kind"),
2813 (Ok(HeaderMatchKind::Present), Some(_)) => Err("a present predicate takes no value"),
2814 (Ok(HeaderMatchKind::Present), None) => Ok(()),
2815 (Ok(HeaderMatchKind::Exact | HeaderMatchKind::Regex), None) => {
2816 Err("an exact or regex predicate requires a value")
2817 }
2818 (Ok(HeaderMatchKind::Exact), Some(value))
2819 if header_value_contains_forbidden_controls(value.as_bytes()) =>
2820 {
2821 Err("an exact value cannot carry a control character no header value can hold")
2822 }
2823 (Ok(HeaderMatchKind::Exact | HeaderMatchKind::Regex), Some(_)) => Ok(()),
2824 }
2825}
2826
2827pub(crate) fn header_name_is_valid_token(bytes: &[u8]) -> bool {
2835 if bytes.is_empty() {
2836 return false;
2837 }
2838 bytes.iter().all(|&b| is_tchar(b))
2839}
2840
2841fn is_tchar(b: u8) -> bool {
2844 b.is_ascii_alphanumeric()
2845 || matches!(
2846 b,
2847 b'!' | b'#'
2848 | b'$'
2849 | b'%'
2850 | b'&'
2851 | b'\''
2852 | b'*'
2853 | b'+'
2854 | b'-'
2855 | b'.'
2856 | b'^'
2857 | b'_'
2858 | b'`'
2859 | b'|'
2860 | b'~'
2861 )
2862}
2863
2864pub(crate) fn header_value_contains_forbidden_controls(bytes: &[u8]) -> bool {
2872 bytes
2873 .iter()
2874 .any(|&b| matches!(b, 0x00..=0x08 | 0x0A..=0x1F | 0x7F))
2875}
2876
2877#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2878#[serde(deny_unknown_fields, rename_all = "lowercase")]
2879pub enum ListenerProtocol {
2880 Http,
2881 Https,
2882 Tcp,
2883 Udp,
2884}
2885
2886#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2887#[serde(deny_unknown_fields, rename_all = "lowercase")]
2888pub enum FileClusterProtocolConfig {
2889 Http,
2890 Tcp,
2891}
2892
2893fn default_health_check_interval() -> u32 {
2894 10
2895}
2896fn default_health_check_timeout() -> u32 {
2897 5
2898}
2899fn default_health_check_threshold() -> u32 {
2900 3
2901}
2902fn default_health_check_uri() -> String {
2903 "/".to_owned()
2904}
2905
2906#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2907#[serde(deny_unknown_fields)]
2908pub struct FileHealthCheckConfig {
2909 #[serde(default = "default_health_check_uri")]
2912 pub uri: String,
2913 #[serde(default = "default_health_check_interval")]
2914 pub interval: u32,
2915 #[serde(default = "default_health_check_timeout")]
2916 pub timeout: u32,
2917 #[serde(default = "default_health_check_threshold")]
2918 pub healthy_threshold: u32,
2919 #[serde(default = "default_health_check_threshold")]
2920 pub unhealthy_threshold: u32,
2921 #[serde(default)]
2922 pub expected_status: u32,
2923 #[serde(default)]
2926 pub mode: Option<HealthCheckMode>,
2927 #[serde(default)]
2930 pub accepted_statuses: Vec<String>,
2931 #[serde(default)]
2934 pub port: Option<u32>,
2935 #[serde(default)]
2940 pub headers: Vec<FileHealthCheckHeader>,
2941}
2942
2943#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2948#[serde(deny_unknown_fields)]
2949pub struct FileHealthCheckHeader {
2950 pub name: String,
2951 pub value_file: String,
2952}
2953
2954impl std::fmt::Debug for FileHealthCheckHeader {
2955 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2956 f.debug_struct("FileHealthCheckHeader")
2957 .field("name_len", &self.name.len())
2958 .field("value_file", &"<redacted>")
2959 .finish_non_exhaustive()
2960 }
2961}
2962
2963impl FileHealthCheckConfig {
2964 pub fn to_proto(&self) -> Result<HealthCheckConfig, &'static str> {
2968 if !self.headers.is_empty() {
2972 return Err(
2973 "health check headers from the configuration file are not available yet: their values need the protected channel",
2974 );
2975 }
2976 self.to_parts().map(|(config, _)| config)
2977 }
2978
2979 pub fn to_parts(
2983 &self,
2984 ) -> Result<(HealthCheckConfig, Vec<FileHealthCheckHeader>), &'static str> {
2985 let accepted_statuses = self
2986 .accepted_statuses
2987 .iter()
2988 .map(|spec| parse_http_status_range(spec))
2989 .collect::<Result<Vec<_>, _>>()?;
2990 let proto = HealthCheckConfig {
2991 uri: self.uri.to_owned(),
2992 interval: self.interval,
2993 timeout: self.timeout,
2994 healthy_threshold: self.healthy_threshold,
2995 unhealthy_threshold: self.unhealthy_threshold,
2996 expected_status: self.expected_status,
2997 mode: self.mode.unwrap_or(HealthCheckMode::Http) as i32,
2998 accepted_statuses,
2999 port: self.port,
3000 headers: self
3001 .headers
3002 .iter()
3003 .map(|header| HealthCheckHeader {
3004 name: header.name.to_owned(),
3005 fingerprint: String::new(),
3006 })
3007 .collect(),
3008 };
3009 debug_assert_eq!(proto.uri, self.uri, "proto URI must mirror the file config");
3013 debug_assert!(
3014 proto.interval == self.interval
3015 && proto.timeout == self.timeout
3016 && proto.healthy_threshold == self.healthy_threshold
3017 && proto.unhealthy_threshold == self.unhealthy_threshold,
3018 "proto timing knobs must mirror the file config"
3019 );
3020 debug_assert_eq!(
3021 proto.port, self.port,
3022 "proto probe port must mirror the file config"
3023 );
3024 debug_assert_eq!(
3025 proto.accepted_statuses.len(),
3026 self.accepted_statuses.len(),
3027 "every accepted status spec must map to exactly one range"
3028 );
3029 debug_assert_eq!(
3030 proto.headers.len(),
3031 self.headers.len(),
3032 "every configured header must map to exactly one proto header"
3033 );
3034 validate_health_check_config(&proto)?;
3035 Ok((proto, self.headers.clone()))
3036 }
3037}
3038
3039pub const HTTP_STATUS_MIN: u32 = 100;
3042pub const HTTP_STATUS_MAX: u32 = 599;
3043
3044pub fn parse_http_status_range(spec: &str) -> Result<HttpStatusRange, &'static str> {
3053 const INVALID: &str = "health check accepted status must be a code (\"404\"), a range \
3054 (\"200-399\"), a class (\"2xx\") or \"any\"";
3055 let spec = spec.trim();
3056 let range = if spec.eq_ignore_ascii_case("any") {
3057 HttpStatusRange {
3058 start: HTTP_STATUS_MIN,
3059 end: HTTP_STATUS_MAX,
3060 }
3061 } else if let [class @ b'1'..=b'5', x1, x2] = spec.as_bytes()
3062 && x1.eq_ignore_ascii_case(&b'x')
3063 && x2.eq_ignore_ascii_case(&b'x')
3064 {
3065 let start = u32::from(class - b'0') * 100;
3066 HttpStatusRange {
3067 start,
3068 end: start + 99,
3069 }
3070 } else if let Some((start, end)) = spec.split_once('-') {
3071 HttpStatusRange {
3072 start: parse_status_code(start).ok_or(INVALID)?,
3073 end: parse_status_code(end).ok_or(INVALID)?,
3074 }
3075 } else {
3076 let code = parse_status_code(spec).ok_or(INVALID)?;
3077 HttpStatusRange {
3078 start: code,
3079 end: code,
3080 }
3081 };
3082 validate_http_status_range(&range)?;
3083 Ok(range)
3084}
3085
3086fn parse_status_code(code: &str) -> Option<u32> {
3089 let code = code.trim();
3090 if code.is_empty() || !code.bytes().all(|b| b.is_ascii_digit()) {
3091 return None;
3092 }
3093 code.parse().ok()
3094}
3095
3096fn validate_http_status_range(range: &HttpStatusRange) -> Result<(), &'static str> {
3097 if range.start < HTTP_STATUS_MIN || range.end > HTTP_STATUS_MAX {
3098 return Err("health check accepted statuses must lie within 100-599");
3099 }
3100 if range.start > range.end {
3101 return Err("health check accepted status range must not end before it starts");
3102 }
3103 debug_assert!(
3104 (HTTP_STATUS_MIN..=HTTP_STATUS_MAX).contains(&range.start)
3105 && (HTTP_STATUS_MIN..=HTTP_STATUS_MAX).contains(&range.end),
3106 "a validated range must lie within the valid status codes"
3107 );
3108 Ok(())
3109}
3110
3111pub fn validate_affinity_key(
3128 affinity_header: Option<&str>,
3129 affinity_cookie: Option<&str>,
3130) -> Result<(), &'static str> {
3131 match (affinity_header, affinity_cookie) {
3132 (Some(_), Some(_)) => {
3133 Err("affinity_header and affinity_cookie are mutually exclusive: set at most one")
3134 }
3135 (Some(name), None) if !header_name_is_valid_token(name.as_bytes()) => {
3136 Err("affinity_header must be a non-empty RFC 9110 token (a header field name)")
3137 }
3138 (Some(name), None) if name.eq_ignore_ascii_case("host") => Err(
3139 "affinity_header cannot be Host: the request authority is not a header Sōzu keeps \
3140 (H1 folds Host into it, H2 sends :authority)",
3141 ),
3142 (Some(name), None) if name.eq_ignore_ascii_case("cookie") => Err(
3143 "affinity_header cannot be Cookie: cookies are parsed into a jar; key on one with \
3144 affinity_cookie instead",
3145 ),
3146 (None, Some(name)) if !header_name_is_valid_token(name.as_bytes()) => {
3147 Err("affinity_cookie must be a non-empty RFC 6265 cookie name (an RFC 9110 token)")
3148 }
3149 _ => Ok(()),
3150 }
3151}
3152
3153pub const DEFAULT_SHARD_MIN_BACKENDS: u32 = 8;
3159
3160pub fn validate_shuffle_sharding(
3168 shard_percent: Option<u32>,
3169 shard_min_backends: Option<u32>,
3170 shard_mode: Option<i32>,
3171) -> Result<(), &'static str> {
3172 let Some(percent) = shard_percent else {
3173 if shard_min_backends.is_some() || shard_mode.is_some() {
3174 return Err("shard_min_backends and shard_mode require shard_percent");
3175 }
3176 return Ok(());
3177 };
3178 if !(1..=100).contains(&percent) {
3179 return Err("shard_percent must lie in 1..=100");
3180 }
3181 if shard_min_backends.is_some_and(|minimum| minimum < 2) {
3182 return Err("shard_min_backends must be at least 2, the smallest shard");
3183 }
3184 if shard_mode.is_some_and(|mode| ShardMode::try_from(mode).is_err()) {
3185 return Err("shard_mode must be FALLBACK or STRICT");
3186 }
3187 Ok(())
3188}
3189
3190pub const HEALTH_CHECK_MAX_HEADERS: usize = 32;
3192pub const HEALTH_CHECK_MAX_HEADER_BYTES: usize = 8192;
3195pub const HEALTH_CHECK_MAX_HPACK_BLOCK: usize = 16384;
3199pub const HEALTH_CHECK_MAX_AUTHORITY_LEN: usize = 1 + 39 + 11 + 1 + 6;
3202
3203const HEALTH_CHECK_FORBIDDEN_HEADERS: [&str; 10] = [
3205 "host",
3206 "connection",
3207 "keep-alive",
3208 "proxy-connection",
3209 "transfer-encoding",
3210 "content-length",
3211 "te",
3212 "trailer",
3213 "upgrade",
3214 "expect",
3215];
3216
3217fn hpack_string_length_bytes(length: usize) -> usize {
3219 if length < 127 {
3220 return 1;
3221 }
3222 let mut rest = length - 127;
3223 let mut bytes = 1;
3224 loop {
3225 bytes += 1;
3226 rest >>= 7;
3227 if rest == 0 {
3228 return bytes;
3229 }
3230 }
3231}
3232
3233pub fn health_check_hpack_upper_bound(cfg: &HealthCheckConfig, value_lens: &[usize]) -> usize {
3238 let mut bound = 2;
3240 bound += 1 + hpack_string_length_bytes(cfg.uri.len()) + cfg.uri.len();
3242 bound += 1
3244 + hpack_string_length_bytes(HEALTH_CHECK_MAX_AUTHORITY_LEN)
3245 + HEALTH_CHECK_MAX_AUTHORITY_LEN;
3246 for (header, value_len) in cfg.headers.iter().zip(value_lens) {
3247 bound += 1
3248 + hpack_string_length_bytes(header.name.len())
3249 + header.name.len()
3250 + hpack_string_length_bytes(*value_len)
3251 + value_len;
3252 }
3253 bound
3254}
3255
3256pub(crate) fn check_health_check_header_value(value: &str) -> Result<(), &'static str> {
3262 if header_value_contains_forbidden_controls(value.as_bytes()) {
3263 return Err("health check header value must not contain control bytes");
3264 }
3265 if value.starts_with([' ', '\t']) || value.ends_with([' ', '\t']) {
3270 return Err("health check header value must not begin or end with a space or a tab");
3271 }
3272 Ok(())
3273}
3274
3275fn validate_health_check_headers(
3279 cfg: &HealthCheckConfig,
3280 mode: HealthCheckMode,
3281) -> Result<(), &'static str> {
3282 if cfg.headers.is_empty() {
3283 return Ok(());
3284 }
3285 if mode == HealthCheckMode::Tcp {
3286 return Err("health check headers apply to HTTP mode only: leave them unset with mode TCP");
3287 }
3288 if cfg.headers.len() > HEALTH_CHECK_MAX_HEADERS {
3289 return Err("health check carries more than 32 headers");
3290 }
3291 for (index, header) in cfg.headers.iter().enumerate() {
3292 if header.name.starts_with(':')
3293 || HEALTH_CHECK_FORBIDDEN_HEADERS
3294 .iter()
3295 .any(|forbidden| header.name.eq_ignore_ascii_case(forbidden))
3296 {
3297 return Err("health check header name is reserved for the probe or its framing");
3298 }
3299 if !header_name_is_valid_token(header.name.as_bytes()) {
3300 return Err("health check header name must be a non-empty RFC 9110 token");
3301 }
3302 if cfg.headers[..index]
3303 .iter()
3304 .any(|earlier| earlier.name.eq_ignore_ascii_case(&header.name))
3305 {
3306 return Err("health check header names must be unique, compared case-insensitively");
3307 }
3308 if !header.fingerprint.is_empty()
3311 && header
3312 .fingerprint
3313 .parse::<crate::secret::HeaderFingerprint>()
3314 .is_err()
3315 {
3316 return Err("health check header fingerprint is malformed");
3317 }
3318 }
3319 Ok(())
3320}
3321
3322pub fn validate_health_check_header_values(
3328 cfg: &HealthCheckConfig,
3329 values: &[HealthCheckHeaderValue],
3330) -> Result<(), &'static str> {
3331 if values.len() != cfg.headers.len() {
3332 return Err("health check carries a header value count different from its header count");
3333 }
3334 let mut wire_bytes = 0usize;
3335 for (header, carrier) in cfg.headers.iter().zip(values) {
3336 if header.name != carrier.name {
3337 return Err(
3338 "health check header values must follow the order and names of the headers",
3339 );
3340 }
3341 let text = std::str::from_utf8(&carrier.value)
3342 .map_err(|_| "health check header value must be valid UTF-8")?;
3343 check_health_check_header_value(text)?;
3344 wire_bytes = wire_bytes
3345 .saturating_add(header.name.len())
3346 .saturating_add(carrier.value.len())
3347 .saturating_add(4);
3348 }
3349 if wire_bytes > HEALTH_CHECK_MAX_HEADER_BYTES {
3350 return Err("health check headers exceed 8192 bytes (name + value + 4 per header)");
3351 }
3352 let value_lens: Vec<usize> = values.iter().map(|carrier| carrier.value.len()).collect();
3353 if health_check_hpack_upper_bound(cfg, &value_lens) > HEALTH_CHECK_MAX_HPACK_BLOCK {
3354 return Err("health check URI and headers may exceed the 16384-byte HTTP/2 probe block");
3355 }
3356 Ok(())
3357}
3358
3359pub fn validate_health_check_config(cfg: &HealthCheckConfig) -> Result<(), &'static str> {
3375 let Ok(mode) = HealthCheckMode::try_from(cfg.mode) else {
3376 return Err("health check mode must be HTTP or TCP");
3377 };
3378 if cfg.interval == 0 {
3379 return Err("health check interval must be > 0");
3380 }
3381 if cfg.timeout == 0 {
3382 return Err("health check timeout must be > 0");
3383 }
3384 if cfg.healthy_threshold == 0 {
3385 return Err("health check healthy_threshold must be > 0");
3386 }
3387 if cfg.unhealthy_threshold == 0 {
3388 return Err("health check unhealthy_threshold must be > 0");
3389 }
3390 if mode == HealthCheckMode::Http && !cfg.uri.starts_with('/') {
3395 return Err("health check URI must start with '/'");
3396 }
3397 if cfg
3398 .uri
3399 .bytes()
3400 .any(|b| b == b'\r' || b == b'\n' || b == 0 || (b < 0x20 && b != b'\t'))
3401 {
3402 return Err("health check URI must not contain CR, LF, NUL, or other C0 control bytes");
3403 }
3404 for range in &cfg.accepted_statuses {
3405 validate_http_status_range(range)?;
3406 }
3407 if cfg.expected_status != 0 && !cfg.accepted_statuses.is_empty() {
3408 return Err(
3409 "health check expected_status and accepted_statuses are mutually exclusive: \
3410 set at most one",
3411 );
3412 }
3413 if mode == HealthCheckMode::Tcp
3416 && (cfg.expected_status != 0 || !cfg.accepted_statuses.is_empty())
3417 {
3418 return Err(
3419 "health check expected_status and accepted_statuses apply to HTTP mode only: \
3420 leave them unset with mode TCP",
3421 );
3422 }
3423 if cfg
3425 .port
3426 .is_some_and(|port| !(1..=u32::from(u16::MAX)).contains(&port))
3427 {
3428 return Err("health check port must lie in 1..=65535");
3429 }
3430 validate_health_check_headers(cfg, mode)?;
3431 debug_assert!(
3437 cfg.interval > 0
3438 && cfg.timeout > 0
3439 && cfg.healthy_threshold > 0
3440 && cfg.unhealthy_threshold > 0,
3441 "validated health-check thresholds must all be strictly positive"
3442 );
3443 debug_assert!(
3444 mode == HealthCheckMode::Tcp || cfg.uri.starts_with('/'),
3445 "a validated HTTP health-check URI must be an absolute path"
3446 );
3447 debug_assert!(
3448 cfg.expected_status == 0 || cfg.accepted_statuses.is_empty(),
3449 "a validated health check sets at most one of expected_status and accepted_statuses"
3450 );
3451 debug_assert!(
3452 mode == HealthCheckMode::Http
3453 || (cfg.expected_status == 0 && cfg.accepted_statuses.is_empty()),
3454 "a validated TCP health check sets no status field"
3455 );
3456 debug_assert!(
3457 cfg.port
3458 .is_none_or(|port| port != 0 && u16::try_from(port).is_ok()),
3459 "a validated health-check probe port is a non-zero 16-bit port"
3460 );
3461 Ok(())
3462}
3463
3464#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3465#[serde(deny_unknown_fields)]
3466pub struct FileClusterConfig {
3467 pub frontends: Vec<FileClusterFrontendConfig>,
3468 pub backends: Vec<FileBackendConfig>,
3469 pub protocol: FileClusterProtocolConfig,
3470 pub sticky_session: Option<bool>,
3471 pub https_redirect: Option<bool>,
3472 #[serde(default)]
3473 pub send_proxy: Option<bool>,
3474 #[serde(default)]
3475 pub load_balancing: LoadBalancingAlgorithms,
3476 pub answer_503: Option<String>,
3477 #[serde(default)]
3478 pub load_metric: Option<LoadMetric>,
3479 pub http2: Option<bool>,
3482 pub answers: Option<BTreeMap<String, String>>,
3493 pub https_redirect_port: Option<u32>,
3498 pub authorized_hashes: Option<Vec<String>>,
3503 pub www_authenticate: Option<String>,
3507 pub max_connections_per_ip: Option<u64>,
3516 pub max_connections_per_subnet: Option<u64>,
3524 pub retry_after: Option<u32>,
3530 #[serde(default)]
3534 pub health_check: Option<FileHealthCheckConfig>,
3535 #[serde(default)]
3539 pub udp: Option<FileUdpClusterConfig>,
3540 #[serde(default)]
3546 pub affinity_header: Option<String>,
3547 #[serde(default)]
3549 pub affinity_cookie: Option<String>,
3550 #[serde(default)]
3555 pub shard_percent: Option<u32>,
3556 #[serde(default)]
3559 pub shard_min_backends: Option<u32>,
3560 #[serde(default)]
3563 pub shard_mode: Option<ShardMode>,
3564 #[serde(default)]
3569 pub max_connection_attempts: Option<u32>,
3570}
3571
3572#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3575#[serde(deny_unknown_fields)]
3576pub struct FileUdpHealthConfig {
3577 pub mode: Option<UdpHealthMode>,
3581 pub tcp_port: Option<u32>,
3582 pub rise: Option<u32>,
3583 pub fall: Option<u32>,
3584 pub fail_open: Option<bool>,
3585 pub udp_probe_payload: Option<String>,
3587 pub probe_interval_seconds: Option<u32>,
3588 pub probe_timeout_seconds: Option<u32>,
3589}
3590
3591impl FileUdpHealthConfig {
3592 pub fn to_proto(&self) -> UdpHealthConfig {
3593 UdpHealthConfig {
3594 mode: self.mode.map(|m| m as i32),
3595 tcp_port: self.tcp_port,
3596 rise: self.rise,
3597 fall: self.fall,
3598 fail_open: self.fail_open,
3599 udp_probe_payload: self
3600 .udp_probe_payload
3601 .as_ref()
3602 .map(|p| p.as_bytes().to_owned()),
3603 probe_interval_seconds: self.probe_interval_seconds,
3604 probe_timeout_seconds: self.probe_timeout_seconds,
3605 }
3606 }
3607}
3608
3609#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3611#[serde(deny_unknown_fields)]
3612pub struct FileUdpClusterConfig {
3613 pub affinity_key: Option<UdpAffinityKey>,
3616 pub responses: Option<u32>,
3618 pub requests: Option<u32>,
3620 pub send_proxy_protocol: Option<bool>,
3622 pub proxy_protocol_every_datagram: Option<bool>,
3624 pub health: Option<FileUdpHealthConfig>,
3626}
3627
3628impl FileUdpClusterConfig {
3629 pub fn to_proto(&self) -> UdpClusterConfig {
3630 UdpClusterConfig {
3631 affinity_key: self.affinity_key.map(|k| k as i32),
3632 responses: self.responses,
3633 requests: self.requests,
3634 send_proxy_protocol: self.send_proxy_protocol,
3635 proxy_protocol_every_datagram: self.proxy_protocol_every_datagram,
3636 health: self.health.as_ref().map(|h| h.to_proto()),
3637 }
3638 }
3639}
3640
3641#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3642#[serde(deny_unknown_fields)]
3643pub struct BackendConfig {
3644 pub address: SocketAddr,
3645 pub weight: Option<u8>,
3646 pub sticky_id: Option<String>,
3647 pub backup: Option<bool>,
3648 pub backend_id: Option<String>,
3649 pub unix_path: Option<String>,
3653}
3654
3655fn validate_backend_unix_paths(
3659 cluster_id: &str,
3660 backends: &[BackendConfig],
3661) -> Result<(), ConfigError> {
3662 for (index, backend) in backends.iter().enumerate() {
3663 let Some(unix_path) = backend.unix_path.as_deref() else {
3664 continue;
3665 };
3666 let invalid = |reason| ConfigError::InvalidBackendUnixPath {
3667 cluster_id: cluster_id.to_owned(),
3668 address: backend.address,
3669 reason,
3670 };
3671 parse_backend_unix_path(unix_path).map_err(invalid)?;
3672 if backends
3673 .iter()
3674 .enumerate()
3675 .any(|(other, b)| other != index && b.address == backend.address)
3676 {
3677 return Err(invalid(
3678 "a unix backend's address is its identity and must be unique in the cluster",
3679 ));
3680 }
3681 }
3682 Ok(())
3683}
3684
3685pub type BackendResolver<'a> = dyn Fn(&str) -> std::io::Result<Vec<SocketAddr>> + 'a;
3687
3688pub fn system_resolver(address: &str) -> std::io::Result<Vec<SocketAddr>> {
3690 address.to_socket_addrs().map(Iterator::collect)
3691}
3692
3693#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3699#[serde(deny_unknown_fields)]
3700pub struct FileBackendConfig {
3701 pub address: String,
3702 pub weight: Option<u8>,
3703 pub sticky_id: Option<String>,
3704 pub backup: Option<bool>,
3705 pub backend_id: Option<String>,
3706 pub unix_path: Option<String>,
3709}
3710
3711fn is_legacy_numeric_host(host: &str) -> bool {
3716 host.split('.').all(|component| {
3717 !component.is_empty()
3718 && match component
3719 .strip_prefix("0x")
3720 .or_else(|| component.strip_prefix("0X"))
3721 {
3722 Some(hex) => hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
3723 None => component.bytes().all(|byte| byte.is_ascii_digit()),
3724 }
3725 })
3726}
3727
3728impl FileBackendConfig {
3729 pub fn resolve(
3753 &self,
3754 cluster_id: &str,
3755 index: usize,
3756 resolver: &BackendResolver<'_>,
3757 ) -> Result<Vec<BackendConfig>, ConfigError> {
3758 if let Ok(address) = self.address.parse::<SocketAddr>() {
3759 return Ok(vec![BackendConfig {
3760 address,
3761 weight: self.weight,
3762 sticky_id: self.sticky_id.clone(),
3763 backup: self.backup,
3764 backend_id: Some(
3765 self.backend_id
3766 .to_owned()
3767 .unwrap_or_else(|| format!("{cluster_id}-{index}-{address}")),
3768 ),
3769 unix_path: self.unix_path.clone(),
3770 }]);
3771 }
3772
3773 let error = |reason: String| ConfigError::BackendAddressResolution {
3774 cluster_id: cluster_id.to_owned(),
3775 address: self.address.clone(),
3776 reason,
3777 };
3778 if let Some((host, _port)) = self.address.rsplit_once(':') {
3779 if host.contains(':') {
3780 return Err(error(format!(
3781 "{host:?} is an unbracketed IPv6 literal, IPv6 literals must use [address]:port"
3782 )));
3783 }
3784 if !host.is_empty() && is_legacy_numeric_host(host) {
3785 return Err(error(format!(
3786 "{host:?} is not a valid IP address literal and numeric shorthand is not accepted"
3787 )));
3788 }
3789 }
3790 let addresses = resolver(&self.address).map_err(|io_error| error(io_error.to_string()))?;
3791 self.expand(cluster_id, addresses)
3792 }
3793
3794 fn expand(
3798 &self,
3799 cluster_id: &str,
3800 mut addresses: Vec<SocketAddr>,
3801 ) -> Result<Vec<BackendConfig>, ConfigError> {
3802 addresses.sort_unstable();
3803 addresses.dedup();
3804 if addresses.is_empty() {
3805 return Err(ConfigError::BackendAddressResolution {
3806 cluster_id: cluster_id.to_owned(),
3807 address: self.address.clone(),
3808 reason: "the name resolved to no address".to_owned(),
3809 });
3810 }
3811
3812 let backends: Vec<BackendConfig> = addresses
3813 .iter()
3814 .map(|address| BackendConfig {
3815 address: *address,
3816 weight: self.weight,
3817 sticky_id: self
3818 .sticky_id
3819 .as_ref()
3820 .map(|sticky_id| format!("{sticky_id}-{address}")),
3821 backup: self.backup,
3822 backend_id: Some(match &self.backend_id {
3823 Some(backend_id) => format!("{backend_id}-{address}"),
3824 None => format!("{cluster_id}-{}-{address}", self.address),
3825 }),
3826 unix_path: self.unix_path.clone(),
3827 })
3828 .collect();
3829
3830 debug_assert_eq!(backends.len(), addresses.len());
3835 debug_assert!(
3836 backends
3837 .iter()
3838 .map(|backend| backend.backend_id.as_deref())
3839 .collect::<HashSet<_>>()
3840 .len()
3841 == backends.len(),
3842 "resolved backends must carry distinct ids"
3843 );
3844 Ok(backends)
3845 }
3846}
3847
3848impl FileClusterConfig {
3849 pub fn to_cluster_config(
3850 self,
3851 cluster_id: &str,
3852 expect_proxy: &HashSet<SocketAddr>,
3853 ) -> Result<ClusterConfig, ConfigError> {
3854 self.to_cluster_config_with(cluster_id, expect_proxy, Some(&system_resolver))
3855 }
3856
3857 pub fn to_cluster_config_with(
3862 self,
3863 cluster_id: &str,
3864 expect_proxy: &HashSet<SocketAddr>,
3865 resolver: Option<&BackendResolver<'_>>,
3866 ) -> Result<ClusterConfig, ConfigError> {
3867 let requested_frontend_count = self.frontends.len();
3870 validate_affinity_key(
3871 self.affinity_header.as_deref(),
3872 self.affinity_cookie.as_deref(),
3873 )
3874 .map_err(|reason| ConfigError::InvalidAffinityKey {
3875 cluster_id: cluster_id.to_owned(),
3876 reason,
3877 })?;
3878 validate_shuffle_sharding(
3879 self.shard_percent,
3880 self.shard_min_backends,
3881 self.shard_mode.map(|mode| mode as i32),
3882 )
3883 .map_err(|reason| ConfigError::InvalidShuffleSharding {
3884 cluster_id: cluster_id.to_owned(),
3885 reason,
3886 })?;
3887 if let Some(attempts) = self.max_connection_attempts {
3888 validate_max_connection_attempts(attempts).map_err(|reason| {
3889 ConfigError::InvalidMaxConnectionAttempts {
3890 cluster_id: Some(cluster_id.to_owned()),
3891 value: attempts,
3892 reason,
3893 }
3894 })?;
3895 }
3896 let mut backends = Vec::with_capacity(self.backends.len());
3897 for (index, backend) in self.backends.iter().enumerate() {
3898 match resolver {
3899 Some(resolver) => backends.extend(backend.resolve(cluster_id, index, resolver)?),
3900 None if backend.address.parse::<SocketAddr>().is_ok() => {
3901 backends.extend(backend.resolve(cluster_id, index, &system_resolver)?)
3902 }
3903 None => {}
3904 }
3905 }
3906 debug_assert!(resolver.is_none() || backends.len() >= self.backends.len());
3908 let (health_check, health_check_header_files) = match self
3909 .health_check
3910 .as_ref()
3911 .map(FileHealthCheckConfig::to_parts)
3912 .transpose()
3913 .map_err(|reason| ConfigError::InvalidHealthCheck {
3914 cluster_id: cluster_id.to_owned(),
3915 reason,
3916 })? {
3917 Some((config, files)) => (Some(config), files),
3918 None => (None, Vec::new()),
3919 };
3920 validate_backend_unix_paths(cluster_id, &backends)?;
3921 if health_check.as_ref().is_some_and(|hc| hc.port.is_some())
3924 && self.backends.iter().any(|b| b.unix_path.is_some())
3925 {
3926 return Err(ConfigError::InvalidHealthCheck {
3927 cluster_id: cluster_id.to_owned(),
3928 reason: HEALTH_CHECK_PORT_UNIX_REFUSED,
3929 });
3930 }
3931 match self.protocol {
3932 FileClusterProtocolConfig::Tcp => {
3933 if self.affinity_header.is_some() || self.affinity_cookie.is_some() {
3937 return Err(ConfigError::InvalidAffinityKey {
3938 cluster_id: cluster_id.to_owned(),
3939 reason: "a TCP cluster always keys on the client source IP: \
3940 affinity_header and affinity_cookie apply to HTTP clusters only",
3941 });
3942 }
3943 let mut has_expect_proxy = None;
3944 let mut frontends = Vec::new();
3945 for f in self.frontends {
3946 if expect_proxy.contains(&f.address) {
3947 match has_expect_proxy {
3948 Some(true) => {}
3949 Some(false) => {
3950 return Err(ConfigError::Incompatible {
3951 object: ObjectKind::Cluster,
3952 id: cluster_id.to_owned(),
3953 kind: IncompatibilityKind::ProxyProtocol,
3954 });
3955 }
3956 None => has_expect_proxy = Some(true),
3957 }
3958 } else {
3959 match has_expect_proxy {
3960 Some(false) => {}
3961 Some(true) => {
3962 return Err(ConfigError::Incompatible {
3963 object: ObjectKind::Cluster,
3964 id: cluster_id.to_owned(),
3965 kind: IncompatibilityKind::ProxyProtocol,
3966 });
3967 }
3968 None => has_expect_proxy = Some(false),
3969 }
3970 }
3971 let tcp_frontend = f.to_tcp_front()?;
3972 frontends.push(tcp_frontend);
3973 }
3974
3975 let send_proxy = self.send_proxy.unwrap_or(false);
3976 let expect_proxy = has_expect_proxy.unwrap_or(false);
3977 let proxy_protocol = match (send_proxy, expect_proxy) {
3978 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
3979 (true, false) => Some(ProxyProtocolConfig::SendHeader),
3980 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
3981 _ => None,
3982 };
3983
3984 let answers = match self.answers.as_ref() {
3985 Some(map) => load_answers(map)?,
3986 None => BTreeMap::new(),
3987 };
3988
3989 let udp = self.udp.as_ref().map(|u| u.to_proto());
3990 debug_assert_eq!(
3996 frontends.len(),
3997 requested_frontend_count,
3998 "every TCP frontend must survive conversion"
3999 );
4000 debug_assert_eq!(
4001 proxy_protocol,
4002 match (send_proxy, expect_proxy) {
4003 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
4004 (true, false) => Some(ProxyProtocolConfig::SendHeader),
4005 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
4006 (false, false) => None,
4007 },
4008 "proxy_protocol must be the (send, expect) function"
4009 );
4010
4011 Ok(ClusterConfig::Tcp(TcpClusterConfig {
4012 cluster_id: cluster_id.to_string(),
4013 frontends,
4014 backends,
4015 proxy_protocol,
4016 load_balancing: self.load_balancing,
4017 load_metric: self.load_metric,
4018 answers,
4019 https_redirect_port: self.https_redirect_port,
4020 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
4021 www_authenticate: self.www_authenticate,
4022 max_connections_per_ip: self.max_connections_per_ip,
4023 max_connections_per_subnet: self.max_connections_per_subnet,
4024 retry_after: self.retry_after,
4025 health_check,
4026 health_check_header_files,
4027 udp,
4028 shard_percent: self.shard_percent,
4029 shard_min_backends: self.shard_min_backends,
4030 shard_mode: self.shard_mode,
4031 max_connection_attempts: self.max_connection_attempts,
4032 }))
4033 }
4034 FileClusterProtocolConfig::Http => {
4035 let mut frontends = Vec::new();
4036 for frontend in self.frontends {
4037 let http_frontend = frontend.to_http_front(cluster_id)?;
4038 frontends.push(http_frontend);
4039 }
4040
4041 let answer_503 = self.answer_503.as_ref().and_then(|path| {
4042 Config::load_file(path)
4043 .map_err(|e| {
4044 error!("cannot load 503 error page at path '{}': {:?}", path, e);
4045 e
4046 })
4047 .ok()
4048 });
4049
4050 let answers = match self.answers.as_ref() {
4051 Some(map) => load_answers(map)?,
4052 None => BTreeMap::new(),
4053 };
4054
4055 let udp = self.udp.as_ref().map(|u| u.to_proto());
4056 debug_assert_eq!(
4059 frontends.len(),
4060 requested_frontend_count,
4061 "every HTTP frontend must survive conversion"
4062 );
4063
4064 Ok(ClusterConfig::Http(HttpClusterConfig {
4065 cluster_id: cluster_id.to_string(),
4066 frontends,
4067 backends,
4068 sticky_session: self.sticky_session.unwrap_or(false),
4069 https_redirect: self.https_redirect.unwrap_or(false),
4070 load_balancing: self.load_balancing,
4071 load_metric: self.load_metric,
4072 answer_503,
4073 http2: self.http2,
4074 answers,
4075 https_redirect_port: self.https_redirect_port,
4076 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
4077 www_authenticate: self.www_authenticate,
4078 max_connections_per_ip: self.max_connections_per_ip,
4079 max_connections_per_subnet: self.max_connections_per_subnet,
4080 retry_after: self.retry_after,
4081 health_check,
4082 health_check_header_files,
4083 udp,
4084 affinity_header: self.affinity_header,
4085 affinity_cookie: self.affinity_cookie,
4086 shard_percent: self.shard_percent,
4087 shard_min_backends: self.shard_min_backends,
4088 shard_mode: self.shard_mode,
4089 max_connection_attempts: self.max_connection_attempts,
4090 }))
4091 }
4092 }
4093 }
4094}
4095
4096#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4097#[serde(deny_unknown_fields)]
4098pub struct HttpFrontendConfig {
4099 pub address: SocketAddr,
4100 pub hostname: String,
4101 pub path: PathRule,
4102 pub method: Option<String>,
4103 pub certificate: Option<String>,
4104 pub key: Option<String>,
4105 pub certificate_chain: Option<Vec<String>>,
4106 #[serde(default)]
4107 pub tls_versions: Vec<TlsVersion>,
4108 #[serde(default)]
4109 pub position: RulePosition,
4110 pub tags: Option<BTreeMap<String, String>>,
4111 #[serde(default)]
4113 pub redirect: Option<RedirectPolicy>,
4114 #[serde(default)]
4116 pub redirect_scheme: Option<RedirectScheme>,
4117 #[serde(default)]
4118 pub redirect_template: Option<String>,
4119 #[serde(default)]
4120 pub rewrite_host: Option<String>,
4121 #[serde(default)]
4122 pub rewrite_path: Option<String>,
4123 #[serde(default)]
4124 pub rewrite_port: Option<u32>,
4125 #[serde(default)]
4126 pub required_auth: Option<bool>,
4127 #[serde(default)]
4130 pub headers: Vec<Header>,
4131 #[serde(default)]
4134 pub hsts: Option<HstsConfig>,
4135 #[serde(default)]
4138 pub header_predicates: Vec<HeaderPredicate>,
4139}
4140
4141impl fmt::Debug for HttpFrontendConfig {
4142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4143 let certificate = self.certificate.as_ref().map(|_| "[redacted]");
4144 let certificate_len = self.certificate.as_ref().map(String::len);
4145 let key = self.key.as_ref().map(|_| "[redacted]");
4146 let key_len = self.key.as_ref().map(String::len);
4147 let certificate_chain = self.certificate_chain.as_ref().map(|_| "[redacted]");
4148 let certificate_chain_count = self.certificate_chain.as_ref().map(Vec::len);
4149 let certificate_chain_len = self.certificate_chain.as_ref().map(|chain| {
4150 chain
4151 .iter()
4152 .map(String::len)
4153 .fold(0usize, usize::saturating_add)
4154 });
4155 let method_len = self.method.as_ref().map(String::len);
4156 let redirect_template_len = self.redirect_template.as_ref().map(String::len);
4157 let rewrite_host_len = self.rewrite_host.as_ref().map(String::len);
4158 let rewrite_path_len = self.rewrite_path.as_ref().map(String::len);
4159
4160 f.debug_struct("HttpFrontendConfig")
4161 .field("address", &self.address)
4162 .field("hostname_len", &self.hostname.len())
4163 .field("path_kind", &self.path.kind)
4164 .field("path_len", &self.path.value.len())
4165 .field("method_len", &method_len)
4166 .field("certificate", &certificate)
4167 .field("certificate_len", &certificate_len)
4168 .field("key", &key)
4169 .field("key_len", &key_len)
4170 .field("certificate_chain", &certificate_chain)
4171 .field("certificate_chain_count", &certificate_chain_count)
4172 .field("certificate_chain_len", &certificate_chain_len)
4173 .field("tls_versions_count", &self.tls_versions.len())
4174 .field("position", &self.position)
4175 .field(
4176 "tags_count",
4177 &self.tags.as_ref().map(BTreeMap::len).unwrap_or_default(),
4178 )
4179 .field("redirect", &self.redirect)
4180 .field("redirect_scheme", &self.redirect_scheme)
4181 .field("redirect_template_len", &redirect_template_len)
4182 .field("rewrite_host_len", &rewrite_host_len)
4183 .field("rewrite_path_len", &rewrite_path_len)
4184 .field("rewrite_port", &self.rewrite_port)
4185 .field("required_auth", &self.required_auth)
4186 .field("headers_count", &self.headers.len())
4187 .field("hsts", &self.hsts)
4188 .field("header_predicates_count", &self.header_predicates.len())
4189 .finish()
4190 }
4191}
4192
4193impl HttpFrontendConfig {
4194 pub fn generate_requests(&self, cluster_id: &str) -> Vec<Request> {
4195 let mut v = Vec::new();
4196
4197 let tags = self.tags.clone().unwrap_or_default();
4198
4199 if self.key.is_some() && self.certificate.is_some() {
4200 v.push(
4201 RequestType::AddCertificate(AddCertificate {
4202 address: self.address.into(),
4203 certificate: CertificateAndKey {
4204 key: self.key.clone().unwrap(),
4205 certificate: self.certificate.clone().unwrap(),
4206 certificate_chain: self.certificate_chain.clone().unwrap_or_default(),
4207 versions: self.tls_versions.iter().map(|v| *v as i32).collect(),
4208 names: vec![],
4213 },
4214 expired_at: None,
4215 })
4216 .into(),
4217 );
4218
4219 v.push(
4220 RequestType::add_https_frontend(RequestHttpFrontend {
4221 cluster_id: Some(cluster_id.to_string()),
4222 address: self.address.into(),
4223 hostname: self.hostname.clone(),
4224 path: self.path.clone(),
4225 method: self.method.clone(),
4226 position: self.position.into(),
4227 tags,
4228 redirect: self.redirect.map(|r| r as i32),
4229 required_auth: self.required_auth,
4230 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
4231 redirect_template: self.redirect_template.clone(),
4232 rewrite_host: self.rewrite_host.clone(),
4233 rewrite_path: self.rewrite_path.clone(),
4234 rewrite_port: self.rewrite_port,
4235 headers: self.headers.clone(),
4236 hsts: self.hsts,
4237 header_predicates: self.header_predicates.clone(),
4238 })
4239 .into(),
4240 );
4241 } else {
4242 v.push(
4244 RequestType::add_http_frontend(RequestHttpFrontend {
4245 cluster_id: Some(cluster_id.to_string()),
4246 address: self.address.into(),
4247 hostname: self.hostname.clone(),
4248 path: self.path.clone(),
4249 method: self.method.clone(),
4250 position: self.position.into(),
4251 tags,
4252 redirect: self.redirect.map(|r| r as i32),
4253 required_auth: self.required_auth,
4254 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
4255 redirect_template: self.redirect_template.clone(),
4256 rewrite_host: self.rewrite_host.clone(),
4257 rewrite_path: self.rewrite_path.clone(),
4258 rewrite_port: self.rewrite_port,
4259 headers: self.headers.clone(),
4260 hsts: self.hsts,
4261 header_predicates: self.header_predicates.clone(),
4262 })
4263 .into(),
4264 );
4265 }
4266
4267 v
4268 }
4269}
4270
4271#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4272#[serde(deny_unknown_fields)]
4273pub struct HttpClusterConfig {
4274 pub cluster_id: String,
4275 pub frontends: Vec<HttpFrontendConfig>,
4276 pub backends: Vec<BackendConfig>,
4277 pub sticky_session: bool,
4278 pub https_redirect: bool,
4279 pub load_balancing: LoadBalancingAlgorithms,
4280 pub load_metric: Option<LoadMetric>,
4281 pub answer_503: Option<String>,
4282 pub http2: Option<bool>,
4283 #[serde(default)]
4286 pub answers: BTreeMap<String, String>,
4287 #[serde(default)]
4288 pub https_redirect_port: Option<u32>,
4289 #[serde(default)]
4290 pub authorized_hashes: Vec<String>,
4291 #[serde(default)]
4292 pub www_authenticate: Option<String>,
4293 #[serde(default)]
4296 pub max_connections_per_ip: Option<u64>,
4297 #[serde(default)]
4302 pub max_connections_per_subnet: Option<u64>,
4303 #[serde(default)]
4306 pub retry_after: Option<u32>,
4307 #[serde(default)]
4311 pub health_check: Option<HealthCheckConfig>,
4312 #[serde(default)]
4316 pub health_check_header_files: Vec<FileHealthCheckHeader>,
4317 #[serde(default)]
4320 pub udp: Option<UdpClusterConfig>,
4321 #[serde(default)]
4324 pub affinity_header: Option<String>,
4325 #[serde(default)]
4328 pub affinity_cookie: Option<String>,
4329 #[serde(default)]
4332 pub shard_percent: Option<u32>,
4333 #[serde(default)]
4335 pub shard_min_backends: Option<u32>,
4336 #[serde(default)]
4338 pub shard_mode: Option<ShardMode>,
4339 #[serde(default)]
4341 pub max_connection_attempts: Option<u32>,
4342}
4343
4344impl HttpClusterConfig {
4345 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
4346 if !self.health_check_header_files.is_empty() {
4350 return Err(ConfigError::InvalidHealthCheck {
4351 cluster_id: self.cluster_id.clone(),
4352 reason: HEALTH_CHECK_VALUE_FILE_UNSUPPORTED,
4353 });
4354 }
4355 let mut v: Vec<Request> = vec![
4356 RequestType::AddCluster(Cluster {
4357 cluster_id: self.cluster_id.clone(),
4358 sticky_session: self.sticky_session,
4359 https_redirect: self.https_redirect,
4360 proxy_protocol: None,
4361 load_balancing: self.load_balancing as i32,
4362 answer_503: self.answer_503.clone(),
4363 load_metric: self.load_metric.map(|s| s as i32),
4364 http2: self.http2,
4365 answers: self.answers.clone(),
4366 https_redirect_port: self.https_redirect_port,
4367 authorized_hashes: self.authorized_hashes.clone(),
4368 www_authenticate: self.www_authenticate.clone(),
4369 max_connections_per_ip: self.max_connections_per_ip,
4370 max_connections_per_subnet: self.max_connections_per_subnet,
4371 retry_after: self.retry_after,
4372 health_check: self.health_check.clone(),
4373 udp: self.udp.clone(),
4374 affinity_header: self.affinity_header.clone(),
4375 affinity_cookie: self.affinity_cookie.clone(),
4376 shard_percent: self.shard_percent,
4377 shard_min_backends: self.shard_min_backends,
4378 shard_mode: self.shard_mode.map(|mode| mode as i32),
4379 max_connection_attempts: self.max_connection_attempts,
4380 })
4381 .into(),
4382 ];
4383
4384 for frontend in &self.frontends {
4385 let mut orders = frontend.generate_requests(&self.cluster_id);
4386 v.append(&mut orders);
4387 }
4388
4389 for (backend_count, backend) in self.backends.iter().enumerate() {
4390 let load_balancing_parameters = Some(LoadBalancingParams {
4391 weight: backend.weight.unwrap_or(100) as i32,
4392 });
4393
4394 v.push(
4395 RequestType::add_backend(AddBackend {
4396 cluster_id: self.cluster_id.clone(),
4397 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
4398 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
4399 }),
4400 address: backend.address.into(),
4401 load_balancing_parameters,
4402 sticky_id: backend.sticky_id.clone(),
4403 backup: backend.backup,
4404 unix_path: backend.unix_path.clone(),
4405 })
4406 .into(),
4407 );
4408 }
4409
4410 debug_assert!(
4415 matches!(
4416 v.first().and_then(|r| r.request_type.as_ref()),
4417 Some(RequestType::AddCluster(_))
4418 ),
4419 "HTTP cluster orders must lead with an AddCluster"
4420 );
4421 debug_assert_eq!(
4422 v.iter()
4423 .filter(|r| matches!(
4424 r.request_type,
4425 Some(RequestType::AddBackend(_)) | Some(RequestType::AddUnixBackend(_))
4426 ))
4427 .count(),
4428 self.backends.len(),
4429 "one AddBackend order per configured backend"
4430 );
4431 Ok(v)
4432 }
4433}
4434
4435#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4436pub struct TcpFrontendConfig {
4437 pub address: SocketAddr,
4438 pub tags: Option<BTreeMap<String, String>>,
4439 #[serde(default)]
4446 pub udp: bool,
4447 #[serde(default)]
4451 pub sni: Option<String>,
4452 #[serde(default)]
4455 pub alpn: Vec<String>,
4456}
4457
4458#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4459pub struct TcpClusterConfig {
4460 pub cluster_id: String,
4461 pub frontends: Vec<TcpFrontendConfig>,
4462 pub backends: Vec<BackendConfig>,
4463 #[serde(default)]
4464 pub proxy_protocol: Option<ProxyProtocolConfig>,
4465 pub load_balancing: LoadBalancingAlgorithms,
4466 pub load_metric: Option<LoadMetric>,
4467 #[serde(default)]
4471 pub answers: BTreeMap<String, String>,
4472 #[serde(default)]
4473 pub https_redirect_port: Option<u32>,
4474 #[serde(default)]
4475 pub authorized_hashes: Vec<String>,
4476 #[serde(default)]
4477 pub www_authenticate: Option<String>,
4478 #[serde(default)]
4481 pub max_connections_per_ip: Option<u64>,
4482 #[serde(default)]
4487 pub max_connections_per_subnet: Option<u64>,
4488 #[serde(default)]
4492 pub retry_after: Option<u32>,
4493 #[serde(default)]
4497 pub health_check: Option<HealthCheckConfig>,
4498 #[serde(default)]
4502 pub health_check_header_files: Vec<FileHealthCheckHeader>,
4503 #[serde(default)]
4506 pub udp: Option<UdpClusterConfig>,
4507 #[serde(default)]
4510 pub shard_percent: Option<u32>,
4511 #[serde(default)]
4513 pub shard_min_backends: Option<u32>,
4514 #[serde(default)]
4516 pub shard_mode: Option<ShardMode>,
4517 #[serde(default)]
4519 pub max_connection_attempts: Option<u32>,
4520}
4521
4522impl TcpClusterConfig {
4523 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
4524 if !self.health_check_header_files.is_empty() {
4528 return Err(ConfigError::InvalidHealthCheck {
4529 cluster_id: self.cluster_id.clone(),
4530 reason: HEALTH_CHECK_VALUE_FILE_UNSUPPORTED,
4531 });
4532 }
4533 let mut v: Vec<Request> = vec![
4534 RequestType::AddCluster(Cluster {
4535 cluster_id: self.cluster_id.clone(),
4536 sticky_session: false,
4537 https_redirect: false,
4538 proxy_protocol: self.proxy_protocol.map(|s| s as i32),
4539 load_balancing: self.load_balancing as i32,
4540 load_metric: self.load_metric.map(|s| s as i32),
4541 answer_503: None,
4542 http2: None,
4543 answers: self.answers.clone(),
4544 https_redirect_port: self.https_redirect_port,
4545 authorized_hashes: self.authorized_hashes.clone(),
4546 www_authenticate: self.www_authenticate.clone(),
4547 max_connections_per_ip: self.max_connections_per_ip,
4548 max_connections_per_subnet: self.max_connections_per_subnet,
4549 retry_after: self.retry_after,
4550 health_check: self.health_check.clone(),
4551 udp: self.udp.clone(),
4552 affinity_header: None,
4554 affinity_cookie: None,
4555 shard_percent: self.shard_percent,
4556 shard_min_backends: self.shard_min_backends,
4557 shard_mode: self.shard_mode.map(|mode| mode as i32),
4558 max_connection_attempts: self.max_connection_attempts,
4559 })
4560 .into(),
4561 ];
4562
4563 for frontend in &self.frontends {
4564 if frontend.udp {
4569 v.push(
4570 RequestType::AddUdpFrontend(RequestUdpFrontend {
4571 cluster_id: self.cluster_id.clone(),
4572 address: frontend.address.into(),
4573 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
4574 })
4575 .into(),
4576 );
4577 } else {
4578 v.push(
4579 RequestType::AddTcpFrontend(RequestTcpFrontend {
4580 cluster_id: self.cluster_id.clone(),
4581 address: frontend.address.into(),
4582 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
4583 sni: frontend.sni.clone(),
4584 alpn: frontend.alpn.clone(),
4585 })
4586 .into(),
4587 );
4588 }
4589 }
4590
4591 for (backend_count, backend) in self.backends.iter().enumerate() {
4592 let load_balancing_parameters = Some(LoadBalancingParams {
4593 weight: backend.weight.unwrap_or(100) as i32,
4594 });
4595
4596 v.push(
4597 RequestType::add_backend(AddBackend {
4598 cluster_id: self.cluster_id.clone(),
4599 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
4600 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
4601 }),
4602 address: backend.address.into(),
4603 load_balancing_parameters,
4604 sticky_id: backend.sticky_id.clone(),
4605 backup: backend.backup,
4606 unix_path: backend.unix_path.clone(),
4607 })
4608 .into(),
4609 );
4610 }
4611
4612 debug_assert!(
4616 matches!(
4617 v.first().and_then(|r| r.request_type.as_ref()),
4618 Some(RequestType::AddCluster(_))
4619 ),
4620 "TCP cluster orders must lead with an AddCluster"
4621 );
4622 debug_assert_eq!(
4623 v.iter()
4624 .filter(|r| matches!(
4625 r.request_type,
4626 Some(RequestType::AddTcpFrontend(_)) | Some(RequestType::AddUdpFrontend(_))
4627 ))
4628 .count(),
4629 self.frontends.len(),
4630 "one AddTcpFrontend or AddUdpFrontend order per configured frontend"
4631 );
4632 debug_assert_eq!(
4633 v.iter()
4634 .filter(|r| matches!(
4635 r.request_type,
4636 Some(RequestType::AddBackend(_)) | Some(RequestType::AddUnixBackend(_))
4637 ))
4638 .count(),
4639 self.backends.len(),
4640 "one AddBackend order per configured backend"
4641 );
4642 Ok(v)
4643 }
4644}
4645
4646#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4647pub enum ClusterConfig {
4648 Http(HttpClusterConfig),
4649 Tcp(TcpClusterConfig),
4650}
4651
4652impl ClusterConfig {
4653 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
4654 match *self {
4655 ClusterConfig::Http(ref http) => http.generate_requests(),
4656 ClusterConfig::Tcp(ref tcp) => tcp.generate_requests(),
4657 }
4658 }
4659}
4660
4661#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
4663pub struct FileConfig {
4664 pub command_socket: Option<String>,
4665 #[serde(default)]
4669 pub command_socket_mode: Option<String>,
4670 pub command_buffer_size: Option<u64>,
4671 pub max_command_buffer_size: Option<u64>,
4672 pub max_connections: Option<usize>,
4673 pub min_buffers: Option<u64>,
4674 pub max_buffers: Option<u64>,
4675 pub buffer_size: Option<u64>,
4676 #[serde(default)]
4680 pub slab_entries_per_connection: Option<u64>,
4681 #[serde(default)]
4691 pub basic_auth_max_credential_bytes: Option<u64>,
4692 #[serde(default)]
4700 pub max_connections_per_ip: Option<u64>,
4701 #[serde(default)]
4712 pub max_connections_per_subnet: Option<u64>,
4713 #[serde(default)]
4718 pub subnet_ipv4_prefix: Option<u32>,
4719 #[serde(default)]
4724 pub subnet_ipv6_prefix: Option<u32>,
4725 #[serde(default)]
4731 pub retry_after: Option<u32>,
4732 #[serde(default)]
4739 pub max_connection_attempts: Option<u32>,
4740 #[serde(default)]
4749 pub splice_pipe_capacity_bytes: Option<u64>,
4750 #[serde(default)]
4757 pub command_allowed_uids: Option<Vec<u32>>,
4758 pub saved_state: Option<String>,
4759 #[serde(default)]
4760 pub automatic_state_save: Option<bool>,
4761 pub log_level: Option<String>,
4762 pub log_target: Option<String>,
4763 #[serde(default)]
4764 pub log_colored: bool,
4765 #[serde(default)]
4773 pub audit_logs_target: Option<String>,
4774 #[serde(default)]
4779 pub audit_logs_json_target: Option<String>,
4780 #[serde(default)]
4781 pub access_logs_target: Option<String>,
4782 #[serde(default)]
4783 pub access_logs_format: Option<AccessLogFormat>,
4784 #[serde(default)]
4785 pub access_logs_colored: Option<bool>,
4786 pub worker_count: Option<u16>,
4787 pub worker_automatic_restart: Option<bool>,
4788 pub metrics: Option<MetricsConfig>,
4789 pub disable_cluster_metrics: Option<bool>,
4790 pub listeners: Option<Vec<ListenerBuilder>>,
4791 pub clusters: Option<HashMap<String, FileClusterConfig>>,
4792 pub handle_process_affinity: Option<bool>,
4793 pub ctl_command_timeout: Option<u64>,
4794 pub pid_file_path: Option<String>,
4795 pub activate_listeners: Option<bool>,
4796 #[serde(default)]
4797 pub front_timeout: Option<u32>,
4798 #[serde(default)]
4799 pub back_timeout: Option<u32>,
4800 #[serde(default)]
4801 pub connect_timeout: Option<u32>,
4802 #[serde(default)]
4803 pub zombie_check_interval: Option<u32>,
4804 #[serde(default)]
4805 pub accept_queue_timeout: Option<u32>,
4806 #[serde(default)]
4807 pub evict_on_queue_full: Option<bool>,
4808 #[serde(default)]
4809 pub request_timeout: Option<u32>,
4810 #[serde(default)]
4811 pub worker_timeout: Option<u32>,
4812}
4813
4814impl FileConfig {
4815 pub fn load_from_path(path: &str) -> Result<FileConfig, ConfigError> {
4816 let data = Config::load_file(path)?;
4817
4818 let config: FileConfig = match toml::from_str(&data) {
4819 Ok(config) => config,
4820 Err(e) => {
4821 return Err(ConfigError::DeserializeToml(toml_error_diagnostic(
4822 path, &e,
4823 )));
4824 }
4825 };
4826
4827 let mut reserved_address: HashSet<ListenerKey> = HashSet::new();
4828
4829 if let Some(listeners) = config.listeners.as_ref() {
4830 for listener in listeners.iter() {
4831 let key = ListenerKey::new(listener.address, listener.interface.as_deref());
4832 if reserved_address.contains(&key) {
4833 return Err(ConfigError::ListenerAlreadyDeclared(key));
4834 }
4835 reserved_address.insert(key);
4836 }
4837 }
4838
4839 Ok(config)
4861 }
4862}
4863
4864pub struct ConfigBuilder {
4866 file: FileConfig,
4867 known_addresses: HashMap<SocketAddr, ListenerProtocol>,
4868 expect_proxy_addresses: HashSet<SocketAddr>,
4869 resolve_backends: bool,
4872 built: Config,
4873}
4874
4875impl ConfigBuilder {
4876 pub fn new<S>(file_config: FileConfig, config_path: S) -> Self
4880 where
4881 S: ToString,
4882 {
4883 let built = Config {
4884 accept_queue_timeout: file_config
4885 .accept_queue_timeout
4886 .unwrap_or(DEFAULT_ACCEPT_QUEUE_TIMEOUT),
4887 evict_on_queue_full: file_config
4888 .evict_on_queue_full
4889 .unwrap_or(DEFAULT_EVICT_ON_QUEUE_FULL),
4890 activate_listeners: file_config.activate_listeners.unwrap_or(true),
4891 automatic_state_save: file_config
4892 .automatic_state_save
4893 .unwrap_or(DEFAULT_AUTOMATIC_STATE_SAVE),
4894 back_timeout: file_config.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
4895 buffer_size: file_config.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE),
4896 command_buffer_size: file_config
4897 .command_buffer_size
4898 .unwrap_or(DEFAULT_COMMAND_BUFFER_SIZE),
4899 config_path: config_path.to_string(),
4900 connect_timeout: file_config
4901 .connect_timeout
4902 .unwrap_or(DEFAULT_CONNECT_TIMEOUT),
4903 ctl_command_timeout: file_config.ctl_command_timeout.unwrap_or(1_000),
4904 front_timeout: file_config.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
4905 handle_process_affinity: file_config.handle_process_affinity.unwrap_or(false),
4906 access_logs_target: file_config.access_logs_target.clone(),
4907 audit_logs_target: file_config.audit_logs_target.clone(),
4908 audit_logs_json_target: file_config.audit_logs_json_target.clone(),
4909 access_logs_format: file_config.access_logs_format.clone(),
4910 access_logs_colored: file_config.access_logs_colored,
4911 log_level: file_config
4912 .log_level
4913 .clone()
4914 .unwrap_or_else(|| String::from("info")),
4915 log_target: file_config
4916 .log_target
4917 .clone()
4918 .unwrap_or_else(|| String::from("stdout")),
4919 log_colored: file_config.log_colored,
4920 max_buffers: file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
4921 max_command_buffer_size: file_config
4922 .max_command_buffer_size
4923 .unwrap_or(DEFAULT_MAX_COMMAND_BUFFER_SIZE),
4924 max_connections: file_config
4925 .max_connections
4926 .unwrap_or(DEFAULT_MAX_CONNECTIONS),
4927 metrics: file_config.metrics.clone(),
4928 disable_cluster_metrics: file_config
4929 .disable_cluster_metrics
4930 .unwrap_or(DEFAULT_DISABLE_CLUSTER_METRICS),
4931 min_buffers: std::cmp::min(
4932 file_config.min_buffers.unwrap_or(DEFAULT_MIN_BUFFERS),
4933 file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
4934 ),
4935 pid_file_path: file_config.pid_file_path.clone(),
4936 request_timeout: file_config
4937 .request_timeout
4938 .unwrap_or(DEFAULT_REQUEST_TIMEOUT),
4939 saved_state: file_config.saved_state.clone(),
4940 worker_automatic_restart: file_config
4941 .worker_automatic_restart
4942 .unwrap_or(DEFAULT_WORKER_AUTOMATIC_RESTART),
4943 worker_count: file_config.worker_count.unwrap_or(DEFAULT_WORKER_COUNT),
4944 zombie_check_interval: file_config
4945 .zombie_check_interval
4946 .unwrap_or(DEFAULT_ZOMBIE_CHECK_INTERVAL),
4947 worker_timeout: file_config.worker_timeout.unwrap_or(DEFAULT_WORKER_TIMEOUT),
4948 slab_entries_per_connection: file_config.slab_entries_per_connection.map(|n| {
4949 n.clamp(
4950 ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION,
4951 ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION,
4952 )
4953 }),
4954 command_allowed_uids: file_config.command_allowed_uids.clone(),
4955 basic_auth_max_credential_bytes: file_config.basic_auth_max_credential_bytes,
4956 max_connections_per_ip: file_config
4957 .max_connections_per_ip
4958 .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_IP),
4959 max_connections_per_subnet: file_config
4960 .max_connections_per_subnet
4961 .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_SUBNET),
4962 subnet_ipv4_prefix: file_config
4963 .subnet_ipv4_prefix
4964 .unwrap_or(DEFAULT_SUBNET_IPV4_PREFIX),
4965 subnet_ipv6_prefix: file_config
4966 .subnet_ipv6_prefix
4967 .unwrap_or(DEFAULT_SUBNET_IPV6_PREFIX),
4968 retry_after: file_config.retry_after.unwrap_or(DEFAULT_RETRY_AFTER),
4969 max_connection_attempts: file_config
4970 .max_connection_attempts
4971 .unwrap_or(DEFAULT_MAX_CONNECTION_ATTEMPTS),
4972 splice_pipe_capacity_bytes: file_config.splice_pipe_capacity_bytes,
4973 ..Default::default()
4974 };
4975
4976 debug_assert!(
4980 built.min_buffers <= built.max_buffers,
4981 "min_buffers must be clamped to <= max_buffers in the builder"
4982 );
4983 debug_assert!(
4986 built.slab_entries_per_connection.is_none_or(|n| {
4987 (ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION
4988 ..=ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION)
4989 .contains(&n)
4990 }),
4991 "a set slab_entries_per_connection must be clamped into [MIN, MAX]"
4992 );
4993
4994 Self {
4995 file: file_config,
4996 known_addresses: HashMap::new(),
4997 expect_proxy_addresses: HashSet::new(),
4998 resolve_backends: true,
4999 built,
5000 }
5001 }
5002
5003 fn push_tls_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
5004 let listener = listener.to_tls(Some(&self.built))?;
5005 self.built.https_listeners.push(listener);
5006 Ok(())
5007 }
5008
5009 fn push_http_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
5010 let listener = listener.to_http(Some(&self.built))?;
5011 self.built.http_listeners.push(listener);
5012 Ok(())
5013 }
5014
5015 fn push_tcp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
5016 let listener = listener.to_tcp(Some(&self.built))?;
5017 self.built.tcp_listeners.push(listener);
5018 Ok(())
5019 }
5020
5021 fn push_udp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
5022 let listener = listener.to_udp(Some(&self.built))?;
5023 self.built.udp_listeners.push(listener);
5024 Ok(())
5025 }
5026
5027 fn populate_listeners(&mut self, listeners: Vec<ListenerBuilder>) -> Result<(), ConfigError> {
5028 let mut declared: HashSet<ListenerKey> = HashSet::new();
5029 for listener in listeners.iter() {
5030 let key = ListenerKey::new(listener.address, listener.interface.as_deref());
5033 if !declared.insert(key.clone()) {
5034 if key.interface.is_none() {
5035 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
5036 }
5037 return Err(ConfigError::ListenerAlreadyDeclared(key));
5038 }
5039
5040 let protocol = listener
5041 .protocol
5042 .ok_or(ConfigError::Missing(MissingKind::Protocol))?;
5043
5044 if let Some(known) = self.known_addresses.get(&listener.address)
5048 && *known != protocol
5049 {
5050 return Err(ConfigError::ListenerProtocolMismatchOnAddress(
5051 listener.address,
5052 ));
5053 }
5054
5055 self.known_addresses.insert(listener.address, protocol);
5056 if listener.expect_proxy == Some(true) {
5057 self.expect_proxy_addresses.insert(listener.address);
5058 }
5059
5060 if listener.public_address.is_some() && listener.expect_proxy == Some(true) {
5061 return Err(ConfigError::Incompatible {
5062 object: ObjectKind::Listener,
5063 id: listener.address.to_string(),
5064 kind: IncompatibilityKind::PublicAddress,
5065 });
5066 }
5067
5068 match protocol {
5069 ListenerProtocol::Https => self.push_tls_listener(listener.clone())?,
5070 ListenerProtocol::Http => self.push_http_listener(listener.clone())?,
5071 ListenerProtocol::Tcp => self.push_tcp_listener(listener.clone())?,
5072 ListenerProtocol::Udp => self.push_udp_listener(listener.clone())?,
5073 }
5074 }
5075 Ok(())
5076 }
5077
5078 fn populate_clusters(
5079 &mut self,
5080 mut file_cluster_configs: HashMap<String, FileClusterConfig>,
5081 ) -> Result<(), ConfigError> {
5082 for (id, file_cluster_config) in file_cluster_configs.drain() {
5083 let resolver: Option<&BackendResolver<'_>> =
5084 self.resolve_backends.then_some(&system_resolver);
5085 let mut cluster_config = file_cluster_config.to_cluster_config_with(
5086 id.as_str(),
5087 &self.expect_proxy_addresses,
5088 resolver,
5089 )?;
5090
5091 match cluster_config {
5092 ClusterConfig::Http(ref mut http) => {
5093 for frontend in http.frontends.iter_mut() {
5094 match self.known_addresses.get(&frontend.address) {
5095 Some(ListenerProtocol::Tcp) => {
5096 return Err(ConfigError::WrongFrontendProtocol(
5097 ListenerProtocol::Tcp,
5098 ));
5099 }
5100 Some(ListenerProtocol::Udp) => {
5101 return Err(ConfigError::WrongFrontendProtocol(
5102 ListenerProtocol::Udp,
5103 ));
5104 }
5105 Some(ListenerProtocol::Http) => {
5106 if frontend.certificate.is_some() {
5107 return Err(ConfigError::WrongFrontendProtocol(
5108 ListenerProtocol::Http,
5109 ));
5110 }
5111 }
5112 Some(ListenerProtocol::Https) => {
5113 if frontend.certificate.is_none() {
5114 if let Some(https_listener) =
5115 self.built.https_listeners.iter().find(|listener| {
5116 listener.address == frontend.address.into()
5117 && listener.certificate.is_some()
5118 })
5119 {
5120 frontend
5122 .certificate
5123 .clone_from(&https_listener.certificate);
5124 frontend.certificate_chain =
5125 Some(https_listener.certificate_chain.clone());
5126 frontend.key.clone_from(&https_listener.key);
5127 }
5128 if frontend.certificate.is_none() {
5129 debug!("known addresses: {:?}", self.known_addresses);
5130 debug!("frontend: {:?}", frontend);
5131 return Err(ConfigError::WrongFrontendProtocol(
5132 ListenerProtocol::Https,
5133 ));
5134 }
5135 }
5136 }
5137 None => {
5138 let file_listener_protocol = if frontend.certificate.is_some() {
5140 self.push_tls_listener(ListenerBuilder::new(
5141 frontend.address.into(),
5142 ListenerProtocol::Https,
5143 ))?;
5144
5145 ListenerProtocol::Https
5146 } else {
5147 self.push_http_listener(ListenerBuilder::new(
5148 frontend.address.into(),
5149 ListenerProtocol::Http,
5150 ))?;
5151
5152 ListenerProtocol::Http
5153 };
5154 self.known_addresses
5155 .insert(frontend.address, file_listener_protocol);
5156 }
5157 }
5158 }
5159 }
5160 ClusterConfig::Tcp(ref mut tcp) => {
5161 for frontend in tcp.frontends.iter_mut() {
5163 match self.known_addresses.get(&frontend.address) {
5164 Some(ListenerProtocol::Http) | Some(ListenerProtocol::Https) => {
5165 return Err(ConfigError::WrongFrontendProtocol(
5166 ListenerProtocol::Http,
5167 ));
5168 }
5169 Some(ListenerProtocol::Udp) => {
5170 frontend.udp = true;
5176 if let Some(backend) =
5179 tcp.backends.iter().find(|b| b.unix_path.is_some())
5180 {
5181 return Err(ConfigError::InvalidBackendUnixPath {
5182 cluster_id: tcp.cluster_id.to_owned(),
5183 address: backend.address,
5184 reason: "a UDP cluster cannot use unix_path backends",
5185 });
5186 }
5187 }
5188 Some(ListenerProtocol::Tcp) => {}
5189 None => {
5190 self.push_tcp_listener(ListenerBuilder::new(
5192 frontend.address.into(),
5193 ListenerProtocol::Tcp,
5194 ))?;
5195 self.known_addresses
5196 .insert(frontend.address, ListenerProtocol::Tcp);
5197 }
5198 }
5199 }
5200 }
5201 }
5202
5203 self.built.clusters.insert(id, cluster_config);
5204 }
5205 Ok(())
5206 }
5207
5208 pub fn without_backend_resolution(mut self) -> Self {
5212 self.resolve_backends = false;
5213 self
5214 }
5215
5216 pub fn into_config(&mut self) -> Result<Config, ConfigError> {
5218 if self.built.subnet_ipv4_prefix > MAX_SUBNET_IPV4_PREFIX {
5223 return Err(ConfigError::InvalidSubnetPrefix {
5224 key: "subnet_ipv4_prefix",
5225 family: "IPv4",
5226 value: self.built.subnet_ipv4_prefix,
5227 maximum: MAX_SUBNET_IPV4_PREFIX,
5228 });
5229 }
5230 if self.built.subnet_ipv6_prefix > MAX_SUBNET_IPV6_PREFIX {
5231 return Err(ConfigError::InvalidSubnetPrefix {
5232 key: "subnet_ipv6_prefix",
5233 family: "IPv6",
5234 value: self.built.subnet_ipv6_prefix,
5235 maximum: MAX_SUBNET_IPV6_PREFIX,
5236 });
5237 }
5238 validate_max_connection_attempts(self.built.max_connection_attempts).map_err(|reason| {
5239 ConfigError::InvalidMaxConnectionAttempts {
5240 cluster_id: None,
5241 value: self.built.max_connection_attempts,
5242 reason,
5243 }
5244 })?;
5245
5246 if let Some(listeners) = &self.file.listeners {
5247 self.populate_listeners(listeners.clone())?;
5248 }
5249
5250 if let Some(file_cluster_configs) = &self.file.clusters {
5251 self.populate_clusters(file_cluster_configs.clone())?;
5252 }
5253
5254 type SniAlpnByAddress = HashMap<SocketAddr, Vec<(Option<String>, Vec<String>)>>;
5274 let mut frontends_by_address: SniAlpnByAddress = HashMap::new();
5275 let mut addresses_with_no_sni_frontend: HashSet<SocketAddr> = HashSet::new();
5276 for cluster in self.built.clusters.values() {
5277 if let ClusterConfig::Tcp(tcp) = cluster {
5278 for frontend in &tcp.frontends {
5279 if frontend.udp {
5286 continue;
5287 }
5288 if frontend.sni.is_none() {
5289 addresses_with_no_sni_frontend.insert(frontend.address);
5290 }
5291 frontends_by_address
5292 .entry(frontend.address)
5293 .or_default()
5294 .push((frontend.sni.clone(), frontend.alpn.clone()));
5295 }
5296 }
5297 }
5298
5299 let mut addresses_with_sni_frontend: HashSet<SocketAddr> = HashSet::new();
5300 for (address, frontends) in &frontends_by_address {
5301 if !frontends.iter().any(|(sni, _)| sni.is_some()) {
5302 continue;
5303 }
5304 addresses_with_sni_frontend.insert(*address);
5305
5306 if addresses_with_no_sni_frontend.contains(address) {
5307 return Err(ConfigError::TcpListenerMixesSniAndNoSni { address: *address });
5308 }
5309
5310 let mut alpn_lists_by_sni: HashMap<Option<String>, Vec<&Vec<String>>> = HashMap::new();
5311 for (sni, alpn) in frontends {
5312 alpn_lists_by_sni.entry(sni.clone()).or_default().push(alpn);
5313 }
5314 for (sni, alpn_lists) in alpn_lists_by_sni {
5315 let mut seen_protocols: HashSet<&str> = HashSet::new();
5316 let mut catch_all_count = 0usize;
5317 for alpn in alpn_lists {
5318 if alpn.is_empty() {
5319 catch_all_count += 1;
5320 if catch_all_count > 1 {
5321 return Err(ConfigError::TcpFrontendMultipleAlpnCatchAll {
5322 address: *address,
5323 sni: sni.clone(),
5324 });
5325 }
5326 continue;
5327 }
5328 for protocol in alpn {
5329 if !seen_protocols.insert(protocol.as_str()) {
5330 return Err(ConfigError::TcpFrontendAlpnOverlap {
5331 address: *address,
5332 sni: sni.clone(),
5333 protocol: protocol.clone(),
5334 });
5335 }
5336 }
5337 }
5338 }
5339 }
5340
5341 for listener in &self.built.tcp_listeners {
5342 let address: SocketAddr = listener.address.into();
5343 if !addresses_with_sni_frontend.contains(&address) {
5344 continue;
5345 }
5346 let sni_preread_timeout = listener
5347 .sni_preread_timeout
5348 .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT);
5349 if sni_preread_timeout > listener.front_timeout {
5350 return Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
5351 address,
5352 sni_preread_timeout,
5353 front_timeout: listener.front_timeout,
5354 });
5355 }
5356 let sni_preread_max_bytes = listener
5357 .sni_preread_max_bytes
5358 .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES);
5359 if sni_preread_max_bytes < MIN_SNI_PREREAD_MAX_BYTES {
5360 return Err(ConfigError::SniPrereadMaxBytesTooSmall {
5361 address,
5362 sni_preread_max_bytes,
5363 minimum: MIN_SNI_PREREAD_MAX_BYTES,
5364 });
5365 }
5366 if u64::from(sni_preread_max_bytes) > self.built.buffer_size {
5367 return Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
5368 address,
5369 sni_preread_max_bytes,
5370 buffer_size: self.built.buffer_size,
5371 });
5372 }
5373 }
5374
5375 let h2_listeners = self
5384 .built
5385 .https_listeners
5386 .iter()
5387 .filter(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
5388 .count();
5389 if h2_listeners > 0 && self.built.buffer_size < H2_MIN_BUFFER_SIZE {
5390 return Err(ConfigError::BufferSizeTooSmallForH2 {
5391 buffer_size: self.built.buffer_size,
5392 minimum: H2_MIN_BUFFER_SIZE,
5393 listeners: h2_listeners,
5394 });
5395 }
5396
5397 if self.built.command_buffer_size > self.built.max_command_buffer_size {
5404 return Err(ConfigError::CommandBufferSizeExceedsMax {
5405 command_buffer_size: self.built.command_buffer_size,
5406 max_command_buffer_size: self.built.max_command_buffer_size,
5407 });
5408 }
5409
5410 if let Some(cap) = self.built.basic_auth_max_credential_bytes {
5420 let third = self.built.buffer_size / 3;
5421 if cap >= third {
5422 warn!(
5423 "basic_auth_max_credential_bytes = {} is >= buffer_size / 3 ({}); \
5424 a hostile peer can pin ~33% of the per-frontend buffer per failed auth \
5425 attempt. Consider lowering basic_auth_max_credential_bytes (typical \
5426 credentials are <100 bytes) or raising buffer_size.",
5427 cap, third
5428 );
5429 }
5430 }
5431
5432 if self.built.evict_on_queue_full && self.built.max_connections < 100 {
5439 let pct = 100usize.div_ceil(self.built.max_connections);
5440 warn!(
5441 "evict_on_queue_full enabled with max_connections = {}; the eviction batch \
5442 clamps to 1, equivalent to ~{}% of capacity per cap event (the knob is \
5443 documented as 1%). Confirm this is intended.",
5444 self.built.max_connections, pct
5445 );
5446 }
5447
5448 let command_socket_path = self.file.command_socket.clone().unwrap_or({
5449 let mut path = env::current_dir().map_err(|e| ConfigError::Env(e.to_string()))?;
5450 path.push("sozu.sock");
5451 let verified_path = path
5452 .to_str()
5453 .ok_or(ConfigError::InvalidPath(path.clone()))?;
5454 verified_path.to_owned()
5455 });
5456
5457 if let (None, Some(true)) = (&self.file.saved_state, &self.file.automatic_state_save) {
5458 return Err(ConfigError::Missing(MissingKind::SavedState));
5459 }
5460
5461 let command_socket_mode = match self.file.command_socket_mode.as_deref() {
5462 Some(mode) => parse_command_socket_mode(mode)?,
5463 None => DEFAULT_COMMAND_SOCKET_MODE,
5464 };
5465
5466 let config = Config {
5467 command_socket: command_socket_path,
5468 command_socket_mode,
5469 ..self.built.clone()
5470 };
5471
5472 debug_assert!(
5478 config.min_buffers <= config.max_buffers,
5479 "min_buffers must not exceed max_buffers"
5480 );
5481 debug_assert!(
5487 !config
5488 .https_listeners
5489 .iter()
5490 .any(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
5491 || config.buffer_size >= H2_MIN_BUFFER_SIZE,
5492 "an h2-advertising config must satisfy the H2 minimum buffer size"
5493 );
5494 Ok(config)
5495 }
5496}
5497
5498#[derive(Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
5502pub struct Config {
5503 pub config_path: String,
5504 pub command_socket: String,
5505 #[serde(default = "default_command_socket_mode")]
5510 pub command_socket_mode: u32,
5511 pub command_buffer_size: u64,
5512 pub max_command_buffer_size: u64,
5513 pub max_connections: usize,
5514 pub min_buffers: u64,
5515 pub max_buffers: u64,
5516 pub buffer_size: u64,
5517 pub saved_state: Option<String>,
5518 #[serde(default)]
5519 pub automatic_state_save: bool,
5520 pub log_level: String,
5521 pub log_target: String,
5522 pub log_colored: bool,
5523 #[serde(default)]
5526 pub audit_logs_target: Option<String>,
5527 #[serde(default)]
5530 pub audit_logs_json_target: Option<String>,
5531 #[serde(default)]
5532 pub access_logs_target: Option<String>,
5533 pub access_logs_format: Option<AccessLogFormat>,
5534 pub access_logs_colored: Option<bool>,
5535 pub worker_count: u16,
5536 pub worker_automatic_restart: bool,
5537 pub metrics: Option<MetricsConfig>,
5538 #[serde(default = "default_disable_cluster_metrics")]
5539 pub disable_cluster_metrics: bool,
5540 pub http_listeners: Vec<HttpListenerConfig>,
5541 pub https_listeners: Vec<HttpsListenerConfig>,
5542 pub tcp_listeners: Vec<TcpListenerConfig>,
5543 #[serde(default)]
5544 pub udp_listeners: Vec<UdpListenerConfig>,
5545 pub clusters: HashMap<String, ClusterConfig>,
5546 pub handle_process_affinity: bool,
5547 pub ctl_command_timeout: u64,
5548 pub pid_file_path: Option<String>,
5549 pub activate_listeners: bool,
5550 #[serde(default = "default_front_timeout")]
5551 pub front_timeout: u32,
5552 #[serde(default = "default_back_timeout")]
5553 pub back_timeout: u32,
5554 #[serde(default = "default_connect_timeout")]
5555 pub connect_timeout: u32,
5556 #[serde(default = "default_zombie_check_interval")]
5557 pub zombie_check_interval: u32,
5558 #[serde(default = "default_accept_queue_timeout")]
5559 pub accept_queue_timeout: u32,
5560 #[serde(default = "default_evict_on_queue_full")]
5561 pub evict_on_queue_full: bool,
5562 #[serde(default = "default_request_timeout")]
5563 pub request_timeout: u32,
5564 #[serde(default = "default_worker_timeout")]
5565 pub worker_timeout: u32,
5566 #[serde(default)]
5573 pub slab_entries_per_connection: Option<u64>,
5574 #[serde(default)]
5579 pub command_allowed_uids: Option<Vec<u32>>,
5580 #[serde(default)]
5585 pub basic_auth_max_credential_bytes: Option<u64>,
5586 #[serde(default = "default_max_connections_per_ip")]
5592 pub max_connections_per_ip: u64,
5593 #[serde(default = "default_max_connections_per_subnet")]
5599 pub max_connections_per_subnet: u64,
5600 #[serde(default = "default_subnet_ipv4_prefix")]
5603 pub subnet_ipv4_prefix: u32,
5604 #[serde(default = "default_subnet_ipv6_prefix")]
5607 pub subnet_ipv6_prefix: u32,
5608 #[serde(default = "default_retry_after")]
5611 pub retry_after: u32,
5612 #[serde(default = "default_max_connection_attempts")]
5615 pub max_connection_attempts: u32,
5616 #[serde(default)]
5623 pub splice_pipe_capacity_bytes: Option<u64>,
5624}
5625
5626fn default_command_socket_mode() -> u32 {
5627 DEFAULT_COMMAND_SOCKET_MODE
5628}
5629
5630pub fn parse_command_socket_mode(value: &str) -> Result<u32, ConfigError> {
5637 let invalid = |reason| ConfigError::InvalidCommandSocketMode {
5638 value: value.to_owned(),
5639 reason,
5640 };
5641 if value.is_empty() || value.len() > 4 || !value.bytes().all(|b| (b'0'..=b'7').contains(&b)) {
5642 return Err(invalid(
5643 "expected one to four octal digits, such as \"0660\" or \"660\"",
5644 ));
5645 }
5646 let mode =
5647 u32::from_str_radix(value, 8).map_err(|_| invalid("expected one to four octal digits"))?;
5648 if mode > MAX_COMMAND_SOCKET_MODE {
5649 return Err(invalid(
5650 "only permission bits (at most 0777) are allowed; setuid, setgid and sticky are refused",
5651 ));
5652 }
5653 if mode & 0o600 != 0o600 {
5654 return Err(invalid("the owner must keep read and write (0600)"));
5655 }
5656 Ok(mode)
5657}
5658
5659fn default_front_timeout() -> u32 {
5660 DEFAULT_FRONT_TIMEOUT
5661}
5662
5663fn default_back_timeout() -> u32 {
5664 DEFAULT_BACK_TIMEOUT
5665}
5666
5667fn default_connect_timeout() -> u32 {
5668 DEFAULT_CONNECT_TIMEOUT
5669}
5670
5671fn default_request_timeout() -> u32 {
5672 DEFAULT_REQUEST_TIMEOUT
5673}
5674
5675fn default_zombie_check_interval() -> u32 {
5676 DEFAULT_ZOMBIE_CHECK_INTERVAL
5677}
5678
5679fn default_accept_queue_timeout() -> u32 {
5680 DEFAULT_ACCEPT_QUEUE_TIMEOUT
5681}
5682
5683fn default_evict_on_queue_full() -> bool {
5684 DEFAULT_EVICT_ON_QUEUE_FULL
5685}
5686
5687fn default_disable_cluster_metrics() -> bool {
5688 DEFAULT_DISABLE_CLUSTER_METRICS
5689}
5690
5691fn default_worker_timeout() -> u32 {
5692 DEFAULT_WORKER_TIMEOUT
5693}
5694
5695fn default_max_connections_per_ip() -> u64 {
5696 DEFAULT_MAX_CONNECTIONS_PER_IP
5697}
5698
5699fn default_max_connections_per_subnet() -> u64 {
5700 DEFAULT_MAX_CONNECTIONS_PER_SUBNET
5701}
5702
5703fn default_subnet_ipv4_prefix() -> u32 {
5704 DEFAULT_SUBNET_IPV4_PREFIX
5705}
5706
5707fn default_subnet_ipv6_prefix() -> u32 {
5708 DEFAULT_SUBNET_IPV6_PREFIX
5709}
5710
5711fn default_retry_after() -> u32 {
5712 DEFAULT_RETRY_AFTER
5713}
5714
5715fn default_max_connection_attempts() -> u32 {
5716 DEFAULT_MAX_CONNECTION_ATTEMPTS
5717}
5718
5719impl Config {
5720 pub fn load_from_path(path: &str) -> Result<Config, ConfigError> {
5722 let file_config = FileConfig::load_from_path(path)?;
5723
5724 let mut config = ConfigBuilder::new(file_config, path).into_config()?;
5725
5726 config.saved_state = config.saved_state_path()?;
5728
5729 Ok(config)
5730 }
5731
5732 pub fn load_from_path_for_control(path: &str) -> Result<Config, ConfigError> {
5736 let file_config = FileConfig::load_from_path(path)?;
5737
5738 let mut config = ConfigBuilder::new(file_config, path)
5739 .without_backend_resolution()
5740 .into_config()?;
5741 config.saved_state = config.saved_state_path()?;
5742
5743 Ok(config)
5744 }
5745
5746 pub fn generate_config_messages(&self) -> Result<Vec<WorkerRequest>, ConfigError> {
5748 let mut v = Vec::new();
5749 let mut count = 0u8;
5750
5751 for listener in &self.http_listeners {
5752 v.push(WorkerRequest {
5753 id: format!("CONFIG-{count}"),
5754 content: RequestType::AddHttpListener(listener.clone()).into(),
5755 });
5756 count += 1;
5757 }
5758
5759 for listener in &self.https_listeners {
5760 v.push(WorkerRequest {
5761 id: format!("CONFIG-{count}"),
5762 content: RequestType::add_https_listener(listener.clone()).into(),
5763 });
5764 count += 1;
5765 }
5766
5767 for listener in &self.tcp_listeners {
5768 v.push(WorkerRequest {
5769 id: format!("CONFIG-{count}"),
5770 content: RequestType::AddTcpListener(listener.clone()).into(),
5771 });
5772 count += 1;
5773 }
5774
5775 for listener in &self.udp_listeners {
5776 v.push(WorkerRequest {
5777 id: format!("CONFIG-{count}"),
5778 content: RequestType::AddUdpListener(listener.clone()).into(),
5779 });
5780 count += 1;
5781 }
5782
5783 for cluster in self.clusters.values() {
5784 let mut orders = cluster.generate_requests()?;
5785 for content in orders.drain(..) {
5786 v.push(WorkerRequest {
5787 id: format!("CONFIG-{count}"),
5788 content,
5789 });
5790 count += 1;
5791 }
5792 }
5793
5794 if self.activate_listeners {
5795 for listener in &self.http_listeners {
5796 v.push(WorkerRequest {
5797 id: format!("CONFIG-{count}"),
5798 content: RequestType::ActivateListener(ActivateListener {
5799 interface: listener.interface.clone(),
5800 address: listener.address,
5801 proxy: ListenerType::Http.into(),
5802 from_scm: false,
5803 })
5804 .into(),
5805 });
5806 count += 1;
5807 }
5808
5809 for listener in &self.https_listeners {
5810 v.push(WorkerRequest {
5811 id: format!("CONFIG-{count}"),
5812 content: RequestType::ActivateListener(ActivateListener {
5813 interface: listener.interface.clone(),
5814 address: listener.address,
5815 proxy: ListenerType::Https.into(),
5816 from_scm: false,
5817 })
5818 .into(),
5819 });
5820 count += 1;
5821 }
5822
5823 for listener in &self.tcp_listeners {
5824 v.push(WorkerRequest {
5825 id: format!("CONFIG-{count}"),
5826 content: RequestType::ActivateListener(ActivateListener {
5827 interface: listener.interface.clone(),
5828 address: listener.address,
5829 proxy: ListenerType::Tcp.into(),
5830 from_scm: false,
5831 })
5832 .into(),
5833 });
5834 count += 1;
5835 }
5836
5837 for listener in &self.udp_listeners {
5838 v.push(WorkerRequest {
5839 id: format!("CONFIG-{count}"),
5840 content: RequestType::ActivateListener(ActivateListener {
5841 interface: listener.interface.clone(),
5842 address: listener.address,
5843 proxy: ListenerType::Udp.into(),
5844 from_scm: false,
5845 })
5846 .into(),
5847 });
5848 count += 1;
5849 }
5850 }
5851
5852 if self.disable_cluster_metrics {
5853 v.push(WorkerRequest {
5854 id: format!("CONFIG-{count}"),
5855 content: RequestType::ConfigureMetrics(MetricsConfiguration::Disabled.into())
5856 .into(),
5857 });
5858 }
5860
5861 Ok(v)
5862 }
5863
5864 pub fn command_socket_path(&self) -> Result<String, ConfigError> {
5866 let config_path_buf = PathBuf::from(self.config_path.clone());
5867 let mut config_dir = config_path_buf
5868 .parent()
5869 .ok_or(ConfigError::NoFileParent(
5870 config_path_buf.to_string_lossy().to_string(),
5871 ))?
5872 .to_path_buf();
5873
5874 let socket_path = PathBuf::from(self.command_socket.clone());
5875
5876 let mut socket_parent_dir = match socket_path.parent() {
5877 None => config_dir,
5880 Some(path) => {
5881 config_dir.push(path);
5883 config_dir.canonicalize().map_err(|io_error| {
5885 ConfigError::SocketPathError(format!(
5886 "Could not canonicalize path {config_dir:?}: {io_error}"
5887 ))
5888 })?
5889 }
5890 };
5891
5892 let socket_name = socket_path
5893 .file_name()
5894 .ok_or(ConfigError::SocketPathError(format!(
5895 "could not get command socket file name from {socket_path:?}"
5896 )))?;
5897
5898 socket_parent_dir.push(socket_name);
5900
5901 let command_socket_path = socket_parent_dir
5902 .to_str()
5903 .ok_or(ConfigError::SocketPathError(format!(
5904 "Invalid socket path {socket_parent_dir:?}"
5905 )))?
5906 .to_string();
5907
5908 Ok(command_socket_path)
5909 }
5910
5911 fn saved_state_path(&self) -> Result<Option<String>, ConfigError> {
5913 let path = match self.saved_state.as_ref() {
5914 Some(path) => path,
5915 None => return Ok(None),
5916 };
5917
5918 debug!("saved_stated path in the config: {}", path);
5919 let config_path = PathBuf::from(self.config_path.clone());
5920
5921 debug!("Config path buffer: {:?}", config_path);
5922 let config_dir = config_path
5923 .parent()
5924 .ok_or(ConfigError::SaveStatePath(format!(
5925 "Could get parent directory of config file {config_path:?}"
5926 )))?;
5927
5928 debug!("Config folder: {:?}", config_dir);
5929 if !config_dir.exists() {
5930 create_dir_all(config_dir).map_err(|io_error| {
5931 ConfigError::SaveStatePath(format!(
5932 "failed to create state parent directory '{config_dir:?}': {io_error}"
5933 ))
5934 })?;
5935 }
5936
5937 let mut saved_state_path_raw = config_dir.to_path_buf();
5938 saved_state_path_raw.push(path);
5939 debug!(
5940 "Looking for saved state on the path {:?}",
5941 saved_state_path_raw
5942 );
5943
5944 match metadata(path) {
5945 Err(err) if matches!(err.kind(), ErrorKind::NotFound) => {
5946 info!("Create an empty state file at '{}'", path);
5947 File::create(path).map_err(|io_error| {
5948 ConfigError::SaveStatePath(format!(
5949 "failed to create state file '{path:?}': {io_error}"
5950 ))
5951 })?;
5952 }
5953 _ => {}
5954 }
5955
5956 saved_state_path_raw.canonicalize().map_err(|io_error| {
5957 ConfigError::SaveStatePath(format!(
5958 "could not get saved state path from config file input {path:?}: {io_error}"
5959 ))
5960 })?;
5961
5962 let stringified_path = saved_state_path_raw
5963 .to_str()
5964 .ok_or(ConfigError::SaveStatePath(format!(
5965 "Invalid path {saved_state_path_raw:?}"
5966 )))?
5967 .to_string();
5968
5969 Ok(Some(stringified_path))
5970 }
5971
5972 pub fn load_file(path: &str) -> Result<String, ConfigError> {
5974 std::fs::read_to_string(path).map_err(|io_error| ConfigError::FileRead {
5975 path_to_read: path.to_owned(),
5976 io_error,
5977 })
5978 }
5979
5980 pub fn load_file_bytes(path: &str) -> Result<Vec<u8>, ConfigError> {
5982 std::fs::read(path).map_err(|io_error| ConfigError::FileRead {
5983 path_to_read: path.to_owned(),
5984 io_error,
5985 })
5986 }
5987}
5988
5989impl fmt::Debug for Config {
5990 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5991 f.debug_struct("Config")
5992 .field("config_path", &self.config_path)
5993 .field("command_socket", &self.command_socket)
5994 .field(
5995 "command_socket_mode",
5996 &format_args!("{:o}", self.command_socket_mode),
5997 )
5998 .field("command_buffer_size", &self.command_buffer_size)
5999 .field("max_command_buffer_size", &self.max_command_buffer_size)
6000 .field("max_connections", &self.max_connections)
6001 .field("min_buffers", &self.min_buffers)
6002 .field("max_buffers", &self.max_buffers)
6003 .field("buffer_size", &self.buffer_size)
6004 .field("saved_state", &self.saved_state)
6005 .field("automatic_state_save", &self.automatic_state_save)
6006 .field("log_level", &self.log_level)
6007 .field("log_target", &self.log_target)
6008 .field("access_logs_target", &self.access_logs_target)
6009 .field("audit_logs_target", &self.audit_logs_target)
6010 .field("audit_logs_json_target", &self.audit_logs_json_target)
6011 .field("access_logs_format", &self.access_logs_format)
6012 .field("worker_count", &self.worker_count)
6013 .field("worker_automatic_restart", &self.worker_automatic_restart)
6014 .field("metrics", &self.metrics)
6015 .field("disable_cluster_metrics", &self.disable_cluster_metrics)
6016 .field("handle_process_affinity", &self.handle_process_affinity)
6017 .field("ctl_command_timeout", &self.ctl_command_timeout)
6018 .field("pid_file_path", &self.pid_file_path)
6019 .field("activate_listeners", &self.activate_listeners)
6020 .field("front_timeout", &self.front_timeout)
6021 .field("back_timeout", &self.back_timeout)
6022 .field("connect_timeout", &self.connect_timeout)
6023 .field("zombie_check_interval", &self.zombie_check_interval)
6024 .field("accept_queue_timeout", &self.accept_queue_timeout)
6025 .field("evict_on_queue_full", &self.evict_on_queue_full)
6026 .field("request_timeout", &self.request_timeout)
6027 .field("worker_timeout", &self.worker_timeout)
6028 .finish()
6029 }
6030}
6031
6032fn toml_error_diagnostic(path: &str, error: &toml::de::Error) -> String {
6039 match error.span() {
6040 Some(Range { start, end }) => format!(
6041 "error parsing the configuration file '{path}' at bytes {start}..{end}: invalid TOML syntax or unexpected value (the offending text is not shown)"
6042 ),
6043 None => format!(
6044 "error parsing the configuration file '{path}': invalid TOML syntax or unexpected value (the offending text is not shown)"
6045 ),
6046 }
6047}
6048
6049impl ServerConfig {
6050 pub const DEFAULT_SLAB_ENTRIES_PER_CONNECTION: u64 = 4;
6057 pub const MIN_SLAB_ENTRIES_PER_CONNECTION: u64 = 2;
6060 pub const MAX_SLAB_ENTRIES_PER_CONNECTION: u64 = 32;
6063
6064 pub fn effective_slab_entries_per_connection(&self) -> u64 {
6067 let effective = match self.slab_entries_per_connection {
6068 Some(0) | None => Self::DEFAULT_SLAB_ENTRIES_PER_CONNECTION,
6069 Some(n) => n.clamp(
6070 Self::MIN_SLAB_ENTRIES_PER_CONNECTION,
6071 Self::MAX_SLAB_ENTRIES_PER_CONNECTION,
6072 ),
6073 };
6074 debug_assert!(
6078 (Self::MIN_SLAB_ENTRIES_PER_CONNECTION..=Self::MAX_SLAB_ENTRIES_PER_CONNECTION)
6079 .contains(&effective),
6080 "effective slab entries per connection must stay within [MIN, MAX]"
6081 );
6082 effective
6083 }
6084
6085 pub fn slab_capacity(&self) -> u64 {
6092 let per_conn = self.effective_slab_entries_per_connection();
6093 let capacity = 10 + per_conn * self.max_connections;
6094 debug_assert!(
6099 capacity >= 10,
6100 "slab capacity must reserve the base entries"
6101 );
6102 debug_assert!(
6103 self.max_connections == 0 || capacity > 10,
6104 "a non-zero connection cap must reserve per-connection slab entries"
6105 );
6106 capacity
6107 }
6108}
6109
6110impl From<&Config> for ServerConfig {
6112 fn from(config: &Config) -> Self {
6113 let metrics = config.metrics.clone().map(|m| ServerMetricsConfig {
6114 address: m.address.to_string(),
6115 tagged_metrics: m.tagged_metrics,
6116 prefix: m.prefix,
6117 detail: Some(MetricDetail::from(m.detail) as i32),
6118 });
6119 let server_config = Self {
6120 max_connections: config.max_connections as u64,
6121 front_timeout: config.front_timeout,
6122 back_timeout: config.back_timeout,
6123 connect_timeout: config.connect_timeout,
6124 zombie_check_interval: config.zombie_check_interval,
6125 accept_queue_timeout: config.accept_queue_timeout,
6126 min_buffers: config.min_buffers,
6127 max_buffers: config.max_buffers,
6128 buffer_size: config.buffer_size,
6129 log_level: config.log_level.clone(),
6130 log_target: config.log_target.clone(),
6131 access_logs_target: config.access_logs_target.clone(),
6132 audit_logs_target: config.audit_logs_target.clone(),
6133 audit_logs_json_target: config.audit_logs_json_target.clone(),
6134 command_buffer_size: config.command_buffer_size,
6135 max_command_buffer_size: config.max_command_buffer_size,
6136 metrics,
6137 access_log_format: ProtobufAccessLogFormat::from(&config.access_logs_format) as i32,
6138 log_colored: config.log_colored,
6139 slab_entries_per_connection: config.slab_entries_per_connection,
6140 basic_auth_max_credential_bytes: config.basic_auth_max_credential_bytes,
6141 evict_on_queue_full: Some(config.evict_on_queue_full),
6142 max_connections_per_ip: Some(config.max_connections_per_ip),
6143 max_connections_per_subnet: Some(config.max_connections_per_subnet),
6144 subnet_ipv4_prefix: Some(config.subnet_ipv4_prefix),
6145 subnet_ipv6_prefix: Some(config.subnet_ipv6_prefix),
6146 retry_after: Some(config.retry_after),
6147 splice_pipe_capacity_bytes: config.splice_pipe_capacity_bytes,
6148 max_connection_attempts: Some(config.max_connection_attempts),
6149 };
6150
6151 debug_assert!(
6156 server_config.min_buffers <= server_config.max_buffers,
6157 "ServerConfig must preserve min_buffers <= max_buffers"
6158 );
6159 debug_assert_eq!(
6160 server_config.buffer_size, config.buffer_size,
6161 "ServerConfig buffer_size must mirror the source config"
6162 );
6163 debug_assert_eq!(
6164 server_config.max_connections, config.max_connections as u64,
6165 "ServerConfig max_connections must mirror the source config"
6166 );
6167 server_config
6168 }
6169}
6170
6171#[cfg(test)]
6172mod tests {
6173 use toml::to_string;
6174
6175 use super::*;
6176
6177 #[test]
6181 fn silent_close_redirect_policy_parses_case_insensitively() {
6182 for spelling in ["silent_close", "SILENT_CLOSE", "Silent_Close"] {
6183 assert_eq!(
6184 parse_redirect_policy(spelling).ok(),
6185 Some(RedirectPolicy::SilentClose),
6186 "{spelling} must parse to SilentClose"
6187 );
6188 }
6189 assert!(matches!(
6190 parse_redirect_policy("silent-close"),
6191 Err(ConfigError::InvalidRedirectPolicy(value)) if value == "silent-close"
6192 ));
6193 }
6194
6195 #[test]
6196 fn http_frontend_debug_redacts_pem_material() {
6197 const CERTIFICATE_SECRET: &str = "HTTP_FRONTEND_CERTIFICATE_PEM_SECRET_SENTINEL";
6198 const CHAIN_SECRET: &str = "HTTP_FRONTEND_CHAIN_PEM_SECRET_SENTINEL";
6199 const KEY_SECRET: &str = "HTTP_FRONTEND_KEY_PEM_SECRET_SENTINEL";
6200 const HOSTNAME_SECRET: &str = "HTTP_FRONTEND_HOSTNAME_SECRET_SENTINEL";
6201 const PATH_SECRET: &str = "HTTP_FRONTEND_PATH_SECRET_SENTINEL";
6202 const METHOD_SECRET: &str = "HTTP_FRONTEND_METHOD_SECRET_SENTINEL";
6203 const TAG_KEY_SECRET: &str = "HTTP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
6204 const TAG_SECRET: &str = "HTTP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
6205 const HEADER_KEY_SECRET: &str = "HTTP_FRONTEND_HEADER_KEY_SECRET_SENTINEL";
6206 const HEADER_SECRET: &str = "HTTP_FRONTEND_HEADER_VALUE_SECRET_SENTINEL";
6207 const REDIRECT_TEMPLATE_SECRET: &str = "HTTP_FRONTEND_REDIRECT_TEMPLATE_SECRET_SENTINEL";
6208 const REWRITE_HOST_SECRET: &str = "HTTP_FRONTEND_REWRITE_HOST_SECRET_SENTINEL";
6209 const REWRITE_PATH_SECRET: &str = "HTTP_FRONTEND_REWRITE_PATH_SECRET_SENTINEL";
6210
6211 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
6212 let certificate = long_value(CERTIFICATE_SECRET);
6213 let certificate_chain = long_value(CHAIN_SECRET);
6214 let key = long_value(KEY_SECRET);
6215 let hostname = long_value(HOSTNAME_SECRET);
6216 let path = long_value(PATH_SECRET);
6217 let method = long_value(METHOD_SECRET);
6218 let redirect_template = long_value(REDIRECT_TEMPLATE_SECRET);
6219 let rewrite_host = long_value(REWRITE_HOST_SECRET);
6220 let rewrite_path = long_value(REWRITE_PATH_SECRET);
6221
6222 let frontend = HttpFrontendConfig {
6223 address: "127.0.0.1:8443".parse().unwrap(),
6224 hostname,
6225 path: PathRule::prefix(path),
6226 method: Some(method),
6227 certificate: Some(certificate),
6228 key: Some(key),
6229 certificate_chain: Some(vec![certificate_chain]),
6230 tls_versions: vec![TlsVersion::TlsV13],
6231 position: RulePosition::Tree,
6232 tags: Some(BTreeMap::from([(
6233 long_value(TAG_KEY_SECRET),
6234 long_value(TAG_SECRET),
6235 )])),
6236 redirect: None,
6237 redirect_scheme: None,
6238 redirect_template: Some(redirect_template),
6239 rewrite_host: Some(rewrite_host),
6240 rewrite_path: Some(rewrite_path),
6241 rewrite_port: None,
6242 required_auth: None,
6243 headers: vec![Header {
6244 position: HeaderPosition::Request as i32,
6245 key: long_value(HEADER_KEY_SECRET),
6246 val: long_value(HEADER_SECRET),
6247 }],
6248 hsts: None,
6249 header_predicates: Vec::new(),
6250 };
6251
6252 let output = format!("{frontend:?}");
6253
6254 let secrets = [
6255 CERTIFICATE_SECRET,
6256 CHAIN_SECRET,
6257 KEY_SECRET,
6258 HOSTNAME_SECRET,
6259 PATH_SECRET,
6260 METHOD_SECRET,
6261 TAG_KEY_SECRET,
6262 TAG_SECRET,
6263 HEADER_KEY_SECRET,
6264 HEADER_SECRET,
6265 REDIRECT_TEMPLATE_SECRET,
6266 REWRITE_HOST_SECRET,
6267 REWRITE_PATH_SECRET,
6268 ];
6269 for secret in secrets {
6270 assert!(
6271 !output.contains(secret),
6272 "HttpFrontendConfig Debug leaked secret marker {secret}: {output}"
6273 );
6274 }
6275 let expected_metadata = [
6276 "address: 127.0.0.1:8443".to_owned(),
6277 format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
6278 format!("path_kind: {}", PathRule::prefix(String::new()).kind),
6279 format!("path_len: {}", long_value(PATH_SECRET).len()),
6280 format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
6281 "certificate: Some(\"[redacted]\")".to_owned(),
6282 format!(
6283 "certificate_len: Some({})",
6284 long_value(CERTIFICATE_SECRET).len()
6285 ),
6286 "key: Some(\"[redacted]\")".to_owned(),
6287 format!("key_len: Some({})", long_value(KEY_SECRET).len()),
6288 "certificate_chain: Some(\"[redacted]\")".to_owned(),
6289 "certificate_chain_count: Some(1)".to_owned(),
6290 format!(
6291 "certificate_chain_len: Some({})",
6292 long_value(CHAIN_SECRET).len()
6293 ),
6294 "tls_versions_count: 1".to_owned(),
6295 "tags_count: 1".to_owned(),
6296 format!(
6297 "redirect_template_len: Some({})",
6298 long_value(REDIRECT_TEMPLATE_SECRET).len()
6299 ),
6300 format!(
6301 "rewrite_host_len: Some({})",
6302 long_value(REWRITE_HOST_SECRET).len()
6303 ),
6304 format!(
6305 "rewrite_path_len: Some({})",
6306 long_value(REWRITE_PATH_SECRET).len()
6307 ),
6308 "headers_count: 1".to_owned(),
6309 ];
6310 for safe_metadata in expected_metadata {
6311 assert!(
6312 output.contains(&safe_metadata),
6313 "HttpFrontendConfig Debug omitted safe metadata {safe_metadata}: {output}"
6314 );
6315 }
6316 assert!(
6317 output.len() <= 1024,
6318 "HttpFrontendConfig Debug output is not bounded: {} bytes",
6319 output.len()
6320 );
6321
6322 for (index, request) in frontend
6323 .generate_requests("safe-cluster-id")
6324 .into_iter()
6325 .enumerate()
6326 {
6327 let generated_output = format!("{request:?}");
6328 for secret in secrets {
6329 assert!(
6330 !generated_output.contains(secret),
6331 "generated frontend request {index} Debug leaked secret marker {secret}: {generated_output}"
6332 );
6333 }
6334 assert!(
6335 generated_output.len() <= 2048,
6336 "generated frontend request {index} Debug output is not bounded: {} bytes",
6337 generated_output.len()
6338 );
6339 }
6340 }
6341
6342 #[test]
6343 fn hsts_to_proto_enabled_substitutes_default_max_age() {
6344 let cfg = FileHstsConfig {
6345 enabled: Some(true),
6346 max_age: None,
6347 include_subdomains: None,
6348 preload: None,
6349 force_replace_backend: None,
6350 };
6351 let proto = cfg.to_proto("test").expect("should validate");
6352 assert_eq!(proto.enabled, Some(true));
6353 assert_eq!(proto.max_age, Some(DEFAULT_HSTS_MAX_AGE));
6354 }
6355
6356 #[test]
6357 fn hsts_to_proto_explicit_max_age_kept() {
6358 let cfg = FileHstsConfig {
6359 enabled: Some(true),
6360 max_age: Some(63_072_000),
6361 include_subdomains: Some(true),
6362 preload: Some(true),
6363 force_replace_backend: None,
6364 };
6365 let proto = cfg.to_proto("test").expect("should validate");
6366 assert_eq!(proto.max_age, Some(63_072_000));
6367 assert_eq!(proto.include_subdomains, Some(true));
6368 assert_eq!(proto.preload, Some(true));
6369 }
6370
6371 #[test]
6372 fn hsts_to_proto_disabled_keeps_zero_intent() {
6373 let cfg = FileHstsConfig {
6377 enabled: Some(false),
6378 max_age: None,
6379 include_subdomains: None,
6380 preload: None,
6381 force_replace_backend: None,
6382 };
6383 let proto = cfg.to_proto("test").expect("should validate");
6384 assert_eq!(proto.enabled, Some(false));
6385 }
6386
6387 #[test]
6388 fn hsts_to_proto_kill_switch_max_age_zero_allowed() {
6389 let cfg = FileHstsConfig {
6393 enabled: Some(true),
6394 max_age: Some(0),
6395 include_subdomains: None,
6396 preload: None,
6397 force_replace_backend: None,
6398 };
6399 let proto = cfg.to_proto("test").expect("kill-switch must validate");
6400 assert_eq!(proto.max_age, Some(0));
6401 }
6402
6403 #[test]
6404 fn hsts_to_proto_missing_enabled_errors() {
6405 let cfg = FileHstsConfig {
6406 enabled: None,
6407 max_age: Some(31_536_000),
6408 include_subdomains: None,
6409 preload: None,
6410 force_replace_backend: None,
6411 };
6412 match cfg.to_proto("test").unwrap_err() {
6413 ConfigError::HstsEnabledRequired(scope) => assert_eq!(scope, "test"),
6414 other => panic!("expected HstsEnabledRequired, got {other:?}"),
6415 }
6416 }
6417
6418 #[test]
6419 fn hsts_rejected_on_http_listener() {
6420 let mut listener = ListenerBuilder::new(
6425 SocketAddress::new_v4(127, 0, 0, 1, 8080),
6426 ListenerProtocol::Http,
6427 );
6428 listener.hsts = Some(FileHstsConfig {
6429 enabled: Some(true),
6430 max_age: Some(31_536_000),
6431 include_subdomains: None,
6432 preload: None,
6433 force_replace_backend: None,
6434 });
6435 match listener.to_http(None).unwrap_err() {
6436 ConfigError::HstsOnPlainHttp(scope) => assert!(
6437 scope.contains("HTTP listener"),
6438 "expected scope to mention 'HTTP listener', got {scope:?}"
6439 ),
6440 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
6441 }
6442 }
6443
6444 #[test]
6453 fn h2_flood_threshold_zero_rejected_on_http_listener() {
6454 let mut listener = ListenerBuilder::new(
6455 SocketAddress::new_v4(127, 0, 0, 1, 8080),
6456 ListenerProtocol::Http,
6457 );
6458 listener.h2_max_rst_stream_per_window = Some(0);
6459 match listener.to_http(None).unwrap_err() {
6460 ConfigError::H2ThresholdBelowMinimum {
6461 key,
6462 value,
6463 minimum,
6464 ..
6465 } => {
6466 assert_eq!(key, "h2_max_rst_stream_per_window");
6467 assert_eq!(value, 0);
6468 assert_eq!(minimum, 1);
6469 }
6470 other => panic!("expected H2ThresholdBelowMinimum, got {other:?}"),
6471 }
6472 }
6473
6474 #[test]
6478 fn h2_flood_threshold_zero_rejected_on_https_listener() {
6479 let mut listener = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
6480 listener.h2_max_rst_stream_abusive_lifetime = Some(0);
6481 match listener.to_tls(None).unwrap_err() {
6482 ConfigError::H2ThresholdBelowMinimum {
6483 key,
6484 value,
6485 minimum,
6486 ..
6487 } => {
6488 assert_eq!(key, "h2_max_rst_stream_abusive_lifetime");
6489 assert_eq!(value, 0);
6490 assert_eq!(minimum, 1);
6491 }
6492 other => panic!("expected H2ThresholdBelowMinimum, got {other:?}"),
6493 }
6494 }
6495
6496 #[test]
6501 fn h2_stream_shrink_ratio_below_two_rejected_on_http_listener() {
6502 let mut listener = ListenerBuilder::new(
6503 SocketAddress::new_v4(127, 0, 0, 1, 8080),
6504 ListenerProtocol::Http,
6505 );
6506 listener.h2_stream_shrink_ratio = Some(1);
6507 match listener.to_http(None).unwrap_err() {
6508 ConfigError::H2ThresholdBelowMinimum {
6509 key,
6510 value,
6511 minimum,
6512 ..
6513 } => {
6514 assert_eq!(key, "h2_stream_shrink_ratio");
6515 assert_eq!(value, 1);
6516 assert_eq!(minimum, 2);
6517 }
6518 other => panic!("expected H2ThresholdBelowMinimum, got {other:?}"),
6519 }
6520 }
6521
6522 #[test]
6525 fn h2_thresholds_at_their_minimum_accepted() {
6526 let mut listener = ListenerBuilder::new(
6527 SocketAddress::new_v4(127, 0, 0, 1, 8080),
6528 ListenerProtocol::Http,
6529 );
6530 listener.h2_max_rst_stream_per_window = Some(1);
6531 listener.h2_max_rst_stream_abusive_lifetime = Some(1);
6532 listener.h2_stream_shrink_ratio = Some(2);
6533 let config = listener
6534 .to_http(None)
6535 .expect("thresholds at their minimum must be accepted");
6536 assert_eq!(config.h2_max_rst_stream_per_window, Some(1));
6537 assert_eq!(config.h2_max_rst_stream_abusive_lifetime, Some(1));
6538 assert_eq!(config.h2_stream_shrink_ratio, Some(2));
6539 }
6540
6541 #[test]
6542 fn hsts_rejected_on_http_frontend() {
6543 let frontend = FileClusterFrontendConfig {
6549 address: "127.0.0.1:8080".parse().unwrap(),
6550 hostname: Some("example.com".to_owned()),
6551 alpn: vec![],
6552 path: None,
6553 path_type: None,
6554 method: None,
6555 certificate: None,
6556 key: None,
6557 certificate_chain: None,
6558 tls_versions: vec![],
6559 position: RulePosition::Tree,
6560 tags: None,
6561 redirect: None,
6562 redirect_scheme: None,
6563 redirect_template: None,
6564 rewrite_host: None,
6565 rewrite_path: None,
6566 rewrite_port: None,
6567 required_auth: None,
6568 headers: None,
6569 hsts: Some(FileHstsConfig {
6570 enabled: Some(true),
6571 max_age: Some(31_536_000),
6572 include_subdomains: None,
6573 preload: None,
6574 force_replace_backend: None,
6575 }),
6576 header_predicates: None,
6577 };
6578 match frontend.to_http_front("api").unwrap_err() {
6579 ConfigError::HstsOnPlainHttp(scope) => {
6580 assert!(
6581 scope.contains("api") && scope.contains("example.com"),
6582 "expected scope to mention 'api' and 'example.com', got {scope:?}"
6583 );
6584 }
6585 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
6586 }
6587 }
6588
6589 #[test]
6590 fn serialize() {
6591 let http = ListenerBuilder::new(
6592 SocketAddress::new_v4(127, 0, 0, 1, 8080),
6593 ListenerProtocol::Http,
6594 )
6595 .with_answer_404_path(Some("404.html"))
6596 .to_owned();
6597 println!("http: {:?}", to_string(&http));
6598
6599 let https = ListenerBuilder::new(
6600 SocketAddress::new_v4(127, 0, 0, 1, 8443),
6601 ListenerProtocol::Https,
6602 )
6603 .with_answer_404_path(Some("404.html"))
6604 .to_owned();
6605 println!("https: {:?}", to_string(&https));
6606
6607 let listeners = vec![http, https];
6608 let config = FileConfig {
6609 command_socket: Some(String::from("./command_folder/sock")),
6610 worker_count: Some(2),
6611 worker_automatic_restart: Some(true),
6612 max_connections: Some(500),
6613 min_buffers: Some(1),
6614 max_buffers: Some(500),
6615 buffer_size: Some(16393),
6616 metrics: Some(MetricsConfig {
6617 address: "127.0.0.1:8125".parse().unwrap(),
6618 tagged_metrics: false,
6619 prefix: Some(String::from("sozu-metrics")),
6620 detail: MetricDetailLevel::default(),
6621 }),
6622 listeners: Some(listeners),
6623 ..Default::default()
6624 };
6625
6626 println!("config: {:?}", to_string(&config));
6627 let encoded = to_string(&config).unwrap();
6628 println!("conf:\n{encoded}");
6629 }
6630
6631 #[test]
6632 fn parse() {
6633 let path = "assets/config.toml";
6634 let config = Config::load_from_path(path).unwrap_or_else(|load_error| {
6635 panic!("Cannot load config from path {path}: {load_error:?}")
6636 });
6637 println!("config: {config:#?}");
6638 }
6640
6641 #[test]
6642 fn multiple_listeners_preserve_per_address_expect_proxy() {
6643 let toml_content = r#"
6644 command_socket = "/tmp/sozu_test.sock"
6645 worker_count = 1
6646
6647 [[listeners]]
6648 protocol = "http"
6649 address = "172.16.20.1:80"
6650 expect_proxy = true
6651
6652 [[listeners]]
6653 protocol = "http"
6654 address = "10.22.0.1:80"
6655 expect_proxy = false
6656
6657 [[listeners]]
6658 protocol = "https"
6659 address = "192.168.1.1:443"
6660 expect_proxy = true
6661
6662 [[listeners]]
6663 protocol = "https"
6664 address = "192.168.2.1:443"
6665 expect_proxy = false
6666 "#;
6667
6668 let file_config: FileConfig =
6669 toml::from_str(toml_content).expect("Could not parse TOML config");
6670
6671 let listeners = file_config.listeners.as_ref().expect("No listeners found");
6672 assert_eq!(listeners.len(), 4);
6673
6674 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
6675 .into_config()
6676 .expect("Could not build config");
6677
6678 assert_eq!(config.http_listeners.len(), 2);
6679 assert_eq!(config.https_listeners.len(), 2);
6680
6681 let http_proxy = config
6683 .http_listeners
6684 .iter()
6685 .find(|l| SocketAddr::from(l.address) == "172.16.20.1:80".parse().unwrap())
6686 .expect("Listener on 172.16.20.1:80 not found");
6687 let http_direct = config
6688 .http_listeners
6689 .iter()
6690 .find(|l| SocketAddr::from(l.address) == "10.22.0.1:80".parse().unwrap())
6691 .expect("Listener on 10.22.0.1:80 not found");
6692
6693 assert!(http_proxy.expect_proxy);
6694 assert!(!http_direct.expect_proxy);
6695
6696 let https_proxy = config
6698 .https_listeners
6699 .iter()
6700 .find(|l| SocketAddr::from(l.address) == "192.168.1.1:443".parse().unwrap())
6701 .expect("Listener on 192.168.1.1:443 not found");
6702 let https_direct = config
6703 .https_listeners
6704 .iter()
6705 .find(|l| SocketAddr::from(l.address) == "192.168.2.1:443".parse().unwrap())
6706 .expect("Listener on 192.168.2.1:443 not found");
6707
6708 assert!(https_proxy.expect_proxy);
6709 assert!(!https_direct.expect_proxy);
6710 }
6711
6712 fn http_cluster_from_backends(backends: &str) -> Result<HttpClusterConfig, ConfigError> {
6714 let file: FileClusterConfig = toml::from_str(&format!(
6715 "protocol = \"http\"\nfrontends = []\nbackends = [{backends}]\n"
6716 ))
6717 .expect("the cluster must parse");
6718 match file.to_cluster_config("cluster", &HashSet::new())? {
6719 ClusterConfig::Http(http) => Ok(http),
6720 ClusterConfig::Tcp(_) => panic!("an http cluster must build as one"),
6721 }
6722 }
6723
6724 #[test]
6728 fn a_backend_hostname_resolves_when_the_file_is_loaded() {
6729 let backends = r#"{ address = "localhost:8080", weight = 50 }"#;
6730 let http = http_cluster_from_backends(backends).expect("localhost must resolve");
6731
6732 assert!(
6733 !http.backends.is_empty(),
6734 "localhost resolves to an address"
6735 );
6736 let mut ids = HashSet::new();
6737 for backend in &http.backends {
6738 assert!(backend.address.ip().is_loopback(), "{backend:?}");
6739 assert_eq!(backend.address.port(), 8080);
6740 assert_eq!(backend.weight, Some(50));
6741 assert_eq!(
6742 backend.backend_id.as_deref(),
6743 Some(format!("cluster-localhost:8080-{}", backend.address).as_str())
6744 );
6745 assert!(ids.insert(backend.backend_id.clone()), "ids are distinct");
6746 }
6747 assert_eq!(
6748 http_cluster_from_backends(backends)
6749 .expect("localhost must resolve")
6750 .backends,
6751 http.backends,
6752 "the same file yields the same backends"
6753 );
6754
6755 let orders = http.generate_requests().expect("the orders must build");
6756 let added: Vec<&AddBackend> = orders
6757 .iter()
6758 .filter_map(|order| match order.request_type.as_ref() {
6759 Some(RequestType::AddBackend(add)) => Some(add),
6760 _ => None,
6761 })
6762 .collect();
6763 assert_eq!(added.len(), http.backends.len());
6764 for (add, backend) in added.iter().zip(&http.backends) {
6765 assert_eq!(Some(&add.backend_id), backend.backend_id.as_ref());
6766 assert_eq!(SocketAddr::from(add.address), backend.address);
6767 }
6768 }
6769
6770 #[test]
6773 fn a_literal_backend_address_keeps_its_positional_id() {
6774 let http = http_cluster_from_backends(
6775 r#"{ address = "127.0.0.1:8080" }, { address = "[::1]:8081", backend_id = "six" }"#,
6776 )
6777 .expect("literals must build");
6778 assert_eq!(
6779 http.backends,
6780 vec![
6781 BackendConfig {
6782 address: "127.0.0.1:8080".parse().unwrap(),
6783 weight: None,
6784 sticky_id: None,
6785 backup: None,
6786 backend_id: Some("cluster-0-127.0.0.1:8080".to_owned()),
6787 unix_path: None,
6788 },
6789 BackendConfig {
6790 address: "[::1]:8081".parse().unwrap(),
6791 weight: None,
6792 sticky_id: None,
6793 backup: None,
6794 backend_id: Some("six".to_owned()),
6795 unix_path: None,
6796 },
6797 ]
6798 );
6799 }
6800
6801 #[test]
6806 fn a_literal_after_a_name_keeps_its_file_position_id() {
6807 let one: SocketAddr = "10.0.0.2:8080".parse().unwrap();
6808 let two: SocketAddr = "10.0.0.3:8080".parse().unwrap();
6809 let answer = |addresses: Vec<SocketAddr>| {
6810 move |_: &str| -> std::io::Result<Vec<SocketAddr>> { Ok(addresses.clone()) }
6811 };
6812
6813 for (protocol, addresses) in [
6814 ("http", vec![one]),
6815 ("http", vec![one, two]),
6816 ("tcp", vec![one]),
6817 ("tcp", vec![one, two]),
6818 ] {
6819 let resolver = answer(addresses.clone());
6820 let file: FileClusterConfig = toml::from_str(&format!(
6821 "protocol = \"{protocol}\"\nfrontends = []\n\
6822 backends = [{{ address = \"name:8080\" }}, {{ address = \"127.0.0.1:9000\" }}]\n"
6823 ))
6824 .expect("the cluster must parse");
6825 let backends = match file
6826 .to_cluster_config_with("cluster", &HashSet::new(), Some(&resolver))
6827 .expect("the cluster must build")
6828 {
6829 ClusterConfig::Http(http) => http.backends,
6830 ClusterConfig::Tcp(tcp) => tcp.backends,
6831 };
6832 assert_eq!(backends.len(), addresses.len() + 1, "{protocol}");
6833 assert_eq!(
6834 backends.last().and_then(|b| b.backend_id.as_deref()),
6835 Some("cluster-1-127.0.0.1:9000"),
6836 "{protocol} with {} resolved address(es)",
6837 addresses.len()
6838 );
6839 }
6840
6841 let error = toml::from_str::<FileClusterConfig>(
6842 "protocol = \"tcp\"\nfrontends = []\nbackends = [{ address = \"localhost\" }]\n",
6843 )
6844 .expect("the cluster must parse")
6845 .to_cluster_config("cluster", &HashSet::new())
6846 .expect_err("a missing port refuses the tcp cluster");
6847 assert!(matches!(
6848 error,
6849 ConfigError::BackendAddressResolution { .. }
6850 ));
6851 }
6852
6853 #[test]
6857 fn numeric_shorthand_backend_hosts_never_reach_the_resolver() {
6858 let shorthand = |_: &str| -> std::io::Result<Vec<SocketAddr>> {
6859 Ok(vec!["8.0.0.1:8080".parse().unwrap()])
6860 };
6861 for address in [
6862 "010.0.0.1:8080",
6863 "1.2.3:8080",
6864 "2130706433:8080",
6865 "0x7f000001:8080",
6866 "0x7f.1:8080",
6867 "0X7F.0.0.1:8080",
6868 "0177.1:8080",
6869 "0x7f.0177.1:8080",
6870 ] {
6871 let entry = FileBackendConfig {
6872 address: address.to_owned(),
6873 weight: None,
6874 sticky_id: None,
6875 backup: None,
6876 backend_id: None,
6877 unix_path: None,
6878 };
6879 let error = entry
6880 .resolve("cluster", 0, &shorthand)
6881 .expect_err("numeric shorthand must be refused");
6882 let ConfigError::BackendAddressResolution {
6883 cluster_id,
6884 address: entry_address,
6885 ..
6886 } = &error
6887 else {
6888 panic!("{address}: unexpected error {error:?}");
6889 };
6890 assert_eq!(cluster_id, "cluster");
6891 assert_eq!(entry_address, address);
6892 }
6893 }
6894
6895 #[test]
6900 fn unbracketed_ipv6_backend_hosts_never_reach_the_resolver() {
6901 let unreachable = |address: &str| -> std::io::Result<Vec<SocketAddr>> {
6902 panic!("{address}: must not reach the resolver")
6903 };
6904 for address in ["::1:8080", "2001:db8::1:8080", "fe80::1%eth0:80"] {
6905 let entry = FileBackendConfig {
6906 address: address.to_owned(),
6907 weight: None,
6908 sticky_id: None,
6909 backup: None,
6910 backend_id: None,
6911 unix_path: None,
6912 };
6913 let error = entry
6914 .resolve("cluster", 0, &unreachable)
6915 .expect_err("an unbracketed IPv6 literal must be refused");
6916 let ConfigError::BackendAddressResolution {
6917 cluster_id,
6918 address: entry_address,
6919 reason,
6920 } = &error
6921 else {
6922 panic!("{address}: unexpected error {error:?}");
6923 };
6924 assert_eq!(cluster_id, "cluster");
6925 assert_eq!(entry_address, address);
6926 assert!(reason.contains("[address]:port"), "{address}: {reason}");
6927 }
6928
6929 let entry = FileBackendConfig {
6930 address: "[2001:db8::1]:8080".to_owned(),
6931 weight: Some(3),
6932 sticky_id: Some("sticky".to_owned()),
6933 backup: Some(true),
6934 backend_id: None,
6935 unix_path: None,
6936 };
6937 let backends = entry
6938 .resolve("cluster", 2, &unreachable)
6939 .expect("a bracketed IPv6 literal parses directly");
6940 let address: SocketAddr = "[2001:db8::1]:8080".parse().unwrap();
6941 assert_eq!(backends.len(), 1);
6942 assert_eq!(backends[0].address, address);
6943 assert_eq!(backends[0].weight, Some(3));
6944 assert_eq!(backends[0].sticky_id.as_deref(), Some("sticky"));
6945 assert_eq!(backends[0].backup, Some(true));
6946 assert_eq!(
6947 backends[0].backend_id.as_deref(),
6948 Some("cluster-2-[2001:db8::1]:8080")
6949 );
6950 }
6951
6952 #[test]
6955 fn benign_backend_hostnames_are_not_numeric_shorthand() {
6956 let resolved = "10.0.0.2:8080".parse::<SocketAddr>().unwrap();
6957 let resolver = |_: &str| -> std::io::Result<Vec<SocketAddr>> { Ok(vec![resolved]) };
6958 for address in [
6959 "123.example.com:8080",
6960 "dead.beef:8080",
6961 "0x.example.com:8080",
6962 "1.2.3.example:8080",
6963 "localhost:8080",
6964 ] {
6965 let entry = FileBackendConfig {
6966 address: address.to_owned(),
6967 weight: None,
6968 sticky_id: None,
6969 backup: None,
6970 backend_id: None,
6971 unix_path: None,
6972 };
6973 let backends = entry
6974 .resolve("cluster", 0, &resolver)
6975 .unwrap_or_else(|error| panic!("{address}: {error:?}"));
6976 assert_eq!(backends.len(), 1, "{address}");
6977 }
6978 assert!(!is_legacy_numeric_host("dead.beef"));
6979 assert!(!is_legacy_numeric_host("1..2"));
6980 assert!(is_legacy_numeric_host("0x7f.1"));
6981 }
6982
6983 #[test]
6987 fn a_control_load_does_not_resolve_backend_names() {
6988 let dir = tempfile::tempdir().expect("tempdir");
6989 let path = dir.path().join("config.toml");
6990 std::fs::write(
6991 &path,
6992 "command_socket = \"/tmp/sozu_test.sock\"\n\
6993 ctl_command_timeout = 4242\n\
6994 [clusters.app]\n\
6995 protocol = \"http\"\n\
6996 frontends = [{ address = \"127.0.0.1:8080\", hostname = \"example.org\" }]\n\
6997 backends = [{ address = \"1.2.3:8080\" }, { address = \"127.0.0.1:9000\" }]\n",
6998 )
6999 .expect("write config");
7000 let path = path.to_str().expect("utf-8 path");
7001
7002 assert!(matches!(
7003 Config::load_from_path(path),
7004 Err(ConfigError::BackendAddressResolution { .. })
7005 ));
7006
7007 let config = Config::load_from_path_for_control(path).expect("control load");
7008 assert_eq!(config.ctl_command_timeout, 4242);
7009 let Some(ClusterConfig::Http(app)) = config.clusters.get("app") else {
7010 panic!("the cluster must still be validated and built");
7011 };
7012 assert_eq!(app.backends.len(), 1, "only the literal is kept");
7013 }
7014
7015 #[test]
7019 fn an_unresolvable_backend_address_is_a_config_error() {
7020 for address in ["localhost", "localhost:notaport"] {
7021 let error = http_cluster_from_backends(&format!(r#"{{ address = "{address}" }}"#))
7022 .expect_err("the address must not resolve");
7023 let ConfigError::BackendAddressResolution {
7024 cluster_id,
7025 address: entry,
7026 ..
7027 } = &error
7028 else {
7029 panic!("{address}: unexpected error {error:?}");
7030 };
7031 assert_eq!(cluster_id, "cluster");
7032 assert_eq!(entry, address);
7033 assert!(error.to_string().contains(address), "{error}");
7034 }
7035 }
7036
7037 #[test]
7040 fn every_expanded_backend_keeps_its_unix_path() {
7041 let entry = |address: &str| FileBackendConfig {
7042 address: address.to_owned(),
7043 weight: None,
7044 sticky_id: None,
7045 backup: None,
7046 backend_id: None,
7047 unix_path: Some("/run/app.sock".to_owned()),
7048 };
7049 let v4: SocketAddr = "10.0.0.2:8080".parse().unwrap();
7050 let v6: SocketAddr = "[fd00::1]:8080".parse().unwrap();
7051 let expanded = entry("ingress:8080")
7052 .expand("cluster", vec![v4, v6])
7053 .expect("two addresses expand");
7054 assert_eq!(expanded.len(), 2);
7055 assert!(
7056 expanded
7057 .iter()
7058 .all(|backend| backend.unix_path.as_deref() == Some("/run/app.sock"))
7059 );
7060
7061 let literal = entry("127.0.0.1:8080")
7062 .resolve("cluster", 0, &system_resolver)
7063 .expect("a literal resolves");
7064 assert_eq!(literal[0].unix_path.as_deref(), Some("/run/app.sock"));
7065 }
7066
7067 #[test]
7071 fn a_name_with_several_addresses_expands_into_distinct_backends() {
7072 let entry = FileBackendConfig {
7073 address: "ingress:8080".to_owned(),
7074 weight: None,
7075 sticky_id: Some("pinned".to_owned()),
7076 backup: Some(true),
7077 backend_id: Some("ingress".to_owned()),
7078 unix_path: None,
7079 };
7080 let v4: SocketAddr = "10.0.0.2:8080".parse().unwrap();
7081 let v6: SocketAddr = "[fd00::1]:8080".parse().unwrap();
7082 let backends = entry
7083 .expand("cluster", vec![v6, v4, v6])
7084 .expect("two addresses expand");
7085 assert_eq!(
7086 backends,
7087 vec![
7088 BackendConfig {
7089 address: v4,
7090 weight: None,
7091 sticky_id: Some(format!("pinned-{v4}")),
7092 backup: Some(true),
7093 backend_id: Some(format!("ingress-{v4}")),
7094 unix_path: None,
7095 },
7096 BackendConfig {
7097 address: v6,
7098 weight: None,
7099 sticky_id: Some(format!("pinned-{v6}")),
7100 backup: Some(true),
7101 backend_id: Some(format!("ingress-{v6}")),
7102 unix_path: None,
7103 },
7104 ]
7105 );
7106
7107 let single = entry.expand("cluster", vec![v4]).expect("one address");
7109 assert_eq!(single[0].sticky_id, Some(format!("pinned-{v4}")));
7110 assert!(matches!(
7111 entry.expand("cluster", Vec::new()),
7112 Err(ConfigError::BackendAddressResolution { .. })
7113 ));
7114 }
7115
7116 #[test]
7119 fn an_affinity_header_reaches_the_add_cluster_order() {
7120 let file: FileClusterConfig = toml::from_str(
7121 r#"
7122 protocol = "http"
7123 load_balancing = "HRW"
7124 affinity_header = "X-Tenant"
7125 frontends = []
7126 backends = []
7127 "#,
7128 )
7129 .expect("the cluster must parse");
7130 let ClusterConfig::Http(http) = file
7131 .to_cluster_config("keyed", &HashSet::new())
7132 .expect("the cluster must build")
7133 else {
7134 panic!("an http cluster must build as one");
7135 };
7136 let orders = http.generate_requests().expect("the orders must build");
7137 let Some(RequestType::AddCluster(cluster)) =
7138 orders.first().and_then(|order| order.request_type.as_ref())
7139 else {
7140 panic!("the orders must lead with an AddCluster");
7141 };
7142 assert_eq!(cluster.affinity_header.as_deref(), Some("X-Tenant"));
7143 assert_eq!(cluster.affinity_cookie, None);
7144 }
7145
7146 #[test]
7150 fn a_unix_path_backend_reaches_the_add_unix_backend_order() {
7151 let file: FileClusterConfig = toml::from_str(
7152 r#"
7153 protocol = "http"
7154 frontends = []
7155 backends = [
7156 { address = "127.0.0.1:1", unix_path = "/run/app.sock" },
7157 { address = "127.0.0.1:2" },
7158 ]
7159 "#,
7160 )
7161 .expect("the cluster must parse");
7162 let ClusterConfig::Http(http) = file
7163 .to_cluster_config("app", &HashSet::new())
7164 .expect("the cluster must build")
7165 else {
7166 panic!("an http cluster must build as one");
7167 };
7168 let unix_paths: Vec<_> = http
7169 .generate_requests()
7170 .expect("the orders must build")
7171 .into_iter()
7172 .filter_map(|order| match order.request_type {
7173 Some(RequestType::AddUnixBackend(backend)) => {
7174 Some(("AddUnixBackend", backend.unix_path))
7175 }
7176 Some(RequestType::AddBackend(backend)) => Some(("AddBackend", backend.unix_path)),
7177 _ => None,
7178 })
7179 .collect();
7180 assert_eq!(
7181 unix_paths,
7182 vec![
7183 ("AddUnixBackend", Some("/run/app.sock".to_owned())),
7184 ("AddBackend", None)
7185 ]
7186 );
7187 }
7188
7189 #[test]
7192 fn a_unix_path_backend_needs_a_unique_address() {
7193 for protocol in ["http", "tcp"] {
7194 let file: FileClusterConfig = toml::from_str(&format!(
7195 "protocol = \"{protocol}\"\nfrontends = []\nbackends = [\n\
7196 {{ address = \"127.0.0.1:1\", unix_path = \"/run/a.sock\" }},\n\
7197 {{ address = \"127.0.0.1:1\", unix_path = \"/run/b.sock\" }},\n]\n"
7198 ))
7199 .expect("the cluster must parse");
7200 assert!(
7201 matches!(
7202 file.to_cluster_config("app", &HashSet::new()),
7203 Err(ConfigError::InvalidBackendUnixPath { .. })
7204 ),
7205 "a {protocol} cluster must refuse a shared unix backend address"
7206 );
7207 }
7208 }
7209
7210 #[test]
7214 fn a_health_check_port_and_a_unix_path_backend_never_share_a_file_cluster() {
7215 let resolver = |_: &str| -> std::io::Result<Vec<SocketAddr>> {
7216 Ok(vec!["10.0.0.2:8080".parse().unwrap()])
7217 };
7218 for protocol in ["http", "tcp"] {
7219 for address in ["127.0.0.1:1", "ingress:8080"] {
7220 let build = |health_check: &str, resolver: Option<&BackendResolver<'_>>| {
7221 toml::from_str::<FileClusterConfig>(&format!(
7222 "protocol = \"{protocol}\"\nfrontends = []\nbackends = [\n\
7223 {{ address = \"{address}\", unix_path = \"/run/app.sock\" }},\n]\n\
7224 [health_check]\n{health_check}"
7225 ))
7226 .expect("the cluster must parse")
7227 .to_cluster_config_with("app", &HashSet::new(), resolver)
7228 };
7229 for resolver in [Some(&resolver as &BackendResolver<'_>), None] {
7230 let error = build("mode = \"TCP\"\nport = 8081\n", resolver)
7231 .expect_err("a probe port next to a unix backend must be refused");
7232 assert!(
7233 matches!(
7234 &error,
7235 ConfigError::InvalidHealthCheck { cluster_id, reason }
7236 if cluster_id == "app" && *reason == HEALTH_CHECK_PORT_UNIX_REFUSED
7237 ),
7238 "{protocol} {address}: unexpected {error:?}"
7239 );
7240 build("mode = \"TCP\"\n", resolver)
7241 .expect("a unix backend without a probe port stays valid");
7242 }
7243 }
7244 }
7245 }
7246
7247 #[test]
7249 fn parse_backend_unix_path_accepts_absolute_and_abstract_names_only() {
7250 let path = parse_backend_unix_path("/run/app.sock").expect("an absolute path parses");
7251 assert_eq!(
7252 path.as_pathname(),
7253 Some(std::path::Path::new("/run/app.sock"))
7254 );
7255 #[cfg(target_os = "linux")]
7256 {
7257 use std::os::linux::net::SocketAddrExt;
7258 let named = parse_backend_unix_path("@app").expect("an abstract name parses");
7259 assert_eq!(named.as_abstract_name(), Some(&b"app"[..]));
7260 assert_eq!(named.as_pathname(), None);
7261 }
7262 let too_long = format!("/{}", "a".repeat(200));
7263 for refused in ["", "run/app.sock", "@", "/run/a\0b", too_long.as_str()] {
7264 assert!(
7265 parse_backend_unix_path(refused).is_err(),
7266 "{refused:?} must be refused"
7267 );
7268 }
7269 }
7270
7271 #[test]
7274 fn header_predicates_reach_the_add_http_frontend_order() {
7275 let file: FileClusterConfig = toml::from_str(
7276 r#"
7277 protocol = "http"
7278 frontends = [
7279 { address = "0.0.0.0:80", hostname = "api.example.com", header_predicates = [
7280 { name = "X-Canary", match = "EXACT", value = "always" },
7281 { name = "X-Region", match = "regex", value = "eu-.*" },
7282 { name = "X-Beta", match = "present" },
7283 ] },
7284 ]
7285 backends = []
7286 "#,
7287 )
7288 .expect("the cluster must parse");
7289 let ClusterConfig::Http(http) = file
7290 .to_cluster_config("canary", &HashSet::new())
7291 .expect("the cluster must build")
7292 else {
7293 panic!("an http cluster must build as one");
7294 };
7295 let orders = http.generate_requests().expect("the orders must build");
7296 let front = orders
7297 .iter()
7298 .find_map(|order| match order.request_type.as_ref() {
7299 Some(RequestType::AddHttpFrontendWithHeaderPredicates(front)) => Some(front),
7300 _ => None,
7301 })
7302 .expect("the orders must add the frontend");
7303 let predicate = |name: &str, kind: HeaderMatchKind, value: Option<&str>| HeaderPredicate {
7304 name: name.to_owned(),
7305 kind: kind as i32,
7306 value: value.map(str::to_owned),
7307 };
7308 assert_eq!(
7309 front.header_predicates,
7310 vec![
7311 predicate("X-Canary", HeaderMatchKind::Exact, Some("always")),
7312 predicate("X-Region", HeaderMatchKind::Regex, Some("eu-.*")),
7313 predicate("X-Beta", HeaderMatchKind::Present, None),
7314 ]
7315 );
7316 }
7317
7318 #[test]
7321 fn unusable_header_predicates_are_refused_at_config_load() {
7322 for entry in [
7323 r#"{ name = "X-Canary", match = "equals", value = "1" }"#,
7324 r#"{ name = "Host", match = "exact", value = "example.com" }"#,
7325 r#"{ name = "Cookie", match = "present" }"#,
7326 r#"{ name = "X-Canary", match = "present", value = "1" }"#,
7327 r#"{ name = "X-Canary", match = "exact" }"#,
7328 r#"{ name = "X Canary", match = "present" }"#,
7329 ] {
7330 let file: FileClusterConfig = toml::from_str(&format!(
7331 r#"
7332 protocol = "http"
7333 frontends = [
7334 {{ address = "0.0.0.0:80", hostname = "api.example.com", header_predicates = [
7335 {{ name = "X-Valid", match = "present" }},
7336 {entry},
7337 ] }},
7338 ]
7339 backends = []
7340 "#
7341 ))
7342 .expect("the cluster must parse");
7343 match file.to_cluster_config("canary", &HashSet::new()) {
7344 Err(ConfigError::InvalidHeaderPredicate { index: 1, .. }) => {}
7345 other => panic!("{entry}: expected InvalidHeaderPredicate at 1, got {other:?}"),
7346 }
7347 }
7348 }
7349
7350 #[test]
7353 fn a_tcp_cluster_refuses_an_affinity_header_or_cookie() {
7354 for key in [
7355 "affinity_header = \"X-Tenant\"",
7356 "affinity_cookie = \"tenant\"",
7357 ] {
7358 let file: FileClusterConfig = toml::from_str(&format!(
7359 "protocol = \"tcp\"\nload_balancing = \"MAGLEV\"\n{key}\nfrontends = []\nbackends = []\n"
7360 ))
7361 .expect("the cluster must parse");
7362 assert!(
7363 matches!(
7364 file.to_cluster_config("keyed", &HashSet::new()),
7365 Err(ConfigError::InvalidAffinityKey { .. })
7366 ),
7367 "a TCP cluster must refuse `{key}`"
7368 );
7369 }
7370 }
7371
7372 #[test]
7374 fn validate_affinity_key_accepts_one_token_and_refuses_the_rest() {
7375 assert!(validate_affinity_key(None, None).is_ok());
7376 assert!(validate_affinity_key(Some("X-Tenant"), None).is_ok());
7377 assert!(validate_affinity_key(None, Some("tenant_id")).is_ok());
7378 assert!(validate_affinity_key(Some("X-Tenant"), Some("tenant")).is_err());
7379 for unreachable in ["Host", "host", "HOST", "Cookie", "cookie"] {
7380 assert!(
7381 validate_affinity_key(Some(unreachable), None).is_err(),
7382 "header {unreachable:?} can never be found and must be refused"
7383 );
7384 }
7385 assert!(validate_affinity_key(None, Some("host")).is_ok());
7387 for bad in ["", "X Tenant", "X-Tenant:", "tenant\r\n", "t=1"] {
7388 assert!(
7389 validate_affinity_key(Some(bad), None).is_err(),
7390 "header {bad:?} must be refused"
7391 );
7392 assert!(
7393 validate_affinity_key(None, Some(bad)).is_err(),
7394 "cookie {bad:?} must be refused"
7395 );
7396 }
7397 }
7398
7399 #[test]
7402 fn shuffle_sharding_reaches_the_add_cluster_order_and_is_validated() {
7403 let parse = |extra: &str| -> FileClusterConfig {
7404 toml::from_str(&format!(
7405 "protocol = \"http\"\nfrontends = []\nbackends = []\n{extra}"
7406 ))
7407 .expect("the cluster must parse")
7408 };
7409 let ClusterConfig::Http(http) =
7410 parse("shard_percent = 25\nshard_min_backends = 4\nshard_mode = \"STRICT\"\n")
7411 .to_cluster_config("sharded", &HashSet::new())
7412 .expect("a valid sharded cluster builds")
7413 else {
7414 panic!("an http cluster must build as one");
7415 };
7416 let orders = http.generate_requests().expect("the orders must build");
7417 let Some(RequestType::AddCluster(cluster)) =
7418 orders.first().and_then(|order| order.request_type.as_ref())
7419 else {
7420 panic!("the orders must lead with an AddCluster");
7421 };
7422 assert_eq!(
7423 (
7424 cluster.shard_percent,
7425 cluster.shard_min_backends,
7426 cluster.shard_mode
7427 ),
7428 (Some(25), Some(4), Some(ShardMode::Strict as i32))
7429 );
7430 for bad in [
7431 "shard_percent = 0\n",
7432 "shard_percent = 101\n",
7433 "shard_percent = 25\nshard_min_backends = 1\n",
7434 "shard_min_backends = 8\n",
7435 "shard_mode = \"STRICT\"\n",
7436 ] {
7437 assert!(
7438 matches!(
7439 parse(bad).to_cluster_config("sharded", &HashSet::new()),
7440 Err(ConfigError::InvalidShuffleSharding { .. })
7441 ),
7442 "{bad:?} must be refused"
7443 );
7444 }
7445 }
7446
7447 #[test]
7450 fn http_status_range_parses_codes_ranges_classes_and_any() {
7451 let range = |start, end| HttpStatusRange { start, end };
7452 for (spec, expected) in [
7453 ("404", range(404, 404)),
7454 ("200-399", range(200, 399)),
7455 (" 200 - 399 ", range(200, 399)),
7456 ("2xx", range(200, 299)),
7457 ("5XX", range(500, 599)),
7458 ("any", range(100, 599)),
7459 ("ANY", range(100, 599)),
7460 ("100-599", range(100, 599)),
7461 ] {
7462 assert_eq!(parse_http_status_range(spec), Ok(expected), "{spec:?}");
7463 }
7464 for spec in [
7465 "", "abc", "99", "600", "0", "6xx", "0xx", "2x", "2xxx", "399-200", "200-", "-200",
7466 "+200", "200-600", "1-2-3", "2xx-3xx",
7467 ] {
7468 assert!(
7469 parse_http_status_range(spec).is_err(),
7470 "{spec:?} must be refused"
7471 );
7472 }
7473 }
7474
7475 #[test]
7478 fn http_status_range_display_round_trips_through_the_parser() {
7479 for spec in ["404", "200-399", "2xx", "any", "101-102"] {
7480 let range = parse_http_status_range(spec).unwrap();
7481 assert_eq!(range.to_string(), spec);
7482 assert_eq!(parse_http_status_range(&range.to_string()), Ok(range));
7483 }
7484 }
7485
7486 #[test]
7491 fn health_check_validation_covers_mode_uri_and_accepted_statuses() {
7492 let http = HealthCheckConfig {
7493 uri: "/health".to_owned(),
7494 interval: 10,
7495 timeout: 5,
7496 healthy_threshold: 3,
7497 unhealthy_threshold: 3,
7498 expected_status: 0,
7499 mode: HealthCheckMode::Http as i32,
7500 accepted_statuses: Vec::new(),
7501 port: None,
7502 headers: Vec::new(),
7503 };
7504 assert_eq!(validate_health_check_config(&http), Ok(()));
7505
7506 let tcp_no_uri = HealthCheckConfig {
7508 uri: String::new(),
7509 mode: HealthCheckMode::Tcp as i32,
7510 ..http.clone()
7511 };
7512 assert_eq!(validate_health_check_config(&tcp_no_uri), Ok(()));
7513 let http_no_uri = HealthCheckConfig {
7514 uri: String::new(),
7515 ..http.clone()
7516 };
7517 assert!(validate_health_check_config(&http_no_uri).is_err());
7518 let tcp_crlf = HealthCheckConfig {
7520 uri: "/a\r\nGET /b".to_owned(),
7521 ..tcp_no_uri.clone()
7522 };
7523 assert!(validate_health_check_config(&tcp_crlf).is_err());
7524
7525 let tcp_expected = HealthCheckConfig {
7527 expected_status: 200,
7528 ..tcp_no_uri.clone()
7529 };
7530 assert!(validate_health_check_config(&tcp_expected).is_err());
7531 let tcp_accepted = HealthCheckConfig {
7532 accepted_statuses: vec![HttpStatusRange {
7533 start: 100,
7534 end: 599,
7535 }],
7536 ..tcp_no_uri.clone()
7537 };
7538 assert!(validate_health_check_config(&tcp_accepted).is_err());
7539
7540 let unknown_mode = HealthCheckConfig {
7541 mode: 7,
7542 ..http.clone()
7543 };
7544 assert!(validate_health_check_config(&unknown_mode).is_err());
7545
7546 let accepted = HealthCheckConfig {
7547 accepted_statuses: vec![HttpStatusRange {
7548 start: 200,
7549 end: 499,
7550 }],
7551 ..http.clone()
7552 };
7553 assert_eq!(validate_health_check_config(&accepted), Ok(()));
7554 let both = HealthCheckConfig {
7555 expected_status: 200,
7556 ..accepted.clone()
7557 };
7558 assert!(
7559 validate_health_check_config(&both).is_err(),
7560 "expected_status and accepted_statuses must not both be set"
7561 );
7562 for (start, end) in [(99, 200), (200, 600), (300, 200)] {
7563 let bad_range = HealthCheckConfig {
7564 accepted_statuses: vec![HttpStatusRange { start, end }],
7565 ..http.clone()
7566 };
7567 assert!(
7568 validate_health_check_config(&bad_range).is_err(),
7569 "{start}-{end} must be refused"
7570 );
7571 }
7572 }
7573
7574 #[test]
7578 fn health_check_toml_mode_and_accepted_statuses_reach_the_add_cluster_order() {
7579 let parse = |block: &str| -> FileClusterConfig {
7580 toml::from_str(&format!(
7581 "protocol = \"http\"\nfrontends = []\nbackends = []\n[health_check]\n{block}"
7582 ))
7583 .expect("the cluster must parse")
7584 };
7585 let health_check = |block: &str| -> HealthCheckConfig {
7586 let ClusterConfig::Http(http) = parse(block)
7587 .to_cluster_config("checked", &HashSet::new())
7588 .expect("a valid health check builds")
7589 else {
7590 panic!("an http cluster must build as one");
7591 };
7592 let orders = http.generate_requests().expect("the orders must build");
7593 let Some(RequestType::AddCluster(cluster)) =
7594 orders.first().and_then(|order| order.request_type.as_ref())
7595 else {
7596 panic!("the orders must lead with an AddCluster");
7597 };
7598 cluster
7599 .health_check
7600 .clone()
7601 .expect("the health check must travel")
7602 };
7603
7604 let legacy = health_check("uri = \"/health\"\nexpected_status = 204\n");
7606 assert_eq!(legacy.mode, HealthCheckMode::Http as i32);
7607 assert_eq!(
7608 (legacy.uri.as_str(), legacy.expected_status),
7609 ("/health", 204)
7610 );
7611 assert!(legacy.accepted_statuses.is_empty());
7612
7613 let defaulted = health_check("");
7614 assert_eq!(defaulted.mode, HealthCheckMode::Http as i32);
7615 assert_eq!(defaulted.uri, "/");
7616
7617 let tcp = health_check("mode = \"TCP\"\n");
7618 assert_eq!(tcp.mode, HealthCheckMode::Tcp as i32);
7619
7620 let accepted = health_check("uri = \"/livez\"\naccepted_statuses = [\"2xx\", \"404\"]\n");
7621 assert_eq!(
7622 accepted.accepted_statuses,
7623 vec![
7624 HttpStatusRange {
7625 start: 200,
7626 end: 299
7627 },
7628 HttpStatusRange {
7629 start: 404,
7630 end: 404
7631 },
7632 ]
7633 );
7634
7635 for bad in [
7636 "accepted_statuses = [\"6xx\"]\n",
7637 "expected_status = 200\naccepted_statuses = [\"any\"]\n",
7638 "uri = \"health\"\n",
7639 "mode = \"TCP\"\nexpected_status = 200\n",
7640 "mode = \"TCP\"\naccepted_statuses = [\"any\"]\n",
7641 ] {
7642 assert!(
7643 matches!(
7644 parse(bad).to_cluster_config("checked", &HashSet::new()),
7645 Err(ConfigError::InvalidHealthCheck { .. })
7646 ),
7647 "{bad:?} must be refused"
7648 );
7649 }
7650 }
7651
7652 #[test]
7655 fn health_check_validation_bounds_the_probe_port() {
7656 for mode in [HealthCheckMode::Http, HealthCheckMode::Tcp] {
7657 let config = |port: Option<u32>| HealthCheckConfig {
7658 uri: "/health".to_owned(),
7659 interval: 10,
7660 timeout: 5,
7661 healthy_threshold: 3,
7662 unhealthy_threshold: 3,
7663 mode: mode as i32,
7664 port,
7665 ..Default::default()
7666 };
7667 for port in [None, Some(1), Some(8080), Some(65535)] {
7668 assert_eq!(
7669 validate_health_check_config(&config(port)),
7670 Ok(()),
7671 "{mode:?} probe port {port:?} must be accepted"
7672 );
7673 }
7674 for port in [0, 65536, u32::MAX] {
7675 assert_eq!(
7676 validate_health_check_config(&config(Some(port))),
7677 Err("health check port must lie in 1..=65535"),
7678 "{mode:?} probe port {port} must be refused"
7679 );
7680 }
7681 }
7682 }
7683
7684 #[test]
7687 fn health_check_toml_port_reaches_the_add_cluster_order() {
7688 let build = |block: &str| {
7689 toml::from_str::<FileClusterConfig>(&format!(
7690 "protocol = \"tcp\"\nfrontends = []\nbackends = []\n[health_check]\n{block}"
7691 ))
7692 .expect("the cluster must parse")
7693 .to_cluster_config("checked", &HashSet::new())
7694 };
7695 let port = |block: &str| -> Option<u32> {
7696 let Ok(ClusterConfig::Tcp(tcp)) = build(block) else {
7697 panic!("a tcp cluster with a valid health check must build as one");
7698 };
7699 tcp.health_check.expect("the health check must travel").port
7700 };
7701
7702 assert_eq!(port("mode = \"TCP\"\n"), None);
7703 assert_eq!(port("mode = \"TCP\"\nport = 8081\n"), Some(8081));
7704 assert_eq!(port("uri = \"/health\"\nport = 65535\n"), Some(65535));
7705 for bad in ["port = 0\n", "port = 65536\n"] {
7706 assert!(
7707 matches!(build(bad), Err(ConfigError::InvalidHealthCheck { .. })),
7708 "{bad:?} must be refused"
7709 );
7710 }
7711 }
7712
7713 fn health_check_with_headers(headers: &[(&str, &str)]) -> HealthCheckConfig {
7715 HealthCheckConfig {
7716 uri: "/".to_owned(),
7717 interval: 10,
7718 timeout: 5,
7719 healthy_threshold: 3,
7720 unhealthy_threshold: 3,
7721 expected_status: 0,
7722 mode: HealthCheckMode::Http as i32,
7723 accepted_statuses: Vec::new(),
7724 port: None,
7725 headers: headers
7726 .iter()
7727 .map(|(name, _)| HealthCheckHeader {
7728 name: (*name).to_owned(),
7729 fingerprint: String::new(),
7730 })
7731 .collect(),
7732 }
7733 }
7734
7735 fn check_headers(headers: &[(&str, &str)]) -> Result<(), &'static str> {
7737 let config = health_check_with_headers(headers);
7738 validate_health_check_config(&config)?;
7739 let values: Vec<HealthCheckHeaderValue> = headers
7740 .iter()
7741 .map(|(name, value)| HealthCheckHeaderValue {
7742 name: (*name).to_owned(),
7743 value: value.as_bytes().to_vec(),
7744 })
7745 .collect();
7746 validate_health_check_header_values(&config, &values)
7747 }
7748
7749 #[test]
7754 fn health_check_header_value_refuses_leading_and_trailing_ows() {
7755 for value in [" v", "v ", "\tv", "v\t", " ", "\t", " v ", "a b \t"] {
7756 assert_eq!(
7757 check_headers(&[("x-a", value)]),
7758 Err("health check header value must not begin or end with a space or a tab"),
7759 "{value:?} must be refused"
7760 );
7761 }
7762 for value in ["", "v", "a b", "a\tb", "é ü", "\u{a0}v"] {
7763 assert_eq!(
7764 check_headers(&[("x-a", value)]),
7765 Ok(()),
7766 "{value:?} must be accepted"
7767 );
7768 }
7769 }
7770
7771 #[test]
7774 fn health_check_header_validation_covers_every_class() {
7775 let refused = |headers: &[(&str, &str)], reason: &str| {
7776 assert_eq!(
7777 check_headers(headers),
7778 Err(reason),
7779 "{headers:?} must be refused"
7780 );
7781 };
7782
7783 assert_eq!(
7786 check_headers(&[
7787 ("Authorization", "Bearer t"),
7788 ("X-Empty", ""),
7789 ("x-tab", "a\tb"),
7790 ("x-utf8", "caf\u{e9} \u{1F600}"),
7791 ]),
7792 Ok(())
7793 );
7794
7795 let mut tcp = health_check_with_headers(&[("x-a", "1")]);
7797 tcp.mode = HealthCheckMode::Tcp as i32;
7798 assert_eq!(
7799 validate_health_check_config(&tcp),
7800 Err("health check headers apply to HTTP mode only: leave them unset with mode TCP")
7801 );
7802
7803 let names: Vec<String> = (0..33).map(|i| format!("x-{i}")).collect();
7805 let many = |count: usize| -> Vec<(&str, &str)> {
7806 names[..count].iter().map(|n| (n.as_str(), "v")).collect()
7807 };
7808 assert_eq!(check_headers(&many(32)), Ok(()));
7809 refused(&many(33), "health check carries more than 32 headers");
7810
7811 for name in [
7813 ":authority",
7814 ":x",
7815 "Host",
7816 "HOST",
7817 "connection",
7818 "Keep-Alive",
7819 "proxy-connection",
7820 "Transfer-Encoding",
7821 "content-length",
7822 "TE",
7823 "trailer",
7824 "upgrade",
7825 "Expect",
7826 ] {
7827 refused(
7828 &[(name, "v")],
7829 "health check header name is reserved for the probe or its framing",
7830 );
7831 }
7832
7833 for name in [
7835 "",
7836 " x",
7837 "x ",
7838 "a b",
7839 "a:b",
7840 "caf\u{e9}",
7841 "a\r",
7842 "a\"b",
7843 "a(b",
7844 ] {
7845 refused(
7846 &[(name, "v")],
7847 "health check header name must be a non-empty RFC 9110 token",
7848 );
7849 }
7850
7851 for value in [
7853 "a\rb", "a\nb", "a\0b", "a\x01b", "a\x08b", "a\x0bb", "a\x1fb", "a\x7fb",
7854 ] {
7855 refused(
7856 &[("x-a", value)],
7857 "health check header value must not contain control bytes",
7858 );
7859 }
7860
7861 refused(
7863 &[("X-Token", "1"), ("x-token", "2")],
7864 "health check header names must be unique, compared case-insensitively",
7865 );
7866
7867 let value_fits = "v".repeat(8192 - 1 - 4);
7869 assert_eq!(check_headers(&[("x", &value_fits)]), Ok(()));
7870 let value_over = "v".repeat(8192 - 1 - 4 + 1);
7871 refused(
7872 &[("x", &value_over)],
7873 "health check headers exceed 8192 bytes (name + value + 4 per header)",
7874 );
7875
7876 let mut long_uri = health_check_with_headers(&[]);
7879 long_uri.uri = format!("/{}", "a".repeat(9000));
7880 assert_eq!(validate_health_check_config(&long_uri), Ok(()));
7881 long_uri.headers = vec![HealthCheckHeader {
7882 name: "x".to_owned(),
7883 fingerprint: String::new(),
7884 }];
7885 assert_eq!(validate_health_check_config(&long_uri), Ok(()));
7886 let carrier = HealthCheckHeaderValue {
7887 name: "x".to_owned(),
7888 value: "v".repeat(8000).into_bytes(),
7889 };
7890 assert_eq!(
7891 validate_health_check_header_values(&long_uri, &[carrier]),
7892 Err("health check URI and headers may exceed the 16384-byte HTTP/2 probe block")
7893 );
7894
7895 let one = health_check_with_headers(&[("x-a", ""), ("x-b", "")]);
7898 let carrier = |name: &str, value: &[u8]| HealthCheckHeaderValue {
7899 name: name.to_owned(),
7900 value: value.to_vec(),
7901 };
7902 assert_eq!(
7903 validate_health_check_header_values(&one, &[carrier("x-a", b"1")]),
7904 Err("health check carries a header value count different from its header count")
7905 );
7906 assert_eq!(
7907 validate_health_check_header_values(
7908 &one,
7909 &[carrier("x-b", b"1"), carrier("x-a", b"2")]
7910 ),
7911 Err("health check header values must follow the order and names of the headers")
7912 );
7913 assert_eq!(
7914 validate_health_check_header_values(
7915 &one,
7916 &[carrier("x-a", b"1"), carrier("x-b", &[0xff])]
7917 ),
7918 Err("health check header value must be valid UTF-8")
7919 );
7920 let mut spoofed = health_check_with_headers(&[("x-a", "")]);
7922 spoofed.headers[0].fingerprint = "not a fingerprint".to_owned();
7923 assert_eq!(
7924 validate_health_check_config(&spoofed),
7925 Err("health check header fingerprint is malformed")
7926 );
7927 }
7928
7929 #[test]
7933 fn health_check_headers_default_empty_and_round_trip_in_order() {
7934 use prost::Message;
7935
7936 let config = health_check_with_headers(&[
7937 ("Z-Last-Alphabetically", "1"),
7938 ("authorization", "Bearer \u{e9}\tx"),
7939 ("A-First-Alphabetically", ""),
7940 ]);
7941
7942 let mut value = serde_json::to_value(&config).expect("serialize");
7944 value
7945 .as_object_mut()
7946 .expect("an object")
7947 .remove("headers")
7948 .expect("the key is serialized");
7949 let legacy: HealthCheckConfig = serde_json::from_value(value).expect("legacy record loads");
7950 assert!(legacy.headers.is_empty());
7951
7952 let json = serde_json::to_string(&config).expect("serialize");
7954 let back: HealthCheckConfig = serde_json::from_str(&json).expect("deserialize");
7955 assert_eq!(back, config);
7956 assert_eq!(
7957 back.headers
7958 .iter()
7959 .map(|h| h.name.as_str())
7960 .collect::<Vec<_>>(),
7961 [
7962 "Z-Last-Alphabetically",
7963 "authorization",
7964 "A-First-Alphabetically"
7965 ]
7966 );
7967
7968 let headerless = HealthCheckConfig {
7971 headers: Vec::new(),
7972 ..config.clone()
7973 };
7974 let decoded =
7975 HealthCheckConfig::decode(headerless.encode_to_vec().as_slice()).expect("decodes");
7976 assert!(decoded.headers.is_empty());
7977 let decoded =
7978 HealthCheckConfig::decode(config.encode_to_vec().as_slice()).expect("decodes");
7979 assert_eq!(decoded, config);
7980 }
7981
7982 #[test]
7996 fn health_check_toml_headers_split_into_metadata_and_ordered_references() {
7997 let parse = |block: &str| {
7998 toml::from_str::<FileHealthCheckConfig>(&format!("uri = \"/health\"\n{block}"))
7999 .expect("the table parses")
8000 };
8001
8002 let file = parse(
8003 "[[headers]]\nname = \"Z-Second\"\nvalue_file = \"/missing/z\"\n\
8004 [[headers]]\nname = \"Authorization\"\nvalue_file = \"rel/a\"\n\
8005 [[headers]]\nname = \"X-Empty\"\nvalue_file = \"/missing/e\"\n",
8006 );
8007 let (config, files) = file.to_parts().expect("a valid list builds");
8008 assert_eq!(
8009 config
8010 .headers
8011 .iter()
8012 .map(|h| (h.name.as_str(), h.fingerprint.as_str()))
8013 .collect::<Vec<_>>(),
8014 [("Z-Second", ""), ("Authorization", ""), ("X-Empty", "")],
8015 "metadata names the headers and carries no value or fingerprint"
8016 );
8017 assert_eq!(
8018 files
8019 .iter()
8020 .map(|f| (f.name.as_str(), f.value_file.as_str()))
8021 .collect::<Vec<_>>(),
8022 [
8023 ("Z-Second", "/missing/z"),
8024 ("Authorization", "rel/a"),
8025 ("X-Empty", "/missing/e")
8026 ]
8027 );
8028 assert_eq!(
8030 file.to_proto(),
8031 Err(
8032 "health check headers from the configuration file are not available yet: their values need the protected channel"
8033 )
8034 );
8035
8036 let bare = parse("");
8038 assert!(bare.to_proto().expect("builds").headers.is_empty());
8039 assert!(bare.to_parts().expect("builds").1.is_empty());
8040
8041 for bad in [
8043 "mode = \"TCP\"\n[[headers]]\nname = \"x\"\nvalue_file = \"/v\"\n",
8044 "[[headers]]\nname = \"Host\"\nvalue_file = \"/v\"\n",
8045 "[[headers]]\nname = \"x\"\nvalue_file = \"/v\"\n\
8046 [[headers]]\nname = \"X\"\nvalue_file = \"/w\"\n",
8047 ] {
8048 assert!(parse(bad).to_parts().is_err(), "{bad:?} must be refused");
8049 }
8050 assert!(
8052 toml::from_str::<FileHealthCheckConfig>(
8053 "uri = \"/\"\n[[headers]]\nname = \"x\"\nvalue_file = \"/v\"\nextra = 1\n"
8054 )
8055 .is_err()
8056 );
8057 }
8058
8059 const HEADER_SENTINEL: &str = "SENTINEL-HEADER-VALUE-7f3a91";
8062 const REF_SENTINEL: &str = "sentinel-ref-dir-9c1e42";
8063
8064 fn value_file_cluster(protocol: &str, headers: &[(&str, &str)]) -> FileClusterConfig {
8067 let tables: String = headers
8068 .iter()
8069 .map(|(name, path)| {
8070 format!("[[health_check.headers]]\nname = \"{name}\"\nvalue_file = \"{path}\"\n")
8071 })
8072 .collect();
8073 toml::from_str(&format!(
8074 "protocol = \"{protocol}\"\nfrontends = []\nbackends = []\n\
8075 [health_check]\nuri = \"/health\"\n{tables}"
8076 ))
8077 .expect("a value_file reference parses")
8078 }
8079
8080 fn cluster_json(cluster: &ClusterConfig) -> serde_json::Value {
8083 match cluster {
8084 ClusterConfig::Http(http) => serde_json::to_value(http),
8085 ClusterConfig::Tcp(tcp) => serde_json::to_value(tcp),
8086 }
8087 .expect("a cluster serializes")
8088 }
8089
8090 #[test]
8094 fn health_check_value_file_is_kept_unopened_in_order_for_http_and_tcp() {
8095 let missing_a = format!("/{REF_SENTINEL}/missing/a.secret");
8096 let missing_b = format!("relative/../{REF_SENTINEL}/b.secret");
8097 let headers = [
8098 ("Z-Second", missing_a.as_str()),
8099 ("Authorization", &missing_b),
8100 ];
8101
8102 for protocol in ["http", "tcp"] {
8103 let file = value_file_cluster(protocol, &headers);
8104 let cluster = file
8105 .to_cluster_config("checked", &HashSet::new())
8106 .expect("a reference to a missing file builds: nothing opens it");
8107 let json = cluster_json(&cluster);
8108
8109 let metadata: Vec<(&str, &str)> = json["health_check"]["headers"]
8110 .as_array()
8111 .expect("the metadata lists the headers")
8112 .iter()
8113 .map(|h| {
8114 (
8115 h["name"].as_str().expect("a name"),
8116 h["fingerprint"].as_str().expect("a fingerprint"),
8117 )
8118 })
8119 .collect();
8120 assert_eq!(
8121 metadata,
8122 [("Z-Second", ""), ("Authorization", "")],
8123 "{protocol}: ordered names, empty fingerprints"
8124 );
8125 assert_eq!(
8126 json["health_check_header_files"],
8127 serde_json::json!([
8128 { "name": "Z-Second", "value_file": missing_a },
8129 { "name": "Authorization", "value_file": missing_b },
8130 ]),
8131 "{protocol}: references are stored unchanged and in order"
8132 );
8133
8134 let mut config = Config::default();
8135 config
8136 .clusters
8137 .insert("checked".to_owned(), cluster.clone());
8138 let text = serde_json::to_string(&config).expect("a Config serializes");
8139 let back: Config = serde_json::from_str(&text).expect("a Config deserializes");
8140 assert_eq!(
8141 back.clusters.get("checked"),
8142 Some(&cluster),
8143 "{protocol}: references survive Config serde"
8144 );
8145 }
8146
8147 let bare: FileClusterConfig = toml::from_str(
8149 "protocol = \"http\"\nfrontends = []\nbackends = []\n[health_check]\nuri = \"/\"\n",
8150 )
8151 .expect("parses");
8152 let built = bare
8153 .to_cluster_config("bare", &HashSet::new())
8154 .expect("builds");
8155 assert_eq!(
8156 cluster_json(&built)["health_check_header_files"],
8157 serde_json::json!([])
8158 );
8159 }
8160
8161 #[test]
8165 fn health_check_header_refuses_a_literal_or_env_value_and_never_echoes() {
8166 let parse = |table: &str| {
8167 toml::from_str::<FileHealthCheckConfig>(&format!("uri = \"/\"\n[[headers]]\n{table}\n"))
8168 };
8169
8170 for bad in [
8171 format!("name = \"x\"\nvalue = \"{HEADER_SENTINEL}\""),
8172 format!("name = \"x\"\nvalue_env = \"{HEADER_SENTINEL}\""),
8173 "name = \"x\"".to_owned(),
8174 "value_file = \"/p\"".to_owned(),
8175 format!("name = \"x\"\nvalue_file = \"/p\"\nvalue = \"{HEADER_SENTINEL}\""),
8176 format!("name = \"x\"\nvalue_file = \"/p\"\nvalue_env = \"{HEADER_SENTINEL}\""),
8177 format!("name = \"x\"\nvalue_file = \"/p\"\nextra = \"{HEADER_SENTINEL}\""),
8178 format!("name = \"x\"\nvalue_file = 12\nextra = \"{HEADER_SENTINEL}\""),
8179 ] {
8180 let error = parse(&bad).expect_err(&format!("{bad:?} must be refused"));
8181 let diagnostic = toml_error_diagnostic("/etc/sozu.toml", &error);
8182 assert!(
8183 !diagnostic.contains(HEADER_SENTINEL),
8184 "the diagnostic must not echo a value: {diagnostic}"
8185 );
8186 }
8187
8188 let accepted = parse(&format!("name = \"x\"\nvalue_file = \"/{REF_SENTINEL}/v\""))
8189 .expect("a name and a value_file parse");
8190 assert_eq!(accepted.headers.len(), 1);
8191 let debug = format!("{accepted:?}");
8192 assert!(
8193 !debug.contains(REF_SENTINEL),
8194 "Debug must redact the reference path: {debug}"
8195 );
8196 }
8197
8198 #[test]
8203 fn health_check_value_file_cluster_generates_no_worker_request() {
8204 for protocol in ["http", "tcp"] {
8205 let path = format!("/{REF_SENTINEL}/v");
8206 let file = value_file_cluster(protocol, &[("x-token", &path)]);
8207 let cluster = file
8208 .to_cluster_config("guarded", &HashSet::new())
8209 .expect("builds");
8210
8211 let direct = cluster
8212 .generate_requests()
8213 .expect_err("no order may carry a name without its value");
8214 assert!(
8215 matches!(direct, ConfigError::InvalidHealthCheck { .. }),
8216 "{protocol}: {direct:?}"
8217 );
8218
8219 let mut config = Config::default();
8220 config.clusters.insert("guarded".to_owned(), cluster);
8221 let error = config
8222 .generate_config_messages()
8223 .expect_err("the whole generation is refused before any order is returned");
8224 let shown = format!("{error} {error:?}");
8225 assert!(
8226 matches!(error, ConfigError::InvalidHealthCheck { .. })
8227 && shown.contains("guarded")
8228 && !shown.contains(REF_SENTINEL),
8229 "{protocol}: {shown}"
8230 );
8231 }
8232
8233 let headerless: FileClusterConfig = toml::from_str(
8234 "protocol = \"http\"\nfrontends = []\nbackends = []\n[health_check]\nuri = \"/\"\n",
8235 )
8236 .expect("parses");
8237 let cluster = headerless
8238 .to_cluster_config("plain", &HashSet::new())
8239 .expect("builds");
8240 assert!(cluster.generate_requests().is_ok());
8241 }
8242
8243 #[test]
8244 fn multiple_listeners_generate_correct_worker_requests() {
8245 let toml_content = r#"
8246 command_socket = "/tmp/sozu_test.sock"
8247 worker_count = 1
8248 activate_listeners = true
8249
8250 [[listeners]]
8251 protocol = "http"
8252 address = "172.16.20.1:80"
8253 expect_proxy = true
8254
8255 [[listeners]]
8256 protocol = "http"
8257 address = "10.22.0.1:80"
8258 expect_proxy = false
8259 "#;
8260
8261 let file_config: FileConfig =
8262 toml::from_str(toml_content).expect("Could not parse TOML config");
8263
8264 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8265 .into_config()
8266 .expect("Could not build config");
8267
8268 let messages = config
8269 .generate_config_messages()
8270 .expect("Could not generate config messages");
8271
8272 let add_listener_count = messages
8273 .iter()
8274 .filter(|m| {
8275 matches!(
8276 m.content.request_type,
8277 Some(RequestType::AddHttpListener(_))
8278 )
8279 })
8280 .count();
8281
8282 let activate_listener_count = messages
8283 .iter()
8284 .filter(|m| {
8285 matches!(
8286 m.content.request_type,
8287 Some(RequestType::ActivateListener(ActivateListener {
8288 proxy,
8289 ..
8290 })) if proxy == ListenerType::Http as i32
8291 )
8292 })
8293 .count();
8294
8295 assert_eq!(add_listener_count, 2);
8296 assert_eq!(activate_listener_count, 2);
8297 }
8298
8299 #[test]
8300 fn documented_udp_dns_example_loads_and_emits_udp_requests() {
8301 let toml_content = r#"
8308 command_socket = "/tmp/sozu_test.sock"
8309 worker_count = 1
8310 activate_listeners = true
8311
8312 [[listeners]]
8313 protocol = "udp"
8314 address = "0.0.0.0:53"
8315
8316 [clusters.dns]
8317 protocol = "tcp"
8318 load_balancing = "HRW"
8319 frontends = [
8320 { address = "0.0.0.0:53" }
8321 ]
8322 backends = [
8323 { address = "10.0.0.10:53" },
8324 { address = "10.0.0.11:53" }
8325 ]
8326
8327 [clusters.dns.udp]
8328 affinity_key = "SOURCE_IP"
8329 responses = 1
8330 requests = 0
8331 send_proxy_protocol = true
8332
8333 [clusters.dns.udp.health]
8334 mode = "TCP_PROBE"
8335 tcp_port = 53
8336 rise = 2
8337 fall = 3
8338 fail_open = true
8339 "#;
8340
8341 let file_config: FileConfig =
8342 toml::from_str(toml_content).expect("Could not parse documented DNS TOML");
8343
8344 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8345 .into_config()
8346 .expect("documented UDP DNS example must load without WrongFrontendProtocol");
8347
8348 assert_eq!(
8350 config.udp_listeners.len(),
8351 1,
8352 "the protocol=\"udp\" listener must be built"
8353 );
8354
8355 let messages = config
8356 .generate_config_messages()
8357 .expect("Could not generate config messages");
8358
8359 let add_udp_listener_count = messages
8360 .iter()
8361 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpListener(_))))
8362 .count();
8363 assert_eq!(
8364 add_udp_listener_count, 1,
8365 "must emit exactly one AddUdpListener"
8366 );
8367
8368 let add_udp_frontend_count = messages
8369 .iter()
8370 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
8371 .count();
8372 assert_eq!(
8373 add_udp_frontend_count, 1,
8374 "the cluster frontend on the UDP listener must emit AddUdpFrontend"
8375 );
8376
8377 let add_tcp_frontend_count = messages
8379 .iter()
8380 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddTcpFrontend(_))))
8381 .count();
8382 assert_eq!(
8383 add_tcp_frontend_count, 0,
8384 "a UDP-listener-addressed frontend must not be emitted as AddTcpFrontend"
8385 );
8386
8387 let udp_frontend = messages
8389 .iter()
8390 .find_map(|m| match &m.content.request_type {
8391 Some(RequestType::AddUdpFrontend(f)) => Some(f),
8392 _ => None,
8393 })
8394 .expect("AddUdpFrontend must be present");
8395 assert_eq!(udp_frontend.cluster_id, "dns");
8396 assert_eq!(
8397 SocketAddr::from(udp_frontend.address),
8398 "0.0.0.0:53".parse().unwrap()
8399 );
8400
8401 let cluster = messages
8404 .iter()
8405 .find_map(|m| match &m.content.request_type {
8406 Some(RequestType::AddCluster(c)) if c.cluster_id == "dns" => Some(c),
8407 _ => None,
8408 })
8409 .expect("AddCluster for 'dns' must be present");
8410 let udp = cluster
8411 .udp
8412 .as_ref()
8413 .expect("[clusters.dns.udp] block must carry onto the cluster");
8414 assert_eq!(udp.responses, Some(1));
8415 }
8416
8417 #[test]
8423 fn listener_interface_round_trips_from_the_config_file() {
8424 let toml_content = r#"
8425 command_socket = "/tmp/sozu_test.sock"
8426 worker_count = 1
8427 activate_listeners = true
8428
8429 [[listeners]]
8430 protocol = "http"
8431 address = "0.0.0.0:8080"
8432 interface = "wg0"
8433
8434 [[listeners]]
8435 protocol = "http"
8436 address = "0.0.0.0:8080"
8437 interface = "lo"
8438
8439 [[listeners]]
8440 protocol = "http"
8441 address = "0.0.0.0:8081"
8442
8443 [[listeners]]
8444 protocol = "https"
8445 address = "0.0.0.0:8443"
8446 interface = "wg0"
8447
8448 [[listeners]]
8449 protocol = "tcp"
8450 address = "0.0.0.0:9000"
8451 interface = "wg0"
8452
8453 [[listeners]]
8454 protocol = "udp"
8455 address = "0.0.0.0:5353"
8456 interface = "wg0"
8457 "#;
8458
8459 let file_config: FileConfig =
8460 toml::from_str(toml_content).expect("the interface key must parse");
8461 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8462 .into_config()
8463 .expect("listeners on one address with different interfaces must build");
8464
8465 let http: Vec<(String, Option<&str>)> = config
8466 .http_listeners
8467 .iter()
8468 .map(|l| {
8469 (
8470 SocketAddr::from(l.address).to_string(),
8471 l.interface.as_deref(),
8472 )
8473 })
8474 .collect();
8475 assert_eq!(
8476 http,
8477 vec![
8478 ("0.0.0.0:8080".to_owned(), Some("wg0")),
8479 ("0.0.0.0:8080".to_owned(), Some("lo")),
8480 ("0.0.0.0:8081".to_owned(), None),
8481 ]
8482 );
8483 assert_eq!(config.https_listeners[0].interface.as_deref(), Some("wg0"));
8484 assert_eq!(config.tcp_listeners[0].interface.as_deref(), Some("wg0"));
8485 assert_eq!(config.udp_listeners[0].interface.as_deref(), Some("wg0"));
8486
8487 let activations: Vec<Option<String>> = config
8488 .generate_config_messages()
8489 .expect("generate the config messages")
8490 .into_iter()
8491 .filter_map(|request| match request.content.request_type {
8492 Some(RequestType::ActivateListener(activate)) => Some(activate.interface),
8493 _ => None,
8494 })
8495 .collect();
8496 assert_eq!(
8497 activations,
8498 vec![
8499 Some("wg0".to_owned()),
8500 Some("lo".to_owned()),
8501 None,
8502 Some("wg0".to_owned()),
8503 Some("wg0".to_owned()),
8504 Some("wg0".to_owned()),
8505 ]
8506 );
8507
8508 let duplicate = r#"
8509 command_socket = "/tmp/sozu_test.sock"
8510 worker_count = 1
8511
8512 [[listeners]]
8513 protocol = "http"
8514 address = "0.0.0.0:8080"
8515 interface = "wg0"
8516
8517 [[listeners]]
8518 protocol = "http"
8519 address = "0.0.0.0:8080"
8520 interface = "wg0"
8521 "#;
8522 let file_config: FileConfig = toml::from_str(duplicate).expect("parse");
8523 assert!(
8524 ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8525 .into_config()
8526 .is_err(),
8527 "the same (address, interface) twice must be refused"
8528 );
8529 }
8530
8531 #[test]
8534 fn invalid_listener_interface_rejected_at_config_load() {
8535 let toml_content = r#"
8536 command_socket = "/tmp/sozu_test.sock"
8537 worker_count = 1
8538
8539 [[listeners]]
8540 protocol = "http"
8541 address = "0.0.0.0:8080"
8542 interface = "wg/0"
8543 "#;
8544 let file_config: FileConfig = toml::from_str(toml_content).expect("parse");
8545 let error = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8546 .into_config()
8547 .expect_err("an invalid interface name must be refused");
8548 let message = error.to_string();
8549 assert!(message.contains("0.0.0.0:8080"), "{message}");
8550 assert!(message.contains("wg/0"), "{message}");
8551 }
8552
8553 #[test]
8554 fn duplicate_listener_address_rejected() {
8555 let toml_content = r#"
8556 command_socket = "/tmp/sozu_test.sock"
8557 worker_count = 1
8558
8559 [[listeners]]
8560 protocol = "http"
8561 address = "0.0.0.0:80"
8562
8563 [[listeners]]
8564 protocol = "http"
8565 address = "0.0.0.0:80"
8566 "#;
8567
8568 let file_config: FileConfig =
8569 toml::from_str(toml_content).expect("Could not parse TOML config");
8570
8571 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8572
8573 assert!(
8574 result.is_err(),
8575 "Should reject duplicate listener addresses"
8576 );
8577 }
8578
8579 #[test]
8580 fn buffer_size_below_h2_minimum_rejected() {
8581 let toml_content = r#"
8583 command_socket = "/tmp/sozu_test.sock"
8584 worker_count = 1
8585 buffer_size = 8192
8586
8587 [[listeners]]
8588 protocol = "https"
8589 address = "127.0.0.1:8443"
8590 "#;
8591 let file_config: FileConfig =
8592 toml::from_str(toml_content).expect("Could not parse TOML config");
8593 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8594 match result {
8595 Err(ConfigError::BufferSizeTooSmallForH2 {
8596 buffer_size: 8192,
8597 minimum: 16_393,
8598 listeners: 1,
8599 }) => {}
8600 other => panic!("expected BufferSizeTooSmallForH2, got {other:?}"),
8601 }
8602 }
8603
8604 #[test]
8605 fn buffer_size_below_h2_minimum_accepted_when_no_h2_listener() {
8606 let toml_content = r#"
8608 command_socket = "/tmp/sozu_test.sock"
8609 worker_count = 1
8610 buffer_size = 8192
8611
8612 [[listeners]]
8613 protocol = "https"
8614 address = "127.0.0.1:8443"
8615 alpn_protocols = ["http/1.1"]
8616 "#;
8617 let file_config: FileConfig =
8618 toml::from_str(toml_content).expect("Could not parse TOML config");
8619 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8620 assert!(
8621 result.is_ok(),
8622 "non-H2 HTTPS listener with sub-16393 buffer should be accepted: {result:?}"
8623 );
8624 }
8625
8626 fn command_socket_mode_of(mode_line: &str) -> Result<Config, ConfigError> {
8627 let toml_content = format!(
8628 r#"
8629 command_socket = "/tmp/sozu_test.sock"
8630 worker_count = 1
8631 {mode_line}
8632 "#
8633 );
8634 let file_config: FileConfig =
8635 toml::from_str(&toml_content).expect("Could not parse TOML config");
8636 ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config()
8637 }
8638
8639 #[test]
8640 fn command_socket_mode_defaults_to_0600() {
8641 let config = command_socket_mode_of("").expect("config without the key must load");
8642 assert_eq!(config.command_socket_mode, 0o600);
8643 assert_eq!(DEFAULT_COMMAND_SOCKET_MODE, 0o600);
8644 }
8645
8646 #[test]
8647 fn command_socket_mode_accepts_octal_strings() {
8648 for (value, expected) in [
8649 ("0660", 0o660),
8650 ("660", 0o660),
8651 ("0600", 0o600),
8652 ("0666", 0o666),
8653 ("777", 0o777),
8654 ("0777", 0o777),
8655 ("600", 0o600),
8656 ("0640", 0o640),
8657 ] {
8658 let config = command_socket_mode_of(&format!("command_socket_mode = \"{value}\""))
8659 .unwrap_or_else(|e| panic!("{value:?} must load: {e}"));
8660 assert_eq!(config.command_socket_mode, expected, "{value:?}");
8661 }
8662 }
8663
8664 #[test]
8665 fn command_socket_mode_rejects_invalid_values() {
8666 for value in [
8667 "", "8", "0668", "abc", "0o660", "0x1b6", "-660", "+660", " 660", "660 ", "1660",
8668 "2770", "4755", "7777", "01000", "00660", "0", "0000", "0066", "0200", "0400", "0466",
8669 ] {
8670 match command_socket_mode_of(&format!("command_socket_mode = \"{value}\"")) {
8671 Err(ConfigError::InvalidCommandSocketMode {
8672 value: rejected, ..
8673 }) => {
8674 assert_eq!(rejected, value)
8675 }
8676 other => panic!("{value:?} must be rejected, got {other:?}"),
8677 }
8678 }
8679 }
8680
8681 #[test]
8682 fn command_socket_mode_rejects_a_toml_integer() {
8683 let dir = tempfile::tempdir().expect("tempdir");
8687 let path = dir.path().join("config.toml");
8688 std::fs::write(
8689 &path,
8690 "command_socket = \"/tmp/sozu_test.sock\"\ncommand_socket_mode = 660\n",
8691 )
8692 .expect("write config");
8693 match FileConfig::load_from_path(path.to_str().expect("utf-8 path")) {
8694 Err(ConfigError::DeserializeToml(message)) => assert!(
8695 message.contains("config.toml") && message.contains(" at bytes "),
8696 "the diagnostic keeps the path and the byte range: {message}"
8697 ),
8698 other => panic!("an integer mode must fail as DeserializeToml, got {other:?}"),
8699 }
8700 }
8701
8702 #[test]
8703 fn config_without_command_socket_mode_deserializes_to_0600() {
8704 let mut encoded = serde_json::to_value(Config::default()).expect("serialize Config");
8707 encoded
8708 .as_object_mut()
8709 .expect("Config serializes to an object")
8710 .remove("command_socket_mode")
8711 .expect("Config serializes command_socket_mode");
8712 let decoded: Config = serde_json::from_value(encoded).expect("deserialize Config");
8713 assert_eq!(decoded.command_socket_mode, 0o600);
8714 }
8715
8716 #[test]
8717 fn command_buffer_size_exceeds_max_rejected() {
8718 let toml_content = r#"
8723 command_socket = "/tmp/sozu_test.sock"
8724 worker_count = 1
8725 command_buffer_size = 2000000
8726 max_command_buffer_size = 1000000
8727 "#;
8728 let file_config: FileConfig =
8729 toml::from_str(toml_content).expect("Could not parse TOML config");
8730 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8731 match result {
8732 Err(ConfigError::CommandBufferSizeExceedsMax {
8733 command_buffer_size: 2_000_000,
8734 max_command_buffer_size: 1_000_000,
8735 }) => {}
8736 other => panic!("expected CommandBufferSizeExceedsMax, got {other:?}"),
8737 }
8738 }
8739
8740 #[test]
8741 fn command_buffer_size_equal_to_max_accepted() {
8742 let toml_content = r#"
8745 command_socket = "/tmp/sozu_test.sock"
8746 worker_count = 1
8747 command_buffer_size = 1000000
8748 max_command_buffer_size = 1000000
8749 "#;
8750 let file_config: FileConfig =
8751 toml::from_str(toml_content).expect("Could not parse TOML config");
8752 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8753 assert!(
8754 result.is_ok(),
8755 "command_buffer_size == max_command_buffer_size should be accepted: {result:?}"
8756 );
8757 }
8758
8759 #[test]
8760 fn buffer_size_at_h2_minimum_accepted() {
8761 let toml_content = r#"
8762 command_socket = "/tmp/sozu_test.sock"
8763 worker_count = 1
8764 buffer_size = 16393
8765
8766 [[listeners]]
8767 protocol = "https"
8768 address = "127.0.0.1:8443"
8769 "#;
8770 let file_config: FileConfig =
8771 toml::from_str(toml_content).expect("Could not parse TOML config");
8772 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8773 assert!(
8774 result.is_ok(),
8775 "buffer_size at the H2 minimum should be accepted: {result:?}"
8776 );
8777 }
8778
8779 #[test]
8780 fn alpn_protocols_default() {
8781 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8782 let config = builder.to_tls(None).expect("to_tls should succeed");
8783 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
8784 }
8785
8786 #[test]
8787 fn alpn_protocols_custom() {
8788 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8789 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned()]));
8790 let config = builder.to_tls(None).expect("to_tls should succeed");
8791 assert_eq!(config.alpn_protocols, vec!["http/1.1"]);
8792 }
8793
8794 #[test]
8795 fn alpn_protocols_invalid_rejected() {
8796 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8797 builder.with_alpn_protocols(Some(vec!["h3".to_owned()]));
8798 let result = builder.to_tls(None);
8799 assert!(result.is_err());
8800 let err = result.unwrap_err();
8801 assert!(
8802 err.to_string().contains("h3"),
8803 "error should mention the invalid protocol: {err}"
8804 );
8805 }
8806
8807 #[test]
8808 fn alpn_protocols_empty_uses_default() {
8809 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8810 builder.with_alpn_protocols(Some(vec![]));
8811 let config = builder.to_tls(None).expect("to_tls should succeed");
8812 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
8813 }
8814
8815 #[test]
8816 fn alpn_protocols_deduplicated() {
8817 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8818 builder.with_alpn_protocols(Some(vec![
8819 "h2".to_owned(),
8820 "h2".to_owned(),
8821 "http/1.1".to_owned(),
8822 ]));
8823 let config = builder.to_tls(None).expect("to_tls should succeed");
8824 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
8825 }
8826
8827 #[test]
8828 fn alpn_protocols_order_preserved() {
8829 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8830 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned(), "h2".to_owned()]));
8831 let config = builder.to_tls(None).expect("to_tls should succeed");
8832 assert_eq!(config.alpn_protocols, vec!["http/1.1", "h2"]);
8833 }
8834
8835 #[test]
8841 fn parse_header_edit_rejects_crlf_in_value() {
8842 let entry = HeaderEditConfig {
8843 position: "request".to_owned(),
8844 key: "X-Test".to_owned(),
8845 value: "value\r\nEvil-Header: stolen".to_owned(),
8846 };
8847 let err = parse_header_edit(0, &entry).expect_err("CRLF in value must be rejected");
8848 match err {
8849 ConfigError::InvalidHeaderBytes { index, field } => {
8850 assert_eq!(index, 0);
8851 assert_eq!(field, "value");
8852 }
8853 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
8854 }
8855 }
8856
8857 #[test]
8858 fn parse_header_edit_rejects_lf_in_key() {
8859 let entry = HeaderEditConfig {
8860 position: "response".to_owned(),
8861 key: "X-\nTest".to_owned(),
8862 value: "ok".to_owned(),
8863 };
8864 let err = parse_header_edit(2, &entry).expect_err("LF in key must be rejected");
8865 match err {
8866 ConfigError::InvalidHeaderBytes { index, field } => {
8867 assert_eq!(index, 2);
8868 assert_eq!(field, "key");
8869 }
8870 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
8871 }
8872 }
8873
8874 #[test]
8875 fn parse_header_edit_rejects_nul() {
8876 let entry = HeaderEditConfig {
8877 position: "both".to_owned(),
8878 key: "X-Test".to_owned(),
8879 value: "with\0nul".to_owned(),
8880 };
8881 assert!(matches!(
8882 parse_header_edit(0, &entry),
8883 Err(ConfigError::InvalidHeaderBytes { .. })
8884 ));
8885 }
8886
8887 #[test]
8893 fn parse_header_edit_accepts_tab_in_value() {
8894 let entry = HeaderEditConfig {
8895 position: "request".to_owned(),
8896 key: "X-Test".to_owned(),
8897 value: "with\ttab".to_owned(),
8898 };
8899 let header = parse_header_edit(0, &entry).expect("tab in value must be accepted");
8900 assert_eq!(header.val, "with\ttab");
8901 }
8902
8903 #[test]
8910 fn parse_header_edit_rejects_tab_in_key() {
8911 let entry = HeaderEditConfig {
8912 position: "request".to_owned(),
8913 key: "Host\t".to_owned(),
8914 value: "ok".to_owned(),
8915 };
8916 let err = parse_header_edit(0, &entry).expect_err("HTAB in key must be rejected");
8917 match err {
8918 ConfigError::InvalidHeaderBytes { field, .. } => assert_eq!(field, "key"),
8919 other => panic!("expected InvalidHeaderBytes{{field=\"key\"}}, got {other:?}"),
8920 }
8921 }
8922
8923 #[test]
8924 fn parse_header_edit_rejects_space_in_key() {
8925 let entry = HeaderEditConfig {
8926 position: "request".to_owned(),
8927 key: "X Test".to_owned(),
8928 value: "ok".to_owned(),
8929 };
8930 let err = parse_header_edit(0, &entry).expect_err("SP in key must be rejected");
8931 assert!(matches!(err, ConfigError::InvalidHeaderBytes { .. }));
8932 }
8933
8934 #[test]
8935 fn parse_header_edit_rejects_empty_key() {
8936 let entry = HeaderEditConfig {
8937 position: "request".to_owned(),
8938 key: String::new(),
8939 value: "ok".to_owned(),
8940 };
8941 let err = parse_header_edit(0, &entry).expect_err("empty key must be rejected");
8942 assert!(matches!(
8943 err,
8944 ConfigError::InvalidHeaderBytes { field: "key", .. }
8945 ));
8946 }
8947
8948 #[test]
8949 fn parse_header_edit_accepts_clean_value() {
8950 let entry = HeaderEditConfig {
8951 position: "request".to_owned(),
8952 key: "X-Tenant".to_owned(),
8953 value: "alpha".to_owned(),
8954 };
8955 let header = parse_header_edit(0, &entry).expect("clean value must be accepted");
8956 assert_eq!(header.key, "X-Tenant");
8957 assert_eq!(header.val, "alpha");
8958 }
8959
8960 #[test]
8964 fn resolve_answer_source_bare_string_is_literal() {
8965 let body = resolve_answer_source("HTTP/1.1 503 Service Unavailable\r\n\r\nbusy")
8966 .expect("bare-string source must resolve");
8967 assert_eq!(body, "HTTP/1.1 503 Service Unavailable\r\n\r\nbusy");
8968 }
8969
8970 #[test]
8971 fn resolve_answer_source_empty_string_is_legitimate() {
8972 let body = resolve_answer_source("").expect("empty source must resolve");
8973 assert_eq!(body, "");
8974 }
8975
8976 #[test]
8981 fn resolve_answer_source_file_scheme_missing_file_errors() {
8982 let err = resolve_answer_source("file:///nonexistent/sozu-test/never.http")
8983 .expect_err("missing path must error");
8984 assert!(matches!(err, ConfigError::FileOpen { .. }));
8985 }
8986
8987 #[test]
8990 fn resolve_answer_source_file_scheme_empty_path_errors() {
8991 let err = resolve_answer_source("file://").expect_err("empty path must error");
8992 assert!(matches!(err, ConfigError::FileOpen { .. }));
8993 }
8994
8995 #[test]
9003 fn legacy_tcp_frontend_toml_without_sni_alpn_parses_unchanged() {
9004 let toml_content = r#"
9005 command_socket = "/tmp/sozu_test.sock"
9006 worker_count = 1
9007
9008 [[listeners]]
9009 protocol = "tcp"
9010 address = "127.0.0.1:9000"
9011
9012 [clusters.legacy]
9013 protocol = "tcp"
9014 load_balancing = "ROUND_ROBIN"
9015 frontends = [
9016 { address = "127.0.0.1:9000" }
9017 ]
9018 backends = [
9019 { address = "10.0.0.1:9000" }
9020 ]
9021 "#;
9022 let file_config: FileConfig =
9023 toml::from_str(toml_content).expect("Could not parse legacy TCP TOML");
9024 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9025 .into_config()
9026 .expect("legacy TCP config without sni/alpn must load unchanged");
9027
9028 assert_eq!(config.tcp_listeners.len(), 1);
9029 let listener = &config.tcp_listeners[0];
9030 assert_eq!(
9031 listener.sni_preread_timeout,
9032 Some(DEFAULT_SNI_PREREAD_TIMEOUT),
9033 "proto default sni_preread_timeout must be populated even though unused"
9034 );
9035 assert_eq!(
9036 listener.sni_preread_max_bytes,
9037 Some(DEFAULT_SNI_PREREAD_MAX_BYTES),
9038 "proto default sni_preread_max_bytes must be populated even though unused"
9039 );
9040
9041 let messages = config
9042 .generate_config_messages()
9043 .expect("Could not generate config messages");
9044 let tcp_frontend = messages
9045 .iter()
9046 .find_map(|m| match &m.content.request_type {
9047 Some(RequestType::AddTcpFrontend(f)) => Some(f),
9048 _ => None,
9049 })
9050 .expect("AddTcpFrontend must be present");
9051 assert_eq!(tcp_frontend.sni, None, "legacy frontend must carry no sni");
9052 assert!(
9053 tcp_frontend.alpn.is_empty(),
9054 "legacy frontend must carry no alpn"
9055 );
9056 }
9057
9058 #[test]
9069 fn a_catch_all_post_frontend_loads_from_toml_and_rejects_a_mixed_case_position() {
9070 let catch_all = |position: &str| {
9071 format!(
9072 r#"
9073 command_socket = "/tmp/sozu_test.sock"
9074 worker_count = 1
9075
9076 [[listeners]]
9077 protocol = "http"
9078 address = "127.0.0.1:8080"
9079
9080 [clusters.fallback]
9081 protocol = "http"
9082 frontends = [
9083 {{ address = "127.0.0.1:8080", hostname = "*", position = "{position}" }}
9084 ]
9085 backends = [
9086 {{ address = "10.0.0.1:8000" }}
9087 ]
9088 "#
9089 )
9090 };
9091
9092 let file_config: FileConfig = toml::from_str(&catch_all("POST"))
9093 .expect("an upper-case `position = \"POST\"` must parse");
9094 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9095 .into_config()
9096 .expect("a catch-all POST frontend must load");
9097 let messages = config
9098 .generate_config_messages()
9099 .expect("Could not generate config messages");
9100 let frontend = messages
9101 .iter()
9102 .find_map(|m| match &m.content.request_type {
9103 Some(RequestType::AddHttpFrontend(f)) => Some(f),
9104 _ => None,
9105 })
9106 .expect("AddHttpFrontend must be present");
9107 assert_eq!(frontend.hostname, "*");
9108 assert_eq!(frontend.position, RulePosition::Post as i32);
9109
9110 let error = toml::from_str::<FileConfig>(&catch_all("Post"))
9111 .expect_err("a mixed-case `position = \"Post\"` must be refused")
9112 .to_string();
9113 assert!(
9114 error.contains("unknown variant `Post`, expected one of `PRE`, `POST`, `TREE`"),
9115 "unexpected error: {error}",
9116 );
9117 }
9118
9119 #[test]
9125 fn forwarded_headers_parses_every_mode_defaults_to_both_and_rejects_others() {
9126 let listeners = |value: Option<&str>| {
9127 let line = value
9128 .map(|value| format!("forwarded_headers = \"{value}\""))
9129 .unwrap_or_default();
9130 format!(
9131 r#"
9132 command_socket = "/tmp/sozu_test.sock"
9133 worker_count = 1
9134
9135 [[listeners]]
9136 protocol = "http"
9137 address = "127.0.0.1:8080"
9138 {line}
9139
9140 [[listeners]]
9141 protocol = "https"
9142 address = "127.0.0.1:8443"
9143 {line}
9144 "#
9145 )
9146 };
9147 let load = |value: Option<&str>| {
9148 let file_config: FileConfig =
9149 toml::from_str(&listeners(value)).expect("the listeners must parse");
9150 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9151 .into_config()
9152 .expect("the listeners must load");
9153 (
9154 config.http_listeners[0].forwarded_headers,
9155 config.https_listeners[0].forwarded_headers,
9156 )
9157 };
9158
9159 for (spelling, expected) in [
9160 (None, ForwardedHeaders::Both),
9161 (Some("both"), ForwardedHeaders::Both),
9162 (Some("x_forwarded"), ForwardedHeaders::XForwarded),
9163 (Some("rfc7239"), ForwardedHeaders::Rfc7239),
9164 (Some("none"), ForwardedHeaders::None),
9165 ] {
9166 let expected = Some(expected as i32);
9167 assert_eq!(load(spelling), (expected, expected), "{spelling:?}");
9168 }
9169
9170 for invalid in ["RFC7239", "x-forwarded", "forwarded", ""] {
9171 let error = toml::from_str::<FileConfig>(&listeners(Some(invalid)))
9172 .expect_err("an unknown forwarded_headers value must be refused")
9173 .to_string();
9174 assert!(
9175 error.contains(&format!(
9176 "unknown variant `{invalid}`, expected one of `both`, `x_forwarded`, `rfc7239`, `none`"
9177 )),
9178 "{invalid:?}: unexpected error: {error}",
9179 );
9180 }
9181 }
9182
9183 #[test]
9186 fn tcp_frontend_hostname_maps_to_sni_exact_and_wildcard() {
9187 let toml_content = r#"
9188 command_socket = "/tmp/sozu_test.sock"
9189 worker_count = 1
9190
9191 [[listeners]]
9192 protocol = "tcp"
9193 address = "127.0.0.1:9010"
9194
9195 [clusters.exact]
9196 protocol = "tcp"
9197 load_balancing = "ROUND_ROBIN"
9198 frontends = [
9199 { address = "127.0.0.1:9010", hostname = "example.com", alpn = ["h2"] }
9200 ]
9201 backends = [ { address = "10.0.0.1:9010" } ]
9202
9203 [clusters.wildcard]
9204 protocol = "tcp"
9205 load_balancing = "ROUND_ROBIN"
9206 frontends = [
9207 { address = "127.0.0.1:9010", hostname = "*.example.com" }
9208 ]
9209 backends = [ { address = "10.0.0.2:9010" } ]
9210 "#;
9211 let file_config: FileConfig =
9212 toml::from_str(toml_content).expect("Could not parse TOML config");
9213 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9214 .into_config()
9215 .expect("exact + wildcard SNI frontends on distinct sni must load");
9216
9217 let messages = config
9218 .generate_config_messages()
9219 .expect("Could not generate config messages");
9220 let mut frontends: Vec<_> = messages
9221 .iter()
9222 .filter_map(|m| match &m.content.request_type {
9223 Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
9224 _ => None,
9225 })
9226 .collect();
9227 frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
9228
9229 assert_eq!(frontends.len(), 2);
9230 assert_eq!(frontends[0].sni, Some("example.com".to_string()));
9231 assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
9232 assert_eq!(frontends[1].sni, Some("*.example.com".to_string()));
9233 assert!(frontends[1].alpn.is_empty());
9234 }
9235
9236 #[test]
9241 fn tcp_frontend_invalid_sni_pattern_rejected() {
9242 for invalid in [
9243 "*.*.example.com",
9244 "foo.*.com",
9245 "*",
9246 "example..com",
9247 "",
9248 "/[a-z]+/.example.com",
9249 "foo/bar.example.com",
9250 ] {
9251 let frontend = FileClusterFrontendConfig {
9252 address: "127.0.0.1:8080".parse().unwrap(),
9253 hostname: Some(invalid.to_string()),
9254 alpn: vec![],
9255 path: None,
9256 path_type: None,
9257 method: None,
9258 certificate: None,
9259 key: None,
9260 certificate_chain: None,
9261 tls_versions: vec![],
9262 position: RulePosition::Tree,
9263 tags: None,
9264 redirect: None,
9265 redirect_scheme: None,
9266 redirect_template: None,
9267 rewrite_host: None,
9268 rewrite_path: None,
9269 rewrite_port: None,
9270 required_auth: None,
9271 headers: None,
9272 hsts: None,
9273 header_predicates: None,
9274 };
9275 match frontend.to_tcp_front() {
9276 Err(ConfigError::InvalidSniPattern { sni }) => assert_eq!(sni, invalid),
9277 other => panic!("expected InvalidSniPattern for {invalid:?}, got {other:?}"),
9278 }
9279 }
9280 }
9281
9282 #[test]
9288 fn tcp_frontend_non_ascii_sni_pattern_rejected() {
9289 for non_ascii in ["münchen.example", "*.bücher.example", "日本.example"] {
9290 let frontend = FileClusterFrontendConfig {
9291 address: "127.0.0.1:8080".parse().unwrap(),
9292 hostname: Some(non_ascii.to_string()),
9293 alpn: vec![],
9294 path: None,
9295 path_type: None,
9296 method: None,
9297 certificate: None,
9298 key: None,
9299 certificate_chain: None,
9300 tls_versions: vec![],
9301 position: RulePosition::Tree,
9302 tags: None,
9303 redirect: None,
9304 redirect_scheme: None,
9305 redirect_template: None,
9306 rewrite_host: None,
9307 rewrite_path: None,
9308 rewrite_port: None,
9309 required_auth: None,
9310 headers: None,
9311 hsts: None,
9312 header_predicates: None,
9313 };
9314 match frontend.to_tcp_front() {
9315 Err(ConfigError::NonAsciiSniPattern { sni }) => assert_eq!(sni, non_ascii),
9316 other => panic!("expected NonAsciiSniPattern for {non_ascii:?}, got {other:?}"),
9317 }
9318 }
9319 }
9320
9321 #[test]
9326 fn tcp_frontend_punycode_sni_pattern_accepted() {
9327 assert_eq!(
9328 validate_sni_pattern("xn--mnchen-3ya.example").expect("A-label must be accepted"),
9329 "xn--mnchen-3ya.example"
9330 );
9331 assert_eq!(
9332 validate_sni_pattern("*.xn--bcher-kva.example")
9333 .expect("wildcarded A-label must be accepted"),
9334 "*.xn--bcher-kva.example"
9335 );
9336 assert_eq!(
9337 validate_sni_pattern("XN--MNCHEN-3YA.Example")
9338 .expect("mixed-case A-label must be accepted"),
9339 "xn--mnchen-3ya.example",
9340 "A-label patterns are ASCII-lowercased like any other pattern"
9341 );
9342 }
9343
9344 #[test]
9347 fn alpn_rejected_on_http_frontend() {
9348 let frontend = FileClusterFrontendConfig {
9349 address: "127.0.0.1:8080".parse().unwrap(),
9350 hostname: Some("example.com".to_owned()),
9351 alpn: vec!["h2".to_string()],
9352 path: None,
9353 path_type: None,
9354 method: None,
9355 certificate: None,
9356 key: None,
9357 certificate_chain: None,
9358 tls_versions: vec![],
9359 position: RulePosition::Tree,
9360 tags: None,
9361 redirect: None,
9362 redirect_scheme: None,
9363 redirect_template: None,
9364 rewrite_host: None,
9365 rewrite_path: None,
9366 rewrite_port: None,
9367 required_auth: None,
9368 headers: None,
9369 hsts: None,
9370 header_predicates: None,
9371 };
9372 match frontend.to_http_front("api") {
9373 Err(ConfigError::InvalidFrontendConfig(field)) => assert_eq!(field, "alpn"),
9374 other => panic!("expected InvalidFrontendConfig(\"alpn\"), got {other:?}"),
9375 }
9376 }
9377
9378 #[test]
9381 fn header_predicates_rejected_on_tcp_frontend() {
9382 let frontend = FileClusterFrontendConfig {
9383 address: "127.0.0.1:8080".parse().unwrap(),
9384 hostname: None,
9385 alpn: vec![],
9386 path: None,
9387 path_type: None,
9388 method: None,
9389 certificate: None,
9390 key: None,
9391 certificate_chain: None,
9392 tls_versions: vec![],
9393 position: RulePosition::Tree,
9394 tags: None,
9395 redirect: None,
9396 redirect_scheme: None,
9397 redirect_template: None,
9398 rewrite_host: None,
9399 rewrite_path: None,
9400 rewrite_port: None,
9401 required_auth: None,
9402 headers: None,
9403 hsts: None,
9404 header_predicates: Some(vec![]),
9405 };
9406 match frontend.to_tcp_front() {
9407 Err(ConfigError::InvalidFrontendConfig(field)) => {
9408 assert_eq!(field, "header_predicates")
9409 }
9410 other => panic!("expected InvalidFrontendConfig(\"header_predicates\"), got {other:?}"),
9411 }
9412 }
9413
9414 #[test]
9420 fn tcp_frontend_alpn_without_sni_rejected() {
9421 let toml_content = r#"
9422 command_socket = "/tmp/sozu_test.sock"
9423 worker_count = 1
9424
9425 [[listeners]]
9426 protocol = "tcp"
9427 address = "127.0.0.1:9019"
9428
9429 [clusters.a]
9430 protocol = "tcp"
9431 load_balancing = "ROUND_ROBIN"
9432 frontends = [
9433 { address = "127.0.0.1:9019", alpn = ["h2"] }
9434 ]
9435 backends = [ { address = "10.0.0.1:9019" } ]
9436 "#;
9437 let file_config: FileConfig =
9438 toml::from_str(toml_content).expect("Could not parse TOML config");
9439 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9440 match result {
9441 Err(ConfigError::AlpnWithoutSni { address }) => {
9442 assert_eq!(address.to_string(), "127.0.0.1:9019");
9443 }
9444 other => panic!("expected AlpnWithoutSni, got {other:?}"),
9445 }
9446 }
9447
9448 #[test]
9452 fn tcp_frontend_alpn_overlap_rejected() {
9453 let toml_content = r#"
9454 command_socket = "/tmp/sozu_test.sock"
9455 worker_count = 1
9456
9457 [[listeners]]
9458 protocol = "tcp"
9459 address = "127.0.0.1:9020"
9460
9461 [clusters.a]
9462 protocol = "tcp"
9463 load_balancing = "ROUND_ROBIN"
9464 frontends = [
9465 { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2"] }
9466 ]
9467 backends = [ { address = "10.0.0.1:9020" } ]
9468
9469 [clusters.b]
9470 protocol = "tcp"
9471 load_balancing = "ROUND_ROBIN"
9472 frontends = [
9473 { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2", "http/1.1"] }
9474 ]
9475 backends = [ { address = "10.0.0.2:9020" } ]
9476 "#;
9477 let file_config: FileConfig =
9478 toml::from_str(toml_content).expect("Could not parse TOML config");
9479 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9480 match result {
9481 Err(ConfigError::TcpFrontendAlpnOverlap { protocol, .. }) => {
9482 assert_eq!(protocol, "h2");
9483 }
9484 other => panic!("expected TcpFrontendAlpnOverlap, got {other:?}"),
9485 }
9486 }
9487
9488 #[test]
9492 fn tcp_frontend_multiple_alpn_catch_all_rejected() {
9493 let toml_content = r#"
9494 command_socket = "/tmp/sozu_test.sock"
9495 worker_count = 1
9496
9497 [[listeners]]
9498 protocol = "tcp"
9499 address = "127.0.0.1:9021"
9500
9501 [clusters.a]
9502 protocol = "tcp"
9503 load_balancing = "ROUND_ROBIN"
9504 frontends = [
9505 { address = "127.0.0.1:9021", hostname = "example.com" }
9506 ]
9507 backends = [ { address = "10.0.0.1:9021" } ]
9508
9509 [clusters.b]
9510 protocol = "tcp"
9511 load_balancing = "ROUND_ROBIN"
9512 frontends = [
9513 { address = "127.0.0.1:9021", hostname = "example.com" }
9514 ]
9515 backends = [ { address = "10.0.0.2:9021" } ]
9516 "#;
9517 let file_config: FileConfig =
9518 toml::from_str(toml_content).expect("Could not parse TOML config");
9519 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9520 assert!(
9521 matches!(
9522 result,
9523 Err(ConfigError::TcpFrontendMultipleAlpnCatchAll { .. })
9524 ),
9525 "expected TcpFrontendMultipleAlpnCatchAll, got {result:?}"
9526 );
9527 }
9528
9529 #[test]
9533 fn tcp_listener_mixes_sni_and_no_sni_rejected() {
9534 let toml_content = r#"
9535 command_socket = "/tmp/sozu_test.sock"
9536 worker_count = 1
9537
9538 [[listeners]]
9539 protocol = "tcp"
9540 address = "127.0.0.1:9022"
9541
9542 [clusters.a]
9543 protocol = "tcp"
9544 load_balancing = "ROUND_ROBIN"
9545 frontends = [
9546 { address = "127.0.0.1:9022", hostname = "example.com" }
9547 ]
9548 backends = [ { address = "10.0.0.1:9022" } ]
9549
9550 [clusters.b]
9551 protocol = "tcp"
9552 load_balancing = "ROUND_ROBIN"
9553 frontends = [
9554 { address = "127.0.0.1:9022" }
9555 ]
9556 backends = [ { address = "10.0.0.2:9022" } ]
9557 "#;
9558 let file_config: FileConfig =
9559 toml::from_str(toml_content).expect("Could not parse TOML config");
9560 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9561 assert!(
9562 matches!(result, Err(ConfigError::TcpListenerMixesSniAndNoSni { .. })),
9563 "expected TcpListenerMixesSniAndNoSni, got {result:?}"
9564 );
9565 }
9566
9567 #[test]
9572 fn sni_preread_timeout_exceeding_front_timeout_rejected() {
9573 let toml_content = r#"
9574 command_socket = "/tmp/sozu_test.sock"
9575 worker_count = 1
9576
9577 [[listeners]]
9578 protocol = "tcp"
9579 address = "127.0.0.1:9030"
9580 front_timeout = 2
9581
9582 [clusters.a]
9583 protocol = "tcp"
9584 load_balancing = "ROUND_ROBIN"
9585 frontends = [
9586 { address = "127.0.0.1:9030", hostname = "example.com" }
9587 ]
9588 backends = [ { address = "10.0.0.1:9030" } ]
9589 "#;
9590 let file_config: FileConfig =
9591 toml::from_str(toml_content).expect("Could not parse TOML config");
9592 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9593 match result {
9594 Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
9595 sni_preread_timeout: 5,
9596 front_timeout: 2,
9597 ..
9598 }) => {}
9599 other => panic!("expected SniPrereadTimeoutExceedsFrontTimeout, got {other:?}"),
9600 }
9601 }
9602
9603 #[test]
9607 fn sni_preread_max_bytes_exceeding_buffer_size_rejected() {
9608 let toml_content = r#"
9609 command_socket = "/tmp/sozu_test.sock"
9610 worker_count = 1
9611 buffer_size = 8192
9612
9613 [[listeners]]
9614 protocol = "tcp"
9615 address = "127.0.0.1:9031"
9616
9617 [clusters.a]
9618 protocol = "tcp"
9619 load_balancing = "ROUND_ROBIN"
9620 frontends = [
9621 { address = "127.0.0.1:9031", hostname = "example.com" }
9622 ]
9623 backends = [ { address = "10.0.0.1:9031" } ]
9624 "#;
9625 let file_config: FileConfig =
9626 toml::from_str(toml_content).expect("Could not parse TOML config");
9627 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9628 match result {
9629 Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
9630 sni_preread_max_bytes: 16384,
9631 buffer_size: 8192,
9632 ..
9633 }) => {}
9634 other => panic!("expected SniPrereadMaxBytesExceedsBufferSize, got {other:?}"),
9635 }
9636 }
9637
9638 #[test]
9643 fn sni_preread_max_bytes_zero_rejected() {
9644 let toml_content = r#"
9645 command_socket = "/tmp/sozu_test.sock"
9646 worker_count = 1
9647
9648 [[listeners]]
9649 protocol = "tcp"
9650 address = "127.0.0.1:9033"
9651 sni_preread_max_bytes = 0
9652
9653 [clusters.a]
9654 protocol = "tcp"
9655 load_balancing = "ROUND_ROBIN"
9656 frontends = [
9657 { address = "127.0.0.1:9033", hostname = "example.com" }
9658 ]
9659 backends = [ { address = "10.0.0.1:9033" } ]
9660 "#;
9661 let file_config: FileConfig =
9662 toml::from_str(toml_content).expect("Could not parse TOML config");
9663 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9664 match result {
9665 Err(ConfigError::SniPrereadMaxBytesTooSmall {
9666 sni_preread_max_bytes: 0,
9667 minimum: 5,
9668 ..
9669 }) => {}
9670 other => panic!("expected SniPrereadMaxBytesTooSmall, got {other:?}"),
9671 }
9672 }
9673
9674 #[test]
9677 fn sni_preread_max_bytes_at_the_floor_loads() {
9678 let toml_content = r#"
9679 command_socket = "/tmp/sozu_test.sock"
9680 worker_count = 1
9681
9682 [[listeners]]
9683 protocol = "tcp"
9684 address = "127.0.0.1:9034"
9685 sni_preread_max_bytes = 5
9686
9687 [clusters.a]
9688 protocol = "tcp"
9689 load_balancing = "ROUND_ROBIN"
9690 frontends = [
9691 { address = "127.0.0.1:9034", hostname = "example.com" }
9692 ]
9693 backends = [ { address = "10.0.0.1:9034" } ]
9694 "#;
9695 let file_config: FileConfig =
9696 toml::from_str(toml_content).expect("Could not parse TOML config");
9697 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9698 assert!(
9699 result.is_ok(),
9700 "sni_preread_max_bytes at the exact floor must load: {result:?}"
9701 );
9702 }
9703
9704 #[test]
9708 fn sni_preread_validation_ignored_without_sni_frontend() {
9709 let toml_content = r#"
9710 command_socket = "/tmp/sozu_test.sock"
9711 worker_count = 1
9712 buffer_size = 8192
9713
9714 [[listeners]]
9715 protocol = "tcp"
9716 address = "127.0.0.1:9032"
9717 front_timeout = 2
9718
9719 [clusters.a]
9720 protocol = "tcp"
9721 load_balancing = "ROUND_ROBIN"
9722 frontends = [
9723 { address = "127.0.0.1:9032" }
9724 ]
9725 backends = [ { address = "10.0.0.1:9032" } ]
9726 "#;
9727 let file_config: FileConfig =
9728 toml::from_str(toml_content).expect("Could not parse TOML config");
9729 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9730 assert!(
9731 result.is_ok(),
9732 "a no-SNI TCP listener must ignore sni_preread validation entirely: {result:?}"
9733 );
9734 }
9735
9736 #[test]
9743 fn tcp_frontend_disjoint_alpn_same_sni_both_load() {
9744 let toml_content = r#"
9745 command_socket = "/tmp/sozu_test.sock"
9746 worker_count = 1
9747
9748 [[listeners]]
9749 protocol = "tcp"
9750 address = "127.0.0.1:9040"
9751
9752 [clusters.h2_cluster]
9753 protocol = "tcp"
9754 load_balancing = "ROUND_ROBIN"
9755 frontends = [
9756 { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["h2"] }
9757 ]
9758 backends = [ { address = "10.0.0.1:9040" } ]
9759
9760 [clusters.http11_cluster]
9761 protocol = "tcp"
9762 load_balancing = "ROUND_ROBIN"
9763 frontends = [
9764 { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["http/1.1"] }
9765 ]
9766 backends = [ { address = "10.0.0.2:9040" } ]
9767 "#;
9768 let file_config: FileConfig =
9769 toml::from_str(toml_content).expect("Could not parse TOML config");
9770 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9771 .into_config()
9772 .expect("disjoint non-empty ALPN lists on the same (address, sni) must load");
9773
9774 let messages = config
9775 .generate_config_messages()
9776 .expect("Could not generate config messages");
9777 let mut frontends: Vec<_> = messages
9778 .iter()
9779 .filter_map(|m| match &m.content.request_type {
9780 Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
9781 _ => None,
9782 })
9783 .collect();
9784 frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
9785
9786 assert_eq!(frontends.len(), 2, "both frontends must be emitted");
9787 assert_eq!(frontends[0].cluster_id, "h2_cluster");
9788 assert_eq!(frontends[0].sni, Some("example.com".to_string()));
9789 assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
9790 assert_eq!(frontends[1].cluster_id, "http11_cluster");
9791 assert_eq!(frontends[1].sni, Some("example.com".to_string()));
9792 assert_eq!(frontends[1].alpn, vec!["http/1.1".to_string()]);
9793 }
9794
9795 #[test]
9811 fn udp_routed_tcp_frontends_are_excluded_from_sni_invariants() {
9812 let toml_content = r#"
9813 command_socket = "/tmp/sozu_test.sock"
9814 worker_count = 1
9815
9816 [[listeners]]
9817 protocol = "udp"
9818 address = "127.0.0.1:9050"
9819
9820 [clusters.a]
9821 protocol = "tcp"
9822 load_balancing = "ROUND_ROBIN"
9823 frontends = [
9824 { address = "127.0.0.1:9050", hostname = "example.com" }
9825 ]
9826 backends = [ { address = "10.0.0.1:9050" } ]
9827
9828 [clusters.b]
9829 protocol = "tcp"
9830 load_balancing = "ROUND_ROBIN"
9831 frontends = [
9832 { address = "127.0.0.1:9050" }
9833 ]
9834 backends = [ { address = "10.0.0.2:9050" } ]
9835 "#;
9836 let file_config: FileConfig =
9837 toml::from_str(toml_content).expect("Could not parse TOML config");
9838 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9839 assert!(
9840 result.is_ok(),
9841 "UDP-routed tcp-cluster frontends must not trip the SNI mixing-ban: {result:?}"
9842 );
9843
9844 let config = result.expect("checked is_ok above");
9845 let messages = config
9846 .generate_config_messages()
9847 .expect("Could not generate config messages");
9848 let add_udp_frontend_count = messages
9849 .iter()
9850 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
9851 .count();
9852 assert_eq!(
9853 add_udp_frontend_count, 2,
9854 "both cluster frontends on the udp listener must emit AddUdpFrontend"
9855 );
9856 }
9857
9858 #[test]
9863 fn subnet_connection_limit_keys_round_trip_from_toml() {
9864 let toml_content = r#"
9865 command_socket = "/tmp/sozu.sock"
9866 saved_state = "./state.json"
9867 worker_count = 1
9868
9869 max_connections_per_ip = 10
9870 max_connections_per_subnet = 100
9871 subnet_ipv4_prefix = 24
9872 subnet_ipv6_prefix = 56
9873
9874 [[listeners]]
9875 protocol = "http"
9876 address = "127.0.0.1:8080"
9877
9878 [clusters.api]
9879 protocol = "http"
9880 load_balancing = "ROUND_ROBIN"
9881 max_connections_per_subnet = 7
9882 frontends = [
9883 { address = "127.0.0.1:8080", hostname = "example.com" }
9884 ]
9885 backends = [ { address = "10.0.0.1:8080" } ]
9886 "#;
9887 let file_config: FileConfig =
9888 toml::from_str(toml_content).expect("Could not parse TOML config");
9889 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9890 .into_config()
9891 .expect("a config with valid subnet prefixes must load");
9892
9893 assert_eq!(
9894 config.max_connections_per_subnet, 100,
9895 "the global subnet cap must survive the TOML round trip"
9896 );
9897 assert_eq!(
9898 config.subnet_ipv4_prefix, 24,
9899 "subnet_ipv4_prefix must survive the TOML round trip"
9900 );
9901 assert_eq!(
9902 config.subnet_ipv6_prefix, 56,
9903 "subnet_ipv6_prefix must survive the TOML round trip — a swap with the IPv6 \
9904 key would leave a cap that silently never fires"
9905 );
9906 assert_eq!(
9907 config.max_connections_per_ip, 10,
9908 "the pre-existing per-IP cap must be unaffected: the two are independent"
9909 );
9910
9911 let cluster = config
9912 .clusters
9913 .get("api")
9914 .expect("the staged cluster must be present");
9915 let override_value = match cluster {
9916 ClusterConfig::Http(http) => http.max_connections_per_subnet,
9917 other => panic!("expected an HTTP cluster, got {other:?}"),
9918 };
9919 assert_eq!(
9920 override_value,
9921 Some(7),
9922 "the per-cluster subnet override must reach the cluster config"
9923 );
9924 }
9925
9926 #[test]
9930 fn max_connection_attempts_defaults_to_five_and_round_trips_from_toml() {
9931 let config_with = |global: &str, cluster: &str| -> Config {
9932 let toml_content = format!(
9933 r#"
9934 command_socket = "/tmp/sozu.sock"
9935 saved_state = "./state.json"
9936 worker_count = 1
9937 {global}
9938
9939 [[listeners]]
9940 protocol = "http"
9941 address = "127.0.0.1:8080"
9942
9943 [clusters.api]
9944 protocol = "http"
9945 load_balancing = "ROUND_ROBIN"
9946 {cluster}
9947 frontends = [
9948 {{ address = "127.0.0.1:8080", hostname = "example.com" }}
9949 ]
9950 backends = [ {{ address = "10.0.0.1:8080" }} ]
9951 "#
9952 );
9953 let file_config: FileConfig =
9954 toml::from_str(&toml_content).expect("Could not parse TOML config");
9955 ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9956 .into_config()
9957 .expect("a valid attempt budget must load")
9958 };
9959 let cluster_attempts = |config: &Config| match config.clusters.get("api") {
9960 Some(ClusterConfig::Http(http)) => http.max_connection_attempts,
9961 other => panic!("expected the HTTP cluster, got {other:?}"),
9962 };
9963 let add_cluster_attempts = |config: &Config| {
9964 config
9965 .generate_config_messages()
9966 .expect("Could not generate config messages")
9967 .into_iter()
9968 .find_map(|message| match message.content.request_type {
9969 Some(RequestType::AddCluster(cluster)) => Some(cluster.max_connection_attempts),
9970 _ => None,
9971 })
9972 .expect("the cluster is added")
9973 };
9974
9975 let defaults = config_with("", "");
9976 assert_eq!(DEFAULT_MAX_CONNECTION_ATTEMPTS, 5);
9977 assert_eq!(defaults.max_connection_attempts, 5);
9978 assert_eq!(
9979 ServerConfig::from(&defaults).max_connection_attempts,
9980 Some(5),
9981 "the worker receives the default"
9982 );
9983 assert_eq!(
9984 cluster_attempts(&defaults),
9985 None,
9986 "a cluster inherits by default"
9987 );
9988 assert_eq!(add_cluster_attempts(&defaults), None);
9989
9990 let set = config_with("max_connection_attempts = 2", "max_connection_attempts = 7");
9991 assert_eq!(set.max_connection_attempts, 2);
9992 assert_eq!(ServerConfig::from(&set).max_connection_attempts, Some(2));
9993 assert_eq!(cluster_attempts(&set), Some(7));
9994 assert_eq!(add_cluster_attempts(&set), Some(7));
9995 }
9996
9997 #[test]
10001 fn an_out_of_range_max_connection_attempts_is_rejected() {
10002 let load = |global: &str, cluster: &str| {
10003 let toml_content = format!(
10004 r#"
10005 command_socket = "/tmp/sozu.sock"
10006 saved_state = "./state.json"
10007 worker_count = 1
10008 {global}
10009
10010 [[listeners]]
10011 protocol = "http"
10012 address = "127.0.0.1:8080"
10013
10014 [clusters.api]
10015 protocol = "http"
10016 load_balancing = "ROUND_ROBIN"
10017 {cluster}
10018 frontends = [
10019 {{ address = "127.0.0.1:8080", hostname = "example.com" }}
10020 ]
10021 backends = [ {{ address = "10.0.0.1:8080" }} ]
10022 "#
10023 );
10024 let file_config: FileConfig =
10025 toml::from_str(&toml_content).expect("Could not parse TOML config");
10026 ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config()
10027 };
10028 for value in ["0", "256"] {
10029 let global = format!("max_connection_attempts = {value}");
10030 assert!(
10031 matches!(
10032 load(&global, ""),
10033 Err(ConfigError::InvalidMaxConnectionAttempts {
10034 cluster_id: None,
10035 ..
10036 })
10037 ),
10038 "a global budget of {value} must be rejected"
10039 );
10040 assert!(
10041 matches!(
10042 load("", &global),
10043 Err(ConfigError::InvalidMaxConnectionAttempts {
10044 cluster_id: Some(_),
10045 ..
10046 })
10047 ),
10048 "a cluster budget of {value} must be rejected"
10049 );
10050 }
10051 for value in ["1", "255"] {
10052 let budget = format!("max_connection_attempts = {value}");
10053 assert!(load(&budget, &budget).is_ok(), "{value} is in range");
10054 }
10055 }
10056
10057 #[test]
10061 fn an_out_of_range_subnet_prefix_is_rejected_not_clamped() {
10062 let with_prefixes = |v4: &str, v6: &str| -> String {
10063 format!(
10064 r#"
10065 command_socket = "/tmp/sozu.sock"
10066 saved_state = "./state.json"
10067 worker_count = 1
10068
10069 max_connections_per_subnet = 100
10070 subnet_ipv4_prefix = {v4}
10071 subnet_ipv6_prefix = {v6}
10072
10073 [[listeners]]
10074 protocol = "http"
10075 address = "127.0.0.1:8080"
10076 "#
10077 )
10078 };
10079
10080 for (v4, v6, key) in [
10081 ("33", "56", "subnet_ipv4_prefix"),
10082 ("24", "129", "subnet_ipv6_prefix"),
10083 ] {
10084 let file_config: FileConfig =
10085 toml::from_str(&with_prefixes(v4, v6)).expect("Could not parse TOML config");
10086 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
10087 match result {
10088 Err(ConfigError::InvalidSubnetPrefix { key: reported, .. }) => {
10089 assert_eq!(
10090 reported, key,
10091 "the rejection must name the key that is out of range"
10092 );
10093 }
10094 other => panic!("{key} out of range must be rejected, got {other:?}"),
10095 }
10096 }
10097
10098 let file_config: FileConfig =
10101 toml::from_str(&with_prefixes("32", "128")).expect("Could not parse TOML config");
10102 assert!(
10103 ConfigBuilder::new(file_config, "/tmp/test_config.toml")
10104 .into_config()
10105 .is_ok(),
10106 "the full-width prefixes are legal and are the defaults — they must load"
10107 );
10108 }
10109
10110 #[test]
10111 fn old_state_file_without_client_auth_fields_deserializes() {
10112 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
10117 let config = ListenerBuilder::new_https(address)
10118 .to_tls(None)
10119 .expect("default HTTPS listener config");
10120
10121 let mut json: serde_json::Value =
10122 serde_json::to_value(&config).expect("serialize HttpsListenerConfig");
10123 let obj = json.as_object_mut().expect("object");
10124 obj.remove("client_ca_certificates");
10125 obj.remove("client_ca_crls");
10126 obj.remove("client_auth");
10127
10128 let restored: HttpsListenerConfig =
10129 serde_json::from_value(json).expect("old state file must deserialize");
10130 assert!(restored.client_ca_certificates.is_empty());
10131 assert!(restored.client_ca_crls.is_empty());
10132 assert_eq!(restored.client_auth, None);
10133 }
10134
10135 #[test]
10136 fn client_auth_fields_rejected_on_non_https_listener() {
10137 let address = SocketAddress::new_v4(127, 0, 0, 1, 9000);
10140 let mut tcp = ListenerBuilder::new_tcp(address);
10141 tcp.client_auth = Some(ClientAuthConfig::Required);
10142 assert!(matches!(
10143 tcp.to_tcp(None),
10144 Err(ConfigError::ClientAuthOnNonHttps(_))
10145 ));
10146
10147 let mut http = ListenerBuilder::new_http(address);
10149 http.client_ca_certificates = Some(vec!["ca.pem".to_owned()]);
10150 assert!(matches!(
10151 http.to_http(None),
10152 Err(ConfigError::ClientAuthOnNonHttps(_))
10153 ));
10154 }
10155
10156 #[test]
10157 fn none_mode_ignores_stale_ca_paths() {
10158 let address = SocketAddress::new_v4(127, 0, 0, 1, 9443);
10161 let mut https = ListenerBuilder::new_https(address);
10162 https.client_auth = Some(ClientAuthConfig::None);
10163 https.client_ca_certificates = Some(vec!["/nonexistent/ca.pem".to_owned()]);
10164 https.client_ca_crls = Some(vec!["/nonexistent/crl.pem".to_owned()]);
10165 let config = https.to_tls(None).expect("NONE mode must ignore CA paths");
10166 assert!(config.client_ca_certificates.is_empty());
10167 assert!(config.client_ca_crls.is_empty());
10168 }
10169
10170 #[test]
10174 fn client_auth_listener_inlines_its_ca_and_crl_files_into_the_listener_request() {
10175 let asset =
10176 |name: &str| format!("{}/../lib/assets/mtls/{name}", env!("CARGO_MANIFEST_DIR"));
10177 let (ca_path, crl_path) = (asset("ca-cert.pem"), asset("crl-revoked.pem"));
10178 let toml_content = format!(
10179 r#"
10180 command_socket = "/tmp/sozu.sock"
10181 saved_state = "./state.json"
10182 worker_count = 1
10183
10184 [[listeners]]
10185 protocol = "https"
10186 address = "127.0.0.1:8443"
10187 client_auth = "required"
10188 client_ca_certificates = ["{ca_path}"]
10189 client_ca_crls = ["{crl_path}"]
10190 "#
10191 );
10192 let file_config: FileConfig =
10193 toml::from_str(&toml_content).expect("Could not parse TOML config");
10194 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
10195 .into_config()
10196 .expect("a client auth listener with readable CA and CRL files must load");
10197
10198 let listeners: Vec<HttpsListenerConfig> = config
10199 .generate_config_messages()
10200 .expect("the config must produce its worker requests")
10201 .into_iter()
10202 .filter_map(|request| match request.content.request_type {
10203 Some(RequestType::AddHttpsListenerWithClientAuth(listener)) => Some(listener),
10204 Some(RequestType::AddHttpsListener(_)) => {
10205 panic!("a client auth listener must not go out as AddHttpsListener")
10206 }
10207 _ => None,
10208 })
10209 .collect();
10210 assert_eq!(listeners.len(), 1, "one HTTPS listener is configured");
10211 let listener = &listeners[0];
10212 assert_eq!(
10213 listener.client_auth,
10214 Some(ClientAuthMode::ClientAuthRequired as i32)
10215 );
10216 assert_eq!(
10217 listener.client_ca_certificates,
10218 vec![std::fs::read_to_string(&ca_path).expect("read the CA fixture")],
10219 "the CA file content, not its path, must reach the worker"
10220 );
10221 assert_eq!(
10222 listener.client_ca_crls,
10223 vec![std::fs::read_to_string(&crl_path).expect("read the CRL fixture")],
10224 "the CRL file content, not its path, must reach the worker"
10225 );
10226 assert!(
10227 listener.client_ca_crls[0].contains("BEGIN X509 CRL"),
10228 "the inlined CRL is the PEM body"
10229 );
10230 }
10231
10232 #[test]
10233 fn client_auth_listener_with_an_unreadable_crl_file_does_not_load() {
10234 let address = SocketAddress::new_v4(127, 0, 0, 1, 9443);
10237 let mut https = ListenerBuilder::new_https(address);
10238 https.client_auth = Some(ClientAuthConfig::Required);
10239 https.client_ca_certificates = Some(vec![format!(
10240 "{}/../lib/assets/mtls/ca-cert.pem",
10241 env!("CARGO_MANIFEST_DIR")
10242 )]);
10243 https.client_ca_crls = Some(vec!["/nonexistent/crl.pem".to_owned()]);
10244 assert!(
10245 https.to_tls(None).is_err(),
10246 "an unreadable CRL file must fail the listener, not drop the revocation check"
10247 );
10248 }
10249
10250 #[test]
10251 fn client_auth_files_without_a_mode_are_rejected() {
10252 let address = SocketAddress::new_v4(127, 0, 0, 1, 9443);
10255 for (cas, crls) in [
10256 (Some(vec!["/etc/sozu/client-ca.pem".to_owned()]), None),
10257 (None, Some(vec!["/etc/sozu/client-ca.crl.pem".to_owned()])),
10258 ] {
10259 let mut https = ListenerBuilder::new_https(address);
10260 https.client_ca_certificates = cas;
10261 https.client_ca_crls = crls;
10262 assert!(matches!(
10263 https.to_tls(None),
10264 Err(ConfigError::ClientAuthModeMissing(_))
10265 ));
10266 }
10267 let mut explicit_none = ListenerBuilder::new_https(address);
10270 explicit_none.client_auth = Some(ClientAuthConfig::None);
10271 explicit_none.client_ca_certificates = Some(vec!["/nonexistent/ca.pem".to_owned()]);
10272 assert!(explicit_none.to_tls(None).is_ok());
10273 assert!(ListenerBuilder::new_https(address).to_tls(None).is_ok());
10274 }
10275
10276 #[test]
10277 fn client_auth_config_parses_documented_lowercase_names() {
10278 assert_eq!(
10281 serde_json::from_str::<ClientAuthConfig>("\"required\"").unwrap(),
10282 ClientAuthConfig::Required
10283 );
10284 assert_eq!(
10285 serde_json::from_str::<ClientAuthConfig>("\"optional\"").unwrap(),
10286 ClientAuthConfig::Optional
10287 );
10288 assert_eq!(
10289 serde_json::from_str::<ClientAuthConfig>("\"none\"").unwrap(),
10290 ClientAuthConfig::None
10291 );
10292 assert_eq!(
10293 ClientAuthMode::from(ClientAuthConfig::Required),
10294 ClientAuthMode::ClientAuthRequired
10295 );
10296 }
10297}