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_REQUEST_TIMEOUT: u32 = 10;
127
128pub const DEFAULT_UDP_FRONT_TIMEOUT: u32 = 30;
130
131pub const DEFAULT_UDP_BACK_TIMEOUT: u32 = 30;
133
134pub const DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE: u32 = 1500;
137
138pub const DEFAULT_UDP_MAX_FLOWS: u32 = 0;
141
142pub const DEFAULT_WORKER_TIMEOUT: u32 = 10;
144
145pub const DEFAULT_STICKY_NAME: &str = "SOZUBALANCEID";
147
148pub const DEFAULT_ZOMBIE_CHECK_INTERVAL: u32 = 1_800;
150
151pub const DEFAULT_ACCEPT_QUEUE_TIMEOUT: u32 = 60;
153
154pub const DEFAULT_HSTS_MAX_AGE: u32 = 31_536_000;
161
162pub const DEFAULT_EVICT_ON_QUEUE_FULL: bool = false;
168
169pub const DEFAULT_WORKER_COUNT: u16 = 2;
171
172pub const DEFAULT_WORKER_AUTOMATIC_RESTART: bool = true;
174
175pub const DEFAULT_AUTOMATIC_STATE_SAVE: bool = false;
177
178pub const DEFAULT_MIN_BUFFERS: u64 = 1;
180
181pub const DEFAULT_MAX_BUFFERS: u64 = 1_000;
183
184pub const DEFAULT_BUFFER_SIZE: u64 = 16_393;
186
187pub const H2_MIN_BUFFER_SIZE: u64 = 16_393;
197
198pub const DEFAULT_MAX_CONNECTIONS: usize = 10_000;
200
201pub const DEFAULT_COMMAND_BUFFER_SIZE: u64 = 1_000_000;
203
204pub const DEFAULT_MAX_COMMAND_BUFFER_SIZE: u64 = 2_000_000;
206
207pub const DEFAULT_DISABLE_CLUSTER_METRICS: bool = false;
209
210pub const MAX_LOOP_ITERATIONS: usize = 100000;
211
212pub const DEFAULT_SEND_TLS_13_TICKETS: u64 = 4;
217
218pub const DEFAULT_LOG_TARGET: &str = "stdout";
220
221pub const DEFAULT_MAX_CONNECTIONS_PER_IP: u64 = 0;
226
227pub const DEFAULT_RETRY_AFTER: u32 = 60;
233
234#[derive(Debug)]
235pub enum IncompatibilityKind {
236 PublicAddress,
237 ProxyProtocol,
238}
239
240#[derive(Debug)]
241pub enum MissingKind {
242 Field(String),
243 Protocol,
244 SavedState,
245}
246
247#[derive(thiserror::Error, Debug)]
248pub enum ConfigError {
249 #[error("env path not found: {0}")]
250 Env(String),
251 #[error("Could not open file {path_to_open}: {io_error}")]
252 FileOpen {
253 path_to_open: String,
254 io_error: std::io::Error,
255 },
256 #[error("Could not read file {path_to_read}: {io_error}")]
257 FileRead {
258 path_to_read: String,
259 io_error: std::io::Error,
260 },
261 #[error(
262 "the field {kind:?} of {object:?} with id or address {id} is incompatible with the rest of the options"
263 )]
264 Incompatible {
265 kind: IncompatibilityKind,
266 object: ObjectKind,
267 id: String,
268 },
269 #[error("Invalid '{0}' field for a TCP frontend")]
270 InvalidFrontendConfig(String),
271 #[error("invalid path {0:?}")]
272 InvalidPath(PathBuf),
273 #[error("listening address {0:?} is already used in the configuration")]
274 ListenerAddressAlreadyInUse(SocketAddr),
275 #[error("missing {0:?}")]
276 Missing(MissingKind),
277 #[error("could not get parent directory for file {0}")]
278 NoFileParent(String),
279 #[error("Could not get the path of the saved state")]
280 SaveStatePath(String),
281 #[error("Can not determine path to sozu socket: {0}")]
282 SocketPathError(String),
283 #[error("toml decoding error: {0}")]
284 DeserializeToml(String),
285 #[error("Can not set this frontend on a {0:?} listener")]
286 WrongFrontendProtocol(ListenerProtocol),
287 #[error("Can not build a {expected:?} listener from a {found:?} config")]
288 WrongListenerProtocol {
289 expected: ListenerProtocol,
290 found: Option<ListenerProtocol>,
291 },
292 #[error("Invalid ALPN protocol '{0}'. Valid values: \"h2\", \"http/1.1\"")]
293 InvalidAlpnProtocol(String),
294 #[error(
300 "disable_http11 = true is incompatible with alpn_protocols containing \"http/1.1\" \
301 on listener {address}. The proxy would advertise http/1.1 then refuse every \
302 connection that negotiates it. Drop \"http/1.1\" from alpn_protocols or unset \
303 disable_http11."
304 )]
305 DisableHttp11WithHttp11Alpn { address: String },
306 #[error(
313 "buffer_size = {buffer_size} is below the H2 minimum of {minimum} but \
314 {listeners} HTTPS listener(s) advertise H2 ALPN. The H2 mux deadlocks \
315 on full-size frames with smaller buffers. Raise buffer_size to >= {minimum} \
316 or remove \"h2\" from those listeners' alpn_protocols."
317 )]
318 BufferSizeTooSmallForH2 {
319 buffer_size: u64,
320 minimum: u64,
321 listeners: usize,
322 },
323 #[error(
327 "invalid redirect policy '{0}'. Valid values: \"forward\", \"permanent\", \"unauthorized\""
328 )]
329 InvalidRedirectPolicy(String),
330 #[error(
334 "invalid redirect scheme '{0}'. Valid values: \"use-same\", \"use-http\", \"use-https\""
335 )]
336 InvalidRedirectScheme(String),
337 #[error(
341 "invalid header position '{position}' at headers[{index}]. Valid values: \"request\", \"response\", \"both\""
342 )]
343 InvalidHeaderPosition { index: usize, position: String },
344 #[error(
351 "invalid header bytes in {field} at headers[{index}]: control characters \
352 (NUL / CR / LF / other C0) are forbidden in header keys and values"
353 )]
354 InvalidHeaderBytes { index: usize, field: &'static str },
355 #[error("invalid HSTS config at {0}: `enabled` is required when an [hsts] block is present")]
361 HstsEnabledRequired(String),
362 #[error(
367 "invalid HSTS config at {0}: HSTS is only valid on HTTPS listeners and frontends \
368 (RFC 6797 §7.2 forbids the header over plaintext HTTP)"
369 )]
370 HstsOnPlainHttp(String),
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375#[serde(deny_unknown_fields)]
376pub struct ListenerBuilder {
377 pub address: SocketAddr,
378 pub protocol: Option<ListenerProtocol>,
379 pub public_address: Option<SocketAddr>,
380 pub answer_301: Option<String>,
381 pub answer_400: Option<String>,
382 pub answer_401: Option<String>,
383 pub answer_404: Option<String>,
384 pub answer_408: Option<String>,
385 pub answer_413: Option<String>,
386 pub answer_421: Option<String>,
389 pub answer_502: Option<String>,
390 pub answer_503: Option<String>,
391 pub answer_504: Option<String>,
392 pub answer_507: Option<String>,
393 pub answer_429: Option<String>,
398 pub tls_versions: Option<Vec<TlsVersion>>,
399 pub cipher_list: Option<Vec<String>>,
400 pub cipher_suites: Option<Vec<String>>,
401 pub groups_list: Option<Vec<String>>,
402 pub expect_proxy: Option<bool>,
403 #[serde(default = "default_sticky_name")]
404 pub sticky_name: String,
405 pub certificate: Option<String>,
406 pub certificate_chain: Option<String>,
407 pub key: Option<String>,
408 pub front_timeout: Option<u32>,
410 pub back_timeout: Option<u32>,
412 pub connect_timeout: Option<u32>,
414 pub request_timeout: Option<u32>,
416 pub config: Option<Config>,
418 pub send_tls13_tickets: Option<u64>,
422 pub alpn_protocols: Option<Vec<String>>,
425 pub h2_max_rst_stream_per_window: Option<u32>,
427 pub h2_max_ping_per_window: Option<u32>,
429 pub h2_max_settings_per_window: Option<u32>,
431 pub h2_max_empty_data_per_window: Option<u32>,
433 pub h2_max_window_update_stream0_per_window: Option<u32>,
437 pub sozu_id_header: Option<String>,
441 pub h2_max_continuation_frames: Option<u32>,
443 pub h2_max_glitch_count: Option<u32>,
445 pub h2_initial_connection_window: Option<u32>,
447 pub h2_max_concurrent_streams: Option<u32>,
449 pub h2_stream_shrink_ratio: Option<u32>,
451 pub h2_max_rst_stream_lifetime: Option<u64>,
454 pub h2_max_rst_stream_abusive_lifetime: Option<u64>,
457 pub h2_max_rst_stream_emitted_lifetime: Option<u64>,
461 pub h2_max_header_list_size: Option<u32>,
465 pub h2_max_header_table_size: Option<u32>,
469 pub h2_max_header_fields: Option<u32>,
473 pub h2_stream_idle_timeout_seconds: Option<u32>,
477 pub h2_graceful_shutdown_deadline_seconds: Option<u32>,
482 pub strict_sni_binding: Option<bool>,
488 pub disable_http11: Option<bool>,
493 pub elide_x_real_ip: Option<bool>,
497 pub send_x_real_ip: Option<bool>,
502 pub answers: Option<BTreeMap<String, String>>,
517 pub hsts: Option<FileHstsConfig>,
523 pub max_rx_datagram_size: Option<u32>,
527 pub max_flows: Option<u32>,
532}
533
534pub fn default_sticky_name() -> String {
535 DEFAULT_STICKY_NAME.to_string()
536}
537
538impl ListenerBuilder {
539 pub fn new_http(address: SocketAddress) -> ListenerBuilder {
542 Self::new(address, ListenerProtocol::Http)
543 }
544
545 pub fn new_tcp(address: SocketAddress) -> ListenerBuilder {
548 Self::new(address, ListenerProtocol::Tcp)
549 }
550
551 pub fn new_https(address: SocketAddress) -> ListenerBuilder {
554 Self::new(address, ListenerProtocol::Https)
555 }
556
557 pub fn new_udp(address: SocketAddress) -> ListenerBuilder {
560 Self::new(address, ListenerProtocol::Udp)
561 }
562
563 fn new(address: SocketAddress, protocol: ListenerProtocol) -> ListenerBuilder {
565 ListenerBuilder {
566 address: address.into(),
567 answer_301: None,
568 answer_401: None,
569 answer_400: None,
570 answer_404: None,
571 answer_408: None,
572 answer_413: None,
573 answer_421: None,
574 answer_502: None,
575 answer_503: None,
576 answer_504: None,
577 answer_507: None,
578 answer_429: None,
579 back_timeout: None,
580 certificate_chain: None,
581 certificate: None,
582 cipher_list: None,
583 cipher_suites: None,
584 groups_list: None,
585 config: None,
586 connect_timeout: None,
587 expect_proxy: None,
588 front_timeout: None,
589 key: None,
590 protocol: Some(protocol),
591 public_address: None,
592 request_timeout: None,
593 send_tls13_tickets: None,
594 sticky_name: DEFAULT_STICKY_NAME.to_string(),
595 tls_versions: None,
596 alpn_protocols: None,
597 h2_max_rst_stream_per_window: None,
598 h2_max_ping_per_window: None,
599 h2_max_settings_per_window: None,
600 h2_max_empty_data_per_window: None,
601 h2_max_window_update_stream0_per_window: None,
602 sozu_id_header: None,
603 h2_max_continuation_frames: None,
604 h2_max_glitch_count: None,
605 h2_initial_connection_window: None,
606 h2_max_concurrent_streams: None,
607 h2_stream_shrink_ratio: None,
608 h2_max_rst_stream_lifetime: None,
609 h2_max_rst_stream_abusive_lifetime: None,
610 h2_max_rst_stream_emitted_lifetime: None,
611 h2_max_header_list_size: None,
612 h2_max_header_table_size: None,
613 h2_max_header_fields: None,
614 h2_stream_idle_timeout_seconds: None,
615 h2_graceful_shutdown_deadline_seconds: None,
616 strict_sni_binding: None,
617 disable_http11: None,
618 elide_x_real_ip: None,
619 send_x_real_ip: None,
620 answers: None,
621 hsts: None,
622 max_rx_datagram_size: None,
623 max_flows: None,
624 }
625 }
626
627 pub fn with_public_address(&mut self, public_address: Option<SocketAddr>) -> &mut Self {
628 if let Some(address) = public_address {
629 self.public_address = Some(address);
630 }
631 self
632 }
633
634 pub fn with_answer_404_path<S>(&mut self, answer_404_path: Option<S>) -> &mut Self
635 where
636 S: ToString,
637 {
638 if let Some(path) = answer_404_path {
639 self.answer_404 = Some(path.to_string());
640 }
641 self
642 }
643
644 pub fn with_answer_503_path<S>(&mut self, answer_503_path: Option<S>) -> &mut Self
645 where
646 S: ToString,
647 {
648 if let Some(path) = answer_503_path {
649 self.answer_503 = Some(path.to_string());
650 }
651 self
652 }
653
654 pub fn with_tls_versions(&mut self, tls_versions: Vec<TlsVersion>) -> &mut Self {
655 self.tls_versions = Some(tls_versions);
656 self
657 }
658
659 pub fn with_cipher_list(&mut self, cipher_list: Option<Vec<String>>) -> &mut Self {
660 self.cipher_list = cipher_list;
661 self
662 }
663
664 pub fn with_cipher_suites(&mut self, cipher_suites: Option<Vec<String>>) -> &mut Self {
665 self.cipher_suites = cipher_suites;
666 self
667 }
668
669 pub fn with_alpn_protocols(&mut self, alpn_protocols: Option<Vec<String>>) -> &mut Self {
670 self.alpn_protocols = alpn_protocols;
671 self
672 }
673
674 pub fn with_elide_x_real_ip(&mut self, elide_x_real_ip: bool) -> &mut Self {
677 self.elide_x_real_ip = Some(elide_x_real_ip);
678 self
679 }
680
681 pub fn with_send_x_real_ip(&mut self, send_x_real_ip: bool) -> &mut Self {
685 self.send_x_real_ip = Some(send_x_real_ip);
686 self
687 }
688
689 pub fn with_expect_proxy(&mut self, expect_proxy: bool) -> &mut Self {
690 self.expect_proxy = Some(expect_proxy);
691 self
692 }
693
694 pub fn with_sticky_name<S>(&mut self, sticky_name: Option<S>) -> &mut Self
695 where
696 S: ToString,
697 {
698 if let Some(name) = sticky_name {
699 self.sticky_name = name.to_string();
700 }
701 self
702 }
703
704 pub fn with_certificate<S>(&mut self, certificate: S) -> &mut Self
705 where
706 S: ToString,
707 {
708 self.certificate = Some(certificate.to_string());
709 self
710 }
711
712 pub fn with_certificate_chain(&mut self, certificate_chain: String) -> &mut Self {
713 self.certificate = Some(certificate_chain);
714 self
715 }
716
717 pub fn with_key<S>(&mut self, key: String) -> &mut Self
718 where
719 S: ToString,
720 {
721 self.key = Some(key);
722 self
723 }
724
725 pub fn with_front_timeout(&mut self, front_timeout: Option<u32>) -> &mut Self {
726 self.front_timeout = front_timeout;
727 self
728 }
729
730 pub fn with_back_timeout(&mut self, back_timeout: Option<u32>) -> &mut Self {
731 self.back_timeout = back_timeout;
732 self
733 }
734
735 pub fn with_connect_timeout(&mut self, connect_timeout: Option<u32>) -> &mut Self {
736 self.connect_timeout = connect_timeout;
737 self
738 }
739
740 pub fn with_request_timeout(&mut self, request_timeout: Option<u32>) -> &mut Self {
741 self.request_timeout = request_timeout;
742 self
743 }
744
745 pub fn with_answer<S, P>(&mut self, code: S, path: P) -> &mut Self
750 where
751 S: ToString,
752 P: ToString,
753 {
754 self.answers
755 .get_or_insert_with(BTreeMap::new)
756 .insert(code.to_string(), path.to_string());
757 self
758 }
759
760 pub fn with_answers(&mut self, answers: BTreeMap<String, String>) -> &mut Self {
763 self.answers = Some(answers);
764 self
765 }
766
767 fn get_http_answers(&self) -> Result<Option<CustomHttpAnswers>, ConfigError> {
769 let http_answers = CustomHttpAnswers {
770 answer_301: read_http_answer_file(&self.answer_301)?,
771 answer_400: read_http_answer_file(&self.answer_400)?,
772 answer_401: read_http_answer_file(&self.answer_401)?,
773 answer_404: read_http_answer_file(&self.answer_404)?,
774 answer_408: read_http_answer_file(&self.answer_408)?,
775 answer_413: read_http_answer_file(&self.answer_413)?,
776 answer_421: read_http_answer_file(&self.answer_421)?,
777 answer_502: read_http_answer_file(&self.answer_502)?,
778 answer_503: read_http_answer_file(&self.answer_503)?,
779 answer_504: read_http_answer_file(&self.answer_504)?,
780 answer_507: read_http_answer_file(&self.answer_507)?,
781 answer_429: read_http_answer_file(&self.answer_429)?,
782 };
783 Ok(Some(http_answers))
784 }
785
786 fn get_listener_answers(&self) -> Result<BTreeMap<String, String>, ConfigError> {
794 let mut out = BTreeMap::new();
795
796 macro_rules! merge_legacy {
800 ($code:literal, $field:ident) => {
801 if let Some(body) = read_http_answer_file(&self.$field)? {
802 out.insert($code.to_owned(), body);
803 }
804 };
805 }
806 merge_legacy!("301", answer_301);
807 merge_legacy!("400", answer_400);
808 merge_legacy!("401", answer_401);
809 merge_legacy!("404", answer_404);
810 merge_legacy!("408", answer_408);
811 merge_legacy!("413", answer_413);
812 merge_legacy!("421", answer_421);
813 merge_legacy!("502", answer_502);
814 merge_legacy!("503", answer_503);
815 merge_legacy!("504", answer_504);
816 merge_legacy!("507", answer_507);
817 merge_legacy!("429", answer_429);
818
819 if let Some(map) = &self.answers {
820 let loaded = load_answers(map)?;
821 out.extend(loaded);
822 }
823 Ok(out)
824 }
825
826 fn assign_config_timeouts(&mut self, config: &Config) {
828 self.front_timeout = Some(self.front_timeout.unwrap_or(config.front_timeout));
829 self.back_timeout = Some(self.back_timeout.unwrap_or(config.back_timeout));
830 self.connect_timeout = Some(self.connect_timeout.unwrap_or(config.connect_timeout));
831 self.request_timeout = Some(self.request_timeout.unwrap_or(config.request_timeout));
832 }
833
834 pub fn to_http(&mut self, config: Option<&Config>) -> Result<HttpListenerConfig, ConfigError> {
836 if self.protocol != Some(ListenerProtocol::Http) {
837 return Err(ConfigError::WrongListenerProtocol {
838 expected: ListenerProtocol::Http,
839 found: self.protocol.to_owned(),
840 });
841 }
842
843 if self.hsts.is_some() {
849 return Err(ConfigError::HstsOnPlainHttp(format!(
850 "HTTP listener {}",
851 self.address
852 )));
853 }
854
855 if let Some(config) = config {
856 self.assign_config_timeouts(config);
857 }
858
859 let http_answers = self.get_http_answers()?;
860 let answers = self.get_listener_answers()?;
861
862 let configuration = HttpListenerConfig {
863 address: self.address.into(),
864 public_address: self.public_address.map(|a| a.into()),
865 expect_proxy: self.expect_proxy.unwrap_or(false),
866 sticky_name: self.sticky_name.clone(),
867 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
868 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
869 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
870 request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
871 http_answers,
872 answers,
873 h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
874 h2_max_ping_per_window: self.h2_max_ping_per_window,
875 h2_max_settings_per_window: self.h2_max_settings_per_window,
876 h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
877 h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
878 h2_max_continuation_frames: self.h2_max_continuation_frames,
879 h2_max_glitch_count: self.h2_max_glitch_count,
880 h2_initial_connection_window: self.h2_initial_connection_window,
881 h2_max_concurrent_streams: self.h2_max_concurrent_streams,
882 h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
883 h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
884 h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
885 h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
886 h2_max_header_list_size: self.h2_max_header_list_size,
887 h2_max_header_table_size: self.h2_max_header_table_size,
888 h2_max_header_fields: self.h2_max_header_fields,
889 h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
890 h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
891 sozu_id_header: self.sozu_id_header.clone(),
892 elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
893 send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
894 ..Default::default()
895 };
896
897 debug_assert_eq!(
902 configuration.address,
903 self.address.into(),
904 "HTTP listener must bind the requested address"
905 );
906 Ok(configuration)
907 }
908
909 pub fn to_tls(&mut self, config: Option<&Config>) -> Result<HttpsListenerConfig, ConfigError> {
911 if self.protocol != Some(ListenerProtocol::Https) {
912 return Err(ConfigError::WrongListenerProtocol {
913 expected: ListenerProtocol::Https,
914 found: self.protocol.to_owned(),
915 });
916 }
917
918 let default_cipher_list = DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect();
919
920 let cipher_list = self.cipher_list.clone().unwrap_or(default_cipher_list);
921
922 let cipher_suites = self
923 .cipher_suites
924 .clone()
925 .unwrap_or_else(|| DEFAULT_CIPHER_LIST.into_iter().map(String::from).collect());
926
927 let signature_algorithms: Vec<String> = DEFAULT_SIGNATURE_ALGORITHMS
928 .into_iter()
929 .map(String::from)
930 .collect();
931
932 let groups_list = self
933 .groups_list
934 .clone()
935 .unwrap_or_else(|| DEFAULT_GROUPS_LIST.into_iter().map(String::from).collect());
936
937 let alpn_protocols: Vec<String> = match &self.alpn_protocols {
938 Some(protos) if !protos.is_empty() => {
939 for proto in protos {
940 match proto.as_str() {
941 "h2" | "http/1.1" => {}
942 other => return Err(ConfigError::InvalidAlpnProtocol(other.to_owned())),
943 }
944 }
945 if self.disable_http11.unwrap_or(false) && protos.iter().any(|p| p == "http/1.1") {
950 return Err(ConfigError::DisableHttp11WithHttp11Alpn {
951 address: self.address.to_string(),
952 });
953 }
954 if !protos.iter().any(|p| p == "http/1.1") {
955 warn!(
956 "ALPN protocols do not include 'http/1.1'. Clients without H2 support will fail TLS negotiation."
957 );
958 }
959 let mut seen = std::collections::HashSet::new();
961 protos
962 .iter()
963 .filter(|p| seen.insert(p.as_str()))
964 .cloned()
965 .collect()
966 }
967 _ => {
968 if self.disable_http11.unwrap_or(false)
972 && DEFAULT_ALPN_PROTOCOLS.contains(&"http/1.1")
973 {
974 return Err(ConfigError::DisableHttp11WithHttp11Alpn {
975 address: self.address.to_string(),
976 });
977 }
978 DEFAULT_ALPN_PROTOCOLS
979 .iter()
980 .map(|s| s.to_string())
981 .collect()
982 }
983 };
984
985 let versions = match self.tls_versions {
986 None => vec![TlsVersion::TlsV12 as i32, TlsVersion::TlsV13 as i32],
987 Some(ref v) => v.iter().map(|v| *v as i32).collect(),
988 };
989
990 let key = self.key.as_ref().and_then(|path| {
991 Config::load_file(path)
992 .map_err(|e| {
993 error!("cannot load key at path '{}': {:?}", path, e);
994 e
995 })
996 .ok()
997 });
998 let certificate = self.certificate.as_ref().and_then(|path| {
999 Config::load_file(path)
1000 .map_err(|e| {
1001 error!("cannot load certificate at path '{}': {:?}", path, e);
1002 e
1003 })
1004 .ok()
1005 });
1006 let certificate_chain = self
1007 .certificate_chain
1008 .as_ref()
1009 .and_then(|path| {
1010 Config::load_file(path)
1011 .map_err(|e| {
1012 error!("cannot load certificate chain at path '{}': {:?}", path, e);
1013 e
1014 })
1015 .ok()
1016 })
1017 .map(split_certificate_chain)
1018 .unwrap_or_default();
1019
1020 let http_answers = self.get_http_answers()?;
1021 let answers = self.get_listener_answers()?;
1022
1023 if let Some(config) = config {
1024 self.assign_config_timeouts(config);
1025 }
1026
1027 let https_listener_config = HttpsListenerConfig {
1028 address: self.address.into(),
1029 sticky_name: self.sticky_name.clone(),
1030 public_address: self.public_address.map(|a| a.into()),
1031 cipher_list,
1032 versions,
1033 expect_proxy: self.expect_proxy.unwrap_or(false),
1034 key,
1035 certificate,
1036 certificate_chain,
1037 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1038 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1039 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1040 request_timeout: self.request_timeout.unwrap_or(DEFAULT_REQUEST_TIMEOUT),
1041 cipher_suites,
1042 signature_algorithms,
1043 groups_list,
1044 active: false,
1045 send_tls13_tickets: self
1046 .send_tls13_tickets
1047 .unwrap_or(DEFAULT_SEND_TLS_13_TICKETS),
1048 http_answers,
1049 answers,
1050 alpn_protocols,
1051 h2_max_rst_stream_per_window: self.h2_max_rst_stream_per_window,
1052 h2_max_ping_per_window: self.h2_max_ping_per_window,
1053 h2_max_settings_per_window: self.h2_max_settings_per_window,
1054 h2_max_empty_data_per_window: self.h2_max_empty_data_per_window,
1055 h2_max_window_update_stream0_per_window: self.h2_max_window_update_stream0_per_window,
1056 h2_max_continuation_frames: self.h2_max_continuation_frames,
1057 h2_max_glitch_count: self.h2_max_glitch_count,
1058 h2_initial_connection_window: self.h2_initial_connection_window,
1059 h2_max_concurrent_streams: self.h2_max_concurrent_streams,
1060 h2_stream_shrink_ratio: self.h2_stream_shrink_ratio,
1061 h2_max_rst_stream_lifetime: self.h2_max_rst_stream_lifetime,
1062 h2_max_rst_stream_abusive_lifetime: self.h2_max_rst_stream_abusive_lifetime,
1063 h2_max_rst_stream_emitted_lifetime: self.h2_max_rst_stream_emitted_lifetime,
1064 h2_max_header_list_size: self.h2_max_header_list_size,
1065 h2_max_header_table_size: self.h2_max_header_table_size,
1066 h2_max_header_fields: self.h2_max_header_fields,
1067 strict_sni_binding: self.strict_sni_binding,
1068 disable_http11: self.disable_http11,
1069 h2_stream_idle_timeout_seconds: self.h2_stream_idle_timeout_seconds,
1070 h2_graceful_shutdown_deadline_seconds: self.h2_graceful_shutdown_deadline_seconds,
1071 sozu_id_header: self.sozu_id_header.clone(),
1072 elide_x_real_ip: Some(self.elide_x_real_ip.unwrap_or(false)),
1073 send_x_real_ip: Some(self.send_x_real_ip.unwrap_or(false)),
1074 hsts: match self.hsts.as_ref() {
1075 Some(h) => Some(h.to_proto("listener")?),
1076 None => None,
1077 },
1078 };
1079
1080 debug_assert_eq!(
1084 https_listener_config.address,
1085 self.address.into(),
1086 "HTTPS listener must bind the requested address"
1087 );
1088 debug_assert!(
1089 !https_listener_config.active,
1090 "a freshly built HTTPS listener must start inactive"
1091 );
1092 debug_assert!(
1097 !https_listener_config.alpn_protocols.is_empty(),
1098 "resolved ALPN list must not be empty"
1099 );
1100 debug_assert!(
1101 https_listener_config
1102 .alpn_protocols
1103 .iter()
1104 .all(|p| p == "h2" || p == "http/1.1"),
1105 "resolved ALPN list must contain only h2 and http/1.1"
1106 );
1107 debug_assert!(
1108 {
1109 let mut seen = std::collections::HashSet::new();
1110 https_listener_config
1111 .alpn_protocols
1112 .iter()
1113 .all(|p| seen.insert(p))
1114 },
1115 "resolved ALPN list must be duplicate-free"
1116 );
1117 debug_assert!(
1120 !(self.disable_http11.unwrap_or(false)
1121 && https_listener_config
1122 .alpn_protocols
1123 .iter()
1124 .any(|p| p == "http/1.1")),
1125 "disable_http11 with http/1.1 in ALPN must have been rejected"
1126 );
1127 Ok(https_listener_config)
1128 }
1129
1130 pub fn to_tcp(&mut self, config: Option<&Config>) -> Result<TcpListenerConfig, ConfigError> {
1132 if self.protocol != Some(ListenerProtocol::Tcp) {
1133 return Err(ConfigError::WrongListenerProtocol {
1134 expected: ListenerProtocol::Tcp,
1135 found: self.protocol.to_owned(),
1136 });
1137 }
1138
1139 if let Some(config) = config {
1140 self.assign_config_timeouts(config);
1141 }
1142
1143 let tcp_listener_config = TcpListenerConfig {
1144 address: self.address.into(),
1145 public_address: self.public_address.map(|a| a.into()),
1146 expect_proxy: self.expect_proxy.unwrap_or(false),
1147 front_timeout: self.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
1148 back_timeout: self.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
1149 connect_timeout: self.connect_timeout.unwrap_or(DEFAULT_CONNECT_TIMEOUT),
1150 active: false,
1151 };
1152
1153 debug_assert_eq!(
1156 tcp_listener_config.address,
1157 self.address.into(),
1158 "TCP listener must bind the requested address"
1159 );
1160 debug_assert!(
1161 !tcp_listener_config.active,
1162 "a freshly built TCP listener must start inactive"
1163 );
1164 Ok(tcp_listener_config)
1165 }
1166
1167 pub fn to_udp(&mut self, config: Option<&Config>) -> Result<UdpListenerConfig, ConfigError> {
1185 if self.protocol != Some(ListenerProtocol::Udp) {
1186 return Err(ConfigError::WrongListenerProtocol {
1187 expected: ListenerProtocol::Udp,
1188 found: self.protocol.to_owned(),
1189 });
1190 }
1191
1192 let mut max_rx_datagram_size = self
1193 .max_rx_datagram_size
1194 .unwrap_or(DEFAULT_UDP_MAX_RX_DATAGRAM_SIZE);
1195 let buffer_size = config.map(|c| c.buffer_size).unwrap_or(DEFAULT_BUFFER_SIZE);
1196 if u64::from(max_rx_datagram_size) > buffer_size {
1197 warn!(
1198 "UDP listener {}: max_rx_datagram_size = {} exceeds buffer_size = {}, clamping to buffer_size",
1199 self.address, max_rx_datagram_size, buffer_size
1200 );
1201 max_rx_datagram_size = buffer_size as u32;
1202 }
1203
1204 let max_flows = self.max_flows.unwrap_or(DEFAULT_UDP_MAX_FLOWS);
1205 if max_flows > 0
1206 && let Some(soft_limit) = soft_rlimit_nofile()
1207 {
1208 let advisory = soft_limit.saturating_mul(7) / 10;
1209 if u64::from(max_flows) > advisory {
1210 warn!(
1211 "UDP listener {}: max_flows = {} exceeds ~70% of the soft RLIMIT_NOFILE ({}); \
1212 per-flow connected sockets may hit EMFILE",
1213 self.address, max_flows, advisory
1214 );
1215 }
1216 }
1217
1218 Ok(UdpListenerConfig {
1219 address: self.address.into(),
1220 public_address: self.public_address.map(|a| a.into()),
1221 front_timeout: self.front_timeout.unwrap_or(DEFAULT_UDP_FRONT_TIMEOUT),
1222 back_timeout: self.back_timeout.unwrap_or(DEFAULT_UDP_BACK_TIMEOUT),
1223 max_rx_datagram_size,
1224 max_flows,
1225 active: false,
1226 })
1227 }
1228}
1229
1230fn soft_rlimit_nofile() -> Option<u64> {
1235 let mut limit = libc::rlimit {
1236 rlim_cur: 0,
1237 rlim_max: 0,
1238 };
1239 let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) };
1243 if rc == 0 {
1244 Some(limit.rlim_cur)
1247 } else {
1248 None
1249 }
1250}
1251
1252fn read_http_answer_file(path: &Option<String>) -> Result<Option<String>, ConfigError> {
1254 match path {
1255 Some(path) => {
1256 let mut content = String::new();
1257 let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
1258 path_to_open: path.to_owned(),
1259 io_error,
1260 })?;
1261
1262 file.read_to_string(&mut content)
1263 .map_err(|io_error| ConfigError::FileRead {
1264 path_to_read: path.to_owned(),
1265 io_error,
1266 })?;
1267
1268 Ok(Some(content))
1269 }
1270 None => Ok(None),
1271 }
1272}
1273
1274pub fn resolve_answer_source(value: &str) -> Result<String, ConfigError> {
1297 if let Some(path) = value.strip_prefix("file://") {
1298 let mut content = String::new();
1299 let mut file = File::open(path).map_err(|io_error| ConfigError::FileOpen {
1300 path_to_open: path.to_owned(),
1301 io_error,
1302 })?;
1303 file.read_to_string(&mut content)
1304 .map_err(|io_error| ConfigError::FileRead {
1305 path_to_read: path.to_owned(),
1306 io_error,
1307 })?;
1308 return Ok(content);
1309 }
1310 Ok(value.to_owned())
1311}
1312
1313pub fn load_answers(
1330 answers: &BTreeMap<String, String>,
1331) -> Result<BTreeMap<String, String>, ConfigError> {
1332 let mut out = BTreeMap::new();
1333 for (code, value) in answers {
1334 if value.is_empty() {
1335 continue;
1336 }
1337 out.insert(code.to_owned(), resolve_answer_source(value)?);
1338 }
1339 debug_assert!(
1343 out.len() <= answers.len(),
1344 "load_answers must not synthesize entries"
1345 );
1346 debug_assert!(
1347 out.keys().all(|k| answers.contains_key(k)),
1348 "every loaded status code must come from the input map"
1349 );
1350 Ok(out)
1351}
1352
1353#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
1366#[serde(rename_all = "lowercase")]
1367#[derive(Default)]
1368pub enum MetricDetailLevel {
1369 Process,
1370 Frontend,
1371 #[default]
1372 Cluster,
1373 Backend,
1374}
1375
1376impl From<MetricDetailLevel> for MetricDetail {
1377 fn from(level: MetricDetailLevel) -> Self {
1378 match level {
1379 MetricDetailLevel::Process => MetricDetail::DetailProcess,
1380 MetricDetailLevel::Frontend => MetricDetail::DetailFrontend,
1381 MetricDetailLevel::Cluster => MetricDetail::DetailCluster,
1382 MetricDetailLevel::Backend => MetricDetail::DetailBackend,
1383 }
1384 }
1385}
1386
1387impl From<MetricDetail> for MetricDetailLevel {
1388 fn from(detail: MetricDetail) -> Self {
1392 match detail {
1393 MetricDetail::DetailProcess => MetricDetailLevel::Process,
1394 MetricDetail::DetailFrontend => MetricDetailLevel::Frontend,
1395 MetricDetail::DetailCluster => MetricDetailLevel::Cluster,
1396 MetricDetail::DetailBackend => MetricDetailLevel::Backend,
1397 }
1398 }
1399}
1400
1401#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1402#[serde(deny_unknown_fields)]
1403pub struct MetricsConfig {
1404 pub address: SocketAddr,
1405 #[serde(default)]
1406 pub tagged_metrics: bool,
1407 #[serde(default)]
1408 pub prefix: Option<String>,
1409 #[serde(default)]
1412 pub detail: MetricDetailLevel,
1413}
1414
1415#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1416#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1417#[serde(deny_unknown_fields)]
1418pub enum PathRuleType {
1419 Prefix,
1420 Regex,
1421 Equals,
1422}
1423
1424#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1425#[serde(deny_unknown_fields)]
1426pub struct FileClusterFrontendConfig {
1427 pub address: SocketAddr,
1428 pub hostname: Option<String>,
1429 pub path: Option<String>,
1431 pub path_type: Option<PathRuleType>,
1433 pub method: Option<String>,
1434 pub certificate: Option<String>,
1435 pub key: Option<String>,
1436 pub certificate_chain: Option<String>,
1437 #[serde(default)]
1438 pub tls_versions: Vec<TlsVersion>,
1439 #[serde(default)]
1440 pub position: RulePosition,
1441 pub tags: Option<BTreeMap<String, String>>,
1442 pub redirect: Option<String>,
1447 pub redirect_scheme: Option<String>,
1451 pub redirect_template: Option<String>,
1455 pub rewrite_host: Option<String>,
1458 pub rewrite_path: Option<String>,
1460 pub rewrite_port: Option<u32>,
1462 pub required_auth: Option<bool>,
1466 pub headers: Option<Vec<HeaderEditConfig>>,
1470 pub hsts: Option<FileHstsConfig>,
1475}
1476
1477#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1484#[serde(deny_unknown_fields)]
1485pub struct HeaderEditConfig {
1486 pub position: String,
1487 pub key: String,
1488 pub value: String,
1489}
1490
1491#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1502#[serde(deny_unknown_fields)]
1503pub struct FileHstsConfig {
1504 pub enabled: Option<bool>,
1507 pub max_age: Option<u32>,
1512 pub include_subdomains: Option<bool>,
1514 pub preload: Option<bool>,
1517 pub force_replace_backend: Option<bool>,
1525}
1526
1527impl FileHstsConfig {
1528 pub fn to_proto(&self, scope: &str) -> Result<HstsConfig, ConfigError> {
1546 let enabled = match self.enabled {
1547 Some(v) => v,
1548 None => return Err(ConfigError::HstsEnabledRequired(scope.to_owned())),
1549 };
1550
1551 let max_age = match (enabled, self.max_age) {
1552 (true, None) => Some(DEFAULT_HSTS_MAX_AGE),
1553 (_, m) => m,
1554 };
1555
1556 if let Some(value) = max_age
1557 && value > 0
1558 && value < 86_400
1559 {
1560 warn!(
1561 "HSTS max_age = {}s on {} is below 1 day — this is almost certainly a \
1562 misconfiguration. RFC 6797 §11.4 reserves max_age = 0 as the explicit kill \
1563 switch.",
1564 value, scope
1565 );
1566 }
1567
1568 let include_subdomains = self.include_subdomains;
1569 let preload = self.preload;
1570
1571 if matches!(preload, Some(true)) {
1572 let max_age_value = max_age.unwrap_or(0);
1573 if max_age_value < DEFAULT_HSTS_MAX_AGE {
1574 warn!(
1575 "HSTS preload = true on {} with max_age = {}s; the Chrome HSTS preload \
1576 list requires max_age >= {} (https://hstspreload.org/).",
1577 scope, max_age_value, DEFAULT_HSTS_MAX_AGE
1578 );
1579 }
1580 if include_subdomains != Some(true) {
1581 warn!(
1582 "HSTS preload = true on {} without include_subdomains = true; the Chrome \
1583 HSTS preload list requires includeSubDomains \
1584 (https://hstspreload.org/).",
1585 scope
1586 );
1587 }
1588 }
1589
1590 let config = HstsConfig {
1591 enabled: Some(enabled),
1592 max_age,
1593 include_subdomains,
1594 preload,
1595 force_replace_backend: self.force_replace_backend,
1596 };
1597
1598 debug_assert_eq!(
1603 config.enabled,
1604 Some(enabled),
1605 "built HSTS config must record the resolved enabled flag"
1606 );
1607 debug_assert!(
1608 !enabled || config.max_age.is_some(),
1609 "an enabled HSTS policy must carry a max_age"
1610 );
1611 Ok(config)
1612 }
1613}
1614
1615impl FileClusterFrontendConfig {
1616 pub fn to_tcp_front(&self) -> Result<TcpFrontendConfig, ConfigError> {
1617 if self.hostname.is_some() {
1618 return Err(ConfigError::InvalidFrontendConfig("hostname".to_string()));
1619 }
1620 if self.path.is_some() {
1621 return Err(ConfigError::InvalidFrontendConfig(
1622 "path_prefix".to_string(),
1623 ));
1624 }
1625 if self.certificate.is_some() {
1626 return Err(ConfigError::InvalidFrontendConfig(
1627 "certificate".to_string(),
1628 ));
1629 }
1630 if self.hostname.is_some() {
1631 return Err(ConfigError::InvalidFrontendConfig("hostname".to_string()));
1632 }
1633 if self.certificate_chain.is_some() {
1634 return Err(ConfigError::InvalidFrontendConfig(
1635 "certificate_chain".to_string(),
1636 ));
1637 }
1638
1639 let tcp_front = TcpFrontendConfig {
1640 address: self.address,
1641 tags: self.tags.clone(),
1642 udp: false,
1645 };
1646 debug_assert_eq!(
1651 tcp_front.address, self.address,
1652 "TCP frontend must bind the requested address"
1653 );
1654 debug_assert!(
1655 self.hostname.is_none()
1656 && self.path.is_none()
1657 && self.certificate.is_none()
1658 && self.certificate_chain.is_none(),
1659 "a built TCP frontend must carry no HTTP-only attributes"
1660 );
1661 Ok(tcp_front)
1662 }
1663
1664 pub fn to_http_front(&self, _cluster_id: &str) -> Result<HttpFrontendConfig, ConfigError> {
1665 let hostname = match &self.hostname {
1666 Some(hostname) => hostname.to_owned(),
1667 None => {
1668 return Err(ConfigError::Missing(MissingKind::Field(
1669 "hostname".to_string(),
1670 )));
1671 }
1672 };
1673
1674 let key_opt = match self.key.as_ref() {
1675 None => None,
1676 Some(path) => {
1677 let key = Config::load_file(path)?;
1678 Some(key)
1679 }
1680 };
1681
1682 let certificate_opt = match self.certificate.as_ref() {
1683 None => None,
1684 Some(path) => {
1685 let certificate = Config::load_file(path)?;
1686 Some(certificate)
1687 }
1688 };
1689
1690 let certificate_chain = match self.certificate_chain.as_ref() {
1691 None => None,
1692 Some(path) => {
1693 let certificate_chain = Config::load_file(path)?;
1694 Some(split_certificate_chain(certificate_chain))
1695 }
1696 };
1697
1698 let path = match (self.path.as_ref(), self.path_type.as_ref()) {
1699 (None, _) => PathRule::prefix("".to_string()),
1700 (Some(s), Some(PathRuleType::Prefix)) => PathRule::prefix(s.to_string()),
1701 (Some(s), Some(PathRuleType::Regex)) => PathRule::regex(s.to_string()),
1702 (Some(s), Some(PathRuleType::Equals)) => PathRule::equals(s.to_string()),
1703 (Some(s), None) => PathRule::prefix(s.clone()),
1704 };
1705
1706 let redirect = match self.redirect.as_deref() {
1707 Some(v) => Some(parse_redirect_policy(v)?),
1708 None => None,
1709 };
1710 let redirect_scheme = match self.redirect_scheme.as_deref() {
1711 Some(v) => Some(parse_redirect_scheme(v)?),
1712 None => None,
1713 };
1714
1715 let headers = match self.headers.as_ref() {
1716 Some(entries) => {
1717 let mut out = Vec::with_capacity(entries.len());
1718 for (index, entry) in entries.iter().enumerate() {
1719 out.push(parse_header_edit(index, entry)?);
1720 }
1721 out
1722 }
1723 None => Vec::new(),
1724 };
1725
1726 let frontend_serves_https = key_opt.is_some() && certificate_opt.is_some();
1733 let hsts = match self.hsts.as_ref() {
1734 Some(h) => {
1735 if !frontend_serves_https {
1736 return Err(ConfigError::HstsOnPlainHttp(format!(
1737 "frontend {_cluster_id}/{hostname}"
1738 )));
1739 }
1740 Some(h.to_proto(&format!("frontend {_cluster_id}/{hostname}"))?)
1741 }
1742 None => None,
1743 };
1744
1745 Ok(HttpFrontendConfig {
1746 address: self.address,
1747 hostname,
1748 certificate: certificate_opt,
1749 key: key_opt,
1750 certificate_chain,
1751 tls_versions: self.tls_versions.clone(),
1752 position: self.position,
1753 path,
1754 method: self.method.clone(),
1755 tags: self.tags.clone(),
1756 redirect,
1757 redirect_scheme,
1758 redirect_template: self.redirect_template.clone(),
1759 rewrite_host: self.rewrite_host.clone(),
1760 rewrite_path: self.rewrite_path.clone(),
1761 rewrite_port: self.rewrite_port,
1762 required_auth: self.required_auth,
1763 headers,
1764 hsts,
1765 })
1766 }
1767}
1768
1769pub(crate) fn parse_redirect_policy(value: &str) -> Result<RedirectPolicy, ConfigError> {
1771 match value.to_ascii_lowercase().as_str() {
1772 "forward" => Ok(RedirectPolicy::Forward),
1773 "permanent" => Ok(RedirectPolicy::Permanent),
1774 "unauthorized" => Ok(RedirectPolicy::Unauthorized),
1775 _ => Err(ConfigError::InvalidRedirectPolicy(value.to_owned())),
1776 }
1777}
1778
1779pub(crate) fn parse_redirect_scheme(value: &str) -> Result<RedirectScheme, ConfigError> {
1781 match value.to_ascii_lowercase().as_str() {
1782 "use-same" | "use_same" => Ok(RedirectScheme::UseSame),
1783 "use-http" | "use_http" => Ok(RedirectScheme::UseHttp),
1784 "use-https" | "use_https" => Ok(RedirectScheme::UseHttps),
1785 _ => Err(ConfigError::InvalidRedirectScheme(value.to_owned())),
1786 }
1787}
1788
1789pub(crate) fn parse_header_edit(
1796 index: usize,
1797 entry: &HeaderEditConfig,
1798) -> Result<Header, ConfigError> {
1799 let position = match entry.position.to_ascii_lowercase().as_str() {
1800 "request" => HeaderPosition::Request,
1801 "response" => HeaderPosition::Response,
1802 "both" => HeaderPosition::Both,
1803 _ => {
1804 return Err(ConfigError::InvalidHeaderPosition {
1805 index,
1806 position: entry.position.clone(),
1807 });
1808 }
1809 };
1810 if !header_name_is_valid_token(entry.key.as_bytes()) {
1811 return Err(ConfigError::InvalidHeaderBytes {
1812 index,
1813 field: "key",
1814 });
1815 }
1816 if header_value_contains_forbidden_controls(entry.value.as_bytes()) {
1817 return Err(ConfigError::InvalidHeaderBytes {
1818 index,
1819 field: "value",
1820 });
1821 }
1822 let header = Header {
1823 position: position as i32,
1824 key: entry.key.clone(),
1825 val: entry.value.clone(),
1826 };
1827 debug_assert!(
1833 header_name_is_valid_token(header.key.as_bytes()),
1834 "an emitted header key must be a valid token"
1835 );
1836 debug_assert!(
1837 !header_value_contains_forbidden_controls(header.val.as_bytes()),
1838 "an emitted header value must be free of forbidden control bytes"
1839 );
1840 Ok(header)
1841}
1842
1843pub(crate) fn header_name_is_valid_token(bytes: &[u8]) -> bool {
1851 if bytes.is_empty() {
1852 return false;
1853 }
1854 bytes.iter().all(|&b| is_tchar(b))
1855}
1856
1857fn is_tchar(b: u8) -> bool {
1860 b.is_ascii_alphanumeric()
1861 || matches!(
1862 b,
1863 b'!' | b'#'
1864 | b'$'
1865 | b'%'
1866 | b'&'
1867 | b'\''
1868 | b'*'
1869 | b'+'
1870 | b'-'
1871 | b'.'
1872 | b'^'
1873 | b'_'
1874 | b'`'
1875 | b'|'
1876 | b'~'
1877 )
1878}
1879
1880pub(crate) fn header_value_contains_forbidden_controls(bytes: &[u8]) -> bool {
1888 bytes
1889 .iter()
1890 .any(|&b| matches!(b, 0x00..=0x08 | 0x0A..=0x1F | 0x7F))
1891}
1892
1893#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1894#[serde(deny_unknown_fields, rename_all = "lowercase")]
1895pub enum ListenerProtocol {
1896 Http,
1897 Https,
1898 Tcp,
1899 Udp,
1900}
1901
1902#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1903#[serde(deny_unknown_fields, rename_all = "lowercase")]
1904pub enum FileClusterProtocolConfig {
1905 Http,
1906 Tcp,
1907}
1908
1909fn default_health_check_interval() -> u32 {
1910 10
1911}
1912fn default_health_check_timeout() -> u32 {
1913 5
1914}
1915fn default_health_check_threshold() -> u32 {
1916 3
1917}
1918
1919#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1920#[serde(deny_unknown_fields)]
1921pub struct FileHealthCheckConfig {
1922 pub uri: String,
1923 #[serde(default = "default_health_check_interval")]
1924 pub interval: u32,
1925 #[serde(default = "default_health_check_timeout")]
1926 pub timeout: u32,
1927 #[serde(default = "default_health_check_threshold")]
1928 pub healthy_threshold: u32,
1929 #[serde(default = "default_health_check_threshold")]
1930 pub unhealthy_threshold: u32,
1931 #[serde(default)]
1932 pub expected_status: u32,
1933}
1934
1935impl FileHealthCheckConfig {
1936 pub fn to_proto(&self) -> HealthCheckConfig {
1937 let proto = HealthCheckConfig {
1938 uri: self.uri.to_owned(),
1939 interval: self.interval,
1940 timeout: self.timeout,
1941 healthy_threshold: self.healthy_threshold,
1942 unhealthy_threshold: self.unhealthy_threshold,
1943 expected_status: self.expected_status,
1944 };
1945 debug_assert_eq!(proto.uri, self.uri, "proto URI must mirror the file config");
1949 debug_assert!(
1950 proto.interval == self.interval
1951 && proto.timeout == self.timeout
1952 && proto.healthy_threshold == self.healthy_threshold
1953 && proto.unhealthy_threshold == self.unhealthy_threshold,
1954 "proto timing knobs must mirror the file config"
1955 );
1956 proto
1957 }
1958}
1959
1960pub fn validate_health_check_config(cfg: &HealthCheckConfig) -> Result<(), &'static str> {
1971 if cfg.interval == 0 {
1972 return Err("health check interval must be > 0");
1973 }
1974 if cfg.timeout == 0 {
1975 return Err("health check timeout must be > 0");
1976 }
1977 if cfg.healthy_threshold == 0 {
1978 return Err("health check healthy_threshold must be > 0");
1979 }
1980 if cfg.unhealthy_threshold == 0 {
1981 return Err("health check unhealthy_threshold must be > 0");
1982 }
1983 if !cfg.uri.starts_with('/') {
1984 return Err("health check URI must start with '/'");
1985 }
1986 if cfg
1987 .uri
1988 .bytes()
1989 .any(|b| b == b'\r' || b == b'\n' || b == 0 || (b < 0x20 && b != b'\t'))
1990 {
1991 return Err("health check URI must not contain CR, LF, NUL, or other C0 control bytes");
1992 }
1993 debug_assert!(
1999 cfg.interval > 0
2000 && cfg.timeout > 0
2001 && cfg.healthy_threshold > 0
2002 && cfg.unhealthy_threshold > 0,
2003 "validated health-check thresholds must all be strictly positive"
2004 );
2005 debug_assert!(
2006 cfg.uri.starts_with('/'),
2007 "validated health-check URI must be an absolute path"
2008 );
2009 Ok(())
2010}
2011
2012#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2013#[serde(deny_unknown_fields)]
2014pub struct FileClusterConfig {
2015 pub frontends: Vec<FileClusterFrontendConfig>,
2016 pub backends: Vec<BackendConfig>,
2017 pub protocol: FileClusterProtocolConfig,
2018 pub sticky_session: Option<bool>,
2019 pub https_redirect: Option<bool>,
2020 #[serde(default)]
2021 pub send_proxy: Option<bool>,
2022 #[serde(default)]
2023 pub load_balancing: LoadBalancingAlgorithms,
2024 pub answer_503: Option<String>,
2025 #[serde(default)]
2026 pub load_metric: Option<LoadMetric>,
2027 pub http2: Option<bool>,
2030 pub answers: Option<BTreeMap<String, String>>,
2041 pub https_redirect_port: Option<u32>,
2046 pub authorized_hashes: Option<Vec<String>>,
2051 pub www_authenticate: Option<String>,
2055 pub max_connections_per_ip: Option<u64>,
2062 pub retry_after: Option<u32>,
2068 #[serde(default)]
2072 pub health_check: Option<FileHealthCheckConfig>,
2073 #[serde(default)]
2077 pub udp: Option<FileUdpClusterConfig>,
2078}
2079
2080#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2083#[serde(deny_unknown_fields)]
2084pub struct FileUdpHealthConfig {
2085 pub mode: Option<UdpHealthMode>,
2089 pub tcp_port: Option<u32>,
2090 pub rise: Option<u32>,
2091 pub fall: Option<u32>,
2092 pub fail_open: Option<bool>,
2093 pub udp_probe_payload: Option<String>,
2095 pub probe_interval_seconds: Option<u32>,
2096 pub probe_timeout_seconds: Option<u32>,
2097}
2098
2099impl FileUdpHealthConfig {
2100 pub fn to_proto(&self) -> UdpHealthConfig {
2101 UdpHealthConfig {
2102 mode: self.mode.map(|m| m as i32),
2103 tcp_port: self.tcp_port,
2104 rise: self.rise,
2105 fall: self.fall,
2106 fail_open: self.fail_open,
2107 udp_probe_payload: self
2108 .udp_probe_payload
2109 .as_ref()
2110 .map(|p| p.as_bytes().to_owned()),
2111 probe_interval_seconds: self.probe_interval_seconds,
2112 probe_timeout_seconds: self.probe_timeout_seconds,
2113 }
2114 }
2115}
2116
2117#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2119#[serde(deny_unknown_fields)]
2120pub struct FileUdpClusterConfig {
2121 pub affinity_key: Option<UdpAffinityKey>,
2124 pub responses: Option<u32>,
2126 pub requests: Option<u32>,
2128 pub send_proxy_protocol: Option<bool>,
2130 pub proxy_protocol_every_datagram: Option<bool>,
2132 pub health: Option<FileUdpHealthConfig>,
2134}
2135
2136impl FileUdpClusterConfig {
2137 pub fn to_proto(&self) -> UdpClusterConfig {
2138 UdpClusterConfig {
2139 affinity_key: self.affinity_key.map(|k| k as i32),
2140 responses: self.responses,
2141 requests: self.requests,
2142 send_proxy_protocol: self.send_proxy_protocol,
2143 proxy_protocol_every_datagram: self.proxy_protocol_every_datagram,
2144 health: self.health.as_ref().map(|h| h.to_proto()),
2145 }
2146 }
2147}
2148
2149#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2150#[serde(deny_unknown_fields)]
2151pub struct BackendConfig {
2152 pub address: SocketAddr,
2153 pub weight: Option<u8>,
2154 pub sticky_id: Option<String>,
2155 pub backup: Option<bool>,
2156 pub backend_id: Option<String>,
2157}
2158
2159impl FileClusterConfig {
2160 pub fn to_cluster_config(
2161 self,
2162 cluster_id: &str,
2163 expect_proxy: &HashSet<SocketAddr>,
2164 ) -> Result<ClusterConfig, ConfigError> {
2165 let requested_frontend_count = self.frontends.len();
2168 match self.protocol {
2169 FileClusterProtocolConfig::Tcp => {
2170 let mut has_expect_proxy = None;
2171 let mut frontends = Vec::new();
2172 for f in self.frontends {
2173 if expect_proxy.contains(&f.address) {
2174 match has_expect_proxy {
2175 Some(true) => {}
2176 Some(false) => {
2177 return Err(ConfigError::Incompatible {
2178 object: ObjectKind::Cluster,
2179 id: cluster_id.to_owned(),
2180 kind: IncompatibilityKind::ProxyProtocol,
2181 });
2182 }
2183 None => has_expect_proxy = Some(true),
2184 }
2185 } else {
2186 match has_expect_proxy {
2187 Some(false) => {}
2188 Some(true) => {
2189 return Err(ConfigError::Incompatible {
2190 object: ObjectKind::Cluster,
2191 id: cluster_id.to_owned(),
2192 kind: IncompatibilityKind::ProxyProtocol,
2193 });
2194 }
2195 None => has_expect_proxy = Some(false),
2196 }
2197 }
2198 let tcp_frontend = f.to_tcp_front()?;
2199 frontends.push(tcp_frontend);
2200 }
2201
2202 let send_proxy = self.send_proxy.unwrap_or(false);
2203 let expect_proxy = has_expect_proxy.unwrap_or(false);
2204 let proxy_protocol = match (send_proxy, expect_proxy) {
2205 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
2206 (true, false) => Some(ProxyProtocolConfig::SendHeader),
2207 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
2208 _ => None,
2209 };
2210
2211 let answers = match self.answers.as_ref() {
2212 Some(map) => load_answers(map)?,
2213 None => BTreeMap::new(),
2214 };
2215
2216 let udp = self.udp.as_ref().map(|u| u.to_proto());
2217 debug_assert_eq!(
2223 frontends.len(),
2224 requested_frontend_count,
2225 "every TCP frontend must survive conversion"
2226 );
2227 debug_assert_eq!(
2228 proxy_protocol,
2229 match (send_proxy, expect_proxy) {
2230 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
2231 (true, false) => Some(ProxyProtocolConfig::SendHeader),
2232 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
2233 (false, false) => None,
2234 },
2235 "proxy_protocol must be the (send, expect) function"
2236 );
2237
2238 Ok(ClusterConfig::Tcp(TcpClusterConfig {
2239 cluster_id: cluster_id.to_string(),
2240 frontends,
2241 backends: self.backends,
2242 proxy_protocol,
2243 load_balancing: self.load_balancing,
2244 load_metric: self.load_metric,
2245 answers,
2246 https_redirect_port: self.https_redirect_port,
2247 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
2248 www_authenticate: self.www_authenticate,
2249 max_connections_per_ip: self.max_connections_per_ip,
2250 retry_after: self.retry_after,
2251 health_check: self.health_check.as_ref().map(|hc| hc.to_proto()),
2252 udp,
2253 }))
2254 }
2255 FileClusterProtocolConfig::Http => {
2256 let mut frontends = Vec::new();
2257 for frontend in self.frontends {
2258 let http_frontend = frontend.to_http_front(cluster_id)?;
2259 frontends.push(http_frontend);
2260 }
2261
2262 let answer_503 = self.answer_503.as_ref().and_then(|path| {
2263 Config::load_file(path)
2264 .map_err(|e| {
2265 error!("cannot load 503 error page at path '{}': {:?}", path, e);
2266 e
2267 })
2268 .ok()
2269 });
2270
2271 let answers = match self.answers.as_ref() {
2272 Some(map) => load_answers(map)?,
2273 None => BTreeMap::new(),
2274 };
2275
2276 let udp = self.udp.as_ref().map(|u| u.to_proto());
2277 debug_assert_eq!(
2280 frontends.len(),
2281 requested_frontend_count,
2282 "every HTTP frontend must survive conversion"
2283 );
2284
2285 Ok(ClusterConfig::Http(HttpClusterConfig {
2286 cluster_id: cluster_id.to_string(),
2287 frontends,
2288 backends: self.backends,
2289 sticky_session: self.sticky_session.unwrap_or(false),
2290 https_redirect: self.https_redirect.unwrap_or(false),
2291 load_balancing: self.load_balancing,
2292 load_metric: self.load_metric,
2293 answer_503,
2294 http2: self.http2,
2295 answers,
2296 https_redirect_port: self.https_redirect_port,
2297 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
2298 www_authenticate: self.www_authenticate,
2299 max_connections_per_ip: self.max_connections_per_ip,
2300 retry_after: self.retry_after,
2301 health_check: self.health_check.as_ref().map(|hc| hc.to_proto()),
2302 udp,
2303 }))
2304 }
2305 }
2306 }
2307}
2308
2309#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2310#[serde(deny_unknown_fields)]
2311pub struct HttpFrontendConfig {
2312 pub address: SocketAddr,
2313 pub hostname: String,
2314 pub path: PathRule,
2315 pub method: Option<String>,
2316 pub certificate: Option<String>,
2317 pub key: Option<String>,
2318 pub certificate_chain: Option<Vec<String>>,
2319 #[serde(default)]
2320 pub tls_versions: Vec<TlsVersion>,
2321 #[serde(default)]
2322 pub position: RulePosition,
2323 pub tags: Option<BTreeMap<String, String>>,
2324 #[serde(default)]
2326 pub redirect: Option<RedirectPolicy>,
2327 #[serde(default)]
2329 pub redirect_scheme: Option<RedirectScheme>,
2330 #[serde(default)]
2331 pub redirect_template: Option<String>,
2332 #[serde(default)]
2333 pub rewrite_host: Option<String>,
2334 #[serde(default)]
2335 pub rewrite_path: Option<String>,
2336 #[serde(default)]
2337 pub rewrite_port: Option<u32>,
2338 #[serde(default)]
2339 pub required_auth: Option<bool>,
2340 #[serde(default)]
2343 pub headers: Vec<Header>,
2344 #[serde(default)]
2347 pub hsts: Option<HstsConfig>,
2348}
2349
2350impl HttpFrontendConfig {
2351 pub fn generate_requests(&self, cluster_id: &str) -> Vec<Request> {
2352 let mut v = Vec::new();
2353
2354 let tags = self.tags.clone().unwrap_or_default();
2355
2356 if self.key.is_some() && self.certificate.is_some() {
2357 v.push(
2358 RequestType::AddCertificate(AddCertificate {
2359 address: self.address.into(),
2360 certificate: CertificateAndKey {
2361 key: self.key.clone().unwrap(),
2362 certificate: self.certificate.clone().unwrap(),
2363 certificate_chain: self.certificate_chain.clone().unwrap_or_default(),
2364 versions: self.tls_versions.iter().map(|v| *v as i32).collect(),
2365 names: vec![],
2370 },
2371 expired_at: None,
2372 })
2373 .into(),
2374 );
2375
2376 v.push(
2377 RequestType::AddHttpsFrontend(RequestHttpFrontend {
2378 cluster_id: Some(cluster_id.to_string()),
2379 address: self.address.into(),
2380 hostname: self.hostname.clone(),
2381 path: self.path.clone(),
2382 method: self.method.clone(),
2383 position: self.position.into(),
2384 tags,
2385 redirect: self.redirect.map(|r| r as i32),
2386 required_auth: self.required_auth,
2387 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
2388 redirect_template: self.redirect_template.clone(),
2389 rewrite_host: self.rewrite_host.clone(),
2390 rewrite_path: self.rewrite_path.clone(),
2391 rewrite_port: self.rewrite_port,
2392 headers: self.headers.clone(),
2393 hsts: self.hsts,
2394 })
2395 .into(),
2396 );
2397 } else {
2398 v.push(
2400 RequestType::AddHttpFrontend(RequestHttpFrontend {
2401 cluster_id: Some(cluster_id.to_string()),
2402 address: self.address.into(),
2403 hostname: self.hostname.clone(),
2404 path: self.path.clone(),
2405 method: self.method.clone(),
2406 position: self.position.into(),
2407 tags,
2408 redirect: self.redirect.map(|r| r as i32),
2409 required_auth: self.required_auth,
2410 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
2411 redirect_template: self.redirect_template.clone(),
2412 rewrite_host: self.rewrite_host.clone(),
2413 rewrite_path: self.rewrite_path.clone(),
2414 rewrite_port: self.rewrite_port,
2415 headers: self.headers.clone(),
2416 hsts: self.hsts,
2417 })
2418 .into(),
2419 );
2420 }
2421
2422 v
2423 }
2424}
2425
2426#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2427#[serde(deny_unknown_fields)]
2428pub struct HttpClusterConfig {
2429 pub cluster_id: String,
2430 pub frontends: Vec<HttpFrontendConfig>,
2431 pub backends: Vec<BackendConfig>,
2432 pub sticky_session: bool,
2433 pub https_redirect: bool,
2434 pub load_balancing: LoadBalancingAlgorithms,
2435 pub load_metric: Option<LoadMetric>,
2436 pub answer_503: Option<String>,
2437 pub http2: Option<bool>,
2438 #[serde(default)]
2441 pub answers: BTreeMap<String, String>,
2442 #[serde(default)]
2443 pub https_redirect_port: Option<u32>,
2444 #[serde(default)]
2445 pub authorized_hashes: Vec<String>,
2446 #[serde(default)]
2447 pub www_authenticate: Option<String>,
2448 #[serde(default)]
2451 pub max_connections_per_ip: Option<u64>,
2452 #[serde(default)]
2455 pub retry_after: Option<u32>,
2456 #[serde(default)]
2460 pub health_check: Option<HealthCheckConfig>,
2461 #[serde(default)]
2464 pub udp: Option<UdpClusterConfig>,
2465}
2466
2467impl HttpClusterConfig {
2468 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2469 let mut v: Vec<Request> = vec![
2470 RequestType::AddCluster(Cluster {
2471 cluster_id: self.cluster_id.clone(),
2472 sticky_session: self.sticky_session,
2473 https_redirect: self.https_redirect,
2474 proxy_protocol: None,
2475 load_balancing: self.load_balancing as i32,
2476 answer_503: self.answer_503.clone(),
2477 load_metric: self.load_metric.map(|s| s as i32),
2478 http2: self.http2,
2479 answers: self.answers.clone(),
2480 https_redirect_port: self.https_redirect_port,
2481 authorized_hashes: self.authorized_hashes.clone(),
2482 www_authenticate: self.www_authenticate.clone(),
2483 max_connections_per_ip: self.max_connections_per_ip,
2484 retry_after: self.retry_after,
2485 health_check: self.health_check.clone(),
2486 udp: self.udp.clone(),
2487 })
2488 .into(),
2489 ];
2490
2491 for frontend in &self.frontends {
2492 let mut orders = frontend.generate_requests(&self.cluster_id);
2493 v.append(&mut orders);
2494 }
2495
2496 for (backend_count, backend) in self.backends.iter().enumerate() {
2497 let load_balancing_parameters = Some(LoadBalancingParams {
2498 weight: backend.weight.unwrap_or(100) as i32,
2499 });
2500
2501 v.push(
2502 RequestType::AddBackend(AddBackend {
2503 cluster_id: self.cluster_id.clone(),
2504 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
2505 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
2506 }),
2507 address: backend.address.into(),
2508 load_balancing_parameters,
2509 sticky_id: backend.sticky_id.clone(),
2510 backup: backend.backup,
2511 })
2512 .into(),
2513 );
2514 }
2515
2516 debug_assert!(
2521 matches!(
2522 v.first().and_then(|r| r.request_type.as_ref()),
2523 Some(RequestType::AddCluster(_))
2524 ),
2525 "HTTP cluster orders must lead with an AddCluster"
2526 );
2527 debug_assert_eq!(
2528 v.iter()
2529 .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
2530 .count(),
2531 self.backends.len(),
2532 "one AddBackend order per configured backend"
2533 );
2534 Ok(v)
2535 }
2536}
2537
2538#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2539pub struct TcpFrontendConfig {
2540 pub address: SocketAddr,
2541 pub tags: Option<BTreeMap<String, String>>,
2542 #[serde(default)]
2549 pub udp: bool,
2550}
2551
2552#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2553pub struct TcpClusterConfig {
2554 pub cluster_id: String,
2555 pub frontends: Vec<TcpFrontendConfig>,
2556 pub backends: Vec<BackendConfig>,
2557 #[serde(default)]
2558 pub proxy_protocol: Option<ProxyProtocolConfig>,
2559 pub load_balancing: LoadBalancingAlgorithms,
2560 pub load_metric: Option<LoadMetric>,
2561 #[serde(default)]
2565 pub answers: BTreeMap<String, String>,
2566 #[serde(default)]
2567 pub https_redirect_port: Option<u32>,
2568 #[serde(default)]
2569 pub authorized_hashes: Vec<String>,
2570 #[serde(default)]
2571 pub www_authenticate: Option<String>,
2572 #[serde(default)]
2575 pub max_connections_per_ip: Option<u64>,
2576 #[serde(default)]
2580 pub retry_after: Option<u32>,
2581 #[serde(default)]
2585 pub health_check: Option<HealthCheckConfig>,
2586 #[serde(default)]
2589 pub udp: Option<UdpClusterConfig>,
2590}
2591
2592impl TcpClusterConfig {
2593 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2594 let mut v: Vec<Request> = vec![
2595 RequestType::AddCluster(Cluster {
2596 cluster_id: self.cluster_id.clone(),
2597 sticky_session: false,
2598 https_redirect: false,
2599 proxy_protocol: self.proxy_protocol.map(|s| s as i32),
2600 load_balancing: self.load_balancing as i32,
2601 load_metric: self.load_metric.map(|s| s as i32),
2602 answer_503: None,
2603 http2: None,
2604 answers: self.answers.clone(),
2605 https_redirect_port: self.https_redirect_port,
2606 authorized_hashes: self.authorized_hashes.clone(),
2607 www_authenticate: self.www_authenticate.clone(),
2608 max_connections_per_ip: self.max_connections_per_ip,
2609 retry_after: self.retry_after,
2610 health_check: self.health_check.clone(),
2611 udp: self.udp.clone(),
2612 })
2613 .into(),
2614 ];
2615
2616 for frontend in &self.frontends {
2617 if frontend.udp {
2622 v.push(
2623 RequestType::AddUdpFrontend(RequestUdpFrontend {
2624 cluster_id: self.cluster_id.clone(),
2625 address: frontend.address.into(),
2626 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
2627 })
2628 .into(),
2629 );
2630 } else {
2631 v.push(
2632 RequestType::AddTcpFrontend(RequestTcpFrontend {
2633 cluster_id: self.cluster_id.clone(),
2634 address: frontend.address.into(),
2635 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
2636 })
2637 .into(),
2638 );
2639 }
2640 }
2641
2642 for (backend_count, backend) in self.backends.iter().enumerate() {
2643 let load_balancing_parameters = Some(LoadBalancingParams {
2644 weight: backend.weight.unwrap_or(100) as i32,
2645 });
2646
2647 v.push(
2648 RequestType::AddBackend(AddBackend {
2649 cluster_id: self.cluster_id.clone(),
2650 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
2651 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
2652 }),
2653 address: backend.address.into(),
2654 load_balancing_parameters,
2655 sticky_id: backend.sticky_id.clone(),
2656 backup: backend.backup,
2657 })
2658 .into(),
2659 );
2660 }
2661
2662 debug_assert!(
2666 matches!(
2667 v.first().and_then(|r| r.request_type.as_ref()),
2668 Some(RequestType::AddCluster(_))
2669 ),
2670 "TCP cluster orders must lead with an AddCluster"
2671 );
2672 debug_assert_eq!(
2673 v.iter()
2674 .filter(|r| matches!(
2675 r.request_type,
2676 Some(RequestType::AddTcpFrontend(_)) | Some(RequestType::AddUdpFrontend(_))
2677 ))
2678 .count(),
2679 self.frontends.len(),
2680 "one AddTcpFrontend or AddUdpFrontend order per configured frontend"
2681 );
2682 debug_assert_eq!(
2683 v.iter()
2684 .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
2685 .count(),
2686 self.backends.len(),
2687 "one AddBackend order per configured backend"
2688 );
2689 Ok(v)
2690 }
2691}
2692
2693#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2694pub enum ClusterConfig {
2695 Http(HttpClusterConfig),
2696 Tcp(TcpClusterConfig),
2697}
2698
2699impl ClusterConfig {
2700 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2701 match *self {
2702 ClusterConfig::Http(ref http) => http.generate_requests(),
2703 ClusterConfig::Tcp(ref tcp) => tcp.generate_requests(),
2704 }
2705 }
2706}
2707
2708#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
2710pub struct FileConfig {
2711 pub command_socket: Option<String>,
2712 pub command_buffer_size: Option<u64>,
2713 pub max_command_buffer_size: Option<u64>,
2714 pub max_connections: Option<usize>,
2715 pub min_buffers: Option<u64>,
2716 pub max_buffers: Option<u64>,
2717 pub buffer_size: Option<u64>,
2718 #[serde(default)]
2722 pub slab_entries_per_connection: Option<u64>,
2723 #[serde(default)]
2733 pub basic_auth_max_credential_bytes: Option<u64>,
2734 #[serde(default)]
2742 pub max_connections_per_ip: Option<u64>,
2743 #[serde(default)]
2749 pub retry_after: Option<u32>,
2750 #[serde(default)]
2759 pub splice_pipe_capacity_bytes: Option<u64>,
2760 #[serde(default)]
2767 pub command_allowed_uids: Option<Vec<u32>>,
2768 pub saved_state: Option<String>,
2769 #[serde(default)]
2770 pub automatic_state_save: Option<bool>,
2771 pub log_level: Option<String>,
2772 pub log_target: Option<String>,
2773 #[serde(default)]
2774 pub log_colored: bool,
2775 #[serde(default)]
2783 pub audit_logs_target: Option<String>,
2784 #[serde(default)]
2789 pub audit_logs_json_target: Option<String>,
2790 #[serde(default)]
2791 pub access_logs_target: Option<String>,
2792 #[serde(default)]
2793 pub access_logs_format: Option<AccessLogFormat>,
2794 #[serde(default)]
2795 pub access_logs_colored: Option<bool>,
2796 pub worker_count: Option<u16>,
2797 pub worker_automatic_restart: Option<bool>,
2798 pub metrics: Option<MetricsConfig>,
2799 pub disable_cluster_metrics: Option<bool>,
2800 pub listeners: Option<Vec<ListenerBuilder>>,
2801 pub clusters: Option<HashMap<String, FileClusterConfig>>,
2802 pub handle_process_affinity: Option<bool>,
2803 pub ctl_command_timeout: Option<u64>,
2804 pub pid_file_path: Option<String>,
2805 pub activate_listeners: Option<bool>,
2806 #[serde(default)]
2807 pub front_timeout: Option<u32>,
2808 #[serde(default)]
2809 pub back_timeout: Option<u32>,
2810 #[serde(default)]
2811 pub connect_timeout: Option<u32>,
2812 #[serde(default)]
2813 pub zombie_check_interval: Option<u32>,
2814 #[serde(default)]
2815 pub accept_queue_timeout: Option<u32>,
2816 #[serde(default)]
2817 pub evict_on_queue_full: Option<bool>,
2818 #[serde(default)]
2819 pub request_timeout: Option<u32>,
2820 #[serde(default)]
2821 pub worker_timeout: Option<u32>,
2822}
2823
2824impl FileConfig {
2825 pub fn load_from_path(path: &str) -> Result<FileConfig, ConfigError> {
2826 let data = Config::load_file(path)?;
2827
2828 let config: FileConfig = match toml::from_str(&data) {
2829 Ok(config) => config,
2830 Err(e) => {
2831 display_toml_error(&data, &e);
2832 return Err(ConfigError::DeserializeToml(e.to_string()));
2833 }
2834 };
2835
2836 let mut reserved_address: HashSet<SocketAddr> = HashSet::new();
2837
2838 if let Some(listeners) = config.listeners.as_ref() {
2839 for listener in listeners.iter() {
2840 if reserved_address.contains(&listener.address) {
2841 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
2842 }
2843 reserved_address.insert(listener.address);
2844 }
2845 }
2846
2847 Ok(config)
2869 }
2870}
2871
2872pub struct ConfigBuilder {
2874 file: FileConfig,
2875 known_addresses: HashMap<SocketAddr, ListenerProtocol>,
2876 expect_proxy_addresses: HashSet<SocketAddr>,
2877 built: Config,
2878}
2879
2880impl ConfigBuilder {
2881 pub fn new<S>(file_config: FileConfig, config_path: S) -> Self
2885 where
2886 S: ToString,
2887 {
2888 let built = Config {
2889 accept_queue_timeout: file_config
2890 .accept_queue_timeout
2891 .unwrap_or(DEFAULT_ACCEPT_QUEUE_TIMEOUT),
2892 evict_on_queue_full: file_config
2893 .evict_on_queue_full
2894 .unwrap_or(DEFAULT_EVICT_ON_QUEUE_FULL),
2895 activate_listeners: file_config.activate_listeners.unwrap_or(true),
2896 automatic_state_save: file_config
2897 .automatic_state_save
2898 .unwrap_or(DEFAULT_AUTOMATIC_STATE_SAVE),
2899 back_timeout: file_config.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
2900 buffer_size: file_config.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE),
2901 command_buffer_size: file_config
2902 .command_buffer_size
2903 .unwrap_or(DEFAULT_COMMAND_BUFFER_SIZE),
2904 config_path: config_path.to_string(),
2905 connect_timeout: file_config
2906 .connect_timeout
2907 .unwrap_or(DEFAULT_CONNECT_TIMEOUT),
2908 ctl_command_timeout: file_config.ctl_command_timeout.unwrap_or(1_000),
2909 front_timeout: file_config.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
2910 handle_process_affinity: file_config.handle_process_affinity.unwrap_or(false),
2911 access_logs_target: file_config.access_logs_target.clone(),
2912 audit_logs_target: file_config.audit_logs_target.clone(),
2913 audit_logs_json_target: file_config.audit_logs_json_target.clone(),
2914 access_logs_format: file_config.access_logs_format.clone(),
2915 access_logs_colored: file_config.access_logs_colored,
2916 log_level: file_config
2917 .log_level
2918 .clone()
2919 .unwrap_or_else(|| String::from("info")),
2920 log_target: file_config
2921 .log_target
2922 .clone()
2923 .unwrap_or_else(|| String::from("stdout")),
2924 log_colored: file_config.log_colored,
2925 max_buffers: file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
2926 max_command_buffer_size: file_config
2927 .max_command_buffer_size
2928 .unwrap_or(DEFAULT_MAX_COMMAND_BUFFER_SIZE),
2929 max_connections: file_config
2930 .max_connections
2931 .unwrap_or(DEFAULT_MAX_CONNECTIONS),
2932 metrics: file_config.metrics.clone(),
2933 disable_cluster_metrics: file_config
2934 .disable_cluster_metrics
2935 .unwrap_or(DEFAULT_DISABLE_CLUSTER_METRICS),
2936 min_buffers: std::cmp::min(
2937 file_config.min_buffers.unwrap_or(DEFAULT_MIN_BUFFERS),
2938 file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
2939 ),
2940 pid_file_path: file_config.pid_file_path.clone(),
2941 request_timeout: file_config
2942 .request_timeout
2943 .unwrap_or(DEFAULT_REQUEST_TIMEOUT),
2944 saved_state: file_config.saved_state.clone(),
2945 worker_automatic_restart: file_config
2946 .worker_automatic_restart
2947 .unwrap_or(DEFAULT_WORKER_AUTOMATIC_RESTART),
2948 worker_count: file_config.worker_count.unwrap_or(DEFAULT_WORKER_COUNT),
2949 zombie_check_interval: file_config
2950 .zombie_check_interval
2951 .unwrap_or(DEFAULT_ZOMBIE_CHECK_INTERVAL),
2952 worker_timeout: file_config.worker_timeout.unwrap_or(DEFAULT_WORKER_TIMEOUT),
2953 slab_entries_per_connection: file_config.slab_entries_per_connection.map(|n| {
2954 n.clamp(
2955 ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION,
2956 ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION,
2957 )
2958 }),
2959 command_allowed_uids: file_config.command_allowed_uids.clone(),
2960 basic_auth_max_credential_bytes: file_config.basic_auth_max_credential_bytes,
2961 max_connections_per_ip: file_config
2962 .max_connections_per_ip
2963 .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_IP),
2964 retry_after: file_config.retry_after.unwrap_or(DEFAULT_RETRY_AFTER),
2965 splice_pipe_capacity_bytes: file_config.splice_pipe_capacity_bytes,
2966 ..Default::default()
2967 };
2968
2969 debug_assert!(
2973 built.min_buffers <= built.max_buffers,
2974 "min_buffers must be clamped to <= max_buffers in the builder"
2975 );
2976 debug_assert!(
2979 built.slab_entries_per_connection.is_none_or(|n| {
2980 (ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION
2981 ..=ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION)
2982 .contains(&n)
2983 }),
2984 "a set slab_entries_per_connection must be clamped into [MIN, MAX]"
2985 );
2986
2987 Self {
2988 file: file_config,
2989 known_addresses: HashMap::new(),
2990 expect_proxy_addresses: HashSet::new(),
2991 built,
2992 }
2993 }
2994
2995 fn push_tls_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
2996 let listener = listener.to_tls(Some(&self.built))?;
2997 self.built.https_listeners.push(listener);
2998 Ok(())
2999 }
3000
3001 fn push_http_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3002 let listener = listener.to_http(Some(&self.built))?;
3003 self.built.http_listeners.push(listener);
3004 Ok(())
3005 }
3006
3007 fn push_tcp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3008 let listener = listener.to_tcp(Some(&self.built))?;
3009 self.built.tcp_listeners.push(listener);
3010 Ok(())
3011 }
3012
3013 fn push_udp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3014 let listener = listener.to_udp(Some(&self.built))?;
3015 self.built.udp_listeners.push(listener);
3016 Ok(())
3017 }
3018
3019 fn populate_listeners(&mut self, listeners: Vec<ListenerBuilder>) -> Result<(), ConfigError> {
3020 for listener in listeners.iter() {
3021 if self.known_addresses.contains_key(&listener.address) {
3022 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
3023 }
3024
3025 let protocol = listener
3026 .protocol
3027 .ok_or(ConfigError::Missing(MissingKind::Protocol))?;
3028
3029 self.known_addresses.insert(listener.address, protocol);
3030 if listener.expect_proxy == Some(true) {
3031 self.expect_proxy_addresses.insert(listener.address);
3032 }
3033
3034 if listener.public_address.is_some() && listener.expect_proxy == Some(true) {
3035 return Err(ConfigError::Incompatible {
3036 object: ObjectKind::Listener,
3037 id: listener.address.to_string(),
3038 kind: IncompatibilityKind::PublicAddress,
3039 });
3040 }
3041
3042 match protocol {
3043 ListenerProtocol::Https => self.push_tls_listener(listener.clone())?,
3044 ListenerProtocol::Http => self.push_http_listener(listener.clone())?,
3045 ListenerProtocol::Tcp => self.push_tcp_listener(listener.clone())?,
3046 ListenerProtocol::Udp => self.push_udp_listener(listener.clone())?,
3047 }
3048 }
3049 Ok(())
3050 }
3051
3052 fn populate_clusters(
3053 &mut self,
3054 mut file_cluster_configs: HashMap<String, FileClusterConfig>,
3055 ) -> Result<(), ConfigError> {
3056 for (id, file_cluster_config) in file_cluster_configs.drain() {
3057 let mut cluster_config =
3058 file_cluster_config.to_cluster_config(id.as_str(), &self.expect_proxy_addresses)?;
3059
3060 match cluster_config {
3061 ClusterConfig::Http(ref mut http) => {
3062 for frontend in http.frontends.iter_mut() {
3063 match self.known_addresses.get(&frontend.address) {
3064 Some(ListenerProtocol::Tcp) => {
3065 return Err(ConfigError::WrongFrontendProtocol(
3066 ListenerProtocol::Tcp,
3067 ));
3068 }
3069 Some(ListenerProtocol::Udp) => {
3070 return Err(ConfigError::WrongFrontendProtocol(
3071 ListenerProtocol::Udp,
3072 ));
3073 }
3074 Some(ListenerProtocol::Http) => {
3075 if frontend.certificate.is_some() {
3076 return Err(ConfigError::WrongFrontendProtocol(
3077 ListenerProtocol::Http,
3078 ));
3079 }
3080 }
3081 Some(ListenerProtocol::Https) => {
3082 if frontend.certificate.is_none() {
3083 if let Some(https_listener) =
3084 self.built.https_listeners.iter().find(|listener| {
3085 listener.address == frontend.address.into()
3086 && listener.certificate.is_some()
3087 })
3088 {
3089 frontend
3091 .certificate
3092 .clone_from(&https_listener.certificate);
3093 frontend.certificate_chain =
3094 Some(https_listener.certificate_chain.clone());
3095 frontend.key.clone_from(&https_listener.key);
3096 }
3097 if frontend.certificate.is_none() {
3098 debug!("known addresses: {:?}", self.known_addresses);
3099 debug!("frontend: {:?}", frontend);
3100 return Err(ConfigError::WrongFrontendProtocol(
3101 ListenerProtocol::Https,
3102 ));
3103 }
3104 }
3105 }
3106 None => {
3107 let file_listener_protocol = if frontend.certificate.is_some() {
3109 self.push_tls_listener(ListenerBuilder::new(
3110 frontend.address.into(),
3111 ListenerProtocol::Https,
3112 ))?;
3113
3114 ListenerProtocol::Https
3115 } else {
3116 self.push_http_listener(ListenerBuilder::new(
3117 frontend.address.into(),
3118 ListenerProtocol::Http,
3119 ))?;
3120
3121 ListenerProtocol::Http
3122 };
3123 self.known_addresses
3124 .insert(frontend.address, file_listener_protocol);
3125 }
3126 }
3127 }
3128 }
3129 ClusterConfig::Tcp(ref mut tcp) => {
3130 for frontend in tcp.frontends.iter_mut() {
3132 match self.known_addresses.get(&frontend.address) {
3133 Some(ListenerProtocol::Http) | Some(ListenerProtocol::Https) => {
3134 return Err(ConfigError::WrongFrontendProtocol(
3135 ListenerProtocol::Http,
3136 ));
3137 }
3138 Some(ListenerProtocol::Udp) => {
3139 frontend.udp = true;
3145 }
3146 Some(ListenerProtocol::Tcp) => {}
3147 None => {
3148 self.push_tcp_listener(ListenerBuilder::new(
3150 frontend.address.into(),
3151 ListenerProtocol::Tcp,
3152 ))?;
3153 self.known_addresses
3154 .insert(frontend.address, ListenerProtocol::Tcp);
3155 }
3156 }
3157 }
3158 }
3159 }
3160
3161 self.built.clusters.insert(id, cluster_config);
3162 }
3163 Ok(())
3164 }
3165
3166 pub fn into_config(&mut self) -> Result<Config, ConfigError> {
3168 if let Some(listeners) = &self.file.listeners {
3169 self.populate_listeners(listeners.clone())?;
3170 }
3171
3172 if let Some(file_cluster_configs) = &self.file.clusters {
3173 self.populate_clusters(file_cluster_configs.clone())?;
3174 }
3175
3176 let h2_listeners = self
3185 .built
3186 .https_listeners
3187 .iter()
3188 .filter(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
3189 .count();
3190 if h2_listeners > 0 && self.built.buffer_size < H2_MIN_BUFFER_SIZE {
3191 return Err(ConfigError::BufferSizeTooSmallForH2 {
3192 buffer_size: self.built.buffer_size,
3193 minimum: H2_MIN_BUFFER_SIZE,
3194 listeners: h2_listeners,
3195 });
3196 }
3197
3198 if let Some(cap) = self.built.basic_auth_max_credential_bytes {
3208 let third = self.built.buffer_size / 3;
3209 if cap >= third {
3210 warn!(
3211 "basic_auth_max_credential_bytes = {} is >= buffer_size / 3 ({}); \
3212 a hostile peer can pin ~33% of the per-frontend buffer per failed auth \
3213 attempt. Consider lowering basic_auth_max_credential_bytes (typical \
3214 credentials are <100 bytes) or raising buffer_size.",
3215 cap, third
3216 );
3217 }
3218 }
3219
3220 if self.built.evict_on_queue_full && self.built.max_connections < 100 {
3227 let pct = 100usize.div_ceil(self.built.max_connections);
3228 warn!(
3229 "evict_on_queue_full enabled with max_connections = {}; the eviction batch \
3230 clamps to 1, equivalent to ~{}% of capacity per cap event (the knob is \
3231 documented as 1%). Confirm this is intended.",
3232 self.built.max_connections, pct
3233 );
3234 }
3235
3236 let command_socket_path = self.file.command_socket.clone().unwrap_or({
3237 let mut path = env::current_dir().map_err(|e| ConfigError::Env(e.to_string()))?;
3238 path.push("sozu.sock");
3239 let verified_path = path
3240 .to_str()
3241 .ok_or(ConfigError::InvalidPath(path.clone()))?;
3242 verified_path.to_owned()
3243 });
3244
3245 if let (None, Some(true)) = (&self.file.saved_state, &self.file.automatic_state_save) {
3246 return Err(ConfigError::Missing(MissingKind::SavedState));
3247 }
3248
3249 let config = Config {
3250 command_socket: command_socket_path,
3251 ..self.built.clone()
3252 };
3253
3254 debug_assert!(
3260 config.min_buffers <= config.max_buffers,
3261 "min_buffers must not exceed max_buffers"
3262 );
3263 debug_assert!(
3269 !config
3270 .https_listeners
3271 .iter()
3272 .any(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
3273 || config.buffer_size >= H2_MIN_BUFFER_SIZE,
3274 "an h2-advertising config must satisfy the H2 minimum buffer size"
3275 );
3276 Ok(config)
3277 }
3278}
3279
3280#[derive(Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
3284pub struct Config {
3285 pub config_path: String,
3286 pub command_socket: String,
3287 pub command_buffer_size: u64,
3288 pub max_command_buffer_size: u64,
3289 pub max_connections: usize,
3290 pub min_buffers: u64,
3291 pub max_buffers: u64,
3292 pub buffer_size: u64,
3293 pub saved_state: Option<String>,
3294 #[serde(default)]
3295 pub automatic_state_save: bool,
3296 pub log_level: String,
3297 pub log_target: String,
3298 pub log_colored: bool,
3299 #[serde(default)]
3302 pub audit_logs_target: Option<String>,
3303 #[serde(default)]
3306 pub audit_logs_json_target: Option<String>,
3307 #[serde(default)]
3308 pub access_logs_target: Option<String>,
3309 pub access_logs_format: Option<AccessLogFormat>,
3310 pub access_logs_colored: Option<bool>,
3311 pub worker_count: u16,
3312 pub worker_automatic_restart: bool,
3313 pub metrics: Option<MetricsConfig>,
3314 #[serde(default = "default_disable_cluster_metrics")]
3315 pub disable_cluster_metrics: bool,
3316 pub http_listeners: Vec<HttpListenerConfig>,
3317 pub https_listeners: Vec<HttpsListenerConfig>,
3318 pub tcp_listeners: Vec<TcpListenerConfig>,
3319 #[serde(default)]
3320 pub udp_listeners: Vec<UdpListenerConfig>,
3321 pub clusters: HashMap<String, ClusterConfig>,
3322 pub handle_process_affinity: bool,
3323 pub ctl_command_timeout: u64,
3324 pub pid_file_path: Option<String>,
3325 pub activate_listeners: bool,
3326 #[serde(default = "default_front_timeout")]
3327 pub front_timeout: u32,
3328 #[serde(default = "default_back_timeout")]
3329 pub back_timeout: u32,
3330 #[serde(default = "default_connect_timeout")]
3331 pub connect_timeout: u32,
3332 #[serde(default = "default_zombie_check_interval")]
3333 pub zombie_check_interval: u32,
3334 #[serde(default = "default_accept_queue_timeout")]
3335 pub accept_queue_timeout: u32,
3336 #[serde(default = "default_evict_on_queue_full")]
3337 pub evict_on_queue_full: bool,
3338 #[serde(default = "default_request_timeout")]
3339 pub request_timeout: u32,
3340 #[serde(default = "default_worker_timeout")]
3341 pub worker_timeout: u32,
3342 #[serde(default)]
3349 pub slab_entries_per_connection: Option<u64>,
3350 #[serde(default)]
3355 pub command_allowed_uids: Option<Vec<u32>>,
3356 #[serde(default)]
3361 pub basic_auth_max_credential_bytes: Option<u64>,
3362 #[serde(default = "default_max_connections_per_ip")]
3367 pub max_connections_per_ip: u64,
3368 #[serde(default = "default_retry_after")]
3371 pub retry_after: u32,
3372 #[serde(default)]
3379 pub splice_pipe_capacity_bytes: Option<u64>,
3380}
3381
3382fn default_front_timeout() -> u32 {
3383 DEFAULT_FRONT_TIMEOUT
3384}
3385
3386fn default_back_timeout() -> u32 {
3387 DEFAULT_BACK_TIMEOUT
3388}
3389
3390fn default_connect_timeout() -> u32 {
3391 DEFAULT_CONNECT_TIMEOUT
3392}
3393
3394fn default_request_timeout() -> u32 {
3395 DEFAULT_REQUEST_TIMEOUT
3396}
3397
3398fn default_zombie_check_interval() -> u32 {
3399 DEFAULT_ZOMBIE_CHECK_INTERVAL
3400}
3401
3402fn default_accept_queue_timeout() -> u32 {
3403 DEFAULT_ACCEPT_QUEUE_TIMEOUT
3404}
3405
3406fn default_evict_on_queue_full() -> bool {
3407 DEFAULT_EVICT_ON_QUEUE_FULL
3408}
3409
3410fn default_disable_cluster_metrics() -> bool {
3411 DEFAULT_DISABLE_CLUSTER_METRICS
3412}
3413
3414fn default_worker_timeout() -> u32 {
3415 DEFAULT_WORKER_TIMEOUT
3416}
3417
3418fn default_max_connections_per_ip() -> u64 {
3419 DEFAULT_MAX_CONNECTIONS_PER_IP
3420}
3421
3422fn default_retry_after() -> u32 {
3423 DEFAULT_RETRY_AFTER
3424}
3425
3426impl Config {
3427 pub fn load_from_path(path: &str) -> Result<Config, ConfigError> {
3429 let file_config = FileConfig::load_from_path(path)?;
3430
3431 let mut config = ConfigBuilder::new(file_config, path).into_config()?;
3432
3433 config.saved_state = config.saved_state_path()?;
3435
3436 Ok(config)
3437 }
3438
3439 pub fn generate_config_messages(&self) -> Result<Vec<WorkerRequest>, ConfigError> {
3441 let mut v = Vec::new();
3442 let mut count = 0u8;
3443
3444 for listener in &self.http_listeners {
3445 v.push(WorkerRequest {
3446 id: format!("CONFIG-{count}"),
3447 content: RequestType::AddHttpListener(listener.clone()).into(),
3448 });
3449 count += 1;
3450 }
3451
3452 for listener in &self.https_listeners {
3453 v.push(WorkerRequest {
3454 id: format!("CONFIG-{count}"),
3455 content: RequestType::AddHttpsListener(listener.clone()).into(),
3456 });
3457 count += 1;
3458 }
3459
3460 for listener in &self.tcp_listeners {
3461 v.push(WorkerRequest {
3462 id: format!("CONFIG-{count}"),
3463 content: RequestType::AddTcpListener(*listener).into(),
3464 });
3465 count += 1;
3466 }
3467
3468 for listener in &self.udp_listeners {
3469 v.push(WorkerRequest {
3470 id: format!("CONFIG-{count}"),
3471 content: RequestType::AddUdpListener(*listener).into(),
3472 });
3473 count += 1;
3474 }
3475
3476 for cluster in self.clusters.values() {
3477 let mut orders = cluster.generate_requests()?;
3478 for content in orders.drain(..) {
3479 v.push(WorkerRequest {
3480 id: format!("CONFIG-{count}"),
3481 content,
3482 });
3483 count += 1;
3484 }
3485 }
3486
3487 if self.activate_listeners {
3488 for listener in &self.http_listeners {
3489 v.push(WorkerRequest {
3490 id: format!("CONFIG-{count}"),
3491 content: RequestType::ActivateListener(ActivateListener {
3492 address: listener.address,
3493 proxy: ListenerType::Http.into(),
3494 from_scm: false,
3495 })
3496 .into(),
3497 });
3498 count += 1;
3499 }
3500
3501 for listener in &self.https_listeners {
3502 v.push(WorkerRequest {
3503 id: format!("CONFIG-{count}"),
3504 content: RequestType::ActivateListener(ActivateListener {
3505 address: listener.address,
3506 proxy: ListenerType::Https.into(),
3507 from_scm: false,
3508 })
3509 .into(),
3510 });
3511 count += 1;
3512 }
3513
3514 for listener in &self.tcp_listeners {
3515 v.push(WorkerRequest {
3516 id: format!("CONFIG-{count}"),
3517 content: RequestType::ActivateListener(ActivateListener {
3518 address: listener.address,
3519 proxy: ListenerType::Tcp.into(),
3520 from_scm: false,
3521 })
3522 .into(),
3523 });
3524 count += 1;
3525 }
3526
3527 for listener in &self.udp_listeners {
3528 v.push(WorkerRequest {
3529 id: format!("CONFIG-{count}"),
3530 content: RequestType::ActivateListener(ActivateListener {
3531 address: listener.address,
3532 proxy: ListenerType::Udp.into(),
3533 from_scm: false,
3534 })
3535 .into(),
3536 });
3537 count += 1;
3538 }
3539 }
3540
3541 if self.disable_cluster_metrics {
3542 v.push(WorkerRequest {
3543 id: format!("CONFIG-{count}"),
3544 content: RequestType::ConfigureMetrics(MetricsConfiguration::Disabled.into())
3545 .into(),
3546 });
3547 }
3549
3550 Ok(v)
3551 }
3552
3553 pub fn command_socket_path(&self) -> Result<String, ConfigError> {
3555 let config_path_buf = PathBuf::from(self.config_path.clone());
3556 let mut config_dir = config_path_buf
3557 .parent()
3558 .ok_or(ConfigError::NoFileParent(
3559 config_path_buf.to_string_lossy().to_string(),
3560 ))?
3561 .to_path_buf();
3562
3563 let socket_path = PathBuf::from(self.command_socket.clone());
3564
3565 let mut socket_parent_dir = match socket_path.parent() {
3566 None => config_dir,
3569 Some(path) => {
3570 config_dir.push(path);
3572 config_dir.canonicalize().map_err(|io_error| {
3574 ConfigError::SocketPathError(format!(
3575 "Could not canonicalize path {config_dir:?}: {io_error}"
3576 ))
3577 })?
3578 }
3579 };
3580
3581 let socket_name = socket_path
3582 .file_name()
3583 .ok_or(ConfigError::SocketPathError(format!(
3584 "could not get command socket file name from {socket_path:?}"
3585 )))?;
3586
3587 socket_parent_dir.push(socket_name);
3589
3590 let command_socket_path = socket_parent_dir
3591 .to_str()
3592 .ok_or(ConfigError::SocketPathError(format!(
3593 "Invalid socket path {socket_parent_dir:?}"
3594 )))?
3595 .to_string();
3596
3597 Ok(command_socket_path)
3598 }
3599
3600 fn saved_state_path(&self) -> Result<Option<String>, ConfigError> {
3602 let path = match self.saved_state.as_ref() {
3603 Some(path) => path,
3604 None => return Ok(None),
3605 };
3606
3607 debug!("saved_stated path in the config: {}", path);
3608 let config_path = PathBuf::from(self.config_path.clone());
3609
3610 debug!("Config path buffer: {:?}", config_path);
3611 let config_dir = config_path
3612 .parent()
3613 .ok_or(ConfigError::SaveStatePath(format!(
3614 "Could get parent directory of config file {config_path:?}"
3615 )))?;
3616
3617 debug!("Config folder: {:?}", config_dir);
3618 if !config_dir.exists() {
3619 create_dir_all(config_dir).map_err(|io_error| {
3620 ConfigError::SaveStatePath(format!(
3621 "failed to create state parent directory '{config_dir:?}': {io_error}"
3622 ))
3623 })?;
3624 }
3625
3626 let mut saved_state_path_raw = config_dir.to_path_buf();
3627 saved_state_path_raw.push(path);
3628 debug!(
3629 "Looking for saved state on the path {:?}",
3630 saved_state_path_raw
3631 );
3632
3633 match metadata(path) {
3634 Err(err) if matches!(err.kind(), ErrorKind::NotFound) => {
3635 info!("Create an empty state file at '{}'", path);
3636 File::create(path).map_err(|io_error| {
3637 ConfigError::SaveStatePath(format!(
3638 "failed to create state file '{path:?}': {io_error}"
3639 ))
3640 })?;
3641 }
3642 _ => {}
3643 }
3644
3645 saved_state_path_raw.canonicalize().map_err(|io_error| {
3646 ConfigError::SaveStatePath(format!(
3647 "could not get saved state path from config file input {path:?}: {io_error}"
3648 ))
3649 })?;
3650
3651 let stringified_path = saved_state_path_raw
3652 .to_str()
3653 .ok_or(ConfigError::SaveStatePath(format!(
3654 "Invalid path {saved_state_path_raw:?}"
3655 )))?
3656 .to_string();
3657
3658 Ok(Some(stringified_path))
3659 }
3660
3661 pub fn load_file(path: &str) -> Result<String, ConfigError> {
3663 std::fs::read_to_string(path).map_err(|io_error| ConfigError::FileRead {
3664 path_to_read: path.to_owned(),
3665 io_error,
3666 })
3667 }
3668
3669 pub fn load_file_bytes(path: &str) -> Result<Vec<u8>, ConfigError> {
3671 std::fs::read(path).map_err(|io_error| ConfigError::FileRead {
3672 path_to_read: path.to_owned(),
3673 io_error,
3674 })
3675 }
3676}
3677
3678impl fmt::Debug for Config {
3679 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3680 f.debug_struct("Config")
3681 .field("config_path", &self.config_path)
3682 .field("command_socket", &self.command_socket)
3683 .field("command_buffer_size", &self.command_buffer_size)
3684 .field("max_command_buffer_size", &self.max_command_buffer_size)
3685 .field("max_connections", &self.max_connections)
3686 .field("min_buffers", &self.min_buffers)
3687 .field("max_buffers", &self.max_buffers)
3688 .field("buffer_size", &self.buffer_size)
3689 .field("saved_state", &self.saved_state)
3690 .field("automatic_state_save", &self.automatic_state_save)
3691 .field("log_level", &self.log_level)
3692 .field("log_target", &self.log_target)
3693 .field("access_logs_target", &self.access_logs_target)
3694 .field("audit_logs_target", &self.audit_logs_target)
3695 .field("audit_logs_json_target", &self.audit_logs_json_target)
3696 .field("access_logs_format", &self.access_logs_format)
3697 .field("worker_count", &self.worker_count)
3698 .field("worker_automatic_restart", &self.worker_automatic_restart)
3699 .field("metrics", &self.metrics)
3700 .field("disable_cluster_metrics", &self.disable_cluster_metrics)
3701 .field("handle_process_affinity", &self.handle_process_affinity)
3702 .field("ctl_command_timeout", &self.ctl_command_timeout)
3703 .field("pid_file_path", &self.pid_file_path)
3704 .field("activate_listeners", &self.activate_listeners)
3705 .field("front_timeout", &self.front_timeout)
3706 .field("back_timeout", &self.back_timeout)
3707 .field("connect_timeout", &self.connect_timeout)
3708 .field("zombie_check_interval", &self.zombie_check_interval)
3709 .field("accept_queue_timeout", &self.accept_queue_timeout)
3710 .field("evict_on_queue_full", &self.evict_on_queue_full)
3711 .field("request_timeout", &self.request_timeout)
3712 .field("worker_timeout", &self.worker_timeout)
3713 .finish()
3714 }
3715}
3716
3717fn display_toml_error(file: &str, error: &toml::de::Error) {
3718 println!("error parsing the configuration file '{file}': {error}");
3719 if let Some(Range { start, end }) = error.span() {
3720 print!("error parsing the configuration file '{file}' at position: {start}, {end}");
3721 }
3722}
3723
3724impl ServerConfig {
3725 pub const DEFAULT_SLAB_ENTRIES_PER_CONNECTION: u64 = 4;
3732 pub const MIN_SLAB_ENTRIES_PER_CONNECTION: u64 = 2;
3735 pub const MAX_SLAB_ENTRIES_PER_CONNECTION: u64 = 32;
3738
3739 pub fn effective_slab_entries_per_connection(&self) -> u64 {
3742 let effective = match self.slab_entries_per_connection {
3743 Some(0) | None => Self::DEFAULT_SLAB_ENTRIES_PER_CONNECTION,
3744 Some(n) => n.clamp(
3745 Self::MIN_SLAB_ENTRIES_PER_CONNECTION,
3746 Self::MAX_SLAB_ENTRIES_PER_CONNECTION,
3747 ),
3748 };
3749 debug_assert!(
3753 (Self::MIN_SLAB_ENTRIES_PER_CONNECTION..=Self::MAX_SLAB_ENTRIES_PER_CONNECTION)
3754 .contains(&effective),
3755 "effective slab entries per connection must stay within [MIN, MAX]"
3756 );
3757 effective
3758 }
3759
3760 pub fn slab_capacity(&self) -> u64 {
3767 let per_conn = self.effective_slab_entries_per_connection();
3768 let capacity = 10 + per_conn * self.max_connections;
3769 debug_assert!(
3774 capacity >= 10,
3775 "slab capacity must reserve the base entries"
3776 );
3777 debug_assert!(
3778 self.max_connections == 0 || capacity > 10,
3779 "a non-zero connection cap must reserve per-connection slab entries"
3780 );
3781 capacity
3782 }
3783}
3784
3785impl From<&Config> for ServerConfig {
3787 fn from(config: &Config) -> Self {
3788 let metrics = config.metrics.clone().map(|m| ServerMetricsConfig {
3789 address: m.address.to_string(),
3790 tagged_metrics: m.tagged_metrics,
3791 prefix: m.prefix,
3792 detail: Some(MetricDetail::from(m.detail) as i32),
3793 });
3794 let server_config = Self {
3795 max_connections: config.max_connections as u64,
3796 front_timeout: config.front_timeout,
3797 back_timeout: config.back_timeout,
3798 connect_timeout: config.connect_timeout,
3799 zombie_check_interval: config.zombie_check_interval,
3800 accept_queue_timeout: config.accept_queue_timeout,
3801 min_buffers: config.min_buffers,
3802 max_buffers: config.max_buffers,
3803 buffer_size: config.buffer_size,
3804 log_level: config.log_level.clone(),
3805 log_target: config.log_target.clone(),
3806 access_logs_target: config.access_logs_target.clone(),
3807 audit_logs_target: config.audit_logs_target.clone(),
3808 audit_logs_json_target: config.audit_logs_json_target.clone(),
3809 command_buffer_size: config.command_buffer_size,
3810 max_command_buffer_size: config.max_command_buffer_size,
3811 metrics,
3812 access_log_format: ProtobufAccessLogFormat::from(&config.access_logs_format) as i32,
3813 log_colored: config.log_colored,
3814 slab_entries_per_connection: config.slab_entries_per_connection,
3815 basic_auth_max_credential_bytes: config.basic_auth_max_credential_bytes,
3816 evict_on_queue_full: Some(config.evict_on_queue_full),
3817 max_connections_per_ip: Some(config.max_connections_per_ip),
3818 retry_after: Some(config.retry_after),
3819 splice_pipe_capacity_bytes: config.splice_pipe_capacity_bytes,
3820 };
3821
3822 debug_assert!(
3827 server_config.min_buffers <= server_config.max_buffers,
3828 "ServerConfig must preserve min_buffers <= max_buffers"
3829 );
3830 debug_assert_eq!(
3831 server_config.buffer_size, config.buffer_size,
3832 "ServerConfig buffer_size must mirror the source config"
3833 );
3834 debug_assert_eq!(
3835 server_config.max_connections, config.max_connections as u64,
3836 "ServerConfig max_connections must mirror the source config"
3837 );
3838 server_config
3839 }
3840}
3841
3842#[cfg(test)]
3843mod tests {
3844 use toml::to_string;
3845
3846 use super::*;
3847
3848 #[test]
3849 fn hsts_to_proto_enabled_substitutes_default_max_age() {
3850 let cfg = FileHstsConfig {
3851 enabled: Some(true),
3852 max_age: None,
3853 include_subdomains: None,
3854 preload: None,
3855 force_replace_backend: None,
3856 };
3857 let proto = cfg.to_proto("test").expect("should validate");
3858 assert_eq!(proto.enabled, Some(true));
3859 assert_eq!(proto.max_age, Some(DEFAULT_HSTS_MAX_AGE));
3860 }
3861
3862 #[test]
3863 fn hsts_to_proto_explicit_max_age_kept() {
3864 let cfg = FileHstsConfig {
3865 enabled: Some(true),
3866 max_age: Some(63_072_000),
3867 include_subdomains: Some(true),
3868 preload: Some(true),
3869 force_replace_backend: None,
3870 };
3871 let proto = cfg.to_proto("test").expect("should validate");
3872 assert_eq!(proto.max_age, Some(63_072_000));
3873 assert_eq!(proto.include_subdomains, Some(true));
3874 assert_eq!(proto.preload, Some(true));
3875 }
3876
3877 #[test]
3878 fn hsts_to_proto_disabled_keeps_zero_intent() {
3879 let cfg = FileHstsConfig {
3883 enabled: Some(false),
3884 max_age: None,
3885 include_subdomains: None,
3886 preload: None,
3887 force_replace_backend: None,
3888 };
3889 let proto = cfg.to_proto("test").expect("should validate");
3890 assert_eq!(proto.enabled, Some(false));
3891 }
3892
3893 #[test]
3894 fn hsts_to_proto_kill_switch_max_age_zero_allowed() {
3895 let cfg = FileHstsConfig {
3899 enabled: Some(true),
3900 max_age: Some(0),
3901 include_subdomains: None,
3902 preload: None,
3903 force_replace_backend: None,
3904 };
3905 let proto = cfg.to_proto("test").expect("kill-switch must validate");
3906 assert_eq!(proto.max_age, Some(0));
3907 }
3908
3909 #[test]
3910 fn hsts_to_proto_missing_enabled_errors() {
3911 let cfg = FileHstsConfig {
3912 enabled: None,
3913 max_age: Some(31_536_000),
3914 include_subdomains: None,
3915 preload: None,
3916 force_replace_backend: None,
3917 };
3918 match cfg.to_proto("test").unwrap_err() {
3919 ConfigError::HstsEnabledRequired(scope) => assert_eq!(scope, "test"),
3920 other => panic!("expected HstsEnabledRequired, got {other:?}"),
3921 }
3922 }
3923
3924 #[test]
3925 fn hsts_rejected_on_http_listener() {
3926 let mut listener = ListenerBuilder::new(
3931 SocketAddress::new_v4(127, 0, 0, 1, 8080),
3932 ListenerProtocol::Http,
3933 );
3934 listener.hsts = Some(FileHstsConfig {
3935 enabled: Some(true),
3936 max_age: Some(31_536_000),
3937 include_subdomains: None,
3938 preload: None,
3939 force_replace_backend: None,
3940 });
3941 match listener.to_http(None).unwrap_err() {
3942 ConfigError::HstsOnPlainHttp(scope) => assert!(
3943 scope.contains("HTTP listener"),
3944 "expected scope to mention 'HTTP listener', got {scope:?}"
3945 ),
3946 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
3947 }
3948 }
3949
3950 #[test]
3951 fn hsts_rejected_on_http_frontend() {
3952 let frontend = FileClusterFrontendConfig {
3958 address: "127.0.0.1:8080".parse().unwrap(),
3959 hostname: Some("example.com".to_owned()),
3960 path: None,
3961 path_type: None,
3962 method: None,
3963 certificate: None,
3964 key: None,
3965 certificate_chain: None,
3966 tls_versions: vec![],
3967 position: RulePosition::Tree,
3968 tags: None,
3969 redirect: None,
3970 redirect_scheme: None,
3971 redirect_template: None,
3972 rewrite_host: None,
3973 rewrite_path: None,
3974 rewrite_port: None,
3975 required_auth: None,
3976 headers: None,
3977 hsts: Some(FileHstsConfig {
3978 enabled: Some(true),
3979 max_age: Some(31_536_000),
3980 include_subdomains: None,
3981 preload: None,
3982 force_replace_backend: None,
3983 }),
3984 };
3985 match frontend.to_http_front("api").unwrap_err() {
3986 ConfigError::HstsOnPlainHttp(scope) => {
3987 assert!(
3988 scope.contains("api") && scope.contains("example.com"),
3989 "expected scope to mention 'api' and 'example.com', got {scope:?}"
3990 );
3991 }
3992 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
3993 }
3994 }
3995
3996 #[test]
3997 fn serialize() {
3998 let http = ListenerBuilder::new(
3999 SocketAddress::new_v4(127, 0, 0, 1, 8080),
4000 ListenerProtocol::Http,
4001 )
4002 .with_answer_404_path(Some("404.html"))
4003 .to_owned();
4004 println!("http: {:?}", to_string(&http));
4005
4006 let https = ListenerBuilder::new(
4007 SocketAddress::new_v4(127, 0, 0, 1, 8443),
4008 ListenerProtocol::Https,
4009 )
4010 .with_answer_404_path(Some("404.html"))
4011 .to_owned();
4012 println!("https: {:?}", to_string(&https));
4013
4014 let listeners = vec![http, https];
4015 let config = FileConfig {
4016 command_socket: Some(String::from("./command_folder/sock")),
4017 worker_count: Some(2),
4018 worker_automatic_restart: Some(true),
4019 max_connections: Some(500),
4020 min_buffers: Some(1),
4021 max_buffers: Some(500),
4022 buffer_size: Some(16393),
4023 metrics: Some(MetricsConfig {
4024 address: "127.0.0.1:8125".parse().unwrap(),
4025 tagged_metrics: false,
4026 prefix: Some(String::from("sozu-metrics")),
4027 detail: MetricDetailLevel::default(),
4028 }),
4029 listeners: Some(listeners),
4030 ..Default::default()
4031 };
4032
4033 println!("config: {:?}", to_string(&config));
4034 let encoded = to_string(&config).unwrap();
4035 println!("conf:\n{encoded}");
4036 }
4037
4038 #[test]
4039 fn parse() {
4040 let path = "assets/config.toml";
4041 let config = Config::load_from_path(path).unwrap_or_else(|load_error| {
4042 panic!("Cannot load config from path {path}: {load_error:?}")
4043 });
4044 println!("config: {config:#?}");
4045 }
4047
4048 #[test]
4049 fn multiple_listeners_preserve_per_address_expect_proxy() {
4050 let toml_content = r#"
4051 command_socket = "/tmp/sozu_test.sock"
4052 worker_count = 1
4053
4054 [[listeners]]
4055 protocol = "http"
4056 address = "172.16.20.1:80"
4057 expect_proxy = true
4058
4059 [[listeners]]
4060 protocol = "http"
4061 address = "10.22.0.1:80"
4062 expect_proxy = false
4063
4064 [[listeners]]
4065 protocol = "https"
4066 address = "192.168.1.1:443"
4067 expect_proxy = true
4068
4069 [[listeners]]
4070 protocol = "https"
4071 address = "192.168.2.1:443"
4072 expect_proxy = false
4073 "#;
4074
4075 let file_config: FileConfig =
4076 toml::from_str(toml_content).expect("Could not parse TOML config");
4077
4078 let listeners = file_config.listeners.as_ref().expect("No listeners found");
4079 assert_eq!(listeners.len(), 4);
4080
4081 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4082 .into_config()
4083 .expect("Could not build config");
4084
4085 assert_eq!(config.http_listeners.len(), 2);
4086 assert_eq!(config.https_listeners.len(), 2);
4087
4088 let http_proxy = config
4090 .http_listeners
4091 .iter()
4092 .find(|l| SocketAddr::from(l.address) == "172.16.20.1:80".parse().unwrap())
4093 .expect("Listener on 172.16.20.1:80 not found");
4094 let http_direct = config
4095 .http_listeners
4096 .iter()
4097 .find(|l| SocketAddr::from(l.address) == "10.22.0.1:80".parse().unwrap())
4098 .expect("Listener on 10.22.0.1:80 not found");
4099
4100 assert!(http_proxy.expect_proxy);
4101 assert!(!http_direct.expect_proxy);
4102
4103 let https_proxy = config
4105 .https_listeners
4106 .iter()
4107 .find(|l| SocketAddr::from(l.address) == "192.168.1.1:443".parse().unwrap())
4108 .expect("Listener on 192.168.1.1:443 not found");
4109 let https_direct = config
4110 .https_listeners
4111 .iter()
4112 .find(|l| SocketAddr::from(l.address) == "192.168.2.1:443".parse().unwrap())
4113 .expect("Listener on 192.168.2.1:443 not found");
4114
4115 assert!(https_proxy.expect_proxy);
4116 assert!(!https_direct.expect_proxy);
4117 }
4118
4119 #[test]
4120 fn multiple_listeners_generate_correct_worker_requests() {
4121 let toml_content = r#"
4122 command_socket = "/tmp/sozu_test.sock"
4123 worker_count = 1
4124 activate_listeners = true
4125
4126 [[listeners]]
4127 protocol = "http"
4128 address = "172.16.20.1:80"
4129 expect_proxy = true
4130
4131 [[listeners]]
4132 protocol = "http"
4133 address = "10.22.0.1:80"
4134 expect_proxy = false
4135 "#;
4136
4137 let file_config: FileConfig =
4138 toml::from_str(toml_content).expect("Could not parse TOML config");
4139
4140 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4141 .into_config()
4142 .expect("Could not build config");
4143
4144 let messages = config
4145 .generate_config_messages()
4146 .expect("Could not generate config messages");
4147
4148 let add_listener_count = messages
4149 .iter()
4150 .filter(|m| {
4151 matches!(
4152 m.content.request_type,
4153 Some(RequestType::AddHttpListener(_))
4154 )
4155 })
4156 .count();
4157
4158 let activate_listener_count = messages
4159 .iter()
4160 .filter(|m| {
4161 matches!(
4162 m.content.request_type,
4163 Some(RequestType::ActivateListener(ActivateListener {
4164 proxy,
4165 ..
4166 })) if proxy == ListenerType::Http as i32
4167 )
4168 })
4169 .count();
4170
4171 assert_eq!(add_listener_count, 2);
4172 assert_eq!(activate_listener_count, 2);
4173 }
4174
4175 #[test]
4176 fn documented_udp_dns_example_loads_and_emits_udp_requests() {
4177 let toml_content = r#"
4184 command_socket = "/tmp/sozu_test.sock"
4185 worker_count = 1
4186 activate_listeners = true
4187
4188 [[listeners]]
4189 protocol = "udp"
4190 address = "0.0.0.0:53"
4191
4192 [clusters.dns]
4193 protocol = "tcp"
4194 load_balancing = "HRW"
4195 frontends = [
4196 { address = "0.0.0.0:53" }
4197 ]
4198 backends = [
4199 { address = "10.0.0.10:53" },
4200 { address = "10.0.0.11:53" }
4201 ]
4202
4203 [clusters.dns.udp]
4204 affinity_key = "SOURCE_IP"
4205 responses = 1
4206 requests = 0
4207 send_proxy_protocol = true
4208
4209 [clusters.dns.udp.health]
4210 mode = "TCP_PROBE"
4211 tcp_port = 53
4212 rise = 2
4213 fall = 3
4214 fail_open = true
4215 "#;
4216
4217 let file_config: FileConfig =
4218 toml::from_str(toml_content).expect("Could not parse documented DNS TOML");
4219
4220 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4221 .into_config()
4222 .expect("documented UDP DNS example must load without WrongFrontendProtocol");
4223
4224 assert_eq!(
4226 config.udp_listeners.len(),
4227 1,
4228 "the protocol=\"udp\" listener must be built"
4229 );
4230
4231 let messages = config
4232 .generate_config_messages()
4233 .expect("Could not generate config messages");
4234
4235 let add_udp_listener_count = messages
4236 .iter()
4237 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpListener(_))))
4238 .count();
4239 assert_eq!(
4240 add_udp_listener_count, 1,
4241 "must emit exactly one AddUdpListener"
4242 );
4243
4244 let add_udp_frontend_count = messages
4245 .iter()
4246 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
4247 .count();
4248 assert_eq!(
4249 add_udp_frontend_count, 1,
4250 "the cluster frontend on the UDP listener must emit AddUdpFrontend"
4251 );
4252
4253 let add_tcp_frontend_count = messages
4255 .iter()
4256 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddTcpFrontend(_))))
4257 .count();
4258 assert_eq!(
4259 add_tcp_frontend_count, 0,
4260 "a UDP-listener-addressed frontend must not be emitted as AddTcpFrontend"
4261 );
4262
4263 let udp_frontend = messages
4265 .iter()
4266 .find_map(|m| match &m.content.request_type {
4267 Some(RequestType::AddUdpFrontend(f)) => Some(f),
4268 _ => None,
4269 })
4270 .expect("AddUdpFrontend must be present");
4271 assert_eq!(udp_frontend.cluster_id, "dns");
4272 assert_eq!(
4273 SocketAddr::from(udp_frontend.address),
4274 "0.0.0.0:53".parse().unwrap()
4275 );
4276
4277 let cluster = messages
4280 .iter()
4281 .find_map(|m| match &m.content.request_type {
4282 Some(RequestType::AddCluster(c)) if c.cluster_id == "dns" => Some(c),
4283 _ => None,
4284 })
4285 .expect("AddCluster for 'dns' must be present");
4286 let udp = cluster
4287 .udp
4288 .as_ref()
4289 .expect("[clusters.dns.udp] block must carry onto the cluster");
4290 assert_eq!(udp.responses, Some(1));
4291 }
4292
4293 #[test]
4294 fn duplicate_listener_address_rejected() {
4295 let toml_content = r#"
4296 command_socket = "/tmp/sozu_test.sock"
4297 worker_count = 1
4298
4299 [[listeners]]
4300 protocol = "http"
4301 address = "0.0.0.0:80"
4302
4303 [[listeners]]
4304 protocol = "http"
4305 address = "0.0.0.0:80"
4306 "#;
4307
4308 let file_config: FileConfig =
4309 toml::from_str(toml_content).expect("Could not parse TOML config");
4310
4311 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4312
4313 assert!(
4314 result.is_err(),
4315 "Should reject duplicate listener addresses"
4316 );
4317 }
4318
4319 #[test]
4320 fn buffer_size_below_h2_minimum_rejected() {
4321 let toml_content = r#"
4323 command_socket = "/tmp/sozu_test.sock"
4324 worker_count = 1
4325 buffer_size = 8192
4326
4327 [[listeners]]
4328 protocol = "https"
4329 address = "127.0.0.1:8443"
4330 "#;
4331 let file_config: FileConfig =
4332 toml::from_str(toml_content).expect("Could not parse TOML config");
4333 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4334 match result {
4335 Err(ConfigError::BufferSizeTooSmallForH2 {
4336 buffer_size: 8192,
4337 minimum: 16_393,
4338 listeners: 1,
4339 }) => {}
4340 other => panic!("expected BufferSizeTooSmallForH2, got {other:?}"),
4341 }
4342 }
4343
4344 #[test]
4345 fn buffer_size_below_h2_minimum_accepted_when_no_h2_listener() {
4346 let toml_content = r#"
4348 command_socket = "/tmp/sozu_test.sock"
4349 worker_count = 1
4350 buffer_size = 8192
4351
4352 [[listeners]]
4353 protocol = "https"
4354 address = "127.0.0.1:8443"
4355 alpn_protocols = ["http/1.1"]
4356 "#;
4357 let file_config: FileConfig =
4358 toml::from_str(toml_content).expect("Could not parse TOML config");
4359 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4360 assert!(
4361 result.is_ok(),
4362 "non-H2 HTTPS listener with sub-16393 buffer should be accepted: {result:?}"
4363 );
4364 }
4365
4366 #[test]
4367 fn buffer_size_at_h2_minimum_accepted() {
4368 let toml_content = r#"
4369 command_socket = "/tmp/sozu_test.sock"
4370 worker_count = 1
4371 buffer_size = 16393
4372
4373 [[listeners]]
4374 protocol = "https"
4375 address = "127.0.0.1:8443"
4376 "#;
4377 let file_config: FileConfig =
4378 toml::from_str(toml_content).expect("Could not parse TOML config");
4379 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4380 assert!(
4381 result.is_ok(),
4382 "buffer_size at the H2 minimum should be accepted: {result:?}"
4383 );
4384 }
4385
4386 #[test]
4387 fn alpn_protocols_default() {
4388 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4389 let config = builder.to_tls(None).expect("to_tls should succeed");
4390 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4391 }
4392
4393 #[test]
4394 fn alpn_protocols_custom() {
4395 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4396 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned()]));
4397 let config = builder.to_tls(None).expect("to_tls should succeed");
4398 assert_eq!(config.alpn_protocols, vec!["http/1.1"]);
4399 }
4400
4401 #[test]
4402 fn alpn_protocols_invalid_rejected() {
4403 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4404 builder.with_alpn_protocols(Some(vec!["h3".to_owned()]));
4405 let result = builder.to_tls(None);
4406 assert!(result.is_err());
4407 let err = result.unwrap_err();
4408 assert!(
4409 err.to_string().contains("h3"),
4410 "error should mention the invalid protocol: {err}"
4411 );
4412 }
4413
4414 #[test]
4415 fn alpn_protocols_empty_uses_default() {
4416 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4417 builder.with_alpn_protocols(Some(vec![]));
4418 let config = builder.to_tls(None).expect("to_tls should succeed");
4419 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4420 }
4421
4422 #[test]
4423 fn alpn_protocols_deduplicated() {
4424 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4425 builder.with_alpn_protocols(Some(vec![
4426 "h2".to_owned(),
4427 "h2".to_owned(),
4428 "http/1.1".to_owned(),
4429 ]));
4430 let config = builder.to_tls(None).expect("to_tls should succeed");
4431 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4432 }
4433
4434 #[test]
4435 fn alpn_protocols_order_preserved() {
4436 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4437 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned(), "h2".to_owned()]));
4438 let config = builder.to_tls(None).expect("to_tls should succeed");
4439 assert_eq!(config.alpn_protocols, vec!["http/1.1", "h2"]);
4440 }
4441
4442 #[test]
4448 fn parse_header_edit_rejects_crlf_in_value() {
4449 let entry = HeaderEditConfig {
4450 position: "request".to_owned(),
4451 key: "X-Test".to_owned(),
4452 value: "value\r\nEvil-Header: stolen".to_owned(),
4453 };
4454 let err = parse_header_edit(0, &entry).expect_err("CRLF in value must be rejected");
4455 match err {
4456 ConfigError::InvalidHeaderBytes { index, field } => {
4457 assert_eq!(index, 0);
4458 assert_eq!(field, "value");
4459 }
4460 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
4461 }
4462 }
4463
4464 #[test]
4465 fn parse_header_edit_rejects_lf_in_key() {
4466 let entry = HeaderEditConfig {
4467 position: "response".to_owned(),
4468 key: "X-\nTest".to_owned(),
4469 value: "ok".to_owned(),
4470 };
4471 let err = parse_header_edit(2, &entry).expect_err("LF in key must be rejected");
4472 match err {
4473 ConfigError::InvalidHeaderBytes { index, field } => {
4474 assert_eq!(index, 2);
4475 assert_eq!(field, "key");
4476 }
4477 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
4478 }
4479 }
4480
4481 #[test]
4482 fn parse_header_edit_rejects_nul() {
4483 let entry = HeaderEditConfig {
4484 position: "both".to_owned(),
4485 key: "X-Test".to_owned(),
4486 value: "with\0nul".to_owned(),
4487 };
4488 assert!(matches!(
4489 parse_header_edit(0, &entry),
4490 Err(ConfigError::InvalidHeaderBytes { .. })
4491 ));
4492 }
4493
4494 #[test]
4500 fn parse_header_edit_accepts_tab_in_value() {
4501 let entry = HeaderEditConfig {
4502 position: "request".to_owned(),
4503 key: "X-Test".to_owned(),
4504 value: "with\ttab".to_owned(),
4505 };
4506 let header = parse_header_edit(0, &entry).expect("tab in value must be accepted");
4507 assert_eq!(header.val, "with\ttab");
4508 }
4509
4510 #[test]
4517 fn parse_header_edit_rejects_tab_in_key() {
4518 let entry = HeaderEditConfig {
4519 position: "request".to_owned(),
4520 key: "Host\t".to_owned(),
4521 value: "ok".to_owned(),
4522 };
4523 let err = parse_header_edit(0, &entry).expect_err("HTAB in key must be rejected");
4524 match err {
4525 ConfigError::InvalidHeaderBytes { field, .. } => assert_eq!(field, "key"),
4526 other => panic!("expected InvalidHeaderBytes{{field=\"key\"}}, got {other:?}"),
4527 }
4528 }
4529
4530 #[test]
4531 fn parse_header_edit_rejects_space_in_key() {
4532 let entry = HeaderEditConfig {
4533 position: "request".to_owned(),
4534 key: "X Test".to_owned(),
4535 value: "ok".to_owned(),
4536 };
4537 let err = parse_header_edit(0, &entry).expect_err("SP in key must be rejected");
4538 assert!(matches!(err, ConfigError::InvalidHeaderBytes { .. }));
4539 }
4540
4541 #[test]
4542 fn parse_header_edit_rejects_empty_key() {
4543 let entry = HeaderEditConfig {
4544 position: "request".to_owned(),
4545 key: String::new(),
4546 value: "ok".to_owned(),
4547 };
4548 let err = parse_header_edit(0, &entry).expect_err("empty key must be rejected");
4549 assert!(matches!(
4550 err,
4551 ConfigError::InvalidHeaderBytes { field: "key", .. }
4552 ));
4553 }
4554
4555 #[test]
4556 fn parse_header_edit_accepts_clean_value() {
4557 let entry = HeaderEditConfig {
4558 position: "request".to_owned(),
4559 key: "X-Tenant".to_owned(),
4560 value: "alpha".to_owned(),
4561 };
4562 let header = parse_header_edit(0, &entry).expect("clean value must be accepted");
4563 assert_eq!(header.key, "X-Tenant");
4564 assert_eq!(header.val, "alpha");
4565 }
4566
4567 #[test]
4571 fn resolve_answer_source_bare_string_is_literal() {
4572 let body = resolve_answer_source("HTTP/1.1 503 Service Unavailable\r\n\r\nbusy")
4573 .expect("bare-string source must resolve");
4574 assert_eq!(body, "HTTP/1.1 503 Service Unavailable\r\n\r\nbusy");
4575 }
4576
4577 #[test]
4578 fn resolve_answer_source_empty_string_is_legitimate() {
4579 let body = resolve_answer_source("").expect("empty source must resolve");
4580 assert_eq!(body, "");
4581 }
4582
4583 #[test]
4588 fn resolve_answer_source_file_scheme_missing_file_errors() {
4589 let err = resolve_answer_source("file:///nonexistent/sozu-test/never.http")
4590 .expect_err("missing path must error");
4591 assert!(matches!(err, ConfigError::FileOpen { .. }));
4592 }
4593
4594 #[test]
4597 fn resolve_answer_source_file_scheme_empty_path_errors() {
4598 let err = resolve_answer_source("file://").expect_err("empty path must error");
4599 assert!(matches!(err, ConfigError::FileOpen { .. }));
4600 }
4601}