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>>| {
4030 m.iter()
4031 .filter(|(_, v)| !v.is_empty())
4032 .map(|(k, v)| {
4033 (
4034 k.clone(),
4035 v.iter().cloned().collect::<BTreeSet<TcpFrontend>>(),
4036 )
4037 })
4038 .collect::<HashMap<_, _>>()
4039 };
4040 debug_assert!(
4041 replayed.clusters == other.clusters
4042 && nonempty(&replayed.backends) == nonempty(&other.backends)
4043 && replayed.http_fronts == other.http_fronts
4044 && replayed.https_fronts == other.https_fronts
4045 && nonempty_tcp(&replayed.tcp_fronts) == nonempty_tcp(&other.tcp_fronts)
4046 && replayed.certificates == other.certificates
4047 && replayed.normalized_certificate_metadata()
4051 == other.normalized_certificate_metadata()
4052 && replayed.http_listeners == other.http_listeners
4053 && replayed.https_listeners == other.https_listeners
4054 && replayed.tcp_listeners == other.tcp_listeners
4055 && replayed.udp_listeners == other.udp_listeners,
4056 "replaying diff(self, other) onto self must reproduce other's clusters/backends/frontends/certificates/listeners"
4057 );
4058 }
4059
4060 v
4061 }
4062
4063 pub fn hash_state(&self) -> BTreeMap<ClusterId, u64> {
4065 let mut hm: HashMap<ClusterId, DefaultHasher> = self
4066 .clusters
4067 .keys()
4068 .map(|cluster_id| {
4069 let mut hasher = DefaultHasher::new();
4070 self.clusters.get(cluster_id).hash(&mut hasher);
4071 if let Some(backends) = self.backends.get(cluster_id) {
4072 backends.iter().collect::<BTreeSet<_>>().hash(&mut hasher)
4073 }
4074 if let Some(tcp_fronts) = self.tcp_fronts.get(cluster_id) {
4075 tcp_fronts.iter().collect::<BTreeSet<_>>().hash(&mut hasher)
4076 }
4077 (cluster_id.to_owned(), hasher)
4078 })
4079 .collect();
4080
4081 for front in self.http_fronts.values() {
4082 if let Some(cluster_id) = &front.cluster_id
4083 && let Some(hasher) = hm.get_mut(cluster_id)
4084 {
4085 front.hash(hasher);
4086 }
4087 }
4088
4089 for front in self.https_fronts.values() {
4090 if let Some(cluster_id) = &front.cluster_id
4091 && let Some(hasher) = hm.get_mut(cluster_id)
4092 {
4093 front.hash(hasher);
4094 }
4095 }
4096
4097 hm.drain()
4098 .map(|(cluster_id, hasher)| (cluster_id, hasher.finish()))
4099 .collect()
4100 }
4101
4102 pub fn cluster_state(&self, cluster_id: &str) -> Option<ClusterInformation> {
4105 let configuration = self.clusters.get(cluster_id).cloned()?;
4106 info!("{:?}", configuration);
4107
4108 let http_frontends: Vec<RequestHttpFrontend> = self
4109 .http_fronts
4110 .values()
4111 .filter(|front| front.cluster_id.as_deref() == Some(cluster_id))
4112 .map(|front| front.clone().into())
4113 .collect();
4114
4115 let https_frontends: Vec<RequestHttpFrontend> = self
4116 .https_fronts
4117 .values()
4118 .filter(|front| front.cluster_id.as_deref() == Some(cluster_id))
4119 .map(|front| front.clone().into())
4120 .collect();
4121
4122 let tcp_frontends: Vec<RequestTcpFrontend> = self
4123 .tcp_fronts
4124 .get(cluster_id)
4125 .cloned()
4126 .unwrap_or_default()
4127 .iter()
4128 .map(|front| front.clone().into())
4129 .collect();
4130
4131 let udp_frontends: Vec<RequestUdpFrontend> = self
4132 .udp_fronts
4133 .get(cluster_id)
4134 .cloned()
4135 .unwrap_or_default()
4136 .iter()
4137 .map(|front| front.clone().into())
4138 .collect();
4139
4140 let backends: Vec<AddBackend> = self
4141 .backends
4142 .get(cluster_id)
4143 .cloned()
4144 .unwrap_or_default()
4145 .iter()
4146 .map(|backend| backend.clone().into())
4147 .collect();
4148
4149 Some(ClusterInformation {
4150 configuration: Some(configuration),
4151 http_frontends,
4152 https_frontends,
4153 tcp_frontends,
4154 backends,
4155 udp_frontends,
4156 })
4157 }
4158
4159 pub fn count_backends(&self) -> usize {
4160 self.backends.values().fold(0, |acc, v| acc + v.len())
4161 }
4162
4163 pub fn count_frontends(&self) -> usize {
4164 self.http_fronts.values().count()
4165 + self.https_fronts.values().count()
4166 + self.tcp_fronts.values().fold(0, |acc, v| acc + v.len())
4167 + self.udp_fronts.values().fold(0, |acc, v| acc + v.len())
4168 }
4169
4170 pub fn get_cluster_ids_by_domain(
4171 &self,
4172 hostname: String,
4173 path: Option<String>,
4174 ) -> HashSet<ClusterId> {
4175 let mut cluster_ids: HashSet<ClusterId> = HashSet::new();
4176
4177 self.http_fronts.values().for_each(|front| {
4178 if domain_check(&front.hostname, &front.path, &hostname, &path)
4179 && let Some(id) = &front.cluster_id
4180 {
4181 cluster_ids.insert(id.clone());
4182 }
4183 });
4184
4185 self.https_fronts.values().for_each(|front| {
4186 if domain_check(&front.hostname, &front.path, &hostname, &path)
4187 && let Some(id) = &front.cluster_id
4188 {
4189 cluster_ids.insert(id.clone());
4190 }
4191 });
4192
4193 cluster_ids
4194 }
4195
4196 pub fn get_certificates(
4214 &self,
4215 filters: QueryCertificatesFilters,
4216 ) -> BTreeMap<String, CertificateAndKey> {
4217 self.certificates
4218 .values()
4219 .flat_map(|hash_map| hash_map.iter())
4220 .filter(|(fingerprint, cert)| {
4221 if let Some(domain) = &filters.domain {
4222 cert.names.contains(domain)
4223 } else if let Some(f) = &filters.fingerprint {
4224 fingerprint.to_string() == *f
4225 } else {
4226 true
4227 }
4228 })
4229 .map(|(fingerprint, cert)| (fingerprint.to_string(), cert.to_owned()))
4230 .collect()
4231 }
4232
4233 pub fn list_frontends(&self, filters: FrontendFilters) -> ListedFrontends {
4234 let list_all = !filters.http && !filters.https && !filters.tcp;
4236
4237 let matches_cluster = |cluster_id: Option<&str>| match &filters.cluster_id {
4240 Some(wanted) => cluster_id == Some(wanted.as_str()),
4241 None => true,
4242 };
4243 let matches_domain = |hostname: &str| match &filters.domain {
4244 Some(domain) => hostname.contains(domain),
4245 None => true,
4246 };
4247
4248 let mut listed_frontends = ListedFrontends::default();
4249
4250 if filters.http || list_all {
4251 for http_frontend in self.http_fronts.iter().filter(|f| {
4252 matches_domain(&f.1.hostname) && matches_cluster(f.1.cluster_id.as_deref())
4253 }) {
4254 listed_frontends
4255 .http_frontends
4256 .push(http_frontend.1.to_owned().into());
4257 }
4258 }
4259
4260 if filters.https || list_all {
4261 for https_frontend in self.https_fronts.iter().filter(|f| {
4262 matches_domain(&f.1.hostname) && matches_cluster(f.1.cluster_id.as_deref())
4263 }) {
4264 listed_frontends
4265 .https_frontends
4266 .push(https_frontend.1.to_owned().into());
4267 }
4268 }
4269
4270 if (filters.tcp || list_all) && filters.domain.is_none() {
4271 for tcp_frontend in fronts_of_cluster(&self.tcp_fronts, filters.cluster_id.as_deref()) {
4272 listed_frontends
4273 .tcp_frontends
4274 .push(tcp_frontend.to_owned().into())
4275 }
4276 }
4277
4278 if (filters.tcp || list_all) && filters.domain.is_none() {
4284 for udp_frontend in fronts_of_cluster(&self.udp_fronts, filters.cluster_id.as_deref()) {
4285 listed_frontends
4286 .udp_frontends
4287 .push(udp_frontend.to_owned().into())
4288 }
4289 }
4290
4291 listed_frontends
4292 }
4293
4294 pub fn list_listeners(&self) -> ListenersList {
4295 ListenersList {
4296 http_listeners: self
4297 .http_listeners
4298 .iter()
4299 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4300 .collect(),
4301 https_listeners: self
4302 .https_listeners
4303 .iter()
4304 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4305 .collect(),
4306 tcp_listeners: self
4307 .tcp_listeners
4308 .iter()
4309 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4310 .collect(),
4311 udp_listeners: self
4312 .udp_listeners
4313 .iter()
4314 .map(|(addr, listener)| (addr.to_string(), listener.clone()))
4315 .collect(),
4316 }
4317 }
4318
4319 pub fn produce_initial_state(&self) -> InitialState {
4321 let mut worker_requests = Vec::new();
4322 for (counter, request) in self.generate_requests().into_iter().enumerate() {
4323 worker_requests.push(WorkerRequest::new(format!("SAVE-{counter}"), request));
4324 }
4325 InitialState {
4326 requests: worker_requests,
4327 }
4328 }
4329
4330 pub fn write_initial_state_to_file(&self, file: &mut File) -> Result<usize, StateError> {
4333 let initial_state = self.produce_initial_state();
4334 let count = initial_state.requests.len();
4335
4336 let bytes_to_write = initial_state.encode_to_vec();
4337 println!("writing {} in the temp file", bytes_to_write.len());
4338 file.write_all(&bytes_to_write)
4339 .map_err(StateError::FileError)?;
4340
4341 file.sync_all().map_err(StateError::FileError)?;
4342
4343 Ok(count)
4344 }
4345
4346 pub fn write_requests_to_file(&self, file: &mut File) -> Result<usize, StateError> {
4350 let mut counter = 0usize;
4351 let requests = self.generate_requests();
4352
4353 for request in requests {
4354 let message = WorkerRequest::new(format!("SAVE-{counter}"), request);
4355
4356 file.write_all(
4357 &serde_json::to_string(&message)
4358 .map(|s| s.into_bytes())
4359 .unwrap_or_default(),
4360 )
4361 .map_err(StateError::FileError)?;
4362
4363 file.write_all(&b"\n\0"[..])
4364 .map_err(StateError::FileError)?;
4365
4366 if counter.is_multiple_of(1000) {
4367 info!("writing {} commands to file", counter);
4368 file.sync_all().map_err(StateError::FileError)?;
4369 }
4370 counter += 1;
4371 }
4372 file.sync_all().map_err(StateError::FileError)?;
4373
4374 Ok(counter)
4375 }
4376}
4377
4378macro_rules! require_h2_knob_floor {
4386 ($config:ident, $field:ident, $minimum:literal) => {
4387 if let Some(value) = $config.$field
4388 && u64::from(value) < $minimum
4389 {
4390 return Err(StateError::InvalidValue {
4391 field: stringify!($field),
4392 reason: concat!("must be >= ", stringify!($minimum)),
4393 });
4394 }
4395 };
4396}
4397
4398pub fn validate_h2_flood_knobs_http(patch: &UpdateHttpListenerConfig) -> Result<(), StateError> {
4416 crate::for_each_h2_knob_floor!(require_h2_knob_floor, patch);
4417 Ok(())
4418}
4419
4420pub fn validate_h2_flood_knobs_https(patch: &UpdateHttpsListenerConfig) -> Result<(), StateError> {
4422 crate::for_each_h2_knob_floor!(require_h2_knob_floor, patch);
4423 Ok(())
4424}
4425
4426pub fn validate_h2_flood_knobs_http_listener(
4450 config: &HttpListenerConfig,
4451) -> Result<(), StateError> {
4452 crate::for_each_h2_knob_floor!(require_h2_knob_floor, config);
4453 Ok(())
4454}
4455
4456pub fn validate_h2_flood_knobs_https_listener(
4460 config: &HttpsListenerConfig,
4461) -> Result<(), StateError> {
4462 crate::for_each_h2_knob_floor!(require_h2_knob_floor, config);
4463 Ok(())
4464}
4465
4466fn merge_answers(stored: &mut BTreeMap<String, String>, patch: &BTreeMap<String, String>) {
4476 for (code, body) in patch {
4477 if !body.is_empty() {
4478 stored.insert(code.to_owned(), body.to_owned());
4479 }
4480 }
4481}
4482
4483pub fn merge_custom_http_answers(
4491 target: &mut Option<CustomHttpAnswers>,
4492 patch: &CustomHttpAnswers,
4493) {
4494 let current = target.get_or_insert_with(CustomHttpAnswers::default);
4495 macro_rules! merge_field {
4496 ($field:ident) => {
4497 if let Some(ref v) = patch.$field {
4498 current.$field = Some(v.clone());
4499 }
4500 };
4501 }
4502 merge_field!(answer_301);
4503 merge_field!(answer_400);
4504 merge_field!(answer_401);
4505 merge_field!(answer_404);
4506 merge_field!(answer_408);
4507 merge_field!(answer_413);
4508 merge_field!(answer_421);
4509 merge_field!(answer_502);
4510 merge_field!(answer_503);
4511 merge_field!(answer_504);
4512 merge_field!(answer_507);
4513}
4514
4515pub fn validate_alpn_protocols(values: &[String]) -> Result<(), StateError> {
4518 for value in values {
4519 if value != "h2" && value != "http/1.1" {
4520 return Err(StateError::InvalidValue {
4521 field: "alpn_protocols",
4522 reason: "each value must be \"h2\" or \"http/1.1\"",
4523 });
4524 }
4525 }
4526 Ok(())
4527}
4528
4529pub fn validate_sozu_id_header(value: &str) -> Result<(), StateError> {
4540 if value.is_empty() {
4541 return Err(StateError::InvalidValue {
4542 field: "sozu_id_header",
4543 reason: "must not be empty",
4544 });
4545 }
4546 for b in value.bytes() {
4547 let is_tchar = b.is_ascii_alphanumeric()
4548 || matches!(
4549 b,
4550 b'!' | b'#'
4551 | b'$'
4552 | b'%'
4553 | b'&'
4554 | b'\''
4555 | b'*'
4556 | b'+'
4557 | b'-'
4558 | b'.'
4559 | b'^'
4560 | b'_'
4561 | b'`'
4562 | b'|'
4563 | b'~'
4564 );
4565 if !is_tchar {
4566 return Err(StateError::InvalidValue {
4567 field: "sozu_id_header",
4568 reason: "must be a valid HTTP header name (RFC 9110 §5.1 token: alphanumeric or one of !#$%&'*+-.^_`|~)",
4569 });
4570 }
4571 }
4572 Ok(())
4573}
4574
4575pub fn validate_hsts_patch(hsts: &HstsConfig) -> Result<(), StateError> {
4580 if hsts.enabled.is_none() {
4581 return Err(StateError::InvalidValue {
4582 field: "hsts",
4583 reason: "`enabled` is required whenever the hsts block is present",
4584 });
4585 }
4586 Ok(())
4587}
4588
4589pub fn validate_forwarded_headers(value: i32) -> Result<(), StateError> {
4594 ForwardedHeaders::try_from(value)
4595 .map(|_| ())
4596 .map_err(|_| StateError::InvalidValue {
4597 field: "forwarded_headers",
4598 reason: "must be one of both, x_forwarded, rfc7239, none",
4599 })
4600}
4601
4602fn fronts_of_cluster<'a, F>(
4606 fronts: &'a HashMap<ClusterId, Vec<F>>,
4607 cluster_id: Option<&str>,
4608) -> impl Iterator<Item = &'a F> {
4609 let (one, all) = match cluster_id {
4610 Some(cluster_id) => (fronts.get(cluster_id), None),
4611 None => (None, Some(fronts.values())),
4612 };
4613 one.into_iter().chain(all.into_iter().flatten()).flatten()
4614}
4615
4616fn domain_check(
4617 front_hostname: &str,
4618 front_path_rule: &PathRule,
4619 hostname: &str,
4620 path_prefix: &Option<String>,
4621) -> bool {
4622 if hostname != front_hostname {
4623 return false;
4624 }
4625
4626 if let Some(path) = &path_prefix {
4627 return path == &front_path_rule.value;
4628 }
4629
4630 true
4631}
4632
4633fn canonical_tcp_alpn(alpn: &[String]) -> Vec<String> {
4643 let mut alpn = alpn.to_vec();
4644 alpn.sort();
4645 alpn.dedup();
4646 debug_assert!(
4650 alpn.windows(2).all(|pair| pair[0] < pair[1]),
4651 "canonical_tcp_alpn must return a strictly sorted, duplicate-free list"
4652 );
4653 alpn
4654}
4655
4656fn tcp_frontend_matches(
4667 front: &TcpFrontend,
4668 address: SocketAddr,
4669 sni: &Option<String>,
4670 alpn: &[String],
4671) -> bool {
4672 front.address == address && front.sni == *sni && front.alpn.as_slice() == alpn
4673}
4674
4675struct DiffMap<'a, K: Ord, V, I1, I2> {
4676 my_it: I1,
4677 other_it: I2,
4678 my: Option<(K, &'a V)>,
4679 other: Option<(K, &'a V)>,
4680}
4681
4682fn diff_map<
4684 'a,
4685 K: Ord,
4686 V: PartialEq,
4687 I1: Iterator<Item = (K, &'a V)>,
4688 I2: Iterator<Item = (K, &'a V)>,
4689>(
4690 my: I1,
4691 other: I2,
4692) -> DiffMap<'a, K, V, I1, I2> {
4693 DiffMap {
4694 my_it: my,
4695 other_it: other,
4696 my: None,
4697 other: None,
4698 }
4699}
4700
4701enum DiffResult {
4702 Added,
4703 Removed,
4704 Changed,
4705}
4706
4707impl<'a, K: Ord, V: PartialEq, I1: Iterator<Item = (K, &'a V)>, I2: Iterator<Item = (K, &'a V)>>
4710 std::iter::Iterator for DiffMap<'a, K, V, I1, I2>
4711{
4712 type Item = (K, DiffResult);
4713
4714 fn next(&mut self) -> Option<Self::Item> {
4715 loop {
4716 if self.my.is_none() {
4717 self.my = self.my_it.next();
4718 }
4719 if self.other.is_none() {
4720 self.other = self.other_it.next();
4721 }
4722
4723 match (self.my.take(), self.other.take()) {
4724 (None, other) => return other.map(|(k, _)| (k, DiffResult::Added)),
4728 (Some((k, _)), None) => return Some((k, DiffResult::Removed)),
4731 (Some((k1, _v1)), Some((k2, v2))) if k1 < k2 => {
4733 self.other = Some((k2, v2));
4734 return Some((k1, DiffResult::Removed));
4735 }
4736 (Some((k1, v1)), Some((k2, _v2))) if k1 > k2 => {
4738 self.my = Some((k1, v1));
4739 return Some((k2, DiffResult::Added));
4740 }
4741 (Some((k1, v1)), Some((_k2, v2))) if v1 != v2 => {
4742 return Some((k1, DiffResult::Changed));
4745 }
4746 _ => {}
4747 }
4748 }
4749 }
4750}
4751
4752#[cfg(test)]
4753mod tests {
4754 use rand::{RngExt, rng, seq::SliceRandom};
4755
4756 use super::*;
4757 use crate::proto::command::{
4758 ClientAuthMode, CloseSessions, CustomHttpAnswers, Header, HeaderPosition, HstsConfig,
4759 LoadBalancingParams, PathRuleKind, RedirectPolicy, RedirectScheme, RequestHttpFrontend,
4760 RequestTcpFrontend, RequestUdpFrontend, RulePosition, UdpListenerConfig,
4761 UpdateUdpListenerConfig,
4762 };
4763
4764 fn capture_test_logs(run: impl FnOnce() + Send + 'static) -> String {
4765 let receiver = std::net::UdpSocket::bind("127.0.0.1:0")
4766 .expect("test log receiver must bind to a loopback port");
4767 let target = format!(
4768 "udp://{}",
4769 receiver
4770 .local_addr()
4771 .expect("test log receiver must have a local address")
4772 );
4773
4774 std::thread::spawn(move || {
4775 crate::logging::Logger::init(
4776 "state-log-redaction-test".to_owned(),
4777 "info",
4778 &target,
4779 false,
4780 None,
4781 None,
4782 None,
4783 )
4784 .expect("test logger must initialize");
4785 run();
4786 })
4787 .join()
4788 .expect("log-producing test thread must not panic");
4789
4790 receiver
4791 .set_nonblocking(true)
4792 .expect("test log receiver must become nonblocking");
4793 let mut output = String::new();
4794 let mut datagram = vec![0; 65_507];
4795 loop {
4796 match receiver.recv(&mut datagram) {
4797 Ok(length) => output.push_str(&String::from_utf8_lossy(&datagram[..length])),
4798 Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => break,
4799 Err(error) => panic!("test log receiver failed: {error}"),
4800 }
4801 }
4802 assert!(!output.is_empty(), "test log capture received no datagrams");
4803 output
4804 }
4805
4806 #[test]
4807 fn duplicate_certificate_log_redacts_names() {
4808 const NAME_SECRET: &str = "DUPLICATE_CERTIFICATE_NAME_SECRET_SENTINEL";
4809
4810 let name = format!("{NAME_SECRET}{}", "x".repeat(4096));
4811 let name_len = name.len();
4812 let certificate = CertificateAndKey {
4813 certificate: include_str!("../assets/certificate.pem").to_owned(),
4814 key: include_str!("../assets/key.pem").to_owned(),
4815 certificate_chain: Vec::new(),
4816 versions: Vec::new(),
4817 names: vec![name],
4818 };
4819 let fingerprint = certificate
4820 .fingerprint()
4821 .expect("test certificate fingerprint must be computable")
4822 .to_string();
4823 let output = capture_test_logs(move || {
4824 let mut state = ConfigState::default();
4825 let add = AddCertificate {
4826 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
4827 certificate,
4828 expired_at: None,
4829 };
4830
4831 state
4832 .add_certificate(&add)
4833 .expect("first certificate insertion must succeed");
4834 state
4835 .add_certificate(&add)
4836 .expect("duplicate certificate insertion must be skipped");
4837 });
4838
4839 assert!(
4840 !output.contains(NAME_SECRET),
4841 "duplicate certificate log leaked certificate name {NAME_SECRET}"
4842 );
4843 assert!(
4844 !output.contains(&fingerprint),
4845 "duplicate certificate log leaked certificate fingerprint {fingerprint}"
4846 );
4847 for metadata in [
4848 "fingerprint_bytes=32".to_owned(),
4849 "names_count=1".to_owned(),
4850 format!("names_bytes={name_len}"),
4851 ] {
4852 assert!(
4853 output.contains(&metadata),
4854 "duplicate certificate log omitted bounded metadata {metadata}: {output}"
4855 );
4856 }
4857 assert!(
4858 output.len() <= 512,
4859 "duplicate certificate log is not bounded: {} bytes",
4860 output.len()
4861 );
4862 }
4863
4864 #[test]
4875 fn replace_certificate_with_malformed_pem_keeps_old_certificate() {
4876 let certificate = CertificateAndKey {
4877 certificate: include_str!("../assets/certificate.pem").to_owned(),
4878 key: include_str!("../assets/key.pem").to_owned(),
4879 certificate_chain: Vec::new(),
4880 versions: Vec::new(),
4881 names: Vec::new(),
4882 };
4883 let old_fingerprint = certificate
4884 .fingerprint()
4885 .expect("test certificate fingerprint must be computable");
4886 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
4887
4888 let mut state = ConfigState::default();
4889 state
4890 .add_certificate(&AddCertificate {
4891 address,
4892 certificate,
4893 expired_at: None,
4894 })
4895 .expect("initial certificate insertion must succeed");
4896
4897 let malformed = CertificateAndKey {
4898 certificate: "not a valid pem certificate".to_owned(),
4899 key: include_str!("../assets/key.pem").to_owned(),
4900 certificate_chain: Vec::new(),
4901 versions: Vec::new(),
4902 names: Vec::new(),
4903 };
4904 let error = state
4905 .replace_certificate(&ReplaceCertificate {
4906 address,
4907 new_certificate: malformed,
4908 old_fingerprint: old_fingerprint.to_string(),
4909 new_expired_at: None,
4910 })
4911 .expect_err("replacing with a malformed PEM must fail");
4912 assert!(
4913 matches!(error, StateError::ReplaceCertificate(_)),
4914 "expected StateError::ReplaceCertificate, got {error:?}"
4915 );
4916
4917 let certificates_for_address = state
4918 .certificates
4919 .get(&SocketAddr::from(address))
4920 .expect("the address must still be present in the certificate map");
4921 assert!(
4922 certificates_for_address.contains_key(&old_fingerprint),
4923 "a failed replace must leave the original certificate in place, found {:?}",
4924 certificates_for_address.keys().collect::<Vec<_>>()
4925 );
4926 assert_eq!(
4927 certificates_for_address.len(),
4928 1,
4929 "a failed replace must not leave the address with zero (or extra) certificates"
4930 );
4931 }
4932
4933 #[test]
4940 fn replace_certificate_with_same_fingerprint_keeps_certificate() {
4941 let certificate = CertificateAndKey {
4942 certificate: include_str!("../assets/certificate.pem").to_owned(),
4943 key: include_str!("../assets/key.pem").to_owned(),
4944 certificate_chain: Vec::new(),
4945 versions: Vec::new(),
4946 names: Vec::new(),
4947 };
4948 let fingerprint = certificate
4949 .fingerprint()
4950 .expect("test certificate fingerprint must be computable");
4951 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
4952
4953 let mut state = ConfigState::default();
4954 state
4955 .add_certificate(&AddCertificate {
4956 address,
4957 certificate: certificate.clone(),
4958 expired_at: None,
4959 })
4960 .expect("initial certificate insertion must succeed");
4961
4962 state
4963 .replace_certificate(&ReplaceCertificate {
4964 address,
4965 new_certificate: certificate,
4966 old_fingerprint: fingerprint.to_string(),
4967 new_expired_at: None,
4968 })
4969 .expect("idempotent replace with the identical certificate must succeed");
4970
4971 let certificates_for_address = state
4972 .certificates
4973 .get(&SocketAddr::from(address))
4974 .expect("the address must still be present in the certificate map");
4975 assert!(
4976 certificates_for_address.contains_key(&fingerprint),
4977 "an idempotent replace (old fingerprint == new fingerprint) must not delete \
4978 the entry the caller meant to retain, found {:?}",
4979 certificates_for_address.keys().collect::<Vec<_>>()
4980 );
4981 assert_eq!(
4982 certificates_for_address.len(),
4983 1,
4984 "an idempotent replace must not change the number of stored certificates"
4985 );
4986 }
4987
4988 #[test]
4993 fn replace_certificate_with_empty_names_round_trips_through_saved_state() {
4994 let certificate = CertificateAndKey {
4995 certificate: include_str!("../assets/certificate.pem").to_owned(),
4996 key: include_str!("../assets/key.pem").to_owned(),
4997 certificate_chain: Vec::new(),
4998 versions: Vec::new(),
4999 names: Vec::new(),
5000 };
5001 let fingerprint = certificate
5002 .fingerprint()
5003 .expect("test certificate fingerprint must be computable");
5004 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5005 let mut state = ConfigState::default();
5006 state
5007 .add_certificate(&AddCertificate {
5008 address,
5009 certificate: certificate.to_owned(),
5010 expired_at: None,
5011 })
5012 .expect("initial certificate insertion must succeed");
5013 assert!(
5014 !state.certificates[&SocketAddr::from(address)][&fingerprint]
5015 .names
5016 .is_empty(),
5017 "the fixture must expose at least one SAN or CN"
5018 );
5019
5020 state
5021 .replace_certificate(&ReplaceCertificate {
5022 address,
5023 new_certificate: certificate,
5024 old_fingerprint: fingerprint.to_string(),
5025 new_expired_at: None,
5026 })
5027 .expect("same-fingerprint replacement must succeed");
5028
5029 let requests = state.generate_requests();
5030 let mut replayed = ConfigState::default();
5031 for request in &requests {
5032 replayed
5033 .dispatch(request)
5034 .unwrap_or_else(|error| panic!("could not replay {request:?}: {error}"));
5035 }
5036 assert_eq!(
5037 replayed.certificates, state.certificates,
5038 "saved-state replay must preserve canonical certificate names"
5039 );
5040 }
5041
5042 fn ordered_saved_certificates(state: &ConfigState) -> Vec<(Fingerprint, Option<i64>)> {
5043 state
5044 .generate_requests()
5045 .into_iter()
5046 .filter_map(|request| match request.request_type {
5047 Some(RequestType::AddCertificate(add)) => Some((
5048 add.certificate
5049 .fingerprint()
5050 .expect("saved certificate fingerprint"),
5051 add.expired_at,
5052 )),
5053 _ => None,
5054 })
5055 .collect()
5056 }
5057
5058 fn distinct_test_certificates() -> (CertificateAndKey, CertificateAndKey) {
5059 (
5060 CertificateAndKey {
5061 certificate: include_str!("../../lib/assets/tests/certificate-1y.pem").to_owned(),
5062 key: include_str!("../../lib/assets/tests/key-1y.pem").to_owned(),
5063 names: vec!["shared.example.test".to_owned()],
5064 ..Default::default()
5065 },
5066 CertificateAndKey {
5067 certificate: include_str!("../../lib/assets/tests/certificate-2y.pem").to_owned(),
5068 key: include_str!("../../lib/assets/tests/key-2y.pem").to_owned(),
5069 names: vec!["shared.example.test".to_owned()],
5070 ..Default::default()
5071 },
5072 )
5073 }
5074
5075 fn third_test_certificate() -> CertificateAndKey {
5076 CertificateAndKey {
5077 certificate: include_str!("../../lib/assets/tests/certificate-3.pem").to_owned(),
5078 key: include_str!("../../lib/assets/tests/key.pem").to_owned(),
5079 names: vec!["shared.example.test".to_owned()],
5080 ..Default::default()
5081 }
5082 }
5083
5084 fn ordered_fingerprints(state: &ConfigState, address: SocketAddress) -> Vec<Fingerprint> {
5085 state
5086 .ordered_certificate_entries(address.into())
5087 .into_iter()
5088 .map(|(fingerprint, _, _)| fingerprint)
5089 .collect()
5090 }
5091
5092 #[test]
5097 fn saved_certificate_requests_preserve_expiration_and_insertion_order() {
5098 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5099 let (certificate_a, certificate_b) = distinct_test_certificates();
5100 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5101 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5102 let mut state = ConfigState::new();
5103 for (certificate, expired_at) in [
5104 (certificate_a, Some(2_100_000_000)),
5105 (certificate_b, Some(2_200_000_000)),
5106 ] {
5107 state
5108 .dispatch(
5109 &RequestType::AddCertificate(AddCertificate {
5110 address,
5111 certificate,
5112 expired_at,
5113 })
5114 .into(),
5115 )
5116 .expect("add certificate with expiration override");
5117 }
5118
5119 assert_eq!(
5120 ordered_saved_certificates(&state),
5121 vec![
5122 (fingerprint_a, Some(2_100_000_000)),
5123 (fingerprint_b, Some(2_200_000_000)),
5124 ],
5125 "saved-state requests must preserve certificate order and expiration overrides"
5126 );
5127
5128 use std::io::{Read, Seek, SeekFrom};
5129
5130 let mut file = tempfile::tempfile().expect("create SaveState file");
5131 state
5132 .write_requests_to_file(&mut file)
5133 .expect("serialize SaveState request stream");
5134 file.seek(SeekFrom::Start(0))
5135 .expect("rewind SaveState file");
5136 let mut bytes = Vec::new();
5137 file.read_to_end(&mut bytes).expect("read SaveState file");
5138 let (remaining, requests) = crate::parser::parse_several_requests::<WorkerRequest>(&bytes)
5139 .expect("parse LoadState request stream");
5140 assert!(remaining.is_empty(), "LoadState parser left trailing bytes");
5141 let mut replayed = ConfigState::new();
5142 for request in requests {
5143 replayed
5144 .dispatch(&request.content)
5145 .expect("LoadState request dispatch");
5146 }
5147 assert_eq!(
5148 replayed, state,
5149 "SaveState file parsing and LoadState dispatch must preserve all certificate metadata"
5150 );
5151 }
5152
5153 #[test]
5158 fn certificate_diff_replays_same_fingerprint_payload_and_expiration_changes() {
5159 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5160 let (mut certificate, _) = distinct_test_certificates();
5161 certificate.names = vec!["before.example.test".to_owned()];
5162 let fingerprint = certificate.fingerprint().expect("fingerprint certificate");
5163 let mut before = ConfigState::new();
5164 before
5165 .dispatch(
5166 &RequestType::AddCertificate(AddCertificate {
5167 address,
5168 certificate: certificate.to_owned(),
5169 expired_at: Some(2_100_000_000),
5170 })
5171 .into(),
5172 )
5173 .expect("add original certificate");
5174 let mut after = before.to_owned();
5175 certificate.names = vec!["after.example.test".to_owned()];
5176 after
5177 .dispatch(
5178 &RequestType::ReplaceCertificate(ReplaceCertificate {
5179 address,
5180 new_certificate: certificate,
5181 old_fingerprint: fingerprint.to_string(),
5182 new_expired_at: Some(2_200_000_000),
5183 })
5184 .into(),
5185 )
5186 .expect("replace same-fingerprint certificate payload");
5187
5188 let diff = before.diff(&after);
5189 assert!(
5190 diff.iter().any(|request| matches!(
5191 request.request_type,
5192 Some(RequestType::ReplaceCertificate(_))
5193 )),
5194 "same-fingerprint payload/expiration changes must emit ReplaceCertificate: {diff:?}"
5195 );
5196 }
5197
5198 #[test]
5202 fn certificate_diff_replays_order_only_changes() {
5203 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5204 let (certificate_a, certificate_b) = distinct_test_certificates();
5205 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5206 let mut before = ConfigState::new();
5207 let mut after = ConfigState::new();
5208 for certificate in [certificate_a.to_owned(), certificate_b.to_owned()] {
5209 before
5210 .dispatch(
5211 &RequestType::AddCertificate(AddCertificate {
5212 address,
5213 certificate,
5214 expired_at: Some(2_100_000_000),
5215 })
5216 .into(),
5217 )
5218 .expect("add before certificate");
5219 }
5220 for certificate in [certificate_b, certificate_a] {
5221 after
5222 .dispatch(
5223 &RequestType::AddCertificate(AddCertificate {
5224 address,
5225 certificate,
5226 expired_at: Some(2_100_000_000),
5227 })
5228 .into(),
5229 )
5230 .expect("add after certificate");
5231 }
5232
5233 let diff = before.diff(&after);
5234 assert_eq!(
5235 diff.len(),
5236 2,
5237 "A,B -> B,A needs only Remove(A), Add(A), not churn for B: {diff:?}"
5238 );
5239 assert!(
5240 matches!(
5241 &diff[0].request_type,
5242 Some(RequestType::RemoveCertificate(remove))
5243 if remove.fingerprint == fingerprint_a.to_string()
5244 ) && matches!(
5245 &diff[1].request_type,
5246 Some(RequestType::AddCertificate(add))
5247 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_a)
5248 ),
5249 "the minimal order-only sequence must move only certificate A to the end: {diff:?}"
5250 );
5251 }
5252
5253 #[test]
5254 fn certificate_diff_rotates_three_entries_by_moving_only_the_suffix() {
5255 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5256 let (certificate_a, certificate_b) = distinct_test_certificates();
5257 let certificate_c = third_test_certificate();
5258 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5259 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5260 let fingerprint_c = certificate_c.fingerprint().expect("fingerprint C");
5261 let build = |certificates: Vec<CertificateAndKey>| {
5262 let mut state = ConfigState::new();
5263 for certificate in certificates {
5264 state
5265 .dispatch(
5266 &RequestType::AddCertificate(AddCertificate {
5267 address,
5268 certificate,
5269 expired_at: Some(2_100_000_000),
5270 })
5271 .into(),
5272 )
5273 .expect("add certificate");
5274 }
5275 state
5276 };
5277 let before = build(vec![
5278 certificate_a.to_owned(),
5279 certificate_b.to_owned(),
5280 certificate_c.to_owned(),
5281 ]);
5282 let after = build(vec![certificate_b, certificate_c, certificate_a]);
5283
5284 let diff = before.diff(&after);
5285 assert_eq!(
5286 diff.len(),
5287 2,
5288 "A,B,C -> B,C,A needs only Remove(A), Add(A): {diff:?}"
5289 );
5290 assert!(
5291 matches!(
5292 &diff[0].request_type,
5293 Some(RequestType::RemoveCertificate(remove))
5294 if remove.fingerprint == fingerprint_a.to_string()
5295 ) && matches!(
5296 &diff[1].request_type,
5297 Some(RequestType::AddCertificate(add))
5298 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_a)
5299 ),
5300 "the rotation must leave B and C in place: {diff:?}"
5301 );
5302 assert!(
5303 diff.iter().all(|request| !matches!(
5304 &request.request_type,
5305 Some(RequestType::RemoveCertificate(remove))
5306 if remove.fingerprint == fingerprint_b.to_string()
5307 || remove.fingerprint == fingerprint_c.to_string()
5308 )),
5309 "B and C are the retained target prefix and must not be removed: {diff:?}"
5310 );
5311
5312 let mut interrupted = before.to_owned();
5313 interrupted
5314 .dispatch(&diff[0])
5315 .expect("apply the minimal sequence's remove boundary");
5316 assert_ne!(
5317 ordered_fingerprints(&interrupted, address),
5318 ordered_fingerprints(&after, address),
5319 "the interruption point must expose a real partial state"
5320 );
5321 for request in interrupted.diff(&after) {
5322 interrupted
5323 .dispatch(&request)
5324 .expect("fresh diff recovers the interrupted rotation");
5325 }
5326 assert_eq!(
5327 ordered_fingerprints(&interrupted, address),
5328 vec![fingerprint_b, fingerprint_c, fingerprint_a],
5329 "recovery must converge on the target order and its final winner"
5330 );
5331 }
5332
5333 #[test]
5334 fn certificate_diff_inserts_around_a_retained_entry_without_readding_new_suffix() {
5335 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5336 let (certificate_a, certificate_b) = distinct_test_certificates();
5337 let certificate_c = third_test_certificate();
5338 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5339 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5340 let fingerprint_c = certificate_c.fingerprint().expect("fingerprint C");
5341 let build = |certificates: Vec<CertificateAndKey>| {
5342 let mut state = ConfigState::new();
5343 for certificate in certificates {
5344 state
5345 .dispatch(
5346 &RequestType::AddCertificate(AddCertificate {
5347 address,
5348 certificate,
5349 expired_at: Some(2_100_000_000),
5350 })
5351 .into(),
5352 )
5353 .expect("add certificate");
5354 }
5355 state
5356 };
5357 let before = build(vec![certificate_b.to_owned()]);
5358 let after = build(vec![certificate_a, certificate_b, certificate_c]);
5359
5360 let diff = before.diff(&after);
5361 assert_eq!(
5362 diff.len(),
5363 4,
5364 "B -> A,B,C needs Add(A), Remove(B), Add(B), Add(C): {diff:?}"
5365 );
5366 assert!(
5367 matches!(
5368 &diff[0].request_type,
5369 Some(RequestType::AddCertificate(add))
5370 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_a)
5371 ) && matches!(
5372 &diff[1].request_type,
5373 Some(RequestType::RemoveCertificate(remove))
5374 if remove.fingerprint == fingerprint_b.to_string()
5375 ) && matches!(
5376 &diff[2].request_type,
5377 Some(RequestType::AddCertificate(add))
5378 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_b)
5379 ) && matches!(
5380 &diff[3].request_type,
5381 Some(RequestType::AddCertificate(add))
5382 if add.certificate.fingerprint().ok().as_ref() == Some(&fingerprint_c)
5383 ),
5384 "new certificate C must never be removed after insertion: {diff:?}"
5385 );
5386 assert!(
5387 diff.iter().all(|request| !matches!(
5388 &request.request_type,
5389 Some(RequestType::RemoveCertificate(remove))
5390 if remove.fingerprint == fingerprint_c.to_string()
5391 )),
5392 "the missing target suffix C is added once and never churned: {diff:?}"
5393 );
5394
5395 let mut interrupted = before.to_owned();
5396 interrupted
5397 .dispatch(&diff[0])
5398 .expect("apply the inserted target prefix before interruption");
5399 assert_ne!(
5400 ordered_fingerprints(&interrupted, address),
5401 ordered_fingerprints(&after, address),
5402 "the prefix interruption must expose a real partial state"
5403 );
5404 for request in interrupted.diff(&after) {
5405 interrupted
5406 .dispatch(&request)
5407 .expect("fresh diff recovers the interrupted insertion sequence");
5408 }
5409 assert_eq!(
5410 ordered_fingerprints(&interrupted, address),
5411 vec![fingerprint_a, fingerprint_b, fingerprint_c],
5412 "recovery must converge on the target order and final winner"
5413 );
5414 }
5415
5416 #[test]
5417 fn duplicate_certificate_add_keeps_payload_expiration_and_order() {
5418 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5419 let (certificate, _) = distinct_test_certificates();
5420 let mut state = ConfigState::new();
5421 state
5422 .add_certificate(&AddCertificate {
5423 address,
5424 certificate: certificate.to_owned(),
5425 expired_at: Some(2_100_000_000),
5426 })
5427 .expect("initial certificate");
5428 let before = state.to_owned();
5429 let mut differing_retry = certificate;
5430 differing_retry.names = vec!["different.example.test".to_owned()];
5431
5432 state
5433 .add_certificate(&AddCertificate {
5434 address,
5435 certificate: differing_retry,
5436 expired_at: Some(2_200_000_000),
5437 })
5438 .expect("duplicate fingerprint stays an accepted no-op");
5439
5440 assert_eq!(
5441 state, before,
5442 "duplicate AddCertificate must be a total no-op"
5443 );
5444 }
5445
5446 #[test]
5447 fn replacing_with_an_existing_identical_target_preserves_its_ordinal() {
5448 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5449 let (certificate_a, certificate_b) = distinct_test_certificates();
5450 let certificate_c = third_test_certificate();
5451 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5452 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5453 let fingerprint_c = certificate_c.fingerprint().expect("fingerprint C");
5454 let mut state = ConfigState::new();
5455 for certificate in [certificate_a, certificate_b.to_owned(), certificate_c] {
5456 state
5457 .add_certificate(&AddCertificate {
5458 address,
5459 certificate,
5460 expired_at: Some(2_100_000_000),
5461 })
5462 .expect("initial certificate");
5463 }
5464
5465 state
5466 .replace_certificate(&ReplaceCertificate {
5467 address,
5468 new_certificate: certificate_b,
5469 old_fingerprint: fingerprint_a.to_string(),
5470 new_expired_at: Some(2_100_000_000),
5471 })
5472 .expect("replace A with already-stored B");
5473
5474 assert_eq!(
5475 ordered_fingerprints(&state, address),
5476 vec![fingerprint_b, fingerprint_c],
5477 "an already-stored replacement keeps B in place while removing A"
5478 );
5479 }
5480
5481 #[test]
5482 fn legacy_metadata_normalizes_once_and_the_second_replay_is_stable() {
5483 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5484 let (certificate_a, certificate_b) = distinct_test_certificates();
5485 let mut current = ConfigState::new();
5486 for (certificate, expired_at) in [
5487 (certificate_a, Some(2_100_000_000)),
5488 (certificate_b, Some(2_200_000_000)),
5489 ] {
5490 current
5491 .add_certificate(&AddCertificate {
5492 address,
5493 certificate,
5494 expired_at,
5495 })
5496 .expect("current certificate");
5497 }
5498 let mut legacy_json = serde_json::to_value(¤t).expect("serialize current state");
5499 legacy_json
5500 .as_object_mut()
5501 .expect("ConfigState JSON object")
5502 .remove("certificate_metadata")
5503 .expect("current state writes certificate metadata");
5504 let legacy: ConfigState =
5505 serde_json::from_value(legacy_json).expect("legacy state without sidecar");
5506 assert!(legacy.certificate_metadata.is_empty());
5507
5508 let mut first_replay = ConfigState::new();
5509 for request in legacy.generate_requests() {
5510 first_replay
5511 .dispatch(&request)
5512 .expect("legacy request stream replays");
5513 }
5514 assert_eq!(
5515 first_replay.certificate_metadata,
5516 legacy.normalized_certificate_metadata(),
5517 "the first replay records deterministic fallback ordinals"
5518 );
5519 assert_ne!(
5520 first_replay, legacy,
5521 "normalization is semantic state and must not be hidden from PartialEq"
5522 );
5523
5524 let mut second_replay = ConfigState::new();
5525 for request in first_replay.generate_requests() {
5526 second_replay
5527 .dispatch(&request)
5528 .expect("normalized request stream replays");
5529 }
5530 assert_eq!(
5531 second_replay, first_replay,
5532 "a second replay of normalized metadata must be byte/PartialEq stable"
5533 );
5534 assert_eq!(
5535 serde_json::to_value(&second_replay).expect("serialize second replay"),
5536 serde_json::to_value(&first_replay).expect("serialize first replay"),
5537 "the derived next-order cache must not affect serialized state"
5538 );
5539 }
5540
5541 fn legacy_state_with_certificates() -> (ConfigState, SocketAddress) {
5545 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5546 let (certificate_a, certificate_b) = distinct_test_certificates();
5547 let mut current = ConfigState::new();
5548 current
5549 .dispatch(&RequestType::AddHttpsListener(make_https_listener(address)).into())
5550 .expect("https listener");
5551 for cluster_id in ["cluster_1", "cluster_2"] {
5552 current
5553 .dispatch(
5554 &RequestType::AddCluster(Cluster {
5555 cluster_id: cluster_id.to_owned(),
5556 ..Default::default()
5557 })
5558 .into(),
5559 )
5560 .expect("cluster");
5561 current
5562 .dispatch(
5563 &RequestType::AddBackend(AddBackend {
5564 cluster_id: cluster_id.to_owned(),
5565 backend_id: format!("{cluster_id}-0"),
5566 address: SocketAddress::new_v4(127, 0, 0, 1, 9000),
5567 ..Default::default()
5568 })
5569 .into(),
5570 )
5571 .expect("backend");
5572 current
5573 .dispatch(
5574 &RequestType::AddHttpsFrontend(RequestHttpFrontend {
5575 cluster_id: Some(cluster_id.to_owned()),
5576 hostname: format!("{cluster_id}.example.test"),
5577 path: PathRule::prefix(String::from("/")),
5578 address,
5579 position: RulePosition::Tree.into(),
5580 ..Default::default()
5581 })
5582 .into(),
5583 )
5584 .expect("frontend");
5585 }
5586 for (certificate, expired_at) in [
5587 (certificate_a, Some(2_100_000_000)),
5588 (certificate_b, Some(2_200_000_000)),
5589 ] {
5590 current
5591 .add_certificate(&AddCertificate {
5592 address,
5593 certificate,
5594 expired_at,
5595 })
5596 .expect("certificate");
5597 }
5598 let mut json = serde_json::to_value(¤t).expect("serialize current state");
5599 json.as_object_mut()
5600 .expect("ConfigState JSON object")
5601 .remove("certificate_metadata")
5602 .expect("current state writes certificate metadata");
5603 let legacy: ConfigState = serde_json::from_value(json).expect("legacy state");
5604 assert!(legacy.certificate_metadata.is_empty());
5605 (legacy, address)
5606 }
5607
5608 fn replay_diff(from: &ConfigState, requests: &[Request]) -> ConfigState {
5609 let mut replayed = from.clone();
5610 for request in requests {
5611 replayed.dispatch(request).expect("diff request replays");
5612 }
5613 replayed
5614 }
5615
5616 #[test]
5617 fn diff_of_legacy_state_without_metadata_tolerates_a_listener_change() {
5618 let (legacy, address) = legacy_state_with_certificates();
5619 let mut desired = legacy.clone();
5620 desired
5621 .https_listeners
5622 .get_mut(&ListenerKey::from(address))
5623 .expect("listener")
5624 .front_timeout = 61;
5625
5626 let requests = legacy.diff(&desired);
5627
5628 assert!(!requests.is_empty(), "a listener change emits requests");
5629 assert!(
5630 requests.iter().all(|request| !matches!(
5631 request.request_type,
5632 Some(
5633 RequestType::AddCertificate(_)
5634 | RequestType::RemoveCertificate(_)
5635 | RequestType::ReplaceCertificate(_)
5636 )
5637 )),
5638 "retained certificates are not re-sent"
5639 );
5640 let replayed = replay_diff(&legacy, &requests);
5641 assert_eq!(replayed.https_listeners, desired.https_listeners);
5642 assert_eq!(replayed.certificates, desired.certificates);
5643 assert_eq!(replayed.https_fronts, legacy.https_fronts);
5644 assert_eq!(replayed.backends, legacy.backends);
5645 assert_eq!(
5646 replayed.normalized_certificate_metadata(),
5647 desired.normalized_certificate_metadata()
5648 );
5649 }
5650
5651 #[test]
5652 fn diff_of_legacy_state_without_metadata_tolerates_unrelated_removals() {
5653 let (legacy, address) = legacy_state_with_certificates();
5654 let mut desired = legacy.clone();
5655 desired
5656 .dispatch(
5657 &RequestType::RemoveBackend(RemoveBackend {
5658 cluster_id: "cluster_2".to_owned(),
5659 backend_id: "cluster_2-0".to_owned(),
5660 address: SocketAddress::new_v4(127, 0, 0, 1, 9000),
5661 })
5662 .into(),
5663 )
5664 .expect("remove backend");
5665 desired
5666 .dispatch(
5667 &RequestType::RemoveHttpsFrontend(RequestHttpFrontend {
5668 cluster_id: Some("cluster_2".to_owned()),
5669 hostname: "cluster_2.example.test".to_owned(),
5670 path: PathRule::prefix(String::from("/")),
5671 address,
5672 position: RulePosition::Tree.into(),
5673 ..Default::default()
5674 })
5675 .into(),
5676 )
5677 .expect("remove frontend");
5678
5679 let requests = legacy.diff(&desired);
5680
5681 assert_eq!(requests.len(), 2, "exactly the two removals: {requests:?}");
5682 let replayed = replay_diff(&legacy, &requests);
5683 assert_eq!(replayed.certificates, legacy.certificates);
5684 assert_eq!(replayed.https_fronts, desired.https_fronts);
5685 assert_eq!(
5686 replayed.backends.get("cluster_1"),
5687 legacy.backends.get("cluster_1"),
5688 "the unaffected cluster keeps its backend"
5689 );
5690 assert!(
5691 replayed
5692 .backends
5693 .get("cluster_2")
5694 .is_none_or(|backends| backends.is_empty())
5695 );
5696 assert_eq!(
5697 replayed.normalized_certificate_metadata(),
5698 desired.normalized_certificate_metadata()
5699 );
5700 }
5701
5702 #[test]
5706 fn diff_still_reports_explicit_metadata_differences() {
5707 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5708 let (certificate_a, certificate_b) = distinct_test_certificates();
5709 let build = |first: (&CertificateAndKey, u64), second: (&CertificateAndKey, u64)| {
5710 let mut state = ConfigState::new();
5711 for (certificate, expired_at) in [first, second] {
5712 state
5713 .add_certificate(&AddCertificate {
5714 address,
5715 certificate: certificate.clone(),
5716 expired_at: Some(expired_at as i64),
5717 })
5718 .expect("certificate");
5719 }
5720 state
5721 };
5722 let base = build(
5723 (&certificate_a, 2_100_000_000),
5724 (&certificate_b, 2_200_000_000),
5725 );
5726 let other_expiry = build(
5727 (&certificate_a, 2_100_000_000),
5728 (&certificate_b, 2_300_000_000),
5729 );
5730 let other_order = build(
5731 (&certificate_b, 2_200_000_000),
5732 (&certificate_a, 2_100_000_000),
5733 );
5734
5735 assert_ne!(
5736 base.normalized_certificate_metadata(),
5737 other_expiry.normalized_certificate_metadata(),
5738 "expired_at survives normalization"
5739 );
5740 assert_ne!(
5741 base.normalized_certificate_metadata(),
5742 other_order.normalized_certificate_metadata(),
5743 "insertion order survives normalization"
5744 );
5745 for other in [&other_expiry, &other_order] {
5746 let requests = base.diff(other);
5747 assert!(
5748 requests.iter().any(|request| matches!(
5749 request.request_type,
5750 Some(
5751 RequestType::AddCertificate(_)
5752 | RequestType::RemoveCertificate(_)
5753 | RequestType::ReplaceCertificate(_)
5754 )
5755 )),
5756 "an explicit metadata difference yields certificate operations"
5757 );
5758 let replayed = replay_diff(&base, &requests);
5759 assert_eq!(
5760 replayed.normalized_certificate_metadata(),
5761 other.normalized_certificate_metadata()
5762 );
5763 }
5764 }
5765
5766 #[test]
5767 fn legacy_reader_ignores_current_certificate_metadata_but_keeps_material() {
5768 #[derive(serde::Deserialize)]
5769 struct LegacyConfigStateMirror {
5770 certificates: HashMap<SocketAddr, HashMap<Fingerprint, CertificateAndKey>>,
5771 }
5772
5773 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5774 let socket_address: SocketAddr = address.into();
5775 let (certificate, _) = distinct_test_certificates();
5776 let fingerprint = certificate.fingerprint().expect("fingerprint");
5777 let mut current = ConfigState::new();
5778 current
5779 .add_certificate(&AddCertificate {
5780 address,
5781 certificate: certificate.to_owned(),
5782 expired_at: Some(2_100_000_000),
5783 })
5784 .expect("current certificate");
5785
5786 let legacy: LegacyConfigStateMirror = serde_json::from_value(
5787 serde_json::to_value(¤t).expect("serialize current ConfigState"),
5788 )
5789 .expect("an old serde reader must ignore the unknown metadata field");
5790
5791 assert_eq!(
5792 legacy.certificates[&socket_address][&fingerprint], certificate,
5793 "new-to-old JSON remains syntactically readable, with metadata intentionally lost"
5794 );
5795 }
5796
5797 #[test]
5798 fn malformed_legacy_metadata_is_unchanged_on_refusal_then_normalized_on_success() {
5799 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5800 let socket_address: SocketAddr = address.into();
5801 let (certificate_a, certificate_b) = distinct_test_certificates();
5802 let fingerprint_a = certificate_a.fingerprint().expect("fingerprint A");
5803 let fingerprint_b = certificate_b.fingerprint().expect("fingerprint B");
5804 let mut state = ConfigState::new();
5805 state.certificates.insert(
5806 socket_address,
5807 HashMap::from([(fingerprint_a.to_owned(), certificate_a)]),
5808 );
5809 state.certificate_metadata.insert(
5810 socket_address,
5811 CertificateMetadataIndex {
5812 entries: HashMap::from([
5813 (
5814 fingerprint_a,
5815 CertificateMetadata {
5816 expired_at: Some(2_100_000_000),
5817 insertion_order: 7,
5818 },
5819 ),
5820 (
5821 Fingerprint(vec![0xff]),
5822 CertificateMetadata {
5823 expired_at: Some(9),
5824 insertion_order: 7,
5825 },
5826 ),
5827 ]),
5828 next_order: None,
5829 },
5830 );
5831 let before = state.to_owned();
5832
5833 let error = state.add_certificate(&AddCertificate {
5834 address,
5835 certificate: CertificateAndKey {
5836 certificate: "not PEM".to_owned(),
5837 ..Default::default()
5838 },
5839 expired_at: None,
5840 });
5841 assert!(error.is_err(), "malformed certificate must be refused");
5842 assert_eq!(
5843 state, before,
5844 "a refusal must not normalize or mutate legacy state"
5845 );
5846
5847 state
5848 .add_certificate(&AddCertificate {
5849 address,
5850 certificate: certificate_b,
5851 expired_at: Some(2_200_000_000),
5852 })
5853 .expect("valid mutation normalizes legacy metadata");
5854 let metadata = &state.certificate_metadata[&socket_address].entries;
5855 assert_eq!(metadata.len(), 2, "orphan metadata must be pruned");
5856 assert_eq!(metadata[&fingerprint_b].insertion_order, 1);
5857 assert_eq!(
5858 metadata
5859 .values()
5860 .map(|entry| entry.insertion_order)
5861 .collect::<BTreeSet<_>>(),
5862 BTreeSet::from([0, 1]),
5863 "normalized ordinals must be unique and contiguous"
5864 );
5865 }
5866
5867 #[test]
5882 fn remove_certificate_evicts_the_address_when_its_last_certificate_is_removed() {
5883 let certificate = CertificateAndKey {
5884 certificate: include_str!("../assets/certificate.pem").to_owned(),
5885 key: include_str!("../assets/key.pem").to_owned(),
5886 certificate_chain: Vec::new(),
5887 versions: Vec::new(),
5888 names: Vec::new(),
5889 };
5890 let fingerprint = certificate
5891 .fingerprint()
5892 .expect("test certificate fingerprint must be computable");
5893 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5894
5895 let mut state = ConfigState::default();
5896 state
5897 .add_certificate(&AddCertificate {
5898 address,
5899 certificate,
5900 expired_at: None,
5901 })
5902 .expect("initial certificate insertion must succeed");
5903
5904 state
5905 .remove_certificate(&RemoveCertificate {
5906 address,
5907 fingerprint: fingerprint.to_string(),
5908 })
5909 .expect("removing the only certificate must succeed");
5910
5911 assert!(
5912 !state.certificates.contains_key(&SocketAddr::from(address)),
5913 "removing an address's last certificate must evict the address key too, \
5914 found {:?}",
5915 state.certificates.get(&SocketAddr::from(address))
5916 );
5917
5918 let _ = state.produce_initial_state();
5922 }
5923
5924 #[test]
5936 fn add_certificate_with_valid_pem_armor_but_invalid_x509_leaves_no_bucket() {
5937 let junk_pem = "-----BEGIN CERTIFICATE-----\n\
5940 dGhpcyBpcyBub3QgYSB2YWxpZCB4NTA5IGRlciBzdHJ1Y3R1cmUgYXQgYWxsLCBq\n\
5941 dXN0IGZpbGxlciBieXRlcyBwYWRkZWQgb3V0\n\
5942 -----END CERTIFICATE-----\n";
5943 let certificate = CertificateAndKey {
5944 certificate: junk_pem.to_owned(),
5945 key: include_str!("../assets/key.pem").to_owned(),
5946 certificate_chain: Vec::new(),
5947 versions: Vec::new(),
5948 names: Vec::new(),
5949 };
5950 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
5951
5952 let mut state = ConfigState::default();
5953 assert!(
5954 !state.certificates.contains_key(&SocketAddr::from(address)),
5955 "the address must be absent before the add attempt"
5956 );
5957
5958 let error = state
5959 .add_certificate(&AddCertificate {
5960 address,
5961 certificate,
5962 expired_at: None,
5963 })
5964 .expect_err("valid PEM armor around invalid X.509 content must be rejected");
5965 assert!(
5966 matches!(error, StateError::AddCertificate(_)),
5967 "expected StateError::AddCertificate, got {error:?}"
5968 );
5969
5970 assert!(
5971 !state.certificates.contains_key(&SocketAddr::from(address)),
5972 "a rejected add_certificate must leave no bucket for a previously-absent address, \
5973 found {:?}",
5974 state.certificates.get(&SocketAddr::from(address))
5975 );
5976 }
5977
5978 #[test]
5979 fn frontend_state_error_redacts_display_key() {
5980 const HOSTNAME_SECRET: &str = "STATE_ERROR_HOSTNAME_SECRET_SENTINEL";
5981 const PATH_SECRET: &str = "STATE_ERROR_PATH_SECRET_SENTINEL";
5982 const METHOD_SECRET: &str = "STATE_ERROR_METHOD_SECRET_SENTINEL";
5983
5984 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
5985 let front = RequestHttpFrontend {
5986 cluster_id: Some("cluster".to_owned()),
5987 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
5988 hostname: long_value(HOSTNAME_SECRET),
5989 path: PathRule {
5990 kind: PathRuleKind::Prefix as i32,
5991 value: long_value(PATH_SECRET),
5992 },
5993 method: Some(long_value(METHOD_SECRET)),
5994 position: RulePosition::Tree as i32,
5995 tags: BTreeMap::new(),
5996 redirect: None,
5997 redirect_scheme: None,
5998 redirect_template: None,
5999 rewrite_host: None,
6000 rewrite_path: None,
6001 rewrite_port: None,
6002 required_auth: None,
6003 headers: Vec::new(),
6004 hsts: None,
6005 header_predicates: Vec::new(),
6006 };
6007 let mut state = ConfigState::default();
6008 state
6009 .add_http_frontend(&front)
6010 .expect("first frontend insertion must succeed");
6011 assert!(
6012 state
6013 .http_fronts
6014 .keys()
6015 .next()
6016 .expect("raw state key must be retained")
6017 .contains(HOSTNAME_SECRET),
6018 "state storage must retain the raw operator-facing frontend key"
6019 );
6020
6021 let error = state
6022 .add_http_frontend(&front)
6023 .expect_err("duplicate frontend insertion must return StateError::Exists");
6024 let raw_frontend_key = front.to_string();
6025 match &error {
6026 StateError::Exists { id, .. } => assert_eq!(
6027 id, &raw_frontend_key,
6028 "StateError must preserve the public raw frontend identifier"
6029 ),
6030 other => panic!("expected StateError::Exists, got {other:?}"),
6031 }
6032 for (label, output) in [
6033 ("Display", error.to_string()),
6034 ("Debug", format!("{error:?}")),
6035 ] {
6036 for secret in [HOSTNAME_SECRET, PATH_SECRET, METHOD_SECRET] {
6037 assert!(
6038 !output.contains(secret),
6039 "StateError {label} leaked frontend marker {secret}"
6040 );
6041 }
6042 let metadata = format!("id_bytes={}", raw_frontend_key.len());
6043 assert!(
6044 output.contains(&metadata),
6045 "StateError {label} omitted bounded metadata {metadata}: {output}"
6046 );
6047 assert!(
6048 output.len() <= 512,
6049 "StateError {label} output is not bounded: {} bytes",
6050 output.len()
6051 );
6052 }
6053 }
6054
6055 #[test]
6056 fn state_error_formatting_bounds_every_string_bearing_variant() {
6057 const SECRET: &str = "STATE_ERROR_VARIANT_SECRET_SENTINEL";
6058
6059 let long_value = || format!("{SECRET}{}", "x".repeat(4096));
6060 let errors = [
6061 StateError::NotFound {
6062 kind: ObjectKind::Backend,
6063 id: long_value(),
6064 },
6065 StateError::Exists {
6066 kind: ObjectKind::Cluster,
6067 id: long_value(),
6068 },
6069 StateError::ClusterNotFound {
6070 kind: ObjectKind::Backend,
6071 cluster_id: long_value(),
6072 },
6073 StateError::AddCertificate(CertificateError::ParsePEMCertificate(long_value())),
6074 StateError::RemoveCertificate(long_value()),
6075 StateError::ReplaceCertificate(long_value()),
6076 StateError::FrontendConversion {
6077 frontend: long_value(),
6078 error: long_value(),
6079 },
6080 StateError::FileError(std::io::Error::other(long_value())),
6081 StateError::InvalidTcpFrontend {
6082 address: "127.0.0.1:443"
6083 .parse()
6084 .expect("test TCP frontend address must parse"),
6085 reason: long_value(),
6086 },
6087 StateError::UdpFrontendAddressTaken {
6088 address: "127.0.0.1:5353"
6089 .parse()
6090 .expect("test UDP frontend address must parse"),
6091 cluster_id: long_value().into(),
6092 current_cluster_id: long_value().into(),
6093 current_tags: BTreeMap::from([(long_value(), long_value())]),
6094 },
6095 ];
6096
6097 for error in errors {
6098 for (label, output) in [
6099 ("Display", error.to_string()),
6100 ("Debug", format!("{error:?}")),
6101 ] {
6102 assert!(
6103 !output.contains(SECRET),
6104 "StateError {label} leaked a string-bearing variant: {output}"
6105 );
6106 assert!(
6107 output.len() <= 512,
6108 "StateError {label} is not bounded: {} bytes",
6109 output.len()
6110 );
6111 }
6112 }
6113 }
6114
6115 #[test]
6116 fn tcp_frontend_not_found_error_retains_the_raw_identity() {
6117 const TCP_SECRET: &str = "STATE_ERROR_TCP_IDENTITY_SECRET_SENTINEL";
6118
6119 let long_value = |suffix: &str| format!("{TCP_SECRET}_{suffix}{}", "x".repeat(1024));
6120 let frontend = RequestTcpFrontend {
6121 cluster_id: long_value("cluster"),
6122 address: SocketAddress::new_v4(127, 0, 0, 1, 443),
6123 tags: BTreeMap::from([(long_value("tag-key"), long_value("tag-value"))]),
6124 sni: Some(long_value("sni")),
6125 alpn: vec![long_value("alpn")],
6126 };
6127 let error = ConfigState::default()
6128 .remove_tcp_frontend(&frontend)
6129 .expect_err("removing from a missing cluster must return StateError::NotFound");
6130
6131 match &error {
6132 StateError::NotFound { id, .. } => assert!(
6133 id.contains(TCP_SECRET),
6134 "StateError must retain the raw TCP frontend identity: {id}"
6135 ),
6136 other => panic!("expected StateError::NotFound, got {other:?}"),
6137 }
6138 for output in [error.to_string(), format!("{error:?}")] {
6139 assert!(
6140 !output.contains(TCP_SECRET),
6141 "StateError formatting leaked the retained TCP identity: {output}"
6142 );
6143 assert!(
6144 output.len() <= 512,
6145 "StateError formatting is not bounded: {} bytes",
6146 output.len()
6147 );
6148 }
6149 }
6150
6151 #[test]
6152 fn retained_state_debug_redacts_http_frontend_and_collection_data() {
6153 const CLUSTER_SECRET: &str = "RETAINED_CLUSTER_SECRET_SENTINEL";
6154 const HOSTNAME_SECRET: &str = "RETAINED_HOSTNAME_SECRET_SENTINEL";
6155 const PATH_SECRET: &str = "RETAINED_PATH_SECRET_SENTINEL";
6156 const METHOD_SECRET: &str = "RETAINED_METHOD_SECRET_SENTINEL";
6157 const TAG_KEY_SECRET: &str = "RETAINED_TAG_KEY_SECRET_SENTINEL";
6158 const TAG_VALUE_SECRET: &str = "RETAINED_TAG_VALUE_SECRET_SENTINEL";
6159 const REDIRECT_TEMPLATE_SECRET: &str = "RETAINED_REDIRECT_TEMPLATE_SECRET_SENTINEL";
6160 const REWRITE_HOST_SECRET: &str = "RETAINED_REWRITE_HOST_SECRET_SENTINEL";
6161 const REWRITE_PATH_SECRET: &str = "RETAINED_REWRITE_PATH_SECRET_SENTINEL";
6162 const HEADER_KEY_SECRET: &str = "RETAINED_HEADER_KEY_SECRET_SENTINEL";
6163 const HEADER_VALUE_SECRET: &str = "RETAINED_HEADER_VALUE_SECRET_SENTINEL";
6164 const REQUEST_COUNT_SECRET: &str = "RETAINED_REQUEST_COUNT_SECRET_SENTINEL";
6165
6166 let long_value = |marker: &str| format!("{marker}{}", "x".repeat(4096));
6167 let request = RequestHttpFrontend {
6168 cluster_id: Some(long_value(CLUSTER_SECRET)),
6169 address: SocketAddress::new_v4(127, 0, 0, 1, 8443),
6170 hostname: long_value(HOSTNAME_SECRET),
6171 path: PathRule {
6172 kind: PathRuleKind::Regex as i32,
6173 value: long_value(PATH_SECRET),
6174 },
6175 method: Some(long_value(METHOD_SECRET)),
6176 position: RulePosition::Tree as i32,
6177 tags: BTreeMap::from([(long_value(TAG_KEY_SECRET), long_value(TAG_VALUE_SECRET))]),
6178 redirect: Some(RedirectPolicy::PermanentRedirect as i32),
6179 redirect_scheme: Some(RedirectScheme::UseHttps as i32),
6180 redirect_template: Some(long_value(REDIRECT_TEMPLATE_SECRET)),
6181 rewrite_host: Some(long_value(REWRITE_HOST_SECRET)),
6182 rewrite_path: Some(long_value(REWRITE_PATH_SECRET)),
6183 rewrite_port: Some(9443),
6184 required_auth: Some(true),
6185 headers: vec![Header {
6186 position: HeaderPosition::Both as i32,
6187 key: long_value(HEADER_KEY_SECRET),
6188 val: long_value(HEADER_VALUE_SECRET),
6189 }],
6190 hsts: Some(HstsConfig {
6191 enabled: Some(true),
6192 max_age: Some(31_536_000),
6193 include_subdomains: Some(true),
6194 preload: Some(false),
6195 force_replace_backend: Some(true),
6196 }),
6197 header_predicates: Vec::new(),
6198 };
6199 let retained = request
6200 .clone()
6201 .to_frontend()
6202 .expect("adversarial frontend must convert");
6203 let retained_debug = format!("{retained:?}");
6204
6205 let mut state = ConfigState::default();
6206 state
6207 .dispatch(&RequestType::AddHttpFrontend(request).into())
6208 .expect("adversarial frontend must be retained");
6209 state
6210 .request_counts
6211 .insert(long_value(REQUEST_COUNT_SECRET), 7);
6212 let retained_key = state
6213 .http_fronts
6214 .keys()
6215 .next()
6216 .expect("dispatch must retain the frontend's Display key");
6217 assert!(retained_key.contains(HOSTNAME_SECRET));
6218 assert!(retained_key.contains(PATH_SECRET));
6219 assert!(retained_key.contains(METHOD_SECRET));
6220 let state_debug = format!("{state:?}");
6221
6222 let secrets = [
6223 CLUSTER_SECRET,
6224 HOSTNAME_SECRET,
6225 PATH_SECRET,
6226 METHOD_SECRET,
6227 TAG_KEY_SECRET,
6228 TAG_VALUE_SECRET,
6229 REDIRECT_TEMPLATE_SECRET,
6230 REWRITE_HOST_SECRET,
6231 REWRITE_PATH_SECRET,
6232 HEADER_KEY_SECRET,
6233 HEADER_VALUE_SECRET,
6234 REQUEST_COUNT_SECRET,
6235 ];
6236 for (label, output) in [
6237 ("HttpFrontend", &retained_debug),
6238 ("ConfigState", &state_debug),
6239 ] {
6240 for secret in secrets {
6241 assert!(
6242 !output.contains(secret),
6243 "{label} Debug leaked retained marker {secret}"
6244 );
6245 }
6246 assert!(
6247 output.len() <= 2048,
6248 "{label} Debug output is not bounded: {} bytes",
6249 output.len()
6250 );
6251 }
6252
6253 for metadata in [
6254 format!("cluster_id_len: Some({})", long_value(CLUSTER_SECRET).len()),
6255 "address: 127.0.0.1:8443".to_owned(),
6256 format!("hostname_len: {}", long_value(HOSTNAME_SECRET).len()),
6257 "path_kind: 1".to_owned(),
6258 format!("path_len: {}", long_value(PATH_SECRET).len()),
6259 format!("method_len: Some({})", long_value(METHOD_SECRET).len()),
6260 "position: Tree".to_owned(),
6261 "tags_count: Some(1)".to_owned(),
6262 format!(
6263 "tags_len: Some({})",
6264 long_value(TAG_KEY_SECRET).len() + long_value(TAG_VALUE_SECRET).len()
6265 ),
6266 "redirect: Some(4)".to_owned(),
6267 "redirect_scheme: Some(2)".to_owned(),
6268 format!(
6269 "redirect_template_len: Some({})",
6270 long_value(REDIRECT_TEMPLATE_SECRET).len()
6271 ),
6272 format!(
6273 "rewrite_host_len: Some({})",
6274 long_value(REWRITE_HOST_SECRET).len()
6275 ),
6276 format!(
6277 "rewrite_path_len: Some({})",
6278 long_value(REWRITE_PATH_SECRET).len()
6279 ),
6280 "rewrite_port: Some(9443)".to_owned(),
6281 "required_auth: Some(true)".to_owned(),
6282 "headers_count: 1".to_owned(),
6283 format!(
6284 "headers_len: {}",
6285 long_value(HEADER_KEY_SECRET).len() + long_value(HEADER_VALUE_SECRET).len()
6286 ),
6287 "hsts: Some(HstsConfig".to_owned(),
6288 ] {
6289 assert!(
6290 retained_debug.contains(&metadata),
6291 "HttpFrontend Debug omitted safe metadata {metadata}: {retained_debug}"
6292 );
6293 }
6294 for metadata in [
6295 "http_frontends_count: 1",
6296 "backends_count: 0",
6297 "tcp_frontends_count: 0",
6298 "udp_frontends_count: 0",
6299 "certificates_count: 0",
6300 "request_counts_count: 2",
6301 ] {
6302 assert!(
6303 state_debug.contains(metadata),
6304 "ConfigState Debug omitted count metadata {metadata}: {state_debug}"
6305 );
6306 }
6307 }
6308
6309 fn add_clusters(state: &mut ConfigState, cluster_ids: &[&str]) {
6312 for cluster_id in cluster_ids {
6313 state
6314 .dispatch(
6315 &RequestType::AddCluster(Cluster {
6316 cluster_id: cluster_id.to_string(),
6317 ..Default::default()
6318 })
6319 .into(),
6320 )
6321 .expect("Could not add the test cluster");
6322 }
6323 }
6324
6325 #[test]
6326 fn serialize() {
6327 let mut state: ConfigState = Default::default();
6328 add_clusters(&mut state, &["cluster_1", "cluster_2"]);
6329 state
6330 .dispatch(
6331 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6332 cluster_id: Some(String::from("cluster_1")),
6333 hostname: String::from("lolcatho.st:8080"),
6334 path: PathRule::prefix(String::from("/")),
6335 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6336 position: RulePosition::Tree.into(),
6337 ..Default::default()
6338 })
6339 .into(),
6340 )
6341 .expect("Could not execute request");
6342 state
6343 .dispatch(
6344 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6345 cluster_id: Some(String::from("cluster_2")),
6346 hostname: String::from("test.local"),
6347 path: PathRule::prefix(String::from("/abc")),
6348 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6349 position: RulePosition::Pre.into(),
6350 ..Default::default()
6351 })
6352 .into(),
6353 )
6354 .expect("Could not execute request");
6355 state
6356 .dispatch(
6357 &RequestType::AddBackend(AddBackend {
6358 cluster_id: String::from("cluster_1"),
6359 backend_id: String::from("cluster_1-0"),
6360 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
6361 ..Default::default()
6362 })
6363 .into(),
6364 )
6365 .expect("Could not execute request");
6366 state
6367 .dispatch(
6368 &RequestType::AddBackend(AddBackend {
6369 cluster_id: String::from("cluster_1"),
6370 backend_id: String::from("cluster_1-1"),
6371 address: SocketAddress::new_v4(127, 0, 0, 1, 1027),
6372 ..Default::default()
6373 })
6374 .into(),
6375 )
6376 .expect("Could not execute request");
6377 state
6378 .dispatch(
6379 &RequestType::AddBackend(AddBackend {
6380 cluster_id: String::from("cluster_2"),
6381 backend_id: String::from("cluster_2-0"),
6382 address: SocketAddress::new_v4(192, 167, 1, 2, 1026),
6383 ..Default::default()
6384 })
6385 .into(),
6386 )
6387 .expect("Could not execute request");
6388 state
6389 .dispatch(
6390 &RequestType::AddBackend(AddBackend {
6391 cluster_id: String::from("cluster_1"),
6392 backend_id: String::from("cluster_1-3"),
6393 address: SocketAddress::new_v4(192, 168, 1, 3, 1027),
6394 ..Default::default()
6395 })
6396 .into(),
6397 )
6398 .expect("Could not execute request");
6399 state
6400 .dispatch(
6401 &RequestType::RemoveBackend(RemoveBackend {
6402 cluster_id: String::from("cluster_1"),
6403 backend_id: String::from("cluster_1-3"),
6404 address: SocketAddress::new_v4(192, 168, 1, 3, 1027),
6405 })
6406 .into(),
6407 )
6408 .expect("Could not execute request");
6409
6410 }
6420
6421 #[test]
6422 fn diff() {
6423 let mut state: ConfigState = Default::default();
6424 add_clusters(&mut state, &["cluster_1", "cluster_2"]);
6425 state
6426 .dispatch(
6427 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6428 cluster_id: Some(String::from("cluster_1")),
6429 hostname: String::from("lolcatho.st:8080"),
6430 path: PathRule::prefix(String::from("/")),
6431 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6432 position: RulePosition::Post.into(),
6433 ..Default::default()
6434 })
6435 .into(),
6436 )
6437 .expect("Could not execute request");
6438 state
6439 .dispatch(
6440 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6441 cluster_id: Some(String::from("cluster_2")),
6442 hostname: String::from("test.local"),
6443 path: PathRule::prefix(String::from("/abc")),
6444 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6445 ..Default::default()
6446 })
6447 .into(),
6448 )
6449 .expect("Could not execute request");
6450 state
6451 .dispatch(
6452 &RequestType::AddBackend(AddBackend {
6453 cluster_id: String::from("cluster_1"),
6454 backend_id: String::from("cluster_1-0"),
6455 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
6456 load_balancing_parameters: Some(LoadBalancingParams::default()),
6457 ..Default::default()
6458 })
6459 .into(),
6460 )
6461 .expect("Could not execute request");
6462 state
6463 .dispatch(
6464 &RequestType::AddBackend(AddBackend {
6465 cluster_id: String::from("cluster_1"),
6466 backend_id: String::from("cluster_1-1"),
6467 address: SocketAddress::new_v4(127, 0, 0, 2, 1027),
6468 load_balancing_parameters: Some(LoadBalancingParams::default()),
6469 ..Default::default()
6470 })
6471 .into(),
6472 )
6473 .expect("Could not execute request");
6474 state
6475 .dispatch(
6476 &RequestType::AddBackend(AddBackend {
6477 cluster_id: String::from("cluster_2"),
6478 backend_id: String::from("cluster_2-0"),
6479 address: SocketAddress::new_v4(192, 167, 1, 2, 1026),
6480 load_balancing_parameters: Some(LoadBalancingParams::default()),
6481 ..Default::default()
6482 })
6483 .into(),
6484 )
6485 .expect("Could not execute request");
6486 state
6487 .dispatch(
6488 &RequestType::AddCluster(Cluster {
6489 cluster_id: String::from("cluster_2"),
6490 sticky_session: true,
6491 https_redirect: true,
6492 ..Default::default()
6493 })
6494 .into(),
6495 )
6496 .expect("Could not execute request");
6497
6498 let mut state2: ConfigState = Default::default();
6499 add_clusters(&mut state2, &["cluster_1"]);
6500 state2
6501 .dispatch(
6502 &RequestType::AddHttpFrontend(RequestHttpFrontend {
6503 cluster_id: Some(String::from("cluster_1")),
6504 hostname: String::from("lolcatho.st:8080"),
6505 path: PathRule::prefix(String::from("/")),
6506 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6507 position: RulePosition::Post.into(),
6508 ..Default::default()
6509 })
6510 .into(),
6511 )
6512 .expect("Could not execute request");
6513 state2
6514 .dispatch(
6515 &RequestType::AddBackend(AddBackend {
6516 cluster_id: String::from("cluster_1"),
6517 backend_id: String::from("cluster_1-0"),
6518 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
6519 load_balancing_parameters: Some(LoadBalancingParams::default()),
6520 ..Default::default()
6521 })
6522 .into(),
6523 )
6524 .expect("Could not execute request");
6525 state2
6526 .dispatch(
6527 &RequestType::AddBackend(AddBackend {
6528 cluster_id: String::from("cluster_1"),
6529 backend_id: String::from("cluster_1-1"),
6530 address: SocketAddress::new_v4(127, 0, 0, 2, 1027),
6531 load_balancing_parameters: Some(LoadBalancingParams::default()),
6532 ..Default::default()
6533 })
6534 .into(),
6535 )
6536 .expect("Could not execute request");
6537 state2
6538 .dispatch(
6539 &RequestType::AddBackend(AddBackend {
6540 cluster_id: String::from("cluster_1"),
6541 backend_id: String::from("cluster_1-2"),
6542 address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
6543 load_balancing_parameters: Some(LoadBalancingParams::default()),
6544 ..Default::default()
6545 })
6546 .into(),
6547 )
6548 .expect("Could not execute request");
6549 state2
6550 .dispatch(
6551 &RequestType::AddCluster(Cluster {
6552 cluster_id: String::from("cluster_3"),
6553 sticky_session: false,
6554 https_redirect: false,
6555 ..Default::default()
6556 })
6557 .into(),
6558 )
6559 .expect("Could not execute request");
6560
6561 let e: Vec<Request> = vec![
6565 RequestType::AddBackend(AddBackend {
6566 cluster_id: String::from("cluster_1"),
6567 backend_id: String::from("cluster_1-2"),
6568 address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
6569 load_balancing_parameters: Some(LoadBalancingParams::default()),
6570 ..Default::default()
6571 })
6572 .into(),
6573 RequestType::RemoveCluster(String::from("cluster_2")).into(),
6574 RequestType::AddCluster(Cluster {
6575 cluster_id: String::from("cluster_3"),
6576 sticky_session: false,
6577 https_redirect: false,
6578 ..Default::default()
6579 })
6580 .into(),
6581 ];
6582 let expected_diff: HashSet<&Request> = HashSet::from_iter(e.iter());
6583
6584 let d = state.diff(&state2);
6585 let diff = HashSet::from_iter(d.iter());
6586 println!("diff requests:\n{diff:#?}\n");
6587 println!("expected diff requests:\n{expected_diff:#?}\n");
6588
6589 let hash1 = state.hash_state();
6590 let hash2 = state2.hash_state();
6591 let mut state3 = state.clone();
6592 state3
6593 .dispatch(
6594 &RequestType::AddBackend(AddBackend {
6595 cluster_id: String::from("cluster_1"),
6596 backend_id: String::from("cluster_1-2"),
6597 address: SocketAddress::new_v4(127, 0, 0, 2, 1028),
6598 load_balancing_parameters: Some(LoadBalancingParams::default()),
6599 ..Default::default()
6600 })
6601 .into(),
6602 )
6603 .expect("Could not execute request");
6604 let hash3 = state3.hash_state();
6605 println!("state 1 hashes: {hash1:#?}");
6606 println!("state 2 hashes: {hash2:#?}");
6607 println!("state 3 hashes: {hash3:#?}");
6608
6609 assert_eq!(diff, expected_diff);
6610 }
6611
6612 fn state_with_clusters_owning_every_object_kind(cluster_ids: &[&str]) -> ConfigState {
6618 let mut state = ConfigState::new();
6619 for cluster_id in cluster_ids {
6620 let port = match *cluster_id {
6621 "cascade_a" => 9200,
6622 "cascade_b" => 9300,
6623 other => panic!("no test ports reserved for {other}"),
6624 };
6625 let requests: Vec<Request> = vec![
6626 RequestType::AddCluster(Cluster {
6627 cluster_id: cluster_id.to_string(),
6628 ..Default::default()
6629 })
6630 .into(),
6631 RequestType::AddHttpFrontend(RequestHttpFrontend {
6632 cluster_id: Some(cluster_id.to_string()),
6633 hostname: format!("{cluster_id}.test"),
6634 path: PathRule::prefix(String::from("/")),
6635 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6636 ..Default::default()
6637 })
6638 .into(),
6639 RequestType::AddHttpsFrontend(RequestHttpFrontend {
6640 cluster_id: Some(cluster_id.to_string()),
6641 hostname: format!("{cluster_id}.test"),
6642 path: PathRule::prefix(String::from("/")),
6643 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
6644 ..Default::default()
6645 })
6646 .into(),
6647 RequestType::AddTcpFrontend(RequestTcpFrontend {
6648 cluster_id: cluster_id.to_string(),
6649 address: SocketAddress::new_v4(127, 0, 0, 1, port),
6650 ..Default::default()
6651 })
6652 .into(),
6653 RequestType::AddUdpFrontend(RequestUdpFrontend {
6654 cluster_id: cluster_id.to_string(),
6655 address: SocketAddress::new_v4(127, 0, 0, 1, port + 1),
6656 ..Default::default()
6657 })
6658 .into(),
6659 RequestType::AddBackend(AddBackend {
6660 cluster_id: cluster_id.to_string(),
6661 backend_id: format!("{cluster_id}-0"),
6662 address: SocketAddress::new_v4(127, 0, 0, 1, port + 2),
6663 load_balancing_parameters: Some(LoadBalancingParams::default()),
6664 ..Default::default()
6665 })
6666 .into(),
6667 ];
6668 for request in &requests {
6669 state
6670 .dispatch(request)
6671 .unwrap_or_else(|e| panic!("could not apply {request:?}: {e}"));
6672 }
6673 }
6674 state
6675 .dispatch(&RequestType::AddHttpFrontend(cascade_deny_route()).into())
6676 .expect("could not add the deny route");
6677 state
6678 }
6679
6680 fn cascade_deny_route() -> RequestHttpFrontend {
6682 RequestHttpFrontend {
6683 cluster_id: None,
6684 hostname: String::from("cascade_a.test"),
6685 path: PathRule::prefix(String::from("/deny")),
6686 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6687 ..Default::default()
6688 }
6689 }
6690
6691 fn cluster_named_by(request: &Request) -> Option<&str> {
6693 match request.request_type.as_ref()? {
6694 RequestType::AddCluster(cluster) => Some(&cluster.cluster_id),
6695 RequestType::RemoveCluster(cluster_id) => Some(cluster_id),
6696 RequestType::AddHttpFrontend(front)
6697 | RequestType::RemoveHttpFrontend(front)
6698 | RequestType::AddHttpsFrontend(front)
6699 | RequestType::RemoveHttpsFrontend(front) => front.cluster_id.as_deref(),
6700 RequestType::AddTcpFrontend(front) | RequestType::RemoveTcpFrontend(front) => {
6701 Some(&front.cluster_id)
6702 }
6703 RequestType::AddUdpFrontend(front) | RequestType::RemoveUdpFrontend(front) => {
6704 Some(&front.cluster_id)
6705 }
6706 RequestType::AddBackend(backend) | RequestType::AddUnixBackend(backend) => {
6707 Some(&backend.cluster_id)
6708 }
6709 RequestType::RemoveBackend(backend) => Some(&backend.cluster_id),
6710 _ => None,
6711 }
6712 }
6713
6714 fn assert_same_routing(actual: &ConfigState, expected: &ConfigState, context: &str) {
6716 assert_eq!(actual.clusters, expected.clusters, "{context}: clusters");
6717 assert_eq!(actual.backends, expected.backends, "{context}: backends");
6718 assert_eq!(
6719 actual.http_fronts, expected.http_fronts,
6720 "{context}: http_fronts"
6721 );
6722 assert_eq!(
6723 actual.https_fronts, expected.https_fronts,
6724 "{context}: https_fronts"
6725 );
6726 assert_eq!(
6727 actual.tcp_fronts, expected.tcp_fronts,
6728 "{context}: tcp_fronts"
6729 );
6730 assert_eq!(
6731 actual.udp_fronts, expected.udp_fronts,
6732 "{context}: udp_fronts"
6733 );
6734 }
6735
6736 #[test]
6740 fn remove_cluster_removes_its_frontends_and_backends_for_every_protocol() {
6741 let mut state = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6742 let expected = state_with_clusters_owning_every_object_kind(&["cascade_b"]);
6743
6744 state
6745 .dispatch(&RequestType::RemoveCluster(String::from("cascade_a")).into())
6746 .expect("removing an existing cluster must succeed");
6747
6748 assert_same_routing(&state, &expected, "after RemoveCluster(cascade_a)");
6749 assert!(
6750 state
6751 .http_fronts
6752 .values()
6753 .any(|front| front.cluster_id.is_none() && front.hostname == "cascade_a.test"),
6754 "a route that names no cluster must survive the removal of a cluster on its hostname"
6755 );
6756
6757 let late: Request = RequestType::RemoveHttpFrontend(RequestHttpFrontend {
6761 cluster_id: Some(String::from("cascade_a")),
6762 hostname: String::from("cascade_a.test"),
6763 path: PathRule::prefix(String::from("/")),
6764 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6765 ..Default::default()
6766 })
6767 .into();
6768 let err = state
6769 .dispatch(&late)
6770 .expect_err("the cascade must already have removed the frontend");
6771 assert!(
6772 matches!(err, StateError::NotFound { .. }),
6773 "expected NotFound, got: {err}"
6774 );
6775 assert!(
6776 state.removed_with_its_cluster(&late, &err),
6777 "a removal of an object that went with its cluster must be recognised"
6778 );
6779
6780 let mut typo = cascade_deny_route();
6784 typo.cluster_id = Some(String::from("cascade_typo"));
6785 let typo: Request = RequestType::RemoveHttpFrontend(typo).into();
6786 let err = state
6787 .dispatch(&typo)
6788 .expect_err("a removal naming another cluster must not match the deny route");
6789 assert!(state.removed_with_its_cluster(&typo, &err));
6790 let answer = state.removed_with_its_cluster_message(&typo);
6791 assert!(
6792 answer.contains("this key belongs to a route with no cluster, left in place"),
6793 "the ok answer must say the deny route holds the key: {answer}"
6794 );
6795 assert!(
6796 state
6797 .http_fronts
6798 .values()
6799 .any(|front| front.cluster_id.is_none() && front.hostname == "cascade_a.test"),
6800 "the deny route must survive a removal naming a cluster that never existed"
6801 );
6802 }
6803
6804 #[test]
6810 fn a_stale_frontend_removal_never_removes_another_clusters_route() {
6811 let route = |cluster_id: &str| RequestHttpFrontend {
6812 cluster_id: Some(cluster_id.to_owned()),
6813 hostname: String::from("reused.test"),
6814 path: PathRule::prefix(String::from("/")),
6815 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6816 ..Default::default()
6817 };
6818 let cluster = |cluster_id: &str| {
6819 RequestType::AddCluster(Cluster {
6820 cluster_id: cluster_id.to_owned(),
6821 ..Default::default()
6822 })
6823 };
6824 let owner =
6825 |state: &ConfigState, fronts: fn(&ConfigState) -> &BTreeMap<String, HttpFrontend>| {
6826 fronts(state)
6827 .get(&route("stale_b").to_string())
6828 .and_then(|front| front.cluster_id.as_deref().map(str::to_owned))
6829 };
6830
6831 for (add, remove, fronts) in [
6832 (
6833 RequestType::AddHttpFrontend as fn(RequestHttpFrontend) -> RequestType,
6834 RequestType::RemoveHttpFrontend as fn(RequestHttpFrontend) -> RequestType,
6835 (|state: &ConfigState| &state.http_fronts)
6836 as fn(&ConfigState) -> &BTreeMap<String, HttpFrontend>,
6837 ),
6838 (
6839 RequestType::AddHttpsFrontend,
6840 RequestType::RemoveHttpsFrontend,
6841 |state: &ConfigState| &state.https_fronts,
6842 ),
6843 ] {
6844 let mut state = ConfigState::new();
6847 for request in [
6848 cluster("stale_a"),
6849 add(route("stale_a")),
6850 RequestType::RemoveCluster(String::from("stale_a")),
6851 cluster("stale_b"),
6852 add(route("stale_b")),
6853 ] {
6854 state
6855 .dispatch(&request.clone().into())
6856 .unwrap_or_else(|e| panic!("could not apply {request:?}: {e}"));
6857 }
6858 let stale: Request = remove(route("stale_a")).into();
6859 let err = state
6860 .dispatch(&stale)
6861 .expect_err("a removal naming cluster A must not match cluster B's route");
6862 assert!(
6863 state.removed_with_its_cluster(&stale, &err),
6864 "A is gone, so its frontend went with it: {err}"
6865 );
6866 let answer = state.removed_with_its_cluster_message(&stale);
6867 assert!(
6868 answer.contains("cluster stale_a does not exist")
6869 && answer.contains("this key belongs to cluster stale_b, left in place"),
6870 "the ok answer must name the key's current owner: {answer}"
6871 );
6872 assert_eq!(
6873 owner(&state, fronts).as_deref(),
6874 Some("stale_b"),
6875 "cluster B's route must survive a stale removal naming cluster A"
6876 );
6877
6878 let mut state = ConfigState::new();
6881 for request in [
6882 cluster("stale_a"),
6883 cluster("stale_b"),
6884 add(route("stale_b")),
6885 ] {
6886 state
6887 .dispatch(&request.clone().into())
6888 .unwrap_or_else(|e| panic!("could not apply {request:?}: {e}"));
6889 }
6890 let err = state
6891 .dispatch(&stale)
6892 .expect_err("a removal naming cluster A must not match cluster B's route");
6893 assert!(
6894 !state.removed_with_its_cluster(&stale, &err),
6895 "A still exists: the removal is refused, not already done"
6896 );
6897 assert_eq!(
6898 owner(&state, fronts).as_deref(),
6899 Some("stale_b"),
6900 "cluster B's route must survive a removal naming cluster A"
6901 );
6902 }
6903 }
6904
6905 #[test]
6909 fn diff_removing_a_cluster_relies_on_the_cascade() {
6910 let before = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6911 let after = state_with_clusters_owning_every_object_kind(&["cascade_b"]);
6912
6913 let diff = before.diff(&after);
6914 assert_eq!(
6915 diff,
6916 vec![Request::from(RequestType::RemoveCluster(String::from(
6917 "cascade_a"
6918 )))],
6919 "the cascade must be the only removal the diff emits"
6920 );
6921
6922 let mut replayed = before.clone();
6923 for request in &diff {
6924 replayed
6925 .dispatch(request)
6926 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
6927 }
6928 assert_same_routing(&replayed, &after, "replaying diff(before, after)");
6929 }
6930
6931 #[test]
6935 fn diff_re_adds_a_frontend_the_target_keeps_for_a_removed_cluster() {
6936 let before = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6937 let kept = RequestHttpFrontend {
6938 cluster_id: Some(String::from("cascade_a")),
6939 hostname: String::from("cascade_a.test"),
6940 path: PathRule::prefix(String::from("/")),
6941 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
6942 ..Default::default()
6943 };
6944 let mut after = state_with_clusters_owning_every_object_kind(&["cascade_b"]);
6945 after
6946 .dispatch(&RequestType::AddHttpFrontend(kept.clone()).into())
6947 .expect("a frontend may name a cluster the state does not hold");
6948
6949 let diff = before.diff(&after);
6950 let removal = diff
6951 .iter()
6952 .position(|r| r.request_type == Some(RequestType::RemoveCluster("cascade_a".into())))
6953 .expect("the diff must remove cascade_a");
6954 let re_add = diff
6955 .iter()
6956 .position(|r| r.request_type == Some(RequestType::AddHttpFrontend(kept.clone())))
6957 .expect("the diff must add back the frontend the cascade removes");
6958 assert!(
6959 removal < re_add,
6960 "the frontend must be added back after its cluster is removed: {diff:?}"
6961 );
6962
6963 let mut replayed = before.clone();
6964 for request in &diff {
6965 replayed
6966 .dispatch(request)
6967 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
6968 }
6969 assert_same_routing(&replayed, &after, "replaying diff(before, after)");
6970 }
6971
6972 #[test]
6976 fn a_removed_cluster_leaves_nothing_for_load_state_to_replay() {
6977 let mut state = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
6978 state
6979 .dispatch(&RequestType::RemoveCluster(String::from("cascade_a")).into())
6980 .expect("removing an existing cluster must succeed");
6981
6982 let requests = state.generate_requests();
6983 let leaked: Vec<&Request> = requests
6984 .iter()
6985 .filter(|request| cluster_named_by(request) == Some("cascade_a"))
6986 .collect();
6987 assert!(
6988 leaked.is_empty(),
6989 "LoadState would replay objects of the removed cluster: {leaked:?}"
6990 );
6991
6992 let mut replayed = ConfigState::new();
6993 for request in &requests {
6994 replayed
6995 .dispatch(request)
6996 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
6997 }
6998 assert_same_routing(&replayed, &state, "replaying generate_requests");
6999 }
7000
7001 fn add_backend_to(cluster_id: &str, port: u16) -> Request {
7002 RequestType::AddBackend(AddBackend {
7003 cluster_id: cluster_id.to_owned(),
7004 backend_id: format!("{cluster_id}-{port}"),
7005 address: SocketAddress::new_v4(127, 0, 0, 1, port),
7006 ..Default::default()
7007 })
7008 .into()
7009 }
7010
7011 #[test]
7015 fn add_backend_for_an_unknown_cluster_is_refused() {
7016 let mut state = ConfigState::new();
7017
7018 let refused = state.dispatch(&add_backend_to("orphan", 1026));
7019 assert!(
7020 matches!(refused, Err(StateError::ClusterNotFound { .. })),
7021 "a backend of a cluster never added must be refused, got {refused:?}"
7022 );
7023 assert!(
7024 state.backends.is_empty(),
7025 "a refused AddBackend must not leave a bucket behind: {:?}",
7026 state.backends
7027 );
7028
7029 state
7030 .dispatch(
7031 &RequestType::AddCluster(Cluster {
7032 cluster_id: String::from("orphan"),
7033 ..Default::default()
7034 })
7035 .into(),
7036 )
7037 .expect("adding the cluster must succeed");
7038 state
7039 .dispatch(&add_backend_to("orphan", 1026))
7040 .expect("a backend of an existing cluster must be added");
7041 assert_eq!(state.count_backends(), 1);
7042
7043 state
7044 .dispatch(&RequestType::RemoveCluster(String::from("orphan")).into())
7045 .expect("removing an existing cluster must succeed");
7046 assert!(
7047 state.backends.is_empty(),
7048 "RemoveCluster must remove the cluster's backends"
7049 );
7050 let refused = state.dispatch(&add_backend_to("orphan", 1027));
7051 assert!(
7052 matches!(refused, Err(StateError::ClusterNotFound { .. })),
7053 "a backend of a removed cluster must be refused, got {refused:?}"
7054 );
7055 assert!(
7056 state.backends.is_empty(),
7057 "a backend must never outlive its cluster: {:?}",
7058 state.backends
7059 );
7060 }
7061
7062 #[test]
7066 fn add_unix_backend_for_an_unknown_cluster_is_refused() {
7067 let mut state = ConfigState::new();
7068 for unix_path in ["/run/app.sock", "relative.sock"] {
7069 let refused = state.dispatch(
7070 &RequestType::AddUnixBackend(AddBackend {
7071 cluster_id: String::from("orphan"),
7072 backend_id: String::from("orphan-unix"),
7073 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
7074 unix_path: Some(unix_path.to_owned()),
7075 ..Default::default()
7076 })
7077 .into(),
7078 );
7079 assert!(
7080 matches!(refused, Err(StateError::ClusterNotFound { .. })),
7081 "{unix_path:?}: an unknown cluster must be refused first, got {refused:?}"
7082 );
7083 assert!(
7084 state.backends.is_empty(),
7085 "a refused AddUnixBackend must not leave a bucket behind: {:?}",
7086 state.backends
7087 );
7088 }
7089 assert!(state.clusters.is_empty());
7090 }
7091
7092 #[test]
7095 fn unknown_cluster_error_names_the_object_without_the_cluster_id() {
7096 let mut state = ConfigState::new();
7097 let error = state
7098 .dispatch(&add_backend_to("secret-cluster-id", 1026))
7099 .expect_err("a backend of an unknown cluster must be refused");
7100 let message = error.to_string();
7101 assert!(
7102 !message.contains("secret-cluster-id"),
7103 "the error must not echo the cluster id: {message}"
7104 );
7105 assert!(
7106 message.contains("Backend") && message.contains("add the cluster first"),
7107 "the error must say what to do: {message}"
7108 );
7109 }
7110
7111 #[test]
7114 fn a_configuration_file_adds_each_cluster_before_its_backends() {
7115 let config = crate::config::Config::load_from_path("assets/config.toml")
7116 .expect("the test configuration must load");
7117 let mut state = ConfigState::new();
7118 for message in config
7119 .generate_config_messages()
7120 .expect("the test configuration must generate its orders")
7121 {
7122 let result = state.dispatch(&message.content);
7123 if let Some(RequestType::AddBackend(_) | RequestType::AddCluster(_)) =
7124 &message.content.request_type
7125 {
7126 result.unwrap_or_else(|e| panic!("could not apply {message:?}: {e}"));
7127 }
7128 }
7129 assert_eq!(state.clusters.len(), 3);
7130 assert_eq!(state.count_backends(), 5);
7131 }
7132
7133 #[test]
7136 fn diff_adds_a_cluster_before_its_backends() {
7137 let target = state_with_clusters_owning_every_object_kind(&["cascade_a", "cascade_b"]);
7138 let mut state = ConfigState::new();
7139 for request in state.diff(&target) {
7140 state
7141 .dispatch(&request)
7142 .unwrap_or_else(|e| panic!("could not replay {request:?}: {e}"));
7143 }
7144 assert_same_routing(&state, &target, "replaying diff(empty, target)");
7145 }
7146
7147 fn app_cluster_probing(port: Option<u32>, unix_path: Option<&str>) -> ConfigState {
7150 let mut state = ConfigState::new();
7151 let health_check = crate::proto::command::HealthCheckConfig {
7152 interval: 10,
7153 timeout: 5,
7154 healthy_threshold: 3,
7155 unhealthy_threshold: 3,
7156 mode: crate::proto::command::HealthCheckMode::Tcp as i32,
7157 port,
7158 ..Default::default()
7159 };
7160 state
7161 .dispatch(
7162 &RequestType::AddCluster(Cluster {
7163 cluster_id: String::from("app"),
7164 health_check: Some(health_check),
7165 ..Default::default()
7166 })
7167 .into(),
7168 )
7169 .expect("the cluster must be accepted");
7170 state
7171 .dispatch(
7172 &RequestType::add_backend(AddBackend {
7173 cluster_id: String::from("app"),
7174 backend_id: String::from("app-0"),
7175 address: SocketAddress::new_v4(127, 0, 0, 1, 9000),
7176 unix_path: unix_path.map(ToOwned::to_owned),
7177 ..Default::default()
7178 })
7179 .into(),
7180 )
7181 .expect("the backend must be accepted");
7182 state
7183 }
7184
7185 #[test]
7191 fn diff_converges_when_a_probe_port_and_a_unix_backend_swap() {
7192 let unix = app_cluster_probing(None, Some("/run/app.sock"));
7193 let ip = app_cluster_probing(Some(8081), None);
7194 for (from, to, direction) in [
7195 (&unix, &ip, "unix -> ip+port"),
7196 (&ip, &unix, "ip+port -> unix"),
7197 ] {
7198 let mut state = from.clone();
7199 for request in from.diff(to) {
7200 state
7201 .dispatch(&request)
7202 .unwrap_or_else(|e| panic!("{direction}: could not apply {request:?}: {e}"));
7203 }
7204 assert_same_routing(&state, to, direction);
7205 }
7206 }
7207
7208 #[test]
7209 fn cluster_ids_by_domain() {
7210 let mut config = ConfigState::new();
7211 let http_front_cluster1 = RequestHttpFrontend {
7212 cluster_id: Some(String::from("MyCluster_1")),
7213 hostname: String::from("lolcatho.st"),
7214 path: PathRule::prefix(String::from("")),
7215 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7216 ..Default::default()
7217 };
7218
7219 let https_front_cluster1 = RequestHttpFrontend {
7220 cluster_id: Some(String::from("MyCluster_1")),
7221 hostname: String::from("lolcatho.st"),
7222 path: PathRule::prefix(String::from("")),
7223 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7224 ..Default::default()
7225 };
7226
7227 let http_front_cluster2 = RequestHttpFrontend {
7228 cluster_id: Some(String::from("MyCluster_2")),
7229 hostname: String::from("lolcatho.st"),
7230 path: PathRule::prefix(String::from("/api")),
7231 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7232 ..Default::default()
7233 };
7234
7235 let https_front_cluster2 = RequestHttpFrontend {
7236 cluster_id: Some(String::from("MyCluster_2")),
7237 hostname: String::from("lolcatho.st"),
7238 path: PathRule::prefix(String::from("/api")),
7239 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7240 ..Default::default()
7241 };
7242
7243 config
7244 .dispatch(&RequestType::AddHttpFrontend(http_front_cluster1).into())
7245 .expect("Could not execute request");
7246 config
7247 .dispatch(&RequestType::AddHttpFrontend(http_front_cluster2).into())
7248 .expect("Could not execute request");
7249 config
7250 .dispatch(&RequestType::AddHttpsFrontend(https_front_cluster1).into())
7251 .expect("Could not execute request");
7252 config
7253 .dispatch(&RequestType::AddHttpsFrontend(https_front_cluster2).into())
7254 .expect("Could not execute request");
7255
7256 let mut cluster1_cluster2: HashSet<ClusterId> = HashSet::new();
7257 cluster1_cluster2.insert(ClusterId::from("MyCluster_1"));
7258 cluster1_cluster2.insert(ClusterId::from("MyCluster_2"));
7259
7260 let mut cluster2: HashSet<ClusterId> = HashSet::new();
7261 cluster2.insert(ClusterId::from("MyCluster_2"));
7262
7263 let empty: HashSet<ClusterId> = HashSet::new();
7264 assert_eq!(
7265 config.get_cluster_ids_by_domain(String::from("lolcatho.st"), None),
7266 cluster1_cluster2
7267 );
7268 assert_eq!(
7269 config
7270 .get_cluster_ids_by_domain(String::from("lolcatho.st"), Some(String::from("/api"))),
7271 cluster2
7272 );
7273 assert_eq!(
7274 config.get_cluster_ids_by_domain(String::from("lolcathost"), None),
7275 empty
7276 );
7277 assert_eq!(
7278 config
7279 .get_cluster_ids_by_domain(String::from("lolcathost"), Some(String::from("/sozu"))),
7280 empty
7281 );
7282 }
7283
7284 #[test]
7285 fn duplicate_backends() {
7286 let mut state: ConfigState = Default::default();
7287 add_clusters(&mut state, &["cluster_1"]);
7288 state
7289 .dispatch(
7290 &RequestType::AddBackend(AddBackend {
7291 cluster_id: String::from("cluster_1"),
7292 backend_id: String::from("cluster_1-0"),
7293 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7294 load_balancing_parameters: Some(LoadBalancingParams::default()),
7295 ..Default::default()
7296 })
7297 .into(),
7298 )
7299 .expect("Could not execute request");
7300
7301 let b = Backend {
7302 cluster_id: ClusterId::from("cluster_1"),
7303 backend_id: String::from("cluster_1-0"),
7304 address: "127.0.0.1:1026".parse().unwrap(),
7305 load_balancing_parameters: Some(LoadBalancingParams::default()),
7306 sticky_id: Some("sticky".to_string()),
7307 backup: None,
7308 unix_path: None,
7309 };
7310
7311 state
7312 .dispatch(&RequestType::AddBackend(b.clone().to_add_backend()).into())
7313 .expect("Could not execute order");
7314
7315 assert_eq!(state.backends.get("cluster_1").unwrap(), &vec![b]);
7316 }
7317
7318 #[test]
7322 fn a_unix_path_backend_round_trips_through_the_state() {
7323 let mut state: ConfigState = Default::default();
7324 add_clusters(&mut state, &["cluster_1"]);
7325 let add = AddBackend {
7326 cluster_id: String::from("cluster_1"),
7327 backend_id: String::from("cluster_1-unix"),
7328 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
7329 unix_path: Some(String::from("/run/app.sock")),
7330 ..Default::default()
7331 };
7332 state
7333 .dispatch(&RequestType::AddBackend(add.clone()).into())
7334 .expect("a valid unix_path is accepted");
7335 assert_eq!(
7336 state.backends["cluster_1"][0].unix_path.as_deref(),
7337 Some("/run/app.sock")
7338 );
7339 let replayed: Vec<AddBackend> = state
7340 .generate_requests()
7341 .into_iter()
7342 .filter_map(|request| match request.request_type {
7343 Some(RequestType::AddUnixBackend(backend)) => Some(backend),
7344 _ => None,
7345 })
7346 .collect();
7347 assert_eq!(replayed, vec![add]);
7348 }
7349
7350 #[test]
7353 fn an_invalid_unix_path_is_refused() {
7354 let mut state: ConfigState = Default::default();
7355 add_clusters(&mut state, &["cluster_1"]);
7356 for refused in ["", "relative.sock", "@"] {
7357 let result = state.dispatch(
7358 &RequestType::AddBackend(AddBackend {
7359 cluster_id: String::from("cluster_1"),
7360 backend_id: String::from("cluster_1-unix"),
7361 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
7362 unix_path: Some(refused.to_owned()),
7363 ..Default::default()
7364 })
7365 .into(),
7366 );
7367 assert!(
7368 matches!(
7369 result,
7370 Err(StateError::InvalidValue {
7371 field: "unix_path",
7372 ..
7373 })
7374 ),
7375 "{refused:?} must be refused"
7376 );
7377 }
7378 assert!(state.backends.is_empty());
7379 }
7380
7381 #[test]
7382 fn remove_backend() {
7383 let mut state: ConfigState = Default::default();
7384 state
7385 .dispatch(
7386 &RequestType::AddCluster(Cluster {
7387 cluster_id: String::from("cluster_1"),
7388 ..Default::default()
7389 })
7390 .into(),
7391 )
7392 .expect("Could not execute request");
7393
7394 for i in 0..10 {
7395 state
7396 .dispatch(
7397 &RequestType::AddBackend(AddBackend {
7398 cluster_id: String::from("cluster_1"),
7399 backend_id: format!("cluster_1-{i}"),
7400 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7401 ..Default::default()
7402 })
7403 .into(),
7404 )
7405 .expect("Could not execute request");
7406 }
7407
7408 assert_eq!(state.backends.get("cluster_1").unwrap().len(), 10);
7409
7410 let remove_backend_2 = RequestType::RemoveBackend(RemoveBackend {
7411 cluster_id: String::from("cluster_1"),
7412 backend_id: String::from("cluster_1-0"),
7413 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7414 })
7415 .into();
7416
7417 let remove_backend_result = state.dispatch(&remove_backend_2);
7418
7419 assert!(remove_backend_result.is_ok());
7420 assert_eq!(state.backends.get("cluster_1").unwrap().len(), 9);
7421
7422 let redundant_remove = state.dispatch(&remove_backend_2);
7423 assert!(matches!(redundant_remove, Err(StateError::NoChange)));
7424 assert_eq!(state.backends.get("cluster_1").unwrap().len(), 9);
7425 }
7426
7427 #[test]
7428 fn remove_backends_randomly() {
7429 let mut state: ConfigState = Default::default();
7430 state
7431 .dispatch(
7432 &RequestType::AddCluster(Cluster {
7433 cluster_id: String::from("cluster_1"),
7434 ..Default::default()
7435 })
7436 .into(),
7437 )
7438 .expect("Could not execute request");
7439
7440 for _ in 0..1000 {
7441 for i in 0..10 {
7442 state
7443 .dispatch(
7444 &RequestType::AddBackend(AddBackend {
7445 cluster_id: String::from("cluster_1"),
7446 backend_id: format!("cluster_1-{i}"),
7447 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7448 ..Default::default()
7449 })
7450 .into(),
7451 )
7452 .expect("Could not execute request");
7453 }
7454
7455 let mut rng = rng();
7456 let mut indexes = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
7457 indexes.shuffle(&mut rng);
7458 let random_count = rng.random_range(1..indexes.len());
7459 let random_indexes: Vec<i32> = indexes.into_iter().take(random_count).collect();
7460
7461 for j in random_indexes {
7462 let remove_backend_result = state.dispatch(
7463 &RequestType::RemoveBackend(RemoveBackend {
7464 cluster_id: String::from("cluster_1"),
7465 backend_id: format!("cluster_1-{j}"),
7466 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7467 })
7468 .into(),
7469 );
7470 assert!(remove_backend_result.is_ok());
7471 }
7472 }
7473 }
7474
7475 #[test]
7476 fn listener_diff() {
7477 let mut state: ConfigState = Default::default();
7478 let custom_http_answers = Some(CustomHttpAnswers {
7479 answer_404: Some("test".to_string()),
7480 ..Default::default()
7481 });
7482 state
7483 .dispatch(
7484 &RequestType::AddTcpListener(TcpListenerConfig {
7485 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7486 ..Default::default()
7487 })
7488 .into(),
7489 )
7490 .expect("Could not execute request");
7491 state
7492 .dispatch(
7493 &RequestType::ActivateListener(ActivateListener {
7494 interface: None,
7495 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7496 proxy: ListenerType::Tcp.into(),
7497 from_scm: false,
7498 })
7499 .into(),
7500 )
7501 .expect("Could not execute request");
7502 state
7503 .dispatch(
7504 &RequestType::AddHttpListener(HttpListenerConfig {
7505 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7506 ..Default::default()
7507 })
7508 .into(),
7509 )
7510 .expect("Could not execute request");
7511 state
7512 .dispatch(
7513 &RequestType::AddHttpsListener(HttpsListenerConfig {
7514 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7515 ..Default::default()
7516 })
7517 .into(),
7518 )
7519 .expect("Could not execute request");
7520 state
7521 .dispatch(
7522 &RequestType::ActivateListener(ActivateListener {
7523 interface: None,
7524 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7525 proxy: ListenerType::Https.into(),
7526 from_scm: false,
7527 })
7528 .into(),
7529 )
7530 .expect("Could not execute request");
7531
7532 let mut state2: ConfigState = Default::default();
7533 state2
7534 .dispatch(
7535 &RequestType::AddTcpListener(TcpListenerConfig {
7536 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7537 expect_proxy: true,
7538 ..Default::default()
7539 })
7540 .into(),
7541 )
7542 .expect("Could not execute request");
7543 state2
7544 .dispatch(
7545 &RequestType::AddHttpListener(HttpListenerConfig {
7546 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7547 http_answers: custom_http_answers.clone(),
7548 ..Default::default()
7549 })
7550 .into(),
7551 )
7552 .expect("Could not execute request");
7553 state2
7554 .dispatch(
7555 &RequestType::ActivateListener(ActivateListener {
7556 interface: None,
7557 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7558 proxy: ListenerType::Http.into(),
7559 from_scm: false,
7560 })
7561 .into(),
7562 )
7563 .expect("Could not execute request");
7564 state2
7565 .dispatch(
7566 &RequestType::AddHttpsListener(HttpsListenerConfig {
7567 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7568 http_answers: custom_http_answers.clone(),
7569 ..Default::default()
7570 })
7571 .into(),
7572 )
7573 .expect("Could not execute request");
7574 state2
7575 .dispatch(
7576 &RequestType::ActivateListener(ActivateListener {
7577 interface: None,
7578 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7579 proxy: ListenerType::Https.into(),
7580 from_scm: false,
7581 })
7582 .into(),
7583 )
7584 .expect("Could not execute request");
7585
7586 let e: Vec<Request> = vec![
7587 RequestType::RemoveListener(RemoveListener {
7588 interface: None,
7589 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7590 proxy: ListenerType::Tcp.into(),
7591 })
7592 .into(),
7593 RequestType::AddTcpListener(TcpListenerConfig {
7594 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7595 expect_proxy: true,
7596 ..Default::default()
7597 })
7598 .into(),
7599 RequestType::DeactivateListener(DeactivateListener {
7600 interface: None,
7601 address: SocketAddress::new_v4(0, 0, 0, 0, 1234),
7602 proxy: ListenerType::Tcp.into(),
7603 to_scm: false,
7604 })
7605 .into(),
7606 RequestType::RemoveListener(RemoveListener {
7607 interface: None,
7608 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7609 proxy: ListenerType::Http.into(),
7610 })
7611 .into(),
7612 RequestType::AddHttpListener(HttpListenerConfig {
7613 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7614 http_answers: custom_http_answers.clone(),
7615 ..Default::default()
7616 })
7617 .into(),
7618 RequestType::ActivateListener(ActivateListener {
7619 interface: None,
7620 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7621 proxy: ListenerType::Http.into(),
7622 from_scm: false,
7623 })
7624 .into(),
7625 RequestType::RemoveListener(RemoveListener {
7626 interface: None,
7627 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7628 proxy: ListenerType::Https.into(),
7629 })
7630 .into(),
7631 RequestType::AddHttpsListener(HttpsListenerConfig {
7632 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7633 http_answers: custom_http_answers.clone(),
7634 ..Default::default()
7635 })
7636 .into(),
7637 RequestType::ActivateListener(ActivateListener {
7643 interface: None,
7644 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7645 proxy: ListenerType::Https.into(),
7646 from_scm: false,
7647 })
7648 .into(),
7649 ];
7650
7651 let diff = state.diff(&state2);
7652 println!("expected diff requests:\n{e:#?}\n");
7654 println!("diff requests:\n{diff:#?}\n");
7655
7656 let _hash1 = state.hash_state();
7657 let _hash2 = state2.hash_state();
7658
7659 assert_eq!(diff, e);
7660
7661 let mut replayed = state.clone();
7668 for request in &diff {
7669 replayed
7670 .dispatch(request)
7671 .expect("every diff request must replay cleanly onto the source state");
7672 }
7673 assert_eq!(
7674 replayed.tcp_listeners, state2.tcp_listeners,
7675 "replayed tcp_listeners must match the target state"
7676 );
7677 assert_eq!(
7678 replayed.http_listeners, state2.http_listeners,
7679 "replayed http_listeners must match the target state"
7680 );
7681 assert_eq!(
7682 replayed.https_listeners, state2.https_listeners,
7683 "replayed https_listeners must match the target state"
7684 );
7685 let replayed_8443 = replayed
7688 .https_listeners
7689 .get(&ListenerKey::from(SocketAddress::new_v4(0, 0, 0, 0, 8443)))
7690 .expect("8443 HTTPS listener must exist after replay");
7691 assert!(
7692 replayed_8443.active,
7693 "the 8443 HTTPS listener must stay ACTIVE across a config change"
7694 );
7695 }
7696
7697 #[test]
7698 fn certificate_retrieval() {
7699 let mut state: ConfigState = Default::default();
7700 let certificate_and_key = CertificateAndKey {
7701 certificate: String::from(include_str!("../assets/certificate.pem")),
7702 key: String::from(include_str!("../assets/key.pem")),
7703 certificate_chain: vec![],
7704 versions: vec![],
7705 names: vec!["lolcatho.st".to_string()],
7706 };
7707 let add_certificate = AddCertificate {
7708 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
7709 certificate: certificate_and_key,
7710 expired_at: None,
7711 };
7712 state
7713 .dispatch(&RequestType::AddCertificate(add_certificate).into())
7714 .expect("Could not add certificate");
7715
7716 println!("state: {state:#?}");
7717
7718 let certificates_found_by_fingerprint = state.get_certificates(QueryCertificatesFilters {
7724 domain: None,
7725 fingerprint: Some(
7726 "ab2618b674e15243fd02a5618c66509e4840ba60e7d64cebec84cdbfeceee0c5".to_string(),
7727 ),
7728 });
7729
7730 println!("found certificate: {certificates_found_by_fingerprint:#?}");
7731
7732 assert!(!certificates_found_by_fingerprint.is_empty());
7733
7734 let certificate_found_by_domain_name = state.get_certificates(QueryCertificatesFilters {
7735 domain: Some("lolcatho.st".to_string()),
7736 fingerprint: None,
7737 });
7738
7739 assert!(!certificate_found_by_domain_name.is_empty());
7740 }
7741
7742 #[test]
7743 fn count_backends_across_clusters() {
7744 let mut state: ConfigState = Default::default();
7745 add_clusters(&mut state, &["cluster_1", "cluster_2"]);
7746
7747 assert_eq!(state.count_backends(), 0);
7748
7749 state
7750 .dispatch(
7751 &RequestType::AddBackend(AddBackend {
7752 cluster_id: String::from("cluster_1"),
7753 backend_id: String::from("cluster_1-0"),
7754 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7755 ..Default::default()
7756 })
7757 .into(),
7758 )
7759 .expect("Could not execute request");
7760 assert_eq!(state.count_backends(), 1);
7761
7762 state
7763 .dispatch(
7764 &RequestType::AddBackend(AddBackend {
7765 cluster_id: String::from("cluster_1"),
7766 backend_id: String::from("cluster_1-1"),
7767 address: SocketAddress::new_v4(127, 0, 0, 1, 1027),
7768 ..Default::default()
7769 })
7770 .into(),
7771 )
7772 .expect("Could not execute request");
7773 assert_eq!(state.count_backends(), 2);
7774
7775 state
7777 .dispatch(
7778 &RequestType::AddBackend(AddBackend {
7779 cluster_id: String::from("cluster_2"),
7780 backend_id: String::from("cluster_2-0"),
7781 address: SocketAddress::new_v4(192, 168, 1, 1, 8080),
7782 ..Default::default()
7783 })
7784 .into(),
7785 )
7786 .expect("Could not execute request");
7787 assert_eq!(state.count_backends(), 3);
7788
7789 state
7791 .dispatch(
7792 &RequestType::RemoveBackend(RemoveBackend {
7793 cluster_id: String::from("cluster_1"),
7794 backend_id: String::from("cluster_1-0"),
7795 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
7796 })
7797 .into(),
7798 )
7799 .expect("Could not execute request");
7800 assert_eq!(state.count_backends(), 2);
7801 }
7802
7803 #[test]
7833 fn a_regex_hostname_is_always_replayed_before_the_exact_name_it_matches() {
7834 let mut state: ConfigState = Default::default();
7835
7836 for hostname in ["test4.example.com", "/test[0-9]/.example.com"] {
7839 state
7840 .dispatch(
7841 &RequestType::AddHttpFrontend(RequestHttpFrontend {
7842 cluster_id: Some(String::from("cluster_1")),
7843 hostname: String::from(hostname),
7844 path: PathRule::prefix(String::from("/")),
7845 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7846 position: RulePosition::Tree.into(),
7847 ..Default::default()
7848 })
7849 .into(),
7850 )
7851 .expect("Could not execute request");
7852 }
7853
7854 let replayed: Vec<String> = state
7857 .produce_initial_state()
7858 .requests
7859 .into_iter()
7860 .filter_map(|worker_request| match worker_request.content.request_type {
7861 Some(RequestType::AddHttpFrontend(front)) => Some(front.hostname),
7862 _ => None,
7863 })
7864 .collect();
7865
7866 assert_eq!(
7867 replayed,
7868 vec![
7869 String::from("/test[0-9]/.example.com"),
7870 String::from("test4.example.com"),
7871 ],
7872 "a worker replays the regex segment before the exact name it \
7873 matches, whatever order the operator added them in",
7874 );
7875 }
7876
7877 #[test]
7878 fn count_frontends_across_types() {
7879 let mut state: ConfigState = Default::default();
7880
7881 assert_eq!(state.count_frontends(), 0);
7882
7883 state
7885 .dispatch(
7886 &RequestType::AddHttpFrontend(RequestHttpFrontend {
7887 cluster_id: Some(String::from("cluster_1")),
7888 hostname: String::from("example.com"),
7889 path: PathRule::prefix(String::from("/")),
7890 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7891 position: RulePosition::Tree.into(),
7892 ..Default::default()
7893 })
7894 .into(),
7895 )
7896 .expect("Could not execute request");
7897 assert_eq!(state.count_frontends(), 1);
7898
7899 state
7901 .dispatch(
7902 &RequestType::AddHttpsFrontend(RequestHttpFrontend {
7903 cluster_id: Some(String::from("cluster_1")),
7904 hostname: String::from("secure.example.com"),
7905 path: PathRule::prefix(String::from("/")),
7906 address: SocketAddress::new_v4(0, 0, 0, 0, 8443),
7907 position: RulePosition::Tree.into(),
7908 ..Default::default()
7909 })
7910 .into(),
7911 )
7912 .expect("Could not execute request");
7913 assert_eq!(state.count_frontends(), 2);
7914
7915 state
7917 .dispatch(
7918 &RequestType::AddTcpFrontend(RequestTcpFrontend {
7919 cluster_id: String::from("cluster_2"),
7920 address: SocketAddress::new_v4(0, 0, 0, 0, 5432),
7921 ..Default::default()
7922 })
7923 .into(),
7924 )
7925 .expect("Could not execute request");
7926 assert_eq!(state.count_frontends(), 3);
7927
7928 state
7930 .dispatch(
7931 &RequestType::AddTcpFrontend(RequestTcpFrontend {
7932 cluster_id: String::from("cluster_2"),
7933 address: SocketAddress::new_v4(0, 0, 0, 0, 5433),
7934 ..Default::default()
7935 })
7936 .into(),
7937 )
7938 .expect("Could not execute request");
7939 assert_eq!(state.count_frontends(), 4);
7940
7941 state
7943 .dispatch(
7944 &RequestType::RemoveHttpFrontend(RequestHttpFrontend {
7945 cluster_id: Some(String::from("cluster_1")),
7946 hostname: String::from("example.com"),
7947 path: PathRule::prefix(String::from("/")),
7948 address: SocketAddress::new_v4(0, 0, 0, 0, 8080),
7949 position: RulePosition::Tree.into(),
7950 ..Default::default()
7951 })
7952 .into(),
7953 )
7954 .expect("Could not execute request");
7955 assert_eq!(state.count_frontends(), 3);
7956 }
7957
7958 #[test]
7964 fn https_listener_requests_carry_client_auth_on_their_own_verb() {
7965 let mtls_address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
7966 let plain_address = SocketAddress::new_v4(127, 0, 0, 1, 8444);
7967 let mtls = HttpsListenerConfig {
7968 client_auth: Some(ClientAuthMode::ClientAuthRequired as i32),
7969 client_ca_certificates: vec!["CLIENT CA PEM".to_owned()],
7970 ..make_https_listener(mtls_address)
7971 };
7972
7973 let mut state = ConfigState::new();
7974 state
7975 .dispatch(&RequestType::AddHttpsListenerWithClientAuth(mtls.clone()).into())
7976 .expect("the mTLS verb must add the listener to the state");
7977 state
7978 .dispatch(&RequestType::AddHttpsListener(make_https_listener(plain_address)).into())
7979 .expect("the historical verb must keep adding listeners without client auth");
7980 assert_eq!(
7981 state.https_listeners.len(),
7982 2,
7983 "both verbs add a listener to the same state"
7984 );
7985
7986 let https_adds = |requests: Vec<Request>| -> Vec<(SocketAddress, bool)> {
7987 requests
7988 .into_iter()
7989 .filter_map(|request| match request.request_type {
7990 Some(RequestType::AddHttpsListener(listener)) => {
7991 Some((listener.address, false))
7992 }
7993 Some(RequestType::AddHttpsListenerWithClientAuth(listener)) => {
7994 Some((listener.address, true))
7995 }
7996 _ => None,
7997 })
7998 .collect()
7999 };
8000 let mut generated = https_adds(state.generate_requests());
8001 generated.sort_by_key(|(address, _)| address.port);
8002 assert_eq!(
8003 generated,
8004 vec![(mtls_address, true), (plain_address, false)],
8005 "generate_requests must put only the listener asking for a client certificate on the mTLS verb"
8006 );
8007
8008 let diff = https_adds(ConfigState::new().diff(&state));
8009 assert!(
8010 diff.contains(&(mtls_address, true)) && !diff.contains(&(mtls_address, false)),
8011 "the reload diff must add the mTLS listener on the mTLS verb: {diff:?}"
8012 );
8013 }
8014
8015 #[test]
8020 fn a_reload_refreshes_the_client_auth_policy_of_a_running_listener() {
8021 let address = SocketAddress::new_v4(127, 0, 0, 1, 8443);
8022 let with_crl = |crl: &str| HttpsListenerConfig {
8023 client_auth: Some(ClientAuthMode::ClientAuthRequired as i32),
8024 client_ca_certificates: vec!["CLIENT CA PEM".to_owned()],
8025 client_ca_crls: vec![crl.to_owned()],
8026 ..make_https_listener(address)
8027 };
8028
8029 let mut state = ConfigState::new();
8030 assert_eq!(
8031 state.client_auth_refresh(&with_crl("CRL 1")),
8032 None,
8033 "a listener the state does not hold is added, not patched"
8034 );
8035 state
8036 .dispatch(&RequestType::add_https_listener(with_crl("CRL 1")).into())
8037 .expect("add the listener");
8038 assert_eq!(
8039 state.client_auth_refresh(&with_crl("CRL 1")),
8040 None,
8041 "an unchanged policy needs no patch"
8042 );
8043 assert!(
8044 matches!(
8045 state.dispatch(&RequestType::add_https_listener(with_crl("CRL 2")).into()),
8046 Err(StateError::Exists { .. })
8047 ),
8048 "precondition: replaying the add for a held address is refused"
8049 );
8050
8051 let patch = state
8052 .client_auth_refresh(&with_crl("CRL 2"))
8053 .expect("a refreshed CRL must produce a patch");
8054 assert_eq!(patch.address, address);
8055 assert_eq!(
8056 patch.client_auth_policy,
8057 Some(with_crl("CRL 2").client_auth_policy())
8058 );
8059 assert_eq!(
8060 UpdateHttpsListenerConfig {
8061 client_auth_policy: None,
8062 ..patch.clone()
8063 },
8064 UpdateHttpsListenerConfig {
8065 address,
8066 ..Default::default()
8067 },
8068 "the patch carries the policy and nothing else"
8069 );
8070
8071 state
8072 .dispatch(&RequestType::update_https_listener(patch).into())
8073 .expect("the state must apply the policy patch");
8074 let running = state
8075 .https_listeners
8076 .values()
8077 .next()
8078 .expect("the listener is still there");
8079 assert_eq!(running.client_ca_crls, vec!["CRL 2".to_owned()]);
8080 assert_eq!(
8081 state.client_auth_refresh(&with_crl("CRL 2")),
8082 None,
8083 "once applied, the same reload has nothing left to refresh"
8084 );
8085 }
8086
8087 fn make_https_listener(address: SocketAddress) -> HttpsListenerConfig {
8090 HttpsListenerConfig {
8091 address,
8092 sticky_name: "SOZUBALANCEID".to_owned(),
8093 front_timeout: 60,
8094 back_timeout: 30,
8095 connect_timeout: 3,
8096 request_timeout: 10,
8097 ..Default::default()
8098 }
8099 }
8100
8101 fn make_http_listener(address: SocketAddress) -> HttpListenerConfig {
8102 HttpListenerConfig {
8103 address,
8104 sticky_name: "SOZUBALANCEID".to_owned(),
8105 front_timeout: 60,
8106 back_timeout: 30,
8107 connect_timeout: 3,
8108 request_timeout: 10,
8109 ..Default::default()
8110 }
8111 }
8112
8113 fn make_tcp_listener(address: SocketAddress) -> TcpListenerConfig {
8114 TcpListenerConfig {
8115 address,
8116 front_timeout: 60,
8117 back_timeout: 30,
8118 connect_timeout: 3,
8119 ..Default::default()
8120 }
8121 }
8122
8123 fn make_udp_listener(address: SocketAddress, active: bool) -> UdpListenerConfig {
8124 UdpListenerConfig {
8125 interface: None,
8126 address,
8127 public_address: None,
8128 front_timeout: 30,
8129 back_timeout: 30,
8130 max_rx_datagram_size: 1500,
8131 max_flows: 0,
8132 active,
8133 }
8134 }
8135
8136 #[test]
8141 fn activation_rollback_restores_the_prior_flag_for_every_listener_type() {
8142 let mut state = ConfigState::new();
8143 let cases = [
8144 (ListenerType::Http, 8200),
8145 (ListenerType::Https, 8201),
8146 (ListenerType::Tcp, 8202),
8147 (ListenerType::Udp, 8203),
8148 ];
8149
8150 for (listener_type, port) in cases {
8151 let address = SocketAddress::new_v4(127, 0, 0, 1, port);
8152 let interface = Some(format!("test-{port}"));
8153 let proxy: i32 = listener_type.into();
8154 let add = match listener_type {
8155 ListenerType::Http => RequestType::AddHttpListener(with_interface(
8156 make_http_listener(address),
8157 |listener| listener.interface = interface.to_owned(),
8158 )),
8159 ListenerType::Https => RequestType::AddHttpsListener(with_interface(
8160 make_https_listener(address),
8161 |listener| listener.interface = interface.to_owned(),
8162 )),
8163 ListenerType::Tcp => RequestType::AddTcpListener(with_interface(
8164 make_tcp_listener(address),
8165 |listener| listener.interface = interface.to_owned(),
8166 )),
8167 ListenerType::Udp => RequestType::AddUdpListener(with_interface(
8168 make_udp_listener(address, false),
8169 |listener| listener.interface = interface.to_owned(),
8170 )),
8171 };
8172 state
8173 .dispatch(&add.into())
8174 .expect("the inactive listener must be recorded");
8175
8176 let activate = ActivateListener {
8177 address,
8178 proxy,
8179 from_scm: false,
8180 interface: interface.to_owned(),
8181 };
8182 let rollback = state
8183 .rollback_for(&RequestType::ActivateListener(activate.to_owned()))
8184 .expect("an inactive listener must receive an activation rollback");
8185 assert!(matches!(
8186 rollback.request_type,
8187 Some(RequestType::DeactivateListener(DeactivateListener {
8188 address: rollback_address,
8189 proxy,
8190 to_scm: false,
8191 interface: rollback_interface,
8192 })) if rollback_address == address
8193 && proxy == activate.proxy
8194 && rollback_interface == interface
8195 ));
8196
8197 state
8198 .dispatch(&RequestType::ActivateListener(activate.to_owned()).into())
8199 .expect("the listener must become active in the stored state");
8200 assert!(
8201 state
8202 .rollback_for(&RequestType::ActivateListener(activate))
8203 .is_none(),
8204 "repeating activation for an already-active {listener_type:?} listener must not create a deactivation rollback"
8205 );
8206 }
8207 }
8208
8209 #[test]
8219 fn test_udp_state_roundtrip() {
8220 let address = SocketAddress::new_v4(127, 0, 0, 1, 5353);
8221
8222 let mut state = ConfigState::default();
8223 state
8224 .dispatch(&RequestType::AddUdpListener(make_udp_listener(address, true)).into())
8225 .expect("could not add udp listener");
8226 state
8227 .dispatch(
8228 &RequestType::ActivateListener(ActivateListener {
8229 interface: None,
8230 address,
8231 proxy: ListenerType::Udp.into(),
8232 from_scm: false,
8233 })
8234 .into(),
8235 )
8236 .expect("could not activate udp listener");
8237 state
8238 .dispatch(
8239 &RequestType::AddUdpFrontend(RequestUdpFrontend {
8240 cluster_id: "udp_cluster".to_string(),
8241 address,
8242 tags: BTreeMap::from([("owner".to_string(), "team".to_string())]),
8243 })
8244 .into(),
8245 )
8246 .expect("could not add udp frontend");
8247
8248 assert_eq!(state.udp_listeners.len(), 1);
8249 assert!(state.udp_listeners[&address.into()].active);
8250 assert_eq!(
8251 state.udp_fronts.get("udp_cluster").map(Vec::len),
8252 Some(1usize)
8253 );
8254
8255 let logical = |s: &ConfigState| {
8260 let mut c = s.clone();
8261 c.request_counts.clear();
8262 c
8263 };
8264
8265 let mut replayed = ConfigState::default();
8267 for request in state.generate_requests() {
8268 replayed
8269 .dispatch(&request)
8270 .expect("could not replay generated request");
8271 }
8272 assert_eq!(
8273 logical(&state),
8274 logical(&replayed),
8275 "UDP listener + frontend must survive generate_requests → replay"
8276 );
8277
8278 let empty = ConfigState::default();
8280 let mut from_diff = ConfigState::default();
8281 for request in empty.diff(&state) {
8282 from_diff
8283 .dispatch(&request)
8284 .expect("could not replay diff request");
8285 }
8286 assert_eq!(
8287 logical(&state),
8288 logical(&from_diff),
8289 "diff(empty -> state) must reconstruct the UDP listener + frontend"
8290 );
8291
8292 let mut torn_down = state.clone();
8294 for request in state.diff(&empty) {
8295 torn_down
8296 .dispatch(&request)
8297 .expect("could not replay teardown diff request");
8298 }
8299 assert!(
8300 torn_down.udp_listeners.is_empty(),
8301 "diff(state -> empty) must remove the UDP listener"
8302 );
8303 assert!(
8304 torn_down
8305 .udp_fronts
8306 .get("udp_cluster")
8307 .map(Vec::is_empty)
8308 .unwrap_or(true),
8309 "diff(state -> empty) must remove the UDP frontend"
8310 );
8311
8312 state
8314 .dispatch(
8315 &RequestType::UpdateUdpListener(UpdateUdpListenerConfig {
8316 address,
8317 max_flows: Some(4096),
8318 front_timeout: Some(15),
8319 ..Default::default()
8320 })
8321 .into(),
8322 )
8323 .expect("could not update udp listener");
8324 let updated = &state.udp_listeners[&address.into()];
8325 assert_eq!(updated.max_flows, 4096);
8326 assert_eq!(updated.front_timeout, 15);
8327 assert_eq!(
8328 updated.back_timeout, 30,
8329 "unpatched field must be preserved"
8330 );
8331 }
8332
8333 #[test]
8348 fn test_tcp_sni_alpn_state_roundtrip() {
8349 let address = SocketAddress::new_v4(127, 0, 0, 1, 6443);
8350
8351 let mut state = ConfigState::default();
8352 state
8353 .dispatch(
8354 &RequestType::AddTcpListener(TcpListenerConfig {
8355 address,
8356 front_timeout: 60,
8357 back_timeout: 30,
8358 connect_timeout: 3,
8359 active: true,
8360 sni_preread_timeout: Some(2),
8361 sni_preread_max_bytes: Some(8192),
8362 ..Default::default()
8363 })
8364 .into(),
8365 )
8366 .expect("could not add tcp listener");
8367 state
8368 .dispatch(
8369 &RequestType::ActivateListener(ActivateListener {
8370 interface: None,
8371 address,
8372 proxy: ListenerType::Tcp.into(),
8373 from_scm: false,
8374 })
8375 .into(),
8376 )
8377 .expect("could not activate tcp listener");
8378
8379 let frontend_a = RequestTcpFrontend {
8380 cluster_id: "cluster_sni_a".to_string(),
8381 address,
8382 tags: BTreeMap::new(),
8383 sni: Some("example.com".to_string()),
8384 alpn: vec!["h2".to_string()],
8385 };
8386 let frontend_b = RequestTcpFrontend {
8387 cluster_id: "cluster_sni_b".to_string(),
8388 address,
8389 tags: BTreeMap::new(),
8390 sni: Some("other.example.com".to_string()),
8391 alpn: vec![],
8392 };
8393 state
8394 .dispatch(&RequestType::AddTcpFrontend(frontend_a.clone()).into())
8395 .expect("could not add sni-a tcp frontend");
8396 state
8397 .dispatch(&RequestType::AddTcpFrontend(frontend_b.clone()).into())
8398 .expect("could not add sni-b tcp frontend");
8399
8400 assert_eq!(state.tcp_listeners.len(), 1);
8401 assert!(state.tcp_listeners[&address.into()].active);
8402 assert_eq!(
8403 state.tcp_listeners[&address.into()].sni_preread_timeout,
8404 Some(2)
8405 );
8406 assert_eq!(
8407 state.tcp_listeners[&address.into()].sni_preread_max_bytes,
8408 Some(8192)
8409 );
8410 assert_eq!(state.count_tcp_frontends_raw(), 2);
8411
8412 let logical = |s: &ConfigState| {
8417 let mut c = s.clone();
8418 c.request_counts.clear();
8419 c
8420 };
8421
8422 let mut replayed = ConfigState::default();
8424 for request in state.generate_requests() {
8425 replayed
8426 .dispatch(&request)
8427 .expect("could not replay generated request");
8428 }
8429 assert_eq!(
8430 logical(&state),
8431 logical(&replayed),
8432 "SNI/ALPN TCP listener + frontends must survive generate_requests → replay"
8433 );
8434
8435 let empty = ConfigState::default();
8437 let mut from_diff = ConfigState::default();
8438 for request in empty.diff(&state) {
8439 from_diff
8440 .dispatch(&request)
8441 .expect("could not replay diff request");
8442 }
8443 assert_eq!(
8444 logical(&state),
8445 logical(&from_diff),
8446 "diff(empty -> state) must reconstruct the tcp listener + both sni frontends"
8447 );
8448
8449 let mut torn_down = state.clone();
8451 for request in state.diff(&empty) {
8452 torn_down
8453 .dispatch(&request)
8454 .expect("could not replay teardown diff request");
8455 }
8456 assert!(
8457 torn_down.tcp_listeners.is_empty(),
8458 "diff(state -> empty) must remove the tcp listener"
8459 );
8460 assert_eq!(
8461 torn_down.count_tcp_frontends_raw(),
8462 0,
8463 "diff(state -> empty) must remove both sni frontends"
8464 );
8465
8466 let mismatched_removal = state.dispatch(
8470 &RequestType::RemoveTcpFrontend(RequestTcpFrontend {
8471 cluster_id: "cluster_sni_a".to_string(),
8472 address,
8473 tags: BTreeMap::new(),
8474 sni: None,
8475 alpn: vec![],
8476 })
8477 .into(),
8478 );
8479 assert!(
8480 mismatched_removal.is_err(),
8481 "removing by address alone (no matching sni/alpn) must be rejected"
8482 );
8483 assert_eq!(
8484 state.count_tcp_frontends_raw(),
8485 2,
8486 "a rejected mismatched removal must not change either sibling"
8487 );
8488
8489 state
8492 .dispatch(&RequestType::RemoveTcpFrontend(frontend_a).into())
8493 .expect("could not remove sni-a tcp frontend");
8494 assert_eq!(state.count_tcp_frontends_raw(), 1);
8495 let remaining = state
8496 .tcp_fronts
8497 .get("cluster_sni_b")
8498 .expect("cluster_sni_b bucket must survive");
8499 assert_eq!(remaining.len(), 1);
8500 assert_eq!(remaining[0].sni, Some("other.example.com".to_string()));
8501 }
8502
8503 const TCP_PERM_CLUSTER: &str = "tcp_perm";
8504
8505 fn tcp_frontend_request(
8507 cluster_id: &str,
8508 address: SocketAddress,
8509 sni: Option<&str>,
8510 alpn: &[&str],
8511 route: &str,
8512 ) -> RequestTcpFrontend {
8513 RequestTcpFrontend {
8514 cluster_id: cluster_id.to_string(),
8515 address,
8516 tags: BTreeMap::from([("route".to_string(), route.to_string())]),
8517 sni: sni.map(str::to_string),
8518 alpn: alpn.iter().map(|protocol| protocol.to_string()).collect(),
8519 }
8520 }
8521
8522 fn tcp_permutation_fixture() -> [RequestTcpFrontend; 4] {
8525 [
8526 tcp_frontend_request(
8527 TCP_PERM_CLUSTER,
8528 SocketAddress::new_v4(127, 0, 0, 1, 7001),
8529 None,
8530 &[],
8531 "A",
8532 ),
8533 tcp_frontend_request(
8534 TCP_PERM_CLUSTER,
8535 SocketAddress::new_v4(127, 0, 0, 1, 7002),
8536 Some("b.example.com"),
8537 &["h2"],
8538 "B",
8539 ),
8540 tcp_frontend_request(
8541 TCP_PERM_CLUSTER,
8542 SocketAddress::new_v4(127, 0, 0, 1, 7002),
8543 Some("b.example.com"),
8544 &["http/1.1"],
8545 "C",
8546 ),
8547 tcp_frontend_request(
8548 TCP_PERM_CLUSTER,
8549 SocketAddress::new_v4(127, 0, 0, 1, 7003),
8550 Some("d.example.com"),
8551 &[],
8552 "D",
8553 ),
8554 ]
8555 }
8556
8557 fn tcp_state(requests: &[RequestTcpFrontend]) -> ConfigState {
8560 let mut state = ConfigState::default();
8561 for request in requests {
8562 state
8563 .dispatch(&RequestType::AddTcpFrontend(request.clone()).into())
8564 .expect("could not add the test tcp frontend");
8565 }
8566 state
8567 }
8568
8569 fn tcp_route_order(state: &ConfigState, cluster_id: &str) -> Vec<String> {
8571 state
8572 .tcp_fronts
8573 .get(cluster_id)
8574 .map(|bucket| {
8575 bucket
8576 .iter()
8577 .map(|front| front.tags["route"].clone())
8578 .collect()
8579 })
8580 .unwrap_or_default()
8581 }
8582
8583 fn tcp_semantic(state: &ConfigState) -> BTreeMap<String, BTreeSet<TcpFrontend>> {
8585 state
8586 .tcp_fronts
8587 .iter()
8588 .filter(|(_, bucket)| !bucket.is_empty())
8589 .map(|(cluster_id, bucket)| (cluster_id.to_string(), bucket.iter().cloned().collect()))
8590 .collect()
8591 }
8592
8593 fn diff_leaving_inputs_unchanged(from: &ConfigState, to: &ConfigState) -> Vec<Request> {
8595 let from_before = from.clone();
8596 let to_before = to.clone();
8597 let requests = from.diff(to);
8598 assert!(
8599 *from == from_before,
8600 "diff must leave the source state unchanged"
8601 );
8602 assert!(
8603 *to == to_before,
8604 "diff must leave the target state unchanged"
8605 );
8606 assert_eq!(
8607 from.tcp_fronts, from_before.tcp_fronts,
8608 "diff must preserve the source stored tcp order"
8609 );
8610 assert_eq!(
8611 to.tcp_fronts, to_before.tcp_fronts,
8612 "diff must preserve the target stored tcp order"
8613 );
8614 requests
8615 }
8616
8617 #[test]
8620 fn diff_tcp_pure_permutation_of_complete_frontends_emits_no_orders() {
8621 let [a, b, c, d] = tcp_permutation_fixture();
8622 let original = tcp_state(&[a.clone(), b.clone(), c.clone(), d.clone()]);
8623 let permuted = tcp_state(&[d, c, a, b]);
8624
8625 assert_eq!(
8626 tcp_route_order(&original, TCP_PERM_CLUSTER),
8627 ["A", "B", "C", "D"]
8628 );
8629 assert_eq!(
8630 tcp_route_order(&permuted, TCP_PERM_CLUSTER),
8631 ["D", "C", "A", "B"]
8632 );
8633 assert_ne!(
8634 original.tcp_fronts, permuted.tcp_fronts,
8635 "the stored vectors must differ in order for this test to be meaningful"
8636 );
8637 assert_eq!(tcp_semantic(&original), tcp_semantic(&permuted));
8638
8639 assert!(
8640 diff_leaving_inputs_unchanged(&original, &permuted).is_empty(),
8641 "a pure permutation must emit no orders"
8642 );
8643 assert!(
8644 diff_leaving_inputs_unchanged(&permuted, &original).is_empty(),
8645 "the reverse permutation must emit no orders"
8646 );
8647 }
8648
8649 #[test]
8651 fn diff_tcp_prepended_frontend_emits_only_the_semantic_addition() {
8652 let [a, _, c, _] = tcp_permutation_fixture();
8653 let from = tcp_state(std::slice::from_ref(&a));
8654 let to = tcp_state(&[c.clone(), a.clone()]);
8655 assert_eq!(tcp_route_order(&to, TCP_PERM_CLUSTER), ["C", "A"]);
8656
8657 let requests = diff_leaving_inputs_unchanged(&from, &to);
8658 let expected: Request = RequestType::AddTcpFrontend(c.clone()).into();
8659 assert_eq!(requests, vec![expected]);
8660
8661 let replayed = replay_diff(&from, &requests);
8662 assert_eq!(
8663 tcp_route_order(&replayed, TCP_PERM_CLUSTER),
8664 ["A", "C"],
8665 "replay appends the added frontend to the stored bucket"
8666 );
8667 assert_eq!(tcp_semantic(&replayed), tcp_semantic(&to));
8668
8669 let reverse = diff_leaving_inputs_unchanged(&to, &from);
8670 let expected_removal: Request = RequestType::RemoveTcpFrontend(c).into();
8671 assert_eq!(reverse, vec![expected_removal]);
8672 let reverted = replay_diff(&to, &reverse);
8673 assert_eq!(tcp_route_order(&reverted, TCP_PERM_CLUSTER), ["A"]);
8674 assert_eq!(tcp_semantic(&reverted), tcp_semantic(&from));
8675 }
8676
8677 #[test]
8681 fn diff_tcp_reconstruction_from_empty_replays_to_same_semantic_sets() {
8682 let [a, b, c, _] = tcp_permutation_fixture();
8683 let cluster_x = "tcp_x";
8684 let cluster_y = "tcp_y";
8685 let mut x_a = a;
8686 let mut x_b = b;
8687 let mut x_c = c;
8688 x_a.cluster_id = cluster_x.to_string();
8689 x_b.cluster_id = cluster_x.to_string();
8690 x_c.cluster_id = cluster_x.to_string();
8691 let y_d = tcp_frontend_request(
8692 cluster_y,
8693 SocketAddress::new_v4(127, 0, 0, 1, 7003),
8694 Some("d.example.com"),
8695 &[],
8696 "D",
8697 );
8698 let y_e = tcp_frontend_request(
8699 cluster_y,
8700 SocketAddress::new_v4(127, 0, 0, 1, 7004),
8701 None,
8702 &[],
8703 "E",
8704 );
8705
8706 let empty = ConfigState::default();
8707 let target = tcp_state(&[x_c, y_e, x_a, y_d, x_b]);
8708 assert_eq!(tcp_route_order(&target, cluster_x), ["C", "A", "B"]);
8709 assert_eq!(tcp_route_order(&target, cluster_y), ["E", "D"]);
8710
8711 let requests = diff_leaving_inputs_unchanged(&empty, &target);
8712 assert_eq!(requests.len(), 5);
8713 assert!(
8714 requests.iter().all(|request| matches!(
8715 request.request_type,
8716 Some(RequestType::AddTcpFrontend(_))
8717 )),
8718 "reconstruction from empty must only add tcp frontends"
8719 );
8720
8721 let replayed = replay_diff(&empty, &requests);
8722 assert_eq!(tcp_semantic(&replayed), tcp_semantic(&target));
8723 assert_eq!(replayed.tcp_fronts.len(), 2);
8724 assert!(
8725 replayed
8726 .tcp_fronts
8727 .values()
8728 .all(|bucket| !bucket.is_empty()),
8729 "replay must not introduce empty buckets"
8730 );
8731
8732 let teardown = diff_leaving_inputs_unchanged(&target, &empty);
8733 assert_eq!(teardown.len(), 5);
8734 let torn_down = replay_diff(&target, &teardown);
8735 assert!(tcp_semantic(&torn_down).is_empty());
8736 assert_eq!(tcp_semantic(&torn_down), tcp_semantic(&empty));
8737 }
8738
8739 #[test]
8742 fn diff_tcp_self_is_empty_and_preserves_stored_order() {
8743 let [a, b, c, d] = tcp_permutation_fixture();
8744 let state = tcp_state(&[d, a, c, b]);
8745
8746 let bucket = state
8747 .tcp_fronts
8748 .get(TCP_PERM_CLUSTER)
8749 .expect("the test cluster bucket must exist");
8750 assert!(
8751 !bucket.windows(2).all(|pair| pair[0] <= pair[1]),
8752 "the fixture bucket must be deliberately non-sorted"
8753 );
8754 assert_eq!(
8755 tcp_route_order(&state, TCP_PERM_CLUSTER),
8756 ["D", "A", "C", "B"]
8757 );
8758
8759 let before = state.clone();
8760 let stored_before = state.tcp_fronts.clone();
8761 assert!(
8762 diff_leaving_inputs_unchanged(&state, &state).is_empty(),
8763 "a state diffed against itself must emit no orders"
8764 );
8765 assert!(state == before, "self-diff must not modify the state");
8766 assert_eq!(state.tcp_fronts, stored_before);
8767 assert_eq!(
8768 tcp_route_order(&state, TCP_PERM_CLUSTER),
8769 ["D", "A", "C", "B"]
8770 );
8771 }
8772
8773 #[test]
8777 fn generate_requests_replay_preserves_non_sorted_tcp_bucket_order() {
8778 let [a, b, c, d] = tcp_permutation_fixture();
8779 let state = tcp_state(&[d, a, c, b]);
8780
8781 let bucket = state
8782 .tcp_fronts
8783 .get(TCP_PERM_CLUSTER)
8784 .expect("the test cluster bucket must exist");
8785 assert!(
8786 !bucket.windows(2).all(|pair| pair[0] <= pair[1]),
8787 "the fixture bucket must be deliberately non-sorted"
8788 );
8789
8790 let mut replayed = ConfigState::default();
8791 for request in state.generate_requests() {
8792 replayed
8793 .dispatch(&request)
8794 .expect("could not replay generated request");
8795 }
8796
8797 let logical = |s: &ConfigState| {
8800 let mut c = s.clone();
8801 c.request_counts.clear();
8802 c
8803 };
8804 assert!(
8805 logical(&state) == logical(&replayed),
8806 "generate_requests -> replay must reproduce the state"
8807 );
8808 assert_eq!(
8809 replayed.tcp_fronts.get(TCP_PERM_CLUSTER),
8810 state.tcp_fronts.get(TCP_PERM_CLUSTER),
8811 "the replayed bucket must equal the original Vec, order included"
8812 );
8813 assert_eq!(
8814 tcp_route_order(&replayed, TCP_PERM_CLUSTER),
8815 ["D", "A", "C", "B"]
8816 );
8817 }
8818
8819 fn add_tcp_test_cluster(state: &mut ConfigState, cluster_id: &str) {
8830 state
8831 .dispatch(
8832 &RequestType::AddCluster(Cluster {
8833 cluster_id: cluster_id.to_string(),
8834 sticky_session: false,
8835 https_redirect: false,
8836 ..Default::default()
8837 })
8838 .into(),
8839 )
8840 .expect("could not add cluster");
8841 }
8842
8843 #[test]
8848 fn add_tcp_frontend_rejects_cross_cluster_duplicate_identity() {
8849 let address = SocketAddress::new_v4(127, 0, 0, 1, 9001);
8850 let mut state = ConfigState::new();
8851 add_tcp_test_cluster(&mut state, "cluster_x");
8852 add_tcp_test_cluster(&mut state, "cluster_y");
8853
8854 state
8855 .dispatch(
8856 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8857 cluster_id: "cluster_x".to_string(),
8858 address,
8859 tags: BTreeMap::new(),
8860 sni: Some("example.com".to_string()),
8861 alpn: vec!["h2".to_string()],
8862 })
8863 .into(),
8864 )
8865 .expect("could not add cluster_x tcp frontend");
8866
8867 let hash_before = state.hash_state();
8868 let count_before = state.count_tcp_frontends_raw();
8869
8870 let result = state.dispatch(
8871 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8872 cluster_id: "cluster_y".to_string(),
8873 address,
8874 tags: BTreeMap::new(),
8875 sni: Some("example.com".to_string()),
8876 alpn: vec!["h2".to_string()],
8877 })
8878 .into(),
8879 );
8880 assert!(
8881 result.is_err(),
8882 "the same (address, sni, alpn) under a different cluster_id must be rejected, \
8883 got: {result:?}"
8884 );
8885 assert_eq!(
8886 state.hash_state(),
8887 hash_before,
8888 "a rejected cross-cluster duplicate must not change the state hash"
8889 );
8890 assert_eq!(
8891 state.count_tcp_frontends_raw(),
8892 count_before,
8893 "a rejected cross-cluster duplicate must not change the frontend count"
8894 );
8895 assert_eq!(state.tcp_fronts.get("cluster_x").map(Vec::len), Some(1));
8896 assert_eq!(
8897 state.tcp_fronts.get("cluster_y").map(Vec::len).unwrap_or(0),
8898 0
8899 );
8900 }
8901
8902 #[test]
8905 fn add_tcp_frontend_rejects_reordered_alpn_duplicate() {
8906 let address = SocketAddress::new_v4(127, 0, 0, 1, 9002);
8907 let mut state = ConfigState::new();
8908 add_tcp_test_cluster(&mut state, "cluster_reorder");
8909
8910 state
8911 .dispatch(
8912 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8913 cluster_id: "cluster_reorder".to_string(),
8914 address,
8915 tags: BTreeMap::new(),
8916 sni: Some("example.com".to_string()),
8917 alpn: vec!["h2".to_string(), "http/1.1".to_string()],
8918 })
8919 .into(),
8920 )
8921 .expect("could not add first tcp frontend");
8922
8923 let hash_before = state.hash_state();
8924 let count_before = state.count_tcp_frontends_raw();
8925
8926 let result = state.dispatch(
8927 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8928 cluster_id: "cluster_reorder".to_string(),
8929 address,
8930 tags: BTreeMap::new(),
8931 sni: Some("example.com".to_string()),
8932 alpn: vec!["http/1.1".to_string(), "h2".to_string()],
8933 })
8934 .into(),
8935 );
8936 assert!(
8937 result.is_err(),
8938 "the same ALPN set in a different order must be rejected as a duplicate \
8939 identity, got: {result:?}"
8940 );
8941 assert_eq!(state.hash_state(), hash_before);
8942 assert_eq!(state.count_tcp_frontends_raw(), count_before);
8943 assert_eq!(
8944 state.tcp_fronts.get("cluster_reorder").map(Vec::len),
8945 Some(1)
8946 );
8947 }
8948
8949 #[test]
8953 fn remove_tcp_frontend_matches_reordered_alpn() {
8954 let address = SocketAddress::new_v4(127, 0, 0, 1, 9003);
8955 let mut state = ConfigState::new();
8956 add_tcp_test_cluster(&mut state, "cluster_remove_reorder");
8957
8958 state
8959 .dispatch(
8960 &RequestType::AddTcpFrontend(RequestTcpFrontend {
8961 cluster_id: "cluster_remove_reorder".to_string(),
8962 address,
8963 tags: BTreeMap::new(),
8964 sni: Some("example.com".to_string()),
8965 alpn: vec!["h2".to_string(), "http/1.1".to_string()],
8966 })
8967 .into(),
8968 )
8969 .expect("could not add tcp frontend");
8970
8971 let result = state.dispatch(
8972 &RequestType::RemoveTcpFrontend(RequestTcpFrontend {
8973 cluster_id: "cluster_remove_reorder".to_string(),
8974 address,
8975 tags: BTreeMap::new(),
8976 sni: Some("example.com".to_string()),
8977 alpn: vec!["http/1.1".to_string(), "h2".to_string()],
8978 })
8979 .into(),
8980 );
8981 assert!(
8982 result.is_ok(),
8983 "removing with the same ALPN set in a different order must succeed, got: {result:?}"
8984 );
8985 assert_eq!(state.count_tcp_frontends_raw(), 0);
8986 }
8987
8988 #[test]
8992 fn add_tcp_frontend_rejects_alpn_overlap_across_clusters_same_sni() {
8993 let address = SocketAddress::new_v4(127, 0, 0, 1, 9004);
8994 let mut state = ConfigState::new();
8995 add_tcp_test_cluster(&mut state, "cluster_overlap_a");
8996 add_tcp_test_cluster(&mut state, "cluster_overlap_b");
8997
8998 state
8999 .dispatch(
9000 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9001 cluster_id: "cluster_overlap_a".to_string(),
9002 address,
9003 tags: BTreeMap::new(),
9004 sni: Some("example.com".to_string()),
9005 alpn: vec!["h2".to_string()],
9006 })
9007 .into(),
9008 )
9009 .expect("could not add cluster_overlap_a tcp frontend");
9010
9011 let hash_before = state.hash_state();
9012 let count_before = state.count_tcp_frontends_raw();
9013
9014 let result = state.dispatch(
9015 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9016 cluster_id: "cluster_overlap_b".to_string(),
9017 address,
9018 tags: BTreeMap::new(),
9019 sni: Some("example.com".to_string()),
9020 alpn: vec!["h2".to_string(), "http/1.1".to_string()],
9021 })
9022 .into(),
9023 );
9024 assert!(
9025 result.is_err(),
9026 "an overlapping ALPN set on the same (address, sni) under a different cluster_id \
9027 must be rejected, got: {result:?}"
9028 );
9029 assert_eq!(state.hash_state(), hash_before);
9030 assert_eq!(state.count_tcp_frontends_raw(), count_before);
9031 assert_eq!(
9032 state
9033 .tcp_fronts
9034 .get("cluster_overlap_b")
9035 .map(Vec::len)
9036 .unwrap_or(0),
9037 0
9038 );
9039 }
9040
9041 #[test]
9044 fn add_tcp_frontend_rejects_no_sni_sni_mixing_both_directions() {
9045 let address_a = SocketAddress::new_v4(127, 0, 0, 1, 9005);
9046 let address_b = SocketAddress::new_v4(127, 0, 0, 1, 9006);
9047 let mut state = ConfigState::new();
9048 add_tcp_test_cluster(&mut state, "cluster_mix_1");
9049 add_tcp_test_cluster(&mut state, "cluster_mix_2");
9050 add_tcp_test_cluster(&mut state, "cluster_mix_3");
9051 add_tcp_test_cluster(&mut state, "cluster_mix_4");
9052
9053 state
9056 .dispatch(
9057 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9058 cluster_id: "cluster_mix_1".to_string(),
9059 address: address_a,
9060 tags: BTreeMap::new(),
9061 sni: None,
9062 alpn: vec![],
9063 })
9064 .into(),
9065 )
9066 .expect("could not add no-sni tcp frontend");
9067 let hash_before_a = state.hash_state();
9068 let count_before_a = state.count_tcp_frontends_raw();
9069 let result_a = state.dispatch(
9070 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9071 cluster_id: "cluster_mix_2".to_string(),
9072 address: address_a,
9073 tags: BTreeMap::new(),
9074 sni: Some("example.com".to_string()),
9075 alpn: vec![],
9076 })
9077 .into(),
9078 );
9079 assert!(
9080 result_a.is_err(),
9081 "an SNI-scoped frontend must not be added to a listener with an existing no-SNI \
9082 front, got: {result_a:?}"
9083 );
9084 assert_eq!(state.hash_state(), hash_before_a);
9085 assert_eq!(state.count_tcp_frontends_raw(), count_before_a);
9086
9087 state
9090 .dispatch(
9091 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9092 cluster_id: "cluster_mix_3".to_string(),
9093 address: address_b,
9094 tags: BTreeMap::new(),
9095 sni: Some("example.com".to_string()),
9096 alpn: vec![],
9097 })
9098 .into(),
9099 )
9100 .expect("could not add sni-scoped tcp frontend");
9101 let hash_before_b = state.hash_state();
9102 let count_before_b = state.count_tcp_frontends_raw();
9103 let result_b = state.dispatch(
9104 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9105 cluster_id: "cluster_mix_4".to_string(),
9106 address: address_b,
9107 tags: BTreeMap::new(),
9108 sni: None,
9109 alpn: vec![],
9110 })
9111 .into(),
9112 );
9113 assert!(
9114 result_b.is_err(),
9115 "a no-SNI frontend must not be added to a listener with existing SNI-scoped \
9116 fronts, got: {result_b:?}"
9117 );
9118 assert_eq!(state.hash_state(), hash_before_b);
9119 assert_eq!(state.count_tcp_frontends_raw(), count_before_b);
9120 }
9121
9122 #[test]
9127 fn add_tcp_frontend_rejects_malformed_sni_shape() {
9128 let address = SocketAddress::new_v4(127, 0, 0, 1, 9007);
9129 let mut state = ConfigState::new();
9130 add_tcp_test_cluster(&mut state, "cluster_malformed");
9131
9132 let hash_before = state.hash_state();
9133 let count_before = state.count_tcp_frontends_raw();
9134
9135 let result = state.dispatch(
9136 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9137 cluster_id: "cluster_malformed".to_string(),
9138 address,
9139 tags: BTreeMap::new(),
9140 sni: Some(".example.com".to_string()),
9142 alpn: vec![],
9143 })
9144 .into(),
9145 );
9146 assert!(
9147 result.is_err(),
9148 "a malformed SNI pattern must be rejected by the master, not merely by \
9149 config-load, got: {result:?}"
9150 );
9151 assert_eq!(state.hash_state(), hash_before);
9152 assert_eq!(state.count_tcp_frontends_raw(), count_before);
9153 assert_eq!(
9154 state
9155 .tcp_fronts
9156 .get("cluster_malformed")
9157 .map(Vec::len)
9158 .unwrap_or(0),
9159 0
9160 );
9161 }
9162
9163 #[test]
9169 fn add_tcp_frontend_rejects_alpn_without_sni() {
9170 let address = SocketAddress::new_v4(127, 0, 0, 1, 9009);
9171 let mut state = ConfigState::new();
9172 add_tcp_test_cluster(&mut state, "cluster_alpn_no_sni");
9173
9174 let hash_before = state.hash_state();
9175 let count_before = state.count_tcp_frontends_raw();
9176
9177 let result = state.dispatch(
9178 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9179 cluster_id: "cluster_alpn_no_sni".to_string(),
9180 address,
9181 tags: BTreeMap::new(),
9182 sni: None,
9183 alpn: vec!["h2".to_string()],
9184 })
9185 .into(),
9186 );
9187 assert!(
9188 result.is_err(),
9189 "a non-empty alpn without sni must be rejected by the master, got: {result:?}"
9190 );
9191 assert_eq!(
9192 state.hash_state(),
9193 hash_before,
9194 "a rejected alpn-without-sni add must not change the state hash"
9195 );
9196 assert_eq!(
9197 state.count_tcp_frontends_raw(),
9198 count_before,
9199 "a rejected alpn-without-sni add must not change the frontend count"
9200 );
9201 assert_eq!(
9202 state
9203 .tcp_fronts
9204 .get("cluster_alpn_no_sni")
9205 .map(Vec::len)
9206 .unwrap_or(0),
9207 0
9208 );
9209 }
9210
9211 #[test]
9217 fn add_tcp_frontend_accepts_exact_and_wildcard_catch_all_siblings() {
9218 let address = SocketAddress::new_v4(127, 0, 0, 1, 9008);
9219 let mut state = ConfigState::new();
9220 add_tcp_test_cluster(&mut state, "cluster_exact_catchall");
9221 add_tcp_test_cluster(&mut state, "cluster_wildcard_catchall");
9222
9223 state
9224 .dispatch(
9225 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9226 cluster_id: "cluster_exact_catchall".to_string(),
9227 address,
9228 tags: BTreeMap::new(),
9229 sni: Some("example.com".to_string()),
9230 alpn: vec![],
9231 })
9232 .into(),
9233 )
9234 .expect("could not add exact catch-all tcp frontend");
9235
9236 let result = state.dispatch(
9237 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9238 cluster_id: "cluster_wildcard_catchall".to_string(),
9239 address,
9240 tags: BTreeMap::new(),
9241 sni: Some("*.example.com".to_string()),
9242 alpn: vec![],
9243 })
9244 .into(),
9245 );
9246 assert!(
9247 result.is_ok(),
9248 "an exact catch-all and a wildcard catch-all on the same address must both be \
9249 accepted, got: {result:?}"
9250 );
9251 assert_eq!(state.count_tcp_frontends_raw(), 2);
9252 }
9253
9254 #[test]
9258 fn list_frontends_includes_udp() {
9259 let tcp_addr = SocketAddress::new_v4(0, 0, 0, 0, 6379);
9260 let udp_addr = SocketAddress::new_v4(0, 0, 0, 0, 5353);
9261
9262 let mut state = ConfigState::default();
9263 state
9264 .dispatch(
9265 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9266 cluster_id: "tcp_cluster".to_string(),
9267 address: tcp_addr,
9268 ..Default::default()
9269 })
9270 .into(),
9271 )
9272 .expect("could not add tcp frontend");
9273 state
9274 .dispatch(
9275 &RequestType::AddUdpFrontend(RequestUdpFrontend {
9276 cluster_id: "udp_cluster".to_string(),
9277 address: udp_addr,
9278 ..Default::default()
9279 })
9280 .into(),
9281 )
9282 .expect("could not add udp frontend");
9283
9284 let all = state.list_frontends(FrontendFilters::default());
9286 assert_eq!(all.tcp_frontends.len(), 1, "tcp frontend must be listed");
9287 assert_eq!(
9288 all.udp_frontends.len(),
9289 1,
9290 "udp frontend must be listed under the default all-pass path"
9291 );
9292 assert_eq!(all.udp_frontends[0].cluster_id, "udp_cluster");
9293 assert_eq!(all.udp_frontends[0].address, udp_addr);
9294
9295 let tcp_only = state.list_frontends(FrontendFilters {
9297 tcp: true,
9298 ..Default::default()
9299 });
9300 assert_eq!(tcp_only.tcp_frontends.len(), 1);
9301 assert_eq!(
9302 tcp_only.udp_frontends.len(),
9303 1,
9304 "udp frontends ride the tcp filter"
9305 );
9306
9307 let http_only = state.list_frontends(FrontendFilters {
9309 http: true,
9310 ..Default::default()
9311 });
9312 assert!(http_only.tcp_frontends.is_empty());
9313 assert!(http_only.udp_frontends.is_empty());
9314
9315 let domain_filtered = state.list_frontends(FrontendFilters {
9317 domain: Some("example.com".to_string()),
9318 ..Default::default()
9319 });
9320 assert!(domain_filtered.tcp_frontends.is_empty());
9321 assert!(domain_filtered.udp_frontends.is_empty());
9322 }
9323
9324 #[test]
9328 fn list_frontends_filters_by_cluster_id() {
9329 let http_addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
9330 let https_addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9331 let mut state = ConfigState::default();
9332 for (cluster_id, hostname) in [
9333 (Some(String::from("wanted")), "wanted.example.com"),
9334 (Some(String::from("other")), "other.example.com"),
9335 (None, "denied.example.com"),
9337 ] {
9338 state
9339 .dispatch(
9340 &RequestType::AddHttpFrontend(RequestHttpFrontend {
9341 cluster_id: cluster_id.clone(),
9342 hostname: hostname.to_string(),
9343 path: PathRule::prefix(String::from("/")),
9344 address: http_addr,
9345 position: RulePosition::Tree.into(),
9346 ..Default::default()
9347 })
9348 .into(),
9349 )
9350 .expect("could not add http frontend");
9351 state
9352 .dispatch(
9353 &RequestType::AddHttpsFrontend(RequestHttpFrontend {
9354 cluster_id,
9355 hostname: hostname.to_string(),
9356 path: PathRule::prefix(String::from("/")),
9357 address: https_addr,
9358 position: RulePosition::Tree.into(),
9359 ..Default::default()
9360 })
9361 .into(),
9362 )
9363 .expect("could not add https frontend");
9364 }
9365 for (cluster_id, port) in [("wanted", 6379u16), ("other", 6380)] {
9367 state
9368 .dispatch(
9369 &RequestType::AddTcpFrontend(RequestTcpFrontend {
9370 cluster_id: cluster_id.to_string(),
9371 address: SocketAddress::new_v4(0, 0, 0, 0, port),
9372 ..Default::default()
9373 })
9374 .into(),
9375 )
9376 .expect("could not add tcp frontend");
9377 state
9378 .dispatch(
9379 &RequestType::AddUdpFrontend(RequestUdpFrontend {
9380 cluster_id: cluster_id.to_string(),
9381 address: SocketAddress::new_v4(0, 0, 0, 0, port + 1000),
9382 ..Default::default()
9383 })
9384 .into(),
9385 )
9386 .expect("could not add udp frontend");
9387 }
9388
9389 let all = state.list_frontends(FrontendFilters::default());
9391 assert_eq!(all.http_frontends.len(), 3);
9392 assert_eq!(all.https_frontends.len(), 3);
9393 assert_eq!(all.tcp_frontends.len(), 2);
9394 assert_eq!(all.udp_frontends.len(), 2);
9395
9396 let wanted = state.list_frontends(FrontendFilters {
9399 cluster_id: Some("wanted".to_string()),
9400 ..Default::default()
9401 });
9402 assert_eq!(wanted.http_frontends.len(), 1);
9403 assert_eq!(wanted.https_frontends.len(), 1);
9404 assert_eq!(wanted.tcp_frontends.len(), 1);
9405 assert_eq!(wanted.udp_frontends.len(), 1);
9406 assert_eq!(
9407 wanted.http_frontends[0].cluster_id.as_deref(),
9408 Some("wanted")
9409 );
9410 assert_eq!(wanted.tcp_frontends[0].cluster_id, "wanted");
9411 assert_eq!(wanted.udp_frontends[0].cluster_id, "wanted");
9412 assert!(
9413 wanted
9414 .http_frontends
9415 .iter()
9416 .chain(wanted.https_frontends.iter())
9417 .all(|f| f.hostname == "wanted.example.com"),
9418 "no frontend of another cluster may leak through the filter"
9419 );
9420
9421 let unknown = state.list_frontends(FrontendFilters {
9423 cluster_id: Some("nope".to_string()),
9424 ..Default::default()
9425 });
9426 assert!(unknown.http_frontends.is_empty());
9427 assert!(unknown.https_frontends.is_empty());
9428 assert!(unknown.tcp_frontends.is_empty());
9429 assert!(unknown.udp_frontends.is_empty());
9430
9431 let conflicting = state.list_frontends(FrontendFilters {
9433 cluster_id: Some("wanted".to_string()),
9434 domain: Some("other.example.com".to_string()),
9435 ..Default::default()
9436 });
9437 assert!(conflicting.http_frontends.is_empty());
9438 assert!(conflicting.https_frontends.is_empty());
9439
9440 let https_only = state.list_frontends(FrontendFilters {
9442 https: true,
9443 cluster_id: Some("wanted".to_string()),
9444 ..Default::default()
9445 });
9446 assert!(https_only.http_frontends.is_empty());
9447 assert_eq!(https_only.https_frontends.len(), 1);
9448 assert!(https_only.tcp_frontends.is_empty());
9449 assert!(https_only.udp_frontends.is_empty());
9450 }
9451
9452 #[test]
9457 fn update_https_listener_happy_path_h2_knobs() {
9458 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9459 let mut state = ConfigState::new();
9460 state
9461 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
9462 .unwrap();
9463
9464 let patch = UpdateHttpsListenerConfig {
9465 address: addr,
9466 h2_max_rst_stream_per_window: Some(50),
9467 h2_max_ping_per_window: Some(20),
9468 ..Default::default()
9469 };
9470 state
9471 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9472 .expect("update must succeed");
9473
9474 let listener = state
9475 .https_listeners
9476 .get(&ListenerKey::from(addr))
9477 .expect("listener must be present");
9478 assert_eq!(listener.h2_max_rst_stream_per_window, Some(50));
9479 assert_eq!(listener.h2_max_ping_per_window, Some(20));
9480 assert_eq!(listener.front_timeout, 60);
9482 assert_eq!(listener.h2_max_settings_per_window, None);
9483 }
9484
9485 #[test]
9488 fn update_https_listener_not_found() {
9489 let mut state = ConfigState::new();
9490 let patch = UpdateHttpsListenerConfig {
9491 address: SocketAddress::new_v4(1, 2, 3, 4, 9999),
9492 h2_max_rst_stream_per_window: Some(50),
9493 ..Default::default()
9494 };
9495 let err = state
9496 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9497 .unwrap_err();
9498 assert!(
9499 matches!(
9500 err,
9501 StateError::NotFound {
9502 kind: ObjectKind::HttpsListener,
9503 ..
9504 }
9505 ),
9506 "expected NotFound, got: {err}"
9507 );
9508 }
9509
9510 #[test]
9513 fn update_https_listener_noop_patch() {
9514 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9515 let mut state = ConfigState::new();
9516 let original = make_https_listener(addr);
9517 state
9518 .dispatch(&RequestType::AddHttpsListener(original.clone()).into())
9519 .unwrap();
9520
9521 let patch = UpdateHttpsListenerConfig {
9522 address: addr,
9523 ..Default::default()
9524 };
9525 state
9526 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9527 .expect("no-op patch must succeed");
9528
9529 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
9530 assert_eq!(listener.front_timeout, original.front_timeout);
9531 assert_eq!(
9532 listener.h2_max_rst_stream_per_window,
9533 original.h2_max_rst_stream_per_window
9534 );
9535 }
9536
9537 #[test]
9539 fn update_https_listener_invalid_value_flood_knob_zero() {
9540 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
9541 let mut state = ConfigState::new();
9542 state
9543 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
9544 .unwrap();
9545
9546 let patch = UpdateHttpsListenerConfig {
9547 address: addr,
9548 h2_max_rst_stream_per_window: Some(0),
9549 ..Default::default()
9550 };
9551 let err = state
9552 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
9553 .unwrap_err();
9554 assert!(
9555 matches!(
9556 err,
9557 StateError::InvalidValue {
9558 field: "h2_max_rst_stream_per_window",
9559 ..
9560 }
9561 ),
9562 "expected InvalidValue for flood knob 0, got: {err}"
9563 );
9564 }
9565
9566 #[test]
9572 fn a_tcp_frontend_and_an_affinity_key_never_share_a_cluster() {
9573 use crate::proto::command::RequestTcpFrontend;
9574
9575 let keyed = |cluster_id: &str| -> Request {
9576 RequestType::AddCluster(Cluster {
9577 cluster_id: cluster_id.to_owned(),
9578 affinity_header: Some(String::from("X-Tenant")),
9579 ..Default::default()
9580 })
9581 .into()
9582 };
9583 let tcp_front = |cluster_id: &str, port: u16| -> Request {
9584 RequestType::AddTcpFrontend(RequestTcpFrontend {
9585 cluster_id: cluster_id.to_owned(),
9586 address: SocketAddress::new_v4(127, 0, 0, 1, port),
9587 ..Default::default()
9588 })
9589 .into()
9590 };
9591
9592 let mut state = ConfigState::new();
9594 state
9595 .dispatch(&keyed("keyed"))
9596 .expect("a keyed cluster is valid");
9597 let err = state
9598 .dispatch(&tcp_front("keyed", 4000))
9599 .expect_err("a TCP frontend may not join a keyed cluster");
9600 assert!(
9601 matches!(err, StateError::InvalidTcpFrontend { .. }),
9602 "unexpected error: {err:?}"
9603 );
9604 assert!(state.tcp_fronts.get("keyed").is_none_or(Vec::is_empty));
9605
9606 let mut state = ConfigState::new();
9608 state
9609 .dispatch(
9610 &RequestType::AddCluster(Cluster {
9611 cluster_id: String::from("tcp"),
9612 ..Default::default()
9613 })
9614 .into(),
9615 )
9616 .expect("a plain cluster is valid");
9617 state
9618 .dispatch(&tcp_front("tcp", 4001))
9619 .expect("a TCP frontend may join a plain cluster");
9620 let err = state
9621 .dispatch(&keyed("tcp"))
9622 .expect_err("a cluster serving TCP may not name a key");
9623 assert!(
9624 matches!(
9625 err,
9626 StateError::InvalidValue {
9627 field: "affinity_key",
9628 ..
9629 }
9630 ),
9631 "unexpected error: {err:?}"
9632 );
9633 assert_eq!(state.clusters["tcp"].affinity_header, None);
9634 }
9635
9636 #[test]
9640 fn add_cluster_with_out_of_range_max_connection_attempts_rejected() {
9641 for attempts in [0, 256] {
9642 let mut state = ConfigState::new();
9643 let err = state
9644 .dispatch(
9645 &RequestType::AddCluster(Cluster {
9646 cluster_id: String::from("budget"),
9647 max_connection_attempts: Some(attempts),
9648 ..Default::default()
9649 })
9650 .into(),
9651 )
9652 .expect_err("an out-of-range budget must be refused");
9653 assert!(
9654 matches!(
9655 err,
9656 StateError::InvalidValue {
9657 field: "max_connection_attempts",
9658 ..
9659 }
9660 ),
9661 "{attempts}: unexpected error {err:?}"
9662 );
9663 assert!(state.clusters.is_empty());
9664 }
9665 let mut state = ConfigState::new();
9666 state
9667 .dispatch(
9668 &RequestType::AddCluster(Cluster {
9669 cluster_id: String::from("budget"),
9670 max_connection_attempts: Some(7),
9671 ..Default::default()
9672 })
9673 .into(),
9674 )
9675 .expect("a budget in range is accepted");
9676 assert_eq!(state.clusters["budget"].max_connection_attempts, Some(7));
9677 }
9678
9679 #[test]
9680 fn add_cluster_with_invalid_shuffle_sharding_rejected() {
9681 for (percent, minimum, mode) in [
9682 (Some(0), None, None),
9683 (Some(101), None, None),
9684 (Some(50), Some(1), None),
9685 (None, Some(8), None),
9686 (None, None, Some(1)),
9687 (Some(50), None, Some(7)),
9688 ] {
9689 let mut state = ConfigState::new();
9690 let err = state
9691 .dispatch(
9692 &RequestType::AddCluster(Cluster {
9693 cluster_id: String::from("sharded"),
9694 shard_percent: percent,
9695 shard_min_backends: minimum,
9696 shard_mode: mode,
9697 ..Default::default()
9698 })
9699 .into(),
9700 )
9701 .expect_err("invalid shuffle sharding must be refused");
9702 assert!(
9703 matches!(
9704 err,
9705 StateError::InvalidValue {
9706 field: "shuffle_sharding",
9707 ..
9708 }
9709 ),
9710 "({percent:?}, {minimum:?}, {mode:?}): unexpected error {err:?}"
9711 );
9712 assert!(state.clusters.is_empty());
9713 }
9714 let mut state = ConfigState::new();
9715 state
9716 .dispatch(
9717 &RequestType::AddCluster(Cluster {
9718 cluster_id: String::from("sharded"),
9719 shard_percent: Some(25),
9720 shard_min_backends: Some(4),
9721 shard_mode: Some(1),
9722 ..Default::default()
9723 })
9724 .into(),
9725 )
9726 .expect("a valid sharded cluster is accepted");
9727 }
9728
9729 #[test]
9730 fn add_cluster_with_two_affinity_key_sources_rejected() {
9731 let mut state = ConfigState::new();
9732 let err = state
9733 .dispatch(
9734 &RequestType::AddCluster(Cluster {
9735 cluster_id: String::from("keyed"),
9736 affinity_header: Some(String::from("X-Tenant")),
9737 affinity_cookie: Some(String::from("tenant")),
9738 ..Default::default()
9739 })
9740 .into(),
9741 )
9742 .expect_err("a cluster keyed on both a header and a cookie must be refused");
9743 assert!(
9744 matches!(
9745 err,
9746 StateError::InvalidValue {
9747 field: "affinity_key",
9748 ..
9749 }
9750 ),
9751 "unexpected error: {err:?}"
9752 );
9753 assert!(
9754 state.clusters.is_empty(),
9755 "a refused cluster must not be stored"
9756 );
9757 }
9758
9759 #[test]
9760 fn add_cluster_invalid_health_check_uri_rejected() {
9761 use crate::proto::command::HealthCheckConfig;
9762
9763 let mut state = ConfigState::new();
9764 let err = state
9765 .dispatch(
9766 &RequestType::AddCluster(Cluster {
9767 cluster_id: String::from("evil_cluster"),
9768 health_check: Some(HealthCheckConfig {
9769 uri: String::from("/foo\r\nGET /admin"),
9770 interval: 5_000,
9771 timeout: 1_000,
9772 healthy_threshold: 2,
9773 unhealthy_threshold: 2,
9774 ..Default::default()
9775 }),
9776 ..Default::default()
9777 })
9778 .into(),
9779 )
9780 .unwrap_err();
9781
9782 assert!(
9783 matches!(
9784 err,
9785 StateError::InvalidValue {
9786 field: "health_check",
9787 ..
9788 }
9789 ),
9790 "expected InvalidValue for CRLF-bearing health-check URI, got: {err}"
9791 );
9792 assert!(
9793 !state.clusters.contains_key("evil_cluster"),
9794 "cluster must not be inserted when health_check fails validation",
9795 );
9796 }
9797
9798 #[test]
9802 fn set_health_check_tcp_mode_with_status_field_rejected() {
9803 use crate::proto::command::{HealthCheckConfig, HealthCheckMode, SetHealthCheck};
9804
9805 let mut state = ConfigState::new();
9806 state
9807 .dispatch(
9808 &RequestType::AddCluster(Cluster {
9809 cluster_id: String::from("app"),
9810 ..Default::default()
9811 })
9812 .into(),
9813 )
9814 .expect("the cluster is added");
9815 let tcp = HealthCheckConfig {
9816 uri: String::new(),
9817 interval: 10,
9818 timeout: 5,
9819 healthy_threshold: 3,
9820 unhealthy_threshold: 3,
9821 mode: HealthCheckMode::Tcp as i32,
9822 ..Default::default()
9823 };
9824 let set = |config: HealthCheckConfig| {
9825 RequestType::SetHealthCheck(SetHealthCheck {
9826 cluster_id: String::from("app"),
9827 config,
9828 })
9829 .into()
9830 };
9831
9832 let err = state
9833 .dispatch(&set(HealthCheckConfig {
9834 expected_status: 200,
9835 ..tcp.clone()
9836 }))
9837 .unwrap_err();
9838 assert!(
9839 matches!(
9840 err,
9841 StateError::InvalidValue {
9842 field: "health_check",
9843 ..
9844 }
9845 ),
9846 "expected InvalidValue for a TCP probe with expected_status, got: {err}"
9847 );
9848 assert_eq!(state.clusters["app"].health_check, None);
9849
9850 state
9851 .dispatch(&set(tcp.clone()))
9852 .expect("a bare TCP probe is accepted");
9853 assert_eq!(state.clusters["app"].health_check, Some(tcp));
9854 }
9855
9856 #[test]
9859 fn set_health_check_out_of_range_port_rejected() {
9860 use crate::proto::command::{HealthCheckConfig, HealthCheckMode, SetHealthCheck};
9861
9862 let mut state = ConfigState::new();
9863 state
9864 .dispatch(
9865 &RequestType::AddCluster(Cluster {
9866 cluster_id: String::from("app"),
9867 ..Default::default()
9868 })
9869 .into(),
9870 )
9871 .expect("the cluster is added");
9872 let probe = HealthCheckConfig {
9873 uri: String::new(),
9874 interval: 10,
9875 timeout: 5,
9876 healthy_threshold: 3,
9877 unhealthy_threshold: 3,
9878 mode: HealthCheckMode::Tcp as i32,
9879 port: Some(8081),
9880 ..Default::default()
9881 };
9882 let set = |config: HealthCheckConfig| {
9883 RequestType::SetHealthCheck(SetHealthCheck {
9884 cluster_id: String::from("app"),
9885 config,
9886 })
9887 .into()
9888 };
9889
9890 for port in [0, 65536] {
9891 let err = state
9892 .dispatch(&set(HealthCheckConfig {
9893 port: Some(port),
9894 ..probe.clone()
9895 }))
9896 .unwrap_err();
9897 assert!(
9898 matches!(
9899 err,
9900 StateError::InvalidValue {
9901 field: "health_check",
9902 ..
9903 }
9904 ),
9905 "expected InvalidValue for probe port {port}, got: {err}"
9906 );
9907 assert_eq!(state.clusters["app"].health_check, None);
9908 }
9909
9910 state
9911 .dispatch(&set(probe.clone()))
9912 .expect("a probe port within 1..=65535 is accepted");
9913 assert_eq!(state.clusters["app"].health_check, Some(probe));
9914 }
9915
9916 #[test]
9920 fn health_check_headers_validate_before_mutation_and_survive_replay() {
9921 use std::io::Read;
9922
9923 use crate::{
9924 parser::parse_several_requests,
9925 proto::command::{
9926 HealthCheckConfig, HealthCheckHeader, HealthCheckMode, SetHealthCheck,
9927 },
9928 };
9929
9930 let header = |name: &str, tag: &str| HealthCheckHeader {
9932 name: name.to_owned(),
9933 fingerprint: format!("sozu-hc-hmac-sha256:v1:{:032x}:{:064x}", 7, tag.len()),
9934 };
9935 let probe = |headers: Vec<HealthCheckHeader>| HealthCheckConfig {
9936 uri: "/health".to_owned(),
9937 interval: 10,
9938 timeout: 5,
9939 healthy_threshold: 3,
9940 unhealthy_threshold: 3,
9941 mode: HealthCheckMode::Http as i32,
9942 headers,
9943 ..Default::default()
9944 };
9945 let ordered = vec![
9946 header("Z-Last", "1"),
9947 header("Authorization", "Bearer"),
9948 header("A-First", ""),
9949 ];
9950 let set = |config: HealthCheckConfig| -> Request {
9951 RequestType::SetHealthCheck(SetHealthCheck {
9952 cluster_id: String::from("app"),
9953 config,
9954 })
9955 .into()
9956 };
9957 let invalid = probe(vec![header("Host", "1")]);
9959
9960 let mut state = ConfigState::new();
9962 let err = state
9963 .dispatch(
9964 &RequestType::AddCluster(Cluster {
9965 cluster_id: String::from("app"),
9966 health_check: Some(invalid.clone()),
9967 ..Default::default()
9968 })
9969 .into(),
9970 )
9971 .unwrap_err();
9972 assert!(
9973 matches!(
9974 err,
9975 StateError::InvalidValue {
9976 field: "health_check",
9977 ..
9978 }
9979 ),
9980 "got {err}"
9981 );
9982 assert!(state.clusters.is_empty(), "no cluster may be inserted");
9983
9984 state
9985 .dispatch(
9986 &RequestType::AddCluster(Cluster {
9987 cluster_id: String::from("app"),
9988 ..Default::default()
9989 })
9990 .into(),
9991 )
9992 .expect("the cluster is added");
9993 state
9994 .dispatch(&set(probe(ordered.clone())))
9995 .expect("a valid header list is accepted");
9996
9997 let before = state.clone();
9999 let err = state.dispatch(&set(invalid)).unwrap_err();
10000 assert!(
10001 matches!(
10002 err,
10003 StateError::InvalidValue {
10004 field: "health_check",
10005 ..
10006 }
10007 ),
10008 "got {err}"
10009 );
10010 let logical = |s: &ConfigState| {
10012 let mut c = s.clone();
10013 c.request_counts.clear();
10014 c
10015 };
10016 assert_eq!(
10017 logical(&state),
10018 logical(&before),
10019 "a refused order must not mutate the state"
10020 );
10021 assert_eq!(
10022 state.clusters["app"].health_check.as_ref().unwrap().headers,
10023 ordered
10024 );
10025
10026 let queried = state.cluster_state("app").expect("cluster exists");
10028 assert_eq!(
10029 queried.configuration.unwrap().health_check.unwrap().headers,
10030 ordered
10031 );
10032 assert_eq!(
10033 state.list_health_checks(Some("app")).map["app"].headers,
10034 ordered
10035 );
10036
10037 let adds = ConfigState::new().diff(&state);
10039 let carried = adds.iter().any(|request| {
10040 matches!(
10041 &request.request_type,
10042 Some(RequestType::AddCluster(cluster))
10043 if cluster.health_check.as_ref().is_some_and(|hc| hc.headers == ordered)
10044 )
10045 });
10046 assert!(carried, "the diff must carry the ordered headers: {adds:?}");
10047 let mut reordered = state.clone();
10048 reordered
10049 .dispatch(&set(probe(ordered.iter().rev().cloned().collect())))
10050 .expect("a reordered list is valid");
10051 assert!(
10052 !state.diff(&reordered).is_empty(),
10053 "a header reorder is a cluster change"
10054 );
10055 assert!(state.diff(&state.clone()).is_empty());
10056
10057 let directory = tempfile::tempdir().expect("a temporary directory");
10059 let path = directory.path().join("headers.state");
10060 let mut file = File::create(&path).expect("creatable");
10061 let written = state.write_requests_to_file(&mut file).expect("saves");
10062 drop(file);
10063 let mut saved = Vec::new();
10064 File::open(&path)
10065 .expect("readable")
10066 .read_to_end(&mut saved)
10067 .expect("readable");
10068 let (remainder, requests) =
10069 parse_several_requests::<WorkerRequest>(&saved).expect("the file parses");
10070 assert!(remainder.is_empty());
10071 assert_eq!(requests.len(), written);
10072 let mut loaded = ConfigState::new();
10073 for request in &requests {
10074 loaded.dispatch(&request.content).expect("replays");
10075 }
10076
10077 let mut replayed = ConfigState::new();
10079 for request in state.generate_requests() {
10080 replayed.dispatch(&request).expect("replays");
10081 }
10082
10083 for (name, other) in [("loaded", &loaded), ("replayed", &replayed)] {
10084 assert_eq!(
10085 other.clusters["app"].health_check.as_ref().unwrap().headers,
10086 ordered,
10087 "{name} state must keep the exact ordered headers"
10088 );
10089 }
10090 }
10091
10092 #[test]
10097 fn protected_health_check_order_stores_metadata_only_and_refuses_before_mutation() {
10098 use crate::proto::command::{
10099 HealthCheckConfig, HealthCheckHeader, HealthCheckHeaderValue, HealthCheckMode,
10100 SetProtectedHealthCheck,
10101 };
10102
10103 const SECRET: &str = "SYNTHETIC-STATE-SECRET-77c1";
10104 let fingerprint = format!("sozu-hc-hmac-sha256:v1:{:032x}:{:064x}", 1, 2);
10105 let order = |fingerprint: &str, carrier_name: &str| {
10106 RequestType::SetProtectedHealthCheck(SetProtectedHealthCheck {
10107 cluster_id: String::from("app"),
10108 config: HealthCheckConfig {
10109 uri: "/health".to_owned(),
10110 interval: 10,
10111 timeout: 5,
10112 healthy_threshold: 3,
10113 unhealthy_threshold: 3,
10114 mode: HealthCheckMode::Http as i32,
10115 headers: vec![HealthCheckHeader {
10116 name: "Authorization".to_owned(),
10117 fingerprint: fingerprint.to_owned(),
10118 }],
10119 ..Default::default()
10120 },
10121 values: vec![HealthCheckHeaderValue {
10122 name: carrier_name.to_owned(),
10123 value: SECRET.as_bytes().to_vec(),
10124 }],
10125 })
10126 .into()
10127 };
10128 let mut state = ConfigState::new();
10129 state
10130 .dispatch(
10131 &RequestType::AddCluster(Cluster {
10132 cluster_id: String::from("app"),
10133 ..Default::default()
10134 })
10135 .into(),
10136 )
10137 .expect("the cluster is added");
10138 let logical = |s: &ConfigState| {
10139 let mut c = s.clone();
10140 c.request_counts.clear();
10141 c
10142 };
10143
10144 let before = logical(&state);
10145 for (what, refused) in [
10146 ("no fingerprint", order("", "Authorization")),
10147 ("another carrier", order(&fingerprint, "X-Other")),
10148 ] {
10149 assert!(state.dispatch(&refused).is_err(), "{what}");
10150 assert_eq!(logical(&state), before, "{what}: no mutation");
10151 }
10152
10153 state
10154 .dispatch(&order(&fingerprint, "Authorization"))
10155 .expect("a fingerprinted order with its carrier is accepted");
10156 let stored = state.clusters["app"].health_check.as_ref().expect("set");
10157 assert_eq!(stored.headers[0].fingerprint, fingerprint);
10158 let rendered = format!("{state:?}{:?}", state.generate_requests());
10159 assert!(!rendered.contains(SECRET), "the state holds no value");
10160 let json = serde_json::to_string(&state.generate_requests()).expect("serializes");
10161 assert!(!json.contains(SECRET), "a replay holds no value");
10162 }
10163
10164 #[test]
10168 fn a_health_check_port_and_a_unix_backend_never_share_a_cluster() {
10169 use crate::proto::command::{HealthCheckConfig, HealthCheckMode, SetHealthCheck};
10170
10171 let probe = |port| HealthCheckConfig {
10172 interval: 10,
10173 timeout: 5,
10174 healthy_threshold: 3,
10175 unhealthy_threshold: 3,
10176 mode: HealthCheckMode::Tcp as i32,
10177 port,
10178 ..Default::default()
10179 };
10180 let cluster = |health_check| -> Request {
10181 RequestType::AddCluster(Cluster {
10182 cluster_id: String::from("app"),
10183 health_check,
10184 ..Default::default()
10185 })
10186 .into()
10187 };
10188 let backend = |unix_path: Option<&str>| AddBackend {
10189 cluster_id: String::from("app"),
10190 backend_id: String::from("app-0"),
10191 address: SocketAddress::new_v4(127, 0, 0, 1, 1),
10192 unix_path: unix_path.map(ToOwned::to_owned),
10193 ..Default::default()
10194 };
10195 let add_unix: Request = RequestType::AddUnixBackend(backend(Some("/run/app.sock"))).into();
10196 let set = |port| -> Request {
10197 RequestType::SetHealthCheck(SetHealthCheck {
10198 cluster_id: String::from("app"),
10199 config: probe(port),
10200 })
10201 .into()
10202 };
10203 let refused = |state: &mut ConfigState, request: &Request| {
10204 let before = (state.clusters.clone(), state.backends.clone());
10205 let error = state.dispatch(request).unwrap_err();
10206 assert!(
10207 matches!(
10208 error,
10209 StateError::InvalidValue {
10210 field: "health_check.port",
10211 reason: crate::config::HEALTH_CHECK_PORT_UNIX_REFUSED,
10212 }
10213 ),
10214 "expected the probe port / unix_path refusal, got: {error}"
10215 );
10216 assert_eq!(before, (state.clusters.clone(), state.backends.clone()));
10217 };
10218
10219 let mut state = ConfigState::new();
10221 state.dispatch(&cluster(Some(probe(Some(8081))))).unwrap();
10222 refused(&mut state, &add_unix);
10223 assert!(state.backends.get("app").is_none_or(Vec::is_empty));
10224 state
10225 .dispatch(&RequestType::AddBackend(backend(None)).into())
10226 .expect("an IP backend takes the probe port");
10227
10228 let mut state = ConfigState::new();
10231 state.dispatch(&cluster(Some(probe(None)))).unwrap();
10232 state.dispatch(&add_unix).unwrap();
10233 refused(&mut state, &set(Some(8081)));
10234 refused(&mut state, &cluster(Some(probe(Some(8081)))));
10235 assert_eq!(state.clusters["app"].health_check, Some(probe(None)));
10236 state
10237 .dispatch(&set(None))
10238 .expect("a unix backend is probed through its socket");
10239 state
10240 .dispatch(&cluster(None))
10241 .expect("an upsert without a health check keeps the unix backend");
10242
10243 let mut state = ConfigState::new();
10245 assert!(matches!(
10246 state.dispatch(&set(Some(8081))).unwrap_err(),
10247 StateError::NotFound { .. }
10248 ));
10249 assert!(matches!(
10250 state.dispatch(&add_unix).unwrap_err(),
10251 StateError::ClusterNotFound { .. }
10252 ));
10253 }
10254
10255 #[test]
10257 fn update_https_listener_alpn_unknown_value_rejected() {
10258 use crate::proto::command::AlpnProtocols;
10259
10260 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10261 let mut state = ConfigState::new();
10262 state
10263 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10264 .unwrap();
10265
10266 let patch = UpdateHttpsListenerConfig {
10267 address: addr,
10268 alpn_protocols: Some(AlpnProtocols {
10269 values: vec!["h3".to_owned()],
10270 }),
10271 ..Default::default()
10272 };
10273 let err = state
10274 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10275 .unwrap_err();
10276 assert!(
10277 matches!(
10278 err,
10279 StateError::InvalidValue {
10280 field: "alpn_protocols",
10281 ..
10282 }
10283 ),
10284 "expected InvalidValue for unknown ALPN, got: {err}"
10285 );
10286 }
10287
10288 #[test]
10290 fn update_https_listener_alpn_empty_reset_accepted() {
10291 use crate::proto::command::AlpnProtocols;
10292
10293 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10294 let mut state = ConfigState::new();
10295 let mut listener = make_https_listener(addr);
10296 listener.alpn_protocols = vec!["h2".to_owned()];
10297 state
10298 .dispatch(&RequestType::AddHttpsListener(listener).into())
10299 .unwrap();
10300
10301 let patch = UpdateHttpsListenerConfig {
10302 address: addr,
10303 alpn_protocols: Some(AlpnProtocols { values: vec![] }),
10304 ..Default::default()
10305 };
10306 state
10307 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10308 .expect("empty ALPN reset must succeed");
10309
10310 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10311 assert!(
10312 listener.alpn_protocols.is_empty(),
10313 "ALPN must have been reset to empty"
10314 );
10315 }
10316
10317 #[test]
10319 fn update_https_listener_alpn_valid_values_accepted() {
10320 use crate::proto::command::AlpnProtocols;
10321
10322 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10323 let mut state = ConfigState::new();
10324 state
10325 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10326 .unwrap();
10327
10328 let patch = UpdateHttpsListenerConfig {
10329 address: addr,
10330 alpn_protocols: Some(AlpnProtocols {
10331 values: vec!["h2".to_owned(), "http/1.1".to_owned()],
10332 }),
10333 ..Default::default()
10334 };
10335 state
10336 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10337 .expect("valid ALPN must be accepted");
10338
10339 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10340 assert_eq!(listener.alpn_protocols, vec!["h2", "http/1.1"]);
10341 }
10342
10343 #[test]
10346 fn update_https_listener_alpn_absent_preserves_existing() {
10347 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10348 let mut state = ConfigState::new();
10349 let mut listener = make_https_listener(addr);
10350 listener.alpn_protocols = vec!["h2".to_owned()];
10351 state
10352 .dispatch(&RequestType::AddHttpsListener(listener).into())
10353 .unwrap();
10354
10355 let patch = UpdateHttpsListenerConfig {
10357 address: addr,
10358 front_timeout: Some(10),
10359 ..Default::default()
10360 };
10361 state
10362 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10363 .unwrap();
10364
10365 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10366 assert_eq!(
10367 listener.alpn_protocols,
10368 vec!["h2"],
10369 "ALPN must be preserved when not patched"
10370 );
10371 }
10372
10373 #[test]
10375 fn update_https_listener_sozu_id_header_empty_rejected() {
10376 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10377 let mut state = ConfigState::new();
10378 state
10379 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10380 .unwrap();
10381
10382 let patch = UpdateHttpsListenerConfig {
10383 address: addr,
10384 sozu_id_header: Some(String::new()),
10385 ..Default::default()
10386 };
10387 let err = state
10388 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10389 .unwrap_err();
10390 assert!(
10391 matches!(
10392 err,
10393 StateError::InvalidValue {
10394 field: "sozu_id_header",
10395 ..
10396 }
10397 ),
10398 "expected InvalidValue for empty header name, got: {err}"
10399 );
10400 }
10401
10402 #[test]
10404 fn update_https_listener_sozu_id_header_colon_rejected() {
10405 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10406 let mut state = ConfigState::new();
10407 state
10408 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10409 .unwrap();
10410
10411 let patch = UpdateHttpsListenerConfig {
10412 address: addr,
10413 sozu_id_header: Some("bad: value".to_owned()),
10414 ..Default::default()
10415 };
10416 let err = state
10417 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10418 .unwrap_err();
10419 assert!(
10420 matches!(
10421 err,
10422 StateError::InvalidValue {
10423 field: "sozu_id_header",
10424 ..
10425 }
10426 ),
10427 "expected InvalidValue for header name with colon, got: {err}"
10428 );
10429 }
10430
10431 #[test]
10433 fn update_https_listener_sozu_id_header_valid_accepted() {
10434 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10435 let mut state = ConfigState::new();
10436 state
10437 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10438 .unwrap();
10439
10440 let patch = UpdateHttpsListenerConfig {
10441 address: addr,
10442 sozu_id_header: Some("X-Edge-Id".to_owned()),
10443 ..Default::default()
10444 };
10445 state
10446 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10447 .expect("valid header name must be accepted");
10448
10449 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10450 assert_eq!(listener.sozu_id_header.as_deref(), Some("X-Edge-Id"));
10451 }
10452
10453 #[test]
10459 fn add_listener_rejects_an_unknown_forwarded_headers_value() {
10460 let http = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10461 let https = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10462 let mut state = ConfigState::new();
10463
10464 let http_error = state
10465 .dispatch(
10466 &RequestType::AddHttpListener(HttpListenerConfig {
10467 forwarded_headers: Some(42),
10468 ..make_http_listener(http)
10469 })
10470 .into(),
10471 )
10472 .unwrap_err();
10473 let https_error = state
10474 .dispatch(
10475 &RequestType::AddHttpsListener(HttpsListenerConfig {
10476 forwarded_headers: Some(-1),
10477 ..make_https_listener(https)
10478 })
10479 .into(),
10480 )
10481 .unwrap_err();
10482 for error in [http_error, https_error] {
10483 assert!(
10484 matches!(
10485 error,
10486 StateError::InvalidValue {
10487 field: "forwarded_headers",
10488 ..
10489 }
10490 ),
10491 "expected InvalidValue for an unknown mode, got: {error}"
10492 );
10493 }
10494 assert!(state.http_listeners.is_empty() && state.https_listeners.is_empty());
10495
10496 state
10497 .dispatch(
10498 &RequestType::AddHttpListener(HttpListenerConfig {
10499 forwarded_headers: Some(ForwardedHeaders::Rfc7239 as i32),
10500 ..make_http_listener(http)
10501 })
10502 .into(),
10503 )
10504 .expect("a known mode must be accepted on an HTTP listener");
10505 state
10506 .dispatch(
10507 &RequestType::AddHttpsListener(HttpsListenerConfig {
10508 forwarded_headers: Some(ForwardedHeaders::None as i32),
10509 ..make_https_listener(https)
10510 })
10511 .into(),
10512 )
10513 .expect("a known mode must be accepted on an HTTPS listener");
10514 }
10515
10516 #[test]
10521 fn update_listener_forwarded_headers_applies_and_rejects_unknown_values() {
10522 let http = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10523 let https = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10524 let mut state = ConfigState::new();
10525 state
10526 .dispatch(&RequestType::AddHttpListener(make_http_listener(http)).into())
10527 .unwrap();
10528 state
10529 .dispatch(&RequestType::AddHttpsListener(make_https_listener(https)).into())
10530 .unwrap();
10531
10532 state
10533 .dispatch(
10534 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
10535 address: http,
10536 forwarded_headers: Some(ForwardedHeaders::Rfc7239 as i32),
10537 ..Default::default()
10538 })
10539 .into(),
10540 )
10541 .expect("a known mode must be accepted on an HTTP listener");
10542 state
10543 .dispatch(
10544 &RequestType::UpdateHttpsListener(UpdateHttpsListenerConfig {
10545 address: https,
10546 forwarded_headers: Some(ForwardedHeaders::None as i32),
10547 ..Default::default()
10548 })
10549 .into(),
10550 )
10551 .expect("a known mode must be accepted on an HTTPS listener");
10552 assert_eq!(
10553 state.http_listeners[&http.into()].forwarded_headers(),
10554 ForwardedHeaders::Rfc7239
10555 );
10556 assert_eq!(
10557 state.https_listeners[&https.into()].forwarded_headers(),
10558 ForwardedHeaders::None
10559 );
10560
10561 let http_error = state
10562 .dispatch(
10563 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
10564 address: http,
10565 forwarded_headers: Some(42),
10566 ..Default::default()
10567 })
10568 .into(),
10569 )
10570 .unwrap_err();
10571 let https_error = state
10572 .dispatch(
10573 &RequestType::UpdateHttpsListener(UpdateHttpsListenerConfig {
10574 address: https,
10575 forwarded_headers: Some(-1),
10576 ..Default::default()
10577 })
10578 .into(),
10579 )
10580 .unwrap_err();
10581 for error in [http_error, https_error] {
10582 assert!(
10583 matches!(
10584 error,
10585 StateError::InvalidValue {
10586 field: "forwarded_headers",
10587 ..
10588 }
10589 ),
10590 "expected InvalidValue for an unknown mode, got: {error}"
10591 );
10592 }
10593 assert_eq!(
10595 state.http_listeners[&http.into()].forwarded_headers(),
10596 ForwardedHeaders::Rfc7239
10597 );
10598 assert_eq!(
10599 state.https_listeners[&https.into()].forwarded_headers(),
10600 ForwardedHeaders::None
10601 );
10602 }
10603
10604 #[test]
10606 fn update_https_listener_graceful_shutdown_zero_allowed() {
10607 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10608 let mut state = ConfigState::new();
10609 state
10610 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10611 .unwrap();
10612
10613 let patch = UpdateHttpsListenerConfig {
10614 address: addr,
10615 h2_graceful_shutdown_deadline_seconds: Some(0),
10616 ..Default::default()
10617 };
10618 state
10619 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10620 .expect("graceful_shutdown_deadline=0 must be allowed");
10621
10622 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10623 assert_eq!(listener.h2_graceful_shutdown_deadline_seconds, Some(0));
10624 }
10625
10626 #[test]
10630 fn update_http_listener_happy_path() {
10631 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10632 let mut state = ConfigState::new();
10633 state
10634 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10635 .unwrap();
10636
10637 let patch = UpdateHttpListenerConfig {
10638 address: addr,
10639 front_timeout: Some(15),
10640 h2_max_rst_stream_per_window: Some(25),
10641 ..Default::default()
10642 };
10643 state
10644 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10645 .expect("HTTP update must succeed");
10646
10647 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10648 assert_eq!(listener.front_timeout, 15);
10649 assert_eq!(listener.h2_max_rst_stream_per_window, Some(25));
10650 assert_eq!(listener.back_timeout, 30);
10652 }
10653
10654 #[test]
10661 fn update_http_listener_records_the_answers_map() {
10662 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10663 let mut state = ConfigState::new();
10664 let mut listener = make_http_listener(addr);
10665 listener.answers = [
10666 ("404".to_owned(), "stored 404".to_owned()),
10667 ("503".to_owned(), "stored 503".to_owned()),
10668 ]
10669 .into();
10670 state
10671 .dispatch(&RequestType::AddHttpListener(listener).into())
10672 .unwrap();
10673
10674 let patch = UpdateHttpListenerConfig {
10675 address: addr,
10676 answers: [
10677 ("404".to_owned(), String::new()),
10678 ("503".to_owned(), "patched 503".to_owned()),
10679 ("429".to_owned(), "patched 429".to_owned()),
10680 ]
10681 .into(),
10682 ..Default::default()
10683 };
10684 state
10685 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10686 .expect("HTTP update must succeed");
10687
10688 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10689 assert_eq!(
10690 listener.answers.get("404").map(String::as_str),
10691 Some("stored 404"),
10692 "an empty body preserves the stored template"
10693 );
10694 assert_eq!(
10695 listener.answers.get("503").map(String::as_str),
10696 Some("patched 503")
10697 );
10698 assert_eq!(
10699 listener.answers.get("429").map(String::as_str),
10700 Some("patched 429")
10701 );
10702 assert_eq!(listener.answers.len(), 3);
10703 }
10704
10705 #[test]
10711 fn update_https_listener_records_the_answers_map_and_hsts() {
10712 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10713 let mut state = ConfigState::new();
10714 let mut listener = make_https_listener(addr);
10715 listener.answers = [("404".to_owned(), "stored 404".to_owned())].into();
10716 listener.hsts = Some(HstsConfig {
10717 enabled: Some(true),
10718 max_age: Some(300),
10719 include_subdomains: Some(true),
10720 ..Default::default()
10721 });
10722 state
10723 .dispatch(&RequestType::AddHttpsListener(listener).into())
10724 .unwrap();
10725
10726 let disabled = HstsConfig {
10727 enabled: Some(false),
10728 ..Default::default()
10729 };
10730 let patch = UpdateHttpsListenerConfig {
10731 address: addr,
10732 answers: [
10733 ("404".to_owned(), String::new()),
10734 ("503".to_owned(), "patched 503".to_owned()),
10735 ]
10736 .into(),
10737 hsts: Some(disabled),
10738 ..Default::default()
10739 };
10740 state
10741 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10742 .expect("HTTPS update must succeed");
10743
10744 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10745 assert_eq!(
10746 listener.answers.get("404").map(String::as_str),
10747 Some("stored 404"),
10748 "an empty body preserves the stored template"
10749 );
10750 assert_eq!(
10751 listener.answers.get("503").map(String::as_str),
10752 Some("patched 503")
10753 );
10754 assert_eq!(listener.answers.len(), 2);
10755 assert_eq!(
10756 listener.hsts,
10757 Some(disabled),
10758 "the patched hsts block replaces the stored one whole"
10759 );
10760
10761 let patch = UpdateHttpsListenerConfig {
10762 address: addr,
10763 front_timeout: Some(15),
10764 ..Default::default()
10765 };
10766 state
10767 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10768 .expect("HTTPS update must succeed");
10769 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10770 assert_eq!(
10771 listener.hsts,
10772 Some(disabled),
10773 "a patch without hsts keeps the recorded policy"
10774 );
10775 }
10776
10777 #[test]
10779 fn update_http_listener_flood_knob_zero_rejected() {
10780 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10781 let mut state = ConfigState::new();
10782 state
10783 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10784 .unwrap();
10785
10786 let patch = UpdateHttpListenerConfig {
10787 address: addr,
10788 h2_max_window_update_stream0_per_window: Some(0),
10789 ..Default::default()
10790 };
10791 let err = state
10792 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10793 .unwrap_err();
10794 assert!(
10795 matches!(
10796 err,
10797 StateError::InvalidValue {
10798 field: "h2_max_window_update_stream0_per_window",
10799 ..
10800 }
10801 ),
10802 "expected InvalidValue, got: {err}"
10803 );
10804 }
10805
10806 #[test]
10812 fn update_http_listener_x_real_ip_patch_applied() {
10813 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10814 let mut state = ConfigState::new();
10815 state
10816 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
10817 .unwrap();
10818
10819 let patch = UpdateHttpListenerConfig {
10820 address: addr,
10821 elide_x_real_ip: Some(true),
10822 send_x_real_ip: Some(true),
10823 ..Default::default()
10824 };
10825 state
10826 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10827 .expect("HTTP update must succeed");
10828
10829 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10830 assert_eq!(listener.elide_x_real_ip, Some(true));
10831 assert_eq!(listener.send_x_real_ip, Some(true));
10832 assert_eq!(listener.front_timeout, 60);
10834 }
10835
10836 #[test]
10839 fn update_http_listener_x_real_ip_absent_preserves_existing() {
10840 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10841 let mut state = ConfigState::new();
10842 let listener = HttpListenerConfig {
10843 elide_x_real_ip: Some(true),
10844 send_x_real_ip: Some(false),
10845 ..make_http_listener(addr)
10846 };
10847 state
10848 .dispatch(&RequestType::AddHttpListener(listener).into())
10849 .unwrap();
10850
10851 let patch = UpdateHttpListenerConfig {
10852 address: addr,
10853 send_x_real_ip: Some(true),
10854 ..Default::default()
10855 };
10856 state
10857 .dispatch(&RequestType::UpdateHttpListener(patch).into())
10858 .expect("HTTP update must succeed");
10859
10860 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
10861 assert_eq!(listener.elide_x_real_ip, Some(true));
10862 assert_eq!(listener.send_x_real_ip, Some(true));
10863 }
10864
10865 #[test]
10868 fn update_https_listener_x_real_ip_patch_applied() {
10869 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10870 let mut state = ConfigState::new();
10871 state
10872 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
10873 .unwrap();
10874
10875 let patch = UpdateHttpsListenerConfig {
10876 address: addr,
10877 elide_x_real_ip: Some(true),
10878 send_x_real_ip: Some(true),
10879 ..Default::default()
10880 };
10881 state
10882 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10883 .expect("HTTPS update must succeed");
10884
10885 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10886 assert_eq!(listener.elide_x_real_ip, Some(true));
10887 assert_eq!(listener.send_x_real_ip, Some(true));
10888 assert_eq!(listener.front_timeout, 60);
10890 }
10891
10892 #[test]
10895 fn update_https_listener_x_real_ip_absent_preserves_existing() {
10896 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10897 let mut state = ConfigState::new();
10898 let listener = HttpsListenerConfig {
10899 elide_x_real_ip: Some(false),
10900 send_x_real_ip: Some(true),
10901 ..make_https_listener(addr)
10902 };
10903 state
10904 .dispatch(&RequestType::AddHttpsListener(listener).into())
10905 .unwrap();
10906
10907 let patch = UpdateHttpsListenerConfig {
10908 address: addr,
10909 elide_x_real_ip: Some(true),
10910 ..Default::default()
10911 };
10912 state
10913 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
10914 .expect("HTTPS update must succeed");
10915
10916 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
10917 assert_eq!(listener.elide_x_real_ip, Some(true));
10918 assert_eq!(listener.send_x_real_ip, Some(true));
10919 }
10920
10921 #[test]
10924 fn update_listener_x_real_ip_survives_generate_requests_replay() {
10925 let http_addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
10926 let https_addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
10927 let mut state = ConfigState::new();
10928 state
10929 .dispatch(&RequestType::AddHttpListener(make_http_listener(http_addr)).into())
10930 .unwrap();
10931 state
10932 .dispatch(&RequestType::AddHttpsListener(make_https_listener(https_addr)).into())
10933 .unwrap();
10934 state
10935 .dispatch(
10936 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
10937 address: http_addr,
10938 elide_x_real_ip: Some(true),
10939 send_x_real_ip: Some(true),
10940 ..Default::default()
10941 })
10942 .into(),
10943 )
10944 .unwrap();
10945 state
10946 .dispatch(
10947 &RequestType::UpdateHttpsListener(UpdateHttpsListenerConfig {
10948 address: https_addr,
10949 elide_x_real_ip: Some(true),
10950 send_x_real_ip: Some(true),
10951 ..Default::default()
10952 })
10953 .into(),
10954 )
10955 .unwrap();
10956
10957 let mut replayed = ConfigState::new();
10958 for request in state.generate_requests() {
10959 replayed
10960 .dispatch(&request)
10961 .expect("could not replay generated request");
10962 }
10963
10964 let http = replayed
10965 .http_listeners
10966 .get(&ListenerKey::from(http_addr))
10967 .expect("replayed HTTP listener");
10968 assert_eq!(http.elide_x_real_ip, Some(true));
10969 assert_eq!(http.send_x_real_ip, Some(true));
10970 let https = replayed
10971 .https_listeners
10972 .get(&ListenerKey::from(https_addr))
10973 .expect("replayed HTTPS listener");
10974 assert_eq!(https.elide_x_real_ip, Some(true));
10975 assert_eq!(https.send_x_real_ip, Some(true));
10976 }
10977
10978 #[test]
10982 fn update_tcp_listener_happy_path() {
10983 let addr = SocketAddress::new_v4(0, 0, 0, 0, 9000);
10984 let mut state = ConfigState::new();
10985 state
10986 .dispatch(&RequestType::AddTcpListener(make_tcp_listener(addr)).into())
10987 .unwrap();
10988
10989 let patch = UpdateTcpListenerConfig {
10990 address: addr,
10991 front_timeout: Some(5),
10992 ..Default::default()
10993 };
10994 state
10995 .dispatch(&RequestType::UpdateTcpListener(patch).into())
10996 .expect("TCP update must succeed");
10997
10998 let listener = state.tcp_listeners.get(&ListenerKey::from(addr)).unwrap();
10999 assert_eq!(listener.front_timeout, 5);
11000 assert_eq!(listener.back_timeout, 30); }
11002
11003 #[test]
11005 fn update_tcp_listener_not_found() {
11006 let mut state = ConfigState::new();
11007 let patch = UpdateTcpListenerConfig {
11008 address: SocketAddress::new_v4(9, 9, 9, 9, 9999),
11009 front_timeout: Some(5),
11010 ..Default::default()
11011 };
11012 let err = state
11013 .dispatch(&RequestType::UpdateTcpListener(patch).into())
11014 .unwrap_err();
11015 assert!(
11016 matches!(
11017 err,
11018 StateError::NotFound {
11019 kind: ObjectKind::TcpListener,
11020 ..
11021 }
11022 ),
11023 "expected NotFound, got: {err}"
11024 );
11025 }
11026
11027 fn without_request_counts(state: &ConfigState) -> ConfigState {
11033 ConfigState {
11034 request_counts: BTreeMap::new(),
11035 ..state.clone()
11036 }
11037 }
11038
11039 #[test]
11050 fn update_http_listener_rejected_patch_leaves_the_state_unchanged() {
11051 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11052 let mut state = ConfigState::new();
11053 state
11054 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
11055 .unwrap();
11056
11057 let valid_prefix = UpdateHttpListenerConfig {
11058 address: addr,
11059 front_timeout: Some(99),
11060 sticky_name: Some("PATCHED".to_owned()),
11061 elide_x_real_ip: Some(true),
11062 http_answers: Some(CustomHttpAnswers {
11063 answer_404: Some("HTTP/1.1 404 Not Found\r\n\r\n".to_owned()),
11064 ..Default::default()
11065 }),
11066 h2_max_rst_stream_per_window: Some(7),
11067 ..Default::default()
11068 };
11069 let rejected = [
11070 (
11071 "sozu_id_header",
11072 UpdateHttpListenerConfig {
11073 sozu_id_header: Some("bad header".to_owned()),
11074 ..valid_prefix.clone()
11075 },
11076 ),
11077 (
11078 "forwarded_headers",
11079 UpdateHttpListenerConfig {
11080 forwarded_headers: Some(i32::MAX),
11081 ..valid_prefix.clone()
11082 },
11083 ),
11084 ];
11085 for (field, patch) in rejected {
11086 let before = without_request_counts(&state);
11087 let err = state
11088 .dispatch(&RequestType::UpdateHttpListener(patch).into())
11089 .unwrap_err();
11090 assert!(
11091 matches!(err, StateError::InvalidValue { field: f, .. } if f == field),
11092 "expected InvalidValue on {field}, got: {err}"
11093 );
11094 assert!(
11095 without_request_counts(&state) == before,
11096 "a patch rejected on {field} must leave the main state unchanged"
11097 );
11098 }
11099
11100 state
11103 .dispatch(&RequestType::UpdateHttpListener(valid_prefix).into())
11104 .expect("the valid prefix alone must apply");
11105 let listener = state.http_listeners.get(&ListenerKey::from(addr)).unwrap();
11106 assert_eq!(listener.front_timeout, 99);
11107 assert_eq!(listener.sticky_name, "PATCHED");
11108 }
11109
11110 #[test]
11120 fn update_https_listener_rejected_patch_leaves_the_state_unchanged() {
11121 use crate::proto::command::AlpnProtocols;
11122
11123 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
11124 let mut state = ConfigState::new();
11125 state
11126 .dispatch(&RequestType::AddHttpsListener(make_https_listener(addr)).into())
11127 .unwrap();
11128
11129 let valid_prefix = UpdateHttpsListenerConfig {
11130 address: addr,
11131 front_timeout: Some(99),
11132 sticky_name: Some("PATCHED".to_owned()),
11133 send_x_real_ip: Some(true),
11134 http_answers: Some(CustomHttpAnswers {
11135 answer_404: Some("HTTP/1.1 404 Not Found\r\n\r\n".to_owned()),
11136 ..Default::default()
11137 }),
11138 strict_sni_binding: Some(false),
11139 h2_max_rst_stream_per_window: Some(7),
11140 ..Default::default()
11141 };
11142 let rejected = [
11143 (
11144 "alpn_protocols",
11145 UpdateHttpsListenerConfig {
11146 alpn_protocols: Some(AlpnProtocols {
11147 values: vec!["h3".to_owned()],
11148 }),
11149 ..valid_prefix.clone()
11150 },
11151 ),
11152 (
11153 "sozu_id_header",
11154 UpdateHttpsListenerConfig {
11155 sozu_id_header: Some(String::new()),
11156 ..valid_prefix.clone()
11157 },
11158 ),
11159 (
11160 "forwarded_headers",
11161 UpdateHttpsListenerConfig {
11162 forwarded_headers: Some(-1),
11163 ..valid_prefix.clone()
11164 },
11165 ),
11166 (
11167 "hsts",
11168 UpdateHttpsListenerConfig {
11169 hsts: Some(HstsConfig {
11170 enabled: None,
11171 max_age: Some(60),
11172 ..Default::default()
11173 }),
11174 ..valid_prefix.clone()
11175 },
11176 ),
11177 ];
11178 for (field, patch) in rejected {
11179 let before = without_request_counts(&state);
11180 let err = state
11181 .dispatch(&RequestType::UpdateHttpsListener(patch).into())
11182 .unwrap_err();
11183 assert!(
11184 matches!(err, StateError::InvalidValue { field: f, .. } if f == field),
11185 "expected InvalidValue on {field}, got: {err}"
11186 );
11187 assert!(
11188 without_request_counts(&state) == before,
11189 "a patch rejected on {field} must leave the main state unchanged"
11190 );
11191 }
11192
11193 state
11194 .dispatch(&RequestType::UpdateHttpsListener(valid_prefix).into())
11195 .expect("the valid prefix alone must apply");
11196 let listener = state.https_listeners.get(&ListenerKey::from(addr)).unwrap();
11197 assert_eq!(listener.front_timeout, 99);
11198 assert_eq!(listener.strict_sni_binding, Some(false));
11199 }
11200
11201 #[test]
11205 fn update_tcp_listener_rejected_patch_leaves_the_state_unchanged() {
11206 let addr = SocketAddress::new_v4(0, 0, 0, 0, 9000);
11207 let mut state = ConfigState::new();
11208 state
11209 .dispatch(&RequestType::AddTcpListener(make_tcp_listener(addr)).into())
11210 .unwrap();
11211
11212 let before = without_request_counts(&state);
11213 let err = state
11214 .dispatch(
11215 &RequestType::UpdateTcpListener(UpdateTcpListenerConfig {
11216 address: SocketAddress::new_v4(0, 0, 0, 0, 9001),
11217 front_timeout: Some(5),
11218 ..Default::default()
11219 })
11220 .into(),
11221 )
11222 .unwrap_err();
11223 assert!(matches!(err, StateError::NotFound { .. }), "got: {err}");
11224 assert!(
11225 without_request_counts(&state) == before,
11226 "a refused TCP patch must change nothing"
11227 );
11228 }
11229
11230 #[test]
11236 fn dispatch_passes_through_set_metric_detail() {
11237 use crate::proto::command::{MetricDetail, SetMetricDetail};
11238 let mut state = ConfigState::new();
11239 let req: Request = RequestType::SetMetricDetail(SetMetricDetail {
11240 client_id: "test:1".to_owned(),
11241 detail: Some(MetricDetail::DetailBackend as i32),
11242 ttl_seconds: Some(60),
11243 clear: Some(false),
11244 reason: Some("regression-guard".to_owned()),
11245 peer_pid: None,
11246 peer_session_ulid: None,
11247 })
11248 .into();
11249 state
11250 .dispatch(&req)
11251 .expect("SetMetricDetail must traverse dispatch without UndispatchableRequest");
11252 }
11253
11254 #[test]
11293 fn remove_udp_frontend_drops_exactly_the_frontend_its_tags_name() {
11294 let team_a_address = SocketAddress::new_v4(127, 0, 0, 1, 9100);
11295 let team_b_address = SocketAddress::new_v4(127, 0, 0, 1, 9101);
11296 let front = |owner: &str, address| RequestUdpFrontend {
11297 cluster_id: "udp_cluster".to_string(),
11298 address,
11299 tags: BTreeMap::from([("owner".to_string(), owner.to_string())]),
11300 };
11301 let mut state = ConfigState::new();
11302
11303 for (owner, address) in [("team-a", team_a_address), ("team-b", team_b_address)] {
11307 state
11308 .dispatch(&RequestType::AddUdpFrontend(front(owner, address)).into())
11309 .expect("one UDP frontend per address must be admitted");
11310 }
11311 assert_eq!(
11312 state.udp_fronts.get("udp_cluster").map(Vec::len),
11313 Some(2usize),
11314 "both tagged UDP frontends must be in the state before the removal"
11315 );
11316
11317 state
11318 .dispatch(&RequestType::RemoveUdpFrontend(front("team-a", team_a_address)).into())
11319 .expect("removing one of the two must succeed");
11320
11321 let surviving: Vec<&str> = state
11322 .udp_fronts
11323 .get("udp_cluster")
11324 .map(|fronts| {
11325 fronts
11326 .iter()
11327 .filter_map(|f| f.tags.get("owner").map(String::as_str))
11328 .collect()
11329 })
11330 .unwrap_or_default();
11331 assert_eq!(
11332 surviving,
11333 vec!["team-b"],
11334 "removing one UDP frontend must not evict the rest of its cluster's bucket"
11335 );
11336
11337 let tagless = RequestUdpFrontend {
11341 cluster_id: "udp_cluster".to_string(),
11342 address: team_b_address,
11343 tags: BTreeMap::new(),
11344 };
11345 let err = state
11346 .dispatch(&RequestType::RemoveUdpFrontend(tagless).into())
11347 .expect_err("a tagless remove must not match a tagged UDP frontend");
11348 assert!(
11349 matches!(err, StateError::NoChange),
11350 "expected NoChange, got: {err}"
11351 );
11352 assert_eq!(
11353 state.udp_fronts.get("udp_cluster").map(Vec::len),
11354 Some(1usize),
11355 "the tagless remove must leave the surviving tagged frontend in place"
11356 );
11357 }
11358
11359 fn udp_front(cluster_id: &str, address: SocketAddress, owner: &str) -> RequestUdpFrontend {
11360 RequestUdpFrontend {
11361 cluster_id: cluster_id.to_string(),
11362 address,
11363 tags: BTreeMap::from([("owner".to_string(), owner.to_string())]),
11364 }
11365 }
11366
11367 #[test]
11398 fn add_udp_frontend_refuses_a_second_frontend_on_a_taken_address() {
11399 let address = SocketAddress::new_v4(127, 0, 0, 1, 5353);
11400 let other_address = SocketAddress::new_v4(127, 0, 0, 1, 5354);
11401 let mut state = ConfigState::new();
11402
11403 state
11404 .dispatch(&RequestType::AddUdpFrontend(udp_front("dns", address, "team-a")).into())
11405 .expect("the first claim on an address must be admitted");
11406
11407 state
11409 .dispatch(&RequestType::AddUdpFrontend(udp_front("dns", address, "team-b")).into())
11410 .expect_err("a second UDP frontend on a taken address must be refused");
11411
11412 state
11415 .dispatch(&RequestType::AddUdpFrontend(udp_front("syslog", address, "team-c")).into())
11416 .expect_err("a second UDP frontend on a taken address must be refused");
11417
11418 assert_eq!(
11419 state.count_udp_frontends_raw(),
11420 1,
11421 "a refused claim must leave the state holding only the admitted frontend"
11422 );
11423 assert!(
11424 state.udp_fronts.get("syslog").is_none_or(Vec::is_empty),
11425 "a refused cross-cluster claim must not leave an opened bucket behind"
11426 );
11427
11428 state
11430 .dispatch(
11431 &RequestType::AddUdpFrontend(udp_front("syslog", other_address, "team-c")).into(),
11432 )
11433 .expect("a UDP frontend on a free address must still be admitted");
11434
11435 state
11438 .dispatch(&RequestType::RemoveUdpFrontend(udp_front("dns", address, "team-a")).into())
11439 .expect("removing the frontend holding the address must succeed");
11440 state
11441 .dispatch(&RequestType::AddUdpFrontend(udp_front("syslog", address, "team-c")).into())
11442 .expect("a freed address must be claimable again, by any cluster");
11443
11444 assert_eq!(
11445 state.count_udp_frontends_raw(),
11446 2,
11447 "the freed address must now be held by exactly one frontend, beside the other address"
11448 );
11449 }
11450
11451 #[test]
11468 fn a_refused_udp_frontend_names_the_cluster_already_holding_the_address() {
11469 const OWNER_SECRET: &str = "UDP_ADDRESS_OWNER_SECRET_SENTINEL";
11470
11471 let address = SocketAddress::new_v4(127, 0, 0, 1, 5353);
11472 let owner_cluster = format!("{OWNER_SECRET}-dns");
11473 let owner_tag = format!("{OWNER_SECRET}-team-a");
11474 let mut state = ConfigState::new();
11475
11476 state
11477 .dispatch(
11478 &RequestType::AddUdpFrontend(udp_front(&owner_cluster, address, &owner_tag)).into(),
11479 )
11480 .expect("the first claim on an address must be admitted");
11481
11482 let error = state
11483 .dispatch(&RequestType::AddUdpFrontend(udp_front("syslog", address, "team-c")).into())
11484 .expect_err("a second UDP frontend on a taken address must be refused");
11485
11486 match &error {
11487 StateError::UdpFrontendAddressTaken {
11488 address: refused_address,
11489 cluster_id,
11490 current_cluster_id,
11491 current_tags,
11492 } => {
11493 assert_eq!(
11494 *refused_address,
11495 SocketAddr::from(address),
11496 "the refusal must name the contested address"
11497 );
11498 assert_eq!(
11499 &**cluster_id, "syslog",
11500 "the refusal must name the claimant"
11501 );
11502 assert_eq!(
11503 **current_cluster_id, *owner_cluster,
11504 "the refusal must name the cluster already holding the address"
11505 );
11506 assert_eq!(
11507 current_tags.get("owner"),
11508 Some(&owner_tag),
11509 "the refusal must carry the current owner's access-log tags"
11510 );
11511 }
11512 other => panic!("expected UdpFrontendAddressTaken, got {other:?}"),
11513 }
11514
11515 for (label, output) in [
11518 ("Display", error.to_string()),
11519 ("Debug", format!("{error:?}")),
11520 ] {
11521 assert!(
11522 output.contains("127.0.0.1:5353"),
11523 "StateError {label} must name the contested address: {output}"
11524 );
11525 assert!(
11526 !output.contains(OWNER_SECRET),
11527 "StateError {label} leaked a protected identifier: {output}"
11528 );
11529 assert!(
11530 output.contains(&format!("current_cluster_id_bytes={}", owner_cluster.len())),
11531 "StateError {label} omitted the bounded owner metadata: {output}"
11532 );
11533 assert!(
11534 output.contains("current_tag_count=1"),
11535 "StateError {label} omitted the bounded owner tag count: {output}"
11536 );
11537 }
11538 }
11539
11540 #[test]
11563 fn a_saved_state_holding_one_udp_frontend_per_address_replays_unchanged() {
11564 use std::io::Read;
11565
11566 use crate::parser::parse_several_requests;
11567
11568 let first = SocketAddress::new_v4(127, 0, 0, 1, 5353);
11569 let second = SocketAddress::new_v4(127, 0, 0, 2, 5353);
11570
11571 let mut state = ConfigState::new();
11572 for (cluster_id, address) in [("dns", first), ("syslog", second)] {
11573 state
11574 .dispatch(&RequestType::AddUdpListener(make_udp_listener(address, true)).into())
11575 .expect("could not add udp listener");
11576 state
11577 .dispatch(
11578 &RequestType::ActivateListener(ActivateListener {
11579 interface: None,
11580 address,
11581 proxy: ListenerType::Udp.into(),
11582 from_scm: false,
11583 })
11584 .into(),
11585 )
11586 .expect("could not activate udp listener");
11587 state
11588 .dispatch(
11589 &RequestType::AddUdpFrontend(udp_front(cluster_id, address, "team-a")).into(),
11590 )
11591 .expect("one UDP frontend per address must be admitted");
11592 }
11593
11594 let directory = tempfile::tempdir().expect("a temporary directory for the state file");
11595 let path = directory.path().join("udpaddr-roundtrip.state");
11596 let mut file = File::create(&path).expect("the state file must be creatable");
11597 let written = state
11598 .write_requests_to_file(&mut file)
11599 .expect("SaveState must serialise the state");
11600 drop(file);
11601
11602 let mut saved = Vec::new();
11603 File::open(&path)
11604 .expect("the state file must be readable")
11605 .read_to_end(&mut saved)
11606 .expect("the state file must be readable");
11607
11608 let (remainder, requests) =
11609 parse_several_requests::<WorkerRequest>(&saved).expect("the state file must parse");
11610 assert!(
11611 remainder.is_empty(),
11612 "load_state would report leftover bytes: {} unparsed",
11613 remainder.len()
11614 );
11615 assert_eq!(
11616 requests.len(),
11617 written,
11618 "every record write_requests_to_file emitted must parse back"
11619 );
11620
11621 let mut replayed = ConfigState::new();
11622 for request in &requests {
11623 replayed
11624 .dispatch(&request.content)
11625 .expect("every saved UDP entry must replay into a fresh state");
11626 }
11627
11628 let logical = |s: &ConfigState| {
11629 let mut c = s.clone();
11630 c.request_counts.clear();
11631 c
11632 };
11633 assert_eq!(
11634 logical(&state),
11635 logical(&replayed),
11636 "a SaveState -> LoadState round trip must reconstruct the UDP configuration"
11637 );
11638 }
11639
11640 fn with_interface<T>(mut listener: T, set: impl FnOnce(&mut T)) -> T {
11643 set(&mut listener);
11644 listener
11645 }
11646
11647 #[test]
11651 fn listeners_on_one_address_with_different_interfaces_coexist() {
11652 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8443);
11653 let mut state = ConfigState::new();
11654 for interface in [None, Some("lo"), Some("wg0")] {
11655 let iface = interface.map(str::to_owned);
11656 state
11657 .dispatch(
11658 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11659 l.interface = iface.clone()
11660 }))
11661 .into(),
11662 )
11663 .expect("an HTTP listener on another interface must not collide");
11664 state
11665 .dispatch(
11666 &RequestType::AddHttpsListener(with_interface(
11667 make_https_listener(addr),
11668 |l| l.interface = iface.clone(),
11669 ))
11670 .into(),
11671 )
11672 .expect("an HTTPS listener on another interface must not collide");
11673 state
11674 .dispatch(
11675 &RequestType::AddTcpListener(with_interface(make_tcp_listener(addr), |l| {
11676 l.interface = iface.clone()
11677 }))
11678 .into(),
11679 )
11680 .expect("a TCP listener on another interface must not collide");
11681 state
11682 .dispatch(
11683 &RequestType::AddUdpListener(with_interface(
11684 make_udp_listener(addr, false),
11685 |l| l.interface = iface.clone(),
11686 ))
11687 .into(),
11688 )
11689 .expect("a UDP listener on another interface must not collide");
11690 }
11691
11692 let expected = vec![
11693 "0.0.0.0:8443".to_owned(),
11694 "0.0.0.0:8443%lo".to_owned(),
11695 "0.0.0.0:8443%wg0".to_owned(),
11696 ];
11697 let list = state.list_listeners();
11698 assert_eq!(
11699 list.http_listeners.keys().cloned().collect::<Vec<_>>(),
11700 expected
11701 );
11702 assert_eq!(
11703 list.https_listeners.keys().cloned().collect::<Vec<_>>(),
11704 expected
11705 );
11706 assert_eq!(
11707 list.tcp_listeners.keys().cloned().collect::<Vec<_>>(),
11708 expected
11709 );
11710 assert_eq!(
11711 list.udp_listeners.keys().cloned().collect::<Vec<_>>(),
11712 expected
11713 );
11714 assert_eq!(
11715 list.http_listeners["0.0.0.0:8443%wg0"].interface.as_deref(),
11716 Some("wg0")
11717 );
11718
11719 assert!(
11721 state
11722 .dispatch(
11723 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11724 l.interface = Some("lo".to_owned())
11725 }))
11726 .into(),
11727 )
11728 .is_err(),
11729 "the same (address, interface) must still be refused as a duplicate"
11730 );
11731
11732 state
11734 .dispatch(
11735 &RequestType::ActivateListener(ActivateListener {
11736 address: addr,
11737 proxy: ListenerType::Http.into(),
11738 from_scm: false,
11739 interface: Some("wg0".to_owned()),
11740 })
11741 .into(),
11742 )
11743 .expect("activating the wg0 listener");
11744 let list = state.list_listeners();
11745 assert!(list.http_listeners["0.0.0.0:8443%wg0"].active);
11746 assert!(!list.http_listeners["0.0.0.0:8443%lo"].active);
11747 assert!(!list.http_listeners["0.0.0.0:8443"].active);
11748
11749 state
11750 .dispatch(
11751 &RequestType::RemoveListener(RemoveListener {
11752 address: addr,
11753 proxy: ListenerType::Tcp.into(),
11754 interface: Some("lo".to_owned()),
11755 })
11756 .into(),
11757 )
11758 .expect("removing the lo TCP listener");
11759 let list = state.list_listeners();
11760 assert_eq!(
11761 list.tcp_listeners.keys().cloned().collect::<Vec<_>>(),
11762 vec!["0.0.0.0:8443".to_owned(), "0.0.0.0:8443%wg0".to_owned()]
11763 );
11764
11765 assert!(
11768 state
11769 .dispatch(
11770 &RequestType::RemoveListener(RemoveListener {
11771 address: addr,
11772 proxy: ListenerType::Tcp.into(),
11773 interface: Some("eth9".to_owned()),
11774 })
11775 .into(),
11776 )
11777 .is_err()
11778 );
11779
11780 let mut replayed = ConfigState::new();
11783 for request in state.generate_requests() {
11784 replayed
11785 .dispatch(&request)
11786 .expect("replaying generated requests");
11787 }
11788 assert_eq!(replayed.list_listeners(), state.list_listeners());
11789 assert!(state.diff(&replayed).is_empty());
11790 }
11791
11792 #[test]
11796 fn listener_diff_distinguishes_interfaces() {
11797 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11798 let mut before = ConfigState::new();
11799 before
11800 .dispatch(
11801 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11802 l.interface = Some("eth0".to_owned())
11803 }))
11804 .into(),
11805 )
11806 .unwrap();
11807 let mut after = ConfigState::new();
11808 after
11809 .dispatch(
11810 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11811 l.interface = Some("wg0".to_owned())
11812 }))
11813 .into(),
11814 )
11815 .unwrap();
11816
11817 let diff = before.diff(&after);
11818 let mut removed = Vec::new();
11819 let mut added = Vec::new();
11820 for request in &diff {
11821 match &request.request_type {
11822 Some(RequestType::RemoveListener(remove)) => removed.push(remove.interface.clone()),
11823 Some(RequestType::AddHttpListener(add)) => added.push(add.interface.clone()),
11824 _ => {}
11825 }
11826 }
11827 assert_eq!(removed, vec![Some("eth0".to_owned())]);
11828 assert_eq!(added, vec![Some("wg0".to_owned())]);
11829 }
11830
11831 #[test]
11837 fn reload_adding_an_interface_listener_sends_only_that_listener() {
11838 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11839 let front = RequestHttpFrontend {
11840 cluster_id: Some("cluster_1".to_owned()),
11841 hostname: "example.com".to_owned(),
11842 path: PathRule::prefix("/".to_owned()),
11843 address: addr,
11844 ..Default::default()
11845 };
11846 let mut before = ConfigState::new();
11847 for request in [
11848 RequestType::AddHttpListener(make_http_listener(addr)),
11849 RequestType::AddHttpFrontend(front),
11850 ] {
11851 before.dispatch(&request.into()).unwrap();
11852 }
11853 let mut after = before.clone();
11854 after
11855 .dispatch(
11856 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11857 l.interface = Some("wg0".to_owned())
11858 }))
11859 .into(),
11860 )
11861 .unwrap();
11862
11863 let diff = before.diff(&after);
11864 assert_eq!(diff.len(), 1, "{diff:?}");
11865 assert!(matches!(
11866 &diff[0].request_type,
11867 Some(RequestType::AddHttpListener(listener))
11868 if listener.interface.as_deref() == Some("wg0")
11869 ));
11870 }
11871
11872 #[test]
11876 fn update_listener_is_refused_when_the_address_is_ambiguous() {
11877 let addr = SocketAddress::new_v4(0, 0, 0, 0, 8080);
11878 let mut state = ConfigState::new();
11879 state
11880 .dispatch(
11881 &RequestType::AddHttpListener(with_interface(make_http_listener(addr), |l| {
11882 l.interface = Some("wg0".to_owned())
11883 }))
11884 .into(),
11885 )
11886 .unwrap();
11887
11888 state
11890 .dispatch(
11891 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
11892 address: addr,
11893 front_timeout: Some(15),
11894 ..Default::default()
11895 })
11896 .into(),
11897 )
11898 .expect("an unambiguous patch applies to the interface-bound listener");
11899 assert_eq!(
11900 state.list_listeners().http_listeners["0.0.0.0:8080%wg0"].front_timeout,
11901 15
11902 );
11903
11904 state
11905 .dispatch(&RequestType::AddHttpListener(make_http_listener(addr)).into())
11906 .unwrap();
11907 let before = state.list_listeners();
11908 let error = state
11909 .dispatch(
11910 &RequestType::UpdateHttpListener(UpdateHttpListenerConfig {
11911 address: addr,
11912 front_timeout: Some(20),
11913 ..Default::default()
11914 })
11915 .into(),
11916 )
11917 .expect_err("a patch matching two listeners must be refused");
11918 assert!(
11919 matches!(error, StateError::AmbiguousListenerAddress { count: 2, .. }),
11920 "{error}"
11921 );
11922 assert_eq!(
11923 before,
11924 state.list_listeners(),
11925 "a refused patch must leave the listeners untouched"
11926 );
11927 }
11928
11929 #[test]
11935 fn header_predicates_are_part_of_the_frontend_state_key() {
11936 use crate::proto::command::{HeaderMatchKind, HeaderPredicate};
11937
11938 let predicate = |name: &str, kind: HeaderMatchKind, value: Option<&str>| HeaderPredicate {
11939 name: name.to_owned(),
11940 kind: kind as i32,
11941 value: value.map(str::to_owned),
11942 };
11943 let base = RequestHttpFrontend {
11944 cluster_id: Some("stable".to_owned()),
11945 address: SocketAddress::new_v4(127, 0, 0, 1, 8080),
11946 hostname: "api.example.com".to_owned(),
11947 path: PathRule::prefix("/".to_owned()),
11948 ..Default::default()
11949 };
11950 let canary = RequestHttpFrontend {
11951 cluster_id: Some("canary".to_owned()),
11952 header_predicates: vec![
11953 predicate("X-Canary", HeaderMatchKind::Exact, Some("always")),
11954 predicate("X-Beta", HeaderMatchKind::Present, None),
11955 ],
11956 ..base.clone()
11957 };
11958 let canary_respelled = RequestHttpFrontend {
11959 header_predicates: vec![
11960 predicate("x-beta", HeaderMatchKind::Present, None),
11961 predicate("x-canary", HeaderMatchKind::Exact, Some("always")),
11962 ],
11963 ..canary.clone()
11964 };
11965
11966 assert_eq!(
11967 base.to_string(),
11968 "127.0.0.1:8080;api.example.com;P/",
11969 "a frontend without predicates keeps its historical key"
11970 );
11971 assert_eq!(canary.to_string(), canary_respelled.to_string());
11972
11973 let single = RequestHttpFrontend {
11976 header_predicates: vec![predicate("X-Beta", HeaderMatchKind::Present, None)],
11977 ..base.clone()
11978 };
11979 let repeated = RequestHttpFrontend {
11980 header_predicates: vec![
11981 predicate("X-Beta", HeaderMatchKind::Present, None),
11982 predicate("x-beta", HeaderMatchKind::Present, None),
11983 ],
11984 ..base.clone()
11985 };
11986 assert_eq!(single.to_string(), repeated.to_string());
11987
11988 let mut state = ConfigState::default();
11989 state.add_http_frontend(&base).expect("base");
11990 state
11991 .add_http_frontend(&canary)
11992 .expect("a canary must coexist with its base");
11993 assert!(matches!(
11994 state.add_http_frontend(&canary_respelled),
11995 Err(StateError::Exists { .. })
11996 ));
11997 assert_eq!(state.http_fronts.len(), 2);
11998
11999 let mut replayed = ConfigState::default();
12002 for request in state.generate_requests() {
12003 replayed.dispatch(&request).expect("replay must succeed");
12004 }
12005 assert_eq!(replayed.http_fronts, state.http_fronts);
12006
12007 let replay_types: Vec<_> = state
12011 .generate_requests()
12012 .into_iter()
12013 .filter_map(|request| request.request_type)
12014 .collect();
12015 assert!(replay_types.contains(&RequestType::AddHttpFrontend(base.clone())));
12016 assert!(
12017 replay_types.contains(&RequestType::AddHttpFrontendWithHeaderPredicates(
12018 canary.clone()
12019 ))
12020 );
12021 assert!(!replay_types.contains(&RequestType::AddHttpFrontend(canary.clone())));
12022
12023 for add in [
12026 RequestType::AddHttpFrontend(canary.clone()),
12027 RequestType::AddHttpFrontendWithHeaderPredicates(canary.clone()),
12028 ] {
12029 assert_eq!(
12030 rollback_request(&add).and_then(|request| request.request_type),
12031 Some(RequestType::RemoveHttpFrontendWithHeaderPredicates(
12032 canary.clone()
12033 ))
12034 );
12035 }
12036 assert_eq!(
12037 rollback_request(&RequestType::AddHttpFrontend(base.clone()))
12038 .and_then(|request| request.request_type),
12039 Some(RequestType::RemoveHttpFrontend(base.clone()))
12040 );
12041
12042 state
12043 .remove_http_frontend(&canary_respelled)
12044 .expect("a respelled removal must find the canary");
12045 assert_eq!(
12046 state
12047 .http_fronts
12048 .values()
12049 .map(|front| front.cluster_id.as_deref())
12050 .collect::<Vec<_>>(),
12051 vec![Some("stable")],
12052 "removing the canary must leave its base"
12053 );
12054 }
12055
12056 #[test]
12061 fn close_sessions_is_never_saved_nor_replayed() {
12062 let mut state: ConfigState = Default::default();
12063 state
12064 .dispatch(
12065 &RequestType::AddCluster(Cluster {
12066 cluster_id: String::from("cluster_1"),
12067 ..Default::default()
12068 })
12069 .into(),
12070 )
12071 .expect("Could not execute request");
12072 state
12073 .dispatch(
12074 &RequestType::AddBackend(AddBackend {
12075 cluster_id: String::from("cluster_1"),
12076 backend_id: String::from("cluster_1-0"),
12077 address: SocketAddress::new_v4(127, 0, 0, 1, 1026),
12078 ..Default::default()
12079 })
12080 .into(),
12081 )
12082 .expect("Could not execute request");
12083 let requests_before = state.generate_requests();
12084 let hashes_before = state.hash_state();
12085
12086 state
12087 .dispatch(
12088 &RequestType::CloseSessions(CloseSessions {
12089 cluster_id: String::from("cluster_1"),
12090 backend_id: Some(String::from("cluster_1-0")),
12091 address: Some(SocketAddress::new_v4(127, 0, 0, 1, 1026)),
12092 })
12093 .into(),
12094 )
12095 .expect("CloseSessions must be accepted by the state");
12096
12097 let requests_after = state.generate_requests();
12098 assert_eq!(requests_after, requests_before);
12099 assert!(
12100 !requests_after
12101 .iter()
12102 .any(|request| matches!(request.request_type, Some(RequestType::CloseSessions(_))))
12103 );
12104 assert_eq!(state.hash_state(), hashes_before);
12105 }
12106}