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 if let Some(soft_limit) = soft_rlimit_nofile() {
1207 let advisory = soft_limit.saturating_mul(7) / 10;
1208 if u64::from(max_flows) > advisory {
1209 warn!(
1210 "UDP listener {}: max_flows = {} exceeds ~70% of the soft RLIMIT_NOFILE ({}); \
1211 per-flow connected sockets may hit EMFILE",
1212 self.address, max_flows, advisory
1213 );
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")]
1367pub enum MetricDetailLevel {
1368 Process,
1369 Frontend,
1370 Cluster,
1371 Backend,
1372}
1373
1374impl Default for MetricDetailLevel {
1375 fn default() -> Self {
1376 Self::Cluster
1379 }
1380}
1381
1382impl From<MetricDetailLevel> for MetricDetail {
1383 fn from(level: MetricDetailLevel) -> Self {
1384 match level {
1385 MetricDetailLevel::Process => MetricDetail::DetailProcess,
1386 MetricDetailLevel::Frontend => MetricDetail::DetailFrontend,
1387 MetricDetailLevel::Cluster => MetricDetail::DetailCluster,
1388 MetricDetailLevel::Backend => MetricDetail::DetailBackend,
1389 }
1390 }
1391}
1392
1393impl From<MetricDetail> for MetricDetailLevel {
1394 fn from(detail: MetricDetail) -> Self {
1398 match detail {
1399 MetricDetail::DetailProcess => MetricDetailLevel::Process,
1400 MetricDetail::DetailFrontend => MetricDetailLevel::Frontend,
1401 MetricDetail::DetailCluster => MetricDetailLevel::Cluster,
1402 MetricDetail::DetailBackend => MetricDetailLevel::Backend,
1403 }
1404 }
1405}
1406
1407#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1408#[serde(deny_unknown_fields)]
1409pub struct MetricsConfig {
1410 pub address: SocketAddr,
1411 #[serde(default)]
1412 pub tagged_metrics: bool,
1413 #[serde(default)]
1414 pub prefix: Option<String>,
1415 #[serde(default)]
1418 pub detail: MetricDetailLevel,
1419}
1420
1421#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1422#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
1423#[serde(deny_unknown_fields)]
1424pub enum PathRuleType {
1425 Prefix,
1426 Regex,
1427 Equals,
1428}
1429
1430#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1431#[serde(deny_unknown_fields)]
1432pub struct FileClusterFrontendConfig {
1433 pub address: SocketAddr,
1434 pub hostname: Option<String>,
1435 pub path: Option<String>,
1437 pub path_type: Option<PathRuleType>,
1439 pub method: Option<String>,
1440 pub certificate: Option<String>,
1441 pub key: Option<String>,
1442 pub certificate_chain: Option<String>,
1443 #[serde(default)]
1444 pub tls_versions: Vec<TlsVersion>,
1445 #[serde(default)]
1446 pub position: RulePosition,
1447 pub tags: Option<BTreeMap<String, String>>,
1448 pub redirect: Option<String>,
1453 pub redirect_scheme: Option<String>,
1457 pub redirect_template: Option<String>,
1461 pub rewrite_host: Option<String>,
1464 pub rewrite_path: Option<String>,
1466 pub rewrite_port: Option<u32>,
1468 pub required_auth: Option<bool>,
1472 pub headers: Option<Vec<HeaderEditConfig>>,
1476 pub hsts: Option<FileHstsConfig>,
1481}
1482
1483#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1490#[serde(deny_unknown_fields)]
1491pub struct HeaderEditConfig {
1492 pub position: String,
1493 pub key: String,
1494 pub value: String,
1495}
1496
1497#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1508#[serde(deny_unknown_fields)]
1509pub struct FileHstsConfig {
1510 pub enabled: Option<bool>,
1513 pub max_age: Option<u32>,
1518 pub include_subdomains: Option<bool>,
1520 pub preload: Option<bool>,
1523 pub force_replace_backend: Option<bool>,
1531}
1532
1533impl FileHstsConfig {
1534 pub fn to_proto(&self, scope: &str) -> Result<HstsConfig, ConfigError> {
1552 let enabled = match self.enabled {
1553 Some(v) => v,
1554 None => return Err(ConfigError::HstsEnabledRequired(scope.to_owned())),
1555 };
1556
1557 let max_age = match (enabled, self.max_age) {
1558 (true, None) => Some(DEFAULT_HSTS_MAX_AGE),
1559 (_, m) => m,
1560 };
1561
1562 if let Some(value) = max_age
1563 && value > 0
1564 && value < 86_400
1565 {
1566 warn!(
1567 "HSTS max_age = {}s on {} is below 1 day — this is almost certainly a \
1568 misconfiguration. RFC 6797 §11.4 reserves max_age = 0 as the explicit kill \
1569 switch.",
1570 value, scope
1571 );
1572 }
1573
1574 let include_subdomains = self.include_subdomains;
1575 let preload = self.preload;
1576
1577 if matches!(preload, Some(true)) {
1578 let max_age_value = max_age.unwrap_or(0);
1579 if max_age_value < DEFAULT_HSTS_MAX_AGE {
1580 warn!(
1581 "HSTS preload = true on {} with max_age = {}s; the Chrome HSTS preload \
1582 list requires max_age >= {} (https://hstspreload.org/).",
1583 scope, max_age_value, DEFAULT_HSTS_MAX_AGE
1584 );
1585 }
1586 if include_subdomains != Some(true) {
1587 warn!(
1588 "HSTS preload = true on {} without include_subdomains = true; the Chrome \
1589 HSTS preload list requires includeSubDomains \
1590 (https://hstspreload.org/).",
1591 scope
1592 );
1593 }
1594 }
1595
1596 let config = HstsConfig {
1597 enabled: Some(enabled),
1598 max_age,
1599 include_subdomains,
1600 preload,
1601 force_replace_backend: self.force_replace_backend,
1602 };
1603
1604 debug_assert_eq!(
1609 config.enabled,
1610 Some(enabled),
1611 "built HSTS config must record the resolved enabled flag"
1612 );
1613 debug_assert!(
1614 !enabled || config.max_age.is_some(),
1615 "an enabled HSTS policy must carry a max_age"
1616 );
1617 Ok(config)
1618 }
1619}
1620
1621impl FileClusterFrontendConfig {
1622 pub fn to_tcp_front(&self) -> Result<TcpFrontendConfig, ConfigError> {
1623 if self.hostname.is_some() {
1624 return Err(ConfigError::InvalidFrontendConfig("hostname".to_string()));
1625 }
1626 if self.path.is_some() {
1627 return Err(ConfigError::InvalidFrontendConfig(
1628 "path_prefix".to_string(),
1629 ));
1630 }
1631 if self.certificate.is_some() {
1632 return Err(ConfigError::InvalidFrontendConfig(
1633 "certificate".to_string(),
1634 ));
1635 }
1636 if self.hostname.is_some() {
1637 return Err(ConfigError::InvalidFrontendConfig("hostname".to_string()));
1638 }
1639 if self.certificate_chain.is_some() {
1640 return Err(ConfigError::InvalidFrontendConfig(
1641 "certificate_chain".to_string(),
1642 ));
1643 }
1644
1645 let tcp_front = TcpFrontendConfig {
1646 address: self.address,
1647 tags: self.tags.clone(),
1648 udp: false,
1651 };
1652 debug_assert_eq!(
1657 tcp_front.address, self.address,
1658 "TCP frontend must bind the requested address"
1659 );
1660 debug_assert!(
1661 self.hostname.is_none()
1662 && self.path.is_none()
1663 && self.certificate.is_none()
1664 && self.certificate_chain.is_none(),
1665 "a built TCP frontend must carry no HTTP-only attributes"
1666 );
1667 Ok(tcp_front)
1668 }
1669
1670 pub fn to_http_front(&self, _cluster_id: &str) -> Result<HttpFrontendConfig, ConfigError> {
1671 let hostname = match &self.hostname {
1672 Some(hostname) => hostname.to_owned(),
1673 None => {
1674 return Err(ConfigError::Missing(MissingKind::Field(
1675 "hostname".to_string(),
1676 )));
1677 }
1678 };
1679
1680 let key_opt = match self.key.as_ref() {
1681 None => None,
1682 Some(path) => {
1683 let key = Config::load_file(path)?;
1684 Some(key)
1685 }
1686 };
1687
1688 let certificate_opt = match self.certificate.as_ref() {
1689 None => None,
1690 Some(path) => {
1691 let certificate = Config::load_file(path)?;
1692 Some(certificate)
1693 }
1694 };
1695
1696 let certificate_chain = match self.certificate_chain.as_ref() {
1697 None => None,
1698 Some(path) => {
1699 let certificate_chain = Config::load_file(path)?;
1700 Some(split_certificate_chain(certificate_chain))
1701 }
1702 };
1703
1704 let path = match (self.path.as_ref(), self.path_type.as_ref()) {
1705 (None, _) => PathRule::prefix("".to_string()),
1706 (Some(s), Some(PathRuleType::Prefix)) => PathRule::prefix(s.to_string()),
1707 (Some(s), Some(PathRuleType::Regex)) => PathRule::regex(s.to_string()),
1708 (Some(s), Some(PathRuleType::Equals)) => PathRule::equals(s.to_string()),
1709 (Some(s), None) => PathRule::prefix(s.clone()),
1710 };
1711
1712 let redirect = match self.redirect.as_deref() {
1713 Some(v) => Some(parse_redirect_policy(v)?),
1714 None => None,
1715 };
1716 let redirect_scheme = match self.redirect_scheme.as_deref() {
1717 Some(v) => Some(parse_redirect_scheme(v)?),
1718 None => None,
1719 };
1720
1721 let headers = match self.headers.as_ref() {
1722 Some(entries) => {
1723 let mut out = Vec::with_capacity(entries.len());
1724 for (index, entry) in entries.iter().enumerate() {
1725 out.push(parse_header_edit(index, entry)?);
1726 }
1727 out
1728 }
1729 None => Vec::new(),
1730 };
1731
1732 let frontend_serves_https = key_opt.is_some() && certificate_opt.is_some();
1739 let hsts = match self.hsts.as_ref() {
1740 Some(h) => {
1741 if !frontend_serves_https {
1742 return Err(ConfigError::HstsOnPlainHttp(format!(
1743 "frontend {_cluster_id}/{hostname}"
1744 )));
1745 }
1746 Some(h.to_proto(&format!("frontend {_cluster_id}/{hostname}"))?)
1747 }
1748 None => None,
1749 };
1750
1751 Ok(HttpFrontendConfig {
1752 address: self.address,
1753 hostname,
1754 certificate: certificate_opt,
1755 key: key_opt,
1756 certificate_chain,
1757 tls_versions: self.tls_versions.clone(),
1758 position: self.position,
1759 path,
1760 method: self.method.clone(),
1761 tags: self.tags.clone(),
1762 redirect,
1763 redirect_scheme,
1764 redirect_template: self.redirect_template.clone(),
1765 rewrite_host: self.rewrite_host.clone(),
1766 rewrite_path: self.rewrite_path.clone(),
1767 rewrite_port: self.rewrite_port,
1768 required_auth: self.required_auth,
1769 headers,
1770 hsts,
1771 })
1772 }
1773}
1774
1775pub(crate) fn parse_redirect_policy(value: &str) -> Result<RedirectPolicy, ConfigError> {
1777 match value.to_ascii_lowercase().as_str() {
1778 "forward" => Ok(RedirectPolicy::Forward),
1779 "permanent" => Ok(RedirectPolicy::Permanent),
1780 "unauthorized" => Ok(RedirectPolicy::Unauthorized),
1781 _ => Err(ConfigError::InvalidRedirectPolicy(value.to_owned())),
1782 }
1783}
1784
1785pub(crate) fn parse_redirect_scheme(value: &str) -> Result<RedirectScheme, ConfigError> {
1787 match value.to_ascii_lowercase().as_str() {
1788 "use-same" | "use_same" => Ok(RedirectScheme::UseSame),
1789 "use-http" | "use_http" => Ok(RedirectScheme::UseHttp),
1790 "use-https" | "use_https" => Ok(RedirectScheme::UseHttps),
1791 _ => Err(ConfigError::InvalidRedirectScheme(value.to_owned())),
1792 }
1793}
1794
1795pub(crate) fn parse_header_edit(
1802 index: usize,
1803 entry: &HeaderEditConfig,
1804) -> Result<Header, ConfigError> {
1805 let position = match entry.position.to_ascii_lowercase().as_str() {
1806 "request" => HeaderPosition::Request,
1807 "response" => HeaderPosition::Response,
1808 "both" => HeaderPosition::Both,
1809 _ => {
1810 return Err(ConfigError::InvalidHeaderPosition {
1811 index,
1812 position: entry.position.clone(),
1813 });
1814 }
1815 };
1816 if !header_name_is_valid_token(entry.key.as_bytes()) {
1817 return Err(ConfigError::InvalidHeaderBytes {
1818 index,
1819 field: "key",
1820 });
1821 }
1822 if header_value_contains_forbidden_controls(entry.value.as_bytes()) {
1823 return Err(ConfigError::InvalidHeaderBytes {
1824 index,
1825 field: "value",
1826 });
1827 }
1828 let header = Header {
1829 position: position as i32,
1830 key: entry.key.clone(),
1831 val: entry.value.clone(),
1832 };
1833 debug_assert!(
1839 header_name_is_valid_token(header.key.as_bytes()),
1840 "an emitted header key must be a valid token"
1841 );
1842 debug_assert!(
1843 !header_value_contains_forbidden_controls(header.val.as_bytes()),
1844 "an emitted header value must be free of forbidden control bytes"
1845 );
1846 Ok(header)
1847}
1848
1849pub(crate) fn header_name_is_valid_token(bytes: &[u8]) -> bool {
1857 if bytes.is_empty() {
1858 return false;
1859 }
1860 bytes.iter().all(|&b| is_tchar(b))
1861}
1862
1863fn is_tchar(b: u8) -> bool {
1866 b.is_ascii_alphanumeric()
1867 || matches!(
1868 b,
1869 b'!' | b'#'
1870 | b'$'
1871 | b'%'
1872 | b'&'
1873 | b'\''
1874 | b'*'
1875 | b'+'
1876 | b'-'
1877 | b'.'
1878 | b'^'
1879 | b'_'
1880 | b'`'
1881 | b'|'
1882 | b'~'
1883 )
1884}
1885
1886pub(crate) fn header_value_contains_forbidden_controls(bytes: &[u8]) -> bool {
1894 bytes
1895 .iter()
1896 .any(|&b| matches!(b, 0x00..=0x08 | 0x0A..=0x1F | 0x7F))
1897}
1898
1899#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1900#[serde(deny_unknown_fields, rename_all = "lowercase")]
1901pub enum ListenerProtocol {
1902 Http,
1903 Https,
1904 Tcp,
1905 Udp,
1906}
1907
1908#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1909#[serde(deny_unknown_fields, rename_all = "lowercase")]
1910pub enum FileClusterProtocolConfig {
1911 Http,
1912 Tcp,
1913}
1914
1915fn default_health_check_interval() -> u32 {
1916 10
1917}
1918fn default_health_check_timeout() -> u32 {
1919 5
1920}
1921fn default_health_check_threshold() -> u32 {
1922 3
1923}
1924
1925#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
1926#[serde(deny_unknown_fields)]
1927pub struct FileHealthCheckConfig {
1928 pub uri: String,
1929 #[serde(default = "default_health_check_interval")]
1930 pub interval: u32,
1931 #[serde(default = "default_health_check_timeout")]
1932 pub timeout: u32,
1933 #[serde(default = "default_health_check_threshold")]
1934 pub healthy_threshold: u32,
1935 #[serde(default = "default_health_check_threshold")]
1936 pub unhealthy_threshold: u32,
1937 #[serde(default)]
1938 pub expected_status: u32,
1939}
1940
1941impl FileHealthCheckConfig {
1942 pub fn to_proto(&self) -> HealthCheckConfig {
1943 let proto = HealthCheckConfig {
1944 uri: self.uri.to_owned(),
1945 interval: self.interval,
1946 timeout: self.timeout,
1947 healthy_threshold: self.healthy_threshold,
1948 unhealthy_threshold: self.unhealthy_threshold,
1949 expected_status: self.expected_status,
1950 };
1951 debug_assert_eq!(proto.uri, self.uri, "proto URI must mirror the file config");
1955 debug_assert!(
1956 proto.interval == self.interval
1957 && proto.timeout == self.timeout
1958 && proto.healthy_threshold == self.healthy_threshold
1959 && proto.unhealthy_threshold == self.unhealthy_threshold,
1960 "proto timing knobs must mirror the file config"
1961 );
1962 proto
1963 }
1964}
1965
1966pub fn validate_health_check_config(cfg: &HealthCheckConfig) -> Result<(), &'static str> {
1977 if cfg.interval == 0 {
1978 return Err("health check interval must be > 0");
1979 }
1980 if cfg.timeout == 0 {
1981 return Err("health check timeout must be > 0");
1982 }
1983 if cfg.healthy_threshold == 0 {
1984 return Err("health check healthy_threshold must be > 0");
1985 }
1986 if cfg.unhealthy_threshold == 0 {
1987 return Err("health check unhealthy_threshold must be > 0");
1988 }
1989 if !cfg.uri.starts_with('/') {
1990 return Err("health check URI must start with '/'");
1991 }
1992 if cfg
1993 .uri
1994 .bytes()
1995 .any(|b| b == b'\r' || b == b'\n' || b == 0 || (b < 0x20 && b != b'\t'))
1996 {
1997 return Err("health check URI must not contain CR, LF, NUL, or other C0 control bytes");
1998 }
1999 debug_assert!(
2005 cfg.interval > 0
2006 && cfg.timeout > 0
2007 && cfg.healthy_threshold > 0
2008 && cfg.unhealthy_threshold > 0,
2009 "validated health-check thresholds must all be strictly positive"
2010 );
2011 debug_assert!(
2012 cfg.uri.starts_with('/'),
2013 "validated health-check URI must be an absolute path"
2014 );
2015 Ok(())
2016}
2017
2018#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2019#[serde(deny_unknown_fields)]
2020pub struct FileClusterConfig {
2021 pub frontends: Vec<FileClusterFrontendConfig>,
2022 pub backends: Vec<BackendConfig>,
2023 pub protocol: FileClusterProtocolConfig,
2024 pub sticky_session: Option<bool>,
2025 pub https_redirect: Option<bool>,
2026 #[serde(default)]
2027 pub send_proxy: Option<bool>,
2028 #[serde(default)]
2029 pub load_balancing: LoadBalancingAlgorithms,
2030 pub answer_503: Option<String>,
2031 #[serde(default)]
2032 pub load_metric: Option<LoadMetric>,
2033 pub http2: Option<bool>,
2036 pub answers: Option<BTreeMap<String, String>>,
2047 pub https_redirect_port: Option<u32>,
2052 pub authorized_hashes: Option<Vec<String>>,
2057 pub www_authenticate: Option<String>,
2061 pub max_connections_per_ip: Option<u64>,
2068 pub retry_after: Option<u32>,
2074 #[serde(default)]
2078 pub health_check: Option<FileHealthCheckConfig>,
2079 #[serde(default)]
2083 pub udp: Option<FileUdpClusterConfig>,
2084}
2085
2086#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2089#[serde(deny_unknown_fields)]
2090pub struct FileUdpHealthConfig {
2091 pub mode: Option<UdpHealthMode>,
2095 pub tcp_port: Option<u32>,
2096 pub rise: Option<u32>,
2097 pub fall: Option<u32>,
2098 pub fail_open: Option<bool>,
2099 pub udp_probe_payload: Option<String>,
2101 pub probe_interval_seconds: Option<u32>,
2102 pub probe_timeout_seconds: Option<u32>,
2103}
2104
2105impl FileUdpHealthConfig {
2106 pub fn to_proto(&self) -> UdpHealthConfig {
2107 UdpHealthConfig {
2108 mode: self.mode.map(|m| m as i32),
2109 tcp_port: self.tcp_port,
2110 rise: self.rise,
2111 fall: self.fall,
2112 fail_open: self.fail_open,
2113 udp_probe_payload: self
2114 .udp_probe_payload
2115 .as_ref()
2116 .map(|p| p.as_bytes().to_owned()),
2117 probe_interval_seconds: self.probe_interval_seconds,
2118 probe_timeout_seconds: self.probe_timeout_seconds,
2119 }
2120 }
2121}
2122
2123#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2125#[serde(deny_unknown_fields)]
2126pub struct FileUdpClusterConfig {
2127 pub affinity_key: Option<UdpAffinityKey>,
2130 pub responses: Option<u32>,
2132 pub requests: Option<u32>,
2134 pub send_proxy_protocol: Option<bool>,
2136 pub proxy_protocol_every_datagram: Option<bool>,
2138 pub health: Option<FileUdpHealthConfig>,
2140}
2141
2142impl FileUdpClusterConfig {
2143 pub fn to_proto(&self) -> UdpClusterConfig {
2144 UdpClusterConfig {
2145 affinity_key: self.affinity_key.map(|k| k as i32),
2146 responses: self.responses,
2147 requests: self.requests,
2148 send_proxy_protocol: self.send_proxy_protocol,
2149 proxy_protocol_every_datagram: self.proxy_protocol_every_datagram,
2150 health: self.health.as_ref().map(|h| h.to_proto()),
2151 }
2152 }
2153}
2154
2155#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2156#[serde(deny_unknown_fields)]
2157pub struct BackendConfig {
2158 pub address: SocketAddr,
2159 pub weight: Option<u8>,
2160 pub sticky_id: Option<String>,
2161 pub backup: Option<bool>,
2162 pub backend_id: Option<String>,
2163}
2164
2165impl FileClusterConfig {
2166 pub fn to_cluster_config(
2167 self,
2168 cluster_id: &str,
2169 expect_proxy: &HashSet<SocketAddr>,
2170 ) -> Result<ClusterConfig, ConfigError> {
2171 let requested_frontend_count = self.frontends.len();
2174 match self.protocol {
2175 FileClusterProtocolConfig::Tcp => {
2176 let mut has_expect_proxy = None;
2177 let mut frontends = Vec::new();
2178 for f in self.frontends {
2179 if expect_proxy.contains(&f.address) {
2180 match has_expect_proxy {
2181 Some(true) => {}
2182 Some(false) => {
2183 return Err(ConfigError::Incompatible {
2184 object: ObjectKind::Cluster,
2185 id: cluster_id.to_owned(),
2186 kind: IncompatibilityKind::ProxyProtocol,
2187 });
2188 }
2189 None => has_expect_proxy = Some(true),
2190 }
2191 } else {
2192 match has_expect_proxy {
2193 Some(false) => {}
2194 Some(true) => {
2195 return Err(ConfigError::Incompatible {
2196 object: ObjectKind::Cluster,
2197 id: cluster_id.to_owned(),
2198 kind: IncompatibilityKind::ProxyProtocol,
2199 });
2200 }
2201 None => has_expect_proxy = Some(false),
2202 }
2203 }
2204 let tcp_frontend = f.to_tcp_front()?;
2205 frontends.push(tcp_frontend);
2206 }
2207
2208 let send_proxy = self.send_proxy.unwrap_or(false);
2209 let expect_proxy = has_expect_proxy.unwrap_or(false);
2210 let proxy_protocol = match (send_proxy, expect_proxy) {
2211 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
2212 (true, false) => Some(ProxyProtocolConfig::SendHeader),
2213 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
2214 _ => None,
2215 };
2216
2217 let answers = match self.answers.as_ref() {
2218 Some(map) => load_answers(map)?,
2219 None => BTreeMap::new(),
2220 };
2221
2222 let udp = self.udp.as_ref().map(|u| u.to_proto());
2223 debug_assert_eq!(
2229 frontends.len(),
2230 requested_frontend_count,
2231 "every TCP frontend must survive conversion"
2232 );
2233 debug_assert_eq!(
2234 proxy_protocol,
2235 match (send_proxy, expect_proxy) {
2236 (true, true) => Some(ProxyProtocolConfig::RelayHeader),
2237 (true, false) => Some(ProxyProtocolConfig::SendHeader),
2238 (false, true) => Some(ProxyProtocolConfig::ExpectHeader),
2239 (false, false) => None,
2240 },
2241 "proxy_protocol must be the (send, expect) function"
2242 );
2243
2244 Ok(ClusterConfig::Tcp(TcpClusterConfig {
2245 cluster_id: cluster_id.to_string(),
2246 frontends,
2247 backends: self.backends,
2248 proxy_protocol,
2249 load_balancing: self.load_balancing,
2250 load_metric: self.load_metric,
2251 answers,
2252 https_redirect_port: self.https_redirect_port,
2253 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
2254 www_authenticate: self.www_authenticate,
2255 max_connections_per_ip: self.max_connections_per_ip,
2256 retry_after: self.retry_after,
2257 health_check: self.health_check.as_ref().map(|hc| hc.to_proto()),
2258 udp,
2259 }))
2260 }
2261 FileClusterProtocolConfig::Http => {
2262 let mut frontends = Vec::new();
2263 for frontend in self.frontends {
2264 let http_frontend = frontend.to_http_front(cluster_id)?;
2265 frontends.push(http_frontend);
2266 }
2267
2268 let answer_503 = self.answer_503.as_ref().and_then(|path| {
2269 Config::load_file(path)
2270 .map_err(|e| {
2271 error!("cannot load 503 error page at path '{}': {:?}", path, e);
2272 e
2273 })
2274 .ok()
2275 });
2276
2277 let answers = match self.answers.as_ref() {
2278 Some(map) => load_answers(map)?,
2279 None => BTreeMap::new(),
2280 };
2281
2282 let udp = self.udp.as_ref().map(|u| u.to_proto());
2283 debug_assert_eq!(
2286 frontends.len(),
2287 requested_frontend_count,
2288 "every HTTP frontend must survive conversion"
2289 );
2290
2291 Ok(ClusterConfig::Http(HttpClusterConfig {
2292 cluster_id: cluster_id.to_string(),
2293 frontends,
2294 backends: self.backends,
2295 sticky_session: self.sticky_session.unwrap_or(false),
2296 https_redirect: self.https_redirect.unwrap_or(false),
2297 load_balancing: self.load_balancing,
2298 load_metric: self.load_metric,
2299 answer_503,
2300 http2: self.http2,
2301 answers,
2302 https_redirect_port: self.https_redirect_port,
2303 authorized_hashes: self.authorized_hashes.unwrap_or_default(),
2304 www_authenticate: self.www_authenticate,
2305 max_connections_per_ip: self.max_connections_per_ip,
2306 retry_after: self.retry_after,
2307 health_check: self.health_check.as_ref().map(|hc| hc.to_proto()),
2308 udp,
2309 }))
2310 }
2311 }
2312 }
2313}
2314
2315#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2316#[serde(deny_unknown_fields)]
2317pub struct HttpFrontendConfig {
2318 pub address: SocketAddr,
2319 pub hostname: String,
2320 pub path: PathRule,
2321 pub method: Option<String>,
2322 pub certificate: Option<String>,
2323 pub key: Option<String>,
2324 pub certificate_chain: Option<Vec<String>>,
2325 #[serde(default)]
2326 pub tls_versions: Vec<TlsVersion>,
2327 #[serde(default)]
2328 pub position: RulePosition,
2329 pub tags: Option<BTreeMap<String, String>>,
2330 #[serde(default)]
2332 pub redirect: Option<RedirectPolicy>,
2333 #[serde(default)]
2335 pub redirect_scheme: Option<RedirectScheme>,
2336 #[serde(default)]
2337 pub redirect_template: Option<String>,
2338 #[serde(default)]
2339 pub rewrite_host: Option<String>,
2340 #[serde(default)]
2341 pub rewrite_path: Option<String>,
2342 #[serde(default)]
2343 pub rewrite_port: Option<u32>,
2344 #[serde(default)]
2345 pub required_auth: Option<bool>,
2346 #[serde(default)]
2349 pub headers: Vec<Header>,
2350 #[serde(default)]
2353 pub hsts: Option<HstsConfig>,
2354}
2355
2356impl HttpFrontendConfig {
2357 pub fn generate_requests(&self, cluster_id: &str) -> Vec<Request> {
2358 let mut v = Vec::new();
2359
2360 let tags = self.tags.clone().unwrap_or_default();
2361
2362 if self.key.is_some() && self.certificate.is_some() {
2363 v.push(
2364 RequestType::AddCertificate(AddCertificate {
2365 address: self.address.into(),
2366 certificate: CertificateAndKey {
2367 key: self.key.clone().unwrap(),
2368 certificate: self.certificate.clone().unwrap(),
2369 certificate_chain: self.certificate_chain.clone().unwrap_or_default(),
2370 versions: self.tls_versions.iter().map(|v| *v as i32).collect(),
2371 names: vec![],
2376 },
2377 expired_at: None,
2378 })
2379 .into(),
2380 );
2381
2382 v.push(
2383 RequestType::AddHttpsFrontend(RequestHttpFrontend {
2384 cluster_id: Some(cluster_id.to_string()),
2385 address: self.address.into(),
2386 hostname: self.hostname.clone(),
2387 path: self.path.clone(),
2388 method: self.method.clone(),
2389 position: self.position.into(),
2390 tags,
2391 redirect: self.redirect.map(|r| r as i32),
2392 required_auth: self.required_auth,
2393 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
2394 redirect_template: self.redirect_template.clone(),
2395 rewrite_host: self.rewrite_host.clone(),
2396 rewrite_path: self.rewrite_path.clone(),
2397 rewrite_port: self.rewrite_port,
2398 headers: self.headers.clone(),
2399 hsts: self.hsts,
2400 })
2401 .into(),
2402 );
2403 } else {
2404 v.push(
2406 RequestType::AddHttpFrontend(RequestHttpFrontend {
2407 cluster_id: Some(cluster_id.to_string()),
2408 address: self.address.into(),
2409 hostname: self.hostname.clone(),
2410 path: self.path.clone(),
2411 method: self.method.clone(),
2412 position: self.position.into(),
2413 tags,
2414 redirect: self.redirect.map(|r| r as i32),
2415 required_auth: self.required_auth,
2416 redirect_scheme: self.redirect_scheme.map(|s| s as i32),
2417 redirect_template: self.redirect_template.clone(),
2418 rewrite_host: self.rewrite_host.clone(),
2419 rewrite_path: self.rewrite_path.clone(),
2420 rewrite_port: self.rewrite_port,
2421 headers: self.headers.clone(),
2422 hsts: self.hsts,
2423 })
2424 .into(),
2425 );
2426 }
2427
2428 v
2429 }
2430}
2431
2432#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2433#[serde(deny_unknown_fields)]
2434pub struct HttpClusterConfig {
2435 pub cluster_id: String,
2436 pub frontends: Vec<HttpFrontendConfig>,
2437 pub backends: Vec<BackendConfig>,
2438 pub sticky_session: bool,
2439 pub https_redirect: bool,
2440 pub load_balancing: LoadBalancingAlgorithms,
2441 pub load_metric: Option<LoadMetric>,
2442 pub answer_503: Option<String>,
2443 pub http2: Option<bool>,
2444 #[serde(default)]
2447 pub answers: BTreeMap<String, String>,
2448 #[serde(default)]
2449 pub https_redirect_port: Option<u32>,
2450 #[serde(default)]
2451 pub authorized_hashes: Vec<String>,
2452 #[serde(default)]
2453 pub www_authenticate: Option<String>,
2454 #[serde(default)]
2457 pub max_connections_per_ip: Option<u64>,
2458 #[serde(default)]
2461 pub retry_after: Option<u32>,
2462 #[serde(default)]
2466 pub health_check: Option<HealthCheckConfig>,
2467 #[serde(default)]
2470 pub udp: Option<UdpClusterConfig>,
2471}
2472
2473impl HttpClusterConfig {
2474 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2475 let mut v: Vec<Request> = vec![
2476 RequestType::AddCluster(Cluster {
2477 cluster_id: self.cluster_id.clone(),
2478 sticky_session: self.sticky_session,
2479 https_redirect: self.https_redirect,
2480 proxy_protocol: None,
2481 load_balancing: self.load_balancing as i32,
2482 answer_503: self.answer_503.clone(),
2483 load_metric: self.load_metric.map(|s| s as i32),
2484 http2: self.http2,
2485 answers: self.answers.clone(),
2486 https_redirect_port: self.https_redirect_port,
2487 authorized_hashes: self.authorized_hashes.clone(),
2488 www_authenticate: self.www_authenticate.clone(),
2489 max_connections_per_ip: self.max_connections_per_ip,
2490 retry_after: self.retry_after,
2491 health_check: self.health_check.clone(),
2492 udp: self.udp.clone(),
2493 })
2494 .into(),
2495 ];
2496
2497 for frontend in &self.frontends {
2498 let mut orders = frontend.generate_requests(&self.cluster_id);
2499 v.append(&mut orders);
2500 }
2501
2502 for (backend_count, backend) in self.backends.iter().enumerate() {
2503 let load_balancing_parameters = Some(LoadBalancingParams {
2504 weight: backend.weight.unwrap_or(100) as i32,
2505 });
2506
2507 v.push(
2508 RequestType::AddBackend(AddBackend {
2509 cluster_id: self.cluster_id.clone(),
2510 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
2511 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
2512 }),
2513 address: backend.address.into(),
2514 load_balancing_parameters,
2515 sticky_id: backend.sticky_id.clone(),
2516 backup: backend.backup,
2517 })
2518 .into(),
2519 );
2520 }
2521
2522 debug_assert!(
2527 matches!(
2528 v.first().and_then(|r| r.request_type.as_ref()),
2529 Some(RequestType::AddCluster(_))
2530 ),
2531 "HTTP cluster orders must lead with an AddCluster"
2532 );
2533 debug_assert_eq!(
2534 v.iter()
2535 .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
2536 .count(),
2537 self.backends.len(),
2538 "one AddBackend order per configured backend"
2539 );
2540 Ok(v)
2541 }
2542}
2543
2544#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2545pub struct TcpFrontendConfig {
2546 pub address: SocketAddr,
2547 pub tags: Option<BTreeMap<String, String>>,
2548 #[serde(default)]
2555 pub udp: bool,
2556}
2557
2558#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2559pub struct TcpClusterConfig {
2560 pub cluster_id: String,
2561 pub frontends: Vec<TcpFrontendConfig>,
2562 pub backends: Vec<BackendConfig>,
2563 #[serde(default)]
2564 pub proxy_protocol: Option<ProxyProtocolConfig>,
2565 pub load_balancing: LoadBalancingAlgorithms,
2566 pub load_metric: Option<LoadMetric>,
2567 #[serde(default)]
2571 pub answers: BTreeMap<String, String>,
2572 #[serde(default)]
2573 pub https_redirect_port: Option<u32>,
2574 #[serde(default)]
2575 pub authorized_hashes: Vec<String>,
2576 #[serde(default)]
2577 pub www_authenticate: Option<String>,
2578 #[serde(default)]
2581 pub max_connections_per_ip: Option<u64>,
2582 #[serde(default)]
2586 pub retry_after: Option<u32>,
2587 #[serde(default)]
2591 pub health_check: Option<HealthCheckConfig>,
2592 #[serde(default)]
2595 pub udp: Option<UdpClusterConfig>,
2596}
2597
2598impl TcpClusterConfig {
2599 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2600 let mut v: Vec<Request> = vec![
2601 RequestType::AddCluster(Cluster {
2602 cluster_id: self.cluster_id.clone(),
2603 sticky_session: false,
2604 https_redirect: false,
2605 proxy_protocol: self.proxy_protocol.map(|s| s as i32),
2606 load_balancing: self.load_balancing as i32,
2607 load_metric: self.load_metric.map(|s| s as i32),
2608 answer_503: None,
2609 http2: None,
2610 answers: self.answers.clone(),
2611 https_redirect_port: self.https_redirect_port,
2612 authorized_hashes: self.authorized_hashes.clone(),
2613 www_authenticate: self.www_authenticate.clone(),
2614 max_connections_per_ip: self.max_connections_per_ip,
2615 retry_after: self.retry_after,
2616 health_check: self.health_check.clone(),
2617 udp: self.udp.clone(),
2618 })
2619 .into(),
2620 ];
2621
2622 for frontend in &self.frontends {
2623 if frontend.udp {
2628 v.push(
2629 RequestType::AddUdpFrontend(RequestUdpFrontend {
2630 cluster_id: self.cluster_id.clone(),
2631 address: frontend.address.into(),
2632 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
2633 })
2634 .into(),
2635 );
2636 } else {
2637 v.push(
2638 RequestType::AddTcpFrontend(RequestTcpFrontend {
2639 cluster_id: self.cluster_id.clone(),
2640 address: frontend.address.into(),
2641 tags: frontend.tags.clone().unwrap_or(BTreeMap::new()),
2642 })
2643 .into(),
2644 );
2645 }
2646 }
2647
2648 for (backend_count, backend) in self.backends.iter().enumerate() {
2649 let load_balancing_parameters = Some(LoadBalancingParams {
2650 weight: backend.weight.unwrap_or(100) as i32,
2651 });
2652
2653 v.push(
2654 RequestType::AddBackend(AddBackend {
2655 cluster_id: self.cluster_id.clone(),
2656 backend_id: backend.backend_id.clone().unwrap_or_else(|| {
2657 format!("{}-{}-{}", self.cluster_id, backend_count, backend.address)
2658 }),
2659 address: backend.address.into(),
2660 load_balancing_parameters,
2661 sticky_id: backend.sticky_id.clone(),
2662 backup: backend.backup,
2663 })
2664 .into(),
2665 );
2666 }
2667
2668 debug_assert!(
2672 matches!(
2673 v.first().and_then(|r| r.request_type.as_ref()),
2674 Some(RequestType::AddCluster(_))
2675 ),
2676 "TCP cluster orders must lead with an AddCluster"
2677 );
2678 debug_assert_eq!(
2679 v.iter()
2680 .filter(|r| matches!(
2681 r.request_type,
2682 Some(RequestType::AddTcpFrontend(_)) | Some(RequestType::AddUdpFrontend(_))
2683 ))
2684 .count(),
2685 self.frontends.len(),
2686 "one AddTcpFrontend or AddUdpFrontend order per configured frontend"
2687 );
2688 debug_assert_eq!(
2689 v.iter()
2690 .filter(|r| matches!(r.request_type, Some(RequestType::AddBackend(_))))
2691 .count(),
2692 self.backends.len(),
2693 "one AddBackend order per configured backend"
2694 );
2695 Ok(v)
2696 }
2697}
2698
2699#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
2700pub enum ClusterConfig {
2701 Http(HttpClusterConfig),
2702 Tcp(TcpClusterConfig),
2703}
2704
2705impl ClusterConfig {
2706 pub fn generate_requests(&self) -> Result<Vec<Request>, ConfigError> {
2707 match *self {
2708 ClusterConfig::Http(ref http) => http.generate_requests(),
2709 ClusterConfig::Tcp(ref tcp) => tcp.generate_requests(),
2710 }
2711 }
2712}
2713
2714#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
2716pub struct FileConfig {
2717 pub command_socket: Option<String>,
2718 pub command_buffer_size: Option<u64>,
2719 pub max_command_buffer_size: Option<u64>,
2720 pub max_connections: Option<usize>,
2721 pub min_buffers: Option<u64>,
2722 pub max_buffers: Option<u64>,
2723 pub buffer_size: Option<u64>,
2724 #[serde(default)]
2728 pub slab_entries_per_connection: Option<u64>,
2729 #[serde(default)]
2739 pub basic_auth_max_credential_bytes: Option<u64>,
2740 #[serde(default)]
2748 pub max_connections_per_ip: Option<u64>,
2749 #[serde(default)]
2755 pub retry_after: Option<u32>,
2756 #[serde(default)]
2765 pub splice_pipe_capacity_bytes: Option<u64>,
2766 #[serde(default)]
2773 pub command_allowed_uids: Option<Vec<u32>>,
2774 pub saved_state: Option<String>,
2775 #[serde(default)]
2776 pub automatic_state_save: Option<bool>,
2777 pub log_level: Option<String>,
2778 pub log_target: Option<String>,
2779 #[serde(default)]
2780 pub log_colored: bool,
2781 #[serde(default)]
2789 pub audit_logs_target: Option<String>,
2790 #[serde(default)]
2795 pub audit_logs_json_target: Option<String>,
2796 #[serde(default)]
2797 pub access_logs_target: Option<String>,
2798 #[serde(default)]
2799 pub access_logs_format: Option<AccessLogFormat>,
2800 #[serde(default)]
2801 pub access_logs_colored: Option<bool>,
2802 pub worker_count: Option<u16>,
2803 pub worker_automatic_restart: Option<bool>,
2804 pub metrics: Option<MetricsConfig>,
2805 pub disable_cluster_metrics: Option<bool>,
2806 pub listeners: Option<Vec<ListenerBuilder>>,
2807 pub clusters: Option<HashMap<String, FileClusterConfig>>,
2808 pub handle_process_affinity: Option<bool>,
2809 pub ctl_command_timeout: Option<u64>,
2810 pub pid_file_path: Option<String>,
2811 pub activate_listeners: Option<bool>,
2812 #[serde(default)]
2813 pub front_timeout: Option<u32>,
2814 #[serde(default)]
2815 pub back_timeout: Option<u32>,
2816 #[serde(default)]
2817 pub connect_timeout: Option<u32>,
2818 #[serde(default)]
2819 pub zombie_check_interval: Option<u32>,
2820 #[serde(default)]
2821 pub accept_queue_timeout: Option<u32>,
2822 #[serde(default)]
2823 pub evict_on_queue_full: Option<bool>,
2824 #[serde(default)]
2825 pub request_timeout: Option<u32>,
2826 #[serde(default)]
2827 pub worker_timeout: Option<u32>,
2828}
2829
2830impl FileConfig {
2831 pub fn load_from_path(path: &str) -> Result<FileConfig, ConfigError> {
2832 let data = Config::load_file(path)?;
2833
2834 let config: FileConfig = match toml::from_str(&data) {
2835 Ok(config) => config,
2836 Err(e) => {
2837 display_toml_error(&data, &e);
2838 return Err(ConfigError::DeserializeToml(e.to_string()));
2839 }
2840 };
2841
2842 let mut reserved_address: HashSet<SocketAddr> = HashSet::new();
2843
2844 if let Some(listeners) = config.listeners.as_ref() {
2845 for listener in listeners.iter() {
2846 if reserved_address.contains(&listener.address) {
2847 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
2848 }
2849 reserved_address.insert(listener.address);
2850 }
2851 }
2852
2853 Ok(config)
2875 }
2876}
2877
2878pub struct ConfigBuilder {
2880 file: FileConfig,
2881 known_addresses: HashMap<SocketAddr, ListenerProtocol>,
2882 expect_proxy_addresses: HashSet<SocketAddr>,
2883 built: Config,
2884}
2885
2886impl ConfigBuilder {
2887 pub fn new<S>(file_config: FileConfig, config_path: S) -> Self
2891 where
2892 S: ToString,
2893 {
2894 let built = Config {
2895 accept_queue_timeout: file_config
2896 .accept_queue_timeout
2897 .unwrap_or(DEFAULT_ACCEPT_QUEUE_TIMEOUT),
2898 evict_on_queue_full: file_config
2899 .evict_on_queue_full
2900 .unwrap_or(DEFAULT_EVICT_ON_QUEUE_FULL),
2901 activate_listeners: file_config.activate_listeners.unwrap_or(true),
2902 automatic_state_save: file_config
2903 .automatic_state_save
2904 .unwrap_or(DEFAULT_AUTOMATIC_STATE_SAVE),
2905 back_timeout: file_config.back_timeout.unwrap_or(DEFAULT_BACK_TIMEOUT),
2906 buffer_size: file_config.buffer_size.unwrap_or(DEFAULT_BUFFER_SIZE),
2907 command_buffer_size: file_config
2908 .command_buffer_size
2909 .unwrap_or(DEFAULT_COMMAND_BUFFER_SIZE),
2910 config_path: config_path.to_string(),
2911 connect_timeout: file_config
2912 .connect_timeout
2913 .unwrap_or(DEFAULT_CONNECT_TIMEOUT),
2914 ctl_command_timeout: file_config.ctl_command_timeout.unwrap_or(1_000),
2915 front_timeout: file_config.front_timeout.unwrap_or(DEFAULT_FRONT_TIMEOUT),
2916 handle_process_affinity: file_config.handle_process_affinity.unwrap_or(false),
2917 access_logs_target: file_config.access_logs_target.clone(),
2918 audit_logs_target: file_config.audit_logs_target.clone(),
2919 audit_logs_json_target: file_config.audit_logs_json_target.clone(),
2920 access_logs_format: file_config.access_logs_format.clone(),
2921 access_logs_colored: file_config.access_logs_colored,
2922 log_level: file_config
2923 .log_level
2924 .clone()
2925 .unwrap_or_else(|| String::from("info")),
2926 log_target: file_config
2927 .log_target
2928 .clone()
2929 .unwrap_or_else(|| String::from("stdout")),
2930 log_colored: file_config.log_colored,
2931 max_buffers: file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
2932 max_command_buffer_size: file_config
2933 .max_command_buffer_size
2934 .unwrap_or(DEFAULT_MAX_COMMAND_BUFFER_SIZE),
2935 max_connections: file_config
2936 .max_connections
2937 .unwrap_or(DEFAULT_MAX_CONNECTIONS),
2938 metrics: file_config.metrics.clone(),
2939 disable_cluster_metrics: file_config
2940 .disable_cluster_metrics
2941 .unwrap_or(DEFAULT_DISABLE_CLUSTER_METRICS),
2942 min_buffers: std::cmp::min(
2943 file_config.min_buffers.unwrap_or(DEFAULT_MIN_BUFFERS),
2944 file_config.max_buffers.unwrap_or(DEFAULT_MAX_BUFFERS),
2945 ),
2946 pid_file_path: file_config.pid_file_path.clone(),
2947 request_timeout: file_config
2948 .request_timeout
2949 .unwrap_or(DEFAULT_REQUEST_TIMEOUT),
2950 saved_state: file_config.saved_state.clone(),
2951 worker_automatic_restart: file_config
2952 .worker_automatic_restart
2953 .unwrap_or(DEFAULT_WORKER_AUTOMATIC_RESTART),
2954 worker_count: file_config.worker_count.unwrap_or(DEFAULT_WORKER_COUNT),
2955 zombie_check_interval: file_config
2956 .zombie_check_interval
2957 .unwrap_or(DEFAULT_ZOMBIE_CHECK_INTERVAL),
2958 worker_timeout: file_config.worker_timeout.unwrap_or(DEFAULT_WORKER_TIMEOUT),
2959 slab_entries_per_connection: file_config.slab_entries_per_connection.map(|n| {
2960 n.clamp(
2961 ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION,
2962 ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION,
2963 )
2964 }),
2965 command_allowed_uids: file_config.command_allowed_uids.clone(),
2966 basic_auth_max_credential_bytes: file_config.basic_auth_max_credential_bytes,
2967 max_connections_per_ip: file_config
2968 .max_connections_per_ip
2969 .unwrap_or(DEFAULT_MAX_CONNECTIONS_PER_IP),
2970 retry_after: file_config.retry_after.unwrap_or(DEFAULT_RETRY_AFTER),
2971 splice_pipe_capacity_bytes: file_config.splice_pipe_capacity_bytes,
2972 ..Default::default()
2973 };
2974
2975 debug_assert!(
2979 built.min_buffers <= built.max_buffers,
2980 "min_buffers must be clamped to <= max_buffers in the builder"
2981 );
2982 debug_assert!(
2985 built.slab_entries_per_connection.is_none_or(|n| {
2986 (ServerConfig::MIN_SLAB_ENTRIES_PER_CONNECTION
2987 ..=ServerConfig::MAX_SLAB_ENTRIES_PER_CONNECTION)
2988 .contains(&n)
2989 }),
2990 "a set slab_entries_per_connection must be clamped into [MIN, MAX]"
2991 );
2992
2993 Self {
2994 file: file_config,
2995 known_addresses: HashMap::new(),
2996 expect_proxy_addresses: HashSet::new(),
2997 built,
2998 }
2999 }
3000
3001 fn push_tls_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3002 let listener = listener.to_tls(Some(&self.built))?;
3003 self.built.https_listeners.push(listener);
3004 Ok(())
3005 }
3006
3007 fn push_http_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3008 let listener = listener.to_http(Some(&self.built))?;
3009 self.built.http_listeners.push(listener);
3010 Ok(())
3011 }
3012
3013 fn push_tcp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3014 let listener = listener.to_tcp(Some(&self.built))?;
3015 self.built.tcp_listeners.push(listener);
3016 Ok(())
3017 }
3018
3019 fn push_udp_listener(&mut self, mut listener: ListenerBuilder) -> Result<(), ConfigError> {
3020 let listener = listener.to_udp(Some(&self.built))?;
3021 self.built.udp_listeners.push(listener);
3022 Ok(())
3023 }
3024
3025 fn populate_listeners(&mut self, listeners: Vec<ListenerBuilder>) -> Result<(), ConfigError> {
3026 for listener in listeners.iter() {
3027 if self.known_addresses.contains_key(&listener.address) {
3028 return Err(ConfigError::ListenerAddressAlreadyInUse(listener.address));
3029 }
3030
3031 let protocol = listener
3032 .protocol
3033 .ok_or(ConfigError::Missing(MissingKind::Protocol))?;
3034
3035 self.known_addresses.insert(listener.address, protocol);
3036 if listener.expect_proxy == Some(true) {
3037 self.expect_proxy_addresses.insert(listener.address);
3038 }
3039
3040 if listener.public_address.is_some() && listener.expect_proxy == Some(true) {
3041 return Err(ConfigError::Incompatible {
3042 object: ObjectKind::Listener,
3043 id: listener.address.to_string(),
3044 kind: IncompatibilityKind::PublicAddress,
3045 });
3046 }
3047
3048 match protocol {
3049 ListenerProtocol::Https => self.push_tls_listener(listener.clone())?,
3050 ListenerProtocol::Http => self.push_http_listener(listener.clone())?,
3051 ListenerProtocol::Tcp => self.push_tcp_listener(listener.clone())?,
3052 ListenerProtocol::Udp => self.push_udp_listener(listener.clone())?,
3053 }
3054 }
3055 Ok(())
3056 }
3057
3058 fn populate_clusters(
3059 &mut self,
3060 mut file_cluster_configs: HashMap<String, FileClusterConfig>,
3061 ) -> Result<(), ConfigError> {
3062 for (id, file_cluster_config) in file_cluster_configs.drain() {
3063 let mut cluster_config =
3064 file_cluster_config.to_cluster_config(id.as_str(), &self.expect_proxy_addresses)?;
3065
3066 match cluster_config {
3067 ClusterConfig::Http(ref mut http) => {
3068 for frontend in http.frontends.iter_mut() {
3069 match self.known_addresses.get(&frontend.address) {
3070 Some(ListenerProtocol::Tcp) => {
3071 return Err(ConfigError::WrongFrontendProtocol(
3072 ListenerProtocol::Tcp,
3073 ));
3074 }
3075 Some(ListenerProtocol::Udp) => {
3076 return Err(ConfigError::WrongFrontendProtocol(
3077 ListenerProtocol::Udp,
3078 ));
3079 }
3080 Some(ListenerProtocol::Http) => {
3081 if frontend.certificate.is_some() {
3082 return Err(ConfigError::WrongFrontendProtocol(
3083 ListenerProtocol::Http,
3084 ));
3085 }
3086 }
3087 Some(ListenerProtocol::Https) => {
3088 if frontend.certificate.is_none() {
3089 if let Some(https_listener) =
3090 self.built.https_listeners.iter().find(|listener| {
3091 listener.address == frontend.address.into()
3092 && listener.certificate.is_some()
3093 })
3094 {
3095 frontend
3097 .certificate
3098 .clone_from(&https_listener.certificate);
3099 frontend.certificate_chain =
3100 Some(https_listener.certificate_chain.clone());
3101 frontend.key.clone_from(&https_listener.key);
3102 }
3103 if frontend.certificate.is_none() {
3104 debug!("known addresses: {:?}", self.known_addresses);
3105 debug!("frontend: {:?}", frontend);
3106 return Err(ConfigError::WrongFrontendProtocol(
3107 ListenerProtocol::Https,
3108 ));
3109 }
3110 }
3111 }
3112 None => {
3113 let file_listener_protocol = if frontend.certificate.is_some() {
3115 self.push_tls_listener(ListenerBuilder::new(
3116 frontend.address.into(),
3117 ListenerProtocol::Https,
3118 ))?;
3119
3120 ListenerProtocol::Https
3121 } else {
3122 self.push_http_listener(ListenerBuilder::new(
3123 frontend.address.into(),
3124 ListenerProtocol::Http,
3125 ))?;
3126
3127 ListenerProtocol::Http
3128 };
3129 self.known_addresses
3130 .insert(frontend.address, file_listener_protocol);
3131 }
3132 }
3133 }
3134 }
3135 ClusterConfig::Tcp(ref mut tcp) => {
3136 for frontend in tcp.frontends.iter_mut() {
3138 match self.known_addresses.get(&frontend.address) {
3139 Some(ListenerProtocol::Http) | Some(ListenerProtocol::Https) => {
3140 return Err(ConfigError::WrongFrontendProtocol(
3141 ListenerProtocol::Http,
3142 ));
3143 }
3144 Some(ListenerProtocol::Udp) => {
3145 frontend.udp = true;
3151 }
3152 Some(ListenerProtocol::Tcp) => {}
3153 None => {
3154 self.push_tcp_listener(ListenerBuilder::new(
3156 frontend.address.into(),
3157 ListenerProtocol::Tcp,
3158 ))?;
3159 self.known_addresses
3160 .insert(frontend.address, ListenerProtocol::Tcp);
3161 }
3162 }
3163 }
3164 }
3165 }
3166
3167 self.built.clusters.insert(id, cluster_config);
3168 }
3169 Ok(())
3170 }
3171
3172 pub fn into_config(&mut self) -> Result<Config, ConfigError> {
3174 if let Some(listeners) = &self.file.listeners {
3175 self.populate_listeners(listeners.clone())?;
3176 }
3177
3178 if let Some(file_cluster_configs) = &self.file.clusters {
3179 self.populate_clusters(file_cluster_configs.clone())?;
3180 }
3181
3182 let h2_listeners = self
3191 .built
3192 .https_listeners
3193 .iter()
3194 .filter(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
3195 .count();
3196 if h2_listeners > 0 && self.built.buffer_size < H2_MIN_BUFFER_SIZE {
3197 return Err(ConfigError::BufferSizeTooSmallForH2 {
3198 buffer_size: self.built.buffer_size,
3199 minimum: H2_MIN_BUFFER_SIZE,
3200 listeners: h2_listeners,
3201 });
3202 }
3203
3204 if let Some(cap) = self.built.basic_auth_max_credential_bytes {
3214 let third = self.built.buffer_size / 3;
3215 if cap >= third {
3216 warn!(
3217 "basic_auth_max_credential_bytes = {} is >= buffer_size / 3 ({}); \
3218 a hostile peer can pin ~33% of the per-frontend buffer per failed auth \
3219 attempt. Consider lowering basic_auth_max_credential_bytes (typical \
3220 credentials are <100 bytes) or raising buffer_size.",
3221 cap, third
3222 );
3223 }
3224 }
3225
3226 if self.built.evict_on_queue_full && self.built.max_connections < 100 {
3233 let pct = 100usize.div_ceil(self.built.max_connections);
3234 warn!(
3235 "evict_on_queue_full enabled with max_connections = {}; the eviction batch \
3236 clamps to 1, equivalent to ~{}% of capacity per cap event (the knob is \
3237 documented as 1%). Confirm this is intended.",
3238 self.built.max_connections, pct
3239 );
3240 }
3241
3242 let command_socket_path = self.file.command_socket.clone().unwrap_or({
3243 let mut path = env::current_dir().map_err(|e| ConfigError::Env(e.to_string()))?;
3244 path.push("sozu.sock");
3245 let verified_path = path
3246 .to_str()
3247 .ok_or(ConfigError::InvalidPath(path.clone()))?;
3248 verified_path.to_owned()
3249 });
3250
3251 if let (None, Some(true)) = (&self.file.saved_state, &self.file.automatic_state_save) {
3252 return Err(ConfigError::Missing(MissingKind::SavedState));
3253 }
3254
3255 let config = Config {
3256 command_socket: command_socket_path,
3257 ..self.built.clone()
3258 };
3259
3260 debug_assert!(
3266 config.min_buffers <= config.max_buffers,
3267 "min_buffers must not exceed max_buffers"
3268 );
3269 debug_assert!(
3275 !config
3276 .https_listeners
3277 .iter()
3278 .any(|l| l.alpn_protocols.iter().any(|p| p == "h2"))
3279 || config.buffer_size >= H2_MIN_BUFFER_SIZE,
3280 "an h2-advertising config must satisfy the H2 minimum buffer size"
3281 );
3282 Ok(config)
3283 }
3284}
3285
3286#[derive(Clone, PartialEq, Eq, Serialize, Default, Deserialize)]
3290pub struct Config {
3291 pub config_path: String,
3292 pub command_socket: String,
3293 pub command_buffer_size: u64,
3294 pub max_command_buffer_size: u64,
3295 pub max_connections: usize,
3296 pub min_buffers: u64,
3297 pub max_buffers: u64,
3298 pub buffer_size: u64,
3299 pub saved_state: Option<String>,
3300 #[serde(default)]
3301 pub automatic_state_save: bool,
3302 pub log_level: String,
3303 pub log_target: String,
3304 pub log_colored: bool,
3305 #[serde(default)]
3308 pub audit_logs_target: Option<String>,
3309 #[serde(default)]
3312 pub audit_logs_json_target: Option<String>,
3313 #[serde(default)]
3314 pub access_logs_target: Option<String>,
3315 pub access_logs_format: Option<AccessLogFormat>,
3316 pub access_logs_colored: Option<bool>,
3317 pub worker_count: u16,
3318 pub worker_automatic_restart: bool,
3319 pub metrics: Option<MetricsConfig>,
3320 #[serde(default = "default_disable_cluster_metrics")]
3321 pub disable_cluster_metrics: bool,
3322 pub http_listeners: Vec<HttpListenerConfig>,
3323 pub https_listeners: Vec<HttpsListenerConfig>,
3324 pub tcp_listeners: Vec<TcpListenerConfig>,
3325 #[serde(default)]
3326 pub udp_listeners: Vec<UdpListenerConfig>,
3327 pub clusters: HashMap<String, ClusterConfig>,
3328 pub handle_process_affinity: bool,
3329 pub ctl_command_timeout: u64,
3330 pub pid_file_path: Option<String>,
3331 pub activate_listeners: bool,
3332 #[serde(default = "default_front_timeout")]
3333 pub front_timeout: u32,
3334 #[serde(default = "default_back_timeout")]
3335 pub back_timeout: u32,
3336 #[serde(default = "default_connect_timeout")]
3337 pub connect_timeout: u32,
3338 #[serde(default = "default_zombie_check_interval")]
3339 pub zombie_check_interval: u32,
3340 #[serde(default = "default_accept_queue_timeout")]
3341 pub accept_queue_timeout: u32,
3342 #[serde(default = "default_evict_on_queue_full")]
3343 pub evict_on_queue_full: bool,
3344 #[serde(default = "default_request_timeout")]
3345 pub request_timeout: u32,
3346 #[serde(default = "default_worker_timeout")]
3347 pub worker_timeout: u32,
3348 #[serde(default)]
3355 pub slab_entries_per_connection: Option<u64>,
3356 #[serde(default)]
3361 pub command_allowed_uids: Option<Vec<u32>>,
3362 #[serde(default)]
3367 pub basic_auth_max_credential_bytes: Option<u64>,
3368 #[serde(default = "default_max_connections_per_ip")]
3373 pub max_connections_per_ip: u64,
3374 #[serde(default = "default_retry_after")]
3377 pub retry_after: u32,
3378 #[serde(default)]
3385 pub splice_pipe_capacity_bytes: Option<u64>,
3386}
3387
3388fn default_front_timeout() -> u32 {
3389 DEFAULT_FRONT_TIMEOUT
3390}
3391
3392fn default_back_timeout() -> u32 {
3393 DEFAULT_BACK_TIMEOUT
3394}
3395
3396fn default_connect_timeout() -> u32 {
3397 DEFAULT_CONNECT_TIMEOUT
3398}
3399
3400fn default_request_timeout() -> u32 {
3401 DEFAULT_REQUEST_TIMEOUT
3402}
3403
3404fn default_zombie_check_interval() -> u32 {
3405 DEFAULT_ZOMBIE_CHECK_INTERVAL
3406}
3407
3408fn default_accept_queue_timeout() -> u32 {
3409 DEFAULT_ACCEPT_QUEUE_TIMEOUT
3410}
3411
3412fn default_evict_on_queue_full() -> bool {
3413 DEFAULT_EVICT_ON_QUEUE_FULL
3414}
3415
3416fn default_disable_cluster_metrics() -> bool {
3417 DEFAULT_DISABLE_CLUSTER_METRICS
3418}
3419
3420fn default_worker_timeout() -> u32 {
3421 DEFAULT_WORKER_TIMEOUT
3422}
3423
3424fn default_max_connections_per_ip() -> u64 {
3425 DEFAULT_MAX_CONNECTIONS_PER_IP
3426}
3427
3428fn default_retry_after() -> u32 {
3429 DEFAULT_RETRY_AFTER
3430}
3431
3432impl Config {
3433 pub fn load_from_path(path: &str) -> Result<Config, ConfigError> {
3435 let file_config = FileConfig::load_from_path(path)?;
3436
3437 let mut config = ConfigBuilder::new(file_config, path).into_config()?;
3438
3439 config.saved_state = config.saved_state_path()?;
3441
3442 Ok(config)
3443 }
3444
3445 pub fn generate_config_messages(&self) -> Result<Vec<WorkerRequest>, ConfigError> {
3447 let mut v = Vec::new();
3448 let mut count = 0u8;
3449
3450 for listener in &self.http_listeners {
3451 v.push(WorkerRequest {
3452 id: format!("CONFIG-{count}"),
3453 content: RequestType::AddHttpListener(listener.clone()).into(),
3454 });
3455 count += 1;
3456 }
3457
3458 for listener in &self.https_listeners {
3459 v.push(WorkerRequest {
3460 id: format!("CONFIG-{count}"),
3461 content: RequestType::AddHttpsListener(listener.clone()).into(),
3462 });
3463 count += 1;
3464 }
3465
3466 for listener in &self.tcp_listeners {
3467 v.push(WorkerRequest {
3468 id: format!("CONFIG-{count}"),
3469 content: RequestType::AddTcpListener(*listener).into(),
3470 });
3471 count += 1;
3472 }
3473
3474 for listener in &self.udp_listeners {
3475 v.push(WorkerRequest {
3476 id: format!("CONFIG-{count}"),
3477 content: RequestType::AddUdpListener(*listener).into(),
3478 });
3479 count += 1;
3480 }
3481
3482 for cluster in self.clusters.values() {
3483 let mut orders = cluster.generate_requests()?;
3484 for content in orders.drain(..) {
3485 v.push(WorkerRequest {
3486 id: format!("CONFIG-{count}"),
3487 content,
3488 });
3489 count += 1;
3490 }
3491 }
3492
3493 if self.activate_listeners {
3494 for listener in &self.http_listeners {
3495 v.push(WorkerRequest {
3496 id: format!("CONFIG-{count}"),
3497 content: RequestType::ActivateListener(ActivateListener {
3498 address: listener.address,
3499 proxy: ListenerType::Http.into(),
3500 from_scm: false,
3501 })
3502 .into(),
3503 });
3504 count += 1;
3505 }
3506
3507 for listener in &self.https_listeners {
3508 v.push(WorkerRequest {
3509 id: format!("CONFIG-{count}"),
3510 content: RequestType::ActivateListener(ActivateListener {
3511 address: listener.address,
3512 proxy: ListenerType::Https.into(),
3513 from_scm: false,
3514 })
3515 .into(),
3516 });
3517 count += 1;
3518 }
3519
3520 for listener in &self.tcp_listeners {
3521 v.push(WorkerRequest {
3522 id: format!("CONFIG-{count}"),
3523 content: RequestType::ActivateListener(ActivateListener {
3524 address: listener.address,
3525 proxy: ListenerType::Tcp.into(),
3526 from_scm: false,
3527 })
3528 .into(),
3529 });
3530 count += 1;
3531 }
3532
3533 for listener in &self.udp_listeners {
3534 v.push(WorkerRequest {
3535 id: format!("CONFIG-{count}"),
3536 content: RequestType::ActivateListener(ActivateListener {
3537 address: listener.address,
3538 proxy: ListenerType::Udp.into(),
3539 from_scm: false,
3540 })
3541 .into(),
3542 });
3543 count += 1;
3544 }
3545 }
3546
3547 if self.disable_cluster_metrics {
3548 v.push(WorkerRequest {
3549 id: format!("CONFIG-{count}"),
3550 content: RequestType::ConfigureMetrics(MetricsConfiguration::Disabled.into())
3551 .into(),
3552 });
3553 }
3555
3556 Ok(v)
3557 }
3558
3559 pub fn command_socket_path(&self) -> Result<String, ConfigError> {
3561 let config_path_buf = PathBuf::from(self.config_path.clone());
3562 let mut config_dir = config_path_buf
3563 .parent()
3564 .ok_or(ConfigError::NoFileParent(
3565 config_path_buf.to_string_lossy().to_string(),
3566 ))?
3567 .to_path_buf();
3568
3569 let socket_path = PathBuf::from(self.command_socket.clone());
3570
3571 let mut socket_parent_dir = match socket_path.parent() {
3572 None => config_dir,
3575 Some(path) => {
3576 config_dir.push(path);
3578 config_dir.canonicalize().map_err(|io_error| {
3580 ConfigError::SocketPathError(format!(
3581 "Could not canonicalize path {config_dir:?}: {io_error}"
3582 ))
3583 })?
3584 }
3585 };
3586
3587 let socket_name = socket_path
3588 .file_name()
3589 .ok_or(ConfigError::SocketPathError(format!(
3590 "could not get command socket file name from {socket_path:?}"
3591 )))?;
3592
3593 socket_parent_dir.push(socket_name);
3595
3596 let command_socket_path = socket_parent_dir
3597 .to_str()
3598 .ok_or(ConfigError::SocketPathError(format!(
3599 "Invalid socket path {socket_parent_dir:?}"
3600 )))?
3601 .to_string();
3602
3603 Ok(command_socket_path)
3604 }
3605
3606 fn saved_state_path(&self) -> Result<Option<String>, ConfigError> {
3608 let path = match self.saved_state.as_ref() {
3609 Some(path) => path,
3610 None => return Ok(None),
3611 };
3612
3613 debug!("saved_stated path in the config: {}", path);
3614 let config_path = PathBuf::from(self.config_path.clone());
3615
3616 debug!("Config path buffer: {:?}", config_path);
3617 let config_dir = config_path
3618 .parent()
3619 .ok_or(ConfigError::SaveStatePath(format!(
3620 "Could get parent directory of config file {config_path:?}"
3621 )))?;
3622
3623 debug!("Config folder: {:?}", config_dir);
3624 if !config_dir.exists() {
3625 create_dir_all(config_dir).map_err(|io_error| {
3626 ConfigError::SaveStatePath(format!(
3627 "failed to create state parent directory '{config_dir:?}': {io_error}"
3628 ))
3629 })?;
3630 }
3631
3632 let mut saved_state_path_raw = config_dir.to_path_buf();
3633 saved_state_path_raw.push(path);
3634 debug!(
3635 "Looking for saved state on the path {:?}",
3636 saved_state_path_raw
3637 );
3638
3639 match metadata(path) {
3640 Err(err) if matches!(err.kind(), ErrorKind::NotFound) => {
3641 info!("Create an empty state file at '{}'", path);
3642 File::create(path).map_err(|io_error| {
3643 ConfigError::SaveStatePath(format!(
3644 "failed to create state file '{path:?}': {io_error}"
3645 ))
3646 })?;
3647 }
3648 _ => {}
3649 }
3650
3651 saved_state_path_raw.canonicalize().map_err(|io_error| {
3652 ConfigError::SaveStatePath(format!(
3653 "could not get saved state path from config file input {path:?}: {io_error}"
3654 ))
3655 })?;
3656
3657 let stringified_path = saved_state_path_raw
3658 .to_str()
3659 .ok_or(ConfigError::SaveStatePath(format!(
3660 "Invalid path {saved_state_path_raw:?}"
3661 )))?
3662 .to_string();
3663
3664 Ok(Some(stringified_path))
3665 }
3666
3667 pub fn load_file(path: &str) -> Result<String, ConfigError> {
3669 std::fs::read_to_string(path).map_err(|io_error| ConfigError::FileRead {
3670 path_to_read: path.to_owned(),
3671 io_error,
3672 })
3673 }
3674
3675 pub fn load_file_bytes(path: &str) -> Result<Vec<u8>, ConfigError> {
3677 std::fs::read(path).map_err(|io_error| ConfigError::FileRead {
3678 path_to_read: path.to_owned(),
3679 io_error,
3680 })
3681 }
3682}
3683
3684impl fmt::Debug for Config {
3685 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3686 f.debug_struct("Config")
3687 .field("config_path", &self.config_path)
3688 .field("command_socket", &self.command_socket)
3689 .field("command_buffer_size", &self.command_buffer_size)
3690 .field("max_command_buffer_size", &self.max_command_buffer_size)
3691 .field("max_connections", &self.max_connections)
3692 .field("min_buffers", &self.min_buffers)
3693 .field("max_buffers", &self.max_buffers)
3694 .field("buffer_size", &self.buffer_size)
3695 .field("saved_state", &self.saved_state)
3696 .field("automatic_state_save", &self.automatic_state_save)
3697 .field("log_level", &self.log_level)
3698 .field("log_target", &self.log_target)
3699 .field("access_logs_target", &self.access_logs_target)
3700 .field("audit_logs_target", &self.audit_logs_target)
3701 .field("audit_logs_json_target", &self.audit_logs_json_target)
3702 .field("access_logs_format", &self.access_logs_format)
3703 .field("worker_count", &self.worker_count)
3704 .field("worker_automatic_restart", &self.worker_automatic_restart)
3705 .field("metrics", &self.metrics)
3706 .field("disable_cluster_metrics", &self.disable_cluster_metrics)
3707 .field("handle_process_affinity", &self.handle_process_affinity)
3708 .field("ctl_command_timeout", &self.ctl_command_timeout)
3709 .field("pid_file_path", &self.pid_file_path)
3710 .field("activate_listeners", &self.activate_listeners)
3711 .field("front_timeout", &self.front_timeout)
3712 .field("back_timeout", &self.back_timeout)
3713 .field("connect_timeout", &self.connect_timeout)
3714 .field("zombie_check_interval", &self.zombie_check_interval)
3715 .field("accept_queue_timeout", &self.accept_queue_timeout)
3716 .field("evict_on_queue_full", &self.evict_on_queue_full)
3717 .field("request_timeout", &self.request_timeout)
3718 .field("worker_timeout", &self.worker_timeout)
3719 .finish()
3720 }
3721}
3722
3723fn display_toml_error(file: &str, error: &toml::de::Error) {
3724 println!("error parsing the configuration file '{file}': {error}");
3725 if let Some(Range { start, end }) = error.span() {
3726 print!("error parsing the configuration file '{file}' at position: {start}, {end}");
3727 }
3728}
3729
3730impl ServerConfig {
3731 pub const DEFAULT_SLAB_ENTRIES_PER_CONNECTION: u64 = 4;
3738 pub const MIN_SLAB_ENTRIES_PER_CONNECTION: u64 = 2;
3741 pub const MAX_SLAB_ENTRIES_PER_CONNECTION: u64 = 32;
3744
3745 pub fn effective_slab_entries_per_connection(&self) -> u64 {
3748 let effective = match self.slab_entries_per_connection {
3749 Some(0) | None => Self::DEFAULT_SLAB_ENTRIES_PER_CONNECTION,
3750 Some(n) => n.clamp(
3751 Self::MIN_SLAB_ENTRIES_PER_CONNECTION,
3752 Self::MAX_SLAB_ENTRIES_PER_CONNECTION,
3753 ),
3754 };
3755 debug_assert!(
3759 (Self::MIN_SLAB_ENTRIES_PER_CONNECTION..=Self::MAX_SLAB_ENTRIES_PER_CONNECTION)
3760 .contains(&effective),
3761 "effective slab entries per connection must stay within [MIN, MAX]"
3762 );
3763 effective
3764 }
3765
3766 pub fn slab_capacity(&self) -> u64 {
3773 let per_conn = self.effective_slab_entries_per_connection();
3774 let capacity = 10 + per_conn * self.max_connections;
3775 debug_assert!(
3780 capacity >= 10,
3781 "slab capacity must reserve the base entries"
3782 );
3783 debug_assert!(
3784 self.max_connections == 0 || capacity > 10,
3785 "a non-zero connection cap must reserve per-connection slab entries"
3786 );
3787 capacity
3788 }
3789}
3790
3791impl From<&Config> for ServerConfig {
3793 fn from(config: &Config) -> Self {
3794 let metrics = config.metrics.clone().map(|m| ServerMetricsConfig {
3795 address: m.address.to_string(),
3796 tagged_metrics: m.tagged_metrics,
3797 prefix: m.prefix,
3798 detail: Some(MetricDetail::from(m.detail) as i32),
3799 });
3800 let server_config = Self {
3801 max_connections: config.max_connections as u64,
3802 front_timeout: config.front_timeout,
3803 back_timeout: config.back_timeout,
3804 connect_timeout: config.connect_timeout,
3805 zombie_check_interval: config.zombie_check_interval,
3806 accept_queue_timeout: config.accept_queue_timeout,
3807 min_buffers: config.min_buffers,
3808 max_buffers: config.max_buffers,
3809 buffer_size: config.buffer_size,
3810 log_level: config.log_level.clone(),
3811 log_target: config.log_target.clone(),
3812 access_logs_target: config.access_logs_target.clone(),
3813 audit_logs_target: config.audit_logs_target.clone(),
3814 audit_logs_json_target: config.audit_logs_json_target.clone(),
3815 command_buffer_size: config.command_buffer_size,
3816 max_command_buffer_size: config.max_command_buffer_size,
3817 metrics,
3818 access_log_format: ProtobufAccessLogFormat::from(&config.access_logs_format) as i32,
3819 log_colored: config.log_colored,
3820 slab_entries_per_connection: config.slab_entries_per_connection,
3821 basic_auth_max_credential_bytes: config.basic_auth_max_credential_bytes,
3822 evict_on_queue_full: Some(config.evict_on_queue_full),
3823 max_connections_per_ip: Some(config.max_connections_per_ip),
3824 retry_after: Some(config.retry_after),
3825 splice_pipe_capacity_bytes: config.splice_pipe_capacity_bytes,
3826 };
3827
3828 debug_assert!(
3833 server_config.min_buffers <= server_config.max_buffers,
3834 "ServerConfig must preserve min_buffers <= max_buffers"
3835 );
3836 debug_assert_eq!(
3837 server_config.buffer_size, config.buffer_size,
3838 "ServerConfig buffer_size must mirror the source config"
3839 );
3840 debug_assert_eq!(
3841 server_config.max_connections, config.max_connections as u64,
3842 "ServerConfig max_connections must mirror the source config"
3843 );
3844 server_config
3845 }
3846}
3847
3848#[cfg(test)]
3849mod tests {
3850 use toml::to_string;
3851
3852 use super::*;
3853
3854 #[test]
3855 fn hsts_to_proto_enabled_substitutes_default_max_age() {
3856 let cfg = FileHstsConfig {
3857 enabled: Some(true),
3858 max_age: None,
3859 include_subdomains: None,
3860 preload: None,
3861 force_replace_backend: None,
3862 };
3863 let proto = cfg.to_proto("test").expect("should validate");
3864 assert_eq!(proto.enabled, Some(true));
3865 assert_eq!(proto.max_age, Some(DEFAULT_HSTS_MAX_AGE));
3866 }
3867
3868 #[test]
3869 fn hsts_to_proto_explicit_max_age_kept() {
3870 let cfg = FileHstsConfig {
3871 enabled: Some(true),
3872 max_age: Some(63_072_000),
3873 include_subdomains: Some(true),
3874 preload: Some(true),
3875 force_replace_backend: None,
3876 };
3877 let proto = cfg.to_proto("test").expect("should validate");
3878 assert_eq!(proto.max_age, Some(63_072_000));
3879 assert_eq!(proto.include_subdomains, Some(true));
3880 assert_eq!(proto.preload, Some(true));
3881 }
3882
3883 #[test]
3884 fn hsts_to_proto_disabled_keeps_zero_intent() {
3885 let cfg = FileHstsConfig {
3889 enabled: Some(false),
3890 max_age: None,
3891 include_subdomains: None,
3892 preload: None,
3893 force_replace_backend: None,
3894 };
3895 let proto = cfg.to_proto("test").expect("should validate");
3896 assert_eq!(proto.enabled, Some(false));
3897 }
3898
3899 #[test]
3900 fn hsts_to_proto_kill_switch_max_age_zero_allowed() {
3901 let cfg = FileHstsConfig {
3905 enabled: Some(true),
3906 max_age: Some(0),
3907 include_subdomains: None,
3908 preload: None,
3909 force_replace_backend: None,
3910 };
3911 let proto = cfg.to_proto("test").expect("kill-switch must validate");
3912 assert_eq!(proto.max_age, Some(0));
3913 }
3914
3915 #[test]
3916 fn hsts_to_proto_missing_enabled_errors() {
3917 let cfg = FileHstsConfig {
3918 enabled: None,
3919 max_age: Some(31_536_000),
3920 include_subdomains: None,
3921 preload: None,
3922 force_replace_backend: None,
3923 };
3924 match cfg.to_proto("test").unwrap_err() {
3925 ConfigError::HstsEnabledRequired(scope) => assert_eq!(scope, "test"),
3926 other => panic!("expected HstsEnabledRequired, got {other:?}"),
3927 }
3928 }
3929
3930 #[test]
3931 fn hsts_rejected_on_http_listener() {
3932 let mut listener = ListenerBuilder::new(
3937 SocketAddress::new_v4(127, 0, 0, 1, 8080),
3938 ListenerProtocol::Http,
3939 );
3940 listener.hsts = Some(FileHstsConfig {
3941 enabled: Some(true),
3942 max_age: Some(31_536_000),
3943 include_subdomains: None,
3944 preload: None,
3945 force_replace_backend: None,
3946 });
3947 match listener.to_http(None).unwrap_err() {
3948 ConfigError::HstsOnPlainHttp(scope) => assert!(
3949 scope.contains("HTTP listener"),
3950 "expected scope to mention 'HTTP listener', got {scope:?}"
3951 ),
3952 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
3953 }
3954 }
3955
3956 #[test]
3957 fn hsts_rejected_on_http_frontend() {
3958 let frontend = FileClusterFrontendConfig {
3964 address: "127.0.0.1:8080".parse().unwrap(),
3965 hostname: Some("example.com".to_owned()),
3966 path: None,
3967 path_type: None,
3968 method: None,
3969 certificate: None,
3970 key: None,
3971 certificate_chain: None,
3972 tls_versions: vec![],
3973 position: RulePosition::Tree,
3974 tags: None,
3975 redirect: None,
3976 redirect_scheme: None,
3977 redirect_template: None,
3978 rewrite_host: None,
3979 rewrite_path: None,
3980 rewrite_port: None,
3981 required_auth: None,
3982 headers: None,
3983 hsts: Some(FileHstsConfig {
3984 enabled: Some(true),
3985 max_age: Some(31_536_000),
3986 include_subdomains: None,
3987 preload: None,
3988 force_replace_backend: None,
3989 }),
3990 };
3991 match frontend.to_http_front("api").unwrap_err() {
3992 ConfigError::HstsOnPlainHttp(scope) => {
3993 assert!(
3994 scope.contains("api") && scope.contains("example.com"),
3995 "expected scope to mention 'api' and 'example.com', got {scope:?}"
3996 );
3997 }
3998 other => panic!("expected HstsOnPlainHttp, got {other:?}"),
3999 }
4000 }
4001
4002 #[test]
4003 fn serialize() {
4004 let http = ListenerBuilder::new(
4005 SocketAddress::new_v4(127, 0, 0, 1, 8080),
4006 ListenerProtocol::Http,
4007 )
4008 .with_answer_404_path(Some("404.html"))
4009 .to_owned();
4010 println!("http: {:?}", to_string(&http));
4011
4012 let https = ListenerBuilder::new(
4013 SocketAddress::new_v4(127, 0, 0, 1, 8443),
4014 ListenerProtocol::Https,
4015 )
4016 .with_answer_404_path(Some("404.html"))
4017 .to_owned();
4018 println!("https: {:?}", to_string(&https));
4019
4020 let listeners = vec![http, https];
4021 let config = FileConfig {
4022 command_socket: Some(String::from("./command_folder/sock")),
4023 worker_count: Some(2),
4024 worker_automatic_restart: Some(true),
4025 max_connections: Some(500),
4026 min_buffers: Some(1),
4027 max_buffers: Some(500),
4028 buffer_size: Some(16393),
4029 metrics: Some(MetricsConfig {
4030 address: "127.0.0.1:8125".parse().unwrap(),
4031 tagged_metrics: false,
4032 prefix: Some(String::from("sozu-metrics")),
4033 detail: MetricDetailLevel::default(),
4034 }),
4035 listeners: Some(listeners),
4036 ..Default::default()
4037 };
4038
4039 println!("config: {:?}", to_string(&config));
4040 let encoded = to_string(&config).unwrap();
4041 println!("conf:\n{encoded}");
4042 }
4043
4044 #[test]
4045 fn parse() {
4046 let path = "assets/config.toml";
4047 let config = Config::load_from_path(path).unwrap_or_else(|load_error| {
4048 panic!("Cannot load config from path {path}: {load_error:?}")
4049 });
4050 println!("config: {config:#?}");
4051 }
4053
4054 #[test]
4055 fn multiple_listeners_preserve_per_address_expect_proxy() {
4056 let toml_content = r#"
4057 command_socket = "/tmp/sozu_test.sock"
4058 worker_count = 1
4059
4060 [[listeners]]
4061 protocol = "http"
4062 address = "172.16.20.1:80"
4063 expect_proxy = true
4064
4065 [[listeners]]
4066 protocol = "http"
4067 address = "10.22.0.1:80"
4068 expect_proxy = false
4069
4070 [[listeners]]
4071 protocol = "https"
4072 address = "192.168.1.1:443"
4073 expect_proxy = true
4074
4075 [[listeners]]
4076 protocol = "https"
4077 address = "192.168.2.1:443"
4078 expect_proxy = false
4079 "#;
4080
4081 let file_config: FileConfig =
4082 toml::from_str(toml_content).expect("Could not parse TOML config");
4083
4084 let listeners = file_config.listeners.as_ref().expect("No listeners found");
4085 assert_eq!(listeners.len(), 4);
4086
4087 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4088 .into_config()
4089 .expect("Could not build config");
4090
4091 assert_eq!(config.http_listeners.len(), 2);
4092 assert_eq!(config.https_listeners.len(), 2);
4093
4094 let http_proxy = config
4096 .http_listeners
4097 .iter()
4098 .find(|l| SocketAddr::from(l.address) == "172.16.20.1:80".parse().unwrap())
4099 .expect("Listener on 172.16.20.1:80 not found");
4100 let http_direct = config
4101 .http_listeners
4102 .iter()
4103 .find(|l| SocketAddr::from(l.address) == "10.22.0.1:80".parse().unwrap())
4104 .expect("Listener on 10.22.0.1:80 not found");
4105
4106 assert!(http_proxy.expect_proxy);
4107 assert!(!http_direct.expect_proxy);
4108
4109 let https_proxy = config
4111 .https_listeners
4112 .iter()
4113 .find(|l| SocketAddr::from(l.address) == "192.168.1.1:443".parse().unwrap())
4114 .expect("Listener on 192.168.1.1:443 not found");
4115 let https_direct = config
4116 .https_listeners
4117 .iter()
4118 .find(|l| SocketAddr::from(l.address) == "192.168.2.1:443".parse().unwrap())
4119 .expect("Listener on 192.168.2.1:443 not found");
4120
4121 assert!(https_proxy.expect_proxy);
4122 assert!(!https_direct.expect_proxy);
4123 }
4124
4125 #[test]
4126 fn multiple_listeners_generate_correct_worker_requests() {
4127 let toml_content = r#"
4128 command_socket = "/tmp/sozu_test.sock"
4129 worker_count = 1
4130 activate_listeners = true
4131
4132 [[listeners]]
4133 protocol = "http"
4134 address = "172.16.20.1:80"
4135 expect_proxy = true
4136
4137 [[listeners]]
4138 protocol = "http"
4139 address = "10.22.0.1:80"
4140 expect_proxy = false
4141 "#;
4142
4143 let file_config: FileConfig =
4144 toml::from_str(toml_content).expect("Could not parse TOML config");
4145
4146 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4147 .into_config()
4148 .expect("Could not build config");
4149
4150 let messages = config
4151 .generate_config_messages()
4152 .expect("Could not generate config messages");
4153
4154 let add_listener_count = messages
4155 .iter()
4156 .filter(|m| {
4157 matches!(
4158 m.content.request_type,
4159 Some(RequestType::AddHttpListener(_))
4160 )
4161 })
4162 .count();
4163
4164 let activate_listener_count = messages
4165 .iter()
4166 .filter(|m| {
4167 matches!(
4168 m.content.request_type,
4169 Some(RequestType::ActivateListener(ActivateListener {
4170 proxy,
4171 ..
4172 })) if proxy == ListenerType::Http as i32
4173 )
4174 })
4175 .count();
4176
4177 assert_eq!(add_listener_count, 2);
4178 assert_eq!(activate_listener_count, 2);
4179 }
4180
4181 #[test]
4182 fn documented_udp_dns_example_loads_and_emits_udp_requests() {
4183 let toml_content = r#"
4190 command_socket = "/tmp/sozu_test.sock"
4191 worker_count = 1
4192 activate_listeners = true
4193
4194 [[listeners]]
4195 protocol = "udp"
4196 address = "0.0.0.0:53"
4197
4198 [clusters.dns]
4199 protocol = "tcp"
4200 load_balancing = "HRW"
4201 frontends = [
4202 { address = "0.0.0.0:53" }
4203 ]
4204 backends = [
4205 { address = "10.0.0.10:53" },
4206 { address = "10.0.0.11:53" }
4207 ]
4208
4209 [clusters.dns.udp]
4210 affinity_key = "SOURCE_IP"
4211 responses = 1
4212 requests = 0
4213 send_proxy_protocol = true
4214
4215 [clusters.dns.udp.health]
4216 mode = "TCP_PROBE"
4217 tcp_port = 53
4218 rise = 2
4219 fall = 3
4220 fail_open = true
4221 "#;
4222
4223 let file_config: FileConfig =
4224 toml::from_str(toml_content).expect("Could not parse documented DNS TOML");
4225
4226 let config = ConfigBuilder::new(file_config, "/tmp/test_config.toml")
4227 .into_config()
4228 .expect("documented UDP DNS example must load without WrongFrontendProtocol");
4229
4230 assert_eq!(
4232 config.udp_listeners.len(),
4233 1,
4234 "the protocol=\"udp\" listener must be built"
4235 );
4236
4237 let messages = config
4238 .generate_config_messages()
4239 .expect("Could not generate config messages");
4240
4241 let add_udp_listener_count = messages
4242 .iter()
4243 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpListener(_))))
4244 .count();
4245 assert_eq!(
4246 add_udp_listener_count, 1,
4247 "must emit exactly one AddUdpListener"
4248 );
4249
4250 let add_udp_frontend_count = messages
4251 .iter()
4252 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddUdpFrontend(_))))
4253 .count();
4254 assert_eq!(
4255 add_udp_frontend_count, 1,
4256 "the cluster frontend on the UDP listener must emit AddUdpFrontend"
4257 );
4258
4259 let add_tcp_frontend_count = messages
4261 .iter()
4262 .filter(|m| matches!(m.content.request_type, Some(RequestType::AddTcpFrontend(_))))
4263 .count();
4264 assert_eq!(
4265 add_tcp_frontend_count, 0,
4266 "a UDP-listener-addressed frontend must not be emitted as AddTcpFrontend"
4267 );
4268
4269 let udp_frontend = messages
4271 .iter()
4272 .find_map(|m| match &m.content.request_type {
4273 Some(RequestType::AddUdpFrontend(f)) => Some(f),
4274 _ => None,
4275 })
4276 .expect("AddUdpFrontend must be present");
4277 assert_eq!(udp_frontend.cluster_id, "dns");
4278 assert_eq!(
4279 SocketAddr::from(udp_frontend.address),
4280 "0.0.0.0:53".parse().unwrap()
4281 );
4282
4283 let cluster = messages
4286 .iter()
4287 .find_map(|m| match &m.content.request_type {
4288 Some(RequestType::AddCluster(c)) if c.cluster_id == "dns" => Some(c),
4289 _ => None,
4290 })
4291 .expect("AddCluster for 'dns' must be present");
4292 let udp = cluster
4293 .udp
4294 .as_ref()
4295 .expect("[clusters.dns.udp] block must carry onto the cluster");
4296 assert_eq!(udp.responses, Some(1));
4297 }
4298
4299 #[test]
4300 fn duplicate_listener_address_rejected() {
4301 let toml_content = r#"
4302 command_socket = "/tmp/sozu_test.sock"
4303 worker_count = 1
4304
4305 [[listeners]]
4306 protocol = "http"
4307 address = "0.0.0.0:80"
4308
4309 [[listeners]]
4310 protocol = "http"
4311 address = "0.0.0.0:80"
4312 "#;
4313
4314 let file_config: FileConfig =
4315 toml::from_str(toml_content).expect("Could not parse TOML config");
4316
4317 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4318
4319 assert!(
4320 result.is_err(),
4321 "Should reject duplicate listener addresses"
4322 );
4323 }
4324
4325 #[test]
4326 fn buffer_size_below_h2_minimum_rejected() {
4327 let toml_content = r#"
4329 command_socket = "/tmp/sozu_test.sock"
4330 worker_count = 1
4331 buffer_size = 8192
4332
4333 [[listeners]]
4334 protocol = "https"
4335 address = "127.0.0.1:8443"
4336 "#;
4337 let file_config: FileConfig =
4338 toml::from_str(toml_content).expect("Could not parse TOML config");
4339 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4340 match result {
4341 Err(ConfigError::BufferSizeTooSmallForH2 {
4342 buffer_size: 8192,
4343 minimum: 16_393,
4344 listeners: 1,
4345 }) => {}
4346 other => panic!("expected BufferSizeTooSmallForH2, got {other:?}"),
4347 }
4348 }
4349
4350 #[test]
4351 fn buffer_size_below_h2_minimum_accepted_when_no_h2_listener() {
4352 let toml_content = r#"
4354 command_socket = "/tmp/sozu_test.sock"
4355 worker_count = 1
4356 buffer_size = 8192
4357
4358 [[listeners]]
4359 protocol = "https"
4360 address = "127.0.0.1:8443"
4361 alpn_protocols = ["http/1.1"]
4362 "#;
4363 let file_config: FileConfig =
4364 toml::from_str(toml_content).expect("Could not parse TOML config");
4365 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4366 assert!(
4367 result.is_ok(),
4368 "non-H2 HTTPS listener with sub-16393 buffer should be accepted: {result:?}"
4369 );
4370 }
4371
4372 #[test]
4373 fn buffer_size_at_h2_minimum_accepted() {
4374 let toml_content = r#"
4375 command_socket = "/tmp/sozu_test.sock"
4376 worker_count = 1
4377 buffer_size = 16393
4378
4379 [[listeners]]
4380 protocol = "https"
4381 address = "127.0.0.1:8443"
4382 "#;
4383 let file_config: FileConfig =
4384 toml::from_str(toml_content).expect("Could not parse TOML config");
4385 let result = ConfigBuilder::new(file_config, "/tmp/test_config.toml").into_config();
4386 assert!(
4387 result.is_ok(),
4388 "buffer_size at the H2 minimum should be accepted: {result:?}"
4389 );
4390 }
4391
4392 #[test]
4393 fn alpn_protocols_default() {
4394 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4395 let config = builder.to_tls(None).expect("to_tls should succeed");
4396 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4397 }
4398
4399 #[test]
4400 fn alpn_protocols_custom() {
4401 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4402 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned()]));
4403 let config = builder.to_tls(None).expect("to_tls should succeed");
4404 assert_eq!(config.alpn_protocols, vec!["http/1.1"]);
4405 }
4406
4407 #[test]
4408 fn alpn_protocols_invalid_rejected() {
4409 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4410 builder.with_alpn_protocols(Some(vec!["h3".to_owned()]));
4411 let result = builder.to_tls(None);
4412 assert!(result.is_err());
4413 let err = result.unwrap_err();
4414 assert!(
4415 err.to_string().contains("h3"),
4416 "error should mention the invalid protocol: {err}"
4417 );
4418 }
4419
4420 #[test]
4421 fn alpn_protocols_empty_uses_default() {
4422 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4423 builder.with_alpn_protocols(Some(vec![]));
4424 let config = builder.to_tls(None).expect("to_tls should succeed");
4425 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4426 }
4427
4428 #[test]
4429 fn alpn_protocols_deduplicated() {
4430 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4431 builder.with_alpn_protocols(Some(vec![
4432 "h2".to_owned(),
4433 "h2".to_owned(),
4434 "http/1.1".to_owned(),
4435 ]));
4436 let config = builder.to_tls(None).expect("to_tls should succeed");
4437 assert_eq!(config.alpn_protocols, vec!["h2", "http/1.1"]);
4438 }
4439
4440 #[test]
4441 fn alpn_protocols_order_preserved() {
4442 let mut builder = ListenerBuilder::new_https(SocketAddress::new_v4(127, 0, 0, 1, 8443));
4443 builder.with_alpn_protocols(Some(vec!["http/1.1".to_owned(), "h2".to_owned()]));
4444 let config = builder.to_tls(None).expect("to_tls should succeed");
4445 assert_eq!(config.alpn_protocols, vec!["http/1.1", "h2"]);
4446 }
4447
4448 #[test]
4454 fn parse_header_edit_rejects_crlf_in_value() {
4455 let entry = HeaderEditConfig {
4456 position: "request".to_owned(),
4457 key: "X-Test".to_owned(),
4458 value: "value\r\nEvil-Header: stolen".to_owned(),
4459 };
4460 let err = parse_header_edit(0, &entry).expect_err("CRLF in value must be rejected");
4461 match err {
4462 ConfigError::InvalidHeaderBytes { index, field } => {
4463 assert_eq!(index, 0);
4464 assert_eq!(field, "value");
4465 }
4466 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
4467 }
4468 }
4469
4470 #[test]
4471 fn parse_header_edit_rejects_lf_in_key() {
4472 let entry = HeaderEditConfig {
4473 position: "response".to_owned(),
4474 key: "X-\nTest".to_owned(),
4475 value: "ok".to_owned(),
4476 };
4477 let err = parse_header_edit(2, &entry).expect_err("LF in key must be rejected");
4478 match err {
4479 ConfigError::InvalidHeaderBytes { index, field } => {
4480 assert_eq!(index, 2);
4481 assert_eq!(field, "key");
4482 }
4483 other => panic!("expected InvalidHeaderBytes, got {other:?}"),
4484 }
4485 }
4486
4487 #[test]
4488 fn parse_header_edit_rejects_nul() {
4489 let entry = HeaderEditConfig {
4490 position: "both".to_owned(),
4491 key: "X-Test".to_owned(),
4492 value: "with\0nul".to_owned(),
4493 };
4494 assert!(matches!(
4495 parse_header_edit(0, &entry),
4496 Err(ConfigError::InvalidHeaderBytes { .. })
4497 ));
4498 }
4499
4500 #[test]
4506 fn parse_header_edit_accepts_tab_in_value() {
4507 let entry = HeaderEditConfig {
4508 position: "request".to_owned(),
4509 key: "X-Test".to_owned(),
4510 value: "with\ttab".to_owned(),
4511 };
4512 let header = parse_header_edit(0, &entry).expect("tab in value must be accepted");
4513 assert_eq!(header.val, "with\ttab");
4514 }
4515
4516 #[test]
4523 fn parse_header_edit_rejects_tab_in_key() {
4524 let entry = HeaderEditConfig {
4525 position: "request".to_owned(),
4526 key: "Host\t".to_owned(),
4527 value: "ok".to_owned(),
4528 };
4529 let err = parse_header_edit(0, &entry).expect_err("HTAB in key must be rejected");
4530 match err {
4531 ConfigError::InvalidHeaderBytes { field, .. } => assert_eq!(field, "key"),
4532 other => panic!("expected InvalidHeaderBytes{{field=\"key\"}}, got {other:?}"),
4533 }
4534 }
4535
4536 #[test]
4537 fn parse_header_edit_rejects_space_in_key() {
4538 let entry = HeaderEditConfig {
4539 position: "request".to_owned(),
4540 key: "X Test".to_owned(),
4541 value: "ok".to_owned(),
4542 };
4543 let err = parse_header_edit(0, &entry).expect_err("SP in key must be rejected");
4544 assert!(matches!(err, ConfigError::InvalidHeaderBytes { .. }));
4545 }
4546
4547 #[test]
4548 fn parse_header_edit_rejects_empty_key() {
4549 let entry = HeaderEditConfig {
4550 position: "request".to_owned(),
4551 key: String::new(),
4552 value: "ok".to_owned(),
4553 };
4554 let err = parse_header_edit(0, &entry).expect_err("empty key must be rejected");
4555 assert!(matches!(
4556 err,
4557 ConfigError::InvalidHeaderBytes { field: "key", .. }
4558 ));
4559 }
4560
4561 #[test]
4562 fn parse_header_edit_accepts_clean_value() {
4563 let entry = HeaderEditConfig {
4564 position: "request".to_owned(),
4565 key: "X-Tenant".to_owned(),
4566 value: "alpha".to_owned(),
4567 };
4568 let header = parse_header_edit(0, &entry).expect("clean value must be accepted");
4569 assert_eq!(header.key, "X-Tenant");
4570 assert_eq!(header.val, "alpha");
4571 }
4572
4573 #[test]
4577 fn resolve_answer_source_bare_string_is_literal() {
4578 let body = resolve_answer_source("HTTP/1.1 503 Service Unavailable\r\n\r\nbusy")
4579 .expect("bare-string source must resolve");
4580 assert_eq!(body, "HTTP/1.1 503 Service Unavailable\r\n\r\nbusy");
4581 }
4582
4583 #[test]
4584 fn resolve_answer_source_empty_string_is_legitimate() {
4585 let body = resolve_answer_source("").expect("empty source must resolve");
4586 assert_eq!(body, "");
4587 }
4588
4589 #[test]
4594 fn resolve_answer_source_file_scheme_missing_file_errors() {
4595 let err = resolve_answer_source("file:///nonexistent/sozu-test/never.http")
4596 .expect_err("missing path must error");
4597 assert!(matches!(err, ConfigError::FileOpen { .. }));
4598 }
4599
4600 #[test]
4603 fn resolve_answer_source_file_scheme_empty_path_errors() {
4604 let err = resolve_answer_source("file://").expect_err("empty path must error");
4605 assert!(matches!(err, ConfigError::FileOpen { .. }));
4606 }
4607}