1use std::{
2 borrow::Cow,
3 collections::{
4 BTreeMap, BTreeSet, HashMap, HashSet, btree_map::Entry as BTreeMapEntry,
5 hash_map::DefaultHasher,
6 },
7 fmt,
8 fs::File,
9 hash::{Hash, Hasher},
10 io::Write,
11 net::SocketAddr,
12};
13
14use prost::{Message, UnknownEnumValue};
15
16use crate::{
17 ObjectKind,
18 certificate::{CertificateError, Fingerprint, calculate_fingerprint},
19 config::validate_sni_pattern,
20 listener_key::{InterfaceError, ListenerKey, validate_listener_interface},
21 proto::{
22 command::{
23 ActivateListener, AddBackend, AddCertificate, CertificateAndKey, Cluster,
24 ClusterInformation, CustomHttpAnswers, DeactivateListener, ForwardedHeaders,
25 FrontendFilters, HealthCheckConfig, HealthChecksList, HstsConfig, HttpListenerConfig,
26 HttpsListenerConfig, InitialState, ListedFrontends, ListenerType, ListenersList,
27 PathRule, QueryCertificatesFilters, RemoveBackend, RemoveCertificate, RemoveListener,
28 ReplaceCertificate, Request, RequestCounts, RequestHttpFrontend, RequestTcpFrontend,
29 RequestUdpFrontend, SetProtectedHealthCheck, SocketAddress, TcpListenerConfig,
30 UdpListenerConfig, UpdateHttpListenerConfig, UpdateHttpsListenerConfig,
31 UpdateTcpListenerConfig, UpdateUdpListenerConfig, WorkerRequest, request::RequestType,
32 },
33 display::format_request_type,
34 },
35 response::{Backend, HttpFrontend, TcpFrontend, UdpFrontend},
36};
37
38pub type ClusterId = std::sync::Arc<str>;
45
46const _: fn() = || {
50 fn assert_send_sync<T: Send + Sync>() {}
51 assert_send_sync::<ClusterId>();
52 assert_send_sync::<ConfigState>();
53 assert_send_sync::<StateError>();
54 assert_send_sync::<crate::response::HttpFrontend>();
55 assert_send_sync::<crate::response::TcpFrontend>();
56 assert_send_sync::<crate::response::UdpFrontend>();
57 assert_send_sync::<crate::response::Backend>();
58};
59
60pub fn rollback_request(request: &RequestType) -> Option<Request> {
68 let inverse = match request {
69 RequestType::AddHttpListener(config) => RequestType::RemoveListener(RemoveListener {
70 interface: config.interface.clone(),
71 address: config.address,
72 proxy: ListenerType::Http.into(),
73 }),
74 RequestType::AddHttpsListener(config)
75 | RequestType::AddHttpsListenerWithClientAuth(config) => {
76 RequestType::RemoveListener(RemoveListener {
77 interface: config.interface.clone(),
78 address: config.address,
79 proxy: ListenerType::Https.into(),
80 })
81 }
82 RequestType::AddTcpListener(config) => RequestType::RemoveListener(RemoveListener {
83 interface: config.interface.clone(),
84 address: config.address,
85 proxy: ListenerType::Tcp.into(),
86 }),
87 RequestType::AddUdpListener(config) => RequestType::RemoveListener(RemoveListener {
88 interface: config.interface.clone(),
89 address: config.address,
90 proxy: ListenerType::Udp.into(),
91 }),
92 RequestType::AddHttpFrontend(front)
93 | RequestType::AddHttpFrontendWithSilentClose(front)
94 | RequestType::AddHttpFrontendWithHeaderPredicates(front) => {
95 RequestType::remove_http_frontend(front.clone())
96 }
97 RequestType::AddHttpsFrontend(front)
98 | RequestType::AddHttpsFrontendWithSilentClose(front)
99 | RequestType::AddHttpsFrontendWithHeaderPredicates(front) => {
100 RequestType::remove_https_frontend(front.clone())
101 }
102 RequestType::AddTcpFrontend(front) => RequestType::RemoveTcpFrontend(front.clone()),
103 RequestType::AddUdpFrontend(front) => RequestType::RemoveUdpFrontend(front.clone()),
104 _ => return None,
105 };
106 Some(inverse.into())
107}
108
109const UDP_UNIX_BACKEND_REFUSED: &str = "a UDP cluster cannot use unix_path backends";
111
112#[derive(thiserror::Error)]
113pub enum StateError {
114 #[error("Request came in empty")]
115 EmptyRequest,
116 #[error("dispatching this request did not bring any change to the state")]
117 NoChange,
118 #[error("State can not handle this request")]
119 UndispatchableRequest,
120 #[error("Did not find {kind:?} with address or id_bytes={}", .id.len())]
121 NotFound { kind: ObjectKind, id: String },
122 #[error(
123 "{kind:?} with id_bytes={} already exists; remove it first, or apply the corresponding update if the object supports one, instead of re-adding it",
124 .id.len()
125 )]
126 Exists { kind: ObjectKind, id: String },
127 #[error(
134 "{kind:?} names a cluster that does not exist (cluster_id_bytes={}); add the cluster first",
135 .cluster_id.len()
136 )]
137 ClusterNotFound {
138 kind: ObjectKind,
139 cluster_id: String,
140 },
141 #[error("Wrong field value: {0}")]
142 WrongFieldValue(UnknownEnumValue),
143 #[error("Could not add certificate: error_kind={}", certificate_error_kind(.0))]
144 AddCertificate(CertificateError),
145 #[error("Could not remove certificate: fingerprint_bytes={}", .0.len())]
146 RemoveCertificate(String),
147 #[error("Could not replace certificate: error_bytes={}", .0.len())]
148 ReplaceCertificate(String),
149 #[error("Could not convert frontend: frontend_bytes={} error_bytes={}", .frontend.len(), .error.len())]
150 FrontendConversion { frontend: String, error: String },
151 #[error("Could not write state to file: io_kind={:?} os_code={:?}", .0.kind(), .0.raw_os_error())]
152 FileError(std::io::Error),
153 #[error("Invalid value for field '{field}': {reason}")]
154 InvalidValue {
155 field: &'static str,
156 reason: &'static str,
157 },
158 #[error("invalid TCP frontend for address {address}: reason_bytes={}", .reason.len())]
164 InvalidTcpFrontend { address: SocketAddr, reason: String },
165 #[error(
183 "UDP frontend address {address} is already claimed by another frontend: \
184 cluster_id_bytes={} current_cluster_id_bytes={} current_tag_count={}",
185 .cluster_id.len(),
186 .current_cluster_id.len(),
187 .current_tags.len()
188 )]
189 UdpFrontendAddressTaken {
190 address: SocketAddr,
191 cluster_id: ClusterId,
192 current_cluster_id: ClusterId,
193 current_tags: BTreeMap<String, String>,
194 },
195 #[error("invalid listener interface: {0}")]
198 ListenerInterface(InterfaceError),
199 #[error(
202 "{count} listeners share the address {address} on different interfaces; an update \
203 patch cannot tell them apart: remove and re-add the one to change"
204 )]
205 AmbiguousListenerAddress { address: SocketAddr, count: usize },
206}
207
208fn validated_listener_key(key: ListenerKey) -> Result<ListenerKey, StateError> {
211 if let Some(interface) = &key.interface {
212 validate_listener_interface(interface).map_err(StateError::ListenerInterface)?;
213 }
214 Ok(key)
215}
216
217fn listener_key_for_update<V>(
221 listeners: &BTreeMap<ListenerKey, V>,
222 address: SocketAddr,
223 kind: ObjectKind,
224) -> Result<ListenerKey, StateError> {
225 let mut matching = listeners.keys().filter(|key| key.address == address);
226 let Some(first) = matching.next() else {
227 return Err(StateError::NotFound {
228 kind,
229 id: address.to_string(),
230 });
231 };
232 let others = matching.count();
233 if others > 0 {
234 return Err(StateError::AmbiguousListenerAddress {
235 address,
236 count: others + 1,
237 });
238 }
239 Ok(first.clone())
240}
241
242impl fmt::Debug for StateError {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 write!(f, "StateError({self})")
245 }
246}
247
248fn certificate_error_kind(error: &CertificateError) -> &'static str {
249 match error {
250 CertificateError::ParsePEMCertificate(_) => "parse_pem_certificate",
251 CertificateError::ParseX509Certificate(_) => "parse_x509_certificate",
252 CertificateError::InvalidTlsVersion(_) => "invalid_tls_version",
253 CertificateError::InvalidFingerprint(_) => "invalid_fingerprint",
254 CertificateError::LoadFile { .. } => "load_file",
255 CertificateError::DecodeError(_) => "decode_error",
256 }
257}
258
259const TCP_CLUSTER_AFFINITY_KEY: &str = "a TCP frontend always keys on the client source IP: \
262 affinity_header and affinity_cookie apply to HTTP/HTTPS clusters only";
263
264#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
277pub struct CertificateMetadata {
278 pub expired_at: Option<i64>,
279 pub insertion_order: u64,
280}
281
282#[derive(Clone, Debug, Default, Serialize, Deserialize)]
289#[serde(transparent)]
290pub struct CertificateMetadataIndex {
291 entries: HashMap<Fingerprint, CertificateMetadata>,
292 #[serde(skip)]
293 next_order: Option<u64>,
294}
295
296impl PartialEq for CertificateMetadataIndex {
297 fn eq(&self, other: &Self) -> bool {
298 self.entries == other.entries
299 }
300}
301
302impl Eq for CertificateMetadataIndex {}
303
304#[derive(Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
313pub struct ConfigState {
314 pub clusters: BTreeMap<ClusterId, Cluster>,
315 pub backends: BTreeMap<ClusterId, Vec<Backend>>,
316 pub http_listeners: BTreeMap<ListenerKey, HttpListenerConfig>,
318 pub https_listeners: BTreeMap<ListenerKey, HttpsListenerConfig>,
320 pub tcp_listeners: BTreeMap<ListenerKey, TcpListenerConfig>,
322 pub udp_listeners: BTreeMap<ListenerKey, UdpListenerConfig>,
324 pub http_fronts: BTreeMap<String, HttpFrontend>,
327 pub https_fronts: BTreeMap<String, HttpFrontend>,
329 pub tcp_fronts: HashMap<ClusterId, Vec<TcpFrontend>>,
330 pub udp_fronts: HashMap<ClusterId, Vec<UdpFrontend>>,
331 pub certificates: HashMap<SocketAddr, HashMap<Fingerprint, CertificateAndKey>>,
341 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
347 pub certificate_metadata: HashMap<SocketAddr, CertificateMetadataIndex>,
348 pub request_counts: BTreeMap<String, i32>,
350}
351
352impl std::fmt::Debug for ConfigState {
353 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
354 let backends_count = self
355 .backends
356 .values()
357 .map(Vec::len)
358 .fold(0usize, usize::saturating_add);
359 let tcp_frontends_count = self
360 .tcp_fronts
361 .values()
362 .map(Vec::len)
363 .fold(0usize, usize::saturating_add);
364 let udp_frontends_count = self
365 .udp_fronts
366 .values()
367 .map(Vec::len)
368 .fold(0usize, usize::saturating_add);
369 let certificates_count = self
370 .certificates
371 .values()
372 .map(HashMap::len)
373 .fold(0usize, usize::saturating_add);
374
375 f.debug_struct("ConfigState")
376 .field("clusters_count", &self.clusters.len())
377 .field("backend_clusters_count", &self.backends.len())
378 .field("backends_count", &backends_count)
379 .field("http_listeners_count", &self.http_listeners.len())
380 .field("https_listeners_count", &self.https_listeners.len())
381 .field("tcp_listeners_count", &self.tcp_listeners.len())
382 .field("udp_listeners_count", &self.udp_listeners.len())
383 .field("http_frontends_count", &self.http_fronts.len())
384 .field("https_frontends_count", &self.https_fronts.len())
385 .field("tcp_frontend_clusters_count", &self.tcp_fronts.len())
386 .field("tcp_frontends_count", &tcp_frontends_count)
387 .field("udp_frontend_clusters_count", &self.udp_fronts.len())
388 .field("udp_frontends_count", &udp_frontends_count)
389 .field("certificate_addresses_count", &self.certificates.len())
390 .field("certificates_count", &certificates_count)
391 .field(
392 "certificate_metadata_addresses_count",
393 &self.certificate_metadata.len(),
394 )
395 .field("request_counts_count", &self.request_counts.len())
396 .finish_non_exhaustive()
397 }
398}
399
400impl ConfigState {
401 pub fn new() -> Self {
402 Self::default()
403 }
404
405 pub fn ordered_certificate_entries(
413 &self,
414 address: SocketAddr,
415 ) -> Vec<(Fingerprint, &CertificateAndKey, CertificateMetadata)> {
416 let Some(certificates) = self.certificates.get(&address) else {
417 return Vec::new();
418 };
419 let metadata = self.certificate_metadata.get(&address);
420 let mut fingerprints: Vec<Fingerprint> =
421 certificates.keys().map(ToOwned::to_owned).collect();
422 fingerprints.sort_by(|left, right| {
423 match (
424 metadata.and_then(|index| index.entries.get(left)),
425 metadata.and_then(|index| index.entries.get(right)),
426 ) {
427 (None, None) => left.cmp(right),
428 (None, Some(_)) => std::cmp::Ordering::Less,
429 (Some(_), None) => std::cmp::Ordering::Greater,
430 (Some(left_metadata), Some(right_metadata)) => left_metadata
431 .insertion_order
432 .cmp(&right_metadata.insertion_order)
433 .then_with(|| left.cmp(right)),
434 }
435 });
436
437 fingerprints
438 .into_iter()
439 .enumerate()
440 .filter_map(|(insertion_order, fingerprint)| {
441 let certificate = certificates.get(&fingerprint)?;
442 let expired_at = metadata
443 .and_then(|index| index.entries.get(&fingerprint))
444 .and_then(|entry| entry.expired_at);
445 Some((
446 fingerprint,
447 certificate,
448 CertificateMetadata {
449 expired_at,
450 insertion_order: insertion_order as u64,
451 },
452 ))
453 })
454 .collect()
455 }
456
457 pub fn certificate_add_requests_for_address(&self, address: SocketAddr) -> Vec<AddCertificate> {
460 self.ordered_certificate_entries(address)
461 .into_iter()
462 .map(|(_, certificate, metadata)| AddCertificate {
463 address: SocketAddress::from(address),
464 certificate: certificate.to_owned(),
465 expired_at: metadata.expired_at,
466 })
467 .collect()
468 }
469
470 fn normalized_certificate_metadata_for_address(
471 &self,
472 address: SocketAddr,
473 ) -> CertificateMetadataIndex {
474 let entries: HashMap<Fingerprint, CertificateMetadata> = self
475 .ordered_certificate_entries(address)
476 .into_iter()
477 .map(|(fingerprint, _, metadata)| (fingerprint, metadata))
478 .collect();
479 CertificateMetadataIndex {
480 next_order: Some(entries.len() as u64),
481 entries,
482 }
483 }
484
485 fn certificate_metadata_is_trusted(&self, address: SocketAddr) -> bool {
486 let certificate_count = self.certificates.get(&address).map_or(0, HashMap::len);
487 self.certificate_metadata
488 .get(&address)
489 .is_some_and(|metadata| {
490 metadata.next_order == Some(certificate_count as u64)
491 && metadata.entries.len() == certificate_count
492 })
493 }
494
495 fn take_certificate_metadata_for_state_change(
501 &mut self,
502 address: SocketAddr,
503 ) -> CertificateMetadataIndex {
504 if self.certificate_metadata_is_trusted(address) {
505 self.certificate_metadata
506 .remove(&address)
507 .expect("trusted metadata index must remain present")
508 } else {
509 self.normalized_certificate_metadata_for_address(address)
510 }
511 }
512
513 #[cfg(any(debug_assertions, test))]
514 fn normalized_certificate_metadata(&self) -> HashMap<SocketAddr, CertificateMetadataIndex> {
515 self.certificates
516 .keys()
517 .filter_map(|address| {
518 let metadata = self.normalized_certificate_metadata_for_address(*address);
519 (!metadata.entries.is_empty()).then_some((*address, metadata))
520 })
521 .collect()
522 }
523
524 fn remove_certificate_metadata(
525 metadata: &mut CertificateMetadataIndex,
526 fingerprint: &Fingerprint,
527 ) -> Option<CertificateMetadata> {
528 let removed = metadata.entries.remove(fingerprint)?;
529 for entry in metadata.entries.values_mut() {
530 if entry.insertion_order > removed.insertion_order {
531 entry.insertion_order -= 1;
532 }
533 }
534 metadata.next_order = Some(metadata.entries.len() as u64);
535 Some(removed)
536 }
537
538 fn certificate_add_requests(&self) -> Vec<AddCertificate> {
539 let mut addresses: Vec<SocketAddr> = self.certificates.keys().copied().collect();
540 addresses.sort_unstable();
541 addresses
542 .into_iter()
543 .flat_map(|address| self.certificate_add_requests_for_address(address))
544 .collect()
545 }
546
547 pub fn rollback_for(&self, request: &RequestType) -> Option<Request> {
557 if let Some(rollback) = rollback_request(request) {
558 return Some(rollback);
559 }
560
561 let inverse = match request {
562 RequestType::RemoveHttpFrontend(selector)
563 | RequestType::RemoveHttpFrontendWithHeaderPredicates(selector) => self
564 .http_fronts
565 .get(&selector.to_string())
566 .filter(|stored| stored.cluster_id.as_deref() == selector.cluster_id.as_deref())
567 .cloned()
568 .map(RequestHttpFrontend::from)
569 .map(RequestType::add_http_frontend),
570 RequestType::RemoveHttpsFrontend(selector)
571 | RequestType::RemoveHttpsFrontendWithHeaderPredicates(selector) => self
572 .https_fronts
573 .get(&selector.to_string())
574 .filter(|stored| stored.cluster_id.as_deref() == selector.cluster_id.as_deref())
575 .cloned()
576 .map(RequestHttpFrontend::from)
577 .map(RequestType::add_https_frontend),
578 RequestType::RemoveTcpFrontend(selector) => {
579 let address: SocketAddr = selector.address.into();
580 let sni = selector.sni.as_deref().map(str::to_ascii_lowercase);
581 let alpn = canonical_tcp_alpn(&selector.alpn);
582 self.tcp_fronts
583 .get(selector.cluster_id.as_str())
584 .and_then(|frontends| {
585 frontends
586 .iter()
587 .find(|stored| tcp_frontend_matches(stored, address, &sni, &alpn))
588 })
589 .cloned()
590 .map(RequestTcpFrontend::from)
591 .map(RequestType::AddTcpFrontend)
592 }
593 RequestType::RemoveUdpFrontend(selector) => {
594 let address: SocketAddr = selector.address.into();
595 self.udp_fronts
596 .get(selector.cluster_id.as_str())
597 .and_then(|frontends| {
598 frontends.iter().find(|stored| {
599 stored.address == address && stored.tags == selector.tags
600 })
601 })
602 .cloned()
603 .map(RequestUdpFrontend::from)
604 .map(RequestType::AddUdpFrontend)
605 }
606 RequestType::ActivateListener(activate) => {
607 let key = activate.listener_key();
608 let was_inactive = match ListenerType::try_from(activate.proxy).ok()? {
609 ListenerType::Http => self
610 .http_listeners
611 .get(&key)
612 .map(|listener| !listener.active),
613 ListenerType::Https => self
614 .https_listeners
615 .get(&key)
616 .map(|listener| !listener.active),
617 ListenerType::Tcp => self
618 .tcp_listeners
619 .get(&key)
620 .map(|listener| !listener.active),
621 ListenerType::Udp => self
622 .udp_listeners
623 .get(&key)
624 .map(|listener| !listener.active),
625 }?;
626
627 was_inactive.then(|| {
628 RequestType::DeactivateListener(DeactivateListener {
629 address: activate.address,
630 proxy: activate.proxy,
631 to_scm: false,
632 interface: activate.interface.to_owned(),
633 })
634 })
635 }
636 _ => None,
637 };
638
639 inverse.map(Into::into)
640 }
641
642 pub fn dispatch(&mut self, request: &Request) -> Result<(), StateError> {
643 let request_type = match &request.request_type {
644 Some(t) => t,
645 None => return Err(StateError::EmptyRequest),
646 };
647
648 self.increment_request_count(request);
649
650 if let Some(reason) = self.health_check_port_unix_conflict(request_type) {
651 return Err(StateError::InvalidValue {
652 field: "health_check.port",
653 reason,
654 });
655 }
656
657 let result = match request_type {
658 RequestType::AddCluster(cluster) => self.add_cluster(cluster),
659 RequestType::RemoveCluster(cluster_id) => self.remove_cluster(cluster_id),
660 RequestType::AddHttpListener(listener) => self.add_http_listener(listener),
661 RequestType::AddHttpsListener(listener)
662 | RequestType::AddHttpsListenerWithClientAuth(listener) => {
663 self.add_https_listener(listener)
664 }
665 RequestType::AddTcpListener(listener) => self.add_tcp_listener(listener),
666 RequestType::AddUdpListener(listener) => self.add_udp_listener(listener),
667 RequestType::RemoveListener(remove) => self.remove_listener(remove),
668 RequestType::ActivateListener(activate) => self.activate_listener(activate),
669 RequestType::DeactivateListener(deactivate) => self.deactivate_listener(deactivate),
670 RequestType::AddHttpFrontend(front)
671 | RequestType::AddHttpFrontendWithSilentClose(front)
672 | RequestType::AddHttpFrontendWithHeaderPredicates(front) => {
673 self.add_http_frontend(front)
674 }
675 RequestType::RemoveHttpFrontend(front)
676 | RequestType::RemoveHttpFrontendWithHeaderPredicates(front) => {
677 self.remove_http_frontend(front)
678 }
679 RequestType::AddCertificate(add) => self.add_certificate(add),
680 RequestType::RemoveCertificate(remove) => self.remove_certificate(remove),
681 RequestType::ReplaceCertificate(replace) => self.replace_certificate(replace),
682 RequestType::AddHttpsFrontend(front)
683 | RequestType::AddHttpsFrontendWithSilentClose(front)
684 | RequestType::AddHttpsFrontendWithHeaderPredicates(front) => {
685 self.add_https_frontend(front)
686 }
687 RequestType::RemoveHttpsFrontend(front)
688 | RequestType::RemoveHttpsFrontendWithHeaderPredicates(front) => {
689 self.remove_https_frontend(front)
690 }
691 RequestType::AddTcpFrontend(front) => self.add_tcp_frontend(front),
692 RequestType::RemoveTcpFrontend(front) => self.remove_tcp_frontend(front),
693 RequestType::AddUdpFrontend(front) => self.add_udp_frontend(front),
694 RequestType::RemoveUdpFrontend(front) => self.remove_udp_frontend(front),
695 RequestType::AddBackend(add_backend) | RequestType::AddUnixBackend(add_backend) => {
696 self.add_backend(add_backend)
697 }
698 RequestType::RemoveBackend(backend) => self.remove_backend(backend),
699 RequestType::UpdateHttpListener(patch) => self.update_http_listener(patch),
700 RequestType::UpdateHttpsListener(patch)
701 | RequestType::UpdateHttpsListenerWithClientAuth(patch) => {
702 self.update_https_listener(patch)
703 }
704 RequestType::UpdateTcpListener(patch) => self.update_tcp_listener(patch),
705 RequestType::UpdateUdpListener(patch) => self.update_udp_listener(patch),
706 RequestType::SetHealthCheck(set) => {
707 self.set_health_check(&set.cluster_id, &set.config)
708 }
709 RequestType::SetProtectedHealthCheck(set) => self.set_protected_health_check(set),
710 RequestType::RemoveHealthCheck(cluster_id) => self.remove_health_check(cluster_id),
711
712 RequestType::Logging(_)
719 | RequestType::CountRequests(_)
720 | RequestType::Status(_)
721 | RequestType::SoftStop(_)
722 | RequestType::QueryCertificatesFromWorkers(_)
723 | RequestType::QueryClusterById(_)
724 | RequestType::QueryClustersByDomain(_)
725 | RequestType::QueryMetrics(_)
726 | RequestType::QueryClustersHashes(_)
727 | RequestType::ConfigureMetrics(_)
728 | RequestType::SetMetricDetail(_)
729 | RequestType::ReturnListenSockets(_)
730 | RequestType::SetMaxConnectionsPerIp(_)
731 | RequestType::QueryMaxConnectionsPerIp(_)
732 | RequestType::SetMaxConnectionsPerSubnet(_)
733 | RequestType::QueryMaxConnectionsPerSubnet(_)
734 | RequestType::SetMaxConnectionAttempts(_)
735 | RequestType::CloseSessions(_)
737 | RequestType::HardStop(_) => Ok(()),
738
739 _other_request => Err(StateError::UndispatchableRequest),
740 };
741
742 #[cfg(debug_assertions)]
748 self.check_invariants();
749
750 result
751 }
752
753 #[cfg(debug_assertions)]
764 fn check_invariants(&self) {
765 for (addr, listener) in &self.http_listeners {
769 debug_assert_eq!(
770 listener.listener_key(),
771 *addr,
772 "http_listener value address must match its map key"
773 );
774 }
775 for (addr, listener) in &self.https_listeners {
776 debug_assert_eq!(
777 listener.listener_key(),
778 *addr,
779 "https_listener value address must match its map key"
780 );
781 }
782 for (addr, listener) in &self.tcp_listeners {
783 debug_assert_eq!(
784 listener.listener_key(),
785 *addr,
786 "tcp_listener value address must match its map key"
787 );
788 }
789
790 for (cluster_id, cluster) in &self.clusters {
793 debug_assert_eq!(
794 cluster.cluster_id.as_str(),
795 &**cluster_id,
796 "cluster value cluster_id must match its map key"
797 );
798 }
799
800 for (cluster_id, backends) in &self.backends {
805 debug_assert!(
808 backends.is_empty() || self.clusters.contains_key(cluster_id),
809 "a backend bucket must belong to an existing cluster"
810 );
811 for backend in backends {
812 debug_assert_eq!(
813 &backend.cluster_id, cluster_id,
814 "backend cluster_id must match its bucket key"
815 );
816 }
817 let unique: HashSet<(&String, &SocketAddr)> = backends
818 .iter()
819 .map(|b| (&b.backend_id, &b.address))
820 .collect();
821 debug_assert_eq!(
822 unique.len(),
823 backends.len(),
824 "backends within a cluster must be unique on (backend_id, address)"
825 );
826 }
827
828 for (cluster_id, fronts) in &self.tcp_fronts {
832 for front in fronts {
833 debug_assert_eq!(
834 &front.cluster_id, cluster_id,
835 "tcp_frontend cluster_id must match its bucket key"
836 );
837 }
838 let unique: HashSet<&TcpFrontend> = fronts.iter().collect();
839 debug_assert_eq!(
840 unique.len(),
841 fronts.len(),
842 "tcp frontends within a cluster must be unique"
843 );
844 }
845
846 for (address, certs) in &self.certificates {
868 debug_assert!(
869 !certs.is_empty(),
870 "certificate bucket for address {address} must never be empty (sozu-proxy/sozu#1404)"
871 );
872 }
873 for (address, metadata) in &self.certificate_metadata {
879 let Some(next_order) = metadata.next_order else {
880 continue;
881 };
882 let certificates = self
883 .certificates
884 .get(address)
885 .expect("trusted certificate metadata must have a certificate bucket");
886 debug_assert_eq!(
887 metadata.entries.len(),
888 certificates.len(),
889 "trusted certificate metadata must cover every stored fingerprint"
890 );
891 debug_assert_eq!(
892 next_order,
893 metadata.entries.len() as u64,
894 "trusted certificate metadata next ordinal must equal its length"
895 );
896 let ordinals: BTreeSet<u64> = metadata
897 .entries
898 .iter()
899 .map(|(fingerprint, entry)| {
900 debug_assert!(
901 certificates.contains_key(fingerprint),
902 "trusted certificate metadata cannot retain an orphan fingerprint"
903 );
904 entry.insertion_order
905 })
906 .collect();
907 debug_assert_eq!(
908 ordinals,
909 (0..next_order).collect(),
910 "trusted certificate ordinals must be unique and contiguous"
911 );
912 }
913
914 let raw_frontends = self.http_fronts.len()
917 + self.https_fronts.len()
918 + self.count_tcp_frontends_raw()
919 + self.count_udp_frontends_raw();
920 debug_assert_eq!(
921 self.count_frontends(),
922 raw_frontends,
923 "count_frontends must equal the sum of all frontend map entries"
924 );
925 let raw_backends: usize = self.backends.values().map(|v| v.len()).sum();
926 debug_assert_eq!(
927 self.count_backends(),
928 raw_backends,
929 "count_backends must equal the sum of all backend Vec lengths"
930 );
931 }
932
933 fn count_tcp_frontends_raw(&self) -> usize {
941 self.tcp_fronts.values().map(|v| v.len()).sum()
942 }
943
944 fn count_udp_frontends_raw(&self) -> usize {
945 self.udp_fronts.values().map(|v| v.len()).sum()
946 }
947
948 fn count_objects_where(&self, owned: impl Fn(&str) -> bool) -> usize {
953 let keyed = |cluster_id: &ClusterId, len: usize| if owned(cluster_id) { len } else { 0 };
954 let routed = |front: &HttpFrontend| front.cluster_id.as_deref().is_some_and(&owned);
955 self.backends
956 .iter()
957 .map(|(k, v)| keyed(k, v.len()))
958 .sum::<usize>()
959 + self
960 .tcp_fronts
961 .iter()
962 .map(|(k, v)| keyed(k, v.len()))
963 .sum::<usize>()
964 + self
965 .udp_fronts
966 .iter()
967 .map(|(k, v)| keyed(k, v.len()))
968 .sum::<usize>()
969 + self.http_fronts.values().filter(|f| routed(f)).count()
970 + self.https_fronts.values().filter(|f| routed(f)).count()
971 }
972
973 fn count_objects_owned_by(&self, cluster_id: &str) -> usize {
974 self.count_objects_where(|owner| owner == cluster_id)
975 }
976
977 #[cfg(debug_assertions)]
980 fn count_objects_not_owned_by(&self, cluster_id: &str) -> usize {
981 self.count_objects_where(|owner| owner != cluster_id)
982 }
983
984 fn increment_request_count(&mut self, request: &Request) {
986 if let Some(request_type) = &request.request_type {
987 let count = self
988 .request_counts
989 .entry(format_request_type(request_type).to_owned())
990 .or_insert(1);
991 *count += 1;
992 }
993 }
994
995 pub fn get_request_counts(&self) -> RequestCounts {
996 RequestCounts {
997 map: self.request_counts.clone(),
998 }
999 }
1000
1001 fn add_cluster(&mut self, cluster: &Cluster) -> Result<(), StateError> {
1002 if let Some(hc) = cluster.health_check.as_ref()
1009 && let Err(reason) = crate::config::validate_health_check_config(hc)
1010 {
1011 return Err(StateError::InvalidValue {
1012 field: "health_check",
1013 reason,
1014 });
1015 }
1016 if cluster.udp.is_some()
1019 && self
1020 .backends
1021 .get(cluster.cluster_id.as_str())
1022 .is_some_and(|backends| backends.iter().any(|b| b.unix_path.is_some()))
1023 {
1024 return Err(StateError::InvalidValue {
1025 field: "unix_path",
1026 reason: UDP_UNIX_BACKEND_REFUSED,
1027 });
1028 }
1029 if let Err(reason) = crate::config::validate_affinity_key(
1033 cluster.affinity_header.as_deref(),
1034 cluster.affinity_cookie.as_deref(),
1035 ) {
1036 return Err(StateError::InvalidValue {
1037 field: "affinity_key",
1038 reason,
1039 });
1040 }
1041 if let Err(reason) = crate::config::validate_shuffle_sharding(
1042 cluster.shard_percent,
1043 cluster.shard_min_backends,
1044 cluster.shard_mode,
1045 ) {
1046 return Err(StateError::InvalidValue {
1047 field: "shuffle_sharding",
1048 reason,
1049 });
1050 }
1051 if let Some(Err(reason)) = cluster
1052 .max_connection_attempts
1053 .map(crate::config::validate_max_connection_attempts)
1054 {
1055 return Err(StateError::InvalidValue {
1056 field: "max_connection_attempts",
1057 reason,
1058 });
1059 }
1060 if (cluster.affinity_header.is_some() || cluster.affinity_cookie.is_some())
1065 && self
1066 .tcp_fronts
1067 .get(cluster.cluster_id.as_str())
1068 .is_some_and(|fronts| !fronts.is_empty())
1069 {
1070 return Err(StateError::InvalidValue {
1071 field: "affinity_key",
1072 reason: TCP_CLUSTER_AFFINITY_KEY,
1073 });
1074 }
1075 let cluster = cluster.clone();
1076 let cluster_id = ClusterId::from(cluster.cluster_id.as_str());
1080 self.clusters.insert(cluster_id.clone(), cluster);
1081 debug_assert!(
1082 self.clusters.contains_key(&cluster_id),
1083 "add_cluster must leave the cluster present in the map"
1084 );
1085 debug_assert_eq!(
1086 self.clusters
1087 .get(&cluster_id)
1088 .map(|c| c.cluster_id.as_str()),
1089 Some(&*cluster_id),
1090 "stored cluster must be keyed by its own cluster_id"
1091 );
1092 Ok(())
1093 }
1094
1095 fn remove_cluster(&mut self, cluster_id: &str) -> Result<(), StateError> {
1101 let before = self.clusters.len();
1102 match self.clusters.remove(cluster_id) {
1103 Some(_) => {
1104 #[cfg(debug_assertions)]
1105 let owned_by_others = self.count_objects_not_owned_by(cluster_id);
1106
1107 self.backends.remove(cluster_id);
1108 self.tcp_fronts.remove(cluster_id);
1109 self.udp_fronts.remove(cluster_id);
1110 self.http_fronts
1111 .retain(|_, front| front.cluster_id.as_deref() != Some(cluster_id));
1112 self.https_fronts
1113 .retain(|_, front| front.cluster_id.as_deref() != Some(cluster_id));
1114
1115 debug_assert!(
1116 !self.clusters.contains_key(cluster_id),
1117 "remove_cluster must evict the cluster"
1118 );
1119 debug_assert_eq!(
1120 self.clusters.len(),
1121 before - 1,
1122 "remove_cluster must drop exactly one entry"
1123 );
1124 debug_assert_eq!(
1125 self.count_objects_owned_by(cluster_id),
1126 0,
1127 "remove_cluster must leave no frontend or backend naming the cluster"
1128 );
1129 #[cfg(debug_assertions)]
1130 debug_assert_eq!(
1131 self.count_objects_not_owned_by(cluster_id),
1132 owned_by_others,
1133 "remove_cluster must not touch a frontend or backend of another cluster"
1134 );
1135 Ok(())
1136 }
1137 None => {
1138 debug_assert_eq!(
1139 self.clusters.len(),
1140 before,
1141 "a failed remove_cluster must not mutate the map"
1142 );
1143 Err(StateError::NotFound {
1144 kind: ObjectKind::Cluster,
1145 id: cluster_id.to_owned(),
1146 })
1147 }
1148 }
1149 }
1150
1151 pub fn cluster_removal_requests(&self, cluster_id: &str) -> Vec<Request> {
1157 if !self.clusters.contains_key(cluster_id) {
1158 return Vec::new();
1159 }
1160 let owned = |front: &&HttpFrontend| front.cluster_id.as_deref() == Some(cluster_id);
1161 let mut requests: Vec<Request> = Vec::new();
1162 requests.extend(
1163 self.http_fronts
1164 .values()
1165 .filter(owned)
1166 .map(|front| RequestType::remove_http_frontend(front.clone().into()).into()),
1167 );
1168 requests.extend(
1169 self.https_fronts
1170 .values()
1171 .filter(owned)
1172 .map(|front| RequestType::remove_https_frontend(front.clone().into()).into()),
1173 );
1174 requests.extend(
1175 self.tcp_fronts
1176 .get(cluster_id)
1177 .into_iter()
1178 .flatten()
1179 .map(|front| RequestType::RemoveTcpFrontend(front.clone().into()).into()),
1180 );
1181 requests.extend(
1182 self.udp_fronts
1183 .get(cluster_id)
1184 .into_iter()
1185 .flatten()
1186 .map(|front| RequestType::RemoveUdpFrontend(front.clone().into()).into()),
1187 );
1188 requests.extend(
1189 self.backends
1190 .get(cluster_id)
1191 .into_iter()
1192 .flatten()
1193 .map(|backend| {
1194 RequestType::RemoveBackend(RemoveBackend {
1195 cluster_id: backend.cluster_id.to_string(),
1196 backend_id: backend.backend_id.clone(),
1197 address: SocketAddress::from(backend.address),
1198 })
1199 .into()
1200 }),
1201 );
1202 debug_assert_eq!(
1203 requests.len(),
1204 self.count_objects_owned_by(cluster_id),
1205 "one removal order per frontend or backend the cluster owns"
1206 );
1207 requests
1208 }
1209
1210 pub fn removed_with_its_cluster(&self, request: &Request, error: &StateError) -> bool {
1223 if !matches!(error, StateError::NotFound { .. }) {
1224 return false;
1225 }
1226 let cluster_id = match &request.request_type {
1227 Some(
1228 RequestType::RemoveHttpFrontend(front)
1229 | RequestType::RemoveHttpFrontendWithHeaderPredicates(front)
1230 | RequestType::RemoveHttpsFrontend(front)
1231 | RequestType::RemoveHttpsFrontendWithHeaderPredicates(front),
1232 ) => front.cluster_id.as_deref(),
1233 Some(RequestType::RemoveTcpFrontend(front)) => Some(front.cluster_id.as_str()),
1234 Some(RequestType::RemoveUdpFrontend(front)) => Some(front.cluster_id.as_str()),
1235 Some(RequestType::RemoveBackend(backend)) => Some(backend.cluster_id.as_str()),
1236 _ => None,
1237 };
1238 cluster_id.is_some_and(|cluster_id| !self.clusters.contains_key(cluster_id))
1239 }
1240
1241 pub fn removed_with_its_cluster_message(&self, request: &Request) -> String {
1247 let (cluster_id, owner) = match &request.request_type {
1248 Some(
1249 RequestType::RemoveHttpFrontend(front)
1250 | RequestType::RemoveHttpFrontendWithHeaderPredicates(front),
1251 ) => (
1252 front.cluster_id.clone(),
1253 self.http_fronts
1254 .get(&front.to_string())
1255 .map(|stored| stored.cluster_id.as_deref().map(str::to_owned)),
1256 ),
1257 Some(
1258 RequestType::RemoveHttpsFrontend(front)
1259 | RequestType::RemoveHttpsFrontendWithHeaderPredicates(front),
1260 ) => (
1261 front.cluster_id.clone(),
1262 self.https_fronts
1263 .get(&front.to_string())
1264 .map(|stored| stored.cluster_id.as_deref().map(str::to_owned)),
1265 ),
1266 Some(RequestType::RemoveTcpFrontend(front)) => {
1267 let address: SocketAddr = front.address.into();
1268 let sni = front.sni.as_deref().map(str::to_ascii_lowercase);
1269 let alpn = canonical_tcp_alpn(&front.alpn);
1270 (
1271 Some(front.cluster_id.clone()),
1272 self.tcp_fronts
1273 .values()
1274 .flatten()
1275 .find(|stored| tcp_frontend_matches(stored, address, &sni, &alpn))
1276 .map(|stored| Some(stored.cluster_id.to_string())),
1277 )
1278 }
1279 Some(RequestType::RemoveUdpFrontend(front)) => {
1280 let address: SocketAddr = front.address.into();
1281 (
1282 Some(front.cluster_id.clone()),
1283 self.udp_fronts
1284 .values()
1285 .flatten()
1286 .find(|stored| stored.address == address)
1287 .map(|stored| Some(stored.cluster_id.to_string())),
1288 )
1289 }
1290 Some(RequestType::RemoveBackend(backend)) => (Some(backend.cluster_id.clone()), None),
1291 _ => (None, None),
1292 };
1293 let mut message = format!(
1294 "nothing to remove: cluster {} does not exist (removed, or never added), \
1295 and none of its frontends or backends either",
1296 cluster_id.as_deref().unwrap_or("<none>")
1297 );
1298 match owner {
1299 Some(Some(owner)) => message.push_str(&format!(
1300 "; this key belongs to cluster {owner}, left in place"
1301 )),
1302 Some(None) => {
1303 message.push_str("; this key belongs to a route with no cluster, left in place")
1304 }
1305 None => {}
1306 }
1307 message
1308 }
1309
1310 fn set_protected_health_check(
1315 &mut self,
1316 set: &SetProtectedHealthCheck,
1317 ) -> Result<(), StateError> {
1318 let invalid = |reason| StateError::InvalidValue {
1319 field: "health_check",
1320 reason,
1321 };
1322 if set.config.headers.iter().any(|h| h.fingerprint.is_empty()) {
1323 return Err(invalid("health check header fingerprint is missing"));
1324 }
1325 crate::config::validate_health_check_header_values(&set.config, &set.values)
1326 .map_err(invalid)?;
1327 self.set_health_check(&set.cluster_id, &set.config)
1328 }
1329
1330 fn set_health_check(
1331 &mut self,
1332 cluster_id: &str,
1333 config: &HealthCheckConfig,
1334 ) -> Result<(), StateError> {
1335 if let Err(reason) = crate::config::validate_health_check_config(config) {
1342 return Err(StateError::InvalidValue {
1343 field: "health_check",
1344 reason,
1345 });
1346 }
1347 match self.clusters.get_mut(cluster_id) {
1348 Some(cluster) => {
1349 cluster.health_check = Some(config.to_owned());
1350 Ok(())
1351 }
1352 None => Err(StateError::NotFound {
1353 kind: ObjectKind::Cluster,
1354 id: cluster_id.to_owned(),
1355 }),
1356 }
1357 }
1358
1359 fn remove_health_check(&mut self, cluster_id: &str) -> Result<(), StateError> {
1360 match self.clusters.get_mut(cluster_id) {
1361 Some(cluster) => {
1362 cluster.health_check = None;
1363 Ok(())
1364 }
1365 None => Err(StateError::NotFound {
1366 kind: ObjectKind::Cluster,
1367 id: cluster_id.to_owned(),
1368 }),
1369 }
1370 }
1371
1372 pub fn list_health_checks(&self, cluster_id: Option<&str>) -> HealthChecksList {
1373 let map = self
1374 .clusters
1375 .iter()
1376 .filter(|(id, _)| cluster_id.is_none_or(|filter| filter == &id[..]))
1377 .filter_map(|(id, cluster)| {
1378 cluster
1379 .health_check
1380 .as_ref()
1381 .map(|hc| (id.to_string(), hc.to_owned()))
1382 })
1383 .collect();
1384 HealthChecksList { map }
1385 }
1386
1387 fn add_http_listener(&mut self, listener: &HttpListenerConfig) -> Result<(), StateError> {
1388 if let Some(v) = listener.forwarded_headers {
1391 validate_forwarded_headers(v)?;
1392 }
1393 let address = validated_listener_key(listener.listener_key())?;
1394 let before = self.http_listeners.len();
1395 match self.http_listeners.entry(address.clone()) {
1396 BTreeMapEntry::Vacant(vacant_entry) => vacant_entry.insert(listener.clone()),
1397 BTreeMapEntry::Occupied(_) => {
1398 debug_assert_eq!(
1399 self.http_listeners.len(),
1400 before,
1401 "a rejected duplicate add_http_listener must not mutate the map"
1402 );
1403 return Err(StateError::Exists {
1404 kind: ObjectKind::HttpListener,
1405 id: address.to_string(),
1406 });
1407 }
1408 };
1409 debug_assert!(
1410 self.http_listeners.contains_key(&address),
1411 "add_http_listener must insert the listener under its address"
1412 );
1413 debug_assert_eq!(
1414 self.http_listeners.len(),
1415 before + 1,
1416 "add_http_listener inserts exactly one entry on the vacant path"
1417 );
1418 Ok(())
1419 }
1420
1421 fn add_https_listener(&mut self, listener: &HttpsListenerConfig) -> Result<(), StateError> {
1422 if let Some(v) = listener.forwarded_headers {
1425 validate_forwarded_headers(v)?;
1426 }
1427 let address = validated_listener_key(listener.listener_key())?;
1428 let before = self.https_listeners.len();
1429 match self.https_listeners.entry(address.clone()) {
1430 BTreeMapEntry::Vacant(vacant_entry) => vacant_entry.insert(listener.clone()),
1431 BTreeMapEntry::Occupied(_) => {
1432 debug_assert_eq!(
1433 self.https_listeners.len(),
1434 before,
1435 "a rejected duplicate add_https_listener must not mutate the map"
1436 );
1437 return Err(StateError::Exists {
1438 kind: ObjectKind::HttpsListener,
1439 id: address.to_string(),
1440 });
1441 }
1442 };
1443 debug_assert!(
1444 self.https_listeners.contains_key(&address),
1445 "add_https_listener must insert the listener under its address"
1446 );
1447 debug_assert_eq!(
1448 self.https_listeners.len(),
1449 before + 1,
1450 "add_https_listener inserts exactly one entry on the vacant path"
1451 );
1452 Ok(())
1453 }
1454
1455 fn add_tcp_listener(&mut self, listener: &TcpListenerConfig) -> Result<(), StateError> {
1456 let address = validated_listener_key(listener.listener_key())?;
1457 let before = self.tcp_listeners.len();
1458 match self.tcp_listeners.entry(address.clone()) {
1459 BTreeMapEntry::Vacant(vacant_entry) => vacant_entry.insert(listener.clone()),
1460 BTreeMapEntry::Occupied(_) => {
1461 debug_assert_eq!(
1462 self.tcp_listeners.len(),
1463 before,
1464 "a rejected duplicate add_tcp_listener must not mutate the map"
1465 );
1466 return Err(StateError::Exists {
1467 kind: ObjectKind::TcpListener,
1468 id: address.to_string(),
1469 });
1470 }
1471 };
1472 debug_assert!(
1473 self.tcp_listeners.contains_key(&address),
1474 "add_tcp_listener must insert the listener under its address"
1475 );
1476 debug_assert_eq!(
1477 self.tcp_listeners.len(),
1478 before + 1,
1479 "add_tcp_listener inserts exactly one entry on the vacant path"
1480 );
1481 Ok(())
1482 }
1483
1484 fn add_udp_listener(&mut self, listener: &UdpListenerConfig) -> Result<(), StateError> {
1485 let address = validated_listener_key(listener.listener_key())?;
1486 match self.udp_listeners.entry(address.clone()) {
1487 BTreeMapEntry::Vacant(vacant_entry) => vacant_entry.insert(listener.clone()),
1488 BTreeMapEntry::Occupied(_) => {
1489 return Err(StateError::Exists {
1490 kind: ObjectKind::UdpListener,
1491 id: address.to_string(),
1492 });
1493 }
1494 };
1495 Ok(())
1496 }
1497
1498 fn remove_listener(&mut self, remove: &RemoveListener) -> Result<(), StateError> {
1499 match ListenerType::try_from(remove.proxy).map_err(StateError::WrongFieldValue)? {
1500 ListenerType::Http => self.remove_http_listener(&remove.listener_key()),
1501 ListenerType::Https => self.remove_https_listener(&remove.listener_key()),
1502 ListenerType::Tcp => self.remove_tcp_listener(&remove.listener_key()),
1503 ListenerType::Udp => self.remove_udp_listener(&remove.listener_key()),
1504 }
1505 }
1506
1507 fn remove_http_listener(&mut self, address: &ListenerKey) -> Result<(), StateError> {
1508 let before = self.http_listeners.len();
1509 if self.http_listeners.remove(address).is_none() {
1510 debug_assert_eq!(
1511 self.http_listeners.len(),
1512 before,
1513 "a failed remove_http_listener must not mutate the map"
1514 );
1515 return Err(StateError::NoChange);
1516 }
1517 debug_assert!(
1518 !self.http_listeners.contains_key(address),
1519 "remove_http_listener must evict the address"
1520 );
1521 debug_assert_eq!(
1522 self.http_listeners.len(),
1523 before - 1,
1524 "remove_http_listener drops exactly one entry"
1525 );
1526 Ok(())
1527 }
1528
1529 fn remove_https_listener(&mut self, address: &ListenerKey) -> Result<(), StateError> {
1530 let before = self.https_listeners.len();
1531 if self.https_listeners.remove(address).is_none() {
1532 debug_assert_eq!(
1533 self.https_listeners.len(),
1534 before,
1535 "a failed remove_https_listener must not mutate the map"
1536 );
1537 return Err(StateError::NoChange);
1538 }
1539 debug_assert!(
1540 !self.https_listeners.contains_key(address),
1541 "remove_https_listener must evict the address"
1542 );
1543 debug_assert_eq!(
1544 self.https_listeners.len(),
1545 before - 1,
1546 "remove_https_listener drops exactly one entry"
1547 );
1548 Ok(())
1549 }
1550
1551 fn remove_tcp_listener(&mut self, address: &ListenerKey) -> Result<(), StateError> {
1552 let before = self.tcp_listeners.len();
1553 if self.tcp_listeners.remove(address).is_none() {
1554 debug_assert_eq!(
1555 self.tcp_listeners.len(),
1556 before,
1557 "a failed remove_tcp_listener must not mutate the map"
1558 );
1559 return Err(StateError::NoChange);
1560 }
1561 debug_assert!(
1562 !self.tcp_listeners.contains_key(address),
1563 "remove_tcp_listener must evict the address"
1564 );
1565 debug_assert_eq!(
1566 self.tcp_listeners.len(),
1567 before - 1,
1568 "remove_tcp_listener drops exactly one entry"
1569 );
1570 Ok(())
1571 }
1572
1573 fn remove_udp_listener(&mut self, address: &ListenerKey) -> Result<(), StateError> {
1574 if self.udp_listeners.remove(address).is_none() {
1575 return Err(StateError::NoChange);
1576 }
1577 Ok(())
1578 }
1579
1580 fn update_http_listener(&mut self, patch: &UpdateHttpListenerConfig) -> Result<(), StateError> {
1592 validate_h2_flood_knobs_http(patch)?;
1593
1594 let key = listener_key_for_update(
1595 &self.http_listeners,
1596 patch.address.into(),
1597 ObjectKind::HttpListener,
1598 )?;
1599 if let Some(ref v) = patch.sozu_id_header {
1600 validate_sozu_id_header(v)?;
1601 }
1602 if let Some(v) = patch.forwarded_headers {
1603 validate_forwarded_headers(v)?;
1604 }
1605
1606 let listener = self
1607 .http_listeners
1608 .get_mut(&key)
1609 .expect("listener_key_for_update returns a key present in the map");
1610
1611 if let Some(v) = patch.public_address {
1613 listener.public_address = Some(v);
1614 }
1615 if let Some(v) = patch.expect_proxy {
1616 listener.expect_proxy = v;
1617 }
1618 if let Some(ref v) = patch.sticky_name {
1619 listener.sticky_name = v.to_owned();
1620 }
1621 if let Some(v) = patch.front_timeout {
1622 listener.front_timeout = v;
1623 }
1624 if let Some(v) = patch.back_timeout {
1625 listener.back_timeout = v;
1626 }
1627 if let Some(v) = patch.connect_timeout {
1628 listener.connect_timeout = v;
1629 }
1630 if let Some(v) = patch.request_timeout {
1631 listener.request_timeout = v;
1632 }
1633 if let Some(v) = patch.elide_x_real_ip {
1634 listener.elide_x_real_ip = Some(v);
1635 }
1636 if let Some(v) = patch.send_x_real_ip {
1637 listener.send_x_real_ip = Some(v);
1638 }
1639 if let Some(patch_answers) = patch.http_answers.as_ref() {
1640 merge_custom_http_answers(&mut listener.http_answers, patch_answers);
1641 }
1642 merge_answers(&mut listener.answers, &patch.answers);
1643 if let Some(v) = patch.h2_max_rst_stream_per_window {
1645 listener.h2_max_rst_stream_per_window = Some(v);
1646 }
1647 if let Some(v) = patch.h2_max_ping_per_window {
1648 listener.h2_max_ping_per_window = Some(v);
1649 }
1650 if let Some(v) = patch.h2_max_settings_per_window {
1651 listener.h2_max_settings_per_window = Some(v);
1652 }
1653 if let Some(v) = patch.h2_max_empty_data_per_window {
1654 listener.h2_max_empty_data_per_window = Some(v);
1655 }
1656 if let Some(v) = patch.h2_max_continuation_frames {
1657 listener.h2_max_continuation_frames = Some(v);
1658 }
1659 if let Some(v) = patch.h2_max_glitch_count {
1660 listener.h2_max_glitch_count = Some(v);
1661 }
1662 if let Some(v) = patch.h2_initial_connection_window {
1663 listener.h2_initial_connection_window = Some(v);
1664 }
1665 if let Some(v) = patch.h2_max_concurrent_streams {
1666 listener.h2_max_concurrent_streams = Some(v);
1667 }
1668 if let Some(v) = patch.h2_stream_shrink_ratio {
1669 listener.h2_stream_shrink_ratio = Some(v);
1670 }
1671 if let Some(v) = patch.h2_max_rst_stream_lifetime {
1672 listener.h2_max_rst_stream_lifetime = Some(v);
1673 }
1674 if let Some(v) = patch.h2_max_rst_stream_abusive_lifetime {
1675 listener.h2_max_rst_stream_abusive_lifetime = Some(v);
1676 }
1677 if let Some(v) = patch.h2_max_rst_stream_emitted_lifetime {
1678 listener.h2_max_rst_stream_emitted_lifetime = Some(v);
1679 }
1680 if let Some(v) = patch.h2_max_header_list_size {
1681 listener.h2_max_header_list_size = Some(v);
1682 }
1683 if let Some(v) = patch.h2_max_header_table_size {
1684 listener.h2_max_header_table_size = Some(v);
1685 }
1686 if let Some(v) = patch.h2_max_header_fields {
1687 listener.h2_max_header_fields = Some(v);
1688 }
1689 if let Some(v) = patch.h2_stream_refusal_percent {
1690 listener.h2_stream_refusal_percent = Some(v);
1691 }
1692 if let Some(v) = patch.h2_stream_idle_timeout_seconds {
1693 listener.h2_stream_idle_timeout_seconds = Some(v);
1694 }
1695 if let Some(v) = patch.h2_graceful_shutdown_deadline_seconds {
1697 listener.h2_graceful_shutdown_deadline_seconds = Some(v);
1698 }
1699 if let Some(v) = patch.h2_max_window_update_stream0_per_window {
1700 listener.h2_max_window_update_stream0_per_window = Some(v);
1701 }
1702 if let Some(ref v) = patch.sozu_id_header {
1703 listener.sozu_id_header = Some(v.to_owned());
1704 }
1705 if let Some(v) = patch.forwarded_headers {
1706 listener.forwarded_headers = Some(v);
1707 }
1708 Ok(())
1709 }
1710
1711 pub fn client_auth_refresh(
1721 &self,
1722 listener: &HttpsListenerConfig,
1723 ) -> Option<UpdateHttpsListenerConfig> {
1724 let running = self.https_listeners.get(&listener.listener_key())?;
1725 let policy = listener.client_auth_policy();
1726 (running.client_auth_policy() != policy).then(|| UpdateHttpsListenerConfig {
1727 address: listener.address,
1728 client_auth_policy: Some(policy),
1729 ..Default::default()
1730 })
1731 }
1732
1733 fn update_https_listener(
1753 &mut self,
1754 patch: &UpdateHttpsListenerConfig,
1755 ) -> Result<(), StateError> {
1756 validate_h2_flood_knobs_https(patch)?;
1757
1758 let key = listener_key_for_update(
1759 &self.https_listeners,
1760 patch.address.into(),
1761 ObjectKind::HttpsListener,
1762 )?;
1763 if let Some(ref alpn_wrapper) = patch.alpn_protocols {
1764 validate_alpn_protocols(&alpn_wrapper.values)?;
1765 }
1766 if let Some(ref v) = patch.sozu_id_header {
1767 validate_sozu_id_header(v)?;
1768 }
1769 if let Some(v) = patch.forwarded_headers {
1770 validate_forwarded_headers(v)?;
1771 }
1772 if let Some(ref hsts) = patch.hsts {
1773 validate_hsts_patch(hsts)?;
1774 }
1775
1776 let listener = self
1777 .https_listeners
1778 .get_mut(&key)
1779 .expect("listener_key_for_update returns a key present in the map");
1780
1781 if let Some(v) = patch.public_address {
1783 listener.public_address = Some(v);
1784 }
1785 if let Some(v) = patch.expect_proxy {
1786 listener.expect_proxy = v;
1787 }
1788 if let Some(ref v) = patch.sticky_name {
1789 listener.sticky_name = v.to_owned();
1790 }
1791 if let Some(v) = patch.front_timeout {
1792 listener.front_timeout = v;
1793 }
1794 if let Some(v) = patch.back_timeout {
1795 listener.back_timeout = v;
1796 }
1797 if let Some(v) = patch.connect_timeout {
1798 listener.connect_timeout = v;
1799 }
1800 if let Some(v) = patch.request_timeout {
1801 listener.request_timeout = v;
1802 }
1803 if let Some(v) = patch.elide_x_real_ip {
1804 listener.elide_x_real_ip = Some(v);
1805 }
1806 if let Some(v) = patch.send_x_real_ip {
1807 listener.send_x_real_ip = Some(v);
1808 }
1809 if let Some(patch_answers) = patch.http_answers.as_ref() {
1810 merge_custom_http_answers(&mut listener.http_answers, patch_answers);
1811 }
1812 merge_answers(&mut listener.answers, &patch.answers);
1813 if let Some(hsts) = patch.hsts {
1816 listener.hsts = Some(hsts);
1817 }
1818 if let Some(ref alpn_wrapper) = patch.alpn_protocols {
1819 listener.alpn_protocols = alpn_wrapper.values.clone();
1821 }
1822 if let Some(v) = patch.strict_sni_binding {
1823 listener.strict_sni_binding = Some(v);
1824 }
1825 if let Some(v) = patch.disable_http11 {
1826 listener.disable_http11 = Some(v);
1827 }
1828 if let Some(ref policy) = patch.client_auth_policy {
1829 listener.apply_client_auth_policy(policy);
1830 }
1831 if let Some(v) = patch.h2_max_rst_stream_per_window {
1833 listener.h2_max_rst_stream_per_window = Some(v);
1834 }
1835 if let Some(v) = patch.h2_max_ping_per_window {
1836 listener.h2_max_ping_per_window = Some(v);
1837 }
1838 if let Some(v) = patch.h2_max_settings_per_window {
1839 listener.h2_max_settings_per_window = Some(v);
1840 }
1841 if let Some(v) = patch.h2_max_empty_data_per_window {
1842 listener.h2_max_empty_data_per_window = Some(v);
1843 }
1844 if let Some(v) = patch.h2_max_continuation_frames {
1845 listener.h2_max_continuation_frames = Some(v);
1846 }
1847 if let Some(v) = patch.h2_max_glitch_count {
1848 listener.h2_max_glitch_count = Some(v);
1849 }
1850 if let Some(v) = patch.h2_initial_connection_window {
1851 listener.h2_initial_connection_window = Some(v);
1852 }
1853 if let Some(v) = patch.h2_max_concurrent_streams {
1854 listener.h2_max_concurrent_streams = Some(v);
1855 }
1856 if let Some(v) = patch.h2_stream_shrink_ratio {
1857 listener.h2_stream_shrink_ratio = Some(v);
1858 }
1859 if let Some(v) = patch.h2_max_rst_stream_lifetime {
1860 listener.h2_max_rst_stream_lifetime = Some(v);
1861 }
1862 if let Some(v) = patch.h2_max_rst_stream_abusive_lifetime {
1863 listener.h2_max_rst_stream_abusive_lifetime = Some(v);
1864 }
1865 if let Some(v) = patch.h2_max_rst_stream_emitted_lifetime {
1866 listener.h2_max_rst_stream_emitted_lifetime = Some(v);
1867 }
1868 if let Some(v) = patch.h2_max_header_list_size {
1869 listener.h2_max_header_list_size = Some(v);
1870 }
1871 if let Some(v) = patch.h2_max_header_table_size {
1872 listener.h2_max_header_table_size = Some(v);
1873 }
1874 if let Some(v) = patch.h2_max_header_fields {
1875 listener.h2_max_header_fields = Some(v);
1876 }
1877 if let Some(v) = patch.h2_stream_refusal_percent {
1878 listener.h2_stream_refusal_percent = Some(v);
1879 }
1880 if let Some(v) = patch.h2_stream_idle_timeout_seconds {
1881 listener.h2_stream_idle_timeout_seconds = Some(v);
1882 }
1883 if let Some(v) = patch.h2_graceful_shutdown_deadline_seconds {
1885 listener.h2_graceful_shutdown_deadline_seconds = Some(v);
1886 }
1887 if let Some(v) = patch.h2_max_window_update_stream0_per_window {
1888 listener.h2_max_window_update_stream0_per_window = Some(v);
1889 }
1890 if let Some(ref v) = patch.sozu_id_header {
1891 listener.sozu_id_header = Some(v.to_owned());
1892 }
1893 if let Some(v) = patch.forwarded_headers {
1894 listener.forwarded_headers = Some(v);
1895 }
1896 Ok(())
1897 }
1898
1899 fn update_tcp_listener(&mut self, patch: &UpdateTcpListenerConfig) -> Result<(), StateError> {
1904 let key = listener_key_for_update(
1905 &self.tcp_listeners,
1906 patch.address.into(),
1907 ObjectKind::TcpListener,
1908 )?;
1909 let listener = self
1910 .tcp_listeners
1911 .get_mut(&key)
1912 .expect("listener_key_for_update returns a key present in the map");
1913
1914 if let Some(v) = patch.public_address {
1915 listener.public_address = Some(v);
1916 }
1917 if let Some(v) = patch.expect_proxy {
1918 listener.expect_proxy = v;
1919 }
1920 if let Some(v) = patch.front_timeout {
1921 listener.front_timeout = v;
1922 }
1923 if let Some(v) = patch.back_timeout {
1924 listener.back_timeout = v;
1925 }
1926 if let Some(v) = patch.connect_timeout {
1927 listener.connect_timeout = v;
1928 }
1929 Ok(())
1930 }
1931
1932 fn update_udp_listener(&mut self, patch: &UpdateUdpListenerConfig) -> Result<(), StateError> {
1937 let key = listener_key_for_update(
1938 &self.udp_listeners,
1939 patch.address.into(),
1940 ObjectKind::UdpListener,
1941 )?;
1942 let listener = self
1943 .udp_listeners
1944 .get_mut(&key)
1945 .expect("listener_key_for_update returns a key present in the map");
1946
1947 if let Some(v) = patch.public_address {
1948 listener.public_address = Some(v);
1949 }
1950 if let Some(v) = patch.front_timeout {
1951 listener.front_timeout = v;
1952 }
1953 if let Some(v) = patch.back_timeout {
1954 listener.back_timeout = v;
1955 }
1956 if let Some(v) = patch.max_rx_datagram_size {
1957 listener.max_rx_datagram_size = v;
1958 }
1959 if let Some(v) = patch.max_flows {
1960 listener.max_flows = v;
1961 }
1962 Ok(())
1963 }
1964
1965 fn activate_listener(&mut self, activate: &ActivateListener) -> Result<(), StateError> {
1966 match ListenerType::try_from(activate.proxy).map_err(StateError::WrongFieldValue)? {
1967 ListenerType::Http => self
1968 .http_listeners
1969 .get_mut(&activate.listener_key())
1970 .map(|listener| listener.active = true)
1971 .ok_or(StateError::NotFound {
1972 kind: ObjectKind::HttpListener,
1973 id: activate.listener_key().to_string(),
1974 }),
1975 ListenerType::Https => self
1976 .https_listeners
1977 .get_mut(&activate.listener_key())
1978 .map(|listener| listener.active = true)
1979 .ok_or(StateError::NotFound {
1980 kind: ObjectKind::HttpsListener,
1981 id: activate.listener_key().to_string(),
1982 }),
1983 ListenerType::Tcp => self
1984 .tcp_listeners
1985 .get_mut(&activate.listener_key())
1986 .map(|listener| listener.active = true)
1987 .ok_or(StateError::NotFound {
1988 kind: ObjectKind::TcpListener,
1989 id: activate.listener_key().to_string(),
1990 }),
1991 ListenerType::Udp => self
1992 .udp_listeners
1993 .get_mut(&activate.listener_key())
1994 .map(|listener| listener.active = true)
1995 .ok_or(StateError::NotFound {
1996 kind: ObjectKind::UdpListener,
1997 id: activate.listener_key().to_string(),
1998 }),
1999 }
2000 }
2001
2002 fn deactivate_listener(&mut self, deactivate: &DeactivateListener) -> Result<(), StateError> {
2003 match ListenerType::try_from(deactivate.proxy).map_err(StateError::WrongFieldValue)? {
2004 ListenerType::Http => self
2005 .http_listeners
2006 .get_mut(&deactivate.listener_key())
2007 .map(|listener| listener.active = false)
2008 .ok_or(StateError::NotFound {
2009 kind: ObjectKind::HttpListener,
2010 id: deactivate.listener_key().to_string(),
2011 }),
2012 ListenerType::Https => self
2013 .https_listeners
2014 .get_mut(&deactivate.listener_key())
2015 .map(|listener| listener.active = false)
2016 .ok_or(StateError::NotFound {
2017 kind: ObjectKind::HttpsListener,
2018 id: deactivate.listener_key().to_string(),
2019 }),
2020 ListenerType::Tcp => self
2021 .tcp_listeners
2022 .get_mut(&deactivate.listener_key())
2023 .map(|listener| listener.active = false)
2024 .ok_or(StateError::NotFound {
2025 kind: ObjectKind::TcpListener,
2026 id: deactivate.listener_key().to_string(),
2027 }),
2028 ListenerType::Udp => self
2029 .udp_listeners
2030 .get_mut(&deactivate.listener_key())
2031 .map(|listener| listener.active = false)
2032 .ok_or(StateError::NotFound {
2033 kind: ObjectKind::UdpListener,
2034 id: deactivate.listener_key().to_string(),
2035 }),
2036 }
2037 }
2038
2039 fn add_frontend_to(
2046 fronts: &mut BTreeMap<String, HttpFrontend>,
2047 front: &RequestHttpFrontend,
2048 kind: ObjectKind,
2049 ) -> Result<(), StateError> {
2050 let front_as_key = front.to_string();
2051 let before = fronts.len();
2052
2053 match fronts.entry(front.to_string()) {
2054 BTreeMapEntry::Vacant(e) => {
2055 e.insert(front.clone().to_frontend().map_err(|into_error| {
2056 StateError::FrontendConversion {
2057 frontend: front_as_key,
2058 error: into_error.to_string(),
2059 }
2060 })?)
2061 }
2062 BTreeMapEntry::Occupied(_) => {
2063 debug_assert_eq!(
2064 fronts.len(),
2065 before,
2066 "a rejected duplicate {kind:?} add must not mutate the map"
2067 );
2068 return Err(StateError::Exists {
2069 kind,
2070 id: front.to_string(),
2071 });
2072 }
2073 };
2074 debug_assert!(
2077 fronts.contains_key(&front.to_string()),
2078 "adding a {kind:?} must insert the route key on success"
2079 );
2080 debug_assert_eq!(
2081 fronts.len(),
2082 before + 1,
2083 "adding a {kind:?} inserts exactly one entry on success"
2084 );
2085 Ok(())
2086 }
2087
2088 fn remove_frontend_from(
2091 fronts: &mut BTreeMap<String, HttpFrontend>,
2092 front: &RequestHttpFrontend,
2093 kind: ObjectKind,
2094 ) -> Result<(), StateError> {
2095 let key = front.to_string();
2096 let before = fronts.len();
2097 match fronts.get(&key) {
2102 Some(stored) if stored.cluster_id.as_deref() == front.cluster_id.as_deref() => {
2103 fronts.remove(&key);
2104 }
2105 _ => {
2106 debug_assert_eq!(fronts.len(), before, "a refused removal must not mutate");
2107 return Err(StateError::NotFound {
2108 kind,
2109 id: front.to_string(),
2110 });
2111 }
2112 }
2113 debug_assert!(
2114 !fronts.contains_key(&key),
2115 "removing a frontend must evict the route key"
2116 );
2117 debug_assert_eq!(
2118 fronts.len(),
2119 before - 1,
2120 "removing a frontend drops exactly one entry"
2121 );
2122 Ok(())
2123 }
2124
2125 fn add_http_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
2126 Self::add_frontend_to(&mut self.http_fronts, front, ObjectKind::HttpFrontend)
2127 }
2128
2129 fn add_https_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
2130 Self::add_frontend_to(&mut self.https_fronts, front, ObjectKind::HttpsFrontend)
2131 }
2132
2133 fn remove_http_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
2134 Self::remove_frontend_from(&mut self.http_fronts, front, ObjectKind::HttpFrontend)
2135 }
2136
2137 fn remove_https_frontend(&mut self, front: &RequestHttpFrontend) -> Result<(), StateError> {
2138 Self::remove_frontend_from(&mut self.https_fronts, front, ObjectKind::HttpsFrontend)
2139 }
2140
2141 fn add_certificate(&mut self, add: &AddCertificate) -> Result<(), StateError> {
2142 let fingerprint = add
2143 .certificate
2144 .fingerprint()
2145 .map_err(StateError::AddCertificate)?;
2146 let address: SocketAddr = add.address.into();
2147
2148 if self
2152 .certificates
2153 .get(&address)
2154 .is_some_and(|entries| entries.contains_key(&fingerprint))
2155 {
2156 let names_bytes = add
2157 .certificate
2158 .names
2159 .iter()
2160 .fold(0usize, |total, name| total.saturating_add(name.len()));
2161 info!(
2162 "Skip loading certificate with fingerprint_bytes={} names_count={} names_bytes={} on listener '{}', the certificate is already present.",
2163 fingerprint.0.len(),
2164 add.certificate.names.len(),
2165 names_bytes,
2166 add.address
2167 );
2168 return Ok(());
2169 }
2170
2171 let mut add = add.clone();
2182 add.certificate
2183 .apply_overriding_names()
2184 .map_err(StateError::AddCertificate)?;
2185 let trusted_next_order = self.certificate_metadata_is_trusted(address).then(|| {
2186 self.certificate_metadata
2187 .get(&address)
2188 .and_then(|index| index.next_order)
2189 .expect("trusted metadata index must carry its next ordinal")
2190 });
2191 let normalized_metadata = trusted_next_order
2192 .is_none()
2193 .then(|| self.normalized_certificate_metadata_for_address(address));
2194 let entry = self.certificates.entry(address).or_default();
2195 let before = entry.len();
2196 entry.insert(fingerprint.clone(), add.certificate);
2197 if let Some(insertion_order) = trusted_next_order {
2198 let metadata = self
2199 .certificate_metadata
2200 .get_mut(&address)
2201 .expect("trusted metadata index must remain present");
2202 metadata.entries.insert(
2203 fingerprint.clone(),
2204 CertificateMetadata {
2205 expired_at: add.expired_at,
2206 insertion_order,
2207 },
2208 );
2209 metadata.next_order = Some(insertion_order + 1);
2210 } else {
2211 let mut metadata =
2215 normalized_metadata.expect("untrusted metadata must have a normalized candidate");
2216 let insertion_order = metadata.entries.len() as u64;
2217 metadata.entries.insert(
2218 fingerprint.clone(),
2219 CertificateMetadata {
2220 expired_at: add.expired_at,
2221 insertion_order,
2222 },
2223 );
2224 metadata.next_order = Some(insertion_order + 1);
2225 self.certificate_metadata.insert(address, metadata);
2226 }
2227 debug_assert!(
2228 entry.contains_key(&fingerprint),
2229 "add_certificate must insert the fingerprint under its address"
2230 );
2231 debug_assert_eq!(
2232 entry.len(),
2233 before + 1,
2234 "add_certificate inserts exactly one fingerprint on the new path"
2235 );
2236 Ok(())
2237 }
2238
2239 fn remove_certificate(&mut self, remove: &RemoveCertificate) -> Result<(), StateError> {
2240 let fingerprint = Fingerprint(
2241 hex::decode(&remove.fingerprint)
2242 .map_err(|decode_error| StateError::RemoveCertificate(decode_error.to_string()))?,
2243 );
2244 let address = remove.address.into();
2245
2246 if self
2247 .certificates
2248 .get(&address)
2249 .is_none_or(|entries| !entries.contains_key(&fingerprint))
2250 {
2251 return Ok(());
2252 }
2253
2254 let mut metadata = self.take_certificate_metadata_for_state_change(address);
2255 Self::remove_certificate_metadata(&mut metadata, &fingerprint);
2256
2257 let became_empty = if let Some(index) = self.certificates.get_mut(&address) {
2258 index.remove(&fingerprint);
2259 debug_assert!(
2260 !index.contains_key(&fingerprint),
2261 "remove_certificate must evict the fingerprint when the address is known"
2262 );
2263 index.is_empty()
2264 } else {
2265 false
2266 };
2267
2268 if became_empty {
2280 self.certificates.remove(&address);
2281 self.certificate_metadata.remove(&address);
2282 } else {
2283 self.certificate_metadata.insert(address, metadata);
2284 }
2285
2286 Ok(())
2287 }
2288
2289 fn replace_certificate(&mut self, replace: &ReplaceCertificate) -> Result<(), StateError> {
2307 let replace_address = replace.address.into();
2308 let old_fingerprint = Fingerprint(
2309 hex::decode(&replace.old_fingerprint)
2310 .map_err(|decode_error| StateError::RemoveCertificate(decode_error.to_string()))?,
2311 );
2312
2313 if !self.certificates.contains_key(&replace_address) {
2314 return Err(StateError::NotFound {
2315 kind: ObjectKind::Certificate,
2316 id: replace.address.to_string(),
2317 });
2318 }
2319
2320 let mut new_certificate = replace.new_certificate.to_owned();
2321 new_certificate
2322 .apply_overriding_names()
2323 .map_err(|error| StateError::ReplaceCertificate(error.to_string()))?;
2324 let new_fingerprint = Fingerprint(
2325 calculate_fingerprint(new_certificate.certificate.as_bytes()).map_err(
2326 |fingerprint_err| StateError::ReplaceCertificate(fingerprint_err.to_string()),
2327 )?,
2328 );
2329 let stored_material_matches = self
2330 .certificates
2331 .get(&replace_address)
2332 .and_then(|entries| entries.get(&new_fingerprint))
2333 .is_some_and(|stored| stored == &new_certificate);
2334 let stored_expiration_matches = self
2335 .certificate_metadata
2336 .get(&replace_address)
2337 .and_then(|metadata| metadata.entries.get(&new_fingerprint))
2338 .and_then(|metadata| metadata.expired_at)
2339 == replace.new_expired_at;
2340
2341 if old_fingerprint == new_fingerprint
2345 && stored_material_matches
2346 && stored_expiration_matches
2347 {
2348 return Ok(());
2349 }
2350
2351 let mut metadata = self.take_certificate_metadata_for_state_change(replace_address);
2352 let new_material_already_stored = stored_material_matches
2353 && metadata
2354 .entries
2355 .get(&new_fingerprint)
2356 .is_some_and(|stored| stored.expired_at == replace.new_expired_at);
2357
2358 let certificates =
2359 self.certificates
2360 .get_mut(&replace_address)
2361 .ok_or(StateError::ReplaceCertificate(
2362 "Unlikely error. This entry in the certificate hashmap should be present"
2363 .to_owned(),
2364 ))?;
2365
2366 if !new_material_already_stored {
2367 certificates.remove(&new_fingerprint);
2368 Self::remove_certificate_metadata(&mut metadata, &new_fingerprint);
2369 }
2370 if old_fingerprint != new_fingerprint {
2371 certificates.remove(&old_fingerprint);
2372 Self::remove_certificate_metadata(&mut metadata, &old_fingerprint);
2373 }
2374 if !new_material_already_stored {
2375 let insertion_order = metadata.entries.len() as u64;
2376 certificates.insert(new_fingerprint.clone(), new_certificate);
2377 metadata.entries.insert(
2378 new_fingerprint.clone(),
2379 CertificateMetadata {
2380 expired_at: replace.new_expired_at,
2381 insertion_order,
2382 },
2383 );
2384 metadata.next_order = Some(insertion_order + 1);
2385 }
2386 self.certificate_metadata.insert(replace_address, metadata);
2387
2388 debug_assert!(
2391 self.certificates
2392 .get(&replace_address)
2393 .is_some_and(|certs| certs.contains_key(&new_fingerprint)),
2394 "replace_certificate must leave the new fingerprint present"
2395 );
2396 debug_assert!(
2397 new_fingerprint == old_fingerprint
2398 || self
2399 .certificates
2400 .get(&replace_address)
2401 .is_none_or(|certs| !certs.contains_key(&old_fingerprint)),
2402 "replace_certificate must evict the old fingerprint unless it equals the new one"
2403 );
2404 Ok(())
2405 }
2406
2407 fn add_tcp_frontend(&mut self, front: &RequestTcpFrontend) -> Result<(), StateError> {
2415 let address: SocketAddr = front.address.into();
2416
2417 if self
2420 .clusters
2421 .get(front.cluster_id.as_str())
2422 .is_some_and(|cluster| {
2423 cluster.affinity_header.is_some() || cluster.affinity_cookie.is_some()
2424 })
2425 {
2426 return Err(StateError::InvalidTcpFrontend {
2427 address,
2428 reason: TCP_CLUSTER_AFFINITY_KEY.to_owned(),
2429 });
2430 }
2431
2432 let normalized_sni = match &front.sni {
2440 Some(sni) => Some(validate_sni_pattern(sni).map_err(|config_error| {
2441 StateError::InvalidTcpFrontend {
2442 address,
2443 reason: format!("sni {sni:?} failed SNI shape validation: {config_error}"),
2444 }
2445 })?),
2446 None => None,
2447 };
2448 let alpn = canonical_tcp_alpn(&front.alpn);
2449
2450 if normalized_sni.is_none() && !alpn.is_empty() {
2456 return Err(StateError::InvalidTcpFrontend {
2457 address,
2458 reason: format!(
2459 "alpn = {alpn:?} set without sni: alpn only matches within an SNI-scoped \
2460 preread, so a frontend without sni would silently ignore its alpn list"
2461 ),
2462 });
2463 }
2464
2465 let total_before = self.count_tcp_frontends_raw();
2466
2467 let mut listener_has_no_sni_front = false;
2473 let mut listener_has_sni_front = false;
2474 for existing in self
2475 .tcp_fronts
2476 .values()
2477 .flatten()
2478 .filter(|existing| existing.address == address)
2479 {
2480 if tcp_frontend_matches(existing, address, &normalized_sni, &alpn) {
2481 return Err(StateError::Exists {
2482 kind: ObjectKind::TcpFrontend,
2483 id: format!(
2484 "TcpFrontend {{ address: {address}, sni: {normalized_sni:?}, \
2485 alpn: {alpn:?} }}"
2486 ),
2487 });
2488 }
2489 match &existing.sni {
2490 None => listener_has_no_sni_front = true,
2491 Some(existing_sni) => {
2492 listener_has_sni_front = true;
2493 if normalized_sni.as_deref() != Some(existing_sni.as_str()) {
2500 continue;
2501 }
2502 let existing_is_catch_all = existing.alpn.is_empty();
2503 let new_is_catch_all = alpn.is_empty();
2504 if existing_is_catch_all && new_is_catch_all {
2505 return Err(StateError::InvalidTcpFrontend {
2506 address,
2507 reason: format!(
2508 "sni {existing_sni:?} already has a catch-all (empty alpn) \
2509 frontend: at most one frontend per (address, sni) may omit \
2510 alpn"
2511 ),
2512 });
2513 }
2514 if !existing_is_catch_all
2519 && !new_is_catch_all
2520 && let Some(overlap) = alpn
2521 .iter()
2522 .find(|protocol| existing.alpn.contains(protocol))
2523 {
2524 return Err(StateError::InvalidTcpFrontend {
2525 address,
2526 reason: format!(
2527 "sni {existing_sni:?} already has a frontend matching ALPN \
2528 protocol {overlap:?}: ALPN matchers for the same (address, \
2529 sni) must not overlap"
2530 ),
2531 });
2532 }
2533 }
2534 }
2535 }
2536 match &normalized_sni {
2537 None if listener_has_sni_front => {
2538 return Err(StateError::InvalidTcpFrontend {
2539 address,
2540 reason: "a no-SNI frontend cannot be added to a listener that already has \
2541 SNI-scoped routes"
2542 .to_string(),
2543 });
2544 }
2545 Some(_) if listener_has_no_sni_front => {
2546 return Err(StateError::InvalidTcpFrontend {
2547 address,
2548 reason: "an SNI-scoped frontend cannot be added to a listener that already \
2549 has a no-SNI catch-all cluster"
2550 .to_string(),
2551 });
2552 }
2553 _ => {}
2554 }
2555 debug_assert_eq!(
2559 self.count_tcp_frontends_raw(),
2560 total_before,
2561 "add_tcp_frontend must not mutate tcp_fronts before every admission check has \
2562 passed"
2563 );
2564
2565 let tcp_frontend = TcpFrontend {
2566 cluster_id: front.cluster_id.as_str().into(),
2567 address,
2568 tags: front.tags.clone(),
2569 sni: normalized_sni,
2570 alpn,
2571 };
2572 let tcp_frontends = self
2573 .tcp_fronts
2574 .entry(tcp_frontend.cluster_id.clone())
2575 .or_default();
2576 let before = tcp_frontends.len();
2577 debug_assert_eq!(
2578 *tcp_frontend.cluster_id, *front.cluster_id,
2579 "the built frontend must carry its bucket's cluster_id"
2580 );
2581 tcp_frontends.push(tcp_frontend);
2582 debug_assert_eq!(
2583 tcp_frontends.len(),
2584 before + 1,
2585 "add_tcp_frontend appends exactly one entry"
2586 );
2587 Ok(())
2588 }
2589
2590 fn remove_tcp_frontend(
2591 &mut self,
2592 front_to_remove: &RequestTcpFrontend,
2593 ) -> Result<(), StateError> {
2594 let tcp_frontends =
2595 self.tcp_fronts
2596 .get_mut(front_to_remove.cluster_id.as_str())
2597 .ok_or(StateError::NotFound {
2598 kind: ObjectKind::TcpFrontend,
2599 id: format!(
2600 "RequestTcpFrontend {{ cluster_id: {:?}, address: {:?}, tags: {:?}, sni: {:?}, alpn: {:?} }}",
2601 front_to_remove.cluster_id,
2602 front_to_remove.address,
2603 front_to_remove.tags,
2604 front_to_remove.sni,
2605 front_to_remove.alpn,
2606 ),
2607 })?;
2608
2609 let len = tcp_frontends.len();
2610 let remove_address: SocketAddr = front_to_remove.address.into();
2611 let remove_sni = front_to_remove.sni.as_deref().map(str::to_ascii_lowercase);
2621 let remove_alpn = canonical_tcp_alpn(&front_to_remove.alpn);
2622 tcp_frontends.retain(|front| {
2627 !tcp_frontend_matches(front, remove_address, &remove_sni, &remove_alpn)
2628 });
2629 let after = tcp_frontends.len();
2630 if after == len {
2631 return Err(StateError::NoChange);
2632 }
2633 debug_assert_eq!(
2638 after,
2639 len - 1,
2640 "remove_tcp_frontend drops exactly one entry"
2641 );
2642 debug_assert!(
2643 !tcp_frontends.iter().any(|f| tcp_frontend_matches(
2644 f,
2645 remove_address,
2646 &remove_sni,
2647 &remove_alpn
2648 )),
2649 "remove_tcp_frontend must leave no frontend matching the removed (address, sni, alpn)"
2650 );
2651 Ok(())
2652 }
2653
2654 fn add_udp_frontend(&mut self, front: &RequestUdpFrontend) -> Result<(), StateError> {
2680 let address: SocketAddr = front.address.into();
2681 let udp_frontend = UdpFrontend {
2682 cluster_id: front.cluster_id.as_str().into(),
2683 address,
2684 tags: front.tags.clone(),
2685 };
2686
2687 if self
2688 .backends
2689 .get(front.cluster_id.as_str())
2690 .is_some_and(|backends| backends.iter().any(|b| b.unix_path.is_some()))
2691 {
2692 return Err(StateError::InvalidValue {
2693 field: "unix_path",
2694 reason: UDP_UNIX_BACKEND_REFUSED,
2695 });
2696 }
2697
2698 let total_before = self.count_udp_frontends_raw();
2699
2700 if let Some(current) = self
2705 .udp_fronts
2706 .values()
2707 .flatten()
2708 .find(|existing| existing.address == address)
2709 {
2710 if *current == udp_frontend {
2715 return Err(StateError::Exists {
2716 kind: ObjectKind::UdpFrontend,
2717 id: format!("{udp_frontend:?}"),
2718 });
2719 }
2720 return Err(StateError::UdpFrontendAddressTaken {
2721 address,
2722 cluster_id: udp_frontend.cluster_id.clone(),
2723 current_cluster_id: current.cluster_id.clone(),
2724 current_tags: current.tags.clone(),
2725 });
2726 }
2727
2728 debug_assert_eq!(
2732 self.count_udp_frontends_raw(),
2733 total_before,
2734 "add_udp_frontend must not mutate udp_fronts before every admission check has passed"
2735 );
2736
2737 let udp_frontends = self
2738 .udp_fronts
2739 .entry(udp_frontend.cluster_id.clone())
2740 .or_default();
2741 let before = udp_frontends.len();
2742 udp_frontends.push(udp_frontend);
2743 debug_assert_eq!(
2744 udp_frontends.len(),
2745 before + 1,
2746 "add_udp_frontend appends exactly one entry"
2747 );
2748 debug_assert_eq!(
2752 self.udp_fronts
2753 .values()
2754 .flatten()
2755 .filter(|existing| existing.address == address)
2756 .count(),
2757 1,
2758 "add_udp_frontend must leave exactly one frontend on an address, across all clusters"
2759 );
2760 Ok(())
2761 }
2762
2763 fn remove_udp_frontend(
2764 &mut self,
2765 front_to_remove: &RequestUdpFrontend,
2766 ) -> Result<(), StateError> {
2767 let udp_frontends = self
2768 .udp_fronts
2769 .get_mut(front_to_remove.cluster_id.as_str())
2770 .ok_or(StateError::NotFound {
2771 kind: ObjectKind::UdpFrontend,
2772 id: format!("{front_to_remove:?}"),
2773 })?;
2774
2775 let len = udp_frontends.len();
2776 let remove_address: SocketAddr = front_to_remove.address.into();
2777 let matches_removal = |front: &UdpFrontend| {
2787 front.address == remove_address && front.tags == front_to_remove.tags
2788 };
2789 udp_frontends.retain(|front| !matches_removal(front));
2790 let after = udp_frontends.len();
2791 if after == len {
2792 return Err(StateError::NoChange);
2793 }
2794 debug_assert_eq!(
2799 after,
2800 len - 1,
2801 "remove_udp_frontend drops exactly one entry"
2802 );
2803 debug_assert!(
2804 !udp_frontends.iter().any(matches_removal),
2805 "remove_udp_frontend must leave no frontend matching the removed (address, tags)"
2806 );
2807 Ok(())
2808 }
2809
2810 pub fn health_check_port_unix_conflict(&self, request: &RequestType) -> Option<&'static str> {
2822 let holds_unix_backend = |cluster_id: &str| {
2823 self.backends
2824 .get(cluster_id)
2825 .is_some_and(|backends| backends.iter().any(|b| b.unix_path.is_some()))
2826 };
2827 let port_conflicts = |config: &crate::proto::command::HealthCheckConfig,
2828 cluster_id: &str| {
2829 config.port.is_some()
2830 && crate::config::validate_health_check_config(config).is_ok()
2831 && holds_unix_backend(cluster_id)
2832 };
2833 let conflict = match request {
2834 RequestType::AddCluster(cluster) => {
2835 cluster.udp.is_none()
2836 && cluster
2837 .health_check
2838 .as_ref()
2839 .is_some_and(|config| port_conflicts(config, &cluster.cluster_id))
2840 }
2841 RequestType::SetHealthCheck(set) => {
2842 self.clusters.contains_key(set.cluster_id.as_str())
2843 && port_conflicts(&set.config, &set.cluster_id)
2844 }
2845 RequestType::SetProtectedHealthCheck(set) => {
2846 self.clusters.contains_key(set.cluster_id.as_str())
2847 && port_conflicts(&set.config, &set.cluster_id)
2848 }
2849 RequestType::AddBackend(add) | RequestType::AddUnixBackend(add) => {
2850 add.unix_path.as_deref().is_some_and(|unix_path| {
2851 crate::config::parse_backend_unix_path(unix_path).is_ok()
2852 && !self.is_udp_cluster(&add.cluster_id)
2853 && self
2854 .clusters
2855 .get(add.cluster_id.as_str())
2856 .and_then(|cluster| cluster.health_check.as_ref())
2857 .is_some_and(|config| config.port.is_some())
2858 })
2859 }
2860 _ => false,
2861 };
2862 conflict.then_some(crate::config::HEALTH_CHECK_PORT_UNIX_REFUSED)
2863 }
2864
2865 fn is_udp_cluster(&self, cluster_id: &str) -> bool {
2868 self.udp_fronts
2869 .get(cluster_id)
2870 .is_some_and(|fronts| !fronts.is_empty())
2871 || self
2872 .clusters
2873 .get(cluster_id)
2874 .is_some_and(|cluster| cluster.udp.is_some())
2875 }
2876
2877 fn add_backend(&mut self, add_backend: &AddBackend) -> Result<(), StateError> {
2884 if !self.clusters.contains_key(add_backend.cluster_id.as_str()) {
2885 debug_assert!(
2886 self.backends
2887 .get(add_backend.cluster_id.as_str())
2888 .is_none_or(Vec::is_empty),
2889 "no backend may outlive its cluster"
2890 );
2891 return Err(StateError::ClusterNotFound {
2892 kind: ObjectKind::Backend,
2893 cluster_id: add_backend.cluster_id.to_owned(),
2894 });
2895 }
2896 if let Some(unix_path) = add_backend.unix_path.as_deref() {
2897 crate::config::parse_backend_unix_path(unix_path).map_err(|reason| {
2898 StateError::InvalidValue {
2899 field: "unix_path",
2900 reason,
2901 }
2902 })?;
2903 if self.is_udp_cluster(&add_backend.cluster_id) {
2909 return Err(StateError::InvalidValue {
2910 field: "unix_path",
2911 reason: UDP_UNIX_BACKEND_REFUSED,
2912 });
2913 }
2914 }
2915 let backend = Backend {
2916 address: add_backend.address.into(),
2917 cluster_id: add_backend.cluster_id.as_str().into(),
2918 backend_id: add_backend.backend_id.clone(),
2919 sticky_id: add_backend.sticky_id.clone(),
2920 load_balancing_parameters: add_backend.load_balancing_parameters,
2921 backup: add_backend.backup,
2922 unix_path: add_backend.unix_path.clone(),
2923 };
2924 let backends = self.backends.entry(backend.cluster_id.clone()).or_default();
2925 let backend_id = backend.backend_id.clone();
2926 let backend_address = backend.address;
2927 let before = backends.len();
2928
2929 let was_present = backends
2934 .iter()
2935 .any(|b| b.backend_id == backend_id && b.address == backend_address);
2936 backends.retain(|b| b.backend_id != backend.backend_id || b.address != backend.address);
2937 debug_assert_eq!(
2938 backends.len(),
2939 before - was_present as usize,
2940 "the upsert retain must drop exactly the prior copy iff it existed"
2941 );
2942 backends.push(backend);
2943 backends.sort();
2944
2945 debug_assert_eq!(
2946 backends.len(),
2947 before + (!was_present) as usize,
2948 "add_backend grows the bucket by one iff the backend was new"
2949 );
2950 debug_assert_eq!(
2951 backends
2952 .iter()
2953 .filter(|b| b.backend_id == backend_id && b.address == backend_address)
2954 .count(),
2955 1,
2956 "exactly one copy of the upserted backend must remain"
2957 );
2958 Ok(())
2959 }
2960
2961 fn remove_backend(&mut self, backend: &RemoveBackend) -> Result<(), StateError> {
2962 let backend_list =
2963 self.backends
2964 .get_mut(backend.cluster_id.as_str())
2965 .ok_or(StateError::NotFound {
2966 kind: ObjectKind::Backend,
2967 id: backend.backend_id.to_owned(),
2968 })?;
2969
2970 let len = backend_list.len();
2971 let remove_address: SocketAddr = backend.address.into();
2972 backend_list.retain(|b| b.backend_id != backend.backend_id || b.address != remove_address);
2973 backend_list.sort();
2974 let after = backend_list.len();
2975 if after == len {
2976 return Err(StateError::NoChange);
2977 }
2978 debug_assert_eq!(after, len - 1, "remove_backend drops exactly one entry");
2981 debug_assert!(
2982 !backend_list
2983 .iter()
2984 .any(|b| b.backend_id == backend.backend_id && b.address == remove_address),
2985 "remove_backend must leave no backend matching (backend_id, address)"
2986 );
2987 Ok(())
2988 }
2989
2990 fn generate_requests(&self) -> Vec<Request> {
2992 let mut v: Vec<Request> = Vec::new();
2993
2994 for listener in self.http_listeners.values() {
2995 v.push(RequestType::AddHttpListener(listener.clone()).into());
2996 if listener.active {
2997 v.push(
2998 RequestType::ActivateListener(ActivateListener {
2999 interface: listener.interface.clone(),
3000 address: listener.address,
3001 proxy: ListenerType::Http.into(),
3002 from_scm: false,
3003 })
3004 .into(),
3005 );
3006 }
3007 }
3008
3009 for listener in self.https_listeners.values() {
3010 v.push(RequestType::add_https_listener(listener.clone()).into());
3011 if listener.active {
3012 v.push(
3013 RequestType::ActivateListener(ActivateListener {
3014 interface: listener.interface.clone(),
3015 address: listener.address,
3016 proxy: ListenerType::Https.into(),
3017 from_scm: false,
3018 })
3019 .into(),
3020 );
3021 }
3022 }
3023
3024 for listener in self.tcp_listeners.values() {
3025 v.push(RequestType::AddTcpListener(listener.clone()).into());
3026 if listener.active {
3027 v.push(
3028 RequestType::ActivateListener(ActivateListener {
3029 interface: listener.interface.clone(),
3030 address: listener.address,
3031 proxy: ListenerType::Tcp.into(),
3032 from_scm: false,
3033 })
3034 .into(),
3035 );
3036 }
3037 }
3038
3039 for listener in self.udp_listeners.values() {
3040 v.push(RequestType::AddUdpListener(listener.clone()).into());
3041 if listener.active {
3042 v.push(
3043 RequestType::ActivateListener(ActivateListener {
3044 interface: listener.interface.clone(),
3045 address: listener.address,
3046 proxy: ListenerType::Udp.into(),
3047 from_scm: false,
3048 })
3049 .into(),
3050 );
3051 }
3052 }
3053
3054 for cluster in self.clusters.values() {
3055 v.push(RequestType::AddCluster(cluster.clone()).into());
3056 }
3057
3058 for front in self.http_fronts.values() {
3059 v.push(RequestType::add_http_frontend(front.clone().into()).into());
3060 }
3061
3062 for add in self.certificate_add_requests() {
3063 v.push(RequestType::AddCertificate(add).into());
3064 }
3065
3066 for front in self.https_fronts.values() {
3067 v.push(RequestType::add_https_frontend(front.clone().into()).into());
3068 }
3069
3070 for front_list in self.tcp_fronts.values() {
3071 for front in front_list {
3072 v.push(RequestType::AddTcpFrontend(front.clone().into()).into());
3073 }
3074 }
3075
3076 for front_list in self.udp_fronts.values() {
3077 for front in front_list {
3078 v.push(RequestType::AddUdpFrontend(front.clone().into()).into());
3079 }
3080 }
3081
3082 for backend_list in self.backends.values() {
3083 for backend in backend_list {
3084 v.push(RequestType::add_backend(backend.clone().to_add_backend()).into());
3085 }
3086 }
3087
3088 #[cfg(debug_assertions)]
3094 {
3095 let mut replayed = ConfigState::new();
3096 for request in &v {
3097 debug_assert!(
3098 replayed.dispatch(request).is_ok(),
3099 "every request from generate_requests must replay cleanly"
3100 );
3101 }
3102 debug_assert!(
3103 replayed.clusters == self.clusters
3104 && replayed.backends == self.backends
3105 && replayed.http_listeners == self.http_listeners
3106 && replayed.https_listeners == self.https_listeners
3107 && replayed.tcp_listeners == self.tcp_listeners
3108 && replayed.http_fronts == self.http_fronts
3109 && replayed.https_fronts == self.https_fronts
3110 && replayed.tcp_fronts == self.tcp_fronts
3111 && replayed.certificates == self.certificates
3112 && replayed.certificate_metadata == self.normalized_certificate_metadata(),
3113 "replaying generate_requests into a fresh state must reproduce self"
3114 );
3115 }
3116
3117 v
3118 }
3119
3120 pub fn generate_activate_requests(&self) -> Vec<Request> {
3121 let mut v: Vec<Request> = Vec::new();
3122 for front in self
3123 .http_listeners
3124 .iter()
3125 .filter(|(_, listener)| listener.active)
3126 .map(|(k, _)| k)
3127 {
3128 v.push(
3129 RequestType::ActivateListener(ActivateListener {
3130 interface: front.interface.clone(),
3131 address: SocketAddress::from(front.address),
3132 proxy: ListenerType::Http.into(),
3133 from_scm: false,
3134 })
3135 .into(),
3136 );
3137 }
3138
3139 for front in self
3140 .https_listeners
3141 .iter()
3142 .filter(|(_, listener)| listener.active)
3143 .map(|(k, _)| k)
3144 {
3145 v.push(
3146 RequestType::ActivateListener(ActivateListener {
3147 interface: front.interface.clone(),
3148 address: SocketAddress::from(front.address),
3149 proxy: ListenerType::Https.into(),
3150 from_scm: false,
3151 })
3152 .into(),
3153 );
3154 }
3155 for front in self
3156 .tcp_listeners
3157 .iter()
3158 .filter(|(_, listener)| listener.active)
3159 .map(|(k, _)| k)
3160 {
3161 v.push(
3162 RequestType::ActivateListener(ActivateListener {
3163 interface: front.interface.clone(),
3164 address: SocketAddress::from(front.address),
3165 proxy: ListenerType::Tcp.into(),
3166 from_scm: false,
3167 })
3168 .into(),
3169 );
3170 }
3171 for front in self
3172 .udp_listeners
3173 .iter()
3174 .filter(|(_, listener)| listener.active)
3175 .map(|(k, _)| k)
3176 {
3177 v.push(
3178 RequestType::ActivateListener(ActivateListener {
3179 interface: front.interface.clone(),
3180 address: SocketAddress::from(front.address),
3181 proxy: ListenerType::Udp.into(),
3182 from_scm: false,
3183 })
3184 .into(),
3185 );
3186 }
3187
3188 #[cfg(debug_assertions)]
3191 {
3192 let active_listeners = self.http_listeners.values().filter(|l| l.active).count()
3193 + self.https_listeners.values().filter(|l| l.active).count()
3194 + self.tcp_listeners.values().filter(|l| l.active).count()
3195 + self.udp_listeners.values().filter(|l| l.active).count();
3196 debug_assert_eq!(
3197 v.len(),
3198 active_listeners,
3199 "generate_activate_requests emits one request per active listener"
3200 );
3201 debug_assert!(
3202 v.iter()
3203 .all(|r| matches!(r.request_type, Some(RequestType::ActivateListener(_)))),
3204 "generate_activate_requests must emit only ActivateListener requests"
3205 );
3206 }
3207
3208 v
3209 }
3210
3211 pub fn diff(&self, other: &ConfigState) -> Vec<Request> {
3212 let my_tcp_listeners: HashSet<&ListenerKey> = self.tcp_listeners.keys().collect();
3213 let their_tcp_listeners: HashSet<&ListenerKey> = other.tcp_listeners.keys().collect();
3214 let removed_tcp_listeners = my_tcp_listeners.difference(&their_tcp_listeners);
3215 let added_tcp_listeners = their_tcp_listeners.difference(&my_tcp_listeners);
3216
3217 let my_udp_listeners: HashSet<&ListenerKey> = self.udp_listeners.keys().collect();
3218 let their_udp_listeners: HashSet<&ListenerKey> = other.udp_listeners.keys().collect();
3219 let removed_udp_listeners = my_udp_listeners.difference(&their_udp_listeners);
3220 let added_udp_listeners = their_udp_listeners.difference(&my_udp_listeners);
3221
3222 let my_http_listeners: HashSet<&ListenerKey> = self.http_listeners.keys().collect();
3223 let their_http_listeners: HashSet<&ListenerKey> = other.http_listeners.keys().collect();
3224 let removed_http_listeners = my_http_listeners.difference(&their_http_listeners);
3225 let added_http_listeners = their_http_listeners.difference(&my_http_listeners);
3226
3227 let my_https_listeners: HashSet<&ListenerKey> = self.https_listeners.keys().collect();
3228 let their_https_listeners: HashSet<&ListenerKey> = other.https_listeners.keys().collect();
3229 let removed_https_listeners = my_https_listeners.difference(&their_https_listeners);
3230 let added_https_listeners = their_https_listeners.difference(&my_https_listeners);
3231
3232 let mut v: Vec<Request> = vec![];
3233
3234 for address in removed_tcp_listeners {
3235 if self.tcp_listeners[*address].active {
3236 v.push(
3237 RequestType::DeactivateListener(DeactivateListener {
3238 interface: address.interface.clone(),
3239 address: SocketAddress::from(address.address),
3240 proxy: ListenerType::Tcp.into(),
3241 to_scm: false,
3242 })
3243 .into(),
3244 );
3245 }
3246
3247 v.push(
3248 RequestType::RemoveListener(RemoveListener {
3249 interface: address.interface.clone(),
3250 address: SocketAddress::from(address.address),
3251 proxy: ListenerType::Tcp.into(),
3252 })
3253 .into(),
3254 );
3255 }
3256
3257 for address in added_tcp_listeners.clone() {
3258 v.push(RequestType::AddTcpListener(other.tcp_listeners[*address].clone()).into());
3259
3260 if other.tcp_listeners[*address].active {
3261 v.push(
3262 RequestType::ActivateListener(ActivateListener {
3263 interface: address.interface.clone(),
3264 address: SocketAddress::from(address.address),
3265 proxy: ListenerType::Tcp.into(),
3266 from_scm: false,
3267 })
3268 .into(),
3269 );
3270 }
3271 }
3272
3273 for address in removed_udp_listeners {
3274 if self.udp_listeners[*address].active {
3275 v.push(
3276 RequestType::DeactivateListener(DeactivateListener {
3277 interface: address.interface.clone(),
3278 address: SocketAddress::from(address.address),
3279 proxy: ListenerType::Udp.into(),
3280 to_scm: false,
3281 })
3282 .into(),
3283 );
3284 }
3285
3286 v.push(
3287 RequestType::RemoveListener(RemoveListener {
3288 interface: address.interface.clone(),
3289 address: SocketAddress::from(address.address),
3290 proxy: ListenerType::Udp.into(),
3291 })
3292 .into(),
3293 );
3294 }
3295
3296 for address in added_udp_listeners.clone() {
3297 v.push(RequestType::AddUdpListener(other.udp_listeners[*address].clone()).into());
3298
3299 if other.udp_listeners[*address].active {
3300 v.push(
3301 RequestType::ActivateListener(ActivateListener {
3302 interface: address.interface.clone(),
3303 address: SocketAddress::from(address.address),
3304 proxy: ListenerType::Udp.into(),
3305 from_scm: false,
3306 })
3307 .into(),
3308 );
3309 }
3310 }
3311
3312 for address in removed_http_listeners {
3313 if self.http_listeners[*address].active {
3314 v.push(
3315 RequestType::DeactivateListener(DeactivateListener {
3316 interface: address.interface.clone(),
3317 address: SocketAddress::from(address.address),
3318 proxy: ListenerType::Http.into(),
3319 to_scm: false,
3320 })
3321 .into(),
3322 );
3323 }
3324
3325 v.push(
3326 RequestType::RemoveListener(RemoveListener {
3327 interface: address.interface.clone(),
3328 address: SocketAddress::from(address.address),
3329 proxy: ListenerType::Http.into(),
3330 })
3331 .into(),
3332 );
3333 }
3334
3335 for address in added_http_listeners.clone() {
3336 v.push(RequestType::AddHttpListener(other.http_listeners[*address].clone()).into());
3337
3338 if other.http_listeners[*address].active {
3339 v.push(
3340 RequestType::ActivateListener(ActivateListener {
3341 interface: address.interface.clone(),
3342 address: SocketAddress::from(address.address),
3343 proxy: ListenerType::Http.into(),
3344 from_scm: false,
3345 })
3346 .into(),
3347 );
3348 }
3349 }
3350
3351 for address in removed_https_listeners {
3352 if self.https_listeners[*address].active {
3353 v.push(
3354 RequestType::DeactivateListener(DeactivateListener {
3355 interface: address.interface.clone(),
3356 address: SocketAddress::from(address.address),
3357 proxy: ListenerType::Https.into(),
3358 to_scm: false,
3359 })
3360 .into(),
3361 );
3362 }
3363
3364 v.push(
3365 RequestType::RemoveListener(RemoveListener {
3366 interface: address.interface.clone(),
3367 address: SocketAddress::from(address.address),
3368 proxy: ListenerType::Https.into(),
3369 })
3370 .into(),
3371 );
3372 }
3373
3374 for address in added_https_listeners.clone() {
3375 v.push(RequestType::add_https_listener(other.https_listeners[*address].clone()).into());
3376
3377 if other.https_listeners[*address].active {
3378 v.push(
3379 RequestType::ActivateListener(ActivateListener {
3380 interface: address.interface.clone(),
3381 address: SocketAddress::from(address.address),
3382 proxy: ListenerType::Https.into(),
3383 from_scm: false,
3384 })
3385 .into(),
3386 );
3387 }
3388 }
3389
3390 for addr in my_tcp_listeners.intersection(&their_tcp_listeners) {
3391 let my_listener = &self.tcp_listeners[*addr];
3392 let their_listener = &other.tcp_listeners[*addr];
3393
3394 if my_listener != their_listener {
3395 v.push(
3396 RequestType::RemoveListener(RemoveListener {
3397 interface: addr.interface.clone(),
3398 address: SocketAddress::from(addr.address),
3399 proxy: ListenerType::Tcp.into(),
3400 })
3401 .into(),
3402 );
3403 let mut listener_to_add = their_listener.clone();
3405 listener_to_add.active = false;
3406 v.push(RequestType::AddTcpListener(listener_to_add).into());
3407
3408 if their_listener.active {
3415 v.push(
3416 RequestType::ActivateListener(ActivateListener {
3417 interface: addr.interface.clone(),
3418 address: SocketAddress::from(addr.address),
3419 proxy: ListenerType::Tcp.into(),
3420 from_scm: false,
3421 })
3422 .into(),
3423 );
3424 }
3425 }
3426
3427 if my_listener.active && !their_listener.active {
3428 v.push(
3429 RequestType::DeactivateListener(DeactivateListener {
3430 interface: addr.interface.clone(),
3431 address: SocketAddress::from(addr.address),
3432 proxy: ListenerType::Tcp.into(),
3433 to_scm: false,
3434 })
3435 .into(),
3436 );
3437 }
3438 }
3439
3440 for addr in my_udp_listeners.intersection(&their_udp_listeners) {
3441 let my_listener = &self.udp_listeners[*addr];
3442 let their_listener = &other.udp_listeners[*addr];
3443
3444 if my_listener != their_listener {
3445 v.push(
3446 RequestType::RemoveListener(RemoveListener {
3447 interface: addr.interface.clone(),
3448 address: SocketAddress::from(addr.address),
3449 proxy: ListenerType::Udp.into(),
3450 })
3451 .into(),
3452 );
3453 let mut listener_to_add = their_listener.clone();
3455 listener_to_add.active = false;
3456 v.push(RequestType::AddUdpListener(listener_to_add).into());
3457
3458 if their_listener.active {
3465 v.push(
3466 RequestType::ActivateListener(ActivateListener {
3467 interface: addr.interface.clone(),
3468 address: SocketAddress::from(addr.address),
3469 proxy: ListenerType::Udp.into(),
3470 from_scm: false,
3471 })
3472 .into(),
3473 );
3474 }
3475 }
3476
3477 if my_listener.active && !their_listener.active {
3478 v.push(
3479 RequestType::DeactivateListener(DeactivateListener {
3480 interface: addr.interface.clone(),
3481 address: SocketAddress::from(addr.address),
3482 proxy: ListenerType::Udp.into(),
3483 to_scm: false,
3484 })
3485 .into(),
3486 );
3487 }
3488 }
3489
3490 for addr in my_http_listeners.intersection(&their_http_listeners) {
3491 let my_listener = &self.http_listeners[*addr];
3492 let their_listener = &other.http_listeners[*addr];
3493
3494 if my_listener != their_listener {
3495 v.push(
3496 RequestType::RemoveListener(RemoveListener {
3497 interface: addr.interface.clone(),
3498 address: SocketAddress::from(addr.address),
3499 proxy: ListenerType::Http.into(),
3500 })
3501 .into(),
3502 );
3503 let mut listener_to_add = their_listener.clone();
3505 listener_to_add.active = false;
3506 v.push(RequestType::AddHttpListener(listener_to_add).into());
3507
3508 if their_listener.active {
3515 v.push(
3516 RequestType::ActivateListener(ActivateListener {
3517 interface: addr.interface.clone(),
3518 address: SocketAddress::from(addr.address),
3519 proxy: ListenerType::Http.into(),
3520 from_scm: false,
3521 })
3522 .into(),
3523 );
3524 }
3525 }
3526
3527 if my_listener.active && !their_listener.active {
3528 v.push(
3529 RequestType::DeactivateListener(DeactivateListener {
3530 interface: addr.interface.clone(),
3531 address: SocketAddress::from(addr.address),
3532 proxy: ListenerType::Http.into(),
3533 to_scm: false,
3534 })
3535 .into(),
3536 );
3537 }
3538 }
3539
3540 for addr in my_https_listeners.intersection(&their_https_listeners) {
3541 let my_listener = &self.https_listeners[*addr];
3542 let their_listener = &other.https_listeners[*addr];
3543
3544 if my_listener != their_listener {
3545 v.push(
3546 RequestType::RemoveListener(RemoveListener {
3547 interface: addr.interface.clone(),
3548 address: SocketAddress::from(addr.address),
3549 proxy: ListenerType::Https.into(),
3550 })
3551 .into(),
3552 );
3553 let mut listener_to_add = their_listener.clone();
3555 listener_to_add.active = false;
3556 v.push(RequestType::add_https_listener(listener_to_add).into());
3557
3558 if their_listener.active {
3565 v.push(
3566 RequestType::ActivateListener(ActivateListener {
3567 interface: addr.interface.clone(),
3568 address: SocketAddress::from(addr.address),
3569 proxy: ListenerType::Https.into(),
3570 from_scm: false,
3571 })
3572 .into(),
3573 );
3574 }
3575 }
3576
3577 if my_listener.active && !their_listener.active {
3578 v.push(
3579 RequestType::DeactivateListener(DeactivateListener {
3580 interface: addr.interface.clone(),
3581 address: SocketAddress::from(addr.address),
3582 proxy: ListenerType::Https.into(),
3583 to_scm: false,
3584 })
3585 .into(),
3586 );
3587 }
3588 }
3589
3590 let mut removed_clusters: Vec<&ClusterId> = Vec::new();
3591 let mut deferred_clusters: Vec<Request> = Vec::new();
3596 for (cluster_id, res) in diff_map(self.clusters.iter(), other.clusters.iter()) {
3597 match res {
3598 DiffResult::Added | DiffResult::Changed => {
3599 let add: Request =
3600 RequestType::AddCluster(other.clusters.get(cluster_id).unwrap().clone())
3601 .into();
3602 if add.request_type.as_ref().is_some_and(|request| {
3603 self.health_check_port_unix_conflict(request).is_some()
3604 }) {
3605 deferred_clusters.push(add);
3606 } else {
3607 v.push(add);
3608 }
3609 }
3610 DiffResult::Removed => {
3611 removed_clusters.push(cluster_id);
3612 v.push(RequestType::RemoveCluster(cluster_id.to_string()).into())
3613 }
3614 }
3615 }
3616
3617 let base: Cow<'_, ConfigState> = if removed_clusters.is_empty() {
3624 Cow::Borrowed(self)
3625 } else {
3626 let mut cascaded = self.clone();
3627 for cluster_id in &removed_clusters {
3628 let removed = cascaded.remove_cluster(cluster_id);
3629 debug_assert!(
3630 removed.is_ok(),
3631 "a cluster diff_map reports removed must exist in self"
3632 );
3633 }
3634 Cow::Owned(cascaded)
3635 };
3636
3637 for ((cluster_id, backend_id), res) in diff_map(
3638 base.backends.iter().flat_map(|(cluster_id, v)| {
3639 v.iter()
3640 .map(move |backend| ((cluster_id, &backend.backend_id), backend))
3641 }),
3642 other.backends.iter().flat_map(|(cluster_id, v)| {
3643 v.iter()
3644 .map(move |backend| ((cluster_id, &backend.backend_id), backend))
3645 }),
3646 ) {
3647 match res {
3648 DiffResult::Added => {
3649 let backend = other
3650 .backends
3651 .get(cluster_id)
3652 .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
3653 .unwrap();
3654 v.push(RequestType::add_backend(backend.clone().to_add_backend()).into());
3655 }
3656 DiffResult::Removed => {
3657 let backend = base
3658 .backends
3659 .get(cluster_id)
3660 .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
3661 .unwrap();
3662
3663 v.push(
3664 RequestType::RemoveBackend(RemoveBackend {
3665 cluster_id: backend.cluster_id.to_string(),
3666 backend_id: backend.backend_id.clone(),
3667 address: SocketAddress::from(backend.address),
3668 })
3669 .into(),
3670 );
3671 }
3672 DiffResult::Changed => {
3673 let backend = base
3674 .backends
3675 .get(cluster_id)
3676 .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
3677 .unwrap();
3678
3679 v.push(
3680 RequestType::RemoveBackend(RemoveBackend {
3681 cluster_id: backend.cluster_id.to_string(),
3682 backend_id: backend.backend_id.clone(),
3683 address: SocketAddress::from(backend.address),
3684 })
3685 .into(),
3686 );
3687
3688 let backend = other
3689 .backends
3690 .get(cluster_id)
3691 .and_then(|v| v.iter().find(|b| &b.backend_id == backend_id))
3692 .unwrap();
3693 v.push(RequestType::add_backend(backend.clone().to_add_backend()).into());
3694 }
3695 }
3696 }
3697
3698 v.append(&mut deferred_clusters);
3699
3700 let mut my_http_fronts: HashSet<(&str, &HttpFrontend)> = HashSet::new();
3701 for (route, front) in base.http_fronts.iter() {
3702 my_http_fronts.insert((route, front));
3703 }
3704 let mut their_http_fronts: HashSet<(&str, &HttpFrontend)> = HashSet::new();
3705 for (route, front) in other.http_fronts.iter() {
3706 their_http_fronts.insert((route, front));
3707 }
3708
3709 let removed_http_fronts = my_http_fronts.difference(&their_http_fronts);
3710 let added_http_fronts = their_http_fronts.difference(&my_http_fronts);
3711
3712 for &(_, front) in removed_http_fronts {
3713 v.push(RequestType::remove_http_frontend(front.clone().into()).into());
3714 }
3715
3716 for &(_, front) in added_http_fronts {
3717 v.push(RequestType::add_http_frontend(front.clone().into()).into());
3718 }
3719
3720 let mut my_https_fronts: HashSet<(&String, &HttpFrontend)> = HashSet::new();
3721 for (route, front) in base.https_fronts.iter() {
3722 my_https_fronts.insert((route, front));
3723 }
3724 let mut their_https_fronts: HashSet<(&String, &HttpFrontend)> = HashSet::new();
3725 for (route, front) in other.https_fronts.iter() {
3726 their_https_fronts.insert((route, front));
3727 }
3728 let removed_https_fronts = my_https_fronts.difference(&their_https_fronts);
3729 let added_https_fronts = their_https_fronts.difference(&my_https_fronts);
3730
3731 for &(_, front) in removed_https_fronts {
3732 v.push(RequestType::remove_https_frontend(front.clone().into()).into());
3733 }
3734
3735 for &(_, front) in added_https_fronts {
3736 v.push(RequestType::add_https_frontend(front.clone().into()).into());
3737 }
3738
3739 let mut my_tcp_fronts: HashSet<(&ClusterId, &TcpFrontend)> = HashSet::new();
3740 for (cluster_id, front_list) in base.tcp_fronts.iter() {
3741 for front in front_list.iter() {
3742 my_tcp_fronts.insert((cluster_id, front));
3743 }
3744 }
3745 let mut their_tcp_fronts: HashSet<(&ClusterId, &TcpFrontend)> = HashSet::new();
3746 for (cluster_id, front_list) in other.tcp_fronts.iter() {
3747 for front in front_list.iter() {
3748 their_tcp_fronts.insert((cluster_id, front));
3749 }
3750 }
3751
3752 let removed_tcp_fronts = my_tcp_fronts.difference(&their_tcp_fronts);
3753 let added_tcp_fronts = their_tcp_fronts.difference(&my_tcp_fronts);
3754
3755 for &(_, front) in removed_tcp_fronts {
3756 v.push(RequestType::RemoveTcpFrontend(front.clone().into()).into());
3757 }
3758
3759 for &(_, front) in added_tcp_fronts {
3760 v.push(RequestType::AddTcpFrontend(front.clone().into()).into());
3761 }
3762
3763 let mut my_udp_fronts: HashSet<(&ClusterId, &UdpFrontend)> = HashSet::new();
3764 for (cluster_id, front_list) in base.udp_fronts.iter() {
3765 for front in front_list.iter() {
3766 my_udp_fronts.insert((cluster_id, front));
3767 }
3768 }
3769 let mut their_udp_fronts: HashSet<(&ClusterId, &UdpFrontend)> = HashSet::new();
3770 for (cluster_id, front_list) in other.udp_fronts.iter() {
3771 for front in front_list.iter() {
3772 their_udp_fronts.insert((cluster_id, front));
3773 }
3774 }
3775
3776 let removed_udp_fronts = my_udp_fronts.difference(&their_udp_fronts);
3777 let added_udp_fronts = their_udp_fronts.difference(&my_udp_fronts);
3778
3779 for &(_, front) in removed_udp_fronts {
3780 v.push(RequestType::RemoveUdpFrontend(front.clone().into()).into());
3781 }
3782
3783 for &(_, front) in added_udp_fronts {
3784 v.push(RequestType::AddUdpFrontend(front.clone().into()).into());
3785 }
3786
3787 let mut certificate_shadow = ConfigState {
3794 certificates: self.certificates.to_owned(),
3795 certificate_metadata: self.certificate_metadata.to_owned(),
3796 ..Default::default()
3797 };
3798 let certificate_addresses: BTreeSet<SocketAddr> = self
3799 .certificates
3800 .keys()
3801 .chain(other.certificates.keys())
3802 .copied()
3803 .collect();
3804
3805 for address in certificate_addresses {
3806 let normalized_metadata =
3812 certificate_shadow.normalized_certificate_metadata_for_address(address);
3813 if normalized_metadata.entries.is_empty() {
3814 certificate_shadow.certificate_metadata.remove(&address);
3815 } else {
3816 certificate_shadow
3817 .certificate_metadata
3818 .insert(address, normalized_metadata);
3819 }
3820 let target: Vec<(Fingerprint, AddCertificate)> = other
3821 .ordered_certificate_entries(address)
3822 .into_iter()
3823 .map(|(fingerprint, certificate, metadata)| {
3824 (
3825 fingerprint,
3826 AddCertificate {
3827 address: SocketAddress::from(address),
3828 certificate: certificate.to_owned(),
3829 expired_at: metadata.expired_at,
3830 },
3831 )
3832 })
3833 .collect();
3834 let target_fingerprints: BTreeSet<Fingerprint> = target
3835 .iter()
3836 .map(|(fingerprint, _)| fingerprint.to_owned())
3837 .collect();
3838 let current_fingerprints: BTreeSet<Fingerprint> = certificate_shadow
3839 .certificates
3840 .get(&address)
3841 .into_iter()
3842 .flat_map(HashMap::keys)
3843 .map(ToOwned::to_owned)
3844 .collect();
3845
3846 for fingerprint in current_fingerprints.difference(&target_fingerprints) {
3847 let request: Request = RequestType::RemoveCertificate(RemoveCertificate {
3848 address: SocketAddress::from(address),
3849 fingerprint: fingerprint.to_string(),
3850 })
3851 .into();
3852 let result = certificate_shadow.dispatch(&request);
3853 debug_assert!(
3854 result.is_ok(),
3855 "certificate removal diff must replay: {result:?}"
3856 );
3857 v.push(request);
3858 }
3859
3860 for (fingerprint, add) in &target {
3861 let current_certificate = certificate_shadow
3862 .certificates
3863 .get(&address)
3864 .and_then(|entries| entries.get(fingerprint));
3865 let current_expired_at = certificate_shadow
3866 .certificate_metadata
3867 .get(&address)
3868 .and_then(|index| index.entries.get(fingerprint))
3869 .and_then(|metadata| metadata.expired_at);
3870
3871 let request_type = match current_certificate {
3872 None => None,
3877 Some(current)
3878 if current != &add.certificate || current_expired_at != add.expired_at =>
3879 {
3880 Some(RequestType::ReplaceCertificate(ReplaceCertificate {
3881 address: SocketAddress::from(address),
3882 new_certificate: add.certificate.to_owned(),
3883 old_fingerprint: fingerprint.to_string(),
3884 new_expired_at: add.expired_at,
3885 }))
3886 }
3887 Some(_) => None,
3888 };
3889
3890 if let Some(request_type) = request_type {
3891 let request: Request = request_type.into();
3892 let result = certificate_shadow.dispatch(&request);
3893 debug_assert!(
3894 result.is_ok(),
3895 "certificate update diff must replay: {result:?}"
3896 );
3897 v.push(request);
3898 }
3899 }
3900
3901 let target_order: Vec<Fingerprint> = target
3902 .iter()
3903 .map(|(fingerprint, _)| fingerprint.to_owned())
3904 .collect();
3905 let shadow_order: Vec<Fingerprint> = certificate_shadow
3906 .ordered_certificate_entries(address)
3907 .into_iter()
3908 .map(|(fingerprint, _, _)| fingerprint)
3909 .collect();
3910 if shadow_order != target_order {
3911 let mut shadow_cursor = 0usize;
3923 let mut retained_prefix_len = 0usize;
3924 for fingerprint in &target_order {
3925 let Some(offset) = shadow_order[shadow_cursor..]
3926 .iter()
3927 .position(|candidate| candidate == fingerprint)
3928 else {
3929 break;
3930 };
3931 shadow_cursor += offset + 1;
3932 retained_prefix_len += 1;
3933 }
3934
3935 for (fingerprint, add) in target.iter().skip(retained_prefix_len) {
3936 let already_present = certificate_shadow
3937 .certificates
3938 .get(&address)
3939 .is_some_and(|certificates| certificates.contains_key(fingerprint));
3940 let mut request_types = Vec::with_capacity(usize::from(already_present) + 1);
3941 if already_present {
3942 request_types.push(RequestType::RemoveCertificate(RemoveCertificate {
3943 address: SocketAddress::from(address),
3944 fingerprint: fingerprint.to_string(),
3945 }));
3946 }
3947 request_types.push(RequestType::AddCertificate(add.to_owned()));
3948 for request_type in request_types {
3949 let request: Request = request_type.into();
3950 let result = certificate_shadow.dispatch(&request);
3951 debug_assert!(
3952 result.is_ok(),
3953 "certificate order diff must replay: {result:?}"
3954 );
3955 v.push(request);
3956 }
3957 }
3958 }
3959
3960 debug_assert_eq!(
3961 certificate_shadow.certificate_add_requests_for_address(address),
3962 other.certificate_add_requests_for_address(address),
3963 "certificate diff must converge payload, expiration and insertion order"
3964 );
3965 }
3966
3967 for address in added_tcp_listeners {
3968 let listener = &other.tcp_listeners[*address];
3969 if listener.active {
3970 v.push(
3971 RequestType::ActivateListener(ActivateListener {
3972 interface: listener.interface.clone(),
3973 address: listener.address,
3974 proxy: ListenerType::Tcp.into(),
3975 from_scm: false,
3976 })
3977 .into(),
3978 );
3979 }
3980 }
3981
3982 for address in added_udp_listeners {
3983 let listener = &other.udp_listeners[*address];
3984 if listener.active {
3985 v.push(
3986 RequestType::ActivateListener(ActivateListener {
3987 interface: listener.interface.clone(),
3988 address: listener.address,
3989 proxy: ListenerType::Udp.into(),
3990 from_scm: false,
3991 })
3992 .into(),
3993 );
3994 }
3995 }
3996
3997 #[cfg(debug_assertions)]
4010 {
4011 let mut replayed = self.clone();
4012 for request in &v {
4013 debug_assert!(
4015 replayed.dispatch(request).is_ok(),
4016 "every request emitted by diff must replay cleanly onto self"
4017 );
4018 }
4019 let nonempty = |m: &BTreeMap<ClusterId, Vec<Backend>>| {
4020 m.iter()
4021 .filter(|(_, v)| !v.is_empty())
4022 .map(|(k, v)| (k.clone(), v.clone()))
4023 .collect::<BTreeMap<_, _>>()
4024 };
4025 let nonempty_tcp = |m: &HashMap<ClusterId, Vec<TcpFrontend>>| {
4026 m.iter()
4027 .filter(|(_, v)| !v.is_empty())
4028 .map(|(k, v)| (k.clone(), v.clone()))
4029 .collect::<HashMap<_, _>>()
4030 };
4031 debug_assert!(
4032 replayed.clusters == other.clusters
4033 && nonempty(&replayed.backends) == nonempty(&other.backends)
4034 && replayed.http_fronts == other.http_fronts
4035 && replayed.https_fronts == other.https_fronts
4036 && nonempty_tcp(&replayed.tcp_fronts) == nonempty_tcp(&other.tcp_fronts)
4037 && replayed.certificates == other.certificates
4038 && replayed.normalized_certificate_metadata()
4042 == other.normalized_certificate_metadata()
4043 && replayed.http_listeners == other.http_listeners
4044 && replayed.https_listeners == other.https_listeners
4045 && replayed.tcp_listeners == other.tcp_listeners
4046 && replayed.udp_listeners == other.udp_listeners,
4047 "replaying diff(self, other) onto self must reproduce other's clusters/backends/frontends/certificates/listeners"
4048 );
4049 }
4050
4051 v
4052 }
4053
4054 pub fn hash_state(&self) -> BTreeMap<ClusterId, u64> {
4056 let mut hm: HashMap<ClusterId, DefaultHasher> = self
4057 .clusters
4058 .keys()
4059 .map(|cluster_id| {
4060 let mut hasher = DefaultHasher::new();
4061 self.clusters.get(cluster_id).hash(&mut hasher);
4062 if let Some(backends) = self.backends.get(cluster_id) {
4063 backends.iter().collect::<BTreeSet<_>>().hash(&mut hasher)
4064 }
4065 if let Some(tcp_fronts) = self.tcp_fronts.get(cluster_id) {
4066 tcp_fronts.iter().collect::<BTreeSet<_>>().hash(&mut hasher)
4067 }
4068 (cluster_id.to_owned(), hasher)
4069 })
4070 .collect();
4071
4072 for front in self.http_fronts.values() {
4073 if let Some(cluster_id) = &front.cluster_id
4074 && let Some(hasher) = hm.get_mut(cluster_id)
4075 {
4076 front.hash(hasher);
4077 }
4078 }
4079
4080 for front in self.https_fronts.values() {
4081 if let Some(cluster_id) = &front.cluster_id
4082 && let Some(hasher) = hm.get_mut(cluster_id)
4083 {
4084 front.hash(hasher);
4085 }
4086 }
4087
4088 hm.drain()
4089 .map(|(cluster_id, hasher)| (cluster_id, hasher.finish()))
4090 .collect()
4091 }
4092
4093 pub fn cluster_state(&self, cluster_id: &str) -> Option<ClusterInformation> {
4096 let configuration = self.clusters.get(cluster_id).cloned()?;
4097 info!("{:?}", configuration);
4098
4099 let http_frontends: Vec<RequestHttpFrontend> = self
4100 .http_fronts
4101 .values()
4102 .filter(|front| front.cluster_id.as_deref() == Some(cluster_id))
4103 .map(|front| front.clone().into())
4104 .collect();
4105
4106 let https_frontends: Vec<RequestHttpFrontend> = self
4107 .https_fronts
4108 .values()
4109 .filter(|front| front.cluster_id.as_deref() == Some(cluster_id))
4110 .map(|front| front.clone().into())
4111 .collect();
4112
4113 let tcp_frontends: Vec<RequestTcpFrontend> = self
4114 .tcp_fronts
4115 .get(cluster_id)
4116 .cloned()
4117 .unwrap_or_default()
4118 .iter()
4119 .map(|front| front.clone().into())
4120 .collect();
4121
4122 let udp_frontends: Vec<RequestUdpFrontend> = self
4123 .udp_fronts
4124 .get(cluster_id)
4125 .cloned()
4126 .unwrap_or_default()
4127 .iter()
4128 .map(|front| front.clone().into())
4129 .collect();
4130
4131 let backends: Vec<AddBackend> = self
4132 .backends
4133 .get(cluster_id)
4134 .cloned()
4135 .unwrap_or_default()
4136 .iter()
4137 .map(|backend| backend.clone().into())
4138 .collect();
4139
4140 Some(ClusterInformation {
4141 configuration: Some(configuration),
4142 http_frontends,
4143 https_frontends,
4144 tcp_frontends,
4145 backends,
4146 udp_frontends,
4147 })
4148 }
4149
4150 pub fn count_backends(&self) -> usize {
4151 self.backends.values().fold(0, |acc, v| acc + v.len())
4152 }
4153
4154 pub fn count_frontends(&self) -> usize {
4155 self.http_fronts.values().count()
4156 + self.https_fronts.values().count()
4157 + self.tcp_fronts.values().fold(0, |acc, v| acc + v.len())
4158 + self.udp_fronts.values().fold(0, |acc, v| acc + v.len())
4159 }
4160
4161 pub fn get_cluster_ids_by_domain(
4162 &self,
4163 hostname: String,
4164 path: Option<String>,
4165 ) -> HashSet<ClusterId> {
4166 let mut cluster_ids: HashSet<ClusterId> = HashSet::new();
4167
4168 self.http_fronts.values().for_each(|front| {
4169 if domain_check(&front.hostname, &front.path, &hostname, &path)
4170 && let Some(id) = &front.cluster_id
4171 {
4172 cluster_ids.insert(id.clone());
4173 }
4174 });
4175
4176 self.https_fronts.values().for_each(|front| {
4177 if domain_check(&front.hostname, &front.path, &hostname, &path)
4178 && let Some(id) = &front.cluster_id
4179 {
4180 cluster_ids.insert(id.clone());
4181 }
4182 });
4183
4184 cluster_ids
4185 }
4186
4187 pub fn get_certificates(
4205 &self,
4206 filters: QueryCertificatesFilters,
4207 ) -> BTreeMap<String, CertificateAndKey> {
4208 self.certificates
4209 .values()
4210 .flat_map(|hash_map| hash_map.iter())
4211 .filter(|(fingerprint, cert)| {
4212 if let Some(domain) = &filters.domain {
4213 cert.names.contains(domain)
4214 } else if let Some(f) = &filters.fingerprint {
4215 fingerprint.to_string() == *f
4216 } else {
4217 true
4218 }
4219 })
4220 .map(|(fingerprint, cert)| (fingerprint.to_string(), cert.to_owned()))
4221 .collect()
4222 }
4223
4224 pub fn list_frontends(&self, filters: FrontendFilters) -> ListedFrontends {
4225 let list_all = !filters.http && !filters.https && !filters.tcp;
4227
4228 let matches_cluster = |cluster_id: Option<&str>| match &filters.cluster_id {
4231 Some(wanted) => cluster_id == Some(wanted.as_str()),
4232 None => true,
4233 };
4234 let matches_domain = |hostname: &str| match &filters.domain {
4235 Some(domain) => hostname.contains(domain),
4236 None => true,
4237 };
4238
4239 let mut listed_frontends = ListedFrontends::default();
4240
4241 if filters.http || list_all {
4242 for http_frontend in self.http_fronts.iter().filter(|f| {
4243 matches_domain(&f.1.hostname) && matches_cluster(f.1.cluster_id.as_deref())
4244 }) {
4245 listed_frontends
4246 .http_frontends
4247 .push(http_frontend.1.to_owned().into());
4248 }
4249 }
4250
4251 if filters.https || list_all {
4252 for https_frontend in self.https_fronts.iter().filter(|f| {
4253 matches_domain(&f.1.hostname) && matches_cluster(f.1.cluster_id.as_deref())
4254 }) {
4255 listed_frontends
4256 .https_frontends
4257 .push(https_frontend.1.to_owned().into());
4258 }
4259 }
4260
4261 if (filters.tcp || list_all) && filters.domain.is_none() {
4262 for tcp_frontend in fronts_of_cluster(&self.tcp_fronts, filters.cluster_id.as_deref()) {
4263 listed_frontends
4264 .tcp_frontends
4265 .push(tcp_frontend.to_owned().into())
4266 }
4267 }
4268
4269 if (filters.tcp || list_all) && filters.domain.is_none() {
4275 for udp_frontend in fronts_of_cluster(&self.udp_fronts, filters.cluster_id.as_deref()) {
4276 listed_frontends
4277 .udp_frontends
4278 .push(udp_frontend.to_owned().into())
4279 }
4280 }
4281
4282 listed_frontends
4283 }
4284
4285 pub fn list_listeners(&self) -> ListenersList {
4286 ListenersList {
4287 http_listeners: self
4288 .http_listeners
4289 .iter()
4290 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4291 .collect(),
4292 https_listeners: self
4293 .https_listeners
4294 .iter()
4295 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4296 .collect(),
4297 tcp_listeners: self
4298 .tcp_listeners
4299 .iter()
4300 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4301 .collect(),
4302 udp_listeners: self
4303 .udp_listeners
4304 .iter()
4305 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4306 .collect(),
4307 }
4308 }
4309
4310 pub fn produce_initial_state(&self) -> InitialState {
4312 let mut worker_requests = Vec::new();
4313 for (counter, request) in self.generate_requests().into_iter().enumerate() {
4314 worker_requests.push(WorkerRequest::new(format!("SAVE-{counter}"), request));
4315 }
4316 InitialState {
4317 requests: worker_requests,
4318 }
4319 }
4320
4321 pub fn write_initial_state_to_file(&self, file: &mut File) -> Result<usize, StateError> {
4324 let initial_state = self.produce_initial_state();
4325 let count = initial_state.requests.len();
4326
4327 let bytes_to_write = initial_state.encode_to_vec();
4328 println!("writing {} in the temp file", bytes_to_write.len());
4329 file.write_all(&bytes_to_write)
4330 .map_err(StateError::FileError)?;
4331
4332 file.sync_all().map_err(StateError::FileError)?;
4333
4334 Ok(count)
4335 }
4336
4337 pub fn write_requests_to_file(&self, file: &mut File) -> Result<usize, StateError> {
4341 let mut counter = 0usize;
4342 let requests = self.generate_requests();
4343
4344 for request in requests {
4345 let message = WorkerRequest::new(format!("SAVE-{counter}"), request);
4346
4347 file.write_all(
4348 &serde_json::to_string(&message)
4349 .map(|s| s.into_bytes())
4350 .unwrap_or_default(),
4351 )
4352 .map_err(StateError::FileError)?;
4353
4354 file.write_all(&b"\n\0"[..])
4355 .map_err(StateError::FileError)?;
4356
4357 if counter.is_multiple_of(1000) {
4358 info!("writing {} commands to file", counter);
4359 file.sync_all().map_err(StateError::FileError)?;
4360 }
4361 counter += 1;
4362 }
4363 file.sync_all().map_err(StateError::FileError)?;
4364
4365 Ok(counter)
4366 }
4367}
4368
4369macro_rules! require_h2_knob_floor {
4377 ($config:ident, $field:ident, $minimum:literal) => {
4378 if let Some(value) = $config.$field
4379 && u64::from(value) < $minimum
4380 {
4381 return Err(StateError::InvalidValue {
4382 field: stringify!($field),
4383 reason: concat!("must be >= ", stringify!($minimum)),
4384 });
4385 }
4386 };
4387}
4388
4389pub fn validate_h2_flood_knobs_http(patch: &UpdateHttpListenerConfig) -> Result<(), StateError> {
4407 crate::for_each_h2_knob_floor!(require_h2_knob_floor, patch);
4408 Ok(())
4409}
4410
4411pub fn validate_h2_flood_knobs_https(patch: &UpdateHttpsListenerConfig) -> Result<(), StateError> {
4413 crate::for_each_h2_knob_floor!(require_h2_knob_floor, patch);
4414 Ok(())
4415}
4416
4417pub fn validate_h2_flood_knobs_http_listener(
4441 config: &HttpListenerConfig,
4442) -> Result<(), StateError> {
4443 crate::for_each_h2_knob_floor!(require_h2_knob_floor, config);
4444 Ok(())
4445}
4446
4447pub fn validate_h2_flood_knobs_https_listener(
4451 config: &HttpsListenerConfig,
4452) -> Result<(), StateError> {
4453 crate::for_each_h2_knob_floor!(require_h2_knob_floor, config);
4454 Ok(())
4455}
4456
4457fn merge_answers(stored: &mut BTreeMap<String, String>, patch: &BTreeMap<String, String>) {
4467 for (code, body) in patch {
4468 if !body.is_empty() {
4469 stored.insert(code.to_owned(), body.to_owned());
4470 }
4471 }
4472}
4473
4474pub fn merge_custom_http_answers(
4482 target: &mut Option<CustomHttpAnswers>,
4483 patch: &CustomHttpAnswers,
4484) {
4485 let current = target.get_or_insert_with(CustomHttpAnswers::default);
4486 macro_rules! merge_field {
4487 ($field:ident) => {
4488 if let Some(ref v) = patch.$field {
4489 current.$field = Some(v.clone());
4490 }
4491 };
4492 }
4493 merge_field!(answer_301);
4494 merge_field!(answer_400);
4495 merge_field!(answer_401);
4496 merge_field!(answer_404);
4497 merge_field!(answer_408);
4498 merge_field!(answer_413);
4499 merge_field!(answer_421);
4500 merge_field!(answer_502);
4501 merge_field!(answer_503);
4502 merge_field!(answer_504);
4503 merge_field!(answer_507);
4504}
4505
4506pub fn validate_alpn_protocols(values: &[String]) -> Result<(), StateError> {
4509 for value in values {
4510 if value != "h2" && value != "http/1.1" {
4511 return Err(StateError::InvalidValue {
4512 field: "alpn_protocols",
4513 reason: "each value must be \"h2\" or \"http/1.1\"",
4514 });
4515 }
4516 }
4517 Ok(())
4518}
4519
4520pub fn validate_sozu_id_header(value: &str) -> Result<(), StateError> {
4531 if value.is_empty() {
4532 return Err(StateError::InvalidValue {
4533 field: "sozu_id_header",
4534 reason: "must not be empty",
4535 });
4536 }
4537 for b in value.bytes() {
4538 let is_tchar = b.is_ascii_alphanumeric()
4539 || matches!(
4540 b,
4541 b'!' | b'#'
4542 | b'$'
4543 | b'%'
4544 | b'&'
4545 | b'\''
4546 | b'*'
4547 | b'+'
4548 | b'-'
4549 | b'.'
4550 | b'^'
4551 | b'_'
4552 | b'`'
4553 | b'|'
4554 | b'~'
4555 );
4556 if !is_tchar {
4557 return Err(StateError::InvalidValue {
4558 field: "sozu_id_header",
4559 reason: "must be a valid HTTP header name (RFC 9110 §5.1 token: alphanumeric or one of !#$%&'*+-.^_`|~)",
4560 });
4561 }
4562 }
4563 Ok(())
4564}
4565
4566pub fn validate_hsts_patch(hsts: &HstsConfig) -> Result<(), StateError> {
4571 if hsts.enabled.is_none() {
4572 return Err(StateError::InvalidValue {
4573 field: "hsts",
4574 reason: "`enabled` is required whenever the hsts block is present",
4575 });
4576 }
4577 Ok(())
4578}
4579
4580pub fn validate_forwarded_headers(value: i32) -> Result<(), StateError> {
4585 ForwardedHeaders::try_from(value)
4586 .map(|_| ())
4587 .map_err(|_| StateError::InvalidValue {
4588 field: "forwarded_headers",
4589 reason: "must be one of both, x_forwarded, rfc7239, none",
4590 })
4591}
4592
4593fn fronts_of_cluster<'a, F>(
4597 fronts: &'a HashMap<ClusterId, Vec<F>>,
4598 cluster_id: Option<&str>,
4599) -> impl Iterator<Item = &'a F> {
4600 let (one, all) = match cluster_id {
4601 Some(cluster_id) => (fronts.get(cluster_id), None),
4602 None => (None, Some(fronts.values())),
4603 };
4604 one.into_iter().chain(all.into_iter().flatten()).flatten()
4605}
4606
4607fn domain_check(
4608 front_hostname: &str,
4609 front_path_rule: &PathRule,
4610 hostname: &str,
4611 path_prefix: &Option<String>,
4612) -> bool {
4613 if hostname != front_hostname {
4614 return false;
4615 }
4616
4617 if let Some(path) = &path_prefix {
4618 return path == &front_path_rule.value;
4619 }
4620
4621 true
4622}
4623
4624fn canonical_tcp_alpn(alpn: &[String]) -> Vec<String> {
4634 let mut alpn = alpn.to_vec();
4635 alpn.sort();
4636 alpn.dedup();
4637 debug_assert!(
4641 alpn.windows(2).all(|pair| pair[0] < pair[1]),
4642 "canonical_tcp_alpn must return a strictly sorted, duplicate-free list"
4643 );
4644 alpn
4645}
4646
4647fn tcp_frontend_matches(
4658 front: &TcpFrontend,
4659 address: SocketAddr,
4660 sni: &Option<String>,
4661 alpn: &[String],
4662) -> bool {
4663 front.address == address && front.sni == *sni && front.alpn.as_slice() == alpn
4664}
4665
4666struct DiffMap<'a, K: Ord, V, I1, I2> {
4667 my_it: I1,
4668 other_it: I2,
4669 my: Option<(K, &'a V)>,
4670 other: Option<(K, &'a V)>,
4671}
4672
4673fn diff_map<
4675 'a,
4676 K: Ord,
4677 V: PartialEq,
4678 I1: Iterator<Item = (K, &'a V)>,
4679 I2: Iterator<Item = (K, &'a V)>,
4680>(
4681 my: I1,
4682 other: I2,
4683) -> DiffMap<'a, K, V, I1, I2> {
4684 DiffMap {
4685 my_it: my,
4686 other_it: other,
4687 my: None,
4688 other: None,
4689 }
4690}
4691
4692enum DiffResult {
4693 Added,
4694 Removed,
4695 Changed,
4696}
4697
4698impl<'a, K: Ord, V: PartialEq, I1: Iterator<Item = (K, &'a V)>, I2: Iterator<Item = (K, &'a V)>>
4701 std::iter::Iterator for DiffMap<'a, K, V, I1, I2>
4702{
4703 type Item = (K, DiffResult);
4704
4705 fn next(&mut self) -> Option<Self::Item> {
4706 loop {
4707 if self.my.is_none() {
4708 self.my = self.my_it.next();
4709 }
4710 if self.other.is_none() {
4711 self.other = self.other_it.next();
4712 }
4713
4714 match (self.my.take(), self.other.take()) {
4715 (None, other) => return other.map(|(k, _)| (k, DiffResult::Added)),
4719 (Some((k, _)), None) => return Some((k, DiffResult::Removed)),
4722 (Some((k1, _v1)), Some((k2, v2))) if k1 < k2 => {
4724 self.other = Some((k2, v2));
4725 return Some((k1, DiffResult::Removed));
4726 }
4727 (Some((k1, v1)), Some((k2, _v2))) if k1 > k2 => {
4729 self.my = Some((k1, v1));
4730 return Some((k2, DiffResult::Added));
4731 }
4732 (Some((k1, v1)), Some((_k2, v2))) if v1 != v2 => {
4733 return Some((k1, DiffResult::Changed));
4736 }
4737 _ => {}
4738 }
4739 }
4740 }
4741}
4742
4743#[cfg(test)]
4744mod tests {
4745 use rand::{RngExt, rng, seq::SliceRandom};
4746
4747 use super::*;
4748 use crate::proto::command::{
4749 ClientAuthMode, CloseSessions, CustomHttpAnswers, Header, HeaderPosition, HstsConfig,
4750 LoadBalancingParams, PathRuleKind, RedirectPolicy, RedirectScheme, RequestHttpFrontend,
4751 RequestTcpFrontend, RequestUdpFrontend, RulePosition, UdpListenerConfig,
4752 UpdateUdpListenerConfig,
4753 };
4754
4755 fn capture_test_logs(run: impl FnOnce() + Send + 'static) -> String {
4756 let receiver = std::net::UdpSocket::bind("127.0.0.1:0")
4757 .expect("test log receiver must bind to a loopback port");
4758 let target = format!(
4759 "udp://{}",
4760 receiver
4761 .local_addr()
4762 .expect("test log receiver must have a local address")
4763 );
4764
4765 std::thread::spawn(move || {
4766 crate::logging::Logger::init(
4767 "state-log-redaction-test".to_owned(),
4768 "info",
4769 &target,
4770 false,
4771 None,
4772 None,
4773 None,
4774 )
4775 .expect("test logger must initialize");
4776 run();
4777 })
4778 .join()
4779 .expect("log-producing test thread must not panic");
4780
4781 receiver
4782 .set_nonblocking(true)
4783 .expect("test log receiver must become nonblocking");
4784 let mut output = String::new();
4785 let mut datagram = vec![0; 65_507];
4786 loop {
4787 match receiver.recv(&mut datagram) {
4788 Ok(length) => output.push_str(&String::from_utf8_lossy(&datagram[..length])),
4789 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
4790 Err(error) => panic!("test log receiver failed: {error}"),
4791 }
4792 }
4793 assert!(!output.is_empty(), "test log capture received no datagrams");
4794 output
4795 }
4796
4797 #[test]
4798 fn duplicate_certificate_log_redacts_names() {
4799 const NAME_SECRET: &str = "DUPLICATE_CERTIFICATE_NAME_SECRET_SENTINEL";
4800
4801 let name = format!("{NAME_SECRET}{}", "x".repeat(4096));
4802 let name_len = name.len();
4803 let certificate = CertificateAndKey {
4804 certificate: include_str!("../assets/certificate.pem").to_owned(),
4805 key: include_str!("../assets/key.pem").to_owned(),
4806 certificate_chain: Vec::new(),
4807 versions: Vec::new(),
4808 names: vec![name],
4809 };
4810 let fingerprint = certificate
4811 .fingerprint()
4812 .expect("test certificate fingerprint must be computable")
4813 .to_string();
4814 let output = capture_test_logs(move || {
4815 let mut state = ConfigState::default();
4816 let add = AddCertificate {
4817 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
4818 certificate,
4819 expired_at: None,
4820 };
4821
4822 state
4823 .add_certificate(&add)
4824 .expect("first certificate insertion must succeed");
4825 state
4826 .add_certificate(&add)
4827 .expect("duplicate certificate insertion must be skipped");
4828 });
4829
4830 assert!(
4831 !output.contains(NAME_SECRET),
4832 "duplicate certificate log leaked certificate name {NAME_SECRET}"
4833 );
4834 assert!(
4835 !output.contains(&fingerprint),
4836 "duplicate certificate log leaked certificate fingerprint {fingerprint}"
4837 );
4838 for metadata in [
4839 "fingerprint_bytes=32".to_owned(),
4840 "names_count=1".to_owned(),
4841 format!("names_bytes={name_len}"),
4842 ] {
4843 assert!(
4844 output.contains(&metadata),
4845 "duplicate certificate log omitted bounded metadata {metadata}: {output}"
4846 );
4847 }
4848 assert!(
4849 output.len() <= 512,
4850 "duplicate certificate log is not bounded: {} bytes",
4851 output.len()
4852 );
4853 }
4854
4855 #[test]
4866 fn replace_certificate_with_malformed_pem_keeps_old_certificate() {
4867 let certificate = CertificateAndKey {
4868 certificate: include_str!("../assets/certificate.pem").to_owned(),
4869 key: include_str!("../assets/key.pem").to_owned(),
4870 certificate_chain: Vec::new(),
4871 versions: Vec::new(),
4872 names: Vec::new(),
4873 };
4874 let old_fingerprint = certificate
4875 .fingerprint()
4876 .expect("test certificate fingerprint must be computable");
4877 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
4878
4879 let mut state = ConfigState::default();
4880 state
4881 .add_certificate(&AddCertificate {
4882 address,
4883 certificate,
4884 expired_at: None,
4885 })
4886 .expect("initial certificate insertion must succeed");
4887
4888 let malformed = CertificateAndKey {
4889 certificate: "not a valid pem certificate".to_owned(),
4890 key: include_str!("../assets/key.pem").to_owned(),
4891 certificate_chain: Vec::new(),
4892 versions: Vec::new(),
4893 names: Vec::new(),
4894 };
4895 let error = state
4896 .replace_certificate(&ReplaceCertificate {
4897 address,
4898 new_certificate: malformed,
4899 old_fingerprint: old_fingerprint.to_string(),
4900 new_expired_at: None,
4901 })
4902 .expect_err("replacing with a malformed PEM must fail");
4903 assert!(
4904 matches!(error, StateError::ReplaceCertificate(_)),
4905 "expected StateError::ReplaceCertificate, got {error:?}"
4906 );
4907
4908 let certificates_for_address = state
4909 .certificates
4910 .get(&SocketAddr::from(address))
4911 .expect("the address must still be present in the certificate map");
4912 assert!(
4913 certificates_for_address.contains_key(&old_fingerprint),
4914 "a failed replace must leave the original certificate in place, found {:?}",
4915 certificates_for_address.keys().collect::<Vec<_>>()
4916 );
4917 assert_eq!(
4918 certificates_for_address.len(),
4919 1,
4920 "a failed replace must not leave the address with zero (or extra) certificates"
4921 );
4922 }
4923
4924 #[test]
4931 fn replace_certificate_with_same_fingerprint_keeps_certificate() {
4932 let certificate = CertificateAndKey {
4933 certificate: include_str!("../assets/certificate.pem").to_owned(),
4934 key: include_str!("../assets/key.pem").to_owned(),
4935 certificate_chain: Vec::new(),
4936 versions: Vec::new(),
4937 names: Vec::new(),
4938 };
4939 let fingerprint = certificate
4940 .fingerprint()
4941 .expect("test certificate fingerprint must be computable");
4942 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
4943
4944 let mut state = ConfigState::default();
4945 state
4946 .add_certificate(&AddCertificate {
4947 address,
4948 certificate: certificate.clone(),
4949 expired_at: None,
4950 })
4951 .expect("initial certificate insertion must succeed");
4952
4953 state
4954 .replace_certificate(&ReplaceCertificate {
4955 address,
4956 new_certificate: certificate,
4957 old_fingerprint: fingerprint.to_string(),
4958 new_expired_at: None,
4959 })
4960 .expect("idempotent replace with the identical certificate must succeed");
4961
4962 let certificates_for_address = state
4963 .certificates
4964 .get(&SocketAddr::from(address))
4965 .expect("the address must still be present in the certificate map");
4966 assert!(
4967 certificates_for_address.contains_key(&fingerprint),
4968 "an idempotent replace (old fingerprint == new fingerprint) must not delete \
4969 the entry the caller meant to retain, found {:?}",
4970 certificates_for_address.keys().collect::<Vec<_>>()
4971 );
4972 assert_eq!(
4973 certificates_for_address.len(),
4974 1,
4975 "an idempotent replace must not change the number of stored certificates"
4976 );
4977 }
4978
4979 #[test]
4984 fn replace_certificate_with_empty_names_round_trips_through_saved_state() {
4985 let certificate = CertificateAndKey {
4986 certificate: include_str!("../assets/certificate.pem").to_owned(),
4987 key: include_str!("../assets/key.pem").to_owned(),
4988 certificate_chain: Vec::new(),
4989 versions: Vec::new(),
4990 names: Vec::new(),
4991 };
4992 let fingerprint = certificate
4993 .fingerprint()
4994 .expect("test certificate fingerprint must be computable");
4995 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
4996 let mut state = ConfigState::default();
4997 state
4998 .add_certificate(&AddCertificate {
4999 address,
5000 certificate: certificate.to_owned(),
5001 expired_at: None,
5002 })
5003 .expect("initial certificate insertion must succeed");
5004 assert!(
5005 !state.certificates[&SocketAddr::from(address)][&fingerprint]
5006 .names
5007 .is_empty(),
5008 "the fixture must expose at least one SAN or CN"
5009 );
5010
5011 state
5012 .replace_certificate(&ReplaceCertificate {
5013 address,
5014 new_certificate: certificate,
5015 old_fingerprint: fingerprint.to_string(),
5016 new_expired_at: None,
5017 })
5018 .expect("same-fingerprint replacement must succeed");
5019
5020 let requests = state.generate_requests();
5021 let mut replayed = ConfigState::default();
5022 for request in &requests {
5023 replayed
5024 .dispatch(request)
5025 .unwrap_or_else(|error| panic!("could not replay {request:?}: {error}"));
5026 }
5027 assert_eq!(
5028 replayed.certificates, state.certificates,
5029 "saved-state replay must preserve canonical certificate names"
5030 );
5031 }
5032
5033 fn ordered_saved_certificates(state: &ConfigState) -> Vec<(Fingerprint, Option<i64>)> {
5034 state
5035 .generate_requests()
5036 .into_iter()
5037 .filter_map(|request| match request.request_type {
5038 Some(RequestType::AddCertificate(add)) => Some((
5039 add.certificate
5040 .fingerprint()
5041 .expect("saved certificate fingerprint"),
5042 add.expired_at,
5043 )),
5044 _ => None,
5045 })
5046 .collect()
5047 }
5048
5049 fn distinct_test_certificates() -> (CertificateAndKey, CertificateAndKey) {
5050 (
5051 CertificateAndKey {
5052 certificate: include_str!("../../lib/assets/tests/certificate-1y.pem").to_owned(),
5053 key: include_str!("../../lib/assets/tests/key-1y.pem").to_owned(),
5054 names: vec!["shared.example.test".to_owned()],
5055 ..Default::default()
5056 },
5057 CertificateAndKey {
5058 certificate: include_str!("../../lib/assets/tests/certificate-2y.pem").to_owned(),
5059 key: include_str!("../../lib/assets/tests/key-2y.pem").to_owned(),
5060 names: vec!["shared.example.test".to_owned()],
5061 ..Default::default()
5062 },
5063 )
5064 }
5065
5066 fn third_test_certificate() -> CertificateAndKey {
5067 CertificateAndKey {
5068 certificate: include_str!("../../lib/assets/tests/certificate-3.pem").to_owned(),
5069 key: include_str!("../../lib/assets/tests/key.pem").to_owned(),
5070 names: vec!["shared.example.test".to_owned()],
5071 ..Default::default()
5072 }
5073 }
5074
5075 fn ordered_fingerprints(state: &ConfigState, address: SocketAddress) -> Vec<Fingerprint> {
5076 state
5077 .ordered_certificate_entries(address.into())
5078 .into_iter()
5079 .map(|(fingerprint, _, _)| fingerprint)
5080 .collect()
5081 }
5082
5083 #[test]
5088 fn saved_certificate_requests_preserve_expiration_and_insertion_order() {
5089 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5090 let (certificate_a, certificate_b) = distinct_test_certificates();
5091 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5092 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5093 let mut state = ConfigState::new();
5094 for (certificate, expired_at) in [
5095 (certificate_a, Some(2_100_000_000)),
5096 (certificate_b, Some(2_200_000_000)),
5097 ] {
5098 state
5099 .dispatch(
5100 &RequestType::AddCertificate(AddCertificate {
5101 address,
5102 certificate,
5103 expired_at,
5104 })
5105 .into(),
5106 )
5107 .expect("add certificate with expiration override");
5108 }
5109
5110 assert_eq!(
5111 ordered_saved_certificates(&state),
5112 vec![
5113 (fingerprint_a, Some(2_100_000_000)),
5114 (fingerprint_b, Some(2_200_000_000)),
5115 ],
5116 "saved-state requests must preserve certificate order and expiration overrides"
5117 );
5118
5119 use std::io::{Read, Seek, SeekFrom};
5120
5121 let mut file = tempfile::tempfile().expect("create SaveState file");
5122 state
5123 .write_requests_to_file(&mut file)
5124 .expect("serialize SaveState request stream");
5125 file.seek(SeekFrom::Start(0))
5126 .expect("rewind SaveState file");
5127 let mut bytes = Vec::new();
5128 file.read_to_end(&mut bytes).expect("read SaveState file");
5129 let (remaining, requests) = crate::parser::parse_several_requests::<WorkerRequest>(&bytes)
5130 .expect("parse LoadState request stream");
5131 assert!(remaining.is_empty(), "LoadState parser left trailing bytes");
5132 let mut replayed = ConfigState::new();
5133 for request in requests {
5134 replayed
5135 .dispatch(&request.content)
5136 .expect("LoadState request dispatch");
5137 }
5138 assert_eq!(
5139 replayed, state,
5140 "SaveState file parsing and LoadState dispatch must preserve all certificate metadata"
5141 );
5142 }
5143
5144 #[test]
5149 fn certificate_diff_replays_same_fingerprint_payload_and_expiration_changes() {
5150 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5151 let (mut certificate, _) = distinct_test_certificates();
5152 certificate.names = vec!["before.example.test".to_owned()];
5153 let fingerprint = certificate.fingerprint().expect("fingerprint certificate");
5154 let mut before = ConfigState::new();
5155 before
5156 .dispatch(
5157 &RequestType::AddCertificate(AddCertificate {
5158 address,
5159 certificate: certificate.to_owned(),
5160 expired_at: Some(2_100_000_000),
5161 })
5162 .into(),
5163 )
5164 .expect("add original certificate");
5165 let mut after = before.to_owned();
5166 certificate.names = vec!["after.example.test".to_owned()];
5167 after
5168 .dispatch(
5169 &RequestType::ReplaceCertificate(ReplaceCertificate {
5170 address,
5171 new_certificate: certificate,
5172 old_fingerprint: fingerprint.to_string(),
5173 new_expired_at: Some(2_200_000_000),
5174 })
5175 .into(),
5176 )
5177 .expect("replace same-fingerprint certificate payload");
5178
5179 let diff = before.diff(&after);
5180 assert!(
5181 diff.iter().any(|request| matches!(
5182 request.request_type,
5183 Some(RequestType::ReplaceCertificate(_))
5184 )),
5185 "same-fingerprint payload/expiration changes must emit ReplaceCertificate: {diff:?}"
5186 );
5187 }
5188
5189 #[test]
5193 fn certificate_diff_replays_order_only_changes() {
5194 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5195 let (certificate_a, certificate_b) = distinct_test_certificates();
5196 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5197 let mut before = ConfigState::new();
5198 let mut after = ConfigState::new();
5199 for certificate in [certificate_a.to_owned(), certificate_b.to_owned()] {
5200 before
5201 .dispatch(
5202 &RequestType::AddCertificate(AddCertificate {
5203 address,
5204 certificate,
5205 expired_at: Some(2_100_000_000),
5206 })
5207 .into(),
5208 )
5209 .expect("add before certificate");
5210 }
5211 for certificate in [certificate_b, certificate_a] {
5212 after
5213 .dispatch(
5214 &RequestType::AddCertificate(AddCertificate {
5215 address,
5216 certificate,
5217 expired_at: Some(2_100_000_000),
5218 })
5219 .into(),
5220 )
5221 .expect("add after certificate");
5222 }
5223
5224 let diff = before.diff(&after);
5225 assert_eq!(
5226 diff.len(),
5227 2,
5228 "A,B -> B,A needs only Remove(A), Add(A), not churn for B: {diff:?}"
5229 );
5230 assert!(
5231 matches!(
5232 &diff[0].request_type,
5233 Some(RequestType::RemoveCertificate(remove))
5234 if remove.fingerprint == fingerprint_a.to_string()
5235 ) && matches!(
5236 &diff[1].request_type,
5237 Some(RequestType::AddCertificate(add))
5238 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_a)
5239 ),
5240 "the minimal order-only sequence must move only certificate A to the end: {diff:?}"
5241 );
5242 }
5243
5244 #[test]
5245 fn certificate_diff_rotates_three_entries_by_moving_only_the_suffix() {
5246 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5247 let (certificate_a, certificate_b) = distinct_test_certificates();
5248 let certificate_c = third_test_certificate();
5249 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5250 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5251 let fingerprint_c = certificate_c.fingerprint().expect("fingerprint C");
5252 let build = |certificates: Vec<CertificateAndKey>| {
5253 let mut state = ConfigState::new();
5254 for certificate in certificates {
5255 state
5256 .dispatch(
5257 &RequestType::AddCertificate(AddCertificate {
5258 address,
5259 certificate,
5260 expired_at: Some(2_100_000_000),
5261 })
5262 .into(),
5263 )
5264 .expect("add certificate");
5265 }
5266 state
5267 };
5268 let before = build(vec![
5269 certificate_a.to_owned(),
5270 certificate_b.to_owned(),
5271 certificate_c.to_owned(),
5272 ]);
5273 let after = build(vec![certificate_b, certificate_c, certificate_a]);
5274
5275 let diff = before.diff(&after);
5276 assert_eq!(
5277 diff.len(),
5278 2,
5279 "A,B,C -> B,C,A needs only Remove(A), Add(A): {diff:?}"
5280 );
5281 assert!(
5282 matches!(
5283 &diff[0].request_type,
5284 Some(RequestType::RemoveCertificate(remove))
5285 if remove.fingerprint == fingerprint_a.to_string()
5286 ) && matches!(
5287 &diff[1].request_type,
5288 Some(RequestType::AddCertificate(add))
5289 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_a)
5290 ),
5291 "the rotation must leave B and C in place: {diff:?}"
5292 );
5293 assert!(
5294 diff.iter().all(|request| !matches!(
5295 &request.request_type,
5296 Some(RequestType::RemoveCertificate(remove))
5297 if remove.fingerprint == fingerprint_b.to_string()
5298 || remove.fingerprint == fingerprint_c.to_string()
5299 )),
5300 "B and C are the retained target prefix and must not be removed: {diff:?}"
5301 );
5302
5303 let mut interrupted = before.to_owned();
5304 interrupted
5305 .dispatch(&diff[0])
5306 .expect("apply the minimal sequence's remove boundary");
5307 assert_ne!(
5308 ordered_fingerprints(&interrupted, address),
5309 ordered_fingerprints(&after, address),
5310 "the interruption point must expose a real partial state"
5311 );
5312 for request in interrupted.diff(&after) {
5313 interrupted
5314 .dispatch(&request)
5315 .expect("fresh diff recovers the interrupted rotation");
5316 }
5317 assert_eq!(
5318 ordered_fingerprints(&interrupted, address),
5319 vec![fingerprint_b, fingerprint_c, fingerprint_a],
5320 "recovery must converge on the target order and its final winner"
5321 );
5322 }
5323
5324 #[test]
5325 fn certificate_diff_inserts_around_a_retained_entry_without_readding_new_suffix() {
5326 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5327 let (certificate_a, certificate_b) = distinct_test_certificates();
5328 let certificate_c = third_test_certificate();
5329 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5330 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5331 let fingerprint_c = certificate_c.fingerprint().expect("fingerprint C");
5332 let build = |certificates: Vec<CertificateAndKey>| {
5333 let mut state = ConfigState::new();
5334 for certificate in certificates {
5335 state
5336 .dispatch(
5337 &RequestType::AddCertificate(AddCertificate {
5338 address,
5339 certificate,
5340 expired_at: Some(2_100_000_000),
5341 })
5342 .into(),
5343 )
5344 .expect("add certificate");
5345 }
5346 state
5347 };
5348 let before = build(vec![certificate_b.to_owned()]);
5349 let after = build(vec![certificate_a, certificate_b, certificate_c]);
5350
5351 let diff = before.diff(&after);
5352 assert_eq!(
5353 diff.len(),
5354 4,
5355 "B -> A,B,C needs Add(A), Remove(B), Add(B), Add(C): {diff:?}"
5356 );
5357 assert!(
5358 matches!(
5359 &diff[0].request_type,
5360 Some(RequestType::AddCertificate(add))
5361 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_a)
5362 ) && matches!(
5363 &diff[1].request_type,
5364 Some(RequestType::RemoveCertificate(remove))
5365 if remove.fingerprint == fingerprint_b.to_string()
5366 ) && matches!(
5367 &diff[2].request_type,
5368 Some(RequestType::AddCertificate(add))
5369 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_b)
5370 ) && matches!(
5371 &diff[3].request_type,
5372 Some(RequestType::AddCertificate(add))
5373 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_c)
5374 ),
5375 "new certificate C must never be removed after insertion: {diff:?}"
5376 );
5377 assert!(
5378 diff.iter().all(|request| !matches!(
5379 &request.request_type,
5380 Some(RequestType::RemoveCertificate(remove))
5381 if remove.fingerprint == fingerprint_c.to_string()
5382 )),
5383 "the missing target suffix C is added once and never churned: {diff:?}"
5384 );
5385
5386 let mut interrupted = before.to_owned();
5387 interrupted
5388 .dispatch(&diff[0])
5389 .expect("apply the inserted target prefix before interruption");
5390 assert_ne!(
5391 ordered_fingerprints(&interrupted, address),
5392 ordered_fingerprints(&after, address),
5393 "the prefix interruption must expose a real partial state"
5394 );
5395 for request in interrupted.diff(&after) {
5396 interrupted
5397 .dispatch(&request)
5398 .expect("fresh diff recovers the interrupted insertion sequence");
5399 }
5400 assert_eq!(
5401 ordered_fingerprints(&interrupted, address),
5402 vec![fingerprint_a, fingerprint_b, fingerprint_c],
5403 "recovery must converge on the target order and final winner"
5404 );
5405 }
5406
5407 #[test]
5408 fn duplicate_certificate_add_keeps_payload_expiration_and_order() {
5409 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5410 let (certificate, _) = distinct_test_certificates();
5411 let mut state = ConfigState::new();
5412 state
5413 .add_certificate(&AddCertificate {
5414 address,
5415 certificate: certificate.to_owned(),
5416 expired_at: Some(2_100_000_000),
5417 })
5418 .expect("initial certificate");
5419 let before = state.to_owned();
5420 let mut differing_retry = certificate;
5421 differing_retry.names = vec!["different.example.test".to_owned()];
5422
5423 state
5424 .add_certificate(&AddCertificate {
5425 address,
5426 certificate: differing_retry,
5427 expired_at: Some(2_200_000_000),
5428 })
5429 .expect("duplicate fingerprint stays an accepted no-op");
5430
5431 assert_eq!(
5432 state, before,
5433 "duplicate AddCertificate must be a total no-op"
5434 );
5435 }
5436
5437 #[test]
5438 fn replacing_with_an_existing_identical_target_preserves_its_ordinal() {
5439 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5440 let (certificate_a, certificate_b) = distinct_test_certificates();
5441 let certificate_c = third_test_certificate();
5442 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5443 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5444 let fingerprint_c = certificate_c.fingerprint().expect("fingerprint C");
5445 let mut state = ConfigState::new();
5446 for certificate in [certificate_a, certificate_b.to_owned(), certificate_c] {
5447 state
5448 .add_certificate(&AddCertificate {
5449 address,
5450 certificate,
5451 expired_at: Some(2_100_000_000),
5452 })
5453 .expect("initial certificate");
5454 }
5455
5456 state
5457 .replace_certificate(&ReplaceCertificate {
5458 address,
5459 new_certificate: certificate_b,
5460 old_fingerprint: fingerprint_a.to_string(),
5461 new_expired_at: Some(2_100_000_000),
5462 })
5463 .expect("replace A with already-stored B");
5464
5465 assert_eq!(
5466 ordered_fingerprints(&state, address),
5467 vec![fingerprint_b, fingerprint_c],
5468 "an already-stored replacement keeps B in place while removing A"
5469 );
5470 }
5471
5472 #[test]
5473 fn legacy_metadata_normalizes_once_and_the_second_replay_is_stable() {
5474 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5475 let (certificate_a, certificate_b) = distinct_test_certificates();
5476 let mut current = ConfigState::new();
5477 for (certificate, expired_at) in [
5478 (certificate_a, Some(2_100_000_000)),
5479 (certificate_b, Some(2_200_000_000)),
5480 ] {
5481 current
5482 .add_certificate(&AddCertificate {
5483 address,
5484 certificate,
5485 expired_at,
5486 })
5487 .expect("current certificate");
5488 }
5489 let mut legacy_json = serde_json::to_value(¤t).expect("serialize current state");
5490 legacy_json
5491 .as_object_mut()
5492 .expect("ConfigState JSON object")
5493 .remove("certificate_metadata")
5494 .expect("current state writes certificate metadata");
5495 let legacy: ConfigState =
5496 serde_json::from_value(legacy_json).expect("legacy state without sidecar");
5497 assert!(legacy.certificate_metadata.is_empty());
5498
5499 let mut first_replay = ConfigState::new();
5500 for request in legacy.generate_requests() {
5501 first_replay
5502 .dispatch(&request)
5503 .expect("legacy request stream replays");
5504 }
5505 assert_eq!(
5506 first_replay.certificate_metadata,
5507 legacy.normalized_certificate_metadata(),
5508 "the first replay records deterministic fallback ordinals"
5509 );
5510 assert_ne!(
5511 first_replay, legacy,
5512 "normalization is semantic state and must not be hidden from PartialEq"
5513 );
5514
5515 let mut second_replay = ConfigState::new();
5516 for request in first_replay.generate_requests() {
5517 second_replay
5518 .dispatch(&request)
5519 .expect("normalized request stream replays");
5520 }
5521 assert_eq!(
5522 second_replay, first_replay,
5523 "a second replay of normalized metadata must be byte/PartialEq stable"
5524 );
5525 assert_eq!(
5526 serde_json::to_value(&second_replay).expect("serialize second replay"),
5527 serde_json::to_value(&first_replay).expect("serialize first replay"),
5528 "the derived next-order cache must not affect serialized state"
5529 );
5530 }
5531
5532 fn legacy_state_with_certificates() -> (ConfigState, SocketAddress) {
5536 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5537 let (certificate_a, certificate_b) = distinct_test_certificates();
5538 let mut current = ConfigState::new();
5539 current
5540 .dispatch(&RequestType::AddHttpsListener(make_https_listener(address)).into())
5541 .expect("https listener");
5542 for cluster_id in ["cluster_1", "cluster_2"] {
5543 current
5544 .dispatch(
5545 &RequestType::AddCluster(Cluster {
5546 cluster_id: cluster_id.to_owned(),
5547 ..Default::default()
5548 })
5549 .into(),
5550 )
5551 .expect("cluster");
5552 current
5553 .dispatch(
5554 &RequestType::AddBackend(AddBackend {
5555 cluster_id: cluster_id.to_owned(),
5556 backend_id: format!("{cluster_id}-0"),
5557 address: SocketAddress::new_v4(127, 0, 0, 1, 9000),
5558 ..Default::default()
5559 })
5560 .into(),
5561 )
5562 .expect("backend");
5563 current
5564 .dispatch(
5565 &RequestType::AddHttpsFrontend(RequestHttpFrontend {
5566 cluster_id: Some(cluster_id.to_owned()),
5567 hostname: format!("{cluster_id}.example.test"),
5568 path: PathRule::prefix(String::from("/")),
5569 address,
5570 position: RulePosition::Tree.into(),
5571 ..Default::default()
5572 })
5573 .into(),
5574 )
5575 .expect("frontend");
5576 }
5577 for (certificate, expired_at) in [
5578 (certificate_a, Some(2_100_000_000)),
5579 (certificate_b, Some(2_200_000_000)),
5580 ] {
5581 current
5582 .add_certificate(&AddCertificate {
5583 address,
5584 certificate,
5585 expired_at,
5586 })
5587 .expect("certificate");
5588 }
5589 let mut json = serde_json::to_value(¤t).expect("serialize current state");
5590 json.as_object_mut()
5591 .expect("ConfigState JSON object")
5592 .remove("certificate_metadata")
5593 .expect("current state writes certificate metadata");
5594 let legacy: ConfigState = serde_json::from_value(json).expect("legacy state");
5595 assert!(legacy.certificate_metadata.is_empty());
5596 (legacy, address)
5597 }
5598
5599 fn replay_diff(from: &ConfigState, requests: &[Request]) -> ConfigState {
5600 let mut replayed = from.clone();
5601 for request in requests {
5602 replayed.dispatch(request).expect("diff request replays");
5603 }
5604 replayed
5605 }
5606
5607 #[test]
5608 fn diff_of_legacy_state_without_metadata_tolerates_a_listener_change() {
5609 let (legacy, address) = legacy_state_with_certificates();
5610 let mut desired = legacy.clone();
5611 desired
5612 .https_listeners
5613 .get_mut(&ListenerKey::from(address))
5614 .expect("listener")
5615 .front_timeout = 61;
5616
5617 let requests = legacy.diff(&desired);
5618
5619 assert!(!requests.is_empty(), "a listener change emits requests");
5620 assert!(
5621 requests.iter().all(|request| !matches!(
5622 request.request_type,
5623 Some(
5624 RequestType::AddCertificate(_)
5625 | RequestType::RemoveCertificate(_)
5626 | RequestType::ReplaceCertificate(_)
5627 )
5628 )),
5629 "retained certificates are not re-sent"
5630 );
5631 let replayed = replay_diff(&legacy, &requests);
5632 assert_eq!(replayed.https_listeners, desired.https_listeners);
5633 assert_eq!(replayed.certificates, desired.certificates);
5634 assert_eq!(replayed.https_fronts, legacy.https_fronts);
5635 assert_eq!(replayed.backends, legacy.backends);
5636 assert_eq!(
5637 replayed.normalized_certificate_metadata(),
5638 desired.normalized_certificate_metadata()
5639 );
5640 }
5641
5642 #[test]
5643 fn diff_of_legacy_state_without_metadata_tolerates_unrelated_removals() {
5644 let (legacy, address) = legacy_state_with_certificates();
5645 let mut desired = legacy.clone();
5646 desired
5647 .dispatch(
5648 &RequestType::RemoveBackend(RemoveBackend {
5649 cluster_id: "cluster_2".to_owned(),
5650 backend_id: "cluster_2-0".to_owned(),
5651 address: SocketAddress::new_v4(127, 0, 0, 1, 9000),
5652 })
5653 .into(),
5654 )
5655 .expect("remove backend");
5656 desired
5657 .dispatch(
5658 &RequestType::RemoveHttpsFrontend(RequestHttpFrontend {
5659 cluster_id: Some("cluster_2".to_owned()),
5660 hostname: "cluster_2.example.test".to_owned(),
5661 path: PathRule::prefix(String::from("/")),
5662 address,
5663 position: RulePosition::Tree.into(),
5664 ..Default::default()
5665 })
5666 .into(),
5667 )
5668 .expect("remove frontend");
5669
5670 let requests = legacy.diff(&desired);
5671
5672 assert_eq!(requests.len(), 2, "exactly the two removals: {requests:?}");
5673 let replayed = replay_diff(&legacy, &requests);
5674 assert_eq!(replayed.certificates, legacy.certificates);
5675 assert_eq!(replayed.https_fronts, desired.https_fronts);
5676 assert_eq!(
5677 replayed.backends.get("cluster_1"),
5678 legacy.backends.get("cluster_1"),
5679 "the unaffected cluster keeps its backend"
5680 );
5681 assert!(
5682 replayed
5683 .backends
5684 .get("cluster_2")
5685 .is_none_or(|backends| backends.is_empty())
5686 );
5687 assert_eq!(
5688 replayed.normalized_certificate_metadata(),
5689 desired.normalized_certificate_metadata()
5690 );
5691 }
5692
5693 #[test]
5697 fn diff_still_reports_explicit_metadata_differences() {
5698 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5699 let (certificate_a, certificate_b) = distinct_test_certificates();
5700 let build = |first: (&CertificateAndKey, u64), second: (&CertificateAndKey, u64)| {
5701 let mut state = ConfigState::new();
5702 for (certificate, expired_at) in [first, second] {
5703 state
5704 .add_certificate(&AddCertificate {
5705 address,
5706 certificate: certificate.clone(),
5707 expired_at: Some(expired_at as i64),
5708 })
5709 .expect("certificate");
5710 }
5711 state
5712 };
5713 let base = build(
5714 (&certificate_a, 2_100_000_000),
5715 (&certificate_b, 2_200_000_000),
5716 );
5717 let other_expiry = build(
5718 (&certificate_a, 2_100_000_000),
5719 (&certificate_b, 2_300_000_000),
5720 );
5721 let other_order = build(
5722 (&certificate_b, 2_200_000_000),
5723 (&certificate_a, 2_100_000_000),
5724 );
5725
5726 assert_ne!(
5727 base.normalized_certificate_metadata(),
5728 other_expiry.normalized_certificate_metadata(),
5729 "expired_at survives normalization"
5730 );
5731 assert_ne!(
5732 base.normalized_certificate_metadata(),
5733 other_order.normalized_certificate_metadata(),
5734 "insertion order survives normalization"
5735 );
5736 for other in [&other_expiry, &other_order] {
5737 let requests = base.diff(other);
5738 assert!(
5739 requests.iter().any(|request| matches!(
5740 request.request_type,
5741 Some(
5742 RequestType::AddCertificate(_)
5743 | RequestType::RemoveCertificate(_)
5744 | RequestType::ReplaceCertificate(_)
5745 )
5746 )),
5747 "an explicit metadata difference yields certificate operations"
5748 );
5749 let replayed = replay_diff(&base, &requests);
5750 assert_eq!(
5751 replayed.normalized_certificate_metadata(),
5752 other.normalized_certificate_metadata()
5753 );
5754 }
5755 }
5756
5757 #[test]
5758 fn legacy_reader_ignores_current_certificate_metadata_but_keeps_material() {
5759 #[derive(serde::Deserialize)]
5760 struct LegacyConfigStateMirror {
5761 certificates: HashMap<SocketAddr, HashMap<Fingerprint, CertificateAndKey>>,
5762 }
5763
5764 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5765 let socket_address: SocketAddr = address.into();
5766 let (certificate, _) = distinct_test_certificates();
5767 let fingerprint = certificate.fingerprint().expect("fingerprint");
5768 let mut current = ConfigState::new();
5769 current
5770 .add_certificate(&AddCertificate {
5771 address,
5772 certificate: certificate.to_owned(),
5773 expired_at: Some(2_100_000_000),
5774 })
5775 .expect("current certificate");
5776
5777 let legacy: LegacyConfigStateMirror = serde_json::from_value(
5778 serde_json::to_value(¤t).expect("serialize current ConfigState"),
5779 )
5780 .expect("an old serde reader must ignore the unknown metadata field");
5781
5782 assert_eq!(
5783 legacy.certificates[&socket_address][&fingerprint], certificate,
5784 "new-to-old JSON remains syntactically readable, with metadata intentionally lost"
5785 );
5786 }
5787
5788 #[test]
5789 fn malformed_legacy_metadata_is_unchanged_on_refusal_then_normalized_on_success() {
5790 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5791 let socket_address: SocketAddr = address.into();
5792 let (certificate_a, certificate_b) = distinct_test_certificates();
5793 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5794 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5795 let mut state = ConfigState::new();
5796 state.certificates.insert(
5797 socket_address,
5798 HashMap::from([(fingerprint_a.to_owned(), certificate_a)]),
5799 );
5800 state.certificate_metadata.insert(
5801 socket_address,
5802 CertificateMetadataIndex {
5803 entries: HashMap::from([
5804 (
5805 fingerprint_a,
5806 CertificateMetadata {
5807 expired_at: Some(2_100_000_000),
5808 insertion_order: 7,
5809 },
5810 ),
5811 (
5812 Fingerprint(vec![0xff]),
5813 CertificateMetadata {
5814 expired_at: Some(9),
5815 insertion_order: 7,
5816 },
5817 ),
5818 ]),
5819 next_order: None,
5820 },
5821 );
5822 let before = state.to_owned();
5823
5824 let error = state.add_certificate(&AddCertificate {
5825 address,
5826 certificate: CertificateAndKey {
5827 certificate: "not PEM".to_owned(),
5828 ..Default::default()
5829 },
5830 expired_at: None,
5831 });
5832 assert!(error.is_err(), "malformed certificate must be refused");
5833 assert_eq!(
5834 state, before,
5835 "a refusal must not normalize or mutate legacy state"
5836 );
5837
5838 state
5839 .add_certificate(&AddCertificate {
5840 address,
5841 certificate: certificate_b,
5842 expired_at: Some(2_200_000_000),
5843 })
5844 .expect("valid mutation normalizes legacy metadata");
5845 let metadata = &state.certificate_metadata[&socket_address].entries;
5846 assert_eq!(metadata.len(), 2, "orphan metadata must be pruned");
5847 assert_eq!(metadata[&fingerprint_b].insertion_order, 1);
5848 assert_eq!(
5849 metadata
5850 .values()
5851 .map(|entry| entry.insertion_order)
5852 .collect::<BTreeSet<_>>(),
5853 BTreeSet::from([0, 1]),
5854 "normalized ordinals must be unique and contiguous"
5855 );
5856 }
5857
5858 #[test]
5873 fn remove_certificate_evicts_the_address_when_its_last_certificate_is_removed() {
5874 let certificate = CertificateAndKey {
5875 certificate: include_str!("../assets/certificate.pem").to_owned(),
5876 key: include_str!("../assets/key.pem").to_owned(),
5877 certificate_chain: Vec::new(),
5878 versions: Vec::new(),
5879 names: Vec::new(),
5880 };
5881 let fingerprint = certificate
5882 .fingerprint()
5883 .expect("test certificate fingerprint must be computable");
5884 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5885
5886 let mut state = ConfigState::default();
5887 state
5888 .add_certificate(&AddCertificate {
5889 address,
5890 certificate,
5891 expired_at: None,
5892 })
5893 .expect("initial certificate insertion must succeed");
5894
5895 state
5896 .remove_certificate(&RemoveCertificate {
5897 address,
5898 fingerprint: fingerprint.to_string(),
5899 })
5900 .expect("removing the only certificate must succeed");
5901
5902 assert!(
5903 !state.certificates.contains_key(&SocketAddr::from(address)),
5904 "removing an address's last certificate must evict the address key too, \
5905 found {:?}",
5906 state.certificates.get(&SocketAddr::from(address))
5907 );
5908
5909 let _ = state.produce_initial_state();
5913 }
5914
5915 #[test]
5927 fn add_certificate_with_valid_pem_armor_but_invalid_x509_leaves_no_bucket() {
5928 let junk_pem = "-----BEGIN CERTIFICATE-----\n\
5931 dGhpcyBpcyBub3QgYSB2YWxpZCB4NTA5IGRlciBzdHJ1Y3R1cmUgYXQgYWxsLCBq\n\
5932 dXN0IGZpbGxlciBieXRlcyBwYWRkZWQgb3V0\n\
5933 -----END CERTIFICATE-----\n";
5934 let certificate = CertificateAndKey {
5935 certificate: junk_pem.to_owned(),
5936 key: include_str!("../assets/key.pem").to_owned(),
5937 certificate_chain: Vec::new(),
5938 versions: Vec::new(),
5939 names: Vec::new(),
5940 };
5941 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5942
5943 let mut state = ConfigState::default();
5944 assert!(
5945 !state.certificates.contains_key(&SocketAddr::from(address)),
5946 "the address must be absent before the add attempt"
5947 );
5948
5949 let error = state
5950 .add_certificate(&AddCertificate {
5951 address,
5952 certificate,
5953 expired_at: None,
5954 })
5955 .expect_err("valid PEM armor around invalid X.509 content must be rejected");
5956 assert!(
5957 matches!(error, StateError::AddCertificate(_)),
5958 "expected StateError::AddCertificate, got {error:?}"
5959 );
5960
5961 assert!(
5962 !state.certificates.contains_key(&SocketAddr::from(address)),
5963 "a rejected add_certificate must leave no bucket for a previously-absent address, \
5964 found {:?}",
5965 state.certificates.get(&SocketAddr::from(address))
5966 );
5967 }
5968
5969 #[test]
5970 fn frontend_state_error_redacts_display_key() {
5971 const HOSTNAME_SECRET: &str = "STATE_ERROR_HOSTNAME_SECRET_SENTINEL";
5972 const PATH_SECRET: &str = "STATE_ERROR_PATH_SECRET_SENTINEL";
5973 const METHOD_SECRET: &str = "STATE_ERROR_METHOD_SECRET_SENTINEL";
5974
5975 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
5976 let front = RequestHttpFrontend {
5977 cluster_id: Some("cluster".to_owned()),
5978 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
5979 hostname: long_value(HOSTNAME_SECRET),
5980 path: PathRule {
5981 kind: PathRuleKind::Prefix as i32,
5982 value: long_value(PATH_SECRET),
5983 },
5984 method: Some(long_value(METHOD_SECRET)),
5985 position: RulePosition::Tree as i32,
5986 tags: BTreeMap::new(),
5987 redirect: None,
5988 redirect_scheme: None,
5989 redirect_template: None,
5990 rewrite_host: None,
5991 rewrite_path: None,
5992 rewrite_port: None,
5993 required_auth: None,
5994 headers: Vec::new(),
5995 hsts: None,
5996 header_predicates: Vec::new(),
5997 };
5998 let mut state = ConfigState::default();
5999 state
6000 .add_http_frontend(&front)
6001 .expect("first frontend insertion must succeed");
6002 assert!(
6003 state
6004 .http_fronts
6005 .keys()
6006 .next()
6007 .expect("raw state key must be retained")
6008 .contains(HOSTNAME_SECRET),
6009 "state storage must retain the raw operator-facing frontend key"
6010 );
6011
6012 let error = state
6013 .add_http_frontend(&front)
6014 .expect_err("duplicate frontend insertion must return StateError::Exists");
6015 let raw_frontend_key = front.to_string();
6016 match &error {
6017 StateError::Exists { id, .. } => assert_eq!(
6018 id, &raw_frontend_key,
6019 "StateError must preserve the public raw frontend identifier"
6020 ),
6021 other => panic!("expected StateError::Exists, got {other:?}"),
6022 }
6023 for (label, output) in [
6024 ("Display", error.to_string()),
6025 ("Debug", format!("{error:?}")),
6026 ] {
6027 for secret in [HOSTNAME_SECRET, PATH_SECRET, METHOD_SECRET] {
6028 assert!(
6029 !output.contains(secret),
6030 "StateError {label} leaked frontend marker {secret}"
6031 );
6032 }
6033 let metadata = format!("id_bytes={}", raw_frontend_key.len());
6034 assert!(
6035 output.contains(&metadata),
6036 "StateError {label} omitted bounded metadata {metadata}: {output}"
6037 );
6038 assert!(
6039 output.len() <= 512,
6040 "StateError {label} output is not bounded: {} bytes",
6041 output.len()
6042 );
6043 }
6044 }
6045
6046 #[test]
6047 fn state_error_formatting_bounds_every_string_bearing_variant() {
6048 const SECRET: &str = "STATE_ERROR_VARIANT_SECRET_SENTINEL";
6049
6050 let long_value = || format!("{SECRET}{}", "x".repeat(4096));
6051 let errors = [
6052 StateError::NotFound {
6053 kind: ObjectKind::Backend,
6054 id: long_value(),
6055 },
6056 StateError::Exists {
6057 kind: ObjectKind::Cluster,
6058 id: long_value(),
6059 },
6060 StateError::ClusterNotFound {
6061 kind: ObjectKind::Backend,
6062 cluster_id: long_value(),
6063 },
6064 StateError::AddCertificate(CertificateError::ParsePEMCertificate(long_value())),
6065 StateError::RemoveCertificate(long_value()),
6066 StateError::ReplaceCertificate(long_value()),
6067 StateError::FrontendConversion {
6068 frontend: long_value(),
6069 error: long_value(),
6070 },
6071 StateError::FileError(std::io::Error::other(long_value())),
6072 StateError::InvalidTcpFrontend {
6073 address: "127.0.0.1:443"
6074 .parse()
6075 .expect("test TCP frontend address must parse"),
6076 reason: long_value(),
6077 },
6078 StateError::UdpFrontendAddressTaken {
6079 address: "127.0.0.1:5353"
6080 .parse()
6081 .expect("test UDP frontend address must parse"),
6082 cluster_id: long_value().into(),
6083 current_cluster_id: long_value().into(),
6084 current_tags: BTreeMap::from([(long_value(), long_value())]),
6085 },
6086 ];
6087
6088 for error in errors {
6089 for (label, output) in [
6090 ("Display", error.to_string()),
6091 ("Debug", format!("{error:?}")),
6092 ] {
6093 assert!(
6094 !output.contains(SECRET),
6095 "StateError {label} leaked a string-bearing variant: {output}"
6096 );
6097 assert!(
6098 output.len() <= 512,
6099 "StateError {label} is not bounded: {} bytes",
6100 output.len()
6101 );
6102 }
6103 }
6104 }
6105
6106 #[test]
6107 fn tcp_frontend_not_found_error_retains_the_raw_identity() {
6108 const TCP_SECRET: &str = "STATE_ERROR_TCP_IDENTITY_SECRET_SENTINEL";
6109
6110 let long_value = |suffix: &str| format!("{TCP_SECRET}_{suffix}{}", "x".repeat(1024));
6111 let frontend = RequestTcpFrontend {
6112 cluster_id: long_value("cluster"),
6113 address: SocketAddress::new_v4(127, 0, 0, 1, 443),
6114 tags: BTreeMap::from([(long_value("tag-key"), long_value("tag-value"))]),
6115 sni: Some(long_value("sni")),
6116 alpn: vec![long_value("alpn")],
6117 };
6118 let error = ConfigState::default()
6119 .remove_tcp_frontend(&frontend)
6120 .expect_err("removing from a missing cluster must return StateError::NotFound");
6121
6122 match &error {
6123 StateError::NotFound { id, .. } => assert!(
6124 id.contains(TCP_SECRET),
6125 "StateError must retain the raw TCP frontend identity: {id}"
6126 ),
6127 other => panic!("expected StateError::NotFound, got {other:?}"),
6128 }
6129 for output in [error.to_string(), format!("{error:?}")] {
6130 assert!(
6131 !output.contains(TCP_SECRET),
6132 "StateError formatting leaked the retained TCP identity: {output}"
6133 );
6134 assert!(
6135 output.len() <= 512,
6136 "StateError formatting is not bounded: {} bytes",
6137 output.len()
6138 );
6139 }
6140 }
6141
6142 #[test]
6143 fn retained_state_debug_redacts_http_frontend_and_collection_data() {
6144 const CLUSTER_SECRET: &str = "RETAINED_CLUSTER_SECRET_SENTINEL";
6145 const HOSTNAME_SECRET: &str = "RETAINED_HOSTNAME_SECRET_SENTINEL";
6146 const PATH_SECRET: &str = "RETAINED_PATH_SECRET_SENTINEL";
6147 const METHOD_SECRET: &str = "RETAINED_METHOD_SECRET_SENTINEL";
6148 const TAG_KEY_SECRET: &str = "RETAINED_TAG_KEY_SECRET_SENTINEL";
6149 const TAG_VALUE_SECRET: &str = "RETAINED_TAG_VALUE_SECRET_SENTINEL";
6150 const REDIRECT_TEMPLATE_SECRET: &str = "RETAINED_REDIRECT_TEMPLATE_SECRET_SENTINEL";
6151 const REWRITE_HOST_SECRET: &str = "RETAINED_REWRITE_HOST_SECRET_SENTINEL";
6152 const REWRITE_PATH_SECRET: &str = "RETAINED_REWRITE_PATH_SECRET_SENTINEL";
6153 const HEADER_KEY_SECRET: &str = "RETAINED_HEADER_KEY_SECRET_SENTINEL";
6154 const HEADER_VALUE_SECRET: &str = "RETAINED_HEADER_VALUE_SECRET_SENTINEL";
6155 const REQUEST_COUNT_SECRET: &str = "RETAINED_REQUEST_COUNT_SECRET_SENTINEL";
6156
6157 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
6158 let request = RequestHttpFrontend {
6159 cluster_id: Some(long_value(CLUSTER_SECRET)),
6160 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
6161 hostname: long_value(HOSTNAME_SECRET),
6162 path: PathRule {
6163 kind: PathRuleKind::Regex as i32,
6164 value: long_value(PATH_SECRET),
6165 },
6166 method: Some(long_value(METHOD_SECRET)),
6167 position: RulePosition::Tree as i32,
6168 tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
6169 redirect: Some(RedirectPolicy::PermanentRedirect as i32),
6170 redirect_scheme: Some(RedirectScheme::UseHttps as i32),
6171 redirect_template: Some(long_value(REDIRECT_TEMPLATE_SECRET)),
6172 rewrite_host: Some(long_value(REWRITE_HOST_SECRET)),
6173 rewrite_path: Some(long_value(REWRITE_PATH_SECRET)),
6174 rewrite_port: Some(9443),
6175 required_auth: Some(true),
6176 headers: vec![Header {
6177 position: HeaderPosition::Both as i32,
6178 key: long_value(HEADER_KEY_SECRET),
6179 val: long_value(HEADER_VALUE_SECRET),
6180 }],
6181 hsts: Some(HstsConfig {
6182 enabled: Some(true),
6183 max_age: Some(31_536_000),
6184 include_subdomains: Some(true),
6185 preload: Some(false),
6186 force_replace_backend: Some(true),
6187 }),
6188 header_predicates: Vec::new(),
6189 };
6190 let retained = request
6191 .clone()
6192 .to_frontend()
6193 .expect("adversarial frontend must convert");
6194 let retained_debug = format!("{retained:?}");
6195
6196 let mut state = ConfigState::default();
6197 state
6198 .dispatch(&RequestType::AddHttpFrontend(request).into())
6199 .expect("adversarial frontend must be retained");
6200 state
6201 .request_counts
6202 .insert(long_value(REQUEST_COUNT_SECRET), 7);
6203 let retained_key = state
6204 .http_fronts
6205 .keys()
6206 .next()
6207 .expect("dispatch must retain the frontend's Display key");
6208 assert!(retained_key.contains(HOSTNAME_SECRET));
6209 assert!(retained_key.contains(PATH_SECRET));
6210 assert!(retained_key.contains(METHOD_SECRET));
6211 let state_debug = format!("{state:?}");
6212
6213 let secrets = [
6214 CLUSTER_SECRET,
6215 HOSTNAME_SECRET,
6216 PATH_SECRET,
6217 METHOD_SECRET,
6218 TAG_KEY_SECRET,
6219 TAG_VALUE_SECRET,
6220 REDIRECT_TEMPLATE_SECRET,
6221 REWRITE_HOST_SECRET,
6222 REWRITE_PATH_SECRET,
6223 HEADER_KEY_SECRET,
6224 HEADER_VALUE_SECRET,
6225 REQUEST_COUNT_SECRET,
6226 ];
6227 for (label, output) in [
6228 ("HttpFrontend", &retained_debug),
6229 ("ConfigState", &state_debug),
6230 ] {
6231 for secret in secrets {
6232 assert!(
6233 !output.contains(secret),
6234 "{label} Debug leaked retained marker {secret}"
6235 );
6236 }
6237 assert!(
6238 output.len() <= 2048,
6239 "{label} Debug output is not bounded: {} bytes",
6240 output.len()
6241 );
6242 }
6243
6244 for metadata in [
6245 format!("cluster_id_len: Some({})", long_value(CLUSTER_SECRET).len()),
6246 "address: 127.0.0.1:8443".to_owned(),
6247 format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
6248 "path_kind: 1".to_owned(),
6249 format!("path_len: {}", long_value(PATH_SECRET).len()),
6250 format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
6251 "position: Tree".to_owned(),
6252 "tags_count: Some(1)".to_owned(),
6253 format!(
6254 "tags_len: Some({})",
6255 long_value(TAG_KEY_SECRET).len() + long_value(TAG_VALUE_SECRET).len()
6256 ),
6257 "redirect: Some(4)".to_owned(),
6258 "redirect_scheme: Some(2)".to_owned(),
6259 format!(
6260 "redirect_template_len: Some({})",
6261 long_value(REDIRECT_TEMPLATE_SECRET).len()
6262 ),
6263 format!(
6264 "rewrite_host_len: Some({})",
6265 long_value(REWRITE_HOST_SECRET).len()
6266 ),
6267 format!(
6268 "rewrite_path_len: Some({})",
6269 long_value(REWRITE_PATH_SECRET).len()
6270 ),
6271 "rewrite_port: Some(9443)".to_owned(),
6272 "required_auth: Some(true)".to_owned(),
6273 "headers_count: 1".to_owned(),
6274 format!(
6275 "headers_len: {}",
6276 long_value(HEADER_KEY_SECRET).len() + long_value(HEADER_VALUE_SECRET).len()
6277 ),
6278 "hsts: Some(HstsConfig".to_owned(),
6279 ] {
6280 assert!(
6281 retained_debug.contains(&metadata),
6282 "HttpFrontend Debug omitted safe metadata {metadata}: {retained_debug}"
6283 );
6284 }
6285 for metadata in [
6286 "http_frontends_count: 1",
6287 "backends_count: 0",
6288 "tcp_frontends_count: 0",
6289 "udp_frontends_count: 0",
6290 "certificates_count: 0",
6291 "request_counts_count: 2",
6292 ] {
6293 assert!(
6294 state_debug.contains(metadata),
6295 "ConfigState Debug omitted count metadata {metadata}: {state_debug}"
6296 );
6297 }
6298 }
6299
6300 fn add_clusters(state: &mut ConfigState, cluster_ids: &[&str]) {
6303 for cluster_id in cluster_ids {
6304 state
6305 .dispatch(
6306 &RequestType::AddCluster(Cluster {
6307 cluster_id: cluster_id.to_string(),
6308 ..Default::default()
6309 })
6310 .into(),
6311 )
6312 .expect("Could not add the test cluster");
6313 }
6314 }
6315
6316 #[test]
6317 fn serialize() {
6318 let mut state: ConfigState = Default::default();
6319 add_clusters(&mut state, &["cluster_1", "cluster_2"]);
6320 state
6321 .dispatch(
6322 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6323 cluster_id: Some(String::from("cluster_1")),
6324 hostname: String::from("lolcatho.st:8080"),
6325 path: PathRule::prefix(String::from("/")),
6326 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6327 position: RulePosition::Tree.into(),
6328 ..Default::default()
6329 })
6330 .into(),
6331 )
6332 .expect("Could not execute request");
6333 state
6334 .dispatch(
6335 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6336 cluster_id: Some(String::from("cluster_2")),
6337 hostname: String::from("test.local"),
6338 path: PathRule::prefix(String::from("/abc")),
6339 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6340 position: RulePosition::Pre.into(),
6341 ..Default::default()
6342 })
6343 .into(),
6344 )
6345 .expect("Could not execute request");
6346 state
6347 .dispatch(
6348 &RequestType::AddBackend(AddBackend {
6349 cluster_id: String::from("cluster_1"),
6350 backend_id: String::from("cluster_1-0"),
6351 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
6352 ..Default::default()
6353 })
6354 .into(),
6355 )
6356 .expect("Could not execute request");
6357 state
6358 .dispatch(
6359 &RequestType::AddBackend(AddBackend {
6360 cluster_id: String::from("cluster_1"),
6361 backend_id: String::from("cluster_1-1"),
6362 address: SocketAddress::new_v4(127, 0, 0, 1, 1027),
6363 ..Default::default()
6364 })
6365 .into(),
6366 )
6367 .expect("Could not execute request");
6368 state
6369 .dispatch(
6370 &RequestType::AddBackend(AddBackend {
6371 cluster_id: String::from("cluster_2"),
6372 backend_id: String::from("cluster_2-0"),
6373 address: SocketAddress::new_v4(192, 167, 1, 2, 1026),
6374 ..Default::default()
6375 })
6376 .into(),
6377 )
6378 .expect("Could not execute request");
6379 state
6380 .dispatch(
6381 &RequestType::AddBackend(AddBackend {
6382 cluster_id: String::from("cluster_1"),
6383 backend_id: String::from("cluster_1-3"),
6384 address: SocketAddress::new_v4(192, 168, 1, 3, 1027),
6385 ..Default::default()
6386 })
6387 .into(),
6388 )
6389 .expect("Could not execute request");
6390 state
6391 .dispatch(
6392 &RequestType::RemoveBackend(RemoveBackend {
6393 cluster_id: String::from("cluster_1"),
6394 backend_id: String::from("cluster_1-3"),
6395 address: SocketAddress::new_v4(192, 168, 1, 3, 1027),
6396 })
6397 .into(),
6398 )
6399 .expect("Could not execute request");
6400
6401 }
6411
6412 #[test]
6413 fn diff() {
6414 let mut state: ConfigState = Default::default();
6415 add_clusters(&mut state, &["cluster_1", "cluster_2"]);
6416 state
6417 .dispatch(
6418 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6419 cluster_id: Some(String::from("cluster_1")),
6420 hostname: String::from("lolcatho.st:8080"),
6421 path: PathRule::prefix(String::from("/")),
6422 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6423 position: RulePosition::Post.into(),
6424 ..Default::default()
6425 })
6426 .into(),
6427 )
6428 .expect("Could not execute request");
6429 state
6430 .dispatch(
6431 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6432 cluster_id: Some(String::from("cluster_2")),
6433 hostname: String::from("test.local"),
6434 path: PathRule::prefix(String::from("/abc")),
6435 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6436 ..Default::default()
6437 })
6438 .into(),
6439 )
6440 .expect("Could not execute request");
6441 state
6442 .dispatch(
6443 &RequestType::AddBackend(AddBackend {
6444 cluster_id: String::from("cluster_1"),
6445 backend_id: String::from("cluster_1-0"),
6446 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
6447 load_balancing_parameters: Some(LoadBalancingParams::default()),
6448 ..Default::default()
6449 })
6450 .into(),
6451 )
6452 .expect("Could not execute request");
6453 state
6454 .dispatch(
6455 &RequestType::AddBackend(AddBackend {
6456 cluster_id: String::from("cluster_1"),
6457 backend_id: String::from("cluster_1-1"),
6458 address: SocketAddress::new_v4(127, 0, 0, 2, 1027),
6459 load_balancing_parameters: Some(LoadBalancingParams::default()),
6460 ..Default::default()
6461 })
6462 .into(),
6463 )
6464 .expect("Could not execute request");
6465 state
6466 .dispatch(
6467 &RequestType::AddBackend(AddBackend {
6468 cluster_id: String::from("cluster_2"),
6469 backend_id: String::from("cluster_2-0"),
6470 address: SocketAddress::new_v4(192, 167, 1, 2, 1026),
6471 load_balancing_parameters: Some(LoadBalancingParams::default()),
6472 ..Default::default()
6473 })
6474 .into(),
6475 )
6476 .expect("Could not execute request");
6477 state
6478 .dispatch(
6479 &RequestType::AddCluster(Cluster {
6480 cluster_id: String::from("cluster_2"),
6481 sticky_session: true,
6482 https_redirect: true,
6483 ..Default::default()
6484 })
6485 .into(),
6486 )
6487 .expect("Could not execute request");
6488
6489 let mut state2: ConfigState = Default::default();
6490 add_clusters(&mut state2, &["cluster_1"]);
6491 state2
6492 .dispatch(
6493 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6494 cluster_id: Some(String::from("cluster_1")),
6495 hostname: String::from("lolcatho.st:8080"),
6496 path: PathRule::prefix(String::from("/")),
6497 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6498 position: RulePosition::Post.into(),
6499 ..Default::default()
6500 })
6501 .into(),
6502 )
6503 .expect("Could not execute request");
6504 state2
6505 .dispatch(
6506 &RequestType::AddBackend(AddBackend {
6507 cluster_id: String::from("cluster_1"),
6508 backend_id: String::from("cluster_1-0"),
6509 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
6510 load_balancing_parameters: Some(LoadBalancingParams::default()),
6511 ..Default::default()
6512 })
6513 .into(),
6514 )
6515 .expect("Could not execute request");
6516 state2
6517 .dispatch(
6518 &RequestType::AddBackend(AddBackend {
6519 cluster_id: String::from("cluster_1"),
6520 backend_id: String::from("cluster_1-1"),
6521 address: SocketAddress::new_v4(127, 0, 0, 2, 1027),
6522 load_balancing_parameters: Some(LoadBalancingParams::default()),
6523 ..Default::default()
6524 })
6525 .into(),
6526 )
6527 .expect("Could not execute request");
6528 state2
6529 .dispatch(
6530 &RequestType::AddBackend(AddBackend {
6531 cluster_id: String::from("cluster_1"),
6532 backend_id: String::from("cluster_1-2"),
6533 address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
6534 load_balancing_parameters: Some(LoadBalancingParams::default()),
6535 ..Default::default()
6536 })
6537 .into(),
6538 )
6539 .expect("Could not execute request");
6540 state2
6541 .dispatch(
6542 &RequestType::AddCluster(Cluster {
6543 cluster_id: String::from("cluster_3"),
6544 sticky_session: false,
6545 https_redirect: false,
6546 ..Default::default()
6547 })
6548 .into(),
6549 )
6550 .expect("Could not execute request");
6551
6552 let e: Vec<Request> = vec![
6556 RequestType::AddBackend(AddBackend {
6557 cluster_id: String::from("cluster_1"),
6558 backend_id: String::from("cluster_1-2"),
6559 address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
6560 load_balancing_parameters: Some(LoadBalancingParams::default()),
6561 ..Default::default()
6562 })
6563 .into(),
6564 RequestType::RemoveCluster(String::from("cluster_2")).into(),
6565 RequestType::AddCluster(Cluster {
6566 cluster_id: String::from("cluster_3"),
6567 sticky_session: false,
6568 https_redirect: false,
6569 ..Default::default()
6570 })
6571 .into(),
6572 ];
6573 let expected_diff: HashSet<&Request> = HashSet::from_iter(e.iter());
6574
6575 let d = state.diff(&state2);
6576 let diff = HashSet::from_iter(d.iter());
6577 println!("diff requests:\n{diff:#?}\n");
6578 println!("expected diff requests:\n{expected_diff:#?}\n");
6579
6580 let hash1 = state.hash_state();
6581 let hash2 = state2.hash_state();
6582 let mut state3 = state.clone();
6583 state3
6584 .dispatch(
6585 &RequestType::AddBackend(AddBackend {
6586 cluster_id: String::from("cluster_1"),
6587 backend_id: String::from("cluster_1-2"),
6588 address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
6589 load_balancing_parameters: Some(LoadBalancingParams::default()),
6590 ..Default::default()
6591 })
6592 .into(),
6593 )
6594 .expect("Could not execute request");
6595 let hash3 = state3.hash_state();
6596 println!("state 1 hashes: {hash1:#?}");
6597 println!("state 2 hashes: {hash2:#?}");
6598 println!("state 3 hashes: {hash3:#?}");
6599
6600 assert_eq!(diff, expected_diff);
6601 }
6602
6603 fn state_with_clusters_owning_every_object_kind(cluster_ids: &[&str]) -> ConfigState {
6609 let mut state = ConfigState::new();
6610 for cluster_id in cluster_ids {
6611 let port = match *cluster_id {
6612 "cascade_a" => 9200,
6613 "cascade_b" => 9300,
6614 other => panic!("no test ports reserved for {other}"),
6615 };
6616 let requests: Vec<Request> = vec![
6617 RequestType::AddCluster(Cluster {
6618 cluster_id: cluster_id.to_string(),
6619 ..Default::default()
6620 })
6621 .into(),
6622 RequestType::AddHttpFrontend(RequestHttpFrontend {
6623 cluster_id: Some(cluster_id.to_string()),
6624 hostname: format!("{cluster_id}.test"),
6625 path: PathRule::prefix(String::from("/")),
6626 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6627 ..Default::default()
6628 })
6629 .into(),
6630 RequestType::AddHttpsFrontend(RequestHttpFrontend {
6631 cluster_id: Some(cluster_id.to_string()),
6632 hostname: format!("{cluster_id}.test"),
6633 path: PathRule::prefix(String::from("/")),
6634 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
6635 ..Default::default()
6636 })
6637 .into(),
6638 RequestType::AddTcpFrontend(RequestTcpFrontend {
6639 cluster_id: cluster_id.to_string(),
6640 address: SocketAddress::new_v4(127, 0, 0, 1, port),
6641 ..Default::default()
6642 })
6643 .into(),
6644 RequestType::AddUdpFrontend(RequestUdpFrontend {
6645 cluster_id: cluster_id.to_string(),
6646 address: SocketAddress::new_v4(127, 0, 0, 1, port + 1),
6647 ..Default::default()
6648 })
6649 .into(),
6650 RequestType::AddBackend(AddBackend {
6651 cluster_id: cluster_id.to_string(),
6652 backend_id: format!("{cluster_id}-0"),
6653 address: SocketAddress::new_v4(127, 0, 0, 1, port + 2),
6654 load_balancing_parameters: Some(LoadBalancingParams::default()),
6655 ..Default::default()
6656 })
6657 .into(),
6658 ];
6659 for request in &requests {
6660 state
6661 .dispatch(request)
6662 .unwrap_or_else(|e| panic!("could not apply {request:?}: {e}"));
6663 }
6664 }
6665 state
6666 .dispatch(&RequestType::AddHttpFrontend(cascade_deny_route()).into())
6667 .expect("could not add the deny route");
6668 state
6669 }
6670
6671 fn cascade_deny_route() -> RequestHttpFrontend {
6673 RequestHttpFrontend {
6674 cluster_id: None,
6675 hostname: String::from("cascade_a.test"),
6676 path: PathRule::prefix(String::from("/deny")),
6677 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6678 ..Default::default()
6679 }
6680 }
6681
6682 fn cluster_named_by(request: &Request) -> Option<&str> {
6684 match request.request_type.as_ref()? {
6685 RequestType::AddCluster(cluster) => Some(&cluster.cluster_id),
6686 RequestType::RemoveCluster(cluster_id) => Some(cluster_id),
6687 RequestType::AddHttpFrontend(front)
6688 | RequestType::RemoveHttpFrontend(front)
6689 | RequestType::AddHttpsFrontend(front)
6690 | RequestType::RemoveHttpsFrontend(front) => front.cluster_id.as_deref(),
6691 RequestType::AddTcpFrontend(front) | RequestType::RemoveTcpFrontend(front) => {
6692 Some(&front.cluster_id)
6693 }
6694 RequestType::AddUdpFrontend(front) | RequestType::RemoveUdpFrontend(front) => {
6695 Some(&front.cluster_id)
6696 }
6697 RequestType::AddBackend(backend) | RequestType::AddUnixBackend(backend) => {
6698 Some(&backend.cluster_id)
6699 }
6700 RequestType::RemoveBackend(backend) => Some(&backend.cluster_id),
6701 _ => None,
6702 }
6703 }
6704
6705 fn assert_same_routing(actual: &ConfigState, expected: &ConfigState, context: &str) {
6707 assert_eq!(actual.clusters, expected.clusters, "{context}: clusters");
6708 assert_eq!(actual.backends, expected.backends, "{context}: backends");
6709 assert_eq!(
6710 actual.http_fronts, expected.http_fronts,
6711 "{context}: http_fronts"
6712 );
6713 assert_eq!(
6714 actual.https_fronts, expected.https_fronts,
6715 "{context}: https_fronts"
6716 );
6717 assert_eq!(
6718 actual.tcp_fronts, expected.tcp_fronts,
6719 "{context}: tcp_fronts"
6720 );
6721 assert_eq!(
6722 actual.udp_fronts, expected.udp_fronts,
6723 "{context}: udp_fronts"
6724 );
6725 }
6726
6727 #[test]
6731 fn remove_cluster_removes_its_frontends_and_backends_for_every_protocol() {
6732 let mut state = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6733 let expected = state_with_clusters_owning_every_object_kind(&["cascade_b"]);
6734
6735 state
6736 .dispatch(&RequestType::RemoveCluster(String::from("cascade_a")).into())
6737 .expect("removing an existing cluster must succeed");
6738
6739 assert_same_routing(&state, &expected, "after RemoveCluster(cascade_a)");
6740 assert!(
6741 state
6742 .http_fronts
6743 .values()
6744 .any(|front| front.cluster_id.is_none() && front.hostname == "cascade_a.test"),
6745 "a route that names no cluster must survive the removal of a cluster on its hostname"
6746 );
6747
6748 let late: Request = RequestType::RemoveHttpFrontend(RequestHttpFrontend {
6752 cluster_id: Some(String::from("cascade_a")),
6753 hostname: String::from("cascade_a.test"),
6754 path: PathRule::prefix(String::from("/")),
6755 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6756 ..Default::default()
6757 })
6758 .into();
6759 let err = state
6760 .dispatch(&late)
6761 .expect_err("the cascade must already have removed the frontend");
6762 assert!(
6763 matches!(err, StateError::NotFound { .. }),
6764 "expected NotFound, got: {err}"
6765 );
6766 assert!(
6767 state.removed_with_its_cluster(&late, &err),
6768 "a removal of an object that went with its cluster must be recognised"
6769 );
6770
6771 let mut typo = cascade_deny_route();
6775 typo.cluster_id = Some(String::from("cascade_typo"));
6776 let typo: Request = RequestType::RemoveHttpFrontend(typo).into();
6777 let err = state
6778 .dispatch(&typo)
6779 .expect_err("a removal naming another cluster must not match the deny route");
6780 assert!(state.removed_with_its_cluster(&typo, &err));
6781 let answer = state.removed_with_its_cluster_message(&typo);
6782 assert!(
6783 answer.contains("this key belongs to a route with no cluster, left in place"),
6784 "the ok answer must say the deny route holds the key: {answer}"
6785 );
6786 assert!(
6787 state
6788 .http_fronts
6789 .values()
6790 .any(|front| front.cluster_id.is_none() && front.hostname == "cascade_a.test"),
6791 "the deny route must survive a removal naming a cluster that never existed"
6792 );
6793 }
6794
6795 #[test]
6801 fn a_stale_frontend_removal_never_removes_another_clusters_route() {
6802 let route = |cluster_id: &str| RequestHttpFrontend {
6803 cluster_id: Some(cluster_id.to_owned()),
6804 hostname: String::from("reused.test"),
6805 path: PathRule::prefix(String::from("/")),
6806 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6807 ..Default::default()
6808 };
6809 let cluster = |cluster_id: &str| {
6810 RequestType::AddCluster(Cluster {
6811 cluster_id: cluster_id.to_owned(),
6812 ..Default::default()
6813 })
6814 };
6815 let owner =
6816 |state: &ConfigState, fronts: fn(&ConfigState) -> &BTreeMap<String, HttpFrontend>| {
6817 fronts(state)
6818 .get(&route("stale_b").to_string())
6819 .and_then(|front| front.cluster_id.as_deref().map(str::to_owned))
6820 };
6821
6822 for (add, remove, fronts) in [
6823 (
6824 RequestType::AddHttpFrontend as fn(RequestHttpFrontend) -> RequestType,
6825 RequestType::RemoveHttpFrontend as fn(RequestHttpFrontend) -> RequestType,
6826 (|state: &ConfigState| &state.http_fronts)
6827 as fn(&ConfigState) -> &BTreeMap<String, HttpFrontend>,
6828 ),
6829 (
6830 RequestType::AddHttpsFrontend,
6831 RequestType::RemoveHttpsFrontend,
6832 |state: &ConfigState| &state.https_fronts,
6833 ),
6834 ] {
6835 let mut state = ConfigState::new();
6838 for request in [
6839 cluster("stale_a"),
6840 add(route("stale_a")),
6841 RequestType::RemoveCluster(String::from("stale_a")),
6842 cluster("stale_b"),
6843 add(route("stale_b")),
6844 ] {
6845 state
6846 .dispatch(&request.clone().into())
6847 .unwrap_or_else(|e| panic!("could not apply {request:?}: {e}"));
6848 }
6849 let stale: Request = remove(route("stale_a")).into();
6850 let err = state
6851 .dispatch(&stale)
6852 .expect_err("a removal naming cluster A must not match cluster B's route");
6853 assert!(
6854 state.removed_with_its_cluster(&stale, &err),
6855 "A is gone, so its frontend went with it: {err}"
6856 );
6857 let answer = state.removed_with_its_cluster_message(&stale);
6858 assert!(
6859 answer.contains("cluster stale_a does not exist")
6860 && answer.contains("this key belongs to cluster stale_b, left in place"),
6861 "the ok answer must name the key's current owner: {answer}"
6862 );
6863 assert_eq!(
6864 owner(&state, fronts).as_deref(),
6865 Some("stale_b"),
6866 "cluster B's route must survive a stale removal naming cluster A"
6867 );
6868
6869 let mut state = ConfigState::new();
6872 for request in [
6873 cluster("stale_a"),
6874 cluster("stale_b"),
6875 add(route("stale_b")),
6876 ] {
6877 state
6878 .dispatch(&request.clone().into())
6879 .unwrap_or_else(|e| panic!("could not apply {request:?}: {e}"));
6880 }
6881 let err = state
6882 .dispatch(&stale)
6883 .expect_err("a removal naming cluster A must not match cluster B's route");
6884 assert!(
6885 !state.removed_with_its_cluster(&stale, &err),
6886 "A still exists: the removal is refused, not already done"
6887 );
6888 assert_eq!(
6889 owner(&state, fronts).as_deref(),
6890 Some("stale_b"),
6891 "cluster B's route must survive a removal naming cluster A"
6892 );
6893 }
6894 }
6895
6896 #[test]
6900 fn diff_removing_a_cluster_relies_on_the_cascade() {
6901 let before = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6902 let after = state_with_clusters_owning_every_object_kind(&["cascade_b"]);
6903
6904 let diff = before.diff(&after);
6905 assert_eq!(
6906 diff,
6907 vec![Request::from(RequestType::RemoveCluster(String::from(
6908 "cascade_a"
6909 )))],
6910 "the cascade must be the only removal the diff emits"
6911 );
6912
6913 let mut replayed = before.clone();
6914 for request in &diff {
6915 replayed
6916 .dispatch(request)
6917 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
6918 }
6919 assert_same_routing(&replayed, &after, "replaying diff(before, after)");
6920 }
6921
6922 #[test]
6926 fn diff_re_adds_a_frontend_the_target_keeps_for_a_removed_cluster() {
6927 let before = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6928 let kept = RequestHttpFrontend {
6929 cluster_id: Some(String::from("cascade_a")),
6930 hostname: String::from("cascade_a.test"),
6931 path: PathRule::prefix(String::from("/")),
6932 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6933 ..Default::default()
6934 };
6935 let mut after = state_with_clusters_owning_every_object_kind(&["cascade_b"]);
6936 after
6937 .dispatch(&RequestType::AddHttpFrontend(kept.clone()).into())
6938 .expect("a frontend may name a cluster the state does not hold");
6939
6940 let diff = before.diff(&after);
6941 let removal = diff
6942 .iter()
6943 .position(|r| r.request_type == Some(RequestType::RemoveCluster("cascade_a".into())))
6944 .expect("the diff must remove cascade_a");
6945 let re_add = diff
6946 .iter()
6947 .position(|r| r.request_type == Some(RequestType::AddHttpFrontend(kept.clone())))
6948 .expect("the diff must add back the frontend the cascade removes");
6949 assert!(
6950 removal < re_add,
6951 "the frontend must be added back after its cluster is removed: {diff:?}"
6952 );
6953
6954 let mut replayed = before.clone();
6955 for request in &diff {
6956 replayed
6957 .dispatch(request)
6958 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
6959 }
6960 assert_same_routing(&replayed, &after, "replaying diff(before, after)");
6961 }
6962
6963 #[test]
6967 fn a_removed_cluster_leaves_nothing_for_load_state_to_replay() {
6968 let mut state = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6969 state
6970 .dispatch(&RequestType::RemoveCluster(String::from("cascade_a")).into())
6971 .expect("removing an existing cluster must succeed");
6972
6973 let requests = state.generate_requests();
6974 let leaked: Vec<&Request> = requests
6975 .iter()
6976 .filter(|request| cluster_named_by(request) == Some("cascade_a"))
6977 .collect();
6978 assert!(
6979 leaked.is_empty(),
6980 "LoadState would replay objects of the removed cluster: {leaked:?}"
6981 );
6982
6983 let mut replayed = ConfigState::new();
6984 for request in &requests {
6985 replayed
6986 .dispatch(request)
6987 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
6988 }
6989 assert_same_routing(&replayed, &state, "replaying generate_requests");
6990 }
6991
6992 fn add_backend_to(cluster_id: &str, port: u16) -> Request {
6993 RequestType::AddBackend(AddBackend {
6994 cluster_id: cluster_id.to_owned(),
6995 backend_id: format!("{cluster_id}-{port}"),
6996 address: SocketAddress::new_v4(127, 0, 0, 1, port),
6997 ..Default::default()
6998 })
6999 .into()
7000 }
7001
7002 #[test]
7006 fn add_backend_for_an_unknown_cluster_is_refused() {
7007 let mut state = ConfigState::new();
7008
7009 let refused = state.dispatch(&add_backend_to("orphan", 1026));
7010 assert!(
7011 matches!(refused, Err(StateError::ClusterNotFound { .. })),
7012 "a backend of a cluster never added must be refused, got {refused:?}"
7013 );
7014 assert!(
7015 state.backends.is_empty(),
7016 "a refused AddBackend must not leave a bucket behind: {:?}",
7017 state.backends
7018 );
7019
7020 state
7021 .dispatch(
7022 &RequestType::AddCluster(Cluster {
7023 cluster_id: String::from("orphan"),
7024 ..Default::default()
7025 })
7026 .into(),
7027 )
7028 .expect("adding the cluster must succeed");
7029 state
7030 .dispatch(&add_backend_to("orphan", 1026))
7031 .expect("a backend of an existing cluster must be added");
7032 assert_eq!(state.count_backends(), 1);
7033
7034 state
7035 .dispatch(&RequestType::RemoveCluster(String::from("orphan")).into())
7036 .expect("removing an existing cluster must succeed");
7037 assert!(
7038 state.backends.is_empty(),
7039 "RemoveCluster must remove the cluster's backends"
7040 );
7041 let refused = state.dispatch(&add_backend_to("orphan", 1027));
7042 assert!(
7043 matches!(refused, Err(StateError::ClusterNotFound { .. })),
7044 "a backend of a removed cluster must be refused, got {refused:?}"
7045 );
7046 assert!(
7047 state.backends.is_empty(),
7048 "a backend must never outlive its cluster: {:?}",
7049 state.backends
7050 );
7051 }
7052
7053 #[test]
7057 fn add_unix_backend_for_an_unknown_cluster_is_refused() {
7058 let mut state = ConfigState::new();
7059 for unix_path in ["/run/app.sock", "relative.sock"] {
7060 let refused = state.dispatch(
7061 &RequestType::AddUnixBackend(AddBackend {
7062 cluster_id: String::from("orphan"),
7063 backend_id: String::from("orphan-unix"),
7064 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
7065 unix_path: Some(unix_path.to_owned()),
7066 ..Default::default()
7067 })
7068 .into(),
7069 );
7070 assert!(
7071 matches!(refused, Err(StateError::ClusterNotFound { .. })),
7072 "{unix_path:?}: an unknown cluster must be refused first, got {refused:?}"
7073 );
7074 assert!(
7075 state.backends.is_empty(),
7076 "a refused AddUnixBackend must not leave a bucket behind: {:?}",
7077 state.backends
7078 );
7079 }
7080 assert!(state.clusters.is_empty());
7081 }
7082
7083 #[test]
7086 fn unknown_cluster_error_names_the_object_without_the_cluster_id() {
7087 let mut state = ConfigState::new();
7088 let error = state
7089 .dispatch(&add_backend_to("secret-cluster-id", 1026))
7090 .expect_err("a backend of an unknown cluster must be refused");
7091 let message = error.to_string();
7092 assert!(
7093 !message.contains("secret-cluster-id"),
7094 "the error must not echo the cluster id: {message}"
7095 );
7096 assert!(
7097 message.contains("Backend") && message.contains("add the cluster first"),
7098 "the error must say what to do: {message}"
7099 );
7100 }
7101
7102 #[test]
7105 fn a_configuration_file_adds_each_cluster_before_its_backends() {
7106 let config = crate::config::Config::load_from_path("assets/config.toml")
7107 .expect("the test configuration must load");
7108 let mut state = ConfigState::new();
7109 for message in config
7110 .generate_config_messages()
7111 .expect("the test configuration must generate its orders")
7112 {
7113 let result = state.dispatch(&message.content);
7114 if let Some(RequestType::AddBackend(_) | RequestType::AddCluster(_)) =
7115 &message.content.request_type
7116 {
7117 result.unwrap_or_else(|e| panic!("could not apply {message:?}: {e}"));
7118 }
7119 }
7120 assert_eq!(state.clusters.len(), 3);
7121 assert_eq!(state.count_backends(), 5);
7122 }
7123
7124 #[test]
7127 fn diff_adds_a_cluster_before_its_backends() {
7128 let target = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
7129 let mut state = ConfigState::new();
7130 for request in state.diff(&target) {
7131 state
7132 .dispatch(&request)
7133 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
7134 }
7135 assert_same_routing(&state, &target, "replaying diff(empty, target)");
7136 }
7137
7138 fn app_cluster_probing(port: Option<u32>, unix_path: Option<&str>) -> ConfigState {
7141 let mut state = ConfigState::new();
7142 let health_check = crate::proto::command::HealthCheckConfig {
7143 interval: 10,
7144 timeout: 5,
7145 healthy_threshold: 3,
7146 unhealthy_threshold: 3,
7147 mode: crate::proto::command::HealthCheckMode::Tcp as i32,
7148 port,
7149 ..Default::default()
7150 };
7151 state
7152 .dispatch(
7153 &RequestType::AddCluster(Cluster {
7154 cluster_id: String::from("app"),
7155 health_check: Some(health_check),
7156 ..Default::default()
7157 })
7158 .into(),
7159 )
7160 .expect("the cluster must be accepted");
7161 state
7162 .dispatch(
7163 &RequestType::add_backend(AddBackend {
7164 cluster_id: String::from("app"),
7165 backend_id: String::from("app-0"),
7166 address: SocketAddress::new_v4(127, 0, 0, 1, 9000),
7167 unix_path: unix_path.map(ToOwned::to_owned),
7168 ..Default::default()
7169 })
7170 .into(),
7171 )
7172 .expect("the backend must be accepted");
7173 state
7174 }
7175
7176 #[test]
7182 fn diff_converges_when_a_probe_port_and_a_unix_backend_swap() {
7183 let unix = app_cluster_probing(None, Some("/run/app.sock"));
7184 let ip = app_cluster_probing(Some(8081), None);
7185 for (from, to, direction) in [
7186 (&unix, &ip, "unix -> ip+port"),
7187 (&ip, &unix, "ip+port -> unix"),
7188 ] {
7189 let mut state = from.clone();
7190 for request in from.diff(to) {
7191 state
7192 .dispatch(&request)
7193 .unwrap_or_else(|e| panic!("{direction}: could not apply {request:?}: {e}"));
7194 }
7195 assert_same_routing(&state, to, direction);
7196 }
7197 }
7198
7199 #[test]
7200 fn cluster_ids_by_domain() {
7201 let mut config = ConfigState::new();
7202 let http_front_cluster1 = RequestHttpFrontend {
7203 cluster_id: Some(String::from("MyCluster_1")),
7204 hostname: String::from("lolcatho.st"),
7205 path: PathRule::prefix(String::from("")),
7206 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7207 ..Default::default()
7208 };
7209
7210 let https_front_cluster1 = RequestHttpFrontend {
7211 cluster_id: Some(String::from("MyCluster_1")),
7212 hostname: String::from("lolcatho.st"),
7213 path: PathRule::prefix(String::from("")),
7214 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7215 ..Default::default()
7216 };
7217
7218 let http_front_cluster2 = RequestHttpFrontend {
7219 cluster_id: Some(String::from("MyCluster_2")),
7220 hostname: String::from("lolcatho.st"),
7221 path: PathRule::prefix(String::from("/api")),
7222 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7223 ..Default::default()
7224 };
7225
7226 let https_front_cluster2 = RequestHttpFrontend {
7227 cluster_id: Some(String::from("MyCluster_2")),
7228 hostname: String::from("lolcatho.st"),
7229 path: PathRule::prefix(String::from("/api")),
7230 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7231 ..Default::default()
7232 };
7233
7234 config
7235 .dispatch(&RequestType::AddHttpFrontend(http_front_cluster1).into())
7236 .expect("Could not execute request");
7237 config
7238 .dispatch(&RequestType::AddHttpFrontend(http_front_cluster2).into())
7239 .expect("Could not execute request");
7240 config
7241 .dispatch(&RequestType::AddHttpsFrontend(https_front_cluster1).into())
7242 .expect("Could not execute request");
7243 config
7244 .dispatch(&RequestType::AddHttpsFrontend(https_front_cluster2).into())
7245 .expect("Could not execute request");
7246
7247 let mut cluster1_cluster2: HashSet<ClusterId> = HashSet::new();
7248 cluster1_cluster2.insert(ClusterId::from("MyCluster_1"));
7249 cluster1_cluster2.insert(ClusterId::from("MyCluster_2"));
7250
7251 let mut cluster2: HashSet<ClusterId> = HashSet::new();
7252 cluster2.insert(ClusterId::from("MyCluster_2"));
7253
7254 let empty: HashSet<ClusterId> = HashSet::new();
7255 assert_eq!(
7256 config.get_cluster_ids_by_domain(String::from("lolcatho.st"), None),
7257 cluster1_cluster2
7258 );
7259 assert_eq!(
7260 config
7261 .get_cluster_ids_by_domain(String::from("lolcatho.st"), Some(String::from("/api"))),
7262 cluster2
7263 );
7264 assert_eq!(
7265 config.get_cluster_ids_by_domain(String::from("lolcathost"), None),
7266 empty
7267 );
7268 assert_eq!(
7269 config
7270 .get_cluster_ids_by_domain(String::from("lolcathost"), Some(String::from("/sozu"))),
7271 empty
7272 );
7273 }
7274
7275 #[test]
7276 fn duplicate_backends() {
7277 let mut state: ConfigState = Default::default();
7278 add_clusters(&mut state, &["cluster_1"]);
7279 state
7280 .dispatch(
7281 &RequestType::AddBackend(AddBackend {
7282 cluster_id: String::from("cluster_1"),
7283 backend_id: String::from("cluster_1-0"),
7284 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7285 load_balancing_parameters: Some(LoadBalancingParams::default()),
7286 ..Default::default()
7287 })
7288 .into(),
7289 )
7290 .expect("Could not execute request");
7291
7292 let b = Backend {
7293 cluster_id: ClusterId::from("cluster_1"),
7294 backend_id: String::from("cluster_1-0"),
7295 address: "127.0.0.1:1026".parse().unwrap(),
7296 load_balancing_parameters: Some(LoadBalancingParams::default()),
7297 sticky_id: Some("sticky".to_string()),
7298 backup: None,
7299 unix_path: None,
7300 };
7301
7302 state
7303 .dispatch(&RequestType::AddBackend(b.clone().to_add_backend()).into())
7304 .expect("Could not execute order");
7305
7306 assert_eq!(state.backends.get("cluster_1").unwrap(), &vec![b]);
7307 }
7308
7309 #[test]
7313 fn a_unix_path_backend_round_trips_through_the_state() {
7314 let mut state: ConfigState = Default::default();
7315 add_clusters(&mut state, &["cluster_1"]);
7316 let add = AddBackend {
7317 cluster_id: String::from("cluster_1"),
7318 backend_id: String::from("cluster_1-unix"),
7319 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
7320 unix_path: Some(String::from("/run/app.sock")),
7321 ..Default::default()
7322 };
7323 state
7324 .dispatch(&RequestType::AddBackend(add.clone()).into())
7325 .expect("a valid unix_path is accepted");
7326 assert_eq!(
7327 state.backends["cluster_1"][0].unix_path.as_deref(),
7328 Some("/run/app.sock")
7329 );
7330 let replayed: Vec<AddBackend> = state
7331 .generate_requests()
7332 .into_iter()
7333 .filter_map(|request| match request.request_type {
7334 Some(RequestType::AddUnixBackend(backend)) => Some(backend),
7335 _ => None,
7336 })
7337 .collect();
7338 assert_eq!(replayed, vec![add]);
7339 }
7340
7341 #[test]
7344 fn an_invalid_unix_path_is_refused() {
7345 let mut state: ConfigState = Default::default();
7346 add_clusters(&mut state, &["cluster_1"]);
7347 for refused in ["", "relative.sock", "@"] {
7348 let result = state.dispatch(
7349 &RequestType::AddBackend(AddBackend {
7350 cluster_id: String::from("cluster_1"),
7351 backend_id: String::from("cluster_1-unix"),
7352 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
7353 unix_path: Some(refused.to_owned()),
7354 ..Default::default()
7355 })
7356 .into(),
7357 );
7358 assert!(
7359 matches!(
7360 result,
7361 Err(StateError::InvalidValue {
7362 field: "unix_path",
7363 ..
7364 })
7365 ),
7366 "{refused:?} must be refused"
7367 );
7368 }
7369 assert!(state.backends.is_empty());
7370 }
7371
7372 #[test]
7373 fn remove_backend() {
7374 let mut state: ConfigState = Default::default();
7375 state
7376 .dispatch(
7377 &RequestType::AddCluster(Cluster {
7378 cluster_id: String::from("cluster_1"),
7379 ..Default::default()
7380 })
7381 .into(),
7382 )
7383 .expect("Could not execute request");
7384
7385 for i in 0..10 {
7386 state
7387 .dispatch(
7388 &RequestType::AddBackend(AddBackend {
7389 cluster_id: String::from("cluster_1"),
7390 backend_id: format!("cluster_1-{i}"),
7391 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7392 ..Default::default()
7393 })
7394 .into(),
7395 )
7396 .expect("Could not execute request");
7397 }
7398
7399 assert_eq!(state.backends.get("cluster_1").unwrap().len(), 10);
7400
7401 let remove_backend_2 = RequestType::RemoveBackend(RemoveBackend {
7402 cluster_id: String::from("cluster_1"),
7403 backend_id: String::from("cluster_1-0"),
7404 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7405 })
7406 .into();
7407
7408 let remove_backend_result = state.dispatch(&remove_backend_2);
7409
7410 assert!(remove_backend_result.is_ok());
7411 assert_eq!(state.backends.get("cluster_1").unwrap().len(), 9);
7412
7413 let redundant_remove = state.dispatch(&remove_backend_2);
7414 assert!(matches!(redundant_remove, Err(StateError::NoChange)));
7415 assert_eq!(state.backends.get("cluster_1").unwrap().len(), 9);
7416 }
7417
7418 #[test]
7419 fn remove_backends_randomly() {
7420 let mut state: ConfigState = Default::default();
7421 state
7422 .dispatch(
7423 &RequestType::AddCluster(Cluster {
7424 cluster_id: String::from("cluster_1"),
7425 ..Default::default()
7426 })
7427 .into(),
7428 )
7429 .expect("Could not execute request");
7430
7431 for _ in 0..1000 {
7432 for i in 0..10 {
7433 state
7434 .dispatch(
7435 &RequestType::AddBackend(AddBackend {
7436 cluster_id: String::from("cluster_1"),
7437 backend_id: format!("cluster_1-{i}"),
7438 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7439 ..Default::default()
7440 })
7441 .into(),
7442 )
7443 .expect("Could not execute request");
7444 }
7445
7446 let mut rng = rng();
7447 let mut indexes = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
7448 indexes.shuffle(&mut rng);
7449 let random_count = rng.random_range(1..indexes.len());
7450 let random_indexes: Vec<i32> = indexes.into_iter().take(random_count).collect();
7451
7452 for j in random_indexes {
7453 let remove_backend_result = state.dispatch(
7454 &RequestType::RemoveBackend(RemoveBackend {
7455 cluster_id: String::from("cluster_1"),
7456 backend_id: format!("cluster_1-{j}"),
7457 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7458 })
7459 .into(),
7460 );
7461 assert!(remove_backend_result.is_ok());
7462 }
7463 }
7464 }
7465
7466 #[test]
7467 fn listener_diff() {
7468 let mut state: ConfigState = Default::default();
7469 let custom_http_answers = Some(CustomHttpAnswers {
7470 answer_404: Some("test".to_string()),
7471 ..Default::default()
7472 });
7473 state
7474 .dispatch(
7475 &RequestType::AddTcpListener(TcpListenerConfig {
7476 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7477 ..Default::default()
7478 })
7479 .into(),
7480 )
7481 .expect("Could not execute request");
7482 state
7483 .dispatch(
7484 &RequestType::ActivateListener(ActivateListener {
7485 interface: None,
7486 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7487 proxy: ListenerType::Tcp.into(),
7488 from_scm: false,
7489 })
7490 .into(),
7491 )
7492 .expect("Could not execute request");
7493 state
7494 .dispatch(
7495 &RequestType::AddHttpListener(HttpListenerConfig {
7496 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7497 ..Default::default()
7498 })
7499 .into(),
7500 )
7501 .expect("Could not execute request");
7502 state
7503 .dispatch(
7504 &RequestType::AddHttpsListener(HttpsListenerConfig {
7505 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7506 ..Default::default()
7507 })
7508 .into(),
7509 )
7510 .expect("Could not execute request");
7511 state
7512 .dispatch(
7513 &RequestType::ActivateListener(ActivateListener {
7514 interface: None,
7515 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7516 proxy: ListenerType::Https.into(),
7517 from_scm: false,
7518 })
7519 .into(),
7520 )
7521 .expect("Could not execute request");
7522
7523 let mut state2: ConfigState = Default::default();
7524 state2
7525 .dispatch(
7526 &RequestType::AddTcpListener(TcpListenerConfig {
7527 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7528 expect_proxy: true,
7529 ..Default::default()
7530 })
7531 .into(),
7532 )
7533 .expect("Could not execute request");
7534 state2
7535 .dispatch(
7536 &RequestType::AddHttpListener(HttpListenerConfig {
7537 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7538 http_answers: custom_http_answers.clone(),
7539 ..Default::default()
7540 })
7541 .into(),
7542 )
7543 .expect("Could not execute request");
7544 state2
7545 .dispatch(
7546 &RequestType::ActivateListener(ActivateListener {
7547 interface: None,
7548 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7549 proxy: ListenerType::Http.into(),
7550 from_scm: false,
7551 })
7552 .into(),
7553 )
7554 .expect("Could not execute request");
7555 state2
7556 .dispatch(
7557 &RequestType::AddHttpsListener(HttpsListenerConfig {
7558 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7559 http_answers: custom_http_answers.clone(),
7560 ..Default::default()
7561 })
7562 .into(),
7563 )
7564 .expect("Could not execute request");
7565 state2
7566 .dispatch(
7567 &RequestType::ActivateListener(ActivateListener {
7568 interface: None,
7569 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7570 proxy: ListenerType::Https.into(),
7571 from_scm: false,
7572 })
7573 .into(),
7574 )
7575 .expect("Could not execute request");
7576
7577 let e: Vec<Request> = vec![
7578 RequestType::RemoveListener(RemoveListener {
7579 interface: None,
7580 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7581 proxy: ListenerType::Tcp.into(),
7582 })
7583 .into(),
7584 RequestType::AddTcpListener(TcpListenerConfig {
7585 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7586 expect_proxy: true,
7587 ..Default::default()
7588 })
7589 .into(),
7590 RequestType::DeactivateListener(DeactivateListener {
7591 interface: None,
7592 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7593 proxy: ListenerType::Tcp.into(),
7594 to_scm: false,
7595 })
7596 .into(),
7597 RequestType::RemoveListener(RemoveListener {
7598 interface: None,
7599 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7600 proxy: ListenerType::Http.into(),
7601 })
7602 .into(),
7603 RequestType::AddHttpListener(HttpListenerConfig {
7604 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7605 http_answers: custom_http_answers.clone(),
7606 ..Default::default()
7607 })
7608 .into(),
7609 RequestType::ActivateListener(ActivateListener {
7610 interface: None,
7611 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7612 proxy: ListenerType::Http.into(),
7613 from_scm: false,
7614 })
7615 .into(),
7616 RequestType::RemoveListener(RemoveListener {
7617 interface: None,
7618 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7619 proxy: ListenerType::Https.into(),
7620 })
7621 .into(),
7622 RequestType::AddHttpsListener(HttpsListenerConfig {
7623 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7624 http_answers: custom_http_answers.clone(),
7625 ..Default::default()
7626 })
7627 .into(),
7628 RequestType::ActivateListener(ActivateListener {
7634 interface: None,
7635 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7636 proxy: ListenerType::Https.into(),
7637 from_scm: false,
7638 })
7639 .into(),
7640 ];
7641
7642 let diff = state.diff(&state2);
7643 println!("expected diff requests:\n{e:#?}\n");
7645 println!("diff requests:\n{diff:#?}\n");
7646
7647 let _hash1 = state.hash_state();
7648 let _hash2 = state2.hash_state();
7649
7650 assert_eq!(diff, e);
7651
7652 let mut replayed = state.clone();
7659 for request in &diff {
7660 replayed
7661 .dispatch(request)
7662 .expect("every diff request must replay cleanly onto the source state");
7663 }
7664 assert_eq!(
7665 replayed.tcp_listeners, state2.tcp_listeners,
7666 "replayed tcp_listeners must match the target state"
7667 );
7668 assert_eq!(
7669 replayed.http_listeners, state2.http_listeners,
7670 "replayed http_listeners must match the target state"
7671 );
7672 assert_eq!(
7673 replayed.https_listeners, state2.https_listeners,
7674 "replayed https_listeners must match the target state"
7675 );
7676 let replayed_8443 = replayed
7679 .https_listeners
7680 .get(&ListenerKey::from(SocketAddress::new_v4(0, 0, 0, 0, 8443)))
7681 .expect("8443 HTTPS listener must exist after replay");
7682 assert!(
7683 replayed_8443.active,
7684 "the 8443 HTTPS listener must stay ACTIVE across a config change"
7685 );
7686 }
7687
7688 #[test]
7689 fn certificate_retrieval() {
7690 let mut state: ConfigState = Default::default();
7691 let certificate_and_key = CertificateAndKey {
7692 certificate: String::from(include_str!("../assets/certificate.pem")),
7693 key: String::from(include_str!("../assets/key.pem")),
7694 certificate_chain: vec![],
7695 versions: vec![],
7696 names: vec!["lolcatho.st".to_string()],
7697 };
7698 let add_certificate = AddCertificate {
7699 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
7700 certificate: certificate_and_key,
7701 expired_at: None,
7702 };
7703 state
7704 .dispatch(&RequestType::AddCertificate(add_certificate).into())
7705 .expect("Could not add certificate");
7706
7707 println!("state: {state:#?}");
7708
7709 let certificates_found_by_fingerprint = state.get_certificates(QueryCertificatesFilters {
7715 domain: None,
7716 fingerprint: Some(
7717 "ab2618b674e15243fd02a5618c66509e4840ba60e7d64cebec84cdbfeceee0c5".to_string(),
7718 ),
7719 });
7720
7721 println!("found certificate: {certificates_found_by_fingerprint:#?}");
7722
7723 assert!(!certificates_found_by_fingerprint.is_empty());
7724
7725 let certificate_found_by_domain_name = state.get_certificates(QueryCertificatesFilters {
7726 domain: Some("lolcatho.st".to_string()),
7727 fingerprint: None,
7728 });
7729
7730 assert!(!certificate_found_by_domain_name.is_empty());
7731 }
7732
7733 #[test]
7734 fn count_backends_across_clusters() {
7735 let mut state: ConfigState = Default::default();
7736 add_clusters(&mut state, &["cluster_1", "cluster_2"]);
7737
7738 assert_eq!(state.count_backends(), 0);
7739
7740 state
7741 .dispatch(
7742 &RequestType::AddBackend(AddBackend {
7743 cluster_id: String::from("cluster_1"),
7744 backend_id: String::from("cluster_1-0"),
7745 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7746 ..Default::default()
7747 })
7748 .into(),
7749 )
7750 .expect("Could not execute request");
7751 assert_eq!(state.count_backends(), 1);
7752
7753 state
7754 .dispatch(
7755 &RequestType::AddBackend(AddBackend {
7756 cluster_id: String::from("cluster_1"),
7757 backend_id: String::from("cluster_1-1"),
7758 address: SocketAddress::new_v4(127, 0, 0, 1, 1027),
7759 ..Default::default()
7760 })
7761 .into(),
7762 )
7763 .expect("Could not execute request");
7764 assert_eq!(state.count_backends(), 2);
7765
7766 state
7768 .dispatch(
7769 &RequestType::AddBackend(AddBackend {
7770 cluster_id: String::from("cluster_2"),
7771 backend_id: String::from("cluster_2-0"),
7772 address: SocketAddress::new_v4(192, 168, 1, 1, 8080),
7773 ..Default::default()
7774 })
7775 .into(),
7776 )
7777 .expect("Could not execute request");
7778 assert_eq!(state.count_backends(), 3);
7779
7780 state
7782 .dispatch(
7783 &RequestType::RemoveBackend(RemoveBackend {
7784 cluster_id: String::from("cluster_1"),
7785 backend_id: String::from("cluster_1-0"),
7786 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7787 })
7788 .into(),
7789 )
7790 .expect("Could not execute request");
7791 assert_eq!(state.count_backends(), 2);
7792 }
7793
7794 #[test]
7824 fn a_regex_hostname_is_always_replayed_before_the_exact_name_it_matches() {
7825 let mut state: ConfigState = Default::default();
7826
7827 for hostname in ["test4.example.com", "/test[0-9]/.example.com"] {
7830 state
7831 .dispatch(
7832 &RequestType::AddHttpFrontend(RequestHttpFrontend {
7833 cluster_id: Some(String::from("cluster_1")),
7834 hostname: String::from(hostname),
7835 path: PathRule::prefix(String::from("/")),
7836 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7837 position: RulePosition::Tree.into(),
7838 ..Default::default()
7839 })
7840 .into(),
7841 )
7842 .expect("Could not execute request");
7843 }
7844
7845 let replayed: Vec<String> = state
7848 .produce_initial_state()
7849 .requests
7850 .into_iter()
7851 .filter_map(|worker_request| match worker_request.content.request_type {
7852 Some(RequestType::AddHttpFrontend(front)) => Some(front.hostname),
7853 _ => None,
7854 })
7855 .collect();
7856
7857 assert_eq!(
7858 replayed,
7859 vec![
7860 String::from("/test[0-9]/.example.com"),
7861 String::from("test4.example.com"),
7862 ],
7863 "a worker replays the regex segment before the exact name it \
7864 matches, whatever order the operator added them in",
7865 );
7866 }
7867
7868 #[test]
7869 fn count_frontends_across_types() {
7870 let mut state: ConfigState = Default::default();
7871
7872 assert_eq!(state.count_frontends(), 0);
7873
7874 state
7876 .dispatch(
7877 &RequestType::AddHttpFrontend(RequestHttpFrontend {
7878 cluster_id: Some(String::from("cluster_1")),
7879 hostname: String::from("example.com"),
7880 path: PathRule::prefix(String::from("/")),
7881 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7882 position: RulePosition::Tree.into(),
7883 ..Default::default()
7884 })
7885 .into(),
7886 )
7887 .expect("Could not execute request");
7888 assert_eq!(state.count_frontends(), 1);
7889
7890 state
7892 .dispatch(
7893 &RequestType::AddHttpsFrontend(RequestHttpFrontend {
7894 cluster_id: Some(String::from("cluster_1")),
7895 hostname: String::from("secure.example.com"),
7896 path: PathRule::prefix(String::from("/")),
7897 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7898 position: RulePosition::Tree.into(),
7899 ..Default::default()
7900 })
7901 .into(),
7902 )
7903 .expect("Could not execute request");
7904 assert_eq!(state.count_frontends(), 2);
7905
7906 state
7908 .dispatch(
7909 &RequestType::AddTcpFrontend(RequestTcpFrontend {
7910 cluster_id: String::from("cluster_2"),
7911 address: SocketAddress::new_v4(0, 0, 0, 0, 5432),
7912 ..Default::default()
7913 })
7914 .into(),
7915 )
7916 .expect("Could not execute request");
7917 assert_eq!(state.count_frontends(), 3);
7918
7919 state
7921 .dispatch(
7922 &RequestType::AddTcpFrontend(RequestTcpFrontend {
7923 cluster_id: String::from("cluster_2"),
7924 address: SocketAddress::new_v4(0, 0, 0, 0, 5433),
7925 ..Default::default()
7926 })
7927 .into(),
7928 )
7929 .expect("Could not execute request");
7930 assert_eq!(state.count_frontends(), 4);
7931
7932 state
7934 .dispatch(
7935 &RequestType::RemoveHttpFrontend(RequestHttpFrontend {
7936 cluster_id: Some(String::from("cluster_1")),
7937 hostname: String::from("example.com"),
7938 path: PathRule::prefix(String::from("/")),
7939 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7940 position: RulePosition::Tree.into(),
7941 ..Default::default()
7942 })
7943 .into(),
7944 )
7945 .expect("Could not execute request");
7946 assert_eq!(state.count_frontends(), 3);
7947 }
7948
7949 #[test]
7955 fn https_listener_requests_carry_client_auth_on_their_own_verb() {
7956 let mtls_address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
7957 let plain_address = SocketAddress::new_v4(127, 0, 0, 1, 8444);
7958 let mtls = HttpsListenerConfig {
7959 client_auth: Some(ClientAuthMode::ClientAuthRequired as i32),
7960 client_ca_certificates: vec!["CLIENT CA PEM".to_owned()],
7961 ..make_https_listener(mtls_address)
7962 };
7963
7964 let mut state = ConfigState::new();
7965 state
7966 .dispatch(&RequestType::AddHttpsListenerWithClientAuth(mtls.clone()).into())
7967 .expect("the mTLS verb must add the listener to the state");
7968 state
7969 .dispatch(&RequestType::AddHttpsListener(make_https_listener(plain_address)).into())
7970 .expect("the historical verb must keep adding listeners without client auth");
7971 assert_eq!(
7972 state.https_listeners.len(),
7973 2,
7974 "both verbs add a listener to the same state"
7975 );
7976
7977 let https_adds = |requests: Vec<Request>| -> Vec<(SocketAddress, bool)> {
7978 requests
7979 .into_iter()
7980 .filter_map(|request| match request.request_type {
7981 Some(RequestType::AddHttpsListener(listener)) => {
7982 Some((listener.address, false))
7983 }
7984 Some(RequestType::AddHttpsListenerWithClientAuth(listener)) => {
7985 Some((listener.address, true))
7986 }
7987 _ => None,
7988 })
7989 .collect()
7990 };
7991 let mut generated = https_adds(state.generate_requests());
7992 generated.sort_by_key(|(address, _)| address.port);
7993 assert_eq!(
7994 generated,
7995 vec![(mtls_address, true), (plain_address, false)],
7996 "generate_requests must put only the listener asking for a client certificate on the mTLS verb"
7997 );
7998
7999 let diff = https_adds(ConfigState::new().diff(&state));
8000 assert!(
8001 diff.contains(&(mtls_address, true)) && !diff.contains(&(mtls_address, false)),
8002 "the reload diff must add the mTLS listener on the mTLS verb: {diff:?}"
8003 );
8004 }
8005
8006 #[test]
8011 fn a_reload_refreshes_the_client_auth_policy_of_a_running_listener() {
8012 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
8013 let with_crl = |crl: &str| HttpsListenerConfig {
8014 client_auth: Some(ClientAuthMode::ClientAuthRequired as i32),
8015 client_ca_certificates: vec!["CLIENT CA PEM".to_owned()],
8016 client_ca_crls: vec![crl.to_owned()],
8017 ..make_https_listener(address)
8018 };
8019
8020 let mut state = ConfigState::new();
8021 assert_eq!(
8022 state.client_auth_refresh(&with_crl("CRL 1")),
8023 None,
8024 "a listener the state does not hold is added, not patched"
8025 );
8026 state
8027 .dispatch(&RequestType::add_https_listener(with_crl("CRL 1")).into())
8028 .expect("add the listener");
8029 assert_eq!(
8030 state.client_auth_refresh(&with_crl("CRL 1")),
8031 None,
8032 "an unchanged policy needs no patch"
8033 );
8034 assert!(
8035 matches!(
8036 state.dispatch(&RequestType::add_https_listener(with_crl("CRL 2")).into()),
8037 Err(StateError::Exists { .. })
8038 ),
8039 "precondition: replaying the add for a held address is refused"
8040 );
8041
8042 let patch = state
8043 .client_auth_refresh(&with_crl("CRL 2"))
8044 .expect("a refreshed CRL must produce a patch");
8045 assert_eq!(patch.address, address);
8046 assert_eq!(
8047 patch.client_auth_policy,
8048 Some(with_crl("CRL 2").client_auth_policy())
8049 );
8050 assert_eq!(
8051 UpdateHttpsListenerConfig {
8052 client_auth_policy: None,
8053 ..patch.clone()
8054 },
8055 UpdateHttpsListenerConfig {
8056 address,
8057 ..Default::default()
8058 },
8059 "the patch carries the policy and nothing else"
8060 );
8061
8062 state
8063 .dispatch(&RequestType::update_https_listener(patch).into())
8064 .expect("the state must apply the policy patch");
8065 let running = state
8066 .https_listeners
8067 .values()
8068 .next()
8069 .expect("the listener is still there");
8070 assert_eq!(running.client_ca_crls, vec!["CRL 2".to_owned()]);
8071 assert_eq!(
8072 state.client_auth_refresh(&with_crl("CRL 2")),
8073 None,
8074 "once applied, the same reload has nothing left to refresh"
8075 );
8076 }
8077
8078 fn make_https_listener(address: SocketAddress) -> HttpsListenerConfig {
8081 HttpsListenerConfig {
8082 address,
8083 sticky_name: "SOZUBALANCEID".to_owned(),
8084 front_timeout: 60,
8085 back_timeout: 30,
8086 connect_timeout: 3,
8087 request_timeout: 10,
8088 ..Default::default()
8089 }
8090 }
8091
8092 fn make_http_listener(address: SocketAddress) -> HttpListenerConfig {
8093 HttpListenerConfig {
8094 address,
8095 sticky_name: "SOZUBALANCEID".to_owned(),
8096 front_timeout: 60,
8097 back_timeout: 30,
8098 connect_timeout: 3,
8099 request_timeout: 10,
8100 ..Default::default()
8101 }
8102 }
8103
8104 fn make_tcp_listener(address: SocketAddress) -> TcpListenerConfig {
8105 TcpListenerConfig {
8106 address,
8107 front_timeout: 60,
8108 back_timeout: 30,
8109 connect_timeout: 3,
8110 ..Default::default()
8111 }
8112 }
8113
8114 fn make_udp_listener(address: SocketAddress, active: bool) -> UdpListenerConfig {
8115 UdpListenerConfig {
8116 interface: None,
8117 address,
8118 public_address: None,
8119 front_timeout: 30,
8120 back_timeout: 30,
8121 max_rx_datagram_size: 1500,
8122 max_flows: 0,
8123 active,
8124 }
8125 }
8126
8127 #[test]
8132 fn activation_rollback_restores_the_prior_flag_for_every_listener_type() {
8133 let mut state = ConfigState::new();
8134 let cases = [
8135 (ListenerType::Http, 8200),
8136 (ListenerType::Https, 8201),
8137 (ListenerType::Tcp, 8202),
8138 (ListenerType::Udp, 8203),
8139 ];
8140
8141 for (listener_type, port) in cases {
8142 let address = SocketAddress::new_v4(127, 0, 0, 1, port);
8143 let interface = Some(format!("test-{port}"));
8144 let proxy: i32 = listener_type.into();
8145 let add = match listener_type {
8146 ListenerType::Http => RequestType::AddHttpListener(with_interface(
8147 make_http_listener(address),
8148 |listener| listener.interface = interface.to_owned(),
8149 )),
8150 ListenerType::Https => RequestType::AddHttpsListener(with_interface(
8151 make_https_listener(address),
8152 |listener| listener.interface = interface.to_owned(),
8153 )),
8154 ListenerType::Tcp => RequestType::AddTcpListener(with_interface(
8155 make_tcp_listener(address),
8156 |listener| listener.interface = interface.to_owned(),
8157 )),
8158 ListenerType::Udp => RequestType::AddUdpListener(with_interface(
8159 make_udp_listener(address, false),
8160 |listener| listener.interface = interface.to_owned(),
8161 )),
8162 };
8163 state
8164 .dispatch(&add.into())
8165 .expect("the inactive listener must be recorded");
8166
8167 let activate = ActivateListener {
8168 address,
8169 proxy,
8170 from_scm: false,
8171 interface: interface.to_owned(),
8172 };
8173 let rollback = state
8174 .rollback_for(&RequestType::ActivateListener(activate.to_owned()))
8175 .expect("an inactive listener must receive an activation rollback");
8176 assert!(matches!(
8177 rollback.request_type,
8178 Some(RequestType::DeactivateListener(DeactivateListener {
8179 address: rollback_address,
8180 proxy,
8181 to_scm: false,
8182 interface: rollback_interface,
8183 })) if rollback_address == address
8184 && proxy == activate.proxy
8185 && rollback_interface == interface
8186 ));
8187
8188 state
8189 .dispatch(&RequestType::ActivateListener(activate.to_owned()).into())
8190 .expect("the listener must become active in the stored state");
8191 assert!(
8192 state
8193 .rollback_for(&RequestType::ActivateListener(activate))
8194 .is_none(),
8195 "repeating activation for an already-active {listener_type:?} listener must not create a deactivation rollback"
8196 );
8197 }
8198 }
8199
8200 #[test]
8210 fn test_udp_state_roundtrip() {
8211 let address = SocketAddress::new_v4(127, 0, 0, 1, 5353);
8212
8213 let mut state = ConfigState::default();
8214 state
8215 .dispatch(&RequestType::AddUdpListener(make_udp_listener(address, true)).into())
8216 .expect("could not add udp listener");
8217 state
8218 .dispatch(
8219 &RequestType::ActivateListener(ActivateListener {
8220 interface: None,
8221 address,
8222 proxy: ListenerType::Udp.into(),
8223 from_scm: false,
8224 })
8225 .into(),
8226 )
8227 .expect("could not activate udp listener");
8228 state
8229 .dispatch(
8230 &RequestType::AddUdpFrontend(RequestUdpFrontend {
8231 cluster_id: "udp_cluster".to_string(),
8232 address,
8233 tags: BTreeMap::from([("owner".to_string(), "team".to_string())]),
8234 })
8235 .into(),
8236 )
8237 .expect("could not add udp frontend");
8238
8239 assert_eq!(state.udp_listeners.len(), 1);
8240 assert!(state.udp_listeners[&address.into()].active);
8241 assert_eq!(
8242 state.udp_fronts.get("udp_cluster").map(Vec::len),
8243 Some(1usize)
8244 );
8245
8246 let logical = |s: &ConfigState| {
8251 let mut c = s.clone();
8252 c.request_counts.clear();
8253 c
8254 };
8255
8256 let mut replayed = ConfigState::default();
8258 for request in state.generate_requests() {
8259 replayed
8260 .dispatch(&request)
8261 .expect("could not replay generated request");
8262 }
8263 assert_eq!(
8264 logical(&state),
8265 logical(&replayed),
8266 "UDP listener + frontend must survive generate_requests → replay"
8267 );
8268
8269 let empty = ConfigState::default();
8271 let mut from_diff = ConfigState::default();
8272 for request in empty.diff(&state) {
8273 from_diff
8274 .dispatch(&request)
8275 .expect("could not replay diff request");
8276 }
8277 assert_eq!(
8278 logical(&state),
8279 logical(&from_diff),
8280 "diff(empty -> state) must reconstruct the UDP listener + frontend"
8281 );
8282
8283 let mut torn_down = state.clone();
8285 for request in state.diff(&empty) {
8286 torn_down
8287 .dispatch(&request)
8288 .expect("could not replay teardown diff request");
8289 }
8290 assert!(
8291 torn_down.udp_listeners.is_empty(),
8292 "diff(state -> empty) must remove the UDP listener"
8293 );
8294 assert!(
8295 torn_down
8296 .udp_fronts
8297 .get("udp_cluster")
8298 .map(Vec::is_empty)
8299 .unwrap_or(true),
8300 "diff(state -> empty) must remove the UDP frontend"
8301 );
8302
8303 state
8305 .dispatch(
8306 &RequestType::UpdateUdpListener(UpdateUdpListenerConfig {
8307 address,
8308 max_flows: Some(4096),
8309 front_timeout: Some(15),
8310 ..Default::default()
8311 })
8312 .into(),
8313 )
8314 .expect("could not update udp listener");
8315 let updated = &state.udp_listeners[&address.into()];
8316 assert_eq!(updated.max_flows, 4096);
8317 assert_eq!(updated.front_timeout, 15);
8318 assert_eq!(
8319 updated.back_timeout, 30,
8320 "unpatched field must be preserved"
8321 );
8322 }
8323
8324 #[test]
8339 fn test_tcp_sni_alpn_state_roundtrip() {
8340 let address = SocketAddress::new_v4(127, 0, 0, 1, 6443);
8341
8342 let mut state = ConfigState::default();
8343 state
8344 .dispatch(
8345 &RequestType::AddTcpListener(TcpListenerConfig {
8346 address,
8347 front_timeout: 60,
8348 back_timeout: 30,
8349 connect_timeout: 3,
8350 active: true,
8351 sni_preread_timeout: Some(2),
8352 sni_preread_max_bytes: Some(8192),
8353 ..Default::default()
8354 })
8355 .into(),
8356 )
8357 .expect("could not add tcp listener");
8358 state
8359 .dispatch(
8360 &RequestType::ActivateListener(ActivateListener {
8361 interface: None,
8362 address,
8363 proxy: ListenerType::Tcp.into(),
8364 from_scm: false,
8365 })
8366 .into(),
8367 )
8368 .expect("could not activate tcp listener");
8369
8370 let frontend_a = RequestTcpFrontend {
8371 cluster_id: "cluster_sni_a".to_string(),
8372 address,
8373 tags: BTreeMap::new(),
8374 sni: Some("example.com".to_string()),
8375 alpn: vec!["h2".to_string()],
8376 };
8377 let frontend_b = RequestTcpFrontend {
8378 cluster_id: "cluster_sni_b".to_string(),
8379 address,
8380 tags: BTreeMap::new(),
8381 sni: Some("other.example.com".to_string()),
8382 alpn: vec![],
8383 };
8384 state
8385 .dispatch(&RequestType::AddTcpFrontend(frontend_a.clone()).into())
8386 .expect("could not add sni-a tcp frontend");
8387 state
8388 .dispatch(&RequestType::AddTcpFrontend(frontend_b.clone()).into())
8389 .expect("could not add sni-b tcp frontend");
8390
8391 assert_eq!(state.tcp_listeners.len(), 1);
8392 assert!(state.tcp_listeners[&address.into()].active);
8393 assert_eq!(
8394 state.tcp_listeners[&address.into()].sni_preread_timeout,
8395 Some(2)
8396 );
8397 assert_eq!(
8398 state.tcp_listeners[&address.into()].sni_preread_max_bytes,
8399 Some(8192)
8400 );
8401 assert_eq!(state.count_tcp_frontends_raw(), 2);
8402
8403 let logical = |s: &ConfigState| {
8408 let mut c = s.clone();
8409 c.request_counts.clear();
8410 c
8411 };
8412
8413 let mut replayed = ConfigState::default();
8415 for request in state.generate_requests() {
8416 replayed
8417 .dispatch(&request)
8418 .expect("could not replay generated request");
8419 }
8420 assert_eq!(
8421 logical(&state),
8422 logical(&replayed),
8423 "SNI/ALPN TCP listener + frontends must survive generate_requests → replay"
8424 );
8425
8426 let empty = ConfigState::default();
8428 let mut from_diff = ConfigState::default();
8429 for request in empty.diff(&state) {
8430 from_diff
8431 .dispatch(&request)
8432 .expect("could not replay diff request");
8433 }
8434 assert_eq!(
8435 logical(&state),
8436 logical(&from_diff),
8437 "diff(empty -> state) must reconstruct the tcp listener + both sni frontends"
8438 );
8439
8440 let mut torn_down = state.clone();
8442 for request in state.diff(&empty) {
8443 torn_down
8444 .dispatch(&request)
8445 .expect("could not replay teardown diff request");
8446 }
8447 assert!(
8448 torn_down.tcp_listeners.is_empty(),
8449 "diff(state -> empty) must remove the tcp listener"
8450 );
8451 assert_eq!(
8452 torn_down.count_tcp_frontends_raw(),
8453 0,
8454 "diff(state -> empty) must remove both sni frontends"
8455 );
8456
8457 let mismatched_removal = state.dispatch(
8461 &RequestType::RemoveTcpFrontend(RequestTcpFrontend {
8462 cluster_id: "cluster_sni_a".to_string(),
8463 address,
8464 tags: BTreeMap::new(),
8465 sni: None,
8466 alpn: vec![],
8467 })
8468 .into(),
8469 );
8470 assert!(
8471 mismatched_removal.is_err(),
8472 "removing by address alone (no matching sni/alpn) must be rejected"
8473 );
8474 assert_eq!(
8475 state.count_tcp_frontends_raw(),
8476 2,
8477 "a rejected mismatched removal must not change either sibling"
8478 );
8479
8480 state
8483 .dispatch(&RequestType::RemoveTcpFrontend(frontend_a).into())
8484 .expect("could not remove sni-a tcp frontend");
8485 assert_eq!(state.count_tcp_frontends_raw(), 1);
8486 let remaining = state
8487 .tcp_fronts
8488 .get("cluster_sni_b")
8489 .expect("cluster_sni_b bucket must survive");
8490 assert_eq!(remaining.len(), 1);
8491 assert_eq!(remaining[0].sni, Some("other.example.com".to_string()));
8492 }
8493
8494 fn add_tcp_test_cluster(state: &mut ConfigState, cluster_id: &str) {
8505 state
8506 .dispatch(
8507 &RequestType::AddCluster(Cluster {
8508 cluster_id: cluster_id.to_string(),
8509 sticky_session: false,
8510 https_redirect: false,
8511 ..Default::default()
8512 })
8513 .into(),
8514 )
8515 .expect("could not add cluster");
8516 }
8517
8518 #[test]
8523 fn add_tcp_frontend_rejects_cross_cluster_duplicate_identity() {
8524 let address = SocketAddress::new_v4(127, 0, 0, 1, 9001);
8525 let mut state = ConfigState::new();
8526 add_tcp_test_cluster(&mut state, "cluster_x");
8527 add_tcp_test_cluster(&mut state, "cluster_y");
8528
8529 state
8530 .dispatch(
8531 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8532 cluster_id: "cluster_x".to_string(),
8533 address,
8534 tags: BTreeMap::new(),
8535 sni: Some("example.com".to_string()),
8536 alpn: vec!["h2".to_string()],
8537 })
8538 .into(),
8539 )
8540 .expect("could not add cluster_x tcp frontend");
8541
8542 let hash_before = state.hash_state();
8543 let count_before = state.count_tcp_frontends_raw();
8544
8545 let result = state.dispatch(
8546 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8547 cluster_id: "cluster_y".to_string(),
8548 address,
8549 tags: BTreeMap::new(),
8550 sni: Some("example.com".to_string()),
8551 alpn: vec!["h2".to_string()],
8552 })
8553 .into(),
8554 );
8555 assert!(
8556 result.is_err(),
8557 "the same (address, sni, alpn) under a different cluster_id must be rejected, \
8558 got: {result:?}"
8559 );
8560 assert_eq!(
8561 state.hash_state(),
8562 hash_before,
8563 "a rejected cross-cluster duplicate must not change the state hash"
8564 );
8565 assert_eq!(
8566 state.count_tcp_frontends_raw(),
8567 count_before,
8568 "a rejected cross-cluster duplicate must not change the frontend count"
8569 );
8570 assert_eq!(state.tcp_fronts.get("cluster_x").map(Vec::len), Some(1));
8571 assert_eq!(
8572 state.tcp_fronts.get("cluster_y").map(Vec::len).unwrap_or(0),
8573 0
8574 );
8575 }
8576
8577 #[test]
8580 fn add_tcp_frontend_rejects_reordered_alpn_duplicate() {
8581 let address = SocketAddress::new_v4(127, 0, 0, 1, 9002);
8582 let mut state = ConfigState::new();
8583 add_tcp_test_cluster(&mut state, "cluster_reorder");
8584
8585 state
8586 .dispatch(
8587 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8588 cluster_id: "cluster_reorder".to_string(),
8589 address,
8590 tags: BTreeMap::new(),
8591 sni: Some("example.com".to_string()),
8592 alpn: vec!["h2".to_string(), "http/1.1".to_string()],
8593 })
8594 .into(),
8595 )
8596 .expect("could not add first tcp frontend");
8597
8598 let hash_before = state.hash_state();
8599 let count_before = state.count_tcp_frontends_raw();
8600
8601 let result = state.dispatch(
8602 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8603 cluster_id: "cluster_reorder".to_string(),
8604 address,
8605 tags: BTreeMap::new(),
8606 sni: Some("example.com".to_string()),
8607 alpn: vec!["http/1.1".to_string(), "h2".to_string()],
8608 })
8609 .into(),
8610 );
8611 assert!(
8612 result.is_err(),
8613 "the same ALPN set in a different order must be rejected as a duplicate \
8614 identity, got: {result:?}"
8615 );
8616 assert_eq!(state.hash_state(), hash_before);
8617 assert_eq!(state.count_tcp_frontends_raw(), count_before);
8618 assert_eq!(
8619 state.tcp_fronts.get("cluster_reorder").map(Vec::len),
8620 Some(1)
8621 );
8622 }
8623
8624 #[test]
8628 fn remove_tcp_frontend_matches_reordered_alpn() {
8629 let address = SocketAddress::new_v4(127, 0, 0, 1, 9003);
8630 let mut state = ConfigState::new();
8631 add_tcp_test_cluster(&mut state, "cluster_remove_reorder");
8632
8633 state
8634 .dispatch(
8635 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8636 cluster_id: "cluster_remove_reorder".to_string(),
8637 address,
8638 tags: BTreeMap::new(),
8639 sni: Some("example.com".to_string()),
8640 alpn: vec!["h2".to_string(), "http/1.1".to_string()],
8641 })
8642 .into(),
8643 )
8644 .expect("could not add tcp frontend");
8645
8646 let result = state.dispatch(
8647 &RequestType::RemoveTcpFrontend(RequestTcpFrontend {
8648 cluster_id: "cluster_remove_reorder".to_string(),
8649 address,
8650 tags: BTreeMap::new(),
8651 sni: Some("example.com".to_string()),
8652 alpn: vec!["http/1.1".to_string(), "h2".to_string()],
8653 })
8654 .into(),
8655 );
8656 assert!(
8657 result.is_ok(),
8658 "removing with the same ALPN set in a different order must succeed, got: {result:?}"
8659 );
8660 assert_eq!(state.count_tcp_frontends_raw(), 0);
8661 }
8662
8663 #[test]
8667 fn add_tcp_frontend_rejects_alpn_overlap_across_clusters_same_sni() {
8668 let address = SocketAddress::new_v4(127, 0, 0, 1, 9004);
8669 let mut state = ConfigState::new();
8670 add_tcp_test_cluster(&mut state, "cluster_overlap_a");
8671 add_tcp_test_cluster(&mut state, "cluster_overlap_b");
8672
8673 state
8674 .dispatch(
8675 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8676 cluster_id: "cluster_overlap_a".to_string(),
8677 address,
8678 tags: BTreeMap::new(),
8679 sni: Some("example.com".to_string()),
8680 alpn: vec!["h2".to_string()],
8681 })
8682 .into(),
8683 )
8684 .expect("could not add cluster_overlap_a tcp frontend");
8685
8686 let hash_before = state.hash_state();
8687 let count_before = state.count_tcp_frontends_raw();
8688
8689 let result = state.dispatch(
8690 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8691 cluster_id: "cluster_overlap_b".to_string(),
8692 address,
8693 tags: BTreeMap::new(),
8694 sni: Some("example.com".to_string()),
8695 alpn: vec!["h2".to_string(), "http/1.1".to_string()],
8696 })
8697 .into(),
8698 );
8699 assert!(
8700 result.is_err(),
8701 "an overlapping ALPN set on the same (address, sni) under a different cluster_id \
8702 must be rejected, got: {result:?}"
8703 );
8704 assert_eq!(state.hash_state(), hash_before);
8705 assert_eq!(state.count_tcp_frontends_raw(), count_before);
8706 assert_eq!(
8707 state
8708 .tcp_fronts
8709 .get("cluster_overlap_b")
8710 .map(Vec::len)
8711 .unwrap_or(0),
8712 0
8713 );
8714 }
8715
8716 #[test]
8719 fn add_tcp_frontend_rejects_no_sni_sni_mixing_both_directions() {
8720 let address_a = SocketAddress::new_v4(127, 0, 0, 1, 9005);
8721 let address_b = SocketAddress::new_v4(127, 0, 0, 1, 9006);
8722 let mut state = ConfigState::new();
8723 add_tcp_test_cluster(&mut state, "cluster_mix_1");
8724 add_tcp_test_cluster(&mut state, "cluster_mix_2");
8725 add_tcp_test_cluster(&mut state, "cluster_mix_3");
8726 add_tcp_test_cluster(&mut state, "cluster_mix_4");
8727
8728 state
8731 .dispatch(
8732 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8733 cluster_id: "cluster_mix_1".to_string(),
8734 address: address_a,
8735 tags: BTreeMap::new(),
8736 sni: None,
8737 alpn: vec![],
8738 })
8739 .into(),
8740 )
8741 .expect("could not add no-sni tcp frontend");
8742 let hash_before_a = state.hash_state();
8743 let count_before_a = state.count_tcp_frontends_raw();
8744 let result_a = state.dispatch(
8745 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8746 cluster_id: "cluster_mix_2".to_string(),
8747 address: address_a,
8748 tags: BTreeMap::new(),
8749 sni: Some("example.com".to_string()),
8750 alpn: vec![],
8751 })
8752 .into(),
8753 );
8754 assert!(
8755 result_a.is_err(),
8756 "an SNI-scoped frontend must not be added to a listener with an existing no-SNI \
8757 front, got: {result_a:?}"
8758 );
8759 assert_eq!(state.hash_state(), hash_before_a);
8760 assert_eq!(state.count_tcp_frontends_raw(), count_before_a);
8761
8762 state
8765 .dispatch(
8766 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8767 cluster_id: "cluster_mix_3".to_string(),
8768 address: address_b,
8769 tags: BTreeMap::new(),
8770 sni: Some("example.com".to_string()),
8771 alpn: vec![],
8772 })
8773 .into(),
8774 )
8775 .expect("could not add sni-scoped tcp frontend");
8776 let hash_before_b = state.hash_state();
8777 let count_before_b = state.count_tcp_frontends_raw();
8778 let result_b = state.dispatch(
8779 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8780 cluster_id: "cluster_mix_4".to_string(),
8781 address: address_b,
8782 tags: BTreeMap::new(),
8783 sni: None,
8784 alpn: vec![],
8785 })
8786 .into(),
8787 );
8788 assert!(
8789 result_b.is_err(),
8790 "a no-SNI frontend must not be added to a listener with existing SNI-scoped \
8791 fronts, got: {result_b:?}"
8792 );
8793 assert_eq!(state.hash_state(), hash_before_b);
8794 assert_eq!(state.count_tcp_frontends_raw(), count_before_b);
8795 }
8796
8797 #[test]
8802 fn add_tcp_frontend_rejects_malformed_sni_shape() {
8803 let address = SocketAddress::new_v4(127, 0, 0, 1, 9007);
8804 let mut state = ConfigState::new();
8805 add_tcp_test_cluster(&mut state, "cluster_malformed");
8806
8807 let hash_before = state.hash_state();
8808 let count_before = state.count_tcp_frontends_raw();
8809
8810 let result = state.dispatch(
8811 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8812 cluster_id: "cluster_malformed".to_string(),
8813 address,
8814 tags: BTreeMap::new(),
8815 sni: Some(".example.com".to_string()),
8817 alpn: vec![],
8818 })
8819 .into(),
8820 );
8821 assert!(
8822 result.is_err(),
8823 "a malformed SNI pattern must be rejected by the master, not merely by \
8824 config-load, got: {result:?}"
8825 );
8826 assert_eq!(state.hash_state(), hash_before);
8827 assert_eq!(state.count_tcp_frontends_raw(), count_before);
8828 assert_eq!(
8829 state
8830 .tcp_fronts
8831 .get("cluster_malformed")
8832 .map(Vec::len)
8833 .unwrap_or(0),
8834 0
8835 );
8836 }
8837
8838 #[test]
8844 fn add_tcp_frontend_rejects_alpn_without_sni() {
8845 let address = SocketAddress::new_v4(127, 0, 0, 1, 9009);
8846 let mut state = ConfigState::new();
8847 add_tcp_test_cluster(&mut state, "cluster_alpn_no_sni");
8848
8849 let hash_before = state.hash_state();
8850 let count_before = state.count_tcp_frontends_raw();
8851
8852 let result = state.dispatch(
8853 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8854 cluster_id: "cluster_alpn_no_sni".to_string(),
8855 address,
8856 tags: BTreeMap::new(),
8857 sni: None,
8858 alpn: vec!["h2".to_string()],
8859 })
8860 .into(),
8861 );
8862 assert!(
8863 result.is_err(),
8864 "a non-empty alpn without sni must be rejected by the master, got: {result:?}"
8865 );
8866 assert_eq!(
8867 state.hash_state(),
8868 hash_before,
8869 "a rejected alpn-without-sni add must not change the state hash"
8870 );
8871 assert_eq!(
8872 state.count_tcp_frontends_raw(),
8873 count_before,
8874 "a rejected alpn-without-sni add must not change the frontend count"
8875 );
8876 assert_eq!(
8877 state
8878 .tcp_fronts
8879 .get("cluster_alpn_no_sni")
8880 .map(Vec::len)
8881 .unwrap_or(0),
8882 0
8883 );
8884 }
8885
8886 #[test]
8892 fn add_tcp_frontend_accepts_exact_and_wildcard_catch_all_siblings() {
8893 let address = SocketAddress::new_v4(127, 0, 0, 1, 9008);
8894 let mut state = ConfigState::new();
8895 add_tcp_test_cluster(&mut state, "cluster_exact_catchall");
8896 add_tcp_test_cluster(&mut state, "cluster_wildcard_catchall");
8897
8898 state
8899 .dispatch(
8900 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8901 cluster_id: "cluster_exact_catchall".to_string(),
8902 address,
8903 tags: BTreeMap::new(),
8904 sni: Some("example.com".to_string()),
8905 alpn: vec![],
8906 })
8907 .into(),
8908 )
8909 .expect("could not add exact catch-all tcp frontend");
8910
8911 let result = state.dispatch(
8912 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8913 cluster_id: "cluster_wildcard_catchall".to_string(),
8914 address,
8915 tags: BTreeMap::new(),
8916 sni: Some("*.example.com".to_string()),
8917 alpn: vec![],
8918 })
8919 .into(),
8920 );
8921 assert!(
8922 result.is_ok(),
8923 "an exact catch-all and a wildcard catch-all on the same address must both be \
8924 accepted, got: {result:?}"
8925 );
8926 assert_eq!(state.count_tcp_frontends_raw(), 2);
8927 }
8928
8929 #[test]
8933 fn list_frontends_includes_udp() {
8934 let tcp_addr = SocketAddress::new_v4(0, 0, 0, 0, 6379);
8935 let udp_addr = SocketAddress::new_v4(0, 0, 0, 0, 5353);
8936
8937 let mut state = ConfigState::default();
8938 state
8939 .dispatch(
8940 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8941 cluster_id: "tcp_cluster".to_string(),
8942 address: tcp_addr,
8943 ..Default::default()
8944 })
8945 .into(),
8946 )
8947 .expect("could not add tcp frontend");
8948 state
8949 .dispatch(
8950 &RequestType::AddUdpFrontend(RequestUdpFrontend {
8951 cluster_id: "udp_cluster".to_string(),
8952 address: udp_addr,
8953 ..Default::default()
8954 })
8955 .into(),
8956 )
8957 .expect("could not add udp frontend");
8958
8959 let all = state.list_frontends(FrontendFilters::default());
8961 assert_eq!(all.tcp_frontends.len(), 1, "tcp frontend must be listed");
8962 assert_eq!(
8963 all.udp_frontends.len(),
8964 1,
8965 "udp frontend must be listed under the default all-pass path"
8966 );
8967 assert_eq!(all.udp_frontends[0].cluster_id, "udp_cluster");
8968 assert_eq!(all.udp_frontends[0].address, udp_addr);
8969
8970 let tcp_only = state.list_frontends(FrontendFilters {
8972 tcp: true,
8973 ..Default::default()
8974 });
8975 assert_eq!(tcp_only.tcp_frontends.len(), 1);
8976 assert_eq!(
8977 tcp_only.udp_frontends.len(),
8978 1,
8979 "udp frontends ride the tcp filter"
8980 );
8981
8982 let http_only = state.list_frontends(FrontendFilters {
8984 http: true,
8985 ..Default::default()
8986 });
8987 assert!(http_only.tcp_frontends.is_empty());
8988 assert!(http_only.udp_frontends.is_empty());
8989
8990 let domain_filtered = state.list_frontends(FrontendFilters {
8992 domain: Some("example.com".to_string()),
8993 ..Default::default()
8994 });
8995 assert!(domain_filtered.tcp_frontends.is_empty());
8996 assert!(domain_filtered.udp_frontends.is_empty());
8997 }
8998
8999 #[test]
9003 fn list_frontends_filters_by_cluster_id() {
9004 let http_addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
9005 let https_addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9006 let mut state = ConfigState::default();
9007 for (cluster_id, hostname) in [
9008 (Some(String::from("wanted")), "wanted.example.com"),
9009 (Some(String::from("other")), "other.example.com"),
9010 (None, "denied.example.com"),
9012 ] {
9013 state
9014 .dispatch(
9015 &RequestType::AddHttpFrontend(RequestHttpFrontend {
9016 cluster_id: cluster_id.clone(),
9017 hostname: hostname.to_string(),
9018 path: PathRule::prefix(String::from("/")),
9019 address: http_addr,
9020 position: RulePosition::Tree.into(),
9021 ..Default::default()
9022 })
9023 .into(),
9024 )
9025 .expect("could not add http frontend");
9026 state
9027 .dispatch(
9028 &RequestType::AddHttpsFrontend(RequestHttpFrontend {
9029 cluster_id,
9030 hostname: hostname.to_string(),
9031 path: PathRule::prefix(String::from("/")),
9032 address: https_addr,
9033 position: RulePosition::Tree.into(),
9034 ..Default::default()
9035 })
9036 .into(),
9037 )
9038 .expect("could not add https frontend");
9039 }
9040 for (cluster_id, port) in [("wanted", 6379u16), ("other", 6380)] {
9042 state
9043 .dispatch(
9044 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9045 cluster_id: cluster_id.to_string(),
9046 address: SocketAddress::new_v4(0, 0, 0, 0, port),
9047 ..Default::default()
9048 })
9049 .into(),
9050 )
9051 .expect("could not add tcp frontend");
9052 state
9053 .dispatch(
9054 &RequestType::AddUdpFrontend(RequestUdpFrontend {
9055 cluster_id: cluster_id.to_string(),
9056 address: SocketAddress::new_v4(0, 0, 0, 0, port + 1000),
9057 ..Default::default()
9058 })
9059 .into(),
9060 )
9061 .expect("could not add udp frontend");
9062 }
9063
9064 let all = state.list_frontends(FrontendFilters::default());
9066 assert_eq!(all.http_frontends.len(), 3);
9067 assert_eq!(all.https_frontends.len(), 3);
9068 assert_eq!(all.tcp_frontends.len(), 2);
9069 assert_eq!(all.udp_frontends.len(), 2);
9070
9071 let wanted = state.list_frontends(FrontendFilters {
9074 cluster_id: Some("wanted".to_string()),
9075 ..Default::default()
9076 });
9077 assert_eq!(wanted.http_frontends.len(), 1);
9078 assert_eq!(wanted.https_frontends.len(), 1);
9079 assert_eq!(wanted.tcp_frontends.len(), 1);
9080 assert_eq!(wanted.udp_frontends.len(), 1);
9081 assert_eq!(
9082 wanted.http_frontends[0].cluster_id.as_deref(),
9083 Some("wanted")
9084 );
9085 assert_eq!(wanted.tcp_frontends[0].cluster_id, "wanted");
9086 assert_eq!(wanted.udp_frontends[0].cluster_id, "wanted");
9087 assert!(
9088 wanted
9089 .http_frontends
9090 .iter()
9091 .chain(wanted.https_frontends.iter())
9092 .all(|f| f.hostname == "wanted.example.com"),
9093 "no frontend of another cluster may leak through the filter"
9094 );
9095
9096 let unknown = state.list_frontends(FrontendFilters {
9098 cluster_id: Some("nope".to_string()),
9099 ..Default::default()
9100 });
9101 assert!(unknown.http_frontends.is_empty());
9102 assert!(unknown.https_frontends.is_empty());
9103 assert!(unknown.tcp_frontends.is_empty());
9104 assert!(unknown.udp_frontends.is_empty());
9105
9106 let conflicting = state.list_frontends(FrontendFilters {
9108 cluster_id: Some("wanted".to_string()),
9109 domain: Some("other.example.com".to_string()),
9110 ..Default::default()
9111 });
9112 assert!(conflicting.http_frontends.is_empty());
9113 assert!(conflicting.https_frontends.is_empty());
9114
9115 let https_only = state.list_frontends(FrontendFilters {
9117 https: true,
9118 cluster_id: Some("wanted".to_string()),
9119 ..Default::default()
9120 });
9121 assert!(https_only.http_frontends.is_empty());
9122 assert_eq!(https_only.https_frontends.len(), 1);
9123 assert!(https_only.tcp_frontends.is_empty());
9124 assert!(https_only.udp_frontends.is_empty());
9125 }
9126
9127 #[test]
9132 fn update_https_listener_happy_path_h2_knobs() {
9133 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9134 let mut state = ConfigState::new();
9135 state
9136 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
9137 .unwrap();
9138
9139 let patch = UpdateHttpsListenerConfig {
9140 address: addr,
9141 h2_max_rst_stream_per_window: Some(50),
9142 h2_max_ping_per_window: Some(20),
9143 ..Default::default()
9144 };
9145 state
9146 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9147 .expect("update must succeed");
9148
9149 let listener = state
9150 .https_listeners
9151 .get(&ListenerKey::from(addr))
9152 .expect("listener must be present");
9153 assert_eq!(listener.h2_max_rst_stream_per_window, Some(50));
9154 assert_eq!(listener.h2_max_ping_per_window, Some(20));
9155 assert_eq!(listener.front_timeout, 60);
9157 assert_eq!(listener.h2_max_settings_per_window, None);
9158 }
9159
9160 #[test]
9163 fn update_https_listener_not_found() {
9164 let mut state = ConfigState::new();
9165 let patch = UpdateHttpsListenerConfig {
9166 address: SocketAddress::new_v4(1, 2, 3, 4, 9999),
9167 h2_max_rst_stream_per_window: Some(50),
9168 ..Default::default()
9169 };
9170 let err = state
9171 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9172 .unwrap_err();
9173 assert!(
9174 matches!(
9175 err,
9176 StateError::NotFound {
9177 kind: ObjectKind::HttpsListener,
9178 ..
9179 }
9180 ),
9181 "expected NotFound, got: {err}"
9182 );
9183 }
9184
9185 #[test]
9188 fn update_https_listener_noop_patch() {
9189 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9190 let mut state = ConfigState::new();
9191 let original = make_https_listener(addr);
9192 state
9193 .dispatch(&RequestType::AddHttpsListener(original.clone()).into())
9194 .unwrap();
9195
9196 let patch = UpdateHttpsListenerConfig {
9197 address: addr,
9198 ..Default::default()
9199 };
9200 state
9201 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9202 .expect("no-op patch must succeed");
9203
9204 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
9205 assert_eq!(listener.front_timeout, original.front_timeout);
9206 assert_eq!(
9207 listener.h2_max_rst_stream_per_window,
9208 original.h2_max_rst_stream_per_window
9209 );
9210 }
9211
9212 #[test]
9214 fn update_https_listener_invalid_value_flood_knob_zero() {
9215 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9216 let mut state = ConfigState::new();
9217 state
9218 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
9219 .unwrap();
9220
9221 let patch = UpdateHttpsListenerConfig {
9222 address: addr,
9223 h2_max_rst_stream_per_window: Some(0),
9224 ..Default::default()
9225 };
9226 let err = state
9227 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9228 .unwrap_err();
9229 assert!(
9230 matches!(
9231 err,
9232 StateError::InvalidValue {
9233 field: "h2_max_rst_stream_per_window",
9234 ..
9235 }
9236 ),
9237 "expected InvalidValue for flood knob 0, got: {err}"
9238 );
9239 }
9240
9241 #[test]
9247 fn a_tcp_frontend_and_an_affinity_key_never_share_a_cluster() {
9248 use crate::proto::command::RequestTcpFrontend;
9249
9250 let keyed = |cluster_id: &str| -> Request {
9251 RequestType::AddCluster(Cluster {
9252 cluster_id: cluster_id.to_owned(),
9253 affinity_header: Some(String::from("X-Tenant")),
9254 ..Default::default()
9255 })
9256 .into()
9257 };
9258 let tcp_front = |cluster_id: &str, port: u16| -> Request {
9259 RequestType::AddTcpFrontend(RequestTcpFrontend {
9260 cluster_id: cluster_id.to_owned(),
9261 address: SocketAddress::new_v4(127, 0, 0, 1, port),
9262 ..Default::default()
9263 })
9264 .into()
9265 };
9266
9267 let mut state = ConfigState::new();
9269 state
9270 .dispatch(&keyed("keyed"))
9271 .expect("a keyed cluster is valid");
9272 let err = state
9273 .dispatch(&tcp_front("keyed", 4000))
9274 .expect_err("a TCP frontend may not join a keyed cluster");
9275 assert!(
9276 matches!(err, StateError::InvalidTcpFrontend { .. }),
9277 "unexpected error: {err:?}"
9278 );
9279 assert!(state.tcp_fronts.get("keyed").is_none_or(Vec::is_empty));
9280
9281 let mut state = ConfigState::new();
9283 state
9284 .dispatch(
9285 &RequestType::AddCluster(Cluster {
9286 cluster_id: String::from("tcp"),
9287 ..Default::default()
9288 })
9289 .into(),
9290 )
9291 .expect("a plain cluster is valid");
9292 state
9293 .dispatch(&tcp_front("tcp", 4001))
9294 .expect("a TCP frontend may join a plain cluster");
9295 let err = state
9296 .dispatch(&keyed("tcp"))
9297 .expect_err("a cluster serving TCP may not name a key");
9298 assert!(
9299 matches!(
9300 err,
9301 StateError::InvalidValue {
9302 field: "affinity_key",
9303 ..
9304 }
9305 ),
9306 "unexpected error: {err:?}"
9307 );
9308 assert_eq!(state.clusters["tcp"].affinity_header, None);
9309 }
9310
9311 #[test]
9315 fn add_cluster_with_out_of_range_max_connection_attempts_rejected() {
9316 for attempts in [0, 256] {
9317 let mut state = ConfigState::new();
9318 let err = state
9319 .dispatch(
9320 &RequestType::AddCluster(Cluster {
9321 cluster_id: String::from("budget"),
9322 max_connection_attempts: Some(attempts),
9323 ..Default::default()
9324 })
9325 .into(),
9326 )
9327 .expect_err("an out-of-range budget must be refused");
9328 assert!(
9329 matches!(
9330 err,
9331 StateError::InvalidValue {
9332 field: "max_connection_attempts",
9333 ..
9334 }
9335 ),
9336 "{attempts}: unexpected error {err:?}"
9337 );
9338 assert!(state.clusters.is_empty());
9339 }
9340 let mut state = ConfigState::new();
9341 state
9342 .dispatch(
9343 &RequestType::AddCluster(Cluster {
9344 cluster_id: String::from("budget"),
9345 max_connection_attempts: Some(7),
9346 ..Default::default()
9347 })
9348 .into(),
9349 )
9350 .expect("a budget in range is accepted");
9351 assert_eq!(state.clusters["budget"].max_connection_attempts, Some(7));
9352 }
9353
9354 #[test]
9355 fn add_cluster_with_invalid_shuffle_sharding_rejected() {
9356 for (percent, minimum, mode) in [
9357 (Some(0), None, None),
9358 (Some(101), None, None),
9359 (Some(50), Some(1), None),
9360 (None, Some(8), None),
9361 (None, None, Some(1)),
9362 (Some(50), None, Some(7)),
9363 ] {
9364 let mut state = ConfigState::new();
9365 let err = state
9366 .dispatch(
9367 &RequestType::AddCluster(Cluster {
9368 cluster_id: String::from("sharded"),
9369 shard_percent: percent,
9370 shard_min_backends: minimum,
9371 shard_mode: mode,
9372 ..Default::default()
9373 })
9374 .into(),
9375 )
9376 .expect_err("invalid shuffle sharding must be refused");
9377 assert!(
9378 matches!(
9379 err,
9380 StateError::InvalidValue {
9381 field: "shuffle_sharding",
9382 ..
9383 }
9384 ),
9385 "({percent:?}, {minimum:?}, {mode:?}): unexpected error {err:?}"
9386 );
9387 assert!(state.clusters.is_empty());
9388 }
9389 let mut state = ConfigState::new();
9390 state
9391 .dispatch(
9392 &RequestType::AddCluster(Cluster {
9393 cluster_id: String::from("sharded"),
9394 shard_percent: Some(25),
9395 shard_min_backends: Some(4),
9396 shard_mode: Some(1),
9397 ..Default::default()
9398 })
9399 .into(),
9400 )
9401 .expect("a valid sharded cluster is accepted");
9402 }
9403
9404 #[test]
9405 fn add_cluster_with_two_affinity_key_sources_rejected() {
9406 let mut state = ConfigState::new();
9407 let err = state
9408 .dispatch(
9409 &RequestType::AddCluster(Cluster {
9410 cluster_id: String::from("keyed"),
9411 affinity_header: Some(String::from("X-Tenant")),
9412 affinity_cookie: Some(String::from("tenant")),
9413 ..Default::default()
9414 })
9415 .into(),
9416 )
9417 .expect_err("a cluster keyed on both a header and a cookie must be refused");
9418 assert!(
9419 matches!(
9420 err,
9421 StateError::InvalidValue {
9422 field: "affinity_key",
9423 ..
9424 }
9425 ),
9426 "unexpected error: {err:?}"
9427 );
9428 assert!(
9429 state.clusters.is_empty(),
9430 "a refused cluster must not be stored"
9431 );
9432 }
9433
9434 #[test]
9435 fn add_cluster_invalid_health_check_uri_rejected() {
9436 use crate::proto::command::HealthCheckConfig;
9437
9438 let mut state = ConfigState::new();
9439 let err = state
9440 .dispatch(
9441 &RequestType::AddCluster(Cluster {
9442 cluster_id: String::from("evil_cluster"),
9443 health_check: Some(HealthCheckConfig {
9444 uri: String::from("/foo\r\nGET /admin"),
9445 interval: 5_000,
9446 timeout: 1_000,
9447 healthy_threshold: 2,
9448 unhealthy_threshold: 2,
9449 ..Default::default()
9450 }),
9451 ..Default::default()
9452 })
9453 .into(),
9454 )
9455 .unwrap_err();
9456
9457 assert!(
9458 matches!(
9459 err,
9460 StateError::InvalidValue {
9461 field: "health_check",
9462 ..
9463 }
9464 ),
9465 "expected InvalidValue for CRLF-bearing health-check URI, got: {err}"
9466 );
9467 assert!(
9468 !state.clusters.contains_key("evil_cluster"),
9469 "cluster must not be inserted when health_check fails validation",
9470 );
9471 }
9472
9473 #[test]
9477 fn set_health_check_tcp_mode_with_status_field_rejected() {
9478 use crate::proto::command::{HealthCheckConfig, HealthCheckMode, SetHealthCheck};
9479
9480 let mut state = ConfigState::new();
9481 state
9482 .dispatch(
9483 &RequestType::AddCluster(Cluster {
9484 cluster_id: String::from("app"),
9485 ..Default::default()
9486 })
9487 .into(),
9488 )
9489 .expect("the cluster is added");
9490 let tcp = HealthCheckConfig {
9491 uri: String::new(),
9492 interval: 10,
9493 timeout: 5,
9494 healthy_threshold: 3,
9495 unhealthy_threshold: 3,
9496 mode: HealthCheckMode::Tcp as i32,
9497 ..Default::default()
9498 };
9499 let set = |config: HealthCheckConfig| {
9500 RequestType::SetHealthCheck(SetHealthCheck {
9501 cluster_id: String::from("app"),
9502 config,
9503 })
9504 .into()
9505 };
9506
9507 let err = state
9508 .dispatch(&set(HealthCheckConfig {
9509 expected_status: 200,
9510 ..tcp.clone()
9511 }))
9512 .unwrap_err();
9513 assert!(
9514 matches!(
9515 err,
9516 StateError::InvalidValue {
9517 field: "health_check",
9518 ..
9519 }
9520 ),
9521 "expected InvalidValue for a TCP probe with expected_status, got: {err}"
9522 );
9523 assert_eq!(state.clusters["app"].health_check, None);
9524
9525 state
9526 .dispatch(&set(tcp.clone()))
9527 .expect("a bare TCP probe is accepted");
9528 assert_eq!(state.clusters["app"].health_check, Some(tcp));
9529 }
9530
9531 #[test]
9534 fn set_health_check_out_of_range_port_rejected() {
9535 use crate::proto::command::{HealthCheckConfig, HealthCheckMode, SetHealthCheck};
9536
9537 let mut state = ConfigState::new();
9538 state
9539 .dispatch(
9540 &RequestType::AddCluster(Cluster {
9541 cluster_id: String::from("app"),
9542 ..Default::default()
9543 })
9544 .into(),
9545 )
9546 .expect("the cluster is added");
9547 let probe = HealthCheckConfig {
9548 uri: String::new(),
9549 interval: 10,
9550 timeout: 5,
9551 healthy_threshold: 3,
9552 unhealthy_threshold: 3,
9553 mode: HealthCheckMode::Tcp as i32,
9554 port: Some(8081),
9555 ..Default::default()
9556 };
9557 let set = |config: HealthCheckConfig| {
9558 RequestType::SetHealthCheck(SetHealthCheck {
9559 cluster_id: String::from("app"),
9560 config,
9561 })
9562 .into()
9563 };
9564
9565 for port in [0, 65536] {
9566 let err = state
9567 .dispatch(&set(HealthCheckConfig {
9568 port: Some(port),
9569 ..probe.clone()
9570 }))
9571 .unwrap_err();
9572 assert!(
9573 matches!(
9574 err,
9575 StateError::InvalidValue {
9576 field: "health_check",
9577 ..
9578 }
9579 ),
9580 "expected InvalidValue for probe port {port}, got: {err}"
9581 );
9582 assert_eq!(state.clusters["app"].health_check, None);
9583 }
9584
9585 state
9586 .dispatch(&set(probe.clone()))
9587 .expect("a probe port within 1..=65535 is accepted");
9588 assert_eq!(state.clusters["app"].health_check, Some(probe));
9589 }
9590
9591 #[test]
9595 fn health_check_headers_validate_before_mutation_and_survive_replay() {
9596 use std::io::Read;
9597
9598 use crate::{
9599 parser::parse_several_requests,
9600 proto::command::{
9601 HealthCheckConfig, HealthCheckHeader, HealthCheckMode, SetHealthCheck,
9602 },
9603 };
9604
9605 let header = |name: &str, tag: &str| HealthCheckHeader {
9607 name: name.to_owned(),
9608 fingerprint: format!("sozu-hc-hmac-sha256:v1:{:032x}:{:064x}", 7, tag.len()),
9609 };
9610 let probe = |headers: Vec<HealthCheckHeader>| HealthCheckConfig {
9611 uri: "/health".to_owned(),
9612 interval: 10,
9613 timeout: 5,
9614 healthy_threshold: 3,
9615 unhealthy_threshold: 3,
9616 mode: HealthCheckMode::Http as i32,
9617 headers,
9618 ..Default::default()
9619 };
9620 let ordered = vec![
9621 header("Z-Last", "1"),
9622 header("Authorization", "Bearer"),
9623 header("A-First", ""),
9624 ];
9625 let set = |config: HealthCheckConfig| -> Request {
9626 RequestType::SetHealthCheck(SetHealthCheck {
9627 cluster_id: String::from("app"),
9628 config,
9629 })
9630 .into()
9631 };
9632 let invalid = probe(vec![header("Host", "1")]);
9634
9635 let mut state = ConfigState::new();
9637 let err = state
9638 .dispatch(
9639 &RequestType::AddCluster(Cluster {
9640 cluster_id: String::from("app"),
9641 health_check: Some(invalid.clone()),
9642 ..Default::default()
9643 })
9644 .into(),
9645 )
9646 .unwrap_err();
9647 assert!(
9648 matches!(
9649 err,
9650 StateError::InvalidValue {
9651 field: "health_check",
9652 ..
9653 }
9654 ),
9655 "got {err}"
9656 );
9657 assert!(state.clusters.is_empty(), "no cluster may be inserted");
9658
9659 state
9660 .dispatch(
9661 &RequestType::AddCluster(Cluster {
9662 cluster_id: String::from("app"),
9663 ..Default::default()
9664 })
9665 .into(),
9666 )
9667 .expect("the cluster is added");
9668 state
9669 .dispatch(&set(probe(ordered.clone())))
9670 .expect("a valid header list is accepted");
9671
9672 let before = state.clone();
9674 let err = state.dispatch(&set(invalid)).unwrap_err();
9675 assert!(
9676 matches!(
9677 err,
9678 StateError::InvalidValue {
9679 field: "health_check",
9680 ..
9681 }
9682 ),
9683 "got {err}"
9684 );
9685 let logical = |s: &ConfigState| {
9687 let mut c = s.clone();
9688 c.request_counts.clear();
9689 c
9690 };
9691 assert_eq!(
9692 logical(&state),
9693 logical(&before),
9694 "a refused order must not mutate the state"
9695 );
9696 assert_eq!(
9697 state.clusters["app"].health_check.as_ref().unwrap().headers,
9698 ordered
9699 );
9700
9701 let queried = state.cluster_state("app").expect("cluster exists");
9703 assert_eq!(
9704 queried.configuration.unwrap().health_check.unwrap().headers,
9705 ordered
9706 );
9707 assert_eq!(
9708 state.list_health_checks(Some("app")).map["app"].headers,
9709 ordered
9710 );
9711
9712 let adds = ConfigState::new().diff(&state);
9714 let carried = adds.iter().any(|request| {
9715 matches!(
9716 &request.request_type,
9717 Some(RequestType::AddCluster(cluster))
9718 if cluster.health_check.as_ref().is_some_and(|hc| hc.headers == ordered)
9719 )
9720 });
9721 assert!(carried, "the diff must carry the ordered headers: {adds:?}");
9722 let mut reordered = state.clone();
9723 reordered
9724 .dispatch(&set(probe(ordered.iter().rev().cloned().collect())))
9725 .expect("a reordered list is valid");
9726 assert!(
9727 !state.diff(&reordered).is_empty(),
9728 "a header reorder is a cluster change"
9729 );
9730 assert!(state.diff(&state.clone()).is_empty());
9731
9732 let directory = tempfile::tempdir().expect("a temporary directory");
9734 let path = directory.path().join("headers.state");
9735 let mut file = File::create(&path).expect("creatable");
9736 let written = state.write_requests_to_file(&mut file).expect("saves");
9737 drop(file);
9738 let mut saved = Vec::new();
9739 File::open(&path)
9740 .expect("readable")
9741 .read_to_end(&mut saved)
9742 .expect("readable");
9743 let (remainder, requests) =
9744 parse_several_requests::<WorkerRequest>(&saved).expect("the file parses");
9745 assert!(remainder.is_empty());
9746 assert_eq!(requests.len(), written);
9747 let mut loaded = ConfigState::new();
9748 for request in &requests {
9749 loaded.dispatch(&request.content).expect("replays");
9750 }
9751
9752 let mut replayed = ConfigState::new();
9754 for request in state.generate_requests() {
9755 replayed.dispatch(&request).expect("replays");
9756 }
9757
9758 for (name, other) in [("loaded", &loaded), ("replayed", &replayed)] {
9759 assert_eq!(
9760 other.clusters["app"].health_check.as_ref().unwrap().headers,
9761 ordered,
9762 "{name} state must keep the exact ordered headers"
9763 );
9764 }
9765 }
9766
9767 #[test]
9772 fn protected_health_check_order_stores_metadata_only_and_refuses_before_mutation() {
9773 use crate::proto::command::{
9774 HealthCheckConfig, HealthCheckHeader, HealthCheckHeaderValue, HealthCheckMode,
9775 SetProtectedHealthCheck,
9776 };
9777
9778 const SECRET: &str = "SYNTHETIC-STATE-SECRET-77c1";
9779 let fingerprint = format!("sozu-hc-hmac-sha256:v1:{:032x}:{:064x}", 1, 2);
9780 let order = |fingerprint: &str, carrier_name: &str| {
9781 RequestType::SetProtectedHealthCheck(SetProtectedHealthCheck {
9782 cluster_id: String::from("app"),
9783 config: HealthCheckConfig {
9784 uri: "/health".to_owned(),
9785 interval: 10,
9786 timeout: 5,
9787 healthy_threshold: 3,
9788 unhealthy_threshold: 3,
9789 mode: HealthCheckMode::Http as i32,
9790 headers: vec![HealthCheckHeader {
9791 name: "Authorization".to_owned(),
9792 fingerprint: fingerprint.to_owned(),
9793 }],
9794 ..Default::default()
9795 },
9796 values: vec![HealthCheckHeaderValue {
9797 name: carrier_name.to_owned(),
9798 value: SECRET.as_bytes().to_vec(),
9799 }],
9800 })
9801 .into()
9802 };
9803 let mut state = ConfigState::new();
9804 state
9805 .dispatch(
9806 &RequestType::AddCluster(Cluster {
9807 cluster_id: String::from("app"),
9808 ..Default::default()
9809 })
9810 .into(),
9811 )
9812 .expect("the cluster is added");
9813 let logical = |s: &ConfigState| {
9814 let mut c = s.clone();
9815 c.request_counts.clear();
9816 c
9817 };
9818
9819 let before = logical(&state);
9820 for (what, refused) in [
9821 ("no fingerprint", order("", "Authorization")),
9822 ("another carrier", order(&fingerprint, "X-Other")),
9823 ] {
9824 assert!(state.dispatch(&refused).is_err(), "{what}");
9825 assert_eq!(logical(&state), before, "{what}: no mutation");
9826 }
9827
9828 state
9829 .dispatch(&order(&fingerprint, "Authorization"))
9830 .expect("a fingerprinted order with its carrier is accepted");
9831 let stored = state.clusters["app"].health_check.as_ref().expect("set");
9832 assert_eq!(stored.headers[0].fingerprint, fingerprint);
9833 let rendered = format!("{state:?}{:?}", state.generate_requests());
9834 assert!(!rendered.contains(SECRET), "the state holds no value");
9835 let json = serde_json::to_string(&state.generate_requests()).expect("serializes");
9836 assert!(!json.contains(SECRET), "a replay holds no value");
9837 }
9838
9839 #[test]
9843 fn a_health_check_port_and_a_unix_backend_never_share_a_cluster() {
9844 use crate::proto::command::{HealthCheckConfig, HealthCheckMode, SetHealthCheck};
9845
9846 let probe = |port| HealthCheckConfig {
9847 interval: 10,
9848 timeout: 5,
9849 healthy_threshold: 3,
9850 unhealthy_threshold: 3,
9851 mode: HealthCheckMode::Tcp as i32,
9852 port,
9853 ..Default::default()
9854 };
9855 let cluster = |health_check| -> Request {
9856 RequestType::AddCluster(Cluster {
9857 cluster_id: String::from("app"),
9858 health_check,
9859 ..Default::default()
9860 })
9861 .into()
9862 };
9863 let backend = |unix_path: Option<&str>| AddBackend {
9864 cluster_id: String::from("app"),
9865 backend_id: String::from("app-0"),
9866 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
9867 unix_path: unix_path.map(ToOwned::to_owned),
9868 ..Default::default()
9869 };
9870 let add_unix: Request = RequestType::AddUnixBackend(backend(Some("/run/app.sock"))).into();
9871 let set = |port| -> Request {
9872 RequestType::SetHealthCheck(SetHealthCheck {
9873 cluster_id: String::from("app"),
9874 config: probe(port),
9875 })
9876 .into()
9877 };
9878 let refused = |state: &mut ConfigState, request: &Request| {
9879 let before = (state.clusters.clone(), state.backends.clone());
9880 let error = state.dispatch(request).unwrap_err();
9881 assert!(
9882 matches!(
9883 error,
9884 StateError::InvalidValue {
9885 field: "health_check.port",
9886 reason: crate::config::HEALTH_CHECK_PORT_UNIX_REFUSED,
9887 }
9888 ),
9889 "expected the probe port / unix_path refusal, got: {error}"
9890 );
9891 assert_eq!(before, (state.clusters.clone(), state.backends.clone()));
9892 };
9893
9894 let mut state = ConfigState::new();
9896 state.dispatch(&cluster(Some(probe(Some(8081))))).unwrap();
9897 refused(&mut state, &add_unix);
9898 assert!(state.backends.get("app").is_none_or(Vec::is_empty));
9899 state
9900 .dispatch(&RequestType::AddBackend(backend(None)).into())
9901 .expect("an IP backend takes the probe port");
9902
9903 let mut state = ConfigState::new();
9906 state.dispatch(&cluster(Some(probe(None)))).unwrap();
9907 state.dispatch(&add_unix).unwrap();
9908 refused(&mut state, &set(Some(8081)));
9909 refused(&mut state, &cluster(Some(probe(Some(8081)))));
9910 assert_eq!(state.clusters["app"].health_check, Some(probe(None)));
9911 state
9912 .dispatch(&set(None))
9913 .expect("a unix backend is probed through its socket");
9914 state
9915 .dispatch(&cluster(None))
9916 .expect("an upsert without a health check keeps the unix backend");
9917
9918 let mut state = ConfigState::new();
9920 assert!(matches!(
9921 state.dispatch(&set(Some(8081))).unwrap_err(),
9922 StateError::NotFound { .. }
9923 ));
9924 assert!(matches!(
9925 state.dispatch(&add_unix).unwrap_err(),
9926 StateError::ClusterNotFound { .. }
9927 ));
9928 }
9929
9930 #[test]
9932 fn update_https_listener_alpn_unknown_value_rejected() {
9933 use crate::proto::command::AlpnProtocols;
9934
9935 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9936 let mut state = ConfigState::new();
9937 state
9938 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
9939 .unwrap();
9940
9941 let patch = UpdateHttpsListenerConfig {
9942 address: addr,
9943 alpn_protocols: Some(AlpnProtocols {
9944 values: vec!["h3".to_owned()],
9945 }),
9946 ..Default::default()
9947 };
9948 let err = state
9949 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9950 .unwrap_err();
9951 assert!(
9952 matches!(
9953 err,
9954 StateError::InvalidValue {
9955 field: "alpn_protocols",
9956 ..
9957 }
9958 ),
9959 "expected InvalidValue for unknown ALPN, got: {err}"
9960 );
9961 }
9962
9963 #[test]
9965 fn update_https_listener_alpn_empty_reset_accepted() {
9966 use crate::proto::command::AlpnProtocols;
9967
9968 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9969 let mut state = ConfigState::new();
9970 let mut listener = make_https_listener(addr);
9971 listener.alpn_protocols = vec!["h2".to_owned()];
9972 state
9973 .dispatch(&RequestType::AddHttpsListener(listener).into())
9974 .unwrap();
9975
9976 let patch = UpdateHttpsListenerConfig {
9977 address: addr,
9978 alpn_protocols: Some(AlpnProtocols { values: vec![] }),
9979 ..Default::default()
9980 };
9981 state
9982 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9983 .expect("empty ALPN reset must succeed");
9984
9985 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
9986 assert!(
9987 listener.alpn_protocols.is_empty(),
9988 "ALPN must have been reset to empty"
9989 );
9990 }
9991
9992 #[test]
9994 fn update_https_listener_alpn_valid_values_accepted() {
9995 use crate::proto::command::AlpnProtocols;
9996
9997 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9998 let mut state = ConfigState::new();
9999 state
10000 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10001 .unwrap();
10002
10003 let patch = UpdateHttpsListenerConfig {
10004 address: addr,
10005 alpn_protocols: Some(AlpnProtocols {
10006 values: vec!["h2".to_owned(), "http/1.1".to_owned()],
10007 }),
10008 ..Default::default()
10009 };
10010 state
10011 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10012 .expect("valid ALPN must be accepted");
10013
10014 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10015 assert_eq!(listener.alpn_protocols, vec!["h2", "http/1.1"]);
10016 }
10017
10018 #[test]
10021 fn update_https_listener_alpn_absent_preserves_existing() {
10022 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10023 let mut state = ConfigState::new();
10024 let mut listener = make_https_listener(addr);
10025 listener.alpn_protocols = vec!["h2".to_owned()];
10026 state
10027 .dispatch(&RequestType::AddHttpsListener(listener).into())
10028 .unwrap();
10029
10030 let patch = UpdateHttpsListenerConfig {
10032 address: addr,
10033 front_timeout: Some(10),
10034 ..Default::default()
10035 };
10036 state
10037 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10038 .unwrap();
10039
10040 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10041 assert_eq!(
10042 listener.alpn_protocols,
10043 vec!["h2"],
10044 "ALPN must be preserved when not patched"
10045 );
10046 }
10047
10048 #[test]
10050 fn update_https_listener_sozu_id_header_empty_rejected() {
10051 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10052 let mut state = ConfigState::new();
10053 state
10054 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10055 .unwrap();
10056
10057 let patch = UpdateHttpsListenerConfig {
10058 address: addr,
10059 sozu_id_header: Some(String::new()),
10060 ..Default::default()
10061 };
10062 let err = state
10063 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10064 .unwrap_err();
10065 assert!(
10066 matches!(
10067 err,
10068 StateError::InvalidValue {
10069 field: "sozu_id_header",
10070 ..
10071 }
10072 ),
10073 "expected InvalidValue for empty header name, got: {err}"
10074 );
10075 }
10076
10077 #[test]
10079 fn update_https_listener_sozu_id_header_colon_rejected() {
10080 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10081 let mut state = ConfigState::new();
10082 state
10083 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10084 .unwrap();
10085
10086 let patch = UpdateHttpsListenerConfig {
10087 address: addr,
10088 sozu_id_header: Some("bad: value".to_owned()),
10089 ..Default::default()
10090 };
10091 let err = state
10092 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10093 .unwrap_err();
10094 assert!(
10095 matches!(
10096 err,
10097 StateError::InvalidValue {
10098 field: "sozu_id_header",
10099 ..
10100 }
10101 ),
10102 "expected InvalidValue for header name with colon, got: {err}"
10103 );
10104 }
10105
10106 #[test]
10108 fn update_https_listener_sozu_id_header_valid_accepted() {
10109 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10110 let mut state = ConfigState::new();
10111 state
10112 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10113 .unwrap();
10114
10115 let patch = UpdateHttpsListenerConfig {
10116 address: addr,
10117 sozu_id_header: Some("X-Edge-Id".to_owned()),
10118 ..Default::default()
10119 };
10120 state
10121 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10122 .expect("valid header name must be accepted");
10123
10124 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10125 assert_eq!(listener.sozu_id_header.as_deref(), Some("X-Edge-Id"));
10126 }
10127
10128 #[test]
10134 fn add_listener_rejects_an_unknown_forwarded_headers_value() {
10135 let http = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10136 let https = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10137 let mut state = ConfigState::new();
10138
10139 let http_error = state
10140 .dispatch(
10141 &RequestType::AddHttpListener(HttpListenerConfig {
10142 forwarded_headers: Some(42),
10143 ..make_http_listener(http)
10144 })
10145 .into(),
10146 )
10147 .unwrap_err();
10148 let https_error = state
10149 .dispatch(
10150 &RequestType::AddHttpsListener(HttpsListenerConfig {
10151 forwarded_headers: Some(-1),
10152 ..make_https_listener(https)
10153 })
10154 .into(),
10155 )
10156 .unwrap_err();
10157 for error in [http_error, https_error] {
10158 assert!(
10159 matches!(
10160 error,
10161 StateError::InvalidValue {
10162 field: "forwarded_headers",
10163 ..
10164 }
10165 ),
10166 "expected InvalidValue for an unknown mode, got: {error}"
10167 );
10168 }
10169 assert!(state.http_listeners.is_empty() && state.https_listeners.is_empty());
10170
10171 state
10172 .dispatch(
10173 &RequestType::AddHttpListener(HttpListenerConfig {
10174 forwarded_headers: Some(ForwardedHeaders::Rfc7239 as i32),
10175 ..make_http_listener(http)
10176 })
10177 .into(),
10178 )
10179 .expect("a known mode must be accepted on an HTTP listener");
10180 state
10181 .dispatch(
10182 &RequestType::AddHttpsListener(HttpsListenerConfig {
10183 forwarded_headers: Some(ForwardedHeaders::None as i32),
10184 ..make_https_listener(https)
10185 })
10186 .into(),
10187 )
10188 .expect("a known mode must be accepted on an HTTPS listener");
10189 }
10190
10191 #[test]
10196 fn update_listener_forwarded_headers_applies_and_rejects_unknown_values() {
10197 let http = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10198 let https = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10199 let mut state = ConfigState::new();
10200 state
10201 .dispatch(&RequestType::AddHttpListener(make_http_listener(http)).into())
10202 .unwrap();
10203 state
10204 .dispatch(&RequestType::AddHttpsListener(make_https_listener(https)).into())
10205 .unwrap();
10206
10207 state
10208 .dispatch(
10209 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
10210 address: http,
10211 forwarded_headers: Some(ForwardedHeaders::Rfc7239 as i32),
10212 ..Default::default()
10213 })
10214 .into(),
10215 )
10216 .expect("a known mode must be accepted on an HTTP listener");
10217 state
10218 .dispatch(
10219 &RequestType::UpdateHttpsListener(UpdateHttpsListenerConfig {
10220 address: https,
10221 forwarded_headers: Some(ForwardedHeaders::None as i32),
10222 ..Default::default()
10223 })
10224 .into(),
10225 )
10226 .expect("a known mode must be accepted on an HTTPS listener");
10227 assert_eq!(
10228 state.http_listeners[&http.into()].forwarded_headers(),
10229 ForwardedHeaders::Rfc7239
10230 );
10231 assert_eq!(
10232 state.https_listeners[&https.into()].forwarded_headers(),
10233 ForwardedHeaders::None
10234 );
10235
10236 let http_error = state
10237 .dispatch(
10238 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
10239 address: http,
10240 forwarded_headers: Some(42),
10241 ..Default::default()
10242 })
10243 .into(),
10244 )
10245 .unwrap_err();
10246 let https_error = state
10247 .dispatch(
10248 &RequestType::UpdateHttpsListener(UpdateHttpsListenerConfig {
10249 address: https,
10250 forwarded_headers: Some(-1),
10251 ..Default::default()
10252 })
10253 .into(),
10254 )
10255 .unwrap_err();
10256 for error in [http_error, https_error] {
10257 assert!(
10258 matches!(
10259 error,
10260 StateError::InvalidValue {
10261 field: "forwarded_headers",
10262 ..
10263 }
10264 ),
10265 "expected InvalidValue for an unknown mode, got: {error}"
10266 );
10267 }
10268 assert_eq!(
10270 state.http_listeners[&http.into()].forwarded_headers(),
10271 ForwardedHeaders::Rfc7239
10272 );
10273 assert_eq!(
10274 state.https_listeners[&https.into()].forwarded_headers(),
10275 ForwardedHeaders::None
10276 );
10277 }
10278
10279 #[test]
10281 fn update_https_listener_graceful_shutdown_zero_allowed() {
10282 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10283 let mut state = ConfigState::new();
10284 state
10285 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10286 .unwrap();
10287
10288 let patch = UpdateHttpsListenerConfig {
10289 address: addr,
10290 h2_graceful_shutdown_deadline_seconds: Some(0),
10291 ..Default::default()
10292 };
10293 state
10294 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10295 .expect("graceful_shutdown_deadline=0 must be allowed");
10296
10297 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10298 assert_eq!(listener.h2_graceful_shutdown_deadline_seconds, Some(0));
10299 }
10300
10301 #[test]
10305 fn update_http_listener_happy_path() {
10306 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10307 let mut state = ConfigState::new();
10308 state
10309 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10310 .unwrap();
10311
10312 let patch = UpdateHttpListenerConfig {
10313 address: addr,
10314 front_timeout: Some(15),
10315 h2_max_rst_stream_per_window: Some(25),
10316 ..Default::default()
10317 };
10318 state
10319 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10320 .expect("HTTP update must succeed");
10321
10322 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10323 assert_eq!(listener.front_timeout, 15);
10324 assert_eq!(listener.h2_max_rst_stream_per_window, Some(25));
10325 assert_eq!(listener.back_timeout, 30);
10327 }
10328
10329 #[test]
10336 fn update_http_listener_records_the_answers_map() {
10337 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10338 let mut state = ConfigState::new();
10339 let mut listener = make_http_listener(addr);
10340 listener.answers = [
10341 ("404".to_owned(), "stored 404".to_owned()),
10342 ("503".to_owned(), "stored 503".to_owned()),
10343 ]
10344 .into();
10345 state
10346 .dispatch(&RequestType::AddHttpListener(listener).into())
10347 .unwrap();
10348
10349 let patch = UpdateHttpListenerConfig {
10350 address: addr,
10351 answers: [
10352 ("404".to_owned(), String::new()),
10353 ("503".to_owned(), "patched 503".to_owned()),
10354 ("429".to_owned(), "patched 429".to_owned()),
10355 ]
10356 .into(),
10357 ..Default::default()
10358 };
10359 state
10360 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10361 .expect("HTTP update must succeed");
10362
10363 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10364 assert_eq!(
10365 listener.answers.get("404").map(String::as_str),
10366 Some("stored 404"),
10367 "an empty body preserves the stored template"
10368 );
10369 assert_eq!(
10370 listener.answers.get("503").map(String::as_str),
10371 Some("patched 503")
10372 );
10373 assert_eq!(
10374 listener.answers.get("429").map(String::as_str),
10375 Some("patched 429")
10376 );
10377 assert_eq!(listener.answers.len(), 3);
10378 }
10379
10380 #[test]
10386 fn update_https_listener_records_the_answers_map_and_hsts() {
10387 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10388 let mut state = ConfigState::new();
10389 let mut listener = make_https_listener(addr);
10390 listener.answers = [("404".to_owned(), "stored 404".to_owned())].into();
10391 listener.hsts = Some(HstsConfig {
10392 enabled: Some(true),
10393 max_age: Some(300),
10394 include_subdomains: Some(true),
10395 ..Default::default()
10396 });
10397 state
10398 .dispatch(&RequestType::AddHttpsListener(listener).into())
10399 .unwrap();
10400
10401 let disabled = HstsConfig {
10402 enabled: Some(false),
10403 ..Default::default()
10404 };
10405 let patch = UpdateHttpsListenerConfig {
10406 address: addr,
10407 answers: [
10408 ("404".to_owned(), String::new()),
10409 ("503".to_owned(), "patched 503".to_owned()),
10410 ]
10411 .into(),
10412 hsts: Some(disabled),
10413 ..Default::default()
10414 };
10415 state
10416 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10417 .expect("HTTPS update must succeed");
10418
10419 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10420 assert_eq!(
10421 listener.answers.get("404").map(String::as_str),
10422 Some("stored 404"),
10423 "an empty body preserves the stored template"
10424 );
10425 assert_eq!(
10426 listener.answers.get("503").map(String::as_str),
10427 Some("patched 503")
10428 );
10429 assert_eq!(listener.answers.len(), 2);
10430 assert_eq!(
10431 listener.hsts,
10432 Some(disabled),
10433 "the patched hsts block replaces the stored one whole"
10434 );
10435
10436 let patch = UpdateHttpsListenerConfig {
10437 address: addr,
10438 front_timeout: Some(15),
10439 ..Default::default()
10440 };
10441 state
10442 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10443 .expect("HTTPS update must succeed");
10444 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10445 assert_eq!(
10446 listener.hsts,
10447 Some(disabled),
10448 "a patch without hsts keeps the recorded policy"
10449 );
10450 }
10451
10452 #[test]
10454 fn update_http_listener_flood_knob_zero_rejected() {
10455 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10456 let mut state = ConfigState::new();
10457 state
10458 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10459 .unwrap();
10460
10461 let patch = UpdateHttpListenerConfig {
10462 address: addr,
10463 h2_max_window_update_stream0_per_window: Some(0),
10464 ..Default::default()
10465 };
10466 let err = state
10467 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10468 .unwrap_err();
10469 assert!(
10470 matches!(
10471 err,
10472 StateError::InvalidValue {
10473 field: "h2_max_window_update_stream0_per_window",
10474 ..
10475 }
10476 ),
10477 "expected InvalidValue, got: {err}"
10478 );
10479 }
10480
10481 #[test]
10487 fn update_http_listener_x_real_ip_patch_applied() {
10488 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10489 let mut state = ConfigState::new();
10490 state
10491 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10492 .unwrap();
10493
10494 let patch = UpdateHttpListenerConfig {
10495 address: addr,
10496 elide_x_real_ip: Some(true),
10497 send_x_real_ip: Some(true),
10498 ..Default::default()
10499 };
10500 state
10501 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10502 .expect("HTTP update must succeed");
10503
10504 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10505 assert_eq!(listener.elide_x_real_ip, Some(true));
10506 assert_eq!(listener.send_x_real_ip, Some(true));
10507 assert_eq!(listener.front_timeout, 60);
10509 }
10510
10511 #[test]
10514 fn update_http_listener_x_real_ip_absent_preserves_existing() {
10515 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10516 let mut state = ConfigState::new();
10517 let listener = HttpListenerConfig {
10518 elide_x_real_ip: Some(true),
10519 send_x_real_ip: Some(false),
10520 ..make_http_listener(addr)
10521 };
10522 state
10523 .dispatch(&RequestType::AddHttpListener(listener).into())
10524 .unwrap();
10525
10526 let patch = UpdateHttpListenerConfig {
10527 address: addr,
10528 send_x_real_ip: Some(true),
10529 ..Default::default()
10530 };
10531 state
10532 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10533 .expect("HTTP update must succeed");
10534
10535 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10536 assert_eq!(listener.elide_x_real_ip, Some(true));
10537 assert_eq!(listener.send_x_real_ip, Some(true));
10538 }
10539
10540 #[test]
10543 fn update_https_listener_x_real_ip_patch_applied() {
10544 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10545 let mut state = ConfigState::new();
10546 state
10547 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10548 .unwrap();
10549
10550 let patch = UpdateHttpsListenerConfig {
10551 address: addr,
10552 elide_x_real_ip: Some(true),
10553 send_x_real_ip: Some(true),
10554 ..Default::default()
10555 };
10556 state
10557 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10558 .expect("HTTPS update must succeed");
10559
10560 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10561 assert_eq!(listener.elide_x_real_ip, Some(true));
10562 assert_eq!(listener.send_x_real_ip, Some(true));
10563 assert_eq!(listener.front_timeout, 60);
10565 }
10566
10567 #[test]
10570 fn update_https_listener_x_real_ip_absent_preserves_existing() {
10571 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10572 let mut state = ConfigState::new();
10573 let listener = HttpsListenerConfig {
10574 elide_x_real_ip: Some(false),
10575 send_x_real_ip: Some(true),
10576 ..make_https_listener(addr)
10577 };
10578 state
10579 .dispatch(&RequestType::AddHttpsListener(listener).into())
10580 .unwrap();
10581
10582 let patch = UpdateHttpsListenerConfig {
10583 address: addr,
10584 elide_x_real_ip: Some(true),
10585 ..Default::default()
10586 };
10587 state
10588 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10589 .expect("HTTPS update must succeed");
10590
10591 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10592 assert_eq!(listener.elide_x_real_ip, Some(true));
10593 assert_eq!(listener.send_x_real_ip, Some(true));
10594 }
10595
10596 #[test]
10599 fn update_listener_x_real_ip_survives_generate_requests_replay() {
10600 let http_addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10601 let https_addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10602 let mut state = ConfigState::new();
10603 state
10604 .dispatch(&RequestType::AddHttpListener(make_http_listener(http_addr)).into())
10605 .unwrap();
10606 state
10607 .dispatch(&RequestType::AddHttpsListener(make_https_listener(https_addr)).into())
10608 .unwrap();
10609 state
10610 .dispatch(
10611 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
10612 address: http_addr,
10613 elide_x_real_ip: Some(true),
10614 send_x_real_ip: Some(true),
10615 ..Default::default()
10616 })
10617 .into(),
10618 )
10619 .unwrap();
10620 state
10621 .dispatch(
10622 &RequestType::UpdateHttpsListener(UpdateHttpsListenerConfig {
10623 address: https_addr,
10624 elide_x_real_ip: Some(true),
10625 send_x_real_ip: Some(true),
10626 ..Default::default()
10627 })
10628 .into(),
10629 )
10630 .unwrap();
10631
10632 let mut replayed = ConfigState::new();
10633 for request in state.generate_requests() {
10634 replayed
10635 .dispatch(&request)
10636 .expect("could not replay generated request");
10637 }
10638
10639 let http = replayed
10640 .http_listeners
10641 .get(&ListenerKey::from(http_addr))
10642 .expect("replayed HTTP listener");
10643 assert_eq!(http.elide_x_real_ip, Some(true));
10644 assert_eq!(http.send_x_real_ip, Some(true));
10645 let https = replayed
10646 .https_listeners
10647 .get(&ListenerKey::from(https_addr))
10648 .expect("replayed HTTPS listener");
10649 assert_eq!(https.elide_x_real_ip, Some(true));
10650 assert_eq!(https.send_x_real_ip, Some(true));
10651 }
10652
10653 #[test]
10657 fn update_tcp_listener_happy_path() {
10658 let addr = SocketAddress::new_v4(0, 0, 0, 0, 9000);
10659 let mut state = ConfigState::new();
10660 state
10661 .dispatch(&RequestType::AddTcpListener(make_tcp_listener(addr)).into())
10662 .unwrap();
10663
10664 let patch = UpdateTcpListenerConfig {
10665 address: addr,
10666 front_timeout: Some(5),
10667 ..Default::default()
10668 };
10669 state
10670 .dispatch(&RequestType::UpdateTcpListener(patch).into())
10671 .expect("TCP update must succeed");
10672
10673 let listener = state.tcp_listeners.get(&ListenerKey::from(addr)).unwrap();
10674 assert_eq!(listener.front_timeout, 5);
10675 assert_eq!(listener.back_timeout, 30); }
10677
10678 #[test]
10680 fn update_tcp_listener_not_found() {
10681 let mut state = ConfigState::new();
10682 let patch = UpdateTcpListenerConfig {
10683 address: SocketAddress::new_v4(9, 9, 9, 9, 9999),
10684 front_timeout: Some(5),
10685 ..Default::default()
10686 };
10687 let err = state
10688 .dispatch(&RequestType::UpdateTcpListener(patch).into())
10689 .unwrap_err();
10690 assert!(
10691 matches!(
10692 err,
10693 StateError::NotFound {
10694 kind: ObjectKind::TcpListener,
10695 ..
10696 }
10697 ),
10698 "expected NotFound, got: {err}"
10699 );
10700 }
10701
10702 fn without_request_counts(state: &ConfigState) -> ConfigState {
10708 ConfigState {
10709 request_counts: BTreeMap::new(),
10710 ..state.clone()
10711 }
10712 }
10713
10714 #[test]
10725 fn update_http_listener_rejected_patch_leaves_the_state_unchanged() {
10726 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10727 let mut state = ConfigState::new();
10728 state
10729 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10730 .unwrap();
10731
10732 let valid_prefix = UpdateHttpListenerConfig {
10733 address: addr,
10734 front_timeout: Some(99),
10735 sticky_name: Some("PATCHED".to_owned()),
10736 elide_x_real_ip: Some(true),
10737 http_answers: Some(CustomHttpAnswers {
10738 answer_404: Some("HTTP/1.1 404 Not Found\r\n\r\n".to_owned()),
10739 ..Default::default()
10740 }),
10741 h2_max_rst_stream_per_window: Some(7),
10742 ..Default::default()
10743 };
10744 let rejected = [
10745 (
10746 "sozu_id_header",
10747 UpdateHttpListenerConfig {
10748 sozu_id_header: Some("bad header".to_owned()),
10749 ..valid_prefix.clone()
10750 },
10751 ),
10752 (
10753 "forwarded_headers",
10754 UpdateHttpListenerConfig {
10755 forwarded_headers: Some(i32::MAX),
10756 ..valid_prefix.clone()
10757 },
10758 ),
10759 ];
10760 for (field, patch) in rejected {
10761 let before = without_request_counts(&state);
10762 let err = state
10763 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10764 .unwrap_err();
10765 assert!(
10766 matches!(err, StateError::InvalidValue { field: f, .. } if f == field),
10767 "expected InvalidValue on {field}, got: {err}"
10768 );
10769 assert!(
10770 without_request_counts(&state) == before,
10771 "a patch rejected on {field} must leave the main state unchanged"
10772 );
10773 }
10774
10775 state
10778 .dispatch(&RequestType::UpdateHttpListener(valid_prefix).into())
10779 .expect("the valid prefix alone must apply");
10780 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10781 assert_eq!(listener.front_timeout, 99);
10782 assert_eq!(listener.sticky_name, "PATCHED");
10783 }
10784
10785 #[test]
10795 fn update_https_listener_rejected_patch_leaves_the_state_unchanged() {
10796 use crate::proto::command::AlpnProtocols;
10797
10798 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10799 let mut state = ConfigState::new();
10800 state
10801 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10802 .unwrap();
10803
10804 let valid_prefix = UpdateHttpsListenerConfig {
10805 address: addr,
10806 front_timeout: Some(99),
10807 sticky_name: Some("PATCHED".to_owned()),
10808 send_x_real_ip: Some(true),
10809 http_answers: Some(CustomHttpAnswers {
10810 answer_404: Some("HTTP/1.1 404 Not Found\r\n\r\n".to_owned()),
10811 ..Default::default()
10812 }),
10813 strict_sni_binding: Some(false),
10814 h2_max_rst_stream_per_window: Some(7),
10815 ..Default::default()
10816 };
10817 let rejected = [
10818 (
10819 "alpn_protocols",
10820 UpdateHttpsListenerConfig {
10821 alpn_protocols: Some(AlpnProtocols {
10822 values: vec!["h3".to_owned()],
10823 }),
10824 ..valid_prefix.clone()
10825 },
10826 ),
10827 (
10828 "sozu_id_header",
10829 UpdateHttpsListenerConfig {
10830 sozu_id_header: Some(String::new()),
10831 ..valid_prefix.clone()
10832 },
10833 ),
10834 (
10835 "forwarded_headers",
10836 UpdateHttpsListenerConfig {
10837 forwarded_headers: Some(-1),
10838 ..valid_prefix.clone()
10839 },
10840 ),
10841 (
10842 "hsts",
10843 UpdateHttpsListenerConfig {
10844 hsts: Some(HstsConfig {
10845 enabled: None,
10846 max_age: Some(60),
10847 ..Default::default()
10848 }),
10849 ..valid_prefix.clone()
10850 },
10851 ),
10852 ];
10853 for (field, patch) in rejected {
10854 let before = without_request_counts(&state);
10855 let err = state
10856 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10857 .unwrap_err();
10858 assert!(
10859 matches!(err, StateError::InvalidValue { field: f, .. } if f == field),
10860 "expected InvalidValue on {field}, got: {err}"
10861 );
10862 assert!(
10863 without_request_counts(&state) == before,
10864 "a patch rejected on {field} must leave the main state unchanged"
10865 );
10866 }
10867
10868 state
10869 .dispatch(&RequestType::UpdateHttpsListener(valid_prefix).into())
10870 .expect("the valid prefix alone must apply");
10871 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10872 assert_eq!(listener.front_timeout, 99);
10873 assert_eq!(listener.strict_sni_binding, Some(false));
10874 }
10875
10876 #[test]
10880 fn update_tcp_listener_rejected_patch_leaves_the_state_unchanged() {
10881 let addr = SocketAddress::new_v4(0, 0, 0, 0, 9000);
10882 let mut state = ConfigState::new();
10883 state
10884 .dispatch(&RequestType::AddTcpListener(make_tcp_listener(addr)).into())
10885 .unwrap();
10886
10887 let before = without_request_counts(&state);
10888 let err = state
10889 .dispatch(
10890 &RequestType::UpdateTcpListener(UpdateTcpListenerConfig {
10891 address: SocketAddress::new_v4(0, 0, 0, 0, 9001),
10892 front_timeout: Some(5),
10893 ..Default::default()
10894 })
10895 .into(),
10896 )
10897 .unwrap_err();
10898 assert!(matches!(err, StateError::NotFound { .. }), "got: {err}");
10899 assert!(
10900 without_request_counts(&state) == before,
10901 "a refused TCP patch must change nothing"
10902 );
10903 }
10904
10905 #[test]
10911 fn dispatch_passes_through_set_metric_detail() {
10912 use crate::proto::command::{MetricDetail, SetMetricDetail};
10913 let mut state = ConfigState::new();
10914 let req: Request = RequestType::SetMetricDetail(SetMetricDetail {
10915 client_id: "test:1".to_owned(),
10916 detail: Some(MetricDetail::DetailBackend as i32),
10917 ttl_seconds: Some(60),
10918 clear: Some(false),
10919 reason: Some("regression-guard".to_owned()),
10920 peer_pid: None,
10921 peer_session_ulid: None,
10922 })
10923 .into();
10924 state
10925 .dispatch(&req)
10926 .expect("SetMetricDetail must traverse dispatch without UndispatchableRequest");
10927 }
10928
10929 #[test]
10968 fn remove_udp_frontend_drops_exactly_the_frontend_its_tags_name() {
10969 let team_a_address = SocketAddress::new_v4(127, 0, 0, 1, 9100);
10970 let team_b_address = SocketAddress::new_v4(127, 0, 0, 1, 9101);
10971 let front = |owner: &str, address| RequestUdpFrontend {
10972 cluster_id: "udp_cluster".to_string(),
10973 address,
10974 tags: BTreeMap::from([("owner".to_string(), owner.to_string())]),
10975 };
10976 let mut state = ConfigState::new();
10977
10978 for (owner, address) in [("team-a", team_a_address), ("team-b", team_b_address)] {
10982 state
10983 .dispatch(&RequestType::AddUdpFrontend(front(owner, address)).into())
10984 .expect("one UDP frontend per address must be admitted");
10985 }
10986 assert_eq!(
10987 state.udp_fronts.get("udp_cluster").map(Vec::len),
10988 Some(2usize),
10989 "both tagged UDP frontends must be in the state before the removal"
10990 );
10991
10992 state
10993 .dispatch(&RequestType::RemoveUdpFrontend(front("team-a", team_a_address)).into())
10994 .expect("removing one of the two must succeed");
10995
10996 let surviving: Vec<&str> = state
10997 .udp_fronts
10998 .get("udp_cluster")
10999 .map(|fronts| {
11000 fronts
11001 .iter()
11002 .filter_map(|f| f.tags.get("owner").map(String::as_str))
11003 .collect()
11004 })
11005 .unwrap_or_default();
11006 assert_eq!(
11007 surviving,
11008 vec!["team-b"],
11009 "removing one UDP frontend must not evict the rest of its cluster's bucket"
11010 );
11011
11012 let tagless = RequestUdpFrontend {
11016 cluster_id: "udp_cluster".to_string(),
11017 address: team_b_address,
11018 tags: BTreeMap::new(),
11019 };
11020 let err = state
11021 .dispatch(&RequestType::RemoveUdpFrontend(tagless).into())
11022 .expect_err("a tagless remove must not match a tagged UDP frontend");
11023 assert!(
11024 matches!(err, StateError::NoChange),
11025 "expected NoChange, got: {err}"
11026 );
11027 assert_eq!(
11028 state.udp_fronts.get("udp_cluster").map(Vec::len),
11029 Some(1usize),
11030 "the tagless remove must leave the surviving tagged frontend in place"
11031 );
11032 }
11033
11034 fn udp_front(cluster_id: &str, address: SocketAddress, owner: &str) -> RequestUdpFrontend {
11035 RequestUdpFrontend {
11036 cluster_id: cluster_id.to_string(),
11037 address,
11038 tags: BTreeMap::from([("owner".to_string(), owner.to_string())]),
11039 }
11040 }
11041
11042 #[test]
11073 fn add_udp_frontend_refuses_a_second_frontend_on_a_taken_address() {
11074 let address = SocketAddress::new_v4(127, 0, 0, 1, 5353);
11075 let other_address = SocketAddress::new_v4(127, 0, 0, 1, 5354);
11076 let mut state = ConfigState::new();
11077
11078 state
11079 .dispatch(&RequestType::AddUdpFrontend(udp_front("dns", address, "team-a")).into())
11080 .expect("the first claim on an address must be admitted");
11081
11082 state
11084 .dispatch(&RequestType::AddUdpFrontend(udp_front("dns", address, "team-b")).into())
11085 .expect_err("a second UDP frontend on a taken address must be refused");
11086
11087 state
11090 .dispatch(&RequestType::AddUdpFrontend(udp_front("syslog", address, "team-c")).into())
11091 .expect_err("a second UDP frontend on a taken address must be refused");
11092
11093 assert_eq!(
11094 state.count_udp_frontends_raw(),
11095 1,
11096 "a refused claim must leave the state holding only the admitted frontend"
11097 );
11098 assert!(
11099 state.udp_fronts.get("syslog").is_none_or(Vec::is_empty),
11100 "a refused cross-cluster claim must not leave an opened bucket behind"
11101 );
11102
11103 state
11105 .dispatch(
11106 &RequestType::AddUdpFrontend(udp_front("syslog", other_address, "team-c")).into(),
11107 )
11108 .expect("a UDP frontend on a free address must still be admitted");
11109
11110 state
11113 .dispatch(&RequestType::RemoveUdpFrontend(udp_front("dns", address, "team-a")).into())
11114 .expect("removing the frontend holding the address must succeed");
11115 state
11116 .dispatch(&RequestType::AddUdpFrontend(udp_front("syslog", address, "team-c")).into())
11117 .expect("a freed address must be claimable again, by any cluster");
11118
11119 assert_eq!(
11120 state.count_udp_frontends_raw(),
11121 2,
11122 "the freed address must now be held by exactly one frontend, beside the other address"
11123 );
11124 }
11125
11126 #[test]
11143 fn a_refused_udp_frontend_names_the_cluster_already_holding_the_address() {
11144 const OWNER_SECRET: &str = "UDP_ADDRESS_OWNER_SECRET_SENTINEL";
11145
11146 let address = SocketAddress::new_v4(127, 0, 0, 1, 5353);
11147 let owner_cluster = format!("{OWNER_SECRET}-dns");
11148 let owner_tag = format!("{OWNER_SECRET}-team-a");
11149 let mut state = ConfigState::new();
11150
11151 state
11152 .dispatch(
11153 &RequestType::AddUdpFrontend(udp_front(&owner_cluster, address, &owner_tag)).into(),
11154 )
11155 .expect("the first claim on an address must be admitted");
11156
11157 let error = state
11158 .dispatch(&RequestType::AddUdpFrontend(udp_front("syslog", address, "team-c")).into())
11159 .expect_err("a second UDP frontend on a taken address must be refused");
11160
11161 match &error {
11162 StateError::UdpFrontendAddressTaken {
11163 address: refused_address,
11164 cluster_id,
11165 current_cluster_id,
11166 current_tags,
11167 } => {
11168 assert_eq!(
11169 *refused_address,
11170 SocketAddr::from(address),
11171 "the refusal must name the contested address"
11172 );
11173 assert_eq!(
11174 &**cluster_id, "syslog",
11175 "the refusal must name the claimant"
11176 );
11177 assert_eq!(
11178 **current_cluster_id, *owner_cluster,
11179 "the refusal must name the cluster already holding the address"
11180 );
11181 assert_eq!(
11182 current_tags.get("owner"),
11183 Some(&owner_tag),
11184 "the refusal must carry the current owner's access-log tags"
11185 );
11186 }
11187 other => panic!("expected UdpFrontendAddressTaken, got {other:?}"),
11188 }
11189
11190 for (label, output) in [
11193 ("Display", error.to_string()),
11194 ("Debug", format!("{error:?}")),
11195 ] {
11196 assert!(
11197 output.contains("127.0.0.1:5353"),
11198 "StateError {label} must name the contested address: {output}"
11199 );
11200 assert!(
11201 !output.contains(OWNER_SECRET),
11202 "StateError {label} leaked a protected identifier: {output}"
11203 );
11204 assert!(
11205 output.contains(&format!("current_cluster_id_bytes={}", owner_cluster.len())),
11206 "StateError {label} omitted the bounded owner metadata: {output}"
11207 );
11208 assert!(
11209 output.contains("current_tag_count=1"),
11210 "StateError {label} omitted the bounded owner tag count: {output}"
11211 );
11212 }
11213 }
11214
11215 #[test]
11238 fn a_saved_state_holding_one_udp_frontend_per_address_replays_unchanged() {
11239 use std::io::Read;
11240
11241 use crate::parser::parse_several_requests;
11242
11243 let first = SocketAddress::new_v4(127, 0, 0, 1, 5353);
11244 let second = SocketAddress::new_v4(127, 0, 0, 2, 5353);
11245
11246 let mut state = ConfigState::new();
11247 for (cluster_id, address) in [("dns", first), ("syslog", second)] {
11248 state
11249 .dispatch(&RequestType::AddUdpListener(make_udp_listener(address, true)).into())
11250 .expect("could not add udp listener");
11251 state
11252 .dispatch(
11253 &RequestType::ActivateListener(ActivateListener {
11254 interface: None,
11255 address,
11256 proxy: ListenerType::Udp.into(),
11257 from_scm: false,
11258 })
11259 .into(),
11260 )
11261 .expect("could not activate udp listener");
11262 state
11263 .dispatch(
11264 &RequestType::AddUdpFrontend(udp_front(cluster_id, address, "team-a")).into(),
11265 )
11266 .expect("one UDP frontend per address must be admitted");
11267 }
11268
11269 let directory = tempfile::tempdir().expect("a temporary directory for the state file");
11270 let path = directory.path().join("udpaddr-roundtrip.state");
11271 let mut file = File::create(&path).expect("the state file must be creatable");
11272 let written = state
11273 .write_requests_to_file(&mut file)
11274 .expect("SaveState must serialise the state");
11275 drop(file);
11276
11277 let mut saved = Vec::new();
11278 File::open(&path)
11279 .expect("the state file must be readable")
11280 .read_to_end(&mut saved)
11281 .expect("the state file must be readable");
11282
11283 let (remainder, requests) =
11284 parse_several_requests::<WorkerRequest>(&saved).expect("the state file must parse");
11285 assert!(
11286 remainder.is_empty(),
11287 "load_state would report leftover bytes: {} unparsed",
11288 remainder.len()
11289 );
11290 assert_eq!(
11291 requests.len(),
11292 written,
11293 "every record write_requests_to_file emitted must parse back"
11294 );
11295
11296 let mut replayed = ConfigState::new();
11297 for request in &requests {
11298 replayed
11299 .dispatch(&request.content)
11300 .expect("every saved UDP entry must replay into a fresh state");
11301 }
11302
11303 let logical = |s: &ConfigState| {
11304 let mut c = s.clone();
11305 c.request_counts.clear();
11306 c
11307 };
11308 assert_eq!(
11309 logical(&state),
11310 logical(&replayed),
11311 "a SaveState -> LoadState round trip must reconstruct the UDP configuration"
11312 );
11313 }
11314
11315 fn with_interface<T>(mut listener: T, set: impl FnOnce(&mut T)) -> T {
11318 set(&mut listener);
11319 listener
11320 }
11321
11322 #[test]
11326 fn listeners_on_one_address_with_different_interfaces_coexist() {
11327 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
11328 let mut state = ConfigState::new();
11329 for interface in [None, Some("lo"), Some("wg0")] {
11330 let iface = interface.map(str::to_owned);
11331 state
11332 .dispatch(
11333 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11334 l.interface = iface.clone()
11335 }))
11336 .into(),
11337 )
11338 .expect("an HTTP listener on another interface must not collide");
11339 state
11340 .dispatch(
11341 &RequestType::AddHttpsListener(with_interface(
11342 make_https_listener(addr),
11343 |l| l.interface = iface.clone(),
11344 ))
11345 .into(),
11346 )
11347 .expect("an HTTPS listener on another interface must not collide");
11348 state
11349 .dispatch(
11350 &RequestType::AddTcpListener(with_interface(make_tcp_listener(addr), |l| {
11351 l.interface = iface.clone()
11352 }))
11353 .into(),
11354 )
11355 .expect("a TCP listener on another interface must not collide");
11356 state
11357 .dispatch(
11358 &RequestType::AddUdpListener(with_interface(
11359 make_udp_listener(addr, false),
11360 |l| l.interface = iface.clone(),
11361 ))
11362 .into(),
11363 )
11364 .expect("a UDP listener on another interface must not collide");
11365 }
11366
11367 let expected = vec![
11368 "0.0.0.0:8443".to_owned(),
11369 "0.0.0.0:8443%lo".to_owned(),
11370 "0.0.0.0:8443%wg0".to_owned(),
11371 ];
11372 let list = state.list_listeners();
11373 assert_eq!(
11374 list.http_listeners.keys().cloned().collect::<Vec<_>>(),
11375 expected
11376 );
11377 assert_eq!(
11378 list.https_listeners.keys().cloned().collect::<Vec<_>>(),
11379 expected
11380 );
11381 assert_eq!(
11382 list.tcp_listeners.keys().cloned().collect::<Vec<_>>(),
11383 expected
11384 );
11385 assert_eq!(
11386 list.udp_listeners.keys().cloned().collect::<Vec<_>>(),
11387 expected
11388 );
11389 assert_eq!(
11390 list.http_listeners["0.0.0.0:8443%wg0"].interface.as_deref(),
11391 Some("wg0")
11392 );
11393
11394 assert!(
11396 state
11397 .dispatch(
11398 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11399 l.interface = Some("lo".to_owned())
11400 }))
11401 .into(),
11402 )
11403 .is_err(),
11404 "the same (address, interface) must still be refused as a duplicate"
11405 );
11406
11407 state
11409 .dispatch(
11410 &RequestType::ActivateListener(ActivateListener {
11411 address: addr,
11412 proxy: ListenerType::Http.into(),
11413 from_scm: false,
11414 interface: Some("wg0".to_owned()),
11415 })
11416 .into(),
11417 )
11418 .expect("activating the wg0 listener");
11419 let list = state.list_listeners();
11420 assert!(list.http_listeners["0.0.0.0:8443%wg0"].active);
11421 assert!(!list.http_listeners["0.0.0.0:8443%lo"].active);
11422 assert!(!list.http_listeners["0.0.0.0:8443"].active);
11423
11424 state
11425 .dispatch(
11426 &RequestType::RemoveListener(RemoveListener {
11427 address: addr,
11428 proxy: ListenerType::Tcp.into(),
11429 interface: Some("lo".to_owned()),
11430 })
11431 .into(),
11432 )
11433 .expect("removing the lo TCP listener");
11434 let list = state.list_listeners();
11435 assert_eq!(
11436 list.tcp_listeners.keys().cloned().collect::<Vec<_>>(),
11437 vec!["0.0.0.0:8443".to_owned(), "0.0.0.0:8443%wg0".to_owned()]
11438 );
11439
11440 assert!(
11443 state
11444 .dispatch(
11445 &RequestType::RemoveListener(RemoveListener {
11446 address: addr,
11447 proxy: ListenerType::Tcp.into(),
11448 interface: Some("eth9".to_owned()),
11449 })
11450 .into(),
11451 )
11452 .is_err()
11453 );
11454
11455 let mut replayed = ConfigState::new();
11458 for request in state.generate_requests() {
11459 replayed
11460 .dispatch(&request)
11461 .expect("replaying generated requests");
11462 }
11463 assert_eq!(replayed.list_listeners(), state.list_listeners());
11464 assert!(state.diff(&replayed).is_empty());
11465 }
11466
11467 #[test]
11471 fn listener_diff_distinguishes_interfaces() {
11472 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11473 let mut before = ConfigState::new();
11474 before
11475 .dispatch(
11476 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11477 l.interface = Some("eth0".to_owned())
11478 }))
11479 .into(),
11480 )
11481 .unwrap();
11482 let mut after = ConfigState::new();
11483 after
11484 .dispatch(
11485 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11486 l.interface = Some("wg0".to_owned())
11487 }))
11488 .into(),
11489 )
11490 .unwrap();
11491
11492 let diff = before.diff(&after);
11493 let mut removed = Vec::new();
11494 let mut added = Vec::new();
11495 for request in &diff {
11496 match &request.request_type {
11497 Some(RequestType::RemoveListener(remove)) => removed.push(remove.interface.clone()),
11498 Some(RequestType::AddHttpListener(add)) => added.push(add.interface.clone()),
11499 _ => {}
11500 }
11501 }
11502 assert_eq!(removed, vec![Some("eth0".to_owned())]);
11503 assert_eq!(added, vec![Some("wg0".to_owned())]);
11504 }
11505
11506 #[test]
11512 fn reload_adding_an_interface_listener_sends_only_that_listener() {
11513 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11514 let front = RequestHttpFrontend {
11515 cluster_id: Some("cluster_1".to_owned()),
11516 hostname: "example.com".to_owned(),
11517 path: PathRule::prefix("/".to_owned()),
11518 address: addr,
11519 ..Default::default()
11520 };
11521 let mut before = ConfigState::new();
11522 for request in [
11523 RequestType::AddHttpListener(make_http_listener(addr)),
11524 RequestType::AddHttpFrontend(front),
11525 ] {
11526 before.dispatch(&request.into()).unwrap();
11527 }
11528 let mut after = before.clone();
11529 after
11530 .dispatch(
11531 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11532 l.interface = Some("wg0".to_owned())
11533 }))
11534 .into(),
11535 )
11536 .unwrap();
11537
11538 let diff = before.diff(&after);
11539 assert_eq!(diff.len(), 1, "{diff:?}");
11540 assert!(matches!(
11541 &diff[0].request_type,
11542 Some(RequestType::AddHttpListener(listener))
11543 if listener.interface.as_deref() == Some("wg0")
11544 ));
11545 }
11546
11547 #[test]
11551 fn update_listener_is_refused_when_the_address_is_ambiguous() {
11552 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11553 let mut state = ConfigState::new();
11554 state
11555 .dispatch(
11556 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11557 l.interface = Some("wg0".to_owned())
11558 }))
11559 .into(),
11560 )
11561 .unwrap();
11562
11563 state
11565 .dispatch(
11566 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
11567 address: addr,
11568 front_timeout: Some(15),
11569 ..Default::default()
11570 })
11571 .into(),
11572 )
11573 .expect("an unambiguous patch applies to the interface-bound listener");
11574 assert_eq!(
11575 state.list_listeners().http_listeners["0.0.0.0:8080%wg0"].front_timeout,
11576 15
11577 );
11578
11579 state
11580 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
11581 .unwrap();
11582 let before = state.list_listeners();
11583 let error = state
11584 .dispatch(
11585 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
11586 address: addr,
11587 front_timeout: Some(20),
11588 ..Default::default()
11589 })
11590 .into(),
11591 )
11592 .expect_err("a patch matching two listeners must be refused");
11593 assert!(
11594 matches!(error, StateError::AmbiguousListenerAddress { count: 2, .. }),
11595 "{error}"
11596 );
11597 assert_eq!(
11598 before,
11599 state.list_listeners(),
11600 "a refused patch must leave the listeners untouched"
11601 );
11602 }
11603
11604 #[test]
11610 fn header_predicates_are_part_of_the_frontend_state_key() {
11611 use crate::proto::command::{HeaderMatchKind, HeaderPredicate};
11612
11613 let predicate = |name: &str, kind: HeaderMatchKind, value: Option<&str>| HeaderPredicate {
11614 name: name.to_owned(),
11615 kind: kind as i32,
11616 value: value.map(str::to_owned),
11617 };
11618 let base = RequestHttpFrontend {
11619 cluster_id: Some("stable".to_owned()),
11620 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
11621 hostname: "api.example.com".to_owned(),
11622 path: PathRule::prefix("/".to_owned()),
11623 ..Default::default()
11624 };
11625 let canary = RequestHttpFrontend {
11626 cluster_id: Some("canary".to_owned()),
11627 header_predicates: vec![
11628 predicate("X-Canary", HeaderMatchKind::Exact, Some("always")),
11629 predicate("X-Beta", HeaderMatchKind::Present, None),
11630 ],
11631 ..base.clone()
11632 };
11633 let canary_respelled = RequestHttpFrontend {
11634 header_predicates: vec![
11635 predicate("x-beta", HeaderMatchKind::Present, None),
11636 predicate("x-canary", HeaderMatchKind::Exact, Some("always")),
11637 ],
11638 ..canary.clone()
11639 };
11640
11641 assert_eq!(
11642 base.to_string(),
11643 "127.0.0.1:8080;api.example.com;P/",
11644 "a frontend without predicates keeps its historical key"
11645 );
11646 assert_eq!(canary.to_string(), canary_respelled.to_string());
11647
11648 let single = RequestHttpFrontend {
11651 header_predicates: vec![predicate("X-Beta", HeaderMatchKind::Present, None)],
11652 ..base.clone()
11653 };
11654 let repeated = RequestHttpFrontend {
11655 header_predicates: vec![
11656 predicate("X-Beta", HeaderMatchKind::Present, None),
11657 predicate("x-beta", HeaderMatchKind::Present, None),
11658 ],
11659 ..base.clone()
11660 };
11661 assert_eq!(single.to_string(), repeated.to_string());
11662
11663 let mut state = ConfigState::default();
11664 state.add_http_frontend(&base).expect("base");
11665 state
11666 .add_http_frontend(&canary)
11667 .expect("a canary must coexist with its base");
11668 assert!(matches!(
11669 state.add_http_frontend(&canary_respelled),
11670 Err(StateError::Exists { .. })
11671 ));
11672 assert_eq!(state.http_fronts.len(), 2);
11673
11674 let mut replayed = ConfigState::default();
11677 for request in state.generate_requests() {
11678 replayed.dispatch(&request).expect("replay must succeed");
11679 }
11680 assert_eq!(replayed.http_fronts, state.http_fronts);
11681
11682 let replay_types: Vec<_> = state
11686 .generate_requests()
11687 .into_iter()
11688 .filter_map(|request| request.request_type)
11689 .collect();
11690 assert!(replay_types.contains(&RequestType::AddHttpFrontend(base.clone())));
11691 assert!(
11692 replay_types.contains(&RequestType::AddHttpFrontendWithHeaderPredicates(
11693 canary.clone()
11694 ))
11695 );
11696 assert!(!replay_types.contains(&RequestType::AddHttpFrontend(canary.clone())));
11697
11698 for add in [
11701 RequestType::AddHttpFrontend(canary.clone()),
11702 RequestType::AddHttpFrontendWithHeaderPredicates(canary.clone()),
11703 ] {
11704 assert_eq!(
11705 rollback_request(&add).and_then(|request| request.request_type),
11706 Some(RequestType::RemoveHttpFrontendWithHeaderPredicates(
11707 canary.clone()
11708 ))
11709 );
11710 }
11711 assert_eq!(
11712 rollback_request(&RequestType::AddHttpFrontend(base.clone()))
11713 .and_then(|request| request.request_type),
11714 Some(RequestType::RemoveHttpFrontend(base.clone()))
11715 );
11716
11717 state
11718 .remove_http_frontend(&canary_respelled)
11719 .expect("a respelled removal must find the canary");
11720 assert_eq!(
11721 state
11722 .http_fronts
11723 .values()
11724 .map(|front| front.cluster_id.as_deref())
11725 .collect::<Vec<_>>(),
11726 vec![Some("stable")],
11727 "removing the canary must leave its base"
11728 );
11729 }
11730
11731 #[test]
11736 fn close_sessions_is_never_saved_nor_replayed() {
11737 let mut state: ConfigState = Default::default();
11738 state
11739 .dispatch(
11740 &RequestType::AddCluster(Cluster {
11741 cluster_id: String::from("cluster_1"),
11742 ..Default::default()
11743 })
11744 .into(),
11745 )
11746 .expect("Could not execute request");
11747 state
11748 .dispatch(
11749 &RequestType::AddBackend(AddBackend {
11750 cluster_id: String::from("cluster_1"),
11751 backend_id: String::from("cluster_1-0"),
11752 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
11753 ..Default::default()
11754 })
11755 .into(),
11756 )
11757 .expect("Could not execute request");
11758 let requests_before = state.generate_requests();
11759 let hashes_before = state.hash_state();
11760
11761 state
11762 .dispatch(
11763 &RequestType::CloseSessions(CloseSessions {
11764 cluster_id: String::from("cluster_1"),
11765 backend_id: Some(String::from("cluster_1-0")),
11766 address: Some(SocketAddress::new_v4(127, 0, 0, 1, 1026)),
11767 })
11768 .into(),
11769 )
11770 .expect("CloseSessions must be accepted by the state");
11771
11772 let requests_after = state.generate_requests();
11773 assert_eq!(requests_after, requests_before);
11774 assert!(
11775 !requests_after
11776 .iter()
11777 .any(|request| matches!(request.request_type, Some(RequestType::CloseSessions(_))))
11778 );
11779 assert_eq!(state.hash_state(), hashes_before);
11780 }
11781}