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} listener(s) serve HTTP/2 (an HTTPS listener advertising H2 \
537 ALPN, or an HTTP listener with h2c_prior_knowledge). The H2 mux deadlocks \
538 on full-size frames with smaller buffers. Raise buffer_size to >= {minimum}, \
539 or remove \"h2\" from those HTTPS listeners' alpn_protocols and turn \
540 h2c_prior_knowledge off on those HTTP listeners."
541 )]
542 BufferSizeTooSmallForH2 {
543 buffer_size: u64,
544 minimum: u64,
545 listeners: usize,
546 },
547 #[error(
555 "command_buffer_size = {command_buffer_size} exceeds max_command_buffer_size = \
556 {max_command_buffer_size}: lower command_buffer_size to <= {max_command_buffer_size} \
557 or raise max_command_buffer_size to >= {command_buffer_size}"
558 )]
559 CommandBufferSizeExceedsMax {
560 command_buffer_size: u64,
561 max_command_buffer_size: u64,
562 },
563 #[error(
606 "listener {address}: {key} = {value} is below the minimum of {minimum} the runtime can \
607 honour. Below the minimum the value does not relax the limit, it breaks it: the H2 flood \
608 detector compares count > threshold, so 0 is already exceeded the first time the counter \
609 it guards leaves zero. How far that reaches depends on the key — \
610 h2_max_header_list_size and h2_max_header_fields are also the HPACK decode budget, so 0 \
611 refuses every request on this listener with ENHANCE_YOUR_CALM (as a stream reset, or as \
612 a connection GOAWAY once a header block spans CONTINUATION frames), while a per-window \
613 or lifetime frame counter only trips once a client sends the frame it counts, and \
614 h2_max_concurrent_streams / h2_stream_shrink_ratio are instead promoted to their floor \
615 behind a warning. Raise {key} to >= {minimum}, or remove it to keep the default."
616 )]
617 H2ThresholdBelowMinimum {
618 address: String,
619 key: &'static str,
620 value: u64,
621 minimum: u64,
622 },
623 #[error(
627 "invalid redirect policy '{0}'. Valid values: \"forward\", \"permanent\", \"unauthorized\", \"silent_close\""
628 )]
629 InvalidRedirectPolicy(String),
630 #[error(
634 "invalid redirect scheme '{0}'. Valid values: \"use-same\", \"use-http\", \"use-https\""
635 )]
636 InvalidRedirectScheme(String),
637 #[error(
641 "invalid header position '{position}' at headers[{index}]. Valid values: \"request\", \"response\", \"both\""
642 )]
643 InvalidHeaderPosition { index: usize, position: String },
644 #[error(
651 "invalid header bytes in {field} at headers[{index}]: control characters \
652 (NUL / CR / LF / other C0) are forbidden in header keys and values"
653 )]
654 InvalidHeaderBytes { index: usize, field: &'static str },
655 #[error("invalid header predicate at header_predicates[{index}]: {reason}")]
660 InvalidHeaderPredicate { index: usize, reason: &'static str },
661 #[error("invalid HSTS config at {0}: `enabled` is required when an [hsts] block is present")]
667 HstsEnabledRequired(String),
668 #[error(
673 "invalid HSTS config at {0}: HSTS is only valid on HTTPS listeners and frontends \
674 (RFC 6797 §7.2 forbids the header over plaintext HTTP)"
675 )]
676 HstsOnPlainHttp(String),
677 #[error(
682 "invalid client auth config at {0}: client_auth / client_ca_certificates / \
683 client_ca_crls are only valid on HTTPS listeners"
684 )]
685 ClientAuthOnNonHttps(String),
686 #[error(
690 "invalid client auth config at {0}: client_ca_certificates / client_ca_crls are set \
691 but client_auth is not; set client_auth to \"optional\" or \"required\", or to \
692 \"none\" to keep them unused"
693 )]
694 ClientAuthModeMissing(String),
695 #[error(
702 "invalid SNI pattern '{sni}' for a TCP frontend: expected an exact hostname or a \
703 single leading \"*.\" wildcard label (e.g. \"example.com\" or \"*.example.com\"); \
704 '/' and non-leading '*' are rejected"
705 )]
706 InvalidSniPattern { sni: String },
707 #[error(
714 "non-ASCII SNI pattern '{sni}' for a TCP frontend: on-wire SNI is always an ASCII \
715 A-label (RFC 6066), so this pattern would never match a ClientHello. Write the \
716 punycode A-label form instead (e.g. \"xn--mnchen-3ya.example\" for \
717 \"münchen.example\")"
718 )]
719 NonAsciiSniPattern { sni: String },
720 #[error(
729 "TCP frontend {address} sets alpn but no hostname (sni): alpn only matches within an \
730 SNI-scoped preread, so a frontend without hostname would silently ignore its alpn list. \
731 Set hostname or drop alpn."
732 )]
733 AlpnWithoutSni { address: SocketAddr },
734 #[error(
740 "TCP frontends on {address} with sni {sni:?} both match ALPN protocol '{protocol}': \
741 ALPN matchers for the same (address, sni) must not overlap"
742 )]
743 TcpFrontendAlpnOverlap {
744 address: SocketAddr,
745 sni: Option<String>,
746 protocol: String,
747 },
748 #[error(
753 "more than one TCP frontend on {address} with sni {sni:?} leaves alpn empty (the \
754 catch-all match): at most one frontend per (address, sni) may omit alpn"
755 )]
756 TcpFrontendMultipleAlpnCatchAll {
757 address: SocketAddr,
758 sni: Option<String>,
759 },
760 #[error(
767 "TCP listener {address} is targeted by both a no-SNI frontend and at least one \
768 SNI-scoped frontend: an SNI-enabled listener must not also have a raw-TCP fallback \
769 frontend on the same address"
770 )]
771 TcpListenerMixesSniAndNoSni { address: SocketAddr },
772 #[error(
778 "sni_preread_timeout = {sni_preread_timeout}s on TCP listener {address} exceeds its \
779 front_timeout = {front_timeout}s: the preread phase cannot outlive the frontend \
780 inactivity timeout that would already have closed the connection"
781 )]
782 SniPrereadTimeoutExceedsFrontTimeout {
783 address: SocketAddr,
784 sni_preread_timeout: u32,
785 front_timeout: u32,
786 },
787 #[error(
793 "sni_preread_max_bytes = {sni_preread_max_bytes} on TCP listener {address} exceeds \
794 buffer_size = {buffer_size}: raise buffer_size to >= {sni_preread_max_bytes} or lower \
795 sni_preread_max_bytes"
796 )]
797 SniPrereadMaxBytesExceedsBufferSize {
798 address: SocketAddr,
799 sni_preread_max_bytes: u32,
800 buffer_size: u64,
801 },
802 #[error(
812 "sni_preread_max_bytes = {sni_preread_max_bytes} on TCP listener {address} is below the \
813 minimum of {minimum} bytes (a full TLS record header): the preread shell could never \
814 read enough bytes to make progress. Raise sni_preread_max_bytes to >= {minimum}"
815 )]
816 SniPrereadMaxBytesTooSmall {
817 address: SocketAddr,
818 sni_preread_max_bytes: u32,
819 minimum: u32,
820 },
821 #[error(
826 "cluster {cluster_id}: cannot resolve backend address {address:?}: {reason}. \
827 Expected an IP:port socket address or a resolvable host:port"
828 )]
829 BackendAddressResolution {
830 cluster_id: String,
831 address: String,
832 reason: String,
833 },
834}
835
836#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
842#[serde(rename_all = "lowercase")]
843pub enum ClientAuthConfig {
844 #[default]
845 None,
846 Optional,
847 Required,
848}
849
850impl From<ClientAuthConfig> for ClientAuthMode {
851 fn from(value: ClientAuthConfig) -> Self {
852 match value {
853 ClientAuthConfig::None => ClientAuthMode::ClientAuthNone,
854 ClientAuthConfig::Optional => ClientAuthMode::ClientAuthOptional,
855 ClientAuthConfig::Required => ClientAuthMode::ClientAuthRequired,
856 }
857 }
858}
859
860#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
862#[serde(deny_unknown_fields)]
863pub struct ListenerBuilder {
864 pub address: SocketAddr,
865 pub interface: Option<String>,
871 pub protocol: Option<ListenerProtocol>,
872 pub public_address: Option<SocketAddr>,
873 pub answer_301: Option<String>,
874 pub answer_400: Option<String>,
875 pub answer_401: Option<String>,
876 pub answer_404: Option<String>,
877 pub answer_408: Option<String>,
878 pub answer_413: Option<String>,
879 pub answer_421: Option<String>,
882 pub answer_502: Option<String>,
883 pub answer_503: Option<String>,
884 pub answer_504: Option<String>,
885 pub answer_507: Option<String>,
886 pub answer_429: Option<String>,
891 pub tls_versions: Option<Vec<TlsVersion>>,
892 pub cipher_list: Option<Vec<String>>,
893 pub cipher_suites: Option<Vec<String>>,
894 pub groups_list: Option<Vec<String>>,
895 pub expect_proxy: Option<bool>,
896 #[serde(default = "default_sticky_name")]
897 pub sticky_name: String,
898 pub certificate: Option<String>,
899 pub certificate_chain: Option<String>,
900 pub key: Option<String>,
901 pub front_timeout: Option<u32>,
903 pub back_timeout: Option<u32>,
905 pub connect_timeout: Option<u32>,
907 pub request_timeout: Option<u32>,
909 pub config: Option<Config>,
911 pub send_tls13_tickets: Option<u64>,
915 pub alpn_protocols: Option<Vec<String>>,
918 pub h2_max_rst_stream_per_window: Option<u32>,
920 pub h2_max_ping_per_window: Option<u32>,
922 pub h2_max_settings_per_window: Option<u32>,
924 pub h2_max_empty_data_per_window: Option<u32>,
926 pub h2_max_window_update_stream0_per_window: Option<u32>,
932 pub sozu_id_header: Option<String>,
936 pub h2_max_continuation_frames: Option<u32>,
938 pub h2_max_glitch_count: Option<u32>,
941 pub h2_initial_connection_window: Option<u32>,
943 pub h2_max_concurrent_streams: Option<u32>,
945 pub h2_stream_shrink_ratio: Option<u32>,
947 pub h2_max_rst_stream_lifetime: Option<u64>,
952 pub h2_max_rst_stream_abusive_lifetime: Option<u64>,
958 pub h2_max_rst_stream_emitted_lifetime: Option<u64>,
965 pub h2_max_header_list_size: Option<u32>,
969 pub h2_max_header_table_size: Option<u32>,
973 pub h2_max_header_fields: Option<u32>,
977 pub h2_stream_refusal_percent: Option<u32>,
984 pub h2_stream_idle_timeout_seconds: Option<u32>,
990 pub h2_graceful_shutdown_deadline_seconds: Option<u32>,
995 pub strict_sni_binding: Option<bool>,
1001 pub disable_http11: Option<bool>,
1006 pub elide_x_real_ip: Option<bool>,
1010 pub send_x_real_ip: Option<bool>,
1015 pub h2c_prior_knowledge: Option<bool>,
1020 pub forwarded_headers: Option<ForwardedHeadersMode>,
1024 pub answers: Option<BTreeMap<String, String>>,
1039 pub hsts: Option<FileHstsConfig>,
1045 pub max_rx_datagram_size: Option<u32>,
1049 pub max_flows: Option<u32>,
1054 pub sni_preread_timeout: Option<u32>,
1059 pub sni_preread_max_bytes: Option<u32>,
1065 pub client_auth: Option<ClientAuthConfig>,
1069 pub client_ca_certificates: Option<Vec<String>>,
1074 pub client_ca_crls: Option<Vec<String>>,
1078}
1079
1080pub fn default_sticky_name() -> String {
1081 DEFAULT_STICKY_NAME.to_string()
1082}
1083
1084impl ListenerBuilder {
1085 pub fn new_http(address: SocketAddress) -> ListenerBuilder {
1088 Self::new(address, ListenerProtocol::Http)
1089 }
1090
1091 pub fn new_tcp(address: SocketAddress) -> ListenerBuilder {
1094 Self::new(address, ListenerProtocol::Tcp)
1095 }
1096
1097 pub fn new_https(address: SocketAddress) -> ListenerBuilder {
1100 Self::new(address, ListenerProtocol::Https)
1101 }
1102
1103 pub fn new_udp(address: SocketAddress) -> ListenerBuilder {
1106 Self::new(address, ListenerProtocol::Udp)
1107 }
1108
1109 fn new(address: SocketAddress, protocol: ListenerProtocol) -> ListenerBuilder {
1111 ListenerBuilder {
1112 address: address.into(),
1113 interface: None,
1114 answer_301: None,
1115 answer_401: None,
1116 answer_400: None,
1117 answer_404: None,
1118 answer_408: None,
1119 answer_413: None,
1120 answer_421: None,
1121 answer_502: None,
1122 answer_503: None,
1123 answer_504: None,
1124 answer_507: None,
1125 answer_429: None,
1126 back_timeout: None,
1127 certificate_chain: None,
1128 certificate: None,
1129 cipher_list: None,
1130 cipher_suites: None,
1131 groups_list: None,
1132 config: None,
1133 connect_timeout: None,
1134 expect_proxy: None,
1135 front_timeout: None,
1136 key: None,
1137 protocol: Some(protocol),
1138 public_address: None,
1139 request_timeout: None,
1140 send_tls13_tickets: None,
1141 sticky_name: DEFAULT_STICKY_NAME.to_string(),
1142 tls_versions: None,
1143 alpn_protocols: None,
1144 h2_max_rst_stream_per_window: None,
1145 h2_max_ping_per_window: None,
1146 h2_max_settings_per_window: None,
1147 h2_max_empty_data_per_window: None,
1148 h2_max_window_update_stream0_per_window: None,
1149 sozu_id_header: None,
1150 h2_max_continuation_frames: None,
1151 h2_max_glitch_count: None,
1152 h2_initial_connection_window: None,
1153 h2_max_concurrent_streams: None,
1154 h2_stream_shrink_ratio: None,
1155 h2_max_rst_stream_lifetime: None,
1156 h2_max_rst_stream_abusive_lifetime: None,
1157 h2_max_rst_stream_emitted_lifetime: None,
1158 h2_max_header_list_size: None,
1159 h2_max_header_table_size: None,
1160 h2_max_header_fields: None,
1161 h2_stream_refusal_percent: None,
1162 h2_stream_idle_timeout_seconds: None,
1163 h2_graceful_shutdown_deadline_seconds: None,
1164 strict_sni_binding: None,
1165 disable_http11: None,
1166 elide_x_real_ip: None,
1167 send_x_real_ip: None,
1168 h2c_prior_knowledge: None,
1169 forwarded_headers: None,
1170 answers: None,
1171 hsts: None,
1172 max_rx_datagram_size: None,
1173 max_flows: None,
1174 sni_preread_timeout: None,
1175 sni_preread_max_bytes: None,
1176 client_auth: None,
1177 client_ca_certificates: None,
1178 client_ca_crls: None,
1179 }
1180 }
1181
1182 pub fn with_interface<S>(&mut self, interface: Option<S>) -> &mut Self
1184 where
1185 S: ToString,
1186 {
1187 self.interface = interface.map(|interface| interface.to_string());
1188 self
1189 }
1190
1191 fn validated_interface(&self) -> Result<Option<String>, ConfigError> {
1194 if let Some(interface) = &self.interface {
1195 validate_listener_interface(interface).map_err(|error| {
1196 ConfigError::ListenerInterface {
1197 address: self.address,
1198 error,
1199 }
1200 })?;
1201 }
1202 Ok(self.interface.clone())
1203 }
1204
1205 pub fn with_public_address(&mut self, public_address: Option<SocketAddr>) -> &mut Self {
1206 if let Some(address) = public_address {
1207 self.public_address = Some(address);
1208 }
1209 self
1210 }
1211
1212 pub fn with_answer_404_path<S>(&mut self, answer_404_path: Option<S>) -> &mut Self
1213 where
1214 S: ToString,
1215 {
1216 if let Some(path) = answer_404_path {
1217 self.answer_404 = Some(path.to_string());
1218 }
1219 self
1220 }
1221
1222 pub fn with_answer_503_path<S>(&mut self, answer_503_path: Option<S>) -> &mut Self
1223 where
1224 S: ToString,
1225 {
1226 if let Some(path) = answer_503_path {
1227 self.answer_503 = Some(path.to_string());
1228 }
1229 self
1230 }
1231
1232 pub fn with_tls_versions(&mut self, tls_versions: Vec<TlsVersion>) -> &mut Self {
1233 self.tls_versions = Some(tls_versions);
1234 self
1235 }
1236
1237 pub fn with_cipher_list(&mut self, cipher_list: Option<Vec<String>>) -> &mut Self {
1238 self.cipher_list = cipher_list;
1239 self
1240 }
1241
1242 pub fn with_cipher_suites(&mut self, cipher_suites: Option<Vec<String>>) -> &mut Self {
1243 self.cipher_suites = cipher_suites;
1244 self
1245 }
1246
1247 pub fn with_alpn_protocols(&mut self, alpn_protocols: Option<Vec<String>>) -> &mut Self {
1248 self.alpn_protocols = alpn_protocols;
1249 self
1250 }
1251
1252 pub fn with_elide_x_real_ip(&mut self, elide_x_real_ip: bool) -> &mut Self {
1255 self.elide_x_real_ip = Some(elide_x_real_ip);
1256 self
1257 }
1258
1259 pub fn with_send_x_real_ip(&mut self, send_x_real_ip: bool) -> &mut Self {
1263 self.send_x_real_ip = Some(send_x_real_ip);
1264 self
1265 }
1266
1267 pub fn with_h2c_prior_knowledge(&mut self, h2c_prior_knowledge: bool) -> &mut Self {
1271 self.h2c_prior_knowledge = Some(h2c_prior_knowledge);
1272 self
1273 }
1274
1275 pub fn with_forwarded_headers(&mut self, forwarded_headers: ForwardedHeadersMode) -> &mut Self {
1278 self.forwarded_headers = Some(forwarded_headers);
1279 self
1280 }
1281
1282 pub fn with_expect_proxy(&mut self, expect_proxy: bool) -> &mut Self {
1283 self.expect_proxy = Some(expect_proxy);
1284 self
1285 }
1286
1287 pub fn with_sticky_name<S>(&mut self, sticky_name: Option<S>) -> &mut Self
1288 where
1289 S: ToString,
1290 {
1291 if let Some(name) = sticky_name {
1292 self.sticky_name = name.to_string();
1293 }
1294 self
1295 }
1296
1297 pub fn with_certificate<S>(&mut self, certificate: S) -> &mut Self
1298 where
1299 S: ToString,
1300 {
1301 self.certificate = Some(certificate.to_string());
1302 self
1303 }
1304
1305 pub fn with_certificate_chain(&mut self, certificate_chain: String) -> &mut Self {
1306 self.certificate = Some(certificate_chain);
1307 self
1308 }
1309
1310 pub fn with_key<S>(&mut self, key: String) -> &mut Self
1311 where
1312 S: ToString,
1313 {
1314 self.key = Some(key);
1315 self
1316 }
1317
1318 pub fn with_front_timeout(&mut self, front_timeout: Option<u32>) -> &mut Self {
1319 self.front_timeout = front_timeout;
1320 self
1321 }
1322
1323 pub fn with_back_timeout(&mut self, back_timeout: Option<u32>) -> &mut Self {
1324 self.back_timeout = back_timeout;
1325 self
1326 }
1327
1328 pub fn with_connect_timeout(&mut self, connect_timeout: Option<u32>) -> &mut Self {
1329 self.connect_timeout = connect_timeout;
1330 self
1331 }
1332
1333 pub fn with_request_timeout(&mut self, request_timeout: Option<u32>) -> &mut Self {
1334 self.request_timeout = request_timeout;
1335 self
1336 }
1337
1338 pub fn with_answer<S, P>(&mut self, code: S, path: P) -> &mut Self
1343 where
1344 S: ToString,
1345 P: ToString,
1346 {
1347 self.answers
1348 .get_or_insert_with(BTreeMap::new)
1349 .insert(code.to_string(), path.to_string());
1350 self
1351 }
1352
1353 pub fn with_answers(&mut self, answers: BTreeMap<String, String>) -> &mut Self {
1356 self.answers = Some(answers);
1357 self
1358 }
1359
1360 fn get_http_answers(&self) -> Result<Option<CustomHttpAnswers>, ConfigError> {
1362 let http_answers = CustomHttpAnswers {
1363 answer_301: read_http_answer_file(&self.answer_301)?,
1364 answer_400: read_http_answer_file(&self.answer_400)?,
1365 answer_401: read_http_answer_file(&self.answer_401)?,
1366 answer_404: read_http_answer_file(&self.answer_404)?,
1367 answer_408: read_http_answer_file(&self.answer_408)?,
1368 answer_413: read_http_answer_file(&self.answer_413)?,
1369 answer_421: read_http_answer_file(&self.answer_421)?,
1370 answer_502: read_http_answer_file(&self.answer_502)?,
1371 answer_503: read_http_answer_file(&self.answer_503)?,
1372 answer_504: read_http_answer_file(&self.answer_504)?,
1373 answer_507: read_http_answer_file(&self.answer_507)?,
1374 answer_429: read_http_answer_file(&self.answer_429)?,
1375 };
1376 Ok(Some(http_answers))
1377 }
1378
1379 fn get_listener_answers(&self) -> Result<BTreeMap<String, String>, ConfigError> {
1387 let mut out = BTreeMap::new();
1388
1389 macro_rules! merge_legacy {
1393 ($code:literal, $field:ident) => {
1394 if let Some(body) = read_http_answer_file(&self.$field)? {
1395 out.insert($code.to_owned(), body);
1396 }
1397 };
1398 }
1399 merge_legacy!("301", answer_301);
1400 merge_legacy!("400", answer_400);
1401 merge_legacy!("401", answer_401);
1402 merge_legacy!("404", answer_404);
1403 merge_legacy!("408", answer_408);
1404 merge_legacy!("413", answer_413);
1405 merge_legacy!("421", answer_421);
1406 merge_legacy!("502", answer_502);
1407 merge_legacy!("503", answer_503);
1408 merge_legacy!("504", answer_504);
1409 merge_legacy!("507", answer_507);
1410 merge_legacy!("429", answer_429);
1411
1412 if let Some(map) = &self.answers {
1413 let loaded = load_answers(map)?;
1414 out.extend(loaded);
1415 }
1416 Ok(out)
1417 }
1418
1419 fn assign_config_timeouts(&mut self, config: &Config) {
1421 self.front_timeout = Some(self.front_timeout.unwrap_or(config.front_timeout));
1422 self.back_timeout = Some(self.back_timeout.unwrap_or(config.back_timeout));
1423 self.connect_timeout = Some(self.connect_timeout.unwrap_or(config.connect_timeout));
1424 self.request_timeout = Some(self.request_timeout.unwrap_or(config.request_timeout));
1425 }
1426
1427 fn validate_h2_thresholds(&self) -> Result<(), ConfigError> {
1449 macro_rules! require_at_least {
1450 ($builder:ident, $field:ident, $minimum:literal) => {
1451 if let Some(value) = $builder.$field
1452 && u64::from(value) < $minimum
1453 {
1454 return Err(ConfigError::H2ThresholdBelowMinimum {
1455 address: $builder.address.to_string(),
1456 key: stringify!($field),
1457 value: u64::from(value),
1458 minimum: $minimum,
1459 });
1460 }
1461 };
1462 }
1463 crate::for_each_h2_knob_floor!(require_at_least, self);
1464 Ok(())
1465 }
1466
1467 fn reject_client_auth_fields(&self, listener_kind: &str) -> Result<(), ConfigError> {
1472 let has_mtls = self
1473 .client_auth
1474 .is_some_and(|m| m != ClientAuthConfig::None)
1475 || self
1476 .client_ca_certificates
1477 .as_ref()
1478 .is_some_and(|v| !v.is_empty())
1479 || self.client_ca_crls.as_ref().is_some_and(|v| !v.is_empty());
1480 if has_mtls {
1481 return Err(ConfigError::ClientAuthOnNonHttps(format!(
1482 "{} listener {}",
1483 listener_kind, self.address
1484 )));
1485 }
1486 Ok(())
1487 }
1488
1489 pub fn to_http(&mut self, config: Option<&Config>) -> Result<HttpListenerConfig, ConfigError> {
1491 if self.protocol != Some(ListenerProtocol::Http) {
1492 return Err(ConfigError::WrongListenerProtocol {
1493 expected: ListenerProtocol::Http,
1494 found: self.protocol.to_owned(),
1495 });
1496 }
1497 self.reject_client_auth_fields("HTTP")?;
1498
1499 self.validate_h2_thresholds()?;
1500
1501 if self.hsts.is_some() {
1507 return Err(ConfigError::HstsOnPlainHttp(format!(
1508 "HTTP listener {}",
1509 self.address
1510 )));
1511 }
1512
1513 if let Some(config) = config {
1514 self.assign_config_timeouts(config);
1515 }
1516
1517 let http_answers = self.get_http_answers()?;
1518 let answers = self.get_listener_answers()?;
1519
1520 let interface = self.validated_interface()?;
1521 let configuration = HttpListenerConfig {
1522 address: self.address.into(),
1523 interface,
1524 public_address: self.public_address.map(|a| a.into()),
1525 expect_proxy: self.expect_proxy.unwrap_or(false),
1526 sticky_name: self.sticky_name.clone(),
1527 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1528 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1529 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1530 request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1531 http_answers,
1532 answers,
1533 h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1534 h2_max_ping_per_window: self.h2_max_ping_per_window,
1535 h2_max_settings_per_window: self.h2_max_settings_per_window,
1536 h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1537 h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1538 h2_max_continuation_frames: self.h2_max_continuation_frames,
1539 h2_max_glitch_count: self.h2_max_glitch_count,
1540 h2_initial_connection_window: self.h2_initial_connection_window,
1541 h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1542 h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1543 h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1544 h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1545 h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1546 h2_max_header_list_size: self.h2_max_header_list_size,
1547 h2_max_header_table_size: self.h2_max_header_table_size,
1548 h2_max_header_fields: self.h2_max_header_fields,
1549 h2_stream_refusal_percent: self.h2_stream_refusal_percent,
1550 h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1551 h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1552 sozu_id_header: self.sozu_id_header.clone(),
1553 elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1554 send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1555 forwarded_headers: Some(ForwardedHeaders::from(
1556 self.forwarded_headers.unwrap_or_default(),
1557 ) as i32),
1558 h2c_prior_knowledge: self.h2c_prior_knowledge,
1561 ..Default::default()
1562 };
1563
1564 debug_assert_eq!(
1569 configuration.address,
1570 self.address.into(),
1571 "HTTP listener must bind the requested address"
1572 );
1573 Ok(configuration)
1574 }
1575
1576 pub fn to_tls(&mut self, config: Option<&Config>) -> Result<HttpsListenerConfig, ConfigError> {
1578 if self.protocol != Some(ListenerProtocol::Https) {
1579 return Err(ConfigError::WrongListenerProtocol {
1580 expected: ListenerProtocol::Https,
1581 found: self.protocol.to_owned(),
1582 });
1583 }
1584
1585 self.validate_h2_thresholds()?;
1586
1587 let default_cipher_list = DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect();
1588
1589 let cipher_list = self.cipher_list.clone().unwrap_or(default_cipher_list);
1590
1591 let cipher_suites = self
1592 .cipher_suites
1593 .clone()
1594 .unwrap_or_else(|| DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect());
1595
1596 let signature_algorithms: Vec<String> = DEFAULT_SIGNATURE_ALGORITHMS
1597 .into_iter()
1598 .map(String::from)
1599 .collect();
1600
1601 let groups_list = self
1602 .groups_list
1603 .clone()
1604 .unwrap_or_else(|| DEFAULT_GROUPS_LIST.into_iter().map(String::from).collect());
1605
1606 let alpn_protocols: Vec<String> = match &self.alpn_protocols {
1607 Some(protos) if !protos.is_empty() => {
1608 for proto in protos {
1609 match proto.as_str() {
1610 "h2" | "http/1.1" => {}
1611 other => return Err(ConfigError::InvalidAlpnProtocol(other.to_owned())),
1612 }
1613 }
1614 if self.disable_http11.unwrap_or(false) && protos.iter().any(|p| p == "http/1.1") {
1619 return Err(ConfigError::DisableHttp11WithHttp11Alpn {
1620 address: self.address.to_string(),
1621 });
1622 }
1623 if !protos.iter().any(|p| p == "http/1.1") {
1624 warn!(
1625 "ALPN protocols do not include 'http/1.1'. Clients without H2 support will fail TLS negotiation."
1626 );
1627 }
1628 let mut seen = std::collections::HashSet::new();
1630 protos
1631 .iter()
1632 .filter(|p| seen.insert(p.as_str()))
1633 .cloned()
1634 .collect()
1635 }
1636 _ => {
1637 if self.disable_http11.unwrap_or(false)
1641 && DEFAULT_ALPN_PROTOCOLS.contains(&"http/1.1")
1642 {
1643 return Err(ConfigError::DisableHttp11WithHttp11Alpn {
1644 address: self.address.to_string(),
1645 });
1646 }
1647 DEFAULT_ALPN_PROTOCOLS
1648 .iter()
1649 .map(|s| s.to_string())
1650 .collect()
1651 }
1652 };
1653
1654 let versions = match self.tls_versions {
1655 None => vec![TlsVersion::TlsV12 as i32, TlsVersion::TlsV13 as i32],
1656 Some(ref v) => v.iter().map(|v| *v as i32).collect(),
1657 };
1658
1659 let key = self.key.as_ref().and_then(|path| {
1660 Config::load_file(path)
1661 .map_err(|e| {
1662 error!("cannot load key at path '{}': {:?}", path, e);
1663 e
1664 })
1665 .ok()
1666 });
1667 let certificate = self.certificate.as_ref().and_then(|path| {
1668 Config::load_file(path)
1669 .map_err(|e| {
1670 error!("cannot load certificate at path '{}': {:?}", path, e);
1671 e
1672 })
1673 .ok()
1674 });
1675 let certificate_chain = self
1676 .certificate_chain
1677 .as_ref()
1678 .and_then(|path| {
1679 Config::load_file(path)
1680 .map_err(|e| {
1681 error!("cannot load certificate chain at path '{}': {:?}", path, e);
1682 e
1683 })
1684 .ok()
1685 })
1686 .map(split_certificate_chain)
1687 .unwrap_or_default();
1688
1689 let has_client_auth_files = self
1702 .client_ca_certificates
1703 .as_ref()
1704 .is_some_and(|paths| !paths.is_empty())
1705 || self
1706 .client_ca_crls
1707 .as_ref()
1708 .is_some_and(|paths| !paths.is_empty());
1709 match self.client_auth {
1710 None if has_client_auth_files => {
1711 return Err(ConfigError::ClientAuthModeMissing(format!(
1712 "HTTPS listener {}",
1713 self.address
1714 )));
1715 }
1716 Some(ClientAuthConfig::None) if has_client_auth_files => {
1717 warn!(
1718 "HTTPS listener {}: client_auth is \"none\", so its client_ca_certificates \
1719 and client_ca_crls are ignored and clients are not authenticated",
1720 self.address
1721 );
1722 }
1723 _ => {}
1724 }
1725 let mode = self.client_auth.unwrap_or_default();
1726 let client_auth = self
1727 .client_auth
1728 .map(|mode| ClientAuthMode::from(mode) as i32);
1729 let load_pem_paths = |paths: &Option<Vec<String>>| -> Result<Vec<String>, ConfigError> {
1730 if mode == ClientAuthConfig::None {
1731 return Ok(Vec::new());
1732 }
1733 paths
1734 .as_ref()
1735 .map(|list| list.iter().map(|path| Config::load_file(path)).collect())
1736 .unwrap_or_else(|| Ok(Vec::new()))
1737 };
1738 let client_ca_certificates = load_pem_paths(&self.client_ca_certificates)?;
1739 let client_ca_crls = load_pem_paths(&self.client_ca_crls)?;
1740
1741 let http_answers = self.get_http_answers()?;
1742 let answers = self.get_listener_answers()?;
1743
1744 if let Some(config) = config {
1745 self.assign_config_timeouts(config);
1746 }
1747
1748 let https_listener_config = HttpsListenerConfig {
1749 interface: self.validated_interface()?,
1750 address: self.address.into(),
1751 sticky_name: self.sticky_name.clone(),
1752 public_address: self.public_address.map(|a| a.into()),
1753 cipher_list,
1754 versions,
1755 expect_proxy: self.expect_proxy.unwrap_or(false),
1756 key,
1757 certificate,
1758 certificate_chain,
1759 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1760 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1761 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1762 request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1763 cipher_suites,
1764 signature_algorithms,
1765 groups_list,
1766 active: false,
1767 send_tls13_tickets: self
1768 .send_tls13_tickets
1769 .unwrap_or(DEFAULT_SEND_TLS_13_TICKETS),
1770 http_answers,
1771 answers,
1772 alpn_protocols,
1773 h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1774 h2_max_ping_per_window: self.h2_max_ping_per_window,
1775 h2_max_settings_per_window: self.h2_max_settings_per_window,
1776 h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1777 h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1778 h2_max_continuation_frames: self.h2_max_continuation_frames,
1779 h2_max_glitch_count: self.h2_max_glitch_count,
1780 h2_initial_connection_window: self.h2_initial_connection_window,
1781 h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1782 h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1783 h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1784 h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1785 h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1786 h2_max_header_list_size: self.h2_max_header_list_size,
1787 h2_max_header_table_size: self.h2_max_header_table_size,
1788 h2_max_header_fields: self.h2_max_header_fields,
1789 h2_stream_refusal_percent: self.h2_stream_refusal_percent,
1790 strict_sni_binding: self.strict_sni_binding,
1791 disable_http11: self.disable_http11,
1792 h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1793 h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1794 sozu_id_header: self.sozu_id_header.clone(),
1795 elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1796 send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1797 forwarded_headers: Some(ForwardedHeaders::from(
1798 self.forwarded_headers.unwrap_or_default(),
1799 ) as i32),
1800 hsts: match self.hsts.as_ref() {
1801 Some(h) => Some(h.to_proto("listener")?),
1802 None => None,
1803 },
1804 client_auth,
1805 client_ca_certificates,
1806 client_ca_crls,
1807 };
1808
1809 debug_assert_eq!(
1813 https_listener_config.address,
1814 self.address.into(),
1815 "HTTPS listener must bind the requested address"
1816 );
1817 debug_assert!(
1818 !https_listener_config.active,
1819 "a freshly built HTTPS listener must start inactive"
1820 );
1821 debug_assert!(
1826 !https_listener_config.alpn_protocols.is_empty(),
1827 "resolved ALPN list must not be empty"
1828 );
1829 debug_assert!(
1830 https_listener_config
1831 .alpn_protocols
1832 .iter()
1833 .all(|p| p == "h2" || p == "http/1.1"),
1834 "resolved ALPN list must contain only h2 and http/1.1"
1835 );
1836 debug_assert!(
1837 {
1838 let mut seen = std::collections::HashSet::new();
1839 https_listener_config
1840 .alpn_protocols
1841 .iter()
1842 .all(|p| seen.insert(p))
1843 },
1844 "resolved ALPN list must be duplicate-free"
1845 );
1846 debug_assert!(
1849 !(self.disable_http11.unwrap_or(false)
1850 && https_listener_config
1851 .alpn_protocols
1852 .iter()
1853 .any(|p| p == "http/1.1")),
1854 "disable_http11 with http/1.1 in ALPN must have been rejected"
1855 );
1856 Ok(https_listener_config)
1857 }
1858
1859 pub fn to_tcp(&mut self, config: Option<&Config>) -> Result<TcpListenerConfig, ConfigError> {
1861 if self.protocol != Some(ListenerProtocol::Tcp) {
1862 return Err(ConfigError::WrongListenerProtocol {
1863 expected: ListenerProtocol::Tcp,
1864 found: self.protocol.to_owned(),
1865 });
1866 }
1867 self.reject_client_auth_fields("TCP")?;
1868
1869 if let Some(config) = config {
1870 self.assign_config_timeouts(config);
1871 }
1872
1873 let tcp_listener_config = TcpListenerConfig {
1874 interface: self.validated_interface()?,
1875 address: self.address.into(),
1876 public_address: self.public_address.map(|a| a.into()),
1877 expect_proxy: self.expect_proxy.unwrap_or(false),
1878 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1879 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1880 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1881 active: false,
1882 sni_preread_timeout: Some(
1883 self.sni_preread_timeout
1884 .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT),
1885 ),
1886 sni_preread_max_bytes: Some(
1887 self.sni_preread_max_bytes
1888 .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES),
1889 ),
1890 };
1891
1892 debug_assert_eq!(
1895 tcp_listener_config.address,
1896 self.address.into(),
1897 "TCP listener must bind the requested address"
1898 );
1899 debug_assert!(
1900 !tcp_listener_config.active,
1901 "a freshly built TCP listener must start inactive"
1902 );
1903 Ok(tcp_listener_config)
1904 }
1905
1906 pub fn to_udp(&mut self, config: Option<&Config>) -> Result<UdpListenerConfig, ConfigError> {
1924 if self.protocol != Some(ListenerProtocol::Udp) {
1925 return Err(ConfigError::WrongListenerProtocol {
1926 expected: ListenerProtocol::Udp,
1927 found: self.protocol.to_owned(),
1928 });
1929 }
1930 self.reject_client_auth_fields("UDP")?;
1931
1932 let mut max_rx_datagram_size = self
1933 .max_rx_datagram_size
1934 .unwrap_or(DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE);
1935 let buffer_size = config.map(|c| c.buffer_size).unwrap_or(DEFAULT_BUFFER_SIZE);
1936 if u64::from(max_rx_datagram_size) > buffer_size {
1937 warn!(
1938 "UDP listener {}: max_rx_datagram_size = {} exceeds buffer_size = {}, clamping to buffer_size",
1939 self.address, max_rx_datagram_size, buffer_size
1940 );
1941 max_rx_datagram_size = buffer_size as u32;
1942 }
1943
1944 let max_flows = self.max_flows.unwrap_or(DEFAULT_UDP_MAX_FLOWS);
1945 if max_flows > 0
1946 && let Some(soft_limit) = soft_rlimit_nofile()
1947 {
1948 let advisory = soft_limit.saturating_mul(7) / 10;
1949 if u64::from(max_flows) > advisory {
1950 warn!(
1951 "UDP listener {}: max_flows = {} exceeds ~70% of the soft RLIMIT_NOFILE ({}); \
1952 per-flow connected sockets may hit EMFILE",
1953 self.address, max_flows, advisory
1954 );
1955 }
1956 }
1957
1958 Ok(UdpListenerConfig {
1959 interface: self.validated_interface()?,
1960 address: self.address.into(),
1961 public_address: self.public_address.map(|a| a.into()),
1962 front_timeout: self.front_timeout.unwrap_or(DEFAULT_UDP_FRONT_TIMEOUT),
1963 back_timeout: self.back_timeout.unwrap_or(DEFAULT_UDP_BACK_TIMEOUT),
1964 max_rx_datagram_size,
1965 max_flows,
1966 active: false,
1967 })
1968 }
1969}
1970
1971fn soft_rlimit_nofile() -> Option<u64> {
1976 let mut limit = libc::rlimit {
1977 rlim_cur: 0,
1978 rlim_max: 0,
1979 };
1980 let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) };
1984 if rc == 0 {
1985 Some(limit.rlim_cur)
1988 } else {
1989 None
1990 }
1991}
1992
1993fn read_http_answer_file(path: &Option<String>) -> Result<Option<String>, ConfigError> {
1995 match path {
1996 Some(path) => {
1997 let mut content = String::new();
1998 let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
1999 path_to_open: path.to_owned(),
2000 io_error,
2001 })?;
2002
2003 file.read_to_string(&mut content)
2004 .map_err(|io_error| ConfigError::FileRead {
2005 path_to_read: path.to_owned(),
2006 io_error,
2007 })?;
2008
2009 Ok(Some(content))
2010 }
2011 None => Ok(None),
2012 }
2013}
2014
2015pub fn resolve_answer_source(value: &str) -> Result<String, ConfigError> {
2038 if let Some(path) = value.strip_prefix("file://") {
2039 let mut content = String::new();
2040 let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
2041 path_to_open: path.to_owned(),
2042 io_error,
2043 })?;
2044 file.read_to_string(&mut content)
2045 .map_err(|io_error| ConfigError::FileRead {
2046 path_to_read: path.to_owned(),
2047 io_error,
2048 })?;
2049 return Ok(content);
2050 }
2051 Ok(value.to_owned())
2052}
2053
2054pub fn load_answers(
2071 answers: &BTreeMap<String, String>,
2072) -> Result<BTreeMap<String, String>, ConfigError> {
2073 let mut out = BTreeMap::new();
2074 for (code, value) in answers {
2075 if value.is_empty() {
2076 continue;
2077 }
2078 out.insert(code.to_owned(), resolve_answer_source(value)?);
2079 }
2080 debug_assert!(
2084 out.len() <= answers.len(),
2085 "load_answers must not synthesize entries"
2086 );
2087 debug_assert!(
2088 out.keys().all(|k| answers.contains_key(k)),
2089 "every loaded status code must come from the input map"
2090 );
2091 Ok(out)
2092}
2093
2094#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
2107#[serde(rename_all = "lowercase")]
2108#[derive(Default)]
2109pub enum MetricDetailLevel {
2110 Process,
2111 Frontend,
2112 #[default]
2113 Cluster,
2114 Backend,
2115}
2116
2117impl From<MetricDetailLevel> for MetricDetail {
2118 fn from(level: MetricDetailLevel) -> Self {
2119 match level {
2120 MetricDetailLevel::Process => MetricDetail::DetailProcess,
2121 MetricDetailLevel::Frontend => MetricDetail::DetailFrontend,
2122 MetricDetailLevel::Cluster => MetricDetail::DetailCluster,
2123 MetricDetailLevel::Backend => MetricDetail::DetailBackend,
2124 }
2125 }
2126}
2127
2128impl From<MetricDetail> for MetricDetailLevel {
2129 fn from(detail: MetricDetail) -> Self {
2133 match detail {
2134 MetricDetail::DetailProcess => MetricDetailLevel::Process,
2135 MetricDetail::DetailFrontend => MetricDetailLevel::Frontend,
2136 MetricDetail::DetailCluster => MetricDetailLevel::Cluster,
2137 MetricDetail::DetailBackend => MetricDetailLevel::Backend,
2138 }
2139 }
2140}
2141
2142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
2157#[serde(rename_all = "snake_case")]
2158pub enum ForwardedHeadersMode {
2159 #[default]
2160 Both,
2161 XForwarded,
2162 Rfc7239,
2163 None,
2164}
2165
2166impl From<ForwardedHeadersMode> for ForwardedHeaders {
2167 fn from(mode: ForwardedHeadersMode) -> Self {
2168 match mode {
2169 ForwardedHeadersMode::Both => ForwardedHeaders::Both,
2170 ForwardedHeadersMode::XForwarded => ForwardedHeaders::XForwarded,
2171 ForwardedHeadersMode::Rfc7239 => ForwardedHeaders::Rfc7239,
2172 ForwardedHeadersMode::None => ForwardedHeaders::None,
2173 }
2174 }
2175}
2176
2177impl From<ForwardedHeaders> for ForwardedHeadersMode {
2178 fn from(headers: ForwardedHeaders) -> Self {
2179 match headers {
2180 ForwardedHeaders::Both => ForwardedHeadersMode::Both,
2181 ForwardedHeaders::XForwarded => ForwardedHeadersMode::XForwarded,
2182 ForwardedHeaders::Rfc7239 => ForwardedHeadersMode::Rfc7239,
2183 ForwardedHeaders::None => ForwardedHeadersMode::None,
2184 }
2185 }
2186}
2187
2188#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2189#[serde(deny_unknown_fields)]
2190pub struct MetricsConfig {
2191 pub address: SocketAddr,
2192 #[serde(default)]
2193 pub tagged_metrics: bool,
2194 #[serde(default)]
2195 pub prefix: Option<String>,
2196 #[serde(default)]
2199 pub detail: MetricDetailLevel,
2200}
2201
2202#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2203#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
2204#[serde(deny_unknown_fields)]
2205pub enum PathRuleType {
2206 Prefix,
2207 Regex,
2208 Equals,
2209 RegexPath,
2212}
2213
2214#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2215#[serde(deny_unknown_fields)]
2216pub struct FileClusterFrontendConfig {
2217 pub address: SocketAddr,
2218 pub hostname: Option<String>,
2219 #[serde(default)]
2227 pub alpn: Vec<String>,
2228 pub path: Option<String>,
2230 pub path_type: Option<PathRuleType>,
2233 pub method: Option<String>,
2234 pub certificate: Option<String>,
2235 pub key: Option<String>,
2236 pub certificate_chain: Option<String>,
2237 #[serde(default)]
2238 pub tls_versions: Vec<TlsVersion>,
2239 #[serde(default)]
2240 pub position: RulePosition,
2241 pub tags: Option<BTreeMap<String, String>>,
2242 pub redirect: Option<String>,
2247 pub redirect_scheme: Option<String>,
2251 pub redirect_template: Option<String>,
2255 pub rewrite_host: Option<String>,
2258 pub rewrite_path: Option<String>,
2260 pub rewrite_port: Option<u32>,
2262 pub required_auth: Option<bool>,
2266 pub headers: Option<Vec<HeaderEditConfig>>,
2270 pub hsts: Option<FileHstsConfig>,
2275 pub header_predicates: Option<Vec<HeaderPredicateConfig>>,
2278}
2279
2280#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2288#[serde(deny_unknown_fields)]
2289pub struct HeaderPredicateConfig {
2290 pub name: String,
2291 #[serde(rename = "match")]
2292 pub kind: String,
2293 pub value: Option<String>,
2294}
2295
2296#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2303#[serde(deny_unknown_fields)]
2304pub struct HeaderEditConfig {
2305 pub position: String,
2306 pub key: String,
2307 pub value: String,
2308}
2309
2310#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2321#[serde(deny_unknown_fields)]
2322pub struct FileHstsConfig {
2323 pub enabled: Option<bool>,
2326 pub max_age: Option<u32>,
2331 pub include_subdomains: Option<bool>,
2333 pub preload: Option<bool>,
2336 pub force_replace_backend: Option<bool>,
2344}
2345
2346impl FileHstsConfig {
2347 pub fn to_proto(&self, scope: &str) -> Result<HstsConfig, ConfigError> {
2365 let enabled = match self.enabled {
2366 Some(v) => v,
2367 None => return Err(ConfigError::HstsEnabledRequired(scope.to_owned())),
2368 };
2369
2370 let max_age = match (enabled, self.max_age) {
2371 (true, None) => Some(DEFAULT_HSTS_MAX_AGE),
2372 (_, m) => m,
2373 };
2374
2375 if let Some(value) = max_age
2376 && value > 0
2377 && value < 86_400
2378 {
2379 warn!(
2380 "HSTS max_age = {}s on {} is below 1 day — this is almost certainly a \
2381 misconfiguration. RFC 6797 §11.4 reserves max_age = 0 as the explicit kill \
2382 switch.",
2383 value, scope
2384 );
2385 }
2386
2387 let include_subdomains = self.include_subdomains;
2388 let preload = self.preload;
2389
2390 if matches!(preload, Some(true)) {
2391 let max_age_value = max_age.unwrap_or(0);
2392 if max_age_value < DEFAULT_HSTS_MAX_AGE {
2393 warn!(
2394 "HSTS preload = true on {} with max_age = {}s; the Chrome HSTS preload \
2395 list requires max_age >= {} (https://hstspreload.org/).",
2396 scope, max_age_value, DEFAULT_HSTS_MAX_AGE
2397 );
2398 }
2399 if include_subdomains != Some(true) {
2400 warn!(
2401 "HSTS preload = true on {} without include_subdomains = true; the Chrome \
2402 HSTS preload list requires includeSubDomains \
2403 (https://hstspreload.org/).",
2404 scope
2405 );
2406 }
2407 }
2408
2409 let config = HstsConfig {
2410 enabled: Some(enabled),
2411 max_age,
2412 include_subdomains,
2413 preload,
2414 force_replace_backend: self.force_replace_backend,
2415 };
2416
2417 debug_assert_eq!(
2422 config.enabled,
2423 Some(enabled),
2424 "built HSTS config must record the resolved enabled flag"
2425 );
2426 debug_assert!(
2427 !enabled || config.max_age.is_some(),
2428 "an enabled HSTS policy must carry a max_age"
2429 );
2430 Ok(config)
2431 }
2432}
2433
2434impl FileClusterFrontendConfig {
2435 pub fn to_tcp_front(&self) -> Result<TcpFrontendConfig, ConfigError> {
2436 if self.path.is_some() {
2437 return Err(ConfigError::InvalidFrontendConfig(
2438 "path_prefix".to_string(),
2439 ));
2440 }
2441 if self.certificate.is_some() {
2442 return Err(ConfigError::InvalidFrontendConfig(
2443 "certificate".to_string(),
2444 ));
2445 }
2446 if self.certificate_chain.is_some() {
2447 return Err(ConfigError::InvalidFrontendConfig(
2448 "certificate_chain".to_string(),
2449 ));
2450 }
2451
2452 if self.header_predicates.is_some() {
2453 return Err(ConfigError::InvalidFrontendConfig(
2454 "header_predicates".to_string(),
2455 ));
2456 }
2457
2458 let sni = match &self.hostname {
2463 Some(hostname) => Some(validate_sni_pattern(hostname)?),
2464 None => None,
2465 };
2466
2467 if sni.is_none() && !self.alpn.is_empty() {
2474 return Err(ConfigError::AlpnWithoutSni {
2475 address: self.address,
2476 });
2477 }
2478
2479 let tcp_front = TcpFrontendConfig {
2480 address: self.address,
2481 tags: self.tags.clone(),
2482 sni,
2483 alpn: self.alpn.clone(),
2484 udp: false,
2487 };
2488 debug_assert_eq!(
2495 tcp_front.address, self.address,
2496 "TCP frontend must bind the requested address"
2497 );
2498 debug_assert!(
2499 self.path.is_none() && self.certificate.is_none() && self.certificate_chain.is_none(),
2500 "a built TCP frontend must carry no HTTP-only attributes"
2501 );
2502 debug_assert!(
2503 tcp_front.sni.is_some() || tcp_front.alpn.is_empty(),
2504 "a built TCP frontend without sni must never carry a non-empty alpn"
2505 );
2506 Ok(tcp_front)
2507 }
2508
2509 pub fn to_http_front(&self, _cluster_id: &str) -> Result<HttpFrontendConfig, ConfigError> {
2510 if !self.alpn.is_empty() {
2511 return Err(ConfigError::InvalidFrontendConfig("alpn".to_string()));
2512 }
2513
2514 let hostname = match &self.hostname {
2515 Some(hostname) => hostname.to_owned(),
2516 None => {
2517 return Err(ConfigError::Missing(MissingKind::Field(
2518 "hostname".to_string(),
2519 )));
2520 }
2521 };
2522
2523 let key_opt = match self.key.as_ref() {
2524 None => None,
2525 Some(path) => {
2526 let key = Config::load_file(path)?;
2527 Some(key)
2528 }
2529 };
2530
2531 let certificate_opt = match self.certificate.as_ref() {
2532 None => None,
2533 Some(path) => {
2534 let certificate = Config::load_file(path)?;
2535 Some(certificate)
2536 }
2537 };
2538
2539 let certificate_chain = match self.certificate_chain.as_ref() {
2540 None => None,
2541 Some(path) => {
2542 let certificate_chain = Config::load_file(path)?;
2543 Some(split_certificate_chain(certificate_chain))
2544 }
2545 };
2546
2547 let path = match (self.path.as_ref(), self.path_type.as_ref()) {
2548 (None, _) => PathRule::prefix("".to_string()),
2549 (Some(s), Some(PathRuleType::Prefix)) => PathRule::prefix(s.to_string()),
2550 (Some(s), Some(PathRuleType::Regex)) => PathRule::regex(s.to_string()),
2551 (Some(s), Some(PathRuleType::Equals)) => PathRule::equals(s.to_string()),
2552 (Some(s), Some(PathRuleType::RegexPath)) => PathRule::regex_path(s.to_string()),
2553 (Some(s), None) => PathRule::prefix(s.clone()),
2554 };
2555
2556 let redirect = match self.redirect.as_deref() {
2557 Some(v) => Some(parse_redirect_policy(v)?),
2558 None => None,
2559 };
2560 let redirect_scheme = match self.redirect_scheme.as_deref() {
2561 Some(v) => Some(parse_redirect_scheme(v)?),
2562 None => None,
2563 };
2564
2565 let headers = match self.headers.as_ref() {
2566 Some(entries) => {
2567 let mut out = Vec::with_capacity(entries.len());
2568 for (index, entry) in entries.iter().enumerate() {
2569 out.push(parse_header_edit(index, entry)?);
2570 }
2571 out
2572 }
2573 None => Vec::new(),
2574 };
2575
2576 let header_predicates = match self.header_predicates.as_ref() {
2583 Some(entries) => entries
2584 .iter()
2585 .enumerate()
2586 .map(|(index, entry)| parse_header_predicate(index, entry))
2587 .collect::<Result<Vec<_>, _>>()?,
2588 None => Vec::new(),
2589 };
2590
2591 let frontend_serves_https = key_opt.is_some() && certificate_opt.is_some();
2592 let hsts = match self.hsts.as_ref() {
2593 Some(h) => {
2594 if !frontend_serves_https {
2595 return Err(ConfigError::HstsOnPlainHttp(format!(
2596 "frontend {_cluster_id}/{hostname}"
2597 )));
2598 }
2599 Some(h.to_proto(&format!("frontend {_cluster_id}/{hostname}"))?)
2600 }
2601 None => None,
2602 };
2603
2604 Ok(HttpFrontendConfig {
2605 address: self.address,
2606 hostname,
2607 certificate: certificate_opt,
2608 key: key_opt,
2609 certificate_chain,
2610 tls_versions: self.tls_versions.clone(),
2611 position: self.position,
2612 path,
2613 method: self.method.clone(),
2614 tags: self.tags.clone(),
2615 redirect,
2616 redirect_scheme,
2617 redirect_template: self.redirect_template.clone(),
2618 rewrite_host: self.rewrite_host.clone(),
2619 rewrite_path: self.rewrite_path.clone(),
2620 rewrite_port: self.rewrite_port,
2621 required_auth: self.required_auth,
2622 headers,
2623 hsts,
2624 header_predicates,
2625 })
2626 }
2627}
2628
2629pub fn validate_sni_pattern(sni: &str) -> Result<String, ConfigError> {
2650 let invalid = || ConfigError::InvalidSniPattern {
2651 sni: sni.to_string(),
2652 };
2653
2654 if sni.is_empty() {
2655 return Err(invalid());
2656 }
2657
2658 if !sni.is_ascii() {
2659 return Err(ConfigError::NonAsciiSniPattern {
2660 sni: sni.to_string(),
2661 });
2662 }
2663
2664 let remainder = sni.strip_prefix("*.").unwrap_or(sni);
2668
2669 if remainder.is_empty() || remainder.contains('*') || remainder.contains('/') {
2676 return Err(invalid());
2677 }
2678 if remainder.split('.').any(|label| label.is_empty()) {
2679 return Err(invalid());
2680 }
2681
2682 let normalized = sni.to_ascii_lowercase();
2683 debug_assert!(
2689 normalized.matches('*').count() <= 1,
2690 "a validated SNI pattern must carry at most one wildcard marker"
2691 );
2692 debug_assert!(
2693 normalized.is_ascii(),
2694 "a validated SNI pattern must be pure ASCII"
2695 );
2696 Ok(normalized)
2697}
2698
2699pub(crate) fn parse_redirect_policy(value: &str) -> Result<RedirectPolicy, ConfigError> {
2701 match value.to_ascii_lowercase().as_str() {
2702 "forward" => Ok(RedirectPolicy::Forward),
2703 "permanent" => Ok(RedirectPolicy::Permanent),
2704 "unauthorized" => Ok(RedirectPolicy::Unauthorized),
2705 "silent_close" => Ok(RedirectPolicy::SilentClose),
2706 _ => Err(ConfigError::InvalidRedirectPolicy(value.to_owned())),
2707 }
2708}
2709
2710pub(crate) fn parse_redirect_scheme(value: &str) -> Result<RedirectScheme, ConfigError> {
2712 match value.to_ascii_lowercase().as_str() {
2713 "use-same" | "use_same" => Ok(RedirectScheme::UseSame),
2714 "use-http" | "use_http" => Ok(RedirectScheme::UseHttp),
2715 "use-https" | "use_https" => Ok(RedirectScheme::UseHttps),
2716 _ => Err(ConfigError::InvalidRedirectScheme(value.to_owned())),
2717 }
2718}
2719
2720pub(crate) fn parse_header_edit(
2727 index: usize,
2728 entry: &HeaderEditConfig,
2729) -> Result<Header, ConfigError> {
2730 let position = match entry.position.to_ascii_lowercase().as_str() {
2731 "request" => HeaderPosition::Request,
2732 "response" => HeaderPosition::Response,
2733 "both" => HeaderPosition::Both,
2734 _ => {
2735 return Err(ConfigError::InvalidHeaderPosition {
2736 index,
2737 position: entry.position.clone(),
2738 });
2739 }
2740 };
2741 if !header_name_is_valid_token(entry.key.as_bytes()) {
2742 return Err(ConfigError::InvalidHeaderBytes {
2743 index,
2744 field: "key",
2745 });
2746 }
2747 if header_value_contains_forbidden_controls(entry.value.as_bytes()) {
2748 return Err(ConfigError::InvalidHeaderBytes {
2749 index,
2750 field: "value",
2751 });
2752 }
2753 let header = Header {
2754 position: position as i32,
2755 key: entry.key.clone(),
2756 val: entry.value.clone(),
2757 };
2758 debug_assert!(
2764 header_name_is_valid_token(header.key.as_bytes()),
2765 "an emitted header key must be a valid token"
2766 );
2767 debug_assert!(
2768 !header_value_contains_forbidden_controls(header.val.as_bytes()),
2769 "an emitted header value must be free of forbidden control bytes"
2770 );
2771 Ok(header)
2772}
2773
2774pub(crate) fn parse_header_predicate(
2778 index: usize,
2779 entry: &HeaderPredicateConfig,
2780) -> Result<HeaderPredicate, ConfigError> {
2781 let kind = match entry.kind.to_ascii_lowercase().as_str() {
2782 "exact" => HeaderMatchKind::Exact,
2783 "regex" => HeaderMatchKind::Regex,
2784 "present" => HeaderMatchKind::Present,
2785 _ => {
2786 return Err(ConfigError::InvalidHeaderPredicate {
2787 index,
2788 reason: "match must be \"exact\", \"regex\" or \"present\"",
2789 });
2790 }
2791 };
2792 let predicate = HeaderPredicate {
2793 name: entry.name.to_owned(),
2794 kind: kind as i32,
2795 value: entry.value.to_owned(),
2796 };
2797 validate_header_predicate(&predicate)
2798 .map_err(|reason| ConfigError::InvalidHeaderPredicate { index, reason })?;
2799 Ok(predicate)
2800}
2801
2802pub fn validate_header_predicate(predicate: &HeaderPredicate) -> Result<(), &'static str> {
2819 let name = predicate.name.as_str();
2820 if !header_name_is_valid_token(name.as_bytes()) {
2821 return Err("name must be a non-empty RFC 9110 token (a header field name)");
2822 }
2823 if name.eq_ignore_ascii_case("host") {
2824 return Err(
2825 "name cannot be Host: the request authority is not a header Sōzu keeps \
2826 (H1 folds Host into it, H2 sends :authority); route on the hostname instead",
2827 );
2828 }
2829 if name.eq_ignore_ascii_case("cookie") {
2830 return Err("name cannot be Cookie: cookies are parsed into a jar, not kept as a header");
2831 }
2832 match (
2833 HeaderMatchKind::try_from(predicate.kind),
2834 predicate.value.as_deref(),
2835 ) {
2836 (Err(_), _) => Err("unknown match kind"),
2837 (Ok(HeaderMatchKind::Present), Some(_)) => Err("a present predicate takes no value"),
2838 (Ok(HeaderMatchKind::Present), None) => Ok(()),
2839 (Ok(HeaderMatchKind::Exact | HeaderMatchKind::Regex), None) => {
2840 Err("an exact or regex predicate requires a value")
2841 }
2842 (Ok(HeaderMatchKind::Exact), Some(value))
2843 if header_value_contains_forbidden_controls(value.as_bytes()) =>
2844 {
2845 Err("an exact value cannot carry a control character no header value can hold")
2846 }
2847 (Ok(HeaderMatchKind::Exact | HeaderMatchKind::Regex), Some(_)) => Ok(()),
2848 }
2849}
2850
2851pub(crate) fn header_name_is_valid_token(bytes: &[u8]) -> bool {
2859 if bytes.is_empty() {
2860 return false;
2861 }
2862 bytes.iter().all(|&b| is_tchar(b))
2863}
2864
2865fn is_tchar(b: u8) -> bool {
2868 b.is_ascii_alphanumeric()
2869 || matches!(
2870 b,
2871 b'!' | b'#'
2872 | b'$'
2873 | b'%'
2874 | b'&'
2875 | b'\''
2876 | b'*'
2877 | b'+'
2878 | b'-'
2879 | b'.'
2880 | b'^'
2881 | b'_'
2882 | b'`'
2883 | b'|'
2884 | b'~'
2885 )
2886}
2887
2888pub(crate) fn header_value_contains_forbidden_controls(bytes: &[u8]) -> bool {
2896 bytes
2897 .iter()
2898 .any(|&b| matches!(b, 0x00..=0x08 | 0x0A..=0x1F | 0x7F))
2899}
2900
2901#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2902#[serde(deny_unknown_fields, rename_all = "lowercase")]
2903pub enum ListenerProtocol {
2904 Http,
2905 Https,
2906 Tcp,
2907 Udp,
2908}
2909
2910#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2911#[serde(deny_unknown_fields, rename_all = "lowercase")]
2912pub enum FileClusterProtocolConfig {
2913 Http,
2914 Tcp,
2915}
2916
2917fn default_health_check_interval() -> u32 {
2918 10
2919}
2920fn default_health_check_timeout() -> u32 {
2921 5
2922}
2923fn default_health_check_threshold() -> u32 {
2924 3
2925}
2926fn default_health_check_uri() -> String {
2927 "/".to_owned()
2928}
2929
2930#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2931#[serde(deny_unknown_fields)]
2932pub struct FileHealthCheckConfig {
2933 #[serde(default = "default_health_check_uri")]
2936 pub uri: String,
2937 #[serde(default = "default_health_check_interval")]
2938 pub interval: u32,
2939 #[serde(default = "default_health_check_timeout")]
2940 pub timeout: u32,
2941 #[serde(default = "default_health_check_threshold")]
2942 pub healthy_threshold: u32,
2943 #[serde(default = "default_health_check_threshold")]
2944 pub unhealthy_threshold: u32,
2945 #[serde(default)]
2946 pub expected_status: u32,
2947 #[serde(default)]
2950 pub mode: Option<HealthCheckMode>,
2951 #[serde(default)]
2954 pub accepted_statuses: Vec<String>,
2955 #[serde(default)]
2958 pub port: Option<u32>,
2959 #[serde(default)]
2964 pub headers: Vec<FileHealthCheckHeader>,
2965}
2966
2967#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2972#[serde(deny_unknown_fields)]
2973pub struct FileHealthCheckHeader {
2974 pub name: String,
2975 pub value_file: String,
2976}
2977
2978impl std::fmt::Debug for FileHealthCheckHeader {
2979 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2980 f.debug_struct("FileHealthCheckHeader")
2981 .field("name_len", &self.name.len())
2982 .field("value_file", &"<redacted>")
2983 .finish_non_exhaustive()
2984 }
2985}
2986
2987impl FileHealthCheckConfig {
2988 pub fn to_proto(&self) -> Result<HealthCheckConfig, &'static str> {
2992 if !self.headers.is_empty() {
2996 return Err(
2997 "health check headers from the configuration file are not available yet: their values need the protected channel",
2998 );
2999 }
3000 self.to_parts().map(|(config, _)| config)
3001 }
3002
3003 pub fn to_parts(
3007 &self,
3008 ) -> Result<(HealthCheckConfig, Vec<FileHealthCheckHeader>), &'static str> {
3009 let accepted_statuses = self
3010 .accepted_statuses
3011 .iter()
3012 .map(|spec| parse_http_status_range(spec))
3013 .collect::<Result<Vec<_>, _>>()?;
3014 let proto = HealthCheckConfig {
3015 uri: self.uri.to_owned(),
3016 interval: self.interval,
3017 timeout: self.timeout,
3018 healthy_threshold: self.healthy_threshold,
3019 unhealthy_threshold: self.unhealthy_threshold,
3020 expected_status: self.expected_status,
3021 mode: self.mode.unwrap_or(HealthCheckMode::Http) as i32,
3022 accepted_statuses,
3023 port: self.port,
3024 headers: self
3025 .headers
3026 .iter()
3027 .map(|header| HealthCheckHeader {
3028 name: header.name.to_owned(),
3029 fingerprint: String::new(),
3030 })
3031 .collect(),
3032 };
3033 debug_assert_eq!(proto.uri, self.uri, "proto URI must mirror the file config");
3037 debug_assert!(
3038 proto.interval == self.interval
3039 && proto.timeout == self.timeout
3040 && proto.healthy_threshold == self.healthy_threshold
3041 && proto.unhealthy_threshold == self.unhealthy_threshold,
3042 "proto timing knobs must mirror the file config"
3043 );
3044 debug_assert_eq!(
3045 proto.port, self.port,
3046 "proto probe port must mirror the file config"
3047 );
3048 debug_assert_eq!(
3049 proto.accepted_statuses.len(),
3050 self.accepted_statuses.len(),
3051 "every accepted status spec must map to exactly one range"
3052 );
3053 debug_assert_eq!(
3054 proto.headers.len(),
3055 self.headers.len(),
3056 "every configured header must map to exactly one proto header"
3057 );
3058 validate_health_check_config(&proto)?;
3059 Ok((proto, self.headers.clone()))
3060 }
3061}
3062
3063pub const HTTP_STATUS_MIN: u32 = 100;
3066pub const HTTP_STATUS_MAX: u32 = 599;
3067
3068pub fn parse_http_status_range(spec: &str) -> Result<HttpStatusRange, &'static str> {
3077 const INVALID: &str = "health check accepted status must be a code (\"404\"), a range \
3078 (\"200-399\"), a class (\"2xx\") or \"any\"";
3079 let spec = spec.trim();
3080 let range = if spec.eq_ignore_ascii_case("any") {
3081 HttpStatusRange {
3082 start: HTTP_STATUS_MIN,
3083 end: HTTP_STATUS_MAX,
3084 }
3085 } else if let [class @ b'1'..=b'5', x1, x2] = spec.as_bytes()
3086 && x1.eq_ignore_ascii_case(&b'x')
3087 && x2.eq_ignore_ascii_case(&b'x')
3088 {
3089 let start = u32::from(class - b'0') * 100;
3090 HttpStatusRange {
3091 start,
3092 end: start + 99,
3093 }
3094 } else if let Some((start, end)) = spec.split_once('-') {
3095 HttpStatusRange {
3096 start: parse_status_code(start).ok_or(INVALID)?,
3097 end: parse_status_code(end).ok_or(INVALID)?,
3098 }
3099 } else {
3100 let code = parse_status_code(spec).ok_or(INVALID)?;
3101 HttpStatusRange {
3102 start: code,
3103 end: code,
3104 }
3105 };
3106 validate_http_status_range(&range)?;
3107 Ok(range)
3108}
3109
3110fn parse_status_code(code: &str) -> Option<u32> {
3113 let code = code.trim();
3114 if code.is_empty() || !code.bytes().all(|b| b.is_ascii_digit()) {
3115 return None;
3116 }
3117 code.parse().ok()
3118}
3119
3120fn validate_http_status_range(range: &HttpStatusRange) -> Result<(), &'static str> {
3121 if range.start < HTTP_STATUS_MIN || range.end > HTTP_STATUS_MAX {
3122 return Err("health check accepted statuses must lie within 100-599");
3123 }
3124 if range.start > range.end {
3125 return Err("health check accepted status range must not end before it starts");
3126 }
3127 debug_assert!(
3128 (HTTP_STATUS_MIN..=HTTP_STATUS_MAX).contains(&range.start)
3129 && (HTTP_STATUS_MIN..=HTTP_STATUS_MAX).contains(&range.end),
3130 "a validated range must lie within the valid status codes"
3131 );
3132 Ok(())
3133}
3134
3135pub fn validate_affinity_key(
3152 affinity_header: Option<&str>,
3153 affinity_cookie: Option<&str>,
3154) -> Result<(), &'static str> {
3155 match (affinity_header, affinity_cookie) {
3156 (Some(_), Some(_)) => {
3157 Err("affinity_header and affinity_cookie are mutually exclusive: set at most one")
3158 }
3159 (Some(name), None) if !header_name_is_valid_token(name.as_bytes()) => {
3160 Err("affinity_header must be a non-empty RFC 9110 token (a header field name)")
3161 }
3162 (Some(name), None) if name.eq_ignore_ascii_case("host") => Err(
3163 "affinity_header cannot be Host: the request authority is not a header Sōzu keeps \
3164 (H1 folds Host into it, H2 sends :authority)",
3165 ),
3166 (Some(name), None) if name.eq_ignore_ascii_case("cookie") => Err(
3167 "affinity_header cannot be Cookie: cookies are parsed into a jar; key on one with \
3168 affinity_cookie instead",
3169 ),
3170 (None, Some(name)) if !header_name_is_valid_token(name.as_bytes()) => {
3171 Err("affinity_cookie must be a non-empty RFC 6265 cookie name (an RFC 9110 token)")
3172 }
3173 _ => Ok(()),
3174 }
3175}
3176
3177pub const DEFAULT_SHARD_MIN_BACKENDS: u32 = 8;
3183
3184pub fn validate_shuffle_sharding(
3192 shard_percent: Option<u32>,
3193 shard_min_backends: Option<u32>,
3194 shard_mode: Option<i32>,
3195) -> Result<(), &'static str> {
3196 let Some(percent) = shard_percent else {
3197 if shard_min_backends.is_some() || shard_mode.is_some() {
3198 return Err("shard_min_backends and shard_mode require shard_percent");
3199 }
3200 return Ok(());
3201 };
3202 if !(1..=100).contains(&percent) {
3203 return Err("shard_percent must lie in 1..=100");
3204 }
3205 if shard_min_backends.is_some_and(|minimum| minimum < 2) {
3206 return Err("shard_min_backends must be at least 2, the smallest shard");
3207 }
3208 if shard_mode.is_some_and(|mode| ShardMode::try_from(mode).is_err()) {
3209 return Err("shard_mode must be FALLBACK or STRICT");
3210 }
3211 Ok(())
3212}
3213
3214pub const HEALTH_CHECK_MAX_HEADERS: usize = 32;
3216pub const HEALTH_CHECK_MAX_HEADER_BYTES: usize = 8192;
3219pub const HEALTH_CHECK_MAX_HPACK_BLOCK: usize = 16384;
3223pub const HEALTH_CHECK_MAX_AUTHORITY_LEN: usize = 1 + 39 + 11 + 1 + 6;
3226
3227const HEALTH_CHECK_FORBIDDEN_HEADERS: [&str; 10] = [
3229 "host",
3230 "connection",
3231 "keep-alive",
3232 "proxy-connection",
3233 "transfer-encoding",
3234 "content-length",
3235 "te",
3236 "trailer",
3237 "upgrade",
3238 "expect",
3239];
3240
3241fn hpack_string_length_bytes(length: usize) -> usize {
3243 if length < 127 {
3244 return 1;
3245 }
3246 let mut rest = length - 127;
3247 let mut bytes = 1;
3248 loop {
3249 bytes += 1;
3250 rest >>= 7;
3251 if rest == 0 {
3252 return bytes;
3253 }
3254 }
3255}
3256
3257pub fn health_check_hpack_upper_bound(cfg: &HealthCheckConfig, value_lens: &[usize]) -> usize {
3262 let mut bound = 2;
3264 bound += 1 + hpack_string_length_bytes(cfg.uri.len()) + cfg.uri.len();
3266 bound += 1
3268 + hpack_string_length_bytes(HEALTH_CHECK_MAX_AUTHORITY_LEN)
3269 + HEALTH_CHECK_MAX_AUTHORITY_LEN;
3270 for (header, value_len) in cfg.headers.iter().zip(value_lens) {
3271 bound += 1
3272 + hpack_string_length_bytes(header.name.len())
3273 + header.name.len()
3274 + hpack_string_length_bytes(*value_len)
3275 + value_len;
3276 }
3277 bound
3278}
3279
3280pub(crate) fn check_health_check_header_value(value: &str) -> Result<(), &'static str> {
3286 if header_value_contains_forbidden_controls(value.as_bytes()) {
3287 return Err("health check header value must not contain control bytes");
3288 }
3289 if value.starts_with([' ', '\t']) || value.ends_with([' ', '\t']) {
3294 return Err("health check header value must not begin or end with a space or a tab");
3295 }
3296 Ok(())
3297}
3298
3299fn validate_health_check_headers(
3303 cfg: &HealthCheckConfig,
3304 mode: HealthCheckMode,
3305) -> Result<(), &'static str> {
3306 if cfg.headers.is_empty() {
3307 return Ok(());
3308 }
3309 if mode == HealthCheckMode::Tcp {
3310 return Err("health check headers apply to HTTP mode only: leave them unset with mode TCP");
3311 }
3312 if cfg.headers.len() > HEALTH_CHECK_MAX_HEADERS {
3313 return Err("health check carries more than 32 headers");
3314 }
3315 for (index, header) in cfg.headers.iter().enumerate() {
3316 if header.name.starts_with(':')
3317 || HEALTH_CHECK_FORBIDDEN_HEADERS
3318 .iter()
3319 .any(|forbidden| header.name.eq_ignore_ascii_case(forbidden))
3320 {
3321 return Err("health check header name is reserved for the probe or its framing");
3322 }
3323 if !header_name_is_valid_token(header.name.as_bytes()) {
3324 return Err("health check header name must be a non-empty RFC 9110 token");
3325 }
3326 if cfg.headers[..index]
3327 .iter()
3328 .any(|earlier| earlier.name.eq_ignore_ascii_case(&header.name))
3329 {
3330 return Err("health check header names must be unique, compared case-insensitively");
3331 }
3332 if !header.fingerprint.is_empty()
3335 && header
3336 .fingerprint
3337 .parse::<crate::secret::HeaderFingerprint>()
3338 .is_err()
3339 {
3340 return Err("health check header fingerprint is malformed");
3341 }
3342 }
3343 Ok(())
3344}
3345
3346pub fn validate_health_check_header_values(
3352 cfg: &HealthCheckConfig,
3353 values: &[HealthCheckHeaderValue],
3354) -> Result<(), &'static str> {
3355 if values.len() != cfg.headers.len() {
3356 return Err("health check carries a header value count different from its header count");
3357 }
3358 let mut wire_bytes = 0usize;
3359 for (header, carrier) in cfg.headers.iter().zip(values) {
3360 if header.name != carrier.name {
3361 return Err(
3362 "health check header values must follow the order and names of the headers",
3363 );
3364 }
3365 let text = std::str::from_utf8(&carrier.value)
3366 .map_err(|_| "health check header value must be valid UTF-8")?;
3367 check_health_check_header_value(text)?;
3368 wire_bytes = wire_bytes
3369 .saturating_add(header.name.len())
3370 .saturating_add(carrier.value.len())
3371 .saturating_add(4);
3372 }
3373 if wire_bytes > HEALTH_CHECK_MAX_HEADER_BYTES {
3374 return Err("health check headers exceed 8192 bytes (name + value + 4 per header)");
3375 }
3376 let value_lens: Vec<usize> = values.iter().map(|carrier| carrier.value.len()).collect();
3377 if health_check_hpack_upper_bound(cfg, &value_lens) > HEALTH_CHECK_MAX_HPACK_BLOCK {
3378 return Err("health check URI and headers may exceed the 16384-byte HTTP/2 probe block");
3379 }
3380 Ok(())
3381}
3382
3383pub fn validate_health_check_config(cfg: &HealthCheckConfig) -> Result<(), &'static str> {
3399 let Ok(mode) = HealthCheckMode::try_from(cfg.mode) else {
3400 return Err("health check mode must be HTTP or TCP");
3401 };
3402 if cfg.interval == 0 {
3403 return Err("health check interval must be > 0");
3404 }
3405 if cfg.timeout == 0 {
3406 return Err("health check timeout must be > 0");
3407 }
3408 if cfg.healthy_threshold == 0 {
3409 return Err("health check healthy_threshold must be > 0");
3410 }
3411 if cfg.unhealthy_threshold == 0 {
3412 return Err("health check unhealthy_threshold must be > 0");
3413 }
3414 if mode == HealthCheckMode::Http && !cfg.uri.starts_with('/') {
3419 return Err("health check URI must start with '/'");
3420 }
3421 if cfg
3422 .uri
3423 .bytes()
3424 .any(|b| b == b'\r' || b == b'\n' || b == 0 || (b < 0x20 && b != b'\t'))
3425 {
3426 return Err("health check URI must not contain CR, LF, NUL, or other C0 control bytes");
3427 }
3428 for range in &cfg.accepted_statuses {
3429 validate_http_status_range(range)?;
3430 }
3431 if cfg.expected_status != 0 && !cfg.accepted_statuses.is_empty() {
3432 return Err(
3433 "health check expected_status and accepted_statuses are mutually exclusive: \
3434 set at most one",
3435 );
3436 }
3437 if mode == HealthCheckMode::Tcp
3440 && (cfg.expected_status != 0 || !cfg.accepted_statuses.is_empty())
3441 {
3442 return Err(
3443 "health check expected_status and accepted_statuses apply to HTTP mode only: \
3444 leave them unset with mode TCP",
3445 );
3446 }
3447 if cfg
3449 .port
3450 .is_some_and(|port| !(1..=u32::from(u16::MAX)).contains(&port))
3451 {
3452 return Err("health check port must lie in 1..=65535");
3453 }
3454 validate_health_check_headers(cfg, mode)?;
3455 debug_assert!(
3461 cfg.interval > 0
3462 && cfg.timeout > 0
3463 && cfg.healthy_threshold > 0
3464 && cfg.unhealthy_threshold > 0,
3465 "validated health-check thresholds must all be strictly positive"
3466 );
3467 debug_assert!(
3468 mode == HealthCheckMode::Tcp || cfg.uri.starts_with('/'),
3469 "a validated HTTP health-check URI must be an absolute path"
3470 );
3471 debug_assert!(
3472 cfg.expected_status == 0 || cfg.accepted_statuses.is_empty(),
3473 "a validated health check sets at most one of expected_status and accepted_statuses"
3474 );
3475 debug_assert!(
3476 mode == HealthCheckMode::Http
3477 || (cfg.expected_status == 0 && cfg.accepted_statuses.is_empty()),
3478 "a validated TCP health check sets no status field"
3479 );
3480 debug_assert!(
3481 cfg.port
3482 .is_none_or(|port| port != 0 && u16::try_from(port).is_ok()),
3483 "a validated health-check probe port is a non-zero 16-bit port"
3484 );
3485 Ok(())
3486}
3487
3488#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3489#[serde(deny_unknown_fields)]
3490pub struct FileClusterConfig {
3491 pub frontends: Vec<FileClusterFrontendConfig>,
3492 pub backends: Vec<FileBackendConfig>,
3493 pub protocol: FileClusterProtocolConfig,
3494 pub sticky_session: Option<bool>,
3495 pub https_redirect: Option<bool>,
3496 #[serde(default)]
3497 pub send_proxy: Option<bool>,
3498 #[serde(default)]
3499 pub load_balancing: LoadBalancingAlgorithms,
3500 pub answer_503: Option<String>,
3501 #[serde(default)]
3502 pub load_metric: Option<LoadMetric>,
3503 pub http2: Option<bool>,
3506 pub answers: Option<BTreeMap<String, String>>,
3517 pub https_redirect_port: Option<u32>,
3522 pub authorized_hashes: Option<Vec<String>>,
3527 pub www_authenticate: Option<String>,
3531 pub max_connections_per_ip: Option<u64>,
3540 pub max_connections_per_subnet: Option<u64>,
3548 pub retry_after: Option<u32>,
3554 #[serde(default)]
3558 pub health_check: Option<FileHealthCheckConfig>,
3559 #[serde(default)]
3563 pub udp: Option<FileUdpClusterConfig>,
3564 #[serde(default)]
3570 pub affinity_header: Option<String>,
3571 #[serde(default)]
3573 pub affinity_cookie: Option<String>,
3574 #[serde(default)]
3579 pub shard_percent: Option<u32>,
3580 #[serde(default)]
3583 pub shard_min_backends: Option<u32>,
3584 #[serde(default)]
3587 pub shard_mode: Option<ShardMode>,
3588 #[serde(default)]
3593 pub max_connection_attempts: Option<u32>,
3594}
3595
3596#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3599#[serde(deny_unknown_fields)]
3600pub struct FileUdpHealthConfig {
3601 pub mode: Option<UdpHealthMode>,
3605 pub tcp_port: Option<u32>,
3606 pub rise: Option<u32>,
3607 pub fall: Option<u32>,
3608 pub fail_open: Option<bool>,
3609 pub udp_probe_payload: Option<String>,
3611 pub probe_interval_seconds: Option<u32>,
3612 pub probe_timeout_seconds: Option<u32>,
3613}
3614
3615impl FileUdpHealthConfig {
3616 pub fn to_proto(&self) -> UdpHealthConfig {
3617 UdpHealthConfig {
3618 mode: self.mode.map(|m| m as i32),
3619 tcp_port: self.tcp_port,
3620 rise: self.rise,
3621 fall: self.fall,
3622 fail_open: self.fail_open,
3623 udp_probe_payload: self
3624 .udp_probe_payload
3625 .as_ref()
3626 .map(|p| p.as_bytes().to_owned()),
3627 probe_interval_seconds: self.probe_interval_seconds,
3628 probe_timeout_seconds: self.probe_timeout_seconds,
3629 }
3630 }
3631}
3632
3633#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3635#[serde(deny_unknown_fields)]
3636pub struct FileUdpClusterConfig {
3637 pub affinity_key: Option<UdpAffinityKey>,
3640 pub responses: Option<u32>,
3642 pub requests: Option<u32>,
3644 pub send_proxy_protocol: Option<bool>,
3646 pub proxy_protocol_every_datagram: Option<bool>,
3648 pub health: Option<FileUdpHealthConfig>,
3650}
3651
3652impl FileUdpClusterConfig {
3653 pub fn to_proto(&self) -> UdpClusterConfig {
3654 UdpClusterConfig {
3655 affinity_key: self.affinity_key.map(|k| k as i32),
3656 responses: self.responses,
3657 requests: self.requests,
3658 send_proxy_protocol: self.send_proxy_protocol,
3659 proxy_protocol_every_datagram: self.proxy_protocol_every_datagram,
3660 health: self.health.as_ref().map(|h| h.to_proto()),
3661 }
3662 }
3663}
3664
3665#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3666#[serde(deny_unknown_fields)]
3667pub struct BackendConfig {
3668 pub address: SocketAddr,
3669 pub weight: Option<u8>,
3670 pub sticky_id: Option<String>,
3671 pub backup: Option<bool>,
3672 pub backend_id: Option<String>,
3673 pub unix_path: Option<String>,
3677}
3678
3679fn validate_backend_unix_paths(
3683 cluster_id: &str,
3684 backends: &[BackendConfig],
3685) -> Result<(), ConfigError> {
3686 for (index, backend) in backends.iter().enumerate() {
3687 let Some(unix_path) = backend.unix_path.as_deref() else {
3688 continue;
3689 };
3690 let invalid = |reason| ConfigError::InvalidBackendUnixPath {
3691 cluster_id: cluster_id.to_owned(),
3692 address: backend.address,
3693 reason,
3694 };
3695 parse_backend_unix_path(unix_path).map_err(invalid)?;
3696 if backends
3697 .iter()
3698 .enumerate()
3699 .any(|(other, b)| other != index && b.address == backend.address)
3700 {
3701 return Err(invalid(
3702 "a unix backend's address is its identity and must be unique in the cluster",
3703 ));
3704 }
3705 }
3706 Ok(())
3707}
3708
3709pub type BackendResolver<'a> = dyn Fn(&str) -> std::io::Result<Vec<SocketAddr>> + 'a;
3711
3712pub fn system_resolver(address: &str) -> std::io::Result<Vec<SocketAddr>> {
3714 address.to_socket_addrs().map(Iterator::collect)
3715}
3716
3717#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3723#[serde(deny_unknown_fields)]
3724pub struct FileBackendConfig {
3725 pub address: String,
3726 pub weight: Option<u8>,
3727 pub sticky_id: Option<String>,
3728 pub backup: Option<bool>,
3729 pub backend_id: Option<String>,
3730 pub unix_path: Option<String>,
3733}
3734
3735fn is_legacy_numeric_host(host: &str) -> bool {
3740 host.split('.').all(|component| {
3741 !component.is_empty()
3742 && match component
3743 .strip_prefix("0x")
3744 .or_else(|| component.strip_prefix("0X"))
3745 {
3746 Some(hex) => hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
3747 None => component.bytes().all(|byte| byte.is_ascii_digit()),
3748 }
3749 })
3750}
3751
3752impl FileBackendConfig {
3753 pub fn resolve(
3777 &self,
3778 cluster_id: &str,
3779 index: usize,
3780 resolver: &BackendResolver<'_>,
3781 ) -> Result<Vec<BackendConfig>, ConfigError> {
3782 if let Ok(address) = self.address.parse::<SocketAddr>() {
3783 return Ok(vec![BackendConfig {
3784 address,
3785 weight: self.weight,
3786 sticky_id: self.sticky_id.clone(),
3787 backup: self.backup,
3788 backend_id: Some(
3789 self.backend_id
3790 .to_owned()
3791 .unwrap_or_else(|| format!("{cluster_id}-{index}-{address}")),
3792 ),
3793 unix_path: self.unix_path.clone(),
3794 }]);
3795 }
3796
3797 let error = |reason: String| ConfigError::BackendAddressResolution {
3798 cluster_id: cluster_id.to_owned(),
3799 address: self.address.clone(),
3800 reason,
3801 };
3802 if let Some((host, _port)) = self.address.rsplit_once(':') {
3803 if host.contains(':') {
3804 return Err(error(format!(
3805 "{host:?} is an unbracketed IPv6 literal, IPv6 literals must use [address]:port"
3806 )));
3807 }
3808 if !host.is_empty() && is_legacy_numeric_host(host) {
3809 return Err(error(format!(
3810 "{host:?} is not a valid IP address literal and numeric shorthand is not accepted"
3811 )));
3812 }
3813 }
3814 let addresses = resolver(&self.address).map_err(|io_error| error(io_error.to_string()))?;
3815 self.expand(cluster_id, addresses)
3816 }
3817
3818 fn expand(
3822 &self,
3823 cluster_id: &str,
3824 mut addresses: Vec<SocketAddr>,
3825 ) -> Result<Vec<BackendConfig>, ConfigError> {
3826 addresses.sort_unstable();
3827 addresses.dedup();
3828 if addresses.is_empty() {
3829 return Err(ConfigError::BackendAddressResolution {
3830 cluster_id: cluster_id.to_owned(),
3831 address: self.address.clone(),
3832 reason: "the name resolved to no address".to_owned(),
3833 });
3834 }
3835
3836 let backends: Vec<BackendConfig> = addresses
3837 .iter()
3838 .map(|address| BackendConfig {
3839 address: *address,
3840 weight: self.weight,
3841 sticky_id: self
3842 .sticky_id
3843 .as_ref()
3844 .map(|sticky_id| format!("{sticky_id}-{address}")),
3845 backup: self.backup,
3846 backend_id: Some(match &self.backend_id {
3847 Some(backend_id) => format!("{backend_id}-{address}"),
3848 None => format!("{cluster_id}-{}-{address}", self.address),
3849 }),
3850 unix_path: self.unix_path.clone(),
3851 })
3852 .collect();
3853
3854 debug_assert_eq!(backends.len(), addresses.len());
3859 debug_assert!(
3860 backends
3861 .iter()
3862 .map(|backend| backend.backend_id.as_deref())
3863 .collect::<HashSet<_>>()
3864 .len()
3865 == backends.len(),
3866 "resolved backends must carry distinct ids"
3867 );
3868 Ok(backends)
3869 }
3870}
3871
3872impl FileClusterConfig {
3873 pub fn to_cluster_config(
3874 self,
3875 cluster_id: &str,
3876 expect_proxy: &HashSet<SocketAddr>,
3877 ) -> Result<ClusterConfig, ConfigError> {
3878 self.to_cluster_config_with(cluster_id, expect_proxy, Some(&system_resolver))
3879 }
3880
3881 pub fn to_cluster_config_with(
3886 self,
3887 cluster_id: &str,
3888 expect_proxy: &HashSet<SocketAddr>,
3889 resolver: Option<&BackendResolver<'_>>,
3890 ) -> Result<ClusterConfig, ConfigError> {
3891 let requested_frontend_count = self.frontends.len();
3894 validate_affinity_key(
3895 self.affinity_header.as_deref(),
3896 self.affinity_cookie.as_deref(),
3897 )
3898 .map_err(|reason| ConfigError::InvalidAffinityKey {
3899 cluster_id: cluster_id.to_owned(),
3900 reason,
3901 })?;
3902 validate_shuffle_sharding(
3903 self.shard_percent,
3904 self.shard_min_backends,
3905 self.shard_mode.map(|mode| mode as i32),
3906 )
3907 .map_err(|reason| ConfigError::InvalidShuffleSharding {
3908 cluster_id: cluster_id.to_owned(),
3909 reason,
3910 })?;
3911 if let Some(attempts) = self.max_connection_attempts {
3912 validate_max_connection_attempts(attempts).map_err(|reason| {
3913 ConfigError::InvalidMaxConnectionAttempts {
3914 cluster_id: Some(cluster_id.to_owned()),
3915 value: attempts,
3916 reason,
3917 }
3918 })?;
3919 }
3920 let mut backends = Vec::with_capacity(self.backends.len());
3921 for (index, backend) in self.backends.iter().enumerate() {
3922 match resolver {
3923 Some(resolver) => backends.extend(backend.resolve(cluster_id, index, resolver)?),
3924 None if backend.address.parse::<SocketAddr>().is_ok() => {
3925 backends.extend(backend.resolve(cluster_id, index, &system_resolver)?)
3926 }
3927 None => {}
3928 }
3929 }
3930 debug_assert!(resolver.is_none() || backends.len() >= self.backends.len());
3932 let (health_check, health_check_header_files) = match self
3933 .health_check
3934 .as_ref()
3935 .map(FileHealthCheckConfig::to_parts)
3936 .transpose()
3937 .map_err(|reason| ConfigError::InvalidHealthCheck {
3938 cluster_id: cluster_id.to_owned(),
3939 reason,
3940 })? {
3941 Some((config, files)) => (Some(config), files),
3942 None => (None, Vec::new()),
3943 };
3944 validate_backend_unix_paths(cluster_id, &backends)?;
3945 if health_check.as_ref().is_some_and(|hc| hc.port.is_some())
3948 && self.backends.iter().any(|b| b.unix_path.is_some())
3949 {
3950 return Err(ConfigError::InvalidHealthCheck {
3951 cluster_id: cluster_id.to_owned(),
3952 reason: HEALTH_CHECK_PORT_UNIX_REFUSED,
3953 });
3954 }
3955 match self.protocol {
3956 FileClusterProtocolConfig::Tcp => {
3957 if self.affinity_header.is_some() || self.affinity_cookie.is_some() {
3961 return Err(ConfigError::InvalidAffinityKey {
3962 cluster_id: cluster_id.to_owned(),
3963 reason: "a TCP cluster always keys on the client source IP: \
3964 affinity_header and affinity_cookie apply to HTTP clusters only",
3965 });
3966 }
3967 let mut has_expect_proxy = None;
3968 let mut frontends = Vec::new();
3969 for f in self.frontends {
3970 if expect_proxy.contains(&f.address) {
3971 match has_expect_proxy {
3972 Some(true) => {}
3973 Some(false) => {
3974 return Err(ConfigError::Incompatible {
3975 object: ObjectKind::Cluster,
3976 id: cluster_id.to_owned(),
3977 kind: IncompatibilityKind::ProxyProtocol,
3978 });
3979 }
3980 None => has_expect_proxy = Some(true),
3981 }
3982 } else {
3983 match has_expect_proxy {
3984 Some(false) => {}
3985 Some(true) => {
3986 return Err(ConfigError::Incompatible {
3987 object: ObjectKind::Cluster,
3988 id: cluster_id.to_owned(),
3989 kind: IncompatibilityKind::ProxyProtocol,
3990 });
3991 }
3992 None => has_expect_proxy = Some(false),
3993 }
3994 }
3995 let tcp_frontend = f.to_tcp_front()?;
3996 frontends.push(tcp_frontend);
3997 }
3998
3999 let send_proxy = self.send_proxy.unwrap_or(false);
4000 let expect_proxy = has_expect_proxy.unwrap_or(false);
4001 let proxy_protocol = match (send_proxy, expect_proxy) {
4002 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
4003 (true, false) => Some(ProxyProtocolConfig::SendHeader),
4004 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
4005 _ => None,
4006 };
4007
4008 let answers = match self.answers.as_ref() {
4009 Some(map) => load_answers(map)?,
4010 None => BTreeMap::new(),
4011 };
4012
4013 let udp = self.udp.as_ref().map(|u| u.to_proto());
4014 debug_assert_eq!(
4020 frontends.len(),
4021 requested_frontend_count,
4022 "every TCP frontend must survive conversion"
4023 );
4024 debug_assert_eq!(
4025 proxy_protocol,
4026 match (send_proxy, expect_proxy) {
4027 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
4028 (true, false) => Some(ProxyProtocolConfig::SendHeader),
4029 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
4030 (false, false) => None,
4031 },
4032 "proxy_protocol must be the (send, expect) function"
4033 );
4034
4035 Ok(ClusterConfig::Tcp(TcpClusterConfig {
4036 cluster_id: cluster_id.to_string(),
4037 frontends,
4038 backends,
4039 proxy_protocol,
4040 load_balancing: self.load_balancing,
4041 load_metric: self.load_metric,
4042 answers,
4043 https_redirect_port: self.https_redirect_port,
4044 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
4045 www_authenticate: self.www_authenticate,
4046 max_connections_per_ip: self.max_connections_per_ip,
4047 max_connections_per_subnet: self.max_connections_per_subnet,
4048 retry_after: self.retry_after,
4049 health_check,
4050 health_check_header_files,
4051 udp,
4052 shard_percent: self.shard_percent,
4053 shard_min_backends: self.shard_min_backends,
4054 shard_mode: self.shard_mode,
4055 max_connection_attempts: self.max_connection_attempts,
4056 }))
4057 }
4058 FileClusterProtocolConfig::Http => {
4059 let mut frontends = Vec::new();
4060 for frontend in self.frontends {
4061 let http_frontend = frontend.to_http_front(cluster_id)?;
4062 frontends.push(http_frontend);
4063 }
4064
4065 let answer_503 = self.answer_503.as_ref().and_then(|path| {
4066 Config::load_file(path)
4067 .map_err(|e| {
4068 error!("cannot load 503 error page at path '{}': {:?}", path, e);
4069 e
4070 })
4071 .ok()
4072 });
4073
4074 let answers = match self.answers.as_ref() {
4075 Some(map) => load_answers(map)?,
4076 None => BTreeMap::new(),
4077 };
4078
4079 let udp = self.udp.as_ref().map(|u| u.to_proto());
4080 debug_assert_eq!(
4083 frontends.len(),
4084 requested_frontend_count,
4085 "every HTTP frontend must survive conversion"
4086 );
4087
4088 Ok(ClusterConfig::Http(HttpClusterConfig {
4089 cluster_id: cluster_id.to_string(),
4090 frontends,
4091 backends,
4092 sticky_session: self.sticky_session.unwrap_or(false),
4093 https_redirect: self.https_redirect.unwrap_or(false),
4094 load_balancing: self.load_balancing,
4095 load_metric: self.load_metric,
4096 answer_503,
4097 http2: self.http2,
4098 answers,
4099 https_redirect_port: self.https_redirect_port,
4100 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
4101 www_authenticate: self.www_authenticate,
4102 max_connections_per_ip: self.max_connections_per_ip,
4103 max_connections_per_subnet: self.max_connections_per_subnet,
4104 retry_after: self.retry_after,
4105 health_check,
4106 health_check_header_files,
4107 udp,
4108 affinity_header: self.affinity_header,
4109 affinity_cookie: self.affinity_cookie,
4110 shard_percent: self.shard_percent,
4111 shard_min_backends: self.shard_min_backends,
4112 shard_mode: self.shard_mode,
4113 max_connection_attempts: self.max_connection_attempts,
4114 }))
4115 }
4116 }
4117 }
4118}
4119
4120#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4121#[serde(deny_unknown_fields)]
4122pub struct HttpFrontendConfig {
4123 pub address: SocketAddr,
4124 pub hostname: String,
4125 pub path: PathRule,
4126 pub method: Option<String>,
4127 pub certificate: Option<String>,
4128 pub key: Option<String>,
4129 pub certificate_chain: Option<Vec<String>>,
4130 #[serde(default)]
4131 pub tls_versions: Vec<TlsVersion>,
4132 #[serde(default)]
4133 pub position: RulePosition,
4134 pub tags: Option<BTreeMap<String, String>>,
4135 #[serde(default)]
4137 pub redirect: Option<RedirectPolicy>,
4138 #[serde(default)]
4140 pub redirect_scheme: Option<RedirectScheme>,
4141 #[serde(default)]
4142 pub redirect_template: Option<String>,
4143 #[serde(default)]
4144 pub rewrite_host: Option<String>,
4145 #[serde(default)]
4146 pub rewrite_path: Option<String>,
4147 #[serde(default)]
4148 pub rewrite_port: Option<u32>,
4149 #[serde(default)]
4150 pub required_auth: Option<bool>,
4151 #[serde(default)]
4154 pub headers: Vec<Header>,
4155 #[serde(default)]
4158 pub hsts: Option<HstsConfig>,
4159 #[serde(default)]
4162 pub header_predicates: Vec<HeaderPredicate>,
4163}
4164
4165impl fmt::Debug for HttpFrontendConfig {
4166 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4167 let certificate = self.certificate.as_ref().map(|_| "[redacted]");
4168 let certificate_len = self.certificate.as_ref().map(String::len);
4169 let key = self.key.as_ref().map(|_| "[redacted]");
4170 let key_len = self.key.as_ref().map(String::len);
4171 let certificate_chain = self.certificate_chain.as_ref().map(|_| "[redacted]");
4172 let certificate_chain_count = self.certificate_chain.as_ref().map(Vec::len);
4173 let certificate_chain_len = self.certificate_chain.as_ref().map(|chain| {
4174 chain
4175 .iter()
4176 .map(String::len)
4177 .fold(0usize, usize::saturating_add)
4178 });
4179 let method_len = self.method.as_ref().map(String::len);
4180 let redirect_template_len = self.redirect_template.as_ref().map(String::len);
4181 let rewrite_host_len = self.rewrite_host.as_ref().map(String::len);
4182 let rewrite_path_len = self.rewrite_path.as_ref().map(String::len);
4183
4184 f.debug_struct("HttpFrontendConfig")
4185 .field("address", &self.address)
4186 .field("hostname_len", &self.hostname.len())
4187 .field("path_kind", &self.path.kind)
4188 .field("path_len", &self.path.value.len())
4189 .field("method_len", &method_len)
4190 .field("certificate", &certificate)
4191 .field("certificate_len", &certificate_len)
4192 .field("key", &key)
4193 .field("key_len", &key_len)
4194 .field("certificate_chain", &certificate_chain)
4195 .field("certificate_chain_count", &certificate_chain_count)
4196 .field("certificate_chain_len", &certificate_chain_len)
4197 .field("tls_versions_count", &self.tls_versions.len())
4198 .field("position", &self.position)
4199 .field(
4200 "tags_count",
4201 &self.tags.as_ref().map(BTreeMap::len).unwrap_or_default(),
4202 )
4203 .field("redirect", &self.redirect)
4204 .field("redirect_scheme", &self.redirect_scheme)
4205 .field("redirect_template_len", &redirect_template_len)
4206 .field("rewrite_host_len", &rewrite_host_len)
4207 .field("rewrite_path_len", &rewrite_path_len)
4208 .field("rewrite_port", &self.rewrite_port)
4209 .field("required_auth", &self.required_auth)
4210 .field("headers_count", &self.headers.len())
4211 .field("hsts", &self.hsts)
4212 .field("header_predicates_count", &self.header_predicates.len())
4213 .finish()
4214 }
4215}
4216
4217impl HttpFrontendConfig {
4218 pub fn generate_requests(&self, cluster_id: &str) -> Vec<Request> {
4219 let mut v = Vec::new();
4220
4221 let tags = self.tags.clone().unwrap_or_default();
4222
4223 if self.key.is_some() && self.certificate.is_some() {
4224 v.push(
4225 RequestType::AddCertificate(AddCertificate {
4226 address: self.address.into(),
4227 certificate: CertificateAndKey {
4228 key: self.key.clone().unwrap(),
4229 certificate: self.certificate.clone().unwrap(),
4230 certificate_chain: self.certificate_chain.clone().unwrap_or_default(),
4231 versions: self.tls_versions.iter().map(|v| *v as i32).collect(),
4232 names: vec![],
4237 },
4238 expired_at: None,
4239 })
4240 .into(),
4241 );
4242
4243 v.push(
4244 RequestType::add_https_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 } else {
4266 v.push(
4268 RequestType::add_http_frontend(RequestHttpFrontend {
4269 cluster_id: Some(cluster_id.to_string()),
4270 address: self.address.into(),
4271 hostname: self.hostname.clone(),
4272 path: self.path.clone(),
4273 method: self.method.clone(),
4274 position: self.position.into(),
4275 tags,
4276 redirect: self.redirect.map(|r| r as i32),
4277 required_auth: self.required_auth,
4278 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
4279 redirect_template: self.redirect_template.clone(),
4280 rewrite_host: self.rewrite_host.clone(),
4281 rewrite_path: self.rewrite_path.clone(),
4282 rewrite_port: self.rewrite_port,
4283 headers: self.headers.clone(),
4284 hsts: self.hsts,
4285 header_predicates: self.header_predicates.clone(),
4286 })
4287 .into(),
4288 );
4289 }
4290
4291 v
4292 }
4293}
4294
4295#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4296#[serde(deny_unknown_fields)]
4297pub struct HttpClusterConfig {
4298 pub cluster_id: String,
4299 pub frontends: Vec<HttpFrontendConfig>,
4300 pub backends: Vec<BackendConfig>,
4301 pub sticky_session: bool,
4302 pub https_redirect: bool,
4303 pub load_balancing: LoadBalancingAlgorithms,
4304 pub load_metric: Option<LoadMetric>,
4305 pub answer_503: Option<String>,
4306 pub http2: Option<bool>,
4307 #[serde(default)]
4310 pub answers: BTreeMap<String, String>,
4311 #[serde(default)]
4312 pub https_redirect_port: Option<u32>,
4313 #[serde(default)]
4314 pub authorized_hashes: Vec<String>,
4315 #[serde(default)]
4316 pub www_authenticate: Option<String>,
4317 #[serde(default)]
4320 pub max_connections_per_ip: Option<u64>,
4321 #[serde(default)]
4326 pub max_connections_per_subnet: Option<u64>,
4327 #[serde(default)]
4330 pub retry_after: Option<u32>,
4331 #[serde(default)]
4335 pub health_check: Option<HealthCheckConfig>,
4336 #[serde(default)]
4340 pub health_check_header_files: Vec<FileHealthCheckHeader>,
4341 #[serde(default)]
4344 pub udp: Option<UdpClusterConfig>,
4345 #[serde(default)]
4348 pub affinity_header: Option<String>,
4349 #[serde(default)]
4352 pub affinity_cookie: Option<String>,
4353 #[serde(default)]
4356 pub shard_percent: Option<u32>,
4357 #[serde(default)]
4359 pub shard_min_backends: Option<u32>,
4360 #[serde(default)]
4362 pub shard_mode: Option<ShardMode>,
4363 #[serde(default)]
4365 pub max_connection_attempts: Option<u32>,
4366}
4367
4368impl HttpClusterConfig {
4369 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
4370 if !self.health_check_header_files.is_empty() {
4374 return Err(ConfigError::InvalidHealthCheck {
4375 cluster_id: self.cluster_id.clone(),
4376 reason: HEALTH_CHECK_VALUE_FILE_UNSUPPORTED,
4377 });
4378 }
4379 let mut v: Vec<Request> = vec![
4380 RequestType::AddCluster(Cluster {
4381 cluster_id: self.cluster_id.clone(),
4382 sticky_session: self.sticky_session,
4383 https_redirect: self.https_redirect,
4384 proxy_protocol: None,
4385 load_balancing: self.load_balancing as i32,
4386 answer_503: self.answer_503.clone(),
4387 load_metric: self.load_metric.map(|s| s as i32),
4388 http2: self.http2,
4389 answers: self.answers.clone(),
4390 https_redirect_port: self.https_redirect_port,
4391 authorized_hashes: self.authorized_hashes.clone(),
4392 www_authenticate: self.www_authenticate.clone(),
4393 max_connections_per_ip: self.max_connections_per_ip,
4394 max_connections_per_subnet: self.max_connections_per_subnet,
4395 retry_after: self.retry_after,
4396 health_check: self.health_check.clone(),
4397 udp: self.udp.clone(),
4398 affinity_header: self.affinity_header.clone(),
4399 affinity_cookie: self.affinity_cookie.clone(),
4400 shard_percent: self.shard_percent,
4401 shard_min_backends: self.shard_min_backends,
4402 shard_mode: self.shard_mode.map(|mode| mode as i32),
4403 max_connection_attempts: self.max_connection_attempts,
4404 })
4405 .into(),
4406 ];
4407
4408 for frontend in &self.frontends {
4409 let mut orders = frontend.generate_requests(&self.cluster_id);
4410 v.append(&mut orders);
4411 }
4412
4413 for (backend_count, backend) in self.backends.iter().enumerate() {
4414 let load_balancing_parameters = Some(LoadBalancingParams {
4415 weight: backend.weight.unwrap_or(100) as i32,
4416 });
4417
4418 v.push(
4419 RequestType::add_backend(AddBackend {
4420 cluster_id: self.cluster_id.clone(),
4421 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
4422 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
4423 }),
4424 address: backend.address.into(),
4425 load_balancing_parameters,
4426 sticky_id: backend.sticky_id.clone(),
4427 backup: backend.backup,
4428 unix_path: backend.unix_path.clone(),
4429 })
4430 .into(),
4431 );
4432 }
4433
4434 debug_assert!(
4439 matches!(
4440 v.first().and_then(|r| r.request_type.as_ref()),
4441 Some(RequestType::AddCluster(_))
4442 ),
4443 "HTTP cluster orders must lead with an AddCluster"
4444 );
4445 debug_assert_eq!(
4446 v.iter()
4447 .filter(|r| matches!(
4448 r.request_type,
4449 Some(RequestType::AddBackend(_)) | Some(RequestType::AddUnixBackend(_))
4450 ))
4451 .count(),
4452 self.backends.len(),
4453 "one AddBackend order per configured backend"
4454 );
4455 Ok(v)
4456 }
4457}
4458
4459#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4460pub struct TcpFrontendConfig {
4461 pub address: SocketAddr,
4462 pub tags: Option<BTreeMap<String, String>>,
4463 #[serde(default)]
4470 pub udp: bool,
4471 #[serde(default)]
4475 pub sni: Option<String>,
4476 #[serde(default)]
4479 pub alpn: Vec<String>,
4480}
4481
4482#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4483pub struct TcpClusterConfig {
4484 pub cluster_id: String,
4485 pub frontends: Vec<TcpFrontendConfig>,
4486 pub backends: Vec<BackendConfig>,
4487 #[serde(default)]
4488 pub proxy_protocol: Option<ProxyProtocolConfig>,
4489 pub load_balancing: LoadBalancingAlgorithms,
4490 pub load_metric: Option<LoadMetric>,
4491 #[serde(default)]
4495 pub answers: BTreeMap<String, String>,
4496 #[serde(default)]
4497 pub https_redirect_port: Option<u32>,
4498 #[serde(default)]
4499 pub authorized_hashes: Vec<String>,
4500 #[serde(default)]
4501 pub www_authenticate: Option<String>,
4502 #[serde(default)]
4505 pub max_connections_per_ip: Option<u64>,
4506 #[serde(default)]
4511 pub max_connections_per_subnet: Option<u64>,
4512 #[serde(default)]
4516 pub retry_after: Option<u32>,
4517 #[serde(default)]
4521 pub health_check: Option<HealthCheckConfig>,
4522 #[serde(default)]
4526 pub health_check_header_files: Vec<FileHealthCheckHeader>,
4527 #[serde(default)]
4530 pub udp: Option<UdpClusterConfig>,
4531 #[serde(default)]
4534 pub shard_percent: Option<u32>,
4535 #[serde(default)]
4537 pub shard_min_backends: Option<u32>,
4538 #[serde(default)]
4540 pub shard_mode: Option<ShardMode>,
4541 #[serde(default)]
4543 pub max_connection_attempts: Option<u32>,
4544}
4545
4546impl TcpClusterConfig {
4547 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
4548 if !self.health_check_header_files.is_empty() {
4552 return Err(ConfigError::InvalidHealthCheck {
4553 cluster_id: self.cluster_id.clone(),
4554 reason: HEALTH_CHECK_VALUE_FILE_UNSUPPORTED,
4555 });
4556 }
4557 let mut v: Vec<Request> = vec![
4558 RequestType::AddCluster(Cluster {
4559 cluster_id: self.cluster_id.clone(),
4560 sticky_session: false,
4561 https_redirect: false,
4562 proxy_protocol: self.proxy_protocol.map(|s| s as i32),
4563 load_balancing: self.load_balancing as i32,
4564 load_metric: self.load_metric.map(|s| s as i32),
4565 answer_503: None,
4566 http2: None,
4567 answers: self.answers.clone(),
4568 https_redirect_port: self.https_redirect_port,
4569 authorized_hashes: self.authorized_hashes.clone(),
4570 www_authenticate: self.www_authenticate.clone(),
4571 max_connections_per_ip: self.max_connections_per_ip,
4572 max_connections_per_subnet: self.max_connections_per_subnet,
4573 retry_after: self.retry_after,
4574 health_check: self.health_check.clone(),
4575 udp: self.udp.clone(),
4576 affinity_header: None,
4578 affinity_cookie: None,
4579 shard_percent: self.shard_percent,
4580 shard_min_backends: self.shard_min_backends,
4581 shard_mode: self.shard_mode.map(|mode| mode as i32),
4582 max_connection_attempts: self.max_connection_attempts,
4583 })
4584 .into(),
4585 ];
4586
4587 for frontend in &self.frontends {
4588 if frontend.udp {
4593 v.push(
4594 RequestType::AddUdpFrontend(RequestUdpFrontend {
4595 cluster_id: self.cluster_id.clone(),
4596 address: frontend.address.into(),
4597 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
4598 })
4599 .into(),
4600 );
4601 } else {
4602 v.push(
4603 RequestType::AddTcpFrontend(RequestTcpFrontend {
4604 cluster_id: self.cluster_id.clone(),
4605 address: frontend.address.into(),
4606 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
4607 sni: frontend.sni.clone(),
4608 alpn: frontend.alpn.clone(),
4609 })
4610 .into(),
4611 );
4612 }
4613 }
4614
4615 for (backend_count, backend) in self.backends.iter().enumerate() {
4616 let load_balancing_parameters = Some(LoadBalancingParams {
4617 weight: backend.weight.unwrap_or(100) as i32,
4618 });
4619
4620 v.push(
4621 RequestType::add_backend(AddBackend {
4622 cluster_id: self.cluster_id.clone(),
4623 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
4624 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
4625 }),
4626 address: backend.address.into(),
4627 load_balancing_parameters,
4628 sticky_id: backend.sticky_id.clone(),
4629 backup: backend.backup,
4630 unix_path: backend.unix_path.clone(),
4631 })
4632 .into(),
4633 );
4634 }
4635
4636 debug_assert!(
4640 matches!(
4641 v.first().and_then(|r| r.request_type.as_ref()),
4642 Some(RequestType::AddCluster(_))
4643 ),
4644 "TCP cluster orders must lead with an AddCluster"
4645 );
4646 debug_assert_eq!(
4647 v.iter()
4648 .filter(|r| matches!(
4649 r.request_type,
4650 Some(RequestType::AddTcpFrontend(_)) | Some(RequestType::AddUdpFrontend(_))
4651 ))
4652 .count(),
4653 self.frontends.len(),
4654 "one AddTcpFrontend or AddUdpFrontend order per configured frontend"
4655 );
4656 debug_assert_eq!(
4657 v.iter()
4658 .filter(|r| matches!(
4659 r.request_type,
4660 Some(RequestType::AddBackend(_)) | Some(RequestType::AddUnixBackend(_))
4661 ))
4662 .count(),
4663 self.backends.len(),
4664 "one AddBackend order per configured backend"
4665 );
4666 Ok(v)
4667 }
4668}
4669
4670#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
4671pub enum ClusterConfig {
4672 Http(HttpClusterConfig),
4673 Tcp(TcpClusterConfig),
4674}
4675
4676impl ClusterConfig {
4677 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
4678 match *self {
4679 ClusterConfig::Http(ref http) => http.generate_requests(),
4680 ClusterConfig::Tcp(ref tcp) => tcp.generate_requests(),
4681 }
4682 }
4683}
4684
4685#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
4687pub struct FileConfig {
4688 pub command_socket: Option<String>,
4689 #[serde(default)]
4693 pub command_socket_mode: Option<String>,
4694 pub command_buffer_size: Option<u64>,
4695 pub max_command_buffer_size: Option<u64>,
4696 pub max_connections: Option<usize>,
4697 pub min_buffers: Option<u64>,
4698 pub max_buffers: Option<u64>,
4699 pub buffer_size: Option<u64>,
4700 #[serde(default)]
4704 pub slab_entries_per_connection: Option<u64>,
4705 #[serde(default)]
4715 pub basic_auth_max_credential_bytes: Option<u64>,
4716 #[serde(default)]
4724 pub max_connections_per_ip: Option<u64>,
4725 #[serde(default)]
4736 pub max_connections_per_subnet: Option<u64>,
4737 #[serde(default)]
4742 pub subnet_ipv4_prefix: Option<u32>,
4743 #[serde(default)]
4748 pub subnet_ipv6_prefix: Option<u32>,
4749 #[serde(default)]
4755 pub retry_after: Option<u32>,
4756 #[serde(default)]
4763 pub max_connection_attempts: Option<u32>,
4764 #[serde(default)]
4773 pub splice_pipe_capacity_bytes: Option<u64>,
4774 #[serde(default)]
4781 pub command_allowed_uids: Option<Vec<u32>>,
4782 pub saved_state: Option<String>,
4783 #[serde(default)]
4784 pub automatic_state_save: Option<bool>,
4785 pub log_level: Option<String>,
4786 pub log_target: Option<String>,
4787 #[serde(default)]
4788 pub log_colored: bool,
4789 #[serde(default)]
4797 pub audit_logs_target: Option<String>,
4798 #[serde(default)]
4803 pub audit_logs_json_target: Option<String>,
4804 #[serde(default)]
4805 pub access_logs_target: Option<String>,
4806 #[serde(default)]
4807 pub access_logs_format: Option<AccessLogFormat>,
4808 #[serde(default)]
4809 pub access_logs_colored: Option<bool>,
4810 pub worker_count: Option<u16>,
4811 pub worker_automatic_restart: Option<bool>,
4812 pub metrics: Option<MetricsConfig>,
4813 pub disable_cluster_metrics: Option<bool>,
4814 pub listeners: Option<Vec<ListenerBuilder>>,
4815 pub clusters: Option<HashMap<String, FileClusterConfig>>,
4816 pub handle_process_affinity: Option<bool>,
4817 pub ctl_command_timeout: Option<u64>,
4818 pub pid_file_path: Option<String>,
4819 pub activate_listeners: Option<bool>,
4820 #[serde(default)]
4821 pub front_timeout: Option<u32>,
4822 #[serde(default)]
4823 pub back_timeout: Option<u32>,
4824 #[serde(default)]
4825 pub connect_timeout: Option<u32>,
4826 #[serde(default)]
4827 pub zombie_check_interval: Option<u32>,
4828 #[serde(default)]
4829 pub accept_queue_timeout: Option<u32>,
4830 #[serde(default)]
4831 pub evict_on_queue_full: Option<bool>,
4832 #[serde(default)]
4833 pub request_timeout: Option<u32>,
4834 #[serde(default)]
4835 pub worker_timeout: Option<u32>,
4836}
4837
4838impl FileConfig {
4839 pub fn load_from_path(path: &str) -> Result<FileConfig, ConfigError> {
4840 let data = Config::load_file(path)?;
4841
4842 let config: FileConfig = match toml::from_str(&data) {
4843 Ok(config) => config,
4844 Err(e) => {
4845 return Err(ConfigError::DeserializeToml(toml_error_diagnostic(
4846 path, &e,
4847 )));
4848 }
4849 };
4850
4851 let mut reserved_address: HashSet<ListenerKey> = HashSet::new();
4852
4853 if let Some(listeners) = config.listeners.as_ref() {
4854 for listener in listeners.iter() {
4855 let key = ListenerKey::new(listener.address, listener.interface.as_deref());
4856 if reserved_address.contains(&key) {
4857 return Err(ConfigError::ListenerAlreadyDeclared(key));
4858 }
4859 reserved_address.insert(key);
4860 }
4861 }
4862
4863 Ok(config)
4885 }
4886}
4887
4888pub struct ConfigBuilder {
4890 file: FileConfig,
4891 known_addresses: HashMap<SocketAddr, ListenerProtocol>,
4892 expect_proxy_addresses: HashSet<SocketAddr>,
4893 resolve_backends: bool,
4896 built: Config,
4897}
4898
4899impl ConfigBuilder {
4900 pub fn new<S>(file_config: FileConfig, config_path: S) -> Self
4904 where
4905 S: ToString,
4906 {
4907 let built = Config {
4908 accept_queue_timeout: file_config
4909 .accept_queue_timeout
4910 .unwrap_or(DEFAULT_ACCEPT_QUEUE_TIMEOUT),
4911 evict_on_queue_full: file_config
4912 .evict_on_queue_full
4913 .unwrap_or(DEFAULT_EVICT_ON_QUEUE_FULL),
4914 activate_listeners: file_config.activate_listeners.unwrap_or(true),
4915 automatic_state_save: file_config
4916 .automatic_state_save
4917 .unwrap_or(DEFAULT_AUTOMATIC_STATE_SAVE),
4918 back_timeout: file_config.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
4919 buffer_size: file_config.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE),
4920 command_buffer_size: file_config
4921 .command_buffer_size
4922 .unwrap_or(DEFAULT_COMMAND_BUFFER_SIZE),
4923 config_path: config_path.to_string(),
4924 connect_timeout: file_config
4925 .connect_timeout
4926 .unwrap_or(DEFAULT_CONNECT_TIMEOUT),
4927 ctl_command_timeout: file_config.ctl_command_timeout.unwrap_or(1_000),
4928 front_timeout: file_config.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
4929 handle_process_affinity: file_config.handle_process_affinity.unwrap_or(false),
4930 access_logs_target: file_config.access_logs_target.clone(),
4931 audit_logs_target: file_config.audit_logs_target.clone(),
4932 audit_logs_json_target: file_config.audit_logs_json_target.clone(),
4933 access_logs_format: file_config.access_logs_format.clone(),
4934 access_logs_colored: file_config.access_logs_colored,
4935 log_level: file_config
4936 .log_level
4937 .clone()
4938 .unwrap_or_else(|| String::from("info")),
4939 log_target: file_config
4940 .log_target
4941 .clone()
4942 .unwrap_or_else(|| String::from("stdout")),
4943 log_colored: file_config.log_colored,
4944 max_buffers: file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
4945 max_command_buffer_size: file_config
4946 .max_command_buffer_size
4947 .unwrap_or(DEFAULT_MAX_COMMAND_BUFFER_SIZE),
4948 max_connections: file_config
4949 .max_connections
4950 .unwrap_or(DEFAULT_MAX_CONNECTIONS),
4951 metrics: file_config.metrics.clone(),
4952 disable_cluster_metrics: file_config
4953 .disable_cluster_metrics
4954 .unwrap_or(DEFAULT_DISABLE_CLUSTER_METRICS),
4955 min_buffers: std::cmp::min(
4956 file_config.min_buffers.unwrap_or(DEFAULT_MIN_BUFFERS),
4957 file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
4958 ),
4959 pid_file_path: file_config.pid_file_path.clone(),
4960 request_timeout: file_config
4961 .request_timeout
4962 .unwrap_or(DEFAULT_REQUEST_TIMEOUT),
4963 saved_state: file_config.saved_state.clone(),
4964 worker_automatic_restart: file_config
4965 .worker_automatic_restart
4966 .unwrap_or(DEFAULT_WORKER_AUTOMATIC_RESTART),
4967 worker_count: file_config.worker_count.unwrap_or(DEFAULT_WORKER_COUNT),
4968 zombie_check_interval: file_config
4969 .zombie_check_interval
4970 .unwrap_or(DEFAULT_ZOMBIE_CHECK_INTERVAL),
4971 worker_timeout: file_config.worker_timeout.unwrap_or(DEFAULT_WORKER_TIMEOUT),
4972 slab_entries_per_connection: file_config.slab_entries_per_connection.map(|n| {
4973 n.clamp(
4974 ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION,
4975 ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION,
4976 )
4977 }),
4978 command_allowed_uids: file_config.command_allowed_uids.clone(),
4979 basic_auth_max_credential_bytes: file_config.basic_auth_max_credential_bytes,
4980 max_connections_per_ip: file_config
4981 .max_connections_per_ip
4982 .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_IP),
4983 max_connections_per_subnet: file_config
4984 .max_connections_per_subnet
4985 .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_SUBNET),
4986 subnet_ipv4_prefix: file_config
4987 .subnet_ipv4_prefix
4988 .unwrap_or(DEFAULT_SUBNET_IPV4_PREFIX),
4989 subnet_ipv6_prefix: file_config
4990 .subnet_ipv6_prefix
4991 .unwrap_or(DEFAULT_SUBNET_IPV6_PREFIX),
4992 retry_after: file_config.retry_after.unwrap_or(DEFAULT_RETRY_AFTER),
4993 max_connection_attempts: file_config
4994 .max_connection_attempts
4995 .unwrap_or(DEFAULT_MAX_CONNECTION_ATTEMPTS),
4996 splice_pipe_capacity_bytes: file_config.splice_pipe_capacity_bytes,
4997 ..Default::default()
4998 };
4999
5000 debug_assert!(
5004 built.min_buffers <= built.max_buffers,
5005 "min_buffers must be clamped to <= max_buffers in the builder"
5006 );
5007 debug_assert!(
5010 built.slab_entries_per_connection.is_none_or(|n| {
5011 (ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION
5012 ..=ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION)
5013 .contains(&n)
5014 }),
5015 "a set slab_entries_per_connection must be clamped into [MIN, MAX]"
5016 );
5017
5018 Self {
5019 file: file_config,
5020 known_addresses: HashMap::new(),
5021 expect_proxy_addresses: HashSet::new(),
5022 resolve_backends: true,
5023 built,
5024 }
5025 }
5026
5027 fn push_tls_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
5028 let listener = listener.to_tls(Some(&self.built))?;
5029 self.built.https_listeners.push(listener);
5030 Ok(())
5031 }
5032
5033 fn push_http_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
5034 let listener = listener.to_http(Some(&self.built))?;
5035 self.built.http_listeners.push(listener);
5036 Ok(())
5037 }
5038
5039 fn push_tcp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
5040 let listener = listener.to_tcp(Some(&self.built))?;
5041 self.built.tcp_listeners.push(listener);
5042 Ok(())
5043 }
5044
5045 fn push_udp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
5046 let listener = listener.to_udp(Some(&self.built))?;
5047 self.built.udp_listeners.push(listener);
5048 Ok(())
5049 }
5050
5051 fn populate_listeners(&mut self, listeners: Vec<ListenerBuilder>) -> Result<(), ConfigError> {
5052 let mut declared: HashSet<ListenerKey> = HashSet::new();
5053 for listener in listeners.iter() {
5054 let key = ListenerKey::new(listener.address, listener.interface.as_deref());
5057 if !declared.insert(key.clone()) {
5058 if key.interface.is_none() {
5059 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
5060 }
5061 return Err(ConfigError::ListenerAlreadyDeclared(key));
5062 }
5063
5064 let protocol = listener
5065 .protocol
5066 .ok_or(ConfigError::Missing(MissingKind::Protocol))?;
5067
5068 if let Some(known) = self.known_addresses.get(&listener.address)
5072 && *known != protocol
5073 {
5074 return Err(ConfigError::ListenerProtocolMismatchOnAddress(
5075 listener.address,
5076 ));
5077 }
5078
5079 self.known_addresses.insert(listener.address, protocol);
5080 if listener.expect_proxy == Some(true) {
5081 self.expect_proxy_addresses.insert(listener.address);
5082 }
5083
5084 if listener.public_address.is_some() && listener.expect_proxy == Some(true) {
5085 return Err(ConfigError::Incompatible {
5086 object: ObjectKind::Listener,
5087 id: listener.address.to_string(),
5088 kind: IncompatibilityKind::PublicAddress,
5089 });
5090 }
5091
5092 match protocol {
5093 ListenerProtocol::Https => self.push_tls_listener(listener.clone())?,
5094 ListenerProtocol::Http => self.push_http_listener(listener.clone())?,
5095 ListenerProtocol::Tcp => self.push_tcp_listener(listener.clone())?,
5096 ListenerProtocol::Udp => self.push_udp_listener(listener.clone())?,
5097 }
5098 }
5099 Ok(())
5100 }
5101
5102 fn populate_clusters(
5103 &mut self,
5104 mut file_cluster_configs: HashMap<String, FileClusterConfig>,
5105 ) -> Result<(), ConfigError> {
5106 for (id, file_cluster_config) in file_cluster_configs.drain() {
5107 let resolver: Option<&BackendResolver<'_>> =
5108 self.resolve_backends.then_some(&system_resolver);
5109 let mut cluster_config = file_cluster_config.to_cluster_config_with(
5110 id.as_str(),
5111 &self.expect_proxy_addresses,
5112 resolver,
5113 )?;
5114
5115 match cluster_config {
5116 ClusterConfig::Http(ref mut http) => {
5117 for frontend in http.frontends.iter_mut() {
5118 match self.known_addresses.get(&frontend.address) {
5119 Some(ListenerProtocol::Tcp) => {
5120 return Err(ConfigError::WrongFrontendProtocol(
5121 ListenerProtocol::Tcp,
5122 ));
5123 }
5124 Some(ListenerProtocol::Udp) => {
5125 return Err(ConfigError::WrongFrontendProtocol(
5126 ListenerProtocol::Udp,
5127 ));
5128 }
5129 Some(ListenerProtocol::Http) => {
5130 if frontend.certificate.is_some() {
5131 return Err(ConfigError::WrongFrontendProtocol(
5132 ListenerProtocol::Http,
5133 ));
5134 }
5135 }
5136 Some(ListenerProtocol::Https) => {
5137 if frontend.certificate.is_none() {
5138 if let Some(https_listener) =
5139 self.built.https_listeners.iter().find(|listener| {
5140 listener.address == frontend.address.into()
5141 && listener.certificate.is_some()
5142 })
5143 {
5144 frontend
5146 .certificate
5147 .clone_from(&https_listener.certificate);
5148 frontend.certificate_chain =
5149 Some(https_listener.certificate_chain.clone());
5150 frontend.key.clone_from(&https_listener.key);
5151 }
5152 if frontend.certificate.is_none() {
5153 debug!("known addresses: {:?}", self.known_addresses);
5154 debug!("frontend: {:?}", frontend);
5155 return Err(ConfigError::WrongFrontendProtocol(
5156 ListenerProtocol::Https,
5157 ));
5158 }
5159 }
5160 }
5161 None => {
5162 let file_listener_protocol = if frontend.certificate.is_some() {
5164 self.push_tls_listener(ListenerBuilder::new(
5165 frontend.address.into(),
5166 ListenerProtocol::Https,
5167 ))?;
5168
5169 ListenerProtocol::Https
5170 } else {
5171 self.push_http_listener(ListenerBuilder::new(
5172 frontend.address.into(),
5173 ListenerProtocol::Http,
5174 ))?;
5175
5176 ListenerProtocol::Http
5177 };
5178 self.known_addresses
5179 .insert(frontend.address, file_listener_protocol);
5180 }
5181 }
5182 }
5183 }
5184 ClusterConfig::Tcp(ref mut tcp) => {
5185 for frontend in tcp.frontends.iter_mut() {
5187 match self.known_addresses.get(&frontend.address) {
5188 Some(ListenerProtocol::Http) | Some(ListenerProtocol::Https) => {
5189 return Err(ConfigError::WrongFrontendProtocol(
5190 ListenerProtocol::Http,
5191 ));
5192 }
5193 Some(ListenerProtocol::Udp) => {
5194 frontend.udp = true;
5200 if let Some(backend) =
5203 tcp.backends.iter().find(|b| b.unix_path.is_some())
5204 {
5205 return Err(ConfigError::InvalidBackendUnixPath {
5206 cluster_id: tcp.cluster_id.to_owned(),
5207 address: backend.address,
5208 reason: "a UDP cluster cannot use unix_path backends",
5209 });
5210 }
5211 }
5212 Some(ListenerProtocol::Tcp) => {}
5213 None => {
5214 self.push_tcp_listener(ListenerBuilder::new(
5216 frontend.address.into(),
5217 ListenerProtocol::Tcp,
5218 ))?;
5219 self.known_addresses
5220 .insert(frontend.address, ListenerProtocol::Tcp);
5221 }
5222 }
5223 }
5224 }
5225 }
5226
5227 self.built.clusters.insert(id, cluster_config);
5228 }
5229 Ok(())
5230 }
5231
5232 pub fn without_backend_resolution(mut self) -> Self {
5236 self.resolve_backends = false;
5237 self
5238 }
5239
5240 pub fn into_config(&mut self) -> Result<Config, ConfigError> {
5242 if self.built.subnet_ipv4_prefix > MAX_SUBNET_IPV4_PREFIX {
5247 return Err(ConfigError::InvalidSubnetPrefix {
5248 key: "subnet_ipv4_prefix",
5249 family: "IPv4",
5250 value: self.built.subnet_ipv4_prefix,
5251 maximum: MAX_SUBNET_IPV4_PREFIX,
5252 });
5253 }
5254 if self.built.subnet_ipv6_prefix > MAX_SUBNET_IPV6_PREFIX {
5255 return Err(ConfigError::InvalidSubnetPrefix {
5256 key: "subnet_ipv6_prefix",
5257 family: "IPv6",
5258 value: self.built.subnet_ipv6_prefix,
5259 maximum: MAX_SUBNET_IPV6_PREFIX,
5260 });
5261 }
5262 validate_max_connection_attempts(self.built.max_connection_attempts).map_err(|reason| {
5263 ConfigError::InvalidMaxConnectionAttempts {
5264 cluster_id: None,
5265 value: self.built.max_connection_attempts,
5266 reason,
5267 }
5268 })?;
5269
5270 if let Some(listeners) = &self.file.listeners {
5271 self.populate_listeners(listeners.clone())?;
5272 }
5273
5274 if let Some(file_cluster_configs) = &self.file.clusters {
5275 self.populate_clusters(file_cluster_configs.clone())?;
5276 }
5277
5278 type SniAlpnByAddress = HashMap<SocketAddr, Vec<(Option<String>, Vec<String>)>>;
5298 let mut frontends_by_address: SniAlpnByAddress = HashMap::new();
5299 let mut addresses_with_no_sni_frontend: HashSet<SocketAddr> = HashSet::new();
5300 for cluster in self.built.clusters.values() {
5301 if let ClusterConfig::Tcp(tcp) = cluster {
5302 for frontend in &tcp.frontends {
5303 if frontend.udp {
5310 continue;
5311 }
5312 if frontend.sni.is_none() {
5313 addresses_with_no_sni_frontend.insert(frontend.address);
5314 }
5315 frontends_by_address
5316 .entry(frontend.address)
5317 .or_default()
5318 .push((frontend.sni.clone(), frontend.alpn.clone()));
5319 }
5320 }
5321 }
5322
5323 let mut addresses_with_sni_frontend: HashSet<SocketAddr> = HashSet::new();
5324 for (address, frontends) in &frontends_by_address {
5325 if !frontends.iter().any(|(sni, _)| sni.is_some()) {
5326 continue;
5327 }
5328 addresses_with_sni_frontend.insert(*address);
5329
5330 if addresses_with_no_sni_frontend.contains(address) {
5331 return Err(ConfigError::TcpListenerMixesSniAndNoSni { address: *address });
5332 }
5333
5334 let mut alpn_lists_by_sni: HashMap<Option<String>, Vec<&Vec<String>>> = HashMap::new();
5335 for (sni, alpn) in frontends {
5336 alpn_lists_by_sni.entry(sni.clone()).or_default().push(alpn);
5337 }
5338 for (sni, alpn_lists) in alpn_lists_by_sni {
5339 let mut seen_protocols: HashSet<&str> = HashSet::new();
5340 let mut catch_all_count = 0usize;
5341 for alpn in alpn_lists {
5342 if alpn.is_empty() {
5343 catch_all_count += 1;
5344 if catch_all_count > 1 {
5345 return Err(ConfigError::TcpFrontendMultipleAlpnCatchAll {
5346 address: *address,
5347 sni: sni.clone(),
5348 });
5349 }
5350 continue;
5351 }
5352 for protocol in alpn {
5353 if !seen_protocols.insert(protocol.as_str()) {
5354 return Err(ConfigError::TcpFrontendAlpnOverlap {
5355 address: *address,
5356 sni: sni.clone(),
5357 protocol: protocol.clone(),
5358 });
5359 }
5360 }
5361 }
5362 }
5363 }
5364
5365 for listener in &self.built.tcp_listeners {
5366 let address: SocketAddr = listener.address.into();
5367 if !addresses_with_sni_frontend.contains(&address) {
5368 continue;
5369 }
5370 let sni_preread_timeout = listener
5371 .sni_preread_timeout
5372 .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT);
5373 if sni_preread_timeout > listener.front_timeout {
5374 return Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
5375 address,
5376 sni_preread_timeout,
5377 front_timeout: listener.front_timeout,
5378 });
5379 }
5380 let sni_preread_max_bytes = listener
5381 .sni_preread_max_bytes
5382 .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES);
5383 if sni_preread_max_bytes < MIN_SNI_PREREAD_MAX_BYTES {
5384 return Err(ConfigError::SniPrereadMaxBytesTooSmall {
5385 address,
5386 sni_preread_max_bytes,
5387 minimum: MIN_SNI_PREREAD_MAX_BYTES,
5388 });
5389 }
5390 if u64::from(sni_preread_max_bytes) > self.built.buffer_size {
5391 return Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
5392 address,
5393 sni_preread_max_bytes,
5394 buffer_size: self.built.buffer_size,
5395 });
5396 }
5397 }
5398
5399 let h2_listeners = self
5410 .built
5411 .https_listeners
5412 .iter()
5413 .filter(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
5414 .count()
5415 + self
5416 .built
5417 .http_listeners
5418 .iter()
5419 .filter(|l| l.h2c_prior_knowledge == Some(true))
5420 .count();
5421 if h2_listeners > 0 && self.built.buffer_size < H2_MIN_BUFFER_SIZE {
5422 return Err(ConfigError::BufferSizeTooSmallForH2 {
5423 buffer_size: self.built.buffer_size,
5424 minimum: H2_MIN_BUFFER_SIZE,
5425 listeners: h2_listeners,
5426 });
5427 }
5428
5429 if self.built.command_buffer_size > self.built.max_command_buffer_size {
5436 return Err(ConfigError::CommandBufferSizeExceedsMax {
5437 command_buffer_size: self.built.command_buffer_size,
5438 max_command_buffer_size: self.built.max_command_buffer_size,
5439 });
5440 }
5441
5442 if let Some(cap) = self.built.basic_auth_max_credential_bytes {
5452 let third = self.built.buffer_size / 3;
5453 if cap >= third {
5454 warn!(
5455 "basic_auth_max_credential_bytes = {} is >= buffer_size / 3 ({}); \
5456 a hostile peer can pin ~33% of the per-frontend buffer per failed auth \
5457 attempt. Consider lowering basic_auth_max_credential_bytes (typical \
5458 credentials are <100 bytes) or raising buffer_size.",
5459 cap, third
5460 );
5461 }
5462 }
5463
5464 if self.built.evict_on_queue_full && self.built.max_connections < 100 {
5471 let pct = 100usize.div_ceil(self.built.max_connections);
5472 warn!(
5473 "evict_on_queue_full enabled with max_connections = {}; the eviction batch \
5474 clamps to 1, equivalent to ~{}% of capacity per cap event (the knob is \
5475 documented as 1%). Confirm this is intended.",
5476 self.built.max_connections, pct
5477 );
5478 }
5479
5480 let command_socket_path = self.file.command_socket.clone().unwrap_or({
5481 let mut path = env::current_dir().map_err(|e| ConfigError::Env(e.to_string()))?;
5482 path.push("sozu.sock");
5483 let verified_path = path
5484 .to_str()
5485 .ok_or(ConfigError::InvalidPath(path.clone()))?;
5486 verified_path.to_owned()
5487 });
5488
5489 if let (None, Some(true)) = (&self.file.saved_state, &self.file.automatic_state_save) {
5490 return Err(ConfigError::Missing(MissingKind::SavedState));
5491 }
5492
5493 let command_socket_mode = match self.file.command_socket_mode.as_deref() {
5494 Some(mode) => parse_command_socket_mode(mode)?,
5495 None => DEFAULT_COMMAND_SOCKET_MODE,
5496 };
5497
5498 let config = Config {
5499 command_socket: command_socket_path,
5500 command_socket_mode,
5501 ..self.built.clone()
5502 };
5503
5504 debug_assert!(
5510 config.min_buffers <= config.max_buffers,
5511 "min_buffers must not exceed max_buffers"
5512 );
5513 debug_assert!(
5520 !config
5521 .https_listeners
5522 .iter()
5523 .any(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
5524 && !config
5525 .http_listeners
5526 .iter()
5527 .any(|l| l.h2c_prior_knowledge == Some(true))
5528 || config.buffer_size >= H2_MIN_BUFFER_SIZE,
5529 "an h2-advertising config must satisfy the H2 minimum buffer size"
5530 );
5531 Ok(config)
5532 }
5533}
5534
5535#[derive(Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
5539pub struct Config {
5540 pub config_path: String,
5541 pub command_socket: String,
5542 #[serde(default = "default_command_socket_mode")]
5547 pub command_socket_mode: u32,
5548 pub command_buffer_size: u64,
5549 pub max_command_buffer_size: u64,
5550 pub max_connections: usize,
5551 pub min_buffers: u64,
5552 pub max_buffers: u64,
5553 pub buffer_size: u64,
5554 pub saved_state: Option<String>,
5555 #[serde(default)]
5556 pub automatic_state_save: bool,
5557 pub log_level: String,
5558 pub log_target: String,
5559 pub log_colored: bool,
5560 #[serde(default)]
5563 pub audit_logs_target: Option<String>,
5564 #[serde(default)]
5567 pub audit_logs_json_target: Option<String>,
5568 #[serde(default)]
5569 pub access_logs_target: Option<String>,
5570 pub access_logs_format: Option<AccessLogFormat>,
5571 pub access_logs_colored: Option<bool>,
5572 pub worker_count: u16,
5573 pub worker_automatic_restart: bool,
5574 pub metrics: Option<MetricsConfig>,
5575 #[serde(default = "default_disable_cluster_metrics")]
5576 pub disable_cluster_metrics: bool,
5577 pub http_listeners: Vec<HttpListenerConfig>,
5578 pub https_listeners: Vec<HttpsListenerConfig>,
5579 pub tcp_listeners: Vec<TcpListenerConfig>,
5580 #[serde(default)]
5581 pub udp_listeners: Vec<UdpListenerConfig>,
5582 pub clusters: HashMap<String, ClusterConfig>,
5583 pub handle_process_affinity: bool,
5584 pub ctl_command_timeout: u64,
5585 pub pid_file_path: Option<String>,
5586 pub activate_listeners: bool,
5587 #[serde(default = "default_front_timeout")]
5588 pub front_timeout: u32,
5589 #[serde(default = "default_back_timeout")]
5590 pub back_timeout: u32,
5591 #[serde(default = "default_connect_timeout")]
5592 pub connect_timeout: u32,
5593 #[serde(default = "default_zombie_check_interval")]
5594 pub zombie_check_interval: u32,
5595 #[serde(default = "default_accept_queue_timeout")]
5596 pub accept_queue_timeout: u32,
5597 #[serde(default = "default_evict_on_queue_full")]
5598 pub evict_on_queue_full: bool,
5599 #[serde(default = "default_request_timeout")]
5600 pub request_timeout: u32,
5601 #[serde(default = "default_worker_timeout")]
5602 pub worker_timeout: u32,
5603 #[serde(default)]
5610 pub slab_entries_per_connection: Option<u64>,
5611 #[serde(default)]
5616 pub command_allowed_uids: Option<Vec<u32>>,
5617 #[serde(default)]
5622 pub basic_auth_max_credential_bytes: Option<u64>,
5623 #[serde(default = "default_max_connections_per_ip")]
5629 pub max_connections_per_ip: u64,
5630 #[serde(default = "default_max_connections_per_subnet")]
5636 pub max_connections_per_subnet: u64,
5637 #[serde(default = "default_subnet_ipv4_prefix")]
5640 pub subnet_ipv4_prefix: u32,
5641 #[serde(default = "default_subnet_ipv6_prefix")]
5644 pub subnet_ipv6_prefix: u32,
5645 #[serde(default = "default_retry_after")]
5648 pub retry_after: u32,
5649 #[serde(default = "default_max_connection_attempts")]
5652 pub max_connection_attempts: u32,
5653 #[serde(default)]
5660 pub splice_pipe_capacity_bytes: Option<u64>,
5661}
5662
5663fn default_command_socket_mode() -> u32 {
5664 DEFAULT_COMMAND_SOCKET_MODE
5665}
5666
5667pub fn parse_command_socket_mode(value: &str) -> Result<u32, ConfigError> {
5674 let invalid = |reason| ConfigError::InvalidCommandSocketMode {
5675 value: value.to_owned(),
5676 reason,
5677 };
5678 if value.is_empty() || value.len() > 4 || !value.bytes().all(|b| (b'0'..=b'7').contains(&b)) {
5679 return Err(invalid(
5680 "expected one to four octal digits, such as \"0660\" or \"660\"",
5681 ));
5682 }
5683 let mode =
5684 u32::from_str_radix(value, 8).map_err(|_| invalid("expected one to four octal digits"))?;
5685 if mode > MAX_COMMAND_SOCKET_MODE {
5686 return Err(invalid(
5687 "only permission bits (at most 0777) are allowed; setuid, setgid and sticky are refused",
5688 ));
5689 }
5690 if mode & 0o600 != 0o600 {
5691 return Err(invalid("the owner must keep read and write (0600)"));
5692 }
5693 Ok(mode)
5694}
5695
5696fn default_front_timeout() -> u32 {
5697 DEFAULT_FRONT_TIMEOUT
5698}
5699
5700fn default_back_timeout() -> u32 {
5701 DEFAULT_BACK_TIMEOUT
5702}
5703
5704fn default_connect_timeout() -> u32 {
5705 DEFAULT_CONNECT_TIMEOUT
5706}
5707
5708fn default_request_timeout() -> u32 {
5709 DEFAULT_REQUEST_TIMEOUT
5710}
5711
5712fn default_zombie_check_interval() -> u32 {
5713 DEFAULT_ZOMBIE_CHECK_INTERVAL
5714}
5715
5716fn default_accept_queue_timeout() -> u32 {
5717 DEFAULT_ACCEPT_QUEUE_TIMEOUT
5718}
5719
5720fn default_evict_on_queue_full() -> bool {
5721 DEFAULT_EVICT_ON_QUEUE_FULL
5722}
5723
5724fn default_disable_cluster_metrics() -> bool {
5725 DEFAULT_DISABLE_CLUSTER_METRICS
5726}
5727
5728fn default_worker_timeout() -> u32 {
5729 DEFAULT_WORKER_TIMEOUT
5730}
5731
5732fn default_max_connections_per_ip() -> u64 {
5733 DEFAULT_MAX_CONNECTIONS_PER_IP
5734}
5735
5736fn default_max_connections_per_subnet() -> u64 {
5737 DEFAULT_MAX_CONNECTIONS_PER_SUBNET
5738}
5739
5740fn default_subnet_ipv4_prefix() -> u32 {
5741 DEFAULT_SUBNET_IPV4_PREFIX
5742}
5743
5744fn default_subnet_ipv6_prefix() -> u32 {
5745 DEFAULT_SUBNET_IPV6_PREFIX
5746}
5747
5748fn default_retry_after() -> u32 {
5749 DEFAULT_RETRY_AFTER
5750}
5751
5752fn default_max_connection_attempts() -> u32 {
5753 DEFAULT_MAX_CONNECTION_ATTEMPTS
5754}
5755
5756impl Config {
5757 pub fn load_from_path(path: &str) -> Result<Config, ConfigError> {
5759 let file_config = FileConfig::load_from_path(path)?;
5760
5761 let mut config = ConfigBuilder::new(file_config, path).into_config()?;
5762
5763 config.saved_state = config.saved_state_path()?;
5765
5766 Ok(config)
5767 }
5768
5769 pub fn load_from_path_for_control(path: &str) -> Result<Config, ConfigError> {
5773 let file_config = FileConfig::load_from_path(path)?;
5774
5775 let mut config = ConfigBuilder::new(file_config, path)
5776 .without_backend_resolution()
5777 .into_config()?;
5778 config.saved_state = config.saved_state_path()?;
5779
5780 Ok(config)
5781 }
5782
5783 pub fn generate_config_messages(&self) -> Result<Vec<WorkerRequest>, ConfigError> {
5785 let mut v = Vec::new();
5786 let mut count = 0u8;
5787
5788 for listener in &self.http_listeners {
5789 v.push(WorkerRequest {
5790 id: format!("CONFIG-{count}"),
5791 content: RequestType::AddHttpListener(listener.clone()).into(),
5792 });
5793 count += 1;
5794 }
5795
5796 for listener in &self.https_listeners {
5797 v.push(WorkerRequest {
5798 id: format!("CONFIG-{count}"),
5799 content: RequestType::add_https_listener(listener.clone()).into(),
5800 });
5801 count += 1;
5802 }
5803
5804 for listener in &self.tcp_listeners {
5805 v.push(WorkerRequest {
5806 id: format!("CONFIG-{count}"),
5807 content: RequestType::AddTcpListener(listener.clone()).into(),
5808 });
5809 count += 1;
5810 }
5811
5812 for listener in &self.udp_listeners {
5813 v.push(WorkerRequest {
5814 id: format!("CONFIG-{count}"),
5815 content: RequestType::AddUdpListener(listener.clone()).into(),
5816 });
5817 count += 1;
5818 }
5819
5820 for cluster in self.clusters.values() {
5821 let mut orders = cluster.generate_requests()?;
5822 for content in orders.drain(..) {
5823 v.push(WorkerRequest {
5824 id: format!("CONFIG-{count}"),
5825 content,
5826 });
5827 count += 1;
5828 }
5829 }
5830
5831 if self.activate_listeners {
5832 for listener in &self.http_listeners {
5833 v.push(WorkerRequest {
5834 id: format!("CONFIG-{count}"),
5835 content: RequestType::ActivateListener(ActivateListener {
5836 interface: listener.interface.clone(),
5837 address: listener.address,
5838 proxy: ListenerType::Http.into(),
5839 from_scm: false,
5840 })
5841 .into(),
5842 });
5843 count += 1;
5844 }
5845
5846 for listener in &self.https_listeners {
5847 v.push(WorkerRequest {
5848 id: format!("CONFIG-{count}"),
5849 content: RequestType::ActivateListener(ActivateListener {
5850 interface: listener.interface.clone(),
5851 address: listener.address,
5852 proxy: ListenerType::Https.into(),
5853 from_scm: false,
5854 })
5855 .into(),
5856 });
5857 count += 1;
5858 }
5859
5860 for listener in &self.tcp_listeners {
5861 v.push(WorkerRequest {
5862 id: format!("CONFIG-{count}"),
5863 content: RequestType::ActivateListener(ActivateListener {
5864 interface: listener.interface.clone(),
5865 address: listener.address,
5866 proxy: ListenerType::Tcp.into(),
5867 from_scm: false,
5868 })
5869 .into(),
5870 });
5871 count += 1;
5872 }
5873
5874 for listener in &self.udp_listeners {
5875 v.push(WorkerRequest {
5876 id: format!("CONFIG-{count}"),
5877 content: RequestType::ActivateListener(ActivateListener {
5878 interface: listener.interface.clone(),
5879 address: listener.address,
5880 proxy: ListenerType::Udp.into(),
5881 from_scm: false,
5882 })
5883 .into(),
5884 });
5885 count += 1;
5886 }
5887 }
5888
5889 if self.disable_cluster_metrics {
5890 v.push(WorkerRequest {
5891 id: format!("CONFIG-{count}"),
5892 content: RequestType::ConfigureMetrics(MetricsConfiguration::Disabled.into())
5893 .into(),
5894 });
5895 }
5897
5898 Ok(v)
5899 }
5900
5901 pub fn command_socket_path(&self) -> Result<String, ConfigError> {
5903 let config_path_buf = PathBuf::from(self.config_path.clone());
5904 let mut config_dir = config_path_buf
5905 .parent()
5906 .ok_or(ConfigError::NoFileParent(
5907 config_path_buf.to_string_lossy().to_string(),
5908 ))?
5909 .to_path_buf();
5910
5911 let socket_path = PathBuf::from(self.command_socket.clone());
5912
5913 let mut socket_parent_dir = match socket_path.parent() {
5914 None => config_dir,
5917 Some(path) => {
5918 config_dir.push(path);
5920 config_dir.canonicalize().map_err(|io_error| {
5922 ConfigError::SocketPathError(format!(
5923 "Could not canonicalize path {config_dir:?}: {io_error}"
5924 ))
5925 })?
5926 }
5927 };
5928
5929 let socket_name = socket_path
5930 .file_name()
5931 .ok_or(ConfigError::SocketPathError(format!(
5932 "could not get command socket file name from {socket_path:?}"
5933 )))?;
5934
5935 socket_parent_dir.push(socket_name);
5937
5938 let command_socket_path = socket_parent_dir
5939 .to_str()
5940 .ok_or(ConfigError::SocketPathError(format!(
5941 "Invalid socket path {socket_parent_dir:?}"
5942 )))?
5943 .to_string();
5944
5945 Ok(command_socket_path)
5946 }
5947
5948 fn saved_state_path(&self) -> Result<Option<String>, ConfigError> {
5950 let path = match self.saved_state.as_ref() {
5951 Some(path) => path,
5952 None => return Ok(None),
5953 };
5954
5955 debug!("saved_stated path in the config: {}", path);
5956 let config_path = PathBuf::from(self.config_path.clone());
5957
5958 debug!("Config path buffer: {:?}", config_path);
5959 let config_dir = config_path
5960 .parent()
5961 .ok_or(ConfigError::SaveStatePath(format!(
5962 "Could get parent directory of config file {config_path:?}"
5963 )))?;
5964
5965 debug!("Config folder: {:?}", config_dir);
5966 if !config_dir.exists() {
5967 create_dir_all(config_dir).map_err(|io_error| {
5968 ConfigError::SaveStatePath(format!(
5969 "failed to create state parent directory '{config_dir:?}': {io_error}"
5970 ))
5971 })?;
5972 }
5973
5974 let mut saved_state_path_raw = config_dir.to_path_buf();
5975 saved_state_path_raw.push(path);
5976 debug!(
5977 "Looking for saved state on the path {:?}",
5978 saved_state_path_raw
5979 );
5980
5981 match metadata(path) {
5982 Err(err) if matches!(err.kind(), ErrorKind::NotFound) => {
5983 info!("Create an empty state file at '{}'", path);
5984 File::create(path).map_err(|io_error| {
5985 ConfigError::SaveStatePath(format!(
5986 "failed to create state file '{path:?}': {io_error}"
5987 ))
5988 })?;
5989 }
5990 _ => {}
5991 }
5992
5993 saved_state_path_raw.canonicalize().map_err(|io_error| {
5994 ConfigError::SaveStatePath(format!(
5995 "could not get saved state path from config file input {path:?}: {io_error}"
5996 ))
5997 })?;
5998
5999 let stringified_path = saved_state_path_raw
6000 .to_str()
6001 .ok_or(ConfigError::SaveStatePath(format!(
6002 "Invalid path {saved_state_path_raw:?}"
6003 )))?
6004 .to_string();
6005
6006 Ok(Some(stringified_path))
6007 }
6008
6009 pub fn load_file(path: &str) -> Result<String, ConfigError> {
6011 std::fs::read_to_string(path).map_err(|io_error| ConfigError::FileRead {
6012 path_to_read: path.to_owned(),
6013 io_error,
6014 })
6015 }
6016
6017 pub fn load_file_bytes(path: &str) -> Result<Vec<u8>, ConfigError> {
6019 std::fs::read(path).map_err(|io_error| ConfigError::FileRead {
6020 path_to_read: path.to_owned(),
6021 io_error,
6022 })
6023 }
6024}
6025
6026impl fmt::Debug for Config {
6027 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
6028 f.debug_struct("Config")
6029 .field("config_path", &self.config_path)
6030 .field("command_socket", &self.command_socket)
6031 .field(
6032 "command_socket_mode",
6033 &format_args!("{:o}", self.command_socket_mode),
6034 )
6035 .field("command_buffer_size", &self.command_buffer_size)
6036 .field("max_command_buffer_size", &self.max_command_buffer_size)
6037 .field("max_connections", &self.max_connections)
6038 .field("min_buffers", &self.min_buffers)
6039 .field("max_buffers", &self.max_buffers)
6040 .field("buffer_size", &self.buffer_size)
6041 .field("saved_state", &self.saved_state)
6042 .field("automatic_state_save", &self.automatic_state_save)
6043 .field("log_level", &self.log_level)
6044 .field("log_target", &self.log_target)
6045 .field("access_logs_target", &self.access_logs_target)
6046 .field("audit_logs_target", &self.audit_logs_target)
6047 .field("audit_logs_json_target", &self.audit_logs_json_target)
6048 .field("access_logs_format", &self.access_logs_format)
6049 .field("worker_count", &self.worker_count)
6050 .field("worker_automatic_restart", &self.worker_automatic_restart)
6051 .field("metrics", &self.metrics)
6052 .field("disable_cluster_metrics", &self.disable_cluster_metrics)
6053 .field("handle_process_affinity", &self.handle_process_affinity)
6054 .field("ctl_command_timeout", &self.ctl_command_timeout)
6055 .field("pid_file_path", &self.pid_file_path)
6056 .field("activate_listeners", &self.activate_listeners)
6057 .field("front_timeout", &self.front_timeout)
6058 .field("back_timeout", &self.back_timeout)
6059 .field("connect_timeout", &self.connect_timeout)
6060 .field("zombie_check_interval", &self.zombie_check_interval)
6061 .field("accept_queue_timeout", &self.accept_queue_timeout)
6062 .field("evict_on_queue_full", &self.evict_on_queue_full)
6063 .field("request_timeout", &self.request_timeout)
6064 .field("worker_timeout", &self.worker_timeout)
6065 .finish()
6066 }
6067}
6068
6069fn toml_error_diagnostic(path: &str, error: &toml::de::Error) -> String {
6076 match error.span() {
6077 Some(Range { start, end }) => format!(
6078 "error parsing the configuration file '{path}' at bytes {start}..{end}: invalid TOML syntax or unexpected value (the offending text is not shown)"
6079 ),
6080 None => format!(
6081 "error parsing the configuration file '{path}': invalid TOML syntax or unexpected value (the offending text is not shown)"
6082 ),
6083 }
6084}
6085
6086impl ServerConfig {
6087 pub const DEFAULT_SLAB_ENTRIES_PER_CONNECTION: u64 = 4;
6094 pub const MIN_SLAB_ENTRIES_PER_CONNECTION: u64 = 2;
6097 pub const MAX_SLAB_ENTRIES_PER_CONNECTION: u64 = 32;
6100
6101 pub fn effective_slab_entries_per_connection(&self) -> u64 {
6104 let effective = match self.slab_entries_per_connection {
6105 Some(0) | None => Self::DEFAULT_SLAB_ENTRIES_PER_CONNECTION,
6106 Some(n) => n.clamp(
6107 Self::MIN_SLAB_ENTRIES_PER_CONNECTION,
6108 Self::MAX_SLAB_ENTRIES_PER_CONNECTION,
6109 ),
6110 };
6111 debug_assert!(
6115 (Self::MIN_SLAB_ENTRIES_PER_CONNECTION..=Self::MAX_SLAB_ENTRIES_PER_CONNECTION)
6116 .contains(&effective),
6117 "effective slab entries per connection must stay within [MIN, MAX]"
6118 );
6119 effective
6120 }
6121
6122 pub fn slab_capacity(&self) -> u64 {
6129 let per_conn = self.effective_slab_entries_per_connection();
6130 let capacity = 10 + per_conn * self.max_connections;
6131 debug_assert!(
6136 capacity >= 10,
6137 "slab capacity must reserve the base entries"
6138 );
6139 debug_assert!(
6140 self.max_connections == 0 || capacity > 10,
6141 "a non-zero connection cap must reserve per-connection slab entries"
6142 );
6143 capacity
6144 }
6145}
6146
6147impl From<&Config> for ServerConfig {
6149 fn from(config: &Config) -> Self {
6150 let metrics = config.metrics.clone().map(|m| ServerMetricsConfig {
6151 address: m.address.to_string(),
6152 tagged_metrics: m.tagged_metrics,
6153 prefix: m.prefix,
6154 detail: Some(MetricDetail::from(m.detail) as i32),
6155 });
6156 let server_config = Self {
6157 max_connections: config.max_connections as u64,
6158 front_timeout: config.front_timeout,
6159 back_timeout: config.back_timeout,
6160 connect_timeout: config.connect_timeout,
6161 zombie_check_interval: config.zombie_check_interval,
6162 accept_queue_timeout: config.accept_queue_timeout,
6163 min_buffers: config.min_buffers,
6164 max_buffers: config.max_buffers,
6165 buffer_size: config.buffer_size,
6166 log_level: config.log_level.clone(),
6167 log_target: config.log_target.clone(),
6168 access_logs_target: config.access_logs_target.clone(),
6169 audit_logs_target: config.audit_logs_target.clone(),
6170 audit_logs_json_target: config.audit_logs_json_target.clone(),
6171 command_buffer_size: config.command_buffer_size,
6172 max_command_buffer_size: config.max_command_buffer_size,
6173 metrics,
6174 access_log_format: ProtobufAccessLogFormat::from(&config.access_logs_format) as i32,
6175 log_colored: config.log_colored,
6176 slab_entries_per_connection: config.slab_entries_per_connection,
6177 basic_auth_max_credential_bytes: config.basic_auth_max_credential_bytes,
6178 evict_on_queue_full: Some(config.evict_on_queue_full),
6179 max_connections_per_ip: Some(config.max_connections_per_ip),
6180 max_connections_per_subnet: Some(config.max_connections_per_subnet),
6181 subnet_ipv4_prefix: Some(config.subnet_ipv4_prefix),
6182 subnet_ipv6_prefix: Some(config.subnet_ipv6_prefix),
6183 retry_after: Some(config.retry_after),
6184 splice_pipe_capacity_bytes: config.splice_pipe_capacity_bytes,
6185 max_connection_attempts: Some(config.max_connection_attempts),
6186 };
6187
6188 debug_assert!(
6193 server_config.min_buffers <= server_config.max_buffers,
6194 "ServerConfig must preserve min_buffers <= max_buffers"
6195 );
6196 debug_assert_eq!(
6197 server_config.buffer_size, config.buffer_size,
6198 "ServerConfig buffer_size must mirror the source config"
6199 );
6200 debug_assert_eq!(
6201 server_config.max_connections, config.max_connections as u64,
6202 "ServerConfig max_connections must mirror the source config"
6203 );
6204 server_config
6205 }
6206}
6207
6208#[cfg(test)]
6209mod tests {
6210 use toml::to_string;
6211
6212 use super::*;
6213
6214 #[test]
6218 fn silent_close_redirect_policy_parses_case_insensitively() {
6219 for spelling in ["silent_close", "SILENT_CLOSE", "Silent_Close"] {
6220 assert_eq!(
6221 parse_redirect_policy(spelling).ok(),
6222 Some(RedirectPolicy::SilentClose),
6223 "{spelling} must parse to SilentClose"
6224 );
6225 }
6226 assert!(matches!(
6227 parse_redirect_policy("silent-close"),
6228 Err(ConfigError::InvalidRedirectPolicy(value)) if value == "silent-close"
6229 ));
6230 }
6231
6232 #[test]
6233 fn http_frontend_debug_redacts_pem_material() {
6234 const CERTIFICATE_SECRET: &str = "HTTP_FRONTEND_CERTIFICATE_PEM_SECRET_SENTINEL";
6235 const CHAIN_SECRET: &str = "HTTP_FRONTEND_CHAIN_PEM_SECRET_SENTINEL";
6236 const KEY_SECRET: &str = "HTTP_FRONTEND_KEY_PEM_SECRET_SENTINEL";
6237 const HOSTNAME_SECRET: &str = "HTTP_FRONTEND_HOSTNAME_SECRET_SENTINEL";
6238 const PATH_SECRET: &str = "HTTP_FRONTEND_PATH_SECRET_SENTINEL";
6239 const METHOD_SECRET: &str = "HTTP_FRONTEND_METHOD_SECRET_SENTINEL";
6240 const TAG_KEY_SECRET: &str = "HTTP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
6241 const TAG_SECRET: &str = "HTTP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
6242 const HEADER_KEY_SECRET: &str = "HTTP_FRONTEND_HEADER_KEY_SECRET_SENTINEL";
6243 const HEADER_SECRET: &str = "HTTP_FRONTEND_HEADER_VALUE_SECRET_SENTINEL";
6244 const REDIRECT_TEMPLATE_SECRET: &str = "HTTP_FRONTEND_REDIRECT_TEMPLATE_SECRET_SENTINEL";
6245 const REWRITE_HOST_SECRET: &str = "HTTP_FRONTEND_REWRITE_HOST_SECRET_SENTINEL";
6246 const REWRITE_PATH_SECRET: &str = "HTTP_FRONTEND_REWRITE_PATH_SECRET_SENTINEL";
6247
6248 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
6249 let certificate = long_value(CERTIFICATE_SECRET);
6250 let certificate_chain = long_value(CHAIN_SECRET);
6251 let key = long_value(KEY_SECRET);
6252 let hostname = long_value(HOSTNAME_SECRET);
6253 let path = long_value(PATH_SECRET);
6254 let method = long_value(METHOD_SECRET);
6255 let redirect_template = long_value(REDIRECT_TEMPLATE_SECRET);
6256 let rewrite_host = long_value(REWRITE_HOST_SECRET);
6257 let rewrite_path = long_value(REWRITE_PATH_SECRET);
6258
6259 let frontend = HttpFrontendConfig {
6260 address: "127.0.0.1:8443".parse().unwrap(),
6261 hostname,
6262 path: PathRule::prefix(path),
6263 method: Some(method),
6264 certificate: Some(certificate),
6265 key: Some(key),
6266 certificate_chain: Some(vec![certificate_chain]),
6267 tls_versions: vec![TlsVersion::TlsV13],
6268 position: RulePosition::Tree,
6269 tags: Some(BTreeMap::from([(
6270 long_value(TAG_KEY_SECRET),
6271 long_value(TAG_SECRET),
6272 )])),
6273 redirect: None,
6274 redirect_scheme: None,
6275 redirect_template: Some(redirect_template),
6276 rewrite_host: Some(rewrite_host),
6277 rewrite_path: Some(rewrite_path),
6278 rewrite_port: None,
6279 required_auth: None,
6280 headers: vec![Header {
6281 position: HeaderPosition::Request as i32,
6282 key: long_value(HEADER_KEY_SECRET),
6283 val: long_value(HEADER_SECRET),
6284 }],
6285 hsts: None,
6286 header_predicates: Vec::new(),
6287 };
6288
6289 let output = format!("{frontend:?}");
6290
6291 let secrets = [
6292 CERTIFICATE_SECRET,
6293 CHAIN_SECRET,
6294 KEY_SECRET,
6295 HOSTNAME_SECRET,
6296 PATH_SECRET,
6297 METHOD_SECRET,
6298 TAG_KEY_SECRET,
6299 TAG_SECRET,
6300 HEADER_KEY_SECRET,
6301 HEADER_SECRET,
6302 REDIRECT_TEMPLATE_SECRET,
6303 REWRITE_HOST_SECRET,
6304 REWRITE_PATH_SECRET,
6305 ];
6306 for secret in secrets {
6307 assert!(
6308 !output.contains(secret),
6309 "HttpFrontendConfig Debug leaked secret marker {secret}: {output}"
6310 );
6311 }
6312 let expected_metadata = [
6313 "address: 127.0.0.1:8443".to_owned(),
6314 format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
6315 format!("path_kind: {}", PathRule::prefix(String::new()).kind),
6316 format!("path_len: {}", long_value(PATH_SECRET).len()),
6317 format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
6318 "certificate: Some(\"[redacted]\")".to_owned(),
6319 format!(
6320 "certificate_len: Some({})",
6321 long_value(CERTIFICATE_SECRET).len()
6322 ),
6323 "key: Some(\"[redacted]\")".to_owned(),
6324 format!("key_len: Some({})", long_value(KEY_SECRET).len()),
6325 "certificate_chain: Some(\"[redacted]\")".to_owned(),
6326 "certificate_chain_count: Some(1)".to_owned(),
6327 format!(
6328 "certificate_chain_len: Some({})",
6329 long_value(CHAIN_SECRET).len()
6330 ),
6331 "tls_versions_count: 1".to_owned(),
6332 "tags_count: 1".to_owned(),
6333 format!(
6334 "redirect_template_len: Some({})",
6335 long_value(REDIRECT_TEMPLATE_SECRET).len()
6336 ),
6337 format!(
6338 "rewrite_host_len: Some({})",
6339 long_value(REWRITE_HOST_SECRET).len()
6340 ),
6341 format!(
6342 "rewrite_path_len: Some({})",
6343 long_value(REWRITE_PATH_SECRET).len()
6344 ),
6345 "headers_count: 1".to_owned(),
6346 ];
6347 for safe_metadata in expected_metadata {
6348 assert!(
6349 output.contains(&safe_metadata),
6350 "HttpFrontendConfig Debug omitted safe metadata {safe_metadata}: {output}"
6351 );
6352 }
6353 assert!(
6354 output.len() <= 1024,
6355 "HttpFrontendConfig Debug output is not bounded: {} bytes",
6356 output.len()
6357 );
6358
6359 for (index, request) in frontend
6360 .generate_requests("safe-cluster-id")
6361 .into_iter()
6362 .enumerate()
6363 {
6364 let generated_output = format!("{request:?}");
6365 for secret in secrets {
6366 assert!(
6367 !generated_output.contains(secret),
6368 "generated frontend request {index} Debug leaked secret marker {secret}: {generated_output}"
6369 );
6370 }
6371 assert!(
6372 generated_output.len() <= 2048,
6373 "generated frontend request {index} Debug output is not bounded: {} bytes",
6374 generated_output.len()
6375 );
6376 }
6377 }
6378
6379 #[test]
6380 fn hsts_to_proto_enabled_substitutes_default_max_age() {
6381 let cfg = FileHstsConfig {
6382 enabled: Some(true),
6383 max_age: None,
6384 include_subdomains: None,
6385 preload: None,
6386 force_replace_backend: None,
6387 };
6388 let proto = cfg.to_proto("test").expect("should validate");
6389 assert_eq!(proto.enabled, Some(true));
6390 assert_eq!(proto.max_age, Some(DEFAULT_HSTS_MAX_AGE));
6391 }
6392
6393 #[test]
6394 fn hsts_to_proto_explicit_max_age_kept() {
6395 let cfg = FileHstsConfig {
6396 enabled: Some(true),
6397 max_age: Some(63_072_000),
6398 include_subdomains: Some(true),
6399 preload: Some(true),
6400 force_replace_backend: None,
6401 };
6402 let proto = cfg.to_proto("test").expect("should validate");
6403 assert_eq!(proto.max_age, Some(63_072_000));
6404 assert_eq!(proto.include_subdomains, Some(true));
6405 assert_eq!(proto.preload, Some(true));
6406 }
6407
6408 #[test]
6409 fn hsts_to_proto_disabled_keeps_zero_intent() {
6410 let cfg = FileHstsConfig {
6414 enabled: Some(false),
6415 max_age: None,
6416 include_subdomains: None,
6417 preload: None,
6418 force_replace_backend: None,
6419 };
6420 let proto = cfg.to_proto("test").expect("should validate");
6421 assert_eq!(proto.enabled, Some(false));
6422 }
6423
6424 #[test]
6425 fn hsts_to_proto_kill_switch_max_age_zero_allowed() {
6426 let cfg = FileHstsConfig {
6430 enabled: Some(true),
6431 max_age: Some(0),
6432 include_subdomains: None,
6433 preload: None,
6434 force_replace_backend: None,
6435 };
6436 let proto = cfg.to_proto("test").expect("kill-switch must validate");
6437 assert_eq!(proto.max_age, Some(0));
6438 }
6439
6440 #[test]
6441 fn hsts_to_proto_missing_enabled_errors() {
6442 let cfg = FileHstsConfig {
6443 enabled: None,
6444 max_age: Some(31_536_000),
6445 include_subdomains: None,
6446 preload: None,
6447 force_replace_backend: None,
6448 };
6449 match cfg.to_proto("test").unwrap_err() {
6450 ConfigError::HstsEnabledRequired(scope) => assert_eq!(scope, "test"),
6451 other => panic!("expected HstsEnabledRequired, got {other:?}"),
6452 }
6453 }
6454
6455 #[test]
6456 fn hsts_rejected_on_http_listener() {
6457 let mut listener = ListenerBuilder::new(
6462 SocketAddress::new_v4(127, 0, 0, 1, 8080),
6463 ListenerProtocol::Http,
6464 );
6465 listener.hsts = Some(FileHstsConfig {
6466 enabled: Some(true),
6467 max_age: Some(31_536_000),
6468 include_subdomains: None,
6469 preload: None,
6470 force_replace_backend: None,
6471 });
6472 match listener.to_http(None).unwrap_err() {
6473 ConfigError::HstsOnPlainHttp(scope) => assert!(
6474 scope.contains("HTTP listener"),
6475 "expected scope to mention 'HTTP listener', got {scope:?}"
6476 ),
6477 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
6478 }
6479 }
6480
6481 #[test]
6490 fn h2_flood_threshold_zero_rejected_on_http_listener() {
6491 let mut listener = ListenerBuilder::new(
6492 SocketAddress::new_v4(127, 0, 0, 1, 8080),
6493 ListenerProtocol::Http,
6494 );
6495 listener.h2_max_rst_stream_per_window = Some(0);
6496 match listener.to_http(None).unwrap_err() {
6497 ConfigError::H2ThresholdBelowMinimum {
6498 key,
6499 value,
6500 minimum,
6501 ..
6502 } => {
6503 assert_eq!(key, "h2_max_rst_stream_per_window");
6504 assert_eq!(value, 0);
6505 assert_eq!(minimum, 1);
6506 }
6507 other => panic!("expected H2ThresholdBelowMinimum, got {other:?}"),
6508 }
6509 }
6510
6511 #[test]
6515 fn h2_flood_threshold_zero_rejected_on_https_listener() {
6516 let mut listener = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
6517 listener.h2_max_rst_stream_abusive_lifetime = Some(0);
6518 match listener.to_tls(None).unwrap_err() {
6519 ConfigError::H2ThresholdBelowMinimum {
6520 key,
6521 value,
6522 minimum,
6523 ..
6524 } => {
6525 assert_eq!(key, "h2_max_rst_stream_abusive_lifetime");
6526 assert_eq!(value, 0);
6527 assert_eq!(minimum, 1);
6528 }
6529 other => panic!("expected H2ThresholdBelowMinimum, got {other:?}"),
6530 }
6531 }
6532
6533 #[test]
6538 fn h2_stream_shrink_ratio_below_two_rejected_on_http_listener() {
6539 let mut listener = ListenerBuilder::new(
6540 SocketAddress::new_v4(127, 0, 0, 1, 8080),
6541 ListenerProtocol::Http,
6542 );
6543 listener.h2_stream_shrink_ratio = Some(1);
6544 match listener.to_http(None).unwrap_err() {
6545 ConfigError::H2ThresholdBelowMinimum {
6546 key,
6547 value,
6548 minimum,
6549 ..
6550 } => {
6551 assert_eq!(key, "h2_stream_shrink_ratio");
6552 assert_eq!(value, 1);
6553 assert_eq!(minimum, 2);
6554 }
6555 other => panic!("expected H2ThresholdBelowMinimum, got {other:?}"),
6556 }
6557 }
6558
6559 #[test]
6562 fn h2_thresholds_at_their_minimum_accepted() {
6563 let mut listener = ListenerBuilder::new(
6564 SocketAddress::new_v4(127, 0, 0, 1, 8080),
6565 ListenerProtocol::Http,
6566 );
6567 listener.h2_max_rst_stream_per_window = Some(1);
6568 listener.h2_max_rst_stream_abusive_lifetime = Some(1);
6569 listener.h2_stream_shrink_ratio = Some(2);
6570 let config = listener
6571 .to_http(None)
6572 .expect("thresholds at their minimum must be accepted");
6573 assert_eq!(config.h2_max_rst_stream_per_window, Some(1));
6574 assert_eq!(config.h2_max_rst_stream_abusive_lifetime, Some(1));
6575 assert_eq!(config.h2_stream_shrink_ratio, Some(2));
6576 }
6577
6578 #[test]
6579 fn hsts_rejected_on_http_frontend() {
6580 let frontend = FileClusterFrontendConfig {
6586 address: "127.0.0.1:8080".parse().unwrap(),
6587 hostname: Some("example.com".to_owned()),
6588 alpn: vec![],
6589 path: None,
6590 path_type: None,
6591 method: None,
6592 certificate: None,
6593 key: None,
6594 certificate_chain: None,
6595 tls_versions: vec![],
6596 position: RulePosition::Tree,
6597 tags: None,
6598 redirect: None,
6599 redirect_scheme: None,
6600 redirect_template: None,
6601 rewrite_host: None,
6602 rewrite_path: None,
6603 rewrite_port: None,
6604 required_auth: None,
6605 headers: None,
6606 hsts: Some(FileHstsConfig {
6607 enabled: Some(true),
6608 max_age: Some(31_536_000),
6609 include_subdomains: None,
6610 preload: None,
6611 force_replace_backend: None,
6612 }),
6613 header_predicates: None,
6614 };
6615 match frontend.to_http_front("api").unwrap_err() {
6616 ConfigError::HstsOnPlainHttp(scope) => {
6617 assert!(
6618 scope.contains("api") && scope.contains("example.com"),
6619 "expected scope to mention 'api' and 'example.com', got {scope:?}"
6620 );
6621 }
6622 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
6623 }
6624 }
6625
6626 #[test]
6630 fn path_type_regex_path_parses_to_the_path_only_regex_kind() {
6631 let parse = |path_type: &str| {
6632 toml::from_str::<FileClusterFrontendConfig>(&format!(
6633 "address = \"127.0.0.1:8080\"\n\
6634 hostname = \"example.com\"\n\
6635 path = \"^/api/v2$\"\n\
6636 path_type = \"{path_type}\"\n"
6637 ))
6638 };
6639 let frontend = parse("REGEX_PATH").expect("REGEX_PATH must parse");
6640 assert_eq!(frontend.path_type, Some(PathRuleType::RegexPath));
6641 let http = frontend
6642 .to_http_front("api")
6643 .expect("a REGEX_PATH frontend must convert");
6644 assert_eq!(http.path, PathRule::regex_path("^/api/v2$"));
6645 assert_eq!(
6646 crate::proto::command::PathRuleKind::try_from(http.path.kind),
6647 Ok(crate::proto::command::PathRuleKind::RegexPath)
6648 );
6649 assert_ne!(
6650 http.path,
6651 PathRule::regex("^/api/v2$"),
6652 "REGEX_PATH must not collapse into REGEX"
6653 );
6654
6655 let regex = parse("REGEX").expect("REGEX must still parse");
6656 assert_eq!(regex.path_type, Some(PathRuleType::Regex));
6657 assert!(
6658 parse("PATH_REGEX").is_err(),
6659 "an unknown path_type is refused"
6660 );
6661 }
6662
6663 #[test]
6664 fn serialize() {
6665 let http = ListenerBuilder::new(
6666 SocketAddress::new_v4(127, 0, 0, 1, 8080),
6667 ListenerProtocol::Http,
6668 )
6669 .with_answer_404_path(Some("404.html"))
6670 .to_owned();
6671 println!("http: {:?}", to_string(&http));
6672
6673 let https = ListenerBuilder::new(
6674 SocketAddress::new_v4(127, 0, 0, 1, 8443),
6675 ListenerProtocol::Https,
6676 )
6677 .with_answer_404_path(Some("404.html"))
6678 .to_owned();
6679 println!("https: {:?}", to_string(&https));
6680
6681 let listeners = vec![http, https];
6682 let config = FileConfig {
6683 command_socket: Some(String::from("./command_folder/sock")),
6684 worker_count: Some(2),
6685 worker_automatic_restart: Some(true),
6686 max_connections: Some(500),
6687 min_buffers: Some(1),
6688 max_buffers: Some(500),
6689 buffer_size: Some(16393),
6690 metrics: Some(MetricsConfig {
6691 address: "127.0.0.1:8125".parse().unwrap(),
6692 tagged_metrics: false,
6693 prefix: Some(String::from("sozu-metrics")),
6694 detail: MetricDetailLevel::default(),
6695 }),
6696 listeners: Some(listeners),
6697 ..Default::default()
6698 };
6699
6700 println!("config: {:?}", to_string(&config));
6701 let encoded = to_string(&config).unwrap();
6702 println!("conf:\n{encoded}");
6703 }
6704
6705 #[test]
6706 fn parse() {
6707 let path = "assets/config.toml";
6708 let config = Config::load_from_path(path).unwrap_or_else(|load_error| {
6709 panic!("Cannot load config from path {path}: {load_error:?}")
6710 });
6711 println!("config: {config:#?}");
6712 }
6714
6715 #[test]
6716 fn multiple_listeners_preserve_per_address_expect_proxy() {
6717 let toml_content = r#"
6718 command_socket = "/tmp/sozu_test.sock"
6719 worker_count = 1
6720
6721 [[listeners]]
6722 protocol = "http"
6723 address = "172.16.20.1:80"
6724 expect_proxy = true
6725
6726 [[listeners]]
6727 protocol = "http"
6728 address = "10.22.0.1:80"
6729 expect_proxy = false
6730
6731 [[listeners]]
6732 protocol = "https"
6733 address = "192.168.1.1:443"
6734 expect_proxy = true
6735
6736 [[listeners]]
6737 protocol = "https"
6738 address = "192.168.2.1:443"
6739 expect_proxy = false
6740 "#;
6741
6742 let file_config: FileConfig =
6743 toml::from_str(toml_content).expect("Could not parse TOML config");
6744
6745 let listeners = file_config.listeners.as_ref().expect("No listeners found");
6746 assert_eq!(listeners.len(), 4);
6747
6748 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
6749 .into_config()
6750 .expect("Could not build config");
6751
6752 assert_eq!(config.http_listeners.len(), 2);
6753 assert_eq!(config.https_listeners.len(), 2);
6754
6755 let http_proxy = config
6757 .http_listeners
6758 .iter()
6759 .find(|l| SocketAddr::from(l.address) == "172.16.20.1:80".parse().unwrap())
6760 .expect("Listener on 172.16.20.1:80 not found");
6761 let http_direct = config
6762 .http_listeners
6763 .iter()
6764 .find(|l| SocketAddr::from(l.address) == "10.22.0.1:80".parse().unwrap())
6765 .expect("Listener on 10.22.0.1:80 not found");
6766
6767 assert!(http_proxy.expect_proxy);
6768 assert!(!http_direct.expect_proxy);
6769
6770 let https_proxy = config
6772 .https_listeners
6773 .iter()
6774 .find(|l| SocketAddr::from(l.address) == "192.168.1.1:443".parse().unwrap())
6775 .expect("Listener on 192.168.1.1:443 not found");
6776 let https_direct = config
6777 .https_listeners
6778 .iter()
6779 .find(|l| SocketAddr::from(l.address) == "192.168.2.1:443".parse().unwrap())
6780 .expect("Listener on 192.168.2.1:443 not found");
6781
6782 assert!(https_proxy.expect_proxy);
6783 assert!(!https_direct.expect_proxy);
6784 }
6785
6786 #[test]
6790 fn h2c_prior_knowledge_is_parsed_from_toml_and_absent_when_unset() {
6791 let toml_content = r#"
6792 [[listeners]]
6793 protocol = "http"
6794 address = "127.0.0.1:8080"
6795 h2c_prior_knowledge = true
6796
6797 [[listeners]]
6798 protocol = "http"
6799 address = "127.0.0.1:8081"
6800 h2c_prior_knowledge = false
6801
6802 [[listeners]]
6803 protocol = "http"
6804 address = "127.0.0.1:8082"
6805 "#;
6806
6807 let file_config: FileConfig =
6808 toml::from_str(toml_content).expect("Could not parse TOML config");
6809 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
6810 .into_config()
6811 .expect("Could not build config");
6812
6813 let knob = |address: &str| {
6814 config
6815 .http_listeners
6816 .iter()
6817 .find(|l| SocketAddr::from(l.address) == address.parse().unwrap())
6818 .unwrap_or_else(|| panic!("listener on {address} not found"))
6819 .h2c_prior_knowledge
6820 };
6821 assert_eq!(knob("127.0.0.1:8080"), Some(true));
6822 assert_eq!(knob("127.0.0.1:8081"), Some(false));
6823 assert_eq!(knob("127.0.0.1:8082"), None);
6824 }
6825
6826 fn http_cluster_from_backends(backends: &str) -> Result<HttpClusterConfig, ConfigError> {
6828 let file: FileClusterConfig = toml::from_str(&format!(
6829 "protocol = \"http\"\nfrontends = []\nbackends = [{backends}]\n"
6830 ))
6831 .expect("the cluster must parse");
6832 match file.to_cluster_config("cluster", &HashSet::new())? {
6833 ClusterConfig::Http(http) => Ok(http),
6834 ClusterConfig::Tcp(_) => panic!("an http cluster must build as one"),
6835 }
6836 }
6837
6838 #[test]
6842 fn a_backend_hostname_resolves_when_the_file_is_loaded() {
6843 let backends = r#"{ address = "localhost:8080", weight = 50 }"#;
6844 let http = http_cluster_from_backends(backends).expect("localhost must resolve");
6845
6846 assert!(
6847 !http.backends.is_empty(),
6848 "localhost resolves to an address"
6849 );
6850 let mut ids = HashSet::new();
6851 for backend in &http.backends {
6852 assert!(backend.address.ip().is_loopback(), "{backend:?}");
6853 assert_eq!(backend.address.port(), 8080);
6854 assert_eq!(backend.weight, Some(50));
6855 assert_eq!(
6856 backend.backend_id.as_deref(),
6857 Some(format!("cluster-localhost:8080-{}", backend.address).as_str())
6858 );
6859 assert!(ids.insert(backend.backend_id.clone()), "ids are distinct");
6860 }
6861 assert_eq!(
6862 http_cluster_from_backends(backends)
6863 .expect("localhost must resolve")
6864 .backends,
6865 http.backends,
6866 "the same file yields the same backends"
6867 );
6868
6869 let orders = http.generate_requests().expect("the orders must build");
6870 let added: Vec<&AddBackend> = orders
6871 .iter()
6872 .filter_map(|order| match order.request_type.as_ref() {
6873 Some(RequestType::AddBackend(add)) => Some(add),
6874 _ => None,
6875 })
6876 .collect();
6877 assert_eq!(added.len(), http.backends.len());
6878 for (add, backend) in added.iter().zip(&http.backends) {
6879 assert_eq!(Some(&add.backend_id), backend.backend_id.as_ref());
6880 assert_eq!(SocketAddr::from(add.address), backend.address);
6881 }
6882 }
6883
6884 #[test]
6887 fn a_literal_backend_address_keeps_its_positional_id() {
6888 let http = http_cluster_from_backends(
6889 r#"{ address = "127.0.0.1:8080" }, { address = "[::1]:8081", backend_id = "six" }"#,
6890 )
6891 .expect("literals must build");
6892 assert_eq!(
6893 http.backends,
6894 vec![
6895 BackendConfig {
6896 address: "127.0.0.1:8080".parse().unwrap(),
6897 weight: None,
6898 sticky_id: None,
6899 backup: None,
6900 backend_id: Some("cluster-0-127.0.0.1:8080".to_owned()),
6901 unix_path: None,
6902 },
6903 BackendConfig {
6904 address: "[::1]:8081".parse().unwrap(),
6905 weight: None,
6906 sticky_id: None,
6907 backup: None,
6908 backend_id: Some("six".to_owned()),
6909 unix_path: None,
6910 },
6911 ]
6912 );
6913 }
6914
6915 #[test]
6920 fn a_literal_after_a_name_keeps_its_file_position_id() {
6921 let one: SocketAddr = "10.0.0.2:8080".parse().unwrap();
6922 let two: SocketAddr = "10.0.0.3:8080".parse().unwrap();
6923 let answer = |addresses: Vec<SocketAddr>| {
6924 move |_: &str| -> std::io::Result<Vec<SocketAddr>> { Ok(addresses.clone()) }
6925 };
6926
6927 for (protocol, addresses) in [
6928 ("http", vec![one]),
6929 ("http", vec![one, two]),
6930 ("tcp", vec![one]),
6931 ("tcp", vec![one, two]),
6932 ] {
6933 let resolver = answer(addresses.clone());
6934 let file: FileClusterConfig = toml::from_str(&format!(
6935 "protocol = \"{protocol}\"\nfrontends = []\n\
6936 backends = [{{ address = \"name:8080\" }}, {{ address = \"127.0.0.1:9000\" }}]\n"
6937 ))
6938 .expect("the cluster must parse");
6939 let backends = match file
6940 .to_cluster_config_with("cluster", &HashSet::new(), Some(&resolver))
6941 .expect("the cluster must build")
6942 {
6943 ClusterConfig::Http(http) => http.backends,
6944 ClusterConfig::Tcp(tcp) => tcp.backends,
6945 };
6946 assert_eq!(backends.len(), addresses.len() + 1, "{protocol}");
6947 assert_eq!(
6948 backends.last().and_then(|b| b.backend_id.as_deref()),
6949 Some("cluster-1-127.0.0.1:9000"),
6950 "{protocol} with {} resolved address(es)",
6951 addresses.len()
6952 );
6953 }
6954
6955 let error = toml::from_str::<FileClusterConfig>(
6956 "protocol = \"tcp\"\nfrontends = []\nbackends = [{ address = \"localhost\" }]\n",
6957 )
6958 .expect("the cluster must parse")
6959 .to_cluster_config("cluster", &HashSet::new())
6960 .expect_err("a missing port refuses the tcp cluster");
6961 assert!(matches!(
6962 error,
6963 ConfigError::BackendAddressResolution { .. }
6964 ));
6965 }
6966
6967 #[test]
6971 fn numeric_shorthand_backend_hosts_never_reach_the_resolver() {
6972 let shorthand = |_: &str| -> std::io::Result<Vec<SocketAddr>> {
6973 Ok(vec!["8.0.0.1:8080".parse().unwrap()])
6974 };
6975 for address in [
6976 "010.0.0.1:8080",
6977 "1.2.3:8080",
6978 "2130706433:8080",
6979 "0x7f000001:8080",
6980 "0x7f.1:8080",
6981 "0X7F.0.0.1:8080",
6982 "0177.1:8080",
6983 "0x7f.0177.1:8080",
6984 ] {
6985 let entry = FileBackendConfig {
6986 address: address.to_owned(),
6987 weight: None,
6988 sticky_id: None,
6989 backup: None,
6990 backend_id: None,
6991 unix_path: None,
6992 };
6993 let error = entry
6994 .resolve("cluster", 0, &shorthand)
6995 .expect_err("numeric shorthand must be refused");
6996 let ConfigError::BackendAddressResolution {
6997 cluster_id,
6998 address: entry_address,
6999 ..
7000 } = &error
7001 else {
7002 panic!("{address}: unexpected error {error:?}");
7003 };
7004 assert_eq!(cluster_id, "cluster");
7005 assert_eq!(entry_address, address);
7006 }
7007 }
7008
7009 #[test]
7014 fn unbracketed_ipv6_backend_hosts_never_reach_the_resolver() {
7015 let unreachable = |address: &str| -> std::io::Result<Vec<SocketAddr>> {
7016 panic!("{address}: must not reach the resolver")
7017 };
7018 for address in ["::1:8080", "2001:db8::1:8080", "fe80::1%eth0:80"] {
7019 let entry = FileBackendConfig {
7020 address: address.to_owned(),
7021 weight: None,
7022 sticky_id: None,
7023 backup: None,
7024 backend_id: None,
7025 unix_path: None,
7026 };
7027 let error = entry
7028 .resolve("cluster", 0, &unreachable)
7029 .expect_err("an unbracketed IPv6 literal must be refused");
7030 let ConfigError::BackendAddressResolution {
7031 cluster_id,
7032 address: entry_address,
7033 reason,
7034 } = &error
7035 else {
7036 panic!("{address}: unexpected error {error:?}");
7037 };
7038 assert_eq!(cluster_id, "cluster");
7039 assert_eq!(entry_address, address);
7040 assert!(reason.contains("[address]:port"), "{address}: {reason}");
7041 }
7042
7043 let entry = FileBackendConfig {
7044 address: "[2001:db8::1]:8080".to_owned(),
7045 weight: Some(3),
7046 sticky_id: Some("sticky".to_owned()),
7047 backup: Some(true),
7048 backend_id: None,
7049 unix_path: None,
7050 };
7051 let backends = entry
7052 .resolve("cluster", 2, &unreachable)
7053 .expect("a bracketed IPv6 literal parses directly");
7054 let address: SocketAddr = "[2001:db8::1]:8080".parse().unwrap();
7055 assert_eq!(backends.len(), 1);
7056 assert_eq!(backends[0].address, address);
7057 assert_eq!(backends[0].weight, Some(3));
7058 assert_eq!(backends[0].sticky_id.as_deref(), Some("sticky"));
7059 assert_eq!(backends[0].backup, Some(true));
7060 assert_eq!(
7061 backends[0].backend_id.as_deref(),
7062 Some("cluster-2-[2001:db8::1]:8080")
7063 );
7064 }
7065
7066 #[test]
7069 fn benign_backend_hostnames_are_not_numeric_shorthand() {
7070 let resolved = "10.0.0.2:8080".parse::<SocketAddr>().unwrap();
7071 let resolver = |_: &str| -> std::io::Result<Vec<SocketAddr>> { Ok(vec![resolved]) };
7072 for address in [
7073 "123.example.com:8080",
7074 "dead.beef:8080",
7075 "0x.example.com:8080",
7076 "1.2.3.example:8080",
7077 "localhost:8080",
7078 ] {
7079 let entry = FileBackendConfig {
7080 address: address.to_owned(),
7081 weight: None,
7082 sticky_id: None,
7083 backup: None,
7084 backend_id: None,
7085 unix_path: None,
7086 };
7087 let backends = entry
7088 .resolve("cluster", 0, &resolver)
7089 .unwrap_or_else(|error| panic!("{address}: {error:?}"));
7090 assert_eq!(backends.len(), 1, "{address}");
7091 }
7092 assert!(!is_legacy_numeric_host("dead.beef"));
7093 assert!(!is_legacy_numeric_host("1..2"));
7094 assert!(is_legacy_numeric_host("0x7f.1"));
7095 }
7096
7097 #[test]
7101 fn a_control_load_does_not_resolve_backend_names() {
7102 let dir = tempfile::tempdir().expect("tempdir");
7103 let path = dir.path().join("config.toml");
7104 std::fs::write(
7105 &path,
7106 "command_socket = \"/tmp/sozu_test.sock\"\n\
7107 ctl_command_timeout = 4242\n\
7108 [clusters.app]\n\
7109 protocol = \"http\"\n\
7110 frontends = [{ address = \"127.0.0.1:8080\", hostname = \"example.org\" }]\n\
7111 backends = [{ address = \"1.2.3:8080\" }, { address = \"127.0.0.1:9000\" }]\n",
7112 )
7113 .expect("write config");
7114 let path = path.to_str().expect("utf-8 path");
7115
7116 assert!(matches!(
7117 Config::load_from_path(path),
7118 Err(ConfigError::BackendAddressResolution { .. })
7119 ));
7120
7121 let config = Config::load_from_path_for_control(path).expect("control load");
7122 assert_eq!(config.ctl_command_timeout, 4242);
7123 let Some(ClusterConfig::Http(app)) = config.clusters.get("app") else {
7124 panic!("the cluster must still be validated and built");
7125 };
7126 assert_eq!(app.backends.len(), 1, "only the literal is kept");
7127 }
7128
7129 #[test]
7133 fn an_unresolvable_backend_address_is_a_config_error() {
7134 for address in ["localhost", "localhost:notaport"] {
7135 let error = http_cluster_from_backends(&format!(r#"{{ address = "{address}" }}"#))
7136 .expect_err("the address must not resolve");
7137 let ConfigError::BackendAddressResolution {
7138 cluster_id,
7139 address: entry,
7140 ..
7141 } = &error
7142 else {
7143 panic!("{address}: unexpected error {error:?}");
7144 };
7145 assert_eq!(cluster_id, "cluster");
7146 assert_eq!(entry, address);
7147 assert!(error.to_string().contains(address), "{error}");
7148 }
7149 }
7150
7151 #[test]
7154 fn every_expanded_backend_keeps_its_unix_path() {
7155 let entry = |address: &str| FileBackendConfig {
7156 address: address.to_owned(),
7157 weight: None,
7158 sticky_id: None,
7159 backup: None,
7160 backend_id: None,
7161 unix_path: Some("/run/app.sock".to_owned()),
7162 };
7163 let v4: SocketAddr = "10.0.0.2:8080".parse().unwrap();
7164 let v6: SocketAddr = "[fd00::1]:8080".parse().unwrap();
7165 let expanded = entry("ingress:8080")
7166 .expand("cluster", vec![v4, v6])
7167 .expect("two addresses expand");
7168 assert_eq!(expanded.len(), 2);
7169 assert!(
7170 expanded
7171 .iter()
7172 .all(|backend| backend.unix_path.as_deref() == Some("/run/app.sock"))
7173 );
7174
7175 let literal = entry("127.0.0.1:8080")
7176 .resolve("cluster", 0, &system_resolver)
7177 .expect("a literal resolves");
7178 assert_eq!(literal[0].unix_path.as_deref(), Some("/run/app.sock"));
7179 }
7180
7181 #[test]
7185 fn a_name_with_several_addresses_expands_into_distinct_backends() {
7186 let entry = FileBackendConfig {
7187 address: "ingress:8080".to_owned(),
7188 weight: None,
7189 sticky_id: Some("pinned".to_owned()),
7190 backup: Some(true),
7191 backend_id: Some("ingress".to_owned()),
7192 unix_path: None,
7193 };
7194 let v4: SocketAddr = "10.0.0.2:8080".parse().unwrap();
7195 let v6: SocketAddr = "[fd00::1]:8080".parse().unwrap();
7196 let backends = entry
7197 .expand("cluster", vec![v6, v4, v6])
7198 .expect("two addresses expand");
7199 assert_eq!(
7200 backends,
7201 vec![
7202 BackendConfig {
7203 address: v4,
7204 weight: None,
7205 sticky_id: Some(format!("pinned-{v4}")),
7206 backup: Some(true),
7207 backend_id: Some(format!("ingress-{v4}")),
7208 unix_path: None,
7209 },
7210 BackendConfig {
7211 address: v6,
7212 weight: None,
7213 sticky_id: Some(format!("pinned-{v6}")),
7214 backup: Some(true),
7215 backend_id: Some(format!("ingress-{v6}")),
7216 unix_path: None,
7217 },
7218 ]
7219 );
7220
7221 let single = entry.expand("cluster", vec![v4]).expect("one address");
7223 assert_eq!(single[0].sticky_id, Some(format!("pinned-{v4}")));
7224 assert!(matches!(
7225 entry.expand("cluster", Vec::new()),
7226 Err(ConfigError::BackendAddressResolution { .. })
7227 ));
7228 }
7229
7230 #[test]
7233 fn an_affinity_header_reaches_the_add_cluster_order() {
7234 let file: FileClusterConfig = toml::from_str(
7235 r#"
7236 protocol = "http"
7237 load_balancing = "HRW"
7238 affinity_header = "X-Tenant"
7239 frontends = []
7240 backends = []
7241 "#,
7242 )
7243 .expect("the cluster must parse");
7244 let ClusterConfig::Http(http) = file
7245 .to_cluster_config("keyed", &HashSet::new())
7246 .expect("the cluster must build")
7247 else {
7248 panic!("an http cluster must build as one");
7249 };
7250 let orders = http.generate_requests().expect("the orders must build");
7251 let Some(RequestType::AddCluster(cluster)) =
7252 orders.first().and_then(|order| order.request_type.as_ref())
7253 else {
7254 panic!("the orders must lead with an AddCluster");
7255 };
7256 assert_eq!(cluster.affinity_header.as_deref(), Some("X-Tenant"));
7257 assert_eq!(cluster.affinity_cookie, None);
7258 }
7259
7260 #[test]
7264 fn a_unix_path_backend_reaches_the_add_unix_backend_order() {
7265 let file: FileClusterConfig = toml::from_str(
7266 r#"
7267 protocol = "http"
7268 frontends = []
7269 backends = [
7270 { address = "127.0.0.1:1", unix_path = "/run/app.sock" },
7271 { address = "127.0.0.1:2" },
7272 ]
7273 "#,
7274 )
7275 .expect("the cluster must parse");
7276 let ClusterConfig::Http(http) = file
7277 .to_cluster_config("app", &HashSet::new())
7278 .expect("the cluster must build")
7279 else {
7280 panic!("an http cluster must build as one");
7281 };
7282 let unix_paths: Vec<_> = http
7283 .generate_requests()
7284 .expect("the orders must build")
7285 .into_iter()
7286 .filter_map(|order| match order.request_type {
7287 Some(RequestType::AddUnixBackend(backend)) => {
7288 Some(("AddUnixBackend", backend.unix_path))
7289 }
7290 Some(RequestType::AddBackend(backend)) => Some(("AddBackend", backend.unix_path)),
7291 _ => None,
7292 })
7293 .collect();
7294 assert_eq!(
7295 unix_paths,
7296 vec![
7297 ("AddUnixBackend", Some("/run/app.sock".to_owned())),
7298 ("AddBackend", None)
7299 ]
7300 );
7301 }
7302
7303 #[test]
7306 fn a_unix_path_backend_needs_a_unique_address() {
7307 for protocol in ["http", "tcp"] {
7308 let file: FileClusterConfig = toml::from_str(&format!(
7309 "protocol = \"{protocol}\"\nfrontends = []\nbackends = [\n\
7310 {{ address = \"127.0.0.1:1\", unix_path = \"/run/a.sock\" }},\n\
7311 {{ address = \"127.0.0.1:1\", unix_path = \"/run/b.sock\" }},\n]\n"
7312 ))
7313 .expect("the cluster must parse");
7314 assert!(
7315 matches!(
7316 file.to_cluster_config("app", &HashSet::new()),
7317 Err(ConfigError::InvalidBackendUnixPath { .. })
7318 ),
7319 "a {protocol} cluster must refuse a shared unix backend address"
7320 );
7321 }
7322 }
7323
7324 #[test]
7328 fn a_health_check_port_and_a_unix_path_backend_never_share_a_file_cluster() {
7329 let resolver = |_: &str| -> std::io::Result<Vec<SocketAddr>> {
7330 Ok(vec!["10.0.0.2:8080".parse().unwrap()])
7331 };
7332 for protocol in ["http", "tcp"] {
7333 for address in ["127.0.0.1:1", "ingress:8080"] {
7334 let build = |health_check: &str, resolver: Option<&BackendResolver<'_>>| {
7335 toml::from_str::<FileClusterConfig>(&format!(
7336 "protocol = \"{protocol}\"\nfrontends = []\nbackends = [\n\
7337 {{ address = \"{address}\", unix_path = \"/run/app.sock\" }},\n]\n\
7338 [health_check]\n{health_check}"
7339 ))
7340 .expect("the cluster must parse")
7341 .to_cluster_config_with("app", &HashSet::new(), resolver)
7342 };
7343 for resolver in [Some(&resolver as &BackendResolver<'_>), None] {
7344 let error = build("mode = \"TCP\"\nport = 8081\n", resolver)
7345 .expect_err("a probe port next to a unix backend must be refused");
7346 assert!(
7347 matches!(
7348 &error,
7349 ConfigError::InvalidHealthCheck { cluster_id, reason }
7350 if cluster_id == "app" && *reason == HEALTH_CHECK_PORT_UNIX_REFUSED
7351 ),
7352 "{protocol} {address}: unexpected {error:?}"
7353 );
7354 build("mode = \"TCP\"\n", resolver)
7355 .expect("a unix backend without a probe port stays valid");
7356 }
7357 }
7358 }
7359 }
7360
7361 #[test]
7363 fn parse_backend_unix_path_accepts_absolute_and_abstract_names_only() {
7364 let path = parse_backend_unix_path("/run/app.sock").expect("an absolute path parses");
7365 assert_eq!(
7366 path.as_pathname(),
7367 Some(std::path::Path::new("/run/app.sock"))
7368 );
7369 #[cfg(target_os = "linux")]
7370 {
7371 use std::os::linux::net::SocketAddrExt;
7372 let named = parse_backend_unix_path("@app").expect("an abstract name parses");
7373 assert_eq!(named.as_abstract_name(), Some(&b"app"[..]));
7374 assert_eq!(named.as_pathname(), None);
7375 }
7376 let too_long = format!("/{}", "a".repeat(200));
7377 for refused in ["", "run/app.sock", "@", "/run/a\0b", too_long.as_str()] {
7378 assert!(
7379 parse_backend_unix_path(refused).is_err(),
7380 "{refused:?} must be refused"
7381 );
7382 }
7383 }
7384
7385 #[test]
7388 fn header_predicates_reach_the_add_http_frontend_order() {
7389 let file: FileClusterConfig = toml::from_str(
7390 r#"
7391 protocol = "http"
7392 frontends = [
7393 { address = "0.0.0.0:80", hostname = "api.example.com", header_predicates = [
7394 { name = "X-Canary", match = "EXACT", value = "always" },
7395 { name = "X-Region", match = "regex", value = "eu-.*" },
7396 { name = "X-Beta", match = "present" },
7397 ] },
7398 ]
7399 backends = []
7400 "#,
7401 )
7402 .expect("the cluster must parse");
7403 let ClusterConfig::Http(http) = file
7404 .to_cluster_config("canary", &HashSet::new())
7405 .expect("the cluster must build")
7406 else {
7407 panic!("an http cluster must build as one");
7408 };
7409 let orders = http.generate_requests().expect("the orders must build");
7410 let front = orders
7411 .iter()
7412 .find_map(|order| match order.request_type.as_ref() {
7413 Some(RequestType::AddHttpFrontendWithHeaderPredicates(front)) => Some(front),
7414 _ => None,
7415 })
7416 .expect("the orders must add the frontend");
7417 let predicate = |name: &str, kind: HeaderMatchKind, value: Option<&str>| HeaderPredicate {
7418 name: name.to_owned(),
7419 kind: kind as i32,
7420 value: value.map(str::to_owned),
7421 };
7422 assert_eq!(
7423 front.header_predicates,
7424 vec![
7425 predicate("X-Canary", HeaderMatchKind::Exact, Some("always")),
7426 predicate("X-Region", HeaderMatchKind::Regex, Some("eu-.*")),
7427 predicate("X-Beta", HeaderMatchKind::Present, None),
7428 ]
7429 );
7430 }
7431
7432 #[test]
7435 fn unusable_header_predicates_are_refused_at_config_load() {
7436 for entry in [
7437 r#"{ name = "X-Canary", match = "equals", value = "1" }"#,
7438 r#"{ name = "Host", match = "exact", value = "example.com" }"#,
7439 r#"{ name = "Cookie", match = "present" }"#,
7440 r#"{ name = "X-Canary", match = "present", value = "1" }"#,
7441 r#"{ name = "X-Canary", match = "exact" }"#,
7442 r#"{ name = "X Canary", match = "present" }"#,
7443 ] {
7444 let file: FileClusterConfig = toml::from_str(&format!(
7445 r#"
7446 protocol = "http"
7447 frontends = [
7448 {{ address = "0.0.0.0:80", hostname = "api.example.com", header_predicates = [
7449 {{ name = "X-Valid", match = "present" }},
7450 {entry},
7451 ] }},
7452 ]
7453 backends = []
7454 "#
7455 ))
7456 .expect("the cluster must parse");
7457 match file.to_cluster_config("canary", &HashSet::new()) {
7458 Err(ConfigError::InvalidHeaderPredicate { index: 1, .. }) => {}
7459 other => panic!("{entry}: expected InvalidHeaderPredicate at 1, got {other:?}"),
7460 }
7461 }
7462 }
7463
7464 #[test]
7467 fn a_tcp_cluster_refuses_an_affinity_header_or_cookie() {
7468 for key in [
7469 "affinity_header = \"X-Tenant\"",
7470 "affinity_cookie = \"tenant\"",
7471 ] {
7472 let file: FileClusterConfig = toml::from_str(&format!(
7473 "protocol = \"tcp\"\nload_balancing = \"MAGLEV\"\n{key}\nfrontends = []\nbackends = []\n"
7474 ))
7475 .expect("the cluster must parse");
7476 assert!(
7477 matches!(
7478 file.to_cluster_config("keyed", &HashSet::new()),
7479 Err(ConfigError::InvalidAffinityKey { .. })
7480 ),
7481 "a TCP cluster must refuse `{key}`"
7482 );
7483 }
7484 }
7485
7486 #[test]
7488 fn validate_affinity_key_accepts_one_token_and_refuses_the_rest() {
7489 assert!(validate_affinity_key(None, None).is_ok());
7490 assert!(validate_affinity_key(Some("X-Tenant"), None).is_ok());
7491 assert!(validate_affinity_key(None, Some("tenant_id")).is_ok());
7492 assert!(validate_affinity_key(Some("X-Tenant"), Some("tenant")).is_err());
7493 for unreachable in ["Host", "host", "HOST", "Cookie", "cookie"] {
7494 assert!(
7495 validate_affinity_key(Some(unreachable), None).is_err(),
7496 "header {unreachable:?} can never be found and must be refused"
7497 );
7498 }
7499 assert!(validate_affinity_key(None, Some("host")).is_ok());
7501 for bad in ["", "X Tenant", "X-Tenant:", "tenant\r\n", "t=1"] {
7502 assert!(
7503 validate_affinity_key(Some(bad), None).is_err(),
7504 "header {bad:?} must be refused"
7505 );
7506 assert!(
7507 validate_affinity_key(None, Some(bad)).is_err(),
7508 "cookie {bad:?} must be refused"
7509 );
7510 }
7511 }
7512
7513 #[test]
7516 fn shuffle_sharding_reaches_the_add_cluster_order_and_is_validated() {
7517 let parse = |extra: &str| -> FileClusterConfig {
7518 toml::from_str(&format!(
7519 "protocol = \"http\"\nfrontends = []\nbackends = []\n{extra}"
7520 ))
7521 .expect("the cluster must parse")
7522 };
7523 let ClusterConfig::Http(http) =
7524 parse("shard_percent = 25\nshard_min_backends = 4\nshard_mode = \"STRICT\"\n")
7525 .to_cluster_config("sharded", &HashSet::new())
7526 .expect("a valid sharded cluster builds")
7527 else {
7528 panic!("an http cluster must build as one");
7529 };
7530 let orders = http.generate_requests().expect("the orders must build");
7531 let Some(RequestType::AddCluster(cluster)) =
7532 orders.first().and_then(|order| order.request_type.as_ref())
7533 else {
7534 panic!("the orders must lead with an AddCluster");
7535 };
7536 assert_eq!(
7537 (
7538 cluster.shard_percent,
7539 cluster.shard_min_backends,
7540 cluster.shard_mode
7541 ),
7542 (Some(25), Some(4), Some(ShardMode::Strict as i32))
7543 );
7544 for bad in [
7545 "shard_percent = 0\n",
7546 "shard_percent = 101\n",
7547 "shard_percent = 25\nshard_min_backends = 1\n",
7548 "shard_min_backends = 8\n",
7549 "shard_mode = \"STRICT\"\n",
7550 ] {
7551 assert!(
7552 matches!(
7553 parse(bad).to_cluster_config("sharded", &HashSet::new()),
7554 Err(ConfigError::InvalidShuffleSharding { .. })
7555 ),
7556 "{bad:?} must be refused"
7557 );
7558 }
7559 }
7560
7561 #[test]
7564 fn http_status_range_parses_codes_ranges_classes_and_any() {
7565 let range = |start, end| HttpStatusRange { start, end };
7566 for (spec, expected) in [
7567 ("404", range(404, 404)),
7568 ("200-399", range(200, 399)),
7569 (" 200 - 399 ", range(200, 399)),
7570 ("2xx", range(200, 299)),
7571 ("5XX", range(500, 599)),
7572 ("any", range(100, 599)),
7573 ("ANY", range(100, 599)),
7574 ("100-599", range(100, 599)),
7575 ] {
7576 assert_eq!(parse_http_status_range(spec), Ok(expected), "{spec:?}");
7577 }
7578 for spec in [
7579 "", "abc", "99", "600", "0", "6xx", "0xx", "2x", "2xxx", "399-200", "200-", "-200",
7580 "+200", "200-600", "1-2-3", "2xx-3xx",
7581 ] {
7582 assert!(
7583 parse_http_status_range(spec).is_err(),
7584 "{spec:?} must be refused"
7585 );
7586 }
7587 }
7588
7589 #[test]
7592 fn http_status_range_display_round_trips_through_the_parser() {
7593 for spec in ["404", "200-399", "2xx", "any", "101-102"] {
7594 let range = parse_http_status_range(spec).unwrap();
7595 assert_eq!(range.to_string(), spec);
7596 assert_eq!(parse_http_status_range(&range.to_string()), Ok(range));
7597 }
7598 }
7599
7600 #[test]
7605 fn health_check_validation_covers_mode_uri_and_accepted_statuses() {
7606 let http = HealthCheckConfig {
7607 uri: "/health".to_owned(),
7608 interval: 10,
7609 timeout: 5,
7610 healthy_threshold: 3,
7611 unhealthy_threshold: 3,
7612 expected_status: 0,
7613 mode: HealthCheckMode::Http as i32,
7614 accepted_statuses: Vec::new(),
7615 port: None,
7616 headers: Vec::new(),
7617 };
7618 assert_eq!(validate_health_check_config(&http), Ok(()));
7619
7620 let tcp_no_uri = HealthCheckConfig {
7622 uri: String::new(),
7623 mode: HealthCheckMode::Tcp as i32,
7624 ..http.clone()
7625 };
7626 assert_eq!(validate_health_check_config(&tcp_no_uri), Ok(()));
7627 let http_no_uri = HealthCheckConfig {
7628 uri: String::new(),
7629 ..http.clone()
7630 };
7631 assert!(validate_health_check_config(&http_no_uri).is_err());
7632 let tcp_crlf = HealthCheckConfig {
7634 uri: "/a\r\nGET /b".to_owned(),
7635 ..tcp_no_uri.clone()
7636 };
7637 assert!(validate_health_check_config(&tcp_crlf).is_err());
7638
7639 let tcp_expected = HealthCheckConfig {
7641 expected_status: 200,
7642 ..tcp_no_uri.clone()
7643 };
7644 assert!(validate_health_check_config(&tcp_expected).is_err());
7645 let tcp_accepted = HealthCheckConfig {
7646 accepted_statuses: vec![HttpStatusRange {
7647 start: 100,
7648 end: 599,
7649 }],
7650 ..tcp_no_uri.clone()
7651 };
7652 assert!(validate_health_check_config(&tcp_accepted).is_err());
7653
7654 let unknown_mode = HealthCheckConfig {
7655 mode: 7,
7656 ..http.clone()
7657 };
7658 assert!(validate_health_check_config(&unknown_mode).is_err());
7659
7660 let accepted = HealthCheckConfig {
7661 accepted_statuses: vec![HttpStatusRange {
7662 start: 200,
7663 end: 499,
7664 }],
7665 ..http.clone()
7666 };
7667 assert_eq!(validate_health_check_config(&accepted), Ok(()));
7668 let both = HealthCheckConfig {
7669 expected_status: 200,
7670 ..accepted.clone()
7671 };
7672 assert!(
7673 validate_health_check_config(&both).is_err(),
7674 "expected_status and accepted_statuses must not both be set"
7675 );
7676 for (start, end) in [(99, 200), (200, 600), (300, 200)] {
7677 let bad_range = HealthCheckConfig {
7678 accepted_statuses: vec![HttpStatusRange { start, end }],
7679 ..http.clone()
7680 };
7681 assert!(
7682 validate_health_check_config(&bad_range).is_err(),
7683 "{start}-{end} must be refused"
7684 );
7685 }
7686 }
7687
7688 #[test]
7692 fn health_check_toml_mode_and_accepted_statuses_reach_the_add_cluster_order() {
7693 let parse = |block: &str| -> FileClusterConfig {
7694 toml::from_str(&format!(
7695 "protocol = \"http\"\nfrontends = []\nbackends = []\n[health_check]\n{block}"
7696 ))
7697 .expect("the cluster must parse")
7698 };
7699 let health_check = |block: &str| -> HealthCheckConfig {
7700 let ClusterConfig::Http(http) = parse(block)
7701 .to_cluster_config("checked", &HashSet::new())
7702 .expect("a valid health check builds")
7703 else {
7704 panic!("an http cluster must build as one");
7705 };
7706 let orders = http.generate_requests().expect("the orders must build");
7707 let Some(RequestType::AddCluster(cluster)) =
7708 orders.first().and_then(|order| order.request_type.as_ref())
7709 else {
7710 panic!("the orders must lead with an AddCluster");
7711 };
7712 cluster
7713 .health_check
7714 .clone()
7715 .expect("the health check must travel")
7716 };
7717
7718 let legacy = health_check("uri = \"/health\"\nexpected_status = 204\n");
7720 assert_eq!(legacy.mode, HealthCheckMode::Http as i32);
7721 assert_eq!(
7722 (legacy.uri.as_str(), legacy.expected_status),
7723 ("/health", 204)
7724 );
7725 assert!(legacy.accepted_statuses.is_empty());
7726
7727 let defaulted = health_check("");
7728 assert_eq!(defaulted.mode, HealthCheckMode::Http as i32);
7729 assert_eq!(defaulted.uri, "/");
7730
7731 let tcp = health_check("mode = \"TCP\"\n");
7732 assert_eq!(tcp.mode, HealthCheckMode::Tcp as i32);
7733
7734 let accepted = health_check("uri = \"/livez\"\naccepted_statuses = [\"2xx\", \"404\"]\n");
7735 assert_eq!(
7736 accepted.accepted_statuses,
7737 vec![
7738 HttpStatusRange {
7739 start: 200,
7740 end: 299
7741 },
7742 HttpStatusRange {
7743 start: 404,
7744 end: 404
7745 },
7746 ]
7747 );
7748
7749 for bad in [
7750 "accepted_statuses = [\"6xx\"]\n",
7751 "expected_status = 200\naccepted_statuses = [\"any\"]\n",
7752 "uri = \"health\"\n",
7753 "mode = \"TCP\"\nexpected_status = 200\n",
7754 "mode = \"TCP\"\naccepted_statuses = [\"any\"]\n",
7755 ] {
7756 assert!(
7757 matches!(
7758 parse(bad).to_cluster_config("checked", &HashSet::new()),
7759 Err(ConfigError::InvalidHealthCheck { .. })
7760 ),
7761 "{bad:?} must be refused"
7762 );
7763 }
7764 }
7765
7766 #[test]
7769 fn health_check_validation_bounds_the_probe_port() {
7770 for mode in [HealthCheckMode::Http, HealthCheckMode::Tcp] {
7771 let config = |port: Option<u32>| HealthCheckConfig {
7772 uri: "/health".to_owned(),
7773 interval: 10,
7774 timeout: 5,
7775 healthy_threshold: 3,
7776 unhealthy_threshold: 3,
7777 mode: mode as i32,
7778 port,
7779 ..Default::default()
7780 };
7781 for port in [None, Some(1), Some(8080), Some(65535)] {
7782 assert_eq!(
7783 validate_health_check_config(&config(port)),
7784 Ok(()),
7785 "{mode:?} probe port {port:?} must be accepted"
7786 );
7787 }
7788 for port in [0, 65536, u32::MAX] {
7789 assert_eq!(
7790 validate_health_check_config(&config(Some(port))),
7791 Err("health check port must lie in 1..=65535"),
7792 "{mode:?} probe port {port} must be refused"
7793 );
7794 }
7795 }
7796 }
7797
7798 #[test]
7801 fn health_check_toml_port_reaches_the_add_cluster_order() {
7802 let build = |block: &str| {
7803 toml::from_str::<FileClusterConfig>(&format!(
7804 "protocol = \"tcp\"\nfrontends = []\nbackends = []\n[health_check]\n{block}"
7805 ))
7806 .expect("the cluster must parse")
7807 .to_cluster_config("checked", &HashSet::new())
7808 };
7809 let port = |block: &str| -> Option<u32> {
7810 let Ok(ClusterConfig::Tcp(tcp)) = build(block) else {
7811 panic!("a tcp cluster with a valid health check must build as one");
7812 };
7813 tcp.health_check.expect("the health check must travel").port
7814 };
7815
7816 assert_eq!(port("mode = \"TCP\"\n"), None);
7817 assert_eq!(port("mode = \"TCP\"\nport = 8081\n"), Some(8081));
7818 assert_eq!(port("uri = \"/health\"\nport = 65535\n"), Some(65535));
7819 for bad in ["port = 0\n", "port = 65536\n"] {
7820 assert!(
7821 matches!(build(bad), Err(ConfigError::InvalidHealthCheck { .. })),
7822 "{bad:?} must be refused"
7823 );
7824 }
7825 }
7826
7827 fn health_check_with_headers(headers: &[(&str, &str)]) -> HealthCheckConfig {
7829 HealthCheckConfig {
7830 uri: "/".to_owned(),
7831 interval: 10,
7832 timeout: 5,
7833 healthy_threshold: 3,
7834 unhealthy_threshold: 3,
7835 expected_status: 0,
7836 mode: HealthCheckMode::Http as i32,
7837 accepted_statuses: Vec::new(),
7838 port: None,
7839 headers: headers
7840 .iter()
7841 .map(|(name, _)| HealthCheckHeader {
7842 name: (*name).to_owned(),
7843 fingerprint: String::new(),
7844 })
7845 .collect(),
7846 }
7847 }
7848
7849 fn check_headers(headers: &[(&str, &str)]) -> Result<(), &'static str> {
7851 let config = health_check_with_headers(headers);
7852 validate_health_check_config(&config)?;
7853 let values: Vec<HealthCheckHeaderValue> = headers
7854 .iter()
7855 .map(|(name, value)| HealthCheckHeaderValue {
7856 name: (*name).to_owned(),
7857 value: value.as_bytes().to_vec(),
7858 })
7859 .collect();
7860 validate_health_check_header_values(&config, &values)
7861 }
7862
7863 #[test]
7868 fn health_check_header_value_refuses_leading_and_trailing_ows() {
7869 for value in [" v", "v ", "\tv", "v\t", " ", "\t", " v ", "a b \t"] {
7870 assert_eq!(
7871 check_headers(&[("x-a", value)]),
7872 Err("health check header value must not begin or end with a space or a tab"),
7873 "{value:?} must be refused"
7874 );
7875 }
7876 for value in ["", "v", "a b", "a\tb", "é ü", "\u{a0}v"] {
7877 assert_eq!(
7878 check_headers(&[("x-a", value)]),
7879 Ok(()),
7880 "{value:?} must be accepted"
7881 );
7882 }
7883 }
7884
7885 #[test]
7888 fn health_check_header_validation_covers_every_class() {
7889 let refused = |headers: &[(&str, &str)], reason: &str| {
7890 assert_eq!(
7891 check_headers(headers),
7892 Err(reason),
7893 "{headers:?} must be refused"
7894 );
7895 };
7896
7897 assert_eq!(
7900 check_headers(&[
7901 ("Authorization", "Bearer t"),
7902 ("X-Empty", ""),
7903 ("x-tab", "a\tb"),
7904 ("x-utf8", "caf\u{e9} \u{1F600}"),
7905 ]),
7906 Ok(())
7907 );
7908
7909 let mut tcp = health_check_with_headers(&[("x-a", "1")]);
7911 tcp.mode = HealthCheckMode::Tcp as i32;
7912 assert_eq!(
7913 validate_health_check_config(&tcp),
7914 Err("health check headers apply to HTTP mode only: leave them unset with mode TCP")
7915 );
7916
7917 let names: Vec<String> = (0..33).map(|i| format!("x-{i}")).collect();
7919 let many = |count: usize| -> Vec<(&str, &str)> {
7920 names[..count].iter().map(|n| (n.as_str(), "v")).collect()
7921 };
7922 assert_eq!(check_headers(&many(32)), Ok(()));
7923 refused(&many(33), "health check carries more than 32 headers");
7924
7925 for name in [
7927 ":authority",
7928 ":x",
7929 "Host",
7930 "HOST",
7931 "connection",
7932 "Keep-Alive",
7933 "proxy-connection",
7934 "Transfer-Encoding",
7935 "content-length",
7936 "TE",
7937 "trailer",
7938 "upgrade",
7939 "Expect",
7940 ] {
7941 refused(
7942 &[(name, "v")],
7943 "health check header name is reserved for the probe or its framing",
7944 );
7945 }
7946
7947 for name in [
7949 "",
7950 " x",
7951 "x ",
7952 "a b",
7953 "a:b",
7954 "caf\u{e9}",
7955 "a\r",
7956 "a\"b",
7957 "a(b",
7958 ] {
7959 refused(
7960 &[(name, "v")],
7961 "health check header name must be a non-empty RFC 9110 token",
7962 );
7963 }
7964
7965 for value in [
7967 "a\rb", "a\nb", "a\0b", "a\x01b", "a\x08b", "a\x0bb", "a\x1fb", "a\x7fb",
7968 ] {
7969 refused(
7970 &[("x-a", value)],
7971 "health check header value must not contain control bytes",
7972 );
7973 }
7974
7975 refused(
7977 &[("X-Token", "1"), ("x-token", "2")],
7978 "health check header names must be unique, compared case-insensitively",
7979 );
7980
7981 let value_fits = "v".repeat(8192 - 1 - 4);
7983 assert_eq!(check_headers(&[("x", &value_fits)]), Ok(()));
7984 let value_over = "v".repeat(8192 - 1 - 4 + 1);
7985 refused(
7986 &[("x", &value_over)],
7987 "health check headers exceed 8192 bytes (name + value + 4 per header)",
7988 );
7989
7990 let mut long_uri = health_check_with_headers(&[]);
7993 long_uri.uri = format!("/{}", "a".repeat(9000));
7994 assert_eq!(validate_health_check_config(&long_uri), Ok(()));
7995 long_uri.headers = vec![HealthCheckHeader {
7996 name: "x".to_owned(),
7997 fingerprint: String::new(),
7998 }];
7999 assert_eq!(validate_health_check_config(&long_uri), Ok(()));
8000 let carrier = HealthCheckHeaderValue {
8001 name: "x".to_owned(),
8002 value: "v".repeat(8000).into_bytes(),
8003 };
8004 assert_eq!(
8005 validate_health_check_header_values(&long_uri, &[carrier]),
8006 Err("health check URI and headers may exceed the 16384-byte HTTP/2 probe block")
8007 );
8008
8009 let one = health_check_with_headers(&[("x-a", ""), ("x-b", "")]);
8012 let carrier = |name: &str, value: &[u8]| HealthCheckHeaderValue {
8013 name: name.to_owned(),
8014 value: value.to_vec(),
8015 };
8016 assert_eq!(
8017 validate_health_check_header_values(&one, &[carrier("x-a", b"1")]),
8018 Err("health check carries a header value count different from its header count")
8019 );
8020 assert_eq!(
8021 validate_health_check_header_values(
8022 &one,
8023 &[carrier("x-b", b"1"), carrier("x-a", b"2")]
8024 ),
8025 Err("health check header values must follow the order and names of the headers")
8026 );
8027 assert_eq!(
8028 validate_health_check_header_values(
8029 &one,
8030 &[carrier("x-a", b"1"), carrier("x-b", &[0xff])]
8031 ),
8032 Err("health check header value must be valid UTF-8")
8033 );
8034 let mut spoofed = health_check_with_headers(&[("x-a", "")]);
8036 spoofed.headers[0].fingerprint = "not a fingerprint".to_owned();
8037 assert_eq!(
8038 validate_health_check_config(&spoofed),
8039 Err("health check header fingerprint is malformed")
8040 );
8041 }
8042
8043 #[test]
8047 fn health_check_headers_default_empty_and_round_trip_in_order() {
8048 use prost::Message;
8049
8050 let config = health_check_with_headers(&[
8051 ("Z-Last-Alphabetically", "1"),
8052 ("authorization", "Bearer \u{e9}\tx"),
8053 ("A-First-Alphabetically", ""),
8054 ]);
8055
8056 let mut value = serde_json::to_value(&config).expect("serialize");
8058 value
8059 .as_object_mut()
8060 .expect("an object")
8061 .remove("headers")
8062 .expect("the key is serialized");
8063 let legacy: HealthCheckConfig = serde_json::from_value(value).expect("legacy record loads");
8064 assert!(legacy.headers.is_empty());
8065
8066 let json = serde_json::to_string(&config).expect("serialize");
8068 let back: HealthCheckConfig = serde_json::from_str(&json).expect("deserialize");
8069 assert_eq!(back, config);
8070 assert_eq!(
8071 back.headers
8072 .iter()
8073 .map(|h| h.name.as_str())
8074 .collect::<Vec<_>>(),
8075 [
8076 "Z-Last-Alphabetically",
8077 "authorization",
8078 "A-First-Alphabetically"
8079 ]
8080 );
8081
8082 let headerless = HealthCheckConfig {
8085 headers: Vec::new(),
8086 ..config.clone()
8087 };
8088 let decoded =
8089 HealthCheckConfig::decode(headerless.encode_to_vec().as_slice()).expect("decodes");
8090 assert!(decoded.headers.is_empty());
8091 let decoded =
8092 HealthCheckConfig::decode(config.encode_to_vec().as_slice()).expect("decodes");
8093 assert_eq!(decoded, config);
8094 }
8095
8096 #[test]
8110 fn health_check_toml_headers_split_into_metadata_and_ordered_references() {
8111 let parse = |block: &str| {
8112 toml::from_str::<FileHealthCheckConfig>(&format!("uri = \"/health\"\n{block}"))
8113 .expect("the table parses")
8114 };
8115
8116 let file = parse(
8117 "[[headers]]\nname = \"Z-Second\"\nvalue_file = \"/missing/z\"\n\
8118 [[headers]]\nname = \"Authorization\"\nvalue_file = \"rel/a\"\n\
8119 [[headers]]\nname = \"X-Empty\"\nvalue_file = \"/missing/e\"\n",
8120 );
8121 let (config, files) = file.to_parts().expect("a valid list builds");
8122 assert_eq!(
8123 config
8124 .headers
8125 .iter()
8126 .map(|h| (h.name.as_str(), h.fingerprint.as_str()))
8127 .collect::<Vec<_>>(),
8128 [("Z-Second", ""), ("Authorization", ""), ("X-Empty", "")],
8129 "metadata names the headers and carries no value or fingerprint"
8130 );
8131 assert_eq!(
8132 files
8133 .iter()
8134 .map(|f| (f.name.as_str(), f.value_file.as_str()))
8135 .collect::<Vec<_>>(),
8136 [
8137 ("Z-Second", "/missing/z"),
8138 ("Authorization", "rel/a"),
8139 ("X-Empty", "/missing/e")
8140 ]
8141 );
8142 assert_eq!(
8144 file.to_proto(),
8145 Err(
8146 "health check headers from the configuration file are not available yet: their values need the protected channel"
8147 )
8148 );
8149
8150 let bare = parse("");
8152 assert!(bare.to_proto().expect("builds").headers.is_empty());
8153 assert!(bare.to_parts().expect("builds").1.is_empty());
8154
8155 for bad in [
8157 "mode = \"TCP\"\n[[headers]]\nname = \"x\"\nvalue_file = \"/v\"\n",
8158 "[[headers]]\nname = \"Host\"\nvalue_file = \"/v\"\n",
8159 "[[headers]]\nname = \"x\"\nvalue_file = \"/v\"\n\
8160 [[headers]]\nname = \"X\"\nvalue_file = \"/w\"\n",
8161 ] {
8162 assert!(parse(bad).to_parts().is_err(), "{bad:?} must be refused");
8163 }
8164 assert!(
8166 toml::from_str::<FileHealthCheckConfig>(
8167 "uri = \"/\"\n[[headers]]\nname = \"x\"\nvalue_file = \"/v\"\nextra = 1\n"
8168 )
8169 .is_err()
8170 );
8171 }
8172
8173 const HEADER_SENTINEL: &str = "SENTINEL-HEADER-VALUE-7f3a91";
8176 const REF_SENTINEL: &str = "sentinel-ref-dir-9c1e42";
8177
8178 fn value_file_cluster(protocol: &str, headers: &[(&str, &str)]) -> FileClusterConfig {
8181 let tables: String = headers
8182 .iter()
8183 .map(|(name, path)| {
8184 format!("[[health_check.headers]]\nname = \"{name}\"\nvalue_file = \"{path}\"\n")
8185 })
8186 .collect();
8187 toml::from_str(&format!(
8188 "protocol = \"{protocol}\"\nfrontends = []\nbackends = []\n\
8189 [health_check]\nuri = \"/health\"\n{tables}"
8190 ))
8191 .expect("a value_file reference parses")
8192 }
8193
8194 fn cluster_json(cluster: &ClusterConfig) -> serde_json::Value {
8197 match cluster {
8198 ClusterConfig::Http(http) => serde_json::to_value(http),
8199 ClusterConfig::Tcp(tcp) => serde_json::to_value(tcp),
8200 }
8201 .expect("a cluster serializes")
8202 }
8203
8204 #[test]
8208 fn health_check_value_file_is_kept_unopened_in_order_for_http_and_tcp() {
8209 let missing_a = format!("/{REF_SENTINEL}/missing/a.secret");
8210 let missing_b = format!("relative/../{REF_SENTINEL}/b.secret");
8211 let headers = [
8212 ("Z-Second", missing_a.as_str()),
8213 ("Authorization", &missing_b),
8214 ];
8215
8216 for protocol in ["http", "tcp"] {
8217 let file = value_file_cluster(protocol, &headers);
8218 let cluster = file
8219 .to_cluster_config("checked", &HashSet::new())
8220 .expect("a reference to a missing file builds: nothing opens it");
8221 let json = cluster_json(&cluster);
8222
8223 let metadata: Vec<(&str, &str)> = json["health_check"]["headers"]
8224 .as_array()
8225 .expect("the metadata lists the headers")
8226 .iter()
8227 .map(|h| {
8228 (
8229 h["name"].as_str().expect("a name"),
8230 h["fingerprint"].as_str().expect("a fingerprint"),
8231 )
8232 })
8233 .collect();
8234 assert_eq!(
8235 metadata,
8236 [("Z-Second", ""), ("Authorization", "")],
8237 "{protocol}: ordered names, empty fingerprints"
8238 );
8239 assert_eq!(
8240 json["health_check_header_files"],
8241 serde_json::json!([
8242 { "name": "Z-Second", "value_file": missing_a },
8243 { "name": "Authorization", "value_file": missing_b },
8244 ]),
8245 "{protocol}: references are stored unchanged and in order"
8246 );
8247
8248 let mut config = Config::default();
8249 config
8250 .clusters
8251 .insert("checked".to_owned(), cluster.clone());
8252 let text = serde_json::to_string(&config).expect("a Config serializes");
8253 let back: Config = serde_json::from_str(&text).expect("a Config deserializes");
8254 assert_eq!(
8255 back.clusters.get("checked"),
8256 Some(&cluster),
8257 "{protocol}: references survive Config serde"
8258 );
8259 }
8260
8261 let bare: FileClusterConfig = toml::from_str(
8263 "protocol = \"http\"\nfrontends = []\nbackends = []\n[health_check]\nuri = \"/\"\n",
8264 )
8265 .expect("parses");
8266 let built = bare
8267 .to_cluster_config("bare", &HashSet::new())
8268 .expect("builds");
8269 assert_eq!(
8270 cluster_json(&built)["health_check_header_files"],
8271 serde_json::json!([])
8272 );
8273 }
8274
8275 #[test]
8279 fn health_check_header_refuses_a_literal_or_env_value_and_never_echoes() {
8280 let parse = |table: &str| {
8281 toml::from_str::<FileHealthCheckConfig>(&format!("uri = \"/\"\n[[headers]]\n{table}\n"))
8282 };
8283
8284 for bad in [
8285 format!("name = \"x\"\nvalue = \"{HEADER_SENTINEL}\""),
8286 format!("name = \"x\"\nvalue_env = \"{HEADER_SENTINEL}\""),
8287 "name = \"x\"".to_owned(),
8288 "value_file = \"/p\"".to_owned(),
8289 format!("name = \"x\"\nvalue_file = \"/p\"\nvalue = \"{HEADER_SENTINEL}\""),
8290 format!("name = \"x\"\nvalue_file = \"/p\"\nvalue_env = \"{HEADER_SENTINEL}\""),
8291 format!("name = \"x\"\nvalue_file = \"/p\"\nextra = \"{HEADER_SENTINEL}\""),
8292 format!("name = \"x\"\nvalue_file = 12\nextra = \"{HEADER_SENTINEL}\""),
8293 ] {
8294 let error = parse(&bad).expect_err(&format!("{bad:?} must be refused"));
8295 let diagnostic = toml_error_diagnostic("/etc/sozu.toml", &error);
8296 assert!(
8297 !diagnostic.contains(HEADER_SENTINEL),
8298 "the diagnostic must not echo a value: {diagnostic}"
8299 );
8300 }
8301
8302 let accepted = parse(&format!("name = \"x\"\nvalue_file = \"/{REF_SENTINEL}/v\""))
8303 .expect("a name and a value_file parse");
8304 assert_eq!(accepted.headers.len(), 1);
8305 let debug = format!("{accepted:?}");
8306 assert!(
8307 !debug.contains(REF_SENTINEL),
8308 "Debug must redact the reference path: {debug}"
8309 );
8310 }
8311
8312 #[test]
8317 fn health_check_value_file_cluster_generates_no_worker_request() {
8318 for protocol in ["http", "tcp"] {
8319 let path = format!("/{REF_SENTINEL}/v");
8320 let file = value_file_cluster(protocol, &[("x-token", &path)]);
8321 let cluster = file
8322 .to_cluster_config("guarded", &HashSet::new())
8323 .expect("builds");
8324
8325 let direct = cluster
8326 .generate_requests()
8327 .expect_err("no order may carry a name without its value");
8328 assert!(
8329 matches!(direct, ConfigError::InvalidHealthCheck { .. }),
8330 "{protocol}: {direct:?}"
8331 );
8332
8333 let mut config = Config::default();
8334 config.clusters.insert("guarded".to_owned(), cluster);
8335 let error = config
8336 .generate_config_messages()
8337 .expect_err("the whole generation is refused before any order is returned");
8338 let shown = format!("{error} {error:?}");
8339 assert!(
8340 matches!(error, ConfigError::InvalidHealthCheck { .. })
8341 && shown.contains("guarded")
8342 && !shown.contains(REF_SENTINEL),
8343 "{protocol}: {shown}"
8344 );
8345 }
8346
8347 let headerless: FileClusterConfig = toml::from_str(
8348 "protocol = \"http\"\nfrontends = []\nbackends = []\n[health_check]\nuri = \"/\"\n",
8349 )
8350 .expect("parses");
8351 let cluster = headerless
8352 .to_cluster_config("plain", &HashSet::new())
8353 .expect("builds");
8354 assert!(cluster.generate_requests().is_ok());
8355 }
8356
8357 #[test]
8358 fn multiple_listeners_generate_correct_worker_requests() {
8359 let toml_content = r#"
8360 command_socket = "/tmp/sozu_test.sock"
8361 worker_count = 1
8362 activate_listeners = true
8363
8364 [[listeners]]
8365 protocol = "http"
8366 address = "172.16.20.1:80"
8367 expect_proxy = true
8368
8369 [[listeners]]
8370 protocol = "http"
8371 address = "10.22.0.1:80"
8372 expect_proxy = false
8373 "#;
8374
8375 let file_config: FileConfig =
8376 toml::from_str(toml_content).expect("Could not parse TOML config");
8377
8378 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8379 .into_config()
8380 .expect("Could not build config");
8381
8382 let messages = config
8383 .generate_config_messages()
8384 .expect("Could not generate config messages");
8385
8386 let add_listener_count = messages
8387 .iter()
8388 .filter(|m| {
8389 matches!(
8390 m.content.request_type,
8391 Some(RequestType::AddHttpListener(_))
8392 )
8393 })
8394 .count();
8395
8396 let activate_listener_count = messages
8397 .iter()
8398 .filter(|m| {
8399 matches!(
8400 m.content.request_type,
8401 Some(RequestType::ActivateListener(ActivateListener {
8402 proxy,
8403 ..
8404 })) if proxy == ListenerType::Http as i32
8405 )
8406 })
8407 .count();
8408
8409 assert_eq!(add_listener_count, 2);
8410 assert_eq!(activate_listener_count, 2);
8411 }
8412
8413 #[test]
8414 fn documented_udp_dns_example_loads_and_emits_udp_requests() {
8415 let toml_content = r#"
8422 command_socket = "/tmp/sozu_test.sock"
8423 worker_count = 1
8424 activate_listeners = true
8425
8426 [[listeners]]
8427 protocol = "udp"
8428 address = "0.0.0.0:53"
8429
8430 [clusters.dns]
8431 protocol = "tcp"
8432 load_balancing = "HRW"
8433 frontends = [
8434 { address = "0.0.0.0:53" }
8435 ]
8436 backends = [
8437 { address = "10.0.0.10:53" },
8438 { address = "10.0.0.11:53" }
8439 ]
8440
8441 [clusters.dns.udp]
8442 affinity_key = "SOURCE_IP"
8443 responses = 1
8444 requests = 0
8445 send_proxy_protocol = true
8446
8447 [clusters.dns.udp.health]
8448 mode = "TCP_PROBE"
8449 tcp_port = 53
8450 rise = 2
8451 fall = 3
8452 fail_open = true
8453 "#;
8454
8455 let file_config: FileConfig =
8456 toml::from_str(toml_content).expect("Could not parse documented DNS TOML");
8457
8458 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8459 .into_config()
8460 .expect("documented UDP DNS example must load without WrongFrontendProtocol");
8461
8462 assert_eq!(
8464 config.udp_listeners.len(),
8465 1,
8466 "the protocol=\"udp\" listener must be built"
8467 );
8468
8469 let messages = config
8470 .generate_config_messages()
8471 .expect("Could not generate config messages");
8472
8473 let add_udp_listener_count = messages
8474 .iter()
8475 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpListener(_))))
8476 .count();
8477 assert_eq!(
8478 add_udp_listener_count, 1,
8479 "must emit exactly one AddUdpListener"
8480 );
8481
8482 let add_udp_frontend_count = messages
8483 .iter()
8484 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
8485 .count();
8486 assert_eq!(
8487 add_udp_frontend_count, 1,
8488 "the cluster frontend on the UDP listener must emit AddUdpFrontend"
8489 );
8490
8491 let add_tcp_frontend_count = messages
8493 .iter()
8494 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddTcpFrontend(_))))
8495 .count();
8496 assert_eq!(
8497 add_tcp_frontend_count, 0,
8498 "a UDP-listener-addressed frontend must not be emitted as AddTcpFrontend"
8499 );
8500
8501 let udp_frontend = messages
8503 .iter()
8504 .find_map(|m| match &m.content.request_type {
8505 Some(RequestType::AddUdpFrontend(f)) => Some(f),
8506 _ => None,
8507 })
8508 .expect("AddUdpFrontend must be present");
8509 assert_eq!(udp_frontend.cluster_id, "dns");
8510 assert_eq!(
8511 SocketAddr::from(udp_frontend.address),
8512 "0.0.0.0:53".parse().unwrap()
8513 );
8514
8515 let cluster = messages
8518 .iter()
8519 .find_map(|m| match &m.content.request_type {
8520 Some(RequestType::AddCluster(c)) if c.cluster_id == "dns" => Some(c),
8521 _ => None,
8522 })
8523 .expect("AddCluster for 'dns' must be present");
8524 let udp = cluster
8525 .udp
8526 .as_ref()
8527 .expect("[clusters.dns.udp] block must carry onto the cluster");
8528 assert_eq!(udp.responses, Some(1));
8529 }
8530
8531 #[test]
8537 fn listener_interface_round_trips_from_the_config_file() {
8538 let toml_content = r#"
8539 command_socket = "/tmp/sozu_test.sock"
8540 worker_count = 1
8541 activate_listeners = true
8542
8543 [[listeners]]
8544 protocol = "http"
8545 address = "0.0.0.0:8080"
8546 interface = "wg0"
8547
8548 [[listeners]]
8549 protocol = "http"
8550 address = "0.0.0.0:8080"
8551 interface = "lo"
8552
8553 [[listeners]]
8554 protocol = "http"
8555 address = "0.0.0.0:8081"
8556
8557 [[listeners]]
8558 protocol = "https"
8559 address = "0.0.0.0:8443"
8560 interface = "wg0"
8561
8562 [[listeners]]
8563 protocol = "tcp"
8564 address = "0.0.0.0:9000"
8565 interface = "wg0"
8566
8567 [[listeners]]
8568 protocol = "udp"
8569 address = "0.0.0.0:5353"
8570 interface = "wg0"
8571 "#;
8572
8573 let file_config: FileConfig =
8574 toml::from_str(toml_content).expect("the interface key must parse");
8575 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8576 .into_config()
8577 .expect("listeners on one address with different interfaces must build");
8578
8579 let http: Vec<(String, Option<&str>)> = config
8580 .http_listeners
8581 .iter()
8582 .map(|l| {
8583 (
8584 SocketAddr::from(l.address).to_string(),
8585 l.interface.as_deref(),
8586 )
8587 })
8588 .collect();
8589 assert_eq!(
8590 http,
8591 vec![
8592 ("0.0.0.0:8080".to_owned(), Some("wg0")),
8593 ("0.0.0.0:8080".to_owned(), Some("lo")),
8594 ("0.0.0.0:8081".to_owned(), None),
8595 ]
8596 );
8597 assert_eq!(config.https_listeners[0].interface.as_deref(), Some("wg0"));
8598 assert_eq!(config.tcp_listeners[0].interface.as_deref(), Some("wg0"));
8599 assert_eq!(config.udp_listeners[0].interface.as_deref(), Some("wg0"));
8600
8601 let activations: Vec<Option<String>> = config
8602 .generate_config_messages()
8603 .expect("generate the config messages")
8604 .into_iter()
8605 .filter_map(|request| match request.content.request_type {
8606 Some(RequestType::ActivateListener(activate)) => Some(activate.interface),
8607 _ => None,
8608 })
8609 .collect();
8610 assert_eq!(
8611 activations,
8612 vec![
8613 Some("wg0".to_owned()),
8614 Some("lo".to_owned()),
8615 None,
8616 Some("wg0".to_owned()),
8617 Some("wg0".to_owned()),
8618 Some("wg0".to_owned()),
8619 ]
8620 );
8621
8622 let duplicate = r#"
8623 command_socket = "/tmp/sozu_test.sock"
8624 worker_count = 1
8625
8626 [[listeners]]
8627 protocol = "http"
8628 address = "0.0.0.0:8080"
8629 interface = "wg0"
8630
8631 [[listeners]]
8632 protocol = "http"
8633 address = "0.0.0.0:8080"
8634 interface = "wg0"
8635 "#;
8636 let file_config: FileConfig = toml::from_str(duplicate).expect("parse");
8637 assert!(
8638 ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8639 .into_config()
8640 .is_err(),
8641 "the same (address, interface) twice must be refused"
8642 );
8643 }
8644
8645 #[test]
8648 fn invalid_listener_interface_rejected_at_config_load() {
8649 let toml_content = r#"
8650 command_socket = "/tmp/sozu_test.sock"
8651 worker_count = 1
8652
8653 [[listeners]]
8654 protocol = "http"
8655 address = "0.0.0.0:8080"
8656 interface = "wg/0"
8657 "#;
8658 let file_config: FileConfig = toml::from_str(toml_content).expect("parse");
8659 let error = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
8660 .into_config()
8661 .expect_err("an invalid interface name must be refused");
8662 let message = error.to_string();
8663 assert!(message.contains("0.0.0.0:8080"), "{message}");
8664 assert!(message.contains("wg/0"), "{message}");
8665 }
8666
8667 #[test]
8668 fn duplicate_listener_address_rejected() {
8669 let toml_content = r#"
8670 command_socket = "/tmp/sozu_test.sock"
8671 worker_count = 1
8672
8673 [[listeners]]
8674 protocol = "http"
8675 address = "0.0.0.0:80"
8676
8677 [[listeners]]
8678 protocol = "http"
8679 address = "0.0.0.0:80"
8680 "#;
8681
8682 let file_config: FileConfig =
8683 toml::from_str(toml_content).expect("Could not parse TOML config");
8684
8685 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8686
8687 assert!(
8688 result.is_err(),
8689 "Should reject duplicate listener addresses"
8690 );
8691 }
8692
8693 #[test]
8694 fn buffer_size_below_h2_minimum_rejected() {
8695 let toml_content = r#"
8697 command_socket = "/tmp/sozu_test.sock"
8698 worker_count = 1
8699 buffer_size = 8192
8700
8701 [[listeners]]
8702 protocol = "https"
8703 address = "127.0.0.1:8443"
8704 "#;
8705 let file_config: FileConfig =
8706 toml::from_str(toml_content).expect("Could not parse TOML config");
8707 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8708 match result {
8709 Err(ConfigError::BufferSizeTooSmallForH2 {
8710 buffer_size: 8192,
8711 minimum: 16_393,
8712 listeners: 1,
8713 }) => {}
8714 other => panic!("expected BufferSizeTooSmallForH2, got {other:?}"),
8715 }
8716 }
8717
8718 #[test]
8719 fn buffer_size_below_h2_minimum_accepted_when_no_h2_listener() {
8720 let toml_content = r#"
8722 command_socket = "/tmp/sozu_test.sock"
8723 worker_count = 1
8724 buffer_size = 8192
8725
8726 [[listeners]]
8727 protocol = "https"
8728 address = "127.0.0.1:8443"
8729 alpn_protocols = ["http/1.1"]
8730 "#;
8731 let file_config: FileConfig =
8732 toml::from_str(toml_content).expect("Could not parse TOML config");
8733 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8734 assert!(
8735 result.is_ok(),
8736 "non-H2 HTTPS listener with sub-16393 buffer should be accepted: {result:?}"
8737 );
8738 }
8739
8740 #[test]
8744 fn buffer_size_below_h2_minimum_rejected_for_an_h2c_http_listener() {
8745 let parse = |h2c: bool| {
8746 let toml_content = format!(
8747 r#"
8748 command_socket = "/tmp/sozu_test.sock"
8749 worker_count = 1
8750 buffer_size = 8192
8751
8752 [[listeners]]
8753 protocol = "http"
8754 address = "127.0.0.1:8080"
8755 h2c_prior_knowledge = {h2c}
8756 "#
8757 );
8758 let file_config: FileConfig =
8759 toml::from_str(&toml_content).expect("Could not parse TOML config");
8760 ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config()
8761 };
8762 match parse(true) {
8763 Err(ConfigError::BufferSizeTooSmallForH2 {
8764 buffer_size: 8192,
8765 minimum: 16_393,
8766 listeners: 1,
8767 }) => {}
8768 other => panic!("expected BufferSizeTooSmallForH2, got {other:?}"),
8769 }
8770 let accepted = parse(false);
8771 assert!(
8772 accepted.is_ok(),
8773 "an HTTP/1-only HTTP listener with a sub-16393 buffer must be accepted: {accepted:?}"
8774 );
8775 }
8776
8777 fn command_socket_mode_of(mode_line: &str) -> Result<Config, ConfigError> {
8778 let toml_content = format!(
8779 r#"
8780 command_socket = "/tmp/sozu_test.sock"
8781 worker_count = 1
8782 {mode_line}
8783 "#
8784 );
8785 let file_config: FileConfig =
8786 toml::from_str(&toml_content).expect("Could not parse TOML config");
8787 ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config()
8788 }
8789
8790 #[test]
8791 fn command_socket_mode_defaults_to_0600() {
8792 let config = command_socket_mode_of("").expect("config without the key must load");
8793 assert_eq!(config.command_socket_mode, 0o600);
8794 assert_eq!(DEFAULT_COMMAND_SOCKET_MODE, 0o600);
8795 }
8796
8797 #[test]
8798 fn command_socket_mode_accepts_octal_strings() {
8799 for (value, expected) in [
8800 ("0660", 0o660),
8801 ("660", 0o660),
8802 ("0600", 0o600),
8803 ("0666", 0o666),
8804 ("777", 0o777),
8805 ("0777", 0o777),
8806 ("600", 0o600),
8807 ("0640", 0o640),
8808 ] {
8809 let config = command_socket_mode_of(&format!("command_socket_mode = \"{value}\""))
8810 .unwrap_or_else(|e| panic!("{value:?} must load: {e}"));
8811 assert_eq!(config.command_socket_mode, expected, "{value:?}");
8812 }
8813 }
8814
8815 #[test]
8816 fn command_socket_mode_rejects_invalid_values() {
8817 for value in [
8818 "", "8", "0668", "abc", "0o660", "0x1b6", "-660", "+660", " 660", "660 ", "1660",
8819 "2770", "4755", "7777", "01000", "00660", "0", "0000", "0066", "0200", "0400", "0466",
8820 ] {
8821 match command_socket_mode_of(&format!("command_socket_mode = \"{value}\"")) {
8822 Err(ConfigError::InvalidCommandSocketMode {
8823 value: rejected, ..
8824 }) => {
8825 assert_eq!(rejected, value)
8826 }
8827 other => panic!("{value:?} must be rejected, got {other:?}"),
8828 }
8829 }
8830 }
8831
8832 #[test]
8833 fn command_socket_mode_rejects_a_toml_integer() {
8834 let dir = tempfile::tempdir().expect("tempdir");
8838 let path = dir.path().join("config.toml");
8839 std::fs::write(
8840 &path,
8841 "command_socket = \"/tmp/sozu_test.sock\"\ncommand_socket_mode = 660\n",
8842 )
8843 .expect("write config");
8844 match FileConfig::load_from_path(path.to_str().expect("utf-8 path")) {
8845 Err(ConfigError::DeserializeToml(message)) => assert!(
8846 message.contains("config.toml") && message.contains(" at bytes "),
8847 "the diagnostic keeps the path and the byte range: {message}"
8848 ),
8849 other => panic!("an integer mode must fail as DeserializeToml, got {other:?}"),
8850 }
8851 }
8852
8853 #[test]
8854 fn config_without_command_socket_mode_deserializes_to_0600() {
8855 let mut encoded = serde_json::to_value(Config::default()).expect("serialize Config");
8858 encoded
8859 .as_object_mut()
8860 .expect("Config serializes to an object")
8861 .remove("command_socket_mode")
8862 .expect("Config serializes command_socket_mode");
8863 let decoded: Config = serde_json::from_value(encoded).expect("deserialize Config");
8864 assert_eq!(decoded.command_socket_mode, 0o600);
8865 }
8866
8867 #[test]
8868 fn command_buffer_size_exceeds_max_rejected() {
8869 let toml_content = r#"
8874 command_socket = "/tmp/sozu_test.sock"
8875 worker_count = 1
8876 command_buffer_size = 2000000
8877 max_command_buffer_size = 1000000
8878 "#;
8879 let file_config: FileConfig =
8880 toml::from_str(toml_content).expect("Could not parse TOML config");
8881 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8882 match result {
8883 Err(ConfigError::CommandBufferSizeExceedsMax {
8884 command_buffer_size: 2_000_000,
8885 max_command_buffer_size: 1_000_000,
8886 }) => {}
8887 other => panic!("expected CommandBufferSizeExceedsMax, got {other:?}"),
8888 }
8889 }
8890
8891 #[test]
8892 fn command_buffer_size_equal_to_max_accepted() {
8893 let toml_content = r#"
8896 command_socket = "/tmp/sozu_test.sock"
8897 worker_count = 1
8898 command_buffer_size = 1000000
8899 max_command_buffer_size = 1000000
8900 "#;
8901 let file_config: FileConfig =
8902 toml::from_str(toml_content).expect("Could not parse TOML config");
8903 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8904 assert!(
8905 result.is_ok(),
8906 "command_buffer_size == max_command_buffer_size should be accepted: {result:?}"
8907 );
8908 }
8909
8910 #[test]
8911 fn buffer_size_at_h2_minimum_accepted() {
8912 let toml_content = r#"
8913 command_socket = "/tmp/sozu_test.sock"
8914 worker_count = 1
8915 buffer_size = 16393
8916
8917 [[listeners]]
8918 protocol = "https"
8919 address = "127.0.0.1:8443"
8920 "#;
8921 let file_config: FileConfig =
8922 toml::from_str(toml_content).expect("Could not parse TOML config");
8923 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
8924 assert!(
8925 result.is_ok(),
8926 "buffer_size at the H2 minimum should be accepted: {result:?}"
8927 );
8928 }
8929
8930 #[test]
8931 fn alpn_protocols_default() {
8932 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8933 let config = builder.to_tls(None).expect("to_tls should succeed");
8934 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
8935 }
8936
8937 #[test]
8938 fn alpn_protocols_custom() {
8939 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8940 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned()]));
8941 let config = builder.to_tls(None).expect("to_tls should succeed");
8942 assert_eq!(config.alpn_protocols, vec!["http/1.1"]);
8943 }
8944
8945 #[test]
8946 fn alpn_protocols_invalid_rejected() {
8947 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8948 builder.with_alpn_protocols(Some(vec!["h3".to_owned()]));
8949 let result = builder.to_tls(None);
8950 assert!(result.is_err());
8951 let err = result.unwrap_err();
8952 assert!(
8953 err.to_string().contains("h3"),
8954 "error should mention the invalid protocol: {err}"
8955 );
8956 }
8957
8958 #[test]
8959 fn alpn_protocols_empty_uses_default() {
8960 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8961 builder.with_alpn_protocols(Some(vec![]));
8962 let config = builder.to_tls(None).expect("to_tls should succeed");
8963 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
8964 }
8965
8966 #[test]
8967 fn alpn_protocols_deduplicated() {
8968 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8969 builder.with_alpn_protocols(Some(vec![
8970 "h2".to_owned(),
8971 "h2".to_owned(),
8972 "http/1.1".to_owned(),
8973 ]));
8974 let config = builder.to_tls(None).expect("to_tls should succeed");
8975 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
8976 }
8977
8978 #[test]
8979 fn alpn_protocols_order_preserved() {
8980 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
8981 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned(), "h2".to_owned()]));
8982 let config = builder.to_tls(None).expect("to_tls should succeed");
8983 assert_eq!(config.alpn_protocols, vec!["http/1.1", "h2"]);
8984 }
8985
8986 #[test]
8992 fn parse_header_edit_rejects_crlf_in_value() {
8993 let entry = HeaderEditConfig {
8994 position: "request".to_owned(),
8995 key: "X-Test".to_owned(),
8996 value: "value\r\nEvil-Header: stolen".to_owned(),
8997 };
8998 let err = parse_header_edit(0, &entry).expect_err("CRLF in value must be rejected");
8999 match err {
9000 ConfigError::InvalidHeaderBytes { index, field } => {
9001 assert_eq!(index, 0);
9002 assert_eq!(field, "value");
9003 }
9004 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
9005 }
9006 }
9007
9008 #[test]
9009 fn parse_header_edit_rejects_lf_in_key() {
9010 let entry = HeaderEditConfig {
9011 position: "response".to_owned(),
9012 key: "X-\nTest".to_owned(),
9013 value: "ok".to_owned(),
9014 };
9015 let err = parse_header_edit(2, &entry).expect_err("LF in key must be rejected");
9016 match err {
9017 ConfigError::InvalidHeaderBytes { index, field } => {
9018 assert_eq!(index, 2);
9019 assert_eq!(field, "key");
9020 }
9021 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
9022 }
9023 }
9024
9025 #[test]
9026 fn parse_header_edit_rejects_nul() {
9027 let entry = HeaderEditConfig {
9028 position: "both".to_owned(),
9029 key: "X-Test".to_owned(),
9030 value: "with\0nul".to_owned(),
9031 };
9032 assert!(matches!(
9033 parse_header_edit(0, &entry),
9034 Err(ConfigError::InvalidHeaderBytes { .. })
9035 ));
9036 }
9037
9038 #[test]
9044 fn parse_header_edit_accepts_tab_in_value() {
9045 let entry = HeaderEditConfig {
9046 position: "request".to_owned(),
9047 key: "X-Test".to_owned(),
9048 value: "with\ttab".to_owned(),
9049 };
9050 let header = parse_header_edit(0, &entry).expect("tab in value must be accepted");
9051 assert_eq!(header.val, "with\ttab");
9052 }
9053
9054 #[test]
9061 fn parse_header_edit_rejects_tab_in_key() {
9062 let entry = HeaderEditConfig {
9063 position: "request".to_owned(),
9064 key: "Host\t".to_owned(),
9065 value: "ok".to_owned(),
9066 };
9067 let err = parse_header_edit(0, &entry).expect_err("HTAB in key must be rejected");
9068 match err {
9069 ConfigError::InvalidHeaderBytes { field, .. } => assert_eq!(field, "key"),
9070 other => panic!("expected InvalidHeaderBytes{{field=\"key\"}}, got {other:?}"),
9071 }
9072 }
9073
9074 #[test]
9075 fn parse_header_edit_rejects_space_in_key() {
9076 let entry = HeaderEditConfig {
9077 position: "request".to_owned(),
9078 key: "X Test".to_owned(),
9079 value: "ok".to_owned(),
9080 };
9081 let err = parse_header_edit(0, &entry).expect_err("SP in key must be rejected");
9082 assert!(matches!(err, ConfigError::InvalidHeaderBytes { .. }));
9083 }
9084
9085 #[test]
9086 fn parse_header_edit_rejects_empty_key() {
9087 let entry = HeaderEditConfig {
9088 position: "request".to_owned(),
9089 key: String::new(),
9090 value: "ok".to_owned(),
9091 };
9092 let err = parse_header_edit(0, &entry).expect_err("empty key must be rejected");
9093 assert!(matches!(
9094 err,
9095 ConfigError::InvalidHeaderBytes { field: "key", .. }
9096 ));
9097 }
9098
9099 #[test]
9100 fn parse_header_edit_accepts_clean_value() {
9101 let entry = HeaderEditConfig {
9102 position: "request".to_owned(),
9103 key: "X-Tenant".to_owned(),
9104 value: "alpha".to_owned(),
9105 };
9106 let header = parse_header_edit(0, &entry).expect("clean value must be accepted");
9107 assert_eq!(header.key, "X-Tenant");
9108 assert_eq!(header.val, "alpha");
9109 }
9110
9111 #[test]
9115 fn resolve_answer_source_bare_string_is_literal() {
9116 let body = resolve_answer_source("HTTP/1.1 503 Service Unavailable\r\n\r\nbusy")
9117 .expect("bare-string source must resolve");
9118 assert_eq!(body, "HTTP/1.1 503 Service Unavailable\r\n\r\nbusy");
9119 }
9120
9121 #[test]
9122 fn resolve_answer_source_empty_string_is_legitimate() {
9123 let body = resolve_answer_source("").expect("empty source must resolve");
9124 assert_eq!(body, "");
9125 }
9126
9127 #[test]
9132 fn resolve_answer_source_file_scheme_missing_file_errors() {
9133 let err = resolve_answer_source("file:///nonexistent/sozu-test/never.http")
9134 .expect_err("missing path must error");
9135 assert!(matches!(err, ConfigError::FileOpen { .. }));
9136 }
9137
9138 #[test]
9141 fn resolve_answer_source_file_scheme_empty_path_errors() {
9142 let err = resolve_answer_source("file://").expect_err("empty path must error");
9143 assert!(matches!(err, ConfigError::FileOpen { .. }));
9144 }
9145
9146 #[test]
9154 fn legacy_tcp_frontend_toml_without_sni_alpn_parses_unchanged() {
9155 let toml_content = r#"
9156 command_socket = "/tmp/sozu_test.sock"
9157 worker_count = 1
9158
9159 [[listeners]]
9160 protocol = "tcp"
9161 address = "127.0.0.1:9000"
9162
9163 [clusters.legacy]
9164 protocol = "tcp"
9165 load_balancing = "ROUND_ROBIN"
9166 frontends = [
9167 { address = "127.0.0.1:9000" }
9168 ]
9169 backends = [
9170 { address = "10.0.0.1:9000" }
9171 ]
9172 "#;
9173 let file_config: FileConfig =
9174 toml::from_str(toml_content).expect("Could not parse legacy TCP TOML");
9175 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9176 .into_config()
9177 .expect("legacy TCP config without sni/alpn must load unchanged");
9178
9179 assert_eq!(config.tcp_listeners.len(), 1);
9180 let listener = &config.tcp_listeners[0];
9181 assert_eq!(
9182 listener.sni_preread_timeout,
9183 Some(DEFAULT_SNI_PREREAD_TIMEOUT),
9184 "proto default sni_preread_timeout must be populated even though unused"
9185 );
9186 assert_eq!(
9187 listener.sni_preread_max_bytes,
9188 Some(DEFAULT_SNI_PREREAD_MAX_BYTES),
9189 "proto default sni_preread_max_bytes must be populated even though unused"
9190 );
9191
9192 let messages = config
9193 .generate_config_messages()
9194 .expect("Could not generate config messages");
9195 let tcp_frontend = messages
9196 .iter()
9197 .find_map(|m| match &m.content.request_type {
9198 Some(RequestType::AddTcpFrontend(f)) => Some(f),
9199 _ => None,
9200 })
9201 .expect("AddTcpFrontend must be present");
9202 assert_eq!(tcp_frontend.sni, None, "legacy frontend must carry no sni");
9203 assert!(
9204 tcp_frontend.alpn.is_empty(),
9205 "legacy frontend must carry no alpn"
9206 );
9207 }
9208
9209 #[test]
9220 fn a_catch_all_post_frontend_loads_from_toml_and_rejects_a_mixed_case_position() {
9221 let catch_all = |position: &str| {
9222 format!(
9223 r#"
9224 command_socket = "/tmp/sozu_test.sock"
9225 worker_count = 1
9226
9227 [[listeners]]
9228 protocol = "http"
9229 address = "127.0.0.1:8080"
9230
9231 [clusters.fallback]
9232 protocol = "http"
9233 frontends = [
9234 {{ address = "127.0.0.1:8080", hostname = "*", position = "{position}" }}
9235 ]
9236 backends = [
9237 {{ address = "10.0.0.1:8000" }}
9238 ]
9239 "#
9240 )
9241 };
9242
9243 let file_config: FileConfig = toml::from_str(&catch_all("POST"))
9244 .expect("an upper-case `position = \"POST\"` must parse");
9245 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9246 .into_config()
9247 .expect("a catch-all POST frontend must load");
9248 let messages = config
9249 .generate_config_messages()
9250 .expect("Could not generate config messages");
9251 let frontend = messages
9252 .iter()
9253 .find_map(|m| match &m.content.request_type {
9254 Some(RequestType::AddHttpFrontend(f)) => Some(f),
9255 _ => None,
9256 })
9257 .expect("AddHttpFrontend must be present");
9258 assert_eq!(frontend.hostname, "*");
9259 assert_eq!(frontend.position, RulePosition::Post as i32);
9260
9261 let error = toml::from_str::<FileConfig>(&catch_all("Post"))
9262 .expect_err("a mixed-case `position = \"Post\"` must be refused")
9263 .to_string();
9264 assert!(
9265 error.contains("unknown variant `Post`, expected one of `PRE`, `POST`, `TREE`"),
9266 "unexpected error: {error}",
9267 );
9268 }
9269
9270 #[test]
9276 fn forwarded_headers_parses_every_mode_defaults_to_both_and_rejects_others() {
9277 let listeners = |value: Option<&str>| {
9278 let line = value
9279 .map(|value| format!("forwarded_headers = \"{value}\""))
9280 .unwrap_or_default();
9281 format!(
9282 r#"
9283 command_socket = "/tmp/sozu_test.sock"
9284 worker_count = 1
9285
9286 [[listeners]]
9287 protocol = "http"
9288 address = "127.0.0.1:8080"
9289 {line}
9290
9291 [[listeners]]
9292 protocol = "https"
9293 address = "127.0.0.1:8443"
9294 {line}
9295 "#
9296 )
9297 };
9298 let load = |value: Option<&str>| {
9299 let file_config: FileConfig =
9300 toml::from_str(&listeners(value)).expect("the listeners must parse");
9301 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9302 .into_config()
9303 .expect("the listeners must load");
9304 (
9305 config.http_listeners[0].forwarded_headers,
9306 config.https_listeners[0].forwarded_headers,
9307 )
9308 };
9309
9310 for (spelling, expected) in [
9311 (None, ForwardedHeaders::Both),
9312 (Some("both"), ForwardedHeaders::Both),
9313 (Some("x_forwarded"), ForwardedHeaders::XForwarded),
9314 (Some("rfc7239"), ForwardedHeaders::Rfc7239),
9315 (Some("none"), ForwardedHeaders::None),
9316 ] {
9317 let expected = Some(expected as i32);
9318 assert_eq!(load(spelling), (expected, expected), "{spelling:?}");
9319 }
9320
9321 for invalid in ["RFC7239", "x-forwarded", "forwarded", ""] {
9322 let error = toml::from_str::<FileConfig>(&listeners(Some(invalid)))
9323 .expect_err("an unknown forwarded_headers value must be refused")
9324 .to_string();
9325 assert!(
9326 error.contains(&format!(
9327 "unknown variant `{invalid}`, expected one of `both`, `x_forwarded`, `rfc7239`, `none`"
9328 )),
9329 "{invalid:?}: unexpected error: {error}",
9330 );
9331 }
9332 }
9333
9334 #[test]
9337 fn tcp_frontend_hostname_maps_to_sni_exact_and_wildcard() {
9338 let toml_content = r#"
9339 command_socket = "/tmp/sozu_test.sock"
9340 worker_count = 1
9341
9342 [[listeners]]
9343 protocol = "tcp"
9344 address = "127.0.0.1:9010"
9345
9346 [clusters.exact]
9347 protocol = "tcp"
9348 load_balancing = "ROUND_ROBIN"
9349 frontends = [
9350 { address = "127.0.0.1:9010", hostname = "example.com", alpn = ["h2"] }
9351 ]
9352 backends = [ { address = "10.0.0.1:9010" } ]
9353
9354 [clusters.wildcard]
9355 protocol = "tcp"
9356 load_balancing = "ROUND_ROBIN"
9357 frontends = [
9358 { address = "127.0.0.1:9010", hostname = "*.example.com" }
9359 ]
9360 backends = [ { address = "10.0.0.2:9010" } ]
9361 "#;
9362 let file_config: FileConfig =
9363 toml::from_str(toml_content).expect("Could not parse TOML config");
9364 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9365 .into_config()
9366 .expect("exact + wildcard SNI frontends on distinct sni must load");
9367
9368 let messages = config
9369 .generate_config_messages()
9370 .expect("Could not generate config messages");
9371 let mut frontends: Vec<_> = messages
9372 .iter()
9373 .filter_map(|m| match &m.content.request_type {
9374 Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
9375 _ => None,
9376 })
9377 .collect();
9378 frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
9379
9380 assert_eq!(frontends.len(), 2);
9381 assert_eq!(frontends[0].sni, Some("example.com".to_string()));
9382 assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
9383 assert_eq!(frontends[1].sni, Some("*.example.com".to_string()));
9384 assert!(frontends[1].alpn.is_empty());
9385 }
9386
9387 #[test]
9392 fn tcp_frontend_invalid_sni_pattern_rejected() {
9393 for invalid in [
9394 "*.*.example.com",
9395 "foo.*.com",
9396 "*",
9397 "example..com",
9398 "",
9399 "/[a-z]+/.example.com",
9400 "foo/bar.example.com",
9401 ] {
9402 let frontend = FileClusterFrontendConfig {
9403 address: "127.0.0.1:8080".parse().unwrap(),
9404 hostname: Some(invalid.to_string()),
9405 alpn: vec![],
9406 path: None,
9407 path_type: None,
9408 method: None,
9409 certificate: None,
9410 key: None,
9411 certificate_chain: None,
9412 tls_versions: vec![],
9413 position: RulePosition::Tree,
9414 tags: None,
9415 redirect: None,
9416 redirect_scheme: None,
9417 redirect_template: None,
9418 rewrite_host: None,
9419 rewrite_path: None,
9420 rewrite_port: None,
9421 required_auth: None,
9422 headers: None,
9423 hsts: None,
9424 header_predicates: None,
9425 };
9426 match frontend.to_tcp_front() {
9427 Err(ConfigError::InvalidSniPattern { sni }) => assert_eq!(sni, invalid),
9428 other => panic!("expected InvalidSniPattern for {invalid:?}, got {other:?}"),
9429 }
9430 }
9431 }
9432
9433 #[test]
9439 fn tcp_frontend_non_ascii_sni_pattern_rejected() {
9440 for non_ascii in ["münchen.example", "*.bücher.example", "日本.example"] {
9441 let frontend = FileClusterFrontendConfig {
9442 address: "127.0.0.1:8080".parse().unwrap(),
9443 hostname: Some(non_ascii.to_string()),
9444 alpn: vec![],
9445 path: None,
9446 path_type: None,
9447 method: None,
9448 certificate: None,
9449 key: None,
9450 certificate_chain: None,
9451 tls_versions: vec![],
9452 position: RulePosition::Tree,
9453 tags: None,
9454 redirect: None,
9455 redirect_scheme: None,
9456 redirect_template: None,
9457 rewrite_host: None,
9458 rewrite_path: None,
9459 rewrite_port: None,
9460 required_auth: None,
9461 headers: None,
9462 hsts: None,
9463 header_predicates: None,
9464 };
9465 match frontend.to_tcp_front() {
9466 Err(ConfigError::NonAsciiSniPattern { sni }) => assert_eq!(sni, non_ascii),
9467 other => panic!("expected NonAsciiSniPattern for {non_ascii:?}, got {other:?}"),
9468 }
9469 }
9470 }
9471
9472 #[test]
9477 fn tcp_frontend_punycode_sni_pattern_accepted() {
9478 assert_eq!(
9479 validate_sni_pattern("xn--mnchen-3ya.example").expect("A-label must be accepted"),
9480 "xn--mnchen-3ya.example"
9481 );
9482 assert_eq!(
9483 validate_sni_pattern("*.xn--bcher-kva.example")
9484 .expect("wildcarded A-label must be accepted"),
9485 "*.xn--bcher-kva.example"
9486 );
9487 assert_eq!(
9488 validate_sni_pattern("XN--MNCHEN-3YA.Example")
9489 .expect("mixed-case A-label must be accepted"),
9490 "xn--mnchen-3ya.example",
9491 "A-label patterns are ASCII-lowercased like any other pattern"
9492 );
9493 }
9494
9495 #[test]
9498 fn alpn_rejected_on_http_frontend() {
9499 let frontend = FileClusterFrontendConfig {
9500 address: "127.0.0.1:8080".parse().unwrap(),
9501 hostname: Some("example.com".to_owned()),
9502 alpn: vec!["h2".to_string()],
9503 path: None,
9504 path_type: None,
9505 method: None,
9506 certificate: None,
9507 key: None,
9508 certificate_chain: None,
9509 tls_versions: vec![],
9510 position: RulePosition::Tree,
9511 tags: None,
9512 redirect: None,
9513 redirect_scheme: None,
9514 redirect_template: None,
9515 rewrite_host: None,
9516 rewrite_path: None,
9517 rewrite_port: None,
9518 required_auth: None,
9519 headers: None,
9520 hsts: None,
9521 header_predicates: None,
9522 };
9523 match frontend.to_http_front("api") {
9524 Err(ConfigError::InvalidFrontendConfig(field)) => assert_eq!(field, "alpn"),
9525 other => panic!("expected InvalidFrontendConfig(\"alpn\"), got {other:?}"),
9526 }
9527 }
9528
9529 #[test]
9532 fn header_predicates_rejected_on_tcp_frontend() {
9533 let frontend = FileClusterFrontendConfig {
9534 address: "127.0.0.1:8080".parse().unwrap(),
9535 hostname: None,
9536 alpn: vec![],
9537 path: None,
9538 path_type: None,
9539 method: None,
9540 certificate: None,
9541 key: None,
9542 certificate_chain: None,
9543 tls_versions: vec![],
9544 position: RulePosition::Tree,
9545 tags: None,
9546 redirect: None,
9547 redirect_scheme: None,
9548 redirect_template: None,
9549 rewrite_host: None,
9550 rewrite_path: None,
9551 rewrite_port: None,
9552 required_auth: None,
9553 headers: None,
9554 hsts: None,
9555 header_predicates: Some(vec![]),
9556 };
9557 match frontend.to_tcp_front() {
9558 Err(ConfigError::InvalidFrontendConfig(field)) => {
9559 assert_eq!(field, "header_predicates")
9560 }
9561 other => panic!("expected InvalidFrontendConfig(\"header_predicates\"), got {other:?}"),
9562 }
9563 }
9564
9565 #[test]
9571 fn tcp_frontend_alpn_without_sni_rejected() {
9572 let toml_content = r#"
9573 command_socket = "/tmp/sozu_test.sock"
9574 worker_count = 1
9575
9576 [[listeners]]
9577 protocol = "tcp"
9578 address = "127.0.0.1:9019"
9579
9580 [clusters.a]
9581 protocol = "tcp"
9582 load_balancing = "ROUND_ROBIN"
9583 frontends = [
9584 { address = "127.0.0.1:9019", alpn = ["h2"] }
9585 ]
9586 backends = [ { address = "10.0.0.1:9019" } ]
9587 "#;
9588 let file_config: FileConfig =
9589 toml::from_str(toml_content).expect("Could not parse TOML config");
9590 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9591 match result {
9592 Err(ConfigError::AlpnWithoutSni { address }) => {
9593 assert_eq!(address.to_string(), "127.0.0.1:9019");
9594 }
9595 other => panic!("expected AlpnWithoutSni, got {other:?}"),
9596 }
9597 }
9598
9599 #[test]
9603 fn tcp_frontend_alpn_overlap_rejected() {
9604 let toml_content = r#"
9605 command_socket = "/tmp/sozu_test.sock"
9606 worker_count = 1
9607
9608 [[listeners]]
9609 protocol = "tcp"
9610 address = "127.0.0.1:9020"
9611
9612 [clusters.a]
9613 protocol = "tcp"
9614 load_balancing = "ROUND_ROBIN"
9615 frontends = [
9616 { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2"] }
9617 ]
9618 backends = [ { address = "10.0.0.1:9020" } ]
9619
9620 [clusters.b]
9621 protocol = "tcp"
9622 load_balancing = "ROUND_ROBIN"
9623 frontends = [
9624 { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2", "http/1.1"] }
9625 ]
9626 backends = [ { address = "10.0.0.2:9020" } ]
9627 "#;
9628 let file_config: FileConfig =
9629 toml::from_str(toml_content).expect("Could not parse TOML config");
9630 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9631 match result {
9632 Err(ConfigError::TcpFrontendAlpnOverlap { protocol, .. }) => {
9633 assert_eq!(protocol, "h2");
9634 }
9635 other => panic!("expected TcpFrontendAlpnOverlap, got {other:?}"),
9636 }
9637 }
9638
9639 #[test]
9643 fn tcp_frontend_multiple_alpn_catch_all_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:9021"
9651
9652 [clusters.a]
9653 protocol = "tcp"
9654 load_balancing = "ROUND_ROBIN"
9655 frontends = [
9656 { address = "127.0.0.1:9021", hostname = "example.com" }
9657 ]
9658 backends = [ { address = "10.0.0.1:9021" } ]
9659
9660 [clusters.b]
9661 protocol = "tcp"
9662 load_balancing = "ROUND_ROBIN"
9663 frontends = [
9664 { address = "127.0.0.1:9021", hostname = "example.com" }
9665 ]
9666 backends = [ { address = "10.0.0.2:9021" } ]
9667 "#;
9668 let file_config: FileConfig =
9669 toml::from_str(toml_content).expect("Could not parse TOML config");
9670 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9671 assert!(
9672 matches!(
9673 result,
9674 Err(ConfigError::TcpFrontendMultipleAlpnCatchAll { .. })
9675 ),
9676 "expected TcpFrontendMultipleAlpnCatchAll, got {result:?}"
9677 );
9678 }
9679
9680 #[test]
9684 fn tcp_listener_mixes_sni_and_no_sni_rejected() {
9685 let toml_content = r#"
9686 command_socket = "/tmp/sozu_test.sock"
9687 worker_count = 1
9688
9689 [[listeners]]
9690 protocol = "tcp"
9691 address = "127.0.0.1:9022"
9692
9693 [clusters.a]
9694 protocol = "tcp"
9695 load_balancing = "ROUND_ROBIN"
9696 frontends = [
9697 { address = "127.0.0.1:9022", hostname = "example.com" }
9698 ]
9699 backends = [ { address = "10.0.0.1:9022" } ]
9700
9701 [clusters.b]
9702 protocol = "tcp"
9703 load_balancing = "ROUND_ROBIN"
9704 frontends = [
9705 { address = "127.0.0.1:9022" }
9706 ]
9707 backends = [ { address = "10.0.0.2:9022" } ]
9708 "#;
9709 let file_config: FileConfig =
9710 toml::from_str(toml_content).expect("Could not parse TOML config");
9711 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9712 assert!(
9713 matches!(result, Err(ConfigError::TcpListenerMixesSniAndNoSni { .. })),
9714 "expected TcpListenerMixesSniAndNoSni, got {result:?}"
9715 );
9716 }
9717
9718 #[test]
9723 fn sni_preread_timeout_exceeding_front_timeout_rejected() {
9724 let toml_content = r#"
9725 command_socket = "/tmp/sozu_test.sock"
9726 worker_count = 1
9727
9728 [[listeners]]
9729 protocol = "tcp"
9730 address = "127.0.0.1:9030"
9731 front_timeout = 2
9732
9733 [clusters.a]
9734 protocol = "tcp"
9735 load_balancing = "ROUND_ROBIN"
9736 frontends = [
9737 { address = "127.0.0.1:9030", hostname = "example.com" }
9738 ]
9739 backends = [ { address = "10.0.0.1:9030" } ]
9740 "#;
9741 let file_config: FileConfig =
9742 toml::from_str(toml_content).expect("Could not parse TOML config");
9743 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9744 match result {
9745 Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
9746 sni_preread_timeout: 5,
9747 front_timeout: 2,
9748 ..
9749 }) => {}
9750 other => panic!("expected SniPrereadTimeoutExceedsFrontTimeout, got {other:?}"),
9751 }
9752 }
9753
9754 #[test]
9758 fn sni_preread_max_bytes_exceeding_buffer_size_rejected() {
9759 let toml_content = r#"
9760 command_socket = "/tmp/sozu_test.sock"
9761 worker_count = 1
9762 buffer_size = 8192
9763
9764 [[listeners]]
9765 protocol = "tcp"
9766 address = "127.0.0.1:9031"
9767
9768 [clusters.a]
9769 protocol = "tcp"
9770 load_balancing = "ROUND_ROBIN"
9771 frontends = [
9772 { address = "127.0.0.1:9031", hostname = "example.com" }
9773 ]
9774 backends = [ { address = "10.0.0.1:9031" } ]
9775 "#;
9776 let file_config: FileConfig =
9777 toml::from_str(toml_content).expect("Could not parse TOML config");
9778 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9779 match result {
9780 Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
9781 sni_preread_max_bytes: 16384,
9782 buffer_size: 8192,
9783 ..
9784 }) => {}
9785 other => panic!("expected SniPrereadMaxBytesExceedsBufferSize, got {other:?}"),
9786 }
9787 }
9788
9789 #[test]
9794 fn sni_preread_max_bytes_zero_rejected() {
9795 let toml_content = r#"
9796 command_socket = "/tmp/sozu_test.sock"
9797 worker_count = 1
9798
9799 [[listeners]]
9800 protocol = "tcp"
9801 address = "127.0.0.1:9033"
9802 sni_preread_max_bytes = 0
9803
9804 [clusters.a]
9805 protocol = "tcp"
9806 load_balancing = "ROUND_ROBIN"
9807 frontends = [
9808 { address = "127.0.0.1:9033", hostname = "example.com" }
9809 ]
9810 backends = [ { address = "10.0.0.1:9033" } ]
9811 "#;
9812 let file_config: FileConfig =
9813 toml::from_str(toml_content).expect("Could not parse TOML config");
9814 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9815 match result {
9816 Err(ConfigError::SniPrereadMaxBytesTooSmall {
9817 sni_preread_max_bytes: 0,
9818 minimum: 5,
9819 ..
9820 }) => {}
9821 other => panic!("expected SniPrereadMaxBytesTooSmall, got {other:?}"),
9822 }
9823 }
9824
9825 #[test]
9828 fn sni_preread_max_bytes_at_the_floor_loads() {
9829 let toml_content = r#"
9830 command_socket = "/tmp/sozu_test.sock"
9831 worker_count = 1
9832
9833 [[listeners]]
9834 protocol = "tcp"
9835 address = "127.0.0.1:9034"
9836 sni_preread_max_bytes = 5
9837
9838 [clusters.a]
9839 protocol = "tcp"
9840 load_balancing = "ROUND_ROBIN"
9841 frontends = [
9842 { address = "127.0.0.1:9034", hostname = "example.com" }
9843 ]
9844 backends = [ { address = "10.0.0.1:9034" } ]
9845 "#;
9846 let file_config: FileConfig =
9847 toml::from_str(toml_content).expect("Could not parse TOML config");
9848 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9849 assert!(
9850 result.is_ok(),
9851 "sni_preread_max_bytes at the exact floor must load: {result:?}"
9852 );
9853 }
9854
9855 #[test]
9859 fn sni_preread_validation_ignored_without_sni_frontend() {
9860 let toml_content = r#"
9861 command_socket = "/tmp/sozu_test.sock"
9862 worker_count = 1
9863 buffer_size = 8192
9864
9865 [[listeners]]
9866 protocol = "tcp"
9867 address = "127.0.0.1:9032"
9868 front_timeout = 2
9869
9870 [clusters.a]
9871 protocol = "tcp"
9872 load_balancing = "ROUND_ROBIN"
9873 frontends = [
9874 { address = "127.0.0.1:9032" }
9875 ]
9876 backends = [ { address = "10.0.0.1:9032" } ]
9877 "#;
9878 let file_config: FileConfig =
9879 toml::from_str(toml_content).expect("Could not parse TOML config");
9880 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9881 assert!(
9882 result.is_ok(),
9883 "a no-SNI TCP listener must ignore sni_preread validation entirely: {result:?}"
9884 );
9885 }
9886
9887 #[test]
9894 fn tcp_frontend_disjoint_alpn_same_sni_both_load() {
9895 let toml_content = r#"
9896 command_socket = "/tmp/sozu_test.sock"
9897 worker_count = 1
9898
9899 [[listeners]]
9900 protocol = "tcp"
9901 address = "127.0.0.1:9040"
9902
9903 [clusters.h2_cluster]
9904 protocol = "tcp"
9905 load_balancing = "ROUND_ROBIN"
9906 frontends = [
9907 { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["h2"] }
9908 ]
9909 backends = [ { address = "10.0.0.1:9040" } ]
9910
9911 [clusters.http11_cluster]
9912 protocol = "tcp"
9913 load_balancing = "ROUND_ROBIN"
9914 frontends = [
9915 { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["http/1.1"] }
9916 ]
9917 backends = [ { address = "10.0.0.2:9040" } ]
9918 "#;
9919 let file_config: FileConfig =
9920 toml::from_str(toml_content).expect("Could not parse TOML config");
9921 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
9922 .into_config()
9923 .expect("disjoint non-empty ALPN lists on the same (address, sni) must load");
9924
9925 let messages = config
9926 .generate_config_messages()
9927 .expect("Could not generate config messages");
9928 let mut frontends: Vec<_> = messages
9929 .iter()
9930 .filter_map(|m| match &m.content.request_type {
9931 Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
9932 _ => None,
9933 })
9934 .collect();
9935 frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
9936
9937 assert_eq!(frontends.len(), 2, "both frontends must be emitted");
9938 assert_eq!(frontends[0].cluster_id, "h2_cluster");
9939 assert_eq!(frontends[0].sni, Some("example.com".to_string()));
9940 assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
9941 assert_eq!(frontends[1].cluster_id, "http11_cluster");
9942 assert_eq!(frontends[1].sni, Some("example.com".to_string()));
9943 assert_eq!(frontends[1].alpn, vec!["http/1.1".to_string()]);
9944 }
9945
9946 #[test]
9962 fn udp_routed_tcp_frontends_are_excluded_from_sni_invariants() {
9963 let toml_content = r#"
9964 command_socket = "/tmp/sozu_test.sock"
9965 worker_count = 1
9966
9967 [[listeners]]
9968 protocol = "udp"
9969 address = "127.0.0.1:9050"
9970
9971 [clusters.a]
9972 protocol = "tcp"
9973 load_balancing = "ROUND_ROBIN"
9974 frontends = [
9975 { address = "127.0.0.1:9050", hostname = "example.com" }
9976 ]
9977 backends = [ { address = "10.0.0.1:9050" } ]
9978
9979 [clusters.b]
9980 protocol = "tcp"
9981 load_balancing = "ROUND_ROBIN"
9982 frontends = [
9983 { address = "127.0.0.1:9050" }
9984 ]
9985 backends = [ { address = "10.0.0.2:9050" } ]
9986 "#;
9987 let file_config: FileConfig =
9988 toml::from_str(toml_content).expect("Could not parse TOML config");
9989 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
9990 assert!(
9991 result.is_ok(),
9992 "UDP-routed tcp-cluster frontends must not trip the SNI mixing-ban: {result:?}"
9993 );
9994
9995 let config = result.expect("checked is_ok above");
9996 let messages = config
9997 .generate_config_messages()
9998 .expect("Could not generate config messages");
9999 let add_udp_frontend_count = messages
10000 .iter()
10001 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
10002 .count();
10003 assert_eq!(
10004 add_udp_frontend_count, 2,
10005 "both cluster frontends on the udp listener must emit AddUdpFrontend"
10006 );
10007 }
10008
10009 #[test]
10014 fn subnet_connection_limit_keys_round_trip_from_toml() {
10015 let toml_content = r#"
10016 command_socket = "/tmp/sozu.sock"
10017 saved_state = "./state.json"
10018 worker_count = 1
10019
10020 max_connections_per_ip = 10
10021 max_connections_per_subnet = 100
10022 subnet_ipv4_prefix = 24
10023 subnet_ipv6_prefix = 56
10024
10025 [[listeners]]
10026 protocol = "http"
10027 address = "127.0.0.1:8080"
10028
10029 [clusters.api]
10030 protocol = "http"
10031 load_balancing = "ROUND_ROBIN"
10032 max_connections_per_subnet = 7
10033 frontends = [
10034 { address = "127.0.0.1:8080", hostname = "example.com" }
10035 ]
10036 backends = [ { address = "10.0.0.1:8080" } ]
10037 "#;
10038 let file_config: FileConfig =
10039 toml::from_str(toml_content).expect("Could not parse TOML config");
10040 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
10041 .into_config()
10042 .expect("a config with valid subnet prefixes must load");
10043
10044 assert_eq!(
10045 config.max_connections_per_subnet, 100,
10046 "the global subnet cap must survive the TOML round trip"
10047 );
10048 assert_eq!(
10049 config.subnet_ipv4_prefix, 24,
10050 "subnet_ipv4_prefix must survive the TOML round trip"
10051 );
10052 assert_eq!(
10053 config.subnet_ipv6_prefix, 56,
10054 "subnet_ipv6_prefix must survive the TOML round trip — a swap with the IPv6 \
10055 key would leave a cap that silently never fires"
10056 );
10057 assert_eq!(
10058 config.max_connections_per_ip, 10,
10059 "the pre-existing per-IP cap must be unaffected: the two are independent"
10060 );
10061
10062 let cluster = config
10063 .clusters
10064 .get("api")
10065 .expect("the staged cluster must be present");
10066 let override_value = match cluster {
10067 ClusterConfig::Http(http) => http.max_connections_per_subnet,
10068 other => panic!("expected an HTTP cluster, got {other:?}"),
10069 };
10070 assert_eq!(
10071 override_value,
10072 Some(7),
10073 "the per-cluster subnet override must reach the cluster config"
10074 );
10075 }
10076
10077 #[test]
10081 fn max_connection_attempts_defaults_to_five_and_round_trips_from_toml() {
10082 let config_with = |global: &str, cluster: &str| -> Config {
10083 let toml_content = format!(
10084 r#"
10085 command_socket = "/tmp/sozu.sock"
10086 saved_state = "./state.json"
10087 worker_count = 1
10088 {global}
10089
10090 [[listeners]]
10091 protocol = "http"
10092 address = "127.0.0.1:8080"
10093
10094 [clusters.api]
10095 protocol = "http"
10096 load_balancing = "ROUND_ROBIN"
10097 {cluster}
10098 frontends = [
10099 {{ address = "127.0.0.1:8080", hostname = "example.com" }}
10100 ]
10101 backends = [ {{ address = "10.0.0.1:8080" }} ]
10102 "#
10103 );
10104 let file_config: FileConfig =
10105 toml::from_str(&toml_content).expect("Could not parse TOML config");
10106 ConfigBuilder::new(file_config, "/tmp/test_config.toml")
10107 .into_config()
10108 .expect("a valid attempt budget must load")
10109 };
10110 let cluster_attempts = |config: &Config| match config.clusters.get("api") {
10111 Some(ClusterConfig::Http(http)) => http.max_connection_attempts,
10112 other => panic!("expected the HTTP cluster, got {other:?}"),
10113 };
10114 let add_cluster_attempts = |config: &Config| {
10115 config
10116 .generate_config_messages()
10117 .expect("Could not generate config messages")
10118 .into_iter()
10119 .find_map(|message| match message.content.request_type {
10120 Some(RequestType::AddCluster(cluster)) => Some(cluster.max_connection_attempts),
10121 _ => None,
10122 })
10123 .expect("the cluster is added")
10124 };
10125
10126 let defaults = config_with("", "");
10127 assert_eq!(DEFAULT_MAX_CONNECTION_ATTEMPTS, 5);
10128 assert_eq!(defaults.max_connection_attempts, 5);
10129 assert_eq!(
10130 ServerConfig::from(&defaults).max_connection_attempts,
10131 Some(5),
10132 "the worker receives the default"
10133 );
10134 assert_eq!(
10135 cluster_attempts(&defaults),
10136 None,
10137 "a cluster inherits by default"
10138 );
10139 assert_eq!(add_cluster_attempts(&defaults), None);
10140
10141 let set = config_with("max_connection_attempts = 2", "max_connection_attempts = 7");
10142 assert_eq!(set.max_connection_attempts, 2);
10143 assert_eq!(ServerConfig::from(&set).max_connection_attempts, Some(2));
10144 assert_eq!(cluster_attempts(&set), Some(7));
10145 assert_eq!(add_cluster_attempts(&set), Some(7));
10146 }
10147
10148 #[test]
10152 fn an_out_of_range_max_connection_attempts_is_rejected() {
10153 let load = |global: &str, cluster: &str| {
10154 let toml_content = format!(
10155 r#"
10156 command_socket = "/tmp/sozu.sock"
10157 saved_state = "./state.json"
10158 worker_count = 1
10159 {global}
10160
10161 [[listeners]]
10162 protocol = "http"
10163 address = "127.0.0.1:8080"
10164
10165 [clusters.api]
10166 protocol = "http"
10167 load_balancing = "ROUND_ROBIN"
10168 {cluster}
10169 frontends = [
10170 {{ address = "127.0.0.1:8080", hostname = "example.com" }}
10171 ]
10172 backends = [ {{ address = "10.0.0.1:8080" }} ]
10173 "#
10174 );
10175 let file_config: FileConfig =
10176 toml::from_str(&toml_content).expect("Could not parse TOML config");
10177 ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config()
10178 };
10179 for value in ["0", "256"] {
10180 let global = format!("max_connection_attempts = {value}");
10181 assert!(
10182 matches!(
10183 load(&global, ""),
10184 Err(ConfigError::InvalidMaxConnectionAttempts {
10185 cluster_id: None,
10186 ..
10187 })
10188 ),
10189 "a global budget of {value} must be rejected"
10190 );
10191 assert!(
10192 matches!(
10193 load("", &global),
10194 Err(ConfigError::InvalidMaxConnectionAttempts {
10195 cluster_id: Some(_),
10196 ..
10197 })
10198 ),
10199 "a cluster budget of {value} must be rejected"
10200 );
10201 }
10202 for value in ["1", "255"] {
10203 let budget = format!("max_connection_attempts = {value}");
10204 assert!(load(&budget, &budget).is_ok(), "{value} is in range");
10205 }
10206 }
10207
10208 #[test]
10212 fn an_out_of_range_subnet_prefix_is_rejected_not_clamped() {
10213 let with_prefixes = |v4: &str, v6: &str| -> String {
10214 format!(
10215 r#"
10216 command_socket = "/tmp/sozu.sock"
10217 saved_state = "./state.json"
10218 worker_count = 1
10219
10220 max_connections_per_subnet = 100
10221 subnet_ipv4_prefix = {v4}
10222 subnet_ipv6_prefix = {v6}
10223
10224 [[listeners]]
10225 protocol = "http"
10226 address = "127.0.0.1:8080"
10227 "#
10228 )
10229 };
10230
10231 for (v4, v6, key) in [
10232 ("33", "56", "subnet_ipv4_prefix"),
10233 ("24", "129", "subnet_ipv6_prefix"),
10234 ] {
10235 let file_config: FileConfig =
10236 toml::from_str(&with_prefixes(v4, v6)).expect("Could not parse TOML config");
10237 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
10238 match result {
10239 Err(ConfigError::InvalidSubnetPrefix { key: reported, .. }) => {
10240 assert_eq!(
10241 reported, key,
10242 "the rejection must name the key that is out of range"
10243 );
10244 }
10245 other => panic!("{key} out of range must be rejected, got {other:?}"),
10246 }
10247 }
10248
10249 let file_config: FileConfig =
10252 toml::from_str(&with_prefixes("32", "128")).expect("Could not parse TOML config");
10253 assert!(
10254 ConfigBuilder::new(file_config, "/tmp/test_config.toml")
10255 .into_config()
10256 .is_ok(),
10257 "the full-width prefixes are legal and are the defaults — they must load"
10258 );
10259 }
10260
10261 #[test]
10262 fn old_state_file_without_client_auth_fields_deserializes() {
10263 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
10268 let config = ListenerBuilder::new_https(address)
10269 .to_tls(None)
10270 .expect("default HTTPS listener config");
10271
10272 let mut json: serde_json::Value =
10273 serde_json::to_value(&config).expect("serialize HttpsListenerConfig");
10274 let obj = json.as_object_mut().expect("object");
10275 obj.remove("client_ca_certificates");
10276 obj.remove("client_ca_crls");
10277 obj.remove("client_auth");
10278
10279 let restored: HttpsListenerConfig =
10280 serde_json::from_value(json).expect("old state file must deserialize");
10281 assert!(restored.client_ca_certificates.is_empty());
10282 assert!(restored.client_ca_crls.is_empty());
10283 assert_eq!(restored.client_auth, None);
10284 }
10285
10286 #[test]
10287 fn client_auth_fields_rejected_on_non_https_listener() {
10288 let address = SocketAddress::new_v4(127, 0, 0, 1, 9000);
10291 let mut tcp = ListenerBuilder::new_tcp(address);
10292 tcp.client_auth = Some(ClientAuthConfig::Required);
10293 assert!(matches!(
10294 tcp.to_tcp(None),
10295 Err(ConfigError::ClientAuthOnNonHttps(_))
10296 ));
10297
10298 let mut http = ListenerBuilder::new_http(address);
10300 http.client_ca_certificates = Some(vec!["ca.pem".to_owned()]);
10301 assert!(matches!(
10302 http.to_http(None),
10303 Err(ConfigError::ClientAuthOnNonHttps(_))
10304 ));
10305 }
10306
10307 #[test]
10308 fn none_mode_ignores_stale_ca_paths() {
10309 let address = SocketAddress::new_v4(127, 0, 0, 1, 9443);
10312 let mut https = ListenerBuilder::new_https(address);
10313 https.client_auth = Some(ClientAuthConfig::None);
10314 https.client_ca_certificates = Some(vec!["/nonexistent/ca.pem".to_owned()]);
10315 https.client_ca_crls = Some(vec!["/nonexistent/crl.pem".to_owned()]);
10316 let config = https.to_tls(None).expect("NONE mode must ignore CA paths");
10317 assert!(config.client_ca_certificates.is_empty());
10318 assert!(config.client_ca_crls.is_empty());
10319 }
10320
10321 #[test]
10325 fn client_auth_listener_inlines_its_ca_and_crl_files_into_the_listener_request() {
10326 let asset =
10327 |name: &str| format!("{}/../lib/assets/mtls/{name}", env!("CARGO_MANIFEST_DIR"));
10328 let (ca_path, crl_path) = (asset("ca-cert.pem"), asset("crl-revoked.pem"));
10329 let toml_content = format!(
10330 r#"
10331 command_socket = "/tmp/sozu.sock"
10332 saved_state = "./state.json"
10333 worker_count = 1
10334
10335 [[listeners]]
10336 protocol = "https"
10337 address = "127.0.0.1:8443"
10338 client_auth = "required"
10339 client_ca_certificates = ["{ca_path}"]
10340 client_ca_crls = ["{crl_path}"]
10341 "#
10342 );
10343 let file_config: FileConfig =
10344 toml::from_str(&toml_content).expect("Could not parse TOML config");
10345 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
10346 .into_config()
10347 .expect("a client auth listener with readable CA and CRL files must load");
10348
10349 let listeners: Vec<HttpsListenerConfig> = config
10350 .generate_config_messages()
10351 .expect("the config must produce its worker requests")
10352 .into_iter()
10353 .filter_map(|request| match request.content.request_type {
10354 Some(RequestType::AddHttpsListenerWithClientAuth(listener)) => Some(listener),
10355 Some(RequestType::AddHttpsListener(_)) => {
10356 panic!("a client auth listener must not go out as AddHttpsListener")
10357 }
10358 _ => None,
10359 })
10360 .collect();
10361 assert_eq!(listeners.len(), 1, "one HTTPS listener is configured");
10362 let listener = &listeners[0];
10363 assert_eq!(
10364 listener.client_auth,
10365 Some(ClientAuthMode::ClientAuthRequired as i32)
10366 );
10367 assert_eq!(
10368 listener.client_ca_certificates,
10369 vec![std::fs::read_to_string(&ca_path).expect("read the CA fixture")],
10370 "the CA file content, not its path, must reach the worker"
10371 );
10372 assert_eq!(
10373 listener.client_ca_crls,
10374 vec![std::fs::read_to_string(&crl_path).expect("read the CRL fixture")],
10375 "the CRL file content, not its path, must reach the worker"
10376 );
10377 assert!(
10378 listener.client_ca_crls[0].contains("BEGIN X509 CRL"),
10379 "the inlined CRL is the PEM body"
10380 );
10381 }
10382
10383 #[test]
10384 fn client_auth_listener_with_an_unreadable_crl_file_does_not_load() {
10385 let address = SocketAddress::new_v4(127, 0, 0, 1, 9443);
10388 let mut https = ListenerBuilder::new_https(address);
10389 https.client_auth = Some(ClientAuthConfig::Required);
10390 https.client_ca_certificates = Some(vec![format!(
10391 "{}/../lib/assets/mtls/ca-cert.pem",
10392 env!("CARGO_MANIFEST_DIR")
10393 )]);
10394 https.client_ca_crls = Some(vec!["/nonexistent/crl.pem".to_owned()]);
10395 assert!(
10396 https.to_tls(None).is_err(),
10397 "an unreadable CRL file must fail the listener, not drop the revocation check"
10398 );
10399 }
10400
10401 #[test]
10402 fn client_auth_files_without_a_mode_are_rejected() {
10403 let address = SocketAddress::new_v4(127, 0, 0, 1, 9443);
10406 for (cas, crls) in [
10407 (Some(vec!["/etc/sozu/client-ca.pem".to_owned()]), None),
10408 (None, Some(vec!["/etc/sozu/client-ca.crl.pem".to_owned()])),
10409 ] {
10410 let mut https = ListenerBuilder::new_https(address);
10411 https.client_ca_certificates = cas;
10412 https.client_ca_crls = crls;
10413 assert!(matches!(
10414 https.to_tls(None),
10415 Err(ConfigError::ClientAuthModeMissing(_))
10416 ));
10417 }
10418 let mut explicit_none = ListenerBuilder::new_https(address);
10421 explicit_none.client_auth = Some(ClientAuthConfig::None);
10422 explicit_none.client_ca_certificates = Some(vec!["/nonexistent/ca.pem".to_owned()]);
10423 assert!(explicit_none.to_tls(None).is_ok());
10424 assert!(ListenerBuilder::new_https(address).to_tls(None).is_ok());
10425 }
10426
10427 #[test]
10428 fn client_auth_config_parses_documented_lowercase_names() {
10429 assert_eq!(
10432 serde_json::from_str::<ClientAuthConfig>("\"required\"").unwrap(),
10433 ClientAuthConfig::Required
10434 );
10435 assert_eq!(
10436 serde_json::from_str::<ClientAuthConfig>("\"optional\"").unwrap(),
10437 ClientAuthConfig::Optional
10438 );
10439 assert_eq!(
10440 serde_json::from_str::<ClientAuthConfig>("\"none\"").unwrap(),
10441 ClientAuthConfig::None
10442 );
10443 assert_eq!(
10444 ClientAuthMode::from(ClientAuthConfig::Required),
10445 ClientAuthMode::ClientAuthRequired
10446 );
10447 }
10448}