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(Debug, 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 HttpFrontendConfig {
2626 pub fn generate_requests(&self, cluster_id: &str) -> Vec<Request> {
2627 let mut v = Vec::new();
2628
2629 let tags = self.tags.clone().unwrap_or_default();
2630
2631 if self.key.is_some() && self.certificate.is_some() {
2632 v.push(
2633 RequestType::AddCertificate(AddCertificate {
2634 address: self.address.into(),
2635 certificate: CertificateAndKey {
2636 key: self.key.clone().unwrap(),
2637 certificate: self.certificate.clone().unwrap(),
2638 certificate_chain: self.certificate_chain.clone().unwrap_or_default(),
2639 versions: self.tls_versions.iter().map(|v| *v as i32).collect(),
2640 names: vec![],
2645 },
2646 expired_at: None,
2647 })
2648 .into(),
2649 );
2650
2651 v.push(
2652 RequestType::AddHttpsFrontend(RequestHttpFrontend {
2653 cluster_id: Some(cluster_id.to_string()),
2654 address: self.address.into(),
2655 hostname: self.hostname.clone(),
2656 path: self.path.clone(),
2657 method: self.method.clone(),
2658 position: self.position.into(),
2659 tags,
2660 redirect: self.redirect.map(|r| r as i32),
2661 required_auth: self.required_auth,
2662 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
2663 redirect_template: self.redirect_template.clone(),
2664 rewrite_host: self.rewrite_host.clone(),
2665 rewrite_path: self.rewrite_path.clone(),
2666 rewrite_port: self.rewrite_port,
2667 headers: self.headers.clone(),
2668 hsts: self.hsts,
2669 })
2670 .into(),
2671 );
2672 } else {
2673 v.push(
2675 RequestType::AddHttpFrontend(RequestHttpFrontend {
2676 cluster_id: Some(cluster_id.to_string()),
2677 address: self.address.into(),
2678 hostname: self.hostname.clone(),
2679 path: self.path.clone(),
2680 method: self.method.clone(),
2681 position: self.position.into(),
2682 tags,
2683 redirect: self.redirect.map(|r| r as i32),
2684 required_auth: self.required_auth,
2685 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
2686 redirect_template: self.redirect_template.clone(),
2687 rewrite_host: self.rewrite_host.clone(),
2688 rewrite_path: self.rewrite_path.clone(),
2689 rewrite_port: self.rewrite_port,
2690 headers: self.headers.clone(),
2691 hsts: self.hsts,
2692 })
2693 .into(),
2694 );
2695 }
2696
2697 v
2698 }
2699}
2700
2701#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2702#[serde(deny_unknown_fields)]
2703pub struct HttpClusterConfig {
2704 pub cluster_id: String,
2705 pub frontends: Vec<HttpFrontendConfig>,
2706 pub backends: Vec<BackendConfig>,
2707 pub sticky_session: bool,
2708 pub https_redirect: bool,
2709 pub load_balancing: LoadBalancingAlgorithms,
2710 pub load_metric: Option<LoadMetric>,
2711 pub answer_503: Option<String>,
2712 pub http2: Option<bool>,
2713 #[serde(default)]
2716 pub answers: BTreeMap<String, String>,
2717 #[serde(default)]
2718 pub https_redirect_port: Option<u32>,
2719 #[serde(default)]
2720 pub authorized_hashes: Vec<String>,
2721 #[serde(default)]
2722 pub www_authenticate: Option<String>,
2723 #[serde(default)]
2726 pub max_connections_per_ip: Option<u64>,
2727 #[serde(default)]
2730 pub retry_after: Option<u32>,
2731 #[serde(default)]
2735 pub health_check: Option<HealthCheckConfig>,
2736 #[serde(default)]
2739 pub udp: Option<UdpClusterConfig>,
2740}
2741
2742impl HttpClusterConfig {
2743 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2744 let mut v: Vec<Request> = vec![
2745 RequestType::AddCluster(Cluster {
2746 cluster_id: self.cluster_id.clone(),
2747 sticky_session: self.sticky_session,
2748 https_redirect: self.https_redirect,
2749 proxy_protocol: None,
2750 load_balancing: self.load_balancing as i32,
2751 answer_503: self.answer_503.clone(),
2752 load_metric: self.load_metric.map(|s| s as i32),
2753 http2: self.http2,
2754 answers: self.answers.clone(),
2755 https_redirect_port: self.https_redirect_port,
2756 authorized_hashes: self.authorized_hashes.clone(),
2757 www_authenticate: self.www_authenticate.clone(),
2758 max_connections_per_ip: self.max_connections_per_ip,
2759 retry_after: self.retry_after,
2760 health_check: self.health_check.clone(),
2761 udp: self.udp.clone(),
2762 })
2763 .into(),
2764 ];
2765
2766 for frontend in &self.frontends {
2767 let mut orders = frontend.generate_requests(&self.cluster_id);
2768 v.append(&mut orders);
2769 }
2770
2771 for (backend_count, backend) in self.backends.iter().enumerate() {
2772 let load_balancing_parameters = Some(LoadBalancingParams {
2773 weight: backend.weight.unwrap_or(100) as i32,
2774 });
2775
2776 v.push(
2777 RequestType::AddBackend(AddBackend {
2778 cluster_id: self.cluster_id.clone(),
2779 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
2780 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
2781 }),
2782 address: backend.address.into(),
2783 load_balancing_parameters,
2784 sticky_id: backend.sticky_id.clone(),
2785 backup: backend.backup,
2786 })
2787 .into(),
2788 );
2789 }
2790
2791 debug_assert!(
2796 matches!(
2797 v.first().and_then(|r| r.request_type.as_ref()),
2798 Some(RequestType::AddCluster(_))
2799 ),
2800 "HTTP cluster orders must lead with an AddCluster"
2801 );
2802 debug_assert_eq!(
2803 v.iter()
2804 .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
2805 .count(),
2806 self.backends.len(),
2807 "one AddBackend order per configured backend"
2808 );
2809 Ok(v)
2810 }
2811}
2812
2813#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2814pub struct TcpFrontendConfig {
2815 pub address: SocketAddr,
2816 pub tags: Option<BTreeMap<String, String>>,
2817 #[serde(default)]
2824 pub udp: bool,
2825 #[serde(default)]
2829 pub sni: Option<String>,
2830 #[serde(default)]
2833 pub alpn: Vec<String>,
2834}
2835
2836#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2837pub struct TcpClusterConfig {
2838 pub cluster_id: String,
2839 pub frontends: Vec<TcpFrontendConfig>,
2840 pub backends: Vec<BackendConfig>,
2841 #[serde(default)]
2842 pub proxy_protocol: Option<ProxyProtocolConfig>,
2843 pub load_balancing: LoadBalancingAlgorithms,
2844 pub load_metric: Option<LoadMetric>,
2845 #[serde(default)]
2849 pub answers: BTreeMap<String, String>,
2850 #[serde(default)]
2851 pub https_redirect_port: Option<u32>,
2852 #[serde(default)]
2853 pub authorized_hashes: Vec<String>,
2854 #[serde(default)]
2855 pub www_authenticate: Option<String>,
2856 #[serde(default)]
2859 pub max_connections_per_ip: Option<u64>,
2860 #[serde(default)]
2864 pub retry_after: Option<u32>,
2865 #[serde(default)]
2869 pub health_check: Option<HealthCheckConfig>,
2870 #[serde(default)]
2873 pub udp: Option<UdpClusterConfig>,
2874}
2875
2876impl TcpClusterConfig {
2877 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2878 let mut v: Vec<Request> = vec![
2879 RequestType::AddCluster(Cluster {
2880 cluster_id: self.cluster_id.clone(),
2881 sticky_session: false,
2882 https_redirect: false,
2883 proxy_protocol: self.proxy_protocol.map(|s| s as i32),
2884 load_balancing: self.load_balancing as i32,
2885 load_metric: self.load_metric.map(|s| s as i32),
2886 answer_503: None,
2887 http2: None,
2888 answers: self.answers.clone(),
2889 https_redirect_port: self.https_redirect_port,
2890 authorized_hashes: self.authorized_hashes.clone(),
2891 www_authenticate: self.www_authenticate.clone(),
2892 max_connections_per_ip: self.max_connections_per_ip,
2893 retry_after: self.retry_after,
2894 health_check: self.health_check.clone(),
2895 udp: self.udp.clone(),
2896 })
2897 .into(),
2898 ];
2899
2900 for frontend in &self.frontends {
2901 if frontend.udp {
2906 v.push(
2907 RequestType::AddUdpFrontend(RequestUdpFrontend {
2908 cluster_id: self.cluster_id.clone(),
2909 address: frontend.address.into(),
2910 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
2911 })
2912 .into(),
2913 );
2914 } else {
2915 v.push(
2916 RequestType::AddTcpFrontend(RequestTcpFrontend {
2917 cluster_id: self.cluster_id.clone(),
2918 address: frontend.address.into(),
2919 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
2920 sni: frontend.sni.clone(),
2921 alpn: frontend.alpn.clone(),
2922 })
2923 .into(),
2924 );
2925 }
2926 }
2927
2928 for (backend_count, backend) in self.backends.iter().enumerate() {
2929 let load_balancing_parameters = Some(LoadBalancingParams {
2930 weight: backend.weight.unwrap_or(100) as i32,
2931 });
2932
2933 v.push(
2934 RequestType::AddBackend(AddBackend {
2935 cluster_id: self.cluster_id.clone(),
2936 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
2937 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
2938 }),
2939 address: backend.address.into(),
2940 load_balancing_parameters,
2941 sticky_id: backend.sticky_id.clone(),
2942 backup: backend.backup,
2943 })
2944 .into(),
2945 );
2946 }
2947
2948 debug_assert!(
2952 matches!(
2953 v.first().and_then(|r| r.request_type.as_ref()),
2954 Some(RequestType::AddCluster(_))
2955 ),
2956 "TCP cluster orders must lead with an AddCluster"
2957 );
2958 debug_assert_eq!(
2959 v.iter()
2960 .filter(|r| matches!(
2961 r.request_type,
2962 Some(RequestType::AddTcpFrontend(_)) | Some(RequestType::AddUdpFrontend(_))
2963 ))
2964 .count(),
2965 self.frontends.len(),
2966 "one AddTcpFrontend or AddUdpFrontend order per configured frontend"
2967 );
2968 debug_assert_eq!(
2969 v.iter()
2970 .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
2971 .count(),
2972 self.backends.len(),
2973 "one AddBackend order per configured backend"
2974 );
2975 Ok(v)
2976 }
2977}
2978
2979#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2980pub enum ClusterConfig {
2981 Http(HttpClusterConfig),
2982 Tcp(TcpClusterConfig),
2983}
2984
2985impl ClusterConfig {
2986 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2987 match *self {
2988 ClusterConfig::Http(ref http) => http.generate_requests(),
2989 ClusterConfig::Tcp(ref tcp) => tcp.generate_requests(),
2990 }
2991 }
2992}
2993
2994#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
2996pub struct FileConfig {
2997 pub command_socket: Option<String>,
2998 pub command_buffer_size: Option<u64>,
2999 pub max_command_buffer_size: Option<u64>,
3000 pub max_connections: Option<usize>,
3001 pub min_buffers: Option<u64>,
3002 pub max_buffers: Option<u64>,
3003 pub buffer_size: Option<u64>,
3004 #[serde(default)]
3008 pub slab_entries_per_connection: Option<u64>,
3009 #[serde(default)]
3019 pub basic_auth_max_credential_bytes: Option<u64>,
3020 #[serde(default)]
3028 pub max_connections_per_ip: Option<u64>,
3029 #[serde(default)]
3035 pub retry_after: Option<u32>,
3036 #[serde(default)]
3045 pub splice_pipe_capacity_bytes: Option<u64>,
3046 #[serde(default)]
3053 pub command_allowed_uids: Option<Vec<u32>>,
3054 pub saved_state: Option<String>,
3055 #[serde(default)]
3056 pub automatic_state_save: Option<bool>,
3057 pub log_level: Option<String>,
3058 pub log_target: Option<String>,
3059 #[serde(default)]
3060 pub log_colored: bool,
3061 #[serde(default)]
3069 pub audit_logs_target: Option<String>,
3070 #[serde(default)]
3075 pub audit_logs_json_target: Option<String>,
3076 #[serde(default)]
3077 pub access_logs_target: Option<String>,
3078 #[serde(default)]
3079 pub access_logs_format: Option<AccessLogFormat>,
3080 #[serde(default)]
3081 pub access_logs_colored: Option<bool>,
3082 pub worker_count: Option<u16>,
3083 pub worker_automatic_restart: Option<bool>,
3084 pub metrics: Option<MetricsConfig>,
3085 pub disable_cluster_metrics: Option<bool>,
3086 pub listeners: Option<Vec<ListenerBuilder>>,
3087 pub clusters: Option<HashMap<String, FileClusterConfig>>,
3088 pub handle_process_affinity: Option<bool>,
3089 pub ctl_command_timeout: Option<u64>,
3090 pub pid_file_path: Option<String>,
3091 pub activate_listeners: Option<bool>,
3092 #[serde(default)]
3093 pub front_timeout: Option<u32>,
3094 #[serde(default)]
3095 pub back_timeout: Option<u32>,
3096 #[serde(default)]
3097 pub connect_timeout: Option<u32>,
3098 #[serde(default)]
3099 pub zombie_check_interval: Option<u32>,
3100 #[serde(default)]
3101 pub accept_queue_timeout: Option<u32>,
3102 #[serde(default)]
3103 pub evict_on_queue_full: Option<bool>,
3104 #[serde(default)]
3105 pub request_timeout: Option<u32>,
3106 #[serde(default)]
3107 pub worker_timeout: Option<u32>,
3108}
3109
3110impl FileConfig {
3111 pub fn load_from_path(path: &str) -> Result<FileConfig, ConfigError> {
3112 let data = Config::load_file(path)?;
3113
3114 let config: FileConfig = match toml::from_str(&data) {
3115 Ok(config) => config,
3116 Err(e) => {
3117 display_toml_error(&data, &e);
3118 return Err(ConfigError::DeserializeToml(e.to_string()));
3119 }
3120 };
3121
3122 let mut reserved_address: HashSet<SocketAddr> = HashSet::new();
3123
3124 if let Some(listeners) = config.listeners.as_ref() {
3125 for listener in listeners.iter() {
3126 if reserved_address.contains(&listener.address) {
3127 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
3128 }
3129 reserved_address.insert(listener.address);
3130 }
3131 }
3132
3133 Ok(config)
3155 }
3156}
3157
3158pub struct ConfigBuilder {
3160 file: FileConfig,
3161 known_addresses: HashMap<SocketAddr, ListenerProtocol>,
3162 expect_proxy_addresses: HashSet<SocketAddr>,
3163 built: Config,
3164}
3165
3166impl ConfigBuilder {
3167 pub fn new<S>(file_config: FileConfig, config_path: S) -> Self
3171 where
3172 S: ToString,
3173 {
3174 let built = Config {
3175 accept_queue_timeout: file_config
3176 .accept_queue_timeout
3177 .unwrap_or(DEFAULT_ACCEPT_QUEUE_TIMEOUT),
3178 evict_on_queue_full: file_config
3179 .evict_on_queue_full
3180 .unwrap_or(DEFAULT_EVICT_ON_QUEUE_FULL),
3181 activate_listeners: file_config.activate_listeners.unwrap_or(true),
3182 automatic_state_save: file_config
3183 .automatic_state_save
3184 .unwrap_or(DEFAULT_AUTOMATIC_STATE_SAVE),
3185 back_timeout: file_config.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
3186 buffer_size: file_config.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE),
3187 command_buffer_size: file_config
3188 .command_buffer_size
3189 .unwrap_or(DEFAULT_COMMAND_BUFFER_SIZE),
3190 config_path: config_path.to_string(),
3191 connect_timeout: file_config
3192 .connect_timeout
3193 .unwrap_or(DEFAULT_CONNECT_TIMEOUT),
3194 ctl_command_timeout: file_config.ctl_command_timeout.unwrap_or(1_000),
3195 front_timeout: file_config.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
3196 handle_process_affinity: file_config.handle_process_affinity.unwrap_or(false),
3197 access_logs_target: file_config.access_logs_target.clone(),
3198 audit_logs_target: file_config.audit_logs_target.clone(),
3199 audit_logs_json_target: file_config.audit_logs_json_target.clone(),
3200 access_logs_format: file_config.access_logs_format.clone(),
3201 access_logs_colored: file_config.access_logs_colored,
3202 log_level: file_config
3203 .log_level
3204 .clone()
3205 .unwrap_or_else(|| String::from("info")),
3206 log_target: file_config
3207 .log_target
3208 .clone()
3209 .unwrap_or_else(|| String::from("stdout")),
3210 log_colored: file_config.log_colored,
3211 max_buffers: file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
3212 max_command_buffer_size: file_config
3213 .max_command_buffer_size
3214 .unwrap_or(DEFAULT_MAX_COMMAND_BUFFER_SIZE),
3215 max_connections: file_config
3216 .max_connections
3217 .unwrap_or(DEFAULT_MAX_CONNECTIONS),
3218 metrics: file_config.metrics.clone(),
3219 disable_cluster_metrics: file_config
3220 .disable_cluster_metrics
3221 .unwrap_or(DEFAULT_DISABLE_CLUSTER_METRICS),
3222 min_buffers: std::cmp::min(
3223 file_config.min_buffers.unwrap_or(DEFAULT_MIN_BUFFERS),
3224 file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
3225 ),
3226 pid_file_path: file_config.pid_file_path.clone(),
3227 request_timeout: file_config
3228 .request_timeout
3229 .unwrap_or(DEFAULT_REQUEST_TIMEOUT),
3230 saved_state: file_config.saved_state.clone(),
3231 worker_automatic_restart: file_config
3232 .worker_automatic_restart
3233 .unwrap_or(DEFAULT_WORKER_AUTOMATIC_RESTART),
3234 worker_count: file_config.worker_count.unwrap_or(DEFAULT_WORKER_COUNT),
3235 zombie_check_interval: file_config
3236 .zombie_check_interval
3237 .unwrap_or(DEFAULT_ZOMBIE_CHECK_INTERVAL),
3238 worker_timeout: file_config.worker_timeout.unwrap_or(DEFAULT_WORKER_TIMEOUT),
3239 slab_entries_per_connection: file_config.slab_entries_per_connection.map(|n| {
3240 n.clamp(
3241 ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION,
3242 ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION,
3243 )
3244 }),
3245 command_allowed_uids: file_config.command_allowed_uids.clone(),
3246 basic_auth_max_credential_bytes: file_config.basic_auth_max_credential_bytes,
3247 max_connections_per_ip: file_config
3248 .max_connections_per_ip
3249 .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_IP),
3250 retry_after: file_config.retry_after.unwrap_or(DEFAULT_RETRY_AFTER),
3251 splice_pipe_capacity_bytes: file_config.splice_pipe_capacity_bytes,
3252 ..Default::default()
3253 };
3254
3255 debug_assert!(
3259 built.min_buffers <= built.max_buffers,
3260 "min_buffers must be clamped to <= max_buffers in the builder"
3261 );
3262 debug_assert!(
3265 built.slab_entries_per_connection.is_none_or(|n| {
3266 (ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION
3267 ..=ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION)
3268 .contains(&n)
3269 }),
3270 "a set slab_entries_per_connection must be clamped into [MIN, MAX]"
3271 );
3272
3273 Self {
3274 file: file_config,
3275 known_addresses: HashMap::new(),
3276 expect_proxy_addresses: HashSet::new(),
3277 built,
3278 }
3279 }
3280
3281 fn push_tls_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3282 let listener = listener.to_tls(Some(&self.built))?;
3283 self.built.https_listeners.push(listener);
3284 Ok(())
3285 }
3286
3287 fn push_http_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3288 let listener = listener.to_http(Some(&self.built))?;
3289 self.built.http_listeners.push(listener);
3290 Ok(())
3291 }
3292
3293 fn push_tcp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3294 let listener = listener.to_tcp(Some(&self.built))?;
3295 self.built.tcp_listeners.push(listener);
3296 Ok(())
3297 }
3298
3299 fn push_udp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3300 let listener = listener.to_udp(Some(&self.built))?;
3301 self.built.udp_listeners.push(listener);
3302 Ok(())
3303 }
3304
3305 fn populate_listeners(&mut self, listeners: Vec<ListenerBuilder>) -> Result<(), ConfigError> {
3306 for listener in listeners.iter() {
3307 if self.known_addresses.contains_key(&listener.address) {
3308 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
3309 }
3310
3311 let protocol = listener
3312 .protocol
3313 .ok_or(ConfigError::Missing(MissingKind::Protocol))?;
3314
3315 self.known_addresses.insert(listener.address, protocol);
3316 if listener.expect_proxy == Some(true) {
3317 self.expect_proxy_addresses.insert(listener.address);
3318 }
3319
3320 if listener.public_address.is_some() && listener.expect_proxy == Some(true) {
3321 return Err(ConfigError::Incompatible {
3322 object: ObjectKind::Listener,
3323 id: listener.address.to_string(),
3324 kind: IncompatibilityKind::PublicAddress,
3325 });
3326 }
3327
3328 match protocol {
3329 ListenerProtocol::Https => self.push_tls_listener(listener.clone())?,
3330 ListenerProtocol::Http => self.push_http_listener(listener.clone())?,
3331 ListenerProtocol::Tcp => self.push_tcp_listener(listener.clone())?,
3332 ListenerProtocol::Udp => self.push_udp_listener(listener.clone())?,
3333 }
3334 }
3335 Ok(())
3336 }
3337
3338 fn populate_clusters(
3339 &mut self,
3340 mut file_cluster_configs: HashMap<String, FileClusterConfig>,
3341 ) -> Result<(), ConfigError> {
3342 for (id, file_cluster_config) in file_cluster_configs.drain() {
3343 let mut cluster_config =
3344 file_cluster_config.to_cluster_config(id.as_str(), &self.expect_proxy_addresses)?;
3345
3346 match cluster_config {
3347 ClusterConfig::Http(ref mut http) => {
3348 for frontend in http.frontends.iter_mut() {
3349 match self.known_addresses.get(&frontend.address) {
3350 Some(ListenerProtocol::Tcp) => {
3351 return Err(ConfigError::WrongFrontendProtocol(
3352 ListenerProtocol::Tcp,
3353 ));
3354 }
3355 Some(ListenerProtocol::Udp) => {
3356 return Err(ConfigError::WrongFrontendProtocol(
3357 ListenerProtocol::Udp,
3358 ));
3359 }
3360 Some(ListenerProtocol::Http) => {
3361 if frontend.certificate.is_some() {
3362 return Err(ConfigError::WrongFrontendProtocol(
3363 ListenerProtocol::Http,
3364 ));
3365 }
3366 }
3367 Some(ListenerProtocol::Https) => {
3368 if frontend.certificate.is_none() {
3369 if let Some(https_listener) =
3370 self.built.https_listeners.iter().find(|listener| {
3371 listener.address == frontend.address.into()
3372 && listener.certificate.is_some()
3373 })
3374 {
3375 frontend
3377 .certificate
3378 .clone_from(&https_listener.certificate);
3379 frontend.certificate_chain =
3380 Some(https_listener.certificate_chain.clone());
3381 frontend.key.clone_from(&https_listener.key);
3382 }
3383 if frontend.certificate.is_none() {
3384 debug!("known addresses: {:?}", self.known_addresses);
3385 debug!("frontend: {:?}", frontend);
3386 return Err(ConfigError::WrongFrontendProtocol(
3387 ListenerProtocol::Https,
3388 ));
3389 }
3390 }
3391 }
3392 None => {
3393 let file_listener_protocol = if frontend.certificate.is_some() {
3395 self.push_tls_listener(ListenerBuilder::new(
3396 frontend.address.into(),
3397 ListenerProtocol::Https,
3398 ))?;
3399
3400 ListenerProtocol::Https
3401 } else {
3402 self.push_http_listener(ListenerBuilder::new(
3403 frontend.address.into(),
3404 ListenerProtocol::Http,
3405 ))?;
3406
3407 ListenerProtocol::Http
3408 };
3409 self.known_addresses
3410 .insert(frontend.address, file_listener_protocol);
3411 }
3412 }
3413 }
3414 }
3415 ClusterConfig::Tcp(ref mut tcp) => {
3416 for frontend in tcp.frontends.iter_mut() {
3418 match self.known_addresses.get(&frontend.address) {
3419 Some(ListenerProtocol::Http) | Some(ListenerProtocol::Https) => {
3420 return Err(ConfigError::WrongFrontendProtocol(
3421 ListenerProtocol::Http,
3422 ));
3423 }
3424 Some(ListenerProtocol::Udp) => {
3425 frontend.udp = true;
3431 }
3432 Some(ListenerProtocol::Tcp) => {}
3433 None => {
3434 self.push_tcp_listener(ListenerBuilder::new(
3436 frontend.address.into(),
3437 ListenerProtocol::Tcp,
3438 ))?;
3439 self.known_addresses
3440 .insert(frontend.address, ListenerProtocol::Tcp);
3441 }
3442 }
3443 }
3444 }
3445 }
3446
3447 self.built.clusters.insert(id, cluster_config);
3448 }
3449 Ok(())
3450 }
3451
3452 pub fn into_config(&mut self) -> Result<Config, ConfigError> {
3454 if let Some(listeners) = &self.file.listeners {
3455 self.populate_listeners(listeners.clone())?;
3456 }
3457
3458 if let Some(file_cluster_configs) = &self.file.clusters {
3459 self.populate_clusters(file_cluster_configs.clone())?;
3460 }
3461
3462 type SniAlpnByAddress = HashMap<SocketAddr, Vec<(Option<String>, Vec<String>)>>;
3482 let mut frontends_by_address: SniAlpnByAddress = HashMap::new();
3483 let mut addresses_with_no_sni_frontend: HashSet<SocketAddr> = HashSet::new();
3484 for cluster in self.built.clusters.values() {
3485 if let ClusterConfig::Tcp(tcp) = cluster {
3486 for frontend in &tcp.frontends {
3487 if frontend.udp {
3494 continue;
3495 }
3496 if frontend.sni.is_none() {
3497 addresses_with_no_sni_frontend.insert(frontend.address);
3498 }
3499 frontends_by_address
3500 .entry(frontend.address)
3501 .or_default()
3502 .push((frontend.sni.clone(), frontend.alpn.clone()));
3503 }
3504 }
3505 }
3506
3507 let mut addresses_with_sni_frontend: HashSet<SocketAddr> = HashSet::new();
3508 for (address, frontends) in &frontends_by_address {
3509 if !frontends.iter().any(|(sni, _)| sni.is_some()) {
3510 continue;
3511 }
3512 addresses_with_sni_frontend.insert(*address);
3513
3514 if addresses_with_no_sni_frontend.contains(address) {
3515 return Err(ConfigError::TcpListenerMixesSniAndNoSni { address: *address });
3516 }
3517
3518 let mut alpn_lists_by_sni: HashMap<Option<String>, Vec<&Vec<String>>> = HashMap::new();
3519 for (sni, alpn) in frontends {
3520 alpn_lists_by_sni.entry(sni.clone()).or_default().push(alpn);
3521 }
3522 for (sni, alpn_lists) in alpn_lists_by_sni {
3523 let mut seen_protocols: HashSet<&str> = HashSet::new();
3524 let mut catch_all_count = 0usize;
3525 for alpn in alpn_lists {
3526 if alpn.is_empty() {
3527 catch_all_count += 1;
3528 if catch_all_count > 1 {
3529 return Err(ConfigError::TcpFrontendMultipleAlpnCatchAll {
3530 address: *address,
3531 sni: sni.clone(),
3532 });
3533 }
3534 continue;
3535 }
3536 for protocol in alpn {
3537 if !seen_protocols.insert(protocol.as_str()) {
3538 return Err(ConfigError::TcpFrontendAlpnOverlap {
3539 address: *address,
3540 sni: sni.clone(),
3541 protocol: protocol.clone(),
3542 });
3543 }
3544 }
3545 }
3546 }
3547 }
3548
3549 for listener in &self.built.tcp_listeners {
3550 let address: SocketAddr = listener.address.into();
3551 if !addresses_with_sni_frontend.contains(&address) {
3552 continue;
3553 }
3554 let sni_preread_timeout = listener
3555 .sni_preread_timeout
3556 .unwrap_or(DEFAULT_SNI_PREREAD_TIMEOUT);
3557 if sni_preread_timeout > listener.front_timeout {
3558 return Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
3559 address,
3560 sni_preread_timeout,
3561 front_timeout: listener.front_timeout,
3562 });
3563 }
3564 let sni_preread_max_bytes = listener
3565 .sni_preread_max_bytes
3566 .unwrap_or(DEFAULT_SNI_PREREAD_MAX_BYTES);
3567 if sni_preread_max_bytes < MIN_SNI_PREREAD_MAX_BYTES {
3568 return Err(ConfigError::SniPrereadMaxBytesTooSmall {
3569 address,
3570 sni_preread_max_bytes,
3571 minimum: MIN_SNI_PREREAD_MAX_BYTES,
3572 });
3573 }
3574 if u64::from(sni_preread_max_bytes) > self.built.buffer_size {
3575 return Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
3576 address,
3577 sni_preread_max_bytes,
3578 buffer_size: self.built.buffer_size,
3579 });
3580 }
3581 }
3582
3583 let h2_listeners = self
3592 .built
3593 .https_listeners
3594 .iter()
3595 .filter(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
3596 .count();
3597 if h2_listeners > 0 && self.built.buffer_size < H2_MIN_BUFFER_SIZE {
3598 return Err(ConfigError::BufferSizeTooSmallForH2 {
3599 buffer_size: self.built.buffer_size,
3600 minimum: H2_MIN_BUFFER_SIZE,
3601 listeners: h2_listeners,
3602 });
3603 }
3604
3605 if let Some(cap) = self.built.basic_auth_max_credential_bytes {
3615 let third = self.built.buffer_size / 3;
3616 if cap >= third {
3617 warn!(
3618 "basic_auth_max_credential_bytes = {} is >= buffer_size / 3 ({}); \
3619 a hostile peer can pin ~33% of the per-frontend buffer per failed auth \
3620 attempt. Consider lowering basic_auth_max_credential_bytes (typical \
3621 credentials are <100 bytes) or raising buffer_size.",
3622 cap, third
3623 );
3624 }
3625 }
3626
3627 if self.built.evict_on_queue_full && self.built.max_connections < 100 {
3634 let pct = 100usize.div_ceil(self.built.max_connections);
3635 warn!(
3636 "evict_on_queue_full enabled with max_connections = {}; the eviction batch \
3637 clamps to 1, equivalent to ~{}% of capacity per cap event (the knob is \
3638 documented as 1%). Confirm this is intended.",
3639 self.built.max_connections, pct
3640 );
3641 }
3642
3643 let command_socket_path = self.file.command_socket.clone().unwrap_or({
3644 let mut path = env::current_dir().map_err(|e| ConfigError::Env(e.to_string()))?;
3645 path.push("sozu.sock");
3646 let verified_path = path
3647 .to_str()
3648 .ok_or(ConfigError::InvalidPath(path.clone()))?;
3649 verified_path.to_owned()
3650 });
3651
3652 if let (None, Some(true)) = (&self.file.saved_state, &self.file.automatic_state_save) {
3653 return Err(ConfigError::Missing(MissingKind::SavedState));
3654 }
3655
3656 let config = Config {
3657 command_socket: command_socket_path,
3658 ..self.built.clone()
3659 };
3660
3661 debug_assert!(
3667 config.min_buffers <= config.max_buffers,
3668 "min_buffers must not exceed max_buffers"
3669 );
3670 debug_assert!(
3676 !config
3677 .https_listeners
3678 .iter()
3679 .any(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
3680 || config.buffer_size >= H2_MIN_BUFFER_SIZE,
3681 "an h2-advertising config must satisfy the H2 minimum buffer size"
3682 );
3683 Ok(config)
3684 }
3685}
3686
3687#[derive(Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
3691pub struct Config {
3692 pub config_path: String,
3693 pub command_socket: String,
3694 pub command_buffer_size: u64,
3695 pub max_command_buffer_size: u64,
3696 pub max_connections: usize,
3697 pub min_buffers: u64,
3698 pub max_buffers: u64,
3699 pub buffer_size: u64,
3700 pub saved_state: Option<String>,
3701 #[serde(default)]
3702 pub automatic_state_save: bool,
3703 pub log_level: String,
3704 pub log_target: String,
3705 pub log_colored: bool,
3706 #[serde(default)]
3709 pub audit_logs_target: Option<String>,
3710 #[serde(default)]
3713 pub audit_logs_json_target: Option<String>,
3714 #[serde(default)]
3715 pub access_logs_target: Option<String>,
3716 pub access_logs_format: Option<AccessLogFormat>,
3717 pub access_logs_colored: Option<bool>,
3718 pub worker_count: u16,
3719 pub worker_automatic_restart: bool,
3720 pub metrics: Option<MetricsConfig>,
3721 #[serde(default = "default_disable_cluster_metrics")]
3722 pub disable_cluster_metrics: bool,
3723 pub http_listeners: Vec<HttpListenerConfig>,
3724 pub https_listeners: Vec<HttpsListenerConfig>,
3725 pub tcp_listeners: Vec<TcpListenerConfig>,
3726 #[serde(default)]
3727 pub udp_listeners: Vec<UdpListenerConfig>,
3728 pub clusters: HashMap<String, ClusterConfig>,
3729 pub handle_process_affinity: bool,
3730 pub ctl_command_timeout: u64,
3731 pub pid_file_path: Option<String>,
3732 pub activate_listeners: bool,
3733 #[serde(default = "default_front_timeout")]
3734 pub front_timeout: u32,
3735 #[serde(default = "default_back_timeout")]
3736 pub back_timeout: u32,
3737 #[serde(default = "default_connect_timeout")]
3738 pub connect_timeout: u32,
3739 #[serde(default = "default_zombie_check_interval")]
3740 pub zombie_check_interval: u32,
3741 #[serde(default = "default_accept_queue_timeout")]
3742 pub accept_queue_timeout: u32,
3743 #[serde(default = "default_evict_on_queue_full")]
3744 pub evict_on_queue_full: bool,
3745 #[serde(default = "default_request_timeout")]
3746 pub request_timeout: u32,
3747 #[serde(default = "default_worker_timeout")]
3748 pub worker_timeout: u32,
3749 #[serde(default)]
3756 pub slab_entries_per_connection: Option<u64>,
3757 #[serde(default)]
3762 pub command_allowed_uids: Option<Vec<u32>>,
3763 #[serde(default)]
3768 pub basic_auth_max_credential_bytes: Option<u64>,
3769 #[serde(default = "default_max_connections_per_ip")]
3774 pub max_connections_per_ip: u64,
3775 #[serde(default = "default_retry_after")]
3778 pub retry_after: u32,
3779 #[serde(default)]
3786 pub splice_pipe_capacity_bytes: Option<u64>,
3787}
3788
3789fn default_front_timeout() -> u32 {
3790 DEFAULT_FRONT_TIMEOUT
3791}
3792
3793fn default_back_timeout() -> u32 {
3794 DEFAULT_BACK_TIMEOUT
3795}
3796
3797fn default_connect_timeout() -> u32 {
3798 DEFAULT_CONNECT_TIMEOUT
3799}
3800
3801fn default_request_timeout() -> u32 {
3802 DEFAULT_REQUEST_TIMEOUT
3803}
3804
3805fn default_zombie_check_interval() -> u32 {
3806 DEFAULT_ZOMBIE_CHECK_INTERVAL
3807}
3808
3809fn default_accept_queue_timeout() -> u32 {
3810 DEFAULT_ACCEPT_QUEUE_TIMEOUT
3811}
3812
3813fn default_evict_on_queue_full() -> bool {
3814 DEFAULT_EVICT_ON_QUEUE_FULL
3815}
3816
3817fn default_disable_cluster_metrics() -> bool {
3818 DEFAULT_DISABLE_CLUSTER_METRICS
3819}
3820
3821fn default_worker_timeout() -> u32 {
3822 DEFAULT_WORKER_TIMEOUT
3823}
3824
3825fn default_max_connections_per_ip() -> u64 {
3826 DEFAULT_MAX_CONNECTIONS_PER_IP
3827}
3828
3829fn default_retry_after() -> u32 {
3830 DEFAULT_RETRY_AFTER
3831}
3832
3833impl Config {
3834 pub fn load_from_path(path: &str) -> Result<Config, ConfigError> {
3836 let file_config = FileConfig::load_from_path(path)?;
3837
3838 let mut config = ConfigBuilder::new(file_config, path).into_config()?;
3839
3840 config.saved_state = config.saved_state_path()?;
3842
3843 Ok(config)
3844 }
3845
3846 pub fn generate_config_messages(&self) -> Result<Vec<WorkerRequest>, ConfigError> {
3848 let mut v = Vec::new();
3849 let mut count = 0u8;
3850
3851 for listener in &self.http_listeners {
3852 v.push(WorkerRequest {
3853 id: format!("CONFIG-{count}"),
3854 content: RequestType::AddHttpListener(listener.clone()).into(),
3855 });
3856 count += 1;
3857 }
3858
3859 for listener in &self.https_listeners {
3860 v.push(WorkerRequest {
3861 id: format!("CONFIG-{count}"),
3862 content: RequestType::AddHttpsListener(listener.clone()).into(),
3863 });
3864 count += 1;
3865 }
3866
3867 for listener in &self.tcp_listeners {
3868 v.push(WorkerRequest {
3869 id: format!("CONFIG-{count}"),
3870 content: RequestType::AddTcpListener(*listener).into(),
3871 });
3872 count += 1;
3873 }
3874
3875 for listener in &self.udp_listeners {
3876 v.push(WorkerRequest {
3877 id: format!("CONFIG-{count}"),
3878 content: RequestType::AddUdpListener(*listener).into(),
3879 });
3880 count += 1;
3881 }
3882
3883 for cluster in self.clusters.values() {
3884 let mut orders = cluster.generate_requests()?;
3885 for content in orders.drain(..) {
3886 v.push(WorkerRequest {
3887 id: format!("CONFIG-{count}"),
3888 content,
3889 });
3890 count += 1;
3891 }
3892 }
3893
3894 if self.activate_listeners {
3895 for listener in &self.http_listeners {
3896 v.push(WorkerRequest {
3897 id: format!("CONFIG-{count}"),
3898 content: RequestType::ActivateListener(ActivateListener {
3899 address: listener.address,
3900 proxy: ListenerType::Http.into(),
3901 from_scm: false,
3902 })
3903 .into(),
3904 });
3905 count += 1;
3906 }
3907
3908 for listener in &self.https_listeners {
3909 v.push(WorkerRequest {
3910 id: format!("CONFIG-{count}"),
3911 content: RequestType::ActivateListener(ActivateListener {
3912 address: listener.address,
3913 proxy: ListenerType::Https.into(),
3914 from_scm: false,
3915 })
3916 .into(),
3917 });
3918 count += 1;
3919 }
3920
3921 for listener in &self.tcp_listeners {
3922 v.push(WorkerRequest {
3923 id: format!("CONFIG-{count}"),
3924 content: RequestType::ActivateListener(ActivateListener {
3925 address: listener.address,
3926 proxy: ListenerType::Tcp.into(),
3927 from_scm: false,
3928 })
3929 .into(),
3930 });
3931 count += 1;
3932 }
3933
3934 for listener in &self.udp_listeners {
3935 v.push(WorkerRequest {
3936 id: format!("CONFIG-{count}"),
3937 content: RequestType::ActivateListener(ActivateListener {
3938 address: listener.address,
3939 proxy: ListenerType::Udp.into(),
3940 from_scm: false,
3941 })
3942 .into(),
3943 });
3944 count += 1;
3945 }
3946 }
3947
3948 if self.disable_cluster_metrics {
3949 v.push(WorkerRequest {
3950 id: format!("CONFIG-{count}"),
3951 content: RequestType::ConfigureMetrics(MetricsConfiguration::Disabled.into())
3952 .into(),
3953 });
3954 }
3956
3957 Ok(v)
3958 }
3959
3960 pub fn command_socket_path(&self) -> Result<String, ConfigError> {
3962 let config_path_buf = PathBuf::from(self.config_path.clone());
3963 let mut config_dir = config_path_buf
3964 .parent()
3965 .ok_or(ConfigError::NoFileParent(
3966 config_path_buf.to_string_lossy().to_string(),
3967 ))?
3968 .to_path_buf();
3969
3970 let socket_path = PathBuf::from(self.command_socket.clone());
3971
3972 let mut socket_parent_dir = match socket_path.parent() {
3973 None => config_dir,
3976 Some(path) => {
3977 config_dir.push(path);
3979 config_dir.canonicalize().map_err(|io_error| {
3981 ConfigError::SocketPathError(format!(
3982 "Could not canonicalize path {config_dir:?}: {io_error}"
3983 ))
3984 })?
3985 }
3986 };
3987
3988 let socket_name = socket_path
3989 .file_name()
3990 .ok_or(ConfigError::SocketPathError(format!(
3991 "could not get command socket file name from {socket_path:?}"
3992 )))?;
3993
3994 socket_parent_dir.push(socket_name);
3996
3997 let command_socket_path = socket_parent_dir
3998 .to_str()
3999 .ok_or(ConfigError::SocketPathError(format!(
4000 "Invalid socket path {socket_parent_dir:?}"
4001 )))?
4002 .to_string();
4003
4004 Ok(command_socket_path)
4005 }
4006
4007 fn saved_state_path(&self) -> Result<Option<String>, ConfigError> {
4009 let path = match self.saved_state.as_ref() {
4010 Some(path) => path,
4011 None => return Ok(None),
4012 };
4013
4014 debug!("saved_stated path in the config: {}", path);
4015 let config_path = PathBuf::from(self.config_path.clone());
4016
4017 debug!("Config path buffer: {:?}", config_path);
4018 let config_dir = config_path
4019 .parent()
4020 .ok_or(ConfigError::SaveStatePath(format!(
4021 "Could get parent directory of config file {config_path:?}"
4022 )))?;
4023
4024 debug!("Config folder: {:?}", config_dir);
4025 if !config_dir.exists() {
4026 create_dir_all(config_dir).map_err(|io_error| {
4027 ConfigError::SaveStatePath(format!(
4028 "failed to create state parent directory '{config_dir:?}': {io_error}"
4029 ))
4030 })?;
4031 }
4032
4033 let mut saved_state_path_raw = config_dir.to_path_buf();
4034 saved_state_path_raw.push(path);
4035 debug!(
4036 "Looking for saved state on the path {:?}",
4037 saved_state_path_raw
4038 );
4039
4040 match metadata(path) {
4041 Err(err) if matches!(err.kind(), ErrorKind::NotFound) => {
4042 info!("Create an empty state file at '{}'", path);
4043 File::create(path).map_err(|io_error| {
4044 ConfigError::SaveStatePath(format!(
4045 "failed to create state file '{path:?}': {io_error}"
4046 ))
4047 })?;
4048 }
4049 _ => {}
4050 }
4051
4052 saved_state_path_raw.canonicalize().map_err(|io_error| {
4053 ConfigError::SaveStatePath(format!(
4054 "could not get saved state path from config file input {path:?}: {io_error}"
4055 ))
4056 })?;
4057
4058 let stringified_path = saved_state_path_raw
4059 .to_str()
4060 .ok_or(ConfigError::SaveStatePath(format!(
4061 "Invalid path {saved_state_path_raw:?}"
4062 )))?
4063 .to_string();
4064
4065 Ok(Some(stringified_path))
4066 }
4067
4068 pub fn load_file(path: &str) -> Result<String, ConfigError> {
4070 std::fs::read_to_string(path).map_err(|io_error| ConfigError::FileRead {
4071 path_to_read: path.to_owned(),
4072 io_error,
4073 })
4074 }
4075
4076 pub fn load_file_bytes(path: &str) -> Result<Vec<u8>, ConfigError> {
4078 std::fs::read(path).map_err(|io_error| ConfigError::FileRead {
4079 path_to_read: path.to_owned(),
4080 io_error,
4081 })
4082 }
4083}
4084
4085impl fmt::Debug for Config {
4086 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4087 f.debug_struct("Config")
4088 .field("config_path", &self.config_path)
4089 .field("command_socket", &self.command_socket)
4090 .field("command_buffer_size", &self.command_buffer_size)
4091 .field("max_command_buffer_size", &self.max_command_buffer_size)
4092 .field("max_connections", &self.max_connections)
4093 .field("min_buffers", &self.min_buffers)
4094 .field("max_buffers", &self.max_buffers)
4095 .field("buffer_size", &self.buffer_size)
4096 .field("saved_state", &self.saved_state)
4097 .field("automatic_state_save", &self.automatic_state_save)
4098 .field("log_level", &self.log_level)
4099 .field("log_target", &self.log_target)
4100 .field("access_logs_target", &self.access_logs_target)
4101 .field("audit_logs_target", &self.audit_logs_target)
4102 .field("audit_logs_json_target", &self.audit_logs_json_target)
4103 .field("access_logs_format", &self.access_logs_format)
4104 .field("worker_count", &self.worker_count)
4105 .field("worker_automatic_restart", &self.worker_automatic_restart)
4106 .field("metrics", &self.metrics)
4107 .field("disable_cluster_metrics", &self.disable_cluster_metrics)
4108 .field("handle_process_affinity", &self.handle_process_affinity)
4109 .field("ctl_command_timeout", &self.ctl_command_timeout)
4110 .field("pid_file_path", &self.pid_file_path)
4111 .field("activate_listeners", &self.activate_listeners)
4112 .field("front_timeout", &self.front_timeout)
4113 .field("back_timeout", &self.back_timeout)
4114 .field("connect_timeout", &self.connect_timeout)
4115 .field("zombie_check_interval", &self.zombie_check_interval)
4116 .field("accept_queue_timeout", &self.accept_queue_timeout)
4117 .field("evict_on_queue_full", &self.evict_on_queue_full)
4118 .field("request_timeout", &self.request_timeout)
4119 .field("worker_timeout", &self.worker_timeout)
4120 .finish()
4121 }
4122}
4123
4124fn display_toml_error(file: &str, error: &toml::de::Error) {
4125 println!("error parsing the configuration file '{file}': {error}");
4126 if let Some(Range { start, end }) = error.span() {
4127 print!("error parsing the configuration file '{file}' at position: {start}, {end}");
4128 }
4129}
4130
4131impl ServerConfig {
4132 pub const DEFAULT_SLAB_ENTRIES_PER_CONNECTION: u64 = 4;
4139 pub const MIN_SLAB_ENTRIES_PER_CONNECTION: u64 = 2;
4142 pub const MAX_SLAB_ENTRIES_PER_CONNECTION: u64 = 32;
4145
4146 pub fn effective_slab_entries_per_connection(&self) -> u64 {
4149 let effective = match self.slab_entries_per_connection {
4150 Some(0) | None => Self::DEFAULT_SLAB_ENTRIES_PER_CONNECTION,
4151 Some(n) => n.clamp(
4152 Self::MIN_SLAB_ENTRIES_PER_CONNECTION,
4153 Self::MAX_SLAB_ENTRIES_PER_CONNECTION,
4154 ),
4155 };
4156 debug_assert!(
4160 (Self::MIN_SLAB_ENTRIES_PER_CONNECTION..=Self::MAX_SLAB_ENTRIES_PER_CONNECTION)
4161 .contains(&effective),
4162 "effective slab entries per connection must stay within [MIN, MAX]"
4163 );
4164 effective
4165 }
4166
4167 pub fn slab_capacity(&self) -> u64 {
4174 let per_conn = self.effective_slab_entries_per_connection();
4175 let capacity = 10 + per_conn * self.max_connections;
4176 debug_assert!(
4181 capacity >= 10,
4182 "slab capacity must reserve the base entries"
4183 );
4184 debug_assert!(
4185 self.max_connections == 0 || capacity > 10,
4186 "a non-zero connection cap must reserve per-connection slab entries"
4187 );
4188 capacity
4189 }
4190}
4191
4192impl From<&Config> for ServerConfig {
4194 fn from(config: &Config) -> Self {
4195 let metrics = config.metrics.clone().map(|m| ServerMetricsConfig {
4196 address: m.address.to_string(),
4197 tagged_metrics: m.tagged_metrics,
4198 prefix: m.prefix,
4199 detail: Some(MetricDetail::from(m.detail) as i32),
4200 });
4201 let server_config = Self {
4202 max_connections: config.max_connections as u64,
4203 front_timeout: config.front_timeout,
4204 back_timeout: config.back_timeout,
4205 connect_timeout: config.connect_timeout,
4206 zombie_check_interval: config.zombie_check_interval,
4207 accept_queue_timeout: config.accept_queue_timeout,
4208 min_buffers: config.min_buffers,
4209 max_buffers: config.max_buffers,
4210 buffer_size: config.buffer_size,
4211 log_level: config.log_level.clone(),
4212 log_target: config.log_target.clone(),
4213 access_logs_target: config.access_logs_target.clone(),
4214 audit_logs_target: config.audit_logs_target.clone(),
4215 audit_logs_json_target: config.audit_logs_json_target.clone(),
4216 command_buffer_size: config.command_buffer_size,
4217 max_command_buffer_size: config.max_command_buffer_size,
4218 metrics,
4219 access_log_format: ProtobufAccessLogFormat::from(&config.access_logs_format) as i32,
4220 log_colored: config.log_colored,
4221 slab_entries_per_connection: config.slab_entries_per_connection,
4222 basic_auth_max_credential_bytes: config.basic_auth_max_credential_bytes,
4223 evict_on_queue_full: Some(config.evict_on_queue_full),
4224 max_connections_per_ip: Some(config.max_connections_per_ip),
4225 retry_after: Some(config.retry_after),
4226 splice_pipe_capacity_bytes: config.splice_pipe_capacity_bytes,
4227 };
4228
4229 debug_assert!(
4234 server_config.min_buffers <= server_config.max_buffers,
4235 "ServerConfig must preserve min_buffers <= max_buffers"
4236 );
4237 debug_assert_eq!(
4238 server_config.buffer_size, config.buffer_size,
4239 "ServerConfig buffer_size must mirror the source config"
4240 );
4241 debug_assert_eq!(
4242 server_config.max_connections, config.max_connections as u64,
4243 "ServerConfig max_connections must mirror the source config"
4244 );
4245 server_config
4246 }
4247}
4248
4249#[cfg(test)]
4250mod tests {
4251 use toml::to_string;
4252
4253 use super::*;
4254
4255 #[test]
4256 fn hsts_to_proto_enabled_substitutes_default_max_age() {
4257 let cfg = FileHstsConfig {
4258 enabled: Some(true),
4259 max_age: None,
4260 include_subdomains: None,
4261 preload: None,
4262 force_replace_backend: None,
4263 };
4264 let proto = cfg.to_proto("test").expect("should validate");
4265 assert_eq!(proto.enabled, Some(true));
4266 assert_eq!(proto.max_age, Some(DEFAULT_HSTS_MAX_AGE));
4267 }
4268
4269 #[test]
4270 fn hsts_to_proto_explicit_max_age_kept() {
4271 let cfg = FileHstsConfig {
4272 enabled: Some(true),
4273 max_age: Some(63_072_000),
4274 include_subdomains: Some(true),
4275 preload: Some(true),
4276 force_replace_backend: None,
4277 };
4278 let proto = cfg.to_proto("test").expect("should validate");
4279 assert_eq!(proto.max_age, Some(63_072_000));
4280 assert_eq!(proto.include_subdomains, Some(true));
4281 assert_eq!(proto.preload, Some(true));
4282 }
4283
4284 #[test]
4285 fn hsts_to_proto_disabled_keeps_zero_intent() {
4286 let cfg = FileHstsConfig {
4290 enabled: Some(false),
4291 max_age: None,
4292 include_subdomains: None,
4293 preload: None,
4294 force_replace_backend: None,
4295 };
4296 let proto = cfg.to_proto("test").expect("should validate");
4297 assert_eq!(proto.enabled, Some(false));
4298 }
4299
4300 #[test]
4301 fn hsts_to_proto_kill_switch_max_age_zero_allowed() {
4302 let cfg = FileHstsConfig {
4306 enabled: Some(true),
4307 max_age: Some(0),
4308 include_subdomains: None,
4309 preload: None,
4310 force_replace_backend: None,
4311 };
4312 let proto = cfg.to_proto("test").expect("kill-switch must validate");
4313 assert_eq!(proto.max_age, Some(0));
4314 }
4315
4316 #[test]
4317 fn hsts_to_proto_missing_enabled_errors() {
4318 let cfg = FileHstsConfig {
4319 enabled: None,
4320 max_age: Some(31_536_000),
4321 include_subdomains: None,
4322 preload: None,
4323 force_replace_backend: None,
4324 };
4325 match cfg.to_proto("test").unwrap_err() {
4326 ConfigError::HstsEnabledRequired(scope) => assert_eq!(scope, "test"),
4327 other => panic!("expected HstsEnabledRequired, got {other:?}"),
4328 }
4329 }
4330
4331 #[test]
4332 fn hsts_rejected_on_http_listener() {
4333 let mut listener = ListenerBuilder::new(
4338 SocketAddress::new_v4(127, 0, 0, 1, 8080),
4339 ListenerProtocol::Http,
4340 );
4341 listener.hsts = Some(FileHstsConfig {
4342 enabled: Some(true),
4343 max_age: Some(31_536_000),
4344 include_subdomains: None,
4345 preload: None,
4346 force_replace_backend: None,
4347 });
4348 match listener.to_http(None).unwrap_err() {
4349 ConfigError::HstsOnPlainHttp(scope) => assert!(
4350 scope.contains("HTTP listener"),
4351 "expected scope to mention 'HTTP listener', got {scope:?}"
4352 ),
4353 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
4354 }
4355 }
4356
4357 #[test]
4358 fn hsts_rejected_on_http_frontend() {
4359 let frontend = FileClusterFrontendConfig {
4365 address: "127.0.0.1:8080".parse().unwrap(),
4366 hostname: Some("example.com".to_owned()),
4367 alpn: vec![],
4368 path: None,
4369 path_type: None,
4370 method: None,
4371 certificate: None,
4372 key: None,
4373 certificate_chain: None,
4374 tls_versions: vec![],
4375 position: RulePosition::Tree,
4376 tags: None,
4377 redirect: None,
4378 redirect_scheme: None,
4379 redirect_template: None,
4380 rewrite_host: None,
4381 rewrite_path: None,
4382 rewrite_port: None,
4383 required_auth: None,
4384 headers: None,
4385 hsts: Some(FileHstsConfig {
4386 enabled: Some(true),
4387 max_age: Some(31_536_000),
4388 include_subdomains: None,
4389 preload: None,
4390 force_replace_backend: None,
4391 }),
4392 };
4393 match frontend.to_http_front("api").unwrap_err() {
4394 ConfigError::HstsOnPlainHttp(scope) => {
4395 assert!(
4396 scope.contains("api") && scope.contains("example.com"),
4397 "expected scope to mention 'api' and 'example.com', got {scope:?}"
4398 );
4399 }
4400 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
4401 }
4402 }
4403
4404 #[test]
4405 fn serialize() {
4406 let http = ListenerBuilder::new(
4407 SocketAddress::new_v4(127, 0, 0, 1, 8080),
4408 ListenerProtocol::Http,
4409 )
4410 .with_answer_404_path(Some("404.html"))
4411 .to_owned();
4412 println!("http: {:?}", to_string(&http));
4413
4414 let https = ListenerBuilder::new(
4415 SocketAddress::new_v4(127, 0, 0, 1, 8443),
4416 ListenerProtocol::Https,
4417 )
4418 .with_answer_404_path(Some("404.html"))
4419 .to_owned();
4420 println!("https: {:?}", to_string(&https));
4421
4422 let listeners = vec![http, https];
4423 let config = FileConfig {
4424 command_socket: Some(String::from("./command_folder/sock")),
4425 worker_count: Some(2),
4426 worker_automatic_restart: Some(true),
4427 max_connections: Some(500),
4428 min_buffers: Some(1),
4429 max_buffers: Some(500),
4430 buffer_size: Some(16393),
4431 metrics: Some(MetricsConfig {
4432 address: "127.0.0.1:8125".parse().unwrap(),
4433 tagged_metrics: false,
4434 prefix: Some(String::from("sozu-metrics")),
4435 detail: MetricDetailLevel::default(),
4436 }),
4437 listeners: Some(listeners),
4438 ..Default::default()
4439 };
4440
4441 println!("config: {:?}", to_string(&config));
4442 let encoded = to_string(&config).unwrap();
4443 println!("conf:\n{encoded}");
4444 }
4445
4446 #[test]
4447 fn parse() {
4448 let path = "assets/config.toml";
4449 let config = Config::load_from_path(path).unwrap_or_else(|load_error| {
4450 panic!("Cannot load config from path {path}: {load_error:?}")
4451 });
4452 println!("config: {config:#?}");
4453 }
4455
4456 #[test]
4457 fn multiple_listeners_preserve_per_address_expect_proxy() {
4458 let toml_content = r#"
4459 command_socket = "/tmp/sozu_test.sock"
4460 worker_count = 1
4461
4462 [[listeners]]
4463 protocol = "http"
4464 address = "172.16.20.1:80"
4465 expect_proxy = true
4466
4467 [[listeners]]
4468 protocol = "http"
4469 address = "10.22.0.1:80"
4470 expect_proxy = false
4471
4472 [[listeners]]
4473 protocol = "https"
4474 address = "192.168.1.1:443"
4475 expect_proxy = true
4476
4477 [[listeners]]
4478 protocol = "https"
4479 address = "192.168.2.1:443"
4480 expect_proxy = false
4481 "#;
4482
4483 let file_config: FileConfig =
4484 toml::from_str(toml_content).expect("Could not parse TOML config");
4485
4486 let listeners = file_config.listeners.as_ref().expect("No listeners found");
4487 assert_eq!(listeners.len(), 4);
4488
4489 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4490 .into_config()
4491 .expect("Could not build config");
4492
4493 assert_eq!(config.http_listeners.len(), 2);
4494 assert_eq!(config.https_listeners.len(), 2);
4495
4496 let http_proxy = config
4498 .http_listeners
4499 .iter()
4500 .find(|l| SocketAddr::from(l.address) == "172.16.20.1:80".parse().unwrap())
4501 .expect("Listener on 172.16.20.1:80 not found");
4502 let http_direct = config
4503 .http_listeners
4504 .iter()
4505 .find(|l| SocketAddr::from(l.address) == "10.22.0.1:80".parse().unwrap())
4506 .expect("Listener on 10.22.0.1:80 not found");
4507
4508 assert!(http_proxy.expect_proxy);
4509 assert!(!http_direct.expect_proxy);
4510
4511 let https_proxy = config
4513 .https_listeners
4514 .iter()
4515 .find(|l| SocketAddr::from(l.address) == "192.168.1.1:443".parse().unwrap())
4516 .expect("Listener on 192.168.1.1:443 not found");
4517 let https_direct = config
4518 .https_listeners
4519 .iter()
4520 .find(|l| SocketAddr::from(l.address) == "192.168.2.1:443".parse().unwrap())
4521 .expect("Listener on 192.168.2.1:443 not found");
4522
4523 assert!(https_proxy.expect_proxy);
4524 assert!(!https_direct.expect_proxy);
4525 }
4526
4527 #[test]
4528 fn multiple_listeners_generate_correct_worker_requests() {
4529 let toml_content = r#"
4530 command_socket = "/tmp/sozu_test.sock"
4531 worker_count = 1
4532 activate_listeners = true
4533
4534 [[listeners]]
4535 protocol = "http"
4536 address = "172.16.20.1:80"
4537 expect_proxy = true
4538
4539 [[listeners]]
4540 protocol = "http"
4541 address = "10.22.0.1:80"
4542 expect_proxy = false
4543 "#;
4544
4545 let file_config: FileConfig =
4546 toml::from_str(toml_content).expect("Could not parse TOML config");
4547
4548 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4549 .into_config()
4550 .expect("Could not build config");
4551
4552 let messages = config
4553 .generate_config_messages()
4554 .expect("Could not generate config messages");
4555
4556 let add_listener_count = messages
4557 .iter()
4558 .filter(|m| {
4559 matches!(
4560 m.content.request_type,
4561 Some(RequestType::AddHttpListener(_))
4562 )
4563 })
4564 .count();
4565
4566 let activate_listener_count = messages
4567 .iter()
4568 .filter(|m| {
4569 matches!(
4570 m.content.request_type,
4571 Some(RequestType::ActivateListener(ActivateListener {
4572 proxy,
4573 ..
4574 })) if proxy == ListenerType::Http as i32
4575 )
4576 })
4577 .count();
4578
4579 assert_eq!(add_listener_count, 2);
4580 assert_eq!(activate_listener_count, 2);
4581 }
4582
4583 #[test]
4584 fn documented_udp_dns_example_loads_and_emits_udp_requests() {
4585 let toml_content = r#"
4592 command_socket = "/tmp/sozu_test.sock"
4593 worker_count = 1
4594 activate_listeners = true
4595
4596 [[listeners]]
4597 protocol = "udp"
4598 address = "0.0.0.0:53"
4599
4600 [clusters.dns]
4601 protocol = "tcp"
4602 load_balancing = "HRW"
4603 frontends = [
4604 { address = "0.0.0.0:53" }
4605 ]
4606 backends = [
4607 { address = "10.0.0.10:53" },
4608 { address = "10.0.0.11:53" }
4609 ]
4610
4611 [clusters.dns.udp]
4612 affinity_key = "SOURCE_IP"
4613 responses = 1
4614 requests = 0
4615 send_proxy_protocol = true
4616
4617 [clusters.dns.udp.health]
4618 mode = "TCP_PROBE"
4619 tcp_port = 53
4620 rise = 2
4621 fall = 3
4622 fail_open = true
4623 "#;
4624
4625 let file_config: FileConfig =
4626 toml::from_str(toml_content).expect("Could not parse documented DNS TOML");
4627
4628 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4629 .into_config()
4630 .expect("documented UDP DNS example must load without WrongFrontendProtocol");
4631
4632 assert_eq!(
4634 config.udp_listeners.len(),
4635 1,
4636 "the protocol=\"udp\" listener must be built"
4637 );
4638
4639 let messages = config
4640 .generate_config_messages()
4641 .expect("Could not generate config messages");
4642
4643 let add_udp_listener_count = messages
4644 .iter()
4645 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpListener(_))))
4646 .count();
4647 assert_eq!(
4648 add_udp_listener_count, 1,
4649 "must emit exactly one AddUdpListener"
4650 );
4651
4652 let add_udp_frontend_count = messages
4653 .iter()
4654 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
4655 .count();
4656 assert_eq!(
4657 add_udp_frontend_count, 1,
4658 "the cluster frontend on the UDP listener must emit AddUdpFrontend"
4659 );
4660
4661 let add_tcp_frontend_count = messages
4663 .iter()
4664 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddTcpFrontend(_))))
4665 .count();
4666 assert_eq!(
4667 add_tcp_frontend_count, 0,
4668 "a UDP-listener-addressed frontend must not be emitted as AddTcpFrontend"
4669 );
4670
4671 let udp_frontend = messages
4673 .iter()
4674 .find_map(|m| match &m.content.request_type {
4675 Some(RequestType::AddUdpFrontend(f)) => Some(f),
4676 _ => None,
4677 })
4678 .expect("AddUdpFrontend must be present");
4679 assert_eq!(udp_frontend.cluster_id, "dns");
4680 assert_eq!(
4681 SocketAddr::from(udp_frontend.address),
4682 "0.0.0.0:53".parse().unwrap()
4683 );
4684
4685 let cluster = messages
4688 .iter()
4689 .find_map(|m| match &m.content.request_type {
4690 Some(RequestType::AddCluster(c)) if c.cluster_id == "dns" => Some(c),
4691 _ => None,
4692 })
4693 .expect("AddCluster for 'dns' must be present");
4694 let udp = cluster
4695 .udp
4696 .as_ref()
4697 .expect("[clusters.dns.udp] block must carry onto the cluster");
4698 assert_eq!(udp.responses, Some(1));
4699 }
4700
4701 #[test]
4702 fn duplicate_listener_address_rejected() {
4703 let toml_content = r#"
4704 command_socket = "/tmp/sozu_test.sock"
4705 worker_count = 1
4706
4707 [[listeners]]
4708 protocol = "http"
4709 address = "0.0.0.0:80"
4710
4711 [[listeners]]
4712 protocol = "http"
4713 address = "0.0.0.0:80"
4714 "#;
4715
4716 let file_config: FileConfig =
4717 toml::from_str(toml_content).expect("Could not parse TOML config");
4718
4719 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4720
4721 assert!(
4722 result.is_err(),
4723 "Should reject duplicate listener addresses"
4724 );
4725 }
4726
4727 #[test]
4728 fn buffer_size_below_h2_minimum_rejected() {
4729 let toml_content = r#"
4731 command_socket = "/tmp/sozu_test.sock"
4732 worker_count = 1
4733 buffer_size = 8192
4734
4735 [[listeners]]
4736 protocol = "https"
4737 address = "127.0.0.1:8443"
4738 "#;
4739 let file_config: FileConfig =
4740 toml::from_str(toml_content).expect("Could not parse TOML config");
4741 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4742 match result {
4743 Err(ConfigError::BufferSizeTooSmallForH2 {
4744 buffer_size: 8192,
4745 minimum: 16_393,
4746 listeners: 1,
4747 }) => {}
4748 other => panic!("expected BufferSizeTooSmallForH2, got {other:?}"),
4749 }
4750 }
4751
4752 #[test]
4753 fn buffer_size_below_h2_minimum_accepted_when_no_h2_listener() {
4754 let toml_content = r#"
4756 command_socket = "/tmp/sozu_test.sock"
4757 worker_count = 1
4758 buffer_size = 8192
4759
4760 [[listeners]]
4761 protocol = "https"
4762 address = "127.0.0.1:8443"
4763 alpn_protocols = ["http/1.1"]
4764 "#;
4765 let file_config: FileConfig =
4766 toml::from_str(toml_content).expect("Could not parse TOML config");
4767 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4768 assert!(
4769 result.is_ok(),
4770 "non-H2 HTTPS listener with sub-16393 buffer should be accepted: {result:?}"
4771 );
4772 }
4773
4774 #[test]
4775 fn buffer_size_at_h2_minimum_accepted() {
4776 let toml_content = r#"
4777 command_socket = "/tmp/sozu_test.sock"
4778 worker_count = 1
4779 buffer_size = 16393
4780
4781 [[listeners]]
4782 protocol = "https"
4783 address = "127.0.0.1:8443"
4784 "#;
4785 let file_config: FileConfig =
4786 toml::from_str(toml_content).expect("Could not parse TOML config");
4787 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4788 assert!(
4789 result.is_ok(),
4790 "buffer_size at the H2 minimum should be accepted: {result:?}"
4791 );
4792 }
4793
4794 #[test]
4795 fn alpn_protocols_default() {
4796 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4797 let config = builder.to_tls(None).expect("to_tls should succeed");
4798 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4799 }
4800
4801 #[test]
4802 fn alpn_protocols_custom() {
4803 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4804 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned()]));
4805 let config = builder.to_tls(None).expect("to_tls should succeed");
4806 assert_eq!(config.alpn_protocols, vec!["http/1.1"]);
4807 }
4808
4809 #[test]
4810 fn alpn_protocols_invalid_rejected() {
4811 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4812 builder.with_alpn_protocols(Some(vec!["h3".to_owned()]));
4813 let result = builder.to_tls(None);
4814 assert!(result.is_err());
4815 let err = result.unwrap_err();
4816 assert!(
4817 err.to_string().contains("h3"),
4818 "error should mention the invalid protocol: {err}"
4819 );
4820 }
4821
4822 #[test]
4823 fn alpn_protocols_empty_uses_default() {
4824 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4825 builder.with_alpn_protocols(Some(vec![]));
4826 let config = builder.to_tls(None).expect("to_tls should succeed");
4827 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4828 }
4829
4830 #[test]
4831 fn alpn_protocols_deduplicated() {
4832 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4833 builder.with_alpn_protocols(Some(vec![
4834 "h2".to_owned(),
4835 "h2".to_owned(),
4836 "http/1.1".to_owned(),
4837 ]));
4838 let config = builder.to_tls(None).expect("to_tls should succeed");
4839 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4840 }
4841
4842 #[test]
4843 fn alpn_protocols_order_preserved() {
4844 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4845 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned(), "h2".to_owned()]));
4846 let config = builder.to_tls(None).expect("to_tls should succeed");
4847 assert_eq!(config.alpn_protocols, vec!["http/1.1", "h2"]);
4848 }
4849
4850 #[test]
4856 fn parse_header_edit_rejects_crlf_in_value() {
4857 let entry = HeaderEditConfig {
4858 position: "request".to_owned(),
4859 key: "X-Test".to_owned(),
4860 value: "value\r\nEvil-Header: stolen".to_owned(),
4861 };
4862 let err = parse_header_edit(0, &entry).expect_err("CRLF in value must be rejected");
4863 match err {
4864 ConfigError::InvalidHeaderBytes { index, field } => {
4865 assert_eq!(index, 0);
4866 assert_eq!(field, "value");
4867 }
4868 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
4869 }
4870 }
4871
4872 #[test]
4873 fn parse_header_edit_rejects_lf_in_key() {
4874 let entry = HeaderEditConfig {
4875 position: "response".to_owned(),
4876 key: "X-\nTest".to_owned(),
4877 value: "ok".to_owned(),
4878 };
4879 let err = parse_header_edit(2, &entry).expect_err("LF in key must be rejected");
4880 match err {
4881 ConfigError::InvalidHeaderBytes { index, field } => {
4882 assert_eq!(index, 2);
4883 assert_eq!(field, "key");
4884 }
4885 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
4886 }
4887 }
4888
4889 #[test]
4890 fn parse_header_edit_rejects_nul() {
4891 let entry = HeaderEditConfig {
4892 position: "both".to_owned(),
4893 key: "X-Test".to_owned(),
4894 value: "with\0nul".to_owned(),
4895 };
4896 assert!(matches!(
4897 parse_header_edit(0, &entry),
4898 Err(ConfigError::InvalidHeaderBytes { .. })
4899 ));
4900 }
4901
4902 #[test]
4908 fn parse_header_edit_accepts_tab_in_value() {
4909 let entry = HeaderEditConfig {
4910 position: "request".to_owned(),
4911 key: "X-Test".to_owned(),
4912 value: "with\ttab".to_owned(),
4913 };
4914 let header = parse_header_edit(0, &entry).expect("tab in value must be accepted");
4915 assert_eq!(header.val, "with\ttab");
4916 }
4917
4918 #[test]
4925 fn parse_header_edit_rejects_tab_in_key() {
4926 let entry = HeaderEditConfig {
4927 position: "request".to_owned(),
4928 key: "Host\t".to_owned(),
4929 value: "ok".to_owned(),
4930 };
4931 let err = parse_header_edit(0, &entry).expect_err("HTAB in key must be rejected");
4932 match err {
4933 ConfigError::InvalidHeaderBytes { field, .. } => assert_eq!(field, "key"),
4934 other => panic!("expected InvalidHeaderBytes{{field=\"key\"}}, got {other:?}"),
4935 }
4936 }
4937
4938 #[test]
4939 fn parse_header_edit_rejects_space_in_key() {
4940 let entry = HeaderEditConfig {
4941 position: "request".to_owned(),
4942 key: "X Test".to_owned(),
4943 value: "ok".to_owned(),
4944 };
4945 let err = parse_header_edit(0, &entry).expect_err("SP in key must be rejected");
4946 assert!(matches!(err, ConfigError::InvalidHeaderBytes { .. }));
4947 }
4948
4949 #[test]
4950 fn parse_header_edit_rejects_empty_key() {
4951 let entry = HeaderEditConfig {
4952 position: "request".to_owned(),
4953 key: String::new(),
4954 value: "ok".to_owned(),
4955 };
4956 let err = parse_header_edit(0, &entry).expect_err("empty key must be rejected");
4957 assert!(matches!(
4958 err,
4959 ConfigError::InvalidHeaderBytes { field: "key", .. }
4960 ));
4961 }
4962
4963 #[test]
4964 fn parse_header_edit_accepts_clean_value() {
4965 let entry = HeaderEditConfig {
4966 position: "request".to_owned(),
4967 key: "X-Tenant".to_owned(),
4968 value: "alpha".to_owned(),
4969 };
4970 let header = parse_header_edit(0, &entry).expect("clean value must be accepted");
4971 assert_eq!(header.key, "X-Tenant");
4972 assert_eq!(header.val, "alpha");
4973 }
4974
4975 #[test]
4979 fn resolve_answer_source_bare_string_is_literal() {
4980 let body = resolve_answer_source("HTTP/1.1 503 Service Unavailable\r\n\r\nbusy")
4981 .expect("bare-string source must resolve");
4982 assert_eq!(body, "HTTP/1.1 503 Service Unavailable\r\n\r\nbusy");
4983 }
4984
4985 #[test]
4986 fn resolve_answer_source_empty_string_is_legitimate() {
4987 let body = resolve_answer_source("").expect("empty source must resolve");
4988 assert_eq!(body, "");
4989 }
4990
4991 #[test]
4996 fn resolve_answer_source_file_scheme_missing_file_errors() {
4997 let err = resolve_answer_source("file:///nonexistent/sozu-test/never.http")
4998 .expect_err("missing path must error");
4999 assert!(matches!(err, ConfigError::FileOpen { .. }));
5000 }
5001
5002 #[test]
5005 fn resolve_answer_source_file_scheme_empty_path_errors() {
5006 let err = resolve_answer_source("file://").expect_err("empty path must error");
5007 assert!(matches!(err, ConfigError::FileOpen { .. }));
5008 }
5009
5010 #[test]
5018 fn legacy_tcp_frontend_toml_without_sni_alpn_parses_unchanged() {
5019 let toml_content = r#"
5020 command_socket = "/tmp/sozu_test.sock"
5021 worker_count = 1
5022
5023 [[listeners]]
5024 protocol = "tcp"
5025 address = "127.0.0.1:9000"
5026
5027 [clusters.legacy]
5028 protocol = "tcp"
5029 load_balancing = "ROUND_ROBIN"
5030 frontends = [
5031 { address = "127.0.0.1:9000" }
5032 ]
5033 backends = [
5034 { address = "10.0.0.1:9000" }
5035 ]
5036 "#;
5037 let file_config: FileConfig =
5038 toml::from_str(toml_content).expect("Could not parse legacy TCP TOML");
5039 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
5040 .into_config()
5041 .expect("legacy TCP config without sni/alpn must load unchanged");
5042
5043 assert_eq!(config.tcp_listeners.len(), 1);
5044 let listener = &config.tcp_listeners[0];
5045 assert_eq!(
5046 listener.sni_preread_timeout,
5047 Some(DEFAULT_SNI_PREREAD_TIMEOUT),
5048 "proto default sni_preread_timeout must be populated even though unused"
5049 );
5050 assert_eq!(
5051 listener.sni_preread_max_bytes,
5052 Some(DEFAULT_SNI_PREREAD_MAX_BYTES),
5053 "proto default sni_preread_max_bytes must be populated even though unused"
5054 );
5055
5056 let messages = config
5057 .generate_config_messages()
5058 .expect("Could not generate config messages");
5059 let tcp_frontend = messages
5060 .iter()
5061 .find_map(|m| match &m.content.request_type {
5062 Some(RequestType::AddTcpFrontend(f)) => Some(f),
5063 _ => None,
5064 })
5065 .expect("AddTcpFrontend must be present");
5066 assert_eq!(tcp_frontend.sni, None, "legacy frontend must carry no sni");
5067 assert!(
5068 tcp_frontend.alpn.is_empty(),
5069 "legacy frontend must carry no alpn"
5070 );
5071 }
5072
5073 #[test]
5076 fn tcp_frontend_hostname_maps_to_sni_exact_and_wildcard() {
5077 let toml_content = r#"
5078 command_socket = "/tmp/sozu_test.sock"
5079 worker_count = 1
5080
5081 [[listeners]]
5082 protocol = "tcp"
5083 address = "127.0.0.1:9010"
5084
5085 [clusters.exact]
5086 protocol = "tcp"
5087 load_balancing = "ROUND_ROBIN"
5088 frontends = [
5089 { address = "127.0.0.1:9010", hostname = "example.com", alpn = ["h2"] }
5090 ]
5091 backends = [ { address = "10.0.0.1:9010" } ]
5092
5093 [clusters.wildcard]
5094 protocol = "tcp"
5095 load_balancing = "ROUND_ROBIN"
5096 frontends = [
5097 { address = "127.0.0.1:9010", hostname = "*.example.com" }
5098 ]
5099 backends = [ { address = "10.0.0.2:9010" } ]
5100 "#;
5101 let file_config: FileConfig =
5102 toml::from_str(toml_content).expect("Could not parse TOML config");
5103 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
5104 .into_config()
5105 .expect("exact + wildcard SNI frontends on distinct sni must load");
5106
5107 let messages = config
5108 .generate_config_messages()
5109 .expect("Could not generate config messages");
5110 let mut frontends: Vec<_> = messages
5111 .iter()
5112 .filter_map(|m| match &m.content.request_type {
5113 Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
5114 _ => None,
5115 })
5116 .collect();
5117 frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
5118
5119 assert_eq!(frontends.len(), 2);
5120 assert_eq!(frontends[0].sni, Some("example.com".to_string()));
5121 assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
5122 assert_eq!(frontends[1].sni, Some("*.example.com".to_string()));
5123 assert!(frontends[1].alpn.is_empty());
5124 }
5125
5126 #[test]
5131 fn tcp_frontend_invalid_sni_pattern_rejected() {
5132 for invalid in [
5133 "*.*.example.com",
5134 "foo.*.com",
5135 "*",
5136 "example..com",
5137 "",
5138 "/[a-z]+/.example.com",
5139 "foo/bar.example.com",
5140 ] {
5141 let frontend = FileClusterFrontendConfig {
5142 address: "127.0.0.1:8080".parse().unwrap(),
5143 hostname: Some(invalid.to_string()),
5144 alpn: vec![],
5145 path: None,
5146 path_type: None,
5147 method: None,
5148 certificate: None,
5149 key: None,
5150 certificate_chain: None,
5151 tls_versions: vec![],
5152 position: RulePosition::Tree,
5153 tags: None,
5154 redirect: None,
5155 redirect_scheme: None,
5156 redirect_template: None,
5157 rewrite_host: None,
5158 rewrite_path: None,
5159 rewrite_port: None,
5160 required_auth: None,
5161 headers: None,
5162 hsts: None,
5163 };
5164 match frontend.to_tcp_front() {
5165 Err(ConfigError::InvalidSniPattern { sni }) => assert_eq!(sni, invalid),
5166 other => panic!("expected InvalidSniPattern for {invalid:?}, got {other:?}"),
5167 }
5168 }
5169 }
5170
5171 #[test]
5177 fn tcp_frontend_non_ascii_sni_pattern_rejected() {
5178 for non_ascii in ["münchen.example", "*.bücher.example", "日本.example"] {
5179 let frontend = FileClusterFrontendConfig {
5180 address: "127.0.0.1:8080".parse().unwrap(),
5181 hostname: Some(non_ascii.to_string()),
5182 alpn: vec![],
5183 path: None,
5184 path_type: None,
5185 method: None,
5186 certificate: None,
5187 key: None,
5188 certificate_chain: None,
5189 tls_versions: vec![],
5190 position: RulePosition::Tree,
5191 tags: None,
5192 redirect: None,
5193 redirect_scheme: None,
5194 redirect_template: None,
5195 rewrite_host: None,
5196 rewrite_path: None,
5197 rewrite_port: None,
5198 required_auth: None,
5199 headers: None,
5200 hsts: None,
5201 };
5202 match frontend.to_tcp_front() {
5203 Err(ConfigError::NonAsciiSniPattern { sni }) => assert_eq!(sni, non_ascii),
5204 other => panic!("expected NonAsciiSniPattern for {non_ascii:?}, got {other:?}"),
5205 }
5206 }
5207 }
5208
5209 #[test]
5214 fn tcp_frontend_punycode_sni_pattern_accepted() {
5215 assert_eq!(
5216 validate_sni_pattern("xn--mnchen-3ya.example").expect("A-label must be accepted"),
5217 "xn--mnchen-3ya.example"
5218 );
5219 assert_eq!(
5220 validate_sni_pattern("*.xn--bcher-kva.example")
5221 .expect("wildcarded A-label must be accepted"),
5222 "*.xn--bcher-kva.example"
5223 );
5224 assert_eq!(
5225 validate_sni_pattern("XN--MNCHEN-3YA.Example")
5226 .expect("mixed-case A-label must be accepted"),
5227 "xn--mnchen-3ya.example",
5228 "A-label patterns are ASCII-lowercased like any other pattern"
5229 );
5230 }
5231
5232 #[test]
5235 fn alpn_rejected_on_http_frontend() {
5236 let frontend = FileClusterFrontendConfig {
5237 address: "127.0.0.1:8080".parse().unwrap(),
5238 hostname: Some("example.com".to_owned()),
5239 alpn: vec!["h2".to_string()],
5240 path: None,
5241 path_type: None,
5242 method: None,
5243 certificate: None,
5244 key: None,
5245 certificate_chain: None,
5246 tls_versions: vec![],
5247 position: RulePosition::Tree,
5248 tags: None,
5249 redirect: None,
5250 redirect_scheme: None,
5251 redirect_template: None,
5252 rewrite_host: None,
5253 rewrite_path: None,
5254 rewrite_port: None,
5255 required_auth: None,
5256 headers: None,
5257 hsts: None,
5258 };
5259 match frontend.to_http_front("api") {
5260 Err(ConfigError::InvalidFrontendConfig(field)) => assert_eq!(field, "alpn"),
5261 other => panic!("expected InvalidFrontendConfig(\"alpn\"), got {other:?}"),
5262 }
5263 }
5264
5265 #[test]
5271 fn tcp_frontend_alpn_without_sni_rejected() {
5272 let toml_content = r#"
5273 command_socket = "/tmp/sozu_test.sock"
5274 worker_count = 1
5275
5276 [[listeners]]
5277 protocol = "tcp"
5278 address = "127.0.0.1:9019"
5279
5280 [clusters.a]
5281 protocol = "tcp"
5282 load_balancing = "ROUND_ROBIN"
5283 frontends = [
5284 { address = "127.0.0.1:9019", alpn = ["h2"] }
5285 ]
5286 backends = [ { address = "10.0.0.1:9019" } ]
5287 "#;
5288 let file_config: FileConfig =
5289 toml::from_str(toml_content).expect("Could not parse TOML config");
5290 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5291 match result {
5292 Err(ConfigError::AlpnWithoutSni { address }) => {
5293 assert_eq!(address.to_string(), "127.0.0.1:9019");
5294 }
5295 other => panic!("expected AlpnWithoutSni, got {other:?}"),
5296 }
5297 }
5298
5299 #[test]
5303 fn tcp_frontend_alpn_overlap_rejected() {
5304 let toml_content = r#"
5305 command_socket = "/tmp/sozu_test.sock"
5306 worker_count = 1
5307
5308 [[listeners]]
5309 protocol = "tcp"
5310 address = "127.0.0.1:9020"
5311
5312 [clusters.a]
5313 protocol = "tcp"
5314 load_balancing = "ROUND_ROBIN"
5315 frontends = [
5316 { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2"] }
5317 ]
5318 backends = [ { address = "10.0.0.1:9020" } ]
5319
5320 [clusters.b]
5321 protocol = "tcp"
5322 load_balancing = "ROUND_ROBIN"
5323 frontends = [
5324 { address = "127.0.0.1:9020", hostname = "example.com", alpn = ["h2", "http/1.1"] }
5325 ]
5326 backends = [ { address = "10.0.0.2:9020" } ]
5327 "#;
5328 let file_config: FileConfig =
5329 toml::from_str(toml_content).expect("Could not parse TOML config");
5330 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5331 match result {
5332 Err(ConfigError::TcpFrontendAlpnOverlap { protocol, .. }) => {
5333 assert_eq!(protocol, "h2");
5334 }
5335 other => panic!("expected TcpFrontendAlpnOverlap, got {other:?}"),
5336 }
5337 }
5338
5339 #[test]
5343 fn tcp_frontend_multiple_alpn_catch_all_rejected() {
5344 let toml_content = r#"
5345 command_socket = "/tmp/sozu_test.sock"
5346 worker_count = 1
5347
5348 [[listeners]]
5349 protocol = "tcp"
5350 address = "127.0.0.1:9021"
5351
5352 [clusters.a]
5353 protocol = "tcp"
5354 load_balancing = "ROUND_ROBIN"
5355 frontends = [
5356 { address = "127.0.0.1:9021", hostname = "example.com" }
5357 ]
5358 backends = [ { address = "10.0.0.1:9021" } ]
5359
5360 [clusters.b]
5361 protocol = "tcp"
5362 load_balancing = "ROUND_ROBIN"
5363 frontends = [
5364 { address = "127.0.0.1:9021", hostname = "example.com" }
5365 ]
5366 backends = [ { address = "10.0.0.2:9021" } ]
5367 "#;
5368 let file_config: FileConfig =
5369 toml::from_str(toml_content).expect("Could not parse TOML config");
5370 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5371 assert!(
5372 matches!(
5373 result,
5374 Err(ConfigError::TcpFrontendMultipleAlpnCatchAll { .. })
5375 ),
5376 "expected TcpFrontendMultipleAlpnCatchAll, got {result:?}"
5377 );
5378 }
5379
5380 #[test]
5384 fn tcp_listener_mixes_sni_and_no_sni_rejected() {
5385 let toml_content = r#"
5386 command_socket = "/tmp/sozu_test.sock"
5387 worker_count = 1
5388
5389 [[listeners]]
5390 protocol = "tcp"
5391 address = "127.0.0.1:9022"
5392
5393 [clusters.a]
5394 protocol = "tcp"
5395 load_balancing = "ROUND_ROBIN"
5396 frontends = [
5397 { address = "127.0.0.1:9022", hostname = "example.com" }
5398 ]
5399 backends = [ { address = "10.0.0.1:9022" } ]
5400
5401 [clusters.b]
5402 protocol = "tcp"
5403 load_balancing = "ROUND_ROBIN"
5404 frontends = [
5405 { address = "127.0.0.1:9022" }
5406 ]
5407 backends = [ { address = "10.0.0.2:9022" } ]
5408 "#;
5409 let file_config: FileConfig =
5410 toml::from_str(toml_content).expect("Could not parse TOML config");
5411 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5412 assert!(
5413 matches!(result, Err(ConfigError::TcpListenerMixesSniAndNoSni { .. })),
5414 "expected TcpListenerMixesSniAndNoSni, got {result:?}"
5415 );
5416 }
5417
5418 #[test]
5423 fn sni_preread_timeout_exceeding_front_timeout_rejected() {
5424 let toml_content = r#"
5425 command_socket = "/tmp/sozu_test.sock"
5426 worker_count = 1
5427
5428 [[listeners]]
5429 protocol = "tcp"
5430 address = "127.0.0.1:9030"
5431 front_timeout = 2
5432
5433 [clusters.a]
5434 protocol = "tcp"
5435 load_balancing = "ROUND_ROBIN"
5436 frontends = [
5437 { address = "127.0.0.1:9030", hostname = "example.com" }
5438 ]
5439 backends = [ { address = "10.0.0.1:9030" } ]
5440 "#;
5441 let file_config: FileConfig =
5442 toml::from_str(toml_content).expect("Could not parse TOML config");
5443 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5444 match result {
5445 Err(ConfigError::SniPrereadTimeoutExceedsFrontTimeout {
5446 sni_preread_timeout: 5,
5447 front_timeout: 2,
5448 ..
5449 }) => {}
5450 other => panic!("expected SniPrereadTimeoutExceedsFrontTimeout, got {other:?}"),
5451 }
5452 }
5453
5454 #[test]
5458 fn sni_preread_max_bytes_exceeding_buffer_size_rejected() {
5459 let toml_content = r#"
5460 command_socket = "/tmp/sozu_test.sock"
5461 worker_count = 1
5462 buffer_size = 8192
5463
5464 [[listeners]]
5465 protocol = "tcp"
5466 address = "127.0.0.1:9031"
5467
5468 [clusters.a]
5469 protocol = "tcp"
5470 load_balancing = "ROUND_ROBIN"
5471 frontends = [
5472 { address = "127.0.0.1:9031", hostname = "example.com" }
5473 ]
5474 backends = [ { address = "10.0.0.1:9031" } ]
5475 "#;
5476 let file_config: FileConfig =
5477 toml::from_str(toml_content).expect("Could not parse TOML config");
5478 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5479 match result {
5480 Err(ConfigError::SniPrereadMaxBytesExceedsBufferSize {
5481 sni_preread_max_bytes: 16384,
5482 buffer_size: 8192,
5483 ..
5484 }) => {}
5485 other => panic!("expected SniPrereadMaxBytesExceedsBufferSize, got {other:?}"),
5486 }
5487 }
5488
5489 #[test]
5494 fn sni_preread_max_bytes_zero_rejected() {
5495 let toml_content = r#"
5496 command_socket = "/tmp/sozu_test.sock"
5497 worker_count = 1
5498
5499 [[listeners]]
5500 protocol = "tcp"
5501 address = "127.0.0.1:9033"
5502 sni_preread_max_bytes = 0
5503
5504 [clusters.a]
5505 protocol = "tcp"
5506 load_balancing = "ROUND_ROBIN"
5507 frontends = [
5508 { address = "127.0.0.1:9033", hostname = "example.com" }
5509 ]
5510 backends = [ { address = "10.0.0.1:9033" } ]
5511 "#;
5512 let file_config: FileConfig =
5513 toml::from_str(toml_content).expect("Could not parse TOML config");
5514 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5515 match result {
5516 Err(ConfigError::SniPrereadMaxBytesTooSmall {
5517 sni_preread_max_bytes: 0,
5518 minimum: 5,
5519 ..
5520 }) => {}
5521 other => panic!("expected SniPrereadMaxBytesTooSmall, got {other:?}"),
5522 }
5523 }
5524
5525 #[test]
5528 fn sni_preread_max_bytes_at_the_floor_loads() {
5529 let toml_content = r#"
5530 command_socket = "/tmp/sozu_test.sock"
5531 worker_count = 1
5532
5533 [[listeners]]
5534 protocol = "tcp"
5535 address = "127.0.0.1:9034"
5536 sni_preread_max_bytes = 5
5537
5538 [clusters.a]
5539 protocol = "tcp"
5540 load_balancing = "ROUND_ROBIN"
5541 frontends = [
5542 { address = "127.0.0.1:9034", hostname = "example.com" }
5543 ]
5544 backends = [ { address = "10.0.0.1:9034" } ]
5545 "#;
5546 let file_config: FileConfig =
5547 toml::from_str(toml_content).expect("Could not parse TOML config");
5548 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5549 assert!(
5550 result.is_ok(),
5551 "sni_preread_max_bytes at the exact floor must load: {result:?}"
5552 );
5553 }
5554
5555 #[test]
5559 fn sni_preread_validation_ignored_without_sni_frontend() {
5560 let toml_content = r#"
5561 command_socket = "/tmp/sozu_test.sock"
5562 worker_count = 1
5563 buffer_size = 8192
5564
5565 [[listeners]]
5566 protocol = "tcp"
5567 address = "127.0.0.1:9032"
5568 front_timeout = 2
5569
5570 [clusters.a]
5571 protocol = "tcp"
5572 load_balancing = "ROUND_ROBIN"
5573 frontends = [
5574 { address = "127.0.0.1:9032" }
5575 ]
5576 backends = [ { address = "10.0.0.1:9032" } ]
5577 "#;
5578 let file_config: FileConfig =
5579 toml::from_str(toml_content).expect("Could not parse TOML config");
5580 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5581 assert!(
5582 result.is_ok(),
5583 "a no-SNI TCP listener must ignore sni_preread validation entirely: {result:?}"
5584 );
5585 }
5586
5587 #[test]
5594 fn tcp_frontend_disjoint_alpn_same_sni_both_load() {
5595 let toml_content = r#"
5596 command_socket = "/tmp/sozu_test.sock"
5597 worker_count = 1
5598
5599 [[listeners]]
5600 protocol = "tcp"
5601 address = "127.0.0.1:9040"
5602
5603 [clusters.h2_cluster]
5604 protocol = "tcp"
5605 load_balancing = "ROUND_ROBIN"
5606 frontends = [
5607 { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["h2"] }
5608 ]
5609 backends = [ { address = "10.0.0.1:9040" } ]
5610
5611 [clusters.http11_cluster]
5612 protocol = "tcp"
5613 load_balancing = "ROUND_ROBIN"
5614 frontends = [
5615 { address = "127.0.0.1:9040", hostname = "example.com", alpn = ["http/1.1"] }
5616 ]
5617 backends = [ { address = "10.0.0.2:9040" } ]
5618 "#;
5619 let file_config: FileConfig =
5620 toml::from_str(toml_content).expect("Could not parse TOML config");
5621 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
5622 .into_config()
5623 .expect("disjoint non-empty ALPN lists on the same (address, sni) must load");
5624
5625 let messages = config
5626 .generate_config_messages()
5627 .expect("Could not generate config messages");
5628 let mut frontends: Vec<_> = messages
5629 .iter()
5630 .filter_map(|m| match &m.content.request_type {
5631 Some(RequestType::AddTcpFrontend(f)) => Some(f.clone()),
5632 _ => None,
5633 })
5634 .collect();
5635 frontends.sort_by(|a, b| a.cluster_id.cmp(&b.cluster_id));
5636
5637 assert_eq!(frontends.len(), 2, "both frontends must be emitted");
5638 assert_eq!(frontends[0].cluster_id, "h2_cluster");
5639 assert_eq!(frontends[0].sni, Some("example.com".to_string()));
5640 assert_eq!(frontends[0].alpn, vec!["h2".to_string()]);
5641 assert_eq!(frontends[1].cluster_id, "http11_cluster");
5642 assert_eq!(frontends[1].sni, Some("example.com".to_string()));
5643 assert_eq!(frontends[1].alpn, vec!["http/1.1".to_string()]);
5644 }
5645
5646 #[test]
5655 fn udp_routed_tcp_frontends_are_excluded_from_sni_invariants() {
5656 let toml_content = r#"
5657 command_socket = "/tmp/sozu_test.sock"
5658 worker_count = 1
5659
5660 [[listeners]]
5661 protocol = "udp"
5662 address = "127.0.0.1:9050"
5663
5664 [clusters.a]
5665 protocol = "tcp"
5666 load_balancing = "ROUND_ROBIN"
5667 frontends = [
5668 { address = "127.0.0.1:9050", hostname = "example.com" }
5669 ]
5670 backends = [ { address = "10.0.0.1:9050" } ]
5671
5672 [clusters.b]
5673 protocol = "tcp"
5674 load_balancing = "ROUND_ROBIN"
5675 frontends = [
5676 { address = "127.0.0.1:9050" }
5677 ]
5678 backends = [ { address = "10.0.0.2:9050" } ]
5679 "#;
5680 let file_config: FileConfig =
5681 toml::from_str(toml_content).expect("Could not parse TOML config");
5682 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
5683 assert!(
5684 result.is_ok(),
5685 "UDP-routed tcp-cluster frontends must not trip the SNI mixing-ban: {result:?}"
5686 );
5687
5688 let config = result.expect("checked is_ok above");
5689 let messages = config
5690 .generate_config_messages()
5691 .expect("Could not generate config messages");
5692 let add_udp_frontend_count = messages
5693 .iter()
5694 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
5695 .count();
5696 assert_eq!(
5697 add_udp_frontend_count, 2,
5698 "both cluster frontends on the udp listener must emit AddUdpFrontend"
5699 );
5700 }
5701}