1use std::{
51 collections::{BTreeMap, HashMap, HashSet},
52 env, fmt,
53 fs::{File, create_dir_all, metadata},
54 io::{ErrorKind, Read},
55 net::SocketAddr,
56 ops::Range,
57 path::PathBuf,
58};
59
60use crate::{
61 ObjectKind,
62 certificate::split_certificate_chain,
63 logging::AccessLogFormat,
64 proto::command::{
65 ActivateListener, AddBackend, AddCertificate, CertificateAndKey, Cluster,
66 CustomHttpAnswers, Header, HeaderPosition, HealthCheckConfig, HstsConfig,
67 HttpListenerConfig, HttpsListenerConfig, ListenerType, LoadBalancingAlgorithms,
68 LoadBalancingParams, LoadMetric, MetricDetail, MetricsConfiguration, PathRule,
69 ProtobufAccessLogFormat, ProxyProtocolConfig, RedirectPolicy, RedirectScheme, Request,
70 RequestHttpFrontend, RequestTcpFrontend, RequestUdpFrontend, RulePosition, ServerConfig,
71 ServerMetricsConfig, SocketAddress, TcpListenerConfig, TlsVersion, UdpAffinityKey,
72 UdpClusterConfig, UdpHealthConfig, UdpHealthMode, UdpListenerConfig, WorkerRequest,
73 request::RequestType,
74 },
75};
76
77pub const DEFAULT_CIPHER_LIST: [&str; 9] = [
85 "TLS13_AES_256_GCM_SHA384",
87 "TLS13_AES_128_GCM_SHA256",
88 "TLS13_CHACHA20_POLY1305_SHA256",
89 "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
91 "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
92 "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
93 "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
94 "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
95 "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
96];
97
98pub const DEFAULT_SIGNATURE_ALGORITHMS: [&str; 9] = [
99 "ECDSA+SHA256",
100 "ECDSA+SHA384",
101 "ECDSA+SHA512",
102 "RSA+SHA256",
103 "RSA+SHA384",
104 "RSA+SHA512",
105 "RSA-PSS+SHA256",
106 "RSA-PSS+SHA384",
107 "RSA-PSS+SHA512",
108];
109
110pub const DEFAULT_GROUPS_LIST: [&str; 4] = ["X25519MLKEM768", "x25519", "P-256", "P-384"];
111
112pub const DEFAULT_ALPN_PROTOCOLS: [&str; 2] = ["h2", "http/1.1"];
115
116pub const DEFAULT_FRONT_TIMEOUT: u32 = 60;
118
119pub const DEFAULT_BACK_TIMEOUT: u32 = 30;
121
122pub const DEFAULT_CONNECT_TIMEOUT: u32 = 3;
124
125pub const DEFAULT_SNI_PREREAD_TIMEOUT: u32 = 5;
130
131pub const DEFAULT_SNI_PREREAD_MAX_BYTES: u32 = 16384;
139
140pub const MIN_SNI_PREREAD_MAX_BYTES: u32 = 5;
149
150pub const DEFAULT_REQUEST_TIMEOUT: u32 = 10;
152
153pub const DEFAULT_UDP_FRONT_TIMEOUT: u32 = 30;
155
156pub const DEFAULT_UDP_BACK_TIMEOUT: u32 = 30;
158
159pub const DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE: u32 = 1500;
162
163pub const DEFAULT_UDP_MAX_FLOWS: u32 = 0;
166
167pub const DEFAULT_WORKER_TIMEOUT: u32 = 10;
169
170pub const DEFAULT_STICKY_NAME: &str = "SOZUBALANCEID";
172
173pub const DEFAULT_ZOMBIE_CHECK_INTERVAL: u32 = 1_800;
175
176pub const DEFAULT_ACCEPT_QUEUE_TIMEOUT: u32 = 60;
178
179pub const DEFAULT_HSTS_MAX_AGE: u32 = 31_536_000;
186
187pub const DEFAULT_EVICT_ON_QUEUE_FULL: bool = false;
193
194pub const DEFAULT_WORKER_COUNT: u16 = 2;
196
197pub const DEFAULT_WORKER_AUTOMATIC_RESTART: bool = true;
199
200pub const DEFAULT_AUTOMATIC_STATE_SAVE: bool = false;
202
203pub const DEFAULT_MIN_BUFFERS: u64 = 1;
205
206pub const DEFAULT_MAX_BUFFERS: u64 = 1_000;
208
209pub const DEFAULT_BUFFER_SIZE: u64 = 16_393;
211
212pub const H2_MIN_BUFFER_SIZE: u64 = 16_393;
222
223pub const DEFAULT_MAX_CONNECTIONS: usize = 10_000;
225
226pub const DEFAULT_COMMAND_BUFFER_SIZE: u64 = 1_000_000;
228
229pub const DEFAULT_MAX_COMMAND_BUFFER_SIZE: u64 = 2_000_000;
231
232pub const DEFAULT_DISABLE_CLUSTER_METRICS: bool = false;
234
235pub const MAX_LOOP_ITERATIONS: usize = 100000;
236
237pub const DEFAULT_SEND_TLS_13_TICKETS: u64 = 4;
242
243pub const DEFAULT_LOG_TARGET: &str = "stdout";
245
246pub const DEFAULT_MAX_CONNECTIONS_PER_IP: u64 = 0;
251
252pub const DEFAULT_RETRY_AFTER: u32 = 60;
258
259#[derive(Debug)]
260pub enum IncompatibilityKind {
261 PublicAddress,
262 ProxyProtocol,
263}
264
265#[derive(Debug)]
266pub enum MissingKind {
267 Field(String),
268 Protocol,
269 SavedState,
270}
271
272#[derive(thiserror::Error, Debug)]
273pub enum ConfigError {
274 #[error("env path not found: {0}")]
275 Env(String),
276 #[error("Could not open file {path_to_open}: {io_error}")]
277 FileOpen {
278 path_to_open: String,
279 io_error: std::io::Error,
280 },
281 #[error("Could not read file {path_to_read}: {io_error}")]
282 FileRead {
283 path_to_read: String,
284 io_error: std::io::Error,
285 },
286 #[error(
287 "the field {kind:?} of {object:?} with id or address {id} is incompatible with the rest of the options"
288 )]
289 Incompatible {
290 kind: IncompatibilityKind,
291 object: ObjectKind,
292 id: String,
293 },
294 #[error("Invalid '{0}' field for a TCP frontend")]
295 InvalidFrontendConfig(String),
296 #[error("invalid path {0:?}")]
297 InvalidPath(PathBuf),
298 #[error("listening address {0:?} is already used in the configuration")]
299 ListenerAddressAlreadyInUse(SocketAddr),
300 #[error("missing {0:?}")]
301 Missing(MissingKind),
302 #[error("could not get parent directory for file {0}")]
303 NoFileParent(String),
304 #[error("Could not get the path of the saved state")]
305 SaveStatePath(String),
306 #[error("Can not determine path to sozu socket: {0}")]
307 SocketPathError(String),
308 #[error("toml decoding error: {0}")]
309 DeserializeToml(String),
310 #[error("Can not set this frontend on a {0:?} listener")]
311 WrongFrontendProtocol(ListenerProtocol),
312 #[error("Can not build a {expected:?} listener from a {found:?} config")]
313 WrongListenerProtocol {
314 expected: ListenerProtocol,
315 found: Option<ListenerProtocol>,
316 },
317 #[error("Invalid ALPN protocol '{0}'. Valid values: \"h2\", \"http/1.1\"")]
318 InvalidAlpnProtocol(String),
319 #[error(
325 "disable_http11 = true is incompatible with alpn_protocols containing \"http/1.1\" \
326 on listener {address}. The proxy would advertise http/1.1 then refuse every \
327 connection that negotiates it. Drop \"http/1.1\" from alpn_protocols or unset \
328 disable_http11."
329 )]
330 DisableHttp11WithHttp11Alpn { address: String },
331 #[error(
338 "buffer_size = {buffer_size} is below the H2 minimum of {minimum} but \
339 {listeners} HTTPS listener(s) advertise H2 ALPN. The H2 mux deadlocks \
340 on full-size frames with smaller buffers. Raise buffer_size to >= {minimum} \
341 or remove \"h2\" from those listeners' alpn_protocols."
342 )]
343 BufferSizeTooSmallForH2 {
344 buffer_size: u64,
345 minimum: u64,
346 listeners: usize,
347 },
348 #[error(
352 "invalid redirect policy '{0}'. Valid values: \"forward\", \"permanent\", \"unauthorized\""
353 )]
354 InvalidRedirectPolicy(String),
355 #[error(
359 "invalid redirect scheme '{0}'. Valid values: \"use-same\", \"use-http\", \"use-https\""
360 )]
361 InvalidRedirectScheme(String),
362 #[error(
366 "invalid header position '{position}' at headers[{index}]. Valid values: \"request\", \"response\", \"both\""
367 )]
368 InvalidHeaderPosition { index: usize, position: String },
369 #[error(
376 "invalid header bytes in {field} at headers[{index}]: control characters \
377 (NUL / CR / LF / other C0) are forbidden in header keys and values"
378 )]
379 InvalidHeaderBytes { index: usize, field: &'static str },
380 #[error("invalid HSTS config at {0}: `enabled` is required when an [hsts] block is present")]
386 HstsEnabledRequired(String),
387 #[error(
392 "invalid HSTS config at {0}: HSTS is only valid on HTTPS listeners and frontends \
393 (RFC 6797 §7.2 forbids the header over plaintext HTTP)"
394 )]
395 HstsOnPlainHttp(String),
396 #[error(
403 "invalid SNI pattern '{sni}' for a TCP frontend: expected an exact hostname or a \
404 single leading \"*.\" wildcard label (e.g. \"example.com\" or \"*.example.com\"); \
405 '/' and non-leading '*' are rejected"
406 )]
407 InvalidSniPattern { sni: String },
408 #[error(
415 "non-ASCII SNI pattern '{sni}' for a TCP frontend: on-wire SNI is always an ASCII \
416 A-label (RFC 6066), so this pattern would never match a ClientHello. Write the \
417 punycode A-label form instead (e.g. \"xn--mnchen-3ya.example\" for \
418 \"münchen.example\")"
419 )]
420 NonAsciiSniPattern { sni: String },
421 #[error(
430 "TCP frontend {address} sets alpn but no hostname (sni): alpn only matches within an \
431 SNI-scoped preread, so a frontend without hostname would silently ignore its alpn list. \
432 Set hostname or drop alpn."
433 )]
434 AlpnWithoutSni { address: SocketAddr },
435 #[error(
441 "TCP frontends on {address} with sni {sni:?} both match ALPN protocol '{protocol}': \
442 ALPN matchers for the same (address, sni) must not overlap"
443 )]
444 TcpFrontendAlpnOverlap {
445 address: SocketAddr,
446 sni: Option<String>,
447 protocol: String,
448 },
449 #[error(
454 "more than one TCP frontend on {address} with sni {sni:?} leaves alpn empty (the \
455 catch-all match): at most one frontend per (address, sni) may omit alpn"
456 )]
457 TcpFrontendMultipleAlpnCatchAll {
458 address: SocketAddr,
459 sni: Option<String>,
460 },
461 #[error(
468 "TCP listener {address} is targeted by both a no-SNI frontend and at least one \
469 SNI-scoped frontend: an SNI-enabled listener must not also have a raw-TCP fallback \
470 frontend on the same address"
471 )]
472 TcpListenerMixesSniAndNoSni { address: SocketAddr },
473 #[error(
479 "sni_preread_timeout = {sni_preread_timeout}s on TCP listener {address} exceeds its \
480 front_timeout = {front_timeout}s: the preread phase cannot outlive the frontend \
481 inactivity timeout that would already have closed the connection"
482 )]
483 SniPrereadTimeoutExceedsFrontTimeout {
484 address: SocketAddr,
485 sni_preread_timeout: u32,
486 front_timeout: u32,
487 },
488 #[error(
494 "sni_preread_max_bytes = {sni_preread_max_bytes} on TCP listener {address} exceeds \
495 buffer_size = {buffer_size}: raise buffer_size to >= {sni_preread_max_bytes} or lower \
496 sni_preread_max_bytes"
497 )]
498 SniPrereadMaxBytesExceedsBufferSize {
499 address: SocketAddr,
500 sni_preread_max_bytes: u32,
501 buffer_size: u64,
502 },
503 #[error(
513 "sni_preread_max_bytes = {sni_preread_max_bytes} on TCP listener {address} is below the \
514 minimum of {minimum} bytes (a full TLS record header): the preread shell could never \
515 read enough bytes to make progress. Raise sni_preread_max_bytes to >= {minimum}"
516 )]
517 SniPrereadMaxBytesTooSmall {
518 address: SocketAddr,
519 sni_preread_max_bytes: u32,
520 minimum: u32,
521 },
522}
523
524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
526#[serde(deny_unknown_fields)]
527pub struct ListenerBuilder {
528 pub address: SocketAddr,
529 pub protocol: Option<ListenerProtocol>,
530 pub public_address: Option<SocketAddr>,
531 pub answer_301: Option<String>,
532 pub answer_400: Option<String>,
533 pub answer_401: Option<String>,
534 pub answer_404: Option<String>,
535 pub answer_408: Option<String>,
536 pub answer_413: Option<String>,
537 pub answer_421: Option<String>,
540 pub answer_502: Option<String>,
541 pub answer_503: Option<String>,
542 pub answer_504: Option<String>,
543 pub answer_507: Option<String>,
544 pub answer_429: Option<String>,
549 pub tls_versions: Option<Vec<TlsVersion>>,
550 pub cipher_list: Option<Vec<String>>,
551 pub cipher_suites: Option<Vec<String>>,
552 pub groups_list: Option<Vec<String>>,
553 pub expect_proxy: Option<bool>,
554 #[serde(default = "default_sticky_name")]
555 pub sticky_name: String,
556 pub certificate: Option<String>,
557 pub certificate_chain: Option<String>,
558 pub key: Option<String>,
559 pub front_timeout: Option<u32>,
561 pub back_timeout: Option<u32>,
563 pub connect_timeout: Option<u32>,
565 pub request_timeout: Option<u32>,
567 pub config: Option<Config>,
569 pub send_tls13_tickets: Option<u64>,
573 pub alpn_protocols: Option<Vec<String>>,
576 pub h2_max_rst_stream_per_window: Option<u32>,
578 pub h2_max_ping_per_window: Option<u32>,
580 pub h2_max_settings_per_window: Option<u32>,
582 pub h2_max_empty_data_per_window: Option<u32>,
584 pub h2_max_window_update_stream0_per_window: Option<u32>,
588 pub sozu_id_header: Option<String>,
592 pub h2_max_continuation_frames: Option<u32>,
594 pub h2_max_glitch_count: Option<u32>,
596 pub h2_initial_connection_window: Option<u32>,
598 pub h2_max_concurrent_streams: Option<u32>,
600 pub h2_stream_shrink_ratio: Option<u32>,
602 pub h2_max_rst_stream_lifetime: Option<u64>,
605 pub h2_max_rst_stream_abusive_lifetime: Option<u64>,
608 pub h2_max_rst_stream_emitted_lifetime: Option<u64>,
612 pub h2_max_header_list_size: Option<u32>,
616 pub h2_max_header_table_size: Option<u32>,
620 pub h2_max_header_fields: Option<u32>,
624 pub h2_stream_idle_timeout_seconds: Option<u32>,
628 pub h2_graceful_shutdown_deadline_seconds: Option<u32>,
633 pub strict_sni_binding: Option<bool>,
639 pub disable_http11: Option<bool>,
644 pub elide_x_real_ip: Option<bool>,
648 pub send_x_real_ip: Option<bool>,
653 pub answers: Option<BTreeMap<String, String>>,
668 pub hsts: Option<FileHstsConfig>,
674 pub max_rx_datagram_size: Option<u32>,
678 pub max_flows: Option<u32>,
683 pub sni_preread_timeout: Option<u32>,
688 pub sni_preread_max_bytes: Option<u32>,
694}
695
696pub fn default_sticky_name() -> String {
697 DEFAULT_STICKY_NAME.to_string()
698}
699
700impl ListenerBuilder {
701 pub fn new_http(address: SocketAddress) -> ListenerBuilder {
704 Self::new(address, ListenerProtocol::Http)
705 }
706
707 pub fn new_tcp(address: SocketAddress) -> ListenerBuilder {
710 Self::new(address, ListenerProtocol::Tcp)
711 }
712
713 pub fn new_https(address: SocketAddress) -> ListenerBuilder {
716 Self::new(address, ListenerProtocol::Https)
717 }
718
719 pub fn new_udp(address: SocketAddress) -> ListenerBuilder {
722 Self::new(address, ListenerProtocol::Udp)
723 }
724
725 fn new(address: SocketAddress, protocol: ListenerProtocol) -> ListenerBuilder {
727 ListenerBuilder {
728 address: address.into(),
729 answer_301: None,
730 answer_401: None,
731 answer_400: None,
732 answer_404: None,
733 answer_408: None,
734 answer_413: None,
735 answer_421: None,
736 answer_502: None,
737 answer_503: None,
738 answer_504: None,
739 answer_507: None,
740 answer_429: None,
741 back_timeout: None,
742 certificate_chain: None,
743 certificate: None,
744 cipher_list: None,
745 cipher_suites: None,
746 groups_list: None,
747 config: None,
748 connect_timeout: None,
749 expect_proxy: None,
750 front_timeout: None,
751 key: None,
752 protocol: Some(protocol),
753 public_address: None,
754 request_timeout: None,
755 send_tls13_tickets: None,
756 sticky_name: DEFAULT_STICKY_NAME.to_string(),
757 tls_versions: None,
758 alpn_protocols: None,
759 h2_max_rst_stream_per_window: None,
760 h2_max_ping_per_window: None,
761 h2_max_settings_per_window: None,
762 h2_max_empty_data_per_window: None,
763 h2_max_window_update_stream0_per_window: None,
764 sozu_id_header: None,
765 h2_max_continuation_frames: None,
766 h2_max_glitch_count: None,
767 h2_initial_connection_window: None,
768 h2_max_concurrent_streams: None,
769 h2_stream_shrink_ratio: None,
770 h2_max_rst_stream_lifetime: None,
771 h2_max_rst_stream_abusive_lifetime: None,
772 h2_max_rst_stream_emitted_lifetime: None,
773 h2_max_header_list_size: None,
774 h2_max_header_table_size: None,
775 h2_max_header_fields: None,
776 h2_stream_idle_timeout_seconds: None,
777 h2_graceful_shutdown_deadline_seconds: None,
778 strict_sni_binding: None,
779 disable_http11: None,
780 elide_x_real_ip: None,
781 send_x_real_ip: None,
782 answers: None,
783 hsts: None,
784 max_rx_datagram_size: None,
785 max_flows: None,
786 sni_preread_timeout: None,
787 sni_preread_max_bytes: None,
788 }
789 }
790
791 pub fn with_public_address(&mut self, public_address: Option<SocketAddr>) -> &mut Self {
792 if let Some(address) = public_address {
793 self.public_address = Some(address);
794 }
795 self
796 }
797
798 pub fn with_answer_404_path<S>(&mut self, answer_404_path: Option<S>) -> &mut Self
799 where
800 S: ToString,
801 {
802 if let Some(path) = answer_404_path {
803 self.answer_404 = Some(path.to_string());
804 }
805 self
806 }
807
808 pub fn with_answer_503_path<S>(&mut self, answer_503_path: Option<S>) -> &mut Self
809 where
810 S: ToString,
811 {
812 if let Some(path) = answer_503_path {
813 self.answer_503 = Some(path.to_string());
814 }
815 self
816 }
817
818 pub fn with_tls_versions(&mut self, tls_versions: Vec<TlsVersion>) -> &mut Self {
819 self.tls_versions = Some(tls_versions);
820 self
821 }
822
823 pub fn with_cipher_list(&mut self, cipher_list: Option<Vec<String>>) -> &mut Self {
824 self.cipher_list = cipher_list;
825 self
826 }
827
828 pub fn with_cipher_suites(&mut self, cipher_suites: Option<Vec<String>>) -> &mut Self {
829 self.cipher_suites = cipher_suites;
830 self
831 }
832
833 pub fn with_alpn_protocols(&mut self, alpn_protocols: Option<Vec<String>>) -> &mut Self {
834 self.alpn_protocols = alpn_protocols;
835 self
836 }
837
838 pub fn with_elide_x_real_ip(&mut self, elide_x_real_ip: bool) -> &mut Self {
841 self.elide_x_real_ip = Some(elide_x_real_ip);
842 self
843 }
844
845 pub fn with_send_x_real_ip(&mut self, send_x_real_ip: bool) -> &mut Self {
849 self.send_x_real_ip = Some(send_x_real_ip);
850 self
851 }
852
853 pub fn with_expect_proxy(&mut self, expect_proxy: bool) -> &mut Self {
854 self.expect_proxy = Some(expect_proxy);
855 self
856 }
857
858 pub fn with_sticky_name<S>(&mut self, sticky_name: Option<S>) -> &mut Self
859 where
860 S: ToString,
861 {
862 if let Some(name) = sticky_name {
863 self.sticky_name = name.to_string();
864 }
865 self
866 }
867
868 pub fn with_certificate<S>(&mut self, certificate: S) -> &mut Self
869 where
870 S: ToString,
871 {
872 self.certificate = Some(certificate.to_string());
873 self
874 }
875
876 pub fn with_certificate_chain(&mut self, certificate_chain: String) -> &mut Self {
877 self.certificate = Some(certificate_chain);
878 self
879 }
880
881 pub fn with_key<S>(&mut self, key: String) -> &mut Self
882 where
883 S: ToString,
884 {
885 self.key = Some(key);
886 self
887 }
888
889 pub fn with_front_timeout(&mut self, front_timeout: Option<u32>) -> &mut Self {
890 self.front_timeout = front_timeout;
891 self
892 }
893
894 pub fn with_back_timeout(&mut self, back_timeout: Option<u32>) -> &mut Self {
895 self.back_timeout = back_timeout;
896 self
897 }
898
899 pub fn with_connect_timeout(&mut self, connect_timeout: Option<u32>) -> &mut Self {
900 self.connect_timeout = connect_timeout;
901 self
902 }
903
904 pub fn with_request_timeout(&mut self, request_timeout: Option<u32>) -> &mut Self {
905 self.request_timeout = request_timeout;
906 self
907 }
908
909 pub fn with_answer<S, P>(&mut self, code: S, path: P) -> &mut Self
914 where
915 S: ToString,
916 P: ToString,
917 {
918 self.answers
919 .get_or_insert_with(BTreeMap::new)
920 .insert(code.to_string(), path.to_string());
921 self
922 }
923
924 pub fn with_answers(&mut self, answers: BTreeMap<String, String>) -> &mut Self {
927 self.answers = Some(answers);
928 self
929 }
930
931 fn get_http_answers(&self) -> Result<Option<CustomHttpAnswers>, ConfigError> {
933 let http_answers = CustomHttpAnswers {
934 answer_301: read_http_answer_file(&self.answer_301)?,
935 answer_400: read_http_answer_file(&self.answer_400)?,
936 answer_401: read_http_answer_file(&self.answer_401)?,
937 answer_404: read_http_answer_file(&self.answer_404)?,
938 answer_408: read_http_answer_file(&self.answer_408)?,
939 answer_413: read_http_answer_file(&self.answer_413)?,
940 answer_421: read_http_answer_file(&self.answer_421)?,
941 answer_502: read_http_answer_file(&self.answer_502)?,
942 answer_503: read_http_answer_file(&self.answer_503)?,
943 answer_504: read_http_answer_file(&self.answer_504)?,
944 answer_507: read_http_answer_file(&self.answer_507)?,
945 answer_429: read_http_answer_file(&self.answer_429)?,
946 };
947 Ok(Some(http_answers))
948 }
949
950 fn get_listener_answers(&self) -> Result<BTreeMap<String, String>, ConfigError> {
958 let mut out = BTreeMap::new();
959
960 macro_rules! merge_legacy {
964 ($code:literal, $field:ident) => {
965 if let Some(body) = read_http_answer_file(&self.$field)? {
966 out.insert($code.to_owned(), body);
967 }
968 };
969 }
970 merge_legacy!("301", answer_301);
971 merge_legacy!("400", answer_400);
972 merge_legacy!("401", answer_401);
973 merge_legacy!("404", answer_404);
974 merge_legacy!("408", answer_408);
975 merge_legacy!("413", answer_413);
976 merge_legacy!("421", answer_421);
977 merge_legacy!("502", answer_502);
978 merge_legacy!("503", answer_503);
979 merge_legacy!("504", answer_504);
980 merge_legacy!("507", answer_507);
981 merge_legacy!("429", answer_429);
982
983 if let Some(map) = &self.answers {
984 let loaded = load_answers(map)?;
985 out.extend(loaded);
986 }
987 Ok(out)
988 }
989
990 fn assign_config_timeouts(&mut self, config: &Config) {
992 self.front_timeout = Some(self.front_timeout.unwrap_or(config.front_timeout));
993 self.back_timeout = Some(self.back_timeout.unwrap_or(config.back_timeout));
994 self.connect_timeout = Some(self.connect_timeout.unwrap_or(config.connect_timeout));
995 self.request_timeout = Some(self.request_timeout.unwrap_or(config.request_timeout));
996 }
997
998 pub fn to_http(&mut self, config: Option<&Config>) -> Result<HttpListenerConfig, ConfigError> {
1000 if self.protocol != Some(ListenerProtocol::Http) {
1001 return Err(ConfigError::WrongListenerProtocol {
1002 expected: ListenerProtocol::Http,
1003 found: self.protocol.to_owned(),
1004 });
1005 }
1006
1007 if self.hsts.is_some() {
1013 return Err(ConfigError::HstsOnPlainHttp(format!(
1014 "HTTP listener {}",
1015 self.address
1016 )));
1017 }
1018
1019 if let Some(config) = config {
1020 self.assign_config_timeouts(config);
1021 }
1022
1023 let http_answers = self.get_http_answers()?;
1024 let answers = self.get_listener_answers()?;
1025
1026 let configuration = HttpListenerConfig {
1027 address: self.address.into(),
1028 public_address: self.public_address.map(|a| a.into()),
1029 expect_proxy: self.expect_proxy.unwrap_or(false),
1030 sticky_name: self.sticky_name.clone(),
1031 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1032 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1033 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1034 request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1035 http_answers,
1036 answers,
1037 h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1038 h2_max_ping_per_window: self.h2_max_ping_per_window,
1039 h2_max_settings_per_window: self.h2_max_settings_per_window,
1040 h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1041 h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1042 h2_max_continuation_frames: self.h2_max_continuation_frames,
1043 h2_max_glitch_count: self.h2_max_glitch_count,
1044 h2_initial_connection_window: self.h2_initial_connection_window,
1045 h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1046 h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1047 h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1048 h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1049 h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1050 h2_max_header_list_size: self.h2_max_header_list_size,
1051 h2_max_header_table_size: self.h2_max_header_table_size,
1052 h2_max_header_fields: self.h2_max_header_fields,
1053 h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1054 h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1055 sozu_id_header: self.sozu_id_header.clone(),
1056 elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1057 send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1058 ..Default::default()
1059 };
1060
1061 debug_assert_eq!(
1066 configuration.address,
1067 self.address.into(),
1068 "HTTP listener must bind the requested address"
1069 );
1070 Ok(configuration)
1071 }
1072
1073 pub fn to_tls(&mut self, config: Option<&Config>) -> Result<HttpsListenerConfig, ConfigError> {
1075 if self.protocol != Some(ListenerProtocol::Https) {
1076 return Err(ConfigError::WrongListenerProtocol {
1077 expected: ListenerProtocol::Https,
1078 found: self.protocol.to_owned(),
1079 });
1080 }
1081
1082 let default_cipher_list = DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect();
1083
1084 let cipher_list = self.cipher_list.clone().unwrap_or(default_cipher_list);
1085
1086 let cipher_suites = self
1087 .cipher_suites
1088 .clone()
1089 .unwrap_or_else(|| DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect());
1090
1091 let signature_algorithms: Vec<String> = DEFAULT_SIGNATURE_ALGORITHMS
1092 .into_iter()
1093 .map(String::from)
1094 .collect();
1095
1096 let groups_list = self
1097 .groups_list
1098 .clone()
1099 .unwrap_or_else(|| DEFAULT_GROUPS_LIST.into_iter().map(String::from).collect());
1100
1101 let alpn_protocols: Vec<String> = match &self.alpn_protocols {
1102 Some(protos) if !protos.is_empty() => {
1103 for proto in protos {
1104 match proto.as_str() {
1105 "h2" | "http/1.1" => {}
1106 other => return Err(ConfigError::InvalidAlpnProtocol(other.to_owned())),
1107 }
1108 }
1109 if self.disable_http11.unwrap_or(false) && protos.iter().any(|p| p == "http/1.1") {
1114 return Err(ConfigError::DisableHttp11WithHttp11Alpn {
1115 address: self.address.to_string(),
1116 });
1117 }
1118 if !protos.iter().any(|p| p == "http/1.1") {
1119 warn!(
1120 "ALPN protocols do not include 'http/1.1'. Clients without H2 support will fail TLS negotiation."
1121 );
1122 }
1123 let mut seen = std::collections::HashSet::new();
1125 protos
1126 .iter()
1127 .filter(|p| seen.insert(p.as_str()))
1128 .cloned()
1129 .collect()
1130 }
1131 _ => {
1132 if self.disable_http11.unwrap_or(false)
1136 && DEFAULT_ALPN_PROTOCOLS.contains(&"http/1.1")
1137 {
1138 return Err(ConfigError::DisableHttp11WithHttp11Alpn {
1139 address: self.address.to_string(),
1140 });
1141 }
1142 DEFAULT_ALPN_PROTOCOLS
1143 .iter()
1144 .map(|s| s.to_string())
1145 .collect()
1146 }
1147 };
1148
1149 let versions = match self.tls_versions {
1150 None => vec![TlsVersion::TlsV12 as i32, TlsVersion::TlsV13 as i32],
1151 Some(ref v) => v.iter().map(|v| *v as i32).collect(),
1152 };
1153
1154 let key = self.key.as_ref().and_then(|path| {
1155 Config::load_file(path)
1156 .map_err(|e| {
1157 error!("cannot load key at path '{}': {:?}", path, e);
1158 e
1159 })
1160 .ok()
1161 });
1162 let certificate = self.certificate.as_ref().and_then(|path| {
1163 Config::load_file(path)
1164 .map_err(|e| {
1165 error!("cannot load certificate at path '{}': {:?}", path, e);
1166 e
1167 })
1168 .ok()
1169 });
1170 let certificate_chain = self
1171 .certificate_chain
1172 .as_ref()
1173 .and_then(|path| {
1174 Config::load_file(path)
1175 .map_err(|e| {
1176 error!("cannot load certificate chain at path '{}': {:?}", path, e);
1177 e
1178 })
1179 .ok()
1180 })
1181 .map(split_certificate_chain)
1182 .unwrap_or_default();
1183
1184 let http_answers = self.get_http_answers()?;
1185 let answers = self.get_listener_answers()?;
1186
1187 if let Some(config) = config {
1188 self.assign_config_timeouts(config);
1189 }
1190
1191 let https_listener_config = HttpsListenerConfig {
1192 address: self.address.into(),
1193 sticky_name: self.sticky_name.clone(),
1194 public_address: self.public_address.map(|a| a.into()),
1195 cipher_list,
1196 versions,
1197 expect_proxy: self.expect_proxy.unwrap_or(false),
1198 key,
1199 certificate,
1200 certificate_chain,
1201 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1202 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1203 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1204 request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1205 cipher_suites,
1206 signature_algorithms,
1207 groups_list,
1208 active: false,
1209 send_tls13_tickets: self
1210 .send_tls13_tickets
1211 .unwrap_or(DEFAULT_SEND_TLS_13_TICKETS),
1212 http_answers,
1213 answers,
1214 alpn_protocols,
1215 h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1216 h2_max_ping_per_window: self.h2_max_ping_per_window,
1217 h2_max_settings_per_window: self.h2_max_settings_per_window,
1218 h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1219 h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1220 h2_max_continuation_frames: self.h2_max_continuation_frames,
1221 h2_max_glitch_count: self.h2_max_glitch_count,
1222 h2_initial_connection_window: self.h2_initial_connection_window,
1223 h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1224 h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1225 h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1226 h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1227 h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1228 h2_max_header_list_size: self.h2_max_header_list_size,
1229 h2_max_header_table_size: self.h2_max_header_table_size,
1230 h2_max_header_fields: self.h2_max_header_fields,
1231 strict_sni_binding: self.strict_sni_binding,
1232 disable_http11: self.disable_http11,
1233 h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1234 h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1235 sozu_id_header: self.sozu_id_header.clone(),
1236 elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1237 send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1238 hsts: match self.hsts.as_ref() {
1239 Some(h) => Some(h.to_proto("listener")?),
1240 None => None,
1241 },
1242 };
1243
1244 debug_assert_eq!(
1248 https_listener_config.address,
1249 self.address.into(),
1250 "HTTPS listener must bind the requested address"
1251 );
1252 debug_assert!(
1253 !https_listener_config.active,
1254 "a freshly built HTTPS listener must start inactive"
1255 );
1256 debug_assert!(
1261 !https_listener_config.alpn_protocols.is_empty(),
1262 "resolved ALPN list must not be empty"
1263 );
1264 debug_assert!(
1265 https_listener_config
1266 .alpn_protocols
1267 .iter()
1268 .all(|p| p == "h2" || p == "http/1.1"),
1269 "resolved ALPN list must contain only h2 and http/1.1"
1270 );
1271 debug_assert!(
1272 {
1273 let mut seen = std::collections::HashSet::new();
1274 https_listener_config
1275 .alpn_protocols
1276 .iter()
1277 .all(|p| seen.insert(p))
1278 },
1279 "resolved ALPN list must be duplicate-free"
1280 );
1281 debug_assert!(
1284 !(self.disable_http11.unwrap_or(false)
1285 && https_listener_config
1286 .alpn_protocols
1287 .iter()
1288 .any(|p| p == "http/1.1")),
1289 "disable_http11 with http/1.1 in ALPN must have been rejected"
1290 );
1291 Ok(https_listener_config)
1292 }
1293
1294 pub fn to_tcp(&mut self, config: Option<&Config>) -> Result<TcpListenerConfig, ConfigError> {
1296 if self.protocol != Some(ListenerProtocol::Tcp) {
1297 return Err(ConfigError::WrongListenerProtocol {
1298 expected: ListenerProtocol::Tcp,
1299 found: self.protocol.to_owned(),
1300 });
1301 }
1302
1303 if let Some(config) = config {
1304 self.assign_config_timeouts(config);
1305 }
1306
1307 let tcp_listener_config = TcpListenerConfig {
1308 address: self.address.into(),
1309 public_address: self.public_address.map(|a| a.into()),
1310 expect_proxy: self.expect_proxy.unwrap_or(false),
1311 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1312 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1313 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1314 active: false,
1315 sni_preread_timeout: Some(
1316 self.sni_preread_timeout
1317 .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT),
1318 ),
1319 sni_preread_max_bytes: Some(
1320 self.sni_preread_max_bytes
1321 .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES),
1322 ),
1323 };
1324
1325 debug_assert_eq!(
1328 tcp_listener_config.address,
1329 self.address.into(),
1330 "TCP listener must bind the requested address"
1331 );
1332 debug_assert!(
1333 !tcp_listener_config.active,
1334 "a freshly built TCP listener must start inactive"
1335 );
1336 Ok(tcp_listener_config)
1337 }
1338
1339 pub fn to_udp(&mut self, config: Option<&Config>) -> Result<UdpListenerConfig, ConfigError> {
1357 if self.protocol != Some(ListenerProtocol::Udp) {
1358 return Err(ConfigError::WrongListenerProtocol {
1359 expected: ListenerProtocol::Udp,
1360 found: self.protocol.to_owned(),
1361 });
1362 }
1363
1364 let mut max_rx_datagram_size = self
1365 .max_rx_datagram_size
1366 .unwrap_or(DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE);
1367 let buffer_size = config.map(|c| c.buffer_size).unwrap_or(DEFAULT_BUFFER_SIZE);
1368 if u64::from(max_rx_datagram_size) > buffer_size {
1369 warn!(
1370 "UDP listener {}: max_rx_datagram_size = {} exceeds buffer_size = {}, clamping to buffer_size",
1371 self.address, max_rx_datagram_size, buffer_size
1372 );
1373 max_rx_datagram_size = buffer_size as u32;
1374 }
1375
1376 let max_flows = self.max_flows.unwrap_or(DEFAULT_UDP_MAX_FLOWS);
1377 if max_flows > 0
1378 && let Some(soft_limit) = soft_rlimit_nofile()
1379 {
1380 let advisory = soft_limit.saturating_mul(7) / 10;
1381 if u64::from(max_flows) > advisory {
1382 warn!(
1383 "UDP listener {}: max_flows = {} exceeds ~70% of the soft RLIMIT_NOFILE ({}); \
1384 per-flow connected sockets may hit EMFILE",
1385 self.address, max_flows, advisory
1386 );
1387 }
1388 }
1389
1390 Ok(UdpListenerConfig {
1391 address: self.address.into(),
1392 public_address: self.public_address.map(|a| a.into()),
1393 front_timeout: self.front_timeout.unwrap_or(DEFAULT_UDP_FRONT_TIMEOUT),
1394 back_timeout: self.back_timeout.unwrap_or(DEFAULT_UDP_BACK_TIMEOUT),
1395 max_rx_datagram_size,
1396 max_flows,
1397 active: false,
1398 })
1399 }
1400}
1401
1402fn soft_rlimit_nofile() -> Option<u64> {
1407 let mut limit = libc::rlimit {
1408 rlim_cur: 0,
1409 rlim_max: 0,
1410 };
1411 let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) };
1415 if rc == 0 {
1416 Some(limit.rlim_cur)
1419 } else {
1420 None
1421 }
1422}
1423
1424fn read_http_answer_file(path: &Option<String>) -> Result<Option<String>, ConfigError> {
1426 match path {
1427 Some(path) => {
1428 let mut content = String::new();
1429 let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
1430 path_to_open: path.to_owned(),
1431 io_error,
1432 })?;
1433
1434 file.read_to_string(&mut content)
1435 .map_err(|io_error| ConfigError::FileRead {
1436 path_to_read: path.to_owned(),
1437 io_error,
1438 })?;
1439
1440 Ok(Some(content))
1441 }
1442 None => Ok(None),
1443 }
1444}
1445
1446pub fn resolve_answer_source(value: &str) -> Result<String, ConfigError> {
1469 if let Some(path) = value.strip_prefix("file://") {
1470 let mut content = String::new();
1471 let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
1472 path_to_open: path.to_owned(),
1473 io_error,
1474 })?;
1475 file.read_to_string(&mut content)
1476 .map_err(|io_error| ConfigError::FileRead {
1477 path_to_read: path.to_owned(),
1478 io_error,
1479 })?;
1480 return Ok(content);
1481 }
1482 Ok(value.to_owned())
1483}
1484
1485pub fn load_answers(
1502 answers: &BTreeMap<String, String>,
1503) -> Result<BTreeMap<String, String>, ConfigError> {
1504 let mut out = BTreeMap::new();
1505 for (code, value) in answers {
1506 if value.is_empty() {
1507 continue;
1508 }
1509 out.insert(code.to_owned(), resolve_answer_source(value)?);
1510 }
1511 debug_assert!(
1515 out.len() <= answers.len(),
1516 "load_answers must not synthesize entries"
1517 );
1518 debug_assert!(
1519 out.keys().all(|k| answers.contains_key(k)),
1520 "every loaded status code must come from the input map"
1521 );
1522 Ok(out)
1523}
1524
1525#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
1538#[serde(rename_all = "lowercase")]
1539#[derive(Default)]
1540pub enum MetricDetailLevel {
1541 Process,
1542 Frontend,
1543 #[default]
1544 Cluster,
1545 Backend,
1546}
1547
1548impl From<MetricDetailLevel> for MetricDetail {
1549 fn from(level: MetricDetailLevel) -> Self {
1550 match level {
1551 MetricDetailLevel::Process => MetricDetail::DetailProcess,
1552 MetricDetailLevel::Frontend => MetricDetail::DetailFrontend,
1553 MetricDetailLevel::Cluster => MetricDetail::DetailCluster,
1554 MetricDetailLevel::Backend => MetricDetail::DetailBackend,
1555 }
1556 }
1557}
1558
1559impl From<MetricDetail> for MetricDetailLevel {
1560 fn from(detail: MetricDetail) -> Self {
1564 match detail {
1565 MetricDetail::DetailProcess => MetricDetailLevel::Process,
1566 MetricDetail::DetailFrontend => MetricDetailLevel::Frontend,
1567 MetricDetail::DetailCluster => MetricDetailLevel::Cluster,
1568 MetricDetail::DetailBackend => MetricDetailLevel::Backend,
1569 }
1570 }
1571}
1572
1573#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1574#[serde(deny_unknown_fields)]
1575pub struct MetricsConfig {
1576 pub address: SocketAddr,
1577 #[serde(default)]
1578 pub tagged_metrics: bool,
1579 #[serde(default)]
1580 pub prefix: Option<String>,
1581 #[serde(default)]
1584 pub detail: MetricDetailLevel,
1585}
1586
1587#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1588#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1589#[serde(deny_unknown_fields)]
1590pub enum PathRuleType {
1591 Prefix,
1592 Regex,
1593 Equals,
1594}
1595
1596#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1597#[serde(deny_unknown_fields)]
1598pub struct FileClusterFrontendConfig {
1599 pub address: SocketAddr,
1600 pub hostname: Option<String>,
1601 #[serde(default)]
1609 pub alpn: Vec<String>,
1610 pub path: Option<String>,
1612 pub path_type: Option<PathRuleType>,
1614 pub method: Option<String>,
1615 pub certificate: Option<String>,
1616 pub key: Option<String>,
1617 pub certificate_chain: Option<String>,
1618 #[serde(default)]
1619 pub tls_versions: Vec<TlsVersion>,
1620 #[serde(default)]
1621 pub position: RulePosition,
1622 pub tags: Option<BTreeMap<String, String>>,
1623 pub redirect: Option<String>,
1628 pub redirect_scheme: Option<String>,
1632 pub redirect_template: Option<String>,
1636 pub rewrite_host: Option<String>,
1639 pub rewrite_path: Option<String>,
1641 pub rewrite_port: Option<u32>,
1643 pub required_auth: Option<bool>,
1647 pub headers: Option<Vec<HeaderEditConfig>>,
1651 pub hsts: Option<FileHstsConfig>,
1656}
1657
1658#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1665#[serde(deny_unknown_fields)]
1666pub struct HeaderEditConfig {
1667 pub position: String,
1668 pub key: String,
1669 pub value: String,
1670}
1671
1672#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1683#[serde(deny_unknown_fields)]
1684pub struct FileHstsConfig {
1685 pub enabled: Option<bool>,
1688 pub max_age: Option<u32>,
1693 pub include_subdomains: Option<bool>,
1695 pub preload: Option<bool>,
1698 pub force_replace_backend: Option<bool>,
1706}
1707
1708impl FileHstsConfig {
1709 pub fn to_proto(&self, scope: &str) -> Result<HstsConfig, ConfigError> {
1727 let enabled = match self.enabled {
1728 Some(v) => v,
1729 None => return Err(ConfigError::HstsEnabledRequired(scope.to_owned())),
1730 };
1731
1732 let max_age = match (enabled, self.max_age) {
1733 (true, None) => Some(DEFAULT_HSTS_MAX_AGE),
1734 (_, m) => m,
1735 };
1736
1737 if let Some(value) = max_age
1738 && value > 0
1739 && value < 86_400
1740 {
1741 warn!(
1742 "HSTS max_age = {}s on {} is below 1 day — this is almost certainly a \
1743 misconfiguration. RFC 6797 §11.4 reserves max_age = 0 as the explicit kill \
1744 switch.",
1745 value, scope
1746 );
1747 }
1748
1749 let include_subdomains = self.include_subdomains;
1750 let preload = self.preload;
1751
1752 if matches!(preload, Some(true)) {
1753 let max_age_value = max_age.unwrap_or(0);
1754 if max_age_value < DEFAULT_HSTS_MAX_AGE {
1755 warn!(
1756 "HSTS preload = true on {} with max_age = {}s; the Chrome HSTS preload \
1757 list requires max_age >= {} (https://hstspreload.org/).",
1758 scope, max_age_value, DEFAULT_HSTS_MAX_AGE
1759 );
1760 }
1761 if include_subdomains != Some(true) {
1762 warn!(
1763 "HSTS preload = true on {} without include_subdomains = true; the Chrome \
1764 HSTS preload list requires includeSubDomains \
1765 (https://hstspreload.org/).",
1766 scope
1767 );
1768 }
1769 }
1770
1771 let config = HstsConfig {
1772 enabled: Some(enabled),
1773 max_age,
1774 include_subdomains,
1775 preload,
1776 force_replace_backend: self.force_replace_backend,
1777 };
1778
1779 debug_assert_eq!(
1784 config.enabled,
1785 Some(enabled),
1786 "built HSTS config must record the resolved enabled flag"
1787 );
1788 debug_assert!(
1789 !enabled || config.max_age.is_some(),
1790 "an enabled HSTS policy must carry a max_age"
1791 );
1792 Ok(config)
1793 }
1794}
1795
1796impl FileClusterFrontendConfig {
1797 pub fn to_tcp_front(&self) -> Result<TcpFrontendConfig, ConfigError> {
1798 if self.path.is_some() {
1799 return Err(ConfigError::InvalidFrontendConfig(
1800 "path_prefix".to_string(),
1801 ));
1802 }
1803 if self.certificate.is_some() {
1804 return Err(ConfigError::InvalidFrontendConfig(
1805 "certificate".to_string(),
1806 ));
1807 }
1808 if self.certificate_chain.is_some() {
1809 return Err(ConfigError::InvalidFrontendConfig(
1810 "certificate_chain".to_string(),
1811 ));
1812 }
1813
1814 let sni = match &self.hostname {
1819 Some(hostname) => Some(validate_sni_pattern(hostname)?),
1820 None => None,
1821 };
1822
1823 if sni.is_none() && !self.alpn.is_empty() {
1830 return Err(ConfigError::AlpnWithoutSni {
1831 address: self.address,
1832 });
1833 }
1834
1835 let tcp_front = TcpFrontendConfig {
1836 address: self.address,
1837 tags: self.tags.clone(),
1838 sni,
1839 alpn: self.alpn.clone(),
1840 udp: false,
1843 };
1844 debug_assert_eq!(
1851 tcp_front.address, self.address,
1852 "TCP frontend must bind the requested address"
1853 );
1854 debug_assert!(
1855 self.path.is_none() && self.certificate.is_none() && self.certificate_chain.is_none(),
1856 "a built TCP frontend must carry no HTTP-only attributes"
1857 );
1858 debug_assert!(
1859 tcp_front.sni.is_some() || tcp_front.alpn.is_empty(),
1860 "a built TCP frontend without sni must never carry a non-empty alpn"
1861 );
1862 Ok(tcp_front)
1863 }
1864
1865 pub fn to_http_front(&self, _cluster_id: &str) -> Result<HttpFrontendConfig, ConfigError> {
1866 if !self.alpn.is_empty() {
1867 return Err(ConfigError::InvalidFrontendConfig("alpn".to_string()));
1868 }
1869
1870 let hostname = match &self.hostname {
1871 Some(hostname) => hostname.to_owned(),
1872 None => {
1873 return Err(ConfigError::Missing(MissingKind::Field(
1874 "hostname".to_string(),
1875 )));
1876 }
1877 };
1878
1879 let key_opt = match self.key.as_ref() {
1880 None => None,
1881 Some(path) => {
1882 let key = Config::load_file(path)?;
1883 Some(key)
1884 }
1885 };
1886
1887 let certificate_opt = match self.certificate.as_ref() {
1888 None => None,
1889 Some(path) => {
1890 let certificate = Config::load_file(path)?;
1891 Some(certificate)
1892 }
1893 };
1894
1895 let certificate_chain = match self.certificate_chain.as_ref() {
1896 None => None,
1897 Some(path) => {
1898 let certificate_chain = Config::load_file(path)?;
1899 Some(split_certificate_chain(certificate_chain))
1900 }
1901 };
1902
1903 let path = match (self.path.as_ref(), self.path_type.as_ref()) {
1904 (None, _) => PathRule::prefix("".to_string()),
1905 (Some(s), Some(PathRuleType::Prefix)) => PathRule::prefix(s.to_string()),
1906 (Some(s), Some(PathRuleType::Regex)) => PathRule::regex(s.to_string()),
1907 (Some(s), Some(PathRuleType::Equals)) => PathRule::equals(s.to_string()),
1908 (Some(s), None) => PathRule::prefix(s.clone()),
1909 };
1910
1911 let redirect = match self.redirect.as_deref() {
1912 Some(v) => Some(parse_redirect_policy(v)?),
1913 None => None,
1914 };
1915 let redirect_scheme = match self.redirect_scheme.as_deref() {
1916 Some(v) => Some(parse_redirect_scheme(v)?),
1917 None => None,
1918 };
1919
1920 let headers = match self.headers.as_ref() {
1921 Some(entries) => {
1922 let mut out = Vec::with_capacity(entries.len());
1923 for (index, entry) in entries.iter().enumerate() {
1924 out.push(parse_header_edit(index, entry)?);
1925 }
1926 out
1927 }
1928 None => Vec::new(),
1929 };
1930
1931 let frontend_serves_https = key_opt.is_some() && certificate_opt.is_some();
1938 let hsts = match self.hsts.as_ref() {
1939 Some(h) => {
1940 if !frontend_serves_https {
1941 return Err(ConfigError::HstsOnPlainHttp(format!(
1942 "frontend {_cluster_id}/{hostname}"
1943 )));
1944 }
1945 Some(h.to_proto(&format!("frontend {_cluster_id}/{hostname}"))?)
1946 }
1947 None => None,
1948 };
1949
1950 Ok(HttpFrontendConfig {
1951 address: self.address,
1952 hostname,
1953 certificate: certificate_opt,
1954 key: key_opt,
1955 certificate_chain,
1956 tls_versions: self.tls_versions.clone(),
1957 position: self.position,
1958 path,
1959 method: self.method.clone(),
1960 tags: self.tags.clone(),
1961 redirect,
1962 redirect_scheme,
1963 redirect_template: self.redirect_template.clone(),
1964 rewrite_host: self.rewrite_host.clone(),
1965 rewrite_path: self.rewrite_path.clone(),
1966 rewrite_port: self.rewrite_port,
1967 required_auth: self.required_auth,
1968 headers,
1969 hsts,
1970 })
1971 }
1972}
1973
1974pub fn validate_sni_pattern(sni: &str) -> Result<String, ConfigError> {
1995 let invalid = || ConfigError::InvalidSniPattern {
1996 sni: sni.to_string(),
1997 };
1998
1999 if sni.is_empty() {
2000 return Err(invalid());
2001 }
2002
2003 if !sni.is_ascii() {
2004 return Err(ConfigError::NonAsciiSniPattern {
2005 sni: sni.to_string(),
2006 });
2007 }
2008
2009 let remainder = sni.strip_prefix("*.").unwrap_or(sni);
2013
2014 if remainder.is_empty() || remainder.contains('*') || remainder.contains('/') {
2021 return Err(invalid());
2022 }
2023 if remainder.split('.').any(|label| label.is_empty()) {
2024 return Err(invalid());
2025 }
2026
2027 let normalized = sni.to_ascii_lowercase();
2028 debug_assert!(
2034 normalized.matches('*').count() <= 1,
2035 "a validated SNI pattern must carry at most one wildcard marker"
2036 );
2037 debug_assert!(
2038 normalized.is_ascii(),
2039 "a validated SNI pattern must be pure ASCII"
2040 );
2041 Ok(normalized)
2042}
2043
2044pub(crate) fn parse_redirect_policy(value: &str) -> Result<RedirectPolicy, ConfigError> {
2046 match value.to_ascii_lowercase().as_str() {
2047 "forward" => Ok(RedirectPolicy::Forward),
2048 "permanent" => Ok(RedirectPolicy::Permanent),
2049 "unauthorized" => Ok(RedirectPolicy::Unauthorized),
2050 _ => Err(ConfigError::InvalidRedirectPolicy(value.to_owned())),
2051 }
2052}
2053
2054pub(crate) fn parse_redirect_scheme(value: &str) -> Result<RedirectScheme, ConfigError> {
2056 match value.to_ascii_lowercase().as_str() {
2057 "use-same" | "use_same" => Ok(RedirectScheme::UseSame),
2058 "use-http" | "use_http" => Ok(RedirectScheme::UseHttp),
2059 "use-https" | "use_https" => Ok(RedirectScheme::UseHttps),
2060 _ => Err(ConfigError::InvalidRedirectScheme(value.to_owned())),
2061 }
2062}
2063
2064pub(crate) fn parse_header_edit(
2071 index: usize,
2072 entry: &HeaderEditConfig,
2073) -> Result<Header, ConfigError> {
2074 let position = match entry.position.to_ascii_lowercase().as_str() {
2075 "request" => HeaderPosition::Request,
2076 "response" => HeaderPosition::Response,
2077 "both" => HeaderPosition::Both,
2078 _ => {
2079 return Err(ConfigError::InvalidHeaderPosition {
2080 index,
2081 position: entry.position.clone(),
2082 });
2083 }
2084 };
2085 if !header_name_is_valid_token(entry.key.as_bytes()) {
2086 return Err(ConfigError::InvalidHeaderBytes {
2087 index,
2088 field: "key",
2089 });
2090 }
2091 if header_value_contains_forbidden_controls(entry.value.as_bytes()) {
2092 return Err(ConfigError::InvalidHeaderBytes {
2093 index,
2094 field: "value",
2095 });
2096 }
2097 let header = Header {
2098 position: position as i32,
2099 key: entry.key.clone(),
2100 val: entry.value.clone(),
2101 };
2102 debug_assert!(
2108 header_name_is_valid_token(header.key.as_bytes()),
2109 "an emitted header key must be a valid token"
2110 );
2111 debug_assert!(
2112 !header_value_contains_forbidden_controls(header.val.as_bytes()),
2113 "an emitted header value must be free of forbidden control bytes"
2114 );
2115 Ok(header)
2116}
2117
2118pub(crate) fn header_name_is_valid_token(bytes: &[u8]) -> bool {
2126 if bytes.is_empty() {
2127 return false;
2128 }
2129 bytes.iter().all(|&b| is_tchar(b))
2130}
2131
2132fn is_tchar(b: u8) -> bool {
2135 b.is_ascii_alphanumeric()
2136 || matches!(
2137 b,
2138 b'!' | b'#'
2139 | b'$'
2140 | b'%'
2141 | b'&'
2142 | b'\''
2143 | b'*'
2144 | b'+'
2145 | b'-'
2146 | b'.'
2147 | b'^'
2148 | b'_'
2149 | b'`'
2150 | b'|'
2151 | b'~'
2152 )
2153}
2154
2155pub(crate) fn header_value_contains_forbidden_controls(bytes: &[u8]) -> bool {
2163 bytes
2164 .iter()
2165 .any(|&b| matches!(b, 0x00..=0x08 | 0x0A..=0x1F | 0x7F))
2166}
2167
2168#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2169#[serde(deny_unknown_fields, rename_all = "lowercase")]
2170pub enum ListenerProtocol {
2171 Http,
2172 Https,
2173 Tcp,
2174 Udp,
2175}
2176
2177#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2178#[serde(deny_unknown_fields, rename_all = "lowercase")]
2179pub enum FileClusterProtocolConfig {
2180 Http,
2181 Tcp,
2182}
2183
2184fn default_health_check_interval() -> u32 {
2185 10
2186}
2187fn default_health_check_timeout() -> u32 {
2188 5
2189}
2190fn default_health_check_threshold() -> u32 {
2191 3
2192}
2193
2194#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2195#[serde(deny_unknown_fields)]
2196pub struct FileHealthCheckConfig {
2197 pub uri: String,
2198 #[serde(default = "default_health_check_interval")]
2199 pub interval: u32,
2200 #[serde(default = "default_health_check_timeout")]
2201 pub timeout: u32,
2202 #[serde(default = "default_health_check_threshold")]
2203 pub healthy_threshold: u32,
2204 #[serde(default = "default_health_check_threshold")]
2205 pub unhealthy_threshold: u32,
2206 #[serde(default)]
2207 pub expected_status: u32,
2208}
2209
2210impl FileHealthCheckConfig {
2211 pub fn to_proto(&self) -> HealthCheckConfig {
2212 let proto = HealthCheckConfig {
2213 uri: self.uri.to_owned(),
2214 interval: self.interval,
2215 timeout: self.timeout,
2216 healthy_threshold: self.healthy_threshold,
2217 unhealthy_threshold: self.unhealthy_threshold,
2218 expected_status: self.expected_status,
2219 };
2220 debug_assert_eq!(proto.uri, self.uri, "proto URI must mirror the file config");
2224 debug_assert!(
2225 proto.interval == self.interval
2226 && proto.timeout == self.timeout
2227 && proto.healthy_threshold == self.healthy_threshold
2228 && proto.unhealthy_threshold == self.unhealthy_threshold,
2229 "proto timing knobs must mirror the file config"
2230 );
2231 proto
2232 }
2233}
2234
2235pub fn validate_health_check_config(cfg: &HealthCheckConfig) -> Result<(), &'static str> {
2246 if cfg.interval == 0 {
2247 return Err("health check interval must be > 0");
2248 }
2249 if cfg.timeout == 0 {
2250 return Err("health check timeout must be > 0");
2251 }
2252 if cfg.healthy_threshold == 0 {
2253 return Err("health check healthy_threshold must be > 0");
2254 }
2255 if cfg.unhealthy_threshold == 0 {
2256 return Err("health check unhealthy_threshold must be > 0");
2257 }
2258 if !cfg.uri.starts_with('/') {
2259 return Err("health check URI must start with '/'");
2260 }
2261 if cfg
2262 .uri
2263 .bytes()
2264 .any(|b| b == b'\r' || b == b'\n' || b == 0 || (b < 0x20 && b != b'\t'))
2265 {
2266 return Err("health check URI must not contain CR, LF, NUL, or other C0 control bytes");
2267 }
2268 debug_assert!(
2274 cfg.interval > 0
2275 && cfg.timeout > 0
2276 && cfg.healthy_threshold > 0
2277 && cfg.unhealthy_threshold > 0,
2278 "validated health-check thresholds must all be strictly positive"
2279 );
2280 debug_assert!(
2281 cfg.uri.starts_with('/'),
2282 "validated health-check URI must be an absolute path"
2283 );
2284 Ok(())
2285}
2286
2287#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2288#[serde(deny_unknown_fields)]
2289pub struct FileClusterConfig {
2290 pub frontends: Vec<FileClusterFrontendConfig>,
2291 pub backends: Vec<BackendConfig>,
2292 pub protocol: FileClusterProtocolConfig,
2293 pub sticky_session: Option<bool>,
2294 pub https_redirect: Option<bool>,
2295 #[serde(default)]
2296 pub send_proxy: Option<bool>,
2297 #[serde(default)]
2298 pub load_balancing: LoadBalancingAlgorithms,
2299 pub answer_503: Option<String>,
2300 #[serde(default)]
2301 pub load_metric: Option<LoadMetric>,
2302 pub http2: Option<bool>,
2305 pub answers: Option<BTreeMap<String, String>>,
2316 pub https_redirect_port: Option<u32>,
2321 pub authorized_hashes: Option<Vec<String>>,
2326 pub www_authenticate: Option<String>,
2330 pub max_connections_per_ip: Option<u64>,
2337 pub retry_after: Option<u32>,
2343 #[serde(default)]
2347 pub health_check: Option<FileHealthCheckConfig>,
2348 #[serde(default)]
2352 pub udp: Option<FileUdpClusterConfig>,
2353}
2354
2355#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2358#[serde(deny_unknown_fields)]
2359pub struct FileUdpHealthConfig {
2360 pub mode: Option<UdpHealthMode>,
2364 pub tcp_port: Option<u32>,
2365 pub rise: Option<u32>,
2366 pub fall: Option<u32>,
2367 pub fail_open: Option<bool>,
2368 pub udp_probe_payload: Option<String>,
2370 pub probe_interval_seconds: Option<u32>,
2371 pub probe_timeout_seconds: Option<u32>,
2372}
2373
2374impl FileUdpHealthConfig {
2375 pub fn to_proto(&self) -> UdpHealthConfig {
2376 UdpHealthConfig {
2377 mode: self.mode.map(|m| m as i32),
2378 tcp_port: self.tcp_port,
2379 rise: self.rise,
2380 fall: self.fall,
2381 fail_open: self.fail_open,
2382 udp_probe_payload: self
2383 .udp_probe_payload
2384 .as_ref()
2385 .map(|p| p.as_bytes().to_owned()),
2386 probe_interval_seconds: self.probe_interval_seconds,
2387 probe_timeout_seconds: self.probe_timeout_seconds,
2388 }
2389 }
2390}
2391
2392#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2394#[serde(deny_unknown_fields)]
2395pub struct FileUdpClusterConfig {
2396 pub affinity_key: Option<UdpAffinityKey>,
2399 pub responses: Option<u32>,
2401 pub requests: Option<u32>,
2403 pub send_proxy_protocol: Option<bool>,
2405 pub proxy_protocol_every_datagram: Option<bool>,
2407 pub health: Option<FileUdpHealthConfig>,
2409}
2410
2411impl FileUdpClusterConfig {
2412 pub fn to_proto(&self) -> UdpClusterConfig {
2413 UdpClusterConfig {
2414 affinity_key: self.affinity_key.map(|k| k as i32),
2415 responses: self.responses,
2416 requests: self.requests,
2417 send_proxy_protocol: self.send_proxy_protocol,
2418 proxy_protocol_every_datagram: self.proxy_protocol_every_datagram,
2419 health: self.health.as_ref().map(|h| h.to_proto()),
2420 }
2421 }
2422}
2423
2424#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2425#[serde(deny_unknown_fields)]
2426pub struct BackendConfig {
2427 pub address: SocketAddr,
2428 pub weight: Option<u8>,
2429 pub sticky_id: Option<String>,
2430 pub backup: Option<bool>,
2431 pub backend_id: Option<String>,
2432}
2433
2434impl FileClusterConfig {
2435 pub fn to_cluster_config(
2436 self,
2437 cluster_id: &str,
2438 expect_proxy: &HashSet<SocketAddr>,
2439 ) -> Result<ClusterConfig, ConfigError> {
2440 let requested_frontend_count = self.frontends.len();
2443 match self.protocol {
2444 FileClusterProtocolConfig::Tcp => {
2445 let mut has_expect_proxy = None;
2446 let mut frontends = Vec::new();
2447 for f in self.frontends {
2448 if expect_proxy.contains(&f.address) {
2449 match has_expect_proxy {
2450 Some(true) => {}
2451 Some(false) => {
2452 return Err(ConfigError::Incompatible {
2453 object: ObjectKind::Cluster,
2454 id: cluster_id.to_owned(),
2455 kind: IncompatibilityKind::ProxyProtocol,
2456 });
2457 }
2458 None => has_expect_proxy = Some(true),
2459 }
2460 } else {
2461 match has_expect_proxy {
2462 Some(false) => {}
2463 Some(true) => {
2464 return Err(ConfigError::Incompatible {
2465 object: ObjectKind::Cluster,
2466 id: cluster_id.to_owned(),
2467 kind: IncompatibilityKind::ProxyProtocol,
2468 });
2469 }
2470 None => has_expect_proxy = Some(false),
2471 }
2472 }
2473 let tcp_frontend = f.to_tcp_front()?;
2474 frontends.push(tcp_frontend);
2475 }
2476
2477 let send_proxy = self.send_proxy.unwrap_or(false);
2478 let expect_proxy = has_expect_proxy.unwrap_or(false);
2479 let proxy_protocol = match (send_proxy, expect_proxy) {
2480 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
2481 (true, false) => Some(ProxyProtocolConfig::SendHeader),
2482 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
2483 _ => None,
2484 };
2485
2486 let answers = match self.answers.as_ref() {
2487 Some(map) => load_answers(map)?,
2488 None => BTreeMap::new(),
2489 };
2490
2491 let udp = self.udp.as_ref().map(|u| u.to_proto());
2492 debug_assert_eq!(
2498 frontends.len(),
2499 requested_frontend_count,
2500 "every TCP frontend must survive conversion"
2501 );
2502 debug_assert_eq!(
2503 proxy_protocol,
2504 match (send_proxy, expect_proxy) {
2505 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
2506 (true, false) => Some(ProxyProtocolConfig::SendHeader),
2507 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
2508 (false, false) => None,
2509 },
2510 "proxy_protocol must be the (send, expect) function"
2511 );
2512
2513 Ok(ClusterConfig::Tcp(TcpClusterConfig {
2514 cluster_id: cluster_id.to_string(),
2515 frontends,
2516 backends: self.backends,
2517 proxy_protocol,
2518 load_balancing: self.load_balancing,
2519 load_metric: self.load_metric,
2520 answers,
2521 https_redirect_port: self.https_redirect_port,
2522 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
2523 www_authenticate: self.www_authenticate,
2524 max_connections_per_ip: self.max_connections_per_ip,
2525 retry_after: self.retry_after,
2526 health_check: self.health_check.as_ref().map(|hc| hc.to_proto()),
2527 udp,
2528 }))
2529 }
2530 FileClusterProtocolConfig::Http => {
2531 let mut frontends = Vec::new();
2532 for frontend in self.frontends {
2533 let http_frontend = frontend.to_http_front(cluster_id)?;
2534 frontends.push(http_frontend);
2535 }
2536
2537 let answer_503 = self.answer_503.as_ref().and_then(|path| {
2538 Config::load_file(path)
2539 .map_err(|e| {
2540 error!("cannot load 503 error page at path '{}': {:?}", path, e);
2541 e
2542 })
2543 .ok()
2544 });
2545
2546 let answers = match self.answers.as_ref() {
2547 Some(map) => load_answers(map)?,
2548 None => BTreeMap::new(),
2549 };
2550
2551 let udp = self.udp.as_ref().map(|u| u.to_proto());
2552 debug_assert_eq!(
2555 frontends.len(),
2556 requested_frontend_count,
2557 "every HTTP frontend must survive conversion"
2558 );
2559
2560 Ok(ClusterConfig::Http(HttpClusterConfig {
2561 cluster_id: cluster_id.to_string(),
2562 frontends,
2563 backends: self.backends,
2564 sticky_session: self.sticky_session.unwrap_or(false),
2565 https_redirect: self.https_redirect.unwrap_or(false),
2566 load_balancing: self.load_balancing,
2567 load_metric: self.load_metric,
2568 answer_503,
2569 http2: self.http2,
2570 answers,
2571 https_redirect_port: self.https_redirect_port,
2572 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
2573 www_authenticate: self.www_authenticate,
2574 max_connections_per_ip: self.max_connections_per_ip,
2575 retry_after: self.retry_after,
2576 health_check: self.health_check.as_ref().map(|hc| hc.to_proto()),
2577 udp,
2578 }))
2579 }
2580 }
2581 }
2582}
2583
2584#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2585#[serde(deny_unknown_fields)]
2586pub struct HttpFrontendConfig {
2587 pub address: SocketAddr,
2588 pub hostname: String,
2589 pub path: PathRule,
2590 pub method: Option<String>,
2591 pub certificate: Option<String>,
2592 pub key: Option<String>,
2593 pub certificate_chain: Option<Vec<String>>,
2594 #[serde(default)]
2595 pub tls_versions: Vec<TlsVersion>,
2596 #[serde(default)]
2597 pub position: RulePosition,
2598 pub tags: Option<BTreeMap<String, String>>,
2599 #[serde(default)]
2601 pub redirect: Option<RedirectPolicy>,
2602 #[serde(default)]
2604 pub redirect_scheme: Option<RedirectScheme>,
2605 #[serde(default)]
2606 pub redirect_template: Option<String>,
2607 #[serde(default)]
2608 pub rewrite_host: Option<String>,
2609 #[serde(default)]
2610 pub rewrite_path: Option<String>,
2611 #[serde(default)]
2612 pub rewrite_port: Option<u32>,
2613 #[serde(default)]
2614 pub required_auth: Option<bool>,
2615 #[serde(default)]
2618 pub headers: Vec<Header>,
2619 #[serde(default)]
2622 pub hsts: Option<HstsConfig>,
2623}
2624
2625impl fmt::Debug for HttpFrontendConfig {
2626 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2627 let certificate = self.certificate.as_ref().map(|_| "[redacted]");
2628 let certificate_len = self.certificate.as_ref().map(String::len);
2629 let key = self.key.as_ref().map(|_| "[redacted]");
2630 let key_len = self.key.as_ref().map(String::len);
2631 let certificate_chain = self.certificate_chain.as_ref().map(|_| "[redacted]");
2632 let certificate_chain_count = self.certificate_chain.as_ref().map(Vec::len);
2633 let certificate_chain_len = self.certificate_chain.as_ref().map(|chain| {
2634 chain
2635 .iter()
2636 .map(String::len)
2637 .fold(0usize, usize::saturating_add)
2638 });
2639 let method_len = self.method.as_ref().map(String::len);
2640 let redirect_template_len = self.redirect_template.as_ref().map(String::len);
2641 let rewrite_host_len = self.rewrite_host.as_ref().map(String::len);
2642 let rewrite_path_len = self.rewrite_path.as_ref().map(String::len);
2643
2644 f.debug_struct("HttpFrontendConfig")
2645 .field("address", &self.address)
2646 .field("hostname_len", &self.hostname.len())
2647 .field("path_kind", &self.path.kind)
2648 .field("path_len", &self.path.value.len())
2649 .field("method_len", &method_len)
2650 .field("certificate", &certificate)
2651 .field("certificate_len", &certificate_len)
2652 .field("key", &key)
2653 .field("key_len", &key_len)
2654 .field("certificate_chain", &certificate_chain)
2655 .field("certificate_chain_count", &certificate_chain_count)
2656 .field("certificate_chain_len", &certificate_chain_len)
2657 .field("tls_versions_count", &self.tls_versions.len())
2658 .field("position", &self.position)
2659 .field(
2660 "tags_count",
2661 &self.tags.as_ref().map(BTreeMap::len).unwrap_or_default(),
2662 )
2663 .field("redirect", &self.redirect)
2664 .field("redirect_scheme", &self.redirect_scheme)
2665 .field("redirect_template_len", &redirect_template_len)
2666 .field("rewrite_host_len", &rewrite_host_len)
2667 .field("rewrite_path_len", &rewrite_path_len)
2668 .field("rewrite_port", &self.rewrite_port)
2669 .field("required_auth", &self.required_auth)
2670 .field("headers_count", &self.headers.len())
2671 .field("hsts", &self.hsts)
2672 .finish()
2673 }
2674}
2675
2676impl HttpFrontendConfig {
2677 pub fn generate_requests(&self, cluster_id: &str) -> Vec<Request> {
2678 let mut v = Vec::new();
2679
2680 let tags = self.tags.clone().unwrap_or_default();
2681
2682 if self.key.is_some() && self.certificate.is_some() {
2683 v.push(
2684 RequestType::AddCertificate(AddCertificate {
2685 address: self.address.into(),
2686 certificate: CertificateAndKey {
2687 key: self.key.clone().unwrap(),
2688 certificate: self.certificate.clone().unwrap(),
2689 certificate_chain: self.certificate_chain.clone().unwrap_or_default(),
2690 versions: self.tls_versions.iter().map(|v| *v as i32).collect(),
2691 names: vec![],
2696 },
2697 expired_at: None,
2698 })
2699 .into(),
2700 );
2701
2702 v.push(
2703 RequestType::AddHttpsFrontend(RequestHttpFrontend {
2704 cluster_id: Some(cluster_id.to_string()),
2705 address: self.address.into(),
2706 hostname: self.hostname.clone(),
2707 path: self.path.clone(),
2708 method: self.method.clone(),
2709 position: self.position.into(),
2710 tags,
2711 redirect: self.redirect.map(|r| r as i32),
2712 required_auth: self.required_auth,
2713 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
2714 redirect_template: self.redirect_template.clone(),
2715 rewrite_host: self.rewrite_host.clone(),
2716 rewrite_path: self.rewrite_path.clone(),
2717 rewrite_port: self.rewrite_port,
2718 headers: self.headers.clone(),
2719 hsts: self.hsts,
2720 })
2721 .into(),
2722 );
2723 } else {
2724 v.push(
2726 RequestType::AddHttpFrontend(RequestHttpFrontend {
2727 cluster_id: Some(cluster_id.to_string()),
2728 address: self.address.into(),
2729 hostname: self.hostname.clone(),
2730 path: self.path.clone(),
2731 method: self.method.clone(),
2732 position: self.position.into(),
2733 tags,
2734 redirect: self.redirect.map(|r| r as i32),
2735 required_auth: self.required_auth,
2736 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
2737 redirect_template: self.redirect_template.clone(),
2738 rewrite_host: self.rewrite_host.clone(),
2739 rewrite_path: self.rewrite_path.clone(),
2740 rewrite_port: self.rewrite_port,
2741 headers: self.headers.clone(),
2742 hsts: self.hsts,
2743 })
2744 .into(),
2745 );
2746 }
2747
2748 v
2749 }
2750}
2751
2752#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2753#[serde(deny_unknown_fields)]
2754pub struct HttpClusterConfig {
2755 pub cluster_id: String,
2756 pub frontends: Vec<HttpFrontendConfig>,
2757 pub backends: Vec<BackendConfig>,
2758 pub sticky_session: bool,
2759 pub https_redirect: bool,
2760 pub load_balancing: LoadBalancingAlgorithms,
2761 pub load_metric: Option<LoadMetric>,
2762 pub answer_503: Option<String>,
2763 pub http2: Option<bool>,
2764 #[serde(default)]
2767 pub answers: BTreeMap<String, String>,
2768 #[serde(default)]
2769 pub https_redirect_port: Option<u32>,
2770 #[serde(default)]
2771 pub authorized_hashes: Vec<String>,
2772 #[serde(default)]
2773 pub www_authenticate: Option<String>,
2774 #[serde(default)]
2777 pub max_connections_per_ip: Option<u64>,
2778 #[serde(default)]
2781 pub retry_after: Option<u32>,
2782 #[serde(default)]
2786 pub health_check: Option<HealthCheckConfig>,
2787 #[serde(default)]
2790 pub udp: Option<UdpClusterConfig>,
2791}
2792
2793impl HttpClusterConfig {
2794 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2795 let mut v: Vec<Request> = vec![
2796 RequestType::AddCluster(Cluster {
2797 cluster_id: self.cluster_id.clone(),
2798 sticky_session: self.sticky_session,
2799 https_redirect: self.https_redirect,
2800 proxy_protocol: None,
2801 load_balancing: self.load_balancing as i32,
2802 answer_503: self.answer_503.clone(),
2803 load_metric: self.load_metric.map(|s| s as i32),
2804 http2: self.http2,
2805 answers: self.answers.clone(),
2806 https_redirect_port: self.https_redirect_port,
2807 authorized_hashes: self.authorized_hashes.clone(),
2808 www_authenticate: self.www_authenticate.clone(),
2809 max_connections_per_ip: self.max_connections_per_ip,
2810 retry_after: self.retry_after,
2811 health_check: self.health_check.clone(),
2812 udp: self.udp.clone(),
2813 })
2814 .into(),
2815 ];
2816
2817 for frontend in &self.frontends {
2818 let mut orders = frontend.generate_requests(&self.cluster_id);
2819 v.append(&mut orders);
2820 }
2821
2822 for (backend_count, backend) in self.backends.iter().enumerate() {
2823 let load_balancing_parameters = Some(LoadBalancingParams {
2824 weight: backend.weight.unwrap_or(100) as i32,
2825 });
2826
2827 v.push(
2828 RequestType::AddBackend(AddBackend {
2829 cluster_id: self.cluster_id.clone(),
2830 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
2831 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
2832 }),
2833 address: backend.address.into(),
2834 load_balancing_parameters,
2835 sticky_id: backend.sticky_id.clone(),
2836 backup: backend.backup,
2837 })
2838 .into(),
2839 );
2840 }
2841
2842 debug_assert!(
2847 matches!(
2848 v.first().and_then(|r| r.request_type.as_ref()),
2849 Some(RequestType::AddCluster(_))
2850 ),
2851 "HTTP cluster orders must lead with an AddCluster"
2852 );
2853 debug_assert_eq!(
2854 v.iter()
2855 .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
2856 .count(),
2857 self.backends.len(),
2858 "one AddBackend order per configured backend"
2859 );
2860 Ok(v)
2861 }
2862}
2863
2864#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2865pub struct TcpFrontendConfig {
2866 pub address: SocketAddr,
2867 pub tags: Option<BTreeMap<String, String>>,
2868 #[serde(default)]
2875 pub udp: bool,
2876 #[serde(default)]
2880 pub sni: Option<String>,
2881 #[serde(default)]
2884 pub alpn: Vec<String>,
2885}
2886
2887#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2888pub struct TcpClusterConfig {
2889 pub cluster_id: String,
2890 pub frontends: Vec<TcpFrontendConfig>,
2891 pub backends: Vec<BackendConfig>,
2892 #[serde(default)]
2893 pub proxy_protocol: Option<ProxyProtocolConfig>,
2894 pub load_balancing: LoadBalancingAlgorithms,
2895 pub load_metric: Option<LoadMetric>,
2896 #[serde(default)]
2900 pub answers: BTreeMap<String, String>,
2901 #[serde(default)]
2902 pub https_redirect_port: Option<u32>,
2903 #[serde(default)]
2904 pub authorized_hashes: Vec<String>,
2905 #[serde(default)]
2906 pub www_authenticate: Option<String>,
2907 #[serde(default)]
2910 pub max_connections_per_ip: Option<u64>,
2911 #[serde(default)]
2915 pub retry_after: Option<u32>,
2916 #[serde(default)]
2920 pub health_check: Option<HealthCheckConfig>,
2921 #[serde(default)]
2924 pub udp: Option<UdpClusterConfig>,
2925}
2926
2927impl TcpClusterConfig {
2928 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2929 let mut v: Vec<Request> = vec![
2930 RequestType::AddCluster(Cluster {
2931 cluster_id: self.cluster_id.clone(),
2932 sticky_session: false,
2933 https_redirect: false,
2934 proxy_protocol: self.proxy_protocol.map(|s| s as i32),
2935 load_balancing: self.load_balancing as i32,
2936 load_metric: self.load_metric.map(|s| s as i32),
2937 answer_503: None,
2938 http2: None,
2939 answers: self.answers.clone(),
2940 https_redirect_port: self.https_redirect_port,
2941 authorized_hashes: self.authorized_hashes.clone(),
2942 www_authenticate: self.www_authenticate.clone(),
2943 max_connections_per_ip: self.max_connections_per_ip,
2944 retry_after: self.retry_after,
2945 health_check: self.health_check.clone(),
2946 udp: self.udp.clone(),
2947 })
2948 .into(),
2949 ];
2950
2951 for frontend in &self.frontends {
2952 if frontend.udp {
2957 v.push(
2958 RequestType::AddUdpFrontend(RequestUdpFrontend {
2959 cluster_id: self.cluster_id.clone(),
2960 address: frontend.address.into(),
2961 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
2962 })
2963 .into(),
2964 );
2965 } else {
2966 v.push(
2967 RequestType::AddTcpFrontend(RequestTcpFrontend {
2968 cluster_id: self.cluster_id.clone(),
2969 address: frontend.address.into(),
2970 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
2971 sni: frontend.sni.clone(),
2972 alpn: frontend.alpn.clone(),
2973 })
2974 .into(),
2975 );
2976 }
2977 }
2978
2979 for (backend_count, backend) in self.backends.iter().enumerate() {
2980 let load_balancing_parameters = Some(LoadBalancingParams {
2981 weight: backend.weight.unwrap_or(100) as i32,
2982 });
2983
2984 v.push(
2985 RequestType::AddBackend(AddBackend {
2986 cluster_id: self.cluster_id.clone(),
2987 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
2988 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
2989 }),
2990 address: backend.address.into(),
2991 load_balancing_parameters,
2992 sticky_id: backend.sticky_id.clone(),
2993 backup: backend.backup,
2994 })
2995 .into(),
2996 );
2997 }
2998
2999 debug_assert!(
3003 matches!(
3004 v.first().and_then(|r| r.request_type.as_ref()),
3005 Some(RequestType::AddCluster(_))
3006 ),
3007 "TCP cluster orders must lead with an AddCluster"
3008 );
3009 debug_assert_eq!(
3010 v.iter()
3011 .filter(|r| matches!(
3012 r.request_type,
3013 Some(RequestType::AddTcpFrontend(_)) | Some(RequestType::AddUdpFrontend(_))
3014 ))
3015 .count(),
3016 self.frontends.len(),
3017 "one AddTcpFrontend or AddUdpFrontend order per configured frontend"
3018 );
3019 debug_assert_eq!(
3020 v.iter()
3021 .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
3022 .count(),
3023 self.backends.len(),
3024 "one AddBackend order per configured backend"
3025 );
3026 Ok(v)
3027 }
3028}
3029
3030#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
3031pub enum ClusterConfig {
3032 Http(HttpClusterConfig),
3033 Tcp(TcpClusterConfig),
3034}
3035
3036impl ClusterConfig {
3037 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
3038 match *self {
3039 ClusterConfig::Http(ref http) => http.generate_requests(),
3040 ClusterConfig::Tcp(ref tcp) => tcp.generate_requests(),
3041 }
3042 }
3043}
3044
3045#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
3047pub struct FileConfig {
3048 pub command_socket: Option<String>,
3049 pub command_buffer_size: Option<u64>,
3050 pub max_command_buffer_size: Option<u64>,
3051 pub max_connections: Option<usize>,
3052 pub min_buffers: Option<u64>,
3053 pub max_buffers: Option<u64>,
3054 pub buffer_size: Option<u64>,
3055 #[serde(default)]
3059 pub slab_entries_per_connection: Option<u64>,
3060 #[serde(default)]
3070 pub basic_auth_max_credential_bytes: Option<u64>,
3071 #[serde(default)]
3079 pub max_connections_per_ip: Option<u64>,
3080 #[serde(default)]
3086 pub retry_after: Option<u32>,
3087 #[serde(default)]
3096 pub splice_pipe_capacity_bytes: Option<u64>,
3097 #[serde(default)]
3104 pub command_allowed_uids: Option<Vec<u32>>,
3105 pub saved_state: Option<String>,
3106 #[serde(default)]
3107 pub automatic_state_save: Option<bool>,
3108 pub log_level: Option<String>,
3109 pub log_target: Option<String>,
3110 #[serde(default)]
3111 pub log_colored: bool,
3112 #[serde(default)]
3120 pub audit_logs_target: Option<String>,
3121 #[serde(default)]
3126 pub audit_logs_json_target: Option<String>,
3127 #[serde(default)]
3128 pub access_logs_target: Option<String>,
3129 #[serde(default)]
3130 pub access_logs_format: Option<AccessLogFormat>,
3131 #[serde(default)]
3132 pub access_logs_colored: Option<bool>,
3133 pub worker_count: Option<u16>,
3134 pub worker_automatic_restart: Option<bool>,
3135 pub metrics: Option<MetricsConfig>,
3136 pub disable_cluster_metrics: Option<bool>,
3137 pub listeners: Option<Vec<ListenerBuilder>>,
3138 pub clusters: Option<HashMap<String, FileClusterConfig>>,
3139 pub handle_process_affinity: Option<bool>,
3140 pub ctl_command_timeout: Option<u64>,
3141 pub pid_file_path: Option<String>,
3142 pub activate_listeners: Option<bool>,
3143 #[serde(default)]
3144 pub front_timeout: Option<u32>,
3145 #[serde(default)]
3146 pub back_timeout: Option<u32>,
3147 #[serde(default)]
3148 pub connect_timeout: Option<u32>,
3149 #[serde(default)]
3150 pub zombie_check_interval: Option<u32>,
3151 #[serde(default)]
3152 pub accept_queue_timeout: Option<u32>,
3153 #[serde(default)]
3154 pub evict_on_queue_full: Option<bool>,
3155 #[serde(default)]
3156 pub request_timeout: Option<u32>,
3157 #[serde(default)]
3158 pub worker_timeout: Option<u32>,
3159}
3160
3161impl FileConfig {
3162 pub fn load_from_path(path: &str) -> Result<FileConfig, ConfigError> {
3163 let data = Config::load_file(path)?;
3164
3165 let config: FileConfig = match toml::from_str(&data) {
3166 Ok(config) => config,
3167 Err(e) => {
3168 display_toml_error(&data, &e);
3169 return Err(ConfigError::DeserializeToml(e.to_string()));
3170 }
3171 };
3172
3173 let mut reserved_address: HashSet<SocketAddr> = HashSet::new();
3174
3175 if let Some(listeners) = config.listeners.as_ref() {
3176 for listener in listeners.iter() {
3177 if reserved_address.contains(&listener.address) {
3178 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
3179 }
3180 reserved_address.insert(listener.address);
3181 }
3182 }
3183
3184 Ok(config)
3206 }
3207}
3208
3209pub struct ConfigBuilder {
3211 file: FileConfig,
3212 known_addresses: HashMap<SocketAddr, ListenerProtocol>,
3213 expect_proxy_addresses: HashSet<SocketAddr>,
3214 built: Config,
3215}
3216
3217impl ConfigBuilder {
3218 pub fn new<S>(file_config: FileConfig, config_path: S) -> Self
3222 where
3223 S: ToString,
3224 {
3225 let built = Config {
3226 accept_queue_timeout: file_config
3227 .accept_queue_timeout
3228 .unwrap_or(DEFAULT_ACCEPT_QUEUE_TIMEOUT),
3229 evict_on_queue_full: file_config
3230 .evict_on_queue_full
3231 .unwrap_or(DEFAULT_EVICT_ON_QUEUE_FULL),
3232 activate_listeners: file_config.activate_listeners.unwrap_or(true),
3233 automatic_state_save: file_config
3234 .automatic_state_save
3235 .unwrap_or(DEFAULT_AUTOMATIC_STATE_SAVE),
3236 back_timeout: file_config.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
3237 buffer_size: file_config.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE),
3238 command_buffer_size: file_config
3239 .command_buffer_size
3240 .unwrap_or(DEFAULT_COMMAND_BUFFER_SIZE),
3241 config_path: config_path.to_string(),
3242 connect_timeout: file_config
3243 .connect_timeout
3244 .unwrap_or(DEFAULT_CONNECT_TIMEOUT),
3245 ctl_command_timeout: file_config.ctl_command_timeout.unwrap_or(1_000),
3246 front_timeout: file_config.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
3247 handle_process_affinity: file_config.handle_process_affinity.unwrap_or(false),
3248 access_logs_target: file_config.access_logs_target.clone(),
3249 audit_logs_target: file_config.audit_logs_target.clone(),
3250 audit_logs_json_target: file_config.audit_logs_json_target.clone(),
3251 access_logs_format: file_config.access_logs_format.clone(),
3252 access_logs_colored: file_config.access_logs_colored,
3253 log_level: file_config
3254 .log_level
3255 .clone()
3256 .unwrap_or_else(|| String::from("info")),
3257 log_target: file_config
3258 .log_target
3259 .clone()
3260 .unwrap_or_else(|| String::from("stdout")),
3261 log_colored: file_config.log_colored,
3262 max_buffers: file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
3263 max_command_buffer_size: file_config
3264 .max_command_buffer_size
3265 .unwrap_or(DEFAULT_MAX_COMMAND_BUFFER_SIZE),
3266 max_connections: file_config
3267 .max_connections
3268 .unwrap_or(DEFAULT_MAX_CONNECTIONS),
3269 metrics: file_config.metrics.clone(),
3270 disable_cluster_metrics: file_config
3271 .disable_cluster_metrics
3272 .unwrap_or(DEFAULT_DISABLE_CLUSTER_METRICS),
3273 min_buffers: std::cmp::min(
3274 file_config.min_buffers.unwrap_or(DEFAULT_MIN_BUFFERS),
3275 file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
3276 ),
3277 pid_file_path: file_config.pid_file_path.clone(),
3278 request_timeout: file_config
3279 .request_timeout
3280 .unwrap_or(DEFAULT_REQUEST_TIMEOUT),
3281 saved_state: file_config.saved_state.clone(),
3282 worker_automatic_restart: file_config
3283 .worker_automatic_restart
3284 .unwrap_or(DEFAULT_WORKER_AUTOMATIC_RESTART),
3285 worker_count: file_config.worker_count.unwrap_or(DEFAULT_WORKER_COUNT),
3286 zombie_check_interval: file_config
3287 .zombie_check_interval
3288 .unwrap_or(DEFAULT_ZOMBIE_CHECK_INTERVAL),
3289 worker_timeout: file_config.worker_timeout.unwrap_or(DEFAULT_WORKER_TIMEOUT),
3290 slab_entries_per_connection: file_config.slab_entries_per_connection.map(|n| {
3291 n.clamp(
3292 ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION,
3293 ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION,
3294 )
3295 }),
3296 command_allowed_uids: file_config.command_allowed_uids.clone(),
3297 basic_auth_max_credential_bytes: file_config.basic_auth_max_credential_bytes,
3298 max_connections_per_ip: file_config
3299 .max_connections_per_ip
3300 .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_IP),
3301 retry_after: file_config.retry_after.unwrap_or(DEFAULT_RETRY_AFTER),
3302 splice_pipe_capacity_bytes: file_config.splice_pipe_capacity_bytes,
3303 ..Default::default()
3304 };
3305
3306 debug_assert!(
3310 built.min_buffers <= built.max_buffers,
3311 "min_buffers must be clamped to <= max_buffers in the builder"
3312 );
3313 debug_assert!(
3316 built.slab_entries_per_connection.is_none_or(|n| {
3317 (ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION
3318 ..=ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION)
3319 .contains(&n)
3320 }),
3321 "a set slab_entries_per_connection must be clamped into [MIN, MAX]"
3322 );
3323
3324 Self {
3325 file: file_config,
3326 known_addresses: HashMap::new(),
3327 expect_proxy_addresses: HashSet::new(),
3328 built,
3329 }
3330 }
3331
3332 fn push_tls_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3333 let listener = listener.to_tls(Some(&self.built))?;
3334 self.built.https_listeners.push(listener);
3335 Ok(())
3336 }
3337
3338 fn push_http_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3339 let listener = listener.to_http(Some(&self.built))?;
3340 self.built.http_listeners.push(listener);
3341 Ok(())
3342 }
3343
3344 fn push_tcp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3345 let listener = listener.to_tcp(Some(&self.built))?;
3346 self.built.tcp_listeners.push(listener);
3347 Ok(())
3348 }
3349
3350 fn push_udp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3351 let listener = listener.to_udp(Some(&self.built))?;
3352 self.built.udp_listeners.push(listener);
3353 Ok(())
3354 }
3355
3356 fn populate_listeners(&mut self, listeners: Vec<ListenerBuilder>) -> Result<(), ConfigError> {
3357 for listener in listeners.iter() {
3358 if self.known_addresses.contains_key(&listener.address) {
3359 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
3360 }
3361
3362 let protocol = listener
3363 .protocol
3364 .ok_or(ConfigError::Missing(MissingKind::Protocol))?;
3365
3366 self.known_addresses.insert(listener.address, protocol);
3367 if listener.expect_proxy == Some(true) {
3368 self.expect_proxy_addresses.insert(listener.address);
3369 }
3370
3371 if listener.public_address.is_some() && listener.expect_proxy == Some(true) {
3372 return Err(ConfigError::Incompatible {
3373 object: ObjectKind::Listener,
3374 id: listener.address.to_string(),
3375 kind: IncompatibilityKind::PublicAddress,
3376 });
3377 }
3378
3379 match protocol {
3380 ListenerProtocol::Https => self.push_tls_listener(listener.clone())?,
3381 ListenerProtocol::Http => self.push_http_listener(listener.clone())?,
3382 ListenerProtocol::Tcp => self.push_tcp_listener(listener.clone())?,
3383 ListenerProtocol::Udp => self.push_udp_listener(listener.clone())?,
3384 }
3385 }
3386 Ok(())
3387 }
3388
3389 fn populate_clusters(
3390 &mut self,
3391 mut file_cluster_configs: HashMap<String, FileClusterConfig>,
3392 ) -> Result<(), ConfigError> {
3393 for (id, file_cluster_config) in file_cluster_configs.drain() {
3394 let mut cluster_config =
3395 file_cluster_config.to_cluster_config(id.as_str(), &self.expect_proxy_addresses)?;
3396
3397 match cluster_config {
3398 ClusterConfig::Http(ref mut http) => {
3399 for frontend in http.frontends.iter_mut() {
3400 match self.known_addresses.get(&frontend.address) {
3401 Some(ListenerProtocol::Tcp) => {
3402 return Err(ConfigError::WrongFrontendProtocol(
3403 ListenerProtocol::Tcp,
3404 ));
3405 }
3406 Some(ListenerProtocol::Udp) => {
3407 return Err(ConfigError::WrongFrontendProtocol(
3408 ListenerProtocol::Udp,
3409 ));
3410 }
3411 Some(ListenerProtocol::Http) => {
3412 if frontend.certificate.is_some() {
3413 return Err(ConfigError::WrongFrontendProtocol(
3414 ListenerProtocol::Http,
3415 ));
3416 }
3417 }
3418 Some(ListenerProtocol::Https) => {
3419 if frontend.certificate.is_none() {
3420 if let Some(https_listener) =
3421 self.built.https_listeners.iter().find(|listener| {
3422 listener.address == frontend.address.into()
3423 && listener.certificate.is_some()
3424 })
3425 {
3426 frontend
3428 .certificate
3429 .clone_from(&https_listener.certificate);
3430 frontend.certificate_chain =
3431 Some(https_listener.certificate_chain.clone());
3432 frontend.key.clone_from(&https_listener.key);
3433 }
3434 if frontend.certificate.is_none() {
3435 debug!("known addresses: {:?}", self.known_addresses);
3436 debug!("frontend: {:?}", frontend);
3437 return Err(ConfigError::WrongFrontendProtocol(
3438 ListenerProtocol::Https,
3439 ));
3440 }
3441 }
3442 }
3443 None => {
3444 let file_listener_protocol = if frontend.certificate.is_some() {
3446 self.push_tls_listener(ListenerBuilder::new(
3447 frontend.address.into(),
3448 ListenerProtocol::Https,
3449 ))?;
3450
3451 ListenerProtocol::Https
3452 } else {
3453 self.push_http_listener(ListenerBuilder::new(
3454 frontend.address.into(),
3455 ListenerProtocol::Http,
3456 ))?;
3457
3458 ListenerProtocol::Http
3459 };
3460 self.known_addresses
3461 .insert(frontend.address, file_listener_protocol);
3462 }
3463 }
3464 }
3465 }
3466 ClusterConfig::Tcp(ref mut tcp) => {
3467 for frontend in tcp.frontends.iter_mut() {
3469 match self.known_addresses.get(&frontend.address) {
3470 Some(ListenerProtocol::Http) | Some(ListenerProtocol::Https) => {
3471 return Err(ConfigError::WrongFrontendProtocol(
3472 ListenerProtocol::Http,
3473 ));
3474 }
3475 Some(ListenerProtocol::Udp) => {
3476 frontend.udp = true;
3482 }
3483 Some(ListenerProtocol::Tcp) => {}
3484 None => {
3485 self.push_tcp_listener(ListenerBuilder::new(
3487 frontend.address.into(),
3488 ListenerProtocol::Tcp,
3489 ))?;
3490 self.known_addresses
3491 .insert(frontend.address, ListenerProtocol::Tcp);
3492 }
3493 }
3494 }
3495 }
3496 }
3497
3498 self.built.clusters.insert(id, cluster_config);
3499 }
3500 Ok(())
3501 }
3502
3503 pub fn into_config(&mut self) -> Result<Config, ConfigError> {
3505 if let Some(listeners) = &self.file.listeners {
3506 self.populate_listeners(listeners.clone())?;
3507 }
3508
3509 if let Some(file_cluster_configs) = &self.file.clusters {
3510 self.populate_clusters(file_cluster_configs.clone())?;
3511 }
3512
3513 type SniAlpnByAddress = HashMap<SocketAddr, Vec<(Option<String>, Vec<String>)>>;
3533 let mut frontends_by_address: SniAlpnByAddress = HashMap::new();
3534 let mut addresses_with_no_sni_frontend: HashSet<SocketAddr> = HashSet::new();
3535 for cluster in self.built.clusters.values() {
3536 if let ClusterConfig::Tcp(tcp) = cluster {
3537 for frontend in &tcp.frontends {
3538 if frontend.udp {
3545 continue;
3546 }
3547 if frontend.sni.is_none() {
3548 addresses_with_no_sni_frontend.insert(frontend.address);
3549 }
3550 frontends_by_address
3551 .entry(frontend.address)
3552 .or_default()
3553 .push((frontend.sni.clone(), frontend.alpn.clone()));
3554 }
3555 }
3556 }
3557
3558 let mut addresses_with_sni_frontend: HashSet<SocketAddr> = HashSet::new();
3559 for (address, frontends) in &frontends_by_address {
3560 if !frontends.iter().any(|(sni, _)| sni.is_some()) {
3561 continue;
3562 }
3563 addresses_with_sni_frontend.insert(*address);
3564
3565 if addresses_with_no_sni_frontend.contains(address) {
3566 return Err(ConfigError::TcpListenerMixesSniAndNoSni { address: *address });
3567 }
3568
3569 let mut alpn_lists_by_sni: HashMap<Option<String>, Vec<&Vec<String>>> = HashMap::new();
3570 for (sni, alpn) in frontends {
3571 alpn_lists_by_sni.entry(sni.clone()).or_default().push(alpn);
3572 }
3573 for (sni, alpn_lists) in alpn_lists_by_sni {
3574 let mut seen_protocols: HashSet<&str> = HashSet::new();
3575 let mut catch_all_count = 0usize;
3576 for alpn in alpn_lists {
3577 if alpn.is_empty() {
3578 catch_all_count += 1;
3579 if catch_all_count > 1 {
3580 return Err(ConfigError::TcpFrontendMultipleAlpnCatchAll {
3581 address: *address,
3582 sni: sni.clone(),
3583 });
3584 }
3585 continue;
3586 }
3587 for protocol in alpn {
3588 if !seen_protocols.insert(protocol.as_str()) {
3589 return Err(ConfigError::TcpFrontendAlpnOverlap {
3590 address: *address,
3591 sni: sni.clone(),
3592 protocol: protocol.clone(),
3593 });
3594 }
3595 }
3596 }
3597 }
3598 }
3599
3600 for listener in &self.built.tcp_listeners {
3601 let address: SocketAddr = listener.address.into();
3602 if !addresses_with_sni_frontend.contains(&address) {
3603 continue;
3604 }
3605 let sni_preread_timeout = listener
3606 .sni_preread_timeout
3607 .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT);
3608 if sni_preread_timeout > listener.front_timeout {
3609 return Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
3610 address,
3611 sni_preread_timeout,
3612 front_timeout: listener.front_timeout,
3613 });
3614 }
3615 let sni_preread_max_bytes = listener
3616 .sni_preread_max_bytes
3617 .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES);
3618 if sni_preread_max_bytes < MIN_SNI_PREREAD_MAX_BYTES {
3619 return Err(ConfigError::SniPrereadMaxBytesTooSmall {
3620 address,
3621 sni_preread_max_bytes,
3622 minimum: MIN_SNI_PREREAD_MAX_BYTES,
3623 });
3624 }
3625 if u64::from(sni_preread_max_bytes) > self.built.buffer_size {
3626 return Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
3627 address,
3628 sni_preread_max_bytes,
3629 buffer_size: self.built.buffer_size,
3630 });
3631 }
3632 }
3633
3634 let h2_listeners = self
3643 .built
3644 .https_listeners
3645 .iter()
3646 .filter(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
3647 .count();
3648 if h2_listeners > 0 && self.built.buffer_size < H2_MIN_BUFFER_SIZE {
3649 return Err(ConfigError::BufferSizeTooSmallForH2 {
3650 buffer_size: self.built.buffer_size,
3651 minimum: H2_MIN_BUFFER_SIZE,
3652 listeners: h2_listeners,
3653 });
3654 }
3655
3656 if let Some(cap) = self.built.basic_auth_max_credential_bytes {
3666 let third = self.built.buffer_size / 3;
3667 if cap >= third {
3668 warn!(
3669 "basic_auth_max_credential_bytes = {} is >= buffer_size / 3 ({}); \
3670 a hostile peer can pin ~33% of the per-frontend buffer per failed auth \
3671 attempt. Consider lowering basic_auth_max_credential_bytes (typical \
3672 credentials are <100 bytes) or raising buffer_size.",
3673 cap, third
3674 );
3675 }
3676 }
3677
3678 if self.built.evict_on_queue_full && self.built.max_connections < 100 {
3685 let pct = 100usize.div_ceil(self.built.max_connections);
3686 warn!(
3687 "evict_on_queue_full enabled with max_connections = {}; the eviction batch \
3688 clamps to 1, equivalent to ~{}% of capacity per cap event (the knob is \
3689 documented as 1%). Confirm this is intended.",
3690 self.built.max_connections, pct
3691 );
3692 }
3693
3694 let command_socket_path = self.file.command_socket.clone().unwrap_or({
3695 let mut path = env::current_dir().map_err(|e| ConfigError::Env(e.to_string()))?;
3696 path.push("sozu.sock");
3697 let verified_path = path
3698 .to_str()
3699 .ok_or(ConfigError::InvalidPath(path.clone()))?;
3700 verified_path.to_owned()
3701 });
3702
3703 if let (None, Some(true)) = (&self.file.saved_state, &self.file.automatic_state_save) {
3704 return Err(ConfigError::Missing(MissingKind::SavedState));
3705 }
3706
3707 let config = Config {
3708 command_socket: command_socket_path,
3709 ..self.built.clone()
3710 };
3711
3712 debug_assert!(
3718 config.min_buffers <= config.max_buffers,
3719 "min_buffers must not exceed max_buffers"
3720 );
3721 debug_assert!(
3727 !config
3728 .https_listeners
3729 .iter()
3730 .any(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
3731 || config.buffer_size >= H2_MIN_BUFFER_SIZE,
3732 "an h2-advertising config must satisfy the H2 minimum buffer size"
3733 );
3734 Ok(config)
3735 }
3736}
3737
3738#[derive(Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
3742pub struct Config {
3743 pub config_path: String,
3744 pub command_socket: String,
3745 pub command_buffer_size: u64,
3746 pub max_command_buffer_size: u64,
3747 pub max_connections: usize,
3748 pub min_buffers: u64,
3749 pub max_buffers: u64,
3750 pub buffer_size: u64,
3751 pub saved_state: Option<String>,
3752 #[serde(default)]
3753 pub automatic_state_save: bool,
3754 pub log_level: String,
3755 pub log_target: String,
3756 pub log_colored: bool,
3757 #[serde(default)]
3760 pub audit_logs_target: Option<String>,
3761 #[serde(default)]
3764 pub audit_logs_json_target: Option<String>,
3765 #[serde(default)]
3766 pub access_logs_target: Option<String>,
3767 pub access_logs_format: Option<AccessLogFormat>,
3768 pub access_logs_colored: Option<bool>,
3769 pub worker_count: u16,
3770 pub worker_automatic_restart: bool,
3771 pub metrics: Option<MetricsConfig>,
3772 #[serde(default = "default_disable_cluster_metrics")]
3773 pub disable_cluster_metrics: bool,
3774 pub http_listeners: Vec<HttpListenerConfig>,
3775 pub https_listeners: Vec<HttpsListenerConfig>,
3776 pub tcp_listeners: Vec<TcpListenerConfig>,
3777 #[serde(default)]
3778 pub udp_listeners: Vec<UdpListenerConfig>,
3779 pub clusters: HashMap<String, ClusterConfig>,
3780 pub handle_process_affinity: bool,
3781 pub ctl_command_timeout: u64,
3782 pub pid_file_path: Option<String>,
3783 pub activate_listeners: bool,
3784 #[serde(default = "default_front_timeout")]
3785 pub front_timeout: u32,
3786 #[serde(default = "default_back_timeout")]
3787 pub back_timeout: u32,
3788 #[serde(default = "default_connect_timeout")]
3789 pub connect_timeout: u32,
3790 #[serde(default = "default_zombie_check_interval")]
3791 pub zombie_check_interval: u32,
3792 #[serde(default = "default_accept_queue_timeout")]
3793 pub accept_queue_timeout: u32,
3794 #[serde(default = "default_evict_on_queue_full")]
3795 pub evict_on_queue_full: bool,
3796 #[serde(default = "default_request_timeout")]
3797 pub request_timeout: u32,
3798 #[serde(default = "default_worker_timeout")]
3799 pub worker_timeout: u32,
3800 #[serde(default)]
3807 pub slab_entries_per_connection: Option<u64>,
3808 #[serde(default)]
3813 pub command_allowed_uids: Option<Vec<u32>>,
3814 #[serde(default)]
3819 pub basic_auth_max_credential_bytes: Option<u64>,
3820 #[serde(default = "default_max_connections_per_ip")]
3825 pub max_connections_per_ip: u64,
3826 #[serde(default = "default_retry_after")]
3829 pub retry_after: u32,
3830 #[serde(default)]
3837 pub splice_pipe_capacity_bytes: Option<u64>,
3838}
3839
3840fn default_front_timeout() -> u32 {
3841 DEFAULT_FRONT_TIMEOUT
3842}
3843
3844fn default_back_timeout() -> u32 {
3845 DEFAULT_BACK_TIMEOUT
3846}
3847
3848fn default_connect_timeout() -> u32 {
3849 DEFAULT_CONNECT_TIMEOUT
3850}
3851
3852fn default_request_timeout() -> u32 {
3853 DEFAULT_REQUEST_TIMEOUT
3854}
3855
3856fn default_zombie_check_interval() -> u32 {
3857 DEFAULT_ZOMBIE_CHECK_INTERVAL
3858}
3859
3860fn default_accept_queue_timeout() -> u32 {
3861 DEFAULT_ACCEPT_QUEUE_TIMEOUT
3862}
3863
3864fn default_evict_on_queue_full() -> bool {
3865 DEFAULT_EVICT_ON_QUEUE_FULL
3866}
3867
3868fn default_disable_cluster_metrics() -> bool {
3869 DEFAULT_DISABLE_CLUSTER_METRICS
3870}
3871
3872fn default_worker_timeout() -> u32 {
3873 DEFAULT_WORKER_TIMEOUT
3874}
3875
3876fn default_max_connections_per_ip() -> u64 {
3877 DEFAULT_MAX_CONNECTIONS_PER_IP
3878}
3879
3880fn default_retry_after() -> u32 {
3881 DEFAULT_RETRY_AFTER
3882}
3883
3884impl Config {
3885 pub fn load_from_path(path: &str) -> Result<Config, ConfigError> {
3887 let file_config = FileConfig::load_from_path(path)?;
3888
3889 let mut config = ConfigBuilder::new(file_config, path).into_config()?;
3890
3891 config.saved_state = config.saved_state_path()?;
3893
3894 Ok(config)
3895 }
3896
3897 pub fn generate_config_messages(&self) -> Result<Vec<WorkerRequest>, ConfigError> {
3899 let mut v = Vec::new();
3900 let mut count = 0u8;
3901
3902 for listener in &self.http_listeners {
3903 v.push(WorkerRequest {
3904 id: format!("CONFIG-{count}"),
3905 content: RequestType::AddHttpListener(listener.clone()).into(),
3906 });
3907 count += 1;
3908 }
3909
3910 for listener in &self.https_listeners {
3911 v.push(WorkerRequest {
3912 id: format!("CONFIG-{count}"),
3913 content: RequestType::AddHttpsListener(listener.clone()).into(),
3914 });
3915 count += 1;
3916 }
3917
3918 for listener in &self.tcp_listeners {
3919 v.push(WorkerRequest {
3920 id: format!("CONFIG-{count}"),
3921 content: RequestType::AddTcpListener(*listener).into(),
3922 });
3923 count += 1;
3924 }
3925
3926 for listener in &self.udp_listeners {
3927 v.push(WorkerRequest {
3928 id: format!("CONFIG-{count}"),
3929 content: RequestType::AddUdpListener(*listener).into(),
3930 });
3931 count += 1;
3932 }
3933
3934 for cluster in self.clusters.values() {
3935 let mut orders = cluster.generate_requests()?;
3936 for content in orders.drain(..) {
3937 v.push(WorkerRequest {
3938 id: format!("CONFIG-{count}"),
3939 content,
3940 });
3941 count += 1;
3942 }
3943 }
3944
3945 if self.activate_listeners {
3946 for listener in &self.http_listeners {
3947 v.push(WorkerRequest {
3948 id: format!("CONFIG-{count}"),
3949 content: RequestType::ActivateListener(ActivateListener {
3950 address: listener.address,
3951 proxy: ListenerType::Http.into(),
3952 from_scm: false,
3953 })
3954 .into(),
3955 });
3956 count += 1;
3957 }
3958
3959 for listener in &self.https_listeners {
3960 v.push(WorkerRequest {
3961 id: format!("CONFIG-{count}"),
3962 content: RequestType::ActivateListener(ActivateListener {
3963 address: listener.address,
3964 proxy: ListenerType::Https.into(),
3965 from_scm: false,
3966 })
3967 .into(),
3968 });
3969 count += 1;
3970 }
3971
3972 for listener in &self.tcp_listeners {
3973 v.push(WorkerRequest {
3974 id: format!("CONFIG-{count}"),
3975 content: RequestType::ActivateListener(ActivateListener {
3976 address: listener.address,
3977 proxy: ListenerType::Tcp.into(),
3978 from_scm: false,
3979 })
3980 .into(),
3981 });
3982 count += 1;
3983 }
3984
3985 for listener in &self.udp_listeners {
3986 v.push(WorkerRequest {
3987 id: format!("CONFIG-{count}"),
3988 content: RequestType::ActivateListener(ActivateListener {
3989 address: listener.address,
3990 proxy: ListenerType::Udp.into(),
3991 from_scm: false,
3992 })
3993 .into(),
3994 });
3995 count += 1;
3996 }
3997 }
3998
3999 if self.disable_cluster_metrics {
4000 v.push(WorkerRequest {
4001 id: format!("CONFIG-{count}"),
4002 content: RequestType::ConfigureMetrics(MetricsConfiguration::Disabled.into())
4003 .into(),
4004 });
4005 }
4007
4008 Ok(v)
4009 }
4010
4011 pub fn command_socket_path(&self) -> Result<String, ConfigError> {
4013 let config_path_buf = PathBuf::from(self.config_path.clone());
4014 let mut config_dir = config_path_buf
4015 .parent()
4016 .ok_or(ConfigError::NoFileParent(
4017 config_path_buf.to_string_lossy().to_string(),
4018 ))?
4019 .to_path_buf();
4020
4021 let socket_path = PathBuf::from(self.command_socket.clone());
4022
4023 let mut socket_parent_dir = match socket_path.parent() {
4024 None => config_dir,
4027 Some(path) => {
4028 config_dir.push(path);
4030 config_dir.canonicalize().map_err(|io_error| {
4032 ConfigError::SocketPathError(format!(
4033 "Could not canonicalize path {config_dir:?}: {io_error}"
4034 ))
4035 })?
4036 }
4037 };
4038
4039 let socket_name = socket_path
4040 .file_name()
4041 .ok_or(ConfigError::SocketPathError(format!(
4042 "could not get command socket file name from {socket_path:?}"
4043 )))?;
4044
4045 socket_parent_dir.push(socket_name);
4047
4048 let command_socket_path = socket_parent_dir
4049 .to_str()
4050 .ok_or(ConfigError::SocketPathError(format!(
4051 "Invalid socket path {socket_parent_dir:?}"
4052 )))?
4053 .to_string();
4054
4055 Ok(command_socket_path)
4056 }
4057
4058 fn saved_state_path(&self) -> Result<Option<String>, ConfigError> {
4060 let path = match self.saved_state.as_ref() {
4061 Some(path) => path,
4062 None => return Ok(None),
4063 };
4064
4065 debug!("saved_stated path in the config: {}", path);
4066 let config_path = PathBuf::from(self.config_path.clone());
4067
4068 debug!("Config path buffer: {:?}", config_path);
4069 let config_dir = config_path
4070 .parent()
4071 .ok_or(ConfigError::SaveStatePath(format!(
4072 "Could get parent directory of config file {config_path:?}"
4073 )))?;
4074
4075 debug!("Config folder: {:?}", config_dir);
4076 if !config_dir.exists() {
4077 create_dir_all(config_dir).map_err(|io_error| {
4078 ConfigError::SaveStatePath(format!(
4079 "failed to create state parent directory '{config_dir:?}': {io_error}"
4080 ))
4081 })?;
4082 }
4083
4084 let mut saved_state_path_raw = config_dir.to_path_buf();
4085 saved_state_path_raw.push(path);
4086 debug!(
4087 "Looking for saved state on the path {:?}",
4088 saved_state_path_raw
4089 );
4090
4091 match metadata(path) {
4092 Err(err) if matches!(err.kind(), ErrorKind::NotFound) => {
4093 info!("Create an empty state file at '{}'", path);
4094 File::create(path).map_err(|io_error| {
4095 ConfigError::SaveStatePath(format!(
4096 "failed to create state file '{path:?}': {io_error}"
4097 ))
4098 })?;
4099 }
4100 _ => {}
4101 }
4102
4103 saved_state_path_raw.canonicalize().map_err(|io_error| {
4104 ConfigError::SaveStatePath(format!(
4105 "could not get saved state path from config file input {path:?}: {io_error}"
4106 ))
4107 })?;
4108
4109 let stringified_path = saved_state_path_raw
4110 .to_str()
4111 .ok_or(ConfigError::SaveStatePath(format!(
4112 "Invalid path {saved_state_path_raw:?}"
4113 )))?
4114 .to_string();
4115
4116 Ok(Some(stringified_path))
4117 }
4118
4119 pub fn load_file(path: &str) -> Result<String, ConfigError> {
4121 std::fs::read_to_string(path).map_err(|io_error| ConfigError::FileRead {
4122 path_to_read: path.to_owned(),
4123 io_error,
4124 })
4125 }
4126
4127 pub fn load_file_bytes(path: &str) -> Result<Vec<u8>, ConfigError> {
4129 std::fs::read(path).map_err(|io_error| ConfigError::FileRead {
4130 path_to_read: path.to_owned(),
4131 io_error,
4132 })
4133 }
4134}
4135
4136impl fmt::Debug for Config {
4137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4138 f.debug_struct("Config")
4139 .field("config_path", &self.config_path)
4140 .field("command_socket", &self.command_socket)
4141 .field("command_buffer_size", &self.command_buffer_size)
4142 .field("max_command_buffer_size", &self.max_command_buffer_size)
4143 .field("max_connections", &self.max_connections)
4144 .field("min_buffers", &self.min_buffers)
4145 .field("max_buffers", &self.max_buffers)
4146 .field("buffer_size", &self.buffer_size)
4147 .field("saved_state", &self.saved_state)
4148 .field("automatic_state_save", &self.automatic_state_save)
4149 .field("log_level", &self.log_level)
4150 .field("log_target", &self.log_target)
4151 .field("access_logs_target", &self.access_logs_target)
4152 .field("audit_logs_target", &self.audit_logs_target)
4153 .field("audit_logs_json_target", &self.audit_logs_json_target)
4154 .field("access_logs_format", &self.access_logs_format)
4155 .field("worker_count", &self.worker_count)
4156 .field("worker_automatic_restart", &self.worker_automatic_restart)
4157 .field("metrics", &self.metrics)
4158 .field("disable_cluster_metrics", &self.disable_cluster_metrics)
4159 .field("handle_process_affinity", &self.handle_process_affinity)
4160 .field("ctl_command_timeout", &self.ctl_command_timeout)
4161 .field("pid_file_path", &self.pid_file_path)
4162 .field("activate_listeners", &self.activate_listeners)
4163 .field("front_timeout", &self.front_timeout)
4164 .field("back_timeout", &self.back_timeout)
4165 .field("connect_timeout", &self.connect_timeout)
4166 .field("zombie_check_interval", &self.zombie_check_interval)
4167 .field("accept_queue_timeout", &self.accept_queue_timeout)
4168 .field("evict_on_queue_full", &self.evict_on_queue_full)
4169 .field("request_timeout", &self.request_timeout)
4170 .field("worker_timeout", &self.worker_timeout)
4171 .finish()
4172 }
4173}
4174
4175fn display_toml_error(file: &str, error: &toml::de::Error) {
4176 println!("error parsing the configuration file '{file}': {error}");
4177 if let Some(Range { start, end }) = error.span() {
4178 print!("error parsing the configuration file '{file}' at position: {start}, {end}");
4179 }
4180}
4181
4182impl ServerConfig {
4183 pub const DEFAULT_SLAB_ENTRIES_PER_CONNECTION: u64 = 4;
4190 pub const MIN_SLAB_ENTRIES_PER_CONNECTION: u64 = 2;
4193 pub const MAX_SLAB_ENTRIES_PER_CONNECTION: u64 = 32;
4196
4197 pub fn effective_slab_entries_per_connection(&self) -> u64 {
4200 let effective = match self.slab_entries_per_connection {
4201 Some(0) | None => Self::DEFAULT_SLAB_ENTRIES_PER_CONNECTION,
4202 Some(n) => n.clamp(
4203 Self::MIN_SLAB_ENTRIES_PER_CONNECTION,
4204 Self::MAX_SLAB_ENTRIES_PER_CONNECTION,
4205 ),
4206 };
4207 debug_assert!(
4211 (Self::MIN_SLAB_ENTRIES_PER_CONNECTION..=Self::MAX_SLAB_ENTRIES_PER_CONNECTION)
4212 .contains(&effective),
4213 "effective slab entries per connection must stay within [MIN, MAX]"
4214 );
4215 effective
4216 }
4217
4218 pub fn slab_capacity(&self) -> u64 {
4225 let per_conn = self.effective_slab_entries_per_connection();
4226 let capacity = 10 + per_conn * self.max_connections;
4227 debug_assert!(
4232 capacity >= 10,
4233 "slab capacity must reserve the base entries"
4234 );
4235 debug_assert!(
4236 self.max_connections == 0 || capacity > 10,
4237 "a non-zero connection cap must reserve per-connection slab entries"
4238 );
4239 capacity
4240 }
4241}
4242
4243impl From<&Config> for ServerConfig {
4245 fn from(config: &Config) -> Self {
4246 let metrics = config.metrics.clone().map(|m| ServerMetricsConfig {
4247 address: m.address.to_string(),
4248 tagged_metrics: m.tagged_metrics,
4249 prefix: m.prefix,
4250 detail: Some(MetricDetail::from(m.detail) as i32),
4251 });
4252 let server_config = Self {
4253 max_connections: config.max_connections as u64,
4254 front_timeout: config.front_timeout,
4255 back_timeout: config.back_timeout,
4256 connect_timeout: config.connect_timeout,
4257 zombie_check_interval: config.zombie_check_interval,
4258 accept_queue_timeout: config.accept_queue_timeout,
4259 min_buffers: config.min_buffers,
4260 max_buffers: config.max_buffers,
4261 buffer_size: config.buffer_size,
4262 log_level: config.log_level.clone(),
4263 log_target: config.log_target.clone(),
4264 access_logs_target: config.access_logs_target.clone(),
4265 audit_logs_target: config.audit_logs_target.clone(),
4266 audit_logs_json_target: config.audit_logs_json_target.clone(),
4267 command_buffer_size: config.command_buffer_size,
4268 max_command_buffer_size: config.max_command_buffer_size,
4269 metrics,
4270 access_log_format: ProtobufAccessLogFormat::from(&config.access_logs_format) as i32,
4271 log_colored: config.log_colored,
4272 slab_entries_per_connection: config.slab_entries_per_connection,
4273 basic_auth_max_credential_bytes: config.basic_auth_max_credential_bytes,
4274 evict_on_queue_full: Some(config.evict_on_queue_full),
4275 max_connections_per_ip: Some(config.max_connections_per_ip),
4276 retry_after: Some(config.retry_after),
4277 splice_pipe_capacity_bytes: config.splice_pipe_capacity_bytes,
4278 };
4279
4280 debug_assert!(
4285 server_config.min_buffers <= server_config.max_buffers,
4286 "ServerConfig must preserve min_buffers <= max_buffers"
4287 );
4288 debug_assert_eq!(
4289 server_config.buffer_size, config.buffer_size,
4290 "ServerConfig buffer_size must mirror the source config"
4291 );
4292 debug_assert_eq!(
4293 server_config.max_connections, config.max_connections as u64,
4294 "ServerConfig max_connections must mirror the source config"
4295 );
4296 server_config
4297 }
4298}
4299
4300#[cfg(test)]
4301mod tests {
4302 use toml::to_string;
4303
4304 use super::*;
4305
4306 #[test]
4307 fn http_frontend_debug_redacts_pem_material() {
4308 const CERTIFICATE_SECRET: &str = "HTTP_FRONTEND_CERTIFICATE_PEM_SECRET_SENTINEL";
4309 const CHAIN_SECRET: &str = "HTTP_FRONTEND_CHAIN_PEM_SECRET_SENTINEL";
4310 const KEY_SECRET: &str = "HTTP_FRONTEND_KEY_PEM_SECRET_SENTINEL";
4311 const HOSTNAME_SECRET: &str = "HTTP_FRONTEND_HOSTNAME_SECRET_SENTINEL";
4312 const PATH_SECRET: &str = "HTTP_FRONTEND_PATH_SECRET_SENTINEL";
4313 const METHOD_SECRET: &str = "HTTP_FRONTEND_METHOD_SECRET_SENTINEL";
4314 const TAG_KEY_SECRET: &str = "HTTP_FRONTEND_TAG_KEY_SECRET_SENTINEL";
4315 const TAG_SECRET: &str = "HTTP_FRONTEND_TAG_VALUE_SECRET_SENTINEL";
4316 const HEADER_KEY_SECRET: &str = "HTTP_FRONTEND_HEADER_KEY_SECRET_SENTINEL";
4317 const HEADER_SECRET: &str = "HTTP_FRONTEND_HEADER_VALUE_SECRET_SENTINEL";
4318 const REDIRECT_TEMPLATE_SECRET: &str = "HTTP_FRONTEND_REDIRECT_TEMPLATE_SECRET_SENTINEL";
4319 const REWRITE_HOST_SECRET: &str = "HTTP_FRONTEND_REWRITE_HOST_SECRET_SENTINEL";
4320 const REWRITE_PATH_SECRET: &str = "HTTP_FRONTEND_REWRITE_PATH_SECRET_SENTINEL";
4321
4322 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
4323 let certificate = long_value(CERTIFICATE_SECRET);
4324 let certificate_chain = long_value(CHAIN_SECRET);
4325 let key = long_value(KEY_SECRET);
4326 let hostname = long_value(HOSTNAME_SECRET);
4327 let path = long_value(PATH_SECRET);
4328 let method = long_value(METHOD_SECRET);
4329 let redirect_template = long_value(REDIRECT_TEMPLATE_SECRET);
4330 let rewrite_host = long_value(REWRITE_HOST_SECRET);
4331 let rewrite_path = long_value(REWRITE_PATH_SECRET);
4332
4333 let frontend = HttpFrontendConfig {
4334 address: "127.0.0.1:8443".parse().unwrap(),
4335 hostname,
4336 path: PathRule::prefix(path),
4337 method: Some(method),
4338 certificate: Some(certificate),
4339 key: Some(key),
4340 certificate_chain: Some(vec![certificate_chain]),
4341 tls_versions: vec![TlsVersion::TlsV13],
4342 position: RulePosition::Tree,
4343 tags: Some(BTreeMap::from([(
4344 long_value(TAG_KEY_SECRET),
4345 long_value(TAG_SECRET),
4346 )])),
4347 redirect: None,
4348 redirect_scheme: None,
4349 redirect_template: Some(redirect_template),
4350 rewrite_host: Some(rewrite_host),
4351 rewrite_path: Some(rewrite_path),
4352 rewrite_port: None,
4353 required_auth: None,
4354 headers: vec![Header {
4355 position: HeaderPosition::Request as i32,
4356 key: long_value(HEADER_KEY_SECRET),
4357 val: long_value(HEADER_SECRET),
4358 }],
4359 hsts: None,
4360 };
4361
4362 let output = format!("{frontend:?}");
4363
4364 let secrets = [
4365 CERTIFICATE_SECRET,
4366 CHAIN_SECRET,
4367 KEY_SECRET,
4368 HOSTNAME_SECRET,
4369 PATH_SECRET,
4370 METHOD_SECRET,
4371 TAG_KEY_SECRET,
4372 TAG_SECRET,
4373 HEADER_KEY_SECRET,
4374 HEADER_SECRET,
4375 REDIRECT_TEMPLATE_SECRET,
4376 REWRITE_HOST_SECRET,
4377 REWRITE_PATH_SECRET,
4378 ];
4379 for secret in secrets {
4380 assert!(
4381 !output.contains(secret),
4382 "HttpFrontendConfig Debug leaked secret marker {secret}: {output}"
4383 );
4384 }
4385 let expected_metadata = [
4386 "address: 127.0.0.1:8443".to_owned(),
4387 format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
4388 format!("path_kind: {}", PathRule::prefix(String::new()).kind),
4389 format!("path_len: {}", long_value(PATH_SECRET).len()),
4390 format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
4391 "certificate: Some(\"[redacted]\")".to_owned(),
4392 format!(
4393 "certificate_len: Some({})",
4394 long_value(CERTIFICATE_SECRET).len()
4395 ),
4396 "key: Some(\"[redacted]\")".to_owned(),
4397 format!("key_len: Some({})", long_value(KEY_SECRET).len()),
4398 "certificate_chain: Some(\"[redacted]\")".to_owned(),
4399 "certificate_chain_count: Some(1)".to_owned(),
4400 format!(
4401 "certificate_chain_len: Some({})",
4402 long_value(CHAIN_SECRET).len()
4403 ),
4404 "tls_versions_count: 1".to_owned(),
4405 "tags_count: 1".to_owned(),
4406 format!(
4407 "redirect_template_len: Some({})",
4408 long_value(REDIRECT_TEMPLATE_SECRET).len()
4409 ),
4410 format!(
4411 "rewrite_host_len: Some({})",
4412 long_value(REWRITE_HOST_SECRET).len()
4413 ),
4414 format!(
4415 "rewrite_path_len: Some({})",
4416 long_value(REWRITE_PATH_SECRET).len()
4417 ),
4418 "headers_count: 1".to_owned(),
4419 ];
4420 for safe_metadata in expected_metadata {
4421 assert!(
4422 output.contains(&safe_metadata),
4423 "HttpFrontendConfig Debug omitted safe metadata {safe_metadata}: {output}"
4424 );
4425 }
4426 assert!(
4427 output.len() <= 1024,
4428 "HttpFrontendConfig Debug output is not bounded: {} bytes",
4429 output.len()
4430 );
4431
4432 for (index, request) in frontend
4433 .generate_requests("safe-cluster-id")
4434 .into_iter()
4435 .enumerate()
4436 {
4437 let generated_output = format!("{request:?}");
4438 for secret in secrets {
4439 assert!(
4440 !generated_output.contains(secret),
4441 "generated frontend request {index} Debug leaked secret marker {secret}: {generated_output}"
4442 );
4443 }
4444 assert!(
4445 generated_output.len() <= 2048,
4446 "generated frontend request {index} Debug output is not bounded: {} bytes",
4447 generated_output.len()
4448 );
4449 }
4450 }
4451
4452 #[test]
4453 fn hsts_to_proto_enabled_substitutes_default_max_age() {
4454 let cfg = FileHstsConfig {
4455 enabled: Some(true),
4456 max_age: None,
4457 include_subdomains: None,
4458 preload: None,
4459 force_replace_backend: None,
4460 };
4461 let proto = cfg.to_proto("test").expect("should validate");
4462 assert_eq!(proto.enabled, Some(true));
4463 assert_eq!(proto.max_age, Some(DEFAULT_HSTS_MAX_AGE));
4464 }
4465
4466 #[test]
4467 fn hsts_to_proto_explicit_max_age_kept() {
4468 let cfg = FileHstsConfig {
4469 enabled: Some(true),
4470 max_age: Some(63_072_000),
4471 include_subdomains: Some(true),
4472 preload: Some(true),
4473 force_replace_backend: None,
4474 };
4475 let proto = cfg.to_proto("test").expect("should validate");
4476 assert_eq!(proto.max_age, Some(63_072_000));
4477 assert_eq!(proto.include_subdomains, Some(true));
4478 assert_eq!(proto.preload, Some(true));
4479 }
4480
4481 #[test]
4482 fn hsts_to_proto_disabled_keeps_zero_intent() {
4483 let cfg = FileHstsConfig {
4487 enabled: Some(false),
4488 max_age: None,
4489 include_subdomains: None,
4490 preload: None,
4491 force_replace_backend: None,
4492 };
4493 let proto = cfg.to_proto("test").expect("should validate");
4494 assert_eq!(proto.enabled, Some(false));
4495 }
4496
4497 #[test]
4498 fn hsts_to_proto_kill_switch_max_age_zero_allowed() {
4499 let cfg = FileHstsConfig {
4503 enabled: Some(true),
4504 max_age: Some(0),
4505 include_subdomains: None,
4506 preload: None,
4507 force_replace_backend: None,
4508 };
4509 let proto = cfg.to_proto("test").expect("kill-switch must validate");
4510 assert_eq!(proto.max_age, Some(0));
4511 }
4512
4513 #[test]
4514 fn hsts_to_proto_missing_enabled_errors() {
4515 let cfg = FileHstsConfig {
4516 enabled: None,
4517 max_age: Some(31_536_000),
4518 include_subdomains: None,
4519 preload: None,
4520 force_replace_backend: None,
4521 };
4522 match cfg.to_proto("test").unwrap_err() {
4523 ConfigError::HstsEnabledRequired(scope) => assert_eq!(scope, "test"),
4524 other => panic!("expected HstsEnabledRequired, got {other:?}"),
4525 }
4526 }
4527
4528 #[test]
4529 fn hsts_rejected_on_http_listener() {
4530 let mut listener = ListenerBuilder::new(
4535 SocketAddress::new_v4(127, 0, 0, 1, 8080),
4536 ListenerProtocol::Http,
4537 );
4538 listener.hsts = Some(FileHstsConfig {
4539 enabled: Some(true),
4540 max_age: Some(31_536_000),
4541 include_subdomains: None,
4542 preload: None,
4543 force_replace_backend: None,
4544 });
4545 match listener.to_http(None).unwrap_err() {
4546 ConfigError::HstsOnPlainHttp(scope) => assert!(
4547 scope.contains("HTTP listener"),
4548 "expected scope to mention 'HTTP listener', got {scope:?}"
4549 ),
4550 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
4551 }
4552 }
4553
4554 #[test]
4555 fn hsts_rejected_on_http_frontend() {
4556 let frontend = FileClusterFrontendConfig {
4562 address: "127.0.0.1:8080".parse().unwrap(),
4563 hostname: Some("example.com".to_owned()),
4564 alpn: vec![],
4565 path: None,
4566 path_type: None,
4567 method: None,
4568 certificate: None,
4569 key: None,
4570 certificate_chain: None,
4571 tls_versions: vec![],
4572 position: RulePosition::Tree,
4573 tags: None,
4574 redirect: None,
4575 redirect_scheme: None,
4576 redirect_template: None,
4577 rewrite_host: None,
4578 rewrite_path: None,
4579 rewrite_port: None,
4580 required_auth: None,
4581 headers: None,
4582 hsts: Some(FileHstsConfig {
4583 enabled: Some(true),
4584 max_age: Some(31_536_000),
4585 include_subdomains: None,
4586 preload: None,
4587 force_replace_backend: None,
4588 }),
4589 };
4590 match frontend.to_http_front("api").unwrap_err() {
4591 ConfigError::HstsOnPlainHttp(scope) => {
4592 assert!(
4593 scope.contains("api") && scope.contains("example.com"),
4594 "expected scope to mention 'api' and 'example.com', got {scope:?}"
4595 );
4596 }
4597 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
4598 }
4599 }
4600
4601 #[test]
4602 fn serialize() {
4603 let http = ListenerBuilder::new(
4604 SocketAddress::new_v4(127, 0, 0, 1, 8080),
4605 ListenerProtocol::Http,
4606 )
4607 .with_answer_404_path(Some("404.html"))
4608 .to_owned();
4609 println!("http: {:?}", to_string(&http));
4610
4611 let https = ListenerBuilder::new(
4612 SocketAddress::new_v4(127, 0, 0, 1, 8443),
4613 ListenerProtocol::Https,
4614 )
4615 .with_answer_404_path(Some("404.html"))
4616 .to_owned();
4617 println!("https: {:?}", to_string(&https));
4618
4619 let listeners = vec![http, https];
4620 let config = FileConfig {
4621 command_socket: Some(String::from("./command_folder/sock")),
4622 worker_count: Some(2),
4623 worker_automatic_restart: Some(true),
4624 max_connections: Some(500),
4625 min_buffers: Some(1),
4626 max_buffers: Some(500),
4627 buffer_size: Some(16393),
4628 metrics: Some(MetricsConfig {
4629 address: "127.0.0.1:8125".parse().unwrap(),
4630 tagged_metrics: false,
4631 prefix: Some(String::from("sozu-metrics")),
4632 detail: MetricDetailLevel::default(),
4633 }),
4634 listeners: Some(listeners),
4635 ..Default::default()
4636 };
4637
4638 println!("config: {:?}", to_string(&config));
4639 let encoded = to_string(&config).unwrap();
4640 println!("conf:\n{encoded}");
4641 }
4642
4643 #[test]
4644 fn parse() {
4645 let path = "assets/config.toml";
4646 let config = Config::load_from_path(path).unwrap_or_else(|load_error| {
4647 panic!("Cannot load config from path {path}: {load_error:?}")
4648 });
4649 println!("config: {config:#?}");
4650 }
4652
4653 #[test]
4654 fn multiple_listeners_preserve_per_address_expect_proxy() {
4655 let toml_content = r#"
4656 command_socket = "/tmp/sozu_test.sock"
4657 worker_count = 1
4658
4659 [[listeners]]
4660 protocol = "http"
4661 address = "172.16.20.1:80"
4662 expect_proxy = true
4663
4664 [[listeners]]
4665 protocol = "http"
4666 address = "10.22.0.1:80"
4667 expect_proxy = false
4668
4669 [[listeners]]
4670 protocol = "https"
4671 address = "192.168.1.1:443"
4672 expect_proxy = true
4673
4674 [[listeners]]
4675 protocol = "https"
4676 address = "192.168.2.1:443"
4677 expect_proxy = false
4678 "#;
4679
4680 let file_config: FileConfig =
4681 toml::from_str(toml_content).expect("Could not parse TOML config");
4682
4683 let listeners = file_config.listeners.as_ref().expect("No listeners found");
4684 assert_eq!(listeners.len(), 4);
4685
4686 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4687 .into_config()
4688 .expect("Could not build config");
4689
4690 assert_eq!(config.http_listeners.len(), 2);
4691 assert_eq!(config.https_listeners.len(), 2);
4692
4693 let http_proxy = config
4695 .http_listeners
4696 .iter()
4697 .find(|l| SocketAddr::from(l.address) == "172.16.20.1:80".parse().unwrap())
4698 .expect("Listener on 172.16.20.1:80 not found");
4699 let http_direct = config
4700 .http_listeners
4701 .iter()
4702 .find(|l| SocketAddr::from(l.address) == "10.22.0.1:80".parse().unwrap())
4703 .expect("Listener on 10.22.0.1:80 not found");
4704
4705 assert!(http_proxy.expect_proxy);
4706 assert!(!http_direct.expect_proxy);
4707
4708 let https_proxy = config
4710 .https_listeners
4711 .iter()
4712 .find(|l| SocketAddr::from(l.address) == "192.168.1.1:443".parse().unwrap())
4713 .expect("Listener on 192.168.1.1:443 not found");
4714 let https_direct = config
4715 .https_listeners
4716 .iter()
4717 .find(|l| SocketAddr::from(l.address) == "192.168.2.1:443".parse().unwrap())
4718 .expect("Listener on 192.168.2.1:443 not found");
4719
4720 assert!(https_proxy.expect_proxy);
4721 assert!(!https_direct.expect_proxy);
4722 }
4723
4724 #[test]
4725 fn multiple_listeners_generate_correct_worker_requests() {
4726 let toml_content = r#"
4727 command_socket = "/tmp/sozu_test.sock"
4728 worker_count = 1
4729 activate_listeners = true
4730
4731 [[listeners]]
4732 protocol = "http"
4733 address = "172.16.20.1:80"
4734 expect_proxy = true
4735
4736 [[listeners]]
4737 protocol = "http"
4738 address = "10.22.0.1:80"
4739 expect_proxy = false
4740 "#;
4741
4742 let file_config: FileConfig =
4743 toml::from_str(toml_content).expect("Could not parse TOML config");
4744
4745 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4746 .into_config()
4747 .expect("Could not build config");
4748
4749 let messages = config
4750 .generate_config_messages()
4751 .expect("Could not generate config messages");
4752
4753 let add_listener_count = messages
4754 .iter()
4755 .filter(|m| {
4756 matches!(
4757 m.content.request_type,
4758 Some(RequestType::AddHttpListener(_))
4759 )
4760 })
4761 .count();
4762
4763 let activate_listener_count = messages
4764 .iter()
4765 .filter(|m| {
4766 matches!(
4767 m.content.request_type,
4768 Some(RequestType::ActivateListener(ActivateListener {
4769 proxy,
4770 ..
4771 })) if proxy == ListenerType::Http as i32
4772 )
4773 })
4774 .count();
4775
4776 assert_eq!(add_listener_count, 2);
4777 assert_eq!(activate_listener_count, 2);
4778 }
4779
4780 #[test]
4781 fn documented_udp_dns_example_loads_and_emits_udp_requests() {
4782 let toml_content = r#"
4789 command_socket = "/tmp/sozu_test.sock"
4790 worker_count = 1
4791 activate_listeners = true
4792
4793 [[listeners]]
4794 protocol = "udp"
4795 address = "0.0.0.0:53"
4796
4797 [clusters.dns]
4798 protocol = "tcp"
4799 load_balancing = "HRW"
4800 frontends = [
4801 { address = "0.0.0.0:53" }
4802 ]
4803 backends = [
4804 { address = "10.0.0.10:53" },
4805 { address = "10.0.0.11:53" }
4806 ]
4807
4808 [clusters.dns.udp]
4809 affinity_key = "SOURCE_IP"
4810 responses = 1
4811 requests = 0
4812 send_proxy_protocol = true
4813
4814 [clusters.dns.udp.health]
4815 mode = "TCP_PROBE"
4816 tcp_port = 53
4817 rise = 2
4818 fall = 3
4819 fail_open = true
4820 "#;
4821
4822 let file_config: FileConfig =
4823 toml::from_str(toml_content).expect("Could not parse documented DNS TOML");
4824
4825 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4826 .into_config()
4827 .expect("documented UDP DNS example must load without WrongFrontendProtocol");
4828
4829 assert_eq!(
4831 config.udp_listeners.len(),
4832 1,
4833 "the protocol=\"udp\" listener must be built"
4834 );
4835
4836 let messages = config
4837 .generate_config_messages()
4838 .expect("Could not generate config messages");
4839
4840 let add_udp_listener_count = messages
4841 .iter()
4842 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpListener(_))))
4843 .count();
4844 assert_eq!(
4845 add_udp_listener_count, 1,
4846 "must emit exactly one AddUdpListener"
4847 );
4848
4849 let add_udp_frontend_count = messages
4850 .iter()
4851 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
4852 .count();
4853 assert_eq!(
4854 add_udp_frontend_count, 1,
4855 "the cluster frontend on the UDP listener must emit AddUdpFrontend"
4856 );
4857
4858 let add_tcp_frontend_count = messages
4860 .iter()
4861 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddTcpFrontend(_))))
4862 .count();
4863 assert_eq!(
4864 add_tcp_frontend_count, 0,
4865 "a UDP-listener-addressed frontend must not be emitted as AddTcpFrontend"
4866 );
4867
4868 let udp_frontend = messages
4870 .iter()
4871 .find_map(|m| match &m.content.request_type {
4872 Some(RequestType::AddUdpFrontend(f)) => Some(f),
4873 _ => None,
4874 })
4875 .expect("AddUdpFrontend must be present");
4876 assert_eq!(udp_frontend.cluster_id, "dns");
4877 assert_eq!(
4878 SocketAddr::from(udp_frontend.address),
4879 "0.0.0.0:53".parse().unwrap()
4880 );
4881
4882 let cluster = messages
4885 .iter()
4886 .find_map(|m| match &m.content.request_type {
4887 Some(RequestType::AddCluster(c)) if c.cluster_id == "dns" => Some(c),
4888 _ => None,
4889 })
4890 .expect("AddCluster for 'dns' must be present");
4891 let udp = cluster
4892 .udp
4893 .as_ref()
4894 .expect("[clusters.dns.udp] block must carry onto the cluster");
4895 assert_eq!(udp.responses, Some(1));
4896 }
4897
4898 #[test]
4899 fn duplicate_listener_address_rejected() {
4900 let toml_content = r#"
4901 command_socket = "/tmp/sozu_test.sock"
4902 worker_count = 1
4903
4904 [[listeners]]
4905 protocol = "http"
4906 address = "0.0.0.0:80"
4907
4908 [[listeners]]
4909 protocol = "http"
4910 address = "0.0.0.0:80"
4911 "#;
4912
4913 let file_config: FileConfig =
4914 toml::from_str(toml_content).expect("Could not parse TOML config");
4915
4916 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4917
4918 assert!(
4919 result.is_err(),
4920 "Should reject duplicate listener addresses"
4921 );
4922 }
4923
4924 #[test]
4925 fn buffer_size_below_h2_minimum_rejected() {
4926 let toml_content = r#"
4928 command_socket = "/tmp/sozu_test.sock"
4929 worker_count = 1
4930 buffer_size = 8192
4931
4932 [[listeners]]
4933 protocol = "https"
4934 address = "127.0.0.1:8443"
4935 "#;
4936 let file_config: FileConfig =
4937 toml::from_str(toml_content).expect("Could not parse TOML config");
4938 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4939 match result {
4940 Err(ConfigError::BufferSizeTooSmallForH2 {
4941 buffer_size: 8192,
4942 minimum: 16_393,
4943 listeners: 1,
4944 }) => {}
4945 other => panic!("expected BufferSizeTooSmallForH2, got {other:?}"),
4946 }
4947 }
4948
4949 #[test]
4950 fn buffer_size_below_h2_minimum_accepted_when_no_h2_listener() {
4951 let toml_content = r#"
4953 command_socket = "/tmp/sozu_test.sock"
4954 worker_count = 1
4955 buffer_size = 8192
4956
4957 [[listeners]]
4958 protocol = "https"
4959 address = "127.0.0.1:8443"
4960 alpn_protocols = ["http/1.1"]
4961 "#;
4962 let file_config: FileConfig =
4963 toml::from_str(toml_content).expect("Could not parse TOML config");
4964 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4965 assert!(
4966 result.is_ok(),
4967 "non-H2 HTTPS listener with sub-16393 buffer should be accepted: {result:?}"
4968 );
4969 }
4970
4971 #[test]
4972 fn buffer_size_at_h2_minimum_accepted() {
4973 let toml_content = r#"
4974 command_socket = "/tmp/sozu_test.sock"
4975 worker_count = 1
4976 buffer_size = 16393
4977
4978 [[listeners]]
4979 protocol = "https"
4980 address = "127.0.0.1:8443"
4981 "#;
4982 let file_config: FileConfig =
4983 toml::from_str(toml_content).expect("Could not parse TOML config");
4984 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4985 assert!(
4986 result.is_ok(),
4987 "buffer_size at the H2 minimum should be accepted: {result:?}"
4988 );
4989 }
4990
4991 #[test]
4992 fn alpn_protocols_default() {
4993 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4994 let config = builder.to_tls(None).expect("to_tls should succeed");
4995 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4996 }
4997
4998 #[test]
4999 fn alpn_protocols_custom() {
5000 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5001 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned()]));
5002 let config = builder.to_tls(None).expect("to_tls should succeed");
5003 assert_eq!(config.alpn_protocols, vec!["http/1.1"]);
5004 }
5005
5006 #[test]
5007 fn alpn_protocols_invalid_rejected() {
5008 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5009 builder.with_alpn_protocols(Some(vec!["h3".to_owned()]));
5010 let result = builder.to_tls(None);
5011 assert!(result.is_err());
5012 let err = result.unwrap_err();
5013 assert!(
5014 err.to_string().contains("h3"),
5015 "error should mention the invalid protocol: {err}"
5016 );
5017 }
5018
5019 #[test]
5020 fn alpn_protocols_empty_uses_default() {
5021 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5022 builder.with_alpn_protocols(Some(vec![]));
5023 let config = builder.to_tls(None).expect("to_tls should succeed");
5024 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
5025 }
5026
5027 #[test]
5028 fn alpn_protocols_deduplicated() {
5029 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5030 builder.with_alpn_protocols(Some(vec![
5031 "h2".to_owned(),
5032 "h2".to_owned(),
5033 "http/1.1".to_owned(),
5034 ]));
5035 let config = builder.to_tls(None).expect("to_tls should succeed");
5036 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
5037 }
5038
5039 #[test]
5040 fn alpn_protocols_order_preserved() {
5041 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
5042 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned(), "h2".to_owned()]));
5043 let config = builder.to_tls(None).expect("to_tls should succeed");
5044 assert_eq!(config.alpn_protocols, vec!["http/1.1", "h2"]);
5045 }
5046
5047 #[test]
5053 fn parse_header_edit_rejects_crlf_in_value() {
5054 let entry = HeaderEditConfig {
5055 position: "request".to_owned(),
5056 key: "X-Test".to_owned(),
5057 value: "value\r\nEvil-Header: stolen".to_owned(),
5058 };
5059 let err = parse_header_edit(0, &entry).expect_err("CRLF in value must be rejected");
5060 match err {
5061 ConfigError::InvalidHeaderBytes { index, field } => {
5062 assert_eq!(index, 0);
5063 assert_eq!(field, "value");
5064 }
5065 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
5066 }
5067 }
5068
5069 #[test]
5070 fn parse_header_edit_rejects_lf_in_key() {
5071 let entry = HeaderEditConfig {
5072 position: "response".to_owned(),
5073 key: "X-\nTest".to_owned(),
5074 value: "ok".to_owned(),
5075 };
5076 let err = parse_header_edit(2, &entry).expect_err("LF in key must be rejected");
5077 match err {
5078 ConfigError::InvalidHeaderBytes { index, field } => {
5079 assert_eq!(index, 2);
5080 assert_eq!(field, "key");
5081 }
5082 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
5083 }
5084 }
5085
5086 #[test]
5087 fn parse_header_edit_rejects_nul() {
5088 let entry = HeaderEditConfig {
5089 position: "both".to_owned(),
5090 key: "X-Test".to_owned(),
5091 value: "with\0nul".to_owned(),
5092 };
5093 assert!(matches!(
5094 parse_header_edit(0, &entry),
5095 Err(ConfigError::InvalidHeaderBytes { .. })
5096 ));
5097 }
5098
5099 #[test]
5105 fn parse_header_edit_accepts_tab_in_value() {
5106 let entry = HeaderEditConfig {
5107 position: "request".to_owned(),
5108 key: "X-Test".to_owned(),
5109 value: "with\ttab".to_owned(),
5110 };
5111 let header = parse_header_edit(0, &entry).expect("tab in value must be accepted");
5112 assert_eq!(header.val, "with\ttab");
5113 }
5114
5115 #[test]
5122 fn parse_header_edit_rejects_tab_in_key() {
5123 let entry = HeaderEditConfig {
5124 position: "request".to_owned(),
5125 key: "Host\t".to_owned(),
5126 value: "ok".to_owned(),
5127 };
5128 let err = parse_header_edit(0, &entry).expect_err("HTAB in key must be rejected");
5129 match err {
5130 ConfigError::InvalidHeaderBytes { field, .. } => assert_eq!(field, "key"),
5131 other => panic!("expected InvalidHeaderBytes{{field=\"key\"}}, got {other:?}"),
5132 }
5133 }
5134
5135 #[test]
5136 fn parse_header_edit_rejects_space_in_key() {
5137 let entry = HeaderEditConfig {
5138 position: "request".to_owned(),
5139 key: "X Test".to_owned(),
5140 value: "ok".to_owned(),
5141 };
5142 let err = parse_header_edit(0, &entry).expect_err("SP in key must be rejected");
5143 assert!(matches!(err, ConfigError::InvalidHeaderBytes { .. }));
5144 }
5145
5146 #[test]
5147 fn parse_header_edit_rejects_empty_key() {
5148 let entry = HeaderEditConfig {
5149 position: "request".to_owned(),
5150 key: String::new(),
5151 value: "ok".to_owned(),
5152 };
5153 let err = parse_header_edit(0, &entry).expect_err("empty key must be rejected");
5154 assert!(matches!(
5155 err,
5156 ConfigError::InvalidHeaderBytes { field: "key", .. }
5157 ));
5158 }
5159
5160 #[test]
5161 fn parse_header_edit_accepts_clean_value() {
5162 let entry = HeaderEditConfig {
5163 position: "request".to_owned(),
5164 key: "X-Tenant".to_owned(),
5165 value: "alpha".to_owned(),
5166 };
5167 let header = parse_header_edit(0, &entry).expect("clean value must be accepted");
5168 assert_eq!(header.key, "X-Tenant");
5169 assert_eq!(header.val, "alpha");
5170 }
5171
5172 #[test]
5176 fn resolve_answer_source_bare_string_is_literal() {
5177 let body = resolve_answer_source("HTTP/1.1 503 Service Unavailable\r\n\r\nbusy")
5178 .expect("bare-string source must resolve");
5179 assert_eq!(body, "HTTP/1.1 503 Service Unavailable\r\n\r\nbusy");
5180 }
5181
5182 #[test]
5183 fn resolve_answer_source_empty_string_is_legitimate() {
5184 let body = resolve_answer_source("").expect("empty source must resolve");
5185 assert_eq!(body, "");
5186 }
5187
5188 #[test]
5193 fn resolve_answer_source_file_scheme_missing_file_errors() {
5194 let err = resolve_answer_source("file:///nonexistent/sozu-test/never.http")
5195 .expect_err("missing path must error");
5196 assert!(matches!(err, ConfigError::FileOpen { .. }));
5197 }
5198
5199 #[test]
5202 fn resolve_answer_source_file_scheme_empty_path_errors() {
5203 let err = resolve_answer_source("file://").expect_err("empty path must error");
5204 assert!(matches!(err, ConfigError::FileOpen { .. }));
5205 }
5206
5207 #[test]
5215 fn legacy_tcp_frontend_toml_without_sni_alpn_parses_unchanged() {
5216 let toml_content = r#"
5217 command_socket = "/tmp/sozu_test.sock"
5218 worker_count = 1
5219
5220 [[listeners]]
5221 protocol = "tcp"
5222 address = "127.0.0.1:9000"
5223
5224 [clusters.legacy]
5225 protocol = "tcp"
5226 load_balancing = "ROUND_ROBIN"
5227 frontends = [
5228 { address = "127.0.0.1:9000" }
5229 ]
5230 backends = [
5231 { address = "10.0.0.1:9000" }
5232 ]
5233 "#;
5234 let file_config: FileConfig =
5235 toml::from_str(toml_content).expect("Could not parse legacy TCP TOML");
5236 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
5237 .into_config()
5238 .expect("legacy TCP config without sni/alpn must load unchanged");
5239
5240 assert_eq!(config.tcp_listeners.len(), 1);
5241 let listener = &config.tcp_listeners[0];
5242 assert_eq!(
5243 listener.sni_preread_timeout,
5244 Some(DEFAULT_SNI_PREREAD_TIMEOUT),
5245 "proto default sni_preread_timeout must be populated even though unused"
5246 );
5247 assert_eq!(
5248 listener.sni_preread_max_bytes,
5249 Some(DEFAULT_SNI_PREREAD_MAX_BYTES),
5250 "proto default sni_preread_max_bytes must be populated even though unused"
5251 );
5252
5253 let messages = config
5254 .generate_config_messages()
5255 .expect("Could not generate config messages");
5256 let tcp_frontend = messages
5257 .iter()
5258 .find_map(|m| match &m.content.request_type {
5259 Some(RequestType::AddTcpFrontend(f)) => Some(f),
5260 _ => None,
5261 })
5262 .expect("AddTcpFrontend must be present");
5263 assert_eq!(tcp_frontend.sni, None, "legacy frontend must carry no sni");
5264 assert!(
5265 tcp_frontend.alpn.is_empty(),
5266 "legacy frontend must carry no alpn"
5267 );
5268 }
5269
5270 #[test]
5273 fn tcp_frontend_hostname_maps_to_sni_exact_and_wildcard() {
5274 let toml_content = r#"
5275 command_socket = "/tmp/sozu_test.sock"
5276 worker_count = 1
5277
5278 [[listeners]]
5279 protocol = "tcp"
5280 address = "127.0.0.1:9010"
5281
5282 [clusters.exact]
5283 protocol = "tcp"
5284 load_balancing = "ROUND_ROBIN"
5285 frontends = [
5286 { address = "127.0.0.1:9010", hostname = "example.com", alpn = ["h2"] }
5287 ]
5288 backends = [ { address = "10.0.0.1:9010" } ]
5289
5290 [clusters.wildcard]
5291 protocol = "tcp"
5292 load_balancing = "ROUND_ROBIN"
5293 frontends = [
5294 { address = "127.0.0.1:9010", hostname = "*.example.com" }
5295 ]
5296 backends = [ { address = "10.0.0.2:9010" } ]
5297 "#;
5298 let file_config: FileConfig =
5299 toml::from_str(toml_content).expect("Could not parse TOML config");
5300 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
5301 .into_config()
5302 .expect("exact + wildcard SNI frontends on distinct sni must load");
5303
5304 let messages = config
5305 .generate_config_messages()
5306 .expect("Could not generate config messages");
5307 let mut frontends: Vec<_> = messages
5308 .iter()
5309 .filter_map(|m| match &m.content.request_type {
5310 Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
5311 _ => None,
5312 })
5313 .collect();
5314 frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
5315
5316 assert_eq!(frontends.len(), 2);
5317 assert_eq!(frontends[0].sni, Some("example.com".to_string()));
5318 assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
5319 assert_eq!(frontends[1].sni, Some("*.example.com".to_string()));
5320 assert!(frontends[1].alpn.is_empty());
5321 }
5322
5323 #[test]
5328 fn tcp_frontend_invalid_sni_pattern_rejected() {
5329 for invalid in [
5330 "*.*.example.com",
5331 "foo.*.com",
5332 "*",
5333 "example..com",
5334 "",
5335 "/[a-z]+/.example.com",
5336 "foo/bar.example.com",
5337 ] {
5338 let frontend = FileClusterFrontendConfig {
5339 address: "127.0.0.1:8080".parse().unwrap(),
5340 hostname: Some(invalid.to_string()),
5341 alpn: vec![],
5342 path: None,
5343 path_type: None,
5344 method: None,
5345 certificate: None,
5346 key: None,
5347 certificate_chain: None,
5348 tls_versions: vec![],
5349 position: RulePosition::Tree,
5350 tags: None,
5351 redirect: None,
5352 redirect_scheme: None,
5353 redirect_template: None,
5354 rewrite_host: None,
5355 rewrite_path: None,
5356 rewrite_port: None,
5357 required_auth: None,
5358 headers: None,
5359 hsts: None,
5360 };
5361 match frontend.to_tcp_front() {
5362 Err(ConfigError::InvalidSniPattern { sni }) => assert_eq!(sni, invalid),
5363 other => panic!("expected InvalidSniPattern for {invalid:?}, got {other:?}"),
5364 }
5365 }
5366 }
5367
5368 #[test]
5374 fn tcp_frontend_non_ascii_sni_pattern_rejected() {
5375 for non_ascii in ["münchen.example", "*.bücher.example", "日本.example"] {
5376 let frontend = FileClusterFrontendConfig {
5377 address: "127.0.0.1:8080".parse().unwrap(),
5378 hostname: Some(non_ascii.to_string()),
5379 alpn: vec![],
5380 path: None,
5381 path_type: None,
5382 method: None,
5383 certificate: None,
5384 key: None,
5385 certificate_chain: None,
5386 tls_versions: vec![],
5387 position: RulePosition::Tree,
5388 tags: None,
5389 redirect: None,
5390 redirect_scheme: None,
5391 redirect_template: None,
5392 rewrite_host: None,
5393 rewrite_path: None,
5394 rewrite_port: None,
5395 required_auth: None,
5396 headers: None,
5397 hsts: None,
5398 };
5399 match frontend.to_tcp_front() {
5400 Err(ConfigError::NonAsciiSniPattern { sni }) => assert_eq!(sni, non_ascii),
5401 other => panic!("expected NonAsciiSniPattern for {non_ascii:?}, got {other:?}"),
5402 }
5403 }
5404 }
5405
5406 #[test]
5411 fn tcp_frontend_punycode_sni_pattern_accepted() {
5412 assert_eq!(
5413 validate_sni_pattern("xn--mnchen-3ya.example").expect("A-label must be accepted"),
5414 "xn--mnchen-3ya.example"
5415 );
5416 assert_eq!(
5417 validate_sni_pattern("*.xn--bcher-kva.example")
5418 .expect("wildcarded A-label must be accepted"),
5419 "*.xn--bcher-kva.example"
5420 );
5421 assert_eq!(
5422 validate_sni_pattern("XN--MNCHEN-3YA.Example")
5423 .expect("mixed-case A-label must be accepted"),
5424 "xn--mnchen-3ya.example",
5425 "A-label patterns are ASCII-lowercased like any other pattern"
5426 );
5427 }
5428
5429 #[test]
5432 fn alpn_rejected_on_http_frontend() {
5433 let frontend = FileClusterFrontendConfig {
5434 address: "127.0.0.1:8080".parse().unwrap(),
5435 hostname: Some("example.com".to_owned()),
5436 alpn: vec!["h2".to_string()],
5437 path: None,
5438 path_type: None,
5439 method: None,
5440 certificate: None,
5441 key: None,
5442 certificate_chain: None,
5443 tls_versions: vec![],
5444 position: RulePosition::Tree,
5445 tags: None,
5446 redirect: None,
5447 redirect_scheme: None,
5448 redirect_template: None,
5449 rewrite_host: None,
5450 rewrite_path: None,
5451 rewrite_port: None,
5452 required_auth: None,
5453 headers: None,
5454 hsts: None,
5455 };
5456 match frontend.to_http_front("api") {
5457 Err(ConfigError::InvalidFrontendConfig(field)) => assert_eq!(field, "alpn"),
5458 other => panic!("expected InvalidFrontendConfig(\"alpn\"), got {other:?}"),
5459 }
5460 }
5461
5462 #[test]
5468 fn tcp_frontend_alpn_without_sni_rejected() {
5469 let toml_content = r#"
5470 command_socket = "/tmp/sozu_test.sock"
5471 worker_count = 1
5472
5473 [[listeners]]
5474 protocol = "tcp"
5475 address = "127.0.0.1:9019"
5476
5477 [clusters.a]
5478 protocol = "tcp"
5479 load_balancing = "ROUND_ROBIN"
5480 frontends = [
5481 { address = "127.0.0.1:9019", alpn = ["h2"] }
5482 ]
5483 backends = [ { address = "10.0.0.1:9019" } ]
5484 "#;
5485 let file_config: FileConfig =
5486 toml::from_str(toml_content).expect("Could not parse TOML config");
5487 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5488 match result {
5489 Err(ConfigError::AlpnWithoutSni { address }) => {
5490 assert_eq!(address.to_string(), "127.0.0.1:9019");
5491 }
5492 other => panic!("expected AlpnWithoutSni, got {other:?}"),
5493 }
5494 }
5495
5496 #[test]
5500 fn tcp_frontend_alpn_overlap_rejected() {
5501 let toml_content = r#"
5502 command_socket = "/tmp/sozu_test.sock"
5503 worker_count = 1
5504
5505 [[listeners]]
5506 protocol = "tcp"
5507 address = "127.0.0.1:9020"
5508
5509 [clusters.a]
5510 protocol = "tcp"
5511 load_balancing = "ROUND_ROBIN"
5512 frontends = [
5513 { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2"] }
5514 ]
5515 backends = [ { address = "10.0.0.1:9020" } ]
5516
5517 [clusters.b]
5518 protocol = "tcp"
5519 load_balancing = "ROUND_ROBIN"
5520 frontends = [
5521 { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2", "http/1.1"] }
5522 ]
5523 backends = [ { address = "10.0.0.2:9020" } ]
5524 "#;
5525 let file_config: FileConfig =
5526 toml::from_str(toml_content).expect("Could not parse TOML config");
5527 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5528 match result {
5529 Err(ConfigError::TcpFrontendAlpnOverlap { protocol, .. }) => {
5530 assert_eq!(protocol, "h2");
5531 }
5532 other => panic!("expected TcpFrontendAlpnOverlap, got {other:?}"),
5533 }
5534 }
5535
5536 #[test]
5540 fn tcp_frontend_multiple_alpn_catch_all_rejected() {
5541 let toml_content = r#"
5542 command_socket = "/tmp/sozu_test.sock"
5543 worker_count = 1
5544
5545 [[listeners]]
5546 protocol = "tcp"
5547 address = "127.0.0.1:9021"
5548
5549 [clusters.a]
5550 protocol = "tcp"
5551 load_balancing = "ROUND_ROBIN"
5552 frontends = [
5553 { address = "127.0.0.1:9021", hostname = "example.com" }
5554 ]
5555 backends = [ { address = "10.0.0.1:9021" } ]
5556
5557 [clusters.b]
5558 protocol = "tcp"
5559 load_balancing = "ROUND_ROBIN"
5560 frontends = [
5561 { address = "127.0.0.1:9021", hostname = "example.com" }
5562 ]
5563 backends = [ { address = "10.0.0.2:9021" } ]
5564 "#;
5565 let file_config: FileConfig =
5566 toml::from_str(toml_content).expect("Could not parse TOML config");
5567 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5568 assert!(
5569 matches!(
5570 result,
5571 Err(ConfigError::TcpFrontendMultipleAlpnCatchAll { .. })
5572 ),
5573 "expected TcpFrontendMultipleAlpnCatchAll, got {result:?}"
5574 );
5575 }
5576
5577 #[test]
5581 fn tcp_listener_mixes_sni_and_no_sni_rejected() {
5582 let toml_content = r#"
5583 command_socket = "/tmp/sozu_test.sock"
5584 worker_count = 1
5585
5586 [[listeners]]
5587 protocol = "tcp"
5588 address = "127.0.0.1:9022"
5589
5590 [clusters.a]
5591 protocol = "tcp"
5592 load_balancing = "ROUND_ROBIN"
5593 frontends = [
5594 { address = "127.0.0.1:9022", hostname = "example.com" }
5595 ]
5596 backends = [ { address = "10.0.0.1:9022" } ]
5597
5598 [clusters.b]
5599 protocol = "tcp"
5600 load_balancing = "ROUND_ROBIN"
5601 frontends = [
5602 { address = "127.0.0.1:9022" }
5603 ]
5604 backends = [ { address = "10.0.0.2:9022" } ]
5605 "#;
5606 let file_config: FileConfig =
5607 toml::from_str(toml_content).expect("Could not parse TOML config");
5608 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5609 assert!(
5610 matches!(result, Err(ConfigError::TcpListenerMixesSniAndNoSni { .. })),
5611 "expected TcpListenerMixesSniAndNoSni, got {result:?}"
5612 );
5613 }
5614
5615 #[test]
5620 fn sni_preread_timeout_exceeding_front_timeout_rejected() {
5621 let toml_content = r#"
5622 command_socket = "/tmp/sozu_test.sock"
5623 worker_count = 1
5624
5625 [[listeners]]
5626 protocol = "tcp"
5627 address = "127.0.0.1:9030"
5628 front_timeout = 2
5629
5630 [clusters.a]
5631 protocol = "tcp"
5632 load_balancing = "ROUND_ROBIN"
5633 frontends = [
5634 { address = "127.0.0.1:9030", hostname = "example.com" }
5635 ]
5636 backends = [ { address = "10.0.0.1:9030" } ]
5637 "#;
5638 let file_config: FileConfig =
5639 toml::from_str(toml_content).expect("Could not parse TOML config");
5640 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5641 match result {
5642 Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
5643 sni_preread_timeout: 5,
5644 front_timeout: 2,
5645 ..
5646 }) => {}
5647 other => panic!("expected SniPrereadTimeoutExceedsFrontTimeout, got {other:?}"),
5648 }
5649 }
5650
5651 #[test]
5655 fn sni_preread_max_bytes_exceeding_buffer_size_rejected() {
5656 let toml_content = r#"
5657 command_socket = "/tmp/sozu_test.sock"
5658 worker_count = 1
5659 buffer_size = 8192
5660
5661 [[listeners]]
5662 protocol = "tcp"
5663 address = "127.0.0.1:9031"
5664
5665 [clusters.a]
5666 protocol = "tcp"
5667 load_balancing = "ROUND_ROBIN"
5668 frontends = [
5669 { address = "127.0.0.1:9031", hostname = "example.com" }
5670 ]
5671 backends = [ { address = "10.0.0.1:9031" } ]
5672 "#;
5673 let file_config: FileConfig =
5674 toml::from_str(toml_content).expect("Could not parse TOML config");
5675 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5676 match result {
5677 Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
5678 sni_preread_max_bytes: 16384,
5679 buffer_size: 8192,
5680 ..
5681 }) => {}
5682 other => panic!("expected SniPrereadMaxBytesExceedsBufferSize, got {other:?}"),
5683 }
5684 }
5685
5686 #[test]
5691 fn sni_preread_max_bytes_zero_rejected() {
5692 let toml_content = r#"
5693 command_socket = "/tmp/sozu_test.sock"
5694 worker_count = 1
5695
5696 [[listeners]]
5697 protocol = "tcp"
5698 address = "127.0.0.1:9033"
5699 sni_preread_max_bytes = 0
5700
5701 [clusters.a]
5702 protocol = "tcp"
5703 load_balancing = "ROUND_ROBIN"
5704 frontends = [
5705 { address = "127.0.0.1:9033", hostname = "example.com" }
5706 ]
5707 backends = [ { address = "10.0.0.1:9033" } ]
5708 "#;
5709 let file_config: FileConfig =
5710 toml::from_str(toml_content).expect("Could not parse TOML config");
5711 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5712 match result {
5713 Err(ConfigError::SniPrereadMaxBytesTooSmall {
5714 sni_preread_max_bytes: 0,
5715 minimum: 5,
5716 ..
5717 }) => {}
5718 other => panic!("expected SniPrereadMaxBytesTooSmall, got {other:?}"),
5719 }
5720 }
5721
5722 #[test]
5725 fn sni_preread_max_bytes_at_the_floor_loads() {
5726 let toml_content = r#"
5727 command_socket = "/tmp/sozu_test.sock"
5728 worker_count = 1
5729
5730 [[listeners]]
5731 protocol = "tcp"
5732 address = "127.0.0.1:9034"
5733 sni_preread_max_bytes = 5
5734
5735 [clusters.a]
5736 protocol = "tcp"
5737 load_balancing = "ROUND_ROBIN"
5738 frontends = [
5739 { address = "127.0.0.1:9034", hostname = "example.com" }
5740 ]
5741 backends = [ { address = "10.0.0.1:9034" } ]
5742 "#;
5743 let file_config: FileConfig =
5744 toml::from_str(toml_content).expect("Could not parse TOML config");
5745 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5746 assert!(
5747 result.is_ok(),
5748 "sni_preread_max_bytes at the exact floor must load: {result:?}"
5749 );
5750 }
5751
5752 #[test]
5756 fn sni_preread_validation_ignored_without_sni_frontend() {
5757 let toml_content = r#"
5758 command_socket = "/tmp/sozu_test.sock"
5759 worker_count = 1
5760 buffer_size = 8192
5761
5762 [[listeners]]
5763 protocol = "tcp"
5764 address = "127.0.0.1:9032"
5765 front_timeout = 2
5766
5767 [clusters.a]
5768 protocol = "tcp"
5769 load_balancing = "ROUND_ROBIN"
5770 frontends = [
5771 { address = "127.0.0.1:9032" }
5772 ]
5773 backends = [ { address = "10.0.0.1:9032" } ]
5774 "#;
5775 let file_config: FileConfig =
5776 toml::from_str(toml_content).expect("Could not parse TOML config");
5777 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5778 assert!(
5779 result.is_ok(),
5780 "a no-SNI TCP listener must ignore sni_preread validation entirely: {result:?}"
5781 );
5782 }
5783
5784 #[test]
5791 fn tcp_frontend_disjoint_alpn_same_sni_both_load() {
5792 let toml_content = r#"
5793 command_socket = "/tmp/sozu_test.sock"
5794 worker_count = 1
5795
5796 [[listeners]]
5797 protocol = "tcp"
5798 address = "127.0.0.1:9040"
5799
5800 [clusters.h2_cluster]
5801 protocol = "tcp"
5802 load_balancing = "ROUND_ROBIN"
5803 frontends = [
5804 { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["h2"] }
5805 ]
5806 backends = [ { address = "10.0.0.1:9040" } ]
5807
5808 [clusters.http11_cluster]
5809 protocol = "tcp"
5810 load_balancing = "ROUND_ROBIN"
5811 frontends = [
5812 { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["http/1.1"] }
5813 ]
5814 backends = [ { address = "10.0.0.2:9040" } ]
5815 "#;
5816 let file_config: FileConfig =
5817 toml::from_str(toml_content).expect("Could not parse TOML config");
5818 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
5819 .into_config()
5820 .expect("disjoint non-empty ALPN lists on the same (address, sni) must load");
5821
5822 let messages = config
5823 .generate_config_messages()
5824 .expect("Could not generate config messages");
5825 let mut frontends: Vec<_> = messages
5826 .iter()
5827 .filter_map(|m| match &m.content.request_type {
5828 Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
5829 _ => None,
5830 })
5831 .collect();
5832 frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
5833
5834 assert_eq!(frontends.len(), 2, "both frontends must be emitted");
5835 assert_eq!(frontends[0].cluster_id, "h2_cluster");
5836 assert_eq!(frontends[0].sni, Some("example.com".to_string()));
5837 assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
5838 assert_eq!(frontends[1].cluster_id, "http11_cluster");
5839 assert_eq!(frontends[1].sni, Some("example.com".to_string()));
5840 assert_eq!(frontends[1].alpn, vec!["http/1.1".to_string()]);
5841 }
5842
5843 #[test]
5852 fn udp_routed_tcp_frontends_are_excluded_from_sni_invariants() {
5853 let toml_content = r#"
5854 command_socket = "/tmp/sozu_test.sock"
5855 worker_count = 1
5856
5857 [[listeners]]
5858 protocol = "udp"
5859 address = "127.0.0.1:9050"
5860
5861 [clusters.a]
5862 protocol = "tcp"
5863 load_balancing = "ROUND_ROBIN"
5864 frontends = [
5865 { address = "127.0.0.1:9050", hostname = "example.com" }
5866 ]
5867 backends = [ { address = "10.0.0.1:9050" } ]
5868
5869 [clusters.b]
5870 protocol = "tcp"
5871 load_balancing = "ROUND_ROBIN"
5872 frontends = [
5873 { address = "127.0.0.1:9050" }
5874 ]
5875 backends = [ { address = "10.0.0.2:9050" } ]
5876 "#;
5877 let file_config: FileConfig =
5878 toml::from_str(toml_content).expect("Could not parse TOML config");
5879 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5880 assert!(
5881 result.is_ok(),
5882 "UDP-routed tcp-cluster frontends must not trip the SNI mixing-ban: {result:?}"
5883 );
5884
5885 let config = result.expect("checked is_ok above");
5886 let messages = config
5887 .generate_config_messages()
5888 .expect("Could not generate config messages");
5889 let add_udp_frontend_count = messages
5890 .iter()
5891 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
5892 .count();
5893 assert_eq!(
5894 add_udp_frontend_count, 2,
5895 "both cluster frontends on the udp listener must emit AddUdpFrontend"
5896 );
5897 }
5898}